Compare commits
15 Commits
Author | SHA1 | Date | |
---|---|---|---|
1dcaccdb6d | |||
35eb410051 | |||
10c43ecd59 | |||
9df4a09414 | |||
7cbc941407 | |||
b31f306106 | |||
1dbbac450c | |||
b5a2bd7436 | |||
931a760ee1 | |||
27414e0284 | |||
d63bc762d0 | |||
505e40a57f | |||
d1ea10d8c6 | |||
1038759d8b | |||
ab9b545c9a |
@ -1,5 +1,14 @@
|
|||||||
# Changelog
|
# Changelog
|
||||||
|
|
||||||
|
## 2025-06-26 - 1.9.8 - fix(deps, windowlayer)
|
||||||
|
Update dependency versions and adjust dees-windowlayer CSS to add pointer-events fix
|
||||||
|
|
||||||
|
- Bump @design.estate/dees-wcctools from ^1.0.98 to ^1.0.101
|
||||||
|
- Bump @tiptap packages from 2.22.3 to 2.23.0
|
||||||
|
- Bump lucide from ^0.522.0 to ^0.523.0
|
||||||
|
- Bump @git.zone/tsbundle from ^2.4.0 to ^2.5.1 and tswatch from ^2.0.37 to ^2.1.2
|
||||||
|
- Add 'pointer-events: none' to dees-windowlayer CSS to improve overlay behavior
|
||||||
|
|
||||||
## 2025-06-22 - 1.9.0 - feat(form-inputs)
|
## 2025-06-22 - 1.9.0 - feat(form-inputs)
|
||||||
Improve form input consistency and auto spacing across inputs and buttons
|
Improve form input consistency and auto spacing across inputs and buttons
|
||||||
|
|
||||||
|
24
package.json
24
package.json
@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "@design.estate/dees-catalog",
|
"name": "@design.estate/dees-catalog",
|
||||||
"version": "1.9.3",
|
"version": "1.9.8",
|
||||||
"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",
|
||||||
@ -8,7 +8,7 @@
|
|||||||
"type": "module",
|
"type": "module",
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"test": "tstest test/ --web --verbose --timeout 30",
|
"test": "tstest test/ --web --verbose --timeout 30",
|
||||||
"build": "tsbuild tsfolders --allowimplicitany && tsbundle element --production",
|
"build": "tsbuild tsfolders --allowimplicitany && tsbundle element --production --bundler esbuild",
|
||||||
"watch": "tswatch element",
|
"watch": "tswatch element",
|
||||||
"buildDocs": "tsdoc"
|
"buildDocs": "tsdoc"
|
||||||
},
|
},
|
||||||
@ -17,7 +17,7 @@
|
|||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@design.estate/dees-domtools": "^2.3.3",
|
"@design.estate/dees-domtools": "^2.3.3",
|
||||||
"@design.estate/dees-element": "^2.0.45",
|
"@design.estate/dees-element": "^2.0.45",
|
||||||
"@design.estate/dees-wcctools": "^1.0.98",
|
"@design.estate/dees-wcctools": "^1.0.101",
|
||||||
"@fortawesome/fontawesome-svg-core": "^6.7.2",
|
"@fortawesome/fontawesome-svg-core": "^6.7.2",
|
||||||
"@fortawesome/free-brands-svg-icons": "^6.7.2",
|
"@fortawesome/free-brands-svg-icons": "^6.7.2",
|
||||||
"@fortawesome/free-regular-svg-icons": "^6.7.2",
|
"@fortawesome/free-regular-svg-icons": "^6.7.2",
|
||||||
@ -25,18 +25,18 @@
|
|||||||
"@push.rocks/smarti18n": "^1.0.4",
|
"@push.rocks/smarti18n": "^1.0.4",
|
||||||
"@push.rocks/smartpromise": "^4.2.0",
|
"@push.rocks/smartpromise": "^4.2.0",
|
||||||
"@push.rocks/smartstring": "^4.0.15",
|
"@push.rocks/smartstring": "^4.0.15",
|
||||||
"@tiptap/core": "^2.22.3",
|
"@tiptap/core": "^2.23.0",
|
||||||
"@tiptap/extension-link": "^2.22.3",
|
"@tiptap/extension-link": "^2.23.0",
|
||||||
"@tiptap/extension-text-align": "^2.22.3",
|
"@tiptap/extension-text-align": "^2.23.0",
|
||||||
"@tiptap/extension-typography": "^2.22.3",
|
"@tiptap/extension-typography": "^2.23.0",
|
||||||
"@tiptap/extension-underline": "^2.22.3",
|
"@tiptap/extension-underline": "^2.23.0",
|
||||||
"@tiptap/starter-kit": "^2.22.3",
|
"@tiptap/starter-kit": "^2.23.0",
|
||||||
"@tsclass/tsclass": "^9.2.0",
|
"@tsclass/tsclass": "^9.2.0",
|
||||||
"@webcontainer/api": "1.2.0",
|
"@webcontainer/api": "1.2.0",
|
||||||
"apexcharts": "^4.7.0",
|
"apexcharts": "^4.7.0",
|
||||||
"highlight.js": "11.11.1",
|
"highlight.js": "11.11.1",
|
||||||
"ibantools": "^4.5.1",
|
"ibantools": "^4.5.1",
|
||||||
"lucide": "^0.522.0",
|
"lucide": "^0.523.0",
|
||||||
"monaco-editor": "^0.52.2",
|
"monaco-editor": "^0.52.2",
|
||||||
"pdfjs-dist": "^4.10.38",
|
"pdfjs-dist": "^4.10.38",
|
||||||
"xterm": "^5.3.0",
|
"xterm": "^5.3.0",
|
||||||
@ -44,9 +44,9 @@
|
|||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@git.zone/tsbuild": "^2.6.4",
|
"@git.zone/tsbuild": "^2.6.4",
|
||||||
"@git.zone/tsbundle": "^2.4.0",
|
"@git.zone/tsbundle": "^2.5.1",
|
||||||
"@git.zone/tstest": "^2.3.1",
|
"@git.zone/tstest": "^2.3.1",
|
||||||
"@git.zone/tswatch": "^2.0.37",
|
"@git.zone/tswatch": "^2.1.2",
|
||||||
"@push.rocks/projectinfo": "^5.0.2",
|
"@push.rocks/projectinfo": "^5.0.2",
|
||||||
"@push.rocks/tapbundle": "^6.0.3",
|
"@push.rocks/tapbundle": "^6.0.3",
|
||||||
"@types/node": "^22.0.0"
|
"@types/node": "^22.0.0"
|
||||||
|
711
pnpm-lock.yaml
generated
711
pnpm-lock.yaml
generated
File diff suppressed because it is too large
Load Diff
@ -513,4 +513,95 @@ Completed comprehensive refactoring to ensure clean, maintainable code with sepa
|
|||||||
The refactoring follows the principles in instructions.md:
|
The refactoring follows the principles in instructions.md:
|
||||||
- Uses static templates with manual DOM operations
|
- Uses static templates with manual DOM operations
|
||||||
- Maintains separated concerns in different classes
|
- Maintains separated concerns in different classes
|
||||||
- Results in clean, concise, and manageable code
|
- Results in clean, concise, and manageable code
|
||||||
|
|
||||||
|
## Z-Index Management System (2025-12-24)
|
||||||
|
|
||||||
|
A comprehensive z-index management system has been implemented to fix overlay stacking conflicts:
|
||||||
|
|
||||||
|
### The Problem:
|
||||||
|
- Modals were hiding dropdown overlays
|
||||||
|
- Context menus appeared behind modals
|
||||||
|
- Inconsistent z-index values across components
|
||||||
|
- No clear hierarchy for overlay stacking
|
||||||
|
|
||||||
|
### The Solution:
|
||||||
|
|
||||||
|
#### 1. Central Z-Index Constants (`00zindex.ts`):
|
||||||
|
Created a centralized file defining all z-index layers:
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
export const zIndexLayers = {
|
||||||
|
// Base layer: Regular content
|
||||||
|
base: {
|
||||||
|
content: 'auto',
|
||||||
|
inputElements: 1,
|
||||||
|
},
|
||||||
|
// Fixed UI elements
|
||||||
|
fixed: {
|
||||||
|
appBar: 10,
|
||||||
|
sideMenu: 10,
|
||||||
|
mobileNav: 250,
|
||||||
|
},
|
||||||
|
// Overlay backdrops
|
||||||
|
backdrop: {
|
||||||
|
dropdown: 1999,
|
||||||
|
modal: 2999,
|
||||||
|
contextMenu: 3999,
|
||||||
|
},
|
||||||
|
// Interactive overlays
|
||||||
|
overlay: {
|
||||||
|
dropdown: 2000, // Dropdowns and select menus
|
||||||
|
modal: 3000, // Modal dialogs
|
||||||
|
contextMenu: 4000, // Context menus and tooltips
|
||||||
|
toast: 5000, // Toast notifications
|
||||||
|
},
|
||||||
|
// Special cases
|
||||||
|
modalDropdown: 3500, // Dropdowns inside modals
|
||||||
|
wysiwygMenus: 4500, // Editor formatting menus
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
#### 2. Updated Components:
|
||||||
|
- **dees-modal**: Changed from 2000 to 3000
|
||||||
|
- **dees-windowlayer**: Changed from 200-201 to 1999-2000 (used by dropdowns)
|
||||||
|
- **dees-contextmenu**: Changed from 10000 to 4000
|
||||||
|
- **dees-toast**: Changed from 10000 to 5000
|
||||||
|
- **wysiwyg menus**: Changed from 10000 to 4500
|
||||||
|
- **dees-appui-profiledropdown**: Uses new dropdown z-index (2000)
|
||||||
|
|
||||||
|
#### 3. Stacking Order (bottom to top):
|
||||||
|
1. Regular page content (auto)
|
||||||
|
2. Fixed navigation elements (10-250)
|
||||||
|
3. Dropdown backdrop (1999)
|
||||||
|
4. Dropdown content (2000)
|
||||||
|
5. Modal backdrop (2999)
|
||||||
|
6. Modal content (3000)
|
||||||
|
7. Context menu (4000)
|
||||||
|
8. WYSIWYG menus (4500)
|
||||||
|
9. Toast notifications (5000)
|
||||||
|
|
||||||
|
#### 4. Key Benefits:
|
||||||
|
- Dropdowns now appear above modals
|
||||||
|
- Context menus appear above dropdowns and modals
|
||||||
|
- Toast notifications always appear on top
|
||||||
|
- Consistent and predictable stacking behavior
|
||||||
|
- Easy to adjust hierarchy by modifying central constants
|
||||||
|
|
||||||
|
#### 5. Testing:
|
||||||
|
Created `test-zindex.demo.ts` to verify stacking behavior with:
|
||||||
|
- Modal containing dropdown
|
||||||
|
- Context menu on modal
|
||||||
|
- Toast notifications
|
||||||
|
- Complex overlay combinations
|
||||||
|
|
||||||
|
### Usage:
|
||||||
|
Import and use the z-index constants in any component:
|
||||||
|
```typescript
|
||||||
|
import { zIndexLayers } from './00zindex.js';
|
||||||
|
|
||||||
|
// In styles
|
||||||
|
z-index: ${zIndexLayers.overlay.modal};
|
||||||
|
```
|
||||||
|
|
||||||
|
This system ensures proper stacking order for all overlay components and prevents z-index conflicts.
|
471
readme.md
471
readme.md
@ -12,14 +12,15 @@ npm install @design.estate/dees-catalog
|
|||||||
|
|
||||||
| Category | Components |
|
| Category | Components |
|
||||||
|----------|------------|
|
|----------|------------|
|
||||||
| Core UI | `DeesButton`, `DeesBadge`, `DeesChips`, `DeesIcon`, `DeesLabel`, `DeesSpinner`, `DeesToast` |
|
| Core UI | `DeesButton`, `DeesButtonExit`, `DeesButtonGroup`, `DeesBadge`, `DeesChips`, `DeesHeading`, `DeesHint`, `DeesIcon`, `DeesLabel`, `DeesPanel`, `DeesSearchbar`, `DeesSpinner`, `DeesToast`, `DeesWindowcontrols` |
|
||||||
| Forms | `DeesForm`, `DeesInputText`, `DeesInputCheckbox`, `DeesInputDropdown`, `DeesInputRadio`, `DeesInputFileupload`, `DeesInputIban`, `DeesInputPhone`, `DeesInputQuantitySelector`, `DeesInputMultitoggle`, `DeesFormSubmit` |
|
| Forms | `DeesForm`, `DeesInputText`, `DeesInputCheckbox`, `DeesInputDropdown`, `DeesInputRadiogroup`, `DeesInputFileupload`, `DeesInputIban`, `DeesInputPhone`, `DeesInputQuantitySelector`, `DeesInputMultitoggle`, `DeesInputTags`, `DeesInputTypelist`, `DeesInputRichtext`, `DeesInputWysiwyg`, `DeesFormSubmit` |
|
||||||
| Layout | `DeesAppuiBase`, `DeesAppuiMainmenu`, `DeesAppuiMainselector`, `DeesAppuiMaincontent`, `DeesAppuiAppbar`, `DeesMobileNavigation` |
|
| Layout | `DeesAppuiBase`, `DeesAppuiMainmenu`, `DeesAppuiMainselector`, `DeesAppuiMaincontent`, `DeesAppuiAppbar`, `DeesAppuiActivitylog`, `DeesAppuiProfiledropdown`, `DeesAppuiTabs`, `DeesAppuiView`, `DeesMobileNavigation` |
|
||||||
| Data Display | `DeesTable`, `DeesDataviewCodebox`, `DeesDataviewStatusobject`, `DeesPdf`, `DeesStatsGrid` |
|
| Data Display | `DeesTable`, `DeesDataviewCodebox`, `DeesDataviewStatusobject`, `DeesPdf`, `DeesStatsGrid`, `DeesPagination` |
|
||||||
| Visualization | `DeesChartArea`, `DeesChartLog` |
|
| Visualization | `DeesChartArea`, `DeesChartLog` |
|
||||||
| Dialogs & Overlays | `DeesModal`, `DeesContextmenu`, `DeesSpeechbubble`, `DeesWindowlayer` |
|
| Dialogs & Overlays | `DeesModal`, `DeesContextmenu`, `DeesSpeechbubble`, `DeesWindowlayer` |
|
||||||
| Navigation | `DeesStepper`, `DeesProgressbar` |
|
| Navigation | `DeesStepper`, `DeesProgressbar`, `DeesMobileNavigation` |
|
||||||
| Development | `DeesEditor`, `DeesEditorMarkdown`, `DeesTerminal`, `DeesUpdater` |
|
| Development | `DeesEditor`, `DeesEditorMarkdown`, `DeesEditorMarkdownoutlet`, `DeesTerminal`, `DeesUpdater` |
|
||||||
|
| Auth & Utilities | `DeesSimpleAppdash`, `DeesSimpleLogin` |
|
||||||
|
|
||||||
## Detailed Component Documentation
|
## Detailed Component Documentation
|
||||||
|
|
||||||
@ -149,6 +150,93 @@ Key Features:
|
|||||||
- Theme-aware styling
|
- Theme-aware styling
|
||||||
- Programmatic control
|
- Programmatic control
|
||||||
|
|
||||||
|
#### `DeesButtonExit`
|
||||||
|
Exit/close button component with consistent styling.
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
<dees-button-exit
|
||||||
|
@click=${handleClose}
|
||||||
|
></dees-button-exit>
|
||||||
|
```
|
||||||
|
|
||||||
|
#### `DeesButtonGroup`
|
||||||
|
Container for grouping related buttons together.
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
<dees-button-group
|
||||||
|
.buttons=${[
|
||||||
|
{ text: 'Save', type: 'highlighted', action: handleSave },
|
||||||
|
{ text: 'Cancel', type: 'normal', action: handleCancel }
|
||||||
|
]}
|
||||||
|
spacing="medium" // Options: small, medium, large
|
||||||
|
></dees-button-group>
|
||||||
|
```
|
||||||
|
|
||||||
|
#### `DeesHeading`
|
||||||
|
Consistent heading component with level and styling options.
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
<dees-heading
|
||||||
|
level={1} // 1-6 for H1-H6
|
||||||
|
text="Page Title"
|
||||||
|
.subheading=${'Optional subtitle'}
|
||||||
|
centered // Optional: center alignment
|
||||||
|
></dees-heading>
|
||||||
|
```
|
||||||
|
|
||||||
|
#### `DeesHint`
|
||||||
|
Hint/tooltip component for providing contextual help.
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
<dees-hint
|
||||||
|
text="This field is required"
|
||||||
|
type="info" // Options: info, warning, error, success
|
||||||
|
position="top" // Options: top, bottom, left, right
|
||||||
|
></dees-hint>
|
||||||
|
```
|
||||||
|
|
||||||
|
#### `DeesPanel`
|
||||||
|
Container component for grouping related content with optional title and actions.
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
<dees-panel
|
||||||
|
.title=${'Panel Title'}
|
||||||
|
.subtitle=${'Optional subtitle'}
|
||||||
|
collapsible // Optional: allow collapse/expand
|
||||||
|
collapsed={false} // Initial collapsed state
|
||||||
|
.actions=${[
|
||||||
|
{ icon: 'settings', action: handleSettings }
|
||||||
|
]}
|
||||||
|
>
|
||||||
|
<!-- Panel content -->
|
||||||
|
</dees-panel>
|
||||||
|
```
|
||||||
|
|
||||||
|
#### `DeesSearchbar`
|
||||||
|
Search input component with suggestions and search handling.
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
<dees-searchbar
|
||||||
|
placeholder="Search..."
|
||||||
|
.suggestions=${['item1', 'item2', 'item3']}
|
||||||
|
showClearButton // Show clear button when has value
|
||||||
|
@search=${handleSearch}
|
||||||
|
@suggestion-select=${handleSuggestionSelect}
|
||||||
|
></dees-searchbar>
|
||||||
|
```
|
||||||
|
|
||||||
|
#### `DeesWindowcontrols`
|
||||||
|
Window control buttons (minimize, maximize, close) for desktop-like applications.
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
<dees-windowcontrols
|
||||||
|
.controls=${['minimize', 'maximize', 'close']}
|
||||||
|
@minimize=${handleMinimize}
|
||||||
|
@maximize=${handleMaximize}
|
||||||
|
@close=${handleClose}
|
||||||
|
></dees-windowcontrols>
|
||||||
|
```
|
||||||
|
|
||||||
### Form Components
|
### Form Components
|
||||||
|
|
||||||
#### `DeesForm`
|
#### `DeesForm`
|
||||||
@ -207,22 +295,6 @@ Dropdown selection component with search and filtering capabilities.
|
|||||||
></dees-input-dropdown>
|
></dees-input-dropdown>
|
||||||
```
|
```
|
||||||
|
|
||||||
#### `DeesInputRadio`
|
|
||||||
Radio button group for single-choice selections.
|
|
||||||
|
|
||||||
```typescript
|
|
||||||
<dees-input-radio
|
|
||||||
key="gender"
|
|
||||||
label="Gender"
|
|
||||||
.options=${[
|
|
||||||
{ key: 'male', option: 'Male' },
|
|
||||||
{ key: 'female', option: 'Female' },
|
|
||||||
{ key: 'other', option: 'Other' }
|
|
||||||
]}
|
|
||||||
required
|
|
||||||
></dees-input-radio>
|
|
||||||
```
|
|
||||||
|
|
||||||
#### `DeesInputFileupload`
|
#### `DeesInputFileupload`
|
||||||
File upload component with drag-and-drop support.
|
File upload component with drag-and-drop support.
|
||||||
|
|
||||||
@ -293,6 +365,121 @@ Multi-state toggle button group.
|
|||||||
></dees-input-multitoggle>
|
></dees-input-multitoggle>
|
||||||
```
|
```
|
||||||
|
|
||||||
|
#### `DeesInputRadiogroup`
|
||||||
|
Radio button group for single-choice selections with internal state management.
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
<dees-input-radiogroup
|
||||||
|
key="plan"
|
||||||
|
label="Select Plan"
|
||||||
|
.options=${['Free', 'Pro', 'Enterprise']}
|
||||||
|
selectedOption="Pro"
|
||||||
|
required
|
||||||
|
@change=${handlePlanChange}
|
||||||
|
></dees-input-radiogroup>
|
||||||
|
|
||||||
|
// With custom option objects
|
||||||
|
<dees-input-radiogroup
|
||||||
|
key="priority"
|
||||||
|
label="Priority Level"
|
||||||
|
.options=${[
|
||||||
|
{ key: 'low', label: 'Low Priority' },
|
||||||
|
{ key: 'medium', label: 'Medium Priority' },
|
||||||
|
{ key: 'high', label: 'High Priority' }
|
||||||
|
]}
|
||||||
|
selectedOption="medium"
|
||||||
|
></dees-input-radiogroup>
|
||||||
|
```
|
||||||
|
|
||||||
|
#### `DeesInputTags`
|
||||||
|
Tag input component for managing lists of tags with auto-complete and validation.
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
<dees-input-tags
|
||||||
|
key="skills"
|
||||||
|
label="Skills"
|
||||||
|
.value=${['JavaScript', 'TypeScript', 'CSS']}
|
||||||
|
placeholder="Add a skill..."
|
||||||
|
.suggestions=${[
|
||||||
|
'JavaScript', 'TypeScript', 'Python', 'Go', 'Rust',
|
||||||
|
'React', 'Vue', 'Angular', 'Node.js', 'Docker'
|
||||||
|
]}
|
||||||
|
maxTags={10} // Optional: limit number of tags
|
||||||
|
required
|
||||||
|
@change=${handleTagsChange}
|
||||||
|
></dees-input-tags>
|
||||||
|
```
|
||||||
|
|
||||||
|
Key Features:
|
||||||
|
- Add tags by pressing Enter or typing comma/semicolon
|
||||||
|
- Remove tags with click or backspace
|
||||||
|
- Auto-complete suggestions with keyboard navigation
|
||||||
|
- Maximum tag limit support
|
||||||
|
- Full theme support
|
||||||
|
- Form validation integration
|
||||||
|
|
||||||
|
#### `DeesInputTypelist`
|
||||||
|
Dynamic list input for managing arrays of typed values.
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
<dees-input-typelist
|
||||||
|
key="features"
|
||||||
|
label="Product Features"
|
||||||
|
placeholder="Add a feature..."
|
||||||
|
.value=${['Feature 1', 'Feature 2']}
|
||||||
|
@change=${handleFeaturesChange}
|
||||||
|
></dees-input-typelist>
|
||||||
|
```
|
||||||
|
|
||||||
|
#### `DeesInputRichtext`
|
||||||
|
Rich text editor with formatting toolbar powered by TipTap.
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
<dees-input-richtext
|
||||||
|
key="content"
|
||||||
|
label="Article Content"
|
||||||
|
.value=${htmlContent}
|
||||||
|
placeholder="Start writing..."
|
||||||
|
minHeight={300} // Minimum editor height
|
||||||
|
showWordCount={true} // Show word/character count
|
||||||
|
@change=${handleContentChange}
|
||||||
|
></dees-input-richtext>
|
||||||
|
```
|
||||||
|
|
||||||
|
Key Features:
|
||||||
|
- Full formatting toolbar (bold, italic, underline, strike, etc.)
|
||||||
|
- Heading levels (H1-H6)
|
||||||
|
- Lists (bullet, ordered)
|
||||||
|
- Links with URL editing
|
||||||
|
- Code blocks and inline code
|
||||||
|
- Blockquotes
|
||||||
|
- Horizontal rules
|
||||||
|
- Undo/redo support
|
||||||
|
- Word and character count
|
||||||
|
- HTML output
|
||||||
|
|
||||||
|
#### `DeesInputWysiwyg`
|
||||||
|
Advanced block-based editor with slash commands and rich content blocks.
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
<dees-input-wysiwyg
|
||||||
|
key="document"
|
||||||
|
label="Document Editor"
|
||||||
|
.value=${documentContent}
|
||||||
|
outputFormat="html" // Options: html, markdown, json
|
||||||
|
@change=${handleDocumentChange}
|
||||||
|
></dees-input-wysiwyg>
|
||||||
|
```
|
||||||
|
|
||||||
|
Key Features:
|
||||||
|
- Slash commands for quick formatting
|
||||||
|
- Block-based editing (paragraphs, headings, lists, etc.)
|
||||||
|
- Drag and drop block reordering
|
||||||
|
- Multiple output formats
|
||||||
|
- Keyboard shortcuts
|
||||||
|
- Collaborative editing ready
|
||||||
|
- Extensible block types
|
||||||
|
|
||||||
#### `DeesFormSubmit`
|
#### `DeesFormSubmit`
|
||||||
Submit button component specifically designed for `DeesForm`.
|
Submit button component specifically designed for `DeesForm`.
|
||||||
|
|
||||||
@ -622,6 +809,91 @@ Best Practices:
|
|||||||
- Test with screen readers
|
- Test with screen readers
|
||||||
- Maintain focus management
|
- Maintain focus management
|
||||||
|
|
||||||
|
#### `DeesAppuiActivitylog`
|
||||||
|
Activity log component for displaying system events and user actions.
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
<dees-appui-activitylog
|
||||||
|
.entries=${[
|
||||||
|
{
|
||||||
|
timestamp: new Date(),
|
||||||
|
type: 'info',
|
||||||
|
message: 'User logged in',
|
||||||
|
details: { userId: '123' }
|
||||||
|
},
|
||||||
|
{
|
||||||
|
timestamp: new Date(),
|
||||||
|
type: 'error',
|
||||||
|
message: 'Failed to save document',
|
||||||
|
details: { error: 'Network error' }
|
||||||
|
}
|
||||||
|
]}
|
||||||
|
maxEntries={100} // Maximum entries to display
|
||||||
|
@entry-click=${handleEntryClick}
|
||||||
|
></dees-appui-activitylog>
|
||||||
|
```
|
||||||
|
|
||||||
|
#### `DeesAppuiProfiledropdown`
|
||||||
|
User profile dropdown component with status and menu options.
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
<dees-appui-profiledropdown
|
||||||
|
.user=${{
|
||||||
|
name: 'John Doe',
|
||||||
|
email: 'john@example.com',
|
||||||
|
avatar: '/path/to/avatar.jpg',
|
||||||
|
status: 'online' // Options: online, offline, busy, away
|
||||||
|
}}
|
||||||
|
.menuItems=${[
|
||||||
|
{ name: 'Profile', iconName: 'user', action: async () => {} },
|
||||||
|
{ name: 'Settings', iconName: 'settings', action: async () => {} },
|
||||||
|
{ divider: true },
|
||||||
|
{ name: 'Logout', iconName: 'logOut', action: async () => {} }
|
||||||
|
]}
|
||||||
|
@status-change=${handleStatusChange}
|
||||||
|
></dees-appui-profiledropdown>
|
||||||
|
```
|
||||||
|
|
||||||
|
#### `DeesAppuiTabs`
|
||||||
|
Tab navigation component for organizing content sections.
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
<dees-appui-tabs
|
||||||
|
.tabs=${[
|
||||||
|
{
|
||||||
|
key: 'overview',
|
||||||
|
label: 'Overview',
|
||||||
|
icon: 'home',
|
||||||
|
content: html`<div>Overview content</div>`
|
||||||
|
},
|
||||||
|
{
|
||||||
|
key: 'details',
|
||||||
|
label: 'Details',
|
||||||
|
icon: 'info',
|
||||||
|
content: html`<div>Details content</div>`
|
||||||
|
}
|
||||||
|
]}
|
||||||
|
selectedTab="overview"
|
||||||
|
@tab-change=${handleTabChange}
|
||||||
|
></dees-appui-tabs>
|
||||||
|
```
|
||||||
|
|
||||||
|
#### `DeesAppuiView`
|
||||||
|
View container component for consistent page layouts.
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
<dees-appui-view
|
||||||
|
viewTitle="Dashboard"
|
||||||
|
viewSubtitle="System Overview"
|
||||||
|
.headerActions=${[
|
||||||
|
{ icon: 'refresh', action: handleRefresh },
|
||||||
|
{ icon: 'settings', action: handleSettings }
|
||||||
|
]}
|
||||||
|
>
|
||||||
|
<!-- View content -->
|
||||||
|
</dees-appui-view>
|
||||||
|
```
|
||||||
|
|
||||||
#### `DeesMobileNavigation`
|
#### `DeesMobileNavigation`
|
||||||
Responsive navigation component for mobile devices.
|
Responsive navigation component for mobile devices.
|
||||||
|
|
||||||
@ -982,6 +1254,27 @@ setInterval(() => {
|
|||||||
}, 3000);
|
}, 3000);
|
||||||
```
|
```
|
||||||
|
|
||||||
|
#### `DeesPagination`
|
||||||
|
Pagination component for navigating through large datasets.
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
<dees-pagination
|
||||||
|
totalItems={500}
|
||||||
|
itemsPerPage={20}
|
||||||
|
currentPage={1}
|
||||||
|
maxVisiblePages={7} // Maximum page numbers to display
|
||||||
|
@page-change=${handlePageChange}
|
||||||
|
></dees-pagination>
|
||||||
|
```
|
||||||
|
|
||||||
|
Key Features:
|
||||||
|
- Page number navigation
|
||||||
|
- Previous/next buttons
|
||||||
|
- Jump to first/last page
|
||||||
|
- Configurable items per page
|
||||||
|
- Responsive design
|
||||||
|
- Keyboard navigation support
|
||||||
|
|
||||||
### Visualization Components
|
### Visualization Components
|
||||||
|
|
||||||
#### `DeesChartArea`
|
#### `DeesChartArea`
|
||||||
@ -1306,52 +1599,6 @@ Key Features:
|
|||||||
- Animation support
|
- Animation support
|
||||||
- Accessibility features
|
- Accessibility features
|
||||||
|
|
||||||
#### `DeesMobileNavigation`
|
|
||||||
Mobile-optimized navigation component with touch support.
|
|
||||||
|
|
||||||
```typescript
|
|
||||||
// Programmatic usage
|
|
||||||
DeesMobilenavigation.createAndShow([
|
|
||||||
{
|
|
||||||
name: 'Home',
|
|
||||||
action: async (nav) => {
|
|
||||||
// Handle navigation
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: 'Settings',
|
|
||||||
action: async (nav) => {
|
|
||||||
// Handle navigation
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
]);
|
|
||||||
|
|
||||||
// Component usage
|
|
||||||
<dees-mobilenavigation
|
|
||||||
heading="MENU"
|
|
||||||
.menuItems=${[
|
|
||||||
{
|
|
||||||
name: 'Profile',
|
|
||||||
action: (nav) => handleNavigation('profile')
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: 'Settings',
|
|
||||||
action: (nav) => handleNavigation('settings')
|
|
||||||
}
|
|
||||||
]}
|
|
||||||
></dees-mobilenavigation>
|
|
||||||
```
|
|
||||||
|
|
||||||
Key Features:
|
|
||||||
- Touch-friendly interface
|
|
||||||
- Slide-in animation
|
|
||||||
- Backdrop overlay
|
|
||||||
- Single instance management
|
|
||||||
- Custom menu items
|
|
||||||
- Responsive design
|
|
||||||
|
|
||||||
Best Practices:
|
Best Practices:
|
||||||
|
|
||||||
1. Stepper Implementation
|
1. Stepper Implementation
|
||||||
@ -1368,13 +1615,6 @@ Best Practices:
|
|||||||
- Performance monitoring
|
- Performance monitoring
|
||||||
- Error state handling
|
- Error state handling
|
||||||
|
|
||||||
3. Mobile Navigation
|
|
||||||
- Touch-optimized targets
|
|
||||||
- Clear visual hierarchy
|
|
||||||
- Smooth animations
|
|
||||||
- Gesture support
|
|
||||||
- Responsive behavior
|
|
||||||
|
|
||||||
Common Use Cases:
|
Common Use Cases:
|
||||||
|
|
||||||
1. Stepper
|
1. Stepper
|
||||||
@ -1391,13 +1631,6 @@ Common Use Cases:
|
|||||||
- Task completion
|
- Task completion
|
||||||
- Step progression
|
- Step progression
|
||||||
|
|
||||||
3. Mobile Navigation
|
|
||||||
- Responsive menus
|
|
||||||
- App navigation
|
|
||||||
- Settings access
|
|
||||||
- User actions
|
|
||||||
- Context switching
|
|
||||||
|
|
||||||
Accessibility Considerations:
|
Accessibility Considerations:
|
||||||
- Keyboard navigation support
|
- Keyboard navigation support
|
||||||
- ARIA labels and roles
|
- ARIA labels and roles
|
||||||
@ -1461,6 +1694,26 @@ Key Features:
|
|||||||
- Spellcheck integration
|
- Spellcheck integration
|
||||||
- Auto-save functionality
|
- Auto-save functionality
|
||||||
|
|
||||||
|
#### `DeesEditorMarkdownoutlet`
|
||||||
|
Markdown preview component for rendering markdown content.
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
<dees-editor-markdownoutlet
|
||||||
|
.markdown=${markdownContent}
|
||||||
|
.theme=${'github'} // Options: github, dark, custom
|
||||||
|
.plugins=${['mermaid', 'highlight']} // Optional plugins
|
||||||
|
allowHtml={false} // Security: disable raw HTML
|
||||||
|
></dees-editor-markdownoutlet>
|
||||||
|
```
|
||||||
|
|
||||||
|
Key Features:
|
||||||
|
- Safe markdown rendering
|
||||||
|
- Multiple themes
|
||||||
|
- Plugin support (mermaid diagrams, syntax highlighting)
|
||||||
|
- XSS protection
|
||||||
|
- Custom CSS injection
|
||||||
|
- Responsive images
|
||||||
|
|
||||||
#### `DeesTerminal`
|
#### `DeesTerminal`
|
||||||
Terminal emulator component for command-line interface.
|
Terminal emulator component for command-line interface.
|
||||||
|
|
||||||
@ -1606,6 +1859,60 @@ Accessibility Features:
|
|||||||
- Focus management
|
- Focus management
|
||||||
- ARIA attributes
|
- ARIA attributes
|
||||||
|
|
||||||
|
### Auth & Utilities Components
|
||||||
|
|
||||||
|
#### `DeesSimpleAppdash`
|
||||||
|
Simple application dashboard component for quick prototyping.
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
<dees-simple-appdash
|
||||||
|
.appTitle=${'My Application'}
|
||||||
|
.menuItems=${[
|
||||||
|
{ name: 'Dashboard', icon: 'home', route: '/dashboard' },
|
||||||
|
{ name: 'Settings', icon: 'settings', route: '/settings' }
|
||||||
|
]}
|
||||||
|
.user=${{
|
||||||
|
name: 'John Doe',
|
||||||
|
role: 'Administrator'
|
||||||
|
}}
|
||||||
|
@menu-select=${handleMenuSelect}
|
||||||
|
>
|
||||||
|
<!-- Dashboard content -->
|
||||||
|
</dees-simple-appdash>
|
||||||
|
```
|
||||||
|
|
||||||
|
Key Features:
|
||||||
|
- Quick setup dashboard layout
|
||||||
|
- Built-in navigation
|
||||||
|
- User profile section
|
||||||
|
- Responsive design
|
||||||
|
- Minimal configuration
|
||||||
|
|
||||||
|
#### `DeesSimpleLogin`
|
||||||
|
Simple login form component with validation and customization.
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
<dees-simple-login
|
||||||
|
.appName=${'My Application'}
|
||||||
|
.logo=${'./assets/logo.png'}
|
||||||
|
.backgroundImage=${'./assets/background.jpg'}
|
||||||
|
.fields=${['username', 'password']} // Options: username, email, password
|
||||||
|
showForgotPassword
|
||||||
|
showRememberMe
|
||||||
|
@login=${handleLogin}
|
||||||
|
@forgot-password=${handleForgotPassword}
|
||||||
|
></dees-simple-login>
|
||||||
|
```
|
||||||
|
|
||||||
|
Key Features:
|
||||||
|
- Customizable fields
|
||||||
|
- Built-in validation
|
||||||
|
- Remember me option
|
||||||
|
- Forgot password link
|
||||||
|
- Custom branding
|
||||||
|
- Responsive layout
|
||||||
|
- Loading states
|
||||||
|
|
||||||
## License and Legal Information
|
## License and Legal Information
|
||||||
|
|
||||||
This repository contains open-source code that is licensed under the MIT License. A copy of the MIT License can be found in the license file within this repository.
|
This repository contains open-source code that is licensed under the MIT License. A copy of the MIT License can be found in the license file within this repository.
|
||||||
|
@ -3,6 +3,6 @@
|
|||||||
*/
|
*/
|
||||||
export const commitinfo = {
|
export const commitinfo = {
|
||||||
name: '@design.estate/dees-catalog',
|
name: '@design.estate/dees-catalog',
|
||||||
version: '1.9.0',
|
version: '1.9.8',
|
||||||
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.'
|
||||||
}
|
}
|
||||||
|
161
ts_web/elements/00zindex.ts
Normal file
161
ts_web/elements/00zindex.ts
Normal file
@ -0,0 +1,161 @@
|
|||||||
|
/**
|
||||||
|
* Central z-index management for consistent stacking order
|
||||||
|
* Higher numbers appear on top of lower numbers
|
||||||
|
*/
|
||||||
|
|
||||||
|
export const zIndexLayers = {
|
||||||
|
// Base layer: Regular content
|
||||||
|
base: {
|
||||||
|
content: 'auto',
|
||||||
|
inputElements: 1,
|
||||||
|
},
|
||||||
|
|
||||||
|
// Fixed UI elements
|
||||||
|
fixed: {
|
||||||
|
appBar: 10,
|
||||||
|
sideMenu: 10,
|
||||||
|
mobileNav: 250,
|
||||||
|
},
|
||||||
|
|
||||||
|
// Overlay backdrops (semi-transparent backgrounds)
|
||||||
|
backdrop: {
|
||||||
|
dropdown: 1999, // Below modals but above fixed elements
|
||||||
|
modal: 2999, // Below dropdowns on modals
|
||||||
|
contextMenu: 3999, // Below critical overlays
|
||||||
|
},
|
||||||
|
|
||||||
|
// Interactive overlays
|
||||||
|
overlay: {
|
||||||
|
dropdown: 2000, // Dropdowns and select menus
|
||||||
|
modal: 3000, // Modal dialogs
|
||||||
|
contextMenu: 4000, // Context menus and tooltips
|
||||||
|
toast: 5000, // Toast notifications (highest priority)
|
||||||
|
},
|
||||||
|
|
||||||
|
// Special cases for nested elements
|
||||||
|
modalDropdown: 3500, // Dropdowns inside modals
|
||||||
|
wysiwygMenus: 4500, // Editor formatting menus
|
||||||
|
} as const;
|
||||||
|
|
||||||
|
// Helper function to get z-index value
|
||||||
|
export function getZIndex(category: keyof typeof zIndexLayers, subcategory?: string): number | string {
|
||||||
|
const categoryObj = zIndexLayers[category];
|
||||||
|
if (typeof categoryObj === 'object' && subcategory) {
|
||||||
|
return categoryObj[subcategory as keyof typeof categoryObj] || 'auto';
|
||||||
|
}
|
||||||
|
return typeof categoryObj === 'number' ? categoryObj : 'auto';
|
||||||
|
}
|
||||||
|
|
||||||
|
// Z-index assignments for components
|
||||||
|
export const componentZIndex = {
|
||||||
|
'dees-modal': zIndexLayers.overlay.modal,
|
||||||
|
'dees-windowlayer': zIndexLayers.overlay.dropdown,
|
||||||
|
'dees-contextmenu': zIndexLayers.overlay.contextMenu,
|
||||||
|
'dees-toast': zIndexLayers.overlay.toast,
|
||||||
|
'dees-appui-mainmenu': zIndexLayers.fixed.appBar,
|
||||||
|
'dees-mobilenavigation': zIndexLayers.fixed.mobileNav,
|
||||||
|
'dees-slash-menu': zIndexLayers.wysiwygMenus,
|
||||||
|
'dees-formatting-menu': zIndexLayers.wysiwygMenus,
|
||||||
|
} as const;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Z-Index Registry for managing stacked elements
|
||||||
|
* Simple incremental z-index assignment based on creation order
|
||||||
|
*/
|
||||||
|
export class ZIndexRegistry {
|
||||||
|
private static instance: ZIndexRegistry;
|
||||||
|
private activeElements = new Set<HTMLElement>();
|
||||||
|
private elementZIndexMap = new WeakMap<HTMLElement, number>();
|
||||||
|
private currentZIndex = 1000; // Starting z-index
|
||||||
|
|
||||||
|
private constructor() {}
|
||||||
|
|
||||||
|
public static getInstance(): ZIndexRegistry {
|
||||||
|
if (!ZIndexRegistry.instance) {
|
||||||
|
ZIndexRegistry.instance = new ZIndexRegistry();
|
||||||
|
}
|
||||||
|
return ZIndexRegistry.instance;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get the next available z-index
|
||||||
|
* @returns The next available z-index
|
||||||
|
*/
|
||||||
|
public getNextZIndex(): number {
|
||||||
|
this.currentZIndex += 10;
|
||||||
|
return this.currentZIndex;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Register an element with the z-index registry
|
||||||
|
* @param element - The HTML element to register
|
||||||
|
* @param zIndex - The z-index assigned to this element
|
||||||
|
*/
|
||||||
|
public register(element: HTMLElement, zIndex: number): void {
|
||||||
|
this.activeElements.add(element);
|
||||||
|
this.elementZIndexMap.set(element, zIndex);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Unregister an element from the z-index registry
|
||||||
|
* @param element - The HTML element to unregister
|
||||||
|
*/
|
||||||
|
public unregister(element: HTMLElement): void {
|
||||||
|
this.activeElements.delete(element);
|
||||||
|
this.elementZIndexMap.delete(element);
|
||||||
|
|
||||||
|
// If no more active elements, reset counter to base
|
||||||
|
if (this.activeElements.size === 0) {
|
||||||
|
this.currentZIndex = 1000;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get the z-index for a specific element
|
||||||
|
* @param element - The HTML element
|
||||||
|
* @returns The z-index or undefined if not registered
|
||||||
|
*/
|
||||||
|
public getElementZIndex(element: HTMLElement): number | undefined {
|
||||||
|
return this.elementZIndexMap.get(element);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get count of active elements
|
||||||
|
* @returns Number of active elements
|
||||||
|
*/
|
||||||
|
public getActiveCount(): number {
|
||||||
|
return this.activeElements.size;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get the current highest z-index
|
||||||
|
* @returns The current z-index value
|
||||||
|
*/
|
||||||
|
public getCurrentZIndex(): number {
|
||||||
|
return this.currentZIndex;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Clear all registrations (useful for testing)
|
||||||
|
*/
|
||||||
|
public clear(): void {
|
||||||
|
this.activeElements.clear();
|
||||||
|
this.elementZIndexMap = new WeakMap();
|
||||||
|
this.currentZIndex = 1000;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get all active elements in z-index order
|
||||||
|
* @returns Array of elements sorted by z-index
|
||||||
|
*/
|
||||||
|
public getActiveElementsInOrder(): HTMLElement[] {
|
||||||
|
return Array.from(this.activeElements).sort((a, b) => {
|
||||||
|
const aZ = this.elementZIndexMap.get(a) || 0;
|
||||||
|
const bZ = this.elementZIndexMap.get(b) || 0;
|
||||||
|
return aZ - bZ;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Export singleton instance for convenience
|
||||||
|
export const zIndexRegistry = ZIndexRegistry.getInstance();
|
@ -1,5 +1,6 @@
|
|||||||
import * as plugins from './00plugins.js';
|
import * as plugins from './00plugins.js';
|
||||||
import * as interfaces from './interfaces/index.js';
|
import * as interfaces from './interfaces/index.js';
|
||||||
|
import { zIndexLayers } from './00zindex.js';
|
||||||
|
|
||||||
import {
|
import {
|
||||||
DeesElement,
|
DeesElement,
|
||||||
@ -46,7 +47,7 @@ export class DeesAppuiMainmenu extends DeesElement {
|
|||||||
.mainContainer {
|
.mainContainer {
|
||||||
--menuSize: 60px;
|
--menuSize: 60px;
|
||||||
color: ${cssManager.bdTheme('#666', '#ccc')};
|
color: ${cssManager.bdTheme('#666', '#ccc')};
|
||||||
z-index: 10;
|
z-index: ${zIndexLayers.fixed.appBar};
|
||||||
display: block;
|
display: block;
|
||||||
position: relative;
|
position: relative;
|
||||||
width: var(--menuSize);
|
width: var(--menuSize);
|
||||||
|
@ -1,4 +1,5 @@
|
|||||||
import * as plugins from './00plugins.js';
|
import * as plugins from './00plugins.js';
|
||||||
|
import { zIndexLayers } from './00zindex.js';
|
||||||
|
|
||||||
import {
|
import {
|
||||||
DeesElement,
|
DeesElement,
|
||||||
@ -73,7 +74,7 @@ export class DeesAppuiProfileDropdown extends DeesElement {
|
|||||||
'0 4px 12px rgba(0, 0, 0, 0.15)',
|
'0 4px 12px rgba(0, 0, 0, 0.15)',
|
||||||
'0 4px 12px rgba(0, 0, 0, 0.3)'
|
'0 4px 12px rgba(0, 0, 0, 0.3)'
|
||||||
)};
|
)};
|
||||||
z-index: 1000;
|
z-index: ${zIndexLayers.overlay.dropdown};
|
||||||
opacity: 0;
|
opacity: 0;
|
||||||
transform: scale(0.95) translateY(-10px);
|
transform: scale(0.95) translateY(-10px);
|
||||||
transition: opacity 0.2s, transform 0.2s;
|
transition: opacity 0.2s, transform 0.2s;
|
||||||
@ -258,7 +259,7 @@ export class DeesAppuiProfileDropdown extends DeesElement {
|
|||||||
right: 0;
|
right: 0;
|
||||||
bottom: 0;
|
bottom: 0;
|
||||||
background: rgba(0, 0, 0, 0.3);
|
background: rgba(0, 0, 0, 0.3);
|
||||||
z-index: 999;
|
z-index: ${zIndexLayers.backdrop.dropdown};
|
||||||
opacity: 0;
|
opacity: 0;
|
||||||
transition: opacity 0.2s;
|
transition: opacity 0.2s;
|
||||||
display: none;
|
display: none;
|
||||||
|
@ -14,6 +14,7 @@ import {
|
|||||||
|
|
||||||
import * as domtools from '@design.estate/dees-domtools';
|
import * as domtools from '@design.estate/dees-domtools';
|
||||||
import { DeesWindowLayer } from './dees-windowlayer.js';
|
import { DeesWindowLayer } from './dees-windowlayer.js';
|
||||||
|
import { zIndexLayers } from './00zindex.js';
|
||||||
import './dees-icon.js';
|
import './dees-icon.js';
|
||||||
|
|
||||||
declare global {
|
declare global {
|
||||||
@ -74,7 +75,7 @@ export class DeesContextmenu extends DeesElement {
|
|||||||
eventArg.stopPropagation();
|
eventArg.stopPropagation();
|
||||||
const contextMenu = new DeesContextmenu();
|
const contextMenu = new DeesContextmenu();
|
||||||
contextMenu.style.position = 'fixed';
|
contextMenu.style.position = 'fixed';
|
||||||
contextMenu.style.zIndex = '10000';
|
contextMenu.style.zIndex = String(zIndexLayers.overlay.contextMenu);
|
||||||
contextMenu.style.opacity = '0';
|
contextMenu.style.opacity = '0';
|
||||||
contextMenu.style.transform = 'scale(0.95) translateY(-10px)';
|
contextMenu.style.transform = 'scale(0.95) translateY(-10px)';
|
||||||
contextMenu.menuItems = menuItemsArg;
|
contextMenu.menuItems = menuItemsArg;
|
||||||
|
@ -1,5 +1,8 @@
|
|||||||
import { html, css } from '@design.estate/dees-element';
|
import { html, css } from '@design.estate/dees-element';
|
||||||
import '@design.estate/dees-wcctools/demotools';
|
import '@design.estate/dees-wcctools/demotools';
|
||||||
|
import './dees-panel.js';
|
||||||
|
import './dees-form.js';
|
||||||
|
import './dees-form-submit.js';
|
||||||
|
|
||||||
export const demoFunc = () => html`
|
export const demoFunc = () => html`
|
||||||
<dees-demowrapper>
|
<dees-demowrapper>
|
||||||
@ -14,37 +17,12 @@ export const demoFunc = () => html`
|
|||||||
margin: 0 auto;
|
margin: 0 auto;
|
||||||
}
|
}
|
||||||
|
|
||||||
.demo-section {
|
dees-panel {
|
||||||
background: #f8f9fa;
|
margin-bottom: 24px;
|
||||||
border-radius: 8px;
|
|
||||||
padding: 24px;
|
|
||||||
position: relative;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@media (prefers-color-scheme: dark) {
|
dees-panel:last-child {
|
||||||
.demo-section {
|
margin-bottom: 0;
|
||||||
background: #1a1a1a;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
.demo-section h3 {
|
|
||||||
margin-top: 0;
|
|
||||||
margin-bottom: 16px;
|
|
||||||
color: #0069f2;
|
|
||||||
font-size: 18px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.demo-section p {
|
|
||||||
margin-top: 0;
|
|
||||||
margin-bottom: 16px;
|
|
||||||
color: #666;
|
|
||||||
font-size: 14px;
|
|
||||||
}
|
|
||||||
|
|
||||||
@media (prefers-color-scheme: dark) {
|
|
||||||
.demo-section p {
|
|
||||||
color: #999;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.horizontal-group {
|
.horizontal-group {
|
||||||
@ -66,10 +44,7 @@ export const demoFunc = () => html`
|
|||||||
</style>
|
</style>
|
||||||
|
|
||||||
<div class="demo-container">
|
<div class="demo-container">
|
||||||
<div class="demo-section">
|
<dees-panel .title=${'1. Basic Dropdowns'} .subtitle=${'Standard dropdown with search functionality and various options'}>
|
||||||
<h3>Basic Dropdowns</h3>
|
|
||||||
<p>Standard dropdown with search functionality and various options</p>
|
|
||||||
|
|
||||||
<dees-input-dropdown
|
<dees-input-dropdown
|
||||||
.label=${'Select Country'}
|
.label=${'Select Country'}
|
||||||
.options=${[
|
.options=${[
|
||||||
@ -94,12 +69,9 @@ export const demoFunc = () => html`
|
|||||||
{ option: 'Guest', key: 'guest' }
|
{ option: 'Guest', key: 'guest' }
|
||||||
]}
|
]}
|
||||||
></dees-input-dropdown>
|
></dees-input-dropdown>
|
||||||
</div>
|
</dees-panel>
|
||||||
|
|
||||||
<div class="demo-section">
|
<dees-panel .title=${'2. Without Search'} .subtitle=${'Dropdown with search functionality disabled for simpler selection'}>
|
||||||
<h3>Without Search</h3>
|
|
||||||
<p>Dropdown with search functionality disabled for simpler selection</p>
|
|
||||||
|
|
||||||
<dees-input-dropdown
|
<dees-input-dropdown
|
||||||
.label=${'Priority Level'}
|
.label=${'Priority Level'}
|
||||||
.enableSearch=${false}
|
.enableSearch=${false}
|
||||||
@ -110,12 +82,9 @@ export const demoFunc = () => html`
|
|||||||
]}
|
]}
|
||||||
.selectedOption=${{ option: 'Medium', key: 'medium' }}
|
.selectedOption=${{ option: 'Medium', key: 'medium' }}
|
||||||
></dees-input-dropdown>
|
></dees-input-dropdown>
|
||||||
</div>
|
</dees-panel>
|
||||||
|
|
||||||
<div class="demo-section">
|
<dees-panel .title=${'3. Horizontal Layout'} .subtitle=${'Multiple dropdowns in a horizontal layout for compact forms'}>
|
||||||
<h3>Horizontal Layout</h3>
|
|
||||||
<p>Multiple dropdowns in a horizontal layout for compact forms</p>
|
|
||||||
|
|
||||||
<div class="horizontal-group">
|
<div class="horizontal-group">
|
||||||
<dees-input-dropdown
|
<dees-input-dropdown
|
||||||
.label=${'Department'}
|
.label=${'Department'}
|
||||||
@ -150,12 +119,9 @@ export const demoFunc = () => html`
|
|||||||
]}
|
]}
|
||||||
></dees-input-dropdown>
|
></dees-input-dropdown>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</dees-panel>
|
||||||
|
|
||||||
<div class="demo-section">
|
<dees-panel .title=${'4. States'} .subtitle=${'Different states and configurations'}>
|
||||||
<h3>States</h3>
|
|
||||||
<p>Different states and configurations</p>
|
|
||||||
|
|
||||||
<dees-input-dropdown
|
<dees-input-dropdown
|
||||||
.label=${'Required Field'}
|
.label=${'Required Field'}
|
||||||
.required=${true}
|
.required=${true}
|
||||||
@ -174,16 +140,13 @@ export const demoFunc = () => html`
|
|||||||
]}
|
]}
|
||||||
.selectedOption=${{ option: 'Cannot Select', key: 'disabled' }}
|
.selectedOption=${{ option: 'Cannot Select', key: 'disabled' }}
|
||||||
></dees-input-dropdown>
|
></dees-input-dropdown>
|
||||||
</div>
|
</dees-panel>
|
||||||
|
|
||||||
<div class="spacer">
|
<div class="spacer">
|
||||||
(Spacer to test dropdown positioning)
|
(Spacer to test dropdown positioning)
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="demo-section">
|
<dees-panel .title=${'5. Bottom Positioning'} .subtitle=${'Dropdown that opens upward when near bottom of viewport'}>
|
||||||
<h3>Bottom Positioning</h3>
|
|
||||||
<p>Dropdown that opens upward when near bottom of viewport</p>
|
|
||||||
|
|
||||||
<dees-input-dropdown
|
<dees-input-dropdown
|
||||||
.label=${'Opens Upward'}
|
.label=${'Opens Upward'}
|
||||||
.options=${[
|
.options=${[
|
||||||
@ -194,7 +157,65 @@ export const demoFunc = () => html`
|
|||||||
{ option: 'Fifth Option', key: 'fifth' }
|
{ option: 'Fifth Option', key: 'fifth' }
|
||||||
]}
|
]}
|
||||||
></dees-input-dropdown>
|
></dees-input-dropdown>
|
||||||
</div>
|
</dees-panel>
|
||||||
|
|
||||||
|
<dees-panel .title=${'6. Event Handling & Payload'} .subtitle=${'Dropdown with payload data and change event handling'}>
|
||||||
|
<dees-input-dropdown
|
||||||
|
.label=${'Select Product'}
|
||||||
|
.options=${[
|
||||||
|
{ option: 'Basic Plan', key: 'basic', payload: { price: 9.99, features: ['Feature A'] } },
|
||||||
|
{ option: 'Pro Plan', key: 'pro', payload: { price: 19.99, features: ['Feature A', 'Feature B'] } },
|
||||||
|
{ option: 'Enterprise Plan', key: 'enterprise', payload: { price: 49.99, features: ['Feature A', 'Feature B', 'Feature C'] } }
|
||||||
|
]}
|
||||||
|
@change=${(e: CustomEvent) => {
|
||||||
|
const output = document.querySelector('#selection-output');
|
||||||
|
if (output && e.detail.value) {
|
||||||
|
output.innerHTML = `
|
||||||
|
<strong>Selected:</strong> ${e.detail.value.option}<br>
|
||||||
|
<strong>Key:</strong> ${e.detail.value.key}<br>
|
||||||
|
<strong>Price:</strong> $${e.detail.value.payload?.price || 'N/A'}<br>
|
||||||
|
<strong>Features:</strong> ${e.detail.value.payload?.features?.join(', ') || 'N/A'}
|
||||||
|
`;
|
||||||
|
}
|
||||||
|
}}
|
||||||
|
></dees-input-dropdown>
|
||||||
|
|
||||||
|
<div id="selection-output" style="margin-top: 16px; padding: 12px; background: rgba(0, 105, 242, 0.1); border-radius: 4px; font-size: 14px;">
|
||||||
|
<em>Select a product to see details...</em>
|
||||||
|
</div>
|
||||||
|
</dees-panel>
|
||||||
|
|
||||||
|
<dees-panel .title=${'7. Form Integration'} .subtitle=${'Dropdown working within a form with validation'}>
|
||||||
|
<dees-form>
|
||||||
|
<dees-input-dropdown
|
||||||
|
.label=${'Project Type'}
|
||||||
|
.key=${'projectType'}
|
||||||
|
.required=${true}
|
||||||
|
.options=${[
|
||||||
|
{ option: 'Web Application', key: 'web' },
|
||||||
|
{ option: 'Mobile Application', key: 'mobile' },
|
||||||
|
{ option: 'Desktop Application', key: 'desktop' },
|
||||||
|
{ option: 'API Service', key: 'api' }
|
||||||
|
]}
|
||||||
|
></dees-input-dropdown>
|
||||||
|
|
||||||
|
<dees-input-dropdown
|
||||||
|
.label=${'Development Framework'}
|
||||||
|
.key=${'framework'}
|
||||||
|
.required=${true}
|
||||||
|
.options=${[
|
||||||
|
{ option: 'React', key: 'react', payload: { type: 'web' } },
|
||||||
|
{ option: 'Vue.js', key: 'vue', payload: { type: 'web' } },
|
||||||
|
{ option: 'Angular', key: 'angular', payload: { type: 'web' } },
|
||||||
|
{ option: 'React Native', key: 'react-native', payload: { type: 'mobile' } },
|
||||||
|
{ option: 'Flutter', key: 'flutter', payload: { type: 'mobile' } },
|
||||||
|
{ option: 'Electron', key: 'electron', payload: { type: 'desktop' } }
|
||||||
|
]}
|
||||||
|
></dees-input-dropdown>
|
||||||
|
|
||||||
|
<dees-form-submit .text=${'Create Project'}></dees-form-submit>
|
||||||
|
</dees-form>
|
||||||
|
</dees-panel>
|
||||||
</div>
|
</div>
|
||||||
</dees-demowrapper>
|
</dees-demowrapper>
|
||||||
`
|
`
|
@ -280,6 +280,11 @@ export class DeesInputDropdown extends DeesInputBase<DeesInputDropdown> {
|
|||||||
elevatedDropdown.style.top = this.getBoundingClientRect().top + 'px';
|
elevatedDropdown.style.top = this.getBoundingClientRect().top + 'px';
|
||||||
elevatedDropdown.style.left = this.getBoundingClientRect().left + 'px';
|
elevatedDropdown.style.left = this.getBoundingClientRect().left + 'px';
|
||||||
elevatedDropdown.style.width = this.clientWidth + 'px';
|
elevatedDropdown.style.width = this.clientWidth + 'px';
|
||||||
|
|
||||||
|
// Get z-index from registry for the elevated dropdown
|
||||||
|
const dropdownZIndex = (await import('./00zindex.js')).zIndexRegistry.getNextZIndex();
|
||||||
|
elevatedDropdown.style.zIndex = dropdownZIndex.toString();
|
||||||
|
(await import('./00zindex.js')).zIndexRegistry.register(elevatedDropdown, dropdownZIndex);
|
||||||
elevatedDropdown.options = this.options;
|
elevatedDropdown.options = this.options;
|
||||||
elevatedDropdown.selectedOption = this.selectedOption;
|
elevatedDropdown.selectedOption = this.selectedOption;
|
||||||
elevatedDropdown.highlightedIndex = elevatedDropdown.selectedOption ? elevatedDropdown.options.indexOf(
|
elevatedDropdown.highlightedIndex = elevatedDropdown.selectedOption ? elevatedDropdown.options.indexOf(
|
||||||
@ -289,6 +294,12 @@ export class DeesInputDropdown extends DeesInputBase<DeesInputDropdown> {
|
|||||||
console.log(elevatedDropdown.selectedOption);
|
console.log(elevatedDropdown.selectedOption);
|
||||||
console.log(elevatedDropdown.highlightedIndex);
|
console.log(elevatedDropdown.highlightedIndex);
|
||||||
this.windowOverlay.appendChild(elevatedDropdown);
|
this.windowOverlay.appendChild(elevatedDropdown);
|
||||||
|
|
||||||
|
// Prevent clicks on the dropdown from closing it
|
||||||
|
elevatedDropdown.addEventListener('click', (e: Event) => {
|
||||||
|
e.stopPropagation();
|
||||||
|
});
|
||||||
|
|
||||||
await domtoolsInstance.convenience.smartdelay.delayFor(0);
|
await domtoolsInstance.convenience.smartdelay.delayFor(0);
|
||||||
elevatedDropdown.toggleSelectionBox();
|
elevatedDropdown.toggleSelectionBox();
|
||||||
const destroyOverlay = async () => {
|
const destroyOverlay = async () => {
|
||||||
@ -296,9 +307,13 @@ export class DeesInputDropdown extends DeesInputBase<DeesInputDropdown> {
|
|||||||
'0';
|
'0';
|
||||||
elevatedDropdown.removeEventListener('selectedOption', handleSelection);
|
elevatedDropdown.removeEventListener('selectedOption', handleSelection);
|
||||||
this.windowOverlay.removeEventListener('clicked', destroyOverlay);
|
this.windowOverlay.removeEventListener('clicked', destroyOverlay);
|
||||||
|
|
||||||
|
// Unregister elevated dropdown from z-index registry
|
||||||
|
(await import('./00zindex.js')).zIndexRegistry.unregister(elevatedDropdown);
|
||||||
|
|
||||||
this.windowOverlay.destroy();
|
this.windowOverlay.destroy();
|
||||||
};
|
};
|
||||||
const handleSelection = async (event) => {
|
const handleSelection = async () => {
|
||||||
await this.updateSelection(elevatedDropdown.selectedOption);
|
await this.updateSelection(elevatedDropdown.selectedOption);
|
||||||
destroyOverlay();
|
destroyOverlay();
|
||||||
};
|
};
|
||||||
@ -323,10 +338,20 @@ export class DeesInputDropdown extends DeesInputBase<DeesInputDropdown> {
|
|||||||
await domtoolsInstance.convenience.smartdelay.delayFor(0);
|
await domtoolsInstance.convenience.smartdelay.delayFor(0);
|
||||||
const searchInput = selectionBox.querySelector('input');
|
const searchInput = selectionBox.querySelector('input');
|
||||||
searchInput?.focus();
|
searchInput?.focus();
|
||||||
|
|
||||||
|
// Get z-index from registry for the selection box
|
||||||
|
const selectionBoxZIndex = (await import('./00zindex.js')).zIndexRegistry.getNextZIndex();
|
||||||
|
selectionBox.style.zIndex = selectionBoxZIndex.toString();
|
||||||
|
(await import('./00zindex.js')).zIndexRegistry.register(selectionBox as HTMLElement, selectionBoxZIndex);
|
||||||
|
|
||||||
selectionBox.classList.add('show');
|
selectionBox.classList.add('show');
|
||||||
} else {
|
} else {
|
||||||
selectedBox.style.pointerEvents = 'none';
|
selectedBox.style.pointerEvents = 'none';
|
||||||
selectionBox.classList.remove('show');
|
selectionBox.classList.remove('show');
|
||||||
|
|
||||||
|
// Unregister selection box from z-index registry
|
||||||
|
(await import('./00zindex.js')).zIndexRegistry.unregister(selectionBox as HTMLElement);
|
||||||
|
|
||||||
// selectedBox.style.opacity = '0';
|
// selectedBox.style.opacity = '0';
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -51,85 +51,151 @@ export const demoFunc = () => html`
|
|||||||
</style>
|
</style>
|
||||||
|
|
||||||
<div class="demo-container">
|
<div class="demo-container">
|
||||||
<dees-panel .title=${'Basic File Upload'} .subtitle=${'Simple file upload with drag and drop support'}>
|
<dees-panel .title=${'1. Basic File Upload'} .subtitle=${'Simple file upload with drag and drop support'}>
|
||||||
<dees-input-fileupload
|
<dees-input-fileupload
|
||||||
.label=${'Attachments'}
|
.label=${'Attachments'}
|
||||||
.description=${'Upload files by clicking or dragging'}
|
.description=${'Upload any files by clicking or dragging them here'}
|
||||||
></dees-input-fileupload>
|
></dees-input-fileupload>
|
||||||
|
|
||||||
<dees-input-fileupload
|
<dees-input-fileupload
|
||||||
.label=${'Resume'}
|
.label=${'Single File Only'}
|
||||||
.description=${'Upload your CV in PDF format'}
|
.description=${'Only one file can be uploaded at a time'}
|
||||||
.buttonText=${'Choose Resume...'}
|
.multiple=${false}
|
||||||
|
.buttonText=${'Choose File'}
|
||||||
></dees-input-fileupload>
|
></dees-input-fileupload>
|
||||||
</dees-panel>
|
</dees-panel>
|
||||||
|
|
||||||
<dees-panel .title=${'Multiple Upload Areas'} .subtitle=${'Different upload zones for various file types'}>
|
<dees-panel .title=${'2. File Type Restrictions'} .subtitle=${'Upload areas with specific file type requirements'}>
|
||||||
<div class="upload-grid">
|
<div class="upload-grid">
|
||||||
<div class="upload-box">
|
<div class="upload-box">
|
||||||
<h4>Profile Picture</h4>
|
<h4>Images Only</h4>
|
||||||
<dees-input-fileupload
|
<dees-input-fileupload
|
||||||
.label=${'Avatar'}
|
.label=${'Profile Picture'}
|
||||||
.description=${'JPG, PNG or GIF'}
|
.description=${'JPG, PNG or GIF (max 5MB)'}
|
||||||
.buttonText=${'Select Image...'}
|
.accept=${'image/jpeg,image/png,image/gif'}
|
||||||
|
.maxSize=${5 * 1024 * 1024}
|
||||||
|
.multiple=${false}
|
||||||
|
.buttonText=${'Select Image'}
|
||||||
></dees-input-fileupload>
|
></dees-input-fileupload>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="upload-box">
|
<div class="upload-box">
|
||||||
<h4>Cover Image</h4>
|
<h4>Documents Only</h4>
|
||||||
<dees-input-fileupload
|
<dees-input-fileupload
|
||||||
.label=${'Banner'}
|
.label=${'Resume'}
|
||||||
.description=${'Recommended: 1200x400px'}
|
.description=${'PDF or Word documents only'}
|
||||||
.buttonText=${'Select Banner...'}
|
.accept=${".pdf,.doc,.docx,application/pdf,application/msword,application/vnd.openxmlformats-officedocument.wordprocessingml.document"}
|
||||||
|
.buttonText=${'Select Document'}
|
||||||
></dees-input-fileupload>
|
></dees-input-fileupload>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</dees-panel>
|
</dees-panel>
|
||||||
|
|
||||||
<dees-panel .title=${'Required & Disabled States'} .subtitle=${'Different upload states for validation'}>
|
<dees-panel .title=${'3. Validation & Limits'} .subtitle=${'File size limits and validation examples'}>
|
||||||
<dees-input-fileupload
|
<dees-input-fileupload
|
||||||
.label=${'Identity Document'}
|
.label=${'Small Files Only'}
|
||||||
.description=${'Required for verification'}
|
.description=${'Maximum file size: 1MB'}
|
||||||
.required=${true}
|
.maxSize=${1024 * 1024}
|
||||||
.buttonText=${'Upload Document...'}
|
.buttonText=${'Upload Small File'}
|
||||||
></dees-input-fileupload>
|
></dees-input-fileupload>
|
||||||
|
|
||||||
<dees-input-fileupload
|
<dees-input-fileupload
|
||||||
.label=${'System Files'}
|
.label=${'Limited Upload'}
|
||||||
.description=${'File upload is disabled'}
|
.description=${'Maximum 3 files, each up to 2MB'}
|
||||||
.disabled=${true}
|
.maxFiles=${3}
|
||||||
.value=${[]}
|
.maxSize=${2 * 1024 * 1024}
|
||||||
|
></dees-input-fileupload>
|
||||||
|
|
||||||
|
<dees-input-fileupload
|
||||||
|
.label=${'Required Upload'}
|
||||||
|
.description=${'This field is required'}
|
||||||
|
.required=${true}
|
||||||
></dees-input-fileupload>
|
></dees-input-fileupload>
|
||||||
</dees-panel>
|
</dees-panel>
|
||||||
|
|
||||||
<dees-panel .title=${'Application Form'} .subtitle=${'Complete form with file upload integration'}>
|
<dees-panel .title=${'4. States & Styling'} .subtitle=${'Different states and validation feedback'}>
|
||||||
|
<dees-input-fileupload
|
||||||
|
.label=${'Disabled Upload'}
|
||||||
|
.description=${'File upload is currently disabled'}
|
||||||
|
.disabled=${true}
|
||||||
|
></dees-input-fileupload>
|
||||||
|
|
||||||
|
<dees-input-fileupload
|
||||||
|
.label=${'Pre-filled Example'}
|
||||||
|
.description=${'Component with pre-loaded files'}
|
||||||
|
.value=${[
|
||||||
|
new File(['Hello World'], 'example.txt', { type: 'text/plain' }),
|
||||||
|
new File(['Test Data'], 'data.json', { type: 'application/json' })
|
||||||
|
]}
|
||||||
|
></dees-input-fileupload>
|
||||||
|
</dees-panel>
|
||||||
|
|
||||||
|
<dees-panel .title=${'5. Form Integration'} .subtitle=${'Complete form with various file upload scenarios'}>
|
||||||
<dees-form>
|
<dees-form>
|
||||||
<dees-input-text .label=${'Full Name'} .required=${true}></dees-input-text>
|
<h3 style="margin-top: 0; margin-bottom: 24px; color: ${cssManager.bdTheme('#333', '#fff')};">Job Application Form</h3>
|
||||||
<dees-input-text .label=${'Email'} .inputType=${'email'} .required=${true}></dees-input-text>
|
|
||||||
|
<dees-input-text
|
||||||
|
.label=${'Full Name'}
|
||||||
|
.required=${true}
|
||||||
|
.key=${'fullName'}
|
||||||
|
></dees-input-text>
|
||||||
|
|
||||||
|
<dees-input-text
|
||||||
|
.label=${'Email'}
|
||||||
|
.inputType=${'email'}
|
||||||
|
.required=${true}
|
||||||
|
.key=${'email'}
|
||||||
|
></dees-input-text>
|
||||||
|
|
||||||
<dees-input-fileupload
|
<dees-input-fileupload
|
||||||
.label=${'Resume'}
|
.label=${'Resume'}
|
||||||
.description=${'Upload your CV (PDF preferred)'}
|
.description=${'Required: PDF format only (max 10MB)'}
|
||||||
.required=${true}
|
.required=${true}
|
||||||
|
.accept=${'application/pdf'}
|
||||||
|
.maxSize=${10 * 1024 * 1024}
|
||||||
|
.multiple=${false}
|
||||||
|
.key=${'resume'}
|
||||||
></dees-input-fileupload>
|
></dees-input-fileupload>
|
||||||
|
|
||||||
<dees-input-fileupload
|
<dees-input-fileupload
|
||||||
.label=${'Portfolio'}
|
.label=${'Portfolio'}
|
||||||
.description=${'Optional: Upload work samples'}
|
.description=${'Optional: Upload up to 5 work samples (images or PDFs, max 5MB each)'}
|
||||||
|
.accept=${'image/*,application/pdf'}
|
||||||
|
.maxFiles=${5}
|
||||||
|
.maxSize=${5 * 1024 * 1024}
|
||||||
|
.key=${'portfolio'}
|
||||||
></dees-input-fileupload>
|
></dees-input-fileupload>
|
||||||
|
|
||||||
|
<dees-input-fileupload
|
||||||
|
.label=${'References'}
|
||||||
|
.description=${'Upload reference letters (optional)'}
|
||||||
|
.accept=${".pdf,.doc,.docx"}
|
||||||
|
.key=${'references'}
|
||||||
|
></dees-input-fileupload>
|
||||||
|
|
||||||
<dees-input-text
|
<dees-input-text
|
||||||
.label=${'Cover Letter'}
|
.label=${'Additional Comments'}
|
||||||
.inputType=${'textarea'}
|
.inputType=${'textarea'}
|
||||||
.description=${'Tell us why you would be a great fit'}
|
.description=${'Any additional information you would like to share'}
|
||||||
|
.key=${'comments'}
|
||||||
></dees-input-text>
|
></dees-input-text>
|
||||||
|
|
||||||
|
<dees-form-submit .text=${'Submit Application'}></dees-form-submit>
|
||||||
</dees-form>
|
</dees-form>
|
||||||
|
|
||||||
<div class="info-section">
|
<div class="info-section">
|
||||||
<h4>Features:</h4>
|
<h4 style="margin-top: 0;">Enhanced Features:</h4>
|
||||||
<ul>
|
<ul style="margin: 0; padding-left: 20px;">
|
||||||
<li>Click to select files or drag & drop</li>
|
<li>Drag & drop with visual feedback</li>
|
||||||
<li>Multiple file selection support</li>
|
<li>File type restrictions via accept attribute</li>
|
||||||
<li>Visual feedback for drag operations</li>
|
<li>File size validation with custom limits</li>
|
||||||
<li>Right-click files to remove them</li>
|
<li>Maximum file count restrictions</li>
|
||||||
<li>Integrates seamlessly with forms</li>
|
<li>Image preview thumbnails</li>
|
||||||
|
<li>File type-specific icons</li>
|
||||||
|
<li>Clear all button for multiple files</li>
|
||||||
|
<li>Proper validation states and messages</li>
|
||||||
|
<li>Keyboard accessible</li>
|
||||||
|
<li>Single or multiple file modes</li>
|
||||||
</ul>
|
</ul>
|
||||||
</div>
|
</div>
|
||||||
</dees-panel>
|
</dees-panel>
|
||||||
|
@ -42,6 +42,21 @@ export class DeesInputFileupload extends DeesInputBase<DeesInputFileupload> {
|
|||||||
})
|
})
|
||||||
public buttonText: string = 'Upload File...';
|
public buttonText: string = 'Upload File...';
|
||||||
|
|
||||||
|
@property({ type: String })
|
||||||
|
public accept: string = '';
|
||||||
|
|
||||||
|
@property({ type: Boolean })
|
||||||
|
public multiple: boolean = true;
|
||||||
|
|
||||||
|
@property({ type: Number })
|
||||||
|
public maxSize: number = 0; // 0 means no limit
|
||||||
|
|
||||||
|
@property({ type: Number })
|
||||||
|
public maxFiles: number = 0; // 0 means no limit
|
||||||
|
|
||||||
|
@property({ type: String, reflect: true })
|
||||||
|
public validationState: 'valid' | 'invalid' | 'warn' | 'pending' = null;
|
||||||
|
|
||||||
constructor() {
|
constructor() {
|
||||||
super();
|
super();
|
||||||
}
|
}
|
||||||
@ -52,7 +67,7 @@ export class DeesInputFileupload extends DeesInputBase<DeesInputFileupload> {
|
|||||||
css`
|
css`
|
||||||
:host {
|
:host {
|
||||||
position: relative;
|
position: relative;
|
||||||
display: grid;
|
display: block;
|
||||||
color: ${cssManager.bdTheme('#333', '#ccc')};
|
color: ${cssManager.bdTheme('#333', '#ccc')};
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -60,13 +75,42 @@ export class DeesInputFileupload extends DeesInputBase<DeesInputFileupload> {
|
|||||||
display: none;
|
display: none;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.input-wrapper {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 8px;
|
||||||
|
}
|
||||||
|
|
||||||
.maincontainer {
|
.maincontainer {
|
||||||
position: relative;
|
position: relative;
|
||||||
border-radius: 3px;
|
border-radius: 8px;
|
||||||
padding: 8px;
|
padding: 16px;
|
||||||
background: ${cssManager.bdTheme('#fafafa', '#222222')};
|
background: ${cssManager.bdTheme('#f8f9fa', '#1a1a1a')};
|
||||||
color: ${cssManager.bdTheme('#333', '#ccc')};
|
color: ${cssManager.bdTheme('#333', '#ccc')};
|
||||||
border-top: 1px solid #ffffff10;
|
border: 1px solid ${cssManager.bdTheme('#e0e0e0', '#333')};
|
||||||
|
transition: all 0.2s ease;
|
||||||
|
}
|
||||||
|
|
||||||
|
.maincontainer:hover {
|
||||||
|
border-color: ${cssManager.bdTheme('#ccc', '#444')};
|
||||||
|
}
|
||||||
|
|
||||||
|
:host([disabled]) .maincontainer {
|
||||||
|
opacity: 0.5;
|
||||||
|
cursor: not-allowed;
|
||||||
|
pointer-events: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
:host([validationState="invalid"]) .maincontainer {
|
||||||
|
border-color: #e74c3c;
|
||||||
|
}
|
||||||
|
|
||||||
|
:host([validationState="valid"]) .maincontainer {
|
||||||
|
border-color: #27ae60;
|
||||||
|
}
|
||||||
|
|
||||||
|
:host([validationState="warn"]) .maincontainer {
|
||||||
|
border-color: #f39c12;
|
||||||
}
|
}
|
||||||
|
|
||||||
.maincontainer::after {
|
.maincontainer::after {
|
||||||
@ -78,115 +122,385 @@ export class DeesInputFileupload extends DeesInputBase<DeesInputFileupload> {
|
|||||||
position: absolute;
|
position: absolute;
|
||||||
content: '';
|
content: '';
|
||||||
display: block;
|
display: block;
|
||||||
border: 2px dashed rgba(255, 255, 255, 0);
|
border: 2px dashed transparent;
|
||||||
transition: all 0.2s;
|
border-radius: 6px;
|
||||||
|
transition: all 0.3s ease;
|
||||||
pointer-events: none;
|
pointer-events: none;
|
||||||
background: #00000000;
|
background: transparent;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.maincontainer.dragOver {
|
||||||
|
border-color: ${cssManager.bdTheme('#0084ff', '#0084ff')};
|
||||||
|
background: ${cssManager.bdTheme('#f0f8ff', '#001933')};
|
||||||
|
}
|
||||||
|
|
||||||
.maincontainer.dragOver::after {
|
.maincontainer.dragOver::after {
|
||||||
transform: scale3d(1, 1, 1);
|
transform: scale3d(1, 1, 1);
|
||||||
border: 2px dashed rgba(255, 255, 255, 0.3);
|
border: 2px dashed ${cssManager.bdTheme('#0084ff', '#0084ff')};
|
||||||
background: #00000080;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.uploadButton {
|
.uploadButton {
|
||||||
position: relative;
|
position: relative;
|
||||||
padding: 8px;
|
padding: 12px 24px;
|
||||||
max-width: 600px;
|
background: ${cssManager.bdTheme('#0084ff', '#0084ff')};
|
||||||
background: ${cssManager.bdTheme('#fafafa', '#333333')};
|
color: white;
|
||||||
border-radius: 3px;
|
border-radius: 6px;
|
||||||
text-align: center;
|
text-align: center;
|
||||||
font-size: 14px;
|
font-size: 14px;
|
||||||
cursor: default;
|
font-weight: 500;
|
||||||
|
cursor: pointer;
|
||||||
|
transition: all 0.2s ease;
|
||||||
|
border: none;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
gap: 8px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.uploadButton:hover {
|
.uploadButton:hover {
|
||||||
color: #fff;
|
background: ${cssManager.bdTheme('#0073e6', '#0073e6')};
|
||||||
background: ${unsafeCSS(colors.dark.blue)};
|
transform: translateY(-1px);
|
||||||
|
box-shadow: 0 2px 8px rgba(0, 132, 255, 0.3);
|
||||||
|
}
|
||||||
|
|
||||||
|
.uploadButton:active {
|
||||||
|
transform: translateY(0);
|
||||||
|
}
|
||||||
|
|
||||||
|
.uploadButton dees-icon {
|
||||||
|
font-size: 16px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.files-container {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 8px;
|
||||||
|
margin-bottom: 12px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.uploadCandidate {
|
.uploadCandidate {
|
||||||
display: grid;
|
display: grid;
|
||||||
grid-template-columns: 48px auto;
|
grid-template-columns: 40px 1fr auto;
|
||||||
background: #333;
|
background: ${cssManager.bdTheme('#ffffff', '#2a2a2a')};
|
||||||
padding: 8px 8px 8px 0px;
|
padding: 12px;
|
||||||
margin-bottom: 8px;
|
|
||||||
text-align: left;
|
text-align: left;
|
||||||
border-radius: 3px;
|
border-radius: 6px;
|
||||||
color: ${cssManager.bdTheme('#666', '#ccc')};
|
color: ${cssManager.bdTheme('#333', '#ccc')};
|
||||||
font-family: 'Geist Sans', sans-serif;
|
|
||||||
cursor: default;
|
cursor: default;
|
||||||
transition: all 0.2s;
|
transition: all 0.2s;
|
||||||
border-top: 1px solid #ffffff10;
|
border: 1px solid ${cssManager.bdTheme('#e0e0e0', '#333')};
|
||||||
|
position: relative;
|
||||||
|
overflow: hidden;
|
||||||
}
|
}
|
||||||
|
|
||||||
.uploadCandidate:last-child {
|
.uploadCandidate:hover {
|
||||||
margin-bottom: 8px;
|
background: ${cssManager.bdTheme('#f5f5f5', '#333')};
|
||||||
|
border-color: ${cssManager.bdTheme('#ccc', '#444')};
|
||||||
}
|
}
|
||||||
|
|
||||||
.uploadCandidate .icon {
|
.uploadCandidate .icon {
|
||||||
display: flex;
|
display: flex;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
justify-content: center;
|
justify-content: center;
|
||||||
font-size: 16px;
|
font-size: 20px;
|
||||||
|
color: ${cssManager.bdTheme('#666', '#999')};
|
||||||
}
|
}
|
||||||
|
|
||||||
.uploadCandidate:hover {
|
.uploadCandidate.image-file .icon {
|
||||||
background: #393939;
|
color: #4CAF50;
|
||||||
|
}
|
||||||
|
|
||||||
|
.uploadCandidate.pdf-file .icon {
|
||||||
|
color: #f44336;
|
||||||
|
}
|
||||||
|
|
||||||
|
.uploadCandidate.doc-file .icon {
|
||||||
|
color: #2196F3;
|
||||||
}
|
}
|
||||||
|
|
||||||
.uploadCandidate .description {
|
.uploadCandidate .info {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 2px;
|
||||||
|
min-width: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.uploadCandidate .filename {
|
||||||
|
font-weight: 500;
|
||||||
font-size: 14px;
|
font-size: 14px;
|
||||||
border-left: 1px solid #ffffff10;
|
white-space: nowrap;
|
||||||
padding-left: 8px;
|
overflow: hidden;
|
||||||
|
text-overflow: ellipsis;
|
||||||
|
}
|
||||||
|
|
||||||
|
.uploadCandidate .filesize {
|
||||||
|
font-size: 12px;
|
||||||
|
color: ${cssManager.bdTheme('#666', '#999')};
|
||||||
|
}
|
||||||
|
|
||||||
|
.uploadCandidate .actions {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 8px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.remove-button {
|
||||||
|
width: 32px;
|
||||||
|
height: 32px;
|
||||||
|
border-radius: 4px;
|
||||||
|
background: transparent;
|
||||||
|
border: none;
|
||||||
|
cursor: pointer;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
transition: all 0.2s;
|
||||||
|
color: ${cssManager.bdTheme('#666', '#999')};
|
||||||
|
}
|
||||||
|
|
||||||
|
.remove-button:hover {
|
||||||
|
background: ${cssManager.bdTheme('#fee', '#4a1c1c')};
|
||||||
|
color: ${cssManager.bdTheme('#e74c3c', '#ff6b6b')};
|
||||||
|
}
|
||||||
|
|
||||||
|
.clear-all-button {
|
||||||
|
margin-bottom: 8px;
|
||||||
|
text-align: right;
|
||||||
|
}
|
||||||
|
|
||||||
|
.clear-all-button button {
|
||||||
|
background: none;
|
||||||
|
border: none;
|
||||||
|
color: ${cssManager.bdTheme('#666', '#999')};
|
||||||
|
cursor: pointer;
|
||||||
|
font-size: 12px;
|
||||||
|
padding: 4px 8px;
|
||||||
|
border-radius: 4px;
|
||||||
|
transition: all 0.2s;
|
||||||
|
}
|
||||||
|
|
||||||
|
.clear-all-button button:hover {
|
||||||
|
background: ${cssManager.bdTheme('#fee', '#4a1c1c')};
|
||||||
|
color: ${cssManager.bdTheme('#e74c3c', '#ff6b6b')};
|
||||||
|
}
|
||||||
|
|
||||||
|
.validation-message {
|
||||||
|
font-size: 12px;
|
||||||
|
margin-top: 4px;
|
||||||
|
color: #e74c3c;
|
||||||
|
}
|
||||||
|
|
||||||
|
.drop-hint {
|
||||||
|
text-align: center;
|
||||||
|
padding: 40px 20px;
|
||||||
|
color: ${cssManager.bdTheme('#999', '#666')};
|
||||||
|
font-size: 14px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.drop-hint dees-icon {
|
||||||
|
font-size: 48px;
|
||||||
|
margin-bottom: 16px;
|
||||||
|
opacity: 0.3;
|
||||||
|
}
|
||||||
|
|
||||||
|
.image-preview {
|
||||||
|
width: 40px;
|
||||||
|
height: 40px;
|
||||||
|
object-fit: cover;
|
||||||
|
border-radius: 4px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.description-text {
|
||||||
|
font-size: 12px;
|
||||||
|
color: ${cssManager.bdTheme('#666', '#999')};
|
||||||
|
margin-top: 4px;
|
||||||
|
line-height: 1.4;
|
||||||
}
|
}
|
||||||
`,
|
`,
|
||||||
];
|
];
|
||||||
|
|
||||||
public render(): TemplateResult {
|
public render(): TemplateResult {
|
||||||
|
const hasFiles = this.value.length > 0;
|
||||||
|
const showClearAll = hasFiles && this.value.length > 1;
|
||||||
|
|
||||||
return html`
|
return html`
|
||||||
<div class="input-wrapper">
|
<div class="input-wrapper">
|
||||||
<dees-label .label=${this.label} .description=${this.description}></dees-label>
|
${this.label ? html`
|
||||||
|
<dees-label .label=${this.label}></dees-label>
|
||||||
|
` : ''}
|
||||||
<div class="hidden">
|
<div class="hidden">
|
||||||
<input type="file">
|
<input
|
||||||
|
type="file"
|
||||||
|
?multiple=${this.multiple}
|
||||||
|
accept="${this.accept}"
|
||||||
|
>
|
||||||
</div>
|
</div>
|
||||||
<div class="maincontainer ${this.state === 'dragOver' ? 'dragOver' : ''}">
|
<div class="maincontainer ${this.state === 'dragOver' ? 'dragOver' : ''}">
|
||||||
${this.value.map(
|
${hasFiles ? html`
|
||||||
(fileArg) => html`
|
${showClearAll ? html`
|
||||||
<div class="uploadCandidate" @contextmenu=${eventArg => {
|
<div class="clear-all-button">
|
||||||
DeesContextmenu.openContextMenuWithOptions(eventArg, [{
|
<button @click=${this.clearAll}>Clear All</button>
|
||||||
iconName: 'trash',
|
</div>
|
||||||
name: 'Remove',
|
` : ''}
|
||||||
action: async () => {
|
<div class="files-container">
|
||||||
this.value.splice(this.value.indexOf(fileArg), 1);
|
${this.value.map((fileArg) => {
|
||||||
this.requestUpdate();
|
const fileType = this.getFileType(fileArg);
|
||||||
}
|
const isImage = fileType === 'image';
|
||||||
}]);
|
return html`
|
||||||
}}>
|
<div class="uploadCandidate ${fileType}-file">
|
||||||
<div class="icon">
|
<div class="icon">
|
||||||
<dees-icon .iconFA=${'paperclip'}></dees-icon>
|
${isImage && this.canShowPreview(fileArg) ? html`
|
||||||
|
<img class="image-preview" src="${URL.createObjectURL(fileArg)}" alt="${fileArg.name}">
|
||||||
|
` : html`
|
||||||
|
<dees-icon .iconName=${this.getFileIcon(fileArg)}></dees-icon>
|
||||||
|
`}
|
||||||
|
</div>
|
||||||
|
<div class="info">
|
||||||
|
<div class="filename" title="${fileArg.name}">${fileArg.name}</div>
|
||||||
|
<div class="filesize">${this.formatFileSize(fileArg.size)}</div>
|
||||||
|
</div>
|
||||||
|
<div class="actions">
|
||||||
|
<button
|
||||||
|
class="remove-button"
|
||||||
|
@click=${() => this.removeFile(fileArg)}
|
||||||
|
title="Remove file"
|
||||||
|
>
|
||||||
|
<dees-icon .iconName=${'lucide:x'}></dees-icon>
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
`;
|
||||||
|
})}
|
||||||
</div>
|
</div>
|
||||||
<div class="description">
|
` : html`
|
||||||
<span style="font-weight: 600">${fileArg.name}</span><br />
|
<div class="drop-hint">
|
||||||
<span style="font-weight: 400">${fileArg.size}</span>
|
<dees-icon .iconName=${'lucide:cloud-upload'}></dees-icon>
|
||||||
|
<div>Drag files here or click to browse</div>
|
||||||
</div>
|
</div>
|
||||||
</div> `
|
`}
|
||||||
)}
|
<div class="uploadButton" @click=${this.openFileSelector}>
|
||||||
<div class="uploadButton" @click=${
|
<dees-icon .iconName=${'lucide:upload'}></dees-icon>
|
||||||
this.openFileSelector
|
${this.buttonText}
|
||||||
}>
|
</div>
|
||||||
${this.buttonText}
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
${this.description ? html`
|
||||||
|
<div class="description-text">${this.description}</div>
|
||||||
|
` : ''}
|
||||||
|
${this.validationState === 'invalid' && this.validationMessage ? html`
|
||||||
|
<div class="validation-message">${this.validationMessage}</div>
|
||||||
|
` : ''}
|
||||||
</div>
|
</div>
|
||||||
`;
|
`;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private validationMessage: string = '';
|
||||||
|
|
||||||
|
// Utility methods
|
||||||
|
private formatFileSize(bytes: number): string {
|
||||||
|
const sizes = ['Bytes', 'KB', 'MB', 'GB'];
|
||||||
|
if (bytes === 0) return '0 Bytes';
|
||||||
|
const i = Math.floor(Math.log(bytes) / Math.log(1024));
|
||||||
|
return Math.round(bytes / Math.pow(1024, i) * 100) / 100 + ' ' + sizes[i];
|
||||||
|
}
|
||||||
|
|
||||||
|
private getFileType(file: File): string {
|
||||||
|
const type = file.type.toLowerCase();
|
||||||
|
if (type.startsWith('image/')) return 'image';
|
||||||
|
if (type === 'application/pdf') return 'pdf';
|
||||||
|
if (type.includes('word') || type.includes('document')) return 'doc';
|
||||||
|
if (type.includes('sheet') || type.includes('excel')) return 'spreadsheet';
|
||||||
|
if (type.includes('presentation') || type.includes('powerpoint')) return 'presentation';
|
||||||
|
if (type.startsWith('video/')) return 'video';
|
||||||
|
if (type.startsWith('audio/')) return 'audio';
|
||||||
|
if (type.includes('zip') || type.includes('compressed')) return 'archive';
|
||||||
|
return 'file';
|
||||||
|
}
|
||||||
|
|
||||||
|
private getFileIcon(file: File): string {
|
||||||
|
const type = this.getFileType(file);
|
||||||
|
const iconMap = {
|
||||||
|
'image': 'lucide:image',
|
||||||
|
'pdf': 'lucide:file-text',
|
||||||
|
'doc': 'lucide:file-text',
|
||||||
|
'spreadsheet': 'lucide:table',
|
||||||
|
'presentation': 'lucide:presentation',
|
||||||
|
'video': 'lucide:video',
|
||||||
|
'audio': 'lucide:music',
|
||||||
|
'archive': 'lucide:archive',
|
||||||
|
'file': 'lucide:file'
|
||||||
|
};
|
||||||
|
return iconMap[type] || 'lucide:file';
|
||||||
|
}
|
||||||
|
|
||||||
|
private canShowPreview(file: File): boolean {
|
||||||
|
return file.type.startsWith('image/') && file.size < 5 * 1024 * 1024; // 5MB limit for previews
|
||||||
|
}
|
||||||
|
|
||||||
|
private validateFile(file: File): boolean {
|
||||||
|
// Check file size
|
||||||
|
if (this.maxSize > 0 && file.size > this.maxSize) {
|
||||||
|
this.validationMessage = `File "${file.name}" exceeds maximum size of ${this.formatFileSize(this.maxSize)}`;
|
||||||
|
this.validationState = 'invalid';
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check file type
|
||||||
|
if (this.accept) {
|
||||||
|
const acceptedTypes = this.accept.split(',').map(s => s.trim());
|
||||||
|
let isAccepted = false;
|
||||||
|
|
||||||
|
for (const acceptType of acceptedTypes) {
|
||||||
|
if (acceptType.startsWith('.')) {
|
||||||
|
// Extension check
|
||||||
|
if (file.name.toLowerCase().endsWith(acceptType.toLowerCase())) {
|
||||||
|
isAccepted = true;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
} else if (acceptType.endsWith('/*')) {
|
||||||
|
// MIME type wildcard check
|
||||||
|
const mimePrefix = acceptType.slice(0, -2);
|
||||||
|
if (file.type.startsWith(mimePrefix)) {
|
||||||
|
isAccepted = true;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
} else if (file.type === acceptType) {
|
||||||
|
// Exact MIME type check
|
||||||
|
isAccepted = true;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!isAccepted) {
|
||||||
|
this.validationMessage = `File type not accepted. Please upload: ${this.accept}`;
|
||||||
|
this.validationState = 'invalid';
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
public async openFileSelector() {
|
public async openFileSelector() {
|
||||||
|
if (this.disabled) return;
|
||||||
const inputFile: HTMLInputElement = this.shadowRoot.querySelector('input[type="file"]');
|
const inputFile: HTMLInputElement = this.shadowRoot.querySelector('input[type="file"]');
|
||||||
inputFile.click();
|
inputFile.click();
|
||||||
this.state = 'idle';
|
}
|
||||||
this.buttonText = 'Upload more files...';
|
|
||||||
|
private removeFile(file: File) {
|
||||||
|
const index = this.value.indexOf(file);
|
||||||
|
if (index > -1) {
|
||||||
|
this.value.splice(index, 1);
|
||||||
|
this.requestUpdate();
|
||||||
|
this.validate();
|
||||||
|
this.changeSubject.next(this);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private clearAll() {
|
||||||
|
this.value = [];
|
||||||
|
this.requestUpdate();
|
||||||
|
this.validate();
|
||||||
|
this.changeSubject.next(this);
|
||||||
}
|
}
|
||||||
|
|
||||||
public async updateValue(eventArg: Event) {
|
public async updateValue(eventArg: Event) {
|
||||||
@ -198,52 +512,131 @@ export class DeesInputFileupload extends DeesInputBase<DeesInputFileupload> {
|
|||||||
public firstUpdated(_changedProperties: Map<string | number | symbol, unknown>) {
|
public firstUpdated(_changedProperties: Map<string | number | symbol, unknown>) {
|
||||||
super.firstUpdated(_changedProperties);
|
super.firstUpdated(_changedProperties);
|
||||||
const inputFile: HTMLInputElement = this.shadowRoot.querySelector('input[type="file"]');
|
const inputFile: HTMLInputElement = this.shadowRoot.querySelector('input[type="file"]');
|
||||||
inputFile.addEventListener('change', (event: Event) => {
|
inputFile.addEventListener('change', async (event: Event) => {
|
||||||
const target = event.target as HTMLInputElement;
|
const target = event.target as HTMLInputElement;
|
||||||
for (const file of Array.from(target.files)) {
|
const newFiles = Array.from(target.files);
|
||||||
this.value.push(file);
|
await this.addFiles(newFiles);
|
||||||
}
|
|
||||||
this.requestUpdate();
|
|
||||||
console.log(`Got ${this.value.length} files!`);
|
|
||||||
// Reset the input value to allow selecting the same file again if needed
|
// Reset the input value to allow selecting the same file again if needed
|
||||||
target.value = '';
|
target.value = '';
|
||||||
});
|
});
|
||||||
|
|
||||||
// lets handle drag and drop
|
// Handle drag and drop
|
||||||
const dropArea = this.shadowRoot.querySelector('.maincontainer');
|
const dropArea = this.shadowRoot.querySelector('.maincontainer');
|
||||||
const handlerFunction = (eventArg: DragEvent) => {
|
const handlerFunction = async (eventArg: DragEvent) => {
|
||||||
eventArg.preventDefault();
|
eventArg.preventDefault();
|
||||||
|
eventArg.stopPropagation();
|
||||||
|
|
||||||
switch (eventArg.type) {
|
switch (eventArg.type) {
|
||||||
|
case 'dragenter':
|
||||||
case 'dragover':
|
case 'dragover':
|
||||||
this.state = 'dragOver';
|
this.state = 'dragOver';
|
||||||
this.buttonText = 'release to upload file...';
|
|
||||||
break;
|
break;
|
||||||
case 'dragleave':
|
case 'dragleave':
|
||||||
this.state = 'idle';
|
// Check if we're actually leaving the drop area
|
||||||
this.buttonText = 'Upload File...';
|
const rect = dropArea.getBoundingClientRect();
|
||||||
|
const x = eventArg.clientX;
|
||||||
|
const y = eventArg.clientY;
|
||||||
|
if (x <= rect.left || x >= rect.right || y <= rect.top || y >= rect.bottom) {
|
||||||
|
this.state = 'idle';
|
||||||
|
}
|
||||||
break;
|
break;
|
||||||
case 'drop':
|
case 'drop':
|
||||||
this.state = 'idle';
|
this.state = 'idle';
|
||||||
this.buttonText = 'Upload more files...';
|
const files = Array.from(eventArg.dataTransfer.files);
|
||||||
|
await this.addFiles(files);
|
||||||
|
break;
|
||||||
}
|
}
|
||||||
console.log(eventArg);
|
|
||||||
for (const file of Array.from(eventArg.dataTransfer.files)) {
|
|
||||||
this.value.push(file);
|
|
||||||
this.requestUpdate();
|
|
||||||
}
|
|
||||||
console.log(`Got ${this.value.length} files!`);
|
|
||||||
};
|
};
|
||||||
|
|
||||||
dropArea.addEventListener('dragenter', handlerFunction, false);
|
dropArea.addEventListener('dragenter', handlerFunction, false);
|
||||||
dropArea.addEventListener('dragleave', handlerFunction, false);
|
dropArea.addEventListener('dragleave', handlerFunction, false);
|
||||||
dropArea.addEventListener('dragover', handlerFunction, false);
|
dropArea.addEventListener('dragover', handlerFunction, false);
|
||||||
dropArea.addEventListener('drop', handlerFunction, false);
|
dropArea.addEventListener('drop', handlerFunction, false);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private async addFiles(files: File[]) {
|
||||||
|
const filesToAdd: File[] = [];
|
||||||
|
|
||||||
|
for (const file of files) {
|
||||||
|
if (this.validateFile(file)) {
|
||||||
|
filesToAdd.push(file);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (filesToAdd.length === 0) return;
|
||||||
|
|
||||||
|
// Check max files limit
|
||||||
|
if (this.maxFiles > 0) {
|
||||||
|
const totalFiles = this.value.length + filesToAdd.length;
|
||||||
|
if (totalFiles > this.maxFiles) {
|
||||||
|
const allowedCount = this.maxFiles - this.value.length;
|
||||||
|
if (allowedCount <= 0) {
|
||||||
|
this.validationMessage = `Maximum ${this.maxFiles} files allowed`;
|
||||||
|
this.validationState = 'invalid';
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
filesToAdd.splice(allowedCount);
|
||||||
|
this.validationMessage = `Only ${allowedCount} more file(s) can be added`;
|
||||||
|
this.validationState = 'warn';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Add files
|
||||||
|
if (!this.multiple && filesToAdd.length > 0) {
|
||||||
|
this.value = [filesToAdd[0]];
|
||||||
|
} else {
|
||||||
|
this.value.push(...filesToAdd);
|
||||||
|
}
|
||||||
|
|
||||||
|
this.requestUpdate();
|
||||||
|
this.validate();
|
||||||
|
this.changeSubject.next(this);
|
||||||
|
|
||||||
|
// Update button text
|
||||||
|
if (this.value.length > 0) {
|
||||||
|
this.buttonText = this.multiple ? 'Add more files' : 'Replace file';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public async validate(): Promise<boolean> {
|
||||||
|
this.validationMessage = '';
|
||||||
|
|
||||||
|
if (this.required && this.value.length === 0) {
|
||||||
|
this.validationState = 'invalid';
|
||||||
|
this.validationMessage = 'Please select at least one file';
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Validate all files
|
||||||
|
for (const file of this.value) {
|
||||||
|
if (!this.validateFile(file)) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
this.validationState = 'valid';
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
public getValue(): File[] {
|
public getValue(): File[] {
|
||||||
return this.value;
|
return this.value;
|
||||||
}
|
}
|
||||||
|
|
||||||
public setValue(value: File[]): void {
|
public setValue(value: File[]): void {
|
||||||
this.value = value;
|
this.value = value;
|
||||||
|
this.requestUpdate();
|
||||||
|
if (value.length > 0) {
|
||||||
|
this.buttonText = this.multiple ? 'Add more files' : 'Replace file';
|
||||||
|
} else {
|
||||||
|
this.buttonText = 'Upload File...';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public updated(changedProperties: Map<string, any>) {
|
||||||
|
super.updated(changedProperties);
|
||||||
|
|
||||||
|
if (changedProperties.has('value')) {
|
||||||
|
this.validate();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
248
ts_web/elements/dees-input-tags.demo.ts
Normal file
248
ts_web/elements/dees-input-tags.demo.ts
Normal file
@ -0,0 +1,248 @@
|
|||||||
|
import { html, css } from '@design.estate/dees-element';
|
||||||
|
import '@design.estate/dees-wcctools/demotools';
|
||||||
|
import './dees-panel.js';
|
||||||
|
|
||||||
|
export const demoFunc = () => html`
|
||||||
|
<dees-demowrapper>
|
||||||
|
<style>
|
||||||
|
${css`
|
||||||
|
.demo-container {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 24px;
|
||||||
|
padding: 24px;
|
||||||
|
max-width: 1200px;
|
||||||
|
margin: 0 auto;
|
||||||
|
}
|
||||||
|
|
||||||
|
dees-panel {
|
||||||
|
margin-bottom: 24px;
|
||||||
|
}
|
||||||
|
|
||||||
|
dees-panel:last-child {
|
||||||
|
margin-bottom: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.grid-layout {
|
||||||
|
display: grid;
|
||||||
|
grid-template-columns: 1fr 1fr;
|
||||||
|
gap: 16px;
|
||||||
|
}
|
||||||
|
|
||||||
|
@media (max-width: 768px) {
|
||||||
|
.grid-layout {
|
||||||
|
grid-template-columns: 1fr;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.output-preview {
|
||||||
|
margin-top: 16px;
|
||||||
|
padding: 16px;
|
||||||
|
background: #f3f4f6;
|
||||||
|
border-radius: 4px;
|
||||||
|
font-size: 12px;
|
||||||
|
color: #374151;
|
||||||
|
word-break: break-all;
|
||||||
|
max-height: 200px;
|
||||||
|
overflow-y: auto;
|
||||||
|
}
|
||||||
|
|
||||||
|
@media (prefers-color-scheme: dark) {
|
||||||
|
.output-preview {
|
||||||
|
background: #2c2c2c;
|
||||||
|
color: #e4e4e7;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.tag-preview {
|
||||||
|
display: flex;
|
||||||
|
flex-wrap: wrap;
|
||||||
|
gap: 8px;
|
||||||
|
padding: 12px;
|
||||||
|
background: #f9fafb;
|
||||||
|
border-radius: 4px;
|
||||||
|
min-height: 40px;
|
||||||
|
align-items: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
@media (prefers-color-scheme: dark) {
|
||||||
|
.tag-preview {
|
||||||
|
background: #1f2937;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.tag-preview-item {
|
||||||
|
display: inline-block;
|
||||||
|
padding: 4px 12px;
|
||||||
|
background: #e0e7ff;
|
||||||
|
color: #4338ca;
|
||||||
|
border-radius: 12px;
|
||||||
|
font-size: 14px;
|
||||||
|
}
|
||||||
|
|
||||||
|
@media (prefers-color-scheme: dark) {
|
||||||
|
.tag-preview-item {
|
||||||
|
background: #312e81;
|
||||||
|
color: #c7d2fe;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
`}
|
||||||
|
</style>
|
||||||
|
|
||||||
|
<div class="demo-container">
|
||||||
|
<dees-panel .title=${'1. Basic Tags Input'} .subtitle=${'Simple tag input with common programming languages'}>
|
||||||
|
<dees-input-tags
|
||||||
|
.label=${'Programming Languages'}
|
||||||
|
.placeholder=${'Add a language...'}
|
||||||
|
.value=${['JavaScript', 'TypeScript', 'Python', 'Go']}
|
||||||
|
.description=${'Press Enter or comma to add tags'}
|
||||||
|
></dees-input-tags>
|
||||||
|
</dees-panel>
|
||||||
|
|
||||||
|
<dees-panel .title=${'2. Tags with Suggestions'} .subtitle=${'Auto-complete suggestions for faster input'}>
|
||||||
|
<dees-input-tags
|
||||||
|
.label=${'Tech Stack'}
|
||||||
|
.placeholder=${'Type to see suggestions...'}
|
||||||
|
.suggestions=${[
|
||||||
|
'React', 'Vue', 'Angular', 'Svelte', 'Lit', 'Next.js', 'Nuxt', 'SvelteKit',
|
||||||
|
'Node.js', 'Deno', 'Bun', 'Express', 'Fastify', 'Nest.js', 'Koa',
|
||||||
|
'MongoDB', 'PostgreSQL', 'Redis', 'MySQL', 'SQLite', 'Cassandra',
|
||||||
|
'Docker', 'Kubernetes', 'AWS', 'Azure', 'GCP', 'Vercel', 'Netlify'
|
||||||
|
]}
|
||||||
|
.value=${['React', 'Node.js', 'PostgreSQL', 'Docker']}
|
||||||
|
.description=${'Start typing to see suggestions from popular technologies'}
|
||||||
|
></dees-input-tags>
|
||||||
|
</dees-panel>
|
||||||
|
|
||||||
|
<dees-panel .title=${'3. Limited Tags'} .subtitle=${'Restrict the number of tags users can add'}>
|
||||||
|
<div class="grid-layout">
|
||||||
|
<dees-input-tags
|
||||||
|
.label=${'Top 3 Skills'}
|
||||||
|
.placeholder=${'Add up to 3 skills...'}
|
||||||
|
.maxTags=${3}
|
||||||
|
.value=${['Design', 'Development']}
|
||||||
|
.description=${'Maximum 3 tags allowed'}
|
||||||
|
></dees-input-tags>
|
||||||
|
|
||||||
|
<dees-input-tags
|
||||||
|
.label=${'Categories (Max 5)'}
|
||||||
|
.placeholder=${'Select categories...'}
|
||||||
|
.maxTags=${5}
|
||||||
|
.suggestions=${['Blog', 'Tutorial', 'News', 'Review', 'Guide', 'Case Study', 'Interview']}
|
||||||
|
.value=${['Tutorial', 'Guide']}
|
||||||
|
.description=${'Choose up to 5 categories'}
|
||||||
|
></dees-input-tags>
|
||||||
|
</div>
|
||||||
|
</dees-panel>
|
||||||
|
|
||||||
|
<dees-panel .title=${'4. Required & Validation'} .subtitle=${'Tags input with validation requirements'}>
|
||||||
|
<dees-input-tags
|
||||||
|
.label=${'Project Tags'}
|
||||||
|
.placeholder=${'Add at least one tag...'}
|
||||||
|
.required=${true}
|
||||||
|
.description=${'This field is required - add at least one tag'}
|
||||||
|
></dees-input-tags>
|
||||||
|
</dees-panel>
|
||||||
|
|
||||||
|
<dees-panel .title=${'5. Disabled State'} .subtitle=${'Read-only tags display'}>
|
||||||
|
<dees-input-tags
|
||||||
|
.label=${'System Tags'}
|
||||||
|
.value=${['System', 'Protected', 'Read-Only', 'Archive']}
|
||||||
|
.disabled=${true}
|
||||||
|
.description=${'These tags cannot be modified'}
|
||||||
|
></dees-input-tags>
|
||||||
|
</dees-panel>
|
||||||
|
|
||||||
|
<dees-panel .title=${'6. Form Integration'} .subtitle=${'Tags input working within a form context'}>
|
||||||
|
<dees-form>
|
||||||
|
<dees-input-text
|
||||||
|
.label=${'Project Name'}
|
||||||
|
.placeholder=${'My Awesome Project'}
|
||||||
|
.required=${true}
|
||||||
|
.key=${'name'}
|
||||||
|
></dees-input-text>
|
||||||
|
|
||||||
|
<div class="grid-layout">
|
||||||
|
<dees-input-tags
|
||||||
|
.label=${'Technologies Used'}
|
||||||
|
.placeholder=${'Add technologies...'}
|
||||||
|
.required=${true}
|
||||||
|
.key=${'technologies'}
|
||||||
|
.suggestions=${[
|
||||||
|
'TypeScript', 'JavaScript', 'Python', 'Go', 'Rust',
|
||||||
|
'React', 'Vue', 'Angular', 'Svelte',
|
||||||
|
'Node.js', 'Deno', 'Express', 'FastAPI'
|
||||||
|
]}
|
||||||
|
></dees-input-tags>
|
||||||
|
|
||||||
|
<dees-input-tags
|
||||||
|
.label=${'Project Tags'}
|
||||||
|
.placeholder=${'Add descriptive tags...'}
|
||||||
|
.key=${'tags'}
|
||||||
|
.maxTags=${10}
|
||||||
|
.suggestions=${[
|
||||||
|
'frontend', 'backend', 'fullstack', 'mobile', 'desktop',
|
||||||
|
'web', 'api', 'database', 'devops', 'ui/ux',
|
||||||
|
'opensource', 'saas', 'enterprise', 'startup'
|
||||||
|
]}
|
||||||
|
></dees-input-tags>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<dees-input-text
|
||||||
|
.label=${'Description'}
|
||||||
|
.inputType=${'textarea'}
|
||||||
|
.placeholder=${'Describe your project...'}
|
||||||
|
.key=${'description'}
|
||||||
|
></dees-input-text>
|
||||||
|
|
||||||
|
<dees-form-submit .text=${'Create Project'}></dees-form-submit>
|
||||||
|
</dees-form>
|
||||||
|
</dees-panel>
|
||||||
|
|
||||||
|
<dees-panel .title=${'7. Interactive Demo'} .subtitle=${'Add tags and see them collected in real-time'}>
|
||||||
|
<dees-input-tags
|
||||||
|
id="interactive-tags"
|
||||||
|
.label=${'Your Interests'}
|
||||||
|
.placeholder=${'Type your interests...'}
|
||||||
|
.suggestions=${[
|
||||||
|
'Music', 'Movies', 'Books', 'Travel', 'Photography',
|
||||||
|
'Cooking', 'Gaming', 'Sports', 'Art', 'Technology',
|
||||||
|
'Fashion', 'Fitness', 'Nature', 'Science', 'History'
|
||||||
|
]}
|
||||||
|
@change=${(e: CustomEvent) => {
|
||||||
|
const preview = document.querySelector('#tags-preview');
|
||||||
|
const tags = e.detail.value;
|
||||||
|
if (preview) {
|
||||||
|
if (tags.length === 0) {
|
||||||
|
preview.innerHTML = '<em style="color: #999;">No tags added yet...</em>';
|
||||||
|
} else {
|
||||||
|
preview.innerHTML = tags.map((tag: string) =>
|
||||||
|
`<span class="tag-preview-item">${tag}</span>`
|
||||||
|
).join('');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}}
|
||||||
|
></dees-input-tags>
|
||||||
|
|
||||||
|
<div class="tag-preview" id="tags-preview">
|
||||||
|
<em style="color: #999;">No tags added yet...</em>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="output-preview" id="tags-json">
|
||||||
|
<em>JSON output will appear here...</em>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
// Update JSON preview
|
||||||
|
const tagsInput = document.querySelector('#interactive-tags');
|
||||||
|
tagsInput?.addEventListener('change', (e) => {
|
||||||
|
const jsonPreview = document.querySelector('#tags-json');
|
||||||
|
if (jsonPreview) {
|
||||||
|
jsonPreview.textContent = JSON.stringify(e.detail.value, null, 2);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
</script>
|
||||||
|
</dees-panel>
|
||||||
|
</div>
|
||||||
|
</dees-demowrapper>
|
||||||
|
`;
|
@ -0,0 +1,401 @@
|
|||||||
|
import {
|
||||||
|
customElement,
|
||||||
|
html,
|
||||||
|
css,
|
||||||
|
cssManager,
|
||||||
|
property,
|
||||||
|
state,
|
||||||
|
type TemplateResult,
|
||||||
|
} from '@design.estate/dees-element';
|
||||||
|
import { DeesInputBase } from './dees-input-base.js';
|
||||||
|
import './dees-icon.js';
|
||||||
|
import { demoFunc } from './dees-input-tags.demo.js';
|
||||||
|
|
||||||
|
declare global {
|
||||||
|
interface HTMLElementTagNameMap {
|
||||||
|
'dees-input-tags': DeesInputTags;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@customElement('dees-input-tags')
|
||||||
|
export class DeesInputTags extends DeesInputBase<DeesInputTags> {
|
||||||
|
// STATIC
|
||||||
|
public static demo = demoFunc;
|
||||||
|
|
||||||
|
// INSTANCE
|
||||||
|
@property({ type: Array })
|
||||||
|
public value: string[] = [];
|
||||||
|
|
||||||
|
@property({ type: String })
|
||||||
|
public placeholder: string = 'Add tags...';
|
||||||
|
|
||||||
|
@property({ type: Number })
|
||||||
|
public maxTags: number = 0; // 0 means unlimited
|
||||||
|
|
||||||
|
@property({ type: Array })
|
||||||
|
public suggestions: string[] = [];
|
||||||
|
|
||||||
|
@state()
|
||||||
|
private inputValue: string = '';
|
||||||
|
|
||||||
|
@state()
|
||||||
|
private showSuggestions: boolean = false;
|
||||||
|
|
||||||
|
@state()
|
||||||
|
private highlightedSuggestionIndex: number = -1;
|
||||||
|
|
||||||
|
@property({ type: String })
|
||||||
|
public validationText: string = '';
|
||||||
|
|
||||||
|
public static styles = [
|
||||||
|
...DeesInputBase.baseStyles,
|
||||||
|
cssManager.defaultStyles,
|
||||||
|
css`
|
||||||
|
:host {
|
||||||
|
display: block;
|
||||||
|
font-family: 'Geist Sans', sans-serif;
|
||||||
|
}
|
||||||
|
|
||||||
|
.input-wrapper {
|
||||||
|
width: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
|
.tags-container {
|
||||||
|
display: flex;
|
||||||
|
flex-wrap: wrap;
|
||||||
|
align-items: center;
|
||||||
|
gap: 8px;
|
||||||
|
padding: 8px;
|
||||||
|
min-height: 40px;
|
||||||
|
background: ${cssManager.bdTheme('#fafafa', '#222222')};
|
||||||
|
border: 1px solid ${cssManager.bdTheme('#e0e0e0', '#333333')};
|
||||||
|
border-radius: 8px;
|
||||||
|
transition: all 0.2s ease;
|
||||||
|
cursor: text;
|
||||||
|
}
|
||||||
|
|
||||||
|
.tags-container:focus-within {
|
||||||
|
border-color: ${cssManager.bdTheme('#0069f2', '#0084ff')};
|
||||||
|
box-shadow: 0 0 0 2px ${cssManager.bdTheme('rgba(0, 105, 242, 0.1)', 'rgba(0, 132, 255, 0.2)')};
|
||||||
|
}
|
||||||
|
|
||||||
|
.tags-container.disabled {
|
||||||
|
background: ${cssManager.bdTheme('#f5f5f5', '#1a1a1a')};
|
||||||
|
cursor: not-allowed;
|
||||||
|
opacity: 0.6;
|
||||||
|
}
|
||||||
|
|
||||||
|
.tag {
|
||||||
|
display: inline-flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 4px;
|
||||||
|
padding: 4px 8px;
|
||||||
|
background: ${cssManager.bdTheme('#e3f2fd', '#1e3a5f')};
|
||||||
|
color: ${cssManager.bdTheme('#1976d2', '#90caf9')};
|
||||||
|
border-radius: 16px;
|
||||||
|
font-size: 14px;
|
||||||
|
line-height: 1.2;
|
||||||
|
user-select: none;
|
||||||
|
animation: tagAppear 0.2s ease;
|
||||||
|
}
|
||||||
|
|
||||||
|
@keyframes tagAppear {
|
||||||
|
from {
|
||||||
|
transform: scale(0.8);
|
||||||
|
opacity: 0;
|
||||||
|
}
|
||||||
|
to {
|
||||||
|
transform: scale(1);
|
||||||
|
opacity: 1;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.tag-remove {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
width: 16px;
|
||||||
|
height: 16px;
|
||||||
|
border-radius: 50%;
|
||||||
|
cursor: pointer;
|
||||||
|
transition: all 0.2s ease;
|
||||||
|
margin-left: 2px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.tag-remove:hover {
|
||||||
|
background: ${cssManager.bdTheme('rgba(0, 0, 0, 0.1)', 'rgba(255, 255, 255, 0.1)')};
|
||||||
|
}
|
||||||
|
|
||||||
|
.tag-remove dees-icon {
|
||||||
|
width: 12px;
|
||||||
|
height: 12px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.tag-input {
|
||||||
|
flex: 1;
|
||||||
|
min-width: 120px;
|
||||||
|
border: none;
|
||||||
|
background: transparent;
|
||||||
|
outline: none;
|
||||||
|
font-size: 14px;
|
||||||
|
font-family: inherit;
|
||||||
|
color: ${cssManager.bdTheme('#333', '#fff')};
|
||||||
|
padding: 4px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.tag-input::placeholder {
|
||||||
|
color: ${cssManager.bdTheme('#999', '#666')};
|
||||||
|
}
|
||||||
|
|
||||||
|
.tag-input:disabled {
|
||||||
|
cursor: not-allowed;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Suggestions dropdown */
|
||||||
|
.suggestions-container {
|
||||||
|
position: relative;
|
||||||
|
}
|
||||||
|
|
||||||
|
.suggestions-dropdown {
|
||||||
|
position: absolute;
|
||||||
|
top: 100%;
|
||||||
|
left: 0;
|
||||||
|
right: 0;
|
||||||
|
margin-top: 4px;
|
||||||
|
background: ${cssManager.bdTheme('#ffffff', '#222222')};
|
||||||
|
border: 1px solid ${cssManager.bdTheme('#e0e0e0', '#333333')};
|
||||||
|
border-radius: 8px;
|
||||||
|
box-shadow: 0 4px 12px ${cssManager.bdTheme('rgba(0, 0, 0, 0.1)', 'rgba(0, 0, 0, 0.3)')};
|
||||||
|
max-height: 200px;
|
||||||
|
overflow-y: auto;
|
||||||
|
z-index: 1000;
|
||||||
|
}
|
||||||
|
|
||||||
|
.suggestion {
|
||||||
|
padding: 8px 12px;
|
||||||
|
cursor: pointer;
|
||||||
|
transition: all 0.2s ease;
|
||||||
|
color: ${cssManager.bdTheme('#333', '#fff')};
|
||||||
|
}
|
||||||
|
|
||||||
|
.suggestion:hover,
|
||||||
|
.suggestion.highlighted {
|
||||||
|
background: ${cssManager.bdTheme('#f5f5f5', '#333333')};
|
||||||
|
}
|
||||||
|
|
||||||
|
.suggestion.highlighted {
|
||||||
|
background: ${cssManager.bdTheme('#e3f2fd', '#1e3a5f')};
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Validation styles */
|
||||||
|
.validation-message {
|
||||||
|
color: #d32f2f;
|
||||||
|
font-size: 12px;
|
||||||
|
margin-top: 4px;
|
||||||
|
min-height: 16px;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Description styles */
|
||||||
|
.description {
|
||||||
|
color: ${cssManager.bdTheme('#666', '#999')};
|
||||||
|
font-size: 12px;
|
||||||
|
margin-top: 4px;
|
||||||
|
}
|
||||||
|
`,
|
||||||
|
];
|
||||||
|
|
||||||
|
public render(): TemplateResult {
|
||||||
|
const filteredSuggestions = this.suggestions.filter(
|
||||||
|
suggestion =>
|
||||||
|
!this.value.includes(suggestion) &&
|
||||||
|
suggestion.toLowerCase().includes(this.inputValue.toLowerCase())
|
||||||
|
);
|
||||||
|
|
||||||
|
return html`
|
||||||
|
<div class="input-wrapper">
|
||||||
|
${this.label ? html`<dees-label .label=${this.label} .required=${this.required}></dees-label>` : ''}
|
||||||
|
|
||||||
|
<div class="suggestions-container">
|
||||||
|
<div
|
||||||
|
class="tags-container ${this.disabled ? 'disabled' : ''}"
|
||||||
|
@click=${this.handleContainerClick}
|
||||||
|
>
|
||||||
|
${this.value.map(tag => html`
|
||||||
|
<div class="tag">
|
||||||
|
<span>${tag}</span>
|
||||||
|
${!this.disabled ? html`
|
||||||
|
<div class="tag-remove" @click=${(e: Event) => this.removeTag(e, tag)}>
|
||||||
|
<dees-icon .icon=${'lucide:x'}></dees-icon>
|
||||||
|
</div>
|
||||||
|
` : ''}
|
||||||
|
</div>
|
||||||
|
`)}
|
||||||
|
|
||||||
|
${!this.disabled && (!this.maxTags || this.value.length < this.maxTags) ? html`
|
||||||
|
<input
|
||||||
|
type="text"
|
||||||
|
class="tag-input"
|
||||||
|
.placeholder=${this.placeholder}
|
||||||
|
.value=${this.inputValue}
|
||||||
|
@input=${this.handleInput}
|
||||||
|
@keydown=${this.handleKeyDown}
|
||||||
|
@focus=${this.handleFocus}
|
||||||
|
@blur=${this.handleBlur}
|
||||||
|
?disabled=${this.disabled}
|
||||||
|
/>
|
||||||
|
` : ''}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
${this.showSuggestions && filteredSuggestions.length > 0 ? html`
|
||||||
|
<div class="suggestions-dropdown">
|
||||||
|
${filteredSuggestions.map((suggestion, index) => html`
|
||||||
|
<div
|
||||||
|
class="suggestion ${index === this.highlightedSuggestionIndex ? 'highlighted' : ''}"
|
||||||
|
@mousedown=${(e: Event) => {
|
||||||
|
e.preventDefault(); // Prevent blur
|
||||||
|
this.addTag(suggestion);
|
||||||
|
}}
|
||||||
|
@mouseenter=${() => this.highlightedSuggestionIndex = index}
|
||||||
|
>
|
||||||
|
${suggestion}
|
||||||
|
</div>
|
||||||
|
`)}
|
||||||
|
</div>
|
||||||
|
` : ''}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
${this.validationText ? html`
|
||||||
|
<div class="validation-message">${this.validationText}</div>
|
||||||
|
` : ''}
|
||||||
|
|
||||||
|
${this.description ? html`
|
||||||
|
<div class="description">${this.description}</div>
|
||||||
|
` : ''}
|
||||||
|
</div>
|
||||||
|
`;
|
||||||
|
}
|
||||||
|
|
||||||
|
private handleContainerClick(e: Event) {
|
||||||
|
if (this.disabled) return;
|
||||||
|
|
||||||
|
const input = this.shadowRoot?.querySelector('.tag-input') as HTMLInputElement;
|
||||||
|
if (input && e.target !== input) {
|
||||||
|
input.focus();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private handleInput(e: Event) {
|
||||||
|
const input = e.target as HTMLInputElement;
|
||||||
|
this.inputValue = input.value;
|
||||||
|
|
||||||
|
// Check for comma or semicolon to add tag
|
||||||
|
if (this.inputValue.includes(',') || this.inputValue.includes(';')) {
|
||||||
|
const tag = this.inputValue.replace(/[,;]/g, '').trim();
|
||||||
|
if (tag) {
|
||||||
|
this.addTag(tag);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private handleKeyDown(e: KeyboardEvent) {
|
||||||
|
const input = e.target as HTMLInputElement;
|
||||||
|
|
||||||
|
if (e.key === 'Enter') {
|
||||||
|
e.preventDefault();
|
||||||
|
if (this.highlightedSuggestionIndex >= 0 && this.showSuggestions) {
|
||||||
|
const filteredSuggestions = this.suggestions.filter(
|
||||||
|
suggestion =>
|
||||||
|
!this.value.includes(suggestion) &&
|
||||||
|
suggestion.toLowerCase().includes(this.inputValue.toLowerCase())
|
||||||
|
);
|
||||||
|
if (filteredSuggestions[this.highlightedSuggestionIndex]) {
|
||||||
|
this.addTag(filteredSuggestions[this.highlightedSuggestionIndex]);
|
||||||
|
}
|
||||||
|
} else if (this.inputValue.trim()) {
|
||||||
|
this.addTag(this.inputValue.trim());
|
||||||
|
}
|
||||||
|
} else if (e.key === 'Backspace' && !this.inputValue && this.value.length > 0) {
|
||||||
|
// Remove last tag when backspace is pressed on empty input
|
||||||
|
this.removeTag(e, this.value[this.value.length - 1]);
|
||||||
|
} else if (e.key === 'ArrowDown' && this.showSuggestions) {
|
||||||
|
e.preventDefault();
|
||||||
|
const filteredCount = this.suggestions.filter(
|
||||||
|
s => !this.value.includes(s) && s.toLowerCase().includes(this.inputValue.toLowerCase())
|
||||||
|
).length;
|
||||||
|
this.highlightedSuggestionIndex = Math.min(
|
||||||
|
this.highlightedSuggestionIndex + 1,
|
||||||
|
filteredCount - 1
|
||||||
|
);
|
||||||
|
} else if (e.key === 'ArrowUp' && this.showSuggestions) {
|
||||||
|
e.preventDefault();
|
||||||
|
this.highlightedSuggestionIndex = Math.max(this.highlightedSuggestionIndex - 1, 0);
|
||||||
|
} else if (e.key === 'Escape') {
|
||||||
|
this.showSuggestions = false;
|
||||||
|
this.highlightedSuggestionIndex = -1;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private handleFocus() {
|
||||||
|
if (this.suggestions.length > 0) {
|
||||||
|
this.showSuggestions = true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private handleBlur() {
|
||||||
|
// Delay to allow click on suggestions
|
||||||
|
setTimeout(() => {
|
||||||
|
this.showSuggestions = false;
|
||||||
|
this.highlightedSuggestionIndex = -1;
|
||||||
|
}, 200);
|
||||||
|
}
|
||||||
|
|
||||||
|
private addTag(tag: string) {
|
||||||
|
if (!tag || this.value.includes(tag)) return;
|
||||||
|
if (this.maxTags && this.value.length >= this.maxTags) return;
|
||||||
|
|
||||||
|
this.value = [...this.value, tag];
|
||||||
|
this.inputValue = '';
|
||||||
|
this.showSuggestions = false;
|
||||||
|
this.highlightedSuggestionIndex = -1;
|
||||||
|
|
||||||
|
// Clear the input
|
||||||
|
const input = this.shadowRoot?.querySelector('.tag-input') as HTMLInputElement;
|
||||||
|
if (input) {
|
||||||
|
input.value = '';
|
||||||
|
}
|
||||||
|
|
||||||
|
this.emitChange();
|
||||||
|
}
|
||||||
|
|
||||||
|
private removeTag(e: Event, tag: string) {
|
||||||
|
e.stopPropagation();
|
||||||
|
this.value = this.value.filter(t => t !== tag);
|
||||||
|
this.emitChange();
|
||||||
|
}
|
||||||
|
|
||||||
|
private emitChange() {
|
||||||
|
this.dispatchEvent(new CustomEvent('change', {
|
||||||
|
detail: { value: this.value },
|
||||||
|
bubbles: true,
|
||||||
|
composed: true
|
||||||
|
}));
|
||||||
|
this.changeSubject.next(this);
|
||||||
|
}
|
||||||
|
|
||||||
|
public getValue(): string[] {
|
||||||
|
return this.value;
|
||||||
|
}
|
||||||
|
|
||||||
|
public setValue(value: string[]): void {
|
||||||
|
this.value = value || [];
|
||||||
|
}
|
||||||
|
|
||||||
|
public async validate(): Promise<boolean> {
|
||||||
|
if (this.required && (!this.value || this.value.length === 0)) {
|
||||||
|
this.validationText = 'At least one tag is required';
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
this.validationText = '';
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
}
|
@ -1,4 +1,5 @@
|
|||||||
import * as plugins from './00plugins.js';
|
import * as plugins from './00plugins.js';
|
||||||
|
import { zIndexLayers } from './00zindex.js';
|
||||||
import {
|
import {
|
||||||
cssManager,
|
cssManager,
|
||||||
css,
|
css,
|
||||||
@ -83,7 +84,7 @@ export class DeesMobilenavigation extends DeesElement {
|
|||||||
min-width: 280px;
|
min-width: 280px;
|
||||||
transform: translateX(200px);
|
transform: translateX(200px);
|
||||||
color: ${cssManager.bdTheme('#333', '#fff')};
|
color: ${cssManager.bdTheme('#333', '#fff')};
|
||||||
z-index: 250;
|
z-index: ${zIndexLayers.fixed.mobileNav};
|
||||||
opacity: 0;
|
opacity: 0;
|
||||||
padding: 16px 32px;
|
padding: 16px 32px;
|
||||||
right: 0px;
|
right: 0px;
|
||||||
|
@ -1,37 +1,356 @@
|
|||||||
import { html } from '@design.estate/dees-element';
|
import { html, css, cssManager } from '@design.estate/dees-element';
|
||||||
import { DeesModal } from './dees-modal.js';
|
import { DeesModal } from './dees-modal.js';
|
||||||
|
|
||||||
export const demoFunc = () => html`
|
export const demoFunc = () => html`
|
||||||
<dees-button @click=${() => {
|
<style>
|
||||||
DeesModal.createAndShow({
|
${css`
|
||||||
heading: 'This is a heading',
|
.demo-container {
|
||||||
content: html`
|
display: flex;
|
||||||
<dees-form>
|
flex-direction: column;
|
||||||
<dees-input-text
|
gap: 24px;
|
||||||
.label=${'Username'}
|
padding: 24px;
|
||||||
>
|
max-width: 1200px;
|
||||||
</dees-input-text>
|
margin: 0 auto;
|
||||||
<dees-input-text
|
}
|
||||||
.label=${'Password'}
|
|
||||||
>
|
.demo-section {
|
||||||
</dees-input-text>
|
background: ${cssManager.bdTheme('#f8f9fa', '#1a1a1a')};
|
||||||
</dees-form>
|
border-radius: 8px;
|
||||||
`,
|
padding: 24px;
|
||||||
menuOptions: [{
|
border: 1px solid ${cssManager.bdTheme('#e0e0e0', '#333')};
|
||||||
name: 'Cancel',
|
}
|
||||||
iconName: null,
|
|
||||||
action: async (deesModalArg) => {
|
.demo-section h3 {
|
||||||
deesModalArg.destroy();
|
margin-top: 0;
|
||||||
return null;
|
margin-bottom: 16px;
|
||||||
}
|
color: ${cssManager.bdTheme('#333', '#fff')};
|
||||||
}, {
|
}
|
||||||
name: 'Ok',
|
|
||||||
iconName: null,
|
.demo-section p {
|
||||||
action: async (deesModalArg) => {
|
color: ${cssManager.bdTheme('#666', '#999')};
|
||||||
deesModalArg.destroy();
|
margin-bottom: 16px;
|
||||||
return null;
|
}
|
||||||
}
|
|
||||||
}],
|
.button-grid {
|
||||||
});
|
display: grid;
|
||||||
}}>open modal</dees-button>
|
grid-template-columns: repeat(auto-fit, minmax(150px, 1fr));
|
||||||
|
gap: 16px;
|
||||||
|
}
|
||||||
|
`}
|
||||||
|
</style>
|
||||||
|
|
||||||
|
<div class="demo-container">
|
||||||
|
<div class="demo-section">
|
||||||
|
<h3>Header Buttons</h3>
|
||||||
|
<p>Modals can have optional header buttons for help and closing.</p>
|
||||||
|
<div class="button-grid">
|
||||||
|
<dees-button @click=${() => {
|
||||||
|
DeesModal.createAndShow({
|
||||||
|
heading: 'With Help Button',
|
||||||
|
showHelpButton: true,
|
||||||
|
onHelp: async () => {
|
||||||
|
const helpModal = await DeesModal.createAndShow({
|
||||||
|
heading: 'Help',
|
||||||
|
width: 'small',
|
||||||
|
showCloseButton: true,
|
||||||
|
showHelpButton: false,
|
||||||
|
content: html`
|
||||||
|
<p>This is the help content for the modal.</p>
|
||||||
|
<p>You can provide context-specific help here.</p>
|
||||||
|
`,
|
||||||
|
menuOptions: [{
|
||||||
|
name: 'Got it',
|
||||||
|
action: async (modal) => modal.destroy()
|
||||||
|
}],
|
||||||
|
});
|
||||||
|
},
|
||||||
|
content: html`
|
||||||
|
<p>This modal has a help button in the header. Click it to see help content.</p>
|
||||||
|
<p>The close button is also visible by default.</p>
|
||||||
|
`,
|
||||||
|
menuOptions: [{
|
||||||
|
name: 'OK',
|
||||||
|
action: async (modal) => modal.destroy()
|
||||||
|
}],
|
||||||
|
});
|
||||||
|
}}>With Help Button</dees-button>
|
||||||
|
|
||||||
|
<dees-button @click=${() => {
|
||||||
|
DeesModal.createAndShow({
|
||||||
|
heading: 'No Close Button',
|
||||||
|
showCloseButton: false,
|
||||||
|
content: html`
|
||||||
|
<p>This modal has no close button in the header.</p>
|
||||||
|
<p>You must use the action buttons or click outside to close it.</p>
|
||||||
|
`,
|
||||||
|
menuOptions: [{
|
||||||
|
name: 'Close',
|
||||||
|
action: async (modal) => modal.destroy()
|
||||||
|
}],
|
||||||
|
});
|
||||||
|
}}>No Close Button</dees-button>
|
||||||
|
|
||||||
|
<dees-button @click=${() => {
|
||||||
|
DeesModal.createAndShow({
|
||||||
|
heading: 'Both Buttons',
|
||||||
|
showHelpButton: true,
|
||||||
|
showCloseButton: true,
|
||||||
|
onHelp: () => alert('Help clicked!'),
|
||||||
|
content: html`
|
||||||
|
<p>This modal has both help and close buttons.</p>
|
||||||
|
`,
|
||||||
|
menuOptions: [{
|
||||||
|
name: 'Done',
|
||||||
|
action: async (modal) => modal.destroy()
|
||||||
|
}],
|
||||||
|
});
|
||||||
|
}}>Both Buttons</dees-button>
|
||||||
|
|
||||||
|
<dees-button @click=${() => {
|
||||||
|
DeesModal.createAndShow({
|
||||||
|
heading: 'Clean Header',
|
||||||
|
showCloseButton: false,
|
||||||
|
showHelpButton: false,
|
||||||
|
content: html`
|
||||||
|
<p>This modal has a clean header with no buttons.</p>
|
||||||
|
`,
|
||||||
|
menuOptions: [{
|
||||||
|
name: 'Close',
|
||||||
|
action: async (modal) => modal.destroy()
|
||||||
|
}],
|
||||||
|
});
|
||||||
|
}}>Clean Header</dees-button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="demo-section">
|
||||||
|
<h3>Modal Width Variations</h3>
|
||||||
|
<p>Modals can have different widths: small, medium, large, fullscreen, or custom pixel values.</p>
|
||||||
|
<div class="button-grid">
|
||||||
|
<dees-button @click=${() => {
|
||||||
|
DeesModal.createAndShow({
|
||||||
|
heading: 'Small Modal',
|
||||||
|
width: 'small',
|
||||||
|
content: html`
|
||||||
|
<p>This is a small modal with a width of 380px. Perfect for simple confirmations or brief messages.</p>
|
||||||
|
`,
|
||||||
|
menuOptions: [{
|
||||||
|
name: 'Cancel',
|
||||||
|
action: async (modal) => modal.destroy()
|
||||||
|
}, {
|
||||||
|
name: 'OK',
|
||||||
|
action: async (modal) => modal.destroy()
|
||||||
|
}],
|
||||||
|
});
|
||||||
|
}}>Small Modal</dees-button>
|
||||||
|
|
||||||
|
<dees-button @click=${() => {
|
||||||
|
DeesModal.createAndShow({
|
||||||
|
heading: 'Medium Modal (Default)',
|
||||||
|
width: 'medium',
|
||||||
|
content: html`
|
||||||
|
<dees-form>
|
||||||
|
<dees-input-text .label=${'Username'}></dees-input-text>
|
||||||
|
<dees-input-text .label=${'Email'} .inputType=${'email'}></dees-input-text>
|
||||||
|
<dees-input-text .label=${'Password'} .inputType=${'password'}></dees-input-text>
|
||||||
|
</dees-form>
|
||||||
|
`,
|
||||||
|
menuOptions: [{
|
||||||
|
name: 'Cancel',
|
||||||
|
action: async (modal) => modal.destroy()
|
||||||
|
}, {
|
||||||
|
name: 'Sign Up',
|
||||||
|
action: async (modal) => modal.destroy()
|
||||||
|
}],
|
||||||
|
});
|
||||||
|
}}>Medium Modal</dees-button>
|
||||||
|
|
||||||
|
<dees-button @click=${() => {
|
||||||
|
DeesModal.createAndShow({
|
||||||
|
heading: 'Large Modal',
|
||||||
|
width: 'large',
|
||||||
|
content: html`
|
||||||
|
<h4>Wide Content Area</h4>
|
||||||
|
<p>This large modal is 800px wide and perfect for displaying more complex content like forms with multiple columns, tables, or detailed information.</p>
|
||||||
|
<div style="display: grid; grid-template-columns: 1fr 1fr; gap: 16px; margin-top: 16px;">
|
||||||
|
<dees-input-text .label=${'First Name'}></dees-input-text>
|
||||||
|
<dees-input-text .label=${'Last Name'}></dees-input-text>
|
||||||
|
<dees-input-text .label=${'Company'}></dees-input-text>
|
||||||
|
<dees-input-text .label=${'Position'}></dees-input-text>
|
||||||
|
</div>
|
||||||
|
`,
|
||||||
|
menuOptions: [{
|
||||||
|
name: 'Cancel',
|
||||||
|
action: async (modal) => modal.destroy()
|
||||||
|
}, {
|
||||||
|
name: 'Save',
|
||||||
|
action: async (modal) => modal.destroy()
|
||||||
|
}],
|
||||||
|
});
|
||||||
|
}}>Large Modal</dees-button>
|
||||||
|
|
||||||
|
<dees-button @click=${() => {
|
||||||
|
DeesModal.createAndShow({
|
||||||
|
heading: 'Fullscreen Editor',
|
||||||
|
width: 'fullscreen',
|
||||||
|
showHelpButton: true,
|
||||||
|
onHelp: async () => {
|
||||||
|
alert('In a real app, this would show editor documentation');
|
||||||
|
},
|
||||||
|
content: html`
|
||||||
|
<h4>Fullscreen Experience with Header Controls</h4>
|
||||||
|
<p>This modal takes up almost the entire viewport with a 20px margin on all sides. The header buttons are particularly useful in fullscreen mode.</p>
|
||||||
|
<p>The content area can be as tall as needed and will scroll if necessary.</p>
|
||||||
|
<div style="height: 200px; background: ${cssManager.bdTheme('#f0f0f0', '#2a2a2a')}; border-radius: 8px; display: flex; align-items: center; justify-content: center; margin-top: 16px;">
|
||||||
|
<span style="color: ${cssManager.bdTheme('#999', '#666')}">Large content area</span>
|
||||||
|
</div>
|
||||||
|
`,
|
||||||
|
menuOptions: [{
|
||||||
|
name: 'Save',
|
||||||
|
action: async (modal) => modal.destroy()
|
||||||
|
}, {
|
||||||
|
name: 'Cancel',
|
||||||
|
action: async (modal) => modal.destroy()
|
||||||
|
}],
|
||||||
|
});
|
||||||
|
}}>Fullscreen Modal</dees-button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="demo-section">
|
||||||
|
<h3>Custom Width & Constraints</h3>
|
||||||
|
<p>You can also set custom pixel widths and min/max constraints.</p>
|
||||||
|
<div class="button-grid">
|
||||||
|
<dees-button @click=${() => {
|
||||||
|
DeesModal.createAndShow({
|
||||||
|
heading: 'Custom Width (700px)',
|
||||||
|
width: 700,
|
||||||
|
content: html`
|
||||||
|
<p>This modal has a custom width of exactly 700 pixels.</p>
|
||||||
|
`,
|
||||||
|
menuOptions: [{
|
||||||
|
name: 'Close',
|
||||||
|
action: async (modal) => modal.destroy()
|
||||||
|
}],
|
||||||
|
});
|
||||||
|
}}>Custom 700px</dees-button>
|
||||||
|
|
||||||
|
<dees-button @click=${() => {
|
||||||
|
DeesModal.createAndShow({
|
||||||
|
heading: 'With Max Width',
|
||||||
|
width: 'large',
|
||||||
|
maxWidth: 600,
|
||||||
|
content: html`
|
||||||
|
<p>This modal is set to 'large' but constrained by a maxWidth of 600px.</p>
|
||||||
|
`,
|
||||||
|
menuOptions: [{
|
||||||
|
name: 'Got it',
|
||||||
|
action: async (modal) => modal.destroy()
|
||||||
|
}],
|
||||||
|
});
|
||||||
|
}}>Max Width 600px</dees-button>
|
||||||
|
|
||||||
|
<dees-button @click=${() => {
|
||||||
|
DeesModal.createAndShow({
|
||||||
|
heading: 'With Min Width',
|
||||||
|
width: 300,
|
||||||
|
minWidth: 400,
|
||||||
|
content: html`
|
||||||
|
<p>This modal width is set to 300px but has a minWidth of 400px, so it will be 400px wide.</p>
|
||||||
|
`,
|
||||||
|
menuOptions: [{
|
||||||
|
name: 'OK',
|
||||||
|
action: async (modal) => modal.destroy()
|
||||||
|
}],
|
||||||
|
});
|
||||||
|
}}>Min Width 400px</dees-button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="demo-section">
|
||||||
|
<h3>Button Variations</h3>
|
||||||
|
<p>Modals can have different button configurations with proper spacing.</p>
|
||||||
|
<div class="button-grid">
|
||||||
|
<dees-button @click=${() => {
|
||||||
|
DeesModal.createAndShow({
|
||||||
|
heading: 'Multiple Actions',
|
||||||
|
content: html`
|
||||||
|
<p>This modal demonstrates multiple buttons with proper spacing between them.</p>
|
||||||
|
`,
|
||||||
|
menuOptions: [{
|
||||||
|
name: 'Delete',
|
||||||
|
action: async (modal) => modal.destroy()
|
||||||
|
}, {
|
||||||
|
name: 'Cancel',
|
||||||
|
action: async (modal) => modal.destroy()
|
||||||
|
}, {
|
||||||
|
name: 'Save Changes',
|
||||||
|
action: async (modal) => modal.destroy()
|
||||||
|
}],
|
||||||
|
});
|
||||||
|
}}>Three Buttons</dees-button>
|
||||||
|
|
||||||
|
<dees-button @click=${() => {
|
||||||
|
DeesModal.createAndShow({
|
||||||
|
heading: 'Single Action',
|
||||||
|
content: html`
|
||||||
|
<p>Sometimes you just need one button.</p>
|
||||||
|
`,
|
||||||
|
menuOptions: [{
|
||||||
|
name: 'Acknowledge',
|
||||||
|
action: async (modal) => modal.destroy()
|
||||||
|
}],
|
||||||
|
});
|
||||||
|
}}>Single Button</dees-button>
|
||||||
|
|
||||||
|
<dees-button @click=${() => {
|
||||||
|
DeesModal.createAndShow({
|
||||||
|
heading: 'No Actions',
|
||||||
|
content: html`
|
||||||
|
<p>This modal has no bottom buttons. Use the X button or click outside to close.</p>
|
||||||
|
<p style="margin-top: 16px; color: ${cssManager.bdTheme('#666', '#999')};">This is useful for informational modals that don't require user action.</p>
|
||||||
|
`,
|
||||||
|
menuOptions: [],
|
||||||
|
});
|
||||||
|
}}>No Buttons</dees-button>
|
||||||
|
|
||||||
|
<dees-button @click=${() => {
|
||||||
|
DeesModal.createAndShow({
|
||||||
|
heading: 'Long Button Labels',
|
||||||
|
content: html`
|
||||||
|
<p>Testing button layout with longer labels.</p>
|
||||||
|
`,
|
||||||
|
menuOptions: [{
|
||||||
|
name: 'Discard All Changes',
|
||||||
|
action: async (modal) => modal.destroy()
|
||||||
|
}, {
|
||||||
|
name: 'Save and Continue Editing',
|
||||||
|
action: async (modal) => modal.destroy()
|
||||||
|
}],
|
||||||
|
});
|
||||||
|
}}>Long Labels</dees-button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="demo-section">
|
||||||
|
<h3>Responsive Behavior</h3>
|
||||||
|
<p>All modals automatically become full-width on mobile devices (< 768px viewport width) for better usability.</p>
|
||||||
|
<dees-button @click=${() => {
|
||||||
|
DeesModal.createAndShow({
|
||||||
|
heading: 'Responsive Modal',
|
||||||
|
width: 'large',
|
||||||
|
showHelpButton: true,
|
||||||
|
onHelp: () => console.log('Help requested for responsive modal'),
|
||||||
|
content: html`
|
||||||
|
<p>Resize your browser window to see how this modal adapts. On mobile viewports, it will automatically take the full width minus margins.</p>
|
||||||
|
<p>The header buttons remain accessible at all viewport sizes.</p>
|
||||||
|
`,
|
||||||
|
menuOptions: [{
|
||||||
|
name: 'Close',
|
||||||
|
action: async (modal) => modal.destroy()
|
||||||
|
}],
|
||||||
|
});
|
||||||
|
}}>Test Responsive</dees-button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
`
|
`
|
@ -1,5 +1,6 @@
|
|||||||
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 { demoFunc } from './dees-modal.demo.js';
|
import { demoFunc } from './dees-modal.demo.js';
|
||||||
import {
|
import {
|
||||||
@ -18,6 +19,7 @@ import {
|
|||||||
|
|
||||||
import * as domtools from '@design.estate/dees-domtools';
|
import * as domtools from '@design.estate/dees-domtools';
|
||||||
import { DeesWindowLayer } from './dees-windowlayer.js';
|
import { DeesWindowLayer } from './dees-windowlayer.js';
|
||||||
|
import './dees-icon.js';
|
||||||
|
|
||||||
declare global {
|
declare global {
|
||||||
interface HTMLElementTagNameMap {
|
interface HTMLElementTagNameMap {
|
||||||
@ -34,12 +36,24 @@ export class DeesModal extends DeesElement {
|
|||||||
heading: string;
|
heading: string;
|
||||||
content: TemplateResult;
|
content: TemplateResult;
|
||||||
menuOptions: plugins.tsclass.website.IMenuItem<DeesModal>[];
|
menuOptions: plugins.tsclass.website.IMenuItem<DeesModal>[];
|
||||||
|
width?: 'small' | 'medium' | 'large' | 'fullscreen' | number;
|
||||||
|
maxWidth?: number;
|
||||||
|
minWidth?: number;
|
||||||
|
showCloseButton?: boolean;
|
||||||
|
showHelpButton?: boolean;
|
||||||
|
onHelp?: () => void | Promise<void>;
|
||||||
}) {
|
}) {
|
||||||
const body = document.body;
|
const body = document.body;
|
||||||
const modal = new DeesModal();
|
const modal = new DeesModal();
|
||||||
modal.heading = optionsArg.heading;
|
modal.heading = optionsArg.heading;
|
||||||
modal.content = optionsArg.content;
|
modal.content = optionsArg.content;
|
||||||
modal.menuOptions = optionsArg.menuOptions;
|
modal.menuOptions = optionsArg.menuOptions;
|
||||||
|
if (optionsArg.width) modal.width = optionsArg.width;
|
||||||
|
if (optionsArg.maxWidth) modal.maxWidth = optionsArg.maxWidth;
|
||||||
|
if (optionsArg.minWidth) modal.minWidth = optionsArg.minWidth;
|
||||||
|
if (optionsArg.showCloseButton !== undefined) modal.showCloseButton = optionsArg.showCloseButton;
|
||||||
|
if (optionsArg.showHelpButton !== undefined) modal.showHelpButton = optionsArg.showHelpButton;
|
||||||
|
if (optionsArg.onHelp) modal.onHelp = optionsArg.onHelp;
|
||||||
modal.windowLayer = await DeesWindowLayer.createAndShow({
|
modal.windowLayer = await DeesWindowLayer.createAndShow({
|
||||||
blur: true,
|
blur: true,
|
||||||
});
|
});
|
||||||
@ -48,6 +62,12 @@ export class DeesModal extends DeesElement {
|
|||||||
});
|
});
|
||||||
body.append(modal.windowLayer);
|
body.append(modal.windowLayer);
|
||||||
body.append(modal);
|
body.append(modal);
|
||||||
|
|
||||||
|
// Get z-index for modal (should be above window layer)
|
||||||
|
modal.modalZIndex = zIndexRegistry.getNextZIndex();
|
||||||
|
zIndexRegistry.register(modal, modal.modalZIndex);
|
||||||
|
|
||||||
|
return modal;
|
||||||
}
|
}
|
||||||
|
|
||||||
// INSTANCE
|
// INSTANCE
|
||||||
@ -63,6 +83,27 @@ export class DeesModal extends DeesElement {
|
|||||||
@state({})
|
@state({})
|
||||||
public menuOptions: plugins.tsclass.website.IMenuItem<DeesModal>[] = [];
|
public menuOptions: plugins.tsclass.website.IMenuItem<DeesModal>[] = [];
|
||||||
|
|
||||||
|
@property({ type: String })
|
||||||
|
public width: 'small' | 'medium' | 'large' | 'fullscreen' | number = 'medium';
|
||||||
|
|
||||||
|
@property({ type: Number })
|
||||||
|
public maxWidth: number;
|
||||||
|
|
||||||
|
@property({ type: Number })
|
||||||
|
public minWidth: number;
|
||||||
|
|
||||||
|
@property({ type: Boolean })
|
||||||
|
public showCloseButton: boolean = true;
|
||||||
|
|
||||||
|
@property({ type: Boolean })
|
||||||
|
public showHelpButton: boolean = false;
|
||||||
|
|
||||||
|
@property({ attribute: false })
|
||||||
|
public onHelp: () => void | Promise<void>;
|
||||||
|
|
||||||
|
@state()
|
||||||
|
private modalZIndex: number = 1000;
|
||||||
|
|
||||||
constructor() {
|
constructor() {
|
||||||
super();
|
super();
|
||||||
}
|
}
|
||||||
@ -85,13 +126,11 @@ export class DeesModal extends DeesElement {
|
|||||||
box-sizing: border-box;
|
box-sizing: border-box;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
justify-content: center;
|
justify-content: center;
|
||||||
z-index: 2000;
|
|
||||||
}
|
}
|
||||||
.modal {
|
.modal {
|
||||||
will-change: transform;
|
will-change: transform;
|
||||||
transform: translateY(0px) scale(0.95);
|
transform: translateY(0px) scale(0.95);
|
||||||
opacity: 0;
|
opacity: 0;
|
||||||
width: 480px;
|
|
||||||
min-height: 120px;
|
min-height: 120px;
|
||||||
background: ${cssManager.bdTheme('#ffffff', '#111')};
|
background: ${cssManager.bdTheme('#ffffff', '#111')};
|
||||||
border-radius: 8px;
|
border-radius: 8px;
|
||||||
@ -99,6 +138,33 @@ export class DeesModal extends DeesElement {
|
|||||||
transition: all 0.2s;
|
transition: all 0.2s;
|
||||||
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: ${cssManager.bdTheme('0px 2px 10px rgba(0, 0, 0, 0.1)', '0px 2px 5px rgba(0, 0, 0, 0.5)')};
|
||||||
|
margin: 20px;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Width variations */
|
||||||
|
.modal.width-small {
|
||||||
|
width: 380px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.modal.width-medium {
|
||||||
|
width: 560px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.modal.width-large {
|
||||||
|
width: 800px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.modal.width-fullscreen {
|
||||||
|
width: calc(100vw - 40px);
|
||||||
|
height: calc(100vh - 40px);
|
||||||
|
max-height: calc(100vh - 40px);
|
||||||
|
}
|
||||||
|
|
||||||
|
@media (max-width: 768px) {
|
||||||
|
.modal {
|
||||||
|
width: calc(100vw - 40px) !important;
|
||||||
|
max-width: none !important;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
.modal.show {
|
.modal.show {
|
||||||
@ -112,13 +178,61 @@ export class DeesModal extends DeesElement {
|
|||||||
}
|
}
|
||||||
|
|
||||||
.modal .heading {
|
.modal .heading {
|
||||||
height: 32px;
|
height: 40px;
|
||||||
font-family: 'Geist Sans', sans-serif;
|
font-family: 'Geist Sans', sans-serif;
|
||||||
line-height: 32px;
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: space-between;
|
||||||
|
padding: 0 12px;
|
||||||
|
border-bottom: 1px solid ${cssManager.bdTheme('#e0e0e0', '#333')};
|
||||||
|
position: relative;
|
||||||
|
}
|
||||||
|
|
||||||
|
.modal .heading .header-buttons {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 4px;
|
||||||
|
position: absolute;
|
||||||
|
right: 8px;
|
||||||
|
top: 50%;
|
||||||
|
transform: translateY(-50%);
|
||||||
|
}
|
||||||
|
|
||||||
|
.modal .heading .header-button {
|
||||||
|
width: 28px;
|
||||||
|
height: 28px;
|
||||||
|
border-radius: 6px;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
cursor: pointer;
|
||||||
|
transition: all 0.2s;
|
||||||
|
background: transparent;
|
||||||
|
color: ${cssManager.bdTheme('#666', '#999')};
|
||||||
|
}
|
||||||
|
|
||||||
|
.modal .heading .header-button:hover {
|
||||||
|
background: ${cssManager.bdTheme('rgba(0, 0, 0, 0.08)', 'rgba(255, 255, 255, 0.08)')};
|
||||||
|
color: ${cssManager.bdTheme('#333', '#fff')};
|
||||||
|
}
|
||||||
|
|
||||||
|
.modal .heading .header-button:active {
|
||||||
|
background: ${cssManager.bdTheme('rgba(0, 0, 0, 0.12)', 'rgba(255, 255, 255, 0.12)')};
|
||||||
|
}
|
||||||
|
|
||||||
|
.modal .heading .header-button dees-icon {
|
||||||
|
width: 16px;
|
||||||
|
height: 16px;
|
||||||
|
display: block;
|
||||||
|
}
|
||||||
|
|
||||||
|
.modal .heading .heading-text {
|
||||||
|
flex: 1;
|
||||||
text-align: center;
|
text-align: center;
|
||||||
font-weight: 600;
|
font-weight: 600;
|
||||||
font-size: 12px;
|
font-size: 14px;
|
||||||
border-bottom: 1px solid ${cssManager.bdTheme('#e0e0e0', '#333')};
|
line-height: 40px;
|
||||||
|
padding: 0 40px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.modal .content {
|
.modal .content {
|
||||||
@ -129,26 +243,22 @@ export class DeesModal extends DeesElement {
|
|||||||
flex-direction: row;
|
flex-direction: row;
|
||||||
border-top: 1px solid ${cssManager.bdTheme('#e0e0e0', '#333')};
|
border-top: 1px solid ${cssManager.bdTheme('#e0e0e0', '#333')};
|
||||||
justify-content: flex-end;
|
justify-content: flex-end;
|
||||||
|
gap: 8px;
|
||||||
|
padding: 8px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.modal .bottomButtons .bottomButton {
|
.modal .bottomButtons .bottomButton {
|
||||||
margin: 8px 0px;
|
padding: 8px 16px;
|
||||||
padding: 8px 12px;
|
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;
|
||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
user-select: none;
|
user-select: none;
|
||||||
transition: all 0.2s;
|
transition: all 0.2s;
|
||||||
background: ${cssManager.bdTheme('rgba(0, 0, 0, 0.05)', 'rgba(255, 255, 255, 0.05)')};
|
background: ${cssManager.bdTheme('rgba(0, 0, 0, 0.05)', 'rgba(255, 255, 255, 0.05)')};
|
||||||
}
|
white-space: nowrap;
|
||||||
|
|
||||||
.modal .bottomButtons .bottomButton:first-child {
|
|
||||||
margin-left: 8px;
|
|
||||||
}
|
|
||||||
.modal .bottomButtons .bottomButton:last-child {
|
|
||||||
margin-right: 8px;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.modal .bottomButtons .bottomButton:hover {
|
.modal .bottomButtons .bottomButton:hover {
|
||||||
@ -177,25 +287,46 @@ export class DeesModal extends DeesElement {
|
|||||||
];
|
];
|
||||||
|
|
||||||
public render(): TemplateResult {
|
public render(): TemplateResult {
|
||||||
|
const widthClass = typeof this.width === 'string' ? `width-${this.width}` : '';
|
||||||
|
const customWidth = typeof this.width === 'number' ? `${this.width}px` : '';
|
||||||
|
const maxWidthStyle = this.maxWidth ? `${this.maxWidth}px` : '';
|
||||||
|
const minWidthStyle = this.minWidth ? `${this.minWidth}px` : '';
|
||||||
|
|
||||||
return html`
|
return html`
|
||||||
<style>
|
<style>
|
||||||
.modal .bottomButtons {
|
${customWidth ? `.modal { width: ${customWidth}; }` : ''}
|
||||||
grid-template-columns: ${cssManager.cssGridColumns(this.menuOptions.length, 0)};
|
${maxWidthStyle ? `.modal { max-width: ${maxWidthStyle}; }` : ''}
|
||||||
}
|
${minWidthStyle ? `.modal { min-width: ${minWidthStyle}; }` : ''}
|
||||||
</style>
|
</style>
|
||||||
<div class="modalContainer" @click=${this.handleOutsideClick}>
|
<div class="modalContainer" @click=${this.handleOutsideClick} style="z-index: ${this.modalZIndex}">
|
||||||
<div class="modal">
|
<div class="modal ${widthClass}">
|
||||||
<div class="heading">${this.heading}</div>
|
<div class="heading">
|
||||||
<div class="content">${this.content}</div>
|
<div class="heading-text">${this.heading}</div>
|
||||||
<div class="bottomButtons">
|
<div class="header-buttons">
|
||||||
${this.menuOptions.map(
|
${this.showHelpButton ? html`
|
||||||
(actionArg, index) => html`
|
<div class="header-button" @click=${this.handleHelp} title="Help">
|
||||||
<div class="bottomButton ${index === this.menuOptions.length - 1 ? 'primary' : ''} ${actionArg.name === 'OK' ? 'ok' : ''}" @click=${() => {
|
<dees-icon .icon=${'lucide:helpCircle'}></dees-icon>
|
||||||
actionArg.action(this);
|
</div>
|
||||||
}}>${actionArg.name}</div>
|
` : ''}
|
||||||
`
|
${this.showCloseButton ? html`
|
||||||
)}
|
<div class="header-button" @click=${() => this.destroy()} title="Close">
|
||||||
|
<dees-icon .icon=${'lucide:x'}></dees-icon>
|
||||||
|
</div>
|
||||||
|
` : ''}
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
<div class="content">${this.content}</div>
|
||||||
|
${this.menuOptions.length > 0 ? html`
|
||||||
|
<div class="bottomButtons">
|
||||||
|
${this.menuOptions.map(
|
||||||
|
(actionArg, index) => html`
|
||||||
|
<div class="bottomButton ${index === this.menuOptions.length - 1 ? 'primary' : ''} ${actionArg.name === 'OK' ? 'ok' : ''}" @click=${() => {
|
||||||
|
actionArg.action(this);
|
||||||
|
}}>${actionArg.name}</div>
|
||||||
|
`
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
` : ''}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
`;
|
`;
|
||||||
@ -225,5 +356,14 @@ export class DeesModal extends DeesElement {
|
|||||||
await domtools.convenience.smartdelay.delayFor(200);
|
await domtools.convenience.smartdelay.delayFor(200);
|
||||||
document.body.removeChild(this);
|
document.body.removeChild(this);
|
||||||
await this.windowLayer.destroy();
|
await this.windowLayer.destroy();
|
||||||
|
|
||||||
|
// Unregister from z-index registry
|
||||||
|
zIndexRegistry.unregister(this);
|
||||||
|
}
|
||||||
|
|
||||||
|
private async handleHelp() {
|
||||||
|
if (this.onHelp) {
|
||||||
|
await this.onHelp();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -1,6 +1,7 @@
|
|||||||
import { customElement, DeesElement, type TemplateResult, html, css, property, cssManager } from '@design.estate/dees-element';
|
import { customElement, DeesElement, type TemplateResult, html, css, property, cssManager } from '@design.estate/dees-element';
|
||||||
|
|
||||||
import * as domtools from '@design.estate/dees-domtools';
|
import * as domtools from '@design.estate/dees-domtools';
|
||||||
|
import { zIndexLayers } from './00zindex.js';
|
||||||
import { demoFunc } from './dees-toast.demo.js';
|
import { demoFunc } from './dees-toast.demo.js';
|
||||||
|
|
||||||
declare global {
|
declare global {
|
||||||
@ -32,7 +33,7 @@ export class DeesToast extends DeesElement {
|
|||||||
container.className = `toast-container toast-container-${position}`;
|
container.className = `toast-container toast-container-${position}`;
|
||||||
container.style.cssText = `
|
container.style.cssText = `
|
||||||
position: fixed;
|
position: fixed;
|
||||||
z-index: 10000;
|
z-index: ${zIndexLayers.overlay.toast};
|
||||||
pointer-events: none;
|
pointer-events: none;
|
||||||
padding: 16px;
|
padding: 16px;
|
||||||
display: flex;
|
display: flex;
|
||||||
@ -105,6 +106,11 @@ export class DeesToast extends DeesElement {
|
|||||||
return toast;
|
return toast;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Alias for consistency with DeesModal
|
||||||
|
public static async createAndShow(options: IToastOptions | string) {
|
||||||
|
return this.show(options);
|
||||||
|
}
|
||||||
|
|
||||||
// Convenience methods
|
// Convenience methods
|
||||||
public static info(message: string, duration?: number) {
|
public static info(message: string, duration?: number) {
|
||||||
return this.show({ message, type: 'info', duration });
|
return this.show({ message, type: 'info', duration });
|
||||||
|
@ -1,4 +1,5 @@
|
|||||||
import { customElement, DeesElement, domtools, type TemplateResult, html, property, type CSSResult, state, } from '@design.estate/dees-element';
|
import { customElement, DeesElement, domtools, type TemplateResult, html, property, type CSSResult, state, } from '@design.estate/dees-element';
|
||||||
|
import { zIndexLayers, zIndexRegistry } from './00zindex.js';
|
||||||
|
|
||||||
declare global {
|
declare global {
|
||||||
interface HTMLElementTagNameMap {
|
interface HTMLElementTagNameMap {
|
||||||
@ -32,6 +33,12 @@ export class DeesWindowLayer extends DeesElement {
|
|||||||
public options: IOptions_DeesWindowLayer = {
|
public options: IOptions_DeesWindowLayer = {
|
||||||
blur: false
|
blur: false
|
||||||
};
|
};
|
||||||
|
|
||||||
|
@state()
|
||||||
|
private backdropZIndex: number = 1000;
|
||||||
|
|
||||||
|
@state()
|
||||||
|
private contentZIndex: number = 1001;
|
||||||
|
|
||||||
// INSTANCE
|
// INSTANCE
|
||||||
@property({
|
@property({
|
||||||
@ -62,7 +69,7 @@ export class DeesWindowLayer extends DeesElement {
|
|||||||
background: rgba(0, 0, 0, 0.0);
|
background: rgba(0, 0, 0, 0.0);
|
||||||
backdrop-filter: brightness(1) ${this.options.blur ? 'blur(0px)' : ''};
|
backdrop-filter: brightness(1) ${this.options.blur ? 'blur(0px)' : ''};
|
||||||
pointer-events: none;
|
pointer-events: none;
|
||||||
z-index: 200;
|
z-index: ${this.backdropZIndex};
|
||||||
}
|
}
|
||||||
.slotContent {
|
.slotContent {
|
||||||
position: fixed;
|
position: fixed;
|
||||||
@ -71,7 +78,12 @@ export class DeesWindowLayer extends DeesElement {
|
|||||||
display: flex;
|
display: flex;
|
||||||
justify-content: center;
|
justify-content: center;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
z-index: 201;
|
z-index: ${this.contentZIndex};
|
||||||
|
pointer-events: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.slotContent > * {
|
||||||
|
pointer-events: auto;
|
||||||
}
|
}
|
||||||
|
|
||||||
.visible {
|
.visible {
|
||||||
@ -80,9 +92,9 @@ export class DeesWindowLayer extends DeesElement {
|
|||||||
pointer-events: all;
|
pointer-events: all;
|
||||||
}
|
}
|
||||||
</style>
|
</style>
|
||||||
<div class="windowOverlay ${this.visible ? 'visible' : null}">
|
<div @click=${this.dispatchClicked} class="windowOverlay ${this.visible ? 'visible' : null}">
|
||||||
</div>
|
</div>
|
||||||
<div @click=${this.dispatchClicked} class="slotContent">
|
<div class="slotContent">
|
||||||
<slot></slot>
|
<slot></slot>
|
||||||
</div>
|
</div>
|
||||||
`;
|
`;
|
||||||
@ -101,9 +113,21 @@ export class DeesWindowLayer extends DeesElement {
|
|||||||
public toggleVisibility () {
|
public toggleVisibility () {
|
||||||
this.visible = !this.visible;
|
this.visible = !this.visible;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public getContentZIndex(): number {
|
||||||
|
return this.contentZIndex;
|
||||||
|
}
|
||||||
|
|
||||||
public async show() {
|
public async show() {
|
||||||
const domtools = await this.domtoolsPromise;
|
const domtools = await this.domtoolsPromise;
|
||||||
|
|
||||||
|
// Get z-indexes from registry
|
||||||
|
this.backdropZIndex = zIndexRegistry.getNextZIndex();
|
||||||
|
this.contentZIndex = zIndexRegistry.getNextZIndex();
|
||||||
|
|
||||||
|
// Register this element
|
||||||
|
zIndexRegistry.register(this, this.backdropZIndex);
|
||||||
|
|
||||||
await domtools.convenience.smartdelay.delayFor(0);
|
await domtools.convenience.smartdelay.delayFor(0);
|
||||||
this.visible = true;
|
this.visible = true;
|
||||||
}
|
}
|
||||||
@ -118,6 +142,10 @@ export class DeesWindowLayer extends DeesElement {
|
|||||||
const domtools = await this.domtoolsPromise;
|
const domtools = await this.domtoolsPromise;
|
||||||
await this.hide();
|
await this.hide();
|
||||||
await domtools.convenience.smartdelay.delayFor(300);
|
await domtools.convenience.smartdelay.delayFor(300);
|
||||||
|
|
||||||
|
// Unregister from z-index registry
|
||||||
|
zIndexRegistry.unregister(this);
|
||||||
|
|
||||||
this.remove();
|
this.remove();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -1,3 +1,4 @@
|
|||||||
|
export * from './00zindex.js';
|
||||||
export * from './dees-appui-activitylog.js';
|
export * from './dees-appui-activitylog.js';
|
||||||
export * from './dees-appui-appbar.js';
|
export * from './dees-appui-appbar.js';
|
||||||
export * from './dees-appui-base.js';
|
export * from './dees-appui-base.js';
|
||||||
@ -36,6 +37,7 @@ export * from './dees-progressbar.js';
|
|||||||
export * from './dees-input-quantityselector.js';
|
export * from './dees-input-quantityselector.js';
|
||||||
export * from './dees-input-radiogroup.js';
|
export * from './dees-input-radiogroup.js';
|
||||||
export * from './dees-input-richtext.js';
|
export * from './dees-input-richtext.js';
|
||||||
|
export * from './dees-input-tags.js';
|
||||||
export * from './dees-input-text.js';
|
export * from './dees-input-text.js';
|
||||||
export * from './dees-label.js';
|
export * from './dees-label.js';
|
||||||
export * from './dees-mobilenavigation.js';
|
export * from './dees-mobilenavigation.js';
|
||||||
|
@ -7,6 +7,7 @@ import {
|
|||||||
css,
|
css,
|
||||||
state,
|
state,
|
||||||
} from '@design.estate/dees-element';
|
} from '@design.estate/dees-element';
|
||||||
|
import { zIndexRegistry } from '../00zindex.js';
|
||||||
|
|
||||||
import { WysiwygFormatting } from './wysiwyg.formatting.js';
|
import { WysiwygFormatting } from './wysiwyg.formatting.js';
|
||||||
|
|
||||||
@ -34,6 +35,9 @@ export class DeesFormattingMenu extends DeesElement {
|
|||||||
@state()
|
@state()
|
||||||
private position: { x: number; y: number } = { x: 0, y: 0 };
|
private position: { x: number; y: number } = { x: 0, y: 0 };
|
||||||
|
|
||||||
|
@state()
|
||||||
|
private menuZIndex: number = 1000;
|
||||||
|
|
||||||
private callback: ((command: string) => void | Promise<void>) | null = null;
|
private callback: ((command: string) => void | Promise<void>) | null = null;
|
||||||
|
|
||||||
public static styles = [
|
public static styles = [
|
||||||
@ -41,7 +45,6 @@ export class DeesFormattingMenu extends DeesElement {
|
|||||||
css`
|
css`
|
||||||
:host {
|
:host {
|
||||||
position: fixed;
|
position: fixed;
|
||||||
z-index: 10000;
|
|
||||||
pointer-events: none;
|
pointer-events: none;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -118,6 +121,9 @@ export class DeesFormattingMenu extends DeesElement {
|
|||||||
render(): TemplateResult {
|
render(): TemplateResult {
|
||||||
if (!this.visible) return html``;
|
if (!this.visible) return html``;
|
||||||
|
|
||||||
|
// Apply z-index to host element
|
||||||
|
this.style.zIndex = this.menuZIndex.toString();
|
||||||
|
|
||||||
return html`
|
return html`
|
||||||
<div
|
<div
|
||||||
class="formatting-menu"
|
class="formatting-menu"
|
||||||
@ -152,6 +158,11 @@ export class DeesFormattingMenu extends DeesElement {
|
|||||||
console.log('FormattingMenu.show called:', { position, visible: this.visible });
|
console.log('FormattingMenu.show called:', { position, visible: this.visible });
|
||||||
this.position = position;
|
this.position = position;
|
||||||
this.callback = callback;
|
this.callback = callback;
|
||||||
|
|
||||||
|
// Get z-index from registry
|
||||||
|
this.menuZIndex = zIndexRegistry.getNextZIndex();
|
||||||
|
zIndexRegistry.register(this, this.menuZIndex);
|
||||||
|
|
||||||
this.visible = true;
|
this.visible = true;
|
||||||
console.log('FormattingMenu.show - visible set to:', this.visible);
|
console.log('FormattingMenu.show - visible set to:', this.visible);
|
||||||
}
|
}
|
||||||
@ -159,6 +170,9 @@ export class DeesFormattingMenu extends DeesElement {
|
|||||||
public hide(): void {
|
public hide(): void {
|
||||||
this.visible = false;
|
this.visible = false;
|
||||||
this.callback = null;
|
this.callback = null;
|
||||||
|
|
||||||
|
// Unregister from z-index registry
|
||||||
|
zIndexRegistry.unregister(this);
|
||||||
}
|
}
|
||||||
|
|
||||||
public updatePosition(position: { x: number; y: number }): void {
|
public updatePosition(position: { x: number; y: number }): void {
|
||||||
|
@ -8,6 +8,7 @@ import {
|
|||||||
css,
|
css,
|
||||||
state,
|
state,
|
||||||
} from '@design.estate/dees-element';
|
} from '@design.estate/dees-element';
|
||||||
|
import { zIndexRegistry } from '../00zindex.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';
|
||||||
@ -42,6 +43,9 @@ export class DeesSlashMenu extends DeesElement {
|
|||||||
@state()
|
@state()
|
||||||
private selectedIndex: number = 0;
|
private selectedIndex: number = 0;
|
||||||
|
|
||||||
|
@state()
|
||||||
|
private menuZIndex: number = 1000;
|
||||||
|
|
||||||
private callback: ((type: string) => void) | null = null;
|
private callback: ((type: string) => void) | null = null;
|
||||||
|
|
||||||
public static styles = [
|
public static styles = [
|
||||||
@ -49,7 +53,6 @@ export class DeesSlashMenu extends DeesElement {
|
|||||||
css`
|
css`
|
||||||
:host {
|
:host {
|
||||||
position: fixed;
|
position: fixed;
|
||||||
z-index: 10000;
|
|
||||||
pointer-events: none;
|
pointer-events: none;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -118,6 +121,9 @@ export class DeesSlashMenu extends DeesElement {
|
|||||||
render(): TemplateResult {
|
render(): TemplateResult {
|
||||||
if (!this.visible) return html``;
|
if (!this.visible) return html``;
|
||||||
|
|
||||||
|
// Apply z-index to host element
|
||||||
|
this.style.zIndex = this.menuZIndex.toString();
|
||||||
|
|
||||||
const menuItems = this.getFilteredMenuItems();
|
const menuItems = this.getFilteredMenuItems();
|
||||||
|
|
||||||
return html`
|
return html`
|
||||||
@ -161,6 +167,11 @@ export class DeesSlashMenu extends DeesElement {
|
|||||||
this.callback = callback;
|
this.callback = callback;
|
||||||
this.filter = '';
|
this.filter = '';
|
||||||
this.selectedIndex = 0;
|
this.selectedIndex = 0;
|
||||||
|
|
||||||
|
// Get z-index from registry
|
||||||
|
this.menuZIndex = zIndexRegistry.getNextZIndex();
|
||||||
|
zIndexRegistry.register(this, this.menuZIndex);
|
||||||
|
|
||||||
this.visible = true;
|
this.visible = true;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -169,6 +180,9 @@ export class DeesSlashMenu extends DeesElement {
|
|||||||
this.callback = null;
|
this.callback = null;
|
||||||
this.filter = '';
|
this.filter = '';
|
||||||
this.selectedIndex = 0;
|
this.selectedIndex = 0;
|
||||||
|
|
||||||
|
// Unregister from z-index registry
|
||||||
|
zIndexRegistry.unregister(this);
|
||||||
}
|
}
|
||||||
|
|
||||||
public updateFilter(filter: string): void {
|
public updateFilter(filter: string): void {
|
||||||
|
@ -1,2 +1,3 @@
|
|||||||
export * from './mainpage.js';
|
export * from './mainpage.js';
|
||||||
export * from './input-showcase.js';
|
export * from './input-showcase.js';
|
||||||
|
export * from './zindex-showcase.js';
|
@ -377,6 +377,29 @@ export const inputShowcase = () => html`
|
|||||||
.placeholder=${'Type and press Enter'}
|
.placeholder=${'Type and press Enter'}
|
||||||
></dees-input-typelist>
|
></dees-input-typelist>
|
||||||
</dees-panel>
|
</dees-panel>
|
||||||
|
|
||||||
|
<dees-panel .title=${'Tags Input'} .subtitle=${'Add and manage tags with suggestions'}>
|
||||||
|
<div class="demo-grid">
|
||||||
|
<dees-input-tags
|
||||||
|
.label=${'Project Tags'}
|
||||||
|
.placeholder=${'Add tags...'}
|
||||||
|
.value=${['frontend', 'typescript', 'webcomponents']}
|
||||||
|
.description=${'Press Enter or comma to add'}
|
||||||
|
></dees-input-tags>
|
||||||
|
|
||||||
|
<dees-input-tags
|
||||||
|
.label=${'Technologies'}
|
||||||
|
.placeholder=${'Type to see suggestions...'}
|
||||||
|
.suggestions=${[
|
||||||
|
'React', 'Vue', 'Angular', 'Svelte',
|
||||||
|
'Node.js', 'Deno', 'Express', 'MongoDB'
|
||||||
|
]}
|
||||||
|
.value=${['React', 'Node.js']}
|
||||||
|
.maxTags=${5}
|
||||||
|
.description=${'Maximum 5 tags, with suggestions'}
|
||||||
|
></dees-input-tags>
|
||||||
|
</div>
|
||||||
|
</dees-panel>
|
||||||
</section>
|
</section>
|
||||||
|
|
||||||
<!-- Numeric Inputs Section -->
|
<!-- Numeric Inputs Section -->
|
||||||
|
795
ts_web/pages/zindex-showcase.ts
Normal file
795
ts_web/pages/zindex-showcase.ts
Normal file
@ -0,0 +1,795 @@
|
|||||||
|
import { html, css, cssManager } from '@design.estate/dees-element';
|
||||||
|
import { DeesModal } from '../elements/dees-modal.js';
|
||||||
|
import { DeesToast } from '../elements/dees-toast.js';
|
||||||
|
import { DeesContextmenu } from '../elements/dees-contextmenu.js';
|
||||||
|
import '../elements/dees-button.js';
|
||||||
|
import '../elements/dees-input-dropdown.js';
|
||||||
|
import '../elements/dees-form.js';
|
||||||
|
import '../elements/dees-panel.js';
|
||||||
|
import '../elements/dees-input-text.js';
|
||||||
|
import '../elements/dees-input-radiogroup.js';
|
||||||
|
import '../elements/dees-input-tags.js';
|
||||||
|
import '../elements/dees-input-wysiwyg.js';
|
||||||
|
import '../elements/dees-appui-profiledropdown.js';
|
||||||
|
|
||||||
|
export const showcasePage = () => html`
|
||||||
|
<style>
|
||||||
|
${css`
|
||||||
|
.page-wrapper {
|
||||||
|
display: block;
|
||||||
|
background: ${cssManager.bdTheme('#f5f5f5', '#0a0a0a')};
|
||||||
|
min-height: 100%;
|
||||||
|
width: 100%;
|
||||||
|
box-sizing: border-box;
|
||||||
|
overflow-x: hidden;
|
||||||
|
overflow-y: auto;
|
||||||
|
}
|
||||||
|
|
||||||
|
.showcase-container {
|
||||||
|
max-width: 1200px;
|
||||||
|
margin: 0 auto;
|
||||||
|
padding: 48px 24px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.showcase-header {
|
||||||
|
text-align: center;
|
||||||
|
margin-bottom: 48px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.showcase-title {
|
||||||
|
font-size: 48px;
|
||||||
|
font-weight: 700;
|
||||||
|
margin: 0 0 16px 0;
|
||||||
|
color: ${cssManager.bdTheme('#1a1a1a', '#ffffff')};
|
||||||
|
}
|
||||||
|
|
||||||
|
.showcase-subtitle {
|
||||||
|
font-size: 20px;
|
||||||
|
color: ${cssManager.bdTheme('#666', '#999')};
|
||||||
|
margin: 0 0 32px 0;
|
||||||
|
line-height: 1.6;
|
||||||
|
}
|
||||||
|
|
||||||
|
.showcase-section {
|
||||||
|
margin-bottom: 48px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.showcase-section:last-child {
|
||||||
|
margin-bottom: 0;
|
||||||
|
padding-bottom: 48px;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Ensure all headings are theme-aware */
|
||||||
|
h1, h2, h3, h4, h5, h6 {
|
||||||
|
color: ${cssManager.bdTheme('#1a1a1a', '#ffffff')};
|
||||||
|
margin: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
p {
|
||||||
|
color: ${cssManager.bdTheme('#666', '#999')};
|
||||||
|
line-height: 1.6;
|
||||||
|
}
|
||||||
|
|
||||||
|
strong {
|
||||||
|
color: ${cssManager.bdTheme('#333', '#e0e0e0')};
|
||||||
|
}
|
||||||
|
|
||||||
|
.section-header {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 16px;
|
||||||
|
margin-bottom: 32px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.section-icon {
|
||||||
|
width: 48px;
|
||||||
|
height: 48px;
|
||||||
|
border-radius: 12px;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
font-size: 24px;
|
||||||
|
background: ${cssManager.bdTheme('#e3f2fd', '#1e3a5f')};
|
||||||
|
}
|
||||||
|
|
||||||
|
.section-icon.layers { background: ${cssManager.bdTheme('#f3e5f5', '#4a148c')}; }
|
||||||
|
.section-icon.registry { background: ${cssManager.bdTheme('#e8f5e9', '#1b5e20')}; }
|
||||||
|
.section-icon.demo { background: ${cssManager.bdTheme('#fff3e0', '#e65100')}; }
|
||||||
|
.section-icon.guidelines { background: ${cssManager.bdTheme('#e0f2f1', '#004d40')}; }
|
||||||
|
|
||||||
|
.section-title {
|
||||||
|
font-size: 32px;
|
||||||
|
font-weight: 600;
|
||||||
|
margin: 0;
|
||||||
|
color: ${cssManager.bdTheme('#1a1a1a', '#ffffff')};
|
||||||
|
}
|
||||||
|
|
||||||
|
.section-description {
|
||||||
|
color: ${cssManager.bdTheme('#666', '#999')};
|
||||||
|
font-size: 16px;
|
||||||
|
line-height: 1.6;
|
||||||
|
margin-bottom: 24px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.demo-grid {
|
||||||
|
display: grid;
|
||||||
|
grid-template-columns: repeat(auto-fit, minmax(300px, 1fr));
|
||||||
|
gap: 24px;
|
||||||
|
margin-bottom: 40px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.demo-card {
|
||||||
|
background: ${cssManager.bdTheme('#fff', '#1a1a1a')};
|
||||||
|
border: 1px solid ${cssManager.bdTheme('#e0e0e0', '#333')};
|
||||||
|
border-radius: 12px;
|
||||||
|
padding: 24px;
|
||||||
|
transition: all 0.2s ease;
|
||||||
|
}
|
||||||
|
|
||||||
|
.demo-card:hover {
|
||||||
|
transform: translateY(-2px);
|
||||||
|
box-shadow: 0 4px 12px ${cssManager.bdTheme('rgba(0,0,0,0.1)', 'rgba(0,0,0,0.3)')};
|
||||||
|
}
|
||||||
|
|
||||||
|
.demo-card h4 {
|
||||||
|
margin-bottom: 16px;
|
||||||
|
font-size: 18px;
|
||||||
|
font-weight: 600;
|
||||||
|
}
|
||||||
|
|
||||||
|
.hierarchy-visual {
|
||||||
|
background: ${cssManager.bdTheme('#fff', '#1a1a1a')};
|
||||||
|
border: 1px solid ${cssManager.bdTheme('#e0e0e0', '#333')};
|
||||||
|
border-radius: 12px;
|
||||||
|
padding: 32px;
|
||||||
|
margin-bottom: 40px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.hierarchy-visual h3 {
|
||||||
|
margin-top: 0;
|
||||||
|
margin-bottom: 24px;
|
||||||
|
font-size: 24px;
|
||||||
|
font-weight: 600;
|
||||||
|
color: ${cssManager.bdTheme('#1a1a1a', '#fff')};
|
||||||
|
}
|
||||||
|
|
||||||
|
.layer-stack {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column-reverse;
|
||||||
|
gap: 8px;
|
||||||
|
margin-top: 16px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.layer {
|
||||||
|
padding: 16px 20px;
|
||||||
|
border-radius: 8px;
|
||||||
|
display: flex;
|
||||||
|
justify-content: space-between;
|
||||||
|
align-items: center;
|
||||||
|
font-family: 'Geist Mono', monospace;
|
||||||
|
font-size: 14px;
|
||||||
|
transition: all 0.2s;
|
||||||
|
border: 1px solid transparent;
|
||||||
|
}
|
||||||
|
|
||||||
|
.layer:hover {
|
||||||
|
transform: translateX(4px);
|
||||||
|
border-color: ${cssManager.bdTheme('#e0e0e0', '#444')};
|
||||||
|
}
|
||||||
|
|
||||||
|
.layer.base {
|
||||||
|
background: ${cssManager.bdTheme('#f0f0f0', '#222')};
|
||||||
|
color: ${cssManager.bdTheme('#666', '#999')};
|
||||||
|
}
|
||||||
|
|
||||||
|
.layer.fixed {
|
||||||
|
background: ${cssManager.bdTheme('#e3f2fd', '#1e3a5f')};
|
||||||
|
color: ${cssManager.bdTheme('#1976d2', '#90caf9')};
|
||||||
|
}
|
||||||
|
|
||||||
|
.layer.dropdown {
|
||||||
|
background: ${cssManager.bdTheme('#f3e5f5', '#4a148c')};
|
||||||
|
color: ${cssManager.bdTheme('#7b1fa2', '#ce93d8')};
|
||||||
|
}
|
||||||
|
|
||||||
|
.layer.modal {
|
||||||
|
background: ${cssManager.bdTheme('#e8f5e9', '#1b5e20')};
|
||||||
|
color: ${cssManager.bdTheme('#388e3c', '#81c784')};
|
||||||
|
}
|
||||||
|
|
||||||
|
.layer.context {
|
||||||
|
background: ${cssManager.bdTheme('#fff3e0', '#e65100')};
|
||||||
|
color: ${cssManager.bdTheme('#f57c00', '#ffb74d')};
|
||||||
|
}
|
||||||
|
|
||||||
|
.layer.toast {
|
||||||
|
background: ${cssManager.bdTheme('#ffebee', '#b71c1c')};
|
||||||
|
color: ${cssManager.bdTheme('#d32f2f', '#ef5350')};
|
||||||
|
}
|
||||||
|
|
||||||
|
.layer-name {
|
||||||
|
font-weight: 600;
|
||||||
|
}
|
||||||
|
|
||||||
|
.layer-value {
|
||||||
|
opacity: 0.8;
|
||||||
|
}
|
||||||
|
|
||||||
|
.warning-box {
|
||||||
|
background: ${cssManager.bdTheme('#fff8e1', '#332701')};
|
||||||
|
border: 1px solid ${cssManager.bdTheme('#ffe082', '#664400')};
|
||||||
|
border-radius: 12px;
|
||||||
|
padding: 20px;
|
||||||
|
margin-bottom: 32px;
|
||||||
|
color: ${cssManager.bdTheme('#f57f17', '#ffecb5')};
|
||||||
|
display: flex;
|
||||||
|
align-items: flex-start;
|
||||||
|
gap: 12px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.warning-box::before {
|
||||||
|
content: '⚠️';
|
||||||
|
font-size: 20px;
|
||||||
|
flex-shrink: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.warning-box strong {
|
||||||
|
color: ${cssManager.bdTheme('#f57f17', '#ffd93d')};
|
||||||
|
}
|
||||||
|
|
||||||
|
code {
|
||||||
|
background: ${cssManager.bdTheme('#f5f5f5', '#2a2a2a')};
|
||||||
|
padding: 2px 6px;
|
||||||
|
border-radius: 3px;
|
||||||
|
font-family: 'Geist Mono', monospace;
|
||||||
|
font-size: 14px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.test-area {
|
||||||
|
position: relative;
|
||||||
|
height: 200px;
|
||||||
|
background: ${cssManager.bdTheme('#fafafa', '#1a1a1a')};
|
||||||
|
border: 2px dashed ${cssManager.bdTheme('#ccc', '#444')};
|
||||||
|
border-radius: 8px;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
margin-top: 16px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.profile-demo {
|
||||||
|
position: relative;
|
||||||
|
display: inline-block;
|
||||||
|
}
|
||||||
|
|
||||||
|
.registry-status {
|
||||||
|
background: ${cssManager.bdTheme('#e8f5e9', '#1a2e1a')};
|
||||||
|
border: 1px solid ${cssManager.bdTheme('#4caf50', '#2e7d32')};
|
||||||
|
border-radius: 12px;
|
||||||
|
padding: 24px;
|
||||||
|
margin-bottom: 32px;
|
||||||
|
position: relative;
|
||||||
|
overflow: hidden;
|
||||||
|
}
|
||||||
|
|
||||||
|
.registry-status::before {
|
||||||
|
content: '';
|
||||||
|
position: absolute;
|
||||||
|
top: 0;
|
||||||
|
left: 0;
|
||||||
|
right: 0;
|
||||||
|
height: 4px;
|
||||||
|
background: ${cssManager.bdTheme('#4caf50', '#2e7d32')};
|
||||||
|
}
|
||||||
|
|
||||||
|
.registry-status h4 {
|
||||||
|
margin-top: 0;
|
||||||
|
margin-bottom: 16px;
|
||||||
|
color: ${cssManager.bdTheme('#2e7d32', '#81c784')};
|
||||||
|
font-size: 18px;
|
||||||
|
font-weight: 600;
|
||||||
|
}
|
||||||
|
|
||||||
|
.registry-item {
|
||||||
|
display: flex;
|
||||||
|
justify-content: space-between;
|
||||||
|
padding: 8px 0;
|
||||||
|
color: ${cssManager.bdTheme('#558b2f', '#aed581')};
|
||||||
|
font-family: 'Geist Mono', monospace;
|
||||||
|
font-size: 14px;
|
||||||
|
border-bottom: 1px solid ${cssManager.bdTheme('#e0f2e1', '#1b5e20')};
|
||||||
|
}
|
||||||
|
|
||||||
|
.registry-item:last-child {
|
||||||
|
border-bottom: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.registry-item.active {
|
||||||
|
color: ${cssManager.bdTheme('#2e7d32', '#4ade80')};
|
||||||
|
font-weight: 600;
|
||||||
|
}
|
||||||
|
|
||||||
|
.registry-item span:last-child {
|
||||||
|
font-weight: 600;
|
||||||
|
}
|
||||||
|
`}
|
||||||
|
</style>
|
||||||
|
|
||||||
|
<div class="page-wrapper">
|
||||||
|
<div class="showcase-container">
|
||||||
|
<div class="showcase-header">
|
||||||
|
<h1 class="showcase-title">Z-Index Management</h1>
|
||||||
|
<p class="showcase-subtitle">
|
||||||
|
A comprehensive system for managing overlay stacking order across all components.
|
||||||
|
Test different scenarios to see how the dynamic z-index registry ensures proper layering.
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="warning-box">
|
||||||
|
<div>
|
||||||
|
<strong>Important:</strong> The z-index values are managed centrally in <code>00zindex.ts</code>.
|
||||||
|
Never use arbitrary z-index values in components - always import and use the z-index registry.
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Registry Status Section -->
|
||||||
|
<div class="showcase-section">
|
||||||
|
<div class="section-header">
|
||||||
|
<div class="section-icon registry">📊</div>
|
||||||
|
<div>
|
||||||
|
<h2 class="section-title">Live Registry Status</h2>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="registry-status" id="registryStatus">
|
||||||
|
<h4>Z-Index Registry</h4>
|
||||||
|
<div class="registry-item">
|
||||||
|
<span>Active Elements:</span>
|
||||||
|
<span id="activeCount">0</span>
|
||||||
|
</div>
|
||||||
|
<div class="registry-item">
|
||||||
|
<span>Current Z-Index:</span>
|
||||||
|
<span id="currentZIndex">1000</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
// Update registry status periodically
|
||||||
|
setInterval(() => {
|
||||||
|
const registryDiv = document.getElementById('registryStatus');
|
||||||
|
if (registryDiv && window.zIndexRegistry) {
|
||||||
|
const activeCount = document.getElementById('activeCount');
|
||||||
|
const currentZIndex = document.getElementById('currentZIndex');
|
||||||
|
|
||||||
|
if (activeCount) activeCount.textContent = window.zIndexRegistry.getActiveCount();
|
||||||
|
if (currentZIndex) currentZIndex.textContent = window.zIndexRegistry.getCurrentZIndex();
|
||||||
|
|
||||||
|
// Update active state
|
||||||
|
const items = registryDiv.querySelectorAll('.registry-item');
|
||||||
|
const count = window.zIndexRegistry.getActiveCount();
|
||||||
|
if (count > 0) {
|
||||||
|
items[0].classList.add('active');
|
||||||
|
} else {
|
||||||
|
items[0].classList.remove('active');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}, 500);
|
||||||
|
|
||||||
|
// Make registry available globally for the demo
|
||||||
|
import('../elements/00zindex.js').then(module => {
|
||||||
|
window.zIndexRegistry = module.zIndexRegistry;
|
||||||
|
});
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<!-- Layer Hierarchy Section -->
|
||||||
|
<div class="showcase-section">
|
||||||
|
<div class="section-header">
|
||||||
|
<div class="section-icon layers">📚</div>
|
||||||
|
<div>
|
||||||
|
<h2 class="section-title">Layer Hierarchy</h2>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<p class="section-description">
|
||||||
|
The traditional z-index layers are still defined for reference, but the new registry system
|
||||||
|
dynamically assigns z-indexes based on creation order.
|
||||||
|
</p>
|
||||||
|
<div class="hierarchy-visual">
|
||||||
|
<h3>Legacy Z-Index Layers (Reference)</h3>
|
||||||
|
<div class="layer-stack">
|
||||||
|
<div class="layer base">
|
||||||
|
<span class="layer-name">Base Content</span>
|
||||||
|
<span class="layer-value">z-index: auto</span>
|
||||||
|
</div>
|
||||||
|
<div class="layer fixed">
|
||||||
|
<span class="layer-name">Fixed Navigation</span>
|
||||||
|
<span class="layer-value">z-index: 10-250</span>
|
||||||
|
</div>
|
||||||
|
<div class="layer dropdown">
|
||||||
|
<span class="layer-name">Dropdown Overlays</span>
|
||||||
|
<span class="layer-value">z-index: 1999-2000</span>
|
||||||
|
</div>
|
||||||
|
<div class="layer modal">
|
||||||
|
<span class="layer-name">Modal Dialogs</span>
|
||||||
|
<span class="layer-value">z-index: 2999-3000</span>
|
||||||
|
</div>
|
||||||
|
<div class="layer context">
|
||||||
|
<span class="layer-name">Context Menus</span>
|
||||||
|
<span class="layer-value">z-index: 4000</span>
|
||||||
|
</div>
|
||||||
|
<div class="layer toast">
|
||||||
|
<span class="layer-name">Toast Notifications</span>
|
||||||
|
<span class="layer-value">z-index: 5000</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Interactive Demos Section -->
|
||||||
|
<div class="showcase-section">
|
||||||
|
<div class="section-header">
|
||||||
|
<div class="section-icon demo">🎮</div>
|
||||||
|
<div>
|
||||||
|
<h2 class="section-title">Interactive Demos</h2>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<p class="section-description">
|
||||||
|
Test the z-index registry in action with these interactive examples. Each element gets the next
|
||||||
|
available z-index when created, ensuring proper stacking order.
|
||||||
|
</p>
|
||||||
|
|
||||||
|
<dees-panel .title=${'Basic Overlay Tests'} .subtitle=${'Test individual overlay components'}>
|
||||||
|
<div class="demo-grid">
|
||||||
|
<div class="demo-card">
|
||||||
|
<h4>Dropdown Test</h4>
|
||||||
|
<dees-input-dropdown
|
||||||
|
.label=${'Select Option'}
|
||||||
|
.options=${[
|
||||||
|
{option: 'Show Toast', key: 'toast', payload: 'toast'},
|
||||||
|
{option: 'Option 2', key: 'opt2', payload: '2'},
|
||||||
|
{option: 'Option 3', key: 'opt3', payload: '3'},
|
||||||
|
{option: 'Option 4', key: 'opt4', payload: '4'},
|
||||||
|
]}
|
||||||
|
@change=${async (e: CustomEvent) => {
|
||||||
|
if (e.detail.value?.payload === 'toast') {
|
||||||
|
DeesToast.createAndShow({ message: 'Toast appears above dropdown!', type: 'success' });
|
||||||
|
}
|
||||||
|
}}
|
||||||
|
></dees-input-dropdown>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="demo-card">
|
||||||
|
<h4>Context Menu Test</h4>
|
||||||
|
<div class="test-area" @contextmenu=${(e: MouseEvent) => {
|
||||||
|
DeesContextmenu.openContextMenuWithOptions(e, [
|
||||||
|
{ name: 'Show Toast', iconName: 'bell', action: async () => {
|
||||||
|
DeesToast.createAndShow({ message: 'Toast from context menu!', type: 'info' });
|
||||||
|
}},
|
||||||
|
{ divider: true },
|
||||||
|
{ name: 'Item 2', iconName: 'check', action: async () => {} },
|
||||||
|
{ name: 'Item 3', iconName: 'copy', action: async () => {} },
|
||||||
|
]);
|
||||||
|
}}>
|
||||||
|
<span style="color: ${cssManager.bdTheme('#999', '#666')}">Right-click here for context menu</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="demo-card">
|
||||||
|
<h4>Toast Notification</h4>
|
||||||
|
<dees-button @click=${async () => {
|
||||||
|
DeesToast.createAndShow({ message: 'I appear on top of everything!', type: 'success' });
|
||||||
|
}}>Show Toast</dees-button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</dees-panel>
|
||||||
|
|
||||||
|
<dees-panel .title=${'Modal with Dropdown'} .subtitle=${'Critical test: Dropdown inside modal should appear above modal'}>
|
||||||
|
<p>This tests the most common z-index conflict scenario.</p>
|
||||||
|
<dees-button @click=${async () => {
|
||||||
|
const modal = await DeesModal.createAndShow({
|
||||||
|
heading: 'Modal with Dropdown',
|
||||||
|
width: 'medium',
|
||||||
|
showHelpButton: true,
|
||||||
|
onHelp: async () => {
|
||||||
|
DeesToast.createAndShow({ message: 'Help requested! Toast appears above modal.', type: 'info' });
|
||||||
|
},
|
||||||
|
content: html`
|
||||||
|
<p>The dropdown below should appear <strong>above</strong> this modal:</p>
|
||||||
|
<dees-form>
|
||||||
|
<dees-input-dropdown
|
||||||
|
.label=${'Select Country'}
|
||||||
|
.options=${[
|
||||||
|
{option: 'United States', key: 'us', payload: 'US'},
|
||||||
|
{option: 'Canada', key: 'ca', payload: 'CA'},
|
||||||
|
{option: 'United Kingdom', key: 'uk', payload: 'UK'},
|
||||||
|
{option: 'Germany', key: 'de', payload: 'DE'},
|
||||||
|
{option: 'France', key: 'fr', payload: 'FR'},
|
||||||
|
{option: 'Japan', key: 'jp', payload: 'JP'},
|
||||||
|
{option: 'Australia', key: 'au', payload: 'AU'},
|
||||||
|
{option: 'Brazil', key: 'br', payload: 'BR'},
|
||||||
|
]}
|
||||||
|
.required=${true}
|
||||||
|
></dees-input-dropdown>
|
||||||
|
|
||||||
|
<dees-input-text
|
||||||
|
.label=${'Additional Field'}
|
||||||
|
.placeholder=${'Just to show form context'}
|
||||||
|
></dees-input-text>
|
||||||
|
|
||||||
|
<dees-input-tags
|
||||||
|
.label=${'Tags'}
|
||||||
|
.placeholder=${'Add tags...'}
|
||||||
|
.suggestions=${['urgent', 'bug', 'feature', 'documentation', 'testing']}
|
||||||
|
.description=${'Add relevant tags'}
|
||||||
|
></dees-input-tags>
|
||||||
|
</dees-form>
|
||||||
|
<p style="margin-top: 16px; color: ${cssManager.bdTheme('#666', '#999')}">
|
||||||
|
You can also right-click anywhere in this modal to test context menus.
|
||||||
|
</p>
|
||||||
|
`,
|
||||||
|
menuOptions: [
|
||||||
|
{ name: 'Cancel', action: async (modal) => modal.destroy() },
|
||||||
|
{ name: 'Save', action: async (modal) => modal.destroy() }
|
||||||
|
]
|
||||||
|
});
|
||||||
|
|
||||||
|
// Add context menu to modal content
|
||||||
|
const modalContent = modal.shadowRoot.querySelector('.modal .content');
|
||||||
|
if (modalContent) {
|
||||||
|
modalContent.addEventListener('contextmenu', async (e: MouseEvent) => {
|
||||||
|
DeesContextmenu.openContextMenuWithOptions(e, [
|
||||||
|
{ name: 'Context menu in modal', iconName: 'check', action: async () => {} },
|
||||||
|
{ divider: true },
|
||||||
|
{ name: 'Show Toast', iconName: 'bell', action: async () => {
|
||||||
|
DeesToast.createAndShow({ message: 'Toast from modal context menu!', type: 'warning' });
|
||||||
|
}}
|
||||||
|
]);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}}>Open Modal with Dropdown</dees-button>
|
||||||
|
</dees-panel>
|
||||||
|
|
||||||
|
<dees-panel .title=${'Complex Stacking Scenario'} .subtitle=${'Multiple overlays active simultaneously'}>
|
||||||
|
<p>This creates a complex scenario with multiple overlays to test the complete hierarchy.</p>
|
||||||
|
<dees-button @click=${async () => {
|
||||||
|
// Show base modal
|
||||||
|
await DeesModal.createAndShow({
|
||||||
|
heading: 'Base Modal',
|
||||||
|
width: 'large',
|
||||||
|
content: html`
|
||||||
|
<h4>Level 1: Modal</h4>
|
||||||
|
<p>This is the base modal. Try the following:</p>
|
||||||
|
<ol>
|
||||||
|
<li>Open the dropdown below</li>
|
||||||
|
<li>Right-click for context menu</li>
|
||||||
|
<li>Click "Show Second Modal" to stack modals</li>
|
||||||
|
</ol>
|
||||||
|
|
||||||
|
<dees-input-dropdown
|
||||||
|
.label=${'Test Dropdown in Modal'}
|
||||||
|
.options=${[
|
||||||
|
{option: 'Trigger Toast', key: 'toast', payload: 'toast'},
|
||||||
|
{option: 'Option 2', key: 'opt2', payload: '2'},
|
||||||
|
{option: 'Option 3', key: 'opt3', payload: '3'},
|
||||||
|
]}
|
||||||
|
@change=${async (e: CustomEvent) => {
|
||||||
|
if (e.detail.value?.payload === 'toast') {
|
||||||
|
DeesToast.createAndShow({ message: 'Toast triggered from dropdown in modal!', type: 'success' });
|
||||||
|
}
|
||||||
|
}}
|
||||||
|
></dees-input-dropdown>
|
||||||
|
|
||||||
|
<div style="margin-top: 16px;">
|
||||||
|
<dees-button @click=${async () => {
|
||||||
|
await DeesModal.createAndShow({
|
||||||
|
heading: 'Second Modal',
|
||||||
|
width: 'small',
|
||||||
|
content: html`
|
||||||
|
<h4>Level 2: Stacked Modal</h4>
|
||||||
|
<p>This modal appears on top of the first one.</p>
|
||||||
|
<p>The dropdown here should still work:</p>
|
||||||
|
<dees-input-dropdown
|
||||||
|
.label=${'Nested Dropdown'}
|
||||||
|
.options=${[
|
||||||
|
{option: 'Option A', key: 'a'},
|
||||||
|
{option: 'Option B', key: 'b'},
|
||||||
|
{option: 'Option C', key: 'c'},
|
||||||
|
]}
|
||||||
|
></dees-input-dropdown>
|
||||||
|
`,
|
||||||
|
menuOptions: [
|
||||||
|
{ name: 'Close', action: async (modal) => modal.destroy() }
|
||||||
|
]
|
||||||
|
});
|
||||||
|
}}>Show Second Modal</dees-button>
|
||||||
|
</div>
|
||||||
|
`,
|
||||||
|
menuOptions: [
|
||||||
|
{ name: 'Close All', action: async (modal) => {
|
||||||
|
modal.destroy();
|
||||||
|
// Also show a toast
|
||||||
|
DeesToast.createAndShow({ message: 'All modals closed!', type: 'info' });
|
||||||
|
}}
|
||||||
|
]
|
||||||
|
});
|
||||||
|
}}>Start Complex Stack Test</dees-button>
|
||||||
|
</dees-panel>
|
||||||
|
|
||||||
|
<dees-panel .title=${'Profile Dropdown'} .subtitle=${'Testing app UI dropdowns'}>
|
||||||
|
<p>Profile dropdowns and similar UI elements use the dropdown z-index layer.</p>
|
||||||
|
<div class="profile-demo">
|
||||||
|
<dees-appui-profiledropdown
|
||||||
|
.user=${{
|
||||||
|
name: 'Test User',
|
||||||
|
email: 'test@example.com',
|
||||||
|
avatar: 'https://randomuser.me/api/portraits/lego/1.jpg',
|
||||||
|
status: 'online' as const
|
||||||
|
}}
|
||||||
|
.menuItems=${[
|
||||||
|
{ name: 'Show Toast', iconName: 'bell', shortcut: '', action: async () => {
|
||||||
|
DeesToast.createAndShow({ message: 'Profile action triggered!', type: 'success' });
|
||||||
|
}},
|
||||||
|
{ divider: true } as const,
|
||||||
|
{ name: 'Settings', iconName: 'settings', shortcut: '', action: async () => {} },
|
||||||
|
{ name: 'Logout', iconName: 'logOut', shortcut: '', action: async () => {} }
|
||||||
|
]}
|
||||||
|
></dees-appui-profiledropdown>
|
||||||
|
</div>
|
||||||
|
</dees-panel>
|
||||||
|
|
||||||
|
<dees-panel .title=${'Edge Cases'} .subtitle=${'Special scenarios and gotchas'}>
|
||||||
|
<div class="demo-grid">
|
||||||
|
<div class="demo-card">
|
||||||
|
<h4>Multiple Toasts</h4>
|
||||||
|
<dees-button @click=${async () => {
|
||||||
|
DeesToast.createAndShow({ message: 'First toast', type: 'info' });
|
||||||
|
setTimeout(() => {
|
||||||
|
DeesToast.createAndShow({ message: 'Second toast', type: 'warning' });
|
||||||
|
}, 500);
|
||||||
|
setTimeout(() => {
|
||||||
|
DeesToast.createAndShow({ message: 'Third toast', type: 'success' });
|
||||||
|
}, 1000);
|
||||||
|
}}>Show Multiple Toasts</dees-button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="demo-card">
|
||||||
|
<h4>Modal with WYSIWYG Editor</h4>
|
||||||
|
<dees-button @click=${async () => {
|
||||||
|
await DeesModal.createAndShow({
|
||||||
|
heading: 'WYSIWYG Editor Test',
|
||||||
|
width: 'large',
|
||||||
|
content: html`
|
||||||
|
<p>Test the WYSIWYG editor slash commands and formatting menus in a modal:</p>
|
||||||
|
<dees-form>
|
||||||
|
<dees-input-wysiwyg
|
||||||
|
.label=${'Document Content'}
|
||||||
|
.placeholder=${'Type "/" to see slash commands or select text to format...'}
|
||||||
|
.outputFormat=${'html'}
|
||||||
|
.description=${'The slash menu and formatting menu should appear above this modal'}
|
||||||
|
></dees-input-wysiwyg>
|
||||||
|
</dees-form>
|
||||||
|
<p style="margin-top: 16px; color: ${cssManager.bdTheme('#666', '#999')}">
|
||||||
|
<strong>Tips:</strong><br>
|
||||||
|
• Type "/" to open the slash command menu<br>
|
||||||
|
• Select text to see the formatting toolbar<br>
|
||||||
|
• Both menus should appear above this modal
|
||||||
|
</p>
|
||||||
|
`,
|
||||||
|
menuOptions: [
|
||||||
|
{ name: 'Cancel', action: async (modal) => modal.destroy() },
|
||||||
|
{ name: 'Save', action: async (modal) => {
|
||||||
|
DeesToast.createAndShow({ message: 'Document saved!', type: 'success' });
|
||||||
|
modal.destroy();
|
||||||
|
}}
|
||||||
|
]
|
||||||
|
});
|
||||||
|
}}>Test WYSIWYG in Modal</dees-button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="demo-card">
|
||||||
|
<h4>Modal with Tags Input</h4>
|
||||||
|
<dees-button @click=${async () => {
|
||||||
|
await DeesModal.createAndShow({
|
||||||
|
heading: 'Tags Input Test',
|
||||||
|
width: 'medium',
|
||||||
|
content: html`
|
||||||
|
<p>Test the tags input component in a modal:</p>
|
||||||
|
<dees-form>
|
||||||
|
<dees-input-tags
|
||||||
|
.label=${'Search Terms'}
|
||||||
|
.placeholder=${'Enter search terms...'}
|
||||||
|
.value=${['typescript', 'modal']}
|
||||||
|
.suggestions=${[
|
||||||
|
'javascript', 'typescript', 'css', 'html',
|
||||||
|
'react', 'vue', 'angular', 'svelte',
|
||||||
|
'modal', 'dropdown', 'form', 'input'
|
||||||
|
]}
|
||||||
|
.description=${'Add search terms to filter results'}
|
||||||
|
></dees-input-tags>
|
||||||
|
|
||||||
|
<dees-input-tags
|
||||||
|
.label=${'Categories'}
|
||||||
|
.placeholder=${'Add categories...'}
|
||||||
|
.required=${true}
|
||||||
|
.maxTags=${3}
|
||||||
|
.description=${'Select up to 3 categories'}
|
||||||
|
></dees-input-tags>
|
||||||
|
</dees-form>
|
||||||
|
`,
|
||||||
|
menuOptions: [
|
||||||
|
{ name: 'Cancel', action: async (modal) => modal.destroy() },
|
||||||
|
{ name: 'Apply', action: async (modal) => {
|
||||||
|
DeesToast.createAndShow({ message: 'Tags applied!', type: 'success' });
|
||||||
|
modal.destroy();
|
||||||
|
}}
|
||||||
|
]
|
||||||
|
});
|
||||||
|
}}>Test Tags in Modal</dees-button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="demo-card">
|
||||||
|
<h4>Fullscreen Modal</h4>
|
||||||
|
<dees-button @click=${async () => {
|
||||||
|
await DeesModal.createAndShow({
|
||||||
|
heading: 'Fullscreen Modal Test',
|
||||||
|
width: 'fullscreen',
|
||||||
|
content: html`
|
||||||
|
<p>Even in fullscreen, overlays should work properly:</p>
|
||||||
|
<dees-input-radiogroup
|
||||||
|
.label=${'Select Option'}
|
||||||
|
.options=${['Option 1', 'Option 2', 'Option 3']}
|
||||||
|
></dees-input-radiogroup>
|
||||||
|
<dees-input-dropdown
|
||||||
|
.label=${'Dropdown in Fullscreen'}
|
||||||
|
.options=${[
|
||||||
|
{option: 'Works properly', key: '1'},
|
||||||
|
{option: 'Above modal', key: '2'},
|
||||||
|
]}
|
||||||
|
></dees-input-dropdown>
|
||||||
|
`,
|
||||||
|
menuOptions: [
|
||||||
|
{ name: 'Exit Fullscreen', action: async (modal) => modal.destroy() }
|
||||||
|
]
|
||||||
|
});
|
||||||
|
}}>Open Fullscreen</dees-button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</dees-panel>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Guidelines Section -->
|
||||||
|
<div class="showcase-section">
|
||||||
|
<div class="section-header">
|
||||||
|
<div class="section-icon guidelines">📖</div>
|
||||||
|
<div>
|
||||||
|
<h2 class="section-title">Usage Guidelines</h2>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<dees-panel>
|
||||||
|
<h4>Best Practices:</h4>
|
||||||
|
<ul>
|
||||||
|
<li>Always use the z-index registry from <code>00zindex.ts</code></li>
|
||||||
|
<li>Never use arbitrary z-index values like <code>z-index: 9999</code></li>
|
||||||
|
<li>Get z-index from registry when showing elements: <code>zIndexRegistry.getNextZIndex()</code></li>
|
||||||
|
<li>Register elements to track them: <code>zIndexRegistry.register(element, zIndex)</code></li>
|
||||||
|
<li>Unregister on cleanup: <code>zIndexRegistry.unregister(element)</code></li>
|
||||||
|
<li>Elements created later automatically appear on top</li>
|
||||||
|
<li>Test overlay interactions, especially dropdowns in modals</li>
|
||||||
|
<li>WYSIWYG menus (slash commands, formatting) now use dynamic z-index</li>
|
||||||
|
</ul>
|
||||||
|
|
||||||
|
<h4>Import Example:</h4>
|
||||||
|
<pre style="background: ${cssManager.bdTheme('#f5f5f5', '#2a2a2a')}; padding: 16px; border-radius: 6px; overflow-x: auto;">
|
||||||
|
<code>import { zIndexRegistry } from './00zindex.js';
|
||||||
|
|
||||||
|
// In your component:
|
||||||
|
const myZIndex = zIndexRegistry.getNextZIndex();
|
||||||
|
element.style.zIndex = myZIndex.toString();
|
||||||
|
zIndexRegistry.register(element, myZIndex);
|
||||||
|
|
||||||
|
// On cleanup:
|
||||||
|
zIndexRegistry.unregister(element);</code></pre>
|
||||||
|
</dees-panel>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
`;
|
Reference in New Issue
Block a user