@atmo-dev/kit 0.4.0 → 0.6.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 (150) hide show
  1. package/README.md +119 -0
  2. package/dist/at-tags.d.ts +18 -0
  3. package/dist/at-tags.js +18 -0
  4. package/dist/components/AtTags.svelte +17 -0
  5. package/dist/components/AtTags.svelte.d.ts +7 -0
  6. package/dist/events/DatePicker.svelte +222 -0
  7. package/dist/events/DatePicker.svelte.d.ts +12 -0
  8. package/dist/events/DateTimePicker.svelte +112 -0
  9. package/dist/events/DateTimePicker.svelte.d.ts +10 -0
  10. package/dist/events/EventAttendees.svelte +199 -0
  11. package/dist/events/EventAttendees.svelte.d.ts +19 -0
  12. package/dist/events/EventCard.svelte +148 -0
  13. package/dist/events/EventCard.svelte.d.ts +8 -0
  14. package/dist/events/EventComments.svelte +98 -0
  15. package/dist/events/EventComments.svelte.d.ts +8 -0
  16. package/dist/events/EventEditor.svelte +540 -0
  17. package/dist/events/EventEditor.svelte.d.ts +18 -0
  18. package/dist/events/EventRsvp.svelte +178 -0
  19. package/dist/events/EventRsvp.svelte.d.ts +16 -0
  20. package/dist/events/EventView.svelte +488 -0
  21. package/dist/events/EventView.svelte.d.ts +23 -0
  22. package/dist/events/ImageDropper.svelte +66 -0
  23. package/dist/events/ImageDropper.svelte.d.ts +7 -0
  24. package/dist/events/Map.svelte +36 -0
  25. package/dist/events/Map.svelte.d.ts +8 -0
  26. package/dist/events/PostToBlueskyModal.svelte +246 -0
  27. package/dist/events/PostToBlueskyModal.svelte.d.ts +22 -0
  28. package/dist/events/ShareModal.svelte +174 -0
  29. package/dist/events/ShareModal.svelte.d.ts +23 -0
  30. package/dist/events/StarterPackButton.svelte +72 -0
  31. package/dist/events/StarterPackButton.svelte.d.ts +10 -0
  32. package/dist/events/ThemeApply.svelte +30 -0
  33. package/dist/events/ThemeApply.svelte.d.ts +7 -0
  34. package/dist/events/ThemeBackground.svelte +33 -0
  35. package/dist/events/ThemeBackground.svelte.d.ts +7 -0
  36. package/dist/events/ThemePicker.svelte +104 -0
  37. package/dist/events/ThemePicker.svelte.d.ts +7 -0
  38. package/dist/events/ThumbnailPresets.svelte +71 -0
  39. package/dist/events/ThumbnailPresets.svelte.d.ts +11 -0
  40. package/dist/events/TimePicker.svelte +117 -0
  41. package/dist/events/TimePicker.svelte.d.ts +10 -0
  42. package/dist/events/TimezonePicker.svelte +67 -0
  43. package/dist/events/TimezonePicker.svelte.d.ts +6 -0
  44. package/dist/events/VodPlayer.svelte +154 -0
  45. package/dist/events/VodPlayer.svelte.d.ts +14 -0
  46. package/dist/events/VodTranscript.svelte +75 -0
  47. package/dist/events/VodTranscript.svelte.d.ts +8 -0
  48. package/dist/events/atproto-helpers.d.ts +21 -0
  49. package/dist/events/atproto-helpers.js +61 -0
  50. package/dist/events/cal/helper.d.ts +1 -0
  51. package/dist/events/cal/helper.js +20 -0
  52. package/dist/events/cal/ical.d.ts +22 -0
  53. package/dist/events/cal/ical.js +188 -0
  54. package/dist/events/cal/sanitize.d.ts +3 -0
  55. package/dist/events/cal/sanitize.js +25 -0
  56. package/dist/events/conference.d.ts +65 -0
  57. package/dist/events/conference.js +61 -0
  58. package/dist/events/contrail.d.ts +49 -0
  59. package/dist/events/contrail.js +12 -0
  60. package/dist/events/date-format.d.ts +22 -0
  61. package/dist/events/date-format.js +43 -0
  62. package/dist/events/editor/LinkedRecordsPicker.svelte +104 -0
  63. package/dist/events/editor/LinkedRecordsPicker.svelte.d.ts +12 -0
  64. package/dist/events/editor/LinksSection.svelte +154 -0
  65. package/dist/events/editor/LinksSection.svelte.d.ts +13 -0
  66. package/dist/events/editor/LocationSection.svelte +163 -0
  67. package/dist/events/editor/LocationSection.svelte.d.ts +8 -0
  68. package/dist/events/editor/RecurringModal.svelte +313 -0
  69. package/dist/events/editor/RecurringModal.svelte.d.ts +30 -0
  70. package/dist/events/editor/ThemeSection.svelte +24 -0
  71. package/dist/events/editor/ThemeSection.svelte.d.ts +7 -0
  72. package/dist/events/editor/ThumbnailSection.svelte +181 -0
  73. package/dist/events/editor/ThumbnailSection.svelte.d.ts +13 -0
  74. package/dist/events/editor/adapter.d.ts +73 -0
  75. package/dist/events/editor/adapter.js +9 -0
  76. package/dist/events/editor/location.d.ts +16 -0
  77. package/dist/events/editor/location.js +50 -0
  78. package/dist/events/editor/save.d.ts +43 -0
  79. package/dist/events/editor/save.js +168 -0
  80. package/dist/events/editor/types.d.ts +39 -0
  81. package/dist/events/editor/types.js +9 -0
  82. package/dist/events/event-media.d.ts +4 -0
  83. package/dist/events/event-media.js +4 -0
  84. package/dist/events/event-types.d.ts +79 -0
  85. package/dist/events/event-types.js +11 -0
  86. package/dist/events/event-view/AddToCalendarButton.svelte +31 -0
  87. package/dist/events/event-view/AddToCalendarButton.svelte.d.ts +9 -0
  88. package/dist/events/event-view/EventBadges.svelte +20 -0
  89. package/dist/events/event-view/EventBadges.svelte.d.ts +7 -0
  90. package/dist/events/event-view/EventDateBlock.svelte +43 -0
  91. package/dist/events/event-view/EventDateBlock.svelte.d.ts +7 -0
  92. package/dist/events/event-view/EventHostedBy.svelte +61 -0
  93. package/dist/events/event-view/EventHostedBy.svelte.d.ts +16 -0
  94. package/dist/events/event-view/EventLinksList.svelte +24 -0
  95. package/dist/events/event-view/EventLinksList.svelte.d.ts +9 -0
  96. package/dist/events/event-view/EventLocationBlock.svelte +62 -0
  97. package/dist/events/event-view/EventLocationBlock.svelte.d.ts +7 -0
  98. package/dist/events/event-view/EventLocationMap.svelte +66 -0
  99. package/dist/events/event-view/EventLocationMap.svelte.d.ts +7 -0
  100. package/dist/events/event-view/ExternalRsvpNotice.svelte +34 -0
  101. package/dist/events/event-view/ExternalRsvpNotice.svelte.d.ts +6 -0
  102. package/dist/events/event-view/StreamPlacePlayer.svelte +209 -0
  103. package/dist/events/event-view/StreamPlacePlayer.svelte.d.ts +8 -0
  104. package/dist/events/event-view/format.d.ts +10 -0
  105. package/dist/events/event-view/format.js +112 -0
  106. package/dist/events/event-view/locations.d.ts +36 -0
  107. package/dist/events/event-view/locations.js +243 -0
  108. package/dist/events/event-view/plyr-icons.d.ts +3 -0
  109. package/dist/events/event-view/plyr-icons.js +46 -0
  110. package/dist/events/linked-records.d.ts +18 -0
  111. package/dist/events/linked-records.js +11 -0
  112. package/dist/events/profile-url.d.ts +3 -0
  113. package/dist/events/profile-url.js +11 -0
  114. package/dist/events/schedule/ConferenceTimetable.svelte +340 -0
  115. package/dist/events/schedule/ConferenceTimetable.svelte.d.ts +22 -0
  116. package/dist/events/schedule/DaySchedule.svelte +298 -0
  117. package/dist/events/schedule/DaySchedule.svelte.d.ts +26 -0
  118. package/dist/events/schedule/ScheduleEventCell.svelte +166 -0
  119. package/dist/events/schedule/ScheduleEventCell.svelte.d.ts +25 -0
  120. package/dist/events/schedule/schedule-utils.d.ts +44 -0
  121. package/dist/events/schedule/schedule-utils.js +166 -0
  122. package/dist/events/starter-pack-url.d.ts +3 -0
  123. package/dist/events/starter-pack-url.js +16 -0
  124. package/dist/events/theme.d.ts +9 -0
  125. package/dist/events/theme.js +36 -0
  126. package/dist/events/themes/Blobs.svelte +64 -0
  127. package/dist/events/themes/Blobs.svelte.d.ts +18 -0
  128. package/dist/events/themes/Butterflies.svelte +183 -0
  129. package/dist/events/themes/Butterflies.svelte.d.ts +3 -0
  130. package/dist/events/themes/Fireflies.svelte +134 -0
  131. package/dist/events/themes/Fireflies.svelte.d.ts +3 -0
  132. package/dist/events/themes/Kaleidoscope.svelte +183 -0
  133. package/dist/events/themes/Kaleidoscope.svelte.d.ts +3 -0
  134. package/dist/events/themes/Matrix.svelte +153 -0
  135. package/dist/events/themes/Matrix.svelte.d.ts +3 -0
  136. package/dist/events/themes/Stars.svelte +98 -0
  137. package/dist/events/themes/Stars.svelte.d.ts +3 -0
  138. package/dist/events/thumbnails/designs.d.ts +18 -0
  139. package/dist/events/thumbnails/designs.js +316 -0
  140. package/dist/events/view-data.d.ts +38 -0
  141. package/dist/events/view-data.js +1 -0
  142. package/dist/events-data.d.ts +15 -0
  143. package/dist/events-data.js +9 -0
  144. package/dist/events-editor.d.ts +2 -0
  145. package/dist/events-editor.js +2 -0
  146. package/dist/events.d.ts +19 -0
  147. package/dist/events.js +17 -0
  148. package/dist/index.d.ts +3 -0
  149. package/dist/index.js +2 -0
  150. package/package.json +50 -4
@@ -0,0 +1,313 @@
1
+ <script lang="ts">
2
+ import { Button, Checkbox, Input, Modal, ToggleGroup, ToggleGroupItem } from '../../index.js';
3
+ import { parseDateTime } from '@internationalized/date';
4
+ import * as TID from '@atcute/tid';
5
+ import type { FlatEventRecord } from '../contrail.js';
6
+ import type { EventLocation, EventMode } from './types';
7
+ import { LocationValidationError, normalizeAddressLocation } from './location';
8
+ import { buildThumbnailMedia, renderPresetThumbnail } from './save';
9
+ import { hashSeed } from '../thumbnails/designs.js';
10
+ import type { EditorAdapter, EditorViewer } from './adapter';
11
+
12
+ let {
13
+ open = $bindable(),
14
+ rkey,
15
+ eventData,
16
+ isNew,
17
+ name,
18
+ startsAt,
19
+ endsAt,
20
+ mode,
21
+ timezone,
22
+ description,
23
+ links,
24
+ location,
25
+ thumbnailDateStr,
26
+ thumbnailFile,
27
+ thumbnailChanged,
28
+ selectedPreset,
29
+ accent,
30
+ adapter,
31
+ viewer
32
+ }: {
33
+ open: boolean;
34
+ rkey: string;
35
+ eventData: FlatEventRecord | null;
36
+ isNew: boolean;
37
+ name: string;
38
+ startsAt: string;
39
+ endsAt: string;
40
+ mode: EventMode;
41
+ timezone: string;
42
+ description: string;
43
+ links: Array<{ uri: string; name: string }>;
44
+ location: EventLocation | null;
45
+ thumbnailDateStr: string;
46
+ thumbnailFile: File | null;
47
+ thumbnailChanged: boolean;
48
+ selectedPreset: string | null;
49
+ accent: string;
50
+ adapter: EditorAdapter;
51
+ viewer: EditorViewer;
52
+ } = $props();
53
+
54
+ let interval = $state(1);
55
+ let unit: 'days' | 'weeks' | 'months' | 'years' = $state('weeks');
56
+ let count = $state(4);
57
+ let numberInTitle = $state(false);
58
+ let creating = $state(false);
59
+ let errorMsg: string | null = $state(null);
60
+ let created = $state(0);
61
+ let validRecurrence = $derived(
62
+ Number.isInteger(count) &&
63
+ count >= 1 &&
64
+ count <= 52 &&
65
+ Number.isInteger(interval) &&
66
+ interval >= 1 &&
67
+ interval <= 99
68
+ );
69
+
70
+ let titleNumberMatch = $derived(name.match(/#?(\d+)\s*$/));
71
+ let detectedStartNumber = $derived(titleNumberMatch ? parseInt(titleNumberMatch[1]) : null);
72
+
73
+ $effect(() => {
74
+ if (detectedStartNumber !== null) numberInTitle = true;
75
+ });
76
+
77
+ async function handleCreate() {
78
+ if (
79
+ creating ||
80
+ !validRecurrence ||
81
+ !name.trim() ||
82
+ !startsAt ||
83
+ !viewer.isLoggedIn ||
84
+ !viewer.did
85
+ )
86
+ return;
87
+ const copies = count;
88
+ const period = interval;
89
+
90
+ creating = true;
91
+ errorMsg = null;
92
+ created = 0;
93
+
94
+ try {
95
+ const normalizedLocation = location ? normalizeAddressLocation(location) : null;
96
+ // Recurring instances advance by wall-clock duration (e.g. "every week
97
+ // at 10am"), so operate on CalendarDateTime — not absolute instants —
98
+ // to preserve the wall time across DST transitions.
99
+ const baseStart = parseDateTime(startsAt);
100
+ const baseEnd = endsAt ? parseDateTime(endsAt) : null;
101
+ const durationMs = baseEnd
102
+ ? baseEnd.toDate(timezone).getTime() - baseStart.toDate(timezone).getTime()
103
+ : 0;
104
+ const baseName =
105
+ numberInTitle && titleNumberMatch ? name.replace(/#?\d+\s*$/, '').trimEnd() : name.trim();
106
+ const startNum = detectedStartNumber ?? 1;
107
+ const hasHash = titleNumberMatch ? titleNumberMatch[0].includes('#') : false;
108
+
109
+ // Generate thumbnail from preset if selected and no custom upload.
110
+ let fileForUpload = thumbnailFile;
111
+ let hasNewThumbnail = thumbnailChanged;
112
+ if (selectedPreset && !fileForUpload) {
113
+ const rendered = await renderPresetThumbnail({
114
+ design: selectedPreset,
115
+ seed: hashSeed(rkey),
116
+ name,
117
+ dateStr: thumbnailDateStr,
118
+ accent
119
+ });
120
+ if (rendered) {
121
+ fileForUpload = rendered;
122
+ hasNewThumbnail = true;
123
+ }
124
+ }
125
+
126
+ const existingMedia = (eventData?.media ?? []) as Array<Record<string, unknown>>;
127
+ const media = await buildThumbnailMedia({
128
+ isNew,
129
+ thumbnailChanged: hasNewThumbnail,
130
+ thumbnailFile: fileForUpload,
131
+ existingMedia,
132
+ uploadBlob: (blob) =>
133
+ adapter.uploadBlob(blob) as unknown as Promise<Record<string, unknown>>
134
+ });
135
+
136
+ const parentUri = `at://${viewer.did}/community.lexicon.calendar.event/${rkey}`;
137
+
138
+ for (let i = 0; i < copies; i++) {
139
+ const offset = i + 1;
140
+ const step = offset * period;
141
+ const eventStart =
142
+ unit === 'days'
143
+ ? baseStart.add({ days: step })
144
+ : unit === 'weeks'
145
+ ? baseStart.add({ weeks: step })
146
+ : unit === 'months'
147
+ ? baseStart.add({ months: step })
148
+ : baseStart.add({ years: step });
149
+
150
+ const eventStartIso = eventStart.toDate(timezone).toISOString();
151
+ // Preserve the original absolute duration (handles events that
152
+ // span midnight or odd wall-clock lengths correctly).
153
+ const eventEndIso = durationMs
154
+ ? new Date(eventStart.toDate(timezone).getTime() + durationMs).toISOString()
155
+ : null;
156
+
157
+ let eventName = baseName;
158
+ if (numberInTitle) {
159
+ const num = startNum + (i + 1);
160
+ eventName = hasHash ? `${baseName} #${num}` : `${baseName} ${num}`;
161
+ }
162
+
163
+ const newRkey = TID.now();
164
+ const record: Record<string, unknown> = {
165
+ $type: 'community.lexicon.calendar.event',
166
+ createdWith: 'https://atmo.rsvp',
167
+ name: eventName,
168
+ mode: `community.lexicon.calendar.event#${mode}`,
169
+ status: 'community.lexicon.calendar.event#scheduled',
170
+ startsAt: eventStartIso,
171
+ timezone,
172
+ createdAt: new Date().toISOString(),
173
+ recurringEventOf: parentUri
174
+ };
175
+
176
+ const trimmedDescription = description.trim();
177
+ if (trimmedDescription) record.description = trimmedDescription;
178
+ if (eventEndIso) record.endsAt = eventEndIso;
179
+ if (media) record.media = media;
180
+ if (links.length > 0) record.uris = links;
181
+ if (normalizedLocation) {
182
+ record.locations = [
183
+ {
184
+ $type: 'community.lexicon.location.address',
185
+ ...normalizedLocation
186
+ }
187
+ ];
188
+ }
189
+
190
+ try {
191
+ const result = await adapter.putRecord({
192
+ collection: 'community.lexicon.calendar.event',
193
+ rkey: newRkey,
194
+ record
195
+ });
196
+ await adapter.notifyUpdate?.(result.uri);
197
+ created = i + 1;
198
+ } catch {
199
+ errorMsg = `Failed to create event ${i + 1}. Stopping.`;
200
+ return;
201
+ }
202
+ }
203
+
204
+ open = false;
205
+ } catch (e) {
206
+ console.error('Failed to create recurring events:', e);
207
+ errorMsg =
208
+ e instanceof LocationValidationError
209
+ ? e.message
210
+ : 'Failed to create recurring events. Please try again.';
211
+ } finally {
212
+ creating = false;
213
+ }
214
+ }
215
+ </script>
216
+
217
+ <Modal bind:open title="Recurring events" dismissible={!creating}>
218
+ <p class="mt-1 text-sm text-muted">Create multiple copies of this event at regular intervals.</p>
219
+
220
+ <div class="mt-4 space-y-4">
221
+ <div>
222
+ <!-- svelte-ignore a11y_label_has_associated_control -->
223
+ <label class="mb-1 block text-sm font-medium text-base-700 dark:text-base-300">
224
+ Number of events to create
225
+ </label>
226
+ <Input
227
+ type="number"
228
+ aria-label="Number of events to create"
229
+ bind:value={count}
230
+ disabled={creating}
231
+ min={1}
232
+ max={52}
233
+ class="w-24"
234
+ />
235
+ </div>
236
+
237
+ <div>
238
+ <!-- svelte-ignore a11y_label_has_associated_control -->
239
+ <label class="mb-1 block text-sm font-medium text-base-700 dark:text-base-300">
240
+ Repeat every
241
+ </label>
242
+ <div class="flex items-center gap-2">
243
+ <Input
244
+ type="number"
245
+ aria-label="Repeat interval"
246
+ bind:value={interval}
247
+ disabled={creating}
248
+ min={1}
249
+ max={99}
250
+ class="w-20"
251
+ />
252
+ <ToggleGroup
253
+ type="single"
254
+ disabled={creating}
255
+ bind:value={
256
+ () => unit,
257
+ (value) => {
258
+ if (value) unit = value as typeof unit;
259
+ }
260
+ }
261
+ >
262
+ <ToggleGroupItem value="days">days</ToggleGroupItem>
263
+ <ToggleGroupItem value="weeks">weeks</ToggleGroupItem>
264
+ <ToggleGroupItem value="months">months</ToggleGroupItem>
265
+ <ToggleGroupItem value="years">years</ToggleGroupItem>
266
+ </ToggleGroup>
267
+ </div>
268
+ </div>
269
+
270
+ <div>
271
+ <div class="flex items-center gap-2">
272
+ <Checkbox bind:checked={numberInTitle} disabled={creating} aria-label="Number in title" />
273
+ <span class="text-sm font-medium text-base-700 dark:text-base-300">Number in title</span>
274
+ </div>
275
+ <p class="mt-1 text-xs text-muted">
276
+ {#if numberInTitle && detectedStartNumber !== null}
277
+ Titles will count up from #{detectedStartNumber + 1}
278
+ {:else if numberInTitle}
279
+ A number will be appended to each title
280
+ {:else}
281
+ Append a number to each event title
282
+ {/if}
283
+ </p>
284
+ </div>
285
+ </div>
286
+
287
+ {#if errorMsg}
288
+ <p role="alert" class="mt-4 text-sm text-red-600 dark:text-red-400">{errorMsg}</p>
289
+ {/if}
290
+
291
+ {#if creating && created > 0}
292
+ <p class="mt-4 text-sm text-muted">
293
+ Created {created} of {count} events...
294
+ </p>
295
+ {/if}
296
+
297
+ {#if created > 0 && !creating}
298
+ <p class="mt-4 text-sm text-action">
299
+ Successfully created {created} recurring events!
300
+ </p>
301
+ {/if}
302
+
303
+ <div class="mt-4 flex justify-end gap-2">
304
+ <Button variant="secondary" onclick={() => (open = false)} disabled={creating}>
305
+ {created > 0 && !creating ? 'Close' : 'Cancel'}
306
+ </Button>
307
+ {#if !created || creating}
308
+ <Button onclick={handleCreate} disabled={creating || !validRecurrence}>
309
+ {creating ? `Creating...` : `Create ${count} event${count === 1 ? '' : 's'}`}
310
+ </Button>
311
+ {/if}
312
+ </div>
313
+ </Modal>
@@ -0,0 +1,30 @@
1
+ import type { FlatEventRecord } from '../contrail.js';
2
+ import type { EventLocation, EventMode } from './types';
3
+ import type { EditorAdapter, EditorViewer } from './adapter';
4
+ type $$ComponentProps = {
5
+ open: boolean;
6
+ rkey: string;
7
+ eventData: FlatEventRecord | null;
8
+ isNew: boolean;
9
+ name: string;
10
+ startsAt: string;
11
+ endsAt: string;
12
+ mode: EventMode;
13
+ timezone: string;
14
+ description: string;
15
+ links: Array<{
16
+ uri: string;
17
+ name: string;
18
+ }>;
19
+ location: EventLocation | null;
20
+ thumbnailDateStr: string;
21
+ thumbnailFile: File | null;
22
+ thumbnailChanged: boolean;
23
+ selectedPreset: string | null;
24
+ accent: string;
25
+ adapter: EditorAdapter;
26
+ viewer: EditorViewer;
27
+ };
28
+ declare const RecurringModal: import("svelte").Component<$$ComponentProps, {}, "open">;
29
+ type RecurringModal = ReturnType<typeof RecurringModal>;
30
+ export default RecurringModal;
@@ -0,0 +1,24 @@
1
+ <script lang="ts">
2
+ import { Palette } from '@sketchyicons/svelte';
3
+ import { Button, Icon, Modal } from '../../index.js';
4
+ import ThemePicker from '../ThemePicker.svelte';
5
+ import { themeBackgrounds, type EventTheme } from '../theme.js';
6
+
7
+ let { theme = $bindable() }: { theme: EventTheme } = $props();
8
+
9
+ let showModal = $state(false);
10
+ </script>
11
+
12
+ <div>
13
+ <p class="mb-3 text-xs font-semibold tracking-wider text-muted uppercase">Theme</p>
14
+ <Button variant="secondary" size="sm" onclick={() => (showModal = true)}>
15
+ <Icon img={Palette} size={16} />
16
+ {themeBackgrounds[theme.name] || theme.name}
17
+ </Button>
18
+ </div>
19
+
20
+ <Modal bind:open={showModal} title="Event theme">
21
+ <div class="mt-4">
22
+ <ThemePicker bind:theme />
23
+ </div>
24
+ </Modal>
@@ -0,0 +1,7 @@
1
+ import { type EventTheme } from '../theme.js';
2
+ type $$ComponentProps = {
3
+ theme: EventTheme;
4
+ };
5
+ declare const ThemeSection: import("svelte").Component<$$ComponentProps, {}, "theme">;
6
+ type ThemeSection = ReturnType<typeof ThemeSection>;
7
+ export default ThemeSection;
@@ -0,0 +1,181 @@
1
+ <script lang="ts">
2
+ import { Image, Upload, X } from '@sketchyicons/svelte';
3
+ import { Button, Icon, Modal, InsetImage } from '../../index.js';
4
+ import Avatar from 'svelte-boring-avatars';
5
+ import ThumbnailPresets from '../ThumbnailPresets.svelte';
6
+ import { designs, hashSeed, resolveAccentColor } from '../thumbnails/designs.js';
7
+
8
+ let {
9
+ rkey,
10
+ name,
11
+ dateStr,
12
+ accent,
13
+ thumbnailFile = $bindable(),
14
+ thumbnailPreview = $bindable(),
15
+ thumbnailChanged = $bindable(),
16
+ selectedPreset = $bindable()
17
+ }: {
18
+ rkey: string;
19
+ name: string;
20
+ dateStr: string;
21
+ accent: string;
22
+ thumbnailFile: File | null;
23
+ thumbnailPreview: string | null;
24
+ thumbnailChanged: boolean;
25
+ selectedPreset: string | null;
26
+ } = $props();
27
+
28
+ let fileInput: HTMLInputElement | undefined = $state();
29
+ let presetPreviewCanvas: HTMLCanvasElement | undefined = $state();
30
+ let showModal = $state(false);
31
+ let isDragOver = $state(false);
32
+
33
+ const seed = $derived(hashSeed(rkey));
34
+
35
+ function setThumbnail(file: File) {
36
+ thumbnailFile = file;
37
+ thumbnailChanged = true;
38
+ selectedPreset = null;
39
+ if (thumbnailPreview) URL.revokeObjectURL(thumbnailPreview);
40
+ thumbnailPreview = URL.createObjectURL(file);
41
+ }
42
+
43
+ function onFileChange(e: Event) {
44
+ const input = e.target as HTMLInputElement;
45
+ const file = input.files?.[0];
46
+ if (!file) return;
47
+ setThumbnail(file);
48
+ showModal = false;
49
+ }
50
+
51
+ function onDragOver(e: DragEvent) {
52
+ e.preventDefault();
53
+ isDragOver = true;
54
+ }
55
+
56
+ function onDragLeave(e: DragEvent) {
57
+ e.preventDefault();
58
+ isDragOver = false;
59
+ }
60
+
61
+ function onDrop(e: DragEvent) {
62
+ e.preventDefault();
63
+ isDragOver = false;
64
+ const file = e.dataTransfer?.files?.[0];
65
+ if (file?.type.startsWith('image/')) {
66
+ setThumbnail(file);
67
+ }
68
+ }
69
+
70
+ function removeThumbnail() {
71
+ thumbnailFile = null;
72
+ thumbnailChanged = true;
73
+ selectedPreset = null;
74
+ if (thumbnailPreview) {
75
+ URL.revokeObjectURL(thumbnailPreview);
76
+ thumbnailPreview = null;
77
+ }
78
+ if (fileInput) fileInput.value = '';
79
+ }
80
+
81
+ // Render preset preview canvas whenever the selection, name, date, or accent changes.
82
+ $effect(() => {
83
+ if (selectedPreset && presetPreviewCanvas && designs[selectedPreset]) {
84
+ const ctx = presetPreviewCanvas.getContext('2d');
85
+ if (!ctx) return;
86
+ presetPreviewCanvas.width = 800;
87
+ presetPreviewCanvas.height = 800;
88
+ designs[selectedPreset](
89
+ ctx,
90
+ 800,
91
+ 800,
92
+ name || 'Event',
93
+ dateStr,
94
+ seed,
95
+ resolveAccentColor(accent)
96
+ );
97
+ }
98
+ });
99
+ </script>
100
+
101
+ <!-- svelte-ignore a11y_no_static_element_interactions -->
102
+ <div
103
+ class="order-1 max-w-sm md:order-0 md:col-start-1 md:max-w-none"
104
+ ondragover={onDragOver}
105
+ ondragleave={onDragLeave}
106
+ ondrop={onDrop}
107
+ >
108
+ <input
109
+ bind:this={fileInput}
110
+ type="file"
111
+ accept="image/*"
112
+ onchange={onFileChange}
113
+ class="hidden"
114
+ />
115
+ <div class="group relative">
116
+ {#if thumbnailPreview}
117
+ <InsetImage src={thumbnailPreview} alt="Thumbnail preview" class="w-full rounded-panel" />
118
+ {:else if selectedPreset && designs[selectedPreset]}
119
+ <div
120
+ class="aspect-square w-full overflow-hidden rounded-2xl border border-base-200 dark:border-base-800"
121
+ >
122
+ <canvas bind:this={presetPreviewCanvas} class="h-full w-full"></canvas>
123
+ </div>
124
+ {:else}
125
+ <div
126
+ class="aspect-square w-full overflow-hidden rounded-2xl bg-base-100 dark:bg-base-900 [&>svg]:h-full [&>svg]:w-full"
127
+ >
128
+ <Avatar
129
+ size={400}
130
+ name={rkey}
131
+ variant="marble"
132
+ colors={['#92A1C6', '#146A7C', '#F0AB3D', '#C271B4', '#C20D90']}
133
+ square
134
+ />
135
+ </div>
136
+ {/if}
137
+ <button
138
+ type="button"
139
+ onclick={() => (showModal = true)}
140
+ class="absolute inset-0 flex cursor-pointer flex-col items-center justify-center gap-1.5 rounded-2xl bg-black/0 text-white/0 transition-colors group-hover:bg-black/40 group-hover:text-white/90 {isDragOver
141
+ ? 'bg-black/40 text-white/90'
142
+ : ''}"
143
+ >
144
+ <Icon img={Image} size={24} />
145
+ <span class="text-sm font-medium">Change thumbnail</span>
146
+ </button>
147
+ {#if thumbnailPreview || selectedPreset}
148
+ <Button
149
+ variant="ghost"
150
+ size="icon"
151
+ aria-label="Remove thumbnail"
152
+ onclick={removeThumbnail}
153
+ class="absolute top-2 right-2 bg-base-900/70 text-white opacity-0 transition-opacity group-hover:opacity-100 hover:bg-red-600 focus-visible:opacity-100"
154
+ >
155
+ <Icon img={X} size={14} />
156
+ </Button>
157
+ {/if}
158
+ </div>
159
+ </div>
160
+
161
+ <Modal bind:open={showModal} title="Choose thumbnail">
162
+ <div class="mt-4 flex max-h-[70vh] flex-col gap-6 overflow-y-auto">
163
+ <Button variant="secondary" class="w-full" onclick={() => fileInput?.click()}>
164
+ <Icon img={Upload} size={16} />
165
+ Upload own thumbnail
166
+ </Button>
167
+ <ThumbnailPresets
168
+ {name}
169
+ {dateStr}
170
+ {accent}
171
+ {seed}
172
+ bind:selected={selectedPreset}
173
+ onselect={() => {
174
+ showModal = false;
175
+ thumbnailPreview = null;
176
+ thumbnailFile = null;
177
+ thumbnailChanged = true;
178
+ }}
179
+ />
180
+ </div>
181
+ </Modal>
@@ -0,0 +1,13 @@
1
+ type $$ComponentProps = {
2
+ rkey: string;
3
+ name: string;
4
+ dateStr: string;
5
+ accent: string;
6
+ thumbnailFile: File | null;
7
+ thumbnailPreview: string | null;
8
+ thumbnailChanged: boolean;
9
+ selectedPreset: string | null;
10
+ };
11
+ declare const ThumbnailSection: import("svelte").Component<$$ComponentProps, {}, "thumbnailFile" | "thumbnailPreview" | "thumbnailChanged" | "selectedPreset">;
12
+ type ThumbnailSection = ReturnType<typeof ThumbnailSection>;
13
+ export default ThumbnailSection;
@@ -0,0 +1,73 @@
1
+ /**
2
+ * Adapter contract for `EventEditor`, `EventView` and their child components.
3
+ *
4
+ * The package never reaches into atproto/session/navigation directly. Consumers
5
+ * implement this interface (typically with their own atcute client + router)
6
+ * and pass it in as a prop. The atmo app provides `createInAppAdapter`; other
7
+ * hosts (e.g. blento) provide their own.
8
+ */
9
+ import type { PreferredSocialClient } from '../../social.js';
10
+ import type { LinkedRecordsClient } from '../linked-records.js';
11
+ export type EditorBlobRef = {
12
+ $type: 'blob';
13
+ ref: {
14
+ $link: string;
15
+ };
16
+ mimeType: string;
17
+ size: number;
18
+ };
19
+ export type EditorViewer = {
20
+ isLoggedIn: boolean;
21
+ /** Explicit capability from the host's token grant; absent means denied. */
22
+ canPostToBluesky?: boolean;
23
+ /** The viewer's public social-client selection; absent defaults to Bluesky. */
24
+ preferredSocialClient?: PreferredSocialClient | null;
25
+ did: string | null;
26
+ handle?: string;
27
+ displayName?: string;
28
+ avatar?: string;
29
+ };
30
+ export type EditorAdapter = {
31
+ /** Optional public read transport, supplied by the host (also used in embeds). */
32
+ linkedRecords?: LinkedRecordsClient;
33
+ features: {
34
+ delete: boolean;
35
+ recurring: boolean;
36
+ };
37
+ putRecord(opts: {
38
+ collection: string;
39
+ rkey: string;
40
+ record: Record<string, unknown>;
41
+ }): Promise<{
42
+ uri: string;
43
+ }>;
44
+ createRecord(opts: {
45
+ collection: string;
46
+ rkey?: string;
47
+ record: Record<string, unknown>;
48
+ }): Promise<{
49
+ uri: string;
50
+ cid?: string;
51
+ }>;
52
+ deleteRecord(opts: {
53
+ collection: string;
54
+ rkey: string;
55
+ }): Promise<void>;
56
+ uploadBlob(blob: Blob): Promise<EditorBlobRef>;
57
+ getRecord(opts: {
58
+ did: string;
59
+ collection: string;
60
+ rkey: string;
61
+ }): Promise<{
62
+ value: Record<string, unknown>;
63
+ }>;
64
+ resolveHandle(handle: string): Promise<string>;
65
+ onSaved(result: {
66
+ uri: string;
67
+ rkey: string;
68
+ isNew: boolean;
69
+ }): void;
70
+ onDeleted?(): void;
71
+ requestLogin(): void;
72
+ notifyUpdate?(uri: string): Promise<void>;
73
+ };
@@ -0,0 +1,9 @@
1
+ /**
2
+ * Adapter contract for `EventEditor`, `EventView` and their child components.
3
+ *
4
+ * The package never reaches into atproto/session/navigation directly. Consumers
5
+ * implement this interface (typically with their own atcute client + router)
6
+ * and pass it in as a prop. The atmo app provides `createInAppAdapter`; other
7
+ * hosts (e.g. blento) provide their own.
8
+ */
9
+ export {};
@@ -0,0 +1,16 @@
1
+ import type { EventData } from '../event-types.js';
2
+ export declare function normalizeCountryCode(value: unknown): string | undefined;
3
+ export declare class LocationValidationError extends Error {
4
+ constructor();
5
+ }
6
+ export declare function normalizeAddressLocation<T extends {
7
+ country?: unknown;
8
+ }>(location: T): T & {
9
+ country: string;
10
+ };
11
+ export declare function normalizeEventLocations(locations: NonNullable<EventData['locations']>): (import("../event-types.js").EventLocationVariant | ({
12
+ country: unknown;
13
+ $type: string;
14
+ } & {
15
+ country: string;
16
+ }))[];