@atmo-dev/kit 0.4.0 → 0.5.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 (144) hide show
  1. package/README.md +35 -0
  2. package/dist/events/DatePicker.svelte +222 -0
  3. package/dist/events/DatePicker.svelte.d.ts +12 -0
  4. package/dist/events/DateTimePicker.svelte +112 -0
  5. package/dist/events/DateTimePicker.svelte.d.ts +10 -0
  6. package/dist/events/EventAttendees.svelte +199 -0
  7. package/dist/events/EventAttendees.svelte.d.ts +19 -0
  8. package/dist/events/EventCard.svelte +148 -0
  9. package/dist/events/EventCard.svelte.d.ts +8 -0
  10. package/dist/events/EventComments.svelte +98 -0
  11. package/dist/events/EventComments.svelte.d.ts +8 -0
  12. package/dist/events/EventEditor.svelte +540 -0
  13. package/dist/events/EventEditor.svelte.d.ts +18 -0
  14. package/dist/events/EventRsvp.svelte +178 -0
  15. package/dist/events/EventRsvp.svelte.d.ts +16 -0
  16. package/dist/events/EventView.svelte +488 -0
  17. package/dist/events/EventView.svelte.d.ts +23 -0
  18. package/dist/events/ImageDropper.svelte +66 -0
  19. package/dist/events/ImageDropper.svelte.d.ts +7 -0
  20. package/dist/events/Map.svelte +36 -0
  21. package/dist/events/Map.svelte.d.ts +8 -0
  22. package/dist/events/PostToBlueskyModal.svelte +246 -0
  23. package/dist/events/PostToBlueskyModal.svelte.d.ts +22 -0
  24. package/dist/events/ShareModal.svelte +174 -0
  25. package/dist/events/ShareModal.svelte.d.ts +23 -0
  26. package/dist/events/StarterPackButton.svelte +72 -0
  27. package/dist/events/StarterPackButton.svelte.d.ts +10 -0
  28. package/dist/events/ThemeApply.svelte +30 -0
  29. package/dist/events/ThemeApply.svelte.d.ts +7 -0
  30. package/dist/events/ThemeBackground.svelte +33 -0
  31. package/dist/events/ThemeBackground.svelte.d.ts +7 -0
  32. package/dist/events/ThemePicker.svelte +104 -0
  33. package/dist/events/ThemePicker.svelte.d.ts +7 -0
  34. package/dist/events/ThumbnailPresets.svelte +71 -0
  35. package/dist/events/ThumbnailPresets.svelte.d.ts +11 -0
  36. package/dist/events/TimePicker.svelte +117 -0
  37. package/dist/events/TimePicker.svelte.d.ts +10 -0
  38. package/dist/events/TimezonePicker.svelte +67 -0
  39. package/dist/events/TimezonePicker.svelte.d.ts +6 -0
  40. package/dist/events/VodPlayer.svelte +154 -0
  41. package/dist/events/VodPlayer.svelte.d.ts +14 -0
  42. package/dist/events/VodTranscript.svelte +75 -0
  43. package/dist/events/VodTranscript.svelte.d.ts +8 -0
  44. package/dist/events/atproto-helpers.d.ts +21 -0
  45. package/dist/events/atproto-helpers.js +61 -0
  46. package/dist/events/cal/helper.d.ts +1 -0
  47. package/dist/events/cal/helper.js +20 -0
  48. package/dist/events/cal/ical.d.ts +22 -0
  49. package/dist/events/cal/ical.js +188 -0
  50. package/dist/events/cal/sanitize.d.ts +3 -0
  51. package/dist/events/cal/sanitize.js +25 -0
  52. package/dist/events/conference.d.ts +65 -0
  53. package/dist/events/conference.js +61 -0
  54. package/dist/events/contrail.d.ts +49 -0
  55. package/dist/events/contrail.js +12 -0
  56. package/dist/events/date-format.d.ts +22 -0
  57. package/dist/events/date-format.js +43 -0
  58. package/dist/events/editor/LinkedRecordsPicker.svelte +104 -0
  59. package/dist/events/editor/LinkedRecordsPicker.svelte.d.ts +12 -0
  60. package/dist/events/editor/LinksSection.svelte +154 -0
  61. package/dist/events/editor/LinksSection.svelte.d.ts +13 -0
  62. package/dist/events/editor/LocationSection.svelte +163 -0
  63. package/dist/events/editor/LocationSection.svelte.d.ts +8 -0
  64. package/dist/events/editor/RecurringModal.svelte +313 -0
  65. package/dist/events/editor/RecurringModal.svelte.d.ts +30 -0
  66. package/dist/events/editor/ThemeSection.svelte +24 -0
  67. package/dist/events/editor/ThemeSection.svelte.d.ts +7 -0
  68. package/dist/events/editor/ThumbnailSection.svelte +181 -0
  69. package/dist/events/editor/ThumbnailSection.svelte.d.ts +13 -0
  70. package/dist/events/editor/adapter.d.ts +73 -0
  71. package/dist/events/editor/adapter.js +9 -0
  72. package/dist/events/editor/location.d.ts +16 -0
  73. package/dist/events/editor/location.js +50 -0
  74. package/dist/events/editor/save.d.ts +43 -0
  75. package/dist/events/editor/save.js +168 -0
  76. package/dist/events/editor/types.d.ts +39 -0
  77. package/dist/events/editor/types.js +9 -0
  78. package/dist/events/event-media.d.ts +4 -0
  79. package/dist/events/event-media.js +4 -0
  80. package/dist/events/event-types.d.ts +79 -0
  81. package/dist/events/event-types.js +11 -0
  82. package/dist/events/event-view/AddToCalendarButton.svelte +31 -0
  83. package/dist/events/event-view/AddToCalendarButton.svelte.d.ts +9 -0
  84. package/dist/events/event-view/EventBadges.svelte +20 -0
  85. package/dist/events/event-view/EventBadges.svelte.d.ts +7 -0
  86. package/dist/events/event-view/EventDateBlock.svelte +43 -0
  87. package/dist/events/event-view/EventDateBlock.svelte.d.ts +7 -0
  88. package/dist/events/event-view/EventHostedBy.svelte +61 -0
  89. package/dist/events/event-view/EventHostedBy.svelte.d.ts +16 -0
  90. package/dist/events/event-view/EventLinksList.svelte +24 -0
  91. package/dist/events/event-view/EventLinksList.svelte.d.ts +9 -0
  92. package/dist/events/event-view/EventLocationBlock.svelte +62 -0
  93. package/dist/events/event-view/EventLocationBlock.svelte.d.ts +7 -0
  94. package/dist/events/event-view/EventLocationMap.svelte +66 -0
  95. package/dist/events/event-view/EventLocationMap.svelte.d.ts +7 -0
  96. package/dist/events/event-view/ExternalRsvpNotice.svelte +34 -0
  97. package/dist/events/event-view/ExternalRsvpNotice.svelte.d.ts +6 -0
  98. package/dist/events/event-view/StreamPlacePlayer.svelte +209 -0
  99. package/dist/events/event-view/StreamPlacePlayer.svelte.d.ts +8 -0
  100. package/dist/events/event-view/format.d.ts +10 -0
  101. package/dist/events/event-view/format.js +112 -0
  102. package/dist/events/event-view/locations.d.ts +36 -0
  103. package/dist/events/event-view/locations.js +243 -0
  104. package/dist/events/event-view/plyr-icons.d.ts +3 -0
  105. package/dist/events/event-view/plyr-icons.js +46 -0
  106. package/dist/events/linked-records.d.ts +18 -0
  107. package/dist/events/linked-records.js +11 -0
  108. package/dist/events/profile-url.d.ts +3 -0
  109. package/dist/events/profile-url.js +11 -0
  110. package/dist/events/schedule/ConferenceTimetable.svelte +340 -0
  111. package/dist/events/schedule/ConferenceTimetable.svelte.d.ts +22 -0
  112. package/dist/events/schedule/DaySchedule.svelte +298 -0
  113. package/dist/events/schedule/DaySchedule.svelte.d.ts +26 -0
  114. package/dist/events/schedule/ScheduleEventCell.svelte +166 -0
  115. package/dist/events/schedule/ScheduleEventCell.svelte.d.ts +25 -0
  116. package/dist/events/schedule/schedule-utils.d.ts +44 -0
  117. package/dist/events/schedule/schedule-utils.js +166 -0
  118. package/dist/events/starter-pack-url.d.ts +3 -0
  119. package/dist/events/starter-pack-url.js +16 -0
  120. package/dist/events/theme.d.ts +9 -0
  121. package/dist/events/theme.js +36 -0
  122. package/dist/events/themes/Blobs.svelte +64 -0
  123. package/dist/events/themes/Blobs.svelte.d.ts +18 -0
  124. package/dist/events/themes/Butterflies.svelte +183 -0
  125. package/dist/events/themes/Butterflies.svelte.d.ts +3 -0
  126. package/dist/events/themes/Fireflies.svelte +134 -0
  127. package/dist/events/themes/Fireflies.svelte.d.ts +3 -0
  128. package/dist/events/themes/Kaleidoscope.svelte +183 -0
  129. package/dist/events/themes/Kaleidoscope.svelte.d.ts +3 -0
  130. package/dist/events/themes/Matrix.svelte +153 -0
  131. package/dist/events/themes/Matrix.svelte.d.ts +3 -0
  132. package/dist/events/themes/Stars.svelte +98 -0
  133. package/dist/events/themes/Stars.svelte.d.ts +3 -0
  134. package/dist/events/thumbnails/designs.d.ts +18 -0
  135. package/dist/events/thumbnails/designs.js +316 -0
  136. package/dist/events/view-data.d.ts +38 -0
  137. package/dist/events/view-data.js +1 -0
  138. package/dist/events-data.d.ts +15 -0
  139. package/dist/events-data.js +9 -0
  140. package/dist/events-editor.d.ts +2 -0
  141. package/dist/events-editor.js +2 -0
  142. package/dist/events.d.ts +19 -0
  143. package/dist/events.js +17 -0
  144. package/package.json +46 -4
@@ -0,0 +1,62 @@
1
+ <script lang="ts">
2
+ import { Icon } from '../../index.js';
3
+ import { Link, MapPin } from '@sketchyicons/svelte';
4
+ import { getPhysicalLocationLines, type LocationData } from './locations.js';
5
+
6
+ let { locations }: { locations: LocationData[] } = $props();
7
+ let physicalLines = $derived(getPhysicalLocationLines(locations));
8
+ let links = $derived(locations.filter((location) => location.kind === 'uri'));
9
+ </script>
10
+
11
+ {#if physicalLines.length || links.length}
12
+ <ul class="mb-6 space-y-4" aria-label="Event locations">
13
+ {#if physicalLines.length}
14
+ <li class="flex min-w-0 items-center gap-4">
15
+ <div
16
+ class="flex size-12 shrink-0 items-center justify-center rounded-ui bg-recess text-muted [box-shadow:var(--ui-recess-shadow)]"
17
+ >
18
+ <Icon img={MapPin} size={22} />
19
+ </div>
20
+ <div class="min-w-0 [overflow-wrap:anywhere]">
21
+ {#each physicalLines as line, i (i)}
22
+ <p class={line.muted ? 'text-sm text-muted' : 'font-semibold text-ink'}>
23
+ <svelte:element
24
+ this={line.href ? 'a' : 'span'}
25
+ href={line.href}
26
+ target={line.href ? '_blank' : undefined}
27
+ rel={line.href ? 'noopener noreferrer' : undefined}
28
+ class={line.href ? 'transition-opacity hover:opacity-80' : undefined}
29
+ >{line.label}</svelte:element
30
+ >
31
+ </p>
32
+ {/each}
33
+ </div>
34
+ </li>
35
+ {/if}
36
+ {#each links as location, i (i)}
37
+ <li>
38
+ <svelte:element
39
+ this={location.href ? 'a' : 'div'}
40
+ href={location.href}
41
+ target={location.href ? '_blank' : undefined}
42
+ rel={location.href ? 'noopener noreferrer' : undefined}
43
+ class="flex min-w-0 items-center gap-4 rounded-ui {location.href
44
+ ? 'transition-opacity hover:opacity-80'
45
+ : ''}"
46
+ >
47
+ <div
48
+ class="flex size-12 shrink-0 items-center justify-center rounded-ui bg-recess text-muted [box-shadow:var(--ui-recess-shadow)]"
49
+ >
50
+ <Icon img={Link} size={22} />
51
+ </div>
52
+ <div class="min-w-0 [overflow-wrap:anywhere]">
53
+ <p class="font-semibold text-ink">{location.label}</p>
54
+ {#if location.detail}
55
+ <p class="text-sm text-muted">{location.detail}</p>
56
+ {/if}
57
+ </div>
58
+ </svelte:element>
59
+ </li>
60
+ {/each}
61
+ </ul>
62
+ {/if}
@@ -0,0 +1,7 @@
1
+ import { type LocationData } from './locations.js';
2
+ type $$ComponentProps = {
3
+ locations: LocationData[];
4
+ };
5
+ declare const EventLocationBlock: import("svelte").Component<$$ComponentProps, {}, "">;
6
+ type EventLocationBlock = ReturnType<typeof EventLocationBlock>;
7
+ export default EventLocationBlock;
@@ -0,0 +1,66 @@
1
+ <script lang="ts">
2
+ import { Badge } from '../../index.js';
3
+ import Map from '../Map.svelte';
4
+ import type { GeoLocation } from './locations.js';
5
+
6
+ let { geoLocation }: { geoLocation: GeoLocation | null } = $props();
7
+
8
+ let copied = $state(false);
9
+
10
+ async function copyCoords() {
11
+ if (!geoLocation) return;
12
+ const text = `${geoLocation.lat.toFixed(5)}, ${geoLocation.lng.toFixed(5)}`;
13
+ try {
14
+ await navigator.clipboard.writeText(text);
15
+ copied = true;
16
+ setTimeout(() => (copied = false), 2000);
17
+ } catch {
18
+ // Clipboard access is optional; the coordinates remain visible.
19
+ }
20
+ }
21
+ </script>
22
+
23
+ {#if geoLocation}
24
+ <div class="mt-8 mb-8">
25
+ <div class="mb-3 flex items-baseline gap-2">
26
+ <p class="text-xs font-semibold tracking-wider text-muted uppercase">Location:</p>
27
+ <button
28
+ type="button"
29
+ onclick={copyCoords}
30
+ class="ml-auto cursor-pointer transition-opacity active:opacity-60"
31
+ title="Copy coordinates"
32
+ aria-label="Copy coordinates"
33
+ >
34
+ <Badge variant="outline" class="font-mono">
35
+ {copied ? 'Copied!' : `${geoLocation.lat.toFixed(5)}, ${geoLocation.lng.toFixed(5)}`}
36
+ </Badge>
37
+ </button>
38
+ </div>
39
+ <div class="h-64 w-full overflow-hidden rounded-xl">
40
+ <Map lat={geoLocation.lat} lng={geoLocation.lng} />
41
+ </div>
42
+ <p class="mt-3 text-sm [overflow-wrap:anywhere] text-base-700 dark:text-base-200">
43
+ {geoLocation.label}
44
+ </p>
45
+ <p class="mt-1 text-xs text-muted">
46
+ Open in
47
+ <a
48
+ href={geoLocation.googleMapsUrl}
49
+ target="_blank"
50
+ rel="noopener noreferrer"
51
+ class="text-base-700 hover:underline dark:text-base-300"
52
+ >
53
+ Google Maps
54
+ </a>
55
+ |
56
+ <a
57
+ href={geoLocation.osmUrl}
58
+ target="_blank"
59
+ rel="noopener noreferrer"
60
+ class="text-base-700 hover:underline dark:text-base-300"
61
+ >
62
+ OpenStreetMap
63
+ </a>
64
+ </p>
65
+ </div>
66
+ {/if}
@@ -0,0 +1,7 @@
1
+ import type { GeoLocation } from './locations.js';
2
+ type $$ComponentProps = {
3
+ geoLocation: GeoLocation | null;
4
+ };
5
+ declare const EventLocationMap: import("svelte").Component<$$ComponentProps, {}, "">;
6
+ type EventLocationMap = ReturnType<typeof EventLocationMap>;
7
+ export default EventLocationMap;
@@ -0,0 +1,34 @@
1
+ <script lang="ts">
2
+ import { ArrowUpRight } from '@sketchyicons/svelte';
3
+ import { Button, Icon } from '../../index.js';
4
+
5
+ // Shown in place of the RSVP controls for imported events whose host opted out
6
+ // of atmo RSVPs (additionalData.externalSource.rsvpMode === 'external_only').
7
+ // Directs attendees to the original event page instead.
8
+ let { url }: { url: string } = $props();
9
+
10
+ let host = $derived.by(() => {
11
+ try {
12
+ return new URL(url).hostname.replace(/^www\./, '');
13
+ } catch {
14
+ return null;
15
+ }
16
+ });
17
+ </script>
18
+
19
+ <div
20
+ class="mt-8 mb-2 flex flex-col gap-3 rounded-2xl border border-base-200 bg-base-100 p-4 sm:flex-row sm:items-center sm:justify-between dark:border-base-800 dark:bg-base-950/50"
21
+ >
22
+ <div class="min-w-0">
23
+ <p class="text-sm font-semibold text-base-900 dark:text-base-50">RSVP on the original page</p>
24
+ <p class="mt-0.5 text-xs text-base-600 dark:text-base-400">
25
+ {host
26
+ ? `This event is hosted on ${host}. Head there to RSVP.`
27
+ : 'This event is hosted elsewhere. Open the original page to RSVP.'}
28
+ </p>
29
+ </div>
30
+ <Button href={url} target="_blank" rel="noopener noreferrer" class="shrink-0">
31
+ {host ? `RSVP on ${host}` : 'Open original event'}
32
+ <Icon img={ArrowUpRight} size={16} class="shrink-0" />
33
+ </Button>
34
+ </div>
@@ -0,0 +1,6 @@
1
+ type $$ComponentProps = {
2
+ url: string;
3
+ };
4
+ declare const ExternalRsvpNotice: import("svelte").Component<$$ComponentProps, {}, "">;
5
+ type ExternalRsvpNotice = ReturnType<typeof ExternalRsvpNotice>;
6
+ export default ExternalRsvpNotice;
@@ -0,0 +1,209 @@
1
+ <script lang="ts">
2
+ import { onMount } from 'svelte';
3
+ import { Icon } from '../../index.js';
4
+ import { ExternalLink } from '@sketchyicons/svelte';
5
+ import 'plyr/dist/plyr.css';
6
+ import type PlyrType from 'plyr';
7
+ import { sketchyPlyrIcons } from './plyr-icons.js';
8
+
9
+ let {
10
+ handle,
11
+ title
12
+ }: {
13
+ handle: string;
14
+ title: string;
15
+ } = $props();
16
+
17
+ let videoEl: HTMLVideoElement | undefined = $state();
18
+ let overlayEl: HTMLAnchorElement | undefined = $state();
19
+ let error = $state(false);
20
+ let controlsVisible = $state(true);
21
+
22
+ let pc: RTCPeerConnection | null = null;
23
+ let plyr: PlyrType | null = null;
24
+
25
+ const posterUrl = $derived(
26
+ `https://stream.place/api/playback/${encodeURIComponent(handle)}/stream.jpg`
27
+ );
28
+
29
+ onMount(() => {
30
+ init();
31
+ return () => {
32
+ if (pc) {
33
+ pc.close();
34
+ pc = null;
35
+ }
36
+ if (videoEl) {
37
+ videoEl.srcObject = null;
38
+ }
39
+ plyr?.destroy();
40
+ };
41
+ });
42
+
43
+ function waitForIceGathering(peer: RTCPeerConnection, timeoutMs = 2000): Promise<void> {
44
+ return new Promise((resolve) => {
45
+ if (peer.iceGatheringState === 'complete') {
46
+ resolve();
47
+ return;
48
+ }
49
+
50
+ let done = false;
51
+ const finish = () => {
52
+ if (done) return;
53
+ done = true;
54
+ peer.removeEventListener('icegatheringstatechange', onStateChange);
55
+ clearTimeout(timer);
56
+ resolve();
57
+ };
58
+
59
+ const onStateChange = () => {
60
+ if (peer.iceGatheringState === 'complete') {
61
+ finish();
62
+ }
63
+ };
64
+
65
+ peer.addEventListener('icegatheringstatechange', onStateChange);
66
+ const timer = setTimeout(finish, timeoutMs);
67
+ });
68
+ }
69
+
70
+ async function init() {
71
+ if (!videoEl) return;
72
+
73
+ try {
74
+ pc = new RTCPeerConnection({
75
+ iceServers: [{ urls: 'stun:stun.l.google.com:19302' }],
76
+ bundlePolicy: 'max-bundle'
77
+ });
78
+
79
+ pc.addTransceiver('video', { direction: 'recvonly' });
80
+ pc.addTransceiver('audio', { direction: 'recvonly' });
81
+
82
+ pc.addEventListener('track', (event) => {
83
+ if (!videoEl) return;
84
+ if (event.streams && event.streams[0]) {
85
+ videoEl.srcObject = event.streams[0];
86
+ } else {
87
+ let stream = videoEl.srcObject as MediaStream | null;
88
+ if (!stream) {
89
+ stream = new MediaStream();
90
+ videoEl.srcObject = stream;
91
+ }
92
+ stream.addTrack(event.track);
93
+ }
94
+ });
95
+
96
+ const offer = await pc.createOffer();
97
+ await pc.setLocalDescription(offer);
98
+ await waitForIceGathering(pc, 2000);
99
+
100
+ const sdp = pc.localDescription?.sdp;
101
+ if (!sdp) {
102
+ error = true;
103
+ pc.close();
104
+ pc = null;
105
+ return;
106
+ }
107
+
108
+ const response = await fetch(
109
+ `https://stream.place/api/playback/${encodeURIComponent(handle)}/webrtc?rendition=source`,
110
+ {
111
+ method: 'POST',
112
+ headers: { 'Content-Type': 'application/sdp' },
113
+ body: sdp
114
+ }
115
+ );
116
+
117
+ if (!response.ok) {
118
+ error = true;
119
+ pc.close();
120
+ pc = null;
121
+ return;
122
+ }
123
+
124
+ const answerSdp = await response.text();
125
+ await pc.setRemoteDescription({ type: 'answer', sdp: answerSdp });
126
+
127
+ const { default: Plyr } = await import('plyr');
128
+
129
+ plyr = new Plyr(videoEl, {
130
+ ...sketchyPlyrIcons(),
131
+ controls: ['play', 'mute', 'volume', 'fullscreen'],
132
+ settings: [],
133
+ ratio: '16:9'
134
+ });
135
+
136
+ plyr.on('controlshidden', () => {
137
+ controlsVisible = false;
138
+ });
139
+ plyr.on('controlsshown', () => {
140
+ controlsVisible = true;
141
+ });
142
+
143
+ // Move the overlay link inside Plyr's container so mousemove over the
144
+ // button bubbles into Plyr's activity tracker — otherwise hovering it
145
+ // hides the controls, which hides the button, which reactivates Plyr
146
+ // (a flicker loop).
147
+ const plyrEl = videoEl.closest('.plyr');
148
+ if (plyrEl && overlayEl) plyrEl.appendChild(overlayEl);
149
+ } catch {
150
+ error = true;
151
+ if (pc) {
152
+ pc.close();
153
+ pc = null;
154
+ }
155
+ }
156
+ }
157
+ </script>
158
+
159
+ {#snippet watchOnStreamPlace(hidden = false)}
160
+ <a
161
+ bind:this={overlayEl}
162
+ href="https://stream.place/{handle}"
163
+ target="_blank"
164
+ rel="noopener noreferrer"
165
+ onclick={() => plyr?.pause()}
166
+ class="absolute top-3 right-3 z-10 inline-flex items-center gap-1.5 rounded-full bg-black/55 px-2.5 py-1 text-xs font-medium text-white backdrop-blur-sm transition-opacity duration-200 hover:bg-black/75 {hidden
167
+ ? 'pointer-events-none opacity-0'
168
+ : 'opacity-100'}"
169
+ >
170
+ Watch on stream.place
171
+ <Icon img={ExternalLink} size={12} />
172
+ </a>
173
+ {/snippet}
174
+
175
+ {#if error}
176
+ <div
177
+ class="relative flex aspect-video w-full items-center justify-center overflow-hidden rounded-xl border border-base-200 bg-base-100 dark:border-base-800 dark:bg-base-900"
178
+ >
179
+ <div
180
+ class="absolute inset-0 bg-cover bg-center"
181
+ style="background-image: url({posterUrl});"
182
+ ></div>
183
+ <div class="absolute inset-0 bg-black/60"></div>
184
+ {@render watchOnStreamPlace()}
185
+ <p class="relative text-sm text-base-100">Stream is offline</p>
186
+ </div>
187
+ {:else}
188
+ <div
189
+ class="relative aspect-video w-full max-w-full overflow-hidden rounded-xl border border-base-300 dark:border-base-400/40"
190
+ >
191
+ {@render watchOnStreamPlace(!controlsVisible)}
192
+ <video
193
+ bind:this={videoEl}
194
+ class="h-full w-full"
195
+ aria-label={title}
196
+ poster={posterUrl}
197
+ autoplay
198
+ playsinline
199
+ muted
200
+ crossorigin="anonymous"
201
+ ></video>
202
+ </div>
203
+ {/if}
204
+
205
+ <style>
206
+ * {
207
+ --plyr-color-main: var(--color-accent-500);
208
+ }
209
+ </style>
@@ -0,0 +1,8 @@
1
+ import 'plyr/dist/plyr.css';
2
+ type $$ComponentProps = {
3
+ handle: string;
4
+ title: string;
5
+ };
6
+ declare const StreamPlacePlayer: import("svelte").Component<$$ComponentProps, {}, "">;
7
+ type StreamPlacePlayer = ReturnType<typeof StreamPlacePlayer>;
8
+ export default StreamPlacePlayer;
@@ -0,0 +1,10 @@
1
+ import { type PreferredSocialClient } from '../../social.js';
2
+ export declare function formatMonth(date: Date): string;
3
+ export declare function formatDay(date: Date): number;
4
+ export declare function formatWeekday(date: Date): string;
5
+ export declare function formatFullDate(date: Date): string;
6
+ export declare function formatTime(date: Date): string;
7
+ export declare function getModeLabel(mode: string): string;
8
+ /** Optional local accent class for an event mode. */
9
+ export declare function getModeColor(mode: string): string;
10
+ export declare function buildDescriptionHtml(description: string | undefined, facets: unknown, socialClient?: PreferredSocialClient | null): string | null;
@@ -0,0 +1,112 @@
1
+ import { marked } from 'marked';
2
+ import { tokenize } from '@atcute/bluesky-richtext-parser';
3
+ import { sanitize } from '../cal/sanitize.js';
4
+ import { getLocalProfileUrl } from '../profile-url.js';
5
+ import { socialHashtagUrl } from '../../social.js';
6
+ export function formatMonth(date) {
7
+ return date.toLocaleDateString('en-US', { month: 'short' }).toUpperCase();
8
+ }
9
+ export function formatDay(date) {
10
+ return date.getDate();
11
+ }
12
+ export function formatWeekday(date) {
13
+ return date.toLocaleDateString('en-US', { weekday: 'long' });
14
+ }
15
+ export function formatFullDate(date) {
16
+ const options = { month: 'long', day: 'numeric' };
17
+ if (date.getFullYear() !== new Date().getFullYear()) {
18
+ options.year = 'numeric';
19
+ }
20
+ return date.toLocaleDateString('en-US', options);
21
+ }
22
+ export function formatTime(date) {
23
+ return date.toLocaleTimeString('en-US', { hour: 'numeric', minute: '2-digit' });
24
+ }
25
+ export function getModeLabel(mode) {
26
+ if (mode.includes('virtual'))
27
+ return 'Virtual';
28
+ if (mode.includes('hybrid'))
29
+ return 'Hybrid';
30
+ if (mode.includes('inperson'))
31
+ return 'In-Person';
32
+ return 'Event';
33
+ }
34
+ /** Optional local accent class for an event mode. */
35
+ export function getModeColor(mode) {
36
+ if (mode.includes('virtual'))
37
+ return 'cyan';
38
+ if (mode.includes('hybrid'))
39
+ return 'purple';
40
+ if (mode.includes('inperson'))
41
+ return 'amber';
42
+ return '';
43
+ }
44
+ const renderer = new marked.Renderer();
45
+ renderer.link = ({ href, text }) => {
46
+ const attributes = href.startsWith('/')
47
+ ? ''
48
+ : ' target="_blank" rel="noopener noreferrer nofollow"';
49
+ const safeHref = href.replace(/&/g, '&amp;').replace(/"/g, '&quot;').replace(/</g, '&lt;');
50
+ return `<a${attributes} href="${safeHref}" class="text-accent-600 dark:text-accent-400 hover:underline">${text}</a>`;
51
+ };
52
+ function renderDescription(text, facets, socialClient) {
53
+ let result = text;
54
+ // Imported descriptions may have no facets. Keep existing Markdown links and
55
+ // code intact; only turn detected standalone mentions and hashtags into links.
56
+ if (!facets?.length) {
57
+ result = tokenize(text)
58
+ .map((token) => {
59
+ if (token.type === 'mention')
60
+ return `[${token.raw}](${getLocalProfileUrl(token.handle)})`;
61
+ if (token.type === 'topic')
62
+ return `[${token.raw}](<${socialHashtagUrl(token.name, socialClient)}>)`;
63
+ return token.raw;
64
+ })
65
+ .join('');
66
+ }
67
+ if (facets && facets.length > 0) {
68
+ const encoded = new TextEncoder().encode(text);
69
+ const decoder = new TextDecoder();
70
+ const sorted = [...facets].sort((a, b) => a.index.byteStart - b.index.byteStart);
71
+ const parts = [];
72
+ let cursor = 0;
73
+ for (const facet of sorted) {
74
+ const feature = facet.features?.[0];
75
+ if (!feature)
76
+ continue;
77
+ if (facet.index.byteStart < cursor)
78
+ continue;
79
+ const segmentText = decoder.decode(encoded.slice(facet.index.byteStart, facet.index.byteEnd));
80
+ let mdLink = null;
81
+ switch (feature.$type) {
82
+ case 'app.bsky.richtext.facet#mention': {
83
+ const handle = segmentText.startsWith('@') ? segmentText.slice(1) : segmentText;
84
+ mdLink = `[${segmentText}](${getLocalProfileUrl(feature.did || handle)})`;
85
+ break;
86
+ }
87
+ case 'app.bsky.richtext.facet#link':
88
+ mdLink = `[${segmentText}](${feature.uri})`;
89
+ break;
90
+ case 'app.bsky.richtext.facet#tag':
91
+ if (feature.tag)
92
+ mdLink = `[${segmentText}](<${socialHashtagUrl(feature.tag, socialClient)}>)`;
93
+ break;
94
+ }
95
+ if (mdLink) {
96
+ parts.push(decoder.decode(encoded.slice(cursor, facet.index.byteStart)));
97
+ parts.push(mdLink);
98
+ cursor = facet.index.byteEnd;
99
+ }
100
+ }
101
+ parts.push(decoder.decode(encoded.slice(cursor)));
102
+ result = parts.join('');
103
+ }
104
+ return marked.parse(result, { renderer });
105
+ }
106
+ export function buildDescriptionHtml(description, facets, socialClient) {
107
+ if (!description)
108
+ return null;
109
+ return sanitize(renderDescription(description, facets, socialClient), {
110
+ ADD_ATTR: ['target']
111
+ });
112
+ }
@@ -0,0 +1,36 @@
1
+ import type { EventLocationVariant } from '../event-types.js';
2
+ export type Coordinates = {
3
+ lat: number;
4
+ lng: number;
5
+ };
6
+ export type LocationData = {
7
+ kind: 'address' | 'geo' | 'fsq' | 'hthree' | 'place' | 'uri';
8
+ name?: string;
9
+ label: string;
10
+ detail?: string;
11
+ href?: string;
12
+ /** Compact label for event cards; addresses keep the existing city-only treatment. */
13
+ summary?: string;
14
+ coordinates?: Coordinates;
15
+ h3Cell?: string;
16
+ /** Only street addresses may be sent to the geocoder. */
17
+ geocodeQuery?: string;
18
+ };
19
+ /** Preserve record order and supported variants, keeping only the first entry for each URI. */
20
+ export declare function getLocations(locations: EventLocationVariant[] | undefined): LocationData[];
21
+ export type LocationLine = {
22
+ label: string;
23
+ href?: string;
24
+ muted: boolean;
25
+ };
26
+ /** One pin for the place: venue names above addresses, coordinates only as a fallback. */
27
+ export declare function getPhysicalLocationLines(locations: LocationData[]): LocationLine[];
28
+ /** Cards stay compact: prefer cities, falling back to the other physical variants. */
29
+ export declare function getLocationSummary(locations: LocationData[]): string | undefined;
30
+ export type GeoLocation = Coordinates & {
31
+ label: string;
32
+ googleMapsUrl: string;
33
+ osmUrl: string;
34
+ };
35
+ /** A single overview map: prefer explicit coordinates; otherwise geocode the first address. */
36
+ export declare function resolveGeoLocation(locations: LocationData[]): Promise<GeoLocation | null>;