@atmo-dev/events-ui 0.1.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 (113) hide show
  1. package/dist/DatePicker.svelte +231 -0
  2. package/dist/DatePicker.svelte.d.ts +11 -0
  3. package/dist/DateTimePicker.svelte +101 -0
  4. package/dist/DateTimePicker.svelte.d.ts +9 -0
  5. package/dist/EventAttendees.svelte +203 -0
  6. package/dist/EventAttendees.svelte.d.ts +13 -0
  7. package/dist/EventCard.svelte +131 -0
  8. package/dist/EventCard.svelte.d.ts +8 -0
  9. package/dist/EventComments.svelte +99 -0
  10. package/dist/EventComments.svelte.d.ts +6 -0
  11. package/dist/EventEditor.svelte +589 -0
  12. package/dist/EventEditor.svelte.d.ts +20 -0
  13. package/dist/EventRsvp.svelte +237 -0
  14. package/dist/EventRsvp.svelte.d.ts +17 -0
  15. package/dist/EventView.svelte +433 -0
  16. package/dist/EventView.svelte.d.ts +16 -0
  17. package/dist/ImageDropper.svelte +66 -0
  18. package/dist/ImageDropper.svelte.d.ts +7 -0
  19. package/dist/Map.svelte +27 -0
  20. package/dist/Map.svelte.d.ts +8 -0
  21. package/dist/PostToBlueskyModal.svelte +244 -0
  22. package/dist/PostToBlueskyModal.svelte.d.ts +22 -0
  23. package/dist/ShareModal.svelte +160 -0
  24. package/dist/ShareModal.svelte.d.ts +23 -0
  25. package/dist/ThemeApply.svelte +50 -0
  26. package/dist/ThemeApply.svelte.d.ts +7 -0
  27. package/dist/ThemeBackground.svelte +33 -0
  28. package/dist/ThemeBackground.svelte.d.ts +7 -0
  29. package/dist/ThemePicker.svelte +102 -0
  30. package/dist/ThemePicker.svelte.d.ts +7 -0
  31. package/dist/ThumbnailPresets.svelte +68 -0
  32. package/dist/ThumbnailPresets.svelte.d.ts +11 -0
  33. package/dist/TimePicker.svelte +188 -0
  34. package/dist/TimePicker.svelte.d.ts +9 -0
  35. package/dist/TimezonePicker.svelte +132 -0
  36. package/dist/TimezonePicker.svelte.d.ts +6 -0
  37. package/dist/VodPlayer.svelte +137 -0
  38. package/dist/VodPlayer.svelte.d.ts +14 -0
  39. package/dist/VodTranscript.svelte +72 -0
  40. package/dist/VodTranscript.svelte.d.ts +8 -0
  41. package/dist/atproto-helpers.d.ts +21 -0
  42. package/dist/atproto-helpers.js +61 -0
  43. package/dist/cal/helper.d.ts +1 -0
  44. package/dist/cal/helper.js +20 -0
  45. package/dist/cal/ical.d.ts +22 -0
  46. package/dist/cal/ical.js +188 -0
  47. package/dist/cal/sanitize.d.ts +3 -0
  48. package/dist/cal/sanitize.js +25 -0
  49. package/dist/contrail.d.ts +54 -0
  50. package/dist/contrail.js +22 -0
  51. package/dist/date-format.d.ts +22 -0
  52. package/dist/date-format.js +43 -0
  53. package/dist/editor/LinksSection.svelte +144 -0
  54. package/dist/editor/LinksSection.svelte.d.ts +10 -0
  55. package/dist/editor/LocationSection.svelte +215 -0
  56. package/dist/editor/LocationSection.svelte.d.ts +8 -0
  57. package/dist/editor/RecurringModal.svelte +270 -0
  58. package/dist/editor/RecurringModal.svelte.d.ts +30 -0
  59. package/dist/editor/ThemeSection.svelte +39 -0
  60. package/dist/editor/ThemeSection.svelte.d.ts +7 -0
  61. package/dist/editor/ThumbnailSection.svelte +219 -0
  62. package/dist/editor/ThumbnailSection.svelte.d.ts +13 -0
  63. package/dist/editor/adapter.d.ts +98 -0
  64. package/dist/editor/adapter.js +9 -0
  65. package/dist/editor/save.d.ts +42 -0
  66. package/dist/editor/save.js +154 -0
  67. package/dist/editor/types.d.ts +39 -0
  68. package/dist/editor/types.js +9 -0
  69. package/dist/event-types.d.ts +70 -0
  70. package/dist/event-types.js +11 -0
  71. package/dist/event-view/AddToCalendarButton.svelte +42 -0
  72. package/dist/event-view/AddToCalendarButton.svelte.d.ts +9 -0
  73. package/dist/event-view/EventBadges.svelte +20 -0
  74. package/dist/event-view/EventBadges.svelte.d.ts +7 -0
  75. package/dist/event-view/EventDateBlock.svelte +43 -0
  76. package/dist/event-view/EventDateBlock.svelte.d.ts +7 -0
  77. package/dist/event-view/EventHostedBy.svelte +63 -0
  78. package/dist/event-view/EventHostedBy.svelte.d.ts +16 -0
  79. package/dist/event-view/EventLinksList.svelte +37 -0
  80. package/dist/event-view/EventLinksList.svelte.d.ts +9 -0
  81. package/dist/event-view/EventLocationBlock.svelte +48 -0
  82. package/dist/event-view/EventLocationBlock.svelte.d.ts +7 -0
  83. package/dist/event-view/EventLocationMap.svelte +72 -0
  84. package/dist/event-view/EventLocationMap.svelte.d.ts +8 -0
  85. package/dist/event-view/ExternalRsvpNotice.svelte +44 -0
  86. package/dist/event-view/ExternalRsvpNotice.svelte.d.ts +6 -0
  87. package/dist/event-view/InviteShareFlow.svelte +177 -0
  88. package/dist/event-view/InviteShareFlow.svelte.d.ts +15 -0
  89. package/dist/event-view/StreamPlacePlayer.svelte +222 -0
  90. package/dist/event-view/StreamPlacePlayer.svelte.d.ts +8 -0
  91. package/dist/event-view/format.d.ts +26 -0
  92. package/dist/event-view/format.js +145 -0
  93. package/dist/index.d.ts +18 -0
  94. package/dist/index.js +18 -0
  95. package/dist/profile-url.d.ts +1 -0
  96. package/dist/profile-url.js +7 -0
  97. package/dist/theme.d.ts +9 -0
  98. package/dist/theme.js +22 -0
  99. package/dist/themes/Blobs.svelte +35 -0
  100. package/dist/themes/Blobs.svelte.d.ts +26 -0
  101. package/dist/themes/Butterflies.svelte +185 -0
  102. package/dist/themes/Butterflies.svelte.d.ts +3 -0
  103. package/dist/themes/Fireflies.svelte +134 -0
  104. package/dist/themes/Fireflies.svelte.d.ts +3 -0
  105. package/dist/themes/Kaleidoscope.svelte +177 -0
  106. package/dist/themes/Kaleidoscope.svelte.d.ts +3 -0
  107. package/dist/themes/Matrix.svelte +150 -0
  108. package/dist/themes/Matrix.svelte.d.ts +3 -0
  109. package/dist/themes/Stars.svelte +98 -0
  110. package/dist/themes/Stars.svelte.d.ts +3 -0
  111. package/dist/thumbnails/designs.d.ts +18 -0
  112. package/dist/thumbnails/designs.js +316 -0
  113. package/package.json +95 -0
@@ -0,0 +1,16 @@
1
+ import type { EditorAdapter, EditorViewer } from './editor/adapter.js';
2
+ type $$ComponentProps = {
3
+ data: any;
4
+ adapter: EditorAdapter;
5
+ viewer: EditorViewer;
6
+ /** Current page URL — used for the OG image link and the calendar button. */
7
+ pageUrl: URL;
8
+ embedMode?: boolean;
9
+ /** When set, the share modal / Bluesky post embed use this URL instead
10
+ * of the canonical atmo.rsvp event URL. Useful for embedders that want
11
+ * share links to point at their own event page. */
12
+ shareUrlOverride?: string;
13
+ };
14
+ declare const EventView: import("svelte").Component<$$ComponentProps, {}, "">;
15
+ type EventView = ReturnType<typeof EventView>;
16
+ export default EventView;
@@ -0,0 +1,66 @@
1
+ <script lang="ts">
2
+ import { Portal } from 'bits-ui';
3
+
4
+ let {
5
+ processImageFile,
6
+ isDragOver = $bindable()
7
+ }: {
8
+ processImageFile: (file: File) => Promise<void>;
9
+ isDragOver: boolean;
10
+ } = $props();
11
+
12
+ function handleDragOver(event: DragEvent) {
13
+ event.preventDefault();
14
+ event.stopPropagation();
15
+
16
+ const dt = event.dataTransfer;
17
+ if (!dt) return;
18
+
19
+ let imageCount = 0;
20
+ if (dt.items) {
21
+ for (let i = 0; i < dt.items.length; i++) {
22
+ const item = dt.items[i];
23
+ if (item && item.kind === 'file' && item.type.startsWith('image/')) {
24
+ imageCount++;
25
+ }
26
+ }
27
+ } else if (dt.files) {
28
+ for (let i = 0; i < dt.files.length; i++) {
29
+ const file = dt.files[i];
30
+ if (file?.type.startsWith('image/')) {
31
+ imageCount++;
32
+ }
33
+ }
34
+ }
35
+
36
+ isDragOver = imageCount > 0;
37
+ }
38
+ function handleDragLeave(event: DragEvent) {
39
+ event.preventDefault();
40
+ event.stopPropagation();
41
+ isDragOver = false;
42
+ }
43
+ async function handleDrop(event: DragEvent) {
44
+ event.preventDefault();
45
+ event.stopPropagation();
46
+ isDragOver = false;
47
+ if (!event.dataTransfer?.files?.length) return;
48
+ for (const file of event.dataTransfer.files) {
49
+ if (file?.type.startsWith('image/')) {
50
+ await processImageFile(file);
51
+ }
52
+ }
53
+ }
54
+ </script>
55
+
56
+ <svelte:window ondragover={handleDragOver} ondragleave={handleDragLeave} ondrop={handleDrop} />
57
+
58
+ {#if isDragOver}
59
+ <Portal>
60
+ <div
61
+ class="bg-base-100/80 dark:bg-base-900/80 text-primary dark:text-base-100 pointer-events-none absolute inset-0 z-[1000] flex items-center justify-center text-4xl font-bold backdrop-blur-md"
62
+ >
63
+ Drop file to add it to your message
64
+ </div>
65
+ </Portal>
66
+ {/if}
@@ -0,0 +1,7 @@
1
+ type $$ComponentProps = {
2
+ processImageFile: (file: File) => Promise<void>;
3
+ isDragOver: boolean;
4
+ };
5
+ declare const ImageDropper: import("svelte").Component<$$ComponentProps, {}, "isDragOver">;
6
+ type ImageDropper = ReturnType<typeof ImageDropper>;
7
+ export default ImageDropper;
@@ -0,0 +1,27 @@
1
+ <script lang="ts">
2
+ import { MapLibre, Projection, Marker, AttributionControl } from 'svelte-maplibre-gl';
3
+ import maplibregl from 'maplibre-gl';
4
+
5
+ let { lat, lng, zoom = 11 }: { lat: number; lng: number; zoom?: number } = $props();
6
+
7
+ let map: maplibregl.Map | undefined = $state();
8
+ </script>
9
+
10
+ <MapLibre
11
+ bind:map
12
+ class="h-full w-full overflow-hidden rounded-xl"
13
+ style="https://tiles.openfreemap.org/styles/liberty"
14
+ {zoom}
15
+ center={[lng, lat]}
16
+ attributionControl={false}
17
+ >
18
+ <AttributionControl position="bottom-left" compact={false} />
19
+ <Projection type="globe" />
20
+ <Marker lnglat={[lng, lat]}>
21
+ {#snippet content()}
22
+ <div class="from-accent-400 size-10 rounded-full bg-radial via-transparent p-3">
23
+ <div class="bg-accent-500 size-4 rounded-full ring-2 ring-white"></div>
24
+ </div>
25
+ {/snippet}
26
+ </Marker>
27
+ </MapLibre>
@@ -0,0 +1,8 @@
1
+ type $$ComponentProps = {
2
+ lat: number;
3
+ lng: number;
4
+ zoom?: number;
5
+ };
6
+ declare const Map: import("svelte").Component<$$ComponentProps, {}, "">;
7
+ type Map = ReturnType<typeof Map>;
8
+ export default Map;
@@ -0,0 +1,244 @@
1
+ <script lang="ts">
2
+ import { Modal, Button, Checkbox, Label } from '@foxui/core';
3
+ import {
4
+ MicrobloggingPostCreator,
5
+ editorJsonToBlueskyPost,
6
+ createBlueskyMentionSearch,
7
+ LinkCard,
8
+ type MicrobloggingPostContent
9
+ } from '@foxui/social';
10
+ import type { JSONContent, SvelteTiptap } from '@foxui/text';
11
+ import type { Readable } from 'svelte/store';
12
+ import { get } from 'svelte/store';
13
+ import type { EditorAdapter, EditorViewer } from './editor/adapter.js';
14
+
15
+ let {
16
+ open = $bindable(false),
17
+ canSetEventComments = false,
18
+ eventDid,
19
+ eventRkey,
20
+ eventName,
21
+ eventUrl,
22
+ eventDescription,
23
+ ogImageUrl,
24
+ initialText,
25
+ adapter,
26
+ viewer,
27
+ onPosted
28
+ }: {
29
+ open: boolean;
30
+ canSetEventComments?: boolean;
31
+ eventDid: string;
32
+ eventRkey: string;
33
+ eventName: string;
34
+ eventUrl: string;
35
+ eventDescription?: string;
36
+ ogImageUrl?: string;
37
+ initialText: string;
38
+ adapter: EditorAdapter;
39
+ viewer: EditorViewer;
40
+ onPosted?: (ref: { uri: string; cid: string; showComments: boolean }) => void;
41
+ } = $props();
42
+
43
+ function textToDoc(text: string): JSONContent {
44
+ const lines = text.split('\n');
45
+ const content = lines.map((line) =>
46
+ line.length > 0
47
+ ? { type: 'paragraph', content: [{ type: 'text', text: line }] }
48
+ : { type: 'paragraph' }
49
+ );
50
+ return { type: 'doc', content } as JSONContent;
51
+ }
52
+
53
+ const searchMentions = createBlueskyMentionSearch();
54
+
55
+ // Seed from the initial prop; the $effect below re-pushes it into the editor
56
+ // each time the modal opens, so only capturing the initial value is intentional.
57
+ // svelte-ignore state_referenced_locally
58
+ let postContent = $state<MicrobloggingPostContent>({
59
+ text: initialText,
60
+ json: textToDoc(initialText)
61
+ });
62
+ let editorStore = $state<Readable<SvelteTiptap.Editor> | undefined>();
63
+ let prefilledForOpen = $state(false);
64
+ let showComments = $state(true);
65
+ let posting = $state(false);
66
+ let errorMessage = $state<string | null>(null);
67
+
68
+ // Each time the modal opens, push the initial text into the editor once it's
69
+ // ready. The editor instance arrives via a readable store after PlainTextEditor
70
+ // has mounted internally, so we subscribe and write content on the first
71
+ // non-null value we see for this open cycle.
72
+ $effect(() => {
73
+ if (!open) {
74
+ prefilledForOpen = false;
75
+ showComments = true;
76
+ errorMessage = null;
77
+ return;
78
+ }
79
+ if (!editorStore || prefilledForOpen) return;
80
+ const unsub = editorStore.subscribe((ed) => {
81
+ if (!ed || prefilledForOpen) return;
82
+ ed.commands.setContent(textToDoc(initialText));
83
+ prefilledForOpen = true;
84
+ });
85
+ return unsub;
86
+ });
87
+
88
+ // Bluesky's external embed accepts a thumb blob up to ~1MB. Fetch the OG
89
+ // image, upload it to the user's PDS, return a clean blob ref. On any failure
90
+ // (CORS, large image, network) we fall back to a thumb-less embed rather than
91
+ // blocking the post.
92
+ async function fetchAndUploadThumbnail(url: string) {
93
+ try {
94
+ const resp = await fetch(url);
95
+ if (!resp.ok) return null;
96
+ const blob = await resp.blob();
97
+ if (!blob.type.startsWith('image/')) return null;
98
+ if (blob.size > 1_000_000) return null;
99
+ const result = await adapter.uploadBlob(blob);
100
+ return {
101
+ $type: result.$type,
102
+ ref: result.ref,
103
+ mimeType: result.mimeType,
104
+ size: result.size
105
+ };
106
+ } catch (err) {
107
+ console.warn('PostToBlueskyModal: thumbnail upload failed, posting without thumb', err);
108
+ return null;
109
+ }
110
+ }
111
+
112
+ async function handlePost() {
113
+ if (!viewer.did || posting) return;
114
+
115
+ // Read the editor's live JSON directly — postContent only updates on the
116
+ // editor's onupdate callback and can lag behind setContent prefills.
117
+ const editor = editorStore ? get(editorStore) : undefined;
118
+ const liveJson: JSONContent = editor
119
+ ? (editor.getJSON() as JSONContent)
120
+ : postContent.json;
121
+
122
+ posting = true;
123
+ errorMessage = null;
124
+ try {
125
+ const { text, facets } = editorJsonToBlueskyPost(liveJson);
126
+
127
+ if (!text.trim()) {
128
+ errorMessage = 'Post text cannot be empty';
129
+ posting = false;
130
+ return;
131
+ }
132
+
133
+ const externalEmbed: Record<string, unknown> = {
134
+ uri: eventUrl,
135
+ title: eventName,
136
+ description: eventDescription ?? ''
137
+ };
138
+ if (ogImageUrl) {
139
+ const thumb = await fetchAndUploadThumbnail(ogImageUrl);
140
+ if (thumb) externalEmbed.thumb = thumb;
141
+ }
142
+
143
+ const postRecord: Record<string, unknown> = {
144
+ $type: 'app.bsky.feed.post',
145
+ text,
146
+ createdAt: new Date().toISOString(),
147
+ embed: {
148
+ $type: 'app.bsky.embed.external',
149
+ external: externalEmbed
150
+ }
151
+ };
152
+ if (facets.length > 0) postRecord.facets = facets;
153
+
154
+ const postResp = await adapter.createRecord({
155
+ collection: 'app.bsky.feed.post',
156
+ record: postRecord
157
+ });
158
+ if (!postResp.uri || !postResp.cid) {
159
+ console.error('PostToBlueskyModal: PDS response missing uri/cid', postResp);
160
+ throw new Error(
161
+ 'PDS rejected the post — try logging out and back in to refresh permissions'
162
+ );
163
+ }
164
+ const postUri = postResp.uri;
165
+ const postCid = postResp.cid;
166
+
167
+ if (canSetEventComments) {
168
+ const fresh = await adapter.getRecord({
169
+ did: eventDid,
170
+ collection: 'community.lexicon.calendar.event',
171
+ rkey: eventRkey
172
+ });
173
+ const updatedRecord = {
174
+ ...fresh.value,
175
+ bskyPostRef: {
176
+ uri: postUri,
177
+ cid: postCid,
178
+ showComments
179
+ }
180
+ };
181
+ await adapter.putRecord({
182
+ collection: 'community.lexicon.calendar.event',
183
+ rkey: eventRkey,
184
+ record: updatedRecord
185
+ });
186
+
187
+ await adapter.notifyUpdate?.(
188
+ `at://${eventDid}/community.lexicon.calendar.event/${eventRkey}`
189
+ );
190
+ }
191
+
192
+ onPosted?.({ uri: postUri, cid: postCid, showComments });
193
+ open = false;
194
+ } catch (err) {
195
+ console.error('PostToBlueskyModal: post failed', err);
196
+ errorMessage = err instanceof Error ? err.message : 'Failed to post';
197
+ } finally {
198
+ posting = false;
199
+ }
200
+ }
201
+ </script>
202
+
203
+ <Modal bind:open>
204
+ <div class="space-y-4">
205
+ <h2 class="text-base-900 dark:text-base-50 text-xl font-bold">Share to Bluesky</h2>
206
+
207
+ <div
208
+ class="border-base-200 dark:border-base-800 bg-base-50 dark:bg-base-950/30 space-y-3 rounded-xl border p-3"
209
+ >
210
+ <MicrobloggingPostCreator
211
+ bind:editor={editorStore}
212
+ bind:content={postContent}
213
+ {searchMentions}
214
+ maxLength={300}
215
+ textEditorClass="max-h-48 overflow-y-auto"
216
+ />
217
+ <LinkCard
218
+ href={eventUrl}
219
+ meta={{
220
+ title: eventName,
221
+ description: eventDescription,
222
+ image: ogImageUrl
223
+ }}
224
+ />
225
+ </div>
226
+
227
+ {#if canSetEventComments}
228
+ <Label class="flex items-center gap-2">
229
+ <Checkbox bind:checked={showComments} />
230
+ <span class="text-base-700 dark:text-base-300 text-sm">
231
+ Show comments on event page
232
+ </span>
233
+ </Label>
234
+ {/if}
235
+
236
+ {#if errorMessage}
237
+ <p class="text-red-600 dark:text-red-400 text-sm">{errorMessage}</p>
238
+ {/if}
239
+
240
+ <Button class="w-full" onclick={handlePost} disabled={posting}>
241
+ {posting ? 'Posting…' : 'Post'}
242
+ </Button>
243
+ </div>
244
+ </Modal>
@@ -0,0 +1,22 @@
1
+ import type { EditorAdapter, EditorViewer } from './editor/adapter.js';
2
+ type $$ComponentProps = {
3
+ open: boolean;
4
+ canSetEventComments?: boolean;
5
+ eventDid: string;
6
+ eventRkey: string;
7
+ eventName: string;
8
+ eventUrl: string;
9
+ eventDescription?: string;
10
+ ogImageUrl?: string;
11
+ initialText: string;
12
+ adapter: EditorAdapter;
13
+ viewer: EditorViewer;
14
+ onPosted?: (ref: {
15
+ uri: string;
16
+ cid: string;
17
+ showComments: boolean;
18
+ }) => void;
19
+ };
20
+ declare const PostToBlueskyModal: import("svelte").Component<$$ComponentProps, {}, "open">;
21
+ type PostToBlueskyModal = ReturnType<typeof PostToBlueskyModal>;
22
+ export default PostToBlueskyModal;
@@ -0,0 +1,160 @@
1
+ <script lang="ts">
2
+ import { Modal, Button, Avatar } from '@foxui/core';
3
+ import { LinkCard } from '@foxui/social';
4
+ import PostToBlueskyModal from './PostToBlueskyModal.svelte';
5
+ import type { EditorAdapter, EditorViewer } from './editor/adapter.js';
6
+
7
+ let {
8
+ open = $bindable(false),
9
+ url,
10
+ title = 'Event created!',
11
+ shareText,
12
+ eventName,
13
+ ogImageUrl,
14
+ canSetEventComments = false,
15
+ eventDid,
16
+ eventRkey,
17
+ eventDescription,
18
+ adapter,
19
+ viewer,
20
+ onPosted
21
+ }: {
22
+ open: boolean;
23
+ url: string;
24
+ title?: string;
25
+ shareText?: string;
26
+ eventName?: string;
27
+ ogImageUrl?: string;
28
+ canSetEventComments?: boolean;
29
+ eventDid?: string;
30
+ eventRkey?: string;
31
+ eventDescription?: string;
32
+ adapter: EditorAdapter;
33
+ viewer: EditorViewer;
34
+ onPosted?: (ref: { uri: string; cid: string; showComments: boolean }) => void;
35
+ } = $props();
36
+
37
+ let copiedUrl = $state(false);
38
+ let copiedText = $state(false);
39
+ let showPostModal = $state(false);
40
+
41
+ let textBeforeUrl = $derived(shareText ? shareText.replace(url, '').trim() : undefined);
42
+
43
+ async function copyUrl() {
44
+ try {
45
+ await navigator.clipboard.writeText(url);
46
+ copiedUrl = true;
47
+ setTimeout(() => (copiedUrl = false), 2000);
48
+ } catch {}
49
+ }
50
+
51
+ async function copyText() {
52
+ if (!shareText) return;
53
+ try {
54
+ await navigator.clipboard.writeText(shareText);
55
+ copiedText = true;
56
+ setTimeout(() => (copiedText = false), 2000);
57
+ } catch {}
58
+ }
59
+
60
+ let blueskyButton: HTMLElement | null = $state(null);
61
+
62
+ let canPostDirectly = $derived(
63
+ !!eventDid && !!eventRkey && !!eventName && viewer.isLoggedIn
64
+ );
65
+
66
+ let blueskyIntentUrl = $derived(
67
+ `https://bsky.app/intent/compose?text=${encodeURIComponent(shareText || url)}`
68
+ );
69
+
70
+ function handleBlueskyClick() {
71
+ showPostModal = true;
72
+ }
73
+
74
+ function handlePostedFromInner(ref: { uri: string; cid: string; showComments: boolean }) {
75
+ onPosted?.(ref);
76
+ open = false;
77
+ }
78
+ </script>
79
+
80
+ <Modal
81
+ bind:open
82
+ onOpenAutoFocus={(e) => {
83
+ e.preventDefault();
84
+ blueskyButton?.focus();
85
+ }}
86
+ >
87
+ <div>
88
+ <h2 class="text-base-900 dark:text-base-50 mb-2 text-xl font-bold">{title}</h2>
89
+ <p class="text-base-500 dark:text-base-400 mb-4 text-sm">Share it with others!</p>
90
+
91
+ <div
92
+ class="bg-base-200 dark:bg-base-950/30 border-base-400/40 dark:border-base-700 mb-6 overflow-hidden rounded-xl border px-4 py-3 text-left"
93
+ >
94
+ {#if viewer.isLoggedIn}
95
+ <div class="flex items-center gap-2 pb-4">
96
+ <Avatar src={viewer.avatar} alt="" class="size-6" />
97
+ <span class="text-base-700 dark:text-base-200 text-sm font-medium"
98
+ >{viewer.handle ?? viewer.did}</span
99
+ >
100
+ </div>
101
+ {/if}
102
+ {#if textBeforeUrl}
103
+ <p class="text-base-700 dark:text-base-200 text-md font-semibold">{textBeforeUrl}</p>
104
+ {/if}
105
+ {#if eventName}
106
+ <LinkCard
107
+ href={url}
108
+ meta={{
109
+ title: eventName,
110
+ image: ogImageUrl
111
+ }}
112
+ class="mb-1"
113
+ />
114
+ {/if}
115
+ </div>
116
+
117
+ <div class="flex flex-col gap-2 sm:flex-row">
118
+ <Button class="flex-1" variant="secondary" onclick={copyUrl}>
119
+ {copiedUrl ? 'Copied!' : 'Copy link'}
120
+ </Button>
121
+ {#if shareText}
122
+ <Button class="flex-1" variant="secondary" onclick={copyText}>
123
+ {copiedText ? 'Copied!' : 'Copy text'}
124
+ </Button>
125
+ {/if}
126
+ {#if canPostDirectly}
127
+ <Button bind:ref={blueskyButton} class="flex-1" onclick={handleBlueskyClick}>
128
+ Share to Bluesky
129
+ </Button>
130
+ {:else}
131
+ <Button
132
+ bind:ref={blueskyButton}
133
+ class="flex-1"
134
+ href={blueskyIntentUrl}
135
+ target="_blank"
136
+ rel="noopener"
137
+ >
138
+ Share to Bluesky
139
+ </Button>
140
+ {/if}
141
+ </div>
142
+ </div>
143
+ </Modal>
144
+
145
+ {#if canPostDirectly && eventDid && eventRkey && eventName}
146
+ <PostToBlueskyModal
147
+ bind:open={showPostModal}
148
+ {canSetEventComments}
149
+ {eventDid}
150
+ {eventRkey}
151
+ {eventName}
152
+ eventUrl={url}
153
+ {eventDescription}
154
+ {ogImageUrl}
155
+ initialText={textBeforeUrl ?? eventName}
156
+ {adapter}
157
+ {viewer}
158
+ onPosted={handlePostedFromInner}
159
+ />
160
+ {/if}
@@ -0,0 +1,23 @@
1
+ import type { EditorAdapter, EditorViewer } from './editor/adapter.js';
2
+ type $$ComponentProps = {
3
+ open: boolean;
4
+ url: string;
5
+ title?: string;
6
+ shareText?: string;
7
+ eventName?: string;
8
+ ogImageUrl?: string;
9
+ canSetEventComments?: boolean;
10
+ eventDid?: string;
11
+ eventRkey?: string;
12
+ eventDescription?: string;
13
+ adapter: EditorAdapter;
14
+ viewer: EditorViewer;
15
+ onPosted?: (ref: {
16
+ uri: string;
17
+ cid: string;
18
+ showComments: boolean;
19
+ }) => void;
20
+ };
21
+ declare const ShareModal: import("svelte").Component<$$ComponentProps, {}, "open">;
22
+ type ShareModal = ReturnType<typeof ShareModal>;
23
+ export default ShareModal;
@@ -0,0 +1,50 @@
1
+ <script lang="ts">
2
+ import { BROWSER as browser } from 'esm-env';
3
+
4
+ let {
5
+ accentColor = 'cyan',
6
+ baseColor = 'mist'
7
+ }: {
8
+ accentColor?: string;
9
+ baseColor?: string;
10
+ } = $props();
11
+
12
+ const allAccentColors = [
13
+ 'red', 'orange', 'amber', 'yellow', 'lime', 'green', 'emerald',
14
+ 'teal', 'cyan', 'sky', 'blue', 'indigo', 'violet', 'purple',
15
+ 'fuchsia', 'pink', 'rose'
16
+ ];
17
+ const allBaseColors = [
18
+ 'gray', 'stone', 'zinc', 'neutral', 'slate', 'mist', 'sand',
19
+ 'olive', 'mauve', 'sage'
20
+ ];
21
+
22
+ const allColors = [...allAccentColors, ...allBaseColors];
23
+
24
+ const safeJson = (v: string) => JSON.stringify(v).replace(/</g, '\\u003c');
25
+
26
+ // SSR: inline script that removes all color classes then adds the correct ones before paint
27
+ const allColorsJson = JSON.stringify(allColors);
28
+
29
+ let script = $derived(
30
+ `<script>(function(){var e=document.documentElement,r=${allColorsJson};r.forEach(function(c){e.classList.remove(c)});e.classList.add(${safeJson(accentColor)},${safeJson(baseColor)});})()<` +
31
+ '/script>'
32
+ );
33
+
34
+ // Client: reactive effect for client-side navigations
35
+ $effect(() => {
36
+ if (!browser) return;
37
+ const el = document.documentElement;
38
+ el.classList.remove(...allColors);
39
+ el.classList.add(accentColor, baseColor);
40
+
41
+ return () => {
42
+ el.classList.remove(...allColors);
43
+ el.classList.add('cyan', 'mist');
44
+ };
45
+ });
46
+ </script>
47
+
48
+ <svelte:head>
49
+ {@html script}
50
+ </svelte:head>
@@ -0,0 +1,7 @@
1
+ type $$ComponentProps = {
2
+ accentColor?: string;
3
+ baseColor?: string;
4
+ };
5
+ declare const ThemeApply: import("svelte").Component<$$ComponentProps, {}, "">;
6
+ type ThemeApply = ReturnType<typeof ThemeApply>;
7
+ export default ThemeApply;
@@ -0,0 +1,33 @@
1
+ <script lang="ts">
2
+ import type { EventTheme } from './theme.js';
3
+ import Blobs from './themes/Blobs.svelte';
4
+ import Stars from './themes/Stars.svelte';
5
+ import Matrix from './themes/Matrix.svelte';
6
+ import Fireflies from './themes/Fireflies.svelte';
7
+ import Butterflies from './themes/Butterflies.svelte';
8
+ import Kaleidoscope from './themes/Kaleidoscope.svelte';
9
+
10
+ let {
11
+ theme
12
+ }: {
13
+ theme: EventTheme;
14
+ } = $props();
15
+
16
+ let key = $derived(`${theme.name}-${theme.accentColor}-${theme.baseColor}`);
17
+ </script>
18
+
19
+ {#key key}
20
+ {#if theme.name === 'blobs'}
21
+ <Blobs />
22
+ {:else if theme.name === 'warp'}
23
+ <Stars />
24
+ {:else if theme.name === 'matrix'}
25
+ <Matrix />
26
+ {:else if theme.name === 'fireflies'}
27
+ <Fireflies />
28
+ {:else if theme.name === 'butterflies'}
29
+ <Butterflies />
30
+ {:else if theme.name === 'kaleidoscope'}
31
+ <Kaleidoscope />
32
+ {/if}
33
+ {/key}
@@ -0,0 +1,7 @@
1
+ import type { EventTheme } from './theme.js';
2
+ type $$ComponentProps = {
3
+ theme: EventTheme;
4
+ };
5
+ declare const ThemeBackground: import("svelte").Component<$$ComponentProps, {}, "">;
6
+ type ThemeBackground = ReturnType<typeof ThemeBackground>;
7
+ export default ThemeBackground;