@design.estate/dees-catalog 9.6.2 → 9.7.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (51) hide show
  1. package/dist_bundle/bundle.js +2945 -2681
  2. package/dist_bundle/bundle.js.map +1 -1
  3. package/dist_ts_web/00_commitinfo_data.js +1 -1
  4. package/dist_ts_web/demos/inputs.d.ts +1 -1
  5. package/dist_ts_web/demos/inputs.js +5 -4
  6. package/dist_ts_web/elements/00group-form/dees-form/dees-form.d.ts +5 -1
  7. package/dist_ts_web/elements/00group-form/dees-form/dees-form.js +27 -3
  8. package/dist_ts_web/elements/00group-harness/dees-harness-chat/dees-harness-chat.d.ts +1 -0
  9. package/dist_ts_web/elements/00group-harness/dees-harness-chat/dees-harness-chat.js +11 -2
  10. package/dist_ts_web/elements/00group-harness/dees-harness-composer/dees-harness-composer.d.ts +3 -0
  11. package/dist_ts_web/elements/00group-harness/dees-harness-composer/dees-harness-composer.demo.js +4 -1
  12. package/dist_ts_web/elements/00group-harness/dees-harness-composer/dees-harness-composer.js +51 -13
  13. package/dist_ts_web/elements/00group-input/dees-input-datepicker/component.d.ts +18 -3
  14. package/dist_ts_web/elements/00group-input/dees-input-datepicker/component.js +181 -144
  15. package/dist_ts_web/elements/00group-input/dees-input-datepicker/date-validation.d.ts +13 -0
  16. package/dist_ts_web/elements/00group-input/dees-input-datepicker/date-validation.js +65 -0
  17. package/dist_ts_web/elements/00group-input/dees-input-datepicker/datepicker-popup.js +44 -56
  18. package/dist_ts_web/elements/00group-input/dees-input-datepicker/styles.js +6 -1
  19. package/dist_ts_web/elements/00group-input/dees-input-datepicker/template.js +11 -5
  20. package/dist_ts_web/elements/00group-input/dees-input-text/dees-input-text.d.ts +8 -8
  21. package/dist_ts_web/elements/00group-input/dees-input-text/dees-input-text.js +6 -53
  22. package/dist_ts_web/elements/00group-input/dees-input-textarea/dees-input-textarea.d.ts +62 -0
  23. package/dist_ts_web/elements/00group-input/dees-input-textarea/dees-input-textarea.demo.d.ts +1 -0
  24. package/dist_ts_web/elements/00group-input/dees-input-textarea/dees-input-textarea.demo.js +3 -0
  25. package/dist_ts_web/elements/00group-input/dees-input-textarea/dees-input-textarea.js +224 -0
  26. package/dist_ts_web/elements/00group-input/dees-input-textarea/index.d.ts +1 -0
  27. package/dist_ts_web/elements/00group-input/dees-input-textarea/index.js +2 -0
  28. package/dist_ts_web/elements/00group-input/index.d.ts +1 -0
  29. package/dist_ts_web/elements/00group-input/index.js +2 -1
  30. package/dist_ts_web/elements/00group-input/text-input-contextmenu.d.ts +29 -0
  31. package/dist_ts_web/elements/00group-input/text-input-contextmenu.js +29 -0
  32. package/package.json +1 -1
  33. package/readme.md +59 -4
  34. package/scripts/check-packed-consumer.mjs +8 -5
  35. package/ts_web/00_commitinfo_data.ts +1 -1
  36. package/ts_web/demos/inputs.ts +5 -4
  37. package/ts_web/elements/00group-form/dees-form/dees-form.ts +25 -2
  38. package/ts_web/elements/00group-harness/dees-harness-chat/dees-harness-chat.ts +4 -0
  39. package/ts_web/elements/00group-harness/dees-harness-composer/dees-harness-composer.demo.ts +3 -0
  40. package/ts_web/elements/00group-harness/dees-harness-composer/dees-harness-composer.ts +37 -11
  41. package/ts_web/elements/00group-input/dees-input-datepicker/component.ts +149 -148
  42. package/ts_web/elements/00group-input/dees-input-datepicker/date-validation.ts +64 -0
  43. package/ts_web/elements/00group-input/dees-input-datepicker/datepicker-popup.ts +40 -40
  44. package/ts_web/elements/00group-input/dees-input-datepicker/styles.ts +5 -0
  45. package/ts_web/elements/00group-input/dees-input-datepicker/template.ts +10 -4
  46. package/ts_web/elements/00group-input/dees-input-text/dees-input-text.ts +5 -52
  47. package/ts_web/elements/00group-input/dees-input-textarea/dees-input-textarea.demo.ts +2 -0
  48. package/ts_web/elements/00group-input/dees-input-textarea/dees-input-textarea.ts +128 -0
  49. package/ts_web/elements/00group-input/dees-input-textarea/index.ts +1 -0
  50. package/ts_web/elements/00group-input/index.ts +1 -0
  51. package/ts_web/elements/00group-input/text-input-contextmenu.ts +31 -0
@@ -203,6 +203,9 @@ export class DeesHarnessChat extends DeesElement {
203
203
  @property({ attribute: false })
204
204
  accessor attachments: IHarnessAttachment[] = [];
205
205
 
206
+ @property({ attribute: false })
207
+ accessor attachmentsEnabled: boolean = true;
208
+
206
209
  @property({ attribute: false })
207
210
  accessor suggestions: IHarnessComposerSuggestion[] = [];
208
211
 
@@ -763,6 +766,7 @@ export class DeesHarnessChat extends DeesElement {
763
766
  .contextDescription=${this.heading ? `Replying to ${this.heading}` : ''}
764
767
  .value=${this.composerValue}
765
768
  .attachments=${this.attachments}
769
+ .attachmentsEnabled=${this.attachmentsEnabled}
766
770
  .suggestions=${this.suggestions}
767
771
  .maxAttachmentCount=${this.maxAttachmentCount}
768
772
  .maxAttachmentBytes=${this.maxAttachmentBytes}
@@ -44,6 +44,9 @@ export const demoFunc = () => demoPage('Composer', 'Write, attach files, choose
44
44
  <dees-harness-composer busy queuedCount="2"></dees-harness-composer>
45
45
  <div class="dees-demo-label">Waiting for a connection</div>
46
46
  <dees-harness-composer disabled placeholder="Connect to a project to chat."></dees-harness-composer>
47
+ <div class="dees-demo-label">Text-only conversation</div>
48
+ <dees-harness-composer .attachmentsEnabled=${false} .showModelControls=${false}
49
+ value="Summarize the outstanding invoices."></dees-harness-composer>
47
50
  <div class="dees-demo-label">Files ready to send</div>
48
51
  <dees-harness-composer
49
52
  .showModelControls=${false}
@@ -112,6 +112,10 @@ export class DeesHarnessComposer extends DeesElement {
112
112
  @property({ attribute: false })
113
113
  accessor attachments: IHarnessAttachment[] = [];
114
114
 
115
+ /** Host capability: false disables every attachment entry point and sends text only. */
116
+ @property({ attribute: false })
117
+ accessor attachmentsEnabled: boolean = true;
118
+
115
119
  @property({ attribute: false })
116
120
  accessor suggestions: IHarnessComposerSuggestion[] = [];
117
121
 
@@ -473,6 +477,10 @@ export class DeesHarnessComposer extends DeesElement {
473
477
 
474
478
  public updated(changedProperties: Map<PropertyKey, unknown>): void {
475
479
  super.updated(changedProperties);
480
+ if (['attachmentsEnabled', 'disabled'].some(key => changedProperties.has(key) && changedProperties.get(key) !== undefined)) {
481
+ this.attachmentGeneration++;
482
+ if (!this.attachmentsEnabled || this.disabled) this.handleDragLeave();
483
+ }
476
484
  this.syncTextareaSuggestionAria();
477
485
  }
478
486
 
@@ -486,13 +494,13 @@ export class DeesHarnessComposer extends DeesElement {
486
494
  @dragleave=${this.handleDragLeave}
487
495
  @drop=${this.handleDrop}
488
496
  >
489
- <input
497
+ ${this.attachmentsEnabled ? html`<input
490
498
  class="fileInput"
491
499
  type="file"
492
500
  multiple
493
501
  @change=${this.handleFileInputChange}
494
- />
495
- ${this.attachments.length
502
+ />` : ''}
503
+ ${this.attachmentsEnabled && this.attachments.length
496
504
  ? html`
497
505
  <div class="attachmentRow">
498
506
  ${this.attachments.map((attachment) => this.renderChip(attachment))}
@@ -505,7 +513,7 @@ export class DeesHarnessComposer extends DeesElement {
505
513
  .value=${this.value}
506
514
  aria-label="Message"
507
515
  aria-description=${this.contextDescription}
508
- placeholder=${this.placeholder}
516
+ placeholder=${!this.attachmentsEnabled && this.placeholder === 'Message the agent — drop or paste files to attach…' ? 'Message the agent…' : this.placeholder}
509
517
  ?disabled=${this.disabled}
510
518
  rows="1"
511
519
  @input=${this.handleTextInput}
@@ -518,14 +526,16 @@ export class DeesHarnessComposer extends DeesElement {
518
526
  ></textarea>
519
527
  </div>
520
528
  <div class="controlsRow">
521
- <dees-button
529
+ ${this.attachmentsEnabled ? html`<dees-button
530
+ class="attachButton"
522
531
  size="sm"
523
532
  type="ghost"
533
+ title="Attach files"
524
534
  ?disabled=${this.disabled}
525
535
  @clicked=${() => this.shadowRoot?.querySelector<HTMLInputElement>('.fileInput')?.click()}
526
536
  >
527
537
  <dees-icon icon="lucide:Paperclip" iconSize="14"></dees-icon>
528
- </dees-button>
538
+ </dees-button>` : ''}
529
539
  ${this.modeOptions.length
530
540
  ? html`
531
541
  <dees-input-multitoggle class="modeSwitch" label="Agent mode" .labelPosition=${'none'}
@@ -869,6 +879,7 @@ export class DeesHarnessComposer extends DeesElement {
869
879
  };
870
880
 
871
881
  private handlePaste = (event: ClipboardEvent): void => {
882
+ if (!this.attachmentsEnabled || this.disabled) return;
872
883
  const files = Array.from(event.clipboardData?.files ?? []);
873
884
  if (!files.length) return;
874
885
  event.preventDefault();
@@ -878,6 +889,10 @@ export class DeesHarnessComposer extends DeesElement {
878
889
  private handleDragOver = (event: DragEvent): void => {
879
890
  if (!event.dataTransfer?.types.includes('Files')) return;
880
891
  event.preventDefault();
892
+ if (!this.attachmentsEnabled || this.disabled) {
893
+ event.dataTransfer.dropEffect = 'none';
894
+ return;
895
+ }
881
896
  this.shadowRoot?.querySelector('.composer')?.classList.add('dragOver');
882
897
  };
883
898
 
@@ -890,23 +905,32 @@ export class DeesHarnessComposer extends DeesElement {
890
905
  const files = Array.from(event.dataTransfer?.files ?? []);
891
906
  if (!files.length) return;
892
907
  event.preventDefault();
908
+ if (!this.attachmentsEnabled || this.disabled) {
909
+ if (event.dataTransfer) event.dataTransfer.dropEffect = 'none';
910
+ return;
911
+ }
893
912
  void this.addFiles(files);
894
913
  };
895
914
 
896
915
  private attachmentAdditionTail = Promise.resolve();
916
+ private attachmentGeneration = 0;
897
917
 
898
918
  public addFiles(fileList: FileList | File[] | null): Promise<void> {
899
919
  const files = Array.from(fileList ?? []);
900
- if (!files.length) return Promise.resolve();
901
- const addition = this.attachmentAdditionTail.then(() => this.addFilesSequential(files));
920
+ if (!files.length || !this.attachmentsEnabled || this.disabled) return Promise.resolve();
921
+ const generation = this.attachmentGeneration;
922
+ const addition = this.attachmentAdditionTail.then(() => this.addFilesSequential(files, generation));
902
923
  this.attachmentAdditionTail = addition.catch(() => undefined);
903
924
  return addition;
904
925
  }
905
926
 
906
- private async addFilesSequential(files: File[]): Promise<void> {
927
+ private async addFilesSequential(files: File[], generation: number): Promise<void> {
928
+ const allowed = () => this.attachmentsEnabled && !this.disabled && generation === this.attachmentGeneration;
929
+ if (!allowed()) return;
907
930
  const added: IHarnessAttachment[] = [];
908
931
  let totalBytes = this.attachments.reduce((sum, attachment) => sum + attachment.size, 0);
909
932
  for (const file of files) {
933
+ if (!allowed()) return;
910
934
  if (this.attachments.length + added.length >= this.maxAttachmentCount) {
911
935
  this.emit('harness-attachment-error', {
912
936
  message: `A maximum of ${this.maxAttachmentCount} attachments is allowed.`,
@@ -928,6 +952,7 @@ export class DeesHarnessComposer extends DeesElement {
928
952
  }
929
953
  try {
930
954
  const dataBase64 = await harnessFileToBase64(file);
955
+ if (!allowed()) return;
931
956
  const kind = harnessAttachmentKindFor(mediaType);
932
957
  added.push({
933
958
  id: globalThis.crypto?.randomUUID?.() ?? `${Date.now()}-${Math.random().toString(16).slice(2)}`,
@@ -939,12 +964,13 @@ export class DeesHarnessComposer extends DeesElement {
939
964
  });
940
965
  totalBytes += file.size;
941
966
  } catch (error) {
967
+ if (!allowed()) return;
942
968
  this.emit('harness-attachment-error', {
943
969
  message: error instanceof Error ? error.message : String(error),
944
970
  });
945
971
  }
946
972
  }
947
- if (added.length) {
973
+ if (added.length && allowed()) {
948
974
  this.emitAttachments([...this.attachments, ...added]);
949
975
  }
950
976
  }
@@ -957,7 +983,7 @@ export class DeesHarnessComposer extends DeesElement {
957
983
  && (!this.busy || !this.inputSteeringEnabled || !this.value.trim())) return;
958
984
  const detail: IHarnessComposerSendDetail = {
959
985
  text: this.value,
960
- attachments: this.attachments,
986
+ attachments: this.attachmentsEnabled ? this.attachments : [],
961
987
  account: this.account || undefined,
962
988
  model: this.model || undefined,
963
989
  reasoningEffort: this.reasoningEffort || undefined,
@@ -9,6 +9,7 @@ import { demoFunc } from './demo.js';
9
9
  import { datepickerStyles } from './styles.js';
10
10
  import { renderDatepicker } from './template.js';
11
11
  import { DeesInputDatepickerPopup } from './datepicker-popup.js';
12
+ import { createCalendarDate, dateValidationMessage, parseDateFields, parseDateValue } from './date-validation.js';
12
13
  import '../../00group-utility/dees-icon/dees-icon.js';
13
14
  import '../../00group-layout/dees-label/dees-label.js';
14
15
 
@@ -78,6 +79,17 @@ export class DeesInputDatepicker extends DeesInputBase<DeesInputDatepicker> {
78
79
  @state()
79
80
  accessor selectedMinute: number = 0;
80
81
 
82
+ /** Uncommitted text stays visible even when it cannot be represented by value. */
83
+ @state()
84
+ accessor draftText: string | null = null;
85
+
86
+ @state()
87
+ accessor validationText = '';
88
+
89
+ public get validationState(): 'invalid' | 'valid' { return this.validationText ? 'invalid' : 'valid'; }
90
+
91
+ private lastCommittedValue: string | undefined;
92
+
81
93
  private popupInstance: DeesInputDatepickerPopup | null = null;
82
94
 
83
95
  public static styles = datepickerStyles;
@@ -86,42 +98,59 @@ export class DeesInputDatepicker extends DeesInputBase<DeesInputDatepicker> {
86
98
  return renderDatepicker(this);
87
99
  }
88
100
 
89
- async firstUpdated() {
90
- if (!this.value) {
91
- this.value = '';
101
+ public willUpdate(changed: Map<PropertyKey, unknown>): void {
102
+ super.willUpdate(changed);
103
+ if (changed.has('value') && this.value !== this.lastCommittedValue) {
104
+ this.syncValue(this.value);
92
105
  }
106
+ }
93
107
 
94
- if (this.value) {
95
- try {
96
- const date = new Date(this.value);
97
- if (!isNaN(date.getTime())) {
98
- this.selectedDate = date;
99
- this.viewDate = new Date(date);
100
- this.selectedHour = date.getHours();
101
- this.selectedMinute = date.getMinutes();
102
- }
103
- } catch {
104
- // Invalid date
108
+ public updated(changed: Map<string, unknown>): void {
109
+ super.updated(changed);
110
+ if (changed.has('disabled') && this.disabled) this.closePopup();
111
+ if (['value', 'minDate', 'maxDate', 'disabledDates', 'required', 'disabled'].some(key => changed.has(key))) {
112
+ this.updateValidation(this.currentValidationMessage());
113
+ }
114
+ if (this.popupInstance) {
115
+ this.popupInstance.minDate = this.minDate;
116
+ this.popupInstance.maxDate = this.maxDate;
117
+ this.popupInstance.disabledDates = this.disabledDates;
118
+ if (changed.has('selectedDate')) {
119
+ this.popupInstance.selectedDate = this.selectedDate;
120
+ this.popupInstance.viewDate = new Date(this.viewDate);
121
+ this.popupInstance.selectedHour = this.selectedHour;
122
+ this.popupInstance.selectedMinute = this.selectedMinute;
105
123
  }
106
- } else {
107
- const now = new Date();
108
- this.viewDate = new Date(now);
109
- this.selectedHour = now.getHours();
110
- this.selectedMinute = 0;
124
+ }
125
+ const input = this.shadowRoot?.querySelector('input');
126
+ input?.setCustomValidity(this.disabled ? '' : this.validationText);
127
+ if (input && (changed.has('draftText') || changed.has('value'))) {
128
+ const display = this.draftText ?? this.formatDate(this.value);
129
+ if (input.value !== display) input.value = display;
111
130
  }
112
131
  }
113
132
 
133
+ private syncValue(value: string | null): void {
134
+ this.value = value ?? '';
135
+ this.lastCommittedValue = this.value;
136
+ this.draftText = null;
137
+ this.selectedDate = parseDateValue(this.value);
138
+ this.viewDate = new Date(this.selectedDate ?? new Date());
139
+ this.selectedHour = this.selectedDate?.getHours() ?? 0;
140
+ this.selectedMinute = this.selectedDate?.getMinutes() ?? 0;
141
+ }
142
+
114
143
  public formatDate(isoString: string): string {
115
144
  if (!isoString) return '';
116
145
 
117
146
  try {
118
- const date = new Date(isoString);
119
- if (isNaN(date.getTime())) return '';
147
+ const date = parseDateValue(isoString);
148
+ if (!date) return isoString;
120
149
 
121
150
  let formatted = this.dateFormat;
122
151
  const day = date.getDate().toString().padStart(2, '0');
123
152
  const month = (date.getMonth() + 1).toString().padStart(2, '0');
124
- const year = date.getFullYear().toString();
153
+ const year = date.getFullYear().toString().padStart(4, '0');
125
154
 
126
155
  formatted = formatted.replace('YYYY', year);
127
156
  formatted = formatted.replace('YY', year.slice(-2));
@@ -171,7 +200,7 @@ export class DeesInputDatepicker extends DeesInputBase<DeesInputDatepicker> {
171
200
  const opensToTop = spaceBelow < 400 && spaceAbove > spaceBelow;
172
201
 
173
202
  if (!this.popupInstance) {
174
- this.popupInstance = new DeesInputDatepickerPopup();
203
+ this.popupInstance = document.createElement('dees-input-datepicker-popup');
175
204
  }
176
205
 
177
206
  // Configure popup
@@ -204,31 +233,50 @@ export class DeesInputDatepicker extends DeesInputBase<DeesInputDatepicker> {
204
233
 
205
234
  private closePopup(): void {
206
235
  this.isOpened = false;
207
- if (this.popupInstance) {
208
- this.popupInstance.removeEventListener('date-selected', this.handleDateSelected);
209
- this.popupInstance.removeEventListener('date-cleared', this.handleDateCleared);
210
- this.popupInstance.removeEventListener('close-request', this.handleCloseRequest);
211
- this.popupInstance.removeEventListener('reposition-request', this.handleRepositionRequest);
212
- this.popupInstance.hide();
236
+ const popup = this.popupInstance;
237
+ this.popupInstance = null;
238
+ if (popup) {
239
+ popup.removeEventListener('date-selected', this.handleDateSelected);
240
+ popup.removeEventListener('date-cleared', this.handleDateCleared);
241
+ popup.removeEventListener('close-request', this.handleCloseRequest);
242
+ popup.removeEventListener('reposition-request', this.handleRepositionRequest);
243
+ void popup.hide();
213
244
  }
214
245
  }
215
246
 
216
247
  private handleDateSelected = (event: Event): void => {
217
- const date = (event as CustomEvent).detail as Date;
218
- this.selectedDate = date;
219
- this.selectedHour = date.getHours();
220
- this.selectedMinute = date.getMinutes();
221
- this.viewDate = new Date(date);
222
- this.value = this.formatValueWithTimezone(date);
223
- this.changeSubject.next(this);
248
+ if (this.disabled) return;
249
+ this.commitDate((event as CustomEvent<Date>).detail, true);
224
250
  };
225
251
 
226
252
  private handleDateCleared = (): void => {
227
- this.value = '';
228
- this.selectedDate = null;
229
- this.changeSubject.next(this);
253
+ if (!this.disabled) this.commitClear();
230
254
  };
231
255
 
256
+ private commitDate(date: Date, normalizeDraft: boolean): boolean {
257
+ const message = dateValidationMessage(date, this);
258
+ if (message) { this.updateValidation(message); return false; }
259
+ const value = this.formatValueWithTimezone(date);
260
+ const changed = value !== this.value;
261
+ this.lastCommittedValue = value;
262
+ this.value = value;
263
+ this.selectedDate = new Date(date);
264
+ this.selectedHour = date.getHours();
265
+ this.selectedMinute = date.getMinutes();
266
+ this.viewDate = new Date(date);
267
+ if (normalizeDraft) this.draftText = null;
268
+ this.updateValidation('');
269
+ if (changed) this.changeSubject.next(this);
270
+ return true;
271
+ }
272
+
273
+ private commitClear(): void {
274
+ const changed = Boolean(this.value);
275
+ this.syncValue('');
276
+ this.updateValidation(this.currentValidationMessage());
277
+ if (changed) this.changeSubject.next(this);
278
+ }
279
+
232
280
  private handleCloseRequest = (): void => {
233
281
  this.closePopup();
234
282
  };
@@ -251,11 +299,17 @@ export class DeesInputDatepicker extends DeesInputBase<DeesInputDatepicker> {
251
299
  };
252
300
 
253
301
  public handleKeydown(e: KeyboardEvent): void {
254
- if (e.key === 'Enter' || e.key === ' ') {
302
+ if (this.disabled || e.isComposing) return;
303
+ if (e.key === 'Enter') {
255
304
  e.preventDefault();
256
- this.toggleCalendar();
305
+ if (this.draftText !== null) this.commitManualInput(true);
306
+ else void this.toggleCalendar();
307
+ } else if (e.key === 'ArrowDown' && e.altKey) {
308
+ e.preventDefault();
309
+ if (!this.isOpened) void this.toggleCalendar();
257
310
  } else if (e.key === 'Escape' && this.isOpened) {
258
311
  e.preventDefault();
312
+ e.stopPropagation();
259
313
  this.closePopup();
260
314
  }
261
315
  }
@@ -270,107 +324,73 @@ export class DeesInputDatepicker extends DeesInputBase<DeesInputDatepicker> {
270
324
 
271
325
  public clearValue(e: Event): void {
272
326
  e.stopPropagation();
273
- this.value = '';
274
- this.selectedDate = null;
275
- this.changeSubject.next(this);
327
+ if (!this.disabled) this.commitClear();
276
328
  }
277
329
 
278
330
  public handleManualInput(e: InputEvent): void {
279
- const input = e.target as HTMLInputElement;
280
- const inputValue = input.value.trim();
281
-
282
- if (!inputValue) {
283
- this.value = '';
284
- this.selectedDate = null;
285
- return;
286
- }
287
-
288
- const parsedDate = this.parseManualDate(inputValue);
289
- if (parsedDate && !isNaN(parsedDate.getTime())) {
290
- this.value = parsedDate.toISOString();
291
- this.selectedDate = parsedDate;
292
- this.viewDate = new Date(parsedDate);
293
- this.selectedHour = parsedDate.getHours();
294
- this.selectedMinute = parsedDate.getMinutes();
295
- this.changeSubject.next(this);
296
- }
331
+ if (this.disabled) return;
332
+ this.draftText = (e.target as HTMLInputElement).value;
333
+ if (!e.isComposing) this.updateValidation(this.currentValidationMessage());
297
334
  }
298
335
 
299
- public handleInputBlur(e: FocusEvent): void {
300
- const input = e.target as HTMLInputElement;
301
- const inputValue = input.value.trim();
302
-
303
- if (!inputValue) {
304
- this.value = '';
305
- this.selectedDate = null;
306
- this.changeSubject.next(this);
307
- return;
308
- }
336
+ public handleInputBlur(_event: FocusEvent): void {
337
+ if (!this.disabled && this.draftText !== null) this.commitManualInput(true);
338
+ }
309
339
 
310
- const parsedDate = this.parseManualDate(inputValue);
311
- if (parsedDate && !isNaN(parsedDate.getTime())) {
312
- this.value = parsedDate.toISOString();
313
- this.selectedDate = parsedDate;
314
- this.viewDate = new Date(parsedDate);
315
- this.selectedHour = parsedDate.getHours();
316
- this.selectedMinute = parsedDate.getMinutes();
317
- this.changeSubject.next(this);
318
- input.value = this.formatDate(this.value);
319
- } else {
320
- input.value = this.formatDate(this.value);
321
- }
340
+ private commitManualInput(normalizeDraft: boolean): void {
341
+ const text = this.draftText?.trim() ?? '';
342
+ if (!text) { this.commitClear(); return; }
343
+ const date = this.parseManualDate(text);
344
+ const message = dateValidationMessage(date, this);
345
+ if (message) { this.updateValidation(message); return; }
346
+ this.commitDate(date!, normalizeDraft);
322
347
  }
323
348
 
324
349
  private parseManualDate(input: string): Date | null {
325
- if (!input) return null;
326
-
327
- const parts = input.split(' ');
328
- let datePart = parts[0];
329
- let timePart = parts[1] || '';
330
- let parsedDate: Date | null = null;
331
-
332
- const isoMatch = datePart.match(/^(\d{4})-(\d{1,2})-(\d{1,2})$/);
333
- if (isoMatch) {
334
- const [_, year, month, day] = isoMatch;
335
- parsedDate = new Date(parseInt(year), parseInt(month) - 1, parseInt(day));
336
- }
337
-
338
- if (!parsedDate) {
339
- const euMatch = datePart.match(/^(\d{1,2})\.(\d{1,2})\.(\d{4})$/);
340
- if (euMatch) {
341
- const [_, day, month, year] = euMatch;
342
- parsedDate = new Date(parseInt(year), parseInt(month) - 1, parseInt(day));
343
- }
350
+ const match = /^(\S+)(?:\s+(\d{1,2}):(\d{2})(?:\s+(AM|PM))?)?$/i.exec(input.trim());
351
+ if (!match) return null;
352
+ const fields = parseDateFields(match[1]);
353
+ if (!fields) return null;
354
+ let hour = Number(match[2] ?? 0);
355
+ const minute = Number(match[3] ?? 0);
356
+ if (match[4]) {
357
+ if (hour < 1 || hour > 12) return null;
358
+ hour = hour % 12 + (match[4].toUpperCase() === 'PM' ? 12 : 0);
344
359
  }
360
+ return createCalendarDate(...fields, match[2] === undefined ? undefined : hour, minute);
361
+ }
345
362
 
346
- if (!parsedDate) {
347
- const usMatch = datePart.match(/^(\d{1,2})\/(\d{1,2})\/(\d{4})$/);
348
- if (usMatch) {
349
- const [_, month, day, year] = usMatch;
350
- parsedDate = new Date(parseInt(year), parseInt(month) - 1, parseInt(day));
351
- }
363
+ private currentValidationMessage(): string {
364
+ if (this.disabled) return '';
365
+ const text = this.draftText?.trim();
366
+ if (this.draftText !== null) {
367
+ if (!text) return this.required ? 'Choose a date.' : '';
368
+ return dateValidationMessage(this.parseManualDate(text), this);
352
369
  }
370
+ if (!this.value) return this.required ? 'Choose a date.' : '';
371
+ return dateValidationMessage(parseDateValue(this.value), this);
372
+ }
353
373
 
354
- if (!parsedDate || isNaN(parsedDate.getTime())) return null;
374
+ private updateValidation(message: string): void {
375
+ const changed = message !== this.validationText;
376
+ this.validationText = message;
377
+ this.shadowRoot?.querySelector('input')?.setCustomValidity(message);
378
+ if (changed) this.dispatchEvent(new CustomEvent('validation-change', {
379
+ detail: { valid: !message, message }, bubbles: true, composed: true,
380
+ }));
381
+ }
355
382
 
356
- if (timePart) {
357
- const timeMatch = timePart.match(/^(\d{1,2}):(\d{2})$/);
358
- if (timeMatch) {
359
- const [_, hours, minutes] = timeMatch;
360
- parsedDate.setHours(parseInt(hours));
361
- parsedDate.setMinutes(parseInt(minutes));
362
- }
363
- } else if (!this.enableTime) {
364
- const now = new Date();
365
- parsedDate.setHours(now.getHours());
366
- parsedDate.setMinutes(now.getMinutes());
367
- parsedDate.setSeconds(0);
368
- parsedDate.setMilliseconds(0);
369
- }
383
+ public checkValidity(): boolean { return !this.currentValidationMessage(); }
370
384
 
371
- return parsedDate;
385
+ public reportValidity(): boolean {
386
+ const message = this.currentValidationMessage();
387
+ this.updateValidation(message);
388
+ if (message) this.focus();
389
+ return !message;
372
390
  }
373
391
 
392
+ public reset(): void { this.setValue(''); }
393
+
374
394
  private formatValueWithTimezone(date: Date): string {
375
395
  if (!this.enableTimezone) return date.toISOString();
376
396
 
@@ -403,32 +423,13 @@ export class DeesInputDatepicker extends DeesInputBase<DeesInputDatepicker> {
403
423
  return this.value;
404
424
  }
405
425
 
406
- public setValue(value: string): void {
407
- this.value = value;
408
- if (value) {
409
- try {
410
- const date = new Date(value);
411
- if (!isNaN(date.getTime())) {
412
- this.selectedDate = date;
413
- this.viewDate = new Date(date);
414
- this.selectedHour = date.getHours();
415
- this.selectedMinute = date.getMinutes();
416
- }
417
- } catch {
418
- // Invalid date
419
- }
420
- }
426
+ public setValue(value: string | null): void {
427
+ this.syncValue(value);
428
+ this.updateValidation(this.currentValidationMessage());
421
429
  }
422
430
 
423
431
  async disconnectedCallback() {
432
+ this.closePopup();
424
433
  await super.disconnectedCallback();
425
- if (this.popupInstance) {
426
- this.popupInstance.removeEventListener('date-selected', this.handleDateSelected);
427
- this.popupInstance.removeEventListener('date-cleared', this.handleDateCleared);
428
- this.popupInstance.removeEventListener('close-request', this.handleCloseRequest);
429
- this.popupInstance.removeEventListener('reposition-request', this.handleRepositionRequest);
430
- this.popupInstance.hide();
431
- this.popupInstance = null;
432
- }
433
434
  }
434
435
  }
@@ -0,0 +1,64 @@
1
+ export interface IDateConstraints {
2
+ minDate: string;
3
+ maxDate: string;
4
+ disabledDates: string[];
5
+ }
6
+
7
+ /** Construct a local calendar date without accepting Date's overflow normalization. */
8
+ export const createCalendarDate = (year: number, month: number, day: number, hour?: number, minute = 0): Date | null => {
9
+ const requestedHour = hour ?? 0;
10
+ if (![year, month, day, requestedHour, minute].every(Number.isInteger)
11
+ || year < 1 || year > 9999 || month < 1 || month > 12 || day < 1 || day > 31
12
+ || requestedHour < 0 || requestedHour > 23 || minute < 0 || minute > 59) return null;
13
+ const date = new Date(0);
14
+ // setFullYear avoids the constructor's special mapping of years 0–99 to 1900–1999.
15
+ date.setFullYear(year, month - 1, day);
16
+ date.setHours(requestedHour, minute, 0, 0);
17
+ // Some zones enter daylight saving at midnight. A date-only entry still names
18
+ // that valid calendar day; an explicitly typed nonexistent time is rejected.
19
+ return date.getFullYear() === year && date.getMonth() === month - 1 && date.getDate() === day
20
+ && (hour === undefined || date.getHours() === hour && date.getMinutes() === minute) ? date : null;
21
+ };
22
+
23
+ export const parseDateFields = (text: string): [number, number, number] | null => {
24
+ let match = /^(\d{4})-(\d{1,2})-(\d{1,2})$/.exec(text);
25
+ if (match) return [Number(match[1]), Number(match[2]), Number(match[3])];
26
+ match = /^(\d{1,2})\.(\d{1,2})\.(\d{4})$/.exec(text);
27
+ if (match) return [Number(match[3]), Number(match[2]), Number(match[1])];
28
+ match = /^(\d{1,2})\/(\d{1,2})\/(\d{4})$/.exec(text);
29
+ return match ? [Number(match[3]), Number(match[1]), Number(match[2])] : null;
30
+ };
31
+
32
+ /** Date-only constraints are local calendar days, never UTC-midnight instants. */
33
+ export const parseDateValue = (value: string): Date | null => {
34
+ if (!value) return null;
35
+ const fields = parseDateFields(value);
36
+ if (fields) return createCalendarDate(...fields);
37
+ const iso = /^(\d{4})-(\d{2})-(\d{2})T/.exec(value);
38
+ if (!iso || !createCalendarDate(Number(iso[1]), Number(iso[2]), Number(iso[3]))) return null;
39
+ const date = new Date(value);
40
+ return Number.isNaN(date.getTime()) ? null : date;
41
+ };
42
+
43
+ export const calendarDayKey = (date: Date): number =>
44
+ date.getFullYear() * 10000 + (date.getMonth() + 1) * 100 + date.getDate();
45
+
46
+ /** Shared by manual entry, calendar buttons and the final commit boundary. */
47
+ export const dateValidationMessage = (date: Date | null, constraints: IDateConstraints): string => {
48
+ if (!date || Number.isNaN(date.getTime()) || date.getFullYear() < 1 || date.getFullYear() > 9999) return 'Enter a valid date (YYYY-MM-DD, DD.MM.YYYY or MM/DD/YYYY).';
49
+ const key = calendarDayKey(date);
50
+ for (const bound of ['minDate', 'maxDate'] as const) {
51
+ if (!constraints[bound]) continue;
52
+ const limit = parseDateValue(constraints[bound]);
53
+ if (!limit) return `${bound === 'minDate' ? 'Minimum' : 'Maximum'} date is invalid.`;
54
+ if (bound === 'minDate' ? key < calendarDayKey(limit) : key > calendarDayKey(limit)) {
55
+ return `Choose a date on or ${bound === 'minDate' ? 'after' : 'before'} ${constraints[bound]}.`;
56
+ }
57
+ }
58
+ for (const value of constraints.disabledDates ?? []) {
59
+ const disabled = parseDateValue(value);
60
+ if (!disabled) return 'A disabled date is invalid.';
61
+ if (key === calendarDayKey(disabled)) return 'This date is unavailable.';
62
+ }
63
+ return '';
64
+ };