valtech-components 4.0.238 → 4.0.239

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.
@@ -56,7 +56,7 @@ import { BrowserMultiFormatReader } from '@zxing/browser';
56
56
  * Current version of valtech-components.
57
57
  * This is automatically updated during the publish process.
58
58
  */
59
- const VERSION = '4.0.238';
59
+ const VERSION = '4.0.239';
60
60
 
61
61
  // Control de estado de refresco (singleton a nivel de módulo)
62
62
  let isRefreshing = false;
@@ -73003,20 +73003,23 @@ const CHAT_COMPOSER_I18N = {
73003
73003
  },
73004
73004
  };
73005
73005
  /**
73006
- * val-chat-composer — barra de redacción de chat moderna (signal inputs).
73006
+ * val-chat-composer — barra de redacción estilo Notion/Claude (full custom).
73007
73007
  *
73008
- * Pill con: adjuntar (+), textarea auto-grow, y mic/enviar (mic cuando está vacío,
73009
- * enviar cuando hay texto o adjuntos). Los adjuntos (botón + / pegar imagen) se
73010
- * acumulan como PREVIEW (miniaturas) y se emiten por `(attach)` recién al ENVIAR
73011
- * no se van solos al chat. Audio grabado se emite directo al parar.
73012
- * No incluye picker de emoji: el teclado del sistema ya lo trae.
73008
+ * El editor es un `contenteditable` (NO ion-textarea): crece hasta un max-height,
73009
+ * soporta pegar texto plano e imágenes del portapapeles, Enter envía / Shift+Enter
73010
+ * salta línea. Debajo, una fila de acciones propias: adjuntar (+), mic/enviar.
73011
+ * Los adjuntos (botón / pegar / audio grabado) se acumulan como miniaturas de
73012
+ * PREVIEW y se mandan en UN solo evento `send {text, files}` (caption + adjuntos).
73013
+ *
73014
+ * Pensado para vivir en un contenedor fijo al fondo del viewport (el consumer
73015
+ * ancla la posición). No incluye emoji picker (el teclado del sistema lo trae).
73013
73016
  */
73014
73017
  class ChatComposerComponent {
73015
73018
  constructor() {
73016
73019
  this.i18n = inject(I18nService);
73017
73020
  this.placeholder = input('');
73018
73021
  this.disabled = input(false);
73019
- this.maxLength = input(2000);
73022
+ this.maxLength = input(4000);
73020
73023
  this.replyingTo = input(null);
73021
73024
  this.showAttach = input(true);
73022
73025
  this.showMic = input(true);
@@ -73024,6 +73027,7 @@ class ChatComposerComponent {
73024
73027
  this.typing = output();
73025
73028
  this.voice = output();
73026
73029
  this.cancelReply = output();
73030
+ this.editableRef = viewChild('editable');
73027
73031
  this.body = signal('');
73028
73032
  this.pending = signal([]);
73029
73033
  this.pendingId = 0;
@@ -73039,9 +73043,9 @@ class ChatComposerComponent {
73039
73043
  });
73040
73044
  this.lastTypingEmit = 0;
73041
73045
  this.canSend = computed(() => {
73042
- const t = this.body().trim();
73043
73046
  if (this.disabled())
73044
73047
  return false;
73048
+ const t = this.body().trim();
73045
73049
  return (t.length > 0 && t.length <= this.maxLength()) || this.pending().length > 0;
73046
73050
  });
73047
73051
  this.placeholderText = computed(() => {
@@ -73050,9 +73054,12 @@ class ChatComposerComponent {
73050
73054
  });
73051
73055
  this.i18n.registerDefaults('ChatComposer', CHAT_COMPOSER_I18N);
73052
73056
  }
73053
- onInput(event) {
73054
- const target = event.target;
73055
- this.body.set(target.value ?? '');
73057
+ /** Lee el texto del contenteditable a la signal `body`. */
73058
+ syncBody() {
73059
+ const el = this.editableRef()?.nativeElement;
73060
+ // innerText conserva los saltos de línea; quita el \n final que el navegador agrega.
73061
+ const text = (el?.innerText ?? '').replace(/\n$/, '');
73062
+ this.body.set(text);
73056
73063
  this.emitTypingDebounced();
73057
73064
  }
73058
73065
  onKeydown(event) {
@@ -73061,11 +73068,30 @@ class ChatComposerComponent {
73061
73068
  this.onSend();
73062
73069
  }
73063
73070
  }
73071
+ /** Pegar: texto plano (sin formato) + imágenes del portapapeles. */
73072
+ onPaste(event) {
73073
+ event.preventDefault();
73074
+ const data = event.clipboardData;
73075
+ if (!data)
73076
+ return;
73077
+ // Imágenes (capturas / copiar imagen).
73078
+ for (const item of Array.from(data.items)) {
73079
+ if (item.type.startsWith('image/')) {
73080
+ const file = item.getAsFile();
73081
+ if (file)
73082
+ this.addPending(file);
73083
+ }
73084
+ }
73085
+ // Texto plano insertado en el caret (evita pegar HTML con estilos).
73086
+ const text = data.getData('text/plain');
73087
+ if (text) {
73088
+ document.execCommand('insertText', false, text);
73089
+ }
73090
+ this.syncBody();
73091
+ }
73064
73092
  onSend() {
73065
73093
  if (!this.canSend())
73066
73094
  return;
73067
- // Un solo evento: texto (caption) + todos los adjuntos staged. El consumer
73068
- // los sube y manda como UN mensaje con body + attachments.
73069
73095
  const files = this.pending().map(p => p.file);
73070
73096
  const text = this.body().trim();
73071
73097
  this.send.emit({ text, files });
@@ -73074,6 +73100,12 @@ class ChatComposerComponent {
73074
73100
  URL.revokeObjectURL(att.url);
73075
73101
  }
73076
73102
  this.pending.set([]);
73103
+ this.clearEditable();
73104
+ }
73105
+ clearEditable() {
73106
+ const el = this.editableRef()?.nativeElement;
73107
+ if (el)
73108
+ el.innerText = '';
73077
73109
  this.body.set('');
73078
73110
  }
73079
73111
  onFile(event) {
@@ -73083,22 +73115,6 @@ class ChatComposerComponent {
73083
73115
  this.addPending(file);
73084
73116
  input.value = '';
73085
73117
  }
73086
- /** Pegar imagen desde el portapapeles (captura de pantalla, copiar imagen). */
73087
- onPaste(event) {
73088
- const items = event.clipboardData?.items;
73089
- if (!items)
73090
- return;
73091
- for (const item of Array.from(items)) {
73092
- if (item.type.startsWith('image/')) {
73093
- const file = item.getAsFile();
73094
- if (file) {
73095
- event.preventDefault();
73096
- this.addPending(file);
73097
- }
73098
- return;
73099
- }
73100
- }
73101
- }
73102
73118
  addPending(file) {
73103
73119
  const kind = file.type.startsWith('image/')
73104
73120
  ? 'image'
@@ -73116,9 +73132,6 @@ class ChatComposerComponent {
73116
73132
  return arr.filter(a => a.id !== id);
73117
73133
  });
73118
73134
  }
73119
- /**
73120
- * Emite typing como máximo 1 vez cada 2s. performance.now es seguro en runtime.
73121
- */
73122
73135
  emitTypingDebounced() {
73123
73136
  const now = performance.now();
73124
73137
  if (now - this.lastTypingEmit > 2000) {
@@ -73127,7 +73140,6 @@ class ChatComposerComponent {
73127
73140
  }
73128
73141
  }
73129
73142
  // ─── Grabación de audio ───────────────────────────────────────────────────
73130
- /** Selecciona un mimeType de audio soportado por el navegador. */
73131
73143
  pickAudioMime() {
73132
73144
  const candidates = ['audio/webm;codecs=opus', 'audio/webm', 'audio/mp4'];
73133
73145
  const MR = window.MediaRecorder;
@@ -73139,7 +73151,7 @@ class ChatComposerComponent {
73139
73151
  if (this.recording())
73140
73152
  return;
73141
73153
  if (!navigator.mediaDevices?.getUserMedia || !('MediaRecorder' in window)) {
73142
- this.voice.emit(); // sin soporte: deja que el consumer decida (fallback)
73154
+ this.voice.emit();
73143
73155
  return;
73144
73156
  }
73145
73157
  try {
@@ -73214,7 +73226,7 @@ class ChatComposerComponent {
73214
73226
  return this.i18n.t(key, 'ChatComposer');
73215
73227
  }
73216
73228
  static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "18.2.14", ngImport: i0, type: ChatComposerComponent, deps: [], target: i0.ɵɵFactoryTarget.Component }); }
73217
- static { this.ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "18.2.14", type: ChatComposerComponent, isStandalone: true, selector: "val-chat-composer", inputs: { placeholder: { classPropertyName: "placeholder", publicName: "placeholder", isSignal: true, isRequired: false, transformFunction: null }, disabled: { classPropertyName: "disabled", publicName: "disabled", isSignal: true, isRequired: false, transformFunction: null }, maxLength: { classPropertyName: "maxLength", publicName: "maxLength", isSignal: true, isRequired: false, transformFunction: null }, replyingTo: { classPropertyName: "replyingTo", publicName: "replyingTo", isSignal: true, isRequired: false, transformFunction: null }, showAttach: { classPropertyName: "showAttach", publicName: "showAttach", isSignal: true, isRequired: false, transformFunction: null }, showMic: { classPropertyName: "showMic", publicName: "showMic", isSignal: true, isRequired: false, transformFunction: null } }, outputs: { send: "send", typing: "typing", voice: "voice", cancelReply: "cancelReply" }, ngImport: i0, template: `
73229
+ static { this.ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "18.2.14", type: ChatComposerComponent, isStandalone: true, selector: "val-chat-composer", inputs: { placeholder: { classPropertyName: "placeholder", publicName: "placeholder", isSignal: true, isRequired: false, transformFunction: null }, disabled: { classPropertyName: "disabled", publicName: "disabled", isSignal: true, isRequired: false, transformFunction: null }, maxLength: { classPropertyName: "maxLength", publicName: "maxLength", isSignal: true, isRequired: false, transformFunction: null }, replyingTo: { classPropertyName: "replyingTo", publicName: "replyingTo", isSignal: true, isRequired: false, transformFunction: null }, showAttach: { classPropertyName: "showAttach", publicName: "showAttach", isSignal: true, isRequired: false, transformFunction: null }, showMic: { classPropertyName: "showMic", publicName: "showMic", isSignal: true, isRequired: false, transformFunction: null } }, outputs: { send: "send", typing: "typing", voice: "voice", cancelReply: "cancelReply" }, viewQueries: [{ propertyName: "editableRef", first: true, predicate: ["editable"], descendants: true, isSignal: true }], ngImport: i0, template: `
73218
73230
  <div class="composer">
73219
73231
  @if (replyingTo()) {
73220
73232
  <div class="reply-bar">
@@ -73268,48 +73280,47 @@ class ChatComposerComponent {
73268
73280
  </button>
73269
73281
  </div>
73270
73282
  } @else {
73271
- <div class="pill">
73272
- @if (showAttach()) {
73273
- <label class="icon-btn" [attr.aria-label]="t('attach')">
73274
- <ion-icon name="add-outline" aria-hidden="true" />
73275
- <input type="file" hidden (change)="onFile($event)" accept="image/*,application/pdf" />
73276
- </label>
73277
- }
73278
-
73279
- <ion-textarea
73280
- class="field"
73281
- [placeholder]="placeholderText()"
73282
- [disabled]="disabled()"
73283
- [maxlength]="maxLength()"
73284
- [autoGrow]="true"
73285
- [rows]="1"
73286
- [value]="body()"
73287
- [spellcheck]="false"
73288
- autocapitalize="sentences"
73289
- autocorrect="off"
73290
- enterkeyhint="enter"
73291
- (ionInput)="onInput($event)"
73283
+ <div class="shell">
73284
+ <div
73285
+ #editable
73286
+ class="editable"
73287
+ [class.is-empty]="!body()"
73288
+ contenteditable="true"
73289
+ role="textbox"
73290
+ aria-multiline="true"
73291
+ [attr.data-placeholder]="placeholderText()"
73292
+ [attr.aria-label]="placeholderText()"
73293
+ (input)="syncBody()"
73292
73294
  (keydown)="onKeydown($event)"
73293
73295
  (paste)="onPaste($event)"
73294
- ></ion-textarea>
73296
+ ></div>
73295
73297
 
73296
- @if (canSend()) {
73297
- <button class="send-btn" [attr.aria-label]="t('send')" (click)="onSend()">
73298
- <ion-icon name="arrow-up-outline" aria-hidden="true" />
73299
- </button>
73300
- } @else if (showMic()) {
73301
- <button class="icon-btn mic" [attr.aria-label]="t('voice')" (click)="startRecording()">
73302
- <ion-icon name="mic-outline" aria-hidden="true" />
73303
- </button>
73304
- }
73298
+ <div class="actions">
73299
+ @if (showAttach()) {
73300
+ <label class="icon-btn" [attr.aria-label]="t('attach')">
73301
+ <ion-icon name="add-outline" aria-hidden="true" />
73302
+ <input type="file" hidden (change)="onFile($event)" accept="image/*,application/pdf" />
73303
+ </label>
73304
+ }
73305
+ <span class="actions-spacer"></span>
73306
+ @if (canSend()) {
73307
+ <button class="send-btn" [attr.aria-label]="t('send')" (click)="onSend()">
73308
+ <ion-icon name="arrow-up-outline" aria-hidden="true" />
73309
+ </button>
73310
+ } @else if (showMic()) {
73311
+ <button class="icon-btn mic" [attr.aria-label]="t('voice')" (click)="startRecording()">
73312
+ <ion-icon name="mic-outline" aria-hidden="true" />
73313
+ </button>
73314
+ }
73315
+ </div>
73305
73316
  </div>
73306
73317
  }
73307
73318
  </div>
73308
- `, isInline: true, styles: [":host{display:block}.composer{display:flex;flex-direction:column;gap:6px;padding:8px 10px calc(8px + env(safe-area-inset-bottom,0px));background:var(--ion-background-color, #fff);border-top:1px solid var(--ion-border-color, rgba(0, 0, 0, .08))}.reply-bar{display:flex;align-items:center;gap:8px;padding:6px 10px;border-left:3px solid var(--ion-color-primary);border-radius:8px;background:var(--ion-color-step-100, rgba(127, 127, 127, .1))}.reply-content{display:flex;flex-direction:column;flex:1;min-width:0;font-size:.8125rem}.reply-name{font-weight:700;color:var(--ion-color-primary)}.reply-body{white-space:nowrap;overflow:hidden;text-overflow:ellipsis;color:var(--ion-color-medium, #92949c)}.pending-row{display:flex;gap:8px;flex-wrap:wrap;padding:2px 4px}.thumb{position:relative;width:64px;height:64px;border-radius:10px;overflow:hidden;border:1px solid var(--ion-border-color, rgba(0, 0, 0, .1));background:var(--ion-color-step-100, rgba(127, 127, 127, .1))}.thumb img{width:100%;height:100%;object-fit:cover}.thumb.file{display:flex;flex-direction:column;align-items:center;justify-content:center;gap:2px;padding:4px;color:var(--ion-color-medium, #92949c);font-size:1.5rem}.thumb-name{font-size:.5625rem;line-height:1.1;max-width:100%;overflow:hidden;text-overflow:ellipsis;white-space:nowrap;color:var(--ion-text-color, #000)}.thumb-remove{position:absolute;top:2px;right:2px;width:20px;height:20px;border:none;border-radius:50%;background:#0000008c;color:#fff;cursor:pointer;display:inline-flex;align-items:center;justify-content:center;font-size:.875rem}.pill{display:flex;align-items:flex-end;gap:4px;background:var(--ion-color-light, #f4f5f8);border:1px solid var(--ion-border-color, rgba(0, 0, 0, .08));border-radius:24px;padding:2px 6px}.field{flex:1;--background: transparent;--padding-start: 6px;--padding-end: 6px;--padding-top: 8px;--padding-bottom: 8px;margin:0;max-height:120px;font-size:.9375rem}.icon-btn{display:inline-flex;align-items:center;justify-content:center;width:38px;height:38px;flex-shrink:0;border:none;border-radius:50%;background:transparent;color:var(--ion-color-medium, #92949c);cursor:pointer;font-size:1.375rem}.icon-btn:hover,.icon-btn.active{color:var(--ion-color-primary)}.send-btn{display:inline-flex;align-items:center;justify-content:center;width:38px;height:38px;flex-shrink:0;border:none;border-radius:50%;background:var(--ion-color-primary);color:var(--ion-color-primary-contrast, #fff);cursor:pointer;font-size:1.25rem;transition:transform .1s ease}.send-btn:active{transform:scale(.92)}.audio-chip{display:flex;align-items:center;gap:6px;width:100%;padding:4px 6px;border-radius:12px;border:1px solid var(--ion-border-color, rgba(0, 0, 0, .1));background:var(--ion-color-step-100, rgba(127, 127, 127, .1))}.audio-chip audio{flex:1;height:36px;min-width:0}.thumb-remove.inline{position:static;flex-shrink:0;background:var(--ion-color-medium, #92949c)}.icon-btn.danger{color:var(--ion-color-danger, #eb445a)}.rec-bar{display:flex;align-items:center;gap:10px;background:var(--ion-color-light, #f4f5f8);border:1px solid var(--ion-border-color, rgba(0, 0, 0, .08));border-radius:24px;padding:2px 6px}.rec-dot{width:10px;height:10px;border-radius:50%;background:var(--ion-color-danger, #eb445a);animation:rec-pulse 1.2s ease-in-out infinite}.rec-time{font-variant-numeric:tabular-nums;font-size:.9375rem;color:var(--ion-text-color, #000)}.rec-spacer{flex:1}@keyframes rec-pulse{0%,to{opacity:1}50%{opacity:.3}}\n"], dependencies: [{ kind: "ngmodule", type: CommonModule }, { kind: "component", type: IonTextarea, selector: "ion-textarea", inputs: ["autoGrow", "autocapitalize", "autofocus", "clearOnEdit", "color", "cols", "counter", "counterFormatter", "debounce", "disabled", "enterkeyhint", "errorText", "fill", "helperText", "inputmode", "label", "labelPlacement", "maxlength", "minlength", "mode", "name", "placeholder", "readonly", "required", "rows", "shape", "spellcheck", "value", "wrap"] }, { kind: "component", type: IonIcon, selector: "ion-icon", inputs: ["color", "flipRtl", "icon", "ios", "lazy", "md", "mode", "name", "sanitize", "size", "src"] }] }); }
73319
+ `, isInline: true, styles: [":host{display:block}.composer{display:flex;flex-direction:column;gap:6px;padding:8px 10px calc(8px + env(safe-area-inset-bottom,0px));background:var(--ion-background-color, #fff);border-top:1px solid var(--ion-border-color, rgba(0, 0, 0, .08))}.reply-bar{display:flex;align-items:center;gap:8px;padding:6px 10px;border-left:3px solid var(--ion-color-primary);border-radius:8px;background:var(--ion-color-step-100, rgba(127, 127, 127, .1))}.reply-content{display:flex;flex-direction:column;flex:1;min-width:0;font-size:.8125rem}.reply-name{font-weight:700;color:var(--ion-color-primary)}.reply-body{white-space:nowrap;overflow:hidden;text-overflow:ellipsis;color:var(--ion-color-medium, #92949c)}.pending-row{display:flex;gap:8px;flex-wrap:wrap;padding:2px 4px}.thumb{position:relative;width:64px;height:64px;border-radius:10px;overflow:hidden;border:1px solid var(--ion-border-color, rgba(0, 0, 0, .1));background:var(--ion-color-step-100, rgba(127, 127, 127, .1))}.thumb img{width:100%;height:100%;object-fit:cover}.thumb.file{display:flex;flex-direction:column;align-items:center;justify-content:center;gap:2px;padding:4px;color:var(--ion-color-medium, #92949c);font-size:1.5rem}.thumb-name{font-size:.5625rem;line-height:1.1;max-width:100%;overflow:hidden;text-overflow:ellipsis;white-space:nowrap;color:var(--ion-text-color, #000)}.thumb-remove{position:absolute;top:2px;right:2px;width:20px;height:20px;border:none;border-radius:50%;background:#0000008c;color:#fff;cursor:pointer;display:inline-flex;align-items:center;justify-content:center;font-size:.875rem}.shell{display:flex;flex-direction:column;gap:4px;background:var(--ion-color-light, #f4f5f8);border:1px solid var(--ion-border-color, rgba(0, 0, 0, .08));border-radius:20px;padding:8px 12px}.editable{min-height:24px;max-height:160px;overflow-y:auto;outline:none;font-size:.9375rem;line-height:1.4;color:var(--ion-text-color, #000);white-space:pre-wrap;word-break:break-word;-webkit-user-select:text;user-select:text}.editable.is-empty:before{content:attr(data-placeholder);color:var(--ion-color-medium, #92949c);pointer-events:none}.actions{display:flex;align-items:center;gap:4px}.actions-spacer{flex:1}.icon-btn{display:inline-flex;align-items:center;justify-content:center;width:34px;height:34px;flex-shrink:0;border:none;border-radius:50%;background:transparent;color:var(--ion-color-medium, #92949c);cursor:pointer;font-size:1.25rem}.icon-btn:hover{color:var(--ion-color-primary)}.send-btn{display:inline-flex;align-items:center;justify-content:center;width:34px;height:34px;flex-shrink:0;border:none;border-radius:50%;background:var(--ion-color-primary);color:var(--ion-color-primary-contrast, #fff);cursor:pointer;font-size:1.125rem;transition:transform .1s ease}.send-btn:active{transform:scale(.92)}.icon-btn.danger{color:var(--ion-color-danger, #eb445a)}.audio-chip{display:flex;align-items:center;gap:6px;width:100%;padding:4px 6px;border-radius:12px;border:1px solid var(--ion-border-color, rgba(0, 0, 0, .1));background:var(--ion-color-step-100, rgba(127, 127, 127, .1))}.audio-chip audio{flex:1;height:36px;min-width:0}.thumb-remove.inline{position:static;flex-shrink:0;background:var(--ion-color-medium, #92949c)}.rec-bar{display:flex;align-items:center;gap:10px;background:var(--ion-color-light, #f4f5f8);border:1px solid var(--ion-border-color, rgba(0, 0, 0, .08));border-radius:20px;padding:4px 8px}.rec-dot{width:10px;height:10px;border-radius:50%;background:var(--ion-color-danger, #eb445a);animation:rec-pulse 1.2s ease-in-out infinite}.rec-time{font-variant-numeric:tabular-nums;font-size:.9375rem;color:var(--ion-text-color, #000)}.rec-spacer{flex:1}@keyframes rec-pulse{0%,to{opacity:1}50%{opacity:.3}}\n"], dependencies: [{ kind: "ngmodule", type: CommonModule }, { kind: "component", type: IonIcon, selector: "ion-icon", inputs: ["color", "flipRtl", "icon", "ios", "lazy", "md", "mode", "name", "sanitize", "size", "src"] }] }); }
73309
73320
  }
73310
73321
  i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "18.2.14", ngImport: i0, type: ChatComposerComponent, decorators: [{
73311
73322
  type: Component,
73312
- args: [{ selector: 'val-chat-composer', standalone: true, imports: [CommonModule, IonTextarea, IonIcon], template: `
73323
+ args: [{ selector: 'val-chat-composer', standalone: true, imports: [CommonModule, IonIcon], template: `
73313
73324
  <div class="composer">
73314
73325
  @if (replyingTo()) {
73315
73326
  <div class="reply-bar">
@@ -73363,44 +73374,43 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "18.2.14", ngImpo
73363
73374
  </button>
73364
73375
  </div>
73365
73376
  } @else {
73366
- <div class="pill">
73367
- @if (showAttach()) {
73368
- <label class="icon-btn" [attr.aria-label]="t('attach')">
73369
- <ion-icon name="add-outline" aria-hidden="true" />
73370
- <input type="file" hidden (change)="onFile($event)" accept="image/*,application/pdf" />
73371
- </label>
73372
- }
73373
-
73374
- <ion-textarea
73375
- class="field"
73376
- [placeholder]="placeholderText()"
73377
- [disabled]="disabled()"
73378
- [maxlength]="maxLength()"
73379
- [autoGrow]="true"
73380
- [rows]="1"
73381
- [value]="body()"
73382
- [spellcheck]="false"
73383
- autocapitalize="sentences"
73384
- autocorrect="off"
73385
- enterkeyhint="enter"
73386
- (ionInput)="onInput($event)"
73377
+ <div class="shell">
73378
+ <div
73379
+ #editable
73380
+ class="editable"
73381
+ [class.is-empty]="!body()"
73382
+ contenteditable="true"
73383
+ role="textbox"
73384
+ aria-multiline="true"
73385
+ [attr.data-placeholder]="placeholderText()"
73386
+ [attr.aria-label]="placeholderText()"
73387
+ (input)="syncBody()"
73387
73388
  (keydown)="onKeydown($event)"
73388
73389
  (paste)="onPaste($event)"
73389
- ></ion-textarea>
73390
+ ></div>
73390
73391
 
73391
- @if (canSend()) {
73392
- <button class="send-btn" [attr.aria-label]="t('send')" (click)="onSend()">
73393
- <ion-icon name="arrow-up-outline" aria-hidden="true" />
73394
- </button>
73395
- } @else if (showMic()) {
73396
- <button class="icon-btn mic" [attr.aria-label]="t('voice')" (click)="startRecording()">
73397
- <ion-icon name="mic-outline" aria-hidden="true" />
73398
- </button>
73399
- }
73392
+ <div class="actions">
73393
+ @if (showAttach()) {
73394
+ <label class="icon-btn" [attr.aria-label]="t('attach')">
73395
+ <ion-icon name="add-outline" aria-hidden="true" />
73396
+ <input type="file" hidden (change)="onFile($event)" accept="image/*,application/pdf" />
73397
+ </label>
73398
+ }
73399
+ <span class="actions-spacer"></span>
73400
+ @if (canSend()) {
73401
+ <button class="send-btn" [attr.aria-label]="t('send')" (click)="onSend()">
73402
+ <ion-icon name="arrow-up-outline" aria-hidden="true" />
73403
+ </button>
73404
+ } @else if (showMic()) {
73405
+ <button class="icon-btn mic" [attr.aria-label]="t('voice')" (click)="startRecording()">
73406
+ <ion-icon name="mic-outline" aria-hidden="true" />
73407
+ </button>
73408
+ }
73409
+ </div>
73400
73410
  </div>
73401
73411
  }
73402
73412
  </div>
73403
- `, styles: [":host{display:block}.composer{display:flex;flex-direction:column;gap:6px;padding:8px 10px calc(8px + env(safe-area-inset-bottom,0px));background:var(--ion-background-color, #fff);border-top:1px solid var(--ion-border-color, rgba(0, 0, 0, .08))}.reply-bar{display:flex;align-items:center;gap:8px;padding:6px 10px;border-left:3px solid var(--ion-color-primary);border-radius:8px;background:var(--ion-color-step-100, rgba(127, 127, 127, .1))}.reply-content{display:flex;flex-direction:column;flex:1;min-width:0;font-size:.8125rem}.reply-name{font-weight:700;color:var(--ion-color-primary)}.reply-body{white-space:nowrap;overflow:hidden;text-overflow:ellipsis;color:var(--ion-color-medium, #92949c)}.pending-row{display:flex;gap:8px;flex-wrap:wrap;padding:2px 4px}.thumb{position:relative;width:64px;height:64px;border-radius:10px;overflow:hidden;border:1px solid var(--ion-border-color, rgba(0, 0, 0, .1));background:var(--ion-color-step-100, rgba(127, 127, 127, .1))}.thumb img{width:100%;height:100%;object-fit:cover}.thumb.file{display:flex;flex-direction:column;align-items:center;justify-content:center;gap:2px;padding:4px;color:var(--ion-color-medium, #92949c);font-size:1.5rem}.thumb-name{font-size:.5625rem;line-height:1.1;max-width:100%;overflow:hidden;text-overflow:ellipsis;white-space:nowrap;color:var(--ion-text-color, #000)}.thumb-remove{position:absolute;top:2px;right:2px;width:20px;height:20px;border:none;border-radius:50%;background:#0000008c;color:#fff;cursor:pointer;display:inline-flex;align-items:center;justify-content:center;font-size:.875rem}.pill{display:flex;align-items:flex-end;gap:4px;background:var(--ion-color-light, #f4f5f8);border:1px solid var(--ion-border-color, rgba(0, 0, 0, .08));border-radius:24px;padding:2px 6px}.field{flex:1;--background: transparent;--padding-start: 6px;--padding-end: 6px;--padding-top: 8px;--padding-bottom: 8px;margin:0;max-height:120px;font-size:.9375rem}.icon-btn{display:inline-flex;align-items:center;justify-content:center;width:38px;height:38px;flex-shrink:0;border:none;border-radius:50%;background:transparent;color:var(--ion-color-medium, #92949c);cursor:pointer;font-size:1.375rem}.icon-btn:hover,.icon-btn.active{color:var(--ion-color-primary)}.send-btn{display:inline-flex;align-items:center;justify-content:center;width:38px;height:38px;flex-shrink:0;border:none;border-radius:50%;background:var(--ion-color-primary);color:var(--ion-color-primary-contrast, #fff);cursor:pointer;font-size:1.25rem;transition:transform .1s ease}.send-btn:active{transform:scale(.92)}.audio-chip{display:flex;align-items:center;gap:6px;width:100%;padding:4px 6px;border-radius:12px;border:1px solid var(--ion-border-color, rgba(0, 0, 0, .1));background:var(--ion-color-step-100, rgba(127, 127, 127, .1))}.audio-chip audio{flex:1;height:36px;min-width:0}.thumb-remove.inline{position:static;flex-shrink:0;background:var(--ion-color-medium, #92949c)}.icon-btn.danger{color:var(--ion-color-danger, #eb445a)}.rec-bar{display:flex;align-items:center;gap:10px;background:var(--ion-color-light, #f4f5f8);border:1px solid var(--ion-border-color, rgba(0, 0, 0, .08));border-radius:24px;padding:2px 6px}.rec-dot{width:10px;height:10px;border-radius:50%;background:var(--ion-color-danger, #eb445a);animation:rec-pulse 1.2s ease-in-out infinite}.rec-time{font-variant-numeric:tabular-nums;font-size:.9375rem;color:var(--ion-text-color, #000)}.rec-spacer{flex:1}@keyframes rec-pulse{0%,to{opacity:1}50%{opacity:.3}}\n"] }]
73413
+ `, styles: [":host{display:block}.composer{display:flex;flex-direction:column;gap:6px;padding:8px 10px calc(8px + env(safe-area-inset-bottom,0px));background:var(--ion-background-color, #fff);border-top:1px solid var(--ion-border-color, rgba(0, 0, 0, .08))}.reply-bar{display:flex;align-items:center;gap:8px;padding:6px 10px;border-left:3px solid var(--ion-color-primary);border-radius:8px;background:var(--ion-color-step-100, rgba(127, 127, 127, .1))}.reply-content{display:flex;flex-direction:column;flex:1;min-width:0;font-size:.8125rem}.reply-name{font-weight:700;color:var(--ion-color-primary)}.reply-body{white-space:nowrap;overflow:hidden;text-overflow:ellipsis;color:var(--ion-color-medium, #92949c)}.pending-row{display:flex;gap:8px;flex-wrap:wrap;padding:2px 4px}.thumb{position:relative;width:64px;height:64px;border-radius:10px;overflow:hidden;border:1px solid var(--ion-border-color, rgba(0, 0, 0, .1));background:var(--ion-color-step-100, rgba(127, 127, 127, .1))}.thumb img{width:100%;height:100%;object-fit:cover}.thumb.file{display:flex;flex-direction:column;align-items:center;justify-content:center;gap:2px;padding:4px;color:var(--ion-color-medium, #92949c);font-size:1.5rem}.thumb-name{font-size:.5625rem;line-height:1.1;max-width:100%;overflow:hidden;text-overflow:ellipsis;white-space:nowrap;color:var(--ion-text-color, #000)}.thumb-remove{position:absolute;top:2px;right:2px;width:20px;height:20px;border:none;border-radius:50%;background:#0000008c;color:#fff;cursor:pointer;display:inline-flex;align-items:center;justify-content:center;font-size:.875rem}.shell{display:flex;flex-direction:column;gap:4px;background:var(--ion-color-light, #f4f5f8);border:1px solid var(--ion-border-color, rgba(0, 0, 0, .08));border-radius:20px;padding:8px 12px}.editable{min-height:24px;max-height:160px;overflow-y:auto;outline:none;font-size:.9375rem;line-height:1.4;color:var(--ion-text-color, #000);white-space:pre-wrap;word-break:break-word;-webkit-user-select:text;user-select:text}.editable.is-empty:before{content:attr(data-placeholder);color:var(--ion-color-medium, #92949c);pointer-events:none}.actions{display:flex;align-items:center;gap:4px}.actions-spacer{flex:1}.icon-btn{display:inline-flex;align-items:center;justify-content:center;width:34px;height:34px;flex-shrink:0;border:none;border-radius:50%;background:transparent;color:var(--ion-color-medium, #92949c);cursor:pointer;font-size:1.25rem}.icon-btn:hover{color:var(--ion-color-primary)}.send-btn{display:inline-flex;align-items:center;justify-content:center;width:34px;height:34px;flex-shrink:0;border:none;border-radius:50%;background:var(--ion-color-primary);color:var(--ion-color-primary-contrast, #fff);cursor:pointer;font-size:1.125rem;transition:transform .1s ease}.send-btn:active{transform:scale(.92)}.icon-btn.danger{color:var(--ion-color-danger, #eb445a)}.audio-chip{display:flex;align-items:center;gap:6px;width:100%;padding:4px 6px;border-radius:12px;border:1px solid var(--ion-border-color, rgba(0, 0, 0, .1));background:var(--ion-color-step-100, rgba(127, 127, 127, .1))}.audio-chip audio{flex:1;height:36px;min-width:0}.thumb-remove.inline{position:static;flex-shrink:0;background:var(--ion-color-medium, #92949c)}.rec-bar{display:flex;align-items:center;gap:10px;background:var(--ion-color-light, #f4f5f8);border:1px solid var(--ion-border-color, rgba(0, 0, 0, .08));border-radius:20px;padding:4px 8px}.rec-dot{width:10px;height:10px;border-radius:50%;background:var(--ion-color-danger, #eb445a);animation:rec-pulse 1.2s ease-in-out infinite}.rec-time{font-variant-numeric:tabular-nums;font-size:.9375rem;color:var(--ion-text-color, #000)}.rec-spacer{flex:1}@keyframes rec-pulse{0%,to{opacity:1}50%{opacity:.3}}\n"] }]
73404
73414
  }], ctorParameters: () => [] });
73405
73415
 
73406
73416
  /**