diff --git a/ts_web/elements/dees-form.ts b/ts_web/elements/dees-form.ts
index 1a835b3..1e8acf1 100644
--- a/ts_web/elements/dees-form.ts
+++ b/ts_web/elements/dees-form.ts
@@ -9,6 +9,7 @@ import {
import * as domtools from '@design.estate/dees-domtools';
import { DeesInputCheckbox } from './dees-input-checkbox.js';
+import { DeesInputDatepicker } from './dees-input-datepicker.js';
import { DeesInputText } from './dees-input-text.js';
import { DeesInputQuantitySelector } from './dees-input-quantityselector.js';
import { DeesInputRadiogroup } from './dees-input-radiogroup.js';
@@ -25,6 +26,7 @@ import { demoFunc } from './dees-form.demo.js';
// Unified set for form input types
const FORM_INPUT_TYPES = [
DeesInputCheckbox,
+ DeesInputDatepicker,
DeesInputDropdown,
DeesInputFileupload,
DeesInputIban,
@@ -39,6 +41,7 @@ const FORM_INPUT_TYPES = [
export type TFormInputElement =
| DeesInputCheckbox
+ | DeesInputDatepicker
| DeesInputDropdown
| DeesInputFileupload
| DeesInputIban
diff --git a/ts_web/elements/dees-input-datepicker.demo.ts b/ts_web/elements/dees-input-datepicker.demo.ts
new file mode 100644
index 0000000..83853e5
--- /dev/null
+++ b/ts_web/elements/dees-input-datepicker.demo.ts
@@ -0,0 +1,146 @@
+import { html } from '@design.estate/dees-element';
+import './dees-input-datepicker.js';
+
+export const demoFunc = () => html`
+
+
+
+
+
Basic Date Picker
+
Simple date selection without time
+
+
+
+
+
Date and Time Picker
+
Date selection with time in 24-hour format
+
+
+
+
+
12-Hour Time Format
+
Date and time with AM/PM selector
+
+
+
+
+
Date Range Constraints
+
Limit selectable dates with min and max
+
+
+
+
+
Custom Date Format
+
Different date display format
+
+
+
+
+
Required Field
+
Date picker as a required form field
+
+
+
+
+
Disabled State
+
Date picker in disabled state
+
+
+
+
+
Week Starts on Sunday
+
Calendar with Sunday as first day of week
+
+
+
+
+
With Disabled Dates
+
Some dates are disabled and cannot be selected
+
+
+
+
+
Event Listeners
+
Check console for change events
+
console.log('Date changed:', (e.target as any).value)}"
+ >
+
+
+`;
\ No newline at end of file
diff --git a/ts_web/elements/dees-input-datepicker.ts b/ts_web/elements/dees-input-datepicker.ts
new file mode 100644
index 0000000..059c76b
--- /dev/null
+++ b/ts_web/elements/dees-input-datepicker.ts
@@ -0,0 +1,917 @@
+import {
+ customElement,
+ type TemplateResult,
+ property,
+ html,
+ css,
+ cssManager,
+ state,
+} from '@design.estate/dees-element';
+import { DeesInputBase } from './dees-input-base.js';
+import { demoFunc } from './dees-input-datepicker.demo.js';
+import './dees-icon.js';
+import './dees-label.js';
+
+declare global {
+ interface HTMLElementTagNameMap {
+ 'dees-input-datepicker': DeesInputDatepicker;
+ }
+}
+
+@customElement('dees-input-datepicker')
+export class DeesInputDatepicker extends DeesInputBase {
+ public static demo = demoFunc;
+
+ @property({ type: String })
+ public value: string = '';
+
+ @property({ type: Boolean })
+ public enableTime: boolean = false;
+
+ @property({ type: String })
+ public timeFormat: '24h' | '12h' = '24h';
+
+ @property({ type: Number })
+ public minuteIncrement: number = 1;
+
+ @property({ type: String })
+ public dateFormat: string = 'DD/MM/YYYY';
+
+ @property({ type: String })
+ public minDate: string = '';
+
+ @property({ type: String })
+ public maxDate: string = '';
+
+ @property({ type: Array })
+ public disabledDates: string[] = [];
+
+ @property({ type: Number })
+ public weekStartsOn: 0 | 1 = 1; // Default to Monday
+
+ @property({ type: String })
+ public placeholder: string = 'Select date';
+
+ @state()
+ private isOpened: boolean = false;
+
+ @state()
+ private opensToTop: boolean = false;
+
+ @state()
+ private selectedDate: Date | null = null;
+
+ @state()
+ private viewDate: Date = new Date();
+
+ @state()
+ private selectedHour: number = 0;
+
+ @state()
+ private selectedMinute: number = 0;
+
+ public static styles = [
+ ...DeesInputBase.baseStyles,
+ cssManager.defaultStyles,
+ css`
+ :host {
+ display: block;
+ position: relative;
+ }
+
+ .input-container {
+ position: relative;
+ width: 100%;
+ }
+
+ .date-input {
+ width: 100%;
+ height: 40px;
+ padding: 0 12px;
+ background: ${cssManager.bdTheme('hsl(0 0% 100%)', 'hsl(224 71.4% 4.1%)')};
+ border: 1px solid ${cssManager.bdTheme('hsl(214.3 31.8% 91.4%)', 'hsl(217.2 32.6% 17.5%)')};
+ border-radius: 6px;
+ font-size: 14px;
+ line-height: 1.5;
+ color: ${cssManager.bdTheme('hsl(224 71.4% 4.1%)', 'hsl(210 20% 98%)')};
+ cursor: pointer;
+ transition: all 0.2s ease;
+ outline: none;
+ font-family: inherit;
+ }
+
+ .date-input::placeholder {
+ color: ${cssManager.bdTheme('hsl(220 8.9% 46.1%)', 'hsl(215 20.2% 65.1%)')};
+ }
+
+ .date-input:hover:not(:disabled) {
+ border-color: ${cssManager.bdTheme('hsl(214.3 31.8% 91.4%)', 'hsl(217.2 32.6% 17.5%)')};
+ background: ${cssManager.bdTheme('hsl(210 20% 98%)', 'hsl(215 27.9% 16.9%)')};
+ }
+
+ .date-input:focus,
+ .date-input.open {
+ border-color: ${cssManager.bdTheme('hsl(222.2 47.4% 11.2%)', 'hsl(210 20% 98%)')};
+ outline: 2px solid transparent;
+ outline-offset: 2px;
+ box-shadow: 0 0 0 2px ${cssManager.bdTheme('hsl(0 0% 100%)', 'hsl(224 71.4% 4.1%)')}, 0 0 0 4px ${cssManager.bdTheme('hsl(222.2 47.4% 11.2% / 0.1)', 'hsl(210 20% 98% / 0.1)')};
+ }
+
+ .date-input:disabled {
+ background: ${cssManager.bdTheme('hsl(210 20% 98%)', 'hsl(215 27.9% 16.9%)')};
+ color: ${cssManager.bdTheme('hsl(220 8.9% 46.1%)', 'hsl(215 20.2% 65.1%)')};
+ cursor: not-allowed;
+ opacity: 0.5;
+ }
+
+ /* Icon container using flexbox for better positioning */
+ .icon-container {
+ position: absolute;
+ right: 0;
+ top: 0;
+ bottom: 0;
+ display: flex;
+ align-items: center;
+ gap: 4px;
+ padding: 0 12px;
+ pointer-events: none;
+ }
+
+ .icon-container > * {
+ pointer-events: auto;
+ }
+
+ .calendar-icon {
+ color: ${cssManager.bdTheme('hsl(220 8.9% 46.1%)', 'hsl(215 20.2% 65.1%)')};
+ pointer-events: none;
+ display: flex;
+ align-items: center;
+ justify-content: center;
+ }
+
+ .clear-button {
+ width: 20px;
+ height: 20px;
+ border: none;
+ background: transparent;
+ cursor: pointer;
+ display: flex;
+ align-items: center;
+ justify-content: center;
+ border-radius: 4px;
+ color: ${cssManager.bdTheme('hsl(220 8.9% 46.1%)', 'hsl(215 20.2% 65.1%)')};
+ transition: opacity 0.2s ease, background-color 0.2s ease;
+ padding: 0;
+ flex-shrink: 0;
+ }
+
+ .clear-button:hover {
+ background: ${cssManager.bdTheme('hsl(210 20% 98%)', 'hsl(215 27.9% 16.9%)')};
+ color: ${cssManager.bdTheme('hsl(224 71.4% 4.1%)', 'hsl(210 20% 98%)')};
+ }
+
+ .clear-button:disabled {
+ display: none;
+ }
+
+ /* Calendar Popup Styles */
+ .calendar-popup {
+ will-change: transform, opacity;
+ pointer-events: none;
+ transition: all 0.2s ease;
+ opacity: 0;
+ transform: translateY(-4px);
+ background: ${cssManager.bdTheme('hsl(0 0% 100%)', 'hsl(224 71.4% 4.1%)')};
+ border: 1px solid ${cssManager.bdTheme('hsl(214.3 31.8% 91.4%)', 'hsl(217.2 32.6% 17.5%)')};
+ box-shadow: ${cssManager.bdTheme(
+ '0 10px 15px -3px hsl(0 0% 0% / 0.1), 0 4px 6px -4px hsl(0 0% 0% / 0.1)',
+ '0 10px 15px -3px hsl(0 0% 0% / 0.2), 0 4px 6px -4px hsl(0 0% 0% / 0.2)'
+ )};
+ border-radius: 6px;
+ padding: 12px;
+ position: absolute;
+ user-select: none;
+ margin-top: 4px;
+ z-index: 50;
+ left: 0;
+ min-width: 280px;
+ }
+
+ .calendar-popup.top {
+ bottom: calc(100% + 4px);
+ top: auto;
+ margin-top: 0;
+ margin-bottom: 4px;
+ transform: translateY(4px);
+ }
+
+ .calendar-popup.bottom {
+ top: 100%;
+ }
+
+ .calendar-popup.show {
+ pointer-events: all;
+ transform: translateY(0);
+ opacity: 1;
+ }
+
+ /* Calendar Header */
+ .calendar-header {
+ display: flex;
+ align-items: center;
+ justify-content: space-between;
+ margin-bottom: 16px;
+ gap: 8px;
+ }
+
+ .month-year-display {
+ font-weight: 500;
+ font-size: 14px;
+ color: ${cssManager.bdTheme('hsl(224 71.4% 4.1%)', 'hsl(210 20% 98%)')};
+ flex: 1;
+ text-align: center;
+ }
+
+ .nav-button {
+ width: 28px;
+ height: 28px;
+ border: none;
+ background: transparent;
+ cursor: pointer;
+ border-radius: 6px;
+ display: flex;
+ align-items: center;
+ justify-content: center;
+ color: ${cssManager.bdTheme('hsl(220 8.9% 46.1%)', 'hsl(215 20.2% 65.1%)')};
+ transition: all 0.2s ease;
+ }
+
+ .nav-button:hover {
+ background: ${cssManager.bdTheme('hsl(210 20% 98%)', 'hsl(215 27.9% 16.9%)')};
+ color: ${cssManager.bdTheme('hsl(224 71.4% 4.1%)', 'hsl(210 20% 98%)')};
+ }
+
+ .nav-button:active {
+ background: ${cssManager.bdTheme('hsl(214.3 31.8% 91.4%)', 'hsl(217.2 32.6% 17.5%)')};
+ }
+
+ /* Weekday headers */
+ .weekdays {
+ display: grid;
+ grid-template-columns: repeat(7, 1fr);
+ gap: 0;
+ margin-bottom: 4px;
+ }
+
+ .weekday {
+ text-align: center;
+ font-size: 12px;
+ font-weight: 400;
+ color: ${cssManager.bdTheme('hsl(220 8.9% 46.1%)', 'hsl(215 20.2% 65.1%)')};
+ padding: 0 0 8px 0;
+ }
+
+ /* Days grid */
+ .days-grid {
+ display: grid;
+ grid-template-columns: repeat(7, 1fr);
+ gap: 2px;
+ }
+
+ .day {
+ aspect-ratio: 1;
+ display: flex;
+ align-items: center;
+ justify-content: center;
+ cursor: pointer;
+ border-radius: 6px;
+ font-size: 14px;
+ transition: all 0.2s ease;
+ color: ${cssManager.bdTheme('hsl(224 71.4% 4.1%)', 'hsl(210 20% 98%)')};
+ border: none;
+ width: 36px;
+ height: 36px;
+ background: transparent;
+ }
+
+ .day:hover:not(.disabled) {
+ background: ${cssManager.bdTheme('hsl(210 20% 98%)', 'hsl(215 27.9% 16.9%)')};
+ }
+
+ .day.other-month {
+ color: ${cssManager.bdTheme('hsl(220 8.9% 46.1%)', 'hsl(215 20.2% 65.1%)')};
+ opacity: 0.5;
+ }
+
+ .day.today {
+ background: ${cssManager.bdTheme('hsl(210 20% 98%)', 'hsl(215 27.9% 16.9%)')};
+ font-weight: 500;
+ }
+
+ .day.selected {
+ background: ${cssManager.bdTheme('hsl(222.2 47.4% 11.2%)', 'hsl(210 20% 98%)')};
+ color: ${cssManager.bdTheme('hsl(210 20% 98%)', 'hsl(222.2 47.4% 11.2%)')};
+ font-weight: 500;
+ }
+
+ .day.disabled {
+ color: ${cssManager.bdTheme('hsl(220 8.9% 46.1%)', 'hsl(215 20.2% 65.1%)')};
+ cursor: not-allowed;
+ opacity: 0.3;
+ }
+
+ /* Time selector */
+ .time-selector {
+ margin-top: 12px;
+ padding-top: 12px;
+ border-top: 1px solid ${cssManager.bdTheme('hsl(214.3 31.8% 91.4%)', 'hsl(217.2 32.6% 17.5%)')};
+ }
+
+ .time-selector-title {
+ font-size: 12px;
+ font-weight: 500;
+ margin-bottom: 8px;
+ color: ${cssManager.bdTheme('hsl(220 8.9% 46.1%)', 'hsl(215 20.2% 65.1%)')};
+ }
+
+ .time-inputs {
+ display: flex;
+ gap: 8px;
+ align-items: center;
+ }
+
+ .time-input {
+ width: 65px;
+ height: 36px;
+ border: 1px solid ${cssManager.bdTheme('hsl(214.3 31.8% 91.4%)', 'hsl(217.2 32.6% 17.5%)')};
+ border-radius: 6px;
+ padding: 0 12px;
+ font-size: 14px;
+ text-align: center;
+ background: ${cssManager.bdTheme('hsl(0 0% 100%)', 'hsl(224 71.4% 4.1%)')};
+ color: ${cssManager.bdTheme('hsl(224 71.4% 4.1%)', 'hsl(210 20% 98%)')};
+ transition: all 0.2s ease;
+ }
+
+ .time-input:hover {
+ border-color: ${cssManager.bdTheme('hsl(214.3 31.8% 91.4%)', 'hsl(217.2 32.6% 17.5%)')};
+ background: ${cssManager.bdTheme('hsl(210 20% 98%)', 'hsl(215 27.9% 16.9%)')};
+ }
+
+ .time-input:focus {
+ outline: none;
+ border-color: ${cssManager.bdTheme('hsl(222.2 47.4% 11.2%)', 'hsl(210 20% 98%)')};
+ box-shadow: 0 0 0 2px ${cssManager.bdTheme('hsl(222.2 47.4% 11.2% / 0.1)', 'hsl(210 20% 98% / 0.1)')};
+ }
+
+ .time-separator {
+ font-size: 14px;
+ font-weight: 500;
+ color: ${cssManager.bdTheme('hsl(220 8.9% 46.1%)', 'hsl(215 20.2% 65.1%)')};
+ }
+
+ .am-pm-selector {
+ display: flex;
+ gap: 4px;
+ margin-left: 8px;
+ }
+
+ .am-pm-button {
+ padding: 6px 12px;
+ border: 1px solid ${cssManager.bdTheme('hsl(214.3 31.8% 91.4%)', 'hsl(217.2 32.6% 17.5%)')};
+ background: ${cssManager.bdTheme('hsl(0 0% 100%)', 'hsl(224 71.4% 4.1%)')};
+ border-radius: 6px;
+ font-size: 12px;
+ font-weight: 500;
+ cursor: pointer;
+ transition: all 0.2s ease;
+ color: ${cssManager.bdTheme('hsl(220 8.9% 46.1%)', 'hsl(215 20.2% 65.1%)')};
+ }
+
+ .am-pm-button.selected {
+ background: ${cssManager.bdTheme('hsl(222.2 47.4% 11.2%)', 'hsl(210 20% 98%)')};
+ color: ${cssManager.bdTheme('hsl(210 20% 98%)', 'hsl(222.2 47.4% 11.2%)')};
+ border-color: ${cssManager.bdTheme('hsl(222.2 47.4% 11.2%)', 'hsl(210 20% 98%)')};
+ }
+
+ .am-pm-button:hover:not(.selected) {
+ background: ${cssManager.bdTheme('hsl(210 20% 98%)', 'hsl(215 27.9% 16.9%)')};
+ border-color: ${cssManager.bdTheme('hsl(214.3 31.8% 91.4%)', 'hsl(217.2 32.6% 17.5%)')};
+ }
+
+ /* Action buttons */
+ .calendar-actions {
+ display: flex;
+ gap: 8px;
+ margin-top: 12px;
+ padding-top: 12px;
+ border-top: 1px solid ${cssManager.bdTheme('hsl(214.3 31.8% 91.4%)', 'hsl(217.2 32.6% 17.5%)')};
+ }
+
+ .action-button {
+ flex: 1;
+ height: 36px;
+ border: none;
+ border-radius: 6px;
+ font-size: 14px;
+ font-weight: 500;
+ cursor: pointer;
+ transition: all 0.2s ease;
+ display: flex;
+ align-items: center;
+ justify-content: center;
+ }
+
+ .today-button {
+ background: ${cssManager.bdTheme('hsl(0 0% 100%)', 'hsl(224 71.4% 4.1%)')};
+ border: 1px solid ${cssManager.bdTheme('hsl(214.3 31.8% 91.4%)', 'hsl(217.2 32.6% 17.5%)')};
+ color: ${cssManager.bdTheme('hsl(224 71.4% 4.1%)', 'hsl(210 20% 98%)')};
+ }
+
+ .today-button:hover {
+ background: ${cssManager.bdTheme('hsl(210 20% 98%)', 'hsl(215 27.9% 16.9%)')};
+ border-color: ${cssManager.bdTheme('hsl(214.3 31.8% 91.4%)', 'hsl(217.2 32.6% 17.5%)')};
+ }
+
+ .today-button:active {
+ background: ${cssManager.bdTheme('hsl(214.3 31.8% 91.4%)', 'hsl(217.2 32.6% 17.5%)')};
+ }
+
+ .clear-button {
+ background: transparent;
+ border: 1px solid transparent;
+ color: ${cssManager.bdTheme('hsl(220 8.9% 46.1%)', 'hsl(215 20.2% 65.1%)')};
+ }
+
+ .clear-button:hover {
+ background: ${cssManager.bdTheme('hsl(0 72.2% 50.6% / 0.1)', 'hsl(0 62.8% 30.6% / 0.1)')};
+ color: ${cssManager.bdTheme('hsl(0 72.2% 50.6%)', 'hsl(0 62.8% 30.6%)')};
+ }
+
+ .clear-button:active {
+ background: ${cssManager.bdTheme('hsl(0 72.2% 50.6% / 0.2)', 'hsl(0 62.8% 30.6% / 0.2)')};
+ }
+ `,
+ ];
+
+ render(): TemplateResult {
+ const monthNames = [
+ 'January', 'February', 'March', 'April', 'May', 'June',
+ 'July', 'August', 'September', 'October', 'November', 'December'
+ ];
+
+ const weekDays = this.weekStartsOn === 1
+ ? ['Mo', 'Tu', 'We', 'Th', 'Fr', 'Sa', 'Su']
+ : ['Su', 'Mo', 'Tu', 'We', 'Th', 'Fr', 'Sa'];
+
+ const days = this.getDaysInMonth();
+ const isAM = this.selectedHour < 12;
+
+ return html`
+
+ `;
+ }
+
+ async connectedCallback() {
+ super.connectedCallback();
+ this.handleClickOutside = this.handleClickOutside.bind(this);
+ }
+
+ async disconnectedCallback() {
+ await super.disconnectedCallback();
+ document.removeEventListener('click', this.handleClickOutside);
+ }
+
+ async firstUpdated() {
+ // Initialize with empty value if not set
+ if (!this.value) {
+ this.value = '';
+ }
+
+ // Initialize view date and selected time
+ if (this.value) {
+ try {
+ const date = new Date(this.value);
+ if (!isNaN(date.getTime())) {
+ this.selectedDate = date;
+ this.viewDate = new Date(date);
+ this.selectedHour = date.getHours();
+ this.selectedMinute = date.getMinutes();
+ }
+ } catch {
+ // Invalid date
+ }
+ } else {
+ const now = new Date();
+ this.viewDate = new Date(now);
+ this.selectedHour = now.getHours();
+ this.selectedMinute = 0;
+ }
+ }
+
+ private formatDate(isoString: string): string {
+ if (!isoString) return '';
+
+ try {
+ const date = new Date(isoString);
+ if (isNaN(date.getTime())) return '';
+
+ let formatted = this.dateFormat;
+
+ // Basic date formatting
+ const day = date.getDate().toString().padStart(2, '0');
+ const month = (date.getMonth() + 1).toString().padStart(2, '0');
+ const year = date.getFullYear().toString();
+
+ formatted = formatted.replace('DD', day);
+ formatted = formatted.replace('MM', month);
+ formatted = formatted.replace('YYYY', year);
+ formatted = formatted.replace('YY', year.slice(-2));
+
+ // Time formatting if enabled
+ if (this.enableTime) {
+ const hours24 = date.getHours();
+ const hours12 = hours24 === 0 ? 12 : hours24 > 12 ? hours24 - 12 : hours24;
+ const minutes = date.getMinutes().toString().padStart(2, '0');
+ const ampm = hours24 >= 12 ? 'PM' : 'AM';
+
+ if (this.timeFormat === '12h') {
+ formatted += ` ${hours12}:${minutes} ${ampm}`;
+ } else {
+ formatted += ` ${hours24.toString().padStart(2, '0')}:${minutes}`;
+ }
+ }
+
+ return formatted;
+ } catch {
+ return '';
+ }
+ }
+
+ private handleClickOutside = (event: MouseEvent) => {
+ const path = event.composedPath();
+ if (!path.includes(this)) {
+ this.isOpened = false;
+ document.removeEventListener('click', this.handleClickOutside);
+ }
+ };
+
+ private async toggleCalendar(): Promise {
+ if (this.disabled) return;
+
+ this.isOpened = !this.isOpened;
+
+ if (this.isOpened) {
+ // Check available space and set position
+ const inputContainer = this.shadowRoot!.querySelector('.input-container') as HTMLElement;
+ const rect = inputContainer.getBoundingClientRect();
+ const spaceBelow = window.innerHeight - rect.bottom;
+ const spaceAbove = rect.top;
+
+ // Determine if we should open upwards (approximate height of 400px)
+ this.opensToTop = spaceBelow < 400 && spaceAbove > spaceBelow;
+
+ // Add click outside listener
+ setTimeout(() => {
+ document.addEventListener('click', this.handleClickOutside);
+ }, 0);
+ } else {
+ document.removeEventListener('click', this.handleClickOutside);
+ }
+ }
+
+ private getDaysInMonth(): Date[] {
+ const year = this.viewDate.getFullYear();
+ const month = this.viewDate.getMonth();
+ const firstDay = new Date(year, month, 1);
+ const lastDay = new Date(year, month + 1, 0);
+ const days: Date[] = [];
+
+ // Adjust for week start
+ const startOffset = this.weekStartsOn === 1
+ ? (firstDay.getDay() === 0 ? 6 : firstDay.getDay() - 1)
+ : firstDay.getDay();
+
+ // Add days from previous month
+ for (let i = startOffset; i > 0; i--) {
+ days.push(new Date(year, month, 1 - i));
+ }
+
+ // Add days of current month
+ for (let i = 1; i <= lastDay.getDate(); i++) {
+ days.push(new Date(year, month, i));
+ }
+
+ // Add days from next month to complete the grid (6 rows)
+ const remainingDays = 42 - days.length;
+ for (let i = 1; i <= remainingDays; i++) {
+ days.push(new Date(year, month + 1, i));
+ }
+
+ return days;
+ }
+
+ private isToday(date: Date): boolean {
+ const today = new Date();
+ return date.getDate() === today.getDate() &&
+ date.getMonth() === today.getMonth() &&
+ date.getFullYear() === today.getFullYear();
+ }
+
+ private isSelected(date: Date): boolean {
+ if (!this.selectedDate) return false;
+ return date.getDate() === this.selectedDate.getDate() &&
+ date.getMonth() === this.selectedDate.getMonth() &&
+ date.getFullYear() === this.selectedDate.getFullYear();
+ }
+
+ private isDisabled(date: Date): boolean {
+ // Check min date
+ if (this.minDate) {
+ const min = new Date(this.minDate);
+ if (date < min) return true;
+ }
+
+ // Check max date
+ if (this.maxDate) {
+ const max = new Date(this.maxDate);
+ if (date > max) return true;
+ }
+
+ // Check disabled dates
+ if (this.disabledDates && this.disabledDates.length > 0) {
+ return this.disabledDates.some(disabledStr => {
+ try {
+ const disabled = new Date(disabledStr);
+ return date.getDate() === disabled.getDate() &&
+ date.getMonth() === disabled.getMonth() &&
+ date.getFullYear() === disabled.getFullYear();
+ } catch {
+ return false;
+ }
+ });
+ }
+
+ return false;
+ }
+
+ private selectDate(date: Date): void {
+ this.selectedDate = new Date(
+ date.getFullYear(),
+ date.getMonth(),
+ date.getDate(),
+ this.selectedHour,
+ this.selectedMinute
+ );
+
+ this.value = this.selectedDate.toISOString();
+ this.changeSubject.next(this);
+
+ if (!this.enableTime) {
+ this.isOpened = false;
+ }
+ }
+
+ private selectToday(): void {
+ const today = new Date();
+ this.selectedDate = today;
+ this.viewDate = new Date(today);
+ this.selectedHour = today.getHours();
+ this.selectedMinute = today.getMinutes();
+
+ this.value = this.selectedDate.toISOString();
+ this.changeSubject.next(this);
+
+ if (!this.enableTime) {
+ this.isOpened = false;
+ }
+ }
+
+ private clear(): void {
+ this.value = '';
+ this.selectedDate = null;
+ this.changeSubject.next(this);
+ this.isOpened = false;
+ }
+
+ private previousMonth(): void {
+ this.viewDate = new Date(this.viewDate.getFullYear(), this.viewDate.getMonth() - 1, 1);
+ }
+
+ private nextMonth(): void {
+ this.viewDate = new Date(this.viewDate.getFullYear(), this.viewDate.getMonth() + 1, 1);
+ }
+
+ private handleHourInput(e: InputEvent): void {
+ const input = e.target as HTMLInputElement;
+ let value = parseInt(input.value) || 0;
+
+ if (this.timeFormat === '12h') {
+ value = Math.max(1, Math.min(12, value));
+ // Convert to 24h format
+ if (this.selectedHour >= 12 && value !== 12) {
+ this.selectedHour = value + 12;
+ } else if (this.selectedHour < 12 && value === 12) {
+ this.selectedHour = 0;
+ } else {
+ this.selectedHour = value;
+ }
+ } else {
+ this.selectedHour = Math.max(0, Math.min(23, value));
+ }
+
+ this.updateSelectedDateTime();
+ }
+
+ private handleMinuteInput(e: InputEvent): void {
+ const input = e.target as HTMLInputElement;
+ let value = parseInt(input.value) || 0;
+ value = Math.max(0, Math.min(59, value));
+
+ if (this.minuteIncrement && this.minuteIncrement > 1) {
+ value = Math.round(value / this.minuteIncrement) * this.minuteIncrement;
+ }
+
+ this.selectedMinute = value;
+ this.updateSelectedDateTime();
+ }
+
+ private setAMPM(period: 'am' | 'pm'): void {
+ if (period === 'am' && this.selectedHour >= 12) {
+ this.selectedHour -= 12;
+ } else if (period === 'pm' && this.selectedHour < 12) {
+ this.selectedHour += 12;
+ }
+ this.updateSelectedDateTime();
+ }
+
+ private updateSelectedDateTime(): void {
+ if (this.selectedDate) {
+ this.selectedDate = new Date(
+ this.selectedDate.getFullYear(),
+ this.selectedDate.getMonth(),
+ this.selectedDate.getDate(),
+ this.selectedHour,
+ this.selectedMinute
+ );
+ this.value = this.selectedDate.toISOString();
+ this.changeSubject.next(this);
+ }
+ }
+
+ private handleKeydown(e: KeyboardEvent): void {
+ if (e.key === 'Enter' || e.key === ' ') {
+ e.preventDefault();
+ this.toggleCalendar();
+ } else if (e.key === 'Escape' && this.isOpened) {
+ e.preventDefault();
+ this.isOpened = false;
+ }
+ }
+
+ private clearValue(e: Event): void {
+ e.stopPropagation();
+ this.value = '';
+ this.selectedDate = null;
+ this.changeSubject.next(this);
+ }
+
+ public getValue(): string {
+ return this.value;
+ }
+
+ public setValue(value: string): void {
+ this.value = value;
+ if (value) {
+ try {
+ const date = new Date(value);
+ if (!isNaN(date.getTime())) {
+ this.selectedDate = date;
+ this.viewDate = new Date(date);
+ this.selectedHour = date.getHours();
+ this.selectedMinute = date.getMinutes();
+ }
+ } catch {
+ // Invalid date
+ }
+ }
+ }
+}
\ No newline at end of file
diff --git a/ts_web/elements/index.ts b/ts_web/elements/index.ts
index cdf29e5..6357b96 100644
--- a/ts_web/elements/index.ts
+++ b/ts_web/elements/index.ts
@@ -28,6 +28,7 @@ export * from './dees-heading.js';
export * from './dees-hint.js';
export * from './dees-icon.js';
export * from './dees-input-checkbox.js';
+export * from './dees-input-datepicker.js';
export * from './dees-input-dropdown.js';
export * from './dees-input-fileupload.js';
export * from './dees-input-iban.js';
diff --git a/ts_web/pages/input-showcase.ts b/ts_web/pages/input-showcase.ts
index f6387ed..85f0561 100644
--- a/ts_web/pages/input-showcase.ts
+++ b/ts_web/pages/input-showcase.ts
@@ -443,6 +443,32 @@ export const inputShowcase = () => html`
Specialized input components for specific data types like phone numbers, IBAN, and file uploads.
+
+
+
+
+
+
+
+
+
+