@ticketlayer/elements 0.3.3 → 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 (179) hide show
  1. package/README.md +3 -2
  2. package/dist/cjs/{index-BpfoKd9Q.js → index-DanPUX0s.js} +1 -1
  3. package/dist/cjs/index.cjs.js +2 -1
  4. package/dist/cjs/loader.cjs.js +2 -2
  5. package/dist/cjs/ticket-selector-BYVL13k_.js +3623 -0
  6. package/dist/cjs/ticketlayer-elements.cjs.js +2 -2
  7. package/dist/cjs/tl-buy-modal.cjs.entry.js +4 -4
  8. package/dist/cjs/tl-buy-tickets-button.cjs.entry.js +89 -71
  9. package/dist/cjs/tl-buy-tickets-wrapper.cjs.entry.js +62 -31
  10. package/dist/cjs/tl-cart-badge.cjs.entry.js +75 -62
  11. package/dist/cjs/tl-cart-drawer.cjs.entry.js +52 -24
  12. package/dist/cjs/tl-cart.cjs.entry.js +72 -98
  13. package/dist/cjs/tl-checkout-wrapper.cjs.entry.js +52 -30
  14. package/dist/cjs/tl-event-card.cjs.entry.js +44 -105
  15. package/dist/cjs/tl-event-list.cjs.entry.js +73 -36
  16. package/dist/cjs/tl-login.cjs.entry.js +75 -141
  17. package/dist/cjs/tl-my-orders.cjs.entry.js +53 -92
  18. package/dist/cjs/tl-occurrence-selector_2.cjs.entry.js +205 -641
  19. package/dist/cjs/tl-order-confirmation.cjs.entry.js +72 -105
  20. package/dist/cjs/tl-order-tickets_2.cjs.entry.js +50 -76
  21. package/dist/cjs/tl-promo-code.cjs.entry.js +51 -47
  22. package/dist/cjs/tl-seat-map.cjs.entry.js +4 -4
  23. package/dist/cjs/web-query-Ce73jB4k.js +16 -0
  24. package/dist/collection/components/tl-buy-modal/tl-buy-modal.css +12 -19
  25. package/dist/collection/components/tl-buy-modal/tl-buy-modal.js +2 -2
  26. package/dist/collection/components/tl-buy-tickets-button/tl-buy-tickets-button.css +41 -4
  27. package/dist/collection/components/tl-buy-tickets-button/tl-buy-tickets-button.js +105 -76
  28. package/dist/collection/components/tl-buy-tickets-wrapper/tl-buy-tickets-wrapper.css +2 -2
  29. package/dist/collection/components/tl-buy-tickets-wrapper/tl-buy-tickets-wrapper.js +73 -30
  30. package/dist/collection/components/tl-cart/tl-cart.css +52 -51
  31. package/dist/collection/components/tl-cart/tl-cart.js +82 -101
  32. package/dist/collection/components/tl-cart-badge/tl-cart-badge.css +11 -11
  33. package/dist/collection/components/tl-cart-badge/tl-cart-badge.js +62 -62
  34. package/dist/collection/components/tl-cart-drawer/tl-cart-drawer.css +6 -6
  35. package/dist/collection/components/tl-cart-drawer/tl-cart-drawer.js +56 -24
  36. package/dist/collection/components/tl-checkout-wrapper/tl-checkout-wrapper.css +2 -2
  37. package/dist/collection/components/tl-checkout-wrapper/tl-checkout-wrapper.js +61 -29
  38. package/dist/collection/components/tl-event-card/event-card-adapter.js +11 -23
  39. package/dist/collection/components/tl-event-card/tl-event-card.css +22 -22
  40. package/dist/collection/components/tl-event-card/tl-event-card.js +51 -84
  41. package/dist/collection/components/tl-event-list/tl-event-list.css +17 -17
  42. package/dist/collection/components/tl-event-list/tl-event-list.js +82 -38
  43. package/dist/collection/components/tl-login/tl-login.css +26 -26
  44. package/dist/collection/components/tl-login/tl-login.js +83 -147
  45. package/dist/collection/components/tl-my-orders/tl-my-orders.css +40 -40
  46. package/dist/collection/components/tl-my-orders/tl-my-orders.js +58 -96
  47. package/dist/collection/components/tl-occurrence-selector/tl-occurrence-selector.css +54 -65
  48. package/dist/collection/components/tl-occurrence-selector/tl-occurrence-selector.js +137 -516
  49. package/dist/collection/components/tl-order-confirmation/tl-order-confirmation.css +38 -38
  50. package/dist/collection/components/tl-order-confirmation/tl-order-confirmation.js +81 -113
  51. package/dist/collection/components/tl-order-tickets/tl-order-tickets.css +9 -9
  52. package/dist/collection/components/tl-order-tickets/tl-order-tickets.js +57 -92
  53. package/dist/collection/components/tl-promo-code/tl-promo-code.css +25 -25
  54. package/dist/collection/components/tl-promo-code/tl-promo-code.js +55 -50
  55. package/dist/collection/components/tl-seat-map/tl-seat-map.css +10 -10
  56. package/dist/collection/components/tl-seat-map/tl-seat-map.js +2 -2
  57. package/dist/collection/components/tl-ticket/tl-ticket.css +13 -13
  58. package/dist/collection/components/tl-ticket-selector/tl-ticket-selector.css +74 -49
  59. package/dist/collection/components/tl-ticket-selector/tl-ticket-selector.js +95 -135
  60. package/dist/collection/utils/cart-totals.js +9 -0
  61. package/dist/collection/utils/order-summary.js +15 -99
  62. package/dist/collection/utils/testing/deep-text.js +29 -0
  63. package/dist/collection/utils/testing/fixtures.js +290 -0
  64. package/dist/collection/utils/testing/mock-sdk.js +13 -2
  65. package/dist/collection/utils/web-query.js +12 -0
  66. package/dist/collection/utils/web-storage.js +22 -0
  67. package/dist/components/ticket-selector.js +1 -0
  68. package/dist/components/tl-buy-modal.js +1 -1
  69. package/dist/components/tl-buy-tickets-button.js +1 -1
  70. package/dist/components/tl-buy-tickets-wrapper2.js +1 -1
  71. package/dist/components/tl-cart-badge.js +1 -1
  72. package/dist/components/tl-cart-drawer.js +1 -1
  73. package/dist/components/tl-cart2.js +1 -1
  74. package/dist/components/tl-checkout-wrapper2.js +1 -1
  75. package/dist/components/tl-event-card2.js +1 -1
  76. package/dist/components/tl-event-list.js +1 -1
  77. package/dist/components/tl-login2.js +1 -1
  78. package/dist/components/tl-my-orders.js +1 -1
  79. package/dist/components/tl-occurrence-selector2.js +1 -1
  80. package/dist/components/tl-order-confirmation.js +1 -1
  81. package/dist/components/tl-order-tickets2.js +1 -1
  82. package/dist/components/tl-promo-code.js +1 -1
  83. package/dist/components/tl-seat-map.js +1 -1
  84. package/dist/components/tl-ticket-selector2.js +1 -1
  85. package/dist/components/tl-ticket2.js +1 -1
  86. package/dist/components/web-query.js +1 -0
  87. package/dist/elements.docs.json +36 -36
  88. package/dist/esm/{index-CNX8hZa8.js → index-imfxp765.js} +1 -1
  89. package/dist/esm/index.js +2 -1
  90. package/dist/esm/loader.js +3 -3
  91. package/dist/esm/ticket-selector-BwChnHVC.js +3606 -0
  92. package/dist/esm/ticketlayer-elements.js +3 -3
  93. package/dist/esm/tl-buy-modal.entry.js +4 -4
  94. package/dist/esm/tl-buy-tickets-button.entry.js +90 -72
  95. package/dist/esm/tl-buy-tickets-wrapper.entry.js +63 -32
  96. package/dist/esm/tl-cart-badge.entry.js +76 -63
  97. package/dist/esm/tl-cart-drawer.entry.js +53 -25
  98. package/dist/esm/tl-cart.entry.js +73 -99
  99. package/dist/esm/tl-checkout-wrapper.entry.js +53 -31
  100. package/dist/esm/tl-event-card.entry.js +45 -106
  101. package/dist/esm/tl-event-list.entry.js +74 -37
  102. package/dist/esm/tl-login.entry.js +76 -142
  103. package/dist/esm/tl-my-orders.entry.js +54 -93
  104. package/dist/esm/tl-occurrence-selector_2.entry.js +206 -642
  105. package/dist/esm/tl-order-confirmation.entry.js +73 -106
  106. package/dist/esm/tl-order-tickets_2.entry.js +51 -77
  107. package/dist/esm/tl-promo-code.entry.js +52 -48
  108. package/dist/esm/tl-seat-map.entry.js +4 -4
  109. package/dist/esm/web-query-Bdbb2lk5.js +14 -0
  110. package/dist/ticketlayer-elements/index.esm.js +1 -1
  111. package/dist/ticketlayer-elements/p-45923d2c.entry.js +1 -0
  112. package/dist/ticketlayer-elements/p-492adc77.entry.js +1 -0
  113. package/dist/ticketlayer-elements/p-4cfc9e53.entry.js +1 -0
  114. package/dist/ticketlayer-elements/p-5135630e.entry.js +1 -0
  115. package/dist/ticketlayer-elements/p-5bbce3b5.entry.js +1 -0
  116. package/dist/ticketlayer-elements/p-7d547721.entry.js +1 -0
  117. package/dist/ticketlayer-elements/p-7fe7b6ba.entry.js +1 -0
  118. package/dist/ticketlayer-elements/p-8122f0c1.entry.js +1 -0
  119. package/dist/ticketlayer-elements/p-98d40869.entry.js +1 -0
  120. package/dist/ticketlayer-elements/p-Bdbb2lk5.js +1 -0
  121. package/dist/ticketlayer-elements/p-BwChnHVC.js +1 -0
  122. package/dist/ticketlayer-elements/p-abec290a.entry.js +1 -0
  123. package/dist/ticketlayer-elements/p-ac79a3f6.entry.js +1 -0
  124. package/dist/ticketlayer-elements/p-d01f4429.entry.js +1 -0
  125. package/dist/ticketlayer-elements/p-d7ffaf46.entry.js +1 -0
  126. package/dist/ticketlayer-elements/p-e7170f3c.entry.js +1 -0
  127. package/dist/ticketlayer-elements/p-e787e862.entry.js +1 -0
  128. package/dist/ticketlayer-elements/p-f1ff931f.entry.js +1 -0
  129. package/dist/ticketlayer-elements/p-imfxp765.js +3 -0
  130. package/dist/ticketlayer-elements/ticketlayer-elements.css +1 -1
  131. package/dist/ticketlayer-elements/ticketlayer-elements.esm.js +1 -1
  132. package/dist/types/components/tl-buy-tickets-button/tl-buy-tickets-button.d.ts +40 -8
  133. package/dist/types/components/tl-buy-tickets-wrapper/tl-buy-tickets-wrapper.d.ts +20 -1
  134. package/dist/types/components/tl-cart/tl-cart.d.ts +40 -32
  135. package/dist/types/components/tl-cart-badge/tl-cart-badge.d.ts +22 -7
  136. package/dist/types/components/tl-cart-drawer/tl-cart-drawer.d.ts +28 -5
  137. package/dist/types/components/tl-checkout-wrapper/tl-checkout-wrapper.d.ts +18 -1
  138. package/dist/types/components/tl-event-card/event-card-adapter.d.ts +9 -20
  139. package/dist/types/components/tl-event-card/tl-event-card.d.ts +25 -28
  140. package/dist/types/components/tl-event-list/tl-event-list.d.ts +46 -6
  141. package/dist/types/components/tl-login/tl-login.d.ts +68 -22
  142. package/dist/types/components/tl-my-orders/tl-my-orders.d.ts +30 -12
  143. package/dist/types/components/tl-occurrence-selector/tl-occurrence-selector.d.ts +73 -161
  144. package/dist/types/components/tl-order-confirmation/tl-order-confirmation.d.ts +48 -15
  145. package/dist/types/components/tl-order-tickets/tl-order-tickets.d.ts +29 -31
  146. package/dist/types/components/tl-promo-code/tl-promo-code.d.ts +28 -10
  147. package/dist/types/components/tl-ticket/tl-ticket.d.ts +2 -11
  148. package/dist/types/components/tl-ticket-selector/tl-ticket-selector.d.ts +61 -32
  149. package/dist/types/components.d.ts +302 -62
  150. package/dist/types/utils/cart-totals.d.ts +10 -0
  151. package/dist/types/utils/order-summary.d.ts +16 -65
  152. package/dist/types/utils/testing/deep-text.d.ts +13 -0
  153. package/dist/types/utils/testing/fixtures.d.ts +225 -0
  154. package/dist/types/utils/testing/mock-sdk.d.ts +14 -1
  155. package/dist/types/utils/web-query.d.ts +18 -0
  156. package/dist/types/utils/web-storage.d.ts +16 -0
  157. package/elements.catalogue.json +676 -55
  158. package/package.json +14 -6
  159. package/dist/cjs/order-summary-ZMkJ5bez.js +0 -116
  160. package/dist/components/order-summary.js +0 -1
  161. package/dist/esm/order-summary-BGekdjN9.js +0 -103
  162. package/dist/ticketlayer-elements/p-0b1d16f8.entry.js +0 -1
  163. package/dist/ticketlayer-elements/p-1471f6d8.entry.js +0 -1
  164. package/dist/ticketlayer-elements/p-1c51cbc1.entry.js +0 -1
  165. package/dist/ticketlayer-elements/p-5169f68b.entry.js +0 -1
  166. package/dist/ticketlayer-elements/p-895ba1f3.entry.js +0 -1
  167. package/dist/ticketlayer-elements/p-8d5722e1.entry.js +0 -1
  168. package/dist/ticketlayer-elements/p-917d8e09.entry.js +0 -1
  169. package/dist/ticketlayer-elements/p-9629029a.entry.js +0 -1
  170. package/dist/ticketlayer-elements/p-BGekdjN9.js +0 -1
  171. package/dist/ticketlayer-elements/p-CNX8hZa8.js +0 -3
  172. package/dist/ticketlayer-elements/p-a22714d0.entry.js +0 -1
  173. package/dist/ticketlayer-elements/p-bf73044b.entry.js +0 -1
  174. package/dist/ticketlayer-elements/p-e02079fa.entry.js +0 -1
  175. package/dist/ticketlayer-elements/p-e04ace15.entry.js +0 -1
  176. package/dist/ticketlayer-elements/p-e6fab1d5.entry.js +0 -1
  177. package/dist/ticketlayer-elements/p-f826b753.entry.js +0 -1
  178. package/dist/ticketlayer-elements/p-fa639d88.entry.js +0 -1
  179. package/dist/ticketlayer-elements/p-fbaca948.entry.js +0 -1
@@ -0,0 +1,3606 @@
1
+ /** The store a core gets when its harness supplies none: it forgets. */
2
+ const NO_STORAGE = {
3
+ get: () => null,
4
+ set: () => undefined,
5
+ };
6
+ /** The parameters a core gets when its harness supplies none: there are none. */
7
+ const NO_QUERY = {
8
+ get: () => null,
9
+ };
10
+ const CATEGORIES = [
11
+ 'discovery',
12
+ 'purchase',
13
+ 'cart',
14
+ 'checkout',
15
+ 'account',
16
+ 'tickets',
17
+ ];
18
+ const RUNTIMES = ['web', 'native'];
19
+ /**
20
+ * Declare a core. The checks here are the ones a type cannot make: a tag that
21
+ * is not a `tl-` custom element name, a category or runtime outside the set the
22
+ * catalogue knows, a duplicated token, a status list that does not include what
23
+ * `status` can return. They run at import time, so a malformed core fails the
24
+ * build rather than a browser.
25
+ */
26
+ function defineElement(core) {
27
+ const fail = (message) => {
28
+ throw new Error(`[elements-core] ${core.tag || '<no tag>'}: ${message}`);
29
+ };
30
+ if (!/^tl-[a-z0-9]+(-[a-z0-9]+)*$/.test(core.tag))
31
+ fail('tag must look like tl-something');
32
+ if (!CATEGORIES.includes(core.category))
33
+ fail(`category must be one of ${CATEGORIES.join(', ')}`);
34
+ if (!core.runtimes.length || core.runtimes.some((r) => !RUNTIMES.includes(r))) {
35
+ fail(`runtimes must be a non-empty subset of ${RUNTIMES.join(', ')}`);
36
+ }
37
+ if (new Set(core.tokens).size !== core.tokens.length)
38
+ fail('tokens must not repeat');
39
+ if (!core.statuses.length)
40
+ fail('statuses must not be empty');
41
+ for (const [name, spec] of Object.entries(core.events)) {
42
+ if (!/^tl[A-Z]/.test(name))
43
+ fail(`event ${name} must be named tlSomething`);
44
+ if (!spec.detail)
45
+ fail(`event ${name} must state its detail type`);
46
+ }
47
+ for (const [name, spec] of Object.entries(core.props)) {
48
+ if (spec.required && spec.default !== undefined)
49
+ fail(`prop ${name} is required and has a default`);
50
+ }
51
+ return Object.freeze({ ...core });
52
+ }
53
+ const DEFAULT_LOCALE = 'en-GB';
54
+ const DEFAULT_CURRENCY = 'GBP';
55
+ /**
56
+ * Instantiate a core. The instance owns the state and the timers; the harness
57
+ * owns the pixels. `subscribe` fires after every change with the whole view, so
58
+ * a harness only has to assign it to whatever makes its runtime re-render.
59
+ */
60
+ function createElement(core, options = {}) {
61
+ const defaults = {};
62
+ for (const [name, spec] of Object.entries(core.props)) {
63
+ if (spec.default !== undefined)
64
+ defaults[name] = spec.default;
65
+ }
66
+ let props = { ...defaults, ...(options.props ?? {}) };
67
+ const strings = { ...core.strings, ...(options.strings ?? {}) };
68
+ const locale = options.locale ?? DEFAULT_LOCALE;
69
+ const fallbackCurrency = options.currency ?? DEFAULT_CURRENCY;
70
+ let resolveClient = toResolver(options.client);
71
+ let state = core.state(props);
72
+ let destroyed = false;
73
+ const timers = new Set();
74
+ const listeners = new Set();
75
+ const formatMoney = (amountInMinorUnits, currency) => new Intl.NumberFormat(locale, {
76
+ style: 'currency',
77
+ currency: currency || fallbackCurrency,
78
+ }).format(amountInMinorUnits / 100);
79
+ const storage = options.storage ?? NO_STORAGE;
80
+ const query = options.query ?? NO_QUERY;
81
+ const viewContext = () => ({ props, state, strings, formatMoney });
82
+ const buildView = () => {
83
+ const ctx = viewContext();
84
+ return {
85
+ status: core.status(ctx),
86
+ props,
87
+ state,
88
+ strings,
89
+ derived: core.derive(ctx),
90
+ };
91
+ };
92
+ const notify = () => {
93
+ if (destroyed)
94
+ return;
95
+ const view = buildView();
96
+ for (const listener of [...listeners])
97
+ listener(view);
98
+ };
99
+ const whenClient = async () => {
100
+ if (options.clientReady)
101
+ await options.clientReady();
102
+ const client = resolveClient();
103
+ if (!client) {
104
+ throw new Error(`[elements-core] ${core.tag}: no client. A harness hands the core the client the host published.`);
105
+ }
106
+ return client;
107
+ };
108
+ const actionContext = {
109
+ get props() {
110
+ return props;
111
+ },
112
+ get state() {
113
+ return state;
114
+ },
115
+ strings,
116
+ formatMoney,
117
+ setState(patch) {
118
+ if (destroyed)
119
+ return;
120
+ const next = typeof patch === 'function' ? patch(state) : patch;
121
+ state = { ...state, ...next };
122
+ notify();
123
+ },
124
+ emit(event, detail) {
125
+ if (!core.events[event]) {
126
+ throw new Error(`[elements-core] ${core.tag}: ${event} is not a declared event`);
127
+ }
128
+ options.emit?.(event, detail);
129
+ },
130
+ client() {
131
+ const client = resolveClient();
132
+ if (!client) {
133
+ throw new Error(`[elements-core] ${core.tag}: no client. A harness hands the core the client the host published.`);
134
+ }
135
+ return client;
136
+ },
137
+ optionalClient() {
138
+ try {
139
+ return resolveClient();
140
+ }
141
+ catch {
142
+ return null;
143
+ }
144
+ },
145
+ storage,
146
+ query,
147
+ whenClient,
148
+ after(ms, run) {
149
+ if (destroyed)
150
+ return;
151
+ const handle = setTimeout(() => {
152
+ timers.delete(handle);
153
+ if (!destroyed)
154
+ run();
155
+ }, ms);
156
+ timers.add(handle);
157
+ },
158
+ };
159
+ const actions = core.actions(actionContext);
160
+ // The live mount, as an identity rather than a boolean: a teardown closes
161
+ // over the token it was made for, so a stale one tears nothing down.
162
+ let mountToken = null;
163
+ let cleanups = [];
164
+ const teardownMount = (token) => {
165
+ if (mountToken !== token)
166
+ return;
167
+ mountToken = null;
168
+ const pending = cleanups;
169
+ cleanups = [];
170
+ for (const cleanup of pending.reverse())
171
+ cleanup();
172
+ };
173
+ const mount = () => {
174
+ if (destroyed)
175
+ return () => { };
176
+ if (mountToken)
177
+ teardownMount(mountToken);
178
+ const token = {};
179
+ mountToken = token;
180
+ const live = () => mountToken === token && !destroyed;
181
+ const returned = core.mount?.({
182
+ ...actionContext,
183
+ get props() {
184
+ return props;
185
+ },
186
+ get state() {
187
+ return state;
188
+ },
189
+ actions,
190
+ active: live,
191
+ onTeardown(cleanup) {
192
+ if (live())
193
+ cleanups.push(cleanup);
194
+ else
195
+ cleanup();
196
+ },
197
+ });
198
+ if (typeof returned === 'function') {
199
+ if (live())
200
+ cleanups.push(returned);
201
+ else
202
+ returned();
203
+ }
204
+ return () => teardownMount(token);
205
+ };
206
+ return {
207
+ core,
208
+ get props() {
209
+ return props;
210
+ },
211
+ get state() {
212
+ return state;
213
+ },
214
+ get view() {
215
+ return buildView();
216
+ },
217
+ actions,
218
+ setProps(next) {
219
+ if (destroyed)
220
+ return;
221
+ props = { ...props, ...next };
222
+ notify();
223
+ },
224
+ setClient(client) {
225
+ resolveClient = toResolver(client);
226
+ },
227
+ subscribe(listener) {
228
+ listeners.add(listener);
229
+ return () => {
230
+ listeners.delete(listener);
231
+ };
232
+ },
233
+ mount,
234
+ get mounted() {
235
+ return mountToken !== null;
236
+ },
237
+ destroy() {
238
+ if (mountToken)
239
+ teardownMount(mountToken);
240
+ destroyed = true;
241
+ for (const handle of timers)
242
+ clearTimeout(handle);
243
+ timers.clear();
244
+ listeners.clear();
245
+ },
246
+ };
247
+ }
248
+ function toResolver(client) {
249
+ if (typeof client === 'function')
250
+ return client;
251
+ return () => client ?? null;
252
+ }
253
+
254
+ /**
255
+ * The money rows a cart-shaped view renders: subtotal, the booking fee, a
256
+ * discount and the total.
257
+ *
258
+ * Pure: no Stencil, no DOM, no SDK. A core derives these figures and a harness
259
+ * renders what it returns, so the rule for what is shown can be unit tested on
260
+ * its own (the platform plan's headless-core split, AGENTS.md). It lived in
261
+ * `packages/elements/src/utils/cart-totals.ts` until the core split (TKT-68)
262
+ * and is re-exported from there, so every existing import still resolves.
263
+ *
264
+ * The client (`@ticketlayer/live`) already derives these figures from the
265
+ * sales-surface response and publishes `subtotal`, `fees`, `discount`,
266
+ * `total`, `hasFees` and `feeLabel` on the cart it hands us. Cores never
267
+ * import the client, so the same rule is stated here for the case where a
268
+ * host binds an older connector, or a bare sales cart: read what is there,
269
+ * and never invent money that is not.
270
+ *
271
+ * Money is integer minor units (2500 = 25.00) everywhere on this platform.
272
+ * Nothing here divides or multiplies money; the only division is the one
273
+ * `formatPrice` does to render it.
274
+ */
275
+ /** Shown when the response names no fee of its own. */
276
+ const FEE_LABEL_FALLBACK = 'Booking fee';
277
+ /**
278
+ * Minor units, or null when the value is not minor units. Integers and
279
+ * all-digit strings only: a decimal is not minor units, and treating it as
280
+ * money here would put floating point into a total.
281
+ */
282
+ function minorUnits(value) {
283
+ if (typeof value === 'number')
284
+ return Number.isInteger(value) ? value : null;
285
+ if (typeof value === 'string' && /^-?\d+$/.test(value))
286
+ return Number(value);
287
+ return null;
288
+ }
289
+ /**
290
+ * THE RULE: a fee of zero is not the same as no fee.
291
+ *
292
+ * Render the fee row when the response EXPRESSES a fee - an amount above
293
+ * zero, or a fee the surface has NAMED (a `type: 'fee'` line or a fee label)
294
+ * even where it computes to zero. A response with no fee figure at all, or a
295
+ * bare zero nothing names, expresses no fee: a channel with no fee profile
296
+ * renders no row, and we never show a "Booking fee 0.00" the surface did not
297
+ * put there.
298
+ */
299
+ function expressesFee(fees, named) {
300
+ if (fees === null)
301
+ return named;
302
+ return fees > 0 || named;
303
+ }
304
+ const lineType = (line) => line.type || 'ticket';
305
+ function cartTotals(cart) {
306
+ const c = cart || {};
307
+ const lines = Array.isArray(c.items) ? c.items : [];
308
+ const feeLines = lines.filter((l) => lineType(l) === 'fee');
309
+ const discountLines = lines.filter((l) => lineType(l) === 'discount');
310
+ const subtotal = minorUnits(c.itemsSubtotal) ??
311
+ minorUnits(c.subtotal) ??
312
+ lines
313
+ .filter((l) => !['fee', 'tax', 'discount'].includes(lineType(l)))
314
+ .reduce((sum, l) => sum + (minorUnits(l.subtotal) ?? 0), 0);
315
+ const feeFromLines = feeLines.length
316
+ ? feeLines.reduce((sum, l) => sum + (minorUnits(l.subtotal) ?? 0), 0)
317
+ : null;
318
+ const fees = minorUnits(c.totalFees) ?? minorUnits(c.fees) ?? feeFromLines;
319
+ const discountFromLines = discountLines.length
320
+ ? discountLines.reduce((sum, l) => sum + Math.abs(minorUnits(l.subtotal) ?? 0), 0)
321
+ : null;
322
+ const discount = minorUnits(c.totalDiscounts) ?? minorUnits(c.discount) ?? discountFromLines;
323
+ const tax = minorUnits(c.totalTax) ?? 0;
324
+ const named = (typeof c.feeLabel === 'string' && !!c.feeLabel) ||
325
+ feeLines.some((l) => typeof l.name === 'string' && !!l.name);
326
+ const label = (typeof c.feeLabel === 'string' && c.feeLabel) ||
327
+ feeLines.map((l) => l.name).find((n) => typeof n === 'string' && !!n) ||
328
+ FEE_LABEL_FALLBACK;
329
+ return {
330
+ currency: c.currency || 'GBP',
331
+ subtotal,
332
+ fees: fees ?? 0,
333
+ discount: discount ?? 0,
334
+ // The server's total is what the buyer is charged, so it wins over any sum
335
+ // we could do here. We only add up when the response carries no total.
336
+ total: minorUnits(c.total) ?? subtotal + (fees ?? 0) + tax - (discount ?? 0),
337
+ // The client already applied the rule; honour its answer when it sent one.
338
+ showFee: typeof c.hasFees === 'boolean' ? c.hasFees : expressesFee(fees, named),
339
+ feeLabel: label,
340
+ };
341
+ }
342
+ /**
343
+ * The totals for a selection that is not yet a cart (tl-ticket-selector).
344
+ *
345
+ * The buyer-facing surface quotes no fee before the cart exists - there is no
346
+ * quote or preview operation, and the channel's fee profile is not exposed to
347
+ * a publishable key - so `quote` is null on every call today and no fee row is
348
+ * rendered. It is a parameter rather than an assumption so that the row lights
349
+ * up from data the moment the surface quotes one, and so the rule stays
350
+ * testable now.
351
+ */
352
+ function selectionTotals(lines, quote = null) {
353
+ const subtotal = (lines || []).reduce((sum, l) => sum + (minorUnits(l.price) ?? 0) * (Math.max(0, Math.trunc(l.quantity || 0)) || 0), 0);
354
+ const fees = quote ? minorUnits(quote.amount) : null;
355
+ const named = !!quote?.label;
356
+ return {
357
+ subtotal,
358
+ fees: fees ?? 0,
359
+ total: subtotal + (fees ?? 0),
360
+ showFee: quote ? expressesFee(fees, named) : false,
361
+ feeLabel: quote?.label || FEE_LABEL_FALLBACK,
362
+ };
363
+ }
364
+
365
+ /**
366
+ * The order shape, and what every harness has to work out from it.
367
+ *
368
+ * The sales order view carries line items, totals and tickets but no event
369
+ * name, no occurrence date and often no customer email, so these derive what
370
+ * they can from the order and resolve the rest through the client. They lived
371
+ * in `packages/elements/src/utils/order-summary.ts`, where only the web could
372
+ * reach them; they moved here with the split of `tl-my-orders` and
373
+ * `tl-order-confirmation` (TKT-69) so the same rules answer on every runtime.
374
+ * That module re-exports them, so every existing import still resolves and
375
+ * `tl-order-tickets` and `tl-ticket`, which are not split yet, read the same
376
+ * helpers they always did.
377
+ *
378
+ * `en-GB` is written out in the date and money helpers rather than taken from
379
+ * the instance's locale. That is what the components did and this is a
380
+ * behaviour-identical move; `ctx.formatMoney` is the locale-aware formatter and
381
+ * these are deliberately not it, because an order's money arrives as a minor
382
+ * unit integer OR a decimal string and `formatMoney` takes only the first.
383
+ */
384
+ /** Money arrives as minor-unit integers or decimal strings ("50.00"). */
385
+ function formatPrice$1(value, currency = 'GBP') {
386
+ if (value === null || value === undefined || value === '')
387
+ return '';
388
+ const fmt = new Intl.NumberFormat('en-GB', { style: 'currency', currency });
389
+ if (typeof value === 'number')
390
+ return fmt.format(value / 100);
391
+ const n = Number(value);
392
+ return Number.isNaN(n) ? String(value) : fmt.format(n);
393
+ }
394
+ function formatDateTime(iso) {
395
+ return new Date(iso).toLocaleDateString('en-GB', {
396
+ weekday: 'short',
397
+ day: 'numeric',
398
+ month: 'short',
399
+ year: 'numeric',
400
+ hour: '2-digit',
401
+ minute: '2-digit',
402
+ });
403
+ }
404
+ function formatDate$3(iso) {
405
+ return new Date(iso).toLocaleDateString('en-GB', {
406
+ day: 'numeric',
407
+ month: 'short',
408
+ year: 'numeric',
409
+ });
410
+ }
411
+ /** Line items that are tickets (fees, taxes and discounts have their own type). */
412
+ function ticketItems(order) {
413
+ return (order.items ?? []).filter((i) => !i.type || i.type === 'ticket');
414
+ }
415
+ function ticketCount(order) {
416
+ const fromItems = ticketItems(order).reduce((sum, i) => sum + (Number(i.quantity) || 0), 0);
417
+ return fromItems || order.tickets?.length || 0;
418
+ }
419
+ /** Event name as the order view exposes it: the first ticket line's name. */
420
+ function orderEventName(order) {
421
+ return order.eventName || ticketItems(order)[0]?.name || order.items?.[0]?.name || null;
422
+ }
423
+ function orderCustomerEmail(order, customer) {
424
+ const fromOrder = order?.customerEmail || order?.customer?.email;
425
+ if (fromOrder)
426
+ return fromOrder;
427
+ const c = customer;
428
+ return c?.email || null;
429
+ }
430
+ function statusLabel(status) {
431
+ if (!status)
432
+ return '';
433
+ return status.replace(/_/g, ' ').replace(/^\w/, (m) => m.toUpperCase());
434
+ }
435
+ function statusTone(status) {
436
+ switch (status) {
437
+ case 'confirmed':
438
+ return 'success';
439
+ case 'completed':
440
+ case 'fulfilled':
441
+ return 'info';
442
+ case 'pending':
443
+ case 'processing':
444
+ return 'warning';
445
+ case 'cancelled':
446
+ case 'expired':
447
+ case 'failed':
448
+ return 'error';
449
+ default:
450
+ return 'muted';
451
+ }
452
+ }
453
+ function errorMessage(err, fallback) {
454
+ return err instanceof Error && err.message ? err.message : fallback;
455
+ }
456
+ /**
457
+ * Find the event and start time for an occurrence id. The order view only
458
+ * carries the occurrence id, so this walks the channel's listings (cached by
459
+ * live-api) until one owns the occurrence. Best effort: null when not found,
460
+ * and null on a client whose events manager cannot list or fetch, which is an
461
+ * older connector rather than a failure.
462
+ */
463
+ async function resolveOccurrence(client, occurrenceId, maxEvents = 12) {
464
+ const events = client.getEventsManager();
465
+ if (!events?.list || !events.get)
466
+ return null;
467
+ const listing = await events.list({ limit: maxEvents });
468
+ const items = (listing?.items ?? []);
469
+ const candidates = items.slice(0, maxEvents);
470
+ for (const candidate of candidates) {
471
+ const detail = (await events.get(candidate.id).catch(() => null));
472
+ const occ = detail?.occurrences?.find((o) => o.id === occurrenceId);
473
+ if (occ) {
474
+ return {
475
+ eventId: detail?.id ?? candidate.id,
476
+ eventName: detail?.name ?? candidate.name ?? '',
477
+ startsAt: occ.startsAt ?? null,
478
+ };
479
+ }
480
+ }
481
+ return null;
482
+ }
483
+ function orderTotals(order) {
484
+ const o = order || {};
485
+ const items = Array.isArray(o.items) ? o.items : [];
486
+ const feeLines = items.filter((i) => i.type === 'fee');
487
+ const discountLines = items.filter((i) => i.type === 'discount');
488
+ const tickets = ticketItems(o);
489
+ const feeFromLines = feeLines.length
490
+ ? feeLines.reduce((sum, i) => sum + (minorUnits(i.subtotal) ?? 0), 0)
491
+ : null;
492
+ const fees = feeFromLines ?? minorUnits(o.totalFees) ?? 0;
493
+ const declaredSubtotal = minorUnits(o.subtotal);
494
+ const subtotal = tickets.length
495
+ ? tickets.reduce((sum, i) => sum + (minorUnits(i.subtotal) ?? 0), 0)
496
+ : (declaredSubtotal ?? 0) - fees;
497
+ const discountFromLines = discountLines.length
498
+ ? discountLines.reduce((sum, i) => sum + Math.abs(minorUnits(i.subtotal) ?? 0), 0)
499
+ : null;
500
+ const discount = minorUnits(o.totalDiscounts) ?? discountFromLines ?? 0;
501
+ const tax = minorUnits(o.totalTax) ?? 0;
502
+ const named = feeLines.some((i) => typeof i.name === 'string' && !!i.name);
503
+ return {
504
+ currency: o.currency || 'GBP',
505
+ subtotal,
506
+ fees,
507
+ feeLabel: feeLines.map((i) => i.name).find((n) => !!n) || FEE_LABEL_FALLBACK,
508
+ showFee: expressesFee(feeFromLines ?? minorUnits(o.totalFees), named),
509
+ tax,
510
+ discount,
511
+ total: minorUnits(o.total) ?? subtotal + fees + tax - discount,
512
+ };
513
+ }
514
+
515
+ /**
516
+ * `tl-buy-tickets-button`: the pre-styled button that opens the purchase
517
+ * modal for one event.
518
+ *
519
+ * The sibling of `tl-buy-tickets-wrapper`. The wrapper turns whatever the host
520
+ * already has into the thing that opens the modal; this one IS the thing, so
521
+ * on top of the wrapper's single action it carries the four sale states a
522
+ * seller puts a button in, the label each one defaults to, and the price the
523
+ * button may show beside it.
524
+ *
525
+ * Until now it could not be split for a different reason from the wrapper's.
526
+ * Its four states painted themselves from literal hex in the component, and a
527
+ * core may hold no value at all, so there was nothing to lift. TKT-203 moved
528
+ * them onto token families - `available` to primary, `soldOut` to error,
529
+ * `comingSoon` to warning, `waitlist` to info, each deriving its hover with
530
+ * `color-mix` - and that is what unblocked this.
531
+ *
532
+ * What a core still cannot say, and this one does not try to: WHICH family a
533
+ * state paints with. `tokens` is a flat list and there is no shape for mapping
534
+ * one onto a status. So this core does what `tl-event-card`'s status badge
535
+ * does - it derives the CLASS NAME the state is rendered under
536
+ * (`derived.stateClass`) and leaves the stylesheet to say which family that
537
+ * class reaches for. The core names the state; the harness names the paint.
538
+ *
539
+ * Behaviour is what the Stencil component did, to the letter:
540
+ *
541
+ * - `disabled`, `soldOut` and `comingSoon` are not pressable, and a press on
542
+ * one of them does nothing whatever: no event, no modal.
543
+ * - a press announces `tlClick` FIRST and opens the modal after, so a host
544
+ * hears the intent even on a page that never started Live.
545
+ * - with no client, or a connector too old to have the modal verbs, the press
546
+ * still announces `tlClick` and then quietly opens nothing. Unlike the
547
+ * wrapper this element has no error event to say so with, and inventing one
548
+ * would be a new published surface rather than a split.
549
+ * - the label is the `label` prop when the host set one, else the state's own.
550
+ * - the price is formatted `en-GB`, in the button's currency, with the pence
551
+ * shown only when there are any.
552
+ *
553
+ * What did not come across is the `console.error` the component wrote when the
554
+ * modal threw, for the reason `tl-cart` and `tl-promo-code` dropped theirs: a
555
+ * library should not write to its host's console.
556
+ *
557
+ * The other thing a core refuses to know is which key press counts as
558
+ * activation. `activate()` is the whole input surface; the harness decides what
559
+ * reaches it - a click and Enter or Space on the web, a press on a phone.
560
+ */
561
+ const STRINGS$e = {
562
+ available: 'Buy Tickets',
563
+ soldOut: 'Sold Out',
564
+ waitlist: 'Join Waitlist',
565
+ comingSoon: 'On Sale Soon',
566
+ };
567
+ /** Sale state -> the class it renders under. */
568
+ const STATE_CLASSES = {
569
+ available: 'state-available',
570
+ soldOut: 'state-sold-out',
571
+ waitlist: 'state-waitlist',
572
+ comingSoon: 'state-coming-soon',
573
+ };
574
+ /** The states a press does nothing in, whatever `disabled` says. */
575
+ const NOT_PRESSABLE = ['soldOut', 'comingSoon'];
576
+ const pressable = (props) => !props.disabled && !NOT_PRESSABLE.includes(props.state);
577
+ /**
578
+ * As the component formatted it: `en-GB`, minor units, and the pence only when
579
+ * there are any. The locale is written down here rather than taken from
580
+ * `ctx.formatMoney` because that is what the element did before the split and
581
+ * this is a split, not a change (see the note on the card core's date).
582
+ */
583
+ function formatPrice(priceInMinorUnits, currency) {
584
+ const amount = priceInMinorUnits / 100;
585
+ const hasDecimals = amount % 1 !== 0;
586
+ return new Intl.NumberFormat('en-GB', {
587
+ style: 'currency',
588
+ currency,
589
+ minimumFractionDigits: hasDecimals ? 2 : 0,
590
+ maximumFractionDigits: hasDecimals ? 2 : 0,
591
+ }).format(amount);
592
+ }
593
+ const buyTicketsButtonCore = defineElement({
594
+ tag: 'tl-buy-tickets-button',
595
+ docs: 'A buy-tickets button in one of four sale states, which opens the purchase modal.',
596
+ category: 'purchase',
597
+ runtimes: ['web'],
598
+ tokens: [
599
+ 'error',
600
+ 'error-foreground',
601
+ 'font-family',
602
+ 'info',
603
+ 'info-foreground',
604
+ 'primary',
605
+ 'primary-foreground',
606
+ 'radius-lg',
607
+ 'radius-md',
608
+ 'radius-sm',
609
+ 'warning',
610
+ 'warning-foreground',
611
+ ],
612
+ sdkMethods: ['event.openModal'],
613
+ props: {
614
+ eventId: {
615
+ type: 'string',
616
+ attr: 'event-id',
617
+ required: true,
618
+ docs: 'Event ID to purchase tickets for',
619
+ },
620
+ occurrenceId: {
621
+ type: 'string',
622
+ tsType: 'string | undefined',
623
+ attr: 'occurrence-id',
624
+ docs: 'Optional pre-selected occurrence ID',
625
+ },
626
+ state: {
627
+ type: 'string',
628
+ tsType: '"available" | "comingSoon" | "soldOut" | "waitlist"',
629
+ attr: 'state',
630
+ default: 'available',
631
+ docs: 'Button state - determines colour, label, and icon',
632
+ },
633
+ label: {
634
+ type: 'string',
635
+ tsType: 'string | undefined',
636
+ attr: 'label',
637
+ docs: 'Button label override (default depends on state)',
638
+ },
639
+ variant: {
640
+ type: 'string',
641
+ tsType: '"outline" | "primary" | "secondary"',
642
+ attr: 'variant',
643
+ default: 'primary',
644
+ docs: 'Button variant',
645
+ },
646
+ size: {
647
+ type: 'string',
648
+ tsType: '"lg" | "md" | "sm"',
649
+ attr: 'size',
650
+ default: 'md',
651
+ docs: 'Button size',
652
+ },
653
+ fullWidth: {
654
+ type: 'boolean',
655
+ attr: 'full-width',
656
+ default: false,
657
+ docs: 'Full width button',
658
+ },
659
+ price: {
660
+ type: 'number',
661
+ tsType: 'number | undefined',
662
+ attr: 'price',
663
+ docs: 'Price to display (in minor units e.g. pence)',
664
+ },
665
+ currency: {
666
+ type: 'string',
667
+ attr: 'currency',
668
+ default: 'GBP',
669
+ docs: 'Currency code for price formatting',
670
+ },
671
+ pricePrefix: {
672
+ type: 'string',
673
+ attr: 'price-prefix',
674
+ default: 'From',
675
+ docs: 'Show price prefix text',
676
+ },
677
+ disabled: {
678
+ type: 'boolean',
679
+ attr: 'disabled',
680
+ default: false,
681
+ docs: 'Disabled state',
682
+ },
683
+ },
684
+ events: {
685
+ tlClick: {
686
+ detail: '{ eventId: string; occurrenceId?: string | undefined; }',
687
+ docs: 'Emitted when button is clicked (before modal opens)',
688
+ },
689
+ },
690
+ strings: STRINGS$e,
691
+ statuses: ['ready', 'disabled'],
692
+ state: () => ({}),
693
+ status: ({ props }) => (pressable(props) ? 'ready' : 'disabled'),
694
+ derive: ({ props, strings }) => ({
695
+ interactive: pressable(props),
696
+ buttonLabel: props.label || strings[props.state],
697
+ stateClass: STATE_CLASSES[props.state],
698
+ priceLabel: props.price === undefined ? null : formatPrice(props.price, props.currency),
699
+ }),
700
+ actions: (ctx) => ({
701
+ /** A click, a tap, Enter or Space: whatever the harness calls a press. */
702
+ activate() {
703
+ if (!pressable(ctx.props))
704
+ return;
705
+ // Announced first, and whatever happens next: a host that tracks buying
706
+ // intent hears it even on a page that never started Live.
707
+ ctx.emit('tlClick', { eventId: ctx.props.eventId, occurrenceId: ctx.props.occurrenceId });
708
+ const client = ctx.optionalClient();
709
+ // Tolerated rather than assumed, like every other accessor a core reads:
710
+ // the same core runs against whatever connector version the host has.
711
+ if (!client || !client.event)
712
+ return;
713
+ try {
714
+ client.event(ctx.props.eventId).openModal({ occurrenceId: ctx.props.occurrenceId });
715
+ }
716
+ catch {
717
+ // The component swallowed this too (it logged and carried on). This
718
+ // element has no error event to announce it with, and giving it one
719
+ // would be a new published surface rather than a split.
720
+ }
721
+ },
722
+ }),
723
+ });
724
+
725
+ /**
726
+ * `tl-buy-tickets-wrapper`: whatever the page already has, turned into the
727
+ * thing that opens the purchase modal.
728
+ *
729
+ * The element renders nothing of its own - the host's own button, link or card
730
+ * is slotted into it - so this is the smallest core in the package: no state,
731
+ * one action, and three events that exist only because the modal reports back
732
+ * to whoever opened it.
733
+ *
734
+ * Until now it could not be split at all. Its events were `tl-complete`,
735
+ * `tl-error` and `tl-close`, and `defineElement` requires `tlSomething`; the
736
+ * names were renamed to `tlComplete`, `tlError` and `tlClose` (TKT-203), which
737
+ * is what made this possible. The rename is a breaking change to a published
738
+ * element and is deliberate.
739
+ *
740
+ * Behaviour is what the Stencil component did, to the letter:
741
+ *
742
+ * - a disabled wrapper does nothing at all, and announces nothing.
743
+ * - with no client on the page the click announces `tlError` rather than
744
+ * throwing, because the host asked for a modal and deserves to hear that it
745
+ * did not open.
746
+ * - the modal is the client's, not the element's. This core asks for it and
747
+ * then only relays what comes back.
748
+ *
749
+ * What did not come across is the `console.error` tracing, for the reason
750
+ * `tl-cart` and `tl-promo-code` dropped theirs: a library should not write to
751
+ * its host's console when it has an event for saying the same thing.
752
+ *
753
+ * What a core still cannot say, and this one does not try to: which key press
754
+ * counts as activation. `activate()` is the whole input surface, and each
755
+ * harness decides what reaches it - a click and Enter or Space on the web, a
756
+ * press on a phone.
757
+ */
758
+ const STRINGS$d = {
759
+ noClient: 'SDK not initialized',
760
+ notAvailable: 'Tickets are not available',
761
+ };
762
+ const buyTicketsWrapperCore = defineElement({
763
+ tag: 'tl-buy-tickets-wrapper',
764
+ docs: 'Wrap any content and open the purchase modal when it is activated.',
765
+ category: 'purchase',
766
+ runtimes: ['web'],
767
+ tokens: ['radius-sm', 'ring'],
768
+ sdkMethods: ['event.openModal'],
769
+ props: {
770
+ eventId: {
771
+ type: 'string',
772
+ attr: 'event-id',
773
+ required: true,
774
+ docs: 'The event ID to purchase tickets for',
775
+ },
776
+ occurrenceId: {
777
+ type: 'string',
778
+ tsType: 'string | undefined',
779
+ attr: 'occurrence-id',
780
+ docs: 'Optional specific occurrence ID (skips occurrence selector)',
781
+ },
782
+ disabled: {
783
+ type: 'boolean',
784
+ attr: 'disabled',
785
+ default: false,
786
+ docs: 'Whether the wrapper is disabled',
787
+ },
788
+ },
789
+ events: {
790
+ // `OrderResult` because that is the name the web harness writes, and the
791
+ // catalogue compares the two: it is the connector's name for the shape
792
+ // `ElementOrderResult` describes here.
793
+ tlComplete: { detail: 'OrderResult', docs: 'Emitted when purchase completes successfully' },
794
+ tlError: { detail: '{ message: string; }', docs: 'Emitted when an error occurs' },
795
+ tlClose: { detail: 'void', docs: 'Emitted when the modal closes' },
796
+ },
797
+ strings: STRINGS$d,
798
+ statuses: ['ready', 'disabled'],
799
+ state: () => ({}),
800
+ status: ({ props }) => (props.disabled ? 'disabled' : 'ready'),
801
+ derive: ({ props }) => ({ interactive: !props.disabled }),
802
+ actions: (ctx) => ({
803
+ /** A click, a tap, Enter or Space: whatever the harness calls a press. */
804
+ activate() {
805
+ if (ctx.props.disabled)
806
+ return;
807
+ const client = ctx.optionalClient();
808
+ if (!client) {
809
+ ctx.emit('tlError', { message: ctx.strings.noClient });
810
+ return;
811
+ }
812
+ // Tolerated rather than assumed, like every other accessor a core reads:
813
+ // the same core runs against whatever connector version the host has.
814
+ if (!client.event) {
815
+ ctx.emit('tlError', { message: ctx.strings.notAvailable });
816
+ return;
817
+ }
818
+ client.event(ctx.props.eventId).openModal({
819
+ occurrenceId: ctx.props.occurrenceId,
820
+ onComplete: (order) => ctx.emit('tlComplete', order),
821
+ onError: (err) => ctx.emit('tlError', { message: err.message }),
822
+ onClose: () => ctx.emit('tlClose', undefined),
823
+ });
824
+ },
825
+ }),
826
+ });
827
+
828
+ /**
829
+ * `tl-cart`: the cart as a list of lines, its money rows and a checkout button.
830
+ *
831
+ * The first core that lives on a subscription. It reads the cart once when a
832
+ * harness mounts it and then keeps itself current from the client's
833
+ * `cart:updated` bus, binding that subscription inside its own `mount` and
834
+ * handing the unsubscribe to `ctx.onTeardown` (TKT-197). No harness owns a
835
+ * subscription any more, on any runtime, and a remount cannot leave two
836
+ * behind: `createElement` keeps at most one mount live.
837
+ *
838
+ * Behaviour is what the Stencil component did before the split, to the letter:
839
+ * the same money rows through `cartTotals`, the same compact mode, the same
840
+ * silence when the host published no client, the same events in the same
841
+ * order. What did not come across is the `console.log` tracing, which a library
842
+ * should not do for its host.
843
+ */
844
+ const STRINGS$c = {
845
+ emptyTitle: 'Your cart is empty',
846
+ emptyMessage: 'Add tickets to get started',
847
+ subtotalRow: 'Subtotal',
848
+ discountRow: 'Discount',
849
+ totalRow: 'Total',
850
+ removeItem: 'Remove item',
851
+ quantityPrefix: 'Qty: ',
852
+ loadFailed: 'Failed to load cart',
853
+ removeFailed: 'Failed to remove item',
854
+ };
855
+ /**
856
+ * The occurrence date, as the component formatted it.
857
+ *
858
+ * `en-GB` is written here rather than taken from the instance's locale because
859
+ * that is what the component did, and this split is behaviour-identical. There
860
+ * is no locale on the view context today; `formatMoney` closes over one and
861
+ * nothing else does.
862
+ */
863
+ function formatDate$2(dateString) {
864
+ return new Date(dateString).toLocaleDateString('en-GB', {
865
+ weekday: 'short',
866
+ day: 'numeric',
867
+ month: 'short',
868
+ hour: '2-digit',
869
+ minute: '2-digit',
870
+ });
871
+ }
872
+ const cartCore = defineElement({
873
+ tag: 'tl-cart',
874
+ docs: "The buyer's cart: its lines, the money rows the surface reported and a checkout button.",
875
+ category: 'cart',
876
+ runtimes: ['web', 'native'],
877
+ tokens: [
878
+ 'background',
879
+ 'border',
880
+ 'error',
881
+ 'error-bg',
882
+ 'font-family',
883
+ 'foreground',
884
+ 'muted',
885
+ 'muted-foreground',
886
+ 'primary',
887
+ 'primary-foreground',
888
+ 'radius-md',
889
+ 'radius-sm',
890
+ 'spacing-1',
891
+ 'spacing-2',
892
+ 'spacing-3',
893
+ 'spacing-4',
894
+ 'spacing-8',
895
+ ],
896
+ sdkMethods: ['cart.get', 'cart.on', 'cart.removeItem'],
897
+ props: {
898
+ showCheckoutButton: {
899
+ type: 'boolean',
900
+ attr: 'show-checkout-button',
901
+ default: true,
902
+ docs: 'Show checkout button',
903
+ },
904
+ checkoutLabel: {
905
+ type: 'string',
906
+ attr: 'checkout-label',
907
+ default: 'Checkout',
908
+ docs: 'Checkout button label',
909
+ },
910
+ showRemoveButtons: {
911
+ type: 'boolean',
912
+ attr: 'show-remove-buttons',
913
+ default: true,
914
+ docs: 'Show remove item buttons',
915
+ },
916
+ compact: { type: 'boolean', attr: 'compact', default: false, docs: 'Compact mode - hides some details' },
917
+ },
918
+ events: {
919
+ tlCheckout: { detail: '{ cart: TlCartData; }', docs: 'Emitted when checkout is clicked' },
920
+ tlItemRemoved: { detail: '{ itemId: string; }', docs: 'Emitted when item is removed' },
921
+ tlCartUpdated: { detail: '{ cart: TlCartData; }', docs: 'Emitted when cart is updated' },
922
+ tlError: { detail: '{ message: string; }', docs: 'Emitted on error' },
923
+ },
924
+ strings: STRINGS$c,
925
+ statuses: ['loading', 'empty', 'ready'],
926
+ state: () => ({ cart: null, loading: true, error: null, removingItemId: null }),
927
+ /**
928
+ * On screen: wait for the host's client, subscribe to the cart, then read it.
929
+ *
930
+ * Subscribing before the read is deliberate. The component started both at
931
+ * once (`componentWillLoad` read, `componentDidLoad` subscribed) and so could
932
+ * miss an update that landed mid-read; going first closes that window
933
+ * without changing anything a host can see.
934
+ *
935
+ * The subscription is the cart manager's and not the client's: the connector
936
+ * adapts the payload on the cart manager's bus and passes it through
937
+ * untouched on the client's, and this element renders the adapted figures.
938
+ */
939
+ mount: (ctx) => {
940
+ void (async () => {
941
+ let client;
942
+ try {
943
+ client = await ctx.whenClient();
944
+ }
945
+ catch (err) {
946
+ if (!ctx.active())
947
+ return;
948
+ ctx.actions.connectFailed(err instanceof Error ? err.message : ctx.strings.loadFailed);
949
+ return;
950
+ }
951
+ if (!ctx.active())
952
+ return;
953
+ const manager = client.getCartManager?.();
954
+ const off = manager?.on?.('cart:updated', (data) => {
955
+ ctx.actions.cartUpdated(data);
956
+ });
957
+ if (typeof off === 'function')
958
+ ctx.onTeardown(off);
959
+ await ctx.actions.load();
960
+ })();
961
+ },
962
+ status: ({ state }) => state.loading ? 'loading' : !state.cart || state.cart.items.length === 0 ? 'empty' : 'ready',
963
+ derive: (ctx) => {
964
+ const { state, strings, props, formatMoney } = ctx;
965
+ // Money comes from the response through cartTotals: the fee row is shown
966
+ // when the surface expressed a fee, and the total is the server's figure,
967
+ // never a sum taken here.
968
+ const totals = cartTotals(state.cart);
969
+ const currency = totals.currency;
970
+ const items = state.cart?.items ?? [];
971
+ return {
972
+ rows: items.map((item) => ({
973
+ id: item.id,
974
+ name: item.ticketTypeName,
975
+ eventName: item.eventName,
976
+ dateLabel: item.occurrenceDate ? formatDate$2(item.occurrenceDate) : null,
977
+ quantityLabel: `${strings.quantityPrefix}${item.quantity}`,
978
+ priceLabel: formatMoney(item.totalPrice, item.currency),
979
+ removing: state.removingItemId === item.id,
980
+ showEvent: !props.compact,
981
+ showDate: !props.compact && !!item.occurrenceDate,
982
+ })),
983
+ totals,
984
+ subtotalLabel: formatMoney(totals.subtotal, currency),
985
+ feeAmountLabel: formatMoney(totals.fees, currency),
986
+ discountLabel: formatMoney(totals.discount, currency),
987
+ totalLabel: formatMoney(totals.total, currency),
988
+ showSubtotalRow: !props.compact,
989
+ showFeeRow: totals.showFee && !props.compact,
990
+ showDiscountRow: totals.discount > 0 && !props.compact,
991
+ checkoutDisabled: items.length === 0,
992
+ };
993
+ },
994
+ actions: (ctx) => ({
995
+ /** Read the cart as it stands. */
996
+ async load() {
997
+ try {
998
+ ctx.setState({ loading: true, error: null });
999
+ const manager = ctx.optionalClient()?.getCartManager?.();
1000
+ if (manager) {
1001
+ // Bound rather than called optionally: a connector that carries a
1002
+ // cart manager without a `get` is broken, and the component said so
1003
+ // through tlError rather than rendering an empty cart.
1004
+ const read = manager.get?.bind(manager);
1005
+ if (!read)
1006
+ throw new TypeError('cartManager.get is not a function');
1007
+ ctx.setState({ cart: ((await read()) ?? null) });
1008
+ }
1009
+ }
1010
+ catch (err) {
1011
+ const message = err instanceof Error ? err.message : ctx.strings.loadFailed;
1012
+ ctx.setState({ error: message });
1013
+ ctx.emit('tlError', { message });
1014
+ }
1015
+ finally {
1016
+ ctx.setState({ loading: false });
1017
+ }
1018
+ },
1019
+ /** The cart changed under us. Show it, and tell the host. */
1020
+ cartUpdated(cart) {
1021
+ ctx.setState({ cart });
1022
+ ctx.emit('tlCartUpdated', { cart });
1023
+ },
1024
+ /**
1025
+ * Take one line out of the cart. With no client at all, nothing happens
1026
+ * and nothing is announced: the buyer's click is simply inert, which is
1027
+ * what it was before.
1028
+ */
1029
+ async removeItem(itemId) {
1030
+ if (!ctx.optionalClient())
1031
+ return;
1032
+ ctx.setState({ removingItemId: itemId });
1033
+ try {
1034
+ const manager = ctx.client().getCartManager?.();
1035
+ if (manager) {
1036
+ const remove = manager.removeItem?.bind(manager);
1037
+ if (!remove)
1038
+ throw new TypeError('cartManager.removeItem is not a function');
1039
+ await remove(itemId);
1040
+ ctx.emit('tlItemRemoved', { itemId });
1041
+ }
1042
+ }
1043
+ catch (err) {
1044
+ const message = err instanceof Error ? err.message : ctx.strings.removeFailed;
1045
+ ctx.setState({ error: message });
1046
+ ctx.emit('tlError', { message });
1047
+ }
1048
+ finally {
1049
+ ctx.setState({ removingItemId: null });
1050
+ }
1051
+ },
1052
+ /** Hand the cart to whatever the host does next. An empty cart says nothing. */
1053
+ checkout() {
1054
+ const cart = ctx.state.cart;
1055
+ if (cart && cart.items.length > 0)
1056
+ ctx.emit('tlCheckout', { cart });
1057
+ },
1058
+ /**
1059
+ * The host never produced a client. Announced, because the component
1060
+ * announced it: the read and the wait were one try block, so a page that
1061
+ * never started Live emitted tlError.
1062
+ */
1063
+ connectFailed(message) {
1064
+ const text = message || ctx.strings.loadFailed;
1065
+ ctx.setState({ error: text, loading: false });
1066
+ ctx.emit('tlError', { message: text });
1067
+ },
1068
+ }),
1069
+ });
1070
+
1071
+ /**
1072
+ * `tl-cart-badge`: the cart icon with the item count on it.
1073
+ *
1074
+ * Small, and the second element that lives on a subscription: it binds
1075
+ * `cart:updated` inside its own `mount` and hands the unsubscribe to
1076
+ * `ctx.onTeardown` (TKT-197), so a badge that is moved in the DOM, or rendered
1077
+ * twice by a host that mounts and unmounts it, never ends up counting twice.
1078
+ *
1079
+ * The one thing it remembers is the last count, so the badge does not flash
1080
+ * empty on the next page before the cart has been read. That is behaviour, so
1081
+ * it is here; where the count is written down is per runtime, so that is
1082
+ * `ctx.storage`, which the harness supplies.
1083
+ */
1084
+ /** Where the last known count is kept. The key the component used. */
1085
+ const CART_COUNT_KEY = 'tl_cart_count';
1086
+ const STRINGS$b = {
1087
+ /** Read as "Cart with 2 items". The count is appended. */
1088
+ ariaPrefix: 'Cart with ',
1089
+ ariaSuffix: ' items',
1090
+ overflow: '99+',
1091
+ };
1092
+ /** Above this the bubble reads "99+" rather than a number nothing can fit. */
1093
+ const OVERFLOW_ABOVE = 99;
1094
+ const countOf = (cart) => {
1095
+ const c = cart;
1096
+ return typeof c?.itemCount === 'number' ? c.itemCount : 0;
1097
+ };
1098
+ const cartBadgeCore = defineElement({
1099
+ tag: 'tl-cart-badge',
1100
+ docs: 'A cart icon carrying the number of items in the cart, kept current from the cart bus.',
1101
+ category: 'cart',
1102
+ runtimes: ['web'],
1103
+ tokens: [
1104
+ 'font-family',
1105
+ 'foreground',
1106
+ 'muted',
1107
+ 'primary',
1108
+ 'primary-foreground',
1109
+ 'radius-md',
1110
+ 'spacing-1',
1111
+ 'spacing-2',
1112
+ 'spacing-3',
1113
+ ],
1114
+ sdkMethods: ['cart.get', 'cart.on'],
1115
+ props: {
1116
+ showEmpty: {
1117
+ type: 'boolean',
1118
+ attr: 'show-empty',
1119
+ default: false,
1120
+ docs: 'Show the badge even when count is 0',
1121
+ },
1122
+ icon: {
1123
+ type: 'string',
1124
+ tsType: 'string | undefined',
1125
+ attr: 'icon',
1126
+ docs: 'Custom icon (emoji or text). If not provided, uses cart SVG',
1127
+ },
1128
+ size: {
1129
+ type: 'string',
1130
+ tsType: '"lg" | "md" | "sm"',
1131
+ attr: 'size',
1132
+ default: 'md',
1133
+ docs: 'Size variant',
1134
+ },
1135
+ badgeOnly: {
1136
+ type: 'boolean',
1137
+ attr: 'badge-only',
1138
+ default: false,
1139
+ docs: 'Badge only mode - only shows the count bubble, no icon or button',
1140
+ },
1141
+ },
1142
+ events: {
1143
+ tlClick: { detail: '{ itemCount: number; }', docs: 'Emitted when the badge is clicked' },
1144
+ },
1145
+ strings: STRINGS$b,
1146
+ statuses: ['ready'],
1147
+ state: () => ({ itemCount: 0 }),
1148
+ /**
1149
+ * On screen: show the remembered count straight away, then wait for the
1150
+ * client, subscribe, and read the cart.
1151
+ *
1152
+ * Hydrating first is what keeps the badge from flashing empty; the component
1153
+ * did it in `componentWillLoad` and this runs earlier still, in the harness's
1154
+ * `connectedCallback`. A host with no store simply starts at zero.
1155
+ */
1156
+ mount: (ctx) => {
1157
+ ctx.actions.hydrate();
1158
+ void (async () => {
1159
+ let client;
1160
+ try {
1161
+ client = await ctx.whenClient();
1162
+ }
1163
+ catch {
1164
+ // A page that never started Live keeps the remembered count and says
1165
+ // nothing, which is what the component did.
1166
+ return;
1167
+ }
1168
+ if (!ctx.active())
1169
+ return;
1170
+ const manager = client.getCartManager?.();
1171
+ if (!manager)
1172
+ return;
1173
+ const off = manager.on?.('cart:updated', (cart) => {
1174
+ ctx.actions.countChanged(countOf(cart));
1175
+ });
1176
+ if (typeof off === 'function')
1177
+ ctx.onTeardown(off);
1178
+ try {
1179
+ const read = manager.get?.bind(manager);
1180
+ if (!read)
1181
+ return;
1182
+ const cart = await read();
1183
+ if (!ctx.active())
1184
+ return;
1185
+ ctx.actions.countChanged(countOf(cart));
1186
+ }
1187
+ catch {
1188
+ // The cart may not exist yet. Keep the remembered value.
1189
+ }
1190
+ })();
1191
+ },
1192
+ status: () => 'ready',
1193
+ derive: ({ state, props, strings }) => ({
1194
+ displayCount: state.itemCount > OVERFLOW_ABOVE ? strings.overflow : state.itemCount.toString(),
1195
+ showBadge: state.itemCount > 0,
1196
+ renderNothing: props.badgeOnly && state.itemCount === 0,
1197
+ ariaLabel: `${strings.ariaPrefix}${state.itemCount}${strings.ariaSuffix}`,
1198
+ sizeClass: `size-${props.size}`,
1199
+ }),
1200
+ actions: (ctx) => ({
1201
+ /** A new count from the cart: show it, and remember it for next time. */
1202
+ countChanged(count) {
1203
+ ctx.setState({ itemCount: count });
1204
+ ctx.storage.set(CART_COUNT_KEY, String(count));
1205
+ },
1206
+ /** Start from whatever the last visit left behind, or zero. */
1207
+ hydrate() {
1208
+ const stored = ctx.storage.get(CART_COUNT_KEY);
1209
+ ctx.setState({ itemCount: (stored ? parseInt(stored, 10) : 0) || 0 });
1210
+ },
1211
+ /** The buyer asked for the cart. What that opens is the host's business. */
1212
+ click() {
1213
+ ctx.emit('tlClick', { itemCount: ctx.state.itemCount });
1214
+ },
1215
+ }),
1216
+ });
1217
+
1218
+ /**
1219
+ * `tl-cart-drawer`: the off-canvas panel the cart slides into.
1220
+ *
1221
+ * It owns no cart of its own - the panel embeds `tl-cart` - so what is here is
1222
+ * the open/closed rule and the two bus intents it listens for, bound inside its
1223
+ * own `mount` and unbound by the teardown (TKT-197).
1224
+ *
1225
+ * Two ways in, and they are not the same, which is the subtlety this core has
1226
+ * to keep: an intent, the backdrop, the close button and Escape all go through
1227
+ * `setOpen`, which announces the change with `tlOpen` or `tlClose`. The host
1228
+ * setting the `open` prop goes through `openChanged`, which does not: the
1229
+ * component's `@Watch('open')` moved the panel without emitting, and a host
1230
+ * that drove the drawer and also listened would otherwise start hearing its
1231
+ * own writes back.
1232
+ *
1233
+ * The subscription is the client's bus and not the cart manager's: these are
1234
+ * UI intents, which `@ticketlayer/live` relays untouched, and nothing here
1235
+ * reads the cart.
1236
+ */
1237
+ /** What opens the drawer, and what closes it. Relayed on the client's bus. */
1238
+ const OPEN_INTENT = 'ui:cart-requested';
1239
+ const CLOSE_INTENT = 'ui:modal-close-requested';
1240
+ const STRINGS$a = {
1241
+ close: 'Close cart',
1242
+ privacy: 'Privacy Policy',
1243
+ privacyHref: 'https://ticketlayer.com/privacy',
1244
+ reportAbuse: 'Report Abuse',
1245
+ reportAbuseHref: 'https://ticketlayer.com/report-abuse',
1246
+ poweredBy: 'Powered by',
1247
+ brand: 'ticketlayer',
1248
+ brandHref: 'https://ticketlayer.com',
1249
+ };
1250
+ const cartDrawerCore = defineElement({
1251
+ tag: 'tl-cart-drawer',
1252
+ docs: 'A slide-in panel holding the cart, opened by the cart intent on the client bus.',
1253
+ category: 'cart',
1254
+ runtimes: ['web'],
1255
+ tokens: ['background', 'border', 'font-family', 'foreground', 'muted', 'muted-foreground'],
1256
+ sdkMethods: ['client.on'],
1257
+ props: {
1258
+ side: {
1259
+ type: 'string',
1260
+ tsType: '"left" | "right"',
1261
+ attr: 'side',
1262
+ default: 'right',
1263
+ docs: 'Edge the panel slides in from.',
1264
+ },
1265
+ heading: {
1266
+ type: 'string',
1267
+ attr: 'heading',
1268
+ default: 'Your cart',
1269
+ docs: 'Heading shown at the top of the panel.',
1270
+ },
1271
+ open: {
1272
+ type: 'boolean',
1273
+ attr: 'open',
1274
+ default: false,
1275
+ docs: 'Controlled open state (also driven by the `ui:cart-requested` intent).',
1276
+ },
1277
+ },
1278
+ events: {
1279
+ tlOpen: { detail: 'void', docs: '' },
1280
+ tlClose: { detail: 'void', docs: '' },
1281
+ },
1282
+ strings: STRINGS$a,
1283
+ statuses: ['open', 'closed'],
1284
+ state: (props) => ({ visible: props.open }),
1285
+ /**
1286
+ * On screen: take the `open` prop as it stands, then listen for the intents.
1287
+ *
1288
+ * Taking the prop again is what the component's `connectedCallback` did, and
1289
+ * it matters here for a second reason: an instance is built before a harness
1290
+ * has read its attributes, so the state a constructor could see is the
1291
+ * default rather than the host's.
1292
+ */
1293
+ mount: (ctx) => {
1294
+ ctx.actions.openChanged(ctx.props.open);
1295
+ void (async () => {
1296
+ let client;
1297
+ try {
1298
+ client = await ctx.whenClient();
1299
+ }
1300
+ catch {
1301
+ // No client, no intents. The drawer still opens from its `open` prop.
1302
+ return;
1303
+ }
1304
+ if (!ctx.active())
1305
+ return;
1306
+ const offOpen = client.on?.(OPEN_INTENT, () => ctx.actions.setOpen(true));
1307
+ if (typeof offOpen === 'function')
1308
+ ctx.onTeardown(offOpen);
1309
+ const offClose = client.on?.(CLOSE_INTENT, () => ctx.actions.setOpen(false));
1310
+ if (typeof offClose === 'function')
1311
+ ctx.onTeardown(offClose);
1312
+ })();
1313
+ },
1314
+ status: ({ state }) => (state.visible ? 'open' : 'closed'),
1315
+ derive: ({ state, props }) => ({
1316
+ visible: state.visible,
1317
+ sideClass: `panel--${props.side}`,
1318
+ ariaHidden: state.visible ? 'false' : 'true',
1319
+ }),
1320
+ actions: (ctx) => ({
1321
+ /** Open or close, and say so. Asking for the state it is already in is nothing. */
1322
+ setOpen(open) {
1323
+ if (ctx.state.visible === open)
1324
+ return;
1325
+ ctx.setState({ visible: open });
1326
+ ctx.emit(open ? 'tlOpen' : 'tlClose', undefined);
1327
+ },
1328
+ /** The host moved the `open` prop. Follow it, and stay quiet about it. */
1329
+ openChanged(open) {
1330
+ if (ctx.state.visible === open)
1331
+ return;
1332
+ ctx.setState({ visible: open });
1333
+ },
1334
+ }),
1335
+ });
1336
+
1337
+ /**
1338
+ * `tl-checkout-wrapper`: the same idea as `tl-buy-tickets-wrapper`, pointed at
1339
+ * the checkout rather than at one event.
1340
+ *
1341
+ * Whatever the host slots in becomes the thing that opens the checkout modal
1342
+ * for the cart as it stands, so this core takes no subject at all: the cart is
1343
+ * the client's, and the modal is the client's.
1344
+ *
1345
+ * It was blocked from being split by the same thing its sibling was - events
1346
+ * named `tl-complete`, `tl-error` and `tl-close`, which `defineElement`
1347
+ * refuses - and is unblocked by the same rename to `tlComplete`, `tlError` and
1348
+ * `tlClose` (TKT-203).
1349
+ *
1350
+ * Behaviour is what the Stencil component did, to the letter, including the
1351
+ * `tlError` a click announces when no client has been published; the
1352
+ * `console.error` beside it did not come across, for the reason `tl-cart` and
1353
+ * `tl-promo-code` dropped theirs.
1354
+ */
1355
+ const STRINGS$9 = {
1356
+ noClient: 'SDK not initialized',
1357
+ notAvailable: 'Checkout is not available',
1358
+ };
1359
+ const checkoutWrapperCore = defineElement({
1360
+ tag: 'tl-checkout-wrapper',
1361
+ docs: 'Wrap any content and open the checkout modal when it is activated.',
1362
+ category: 'checkout',
1363
+ runtimes: ['web'],
1364
+ tokens: ['radius-sm', 'ring'],
1365
+ sdkMethods: ['checkout.openModal'],
1366
+ props: {
1367
+ disabled: {
1368
+ type: 'boolean',
1369
+ attr: 'disabled',
1370
+ default: false,
1371
+ docs: 'Whether the wrapper is disabled',
1372
+ },
1373
+ },
1374
+ events: {
1375
+ // `OrderResult` because that is the name the web harness writes; see
1376
+ // `buy-tickets-wrapper.ts`.
1377
+ tlComplete: { detail: 'OrderResult', docs: 'Emitted when checkout completes successfully' },
1378
+ tlError: { detail: '{ message: string; }', docs: 'Emitted when an error occurs' },
1379
+ tlClose: { detail: 'void', docs: 'Emitted when the modal closes' },
1380
+ },
1381
+ strings: STRINGS$9,
1382
+ statuses: ['ready', 'disabled'],
1383
+ state: () => ({}),
1384
+ status: ({ props }) => (props.disabled ? 'disabled' : 'ready'),
1385
+ derive: ({ props }) => ({ interactive: !props.disabled }),
1386
+ actions: (ctx) => ({
1387
+ /** A click, a tap, Enter or Space: whatever the harness calls a press. */
1388
+ activate() {
1389
+ if (ctx.props.disabled)
1390
+ return;
1391
+ const client = ctx.optionalClient();
1392
+ if (!client) {
1393
+ ctx.emit('tlError', { message: ctx.strings.noClient });
1394
+ return;
1395
+ }
1396
+ if (!client.checkout) {
1397
+ ctx.emit('tlError', { message: ctx.strings.notAvailable });
1398
+ return;
1399
+ }
1400
+ client.checkout.openModal({
1401
+ onComplete: (order) => ctx.emit('tlComplete', order),
1402
+ onError: (err) => ctx.emit('tlError', { message: err.message }),
1403
+ onClose: () => ctx.emit('tlClose', undefined),
1404
+ });
1405
+ },
1406
+ }),
1407
+ });
1408
+
1409
+ /**
1410
+ * `tl-event-card`: one event, as a card.
1411
+ *
1412
+ * Two ways to fill it, which is the whole of its behaviour: a host hands it an
1413
+ * event through the `event` prop (that is what `tl-event-list` does), or it is
1414
+ * given an `eventId` and reads the event itself. The prop wins, and a card that
1415
+ * has one never touches the client.
1416
+ *
1417
+ * Behaviour is what the Stencil component did before the split, to the letter,
1418
+ * including the status badge's three labels and the `en-GB` date. What did not
1419
+ * come across is the `console.log` tracing, which a library should not do for
1420
+ * its host.
1421
+ */
1422
+ /**
1423
+ * Adapt an event detail to the card shape. The list adapter lives in the SDK
1424
+ * connector; this one is the card's own, and it is behaviour, so it is here.
1425
+ */
1426
+ function toEventCardEvent(detail) {
1427
+ const occurrences = detail.occurrences ?? [];
1428
+ const starts = occurrences
1429
+ .map((o) => o.startsAt)
1430
+ .filter((s) => !!s)
1431
+ .sort();
1432
+ const now = Date.now();
1433
+ const next = starts.find((s) => new Date(s).getTime() >= now) ?? starts[0];
1434
+ const soldOut = detail.status === 'sold_out' ||
1435
+ (occurrences.length > 0 && occurrences.every((o) => o.status === 'sold_out'));
1436
+ return {
1437
+ id: detail.id,
1438
+ name: detail.name,
1439
+ ...(detail.shortDescription ? { shortDescription: detail.shortDescription } : {}),
1440
+ ...(detail.imageUrl ? { imageUrl: detail.imageUrl } : {}),
1441
+ ...(detail.venueName
1442
+ ? { venue: { id: detail.venueId ?? '', name: detail.venueName, city: detail.venueCity ?? '' } }
1443
+ : {}),
1444
+ ...(next ? { nextOccurrence: { startsAt: next } } : {}),
1445
+ availabilityStatus: soldOut ? 'sold_out' : 'available',
1446
+ };
1447
+ }
1448
+ const STRINGS$8 = {
1449
+ available: 'Available',
1450
+ limited: 'Limited',
1451
+ soldOut: 'Sold Out',
1452
+ notFound: 'Event not found',
1453
+ loadFailed: 'Failed to load event',
1454
+ };
1455
+ /** As the component formatted it. See the note on the cart core's date. */
1456
+ function formatDate$1(dateString) {
1457
+ return new Date(dateString).toLocaleDateString('en-GB', {
1458
+ weekday: 'short',
1459
+ day: 'numeric',
1460
+ month: 'short',
1461
+ hour: '2-digit',
1462
+ minute: '2-digit',
1463
+ });
1464
+ }
1465
+ const eventCardCore = defineElement({
1466
+ tag: 'tl-event-card',
1467
+ docs: 'One event as a card: its image, name, next date, venue and how it is selling.',
1468
+ category: 'discovery',
1469
+ runtimes: ['web', 'native'],
1470
+ tokens: [
1471
+ 'error',
1472
+ 'error-foreground',
1473
+ 'font-family',
1474
+ 'foreground',
1475
+ 'muted',
1476
+ 'muted-foreground',
1477
+ 'primary',
1478
+ 'radius-lg',
1479
+ 'radius-sm',
1480
+ 'secondary',
1481
+ 'spacing-4',
1482
+ 'success',
1483
+ 'success-foreground',
1484
+ 'warning',
1485
+ 'warning-foreground',
1486
+ ],
1487
+ sdkMethods: ['events.get'],
1488
+ props: {
1489
+ event: {
1490
+ type: 'object',
1491
+ tsType: 'TlEventCardEvent | undefined',
1492
+ attr: null,
1493
+ docs: 'Event data to display (tl-event-list passes this)',
1494
+ },
1495
+ eventId: {
1496
+ type: 'string',
1497
+ tsType: 'string | undefined',
1498
+ attr: 'event-id',
1499
+ docs: 'Load the event from the SDK by id when no `event` is given, so a single\ncard works standalone (e.g. on a Webflow page).',
1500
+ },
1501
+ },
1502
+ events: {
1503
+ tlClick: { detail: '{ eventId: string; }', docs: 'Emitted when the card is clicked' },
1504
+ tlError: { detail: '{ message: string; }', docs: 'Emitted when the event could not be loaded by id' },
1505
+ },
1506
+ strings: STRINGS$8,
1507
+ statuses: ['loading', 'error', 'empty', 'ready'],
1508
+ state: () => ({ loadedEvent: null, loading: false, error: null }),
1509
+ /**
1510
+ * On screen: read the event, but only when there is one to read and nobody
1511
+ * has handed us the event already.
1512
+ *
1513
+ * `loading` is set here, synchronously, rather than inside the read. The
1514
+ * component awaited its load in `componentWillLoad`, so its first paint was
1515
+ * already past it; a mount hook cannot hold the first paint, so the card
1516
+ * shows its own skeleton instead of a blank. That is the same change
1517
+ * `tl-ticket-selector` took when it moved onto the hook (TKT-197).
1518
+ */
1519
+ mount: (ctx) => {
1520
+ if (ctx.props.event || !ctx.props.eventId)
1521
+ return;
1522
+ ctx.setState({ loading: true, error: null });
1523
+ void (async () => {
1524
+ try {
1525
+ await ctx.whenClient();
1526
+ }
1527
+ catch (err) {
1528
+ if (!ctx.active())
1529
+ return;
1530
+ ctx.actions.loadFailed(err instanceof Error && err.message ? err.message : ctx.strings.loadFailed);
1531
+ return;
1532
+ }
1533
+ if (!ctx.active())
1534
+ return;
1535
+ await ctx.actions.load();
1536
+ })();
1537
+ },
1538
+ status: ({ state, props }) => state.loading ? 'loading' : state.error ? 'error' : props.event || state.loadedEvent ? 'ready' : 'empty',
1539
+ derive: ({ state, props, strings }) => {
1540
+ const event = props.event ?? state.loadedEvent;
1541
+ const badge = (suffix) => `status-badge ${suffix}`;
1542
+ const status = event?.availabilityStatus;
1543
+ return {
1544
+ event,
1545
+ // The sales surface exposes a single `imageUrl` (card/listing
1546
+ // thumbnail); `thumbnailUrl` is an accepted alias if a host ever
1547
+ // supplies a distinct one.
1548
+ image: event ? event.thumbnailUrl || event.imageUrl || null : null,
1549
+ statusBadgeClass: status === 'available'
1550
+ ? badge('status-available')
1551
+ : status === 'limited'
1552
+ ? badge('status-limited')
1553
+ : status === 'sold_out'
1554
+ ? badge('status-sold-out')
1555
+ : 'status-badge',
1556
+ statusLabel: status === 'available'
1557
+ ? strings.available
1558
+ : status === 'limited'
1559
+ ? strings.limited
1560
+ : status === 'sold_out'
1561
+ ? strings.soldOut
1562
+ : '',
1563
+ dateLabel: event?.nextOccurrence ? formatDate$1(event.nextOccurrence.startsAt) : null,
1564
+ venueLabel: event?.venue ? `${event.venue.name}, ${event.venue.city}` : null,
1565
+ };
1566
+ },
1567
+ actions: (ctx) => ({
1568
+ /**
1569
+ * Read the event by id. A card that was handed an event, or has no id, has
1570
+ * nothing to read, and this is the guard the component's watcher relied on.
1571
+ */
1572
+ async load() {
1573
+ if (ctx.props.event || !ctx.props.eventId)
1574
+ return;
1575
+ try {
1576
+ ctx.setState({ loading: true, error: null });
1577
+ const events = ctx.client().getEventsManager();
1578
+ const read = events.get?.bind(events);
1579
+ if (!read)
1580
+ throw new TypeError('eventsManager.get is not a function');
1581
+ const detail = (await read(ctx.props.eventId));
1582
+ if (!detail)
1583
+ throw new Error(ctx.strings.notFound);
1584
+ ctx.setState({ loadedEvent: toEventCardEvent(detail) });
1585
+ }
1586
+ catch (err) {
1587
+ const message = err instanceof Error && err.message ? err.message : ctx.strings.loadFailed;
1588
+ ctx.setState({ error: message });
1589
+ ctx.emit('tlError', { message });
1590
+ }
1591
+ finally {
1592
+ ctx.setState({ loading: false });
1593
+ }
1594
+ },
1595
+ /**
1596
+ * The host never produced a client. Announced, because the component
1597
+ * announced it: the wait and the read were one try block.
1598
+ */
1599
+ loadFailed(message) {
1600
+ const text = message || ctx.strings.loadFailed;
1601
+ ctx.setState({ error: text, loading: false });
1602
+ ctx.emit('tlError', { message: text });
1603
+ },
1604
+ /** The buyer picked this card. Where that goes is the host's business. */
1605
+ click() {
1606
+ const event = ctx.props.event ?? ctx.state.loadedEvent;
1607
+ if (!event)
1608
+ return;
1609
+ ctx.emit('tlClick', { eventId: event.id });
1610
+ },
1611
+ }),
1612
+ });
1613
+
1614
+ /**
1615
+ * `tl-event-list`: what the channel is selling, as a grid of cards.
1616
+ *
1617
+ * One read, and a re-read whenever the host changes what it is asking for. The
1618
+ * cards themselves are `tl-event-card`, which has its own core; this one
1619
+ * decides only what to ask for and what the grid says while there is nothing
1620
+ * in it.
1621
+ *
1622
+ * Behaviour is what the Stencil component did, to the letter, including the
1623
+ * two things that read like oversights and are kept:
1624
+ *
1625
+ * - there is NO error event. A failed listing is drawn and never announced,
1626
+ * which makes this the only element of the eighteen that can fail silently
1627
+ * as far as a host is concerned. Adding `tlError` would be a new event on a
1628
+ * published element, so it is a decision for somebody rather than something
1629
+ * to slip into a refactor.
1630
+ * - the element paints nothing until the first listing lands. It holds
1631
+ * skeleton markup and uses it for every read but the first, because the
1632
+ * component awaited its read in `componentWillLoad` and Stencil does not
1633
+ * paint until that resolves. The harness keeps that by waiting on the same
1634
+ * thing; the core simply reports `loading` and lets the harness decide when
1635
+ * a first paint happens, which is a rendering question.
1636
+ *
1637
+ * What did not come across is the `console.log` tracing, which a library
1638
+ * should not do for its host (`tl-cart` dropped its own for the same reason).
1639
+ */
1640
+ const STRINGS$7 = {
1641
+ errorTitle: 'Error loading events',
1642
+ emptyTitle: 'No events available',
1643
+ emptyMessage: 'Check back later for upcoming events.',
1644
+ loadFailed: 'Failed to load events',
1645
+ noListing: 'Events are not available',
1646
+ };
1647
+ /** The grid has a class per column count, and only four of them. */
1648
+ const MIN_COLUMNS = 1;
1649
+ const MAX_COLUMNS = 4;
1650
+ const eventListCore = defineElement({
1651
+ tag: 'tl-event-list',
1652
+ docs: "What the channel is selling, as a grid of event cards, optionally narrowed to one category.",
1653
+ category: 'discovery',
1654
+ runtimes: ['web', 'native'],
1655
+ tokens: [
1656
+ 'error',
1657
+ 'font-family',
1658
+ 'foreground',
1659
+ 'muted',
1660
+ 'muted-foreground',
1661
+ 'radius-lg',
1662
+ 'radius-sm',
1663
+ 'secondary',
1664
+ 'spacing-4',
1665
+ 'spacing-6',
1666
+ 'spacing-8',
1667
+ ],
1668
+ sdkMethods: ['events.list'],
1669
+ props: {
1670
+ limit: { type: 'number', attr: 'limit', default: 12, docs: 'Maximum number of events to display' },
1671
+ category: {
1672
+ type: 'string',
1673
+ tsType: 'string | undefined',
1674
+ attr: 'category',
1675
+ docs: 'Category filter',
1676
+ },
1677
+ columns: { type: 'number', attr: 'columns', default: 3, docs: 'Columns for grid layout (1, 2, 3, or 4)' },
1678
+ },
1679
+ events: {
1680
+ tlEventClick: { detail: '{ eventId: string; }', docs: 'Emitted when an event card is clicked' },
1681
+ },
1682
+ strings: STRINGS$7,
1683
+ statuses: ['loading', 'error', 'empty', 'ready'],
1684
+ state: () => ({ events: [], loading: true, error: null }),
1685
+ mount: (ctx) => {
1686
+ void ctx.actions.load();
1687
+ },
1688
+ status: ({ state }) => state.loading ? 'loading' : state.error ? 'error' : state.events.length === 0 ? 'empty' : 'ready',
1689
+ derive: ({ state, props }) => ({
1690
+ events: state.events,
1691
+ columns: Math.min(Math.max(props.columns, MIN_COLUMNS), MAX_COLUMNS),
1692
+ }),
1693
+ actions: (ctx) => ({
1694
+ /**
1695
+ * Read the listing. Also what a harness calls when the limit or the
1696
+ * category changes, because a different question deserves a fresh answer
1697
+ * and the previous one is not narrowed down to it.
1698
+ */
1699
+ async load() {
1700
+ try {
1701
+ ctx.setState({ loading: true, error: null });
1702
+ const client = await ctx.whenClient();
1703
+ const events = client.getEventsManager();
1704
+ if (!events?.list)
1705
+ throw new TypeError(ctx.strings.noListing);
1706
+ const response = (await events.list({ limit: ctx.props.limit, category: ctx.props.category }));
1707
+ ctx.setState({ events: (response?.items ?? []) });
1708
+ }
1709
+ catch (err) {
1710
+ // Shown and not announced: this element has no error event.
1711
+ ctx.setState({ error: err instanceof Error ? err.message : ctx.strings.loadFailed });
1712
+ }
1713
+ finally {
1714
+ ctx.setState({ loading: false });
1715
+ }
1716
+ },
1717
+ }),
1718
+ });
1719
+
1720
+ /**
1721
+ * `tl-login`: magic-link sign-in. No password is handled anywhere in here.
1722
+ *
1723
+ * The customer enters an email, the client is asked to send a link, and the
1724
+ * code that comes back is exchanged for a session either because the link
1725
+ * landed on this page with `?code=` or because the customer typed the six
1726
+ * digits out of the email. Both go through the same `exchange` action.
1727
+ *
1728
+ * The first core to read {@link ActionContext.query}, the seam this split
1729
+ * added. An element that is the far end of an emailed link has to be able to
1730
+ * see what the link said, and where that is written down is per runtime: the
1731
+ * web has `window.location.search`, a native harness has its deep-link router.
1732
+ * So the core says it reads `code`, once, on mount, and the harness says where
1733
+ * from. A harness that supplies no query reads null and the element simply
1734
+ * starts on the email step, which is what a page with no code in its URL does
1735
+ * anyway.
1736
+ *
1737
+ * Behaviour is what the Stencil component did, to the letter, including:
1738
+ *
1739
+ * - the URL code is single use. It is taken once per instance and never read
1740
+ * again, so a remount cannot spend a code that was already exchanged.
1741
+ * - a failed exchange goes back to where it came from, and the two ways in
1742
+ * are told apart: a typed code returns to the code step, a URL code returns
1743
+ * to the email step.
1744
+ * - a failed sign out is swallowed. The customer is shown as signed out and
1745
+ * `tlAuthChanged` says so, because the session is gone locally either way,
1746
+ * and that is what the component did.
1747
+ *
1748
+ * What did not come across is the `console.error` tracing, which a library
1749
+ * should not do for its host (`tl-cart` dropped its own for the same reason).
1750
+ */
1751
+ const STRINGS$6 = {
1752
+ signedInPrefix: 'Signed in as ',
1753
+ fallbackAccount: 'your account',
1754
+ signOut: 'Sign out',
1755
+ verifyingTitle: 'Signing you in…',
1756
+ verifyingLead: 'Just a moment.',
1757
+ sentTitle: 'Check your inbox',
1758
+ // The lead reads "We've sent a sign-in link to <strong>you@…</strong>. Open
1759
+ // it on this device to continue." Split in two because the address is
1760
+ // emphasised in the middle of it, and the shape has no way to say "this copy
1761
+ // has a slot in it" - see the README's note on what a core still cannot
1762
+ // express.
1763
+ sentLeadBefore: "We've sent a sign-in link to ",
1764
+ sentLeadAfter: '. Open it on this device to continue.',
1765
+ sentAgain: 'Sent again. Check your inbox.',
1766
+ resendLink: 'Resend link',
1767
+ resending: 'Sending…',
1768
+ enterCodeInstead: 'Enter the code instead',
1769
+ differentEmail: 'Use a different email',
1770
+ codeTitle: 'Enter your code',
1771
+ codeLeadBefore: 'Type the 6-digit code from the email we sent to ',
1772
+ codeLeadAfter: '.',
1773
+ codeLabel: 'Code',
1774
+ codePlaceholder: '123456',
1775
+ codeSubmit: 'Sign in',
1776
+ checking: 'Checking…',
1777
+ back: 'Back',
1778
+ emailLead: "Enter your email and we'll send you a secure sign-in link. No password needed.",
1779
+ emailLabel: 'Email address',
1780
+ emailPlaceholder: 'you@example.com',
1781
+ emailSubmit: 'Email me a sign-in link',
1782
+ sendingLink: 'Sending link…',
1783
+ enterEmail: 'Enter your email address',
1784
+ enterCode: 'Enter the code from your email',
1785
+ codeRejected: 'That code did not work. Check it and try again, or request a new link.',
1786
+ requestFailed: 'Could not send your sign-in link',
1787
+ unavailable: 'Sign-in is not available right now',
1788
+ unsupported: 'Sign-in is not available with this SDK',
1789
+ };
1790
+ const loginCore = defineElement({
1791
+ tag: 'tl-login',
1792
+ docs: 'Magic-link sign-in: an email in, a link or a six-digit code back, and a session. No password is ever handled.',
1793
+ category: 'account',
1794
+ runtimes: ['web', 'native'],
1795
+ tokens: [
1796
+ 'background',
1797
+ 'border',
1798
+ 'error',
1799
+ 'font-family',
1800
+ 'font-family-mono',
1801
+ 'foreground',
1802
+ 'muted-foreground',
1803
+ 'primary',
1804
+ 'primary-foreground',
1805
+ 'radius-lg',
1806
+ 'radius-md',
1807
+ 'secondary',
1808
+ 'spacing-3',
1809
+ 'spacing-4',
1810
+ 'spacing-6',
1811
+ 'success',
1812
+ ],
1813
+ sdkMethods: ['auth.requestMagicLink', 'auth.exchange', 'auth.logout', 'on'],
1814
+ props: {
1815
+ heading: { type: 'string', attr: 'heading', default: 'Sign in', docs: 'Heading for the email step.' },
1816
+ allowCodeEntry: {
1817
+ type: 'boolean',
1818
+ attr: 'allow-code-entry',
1819
+ default: true,
1820
+ docs: 'Offer a "enter the code" path alongside the emailed link (default true).',
1821
+ },
1822
+ },
1823
+ events: {
1824
+ tlAuthChanged: { detail: 'TlAuthChanged', docs: 'signed in or out ({ customer, authenticated })' },
1825
+ tlMagicLinkSent: { detail: '{ email: string; }', docs: 'a link was requested for an email' },
1826
+ tlError: { detail: '{ message: string; }', docs: 'request or code exchange failed' },
1827
+ },
1828
+ strings: STRINGS$6,
1829
+ statuses: ['email', 'sent', 'code', 'verifying', 'signed-in'],
1830
+ state: () => ({
1831
+ phase: 'email',
1832
+ email: '',
1833
+ code: '',
1834
+ busy: false,
1835
+ error: null,
1836
+ notice: null,
1837
+ customer: null,
1838
+ authAvailable: false,
1839
+ urlCodeUsed: false,
1840
+ }),
1841
+ /**
1842
+ * On screen: wait for the client, read who is signed in, follow both auth
1843
+ * signals, then spend the code the link arrived with, if it did.
1844
+ *
1845
+ * The code is taken from the query whether or not it gets used, which is
1846
+ * what makes it single use: arriving already signed in burns it, exactly as
1847
+ * the component did.
1848
+ */
1849
+ mount: (ctx) => {
1850
+ void (async () => {
1851
+ let client;
1852
+ try {
1853
+ client = await ctx.whenClient();
1854
+ }
1855
+ catch {
1856
+ if (!ctx.active())
1857
+ return;
1858
+ ctx.actions.connectFailed();
1859
+ return;
1860
+ }
1861
+ if (!ctx.active())
1862
+ return;
1863
+ const auth = client.getAuthManager?.();
1864
+ if (!auth) {
1865
+ ctx.actions.unsupported();
1866
+ return;
1867
+ }
1868
+ ctx.setState({ authAvailable: true });
1869
+ ctx.actions.syncFromClient();
1870
+ // The session hydrates after the client publishes; follow both signals.
1871
+ for (const event of ['auth:changed', 'session:changed']) {
1872
+ const off = client.on?.(event, () => ctx.actions.syncFromClient());
1873
+ if (typeof off === 'function')
1874
+ ctx.onTeardown(off);
1875
+ }
1876
+ const urlCode = ctx.state.urlCodeUsed ? null : ctx.query.get('code');
1877
+ if (urlCode) {
1878
+ ctx.setState({ urlCodeUsed: true });
1879
+ if (!auth.authenticated)
1880
+ await ctx.actions.exchange(urlCode);
1881
+ }
1882
+ })();
1883
+ },
1884
+ status: ({ state }) => state.phase,
1885
+ derive: ({ state, strings, props }) => ({
1886
+ signedInEmail: state.customer?.email ?? strings.fallbackAccount,
1887
+ inputsDisabled: state.busy || !state.authAvailable,
1888
+ emailSubmitLabel: state.busy ? strings.sendingLink : strings.emailSubmit,
1889
+ resendLabel: state.busy ? strings.resending : strings.resendLink,
1890
+ codeSubmitLabel: state.busy ? strings.checking : strings.codeSubmit,
1891
+ showCodeEntry: props.allowCodeEntry,
1892
+ }),
1893
+ // Named so `submitCode` can call `exchange`: the action context does not
1894
+ // carry the actions, only a mount's does.
1895
+ actions: (ctx) => {
1896
+ /** The auth manager as it stands, or null on a client that has none. */
1897
+ const authManager = () => ctx.optionalClient()?.getAuthManager?.() ?? null;
1898
+ const self = {
1899
+ /** Typing clears whatever the last attempt complained about. */
1900
+ setEmail(value) {
1901
+ ctx.setState({ email: value, error: null });
1902
+ },
1903
+ setCode(value) {
1904
+ ctx.setState({ code: value, error: null });
1905
+ },
1906
+ /**
1907
+ * Ask for a link. The response always reports success, so nothing here
1908
+ * can reveal whether an account exists.
1909
+ */
1910
+ async requestLink(resend = false) {
1911
+ const email = ctx.state.email.trim();
1912
+ if (!email) {
1913
+ ctx.setState({ error: ctx.strings.enterEmail });
1914
+ return;
1915
+ }
1916
+ const auth = authManager();
1917
+ if (!auth)
1918
+ return;
1919
+ ctx.setState({ busy: true, error: null, notice: null });
1920
+ try {
1921
+ await auth.requestMagicLink({ email });
1922
+ ctx.setState({ phase: 'sent', notice: resend ? ctx.strings.sentAgain : null });
1923
+ ctx.emit('tlMagicLinkSent', { email });
1924
+ }
1925
+ catch (err) {
1926
+ const message = errorMessage(err, ctx.strings.requestFailed);
1927
+ ctx.setState({ error: message });
1928
+ ctx.emit('tlError', { message });
1929
+ }
1930
+ finally {
1931
+ ctx.setState({ busy: false });
1932
+ }
1933
+ },
1934
+ /** The code the customer typed. */
1935
+ async submitCode() {
1936
+ await self.exchange(ctx.state.code.trim());
1937
+ },
1938
+ /**
1939
+ * Trade a code for a session. On failure the step it came from decides
1940
+ * where it goes back to: a typed code returns to the code step, a URL
1941
+ * code (which arrives while the phase is still `email`) returns to the
1942
+ * email step.
1943
+ */
1944
+ async exchange(code) {
1945
+ if (!code) {
1946
+ ctx.setState({ error: ctx.strings.enterCode });
1947
+ return;
1948
+ }
1949
+ const auth = authManager();
1950
+ if (!auth)
1951
+ return;
1952
+ const previous = ctx.state.phase;
1953
+ ctx.setState({ busy: true, error: null, phase: 'verifying' });
1954
+ try {
1955
+ const res = (await auth.exchange(code));
1956
+ const customer = auth.customer ?? res?.customer ?? null;
1957
+ ctx.setState({ customer, phase: 'signed-in', code: '' });
1958
+ ctx.emit('tlAuthChanged', { customer, authenticated: true });
1959
+ }
1960
+ catch {
1961
+ ctx.setState({
1962
+ phase: previous === 'verifying' || previous === 'email' ? 'email' : 'code',
1963
+ error: ctx.strings.codeRejected,
1964
+ });
1965
+ ctx.emit('tlError', { message: ctx.strings.codeRejected });
1966
+ }
1967
+ finally {
1968
+ ctx.setState({ busy: false });
1969
+ }
1970
+ },
1971
+ /**
1972
+ * Sign out. A failure is swallowed on purpose: the session is gone from
1973
+ * this page either way, so the customer is shown as signed out and the
1974
+ * host is told so.
1975
+ */
1976
+ async signOut() {
1977
+ const auth = authManager();
1978
+ if (!auth)
1979
+ return;
1980
+ ctx.setState({ busy: true });
1981
+ try {
1982
+ await auth.logout();
1983
+ }
1984
+ catch {
1985
+ // Shown as signed out regardless, which is what the component did.
1986
+ }
1987
+ finally {
1988
+ ctx.setState({ busy: false, customer: null, phase: 'email' });
1989
+ ctx.emit('tlAuthChanged', { customer: null, authenticated: false });
1990
+ }
1991
+ },
1992
+ /** The steps a buyer can walk between by hand. */
1993
+ goTo(phase) {
1994
+ ctx.setState({ phase });
1995
+ },
1996
+ /**
1997
+ * The client says the session changed. Signed in wins outright; signed
1998
+ * out only pulls the element back when it was showing the signed-in
1999
+ * step, so a customer half way through typing a code is left alone.
2000
+ */
2001
+ syncFromClient() {
2002
+ const auth = authManager();
2003
+ if (!auth)
2004
+ return;
2005
+ if (auth.authenticated) {
2006
+ ctx.setState({ customer: auth.customer ?? null, phase: 'signed-in' });
2007
+ }
2008
+ else if (ctx.state.phase === 'signed-in') {
2009
+ ctx.setState({ customer: null, phase: 'email' });
2010
+ }
2011
+ },
2012
+ /** The host never started Live. Said on screen, and nowhere else. */
2013
+ connectFailed() {
2014
+ ctx.setState({ error: ctx.strings.unavailable });
2015
+ },
2016
+ /** A connector with no auth accessor cannot sign anybody in. */
2017
+ unsupported() {
2018
+ ctx.setState({ error: ctx.strings.unsupported });
2019
+ },
2020
+ };
2021
+ return self;
2022
+ },
2023
+ });
2024
+
2025
+ /**
2026
+ * `tl-promo-code`: an input and an Apply button that redeems a presale or
2027
+ * promo code for the current cart.
2028
+ *
2029
+ * Redemption unlocks gated pricing on the customer; the client re-reads the
2030
+ * cart and publishes `cart:updated`, so `tl-cart` and the badges re-render on
2031
+ * their own and this element subscribes to nothing. It has nothing to do when
2032
+ * a harness mounts it either, so it declares no `mount` hook - the harness
2033
+ * still calls `mount()` and still gets a teardown, which is the point of the
2034
+ * hook being unconditional.
2035
+ *
2036
+ * The API has no un-redeem, so the applied state offers only "use a different
2037
+ * code", which clears the input for another redemption.
2038
+ *
2039
+ * Behaviour is what the Stencil component did before the split, to the letter,
2040
+ * including the two error messages and the uppercased code in the receipt. What
2041
+ * did not come across is the `console.error` tracing.
2042
+ */
2043
+ const STRINGS$5 = {
2044
+ applying: 'Applying…',
2045
+ codePrefix: 'Code ',
2046
+ appliedSuffix: ' applied',
2047
+ alreadyHeldSuffix: ' was already applied',
2048
+ useDifferent: 'Use a different code',
2049
+ enterCode: 'Enter a code',
2050
+ notAvailable: 'Promo codes are not available',
2051
+ signIn: 'Sign in to use this code',
2052
+ rejected: 'That code could not be applied',
2053
+ };
2054
+ /** True for the "not signed in" family of API errors (401/403, NO_SESSION, NOT_AUTHENTICATED). */
2055
+ function isAuthError(err) {
2056
+ const e = err;
2057
+ if (!e || typeof e !== 'object')
2058
+ return false;
2059
+ return e.status === 401 || e.status === 403 || e.code === 'NO_SESSION' || e.code === 'NOT_AUTHENTICATED';
2060
+ }
2061
+ const promoCodeCore = defineElement({
2062
+ tag: 'tl-promo-code',
2063
+ docs: 'Redeem a presale or promo code for the current cart.',
2064
+ category: 'purchase',
2065
+ runtimes: ['web'],
2066
+ tokens: [
2067
+ 'background',
2068
+ 'border',
2069
+ 'error',
2070
+ 'font-family',
2071
+ 'font-family-mono',
2072
+ 'foreground',
2073
+ 'muted',
2074
+ 'muted-foreground',
2075
+ 'primary',
2076
+ 'primary-foreground',
2077
+ 'radius-md',
2078
+ 'spacing-2',
2079
+ 'spacing-3',
2080
+ 'spacing-4',
2081
+ 'success',
2082
+ ],
2083
+ sdkMethods: ['cart.get', 'presale.redeem'],
2084
+ props: {
2085
+ placeholder: {
2086
+ type: 'string',
2087
+ attr: 'placeholder',
2088
+ default: 'Promo or presale code',
2089
+ docs: 'Input placeholder.',
2090
+ },
2091
+ buttonLabel: { type: 'string', attr: 'button-label', default: 'Apply', docs: 'Apply button label.' },
2092
+ },
2093
+ events: {
2094
+ tlPromoApplied: { detail: 'TlPromoApplied', docs: '' },
2095
+ tlError: { detail: '{ message: string; }', docs: '' },
2096
+ },
2097
+ strings: STRINGS$5,
2098
+ statuses: ['idle', 'applied'],
2099
+ state: () => ({ code: '', applying: false, applied: null, error: null }),
2100
+ status: ({ state }) => (state.applied ? 'applied' : 'idle'),
2101
+ derive: ({ state, props, strings }) => ({
2102
+ actionLabel: state.applying ? strings.applying : props.buttonLabel,
2103
+ disabled: state.applying,
2104
+ appliedCode: state.applied?.code ?? null,
2105
+ appliedSuffix: state.applied?.alreadyHeld ? strings.alreadyHeldSuffix : strings.appliedSuffix,
2106
+ }),
2107
+ actions: (ctx) => ({
2108
+ /** The buyer typed. Typing clears whatever the last attempt complained about. */
2109
+ setCode(code) {
2110
+ ctx.setState({ code, error: null });
2111
+ },
2112
+ /**
2113
+ * Redeem what is in the box.
2114
+ *
2115
+ * The client refreshes the cart on redeem, so the cart is read back only
2116
+ * to put it in the payload; a read that fails is not a failed redemption
2117
+ * and does not become one.
2118
+ */
2119
+ async apply() {
2120
+ const code = ctx.state.code.trim();
2121
+ if (!code) {
2122
+ ctx.setState({ error: ctx.strings.enterCode });
2123
+ return;
2124
+ }
2125
+ ctx.setState({ applying: true, error: null });
2126
+ try {
2127
+ const client = await ctx.whenClient();
2128
+ const presale = client.getPresaleManager?.();
2129
+ if (!presale)
2130
+ throw new Error(ctx.strings.notAvailable);
2131
+ const result = await presale.redeem(code);
2132
+ const cart = (await client.getCartManager?.()?.get?.().catch(() => null)) ?? null;
2133
+ const applied = { code: code.toUpperCase(), ...result, cart };
2134
+ ctx.setState({ applied, code: '' });
2135
+ ctx.emit('tlPromoApplied', applied);
2136
+ }
2137
+ catch (err) {
2138
+ const message = isAuthError(err)
2139
+ ? ctx.strings.signIn
2140
+ : err instanceof Error && err.message
2141
+ ? err.message
2142
+ : ctx.strings.rejected;
2143
+ ctx.setState({ error: message });
2144
+ ctx.emit('tlError', { message });
2145
+ }
2146
+ finally {
2147
+ ctx.setState({ applying: false });
2148
+ }
2149
+ },
2150
+ /** Back to the input, for a second code. There is no un-redeem to offer. */
2151
+ reset() {
2152
+ ctx.setState({ applied: null, error: null });
2153
+ },
2154
+ }),
2155
+ });
2156
+
2157
+ /**
2158
+ * `tl-my-orders`: the signed-in customer's orders, one expandable row each.
2159
+ *
2160
+ * The first core whose whole shape is decided by whether somebody is signed
2161
+ * in. It reads the client's auth manager once, follows `auth:changed` and
2162
+ * `session:changed` from there, and lists the orders whenever the answer turns
2163
+ * from signed out to signed in. Both subscriptions are bound in its own
2164
+ * `mount` and handed to `ctx.onTeardown` (TKT-197), so a remount cannot leave
2165
+ * four listeners on the bus.
2166
+ *
2167
+ * Behaviour is what the Stencil component did, to the letter, including the
2168
+ * three things that read like accidents and are not:
2169
+ *
2170
+ * - a connector with no auth accessor is treated as SIGNED IN, and a 401 from
2171
+ * the listing is what decides otherwise. That is deliberate: an older
2172
+ * connector has no way to answer the question, and showing the sign-in form
2173
+ * to someone who is signed in is the worse failure.
2174
+ * - a 401 or 403 from the listing is not an error. It drops back to the
2175
+ * signed-out view, which renders the inline sign-in, and emits nothing.
2176
+ * - the element does NOT announce that the host never started Live. It shows
2177
+ * "Orders are not available right now" and stays quiet, where `tl-cart`
2178
+ * emits `tlError` for the same condition, because the component's wait and
2179
+ * its read were in different places. Kept rather than made consistent.
2180
+ *
2181
+ * What did not come across is the `console.error` tracing, which a library
2182
+ * should not do for its host (`tl-cart` dropped its own for the same reason).
2183
+ */
2184
+ const STRINGS$4 = {
2185
+ signInTitle: 'Sign in to see your orders',
2186
+ signInText: 'Your tickets and order history live in your account.',
2187
+ errorTitle: 'Could not load your orders',
2188
+ emptyTitle: 'No orders yet',
2189
+ orderPrefix: 'Order ',
2190
+ unnamedEvent: 'Order',
2191
+ ticketSingular: 'ticket',
2192
+ ticketPlural: 'tickets',
2193
+ unavailable: 'Orders are not available right now',
2194
+ noOrdersManager: 'Orders are not available',
2195
+ loadFailed: 'Failed to load your orders',
2196
+ };
2197
+ const myOrdersCore = defineElement({
2198
+ tag: 'tl-my-orders',
2199
+ docs: "The signed-in customer's orders: event, date, ticket count, total and status, each row expanding to its tickets.",
2200
+ category: 'account',
2201
+ runtimes: ['web', 'native'],
2202
+ tokens: [
2203
+ 'background',
2204
+ 'border',
2205
+ 'error',
2206
+ 'error-foreground',
2207
+ 'font-family',
2208
+ 'foreground',
2209
+ 'muted-foreground',
2210
+ 'primary',
2211
+ 'primary-foreground',
2212
+ 'radius-lg',
2213
+ 'secondary',
2214
+ 'secondary-foreground',
2215
+ 'spacing-1',
2216
+ 'spacing-3',
2217
+ 'spacing-4',
2218
+ 'spacing-8',
2219
+ 'success',
2220
+ 'success-foreground',
2221
+ 'warning',
2222
+ 'warning-foreground',
2223
+ ],
2224
+ sdkMethods: ['orders.mine', 'auth.authenticated', 'on'],
2225
+ props: {
2226
+ emptyMessage: {
2227
+ type: 'string',
2228
+ attr: 'empty-message',
2229
+ default: "When you buy tickets they'll show up here, ready to view and download.",
2230
+ docs: 'Copy for the empty state.',
2231
+ },
2232
+ columns: { type: 'number', attr: 'columns', default: 2, docs: 'Columns for the expanded ticket grid.' },
2233
+ },
2234
+ events: {
2235
+ tlOrderSelect: { detail: '{ orderId: string; }', docs: 'a row was expanded ({ orderId })' },
2236
+ tlError: { detail: '{ message: string; }', docs: 'the orders could not be loaded' },
2237
+ },
2238
+ strings: STRINGS$4,
2239
+ statuses: ['loading', 'signed-out', 'error', 'empty', 'ready'],
2240
+ state: () => ({ orders: [], authenticated: false, loading: true, error: null, expandedId: null }),
2241
+ /**
2242
+ * On screen: wait for the client, read who is signed in, follow both auth
2243
+ * signals, then list the orders if there is somebody to list them for.
2244
+ *
2245
+ * The session hydrates after the client publishes, so `auth:changed` and
2246
+ * `session:changed` are both followed: the connector raises one or the other
2247
+ * depending on whether the customer signed in here or arrived with a session
2248
+ * already warm.
2249
+ */
2250
+ mount: (ctx) => {
2251
+ void (async () => {
2252
+ let client;
2253
+ try {
2254
+ client = await ctx.whenClient();
2255
+ }
2256
+ catch {
2257
+ if (!ctx.active())
2258
+ return;
2259
+ ctx.actions.connectFailed();
2260
+ return;
2261
+ }
2262
+ if (!ctx.active())
2263
+ return;
2264
+ // An older connector has no auth accessor and cannot answer, so assume
2265
+ // signed in and let a 401 from the listing decide.
2266
+ const auth = client.getAuthManager?.();
2267
+ ctx.setState({ authenticated: auth ? auth.authenticated : true });
2268
+ for (const event of ['auth:changed', 'session:changed']) {
2269
+ const off = client.on?.(event, () => ctx.actions.syncAuth());
2270
+ if (typeof off === 'function')
2271
+ ctx.onTeardown(off);
2272
+ }
2273
+ if (ctx.state.authenticated)
2274
+ await ctx.actions.load();
2275
+ else
2276
+ ctx.setState({ loading: false });
2277
+ })();
2278
+ },
2279
+ status: ({ state }) => state.loading
2280
+ ? 'loading'
2281
+ : !state.authenticated
2282
+ ? 'signed-out'
2283
+ : state.error
2284
+ ? 'error'
2285
+ : state.orders.length === 0
2286
+ ? 'empty'
2287
+ : 'ready',
2288
+ derive: ({ state, strings }) => ({
2289
+ rows: state.orders.map((order) => {
2290
+ const count = ticketCount(order);
2291
+ return {
2292
+ id: order.id,
2293
+ numberLabel: `${strings.orderPrefix}${order.orderNumber}`,
2294
+ eventName: orderEventName(order) ?? strings.unnamedEvent,
2295
+ dateLabel: order.createdAt ? formatDate$3(order.createdAt) : null,
2296
+ countLabel: `${count} ${count === 1 ? strings.ticketSingular : strings.ticketPlural}`,
2297
+ totalLabel: formatPrice$1(order.total, order.currency),
2298
+ statusLabel: statusLabel(order.status),
2299
+ statusTone: statusTone(order.status),
2300
+ expanded: state.expandedId === order.id,
2301
+ };
2302
+ }),
2303
+ }),
2304
+ // The action map is named so `syncAuth` can call `load`: the action context
2305
+ // does not carry the actions (only a mount's does), and listing the orders
2306
+ // when the session turns warm is the same work the mount does.
2307
+ actions: (ctx) => {
2308
+ const self = {
2309
+ /** List the orders. A 401 means signed out, which is not an error. */
2310
+ async load() {
2311
+ const client = ctx.optionalClient();
2312
+ if (!client)
2313
+ return;
2314
+ try {
2315
+ ctx.setState({ loading: true, error: null });
2316
+ const orders = client.getOrdersManager?.();
2317
+ if (!orders)
2318
+ throw new Error(ctx.strings.noOrdersManager);
2319
+ const list = (await orders.mine());
2320
+ ctx.setState({ orders: list ?? [], authenticated: true });
2321
+ }
2322
+ catch (err) {
2323
+ if (isAuthError(err)) {
2324
+ // Not signed in: fall through to the inline sign-in rather than an error.
2325
+ ctx.setState({ authenticated: false, orders: [] });
2326
+ }
2327
+ else {
2328
+ const message = errorMessage(err, ctx.strings.loadFailed);
2329
+ ctx.setState({ error: message });
2330
+ ctx.emit('tlError', { message });
2331
+ }
2332
+ }
2333
+ finally {
2334
+ ctx.setState({ loading: false });
2335
+ }
2336
+ },
2337
+ /**
2338
+ * The client says the session changed. Listing on the way in and clearing
2339
+ * on the way out; an unchanged answer does nothing, which is what keeps a
2340
+ * burst of `session:changed` from listing the orders four times.
2341
+ */
2342
+ syncAuth() {
2343
+ const auth = ctx.optionalClient()?.getAuthManager?.();
2344
+ if (!auth)
2345
+ return;
2346
+ const was = ctx.state.authenticated;
2347
+ ctx.setState({ authenticated: auth.authenticated });
2348
+ if (auth.authenticated && !was)
2349
+ void self.load();
2350
+ else if (!auth.authenticated && was)
2351
+ ctx.setState({ orders: [], expandedId: null });
2352
+ },
2353
+ /** Open a row, or close the open one. Only opening is announced. */
2354
+ toggle(orderId) {
2355
+ const opening = ctx.state.expandedId !== orderId;
2356
+ ctx.setState({ expandedId: opening ? orderId : null });
2357
+ if (opening)
2358
+ ctx.emit('tlOrderSelect', { orderId });
2359
+ },
2360
+ /**
2361
+ * The host never produced a client. Said on screen and nowhere else: the
2362
+ * component did not emit `tlError` here, because its wait for the client
2363
+ * was outside the try block that announces a failure.
2364
+ */
2365
+ connectFailed() {
2366
+ ctx.setState({ loading: false, error: ctx.strings.unavailable });
2367
+ },
2368
+ };
2369
+ return self;
2370
+ },
2371
+ });
2372
+
2373
+ /**
2374
+ * `tl-occurrence-selector`: every date and time one event is on, and the one
2375
+ * the buyer picks.
2376
+ *
2377
+ * The largest of the eighteen, and the last big one: 883 lines of Stencil with
2378
+ * no spec at all, which is why TKT-202 wrote the characterisation spec first
2379
+ * and split it on its own. Everything below is what that component did, ported
2380
+ * rather than redesigned, and the spec
2381
+ * (`packages/elements/src/components/tl-occurrence-selector/test/`) is the
2382
+ * proof - it passed against the component before this file existed and passes
2383
+ * against the harness now, unchanged.
2384
+ *
2385
+ * What it decides, in the order it decides it:
2386
+ *
2387
+ * - Which of three views to render. A host can force one; otherwise five
2388
+ * occurrences or fewer is a LIST, more than five all on one calendar date
2389
+ * is a grid of SLOTS, and more than five over several dates is a CALENDAR
2390
+ * with the picked date's times beside it.
2391
+ * - Where the calendar opens. Ported with its bug intact: the dates are sorted
2392
+ * as `Date.toDateString()` STRINGS, so "Fri Jul 05 2030" sorts before
2393
+ * "Fri Jun 14 2030" and a weekday name decides the order before a month
2394
+ * does. Fixing that is a decision about the product, not part of moving the
2395
+ * code, and a split that quietly corrected it would be exactly the kind of
2396
+ * change the characterisation spec exists to catch.
2397
+ * - Which days a buyer may press, which are sold out, and how far the month
2398
+ * navigation may walk in each direction.
2399
+ * - Every label: the `en-GB` dates and times, the capacity line, the weekday
2400
+ * headings and the two month-navigation labels a screen reader reads.
2401
+ *
2402
+ * What is NOT here, and why:
2403
+ *
2404
+ * - The `theme` prop. It is declared, because it is part of the element's
2405
+ * published surface, and it is the harness that acts on it: the prop is a
2406
+ * set of CSS CUSTOM PROPERTIES written onto the element, and a custom
2407
+ * property is not a fact every runtime has. A core holds no values at all,
2408
+ * so there is nothing here for it to hold.
2409
+ * - How wide the element is. `containerWidth` is state the harness measures
2410
+ * and pushes in through `resized()`; the core decides what a width MEANS
2411
+ * (`auto` becomes horizontal at 768px and not before), which is the same
2412
+ * bargain as `ctx.storage` - the core says what, the harness says where.
2413
+ * - Which token family a state paints with. The core derives the CLASS a
2414
+ * calendar day renders under and the stylesheet says what that class paints,
2415
+ * because a core cannot map a status onto a token family
2416
+ * (`tl-event-card`'s status badge and `tl-buy-tickets-button` make the same
2417
+ * bargain).
2418
+ *
2419
+ * What did not come across is the `console.error` on a failed read and the
2420
+ * `console.log` on every selection, for the reason `tl-cart` and
2421
+ * `tl-promo-code` dropped theirs: a library should not write to its host's
2422
+ * console.
2423
+ */
2424
+ const STRINGS$3 = {
2425
+ noEventId: 'Event ID is required',
2426
+ noClient: 'SDK not initialized.',
2427
+ notFound: 'Event not found',
2428
+ loadFailed: 'Failed to load occurrences',
2429
+ empty: 'No dates available for this event.',
2430
+ retry: 'Retry',
2431
+ soldOut: 'Sold Out',
2432
+ /** Follows the figure: "120 available". */
2433
+ available: 'available',
2434
+ selectADate: 'Select a date to see available times',
2435
+ previousMonth: 'Previous month',
2436
+ nextMonth: 'Next month',
2437
+ weekdayMon: 'Mon',
2438
+ weekdayTue: 'Tue',
2439
+ weekdayWed: 'Wed',
2440
+ weekdayThu: 'Thu',
2441
+ weekdayFri: 'Fri',
2442
+ weekdaySat: 'Sat',
2443
+ weekdaySun: 'Sun',
2444
+ };
2445
+ /** Above this many pixels an `auto` layout puts the times beside the calendar. */
2446
+ const HORIZONTAL_MIN_WIDTH = 768;
2447
+ /** More than this many occurrences and the list view gives way. */
2448
+ const LIST_LIMIT = 5;
2449
+ // ---------------------------------------------------------------------------
2450
+ // Dates. Every one of these is the component's own arithmetic, moved.
2451
+ // ---------------------------------------------------------------------------
2452
+ /** Local midnight on the day `date` falls on. */
2453
+ function startOfDay(date) {
2454
+ const start = new Date(date);
2455
+ start.setHours(0, 0, 0, 0);
2456
+ return start;
2457
+ }
2458
+ /** The key a day is held under, which is also what a harness hands back. */
2459
+ function dayKey(date) {
2460
+ return date.toDateString();
2461
+ }
2462
+ /** Monday of the week `date` falls in. */
2463
+ function mondayOf(date) {
2464
+ const monday = new Date(date);
2465
+ const dayOfWeek = monday.getDay();
2466
+ monday.setDate(monday.getDate() - (dayOfWeek === 0 ? 6 : dayOfWeek - 1));
2467
+ monday.setHours(0, 0, 0, 0);
2468
+ return monday;
2469
+ }
2470
+ /** The first of the month `date` falls in. */
2471
+ function firstOfMonth(date) {
2472
+ return new Date(date.getFullYear(), date.getMonth(), 1);
2473
+ }
2474
+ const startTime = (occurrence) => new Date(occurrence.startsAt).getTime();
2475
+ const byStart = (a, b) => startTime(a) - startTime(b);
2476
+ /** As the component decided it: an explicit zero counts as gone. */
2477
+ const isSoldOut = (occurrence) => !!occurrence.soldOut || occurrence.availableCapacity === 0;
2478
+ /** Day key -> the occurrences on it, in the order the event gave them. */
2479
+ function occurrencesByDate(occurrences) {
2480
+ const map = new Map();
2481
+ for (const occurrence of occurrences) {
2482
+ const key = dayKey(startOfDay(new Date(occurrence.startsAt)));
2483
+ const on = map.get(key);
2484
+ if (on)
2485
+ on.push(occurrence);
2486
+ else
2487
+ map.set(key, [occurrence]);
2488
+ }
2489
+ return map;
2490
+ }
2491
+ /** The earliest and latest occurrence, or null when there are none. */
2492
+ function dateRangeOf(occurrences) {
2493
+ if (!occurrences.length)
2494
+ return null;
2495
+ let first = new Date(occurrences[0].startsAt);
2496
+ let last = new Date(occurrences[0].startsAt);
2497
+ for (const occurrence of occurrences) {
2498
+ const date = new Date(occurrence.startsAt);
2499
+ if (date < first)
2500
+ first = date;
2501
+ if (date > last)
2502
+ last = date;
2503
+ }
2504
+ return { first, last };
2505
+ }
2506
+ /** Whether everything fits between one Monday and the Sunday after it. */
2507
+ function isSingleWeek(occurrences) {
2508
+ const range = dateRangeOf(occurrences);
2509
+ if (!range)
2510
+ return false;
2511
+ const sunday = new Date(mondayOf(range.first));
2512
+ sunday.setDate(sunday.getDate() + 6);
2513
+ sunday.setHours(23, 59, 59, 999);
2514
+ return range.last <= sunday;
2515
+ }
2516
+ /** Whether everything falls in one calendar month. */
2517
+ function isSingleMonth(occurrences) {
2518
+ const range = dateRangeOf(occurrences);
2519
+ if (!range)
2520
+ return false;
2521
+ return (range.first.getMonth() === range.last.getMonth() &&
2522
+ range.first.getFullYear() === range.last.getFullYear());
2523
+ }
2524
+ /**
2525
+ * Where the calendar opens.
2526
+ *
2527
+ * The sort is on `toDateString()` STRINGS and that is not an oversight here:
2528
+ * it is what the component did, so "Fri Jul 05 2030" comes before
2529
+ * "Fri Jun 14 2030" and the element opens on July. See the note at the top.
2530
+ */
2531
+ function initialSelection(occurrences) {
2532
+ if (!occurrences.length)
2533
+ return null;
2534
+ const today = startOfDay(new Date());
2535
+ const keys = Array.from(occurrencesByDate(occurrences).keys()).sort();
2536
+ let selected = null;
2537
+ for (const key of keys) {
2538
+ const date = new Date(key);
2539
+ if (date >= today) {
2540
+ selected = date;
2541
+ break;
2542
+ }
2543
+ }
2544
+ if (!selected && keys.length)
2545
+ selected = new Date(keys[keys.length - 1]);
2546
+ if (!selected)
2547
+ return null;
2548
+ return { selectedDate: selected, currentMonth: firstOfMonth(selected) };
2549
+ }
2550
+ /**
2551
+ * The grid: one week when everything fits in one, else the whole month padded
2552
+ * out to whole Monday-to-Sunday weeks.
2553
+ */
2554
+ function calendarDays(occurrences, currentMonth) {
2555
+ const days = [];
2556
+ if (isSingleWeek(occurrences)) {
2557
+ const range = dateRangeOf(occurrences);
2558
+ if (!range)
2559
+ return days;
2560
+ const cursor = mondayOf(range.first);
2561
+ for (let i = 0; i < 7; i += 1) {
2562
+ days.push(new Date(cursor));
2563
+ cursor.setDate(cursor.getDate() + 1);
2564
+ }
2565
+ return days;
2566
+ }
2567
+ const year = currentMonth.getFullYear();
2568
+ const month = currentMonth.getMonth();
2569
+ const monthStart = new Date(year, month, 1);
2570
+ const monthEnd = new Date(year, month + 1, 0);
2571
+ const startDay = monthStart.getDay();
2572
+ const calendarStart = new Date(monthStart);
2573
+ calendarStart.setDate(calendarStart.getDate() - (startDay === 0 ? 6 : startDay - 1));
2574
+ const endDay = monthEnd.getDay();
2575
+ const calendarEnd = new Date(monthEnd);
2576
+ calendarEnd.setDate(calendarEnd.getDate() + (endDay === 0 ? 0 : 7 - endDay));
2577
+ const cursor = new Date(calendarStart);
2578
+ while (cursor <= calendarEnd) {
2579
+ days.push(new Date(cursor));
2580
+ cursor.setDate(cursor.getDate() + 1);
2581
+ }
2582
+ return days;
2583
+ }
2584
+ /**
2585
+ * As the component formatted them: `en-GB`, written down here rather than taken
2586
+ * from the host's locale because that is what the element did before the split
2587
+ * and this is a split, not a change (see the note on the card core's date).
2588
+ */
2589
+ function formatDate(date) {
2590
+ return date.toLocaleDateString('en-GB', {
2591
+ weekday: 'long',
2592
+ day: 'numeric',
2593
+ month: 'long',
2594
+ year: 'numeric',
2595
+ });
2596
+ }
2597
+ function formatTime(dateString) {
2598
+ return new Date(dateString).toLocaleTimeString('en-GB', { hour: '2-digit', minute: '2-digit' });
2599
+ }
2600
+ function formatMonth(date) {
2601
+ return date.toLocaleDateString('en-GB', { month: 'long', year: 'numeric' });
2602
+ }
2603
+ /** The event detail's occurrences, as this element holds them. */
2604
+ function toOccurrence(occurrence) {
2605
+ return {
2606
+ id: occurrence.id,
2607
+ startsAt: occurrence.startsAt,
2608
+ endsAt: occurrence.endsAt,
2609
+ status: occurrence.availabilityStatus,
2610
+ availableCapacity: occurrence.availableCapacity,
2611
+ soldOut: occurrence.availabilityStatus === 'sold_out',
2612
+ };
2613
+ }
2614
+ const occurrenceSelectorCore = defineElement({
2615
+ tag: 'tl-occurrence-selector',
2616
+ docs: 'A smart occurrence selector component that fetches and displays event occurrences from the Ticketlayer API.',
2617
+ category: 'discovery',
2618
+ // Not `native`: the hand-kept manifest said `web` and there is no React
2619
+ // Native harness for a calendar yet. Splitting it does not make that claim
2620
+ // true, and the catalogue is read as a promise about where an element runs.
2621
+ runtimes: ['web'],
2622
+ tokens: [
2623
+ 'background',
2624
+ 'border',
2625
+ 'font-family',
2626
+ 'foreground',
2627
+ 'muted',
2628
+ 'muted-foreground',
2629
+ 'primary',
2630
+ 'primary-foreground',
2631
+ 'radius-md',
2632
+ ],
2633
+ sdkMethods: ['events.get'],
2634
+ props: {
2635
+ eventId: {
2636
+ type: 'string',
2637
+ attr: 'event-id',
2638
+ required: true,
2639
+ docs: 'Event ID to fetch occurrences for',
2640
+ },
2641
+ displayMode: {
2642
+ type: 'string',
2643
+ tsType: '"calendar" | "list" | "slots" | undefined',
2644
+ attr: 'display-mode',
2645
+ docs: 'Force a specific display mode (auto-detected if not set)',
2646
+ },
2647
+ orientation: {
2648
+ type: 'string',
2649
+ tsType: '"auto" | "horizontal" | "vertical"',
2650
+ attr: 'orientation',
2651
+ default: 'auto',
2652
+ docs: 'Layout orientation (auto = horizontal on desktop, vertical on mobile)',
2653
+ },
2654
+ theme: {
2655
+ type: 'object',
2656
+ tsType: 'TlOccurrenceSelectorTheme | undefined',
2657
+ attr: null,
2658
+ docs: 'Theme configuration',
2659
+ },
2660
+ },
2661
+ events: {
2662
+ tlSelect: {
2663
+ detail: '{ occurrence: TlOccurrence; }',
2664
+ docs: 'Emitted when an occurrence is selected',
2665
+ },
2666
+ },
2667
+ strings: STRINGS$3,
2668
+ statuses: ['loading', 'error', 'empty', 'ready'],
2669
+ state: () => ({
2670
+ loading: true,
2671
+ error: null,
2672
+ occurrences: [],
2673
+ currentMonth: new Date(),
2674
+ selectedDate: null,
2675
+ containerWidth: 0,
2676
+ }),
2677
+ /**
2678
+ * On screen: wait for the host's client, then read the event.
2679
+ *
2680
+ * The component did this in `componentWillLoad`, which held the first paint
2681
+ * until the read came back. A mount hook cannot hold a paint, so the element
2682
+ * shows its own skeleton first instead of a blank - the same change
2683
+ * `tl-ticket-selector` and `tl-event-card` took when they moved onto the hook
2684
+ * (TKT-197).
2685
+ *
2686
+ * There is nothing to unsubscribe: this element reads once and lives on what
2687
+ * it read, so the mount registers no teardown and the harness still gets one.
2688
+ */
2689
+ mount: (ctx) => {
2690
+ if (!ctx.props.eventId) {
2691
+ ctx.actions.loadFailed(ctx.strings.noEventId);
2692
+ return;
2693
+ }
2694
+ void (async () => {
2695
+ try {
2696
+ await ctx.whenClient();
2697
+ }
2698
+ catch (err) {
2699
+ if (!ctx.active())
2700
+ return;
2701
+ ctx.actions.loadFailed(err instanceof Error ? err.message : ctx.strings.noClient);
2702
+ return;
2703
+ }
2704
+ if (!ctx.active())
2705
+ return;
2706
+ await ctx.actions.load();
2707
+ })();
2708
+ },
2709
+ status: ({ state }) => state.loading ? 'loading' : state.error ? 'error' : state.occurrences.length === 0 ? 'empty' : 'ready',
2710
+ derive: ({ state, props, strings }) => {
2711
+ const { occurrences, selectedDate } = state;
2712
+ const byDate = occurrencesByDate(occurrences);
2713
+ const range = dateRangeOf(occurrences);
2714
+ // Which view. The component decided this once, when the read came back;
2715
+ // deriving it means a host that changes `display-mode` afterwards is
2716
+ // answered at once rather than at the next read (TKT-202, named in the
2717
+ // pull request as the one behaviour this split does not preserve exactly).
2718
+ const mode = props.displayMode
2719
+ ? props.displayMode
2720
+ : occurrences.length <= LIST_LIMIT
2721
+ ? 'list'
2722
+ : byDate.size === 1
2723
+ ? 'slots'
2724
+ : 'calendar';
2725
+ const horizontal = props.orientation === 'horizontal'
2726
+ ? true
2727
+ : props.orientation === 'vertical'
2728
+ ? false
2729
+ : state.containerWidth >= HORIZONTAL_MIN_WIDTH;
2730
+ const row = (occurrence) => {
2731
+ const soldOut = isSoldOut(occurrence);
2732
+ const start = formatTime(occurrence.startsAt);
2733
+ return {
2734
+ id: occurrence.id,
2735
+ dateLabel: formatDate(new Date(occurrence.startsAt)),
2736
+ timeLabel: occurrence.endsAt ? `${start} - ${formatTime(occurrence.endsAt)}` : start,
2737
+ startLabel: start,
2738
+ soldOut,
2739
+ capacityLabel: occurrence.availableCapacity
2740
+ ? `${occurrence.availableCapacity} ${strings.available}`
2741
+ : null,
2742
+ statusKind: soldOut ? 'soldOut' : occurrence.availableCapacity ? 'available' : 'arrow',
2743
+ };
2744
+ };
2745
+ const inTimeOrder = [...occurrences].sort(byStart);
2746
+ const selectedKey = selectedDate ? dayKey(selectedDate) : null;
2747
+ const selectedOn = selectedKey ? [...(byDate.get(selectedKey) ?? [])].sort(byStart) : [];
2748
+ const today = startOfDay(new Date());
2749
+ const firstMonth = range ? firstOfMonth(range.first) : null;
2750
+ const lastMonth = range ? firstOfMonth(range.last) : null;
2751
+ const days = calendarDays(occurrences, state.currentMonth).map((date) => {
2752
+ const key = dayKey(date);
2753
+ const on = byDate.get(key);
2754
+ const hasOccurrences = !!on;
2755
+ const soldOut = hasOccurrences && on.every(isSoldOut);
2756
+ const otherMonth = date.getMonth() !== state.currentMonth.getMonth();
2757
+ const selected = selectedKey === key;
2758
+ const isToday = key === dayKey(today);
2759
+ const past = date < today;
2760
+ return {
2761
+ key,
2762
+ dayNumber: String(date.getDate()),
2763
+ otherMonth,
2764
+ hasOccurrences,
2765
+ soldOut,
2766
+ selected,
2767
+ today: isToday,
2768
+ past,
2769
+ selectable: hasOccurrences,
2770
+ className: [
2771
+ 'calendar-day',
2772
+ otherMonth ? 'other-month' : '',
2773
+ hasOccurrences ? 'has-occurrences' : '',
2774
+ soldOut ? 'sold-out' : '',
2775
+ selected ? 'selected' : '',
2776
+ isToday ? 'today' : '',
2777
+ past ? 'past' : '',
2778
+ ]
2779
+ .filter(Boolean)
2780
+ .join(' '),
2781
+ };
2782
+ });
2783
+ return {
2784
+ mode,
2785
+ horizontal,
2786
+ viewClass: `calendar-view ${horizontal ? 'horizontal' : 'vertical'}`,
2787
+ rows: occurrences.map(row),
2788
+ slots: inTimeOrder.map(row),
2789
+ slotsDateLabel: inTimeOrder.length ? formatDate(new Date(inTimeOrder[0].startsAt)) : null,
2790
+ monthLabel: formatMonth(state.currentMonth),
2791
+ showMonthNav: !isSingleMonth(occurrences),
2792
+ canGoBack: !!firstMonth && state.currentMonth > firstMonth,
2793
+ canGoForward: !!lastMonth && state.currentMonth < lastMonth,
2794
+ weekdayLabels: [
2795
+ strings.weekdayMon,
2796
+ strings.weekdayTue,
2797
+ strings.weekdayWed,
2798
+ strings.weekdayThu,
2799
+ strings.weekdayFri,
2800
+ strings.weekdaySat,
2801
+ strings.weekdaySun,
2802
+ ],
2803
+ days,
2804
+ selectedDateLabel: selectedDate && selectedOn.length ? formatDate(selectedDate) : null,
2805
+ selectedSlots: selectedOn.map(row),
2806
+ };
2807
+ },
2808
+ actions: (ctx) => ({
2809
+ /**
2810
+ * Read the event and take its occurrences. Also what Retry runs, which is
2811
+ * why it sets `loading` and clears the error itself rather than relying on
2812
+ * the state it started in.
2813
+ */
2814
+ async load() {
2815
+ ctx.setState({ loading: true, error: null });
2816
+ try {
2817
+ const events = ctx.client().getEventsManager();
2818
+ const read = events.get?.bind(events);
2819
+ if (!read)
2820
+ throw new TypeError('eventsManager.get is not a function');
2821
+ const detail = (await read(ctx.props.eventId));
2822
+ if (!detail)
2823
+ throw new Error(ctx.strings.notFound);
2824
+ const occurrences = (detail.occurrences ?? []).map(toOccurrence);
2825
+ ctx.setState({ occurrences, ...(initialSelection(occurrences) ?? {}) });
2826
+ }
2827
+ catch (err) {
2828
+ ctx.setState({
2829
+ error: err instanceof Error ? err.message : ctx.strings.loadFailed,
2830
+ occurrences: [],
2831
+ });
2832
+ }
2833
+ finally {
2834
+ ctx.setState({ loading: false });
2835
+ }
2836
+ },
2837
+ /** Nothing to read: no event id, or the host never produced a client. */
2838
+ loadFailed(message) {
2839
+ ctx.setState({ error: message, loading: false });
2840
+ },
2841
+ /**
2842
+ * The buyer picked an occurrence. A sold out one is not pickable, which the
2843
+ * harness also renders as disabled: the core decides, the harness shows.
2844
+ */
2845
+ select(occurrenceId) {
2846
+ const occurrence = ctx.state.occurrences.find((o) => o.id === occurrenceId);
2847
+ if (!occurrence || isSoldOut(occurrence))
2848
+ return;
2849
+ ctx.emit('tlSelect', { occurrence });
2850
+ },
2851
+ /** The buyer picked a day. Only a day with something on it counts. */
2852
+ selectDate(key) {
2853
+ if (!occurrencesByDate(ctx.state.occurrences).has(key))
2854
+ return;
2855
+ const date = new Date(key);
2856
+ if (Number.isNaN(date.getTime()))
2857
+ return;
2858
+ const { currentMonth } = ctx.state;
2859
+ const movedMonth = date.getMonth() !== currentMonth.getMonth() || date.getFullYear() !== currentMonth.getFullYear();
2860
+ ctx.setState({
2861
+ selectedDate: date,
2862
+ ...(movedMonth ? { currentMonth: firstOfMonth(date) } : {}),
2863
+ });
2864
+ },
2865
+ /** Back a month, and never before the first month there is anything in. */
2866
+ previousMonth() {
2867
+ const range = dateRangeOf(ctx.state.occurrences);
2868
+ if (!range)
2869
+ return;
2870
+ const next = new Date(ctx.state.currentMonth);
2871
+ next.setMonth(next.getMonth() - 1);
2872
+ if (next >= firstOfMonth(range.first))
2873
+ ctx.setState({ currentMonth: next });
2874
+ },
2875
+ /** Forward a month, and never past the last month there is anything in. */
2876
+ nextMonth() {
2877
+ const range = dateRangeOf(ctx.state.occurrences);
2878
+ if (!range)
2879
+ return;
2880
+ const next = new Date(ctx.state.currentMonth);
2881
+ next.setMonth(next.getMonth() + 1);
2882
+ if (next <= firstOfMonth(range.last))
2883
+ ctx.setState({ currentMonth: next });
2884
+ },
2885
+ /**
2886
+ * How wide the harness measures the element. What a width means is the
2887
+ * core's (`auto` turns horizontal at 768px); measuring one is the
2888
+ * runtime's, and on the web that is a `ResizeObserver`.
2889
+ */
2890
+ resized(width) {
2891
+ if (width === ctx.state.containerWidth)
2892
+ return;
2893
+ ctx.setState({ containerWidth: width });
2894
+ },
2895
+ }),
2896
+ });
2897
+
2898
+ /**
2899
+ * `tl-order-confirmation`: the thank-you block - reference, email, event and
2900
+ * date, the ticket lines, the money rows, and the tickets themselves.
2901
+ *
2902
+ * It finds its order three ways, in this order, and that priority is the whole
2903
+ * point of the element:
2904
+ *
2905
+ * 1. the `order-id` prop, for a host that already knows;
2906
+ * 2. the query the page was opened with (`?orderId=`), which is the contract
2907
+ * the hosted box office confirmation page uses, plus `?redirect_status=`,
2908
+ * which is how a payment provider says on the way back whether the money
2909
+ * actually moved;
2910
+ * 3. the client's `checkout:completed`, for a page that mounts this before
2911
+ * the customer has paid.
2912
+ *
2913
+ * (2) is why this core reads {@link ActionContext.query} and (3) is why it
2914
+ * holds a subscription, bound in its own `mount` and handed to
2915
+ * `ctx.onTeardown` (TKT-197). It is the last of the eighteen elements to live
2916
+ * on a subscription.
2917
+ *
2918
+ * Behaviour is what the Stencil component did, to the letter, including:
2919
+ *
2920
+ * - a redirect that is not `succeeded` shows "Payment not completed" and does
2921
+ * NOT read the order, even when there is an order id to read.
2922
+ * - a re-read while an order is already on screen does not blank it: the
2923
+ * skeleton is only for the first read.
2924
+ * - a failed read keeps the thank-you header and puts the message underneath,
2925
+ * because someone who has just paid should not be told the purchase failed.
2926
+ * - `checkout:completed` shows what it carries at once and THEN re-reads,
2927
+ * because an embed's payload is only the id and the order number until the
2928
+ * tickets have been issued.
2929
+ * - the event name and date are best effort. The order view carries neither,
2930
+ * so they are walked out of the channel's listing, and a failure there
2931
+ * leaves the line at whatever the order itself said.
2932
+ *
2933
+ * What did not come across is the `console.error` tracing, which a library
2934
+ * should not do for its host (`tl-cart` dropped its own for the same reason).
2935
+ */
2936
+ const STRINGS$2 = {
2937
+ paymentFailedTitle: 'Payment not completed',
2938
+ paymentFailedLead: "Your payment wasn't completed. Your cart is still saved, so you can try again.",
2939
+ title: "You're going!",
2940
+ leadPrefix: 'Order ',
2941
+ leadSuffix: ' is confirmed. Your tickets are on their way to your email.',
2942
+ leadGeneric: 'Your order is confirmed. Your tickets are on their way to your email.',
2943
+ errorPrefix: "We couldn't load your order details yet. ",
2944
+ rowReference: 'Order reference',
2945
+ rowEmail: 'Email',
2946
+ rowEvent: 'Event',
2947
+ rowDate: 'Date',
2948
+ subtotalRow: 'Subtotal',
2949
+ taxRow: 'Tax',
2950
+ discountRow: 'Discount',
2951
+ totalRow: 'Total',
2952
+ ticketsTitle: 'Your tickets',
2953
+ ticketsHint: 'Show these QR codes at the door for entry. Use Download PDF on a ticket to save it.',
2954
+ noOrdersManager: 'Orders are not available',
2955
+ notFound: 'Order not found',
2956
+ loadFailed: 'Failed to load your order',
2957
+ };
2958
+ /** The query parameter a payment provider comes back on, and the one value that means paid. */
2959
+ const REDIRECT_STATUS = 'redirect_status';
2960
+ const REDIRECT_SUCCEEDED = 'succeeded';
2961
+ const orderConfirmationCore = defineElement({
2962
+ tag: 'tl-order-confirmation',
2963
+ docs: 'The thank-you block: order reference, email, event and date, the ticket lines, the money rows and the tickets.',
2964
+ category: 'checkout',
2965
+ runtimes: ['web', 'native'],
2966
+ tokens: [
2967
+ 'border',
2968
+ 'error',
2969
+ 'font-family',
2970
+ 'font-family-mono',
2971
+ 'foreground',
2972
+ 'muted',
2973
+ 'muted-foreground',
2974
+ 'radius-lg',
2975
+ 'radius-md',
2976
+ 'secondary',
2977
+ 'spacing-1',
2978
+ 'spacing-2',
2979
+ 'spacing-3',
2980
+ 'spacing-4',
2981
+ 'spacing-6',
2982
+ 'success',
2983
+ 'success-foreground',
2984
+ ],
2985
+ sdkMethods: ['orders.get', 'events.list', 'events.get', 'auth.customer', 'on'],
2986
+ props: {
2987
+ orderId: {
2988
+ type: 'string',
2989
+ tsType: 'string | undefined',
2990
+ attr: 'order-id',
2991
+ docs: 'The order to show. Falls back to `?orderId=` in the page URL.',
2992
+ },
2993
+ customerEmail: {
2994
+ type: 'string',
2995
+ tsType: 'string | undefined',
2996
+ attr: 'customer-email',
2997
+ docs: 'Email to show when the order view carries none (a host that collected it at checkout).',
2998
+ },
2999
+ showTickets: {
3000
+ type: 'boolean',
3001
+ attr: 'show-tickets',
3002
+ default: true,
3003
+ docs: 'Mount tl-order-tickets under the summary (default true).',
3004
+ },
3005
+ columns: { type: 'number', attr: 'columns', default: 2, docs: 'Columns for the ticket grid.' },
3006
+ },
3007
+ events: {
3008
+ tlOrderLoaded: { detail: '{ order: TlOrderData; }', docs: 'the order was loaded' },
3009
+ tlError: { detail: '{ message: string; }', docs: 'the order could not be loaded' },
3010
+ },
3011
+ strings: STRINGS$2,
3012
+ statuses: ['payment-failed', 'idle', 'loading', 'ready'],
3013
+ state: () => ({
3014
+ order: null,
3015
+ resolvedOrderId: null,
3016
+ loading: false,
3017
+ error: null,
3018
+ paymentFailed: false,
3019
+ occurrence: null,
3020
+ signedInCustomer: null,
3021
+ }),
3022
+ /**
3023
+ * On screen: read the link, start the read, subscribe.
3024
+ *
3025
+ * The query goes first because it decides whether there is anything to read
3026
+ * at all: a payment that did not go through is not an order to fetch. The
3027
+ * read is started and not awaited before subscribing, so a
3028
+ * `checkout:completed` that lands mid-read is not missed - the component
3029
+ * read in `componentWillLoad` and subscribed in `componentDidLoad`, and had
3030
+ * exactly that window.
3031
+ *
3032
+ * The subscription needs the client and the read does not: `load` waits for
3033
+ * the client itself, inside the try block that announces a failure, which is
3034
+ * why a page that never starts Live still emits `tlError` here when it was
3035
+ * given an order to show.
3036
+ */
3037
+ mount: (ctx) => {
3038
+ void (async () => {
3039
+ // The link, before anything else: a failed redirect means no read.
3040
+ const redirect = ctx.query.get(REDIRECT_STATUS);
3041
+ ctx.setState({
3042
+ paymentFailed: !!redirect && redirect !== REDIRECT_SUCCEEDED,
3043
+ resolvedOrderId: ctx.props.orderId || ctx.query.get('orderId') || null,
3044
+ });
3045
+ const reading = ctx.state.resolvedOrderId && !ctx.state.paymentFailed
3046
+ ? ctx.actions.load()
3047
+ : Promise.resolve();
3048
+ let client = null;
3049
+ try {
3050
+ client = await ctx.whenClient();
3051
+ }
3052
+ catch {
3053
+ client = null;
3054
+ }
3055
+ if (client && ctx.active()) {
3056
+ const off = client.on?.('checkout:completed', (payload) => {
3057
+ ctx.actions.checkoutCompleted(payload);
3058
+ });
3059
+ if (typeof off === 'function')
3060
+ ctx.onTeardown(off);
3061
+ }
3062
+ await reading;
3063
+ })();
3064
+ },
3065
+ status: ({ state }) => state.paymentFailed
3066
+ ? 'payment-failed'
3067
+ : !state.resolvedOrderId && !state.order
3068
+ ? 'idle'
3069
+ : state.loading
3070
+ ? 'loading'
3071
+ : 'ready',
3072
+ derive: ({ state, props, strings }) => {
3073
+ const order = state.order;
3074
+ const totals = orderTotals(order);
3075
+ const currency = totals.currency;
3076
+ const email = props.customerEmail || orderCustomerEmail(order, state.signedInCustomer);
3077
+ const eventName = state.occurrence?.eventName || (order ? orderEventName(order) : null);
3078
+ const startsAt = state.occurrence?.startsAt;
3079
+ const rows = [];
3080
+ if (order)
3081
+ rows.push({ label: strings.rowReference, value: order.orderNumber, reference: true });
3082
+ if (email)
3083
+ rows.push({ label: strings.rowEmail, value: email, reference: false });
3084
+ if (eventName)
3085
+ rows.push({ label: strings.rowEvent, value: eventName, reference: false });
3086
+ if (startsAt)
3087
+ rows.push({ label: strings.rowDate, value: formatDateTime(startsAt), reference: false });
3088
+ return {
3089
+ leadText: order?.orderNumber
3090
+ ? `${strings.leadPrefix}${order.orderNumber}${strings.leadSuffix}`
3091
+ : strings.leadGeneric,
3092
+ rows,
3093
+ lines: order
3094
+ ? ticketItems(order).map((item) => ({
3095
+ label: `${item.quantity} x ${item.name}`,
3096
+ priceLabel: formatPrice$1(item.subtotal, order.currency),
3097
+ }))
3098
+ : [],
3099
+ totals,
3100
+ subtotalLabel: formatPrice$1(totals.subtotal, currency),
3101
+ feeAmountLabel: formatPrice$1(totals.fees, currency),
3102
+ taxLabel: formatPrice$1(totals.tax, currency),
3103
+ discountLabel: formatPrice$1(totals.discount, currency),
3104
+ totalLabel: formatPrice$1(totals.total, currency),
3105
+ showTaxRow: totals.tax > 0,
3106
+ showDiscountRow: totals.discount > 0,
3107
+ // An embed's checkout:completed carries only id and order number until
3108
+ // the refetch lands, so there is nothing to summarise yet.
3109
+ showSummary: !!order && Array.isArray(order.items),
3110
+ showError: !!state.error && !order,
3111
+ showTickets: props.showTickets && !!order,
3112
+ ticketsOrderId: order?.id || state.resolvedOrderId || '',
3113
+ };
3114
+ },
3115
+ actions: (ctx) => {
3116
+ /**
3117
+ * Put an event name and a start time on the summary. The order view has
3118
+ * neither, so the occurrence is walked out of the channel's listing. Best
3119
+ * effort and never blocking: a failure leaves the line at what the order
3120
+ * said, and a result that arrives after the order changed underneath is
3121
+ * dropped.
3122
+ */
3123
+ const resolveEvent = async (client, order) => {
3124
+ const occurrenceId = order.tickets?.find((t) => t.eventOccurrenceId)?.eventOccurrenceId;
3125
+ if (!occurrenceId)
3126
+ return;
3127
+ try {
3128
+ const resolved = await resolveOccurrence(client, occurrenceId);
3129
+ if (resolved && ctx.state.order?.id === order.id)
3130
+ ctx.setState({ occurrence: resolved });
3131
+ }
3132
+ catch {
3133
+ // Leave the event line at what the order gives us.
3134
+ }
3135
+ };
3136
+ const self = {
3137
+ /** Read the order. The skeleton is only for the first read. */
3138
+ async load() {
3139
+ const id = ctx.state.resolvedOrderId;
3140
+ if (!id)
3141
+ return;
3142
+ try {
3143
+ ctx.setState({ loading: !ctx.state.order, error: null });
3144
+ const client = await ctx.whenClient();
3145
+ const orders = client.getOrdersManager?.();
3146
+ if (!orders)
3147
+ throw new Error(ctx.strings.noOrdersManager);
3148
+ const order = (await orders.get(id));
3149
+ if (!order)
3150
+ throw new Error(ctx.strings.notFound);
3151
+ ctx.setState({
3152
+ order,
3153
+ signedInCustomer: client.getAuthManager?.()?.customer ?? null,
3154
+ });
3155
+ ctx.emit('tlOrderLoaded', { order });
3156
+ void resolveEvent(client, order);
3157
+ }
3158
+ catch (err) {
3159
+ const message = errorMessage(err, ctx.strings.loadFailed);
3160
+ ctx.setState({ error: message });
3161
+ ctx.emit('tlError', { message });
3162
+ }
3163
+ finally {
3164
+ ctx.setState({ loading: false });
3165
+ }
3166
+ },
3167
+ /**
3168
+ * The host pointed the element at a different order. It resolves again
3169
+ * the way the mount did - the prop first, the link second - and reads.
3170
+ * Not a remount: the `checkout:completed` subscription belongs to the
3171
+ * element, not to the order it happens to be showing.
3172
+ */
3173
+ orderIdChanged() {
3174
+ ctx.setState({ resolvedOrderId: ctx.props.orderId || ctx.query.get('orderId') || null });
3175
+ void self.load();
3176
+ },
3177
+ /**
3178
+ * The client finished a checkout. Show what it carries at once, because
3179
+ * an order number on screen beats a skeleton, then read the order again:
3180
+ * the tickets are issued after the order exists, so the payload almost
3181
+ * never has them.
3182
+ */
3183
+ checkoutCompleted(order) {
3184
+ if (!order?.id)
3185
+ return;
3186
+ ctx.setState({ paymentFailed: false, order, resolvedOrderId: order.id });
3187
+ ctx.emit('tlOrderLoaded', { order });
3188
+ void self.load();
3189
+ },
3190
+ };
3191
+ return self;
3192
+ },
3193
+ });
3194
+
3195
+ /**
3196
+ * `tl-order-tickets`: every ticket in an order, as scannable cards.
3197
+ *
3198
+ * The one core that waits rather than concluding. Tickets are issued
3199
+ * asynchronously - the payment confirms, the order is marked paid, and
3200
+ * entitlement issuance follows a moment later - so a confirmation page renders
3201
+ * BEFORE the passes exist, reliably rather than occasionally. The element used
3202
+ * to answer that with "no tickets for this order yet", which is true and reads
3203
+ * as "something went wrong" to somebody who has just been charged. Refreshing
3204
+ * showed them, so the only thing missing was the waiting.
3205
+ *
3206
+ * So an empty order is treated as issuance in flight and polled, with the gaps
3207
+ * widening, for about a minute. Then it stops and says the tickets are coming
3208
+ * by email, because at that point something probably is wrong and an element
3209
+ * that polls forever is worse than one that admits it.
3210
+ *
3211
+ * The polling belongs to the mount, which is what the split adds: the
3212
+ * component cancelled its timer in `disconnectedCallback`, and here the mount's
3213
+ * teardown does it, so every runtime gets the same guarantee rather than each
3214
+ * harness remembering. A scheduled poll is cancelled by moving the generation
3215
+ * on rather than by clearing a handle - `ctx.after` has no handle to clear -
3216
+ * so a stale timer still fires and does nothing, which is the same thing from
3217
+ * outside: no request is made.
3218
+ *
3219
+ * Behaviour is what the Stencil component did, to the letter, including:
3220
+ *
3221
+ * - a failed read STOPS the polling. Retrying a 403 a dozen times helps
3222
+ * nobody, and a failure is a different thing from an order with no tickets.
3223
+ * - `loading` is only ever true before the first read finishes. Every later
3224
+ * read, including a poll and a new order id, leaves whatever is on screen.
3225
+ * - pointing it at another order clears the tickets without putting the
3226
+ * loading state back, so there is a beat where the element reads as "taking
3227
+ * longer than usual" before the new read lands. Kept, not tidied.
3228
+ *
3229
+ * There is no `tlError` here, and no event of any kind: nothing this element
3230
+ * does is announced to the host. Kept as found.
3231
+ */
3232
+ /**
3233
+ * Gaps between polls, in milliseconds. Front-loaded because issuance usually
3234
+ * lands in the first few seconds, then widening so a genuinely stuck order is
3235
+ * not hammered. Sums to roughly one minute, and running out is what turns
3236
+ * `issuing` into `delayed`.
3237
+ */
3238
+ const POLL_DELAYS_MS = [
3239
+ 1000, 1500, 2000, 3000, 4000, 5000, 6000, 8000, 10000, 12000,
3240
+ ];
3241
+ const STRINGS$1 = {
3242
+ loading: 'Loading tickets…',
3243
+ preparing: 'Preparing your tickets…',
3244
+ delayed: 'Your tickets are taking longer than usual. They will be emailed to you as soon as they are ready.',
3245
+ notAvailable: 'Tickets are not available',
3246
+ loadFailed: 'Failed to load tickets',
3247
+ };
3248
+ const orderTicketsCore = defineElement({
3249
+ tag: 'tl-order-tickets',
3250
+ docs: "Every ticket in an order as a scannable card, waiting out issuance rather than reporting an empty order as empty.",
3251
+ category: 'tickets',
3252
+ runtimes: ['web', 'native'],
3253
+ tokens: ['border', 'error', 'font-family', 'muted-foreground', 'primary', 'spacing-2', 'spacing-4', 'spacing-6'],
3254
+ sdkMethods: ['orders.get'],
3255
+ props: {
3256
+ orderId: { type: 'string', attr: 'order-id', required: true, docs: 'The order whose tickets to show.' },
3257
+ columns: { type: 'number', attr: 'columns', default: 2, docs: 'Columns in the grid (default 2).' },
3258
+ },
3259
+ events: {},
3260
+ strings: STRINGS$1,
3261
+ statuses: ['loading', 'error', 'issuing', 'delayed', 'ready'],
3262
+ state: () => ({ tickets: [], loading: true, error: null, issuing: false }),
3263
+ /**
3264
+ * On screen: read, and keep reading while issuance is in flight. Off screen:
3265
+ * stop, which is the whole reason the polling lives in the mount.
3266
+ */
3267
+ mount: (ctx) => {
3268
+ ctx.onTeardown(() => ctx.actions.stopPolling());
3269
+ void ctx.actions.load();
3270
+ },
3271
+ status: ({ state }) => state.loading
3272
+ ? 'loading'
3273
+ : state.error
3274
+ ? 'error'
3275
+ : state.tickets.length > 0
3276
+ ? 'ready'
3277
+ : state.issuing
3278
+ ? 'issuing'
3279
+ : 'delayed',
3280
+ derive: ({ state, props }) => ({
3281
+ tickets: state.tickets,
3282
+ columns: props.columns,
3283
+ orderId: props.orderId,
3284
+ }),
3285
+ actions: (ctx) => {
3286
+ // Neither of these is state: nothing renders from them, and the component
3287
+ // held both as instance fields for the same reason. `generation` is how a
3288
+ // scheduled poll is cancelled, since `ctx.after` hands back no handle.
3289
+ let generation = 0;
3290
+ let poll = 0;
3291
+ const self = {
3292
+ /**
3293
+ * Read the order. Tickets end the waiting; no tickets and a gap left in
3294
+ * the schedule extends it; no tickets and no gap left gives up.
3295
+ */
3296
+ async load() {
3297
+ try {
3298
+ ctx.setState({ error: null });
3299
+ const client = await ctx.whenClient();
3300
+ const orders = client.getOrdersManager?.();
3301
+ if (!orders)
3302
+ throw new Error(ctx.strings.notAvailable);
3303
+ const order = (await orders.get(ctx.props.orderId));
3304
+ const tickets = order?.tickets ?? [];
3305
+ ctx.setState({ tickets });
3306
+ if (tickets.length > 0) {
3307
+ // Arrived. Stop asking.
3308
+ ctx.setState({ issuing: false });
3309
+ self.stopPolling();
3310
+ }
3311
+ else {
3312
+ const delay = POLL_DELAYS_MS[poll];
3313
+ // `undefined` once the schedule runs out: give up waiting, and say so.
3314
+ ctx.setState({ issuing: delay !== undefined });
3315
+ if (delay !== undefined) {
3316
+ poll += 1;
3317
+ self.stopPolling();
3318
+ const mine = generation;
3319
+ ctx.after(delay, () => {
3320
+ if (mine === generation)
3321
+ void self.load();
3322
+ });
3323
+ }
3324
+ }
3325
+ }
3326
+ catch (err) {
3327
+ // A failed request is a different thing from an order with no
3328
+ // tickets, and stops the polling.
3329
+ //
3330
+ // Defensive rather than load bearing, as it was in the component:
3331
+ // a poll is scheduled only on the success path, and the timer that
3332
+ // triggered this read has already fired, so there is normally
3333
+ // nothing left to cancel. Mutation testing says so out loud -
3334
+ // removing this line kills no test - and it is kept because the
3335
+ // component had it and because it is what makes the intent legible.
3336
+ self.stopPolling();
3337
+ ctx.setState({ issuing: false, error: err instanceof Error ? err.message : ctx.strings.loadFailed });
3338
+ }
3339
+ finally {
3340
+ ctx.setState({ loading: false });
3341
+ }
3342
+ },
3343
+ /**
3344
+ * A different order: forget this one, put the schedule back to the top
3345
+ * and read. The loading state is deliberately NOT restored, so the
3346
+ * element reads as "taking longer than usual" for a beat, exactly as the
3347
+ * component did.
3348
+ */
3349
+ orderIdChanged() {
3350
+ self.stopPolling();
3351
+ poll = 0;
3352
+ ctx.setState({ tickets: [], issuing: false });
3353
+ void self.load();
3354
+ },
3355
+ /** Cancel whatever poll is scheduled. Also what the teardown runs. */
3356
+ stopPolling() {
3357
+ generation += 1;
3358
+ },
3359
+ };
3360
+ return self;
3361
+ },
3362
+ });
3363
+
3364
+ /**
3365
+ * `tl-ticket-selector`: the reference core.
3366
+ *
3367
+ * This is the whole of what the element does. The Stencil harness in
3368
+ * `packages/elements/src/components/tl-ticket-selector/` renders `view` and
3369
+ * forwards the three `tl*` events; it decides nothing, holds no copy and does
3370
+ * no arithmetic. A React Native harness will render the same view.
3371
+ *
3372
+ * Behaviour is the behaviour the Stencil component had before the split, to
3373
+ * the letter: the same quantity clamping, the same reset after a successful
3374
+ * add, the same three second success message, the same button text, and the
3375
+ * same rule that no fee row is shown until the surface quotes a fee. The one
3376
+ * thing that did not come across is the `console.log` tracing, which a library
3377
+ * should not do for its host.
3378
+ */
3379
+ const STRINGS = {
3380
+ selectTickets: 'Select Tickets',
3381
+ adding: 'Adding...',
3382
+ added: 'Added to cart!',
3383
+ limited: 'Limited',
3384
+ soldOut: 'Sold Out',
3385
+ ticketsRow: 'Tickets',
3386
+ totalRow: 'Total',
3387
+ decrease: 'Decrease quantity',
3388
+ increase: 'Increase quantity',
3389
+ loadFailed: 'Failed to load tickets',
3390
+ addFailed: 'Failed to add to cart',
3391
+ noClient: 'SDK not initialized.',
3392
+ };
3393
+ /** How long the "Added to cart!" message stays up. */
3394
+ const SUCCESS_MS = 3000;
3395
+ const totalsOf = (ctx) => selectionTotals(ctx.state.ticketTypes.map((tt) => ({ price: tt.price, quantity: ctx.state.quantities[tt.id] || 0 })), ctx.state.feeQuote);
3396
+ const totalItemsOf = (state) => Object.values(state.quantities).reduce((sum, qty) => sum + qty, 0);
3397
+ const ticketSelectorCore = defineElement({
3398
+ tag: 'tl-ticket-selector',
3399
+ docs: 'Pick quantities of the ticket types on sale for one occurrence, and add them to the cart.',
3400
+ category: 'purchase',
3401
+ runtimes: ['web', 'native'],
3402
+ tokens: [
3403
+ 'background',
3404
+ 'border',
3405
+ 'border-hover',
3406
+ 'error',
3407
+ 'error-bg',
3408
+ 'font-family',
3409
+ 'foreground',
3410
+ 'muted',
3411
+ 'muted-foreground',
3412
+ 'primary',
3413
+ 'primary-foreground',
3414
+ 'radius-md',
3415
+ 'radius-sm',
3416
+ 'spacing-1',
3417
+ 'spacing-2',
3418
+ 'spacing-3',
3419
+ 'spacing-4',
3420
+ 'success',
3421
+ 'warning',
3422
+ 'warning-bg',
3423
+ ],
3424
+ sdkMethods: ['cart.addItem', 'events.getTicketTypes'],
3425
+ props: {
3426
+ eventId: { type: 'string', attr: 'event-id', required: true, docs: 'Event ID' },
3427
+ occurrenceId: { type: 'string', attr: 'occurrence-id', required: true, docs: 'Occurrence ID' },
3428
+ showButton: { type: 'boolean', attr: 'show-button', default: true, docs: 'Show add to cart button' },
3429
+ buttonLabel: { type: 'string', attr: 'button-label', default: 'Add to Cart', docs: 'Button label' },
3430
+ },
3431
+ events: {
3432
+ tlAddToCart: {
3433
+ detail: '{ items: AddToCartItem[]; }',
3434
+ docs: 'Emitted when items are added to cart',
3435
+ },
3436
+ tlQuantityChange: {
3437
+ detail: '{ ticketTypeId: string; quantity: number; total: number; }',
3438
+ docs: 'Emitted when quantity changes',
3439
+ },
3440
+ tlError: {
3441
+ detail: '{ message: string; }',
3442
+ docs: 'Emitted on error',
3443
+ },
3444
+ },
3445
+ strings: STRINGS,
3446
+ statuses: ['loading', 'error', 'ready'],
3447
+ state: () => ({
3448
+ ticketTypes: [],
3449
+ quantities: {},
3450
+ loading: true,
3451
+ error: null,
3452
+ adding: false,
3453
+ addSuccess: false,
3454
+ feeQuote: null,
3455
+ }),
3456
+ /**
3457
+ * On screen: wait for the host's client, then read the ticket types. The
3458
+ * harness used to do this (it awaited `whenSDK()` and called `load()`
3459
+ * itself); the core does it now, so every runtime loads at the same moment
3460
+ * and in the same order (TKT-197).
3461
+ *
3462
+ * There is nothing to undo, so nothing is returned. The guards are what make
3463
+ * a remount safe: `active()` is false once this mount has been torn down, so
3464
+ * an element that was removed while the client was still coming up neither
3465
+ * loads nor writes an error into a view nobody is rendering.
3466
+ */
3467
+ mount: (ctx) => {
3468
+ // Nothing to read until the host has said which occurrence is on sale.
3469
+ // Until then the element stays in `loading`, which is what it did before.
3470
+ if (!ctx.props.eventId || !ctx.props.occurrenceId)
3471
+ return;
3472
+ void (async () => {
3473
+ try {
3474
+ await ctx.whenClient();
3475
+ if (!ctx.active())
3476
+ return;
3477
+ await ctx.actions.load();
3478
+ }
3479
+ catch (err) {
3480
+ if (!ctx.active())
3481
+ return;
3482
+ ctx.actions.connectFailed(err instanceof Error ? err.message : ctx.strings.noClient);
3483
+ }
3484
+ })();
3485
+ },
3486
+ status: ({ state }) => (state.loading ? 'loading' : state.error ? 'error' : 'ready'),
3487
+ derive: (ctx) => {
3488
+ const { state, strings, props, formatMoney } = ctx;
3489
+ const totals = totalsOf(ctx);
3490
+ const totalItems = totalItemsOf(state);
3491
+ const currency = state.ticketTypes[0]?.currency;
3492
+ const rows = state.ticketTypes.map((tt) => {
3493
+ const quantity = state.quantities[tt.id] || 0;
3494
+ const soldOut = tt.availabilityStatus === 'sold_out';
3495
+ return {
3496
+ id: tt.id,
3497
+ name: tt.name,
3498
+ description: tt.description ?? null,
3499
+ priceLabel: formatMoney(tt.price, tt.currency),
3500
+ quantity,
3501
+ soldOut,
3502
+ limited: tt.availabilityStatus === 'limited',
3503
+ canDecrease: !soldOut && quantity > 0,
3504
+ canIncrease: !soldOut && quantity < tt.maxPerOrder,
3505
+ };
3506
+ });
3507
+ return {
3508
+ rows,
3509
+ totals,
3510
+ totalItems,
3511
+ currency,
3512
+ subtotalLabel: formatMoney(totals.subtotal, currency),
3513
+ feeLabel: formatMoney(totals.fees, currency),
3514
+ totalLabel: formatMoney(totals.total, currency),
3515
+ showTotals: totals.showFee && totalItems > 0,
3516
+ actionLabel: state.adding
3517
+ ? strings.adding
3518
+ : totalItems === 0
3519
+ ? strings.selectTickets
3520
+ : `${props.buttonLabel} - ${formatMoney(totals.total)}`,
3521
+ actionDisabled: totalItems === 0 || state.adding,
3522
+ };
3523
+ },
3524
+ actions: (ctx) => ({
3525
+ /** Read the ticket types for the current event and occurrence. */
3526
+ async load() {
3527
+ try {
3528
+ ctx.setState({ loading: true, error: null });
3529
+ const types = (await ctx.client().getEventsManager().getTicketTypes(ctx.props.eventId, ctx.props.occurrenceId));
3530
+ const quantities = {};
3531
+ for (const type of types)
3532
+ quantities[type.id] = 0;
3533
+ ctx.setState({ ticketTypes: types, quantities });
3534
+ }
3535
+ catch (err) {
3536
+ const message = err instanceof Error ? err.message : ctx.strings.loadFailed;
3537
+ ctx.setState({ error: message });
3538
+ ctx.emit('tlError', { message });
3539
+ }
3540
+ finally {
3541
+ ctx.setState({ loading: false });
3542
+ }
3543
+ },
3544
+ /** Nudge one row's quantity, clamped to 0 and the type's maxPerOrder. */
3545
+ changeQuantity(ticketTypeId, delta) {
3546
+ const ticketType = ctx.state.ticketTypes.find((t) => t.id === ticketTypeId);
3547
+ if (!ticketType)
3548
+ return;
3549
+ const current = ctx.state.quantities[ticketTypeId] || 0;
3550
+ const quantity = Math.max(0, Math.min(ticketType.maxPerOrder, current + delta));
3551
+ ctx.setState((state) => ({ quantities: { ...state.quantities, [ticketTypeId]: quantity } }));
3552
+ // After setState, so the total is the one the buyer can now see.
3553
+ ctx.emit('tlQuantityChange', { ticketTypeId, quantity, total: totalsOf(ctx).total });
3554
+ },
3555
+ /**
3556
+ * Add every selected line to the cart, then clear the selection.
3557
+ *
3558
+ * With no client, or a client too old to carry a cart manager, the
3559
+ * selection is still cleared and `tlAddToCart` still fires: a host that
3560
+ * listens for the event and does its own cart keeps working.
3561
+ */
3562
+ async addToCart() {
3563
+ const items = Object.entries(ctx.state.quantities)
3564
+ .filter(([, quantity]) => quantity > 0)
3565
+ .map(([ticketTypeId, quantity]) => ({ ticketTypeId, quantity }));
3566
+ if (items.length === 0)
3567
+ return;
3568
+ ctx.setState({ adding: true, addSuccess: false });
3569
+ try {
3570
+ const cart = ctx.optionalClient()?.getCartManager?.();
3571
+ if (cart) {
3572
+ for (const item of items) {
3573
+ await cart.addItem(item.ticketTypeId, item.quantity, ctx.props.occurrenceId);
3574
+ }
3575
+ }
3576
+ const quantities = {};
3577
+ for (const type of ctx.state.ticketTypes)
3578
+ quantities[type.id] = 0;
3579
+ ctx.setState({ quantities, addSuccess: true });
3580
+ ctx.after(SUCCESS_MS, () => ctx.setState({ addSuccess: false }));
3581
+ ctx.emit('tlAddToCart', { items });
3582
+ }
3583
+ catch (err) {
3584
+ const message = err instanceof Error ? err.message : ctx.strings.addFailed;
3585
+ ctx.setState({ error: message });
3586
+ ctx.emit('tlError', { message });
3587
+ }
3588
+ finally {
3589
+ ctx.setState({ adding: false });
3590
+ }
3591
+ },
3592
+ /** What the surface quoted for this selection, when it ever quotes one. */
3593
+ setFeeQuote(quote) {
3594
+ ctx.setState({ feeQuote: quote });
3595
+ },
3596
+ /**
3597
+ * The host never produced a client. Shown, not emitted: the element has
3598
+ * nothing to say to a page that has not started Live yet.
3599
+ */
3600
+ connectFailed(message) {
3601
+ ctx.setState({ error: message || ctx.strings.noClient, loading: false });
3602
+ },
3603
+ }),
3604
+ });
3605
+
3606
+ export { cartDrawerCore as a, buyTicketsButtonCore as b, createElement as c, cartCore as d, eventListCore as e, eventCardCore as f, cartBadgeCore as g, occurrenceSelectorCore as h, orderTicketsCore as i, buyTicketsWrapperCore as j, checkoutWrapperCore as k, loginCore as l, myOrdersCore as m, orderConfirmationCore as o, promoCodeCore as p, ticketSelectorCore as t };