@atmo-dev/kit 0.3.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 (180) hide show
  1. package/README.md +94 -1
  2. package/dist/components/ExternalEmbed.svelte +174 -0
  3. package/dist/components/ExternalEmbed.svelte.d.ts +8 -0
  4. package/dist/components/GrainAlbumPicker.svelte +218 -0
  5. package/dist/components/GrainAlbumPicker.svelte.d.ts +14 -0
  6. package/dist/components/GrainAlbumView.svelte +94 -0
  7. package/dist/components/GrainAlbumView.svelte.d.ts +10 -0
  8. package/dist/components/NestedComments.svelte +20 -10
  9. package/dist/components/NestedComments.svelte.d.ts +2 -0
  10. package/dist/components/PhotoGrid.svelte +53 -0
  11. package/dist/components/PhotoGrid.svelte.d.ts +9 -0
  12. package/dist/components/PhotoLightbox.svelte +332 -0
  13. package/dist/components/PhotoLightbox.svelte.d.ts +12 -0
  14. package/dist/components/PostContent.svelte +78 -20
  15. package/dist/components/PostContent.svelte.d.ts +5 -0
  16. package/dist/components/StreamplacePicker.svelte +94 -0
  17. package/dist/components/StreamplacePicker.svelte.d.ts +12 -0
  18. package/dist/events/DatePicker.svelte +222 -0
  19. package/dist/events/DatePicker.svelte.d.ts +12 -0
  20. package/dist/events/DateTimePicker.svelte +112 -0
  21. package/dist/events/DateTimePicker.svelte.d.ts +10 -0
  22. package/dist/events/EventAttendees.svelte +199 -0
  23. package/dist/events/EventAttendees.svelte.d.ts +19 -0
  24. package/dist/events/EventCard.svelte +148 -0
  25. package/dist/events/EventCard.svelte.d.ts +8 -0
  26. package/dist/events/EventComments.svelte +98 -0
  27. package/dist/events/EventComments.svelte.d.ts +8 -0
  28. package/dist/events/EventEditor.svelte +540 -0
  29. package/dist/events/EventEditor.svelte.d.ts +18 -0
  30. package/dist/events/EventRsvp.svelte +178 -0
  31. package/dist/events/EventRsvp.svelte.d.ts +16 -0
  32. package/dist/events/EventView.svelte +488 -0
  33. package/dist/events/EventView.svelte.d.ts +23 -0
  34. package/dist/events/ImageDropper.svelte +66 -0
  35. package/dist/events/ImageDropper.svelte.d.ts +7 -0
  36. package/dist/events/Map.svelte +36 -0
  37. package/dist/events/Map.svelte.d.ts +8 -0
  38. package/dist/events/PostToBlueskyModal.svelte +246 -0
  39. package/dist/events/PostToBlueskyModal.svelte.d.ts +22 -0
  40. package/dist/events/ShareModal.svelte +174 -0
  41. package/dist/events/ShareModal.svelte.d.ts +23 -0
  42. package/dist/events/StarterPackButton.svelte +72 -0
  43. package/dist/events/StarterPackButton.svelte.d.ts +10 -0
  44. package/dist/events/ThemeApply.svelte +30 -0
  45. package/dist/events/ThemeApply.svelte.d.ts +7 -0
  46. package/dist/events/ThemeBackground.svelte +33 -0
  47. package/dist/events/ThemeBackground.svelte.d.ts +7 -0
  48. package/dist/events/ThemePicker.svelte +104 -0
  49. package/dist/events/ThemePicker.svelte.d.ts +7 -0
  50. package/dist/events/ThumbnailPresets.svelte +71 -0
  51. package/dist/events/ThumbnailPresets.svelte.d.ts +11 -0
  52. package/dist/events/TimePicker.svelte +117 -0
  53. package/dist/events/TimePicker.svelte.d.ts +10 -0
  54. package/dist/events/TimezonePicker.svelte +67 -0
  55. package/dist/events/TimezonePicker.svelte.d.ts +6 -0
  56. package/dist/events/VodPlayer.svelte +154 -0
  57. package/dist/events/VodPlayer.svelte.d.ts +14 -0
  58. package/dist/events/VodTranscript.svelte +75 -0
  59. package/dist/events/VodTranscript.svelte.d.ts +8 -0
  60. package/dist/events/atproto-helpers.d.ts +21 -0
  61. package/dist/events/atproto-helpers.js +61 -0
  62. package/dist/events/cal/helper.d.ts +1 -0
  63. package/dist/events/cal/helper.js +20 -0
  64. package/dist/events/cal/ical.d.ts +22 -0
  65. package/dist/events/cal/ical.js +188 -0
  66. package/dist/events/cal/sanitize.d.ts +3 -0
  67. package/dist/events/cal/sanitize.js +25 -0
  68. package/dist/events/conference.d.ts +65 -0
  69. package/dist/events/conference.js +61 -0
  70. package/dist/events/contrail.d.ts +49 -0
  71. package/dist/events/contrail.js +12 -0
  72. package/dist/events/date-format.d.ts +22 -0
  73. package/dist/events/date-format.js +43 -0
  74. package/dist/events/editor/LinkedRecordsPicker.svelte +104 -0
  75. package/dist/events/editor/LinkedRecordsPicker.svelte.d.ts +12 -0
  76. package/dist/events/editor/LinksSection.svelte +154 -0
  77. package/dist/events/editor/LinksSection.svelte.d.ts +13 -0
  78. package/dist/events/editor/LocationSection.svelte +163 -0
  79. package/dist/events/editor/LocationSection.svelte.d.ts +8 -0
  80. package/dist/events/editor/RecurringModal.svelte +313 -0
  81. package/dist/events/editor/RecurringModal.svelte.d.ts +30 -0
  82. package/dist/events/editor/ThemeSection.svelte +24 -0
  83. package/dist/events/editor/ThemeSection.svelte.d.ts +7 -0
  84. package/dist/events/editor/ThumbnailSection.svelte +181 -0
  85. package/dist/events/editor/ThumbnailSection.svelte.d.ts +13 -0
  86. package/dist/events/editor/adapter.d.ts +73 -0
  87. package/dist/events/editor/adapter.js +9 -0
  88. package/dist/events/editor/location.d.ts +16 -0
  89. package/dist/events/editor/location.js +50 -0
  90. package/dist/events/editor/save.d.ts +43 -0
  91. package/dist/events/editor/save.js +168 -0
  92. package/dist/events/editor/types.d.ts +39 -0
  93. package/dist/events/editor/types.js +9 -0
  94. package/dist/events/event-media.d.ts +4 -0
  95. package/dist/events/event-media.js +4 -0
  96. package/dist/events/event-types.d.ts +79 -0
  97. package/dist/events/event-types.js +11 -0
  98. package/dist/events/event-view/AddToCalendarButton.svelte +31 -0
  99. package/dist/events/event-view/AddToCalendarButton.svelte.d.ts +9 -0
  100. package/dist/events/event-view/EventBadges.svelte +20 -0
  101. package/dist/events/event-view/EventBadges.svelte.d.ts +7 -0
  102. package/dist/events/event-view/EventDateBlock.svelte +43 -0
  103. package/dist/events/event-view/EventDateBlock.svelte.d.ts +7 -0
  104. package/dist/events/event-view/EventHostedBy.svelte +61 -0
  105. package/dist/events/event-view/EventHostedBy.svelte.d.ts +16 -0
  106. package/dist/events/event-view/EventLinksList.svelte +24 -0
  107. package/dist/events/event-view/EventLinksList.svelte.d.ts +9 -0
  108. package/dist/events/event-view/EventLocationBlock.svelte +62 -0
  109. package/dist/events/event-view/EventLocationBlock.svelte.d.ts +7 -0
  110. package/dist/events/event-view/EventLocationMap.svelte +66 -0
  111. package/dist/events/event-view/EventLocationMap.svelte.d.ts +7 -0
  112. package/dist/events/event-view/ExternalRsvpNotice.svelte +34 -0
  113. package/dist/events/event-view/ExternalRsvpNotice.svelte.d.ts +6 -0
  114. package/dist/events/event-view/StreamPlacePlayer.svelte +209 -0
  115. package/dist/events/event-view/StreamPlacePlayer.svelte.d.ts +8 -0
  116. package/dist/events/event-view/format.d.ts +10 -0
  117. package/dist/events/event-view/format.js +112 -0
  118. package/dist/events/event-view/locations.d.ts +36 -0
  119. package/dist/events/event-view/locations.js +243 -0
  120. package/dist/events/event-view/plyr-icons.d.ts +3 -0
  121. package/dist/events/event-view/plyr-icons.js +46 -0
  122. package/dist/events/linked-records.d.ts +18 -0
  123. package/dist/events/linked-records.js +11 -0
  124. package/dist/events/profile-url.d.ts +3 -0
  125. package/dist/events/profile-url.js +11 -0
  126. package/dist/events/schedule/ConferenceTimetable.svelte +340 -0
  127. package/dist/events/schedule/ConferenceTimetable.svelte.d.ts +22 -0
  128. package/dist/events/schedule/DaySchedule.svelte +298 -0
  129. package/dist/events/schedule/DaySchedule.svelte.d.ts +26 -0
  130. package/dist/events/schedule/ScheduleEventCell.svelte +166 -0
  131. package/dist/events/schedule/ScheduleEventCell.svelte.d.ts +25 -0
  132. package/dist/events/schedule/schedule-utils.d.ts +44 -0
  133. package/dist/events/schedule/schedule-utils.js +166 -0
  134. package/dist/events/starter-pack-url.d.ts +3 -0
  135. package/dist/events/starter-pack-url.js +16 -0
  136. package/dist/events/theme.d.ts +9 -0
  137. package/dist/events/theme.js +36 -0
  138. package/dist/events/themes/Blobs.svelte +64 -0
  139. package/dist/events/themes/Blobs.svelte.d.ts +18 -0
  140. package/dist/events/themes/Butterflies.svelte +183 -0
  141. package/dist/events/themes/Butterflies.svelte.d.ts +3 -0
  142. package/dist/events/themes/Fireflies.svelte +134 -0
  143. package/dist/events/themes/Fireflies.svelte.d.ts +3 -0
  144. package/dist/events/themes/Kaleidoscope.svelte +183 -0
  145. package/dist/events/themes/Kaleidoscope.svelte.d.ts +3 -0
  146. package/dist/events/themes/Matrix.svelte +153 -0
  147. package/dist/events/themes/Matrix.svelte.d.ts +3 -0
  148. package/dist/events/themes/Stars.svelte +98 -0
  149. package/dist/events/themes/Stars.svelte.d.ts +3 -0
  150. package/dist/events/thumbnails/designs.d.ts +18 -0
  151. package/dist/events/thumbnails/designs.js +316 -0
  152. package/dist/events/view-data.d.ts +38 -0
  153. package/dist/events/view-data.js +1 -0
  154. package/dist/events-data.d.ts +15 -0
  155. package/dist/events-data.js +9 -0
  156. package/dist/events-editor.d.ts +2 -0
  157. package/dist/events-editor.js +2 -0
  158. package/dist/events.d.ts +19 -0
  159. package/dist/events.js +17 -0
  160. package/dist/grain/types.d.ts +41 -0
  161. package/dist/grain/types.js +1 -0
  162. package/dist/grain/urls.d.ts +7 -0
  163. package/dist/grain/urls.js +74 -0
  164. package/dist/grain/xrpc.d.ts +3 -0
  165. package/dist/grain/xrpc.js +129 -0
  166. package/dist/grain.d.ts +3 -0
  167. package/dist/grain.js +2 -0
  168. package/dist/index.d.ts +16 -0
  169. package/dist/index.js +9 -0
  170. package/dist/media.d.ts +10 -0
  171. package/dist/media.js +1 -0
  172. package/dist/social/external-embeds.d.ts +39 -0
  173. package/dist/social/external-embeds.js +107 -0
  174. package/dist/social/posts.d.ts +59 -17
  175. package/dist/social/posts.js +99 -12
  176. package/dist/social.d.ts +2 -0
  177. package/dist/social.js +1 -0
  178. package/dist/streamplace.d.ts +6 -0
  179. package/dist/streamplace.js +1 -0
  180. package/package.json +58 -4
@@ -0,0 +1,23 @@
1
+ import type { EventViewData } from './view-data.js';
2
+ import { type AttendeeCounts } from './EventAttendees.svelte';
3
+ import { type Snippet } from 'svelte';
4
+ import type { EditorAdapter, EditorViewer } from './editor/adapter.js';
5
+ type $$ComponentProps = {
6
+ data: EventViewData;
7
+ adapter: EditorAdapter;
8
+ viewer: EditorViewer;
9
+ /** Current page URL — used for the OG image link and the calendar button. */
10
+ pageUrl: URL;
11
+ embedMode?: boolean;
12
+ /** When set, the share modal / Bluesky post embed use this URL instead
13
+ * of the canonical atmo.rsvp event URL. Useful for embedders that want
14
+ * share links to point at their own event page. */
15
+ shareUrlOverride?: string;
16
+ /** App-owned actions such as private reporting; omitted in embeds. */
17
+ extraActions?: Snippet;
18
+ /** App-owned actions below the attendee lists, with current RSVP counts. */
19
+ attendeeActions?: Snippet<[AttendeeCounts]>;
20
+ };
21
+ declare const EventView: import("svelte").Component<$$ComponentProps, {}, "">;
22
+ type EventView = ReturnType<typeof EventView>;
23
+ 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="text-primary pointer-events-none absolute inset-0 z-[1000] flex items-center justify-center bg-base-100/80 text-4xl font-bold backdrop-blur-md dark:bg-base-900/80 dark:text-base-100"
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,36 @@
1
+ <script lang="ts" module>
2
+ import { setWorkerUrl } from 'maplibre-gl';
3
+ import workerUrl from 'maplibre-gl/dist/maplibre-gl-worker.mjs?url';
4
+
5
+ // Vite moves MapLibre's main module when optimizing dependencies, so its
6
+ // default relative worker URL breaks. Emit the worker as an explicit asset.
7
+ setWorkerUrl(workerUrl);
8
+ </script>
9
+
10
+ <script lang="ts">
11
+ import { MapLibre, Projection, Marker, AttributionControl } from 'svelte-maplibre-gl';
12
+ import type { Map as MapLibreMap } from 'maplibre-gl';
13
+
14
+ let { lat, lng, zoom = 11 }: { lat: number; lng: number; zoom?: number } = $props();
15
+
16
+ let map: MapLibreMap | undefined = $state();
17
+ </script>
18
+
19
+ <MapLibre
20
+ bind:map
21
+ class="h-full w-full overflow-hidden rounded-xl"
22
+ style="https://tiles.openfreemap.org/styles/liberty"
23
+ {zoom}
24
+ center={[lng, lat]}
25
+ attributionControl={false}
26
+ >
27
+ <AttributionControl position="bottom-left" compact={false} />
28
+ <Projection type="globe" />
29
+ <Marker lnglat={[lng, lat]}>
30
+ {#snippet content()}
31
+ <div class="size-10 rounded-full bg-radial from-accent-400 via-transparent p-3">
32
+ <div class="size-4 rounded-full bg-accent-500 ring-2 ring-white"></div>
33
+ </div>
34
+ {/snippet}
35
+ </Marker>
36
+ </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,246 @@
1
+ <script lang="ts">
2
+ import {
3
+ Modal,
4
+ Button,
5
+ Checkbox,
6
+ Label,
7
+ PostComposer,
8
+ editorJsonToBlueskyPost,
9
+ createActorSearch,
10
+ LinkCard,
11
+ textToDoc,
12
+ graphemeLength,
13
+ type Editor
14
+ } from '../index.js';
15
+ import { tokenize } from '@atcute/bluesky-richtext-parser';
16
+ import { tokensToFacets } from './editor/save.js';
17
+ import type { EditorAdapter, EditorViewer } from './editor/adapter.js';
18
+
19
+ let {
20
+ open = $bindable(false),
21
+ canSetEventComments = false,
22
+ eventDid,
23
+ eventRkey,
24
+ eventName,
25
+ eventUrl,
26
+ eventDescription,
27
+ ogImageUrl,
28
+ initialText,
29
+ adapter,
30
+ viewer,
31
+ onPosted
32
+ }: {
33
+ open: boolean;
34
+ canSetEventComments?: boolean;
35
+ eventDid: string;
36
+ eventRkey: string;
37
+ eventName: string;
38
+ eventUrl: string;
39
+ eventDescription?: string;
40
+ ogImageUrl?: string;
41
+ initialText: string;
42
+ adapter: EditorAdapter;
43
+ viewer: EditorViewer;
44
+ onPosted?: (ref: { uri: string; cid: string; showComments: boolean }) => void;
45
+ } = $props();
46
+
47
+ const searchActors = createActorSearch();
48
+ let text = $state('');
49
+ let editor = $state<Editor | null>(null);
50
+ let prefilledForOpen = $state(false);
51
+ let showComments = $state(true);
52
+ let posting = $state(false);
53
+ let errorMessage = $state<string | null>(null);
54
+
55
+ // Seed once per opening, after the lazy editor has mounted.
56
+ $effect(() => {
57
+ if (!open) {
58
+ prefilledForOpen = false;
59
+ showComments = true;
60
+ errorMessage = null;
61
+ return;
62
+ }
63
+ if (!editor || prefilledForOpen) return;
64
+ text = initialText;
65
+ editor.commands.setContent(textToDoc(initialText));
66
+ prefilledForOpen = true;
67
+ });
68
+
69
+ // Bluesky's external embed accepts a thumb blob up to ~1MB. Fetch the OG
70
+ // image, upload it to the user's PDS, return a clean blob ref. On any failure
71
+ // (CORS, large image, network) we fall back to a thumb-less embed rather than
72
+ // blocking the post.
73
+ async function fetchAndUploadThumbnail(url: string) {
74
+ try {
75
+ const resp = await fetch(url);
76
+ if (!resp.ok) return null;
77
+ const blob = await resp.blob();
78
+ if (!blob.type.startsWith('image/')) return null;
79
+ if (blob.size > 1_000_000) return null;
80
+ const result = await adapter.uploadBlob(blob);
81
+ return {
82
+ $type: result.$type,
83
+ ref: result.ref,
84
+ mimeType: result.mimeType,
85
+ size: result.size
86
+ };
87
+ } catch (err) {
88
+ console.warn('PostToBlueskyModal: thumbnail upload failed, posting without thumb', err);
89
+ return null;
90
+ }
91
+ }
92
+
93
+ async function handlePost() {
94
+ if (!viewer.did || !viewer.isLoggedIn || viewer.canPostToBluesky !== true || posting) return;
95
+
96
+ const liveJson = editor?.getJSON() ?? textToDoc(text);
97
+
98
+ posting = true;
99
+ errorMessage = null;
100
+ try {
101
+ const { text, facets } = editorJsonToBlueskyPost(liveJson);
102
+ if (graphemeLength(text) > 300 || new TextEncoder().encode(text).length > 3000) {
103
+ errorMessage = 'Posts must be 300 characters or fewer.';
104
+ return;
105
+ }
106
+ // Also recognize handles typed or pasted without selecting a suggestion.
107
+ const detected = await tokensToFacets(tokenize(text), (handle) =>
108
+ adapter.resolveHandle(handle)
109
+ );
110
+ for (const facet of detected) {
111
+ const index = facet.index;
112
+ if (
113
+ !facets.some(
114
+ (existing) =>
115
+ index.byteStart < existing.index.byteEnd && index.byteEnd > existing.index.byteStart
116
+ )
117
+ )
118
+ facets.push(facet);
119
+ }
120
+ facets.sort((a, b) => a.index.byteStart - b.index.byteStart);
121
+
122
+ if (!text.trim()) {
123
+ errorMessage = 'Post text cannot be empty';
124
+ posting = false;
125
+ return;
126
+ }
127
+
128
+ const externalEmbed: Record<string, unknown> = {
129
+ uri: eventUrl,
130
+ title: eventName,
131
+ description: eventDescription ?? ''
132
+ };
133
+ if (ogImageUrl) {
134
+ const thumb = await fetchAndUploadThumbnail(ogImageUrl);
135
+ if (thumb) externalEmbed.thumb = thumb;
136
+ }
137
+
138
+ const postRecord: Record<string, unknown> = {
139
+ $type: 'app.bsky.feed.post',
140
+ text,
141
+ createdAt: new Date().toISOString(),
142
+ embed: {
143
+ $type: 'app.bsky.embed.external',
144
+ external: externalEmbed
145
+ }
146
+ };
147
+ if (facets.length > 0) postRecord.facets = facets;
148
+
149
+ // The grant may have changed while resolving facets or uploading a thumb.
150
+ if (viewer.canPostToBluesky !== true) return;
151
+ const postResp = await adapter.createRecord({
152
+ collection: 'app.bsky.feed.post',
153
+ record: postRecord
154
+ });
155
+ if (!postResp.uri || !postResp.cid) {
156
+ console.error('PostToBlueskyModal: PDS response missing uri/cid', postResp);
157
+ throw new Error(
158
+ 'PDS rejected the post — try logging out and back in to refresh permissions'
159
+ );
160
+ }
161
+ const postUri = postResp.uri;
162
+ const postCid = postResp.cid;
163
+
164
+ if (canSetEventComments) {
165
+ const fresh = await adapter.getRecord({
166
+ did: eventDid,
167
+ collection: 'community.lexicon.calendar.event',
168
+ rkey: eventRkey
169
+ });
170
+ const updatedRecord = {
171
+ ...fresh.value,
172
+ bskyPostRef: {
173
+ uri: postUri,
174
+ cid: postCid,
175
+ showComments
176
+ }
177
+ };
178
+ await adapter.putRecord({
179
+ collection: 'community.lexicon.calendar.event',
180
+ rkey: eventRkey,
181
+ record: updatedRecord
182
+ });
183
+
184
+ await adapter.notifyUpdate?.(
185
+ `at://${eventDid}/community.lexicon.calendar.event/${eventRkey}`
186
+ );
187
+ }
188
+
189
+ onPosted?.({ uri: postUri, cid: postCid, showComments });
190
+ open = false;
191
+ } catch (err) {
192
+ console.error('PostToBlueskyModal: post failed', err);
193
+ errorMessage = err instanceof Error ? err.message : 'Failed to post';
194
+ } finally {
195
+ posting = false;
196
+ }
197
+ }
198
+ </script>
199
+
200
+ <Modal bind:open title="Share to Bluesky" dismissible={!posting}>
201
+ <div class="space-y-4">
202
+ <div
203
+ class="space-y-3 rounded-xl border border-base-200 bg-base-50 p-3 dark:border-base-800 dark:bg-base-950/30"
204
+ >
205
+ <PostComposer
206
+ bind:editor
207
+ bind:value={text}
208
+ {searchActors}
209
+ disabled={posting}
210
+ maxLength={300}
211
+ />
212
+ <LinkCard
213
+ href={eventUrl}
214
+ meta={{
215
+ title: eventName,
216
+ description: eventDescription,
217
+ image: ogImageUrl
218
+ }}
219
+ />
220
+ </div>
221
+
222
+ {#if canSetEventComments}
223
+ <Label class="flex items-center gap-2">
224
+ <Checkbox bind:checked={showComments} disabled={posting} />
225
+ <span class="text-sm text-base-700 dark:text-base-300"> Show comments on event page </span>
226
+ </Label>
227
+ {/if}
228
+
229
+ {#if errorMessage}
230
+ <p role="alert" class="text-sm text-red-600 dark:text-red-400">{errorMessage}</p>
231
+ {/if}
232
+
233
+ <Button
234
+ class="w-full"
235
+ onclick={handlePost}
236
+ disabled={posting ||
237
+ !viewer.isLoggedIn ||
238
+ !viewer.did ||
239
+ viewer.canPostToBluesky !== true ||
240
+ !text.trim() ||
241
+ graphemeLength(text) > 300}
242
+ >
243
+ {posting ? 'Posting…' : 'Post'}
244
+ </Button>
245
+ </div>
246
+ </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,174 @@
1
+ <script lang="ts">
2
+ import { Modal, Button, Avatar, LinkCard } from '../index.js';
3
+ import PostToBlueskyModal from './PostToBlueskyModal.svelte';
4
+ import type { EditorAdapter, EditorViewer } from './editor/adapter.js';
5
+
6
+ let {
7
+ open = $bindable(false),
8
+ url,
9
+ title = 'Event created!',
10
+ shareText,
11
+ eventName,
12
+ ogImageUrl,
13
+ canSetEventComments = false,
14
+ eventDid,
15
+ eventRkey,
16
+ eventDescription,
17
+ adapter,
18
+ viewer,
19
+ onPosted
20
+ }: {
21
+ open: boolean;
22
+ url: string;
23
+ title?: string;
24
+ shareText?: string;
25
+ eventName?: string;
26
+ ogImageUrl?: string;
27
+ canSetEventComments?: boolean;
28
+ eventDid?: string;
29
+ eventRkey?: string;
30
+ eventDescription?: string;
31
+ adapter: EditorAdapter;
32
+ viewer: EditorViewer;
33
+ onPosted?: (ref: { uri: string; cid: string; showComments: boolean }) => void;
34
+ } = $props();
35
+
36
+ let copiedUrl = $state(false);
37
+ let copiedText = $state(false);
38
+ let showPostModal = $state(false);
39
+
40
+ let textBeforeUrl = $derived(shareText ? shareText.replace(url, '').trim() : undefined);
41
+
42
+ async function copyUrl() {
43
+ try {
44
+ await navigator.clipboard.writeText(url);
45
+ copiedUrl = true;
46
+ setTimeout(() => (copiedUrl = false), 2000);
47
+ } catch {
48
+ // Clipboard access may be denied; keep the selectable link available.
49
+ }
50
+ }
51
+
52
+ async function copyText() {
53
+ if (!shareText) return;
54
+ try {
55
+ await navigator.clipboard.writeText(shareText);
56
+ copiedText = true;
57
+ setTimeout(() => (copiedText = false), 2000);
58
+ } catch {
59
+ // Clipboard access may be denied; sharing remains available.
60
+ }
61
+ }
62
+
63
+ let blueskyButton = $state<HTMLButtonElement | HTMLAnchorElement | null>(null);
64
+
65
+ let canPostDirectly = $derived(
66
+ !!eventDid &&
67
+ !!eventRkey &&
68
+ !!eventName &&
69
+ viewer.isLoggedIn &&
70
+ viewer.canPostToBluesky === true
71
+ );
72
+
73
+ $effect(() => {
74
+ if (!canPostDirectly) showPostModal = false;
75
+ });
76
+
77
+ let blueskyIntentUrl = $derived(
78
+ `https://bsky.app/intent/compose?text=${encodeURIComponent(shareText || url)}`
79
+ );
80
+
81
+ function handleBlueskyClick() {
82
+ if (canPostDirectly) showPostModal = true;
83
+ }
84
+
85
+ function handlePostedFromInner(ref: { uri: string; cid: string; showComments: boolean }) {
86
+ onPosted?.(ref);
87
+ open = false;
88
+ }
89
+ </script>
90
+
91
+ <Modal
92
+ bind:open
93
+ {title}
94
+ description="Share it with others."
95
+ onOpenAutoFocus={(e) => {
96
+ e.preventDefault();
97
+ blueskyButton?.focus();
98
+ }}
99
+ >
100
+ <div>
101
+ <div
102
+ class="mb-6 overflow-hidden rounded-xl border border-base-400/40 bg-base-200 px-4 py-3 text-left dark:border-base-700 dark:bg-base-950/30"
103
+ >
104
+ {#if viewer.isLoggedIn}
105
+ <div class="flex items-center gap-2 pb-4">
106
+ <Avatar
107
+ src={viewer.avatar}
108
+ name={viewer.displayName || viewer.handle || viewer.did || 'Account'}
109
+ class="size-6"
110
+ />
111
+ <span class="text-sm font-medium text-base-700 dark:text-base-200"
112
+ >{viewer.handle ?? viewer.did}</span
113
+ >
114
+ </div>
115
+ {/if}
116
+ {#if textBeforeUrl}
117
+ <p class="text-md mb-3 font-semibold text-base-700 dark:text-base-200">{textBeforeUrl}</p>
118
+ {/if}
119
+ {#if eventName}
120
+ <LinkCard
121
+ href={url}
122
+ meta={{
123
+ title: eventName,
124
+ image: ogImageUrl
125
+ }}
126
+ class="mb-1"
127
+ />
128
+ {/if}
129
+ </div>
130
+
131
+ <div class="flex flex-col gap-2 sm:flex-row">
132
+ <Button class="flex-1" variant="secondary" onclick={copyUrl}>
133
+ {copiedUrl ? 'Copied!' : 'Copy link'}
134
+ </Button>
135
+ {#if shareText}
136
+ <Button class="flex-1" variant="secondary" onclick={copyText}>
137
+ {copiedText ? 'Copied!' : 'Copy text'}
138
+ </Button>
139
+ {/if}
140
+ {#if canPostDirectly}
141
+ <Button bind:ref={blueskyButton} class="flex-1" onclick={handleBlueskyClick}>
142
+ Share to Bluesky
143
+ </Button>
144
+ {:else}
145
+ <Button
146
+ bind:ref={blueskyButton}
147
+ class="flex-1"
148
+ href={blueskyIntentUrl}
149
+ target="_blank"
150
+ rel="noopener"
151
+ >
152
+ Share to Bluesky
153
+ </Button>
154
+ {/if}
155
+ </div>
156
+ </div>
157
+ </Modal>
158
+
159
+ {#if canPostDirectly && eventDid && eventRkey && eventName}
160
+ <PostToBlueskyModal
161
+ bind:open={showPostModal}
162
+ {canSetEventComments}
163
+ {eventDid}
164
+ {eventRkey}
165
+ {eventName}
166
+ eventUrl={url}
167
+ {eventDescription}
168
+ {ogImageUrl}
169
+ initialText={textBeforeUrl ?? eventName}
170
+ {adapter}
171
+ {viewer}
172
+ onPosted={handlePostedFromInner}
173
+ />
174
+ {/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,72 @@
1
+ <script lang="ts">
2
+ import { Button } from '../index.js';
3
+ import { socialClientInfo, type PreferredSocialClient } from '../social.js';
4
+ import { starterPackClientUrl } from './starter-pack-url.js';
5
+
6
+ let {
7
+ eventUri,
8
+ openPack,
9
+ socialClient
10
+ }: {
11
+ eventUri: string;
12
+ socialClient?: PreferredSocialClient | null;
13
+ /** The host owns account access and creation; this component never writes records. */
14
+ openPack: (eventUri: string) => Promise<string>;
15
+ } = $props();
16
+ let pending = $state(false);
17
+ let message: string | null = $state(null);
18
+ let canonicalPackUrl: string | null = $state(null);
19
+ let packUrl = $derived(
20
+ canonicalPackUrl ? starterPackClientUrl(canonicalPackUrl, socialClient) : null
21
+ );
22
+ let clientLabel = $derived(socialClientInfo(socialClient).label);
23
+
24
+ async function open() {
25
+ if (pending) return;
26
+ // Open synchronously with the click so async creation doesn't hit popup blockers.
27
+ const tab = window.open('about:blank', '_blank');
28
+ if (tab) tab.opener = null;
29
+ pending = true;
30
+ message = null;
31
+ try {
32
+ const canonicalUrl = await openPack(eventUri);
33
+ const url = starterPackClientUrl(canonicalUrl, socialClient);
34
+ if (!url) throw new Error('Invalid starter pack URL');
35
+ canonicalPackUrl = canonicalUrl;
36
+ if (tab) {
37
+ if (!tab.closed) tab.location.replace(url);
38
+ } else window.location.assign(url);
39
+ } catch {
40
+ tab?.close();
41
+ message = 'Couldn’t open the starter pack. Please try again.';
42
+ } finally {
43
+ pending = false;
44
+ }
45
+ }
46
+ </script>
47
+
48
+ <div class="mt-4">
49
+ <p class="mb-1 text-xs text-muted">Follow attendees on {clientLabel}.</p>
50
+ {#if packUrl}
51
+ <a
52
+ href={packUrl}
53
+ target="_blank"
54
+ rel="noopener noreferrer"
55
+ class="rounded text-sm text-action underline-offset-4 hover:underline focus-visible:outline-2 focus-visible:outline-offset-4 focus-visible:outline-focus"
56
+ >Starter pack ↗</a
57
+ >
58
+ {:else}
59
+ <Button
60
+ variant="link"
61
+ size="sm"
62
+ class="min-h-0 p-0 text-sm font-normal"
63
+ onclick={open}
64
+ disabled={pending}
65
+ >
66
+ {pending ? 'Opening…' : 'Starter pack ↗'}
67
+ </Button>
68
+ {/if}
69
+ {#if message}
70
+ <p role="alert" class="mt-2 text-sm text-muted">{message}</p>
71
+ {/if}
72
+ </div>
@@ -0,0 +1,10 @@
1
+ import { type PreferredSocialClient } from '../social.js';
2
+ type $$ComponentProps = {
3
+ eventUri: string;
4
+ socialClient?: PreferredSocialClient | null;
5
+ /** The host owns account access and creation; this component never writes records. */
6
+ openPack: (eventUri: string) => Promise<string>;
7
+ };
8
+ declare const StarterPackButton: import("svelte").Component<$$ComponentProps, {}, "">;
9
+ type StarterPackButton = ReturnType<typeof StarterPackButton>;
10
+ export default StarterPackButton;