@session-services/web-elements 0.1.19 → 0.1.21

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 (32) hide show
  1. package/dist/cjs/{index-C6e98Atu.js → index-BYBgrATL.js} +1 -0
  2. package/dist/cjs/loader.cjs.js +2 -2
  3. package/dist/cjs/session-services-elements.cjs.js +2 -2
  4. package/dist/cjs/session-services-formatted-text.cjs.entry.js +181 -165
  5. package/dist/cjs/session-services-ticketing.cjs.entry.js +24398 -19366
  6. package/dist/collection/components/session-services-formatted-text/session-services-formatted-text.js +33 -34
  7. package/dist/collection/components/session-services-ticketing/session-services-ticketing.css +401 -339
  8. package/dist/collection/components/session-services-ticketing/session-services-ticketing.js +1087 -269
  9. package/dist/collection/components/session-services-ticketing/session-services-ticketing.logic.js +263 -0
  10. package/dist/collection/components/session-services-ticketing/session-services-ticketing.logic.test.js +276 -0
  11. package/dist/collection/components/session-services-ticketing/session-services-ticketing.test.js +264 -0
  12. package/dist/components/session-services-formatted-text.js +180 -164
  13. package/dist/components/session-services-ticketing.js +24442 -19392
  14. package/dist/esm/{index-CeXOR4aK.js → index-CNOpF6tk.js} +1 -1
  15. package/dist/esm/loader.js +3 -3
  16. package/dist/esm/session-services-elements.js +3 -3
  17. package/dist/esm/session-services-formatted-text.entry.js +181 -165
  18. package/dist/esm/session-services-ticketing.entry.js +24406 -19374
  19. package/dist/session-services-elements/{p-a2404c46.entry.js → p-703444ed.entry.js} +1 -1
  20. package/dist/session-services-elements/p-CNOpF6tk.js +2 -0
  21. package/dist/session-services-elements/p-b8703bf3.entry.js +1 -0
  22. package/dist/session-services-elements/session-services-elements.esm.js +1 -1
  23. package/dist/sst-env.d.ts +1 -0
  24. package/dist/types/components/session-services-formatted-text/session-services-formatted-text.d.ts +3 -0
  25. package/dist/types/components/session-services-ticketing/session-services-ticketing.d.ts +117 -3
  26. package/dist/types/components/session-services-ticketing/session-services-ticketing.logic.d.ts +193 -0
  27. package/dist/types/components/session-services-ticketing/session-services-ticketing.logic.test.d.ts +1 -0
  28. package/dist/types/components/session-services-ticketing/session-services-ticketing.test.d.ts +1 -0
  29. package/dist/types/components.d.ts +18 -0
  30. package/package.json +8 -7
  31. package/dist/session-services-elements/p-62a8e2cc.entry.js +0 -1
  32. package/dist/session-services-elements/p-CeXOR4aK.js +0 -2
@@ -0,0 +1,264 @@
1
+ import { describe, expect, it, vi } from "vitest";
2
+ import { SessionsTicketing } from "./session-services-ticketing";
3
+ import { selectionKey, } from "./session-services-ticketing.logic";
4
+ vi.mock("@stencil/core", () => ({
5
+ Component: () => () => undefined,
6
+ Element: () => () => undefined,
7
+ Event: () => () => undefined,
8
+ Host: "mock-host",
9
+ Prop: () => () => undefined,
10
+ State: () => () => undefined,
11
+ Watch: () => () => undefined,
12
+ h: () => undefined,
13
+ }));
14
+ const futureStart = "2030-01-01T20:00:00";
15
+ const futureEnd = "2030-01-01T23:00:00";
16
+ const pastStart = "2020-01-01T20:00:00";
17
+ function admission(data = {}) {
18
+ return {
19
+ id: "adm_general",
20
+ name: "General Admission",
21
+ capacity: 100,
22
+ available: 100,
23
+ sold: 0,
24
+ reserved: 0,
25
+ limit: 10,
26
+ order: 1,
27
+ entryStartsAt: futureStart,
28
+ entryEndsAt: futureEnd,
29
+ saleStartsAt: pastStart,
30
+ saleEndsAt: futureEnd,
31
+ saleEndsBehavior: "SALE_ENDED",
32
+ price: {
33
+ base: 2000,
34
+ booking: 200,
35
+ currency: "AUD",
36
+ },
37
+ visibility: "ALWAYS",
38
+ teamId: "team_1",
39
+ promoterId: "pro_1",
40
+ eventId: "evt_1",
41
+ eventSlug: "event",
42
+ ...data,
43
+ };
44
+ }
45
+ function event(data = {}) {
46
+ return {
47
+ id: "evt_1",
48
+ name: "Test Event",
49
+ slug: "event",
50
+ currency: "AUD",
51
+ entryStartsAt: futureStart,
52
+ entryEndsAt: futureEnd,
53
+ publishAt: pastStart,
54
+ updatedAt: pastStart,
55
+ status: "UPCOMING",
56
+ visibility: "PUBLIC",
57
+ approval: "APPROVED",
58
+ teamId: "team_1",
59
+ promoterId: "pro_1",
60
+ admissions: [admission()],
61
+ sections: [],
62
+ images: {},
63
+ location: {
64
+ id: "loc_1",
65
+ name: "Club",
66
+ address: "1 Test St",
67
+ city: "Sydney",
68
+ country: "AU",
69
+ latitude: -33.86,
70
+ longitude: 151.2,
71
+ timeZone: "Australia/Sydney",
72
+ createdAt: pastStart,
73
+ updatedAt: pastStart,
74
+ },
75
+ ...data,
76
+ };
77
+ }
78
+ function quote(data = {}) {
79
+ return {
80
+ items: [
81
+ {
82
+ id: "item_1",
83
+ kind: "ADMISSION",
84
+ admissionId: "adm_general",
85
+ quantity: 1,
86
+ price: {
87
+ base: 2000,
88
+ booking: 200,
89
+ currency: "AUD",
90
+ discount: 0,
91
+ gross: 2200,
92
+ net: 2200,
93
+ refunded: 0,
94
+ total: 2200,
95
+ },
96
+ },
97
+ ],
98
+ price: {
99
+ base: 2000,
100
+ booking: 200,
101
+ currency: "AUD",
102
+ discount: 0,
103
+ gross: 2200,
104
+ net: 2200,
105
+ refunded: 0,
106
+ total: 2200,
107
+ },
108
+ ...data,
109
+ };
110
+ }
111
+ function deferred() {
112
+ let resolve;
113
+ const promise = new Promise((done) => {
114
+ resolve = done;
115
+ });
116
+ return { promise, resolve };
117
+ }
118
+ function widget() {
119
+ const item = new SessionsTicketing();
120
+ item.eventId = "evt_1";
121
+ item.event = event();
122
+ item.selectedItems = new Map();
123
+ item.quote = null;
124
+ item.previewQuote = null;
125
+ item.quoteError = null;
126
+ item.couponError = null;
127
+ item.appliedCouponCode = "";
128
+ item.couponInput = "";
129
+ item.couponCode = undefined;
130
+ item.initialPreviewQuote = undefined;
131
+ item.quoteLoading = false;
132
+ item.previewLoading = false;
133
+ item.passcodeDenied = false;
134
+ item.error = null;
135
+ item.activePasscode = "";
136
+ item.cartUpdate = { emit: vi.fn() };
137
+ item.errorOccurred = { emit: vi.fn() };
138
+ item.client = {
139
+ event: {
140
+ get: vi.fn(),
141
+ },
142
+ order: {
143
+ create: vi.fn(),
144
+ quote: vi.fn(),
145
+ },
146
+ };
147
+ return item;
148
+ }
149
+ describe("session-services-ticketing component", () => {
150
+ it("clears stale cart quotes when requoting fails", async () => {
151
+ const item = widget();
152
+ item.quote = quote();
153
+ item.selectedItems = new Map([[selectionKey("adm_general", null), 2]]);
154
+ item.client.order.quote = vi.fn().mockRejectedValue(new Error("Down"));
155
+ await item.updateQuote();
156
+ expect(item.quote).toBeNull();
157
+ expect(item.quoteError).toBe("Failed to update cart. Please try again.");
158
+ expect(item.isCheckoutDisabled()).toBe(true);
159
+ });
160
+ it("does not commit failed cart coupon applications", async () => {
161
+ const item = widget();
162
+ const current = quote();
163
+ item.quote = current;
164
+ item.appliedCouponCode = "SAVE10";
165
+ item.couponInput = "bad";
166
+ item.selectedItems = new Map([[selectionKey("adm_general", null), 1]]);
167
+ item.client.order.quote = vi
168
+ .fn()
169
+ .mockRejectedValue(new Error("Invalid coupon"));
170
+ await item.applyCoupon();
171
+ expect(item.appliedCouponCode).toBe("SAVE10");
172
+ expect(item.couponInput).toBe("BAD");
173
+ expect(item.quote).toBe(current);
174
+ expect(item.couponError).toBe("Failed to apply coupon. Please check the code.");
175
+ expect(item.client.order.quote).toHaveBeenCalledWith(expect.objectContaining({ couponCode: "BAD" }));
176
+ });
177
+ it("keeps preview quote loading separate from cart quote loading", async () => {
178
+ const item = widget();
179
+ const next = deferred();
180
+ item.appliedCouponCode = "SAVE20";
181
+ item.client.order.quote = vi.fn().mockReturnValue(next.promise);
182
+ const pending = item.updatePreviewQuote();
183
+ await Promise.resolve();
184
+ expect(item.previewLoading).toBe(true);
185
+ expect(item.quoteLoading).toBe(false);
186
+ next.resolve({ quote: quote() });
187
+ await pending;
188
+ expect(item.previewLoading).toBe(false);
189
+ expect(item.previewQuote).not.toBeNull();
190
+ });
191
+ it("uses initial preview quotes without refetching", async () => {
192
+ const item = widget();
193
+ const initial = quote({
194
+ price: {
195
+ base: 2000,
196
+ booking: 200,
197
+ currency: "AUD",
198
+ discount: -1000,
199
+ dispute: 0,
200
+ gateway: 0,
201
+ gross: 2200,
202
+ net: 1200,
203
+ refunded: 0,
204
+ shipping: 0,
205
+ sms: 0,
206
+ total: 1200,
207
+ },
208
+ });
209
+ item.couponCode = "SAVE20";
210
+ item.appliedCouponCode = "SAVE20";
211
+ item.initialPreviewQuote = initial;
212
+ item.client.order.quote = vi.fn();
213
+ await item.updatePreviewQuote();
214
+ expect(item.previewQuote).toBe(initial);
215
+ expect(item.client.order.quote).not.toHaveBeenCalled();
216
+ });
217
+ it("debounces cart quotes after quantity changes", async () => {
218
+ vi.useFakeTimers();
219
+ const item = widget();
220
+ item.client.order.quote = vi.fn().mockResolvedValue({ quote: quote() });
221
+ await item.updateQuantity("adm_general", null, 1);
222
+ expect(item.quoteLoading).toBe(true);
223
+ expect(item.client.order.quote).not.toHaveBeenCalled();
224
+ await vi.advanceTimersByTimeAsync(150);
225
+ expect(item.client.order.quote).toHaveBeenCalledOnce();
226
+ expect(item.quote).not.toBeNull();
227
+ vi.useRealTimers();
228
+ });
229
+ it("labels free checkout quotes as continue", () => {
230
+ const item = widget();
231
+ item.quote = quote({
232
+ price: {
233
+ base: 0,
234
+ booking: 0,
235
+ currency: "AUD",
236
+ dispute: 0,
237
+ discount: 0,
238
+ gateway: 0,
239
+ gross: 0,
240
+ net: 0,
241
+ refunded: 0,
242
+ shipping: 0,
243
+ sms: 0,
244
+ total: 0,
245
+ },
246
+ });
247
+ expect(item.getCheckoutButtonText()).toBe("Continue");
248
+ });
249
+ it("classifies only passcode access failures as passcode errors", async () => {
250
+ const item = widget();
251
+ item.client.event.get = vi
252
+ .fn()
253
+ .mockRejectedValueOnce(Object.assign(new Error("Authentication required"), {
254
+ code: "UNAUTHORIZED",
255
+ }))
256
+ .mockRejectedValueOnce(Object.assign(new Error("Incorrect passcode"), {
257
+ code: "UNAUTHORIZED",
258
+ }));
259
+ await item.loadEventData();
260
+ expect(item.passcodeDenied).toBe(false);
261
+ await item.loadEventData();
262
+ expect(item.passcodeDenied).toBe(true);
263
+ });
264
+ });
@@ -1443,7 +1443,7 @@ var tlds = [
1443
1443
 
1444
1444
  const URL_WITH_PROTOCOL = /https?:\/\/[^\s<>"]+/gi;
1445
1445
  const EMAIL = /[a-z0-9._%+-]+@[a-z0-9.-]+\.[a-z]{2,}/gi;
1446
- const INSTAGRAM = /(?<![a-z0-9._%+-])@([a-z0-9][a-z0-9._]{0,29})\b/gi;
1446
+ const INSTAGRAM = /@([a-z0-9][a-z0-9._]{0,29})\b/gi;
1447
1447
  const SOUNDCLOUD = /https?:\/\/(?:www\.)?soundcloud\.com\/[a-z0-9_-]+(?:\/[^\s<>"]*)?/gi;
1448
1448
  const DIVIDER = /^[-=]{3,}$/gm;
1449
1449
  let bareDomainPattern = null;
@@ -1461,6 +1461,9 @@ const MAX_TEXT_LENGTH_FOR_BARE_DOMAIN = 2048;
1461
1461
  function cleanUrl(url) {
1462
1462
  return url.replace(/[.,;:!?)]+$/, "");
1463
1463
  }
1464
+ function matchIndex(match) {
1465
+ return match.index ?? 0;
1466
+ }
1464
1467
  function isSoundCloudUrl(url) {
1465
1468
  try {
1466
1469
  const parsed = new URL(url);
@@ -1469,86 +1472,105 @@ function isSoundCloudUrl(url) {
1469
1472
  return false;
1470
1473
  }
1471
1474
  }
1472
- function findLinks(text) {
1473
- const matches = [];
1474
- const usedRanges = [];
1475
- const isOverlapping = (start, end) => {
1476
- return usedRanges.some(
1477
- ([s, e]) => start >= s && start < e || end > s && end <= e || start <= s && end >= e
1478
- );
1475
+ function isLinkMatch(match) {
1476
+ return Boolean(match);
1477
+ }
1478
+ function isInstagramBoundary(text, index) {
1479
+ if (index === 0) return true;
1480
+ const prev = text[index - 1];
1481
+ return prev ? !/[a-z0-9._%+-]/i.test(prev) : true;
1482
+ }
1483
+ function soundCloudMatch(_text, match) {
1484
+ const index = matchIndex(match);
1485
+ const url = cleanUrl(match[0]);
1486
+ return {
1487
+ type: "soundcloud",
1488
+ start: index,
1489
+ end: index + url.length,
1490
+ text: url,
1491
+ href: url
1479
1492
  };
1480
- const addMatch = (match) => {
1481
- if (!isOverlapping(match.start, match.end)) {
1482
- matches.push(match);
1483
- usedRanges.push([match.start, match.end]);
1484
- }
1493
+ }
1494
+ function urlMatch(_text, match) {
1495
+ const url = cleanUrl(match[0]);
1496
+ if (isSoundCloudUrl(url)) return void 0;
1497
+ const index = matchIndex(match);
1498
+ return {
1499
+ type: "url",
1500
+ start: index,
1501
+ end: index + url.length,
1502
+ text: url,
1503
+ href: url
1485
1504
  };
1486
- SOUNDCLOUD.lastIndex = 0;
1487
- for (const match of text.matchAll(SOUNDCLOUD)) {
1488
- const index = match.index ?? 0;
1489
- const url = cleanUrl(match[0]);
1490
- addMatch({
1491
- type: "soundcloud",
1492
- start: index,
1493
- end: index + url.length,
1494
- text: url,
1495
- href: url
1496
- });
1497
- }
1498
- URL_WITH_PROTOCOL.lastIndex = 0;
1499
- for (const match of text.matchAll(URL_WITH_PROTOCOL)) {
1500
- const index = match.index ?? 0;
1501
- const url = cleanUrl(match[0]);
1502
- if (!isSoundCloudUrl(url)) {
1503
- addMatch({
1504
- type: "url",
1505
- start: index,
1506
- end: index + url.length,
1507
- text: url,
1508
- href: url
1509
- });
1510
- }
1511
- }
1512
- EMAIL.lastIndex = 0;
1513
- for (const match of text.matchAll(EMAIL)) {
1514
- const index = match.index ?? 0;
1515
- addMatch({
1516
- type: "email",
1517
- start: index,
1518
- end: index + match[0].length,
1519
- text: match[0],
1520
- href: `mailto:${match[0]}`
1521
- });
1522
- }
1523
- INSTAGRAM.lastIndex = 0;
1524
- for (const match of text.matchAll(INSTAGRAM)) {
1525
- const index = match.index ?? 0;
1526
- const username = match[1];
1527
- addMatch({
1528
- type: "instagram",
1529
- start: index,
1530
- end: index + match[0].length,
1531
- text: match[0],
1532
- href: `https://instagram.com/${username}`
1533
- });
1534
- }
1535
- if (text.length <= MAX_TEXT_LENGTH_FOR_BARE_DOMAIN) {
1536
- const bareDomain = getBareDomainPattern();
1537
- bareDomain.lastIndex = 0;
1538
- for (const match of text.matchAll(bareDomain)) {
1539
- const index = match.index ?? 0;
1540
- const domain = cleanUrl(match[0]);
1541
- addMatch({
1542
- type: "url",
1543
- start: index,
1544
- end: index + domain.length,
1545
- text: domain,
1546
- href: `https://${domain}`
1547
- });
1548
- }
1505
+ }
1506
+ function emailMatch(_text, match) {
1507
+ const index = matchIndex(match);
1508
+ return {
1509
+ type: "email",
1510
+ start: index,
1511
+ end: index + match[0].length,
1512
+ text: match[0],
1513
+ href: `mailto:${match[0]}`
1514
+ };
1515
+ }
1516
+ function instagramMatch(text, match) {
1517
+ const index = matchIndex(match);
1518
+ if (!isInstagramBoundary(text, index)) return void 0;
1519
+ const username = match[1];
1520
+ return {
1521
+ type: "instagram",
1522
+ start: index,
1523
+ end: index + match[0].length,
1524
+ text: match[0],
1525
+ href: `https://instagram.com/${username}`
1526
+ };
1527
+ }
1528
+ function bareDomainMatch(_text, match) {
1529
+ const index = matchIndex(match);
1530
+ const domain = cleanUrl(match[0]);
1531
+ return {
1532
+ type: "url",
1533
+ start: index,
1534
+ end: index + domain.length,
1535
+ text: domain,
1536
+ href: `https://${domain}`
1537
+ };
1538
+ }
1539
+ const BASE_LINK_EXTRACTORS = [
1540
+ { pattern: SOUNDCLOUD, toMatch: soundCloudMatch },
1541
+ { pattern: URL_WITH_PROTOCOL, toMatch: urlMatch },
1542
+ { pattern: EMAIL, toMatch: emailMatch },
1543
+ { pattern: INSTAGRAM, toMatch: instagramMatch }
1544
+ ];
1545
+ function bareDomainExtractor() {
1546
+ return { pattern: getBareDomainPattern(), toMatch: bareDomainMatch };
1547
+ }
1548
+ function matchesForExtractor(text, extractor) {
1549
+ extractor.pattern.lastIndex = 0;
1550
+ return Array.from(
1551
+ text.matchAll(extractor.pattern),
1552
+ (match) => extractor.toMatch(text, match)
1553
+ ).filter(isLinkMatch);
1554
+ }
1555
+ function rangesOverlap(a, b) {
1556
+ return a.start < b.end && a.end > b.start;
1557
+ }
1558
+ function withoutOverlaps(candidates) {
1559
+ const matches = [];
1560
+ const used = [];
1561
+ for (const candidate of candidates) {
1562
+ if (used.some((range) => rangesOverlap(candidate, range))) continue;
1563
+ matches.push(candidate);
1564
+ used.push(candidate);
1549
1565
  }
1550
1566
  return matches.sort((a, b) => a.start - b.start);
1551
1567
  }
1568
+ function findLinks(text) {
1569
+ const extractors = text.length <= MAX_TEXT_LENGTH_FOR_BARE_DOMAIN ? [...BASE_LINK_EXTRACTORS, bareDomainExtractor()] : BASE_LINK_EXTRACTORS;
1570
+ return withoutOverlaps(
1571
+ extractors.flatMap((extractor) => matchesForExtractor(text, extractor))
1572
+ );
1573
+ }
1552
1574
  function findDividers(text) {
1553
1575
  const dividers = [];
1554
1576
  DIVIDER.lastIndex = 0;
@@ -1573,72 +1595,67 @@ function parseFormattedText(text) {
1573
1595
  if (!text) {
1574
1596
  return [];
1575
1597
  }
1576
- const links = findLinks(text);
1577
- const dividers = findDividers(text);
1578
1598
  const segments = [];
1579
- const markers = [
1580
- ...links.map((l) => ({
1581
- type: "link",
1582
- data: l,
1583
- start: l.start,
1584
- end: l.end
1585
- })),
1586
- ...dividers.map((d) => ({
1587
- type: "divider",
1588
- start: d.start,
1589
- end: d.end
1590
- }))
1591
- ].sort((a, b) => a.start - b.start);
1599
+ const markers = buildMarkers(findLinks(text), findDividers(text));
1592
1600
  let lastEnd = 0;
1593
1601
  let lastWasDivider = false;
1594
1602
  for (const marker of markers) {
1595
1603
  if (marker.start < lastEnd) continue;
1596
- if (marker.start > lastEnd) {
1597
- let textContent = text.slice(lastEnd, marker.start);
1598
- if (lastWasDivider) {
1599
- textContent = textContent.replace(/^\n+/, "");
1600
- }
1601
- if (marker.type === "divider") {
1602
- textContent = textContent.replace(/\n+$/, "");
1603
- }
1604
- if (textContent) {
1605
- segments.push({ type: "text", content: textContent });
1606
- }
1607
- }
1608
- if (marker.type === "link") {
1609
- const link = marker.data;
1610
- if (link.type === "soundcloud") {
1611
- segments.push({
1612
- type: "soundcloud",
1613
- content: link.text,
1614
- href: link.href
1615
- });
1616
- } else {
1617
- segments.push({
1618
- type: "link",
1619
- content: link.text,
1620
- href: link.href,
1621
- linkType: link.type
1622
- });
1623
- }
1624
- lastWasDivider = false;
1625
- } else {
1626
- segments.push({ type: "divider", content: "" });
1627
- lastWasDivider = true;
1628
- }
1604
+ appendTextSegment(segments, text, lastEnd, marker.start, {
1605
+ trimLeading: lastWasDivider,
1606
+ trimTrailing: marker.type === "divider"
1607
+ });
1608
+ segments.push(markerSegment(marker));
1629
1609
  lastEnd = marker.end;
1610
+ lastWasDivider = marker.type === "divider";
1630
1611
  }
1631
- if (lastEnd < text.length) {
1632
- let textContent = text.slice(lastEnd);
1633
- if (lastWasDivider) {
1634
- textContent = textContent.replace(/^\n+/, "");
1635
- }
1636
- if (textContent) {
1637
- segments.push({ type: "text", content: textContent });
1638
- }
1639
- }
1612
+ appendTextSegment(segments, text, lastEnd, text.length, {
1613
+ trimLeading: lastWasDivider,
1614
+ trimTrailing: false
1615
+ });
1640
1616
  return segments;
1641
1617
  }
1618
+ function buildMarkers(links, dividers) {
1619
+ return [
1620
+ ...links.map((link) => ({
1621
+ type: "link",
1622
+ data: link,
1623
+ start: link.start,
1624
+ end: link.end
1625
+ })),
1626
+ ...dividers.map((divider) => ({
1627
+ type: "divider",
1628
+ start: divider.start,
1629
+ end: divider.end
1630
+ }))
1631
+ ].sort((a, b) => a.start - b.start);
1632
+ }
1633
+ function trimTextSegment(content, opts) {
1634
+ const leading = opts.trimLeading ? content.replace(/^\n+/, "") : content;
1635
+ return opts.trimTrailing ? leading.replace(/\n+$/, "") : leading;
1636
+ }
1637
+ function appendTextSegment(segments, text, start, end, opts) {
1638
+ if (start >= end) return;
1639
+ const content = trimTextSegment(text.slice(start, end), opts);
1640
+ if (content) segments.push({ type: "text", content });
1641
+ }
1642
+ function markerSegment(marker) {
1643
+ if (marker.type === "divider") return { type: "divider", content: "" };
1644
+ const link = marker.data;
1645
+ if (link.type === "soundcloud") {
1646
+ return {
1647
+ type: "soundcloud",
1648
+ content: link.text,
1649
+ href: link.href
1650
+ };
1651
+ }
1652
+ return {
1653
+ type: "link",
1654
+ content: link.text,
1655
+ href: link.href,
1656
+ linkType: link.type
1657
+ };
1658
+ }
1642
1659
 
1643
1660
  const sessionServicesFormattedTextCss = () => `:host{--foreground:oklch(0.145 0 0);--muted-foreground:oklch(0.556 0 0);--primary:oklch(0.42 0.24 264.376);--border:oklch(0.922 0 0);--ring:oklch(0.708 0 0);--radius:0.5rem;--spacing-md:1rem;--spacing-lg:1.5rem;--font-family:ui-sans-serif, system-ui, -apple-system, BlinkMacSystemFont,
1644
1661
  "Segoe UI", Roboto, "Helvetica Neue", Arial, "Noto Sans", sans-serif;--transition-fast:150ms cubic-bezier(0.4, 0, 0.2, 1);--internal-foreground:var(--foreground, oklch(0.145 0 0));--internal-muted-foreground:var(--muted-foreground, oklch(0.556 0 0));--internal-primary:var(--primary, oklch(0.42 0.24 264.376));--internal-border:var(--border, oklch(0.922 0 0));--internal-ring:var(--ring, oklch(0.708 0 0));--internal-radius:var(--radius, 0.5rem);--internal-spacing-md:var(--spacing-md, 1rem);--internal-spacing-lg:var(--spacing-lg, 1.5rem);--internal-font-family:var(
@@ -1682,28 +1699,27 @@ const SessionServicesFormattedText$1 = /*@__PURE__*/ proxyCustomElement(class Se
1682
1699
  this.setupTheme();
1683
1700
  }
1684
1701
  disconnectedCallback() {
1685
- if (this.mediaQueryList) {
1686
- this.mediaQueryList.removeEventListener("change", this.handleSystemThemeChange);
1687
- }
1702
+ this.removeThemeListener();
1688
1703
  }
1689
1704
  setupTheme() {
1690
- if (this.mediaQueryList) {
1691
- this.mediaQueryList.removeEventListener("change", this.handleSystemThemeChange);
1692
- }
1693
- if (this.theme === "system") {
1694
- // Guard for testing environments (JSDOM) where matchMedia may not exist
1695
- if (typeof globalThis.matchMedia === "function") {
1696
- this.mediaQueryList = globalThis.matchMedia("(prefers-color-scheme: dark)");
1697
- this.currentTheme = this.mediaQueryList.matches ? "dark" : "light";
1698
- this.mediaQueryList.addEventListener("change", this.handleSystemThemeChange);
1699
- }
1700
- else {
1701
- this.currentTheme = "light";
1702
- }
1703
- }
1704
- else {
1705
+ this.removeThemeListener();
1706
+ if (this.theme !== "system") {
1705
1707
  this.currentTheme = this.theme;
1708
+ return;
1706
1709
  }
1710
+ this.setupSystemTheme();
1711
+ }
1712
+ removeThemeListener() {
1713
+ this.mediaQueryList?.removeEventListener("change", this.handleSystemThemeChange);
1714
+ }
1715
+ setupSystemTheme() {
1716
+ if (typeof globalThis.matchMedia !== "function") {
1717
+ this.currentTheme = "light";
1718
+ return;
1719
+ }
1720
+ this.mediaQueryList = globalThis.matchMedia("(prefers-color-scheme: dark)");
1721
+ this.currentTheme = this.mediaQueryList.matches ? "dark" : "light";
1722
+ this.mediaQueryList.addEventListener("change", this.handleSystemThemeChange);
1707
1723
  }
1708
1724
  handleSystemThemeChange = (e) => {
1709
1725
  if (this.theme === "system") {
@@ -1735,21 +1751,21 @@ const SessionServicesFormattedText$1 = /*@__PURE__*/ proxyCustomElement(class Se
1735
1751
  return (h("div", { key: key, class: "soundcloud-embed" }, h("iframe", { width: "100%", height: "166", scrolling: "no", allow: "autoplay", sandbox: "allow-scripts allow-same-origin allow-popups", src: embedUrl, title: iframeTitle, loading: "lazy" })));
1736
1752
  }
1737
1753
  renderSegment(segment, index) {
1738
- switch (segment.type) {
1739
- case "text":
1740
- return h("span", { key: index }, segment.content);
1741
- case "link":
1742
- return this.renderLink(segment, index);
1743
- case "divider":
1744
- return h("hr", { key: index, class: "formatted-divider" });
1745
- case "soundcloud":
1746
- return this.renderSoundCloudEmbed(segment.href || "", index, segment.content);
1747
- default:
1748
- return null;
1749
- }
1754
+ if (segment.type === "text")
1755
+ return h("span", { key: index }, segment.content);
1756
+ if (segment.type === "link")
1757
+ return this.renderLink(segment, index);
1758
+ if (segment.type === "divider")
1759
+ return h("hr", { key: index, class: "formatted-divider" });
1760
+ return this.renderMediaSegment(segment, index);
1761
+ }
1762
+ renderMediaSegment(segment, index) {
1763
+ if (segment.type !== "soundcloud")
1764
+ return null;
1765
+ return this.renderSoundCloudEmbed(segment.href || "", index, segment.content);
1750
1766
  }
1751
1767
  render() {
1752
- return (h(Host, { key: '1a7f36efb8cdbd1efdaa97c36b60593a13c99c4e', "data-theme": this.currentTheme }, h("div", { key: 'fce1176ff0ef2ea5c3f1b282ab9a52554491f9bf', class: "formatted-text" }, this.segments.map((segment, index) => this.renderSegment(segment, index)))));
1768
+ return (h(Host, { key: '3b477ff5db918a1b3b6872f48d330305d320ae3b', "data-theme": this.currentTheme }, h("div", { key: 'efe7fc93f9f1c2b38271a30e4a8fdf7306793d6e', class: "formatted-text" }, this.segments.map((segment, index) => this.renderSegment(segment, index)))));
1753
1769
  }
1754
1770
  static get watchers() { return {
1755
1771
  "text": ["textChanged"],