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