@ticketlayer/elements 0.3.3 → 1.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (179) hide show
  1. package/README.md +3 -2
  2. package/dist/cjs/{index-BpfoKd9Q.js → index-DanPUX0s.js} +1 -1
  3. package/dist/cjs/index.cjs.js +2 -1
  4. package/dist/cjs/loader.cjs.js +2 -2
  5. package/dist/cjs/ticket-selector-BYVL13k_.js +3623 -0
  6. package/dist/cjs/ticketlayer-elements.cjs.js +2 -2
  7. package/dist/cjs/tl-buy-modal.cjs.entry.js +4 -4
  8. package/dist/cjs/tl-buy-tickets-button.cjs.entry.js +89 -71
  9. package/dist/cjs/tl-buy-tickets-wrapper.cjs.entry.js +62 -31
  10. package/dist/cjs/tl-cart-badge.cjs.entry.js +75 -62
  11. package/dist/cjs/tl-cart-drawer.cjs.entry.js +52 -24
  12. package/dist/cjs/tl-cart.cjs.entry.js +72 -98
  13. package/dist/cjs/tl-checkout-wrapper.cjs.entry.js +52 -30
  14. package/dist/cjs/tl-event-card.cjs.entry.js +44 -105
  15. package/dist/cjs/tl-event-list.cjs.entry.js +73 -36
  16. package/dist/cjs/tl-login.cjs.entry.js +75 -141
  17. package/dist/cjs/tl-my-orders.cjs.entry.js +53 -92
  18. package/dist/cjs/tl-occurrence-selector_2.cjs.entry.js +205 -641
  19. package/dist/cjs/tl-order-confirmation.cjs.entry.js +72 -105
  20. package/dist/cjs/tl-order-tickets_2.cjs.entry.js +50 -76
  21. package/dist/cjs/tl-promo-code.cjs.entry.js +51 -47
  22. package/dist/cjs/tl-seat-map.cjs.entry.js +4 -4
  23. package/dist/cjs/web-query-Ce73jB4k.js +16 -0
  24. package/dist/collection/components/tl-buy-modal/tl-buy-modal.css +12 -19
  25. package/dist/collection/components/tl-buy-modal/tl-buy-modal.js +2 -2
  26. package/dist/collection/components/tl-buy-tickets-button/tl-buy-tickets-button.css +41 -4
  27. package/dist/collection/components/tl-buy-tickets-button/tl-buy-tickets-button.js +105 -76
  28. package/dist/collection/components/tl-buy-tickets-wrapper/tl-buy-tickets-wrapper.css +2 -2
  29. package/dist/collection/components/tl-buy-tickets-wrapper/tl-buy-tickets-wrapper.js +73 -30
  30. package/dist/collection/components/tl-cart/tl-cart.css +52 -51
  31. package/dist/collection/components/tl-cart/tl-cart.js +82 -101
  32. package/dist/collection/components/tl-cart-badge/tl-cart-badge.css +11 -11
  33. package/dist/collection/components/tl-cart-badge/tl-cart-badge.js +62 -62
  34. package/dist/collection/components/tl-cart-drawer/tl-cart-drawer.css +6 -6
  35. package/dist/collection/components/tl-cart-drawer/tl-cart-drawer.js +56 -24
  36. package/dist/collection/components/tl-checkout-wrapper/tl-checkout-wrapper.css +2 -2
  37. package/dist/collection/components/tl-checkout-wrapper/tl-checkout-wrapper.js +61 -29
  38. package/dist/collection/components/tl-event-card/event-card-adapter.js +11 -23
  39. package/dist/collection/components/tl-event-card/tl-event-card.css +22 -22
  40. package/dist/collection/components/tl-event-card/tl-event-card.js +51 -84
  41. package/dist/collection/components/tl-event-list/tl-event-list.css +17 -17
  42. package/dist/collection/components/tl-event-list/tl-event-list.js +82 -38
  43. package/dist/collection/components/tl-login/tl-login.css +26 -26
  44. package/dist/collection/components/tl-login/tl-login.js +83 -147
  45. package/dist/collection/components/tl-my-orders/tl-my-orders.css +40 -40
  46. package/dist/collection/components/tl-my-orders/tl-my-orders.js +58 -96
  47. package/dist/collection/components/tl-occurrence-selector/tl-occurrence-selector.css +54 -65
  48. package/dist/collection/components/tl-occurrence-selector/tl-occurrence-selector.js +137 -516
  49. package/dist/collection/components/tl-order-confirmation/tl-order-confirmation.css +38 -38
  50. package/dist/collection/components/tl-order-confirmation/tl-order-confirmation.js +81 -113
  51. package/dist/collection/components/tl-order-tickets/tl-order-tickets.css +9 -9
  52. package/dist/collection/components/tl-order-tickets/tl-order-tickets.js +57 -92
  53. package/dist/collection/components/tl-promo-code/tl-promo-code.css +25 -25
  54. package/dist/collection/components/tl-promo-code/tl-promo-code.js +55 -50
  55. package/dist/collection/components/tl-seat-map/tl-seat-map.css +10 -10
  56. package/dist/collection/components/tl-seat-map/tl-seat-map.js +2 -2
  57. package/dist/collection/components/tl-ticket/tl-ticket.css +13 -13
  58. package/dist/collection/components/tl-ticket-selector/tl-ticket-selector.css +74 -49
  59. package/dist/collection/components/tl-ticket-selector/tl-ticket-selector.js +95 -135
  60. package/dist/collection/utils/cart-totals.js +9 -0
  61. package/dist/collection/utils/order-summary.js +15 -99
  62. package/dist/collection/utils/testing/deep-text.js +29 -0
  63. package/dist/collection/utils/testing/fixtures.js +290 -0
  64. package/dist/collection/utils/testing/mock-sdk.js +13 -2
  65. package/dist/collection/utils/web-query.js +12 -0
  66. package/dist/collection/utils/web-storage.js +22 -0
  67. package/dist/components/ticket-selector.js +1 -0
  68. package/dist/components/tl-buy-modal.js +1 -1
  69. package/dist/components/tl-buy-tickets-button.js +1 -1
  70. package/dist/components/tl-buy-tickets-wrapper2.js +1 -1
  71. package/dist/components/tl-cart-badge.js +1 -1
  72. package/dist/components/tl-cart-drawer.js +1 -1
  73. package/dist/components/tl-cart2.js +1 -1
  74. package/dist/components/tl-checkout-wrapper2.js +1 -1
  75. package/dist/components/tl-event-card2.js +1 -1
  76. package/dist/components/tl-event-list.js +1 -1
  77. package/dist/components/tl-login2.js +1 -1
  78. package/dist/components/tl-my-orders.js +1 -1
  79. package/dist/components/tl-occurrence-selector2.js +1 -1
  80. package/dist/components/tl-order-confirmation.js +1 -1
  81. package/dist/components/tl-order-tickets2.js +1 -1
  82. package/dist/components/tl-promo-code.js +1 -1
  83. package/dist/components/tl-seat-map.js +1 -1
  84. package/dist/components/tl-ticket-selector2.js +1 -1
  85. package/dist/components/tl-ticket2.js +1 -1
  86. package/dist/components/web-query.js +1 -0
  87. package/dist/elements.docs.json +36 -36
  88. package/dist/esm/{index-CNX8hZa8.js → index-imfxp765.js} +1 -1
  89. package/dist/esm/index.js +2 -1
  90. package/dist/esm/loader.js +3 -3
  91. package/dist/esm/ticket-selector-BwChnHVC.js +3606 -0
  92. package/dist/esm/ticketlayer-elements.js +3 -3
  93. package/dist/esm/tl-buy-modal.entry.js +4 -4
  94. package/dist/esm/tl-buy-tickets-button.entry.js +90 -72
  95. package/dist/esm/tl-buy-tickets-wrapper.entry.js +63 -32
  96. package/dist/esm/tl-cart-badge.entry.js +76 -63
  97. package/dist/esm/tl-cart-drawer.entry.js +53 -25
  98. package/dist/esm/tl-cart.entry.js +73 -99
  99. package/dist/esm/tl-checkout-wrapper.entry.js +53 -31
  100. package/dist/esm/tl-event-card.entry.js +45 -106
  101. package/dist/esm/tl-event-list.entry.js +74 -37
  102. package/dist/esm/tl-login.entry.js +76 -142
  103. package/dist/esm/tl-my-orders.entry.js +54 -93
  104. package/dist/esm/tl-occurrence-selector_2.entry.js +206 -642
  105. package/dist/esm/tl-order-confirmation.entry.js +73 -106
  106. package/dist/esm/tl-order-tickets_2.entry.js +51 -77
  107. package/dist/esm/tl-promo-code.entry.js +52 -48
  108. package/dist/esm/tl-seat-map.entry.js +4 -4
  109. package/dist/esm/web-query-Bdbb2lk5.js +14 -0
  110. package/dist/ticketlayer-elements/index.esm.js +1 -1
  111. package/dist/ticketlayer-elements/p-45923d2c.entry.js +1 -0
  112. package/dist/ticketlayer-elements/p-492adc77.entry.js +1 -0
  113. package/dist/ticketlayer-elements/p-4cfc9e53.entry.js +1 -0
  114. package/dist/ticketlayer-elements/p-5135630e.entry.js +1 -0
  115. package/dist/ticketlayer-elements/p-5bbce3b5.entry.js +1 -0
  116. package/dist/ticketlayer-elements/p-7d547721.entry.js +1 -0
  117. package/dist/ticketlayer-elements/p-7fe7b6ba.entry.js +1 -0
  118. package/dist/ticketlayer-elements/p-8122f0c1.entry.js +1 -0
  119. package/dist/ticketlayer-elements/p-98d40869.entry.js +1 -0
  120. package/dist/ticketlayer-elements/p-Bdbb2lk5.js +1 -0
  121. package/dist/ticketlayer-elements/p-BwChnHVC.js +1 -0
  122. package/dist/ticketlayer-elements/p-abec290a.entry.js +1 -0
  123. package/dist/ticketlayer-elements/p-ac79a3f6.entry.js +1 -0
  124. package/dist/ticketlayer-elements/p-d01f4429.entry.js +1 -0
  125. package/dist/ticketlayer-elements/p-d7ffaf46.entry.js +1 -0
  126. package/dist/ticketlayer-elements/p-e7170f3c.entry.js +1 -0
  127. package/dist/ticketlayer-elements/p-e787e862.entry.js +1 -0
  128. package/dist/ticketlayer-elements/p-f1ff931f.entry.js +1 -0
  129. package/dist/ticketlayer-elements/p-imfxp765.js +3 -0
  130. package/dist/ticketlayer-elements/ticketlayer-elements.css +1 -1
  131. package/dist/ticketlayer-elements/ticketlayer-elements.esm.js +1 -1
  132. package/dist/types/components/tl-buy-tickets-button/tl-buy-tickets-button.d.ts +40 -8
  133. package/dist/types/components/tl-buy-tickets-wrapper/tl-buy-tickets-wrapper.d.ts +20 -1
  134. package/dist/types/components/tl-cart/tl-cart.d.ts +40 -32
  135. package/dist/types/components/tl-cart-badge/tl-cart-badge.d.ts +22 -7
  136. package/dist/types/components/tl-cart-drawer/tl-cart-drawer.d.ts +28 -5
  137. package/dist/types/components/tl-checkout-wrapper/tl-checkout-wrapper.d.ts +18 -1
  138. package/dist/types/components/tl-event-card/event-card-adapter.d.ts +9 -20
  139. package/dist/types/components/tl-event-card/tl-event-card.d.ts +25 -28
  140. package/dist/types/components/tl-event-list/tl-event-list.d.ts +46 -6
  141. package/dist/types/components/tl-login/tl-login.d.ts +68 -22
  142. package/dist/types/components/tl-my-orders/tl-my-orders.d.ts +30 -12
  143. package/dist/types/components/tl-occurrence-selector/tl-occurrence-selector.d.ts +73 -161
  144. package/dist/types/components/tl-order-confirmation/tl-order-confirmation.d.ts +48 -15
  145. package/dist/types/components/tl-order-tickets/tl-order-tickets.d.ts +29 -31
  146. package/dist/types/components/tl-promo-code/tl-promo-code.d.ts +28 -10
  147. package/dist/types/components/tl-ticket/tl-ticket.d.ts +2 -11
  148. package/dist/types/components/tl-ticket-selector/tl-ticket-selector.d.ts +61 -32
  149. package/dist/types/components.d.ts +302 -62
  150. package/dist/types/utils/cart-totals.d.ts +10 -0
  151. package/dist/types/utils/order-summary.d.ts +16 -65
  152. package/dist/types/utils/testing/deep-text.d.ts +13 -0
  153. package/dist/types/utils/testing/fixtures.d.ts +225 -0
  154. package/dist/types/utils/testing/mock-sdk.d.ts +14 -1
  155. package/dist/types/utils/web-query.d.ts +18 -0
  156. package/dist/types/utils/web-storage.d.ts +16 -0
  157. package/elements.catalogue.json +676 -55
  158. package/package.json +14 -6
  159. package/dist/cjs/order-summary-ZMkJ5bez.js +0 -116
  160. package/dist/components/order-summary.js +0 -1
  161. package/dist/esm/order-summary-BGekdjN9.js +0 -103
  162. package/dist/ticketlayer-elements/p-0b1d16f8.entry.js +0 -1
  163. package/dist/ticketlayer-elements/p-1471f6d8.entry.js +0 -1
  164. package/dist/ticketlayer-elements/p-1c51cbc1.entry.js +0 -1
  165. package/dist/ticketlayer-elements/p-5169f68b.entry.js +0 -1
  166. package/dist/ticketlayer-elements/p-895ba1f3.entry.js +0 -1
  167. package/dist/ticketlayer-elements/p-8d5722e1.entry.js +0 -1
  168. package/dist/ticketlayer-elements/p-917d8e09.entry.js +0 -1
  169. package/dist/ticketlayer-elements/p-9629029a.entry.js +0 -1
  170. package/dist/ticketlayer-elements/p-BGekdjN9.js +0 -1
  171. package/dist/ticketlayer-elements/p-CNX8hZa8.js +0 -3
  172. package/dist/ticketlayer-elements/p-a22714d0.entry.js +0 -1
  173. package/dist/ticketlayer-elements/p-bf73044b.entry.js +0 -1
  174. package/dist/ticketlayer-elements/p-e02079fa.entry.js +0 -1
  175. package/dist/ticketlayer-elements/p-e04ace15.entry.js +0 -1
  176. package/dist/ticketlayer-elements/p-e6fab1d5.entry.js +0 -1
  177. package/dist/ticketlayer-elements/p-f826b753.entry.js +0 -1
  178. package/dist/ticketlayer-elements/p-fa639d88.entry.js +0 -1
  179. package/dist/ticketlayer-elements/p-fbaca948.entry.js +0 -1
@@ -1,10 +1,23 @@
1
1
  'use strict';
2
2
 
3
- var index = require('./index-BpfoKd9Q.js');
3
+ var index = require('./index-DanPUX0s.js');
4
4
  var sdkConnector = require('./sdk-connector-ClDF3013.js');
5
+ var ticketSelector = require('./ticket-selector-BYVL13k_.js');
5
6
 
6
- const tlOccurrenceSelectorCss = () => `:host{--tl-radius:var(--tl-radius-md, 0.5rem);display:block;font-family:var(--tl-font-family, system-ui, -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif);color:var(--tl-foreground, #0f172a)}*{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, #64748b)}.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, #ffffff);border:1px solid var(--tl-border, #e2e8f0);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, #e64b12);background:color-mix(in srgb, var(--tl-primary, #e64b12) 5%, var(--tl-background, #ffffff))}.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, #f1f5f9)}.occurrence-item.sold-out{opacity:0.6;background:var(--tl-muted, #f1f5f9)}.occurrence-info{flex:1}.occurrence-date{font-weight:600;font-size:0.9375rem;color:var(--tl-foreground, #0f172a);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, #e64b12)}.slots-view{display:flex;flex-direction:column;gap:1rem}.slots-date-header{font-size:1.125rem;font-weight:600;color:var(--tl-foreground, #0f172a);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, #ffffff);border:1px solid var(--tl-border, #e2e8f0);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, #e64b12);background:color-mix(in srgb, var(--tl-primary, #e64b12) 5%, var(--tl-background, #ffffff))}.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, #f1f5f9)}.time-slot.sold-out{opacity:0.6;background:var(--tl-muted, #f1f5f9)}.slot-time{font-size:0.9375rem;font-weight:600;color:var(--tl-foreground, #0f172a)}.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, #e2e8f0);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, #0f172a);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, #ffffff);border:1px solid var(--tl-border, #e2e8f0);border-radius:var(--tl-radius);cursor:pointer;transition:all 0.15s ease;color:var(--tl-foreground, #0f172a)}.nav-button:hover:not(:disabled){background:var(--tl-muted, #f1f5f9)}.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, #ffffff);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, #0f172a)}.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, #e64b12) 10%, var(--tl-background, #ffffff));border-color:transparent;font-weight:600;color:var(--tl-primary, #e64b12)}.calendar-day.has-occurrences:hover{background:color-mix(in srgb, var(--tl-primary, #e64b12) 18%, var(--tl-background, #ffffff));border-color:transparent}.calendar-day.has-occurrences.sold-out{background:var(--tl-muted, #f1f5f9);border-color:transparent}.calendar-day.has-occurrences.sold-out:hover{background:color-mix(in srgb, var(--tl-muted, #f1f5f9) 80%, var(--tl-background, #ffffff));border-color:transparent}.calendar-day.selected{background:var(--tl-primary, #e64b12);border-color:var(--tl-primary, #e64b12);color:var(--tl-primary-foreground, #ffffff)}.calendar-day.selected:hover{background:color-mix(in srgb, var(--tl-primary, #e64b12) 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, #f1f5f9);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, #0f172a);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, #ffffff);border:1px solid var(--tl-border, #e2e8f0);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, #e64b12);background:color-mix(in srgb, var(--tl-primary, #e64b12) 5%, var(--tl-background, #ffffff))}.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, #f1f5f9)}.time-slot-item.sold-out{opacity:0.6;background:var(--tl-muted, #f1f5f9)}.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, #f1f5f9);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, #e2e8f0);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, #0f172a);font-size:0.9375rem;max-width:400px}.retry-button{margin-top:0.5rem;padding:0.625rem 1.25rem;background:var(--tl-primary, #e64b12);color:var(--tl-primary-foreground, #ffffff);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, #e64b12) 90%, black)}.retry-button:focus-visible{outline:2px solid var(--tl-primary);outline-offset:2px}`;
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}`;
7
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
+ ];
8
21
  const TlOccurrenceSelector = class {
9
22
  constructor(hostRef) {
10
23
  index.registerInstance(this, hostRef);
@@ -13,565 +26,161 @@ const TlOccurrenceSelector = class {
13
26
  * Layout orientation (auto = horizontal on desktop, vertical on mobile)
14
27
  */
15
28
  this.orientation = 'auto';
16
- /**
17
- * Loading state
18
- */
19
- this.loading = true;
20
- /**
21
- * Error message
22
- */
23
- this.error = null;
24
- /**
25
- * Fetched occurrences
26
- */
27
- this.occurrences = [];
28
- /**
29
- * Current month being displayed in calendar view
30
- */
31
- this.currentMonth = new Date();
32
- /**
33
- * Currently selected date in calendar view
34
- */
35
- this.selectedDate = null;
36
- /**
37
- * Computed display mode based on occurrences
38
- */
39
- this.computedDisplayMode = 'list';
40
- /**
41
- * Container width for responsive layout
42
- */
43
- this.containerWidth = 0;
44
- }
45
- async componentWillLoad() {
46
- await this.waitForSDKAndFetch();
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;
47
38
  }
48
39
  /**
49
- * Wait for SDK to be ready, then fetch occurrences
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.
50
43
  */
51
- async waitForSDKAndFetch() {
52
- if (!this.eventId) {
53
- this.error = 'Event ID is required';
54
- this.loading = false;
55
- return;
56
- }
57
- try {
58
- await sdkConnector.whenSDK();
59
- await this.fetchOccurrences();
60
- }
61
- catch (err) {
62
- this.error = err instanceof Error ? err.message : 'SDK not initialized.';
63
- this.loading = false;
64
- }
44
+ watchSubject() {
45
+ this.syncProps();
46
+ this.teardown?.();
47
+ this.teardown = this.element.mount();
48
+ }
49
+ syncProps() {
50
+ this.element.setProps({
51
+ eventId: this.eventId,
52
+ displayMode: this.displayMode,
53
+ orientation: this.orientation,
54
+ theme: this.theme,
55
+ });
65
56
  }
57
+ /**
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.
63
+ */
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();
71
+ }
72
+ /** Measuring the element is the runtime's job; what a width means is not. */
66
73
  componentDidLoad() {
67
- // Set up resize observer for responsive layout
68
74
  if (this.containerRef && typeof ResizeObserver !== 'undefined') {
69
75
  this.resizeObserver = new ResizeObserver((entries) => {
70
- for (const entry of entries) {
71
- this.containerWidth = entry.contentRect.width;
72
- }
76
+ for (const entry of entries)
77
+ this.element.actions.resized(entry.contentRect.width);
73
78
  });
74
79
  this.resizeObserver.observe(this.containerRef);
75
80
  }
76
81
  }
77
- disconnectedCallback() {
78
- if (this.resizeObserver) {
79
- this.resizeObserver.disconnect();
80
- }
81
- }
82
- async onEventIdChange() {
83
- await this.fetchOccurrences();
84
- }
85
- /**
86
- * Fetch occurrences from the SDK
87
- */
88
- async fetchOccurrences() {
89
- this.loading = true;
90
- this.error = null;
91
- try {
92
- const sdk = sdkConnector.getSDK();
93
- const eventsManager = sdk.getEventsManager();
94
- const event = await eventsManager.get(this.eventId);
95
- if (!event) {
96
- throw new Error('Event not found');
97
- }
98
- // Map EventOccurrence to TlOccurrence
99
- this.occurrences = (event.occurrences || []).map((occ) => ({
100
- id: occ.id,
101
- startsAt: occ.startsAt,
102
- endsAt: occ.endsAt,
103
- status: occ.availabilityStatus,
104
- availableCapacity: occ.availableCapacity,
105
- soldOut: occ.availabilityStatus === 'sold_out',
106
- }));
107
- this.computeDisplayMode();
108
- this.computeInitialSelection();
109
- }
110
- catch (err) {
111
- console.error('[tl-occurrence-selector] Failed to fetch occurrences:', err);
112
- this.error = err instanceof Error ? err.message : 'Failed to load occurrences';
113
- this.occurrences = [];
114
- }
115
- finally {
116
- this.loading = false;
117
- }
118
- }
119
- /**
120
- * Compute the display mode based on occurrence count and date distribution
121
- */
122
- computeDisplayMode() {
123
- if (this.displayMode) {
124
- this.computedDisplayMode = this.displayMode;
125
- return;
126
- }
127
- const count = this.occurrences.length;
128
- if (count <= 5) {
129
- this.computedDisplayMode = 'list';
130
- return;
131
- }
132
- // Check if all occurrences are on the same calendar date
133
- const uniqueDates = new Set();
134
- for (const occ of this.occurrences) {
135
- const date = new Date(occ.startsAt);
136
- uniqueDates.add(date.toDateString());
137
- }
138
- if (uniqueDates.size === 1) {
139
- this.computedDisplayMode = 'slots';
140
- }
141
- else {
142
- this.computedDisplayMode = 'calendar';
143
- }
144
- }
145
- /**
146
- * Compute the initial date selection for calendar/slots views
147
- */
148
- computeInitialSelection() {
149
- if (this.occurrences.length === 0)
150
- return;
151
- const today = new Date();
152
- today.setHours(0, 0, 0, 0);
153
- // Get all dates with occurrences
154
- const datesWithOccurrences = this.getDatesWithOccurrences();
155
- // Find the first date that is today or in the future
156
- let selectedDate = null;
157
- for (const dateStr of Array.from(datesWithOccurrences.keys()).sort()) {
158
- const date = new Date(dateStr);
159
- if (date >= today) {
160
- selectedDate = date;
161
- break;
162
- }
163
- }
164
- // If no future dates, select the last available date
165
- if (!selectedDate && datesWithOccurrences.size > 0) {
166
- const sortedDates = Array.from(datesWithOccurrences.keys()).sort();
167
- selectedDate = new Date(sortedDates[sortedDates.length - 1]);
168
- }
169
- if (selectedDate) {
170
- this.selectedDate = selectedDate;
171
- this.currentMonth = new Date(selectedDate.getFullYear(), selectedDate.getMonth(), 1);
172
- }
173
- }
174
- /**
175
- * Get a map of dates to occurrences
176
- */
177
- getDatesWithOccurrences() {
178
- const map = new Map();
179
- for (const occ of this.occurrences) {
180
- const date = new Date(occ.startsAt);
181
- date.setHours(0, 0, 0, 0);
182
- const dateStr = date.toDateString();
183
- if (!map.has(dateStr)) {
184
- map.set(dateStr, []);
185
- }
186
- map.get(dateStr).push(occ);
187
- }
188
- return map;
189
- }
190
- /**
191
- * Get the first and last occurrence dates
192
- */
193
- getDateRange() {
194
- if (this.occurrences.length === 0)
195
- return null;
196
- let first = new Date(this.occurrences[0].startsAt);
197
- let last = new Date(this.occurrences[0].startsAt);
198
- for (const occ of this.occurrences) {
199
- const date = new Date(occ.startsAt);
200
- if (date < first)
201
- first = date;
202
- if (date > last)
203
- last = date;
204
- }
205
- return { first, last };
206
- }
207
- /**
208
- * Check if all occurrences fit in one week (Monday-Sunday)
209
- */
210
- isSingleWeek() {
211
- const dateRange = this.getDateRange();
212
- if (!dateRange)
213
- return false;
214
- // Get Monday of the week containing the first date
215
- const firstMonday = new Date(dateRange.first);
216
- const dayOfWeek = firstMonday.getDay();
217
- const daysToSubtract = dayOfWeek === 0 ? 6 : dayOfWeek - 1; // Monday = 0 days
218
- firstMonday.setDate(firstMonday.getDate() - daysToSubtract);
219
- firstMonday.setHours(0, 0, 0, 0);
220
- // Get Sunday of that week
221
- const sunday = new Date(firstMonday);
222
- sunday.setDate(sunday.getDate() + 6);
223
- sunday.setHours(23, 59, 59, 999);
224
- // Check if last date is within this week
225
- return dateRange.last <= sunday;
226
- }
227
- /**
228
- * Check if all occurrences are in the same month
229
- */
230
- isSingleMonth() {
231
- const dateRange = this.getDateRange();
232
- if (!dateRange)
233
- return false;
234
- return (dateRange.first.getMonth() === dateRange.last.getMonth() &&
235
- dateRange.first.getFullYear() === dateRange.last.getFullYear());
236
- }
237
- /**
238
- * Check if we should use horizontal layout
239
- */
240
- isHorizontalLayout() {
241
- if (this.orientation === 'horizontal')
242
- return true;
243
- if (this.orientation === 'vertical')
244
- return false;
245
- // Auto: use horizontal if container is wide enough (768px)
246
- return this.containerWidth >= 768;
247
- }
248
- /**
249
- * Format a date for display
250
- */
251
- formatDate(date) {
252
- return date.toLocaleDateString('en-GB', {
253
- weekday: 'long',
254
- day: 'numeric',
255
- month: 'long',
256
- year: 'numeric',
257
- });
258
- }
259
- /**
260
- * Format a time for display
261
- */
262
- formatTime(dateString) {
263
- const date = new Date(dateString);
264
- return date.toLocaleTimeString('en-GB', {
265
- hour: '2-digit',
266
- minute: '2-digit',
267
- });
268
- }
269
- /**
270
- * Format a full date and time for list view
271
- */
272
- formatDateTime(dateString) {
273
- const date = new Date(dateString);
274
- return {
275
- date: date.toLocaleDateString('en-GB', {
276
- weekday: 'long',
277
- day: 'numeric',
278
- month: 'long',
279
- year: 'numeric',
280
- }),
281
- time: date.toLocaleTimeString('en-GB', {
282
- hour: '2-digit',
283
- minute: '2-digit',
284
- }),
285
- };
286
- }
287
- /**
288
- * Handle occurrence selection
289
- */
290
- handleSelect(occurrence) {
291
- if (occurrence.soldOut || occurrence.availableCapacity === 0)
292
- return;
293
- console.log('[tl-occurrence-selector] Selected:', occurrence.id);
294
- this.tlSelect.emit({ occurrence });
295
- }
296
- /**
297
- * Handle date selection in calendar view
298
- */
299
- handleDateSelect(date) {
300
- this.selectedDate = date;
301
- // If date is in a different month, navigate to that month
302
- if (date.getMonth() !== this.currentMonth.getMonth() ||
303
- date.getFullYear() !== this.currentMonth.getFullYear()) {
304
- this.currentMonth = new Date(date.getFullYear(), date.getMonth(), 1);
305
- }
306
- }
307
82
  /**
308
- * Navigate to previous 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.
309
86
  */
310
- handlePrevMonth() {
311
- const dateRange = this.getDateRange();
312
- if (!dateRange)
313
- return;
314
- const newMonth = new Date(this.currentMonth);
315
- newMonth.setMonth(newMonth.getMonth() - 1);
316
- // Don't go before the first occurrence month
317
- const firstMonth = new Date(dateRange.first.getFullYear(), dateRange.first.getMonth(), 1);
318
- if (newMonth >= firstMonth) {
319
- this.currentMonth = newMonth;
320
- }
321
- }
322
- /**
323
- * Navigate to next month
324
- */
325
- handleNextMonth() {
326
- const dateRange = this.getDateRange();
327
- if (!dateRange)
328
- return;
329
- const newMonth = new Date(this.currentMonth);
330
- newMonth.setMonth(newMonth.getMonth() + 1);
331
- // Don't go past the last occurrence month
332
- const lastMonth = new Date(dateRange.last.getFullYear(), dateRange.last.getMonth(), 1);
333
- if (newMonth <= lastMonth) {
334
- this.currentMonth = newMonth;
335
- }
336
- }
337
- /**
338
- * Check if prev month button should be disabled
339
- */
340
- isPrevMonthDisabled() {
341
- const dateRange = this.getDateRange();
342
- if (!dateRange)
343
- return true;
344
- const firstMonth = new Date(dateRange.first.getFullYear(), dateRange.first.getMonth(), 1);
345
- return this.currentMonth <= firstMonth;
346
- }
347
- /**
348
- * Check if next month button should be disabled
349
- */
350
- isNextMonthDisabled() {
351
- const dateRange = this.getDateRange();
352
- if (!dateRange)
353
- return true;
354
- const lastMonth = new Date(dateRange.last.getFullYear(), dateRange.last.getMonth(), 1);
355
- return this.currentMonth >= lastMonth;
356
- }
357
- /**
358
- * Get calendar grid days for the current month
359
- */
360
- getCalendarDays() {
361
- // If all occurrences fit in one week, only show that week
362
- if (this.isSingleWeek()) {
363
- const dateRange = this.getDateRange();
364
- if (!dateRange)
365
- return [];
366
- // Get Monday of the week containing the first date
367
- const firstMonday = new Date(dateRange.first);
368
- const dayOfWeek = firstMonday.getDay();
369
- const daysToSubtract = dayOfWeek === 0 ? 6 : dayOfWeek - 1;
370
- firstMonday.setDate(firstMonday.getDate() - daysToSubtract);
371
- firstMonday.setHours(0, 0, 0, 0);
372
- // Generate 7 days (Monday to Sunday)
373
- const days = [];
374
- const current = new Date(firstMonday);
375
- for (let i = 0; i < 7; i++) {
376
- days.push(new Date(current));
377
- current.setDate(current.getDate() + 1);
378
- }
379
- return days;
380
- }
381
- // Otherwise show full month grid
382
- const year = this.currentMonth.getFullYear();
383
- const month = this.currentMonth.getMonth();
384
- // Start of month
385
- const monthStart = new Date(year, month, 1);
386
- // End of month
387
- const monthEnd = new Date(year, month + 1, 0);
388
- // Get the start of the week containing the first day (Monday = 1)
389
- const startDay = monthStart.getDay();
390
- const daysToSubtract = startDay === 0 ? 6 : startDay - 1; // Monday = 0 days to subtract
391
- const calendarStart = new Date(monthStart);
392
- calendarStart.setDate(calendarStart.getDate() - daysToSubtract);
393
- // Get the end of the week containing the last day
394
- const endDay = monthEnd.getDay();
395
- const daysToAdd = endDay === 0 ? 0 : 7 - endDay;
396
- const calendarEnd = new Date(monthEnd);
397
- calendarEnd.setDate(calendarEnd.getDate() + daysToAdd);
398
- // Generate all days
399
- const days = [];
400
- const current = new Date(calendarStart);
401
- while (current <= calendarEnd) {
402
- days.push(new Date(current));
403
- current.setDate(current.getDate() + 1);
404
- }
405
- return days;
406
- }
407
- /**
408
- * Check if a date has occurrences
409
- */
410
- hasOccurrencesOnDate(date) {
411
- const dateStr = date.toDateString();
412
- return this.getDatesWithOccurrences().has(dateStr);
413
- }
414
- /**
415
- * Check if all occurrences on a date are sold out
416
- */
417
- isDateSoldOut(date) {
418
- const dateStr = date.toDateString();
419
- const occurrences = this.getDatesWithOccurrences().get(dateStr);
420
- if (!occurrences)
421
- return false;
422
- 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;
423
94
  }
424
- /**
425
- * Get occurrences for the selected date
426
- */
427
- getSelectedDateOccurrences() {
428
- if (!this.selectedDate)
429
- return [];
430
- const dateStr = this.selectedDate.toDateString();
431
- const occurrences = this.getDatesWithOccurrences().get(dateStr) || [];
432
- // Sort by start time
433
- 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);
434
98
  }
435
99
  /**
436
- * 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.
437
103
  */
438
- getThemeStyles() {
439
- if (!this.theme)
440
- return {};
104
+ themeStyles() {
441
105
  const styles = {};
442
- if (this.theme.primaryColor) {
443
- styles['--tl-primary'] = this.theme.primaryColor;
444
- }
445
- if (this.theme.primaryForeground) {
446
- styles['--tl-primary-foreground'] = this.theme.primaryForeground;
447
- }
448
- if (this.theme.backgroundColor) {
449
- styles['--tl-background'] = this.theme.backgroundColor;
450
- }
451
- if (this.theme.foregroundColor) {
452
- styles['--tl-foreground'] = this.theme.foregroundColor;
453
- }
454
- if (this.theme.mutedColor) {
455
- styles['--tl-muted'] = this.theme.mutedColor;
456
- }
457
- if (this.theme.mutedForeground) {
458
- styles['--tl-muted-foreground'] = this.theme.mutedForeground;
459
- }
460
- if (this.theme.borderColor) {
461
- styles['--tl-border'] = this.theme.borderColor;
462
- }
463
- if (this.theme.borderRadius) {
464
- styles['--tl-radius'] = this.theme.borderRadius;
465
- }
466
- if (this.theme.fontFamily) {
467
- 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;
468
112
  }
469
113
  return styles;
470
114
  }
471
115
  // =========================================================================
472
116
  // RENDER METHODS
473
117
  // =========================================================================
474
- /**
475
- * Render the list view (≤5 occurrences)
476
- */
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). */
477
128
  renderListView() {
478
- return (index.h("div", { class: "list-view" }, this.occurrences.map((occurrence) => {
479
- const isSoldOut = occurrence.soldOut || occurrence.availableCapacity === 0;
480
- const { date, time } = this.formatDateTime(occurrence.startsAt);
481
- 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")))));
482
- })));
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)))))));
483
130
  }
484
- /**
485
- * Render the time slots view (>5 occurrences, same day)
486
- */
131
+ /** The time slots view (more than five, all on one date). */
487
132
  renderSlotsView() {
488
- // Sort occurrences by time
489
- const sortedOccurrences = [...this.occurrences].sort((a, b) => new Date(a.startsAt).getTime() - new Date(b.startsAt).getTime());
490
- // Get the date from the first occurrence
491
- const dateLabel = sortedOccurrences.length > 0
492
- ? this.formatDate(new Date(sortedOccurrences[0].startsAt))
493
- : '';
494
- 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) => {
495
- const isSoldOut = occurrence.soldOut || occurrence.availableCapacity === 0;
496
- 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")));
497
- }))));
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)))))));
498
135
  }
499
- /**
500
- * Render the calendar header
501
- */
502
136
  renderCalendarHeader() {
503
- const monthLabel = this.currentMonth.toLocaleDateString('en-GB', {
504
- month: 'long',
505
- year: 'numeric',
506
- });
507
- const showNavigation = !this.isSingleMonth();
508
- 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" })));
509
142
  }
510
- /**
511
- * Render the calendar grid
512
- */
513
143
  renderCalendarGrid() {
514
- const days = this.getCalendarDays();
515
- const weekDays = ['Mon', 'Tue', 'Wed', 'Thu', 'Fri', 'Sat', 'Sun'];
516
- const today = new Date();
517
- today.setHours(0, 0, 0, 0);
518
- return (index.h("div", { class: "calendar-grid" }, weekDays.map((day) => (index.h("div", { class: "weekday-header", key: day }, day))), days.map((date) => {
519
- const isCurrentMonth = date.getMonth() === this.currentMonth.getMonth();
520
- const hasOccurrences = this.hasOccurrencesOnDate(date);
521
- const isSoldOut = this.isDateSoldOut(date);
522
- const isSelected = this.selectedDate && date.toDateString() === this.selectedDate.toDateString();
523
- const isToday = date.toDateString() === today.toDateString();
524
- const isPast = date < today;
525
- return (index.h("button", { key: date.toISOString(), class: `calendar-day
526
- ${!isCurrentMonth ? 'other-month' : ''}
527
- ${hasOccurrences ? 'has-occurrences' : ''}
528
- ${isSoldOut ? 'sold-out' : ''}
529
- ${isSelected ? 'selected' : ''}
530
- ${isToday ? 'today' : ''}
531
- ${isPast ? 'past' : ''}`, onClick: () => hasOccurrences && this.handleDateSelect(date), disabled: !hasOccurrences }, index.h("span", { class: "day-number" }, date.getDate()), hasOccurrences && index.h("span", { class: "occurrence-dot" })));
532
- })));
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))));
533
146
  }
534
- /**
535
- * Render the selected date's time slots
536
- */
147
+ /** The picked date's times, beside or below the calendar. */
537
148
  renderSelectedDateSlots() {
538
- const occurrences = this.getSelectedDateOccurrences();
539
- if (!this.selectedDate || occurrences.length === 0) {
540
- 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)));
541
152
  }
542
- const dateLabel = this.formatDate(this.selectedDate);
543
- 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) => {
544
- const isSoldOut = occurrence.soldOut || occurrence.availableCapacity === 0;
545
- 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")))));
546
- }))));
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))))))));
547
154
  }
548
- /**
549
- * Render the calendar view with slots panel
550
- */
551
155
  renderCalendarView() {
552
- const isHorizontal = this.isHorizontalLayout();
553
- 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())));
554
157
  }
555
- /**
556
- * Render skeleton loading state
557
- */
158
+ /** The skeleton, which is a calendar because that is the common shape. */
558
159
  renderSkeleton() {
559
- // Default to calendar skeleton as it's the most common
560
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" })))))));
561
161
  }
562
162
  render() {
563
- const themeStyles = this.getThemeStyles();
564
- 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()))));
565
165
  }
566
166
  static get watchers() { return {
567
167
  "eventId": [{
568
- "onEventIdChange": 0
168
+ "watchSubject": 0
169
+ }],
170
+ "displayMode": [{
171
+ "syncProps": 0
172
+ }],
173
+ "orientation": [{
174
+ "syncProps": 0
175
+ }],
176
+ "theme": [{
177
+ "syncProps": 0
569
178
  }]
570
179
  }; }
571
180
  };
572
181
  TlOccurrenceSelector.style = tlOccurrenceSelectorCss();
573
182
 
574
- const tlTicketSelectorCss = () => `:host{--tl-color-primary:var(--tl-primary, #6366f1);--tl-color-primary-foreground:var(--tl-primary-foreground, #ffffff);--tl-color-background:var(--tl-background, #ffffff);--tl-color-foreground:var(--tl-foreground, #111827);--tl-color-muted:var(--tl-muted, #f3f4f6);--tl-color-muted-foreground:var(--tl-muted-foreground, #6b7280);--tl-color-border:var(--tl-border, #e5e7eb);--tl-color-destructive:var(--tl-error, #ef4444);display:block;font-family:var(--tl-font-family, system-ui, sans-serif)}.ticket-selector{background:var(--tl-color-background, #ffffff)}.skeleton-container{padding:var(--tl-spacing-4, 1rem)}.skeleton-row{height:60px;background:var(--tl-color-muted, #f3f4f6);border-radius:var(--tl-radius-md, 0.5rem);margin-bottom:var(--tl-spacing-3, 0.75rem);animation:pulse 1.5s ease-in-out infinite}@keyframes pulse{0%,100%{opacity:1}50%{opacity:0.5}}.error-state{padding:var(--tl-spacing-4, 1rem);color:var(--tl-color-destructive, #ef4444)}.error-message{margin:0;font-size:14px}.ticket-list{margin-bottom:var(--tl-spacing-4, 1rem)}.ticket-row{display:flex;align-items:center;justify-content:space-between;padding:var(--tl-spacing-3, 0.75rem) 0;border-bottom:1px solid var(--tl-color-border, #e5e7eb)}.ticket-row.sold-out{opacity:0.5}.ticket-info{flex:1;min-width:0}.ticket-header{display:flex;align-items:center;gap:var(--tl-spacing-2, 0.5rem);flex-wrap:wrap}.ticket-name{font-weight:600;color:var(--tl-color-foreground, #111827);font-size:15px}.status-badge{font-size:12px;font-weight:500;padding:2px 8px;border-radius:4px}.status-badge.limited{background-color:var(--tl-color-warning-bg, #fef3c7);color:var(--tl-color-warning, #d97706)}.status-badge.sold-out{background-color:var(--tl-color-destructive-bg, #fee2e2);color:var(--tl-color-destructive, #ef4444)}.ticket-description{margin:var(--tl-spacing-1, 0.25rem) 0 0;font-size:14px;color:var(--tl-color-muted-foreground, #6b7280)}.ticket-price{margin:var(--tl-spacing-1, 0.25rem) 0 0;font-size:14px;font-weight:600;color:var(--tl-color-foreground, #111827)}.quantity-controls{display:flex;align-items:center;gap:var(--tl-spacing-2, 0.5rem);margin-left:var(--tl-spacing-4, 1rem)}.qty-btn{width:32px;height:32px;border-radius:var(--tl-radius-sm, 0.25rem);border:1px solid var(--tl-color-border, #d1d5db);background:var(--tl-color-background, #ffffff);color:var(--tl-color-foreground, #111827);font-size:18px;font-weight:500;cursor:pointer;display:flex;align-items:center;justify-content:center;transition:background-color 0.15s, border-color 0.15s}.qty-btn:hover:not(:disabled){background-color:var(--tl-color-muted, #f3f4f6);border-color:var(--tl-color-border-hover, #9ca3af)}.qty-btn:disabled{cursor:not-allowed;opacity:0.5}.qty-value{width:32px;text-align:center;font-weight:600;font-size:16px;color:var(--tl-color-foreground, #111827)}.success-message{display:flex;align-items:center;justify-content:center;gap:var(--tl-spacing-2, 0.5rem);padding:var(--tl-spacing-3, 0.75rem);background-color:var(--tl-color-success, #10b981);color:#ffffff;border-radius:var(--tl-radius-md, 0.5rem);margin-bottom:var(--tl-spacing-3, 0.75rem);font-weight:500;font-size:14px}.success-icon{width:18px;height:18px}.add-to-cart-btn{width:100%;padding:var(--tl-spacing-3, 0.75rem) var(--tl-spacing-4, 1rem);border-radius:var(--tl-radius-md, 0.5rem);border:none;background-color:var(--tl-color-primary, #6366f1);color:var(--tl-color-primary-foreground, #ffffff);font-weight:600;font-size:16px;cursor:pointer;transition:opacity 0.15s, transform 0.1s}.add-to-cart-btn:hover:not(:disabled){opacity:0.9}.add-to-cart-btn:active:not(:disabled){transform:scale(0.98)}.add-to-cart-btn.disabled,.add-to-cart-btn:disabled{background-color:var(--tl-color-muted, #d1d5db);color:var(--tl-color-muted-foreground, #6b7280);cursor:not-allowed}`;
183
+ const tlTicketSelectorCss = () => `:host{--tl-color-primary:var(--tl-primary);--tl-color-primary-foreground:var(--tl-primary-foreground);--tl-color-background:var(--tl-background);--tl-color-foreground:var(--tl-foreground);--tl-color-muted:var(--tl-muted);--tl-color-muted-foreground:var(--tl-muted-foreground);--tl-color-border:var(--tl-border);--tl-color-destructive:var(--tl-error);display:block;font-family:var(--tl-font-family)}.ticket-selector{background:var(--tl-color-background)}.skeleton-container{padding:var(--tl-spacing-4)}.skeleton-row{height:60px;background:var(--tl-color-muted);border-radius:var(--tl-radius-md);margin-bottom:var(--tl-spacing-3);animation:pulse 1.5s ease-in-out infinite}@keyframes pulse{0%,100%{opacity:1}50%{opacity:0.5}}.error-state{padding:var(--tl-spacing-4);color:var(--tl-color-destructive)}.error-message{margin:0;font-size:14px}.ticket-list{margin-bottom:var(--tl-spacing-4)}.ticket-row{display:flex;align-items:center;justify-content:space-between;padding:var(--tl-spacing-3) 0;border-bottom:1px solid var(--tl-color-border)}.ticket-row.sold-out{opacity:0.5}.ticket-info{flex:1;min-width:0}.ticket-header{display:flex;align-items:center;gap:var(--tl-spacing-2);flex-wrap:wrap}.ticket-name{font-weight:600;color:var(--tl-color-foreground);font-size:15px}.status-badge{font-size:12px;font-weight:500;padding:2px 8px;border-radius:4px}.status-badge.limited{background-color:var(--tl-color-warning-bg);color:var(--tl-color-warning)}.status-badge.sold-out{background-color:var(--tl-color-destructive-bg);color:var(--tl-color-destructive)}.ticket-description{margin:var(--tl-spacing-1) 0 0;font-size:14px;color:var(--tl-color-muted-foreground)}.ticket-price{margin:var(--tl-spacing-1) 0 0;font-size:14px;font-weight:600;color:var(--tl-color-foreground)}.quantity-controls{display:flex;align-items:center;gap:var(--tl-spacing-2);margin-left:var(--tl-spacing-4)}.qty-btn{width:32px;height:32px;border-radius:var(--tl-radius-sm);border:1px solid var(--tl-color-border);background:var(--tl-color-background);color:var(--tl-color-foreground);font-size:18px;font-weight:500;cursor:pointer;display:flex;align-items:center;justify-content:center;transition:background-color 0.15s, border-color 0.15s}.qty-btn:hover:not(:disabled){background-color:var(--tl-color-muted);border-color:var(--tl-color-border-hover)}.qty-btn:disabled{cursor:not-allowed;opacity:0.5}.qty-value{width:32px;text-align:center;font-weight:600;font-size:16px;color:var(--tl-color-foreground)}.success-message{display:flex;align-items:center;justify-content:center;gap:var(--tl-spacing-2);padding:var(--tl-spacing-3);background-color:var(--tl-color-success);color:#ffffff;border-radius:var(--tl-radius-md);margin-bottom:var(--tl-spacing-3);font-weight:500;font-size:14px}.success-icon{width:18px;height:18px}.add-to-cart-btn{width:100%;padding:var(--tl-spacing-3) var(--tl-spacing-4);border-radius:var(--tl-radius-md);border:none;background-color:var(--tl-color-primary);color:var(--tl-color-primary-foreground);font-weight:600;font-size:16px;cursor:pointer;transition:opacity 0.15s, transform 0.1s}.add-to-cart-btn:hover:not(:disabled){opacity:0.9}.add-to-cart-btn:active:not(:disabled){transform:scale(0.98)}.add-to-cart-btn.disabled,.add-to-cart-btn:disabled{background-color:var(--tl-color-muted);color:var(--tl-color-muted-foreground);cursor:not-allowed}.selection-totals{padding:var(--tl-spacing-3) 0;border-top:1px solid var(--tl-color-border)}.selection-totals .total-row{display:flex;justify-content:space-between;align-items:center;padding:var(--tl-spacing-1) 0;font-size:14px;color:var(--tl-color-muted-foreground)}.selection-totals .total-row.total{font-size:16px;font-weight:700;color:var(--tl-color-foreground);padding-top:var(--tl-spacing-2);border-top:1px solid var(--tl-color-border);margin-top:var(--tl-spacing-2)}`;
575
184
 
576
185
  const TlTicketSelector = class {
577
186
  constructor(hostRef) {
@@ -587,157 +196,106 @@ const TlTicketSelector = class {
587
196
  * Button label
588
197
  */
589
198
  this.buttonLabel = 'Add to Cart';
590
- this.ticketTypes = [];
591
- this.quantities = {};
592
- this.loading = true;
593
- this.error = null;
594
- this.adding = false;
595
- this.addSuccess = false;
199
+ this.element = ticketSelector.createElement(ticketSelector.ticketSelectorCore, {
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(),
204
+ emit: (event, detail) => this.forward(event, detail),
205
+ });
206
+ /** The core's whole view. Assigning it is what makes Stencil re-render. */
207
+ this.view = this.element.view;
596
208
  }
597
- watchPropsChange() {
598
- this.loadTicketTypes();
209
+ /**
210
+ * The fee the surface has quoted for this selection. Core state, exposed here
211
+ * because it is set from outside the element and not from an attribute: there
212
+ * is no quote operation on the buyer-facing surface yet, so it stays null and
213
+ * no fee row is rendered.
214
+ */
215
+ get feeQuote() {
216
+ return this.element.state.feeQuote;
599
217
  }
600
- async componentWillLoad() {
601
- await this.waitForSDKAndLoad();
218
+ set feeQuote(quote) {
219
+ this.element.actions.setFeeQuote(quote);
602
220
  }
603
221
  /**
604
- * Wait for SDK to be ready, then load ticket types
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.
605
225
  */
606
- async waitForSDKAndLoad() {
607
- if (!this.eventId || !this.occurrenceId) {
608
- return;
609
- }
610
- try {
611
- await sdkConnector.whenSDK();
612
- await this.loadTicketTypes();
613
- }
614
- catch (err) {
615
- this.error = err instanceof Error ? err.message : 'SDK not initialized.';
616
- this.loading = false;
617
- }
618
- }
619
- async loadTicketTypes() {
620
- try {
621
- console.log('[tl-ticket-selector] Loading ticket types...', {
622
- eventId: this.eventId,
623
- occurrenceId: this.occurrenceId
624
- });
625
- this.loading = true;
626
- this.error = null;
627
- const sdk = sdkConnector.getSDK();
628
- const eventsManager = sdk.getEventsManager();
629
- const types = await eventsManager.getTicketTypes(this.eventId, this.occurrenceId);
630
- console.log('[tl-ticket-selector] Ticket types loaded:', types.length);
631
- this.ticketTypes = types;
632
- // Initialise quantities to 0
633
- const initialQuantities = {};
634
- types.forEach((t) => {
635
- initialQuantities[t.id] = 0;
636
- });
637
- this.quantities = initialQuantities;
638
- }
639
- catch (err) {
640
- console.error('[tl-ticket-selector] Failed to load ticket types:', err);
641
- this.error = err instanceof Error ? err.message : 'Failed to load tickets';
642
- this.tlError.emit({ message: this.error });
643
- }
644
- finally {
645
- this.loading = false;
646
- }
226
+ watchPropsChange() {
227
+ this.syncProps();
228
+ this.remount();
229
+ }
230
+ syncProps() {
231
+ this.element.setProps({
232
+ eventId: this.eventId,
233
+ occurrenceId: this.occurrenceId,
234
+ showButton: this.showButton,
235
+ buttonLabel: this.buttonLabel,
236
+ });
647
237
  }
648
- handleQuantityChange(ticketTypeId, delta) {
649
- const ticketType = this.ticketTypes.find(t => t.id === ticketTypeId);
650
- if (!ticketType)
651
- return;
652
- const currentQty = this.quantities[ticketTypeId] || 0;
653
- const newQty = Math.max(0, Math.min(ticketType.maxPerOrder, currentQty + delta));
654
- this.quantities = {
655
- ...this.quantities,
656
- [ticketTypeId]: newQty,
657
- };
658
- const total = this.calculateTotal();
659
- this.tlQuantityChange.emit({ ticketTypeId, quantity: newQty, total });
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() {
248
+ this.syncProps();
249
+ this.unsubscribe ?? (this.unsubscribe = this.element.subscribe((view) => {
250
+ this.view = view;
251
+ }));
252
+ this.view = this.element.view;
253
+ this.teardown = this.element.mount();
660
254
  }
661
- calculateTotal() {
662
- return this.ticketTypes.reduce((sum, tt) => {
663
- return sum + (this.quantities[tt.id] || 0) * tt.price;
664
- }, 0);
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
+ */
261
+ disconnectedCallback() {
262
+ this.teardown?.();
263
+ this.teardown = undefined;
264
+ this.unsubscribe?.();
265
+ this.unsubscribe = undefined;
665
266
  }
666
- getTotalItems() {
667
- return Object.values(this.quantities).reduce((sum, qty) => sum + qty, 0);
267
+ remount() {
268
+ this.teardown?.();
269
+ this.teardown = this.element.mount();
668
270
  }
669
- async handleAddToCart() {
670
- const itemsToAdd = Object.entries(this.quantities)
671
- .filter(([_, qty]) => qty > 0)
672
- .map(([ticketTypeId, quantity]) => ({ ticketTypeId, quantity }));
673
- if (itemsToAdd.length === 0)
674
- return;
675
- this.adding = true;
676
- this.addSuccess = false;
677
- try {
678
- if (sdkConnector.hasSDK()) {
679
- const sdk = sdkConnector.getSDK();
680
- const cartManager = sdk.getCartManager?.();
681
- if (cartManager) {
682
- for (const item of itemsToAdd) {
683
- await cartManager.addItem(item.ticketTypeId, item.quantity, this.occurrenceId);
684
- }
685
- console.log('[tl-ticket-selector] Items added to cart');
686
- }
687
- }
688
- // Reset quantities after adding
689
- const resetQuantities = {};
690
- this.ticketTypes.forEach(t => {
691
- resetQuantities[t.id] = 0;
692
- });
693
- this.quantities = resetQuantities;
694
- this.addSuccess = true;
695
- // Clear success message after 3 seconds
696
- setTimeout(() => {
697
- this.addSuccess = false;
698
- }, 3000);
699
- this.tlAddToCart.emit({ items: itemsToAdd });
271
+ forward(event, detail) {
272
+ if (event === 'tlAddToCart')
273
+ this.tlAddToCart.emit(detail);
274
+ else if (event === 'tlQuantityChange') {
275
+ this.tlQuantityChange.emit(detail);
700
276
  }
701
- catch (err) {
702
- console.error('[tl-ticket-selector] Failed to add to cart:', err);
703
- this.error = err instanceof Error ? err.message : 'Failed to add to cart';
704
- this.tlError.emit({ message: this.error });
705
- }
706
- finally {
707
- this.adding = false;
708
- }
709
- }
710
- formatPrice(priceInPence, currency = 'GBP') {
711
- return new Intl.NumberFormat('en-GB', {
712
- style: 'currency',
713
- currency,
714
- }).format(priceInPence / 100);
277
+ else if (event === 'tlError')
278
+ this.tlError.emit(detail);
715
279
  }
716
280
  renderSkeleton() {
717
281
  return (index.h("div", { class: "skeleton-container" }, index.h("div", { class: "skeleton-row" }), index.h("div", { class: "skeleton-row" }), index.h("div", { class: "skeleton-row" })));
718
282
  }
719
283
  renderError() {
720
- return (index.h("div", { class: "error-state" }, index.h("p", { class: "error-message" }, this.error)));
284
+ return (index.h("div", { class: "error-state" }, index.h("p", { class: "error-message" }, this.view.state.error)));
721
285
  }
722
- renderTicketRow(tt) {
723
- const isSoldOut = tt.availabilityStatus === 'sold_out';
724
- const qty = this.quantities[tt.id] || 0;
725
- return (index.h("div", { class: `ticket-row ${isSoldOut ? 'sold-out' : ''}`, key: tt.id }, index.h("div", { class: "ticket-info" }, index.h("div", { class: "ticket-header" }, index.h("span", { class: "ticket-name" }, tt.name), tt.availabilityStatus === 'limited' && (index.h("span", { class: "status-badge limited" }, "Limited")), isSoldOut && (index.h("span", { class: "status-badge sold-out" }, "Sold Out"))), tt.description && (index.h("p", { class: "ticket-description" }, tt.description)), index.h("p", { class: "ticket-price" }, this.formatPrice(tt.price, tt.currency))), index.h("div", { class: "quantity-controls" }, index.h("button", { class: "qty-btn", onClick: () => this.handleQuantityChange(tt.id, -1), disabled: isSoldOut || qty === 0, "aria-label": "Decrease quantity" }, "\u2212"), index.h("span", { class: "qty-value" }, qty), index.h("button", { class: "qty-btn", onClick: () => this.handleQuantityChange(tt.id, 1), disabled: isSoldOut || qty >= tt.maxPerOrder, "aria-label": "Increase quantity" }, "+"))));
286
+ renderTicketRow(row) {
287
+ const strings = this.view.strings;
288
+ return (index.h("div", { class: `ticket-row ${row.soldOut ? 'sold-out' : ''}`, key: row.id }, index.h("div", { class: "ticket-info" }, index.h("div", { class: "ticket-header" }, index.h("span", { class: "ticket-name" }, row.name), row.limited && index.h("span", { class: "status-badge limited" }, strings.limited), row.soldOut && index.h("span", { class: "status-badge sold-out" }, strings.soldOut)), row.description && index.h("p", { class: "ticket-description" }, row.description), index.h("p", { class: "ticket-price" }, row.priceLabel)), index.h("div", { class: "quantity-controls" }, index.h("button", { class: "qty-btn", onClick: () => this.element.actions.changeQuantity(row.id, -1), disabled: !row.canDecrease, "aria-label": strings.decrease }, "\u2212"), index.h("span", { class: "qty-value" }, row.quantity), index.h("button", { class: "qty-btn", onClick: () => this.element.actions.changeQuantity(row.id, 1), disabled: !row.canIncrease, "aria-label": strings.increase }, "+"))));
726
289
  }
727
290
  render() {
728
- if (this.loading) {
291
+ const { status, state, strings, derived } = this.view;
292
+ if (status === 'loading') {
729
293
  return this.renderSkeleton();
730
294
  }
731
- if (this.error) {
295
+ if (status === 'error') {
732
296
  return this.renderError();
733
297
  }
734
- const totalItems = this.getTotalItems();
735
- const totalPrice = this.calculateTotal();
736
- return (index.h("div", { class: "ticket-selector" }, index.h("div", { class: "ticket-list" }, this.ticketTypes.map(tt => this.renderTicketRow(tt))), this.addSuccess && (index.h("div", { class: "success-message" }, index.h("svg", { class: "success-icon", viewBox: "0 0 24 24", fill: "none", stroke: "currentColor" }, index.h("path", { "stroke-linecap": "round", "stroke-linejoin": "round", "stroke-width": "2", d: "M5 13l4 4L19 7" })), "Added to cart!")), this.showButton && (index.h("button", { class: `add-to-cart-btn ${totalItems === 0 ? 'disabled' : ''}`, onClick: () => this.handleAddToCart(), disabled: totalItems === 0 || this.adding }, this.adding
737
- ? 'Adding...'
738
- : totalItems === 0
739
- ? 'Select Tickets'
740
- : `${this.buttonLabel} - ${this.formatPrice(totalPrice)}`))));
298
+ return (index.h("div", { class: "ticket-selector" }, index.h("div", { class: "ticket-list" }, derived.rows.map((row) => this.renderTicketRow(row))), derived.showTotals && (index.h("div", { class: "selection-totals" }, index.h("div", { class: "total-row" }, index.h("span", null, strings.ticketsRow), index.h("span", null, derived.subtotalLabel)), index.h("div", { class: "total-row fee" }, index.h("span", null, derived.totals.feeLabel), index.h("span", null, derived.feeLabel)), index.h("div", { class: "total-row total" }, index.h("span", null, strings.totalRow), index.h("span", null, derived.totalLabel)))), state.addSuccess && (index.h("div", { class: "success-message" }, index.h("svg", { class: "success-icon", viewBox: "0 0 24 24", fill: "none", stroke: "currentColor" }, index.h("path", { "stroke-linecap": "round", "stroke-linejoin": "round", "stroke-width": "2", d: "M5 13l4 4L19 7" })), strings.added)), this.showButton && (index.h("button", { class: `add-to-cart-btn ${derived.totalItems === 0 ? 'disabled' : ''}`, onClick: () => this.element.actions.addToCart(), disabled: derived.actionDisabled }, derived.actionLabel))));
741
299
  }
742
300
  static get watchers() { return {
743
301
  "eventId": [{
@@ -745,6 +303,12 @@ const TlTicketSelector = class {
745
303
  }],
746
304
  "occurrenceId": [{
747
305
  "watchPropsChange": 0
306
+ }],
307
+ "showButton": [{
308
+ "syncProps": 0
309
+ }],
310
+ "buttonLabel": [{
311
+ "syncProps": 0
748
312
  }]
749
313
  }; }
750
314
  };