@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.
- package/README.md +119 -0
- package/dist/at-tags.d.ts +18 -0
- package/dist/at-tags.js +18 -0
- package/dist/components/AtTags.svelte +17 -0
- package/dist/components/AtTags.svelte.d.ts +7 -0
- package/dist/events/DatePicker.svelte +222 -0
- package/dist/events/DatePicker.svelte.d.ts +12 -0
- package/dist/events/DateTimePicker.svelte +112 -0
- package/dist/events/DateTimePicker.svelte.d.ts +10 -0
- package/dist/events/EventAttendees.svelte +199 -0
- package/dist/events/EventAttendees.svelte.d.ts +19 -0
- package/dist/events/EventCard.svelte +148 -0
- package/dist/events/EventCard.svelte.d.ts +8 -0
- package/dist/events/EventComments.svelte +98 -0
- package/dist/events/EventComments.svelte.d.ts +8 -0
- package/dist/events/EventEditor.svelte +540 -0
- package/dist/events/EventEditor.svelte.d.ts +18 -0
- package/dist/events/EventRsvp.svelte +178 -0
- package/dist/events/EventRsvp.svelte.d.ts +16 -0
- package/dist/events/EventView.svelte +488 -0
- package/dist/events/EventView.svelte.d.ts +23 -0
- package/dist/events/ImageDropper.svelte +66 -0
- package/dist/events/ImageDropper.svelte.d.ts +7 -0
- package/dist/events/Map.svelte +36 -0
- package/dist/events/Map.svelte.d.ts +8 -0
- package/dist/events/PostToBlueskyModal.svelte +246 -0
- package/dist/events/PostToBlueskyModal.svelte.d.ts +22 -0
- package/dist/events/ShareModal.svelte +174 -0
- package/dist/events/ShareModal.svelte.d.ts +23 -0
- package/dist/events/StarterPackButton.svelte +72 -0
- package/dist/events/StarterPackButton.svelte.d.ts +10 -0
- package/dist/events/ThemeApply.svelte +30 -0
- package/dist/events/ThemeApply.svelte.d.ts +7 -0
- package/dist/events/ThemeBackground.svelte +33 -0
- package/dist/events/ThemeBackground.svelte.d.ts +7 -0
- package/dist/events/ThemePicker.svelte +104 -0
- package/dist/events/ThemePicker.svelte.d.ts +7 -0
- package/dist/events/ThumbnailPresets.svelte +71 -0
- package/dist/events/ThumbnailPresets.svelte.d.ts +11 -0
- package/dist/events/TimePicker.svelte +117 -0
- package/dist/events/TimePicker.svelte.d.ts +10 -0
- package/dist/events/TimezonePicker.svelte +67 -0
- package/dist/events/TimezonePicker.svelte.d.ts +6 -0
- package/dist/events/VodPlayer.svelte +154 -0
- package/dist/events/VodPlayer.svelte.d.ts +14 -0
- package/dist/events/VodTranscript.svelte +75 -0
- package/dist/events/VodTranscript.svelte.d.ts +8 -0
- package/dist/events/atproto-helpers.d.ts +21 -0
- package/dist/events/atproto-helpers.js +61 -0
- package/dist/events/cal/helper.d.ts +1 -0
- package/dist/events/cal/helper.js +20 -0
- package/dist/events/cal/ical.d.ts +22 -0
- package/dist/events/cal/ical.js +188 -0
- package/dist/events/cal/sanitize.d.ts +3 -0
- package/dist/events/cal/sanitize.js +25 -0
- package/dist/events/conference.d.ts +65 -0
- package/dist/events/conference.js +61 -0
- package/dist/events/contrail.d.ts +49 -0
- package/dist/events/contrail.js +12 -0
- package/dist/events/date-format.d.ts +22 -0
- package/dist/events/date-format.js +43 -0
- package/dist/events/editor/LinkedRecordsPicker.svelte +104 -0
- package/dist/events/editor/LinkedRecordsPicker.svelte.d.ts +12 -0
- package/dist/events/editor/LinksSection.svelte +154 -0
- package/dist/events/editor/LinksSection.svelte.d.ts +13 -0
- package/dist/events/editor/LocationSection.svelte +163 -0
- package/dist/events/editor/LocationSection.svelte.d.ts +8 -0
- package/dist/events/editor/RecurringModal.svelte +313 -0
- package/dist/events/editor/RecurringModal.svelte.d.ts +30 -0
- package/dist/events/editor/ThemeSection.svelte +24 -0
- package/dist/events/editor/ThemeSection.svelte.d.ts +7 -0
- package/dist/events/editor/ThumbnailSection.svelte +181 -0
- package/dist/events/editor/ThumbnailSection.svelte.d.ts +13 -0
- package/dist/events/editor/adapter.d.ts +73 -0
- package/dist/events/editor/adapter.js +9 -0
- package/dist/events/editor/location.d.ts +16 -0
- package/dist/events/editor/location.js +50 -0
- package/dist/events/editor/save.d.ts +43 -0
- package/dist/events/editor/save.js +168 -0
- package/dist/events/editor/types.d.ts +39 -0
- package/dist/events/editor/types.js +9 -0
- package/dist/events/event-media.d.ts +4 -0
- package/dist/events/event-media.js +4 -0
- package/dist/events/event-types.d.ts +79 -0
- package/dist/events/event-types.js +11 -0
- package/dist/events/event-view/AddToCalendarButton.svelte +31 -0
- package/dist/events/event-view/AddToCalendarButton.svelte.d.ts +9 -0
- package/dist/events/event-view/EventBadges.svelte +20 -0
- package/dist/events/event-view/EventBadges.svelte.d.ts +7 -0
- package/dist/events/event-view/EventDateBlock.svelte +43 -0
- package/dist/events/event-view/EventDateBlock.svelte.d.ts +7 -0
- package/dist/events/event-view/EventHostedBy.svelte +61 -0
- package/dist/events/event-view/EventHostedBy.svelte.d.ts +16 -0
- package/dist/events/event-view/EventLinksList.svelte +24 -0
- package/dist/events/event-view/EventLinksList.svelte.d.ts +9 -0
- package/dist/events/event-view/EventLocationBlock.svelte +62 -0
- package/dist/events/event-view/EventLocationBlock.svelte.d.ts +7 -0
- package/dist/events/event-view/EventLocationMap.svelte +66 -0
- package/dist/events/event-view/EventLocationMap.svelte.d.ts +7 -0
- package/dist/events/event-view/ExternalRsvpNotice.svelte +34 -0
- package/dist/events/event-view/ExternalRsvpNotice.svelte.d.ts +6 -0
- package/dist/events/event-view/StreamPlacePlayer.svelte +209 -0
- package/dist/events/event-view/StreamPlacePlayer.svelte.d.ts +8 -0
- package/dist/events/event-view/format.d.ts +10 -0
- package/dist/events/event-view/format.js +112 -0
- package/dist/events/event-view/locations.d.ts +36 -0
- package/dist/events/event-view/locations.js +243 -0
- package/dist/events/event-view/plyr-icons.d.ts +3 -0
- package/dist/events/event-view/plyr-icons.js +46 -0
- package/dist/events/linked-records.d.ts +18 -0
- package/dist/events/linked-records.js +11 -0
- package/dist/events/profile-url.d.ts +3 -0
- package/dist/events/profile-url.js +11 -0
- package/dist/events/schedule/ConferenceTimetable.svelte +340 -0
- package/dist/events/schedule/ConferenceTimetable.svelte.d.ts +22 -0
- package/dist/events/schedule/DaySchedule.svelte +298 -0
- package/dist/events/schedule/DaySchedule.svelte.d.ts +26 -0
- package/dist/events/schedule/ScheduleEventCell.svelte +166 -0
- package/dist/events/schedule/ScheduleEventCell.svelte.d.ts +25 -0
- package/dist/events/schedule/schedule-utils.d.ts +44 -0
- package/dist/events/schedule/schedule-utils.js +166 -0
- package/dist/events/starter-pack-url.d.ts +3 -0
- package/dist/events/starter-pack-url.js +16 -0
- package/dist/events/theme.d.ts +9 -0
- package/dist/events/theme.js +36 -0
- package/dist/events/themes/Blobs.svelte +64 -0
- package/dist/events/themes/Blobs.svelte.d.ts +18 -0
- package/dist/events/themes/Butterflies.svelte +183 -0
- package/dist/events/themes/Butterflies.svelte.d.ts +3 -0
- package/dist/events/themes/Fireflies.svelte +134 -0
- package/dist/events/themes/Fireflies.svelte.d.ts +3 -0
- package/dist/events/themes/Kaleidoscope.svelte +183 -0
- package/dist/events/themes/Kaleidoscope.svelte.d.ts +3 -0
- package/dist/events/themes/Matrix.svelte +153 -0
- package/dist/events/themes/Matrix.svelte.d.ts +3 -0
- package/dist/events/themes/Stars.svelte +98 -0
- package/dist/events/themes/Stars.svelte.d.ts +3 -0
- package/dist/events/thumbnails/designs.d.ts +18 -0
- package/dist/events/thumbnails/designs.js +316 -0
- package/dist/events/view-data.d.ts +38 -0
- package/dist/events/view-data.js +1 -0
- package/dist/events-data.d.ts +15 -0
- package/dist/events-data.js +9 -0
- package/dist/events-editor.d.ts +2 -0
- package/dist/events-editor.js +2 -0
- package/dist/events.d.ts +19 -0
- package/dist/events.js +17 -0
- package/dist/index.d.ts +3 -0
- package/dist/index.js +2 -0
- package/package.json +50 -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
|
+
/ ©
|
|
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;
|