@ticketlayer/elements 0.4.0 → 1.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (161) hide show
  1. package/dist/cjs/{index-I9CoYv3k.js → index-DanPUX0s.js} +1 -1
  2. package/dist/cjs/index.cjs.js +2 -1
  3. package/dist/cjs/loader.cjs.js +2 -2
  4. package/dist/cjs/ticket-selector-BYVL13k_.js +3623 -0
  5. package/dist/cjs/ticketlayer-elements.cjs.js +2 -2
  6. package/dist/cjs/tl-buy-modal.cjs.entry.js +3 -3
  7. package/dist/cjs/tl-buy-tickets-button.cjs.entry.js +89 -74
  8. package/dist/cjs/tl-buy-tickets-wrapper.cjs.entry.js +61 -30
  9. package/dist/cjs/tl-cart-badge.cjs.entry.js +74 -61
  10. package/dist/cjs/tl-cart-drawer.cjs.entry.js +51 -23
  11. package/dist/cjs/tl-cart.cjs.entry.js +71 -103
  12. package/dist/cjs/tl-checkout-wrapper.cjs.entry.js +51 -29
  13. package/dist/cjs/tl-event-card.cjs.entry.js +43 -105
  14. package/dist/cjs/tl-event-list.cjs.entry.js +72 -35
  15. package/dist/cjs/tl-login.cjs.entry.js +74 -141
  16. package/dist/cjs/tl-my-orders.cjs.entry.js +52 -92
  17. package/dist/cjs/tl-occurrence-selector_2.cjs.entry.js +153 -921
  18. package/dist/cjs/tl-order-confirmation.cjs.entry.js +71 -107
  19. package/dist/cjs/tl-order-tickets_2.cjs.entry.js +48 -74
  20. package/dist/cjs/tl-promo-code.cjs.entry.js +50 -47
  21. package/dist/cjs/tl-seat-map.cjs.entry.js +3 -3
  22. package/dist/cjs/web-query-Ce73jB4k.js +16 -0
  23. package/dist/collection/components/tl-buy-modal/tl-buy-modal.js +2 -2
  24. package/dist/collection/components/tl-buy-tickets-button/tl-buy-tickets-button.css +37 -0
  25. package/dist/collection/components/tl-buy-tickets-button/tl-buy-tickets-button.js +105 -79
  26. package/dist/collection/components/tl-buy-tickets-wrapper/tl-buy-tickets-wrapper.js +73 -30
  27. package/dist/collection/components/tl-cart/tl-cart.js +82 -107
  28. package/dist/collection/components/tl-cart-badge/tl-cart-badge.js +62 -62
  29. package/dist/collection/components/tl-cart-drawer/tl-cart-drawer.js +56 -24
  30. package/dist/collection/components/tl-checkout-wrapper/tl-checkout-wrapper.js +61 -29
  31. package/dist/collection/components/tl-event-card/event-card-adapter.js +11 -23
  32. package/dist/collection/components/tl-event-card/tl-event-card.js +51 -84
  33. package/dist/collection/components/tl-event-list/tl-event-list.js +82 -38
  34. package/dist/collection/components/tl-login/tl-login.js +83 -147
  35. package/dist/collection/components/tl-my-orders/tl-my-orders.js +58 -96
  36. package/dist/collection/components/tl-occurrence-selector/tl-occurrence-selector.js +137 -516
  37. package/dist/collection/components/tl-order-confirmation/tl-order-confirmation.js +81 -115
  38. package/dist/collection/components/tl-order-tickets/tl-order-tickets.js +57 -92
  39. package/dist/collection/components/tl-promo-code/tl-promo-code.js +55 -50
  40. package/dist/collection/components/tl-seat-map/tl-seat-map.js +2 -2
  41. package/dist/collection/components/tl-ticket-selector/tl-ticket-selector.js +34 -20
  42. package/dist/collection/utils/order-summary.js +15 -135
  43. package/dist/collection/utils/testing/deep-text.js +29 -0
  44. package/dist/collection/utils/testing/fixtures.js +290 -0
  45. package/dist/collection/utils/testing/mock-sdk.js +13 -2
  46. package/dist/collection/utils/web-query.js +12 -0
  47. package/dist/collection/utils/web-storage.js +22 -0
  48. package/dist/components/ticket-selector.js +1 -0
  49. package/dist/components/tl-buy-modal.js +1 -1
  50. package/dist/components/tl-buy-tickets-button.js +1 -1
  51. package/dist/components/tl-buy-tickets-wrapper2.js +1 -1
  52. package/dist/components/tl-cart-badge.js +1 -1
  53. package/dist/components/tl-cart-drawer.js +1 -1
  54. package/dist/components/tl-cart2.js +1 -1
  55. package/dist/components/tl-checkout-wrapper2.js +1 -1
  56. package/dist/components/tl-event-card2.js +1 -1
  57. package/dist/components/tl-event-list.js +1 -1
  58. package/dist/components/tl-login2.js +1 -1
  59. package/dist/components/tl-my-orders.js +1 -1
  60. package/dist/components/tl-occurrence-selector2.js +1 -1
  61. package/dist/components/tl-order-confirmation.js +1 -1
  62. package/dist/components/tl-order-tickets2.js +1 -1
  63. package/dist/components/tl-promo-code.js +1 -1
  64. package/dist/components/tl-seat-map.js +1 -1
  65. package/dist/components/tl-ticket-selector2.js +1 -1
  66. package/dist/components/web-query.js +1 -0
  67. package/dist/elements.docs.json +34 -34
  68. package/dist/esm/{index-OoYV1faY.js → index-imfxp765.js} +1 -1
  69. package/dist/esm/index.js +2 -1
  70. package/dist/esm/loader.js +3 -3
  71. package/dist/esm/ticket-selector-BwChnHVC.js +3606 -0
  72. package/dist/esm/ticketlayer-elements.js +3 -3
  73. package/dist/esm/tl-buy-modal.entry.js +3 -3
  74. package/dist/esm/tl-buy-tickets-button.entry.js +90 -75
  75. package/dist/esm/tl-buy-tickets-wrapper.entry.js +62 -31
  76. package/dist/esm/tl-cart-badge.entry.js +75 -62
  77. package/dist/esm/tl-cart-drawer.entry.js +52 -24
  78. package/dist/esm/tl-cart.entry.js +72 -104
  79. package/dist/esm/tl-checkout-wrapper.entry.js +52 -30
  80. package/dist/esm/tl-event-card.entry.js +44 -106
  81. package/dist/esm/tl-event-list.entry.js +73 -36
  82. package/dist/esm/tl-login.entry.js +75 -142
  83. package/dist/esm/tl-my-orders.entry.js +53 -93
  84. package/dist/esm/tl-occurrence-selector_2.entry.js +152 -920
  85. package/dist/esm/tl-order-confirmation.entry.js +72 -108
  86. package/dist/esm/tl-order-tickets_2.entry.js +49 -75
  87. package/dist/esm/tl-promo-code.entry.js +51 -48
  88. package/dist/esm/tl-seat-map.entry.js +3 -3
  89. package/dist/esm/web-query-Bdbb2lk5.js +14 -0
  90. package/dist/ticketlayer-elements/index.esm.js +1 -1
  91. package/dist/ticketlayer-elements/p-45923d2c.entry.js +1 -0
  92. package/dist/ticketlayer-elements/p-492adc77.entry.js +1 -0
  93. package/dist/ticketlayer-elements/p-4cfc9e53.entry.js +1 -0
  94. package/dist/ticketlayer-elements/{p-ac264adc.entry.js → p-5135630e.entry.js} +1 -1
  95. package/dist/ticketlayer-elements/p-5bbce3b5.entry.js +1 -0
  96. package/dist/ticketlayer-elements/p-7d547721.entry.js +1 -0
  97. package/dist/ticketlayer-elements/p-7fe7b6ba.entry.js +1 -0
  98. package/dist/ticketlayer-elements/p-8122f0c1.entry.js +1 -0
  99. package/dist/ticketlayer-elements/p-98d40869.entry.js +1 -0
  100. package/dist/ticketlayer-elements/p-Bdbb2lk5.js +1 -0
  101. package/dist/ticketlayer-elements/p-BwChnHVC.js +1 -0
  102. package/dist/ticketlayer-elements/p-abec290a.entry.js +1 -0
  103. package/dist/ticketlayer-elements/p-ac79a3f6.entry.js +1 -0
  104. package/dist/ticketlayer-elements/p-d01f4429.entry.js +1 -0
  105. package/dist/ticketlayer-elements/p-d7ffaf46.entry.js +1 -0
  106. package/dist/ticketlayer-elements/p-e7170f3c.entry.js +1 -0
  107. package/dist/ticketlayer-elements/p-e787e862.entry.js +1 -0
  108. package/dist/ticketlayer-elements/p-f1ff931f.entry.js +1 -0
  109. package/dist/ticketlayer-elements/p-imfxp765.js +3 -0
  110. package/dist/ticketlayer-elements/ticketlayer-elements.css +1 -1
  111. package/dist/ticketlayer-elements/ticketlayer-elements.esm.js +1 -1
  112. package/dist/types/components/tl-buy-tickets-button/tl-buy-tickets-button.d.ts +40 -8
  113. package/dist/types/components/tl-buy-tickets-wrapper/tl-buy-tickets-wrapper.d.ts +20 -1
  114. package/dist/types/components/tl-cart/tl-cart.d.ts +40 -42
  115. package/dist/types/components/tl-cart-badge/tl-cart-badge.d.ts +22 -7
  116. package/dist/types/components/tl-cart-drawer/tl-cart-drawer.d.ts +28 -5
  117. package/dist/types/components/tl-checkout-wrapper/tl-checkout-wrapper.d.ts +18 -1
  118. package/dist/types/components/tl-event-card/event-card-adapter.d.ts +9 -20
  119. package/dist/types/components/tl-event-card/tl-event-card.d.ts +25 -28
  120. package/dist/types/components/tl-event-list/tl-event-list.d.ts +46 -6
  121. package/dist/types/components/tl-login/tl-login.d.ts +68 -22
  122. package/dist/types/components/tl-my-orders/tl-my-orders.d.ts +30 -12
  123. package/dist/types/components/tl-occurrence-selector/tl-occurrence-selector.d.ts +73 -161
  124. package/dist/types/components/tl-order-confirmation/tl-order-confirmation.d.ts +48 -15
  125. package/dist/types/components/tl-order-tickets/tl-order-tickets.d.ts +29 -31
  126. package/dist/types/components/tl-promo-code/tl-promo-code.d.ts +28 -10
  127. package/dist/types/components/tl-ticket/tl-ticket.d.ts +2 -11
  128. package/dist/types/components/tl-ticket-selector/tl-ticket-selector.d.ts +22 -4
  129. package/dist/types/components.d.ts +270 -62
  130. package/dist/types/utils/order-summary.d.ts +14 -97
  131. package/dist/types/utils/testing/deep-text.d.ts +13 -0
  132. package/dist/types/utils/testing/fixtures.d.ts +225 -0
  133. package/dist/types/utils/testing/mock-sdk.d.ts +14 -1
  134. package/dist/types/utils/web-query.d.ts +18 -0
  135. package/dist/types/utils/web-storage.d.ts +16 -0
  136. package/elements.catalogue.json +602 -50
  137. package/package.json +8 -6
  138. package/dist/cjs/money-BtmM2gt-.js +0 -118
  139. package/dist/cjs/order-summary-X3TlZxc2.js +0 -150
  140. package/dist/components/money.js +0 -1
  141. package/dist/components/order-summary.js +0 -1
  142. package/dist/esm/money-4ig0-WB6.js +0 -112
  143. package/dist/esm/order-summary-DiDtvfZM.js +0 -136
  144. package/dist/ticketlayer-elements/p-0814420f.entry.js +0 -1
  145. package/dist/ticketlayer-elements/p-0acabc26.entry.js +0 -1
  146. package/dist/ticketlayer-elements/p-109a3feb.entry.js +0 -1
  147. package/dist/ticketlayer-elements/p-1a4ba71c.entry.js +0 -1
  148. package/dist/ticketlayer-elements/p-318965c1.entry.js +0 -1
  149. package/dist/ticketlayer-elements/p-347c7fd1.entry.js +0 -1
  150. package/dist/ticketlayer-elements/p-3649c26d.entry.js +0 -1
  151. package/dist/ticketlayer-elements/p-40260cc9.entry.js +0 -1
  152. package/dist/ticketlayer-elements/p-4ig0-WB6.js +0 -1
  153. package/dist/ticketlayer-elements/p-55fd2f9f.entry.js +0 -1
  154. package/dist/ticketlayer-elements/p-62ee659b.entry.js +0 -1
  155. package/dist/ticketlayer-elements/p-7d4ecc2b.entry.js +0 -1
  156. package/dist/ticketlayer-elements/p-7f0abf95.entry.js +0 -1
  157. package/dist/ticketlayer-elements/p-BXpVGW51.js +0 -1
  158. package/dist/ticketlayer-elements/p-OoYV1faY.js +0 -3
  159. package/dist/ticketlayer-elements/p-a3c1b26c.entry.js +0 -1
  160. package/dist/ticketlayer-elements/p-aeb4c6fb.entry.js +0 -1
  161. package/dist/ticketlayer-elements/p-ba62541f.entry.js +0 -1
@@ -1,399 +1,21 @@
1
- import { r as registerInstance, c as createEvent, h } from './index-OoYV1faY.js';
1
+ import { r as registerInstance, c as createEvent, h } from './index-imfxp765.js';
2
2
  import { w as whenSDK, g as getSDK } from './sdk-connector-CqhvfyKV.js';
3
- import { s as selectionTotals } from './money-4ig0-WB6.js';
4
-
5
- const CATEGORIES = [
6
- 'discovery',
7
- 'purchase',
8
- 'cart',
9
- 'checkout',
10
- 'account',
11
- 'tickets',
12
- ];
13
- const RUNTIMES = ['web', 'native'];
14
- /**
15
- * Declare a core. The checks here are the ones a type cannot make: a tag that
16
- * is not a `tl-` custom element name, a category or runtime outside the set the
17
- * catalogue knows, a duplicated token, a status list that does not include what
18
- * `status` can return. They run at import time, so a malformed core fails the
19
- * build rather than a browser.
20
- */
21
- function defineElement(core) {
22
- const fail = (message) => {
23
- throw new Error(`[elements-core] ${core.tag || '<no tag>'}: ${message}`);
24
- };
25
- if (!/^tl-[a-z0-9]+(-[a-z0-9]+)*$/.test(core.tag))
26
- fail('tag must look like tl-something');
27
- if (!CATEGORIES.includes(core.category))
28
- fail(`category must be one of ${CATEGORIES.join(', ')}`);
29
- if (!core.runtimes.length || core.runtimes.some((r) => !RUNTIMES.includes(r))) {
30
- fail(`runtimes must be a non-empty subset of ${RUNTIMES.join(', ')}`);
31
- }
32
- if (new Set(core.tokens).size !== core.tokens.length)
33
- fail('tokens must not repeat');
34
- if (!core.statuses.length)
35
- fail('statuses must not be empty');
36
- for (const [name, spec] of Object.entries(core.events)) {
37
- if (!/^tl[A-Z]/.test(name))
38
- fail(`event ${name} must be named tlSomething`);
39
- if (!spec.detail)
40
- fail(`event ${name} must state its detail type`);
41
- }
42
- for (const [name, spec] of Object.entries(core.props)) {
43
- if (spec.required && spec.default !== undefined)
44
- fail(`prop ${name} is required and has a default`);
45
- }
46
- return Object.freeze({ ...core });
47
- }
48
- const DEFAULT_LOCALE = 'en-GB';
49
- const DEFAULT_CURRENCY = 'GBP';
50
- /**
51
- * Instantiate a core. The instance owns the state and the timers; the harness
52
- * owns the pixels. `subscribe` fires after every change with the whole view, so
53
- * a harness only has to assign it to whatever makes its runtime re-render.
54
- */
55
- function createElement(core, options = {}) {
56
- const defaults = {};
57
- for (const [name, spec] of Object.entries(core.props)) {
58
- if (spec.default !== undefined)
59
- defaults[name] = spec.default;
60
- }
61
- let props = { ...defaults, ...(options.props ?? {}) };
62
- const strings = { ...core.strings, ...(options.strings ?? {}) };
63
- const locale = options.locale ?? DEFAULT_LOCALE;
64
- const fallbackCurrency = options.currency ?? DEFAULT_CURRENCY;
65
- let resolveClient = toResolver(options.client);
66
- let state = core.state(props);
67
- let destroyed = false;
68
- const timers = new Set();
69
- const listeners = new Set();
70
- const formatMoney = (amountInMinorUnits, currency) => new Intl.NumberFormat(locale, {
71
- style: 'currency',
72
- currency: currency || fallbackCurrency,
73
- }).format(amountInMinorUnits / 100);
74
- const viewContext = () => ({ props, state, strings, formatMoney });
75
- const buildView = () => {
76
- const ctx = viewContext();
77
- return {
78
- status: core.status(ctx),
79
- props,
80
- state,
81
- strings,
82
- derived: core.derive(ctx),
83
- };
84
- };
85
- const notify = () => {
86
- if (destroyed)
87
- return;
88
- const view = buildView();
89
- for (const listener of [...listeners])
90
- listener(view);
91
- };
92
- const actionContext = {
93
- get props() {
94
- return props;
95
- },
96
- get state() {
97
- return state;
98
- },
99
- strings,
100
- formatMoney,
101
- setState(patch) {
102
- if (destroyed)
103
- return;
104
- const next = typeof patch === 'function' ? patch(state) : patch;
105
- state = { ...state, ...next };
106
- notify();
107
- },
108
- emit(event, detail) {
109
- if (!core.events[event]) {
110
- throw new Error(`[elements-core] ${core.tag}: ${event} is not a declared event`);
111
- }
112
- options.emit?.(event, detail);
113
- },
114
- client() {
115
- const client = resolveClient();
116
- if (!client) {
117
- throw new Error(`[elements-core] ${core.tag}: no client. A harness hands the core the client the host published.`);
118
- }
119
- return client;
120
- },
121
- optionalClient() {
122
- try {
123
- return resolveClient();
124
- }
125
- catch {
126
- return null;
127
- }
128
- },
129
- after(ms, run) {
130
- if (destroyed)
131
- return;
132
- const handle = setTimeout(() => {
133
- timers.delete(handle);
134
- if (!destroyed)
135
- run();
136
- }, ms);
137
- timers.add(handle);
138
- },
139
- };
140
- const actions = core.actions(actionContext);
141
- return {
142
- core,
143
- get props() {
144
- return props;
145
- },
146
- get state() {
147
- return state;
148
- },
149
- get view() {
150
- return buildView();
151
- },
152
- actions,
153
- setProps(next) {
154
- if (destroyed)
155
- return;
156
- props = { ...props, ...next };
157
- notify();
158
- },
159
- setClient(client) {
160
- resolveClient = toResolver(client);
161
- },
162
- subscribe(listener) {
163
- listeners.add(listener);
164
- return () => {
165
- listeners.delete(listener);
166
- };
167
- },
168
- destroy() {
169
- destroyed = true;
170
- for (const handle of timers)
171
- clearTimeout(handle);
172
- timers.clear();
173
- listeners.clear();
174
- },
175
- };
176
- }
177
- function toResolver(client) {
178
- if (typeof client === 'function')
179
- return client;
180
- return () => client ?? null;
181
- }
182
-
183
- /**
184
- * `tl-ticket-selector`: the reference core.
185
- *
186
- * This is the whole of what the element does. The Stencil harness in
187
- * `packages/elements/src/components/tl-ticket-selector/` renders `view` and
188
- * forwards the three `tl*` events; it decides nothing, holds no copy and does
189
- * no arithmetic. A React Native harness will render the same view.
190
- *
191
- * Behaviour is the behaviour the Stencil component had before the split, to
192
- * the letter: the same quantity clamping, the same reset after a successful
193
- * add, the same three second success message, the same button text, and the
194
- * same rule that no fee row is shown until the surface quotes a fee. The one
195
- * thing that did not come across is the `console.log` tracing, which a library
196
- * should not do for its host.
197
- */
198
- const STRINGS = {
199
- selectTickets: 'Select Tickets',
200
- adding: 'Adding...',
201
- added: 'Added to cart!',
202
- limited: 'Limited',
203
- soldOut: 'Sold Out',
204
- ticketsRow: 'Tickets',
205
- totalRow: 'Total',
206
- decrease: 'Decrease quantity',
207
- increase: 'Increase quantity',
208
- loadFailed: 'Failed to load tickets',
209
- addFailed: 'Failed to add to cart',
210
- noClient: 'SDK not initialized.',
211
- };
212
- /** How long the "Added to cart!" message stays up. */
213
- const SUCCESS_MS = 3000;
214
- const totalsOf = (ctx) => selectionTotals(ctx.state.ticketTypes.map((tt) => ({ price: tt.price, quantity: ctx.state.quantities[tt.id] || 0 })), ctx.state.feeQuote);
215
- const totalItemsOf = (state) => Object.values(state.quantities).reduce((sum, qty) => sum + qty, 0);
216
- const ticketSelectorCore = defineElement({
217
- tag: 'tl-ticket-selector',
218
- docs: 'Pick quantities of the ticket types on sale for one occurrence, and add them to the cart.',
219
- category: 'purchase',
220
- runtimes: ['web', 'native'],
221
- tokens: [
222
- 'background',
223
- 'border',
224
- 'border-hover',
225
- 'error',
226
- 'error-bg',
227
- 'font-family',
228
- 'foreground',
229
- 'muted',
230
- 'muted-foreground',
231
- 'primary',
232
- 'primary-foreground',
233
- 'radius-md',
234
- 'radius-sm',
235
- 'spacing-1',
236
- 'spacing-2',
237
- 'spacing-3',
238
- 'spacing-4',
239
- 'success',
240
- 'warning',
241
- 'warning-bg',
242
- ],
243
- sdkMethods: ['cart.addItem', 'events.getTicketTypes'],
244
- props: {
245
- eventId: { type: 'string', attr: 'event-id', required: true, docs: 'Event ID' },
246
- occurrenceId: { type: 'string', attr: 'occurrence-id', required: true, docs: 'Occurrence ID' },
247
- showButton: { type: 'boolean', attr: 'show-button', default: true, docs: 'Show add to cart button' },
248
- buttonLabel: { type: 'string', attr: 'button-label', default: 'Add to Cart', docs: 'Button label' },
249
- },
250
- events: {
251
- tlAddToCart: {
252
- detail: '{ items: AddToCartItem[]; }',
253
- docs: 'Emitted when items are added to cart',
254
- },
255
- tlQuantityChange: {
256
- detail: '{ ticketTypeId: string; quantity: number; total: number; }',
257
- docs: 'Emitted when quantity changes',
258
- },
259
- tlError: {
260
- detail: '{ message: string; }',
261
- docs: 'Emitted on error',
262
- },
263
- },
264
- strings: STRINGS,
265
- statuses: ['loading', 'error', 'ready'],
266
- state: () => ({
267
- ticketTypes: [],
268
- quantities: {},
269
- loading: true,
270
- error: null,
271
- adding: false,
272
- addSuccess: false,
273
- feeQuote: null,
274
- }),
275
- status: ({ state }) => (state.loading ? 'loading' : state.error ? 'error' : 'ready'),
276
- derive: (ctx) => {
277
- const { state, strings, props, formatMoney } = ctx;
278
- const totals = totalsOf(ctx);
279
- const totalItems = totalItemsOf(state);
280
- const currency = state.ticketTypes[0]?.currency;
281
- const rows = state.ticketTypes.map((tt) => {
282
- const quantity = state.quantities[tt.id] || 0;
283
- const soldOut = tt.availabilityStatus === 'sold_out';
284
- return {
285
- id: tt.id,
286
- name: tt.name,
287
- description: tt.description ?? null,
288
- priceLabel: formatMoney(tt.price, tt.currency),
289
- quantity,
290
- soldOut,
291
- limited: tt.availabilityStatus === 'limited',
292
- canDecrease: !soldOut && quantity > 0,
293
- canIncrease: !soldOut && quantity < tt.maxPerOrder,
294
- };
295
- });
296
- return {
297
- rows,
298
- totals,
299
- totalItems,
300
- currency,
301
- subtotalLabel: formatMoney(totals.subtotal, currency),
302
- feeLabel: formatMoney(totals.fees, currency),
303
- totalLabel: formatMoney(totals.total, currency),
304
- showTotals: totals.showFee && totalItems > 0,
305
- actionLabel: state.adding
306
- ? strings.adding
307
- : totalItems === 0
308
- ? strings.selectTickets
309
- : `${props.buttonLabel} - ${formatMoney(totals.total)}`,
310
- actionDisabled: totalItems === 0 || state.adding,
311
- };
312
- },
313
- actions: (ctx) => ({
314
- /** Read the ticket types for the current event and occurrence. */
315
- async load() {
316
- try {
317
- ctx.setState({ loading: true, error: null });
318
- const types = (await ctx.client().getEventsManager().getTicketTypes(ctx.props.eventId, ctx.props.occurrenceId));
319
- const quantities = {};
320
- for (const type of types)
321
- quantities[type.id] = 0;
322
- ctx.setState({ ticketTypes: types, quantities });
323
- }
324
- catch (err) {
325
- const message = err instanceof Error ? err.message : ctx.strings.loadFailed;
326
- ctx.setState({ error: message });
327
- ctx.emit('tlError', { message });
328
- }
329
- finally {
330
- ctx.setState({ loading: false });
331
- }
332
- },
333
- /** Nudge one row's quantity, clamped to 0 and the type's maxPerOrder. */
334
- changeQuantity(ticketTypeId, delta) {
335
- const ticketType = ctx.state.ticketTypes.find((t) => t.id === ticketTypeId);
336
- if (!ticketType)
337
- return;
338
- const current = ctx.state.quantities[ticketTypeId] || 0;
339
- const quantity = Math.max(0, Math.min(ticketType.maxPerOrder, current + delta));
340
- ctx.setState((state) => ({ quantities: { ...state.quantities, [ticketTypeId]: quantity } }));
341
- // After setState, so the total is the one the buyer can now see.
342
- ctx.emit('tlQuantityChange', { ticketTypeId, quantity, total: totalsOf(ctx).total });
343
- },
344
- /**
345
- * Add every selected line to the cart, then clear the selection.
346
- *
347
- * With no client, or a client too old to carry a cart manager, the
348
- * selection is still cleared and `tlAddToCart` still fires: a host that
349
- * listens for the event and does its own cart keeps working.
350
- */
351
- async addToCart() {
352
- const items = Object.entries(ctx.state.quantities)
353
- .filter(([, quantity]) => quantity > 0)
354
- .map(([ticketTypeId, quantity]) => ({ ticketTypeId, quantity }));
355
- if (items.length === 0)
356
- return;
357
- ctx.setState({ adding: true, addSuccess: false });
358
- try {
359
- const cart = ctx.optionalClient()?.getCartManager?.();
360
- if (cart) {
361
- for (const item of items) {
362
- await cart.addItem(item.ticketTypeId, item.quantity, ctx.props.occurrenceId);
363
- }
364
- }
365
- const quantities = {};
366
- for (const type of ctx.state.ticketTypes)
367
- quantities[type.id] = 0;
368
- ctx.setState({ quantities, addSuccess: true });
369
- ctx.after(SUCCESS_MS, () => ctx.setState({ addSuccess: false }));
370
- ctx.emit('tlAddToCart', { items });
371
- }
372
- catch (err) {
373
- const message = err instanceof Error ? err.message : ctx.strings.addFailed;
374
- ctx.setState({ error: message });
375
- ctx.emit('tlError', { message });
376
- }
377
- finally {
378
- ctx.setState({ adding: false });
379
- }
380
- },
381
- /** What the surface quoted for this selection, when it ever quotes one. */
382
- setFeeQuote(quote) {
383
- ctx.setState({ feeQuote: quote });
384
- },
385
- /**
386
- * The host never produced a client. Shown, not emitted: the element has
387
- * nothing to say to a page that has not started Live yet.
388
- */
389
- connectFailed(message) {
390
- ctx.setState({ error: message || ctx.strings.noClient, loading: false });
391
- },
392
- }),
393
- });
3
+ import { c as createElement, h as occurrenceSelectorCore, t as ticketSelectorCore } from './ticket-selector-BwChnHVC.js';
394
4
 
395
5
  const tlOccurrenceSelectorCss = () => `:host{display:block;font-family:var(--tl-font-family);color:var(--tl-foreground)}*{box-sizing:border-box;margin:0;padding:0}.occurrence-selector{width:100%}.no-occurrences{text-align:center;padding:3rem 1rem;color:var(--tl-muted-foreground)}.list-view{display:flex;flex-direction:column;gap:0.75rem}.occurrence-item{display:flex;justify-content:space-between;align-items:flex-start;width:100%;padding:1rem 1.25rem;background:var(--tl-background);border:1px solid var(--tl-border);border-radius:var(--tl-radius);text-align:left;cursor:pointer;transition:all 0.15s ease;font-family:inherit}.occurrence-item:hover:not(:disabled){border-color:var(--tl-primary);background:color-mix(in srgb, var(--tl-primary) 5%, var(--tl-background))}.occurrence-item:focus-visible{outline:2px solid var(--tl-primary);outline-offset:2px}.occurrence-item:disabled{cursor:not-allowed;opacity:0.6;background:var(--tl-muted)}.occurrence-item.sold-out{opacity:0.6;background:var(--tl-muted)}.occurrence-info{flex:1}.occurrence-date{font-weight:600;font-size:0.9375rem;color:var(--tl-foreground);margin-bottom:0.25rem}.occurrence-time{font-size:0.875rem;color:var(--tl-muted-foreground)}.occurrence-status{flex-shrink:0;text-align:right}.status-sold-out{font-size:0.875rem;font-weight:500;color:var(--tl-muted-foreground)}.status-available{font-size:0.875rem;color:var(--tl-muted-foreground)}.status-arrow{font-size:1.25rem;color:var(--tl-primary)}.slots-view{display:flex;flex-direction:column;gap:1rem}.slots-date-header{font-size:1.125rem;font-weight:600;color:var(--tl-foreground);padding-bottom:0.5rem;border-bottom:1px solid var(--tl-border)}.time-slots-grid{display:grid;grid-template-columns:repeat(auto-fill, minmax(100px, 1fr));gap:0.5rem}.time-slot{display:flex;flex-direction:column;align-items:center;justify-content:center;padding:0.75rem 0.5rem;background:var(--tl-background);border:1px solid var(--tl-border);border-radius:var(--tl-radius);cursor:pointer;transition:all 0.15s ease;font-family:inherit;min-height:3.5rem}.time-slot:hover:not(:disabled){border-color:var(--tl-primary);background:color-mix(in srgb, var(--tl-primary) 5%, var(--tl-background))}.time-slot:focus-visible{outline:2px solid var(--tl-primary);outline-offset:2px}.time-slot:disabled{cursor:not-allowed;opacity:0.6;background:var(--tl-muted)}.time-slot.sold-out{opacity:0.6;background:var(--tl-muted)}.slot-time{font-size:0.9375rem;font-weight:600;color:var(--tl-foreground)}.slot-sold-out{font-size:0.6875rem;color:var(--tl-muted-foreground);margin-top:0.125rem}.calendar-view{display:flex;flex-direction:column;gap:1.5rem}.calendar-view.horizontal{flex-direction:row;gap:1.5rem;align-items:flex-start}.calendar-view.horizontal .calendar-panel{flex-shrink:0;width:340px}.calendar-view.horizontal .slots-panel{flex:1;min-width:0}.calendar-view.vertical .calendar-panel{width:100%}.calendar-view.vertical .slots-panel{width:100%}.calendar-panel{border:1px solid var(--tl-border);border-radius:var(--tl-radius);padding:1rem}.calendar-header{display:flex;align-items:center;justify-content:space-between;margin-bottom:0.75rem}.month-label{font-size:1rem;font-weight:600;color:var(--tl-foreground);flex:1}.calendar-nav-buttons{display:flex;gap:0.375rem}.nav-button{display:flex;align-items:center;justify-content:center;width:1.75rem;height:1.75rem;padding:0;background:var(--tl-background);border:1px solid var(--tl-border);border-radius:var(--tl-radius);cursor:pointer;transition:all 0.15s ease;color:var(--tl-foreground)}.nav-button:hover:not(:disabled){background:var(--tl-muted)}.nav-button:disabled{cursor:not-allowed;opacity:0.4}.nav-button svg{width:0.875rem;height:0.875rem}.calendar-grid{display:grid;grid-template-columns:repeat(7, 1fr);gap:0.1875rem}.weekday-header{text-align:center;font-size:0.6875rem;font-weight:600;color:var(--tl-muted-foreground);padding:0.375rem 0;text-transform:uppercase;letter-spacing:0.05em}.calendar-day{position:relative;display:flex;flex-direction:column;align-items:center;justify-content:center;aspect-ratio:1;padding:0.1875rem;background:var(--tl-background);border:1px solid transparent;border-radius:var(--tl-radius);cursor:default;transition:all 0.15s ease;font-family:inherit;font-size:0.9375rem;font-weight:500;color:var(--tl-foreground)}.calendar-day.other-month{color:var(--tl-muted-foreground);opacity:0.5}.calendar-day.has-occurrences{cursor:pointer;background:color-mix(in srgb, var(--tl-primary) 10%, var(--tl-background));border-color:transparent;font-weight:600;color:var(--tl-primary)}.calendar-day.has-occurrences:hover{background:color-mix(in srgb, var(--tl-primary) 18%, var(--tl-background));border-color:transparent}.calendar-day.has-occurrences.sold-out{background:var(--tl-muted);border-color:transparent}.calendar-day.has-occurrences.sold-out:hover{background:color-mix(in srgb, var(--tl-muted) 80%, var(--tl-background));border-color:transparent}.calendar-day.selected{background:var(--tl-primary);border-color:var(--tl-primary);color:var(--tl-primary-foreground)}.calendar-day.selected:hover{background:color-mix(in srgb, var(--tl-primary) 90%, black)}.calendar-day.selected .occurrence-dot{background:var(--tl-primary-foreground)}.calendar-day.today .day-number{font-weight:700;text-decoration:underline;text-underline-offset:2px}.calendar-day.past:not(.has-occurrences){opacity:0.4}.day-number{line-height:1}.occurrence-dot{display:none}.slots-panel{min-height:200px}.selected-date-slots{height:100%}.selected-date-slots.empty{display:flex;align-items:center;justify-content:center;color:var(--tl-muted-foreground);background:var(--tl-muted);border-radius:var(--tl-radius);padding:2rem 1rem;text-align:center;min-height:200px;font-size:0.875rem}.selected-date-header{font-size:0.9375rem;font-weight:600;color:var(--tl-foreground);padding-bottom:0.625rem;margin-bottom:0.625rem}.time-slots-list{display:flex;flex-direction:column;gap:0.5rem}.time-slot-item{display:flex;align-items:center;justify-content:space-between;width:100%;padding:0.625rem 0.875rem;background:var(--tl-background);border:1px solid var(--tl-border);border-radius:var(--tl-radius);text-align:left;cursor:pointer;transition:all 0.15s ease;font-family:inherit}.time-slot-item:hover:not(:disabled){border-color:var(--tl-primary);background:color-mix(in srgb, var(--tl-primary) 5%, var(--tl-background))}.time-slot-item:focus-visible{outline:2px solid var(--tl-primary);outline-offset:2px}.time-slot-item:disabled{cursor:not-allowed;opacity:0.6;background:var(--tl-muted)}.time-slot-item.sold-out{opacity:0.6;background:var(--tl-muted)}.time-slot-item .slot-time{font-size:0.9375rem;font-weight:600}.slot-status{flex-shrink:0}.slot-status .status-sold-out{font-size:0.75rem}.slot-status .status-available{font-size:0.75rem}.slot-status .status-arrow{font-size:1rem}@media (max-width: 640px){.event-name{font-size:1.25rem}.occurrence-date{font-size:0.875rem}.time-slots-grid{grid-template-columns:repeat(auto-fill, minmax(80px, 1fr))}.time-slot{padding:0.625rem 0.375rem;min-height:3rem}.slot-time{font-size:0.875rem}.calendar-day{font-size:0.75rem}.weekday-header{font-size:0.625rem}.calendar-view.horizontal .calendar-panel{width:100%}}@media (max-width: 767px){.calendar-view.horizontal{flex-direction:column}}.loading-container{padding:1rem 0;min-height:400px}@keyframes skeleton-pulse{0%,100%{opacity:1}50%{opacity:0.5}}.skeleton{background:var(--tl-muted);border-radius:var(--tl-radius);animation:skeleton-pulse 1.5s ease-in-out infinite}.skeleton-list{display:flex;flex-direction:column;gap:0.75rem}.skeleton-list-item{height:4.5rem;border-radius:var(--tl-radius)}.skeleton-calendar{border:1px solid var(--tl-border);border-radius:var(--tl-radius);padding:1rem;min-height:350px}.skeleton-calendar-header{height:1.75rem;width:50%;margin-bottom:0.75rem;border-radius:var(--tl-radius)}.skeleton-calendar-grid{display:grid;grid-template-columns:repeat(7, 1fr);gap:0.1875rem;margin-top:0.75rem}.skeleton-calendar-day{aspect-ratio:1;border-radius:var(--tl-radius)}.skeleton-slots{display:flex;flex-direction:column;gap:1rem}.skeleton-slots-header{height:1.75rem;width:60%;border-radius:var(--tl-radius);margin-bottom:0.5rem}.skeleton-slots-grid{display:grid;grid-template-columns:repeat(auto-fill, minmax(100px, 1fr));gap:0.5rem}.skeleton-slot-item{height:3.5rem;border-radius:var(--tl-radius)}.error-container{display:flex;flex-direction:column;align-items:center;justify-content:center;padding:3rem 1rem;gap:1rem;text-align:center}.error-icon{width:3rem;height:3rem;color:var(--tl-muted-foreground);stroke-width:1.5}.error-message{color:var(--tl-foreground);font-size:0.9375rem;max-width:400px}.retry-button{margin-top:0.5rem;padding:0.625rem 1.25rem;background:var(--tl-primary);color:var(--tl-primary-foreground);border:none;border-radius:var(--tl-radius);font-family:inherit;font-size:0.875rem;font-weight:500;cursor:pointer;transition:all 0.15s ease}.retry-button:hover{background:color-mix(in srgb, var(--tl-primary) 90%, black)}.retry-button:focus-visible{outline:2px solid var(--tl-primary);outline-offset:2px}`;
396
6
 
7
+ /** Theme field -> the custom property the stylesheet reads it through. */
8
+ const THEME_PROPERTIES = [
9
+ ['primaryColor', '--tl-primary'],
10
+ ['primaryForeground', '--tl-primary-foreground'],
11
+ ['backgroundColor', '--tl-background'],
12
+ ['foregroundColor', '--tl-foreground'],
13
+ ['mutedColor', '--tl-muted'],
14
+ ['mutedForeground', '--tl-muted-foreground'],
15
+ ['borderColor', '--tl-border'],
16
+ ['borderRadius', '--tl-radius'],
17
+ ['fontFamily', '--tl-font-family'],
18
+ ];
397
19
  const TlOccurrenceSelector = class {
398
20
  constructor(hostRef) {
399
21
  registerInstance(this, hostRef);
@@ -402,559 +24,155 @@ const TlOccurrenceSelector = class {
402
24
  * Layout orientation (auto = horizontal on desktop, vertical on mobile)
403
25
  */
404
26
  this.orientation = 'auto';
405
- /**
406
- * Loading state
407
- */
408
- this.loading = true;
409
- /**
410
- * Error message
411
- */
412
- this.error = null;
413
- /**
414
- * Fetched occurrences
415
- */
416
- this.occurrences = [];
417
- /**
418
- * Current month being displayed in calendar view
419
- */
420
- this.currentMonth = new Date();
421
- /**
422
- * Currently selected date in calendar view
423
- */
424
- this.selectedDate = null;
425
- /**
426
- * Computed display mode based on occurrences
427
- */
428
- this.computedDisplayMode = 'list';
429
- /**
430
- * Container width for responsive layout
431
- */
432
- this.containerWidth = 0;
27
+ this.element = createElement(occurrenceSelectorCore, {
28
+ client: () => getSDK(),
29
+ // What "the client is up" means on the web. The core waits on it inside its
30
+ // own mount and reads there; the harness decides none of it (TKT-197).
31
+ clientReady: () => whenSDK(),
32
+ emit: (event, detail) => this.forward(event, detail),
33
+ });
34
+ /** The core's whole view. Assigning it is what makes Stencil re-render. */
35
+ this.view = this.element.view;
36
+ }
37
+ /**
38
+ * A new event is a new subject: the element is torn down and mounted again,
39
+ * so whatever the core set up for the old one is undone before anything is
40
+ * set up for the new one.
41
+ */
42
+ watchSubject() {
43
+ this.syncProps();
44
+ this.teardown?.();
45
+ this.teardown = this.element.mount();
433
46
  }
434
- async componentWillLoad() {
435
- await this.waitForSDKAndFetch();
47
+ syncProps() {
48
+ this.element.setProps({
49
+ eventId: this.eventId,
50
+ displayMode: this.displayMode,
51
+ orientation: this.orientation,
52
+ theme: this.theme,
53
+ });
436
54
  }
437
55
  /**
438
- * Wait for SDK to be ready, then fetch occurrences
56
+ * On screen: subscribe to the view, then put the core to work.
57
+ *
58
+ * `connectedCallback` and not `componentWillLoad` because this runs again
59
+ * when the element is moved in the DOM, and that is exactly when a mount and
60
+ * its teardown have to pair up.
439
61
  */
440
- async waitForSDKAndFetch() {
441
- if (!this.eventId) {
442
- this.error = 'Event ID is required';
443
- this.loading = false;
444
- return;
445
- }
446
- try {
447
- await whenSDK();
448
- await this.fetchOccurrences();
449
- }
450
- catch (err) {
451
- this.error = err instanceof Error ? err.message : 'SDK not initialized.';
452
- this.loading = false;
453
- }
62
+ connectedCallback() {
63
+ this.syncProps();
64
+ this.unsubscribe ?? (this.unsubscribe = this.element.subscribe((view) => {
65
+ this.view = view;
66
+ }));
67
+ this.view = this.element.view;
68
+ this.teardown = this.element.mount();
454
69
  }
70
+ /** Measuring the element is the runtime's job; what a width means is not. */
455
71
  componentDidLoad() {
456
- // Set up resize observer for responsive layout
457
72
  if (this.containerRef && typeof ResizeObserver !== 'undefined') {
458
73
  this.resizeObserver = new ResizeObserver((entries) => {
459
- for (const entry of entries) {
460
- this.containerWidth = entry.contentRect.width;
461
- }
74
+ for (const entry of entries)
75
+ this.element.actions.resized(entry.contentRect.width);
462
76
  });
463
77
  this.resizeObserver.observe(this.containerRef);
464
78
  }
465
79
  }
466
- disconnectedCallback() {
467
- if (this.resizeObserver) {
468
- this.resizeObserver.disconnect();
469
- }
470
- }
471
- async onEventIdChange() {
472
- await this.fetchOccurrences();
473
- }
474
- /**
475
- * Fetch occurrences from the SDK
476
- */
477
- async fetchOccurrences() {
478
- this.loading = true;
479
- this.error = null;
480
- try {
481
- const sdk = getSDK();
482
- const eventsManager = sdk.getEventsManager();
483
- const event = await eventsManager.get(this.eventId);
484
- if (!event) {
485
- throw new Error('Event not found');
486
- }
487
- // Map EventOccurrence to TlOccurrence
488
- this.occurrences = (event.occurrences || []).map((occ) => ({
489
- id: occ.id,
490
- startsAt: occ.startsAt,
491
- endsAt: occ.endsAt,
492
- status: occ.availabilityStatus,
493
- availableCapacity: occ.availableCapacity,
494
- soldOut: occ.availabilityStatus === 'sold_out',
495
- }));
496
- this.computeDisplayMode();
497
- this.computeInitialSelection();
498
- }
499
- catch (err) {
500
- console.error('[tl-occurrence-selector] Failed to fetch occurrences:', err);
501
- this.error = err instanceof Error ? err.message : 'Failed to load occurrences';
502
- this.occurrences = [];
503
- }
504
- finally {
505
- this.loading = false;
506
- }
507
- }
508
- /**
509
- * Compute the display mode based on occurrence count and date distribution
510
- */
511
- computeDisplayMode() {
512
- if (this.displayMode) {
513
- this.computedDisplayMode = this.displayMode;
514
- return;
515
- }
516
- const count = this.occurrences.length;
517
- if (count <= 5) {
518
- this.computedDisplayMode = 'list';
519
- return;
520
- }
521
- // Check if all occurrences are on the same calendar date
522
- const uniqueDates = new Set();
523
- for (const occ of this.occurrences) {
524
- const date = new Date(occ.startsAt);
525
- uniqueDates.add(date.toDateString());
526
- }
527
- if (uniqueDates.size === 1) {
528
- this.computedDisplayMode = 'slots';
529
- }
530
- else {
531
- this.computedDisplayMode = 'calendar';
532
- }
533
- }
534
- /**
535
- * Compute the initial date selection for calendar/slots views
536
- */
537
- computeInitialSelection() {
538
- if (this.occurrences.length === 0)
539
- return;
540
- const today = new Date();
541
- today.setHours(0, 0, 0, 0);
542
- // Get all dates with occurrences
543
- const datesWithOccurrences = this.getDatesWithOccurrences();
544
- // Find the first date that is today or in the future
545
- let selectedDate = null;
546
- for (const dateStr of Array.from(datesWithOccurrences.keys()).sort()) {
547
- const date = new Date(dateStr);
548
- if (date >= today) {
549
- selectedDate = date;
550
- break;
551
- }
552
- }
553
- // If no future dates, select the last available date
554
- if (!selectedDate && datesWithOccurrences.size > 0) {
555
- const sortedDates = Array.from(datesWithOccurrences.keys()).sort();
556
- selectedDate = new Date(sortedDates[sortedDates.length - 1]);
557
- }
558
- if (selectedDate) {
559
- this.selectedDate = selectedDate;
560
- this.currentMonth = new Date(selectedDate.getFullYear(), selectedDate.getMonth(), 1);
561
- }
562
- }
563
- /**
564
- * Get a map of dates to occurrences
565
- */
566
- getDatesWithOccurrences() {
567
- const map = new Map();
568
- for (const occ of this.occurrences) {
569
- const date = new Date(occ.startsAt);
570
- date.setHours(0, 0, 0, 0);
571
- const dateStr = date.toDateString();
572
- if (!map.has(dateStr)) {
573
- map.set(dateStr, []);
574
- }
575
- map.get(dateStr).push(occ);
576
- }
577
- return map;
578
- }
579
- /**
580
- * Get the first and last occurrence dates
581
- */
582
- getDateRange() {
583
- if (this.occurrences.length === 0)
584
- return null;
585
- let first = new Date(this.occurrences[0].startsAt);
586
- let last = new Date(this.occurrences[0].startsAt);
587
- for (const occ of this.occurrences) {
588
- const date = new Date(occ.startsAt);
589
- if (date < first)
590
- first = date;
591
- if (date > last)
592
- last = date;
593
- }
594
- return { first, last };
595
- }
596
- /**
597
- * Check if all occurrences fit in one week (Monday-Sunday)
598
- */
599
- isSingleWeek() {
600
- const dateRange = this.getDateRange();
601
- if (!dateRange)
602
- return false;
603
- // Get Monday of the week containing the first date
604
- const firstMonday = new Date(dateRange.first);
605
- const dayOfWeek = firstMonday.getDay();
606
- const daysToSubtract = dayOfWeek === 0 ? 6 : dayOfWeek - 1; // Monday = 0 days
607
- firstMonday.setDate(firstMonday.getDate() - daysToSubtract);
608
- firstMonday.setHours(0, 0, 0, 0);
609
- // Get Sunday of that week
610
- const sunday = new Date(firstMonday);
611
- sunday.setDate(sunday.getDate() + 6);
612
- sunday.setHours(23, 59, 59, 999);
613
- // Check if last date is within this week
614
- return dateRange.last <= sunday;
615
- }
616
80
  /**
617
- * Check if all occurrences are in the same month
81
+ * Off screen: run the mount's teardown, and nothing more. Not `destroy()`:
82
+ * Stencil calls this for a DOM move as well as a removal, and a destroyed
83
+ * core never mounts again.
618
84
  */
619
- isSingleMonth() {
620
- const dateRange = this.getDateRange();
621
- if (!dateRange)
622
- return false;
623
- return (dateRange.first.getMonth() === dateRange.last.getMonth() &&
624
- dateRange.first.getFullYear() === dateRange.last.getFullYear());
625
- }
626
- /**
627
- * Check if we should use horizontal layout
628
- */
629
- isHorizontalLayout() {
630
- if (this.orientation === 'horizontal')
631
- return true;
632
- if (this.orientation === 'vertical')
633
- return false;
634
- // Auto: use horizontal if container is wide enough (768px)
635
- return this.containerWidth >= 768;
636
- }
637
- /**
638
- * Format a date for display
639
- */
640
- formatDate(date) {
641
- return date.toLocaleDateString('en-GB', {
642
- weekday: 'long',
643
- day: 'numeric',
644
- month: 'long',
645
- year: 'numeric',
646
- });
647
- }
648
- /**
649
- * Format a time for display
650
- */
651
- formatTime(dateString) {
652
- const date = new Date(dateString);
653
- return date.toLocaleTimeString('en-GB', {
654
- hour: '2-digit',
655
- minute: '2-digit',
656
- });
657
- }
658
- /**
659
- * Format a full date and time for list view
660
- */
661
- formatDateTime(dateString) {
662
- const date = new Date(dateString);
663
- return {
664
- date: date.toLocaleDateString('en-GB', {
665
- weekday: 'long',
666
- day: 'numeric',
667
- month: 'long',
668
- year: 'numeric',
669
- }),
670
- time: date.toLocaleTimeString('en-GB', {
671
- hour: '2-digit',
672
- minute: '2-digit',
673
- }),
674
- };
675
- }
676
- /**
677
- * Handle occurrence selection
678
- */
679
- handleSelect(occurrence) {
680
- if (occurrence.soldOut || occurrence.availableCapacity === 0)
681
- return;
682
- console.log('[tl-occurrence-selector] Selected:', occurrence.id);
683
- this.tlSelect.emit({ occurrence });
684
- }
685
- /**
686
- * Handle date selection in calendar view
687
- */
688
- handleDateSelect(date) {
689
- this.selectedDate = date;
690
- // If date is in a different month, navigate to that month
691
- if (date.getMonth() !== this.currentMonth.getMonth() ||
692
- date.getFullYear() !== this.currentMonth.getFullYear()) {
693
- this.currentMonth = new Date(date.getFullYear(), date.getMonth(), 1);
694
- }
695
- }
696
- /**
697
- * Navigate to previous month
698
- */
699
- handlePrevMonth() {
700
- const dateRange = this.getDateRange();
701
- if (!dateRange)
702
- return;
703
- const newMonth = new Date(this.currentMonth);
704
- newMonth.setMonth(newMonth.getMonth() - 1);
705
- // Don't go before the first occurrence month
706
- const firstMonth = new Date(dateRange.first.getFullYear(), dateRange.first.getMonth(), 1);
707
- if (newMonth >= firstMonth) {
708
- this.currentMonth = newMonth;
709
- }
710
- }
711
- /**
712
- * Navigate to next month
713
- */
714
- handleNextMonth() {
715
- const dateRange = this.getDateRange();
716
- if (!dateRange)
717
- return;
718
- const newMonth = new Date(this.currentMonth);
719
- newMonth.setMonth(newMonth.getMonth() + 1);
720
- // Don't go past the last occurrence month
721
- const lastMonth = new Date(dateRange.last.getFullYear(), dateRange.last.getMonth(), 1);
722
- if (newMonth <= lastMonth) {
723
- this.currentMonth = newMonth;
724
- }
725
- }
726
- /**
727
- * Check if prev month button should be disabled
728
- */
729
- isPrevMonthDisabled() {
730
- const dateRange = this.getDateRange();
731
- if (!dateRange)
732
- return true;
733
- const firstMonth = new Date(dateRange.first.getFullYear(), dateRange.first.getMonth(), 1);
734
- return this.currentMonth <= firstMonth;
735
- }
736
- /**
737
- * Check if next month button should be disabled
738
- */
739
- isNextMonthDisabled() {
740
- const dateRange = this.getDateRange();
741
- if (!dateRange)
742
- return true;
743
- const lastMonth = new Date(dateRange.last.getFullYear(), dateRange.last.getMonth(), 1);
744
- return this.currentMonth >= lastMonth;
745
- }
746
- /**
747
- * Get calendar grid days for the current month
748
- */
749
- getCalendarDays() {
750
- // If all occurrences fit in one week, only show that week
751
- if (this.isSingleWeek()) {
752
- const dateRange = this.getDateRange();
753
- if (!dateRange)
754
- return [];
755
- // Get Monday of the week containing the first date
756
- const firstMonday = new Date(dateRange.first);
757
- const dayOfWeek = firstMonday.getDay();
758
- const daysToSubtract = dayOfWeek === 0 ? 6 : dayOfWeek - 1;
759
- firstMonday.setDate(firstMonday.getDate() - daysToSubtract);
760
- firstMonday.setHours(0, 0, 0, 0);
761
- // Generate 7 days (Monday to Sunday)
762
- const days = [];
763
- const current = new Date(firstMonday);
764
- for (let i = 0; i < 7; i++) {
765
- days.push(new Date(current));
766
- current.setDate(current.getDate() + 1);
767
- }
768
- return days;
769
- }
770
- // Otherwise show full month grid
771
- const year = this.currentMonth.getFullYear();
772
- const month = this.currentMonth.getMonth();
773
- // Start of month
774
- const monthStart = new Date(year, month, 1);
775
- // End of month
776
- const monthEnd = new Date(year, month + 1, 0);
777
- // Get the start of the week containing the first day (Monday = 1)
778
- const startDay = monthStart.getDay();
779
- const daysToSubtract = startDay === 0 ? 6 : startDay - 1; // Monday = 0 days to subtract
780
- const calendarStart = new Date(monthStart);
781
- calendarStart.setDate(calendarStart.getDate() - daysToSubtract);
782
- // Get the end of the week containing the last day
783
- const endDay = monthEnd.getDay();
784
- const daysToAdd = endDay === 0 ? 0 : 7 - endDay;
785
- const calendarEnd = new Date(monthEnd);
786
- calendarEnd.setDate(calendarEnd.getDate() + daysToAdd);
787
- // Generate all days
788
- const days = [];
789
- const current = new Date(calendarStart);
790
- while (current <= calendarEnd) {
791
- days.push(new Date(current));
792
- current.setDate(current.getDate() + 1);
793
- }
794
- return days;
795
- }
796
- /**
797
- * Check if a date has occurrences
798
- */
799
- hasOccurrencesOnDate(date) {
800
- const dateStr = date.toDateString();
801
- return this.getDatesWithOccurrences().has(dateStr);
802
- }
803
- /**
804
- * Check if all occurrences on a date are sold out
805
- */
806
- isDateSoldOut(date) {
807
- const dateStr = date.toDateString();
808
- const occurrences = this.getDatesWithOccurrences().get(dateStr);
809
- if (!occurrences)
810
- return false;
811
- return occurrences.every(occ => occ.soldOut || occ.availableCapacity === 0);
85
+ disconnectedCallback() {
86
+ this.resizeObserver?.disconnect();
87
+ this.resizeObserver = undefined;
88
+ this.teardown?.();
89
+ this.teardown = undefined;
90
+ this.unsubscribe?.();
91
+ this.unsubscribe = undefined;
812
92
  }
813
- /**
814
- * Get occurrences for the selected date
815
- */
816
- getSelectedDateOccurrences() {
817
- if (!this.selectedDate)
818
- return [];
819
- const dateStr = this.selectedDate.toDateString();
820
- const occurrences = this.getDatesWithOccurrences().get(dateStr) || [];
821
- // Sort by start time
822
- return occurrences.sort((a, b) => new Date(a.startsAt).getTime() - new Date(b.startsAt).getTime());
93
+ forward(event, detail) {
94
+ if (event === 'tlSelect')
95
+ this.tlSelect.emit(detail);
823
96
  }
824
97
  /**
825
- * Get CSS custom properties from theme
98
+ * The host's theme, as custom properties over the ones the theme package
99
+ * ships. Web only: a custom property is not a fact every runtime has, so the
100
+ * core declares the prop and the harness is what acts on it.
826
101
  */
827
- getThemeStyles() {
828
- if (!this.theme)
829
- return {};
102
+ themeStyles() {
830
103
  const styles = {};
831
- if (this.theme.primaryColor) {
832
- styles['--tl-primary'] = this.theme.primaryColor;
833
- }
834
- if (this.theme.primaryForeground) {
835
- styles['--tl-primary-foreground'] = this.theme.primaryForeground;
836
- }
837
- if (this.theme.backgroundColor) {
838
- styles['--tl-background'] = this.theme.backgroundColor;
839
- }
840
- if (this.theme.foregroundColor) {
841
- styles['--tl-foreground'] = this.theme.foregroundColor;
842
- }
843
- if (this.theme.mutedColor) {
844
- styles['--tl-muted'] = this.theme.mutedColor;
845
- }
846
- if (this.theme.mutedForeground) {
847
- styles['--tl-muted-foreground'] = this.theme.mutedForeground;
848
- }
849
- if (this.theme.borderColor) {
850
- styles['--tl-border'] = this.theme.borderColor;
851
- }
852
- if (this.theme.borderRadius) {
853
- styles['--tl-radius'] = this.theme.borderRadius;
854
- }
855
- if (this.theme.fontFamily) {
856
- styles['--tl-font-family'] = this.theme.fontFamily;
104
+ if (!this.theme)
105
+ return styles;
106
+ for (const [field, property] of THEME_PROPERTIES) {
107
+ const value = this.theme[field];
108
+ if (value)
109
+ styles[property] = value;
857
110
  }
858
111
  return styles;
859
112
  }
860
113
  // =========================================================================
861
114
  // RENDER METHODS
862
115
  // =========================================================================
863
- /**
864
- * Render the list view (≤5 occurrences)
865
- */
116
+ /** The availability mark a row carries, in the list and the slots panel. */
117
+ renderRowStatus(row) {
118
+ const { strings } = this.view;
119
+ if (row.statusKind === 'soldOut')
120
+ return h("span", { class: "status-sold-out" }, strings.soldOut);
121
+ if (row.statusKind === 'available')
122
+ return h("span", { class: "status-available" }, row.capacityLabel);
123
+ return h("span", { class: "status-arrow" }, "\u2192");
124
+ }
125
+ /** The list view (five occurrences or fewer). */
866
126
  renderListView() {
867
- return (h("div", { class: "list-view" }, this.occurrences.map((occurrence) => {
868
- const isSoldOut = occurrence.soldOut || occurrence.availableCapacity === 0;
869
- const { date, time } = this.formatDateTime(occurrence.startsAt);
870
- return (h("button", { key: occurrence.id, class: `occurrence-item ${isSoldOut ? 'sold-out' : ''}`, onClick: () => this.handleSelect(occurrence), disabled: isSoldOut }, h("div", { class: "occurrence-info" }, h("div", { class: "occurrence-date" }, date), h("div", { class: "occurrence-time" }, time, occurrence.endsAt && ` - ${this.formatTime(occurrence.endsAt)}`)), h("div", { class: "occurrence-status" }, isSoldOut ? (h("span", { class: "status-sold-out" }, "Sold Out")) : occurrence.availableCapacity ? (h("span", { class: "status-available" }, occurrence.availableCapacity, " available")) : (h("span", { class: "status-arrow" }, "\u2192")))));
871
- })));
127
+ return (h("div", { class: "list-view" }, this.view.derived.rows.map((row) => (h("button", { key: row.id, class: `occurrence-item ${row.soldOut ? 'sold-out' : ''}`, onClick: () => this.element.actions.select(row.id), disabled: row.soldOut }, h("div", { class: "occurrence-info" }, h("div", { class: "occurrence-date" }, row.dateLabel), h("div", { class: "occurrence-time" }, row.timeLabel)), h("div", { class: "occurrence-status" }, this.renderRowStatus(row)))))));
872
128
  }
873
- /**
874
- * Render the time slots view (>5 occurrences, same day)
875
- */
129
+ /** The time slots view (more than five, all on one date). */
876
130
  renderSlotsView() {
877
- // Sort occurrences by time
878
- const sortedOccurrences = [...this.occurrences].sort((a, b) => new Date(a.startsAt).getTime() - new Date(b.startsAt).getTime());
879
- // Get the date from the first occurrence
880
- const dateLabel = sortedOccurrences.length > 0
881
- ? this.formatDate(new Date(sortedOccurrences[0].startsAt))
882
- : '';
883
- return (h("div", { class: "slots-view" }, dateLabel && h("div", { class: "slots-date-header" }, dateLabel), h("div", { class: "time-slots-grid" }, sortedOccurrences.map((occurrence) => {
884
- const isSoldOut = occurrence.soldOut || occurrence.availableCapacity === 0;
885
- return (h("button", { key: occurrence.id, class: `time-slot ${isSoldOut ? 'sold-out' : ''}`, onClick: () => this.handleSelect(occurrence), disabled: isSoldOut }, h("span", { class: "slot-time" }, this.formatTime(occurrence.startsAt)), isSoldOut && h("span", { class: "slot-sold-out" }, "Sold Out")));
886
- }))));
131
+ const { slots, slotsDateLabel } = this.view.derived;
132
+ return (h("div", { class: "slots-view" }, slotsDateLabel && h("div", { class: "slots-date-header" }, slotsDateLabel), h("div", { class: "time-slots-grid" }, slots.map((row) => (h("button", { key: row.id, class: `time-slot ${row.soldOut ? 'sold-out' : ''}`, onClick: () => this.element.actions.select(row.id), disabled: row.soldOut }, h("span", { class: "slot-time" }, row.startLabel), row.soldOut && h("span", { class: "slot-sold-out" }, this.view.strings.soldOut)))))));
887
133
  }
888
- /**
889
- * Render the calendar header
890
- */
891
134
  renderCalendarHeader() {
892
- const monthLabel = this.currentMonth.toLocaleDateString('en-GB', {
893
- month: 'long',
894
- year: 'numeric',
895
- });
896
- const showNavigation = !this.isSingleMonth();
897
- return (h("div", { class: "calendar-header" }, h("span", { class: "month-label" }, monthLabel), showNavigation && (h("div", { class: "calendar-nav-buttons" }, h("button", { class: "nav-button", onClick: () => this.handlePrevMonth(), disabled: this.isPrevMonthDisabled(), "aria-label": "Previous month" }, h("svg", { viewBox: "0 0 24 24", fill: "none", stroke: "currentColor", "stroke-width": "2" }, h("path", { d: "M15 18l-6-6 6-6" }))), h("button", { class: "nav-button", onClick: () => this.handleNextMonth(), disabled: this.isNextMonthDisabled(), "aria-label": "Next month" }, h("svg", { viewBox: "0 0 24 24", fill: "none", stroke: "currentColor", "stroke-width": "2" }, h("path", { d: "M9 18l6-6-6-6" })))))));
135
+ const { strings, derived } = this.view;
136
+ return (h("div", { class: "calendar-header" }, h("span", { class: "month-label" }, derived.monthLabel), derived.showMonthNav && (h("div", { class: "calendar-nav-buttons" }, h("button", { class: "nav-button", onClick: () => this.element.actions.previousMonth(), disabled: !derived.canGoBack, "aria-label": strings.previousMonth }, h("svg", { viewBox: "0 0 24 24", fill: "none", stroke: "currentColor", "stroke-width": "2" }, h("path", { d: "M15 18l-6-6 6-6" }))), h("button", { class: "nav-button", onClick: () => this.element.actions.nextMonth(), disabled: !derived.canGoForward, "aria-label": strings.nextMonth }, h("svg", { viewBox: "0 0 24 24", fill: "none", stroke: "currentColor", "stroke-width": "2" }, h("path", { d: "M9 18l6-6-6-6" })))))));
137
+ }
138
+ renderCalendarDay(day) {
139
+ return (h("button", { key: day.key, class: day.className, onClick: () => this.element.actions.selectDate(day.key), disabled: !day.selectable }, h("span", { class: "day-number" }, day.dayNumber), day.hasOccurrences && h("span", { class: "occurrence-dot" })));
898
140
  }
899
- /**
900
- * Render the calendar grid
901
- */
902
141
  renderCalendarGrid() {
903
- const days = this.getCalendarDays();
904
- const weekDays = ['Mon', 'Tue', 'Wed', 'Thu', 'Fri', 'Sat', 'Sun'];
905
- const today = new Date();
906
- today.setHours(0, 0, 0, 0);
907
- return (h("div", { class: "calendar-grid" }, weekDays.map((day) => (h("div", { class: "weekday-header", key: day }, day))), days.map((date) => {
908
- const isCurrentMonth = date.getMonth() === this.currentMonth.getMonth();
909
- const hasOccurrences = this.hasOccurrencesOnDate(date);
910
- const isSoldOut = this.isDateSoldOut(date);
911
- const isSelected = this.selectedDate && date.toDateString() === this.selectedDate.toDateString();
912
- const isToday = date.toDateString() === today.toDateString();
913
- const isPast = date < today;
914
- return (h("button", { key: date.toISOString(), class: `calendar-day
915
- ${!isCurrentMonth ? 'other-month' : ''}
916
- ${hasOccurrences ? 'has-occurrences' : ''}
917
- ${isSoldOut ? 'sold-out' : ''}
918
- ${isSelected ? 'selected' : ''}
919
- ${isToday ? 'today' : ''}
920
- ${isPast ? 'past' : ''}`, onClick: () => hasOccurrences && this.handleDateSelect(date), disabled: !hasOccurrences }, h("span", { class: "day-number" }, date.getDate()), hasOccurrences && h("span", { class: "occurrence-dot" })));
921
- })));
142
+ const { derived } = this.view;
143
+ return (h("div", { class: "calendar-grid" }, derived.weekdayLabels.map((day) => (h("div", { class: "weekday-header", key: day }, day))), derived.days.map((day) => this.renderCalendarDay(day))));
922
144
  }
923
- /**
924
- * Render the selected date's time slots
925
- */
145
+ /** The picked date's times, beside or below the calendar. */
926
146
  renderSelectedDateSlots() {
927
- const occurrences = this.getSelectedDateOccurrences();
928
- if (!this.selectedDate || occurrences.length === 0) {
929
- return (h("div", { class: "selected-date-slots empty" }, h("p", null, "Select a date to see available times")));
147
+ const { strings, derived } = this.view;
148
+ if (!derived.selectedDateLabel) {
149
+ return (h("div", { class: "selected-date-slots empty" }, h("p", null, strings.selectADate)));
930
150
  }
931
- const dateLabel = this.formatDate(this.selectedDate);
932
- return (h("div", { class: "selected-date-slots" }, h("div", { class: "selected-date-header" }, dateLabel), h("div", { class: "time-slots-list" }, occurrences.map((occurrence) => {
933
- const isSoldOut = occurrence.soldOut || occurrence.availableCapacity === 0;
934
- return (h("button", { key: occurrence.id, class: `time-slot-item ${isSoldOut ? 'sold-out' : ''}`, onClick: () => this.handleSelect(occurrence), disabled: isSoldOut }, h("span", { class: "slot-time" }, this.formatTime(occurrence.startsAt)), h("div", { class: "slot-status" }, isSoldOut ? (h("span", { class: "status-sold-out" }, "Sold Out")) : occurrence.availableCapacity ? (h("span", { class: "status-available" }, occurrence.availableCapacity, " available")) : (h("span", { class: "status-arrow" }, "\u2192")))));
935
- }))));
151
+ return (h("div", { class: "selected-date-slots" }, h("div", { class: "selected-date-header" }, derived.selectedDateLabel), h("div", { class: "time-slots-list" }, derived.selectedSlots.map((row) => (h("button", { key: row.id, class: `time-slot-item ${row.soldOut ? 'sold-out' : ''}`, onClick: () => this.element.actions.select(row.id), disabled: row.soldOut }, h("span", { class: "slot-time" }, row.startLabel), h("div", { class: "slot-status" }, this.renderRowStatus(row))))))));
936
152
  }
937
- /**
938
- * Render the calendar view with slots panel
939
- */
940
153
  renderCalendarView() {
941
- const isHorizontal = this.isHorizontalLayout();
942
- return (h("div", { class: `calendar-view ${isHorizontal ? 'horizontal' : 'vertical'}` }, h("div", { class: "calendar-panel" }, this.renderCalendarHeader(), this.renderCalendarGrid()), h("div", { class: "slots-panel" }, this.renderSelectedDateSlots())));
154
+ return (h("div", { class: this.view.derived.viewClass }, h("div", { class: "calendar-panel" }, this.renderCalendarHeader(), this.renderCalendarGrid()), h("div", { class: "slots-panel" }, this.renderSelectedDateSlots())));
943
155
  }
944
- /**
945
- * Render skeleton loading state
946
- */
156
+ /** The skeleton, which is a calendar because that is the common shape. */
947
157
  renderSkeleton() {
948
- // Default to calendar skeleton as it's the most common
949
158
  return (h("div", { class: "loading-container" }, h("div", { class: "skeleton-calendar" }, h("div", { class: "skeleton skeleton-calendar-header" }), h("div", { class: "skeleton-calendar-grid" }, Array.from({ length: 35 }, (_, i) => (h("div", { key: i, class: "skeleton skeleton-calendar-day" })))))));
950
159
  }
951
160
  render() {
952
- const themeStyles = this.getThemeStyles();
953
- return (h("div", { key: '40758aaa06e5f062de92675766e4531995f74bc7', class: "occurrence-selector", ref: (el) => (this.containerRef = el), style: themeStyles }, this.loading && this.renderSkeleton(), !this.loading && this.error && (h("div", { key: '6d2f5b9c07f65d4941bfb4870729fe3248cb277e', class: "error-container" }, h("svg", { key: 'daaf1f48dcff80ae7cf94ae0da1d66d9c9b7fb0f', class: "error-icon", viewBox: "0 0 24 24", fill: "none", stroke: "currentColor", "stroke-width": "2" }, h("circle", { key: '0a9f494a5cf7f79146e56fc44469a3ca9e5238ae', cx: "12", cy: "12", r: "10" }), h("path", { key: '67d7b453508c847593136bd60e34ea0c38d30cda', d: "M12 8v4m0 4h.01" })), h("p", { key: 'c1cc92585ed4451b686350b6d718c3ba06b72dd3', class: "error-message" }, this.error), h("button", { key: '4ec9d048af704af12a800385b9868501973ba477', class: "retry-button", onClick: () => this.fetchOccurrences() }, "Retry"))), !this.loading && !this.error && (this.occurrences.length === 0 ? (h("div", { class: "no-occurrences" }, h("p", null, "No dates available for this event."))) : (h("div", { class: "selector-content" }, this.computedDisplayMode === 'list' && this.renderListView(), this.computedDisplayMode === 'slots' && this.renderSlotsView(), this.computedDisplayMode === 'calendar' && this.renderCalendarView())))));
161
+ const { status, strings, state, derived } = this.view;
162
+ return (h("div", { key: '84951fb5171ed6c79604861a171f49f43c8b809c', class: "occurrence-selector", ref: (el) => (this.containerRef = el), style: this.themeStyles() }, status === 'loading' && this.renderSkeleton(), status === 'error' && (h("div", { key: '2850712eae277f32f3538426c8a5d36b73ca918e', class: "error-container" }, h("svg", { key: '89192cd7cce29781b87c83c8dee71d59f6a685ed', class: "error-icon", viewBox: "0 0 24 24", fill: "none", stroke: "currentColor", "stroke-width": "2" }, h("circle", { key: '27a52ecb1228c541202752be84323abe687c1655', cx: "12", cy: "12", r: "10" }), h("path", { key: 'f51c2b49de89b9c66cde01e11a2de4c56d4dbad9', d: "M12 8v4m0 4h.01" })), h("p", { key: '3c85d762de430f477d2c9ad4341b23765c93e2e3', class: "error-message" }, state.error), h("button", { key: '22fa271a8f0a15e18141cf6d66a25a2bf9299c72', class: "retry-button", onClick: () => void this.element.actions.load() }, strings.retry))), status === 'empty' && (h("div", { key: '285403135ccff6cd6c8de6a9c62c9186c67c102c', class: "no-occurrences" }, h("p", { key: '98373f0c5793cb1aba1328558fac3210e7c6aeeb' }, strings.empty))), status === 'ready' && (h("div", { key: 'a61d2c02538f3339e54770e53a4f807e72182357', class: "selector-content" }, derived.mode === 'list' && this.renderListView(), derived.mode === 'slots' && this.renderSlotsView(), derived.mode === 'calendar' && this.renderCalendarView()))));
954
163
  }
955
164
  static get watchers() { return {
956
165
  "eventId": [{
957
- "onEventIdChange": 0
166
+ "watchSubject": 0
167
+ }],
168
+ "displayMode": [{
169
+ "syncProps": 0
170
+ }],
171
+ "orientation": [{
172
+ "syncProps": 0
173
+ }],
174
+ "theme": [{
175
+ "syncProps": 0
958
176
  }]
959
177
  }; }
960
178
  };
@@ -978,6 +196,9 @@ const TlTicketSelector = class {
978
196
  this.buttonLabel = 'Add to Cart';
979
197
  this.element = createElement(ticketSelectorCore, {
980
198
  client: () => getSDK(),
199
+ // What "the client is up" means on the web. The core waits on it in its
200
+ // own mount; the harness no longer decides when to load (TKT-197).
201
+ clientReady: () => whenSDK(),
981
202
  emit: (event, detail) => this.forward(event, detail),
982
203
  });
983
204
  /** The core's whole view. Assigning it is what makes Stencil re-render. */
@@ -995,9 +216,14 @@ const TlTicketSelector = class {
995
216
  set feeQuote(quote) {
996
217
  this.element.actions.setFeeQuote(quote);
997
218
  }
219
+ /**
220
+ * A new event or occurrence is a new subject: the element is torn down and
221
+ * mounted again, so whatever the core set up for the old one is undone
222
+ * before anything is set up for the new one.
223
+ */
998
224
  watchPropsChange() {
999
225
  this.syncProps();
1000
- this.element.actions.load();
226
+ this.remount();
1001
227
  }
1002
228
  syncProps() {
1003
229
  this.element.setProps({
@@ -1007,32 +233,38 @@ const TlTicketSelector = class {
1007
233
  buttonLabel: this.buttonLabel,
1008
234
  });
1009
235
  }
1010
- async componentWillLoad() {
236
+ /**
237
+ * On screen: render what the core says and put the core to work. What "to
238
+ * work" means is the core's own business - for this element, waiting for the
239
+ * client and reading the ticket types.
240
+ *
241
+ * `connectedCallback` and not `componentWillLoad` because this runs again
242
+ * when the element is moved in the DOM, and that is exactly when a mount and
243
+ * its teardown have to pair up.
244
+ */
245
+ connectedCallback() {
1011
246
  this.syncProps();
1012
- this.unsubscribe = this.element.subscribe((view) => {
247
+ this.unsubscribe ?? (this.unsubscribe = this.element.subscribe((view) => {
1013
248
  this.view = view;
1014
- });
249
+ }));
1015
250
  this.view = this.element.view;
1016
- await this.waitForSDKAndLoad();
251
+ this.teardown = this.element.mount();
1017
252
  }
253
+ /**
254
+ * Off screen: run the mount's teardown, and nothing more. `destroy()` is
255
+ * terminal and Stencil calls this for a move as well as a removal, so
256
+ * destroying here would leave a re-inserted element frozen. The instance
257
+ * holds no listener and no subscription once these two have run.
258
+ */
1018
259
  disconnectedCallback() {
260
+ this.teardown?.();
261
+ this.teardown = undefined;
1019
262
  this.unsubscribe?.();
1020
- this.element.destroy();
263
+ this.unsubscribe = undefined;
1021
264
  }
1022
- /**
1023
- * Wait for SDK to be ready, then load ticket types
1024
- */
1025
- async waitForSDKAndLoad() {
1026
- if (!this.eventId || !this.occurrenceId) {
1027
- return;
1028
- }
1029
- try {
1030
- await whenSDK();
1031
- await this.element.actions.load();
1032
- }
1033
- catch (err) {
1034
- this.element.actions.connectFailed(err instanceof Error ? err.message : this.view.strings.noClient);
1035
- }
265
+ remount() {
266
+ this.teardown?.();
267
+ this.teardown = this.element.mount();
1036
268
  }
1037
269
  forward(event, detail) {
1038
270
  if (event === 'tlAddToCart')