@forgeroute/ui 0.4.71 → 0.4.72

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.
@@ -4,8 +4,8 @@ import * as i1 from "@angular/forms";
4
4
  import { FormsModule } from "@angular/forms";
5
5
  import { HttpClient, HttpErrorResponse } from "@angular/common/http";
6
6
  import { takeUntilDestroyed, toObservable } from "@angular/core/rxjs-interop";
7
- import { AlertComponent, ButtonComponent, CheckboxComponent, ChipComponent, ComboboxComponent, ContactPickerComponent, DatetimeInputComponent, ErrorComponent, FORGEROUTE_CONFIG, FormRowComponent, FormSectionComponent, HintComponent, PillTabsComponent, RadioGroupComponent, RichTextEditorComponent, SelectComponent, SidePanelComponent, TextInputComponent, TextareaInputComponent, ToastService, ToggleBadgeComponent, UiFieldValidity, createFieldReveal, getErrorMessage, requiredIssue } from "@forgeroute/ui";
8
- import { Subject, catchError, debounceTime, defer, distinctUntilChanged, map, of, switchMap, take } from "rxjs";
7
+ import { AlertComponent, BadgeComponent, ButtonComponent, CheckboxComponent, ChipComponent, ComboboxComponent, ContactPickerComponent, DatetimeInputComponent, ErrorComponent, FORGEROUTE_CONFIG, FormRowComponent, FormSectionComponent, HintComponent, PillTabsComponent, RadioGroupComponent, RichTextEditorComponent, SelectComponent, SidePanelComponent, TextInputComponent, TextareaInputComponent, ToastService, ToggleBadgeComponent, UiFieldValidity, createFieldReveal, getErrorMessage, requiredIssue } from "@forgeroute/ui";
8
+ import { Subject, catchError, debounceTime, defer, distinctUntilChanged, map, of, switchMap, take, throwError } from "rxjs";
9
9
  let nextVideoPickerId = 0;
10
10
  var VideoPickerComponent = class VideoPickerComponent {
11
11
  optionId(i) {
@@ -1958,6 +1958,7 @@ var DocumentSendOptionsComponent = class DocumentSendOptionsComponent {
1958
1958
  inputs: [
1959
1959
  "label",
1960
1960
  "ariaLabel",
1961
+ "inputName",
1961
1962
  "placeholder",
1962
1963
  "options",
1963
1964
  "valueType",
@@ -2080,6 +2081,7 @@ var DocumentSendOptionsComponent = class DocumentSendOptionsComponent {
2080
2081
  "label",
2081
2082
  "ariaLabel",
2082
2083
  "inputTestId",
2084
+ "inputName",
2083
2085
  "placeholder",
2084
2086
  "prefix",
2085
2087
  "revealable",
@@ -3287,6 +3289,7 @@ var VideoLinkSettingsComponent = class VideoLinkSettingsComponent {
3287
3289
  inputs: [
3288
3290
  "label",
3289
3291
  "ariaLabel",
3292
+ "inputName",
3290
3293
  "placeholder",
3291
3294
  "options",
3292
3295
  "valueType",
@@ -3308,6 +3311,7 @@ var VideoLinkSettingsComponent = class VideoLinkSettingsComponent {
3308
3311
  "label",
3309
3312
  "ariaLabel",
3310
3313
  "inputTestId",
3314
+ "inputName",
3311
3315
  "placeholder",
3312
3316
  "prefix",
3313
3317
  "revealable",
@@ -3526,8 +3530,18 @@ function applyMergeTags(text, values) {
3526
3530
  return v != null && String(v).trim() !== "" ? String(v) : whole;
3527
3531
  });
3528
3532
  }
3529
- function hasLeftoverTags(text) {
3530
- return ANY_TAG_RE.test(text ?? "");
3533
+ const PERSONAL_TAG_KEYS = [
3534
+ "first_name",
3535
+ "last_name",
3536
+ "full_name",
3537
+ "name",
3538
+ "email",
3539
+ "phone"
3540
+ ];
3541
+ function hasLeftoverTags(text, ignoreKeys = []) {
3542
+ const t = text ?? "";
3543
+ if (ignoreKeys.length === 0) return ANY_TAG_RE.test(t);
3544
+ return ANY_TAG_RE.test(t.replace(TAG_RE, (whole, k) => ignoreKeys.includes(k) ? "" : whole));
3531
3545
  }
3532
3546
  function nonBlankValues(...sources) {
3533
3547
  const out = {};
@@ -3561,6 +3575,7 @@ function marketVarsFrom(resp) {
3561
3575
  function formatVarLabel(key) {
3562
3576
  return key.replace(/[_.]/g, " ").replace(/\b\w/g, (c) => c.toUpperCase());
3563
3577
  }
3578
+ const BULK_NO_SEND_MESSAGE = "Sending to several recipients is not set up for this page, so nothing was sent.";
3564
3579
  const EMAIL_RE = /^[^\s@]+@[^\s@]+\.[^\s@]+$/;
3565
3580
  function messagePreviewHtml(body) {
3566
3581
  if (!body) return "";
@@ -3569,6 +3584,9 @@ function messagePreviewHtml(body) {
3569
3584
  }
3570
3585
  const NO_DOCUMENTS$1 = [];
3571
3586
  var SendEmailComponent = class SendEmailComponent {
3587
+ get deferredKeys() {
3588
+ return this.bulk() ? PERSONAL_TAG_KEYS : [];
3589
+ }
3572
3590
  ngOnInit() {
3573
3591
  this.includeReceipt = this.receiptDefault();
3574
3592
  this.http.get(`${this.apiBase}/service-areas?size=100`).pipe(takeUntilDestroyed(this.destroyRef)).subscribe({ next: (resp) => {
@@ -3586,6 +3604,8 @@ var SendEmailComponent = class SendEmailComponent {
3586
3604
  this.contactEmail = input("", ...ngDevMode ? [{ debugName: "contactEmail" }] : /* istanbul ignore next */ []);
3587
3605
  this.contactPhone = input("", ...ngDevMode ? [{ debugName: "contactPhone" }] : /* istanbul ignore next */ []);
3588
3606
  this.contactId = input("", ...ngDevMode ? [{ debugName: "contactId" }] : /* istanbul ignore next */ []);
3607
+ this.bulkRecipients = input(null, ...ngDevMode ? [{ debugName: "bulkRecipients" }] : /* istanbul ignore next */ []);
3608
+ this.bulk = computed(() => this.bulkRecipients() !== null, ...ngDevMode ? [{ debugName: "bulk" }] : /* istanbul ignore next */ []);
3589
3609
  this.defaultSubject = input("", ...ngDevMode ? [{ debugName: "defaultSubject" }] : /* istanbul ignore next */ []);
3590
3610
  this.defaultBody = input("", ...ngDevMode ? [{ debugName: "defaultBody" }] : /* istanbul ignore next */ []);
3591
3611
  this.context = input({}, ...ngDevMode ? [{ debugName: "context" }] : /* istanbul ignore next */ []);
@@ -3626,6 +3646,8 @@ var SendEmailComponent = class SendEmailComponent {
3626
3646
  this.showTemplates = input(true, ...ngDevMode ? [{ debugName: "showTemplates" }] : /* istanbul ignore next */ []);
3627
3647
  this.showVideo = input(true, ...ngDevMode ? [{ debugName: "showVideo" }] : /* istanbul ignore next */ []);
3628
3648
  this.sent = output();
3649
+ this.bulkResult = output();
3650
+ this.bulkSending = output();
3629
3651
  this.emailAdded = output();
3630
3652
  this.validChange = output();
3631
3653
  this.rejected = output();
@@ -3684,6 +3706,7 @@ var SendEmailComponent = class SendEmailComponent {
3684
3706
  this.contactEmail();
3685
3707
  this.contactId();
3686
3708
  this.context();
3709
+ this.bulkRecipients();
3687
3710
  untracked(() => this.notifyValid());
3688
3711
  });
3689
3712
  effect(() => {
@@ -3823,7 +3846,7 @@ var SendEmailComponent = class SendEmailComponent {
3823
3846
  if (this.ccError || this.documents() && this.documentsInvalid) return false;
3824
3847
  if (this.signersActive && (this.signersInvalid || !this.signersValue)) return false;
3825
3848
  if (this.viaDocuSign && !this.contactId()) return false;
3826
- const hasContent = (this.viaDocuSign || (!this.showSubject() || !!this.subject.trim()) && !!this.body.trim()) && this.unresolvedVars.length === 0 && !hasLeftoverTags(this.resolvedSubject) && !hasLeftoverTags(this.resolvedBody);
3849
+ const hasContent = (this.viaDocuSign || (!this.showSubject() || !!this.subject.trim()) && !!this.body.trim()) && this.unresolvedVars.length === 0 && !hasLeftoverTags(this.resolvedSubject, this.deferredKeys) && !hasLeftoverTags(this.resolvedBody, this.deferredKeys);
3827
3850
  if (this.showSchedule) return hasContent && !!this.scheduledAt && new Date(this.scheduledAt).getTime() > Date.now();
3828
3851
  return hasContent;
3829
3852
  }
@@ -3833,7 +3856,7 @@ var SendEmailComponent = class SendEmailComponent {
3833
3856
  get knownVars() {
3834
3857
  const name = this.contactName().trim();
3835
3858
  const parts = name.split(/\s+/);
3836
- return nonBlankValues({
3859
+ return nonBlankValues(this.bulk() ? {} : {
3837
3860
  first_name: parts[0] ?? "",
3838
3861
  last_name: parts.slice(1).join(" "),
3839
3862
  full_name: name,
@@ -3870,11 +3893,11 @@ var SendEmailComponent = class SendEmailComponent {
3870
3893
  }
3871
3894
  get missingVars() {
3872
3895
  const known = this.knownVars;
3873
- return mergeTagKeys(this.subject + " " + this.body).filter((k) => !known[k]);
3896
+ return mergeTagKeys(this.subject + " " + this.body).filter((k) => !known[k] && !this.deferredKeys.includes(k));
3874
3897
  }
3875
3898
  get composeTagError() {
3876
3899
  if (this.mode !== "compose") return "";
3877
- return hasLeftoverTags(this.resolvedSubject) || hasLeftoverTags(this.resolvedBody) ? "Replace every {{…}} field with text before sending." : "";
3900
+ return hasLeftoverTags(this.resolvedSubject, this.deferredKeys) || hasLeftoverTags(this.resolvedBody, this.deferredKeys) ? "Replace every {{…}} field with text before sending." : "";
3878
3901
  }
3879
3902
  get unresolvedVars() {
3880
3903
  return this.missingVars.filter((k) => !(this.pendingInputs[k] ?? "").trim());
@@ -3916,7 +3939,7 @@ var SendEmailComponent = class SendEmailComponent {
3916
3939
  this.cdr.markForCheck();
3917
3940
  }
3918
3941
  notifyValid() {
3919
- this.validChange.emit(!!this.contactEmail() && this.canSend);
3942
+ this.validChange.emit((this.bulk() || !!this.contactEmail()) && this.canSend);
3920
3943
  this.cdr.markForCheck();
3921
3944
  }
3922
3945
  setMode(key) {
@@ -4036,7 +4059,11 @@ var SendEmailComponent = class SendEmailComponent {
4036
4059
  this.videoPickerKey++;
4037
4060
  }
4038
4061
  submit() {
4039
- if (!this.canSend || this.loading || !this.contactEmail()) return;
4062
+ if (!this.canSend || this.loading || !this.bulk() && !this.contactEmail()) return;
4063
+ if (this.bulk() && !this.sendFn()) {
4064
+ this.toast.error(BULK_NO_SEND_MESSAGE);
4065
+ return;
4066
+ }
4040
4067
  if (this.mode === "template") this.applyPendingVars();
4041
4068
  this.loading = true;
4042
4069
  this.cdr.markForCheck();
@@ -4054,6 +4081,10 @@ var SendEmailComponent = class SendEmailComponent {
4054
4081
  scheduledAt: this.showSchedule && this.scheduledAt ? new Date(this.scheduledAt).toISOString() : void 0,
4055
4082
  attachments: attachments.length ? attachments : void 0
4056
4083
  };
4084
+ if (this.bulk()) {
4085
+ payload.recipients = [...this.bulkRecipients()];
4086
+ this.bulkSending.emit(payload.recipients);
4087
+ }
4057
4088
  if (this.showReceiptOption() === true) payload.includeReceipt = this.includeReceipt;
4058
4089
  if (this.mode === "template" && this.templateId) payload.templateId = this.templateId;
4059
4090
  const cc = this.ccList;
@@ -4069,7 +4100,14 @@ var SendEmailComponent = class SendEmailComponent {
4069
4100
  payload.scheduledAt = void 0;
4070
4101
  }
4071
4102
  const custom = this.sendFn();
4072
- (custom ? custom(payload) : docs?.provider === "DOCUSIGN" ? sendDocuSignRequest(this.http, this.apiBase, this.docuSignBody(docs, payload)) : docs && this.documentEnvelope() ? sendExistingEnvelope(this.http, this.apiBase, this.documentEnvelope().id, {
4103
+ const safeHostCall = () => {
4104
+ try {
4105
+ return custom(payload);
4106
+ } catch (e) {
4107
+ return throwError(() => e);
4108
+ }
4109
+ };
4110
+ const req$ = custom && this.bulk() ? safeHostCall() : custom ? custom(payload) : docs?.provider === "DOCUSIGN" ? sendDocuSignRequest(this.http, this.apiBase, this.docuSignBody(docs, payload)) : docs && this.documentEnvelope() ? sendExistingEnvelope(this.http, this.apiBase, this.documentEnvelope().id, {
4073
4111
  subject: payload.subject || "Document Signature Required",
4074
4112
  message: payload.body,
4075
4113
  customerId: payload.contactId,
@@ -4093,8 +4131,22 @@ var SendEmailComponent = class SendEmailComponent {
4093
4131
  ...cc.length ? { ccEmails: cc } : {},
4094
4132
  ...signingBodyFields(payload.documents),
4095
4133
  ...signersContextBodyFields(payload.documents)
4096
- }, payload.templateId) : this.http.post(`${this.apiBase}/email-messages`, payload)).pipe(takeUntilDestroyed(this.destroyRef)).subscribe({
4097
- next: () => {
4134
+ }, payload.templateId) : this.http.post(`${this.apiBase}/email-messages`, payload);
4135
+ let destroyed = false;
4136
+ this.destroyRef.onDestroy(() => {
4137
+ destroyed = true;
4138
+ });
4139
+ const onError = (err) => {
4140
+ if (this.bulk() && (destroyed || !this.loading)) return;
4141
+ this.toast.error(custom || docs ? getErrorMessage(err, "email", "send") : serverMessage(err) || "Failed to send email");
4142
+ this.loading = false;
4143
+ this.cdr.markForCheck();
4144
+ if (this.bulk()) this.bulkSending.emit(null);
4145
+ const status = err?.status;
4146
+ if (docs && (status === 404 || status === 409)) this.rejected.emit();
4147
+ };
4148
+ req$.pipe(takeUntilDestroyed(this.destroyRef)).subscribe({
4149
+ next: (result) => {
4098
4150
  const sentText = docs ? this.documentSentMessage()?.({
4099
4151
  titles: docs.documents.map((d) => d.title),
4100
4152
  envelopeName: this.documentEnvelope()?.name,
@@ -4106,15 +4158,16 @@ var SendEmailComponent = class SendEmailComponent {
4106
4158
  this.loading = false;
4107
4159
  this.attachments = [];
4108
4160
  this.includeReceipt = this.receiptDefault();
4161
+ if (this.bulk()) this.bulkResult.emit({
4162
+ result,
4163
+ recipients: payload.recipients ?? []
4164
+ });
4109
4165
  this.sent.emit();
4110
4166
  this.cdr.markForCheck();
4111
4167
  },
4112
- error: (err) => {
4113
- this.toast.error(custom || docs ? getErrorMessage(err, "email", "send") : serverMessage(err) || "Failed to send email");
4114
- this.loading = false;
4115
- this.cdr.markForCheck();
4116
- const status = err?.status;
4117
- if (docs && (status === 404 || status === 409)) this.rejected.emit();
4168
+ error: onError,
4169
+ complete: () => {
4170
+ if (!destroyed && this.bulk() && this.loading) onError(null);
4118
4171
  }
4119
4172
  });
4120
4173
  }
@@ -4164,6 +4217,13 @@ var SendEmailComponent = class SendEmailComponent {
4164
4217
  isRequired: false,
4165
4218
  transformFunction: null
4166
4219
  },
4220
+ bulkRecipients: {
4221
+ classPropertyName: "bulkRecipients",
4222
+ publicName: "bulkRecipients",
4223
+ isSignal: true,
4224
+ isRequired: false,
4225
+ transformFunction: null
4226
+ },
4167
4227
  defaultSubject: {
4168
4228
  classPropertyName: "defaultSubject",
4169
4229
  publicName: "defaultSubject",
@@ -4349,6 +4409,8 @@ var SendEmailComponent = class SendEmailComponent {
4349
4409
  },
4350
4410
  outputs: {
4351
4411
  sent: "sent",
4412
+ bulkResult: "bulkResult",
4413
+ bulkSending: "bulkSending",
4352
4414
  emailAdded: "emailAdded",
4353
4415
  validChange: "validChange",
4354
4416
  rejected: "rejected",
@@ -4357,7 +4419,7 @@ var SendEmailComponent = class SendEmailComponent {
4357
4419
  ngImport: i0,
4358
4420
  template: `
4359
4421
  <!-- Missing email guard -->
4360
- @if (!contactEmail()) {
4422
+ @if (!bulk() && !contactEmail()) {
4361
4423
  <div class="sm-missing-banner">
4362
4424
  <svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" aria-hidden="true"><circle cx="12" cy="12" r="10"/><line x1="12" y1="8" x2="12" y2="12"/><line x1="12" y1="16" x2="12.01" y2="16"/></svg>
4363
4425
  No email address on file for this contact.
@@ -4374,7 +4436,8 @@ var SendEmailComponent = class SendEmailComponent {
4374
4436
  Save &amp; Continue
4375
4437
  </ui-button>
4376
4438
  } @else {
4377
- <!-- To -->
4439
+ <!-- To (bulk mode: the panel shows the recipient summary instead) -->
4440
+ @if (!bulk()) {
4378
4441
  <div class="sm-group">
4379
4442
  <span class="sm-group-label">To</span>
4380
4443
  <div class="sm-to-card">
@@ -4382,6 +4445,7 @@ var SendEmailComponent = class SendEmailComponent {
4382
4445
  <span class="sm-to-detail">{{ contactEmail() }}</span>
4383
4446
  </div>
4384
4447
  </div>
4448
+ }
4385
4449
 
4386
4450
  <ui-error errorId="sm-email-disabled" [message]="emailEnabled() ? '' : EMAIL_DISABLED_MESSAGE" />
4387
4451
 
@@ -4608,7 +4672,7 @@ var SendEmailComponent = class SendEmailComponent {
4608
4672
  <div class="sp-email-meta">
4609
4673
  <div class="sp-meta-row">
4610
4674
  <span class="sp-meta-label">To</span>
4611
- <span class="sp-meta-value">{{ contactName() }} &lt;{{ contactEmail() }}&gt;</span>
4675
+ <span class="sp-meta-value">@if (bulk()) { {{ bulkRecipients()!.length }} recipient{{ bulkRecipients()!.length === 1 ? '' : 's' }} } @else { {{ contactName() }} &lt;{{ contactEmail() }}&gt; }</span>
4612
4676
  </div>
4613
4677
  <div class="sp-meta-row">
4614
4678
  <span class="sp-meta-label">Subject</span>
@@ -4768,6 +4832,7 @@ var SendEmailComponent = class SendEmailComponent {
4768
4832
  "label",
4769
4833
  "ariaLabel",
4770
4834
  "inputTestId",
4835
+ "inputName",
4771
4836
  "placeholder",
4772
4837
  "prefix",
4773
4838
  "revealable",
@@ -4965,7 +5030,7 @@ i0.ɵɵngDeclareClassMetadata({
4965
5030
  ],
4966
5031
  template: `
4967
5032
  <!-- Missing email guard -->
4968
- @if (!contactEmail()) {
5033
+ @if (!bulk() && !contactEmail()) {
4969
5034
  <div class="sm-missing-banner">
4970
5035
  <svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" aria-hidden="true"><circle cx="12" cy="12" r="10"/><line x1="12" y1="8" x2="12" y2="12"/><line x1="12" y1="16" x2="12.01" y2="16"/></svg>
4971
5036
  No email address on file for this contact.
@@ -4982,7 +5047,8 @@ i0.ɵɵngDeclareClassMetadata({
4982
5047
  Save &amp; Continue
4983
5048
  </ui-button>
4984
5049
  } @else {
4985
- <!-- To -->
5050
+ <!-- To (bulk mode: the panel shows the recipient summary instead) -->
5051
+ @if (!bulk()) {
4986
5052
  <div class="sm-group">
4987
5053
  <span class="sm-group-label">To</span>
4988
5054
  <div class="sm-to-card">
@@ -4990,6 +5056,7 @@ i0.ɵɵngDeclareClassMetadata({
4990
5056
  <span class="sm-to-detail">{{ contactEmail() }}</span>
4991
5057
  </div>
4992
5058
  </div>
5059
+ }
4993
5060
 
4994
5061
  <ui-error errorId="sm-email-disabled" [message]="emailEnabled() ? '' : EMAIL_DISABLED_MESSAGE" />
4995
5062
 
@@ -5216,7 +5283,7 @@ i0.ɵɵngDeclareClassMetadata({
5216
5283
  <div class="sp-email-meta">
5217
5284
  <div class="sp-meta-row">
5218
5285
  <span class="sp-meta-label">To</span>
5219
- <span class="sp-meta-value">{{ contactName() }} &lt;{{ contactEmail() }}&gt;</span>
5286
+ <span class="sp-meta-value">@if (bulk()) { {{ bulkRecipients()!.length }} recipient{{ bulkRecipients()!.length === 1 ? '' : 's' }} } @else { {{ contactName() }} &lt;{{ contactEmail() }}&gt; }</span>
5220
5287
  </div>
5221
5288
  <div class="sp-meta-row">
5222
5289
  <span class="sp-meta-label">Subject</span>
@@ -5265,6 +5332,14 @@ i0.ɵɵngDeclareClassMetadata({
5265
5332
  required: false
5266
5333
  }]
5267
5334
  }],
5335
+ bulkRecipients: [{
5336
+ type: i0.Input,
5337
+ args: [{
5338
+ isSignal: true,
5339
+ alias: "bulkRecipients",
5340
+ required: false
5341
+ }]
5342
+ }],
5268
5343
  defaultSubject: [{
5269
5344
  type: i0.Input,
5270
5345
  args: [{
@@ -5477,6 +5552,14 @@ i0.ɵɵngDeclareClassMetadata({
5477
5552
  type: i0.Output,
5478
5553
  args: ["sent"]
5479
5554
  }],
5555
+ bulkResult: [{
5556
+ type: i0.Output,
5557
+ args: ["bulkResult"]
5558
+ }],
5559
+ bulkSending: [{
5560
+ type: i0.Output,
5561
+ args: ["bulkSending"]
5562
+ }],
5480
5563
  emailAdded: [{
5481
5564
  type: i0.Output,
5482
5565
  args: ["emailAdded"]
@@ -5798,6 +5881,7 @@ var SendInAppComponent = class SendInAppComponent {
5798
5881
  "label",
5799
5882
  "ariaLabel",
5800
5883
  "inputTestId",
5884
+ "inputName",
5801
5885
  "placeholder",
5802
5886
  "prefix",
5803
5887
  "revealable",
@@ -6063,6 +6147,9 @@ function formatPhone(raw) {
6063
6147
  }
6064
6148
  const NO_DOCUMENTS = [];
6065
6149
  var SendTextComponent = class SendTextComponent {
6150
+ get deferredKeys() {
6151
+ return this.bulk() ? PERSONAL_TAG_KEYS : [];
6152
+ }
6066
6153
  ngOnInit() {
6067
6154
  this.http.get(`${this.apiBase}/service-areas?size=100`).pipe(takeUntilDestroyed(this.destroyRef)).subscribe({ next: (resp) => {
6068
6155
  this.marketVars = marketVarsFrom(resp);
@@ -6080,6 +6167,8 @@ var SendTextComponent = class SendTextComponent {
6080
6167
  this.contactPhone = input("", ...ngDevMode ? [{ debugName: "contactPhone" }] : /* istanbul ignore next */ []);
6081
6168
  this.contactEmail = input("", ...ngDevMode ? [{ debugName: "contactEmail" }] : /* istanbul ignore next */ []);
6082
6169
  this.contactId = input("", ...ngDevMode ? [{ debugName: "contactId" }] : /* istanbul ignore next */ []);
6170
+ this.bulkRecipients = input(null, ...ngDevMode ? [{ debugName: "bulkRecipients" }] : /* istanbul ignore next */ []);
6171
+ this.bulk = computed(() => this.bulkRecipients() !== null, ...ngDevMode ? [{ debugName: "bulk" }] : /* istanbul ignore next */ []);
6083
6172
  this.defaultBody = input("", ...ngDevMode ? [{ debugName: "defaultBody" }] : /* istanbul ignore next */ []);
6084
6173
  this.context = input({}, ...ngDevMode ? [{ debugName: "context" }] : /* istanbul ignore next */ []);
6085
6174
  this.sendFn = input(null, ...ngDevMode ? [{ debugName: "sendFn" }] : /* istanbul ignore next */ []);
@@ -6112,6 +6201,8 @@ var SendTextComponent = class SendTextComponent {
6112
6201
  }, ...ngDevMode ? [{ debugName: "previewContact" }] : /* istanbul ignore next */ []);
6113
6202
  this.videoDefaults = input(null, ...ngDevMode ? [{ debugName: "videoDefaults" }] : /* istanbul ignore next */ []);
6114
6203
  this.sent = output();
6204
+ this.bulkResult = output();
6205
+ this.bulkSending = output();
6115
6206
  this.phoneAdded = output();
6116
6207
  this.validChange = output();
6117
6208
  this.rejected = output();
@@ -6158,6 +6249,7 @@ var SendTextComponent = class SendTextComponent {
6158
6249
  this.contactPhone();
6159
6250
  this.contactId();
6160
6251
  this.context();
6252
+ this.bulkRecipients();
6161
6253
  untracked(() => this.notifyValid());
6162
6254
  });
6163
6255
  effect(() => {
@@ -6218,7 +6310,7 @@ var SendTextComponent = class SendTextComponent {
6218
6310
  get canSend() {
6219
6311
  if (this.documents() && this.documentsInvalid) return false;
6220
6312
  if (this._videoLine && this.body.length > this.MAX_LENGTH) return false;
6221
- const hasBody = this.body.trim().length > 0 && this.unresolvedVars.length === 0 && !hasLeftoverTags(this.resolvedBody);
6313
+ const hasBody = this.body.trim().length > 0 && this.unresolvedVars.length === 0 && !hasLeftoverTags(this.resolvedBody, this.deferredKeys);
6222
6314
  if (this.showSchedule) return hasBody && !!this.scheduledAt && new Date(this.scheduledAt).getTime() > Date.now();
6223
6315
  return hasBody;
6224
6316
  }
@@ -6228,7 +6320,7 @@ var SendTextComponent = class SendTextComponent {
6228
6320
  get knownVars() {
6229
6321
  const name = this.contactName().trim();
6230
6322
  const parts = name.split(/\s+/);
6231
- return nonBlankValues({
6323
+ return nonBlankValues(this.bulk() ? {} : {
6232
6324
  first_name: parts[0] ?? "",
6233
6325
  last_name: parts.slice(1).join(" "),
6234
6326
  full_name: name,
@@ -6253,13 +6345,13 @@ var SendTextComponent = class SendTextComponent {
6253
6345
  }
6254
6346
  get missingVars() {
6255
6347
  const known = this.knownVars;
6256
- return mergeTagKeys(this.body).filter((k) => !known[k]);
6348
+ return mergeTagKeys(this.body).filter((k) => !known[k] && !this.deferredKeys.includes(k));
6257
6349
  }
6258
6350
  get videoLengthError() {
6259
6351
  return this._videoLine && this.body.length > this.MAX_LENGTH ? `The video link makes this text longer than ${this.MAX_LENGTH} characters. Shorten the message or remove the video.` : "";
6260
6352
  }
6261
6353
  get composeTagError() {
6262
- return this.mode === "compose" && hasLeftoverTags(this.resolvedBody) ? "Replace every {{…}} field with text before sending." : "";
6354
+ return this.mode === "compose" && hasLeftoverTags(this.resolvedBody, this.deferredKeys) ? "Replace every {{…}} field with text before sending." : "";
6263
6355
  }
6264
6356
  get unresolvedVars() {
6265
6357
  return this.missingVars.filter((k) => !(this.pendingInputs[k] ?? "").trim());
@@ -6303,7 +6395,7 @@ var SendTextComponent = class SendTextComponent {
6303
6395
  this.notifyValid();
6304
6396
  }
6305
6397
  notifyValid() {
6306
- this.validChange.emit(!!this.contactPhone() && this.canSend);
6398
+ this.validChange.emit((this.bulk() || !!this.contactPhone()) && this.canSend);
6307
6399
  this.cdr.detectChanges();
6308
6400
  }
6309
6401
  setMode(key) {
@@ -6404,7 +6496,11 @@ var SendTextComponent = class SendTextComponent {
6404
6496
  this.videoPickerKey++;
6405
6497
  }
6406
6498
  submit() {
6407
- if (!this.canSend || this.loading || !this.contactPhone()) return;
6499
+ if (!this.canSend || this.loading || !this.bulk() && !this.contactPhone()) return;
6500
+ if (this.bulk() && !this.sendFn()) {
6501
+ this.toast.error(BULK_NO_SEND_MESSAGE);
6502
+ return;
6503
+ }
6408
6504
  if (this.mode === "template") this.applyPendingVars();
6409
6505
  this.loading = true;
6410
6506
  this.cdr.detectChanges();
@@ -6419,6 +6515,10 @@ var SendTextComponent = class SendTextComponent {
6419
6515
  mediaType: media?.mediaType ?? void 0,
6420
6516
  mediaFileName: media?.mediaFileName ?? void 0
6421
6517
  };
6518
+ if (this.bulk()) {
6519
+ payload.recipients = [...this.bulkRecipients()];
6520
+ this.bulkSending.emit(payload.recipients);
6521
+ }
6422
6522
  if (this.mode === "template" && this.templateId) payload.templateId = this.templateId;
6423
6523
  const docs = this.hasDocuments ? this.documentsValue : null;
6424
6524
  if (docs) {
@@ -6432,7 +6532,14 @@ var SendTextComponent = class SendTextComponent {
6432
6532
  payload.scheduledAt = void 0;
6433
6533
  }
6434
6534
  const custom = this.sendFn();
6435
- (custom ? custom(payload) : docs && this.documentEnvelope() ? sendExistingEnvelope(this.http, this.apiBase, this.documentEnvelope().id, {
6535
+ const safeHostCall = () => {
6536
+ try {
6537
+ return custom(payload);
6538
+ } catch (e) {
6539
+ return throwError(() => e);
6540
+ }
6541
+ };
6542
+ const req$ = custom && this.bulk() ? safeHostCall() : custom ? custom(payload) : docs && this.documentEnvelope() ? sendExistingEnvelope(this.http, this.apiBase, this.documentEnvelope().id, {
6436
6543
  subject: DEFAULT_DOCUMENT_SUBJECT,
6437
6544
  message: payload.body,
6438
6545
  customerId: payload.contactId,
@@ -6456,8 +6563,22 @@ var SendTextComponent = class SendTextComponent {
6456
6563
  delivery: "TEXT",
6457
6564
  ...signingBodyFields(payload.documents),
6458
6565
  ...signersContextBodyFields(payload.documents)
6459
- }, payload.templateId) : this.http.post(`${this.apiBase}/text-messages`, payload)).pipe(takeUntilDestroyed(this.destroyRef)).subscribe({
6460
- next: () => {
6566
+ }, payload.templateId) : this.http.post(`${this.apiBase}/text-messages`, payload);
6567
+ let destroyed = false;
6568
+ this.destroyRef.onDestroy(() => {
6569
+ destroyed = true;
6570
+ });
6571
+ const onError = (err) => {
6572
+ if (this.bulk() && (destroyed || !this.loading)) return;
6573
+ this.toast.error(custom || docs ? getErrorMessage(err, "text message", "send") : serverMessage(err) || "Failed to send text message");
6574
+ this.loading = false;
6575
+ this.cdr.detectChanges();
6576
+ if (this.bulk()) this.bulkSending.emit(null);
6577
+ const status = err?.status;
6578
+ if (docs && (status === 404 || status === 409)) this.rejected.emit();
6579
+ };
6580
+ req$.pipe(takeUntilDestroyed(this.destroyRef)).subscribe({
6581
+ next: (result) => {
6461
6582
  const sentText = docs ? this.documentSentMessage()?.({
6462
6583
  titles: docs.documents.map((d) => d.title),
6463
6584
  envelopeName: this.documentEnvelope()?.name,
@@ -6467,15 +6588,16 @@ var SendTextComponent = class SendTextComponent {
6467
6588
  this.toast.show(sentText ?? (docs && this.documentEnvelope() ? `Envelope "${this.documentEnvelope().name}" sent` : this.showSchedule ? "Message scheduled" : "Text message sent"));
6468
6589
  this.loading = false;
6469
6590
  this.mediaAttachment.set(null);
6591
+ if (this.bulk()) this.bulkResult.emit({
6592
+ result,
6593
+ recipients: payload.recipients ?? []
6594
+ });
6470
6595
  this.sent.emit();
6471
6596
  this.cdr.detectChanges();
6472
6597
  },
6473
- error: (err) => {
6474
- this.toast.error(custom || docs ? getErrorMessage(err, "text message", "send") : serverMessage(err) || "Failed to send text message");
6475
- this.loading = false;
6476
- this.cdr.detectChanges();
6477
- const status = err?.status;
6478
- if (docs && (status === 404 || status === 409)) this.rejected.emit();
6598
+ error: onError,
6599
+ complete: () => {
6600
+ if (!destroyed && this.bulk() && this.loading) onError(null);
6479
6601
  }
6480
6602
  });
6481
6603
  }
@@ -6525,6 +6647,13 @@ var SendTextComponent = class SendTextComponent {
6525
6647
  isRequired: false,
6526
6648
  transformFunction: null
6527
6649
  },
6650
+ bulkRecipients: {
6651
+ classPropertyName: "bulkRecipients",
6652
+ publicName: "bulkRecipients",
6653
+ isSignal: true,
6654
+ isRequired: false,
6655
+ transformFunction: null
6656
+ },
6528
6657
  defaultBody: {
6529
6658
  classPropertyName: "defaultBody",
6530
6659
  publicName: "defaultBody",
@@ -6654,6 +6783,8 @@ var SendTextComponent = class SendTextComponent {
6654
6783
  },
6655
6784
  outputs: {
6656
6785
  sent: "sent",
6786
+ bulkResult: "bulkResult",
6787
+ bulkSending: "bulkSending",
6657
6788
  phoneAdded: "phoneAdded",
6658
6789
  validChange: "validChange",
6659
6790
  rejected: "rejected"
@@ -6661,7 +6792,7 @@ var SendTextComponent = class SendTextComponent {
6661
6792
  ngImport: i0,
6662
6793
  template: `
6663
6794
  <!-- Missing phone guard -->
6664
- @if (!contactPhone()) {
6795
+ @if (!bulk() && !contactPhone()) {
6665
6796
  <div class="sm-missing-banner">
6666
6797
  <svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" aria-hidden="true"><circle cx="12" cy="12" r="10"/><line x1="12" y1="8" x2="12" y2="12"/><line x1="12" y1="16" x2="12.01" y2="16"/></svg>
6667
6798
  No phone number on file for this contact.
@@ -6678,7 +6809,8 @@ var SendTextComponent = class SendTextComponent {
6678
6809
  Save &amp; Continue
6679
6810
  </ui-button>
6680
6811
  } @else {
6681
- <!-- To -->
6812
+ <!-- To (bulk mode: the panel shows the recipient summary instead) -->
6813
+ @if (!bulk()) {
6682
6814
  <div class="sm-group">
6683
6815
  <span class="sm-group-label">To</span>
6684
6816
  <div class="sm-to-card">
@@ -6686,6 +6818,7 @@ var SendTextComponent = class SendTextComponent {
6686
6818
  <span class="sm-to-detail">{{ formatPhone(contactPhone()) }}</span>
6687
6819
  </div>
6688
6820
  </div>
6821
+ }
6689
6822
 
6690
6823
  <!-- Mode (hidden with [showTemplates]="false": the form is Create Message only) -->
6691
6824
  @if (showTemplates()) {
@@ -6869,7 +7002,7 @@ var SendTextComponent = class SendTextComponent {
6869
7002
  <div class="sp-text-meta">
6870
7003
  <div class="sp-meta-row">
6871
7004
  <span class="sp-meta-label">To</span>
6872
- <span class="sp-meta-value">{{ contactName() }} {{ formatPhone(contactPhone()) }}</span>
7005
+ <span class="sp-meta-value">@if (bulk()) { {{ bulkRecipients()!.length }} recipient{{ bulkRecipients()!.length === 1 ? '' : 's' }} } @else { {{ contactName() }} {{ formatPhone(contactPhone()) }} }</span>
6873
7006
  </div>
6874
7007
  </div>
6875
7008
  <div class="sp-text-body">{{ resolvedBody }}</div>
@@ -7043,6 +7176,7 @@ var SendTextComponent = class SendTextComponent {
7043
7176
  "label",
7044
7177
  "ariaLabel",
7045
7178
  "inputTestId",
7179
+ "inputName",
7046
7180
  "placeholder",
7047
7181
  "prefix",
7048
7182
  "revealable",
@@ -7215,7 +7349,7 @@ i0.ɵɵngDeclareClassMetadata({
7215
7349
  ],
7216
7350
  template: `
7217
7351
  <!-- Missing phone guard -->
7218
- @if (!contactPhone()) {
7352
+ @if (!bulk() && !contactPhone()) {
7219
7353
  <div class="sm-missing-banner">
7220
7354
  <svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" aria-hidden="true"><circle cx="12" cy="12" r="10"/><line x1="12" y1="8" x2="12" y2="12"/><line x1="12" y1="16" x2="12.01" y2="16"/></svg>
7221
7355
  No phone number on file for this contact.
@@ -7232,7 +7366,8 @@ i0.ɵɵngDeclareClassMetadata({
7232
7366
  Save &amp; Continue
7233
7367
  </ui-button>
7234
7368
  } @else {
7235
- <!-- To -->
7369
+ <!-- To (bulk mode: the panel shows the recipient summary instead) -->
7370
+ @if (!bulk()) {
7236
7371
  <div class="sm-group">
7237
7372
  <span class="sm-group-label">To</span>
7238
7373
  <div class="sm-to-card">
@@ -7240,6 +7375,7 @@ i0.ɵɵngDeclareClassMetadata({
7240
7375
  <span class="sm-to-detail">{{ formatPhone(contactPhone()) }}</span>
7241
7376
  </div>
7242
7377
  </div>
7378
+ }
7243
7379
 
7244
7380
  <!-- Mode (hidden with [showTemplates]="false": the form is Create Message only) -->
7245
7381
  @if (showTemplates()) {
@@ -7423,7 +7559,7 @@ i0.ɵɵngDeclareClassMetadata({
7423
7559
  <div class="sp-text-meta">
7424
7560
  <div class="sp-meta-row">
7425
7561
  <span class="sp-meta-label">To</span>
7426
- <span class="sp-meta-value">{{ contactName() }} {{ formatPhone(contactPhone()) }}</span>
7562
+ <span class="sp-meta-value">@if (bulk()) { {{ bulkRecipients()!.length }} recipient{{ bulkRecipients()!.length === 1 ? '' : 's' }} } @else { {{ contactName() }} {{ formatPhone(contactPhone()) }} }</span>
7427
7563
  </div>
7428
7564
  </div>
7429
7565
  <div class="sp-text-body">{{ resolvedBody }}</div>
@@ -7486,6 +7622,14 @@ i0.ɵɵngDeclareClassMetadata({
7486
7622
  required: false
7487
7623
  }]
7488
7624
  }],
7625
+ bulkRecipients: [{
7626
+ type: i0.Input,
7627
+ args: [{
7628
+ isSignal: true,
7629
+ alias: "bulkRecipients",
7630
+ required: false
7631
+ }]
7632
+ }],
7489
7633
  defaultBody: [{
7490
7634
  type: i0.Input,
7491
7635
  args: [{
@@ -7634,6 +7778,14 @@ i0.ɵɵngDeclareClassMetadata({
7634
7778
  type: i0.Output,
7635
7779
  args: ["sent"]
7636
7780
  }],
7781
+ bulkResult: [{
7782
+ type: i0.Output,
7783
+ args: ["bulkResult"]
7784
+ }],
7785
+ bulkSending: [{
7786
+ type: i0.Output,
7787
+ args: ["bulkSending"]
7788
+ }],
7637
7789
  phoneAdded: [{
7638
7790
  type: i0.Output,
7639
7791
  args: ["phoneAdded"]
@@ -7648,7 +7800,46 @@ i0.ɵɵngDeclareClassMetadata({
7648
7800
  }]
7649
7801
  }
7650
7802
  });
7803
+ const BULK_MERGE_TAG_HINT = "Personal details such as {{first_name}} are filled in for each recipient when sent.";
7804
+ const SKIPPED_NAMES_SHOWN = 5;
7651
7805
  var SendMessagePanelComponent = class SendMessagePanelComponent {
7806
+ skippedReason() {
7807
+ return this.channel() === "email" ? "no email address" : "no phone number or no opt-in to texts";
7808
+ }
7809
+ skippedNames() {
7810
+ const names = this.skippedRecipients().map((r) => r.name || "Unnamed");
7811
+ const shown = names.slice(0, SKIPPED_NAMES_SHOWN).join(", ");
7812
+ return names.length > SKIPPED_NAMES_SHOWN ? `${shown} and ${names.length - SKIPPED_NAMES_SHOWN} more` : shown;
7813
+ }
7814
+ bulkRecipientsFor(ch) {
7815
+ if (!this.bulkMode()) return null;
7816
+ return ch === "email" ? this.emailRecipients() : this.textRecipients();
7817
+ }
7818
+ onBulkSending(sent) {
7819
+ if (sent) {
7820
+ this.submitSnapshot = {
7821
+ channel: this.channel(),
7822
+ selected: [...this.bulkList()],
7823
+ sent: [...sent]
7824
+ };
7825
+ this.sending.set(true);
7826
+ } else {
7827
+ this.submitSnapshot = null;
7828
+ this.sending.set(false);
7829
+ }
7830
+ }
7831
+ resetBulkSend() {
7832
+ this.submitSnapshot = null;
7833
+ this.lastBulkResult = void 0;
7834
+ this.sending.set(false);
7835
+ }
7836
+ onBulkResult(e) {
7837
+ this.lastBulkResult = e.result;
7838
+ if (this.submitSnapshot) this.submitSnapshot = {
7839
+ ...this.submitSnapshot,
7840
+ sent: [...e.recipients]
7841
+ };
7842
+ }
7652
7843
  onJobPick(o) {
7653
7844
  const job = o ? {
7654
7845
  id: o.value,
@@ -7669,6 +7860,10 @@ var SendMessagePanelComponent = class SendMessagePanelComponent {
7669
7860
  this.open = input(false, ...ngDevMode ? [{ debugName: "open" }] : /* istanbul ignore next */ []);
7670
7861
  this.contact = input(null, ...ngDevMode ? [{ debugName: "contact" }] : /* istanbul ignore next */ []);
7671
7862
  this.loading = input(false, ...ngDevMode ? [{ debugName: "loading" }] : /* istanbul ignore next */ []);
7863
+ this.recipients = input(null, ...ngDevMode ? [{ debugName: "recipients" }] : /* istanbul ignore next */ []);
7864
+ this.initialChannel = input(null, ...ngDevMode ? [{ debugName: "initialChannel" }] : /* istanbul ignore next */ []);
7865
+ this.recipientNoun = input("recipients", ...ngDevMode ? [{ debugName: "recipientNoun" }] : /* istanbul ignore next */ []);
7866
+ this.recipientNounSingular = input("", ...ngDevMode ? [{ debugName: "recipientNounSingular" }] : /* istanbul ignore next */ []);
7672
7867
  this.textEnabled = input(false, ...ngDevMode ? [{ debugName: "textEnabled" }] : /* istanbul ignore next */ []);
7673
7868
  this.title = input("Send Message", ...ngDevMode ? [{ debugName: "title" }] : /* istanbul ignore next */ []);
7674
7869
  this.defaultSubject = input("", ...ngDevMode ? [{ debugName: "defaultSubject" }] : /* istanbul ignore next */ []);
@@ -7691,7 +7886,9 @@ var SendMessagePanelComponent = class SendMessagePanelComponent {
7691
7886
  this.showSubject = input(true, ...ngDevMode ? [{ debugName: "showSubject" }] : /* istanbul ignore next */ []);
7692
7887
  this.showTemplates = input(true, ...ngDevMode ? [{ debugName: "showTemplates" }] : /* istanbul ignore next */ []);
7693
7888
  this.showVideo = input(true, ...ngDevMode ? [{ debugName: "showVideo" }] : /* istanbul ignore next */ []);
7694
- this.inApp = computed(() => this.recipientMode() === "in-app", ...ngDevMode ? [{ debugName: "inApp" }] : /* istanbul ignore next */ []);
7889
+ this.inApp = computed(() => this.recipientMode() === "in-app" && !this.bulkMode(), ...ngDevMode ? [{ debugName: "inApp" }] : /* istanbul ignore next */ []);
7890
+ this.BULK_NO_SEND_MESSAGE = BULK_NO_SEND_MESSAGE;
7891
+ this.BULK_MERGE_TAG_HINT = BULK_MERGE_TAG_HINT;
7695
7892
  this.attachmentsAndScheduling = input(true, ...ngDevMode ? [{ debugName: "attachmentsAndScheduling" }] : /* istanbul ignore next */ []);
7696
7893
  this.documentPurposes = input([
7697
7894
  "ESIGN",
@@ -7712,7 +7909,50 @@ var SendMessagePanelComponent = class SendMessagePanelComponent {
7712
7909
  this.docuSignConnected = input(false, ...ngDevMode ? [{ debugName: "docuSignConnected" }] : /* istanbul ignore next */ []);
7713
7910
  this.documentPreview = input(null, ...ngDevMode ? [{ debugName: "documentPreview" }] : /* istanbul ignore next */ []);
7714
7911
  this.videoDefaults = input(null, ...ngDevMode ? [{ debugName: "videoDefaults" }] : /* istanbul ignore next */ []);
7715
- this.documentsOn = computed(() => (this.documents() || !!this.documentEnvelope()) && this.documentsEnabled(), ...ngDevMode ? [{ debugName: "documentsOn" }] : /* istanbul ignore next */ []);
7912
+ this.documentsOn = computed(() => (this.documents() || !!this.documentEnvelope()) && this.documentsEnabled() && !this.bulkMode(), ...ngDevMode ? [{ debugName: "documentsOn" }] : /* istanbul ignore next */ []);
7913
+ this.bulkList = computed(() => {
7914
+ const seen = /* @__PURE__ */ new Set();
7915
+ return (this.recipients() ?? []).filter((r) => !seen.has(r.id) && !!seen.add(r.id));
7916
+ }, ...ngDevMode ? [{ debugName: "bulkList" }] : /* istanbul ignore next */ []);
7917
+ this.bulkMode = computed(() => this.bulkList().length > 0, ...ngDevMode ? [{ debugName: "bulkMode" }] : /* istanbul ignore next */ []);
7918
+ this.emailRecipients = computed(() => this.bulkList().filter((r) => !!r.email?.trim()), ...ngDevMode ? [{ debugName: "emailRecipients" }] : /* istanbul ignore next */ []);
7919
+ this.textRecipients = computed(() => this.textEnabled() ? this.bulkList().filter((r) => !!r.phone?.trim() && r.smsOptedIn === true) : [], ...ngDevMode ? [{ debugName: "textRecipients" }] : /* istanbul ignore next */ []);
7920
+ this.activeRecipients = computed(() => this.channel() === "email" ? this.emailRecipients() : this.textRecipients(), ...ngDevMode ? [{ debugName: "activeRecipients" }] : /* istanbul ignore next */ []);
7921
+ this.skippedRecipients = computed(() => {
7922
+ const ok = new Set(this.activeRecipients());
7923
+ return this.bulkList().filter((r) => !ok.has(r));
7924
+ }, ...ngDevMode ? [{ debugName: "skippedRecipients" }] : /* istanbul ignore next */ []);
7925
+ this.bulkNoneEligible = computed(() => this.bulkMode() && this.activeRecipients().length === 0, ...ngDevMode ? [{ debugName: "bulkNoneEligible" }] : /* istanbul ignore next */ []);
7926
+ this.bulkMissingSend = computed(() => this.bulkMode() && !(this.channel() === "email" ? this.sendEmail() : this.sendText()), ...ngDevMode ? [{ debugName: "bulkMissingSend" }] : /* istanbul ignore next */ []);
7927
+ this.bulkBlocked = computed(() => this.bulkMode() && (this.bulkNoneEligible() || this.bulkMissingSend()), ...ngDevMode ? [{ debugName: "bulkBlocked" }] : /* istanbul ignore next */ []);
7928
+ this.hasTarget = computed(() => this.bulkMode() || !!this.activeContact(), ...ngDevMode ? [{ debugName: "hasTarget" }] : /* istanbul ignore next */ []);
7929
+ this.sending = signal(false, ...ngDevMode ? [{ debugName: "sending" }] : /* istanbul ignore next */ []);
7930
+ this.submitSnapshot = null;
7931
+ this.recipientWord = computed(() => {
7932
+ const n = this.bulkList().length;
7933
+ const plural = this.recipientNoun() || "recipients";
7934
+ if (n !== 1) return plural;
7935
+ return this.recipientNounSingular() || (plural.endsWith("s") ? plural.slice(0, -1) : plural);
7936
+ }, ...ngDevMode ? [{ debugName: "recipientWord" }] : /* istanbul ignore next */ []);
7937
+ this.skippedText = computed(() => {
7938
+ const n = this.skippedRecipients().length;
7939
+ if (n === 0) return "";
7940
+ const what = this.channel() === "email" ? "email" : "text";
7941
+ return `${n} ${n === 1 ? "has" : "have"} ${this.skippedReason()} and will be skipped (${this.skippedNames()}). Only the others get this ${what}.`;
7942
+ }, ...ngDevMode ? [{ debugName: "skippedText" }] : /* istanbul ignore next */ []);
7943
+ this.noneEligibleText = computed(() => {
7944
+ return `None of the ${this.bulkList().length} selected can receive this ${this.channel() === "email" ? "email" : "text message"} (${this.skippedReason()}), so there is nothing to send.`;
7945
+ }, ...ngDevMode ? [{ debugName: "noneEligibleText" }] : /* istanbul ignore next */ []);
7946
+ this.bulkAnnouncement = computed(() => {
7947
+ if (!this.bulkMode()) return "";
7948
+ const n = this.bulkList().length;
7949
+ const ok = this.activeRecipients().length;
7950
+ const ch = this.channel() === "email" ? "email" : "text";
7951
+ if (ok === 0) return `${n} ${this.recipientWord()} selected. None can receive the ${ch}.`;
7952
+ const skipped = this.skippedRecipients().length;
7953
+ return `${n} ${this.recipientWord()} selected. ${ok} can receive the ${ch}.${skipped ? ` ${skipped} will be skipped.` : ""}`;
7954
+ }, ...ngDevMode ? [{ debugName: "bulkAnnouncement" }] : /* istanbul ignore next */ []);
7955
+ this.lastBulkResult = void 0;
7716
7956
  this.showJobPicker = input(false, ...ngDevMode ? [{ debugName: "showJobPicker" }] : /* istanbul ignore next */ []);
7717
7957
  this.jobLabel = input("Job", ...ngDevMode ? [{ debugName: "jobLabel" }] : /* istanbul ignore next */ []);
7718
7958
  this.jobHint = input("", ...ngDevMode ? [{ debugName: "jobHint" }] : /* istanbul ignore next */ []);
@@ -7720,6 +7960,7 @@ var SendMessagePanelComponent = class SendMessagePanelComponent {
7720
7960
  this.jobSelected = output();
7721
7961
  this.closed = output();
7722
7962
  this.sent = output();
7963
+ this.sentBulk = output();
7723
7964
  this.emailAdded = output();
7724
7965
  this.rejected = output();
7725
7966
  this.mapFieldsClick = output();
@@ -7777,6 +8018,31 @@ var SendMessagePanelComponent = class SendMessagePanelComponent {
7777
8018
  const loadingVal = this.loading();
7778
8019
  const openedNow = openVal && !this._prevOpen;
7779
8020
  this._prevOpen = openVal;
8021
+ if (this.bulkMode()) {
8022
+ const emailCount = this.emailRecipients().length;
8023
+ const textCount = this.textRecipients().length;
8024
+ untracked(() => {
8025
+ const wasSingle = !!this.activeContact();
8026
+ const ch = this.channel();
8027
+ const activeCount = ch === "email" ? emailCount : textCount;
8028
+ const otherCount = ch === "email" ? textCount : emailCount;
8029
+ const pref = this.initialChannel();
8030
+ const defaultCh = pref === "text" && textCount > 0 ? "text" : pref === "email" && emailCount > 0 ? "email" : emailCount > 0 || textCount === 0 ? "email" : "text";
8031
+ if (openedNow || wasSingle) {
8032
+ this.activeContact.set(null);
8033
+ this.channel.set(defaultCh);
8034
+ } else if (openVal && activeCount === 0 && otherCount > 0 && !this.sending()) this.channel.set(ch === "email" ? "text" : "email");
8035
+ else return;
8036
+ this.resetBulkSend();
8037
+ this.valid.set(false);
8038
+ this.resetJob();
8039
+ this.formEpoch.update((n) => n + 1);
8040
+ });
8041
+ return;
8042
+ }
8043
+ untracked(() => {
8044
+ if (openedNow || this.sending() || this.submitSnapshot) this.resetBulkSend();
8045
+ });
7780
8046
  if (!openedNow && !(openVal && !loadingVal && !!contactVal)) return;
7781
8047
  const c = contactVal ?? null;
7782
8048
  const active = untracked(() => this.activeContact());
@@ -7800,6 +8066,7 @@ var SendMessagePanelComponent = class SendMessagePanelComponent {
7800
8066
  this.formEpoch.update((n) => n + 1);
7801
8067
  }
7802
8068
  onChannelChange(ch) {
8069
+ if (this.sending()) return;
7803
8070
  this.channel.set(ch);
7804
8071
  this.valid.set(false);
7805
8072
  }
@@ -7820,12 +8087,29 @@ var SendMessagePanelComponent = class SendMessagePanelComponent {
7820
8087
  this.phoneAdded.emit(phone);
7821
8088
  }
7822
8089
  submit() {
8090
+ if (this.bulkBlocked()) return;
7823
8091
  if (this.inApp()) this.sendInAppRef()?.submit();
7824
8092
  else if (this.channel() === "email") this.sendEmailRef()?.submit();
7825
8093
  else this.sendTextRef()?.submit();
7826
8094
  }
7827
8095
  onSent() {
8096
+ const snap = this.submitSnapshot;
8097
+ const bulk = !!snap || this.bulkMode();
8098
+ const channel = snap?.channel ?? this.channel();
8099
+ const selected = snap?.selected ?? [...this.bulkList()];
8100
+ const sentList = snap?.sent ?? [...this.activeRecipients()];
8101
+ const sentIds = new Set(sentList.map((r) => r.id));
8102
+ const event = bulk ? {
8103
+ channel,
8104
+ recipients: sentList,
8105
+ skipped: selected.filter((r) => !sentIds.has(r.id)),
8106
+ result: this.lastBulkResult
8107
+ } : null;
8108
+ this.lastBulkResult = void 0;
8109
+ this.submitSnapshot = null;
8110
+ this.sending.set(false);
7828
8111
  this.sent.emit();
8112
+ if (event) this.sentBulk.emit(event);
7829
8113
  this.closed.emit();
7830
8114
  }
7831
8115
  onClose() {
@@ -7870,6 +8154,34 @@ var SendMessagePanelComponent = class SendMessagePanelComponent {
7870
8154
  isRequired: false,
7871
8155
  transformFunction: null
7872
8156
  },
8157
+ recipients: {
8158
+ classPropertyName: "recipients",
8159
+ publicName: "recipients",
8160
+ isSignal: true,
8161
+ isRequired: false,
8162
+ transformFunction: null
8163
+ },
8164
+ initialChannel: {
8165
+ classPropertyName: "initialChannel",
8166
+ publicName: "initialChannel",
8167
+ isSignal: true,
8168
+ isRequired: false,
8169
+ transformFunction: null
8170
+ },
8171
+ recipientNoun: {
8172
+ classPropertyName: "recipientNoun",
8173
+ publicName: "recipientNoun",
8174
+ isSignal: true,
8175
+ isRequired: false,
8176
+ transformFunction: null
8177
+ },
8178
+ recipientNounSingular: {
8179
+ classPropertyName: "recipientNounSingular",
8180
+ publicName: "recipientNounSingular",
8181
+ isSignal: true,
8182
+ isRequired: false,
8183
+ transformFunction: null
8184
+ },
7873
8185
  textEnabled: {
7874
8186
  classPropertyName: "textEnabled",
7875
8187
  publicName: "textEnabled",
@@ -8169,6 +8481,7 @@ var SendMessagePanelComponent = class SendMessagePanelComponent {
8169
8481
  jobSelected: "jobSelected",
8170
8482
  closed: "closed",
8171
8483
  sent: "sent",
8484
+ sentBulk: "sentBulk",
8172
8485
  emailAdded: "emailAdded",
8173
8486
  rejected: "rejected",
8174
8487
  mapFieldsClick: "mapFieldsClick",
@@ -8204,9 +8517,9 @@ var SendMessagePanelComponent = class SendMessagePanelComponent {
8204
8517
  [title]="title()"
8205
8518
  [wide]="wide()"
8206
8519
  cancelLabel="Cancel"
8207
- [primaryLabel]="!loading() && activeContact() ? (sendLabel() || (inApp() ? 'Send Message' : channel() === 'email' ? 'Send Email' : 'Send Text')) : ''"
8520
+ [primaryLabel]="!loading() && hasTarget() ? (sendLabel() || (inApp() ? 'Send Message' : channel() === 'email' ? 'Send Email' : 'Send Text')) : ''"
8208
8521
  primaryVariant="warning"
8209
- [primaryDisabled]="!valid()"
8522
+ [primaryDisabled]="!valid() || bulkBlocked()"
8210
8523
  (closed)="onClose()"
8211
8524
  (primaryClick)="submit()">
8212
8525
 
@@ -8218,7 +8531,7 @@ var SendMessagePanelComponent = class SendMessagePanelComponent {
8218
8531
  </div>
8219
8532
  }
8220
8533
 
8221
- @if (!loading() && !activeContact() && !inApp()) {
8534
+ @if (!loading() && !hasTarget() && !inApp()) {
8222
8535
  <div class="msg-step">
8223
8536
  <span class="msg-step-badge">1</span>
8224
8537
  <span class="msg-step-text">Who would you like to message?</span>
@@ -8228,8 +8541,60 @@ var SendMessagePanelComponent = class SendMessagePanelComponent {
8228
8541
  (contactSelected)="onContactSelected($event)" />
8229
8542
  }
8230
8543
 
8231
- @if (!loading() && activeContact()) {
8232
- @if (!inApp() && textEnabled() && activeContact()?.smsOptedIn) {
8544
+ @if (!loading() && hasTarget()) {
8545
+ @if (bulkMode()) {
8546
+ <div class="msg-bulk" role="group" aria-label="Recipients" data-testid="bulk-summary">
8547
+ <p class="msg-bulk-to" data-testid="bulk-to">To: {{ bulkList().length }} {{ recipientWord() }}</p>
8548
+ <div class="msg-bulk-counts">
8549
+ <ui-badge [label]="emailRecipients().length + ' can receive email'" [variant]="emailRecipients().length ? 'blue' : 'gray'" />
8550
+ @if (textEnabled()) {
8551
+ <ui-badge [label]="textRecipients().length + ' can receive text'" [variant]="textRecipients().length ? 'blue' : 'gray'" />
8552
+ }
8553
+ </div>
8554
+ @if (!textEnabled()) {
8555
+ <ui-hint>Text messaging is not enabled. Connect Twilio in Comm Settings to send texts.</ui-hint>
8556
+ }
8557
+ <span class="msg-sr" role="status" data-testid="bulk-live">{{ bulkAnnouncement() }}</span>
8558
+ @if (skippedRecipients().length > 0 && !bulkNoneEligible()) {
8559
+ <ui-alert variant="warning" [quiet]="true" data-testid="bulk-skipped">{{ skippedText() }}</ui-alert>
8560
+ }
8561
+ @if (bulkNoneEligible()) {
8562
+ <ui-alert variant="warning" [quiet]="true" data-testid="bulk-none-eligible">{{ noneEligibleText() }}</ui-alert>
8563
+ }
8564
+ @if (bulkMissingSend()) {
8565
+ <ui-alert variant="danger" data-testid="bulk-no-send">{{ BULK_NO_SEND_MESSAGE }}</ui-alert>
8566
+ }
8567
+ @if (!bulkNoneEligible()) {
8568
+ <ui-hint data-testid="bulk-merge-hint">{{ BULK_MERGE_TAG_HINT }}</ui-hint>
8569
+ }
8570
+ </div>
8571
+ }
8572
+
8573
+ @if (bulkMode() && textEnabled() && emailRecipients().length > 0 && textRecipients().length > 0) {
8574
+ <div class="msg-channel-row" data-testid="bulk-channel-row">
8575
+ <label class="form-label msg-via-label">Send via</label>
8576
+ @if (sending()) {
8577
+ <!-- A send is in flight: the toggle is replaced by plain text (ui-toggle-badge has no disabled state), so nothing pressable remains. -->
8578
+ <div class="msg-via-tabs">
8579
+ <ui-badge [label]="channel() === 'email' ? 'Email' : 'Text'" variant="blue" />
8580
+ <span class="msg-locked-note" role="status" data-testid="bulk-channel-locked">Channel locked while sending.</span>
8581
+ </div>
8582
+ } @else {
8583
+ <div class="msg-via-tabs">
8584
+ <ui-toggle-badge [active]="channel() === 'email'" (toggle)="onChannelChange('email')">
8585
+ <svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" aria-hidden="true"><rect x="2" y="4" width="20" height="16" rx="2"/><path d="m22 7-8.97 5.7a1.94 1.94 0 0 1-2.06 0L2 7"/></svg>
8586
+ Email
8587
+ </ui-toggle-badge>
8588
+ <ui-toggle-badge [active]="channel() === 'text'" (toggle)="onChannelChange('text')">
8589
+ <svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" aria-hidden="true"><path d="M21 15a2 2 0 0 1-2 2H7l-4 4V5a2 2 0 0 1 2-2h14a2 2 0 0 1 2 2z"/></svg>
8590
+ Text
8591
+ </ui-toggle-badge>
8592
+ </div>
8593
+ }
8594
+ </div>
8595
+ }
8596
+
8597
+ @if (!bulkMode() && !inApp() && textEnabled() && activeContact()?.smsOptedIn) {
8233
8598
  <div class="msg-channel-row">
8234
8599
  <label class="form-label msg-via-label">Send via</label>
8235
8600
  <div class="msg-via-tabs">
@@ -8253,7 +8618,7 @@ var SendMessagePanelComponent = class SendMessagePanelComponent {
8253
8618
  }
8254
8619
 
8255
8620
  <!-- Optional job link (hidden unless the host turns it on). -->
8256
- @if (showJobPicker()) {
8621
+ @if (showJobPicker() && !bulkMode()) {
8257
8622
  @for (epoch of [formEpoch()]; track epoch) {
8258
8623
  <div class="msg-job">
8259
8624
  <ui-combobox
@@ -8266,13 +8631,14 @@ var SendMessagePanelComponent = class SendMessagePanelComponent {
8266
8631
  }
8267
8632
 
8268
8633
  <!-- Keyed on formEpoch: a fresh open or another contact gets brand-new forms (no stale draft / template). -->
8634
+ <div [hidden]="bulkNoneEligible()" data-testid="msg-forms">
8269
8635
  @for (epoch of [formEpoch()]; track epoch) {
8270
8636
  @if (inApp()) {
8271
8637
  <ui-send-in-app
8272
8638
  #sendInAppRef
8273
- [contactName]="activeContact()!.name"
8274
- [contactId]="activeContact()!.id"
8275
- [contactEmail]="activeContact()!.email ?? ''"
8639
+ [contactName]="(activeContact()?.name ?? '')"
8640
+ [contactId]="(activeContact()?.id ?? '')"
8641
+ [contactEmail]="activeContact()?.email ?? ''"
8276
8642
  [allowAttachments]="inAppAttachments()"
8277
8643
  [richText]="inAppRichText()"
8278
8644
  [defaultSubject]="defaultSubject()"
@@ -8285,10 +8651,11 @@ var SendMessagePanelComponent = class SendMessagePanelComponent {
8285
8651
  } @else if (channel() === 'email') {
8286
8652
  <ui-send-email
8287
8653
  #sendEmailRef
8288
- [contactName]="activeContact()!.name"
8289
- [contactEmail]="activeContact()!.email ?? ''"
8290
- [contactPhone]="activeContact()!.phone ?? ''"
8291
- [contactId]="activeContact()!.id"
8654
+ [contactName]="(activeContact()?.name ?? '')"
8655
+ [contactEmail]="activeContact()?.email ?? ''"
8656
+ [contactPhone]="activeContact()?.phone ?? ''"
8657
+ [contactId]="(activeContact()?.id ?? '')"
8658
+ [bulkRecipients]="bulkRecipientsFor('email')"
8292
8659
  [defaultSubject]="defaultSubject()"
8293
8660
  [defaultBody]="defaultBody()"
8294
8661
  [context]="context()"
@@ -8298,13 +8665,13 @@ var SendMessagePanelComponent = class SendMessagePanelComponent {
8298
8665
  [receiptDefault]="receiptDefault()"
8299
8666
  [documents]="documentsOn()"
8300
8667
  [documentPurposes]="documentPurposes()"
8301
- [initialDocuments]="initialDocuments()"
8668
+ [initialDocuments]="bulkMode() ? [] : initialDocuments()"
8302
8669
  [documentSigningOptions]="documentSigningOptions()"
8303
- [documentSigners]="documentSigners()"
8670
+ [documentSigners]="bulkMode() ? null : documentSigners()"
8304
8671
  [sendContext]="sendContext()"
8305
8672
  [documentContextTypes]="documentContextTypes()"
8306
8673
  [documentMergeCheck]="documentMergeCheck()"
8307
- [documentEnvelope]="documentEnvelope()"
8674
+ [documentEnvelope]="bulkMode() ? null : documentEnvelope()"
8308
8675
  [emailEnabled]="emailEnabled()"
8309
8676
  [documentSentMessage]="documentSentMessage()"
8310
8677
  (rejected)="rejected.emit()"
@@ -8317,46 +8684,52 @@ var SendMessagePanelComponent = class SendMessagePanelComponent {
8317
8684
  [showSubject]="showSubject()"
8318
8685
  [showTemplates]="showTemplates()"
8319
8686
  [showVideo]="showVideo()"
8687
+ (bulkSending)="onBulkSending($event)"
8688
+ (bulkResult)="onBulkResult($event)"
8320
8689
  (sent)="onSent()"
8321
8690
  (emailAdded)="onEmailAdded($event)"
8322
8691
  (validChange)="valid.set($event)" />
8323
8692
  } @else {
8324
8693
  <ui-send-text
8325
8694
  #sendTextRef
8326
- [contactName]="activeContact()!.name"
8327
- [contactPhone]="activeContact()!.phone ?? ''"
8328
- [contactEmail]="activeContact()!.email ?? ''"
8329
- [contactId]="activeContact()!.id"
8695
+ [contactName]="(activeContact()?.name ?? '')"
8696
+ [contactPhone]="activeContact()?.phone ?? ''"
8697
+ [contactEmail]="activeContact()?.email ?? ''"
8698
+ [contactId]="(activeContact()?.id ?? '')"
8699
+ [bulkRecipients]="bulkRecipientsFor('text')"
8330
8700
  [defaultBody]="defaultMessage()"
8331
8701
  [context]="context()"
8332
8702
  [sendFn]="sendText()"
8333
8703
  [attachmentsAndScheduling]="attachmentsAndScheduling()"
8334
8704
  [documents]="documentsOn()"
8335
8705
  [documentPurposes]="documentPurposes()"
8336
- [initialDocuments]="initialDocuments()"
8706
+ [initialDocuments]="bulkMode() ? [] : initialDocuments()"
8337
8707
  [documentSigningOptions]="documentSigningOptions()"
8338
- [documentSigners]="documentSigners()"
8708
+ [documentSigners]="bulkMode() ? null : documentSigners()"
8339
8709
  [sendContext]="sendContext()"
8340
8710
  [documentContextTypes]="documentContextTypes()"
8341
8711
  [documentMergeCheck]="documentMergeCheck()"
8342
- [documentEnvelope]="documentEnvelope()"
8712
+ [documentEnvelope]="bulkMode() ? null : documentEnvelope()"
8343
8713
  [documentSentMessage]="documentSentMessage()"
8344
8714
  (rejected)="rejected.emit()"
8345
8715
  [documentPreview]="documentPreview()"
8346
8716
  [videoDefaults]="videoDefaults()"
8347
8717
  [showTemplates]="showTemplates()"
8348
8718
  [showVideo]="showVideo()"
8719
+ (bulkSending)="onBulkSending($event)"
8720
+ (bulkResult)="onBulkResult($event)"
8349
8721
  (sent)="onSent()"
8350
8722
  (phoneAdded)="onPhoneAdded($event)"
8351
8723
  (validChange)="valid.set($event)" />
8352
8724
  }
8353
8725
  }
8726
+ </div>
8354
8727
  }
8355
8728
 
8356
8729
  </ui-side-panel>
8357
8730
  `,
8358
8731
  isInline: true,
8359
- styles: [".msg-step{display:flex;align-items:center;gap:10px;margin-bottom:14px}.msg-step-badge{display:flex;align-items:center;justify-content:center;width:24px;height:24px;flex-shrink:0;border-radius:50%;background:#1e293b;color:#fff;font-size:.75rem;font-weight:700}.msg-step-text{font-size:.9375rem;font-weight:600;color:#1e293b}.msg-channel-row{display:flex;align-items:center;gap:10px;margin-bottom:16px;flex-wrap:wrap}.msg-sms-notice{display:flex;align-items:center;gap:8px;margin-bottom:16px;padding:10px 12px;border-radius:8px;background:#f8fafc;border:1px solid #e2e8f0;font-size:.8125rem;color:#64748b}.msg-job{margin-bottom:16px}.msg-via-tabs{display:flex;gap:6px;flex-wrap:wrap}.msg-skeleton{display:flex;flex-direction:column;gap:12px;padding:4px 0}.skeleton-line{height:14px;border-radius:6px;background:linear-gradient(90deg,#e2e8f0 25%,#f1f5f9,#e2e8f0 75%);background-size:200% 100%;animation:skeleton-shimmer 1.4s infinite}.skeleton-line--wide{width:100%}.skeleton-line--medium{width:70%}.skeleton-line--narrow{width:45%}@keyframes skeleton-shimmer{0%{background-position:200% 0}to{background-position:-200% 0}}\n"],
8732
+ styles: [".msg-step{display:flex;align-items:center;gap:10px;margin-bottom:14px}.msg-step-badge{display:flex;align-items:center;justify-content:center;width:24px;height:24px;flex-shrink:0;border-radius:50%;background:#1e293b;color:#fff;font-size:.75rem;font-weight:700}.msg-step-text{font-size:.9375rem;font-weight:600;color:#1e293b}.msg-channel-row{display:flex;align-items:center;gap:10px;margin-bottom:16px;flex-wrap:wrap}.msg-sms-notice{display:flex;align-items:center;gap:8px;margin-bottom:16px;padding:10px 12px;border-radius:8px;background:#f8fafc;border:1px solid #e2e8f0;font-size:.8125rem;color:#64748b}.msg-job{margin-bottom:16px}.msg-locked-note{font-size:.75rem;color:#64748b}.msg-bulk{display:flex;flex-direction:column;gap:8px;margin-bottom:16px}.msg-bulk-to{margin:0;font-size:.9375rem;font-weight:600;color:var(--shell-text, #1e293b)}.msg-bulk-counts{display:flex;flex-wrap:wrap;gap:6px}.msg-sr{position:absolute;width:1px;height:1px;margin:-1px;padding:0;overflow:hidden;clip:rect(0,0,0,0);white-space:nowrap;border:0}.msg-via-tabs{display:flex;align-items:center;gap:6px;flex-wrap:wrap}.msg-skeleton{display:flex;flex-direction:column;gap:12px;padding:4px 0}.skeleton-line{height:14px;border-radius:6px;background:linear-gradient(90deg,#e2e8f0 25%,#f1f5f9,#e2e8f0 75%);background-size:200% 100%;animation:skeleton-shimmer 1.4s infinite}.skeleton-line--wide{width:100%}.skeleton-line--medium{width:70%}.skeleton-line--narrow{width:45%}@keyframes skeleton-shimmer{0%{background-position:200% 0}to{background-position:-200% 0}}\n"],
8360
8733
  dependencies: [
8361
8734
  {
8362
8735
  kind: "component",
@@ -8400,6 +8773,7 @@ var SendMessagePanelComponent = class SendMessagePanelComponent {
8400
8773
  "contactEmail",
8401
8774
  "contactPhone",
8402
8775
  "contactId",
8776
+ "bulkRecipients",
8403
8777
  "defaultSubject",
8404
8778
  "defaultBody",
8405
8779
  "context",
@@ -8429,6 +8803,8 @@ var SendMessagePanelComponent = class SendMessagePanelComponent {
8429
8803
  ],
8430
8804
  outputs: [
8431
8805
  "sent",
8806
+ "bulkResult",
8807
+ "bulkSending",
8432
8808
  "emailAdded",
8433
8809
  "validChange",
8434
8810
  "rejected",
@@ -8462,6 +8838,7 @@ var SendMessagePanelComponent = class SendMessagePanelComponent {
8462
8838
  "contactPhone",
8463
8839
  "contactEmail",
8464
8840
  "contactId",
8841
+ "bulkRecipients",
8465
8842
  "defaultBody",
8466
8843
  "context",
8467
8844
  "sendFn",
@@ -8483,6 +8860,8 @@ var SendMessagePanelComponent = class SendMessagePanelComponent {
8483
8860
  ],
8484
8861
  outputs: [
8485
8862
  "sent",
8863
+ "bulkResult",
8864
+ "bulkSending",
8486
8865
  "phoneAdded",
8487
8866
  "validChange",
8488
8867
  "rejected"
@@ -8540,6 +8919,33 @@ var SendMessagePanelComponent = class SendMessagePanelComponent {
8540
8919
  "focused",
8541
8920
  "blurred"
8542
8921
  ]
8922
+ },
8923
+ {
8924
+ kind: "component",
8925
+ type: AlertComponent,
8926
+ selector: "ui-alert",
8927
+ inputs: [
8928
+ "variant",
8929
+ "title",
8930
+ "dismissible",
8931
+ "dismissAriaLabel",
8932
+ "quiet",
8933
+ "compact",
8934
+ "inline",
8935
+ "fill"
8936
+ ],
8937
+ outputs: ["dismissed"]
8938
+ },
8939
+ {
8940
+ kind: "component",
8941
+ type: BadgeComponent,
8942
+ selector: "ui-badge",
8943
+ inputs: ["label", "variant"]
8944
+ },
8945
+ {
8946
+ kind: "component",
8947
+ type: HintComponent,
8948
+ selector: "ui-hint"
8543
8949
  }
8544
8950
  ],
8545
8951
  changeDetection: i0.ChangeDetectionStrategy.OnPush
@@ -8564,7 +8970,10 @@ i0.ɵɵngDeclareClassMetadata({
8564
8970
  SendInAppComponent,
8565
8971
  SendTextComponent,
8566
8972
  ContactPickerComponent,
8567
- ComboboxComponent
8973
+ ComboboxComponent,
8974
+ AlertComponent,
8975
+ BadgeComponent,
8976
+ HintComponent
8568
8977
  ],
8569
8978
  template: `
8570
8979
  <ui-side-panel
@@ -8572,9 +8981,9 @@ i0.ɵɵngDeclareClassMetadata({
8572
8981
  [title]="title()"
8573
8982
  [wide]="wide()"
8574
8983
  cancelLabel="Cancel"
8575
- [primaryLabel]="!loading() && activeContact() ? (sendLabel() || (inApp() ? 'Send Message' : channel() === 'email' ? 'Send Email' : 'Send Text')) : ''"
8984
+ [primaryLabel]="!loading() && hasTarget() ? (sendLabel() || (inApp() ? 'Send Message' : channel() === 'email' ? 'Send Email' : 'Send Text')) : ''"
8576
8985
  primaryVariant="warning"
8577
- [primaryDisabled]="!valid()"
8986
+ [primaryDisabled]="!valid() || bulkBlocked()"
8578
8987
  (closed)="onClose()"
8579
8988
  (primaryClick)="submit()">
8580
8989
 
@@ -8586,7 +8995,7 @@ i0.ɵɵngDeclareClassMetadata({
8586
8995
  </div>
8587
8996
  }
8588
8997
 
8589
- @if (!loading() && !activeContact() && !inApp()) {
8998
+ @if (!loading() && !hasTarget() && !inApp()) {
8590
8999
  <div class="msg-step">
8591
9000
  <span class="msg-step-badge">1</span>
8592
9001
  <span class="msg-step-text">Who would you like to message?</span>
@@ -8596,8 +9005,60 @@ i0.ɵɵngDeclareClassMetadata({
8596
9005
  (contactSelected)="onContactSelected($event)" />
8597
9006
  }
8598
9007
 
8599
- @if (!loading() && activeContact()) {
8600
- @if (!inApp() && textEnabled() && activeContact()?.smsOptedIn) {
9008
+ @if (!loading() && hasTarget()) {
9009
+ @if (bulkMode()) {
9010
+ <div class="msg-bulk" role="group" aria-label="Recipients" data-testid="bulk-summary">
9011
+ <p class="msg-bulk-to" data-testid="bulk-to">To: {{ bulkList().length }} {{ recipientWord() }}</p>
9012
+ <div class="msg-bulk-counts">
9013
+ <ui-badge [label]="emailRecipients().length + ' can receive email'" [variant]="emailRecipients().length ? 'blue' : 'gray'" />
9014
+ @if (textEnabled()) {
9015
+ <ui-badge [label]="textRecipients().length + ' can receive text'" [variant]="textRecipients().length ? 'blue' : 'gray'" />
9016
+ }
9017
+ </div>
9018
+ @if (!textEnabled()) {
9019
+ <ui-hint>Text messaging is not enabled. Connect Twilio in Comm Settings to send texts.</ui-hint>
9020
+ }
9021
+ <span class="msg-sr" role="status" data-testid="bulk-live">{{ bulkAnnouncement() }}</span>
9022
+ @if (skippedRecipients().length > 0 && !bulkNoneEligible()) {
9023
+ <ui-alert variant="warning" [quiet]="true" data-testid="bulk-skipped">{{ skippedText() }}</ui-alert>
9024
+ }
9025
+ @if (bulkNoneEligible()) {
9026
+ <ui-alert variant="warning" [quiet]="true" data-testid="bulk-none-eligible">{{ noneEligibleText() }}</ui-alert>
9027
+ }
9028
+ @if (bulkMissingSend()) {
9029
+ <ui-alert variant="danger" data-testid="bulk-no-send">{{ BULK_NO_SEND_MESSAGE }}</ui-alert>
9030
+ }
9031
+ @if (!bulkNoneEligible()) {
9032
+ <ui-hint data-testid="bulk-merge-hint">{{ BULK_MERGE_TAG_HINT }}</ui-hint>
9033
+ }
9034
+ </div>
9035
+ }
9036
+
9037
+ @if (bulkMode() && textEnabled() && emailRecipients().length > 0 && textRecipients().length > 0) {
9038
+ <div class="msg-channel-row" data-testid="bulk-channel-row">
9039
+ <label class="form-label msg-via-label">Send via</label>
9040
+ @if (sending()) {
9041
+ <!-- A send is in flight: the toggle is replaced by plain text (ui-toggle-badge has no disabled state), so nothing pressable remains. -->
9042
+ <div class="msg-via-tabs">
9043
+ <ui-badge [label]="channel() === 'email' ? 'Email' : 'Text'" variant="blue" />
9044
+ <span class="msg-locked-note" role="status" data-testid="bulk-channel-locked">Channel locked while sending.</span>
9045
+ </div>
9046
+ } @else {
9047
+ <div class="msg-via-tabs">
9048
+ <ui-toggle-badge [active]="channel() === 'email'" (toggle)="onChannelChange('email')">
9049
+ <svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" aria-hidden="true"><rect x="2" y="4" width="20" height="16" rx="2"/><path d="m22 7-8.97 5.7a1.94 1.94 0 0 1-2.06 0L2 7"/></svg>
9050
+ Email
9051
+ </ui-toggle-badge>
9052
+ <ui-toggle-badge [active]="channel() === 'text'" (toggle)="onChannelChange('text')">
9053
+ <svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" aria-hidden="true"><path d="M21 15a2 2 0 0 1-2 2H7l-4 4V5a2 2 0 0 1 2-2h14a2 2 0 0 1 2 2z"/></svg>
9054
+ Text
9055
+ </ui-toggle-badge>
9056
+ </div>
9057
+ }
9058
+ </div>
9059
+ }
9060
+
9061
+ @if (!bulkMode() && !inApp() && textEnabled() && activeContact()?.smsOptedIn) {
8601
9062
  <div class="msg-channel-row">
8602
9063
  <label class="form-label msg-via-label">Send via</label>
8603
9064
  <div class="msg-via-tabs">
@@ -8621,7 +9082,7 @@ i0.ɵɵngDeclareClassMetadata({
8621
9082
  }
8622
9083
 
8623
9084
  <!-- Optional job link (hidden unless the host turns it on). -->
8624
- @if (showJobPicker()) {
9085
+ @if (showJobPicker() && !bulkMode()) {
8625
9086
  @for (epoch of [formEpoch()]; track epoch) {
8626
9087
  <div class="msg-job">
8627
9088
  <ui-combobox
@@ -8634,13 +9095,14 @@ i0.ɵɵngDeclareClassMetadata({
8634
9095
  }
8635
9096
 
8636
9097
  <!-- Keyed on formEpoch: a fresh open or another contact gets brand-new forms (no stale draft / template). -->
9098
+ <div [hidden]="bulkNoneEligible()" data-testid="msg-forms">
8637
9099
  @for (epoch of [formEpoch()]; track epoch) {
8638
9100
  @if (inApp()) {
8639
9101
  <ui-send-in-app
8640
9102
  #sendInAppRef
8641
- [contactName]="activeContact()!.name"
8642
- [contactId]="activeContact()!.id"
8643
- [contactEmail]="activeContact()!.email ?? ''"
9103
+ [contactName]="(activeContact()?.name ?? '')"
9104
+ [contactId]="(activeContact()?.id ?? '')"
9105
+ [contactEmail]="activeContact()?.email ?? ''"
8644
9106
  [allowAttachments]="inAppAttachments()"
8645
9107
  [richText]="inAppRichText()"
8646
9108
  [defaultSubject]="defaultSubject()"
@@ -8653,10 +9115,11 @@ i0.ɵɵngDeclareClassMetadata({
8653
9115
  } @else if (channel() === 'email') {
8654
9116
  <ui-send-email
8655
9117
  #sendEmailRef
8656
- [contactName]="activeContact()!.name"
8657
- [contactEmail]="activeContact()!.email ?? ''"
8658
- [contactPhone]="activeContact()!.phone ?? ''"
8659
- [contactId]="activeContact()!.id"
9118
+ [contactName]="(activeContact()?.name ?? '')"
9119
+ [contactEmail]="activeContact()?.email ?? ''"
9120
+ [contactPhone]="activeContact()?.phone ?? ''"
9121
+ [contactId]="(activeContact()?.id ?? '')"
9122
+ [bulkRecipients]="bulkRecipientsFor('email')"
8660
9123
  [defaultSubject]="defaultSubject()"
8661
9124
  [defaultBody]="defaultBody()"
8662
9125
  [context]="context()"
@@ -8666,13 +9129,13 @@ i0.ɵɵngDeclareClassMetadata({
8666
9129
  [receiptDefault]="receiptDefault()"
8667
9130
  [documents]="documentsOn()"
8668
9131
  [documentPurposes]="documentPurposes()"
8669
- [initialDocuments]="initialDocuments()"
9132
+ [initialDocuments]="bulkMode() ? [] : initialDocuments()"
8670
9133
  [documentSigningOptions]="documentSigningOptions()"
8671
- [documentSigners]="documentSigners()"
9134
+ [documentSigners]="bulkMode() ? null : documentSigners()"
8672
9135
  [sendContext]="sendContext()"
8673
9136
  [documentContextTypes]="documentContextTypes()"
8674
9137
  [documentMergeCheck]="documentMergeCheck()"
8675
- [documentEnvelope]="documentEnvelope()"
9138
+ [documentEnvelope]="bulkMode() ? null : documentEnvelope()"
8676
9139
  [emailEnabled]="emailEnabled()"
8677
9140
  [documentSentMessage]="documentSentMessage()"
8678
9141
  (rejected)="rejected.emit()"
@@ -8685,45 +9148,51 @@ i0.ɵɵngDeclareClassMetadata({
8685
9148
  [showSubject]="showSubject()"
8686
9149
  [showTemplates]="showTemplates()"
8687
9150
  [showVideo]="showVideo()"
9151
+ (bulkSending)="onBulkSending($event)"
9152
+ (bulkResult)="onBulkResult($event)"
8688
9153
  (sent)="onSent()"
8689
9154
  (emailAdded)="onEmailAdded($event)"
8690
9155
  (validChange)="valid.set($event)" />
8691
9156
  } @else {
8692
9157
  <ui-send-text
8693
9158
  #sendTextRef
8694
- [contactName]="activeContact()!.name"
8695
- [contactPhone]="activeContact()!.phone ?? ''"
8696
- [contactEmail]="activeContact()!.email ?? ''"
8697
- [contactId]="activeContact()!.id"
9159
+ [contactName]="(activeContact()?.name ?? '')"
9160
+ [contactPhone]="activeContact()?.phone ?? ''"
9161
+ [contactEmail]="activeContact()?.email ?? ''"
9162
+ [contactId]="(activeContact()?.id ?? '')"
9163
+ [bulkRecipients]="bulkRecipientsFor('text')"
8698
9164
  [defaultBody]="defaultMessage()"
8699
9165
  [context]="context()"
8700
9166
  [sendFn]="sendText()"
8701
9167
  [attachmentsAndScheduling]="attachmentsAndScheduling()"
8702
9168
  [documents]="documentsOn()"
8703
9169
  [documentPurposes]="documentPurposes()"
8704
- [initialDocuments]="initialDocuments()"
9170
+ [initialDocuments]="bulkMode() ? [] : initialDocuments()"
8705
9171
  [documentSigningOptions]="documentSigningOptions()"
8706
- [documentSigners]="documentSigners()"
9172
+ [documentSigners]="bulkMode() ? null : documentSigners()"
8707
9173
  [sendContext]="sendContext()"
8708
9174
  [documentContextTypes]="documentContextTypes()"
8709
9175
  [documentMergeCheck]="documentMergeCheck()"
8710
- [documentEnvelope]="documentEnvelope()"
9176
+ [documentEnvelope]="bulkMode() ? null : documentEnvelope()"
8711
9177
  [documentSentMessage]="documentSentMessage()"
8712
9178
  (rejected)="rejected.emit()"
8713
9179
  [documentPreview]="documentPreview()"
8714
9180
  [videoDefaults]="videoDefaults()"
8715
9181
  [showTemplates]="showTemplates()"
8716
9182
  [showVideo]="showVideo()"
9183
+ (bulkSending)="onBulkSending($event)"
9184
+ (bulkResult)="onBulkResult($event)"
8717
9185
  (sent)="onSent()"
8718
9186
  (phoneAdded)="onPhoneAdded($event)"
8719
9187
  (validChange)="valid.set($event)" />
8720
9188
  }
8721
9189
  }
9190
+ </div>
8722
9191
  }
8723
9192
 
8724
9193
  </ui-side-panel>
8725
9194
  `,
8726
- styles: [".msg-step{display:flex;align-items:center;gap:10px;margin-bottom:14px}.msg-step-badge{display:flex;align-items:center;justify-content:center;width:24px;height:24px;flex-shrink:0;border-radius:50%;background:#1e293b;color:#fff;font-size:.75rem;font-weight:700}.msg-step-text{font-size:.9375rem;font-weight:600;color:#1e293b}.msg-channel-row{display:flex;align-items:center;gap:10px;margin-bottom:16px;flex-wrap:wrap}.msg-sms-notice{display:flex;align-items:center;gap:8px;margin-bottom:16px;padding:10px 12px;border-radius:8px;background:#f8fafc;border:1px solid #e2e8f0;font-size:.8125rem;color:#64748b}.msg-job{margin-bottom:16px}.msg-via-tabs{display:flex;gap:6px;flex-wrap:wrap}.msg-skeleton{display:flex;flex-direction:column;gap:12px;padding:4px 0}.skeleton-line{height:14px;border-radius:6px;background:linear-gradient(90deg,#e2e8f0 25%,#f1f5f9,#e2e8f0 75%);background-size:200% 100%;animation:skeleton-shimmer 1.4s infinite}.skeleton-line--wide{width:100%}.skeleton-line--medium{width:70%}.skeleton-line--narrow{width:45%}@keyframes skeleton-shimmer{0%{background-position:200% 0}to{background-position:-200% 0}}\n"]
9195
+ styles: [".msg-step{display:flex;align-items:center;gap:10px;margin-bottom:14px}.msg-step-badge{display:flex;align-items:center;justify-content:center;width:24px;height:24px;flex-shrink:0;border-radius:50%;background:#1e293b;color:#fff;font-size:.75rem;font-weight:700}.msg-step-text{font-size:.9375rem;font-weight:600;color:#1e293b}.msg-channel-row{display:flex;align-items:center;gap:10px;margin-bottom:16px;flex-wrap:wrap}.msg-sms-notice{display:flex;align-items:center;gap:8px;margin-bottom:16px;padding:10px 12px;border-radius:8px;background:#f8fafc;border:1px solid #e2e8f0;font-size:.8125rem;color:#64748b}.msg-job{margin-bottom:16px}.msg-locked-note{font-size:.75rem;color:#64748b}.msg-bulk{display:flex;flex-direction:column;gap:8px;margin-bottom:16px}.msg-bulk-to{margin:0;font-size:.9375rem;font-weight:600;color:var(--shell-text, #1e293b)}.msg-bulk-counts{display:flex;flex-wrap:wrap;gap:6px}.msg-sr{position:absolute;width:1px;height:1px;margin:-1px;padding:0;overflow:hidden;clip:rect(0,0,0,0);white-space:nowrap;border:0}.msg-via-tabs{display:flex;align-items:center;gap:6px;flex-wrap:wrap}.msg-skeleton{display:flex;flex-direction:column;gap:12px;padding:4px 0}.skeleton-line{height:14px;border-radius:6px;background:linear-gradient(90deg,#e2e8f0 25%,#f1f5f9,#e2e8f0 75%);background-size:200% 100%;animation:skeleton-shimmer 1.4s infinite}.skeleton-line--wide{width:100%}.skeleton-line--medium{width:70%}.skeleton-line--narrow{width:45%}@keyframes skeleton-shimmer{0%{background-position:200% 0}to{background-position:-200% 0}}\n"]
8727
9196
  }]
8728
9197
  }],
8729
9198
  ctorParameters: () => [],
@@ -8752,6 +9221,38 @@ i0.ɵɵngDeclareClassMetadata({
8752
9221
  required: false
8753
9222
  }]
8754
9223
  }],
9224
+ recipients: [{
9225
+ type: i0.Input,
9226
+ args: [{
9227
+ isSignal: true,
9228
+ alias: "recipients",
9229
+ required: false
9230
+ }]
9231
+ }],
9232
+ initialChannel: [{
9233
+ type: i0.Input,
9234
+ args: [{
9235
+ isSignal: true,
9236
+ alias: "initialChannel",
9237
+ required: false
9238
+ }]
9239
+ }],
9240
+ recipientNoun: [{
9241
+ type: i0.Input,
9242
+ args: [{
9243
+ isSignal: true,
9244
+ alias: "recipientNoun",
9245
+ required: false
9246
+ }]
9247
+ }],
9248
+ recipientNounSingular: [{
9249
+ type: i0.Input,
9250
+ args: [{
9251
+ isSignal: true,
9252
+ alias: "recipientNounSingular",
9253
+ required: false
9254
+ }]
9255
+ }],
8755
9256
  textEnabled: [{
8756
9257
  type: i0.Input,
8757
9258
  args: [{
@@ -9100,6 +9601,10 @@ i0.ɵɵngDeclareClassMetadata({
9100
9601
  type: i0.Output,
9101
9602
  args: ["sent"]
9102
9603
  }],
9604
+ sentBulk: [{
9605
+ type: i0.Output,
9606
+ args: ["sentBulk"]
9607
+ }],
9103
9608
  emailAdded: [{
9104
9609
  type: i0.Output,
9105
9610
  args: ["emailAdded"]
@@ -9130,6 +9635,6 @@ i0.ɵɵngDeclareClassMetadata({
9130
9635
  }]
9131
9636
  }
9132
9637
  });
9133
- export { DEFAULT_DOCUMENT_SUBJECT, DEFAULT_SIGNING_OPTIONS, DOCUMENT_PROVIDER_OPTIONS, DOCUMENT_PURPOSE_OPTIONS, DocumentMergeCheckComponent, DocumentSendOptionsComponent, DocumentSignersComponent, MAX_CC, MERGE_CHECK_DEBOUNCE_MS, SIGNER_ROLES, SIGNER_ROLE_LABEL_DEFAULT, SIGNING_LIMITS, SendMessagePanelComponent, TemplatePickerComponent, VideoPickerComponent, appendVideoLink, applyVideoLine, buildSigners, buildVideoLink, countersignLine, countersignPlaceLabel, describeDocumentSigners, effectiveRoles, forwardContext, hasMappedDataFields, mergeFieldText, mergeWarningLines, messagePreviewHtml, normalizeRoles, parseCcList, roleNumber, sendExistingEnvelope, signerProblems, signerRoleLabel, stripVideoLink, suggestDocumentSubject, videoMessageLine };
9638
+ export { BULK_MERGE_TAG_HINT, BULK_NO_SEND_MESSAGE, DEFAULT_DOCUMENT_SUBJECT, DEFAULT_SIGNING_OPTIONS, DOCUMENT_PROVIDER_OPTIONS, DOCUMENT_PURPOSE_OPTIONS, DocumentMergeCheckComponent, DocumentSendOptionsComponent, DocumentSignersComponent, MAX_CC, MERGE_CHECK_DEBOUNCE_MS, PERSONAL_TAG_KEYS, SIGNER_ROLES, SIGNER_ROLE_LABEL_DEFAULT, SIGNING_LIMITS, SendMessagePanelComponent, TemplatePickerComponent, VideoPickerComponent, appendVideoLink, applyVideoLine, buildSigners, buildVideoLink, countersignLine, countersignPlaceLabel, describeDocumentSigners, effectiveRoles, forwardContext, hasMappedDataFields, mergeFieldText, mergeWarningLines, messagePreviewHtml, normalizeRoles, parseCcList, roleNumber, sendExistingEnvelope, signerProblems, signerRoleLabel, stripVideoLink, suggestDocumentSubject, videoMessageLine };
9134
9639
 
9135
9640
  //# sourceMappingURL=forgeroute-ui-messaging.mjs.map