@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
@@ -14,6 +14,7 @@ import { themeDefaultStyles } from '../../00theme.js';
14
14
  import { DeesWindowLayer } from '../../00group-overlay/dees-windowlayer/dees-windowlayer.js';
15
15
  import '../../00group-utility/dees-icon/dees-icon.js';
16
16
  import type { IDateEvent } from './types.js';
17
+ import { createCalendarDate, dateValidationMessage } from './date-validation.js';
17
18
 
18
19
  declare global {
19
20
  interface HTMLElementTagNameMap {
@@ -534,29 +535,32 @@ export class DeesInputDatepickerPopup extends DeesElement {
534
535
  const totalEventCount = dayEvents.reduce((sum, event) => sum + (event.count || 1), 0);
535
536
 
536
537
  return html`
537
- <div
538
+ <button type="button"
538
539
  class="day ${isOtherMonth ? 'other-month' : ''} ${isToday ? 'today' : ''} ${isSelected ? 'selected' : ''} ${isDayDisabled ? 'disabled' : ''} ${hasEvents ? 'has-event' : ''}"
540
+ ?disabled=${isDayDisabled}
541
+ aria-label=${`${String(day.getFullYear()).padStart(4, '0')}-${String(day.getMonth() + 1).padStart(2, '0')}-${String(day.getDate()).padStart(2, '0')}`}
542
+ aria-pressed=${isSelected ? 'true' : 'false'}
539
543
  @click=${() => !isDayDisabled && this.handleSelectDate(day)}
540
544
  >
541
545
  ${day.getDate()}
542
546
  ${hasEvents ? html`
543
547
  ${totalEventCount > 3 ? html`
544
- <div class="event-count">${totalEventCount}</div>
548
+ <span class="event-count">${totalEventCount}</span>
545
549
  ` : html`
546
- <div class="event-indicator">
550
+ <span class="event-indicator">
547
551
  ${dayEvents.slice(0, 3).map(event => html`
548
- <div class="event-dot ${event.type || 'info'}"></div>
552
+ <span class="event-dot ${event.type || 'info'}"></span>
549
553
  `)}
550
- </div>
554
+ </span>
551
555
  `}
552
556
  ${dayEvents[0].title ? html`
553
- <div class="event-tooltip">
557
+ <span class="event-tooltip">
554
558
  ${dayEvents[0].title}
555
559
  ${totalEventCount > 1 ? html` (+${totalEventCount - 1} more)` : ''}
556
- </div>
560
+ </span>
557
561
  ` : ''}
558
562
  ` : ''}
559
- </div>
563
+ </button>
560
564
  `;
561
565
  })}
562
566
  </div>
@@ -601,8 +605,8 @@ export class DeesInputDatepickerPopup extends DeesElement {
601
605
  ` : ''}
602
606
 
603
607
  <div class="calendar-actions">
604
- <button class="action-button today-button" @click=${this.handleSelectToday}>Today</button>
605
- <button class="action-button clear-action-button" @click=${this.handleClear}>Clear</button>
608
+ <button type="button" class="action-button today-button" ?disabled=${this.isDayDisabled(new Date())} @click=${this.handleSelectToday}>Today</button>
609
+ <button type="button" class="action-button clear-action-button" @click=${this.handleClear}>Clear</button>
606
610
  </div>
607
611
  </div>
608
612
  `;
@@ -624,22 +628,19 @@ export class DeesInputDatepickerPopup extends DeesElement {
624
628
 
625
629
  // Calendar logic
626
630
  private getDaysInMonth(): Date[] {
627
- const year = this.viewDate.getFullYear();
628
- const month = this.viewDate.getMonth();
629
- const firstDay = new Date(year, month, 1);
630
- const lastDay = new Date(year, month + 1, 0);
631
- const days: Date[] = [];
631
+ const firstDay = new Date(this.viewDate);
632
+ firstDay.setDate(1);
633
+ firstDay.setHours(12, 0, 0, 0);
632
634
 
633
635
  const startOffset = this.weekStartsOn === 1
634
636
  ? (firstDay.getDay() === 0 ? 6 : firstDay.getDay() - 1)
635
637
  : firstDay.getDay();
636
638
 
637
- for (let i = startOffset; i > 0; i--) days.push(new Date(year, month, 1 - i));
638
- for (let i = 1; i <= lastDay.getDate(); i++) days.push(new Date(year, month, i));
639
- const remaining = 42 - days.length;
640
- for (let i = 1; i <= remaining; i++) days.push(new Date(year, month + 1, i));
641
-
642
- return days;
639
+ return Array.from({ length: 42 }, (_, index) => {
640
+ const day = new Date(firstDay);
641
+ day.setDate(1 - startOffset + index);
642
+ return day;
643
+ });
643
644
  }
644
645
 
645
646
  private isToday(date: Date): boolean {
@@ -653,15 +654,7 @@ export class DeesInputDatepickerPopup extends DeesElement {
653
654
  }
654
655
 
655
656
  private isDayDisabled(date: Date): boolean {
656
- if (this.minDate) { const min = new Date(this.minDate); if (date < min) return true; }
657
- if (this.maxDate) { const max = new Date(this.maxDate); if (date > max) return true; }
658
- if (this.disabledDates?.length) {
659
- return this.disabledDates.some(ds => {
660
- try { const d = new Date(ds); return date.getDate() === d.getDate() && date.getMonth() === d.getMonth() && date.getFullYear() === d.getFullYear(); }
661
- catch { return false; }
662
- });
663
- }
664
- return false;
657
+ return Boolean(dateValidationMessage(date, this));
665
658
  }
666
659
 
667
660
  private getEventsForDate(date: Date): IDateEvent[] {
@@ -671,16 +664,24 @@ export class DeesInputDatepickerPopup extends DeesElement {
671
664
  }
672
665
 
673
666
  private previousMonth(): void {
674
- this.viewDate = new Date(this.viewDate.getFullYear(), this.viewDate.getMonth() - 1, 1);
667
+ const date = new Date(this.viewDate);
668
+ date.setMonth(date.getMonth() - 1, 1);
669
+ this.viewDate = date;
675
670
  }
676
671
 
677
672
  private nextMonth(): void {
678
- this.viewDate = new Date(this.viewDate.getFullYear(), this.viewDate.getMonth() + 1, 1);
673
+ const date = new Date(this.viewDate);
674
+ date.setMonth(date.getMonth() + 1, 1);
675
+ this.viewDate = date;
679
676
  }
680
677
 
681
678
  // Event dispatching
682
679
  private handleSelectDate(day: Date): void {
683
- this.selectedDate = new Date(day.getFullYear(), day.getMonth(), day.getDate(), this.selectedHour, this.selectedMinute);
680
+ if (this.isDayDisabled(day)) return;
681
+ const selected = createCalendarDate(day.getFullYear(), day.getMonth() + 1, day.getDate(),
682
+ this.enableTime ? this.selectedHour : undefined, this.enableTime ? this.selectedMinute : 0);
683
+ if (!selected || this.isDayDisabled(selected)) return;
684
+ this.selectedDate = selected;
684
685
  this.dispatchEvent(new CustomEvent('date-selected', { detail: this.selectedDate }));
685
686
  if (!this.enableTime) {
686
687
  this.dispatchEvent(new CustomEvent('close-request'));
@@ -689,14 +690,11 @@ export class DeesInputDatepickerPopup extends DeesElement {
689
690
 
690
691
  private handleSelectToday(): void {
691
692
  const today = new Date();
692
- this.selectedDate = today;
693
+ if (this.isDayDisabled(today)) return;
693
694
  this.viewDate = new Date(today);
694
695
  this.selectedHour = today.getHours();
695
696
  this.selectedMinute = today.getMinutes();
696
- this.dispatchEvent(new CustomEvent('date-selected', { detail: this.selectedDate }));
697
- if (!this.enableTime) {
698
- this.dispatchEvent(new CustomEvent('close-request'));
699
- }
697
+ this.handleSelectDate(today);
700
698
  }
701
699
 
702
700
  private handleClear(): void {
@@ -722,7 +720,7 @@ export class DeesInputDatepickerPopup extends DeesElement {
722
720
  const input = e.target as HTMLInputElement;
723
721
  let value = parseInt(input.value) || 0;
724
722
  value = Math.max(0, Math.min(59, value));
725
- if (this.minuteIncrement > 1) value = Math.round(value / this.minuteIncrement) * this.minuteIncrement;
723
+ if (this.minuteIncrement > 1) value = Math.min(59, Math.round(value / this.minuteIncrement) * this.minuteIncrement);
726
724
  this.selectedMinute = value;
727
725
  this.emitTimeUpdate();
728
726
  };
@@ -740,7 +738,9 @@ export class DeesInputDatepickerPopup extends DeesElement {
740
738
 
741
739
  private emitTimeUpdate(): void {
742
740
  if (this.selectedDate) {
743
- this.selectedDate = new Date(this.selectedDate.getFullYear(), this.selectedDate.getMonth(), this.selectedDate.getDate(), this.selectedHour, this.selectedMinute);
741
+ const selected = createCalendarDate(this.selectedDate.getFullYear(), this.selectedDate.getMonth() + 1, this.selectedDate.getDate(), this.selectedHour, this.selectedMinute);
742
+ if (!selected || this.isDayDisabled(selected)) return;
743
+ this.selectedDate = selected;
744
744
  this.dispatchEvent(new CustomEvent('date-selected', { detail: this.selectedDate }));
745
745
  }
746
746
  }
@@ -38,6 +38,11 @@ export const datepickerStyles = [
38
38
  color: var(--dees-color-text-muted);
39
39
  }
40
40
 
41
+ .date-input[aria-invalid="true"] { border-color: var(--dees-color-accent-error); }
42
+ .field-messages { grid-column: 1 / -1; }
43
+ .validationText { color: var(--dees-color-text-error); font-size: var(--dees-font-footnote-size); }
44
+ .validationText:not(:empty) { margin-top: var(--dees-spacing-xs); }
45
+
41
46
  .date-input:hover:not(:disabled) {
42
47
  border-color: var(--dees-color-border-strong);
43
48
  }
@@ -11,18 +11,21 @@ export const renderDatepicker = (component: DeesInputDatepicker): TemplateResult
11
11
  aria-label=${component.label}
12
12
  aria-haspopup="dialog"
13
13
  aria-expanded=${component.isOpened ? 'true' : 'false'}
14
+ aria-invalid=${component.validationText ? 'true' : 'false'}
15
+ aria-describedby="date-description date-validation"
14
16
  class="date-input field-control ${component.isOpened ? 'open' : ''}"
15
- .value=${component.formatDate(component.value)}
17
+ .value=${component.draftText ?? component.formatDate(component.value)}
16
18
  .placeholder=${component.placeholder}
17
19
  ?disabled=${component.disabled}
20
+ ?required=${component.required}
18
21
  @click=${component.toggleCalendar}
19
22
  @keydown=${component.handleKeydown}
20
23
  @input=${component.handleManualInput}
21
24
  @blur=${component.handleInputBlur}
22
- style="padding-right: ${component.value ? 'calc(var(--dees-input-action-size) + 40px)' : '40px'}"
25
+ style="padding-right: ${component.value || component.draftText ? 'calc(var(--dees-input-action-size) + 40px)' : '40px'}"
23
26
  />
24
27
  <div class="icon-container">
25
- ${component.value && !component.disabled ? html`
28
+ ${(component.value || component.draftText) && !component.disabled ? html`
26
29
  <button type="button" class="clear-button input-action" @click=${component.clearValue} aria-label="Clear date">
27
30
  <dees-icon icon="lucide:x" iconSize="14"></dees-icon>
28
31
  </button>
@@ -30,7 +33,10 @@ export const renderDatepicker = (component: DeesInputDatepicker): TemplateResult
30
33
  <dees-icon class="calendar-icon" icon="lucide:calendar" iconSize="16"></dees-icon>
31
34
  </div>
32
35
  </div>
33
- ${component.renderDescription()}
36
+ <div class="field-messages">
37
+ <div id="date-description">${component.renderDescription()}</div>
38
+ <div id="date-validation" class="validationText" role="status">${component.validationText}</div>
39
+ </div>
34
40
  </div>
35
41
  `;
36
42
 
@@ -1,5 +1,6 @@
1
1
  import * as colors from '../../00colors.js';
2
2
  import { DeesInputBase } from '../dees-input-base/dees-input-base.js';
3
+ import { textInputContextMenuItems } from '../text-input-contextmenu.js';
3
4
  import { demoFunc } from './dees-input-text.demo.js';
4
5
 
5
6
  import {
@@ -408,58 +409,10 @@ export class DeesInputText extends DeesInputBase {
408
409
  }
409
410
 
410
411
  public getContextMenuItems() {
411
- const input = this.shadowRoot!.querySelector('input')!;
412
- const hasSelection = input.selectionStart !== input.selectionEnd;
413
- return [
414
- {
415
- name: 'Cut',
416
- iconName: 'lucide:Scissors',
417
- shortcut: 'Cmd+X',
418
- disabled: !hasSelection,
419
- action: async () => {
420
- const selected = this.value.substring(input.selectionStart!, input.selectionEnd!);
421
- await navigator.clipboard.writeText(selected);
422
- const start = input.selectionStart!;
423
- this.value = this.value.substring(0, start) + this.value.substring(input.selectionEnd!);
424
- input.value = this.value;
425
- input.setSelectionRange(start, start);
426
- this.changeSubject.next(this);
427
- },
428
- },
429
- {
430
- name: 'Copy',
431
- iconName: 'lucide:Copy',
432
- shortcut: 'Cmd+C',
433
- disabled: !hasSelection,
434
- action: async () => {
435
- const selected = this.value.substring(input.selectionStart!, input.selectionEnd!);
436
- await navigator.clipboard.writeText(selected);
437
- },
438
- },
439
- {
440
- name: 'Paste',
441
- iconName: 'lucide:ClipboardPaste',
442
- shortcut: 'Cmd+V',
443
- action: async () => {
444
- const text = await navigator.clipboard.readText();
445
- const start = input.selectionStart!;
446
- const end = input.selectionEnd!;
447
- this.value = this.value.substring(0, start) + text + this.value.substring(end);
448
- input.value = this.value;
449
- input.setSelectionRange(start + text.length, start + text.length);
450
- this.changeSubject.next(this);
451
- },
452
- },
453
- { divider: true },
454
- {
455
- name: 'Select All',
456
- iconName: 'lucide:TextCursorInput',
457
- shortcut: 'Cmd+A',
458
- action: async () => {
459
- input.select();
460
- },
461
- },
462
- ];
412
+ return textInputContextMenuItems({
413
+ input: this.shadowRoot!.querySelector('input')!, disabled: this.disabled,
414
+ commit: value => { this.value = value; this.changeSubject.next(this); },
415
+ });
463
416
  }
464
417
 
465
418
  public getValue(): string {
@@ -0,0 +1,2 @@
1
+ import { focusedInputDemo } from '../../../demos/inputs.js';
2
+ export const demoFunc = () => focusedInputDemo('textarea');
@@ -0,0 +1,128 @@
1
+ import { customElement, property, html, css, type TemplateResult } from '@design.estate/dees-element';
2
+ import { DeesInputBase } from '../dees-input-base/dees-input-base.js';
3
+ import { textInputContextMenuItems } from '../text-input-contextmenu.js';
4
+ import { demoFunc } from './dees-input-textarea.demo.js';
5
+ import '../../00group-overlay/dees-contextmenu/dees-contextmenu.js';
6
+
7
+ declare global { interface HTMLElementTagNameMap { 'dees-input-textarea': DeesInputTextarea; } }
8
+
9
+ @customElement('dees-input-textarea')
10
+ export class DeesInputTextarea extends DeesInputBase<DeesInputTextarea> {
11
+ public static demo = demoFunc;
12
+ public static demoGroups = ['Input'];
13
+ public static inputLayout = 'compound' as const;
14
+
15
+ @property({ type: String }) accessor value = '';
16
+ @property({ type: String }) accessor placeholder = '';
17
+ @property({ type: Number }) accessor rows = 3;
18
+ @property({ type: Number }) accessor maxLength = -1;
19
+ @property({ type: String, reflect: true }) accessor validationState: 'valid' | 'warn' | 'invalid' | undefined;
20
+ @property({ type: String }) accessor validationText = '';
21
+ @property({ attribute: false }) accessor validationFunction: ((value: string) => { valid: boolean; message?: string }) | undefined;
22
+ private touched = false;
23
+ private lastValidity: boolean | undefined;
24
+
25
+ public static styles = [...DeesInputBase.baseStyles, css`
26
+ textarea { display: block; width: 100%; height: auto; padding: 10px 12px; resize: vertical; text-align: left; }
27
+ .field-messages { grid-column: 1 / -1; }
28
+ .validationText { font-size: var(--dees-font-footnote-size); line-height: var(--dees-font-footnote-line); color: var(--dees-color-text-error); }
29
+ .validationText:not(:empty) { margin-top: var(--dees-spacing-xs); }
30
+ :host([validationstate="invalid"]) textarea { border-color: var(--dees-color-accent-error); }
31
+ :host([validationstate="warn"]) .validationText { color: var(--dees-color-text-warning); }
32
+ :host([validationstate="valid"]) .validationText { color: var(--dees-color-text-success); }
33
+ `];
34
+
35
+ public render(): TemplateResult {
36
+ return html`<div class="input-wrapper">
37
+ <dees-label .label=${this.label} .infoText=${this.infoText} .required=${this.required} @click=${() => this.focus()}></dees-label>
38
+ <div class="input-container">
39
+ <textarea class="field-control" .value=${this.value} .rows=${Math.max(1, this.rows)}
40
+ placeholder=${this.placeholder} ?disabled=${this.disabled} ?required=${this.required}
41
+ aria-label=${this.label ?? ''} aria-invalid=${this.validationState === 'invalid' ? 'true' : 'false'}
42
+ aria-describedby="field-description field-validation"
43
+ @input=${this.handleInput} @blur=${() => this.validate()}></textarea>
44
+ </div>
45
+ <div class="field-messages">
46
+ <div id="field-description">${this.renderDescription()}</div>
47
+ <div id="field-validation" class="validationText" role="status">${this.validationText}</div>
48
+ </div>
49
+ </div>`;
50
+ }
51
+
52
+ public willUpdate(changed: Map<PropertyKey, unknown>): void {
53
+ super.willUpdate(changed);
54
+ if (changed.has('value') && this.value == null) this.value = '';
55
+ }
56
+
57
+ public updated(changed: Map<string, unknown>): void {
58
+ super.updated(changed);
59
+ const input = this.shadowRoot?.querySelector('textarea');
60
+ if (this.maxLength >= 0) input?.setAttribute('maxlength', String(this.maxLength));
61
+ else input?.removeAttribute('maxlength');
62
+ if (['value', 'required', 'maxLength', 'validationFunction', 'disabled'].some(key => changed.has(key))) {
63
+ this.validate(this.touched || Boolean(this.value));
64
+ } else if (changed.has('validationState') || changed.has('validationText')) {
65
+ this.syncValidity();
66
+ }
67
+ }
68
+
69
+ private ruleMessage(): string {
70
+ if (this.disabled) return '';
71
+ if (this.required && !this.value.length) return 'Enter a value.';
72
+ if (this.maxLength >= 0 && this.value.length > this.maxLength) return `Use ${this.maxLength} characters or fewer.`;
73
+ const result = this.validationFunction?.(this.value);
74
+ return result && !result.valid ? result.message || 'Check this value.' : '';
75
+ }
76
+
77
+ private syncValidity(): void {
78
+ const message = this.disabled ? '' : this.ruleMessage() || (this.validationState === 'invalid' ? this.validationText || 'Check this value.' : '');
79
+ this.shadowRoot?.querySelector('textarea')?.setCustomValidity(message);
80
+ const valid = !message;
81
+ if (valid !== this.lastValidity) {
82
+ this.lastValidity = valid;
83
+ this.dispatchEvent(new CustomEvent('validation-change', { detail: { valid, message }, bubbles: true, composed: true }));
84
+ }
85
+ }
86
+
87
+ public validate(showMessage = true): boolean {
88
+ if (showMessage) this.touched = true;
89
+ const message = this.ruleMessage();
90
+ this.validationText = showMessage ? message : '';
91
+ this.validationState = showMessage ? message ? 'invalid' : 'valid' : undefined;
92
+ this.syncValidity();
93
+ return !message;
94
+ }
95
+
96
+ public checkValidity(): boolean {
97
+ return this.disabled || (!this.ruleMessage() && this.validationState !== 'invalid');
98
+ }
99
+
100
+ public reportValidity(): boolean {
101
+ // Preserve a host-supplied validation message until the value changes.
102
+ const valid = this.validationState === 'invalid' ? this.checkValidity() : this.validate();
103
+ if (!valid) this.focus();
104
+ return valid;
105
+ }
106
+
107
+ private handleInput = (): void => {
108
+ if (this.disabled) return;
109
+ this.value = this.shadowRoot!.querySelector('textarea')!.value;
110
+ this.validate();
111
+ this.changeSubject.next(this);
112
+ };
113
+
114
+ public getContextMenuItems() {
115
+ return textInputContextMenuItems({ input: this.shadowRoot!.querySelector('textarea')!, disabled: this.disabled,
116
+ commit: value => { this.value = value; this.validate(); this.changeSubject.next(this); } });
117
+ }
118
+
119
+ public getValue(): string { return this.value; }
120
+ public setValue(value: string | null): void {
121
+ this.value = value ?? '';
122
+ this.touched = false;
123
+ this.validate(false);
124
+ }
125
+ public reset(): void { this.setValue(''); }
126
+ public focus(options?: FocusOptions): void { this.shadowRoot?.querySelector('textarea')?.focus(options); }
127
+ public blur(): void { this.shadowRoot?.querySelector('textarea')?.blur(); }
128
+ }
@@ -0,0 +1 @@
1
+ export * from './dees-input-textarea.js';
@@ -15,6 +15,7 @@ export * from './dees-input-richtext/index.js';
15
15
  export * from './dees-input-searchselect/index.js';
16
16
  export * from './dees-input-tags/index.js';
17
17
  export * from './dees-input-text/index.js';
18
+ export * from './dees-input-textarea/index.js';
18
19
  export * from './dees-input-toggle/index.js';
19
20
  export * from './dees-input-typelist/index.js';
20
21
  export * from './dees-input-wysiwyg.js';
@@ -0,0 +1,31 @@
1
+ interface ITextInputContext {
2
+ input: HTMLInputElement | HTMLTextAreaElement;
3
+ disabled: boolean;
4
+ commit: (value: string) => void;
5
+ }
6
+
7
+ /** Shared plain-text editing commands for single- and multiline fields. */
8
+ export const textInputContextMenuItems = ({ input, disabled, commit }: ITextInputContext) => {
9
+ const selectedText = () => input.value.substring(input.selectionStart ?? 0, input.selectionEnd ?? 0);
10
+ const replaceSelection = (text: string) => {
11
+ if (disabled || input.disabled) return;
12
+ input.setRangeText(text, input.selectionStart ?? 0, input.selectionEnd ?? 0, 'end');
13
+ commit(input.value);
14
+ };
15
+ return [
16
+ { name: 'Cut', iconName: 'lucide:Scissors', shortcut: 'Cmd+X', disabled: disabled || !selectedText(), action: async () => {
17
+ if (disabled || input.disabled) return;
18
+ await navigator.clipboard.writeText(selectedText());
19
+ replaceSelection('');
20
+ } },
21
+ { name: 'Copy', iconName: 'lucide:Copy', shortcut: 'Cmd+C', disabled: !selectedText(), action: async () => {
22
+ await navigator.clipboard.writeText(selectedText());
23
+ } },
24
+ { name: 'Paste', iconName: 'lucide:ClipboardPaste', shortcut: 'Cmd+V', disabled, action: async () => {
25
+ if (disabled || input.disabled) return;
26
+ replaceSelection(await navigator.clipboard.readText());
27
+ } },
28
+ { divider: true as const },
29
+ { name: 'Select All', iconName: 'lucide:TextCursorInput', shortcut: 'Cmd+A', action: () => input.select() },
30
+ ];
31
+ };