Compare commits

...

10 Commits

Author SHA1 Message Date
b31f306106 1.9.6
Some checks failed
Default (tags) / security (push) Failing after 36s
Default (tags) / test (push) Failing after 29s
Default (tags) / release (push) Has been skipped
Default (tags) / metadata (push) Has been skipped
2025-06-26 19:00:57 +00:00
1dbbac450c update dees-tags 2025-06-26 19:00:15 +00:00
b5a2bd7436 fix zindex 2025-06-26 18:37:49 +00:00
931a760ee1 update z-index showcase 2025-06-26 18:13:08 +00:00
27414e0284 1.9.5
Some checks failed
Default (tags) / security (push) Failing after 39s
Default (tags) / test (push) Failing after 31s
Default (tags) / release (push) Has been skipped
Default (tags) / metadata (push) Has been skipped
2025-06-26 15:57:43 +00:00
d63bc762d0 update 2025-06-26 15:57:27 +00:00
505e40a57f update modals 2025-06-26 15:51:05 +00:00
d1ea10d8c6 update z-index use 2025-06-26 15:46:44 +00:00
1038759d8b 1.9.4
Some checks failed
Default (tags) / security (push) Failing after 40s
Default (tags) / test (push) Failing after 28s
Default (tags) / release (push) Has been skipped
Default (tags) / metadata (push) Has been skipped
2025-06-26 15:33:18 +00:00
ab9b545c9a update file upload 2025-06-26 15:32:29 +00:00
22 changed files with 2852 additions and 194 deletions

View File

@ -1,6 +1,6 @@
{ {
"name": "@design.estate/dees-catalog", "name": "@design.estate/dees-catalog",
"version": "1.9.3", "version": "1.9.6",
"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",

View File

@ -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.

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

@ -0,0 +1,161 @@
/**
* Central z-index management for consistent stacking order
* Higher numbers appear on top of lower numbers
*/
export const zIndexLayers = {
// Base layer: Regular content
base: {
content: 'auto',
inputElements: 1,
},
// Fixed UI elements
fixed: {
appBar: 10,
sideMenu: 10,
mobileNav: 250,
},
// Overlay backdrops (semi-transparent backgrounds)
backdrop: {
dropdown: 1999, // Below modals but above fixed elements
modal: 2999, // Below dropdowns on modals
contextMenu: 3999, // Below critical overlays
},
// Interactive overlays
overlay: {
dropdown: 2000, // Dropdowns and select menus
modal: 3000, // Modal dialogs
contextMenu: 4000, // Context menus and tooltips
toast: 5000, // Toast notifications (highest priority)
},
// Special cases for nested elements
modalDropdown: 3500, // Dropdowns inside modals
wysiwygMenus: 4500, // Editor formatting menus
} as const;
// Helper function to get z-index value
export function getZIndex(category: keyof typeof zIndexLayers, subcategory?: string): number | string {
const categoryObj = zIndexLayers[category];
if (typeof categoryObj === 'object' && subcategory) {
return categoryObj[subcategory as keyof typeof categoryObj] || 'auto';
}
return typeof categoryObj === 'number' ? categoryObj : 'auto';
}
// Z-index assignments for components
export const componentZIndex = {
'dees-modal': zIndexLayers.overlay.modal,
'dees-windowlayer': zIndexLayers.overlay.dropdown,
'dees-contextmenu': zIndexLayers.overlay.contextMenu,
'dees-toast': zIndexLayers.overlay.toast,
'dees-appui-mainmenu': zIndexLayers.fixed.appBar,
'dees-mobilenavigation': zIndexLayers.fixed.mobileNav,
'dees-slash-menu': zIndexLayers.wysiwygMenus,
'dees-formatting-menu': zIndexLayers.wysiwygMenus,
} as const;
/**
* Z-Index Registry for managing stacked elements
* Simple incremental z-index assignment based on creation order
*/
export class ZIndexRegistry {
private static instance: ZIndexRegistry;
private activeElements = new Set<HTMLElement>();
private elementZIndexMap = new WeakMap<HTMLElement, number>();
private currentZIndex = 1000; // Starting z-index
private constructor() {}
public static getInstance(): ZIndexRegistry {
if (!ZIndexRegistry.instance) {
ZIndexRegistry.instance = new ZIndexRegistry();
}
return ZIndexRegistry.instance;
}
/**
* Get the next available z-index
* @returns The next available z-index
*/
public getNextZIndex(): number {
this.currentZIndex += 10;
return this.currentZIndex;
}
/**
* Register an element with the z-index registry
* @param element - The HTML element to register
* @param zIndex - The z-index assigned to this element
*/
public register(element: HTMLElement, zIndex: number): void {
this.activeElements.add(element);
this.elementZIndexMap.set(element, zIndex);
}
/**
* Unregister an element from the z-index registry
* @param element - The HTML element to unregister
*/
public unregister(element: HTMLElement): void {
this.activeElements.delete(element);
this.elementZIndexMap.delete(element);
// If no more active elements, reset counter to base
if (this.activeElements.size === 0) {
this.currentZIndex = 1000;
}
}
/**
* Get the z-index for a specific element
* @param element - The HTML element
* @returns The z-index or undefined if not registered
*/
public getElementZIndex(element: HTMLElement): number | undefined {
return this.elementZIndexMap.get(element);
}
/**
* Get count of active elements
* @returns Number of active elements
*/
public getActiveCount(): number {
return this.activeElements.size;
}
/**
* Get the current highest z-index
* @returns The current z-index value
*/
public getCurrentZIndex(): number {
return this.currentZIndex;
}
/**
* Clear all registrations (useful for testing)
*/
public clear(): void {
this.activeElements.clear();
this.elementZIndexMap = new WeakMap();
this.currentZIndex = 1000;
}
/**
* Get all active elements in z-index order
* @returns Array of elements sorted by z-index
*/
public getActiveElementsInOrder(): HTMLElement[] {
return Array.from(this.activeElements).sort((a, b) => {
const aZ = this.elementZIndexMap.get(a) || 0;
const bZ = this.elementZIndexMap.get(b) || 0;
return aZ - bZ;
});
}
}
// Export singleton instance for convenience
export const zIndexRegistry = ZIndexRegistry.getInstance();

View File

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

View File

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

View File

@ -14,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;

View File

@ -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(
@ -296,9 +301,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 +332,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';
} }
} }

View File

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

View File

@ -42,6 +42,21 @@ export class DeesInputFileupload extends DeesInputBase<DeesInputFileupload> {
}) })
public buttonText: string = 'Upload File...'; public buttonText: string = 'Upload File...';
@property({ type: String })
public accept: string = '';
@property({ type: Boolean })
public multiple: boolean = true;
@property({ type: Number })
public maxSize: number = 0; // 0 means no limit
@property({ type: Number })
public maxFiles: number = 0; // 0 means no limit
@property({ type: String, reflect: true })
public validationState: 'valid' | 'invalid' | 'warn' | 'pending' = null;
constructor() { constructor() {
super(); super();
} }
@ -52,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();
}
} }
} }

View File

@ -0,0 +1,248 @@
import { html, css } from '@design.estate/dees-element';
import '@design.estate/dees-wcctools/demotools';
import './dees-panel.js';
export const demoFunc = () => html`
<dees-demowrapper>
<style>
${css`
.demo-container {
display: flex;
flex-direction: column;
gap: 24px;
padding: 24px;
max-width: 1200px;
margin: 0 auto;
}
dees-panel {
margin-bottom: 24px;
}
dees-panel:last-child {
margin-bottom: 0;
}
.grid-layout {
display: grid;
grid-template-columns: 1fr 1fr;
gap: 16px;
}
@media (max-width: 768px) {
.grid-layout {
grid-template-columns: 1fr;
}
}
.output-preview {
margin-top: 16px;
padding: 16px;
background: #f3f4f6;
border-radius: 4px;
font-size: 12px;
color: #374151;
word-break: break-all;
max-height: 200px;
overflow-y: auto;
}
@media (prefers-color-scheme: dark) {
.output-preview {
background: #2c2c2c;
color: #e4e4e7;
}
}
.tag-preview {
display: flex;
flex-wrap: wrap;
gap: 8px;
padding: 12px;
background: #f9fafb;
border-radius: 4px;
min-height: 40px;
align-items: center;
}
@media (prefers-color-scheme: dark) {
.tag-preview {
background: #1f2937;
}
}
.tag-preview-item {
display: inline-block;
padding: 4px 12px;
background: #e0e7ff;
color: #4338ca;
border-radius: 12px;
font-size: 14px;
}
@media (prefers-color-scheme: dark) {
.tag-preview-item {
background: #312e81;
color: #c7d2fe;
}
}
`}
</style>
<div class="demo-container">
<dees-panel .title=${'1. Basic Tags Input'} .subtitle=${'Simple tag input with common programming languages'}>
<dees-input-tags
.label=${'Programming Languages'}
.placeholder=${'Add a language...'}
.value=${['JavaScript', 'TypeScript', 'Python', 'Go']}
.description=${'Press Enter or comma to add tags'}
></dees-input-tags>
</dees-panel>
<dees-panel .title=${'2. Tags with Suggestions'} .subtitle=${'Auto-complete suggestions for faster input'}>
<dees-input-tags
.label=${'Tech Stack'}
.placeholder=${'Type to see suggestions...'}
.suggestions=${[
'React', 'Vue', 'Angular', 'Svelte', 'Lit', 'Next.js', 'Nuxt', 'SvelteKit',
'Node.js', 'Deno', 'Bun', 'Express', 'Fastify', 'Nest.js', 'Koa',
'MongoDB', 'PostgreSQL', 'Redis', 'MySQL', 'SQLite', 'Cassandra',
'Docker', 'Kubernetes', 'AWS', 'Azure', 'GCP', 'Vercel', 'Netlify'
]}
.value=${['React', 'Node.js', 'PostgreSQL', 'Docker']}
.description=${'Start typing to see suggestions from popular technologies'}
></dees-input-tags>
</dees-panel>
<dees-panel .title=${'3. Limited Tags'} .subtitle=${'Restrict the number of tags users can add'}>
<div class="grid-layout">
<dees-input-tags
.label=${'Top 3 Skills'}
.placeholder=${'Add up to 3 skills...'}
.maxTags=${3}
.value=${['Design', 'Development']}
.description=${'Maximum 3 tags allowed'}
></dees-input-tags>
<dees-input-tags
.label=${'Categories (Max 5)'}
.placeholder=${'Select categories...'}
.maxTags=${5}
.suggestions=${['Blog', 'Tutorial', 'News', 'Review', 'Guide', 'Case Study', 'Interview']}
.value=${['Tutorial', 'Guide']}
.description=${'Choose up to 5 categories'}
></dees-input-tags>
</div>
</dees-panel>
<dees-panel .title=${'4. Required & Validation'} .subtitle=${'Tags input with validation requirements'}>
<dees-input-tags
.label=${'Project Tags'}
.placeholder=${'Add at least one tag...'}
.required=${true}
.description=${'This field is required - add at least one tag'}
></dees-input-tags>
</dees-panel>
<dees-panel .title=${'5. Disabled State'} .subtitle=${'Read-only tags display'}>
<dees-input-tags
.label=${'System Tags'}
.value=${['System', 'Protected', 'Read-Only', 'Archive']}
.disabled=${true}
.description=${'These tags cannot be modified'}
></dees-input-tags>
</dees-panel>
<dees-panel .title=${'6. Form Integration'} .subtitle=${'Tags input working within a form context'}>
<dees-form>
<dees-input-text
.label=${'Project Name'}
.placeholder=${'My Awesome Project'}
.required=${true}
.key=${'name'}
></dees-input-text>
<div class="grid-layout">
<dees-input-tags
.label=${'Technologies Used'}
.placeholder=${'Add technologies...'}
.required=${true}
.key=${'technologies'}
.suggestions=${[
'TypeScript', 'JavaScript', 'Python', 'Go', 'Rust',
'React', 'Vue', 'Angular', 'Svelte',
'Node.js', 'Deno', 'Express', 'FastAPI'
]}
></dees-input-tags>
<dees-input-tags
.label=${'Project Tags'}
.placeholder=${'Add descriptive tags...'}
.key=${'tags'}
.maxTags=${10}
.suggestions=${[
'frontend', 'backend', 'fullstack', 'mobile', 'desktop',
'web', 'api', 'database', 'devops', 'ui/ux',
'opensource', 'saas', 'enterprise', 'startup'
]}
></dees-input-tags>
</div>
<dees-input-text
.label=${'Description'}
.inputType=${'textarea'}
.placeholder=${'Describe your project...'}
.key=${'description'}
></dees-input-text>
<dees-form-submit .text=${'Create Project'}></dees-form-submit>
</dees-form>
</dees-panel>
<dees-panel .title=${'7. Interactive Demo'} .subtitle=${'Add tags and see them collected in real-time'}>
<dees-input-tags
id="interactive-tags"
.label=${'Your Interests'}
.placeholder=${'Type your interests...'}
.suggestions=${[
'Music', 'Movies', 'Books', 'Travel', 'Photography',
'Cooking', 'Gaming', 'Sports', 'Art', 'Technology',
'Fashion', 'Fitness', 'Nature', 'Science', 'History'
]}
@change=${(e: CustomEvent) => {
const preview = document.querySelector('#tags-preview');
const tags = e.detail.value;
if (preview) {
if (tags.length === 0) {
preview.innerHTML = '<em style="color: #999;">No tags added yet...</em>';
} else {
preview.innerHTML = tags.map((tag: string) =>
`<span class="tag-preview-item">${tag}</span>`
).join('');
}
}
}}
></dees-input-tags>
<div class="tag-preview" id="tags-preview">
<em style="color: #999;">No tags added yet...</em>
</div>
<div class="output-preview" id="tags-json">
<em>JSON output will appear here...</em>
</div>
<script>
// Update JSON preview
const tagsInput = document.querySelector('#interactive-tags');
tagsInput?.addEventListener('change', (e) => {
const jsonPreview = document.querySelector('#tags-json');
if (jsonPreview) {
jsonPreview.textContent = JSON.stringify(e.detail.value, null, 2);
}
});
</script>
</dees-panel>
</div>
</dees-demowrapper>
`;

View File

@ -0,0 +1,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;
}
}

View File

@ -1,4 +1,5 @@
import * as plugins from './00plugins.js'; import * as plugins from './00plugins.js';
import { zIndexLayers } from './00zindex.js';
import { import {
cssManager, cssManager,
css, css,
@ -83,7 +84,7 @@ export class DeesMobilenavigation extends DeesElement {
min-width: 280px; min-width: 280px;
transform: translateX(200px); transform: translateX(200px);
color: ${cssManager.bdTheme('#333', '#fff')}; color: ${cssManager.bdTheme('#333', '#fff')};
z-index: 250; z-index: ${zIndexLayers.fixed.mobileNav};
opacity: 0; opacity: 0;
padding: 16px 32px; padding: 16px 32px;
right: 0px; right: 0px;

View File

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

View File

@ -1,5 +1,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();
}
} }
} }

View File

@ -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 });

View File

@ -1,4 +1,5 @@
import { customElement, DeesElement, domtools, type TemplateResult, html, property, type CSSResult, state, } from '@design.estate/dees-element'; import { customElement, DeesElement, domtools, type TemplateResult, html, property, type CSSResult, state, } from '@design.estate/dees-element';
import { zIndexLayers, zIndexRegistry } from './00zindex.js';
declare global { declare global {
interface HTMLElementTagNameMap { interface HTMLElementTagNameMap {
@ -32,6 +33,12 @@ export class DeesWindowLayer extends DeesElement {
public options: IOptions_DeesWindowLayer = { public options: IOptions_DeesWindowLayer = {
blur: false blur: false
}; };
@state()
private backdropZIndex: number = 1000;
@state()
private contentZIndex: number = 1001;
// INSTANCE // INSTANCE
@property({ @property({
@ -62,7 +69,7 @@ export class DeesWindowLayer extends DeesElement {
background: rgba(0, 0, 0, 0.0); background: rgba(0, 0, 0, 0.0);
backdrop-filter: brightness(1) ${this.options.blur ? 'blur(0px)' : ''}; backdrop-filter: brightness(1) ${this.options.blur ? 'blur(0px)' : ''};
pointer-events: none; pointer-events: none;
z-index: 200; z-index: ${this.backdropZIndex};
} }
.slotContent { .slotContent {
position: fixed; position: fixed;
@ -71,7 +78,7 @@ 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};
} }
.visible { .visible {
@ -101,9 +108,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 +137,10 @@ export class DeesWindowLayer extends DeesElement {
const domtools = await this.domtoolsPromise; const domtools = await this.domtoolsPromise;
await this.hide(); await this.hide();
await domtools.convenience.smartdelay.delayFor(300); await domtools.convenience.smartdelay.delayFor(300);
// Unregister from z-index registry
zIndexRegistry.unregister(this);
this.remove(); this.remove();
} }
} }

View File

@ -1,3 +1,4 @@
export * from './00zindex.js';
export * from './dees-appui-activitylog.js'; export * from './dees-appui-activitylog.js';
export * from './dees-appui-appbar.js'; export * from './dees-appui-appbar.js';
export * from './dees-appui-base.js'; export * from './dees-appui-base.js';
@ -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';

View File

@ -7,6 +7,7 @@ import {
css, css,
state, state,
} from '@design.estate/dees-element'; } from '@design.estate/dees-element';
import { zIndexLayers } from '../00zindex.js';
import { WysiwygFormatting } from './wysiwyg.formatting.js'; import { WysiwygFormatting } from './wysiwyg.formatting.js';
@ -41,7 +42,7 @@ export class DeesFormattingMenu extends DeesElement {
css` css`
:host { :host {
position: fixed; position: fixed;
z-index: 10000; z-index: ${zIndexLayers.wysiwygMenus};
pointer-events: none; pointer-events: none;
} }

View File

@ -8,6 +8,7 @@ import {
css, css,
state, state,
} from '@design.estate/dees-element'; } from '@design.estate/dees-element';
import { zIndexLayers } 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';
@ -49,7 +50,7 @@ export class DeesSlashMenu extends DeesElement {
css` css`
:host { :host {
position: fixed; position: fixed;
z-index: 10000; z-index: ${zIndexLayers.wysiwygMenus};
pointer-events: none; pointer-events: none;
} }

View File

@ -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';

View File

@ -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 -->

View File

@ -0,0 +1,759 @@
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-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 & WYSIWYG</span>
<span class="layer-value">z-index: 4000-4500</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 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>
</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>
`;