@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,43 @@
1
+ import { parseAbsolute, parseDateTime } from '@internationalized/date';
2
+ /**
3
+ * Convert a datetime-local string (e.g. "2026-04-09T22:00") to a UTC ISO string,
4
+ * interpreting the wall-clock components in the given IANA timezone.
5
+ */
6
+ export function datetimeLocalToISO(dt, tz) {
7
+ return parseDateTime(dt).toDate(tz).toISOString();
8
+ }
9
+ /**
10
+ * Convert a UTC ISO string to a datetime-local string ("YYYY-MM-DDTHH:mm") whose
11
+ * components represent the wall-clock time in the given IANA timezone.
12
+ */
13
+ export function isoToDatetimeLocalInTz(iso, tz) {
14
+ const zdt = parseAbsolute(iso, tz);
15
+ const pad = (n) => n.toString().padStart(2, '0');
16
+ return `${zdt.year}-${pad(zdt.month)}-${pad(zdt.day)}T${pad(zdt.hour)}:${pad(zdt.minute)}`;
17
+ }
18
+ /**
19
+ * Format an ISO timestamp using Intl options, rendered in the event's timezone
20
+ * when available. Falls back to the viewer's local zone for legacy events that
21
+ * predate the timezone field.
22
+ */
23
+ export function formatInTz(iso, tz, options, locale = 'en-US') {
24
+ const date = new Date(iso);
25
+ return new Intl.DateTimeFormat(locale, { ...options, timeZone: tz || undefined }).format(date);
26
+ }
27
+ /**
28
+ * Returns the parts of an ISO timestamp in the given timezone (or viewer-local
29
+ * when tz is falsy). Useful when the caller wants numeric components like the
30
+ * day-of-month rendered in the event's zone.
31
+ */
32
+ export function partsInTz(iso, tz, options, locale = 'en-US') {
33
+ const date = new Date(iso);
34
+ const parts = new Intl.DateTimeFormat(locale, {
35
+ ...options,
36
+ timeZone: tz || undefined
37
+ }).formatToParts(date);
38
+ const out = {};
39
+ for (const p of parts)
40
+ if (p.type !== 'literal')
41
+ out[p.type] = p.value;
42
+ return out;
43
+ }
@@ -0,0 +1,104 @@
1
+ <script lang="ts">
2
+ import { Button, Icon, Popover, GrainAlbumPicker, StreamplacePicker } from '../../index.js';
3
+ import { Camera, Plus, Video } from '@sketchyicons/svelte';
4
+ import type { GrainAlbum } from '../../grain.js';
5
+ import type { EditorAdapter, EditorViewer } from './adapter.js';
6
+ import { eventStreamLink, parseGrainLink } from '../linked-records.js';
7
+
8
+ let {
9
+ links = $bindable(),
10
+ adapter,
11
+ viewer
12
+ }: {
13
+ links: Array<{ uri: string; name: string }>;
14
+ adapter: EditorAdapter;
15
+ viewer: EditorViewer;
16
+ } = $props();
17
+ let menuOpen = $state(false);
18
+ let grainOpen = $state(false);
19
+ let streamOpen = $state(false);
20
+ let session = 0;
21
+ let stream = $derived(links.find((link) => eventStreamLink(link.uri)));
22
+ function added(album: GrainAlbum) {
23
+ return links.some((link) => {
24
+ const ref = parseGrainLink(link.uri);
25
+ return (
26
+ ref?.atUri === album.uri ||
27
+ (ref?.actor === viewer.handle &&
28
+ album.creator.did === viewer.did &&
29
+ ref?.rkey === parseGrainLink(album.uri)?.rkey)
30
+ );
31
+ });
32
+ }
33
+ async function attachAlbum(album: GrainAlbum) {
34
+ const current = session;
35
+ for (const link of links) {
36
+ const ref = parseGrainLink(link.uri);
37
+ if (!ref) continue;
38
+ if (ref.atUri === album.uri) return 'This album is already added.';
39
+ if (ref.rkey === parseGrainLink(album.uri)?.rkey && !ref.actor.startsWith('did:')) {
40
+ try {
41
+ if (
42
+ `at://${await adapter.resolveHandle(ref.actor)}/social.grain.gallery/${ref.rkey}` ===
43
+ album.uri
44
+ )
45
+ return 'This album is already added.';
46
+ } catch {
47
+ /* An unrelated legacy link must not prevent attaching another album. */
48
+ }
49
+ }
50
+ }
51
+ if (current !== session || !grainOpen) return;
52
+ links = [...links, { uri: album.url, name: album.title }];
53
+ }
54
+ </script>
55
+
56
+ <Popover
57
+ bind:open={menuOpen}
58
+ side="bottom"
59
+ sideOffset={8}
60
+ class="w-56 p-1"
61
+ aria-label="Add linked record"
62
+ >
63
+ {#snippet trigger({ props })}<Button {...props} size="sm"
64
+ ><Icon img={Plus} size={16} />Add linked record</Button
65
+ >{/snippet}
66
+ <Button
67
+ variant="ghost"
68
+ class="w-full justify-start"
69
+ onclick={() => {
70
+ session++;
71
+ menuOpen = false;
72
+ grainOpen = true;
73
+ }}><Icon img={Camera} size={16} />Grain.social album</Button
74
+ >
75
+ <Button
76
+ variant="ghost"
77
+ class="w-full justify-start"
78
+ onclick={() => {
79
+ menuOpen = false;
80
+ streamOpen = true;
81
+ }}><Icon img={Video} size={16} />Streamplace stream</Button
82
+ >
83
+ </Popover>
84
+
85
+ <GrainAlbumPicker
86
+ bind:open={grainOpen}
87
+ actor={viewer.did}
88
+ listAlbums={adapter.linkedRecords?.listGrainAlbums}
89
+ loadAlbum={adapter.linkedRecords?.getGrainGallery}
90
+ isSelected={added}
91
+ onselect={attachAlbum}
92
+ />
93
+ <StreamplacePicker
94
+ bind:open={streamOpen}
95
+ actor={viewer.handle && viewer.handle !== 'handle.invalid' ? viewer.handle : viewer.did}
96
+ replacing={!!stream}
97
+ resolveHandle={adapter.resolveHandle}
98
+ onselect={(selected) => {
99
+ links = [
100
+ ...links.filter((link) => !eventStreamLink(link.uri)),
101
+ { uri: selected.url, name: `Streamplace · ${selected.actor}` }
102
+ ];
103
+ }}
104
+ />
@@ -0,0 +1,12 @@
1
+ import type { EditorAdapter, EditorViewer } from './adapter.js';
2
+ type $$ComponentProps = {
3
+ links: Array<{
4
+ uri: string;
5
+ name: string;
6
+ }>;
7
+ adapter: EditorAdapter;
8
+ viewer: EditorViewer;
9
+ };
10
+ declare const LinkedRecordsPicker: import("svelte").Component<$$ComponentProps, {}, "links">;
11
+ type LinkedRecordsPicker = ReturnType<typeof LinkedRecordsPicker>;
12
+ export default LinkedRecordsPicker;
@@ -0,0 +1,154 @@
1
+ <script lang="ts">
2
+ import { Camera, Link as LinkIcon, Plus, Video, X } from '@sketchyicons/svelte';
3
+ import { Button, Icon, Input, Popover } from '../../index.js';
4
+ import { validateLink } from '../cal/helper.js';
5
+ import { eventStreamLink, isLinkedRecord, parseGrainLink } from '../linked-records.js';
6
+ import type { EditorAdapter, EditorViewer } from './adapter.js';
7
+ import LinkedRecordsPicker from './LinkedRecordsPicker.svelte';
8
+
9
+ type Link = { uri: string; name: string };
10
+ let {
11
+ links = $bindable(),
12
+ adapter,
13
+ viewer
14
+ }: { links: Link[]; adapter: EditorAdapter; viewer: EditorViewer } = $props();
15
+ let showPopup = $state(false);
16
+ let newUri = $state('');
17
+ let newName = $state('');
18
+ let error = $state('');
19
+ let ordinary = $derived(links.filter((l) => !isLinkedRecord(l)));
20
+ let linked = $derived(links.filter(isLinkedRecord));
21
+
22
+ function addLink() {
23
+ const raw = newUri.trim();
24
+ if (!raw) return;
25
+ const uri = validateLink(raw);
26
+ if (!uri) {
27
+ error = 'Please enter a valid URL';
28
+ return;
29
+ }
30
+ if (links.some((link) => link.uri === uri)) {
31
+ error = 'This link is already added.';
32
+ return;
33
+ }
34
+ const grain = parseGrainLink(uri),
35
+ stream = eventStreamLink(uri);
36
+ // Keep one stream even when it was added through the generic URL input.
37
+ if (stream && links.some((l) => eventStreamLink(l.uri))) {
38
+ error = 'Use the Streamplace picker to replace the current stream.';
39
+ return;
40
+ }
41
+ links = [
42
+ ...links,
43
+ {
44
+ uri,
45
+ name:
46
+ newName.trim() ||
47
+ (grain ? 'Grain photo album' : stream ? `Streamplace · ${stream.actor}` : '')
48
+ }
49
+ ];
50
+ cancel();
51
+ }
52
+ function removeLink(link: Link) {
53
+ links = links.filter((l) => l !== link);
54
+ }
55
+ function cancel() {
56
+ showPopup = false;
57
+ error = '';
58
+ newUri = '';
59
+ newName = '';
60
+ }
61
+ </script>
62
+
63
+ {#snippet row(link: Link, provider?: 'grain' | 'stream')}
64
+ <div class="group flex items-center gap-2">
65
+ <Icon
66
+ img={provider === 'grain' ? Camera : provider === 'stream' ? Video : LinkIcon}
67
+ size={16}
68
+ class="shrink-0 text-muted"
69
+ />
70
+ <div class="min-w-0 flex-1">
71
+ <p class="truncate text-sm">
72
+ {link.name ||
73
+ (provider === 'grain'
74
+ ? 'Grain photo album'
75
+ : provider === 'stream'
76
+ ? eventStreamLink(link.uri)?.actor
77
+ : link.uri.replace(/^https?:\/\//, ''))}
78
+ </p>
79
+ </div>
80
+ <Button
81
+ variant="ghost"
82
+ size="icon"
83
+ onclick={() => removeLink(link)}
84
+ aria-label={`Remove ${link.name || link.uri}`}
85
+ class="shrink-0"><Icon img={X} size={14} /></Button
86
+ >
87
+ </div>
88
+ {/snippet}
89
+
90
+ <div>
91
+ {#if ordinary.length}
92
+ <p class="mb-3 text-xs font-semibold tracking-wider text-muted uppercase">Links</p>
93
+ <div class="mb-4 space-y-3">
94
+ {#each ordinary as link, i (i)}{@render row(link)}{/each}
95
+ </div>
96
+ {/if}
97
+ {#if linked.length}
98
+ <p class="mb-3 text-xs font-semibold tracking-wider text-muted uppercase">Linked records</p>
99
+ <div class="mb-4 space-y-3">
100
+ {#each linked as link, i (i)}{@render row(
101
+ link,
102
+ parseGrainLink(link.uri) ? 'grain' : 'stream'
103
+ )}{/each}
104
+ </div>
105
+ {/if}
106
+ <div class="flex flex-wrap gap-2">
107
+ <Popover
108
+ bind:open={showPopup}
109
+ side="bottom"
110
+ sideOffset={8}
111
+ class="w-64 p-3"
112
+ aria-label="Add event link"
113
+ >
114
+ {#snippet trigger({ props })}<Button {...props} size="sm"
115
+ ><Icon img={Plus} size={16} />Add link</Button
116
+ >{/snippet}
117
+ <Input
118
+ type="url"
119
+ bind:value={newUri}
120
+ placeholder="https://..."
121
+ aria-label="Link URL"
122
+ class="mb-2"
123
+ onkeydown={(e) => {
124
+ if (e.key === 'Enter') {
125
+ e.preventDefault();
126
+ addLink();
127
+ }
128
+ }}
129
+ />
130
+ <Input
131
+ type="text"
132
+ bind:value={newName}
133
+ placeholder="Label (optional)"
134
+ aria-label="Link label"
135
+ class="mb-2"
136
+ onkeydown={(e) => {
137
+ if (e.key === 'Enter') {
138
+ e.preventDefault();
139
+ addLink();
140
+ }
141
+ }}
142
+ />
143
+ {#if error}<p class="mb-2 text-xs text-red-500" role="alert">{error}</p>{/if}
144
+ <div class="flex justify-end gap-2">
145
+ <Button variant="ghost" size="sm" onclick={cancel}>Cancel</Button><Button
146
+ onclick={addLink}
147
+ size="sm"
148
+ disabled={!newUri.trim()}>Add</Button
149
+ >
150
+ </div>
151
+ </Popover>
152
+ <LinkedRecordsPicker bind:links {adapter} {viewer} />
153
+ </div>
154
+ </div>
@@ -0,0 +1,13 @@
1
+ import type { EditorAdapter, EditorViewer } from './adapter.js';
2
+ type Link = {
3
+ uri: string;
4
+ name: string;
5
+ };
6
+ type $$ComponentProps = {
7
+ links: Link[];
8
+ adapter: EditorAdapter;
9
+ viewer: EditorViewer;
10
+ };
11
+ declare const LinksSection: import("svelte").Component<$$ComponentProps, {}, "links">;
12
+ type LinksSection = ReturnType<typeof LinksSection>;
13
+ export default LinksSection;
@@ -0,0 +1,163 @@
1
+ <script lang="ts">
2
+ import { MapPin, X } from '@sketchyicons/svelte';
3
+ import { Button, Icon, Input, Modal } from '../../index.js';
4
+ import { getLocationDisplayString, type EventLocation } from './types';
5
+
6
+ let {
7
+ location = $bindable(),
8
+ locationChanged = $bindable()
9
+ }: {
10
+ location: EventLocation | null;
11
+ locationChanged: boolean;
12
+ } = $props();
13
+
14
+ let showModal = $state(false);
15
+ let searchText = $state('');
16
+ let searching = $state(false);
17
+ let error = $state('');
18
+ let result: { displayName: string; location: EventLocation } | null = $state(null);
19
+
20
+ async function search() {
21
+ const q = searchText.trim();
22
+ if (!q) return;
23
+ error = '';
24
+ searching = true;
25
+ result = null;
26
+
27
+ try {
28
+ const response = await fetch('/api/geocoding?q=' + encodeURIComponent(q));
29
+ if (!response.ok) throw new Error('response not ok');
30
+ const data: Record<string, unknown> = await response.json();
31
+ if (!data || data.error) throw new Error('no results');
32
+
33
+ const addr = (data.address || {}) as Record<string, string>;
34
+ const road = addr.road || '';
35
+ const houseNumber = addr.house_number || '';
36
+ const street = road ? (houseNumber ? `${road} ${houseNumber}` : road) : '';
37
+ const locality =
38
+ addr.city || addr.town || addr.village || addr.municipality || addr.hamlet || '';
39
+ const region = addr.state || addr.county || '';
40
+ const country = addr.country_code || addr.country || '';
41
+
42
+ result = {
43
+ displayName: (data.label as string) || q,
44
+ location: {
45
+ ...(street && { street }),
46
+ ...(locality && { locality }),
47
+ ...(region && { region }),
48
+ ...(country && { country })
49
+ }
50
+ };
51
+ } catch {
52
+ error = "Couldn't find that location.";
53
+ } finally {
54
+ searching = false;
55
+ }
56
+ }
57
+
58
+ function confirm() {
59
+ if (result) {
60
+ location = result.location;
61
+ locationChanged = true;
62
+ }
63
+ showModal = false;
64
+ searchText = '';
65
+ result = null;
66
+ error = '';
67
+ }
68
+
69
+ function remove() {
70
+ location = null;
71
+ locationChanged = true;
72
+ }
73
+ </script>
74
+
75
+ {#if location}
76
+ <div class="mb-6 flex items-center gap-4">
77
+ <div
78
+ class="flex size-12 shrink-0 items-center justify-center rounded-xl border border-base-200 bg-base-100 dark:border-base-700 dark:bg-base-950/30"
79
+ >
80
+ <Icon img={MapPin} size={20} class="text-base-900 dark:text-base-200" />
81
+ </div>
82
+ <p class="flex-1 font-semibold text-base-900 dark:text-base-50">
83
+ {getLocationDisplayString(location)}
84
+ </p>
85
+ <Button
86
+ variant="ghost"
87
+ size="icon"
88
+ aria-label="Remove location"
89
+ onclick={remove}
90
+ class="shrink-0"
91
+ >
92
+ <Icon img={X} size={14} />
93
+ </Button>
94
+ </div>
95
+ {:else}
96
+ <div class="mb-6">
97
+ <Button variant="secondary" onclick={() => (showModal = true)}>
98
+ <Icon img={MapPin} size={16} />
99
+ Add location
100
+ </Button>
101
+ </div>
102
+ {/if}
103
+
104
+ <Modal bind:open={showModal} title="Event location">
105
+ <form
106
+ onsubmit={(e) => {
107
+ e.preventDefault();
108
+ search();
109
+ }}
110
+ class="mt-2"
111
+ >
112
+ <div class="flex gap-2">
113
+ <Input
114
+ type="text"
115
+ aria-label="Search location"
116
+ class="min-w-0 flex-1"
117
+ bind:value={searchText}
118
+ />
119
+ <Button type="submit" disabled={searching || !searchText.trim()}>
120
+ {searching ? 'Searching...' : 'Search'}
121
+ </Button>
122
+ </div>
123
+ </form>
124
+
125
+ {#if error}
126
+ <p class="mt-3 text-sm text-red-600 dark:text-red-400">{error}</p>
127
+ {/if}
128
+
129
+ {#if result}
130
+ <div
131
+ class="mt-4 overflow-hidden rounded-xl border border-base-200 bg-base-50 p-4 dark:border-base-700 dark:bg-base-900"
132
+ >
133
+ <div class="flex items-start gap-3">
134
+ <Icon img={MapPin} size={20} class="mt-0.5 shrink-0 text-base-500" />
135
+ <div class="min-w-0 flex-1">
136
+ <p class="font-medium text-base-900 dark:text-base-50">
137
+ {getLocationDisplayString(result.location)}
138
+ </p>
139
+ <p class="mt-0.5 truncate text-xs text-muted">
140
+ {result.displayName}
141
+ </p>
142
+ </div>
143
+ </div>
144
+ <div class="mt-4 flex justify-end">
145
+ <Button onclick={confirm}>Use this location</Button>
146
+ </div>
147
+ </div>
148
+ {/if}
149
+
150
+ <p class="mt-4 text-xs text-base-400 dark:text-base-500">
151
+ Geocoding by <a
152
+ href="https://nominatim.openstreetmap.org/"
153
+ class="underline hover:text-base-600 dark:hover:text-base-400"
154
+ target="_blank">Nominatim</a
155
+ >
156
+ / &copy;
157
+ <a
158
+ href="https://www.openstreetmap.org/copyright"
159
+ class="underline hover:text-base-600 dark:hover:text-base-400"
160
+ target="_blank">OpenStreetMap contributors</a
161
+ >
162
+ </p>
163
+ </Modal>
@@ -0,0 +1,8 @@
1
+ import { type EventLocation } from './types';
2
+ type $$ComponentProps = {
3
+ location: EventLocation | null;
4
+ locationChanged: boolean;
5
+ };
6
+ declare const LocationSection: import("svelte").Component<$$ComponentProps, {}, "location" | "locationChanged">;
7
+ type LocationSection = ReturnType<typeof LocationSection>;
8
+ export default LocationSection;