diff --git a/changelog.md b/changelog.md index 4ae65c1..f969f9d 100644 --- a/changelog.md +++ b/changelog.md @@ -1,5 +1,13 @@ # Changelog +## 2026-01-07 - 3.34.0 - feat(dees-input-toggle) +Add DeesInputToggle component (toggle switch) with demo and exports; integrate into inputs and DeesForm + +- New UI input component: ts_web/elements/00group-input/dees-input-toggle/dees-input-toggle.ts — toggle switch with pointer drag, keyboard support, value syncing and DeesInputBase integration. +- Interactive demo added: ts_web/elements/00group-input/dees-input-toggle/dees-input-toggle.demo.ts demonstrating usage, batch operations and event handling. +- Module exports updated: added ts_web/elements/00group-input/dees-input-toggle/index.ts and exported from ts_web/elements/00group-input/index.ts. +- DeesForm integration: imported DeesInputToggle and added it to the form components array and input union types in ts_web/elements/00group-form/dees-form/dees-form.ts + ## 2026-01-06 - 3.33.0 - feat(dees-statsgrid) add multiPercentage tile type to stats grid diff --git a/ts_web/00_commitinfo_data.ts b/ts_web/00_commitinfo_data.ts index cf844ac..46227fb 100644 --- a/ts_web/00_commitinfo_data.ts +++ b/ts_web/00_commitinfo_data.ts @@ -3,6 +3,6 @@ */ export const commitinfo = { name: '@design.estate/dees-catalog', - version: '3.33.0', + version: '3.34.0', description: 'A comprehensive library that provides dynamic web components for building sophisticated and modern web applications using JavaScript and TypeScript.' } diff --git a/ts_web/elements/00group-form/dees-form/dees-form.ts b/ts_web/elements/00group-form/dees-form/dees-form.ts index 903f923..f76cd50 100644 --- a/ts_web/elements/00group-form/dees-form/dees-form.ts +++ b/ts_web/elements/00group-form/dees-form/dees-form.ts @@ -19,6 +19,7 @@ import { DeesInputFileupload } from '../../00group-input/dees-input-fileupload/i import { DeesInputIban } from '../../00group-input/dees-input-iban/dees-input-iban.js'; import { DeesInputMultitoggle } from '../../00group-input/dees-input-multitoggle/dees-input-multitoggle.js'; import { DeesInputPhone } from '../../00group-input/dees-input-phone/dees-input-phone.js'; +import { DeesInputToggle } from '../../00group-input/dees-input-toggle/dees-input-toggle.js'; import { DeesInputTypelist } from '../../00group-input/dees-input-typelist/dees-input-typelist.js'; import { DeesFormSubmit } from '../dees-form-submit/dees-form-submit.js'; import { DeesTable } from '../../dees-table/index.js'; @@ -37,6 +38,7 @@ const FORM_INPUT_TYPES = [ DeesInputQuantitySelector, DeesInputRadiogroup, DeesInputText, + DeesInputToggle, DeesInputTypelist, DeesTable, ]; @@ -53,6 +55,7 @@ export type TFormInputElement = | DeesInputQuantitySelector | DeesInputRadiogroup | DeesInputText + | DeesInputToggle | DeesInputTypelist | DeesTable; diff --git a/ts_web/elements/00group-input/dees-input-toggle/dees-input-toggle.demo.ts b/ts_web/elements/00group-input/dees-input-toggle/dees-input-toggle.demo.ts new file mode 100644 index 0000000..7b0e2fa --- /dev/null +++ b/ts_web/elements/00group-input/dees-input-toggle/dees-input-toggle.demo.ts @@ -0,0 +1,310 @@ +import { html, css, cssManager } from '@design.estate/dees-element'; +import '@design.estate/dees-wcctools/demotools'; +import '../../dees-panel/dees-panel.js'; +import type { DeesInputToggle } from './dees-input-toggle.js'; + +export const demoFunc = () => html` + { + // Example of programmatic interaction + const toggleAllOnBtn = elementArg.querySelector('#toggle-all-on'); + const toggleAllOffBtn = elementArg.querySelector('#toggle-all-off'); + const featureToggles = elementArg.querySelectorAll('.feature-toggles dees-input-toggle'); + + if (toggleAllOnBtn && toggleAllOffBtn) { + toggleAllOnBtn.addEventListener('click', () => { + featureToggles.forEach((toggle: DeesInputToggle) => { + if (!toggle.disabled && !toggle.required) { + toggle.value = true; + } + }); + }); + + toggleAllOffBtn.addEventListener('click', () => { + featureToggles.forEach((toggle: DeesInputToggle) => { + if (!toggle.disabled && !toggle.required) { + toggle.value = false; + } + }); + }); + } + }}> + + +
+ +
+ + + + + +
+

Tip: You can drag the toggle knob to switch states

+
+ + +
+ + + + + + + + + +
+
+ + +
+ + + + + + + +
+
+ + +
+

Notification Settings

+ +
+ + + + + + + +
+
+
+ + +
+ Enable All + Disable All +
+ +
+
+ + + + + + + + + +
+
+
+ + +
+ { + const output = document.querySelector('#airplane-output'); + if (output) { + output.textContent = `Airplane mode: ${event.detail ? 'ON' : 'OFF'}`; + } + }} + > + + { + const output = document.querySelector('#dnd-output'); + if (output) { + output.textContent = `Do not disturb: ${event.detail ? 'ENABLED' : 'DISABLED'}`; + } + }} + > +
+ +
+
Airplane mode: OFF
+
Do not disturb: DISABLED
+
+
+
+
+`; diff --git a/ts_web/elements/00group-input/dees-input-toggle/dees-input-toggle.ts b/ts_web/elements/00group-input/dees-input-toggle/dees-input-toggle.ts new file mode 100644 index 0000000..a667f75 --- /dev/null +++ b/ts_web/elements/00group-input/dees-input-toggle/dees-input-toggle.ts @@ -0,0 +1,372 @@ +import { + customElement, + type TemplateResult, + property, + html, + css, + cssManager, +} from '@design.estate/dees-element'; +import * as domtools from '@design.estate/dees-domtools'; +import { DeesInputBase } from '../dees-input-base/dees-input-base.js'; +import { demoFunc } from './dees-input-toggle.demo.js'; +import { cssGeistFontFamily } from '../../00fonts.js'; +import { themeDefaultStyles } from '../../00theme.js'; + +declare global { + interface HTMLElementTagNameMap { + 'dees-input-toggle': DeesInputToggle; + } +} + +@customElement('dees-input-toggle') +export class DeesInputToggle extends DeesInputBase { + // STATIC + public static demo = demoFunc; + public static demoGroup = 'Input'; + + // INSTANCE + + @property({ type: Boolean, reflect: true }) + accessor value: boolean = false; + + /** + * Knob position tracking (0 = off, maxTravel = on) + */ + private currentX = 0; + + /** + * Drag state + */ + private isDragging = false; + private hasDragged = false; + private startX = 0; + + // Toggle dimensions + private readonly trackWidth = 36; + private readonly trackHeight = 20; + private readonly knobSize = 14; + private readonly padding = 2; + private readonly maxTravel = 16; // trackWidth - knobSize - (padding * 2) - border + + constructor() { + super(); + this.labelPosition = 'right'; // Toggle defaults to label on the right + } + + public static styles = [ + themeDefaultStyles, + ...DeesInputBase.baseStyles, + cssManager.defaultStyles, + css` + * { + box-sizing: border-box; + } + + :host { + position: relative; + cursor: default; + font-family: ${cssGeistFontFamily}; + } + + .maincontainer { + display: inline-flex; + align-items: flex-start; + gap: 8px; + cursor: pointer; + user-select: none; + transition: all 0.15s ease; + } + + .toggle-track { + position: relative; + flex-shrink: 0; + height: 20px; + width: 36px; + border-radius: 10px; + background: ${cssManager.bdTheme('hsl(0 0% 100%)', 'hsl(0 0% 3.9%)')}; + border: 1px solid ${cssManager.bdTheme('hsl(0 0% 89.8%)', 'hsl(0 0% 14.9%)')}; + overflow: hidden; + transition: all 0.15s ease; + margin-top: 1px; + } + + .maincontainer:hover .toggle-track { + border-color: ${cssManager.bdTheme('hsl(0 0% 79.8%)', 'hsl(0 0% 20.9%)')}; + } + + :host([value]) .toggle-track { + background: ${cssManager.bdTheme('hsl(222.2 47.4% 51.2%)', 'hsl(217.2 91.2% 59.8%)')}; + border-color: ${cssManager.bdTheme('hsl(222.2 47.4% 51.2%)', 'hsl(217.2 91.2% 59.8%)')}; + } + + .toggle-track:focus-visible { + outline: none; + box-shadow: 0 0 0 3px ${cssManager.bdTheme('hsl(222.2 47.4% 51.2% / 0.1)', 'hsl(217.2 91.2% 59.8% / 0.1)')}; + } + + .toggle-knob { + position: absolute; + top: 2px; + width: 14px; + height: 14px; + border-radius: 7px; + background: ${cssManager.bdTheme('hsl(0 0% 63.9%)', 'hsl(0 0% 45.1%)')}; + transition: left 0.15s ease, background 0.15s ease; + touch-action: none; + } + + .toggle-knob.dragging { + transition: background 0.15s ease; + } + + :host([value]) .toggle-knob { + background: white; + } + + /* Disabled state */ + .maincontainer.disabled { + cursor: not-allowed; + opacity: 0.5; + } + + .toggle-track.disabled { + background: ${cssManager.bdTheme('hsl(0 0% 95.1%)', 'hsl(0 0% 14.9%)')}; + border-color: ${cssManager.bdTheme('hsl(0 0% 89.8%)', 'hsl(0 0% 14.9%)')}; + } + + /* Required state (locked on) */ + :host([required][value]) .toggle-track { + background: ${cssManager.bdTheme('hsl(222.2 47.4% 61.2%)', 'hsl(217.2 91.2% 49.8%)')}; + border-color: ${cssManager.bdTheme('hsl(222.2 47.4% 61.2%)', 'hsl(217.2 91.2% 49.8%)')}; + cursor: not-allowed; + } + + :host([required][value]) .toggle-knob { + background: ${cssManager.bdTheme('hsl(0 0% 85%)', 'hsl(0 0% 70%)')}; + } + + /* Label */ + .label-container { + display: flex; + flex-direction: column; + gap: 2px; + flex: 1; + } + + .toggle-label { + font-size: 14px; + font-weight: 500; + line-height: 20px; + color: ${cssManager.bdTheme('hsl(0 0% 15%)', 'hsl(0 0% 90%)')}; + transition: color 0.15s ease; + letter-spacing: -0.01em; + } + + .maincontainer:hover .toggle-label { + color: ${cssManager.bdTheme('hsl(0 0% 9%)', 'hsl(0 0% 95%)')}; + } + + .maincontainer.disabled:hover .toggle-label { + color: ${cssManager.bdTheme('hsl(0 0% 15%)', 'hsl(0 0% 90%)')}; + } + + /* Description */ + .description-text { + font-size: 12px; + color: ${cssManager.bdTheme('hsl(0 0% 45.1%)', 'hsl(0 0% 63.9%)')}; + line-height: 1.5; + } + `, + ]; + + public render(): TemplateResult { + return html` +
+
+
+
+
+
+ ${this.label ? html`
${this.label}
` : ''} + ${this.description ? html`
${this.description}
` : ''} +
+
+
+ `; + } + + public async firstUpdated(_changedProperties: Map) { + await super.firstUpdated(_changedProperties); + // Initialize knob position based on initial value + if (this.required && !this.value) { + this.value = true; + } + this.currentX = this.value ? this.maxTravel : 0; + this.requestUpdate(); + } + + /** + * Click handler - toggles the value + */ + private async handleClick(event: MouseEvent) { + if (this.isDragging || this.hasDragged) { + event.stopPropagation(); + event.preventDefault(); + return; + } + + if (this.disabled) { + return; + } + + if (this.required) { + // Bounce animation for required toggles + this.currentX = this.maxTravel; + this.requestUpdate(); + await new Promise((r) => setTimeout(r, 10)); + this.currentX = this.maxTravel - 3; + this.requestUpdate(); + await domtools.plugins.smartdelay.delayFor(150); + this.currentX = this.maxTravel; + this.requestUpdate(); + return; + } + + event.stopPropagation(); + event.preventDefault(); + + this.value = !this.value; + this.currentX = this.value ? this.maxTravel : 0; + this.requestUpdate(); + + this.dispatchEvent( + new CustomEvent('newValue', { + detail: this.value, + bubbles: true, + }) + ); + this.changeSubject.next(this); + + domtools.plugins.smartdelay.delayFor(0).then(() => { + this.hasDragged = false; + }); + } + + /** + * Pointer down - start dragging + */ + private onPointerDown(event: PointerEvent) { + if (this.required || this.disabled) { + return; + } + + this.isDragging = true; + this.startX = event.clientX - this.currentX; + (event.target as HTMLElement).setPointerCapture(event.pointerId); + } + + /** + * Pointer move - track drag position + */ + private onPointerMove(event: PointerEvent) { + if (!this.isDragging) return; + const newX = event.clientX - this.startX; + this.hasDragged = true; + + const toggleKnob = this.shadowRoot?.querySelector('.toggle-knob') as HTMLDivElement; + if (toggleKnob) { + toggleKnob.classList.add('dragging'); + } + + this.currentX = Math.max(0, Math.min(newX, this.maxTravel)); + this.requestUpdate(); + } + + /** + * Pointer up - complete drag and snap to nearest side + */ + private onPointerUp(event: PointerEvent) { + if (!this.isDragging) return; + (event.target as HTMLElement).releasePointerCapture(event.pointerId); + this.isDragging = false; + + if (!this.hasDragged) { + return; + } + + const toggleKnob = this.shadowRoot?.querySelector('.toggle-knob') as HTMLDivElement; + if (toggleKnob) { + toggleKnob.classList.remove('dragging'); + } + + // Snap to nearest side based on midpoint + const midpoint = this.maxTravel / 2; + this.value = this.currentX > midpoint; + this.currentX = this.value ? this.maxTravel : 0; + this.requestUpdate(); + + this.dispatchEvent( + new CustomEvent('newValue', { + detail: this.value, + bubbles: true, + }) + ); + this.changeSubject.next(this); + + domtools.plugins.smartdelay.delayFor(0).then(() => { + this.hasDragged = false; + }); + } + + /** + * Sync knob position when value is changed externally + */ + updated(changedProperties: Map): void { + super.updated(changedProperties); + if ( + changedProperties.has('value') && + !this.isDragging && + !this.hasDragged + ) { + this.currentX = this.value ? this.maxTravel : 0; + this.requestUpdate(); + } + } + + /** + * Keyboard support + */ + private handleKeydown(event: KeyboardEvent) { + if (event.key === ' ' || event.key === 'Enter') { + event.preventDefault(); + this.handleClick(event as unknown as MouseEvent); + } + } + + // DeesInputBase interface implementation + public getValue(): boolean { + return this.value; + } + + public setValue(valueArg: boolean): void { + this.value = valueArg; + } + + public focus(): void { + const track = this.shadowRoot?.querySelector('.toggle-track'); + if (track) { + (track as HTMLElement).focus(); + } + } +} diff --git a/ts_web/elements/00group-input/dees-input-toggle/index.ts b/ts_web/elements/00group-input/dees-input-toggle/index.ts new file mode 100644 index 0000000..975fdf2 --- /dev/null +++ b/ts_web/elements/00group-input/dees-input-toggle/index.ts @@ -0,0 +1 @@ +export * from './dees-input-toggle.js'; diff --git a/ts_web/elements/00group-input/index.ts b/ts_web/elements/00group-input/index.ts index a4fcbdd..9fca3f5 100644 --- a/ts_web/elements/00group-input/index.ts +++ b/ts_web/elements/00group-input/index.ts @@ -15,6 +15,7 @@ export * from './dees-input-richtext/index.js'; export * from './dees-input-searchselect/index.js'; export * from './dees-input-tags/index.js'; export * from './dees-input-text/index.js'; +export * from './dees-input-toggle/index.js'; export * from './dees-input-typelist/index.js'; export * from './dees-input-wysiwyg.js'; export * from './profilepicture/dees-input-profilepicture.js';