@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,540 @@
1
+ <script lang="ts">
2
+ import { getCDNImageBlobUrl } from './atproto-helpers.js';
3
+ import { getEventThumbnail } from './event-media.js';
4
+ import { Avatar, Button, ToggleGroup, ToggleGroupItem } from '../index.js';
5
+ import { onMount } from 'svelte';
6
+ import { InlineEditor, type Editor } from '../index.js';
7
+ import DateTimePicker from './DateTimePicker.svelte';
8
+ import TimezonePicker from './TimezonePicker.svelte';
9
+ import { parseDateTime } from '@internationalized/date';
10
+ import { isoToDatetimeLocalInTz } from './date-format.js';
11
+ import type { FlatEventRecord } from './contrail.js';
12
+ import ThemeApply from './ThemeApply.svelte';
13
+ import ThemeBackground from './ThemeBackground.svelte';
14
+ import { defaultTheme, type EventTheme } from './theme.js';
15
+
16
+ import ThumbnailSection from './editor/ThumbnailSection.svelte';
17
+ import LocationSection from './editor/LocationSection.svelte';
18
+ import LinksSection from './editor/LinksSection.svelte';
19
+ import ThemeSection from './editor/ThemeSection.svelte';
20
+ import RecurringModal from './editor/RecurringModal.svelte';
21
+ import {
22
+ stripModePrefix,
23
+ type EventEditorPrefill,
24
+ type EventLocation,
25
+ type EventMode,
26
+ type Visibility
27
+ } from './editor/types';
28
+ import { buildEventRecord, buildThumbnailMedia, renderPresetThumbnail } from './editor/save';
29
+ import { LocationValidationError } from './editor/location';
30
+ import { DEFAULT_PRESET, hashSeed } from './thumbnails/designs';
31
+ import type { EditorAdapter, EditorViewer } from './editor/adapter';
32
+
33
+ let {
34
+ eventData = null,
35
+ actorDid,
36
+ rkey,
37
+ adapter,
38
+ viewer,
39
+ initialTheme,
40
+ prefill = null
41
+ }: {
42
+ eventData: FlatEventRecord | null;
43
+ actorDid: string;
44
+ rkey: string;
45
+ adapter: EditorAdapter;
46
+ viewer: EditorViewer;
47
+ /** Override default theme for new events (e.g. inherit embedder's palette). */
48
+ initialTheme?: Partial<EventTheme>;
49
+ /** Autofill payload for new events (e.g. imported from Luma/Meetup). */
50
+ prefill?: EventEditorPrefill | null;
51
+ } = $props();
52
+
53
+ let isNew = $derived(eventData === null);
54
+
55
+ // svelte-ignore state_referenced_locally
56
+ let thumbnailChanged = $state(eventData === null && (prefill?.thumbnailFile ?? null) !== null);
57
+
58
+ // Initial values: prefer prefill (only honored for brand-new events) so the
59
+ // title editor and date inputs see the imported values on their first render
60
+ // — TipTap reads `content` only once at mount.
61
+ // svelte-ignore state_referenced_locally
62
+ const initialPrefill = eventData === null ? prefill : null;
63
+ const initialTimezone =
64
+ initialPrefill?.timezone ?? Intl.DateTimeFormat().resolvedOptions().timeZone;
65
+
66
+ // svelte-ignore state_referenced_locally
67
+ let name = $state(initialPrefill?.name ?? eventData?.name ?? '');
68
+ let description = $state(initialPrefill?.description ?? '');
69
+ let startsAt = $state(
70
+ initialPrefill?.startsAt ? isoToDatetimeLocalInTz(initialPrefill.startsAt, initialTimezone) : ''
71
+ );
72
+ let endsAt = $state(
73
+ initialPrefill?.endsAt ? isoToDatetimeLocalInTz(initialPrefill.endsAt, initialTimezone) : ''
74
+ );
75
+ let timezone = $state(initialTimezone);
76
+ let mode: EventMode = $state(initialPrefill?.mode ?? 'inperson');
77
+ let visibility: Visibility = $state('public');
78
+ // svelte-ignore state_referenced_locally
79
+ let eventTheme: EventTheme = $state(
80
+ eventData === null
81
+ ? {
82
+ ...defaultTheme,
83
+ accentColor: initialTheme?.accentColor ?? defaultTheme.accentColor,
84
+ ...(initialTheme?.baseColor ? { baseColor: initialTheme.baseColor } : {})
85
+ }
86
+ : { ...defaultTheme }
87
+ );
88
+ const initialThumbnailFile = initialPrefill?.thumbnailFile ?? null;
89
+ let thumbnailFile: File | null = $state(initialThumbnailFile);
90
+ let thumbnailPreview: string | null = $state(
91
+ initialThumbnailFile ? URL.createObjectURL(initialThumbnailFile) : null
92
+ );
93
+ // svelte-ignore state_referenced_locally
94
+ let selectedPreset: string | null = $state(
95
+ initialThumbnailFile ? null : eventData === null ? DEFAULT_PRESET : null
96
+ );
97
+ let submitting = $state(false);
98
+ let error: string | null = $state(null);
99
+ let titleEditor = $state<Editor | null>(null);
100
+
101
+ let location: EventLocation | null = $state(
102
+ initialPrefill?.location ? { ...initialPrefill.location } : null
103
+ );
104
+ let locationChanged = $state(initialPrefill?.location != null);
105
+
106
+ let links: Array<{ uri: string; name: string }> = $state(
107
+ initialPrefill?.links ? initialPrefill.links.map((l) => ({ ...l })) : []
108
+ );
109
+
110
+ let showRecurringModal = $state(false);
111
+
112
+ function populateLocationFromEventData() {
113
+ if (!eventData) return;
114
+ if (eventData.locations && eventData.locations.length > 0) {
115
+ const loc = eventData.locations.find(
116
+ (v) => v.$type === 'community.lexicon.location.address'
117
+ ) as { street?: string; locality?: string; region?: string; country?: string } | undefined;
118
+ if (loc) {
119
+ const street = loc.street || undefined;
120
+ const locality = loc.locality || undefined;
121
+ const region = loc.region || undefined;
122
+ const country = loc.country || undefined;
123
+ location = {
124
+ ...(street && { street }),
125
+ ...(locality && { locality }),
126
+ ...(region && { region }),
127
+ ...(country && { country })
128
+ };
129
+ }
130
+ }
131
+ locationChanged = false;
132
+ }
133
+
134
+ function populateThumbnailFromEventData() {
135
+ if (!eventData) return;
136
+ if (eventData.media && eventData.media.length > 0) {
137
+ const media = getEventThumbnail(eventData.media);
138
+ if (media?.content) {
139
+ const url = getCDNImageBlobUrl({ did: actorDid, blob: media.content });
140
+ if (url) {
141
+ thumbnailPreview = url;
142
+ thumbnailChanged = false;
143
+ }
144
+ }
145
+ }
146
+ }
147
+
148
+ function populateFromEventData() {
149
+ if (!eventData) return;
150
+ name = eventData.name || '';
151
+ description = eventData.description || '';
152
+ // Restore the event's authored timezone first so the wall-clock fields we
153
+ // populate below land in that zone (not the viewer's browser zone).
154
+ if (eventData.timezone) timezone = eventData.timezone;
155
+ startsAt = eventData.startsAt ? isoToDatetimeLocalInTz(eventData.startsAt, timezone) : '';
156
+ endsAt = eventData.endsAt ? isoToDatetimeLocalInTz(eventData.endsAt, timezone) : '';
157
+ mode = eventData.mode ? stripModePrefix(eventData.mode) : 'inperson';
158
+ const prefs = (eventData as unknown as { preferences?: { showInDiscovery?: boolean } })
159
+ .preferences;
160
+ if (prefs && prefs.showInDiscovery === false) visibility = 'unlisted';
161
+ else visibility = 'public';
162
+ links = eventData.uris ? eventData.uris.map((l) => ({ uri: l.uri, name: l.name || '' })) : [];
163
+ if (eventData.theme) eventTheme = { ...eventData.theme };
164
+ populateLocationFromEventData();
165
+ populateThumbnailFromEventData();
166
+ }
167
+
168
+ onMount(() => {
169
+ if (!isNew) populateFromEventData();
170
+ titleEditor?.commands.focus();
171
+ });
172
+
173
+ let hostName = $derived(viewer.displayName || viewer.handle || viewer.did || '');
174
+
175
+ let thumbnailDateStr = $derived.by(() => {
176
+ if (!startsAt) return '';
177
+ const d = new Date(startsAt);
178
+ if (isNaN(d.getTime())) return '';
179
+ return d.toLocaleDateString('en-US', { weekday: 'short', month: 'short', day: 'numeric' });
180
+ });
181
+
182
+ // Trim a CalendarDateTime.toString() ("YYYY-MM-DDTHH:mm:ss[.sss]") down to
183
+ // the "YYYY-MM-DDTHH:mm" shape that <input type="datetime-local"> expects.
184
+ function cdtToDatetimeLocal(s: string): string {
185
+ return s.slice(0, 16);
186
+ }
187
+
188
+ // Auto-set end date to 1 hour after start if empty
189
+ $effect(() => {
190
+ if (startsAt && !endsAt) {
191
+ endsAt = cdtToDatetimeLocal(parseDateTime(startsAt).add({ hours: 1 }).toString());
192
+ }
193
+ });
194
+
195
+ // Auto-adjust end date if start moves past it
196
+ $effect(() => {
197
+ if (startsAt && endsAt) {
198
+ const s = parseDateTime(startsAt);
199
+ const e = parseDateTime(endsAt);
200
+ if (s.compare(e) >= 0) {
201
+ endsAt = cdtToDatetimeLocal(s.add({ hours: 1 }).toString());
202
+ }
203
+ }
204
+ });
205
+
206
+ async function handleSubmit() {
207
+ error = null;
208
+
209
+ if (!name.trim()) return void (error = 'Name is required.');
210
+ if (!startsAt) return void (error = 'Start date is required.');
211
+ if (!endsAt) return void (error = 'End date is required.');
212
+ if (!viewer.isLoggedIn || !viewer.did) return void (error = 'You must be logged in.');
213
+
214
+ submitting = true;
215
+
216
+ try {
217
+ // Generate thumbnail from preset if selected and no custom upload
218
+ if (selectedPreset && !thumbnailFile) {
219
+ const rendered = await renderPresetThumbnail({
220
+ design: selectedPreset,
221
+ seed: hashSeed(rkey),
222
+ name,
223
+ dateStr: thumbnailDateStr,
224
+ accent: eventTheme.accentColor
225
+ });
226
+ if (rendered) {
227
+ thumbnailFile = rendered;
228
+ thumbnailChanged = true;
229
+ }
230
+ }
231
+
232
+ const existingMedia = (eventData?.media ?? []) as Array<Record<string, unknown>>;
233
+ const media = await buildThumbnailMedia({
234
+ isNew,
235
+ thumbnailChanged,
236
+ thumbnailFile,
237
+ existingMedia,
238
+ uploadBlob: (blob) =>
239
+ adapter.uploadBlob(blob) as unknown as Promise<Record<string, unknown>>
240
+ });
241
+
242
+ const record = await buildEventRecord({
243
+ eventData,
244
+ isNew,
245
+ name,
246
+ description,
247
+ startsAt,
248
+ endsAt,
249
+ timezone,
250
+ mode,
251
+ visibility,
252
+ theme: eventTheme,
253
+ links,
254
+ location,
255
+ locationChanged,
256
+ media,
257
+ resolveHandle: (handle) => adapter.resolveHandle(handle)
258
+ });
259
+
260
+ if (isNew && prefill?.additionalData) {
261
+ const existing = (record.additionalData ?? {}) as Record<string, unknown>;
262
+ record.additionalData = { ...existing, ...prefill.additionalData };
263
+ }
264
+
265
+ const result = await adapter.putRecord({
266
+ collection: 'community.lexicon.calendar.event',
267
+ rkey,
268
+ record
269
+ });
270
+
271
+ await adapter.notifyUpdate?.(result.uri);
272
+ adapter.onSaved({ uri: result.uri, rkey, isNew });
273
+ } catch (e) {
274
+ console.error(`Failed to ${isNew ? 'create' : 'save'} event:`, e);
275
+ error =
276
+ e instanceof LocationValidationError
277
+ ? e.message
278
+ : `Failed to ${isNew ? 'create' : 'save'} event. Please try again.`;
279
+ } finally {
280
+ submitting = false;
281
+ }
282
+ }
283
+
284
+ let showDeleteConfirm = $state(false);
285
+ let deleting = $state(false);
286
+
287
+ async function handleDelete() {
288
+ deleting = true;
289
+ try {
290
+ await adapter.deleteRecord({
291
+ collection: 'community.lexicon.calendar.event',
292
+ rkey
293
+ });
294
+ const eventUri = `at://${viewer.did}/community.lexicon.calendar.event/${rkey}`;
295
+ await adapter.notifyUpdate?.(eventUri);
296
+ adapter.onDeleted?.();
297
+ } catch (e) {
298
+ console.error('Failed to delete event:', e);
299
+ error = 'Failed to delete event. Please try again.';
300
+ } finally {
301
+ deleting = false;
302
+ showDeleteConfirm = false;
303
+ }
304
+ }
305
+ </script>
306
+
307
+ <ThemeApply accentColor={eventTheme.accentColor} baseColor={eventTheme.baseColor} />
308
+ <div class="motion-reduce:hidden"><ThemeBackground theme={eventTheme} /></div>
309
+
310
+ <div class="px-6 py-12 sm:py-12">
311
+ <div class="mx-auto max-w-3xl">
312
+ {#if !viewer.isLoggedIn}
313
+ <div
314
+ class="rounded-2xl border border-base-200 bg-base-100 p-8 text-center dark:border-base-800 dark:bg-base-900/50"
315
+ >
316
+ <p class="mb-4 text-base-600 dark:text-base-400">
317
+ Log in to {isNew ? 'create an event' : 'edit this event'}.
318
+ </p>
319
+ <Button onclick={() => adapter.requestLogin()}>Log in</Button>
320
+ </div>
321
+ {:else}
322
+ <form
323
+ onsubmit={(e) => {
324
+ e.preventDefault();
325
+ handleSubmit();
326
+ }}
327
+ >
328
+ <!-- Two-column layout mirroring detail page -->
329
+ <div
330
+ class="grid grid-cols-1 gap-8 md:grid-cols-[14rem_1fr] md:gap-x-10 md:gap-y-6 lg:grid-cols-[16rem_1fr]"
331
+ >
332
+ <ThumbnailSection
333
+ {rkey}
334
+ {name}
335
+ dateStr={thumbnailDateStr}
336
+ accent={eventTheme.accentColor}
337
+ bind:thumbnailFile
338
+ bind:thumbnailPreview
339
+ bind:thumbnailChanged
340
+ bind:selectedPreset
341
+ />
342
+
343
+ <!-- Right column: event details -->
344
+ <div class="order-2 min-w-0 md:order-0 md:col-start-2 md:row-span-5 md:row-start-1">
345
+ <!-- Name -->
346
+ <div class="mb-2 min-h-14">
347
+ <InlineEditor
348
+ bind:value={name}
349
+ bind:editor={titleEditor}
350
+ label="Event name"
351
+ singleLine
352
+ placeholder="Event name"
353
+ class="w-full text-3xl leading-tight font-semibold tracking-tight text-base-900 placeholder:text-base-500 focus-within:outline-none focus:outline-none sm:text-4xl dark:text-base-50 dark:placeholder:text-base-500"
354
+ />
355
+ </div>
356
+
357
+ <!-- Mode toggle -->
358
+ <div class="mb-3">
359
+ <ToggleGroup
360
+ type="single"
361
+ bind:value={
362
+ () => mode,
363
+ (val) => {
364
+ if (val) mode = val;
365
+ }
366
+ }
367
+ class="w-fit"
368
+ size="xs"
369
+ >
370
+ <ToggleGroupItem value="inperson">In Person</ToggleGroupItem>
371
+ <ToggleGroupItem value="virtual">Virtual</ToggleGroupItem>
372
+ <ToggleGroupItem value="hybrid">Hybrid</ToggleGroupItem>
373
+ </ToggleGroup>
374
+ </div>
375
+
376
+ <!-- Visibility toggle -->
377
+ <div class="mb-8">
378
+ <ToggleGroup
379
+ type="single"
380
+ bind:value={
381
+ () => visibility,
382
+ (val) => {
383
+ if (val) visibility = val as Visibility;
384
+ }
385
+ }
386
+ class="w-fit"
387
+ size="xs"
388
+ >
389
+ <ToggleGroupItem value="public">Public</ToggleGroupItem>
390
+ <ToggleGroupItem value="unlisted">Unlisted</ToggleGroupItem>
391
+ </ToggleGroup>
392
+ <div class="mt-1.5 text-xs text-muted">
393
+ {#if visibility === 'public'}
394
+ Anyone can view and it appears in discovery.
395
+ {:else}
396
+ Public to anyone with the link, but hidden from discovery.
397
+ {/if}
398
+ </div>
399
+ </div>
400
+
401
+ <!-- Date row -->
402
+ <div class="mb-5 grid min-w-0 gap-2">
403
+ <div class="flex min-w-0 items-center gap-2">
404
+ <span class="w-9 shrink-0 text-sm text-muted">Start</span>
405
+ <DateTimePicker bind:value={startsAt} label="Start" required />
406
+ </div>
407
+ <div class="flex min-w-0 items-center gap-2">
408
+ <span class="w-9 shrink-0 text-sm text-muted">End</span>
409
+ <DateTimePicker
410
+ bind:value={endsAt}
411
+ label="End"
412
+ minValue={startsAt}
413
+ referenceTime={startsAt}
414
+ />
415
+ </div>
416
+ <div class="min-w-0 pl-11">
417
+ <TimezonePicker bind:value={timezone} />
418
+ </div>
419
+ </div>
420
+
421
+ <LocationSection bind:location bind:locationChanged />
422
+
423
+ <!-- About Event -->
424
+ <div class="mt-8 mb-8">
425
+ <p class="mb-3 text-xs font-semibold tracking-wider text-muted uppercase">About</p>
426
+ <textarea
427
+ aria-label="Event description"
428
+ bind:value={description}
429
+ rows={4}
430
+ placeholder="What's this event about? @mentions, #hashtags and links will be detected automatically."
431
+ class="w-full resize-none border-0 bg-transparent px-0 leading-relaxed text-base-700 placeholder:text-base-500 focus:border-0 focus:ring-0 focus:outline-none dark:text-base-300 dark:placeholder:text-base-500"
432
+ style="field-sizing: content;"></textarea>
433
+ <p class="mt-2 text-xs text-muted">
434
+ Add a hashtag like <span class="font-medium">#webdev</span> or
435
+ <span class="font-medium">#music</span>
436
+ to have this event appear under a
437
+ <a href="/topics" class="text-action underline underline-offset-2">topic</a>.
438
+ </p>
439
+ </div>
440
+
441
+ {#if error}
442
+ <p class="mb-4 text-sm text-red-600 dark:text-red-400">{error}</p>
443
+ {/if}
444
+
445
+ <Button type="submit" disabled={submitting || !name.trim() || !startsAt || !endsAt}>
446
+ {submitting
447
+ ? isNew
448
+ ? 'Publishing...'
449
+ : 'Saving...'
450
+ : isNew
451
+ ? 'Publish Event'
452
+ : 'Save Changes'}
453
+ </Button>
454
+ {#if !isNew && adapter.features.recurring}
455
+ <Button
456
+ type="button"
457
+ variant="secondary"
458
+ disabled={submitting || !name.trim() || !startsAt || !endsAt}
459
+ onclick={() => (showRecurringModal = true)}
460
+ >
461
+ Add recurring events
462
+ </Button>
463
+ {/if}
464
+ </div>
465
+
466
+ <!-- Hosted By -->
467
+ <div class="order-3 md:order-0 md:col-start-1">
468
+ <p class="mb-3 text-xs font-semibold tracking-wider text-muted uppercase">Hosted By</p>
469
+ <div class="flex items-center gap-2.5">
470
+ <Avatar src={viewer.avatar} name={hostName} class="size-8 shrink-0" />
471
+ <span class="truncate text-sm font-medium text-base-900 dark:text-base-100">
472
+ {hostName}
473
+ </span>
474
+ </div>
475
+ </div>
476
+
477
+ <div class="order-4 space-y-6 md:order-0 md:col-start-1">
478
+ <LinksSection bind:links {adapter} {viewer} />
479
+ <ThemeSection bind:theme={eventTheme} />
480
+ </div>
481
+ </div>
482
+
483
+ {#if !isNew && adapter.features.delete}
484
+ <div class="mt-12 border-t border-base-200 pt-8 dark:border-base-800">
485
+ {#if showDeleteConfirm}
486
+ <div class="flex items-center gap-3">
487
+ <p class="text-sm text-red-600 dark:text-red-400">
488
+ Are you sure? This cannot be undone.
489
+ </p>
490
+ <Button
491
+ variant="secondary"
492
+ size="sm"
493
+ onclick={() => (showDeleteConfirm = false)}
494
+ disabled={deleting}
495
+ >
496
+ Cancel
497
+ </Button>
498
+ <Button
499
+ size="sm"
500
+ onclick={handleDelete}
501
+ disabled={deleting}
502
+ variant="primary"
503
+ class="red"
504
+ >
505
+ {deleting ? 'Deleting...' : 'Delete'}
506
+ </Button>
507
+ </div>
508
+ {:else}
509
+ <Button variant="primary" class="red" onclick={() => (showDeleteConfirm = true)}
510
+ >Delete event</Button
511
+ >
512
+ {/if}
513
+ </div>
514
+ {/if}
515
+ </form>
516
+ {/if}
517
+ </div>
518
+ </div>
519
+
520
+ <RecurringModal
521
+ bind:open={showRecurringModal}
522
+ {rkey}
523
+ {eventData}
524
+ {isNew}
525
+ {name}
526
+ {startsAt}
527
+ {endsAt}
528
+ {mode}
529
+ {timezone}
530
+ {description}
531
+ {links}
532
+ {location}
533
+ {thumbnailDateStr}
534
+ {thumbnailFile}
535
+ {thumbnailChanged}
536
+ {selectedPreset}
537
+ {adapter}
538
+ {viewer}
539
+ accent={eventTheme.accentColor}
540
+ />
@@ -0,0 +1,18 @@
1
+ import type { FlatEventRecord } from './contrail.js';
2
+ import { type EventTheme } from './theme.js';
3
+ import { type EventEditorPrefill } from './editor/types';
4
+ import type { EditorAdapter, EditorViewer } from './editor/adapter';
5
+ type $$ComponentProps = {
6
+ eventData: FlatEventRecord | null;
7
+ actorDid: string;
8
+ rkey: string;
9
+ adapter: EditorAdapter;
10
+ viewer: EditorViewer;
11
+ /** Override default theme for new events (e.g. inherit embedder's palette). */
12
+ initialTheme?: Partial<EventTheme>;
13
+ /** Autofill payload for new events (e.g. imported from Luma/Meetup). */
14
+ prefill?: EventEditorPrefill | null;
15
+ };
16
+ declare const EventEditor: import("svelte").Component<$$ComponentProps, {}, "">;
17
+ type EventEditor = ReturnType<typeof EventEditor>;
18
+ export default EventEditor;