Compare commits
4 Commits
Author | SHA1 | Date | |
---|---|---|---|
b31f306106 | |||
1dbbac450c | |||
b5a2bd7436 | |||
931a760ee1 |
@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@design.estate/dees-catalog",
|
||||
"version": "1.9.5",
|
||||
"version": "1.9.6",
|
||||
"private": false,
|
||||
"description": "A comprehensive library that provides dynamic web components for building sophisticated and modern web applications using JavaScript and TypeScript.",
|
||||
"main": "dist_ts_web/index.js",
|
||||
|
@ -57,3 +57,105 @@ export const componentZIndex = {
|
||||
'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();
|
@ -280,6 +280,11 @@ export class DeesInputDropdown extends DeesInputBase<DeesInputDropdown> {
|
||||
elevatedDropdown.style.top = this.getBoundingClientRect().top + 'px';
|
||||
elevatedDropdown.style.left = this.getBoundingClientRect().left + 'px';
|
||||
elevatedDropdown.style.width = this.clientWidth + 'px';
|
||||
|
||||
// Get z-index from registry for the elevated dropdown
|
||||
const dropdownZIndex = (await import('./00zindex.js')).zIndexRegistry.getNextZIndex();
|
||||
elevatedDropdown.style.zIndex = dropdownZIndex.toString();
|
||||
(await import('./00zindex.js')).zIndexRegistry.register(elevatedDropdown, dropdownZIndex);
|
||||
elevatedDropdown.options = this.options;
|
||||
elevatedDropdown.selectedOption = this.selectedOption;
|
||||
elevatedDropdown.highlightedIndex = elevatedDropdown.selectedOption ? elevatedDropdown.options.indexOf(
|
||||
@ -296,9 +301,13 @@ export class DeesInputDropdown extends DeesInputBase<DeesInputDropdown> {
|
||||
'0';
|
||||
elevatedDropdown.removeEventListener('selectedOption', handleSelection);
|
||||
this.windowOverlay.removeEventListener('clicked', destroyOverlay);
|
||||
|
||||
// Unregister elevated dropdown from z-index registry
|
||||
(await import('./00zindex.js')).zIndexRegistry.unregister(elevatedDropdown);
|
||||
|
||||
this.windowOverlay.destroy();
|
||||
};
|
||||
const handleSelection = async (event) => {
|
||||
const handleSelection = async () => {
|
||||
await this.updateSelection(elevatedDropdown.selectedOption);
|
||||
destroyOverlay();
|
||||
};
|
||||
@ -323,10 +332,20 @@ export class DeesInputDropdown extends DeesInputBase<DeesInputDropdown> {
|
||||
await domtoolsInstance.convenience.smartdelay.delayFor(0);
|
||||
const searchInput = selectionBox.querySelector('input');
|
||||
searchInput?.focus();
|
||||
|
||||
// Get z-index from registry for the selection box
|
||||
const selectionBoxZIndex = (await import('./00zindex.js')).zIndexRegistry.getNextZIndex();
|
||||
selectionBox.style.zIndex = selectionBoxZIndex.toString();
|
||||
(await import('./00zindex.js')).zIndexRegistry.register(selectionBox as HTMLElement, selectionBoxZIndex);
|
||||
|
||||
selectionBox.classList.add('show');
|
||||
} else {
|
||||
selectedBox.style.pointerEvents = 'none';
|
||||
selectionBox.classList.remove('show');
|
||||
|
||||
// Unregister selection box from z-index registry
|
||||
(await import('./00zindex.js')).zIndexRegistry.unregister(selectionBox as HTMLElement);
|
||||
|
||||
// selectedBox.style.opacity = '0';
|
||||
}
|
||||
}
|
||||
|
248
ts_web/elements/dees-input-tags.demo.ts
Normal file
248
ts_web/elements/dees-input-tags.demo.ts
Normal file
@ -0,0 +1,248 @@
|
||||
import { html, css } from '@design.estate/dees-element';
|
||||
import '@design.estate/dees-wcctools/demotools';
|
||||
import './dees-panel.js';
|
||||
|
||||
export const demoFunc = () => html`
|
||||
<dees-demowrapper>
|
||||
<style>
|
||||
${css`
|
||||
.demo-container {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 24px;
|
||||
padding: 24px;
|
||||
max-width: 1200px;
|
||||
margin: 0 auto;
|
||||
}
|
||||
|
||||
dees-panel {
|
||||
margin-bottom: 24px;
|
||||
}
|
||||
|
||||
dees-panel:last-child {
|
||||
margin-bottom: 0;
|
||||
}
|
||||
|
||||
.grid-layout {
|
||||
display: grid;
|
||||
grid-template-columns: 1fr 1fr;
|
||||
gap: 16px;
|
||||
}
|
||||
|
||||
@media (max-width: 768px) {
|
||||
.grid-layout {
|
||||
grid-template-columns: 1fr;
|
||||
}
|
||||
}
|
||||
|
||||
.output-preview {
|
||||
margin-top: 16px;
|
||||
padding: 16px;
|
||||
background: #f3f4f6;
|
||||
border-radius: 4px;
|
||||
font-size: 12px;
|
||||
color: #374151;
|
||||
word-break: break-all;
|
||||
max-height: 200px;
|
||||
overflow-y: auto;
|
||||
}
|
||||
|
||||
@media (prefers-color-scheme: dark) {
|
||||
.output-preview {
|
||||
background: #2c2c2c;
|
||||
color: #e4e4e7;
|
||||
}
|
||||
}
|
||||
|
||||
.tag-preview {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
gap: 8px;
|
||||
padding: 12px;
|
||||
background: #f9fafb;
|
||||
border-radius: 4px;
|
||||
min-height: 40px;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
@media (prefers-color-scheme: dark) {
|
||||
.tag-preview {
|
||||
background: #1f2937;
|
||||
}
|
||||
}
|
||||
|
||||
.tag-preview-item {
|
||||
display: inline-block;
|
||||
padding: 4px 12px;
|
||||
background: #e0e7ff;
|
||||
color: #4338ca;
|
||||
border-radius: 12px;
|
||||
font-size: 14px;
|
||||
}
|
||||
|
||||
@media (prefers-color-scheme: dark) {
|
||||
.tag-preview-item {
|
||||
background: #312e81;
|
||||
color: #c7d2fe;
|
||||
}
|
||||
}
|
||||
`}
|
||||
</style>
|
||||
|
||||
<div class="demo-container">
|
||||
<dees-panel .title=${'1. Basic Tags Input'} .subtitle=${'Simple tag input with common programming languages'}>
|
||||
<dees-input-tags
|
||||
.label=${'Programming Languages'}
|
||||
.placeholder=${'Add a language...'}
|
||||
.value=${['JavaScript', 'TypeScript', 'Python', 'Go']}
|
||||
.description=${'Press Enter or comma to add tags'}
|
||||
></dees-input-tags>
|
||||
</dees-panel>
|
||||
|
||||
<dees-panel .title=${'2. Tags with Suggestions'} .subtitle=${'Auto-complete suggestions for faster input'}>
|
||||
<dees-input-tags
|
||||
.label=${'Tech Stack'}
|
||||
.placeholder=${'Type to see suggestions...'}
|
||||
.suggestions=${[
|
||||
'React', 'Vue', 'Angular', 'Svelte', 'Lit', 'Next.js', 'Nuxt', 'SvelteKit',
|
||||
'Node.js', 'Deno', 'Bun', 'Express', 'Fastify', 'Nest.js', 'Koa',
|
||||
'MongoDB', 'PostgreSQL', 'Redis', 'MySQL', 'SQLite', 'Cassandra',
|
||||
'Docker', 'Kubernetes', 'AWS', 'Azure', 'GCP', 'Vercel', 'Netlify'
|
||||
]}
|
||||
.value=${['React', 'Node.js', 'PostgreSQL', 'Docker']}
|
||||
.description=${'Start typing to see suggestions from popular technologies'}
|
||||
></dees-input-tags>
|
||||
</dees-panel>
|
||||
|
||||
<dees-panel .title=${'3. Limited Tags'} .subtitle=${'Restrict the number of tags users can add'}>
|
||||
<div class="grid-layout">
|
||||
<dees-input-tags
|
||||
.label=${'Top 3 Skills'}
|
||||
.placeholder=${'Add up to 3 skills...'}
|
||||
.maxTags=${3}
|
||||
.value=${['Design', 'Development']}
|
||||
.description=${'Maximum 3 tags allowed'}
|
||||
></dees-input-tags>
|
||||
|
||||
<dees-input-tags
|
||||
.label=${'Categories (Max 5)'}
|
||||
.placeholder=${'Select categories...'}
|
||||
.maxTags=${5}
|
||||
.suggestions=${['Blog', 'Tutorial', 'News', 'Review', 'Guide', 'Case Study', 'Interview']}
|
||||
.value=${['Tutorial', 'Guide']}
|
||||
.description=${'Choose up to 5 categories'}
|
||||
></dees-input-tags>
|
||||
</div>
|
||||
</dees-panel>
|
||||
|
||||
<dees-panel .title=${'4. Required & Validation'} .subtitle=${'Tags input with validation requirements'}>
|
||||
<dees-input-tags
|
||||
.label=${'Project Tags'}
|
||||
.placeholder=${'Add at least one tag...'}
|
||||
.required=${true}
|
||||
.description=${'This field is required - add at least one tag'}
|
||||
></dees-input-tags>
|
||||
</dees-panel>
|
||||
|
||||
<dees-panel .title=${'5. Disabled State'} .subtitle=${'Read-only tags display'}>
|
||||
<dees-input-tags
|
||||
.label=${'System Tags'}
|
||||
.value=${['System', 'Protected', 'Read-Only', 'Archive']}
|
||||
.disabled=${true}
|
||||
.description=${'These tags cannot be modified'}
|
||||
></dees-input-tags>
|
||||
</dees-panel>
|
||||
|
||||
<dees-panel .title=${'6. Form Integration'} .subtitle=${'Tags input working within a form context'}>
|
||||
<dees-form>
|
||||
<dees-input-text
|
||||
.label=${'Project Name'}
|
||||
.placeholder=${'My Awesome Project'}
|
||||
.required=${true}
|
||||
.key=${'name'}
|
||||
></dees-input-text>
|
||||
|
||||
<div class="grid-layout">
|
||||
<dees-input-tags
|
||||
.label=${'Technologies Used'}
|
||||
.placeholder=${'Add technologies...'}
|
||||
.required=${true}
|
||||
.key=${'technologies'}
|
||||
.suggestions=${[
|
||||
'TypeScript', 'JavaScript', 'Python', 'Go', 'Rust',
|
||||
'React', 'Vue', 'Angular', 'Svelte',
|
||||
'Node.js', 'Deno', 'Express', 'FastAPI'
|
||||
]}
|
||||
></dees-input-tags>
|
||||
|
||||
<dees-input-tags
|
||||
.label=${'Project Tags'}
|
||||
.placeholder=${'Add descriptive tags...'}
|
||||
.key=${'tags'}
|
||||
.maxTags=${10}
|
||||
.suggestions=${[
|
||||
'frontend', 'backend', 'fullstack', 'mobile', 'desktop',
|
||||
'web', 'api', 'database', 'devops', 'ui/ux',
|
||||
'opensource', 'saas', 'enterprise', 'startup'
|
||||
]}
|
||||
></dees-input-tags>
|
||||
</div>
|
||||
|
||||
<dees-input-text
|
||||
.label=${'Description'}
|
||||
.inputType=${'textarea'}
|
||||
.placeholder=${'Describe your project...'}
|
||||
.key=${'description'}
|
||||
></dees-input-text>
|
||||
|
||||
<dees-form-submit .text=${'Create Project'}></dees-form-submit>
|
||||
</dees-form>
|
||||
</dees-panel>
|
||||
|
||||
<dees-panel .title=${'7. Interactive Demo'} .subtitle=${'Add tags and see them collected in real-time'}>
|
||||
<dees-input-tags
|
||||
id="interactive-tags"
|
||||
.label=${'Your Interests'}
|
||||
.placeholder=${'Type your interests...'}
|
||||
.suggestions=${[
|
||||
'Music', 'Movies', 'Books', 'Travel', 'Photography',
|
||||
'Cooking', 'Gaming', 'Sports', 'Art', 'Technology',
|
||||
'Fashion', 'Fitness', 'Nature', 'Science', 'History'
|
||||
]}
|
||||
@change=${(e: CustomEvent) => {
|
||||
const preview = document.querySelector('#tags-preview');
|
||||
const tags = e.detail.value;
|
||||
if (preview) {
|
||||
if (tags.length === 0) {
|
||||
preview.innerHTML = '<em style="color: #999;">No tags added yet...</em>';
|
||||
} else {
|
||||
preview.innerHTML = tags.map((tag: string) =>
|
||||
`<span class="tag-preview-item">${tag}</span>`
|
||||
).join('');
|
||||
}
|
||||
}
|
||||
}}
|
||||
></dees-input-tags>
|
||||
|
||||
<div class="tag-preview" id="tags-preview">
|
||||
<em style="color: #999;">No tags added yet...</em>
|
||||
</div>
|
||||
|
||||
<div class="output-preview" id="tags-json">
|
||||
<em>JSON output will appear here...</em>
|
||||
</div>
|
||||
|
||||
<script>
|
||||
// Update JSON preview
|
||||
const tagsInput = document.querySelector('#interactive-tags');
|
||||
tagsInput?.addEventListener('change', (e) => {
|
||||
const jsonPreview = document.querySelector('#tags-json');
|
||||
if (jsonPreview) {
|
||||
jsonPreview.textContent = JSON.stringify(e.detail.value, null, 2);
|
||||
}
|
||||
});
|
||||
</script>
|
||||
</dees-panel>
|
||||
</div>
|
||||
</dees-demowrapper>
|
||||
`;
|
@ -0,0 +1,401 @@
|
||||
import {
|
||||
customElement,
|
||||
html,
|
||||
css,
|
||||
cssManager,
|
||||
property,
|
||||
state,
|
||||
type TemplateResult,
|
||||
} from '@design.estate/dees-element';
|
||||
import { DeesInputBase } from './dees-input-base.js';
|
||||
import './dees-icon.js';
|
||||
import { demoFunc } from './dees-input-tags.demo.js';
|
||||
|
||||
declare global {
|
||||
interface HTMLElementTagNameMap {
|
||||
'dees-input-tags': DeesInputTags;
|
||||
}
|
||||
}
|
||||
|
||||
@customElement('dees-input-tags')
|
||||
export class DeesInputTags extends DeesInputBase<DeesInputTags> {
|
||||
// STATIC
|
||||
public static demo = demoFunc;
|
||||
|
||||
// INSTANCE
|
||||
@property({ type: Array })
|
||||
public value: string[] = [];
|
||||
|
||||
@property({ type: String })
|
||||
public placeholder: string = 'Add tags...';
|
||||
|
||||
@property({ type: Number })
|
||||
public maxTags: number = 0; // 0 means unlimited
|
||||
|
||||
@property({ type: Array })
|
||||
public suggestions: string[] = [];
|
||||
|
||||
@state()
|
||||
private inputValue: string = '';
|
||||
|
||||
@state()
|
||||
private showSuggestions: boolean = false;
|
||||
|
||||
@state()
|
||||
private highlightedSuggestionIndex: number = -1;
|
||||
|
||||
@property({ type: String })
|
||||
public validationText: string = '';
|
||||
|
||||
public static styles = [
|
||||
...DeesInputBase.baseStyles,
|
||||
cssManager.defaultStyles,
|
||||
css`
|
||||
:host {
|
||||
display: block;
|
||||
font-family: 'Geist Sans', sans-serif;
|
||||
}
|
||||
|
||||
.input-wrapper {
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.tags-container {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
padding: 8px;
|
||||
min-height: 40px;
|
||||
background: ${cssManager.bdTheme('#fafafa', '#222222')};
|
||||
border: 1px solid ${cssManager.bdTheme('#e0e0e0', '#333333')};
|
||||
border-radius: 8px;
|
||||
transition: all 0.2s ease;
|
||||
cursor: text;
|
||||
}
|
||||
|
||||
.tags-container:focus-within {
|
||||
border-color: ${cssManager.bdTheme('#0069f2', '#0084ff')};
|
||||
box-shadow: 0 0 0 2px ${cssManager.bdTheme('rgba(0, 105, 242, 0.1)', 'rgba(0, 132, 255, 0.2)')};
|
||||
}
|
||||
|
||||
.tags-container.disabled {
|
||||
background: ${cssManager.bdTheme('#f5f5f5', '#1a1a1a')};
|
||||
cursor: not-allowed;
|
||||
opacity: 0.6;
|
||||
}
|
||||
|
||||
.tag {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
gap: 4px;
|
||||
padding: 4px 8px;
|
||||
background: ${cssManager.bdTheme('#e3f2fd', '#1e3a5f')};
|
||||
color: ${cssManager.bdTheme('#1976d2', '#90caf9')};
|
||||
border-radius: 16px;
|
||||
font-size: 14px;
|
||||
line-height: 1.2;
|
||||
user-select: none;
|
||||
animation: tagAppear 0.2s ease;
|
||||
}
|
||||
|
||||
@keyframes tagAppear {
|
||||
from {
|
||||
transform: scale(0.8);
|
||||
opacity: 0;
|
||||
}
|
||||
to {
|
||||
transform: scale(1);
|
||||
opacity: 1;
|
||||
}
|
||||
}
|
||||
|
||||
.tag-remove {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
width: 16px;
|
||||
height: 16px;
|
||||
border-radius: 50%;
|
||||
cursor: pointer;
|
||||
transition: all 0.2s ease;
|
||||
margin-left: 2px;
|
||||
}
|
||||
|
||||
.tag-remove:hover {
|
||||
background: ${cssManager.bdTheme('rgba(0, 0, 0, 0.1)', 'rgba(255, 255, 255, 0.1)')};
|
||||
}
|
||||
|
||||
.tag-remove dees-icon {
|
||||
width: 12px;
|
||||
height: 12px;
|
||||
}
|
||||
|
||||
.tag-input {
|
||||
flex: 1;
|
||||
min-width: 120px;
|
||||
border: none;
|
||||
background: transparent;
|
||||
outline: none;
|
||||
font-size: 14px;
|
||||
font-family: inherit;
|
||||
color: ${cssManager.bdTheme('#333', '#fff')};
|
||||
padding: 4px;
|
||||
}
|
||||
|
||||
.tag-input::placeholder {
|
||||
color: ${cssManager.bdTheme('#999', '#666')};
|
||||
}
|
||||
|
||||
.tag-input:disabled {
|
||||
cursor: not-allowed;
|
||||
}
|
||||
|
||||
/* Suggestions dropdown */
|
||||
.suggestions-container {
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.suggestions-dropdown {
|
||||
position: absolute;
|
||||
top: 100%;
|
||||
left: 0;
|
||||
right: 0;
|
||||
margin-top: 4px;
|
||||
background: ${cssManager.bdTheme('#ffffff', '#222222')};
|
||||
border: 1px solid ${cssManager.bdTheme('#e0e0e0', '#333333')};
|
||||
border-radius: 8px;
|
||||
box-shadow: 0 4px 12px ${cssManager.bdTheme('rgba(0, 0, 0, 0.1)', 'rgba(0, 0, 0, 0.3)')};
|
||||
max-height: 200px;
|
||||
overflow-y: auto;
|
||||
z-index: 1000;
|
||||
}
|
||||
|
||||
.suggestion {
|
||||
padding: 8px 12px;
|
||||
cursor: pointer;
|
||||
transition: all 0.2s ease;
|
||||
color: ${cssManager.bdTheme('#333', '#fff')};
|
||||
}
|
||||
|
||||
.suggestion:hover,
|
||||
.suggestion.highlighted {
|
||||
background: ${cssManager.bdTheme('#f5f5f5', '#333333')};
|
||||
}
|
||||
|
||||
.suggestion.highlighted {
|
||||
background: ${cssManager.bdTheme('#e3f2fd', '#1e3a5f')};
|
||||
}
|
||||
|
||||
/* Validation styles */
|
||||
.validation-message {
|
||||
color: #d32f2f;
|
||||
font-size: 12px;
|
||||
margin-top: 4px;
|
||||
min-height: 16px;
|
||||
}
|
||||
|
||||
/* Description styles */
|
||||
.description {
|
||||
color: ${cssManager.bdTheme('#666', '#999')};
|
||||
font-size: 12px;
|
||||
margin-top: 4px;
|
||||
}
|
||||
`,
|
||||
];
|
||||
|
||||
public render(): TemplateResult {
|
||||
const filteredSuggestions = this.suggestions.filter(
|
||||
suggestion =>
|
||||
!this.value.includes(suggestion) &&
|
||||
suggestion.toLowerCase().includes(this.inputValue.toLowerCase())
|
||||
);
|
||||
|
||||
return html`
|
||||
<div class="input-wrapper">
|
||||
${this.label ? html`<dees-label .label=${this.label} .required=${this.required}></dees-label>` : ''}
|
||||
|
||||
<div class="suggestions-container">
|
||||
<div
|
||||
class="tags-container ${this.disabled ? 'disabled' : ''}"
|
||||
@click=${this.handleContainerClick}
|
||||
>
|
||||
${this.value.map(tag => html`
|
||||
<div class="tag">
|
||||
<span>${tag}</span>
|
||||
${!this.disabled ? html`
|
||||
<div class="tag-remove" @click=${(e: Event) => this.removeTag(e, tag)}>
|
||||
<dees-icon .icon=${'lucide:x'}></dees-icon>
|
||||
</div>
|
||||
` : ''}
|
||||
</div>
|
||||
`)}
|
||||
|
||||
${!this.disabled && (!this.maxTags || this.value.length < this.maxTags) ? html`
|
||||
<input
|
||||
type="text"
|
||||
class="tag-input"
|
||||
.placeholder=${this.placeholder}
|
||||
.value=${this.inputValue}
|
||||
@input=${this.handleInput}
|
||||
@keydown=${this.handleKeyDown}
|
||||
@focus=${this.handleFocus}
|
||||
@blur=${this.handleBlur}
|
||||
?disabled=${this.disabled}
|
||||
/>
|
||||
` : ''}
|
||||
</div>
|
||||
|
||||
${this.showSuggestions && filteredSuggestions.length > 0 ? html`
|
||||
<div class="suggestions-dropdown">
|
||||
${filteredSuggestions.map((suggestion, index) => html`
|
||||
<div
|
||||
class="suggestion ${index === this.highlightedSuggestionIndex ? 'highlighted' : ''}"
|
||||
@mousedown=${(e: Event) => {
|
||||
e.preventDefault(); // Prevent blur
|
||||
this.addTag(suggestion);
|
||||
}}
|
||||
@mouseenter=${() => this.highlightedSuggestionIndex = index}
|
||||
>
|
||||
${suggestion}
|
||||
</div>
|
||||
`)}
|
||||
</div>
|
||||
` : ''}
|
||||
</div>
|
||||
|
||||
${this.validationText ? html`
|
||||
<div class="validation-message">${this.validationText}</div>
|
||||
` : ''}
|
||||
|
||||
${this.description ? html`
|
||||
<div class="description">${this.description}</div>
|
||||
` : ''}
|
||||
</div>
|
||||
`;
|
||||
}
|
||||
|
||||
private handleContainerClick(e: Event) {
|
||||
if (this.disabled) return;
|
||||
|
||||
const input = this.shadowRoot?.querySelector('.tag-input') as HTMLInputElement;
|
||||
if (input && e.target !== input) {
|
||||
input.focus();
|
||||
}
|
||||
}
|
||||
|
||||
private handleInput(e: Event) {
|
||||
const input = e.target as HTMLInputElement;
|
||||
this.inputValue = input.value;
|
||||
|
||||
// Check for comma or semicolon to add tag
|
||||
if (this.inputValue.includes(',') || this.inputValue.includes(';')) {
|
||||
const tag = this.inputValue.replace(/[,;]/g, '').trim();
|
||||
if (tag) {
|
||||
this.addTag(tag);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private handleKeyDown(e: KeyboardEvent) {
|
||||
const input = e.target as HTMLInputElement;
|
||||
|
||||
if (e.key === 'Enter') {
|
||||
e.preventDefault();
|
||||
if (this.highlightedSuggestionIndex >= 0 && this.showSuggestions) {
|
||||
const filteredSuggestions = this.suggestions.filter(
|
||||
suggestion =>
|
||||
!this.value.includes(suggestion) &&
|
||||
suggestion.toLowerCase().includes(this.inputValue.toLowerCase())
|
||||
);
|
||||
if (filteredSuggestions[this.highlightedSuggestionIndex]) {
|
||||
this.addTag(filteredSuggestions[this.highlightedSuggestionIndex]);
|
||||
}
|
||||
} else if (this.inputValue.trim()) {
|
||||
this.addTag(this.inputValue.trim());
|
||||
}
|
||||
} else if (e.key === 'Backspace' && !this.inputValue && this.value.length > 0) {
|
||||
// Remove last tag when backspace is pressed on empty input
|
||||
this.removeTag(e, this.value[this.value.length - 1]);
|
||||
} else if (e.key === 'ArrowDown' && this.showSuggestions) {
|
||||
e.preventDefault();
|
||||
const filteredCount = this.suggestions.filter(
|
||||
s => !this.value.includes(s) && s.toLowerCase().includes(this.inputValue.toLowerCase())
|
||||
).length;
|
||||
this.highlightedSuggestionIndex = Math.min(
|
||||
this.highlightedSuggestionIndex + 1,
|
||||
filteredCount - 1
|
||||
);
|
||||
} else if (e.key === 'ArrowUp' && this.showSuggestions) {
|
||||
e.preventDefault();
|
||||
this.highlightedSuggestionIndex = Math.max(this.highlightedSuggestionIndex - 1, 0);
|
||||
} else if (e.key === 'Escape') {
|
||||
this.showSuggestions = false;
|
||||
this.highlightedSuggestionIndex = -1;
|
||||
}
|
||||
}
|
||||
|
||||
private handleFocus() {
|
||||
if (this.suggestions.length > 0) {
|
||||
this.showSuggestions = true;
|
||||
}
|
||||
}
|
||||
|
||||
private handleBlur() {
|
||||
// Delay to allow click on suggestions
|
||||
setTimeout(() => {
|
||||
this.showSuggestions = false;
|
||||
this.highlightedSuggestionIndex = -1;
|
||||
}, 200);
|
||||
}
|
||||
|
||||
private addTag(tag: string) {
|
||||
if (!tag || this.value.includes(tag)) return;
|
||||
if (this.maxTags && this.value.length >= this.maxTags) return;
|
||||
|
||||
this.value = [...this.value, tag];
|
||||
this.inputValue = '';
|
||||
this.showSuggestions = false;
|
||||
this.highlightedSuggestionIndex = -1;
|
||||
|
||||
// Clear the input
|
||||
const input = this.shadowRoot?.querySelector('.tag-input') as HTMLInputElement;
|
||||
if (input) {
|
||||
input.value = '';
|
||||
}
|
||||
|
||||
this.emitChange();
|
||||
}
|
||||
|
||||
private removeTag(e: Event, tag: string) {
|
||||
e.stopPropagation();
|
||||
this.value = this.value.filter(t => t !== tag);
|
||||
this.emitChange();
|
||||
}
|
||||
|
||||
private emitChange() {
|
||||
this.dispatchEvent(new CustomEvent('change', {
|
||||
detail: { value: this.value },
|
||||
bubbles: true,
|
||||
composed: true
|
||||
}));
|
||||
this.changeSubject.next(this);
|
||||
}
|
||||
|
||||
public getValue(): string[] {
|
||||
return this.value;
|
||||
}
|
||||
|
||||
public setValue(value: string[]): void {
|
||||
this.value = value || [];
|
||||
}
|
||||
|
||||
public async validate(): Promise<boolean> {
|
||||
if (this.required && (!this.value || this.value.length === 0)) {
|
||||
this.validationText = 'At least one tag is required';
|
||||
return false;
|
||||
}
|
||||
this.validationText = '';
|
||||
return true;
|
||||
}
|
||||
}
|
@ -1,6 +1,6 @@
|
||||
import * as colors from './00colors.js';
|
||||
import * as plugins from './00plugins.js';
|
||||
import { zIndexLayers } from './00zindex.js';
|
||||
import { zIndexLayers, zIndexRegistry } from './00zindex.js';
|
||||
|
||||
import { demoFunc } from './dees-modal.demo.js';
|
||||
import {
|
||||
@ -62,6 +62,11 @@ export class DeesModal extends DeesElement {
|
||||
});
|
||||
body.append(modal.windowLayer);
|
||||
body.append(modal);
|
||||
|
||||
// Get z-index for modal (should be above window layer)
|
||||
modal.modalZIndex = zIndexRegistry.getNextZIndex();
|
||||
zIndexRegistry.register(modal, modal.modalZIndex);
|
||||
|
||||
return modal;
|
||||
}
|
||||
|
||||
@ -96,6 +101,9 @@ export class DeesModal extends DeesElement {
|
||||
@property({ attribute: false })
|
||||
public onHelp: () => void | Promise<void>;
|
||||
|
||||
@state()
|
||||
private modalZIndex: number = 1000;
|
||||
|
||||
constructor() {
|
||||
super();
|
||||
}
|
||||
@ -118,7 +126,6 @@ export class DeesModal extends DeesElement {
|
||||
box-sizing: border-box;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
z-index: ${zIndexLayers.overlay.modal};
|
||||
}
|
||||
.modal {
|
||||
will-change: transform;
|
||||
@ -291,7 +298,7 @@ export class DeesModal extends DeesElement {
|
||||
${maxWidthStyle ? `.modal { max-width: ${maxWidthStyle}; }` : ''}
|
||||
${minWidthStyle ? `.modal { min-width: ${minWidthStyle}; }` : ''}
|
||||
</style>
|
||||
<div class="modalContainer" @click=${this.handleOutsideClick}>
|
||||
<div class="modalContainer" @click=${this.handleOutsideClick} style="z-index: ${this.modalZIndex}">
|
||||
<div class="modal ${widthClass}">
|
||||
<div class="heading">
|
||||
<div class="heading-text">${this.heading}</div>
|
||||
@ -349,6 +356,9 @@ export class DeesModal extends DeesElement {
|
||||
await domtools.convenience.smartdelay.delayFor(200);
|
||||
document.body.removeChild(this);
|
||||
await this.windowLayer.destroy();
|
||||
|
||||
// Unregister from z-index registry
|
||||
zIndexRegistry.unregister(this);
|
||||
}
|
||||
|
||||
private async handleHelp() {
|
||||
|
@ -106,6 +106,11 @@ export class DeesToast extends DeesElement {
|
||||
return toast;
|
||||
}
|
||||
|
||||
// Alias for consistency with DeesModal
|
||||
public static async createAndShow(options: IToastOptions | string) {
|
||||
return this.show(options);
|
||||
}
|
||||
|
||||
// Convenience methods
|
||||
public static info(message: string, duration?: number) {
|
||||
return this.show({ message, type: 'info', duration });
|
||||
|
@ -1,5 +1,5 @@
|
||||
import { customElement, DeesElement, domtools, type TemplateResult, html, property, type CSSResult, state, } from '@design.estate/dees-element';
|
||||
import { zIndexLayers } from './00zindex.js';
|
||||
import { zIndexLayers, zIndexRegistry } from './00zindex.js';
|
||||
|
||||
declare global {
|
||||
interface HTMLElementTagNameMap {
|
||||
@ -34,6 +34,12 @@ export class DeesWindowLayer extends DeesElement {
|
||||
blur: false
|
||||
};
|
||||
|
||||
@state()
|
||||
private backdropZIndex: number = 1000;
|
||||
|
||||
@state()
|
||||
private contentZIndex: number = 1001;
|
||||
|
||||
// INSTANCE
|
||||
@property({
|
||||
type: Boolean
|
||||
@ -63,7 +69,7 @@ export class DeesWindowLayer extends DeesElement {
|
||||
background: rgba(0, 0, 0, 0.0);
|
||||
backdrop-filter: brightness(1) ${this.options.blur ? 'blur(0px)' : ''};
|
||||
pointer-events: none;
|
||||
z-index: ${zIndexLayers.backdrop.dropdown};
|
||||
z-index: ${this.backdropZIndex};
|
||||
}
|
||||
.slotContent {
|
||||
position: fixed;
|
||||
@ -72,7 +78,7 @@ export class DeesWindowLayer extends DeesElement {
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
z-index: ${zIndexLayers.overlay.dropdown};
|
||||
z-index: ${this.contentZIndex};
|
||||
}
|
||||
|
||||
.visible {
|
||||
@ -103,8 +109,20 @@ export class DeesWindowLayer extends DeesElement {
|
||||
this.visible = !this.visible;
|
||||
}
|
||||
|
||||
public getContentZIndex(): number {
|
||||
return this.contentZIndex;
|
||||
}
|
||||
|
||||
public async show() {
|
||||
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);
|
||||
this.visible = true;
|
||||
}
|
||||
@ -119,6 +137,10 @@ export class DeesWindowLayer extends DeesElement {
|
||||
const domtools = await this.domtoolsPromise;
|
||||
await this.hide();
|
||||
await domtools.convenience.smartdelay.delayFor(300);
|
||||
|
||||
// Unregister from z-index registry
|
||||
zIndexRegistry.unregister(this);
|
||||
|
||||
this.remove();
|
||||
}
|
||||
}
|
||||
|
@ -37,6 +37,7 @@ export * from './dees-progressbar.js';
|
||||
export * from './dees-input-quantityselector.js';
|
||||
export * from './dees-input-radiogroup.js';
|
||||
export * from './dees-input-richtext.js';
|
||||
export * from './dees-input-tags.js';
|
||||
export * from './dees-input-text.js';
|
||||
export * from './dees-label.js';
|
||||
export * from './dees-mobilenavigation.js';
|
||||
|
@ -1,2 +1,3 @@
|
||||
export * from './mainpage.js';
|
||||
export * from './input-showcase.js';
|
||||
export * from './zindex-showcase.js';
|
@ -377,6 +377,29 @@ export const inputShowcase = () => html`
|
||||
.placeholder=${'Type and press Enter'}
|
||||
></dees-input-typelist>
|
||||
</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>
|
||||
|
||||
<!-- Numeric Inputs Section -->
|
||||
|
759
ts_web/pages/zindex-showcase.ts
Normal file
759
ts_web/pages/zindex-showcase.ts
Normal 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>
|
||||
`;
|
Reference in New Issue
Block a user