@ticketlayer/elements 0.4.0 → 1.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (161) hide show
  1. package/dist/cjs/{index-I9CoYv3k.js → index-DanPUX0s.js} +1 -1
  2. package/dist/cjs/index.cjs.js +2 -1
  3. package/dist/cjs/loader.cjs.js +2 -2
  4. package/dist/cjs/ticket-selector-BYVL13k_.js +3623 -0
  5. package/dist/cjs/ticketlayer-elements.cjs.js +2 -2
  6. package/dist/cjs/tl-buy-modal.cjs.entry.js +3 -3
  7. package/dist/cjs/tl-buy-tickets-button.cjs.entry.js +89 -74
  8. package/dist/cjs/tl-buy-tickets-wrapper.cjs.entry.js +61 -30
  9. package/dist/cjs/tl-cart-badge.cjs.entry.js +74 -61
  10. package/dist/cjs/tl-cart-drawer.cjs.entry.js +51 -23
  11. package/dist/cjs/tl-cart.cjs.entry.js +71 -103
  12. package/dist/cjs/tl-checkout-wrapper.cjs.entry.js +51 -29
  13. package/dist/cjs/tl-event-card.cjs.entry.js +43 -105
  14. package/dist/cjs/tl-event-list.cjs.entry.js +72 -35
  15. package/dist/cjs/tl-login.cjs.entry.js +74 -141
  16. package/dist/cjs/tl-my-orders.cjs.entry.js +52 -92
  17. package/dist/cjs/tl-occurrence-selector_2.cjs.entry.js +153 -921
  18. package/dist/cjs/tl-order-confirmation.cjs.entry.js +71 -107
  19. package/dist/cjs/tl-order-tickets_2.cjs.entry.js +48 -74
  20. package/dist/cjs/tl-promo-code.cjs.entry.js +50 -47
  21. package/dist/cjs/tl-seat-map.cjs.entry.js +3 -3
  22. package/dist/cjs/web-query-Ce73jB4k.js +16 -0
  23. package/dist/collection/components/tl-buy-modal/tl-buy-modal.js +2 -2
  24. package/dist/collection/components/tl-buy-tickets-button/tl-buy-tickets-button.css +37 -0
  25. package/dist/collection/components/tl-buy-tickets-button/tl-buy-tickets-button.js +105 -79
  26. package/dist/collection/components/tl-buy-tickets-wrapper/tl-buy-tickets-wrapper.js +73 -30
  27. package/dist/collection/components/tl-cart/tl-cart.js +82 -107
  28. package/dist/collection/components/tl-cart-badge/tl-cart-badge.js +62 -62
  29. package/dist/collection/components/tl-cart-drawer/tl-cart-drawer.js +56 -24
  30. package/dist/collection/components/tl-checkout-wrapper/tl-checkout-wrapper.js +61 -29
  31. package/dist/collection/components/tl-event-card/event-card-adapter.js +11 -23
  32. package/dist/collection/components/tl-event-card/tl-event-card.js +51 -84
  33. package/dist/collection/components/tl-event-list/tl-event-list.js +82 -38
  34. package/dist/collection/components/tl-login/tl-login.js +83 -147
  35. package/dist/collection/components/tl-my-orders/tl-my-orders.js +58 -96
  36. package/dist/collection/components/tl-occurrence-selector/tl-occurrence-selector.js +137 -516
  37. package/dist/collection/components/tl-order-confirmation/tl-order-confirmation.js +81 -115
  38. package/dist/collection/components/tl-order-tickets/tl-order-tickets.js +57 -92
  39. package/dist/collection/components/tl-promo-code/tl-promo-code.js +55 -50
  40. package/dist/collection/components/tl-seat-map/tl-seat-map.js +2 -2
  41. package/dist/collection/components/tl-ticket-selector/tl-ticket-selector.js +34 -20
  42. package/dist/collection/utils/order-summary.js +15 -135
  43. package/dist/collection/utils/testing/deep-text.js +29 -0
  44. package/dist/collection/utils/testing/fixtures.js +290 -0
  45. package/dist/collection/utils/testing/mock-sdk.js +13 -2
  46. package/dist/collection/utils/web-query.js +12 -0
  47. package/dist/collection/utils/web-storage.js +22 -0
  48. package/dist/components/ticket-selector.js +1 -0
  49. package/dist/components/tl-buy-modal.js +1 -1
  50. package/dist/components/tl-buy-tickets-button.js +1 -1
  51. package/dist/components/tl-buy-tickets-wrapper2.js +1 -1
  52. package/dist/components/tl-cart-badge.js +1 -1
  53. package/dist/components/tl-cart-drawer.js +1 -1
  54. package/dist/components/tl-cart2.js +1 -1
  55. package/dist/components/tl-checkout-wrapper2.js +1 -1
  56. package/dist/components/tl-event-card2.js +1 -1
  57. package/dist/components/tl-event-list.js +1 -1
  58. package/dist/components/tl-login2.js +1 -1
  59. package/dist/components/tl-my-orders.js +1 -1
  60. package/dist/components/tl-occurrence-selector2.js +1 -1
  61. package/dist/components/tl-order-confirmation.js +1 -1
  62. package/dist/components/tl-order-tickets2.js +1 -1
  63. package/dist/components/tl-promo-code.js +1 -1
  64. package/dist/components/tl-seat-map.js +1 -1
  65. package/dist/components/tl-ticket-selector2.js +1 -1
  66. package/dist/components/web-query.js +1 -0
  67. package/dist/elements.docs.json +34 -34
  68. package/dist/esm/{index-OoYV1faY.js → index-imfxp765.js} +1 -1
  69. package/dist/esm/index.js +2 -1
  70. package/dist/esm/loader.js +3 -3
  71. package/dist/esm/ticket-selector-BwChnHVC.js +3606 -0
  72. package/dist/esm/ticketlayer-elements.js +3 -3
  73. package/dist/esm/tl-buy-modal.entry.js +3 -3
  74. package/dist/esm/tl-buy-tickets-button.entry.js +90 -75
  75. package/dist/esm/tl-buy-tickets-wrapper.entry.js +62 -31
  76. package/dist/esm/tl-cart-badge.entry.js +75 -62
  77. package/dist/esm/tl-cart-drawer.entry.js +52 -24
  78. package/dist/esm/tl-cart.entry.js +72 -104
  79. package/dist/esm/tl-checkout-wrapper.entry.js +52 -30
  80. package/dist/esm/tl-event-card.entry.js +44 -106
  81. package/dist/esm/tl-event-list.entry.js +73 -36
  82. package/dist/esm/tl-login.entry.js +75 -142
  83. package/dist/esm/tl-my-orders.entry.js +53 -93
  84. package/dist/esm/tl-occurrence-selector_2.entry.js +152 -920
  85. package/dist/esm/tl-order-confirmation.entry.js +72 -108
  86. package/dist/esm/tl-order-tickets_2.entry.js +49 -75
  87. package/dist/esm/tl-promo-code.entry.js +51 -48
  88. package/dist/esm/tl-seat-map.entry.js +3 -3
  89. package/dist/esm/web-query-Bdbb2lk5.js +14 -0
  90. package/dist/ticketlayer-elements/index.esm.js +1 -1
  91. package/dist/ticketlayer-elements/p-45923d2c.entry.js +1 -0
  92. package/dist/ticketlayer-elements/p-492adc77.entry.js +1 -0
  93. package/dist/ticketlayer-elements/p-4cfc9e53.entry.js +1 -0
  94. package/dist/ticketlayer-elements/{p-ac264adc.entry.js → p-5135630e.entry.js} +1 -1
  95. package/dist/ticketlayer-elements/p-5bbce3b5.entry.js +1 -0
  96. package/dist/ticketlayer-elements/p-7d547721.entry.js +1 -0
  97. package/dist/ticketlayer-elements/p-7fe7b6ba.entry.js +1 -0
  98. package/dist/ticketlayer-elements/p-8122f0c1.entry.js +1 -0
  99. package/dist/ticketlayer-elements/p-98d40869.entry.js +1 -0
  100. package/dist/ticketlayer-elements/p-Bdbb2lk5.js +1 -0
  101. package/dist/ticketlayer-elements/p-BwChnHVC.js +1 -0
  102. package/dist/ticketlayer-elements/p-abec290a.entry.js +1 -0
  103. package/dist/ticketlayer-elements/p-ac79a3f6.entry.js +1 -0
  104. package/dist/ticketlayer-elements/p-d01f4429.entry.js +1 -0
  105. package/dist/ticketlayer-elements/p-d7ffaf46.entry.js +1 -0
  106. package/dist/ticketlayer-elements/p-e7170f3c.entry.js +1 -0
  107. package/dist/ticketlayer-elements/p-e787e862.entry.js +1 -0
  108. package/dist/ticketlayer-elements/p-f1ff931f.entry.js +1 -0
  109. package/dist/ticketlayer-elements/p-imfxp765.js +3 -0
  110. package/dist/ticketlayer-elements/ticketlayer-elements.css +1 -1
  111. package/dist/ticketlayer-elements/ticketlayer-elements.esm.js +1 -1
  112. package/dist/types/components/tl-buy-tickets-button/tl-buy-tickets-button.d.ts +40 -8
  113. package/dist/types/components/tl-buy-tickets-wrapper/tl-buy-tickets-wrapper.d.ts +20 -1
  114. package/dist/types/components/tl-cart/tl-cart.d.ts +40 -42
  115. package/dist/types/components/tl-cart-badge/tl-cart-badge.d.ts +22 -7
  116. package/dist/types/components/tl-cart-drawer/tl-cart-drawer.d.ts +28 -5
  117. package/dist/types/components/tl-checkout-wrapper/tl-checkout-wrapper.d.ts +18 -1
  118. package/dist/types/components/tl-event-card/event-card-adapter.d.ts +9 -20
  119. package/dist/types/components/tl-event-card/tl-event-card.d.ts +25 -28
  120. package/dist/types/components/tl-event-list/tl-event-list.d.ts +46 -6
  121. package/dist/types/components/tl-login/tl-login.d.ts +68 -22
  122. package/dist/types/components/tl-my-orders/tl-my-orders.d.ts +30 -12
  123. package/dist/types/components/tl-occurrence-selector/tl-occurrence-selector.d.ts +73 -161
  124. package/dist/types/components/tl-order-confirmation/tl-order-confirmation.d.ts +48 -15
  125. package/dist/types/components/tl-order-tickets/tl-order-tickets.d.ts +29 -31
  126. package/dist/types/components/tl-promo-code/tl-promo-code.d.ts +28 -10
  127. package/dist/types/components/tl-ticket/tl-ticket.d.ts +2 -11
  128. package/dist/types/components/tl-ticket-selector/tl-ticket-selector.d.ts +22 -4
  129. package/dist/types/components.d.ts +270 -62
  130. package/dist/types/utils/order-summary.d.ts +14 -97
  131. package/dist/types/utils/testing/deep-text.d.ts +13 -0
  132. package/dist/types/utils/testing/fixtures.d.ts +225 -0
  133. package/dist/types/utils/testing/mock-sdk.d.ts +14 -1
  134. package/dist/types/utils/web-query.d.ts +18 -0
  135. package/dist/types/utils/web-storage.d.ts +16 -0
  136. package/elements.catalogue.json +602 -50
  137. package/package.json +8 -6
  138. package/dist/cjs/money-BtmM2gt-.js +0 -118
  139. package/dist/cjs/order-summary-X3TlZxc2.js +0 -150
  140. package/dist/components/money.js +0 -1
  141. package/dist/components/order-summary.js +0 -1
  142. package/dist/esm/money-4ig0-WB6.js +0 -112
  143. package/dist/esm/order-summary-DiDtvfZM.js +0 -136
  144. package/dist/ticketlayer-elements/p-0814420f.entry.js +0 -1
  145. package/dist/ticketlayer-elements/p-0acabc26.entry.js +0 -1
  146. package/dist/ticketlayer-elements/p-109a3feb.entry.js +0 -1
  147. package/dist/ticketlayer-elements/p-1a4ba71c.entry.js +0 -1
  148. package/dist/ticketlayer-elements/p-318965c1.entry.js +0 -1
  149. package/dist/ticketlayer-elements/p-347c7fd1.entry.js +0 -1
  150. package/dist/ticketlayer-elements/p-3649c26d.entry.js +0 -1
  151. package/dist/ticketlayer-elements/p-40260cc9.entry.js +0 -1
  152. package/dist/ticketlayer-elements/p-4ig0-WB6.js +0 -1
  153. package/dist/ticketlayer-elements/p-55fd2f9f.entry.js +0 -1
  154. package/dist/ticketlayer-elements/p-62ee659b.entry.js +0 -1
  155. package/dist/ticketlayer-elements/p-7d4ecc2b.entry.js +0 -1
  156. package/dist/ticketlayer-elements/p-7f0abf95.entry.js +0 -1
  157. package/dist/ticketlayer-elements/p-BXpVGW51.js +0 -1
  158. package/dist/ticketlayer-elements/p-OoYV1faY.js +0 -3
  159. package/dist/ticketlayer-elements/p-a3c1b26c.entry.js +0 -1
  160. package/dist/ticketlayer-elements/p-aeb4c6fb.entry.js +0 -1
  161. package/dist/ticketlayer-elements/p-ba62541f.entry.js +0 -1
@@ -1,6 +1,15 @@
1
1
  import { h } from "@stencil/core";
2
- import { getSDK, hasSDK, whenSDK } from "../../utils/sdk-connector";
3
- import { cartTotals } from "../../utils/cart-totals";
2
+ import { cartCore, createElement, } from "@ticketlayer/elements-core";
3
+ import { getSDK, whenSDK } from "../../utils/sdk-connector";
4
+ /**
5
+ * The web harness for `cartCore`.
6
+ *
7
+ * It renders and it forwards. The cart, the money rows, the subscription that
8
+ * keeps them current and every string are in `@ticketlayer/elements-core`
9
+ * (AGENTS.md, TKT-11); what is left here is the Stencil surface a host writes
10
+ * HTML against and the one genuinely web thing, finding the client the page
11
+ * published on `window.Ticketlayer`.
12
+ */
4
13
  export class TlCart {
5
14
  constructor() {
6
15
  /**
@@ -19,124 +28,78 @@ export class TlCart {
19
28
  * Compact mode - hides some details
20
29
  */
21
30
  this.compact = false;
22
- this.cart = null;
23
- this.loading = true;
24
- this.error = null;
25
- this.removingItemId = null;
26
- this.handleCheckout = () => {
27
- if (this.cart && this.cart.items.length > 0) {
28
- this.tlCheckout.emit({ cart: this.cart });
29
- }
30
- };
31
- }
32
- componentWillLoad() {
33
- this.loadCart();
34
- }
35
- componentDidLoad() {
36
- this.subscribeToCartUpdates();
37
- }
38
- disconnectedCallback() {
39
- if (this.unsubscribe) {
40
- this.unsubscribe();
41
- }
42
- }
43
- async loadCart() {
44
- try {
45
- console.log('[tl-cart] Loading cart...');
46
- this.loading = true;
47
- this.error = null;
48
- const sdk = await whenSDK();
49
- const cartManager = sdk.getCartManager?.();
50
- if (cartManager) {
51
- const cartData = await cartManager.get();
52
- console.log('[tl-cart] Cart loaded:', cartData);
53
- this.cart = cartData;
54
- }
55
- }
56
- catch (err) {
57
- console.error('[tl-cart] Failed to load cart:', err);
58
- this.error = err instanceof Error ? err.message : 'Failed to load cart';
59
- this.tlError.emit({ message: this.error });
60
- }
61
- finally {
62
- this.loading = false;
63
- }
31
+ this.element = createElement(cartCore, {
32
+ client: () => getSDK(),
33
+ // What "the client is up" means on the web. The core waits on it in its
34
+ // own mount, subscribes there and loads there; the harness decides none
35
+ // of it (TKT-197).
36
+ clientReady: () => whenSDK(),
37
+ emit: (event, detail) => this.forward(event, detail),
38
+ });
39
+ /** The core's whole view. Assigning it is what makes Stencil re-render. */
40
+ this.view = this.element.view;
64
41
  }
65
- async subscribeToCartUpdates() {
66
- const sdk = await whenSDK().catch(() => null);
67
- if (!sdk)
68
- return;
69
- const cartManager = sdk.getCartManager?.();
70
- if (!cartManager)
71
- return;
72
- this.unsubscribe = cartManager.on?.('cart:updated', (cartData) => {
73
- console.log('[tl-cart] Cart updated:', cartData);
74
- this.cart = cartData;
75
- this.tlCartUpdated.emit({ cart: cartData });
42
+ syncProps() {
43
+ this.element.setProps({
44
+ showCheckoutButton: this.showCheckoutButton,
45
+ checkoutLabel: this.checkoutLabel,
46
+ showRemoveButtons: this.showRemoveButtons,
47
+ compact: this.compact,
76
48
  });
77
49
  }
78
- async handleRemoveItem(itemId) {
79
- if (!hasSDK())
80
- return;
81
- this.removingItemId = itemId;
82
- try {
83
- const sdk = getSDK();
84
- const cartManager = sdk.getCartManager?.();
85
- if (cartManager) {
86
- await cartManager.removeItem(itemId);
87
- console.log('[tl-cart] Item removed:', itemId);
88
- this.tlItemRemoved.emit({ itemId });
89
- }
90
- }
91
- catch (err) {
92
- console.error('[tl-cart] Failed to remove item:', err);
93
- this.error = err instanceof Error ? err.message : 'Failed to remove item';
94
- this.tlError.emit({ message: this.error });
95
- }
96
- finally {
97
- this.removingItemId = null;
98
- }
50
+ /**
51
+ * On screen: render what the core says and put the core to work. For this
52
+ * element that means waiting for the client, subscribing to `cart:updated`
53
+ * and reading the cart - all of it the core's business.
54
+ */
55
+ connectedCallback() {
56
+ this.syncProps();
57
+ this.unsubscribe ?? (this.unsubscribe = this.element.subscribe((view) => {
58
+ this.view = view;
59
+ }));
60
+ this.view = this.element.view;
61
+ this.teardown = this.element.mount();
99
62
  }
100
- formatPrice(priceInPence, currency = 'GBP') {
101
- return new Intl.NumberFormat('en-GB', {
102
- style: 'currency',
103
- currency,
104
- }).format(priceInPence / 100);
63
+ /**
64
+ * Off screen: run the mount's teardown, which is what drops the cart
65
+ * subscription. Not `destroy()`: Stencil calls this for a DOM move as well
66
+ * as a removal, and a destroyed core never mounts again.
67
+ */
68
+ disconnectedCallback() {
69
+ this.teardown?.();
70
+ this.teardown = undefined;
71
+ this.unsubscribe?.();
72
+ this.unsubscribe = undefined;
105
73
  }
106
- formatDate(dateString) {
107
- const date = new Date(dateString);
108
- return date.toLocaleDateString('en-GB', {
109
- weekday: 'short',
110
- day: 'numeric',
111
- month: 'short',
112
- hour: '2-digit',
113
- minute: '2-digit',
114
- });
74
+ forward(event, detail) {
75
+ if (event === 'tlCheckout')
76
+ this.tlCheckout.emit(detail);
77
+ else if (event === 'tlItemRemoved')
78
+ this.tlItemRemoved.emit(detail);
79
+ else if (event === 'tlCartUpdated')
80
+ this.tlCartUpdated.emit(detail);
81
+ else if (event === 'tlError')
82
+ this.tlError.emit(detail);
115
83
  }
116
84
  renderSkeleton() {
117
85
  return (h("div", { class: "skeleton-container" }, h("div", { class: "skeleton-item" }), h("div", { class: "skeleton-item" }), h("div", { class: "skeleton-totals" })));
118
86
  }
119
87
  renderEmptyCart() {
120
- return (h("div", { class: "empty-cart" }, h("svg", { class: "empty-icon", viewBox: "0 0 24 24", fill: "none", stroke: "currentColor" }, h("path", { "stroke-linecap": "round", "stroke-linejoin": "round", "stroke-width": "1.5", d: "M3 3h2l.4 2M7 13h10l4-8H5.4M7 13L5.4 5M7 13l-2.293 2.293c-.63.63-.184 1.707.707 1.707H17m0 0a2 2 0 100 4 2 2 0 000-4zm-8 2a2 2 0 11-4 0 2 2 0 014 0z" })), h("p", { class: "empty-title" }, "Your cart is empty"), h("p", { class: "empty-message" }, "Add tickets to get started")));
88
+ const strings = this.view.strings;
89
+ return (h("div", { class: "empty-cart" }, h("svg", { class: "empty-icon", viewBox: "0 0 24 24", fill: "none", stroke: "currentColor" }, h("path", { "stroke-linecap": "round", "stroke-linejoin": "round", "stroke-width": "1.5", d: "M3 3h2l.4 2M7 13h10l4-8H5.4M7 13L5.4 5M7 13l-2.293 2.293c-.63.63-.184 1.707.707 1.707H17m0 0a2 2 0 100 4 2 2 0 000-4zm-8 2a2 2 0 11-4 0 2 2 0 014 0z" })), h("p", { class: "empty-title" }, strings.emptyTitle), h("p", { class: "empty-message" }, strings.emptyMessage)));
121
90
  }
122
- renderCartItem(item) {
123
- const isRemoving = this.removingItemId === item.id;
124
- return (h("div", { class: `cart-item ${isRemoving ? 'removing' : ''}`, key: item.id }, h("div", { class: "item-info" }, h("p", { class: "item-name" }, item.ticketTypeName), !this.compact && (h("p", { class: "item-event" }, item.eventName)), !this.compact && item.occurrenceDate && (h("p", { class: "item-date" }, this.formatDate(item.occurrenceDate))), h("p", { class: "item-quantity" }, "Qty: ", item.quantity)), h("div", { class: "item-actions" }, h("p", { class: "item-price" }, this.formatPrice(item.totalPrice, item.currency)), this.showRemoveButtons && (h("button", { class: "remove-btn", onClick: () => this.handleRemoveItem(item.id), disabled: isRemoving, "aria-label": "Remove item" }, isRemoving ? (h("span", { class: "spinner" })) : (h("svg", { viewBox: "0 0 24 24", fill: "none", stroke: "currentColor" }, h("path", { "stroke-linecap": "round", "stroke-linejoin": "round", "stroke-width": "2", d: "M6 18L18 6M6 6l12 12" }))))))));
91
+ renderCartItem(row) {
92
+ return (h("div", { class: `cart-item ${row.removing ? 'removing' : ''}`, key: row.id }, h("div", { class: "item-info" }, h("p", { class: "item-name" }, row.name), row.showEvent && h("p", { class: "item-event" }, row.eventName), row.showDate && h("p", { class: "item-date" }, row.dateLabel), h("p", { class: "item-quantity" }, row.quantityLabel)), h("div", { class: "item-actions" }, h("p", { class: "item-price" }, row.priceLabel), this.showRemoveButtons && (h("button", { class: "remove-btn", onClick: () => this.element.actions.removeItem(row.id), disabled: row.removing, "aria-label": this.view.strings.removeItem }, row.removing ? (h("span", { class: "spinner" })) : (h("svg", { viewBox: "0 0 24 24", fill: "none", stroke: "currentColor" }, h("path", { "stroke-linecap": "round", "stroke-linejoin": "round", "stroke-width": "2", d: "M6 18L18 6M6 6l12 12" }))))))));
125
93
  }
126
94
  render() {
127
- if (this.loading) {
95
+ const { status, strings, derived } = this.view;
96
+ if (status === 'loading') {
128
97
  return this.renderSkeleton();
129
98
  }
130
- if (!this.cart || this.cart.items.length === 0) {
99
+ if (status === 'empty') {
131
100
  return this.renderEmptyCart();
132
101
  }
133
- const { items } = this.cart;
134
- // Money comes from the response through cartTotals: the fee row is shown
135
- // when the surface expressed a fee, and the total is the server's figure,
136
- // never a sum taken here.
137
- const totals = cartTotals(this.cart);
138
- const currency = totals.currency;
139
- return (h("div", { class: `cart ${this.compact ? 'compact' : ''}` }, h("div", { class: "cart-items" }, items.map(item => this.renderCartItem(item))), h("div", { class: "cart-totals" }, !this.compact && (h("div", { class: "total-row subtotal" }, h("span", null, "Subtotal"), h("span", null, this.formatPrice(totals.subtotal, currency)))), totals.showFee && !this.compact && (h("div", { class: "total-row fees" }, h("span", null, totals.feeLabel), h("span", null, this.formatPrice(totals.fees, currency)))), totals.discount > 0 && !this.compact && (h("div", { class: "total-row discount" }, h("span", null, "Discount"), h("span", null, "-", this.formatPrice(totals.discount, currency)))), h("div", { class: "total-row total" }, h("span", null, "Total"), h("span", null, this.formatPrice(totals.total, currency)))), this.showCheckoutButton && (h("button", { class: "checkout-btn", onClick: this.handleCheckout, disabled: items.length === 0 }, this.checkoutLabel))));
102
+ return (h("div", { class: `cart ${this.compact ? 'compact' : ''}` }, h("div", { class: "cart-items" }, derived.rows.map((row) => this.renderCartItem(row))), h("div", { class: "cart-totals" }, derived.showSubtotalRow && (h("div", { class: "total-row subtotal" }, h("span", null, strings.subtotalRow), h("span", null, derived.subtotalLabel))), derived.showFeeRow && (h("div", { class: "total-row fees" }, h("span", null, derived.totals.feeLabel), h("span", null, derived.feeAmountLabel))), derived.showDiscountRow && (h("div", { class: "total-row discount" }, h("span", null, strings.discountRow), h("span", null, "-", derived.discountLabel))), h("div", { class: "total-row total" }, h("span", null, strings.totalRow), h("span", null, derived.totalLabel))), this.showCheckoutButton && (h("button", { class: "checkout-btn", onClick: () => this.element.actions.checkout(), disabled: derived.checkoutDisabled }, this.checkoutLabel))));
140
103
  }
141
104
  static get is() { return "tl-cart"; }
142
105
  static get encapsulation() { return "shadow"; }
@@ -236,10 +199,7 @@ export class TlCart {
236
199
  }
237
200
  static get states() {
238
201
  return {
239
- "cart": {},
240
- "loading": {},
241
- "error": {},
242
- "removingItemId": {}
202
+ "view": {}
243
203
  };
244
204
  }
245
205
  static get events() {
@@ -317,4 +277,19 @@ export class TlCart {
317
277
  }
318
278
  }];
319
279
  }
280
+ static get watchers() {
281
+ return [{
282
+ "propName": "showCheckoutButton",
283
+ "methodName": "syncProps"
284
+ }, {
285
+ "propName": "checkoutLabel",
286
+ "methodName": "syncProps"
287
+ }, {
288
+ "propName": "showRemoveButtons",
289
+ "methodName": "syncProps"
290
+ }, {
291
+ "propName": "compact",
292
+ "methodName": "syncProps"
293
+ }];
294
+ }
320
295
  }
@@ -1,7 +1,15 @@
1
1
  import { h } from "@stencil/core";
2
- import { whenSDK } from "../../utils/sdk-connector";
3
- // Storage key for persisted cart count
4
- const CART_COUNT_KEY = 'tl_cart_count';
2
+ import { cartBadgeCore, createElement, } from "@ticketlayer/elements-core";
3
+ import { getSDK, whenSDK } from "../../utils/sdk-connector";
4
+ import { webStorage } from "../../utils/web-storage";
5
+ /**
6
+ * The web harness for `cartBadgeCore`.
7
+ *
8
+ * The count, the "99+" rule, the cart subscription and the remembered value
9
+ * are all in `@ticketlayer/elements-core`. What is here is the Stencil surface,
10
+ * the icon, and the two web things the core is handed: the client the page
11
+ * published and `localStorage`.
12
+ */
5
13
  export class TlCartBadge {
6
14
  constructor() {
7
15
  /**
@@ -16,66 +24,44 @@ export class TlCartBadge {
16
24
  * Badge only mode - only shows the count bubble, no icon or button
17
25
  */
18
26
  this.badgeOnly = false;
19
- this.itemCount = 0;
27
+ this.element = createElement(cartBadgeCore, {
28
+ client: () => getSDK(),
29
+ clientReady: () => whenSDK(),
30
+ storage: webStorage,
31
+ emit: (event, detail) => this.forward(event, detail),
32
+ });
33
+ /** The core's whole view. Assigning it is what makes Stencil re-render. */
34
+ this.view = this.element.view;
20
35
  this.handleClick = () => {
21
- this.tlClick.emit({ itemCount: this.itemCount });
36
+ this.element.actions.click();
22
37
  };
23
38
  }
24
- componentWillLoad() {
25
- // Initialize with persisted count to prevent FOUC
26
- this.itemCount = this.getPersistedCartCount();
39
+ syncProps() {
40
+ this.element.setProps({
41
+ showEmpty: this.showEmpty,
42
+ icon: this.icon,
43
+ size: this.size,
44
+ badgeOnly: this.badgeOnly,
45
+ });
27
46
  }
28
- componentDidLoad() {
29
- this.subscribeToCartUpdates();
47
+ connectedCallback() {
48
+ this.syncProps();
49
+ this.unsubscribe ?? (this.unsubscribe = this.element.subscribe((view) => {
50
+ this.view = view;
51
+ }));
52
+ this.view = this.element.view;
53
+ this.teardown = this.element.mount();
30
54
  }
55
+ /** Off screen: the mount's teardown is what drops the cart subscription. */
31
56
  disconnectedCallback() {
32
- if (this.unsubscribe) {
33
- this.unsubscribe();
34
- }
35
- }
36
- getPersistedCartCount() {
37
- if (typeof window === 'undefined')
38
- return 0;
39
- try {
40
- const stored = localStorage.getItem(CART_COUNT_KEY);
41
- return stored ? parseInt(stored, 10) || 0 : 0;
42
- }
43
- catch {
44
- return 0;
45
- }
46
- }
47
- persistCartCount(count) {
48
- if (typeof window === 'undefined')
49
- return;
50
- try {
51
- localStorage.setItem(CART_COUNT_KEY, count.toString());
52
- }
53
- catch {
54
- // localStorage may be unavailable
55
- }
57
+ this.teardown?.();
58
+ this.teardown = undefined;
59
+ this.unsubscribe?.();
60
+ this.unsubscribe = undefined;
56
61
  }
57
- async subscribeToCartUpdates() {
58
- const sdk = await whenSDK().catch(() => null);
59
- if (!sdk)
60
- return;
61
- const cartManager = sdk.getCartManager?.();
62
- if (!cartManager)
63
- return;
64
- // Subscribe to cart updates
65
- this.unsubscribe = cartManager.on?.('cart:updated', (cart) => {
66
- const newCount = cart?.itemCount || 0;
67
- console.log('[tl-cart-badge] Cart updated:', newCount);
68
- this.itemCount = newCount;
69
- this.persistCartCount(newCount);
70
- });
71
- // Load initial cart
72
- cartManager.get?.().then((cart) => {
73
- const newCount = cart?.itemCount || 0;
74
- this.itemCount = newCount;
75
- this.persistCartCount(newCount);
76
- }).catch(() => {
77
- // Cart may not exist yet - keep persisted value
78
- });
62
+ forward(event, detail) {
63
+ if (event === 'tlClick')
64
+ this.tlClick.emit(detail);
79
65
  }
80
66
  renderIcon() {
81
67
  if (this.icon) {
@@ -84,17 +70,16 @@ export class TlCartBadge {
84
70
  return (h("svg", { class: "icon-svg", viewBox: "0 0 24 24", fill: "none", stroke: "currentColor" }, h("path", { "stroke-linecap": "round", "stroke-linejoin": "round", "stroke-width": "2", d: "M3 3h2l.4 2M7 13h10l4-8H5.4M7 13L5.4 5M7 13l-2.293 2.293c-.63.63-.184 1.707.707 1.707H17m0 0a2 2 0 100 4 2 2 0 000-4zm-8 2a2 2 0 11-4 0 2 2 0 014 0z" })));
85
71
  }
86
72
  render() {
87
- const showBadge = this.itemCount > 0 || this.showEmpty;
88
- const displayCount = this.itemCount > 99 ? '99+' : this.itemCount.toString();
73
+ const { derived } = this.view;
89
74
  // Badge only mode - just show the count bubble
90
75
  if (this.badgeOnly) {
91
- if (this.itemCount === 0) {
76
+ if (derived.renderNothing) {
92
77
  return null;
93
78
  }
94
- return (h("span", { class: `badge-only size-${this.size}` }, displayCount));
79
+ return h("span", { class: `badge-only ${derived.sizeClass}` }, derived.displayCount);
95
80
  }
96
81
  // Full mode with icon and badge
97
- return (h("button", { class: `cart-badge size-${this.size}`, onClick: this.handleClick, "aria-label": `Cart with ${this.itemCount} items` }, this.renderIcon(), showBadge && this.itemCount > 0 && (h("span", { class: "badge-count" }, displayCount))));
82
+ return (h("button", { class: `cart-badge ${derived.sizeClass}`, onClick: this.handleClick, "aria-label": derived.ariaLabel }, this.renderIcon(), derived.showBadge && h("span", { class: "badge-count" }, derived.displayCount)));
98
83
  }
99
84
  static get is() { return "tl-cart-badge"; }
100
85
  static get encapsulation() { return "shadow"; }
@@ -193,7 +178,7 @@ export class TlCartBadge {
193
178
  }
194
179
  static get states() {
195
180
  return {
196
- "itemCount": {}
181
+ "view": {}
197
182
  };
198
183
  }
199
184
  static get events() {
@@ -214,4 +199,19 @@ export class TlCartBadge {
214
199
  }
215
200
  }];
216
201
  }
202
+ static get watchers() {
203
+ return [{
204
+ "propName": "showEmpty",
205
+ "methodName": "syncProps"
206
+ }, {
207
+ "propName": "icon",
208
+ "methodName": "syncProps"
209
+ }, {
210
+ "propName": "size",
211
+ "methodName": "syncProps"
212
+ }, {
213
+ "propName": "badgeOnly",
214
+ "methodName": "syncProps"
215
+ }];
216
+ }
217
217
  }
@@ -1,5 +1,6 @@
1
1
  import { h, Host } from "@stencil/core";
2
- import { onSDKEvent } from "../../utils/sdk-connector";
2
+ import { cartDrawerCore, createElement, } from "@ticketlayer/elements-core";
3
+ import { getSDK, whenSDK } from "../../utils/sdk-connector";
3
4
  /**
4
5
  * @component tl-cart-drawer
5
6
  *
@@ -10,6 +11,10 @@ import { onSDKEvent } from "../../utils/sdk-connector";
10
11
  * so the actual cart rendering / remove / checkout is shared with the inline
11
12
  * cart; the drawer only adds the off-canvas chrome.
12
13
  *
14
+ * The web harness for `cartDrawerCore`: the open/closed rule and the two
15
+ * intents are the core's (TKT-11). What stays here is the chrome and the one
16
+ * thing only a browser has, the Escape key.
17
+ *
13
18
  * @prop side - which edge the panel slides from (default 'right')
14
19
  * @event tlOpen - the drawer opened
15
20
  * @event tlClose - the drawer closed
@@ -22,45 +27,66 @@ export class TlCartDrawer {
22
27
  this.heading = 'Your cart';
23
28
  /** Controlled open state (also driven by the `ui:cart-requested` intent). */
24
29
  this.open = false;
25
- this.visible = false;
30
+ this.element = createElement(cartDrawerCore, {
31
+ client: () => getSDK(),
32
+ clientReady: () => whenSDK(),
33
+ emit: (event, detail) => this.forward(event, detail),
34
+ });
35
+ /** The core's whole view. Assigning it is what makes Stencil re-render. */
36
+ this.view = this.element.view;
37
+ /** The one thing only a browser has. What Escape means is the core's answer. */
26
38
  this.onKey = (e) => {
27
- if (e.key === 'Escape' && this.visible)
28
- this.setOpen(false);
39
+ if (e.key === 'Escape')
40
+ this.element.actions.setOpen(false);
29
41
  };
30
- this.close = () => this.setOpen(false);
42
+ this.close = () => this.element.actions.setOpen(false);
43
+ }
44
+ syncProps() {
45
+ this.element.setProps({ side: this.side, heading: this.heading, open: this.open });
46
+ }
47
+ /**
48
+ * The host moved `open`. The core follows it without announcing it, which is
49
+ * what the component's watcher did: an event here would be the host hearing
50
+ * its own write back.
51
+ */
52
+ onOpenProp(v) {
53
+ this.element.setProps({ open: v });
54
+ this.element.actions.openChanged(v);
31
55
  }
32
56
  connectedCallback() {
33
- this.visible = this.open;
34
- // Open on the cart intent; close on the shared modal-close intent.
35
- this.offCart = onSDKEvent('ui:cart-requested', () => this.setOpen(true));
36
- this.offClose = onSDKEvent('ui:modal-close-requested', () => this.setOpen(false));
57
+ this.syncProps();
58
+ this.unsubscribe ?? (this.unsubscribe = this.element.subscribe((view) => {
59
+ this.view = view;
60
+ // Reflect the core's state back onto the prop a host reads. Writing it
61
+ // re-enters onOpenProp, which is a no-op once the core already agrees.
62
+ if (this.open !== view.state.visible)
63
+ this.open = view.state.visible;
64
+ }));
65
+ this.view = this.element.view;
66
+ this.teardown = this.element.mount();
37
67
  if (typeof document !== 'undefined')
38
68
  document.addEventListener('keydown', this.onKey);
39
69
  }
40
70
  disconnectedCallback() {
41
- this.offCart?.();
42
- this.offClose?.();
71
+ this.teardown?.();
72
+ this.teardown = undefined;
73
+ this.unsubscribe?.();
74
+ this.unsubscribe = undefined;
43
75
  if (typeof document !== 'undefined')
44
76
  document.removeEventListener('keydown', this.onKey);
45
77
  }
46
- onOpenProp(v) {
47
- this.visible = v;
48
- }
49
- setOpen(v) {
50
- if (this.visible === v)
51
- return;
52
- this.open = v;
53
- this.visible = v;
54
- if (v)
78
+ forward(event, _detail) {
79
+ if (event === 'tlOpen')
55
80
  this.tlOpen.emit();
56
- else
81
+ else if (event === 'tlClose')
57
82
  this.tlClose.emit();
58
83
  }
59
84
  render() {
60
- return (h(Host, { key: '022314d9fb612e930da52ece34f2ab8ae8ce44da' }, h("div", { key: '985b4001145d72ba91ef7d466adba8ab939e9cc4', class: { overlay: true, 'is-open': this.visible }, onClick: (e) => {
85
+ const { strings, derived } = this.view;
86
+ return (h(Host, { key: 'ab41f3557cd8dd2b182bbfc3ed631bca7839d4a9' }, h("div", { key: '70bbe63d1f9a2d449cb9ff970631c0c69a2eb757', class: { overlay: true, 'is-open': derived.visible }, onClick: (e) => {
61
87
  if (e.target === e.currentTarget)
62
88
  this.close();
63
- } }, h("aside", { key: '3ac3588ae8feacd4f8bb96bdae8ef71e80ecb4b3', class: { panel: true, [`panel--${this.side}`]: true, 'is-open': this.visible }, role: "dialog", "aria-modal": "true", "aria-label": this.heading, "aria-hidden": this.visible ? 'false' : 'true' }, h("header", { key: 'e0126d634e952ebf56b73e54545f6c3981210827', class: "drawer-header" }, h("h2", { key: 'a72b23114aa836076ec59cd4a5d1438d228179d9', class: "drawer-title" }, this.heading), h("button", { key: '92fb5b159490440352a6c23d65aa6e8c956ae3d8', class: "close-btn", onClick: this.close, "aria-label": "Close cart" }, h("svg", { key: '138f4dc7669e62fb667091da0ba26dfbe94327c6', viewBox: "0 0 24 24", fill: "none", stroke: "currentColor", "stroke-width": "2" }, h("path", { key: '21fedd9dc89937a92f9c1124d4fa834b55908f1e', d: "M6 6l12 12M18 6L6 18", "stroke-linecap": "round" })))), h("div", { key: 'ebee87cb6a3c98dbf813d0a903a9cb4da2c696f3', class: "drawer-body" }, h("tl-cart", { key: '323db3a160837b68ffaa767e647ec67b06fa53b0' })), h("footer", { key: 'd35543373eb13ad1acc46ae50aefcb5a04f305a1', class: "drawer-footer" }, h("div", { key: '52229c74d39471bf186c965b6a53b38f79ce8d61', class: "pb-links" }, h("a", { key: 'd0430bf93d920c57fc42043faed7e8fd895855aa', href: "https://ticketlayer.com/privacy", target: "_blank", rel: "noopener noreferrer" }, "Privacy Policy"), h("a", { key: '2a75dc8dcdc9ec3010fe83f97d903cdc32767e49', href: "https://ticketlayer.com/report-abuse", target: "_blank", rel: "noopener noreferrer" }, "Report Abuse")), h("a", { key: '40642c5c4c003a454dd7ab2b66ebe5948a03df7d', class: "pb-brand", href: "https://ticketlayer.com", target: "_blank", rel: "noopener noreferrer" }, h("span", { key: 'a5d9b5c023381d25b6aaddf61e56dbdb21580975', class: "pb-pre" }, "Powered by"), h("svg", { key: '35bc23af9a35272d7ada24fd66d5f12f1d951ae8', width: "13", height: "13", viewBox: "0 0 50 50", fill: "none", "aria-hidden": "true" }, h("g", { key: 'fcca5f9bbc13d2781ea65bb907c9c07516972e28', transform: "matrix(0.0129 0 0 -0.0129 25.4752 25.4752)" }, h("path", { key: '7bf2071ba8d6d14c9ac9fc7c7a5a36a8008136e8', fill: "currentColor", transform: "translate(-1781.5, -1759.4003)", d: "M 41 2973 c 1 -285 5 -734 9 -998 c 7 -444 9 -488 30 -580 c 81 -364 235 -634 512 -900 c 135 -130 263 -220 423 -299 c 156 -77 249 -109 409 -143 c 110 -23 142 -25 354 -24 c 230 1 310 8 427 37 c 241 60 453 162 640 309 c 82 64 252 234 317 315 c 210 264 340 604 355 926 l 6 121 l -69 6 c -86 8 -706 9 -755 1 l -36 -6 l -21 -125 c -34 -191 -98 -317 -236 -462 c -111 -115 -249 -198 -396 -238 c -98 -27 -356 -24 -463 5 c -100 27 -183 69 -273 137 c -226 173 -353 409 -354 660 c 0 44 -5 120 -10 169 c -6 49 -8 91 -6 94 c 3 3 65 -22 138 -55 c 140 -64 251 -103 358 -127 c 71 -17 295 -46 347 -46 l 33 0 l 0 434 l 0 434 l -67 7 c -147 14 -269 56 -393 133 c -222 139 -378 393 -397 648 l -6 71 l -75 7 c -41 3 -239 6 -439 6 l -363 0 l 1 -517 z" }))), h("span", { key: 'c97081daa81688106395f6ef6c497b11df53a6a9', class: "pb-word" }, "ticketlayer")))))));
89
+ } }, h("aside", { key: 'eefe601ef6d2d8fc6bcfe62f1777d139875f875d', class: { panel: true, [derived.sideClass]: true, 'is-open': derived.visible }, role: "dialog", "aria-modal": "true", "aria-label": this.heading, "aria-hidden": derived.ariaHidden }, h("header", { key: '753d616182c3a903111dcaaf9b37f03186e17b48', class: "drawer-header" }, h("h2", { key: '3e839ddbc2a2da1f4c8eb0c1e38c421de13426cd', class: "drawer-title" }, this.heading), h("button", { key: '0d80fff5f425d6150fb391fd930161167b5f7d27', class: "close-btn", onClick: this.close, "aria-label": strings.close }, h("svg", { key: '7ffcbfb86f921d13deb5070140f21e4c7ca668f1', viewBox: "0 0 24 24", fill: "none", stroke: "currentColor", "stroke-width": "2" }, h("path", { key: '53d690ef2ac8d1d73038c53fa3aa0146bde0fb51', d: "M6 6l12 12M18 6L6 18", "stroke-linecap": "round" })))), h("div", { key: 'a782b1527cde89656e5f776311f848c446d92d48', class: "drawer-body" }, h("tl-cart", { key: '3cd2c09f728cf14f0ea5b6ff4493b3e51e0dda62' })), h("footer", { key: '2df57237bbd548ae4af09098676d7e1315e11d71', class: "drawer-footer" }, h("div", { key: 'f02f5970181a2d83b70ac44188f00523a188771d', class: "pb-links" }, h("a", { key: 'd799da3a8c1c33b10a268500b02fda5523cd8bcf', href: strings.privacyHref, target: "_blank", rel: "noopener noreferrer" }, strings.privacy), h("a", { key: 'e928acb4c78cef8762d7ae8d258fa705554fd121', href: strings.reportAbuseHref, target: "_blank", rel: "noopener noreferrer" }, strings.reportAbuse)), h("a", { key: '39278516b8da6dd14fe8bf0d0379c32ccfeb1d29', class: "pb-brand", href: strings.brandHref, target: "_blank", rel: "noopener noreferrer" }, h("span", { key: '96b2cb7ac0a73cd0c60fb7977999a00b4989177e', class: "pb-pre" }, strings.poweredBy), h("svg", { key: 'b3950f83eab4a41e63a7919e6872a9ecdafab38b', width: "13", height: "13", viewBox: "0 0 50 50", fill: "none", "aria-hidden": "true" }, h("g", { key: '4c0c7ec66952f8a1956b3061f6bf2eb45c438989', transform: "matrix(0.0129 0 0 -0.0129 25.4752 25.4752)" }, h("path", { key: 'b1c9a143f0b2ad24da843323a4c1c550e9717d1b', fill: "currentColor", transform: "translate(-1781.5, -1759.4003)", d: "M 41 2973 c 1 -285 5 -734 9 -998 c 7 -444 9 -488 30 -580 c 81 -364 235 -634 512 -900 c 135 -130 263 -220 423 -299 c 156 -77 249 -109 409 -143 c 110 -23 142 -25 354 -24 c 230 1 310 8 427 37 c 241 60 453 162 640 309 c 82 64 252 234 317 315 c 210 264 340 604 355 926 l 6 121 l -69 6 c -86 8 -706 9 -755 1 l -36 -6 l -21 -125 c -34 -191 -98 -317 -236 -462 c -111 -115 -249 -198 -396 -238 c -98 -27 -356 -24 -463 5 c -100 27 -183 69 -273 137 c -226 173 -353 409 -354 660 c 0 44 -5 120 -10 169 c -6 49 -8 91 -6 94 c 3 3 65 -22 138 -55 c 140 -64 251 -103 358 -127 c 71 -17 295 -46 347 -46 l 33 0 l 0 434 l 0 434 l -67 7 c -147 14 -269 56 -393 133 c -222 139 -378 393 -397 648 l -6 71 l -75 7 c -41 3 -239 6 -439 6 l -363 0 l 1 -517 z" }))), h("span", { key: '053ca451a9674161799458e731862a93c954be23', class: "pb-word" }, strings.brand)))))));
64
90
  }
65
91
  static get is() { return "tl-cart-drawer"; }
66
92
  static get encapsulation() { return "shadow"; }
@@ -140,7 +166,7 @@ export class TlCartDrawer {
140
166
  }
141
167
  static get states() {
142
168
  return {
143
- "visible": {}
169
+ "view": {}
144
170
  };
145
171
  }
146
172
  static get events() {
@@ -178,6 +204,12 @@ export class TlCartDrawer {
178
204
  }
179
205
  static get watchers() {
180
206
  return [{
207
+ "propName": "side",
208
+ "methodName": "syncProps"
209
+ }, {
210
+ "propName": "heading",
211
+ "methodName": "syncProps"
212
+ }, {
181
213
  "propName": "open",
182
214
  "methodName": "onOpenProp"
183
215
  }];