@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.
- package/README.md +94 -1
- package/dist/components/ExternalEmbed.svelte +174 -0
- package/dist/components/ExternalEmbed.svelte.d.ts +8 -0
- package/dist/components/GrainAlbumPicker.svelte +218 -0
- package/dist/components/GrainAlbumPicker.svelte.d.ts +14 -0
- package/dist/components/GrainAlbumView.svelte +94 -0
- package/dist/components/GrainAlbumView.svelte.d.ts +10 -0
- package/dist/components/NestedComments.svelte +20 -10
- package/dist/components/NestedComments.svelte.d.ts +2 -0
- package/dist/components/PhotoGrid.svelte +53 -0
- package/dist/components/PhotoGrid.svelte.d.ts +9 -0
- package/dist/components/PhotoLightbox.svelte +332 -0
- package/dist/components/PhotoLightbox.svelte.d.ts +12 -0
- package/dist/components/PostContent.svelte +78 -20
- package/dist/components/PostContent.svelte.d.ts +5 -0
- package/dist/components/StreamplacePicker.svelte +94 -0
- package/dist/components/StreamplacePicker.svelte.d.ts +12 -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/grain/types.d.ts +41 -0
- package/dist/grain/types.js +1 -0
- package/dist/grain/urls.d.ts +7 -0
- package/dist/grain/urls.js +74 -0
- package/dist/grain/xrpc.d.ts +3 -0
- package/dist/grain/xrpc.js +129 -0
- package/dist/grain.d.ts +3 -0
- package/dist/grain.js +2 -0
- package/dist/index.d.ts +16 -0
- package/dist/index.js +9 -0
- package/dist/media.d.ts +10 -0
- package/dist/media.js +1 -0
- package/dist/social/external-embeds.d.ts +39 -0
- package/dist/social/external-embeds.js +107 -0
- package/dist/social/posts.d.ts +59 -17
- package/dist/social/posts.js +99 -12
- package/dist/social.d.ts +2 -0
- package/dist/social.js +1 -0
- package/dist/streamplace.d.ts +6 -0
- package/dist/streamplace.js +1 -0
- package/package.json +58 -4
|
@@ -0,0 +1,94 @@
|
|
|
1
|
+
<script lang="ts">
|
|
2
|
+
import Button from './Button.svelte';
|
|
3
|
+
import Input from './Input.svelte';
|
|
4
|
+
import Modal from './Modal.svelte';
|
|
5
|
+
import { parseStreamplaceLink } from '../streamplace.js';
|
|
6
|
+
import type { StreamplaceSelection } from '../streamplace.js';
|
|
7
|
+
|
|
8
|
+
export type StreamplacePickerProps = {
|
|
9
|
+
open?: boolean;
|
|
10
|
+
actor?: string | null;
|
|
11
|
+
replacing?: boolean;
|
|
12
|
+
resolveHandle: (handle: string) => Promise<string>;
|
|
13
|
+
onselect: StreamplaceSelection;
|
|
14
|
+
class?: string;
|
|
15
|
+
};
|
|
16
|
+
let {
|
|
17
|
+
open = $bindable(false),
|
|
18
|
+
actor,
|
|
19
|
+
replacing = false,
|
|
20
|
+
resolveHandle,
|
|
21
|
+
onselect,
|
|
22
|
+
class: className
|
|
23
|
+
}: StreamplacePickerProps = $props();
|
|
24
|
+
let input = $state('');
|
|
25
|
+
let error = $state('');
|
|
26
|
+
let busy = $state(false);
|
|
27
|
+
let generation = 0;
|
|
28
|
+
const id = $props.id();
|
|
29
|
+
$effect(() => {
|
|
30
|
+
void open;
|
|
31
|
+
void actor;
|
|
32
|
+
generation++;
|
|
33
|
+
input = '';
|
|
34
|
+
error = '';
|
|
35
|
+
busy = false;
|
|
36
|
+
return () => {
|
|
37
|
+
generation++;
|
|
38
|
+
};
|
|
39
|
+
});
|
|
40
|
+
async function addStream(value: string) {
|
|
41
|
+
if (busy) return;
|
|
42
|
+
const stream = parseStreamplaceLink(value);
|
|
43
|
+
if (!stream) {
|
|
44
|
+
error = 'Enter a handle or Streamplace URL.';
|
|
45
|
+
return;
|
|
46
|
+
}
|
|
47
|
+
const session = generation;
|
|
48
|
+
busy = true;
|
|
49
|
+
error = '';
|
|
50
|
+
try {
|
|
51
|
+
if (!stream.actor.startsWith('did:')) await resolveHandle(stream.actor);
|
|
52
|
+
if (session !== generation) return;
|
|
53
|
+
const message = await onselect(stream);
|
|
54
|
+
if (session !== generation) return;
|
|
55
|
+
if (message) error = message;
|
|
56
|
+
else open = false;
|
|
57
|
+
} catch {
|
|
58
|
+
if (session === generation)
|
|
59
|
+
error = 'Couldn’t add that stream. Check the handle and try again.';
|
|
60
|
+
} finally {
|
|
61
|
+
if (session === generation) busy = false;
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
</script>
|
|
65
|
+
|
|
66
|
+
<Modal bind:open title="Add stream" class={className}>
|
|
67
|
+
{#if actor}<Button class="mb-5" disabled={busy} onclick={() => addStream(actor!)}
|
|
68
|
+
>{replacing ? 'Use my Streamplace link instead' : 'Add my Streamplace link'}</Button
|
|
69
|
+
>{/if}
|
|
70
|
+
<label for={`${id}-handle`} class="mb-2 block text-sm"
|
|
71
|
+
>Or enter another person’s handle or Streamplace URL</label
|
|
72
|
+
>
|
|
73
|
+
<div class="flex flex-col gap-2 sm:flex-row">
|
|
74
|
+
<Input
|
|
75
|
+
id={`${id}-handle`}
|
|
76
|
+
placeholder="handle.example"
|
|
77
|
+
bind:value={input}
|
|
78
|
+
class="min-w-0 flex-1"
|
|
79
|
+
onkeydown={(event) => {
|
|
80
|
+
if (event.key === 'Enter') {
|
|
81
|
+
event.preventDefault();
|
|
82
|
+
void addStream(input);
|
|
83
|
+
}
|
|
84
|
+
}}
|
|
85
|
+
/>
|
|
86
|
+
<Button disabled={busy || !input.trim()} onclick={() => addStream(input)}
|
|
87
|
+
>{replacing ? 'Replace stream' : 'Add stream'}</Button
|
|
88
|
+
>
|
|
89
|
+
</div>
|
|
90
|
+
{#if replacing}<p class="mt-3 text-sm text-muted">
|
|
91
|
+
This replaces the current Streamplace link.
|
|
92
|
+
</p>{/if}
|
|
93
|
+
{#if error}<p class="mt-3 text-sm text-red-600 dark:text-red-400" role="alert">{error}</p>{/if}
|
|
94
|
+
</Modal>
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
import type { StreamplaceSelection } from '../streamplace.js';
|
|
2
|
+
export type StreamplacePickerProps = {
|
|
3
|
+
open?: boolean;
|
|
4
|
+
actor?: string | null;
|
|
5
|
+
replacing?: boolean;
|
|
6
|
+
resolveHandle: (handle: string) => Promise<string>;
|
|
7
|
+
onselect: StreamplaceSelection;
|
|
8
|
+
class?: string;
|
|
9
|
+
};
|
|
10
|
+
declare const StreamplacePicker: import("svelte").Component<StreamplacePickerProps, {}, "open">;
|
|
11
|
+
type StreamplacePicker = ReturnType<typeof StreamplacePicker>;
|
|
12
|
+
export default StreamplacePicker;
|
|
@@ -0,0 +1,222 @@
|
|
|
1
|
+
<script lang="ts">
|
|
2
|
+
// @ts-nocheck
|
|
3
|
+
import { DatePicker } from 'bits-ui';
|
|
4
|
+
import { CalendarDate, type DateValue } from '@internationalized/date';
|
|
5
|
+
import { untrack } from 'svelte';
|
|
6
|
+
import { Icon } from '../index.js';
|
|
7
|
+
import { ChevronLeft, ChevronRight } from '@sketchyicons/svelte';
|
|
8
|
+
|
|
9
|
+
let {
|
|
10
|
+
value = $bindable(''),
|
|
11
|
+
required = false,
|
|
12
|
+
label = 'Date',
|
|
13
|
+
minValue = '',
|
|
14
|
+
locale = 'en',
|
|
15
|
+
onSelect
|
|
16
|
+
}: {
|
|
17
|
+
value: string;
|
|
18
|
+
required?: boolean;
|
|
19
|
+
label?: string;
|
|
20
|
+
minValue?: string;
|
|
21
|
+
locale?: string;
|
|
22
|
+
onSelect?: () => void;
|
|
23
|
+
} = $props();
|
|
24
|
+
|
|
25
|
+
let isOpen = $state(false);
|
|
26
|
+
|
|
27
|
+
const now = new Date();
|
|
28
|
+
const currentYear = now.getFullYear();
|
|
29
|
+
const yearRange = Array.from({ length: 7 }, (_, i) => currentYear - 1 + i);
|
|
30
|
+
const today = new Date();
|
|
31
|
+
const todayDay = today.getDate();
|
|
32
|
+
const todayMonth = today.getMonth() + 1;
|
|
33
|
+
const todayYear = today.getFullYear();
|
|
34
|
+
|
|
35
|
+
let internalValue: CalendarDate | undefined = $state(undefined);
|
|
36
|
+
|
|
37
|
+
function parseDateStr(str: string): CalendarDate | undefined {
|
|
38
|
+
if (!str) return undefined;
|
|
39
|
+
const [yearStr, monthStr, dayStr] = str.split('-');
|
|
40
|
+
const year = parseInt(yearStr, 10);
|
|
41
|
+
const month = parseInt(monthStr, 10);
|
|
42
|
+
const day = parseInt(dayStr, 10);
|
|
43
|
+
if (isNaN(year) || isNaN(month) || isNaN(day)) return undefined;
|
|
44
|
+
return new CalendarDate(year, month, day);
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
function formatDateStr(dt: CalendarDate): string {
|
|
48
|
+
const y = String(dt.year).padStart(4, '0');
|
|
49
|
+
const m = String(dt.month).padStart(2, '0');
|
|
50
|
+
const d = String(dt.day).padStart(2, '0');
|
|
51
|
+
return `${y}-${m}-${d}`;
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
let internalMinValue: CalendarDate | undefined = $derived.by(() => {
|
|
55
|
+
return parseDateStr(minValue);
|
|
56
|
+
});
|
|
57
|
+
|
|
58
|
+
$effect(() => {
|
|
59
|
+
const parsed = parseDateStr(value);
|
|
60
|
+
untrack(() => {
|
|
61
|
+
if (parsed) {
|
|
62
|
+
if (
|
|
63
|
+
!internalValue ||
|
|
64
|
+
parsed.year !== internalValue.year ||
|
|
65
|
+
parsed.month !== internalValue.month ||
|
|
66
|
+
parsed.day !== internalValue.day
|
|
67
|
+
) {
|
|
68
|
+
internalValue = parsed;
|
|
69
|
+
previousValue = parsed;
|
|
70
|
+
}
|
|
71
|
+
} else {
|
|
72
|
+
internalValue = undefined;
|
|
73
|
+
}
|
|
74
|
+
});
|
|
75
|
+
});
|
|
76
|
+
|
|
77
|
+
let previousValue: CalendarDate | undefined = $state(undefined);
|
|
78
|
+
|
|
79
|
+
function handleValueChange(newVal: DateValue | undefined) {
|
|
80
|
+
if (newVal && newVal instanceof CalendarDate) {
|
|
81
|
+
previousValue = newVal;
|
|
82
|
+
internalValue = newVal;
|
|
83
|
+
value = formatDateStr(newVal);
|
|
84
|
+
} else if (!newVal && previousValue) {
|
|
85
|
+
// Prevent deselection — restore previous value
|
|
86
|
+
internalValue = previousValue;
|
|
87
|
+
}
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
function handleOpenChangeComplete(open: boolean) {
|
|
91
|
+
if (!open && internalValue) {
|
|
92
|
+
onSelect?.();
|
|
93
|
+
}
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
function formatDisplayDate(compact: boolean) {
|
|
97
|
+
if (!internalValue) return '';
|
|
98
|
+
const date = new Date(internalValue.year, internalValue.month - 1, internalValue.day);
|
|
99
|
+
const opts: Intl.DateTimeFormatOptions = {
|
|
100
|
+
...(compact ? {} : { weekday: 'short' }),
|
|
101
|
+
month: 'short',
|
|
102
|
+
day: 'numeric'
|
|
103
|
+
};
|
|
104
|
+
if (internalValue.year !== currentYear) {
|
|
105
|
+
opts.year = 'numeric';
|
|
106
|
+
}
|
|
107
|
+
return date.toLocaleDateString(locale, opts);
|
|
108
|
+
}
|
|
109
|
+
let displayText = $derived(formatDisplayDate(false));
|
|
110
|
+
let compactText = $derived(formatDisplayDate(true));
|
|
111
|
+
</script>
|
|
112
|
+
|
|
113
|
+
<DatePicker.Root
|
|
114
|
+
bind:value={internalValue}
|
|
115
|
+
bind:open={isOpen}
|
|
116
|
+
onValueChange={handleValueChange}
|
|
117
|
+
onOpenChangeComplete={handleOpenChangeComplete}
|
|
118
|
+
minValue={internalMinValue}
|
|
119
|
+
granularity="day"
|
|
120
|
+
fixedWeeks={true}
|
|
121
|
+
weekdayFormat="short"
|
|
122
|
+
{locale}
|
|
123
|
+
{required}
|
|
124
|
+
>
|
|
125
|
+
<DatePicker.Trigger
|
|
126
|
+
aria-label={`${label}: ${displayText || 'Select date'}`}
|
|
127
|
+
class="flex h-11 min-w-0 flex-1 cursor-pointer items-center rounded-ui border border-line bg-recess px-2 text-xs text-ink shadow-(--ui-recess-shadow) transition-colors hover:border-control-hover focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-focus min-[360px]:px-3 min-[360px]:text-sm"
|
|
128
|
+
>
|
|
129
|
+
<span class="truncate select-none">
|
|
130
|
+
{#if displayText}
|
|
131
|
+
<span class="min-[360px]:hidden">{compactText}</span>
|
|
132
|
+
<span class="hidden min-[360px]:inline">{displayText}</span>
|
|
133
|
+
{:else}
|
|
134
|
+
<span class="text-base-400 dark:text-base-500">Select date</span>
|
|
135
|
+
{/if}
|
|
136
|
+
</span>
|
|
137
|
+
</DatePicker.Trigger>
|
|
138
|
+
|
|
139
|
+
<DatePicker.Content
|
|
140
|
+
sideOffset={8}
|
|
141
|
+
class="z-50 max-w-[calc(100vw-2rem)] rounded-panel border border-line bg-page p-3 text-ink"
|
|
142
|
+
>
|
|
143
|
+
<DatePicker.Calendar>
|
|
144
|
+
{#snippet children({ months, weekdays })}
|
|
145
|
+
<DatePicker.Header class="flex items-center justify-between">
|
|
146
|
+
<DatePicker.PrevButton
|
|
147
|
+
class="inline-flex size-8 items-center justify-center rounded-lg text-base-500 hover:bg-base-200 dark:text-base-400 dark:hover:bg-base-700"
|
|
148
|
+
>
|
|
149
|
+
<Icon img={ChevronLeft} size={20} />
|
|
150
|
+
</DatePicker.PrevButton>
|
|
151
|
+
|
|
152
|
+
<div class="flex items-center gap-1.5">
|
|
153
|
+
<DatePicker.MonthSelect
|
|
154
|
+
monthFormat="long"
|
|
155
|
+
class="cursor-pointer border-0 bg-transparent text-sm font-medium text-base-900 outline-none hover:text-accent-500 focus:ring-0 focus:outline-none dark:text-base-100 dark:hover:text-accent-400"
|
|
156
|
+
/>
|
|
157
|
+
<DatePicker.YearSelect
|
|
158
|
+
years={yearRange}
|
|
159
|
+
class="cursor-pointer border-0 bg-transparent text-sm font-medium text-base-900 outline-none hover:text-accent-500 focus:ring-0 focus:outline-none dark:text-base-100 dark:hover:text-accent-400"
|
|
160
|
+
/>
|
|
161
|
+
</div>
|
|
162
|
+
|
|
163
|
+
<DatePicker.NextButton
|
|
164
|
+
class="inline-flex size-8 items-center justify-center rounded-lg text-base-500 hover:bg-base-200 dark:text-base-400 dark:hover:bg-base-700"
|
|
165
|
+
>
|
|
166
|
+
<Icon img={ChevronRight} size={20} />
|
|
167
|
+
</DatePicker.NextButton>
|
|
168
|
+
</DatePicker.Header>
|
|
169
|
+
|
|
170
|
+
{#each months as month (month.value.month)}
|
|
171
|
+
<DatePicker.Grid class="mt-3 w-full">
|
|
172
|
+
<DatePicker.GridHead>
|
|
173
|
+
<DatePicker.GridRow class="flex w-full">
|
|
174
|
+
{#each weekdays as weekday, i (i)}
|
|
175
|
+
<DatePicker.HeadCell
|
|
176
|
+
class="flex-1 text-center text-xs font-medium text-base-400 dark:text-base-500"
|
|
177
|
+
>
|
|
178
|
+
{weekday}
|
|
179
|
+
</DatePicker.HeadCell>
|
|
180
|
+
{/each}
|
|
181
|
+
</DatePicker.GridRow>
|
|
182
|
+
</DatePicker.GridHead>
|
|
183
|
+
|
|
184
|
+
<DatePicker.GridBody>
|
|
185
|
+
{#each month.weeks as week, weekIndex (weekIndex)}
|
|
186
|
+
<DatePicker.GridRow class="flex w-full">
|
|
187
|
+
{#each week as day (day.toString())}
|
|
188
|
+
<DatePicker.Cell date={day} month={month.value} class="flex-1 p-0.5">
|
|
189
|
+
<DatePicker.Day>
|
|
190
|
+
{#snippet children({ selected, disabled, day: dayText })}
|
|
191
|
+
<div
|
|
192
|
+
class="relative flex size-9 items-center justify-center rounded-lg text-sm
|
|
193
|
+
{selected
|
|
194
|
+
? 'bg-accent-400 font-medium text-base-950'
|
|
195
|
+
: disabled
|
|
196
|
+
? 'pointer-events-none text-base-300 dark:text-base-600'
|
|
197
|
+
: day.month !== month.value.month
|
|
198
|
+
? 'text-base-300 dark:text-base-600'
|
|
199
|
+
: 'text-base-700 hover:bg-base-200 dark:text-base-300 dark:hover:bg-base-700'}"
|
|
200
|
+
>
|
|
201
|
+
{dayText}
|
|
202
|
+
{#if day.day === todayDay && day.month === todayMonth && day.year === todayYear}
|
|
203
|
+
<span
|
|
204
|
+
class="absolute bottom-1 left-1/2 size-1 -translate-x-1/2 rounded-full {selected
|
|
205
|
+
? 'bg-base-950'
|
|
206
|
+
: 'bg-accent-500'}"
|
|
207
|
+
></span>
|
|
208
|
+
{/if}
|
|
209
|
+
</div>
|
|
210
|
+
{/snippet}
|
|
211
|
+
</DatePicker.Day>
|
|
212
|
+
</DatePicker.Cell>
|
|
213
|
+
{/each}
|
|
214
|
+
</DatePicker.GridRow>
|
|
215
|
+
{/each}
|
|
216
|
+
</DatePicker.GridBody>
|
|
217
|
+
</DatePicker.Grid>
|
|
218
|
+
{/each}
|
|
219
|
+
{/snippet}
|
|
220
|
+
</DatePicker.Calendar>
|
|
221
|
+
</DatePicker.Content>
|
|
222
|
+
</DatePicker.Root>
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
import { DatePicker } from 'bits-ui';
|
|
2
|
+
type $$ComponentProps = {
|
|
3
|
+
value: string;
|
|
4
|
+
required?: boolean;
|
|
5
|
+
label?: string;
|
|
6
|
+
minValue?: string;
|
|
7
|
+
locale?: string;
|
|
8
|
+
onSelect?: () => void;
|
|
9
|
+
};
|
|
10
|
+
declare const DatePicker: import("svelte").Component<$$ComponentProps, {}, "value">;
|
|
11
|
+
type DatePicker = ReturnType<typeof DatePicker>;
|
|
12
|
+
export default DatePicker;
|
|
@@ -0,0 +1,112 @@
|
|
|
1
|
+
<script lang="ts">
|
|
2
|
+
// @ts-nocheck
|
|
3
|
+
import DatePickerField from './DatePicker.svelte';
|
|
4
|
+
import TimePicker from './TimePicker.svelte';
|
|
5
|
+
import { untrack } from 'svelte';
|
|
6
|
+
import { BROWSER as browser } from 'esm-env';
|
|
7
|
+
|
|
8
|
+
let {
|
|
9
|
+
value = $bindable(''),
|
|
10
|
+
required = false,
|
|
11
|
+
label = '',
|
|
12
|
+
minValue = '',
|
|
13
|
+
referenceTime = ''
|
|
14
|
+
}: {
|
|
15
|
+
value: string;
|
|
16
|
+
required?: boolean;
|
|
17
|
+
label?: string;
|
|
18
|
+
minValue?: string;
|
|
19
|
+
referenceTime?: string;
|
|
20
|
+
} = $props();
|
|
21
|
+
|
|
22
|
+
let datePart = $state('');
|
|
23
|
+
let timePart = $state('00:00');
|
|
24
|
+
let timeEl: HTMLDivElement | undefined = $state(undefined);
|
|
25
|
+
|
|
26
|
+
const locale = browser ? navigator.language || 'en' : 'en';
|
|
27
|
+
let minDatePart = $derived(minValue ? minValue.split('T')[0] || '' : '');
|
|
28
|
+
let refTimePart = $derived.by(() => {
|
|
29
|
+
if (!referenceTime) return '';
|
|
30
|
+
const [refDate, refTime] = referenceTime.split('T');
|
|
31
|
+
if (refDate && refDate === datePart && refTime) return refTime;
|
|
32
|
+
return '';
|
|
33
|
+
});
|
|
34
|
+
|
|
35
|
+
// Default to current date/time rounded up to the next hour when no initial value
|
|
36
|
+
if (browser && !value) {
|
|
37
|
+
const now = new Date();
|
|
38
|
+
// Local calculation only, not component state.
|
|
39
|
+
// eslint-disable-next-line svelte/prefer-svelte-reactivity
|
|
40
|
+
const rounded = new Date(now);
|
|
41
|
+
rounded.setMinutes(0, 0, 0);
|
|
42
|
+
rounded.setHours(rounded.getHours() + 1);
|
|
43
|
+
|
|
44
|
+
const yyyy = rounded.getFullYear();
|
|
45
|
+
const mm = String(rounded.getMonth() + 1).padStart(2, '0');
|
|
46
|
+
const dd = String(rounded.getDate()).padStart(2, '0');
|
|
47
|
+
const hh = String(rounded.getHours()).padStart(2, '0');
|
|
48
|
+
const min = String(rounded.getMinutes()).padStart(2, '0');
|
|
49
|
+
|
|
50
|
+
const defaultDate = `${yyyy}-${mm}-${dd}`;
|
|
51
|
+
const defaultTime = `${hh}:${min}`;
|
|
52
|
+
datePart = defaultDate;
|
|
53
|
+
timePart = defaultTime;
|
|
54
|
+
value = `${defaultDate}T${defaultTime}`;
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
// Sync external value -> date/time parts
|
|
58
|
+
$effect(() => {
|
|
59
|
+
const v = value;
|
|
60
|
+
untrack(() => {
|
|
61
|
+
if (v) {
|
|
62
|
+
const [d, t] = v.split('T');
|
|
63
|
+
if (d && d !== datePart) datePart = d;
|
|
64
|
+
if (t && t !== timePart) timePart = t;
|
|
65
|
+
}
|
|
66
|
+
});
|
|
67
|
+
});
|
|
68
|
+
|
|
69
|
+
// Sync date/time parts -> external value
|
|
70
|
+
$effect(() => {
|
|
71
|
+
const d = datePart;
|
|
72
|
+
const t = timePart;
|
|
73
|
+
untrack(() => {
|
|
74
|
+
if (d) {
|
|
75
|
+
const newVal = `${d}T${t || '00:00'}`;
|
|
76
|
+
if (newVal !== value) value = newVal;
|
|
77
|
+
}
|
|
78
|
+
});
|
|
79
|
+
});
|
|
80
|
+
|
|
81
|
+
function focusTime() {
|
|
82
|
+
// Small delay to let the popover finish closing
|
|
83
|
+
setTimeout(() => {
|
|
84
|
+
if (timeEl) {
|
|
85
|
+
const segment = timeEl.querySelector('[data-segment]');
|
|
86
|
+
if (segment instanceof HTMLElement) {
|
|
87
|
+
segment.focus();
|
|
88
|
+
}
|
|
89
|
+
}
|
|
90
|
+
}, 50);
|
|
91
|
+
}
|
|
92
|
+
</script>
|
|
93
|
+
|
|
94
|
+
<div class="flex min-w-0 flex-1 items-center gap-2">
|
|
95
|
+
<DatePickerField
|
|
96
|
+
bind:value={datePart}
|
|
97
|
+
label={label ? `${label} date` : 'Date'}
|
|
98
|
+
{required}
|
|
99
|
+
minValue={minDatePart}
|
|
100
|
+
{locale}
|
|
101
|
+
onSelect={focusTime}
|
|
102
|
+
/>
|
|
103
|
+
<div bind:this={timeEl} class="shrink-0">
|
|
104
|
+
<TimePicker
|
|
105
|
+
bind:value={timePart}
|
|
106
|
+
label={label ? `${label} time` : 'Time'}
|
|
107
|
+
{required}
|
|
108
|
+
{locale}
|
|
109
|
+
referenceTime={refTimePart}
|
|
110
|
+
/>
|
|
111
|
+
</div>
|
|
112
|
+
</div>
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
type $$ComponentProps = {
|
|
2
|
+
value: string;
|
|
3
|
+
required?: boolean;
|
|
4
|
+
label?: string;
|
|
5
|
+
minValue?: string;
|
|
6
|
+
referenceTime?: string;
|
|
7
|
+
};
|
|
8
|
+
declare const DateTimePicker: import("svelte").Component<$$ComponentProps, {}, "value">;
|
|
9
|
+
type DateTimePicker = ReturnType<typeof DateTimePicker>;
|
|
10
|
+
export default DateTimePicker;
|
|
@@ -0,0 +1,199 @@
|
|
|
1
|
+
<script lang="ts" module>
|
|
2
|
+
export interface AttendeeCounts {
|
|
3
|
+
goingCount: number;
|
|
4
|
+
interestedCount: number;
|
|
5
|
+
}
|
|
6
|
+
</script>
|
|
7
|
+
|
|
8
|
+
<script lang="ts">
|
|
9
|
+
import type { Snippet } from 'svelte';
|
|
10
|
+
import type { AttendeeInfo } from './contrail.js';
|
|
11
|
+
import { Avatar } from '../index.js';
|
|
12
|
+
import { scale } from 'svelte/transition';
|
|
13
|
+
import { flip } from 'svelte/animate';
|
|
14
|
+
import { Modal } from '../index.js';
|
|
15
|
+
|
|
16
|
+
let {
|
|
17
|
+
going = [],
|
|
18
|
+
interested = [],
|
|
19
|
+
goingCount: initialGoingCount = going.length,
|
|
20
|
+
interestedCount: initialInterestedCount = interested.length,
|
|
21
|
+
footer
|
|
22
|
+
}: {
|
|
23
|
+
going?: AttendeeInfo[];
|
|
24
|
+
interested?: AttendeeInfo[];
|
|
25
|
+
goingCount?: number;
|
|
26
|
+
interestedCount?: number;
|
|
27
|
+
footer?: Snippet<[AttendeeCounts]>;
|
|
28
|
+
} = $props();
|
|
29
|
+
|
|
30
|
+
let goingCountOverride: number | null = $state(null);
|
|
31
|
+
let interestedCountOverride: number | null = $state(null);
|
|
32
|
+
let goingAttendeesOverride: AttendeeInfo[] | null = $state(null);
|
|
33
|
+
let interestedAttendeesOverride: AttendeeInfo[] | null = $state(null);
|
|
34
|
+
|
|
35
|
+
let modalOpen = $state(false);
|
|
36
|
+
let modalGroup: 'going' | 'interested' = $state('going');
|
|
37
|
+
|
|
38
|
+
const MAX_AVATARS = 18;
|
|
39
|
+
|
|
40
|
+
let goingCount = $derived(goingCountOverride ?? initialGoingCount);
|
|
41
|
+
let interestedCount = $derived(interestedCountOverride ?? initialInterestedCount);
|
|
42
|
+
let goingAttendees = $derived(goingAttendeesOverride ?? going);
|
|
43
|
+
let interestedAttendees = $derived(interestedAttendeesOverride ?? interested);
|
|
44
|
+
|
|
45
|
+
let totalCount = $derived(goingCount + interestedCount);
|
|
46
|
+
|
|
47
|
+
let goingDisplay = $derived(goingAttendees.slice(0, MAX_AVATARS));
|
|
48
|
+
let goingOverflow = $derived(goingCount - goingDisplay.length);
|
|
49
|
+
|
|
50
|
+
let interestedDisplay = $derived(interestedAttendees.slice(0, MAX_AVATARS));
|
|
51
|
+
let interestedOverflow = $derived(interestedCount - interestedDisplay.length);
|
|
52
|
+
|
|
53
|
+
let modalAttendees = $derived(modalGroup === 'going' ? goingAttendees : interestedAttendees);
|
|
54
|
+
let modalTitle = $derived(modalGroup === 'going' ? 'Going' : 'Interested');
|
|
55
|
+
|
|
56
|
+
function openModal(group: 'going' | 'interested') {
|
|
57
|
+
modalGroup = group;
|
|
58
|
+
modalOpen = true;
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
export function addAttendee(attendee: AttendeeInfo) {
|
|
62
|
+
const wasGoing = goingAttendees.some((a) => a.did === attendee.did);
|
|
63
|
+
const wasInterested = interestedAttendees.some((a) => a.did === attendee.did);
|
|
64
|
+
const nextGoing = goingAttendees.filter((a) => a.did !== attendee.did);
|
|
65
|
+
const nextInterested = interestedAttendees.filter((a) => a.did !== attendee.did);
|
|
66
|
+
|
|
67
|
+
// Remove from both lists first (in case of status change)
|
|
68
|
+
if (attendee.status === 'going') {
|
|
69
|
+
goingAttendeesOverride = [attendee, ...nextGoing];
|
|
70
|
+
interestedAttendeesOverride = nextInterested;
|
|
71
|
+
goingCountOverride = goingCount + (wasGoing ? 0 : 1);
|
|
72
|
+
interestedCountOverride = interestedCount - (wasInterested ? 1 : 0);
|
|
73
|
+
} else if (attendee.status === 'interested') {
|
|
74
|
+
goingAttendeesOverride = nextGoing;
|
|
75
|
+
interestedAttendeesOverride = [attendee, ...nextInterested];
|
|
76
|
+
goingCountOverride = goingCount - (wasGoing ? 1 : 0);
|
|
77
|
+
interestedCountOverride = interestedCount + (wasInterested ? 0 : 1);
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
function thumbnail(url: string | undefined) {
|
|
82
|
+
return url?.replace('/avatar/', '/avatar_thumbnail/');
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
export function removeAttendee(did: string) {
|
|
86
|
+
const wasGoing = goingAttendees.some((a) => a.did === did);
|
|
87
|
+
const wasInterested = interestedAttendees.some((a) => a.did === did);
|
|
88
|
+
goingAttendeesOverride = goingAttendees.filter((a) => a.did !== did);
|
|
89
|
+
interestedAttendeesOverride = interestedAttendees.filter((a) => a.did !== did);
|
|
90
|
+
if (wasGoing) goingCountOverride = goingCount - 1;
|
|
91
|
+
if (wasInterested) interestedCountOverride = interestedCount - 1;
|
|
92
|
+
}
|
|
93
|
+
</script>
|
|
94
|
+
|
|
95
|
+
{#if totalCount > 0}
|
|
96
|
+
<div class="mb-2">
|
|
97
|
+
{#if goingCount > 0}
|
|
98
|
+
<button
|
|
99
|
+
type="button"
|
|
100
|
+
class="-mx-2 block w-full cursor-pointer rounded-xl px-2 py-2 text-left transition-colors hover:bg-base-100 dark:hover:bg-base-800/50"
|
|
101
|
+
onclick={() => openModal('going')}
|
|
102
|
+
>
|
|
103
|
+
<p class="mb-2 text-sm text-base-900 dark:text-base-50">
|
|
104
|
+
<span class="font-bold">{goingCount}</span>
|
|
105
|
+
<span class="text-xs font-semibold tracking-wider text-muted uppercase">Going</span>
|
|
106
|
+
</p>
|
|
107
|
+
<div class="flex items-center">
|
|
108
|
+
<div class="flex flex-wrap -space-y-2 -space-x-4 pr-4">
|
|
109
|
+
{#each goingDisplay as person (person.did)}
|
|
110
|
+
<div
|
|
111
|
+
animate:flip={{ duration: 300 }}
|
|
112
|
+
in:scale={{ duration: 300, start: 0.5 }}
|
|
113
|
+
out:scale={{ duration: 200, start: 0.5 }}
|
|
114
|
+
>
|
|
115
|
+
<Avatar
|
|
116
|
+
src={thumbnail(person.avatar)}
|
|
117
|
+
name={person.name}
|
|
118
|
+
class="size-12 border-2"
|
|
119
|
+
/>
|
|
120
|
+
</div>
|
|
121
|
+
{/each}
|
|
122
|
+
{#if goingOverflow > 0}
|
|
123
|
+
<span
|
|
124
|
+
class="z-10 inline-flex size-12 items-center justify-center rounded-full border-2 border-base-100 bg-base-200 text-sm font-semibold text-base-950 dark:border-base-800 dark:bg-base-800 dark:text-base-100"
|
|
125
|
+
>
|
|
126
|
+
+{goingOverflow}
|
|
127
|
+
</span>
|
|
128
|
+
{/if}
|
|
129
|
+
</div>
|
|
130
|
+
</div>
|
|
131
|
+
</button>
|
|
132
|
+
{/if}
|
|
133
|
+
|
|
134
|
+
{#if interestedCount > 0}
|
|
135
|
+
<button
|
|
136
|
+
type="button"
|
|
137
|
+
class="-mx-2 mt-4 block w-full cursor-pointer rounded-xl px-2 py-2 text-left transition-colors hover:bg-base-100 dark:hover:bg-base-800/50"
|
|
138
|
+
onclick={() => openModal('interested')}
|
|
139
|
+
>
|
|
140
|
+
<p class="mb-2 text-sm text-base-900 dark:text-base-50">
|
|
141
|
+
<span class="font-bold">{interestedCount}</span>
|
|
142
|
+
<span class="text-xs font-semibold tracking-wider text-muted uppercase">Interested</span>
|
|
143
|
+
</p>
|
|
144
|
+
<div class="flex items-center">
|
|
145
|
+
<div class="flex flex-wrap -space-y-2 -space-x-4 pr-4">
|
|
146
|
+
{#each interestedDisplay as person (person.did)}
|
|
147
|
+
<div
|
|
148
|
+
animate:flip={{ duration: 300 }}
|
|
149
|
+
in:scale={{ duration: 300, start: 0.5 }}
|
|
150
|
+
out:scale={{ duration: 200, start: 0.5 }}
|
|
151
|
+
>
|
|
152
|
+
<Avatar
|
|
153
|
+
src={thumbnail(person.avatar)}
|
|
154
|
+
name={person.name}
|
|
155
|
+
class="size-12 border-2"
|
|
156
|
+
/>
|
|
157
|
+
</div>
|
|
158
|
+
{/each}
|
|
159
|
+
{#if interestedOverflow > 0}
|
|
160
|
+
<span
|
|
161
|
+
class="z-10 inline-flex size-12 items-center justify-center rounded-full border-2 border-base-100 bg-base-200 text-sm font-semibold text-base-950 dark:border-base-800 dark:bg-base-800 dark:text-base-100"
|
|
162
|
+
>
|
|
163
|
+
+{interestedOverflow}
|
|
164
|
+
</span>
|
|
165
|
+
{/if}
|
|
166
|
+
</div>
|
|
167
|
+
</div>
|
|
168
|
+
</button>
|
|
169
|
+
{/if}
|
|
170
|
+
{@render footer?.({ goingCount, interestedCount })}
|
|
171
|
+
</div>
|
|
172
|
+
{/if}
|
|
173
|
+
|
|
174
|
+
<Modal bind:open={modalOpen} title={`${modalTitle} (${modalAttendees.length})`}>
|
|
175
|
+
<div
|
|
176
|
+
class="mx-4 mb-4 max-h-80 space-y-1 overflow-y-auto rounded-xl bg-base-200/30 p-2 dark:bg-base-900/50"
|
|
177
|
+
>
|
|
178
|
+
{#each modalAttendees as person (person.did)}
|
|
179
|
+
<a
|
|
180
|
+
href={person.url}
|
|
181
|
+
target={person.url?.startsWith('/') ? undefined : '_blank'}
|
|
182
|
+
rel={person.url?.startsWith('/') ? undefined : 'noopener noreferrer'}
|
|
183
|
+
class="flex items-center gap-3 rounded-xl px-2 py-2 transition-colors hover:bg-base-200 dark:hover:bg-base-900"
|
|
184
|
+
>
|
|
185
|
+
<Avatar src={thumbnail(person.avatar)} name={person.name} class="size-10 shrink-0" />
|
|
186
|
+
<div class="min-w-0">
|
|
187
|
+
<p class="truncate text-sm font-medium text-base-900 dark:text-base-50">
|
|
188
|
+
{person.name}
|
|
189
|
+
</p>
|
|
190
|
+
{#if person.handle}
|
|
191
|
+
<p class="truncate text-xs text-muted">
|
|
192
|
+
@{person.handle}
|
|
193
|
+
</p>
|
|
194
|
+
{/if}
|
|
195
|
+
</div>
|
|
196
|
+
</a>
|
|
197
|
+
{/each}
|
|
198
|
+
</div>
|
|
199
|
+
</Modal>
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
export interface AttendeeCounts {
|
|
2
|
+
goingCount: number;
|
|
3
|
+
interestedCount: number;
|
|
4
|
+
}
|
|
5
|
+
import type { Snippet } from 'svelte';
|
|
6
|
+
import type { AttendeeInfo } from './contrail.js';
|
|
7
|
+
type $$ComponentProps = {
|
|
8
|
+
going?: AttendeeInfo[];
|
|
9
|
+
interested?: AttendeeInfo[];
|
|
10
|
+
goingCount?: number;
|
|
11
|
+
interestedCount?: number;
|
|
12
|
+
footer?: Snippet<[AttendeeCounts]>;
|
|
13
|
+
};
|
|
14
|
+
declare const EventAttendees: import("svelte").Component<$$ComponentProps, {
|
|
15
|
+
addAttendee: (attendee: AttendeeInfo) => void;
|
|
16
|
+
removeAttendee: (did: string) => void;
|
|
17
|
+
}, "">;
|
|
18
|
+
type EventAttendees = ReturnType<typeof EventAttendees>;
|
|
19
|
+
export default EventAttendees;
|