@session-services/web-elements 0.1.21 → 0.1.23

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.
@@ -184,7 +184,13 @@
184
184
  padding: 0;
185
185
  }
186
186
 
187
- /* Remove default focus outline and add focus-visible styles */
187
+ button,
188
+ input,
189
+ select,
190
+ textarea {
191
+ font: inherit;
192
+ }
193
+
188
194
  *:focus {
189
195
  outline: none;
190
196
  }
@@ -245,8 +251,40 @@
245
251
  margin: 0 0 var(--internal-spacing-md);
246
252
  }
247
253
 
248
- .door-alert,
249
- .empty-ticketing-state,
254
+ .ticket-section.loading-ticket-section {
255
+ overflow: visible;
256
+ background: transparent;
257
+ border: 0;
258
+ box-shadow: none;
259
+ }
260
+
261
+ .loading-ticket-section .admissions-list {
262
+ display: flex;
263
+ flex-direction: column;
264
+ gap: var(--internal-spacing-sm);
265
+ }
266
+
267
+ .admission-card.loading-admission {
268
+ min-height: 5.25rem;
269
+ padding: 0;
270
+ background: oklch(from var(--internal-foreground) l c h / 0.07);
271
+ border: 0;
272
+ border-radius: var(--internal-radius);
273
+ box-shadow: none;
274
+ pointer-events: none;
275
+ animation: loading-card 1.6s ease-out infinite;
276
+ }
277
+
278
+ @keyframes loading-card {
279
+ 0%,
280
+ 100% {
281
+ opacity: 0.48;
282
+ }
283
+ 50% {
284
+ opacity: 0.74;
285
+ }
286
+ }
287
+
250
288
  .passcode-form,
251
289
  .passcode-form input,
252
290
  .ticket-section,
@@ -280,8 +318,11 @@
280
318
  transform: translateY(-1px);
281
319
  }
282
320
 
283
- .door-alert,
284
- .empty-ticketing-state,
321
+ .external-ticket-button:focus-visible {
322
+ outline: 2px solid var(--internal-ring);
323
+ outline-offset: 2px;
324
+ }
325
+
285
326
  .passcode-form {
286
327
  display: flex;
287
328
  flex-direction: column;
@@ -292,14 +333,36 @@
292
333
  border-radius: var(--internal-radius);
293
334
  }
294
335
 
295
- .door-alert {
296
- background: oklch(from var(--internal-success) l c h / 0.1);
297
- border-color: oklch(from var(--internal-success) l c h / 0.48);
336
+ .passcode-form h2 {
337
+ color: var(--internal-foreground);
338
+ font-size: var(--internal-font-size-base);
339
+ font-weight: var(--internal-font-weight-semibold);
340
+ line-height: 1.3;
341
+ }
342
+
343
+ .passcode-form p {
344
+ color: var(--internal-muted-foreground);
345
+ font-size: var(--internal-font-size-sm);
346
+ line-height: 1.5;
347
+ }
348
+
349
+ .door-alert,
350
+ .empty-ticketing-state,
351
+ .message-state {
352
+ display: flex;
353
+ flex-direction: column;
354
+ gap: calc(var(--internal-spacing-xs) * 1.5);
355
+ margin-bottom: var(--internal-spacing-md);
356
+ padding: 0;
357
+ background: transparent;
358
+ border: none;
359
+ border-radius: 0;
360
+ box-shadow: none;
298
361
  }
299
362
 
300
363
  .door-alert strong,
301
364
  .empty-ticketing-state strong,
302
- .passcode-form h2 {
365
+ .message-state strong {
303
366
  color: var(--internal-foreground);
304
367
  font-size: var(--internal-font-size-base);
305
368
  font-weight: var(--internal-font-weight-semibold);
@@ -308,19 +371,45 @@
308
371
 
309
372
  .door-alert span,
310
373
  .empty-ticketing-state span,
311
- .passcode-form p {
374
+ .message-state span {
312
375
  color: var(--internal-muted-foreground);
313
376
  font-size: var(--internal-font-size-sm);
314
- line-height: 1.5;
377
+ line-height: 1.4;
315
378
  }
316
379
 
317
380
  .external-ticket-button {
381
+ flex-direction: column;
382
+ gap: calc(var(--internal-spacing-xs) * 0.5);
318
383
  width: 100%;
319
384
  padding: calc(var(--internal-spacing-sm) + var(--internal-spacing-xs))
320
385
  var(--internal-spacing-lg);
386
+ text-decoration: none;
387
+ }
388
+
389
+ .external-ticket-panel {
390
+ display: flex;
391
+ flex-direction: column;
392
+ gap: var(--internal-spacing-md);
393
+ }
394
+
395
+ .external-ticket-warning {
396
+ color: var(--internal-muted-foreground);
397
+ font-size: var(--internal-font-size-sm);
398
+ font-weight: var(--internal-font-weight-medium);
399
+ line-height: 1.35;
400
+ }
401
+
402
+ .external-ticket-label {
321
403
  font-size: var(--internal-font-size-base);
322
404
  font-weight: var(--internal-font-weight-bold);
323
- text-decoration: none;
405
+ line-height: 1.2;
406
+ }
407
+
408
+ .external-ticket-domain {
409
+ font-size: var(--internal-font-size-xs);
410
+ font-weight: var(--internal-font-weight-medium);
411
+ line-height: 1.2;
412
+ opacity: 0.82;
324
413
  }
325
414
 
326
415
  .passcode-form input {
@@ -563,22 +652,6 @@
563
652
  flex-shrink: 0;
564
653
  }
565
654
 
566
- .sold-out-badge {
567
- display: inline-flex;
568
- align-items: center;
569
- justify-content: center;
570
- padding: 0;
571
- background: transparent;
572
- color: var(--internal-muted-foreground);
573
- border: none;
574
- font-size: var(--internal-font-size-sm);
575
- font-weight: var(--internal-font-weight-medium);
576
- text-transform: uppercase;
577
- letter-spacing: 0.05em;
578
- margin-left: auto;
579
- opacity: 0.7;
580
- }
581
-
582
655
  .admission-price-info {
583
656
  display: flex;
584
657
  align-items: center;
@@ -591,6 +664,7 @@
591
664
  .admission-price-wrapper {
592
665
  display: flex;
593
666
  flex-direction: column;
667
+ row-gap: calc(var(--internal-spacing-xs) * 0.5);
594
668
  }
595
669
 
596
670
  .admission-price {
@@ -614,12 +688,18 @@
614
688
  text-decoration: line-through;
615
689
  }
616
690
 
691
+ .admission-booking-fee {
692
+ color: var(--internal-muted-foreground);
693
+ font-size: var(--internal-font-size-xs);
694
+ font-weight: var(--internal-font-weight-medium);
695
+ line-height: 1.2;
696
+ }
697
+
617
698
  .admission-savings,
618
699
  .admission-availability {
619
700
  font-size: var(--internal-font-size-xs);
620
701
  font-weight: var(--internal-font-weight-medium);
621
- line-height: 1.35;
622
- margin-top: var(--internal-spacing-xs);
702
+ line-height: 1.2;
623
703
  }
624
704
 
625
705
  .admission-savings {
@@ -654,6 +734,10 @@
654
734
  transition: all var(--internal-transition-fast);
655
735
  }
656
736
 
737
+ .quantity-controls:has(.quantity-value:focus-visible) {
738
+ box-shadow: 0 0 0 2px var(--internal-ring);
739
+ }
740
+
657
741
  .quantity-btn {
658
742
  width: calc(var(--internal-spacing-lg) * 1.5);
659
743
  height: calc(var(--internal-spacing-lg) * 1.5);
@@ -715,6 +799,10 @@
715
799
  padding: 0 var(--internal-spacing-xs);
716
800
  }
717
801
 
802
+ .quantity-value:focus-visible {
803
+ outline: none;
804
+ }
805
+
718
806
  .quantity-value::-webkit-inner-spin-button,
719
807
  .quantity-value::-webkit-outer-spin-button {
720
808
  appearance: none;
@@ -733,20 +821,26 @@
733
821
  justify-content: center;
734
822
  min-width: calc(var(--internal-spacing-lg) * 1.5 * 2 + 2.5rem);
735
823
  height: calc(var(--internal-spacing-lg) * 1.5);
736
- padding: 0 var(--internal-spacing-md);
737
- background: var(--internal-muted);
738
- border-radius: calc(var(--internal-radius) - 2px);
824
+ padding: 0;
825
+ background: transparent;
826
+ border: none;
827
+ box-shadow: none;
739
828
  }
740
829
 
741
830
  .sold-out-controls,
742
831
  .sold-out-text {
743
- color: var(--internal-muted-foreground);
832
+ color: var(--internal-destructive);
744
833
  font-size: var(--internal-font-size-xs);
745
834
  font-weight: var(--internal-font-weight-medium);
746
835
  letter-spacing: 0.025em;
747
836
  text-transform: uppercase;
748
837
  }
749
838
 
839
+ .sold-out-controls.success,
840
+ .sold-out-controls.success .sold-out-text {
841
+ color: var(--internal-success);
842
+ }
843
+
750
844
  .checkout-actions {
751
845
  margin: var(--internal-spacing-lg) 0 0 0;
752
846
  width: 100%;
@@ -816,7 +910,7 @@
816
910
  background: var(--internal-secondary);
817
911
  color: var(--internal-foreground);
818
912
  border-radius: calc(var(--internal-radius) - 2px);
819
- font-size: var(--internal-font-size-sm);
913
+ font-size: var(--internal-font-size-xs);
820
914
  font-weight: var(--internal-font-weight-medium);
821
915
  cursor: pointer;
822
916
  transition: all var(--internal-transition-fast);
@@ -914,40 +1008,9 @@
914
1008
  }
915
1009
  }
916
1010
 
917
- /* Loading & Error States */
918
- .loading-state {
919
- display: flex;
920
- justify-content: center;
921
- align-items: center;
922
- padding: var(--internal-spacing-xl);
923
- text-align: center;
924
- min-height: 200px;
925
- }
926
-
927
- .error-banner {
928
- background: oklch(
929
- from var(--internal-destructive) calc(l + 0.4) calc(c * 0.2) h / 0.05
930
- );
931
- border: 1px solid oklch(from var(--internal-destructive) l c h / 0.2);
932
- border-radius: var(--internal-radius);
933
- padding: var(--internal-spacing-lg);
934
- margin: var(--internal-spacing-lg);
935
- text-align: center;
936
- }
937
-
938
- .error-banner strong {
939
- display: block;
940
- color: var(--internal-destructive);
941
- font-size: var(--internal-font-size-lg);
942
- margin-bottom: var(--internal-spacing-sm);
943
- }
944
-
945
- .error-banner p {
946
- color: var(--internal-muted-foreground);
947
- margin-bottom: var(--internal-spacing-md);
948
- }
949
-
950
1011
  .retry-button {
1012
+ align-self: flex-start;
1013
+ margin-top: var(--internal-spacing-xs);
951
1014
  background: var(--internal-primary);
952
1015
  color: var(--internal-primary-foreground);
953
1016
  border: 1px solid oklch(from var(--internal-primary) calc(l - 0.08) c h);
@@ -1000,6 +1063,11 @@
1000
1063
  padding-bottom: calc(var(--internal-spacing-xl) * 3);
1001
1064
  }
1002
1065
 
1066
+ :host([data-sticky-checkout="true"]) .checkout-actions > .checkout-btn,
1067
+ :host([data-sticky-checkout="true"]) .inline-external-ticket-button {
1068
+ display: none;
1069
+ }
1070
+
1003
1071
  .sticky-checkout-bar {
1004
1072
  display: block;
1005
1073
  }
@@ -44,6 +44,16 @@ function eventVenueName(event) {
44
44
  function optionalId(id) {
45
45
  return id || undefined;
46
46
  }
47
+ function urlDomain(url) {
48
+ if (!url)
49
+ return "external site";
50
+ try {
51
+ return new URL(url).hostname.replace(/^www\./, "");
52
+ }
53
+ catch {
54
+ return "external site";
55
+ }
56
+ }
47
57
  function admissionAriaLabel(admission, section, status) {
48
58
  if (section)
49
59
  return `${admission.name} in ${section.name} - ${status}`;
@@ -981,21 +991,21 @@ export class SessionsTicketing {
981
991
  previewItem: quoteItemsBySelection(this.previewQuote).get(key),
982
992
  });
983
993
  const savings = admissionPriceSavings(price);
984
- return (h("div", { class: "admission-price-info" }, h("div", { class: "admission-price-wrapper" }, this.renderAdmissionPrice(price), state.showScarcity && (h("span", { class: "admission-availability" }, admission.available, " tickets remaining")), !state.showScarcity && savings > 0 && (h("span", { class: "admission-savings" }, "Save ", this.formatPriceValue(savings))), state.label && (h("span", { class: `admission-sale-label ${state.label.tone}` }, state.label.text))), this.renderAdmissionControls(admission, sectionId, state, mode)));
994
+ return (h("div", { class: "admission-price-info" }, h("div", { class: "admission-price-wrapper" }, savings > 0 && (h("span", { class: "admission-savings" }, "Save ", this.formatPriceValue(savings))), this.renderAdmissionPrice(price), price.fees > 0 && (h("span", { class: "admission-booking-fee" }, this.formatPriceValue(price.fees), " Booking Fee")), state.showScarcity && (h("span", { class: "admission-availability" }, admission.available, " tickets remaining")), state.label && (h("span", { class: `admission-sale-label ${state.label.tone}` }, state.label.text))), this.renderAdmissionControls(admission, sectionId, state, mode)));
985
995
  }
986
996
  renderAdmissionPrice(price) {
987
997
  if (price.discounted) {
988
- return (h("span", { class: "admission-price-row" }, h("span", { class: "admission-price discounted" }, this.formatPriceValue(price.total)), price.gross !== undefined && (h("span", { class: "admission-price-original" }, this.formatPriceValue(price.gross)))));
998
+ return (h("span", { class: "admission-price-row" }, h("span", { class: "admission-price discounted" }, this.formatPriceValue(price.base)), price.gross !== undefined && (h("span", { class: "admission-price-original" }, this.formatPriceValue(price.gross)))));
989
999
  }
990
- return (h("span", { class: "admission-price" }, this.formatPriceValue(price.total)));
1000
+ return (h("span", { class: "admission-price" }, this.formatPriceValue(price.base)));
991
1001
  }
992
1002
  renderAdmissionControls(admission, sectionId, state, mode) {
993
- if (mode === "door" || state.postSale)
994
- return null;
995
- if (!state.soldOut) {
996
- return this.renderQuantityControls(admission, sectionId, state.disabled, state.maxQuantity);
1003
+ if (state.statusLabel) {
1004
+ return (h("div", { class: `sold-out-controls ${state.statusTone ?? ""}` }, h("span", { class: "sold-out-text" }, state.statusLabel)));
997
1005
  }
998
- return (h("div", { class: "sold-out-controls" }, h("span", { class: "sold-out-text" }, "Sold Out")));
1006
+ if (mode === "door")
1007
+ return null;
1008
+ return this.renderQuantityControls(admission, sectionId, state.disabled, state.maxQuantity);
999
1009
  }
1000
1010
  renderAdmission(admission, event, section, isSectionSoldOut = false, mode = "default") {
1001
1011
  const state = getAdmissionDisplayState({
@@ -1011,13 +1021,16 @@ export class SessionsTicketing {
1011
1021
  return `admission-card ${state.soldOut ? "sold-out" : ""} ${state.disabled && !state.soldOut ? "unavailable" : ""} ${section ? "" : "solo-admission"}`;
1012
1022
  }
1013
1023
  getAdmissionAriaLabel(admission, section, state) {
1014
- const status = this.admissionStatusText(admission, state.soldOut);
1024
+ const status = state.statusLabel ?? this.admissionStatusText(admission, state.soldOut);
1015
1025
  return admissionAriaLabel(admission, section, status);
1016
1026
  }
1017
1027
  admissionStatusText(admission, soldOut) {
1018
1028
  if (soldOut)
1019
1029
  return "Sold Out";
1020
- return this.formatPriceValue(priceTotal(admission.price));
1030
+ if ((admission.price.booking ?? 0) <= 0) {
1031
+ return this.formatPriceValue(admission.price.base);
1032
+ }
1033
+ return `${this.formatPriceValue(admission.price.base)} plus ${this.formatPriceValue(admission.price.booking)} booking fee`;
1021
1034
  }
1022
1035
  renderAdmissionHeader(admission) {
1023
1036
  return (h("div", { class: "admission-header" }, h("div", { class: "admission-header-text" }, h("h3", { class: "admission-title" }, admission.name), admission.subtitle && (h("p", { class: "admission-subtitle" }, admission.subtitle))), this.renderMoreInfoButton(admission)));
@@ -1033,29 +1046,29 @@ export class SessionsTicketing {
1033
1046
  }
1034
1047
  return (h(h.Fragment, null, this.renderEventTimeAndLocation(event), this.renderPriceAndControls(admission, sectionId, state, mode)));
1035
1048
  }
1036
- renderSectionHeader(section, admissions, isSectionSoldOut) {
1037
- return (h("div", { class: "section-header" }, h("h2", { class: "section-name" }, section.name, isSectionSoldOut && h("span", { class: "sold-out-badge" }, "Sold Out")), this.renderAdmissionTime(admissions[0], "section-time"), this.renderSectionLocation(section)));
1049
+ renderSectionHeader(section, admissions) {
1050
+ return (h("div", { class: "section-header" }, h("h2", { class: "section-name" }, section.name), this.renderAdmissionTime(admissions[0], "section-time"), this.renderSectionLocation(section)));
1038
1051
  }
1039
1052
  renderSectionWithAdmissions(section, admissions, event, mode) {
1040
1053
  const soldOut = isSectionSoldOut({
1041
1054
  admissions,
1042
1055
  section,
1043
1056
  });
1044
- return (h("div", { class: `ticket-section ${soldOut ? "section-sold-out" : ""}`, key: section.id }, this.renderSectionHeader(section, admissions, soldOut), h("div", { class: "admissions-list" }, admissions.map((admission) => this.renderAdmission(admission, event, section, soldOut, mode)))));
1057
+ return (h("div", { class: `ticket-section ${soldOut ? "section-sold-out" : ""}`, key: section.id }, this.renderSectionHeader(section, admissions), h("div", { class: "admissions-list" }, admissions.map((admission) => this.renderAdmission(admission, event, section, soldOut, mode)))));
1045
1058
  }
1046
1059
  renderLoadingState() {
1047
- return (h(Host, { "data-theme": this.currentTheme }, h("div", { class: "loading-state" }, "Loading event details...")));
1060
+ return (h(Host, { "data-theme": this.currentTheme }, h("div", { class: "session-services-ticketing-container" }, h("div", { class: "tickets-wrapper" }, h("h2", { class: "tickets-heading" }, "Get Tickets"), h("div", { class: "ticket-section loading-ticket-section" }, h("div", { class: "admissions-list" }, [0, 1, 2].map((item) => (h("div", { "aria-hidden": "true", class: "admission-card loading-admission", key: item })))))))));
1048
1061
  }
1049
1062
  renderErrorState() {
1050
1063
  if (this.passcodeDenied)
1051
1064
  return this.renderPasscodeState();
1052
- return (h(Host, { "data-theme": this.currentTheme }, h("div", { class: "session-services-ticketing-container" }, h("div", { class: "error-banner" }, h("strong", null, "Unable to load event"), h("p", null, this.error), h("button", { type: "button", class: "retry-button", onClick: () => this.loadEventData() }, "Try Again")))));
1065
+ return (h(Host, { "data-theme": this.currentTheme }, h("div", { class: "session-services-ticketing-container" }, h("div", { class: "message-state" }, h("strong", null, "Tickets Couldn\u2019t Load"), h("span", null, "We couldn\u2019t load ticket details. Try again in a moment."), h("button", { type: "button", class: "retry-button", onClick: () => this.loadEventData() }, "Try Again")))));
1053
1066
  }
1054
1067
  renderPasscodeState() {
1055
1068
  return (h(Host, { "data-theme": this.currentTheme }, h("div", { class: "session-services-ticketing-container" }, h("form", { class: "passcode-form", onSubmit: (e) => this.submitPasscode(e) }, h("h2", null, "Enter passcode"), h("p", null, "Enter the event passcode to view tickets and event details."), h("input", { type: "text", name: "passcode", value: this.passcodeInput, onInput: this.handlePasscodeInput, autocomplete: "one-time-code", placeholder: "Passcode", "aria-label": "Passcode" }), this.passcodeDenied && this.activePasscode && (h("span", { class: "error-message" }, "Incorrect passcode")), h("button", { type: "submit", class: "checkout-btn", disabled: !this.passcodeInput.trim() }, "Unlock event")))));
1056
1069
  }
1057
1070
  renderNotFoundState() {
1058
- return (h(Host, { "data-theme": this.currentTheme }, h("div", { class: "session-services-ticketing-container" }, h("div", { class: "error-banner" }, h("strong", null, "Event not found"), h("p", null, "The requested event could not be found.")))));
1071
+ return (h(Host, { "data-theme": this.currentTheme }, h("div", { class: "session-services-ticketing-container" }, h("div", { class: "message-state" }, h("strong", null, "Event not found"), h("span", null, "We couldn\u2019t find this event. The link may be incorrect or expired.")))));
1059
1072
  }
1060
1073
  renderCouponPanel() {
1061
1074
  const hasInput = this.couponInput.trim().length > 0;
@@ -1137,6 +1150,13 @@ export class SessionsTicketing {
1137
1150
  return null;
1138
1151
  return (h("div", { class: "sticky-checkout-bar" }, h("button", { type: "button", class: `checkout-btn sticky-checkout-btn ${this.selectionPrompt ? "selection-prompt" : ""}`, onClick: () => this.handleStickyCheckoutClick(state), disabled: this.isStickyCheckoutDisabled(state), "aria-label": this.quoteLoading ? "Updating" : undefined }, this.renderStickyCheckoutButtonContent(state))));
1139
1152
  }
1153
+ renderStickyExternalTicketing(state) {
1154
+ if (!this.stickyCheckout)
1155
+ return null;
1156
+ if (!state.externalTicketUrl)
1157
+ return null;
1158
+ return (h("div", { class: "sticky-checkout-bar" }, h("a", { class: "external-ticket-button sticky-checkout-btn", href: state.externalTicketUrl, target: "_blank", rel: "noreferrer" }, h("span", { class: "external-ticket-label" }, "Get tickets"))));
1159
+ }
1140
1160
  renderModal() {
1141
1161
  if (!this.openModalAdmissionId)
1142
1162
  return null;
@@ -1163,7 +1183,7 @@ export class SessionsTicketing {
1163
1183
  if (mode !== "door")
1164
1184
  return null;
1165
1185
  const location = event.location?.name?.trim() || "the venue";
1166
- return (h("div", { class: "door-alert" }, h("strong", null, "Tickets are available at ", location), h("span", null, "Online sales are closed. Buy these tickets at the door when you arrive.")));
1186
+ return (h("div", { class: "door-alert" }, h("strong", null, "Tickets are available at ", location), h("span", null, "Online sales are closed. Tickets are available at the venue when you arrive.")));
1167
1187
  }
1168
1188
  loadingView() {
1169
1189
  return this.loading ? this.renderLoadingState() : undefined;
@@ -1183,7 +1203,7 @@ export class SessionsTicketing {
1183
1203
  return this.renderNotFoundState();
1184
1204
  const state = getTicketingState(event);
1185
1205
  if (state.showExternalTicketing) {
1186
- return this.renderExternalTicketing(state.externalTicketUrl);
1206
+ return (h(Host, { "data-theme": this.currentTheme, "data-sticky-checkout": this.stickyCheckout ? "true" : undefined }, h("div", { class: "session-services-ticketing-container" }, this.renderExternalTicketing(state)), this.renderStickyExternalTicketing(state)));
1187
1207
  }
1188
1208
  return (h(Host, { "data-theme": this.currentTheme, "data-sticky-checkout": this.stickyCheckout ? "true" : undefined }, h("div", { class: "session-services-ticketing-container" }, state.showTicketing
1189
1209
  ? this.renderTicketSections(event)
@@ -1191,13 +1211,16 @@ export class SessionsTicketing {
1191
1211
  state.showTicketing &&
1192
1212
  this.renderCheckoutActions()), this.renderStickyCheckout(state), this.renderModal()));
1193
1213
  }
1194
- renderExternalTicketing(url) {
1195
- return (h(Host, { "data-theme": this.currentTheme }, h("div", { class: "session-services-ticketing-container" }, h("a", { class: "external-ticket-button", href: url, target: "_blank", rel: "noreferrer" }, "Get tickets"))));
1214
+ renderExternalTicketing(state) {
1215
+ const domain = urlDomain(state.externalTicketUrl);
1216
+ if (!state.externalTicketUrl)
1217
+ return null;
1218
+ return (h("div", { class: "tickets-wrapper" }, h("h2", { class: "tickets-heading" }, state.ticketHeading), h("div", { class: "external-ticket-panel" }, h("p", { class: "external-ticket-warning" }, "This event sells through an external ticketing site. Checkout, tickets, and support are handled there."), h("a", { class: "external-ticket-button inline-external-ticket-button", href: state.externalTicketUrl, target: "_blank", rel: "noreferrer" }, h("span", { class: "external-ticket-label" }, "Get tickets"), h("span", { class: "external-ticket-domain" }, "via ", domain)))));
1196
1219
  }
1197
1220
  renderUnavailableState(state) {
1198
- return (h("div", { class: "empty-ticketing-state" }, h("strong", null, state.isEnded ? "Event ended" : "Tickets unavailable"), h("span", null, state.isEnded
1199
- ? "Ticket sales are closed for this event."
1200
- : "Tickets are not currently available for this event.")));
1221
+ return (h("div", { class: "empty-ticketing-state" }, h("strong", null, "Tickets Unavailable"), h("span", null, state.isEnded
1222
+ ? "This event has ended, so tickets are no longer available."
1223
+ : "Tickets are not available for this event right now.")));
1201
1224
  }
1202
1225
  render() {
1203
1226
  return this.pendingView() ?? this.renderReadyState();
@@ -62,25 +62,7 @@ function hasPurchasableAdmissions(event) {
62
62
  const zone = eventTimeZone(event);
63
63
  return event.admissions.some((admission) => isAdmissionPurchasable(admission, zone, sections));
64
64
  }
65
- function postSaleLabel(value) {
66
- if (value === "SOLD_OUT")
67
- return "Sold out";
68
- if (value === "DOOR")
69
- return "Tickets available at the door";
70
- return "Sale ended";
71
- }
72
- function postSaleTone(value) {
73
- if (value === "DOOR")
74
- return "success";
75
- return "muted";
76
- }
77
65
  function admissionSaleLabel(admission, zone) {
78
- if (isNaiveDateTimePast(admission.saleEndsAt, zone)) {
79
- return {
80
- text: postSaleLabel(admission.saleEndsBehavior),
81
- tone: postSaleTone(admission.saleEndsBehavior),
82
- };
83
- }
84
66
  if (!isNaiveDateTimeFuture(admission.saleStartsAt, zone)) {
85
67
  return null;
86
68
  }
@@ -89,6 +71,16 @@ function admissionSaleLabel(admission, zone) {
89
71
  tone: "muted",
90
72
  };
91
73
  }
74
+ function admissionStatus(input) {
75
+ if (input.value === "DOOR" && (input.soldOut || input.saleEnded)) {
76
+ return { text: "Available At Door", tone: "success" };
77
+ }
78
+ if (input.soldOut)
79
+ return { text: "Sold Out", tone: "destructive" };
80
+ if (!input.saleEnded)
81
+ return null;
82
+ return { text: "Sale Ended", tone: "destructive" };
83
+ }
92
84
  function endedStatus(admissions, zone) {
93
85
  const ended = admissions.filter((admission) => isNaiveDateTimePast(admission.saleEndsAt, zone));
94
86
  if (ended.length === 0)
@@ -189,12 +181,19 @@ export function getAdmissionDisplayState(input) {
189
181
  const zone = eventTimeZone(input.event);
190
182
  const saleNotStarted = isNaiveDateTimeFuture(input.admission.saleStartsAt, zone);
191
183
  const saleEnded = isNaiveDateTimePast(input.admission.saleEndsAt, zone);
184
+ const saleEndedSoldOut = saleEnded && input.admission.saleEndsBehavior === "SOLD_OUT";
192
185
  const soldOut = Boolean(input.sectionSoldOut) ||
186
+ saleEndedSoldOut ||
193
187
  isAdmissionSoldOut({
194
188
  admission: input.admission,
195
189
  section: input.section,
196
190
  });
197
191
  const disabled = soldOut || saleNotStarted || saleEnded;
192
+ const status = admissionStatus({
193
+ saleEnded,
194
+ soldOut,
195
+ value: input.admission.saleEndsBehavior,
196
+ });
198
197
  return {
199
198
  disabled,
200
199
  label: admissionSaleLabel(input.admission, zone),
@@ -202,6 +201,8 @@ export function getAdmissionDisplayState(input) {
202
201
  postSale: saleEnded,
203
202
  showScarcity: showScarcity(input.admission, soldOut, disabled),
204
203
  soldOut,
204
+ statusLabel: status?.text ?? null,
205
+ statusTone: status?.tone ?? null,
205
206
  };
206
207
  }
207
208
  function quoteItemUnits(item) {
@@ -211,29 +212,35 @@ export function getAdmissionPriceDisplay(input) {
211
212
  const item = input.quoteItem ?? input.previewItem;
212
213
  if (item) {
213
214
  const units = quoteItemUnits(item);
214
- const total = Math.round(item.price.total / units);
215
+ const base = Math.round(Math.max((item.price.base + item.price.discount) / units, 0));
216
+ const fees = Math.round(item.price.booking / units);
215
217
  if (!item.price.discount || item.price.discount >= 0) {
216
218
  return {
219
+ base,
217
220
  discounted: false,
218
- total,
221
+ fees,
219
222
  };
220
223
  }
221
224
  return {
225
+ base,
222
226
  discounted: true,
223
- gross: Math.round(item.price.gross / units),
224
- total,
227
+ fees,
228
+ gross: Math.round(item.price.base / units),
225
229
  };
226
230
  }
227
231
  if (input.initialPrice && input.initialPrice.discount < 0) {
232
+ const fees = input.admission.price.booking ?? 0;
228
233
  return {
234
+ base: Math.max(input.initialPrice.total - fees, 0),
229
235
  discounted: true,
230
- gross: input.initialPrice.gross,
231
- total: input.initialPrice.total,
236
+ fees,
237
+ gross: Math.max(input.initialPrice.gross - fees, 0),
232
238
  };
233
239
  }
234
240
  return {
241
+ base: input.admission.price.base ?? 0,
235
242
  discounted: false,
236
- total: priceTotal(input.admission.price),
243
+ fees: input.admission.price.booking ?? 0,
237
244
  };
238
245
  }
239
246
  export function quoteItemsBySelection(quote) {
@@ -242,7 +249,7 @@ export function quoteItemsBySelection(quote) {
242
249
  export function admissionPriceSavings(price) {
243
250
  if (!price.discounted || price.gross === undefined)
244
251
  return 0;
245
- return Math.max(price.gross - price.total, 0);
252
+ return Math.max(price.gross - price.base, 0);
246
253
  }
247
254
  export function previewQuoteItems(event) {
248
255
  const ids = new Set(event.sections.map((section) => section.id));