@atmo-dev/kit 0.4.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 (144) hide show
  1. package/README.md +35 -0
  2. package/dist/events/DatePicker.svelte +222 -0
  3. package/dist/events/DatePicker.svelte.d.ts +12 -0
  4. package/dist/events/DateTimePicker.svelte +112 -0
  5. package/dist/events/DateTimePicker.svelte.d.ts +10 -0
  6. package/dist/events/EventAttendees.svelte +199 -0
  7. package/dist/events/EventAttendees.svelte.d.ts +19 -0
  8. package/dist/events/EventCard.svelte +148 -0
  9. package/dist/events/EventCard.svelte.d.ts +8 -0
  10. package/dist/events/EventComments.svelte +98 -0
  11. package/dist/events/EventComments.svelte.d.ts +8 -0
  12. package/dist/events/EventEditor.svelte +540 -0
  13. package/dist/events/EventEditor.svelte.d.ts +18 -0
  14. package/dist/events/EventRsvp.svelte +178 -0
  15. package/dist/events/EventRsvp.svelte.d.ts +16 -0
  16. package/dist/events/EventView.svelte +488 -0
  17. package/dist/events/EventView.svelte.d.ts +23 -0
  18. package/dist/events/ImageDropper.svelte +66 -0
  19. package/dist/events/ImageDropper.svelte.d.ts +7 -0
  20. package/dist/events/Map.svelte +36 -0
  21. package/dist/events/Map.svelte.d.ts +8 -0
  22. package/dist/events/PostToBlueskyModal.svelte +246 -0
  23. package/dist/events/PostToBlueskyModal.svelte.d.ts +22 -0
  24. package/dist/events/ShareModal.svelte +174 -0
  25. package/dist/events/ShareModal.svelte.d.ts +23 -0
  26. package/dist/events/StarterPackButton.svelte +72 -0
  27. package/dist/events/StarterPackButton.svelte.d.ts +10 -0
  28. package/dist/events/ThemeApply.svelte +30 -0
  29. package/dist/events/ThemeApply.svelte.d.ts +7 -0
  30. package/dist/events/ThemeBackground.svelte +33 -0
  31. package/dist/events/ThemeBackground.svelte.d.ts +7 -0
  32. package/dist/events/ThemePicker.svelte +104 -0
  33. package/dist/events/ThemePicker.svelte.d.ts +7 -0
  34. package/dist/events/ThumbnailPresets.svelte +71 -0
  35. package/dist/events/ThumbnailPresets.svelte.d.ts +11 -0
  36. package/dist/events/TimePicker.svelte +117 -0
  37. package/dist/events/TimePicker.svelte.d.ts +10 -0
  38. package/dist/events/TimezonePicker.svelte +67 -0
  39. package/dist/events/TimezonePicker.svelte.d.ts +6 -0
  40. package/dist/events/VodPlayer.svelte +154 -0
  41. package/dist/events/VodPlayer.svelte.d.ts +14 -0
  42. package/dist/events/VodTranscript.svelte +75 -0
  43. package/dist/events/VodTranscript.svelte.d.ts +8 -0
  44. package/dist/events/atproto-helpers.d.ts +21 -0
  45. package/dist/events/atproto-helpers.js +61 -0
  46. package/dist/events/cal/helper.d.ts +1 -0
  47. package/dist/events/cal/helper.js +20 -0
  48. package/dist/events/cal/ical.d.ts +22 -0
  49. package/dist/events/cal/ical.js +188 -0
  50. package/dist/events/cal/sanitize.d.ts +3 -0
  51. package/dist/events/cal/sanitize.js +25 -0
  52. package/dist/events/conference.d.ts +65 -0
  53. package/dist/events/conference.js +61 -0
  54. package/dist/events/contrail.d.ts +49 -0
  55. package/dist/events/contrail.js +12 -0
  56. package/dist/events/date-format.d.ts +22 -0
  57. package/dist/events/date-format.js +43 -0
  58. package/dist/events/editor/LinkedRecordsPicker.svelte +104 -0
  59. package/dist/events/editor/LinkedRecordsPicker.svelte.d.ts +12 -0
  60. package/dist/events/editor/LinksSection.svelte +154 -0
  61. package/dist/events/editor/LinksSection.svelte.d.ts +13 -0
  62. package/dist/events/editor/LocationSection.svelte +163 -0
  63. package/dist/events/editor/LocationSection.svelte.d.ts +8 -0
  64. package/dist/events/editor/RecurringModal.svelte +313 -0
  65. package/dist/events/editor/RecurringModal.svelte.d.ts +30 -0
  66. package/dist/events/editor/ThemeSection.svelte +24 -0
  67. package/dist/events/editor/ThemeSection.svelte.d.ts +7 -0
  68. package/dist/events/editor/ThumbnailSection.svelte +181 -0
  69. package/dist/events/editor/ThumbnailSection.svelte.d.ts +13 -0
  70. package/dist/events/editor/adapter.d.ts +73 -0
  71. package/dist/events/editor/adapter.js +9 -0
  72. package/dist/events/editor/location.d.ts +16 -0
  73. package/dist/events/editor/location.js +50 -0
  74. package/dist/events/editor/save.d.ts +43 -0
  75. package/dist/events/editor/save.js +168 -0
  76. package/dist/events/editor/types.d.ts +39 -0
  77. package/dist/events/editor/types.js +9 -0
  78. package/dist/events/event-media.d.ts +4 -0
  79. package/dist/events/event-media.js +4 -0
  80. package/dist/events/event-types.d.ts +79 -0
  81. package/dist/events/event-types.js +11 -0
  82. package/dist/events/event-view/AddToCalendarButton.svelte +31 -0
  83. package/dist/events/event-view/AddToCalendarButton.svelte.d.ts +9 -0
  84. package/dist/events/event-view/EventBadges.svelte +20 -0
  85. package/dist/events/event-view/EventBadges.svelte.d.ts +7 -0
  86. package/dist/events/event-view/EventDateBlock.svelte +43 -0
  87. package/dist/events/event-view/EventDateBlock.svelte.d.ts +7 -0
  88. package/dist/events/event-view/EventHostedBy.svelte +61 -0
  89. package/dist/events/event-view/EventHostedBy.svelte.d.ts +16 -0
  90. package/dist/events/event-view/EventLinksList.svelte +24 -0
  91. package/dist/events/event-view/EventLinksList.svelte.d.ts +9 -0
  92. package/dist/events/event-view/EventLocationBlock.svelte +62 -0
  93. package/dist/events/event-view/EventLocationBlock.svelte.d.ts +7 -0
  94. package/dist/events/event-view/EventLocationMap.svelte +66 -0
  95. package/dist/events/event-view/EventLocationMap.svelte.d.ts +7 -0
  96. package/dist/events/event-view/ExternalRsvpNotice.svelte +34 -0
  97. package/dist/events/event-view/ExternalRsvpNotice.svelte.d.ts +6 -0
  98. package/dist/events/event-view/StreamPlacePlayer.svelte +209 -0
  99. package/dist/events/event-view/StreamPlacePlayer.svelte.d.ts +8 -0
  100. package/dist/events/event-view/format.d.ts +10 -0
  101. package/dist/events/event-view/format.js +112 -0
  102. package/dist/events/event-view/locations.d.ts +36 -0
  103. package/dist/events/event-view/locations.js +243 -0
  104. package/dist/events/event-view/plyr-icons.d.ts +3 -0
  105. package/dist/events/event-view/plyr-icons.js +46 -0
  106. package/dist/events/linked-records.d.ts +18 -0
  107. package/dist/events/linked-records.js +11 -0
  108. package/dist/events/profile-url.d.ts +3 -0
  109. package/dist/events/profile-url.js +11 -0
  110. package/dist/events/schedule/ConferenceTimetable.svelte +340 -0
  111. package/dist/events/schedule/ConferenceTimetable.svelte.d.ts +22 -0
  112. package/dist/events/schedule/DaySchedule.svelte +298 -0
  113. package/dist/events/schedule/DaySchedule.svelte.d.ts +26 -0
  114. package/dist/events/schedule/ScheduleEventCell.svelte +166 -0
  115. package/dist/events/schedule/ScheduleEventCell.svelte.d.ts +25 -0
  116. package/dist/events/schedule/schedule-utils.d.ts +44 -0
  117. package/dist/events/schedule/schedule-utils.js +166 -0
  118. package/dist/events/starter-pack-url.d.ts +3 -0
  119. package/dist/events/starter-pack-url.js +16 -0
  120. package/dist/events/theme.d.ts +9 -0
  121. package/dist/events/theme.js +36 -0
  122. package/dist/events/themes/Blobs.svelte +64 -0
  123. package/dist/events/themes/Blobs.svelte.d.ts +18 -0
  124. package/dist/events/themes/Butterflies.svelte +183 -0
  125. package/dist/events/themes/Butterflies.svelte.d.ts +3 -0
  126. package/dist/events/themes/Fireflies.svelte +134 -0
  127. package/dist/events/themes/Fireflies.svelte.d.ts +3 -0
  128. package/dist/events/themes/Kaleidoscope.svelte +183 -0
  129. package/dist/events/themes/Kaleidoscope.svelte.d.ts +3 -0
  130. package/dist/events/themes/Matrix.svelte +153 -0
  131. package/dist/events/themes/Matrix.svelte.d.ts +3 -0
  132. package/dist/events/themes/Stars.svelte +98 -0
  133. package/dist/events/themes/Stars.svelte.d.ts +3 -0
  134. package/dist/events/thumbnails/designs.d.ts +18 -0
  135. package/dist/events/thumbnails/designs.js +316 -0
  136. package/dist/events/view-data.d.ts +38 -0
  137. package/dist/events/view-data.js +1 -0
  138. package/dist/events-data.d.ts +15 -0
  139. package/dist/events-data.js +9 -0
  140. package/dist/events-editor.d.ts +2 -0
  141. package/dist/events-editor.js +2 -0
  142. package/dist/events.d.ts +19 -0
  143. package/dist/events.js +17 -0
  144. package/package.json +46 -4
@@ -0,0 +1,50 @@
1
+ import codes from 'i18n-iso-countries/codes.json' with { type: 'json' };
2
+ import english from 'i18n-iso-countries/langs/en.json' with { type: 'json' };
3
+ // Use only the maintained JSON datasets, not the package's CommonJS runtime:
4
+ // Vite's dev SSR module runner cannot evaluate its require() calls as ESM.
5
+ const countryCodes = new Map();
6
+ for (const [alpha2, alpha3] of codes) {
7
+ countryCodes.set(alpha2, alpha2);
8
+ countryCodes.set(alpha3, alpha2);
9
+ }
10
+ function countryNameKey(name) {
11
+ return name.trim().normalize('NFKD').replace(/\p{M}/gu, '').replace(/[‘’]/g, "'").toLowerCase();
12
+ }
13
+ const countryNames = new Map();
14
+ for (const [code, names] of Object.entries(english.countries)) {
15
+ for (const name of Array.isArray(names) ? names : [names]) {
16
+ countryNames.set(countryNameKey(name), code);
17
+ }
18
+ }
19
+ export function normalizeCountryCode(value) {
20
+ if (typeof value !== 'string')
21
+ return undefined;
22
+ const name = value.trim().normalize('NFKC').replace(/[‘’]/g, "'");
23
+ const code = name.toUpperCase();
24
+ const alpha2 = countryCodes.get(code);
25
+ if (alpha2)
26
+ return alpha2;
27
+ // UK is a common alias, but GB is the assigned ISO 3166-1 code.
28
+ if (code === 'UK')
29
+ return 'GB';
30
+ return countryNames.get(countryNameKey(name));
31
+ }
32
+ export class LocationValidationError extends Error {
33
+ constructor() {
34
+ super('Choose a location with a valid country before publishing. Remove and re-add the location to update it.');
35
+ this.name = 'LocationValidationError';
36
+ }
37
+ }
38
+ export function normalizeAddressLocation(location) {
39
+ const country = normalizeCountryCode(location.country);
40
+ // country is required by community.lexicon.location.address. Never omit it,
41
+ // truncate a name, or guess a country when the input cannot be resolved.
42
+ if (!country)
43
+ throw new LocationValidationError();
44
+ return { ...location, country };
45
+ }
46
+ export function normalizeEventLocations(locations) {
47
+ return locations.map((location) => location.$type === 'community.lexicon.location.address'
48
+ ? normalizeAddressLocation({ ...location, country: location.country })
49
+ : location);
50
+ }
@@ -0,0 +1,43 @@
1
+ import { type Token } from '@atcute/bluesky-richtext-parser';
2
+ import type { Facet } from '../../index.js';
3
+ import type { FlatEventRecord } from '../contrail.js';
4
+ import type { EventTheme } from '../theme.js';
5
+ import type { EventLocation, EventMode, Visibility } from './types.js';
6
+ export declare function tokensToFacets(tokens: Token[], resolveHandle: (handle: string) => Promise<string>): Promise<Facet[]>;
7
+ /** Render a selected thumbnail preset to a PNG File so it can be uploaded
8
+ * as a blob like a user-provided image. Returns null if the preset design
9
+ * is missing or the canvas fails to produce a blob. */
10
+ export declare function renderPresetThumbnail(args: {
11
+ design: string;
12
+ seed: number;
13
+ name: string;
14
+ dateStr: string;
15
+ accent?: string;
16
+ }): Promise<File | null>;
17
+ export declare function buildThumbnailMedia(args: {
18
+ isNew: boolean;
19
+ thumbnailChanged: boolean;
20
+ thumbnailFile: File | null;
21
+ existingMedia: Array<Record<string, unknown>>;
22
+ uploadBlob: (blob: Blob) => Promise<Record<string, unknown>>;
23
+ }): Promise<Array<Record<string, unknown>> | undefined>;
24
+ export declare function buildEventRecord(args: {
25
+ eventData: FlatEventRecord | null;
26
+ isNew: boolean;
27
+ name: string;
28
+ description: string;
29
+ startsAt: string;
30
+ endsAt: string;
31
+ timezone: string;
32
+ mode: EventMode;
33
+ visibility: Visibility;
34
+ theme: EventTheme;
35
+ links: Array<{
36
+ uri: string;
37
+ name: string;
38
+ }>;
39
+ location: EventLocation | null;
40
+ locationChanged: boolean;
41
+ media: Array<Record<string, unknown>> | undefined;
42
+ resolveHandle: (handle: string) => Promise<string>;
43
+ }): Promise<Record<string, unknown>>;
@@ -0,0 +1,168 @@
1
+ import { compressImage } from '../atproto-helpers.js';
2
+ import { tokenize } from '@atcute/bluesky-richtext-parser';
3
+ import { datetimeLocalToISO } from '../date-format.js';
4
+ import { designs, resolveAccentColor } from '../thumbnails/designs.js';
5
+ import { normalizeEventLocations } from './location.js';
6
+ import { getEventThumbnail } from '../event-media.js';
7
+ export async function tokensToFacets(tokens, resolveHandle) {
8
+ const encoder = new TextEncoder();
9
+ const facets = [];
10
+ let byteOffset = 0;
11
+ for (const token of tokens) {
12
+ const tokenBytes = encoder.encode(token.raw);
13
+ const byteStart = byteOffset;
14
+ const byteEnd = byteOffset + tokenBytes.length;
15
+ if (token.type === 'mention') {
16
+ try {
17
+ const did = await resolveHandle(token.handle);
18
+ if (did) {
19
+ facets.push({
20
+ index: { byteStart, byteEnd },
21
+ features: [{ $type: 'app.bsky.richtext.facet#mention', did }]
22
+ });
23
+ }
24
+ }
25
+ catch {
26
+ // skip unresolvable mentions
27
+ }
28
+ }
29
+ else if (token.type === 'autolink') {
30
+ facets.push({
31
+ index: { byteStart, byteEnd },
32
+ features: [{ $type: 'app.bsky.richtext.facet#link', uri: token.url }]
33
+ });
34
+ }
35
+ else if (token.type === 'topic') {
36
+ facets.push({
37
+ index: { byteStart, byteEnd },
38
+ features: [{ $type: 'app.bsky.richtext.facet#tag', tag: token.name }]
39
+ });
40
+ }
41
+ byteOffset = byteEnd;
42
+ }
43
+ return facets;
44
+ }
45
+ /** Render a selected thumbnail preset to a PNG File so it can be uploaded
46
+ * as a blob like a user-provided image. Returns null if the preset design
47
+ * is missing or the canvas fails to produce a blob. */
48
+ export async function renderPresetThumbnail(args) {
49
+ const drawer = designs[args.design];
50
+ if (!drawer)
51
+ return null;
52
+ const canvas = document.createElement('canvas');
53
+ canvas.width = 800;
54
+ canvas.height = 800;
55
+ const ctx = canvas.getContext('2d');
56
+ if (!ctx)
57
+ return null;
58
+ drawer(ctx, 800, 800, args.name.trim() || 'Event', args.dateStr, args.seed, resolveAccentColor(args.accent));
59
+ const blob = await new Promise((r) => canvas.toBlob(r, 'image/png'));
60
+ if (!blob)
61
+ return null;
62
+ return new File([blob], 'thumbnail.png', { type: 'image/png' });
63
+ }
64
+ export async function buildThumbnailMedia(args) {
65
+ const { isNew, thumbnailChanged, thumbnailFile, existingMedia, uploadBlob } = args;
66
+ if (!isNew && !thumbnailChanged) {
67
+ return existingMedia.length > 0 ? existingMedia : undefined;
68
+ }
69
+ // Editing a flyer should preserve its role and leave other media untouched.
70
+ const role = getEventThumbnail(existingMedia)?.role ?? 'thumbnail';
71
+ if (!thumbnailFile) {
72
+ const remaining = existingMedia.filter((m) => m.role !== role);
73
+ return remaining.length > 0 ? remaining : undefined;
74
+ }
75
+ const compressed = await compressImage(thumbnailFile);
76
+ const blobRef = await uploadBlob(compressed.blob);
77
+ if (!blobRef)
78
+ return existingMedia.length > 0 ? existingMedia : undefined;
79
+ return [
80
+ ...existingMedia.filter((m) => m.role !== role),
81
+ {
82
+ role,
83
+ content: blobRef,
84
+ aspect_ratio: {
85
+ width: compressed.aspectRatio.width,
86
+ height: compressed.aspectRatio.height
87
+ }
88
+ }
89
+ ];
90
+ }
91
+ export async function buildEventRecord(args) {
92
+ const { eventData, isNew, name, description, startsAt, endsAt, timezone, mode, visibility, theme, links, location, locationChanged, media, resolveHandle } = args;
93
+ const createdAt = isNew
94
+ ? new Date().toISOString()
95
+ : eventData?.createdAt || new Date().toISOString();
96
+ // Spread original record to preserve unspecced fields (e.g. additionalData)
97
+ const record = {
98
+ ...(eventData ? { ...eventData } : {}),
99
+ $type: 'community.lexicon.calendar.event',
100
+ createdWith: 'https://atmo.rsvp',
101
+ name: name.trim(),
102
+ mode: `community.lexicon.calendar.event#${mode}`,
103
+ status: 'community.lexicon.calendar.event#scheduled',
104
+ startsAt: datetimeLocalToISO(startsAt, timezone),
105
+ timezone,
106
+ createdAt,
107
+ theme
108
+ };
109
+ // Remove flattened fields that aren't part of the actual record
110
+ for (const k of [
111
+ 'cid',
112
+ 'did',
113
+ 'rkey',
114
+ 'uri',
115
+ 'rsvps',
116
+ 'rsvpsCount',
117
+ 'rsvpsGoingCount',
118
+ 'rsvpsInterestedCount',
119
+ 'rsvpsNotgoingCount'
120
+ ]) {
121
+ delete record[k];
122
+ }
123
+ const trimmedDescription = description.trim();
124
+ if (trimmedDescription) {
125
+ record.description = trimmedDescription;
126
+ const tokens = tokenize(trimmedDescription);
127
+ const facets = await tokensToFacets(tokens, resolveHandle);
128
+ if (facets.length > 0)
129
+ record.facets = facets;
130
+ }
131
+ if (endsAt)
132
+ record.endsAt = datetimeLocalToISO(endsAt, timezone);
133
+ if (media)
134
+ record.media = media;
135
+ else
136
+ delete record.media;
137
+ if (links.length > 0)
138
+ record.uris = links;
139
+ else
140
+ delete record.uris;
141
+ if (isNew || locationChanged) {
142
+ delete record.locations;
143
+ if (location) {
144
+ record.locations = [
145
+ {
146
+ $type: 'community.lexicon.location.address',
147
+ ...location
148
+ }
149
+ ];
150
+ }
151
+ // If changed/new but no location, locations stays undefined (removed/absent)
152
+ }
153
+ else if (eventData?.locations && eventData.locations.length > 0) {
154
+ record.locations = eventData.locations;
155
+ }
156
+ // Normalize every address, including unchanged legacy locations and imported
157
+ // names. Preserve other location variants and address metadata as-is.
158
+ if (Array.isArray(record.locations)) {
159
+ record.locations = normalizeEventLocations(record.locations);
160
+ }
161
+ const existingPrefs = (record.preferences ??
162
+ {});
163
+ record.preferences = {
164
+ ...existingPrefs,
165
+ showInDiscovery: visibility !== 'unlisted'
166
+ };
167
+ return record;
168
+ }
@@ -0,0 +1,39 @@
1
+ export type EventMode = 'inperson' | 'virtual' | 'hybrid';
2
+ export type Visibility = 'public' | 'unlisted';
3
+ export interface EventLocation {
4
+ street?: string;
5
+ locality?: string;
6
+ region?: string;
7
+ country?: string;
8
+ }
9
+ /**
10
+ * Optional autofill payload for a brand-new event. EventEditor populates its
11
+ * fields from this on mount (only when there is no `eventData`), while leaving
12
+ * `isNew` true so the save path still treats the result as a creation. Use
13
+ * `additionalData` to carry atmo-specific extras (e.g. an external source link
14
+ * + rsvp mode) into the saved record.
15
+ */
16
+ export interface EventEditorPrefill {
17
+ name?: string;
18
+ description?: string;
19
+ /** ISO 8601 string. */
20
+ startsAt?: string;
21
+ /** ISO 8601 string. */
22
+ endsAt?: string;
23
+ timezone?: string;
24
+ mode?: EventMode;
25
+ location?: EventLocation;
26
+ links?: Array<{
27
+ uri: string;
28
+ name: string;
29
+ }>;
30
+ additionalData?: Record<string, unknown>;
31
+ /**
32
+ * Pre-supplied cover image. When set, the editor uses this instead of
33
+ * auto-generating a preset thumbnail and the file is uploaded as a blob on
34
+ * save.
35
+ */
36
+ thumbnailFile?: File;
37
+ }
38
+ export declare function stripModePrefix(modeStr: string): EventMode;
39
+ export declare function getLocationDisplayString(loc: EventLocation): string;
@@ -0,0 +1,9 @@
1
+ export function stripModePrefix(modeStr) {
2
+ const stripped = modeStr.replace('community.lexicon.calendar.event#', '');
3
+ if (stripped === 'virtual' || stripped === 'hybrid' || stripped === 'inperson')
4
+ return stripped;
5
+ return 'inperson';
6
+ }
7
+ export function getLocationDisplayString(loc) {
8
+ return [loc.street, loc.locality, loc.region, loc.country].filter(Boolean).join(', ');
9
+ }
@@ -0,0 +1,4 @@
1
+ /** Select the event's thumbnail, falling back to a flyer before any header image. */
2
+ export declare function getEventThumbnail<T extends {
3
+ role?: unknown;
4
+ }>(media: readonly T[] | null | undefined): T | undefined;
@@ -0,0 +1,4 @@
1
+ /** Select the event's thumbnail, falling back to a flyer before any header image. */
2
+ export function getEventThumbnail(media) {
3
+ return media?.find((m) => m.role === 'thumbnail') ?? media?.find((m) => m.role === 'flyer');
4
+ }
@@ -0,0 +1,79 @@
1
+ /**
2
+ * Structural shape of a `community.lexicon.calendar.event` record, plus the
3
+ * extensions atmo adds (timezone, media, facets, theme, bskyPostRef).
4
+ *
5
+ * Defined permissively so consumers using project-local generated lexicon
6
+ * types can still pass their records into our components without type
7
+ * gymnastics. Components inspect specific fields and `$type` strings as
8
+ * needed; rich variant unions live in the consumer's lexicon types if they
9
+ * want them.
10
+ */
11
+ export type EventLocationVariant = {
12
+ $type: string;
13
+ [key: string]: unknown;
14
+ };
15
+ export type EventStatus = string;
16
+ export type EventMode = string;
17
+ export interface EventLexiconMain {
18
+ $type?: 'community.lexicon.calendar.event';
19
+ createdAt: string;
20
+ name: string;
21
+ description?: string;
22
+ startsAt?: string;
23
+ endsAt?: string;
24
+ locations?: EventLocationVariant[];
25
+ mode?: EventMode;
26
+ status?: EventStatus;
27
+ /** Whether a response is requested from attendees. Only explicit false opts out. */
28
+ rsvpExpected?: boolean;
29
+ }
30
+ export type EventData = EventLexiconMain & {
31
+ startsAt: string;
32
+ timezone?: string;
33
+ /** Atmo stores URIs as objects with optional names rather than bare strings. */
34
+ uris?: Array<{
35
+ uri: string;
36
+ name?: string;
37
+ }>;
38
+ media?: Array<{
39
+ role: string;
40
+ alt?: string;
41
+ aspect_ratio?: {
42
+ width: number;
43
+ height: number;
44
+ };
45
+ content: {
46
+ $type: 'blob';
47
+ ref: {
48
+ $link: string;
49
+ };
50
+ };
51
+ [key: string]: unknown;
52
+ }>;
53
+ facets?: Array<{
54
+ index: {
55
+ byteStart: number;
56
+ byteEnd: number;
57
+ };
58
+ features: Array<{
59
+ $type: string;
60
+ did?: string;
61
+ uri?: string;
62
+ tag?: string;
63
+ }>;
64
+ }>;
65
+ additionalData?: Record<string, unknown>;
66
+ preferences?: {
67
+ showInDiscovery?: boolean;
68
+ };
69
+ theme?: {
70
+ name: string;
71
+ accentColor: string;
72
+ baseColor: string;
73
+ };
74
+ bskyPostRef?: {
75
+ uri: string;
76
+ cid: string;
77
+ showComments: boolean;
78
+ };
79
+ };
@@ -0,0 +1,11 @@
1
+ /**
2
+ * Structural shape of a `community.lexicon.calendar.event` record, plus the
3
+ * extensions atmo adds (timezone, media, facets, theme, bskyPostRef).
4
+ *
5
+ * Defined permissively so consumers using project-local generated lexicon
6
+ * types can still pass their records into our components without type
7
+ * gymnastics. Components inspect specific fields and `$type` strings as
8
+ * needed; rich variant unions live in the consumer's lexicon types if they
9
+ * want them.
10
+ */
11
+ export {};
@@ -0,0 +1,31 @@
1
+ <script lang="ts">
2
+ import { Icon } from '../../index.js';
3
+ import { CalendarPlus } from '@sketchyicons/svelte';
4
+ import { generateICalEvent } from '../cal/ical.js';
5
+ import type { FlatEventRecord } from '../contrail.js';
6
+
7
+ let {
8
+ eventData,
9
+ eventUri,
10
+ pageHref
11
+ }: { eventData: FlatEventRecord; eventUri: string; pageHref: string } = $props();
12
+
13
+ function downloadIcs() {
14
+ const ical = generateICalEvent(eventData, eventUri, pageHref);
15
+ const blob = new Blob([ical], { type: 'text/calendar;charset=utf-8' });
16
+ const url = URL.createObjectURL(blob);
17
+ const a = document.createElement('a');
18
+ a.href = url;
19
+ a.download = `${eventData.name.replace(/[^a-zA-Z0-9]/g, '-')}.ics`;
20
+ a.click();
21
+ URL.revokeObjectURL(url);
22
+ }
23
+ </script>
24
+
25
+ <button
26
+ onclick={downloadIcs}
27
+ class="flex cursor-pointer items-center gap-2 text-sm font-medium text-base-700 transition-colors hover:text-base-900 dark:text-base-300 dark:hover:text-base-100"
28
+ >
29
+ <Icon img={CalendarPlus} size={16} />
30
+ Add to Calendar
31
+ </button>
@@ -0,0 +1,9 @@
1
+ import type { FlatEventRecord } from '../contrail.js';
2
+ type $$ComponentProps = {
3
+ eventData: FlatEventRecord;
4
+ eventUri: string;
5
+ pageHref: string;
6
+ };
7
+ declare const AddToCalendarButton: import("svelte").Component<$$ComponentProps, {}, "">;
8
+ type AddToCalendarButton = ReturnType<typeof AddToCalendarButton>;
9
+ export default AddToCalendarButton;
@@ -0,0 +1,20 @@
1
+ <script lang="ts">
2
+ import { Badge } from '../../index.js';
3
+ import { getModeLabel } from './format';
4
+
5
+ let { mode, isOngoing }: { mode?: string; isOngoing: boolean } = $props();
6
+ </script>
7
+
8
+ {#if mode || isOngoing}
9
+ <div class="mb-8 flex items-center gap-2">
10
+ {#if isOngoing}
11
+ <Badge>
12
+ <span class="mr-1 inline-block size-1.5 animate-pulse rounded-full bg-accent-500"></span>
13
+ Live
14
+ </Badge>
15
+ {/if}
16
+ {#if mode}
17
+ <Badge variant="outline">{getModeLabel(mode)}</Badge>
18
+ {/if}
19
+ </div>
20
+ {/if}
@@ -0,0 +1,7 @@
1
+ type $$ComponentProps = {
2
+ mode?: string;
3
+ isOngoing: boolean;
4
+ };
5
+ declare const EventBadges: import("svelte").Component<$$ComponentProps, {}, "">;
6
+ type EventBadges = ReturnType<typeof EventBadges>;
7
+ export default EventBadges;
@@ -0,0 +1,43 @@
1
+ <script lang="ts">
2
+ import { Badge } from '../../index.js';
3
+ import { formatDay, formatFullDate, formatMonth, formatTime, formatWeekday } from './format';
4
+
5
+ let { startDate, endDate }: { startDate: Date; endDate: Date | null } = $props();
6
+
7
+ let isSameDay = $derived(
8
+ endDate &&
9
+ startDate.getFullYear() === endDate.getFullYear() &&
10
+ startDate.getMonth() === endDate.getMonth() &&
11
+ startDate.getDate() === endDate.getDate()
12
+ );
13
+ </script>
14
+
15
+ <div class="mb-4 flex items-center gap-4">
16
+ <div
17
+ class="flex size-12 shrink-0 flex-col items-center justify-center overflow-hidden rounded-ui bg-recess [box-shadow:var(--ui-recess-shadow)]"
18
+ >
19
+ <span class="text-[9px] leading-none font-semibold text-muted">
20
+ {formatMonth(startDate)}
21
+ </span>
22
+ <span class="text-lg leading-tight font-bold text-base-900 dark:text-base-50">
23
+ {formatDay(startDate)}
24
+ </span>
25
+ </div>
26
+ <div>
27
+ <p class="font-semibold text-base-900 dark:text-base-50">
28
+ {formatWeekday(startDate)}, {formatFullDate(startDate)}
29
+ {#if endDate && !isSameDay}
30
+ - {formatWeekday(endDate)}, {formatFullDate(endDate)}
31
+ {/if}
32
+ </p>
33
+ <p class="flex flex-wrap items-center gap-2 text-sm text-muted">
34
+ <span>
35
+ {formatTime(startDate)}
36
+ {#if endDate && isSameDay}
37
+ - {formatTime(endDate)}
38
+ {/if}
39
+ </span>
40
+ <Badge variant="outline">local time</Badge>
41
+ </p>
42
+ </div>
43
+ </div>
@@ -0,0 +1,7 @@
1
+ type $$ComponentProps = {
2
+ startDate: Date;
3
+ endDate: Date | null;
4
+ };
5
+ declare const EventDateBlock: import("svelte").Component<$$ComponentProps, {}, "">;
6
+ type EventDateBlock = ReturnType<typeof EventDateBlock>;
7
+ export default EventDateBlock;
@@ -0,0 +1,61 @@
1
+ <script lang="ts">
2
+ import { Avatar } from '../../index.js';
3
+ import type { HostProfile } from '../contrail.js';
4
+
5
+ type Speaker = { id?: string; handle?: string; name: string; avatar?: string };
6
+
7
+ let {
8
+ hostProfile,
9
+ hostUrl,
10
+ did,
11
+ speakers = []
12
+ }: {
13
+ hostProfile: HostProfile | null | undefined;
14
+ hostUrl: string;
15
+ did: string;
16
+ speakers?: Speaker[];
17
+ } = $props();
18
+ </script>
19
+
20
+ <div>
21
+ <p class="mb-3 text-xs font-semibold tracking-wider text-muted uppercase">Hosted By</p>
22
+ <a
23
+ href={hostUrl}
24
+ class="flex items-center gap-2.5 font-medium text-base-900 transition-opacity hover:opacity-80 dark:text-base-100"
25
+ >
26
+ <Avatar
27
+ src={hostProfile?.avatar}
28
+ name={hostProfile?.displayName || hostProfile?.handle || did}
29
+ class="size-8 shrink-0"
30
+ />
31
+ <span class="truncate text-sm">
32
+ {hostProfile?.displayName || hostProfile?.handle || did}
33
+ </span>
34
+ </a>
35
+ </div>
36
+
37
+ {#if speakers.length > 0}
38
+ <div>
39
+ <p class="mb-3 text-xs font-semibold tracking-wider text-muted uppercase">Speakers</p>
40
+ <div class="space-y-2">
41
+ {#each speakers as speaker, i (speaker.id || i)}
42
+ {#if speaker.handle}
43
+ <a
44
+ href="https://bsky.app/profile/{speaker.handle}"
45
+ target="_blank"
46
+ rel="noopener noreferrer"
47
+ class="flex items-center gap-2.5 font-medium text-base-900 transition-opacity hover:opacity-80 dark:text-base-100"
48
+ >
49
+ <Avatar src={speaker.avatar} name={speaker.name} class="size-8 shrink-0" />
50
+ <span class="truncate text-sm">{speaker.name}</span>
51
+ </a>
52
+ {:else}
53
+ <div class="flex items-center gap-2.5 font-medium text-base-900 dark:text-base-100">
54
+ <Avatar name={speaker.name} class="size-8 shrink-0" />
55
+ <span class="truncate text-sm">{speaker.name}</span>
56
+ </div>
57
+ {/if}
58
+ {/each}
59
+ </div>
60
+ </div>
61
+ {/if}
@@ -0,0 +1,16 @@
1
+ import type { HostProfile } from '../contrail.js';
2
+ type Speaker = {
3
+ id?: string;
4
+ handle?: string;
5
+ name: string;
6
+ avatar?: string;
7
+ };
8
+ type $$ComponentProps = {
9
+ hostProfile: HostProfile | null | undefined;
10
+ hostUrl: string;
11
+ did: string;
12
+ speakers?: Speaker[];
13
+ };
14
+ declare const EventHostedBy: import("svelte").Component<$$ComponentProps, {}, "">;
15
+ type EventHostedBy = ReturnType<typeof EventHostedBy>;
16
+ export default EventHostedBy;
@@ -0,0 +1,24 @@
1
+ <script lang="ts">
2
+ import { Icon } from '../../index.js';
3
+ import { Link } from '@sketchyicons/svelte';
4
+ let { uris = [] }: { uris?: Array<{ uri: string; name?: string }> } = $props();
5
+ </script>
6
+
7
+ {#if uris.length}
8
+ <div>
9
+ <p class="mb-3 text-xs font-medium text-muted">Links</p>
10
+ <div class="space-y-3">
11
+ {#each uris as link (link.name + link.uri)}
12
+ <a
13
+ href={link.uri}
14
+ target="_blank"
15
+ rel="noopener noreferrer"
16
+ class="flex items-center gap-2 rounded-sm text-sm text-muted transition-colors hover:text-action"
17
+ ><Icon img={Link} size={15} /><span class="truncate"
18
+ >{link.name || link.uri.replace(/^https?:\/\//, '')}</span
19
+ ></a
20
+ >
21
+ {/each}
22
+ </div>
23
+ </div>
24
+ {/if}
@@ -0,0 +1,9 @@
1
+ type $$ComponentProps = {
2
+ uris?: Array<{
3
+ uri: string;
4
+ name?: string;
5
+ }>;
6
+ };
7
+ declare const EventLinksList: import("svelte").Component<$$ComponentProps, {}, "">;
8
+ type EventLinksList = ReturnType<typeof EventLinksList>;
9
+ export default EventLinksList;