@ticketlayer/elements 0.3.3 → 0.4.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 (134) hide show
  1. package/README.md +3 -2
  2. package/dist/cjs/{index-BpfoKd9Q.js → index-I9CoYv3k.js} +1 -1
  3. package/dist/cjs/index.cjs.js +1 -1
  4. package/dist/cjs/loader.cjs.js +2 -2
  5. package/dist/cjs/money-BtmM2gt-.js +118 -0
  6. package/dist/cjs/{order-summary-ZMkJ5bez.js → order-summary-X3TlZxc2.js} +34 -0
  7. package/dist/cjs/ticketlayer-elements.cjs.js +2 -2
  8. package/dist/cjs/tl-buy-modal.cjs.entry.js +3 -3
  9. package/dist/cjs/tl-buy-tickets-button.cjs.entry.js +10 -7
  10. package/dist/cjs/tl-buy-tickets-wrapper.cjs.entry.js +2 -2
  11. package/dist/cjs/tl-cart-badge.cjs.entry.js +2 -2
  12. package/dist/cjs/tl-cart-drawer.cjs.entry.js +3 -3
  13. package/dist/cjs/tl-cart.cjs.entry.js +10 -4
  14. package/dist/cjs/tl-checkout-wrapper.cjs.entry.js +2 -2
  15. package/dist/cjs/tl-event-card.cjs.entry.js +4 -3
  16. package/dist/cjs/tl-event-list.cjs.entry.js +2 -2
  17. package/dist/cjs/tl-login.cjs.entry.js +4 -3
  18. package/dist/cjs/tl-my-orders.cjs.entry.js +4 -3
  19. package/dist/cjs/tl-occurrence-selector_2.cjs.entry.js +454 -122
  20. package/dist/cjs/tl-order-confirmation.cjs.entry.js +11 -8
  21. package/dist/cjs/tl-order-tickets_2.cjs.entry.js +3 -3
  22. package/dist/cjs/tl-promo-code.cjs.entry.js +4 -3
  23. package/dist/cjs/tl-seat-map.cjs.entry.js +2 -2
  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 +1 -1
  26. package/dist/collection/components/tl-buy-tickets-button/tl-buy-tickets-button.css +4 -4
  27. package/dist/collection/components/tl-buy-tickets-button/tl-buy-tickets-button.js +8 -5
  28. package/dist/collection/components/tl-buy-tickets-wrapper/tl-buy-tickets-wrapper.css +2 -2
  29. package/dist/collection/components/tl-cart/tl-cart.css +52 -51
  30. package/dist/collection/components/tl-cart/tl-cart.js +8 -2
  31. package/dist/collection/components/tl-cart-badge/tl-cart-badge.css +11 -11
  32. package/dist/collection/components/tl-cart-drawer/tl-cart-drawer.css +6 -6
  33. package/dist/collection/components/tl-cart-drawer/tl-cart-drawer.js +1 -1
  34. package/dist/collection/components/tl-checkout-wrapper/tl-checkout-wrapper.css +2 -2
  35. package/dist/collection/components/tl-event-card/tl-event-card.css +22 -22
  36. package/dist/collection/components/tl-event-list/tl-event-list.css +17 -17
  37. package/dist/collection/components/tl-login/tl-login.css +26 -26
  38. package/dist/collection/components/tl-my-orders/tl-my-orders.css +40 -40
  39. package/dist/collection/components/tl-occurrence-selector/tl-occurrence-selector.css +54 -65
  40. package/dist/collection/components/tl-order-confirmation/tl-order-confirmation.css +38 -38
  41. package/dist/collection/components/tl-order-confirmation/tl-order-confirmation.js +8 -6
  42. package/dist/collection/components/tl-order-tickets/tl-order-tickets.css +9 -9
  43. package/dist/collection/components/tl-promo-code/tl-promo-code.css +25 -25
  44. package/dist/collection/components/tl-seat-map/tl-seat-map.css +10 -10
  45. package/dist/collection/components/tl-ticket/tl-ticket.css +13 -13
  46. package/dist/collection/components/tl-ticket-selector/tl-ticket-selector.css +74 -49
  47. package/dist/collection/components/tl-ticket-selector/tl-ticket-selector.js +72 -126
  48. package/dist/collection/utils/cart-totals.js +9 -0
  49. package/dist/collection/utils/order-summary.js +36 -0
  50. package/dist/components/money.js +1 -0
  51. package/dist/components/order-summary.js +1 -1
  52. package/dist/components/tl-buy-modal.js +1 -1
  53. package/dist/components/tl-buy-tickets-button.js +1 -1
  54. package/dist/components/tl-buy-tickets-wrapper2.js +1 -1
  55. package/dist/components/tl-cart-badge.js +1 -1
  56. package/dist/components/tl-cart-drawer.js +1 -1
  57. package/dist/components/tl-cart2.js +1 -1
  58. package/dist/components/tl-checkout-wrapper2.js +1 -1
  59. package/dist/components/tl-event-card2.js +1 -1
  60. package/dist/components/tl-event-list.js +1 -1
  61. package/dist/components/tl-login2.js +1 -1
  62. package/dist/components/tl-my-orders.js +1 -1
  63. package/dist/components/tl-occurrence-selector2.js +1 -1
  64. package/dist/components/tl-order-confirmation.js +1 -1
  65. package/dist/components/tl-order-tickets2.js +1 -1
  66. package/dist/components/tl-promo-code.js +1 -1
  67. package/dist/components/tl-seat-map.js +1 -1
  68. package/dist/components/tl-ticket-selector2.js +1 -1
  69. package/dist/components/tl-ticket2.js +1 -1
  70. package/dist/elements.docs.json +4 -4
  71. package/dist/esm/{index-CNX8hZa8.js → index-OoYV1faY.js} +1 -1
  72. package/dist/esm/index.js +1 -1
  73. package/dist/esm/loader.js +3 -3
  74. package/dist/esm/money-4ig0-WB6.js +112 -0
  75. package/dist/esm/{order-summary-BGekdjN9.js → order-summary-DiDtvfZM.js} +34 -1
  76. package/dist/esm/ticketlayer-elements.js +3 -3
  77. package/dist/esm/tl-buy-modal.entry.js +3 -3
  78. package/dist/esm/tl-buy-tickets-button.entry.js +10 -7
  79. package/dist/esm/tl-buy-tickets-wrapper.entry.js +2 -2
  80. package/dist/esm/tl-cart-badge.entry.js +2 -2
  81. package/dist/esm/tl-cart-drawer.entry.js +3 -3
  82. package/dist/esm/tl-cart.entry.js +10 -4
  83. package/dist/esm/tl-checkout-wrapper.entry.js +2 -2
  84. package/dist/esm/tl-event-card.entry.js +4 -3
  85. package/dist/esm/tl-event-list.entry.js +2 -2
  86. package/dist/esm/tl-login.entry.js +4 -3
  87. package/dist/esm/tl-my-orders.entry.js +4 -3
  88. package/dist/esm/tl-occurrence-selector_2.entry.js +455 -123
  89. package/dist/esm/tl-order-confirmation.entry.js +11 -8
  90. package/dist/esm/tl-order-tickets_2.entry.js +3 -3
  91. package/dist/esm/tl-promo-code.entry.js +4 -3
  92. package/dist/esm/tl-seat-map.entry.js +2 -2
  93. package/dist/ticketlayer-elements/index.esm.js +1 -1
  94. package/dist/ticketlayer-elements/p-0814420f.entry.js +1 -0
  95. package/dist/ticketlayer-elements/p-0acabc26.entry.js +1 -0
  96. package/dist/ticketlayer-elements/{p-9629029a.entry.js → p-109a3feb.entry.js} +1 -1
  97. package/dist/ticketlayer-elements/{p-e02079fa.entry.js → p-1a4ba71c.entry.js} +1 -1
  98. package/dist/ticketlayer-elements/p-318965c1.entry.js +1 -0
  99. package/dist/ticketlayer-elements/p-347c7fd1.entry.js +1 -0
  100. package/dist/ticketlayer-elements/p-3649c26d.entry.js +1 -0
  101. package/dist/ticketlayer-elements/p-40260cc9.entry.js +1 -0
  102. package/dist/ticketlayer-elements/p-4ig0-WB6.js +1 -0
  103. package/dist/ticketlayer-elements/p-55fd2f9f.entry.js +1 -0
  104. package/dist/ticketlayer-elements/p-62ee659b.entry.js +1 -0
  105. package/dist/ticketlayer-elements/p-7d4ecc2b.entry.js +1 -0
  106. package/dist/ticketlayer-elements/p-7f0abf95.entry.js +1 -0
  107. package/dist/ticketlayer-elements/p-BXpVGW51.js +1 -0
  108. package/dist/ticketlayer-elements/p-OoYV1faY.js +3 -0
  109. package/dist/ticketlayer-elements/p-a3c1b26c.entry.js +1 -0
  110. package/dist/ticketlayer-elements/{p-1c51cbc1.entry.js → p-ac264adc.entry.js} +1 -1
  111. package/dist/ticketlayer-elements/{p-895ba1f3.entry.js → p-aeb4c6fb.entry.js} +1 -1
  112. package/dist/ticketlayer-elements/{p-fa639d88.entry.js → p-ba62541f.entry.js} +1 -1
  113. package/dist/ticketlayer-elements/ticketlayer-elements.css +1 -1
  114. package/dist/ticketlayer-elements/ticketlayer-elements.esm.js +1 -1
  115. package/dist/types/components/tl-cart/tl-cart.d.ts +10 -0
  116. package/dist/types/components/tl-ticket-selector/tl-ticket-selector.d.ts +40 -29
  117. package/dist/types/components.d.ts +32 -0
  118. package/dist/types/utils/cart-totals.d.ts +10 -0
  119. package/dist/types/utils/order-summary.d.ts +34 -0
  120. package/elements.catalogue.json +91 -22
  121. package/package.json +12 -6
  122. package/dist/ticketlayer-elements/p-0b1d16f8.entry.js +0 -1
  123. package/dist/ticketlayer-elements/p-1471f6d8.entry.js +0 -1
  124. package/dist/ticketlayer-elements/p-5169f68b.entry.js +0 -1
  125. package/dist/ticketlayer-elements/p-8d5722e1.entry.js +0 -1
  126. package/dist/ticketlayer-elements/p-917d8e09.entry.js +0 -1
  127. package/dist/ticketlayer-elements/p-BGekdjN9.js +0 -1
  128. package/dist/ticketlayer-elements/p-CNX8hZa8.js +0 -3
  129. package/dist/ticketlayer-elements/p-a22714d0.entry.js +0 -1
  130. package/dist/ticketlayer-elements/p-bf73044b.entry.js +0 -1
  131. package/dist/ticketlayer-elements/p-e04ace15.entry.js +0 -1
  132. package/dist/ticketlayer-elements/p-e6fab1d5.entry.js +0 -1
  133. package/dist/ticketlayer-elements/p-f826b753.entry.js +0 -1
  134. package/dist/ticketlayer-elements/p-fbaca948.entry.js +0 -1
@@ -1,7 +1,398 @@
1
- import { r as registerInstance, c as createEvent, h } from './index-CNX8hZa8.js';
2
- import { w as whenSDK, g as getSDK, h as hasSDK } from './sdk-connector-CqhvfyKV.js';
1
+ import { r as registerInstance, c as createEvent, h } from './index-OoYV1faY.js';
2
+ import { w as whenSDK, g as getSDK } from './sdk-connector-CqhvfyKV.js';
3
+ import { s as selectionTotals } from './money-4ig0-WB6.js';
3
4
 
4
- 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}`;
5
+ const CATEGORIES = [
6
+ 'discovery',
7
+ 'purchase',
8
+ 'cart',
9
+ 'checkout',
10
+ 'account',
11
+ 'tickets',
12
+ ];
13
+ const RUNTIMES = ['web', 'native'];
14
+ /**
15
+ * Declare a core. The checks here are the ones a type cannot make: a tag that
16
+ * is not a `tl-` custom element name, a category or runtime outside the set the
17
+ * catalogue knows, a duplicated token, a status list that does not include what
18
+ * `status` can return. They run at import time, so a malformed core fails the
19
+ * build rather than a browser.
20
+ */
21
+ function defineElement(core) {
22
+ const fail = (message) => {
23
+ throw new Error(`[elements-core] ${core.tag || '<no tag>'}: ${message}`);
24
+ };
25
+ if (!/^tl-[a-z0-9]+(-[a-z0-9]+)*$/.test(core.tag))
26
+ fail('tag must look like tl-something');
27
+ if (!CATEGORIES.includes(core.category))
28
+ fail(`category must be one of ${CATEGORIES.join(', ')}`);
29
+ if (!core.runtimes.length || core.runtimes.some((r) => !RUNTIMES.includes(r))) {
30
+ fail(`runtimes must be a non-empty subset of ${RUNTIMES.join(', ')}`);
31
+ }
32
+ if (new Set(core.tokens).size !== core.tokens.length)
33
+ fail('tokens must not repeat');
34
+ if (!core.statuses.length)
35
+ fail('statuses must not be empty');
36
+ for (const [name, spec] of Object.entries(core.events)) {
37
+ if (!/^tl[A-Z]/.test(name))
38
+ fail(`event ${name} must be named tlSomething`);
39
+ if (!spec.detail)
40
+ fail(`event ${name} must state its detail type`);
41
+ }
42
+ for (const [name, spec] of Object.entries(core.props)) {
43
+ if (spec.required && spec.default !== undefined)
44
+ fail(`prop ${name} is required and has a default`);
45
+ }
46
+ return Object.freeze({ ...core });
47
+ }
48
+ const DEFAULT_LOCALE = 'en-GB';
49
+ const DEFAULT_CURRENCY = 'GBP';
50
+ /**
51
+ * Instantiate a core. The instance owns the state and the timers; the harness
52
+ * owns the pixels. `subscribe` fires after every change with the whole view, so
53
+ * a harness only has to assign it to whatever makes its runtime re-render.
54
+ */
55
+ function createElement(core, options = {}) {
56
+ const defaults = {};
57
+ for (const [name, spec] of Object.entries(core.props)) {
58
+ if (spec.default !== undefined)
59
+ defaults[name] = spec.default;
60
+ }
61
+ let props = { ...defaults, ...(options.props ?? {}) };
62
+ const strings = { ...core.strings, ...(options.strings ?? {}) };
63
+ const locale = options.locale ?? DEFAULT_LOCALE;
64
+ const fallbackCurrency = options.currency ?? DEFAULT_CURRENCY;
65
+ let resolveClient = toResolver(options.client);
66
+ let state = core.state(props);
67
+ let destroyed = false;
68
+ const timers = new Set();
69
+ const listeners = new Set();
70
+ const formatMoney = (amountInMinorUnits, currency) => new Intl.NumberFormat(locale, {
71
+ style: 'currency',
72
+ currency: currency || fallbackCurrency,
73
+ }).format(amountInMinorUnits / 100);
74
+ const viewContext = () => ({ props, state, strings, formatMoney });
75
+ const buildView = () => {
76
+ const ctx = viewContext();
77
+ return {
78
+ status: core.status(ctx),
79
+ props,
80
+ state,
81
+ strings,
82
+ derived: core.derive(ctx),
83
+ };
84
+ };
85
+ const notify = () => {
86
+ if (destroyed)
87
+ return;
88
+ const view = buildView();
89
+ for (const listener of [...listeners])
90
+ listener(view);
91
+ };
92
+ const actionContext = {
93
+ get props() {
94
+ return props;
95
+ },
96
+ get state() {
97
+ return state;
98
+ },
99
+ strings,
100
+ formatMoney,
101
+ setState(patch) {
102
+ if (destroyed)
103
+ return;
104
+ const next = typeof patch === 'function' ? patch(state) : patch;
105
+ state = { ...state, ...next };
106
+ notify();
107
+ },
108
+ emit(event, detail) {
109
+ if (!core.events[event]) {
110
+ throw new Error(`[elements-core] ${core.tag}: ${event} is not a declared event`);
111
+ }
112
+ options.emit?.(event, detail);
113
+ },
114
+ client() {
115
+ const client = resolveClient();
116
+ if (!client) {
117
+ throw new Error(`[elements-core] ${core.tag}: no client. A harness hands the core the client the host published.`);
118
+ }
119
+ return client;
120
+ },
121
+ optionalClient() {
122
+ try {
123
+ return resolveClient();
124
+ }
125
+ catch {
126
+ return null;
127
+ }
128
+ },
129
+ after(ms, run) {
130
+ if (destroyed)
131
+ return;
132
+ const handle = setTimeout(() => {
133
+ timers.delete(handle);
134
+ if (!destroyed)
135
+ run();
136
+ }, ms);
137
+ timers.add(handle);
138
+ },
139
+ };
140
+ const actions = core.actions(actionContext);
141
+ return {
142
+ core,
143
+ get props() {
144
+ return props;
145
+ },
146
+ get state() {
147
+ return state;
148
+ },
149
+ get view() {
150
+ return buildView();
151
+ },
152
+ actions,
153
+ setProps(next) {
154
+ if (destroyed)
155
+ return;
156
+ props = { ...props, ...next };
157
+ notify();
158
+ },
159
+ setClient(client) {
160
+ resolveClient = toResolver(client);
161
+ },
162
+ subscribe(listener) {
163
+ listeners.add(listener);
164
+ return () => {
165
+ listeners.delete(listener);
166
+ };
167
+ },
168
+ destroy() {
169
+ destroyed = true;
170
+ for (const handle of timers)
171
+ clearTimeout(handle);
172
+ timers.clear();
173
+ listeners.clear();
174
+ },
175
+ };
176
+ }
177
+ function toResolver(client) {
178
+ if (typeof client === 'function')
179
+ return client;
180
+ return () => client ?? null;
181
+ }
182
+
183
+ /**
184
+ * `tl-ticket-selector`: the reference core.
185
+ *
186
+ * This is the whole of what the element does. The Stencil harness in
187
+ * `packages/elements/src/components/tl-ticket-selector/` renders `view` and
188
+ * forwards the three `tl*` events; it decides nothing, holds no copy and does
189
+ * no arithmetic. A React Native harness will render the same view.
190
+ *
191
+ * Behaviour is the behaviour the Stencil component had before the split, to
192
+ * the letter: the same quantity clamping, the same reset after a successful
193
+ * add, the same three second success message, the same button text, and the
194
+ * same rule that no fee row is shown until the surface quotes a fee. The one
195
+ * thing that did not come across is the `console.log` tracing, which a library
196
+ * should not do for its host.
197
+ */
198
+ const STRINGS = {
199
+ selectTickets: 'Select Tickets',
200
+ adding: 'Adding...',
201
+ added: 'Added to cart!',
202
+ limited: 'Limited',
203
+ soldOut: 'Sold Out',
204
+ ticketsRow: 'Tickets',
205
+ totalRow: 'Total',
206
+ decrease: 'Decrease quantity',
207
+ increase: 'Increase quantity',
208
+ loadFailed: 'Failed to load tickets',
209
+ addFailed: 'Failed to add to cart',
210
+ noClient: 'SDK not initialized.',
211
+ };
212
+ /** How long the "Added to cart!" message stays up. */
213
+ const SUCCESS_MS = 3000;
214
+ const totalsOf = (ctx) => selectionTotals(ctx.state.ticketTypes.map((tt) => ({ price: tt.price, quantity: ctx.state.quantities[tt.id] || 0 })), ctx.state.feeQuote);
215
+ const totalItemsOf = (state) => Object.values(state.quantities).reduce((sum, qty) => sum + qty, 0);
216
+ const ticketSelectorCore = defineElement({
217
+ tag: 'tl-ticket-selector',
218
+ docs: 'Pick quantities of the ticket types on sale for one occurrence, and add them to the cart.',
219
+ category: 'purchase',
220
+ runtimes: ['web', 'native'],
221
+ tokens: [
222
+ 'background',
223
+ 'border',
224
+ 'border-hover',
225
+ 'error',
226
+ 'error-bg',
227
+ 'font-family',
228
+ 'foreground',
229
+ 'muted',
230
+ 'muted-foreground',
231
+ 'primary',
232
+ 'primary-foreground',
233
+ 'radius-md',
234
+ 'radius-sm',
235
+ 'spacing-1',
236
+ 'spacing-2',
237
+ 'spacing-3',
238
+ 'spacing-4',
239
+ 'success',
240
+ 'warning',
241
+ 'warning-bg',
242
+ ],
243
+ sdkMethods: ['cart.addItem', 'events.getTicketTypes'],
244
+ props: {
245
+ eventId: { type: 'string', attr: 'event-id', required: true, docs: 'Event ID' },
246
+ occurrenceId: { type: 'string', attr: 'occurrence-id', required: true, docs: 'Occurrence ID' },
247
+ showButton: { type: 'boolean', attr: 'show-button', default: true, docs: 'Show add to cart button' },
248
+ buttonLabel: { type: 'string', attr: 'button-label', default: 'Add to Cart', docs: 'Button label' },
249
+ },
250
+ events: {
251
+ tlAddToCart: {
252
+ detail: '{ items: AddToCartItem[]; }',
253
+ docs: 'Emitted when items are added to cart',
254
+ },
255
+ tlQuantityChange: {
256
+ detail: '{ ticketTypeId: string; quantity: number; total: number; }',
257
+ docs: 'Emitted when quantity changes',
258
+ },
259
+ tlError: {
260
+ detail: '{ message: string; }',
261
+ docs: 'Emitted on error',
262
+ },
263
+ },
264
+ strings: STRINGS,
265
+ statuses: ['loading', 'error', 'ready'],
266
+ state: () => ({
267
+ ticketTypes: [],
268
+ quantities: {},
269
+ loading: true,
270
+ error: null,
271
+ adding: false,
272
+ addSuccess: false,
273
+ feeQuote: null,
274
+ }),
275
+ status: ({ state }) => (state.loading ? 'loading' : state.error ? 'error' : 'ready'),
276
+ derive: (ctx) => {
277
+ const { state, strings, props, formatMoney } = ctx;
278
+ const totals = totalsOf(ctx);
279
+ const totalItems = totalItemsOf(state);
280
+ const currency = state.ticketTypes[0]?.currency;
281
+ const rows = state.ticketTypes.map((tt) => {
282
+ const quantity = state.quantities[tt.id] || 0;
283
+ const soldOut = tt.availabilityStatus === 'sold_out';
284
+ return {
285
+ id: tt.id,
286
+ name: tt.name,
287
+ description: tt.description ?? null,
288
+ priceLabel: formatMoney(tt.price, tt.currency),
289
+ quantity,
290
+ soldOut,
291
+ limited: tt.availabilityStatus === 'limited',
292
+ canDecrease: !soldOut && quantity > 0,
293
+ canIncrease: !soldOut && quantity < tt.maxPerOrder,
294
+ };
295
+ });
296
+ return {
297
+ rows,
298
+ totals,
299
+ totalItems,
300
+ currency,
301
+ subtotalLabel: formatMoney(totals.subtotal, currency),
302
+ feeLabel: formatMoney(totals.fees, currency),
303
+ totalLabel: formatMoney(totals.total, currency),
304
+ showTotals: totals.showFee && totalItems > 0,
305
+ actionLabel: state.adding
306
+ ? strings.adding
307
+ : totalItems === 0
308
+ ? strings.selectTickets
309
+ : `${props.buttonLabel} - ${formatMoney(totals.total)}`,
310
+ actionDisabled: totalItems === 0 || state.adding,
311
+ };
312
+ },
313
+ actions: (ctx) => ({
314
+ /** Read the ticket types for the current event and occurrence. */
315
+ async load() {
316
+ try {
317
+ ctx.setState({ loading: true, error: null });
318
+ const types = (await ctx.client().getEventsManager().getTicketTypes(ctx.props.eventId, ctx.props.occurrenceId));
319
+ const quantities = {};
320
+ for (const type of types)
321
+ quantities[type.id] = 0;
322
+ ctx.setState({ ticketTypes: types, quantities });
323
+ }
324
+ catch (err) {
325
+ const message = err instanceof Error ? err.message : ctx.strings.loadFailed;
326
+ ctx.setState({ error: message });
327
+ ctx.emit('tlError', { message });
328
+ }
329
+ finally {
330
+ ctx.setState({ loading: false });
331
+ }
332
+ },
333
+ /** Nudge one row's quantity, clamped to 0 and the type's maxPerOrder. */
334
+ changeQuantity(ticketTypeId, delta) {
335
+ const ticketType = ctx.state.ticketTypes.find((t) => t.id === ticketTypeId);
336
+ if (!ticketType)
337
+ return;
338
+ const current = ctx.state.quantities[ticketTypeId] || 0;
339
+ const quantity = Math.max(0, Math.min(ticketType.maxPerOrder, current + delta));
340
+ ctx.setState((state) => ({ quantities: { ...state.quantities, [ticketTypeId]: quantity } }));
341
+ // After setState, so the total is the one the buyer can now see.
342
+ ctx.emit('tlQuantityChange', { ticketTypeId, quantity, total: totalsOf(ctx).total });
343
+ },
344
+ /**
345
+ * Add every selected line to the cart, then clear the selection.
346
+ *
347
+ * With no client, or a client too old to carry a cart manager, the
348
+ * selection is still cleared and `tlAddToCart` still fires: a host that
349
+ * listens for the event and does its own cart keeps working.
350
+ */
351
+ async addToCart() {
352
+ const items = Object.entries(ctx.state.quantities)
353
+ .filter(([, quantity]) => quantity > 0)
354
+ .map(([ticketTypeId, quantity]) => ({ ticketTypeId, quantity }));
355
+ if (items.length === 0)
356
+ return;
357
+ ctx.setState({ adding: true, addSuccess: false });
358
+ try {
359
+ const cart = ctx.optionalClient()?.getCartManager?.();
360
+ if (cart) {
361
+ for (const item of items) {
362
+ await cart.addItem(item.ticketTypeId, item.quantity, ctx.props.occurrenceId);
363
+ }
364
+ }
365
+ const quantities = {};
366
+ for (const type of ctx.state.ticketTypes)
367
+ quantities[type.id] = 0;
368
+ ctx.setState({ quantities, addSuccess: true });
369
+ ctx.after(SUCCESS_MS, () => ctx.setState({ addSuccess: false }));
370
+ ctx.emit('tlAddToCart', { items });
371
+ }
372
+ catch (err) {
373
+ const message = err instanceof Error ? err.message : ctx.strings.addFailed;
374
+ ctx.setState({ error: message });
375
+ ctx.emit('tlError', { message });
376
+ }
377
+ finally {
378
+ ctx.setState({ adding: false });
379
+ }
380
+ },
381
+ /** What the surface quoted for this selection, when it ever quotes one. */
382
+ setFeeQuote(quote) {
383
+ ctx.setState({ feeQuote: quote });
384
+ },
385
+ /**
386
+ * The host never produced a client. Shown, not emitted: the element has
387
+ * nothing to say to a page that has not started Live yet.
388
+ */
389
+ connectFailed(message) {
390
+ ctx.setState({ error: message || ctx.strings.noClient, loading: false });
391
+ },
392
+ }),
393
+ });
394
+
395
+ 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}`;
5
396
 
6
397
  const TlOccurrenceSelector = class {
7
398
  constructor(hostRef) {
@@ -569,7 +960,7 @@ const TlOccurrenceSelector = class {
569
960
  };
570
961
  TlOccurrenceSelector.style = tlOccurrenceSelectorCss();
571
962
 
572
- 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}`;
963
+ 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)}`;
573
964
 
574
965
  const TlTicketSelector = class {
575
966
  constructor(hostRef) {
@@ -585,19 +976,49 @@ const TlTicketSelector = class {
585
976
  * Button label
586
977
  */
587
978
  this.buttonLabel = 'Add to Cart';
588
- this.ticketTypes = [];
589
- this.quantities = {};
590
- this.loading = true;
591
- this.error = null;
592
- this.adding = false;
593
- this.addSuccess = false;
979
+ this.element = createElement(ticketSelectorCore, {
980
+ client: () => getSDK(),
981
+ emit: (event, detail) => this.forward(event, detail),
982
+ });
983
+ /** The core's whole view. Assigning it is what makes Stencil re-render. */
984
+ this.view = this.element.view;
985
+ }
986
+ /**
987
+ * The fee the surface has quoted for this selection. Core state, exposed here
988
+ * because it is set from outside the element and not from an attribute: there
989
+ * is no quote operation on the buyer-facing surface yet, so it stays null and
990
+ * no fee row is rendered.
991
+ */
992
+ get feeQuote() {
993
+ return this.element.state.feeQuote;
994
+ }
995
+ set feeQuote(quote) {
996
+ this.element.actions.setFeeQuote(quote);
594
997
  }
595
998
  watchPropsChange() {
596
- this.loadTicketTypes();
999
+ this.syncProps();
1000
+ this.element.actions.load();
1001
+ }
1002
+ syncProps() {
1003
+ this.element.setProps({
1004
+ eventId: this.eventId,
1005
+ occurrenceId: this.occurrenceId,
1006
+ showButton: this.showButton,
1007
+ buttonLabel: this.buttonLabel,
1008
+ });
597
1009
  }
598
1010
  async componentWillLoad() {
1011
+ this.syncProps();
1012
+ this.unsubscribe = this.element.subscribe((view) => {
1013
+ this.view = view;
1014
+ });
1015
+ this.view = this.element.view;
599
1016
  await this.waitForSDKAndLoad();
600
1017
  }
1018
+ disconnectedCallback() {
1019
+ this.unsubscribe?.();
1020
+ this.element.destroy();
1021
+ }
601
1022
  /**
602
1023
  * Wait for SDK to be ready, then load ticket types
603
1024
  */
@@ -607,135 +1028,40 @@ const TlTicketSelector = class {
607
1028
  }
608
1029
  try {
609
1030
  await whenSDK();
610
- await this.loadTicketTypes();
611
- }
612
- catch (err) {
613
- this.error = err instanceof Error ? err.message : 'SDK not initialized.';
614
- this.loading = false;
615
- }
616
- }
617
- async loadTicketTypes() {
618
- try {
619
- console.log('[tl-ticket-selector] Loading ticket types...', {
620
- eventId: this.eventId,
621
- occurrenceId: this.occurrenceId
622
- });
623
- this.loading = true;
624
- this.error = null;
625
- const sdk = getSDK();
626
- const eventsManager = sdk.getEventsManager();
627
- const types = await eventsManager.getTicketTypes(this.eventId, this.occurrenceId);
628
- console.log('[tl-ticket-selector] Ticket types loaded:', types.length);
629
- this.ticketTypes = types;
630
- // Initialise quantities to 0
631
- const initialQuantities = {};
632
- types.forEach((t) => {
633
- initialQuantities[t.id] = 0;
634
- });
635
- this.quantities = initialQuantities;
1031
+ await this.element.actions.load();
636
1032
  }
637
1033
  catch (err) {
638
- console.error('[tl-ticket-selector] Failed to load ticket types:', err);
639
- this.error = err instanceof Error ? err.message : 'Failed to load tickets';
640
- this.tlError.emit({ message: this.error });
641
- }
642
- finally {
643
- this.loading = false;
1034
+ this.element.actions.connectFailed(err instanceof Error ? err.message : this.view.strings.noClient);
644
1035
  }
645
1036
  }
646
- handleQuantityChange(ticketTypeId, delta) {
647
- const ticketType = this.ticketTypes.find(t => t.id === ticketTypeId);
648
- if (!ticketType)
649
- return;
650
- const currentQty = this.quantities[ticketTypeId] || 0;
651
- const newQty = Math.max(0, Math.min(ticketType.maxPerOrder, currentQty + delta));
652
- this.quantities = {
653
- ...this.quantities,
654
- [ticketTypeId]: newQty,
655
- };
656
- const total = this.calculateTotal();
657
- this.tlQuantityChange.emit({ ticketTypeId, quantity: newQty, total });
658
- }
659
- calculateTotal() {
660
- return this.ticketTypes.reduce((sum, tt) => {
661
- return sum + (this.quantities[tt.id] || 0) * tt.price;
662
- }, 0);
663
- }
664
- getTotalItems() {
665
- return Object.values(this.quantities).reduce((sum, qty) => sum + qty, 0);
666
- }
667
- async handleAddToCart() {
668
- const itemsToAdd = Object.entries(this.quantities)
669
- .filter(([_, qty]) => qty > 0)
670
- .map(([ticketTypeId, quantity]) => ({ ticketTypeId, quantity }));
671
- if (itemsToAdd.length === 0)
672
- return;
673
- this.adding = true;
674
- this.addSuccess = false;
675
- try {
676
- if (hasSDK()) {
677
- const sdk = getSDK();
678
- const cartManager = sdk.getCartManager?.();
679
- if (cartManager) {
680
- for (const item of itemsToAdd) {
681
- await cartManager.addItem(item.ticketTypeId, item.quantity, this.occurrenceId);
682
- }
683
- console.log('[tl-ticket-selector] Items added to cart');
684
- }
685
- }
686
- // Reset quantities after adding
687
- const resetQuantities = {};
688
- this.ticketTypes.forEach(t => {
689
- resetQuantities[t.id] = 0;
690
- });
691
- this.quantities = resetQuantities;
692
- this.addSuccess = true;
693
- // Clear success message after 3 seconds
694
- setTimeout(() => {
695
- this.addSuccess = false;
696
- }, 3000);
697
- this.tlAddToCart.emit({ items: itemsToAdd });
698
- }
699
- catch (err) {
700
- console.error('[tl-ticket-selector] Failed to add to cart:', err);
701
- this.error = err instanceof Error ? err.message : 'Failed to add to cart';
702
- this.tlError.emit({ message: this.error });
703
- }
704
- finally {
705
- this.adding = false;
1037
+ forward(event, detail) {
1038
+ if (event === 'tlAddToCart')
1039
+ this.tlAddToCart.emit(detail);
1040
+ else if (event === 'tlQuantityChange') {
1041
+ this.tlQuantityChange.emit(detail);
706
1042
  }
707
- }
708
- formatPrice(priceInPence, currency = 'GBP') {
709
- return new Intl.NumberFormat('en-GB', {
710
- style: 'currency',
711
- currency,
712
- }).format(priceInPence / 100);
1043
+ else if (event === 'tlError')
1044
+ this.tlError.emit(detail);
713
1045
  }
714
1046
  renderSkeleton() {
715
1047
  return (h("div", { class: "skeleton-container" }, h("div", { class: "skeleton-row" }), h("div", { class: "skeleton-row" }), h("div", { class: "skeleton-row" })));
716
1048
  }
717
1049
  renderError() {
718
- return (h("div", { class: "error-state" }, h("p", { class: "error-message" }, this.error)));
1050
+ return (h("div", { class: "error-state" }, h("p", { class: "error-message" }, this.view.state.error)));
719
1051
  }
720
- renderTicketRow(tt) {
721
- const isSoldOut = tt.availabilityStatus === 'sold_out';
722
- const qty = this.quantities[tt.id] || 0;
723
- return (h("div", { class: `ticket-row ${isSoldOut ? 'sold-out' : ''}`, key: tt.id }, h("div", { class: "ticket-info" }, h("div", { class: "ticket-header" }, h("span", { class: "ticket-name" }, tt.name), tt.availabilityStatus === 'limited' && (h("span", { class: "status-badge limited" }, "Limited")), isSoldOut && (h("span", { class: "status-badge sold-out" }, "Sold Out"))), tt.description && (h("p", { class: "ticket-description" }, tt.description)), h("p", { class: "ticket-price" }, this.formatPrice(tt.price, tt.currency))), h("div", { class: "quantity-controls" }, h("button", { class: "qty-btn", onClick: () => this.handleQuantityChange(tt.id, -1), disabled: isSoldOut || qty === 0, "aria-label": "Decrease quantity" }, "\u2212"), h("span", { class: "qty-value" }, qty), h("button", { class: "qty-btn", onClick: () => this.handleQuantityChange(tt.id, 1), disabled: isSoldOut || qty >= tt.maxPerOrder, "aria-label": "Increase quantity" }, "+"))));
1052
+ renderTicketRow(row) {
1053
+ const strings = this.view.strings;
1054
+ return (h("div", { class: `ticket-row ${row.soldOut ? 'sold-out' : ''}`, key: row.id }, h("div", { class: "ticket-info" }, h("div", { class: "ticket-header" }, h("span", { class: "ticket-name" }, row.name), row.limited && h("span", { class: "status-badge limited" }, strings.limited), row.soldOut && h("span", { class: "status-badge sold-out" }, strings.soldOut)), row.description && h("p", { class: "ticket-description" }, row.description), h("p", { class: "ticket-price" }, row.priceLabel)), h("div", { class: "quantity-controls" }, h("button", { class: "qty-btn", onClick: () => this.element.actions.changeQuantity(row.id, -1), disabled: !row.canDecrease, "aria-label": strings.decrease }, "\u2212"), h("span", { class: "qty-value" }, row.quantity), h("button", { class: "qty-btn", onClick: () => this.element.actions.changeQuantity(row.id, 1), disabled: !row.canIncrease, "aria-label": strings.increase }, "+"))));
724
1055
  }
725
1056
  render() {
726
- if (this.loading) {
1057
+ const { status, state, strings, derived } = this.view;
1058
+ if (status === 'loading') {
727
1059
  return this.renderSkeleton();
728
1060
  }
729
- if (this.error) {
1061
+ if (status === 'error') {
730
1062
  return this.renderError();
731
1063
  }
732
- const totalItems = this.getTotalItems();
733
- const totalPrice = this.calculateTotal();
734
- return (h("div", { class: "ticket-selector" }, h("div", { class: "ticket-list" }, this.ticketTypes.map(tt => this.renderTicketRow(tt))), this.addSuccess && (h("div", { class: "success-message" }, h("svg", { class: "success-icon", viewBox: "0 0 24 24", fill: "none", stroke: "currentColor" }, h("path", { "stroke-linecap": "round", "stroke-linejoin": "round", "stroke-width": "2", d: "M5 13l4 4L19 7" })), "Added to cart!")), this.showButton && (h("button", { class: `add-to-cart-btn ${totalItems === 0 ? 'disabled' : ''}`, onClick: () => this.handleAddToCart(), disabled: totalItems === 0 || this.adding }, this.adding
735
- ? 'Adding...'
736
- : totalItems === 0
737
- ? 'Select Tickets'
738
- : `${this.buttonLabel} - ${this.formatPrice(totalPrice)}`))));
1064
+ return (h("div", { class: "ticket-selector" }, h("div", { class: "ticket-list" }, derived.rows.map((row) => this.renderTicketRow(row))), derived.showTotals && (h("div", { class: "selection-totals" }, h("div", { class: "total-row" }, h("span", null, strings.ticketsRow), h("span", null, derived.subtotalLabel)), h("div", { class: "total-row fee" }, h("span", null, derived.totals.feeLabel), h("span", null, derived.feeLabel)), h("div", { class: "total-row total" }, h("span", null, strings.totalRow), h("span", null, derived.totalLabel)))), state.addSuccess && (h("div", { class: "success-message" }, h("svg", { class: "success-icon", viewBox: "0 0 24 24", fill: "none", stroke: "currentColor" }, h("path", { "stroke-linecap": "round", "stroke-linejoin": "round", "stroke-width": "2", d: "M5 13l4 4L19 7" })), strings.added)), this.showButton && (h("button", { class: `add-to-cart-btn ${derived.totalItems === 0 ? 'disabled' : ''}`, onClick: () => this.element.actions.addToCart(), disabled: derived.actionDisabled }, derived.actionLabel))));
739
1065
  }
740
1066
  static get watchers() { return {
741
1067
  "eventId": [{
@@ -743,6 +1069,12 @@ const TlTicketSelector = class {
743
1069
  }],
744
1070
  "occurrenceId": [{
745
1071
  "watchPropsChange": 0
1072
+ }],
1073
+ "showButton": [{
1074
+ "syncProps": 0
1075
+ }],
1076
+ "buttonLabel": [{
1077
+ "syncProps": 0
746
1078
  }]
747
1079
  }; }
748
1080
  };