@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,9 +1,400 @@
1
1
  'use strict';
2
2
 
3
- var index = require('./index-BpfoKd9Q.js');
3
+ var index = require('./index-I9CoYv3k.js');
4
4
  var sdkConnector = require('./sdk-connector-ClDF3013.js');
5
+ var money = require('./money-BtmM2gt-.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 CATEGORIES = [
8
+ 'discovery',
9
+ 'purchase',
10
+ 'cart',
11
+ 'checkout',
12
+ 'account',
13
+ 'tickets',
14
+ ];
15
+ const RUNTIMES = ['web', 'native'];
16
+ /**
17
+ * Declare a core. The checks here are the ones a type cannot make: a tag that
18
+ * is not a `tl-` custom element name, a category or runtime outside the set the
19
+ * catalogue knows, a duplicated token, a status list that does not include what
20
+ * `status` can return. They run at import time, so a malformed core fails the
21
+ * build rather than a browser.
22
+ */
23
+ function defineElement(core) {
24
+ const fail = (message) => {
25
+ throw new Error(`[elements-core] ${core.tag || '<no tag>'}: ${message}`);
26
+ };
27
+ if (!/^tl-[a-z0-9]+(-[a-z0-9]+)*$/.test(core.tag))
28
+ fail('tag must look like tl-something');
29
+ if (!CATEGORIES.includes(core.category))
30
+ fail(`category must be one of ${CATEGORIES.join(', ')}`);
31
+ if (!core.runtimes.length || core.runtimes.some((r) => !RUNTIMES.includes(r))) {
32
+ fail(`runtimes must be a non-empty subset of ${RUNTIMES.join(', ')}`);
33
+ }
34
+ if (new Set(core.tokens).size !== core.tokens.length)
35
+ fail('tokens must not repeat');
36
+ if (!core.statuses.length)
37
+ fail('statuses must not be empty');
38
+ for (const [name, spec] of Object.entries(core.events)) {
39
+ if (!/^tl[A-Z]/.test(name))
40
+ fail(`event ${name} must be named tlSomething`);
41
+ if (!spec.detail)
42
+ fail(`event ${name} must state its detail type`);
43
+ }
44
+ for (const [name, spec] of Object.entries(core.props)) {
45
+ if (spec.required && spec.default !== undefined)
46
+ fail(`prop ${name} is required and has a default`);
47
+ }
48
+ return Object.freeze({ ...core });
49
+ }
50
+ const DEFAULT_LOCALE = 'en-GB';
51
+ const DEFAULT_CURRENCY = 'GBP';
52
+ /**
53
+ * Instantiate a core. The instance owns the state and the timers; the harness
54
+ * owns the pixels. `subscribe` fires after every change with the whole view, so
55
+ * a harness only has to assign it to whatever makes its runtime re-render.
56
+ */
57
+ function createElement(core, options = {}) {
58
+ const defaults = {};
59
+ for (const [name, spec] of Object.entries(core.props)) {
60
+ if (spec.default !== undefined)
61
+ defaults[name] = spec.default;
62
+ }
63
+ let props = { ...defaults, ...(options.props ?? {}) };
64
+ const strings = { ...core.strings, ...(options.strings ?? {}) };
65
+ const locale = options.locale ?? DEFAULT_LOCALE;
66
+ const fallbackCurrency = options.currency ?? DEFAULT_CURRENCY;
67
+ let resolveClient = toResolver(options.client);
68
+ let state = core.state(props);
69
+ let destroyed = false;
70
+ const timers = new Set();
71
+ const listeners = new Set();
72
+ const formatMoney = (amountInMinorUnits, currency) => new Intl.NumberFormat(locale, {
73
+ style: 'currency',
74
+ currency: currency || fallbackCurrency,
75
+ }).format(amountInMinorUnits / 100);
76
+ const viewContext = () => ({ props, state, strings, formatMoney });
77
+ const buildView = () => {
78
+ const ctx = viewContext();
79
+ return {
80
+ status: core.status(ctx),
81
+ props,
82
+ state,
83
+ strings,
84
+ derived: core.derive(ctx),
85
+ };
86
+ };
87
+ const notify = () => {
88
+ if (destroyed)
89
+ return;
90
+ const view = buildView();
91
+ for (const listener of [...listeners])
92
+ listener(view);
93
+ };
94
+ const actionContext = {
95
+ get props() {
96
+ return props;
97
+ },
98
+ get state() {
99
+ return state;
100
+ },
101
+ strings,
102
+ formatMoney,
103
+ setState(patch) {
104
+ if (destroyed)
105
+ return;
106
+ const next = typeof patch === 'function' ? patch(state) : patch;
107
+ state = { ...state, ...next };
108
+ notify();
109
+ },
110
+ emit(event, detail) {
111
+ if (!core.events[event]) {
112
+ throw new Error(`[elements-core] ${core.tag}: ${event} is not a declared event`);
113
+ }
114
+ options.emit?.(event, detail);
115
+ },
116
+ client() {
117
+ const client = resolveClient();
118
+ if (!client) {
119
+ throw new Error(`[elements-core] ${core.tag}: no client. A harness hands the core the client the host published.`);
120
+ }
121
+ return client;
122
+ },
123
+ optionalClient() {
124
+ try {
125
+ return resolveClient();
126
+ }
127
+ catch {
128
+ return null;
129
+ }
130
+ },
131
+ after(ms, run) {
132
+ if (destroyed)
133
+ return;
134
+ const handle = setTimeout(() => {
135
+ timers.delete(handle);
136
+ if (!destroyed)
137
+ run();
138
+ }, ms);
139
+ timers.add(handle);
140
+ },
141
+ };
142
+ const actions = core.actions(actionContext);
143
+ return {
144
+ core,
145
+ get props() {
146
+ return props;
147
+ },
148
+ get state() {
149
+ return state;
150
+ },
151
+ get view() {
152
+ return buildView();
153
+ },
154
+ actions,
155
+ setProps(next) {
156
+ if (destroyed)
157
+ return;
158
+ props = { ...props, ...next };
159
+ notify();
160
+ },
161
+ setClient(client) {
162
+ resolveClient = toResolver(client);
163
+ },
164
+ subscribe(listener) {
165
+ listeners.add(listener);
166
+ return () => {
167
+ listeners.delete(listener);
168
+ };
169
+ },
170
+ destroy() {
171
+ destroyed = true;
172
+ for (const handle of timers)
173
+ clearTimeout(handle);
174
+ timers.clear();
175
+ listeners.clear();
176
+ },
177
+ };
178
+ }
179
+ function toResolver(client) {
180
+ if (typeof client === 'function')
181
+ return client;
182
+ return () => client ?? null;
183
+ }
184
+
185
+ /**
186
+ * `tl-ticket-selector`: the reference core.
187
+ *
188
+ * This is the whole of what the element does. The Stencil harness in
189
+ * `packages/elements/src/components/tl-ticket-selector/` renders `view` and
190
+ * forwards the three `tl*` events; it decides nothing, holds no copy and does
191
+ * no arithmetic. A React Native harness will render the same view.
192
+ *
193
+ * Behaviour is the behaviour the Stencil component had before the split, to
194
+ * the letter: the same quantity clamping, the same reset after a successful
195
+ * add, the same three second success message, the same button text, and the
196
+ * same rule that no fee row is shown until the surface quotes a fee. The one
197
+ * thing that did not come across is the `console.log` tracing, which a library
198
+ * should not do for its host.
199
+ */
200
+ const STRINGS = {
201
+ selectTickets: 'Select Tickets',
202
+ adding: 'Adding...',
203
+ added: 'Added to cart!',
204
+ limited: 'Limited',
205
+ soldOut: 'Sold Out',
206
+ ticketsRow: 'Tickets',
207
+ totalRow: 'Total',
208
+ decrease: 'Decrease quantity',
209
+ increase: 'Increase quantity',
210
+ loadFailed: 'Failed to load tickets',
211
+ addFailed: 'Failed to add to cart',
212
+ noClient: 'SDK not initialized.',
213
+ };
214
+ /** How long the "Added to cart!" message stays up. */
215
+ const SUCCESS_MS = 3000;
216
+ const totalsOf = (ctx) => money.selectionTotals(ctx.state.ticketTypes.map((tt) => ({ price: tt.price, quantity: ctx.state.quantities[tt.id] || 0 })), ctx.state.feeQuote);
217
+ const totalItemsOf = (state) => Object.values(state.quantities).reduce((sum, qty) => sum + qty, 0);
218
+ const ticketSelectorCore = defineElement({
219
+ tag: 'tl-ticket-selector',
220
+ docs: 'Pick quantities of the ticket types on sale for one occurrence, and add them to the cart.',
221
+ category: 'purchase',
222
+ runtimes: ['web', 'native'],
223
+ tokens: [
224
+ 'background',
225
+ 'border',
226
+ 'border-hover',
227
+ 'error',
228
+ 'error-bg',
229
+ 'font-family',
230
+ 'foreground',
231
+ 'muted',
232
+ 'muted-foreground',
233
+ 'primary',
234
+ 'primary-foreground',
235
+ 'radius-md',
236
+ 'radius-sm',
237
+ 'spacing-1',
238
+ 'spacing-2',
239
+ 'spacing-3',
240
+ 'spacing-4',
241
+ 'success',
242
+ 'warning',
243
+ 'warning-bg',
244
+ ],
245
+ sdkMethods: ['cart.addItem', 'events.getTicketTypes'],
246
+ props: {
247
+ eventId: { type: 'string', attr: 'event-id', required: true, docs: 'Event ID' },
248
+ occurrenceId: { type: 'string', attr: 'occurrence-id', required: true, docs: 'Occurrence ID' },
249
+ showButton: { type: 'boolean', attr: 'show-button', default: true, docs: 'Show add to cart button' },
250
+ buttonLabel: { type: 'string', attr: 'button-label', default: 'Add to Cart', docs: 'Button label' },
251
+ },
252
+ events: {
253
+ tlAddToCart: {
254
+ detail: '{ items: AddToCartItem[]; }',
255
+ docs: 'Emitted when items are added to cart',
256
+ },
257
+ tlQuantityChange: {
258
+ detail: '{ ticketTypeId: string; quantity: number; total: number; }',
259
+ docs: 'Emitted when quantity changes',
260
+ },
261
+ tlError: {
262
+ detail: '{ message: string; }',
263
+ docs: 'Emitted on error',
264
+ },
265
+ },
266
+ strings: STRINGS,
267
+ statuses: ['loading', 'error', 'ready'],
268
+ state: () => ({
269
+ ticketTypes: [],
270
+ quantities: {},
271
+ loading: true,
272
+ error: null,
273
+ adding: false,
274
+ addSuccess: false,
275
+ feeQuote: null,
276
+ }),
277
+ status: ({ state }) => (state.loading ? 'loading' : state.error ? 'error' : 'ready'),
278
+ derive: (ctx) => {
279
+ const { state, strings, props, formatMoney } = ctx;
280
+ const totals = totalsOf(ctx);
281
+ const totalItems = totalItemsOf(state);
282
+ const currency = state.ticketTypes[0]?.currency;
283
+ const rows = state.ticketTypes.map((tt) => {
284
+ const quantity = state.quantities[tt.id] || 0;
285
+ const soldOut = tt.availabilityStatus === 'sold_out';
286
+ return {
287
+ id: tt.id,
288
+ name: tt.name,
289
+ description: tt.description ?? null,
290
+ priceLabel: formatMoney(tt.price, tt.currency),
291
+ quantity,
292
+ soldOut,
293
+ limited: tt.availabilityStatus === 'limited',
294
+ canDecrease: !soldOut && quantity > 0,
295
+ canIncrease: !soldOut && quantity < tt.maxPerOrder,
296
+ };
297
+ });
298
+ return {
299
+ rows,
300
+ totals,
301
+ totalItems,
302
+ currency,
303
+ subtotalLabel: formatMoney(totals.subtotal, currency),
304
+ feeLabel: formatMoney(totals.fees, currency),
305
+ totalLabel: formatMoney(totals.total, currency),
306
+ showTotals: totals.showFee && totalItems > 0,
307
+ actionLabel: state.adding
308
+ ? strings.adding
309
+ : totalItems === 0
310
+ ? strings.selectTickets
311
+ : `${props.buttonLabel} - ${formatMoney(totals.total)}`,
312
+ actionDisabled: totalItems === 0 || state.adding,
313
+ };
314
+ },
315
+ actions: (ctx) => ({
316
+ /** Read the ticket types for the current event and occurrence. */
317
+ async load() {
318
+ try {
319
+ ctx.setState({ loading: true, error: null });
320
+ const types = (await ctx.client().getEventsManager().getTicketTypes(ctx.props.eventId, ctx.props.occurrenceId));
321
+ const quantities = {};
322
+ for (const type of types)
323
+ quantities[type.id] = 0;
324
+ ctx.setState({ ticketTypes: types, quantities });
325
+ }
326
+ catch (err) {
327
+ const message = err instanceof Error ? err.message : ctx.strings.loadFailed;
328
+ ctx.setState({ error: message });
329
+ ctx.emit('tlError', { message });
330
+ }
331
+ finally {
332
+ ctx.setState({ loading: false });
333
+ }
334
+ },
335
+ /** Nudge one row's quantity, clamped to 0 and the type's maxPerOrder. */
336
+ changeQuantity(ticketTypeId, delta) {
337
+ const ticketType = ctx.state.ticketTypes.find((t) => t.id === ticketTypeId);
338
+ if (!ticketType)
339
+ return;
340
+ const current = ctx.state.quantities[ticketTypeId] || 0;
341
+ const quantity = Math.max(0, Math.min(ticketType.maxPerOrder, current + delta));
342
+ ctx.setState((state) => ({ quantities: { ...state.quantities, [ticketTypeId]: quantity } }));
343
+ // After setState, so the total is the one the buyer can now see.
344
+ ctx.emit('tlQuantityChange', { ticketTypeId, quantity, total: totalsOf(ctx).total });
345
+ },
346
+ /**
347
+ * Add every selected line to the cart, then clear the selection.
348
+ *
349
+ * With no client, or a client too old to carry a cart manager, the
350
+ * selection is still cleared and `tlAddToCart` still fires: a host that
351
+ * listens for the event and does its own cart keeps working.
352
+ */
353
+ async addToCart() {
354
+ const items = Object.entries(ctx.state.quantities)
355
+ .filter(([, quantity]) => quantity > 0)
356
+ .map(([ticketTypeId, quantity]) => ({ ticketTypeId, quantity }));
357
+ if (items.length === 0)
358
+ return;
359
+ ctx.setState({ adding: true, addSuccess: false });
360
+ try {
361
+ const cart = ctx.optionalClient()?.getCartManager?.();
362
+ if (cart) {
363
+ for (const item of items) {
364
+ await cart.addItem(item.ticketTypeId, item.quantity, ctx.props.occurrenceId);
365
+ }
366
+ }
367
+ const quantities = {};
368
+ for (const type of ctx.state.ticketTypes)
369
+ quantities[type.id] = 0;
370
+ ctx.setState({ quantities, addSuccess: true });
371
+ ctx.after(SUCCESS_MS, () => ctx.setState({ addSuccess: false }));
372
+ ctx.emit('tlAddToCart', { items });
373
+ }
374
+ catch (err) {
375
+ const message = err instanceof Error ? err.message : ctx.strings.addFailed;
376
+ ctx.setState({ error: message });
377
+ ctx.emit('tlError', { message });
378
+ }
379
+ finally {
380
+ ctx.setState({ adding: false });
381
+ }
382
+ },
383
+ /** What the surface quoted for this selection, when it ever quotes one. */
384
+ setFeeQuote(quote) {
385
+ ctx.setState({ feeQuote: quote });
386
+ },
387
+ /**
388
+ * The host never produced a client. Shown, not emitted: the element has
389
+ * nothing to say to a page that has not started Live yet.
390
+ */
391
+ connectFailed(message) {
392
+ ctx.setState({ error: message || ctx.strings.noClient, loading: false });
393
+ },
394
+ }),
395
+ });
396
+
397
+ 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
398
 
8
399
  const TlOccurrenceSelector = class {
9
400
  constructor(hostRef) {
@@ -571,7 +962,7 @@ const TlOccurrenceSelector = class {
571
962
  };
572
963
  TlOccurrenceSelector.style = tlOccurrenceSelectorCss();
573
964
 
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}`;
965
+ 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
966
 
576
967
  const TlTicketSelector = class {
577
968
  constructor(hostRef) {
@@ -587,19 +978,49 @@ const TlTicketSelector = class {
587
978
  * Button label
588
979
  */
589
980
  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;
981
+ this.element = createElement(ticketSelectorCore, {
982
+ client: () => sdkConnector.getSDK(),
983
+ emit: (event, detail) => this.forward(event, detail),
984
+ });
985
+ /** The core's whole view. Assigning it is what makes Stencil re-render. */
986
+ this.view = this.element.view;
987
+ }
988
+ /**
989
+ * The fee the surface has quoted for this selection. Core state, exposed here
990
+ * because it is set from outside the element and not from an attribute: there
991
+ * is no quote operation on the buyer-facing surface yet, so it stays null and
992
+ * no fee row is rendered.
993
+ */
994
+ get feeQuote() {
995
+ return this.element.state.feeQuote;
996
+ }
997
+ set feeQuote(quote) {
998
+ this.element.actions.setFeeQuote(quote);
596
999
  }
597
1000
  watchPropsChange() {
598
- this.loadTicketTypes();
1001
+ this.syncProps();
1002
+ this.element.actions.load();
1003
+ }
1004
+ syncProps() {
1005
+ this.element.setProps({
1006
+ eventId: this.eventId,
1007
+ occurrenceId: this.occurrenceId,
1008
+ showButton: this.showButton,
1009
+ buttonLabel: this.buttonLabel,
1010
+ });
599
1011
  }
600
1012
  async componentWillLoad() {
1013
+ this.syncProps();
1014
+ this.unsubscribe = this.element.subscribe((view) => {
1015
+ this.view = view;
1016
+ });
1017
+ this.view = this.element.view;
601
1018
  await this.waitForSDKAndLoad();
602
1019
  }
1020
+ disconnectedCallback() {
1021
+ this.unsubscribe?.();
1022
+ this.element.destroy();
1023
+ }
603
1024
  /**
604
1025
  * Wait for SDK to be ready, then load ticket types
605
1026
  */
@@ -609,135 +1030,40 @@ const TlTicketSelector = class {
609
1030
  }
610
1031
  try {
611
1032
  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;
1033
+ await this.element.actions.load();
638
1034
  }
639
1035
  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;
1036
+ this.element.actions.connectFailed(err instanceof Error ? err.message : this.view.strings.noClient);
646
1037
  }
647
1038
  }
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 });
660
- }
661
- calculateTotal() {
662
- return this.ticketTypes.reduce((sum, tt) => {
663
- return sum + (this.quantities[tt.id] || 0) * tt.price;
664
- }, 0);
665
- }
666
- getTotalItems() {
667
- return Object.values(this.quantities).reduce((sum, qty) => sum + qty, 0);
668
- }
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 });
700
- }
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;
1039
+ forward(event, detail) {
1040
+ if (event === 'tlAddToCart')
1041
+ this.tlAddToCart.emit(detail);
1042
+ else if (event === 'tlQuantityChange') {
1043
+ this.tlQuantityChange.emit(detail);
708
1044
  }
709
- }
710
- formatPrice(priceInPence, currency = 'GBP') {
711
- return new Intl.NumberFormat('en-GB', {
712
- style: 'currency',
713
- currency,
714
- }).format(priceInPence / 100);
1045
+ else if (event === 'tlError')
1046
+ this.tlError.emit(detail);
715
1047
  }
716
1048
  renderSkeleton() {
717
1049
  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
1050
  }
719
1051
  renderError() {
720
- return (index.h("div", { class: "error-state" }, index.h("p", { class: "error-message" }, this.error)));
1052
+ return (index.h("div", { class: "error-state" }, index.h("p", { class: "error-message" }, this.view.state.error)));
721
1053
  }
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" }, "+"))));
1054
+ renderTicketRow(row) {
1055
+ const strings = this.view.strings;
1056
+ 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
1057
  }
727
1058
  render() {
728
- if (this.loading) {
1059
+ const { status, state, strings, derived } = this.view;
1060
+ if (status === 'loading') {
729
1061
  return this.renderSkeleton();
730
1062
  }
731
- if (this.error) {
1063
+ if (status === 'error') {
732
1064
  return this.renderError();
733
1065
  }
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)}`))));
1066
+ 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
1067
  }
742
1068
  static get watchers() { return {
743
1069
  "eventId": [{
@@ -745,6 +1071,12 @@ const TlTicketSelector = class {
745
1071
  }],
746
1072
  "occurrenceId": [{
747
1073
  "watchPropsChange": 0
1074
+ }],
1075
+ "showButton": [{
1076
+ "syncProps": 0
1077
+ }],
1078
+ "buttonLabel": [{
1079
+ "syncProps": 0
748
1080
  }]
749
1081
  }; }
750
1082
  };