@session-services/web-elements 0.1.21 → 0.1.22

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
  }
@@ -569,7 +575,7 @@
569
575
  justify-content: center;
570
576
  padding: 0;
571
577
  background: transparent;
572
- color: var(--internal-muted-foreground);
578
+ color: var(--internal-destructive);
573
579
  border: none;
574
580
  font-size: var(--internal-font-size-sm);
575
581
  font-weight: var(--internal-font-weight-medium);
@@ -591,6 +597,7 @@
591
597
  .admission-price-wrapper {
592
598
  display: flex;
593
599
  flex-direction: column;
600
+ row-gap: calc(var(--internal-spacing-xs) * 0.5);
594
601
  }
595
602
 
596
603
  .admission-price {
@@ -614,12 +621,18 @@
614
621
  text-decoration: line-through;
615
622
  }
616
623
 
624
+ .admission-booking-fee {
625
+ color: var(--internal-muted-foreground);
626
+ font-size: var(--internal-font-size-xs);
627
+ font-weight: var(--internal-font-weight-medium);
628
+ line-height: 1.2;
629
+ }
630
+
617
631
  .admission-savings,
618
632
  .admission-availability {
619
633
  font-size: var(--internal-font-size-xs);
620
634
  font-weight: var(--internal-font-weight-medium);
621
- line-height: 1.35;
622
- margin-top: var(--internal-spacing-xs);
635
+ line-height: 1.2;
623
636
  }
624
637
 
625
638
  .admission-savings {
@@ -654,6 +667,10 @@
654
667
  transition: all var(--internal-transition-fast);
655
668
  }
656
669
 
670
+ .quantity-controls:has(.quantity-value:focus-visible) {
671
+ box-shadow: 0 0 0 2px var(--internal-ring);
672
+ }
673
+
657
674
  .quantity-btn {
658
675
  width: calc(var(--internal-spacing-lg) * 1.5);
659
676
  height: calc(var(--internal-spacing-lg) * 1.5);
@@ -715,6 +732,10 @@
715
732
  padding: 0 var(--internal-spacing-xs);
716
733
  }
717
734
 
735
+ .quantity-value:focus-visible {
736
+ outline: none;
737
+ }
738
+
718
739
  .quantity-value::-webkit-inner-spin-button,
719
740
  .quantity-value::-webkit-outer-spin-button {
720
741
  appearance: none;
@@ -733,20 +754,26 @@
733
754
  justify-content: center;
734
755
  min-width: calc(var(--internal-spacing-lg) * 1.5 * 2 + 2.5rem);
735
756
  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);
757
+ padding: 0;
758
+ background: transparent;
759
+ border: none;
760
+ box-shadow: none;
739
761
  }
740
762
 
741
763
  .sold-out-controls,
742
764
  .sold-out-text {
743
- color: var(--internal-muted-foreground);
765
+ color: var(--internal-destructive);
744
766
  font-size: var(--internal-font-size-xs);
745
767
  font-weight: var(--internal-font-weight-medium);
746
768
  letter-spacing: 0.025em;
747
769
  text-transform: uppercase;
748
770
  }
749
771
 
772
+ .sold-out-controls.success,
773
+ .sold-out-controls.success .sold-out-text {
774
+ color: var(--internal-success);
775
+ }
776
+
750
777
  .checkout-actions {
751
778
  margin: var(--internal-spacing-lg) 0 0 0;
752
779
  width: 100%;
@@ -816,7 +843,7 @@
816
843
  background: var(--internal-secondary);
817
844
  color: var(--internal-foreground);
818
845
  border-radius: calc(var(--internal-radius) - 2px);
819
- font-size: var(--internal-font-size-sm);
846
+ font-size: var(--internal-font-size-xs);
820
847
  font-weight: var(--internal-font-weight-medium);
821
848
  cursor: pointer;
822
849
  transition: all var(--internal-transition-fast);
@@ -981,21 +981,21 @@ export class SessionsTicketing {
981
981
  previewItem: quoteItemsBySelection(this.previewQuote).get(key),
982
982
  });
983
983
  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)));
984
+ 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
985
  }
986
986
  renderAdmissionPrice(price) {
987
987
  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)))));
988
+ 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
989
  }
990
- return (h("span", { class: "admission-price" }, this.formatPriceValue(price.total)));
990
+ return (h("span", { class: "admission-price" }, this.formatPriceValue(price.base)));
991
991
  }
992
992
  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);
993
+ if (state.statusLabel) {
994
+ return (h("div", { class: `sold-out-controls ${state.statusTone ?? ""}` }, h("span", { class: "sold-out-text" }, state.statusLabel)));
997
995
  }
998
- return (h("div", { class: "sold-out-controls" }, h("span", { class: "sold-out-text" }, "Sold Out")));
996
+ if (mode === "door")
997
+ return null;
998
+ return this.renderQuantityControls(admission, sectionId, state.disabled, state.maxQuantity);
999
999
  }
1000
1000
  renderAdmission(admission, event, section, isSectionSoldOut = false, mode = "default") {
1001
1001
  const state = getAdmissionDisplayState({
@@ -1011,13 +1011,16 @@ export class SessionsTicketing {
1011
1011
  return `admission-card ${state.soldOut ? "sold-out" : ""} ${state.disabled && !state.soldOut ? "unavailable" : ""} ${section ? "" : "solo-admission"}`;
1012
1012
  }
1013
1013
  getAdmissionAriaLabel(admission, section, state) {
1014
- const status = this.admissionStatusText(admission, state.soldOut);
1014
+ const status = state.statusLabel ?? this.admissionStatusText(admission, state.soldOut);
1015
1015
  return admissionAriaLabel(admission, section, status);
1016
1016
  }
1017
1017
  admissionStatusText(admission, soldOut) {
1018
1018
  if (soldOut)
1019
1019
  return "Sold Out";
1020
- return this.formatPriceValue(priceTotal(admission.price));
1020
+ if ((admission.price.booking ?? 0) <= 0) {
1021
+ return this.formatPriceValue(admission.price.base);
1022
+ }
1023
+ return `${this.formatPriceValue(admission.price.base)} plus ${this.formatPriceValue(admission.price.booking)} booking fee`;
1021
1024
  }
1022
1025
  renderAdmissionHeader(admission) {
1023
1026
  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)));
@@ -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));
@@ -158,6 +158,92 @@ describe("session-services-ticketing logic", () => {
158
158
  expect(state.disabled).toBe(true);
159
159
  expect(state.label?.text).toContain("On sale");
160
160
  });
161
+ it("treats sold-out sale endings as sold out instead of a price label", () => {
162
+ const data = event({
163
+ admissions: [
164
+ admission({
165
+ saleEndsAt: pastEnd,
166
+ saleEndsBehavior: "SOLD_OUT",
167
+ }),
168
+ ],
169
+ });
170
+ const state = getAdmissionDisplayState({
171
+ admission: data.admissions[0],
172
+ event: data,
173
+ });
174
+ expect(state.disabled).toBe(true);
175
+ expect(state.label).toBeNull();
176
+ expect(state.soldOut).toBe(true);
177
+ expect(state.statusLabel).toBe("Sold Out");
178
+ expect(state.statusTone).toBe("destructive");
179
+ });
180
+ it("moves post-sale labels into admission status", () => {
181
+ const data = event({
182
+ admissions: [
183
+ admission({
184
+ saleEndsAt: pastEnd,
185
+ saleEndsBehavior: "SALE_ENDED",
186
+ }),
187
+ admission({
188
+ id: "adm_door",
189
+ saleEndsAt: pastEnd,
190
+ saleEndsBehavior: "DOOR",
191
+ }),
192
+ ],
193
+ });
194
+ const ended = getAdmissionDisplayState({
195
+ admission: data.admissions[0],
196
+ event: data,
197
+ });
198
+ const door = getAdmissionDisplayState({
199
+ admission: data.admissions[1],
200
+ event: data,
201
+ });
202
+ expect(ended.label).toBeNull();
203
+ expect(ended.statusLabel).toBe("Sale Ended");
204
+ expect(ended.statusTone).toBe("destructive");
205
+ expect(door.label).toBeNull();
206
+ expect(door.statusLabel).toBe("Available At Door");
207
+ expect(door.statusTone).toBe("success");
208
+ });
209
+ it("shows sold-out door admissions as available at door while online sales continue", () => {
210
+ const data = event({
211
+ admissions: [
212
+ admission({
213
+ id: "adm_first_release",
214
+ available: 0,
215
+ sold: 54,
216
+ capacity: 54,
217
+ order: 20,
218
+ saleEndsAt: futureEnd,
219
+ saleEndsBehavior: "DOOR",
220
+ }),
221
+ admission({
222
+ id: "adm_final_release",
223
+ available: 997,
224
+ sold: 3,
225
+ capacity: 1000,
226
+ order: 40,
227
+ saleEndsAt: futureEnd,
228
+ saleEndsBehavior: "DOOR",
229
+ }),
230
+ ],
231
+ });
232
+ const first = getAdmissionDisplayState({
233
+ admission: data.admissions[0],
234
+ event: data,
235
+ });
236
+ const final = getAdmissionDisplayState({
237
+ admission: data.admissions[1],
238
+ event: data,
239
+ });
240
+ expect(first.disabled).toBe(true);
241
+ expect(first.soldOut).toBe(true);
242
+ expect(first.statusLabel).toBe("Available At Door");
243
+ expect(first.statusTone).toBe("success");
244
+ expect(final.disabled).toBe(false);
245
+ expect(final.statusLabel).toBeNull();
246
+ });
161
247
  it("orders top-level sections and unsectioned admissions together", () => {
162
248
  const main = section({ id: "sec_main", order: 2 });
163
249
  const early = admission({ id: "adm_early", order: 1 });
@@ -185,12 +271,13 @@ describe("session-services-ticketing logic", () => {
185
271
  quoteItem: quoteItem(),
186
272
  });
187
273
  expect(price).toEqual({
274
+ base: 1000,
188
275
  discounted: true,
189
- gross: 2200,
190
- total: 1200,
276
+ fees: 200,
277
+ gross: 2000,
191
278
  });
192
279
  });
193
- it("uses quote item total pricing without discount styling", () => {
280
+ it("uses quote item base pricing without discount styling", () => {
194
281
  const price = getAdmissionPriceDisplay({
195
282
  admission: admission(),
196
283
  mode: "default",
@@ -208,19 +295,20 @@ describe("session-services-ticketing logic", () => {
208
295
  }),
209
296
  });
210
297
  expect(price).toEqual({
298
+ base: 2000,
211
299
  discounted: false,
212
- total: 2250,
300
+ fees: 200,
213
301
  });
214
302
  });
215
- it("includes booking fees in admission prices for every mode", () => {
303
+ it("separates admission base and booking fees for every mode", () => {
216
304
  expect(getAdmissionPriceDisplay({
217
305
  admission: admission(),
218
306
  mode: "default",
219
- }).total).toBe(2200);
307
+ })).toMatchObject({ base: 2000, fees: 200 });
220
308
  expect(getAdmissionPriceDisplay({
221
309
  admission: admission(),
222
310
  mode: "door",
223
- }).total).toBe(2200);
311
+ })).toMatchObject({ base: 2000, fees: 200 });
224
312
  });
225
313
  it("uses discounted initial preview pricing when available", () => {
226
314
  const price = getAdmissionPriceDisplay({
@@ -233,20 +321,23 @@ describe("session-services-ticketing logic", () => {
233
321
  mode: "default",
234
322
  });
235
323
  expect(price).toEqual({
324
+ base: 1000,
236
325
  discounted: true,
237
- gross: 2200,
238
- total: 1200,
326
+ fees: 200,
327
+ gross: 2000,
239
328
  });
240
329
  });
241
330
  it("calculates admission savings from discounted display prices", () => {
242
331
  expect(admissionPriceSavings({
332
+ base: 1000,
243
333
  discounted: true,
244
- gross: 2200,
245
- total: 1200,
334
+ fees: 200,
335
+ gross: 2000,
246
336
  })).toBe(1000);
247
337
  expect(admissionPriceSavings({
338
+ base: 2200,
248
339
  discounted: false,
249
- total: 2200,
340
+ fees: 200,
250
341
  })).toBe(0);
251
342
  });
252
343
  it("divides preview quote pricing by preview quantity", () => {
@@ -268,9 +359,10 @@ describe("session-services-ticketing logic", () => {
268
359
  }),
269
360
  });
270
361
  expect(price).toEqual({
362
+ base: 1000,
271
363
  discounted: true,
272
- gross: 2200,
273
- total: 1200,
364
+ fees: 200,
365
+ gross: 2000,
274
366
  });
275
367
  });
276
368
  });