import { customElement, type TemplateResult, html, property, css, cssManager, } from '@design.estate/dees-element'; import { DeesInputBase } from './dees-input-base.js'; import * as colors from './00colors.js' import { demoFunc } from './dees-input-multitoggle.demo.js'; declare global { interface HTMLElementTagNameMap { 'dees-input-multitoggle': DeesInputMultitoggle; } } @customElement('dees-input-multitoggle') export class DeesInputMultitoggle extends DeesInputBase { public static demo = demoFunc; @property() type: 'boolean' | 'multi' | 'single' = 'multi'; @property() booleanTrueName: string = 'true'; @property() booleanFalseName: string = 'false'; @property({ type: Array, }) options: string[] = []; @property() selectedOption: string = ''; @property({ type: Boolean }) boolValue: boolean = false; // Add value property for form compatibility public get value(): string | boolean { if (this.type === 'boolean') { return this.selectedOption === this.booleanTrueName; } return this.selectedOption; } public set value(val: string | boolean) { if (this.type === 'boolean' && typeof val === 'boolean') { this.selectedOption = val ? this.booleanTrueName : this.booleanFalseName; } else { this.selectedOption = val as string; } this.requestUpdate(); // Defer indicator update to next frame if component not yet updated if (this.hasUpdated) { requestAnimationFrame(() => { this.setIndicator(); }); } } public static styles = [ ...DeesInputBase.baseStyles, cssManager.defaultStyles, css` :host { color: ${cssManager.bdTheme('#09090b', '#fafafa')}; user-select: none; } .selections { position: relative; display: inline-flex; align-items: center; background: ${cssManager.bdTheme('#ffffff', '#18181b')}; border: 1px solid ${cssManager.bdTheme('#e5e7eb', '#27272a')}; padding: 4px; border-radius: 8px; box-shadow: 0 1px 2px 0 rgba(0, 0, 0, 0.05); } .option { position: relative; padding: 8px 20px; border-radius: 6px; cursor: pointer; white-space: nowrap; transition: color 0.2s ease; font-size: 14px; font-weight: 500; color: ${cssManager.bdTheme('#71717a', '#71717a')}; line-height: 1; z-index: 2; } .option:hover { color: ${cssManager.bdTheme('#18181b', '#e4e4e7')}; } .option.selected { color: ${cssManager.bdTheme('#3b82f6', '#60a5fa')}; } .indicator { opacity: 0; position: absolute; height: calc(100% - 8px); top: 4px; border-radius: 6px; background: ${cssManager.bdTheme('rgba(59, 130, 246, 0.15)', 'rgba(59, 130, 246, 0.15)')}; transition: all 0.2s cubic-bezier(0.4, 0, 0.2, 1); z-index: 1; } .indicator.no-transition { transition: none; } :host([disabled]) .selections { opacity: 0.5; cursor: not-allowed; } :host([disabled]) .option { cursor: not-allowed; pointer-events: none; } :host([disabled]) .indicator { background: ${cssManager.bdTheme('rgba(113, 113, 122, 0.15)', 'rgba(113, 113, 122, 0.15)')}; } `, ]; public render(): TemplateResult { return html`
${this.options.map( (option) => html`
this.handleSelection(option)}> ${option}
` )}
`; } public async connectedCallback() { await super.connectedCallback(); // Initialize boolean options early if (this.type === 'boolean' && this.options.length === 0) { this.options = [this.booleanTrueName || 'true', this.booleanFalseName || 'false']; // Set default selection for boolean if not set if (!this.selectedOption) { this.selectedOption = this.booleanFalseName || 'false'; } } // Set default selection to first option if not set if (!this.selectedOption && this.options.length > 0) { this.selectedOption = this.options[0]; } } public async firstUpdated(_changedProperties: Map) { super.firstUpdated(_changedProperties); // Update boolean options if they changed if (this.type === 'boolean') { this.options = [this.booleanTrueName || 'true', this.booleanFalseName || 'false']; } // Wait for the next frame to ensure DOM is fully rendered await this.updateComplete; // Wait for fonts to load if (document.fonts) { await document.fonts.ready; } // Wait one more frame after fonts are loaded await new Promise(resolve => requestAnimationFrame(resolve)); // Now set the indicator this.setIndicator(); } public async handleSelection(optionArg: string) { if (this.disabled) return; this.selectedOption = optionArg; this.requestUpdate(); this.changeSubject.next(this); await this.updateComplete; this.setIndicator(); } private indicatorInitialized = false; public async setIndicator() { const indicator: HTMLDivElement = this.shadowRoot.querySelector('.indicator'); const selectedIndex = this.options.indexOf(this.selectedOption); // If no valid selection, hide indicator if (selectedIndex === -1 || !indicator) { if (indicator) { indicator.style.opacity = '0'; } return; } const option: HTMLDivElement = this.shadowRoot.querySelector( `.option:nth-child(${selectedIndex + 2})` ); if (indicator && option) { // Only disable transition for the very first positioning if (!this.indicatorInitialized) { indicator.classList.add('no-transition'); this.indicatorInitialized = true; // Remove the no-transition class after a brief delay setTimeout(() => { indicator.classList.remove('no-transition'); }, 50); } indicator.style.width = `${option.clientWidth}px`; indicator.style.left = `${option.offsetLeft}px`; indicator.style.opacity = '1'; } } public getValue(): string | boolean { if (this.type === 'boolean') { return this.selectedOption === this.booleanTrueName; } return this.selectedOption; } public setValue(value: string | boolean): void { if (this.type === 'boolean' && typeof value === 'boolean') { this.selectedOption = value ? (this.booleanTrueName || 'true') : (this.booleanFalseName || 'false'); } else { this.selectedOption = value as string; } this.requestUpdate(); if (this.hasUpdated) { requestAnimationFrame(() => { this.setIndicator(); }); } } }