@design.estate/dees-catalog 9.6.2 → 9.8.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 (69) hide show
  1. package/dist_bundle/bundle.js +3138 -2725
  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-appui/dees-appui/dees-appui.js +6 -1
  7. package/dist_ts_web/elements/00group-form/dees-form/dees-form.d.ts +5 -1
  8. package/dist_ts_web/elements/00group-form/dees-form/dees-form.js +27 -3
  9. package/dist_ts_web/elements/00group-harness/dees-harness-chat/dees-harness-chat.d.ts +1 -0
  10. package/dist_ts_web/elements/00group-harness/dees-harness-chat/dees-harness-chat.js +11 -2
  11. package/dist_ts_web/elements/00group-harness/dees-harness-composer/dees-harness-composer.d.ts +3 -0
  12. package/dist_ts_web/elements/00group-harness/dees-harness-composer/dees-harness-composer.demo.js +4 -1
  13. package/dist_ts_web/elements/00group-harness/dees-harness-composer/dees-harness-composer.js +64 -16
  14. package/dist_ts_web/elements/00group-harness/dees-harness-message-list/dees-harness-message-list.d.ts +4 -0
  15. package/dist_ts_web/elements/00group-harness/dees-harness-message-list/dees-harness-message-list.js +52 -1
  16. package/dist_ts_web/elements/00group-harness/dees-harness-tool-card/dees-harness-tool-card.d.ts +4 -1
  17. package/dist_ts_web/elements/00group-harness/dees-harness-tool-card/dees-harness-tool-card.js +93 -20
  18. package/dist_ts_web/elements/00group-harness/dees-harness-tool-card/renderers.js +7 -21
  19. package/dist_ts_web/elements/00group-harness/harness.helpers.d.ts +20 -0
  20. package/dist_ts_web/elements/00group-harness/harness.helpers.js +58 -1
  21. package/dist_ts_web/elements/00group-harness/interfaces.d.ts +6 -0
  22. package/dist_ts_web/elements/00group-harness/toolregistry.d.ts +8 -0
  23. package/dist_ts_web/elements/00group-harness/toolregistry.js +10 -2
  24. package/dist_ts_web/elements/00group-input/dees-input-datepicker/component.d.ts +18 -3
  25. package/dist_ts_web/elements/00group-input/dees-input-datepicker/component.js +181 -144
  26. package/dist_ts_web/elements/00group-input/dees-input-datepicker/date-validation.d.ts +13 -0
  27. package/dist_ts_web/elements/00group-input/dees-input-datepicker/date-validation.js +65 -0
  28. package/dist_ts_web/elements/00group-input/dees-input-datepicker/datepicker-popup.js +44 -56
  29. package/dist_ts_web/elements/00group-input/dees-input-datepicker/styles.js +6 -1
  30. package/dist_ts_web/elements/00group-input/dees-input-datepicker/template.js +11 -5
  31. package/dist_ts_web/elements/00group-input/dees-input-text/dees-input-text.d.ts +8 -8
  32. package/dist_ts_web/elements/00group-input/dees-input-text/dees-input-text.js +6 -53
  33. package/dist_ts_web/elements/00group-input/dees-input-textarea/dees-input-textarea.d.ts +62 -0
  34. package/dist_ts_web/elements/00group-input/dees-input-textarea/dees-input-textarea.demo.d.ts +1 -0
  35. package/dist_ts_web/elements/00group-input/dees-input-textarea/dees-input-textarea.demo.js +3 -0
  36. package/dist_ts_web/elements/00group-input/dees-input-textarea/dees-input-textarea.js +224 -0
  37. package/dist_ts_web/elements/00group-input/dees-input-textarea/index.d.ts +1 -0
  38. package/dist_ts_web/elements/00group-input/dees-input-textarea/index.js +2 -0
  39. package/dist_ts_web/elements/00group-input/index.d.ts +1 -0
  40. package/dist_ts_web/elements/00group-input/index.js +2 -1
  41. package/dist_ts_web/elements/00group-input/text-input-contextmenu.d.ts +29 -0
  42. package/dist_ts_web/elements/00group-input/text-input-contextmenu.js +29 -0
  43. package/package.json +2 -2
  44. package/readme.md +61 -6
  45. package/scripts/check-packed-consumer.mjs +8 -5
  46. package/ts_web/00_commitinfo_data.ts +1 -1
  47. package/ts_web/demos/inputs.ts +5 -4
  48. package/ts_web/elements/00group-appui/dees-appui/dees-appui.ts +4 -0
  49. package/ts_web/elements/00group-form/dees-form/dees-form.ts +25 -2
  50. package/ts_web/elements/00group-harness/dees-harness-chat/dees-harness-chat.ts +4 -0
  51. package/ts_web/elements/00group-harness/dees-harness-composer/dees-harness-composer.demo.ts +3 -0
  52. package/ts_web/elements/00group-harness/dees-harness-composer/dees-harness-composer.ts +49 -14
  53. package/ts_web/elements/00group-harness/dees-harness-message-list/dees-harness-message-list.ts +50 -0
  54. package/ts_web/elements/00group-harness/dees-harness-tool-card/dees-harness-tool-card.ts +95 -23
  55. package/ts_web/elements/00group-harness/dees-harness-tool-card/renderers.ts +7 -20
  56. package/ts_web/elements/00group-harness/harness.helpers.ts +63 -0
  57. package/ts_web/elements/00group-harness/interfaces.ts +6 -0
  58. package/ts_web/elements/00group-harness/toolregistry.ts +22 -1
  59. package/ts_web/elements/00group-input/dees-input-datepicker/component.ts +149 -148
  60. package/ts_web/elements/00group-input/dees-input-datepicker/date-validation.ts +64 -0
  61. package/ts_web/elements/00group-input/dees-input-datepicker/datepicker-popup.ts +40 -40
  62. package/ts_web/elements/00group-input/dees-input-datepicker/styles.ts +5 -0
  63. package/ts_web/elements/00group-input/dees-input-datepicker/template.ts +10 -4
  64. package/ts_web/elements/00group-input/dees-input-text/dees-input-text.ts +5 -52
  65. package/ts_web/elements/00group-input/dees-input-textarea/dees-input-textarea.demo.ts +2 -0
  66. package/ts_web/elements/00group-input/dees-input-textarea/dees-input-textarea.ts +128 -0
  67. package/ts_web/elements/00group-input/dees-input-textarea/index.ts +1 -0
  68. package/ts_web/elements/00group-input/index.ts +1 -0
  69. package/ts_web/elements/00group-input/text-input-contextmenu.ts +31 -0
@@ -313,3 +313,66 @@ export const harnessSummarizeValue = (value: unknown): string => {
313
313
  export const harnessSummaryLine = (value: unknown): string => {
314
314
  return harnessSummarizeValue(value).split('\n')[0] || '';
315
315
  };
316
+
317
+ // eslint-disable-next-line no-control-regex
318
+ const ansiEscapePattern = /\x1b\[[0-9;?]*[A-Za-z]|\x1b\][^\x07]*(?:\x07|\x1b\\)|\x1b[=>]/g;
319
+
320
+ /** Terminal output carries raw escape sequences; they must never reach a header or a summary. */
321
+ export const harnessStripAnsi = (text: string): string => text.replace(ansiEscapePattern, '');
322
+
323
+ export interface IHarnessTerminalOutcome {
324
+ stdout: string;
325
+ stderr: string;
326
+ /** Top-level exitCode wins over a nested one, matching how producers report it. */
327
+ exitCode?: number;
328
+ }
329
+
330
+ /**
331
+ * Normalizes the three shapes producers use for shell output: a `{stdout, stderr, exitCode}`
332
+ * record, a combined string with a separate top-level `exitCode`, or an arbitrary value.
333
+ */
334
+ export const harnessTerminalOutcome = (callArg: IHarnessToolCall): IHarnessTerminalOutcome => {
335
+ const output = callArg.output;
336
+ let stdout = '';
337
+ let stderr = '';
338
+ let nestedExitCode: number | undefined;
339
+ if (harnessIsRecord(output)) {
340
+ stdout = typeof output.stdout === 'string' ? output.stdout : harnessStringifyValue(output.stdout ?? '');
341
+ stderr = typeof output.stderr === 'string' ? output.stderr : '';
342
+ nestedExitCode = typeof output.exitCode === 'number' ? output.exitCode : undefined;
343
+ } else {
344
+ stdout = harnessStringifyValue(output);
345
+ }
346
+ const exitCode = typeof callArg.exitCode === 'number' ? callArg.exitCode : nestedExitCode;
347
+ return { stdout, stderr, ...(exitCode === undefined ? {} : { exitCode }) };
348
+ };
349
+
350
+ const harnessLastMeaningfulLine = (text: string): string => {
351
+ const lines = harnessStripAnsi(text).split('\n');
352
+ for (let index = lines.length - 1; index >= 0; index -= 1) {
353
+ const line = lines[index].trim();
354
+ if (line) return line;
355
+ }
356
+ return '';
357
+ };
358
+
359
+ /**
360
+ * One-line outcome for a finished shell call, for the collapsed card header. The command itself
361
+ * is already the card subtitle, so this describes the RESULT: exit code, how much came back, and
362
+ * the last meaningful line. Harness support for a richer summary is uneven — only OpenCode
363
+ * supplies a model-authored description, so this is derived from output that every harness has.
364
+ */
365
+ export const harnessTerminalResultSummary = (callArg: IHarnessToolCall): string => {
366
+ const { stdout, stderr, exitCode } = harnessTerminalOutcome(callArg);
367
+ const parts: string[] = [];
368
+ if (exitCode !== undefined) parts.push(exitCode === 0 ? 'exit 0' : `exit ${exitCode}`);
369
+ const combined = `${stdout}${stdout && stderr ? '\n' : ''}${stderr}`;
370
+ const lineCount = combined ? harnessStripAnsi(combined).split('\n').filter((line) => line.trim()).length : 0;
371
+ if (lineCount > 0) parts.push(`${lineCount} ${lineCount === 1 ? 'line' : 'lines'}`);
372
+ else if (exitCode !== undefined) parts.push('no output');
373
+ const lastLine = harnessLastMeaningfulLine(stderr) || harnessLastMeaningfulLine(stdout);
374
+ const prefix = parts.join(' · ');
375
+ if (!lastLine) return prefix;
376
+ const trimmed = lastLine.length > 160 ? `${lastLine.slice(0, 160)}…` : lastLine;
377
+ return prefix ? `${prefix} · ${trimmed}` : trimmed;
378
+ };
@@ -230,6 +230,12 @@ export interface IHarnessToolCall {
230
230
  subtask?: IHarnessSubtaskStream;
231
231
  /** model the spawned session runs on, e.g. "anthropic/claude-opus-5" */
232
232
  model?: string;
233
+ /**
234
+ * Producer-supplied one-line description of the call, e.g. OpenCode's tool title or a parsed
235
+ * shell action. Single line, already bounded by the producer. Preferred over any line the card
236
+ * derives for itself; absent whenever the producer offers nothing.
237
+ */
238
+ title?: string;
233
239
  /** epoch ms */
234
240
  startedAt?: number;
235
241
  finishedAt?: number;
@@ -1,7 +1,12 @@
1
1
  import type { TemplateResult } from '@design.estate/dees-element';
2
2
  import type { TDiffViewRequest } from '../00group-dataview/dees-dataview-codebox/dees-dataview-codebox.js';
3
3
  import type { IHarnessToolCall, THarnessContentBlock } from './interfaces.js';
4
- import { harnessIsRecord, harnessSummaryLine } from './harness.helpers.js';
4
+ import {
5
+ harnessIsRecord,
6
+ harnessSummaryLine,
7
+ harnessTerminalOutcome,
8
+ harnessTerminalResultSummary,
9
+ } from './harness.helpers.js';
5
10
 
6
11
  /**
7
12
  * Render helpers handed to tool body renderers. Implemented by
@@ -49,6 +54,14 @@ export interface IHarnessToolDescriptor {
49
54
  */
50
55
  renderBody?: (call: IHarnessToolCall, helpers: IHarnessToolRenderHelpers) => TemplateResult;
51
56
  defaultExpanded?: boolean | ((call: IHarnessToolCall) => boolean);
57
+ /**
58
+ * Collapses a card that opened while it was live once its work is done, so a finished run
59
+ * stops occupying the transcript. Evaluated only for a settled call, and always subordinate to
60
+ * an explicit user toggle.
61
+ */
62
+ collapseWhenFinished?: boolean | ((call: IHarnessToolCall) => boolean);
63
+ /** One-line outcome shown in the header while the card is collapsed. */
64
+ summary?: (call: IHarnessToolCall) => string;
52
65
  /** Transcript grid packing; omitted descriptors use the compact layout. */
53
66
  layout?: THarnessToolLayout;
54
67
  /** styles the card with a destructive accent regardless of status */
@@ -101,6 +114,14 @@ const defaultDescriptors: IHarnessToolDescriptor[] = [
101
114
  },
102
115
  defaultExpanded: true,
103
116
  layout: 'full-row',
117
+ /*
118
+ * Only a command that can be PROVEN to have succeeded collapses. An absent exit code is not
119
+ * evidence of success: a producer that reports a failed command as completed and encodes the
120
+ * failure in its output would otherwise have that failure hidden behind a one-line summary.
121
+ * Such a card stays open, and the producer reporting an exit code is the real fix.
122
+ */
123
+ collapseWhenFinished: (call) => harnessTerminalOutcome(call).exitCode === 0,
124
+ summary: harnessTerminalResultSummary,
104
125
  },
105
126
  {
106
127
  kind: 'file-read',
@@ -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
+ };