@atmo-dev/kit 0.3.0 → 0.5.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (180) hide show
  1. package/README.md +94 -1
  2. package/dist/components/ExternalEmbed.svelte +174 -0
  3. package/dist/components/ExternalEmbed.svelte.d.ts +8 -0
  4. package/dist/components/GrainAlbumPicker.svelte +218 -0
  5. package/dist/components/GrainAlbumPicker.svelte.d.ts +14 -0
  6. package/dist/components/GrainAlbumView.svelte +94 -0
  7. package/dist/components/GrainAlbumView.svelte.d.ts +10 -0
  8. package/dist/components/NestedComments.svelte +20 -10
  9. package/dist/components/NestedComments.svelte.d.ts +2 -0
  10. package/dist/components/PhotoGrid.svelte +53 -0
  11. package/dist/components/PhotoGrid.svelte.d.ts +9 -0
  12. package/dist/components/PhotoLightbox.svelte +332 -0
  13. package/dist/components/PhotoLightbox.svelte.d.ts +12 -0
  14. package/dist/components/PostContent.svelte +78 -20
  15. package/dist/components/PostContent.svelte.d.ts +5 -0
  16. package/dist/components/StreamplacePicker.svelte +94 -0
  17. package/dist/components/StreamplacePicker.svelte.d.ts +12 -0
  18. package/dist/events/DatePicker.svelte +222 -0
  19. package/dist/events/DatePicker.svelte.d.ts +12 -0
  20. package/dist/events/DateTimePicker.svelte +112 -0
  21. package/dist/events/DateTimePicker.svelte.d.ts +10 -0
  22. package/dist/events/EventAttendees.svelte +199 -0
  23. package/dist/events/EventAttendees.svelte.d.ts +19 -0
  24. package/dist/events/EventCard.svelte +148 -0
  25. package/dist/events/EventCard.svelte.d.ts +8 -0
  26. package/dist/events/EventComments.svelte +98 -0
  27. package/dist/events/EventComments.svelte.d.ts +8 -0
  28. package/dist/events/EventEditor.svelte +540 -0
  29. package/dist/events/EventEditor.svelte.d.ts +18 -0
  30. package/dist/events/EventRsvp.svelte +178 -0
  31. package/dist/events/EventRsvp.svelte.d.ts +16 -0
  32. package/dist/events/EventView.svelte +488 -0
  33. package/dist/events/EventView.svelte.d.ts +23 -0
  34. package/dist/events/ImageDropper.svelte +66 -0
  35. package/dist/events/ImageDropper.svelte.d.ts +7 -0
  36. package/dist/events/Map.svelte +36 -0
  37. package/dist/events/Map.svelte.d.ts +8 -0
  38. package/dist/events/PostToBlueskyModal.svelte +246 -0
  39. package/dist/events/PostToBlueskyModal.svelte.d.ts +22 -0
  40. package/dist/events/ShareModal.svelte +174 -0
  41. package/dist/events/ShareModal.svelte.d.ts +23 -0
  42. package/dist/events/StarterPackButton.svelte +72 -0
  43. package/dist/events/StarterPackButton.svelte.d.ts +10 -0
  44. package/dist/events/ThemeApply.svelte +30 -0
  45. package/dist/events/ThemeApply.svelte.d.ts +7 -0
  46. package/dist/events/ThemeBackground.svelte +33 -0
  47. package/dist/events/ThemeBackground.svelte.d.ts +7 -0
  48. package/dist/events/ThemePicker.svelte +104 -0
  49. package/dist/events/ThemePicker.svelte.d.ts +7 -0
  50. package/dist/events/ThumbnailPresets.svelte +71 -0
  51. package/dist/events/ThumbnailPresets.svelte.d.ts +11 -0
  52. package/dist/events/TimePicker.svelte +117 -0
  53. package/dist/events/TimePicker.svelte.d.ts +10 -0
  54. package/dist/events/TimezonePicker.svelte +67 -0
  55. package/dist/events/TimezonePicker.svelte.d.ts +6 -0
  56. package/dist/events/VodPlayer.svelte +154 -0
  57. package/dist/events/VodPlayer.svelte.d.ts +14 -0
  58. package/dist/events/VodTranscript.svelte +75 -0
  59. package/dist/events/VodTranscript.svelte.d.ts +8 -0
  60. package/dist/events/atproto-helpers.d.ts +21 -0
  61. package/dist/events/atproto-helpers.js +61 -0
  62. package/dist/events/cal/helper.d.ts +1 -0
  63. package/dist/events/cal/helper.js +20 -0
  64. package/dist/events/cal/ical.d.ts +22 -0
  65. package/dist/events/cal/ical.js +188 -0
  66. package/dist/events/cal/sanitize.d.ts +3 -0
  67. package/dist/events/cal/sanitize.js +25 -0
  68. package/dist/events/conference.d.ts +65 -0
  69. package/dist/events/conference.js +61 -0
  70. package/dist/events/contrail.d.ts +49 -0
  71. package/dist/events/contrail.js +12 -0
  72. package/dist/events/date-format.d.ts +22 -0
  73. package/dist/events/date-format.js +43 -0
  74. package/dist/events/editor/LinkedRecordsPicker.svelte +104 -0
  75. package/dist/events/editor/LinkedRecordsPicker.svelte.d.ts +12 -0
  76. package/dist/events/editor/LinksSection.svelte +154 -0
  77. package/dist/events/editor/LinksSection.svelte.d.ts +13 -0
  78. package/dist/events/editor/LocationSection.svelte +163 -0
  79. package/dist/events/editor/LocationSection.svelte.d.ts +8 -0
  80. package/dist/events/editor/RecurringModal.svelte +313 -0
  81. package/dist/events/editor/RecurringModal.svelte.d.ts +30 -0
  82. package/dist/events/editor/ThemeSection.svelte +24 -0
  83. package/dist/events/editor/ThemeSection.svelte.d.ts +7 -0
  84. package/dist/events/editor/ThumbnailSection.svelte +181 -0
  85. package/dist/events/editor/ThumbnailSection.svelte.d.ts +13 -0
  86. package/dist/events/editor/adapter.d.ts +73 -0
  87. package/dist/events/editor/adapter.js +9 -0
  88. package/dist/events/editor/location.d.ts +16 -0
  89. package/dist/events/editor/location.js +50 -0
  90. package/dist/events/editor/save.d.ts +43 -0
  91. package/dist/events/editor/save.js +168 -0
  92. package/dist/events/editor/types.d.ts +39 -0
  93. package/dist/events/editor/types.js +9 -0
  94. package/dist/events/event-media.d.ts +4 -0
  95. package/dist/events/event-media.js +4 -0
  96. package/dist/events/event-types.d.ts +79 -0
  97. package/dist/events/event-types.js +11 -0
  98. package/dist/events/event-view/AddToCalendarButton.svelte +31 -0
  99. package/dist/events/event-view/AddToCalendarButton.svelte.d.ts +9 -0
  100. package/dist/events/event-view/EventBadges.svelte +20 -0
  101. package/dist/events/event-view/EventBadges.svelte.d.ts +7 -0
  102. package/dist/events/event-view/EventDateBlock.svelte +43 -0
  103. package/dist/events/event-view/EventDateBlock.svelte.d.ts +7 -0
  104. package/dist/events/event-view/EventHostedBy.svelte +61 -0
  105. package/dist/events/event-view/EventHostedBy.svelte.d.ts +16 -0
  106. package/dist/events/event-view/EventLinksList.svelte +24 -0
  107. package/dist/events/event-view/EventLinksList.svelte.d.ts +9 -0
  108. package/dist/events/event-view/EventLocationBlock.svelte +62 -0
  109. package/dist/events/event-view/EventLocationBlock.svelte.d.ts +7 -0
  110. package/dist/events/event-view/EventLocationMap.svelte +66 -0
  111. package/dist/events/event-view/EventLocationMap.svelte.d.ts +7 -0
  112. package/dist/events/event-view/ExternalRsvpNotice.svelte +34 -0
  113. package/dist/events/event-view/ExternalRsvpNotice.svelte.d.ts +6 -0
  114. package/dist/events/event-view/StreamPlacePlayer.svelte +209 -0
  115. package/dist/events/event-view/StreamPlacePlayer.svelte.d.ts +8 -0
  116. package/dist/events/event-view/format.d.ts +10 -0
  117. package/dist/events/event-view/format.js +112 -0
  118. package/dist/events/event-view/locations.d.ts +36 -0
  119. package/dist/events/event-view/locations.js +243 -0
  120. package/dist/events/event-view/plyr-icons.d.ts +3 -0
  121. package/dist/events/event-view/plyr-icons.js +46 -0
  122. package/dist/events/linked-records.d.ts +18 -0
  123. package/dist/events/linked-records.js +11 -0
  124. package/dist/events/profile-url.d.ts +3 -0
  125. package/dist/events/profile-url.js +11 -0
  126. package/dist/events/schedule/ConferenceTimetable.svelte +340 -0
  127. package/dist/events/schedule/ConferenceTimetable.svelte.d.ts +22 -0
  128. package/dist/events/schedule/DaySchedule.svelte +298 -0
  129. package/dist/events/schedule/DaySchedule.svelte.d.ts +26 -0
  130. package/dist/events/schedule/ScheduleEventCell.svelte +166 -0
  131. package/dist/events/schedule/ScheduleEventCell.svelte.d.ts +25 -0
  132. package/dist/events/schedule/schedule-utils.d.ts +44 -0
  133. package/dist/events/schedule/schedule-utils.js +166 -0
  134. package/dist/events/starter-pack-url.d.ts +3 -0
  135. package/dist/events/starter-pack-url.js +16 -0
  136. package/dist/events/theme.d.ts +9 -0
  137. package/dist/events/theme.js +36 -0
  138. package/dist/events/themes/Blobs.svelte +64 -0
  139. package/dist/events/themes/Blobs.svelte.d.ts +18 -0
  140. package/dist/events/themes/Butterflies.svelte +183 -0
  141. package/dist/events/themes/Butterflies.svelte.d.ts +3 -0
  142. package/dist/events/themes/Fireflies.svelte +134 -0
  143. package/dist/events/themes/Fireflies.svelte.d.ts +3 -0
  144. package/dist/events/themes/Kaleidoscope.svelte +183 -0
  145. package/dist/events/themes/Kaleidoscope.svelte.d.ts +3 -0
  146. package/dist/events/themes/Matrix.svelte +153 -0
  147. package/dist/events/themes/Matrix.svelte.d.ts +3 -0
  148. package/dist/events/themes/Stars.svelte +98 -0
  149. package/dist/events/themes/Stars.svelte.d.ts +3 -0
  150. package/dist/events/thumbnails/designs.d.ts +18 -0
  151. package/dist/events/thumbnails/designs.js +316 -0
  152. package/dist/events/view-data.d.ts +38 -0
  153. package/dist/events/view-data.js +1 -0
  154. package/dist/events-data.d.ts +15 -0
  155. package/dist/events-data.js +9 -0
  156. package/dist/events-editor.d.ts +2 -0
  157. package/dist/events-editor.js +2 -0
  158. package/dist/events.d.ts +19 -0
  159. package/dist/events.js +17 -0
  160. package/dist/grain/types.d.ts +41 -0
  161. package/dist/grain/types.js +1 -0
  162. package/dist/grain/urls.d.ts +7 -0
  163. package/dist/grain/urls.js +74 -0
  164. package/dist/grain/xrpc.d.ts +3 -0
  165. package/dist/grain/xrpc.js +129 -0
  166. package/dist/grain.d.ts +3 -0
  167. package/dist/grain.js +2 -0
  168. package/dist/index.d.ts +16 -0
  169. package/dist/index.js +9 -0
  170. package/dist/media.d.ts +10 -0
  171. package/dist/media.js +1 -0
  172. package/dist/social/external-embeds.d.ts +39 -0
  173. package/dist/social/external-embeds.js +107 -0
  174. package/dist/social/posts.d.ts +59 -17
  175. package/dist/social/posts.js +99 -12
  176. package/dist/social.d.ts +2 -0
  177. package/dist/social.js +1 -0
  178. package/dist/streamplace.d.ts +6 -0
  179. package/dist/streamplace.js +1 -0
  180. package/package.json +58 -4
@@ -0,0 +1,243 @@
1
+ function text(record, key) {
2
+ const value = record[key];
3
+ return typeof value === 'string' ? value.trim() || undefined : undefined;
4
+ }
5
+ function coordinates(record) {
6
+ const latitude = text(record, 'latitude');
7
+ const longitude = text(record, 'longitude');
8
+ const decimal = /^[+-]?(?:\d+(?:\.\d*)?|\.\d+)$/;
9
+ if (!latitude || !longitude || !decimal.test(latitude) || !decimal.test(longitude))
10
+ return;
11
+ const lat = Number(latitude);
12
+ const lng = Number(longitude);
13
+ if (!Number.isFinite(lat) || !Number.isFinite(lng) || Math.abs(lat) > 90 || Math.abs(lng) > 180)
14
+ return;
15
+ return { lat, lng };
16
+ }
17
+ function coordinateLabel({ lat, lng }) {
18
+ return `${lat.toFixed(5)}, ${lng.toFixed(5)}`;
19
+ }
20
+ function mapsUrl(query) {
21
+ return `https://www.google.com/maps/search/?api=1&query=${encodeURIComponent(query)}`;
22
+ }
23
+ function webUrl(uri) {
24
+ try {
25
+ const url = new URL(uri);
26
+ if (url.protocol === 'https:' || url.protocol === 'http:')
27
+ return url.href;
28
+ }
29
+ catch {
30
+ // Invalid URLs render as non-interactive location text.
31
+ }
32
+ return undefined;
33
+ }
34
+ function formatLocation(value) {
35
+ if (!value || typeof value !== 'object')
36
+ return null;
37
+ const record = value;
38
+ const name = text(record, 'name');
39
+ switch (record.$type) {
40
+ case 'community.lexicon.location.address': {
41
+ const locality = text(record, 'locality');
42
+ const street = text(record, 'street');
43
+ const address = [
44
+ street,
45
+ locality,
46
+ text(record, 'region'),
47
+ text(record, 'postalCode'),
48
+ text(record, 'country')
49
+ ]
50
+ .filter(Boolean)
51
+ .join(', ');
52
+ const label = name ?? address;
53
+ if (!label)
54
+ return null;
55
+ const fullString = name && address && name !== address ? `${name}, ${address}` : label;
56
+ return {
57
+ kind: 'address',
58
+ name,
59
+ label,
60
+ detail: name && address !== name ? address || undefined : undefined,
61
+ href: mapsUrl(fullString),
62
+ summary: locality ?? name ?? street,
63
+ geocodeQuery: fullString
64
+ };
65
+ }
66
+ case 'community.lexicon.location.geo': {
67
+ const point = coordinates(record);
68
+ if (!point)
69
+ return null;
70
+ const coords = coordinateLabel(point);
71
+ return {
72
+ kind: 'geo',
73
+ name,
74
+ label: name ?? coords,
75
+ detail: name ? coords : undefined,
76
+ href: mapsUrl(`${point.lat},${point.lng}`),
77
+ summary: name ?? coords,
78
+ coordinates: point
79
+ };
80
+ }
81
+ case 'community.lexicon.location.fsq': {
82
+ const id = text(record, 'fsq_place_id');
83
+ if (!id)
84
+ return null;
85
+ return {
86
+ kind: 'fsq',
87
+ name,
88
+ label: name ?? `Foursquare: ${id}`,
89
+ href: `https://foursquare.com/v/${encodeURIComponent(id)}`,
90
+ summary: name ?? `Foursquare: ${id}`,
91
+ coordinates: coordinates(record)
92
+ };
93
+ }
94
+ case 'community.lexicon.location.hthree': {
95
+ const cell = text(record, 'value');
96
+ if (!cell)
97
+ return null;
98
+ return {
99
+ kind: 'hthree',
100
+ name,
101
+ label: name ?? `H3: ${cell}`,
102
+ detail: name ? `H3: ${cell}` : undefined,
103
+ summary: name ?? `H3: ${cell}`,
104
+ h3Cell: cell
105
+ };
106
+ }
107
+ case 'community.lexicon.calendar.event#uri': {
108
+ const uri = text(record, 'uri');
109
+ if (!uri)
110
+ return null;
111
+ // AT references describe a place, not a user-facing link. Without a
112
+ // supplied name there is nothing useful to show until it is resolved.
113
+ if (uri.startsWith('at://')) {
114
+ return name ? { kind: 'place', name, label: name, summary: name } : null;
115
+ }
116
+ return {
117
+ kind: 'uri',
118
+ label: name ?? uri,
119
+ detail: name && name !== uri ? uri : undefined,
120
+ href: webUrl(uri),
121
+ summary: name ?? uri
122
+ };
123
+ }
124
+ default:
125
+ return null;
126
+ }
127
+ }
128
+ /** Preserve record order and supported variants, keeping only the first entry for each URI. */
129
+ export function getLocations(locations) {
130
+ if (!Array.isArray(locations))
131
+ return [];
132
+ const seenUris = new Set();
133
+ return locations.map(formatLocation).filter((location) => {
134
+ if (!location)
135
+ return false;
136
+ if (location.kind !== 'uri')
137
+ return true;
138
+ const uri = location.href ?? location.detail ?? location.label;
139
+ if (seenUris.has(uri))
140
+ return false;
141
+ seenUris.add(uri);
142
+ return true;
143
+ });
144
+ }
145
+ /** One pin for the place: venue names above addresses, coordinates only as a fallback. */
146
+ export function getPhysicalLocationLines(locations) {
147
+ const physical = locations.filter((location) => location.kind !== 'uri');
148
+ const names = physical.filter((location) => location.name);
149
+ const mapHref = physical.find((location) => location.kind === 'address')?.href ??
150
+ physical.find((location) => location.coordinates)?.href;
151
+ const lines = names.map((location) => ({
152
+ label: location.name,
153
+ href: location.href ?? (location.kind === 'place' ? mapHref : undefined),
154
+ muted: false
155
+ }));
156
+ const hasDescription = names.length > 0 ||
157
+ locations.some((location) => location.kind !== 'geo' && location.kind !== 'hthree');
158
+ const hasAddress = physical.some((location) => location.kind === 'address');
159
+ for (const location of physical) {
160
+ if (location.kind === 'address') {
161
+ const label = location.name ? location.detail : location.label;
162
+ if (label)
163
+ lines.push({ label, href: location.href, muted: names.length > 0 });
164
+ }
165
+ else if (location.kind === 'fsq' && !location.name && !hasAddress) {
166
+ lines.push({ label: location.label, href: location.href, muted: false });
167
+ }
168
+ else if (!hasDescription && (location.kind === 'geo' || location.kind === 'hthree')) {
169
+ lines.push({ label: location.label, href: location.href, muted: false });
170
+ }
171
+ }
172
+ return lines.filter((line, index) => lines.findIndex((other) => other.label === line.label) === index);
173
+ }
174
+ /** Cards stay compact: prefer cities, falling back to the other physical variants. */
175
+ export function getLocationSummary(locations) {
176
+ const physical = locations.filter((location) => location.kind !== 'uri' && location.summary);
177
+ const cities = physical.filter((location) => location.kind === 'address');
178
+ const links = locations.filter((location) => location.kind === 'uri');
179
+ const described = physical.filter((location) => location.name || location.kind === 'fsq');
180
+ const preferred = cities.length
181
+ ? cities
182
+ : described.length
183
+ ? described.slice(0, 1)
184
+ : links.length
185
+ ? []
186
+ : physical.slice(0, 1);
187
+ const labels = [...preferred, ...links].map((location) => location.summary);
188
+ return [...new Set(labels)].filter(Boolean).join(' · ') || undefined;
189
+ }
190
+ function geoLocation(point, label, osmType, osmId) {
191
+ const { lat, lng } = point;
192
+ return {
193
+ ...point,
194
+ label,
195
+ googleMapsUrl: mapsUrl(`${lat},${lng}`),
196
+ osmUrl: (osmType === 'node' || osmType === 'way' || osmType === 'relation') && osmId
197
+ ? `https://www.openstreetmap.org/${osmType}/${osmId}`
198
+ : `https://www.openstreetmap.org/?mlat=${lat}&mlon=${lng}#map=17/${lat}/${lng}`
199
+ };
200
+ }
201
+ /** A single overview map: prefer explicit coordinates; otherwise geocode the first address. */
202
+ export async function resolveGeoLocation(locations) {
203
+ const explicit = locations.find((location) => location.kind === 'geo' && location.coordinates) ??
204
+ locations.find((location) => location.coordinates);
205
+ if (explicit?.coordinates)
206
+ return geoLocation(explicit.coordinates, explicit.label);
207
+ const cells = locations.filter((location) => location.h3Cell);
208
+ if (cells.length) {
209
+ // H3 decoding is only loaded for events that actually use it, never for cards.
210
+ try {
211
+ const { isValidCell, cellToLatLng } = await import('h3-js');
212
+ for (const location of cells) {
213
+ if (!isValidCell(location.h3Cell))
214
+ continue;
215
+ const [lat, lng] = cellToLatLng(location.h3Cell);
216
+ return geoLocation({ lat, lng }, `${location.label} · Approximate area`);
217
+ }
218
+ }
219
+ catch {
220
+ // Fall back to a geocodable address when H3 decoding is unavailable.
221
+ }
222
+ }
223
+ const address = locations.find((location) => location.geocodeQuery);
224
+ if (!address?.geocodeQuery)
225
+ return null;
226
+ try {
227
+ const response = await fetch(`/api/geocoding?q=${encodeURIComponent(address.geocodeQuery)}`);
228
+ if (!response.ok)
229
+ return null;
230
+ const data = (await response.json());
231
+ if (typeof data?.lat !== 'number' ||
232
+ typeof data.lng !== 'number' ||
233
+ !Number.isFinite(data.lat) ||
234
+ !Number.isFinite(data.lng) ||
235
+ Math.abs(data.lat) > 90 ||
236
+ Math.abs(data.lng) > 180)
237
+ return null;
238
+ return geoLocation({ lat: data.lat, lng: data.lng }, address.geocodeQuery, data.osmType, data.osmId);
239
+ }
240
+ catch {
241
+ return null;
242
+ }
243
+ }
@@ -0,0 +1,3 @@
1
+ import type Plyr from 'plyr';
2
+ /** Called in the browser before constructing either video player. */
3
+ export declare function sketchyPlyrIcons(): Pick<Plyr.Options, 'iconPrefix' | 'iconUrl' | 'loadSprite'>;
@@ -0,0 +1,46 @@
1
+ import { Captions, CaptionsOff, Maximize, Minimize, Pause, Play, Settings, Volume2, VolumeX } from '@sketchyicons/svelte';
2
+ const prefix = 'atmo-sketchy-plyr';
3
+ const icons = {
4
+ play: Play,
5
+ pause: Pause,
6
+ volume: Volume2,
7
+ muted: VolumeX,
8
+ 'captions-on': Captions,
9
+ 'captions-off': CaptionsOff,
10
+ 'enter-fullscreen': Maximize,
11
+ 'exit-fullscreen': Minimize,
12
+ settings: Settings
13
+ };
14
+ /** Called in the browser before constructing either video player. */
15
+ export function sketchyPlyrIcons() {
16
+ // One local sprite serves every player, without fetching Plyr's stock CDN icons.
17
+ if (!document.getElementById(prefix)) {
18
+ const ns = 'http://www.w3.org/2000/svg';
19
+ const sprite = document.createElementNS(ns, 'svg');
20
+ sprite.id = prefix;
21
+ sprite.setAttribute('width', '0');
22
+ sprite.setAttribute('height', '0');
23
+ sprite.setAttribute('aria-hidden', 'true');
24
+ sprite.setAttribute('focusable', 'false');
25
+ sprite.style.position = 'absolute';
26
+ for (const [name, nodes] of Object.entries(icons)) {
27
+ const symbol = document.createElementNS(ns, 'symbol');
28
+ symbol.id = `${prefix}-${name}`;
29
+ symbol.setAttribute('viewBox', '0 0 24 24');
30
+ symbol.setAttribute('stroke', 'currentColor');
31
+ symbol.setAttribute('stroke-width', '1.75');
32
+ symbol.setAttribute('stroke-linecap', 'round');
33
+ symbol.setAttribute('stroke-linejoin', 'round');
34
+ for (const [, attributes] of nodes) {
35
+ const path = document.createElementNS(ns, 'path');
36
+ path.setAttribute('d', String(attributes.d));
37
+ path.setAttribute('fill', String(attributes.fill ?? 'none'));
38
+ symbol.appendChild(path);
39
+ }
40
+ sprite.appendChild(symbol);
41
+ }
42
+ document.body.appendChild(sprite);
43
+ }
44
+ // An empty iconUrl makes Plyr reference the local symbols by fragment only.
45
+ return { iconPrefix: prefix, iconUrl: '', loadSprite: false };
46
+ }
@@ -0,0 +1,18 @@
1
+ import { type GrainClient } from '../grain.js';
2
+ export { parseGrainLink, isLinkedActor } from '../grain.js';
3
+ export { parseStreamplaceLink } from '../streamplace.js';
4
+ export type { GrainReference, GrainAlbum, GrainPhoto, GrainGallery, GrainAlbumPage } from '../grain.js';
5
+ export type EventLink = {
6
+ uri: string;
7
+ name?: string;
8
+ };
9
+ export type LinkedRecordsClient = {
10
+ getGrainGallery: GrainClient['getGallery'];
11
+ listGrainAlbums: GrainClient['listAlbums'];
12
+ };
13
+ /** Recognize event stream URLs, not arbitrary bare handles in `uris`. */
14
+ export declare function eventStreamLink(uri: string): {
15
+ actor: string;
16
+ url: string;
17
+ } | null;
18
+ export declare function isLinkedRecord(link: EventLink): boolean;
@@ -0,0 +1,11 @@
1
+ import { parseGrainLink } from '../grain.js';
2
+ import { parseStreamplaceLink } from '../streamplace.js';
3
+ export { parseGrainLink, isLinkedActor } from '../grain.js';
4
+ export { parseStreamplaceLink } from '../streamplace.js';
5
+ /** Recognize event stream URLs, not arbitrary bare handles in `uris`. */
6
+ export function eventStreamLink(uri) {
7
+ return /^https?:\/\//i.test(uri) ? parseStreamplaceLink(uri) : null;
8
+ }
9
+ export function isLinkedRecord(link) {
10
+ return !!parseGrainLink(link.uri) || !!eventStreamLink(link.uri);
11
+ }
@@ -0,0 +1,3 @@
1
+ /** Profile routes shared by the RSVP app and its event components. */
2
+ export declare function getLocalProfileUrl(handleOrDid: string): string;
3
+ export declare function getProfileUrl(handleOrDid: string): string;
@@ -0,0 +1,11 @@
1
+ /** Profile routes shared by the RSVP app and its event components. */
2
+ export function getLocalProfileUrl(handleOrDid) {
3
+ return `/p/${encodeURIComponent(handleOrDid)}`;
4
+ }
5
+ export function getProfileUrl(handleOrDid) {
6
+ const lower = handleOrDid.toLowerCase();
7
+ if (lower.endsWith('.blacksky.team') || lower.endsWith('.blacksky.app')) {
8
+ return `https://blacksky.community/profile/${handleOrDid}`;
9
+ }
10
+ return `https://bsky.app/profile/${handleOrDid}`;
11
+ }
@@ -0,0 +1,340 @@
1
+ <script lang="ts">
2
+ import { ArrowDown, Check, Star } from '@sketchyicons/svelte';
3
+ import { Button, Icon, ToggleGroup, ToggleGroupItem } from '../../index.js';
4
+ import { onMount } from 'svelte';
5
+ import DaySchedule from './DaySchedule.svelte';
6
+ import ScheduleEventCell from './ScheduleEventCell.svelte';
7
+ import {
8
+ type ScheduleEvent,
9
+ type GridEvent,
10
+ getDayKey,
11
+ getDayLabel,
12
+ getDayShortLabel,
13
+ getRooms,
14
+ buildGrid,
15
+ isoToMinutes,
16
+ formatTime
17
+ } from './schedule-utils.js';
18
+ import type { EditorAdapter, EditorViewer } from '../editor/adapter.js';
19
+
20
+ type VodInfo = { playlistUrl: string; subtitlesUrl?: string };
21
+
22
+ let {
23
+ scheduleEvents,
24
+ tz,
25
+ eventActor,
26
+ adapter,
27
+ viewer,
28
+ rooms: explicitRooms,
29
+ rsvpStatuses: initialRsvpStatuses = {},
30
+ rsvpRkeys: initialRsvpRkeys = {},
31
+ eventVods = {},
32
+ loggedIn = false
33
+ }: {
34
+ scheduleEvents: ScheduleEvent[];
35
+ tz: string;
36
+ eventActor: string;
37
+ adapter: EditorAdapter;
38
+ viewer: EditorViewer;
39
+ /** Explicit room ordering from the conference event, if declared. */
40
+ rooms?: string[];
41
+ rsvpStatuses?: Record<string, string>;
42
+ rsvpRkeys?: Record<string, string>;
43
+ eventVods?: Record<string, VodInfo>;
44
+ loggedIn?: boolean;
45
+ } = $props();
46
+
47
+ // Writable so RSVPs made from the grid reflect immediately; resets when the props change.
48
+ let rsvpStatuses = $derived({ ...initialRsvpStatuses });
49
+ let rsvpRkeys = $derived({ ...initialRsvpRkeys });
50
+
51
+ let filterMode = $state('all');
52
+ let selectedDay = $state('all');
53
+ let activeRooms: Record<number, number> = $state({});
54
+
55
+ let now = $state(new Date());
56
+ onMount(() => {
57
+ const interval = setInterval(() => {
58
+ now = new Date();
59
+ }, 60_000);
60
+ return () => clearInterval(interval);
61
+ });
62
+
63
+ let nowIso = $derived(now.toISOString());
64
+ let nowKey = $derived(getDayKey(nowIso, tz));
65
+ let nowMinutes = $derived(isoToMinutes(nowIso, tz));
66
+
67
+ let dayGroups = $derived.by(() => {
68
+ // Temporary accumulator; the derived value returns a new array.
69
+ // eslint-disable-next-line svelte/prefer-svelte-reactivity
70
+ const groups = new Map<
71
+ string,
72
+ { key: string; label: string; shortLabel: string; events: ScheduleEvent[] }
73
+ >();
74
+ for (const event of scheduleEvents) {
75
+ const key = getDayKey(event.start, tz);
76
+ if (!groups.has(key)) {
77
+ groups.set(key, {
78
+ key,
79
+ label: getDayLabel(event.start, tz),
80
+ shortLabel: getDayShortLabel(event.start, tz),
81
+ events: []
82
+ });
83
+ }
84
+ groups.get(key)!.events.push(event);
85
+ }
86
+ return [...groups.values()];
87
+ });
88
+
89
+ let filteredDayGroups = $derived(
90
+ selectedDay === 'all' ? dayGroups : dayGroups.filter((d) => d.key === selectedDay)
91
+ );
92
+
93
+ let isDuringConference = $derived.by(() => {
94
+ if (scheduleEvents.length === 0) return false;
95
+ const first = scheduleEvents[0].start;
96
+ const last = scheduleEvents[scheduleEvents.length - 1];
97
+ const lastEnd = last.end || last.start;
98
+ return now >= new Date(first) && now <= new Date(lastEnd);
99
+ });
100
+
101
+ // The viewer's RSVP'd talks (going/interested) that haven't ended yet, shown
102
+ // a few at a time via "Show more".
103
+ let visibleCount = $state(3);
104
+ let upNextAll = $derived.by(() => {
105
+ if (!loggedIn) return [];
106
+ const nowMs = now.getTime();
107
+ return scheduleEvents
108
+ .filter((e) => {
109
+ const status = rsvpStatuses[e.uri];
110
+ if (status !== 'going' && status !== 'interested') return false;
111
+ const endMs = new Date(e.end ?? e.start).getTime();
112
+ return endMs >= nowMs;
113
+ })
114
+ .sort((a, b) => a.start.localeCompare(b.start));
115
+ });
116
+ let upNext = $derived(upNextAll.slice(0, visibleCount));
117
+
118
+ function whenLabel(ev: ScheduleEvent): { text: string; soon: boolean } {
119
+ const start = new Date(ev.start);
120
+ const end = new Date(ev.end ?? ev.start);
121
+ if (now >= start && now <= end) return { text: 'Now', soon: true };
122
+ const diffMin = Math.round((start.getTime() - now.getTime()) / 60_000);
123
+ if (diffMin >= 0 && diffMin < 60) return { text: `in ${diffMin} min`, soon: true };
124
+ const time = formatTime(ev.start, tz);
125
+ const sameDay = getDayKey(ev.start, tz) === nowKey;
126
+ return { text: sameDay ? time : `${getDayShortLabel(ev.start, tz)} ${time}`, soon: false };
127
+ }
128
+
129
+ function subtitle(ev: ScheduleEvent): string {
130
+ return [
131
+ ev.room && ev.room !== 'none' ? ev.room : null,
132
+ ev.speakers?.length ? ev.speakers.map((s) => s.name).join(', ') : null
133
+ ]
134
+ .filter(Boolean)
135
+ .join(' · ');
136
+ }
137
+
138
+ // Wrap a schedule event as a GridEvent so ScheduleEventCell can own its modal
139
+ // (the layout fields are unused; only the start/end-minute span matters).
140
+ function toGridEvent(ev: ScheduleEvent): GridEvent {
141
+ const durMin = ev.end
142
+ ? Math.max(
143
+ 0,
144
+ Math.round((new Date(ev.end).getTime() - new Date(ev.start).getTime()) / 60_000)
145
+ )
146
+ : 30;
147
+ return {
148
+ ...ev,
149
+ startMin: 0,
150
+ endMin: durMin,
151
+ startRow: 1,
152
+ spanRows: 1,
153
+ colStart: 1,
154
+ colSpan: 1,
155
+ zIndex: 0
156
+ };
157
+ }
158
+
159
+ let tzLabel = $derived.by(() => {
160
+ try {
161
+ const parts = new Intl.DateTimeFormat('en-US', {
162
+ timeZone: tz,
163
+ timeZoneName: 'short'
164
+ }).formatToParts(now);
165
+ return parts.find((p) => p.type === 'timeZoneName')?.value ?? tz;
166
+ } catch {
167
+ return tz;
168
+ }
169
+ });
170
+
171
+ function handleRsvpChange(uri: string, status: string | null, rkey?: string) {
172
+ if (status) {
173
+ rsvpStatuses = { ...rsvpStatuses, [uri]: status };
174
+ if (rkey) rsvpRkeys = { ...rsvpRkeys, [uri]: rkey };
175
+ } else {
176
+ const nextStatuses = { ...rsvpStatuses };
177
+ delete nextStatuses[uri];
178
+ rsvpStatuses = nextStatuses;
179
+ const nextKeys = { ...rsvpRkeys };
180
+ delete nextKeys[uri];
181
+ rsvpRkeys = nextKeys;
182
+ }
183
+ }
184
+
185
+ function scrollToNow() {
186
+ const els = document.querySelectorAll('[data-now-line]');
187
+ for (const el of els) {
188
+ if (el instanceof HTMLElement && el.offsetParent !== null) {
189
+ el.scrollIntoView({ behavior: 'smooth', block: 'center' });
190
+ return;
191
+ }
192
+ }
193
+ els[0]?.scrollIntoView({ behavior: 'smooth', block: 'center' });
194
+ }
195
+ </script>
196
+
197
+ {#if scheduleEvents.length > 0}
198
+ <div class="mt-8 mb-6 flex items-center justify-between">
199
+ <h2 class="text-2xl font-bold text-base-900 dark:text-base-50">Schedule</h2>
200
+ {#if isDuringConference}
201
+ <Button onclick={scrollToNow} size="sm">
202
+ <Icon img={ArrowDown} size={16} />
203
+ Jump to now
204
+ </Button>
205
+ {/if}
206
+ </div>
207
+
208
+ <p class="mb-4 text-sm text-muted">All times shown in {tzLabel}.</p>
209
+
210
+ {#if upNextAll.length > 0}
211
+ <div class="mb-8">
212
+ <h2 class="mb-3 text-xl font-bold text-base-900 dark:text-base-50">Up next</h2>
213
+ <div class="space-y-2">
214
+ {#each upNext as ev (ev.uri)}
215
+ {@const w = whenLabel(ev)}
216
+ <ScheduleEventCell
217
+ event={toGridEvent(ev)}
218
+ {tz}
219
+ {eventActor}
220
+ {adapter}
221
+ {viewer}
222
+ {rsvpStatuses}
223
+ {rsvpRkeys}
224
+ vodPlaylistUrl={eventVods[ev.uri]?.playlistUrl}
225
+ vodSubtitlesUrl={eventVods[ev.uri]?.subtitlesUrl}
226
+ onrsvpchange={handleRsvpChange}
227
+ >
228
+ {#snippet trigger({ open })}
229
+ <button
230
+ onclick={open}
231
+ class="flex w-full cursor-pointer items-center gap-3 rounded-xl border border-base-200 bg-base-100 px-4 py-3 text-left leading-tight transition-colors hover:border-base-300 dark:border-base-800 dark:bg-base-950/50 dark:hover:border-base-700"
232
+ >
233
+ <div class="min-w-0 flex-1">
234
+ <p class="truncate text-sm font-semibold text-base-900 dark:text-base-100">
235
+ {ev.title}
236
+ </p>
237
+ {#if subtitle(ev)}
238
+ <p class="mt-0.5 truncate text-xs text-muted">
239
+ {subtitle(ev)}
240
+ </p>
241
+ {/if}
242
+ </div>
243
+ <span
244
+ class="flex shrink-0 items-center gap-1.5 text-xs whitespace-nowrap {w.soon
245
+ ? 'font-semibold text-accent-600 dark:text-accent-400'
246
+ : 'text-muted'}"
247
+ >
248
+ {#if rsvpStatuses[ev.uri] === 'going'}
249
+ <Icon img={Check} size={14} class="text-action" />
250
+ {:else}
251
+ <Icon img={Star} size={14} class="text-action" />
252
+ {/if}
253
+ {w.text}
254
+ </span>
255
+ </button>
256
+ {/snippet}
257
+ </ScheduleEventCell>
258
+ {/each}
259
+ </div>
260
+ {#if upNextAll.length > upNext.length}
261
+ <button
262
+ type="button"
263
+ onclick={() => (visibleCount += 5)}
264
+ class="mt-2 text-sm font-medium text-accent-600 hover:underline dark:text-accent-400"
265
+ >
266
+ Show more
267
+ </button>
268
+ {/if}
269
+ </div>
270
+ {/if}
271
+
272
+ {#if dayGroups.length > 1 || loggedIn}
273
+ <div class="mb-6 space-y-3">
274
+ {#if dayGroups.length > 1}
275
+ <div class="flex items-center gap-3">
276
+ <span class="w-14 text-xs text-muted">Days</span>
277
+ <ToggleGroup
278
+ type="single"
279
+ bind:value={
280
+ () => selectedDay,
281
+ (v) => {
282
+ if (v) selectedDay = v;
283
+ }
284
+ }
285
+ class="w-fit"
286
+ >
287
+ <ToggleGroupItem value="all" size="sm">All</ToggleGroupItem>
288
+ {#each dayGroups as day (day.key)}
289
+ <ToggleGroupItem value={day.key} size="sm">{day.shortLabel}</ToggleGroupItem>
290
+ {/each}
291
+ </ToggleGroup>
292
+ </div>
293
+ {/if}
294
+ {#if loggedIn}
295
+ <div class="flex items-center gap-3">
296
+ <span class="w-14 text-xs text-muted">Events</span>
297
+ <ToggleGroup
298
+ type="single"
299
+ bind:value={
300
+ () => filterMode,
301
+ (v) => {
302
+ if (v) filterMode = v;
303
+ }
304
+ }
305
+ class="w-fit"
306
+ >
307
+ <ToggleGroupItem value="all" size="sm">All</ToggleGroupItem>
308
+ <ToggleGroupItem value="attending" size="sm">Attending</ToggleGroupItem>
309
+ </ToggleGroup>
310
+ </div>
311
+ {/if}
312
+ </div>
313
+ {/if}
314
+
315
+ {#each filteredDayGroups as day, dayIndex (day.key)}
316
+ {@const dayRooms = getRooms(day.events, explicitRooms)}
317
+ {@const grid = buildGrid(day.events, dayRooms, tz)}
318
+
319
+ <section class="isolate mb-12">
320
+ <h3 class="mb-4 text-lg font-bold text-base-900 dark:text-base-50">{day.label}</h3>
321
+ <DaySchedule
322
+ {grid}
323
+ rooms={dayRooms}
324
+ dayEvents={day.events}
325
+ {tz}
326
+ {eventActor}
327
+ {adapter}
328
+ {viewer}
329
+ bind:activeRoom={() => activeRooms[dayIndex] ?? 0, (v) => (activeRooms[dayIndex] = v)}
330
+ {nowKey}
331
+ {nowMinutes}
332
+ {rsvpStatuses}
333
+ {rsvpRkeys}
334
+ {eventVods}
335
+ dimUnattended={filterMode === 'attending'}
336
+ onrsvpchange={handleRsvpChange}
337
+ />
338
+ </section>
339
+ {/each}
340
+ {/if}