@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,30 @@
1
+ <script lang="ts">
2
+ import { accentColors, baseColors } from '../index.js';
3
+ let { accentColor = 'pink', baseColor = 'zinc' }: { accentColor?: string; baseColor?: string } =
4
+ $props();
5
+ const bases: readonly string[] = [...baseColors, 'sand', 'sage'];
6
+ const accents: readonly string[] = accentColors;
7
+ const allColors = [...bases, ...accents];
8
+ let accent = $derived(accents.includes(accentColor) ? accentColor : 'pink');
9
+ let base = $derived(bases.includes(baseColor) ? baseColor : 'zinc');
10
+ // Values are chosen from the kit's palette names, never interpolated user HTML.
11
+ let script = $derived(
12
+ `<script>(function(){var e=document.documentElement,r=${JSON.stringify(allColors)};r.forEach(function(c){e.classList.remove(c)});e.classList.add(${JSON.stringify(accent)},${JSON.stringify(base)});})()<` +
13
+ '/script>'
14
+ );
15
+ $effect(() => {
16
+ const element = document.documentElement;
17
+ element.classList.remove(...allColors);
18
+ element.classList.add(accent, base);
19
+ return () => {
20
+ element.classList.remove(...allColors);
21
+ element.classList.add('pink', 'zinc');
22
+ };
23
+ });
24
+ </script>
25
+
26
+ <svelte:head>
27
+ <!-- Script values come only from the allowlisted Kit palettes above. -->
28
+ <!-- eslint-disable-next-line svelte/no-at-html-tags -->
29
+ {@html script}
30
+ </svelte:head>
@@ -0,0 +1,7 @@
1
+ type $$ComponentProps = {
2
+ accentColor?: string;
3
+ baseColor?: string;
4
+ };
5
+ declare const ThemeApply: import("svelte").Component<$$ComponentProps, {}, "">;
6
+ type ThemeApply = ReturnType<typeof ThemeApply>;
7
+ export default ThemeApply;
@@ -0,0 +1,33 @@
1
+ <script lang="ts">
2
+ import type { EventTheme } from './theme.js';
3
+ import Blobs from './themes/Blobs.svelte';
4
+ import Stars from './themes/Stars.svelte';
5
+ import Matrix from './themes/Matrix.svelte';
6
+ import Fireflies from './themes/Fireflies.svelte';
7
+ import Butterflies from './themes/Butterflies.svelte';
8
+ import Kaleidoscope from './themes/Kaleidoscope.svelte';
9
+
10
+ let {
11
+ theme
12
+ }: {
13
+ theme: EventTheme;
14
+ } = $props();
15
+
16
+ let key = $derived(`${theme.name}-${theme.accentColor}-${theme.baseColor}`);
17
+ </script>
18
+
19
+ {#key key}
20
+ {#if theme.name === 'blobs'}
21
+ <Blobs />
22
+ {:else if theme.name === 'warp'}
23
+ <Stars />
24
+ {:else if theme.name === 'matrix'}
25
+ <Matrix />
26
+ {:else if theme.name === 'fireflies'}
27
+ <Fireflies />
28
+ {:else if theme.name === 'butterflies'}
29
+ <Butterflies />
30
+ {:else if theme.name === 'kaleidoscope'}
31
+ <Kaleidoscope />
32
+ {/if}
33
+ {/key}
@@ -0,0 +1,7 @@
1
+ import type { EventTheme } from './theme.js';
2
+ type $$ComponentProps = {
3
+ theme: EventTheme;
4
+ };
5
+ declare const ThemeBackground: import("svelte").Component<$$ComponentProps, {}, "">;
6
+ type ThemeBackground = ReturnType<typeof ThemeBackground>;
7
+ export default ThemeBackground;
@@ -0,0 +1,104 @@
1
+ <script lang="ts">
2
+ import { defaultTheme, themeBackgrounds, type EventTheme } from './theme.js';
3
+
4
+ let {
5
+ theme = $bindable<EventTheme>({ ...defaultTheme })
6
+ }: {
7
+ theme: EventTheme;
8
+ } = $props();
9
+
10
+ const bgKeys = Object.keys(themeBackgrounds);
11
+
12
+ const accentColors = [
13
+ { label: 'red', cls: 'bg-red-500' },
14
+ { label: 'orange', cls: 'bg-orange-500' },
15
+ { label: 'amber', cls: 'bg-amber-500' },
16
+ { label: 'yellow', cls: 'bg-yellow-500' },
17
+ { label: 'lime', cls: 'bg-lime-500' },
18
+ { label: 'green', cls: 'bg-green-500' },
19
+ { label: 'emerald', cls: 'bg-emerald-500' },
20
+ { label: 'teal', cls: 'bg-teal-500' },
21
+ { label: 'cyan', cls: 'bg-cyan-500' },
22
+ { label: 'sky', cls: 'bg-sky-500' },
23
+ { label: 'blue', cls: 'bg-blue-500' },
24
+ { label: 'indigo', cls: 'bg-indigo-500' },
25
+ { label: 'violet', cls: 'bg-violet-500' },
26
+ { label: 'purple', cls: 'bg-purple-500' },
27
+ { label: 'fuchsia', cls: 'bg-fuchsia-500' },
28
+ { label: 'pink', cls: 'bg-pink-500' },
29
+ { label: 'rose', cls: 'bg-rose-500' }
30
+ ];
31
+
32
+ const baseColors = [
33
+ { label: 'gray', cls: 'bg-gray-500' },
34
+ { label: 'stone', cls: 'bg-stone-500' },
35
+ { label: 'zinc', cls: 'bg-zinc-500' },
36
+ { label: 'neutral', cls: 'bg-neutral-500' },
37
+ { label: 'slate', cls: 'bg-slate-500' },
38
+ { label: 'olive', cls: 'bg-olive-500' },
39
+ { label: 'mauve', cls: 'bg-mauve-500' },
40
+ { label: 'mist', cls: 'bg-mist-500' },
41
+ { label: 'taupe', cls: 'bg-taupe-500' }
42
+ ];
43
+ </script>
44
+
45
+ <div class="flex flex-col gap-6">
46
+ <!-- Theme background -->
47
+ <div>
48
+ <p class="mb-2 text-xs font-medium text-muted">Background style</p>
49
+ <div class="flex flex-wrap gap-2">
50
+ {#each bgKeys as key (key)}
51
+ <button
52
+ type="button"
53
+ class="relative flex aspect-video w-24 cursor-pointer items-center justify-center overflow-hidden rounded-xl border-2 transition-colors
54
+ {theme.name === key
55
+ ? 'border-accent-500'
56
+ : 'border-base-200 hover:border-accent-400 dark:border-base-700 dark:hover:border-accent-500'}"
57
+ onclick={() => (theme = { ...theme, name: key })}
58
+ >
59
+ <span class="text-xs font-medium text-base-600 dark:text-base-400">
60
+ {themeBackgrounds[key]}
61
+ </span>
62
+ </button>
63
+ {/each}
64
+ </div>
65
+ </div>
66
+
67
+ <!-- Accent color -->
68
+ <div>
69
+ <p class="mb-2 text-xs font-medium text-muted">Accent color</p>
70
+ <div class="flex flex-wrap gap-2">
71
+ {#each accentColors as color (color.label)}
72
+ <button
73
+ type="button"
74
+ aria-label="Accent color {color.label}"
75
+ aria-pressed={theme.accentColor === color.label}
76
+ class="size-7 cursor-pointer rounded-full border-2 transition-all {color.cls}
77
+ {theme.accentColor === color.label
78
+ ? 'border-white ring-2 ring-control-line'
79
+ : 'border-transparent hover:border-control-line'}"
80
+ onclick={() => (theme = { ...theme, accentColor: color.label })}
81
+ ></button>
82
+ {/each}
83
+ </div>
84
+ </div>
85
+
86
+ <!-- Base color -->
87
+ <div>
88
+ <p class="mb-2 text-xs font-medium text-muted">Base color</p>
89
+ <div class="flex flex-wrap gap-2">
90
+ {#each baseColors as color (color.label)}
91
+ <button
92
+ type="button"
93
+ aria-label="Base color {color.label}"
94
+ aria-pressed={theme.baseColor === color.label}
95
+ class="size-7 cursor-pointer rounded-full border-2 transition-all {color.cls}
96
+ {theme.baseColor === color.label
97
+ ? 'border-white ring-2 ring-control-line'
98
+ : 'border-transparent hover:border-control-line'}"
99
+ onclick={() => (theme = { ...theme, baseColor: color.label })}
100
+ ></button>
101
+ {/each}
102
+ </div>
103
+ </div>
104
+ </div>
@@ -0,0 +1,7 @@
1
+ import { type EventTheme } from './theme.js';
2
+ type $$ComponentProps = {
3
+ theme: EventTheme;
4
+ };
5
+ declare const ThemePicker: import("svelte").Component<$$ComponentProps, {}, "theme">;
6
+ type ThemePicker = ReturnType<typeof ThemePicker>;
7
+ export default ThemePicker;
@@ -0,0 +1,71 @@
1
+ <script lang="ts">
2
+ // @ts-nocheck
3
+ import { designs, resolveAccentColor } from './thumbnails/designs';
4
+ import { tick } from 'svelte';
5
+
6
+ let {
7
+ name = '',
8
+ dateStr = '',
9
+ accent = '',
10
+ seed = 1,
11
+ selected = $bindable<string | null>(null),
12
+ onselect
13
+ }: {
14
+ name?: string;
15
+ dateStr?: string;
16
+ accent?: string;
17
+ seed?: number;
18
+ selected?: string | null;
19
+ onselect?: () => void;
20
+ } = $props();
21
+
22
+ const presetKeys = Object.keys(designs);
23
+ const previewSize = 200;
24
+
25
+ let containerEl: HTMLDivElement | undefined = $state(undefined);
26
+
27
+ function renderAll() {
28
+ if (!containerEl) return;
29
+ const color = resolveAccentColor(accent);
30
+ const canvases = containerEl.querySelectorAll<HTMLCanvasElement>('canvas');
31
+ canvases.forEach((canvas) => {
32
+ const key = canvas.dataset.key!;
33
+ const ctx = canvas.getContext('2d');
34
+ if (!ctx) return;
35
+ canvas.width = previewSize;
36
+ canvas.height = previewSize;
37
+ designs[key](ctx, previewSize, previewSize, name || 'Event', dateStr, seed, color);
38
+ });
39
+ }
40
+
41
+ $effect(() => {
42
+ void name;
43
+ void dateStr;
44
+ void accent;
45
+ void seed;
46
+ void containerEl;
47
+ tick().then(renderAll);
48
+ });
49
+ </script>
50
+
51
+ <div class="flex flex-col gap-3">
52
+ <p class="text-xs font-medium text-muted">Preset thumbnails</p>
53
+ <div class="grid grid-cols-3 gap-2" bind:this={containerEl}>
54
+ {#each presetKeys as key (key)}
55
+ <button
56
+ type="button"
57
+ aria-label="Use {key} preset thumbnail"
58
+ class="aspect-square cursor-pointer overflow-hidden rounded-xl border-2 transition-colors
59
+ {selected === key
60
+ ? 'border-accent-500'
61
+ : 'border-base-200 hover:border-accent-400 dark:border-base-700 dark:hover:border-accent-500'}"
62
+ onclick={() => {
63
+ selected = key;
64
+ onselect?.();
65
+ }}
66
+ >
67
+ <canvas data-key={key} class="h-full w-full"></canvas>
68
+ </button>
69
+ {/each}
70
+ </div>
71
+ </div>
@@ -0,0 +1,11 @@
1
+ type $$ComponentProps = {
2
+ name?: string;
3
+ dateStr?: string;
4
+ accent?: string;
5
+ seed?: number;
6
+ selected?: string | null;
7
+ onselect?: () => void;
8
+ };
9
+ declare const ThumbnailPresets: import("svelte").Component<$$ComponentProps, {}, "selected">;
10
+ type ThumbnailPresets = ReturnType<typeof ThumbnailPresets>;
11
+ export default ThumbnailPresets;
@@ -0,0 +1,117 @@
1
+ <script lang="ts">
2
+ import { TimeField } from 'bits-ui';
3
+ import { Time } from '@internationalized/date';
4
+ import { untrack } from 'svelte';
5
+ import { Icon, IconButton, Popover } from '../index.js';
6
+ import { Clock } from '@sketchyicons/svelte';
7
+
8
+ let {
9
+ value = $bindable(''),
10
+ required = false,
11
+ label = 'Time',
12
+ locale = 'en',
13
+ referenceTime = ''
14
+ }: {
15
+ value: string;
16
+ required?: boolean;
17
+ label?: string;
18
+ locale?: string;
19
+ referenceTime?: string;
20
+ } = $props();
21
+ let isOpen = $state(false);
22
+ let listEl = $state<HTMLDivElement>();
23
+ let internalValue = $state<Time>();
24
+ $effect(() => {
25
+ const [hour, minute] = value.split(':').map(Number);
26
+ untrack(() => {
27
+ if (!value || !Number.isFinite(hour) || !Number.isFinite(minute)) internalValue = undefined;
28
+ else if (internalValue?.hour !== hour || internalValue?.minute !== minute)
29
+ internalValue = new Time(hour, minute);
30
+ });
31
+ });
32
+ function handleValueChange(next: Time | undefined) {
33
+ if (next)
34
+ value = `${String(next.hour).padStart(2, '0')}:${String(next.minute).padStart(2, '0')}`;
35
+ }
36
+ let slots = $derived(
37
+ Array.from({ length: 48 }, (_, i) => {
38
+ const h = Math.floor(i / 2),
39
+ m = i % 2 === 0 ? 0 : 30;
40
+ return {
41
+ key: `${String(h).padStart(2, '0')}:${String(m).padStart(2, '0')}`,
42
+ label: new Date(2000, 0, 1, h, m).toLocaleTimeString(locale, {
43
+ hour: 'numeric',
44
+ minute: '2-digit'
45
+ })
46
+ };
47
+ })
48
+ );
49
+ function durationLabel(key: string) {
50
+ if (!referenceTime) return '';
51
+ const [rh, rm] = referenceTime.split(':').map(Number),
52
+ [h, m] = key.split(':').map(Number);
53
+ const diff = h * 60 + m - rh * 60 - rm;
54
+ if (diff <= 0) return '';
55
+ const hours = Math.floor(diff / 60),
56
+ minutes = diff % 60;
57
+ return hours ? (minutes ? `${hours}h ${minutes}m` : `${hours}h`) : `${minutes}m`;
58
+ }
59
+ </script>
60
+
61
+ <div
62
+ class="flex h-11 items-center gap-1 rounded-ui border border-line bg-recess px-2 text-sm text-ink shadow-(--ui-recess-shadow) focus-within:outline-2 focus-within:outline-offset-2 focus-within:outline-focus hover:border-control-hover"
63
+ >
64
+ <TimeField.Root
65
+ bind:value={internalValue}
66
+ onValueChange={handleValueChange}
67
+ granularity="minute"
68
+ {locale}
69
+ {required}
70
+ >
71
+ <TimeField.Label class="sr-only">{label}</TimeField.Label>
72
+ <TimeField.Input class="whitespace-nowrap">
73
+ {#snippet children({ segments })}{#each segments as segment, i (segment.part + i)}
74
+ {#if segment.part === 'literal'}<span class="text-muted">{segment.value}</span
75
+ >{:else}<TimeField.Segment
76
+ part={segment.part}
77
+ class="rounded px-0.5 hover:bg-recess focus:bg-accent-soft focus:outline-none"
78
+ >{segment.value}</TimeField.Segment
79
+ >{/if}
80
+ {/each}{/snippet}
81
+ </TimeField.Input>
82
+ </TimeField.Root>
83
+ <Popover
84
+ bind:open={isOpen}
85
+ class="max-h-60 overflow-y-auto p-2"
86
+ aria-label={`Choose ${label.toLowerCase()}`}
87
+ onOpenAutoFocus={(event) => {
88
+ const selected = listEl?.querySelector<HTMLButtonElement>('[data-selected]');
89
+ if (selected) {
90
+ event.preventDefault();
91
+ selected.focus();
92
+ selected.scrollIntoView({ block: 'center' });
93
+ }
94
+ }}
95
+ >
96
+ {#snippet trigger({ props })}<IconButton
97
+ {...props}
98
+ label={`Choose ${label.toLowerCase()}`}
99
+ class="size-7"><Icon img={Clock} size={15} /></IconButton
100
+ >{/snippet}
101
+ <div bind:this={listEl}>
102
+ {#each slots as slot (slot.key)}<button
103
+ type="button"
104
+ class="flex w-full items-center justify-between gap-4 rounded-ui px-3 py-1.5 text-left text-sm text-ink hover:bg-recess aria-pressed:bg-accent-soft aria-pressed:text-action"
105
+ aria-pressed={value === slot.key}
106
+ data-selected={value === slot.key ? '' : undefined}
107
+ onclick={() => {
108
+ value = slot.key;
109
+ isOpen = false;
110
+ }}
111
+ >{slot.label}{#if durationLabel(slot.key)}<span class="text-xs text-muted"
112
+ >{durationLabel(slot.key)}</span
113
+ >{/if}</button
114
+ >{/each}
115
+ </div>
116
+ </Popover>
117
+ </div>
@@ -0,0 +1,10 @@
1
+ type $$ComponentProps = {
2
+ value: string;
3
+ required?: boolean;
4
+ label?: string;
5
+ locale?: string;
6
+ referenceTime?: string;
7
+ };
8
+ declare const TimePicker: import("svelte").Component<$$ComponentProps, {}, "value">;
9
+ type TimePicker = ReturnType<typeof TimePicker>;
10
+ export default TimePicker;
@@ -0,0 +1,67 @@
1
+ <script lang="ts">
2
+ import { Button, Icon, Input, Popover } from '../index.js';
3
+ import { Globe } from '@sketchyicons/svelte';
4
+ let { value = $bindable('') }: { value: string } = $props();
5
+ let isOpen = $state(false);
6
+ let search = $state('');
7
+ const allTimezones = ['UTC', ...Intl.supportedValuesOf('timeZone')];
8
+ // Memoized formatting results are not reactive state.
9
+ // eslint-disable-next-line svelte/prefer-svelte-reactivity
10
+ const offsets = new Map<string, string>();
11
+ function getOffset(tz: string) {
12
+ if (offsets.has(tz)) return offsets.get(tz)!;
13
+ try {
14
+ const result =
15
+ new Intl.DateTimeFormat('en', { timeZone: tz, timeZoneName: 'shortOffset' })
16
+ .formatToParts(new Date())
17
+ .find((part) => part.type === 'timeZoneName')?.value ?? '';
18
+ offsets.set(tz, result);
19
+ return result;
20
+ } catch {
21
+ return '';
22
+ }
23
+ }
24
+ function getCityName(tz: string) {
25
+ return (tz.split('/').at(-1) || tz).replace(/_/g, ' ');
26
+ }
27
+ let filtered = $derived(
28
+ allTimezones.filter(
29
+ (tz) =>
30
+ !search.trim() ||
31
+ `${tz} ${getCityName(tz)} ${getOffset(tz)}`.toLowerCase().includes(search.toLowerCase())
32
+ )
33
+ );
34
+ </script>
35
+
36
+ <Popover bind:open={isOpen} class="w-64 p-2" aria-label="Choose timezone">
37
+ {#snippet trigger({ props })}<Button
38
+ {...props}
39
+ variant="secondary"
40
+ class="min-h-11 w-full justify-start gap-2.5 border-line bg-recess px-3 text-xs shadow-(--ui-recess-shadow) hover:border-control-hover"
41
+ aria-label={`Timezone: ${value}`}
42
+ >
43
+ <Icon img={Globe} size={17} class="text-muted" />
44
+ <span class="min-w-0 flex-1 truncate text-left">{getCityName(value)}</span>
45
+ <span class="shrink-0 text-muted">{getOffset(value)}</span>
46
+ </Button>{/snippet}
47
+ <Input
48
+ type="search"
49
+ bind:value={search}
50
+ aria-label="Search timezone"
51
+ placeholder="Search timezone…"
52
+ class="mb-2"
53
+ />
54
+ <div class="max-h-60 overflow-y-auto">
55
+ {#each filtered as tz (tz)}<button
56
+ type="button"
57
+ class="flex w-full items-center justify-between gap-3 rounded-ui px-3 py-2 text-left text-xs text-ink hover:bg-recess aria-pressed:bg-accent-soft aria-pressed:text-action"
58
+ aria-pressed={value === tz}
59
+ onclick={() => {
60
+ value = tz;
61
+ isOpen = false;
62
+ search = '';
63
+ }}><span>{getCityName(tz)}</span><span class="text-muted">{getOffset(tz)}</span></button
64
+ >{/each}
65
+ {#if !filtered.length}<p class="p-3 text-xs text-muted">No results</p>{/if}
66
+ </div>
67
+ </Popover>
@@ -0,0 +1,6 @@
1
+ type $$ComponentProps = {
2
+ value: string;
3
+ };
4
+ declare const TimezonePicker: import("svelte").Component<$$ComponentProps, {}, "value">;
5
+ type TimezonePicker = ReturnType<typeof TimezonePicker>;
6
+ export default TimezonePicker;
@@ -0,0 +1,154 @@
1
+ <script lang="ts" module>
2
+ export type VodPlayerApi = {
3
+ seek: (time: number) => void;
4
+ };
5
+ </script>
6
+
7
+ <script lang="ts">
8
+ import { onMount } from 'svelte';
9
+ import 'plyr/dist/plyr.css';
10
+ import type HlsType from 'hls.js';
11
+ import type PlyrType from 'plyr';
12
+ import { sketchyPlyrIcons } from './event-view/plyr-icons.js';
13
+
14
+ let {
15
+ playlistUrl,
16
+ title,
17
+ subtitlesUrl,
18
+ currentTime = $bindable(0),
19
+ api = $bindable(undefined)
20
+ }: {
21
+ playlistUrl: string;
22
+ title: string;
23
+ subtitlesUrl?: string;
24
+ currentTime?: number;
25
+ api?: VodPlayerApi;
26
+ } = $props();
27
+
28
+ let videoEl: HTMLVideoElement | undefined = $state();
29
+ let error = $state(false);
30
+
31
+ let hls: HlsType | null = null;
32
+ let plyr: PlyrType | null = null;
33
+
34
+ api = {
35
+ seek(time: number) {
36
+ if (plyr) {
37
+ plyr.currentTime = time;
38
+ plyr.play();
39
+ }
40
+ }
41
+ };
42
+
43
+ onMount(() => {
44
+ init();
45
+ return () => {
46
+ hls?.destroy();
47
+ plyr?.destroy();
48
+ };
49
+ });
50
+
51
+ async function init() {
52
+ if (!videoEl) return;
53
+
54
+ try {
55
+ const [{ default: Plyr }, { default: Hls }] = await Promise.all([
56
+ import('plyr'),
57
+ import('hls.js')
58
+ ]);
59
+
60
+ if (Hls.isSupported()) {
61
+ hls = new Hls({ autoStartLoad: false });
62
+ hls.loadSource(playlistUrl);
63
+ hls.attachMedia(videoEl);
64
+ hls.on(Hls.Events.ERROR, (_event, data) => {
65
+ if (data.fatal) {
66
+ if (data.type === Hls.ErrorTypes.NETWORK_ERROR) {
67
+ hls?.startLoad();
68
+ } else if (data.type === Hls.ErrorTypes.MEDIA_ERROR) {
69
+ hls?.recoverMediaError();
70
+ } else {
71
+ error = true;
72
+ }
73
+ }
74
+ });
75
+ } else if (videoEl.canPlayType('application/vnd.apple.mpegurl')) {
76
+ videoEl.src = playlistUrl;
77
+ } else {
78
+ error = true;
79
+ return;
80
+ }
81
+
82
+ plyr = new Plyr(videoEl, {
83
+ ...sketchyPlyrIcons(),
84
+ controls: [
85
+ 'play-large',
86
+ 'play',
87
+ 'progress',
88
+ 'current-time',
89
+ 'mute',
90
+ 'volume',
91
+ 'captions',
92
+ 'settings',
93
+ 'fullscreen'
94
+ ],
95
+ settings: ['captions', 'speed'],
96
+ speed: { selected: 1, options: [0.5, 0.75, 1, 1.25, 1.5, 1.75, 2] },
97
+ captions: { active: !!subtitlesUrl, language: 'en', update: true },
98
+ ratio: '16:9'
99
+ });
100
+
101
+ plyr.on('play', () => {
102
+ hls?.startLoad();
103
+ });
104
+
105
+ plyr.on('timeupdate', () => {
106
+ currentTime = plyr?.currentTime ?? 0;
107
+ });
108
+
109
+ // Add track after Plyr init and force captions on
110
+ if (subtitlesUrl) {
111
+ const media = videoEl;
112
+ // Remove any existing tracks first
113
+ media.querySelectorAll('track').forEach((t) => t.remove());
114
+ // Add fresh track
115
+ const track = document.createElement('track');
116
+ track.kind = 'captions';
117
+ track.label = 'English';
118
+ track.srclang = 'en';
119
+ track.src = subtitlesUrl;
120
+ track.default = true;
121
+ media.appendChild(track);
122
+ // Wait for track to load, then activate
123
+ track.addEventListener('load', () => {
124
+ plyr!.toggleCaptions(true);
125
+ });
126
+ // Also try toggling after a short delay as fallback
127
+ setTimeout(() => plyr?.toggleCaptions(true), 500);
128
+ }
129
+ } catch {
130
+ error = true;
131
+ }
132
+ }
133
+ </script>
134
+
135
+ {#if error}
136
+ <div
137
+ class="flex aspect-video w-full items-center justify-center rounded-xl border border-base-200 bg-base-100 dark:border-base-800 dark:bg-base-900"
138
+ >
139
+ <p class="text-sm text-muted">Failed to load video</p>
140
+ </div>
141
+ {:else}
142
+ <div
143
+ class="aspect-video w-full max-w-full overflow-hidden rounded-xl border border-base-300 dark:border-base-400/40"
144
+ >
145
+ <video bind:this={videoEl} class="h-full w-full" aria-label={title} crossorigin="anonymous"
146
+ ></video>
147
+ </div>
148
+ {/if}
149
+
150
+ <style>
151
+ * {
152
+ --plyr-color-main: var(--color-accent-500);
153
+ }
154
+ </style>
@@ -0,0 +1,14 @@
1
+ export type VodPlayerApi = {
2
+ seek: (time: number) => void;
3
+ };
4
+ import 'plyr/dist/plyr.css';
5
+ type $$ComponentProps = {
6
+ playlistUrl: string;
7
+ title: string;
8
+ subtitlesUrl?: string;
9
+ currentTime?: number;
10
+ api?: VodPlayerApi;
11
+ };
12
+ declare const VodPlayer: import("svelte").Component<$$ComponentProps, {}, "currentTime" | "api">;
13
+ type VodPlayer = ReturnType<typeof VodPlayer>;
14
+ export default VodPlayer;