@atmo-dev/kit 0.4.0 → 0.6.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +119 -0
- package/dist/at-tags.d.ts +18 -0
- package/dist/at-tags.js +18 -0
- package/dist/components/AtTags.svelte +17 -0
- package/dist/components/AtTags.svelte.d.ts +7 -0
- package/dist/events/DatePicker.svelte +222 -0
- package/dist/events/DatePicker.svelte.d.ts +12 -0
- package/dist/events/DateTimePicker.svelte +112 -0
- package/dist/events/DateTimePicker.svelte.d.ts +10 -0
- package/dist/events/EventAttendees.svelte +199 -0
- package/dist/events/EventAttendees.svelte.d.ts +19 -0
- package/dist/events/EventCard.svelte +148 -0
- package/dist/events/EventCard.svelte.d.ts +8 -0
- package/dist/events/EventComments.svelte +98 -0
- package/dist/events/EventComments.svelte.d.ts +8 -0
- package/dist/events/EventEditor.svelte +540 -0
- package/dist/events/EventEditor.svelte.d.ts +18 -0
- package/dist/events/EventRsvp.svelte +178 -0
- package/dist/events/EventRsvp.svelte.d.ts +16 -0
- package/dist/events/EventView.svelte +488 -0
- package/dist/events/EventView.svelte.d.ts +23 -0
- package/dist/events/ImageDropper.svelte +66 -0
- package/dist/events/ImageDropper.svelte.d.ts +7 -0
- package/dist/events/Map.svelte +36 -0
- package/dist/events/Map.svelte.d.ts +8 -0
- package/dist/events/PostToBlueskyModal.svelte +246 -0
- package/dist/events/PostToBlueskyModal.svelte.d.ts +22 -0
- package/dist/events/ShareModal.svelte +174 -0
- package/dist/events/ShareModal.svelte.d.ts +23 -0
- package/dist/events/StarterPackButton.svelte +72 -0
- package/dist/events/StarterPackButton.svelte.d.ts +10 -0
- package/dist/events/ThemeApply.svelte +30 -0
- package/dist/events/ThemeApply.svelte.d.ts +7 -0
- package/dist/events/ThemeBackground.svelte +33 -0
- package/dist/events/ThemeBackground.svelte.d.ts +7 -0
- package/dist/events/ThemePicker.svelte +104 -0
- package/dist/events/ThemePicker.svelte.d.ts +7 -0
- package/dist/events/ThumbnailPresets.svelte +71 -0
- package/dist/events/ThumbnailPresets.svelte.d.ts +11 -0
- package/dist/events/TimePicker.svelte +117 -0
- package/dist/events/TimePicker.svelte.d.ts +10 -0
- package/dist/events/TimezonePicker.svelte +67 -0
- package/dist/events/TimezonePicker.svelte.d.ts +6 -0
- package/dist/events/VodPlayer.svelte +154 -0
- package/dist/events/VodPlayer.svelte.d.ts +14 -0
- package/dist/events/VodTranscript.svelte +75 -0
- package/dist/events/VodTranscript.svelte.d.ts +8 -0
- package/dist/events/atproto-helpers.d.ts +21 -0
- package/dist/events/atproto-helpers.js +61 -0
- package/dist/events/cal/helper.d.ts +1 -0
- package/dist/events/cal/helper.js +20 -0
- package/dist/events/cal/ical.d.ts +22 -0
- package/dist/events/cal/ical.js +188 -0
- package/dist/events/cal/sanitize.d.ts +3 -0
- package/dist/events/cal/sanitize.js +25 -0
- package/dist/events/conference.d.ts +65 -0
- package/dist/events/conference.js +61 -0
- package/dist/events/contrail.d.ts +49 -0
- package/dist/events/contrail.js +12 -0
- package/dist/events/date-format.d.ts +22 -0
- package/dist/events/date-format.js +43 -0
- package/dist/events/editor/LinkedRecordsPicker.svelte +104 -0
- package/dist/events/editor/LinkedRecordsPicker.svelte.d.ts +12 -0
- package/dist/events/editor/LinksSection.svelte +154 -0
- package/dist/events/editor/LinksSection.svelte.d.ts +13 -0
- package/dist/events/editor/LocationSection.svelte +163 -0
- package/dist/events/editor/LocationSection.svelte.d.ts +8 -0
- package/dist/events/editor/RecurringModal.svelte +313 -0
- package/dist/events/editor/RecurringModal.svelte.d.ts +30 -0
- package/dist/events/editor/ThemeSection.svelte +24 -0
- package/dist/events/editor/ThemeSection.svelte.d.ts +7 -0
- package/dist/events/editor/ThumbnailSection.svelte +181 -0
- package/dist/events/editor/ThumbnailSection.svelte.d.ts +13 -0
- package/dist/events/editor/adapter.d.ts +73 -0
- package/dist/events/editor/adapter.js +9 -0
- package/dist/events/editor/location.d.ts +16 -0
- package/dist/events/editor/location.js +50 -0
- package/dist/events/editor/save.d.ts +43 -0
- package/dist/events/editor/save.js +168 -0
- package/dist/events/editor/types.d.ts +39 -0
- package/dist/events/editor/types.js +9 -0
- package/dist/events/event-media.d.ts +4 -0
- package/dist/events/event-media.js +4 -0
- package/dist/events/event-types.d.ts +79 -0
- package/dist/events/event-types.js +11 -0
- package/dist/events/event-view/AddToCalendarButton.svelte +31 -0
- package/dist/events/event-view/AddToCalendarButton.svelte.d.ts +9 -0
- package/dist/events/event-view/EventBadges.svelte +20 -0
- package/dist/events/event-view/EventBadges.svelte.d.ts +7 -0
- package/dist/events/event-view/EventDateBlock.svelte +43 -0
- package/dist/events/event-view/EventDateBlock.svelte.d.ts +7 -0
- package/dist/events/event-view/EventHostedBy.svelte +61 -0
- package/dist/events/event-view/EventHostedBy.svelte.d.ts +16 -0
- package/dist/events/event-view/EventLinksList.svelte +24 -0
- package/dist/events/event-view/EventLinksList.svelte.d.ts +9 -0
- package/dist/events/event-view/EventLocationBlock.svelte +62 -0
- package/dist/events/event-view/EventLocationBlock.svelte.d.ts +7 -0
- package/dist/events/event-view/EventLocationMap.svelte +66 -0
- package/dist/events/event-view/EventLocationMap.svelte.d.ts +7 -0
- package/dist/events/event-view/ExternalRsvpNotice.svelte +34 -0
- package/dist/events/event-view/ExternalRsvpNotice.svelte.d.ts +6 -0
- package/dist/events/event-view/StreamPlacePlayer.svelte +209 -0
- package/dist/events/event-view/StreamPlacePlayer.svelte.d.ts +8 -0
- package/dist/events/event-view/format.d.ts +10 -0
- package/dist/events/event-view/format.js +112 -0
- package/dist/events/event-view/locations.d.ts +36 -0
- package/dist/events/event-view/locations.js +243 -0
- package/dist/events/event-view/plyr-icons.d.ts +3 -0
- package/dist/events/event-view/plyr-icons.js +46 -0
- package/dist/events/linked-records.d.ts +18 -0
- package/dist/events/linked-records.js +11 -0
- package/dist/events/profile-url.d.ts +3 -0
- package/dist/events/profile-url.js +11 -0
- package/dist/events/schedule/ConferenceTimetable.svelte +340 -0
- package/dist/events/schedule/ConferenceTimetable.svelte.d.ts +22 -0
- package/dist/events/schedule/DaySchedule.svelte +298 -0
- package/dist/events/schedule/DaySchedule.svelte.d.ts +26 -0
- package/dist/events/schedule/ScheduleEventCell.svelte +166 -0
- package/dist/events/schedule/ScheduleEventCell.svelte.d.ts +25 -0
- package/dist/events/schedule/schedule-utils.d.ts +44 -0
- package/dist/events/schedule/schedule-utils.js +166 -0
- package/dist/events/starter-pack-url.d.ts +3 -0
- package/dist/events/starter-pack-url.js +16 -0
- package/dist/events/theme.d.ts +9 -0
- package/dist/events/theme.js +36 -0
- package/dist/events/themes/Blobs.svelte +64 -0
- package/dist/events/themes/Blobs.svelte.d.ts +18 -0
- package/dist/events/themes/Butterflies.svelte +183 -0
- package/dist/events/themes/Butterflies.svelte.d.ts +3 -0
- package/dist/events/themes/Fireflies.svelte +134 -0
- package/dist/events/themes/Fireflies.svelte.d.ts +3 -0
- package/dist/events/themes/Kaleidoscope.svelte +183 -0
- package/dist/events/themes/Kaleidoscope.svelte.d.ts +3 -0
- package/dist/events/themes/Matrix.svelte +153 -0
- package/dist/events/themes/Matrix.svelte.d.ts +3 -0
- package/dist/events/themes/Stars.svelte +98 -0
- package/dist/events/themes/Stars.svelte.d.ts +3 -0
- package/dist/events/thumbnails/designs.d.ts +18 -0
- package/dist/events/thumbnails/designs.js +316 -0
- package/dist/events/view-data.d.ts +38 -0
- package/dist/events/view-data.js +1 -0
- package/dist/events-data.d.ts +15 -0
- package/dist/events-data.js +9 -0
- package/dist/events-editor.d.ts +2 -0
- package/dist/events-editor.js +2 -0
- package/dist/events.d.ts +19 -0
- package/dist/events.js +17 -0
- package/dist/index.d.ts +3 -0
- package/dist/index.js +2 -0
- package/package.json +50 -4
|
@@ -0,0 +1,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,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,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;
|