@orochibraru/ui 0.0.0 → 1.0.0-canary.2
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 +97 -21
- package/dist/components/blocks/accent-toggle.svelte +85 -0
- package/dist/components/blocks/accent-toggle.svelte.d.ts +18 -0
- package/dist/components/blocks/bottom-nav.svelte +67 -0
- package/dist/components/blocks/bottom-nav.svelte.d.ts +18 -0
- package/dist/components/blocks/music-player/format-time.d.ts +2 -0
- package/dist/components/blocks/music-player/format-time.js +13 -0
- package/dist/components/blocks/music-player/labelled-slider.svelte +36 -0
- package/dist/components/blocks/music-player/labelled-slider.svelte.d.ts +15 -0
- package/dist/components/blocks/music-player/volume-control.svelte +48 -0
- package/dist/components/blocks/music-player/volume-control.svelte.d.ts +7 -0
- package/dist/components/blocks/music-player.svelte +263 -0
- package/dist/components/blocks/music-player.svelte.d.ts +26 -0
- package/dist/components/blocks/nav-cards.svelte +42 -0
- package/dist/components/blocks/nav-cards.svelte.d.ts +15 -0
- package/dist/components/blocks/new-password-fields.svelte +127 -0
- package/dist/components/blocks/new-password-fields.svelte.d.ts +19 -0
- package/dist/components/blocks/option-group.svelte +71 -0
- package/dist/components/blocks/option-group.svelte.d.ts +20 -0
- package/dist/components/blocks/otp-field.svelte +59 -0
- package/dist/components/blocks/otp-field.svelte.d.ts +12 -0
- package/dist/components/blocks/password-input.svelte +51 -0
- package/dist/components/blocks/password-input.svelte.d.ts +11 -0
- package/dist/components/blocks/stat-card.svelte +1 -1
- package/dist/components/blocks/tab-nav.svelte +38 -0
- package/dist/components/blocks/tab-nav.svelte.d.ts +14 -0
- package/dist/components/blocks/tab-section.svelte +48 -0
- package/dist/components/blocks/tab-section.svelte.d.ts +12 -0
- package/dist/components/blocks/theme-picker.svelte +69 -0
- package/dist/components/blocks/theme-picker.svelte.d.ts +16 -0
- package/dist/components/blocks/theme-toggle.svelte +68 -0
- package/dist/components/blocks/theme-toggle.svelte.d.ts +12 -0
- package/dist/components/blocks/video-player/center-controls.svelte +54 -0
- package/dist/components/blocks/video-player/center-controls.svelte.d.ts +10 -0
- package/dist/components/blocks/video-player/control-bar.svelte +184 -0
- package/dist/components/blocks/video-player/control-bar.svelte.d.ts +18 -0
- package/dist/components/blocks/video-player/control-button.svelte +54 -0
- package/dist/components/blocks/video-player/control-button.svelte.d.ts +13 -0
- package/dist/components/blocks/video-player/format.d.ts +9 -0
- package/dist/components/blocks/video-player/format.js +24 -0
- package/dist/components/blocks/video-player/gesture-feedback.svelte +32 -0
- package/dist/components/blocks/video-player/gesture-feedback.svelte.d.ts +7 -0
- package/dist/components/blocks/video-player/gestures.d.ts +13 -0
- package/dist/components/blocks/video-player/gestures.js +25 -0
- package/dist/components/blocks/video-player/keymap.d.ts +14 -0
- package/dist/components/blocks/video-player/keymap.js +55 -0
- package/dist/components/blocks/video-player/messages.d.ts +22 -0
- package/dist/components/blocks/video-player/messages.js +21 -0
- package/dist/components/blocks/video-player/option-menu.svelte +85 -0
- package/dist/components/blocks/video-player/option-menu.svelte.d.ts +18 -0
- package/dist/components/blocks/video-player/player.svelte.d.ts +37 -0
- package/dist/components/blocks/video-player/player.svelte.js +115 -0
- package/dist/components/blocks/video-player/scrub-bar.svelte +95 -0
- package/dist/components/blocks/video-player/scrub-bar.svelte.d.ts +12 -0
- package/dist/components/blocks/video-player/tap-gestures.svelte.d.ts +19 -0
- package/dist/components/blocks/video-player/tap-gestures.svelte.js +56 -0
- package/dist/components/blocks/video-player.svelte +288 -0
- package/dist/components/blocks/video-player.svelte.d.ts +29 -0
- package/dist/components/ui/accordion/accordion.svelte.d.ts +1 -1
- package/dist/components/ui/calendar/calendar.svelte.d.ts +1 -1
- package/dist/components/ui/command/command-dialog.svelte.d.ts +1 -1
- package/dist/components/ui/command/command-input.svelte.d.ts +1 -1
- package/dist/components/ui/command/command.svelte.d.ts +1 -1
- package/dist/components/ui/context-menu/context-menu-radio-group.svelte.d.ts +1 -1
- package/dist/components/ui/dropdown-menu/dropdown-menu-checkbox-group.svelte.d.ts +1 -1
- package/dist/components/ui/dropdown-menu/dropdown-menu-radio-group.svelte.d.ts +1 -1
- package/dist/components/ui/input/input.svelte +2 -2
- package/dist/components/ui/input/input.svelte.d.ts +1 -1
- package/dist/components/ui/input-group/input-group-input.svelte.d.ts +2 -2
- package/dist/components/ui/input-group/input-group-textarea.svelte.d.ts +1 -1
- package/dist/components/ui/input-group/input-group.svelte +1 -1
- package/dist/components/ui/input-otp/input-otp-slot.svelte +1 -1
- package/dist/components/ui/input-otp/input-otp.svelte.d.ts +1 -1
- package/dist/components/ui/menubar/menubar-radio-group.svelte.d.ts +1 -1
- package/dist/components/ui/radio-group/radio-group.svelte.d.ts +1 -1
- package/dist/components/ui/range-calendar/range-calendar.svelte.d.ts +1 -1
- package/dist/components/ui/select/select-trigger.svelte +1 -1
- package/dist/components/ui/sidebar/sidebar-input.svelte.d.ts +2 -2
- package/dist/components/ui/slider/slider.svelte.d.ts +1 -1
- package/dist/components/ui/tabs/tabs.svelte.d.ts +1 -1
- package/dist/components/ui/textarea/textarea.svelte +1 -1
- package/dist/components/ui/textarea/textarea.svelte.d.ts +1 -1
- package/dist/components/ui/toggle/toggle.svelte.d.ts +1 -1
- package/dist/components/ui/toggle-group/toggle-group-item.svelte.d.ts +1 -1
- package/dist/components/ui/toggle-group/toggle-group.svelte.d.ts +1 -1
- package/dist/mcp-content.json +1 -0
- package/dist/mcp.js +83 -0
- package/dist/password.d.ts +11 -0
- package/dist/password.js +38 -0
- package/package.json +15 -3
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
<script lang="ts">
|
|
2
|
+
import EyeIcon from "@lucide/svelte/icons/eye";
|
|
3
|
+
import EyeOffIcon from "@lucide/svelte/icons/eye-off";
|
|
4
|
+
import type { HTMLInputAttributes } from "svelte/elements";
|
|
5
|
+
|
|
6
|
+
import IconButton from "./icon-button.svelte";
|
|
7
|
+
import { Input } from "../ui/input/index.js";
|
|
8
|
+
import { cn } from "../../utils.js";
|
|
9
|
+
|
|
10
|
+
const defaultMessages = {
|
|
11
|
+
show: "Show password",
|
|
12
|
+
hide: "Hide password",
|
|
13
|
+
};
|
|
14
|
+
|
|
15
|
+
type Props = Omit<HTMLInputAttributes, "type" | "value" | "files"> & {
|
|
16
|
+
value?: string;
|
|
17
|
+
visible?: boolean;
|
|
18
|
+
messages?: Partial<typeof defaultMessages>;
|
|
19
|
+
};
|
|
20
|
+
|
|
21
|
+
let {
|
|
22
|
+
value = $bindable(""),
|
|
23
|
+
visible = $bindable(false),
|
|
24
|
+
name = "password",
|
|
25
|
+
autocomplete = "current-password",
|
|
26
|
+
messages = {},
|
|
27
|
+
class: className,
|
|
28
|
+
...rest
|
|
29
|
+
}: Props = $props();
|
|
30
|
+
|
|
31
|
+
const text = $derived({ ...defaultMessages, ...messages });
|
|
32
|
+
</script>
|
|
33
|
+
|
|
34
|
+
<div class="relative">
|
|
35
|
+
<Input
|
|
36
|
+
{...rest}
|
|
37
|
+
{name}
|
|
38
|
+
{autocomplete}
|
|
39
|
+
type={visible ? "text" : "password"}
|
|
40
|
+
bind:value
|
|
41
|
+
class={cn("h-11 pr-12", className)}
|
|
42
|
+
/>
|
|
43
|
+
<div class="absolute inset-y-0 right-0 flex items-center">
|
|
44
|
+
<IconButton
|
|
45
|
+
label={visible ? text.hide : text.show}
|
|
46
|
+
icon={visible ? EyeOffIcon : EyeIcon}
|
|
47
|
+
onclick={() => (visible = !visible)}
|
|
48
|
+
class="size-11 md:size-11"
|
|
49
|
+
/>
|
|
50
|
+
</div>
|
|
51
|
+
</div>
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
import type { HTMLInputAttributes } from "svelte/elements";
|
|
2
|
+
declare const PasswordInput: import("svelte").Component<Omit<HTMLInputAttributes, "type" | "value" | "files"> & {
|
|
3
|
+
value?: string;
|
|
4
|
+
visible?: boolean;
|
|
5
|
+
messages?: Partial<{
|
|
6
|
+
show: string;
|
|
7
|
+
hide: string;
|
|
8
|
+
}>;
|
|
9
|
+
}, {}, "value" | "visible">;
|
|
10
|
+
type PasswordInput = ReturnType<typeof PasswordInput>;
|
|
11
|
+
export default PasswordInput;
|
|
@@ -31,7 +31,7 @@
|
|
|
31
31
|
<svelte:element
|
|
32
32
|
this={href ? "a" : "div"}
|
|
33
33
|
{href}
|
|
34
|
-
class="glass flex items-start gap-3 rounded-2xl p-4 transition-colors {href
|
|
34
|
+
class="glass flex min-w-0 items-start gap-3 rounded-2xl p-4 transition-colors {href
|
|
35
35
|
? 'hover:border-primary/40 active:bg-muted'
|
|
36
36
|
: ''}"
|
|
37
37
|
>
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
<script lang="ts" module>
|
|
2
|
+
import type { Component } from "svelte";
|
|
3
|
+
|
|
4
|
+
export interface Tab {
|
|
5
|
+
href: string;
|
|
6
|
+
label: string;
|
|
7
|
+
icon?: Component<{ class?: string }>;
|
|
8
|
+
}
|
|
9
|
+
</script>
|
|
10
|
+
|
|
11
|
+
<script lang="ts">
|
|
12
|
+
import { page } from "$app/state";
|
|
13
|
+
|
|
14
|
+
const { tabs }: { tabs: Tab[] } = $props();
|
|
15
|
+
|
|
16
|
+
function scrollIntoViewWhenActive(node: HTMLElement, active: boolean) {
|
|
17
|
+
if (active) {
|
|
18
|
+
node.scrollIntoView({ block: "nearest", inline: "center" });
|
|
19
|
+
}
|
|
20
|
+
}
|
|
21
|
+
</script>
|
|
22
|
+
|
|
23
|
+
<nav class="no-scrollbar flex overflow-x-auto py-1" aria-label="Sections">
|
|
24
|
+
<div class="glass flex w-full gap-1 rounded-full p-1 md:w-auto md:shrink-0">
|
|
25
|
+
{#each tabs as tab (tab.href)}
|
|
26
|
+
{@const active = page.url.pathname === tab.href}
|
|
27
|
+
<a
|
|
28
|
+
href={tab.href}
|
|
29
|
+
aria-current={active ? "page" : undefined}
|
|
30
|
+
use:scrollIntoViewWhenActive={active}
|
|
31
|
+
class="text-muted-foreground hover:bg-accent/70 hover:text-foreground aria-[current=page]:bg-primary aria-[current=page]:text-primary-foreground aria-[current=page]:glow focus-visible:ring-ring/50 flex h-9 min-w-0 flex-1 items-center justify-center gap-2 rounded-full px-2 md:flex-none md:shrink-0 md:px-4 text-sm font-medium transition-[color,background-color,box-shadow] outline-none focus-visible:ring-3"
|
|
32
|
+
>
|
|
33
|
+
{#if tab.icon}<tab.icon class="hidden size-4 sm:block" />{/if}
|
|
34
|
+
{tab.label}
|
|
35
|
+
</a>
|
|
36
|
+
{/each}
|
|
37
|
+
</div>
|
|
38
|
+
</nav>
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
import type { Component } from "svelte";
|
|
2
|
+
export interface Tab {
|
|
3
|
+
href: string;
|
|
4
|
+
label: string;
|
|
5
|
+
icon?: Component<{
|
|
6
|
+
class?: string;
|
|
7
|
+
}>;
|
|
8
|
+
}
|
|
9
|
+
type $$ComponentProps = {
|
|
10
|
+
tabs: Tab[];
|
|
11
|
+
};
|
|
12
|
+
declare const TabNav: Component<$$ComponentProps, {}, "">;
|
|
13
|
+
type TabNav = ReturnType<typeof TabNav>;
|
|
14
|
+
export default TabNav;
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
<script lang="ts">
|
|
2
|
+
import { goto } from "$app/navigation";
|
|
3
|
+
import { page } from "$app/state";
|
|
4
|
+
import type { Snippet } from "svelte";
|
|
5
|
+
import { MediaQuery } from "svelte/reactivity";
|
|
6
|
+
|
|
7
|
+
import type { SectionTab } from "./nav-cards.svelte";
|
|
8
|
+
import TabNav from "./tab-nav.svelte";
|
|
9
|
+
import { setPageTitle } from "../../page-title.svelte.js";
|
|
10
|
+
|
|
11
|
+
interface Props {
|
|
12
|
+
title: string;
|
|
13
|
+
indexHref: string;
|
|
14
|
+
backHref?: string;
|
|
15
|
+
tabs: SectionTab[];
|
|
16
|
+
children: Snippet;
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
const { title, indexHref, backHref, tabs, children }: Props = $props();
|
|
20
|
+
|
|
21
|
+
const onIndex = $derived(page.url.pathname === indexHref);
|
|
22
|
+
const active = $derived(
|
|
23
|
+
tabs.find(
|
|
24
|
+
(tab) =>
|
|
25
|
+
page.url.pathname === tab.href ||
|
|
26
|
+
page.url.pathname.startsWith(`${tab.href}/`),
|
|
27
|
+
),
|
|
28
|
+
);
|
|
29
|
+
|
|
30
|
+
const desktop = new MediaQuery("(min-width: 768px)");
|
|
31
|
+
|
|
32
|
+
$effect(() => {
|
|
33
|
+
const first = tabs[0];
|
|
34
|
+
if (desktop.current && onIndex && first) {
|
|
35
|
+
goto(first.href, { replace: true });
|
|
36
|
+
}
|
|
37
|
+
});
|
|
38
|
+
|
|
39
|
+
setPageTitle(
|
|
40
|
+
() => (desktop.current || onIndex || !active ? title : active.label),
|
|
41
|
+
() => (desktop.current || onIndex ? backHref : indexHref),
|
|
42
|
+
);
|
|
43
|
+
</script>
|
|
44
|
+
|
|
45
|
+
<div class="mb-6 hidden md:block">
|
|
46
|
+
<TabNav {tabs} />
|
|
47
|
+
</div>
|
|
48
|
+
{@render children()}
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
import type { Snippet } from "svelte";
|
|
2
|
+
import type { SectionTab } from "./nav-cards.svelte";
|
|
3
|
+
interface Props {
|
|
4
|
+
title: string;
|
|
5
|
+
indexHref: string;
|
|
6
|
+
backHref?: string;
|
|
7
|
+
tabs: SectionTab[];
|
|
8
|
+
children: Snippet;
|
|
9
|
+
}
|
|
10
|
+
declare const TabSection: import("svelte").Component<Props, {}, "">;
|
|
11
|
+
type TabSection = ReturnType<typeof TabSection>;
|
|
12
|
+
export default TabSection;
|
|
@@ -0,0 +1,69 @@
|
|
|
1
|
+
<script lang="ts">
|
|
2
|
+
import MonitorSmartphoneIcon from "@lucide/svelte/icons/monitor-smartphone";
|
|
3
|
+
import MoonIcon from "@lucide/svelte/icons/moon";
|
|
4
|
+
import SunIcon from "@lucide/svelte/icons/sun";
|
|
5
|
+
import { setMode, userPrefersMode } from "mode-watcher";
|
|
6
|
+
|
|
7
|
+
import OptionGroup from "./option-group.svelte";
|
|
8
|
+
|
|
9
|
+
type Theme = "system" | "light" | "dark";
|
|
10
|
+
|
|
11
|
+
const defaultMessages = {
|
|
12
|
+
legend: "Theme",
|
|
13
|
+
system: "System",
|
|
14
|
+
systemDescription: "Follows your device: light or dark as it switches.",
|
|
15
|
+
light: "Light",
|
|
16
|
+
lightDescription: "Always light, whatever the device says.",
|
|
17
|
+
dark: "Dark",
|
|
18
|
+
darkDescription: "Always dark, easier on the eyes at night.",
|
|
19
|
+
};
|
|
20
|
+
|
|
21
|
+
interface Props {
|
|
22
|
+
value?: Theme;
|
|
23
|
+
name?: string;
|
|
24
|
+
onchange?: (theme: Theme) => void;
|
|
25
|
+
messages?: Partial<typeof defaultMessages>;
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
let {
|
|
29
|
+
value = $bindable(userPrefersMode.current),
|
|
30
|
+
name = "theme",
|
|
31
|
+
onchange,
|
|
32
|
+
messages = {},
|
|
33
|
+
}: Props = $props();
|
|
34
|
+
|
|
35
|
+
const text = $derived({ ...defaultMessages, ...messages });
|
|
36
|
+
const themes = $derived([
|
|
37
|
+
{
|
|
38
|
+
value: "system",
|
|
39
|
+
label: text.system,
|
|
40
|
+
description: text.systemDescription,
|
|
41
|
+
icon: MonitorSmartphoneIcon,
|
|
42
|
+
},
|
|
43
|
+
{
|
|
44
|
+
value: "light",
|
|
45
|
+
label: text.light,
|
|
46
|
+
description: text.lightDescription,
|
|
47
|
+
icon: SunIcon,
|
|
48
|
+
},
|
|
49
|
+
{
|
|
50
|
+
value: "dark",
|
|
51
|
+
label: text.dark,
|
|
52
|
+
description: text.darkDescription,
|
|
53
|
+
icon: MoonIcon,
|
|
54
|
+
},
|
|
55
|
+
]);
|
|
56
|
+
|
|
57
|
+
function change() {
|
|
58
|
+
setMode(value);
|
|
59
|
+
onchange?.(value);
|
|
60
|
+
}
|
|
61
|
+
</script>
|
|
62
|
+
|
|
63
|
+
<OptionGroup
|
|
64
|
+
{name}
|
|
65
|
+
legend={text.legend}
|
|
66
|
+
options={themes}
|
|
67
|
+
bind:value
|
|
68
|
+
onchange={change}
|
|
69
|
+
/>
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
declare const ThemePicker: import("svelte").Component<{
|
|
2
|
+
value?: "system" | "light" | "dark";
|
|
3
|
+
name?: string;
|
|
4
|
+
onchange?: (theme: "system" | "light" | "dark") => void;
|
|
5
|
+
messages?: Partial<{
|
|
6
|
+
legend: string;
|
|
7
|
+
system: string;
|
|
8
|
+
systemDescription: string;
|
|
9
|
+
light: string;
|
|
10
|
+
lightDescription: string;
|
|
11
|
+
dark: string;
|
|
12
|
+
darkDescription: string;
|
|
13
|
+
}>;
|
|
14
|
+
}, {}, "value">;
|
|
15
|
+
type ThemePicker = ReturnType<typeof ThemePicker>;
|
|
16
|
+
export default ThemePicker;
|
|
@@ -0,0 +1,68 @@
|
|
|
1
|
+
<script lang="ts">
|
|
2
|
+
import MonitorSmartphoneIcon from "@lucide/svelte/icons/monitor-smartphone";
|
|
3
|
+
import MoonIcon from "@lucide/svelte/icons/moon";
|
|
4
|
+
import SunIcon from "@lucide/svelte/icons/sun";
|
|
5
|
+
import { setMode, userPrefersMode } from "mode-watcher";
|
|
6
|
+
import { MediaQuery } from "svelte/reactivity";
|
|
7
|
+
|
|
8
|
+
import * as ToggleGroup from "../ui/toggle-group/index.js";
|
|
9
|
+
|
|
10
|
+
type Theme = "system" | "light" | "dark";
|
|
11
|
+
|
|
12
|
+
const defaultMessages = {
|
|
13
|
+
label: "Theme",
|
|
14
|
+
system: "System",
|
|
15
|
+
light: "Light",
|
|
16
|
+
dark: "Dark",
|
|
17
|
+
};
|
|
18
|
+
|
|
19
|
+
interface Props {
|
|
20
|
+
value?: Theme;
|
|
21
|
+
onchange?: (theme: Theme) => void;
|
|
22
|
+
messages?: Partial<typeof defaultMessages>;
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
let {
|
|
26
|
+
value = $bindable(userPrefersMode.current),
|
|
27
|
+
onchange,
|
|
28
|
+
messages = {},
|
|
29
|
+
}: Props = $props();
|
|
30
|
+
|
|
31
|
+
const desktop = new MediaQuery("(min-width: 768px)");
|
|
32
|
+
const text = $derived({ ...defaultMessages, ...messages });
|
|
33
|
+
const themes = $derived([
|
|
34
|
+
{ value: "system", label: text.system, icon: MonitorSmartphoneIcon },
|
|
35
|
+
{ value: "light", label: text.light, icon: SunIcon },
|
|
36
|
+
{ value: "dark", label: text.dark, icon: MoonIcon },
|
|
37
|
+
] as const);
|
|
38
|
+
|
|
39
|
+
function pick(next: string) {
|
|
40
|
+
if (!next) {
|
|
41
|
+
return;
|
|
42
|
+
}
|
|
43
|
+
value = next as Theme;
|
|
44
|
+
setMode(value);
|
|
45
|
+
onchange?.(value);
|
|
46
|
+
}
|
|
47
|
+
</script>
|
|
48
|
+
|
|
49
|
+
<ToggleGroup.Root
|
|
50
|
+
type="single"
|
|
51
|
+
variant="outline"
|
|
52
|
+
orientation={desktop.current ? "horizontal" : "vertical"}
|
|
53
|
+
aria-label={text.label}
|
|
54
|
+
class="max-md:w-full"
|
|
55
|
+
bind:value={() => value, pick}
|
|
56
|
+
>
|
|
57
|
+
{#each themes as theme (theme.value)}
|
|
58
|
+
<ToggleGroup.Item
|
|
59
|
+
value={theme.value}
|
|
60
|
+
aria-label={theme.label}
|
|
61
|
+
title={theme.label}
|
|
62
|
+
class="max-md:h-12 max-md:w-full max-md:justify-start max-md:gap-3 max-md:px-4 md:size-9"
|
|
63
|
+
>
|
|
64
|
+
<theme.icon class="size-4" />
|
|
65
|
+
<span class="md:hidden">{theme.label}</span>
|
|
66
|
+
</ToggleGroup.Item>
|
|
67
|
+
{/each}
|
|
68
|
+
</ToggleGroup.Root>
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
declare const ThemeToggle: import("svelte").Component<{
|
|
2
|
+
value?: "system" | "light" | "dark";
|
|
3
|
+
onchange?: (theme: "system" | "light" | "dark") => void;
|
|
4
|
+
messages?: Partial<{
|
|
5
|
+
label: string;
|
|
6
|
+
system: string;
|
|
7
|
+
light: string;
|
|
8
|
+
dark: string;
|
|
9
|
+
}>;
|
|
10
|
+
}, {}, "value">;
|
|
11
|
+
type ThemeToggle = ReturnType<typeof ThemeToggle>;
|
|
12
|
+
export default ThemeToggle;
|
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
<script lang="ts">
|
|
2
|
+
import PauseIcon from "@lucide/svelte/icons/pause";
|
|
3
|
+
import PlayIcon from "@lucide/svelte/icons/play";
|
|
4
|
+
import RotateCcwIcon from "@lucide/svelte/icons/rotate-ccw";
|
|
5
|
+
import RotateCwIcon from "@lucide/svelte/icons/rotate-cw";
|
|
6
|
+
|
|
7
|
+
import { SEEK_SECONDS } from "./keymap.js";
|
|
8
|
+
import type { VideoPlayerMessages } from "./messages.js";
|
|
9
|
+
import type { VideoPlayerState } from "./player.svelte.js";
|
|
10
|
+
|
|
11
|
+
interface Props {
|
|
12
|
+
player: VideoPlayerState;
|
|
13
|
+
paused: boolean;
|
|
14
|
+
text: VideoPlayerMessages;
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
const { player, paused, text }: Props = $props();
|
|
18
|
+
|
|
19
|
+
const side =
|
|
20
|
+
"bg-chrome text-foreground ring-border pointer-events-auto flex size-11 items-center justify-center rounded-full ring-1 backdrop-blur-xl transition-transform active:scale-90 sm:size-14";
|
|
21
|
+
</script>
|
|
22
|
+
|
|
23
|
+
<div class="flex items-center justify-center gap-8 sm:gap-12">
|
|
24
|
+
<button
|
|
25
|
+
type="button"
|
|
26
|
+
aria-label={text.seekBackward}
|
|
27
|
+
class={side}
|
|
28
|
+
onclick={() => player.seek(-SEEK_SECONDS)}
|
|
29
|
+
>
|
|
30
|
+
<RotateCcwIcon class="size-5 sm:size-6" />
|
|
31
|
+
</button>
|
|
32
|
+
<button
|
|
33
|
+
type="button"
|
|
34
|
+
aria-label={player.ended ? text.replay : paused ? text.play : text.pause}
|
|
35
|
+
class="bg-primary text-primary-foreground glow pointer-events-auto flex size-14 items-center justify-center rounded-full transition-transform active:scale-90 sm:size-18"
|
|
36
|
+
onclick={player.togglePlay}
|
|
37
|
+
>
|
|
38
|
+
{#if player.ended}
|
|
39
|
+
<RotateCcwIcon class="size-7 sm:size-8" />
|
|
40
|
+
{:else if paused}
|
|
41
|
+
<PlayIcon class="size-7 fill-current sm:size-8" />
|
|
42
|
+
{:else}
|
|
43
|
+
<PauseIcon class="size-7 fill-current sm:size-8" />
|
|
44
|
+
{/if}
|
|
45
|
+
</button>
|
|
46
|
+
<button
|
|
47
|
+
type="button"
|
|
48
|
+
aria-label={text.seekForward}
|
|
49
|
+
class={side}
|
|
50
|
+
onclick={() => player.seek(SEEK_SECONDS)}
|
|
51
|
+
>
|
|
52
|
+
<RotateCwIcon class="size-5 sm:size-6" />
|
|
53
|
+
</button>
|
|
54
|
+
</div>
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
import type { VideoPlayerMessages } from "./messages.js";
|
|
2
|
+
import type { VideoPlayerState } from "./player.svelte.js";
|
|
3
|
+
interface Props {
|
|
4
|
+
player: VideoPlayerState;
|
|
5
|
+
paused: boolean;
|
|
6
|
+
text: VideoPlayerMessages;
|
|
7
|
+
}
|
|
8
|
+
declare const CenterControls: import("svelte").Component<Props, {}, "">;
|
|
9
|
+
type CenterControls = ReturnType<typeof CenterControls>;
|
|
10
|
+
export default CenterControls;
|
|
@@ -0,0 +1,184 @@
|
|
|
1
|
+
<script lang="ts">
|
|
2
|
+
import CaptionsIcon from "@lucide/svelte/icons/captions";
|
|
3
|
+
import CaptionsOffIcon from "@lucide/svelte/icons/captions-off";
|
|
4
|
+
import GaugeIcon from "@lucide/svelte/icons/gauge";
|
|
5
|
+
import MaximizeIcon from "@lucide/svelte/icons/maximize";
|
|
6
|
+
import MinimizeIcon from "@lucide/svelte/icons/minimize";
|
|
7
|
+
import PauseIcon from "@lucide/svelte/icons/pause";
|
|
8
|
+
import PictureInPictureIcon from "@lucide/svelte/icons/picture-in-picture-2";
|
|
9
|
+
import PlayIcon from "@lucide/svelte/icons/play";
|
|
10
|
+
import RotateCcwIcon from "@lucide/svelte/icons/rotate-ccw";
|
|
11
|
+
import Volume1Icon from "@lucide/svelte/icons/volume-1";
|
|
12
|
+
import Volume2Icon from "@lucide/svelte/icons/volume-2";
|
|
13
|
+
import VolumeXIcon from "@lucide/svelte/icons/volume-x";
|
|
14
|
+
|
|
15
|
+
import ControlButton from "./control-button.svelte";
|
|
16
|
+
import {
|
|
17
|
+
type Chapter,
|
|
18
|
+
formatTime,
|
|
19
|
+
} from "./format.js";
|
|
20
|
+
import type { VideoPlayerMessages } from "./messages.js";
|
|
21
|
+
import OptionMenu from "./option-menu.svelte";
|
|
22
|
+
import type { VideoPlayerState } from "./player.svelte.js";
|
|
23
|
+
import ScrubBar from "./scrub-bar.svelte";
|
|
24
|
+
|
|
25
|
+
const RATES = [0.5, 0.75, 1, 1.25, 1.5, 2];
|
|
26
|
+
|
|
27
|
+
interface Props {
|
|
28
|
+
player: VideoPlayerState;
|
|
29
|
+
paused: boolean;
|
|
30
|
+
currentTime: number;
|
|
31
|
+
chapters: Chapter[];
|
|
32
|
+
subtitles: string[];
|
|
33
|
+
activeSubtitle: number;
|
|
34
|
+
onsubtitlechange: (index: number) => void;
|
|
35
|
+
pictureInPicture: boolean;
|
|
36
|
+
portalTarget: HTMLElement | string;
|
|
37
|
+
text: VideoPlayerMessages;
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
const {
|
|
41
|
+
player,
|
|
42
|
+
paused,
|
|
43
|
+
currentTime,
|
|
44
|
+
chapters,
|
|
45
|
+
subtitles,
|
|
46
|
+
activeSubtitle,
|
|
47
|
+
onsubtitlechange,
|
|
48
|
+
pictureInPicture,
|
|
49
|
+
portalTarget,
|
|
50
|
+
text,
|
|
51
|
+
}: Props = $props();
|
|
52
|
+
|
|
53
|
+
const bufferedEnd = $derived(player.buffered.at(-1)?.end ?? 0);
|
|
54
|
+
const silent = $derived(player.muted || player.volume === 0);
|
|
55
|
+
const playLabel = $derived(
|
|
56
|
+
player.ended ? text.replay : paused ? text.play : text.pause,
|
|
57
|
+
);
|
|
58
|
+
</script>
|
|
59
|
+
|
|
60
|
+
<div
|
|
61
|
+
class="glass pointer-events-auto mx-2 mb-2 rounded-2xl px-3 pt-1.5 pb-1 sm:mx-3 sm:mb-3"
|
|
62
|
+
>
|
|
63
|
+
<div class="flex items-center gap-3 text-xs font-medium tabular">
|
|
64
|
+
<span class="w-11 shrink-0">{formatTime(currentTime)}</span>
|
|
65
|
+
<div class="min-w-0 flex-1">
|
|
66
|
+
<ScrubBar
|
|
67
|
+
{currentTime}
|
|
68
|
+
duration={player.duration}
|
|
69
|
+
{bufferedEnd}
|
|
70
|
+
{chapters}
|
|
71
|
+
label={text.seek}
|
|
72
|
+
onseek={player.seekTo}
|
|
73
|
+
/>
|
|
74
|
+
</div>
|
|
75
|
+
<span class="text-muted-foreground w-11 shrink-0 text-right"
|
|
76
|
+
>-{formatTime(Math.max(0, player.duration - currentTime))}</span
|
|
77
|
+
>
|
|
78
|
+
</div>
|
|
79
|
+
<div class="flex items-center gap-0.5">
|
|
80
|
+
<ControlButton
|
|
81
|
+
label={playLabel}
|
|
82
|
+
shortcut="K"
|
|
83
|
+
{portalTarget}
|
|
84
|
+
onclick={player.togglePlay}
|
|
85
|
+
class="bg-primary text-primary-foreground hover:bg-primary/85 hover:text-primary-foreground dark:hover:bg-primary/85 glow"
|
|
86
|
+
>
|
|
87
|
+
{#if player.ended}
|
|
88
|
+
<RotateCcwIcon />
|
|
89
|
+
{:else if paused}
|
|
90
|
+
<PlayIcon class="size-5 fill-current" />
|
|
91
|
+
{:else}
|
|
92
|
+
<PauseIcon class="size-5 fill-current" />
|
|
93
|
+
{/if}
|
|
94
|
+
</ControlButton>
|
|
95
|
+
<ControlButton
|
|
96
|
+
label={silent ? text.unmute : text.mute}
|
|
97
|
+
shortcut="M"
|
|
98
|
+
{portalTarget}
|
|
99
|
+
onclick={player.toggleMute}
|
|
100
|
+
>
|
|
101
|
+
{#if silent}
|
|
102
|
+
<VolumeXIcon />
|
|
103
|
+
{:else if player.volume < 0.5}
|
|
104
|
+
<Volume1Icon />
|
|
105
|
+
{:else}
|
|
106
|
+
<Volume2Icon />
|
|
107
|
+
{/if}
|
|
108
|
+
</ControlButton>
|
|
109
|
+
<input
|
|
110
|
+
type="range"
|
|
111
|
+
min="0"
|
|
112
|
+
max="1"
|
|
113
|
+
step="0.05"
|
|
114
|
+
value={player.muted ? 0 : player.volume}
|
|
115
|
+
aria-label={text.volume}
|
|
116
|
+
oninput={(event) => {
|
|
117
|
+
player.volume = Number(event.currentTarget.value);
|
|
118
|
+
player.muted = player.volume === 0;
|
|
119
|
+
}}
|
|
120
|
+
class="accent-primary pointer-fine:block hidden h-1 w-20 cursor-pointer"
|
|
121
|
+
/>
|
|
122
|
+
<div class="ml-auto flex items-center gap-0.5">
|
|
123
|
+
{#if subtitles.length > 0}
|
|
124
|
+
<OptionMenu
|
|
125
|
+
label={text.subtitles}
|
|
126
|
+
shortcut="C"
|
|
127
|
+
options={[
|
|
128
|
+
{ value: "-1", label: text.subtitlesOff },
|
|
129
|
+
...subtitles.map((label, index) => ({
|
|
130
|
+
value: String(index),
|
|
131
|
+
label,
|
|
132
|
+
})),
|
|
133
|
+
]}
|
|
134
|
+
value={String(activeSubtitle)}
|
|
135
|
+
active={activeSubtitle >= 0}
|
|
136
|
+
{portalTarget}
|
|
137
|
+
onselect={(value) => onsubtitlechange(Number(value))}
|
|
138
|
+
onopenchange={(open) => (player.menuOpen = open)}
|
|
139
|
+
>
|
|
140
|
+
{#if activeSubtitle >= 0}
|
|
141
|
+
<CaptionsIcon />
|
|
142
|
+
{:else}
|
|
143
|
+
<CaptionsOffIcon />
|
|
144
|
+
{/if}
|
|
145
|
+
</OptionMenu>
|
|
146
|
+
{/if}
|
|
147
|
+
<OptionMenu
|
|
148
|
+
label={text.speed}
|
|
149
|
+
options={RATES.map((rate) => ({
|
|
150
|
+
value: String(rate),
|
|
151
|
+
label: rate === 1 ? text.speedNormal : `${rate}×`,
|
|
152
|
+
}))}
|
|
153
|
+
value={String(player.rate)}
|
|
154
|
+
active={player.rate !== 1}
|
|
155
|
+
{portalTarget}
|
|
156
|
+
onselect={(value) => (player.rate = Number(value))}
|
|
157
|
+
onopenchange={(open) => (player.menuOpen = open)}
|
|
158
|
+
>
|
|
159
|
+
<GaugeIcon />
|
|
160
|
+
</OptionMenu>
|
|
161
|
+
{#if pictureInPicture}
|
|
162
|
+
<ControlButton
|
|
163
|
+
label={text.pictureInPicture}
|
|
164
|
+
{portalTarget}
|
|
165
|
+
onclick={() => void player.togglePictureInPicture()}
|
|
166
|
+
>
|
|
167
|
+
<PictureInPictureIcon />
|
|
168
|
+
</ControlButton>
|
|
169
|
+
{/if}
|
|
170
|
+
<ControlButton
|
|
171
|
+
label={player.fullscreen ? text.exitFullscreen : text.fullscreen}
|
|
172
|
+
shortcut="F"
|
|
173
|
+
{portalTarget}
|
|
174
|
+
onclick={() => void player.toggleFullscreen()}
|
|
175
|
+
>
|
|
176
|
+
{#if player.fullscreen}
|
|
177
|
+
<MinimizeIcon />
|
|
178
|
+
{:else}
|
|
179
|
+
<MaximizeIcon />
|
|
180
|
+
{/if}
|
|
181
|
+
</ControlButton>
|
|
182
|
+
</div>
|
|
183
|
+
</div>
|
|
184
|
+
</div>
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
import { type Chapter } from "./format.js";
|
|
2
|
+
import type { VideoPlayerMessages } from "./messages.js";
|
|
3
|
+
import type { VideoPlayerState } from "./player.svelte.js";
|
|
4
|
+
interface Props {
|
|
5
|
+
player: VideoPlayerState;
|
|
6
|
+
paused: boolean;
|
|
7
|
+
currentTime: number;
|
|
8
|
+
chapters: Chapter[];
|
|
9
|
+
subtitles: string[];
|
|
10
|
+
activeSubtitle: number;
|
|
11
|
+
onsubtitlechange: (index: number) => void;
|
|
12
|
+
pictureInPicture: boolean;
|
|
13
|
+
portalTarget: HTMLElement | string;
|
|
14
|
+
text: VideoPlayerMessages;
|
|
15
|
+
}
|
|
16
|
+
declare const ControlBar: import("svelte").Component<Props, {}, "">;
|
|
17
|
+
type ControlBar = ReturnType<typeof ControlBar>;
|
|
18
|
+
export default ControlBar;
|
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
<script lang="ts">
|
|
2
|
+
import type { Snippet } from "svelte";
|
|
3
|
+
|
|
4
|
+
import { Button } from "../../ui/button/index.js";
|
|
5
|
+
import * as Tooltip from "../../ui/tooltip/index.js";
|
|
6
|
+
import { cn } from "../../../utils.js";
|
|
7
|
+
|
|
8
|
+
interface Props {
|
|
9
|
+
label: string;
|
|
10
|
+
shortcut?: string;
|
|
11
|
+
pressed?: boolean;
|
|
12
|
+
portalTarget: HTMLElement | string;
|
|
13
|
+
onclick: () => void;
|
|
14
|
+
class?: string;
|
|
15
|
+
children: Snippet;
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
const {
|
|
19
|
+
label,
|
|
20
|
+
shortcut,
|
|
21
|
+
pressed,
|
|
22
|
+
portalTarget,
|
|
23
|
+
onclick,
|
|
24
|
+
class: className,
|
|
25
|
+
children,
|
|
26
|
+
}: Props = $props();
|
|
27
|
+
</script>
|
|
28
|
+
|
|
29
|
+
<Tooltip.Root>
|
|
30
|
+
<Tooltip.Trigger>
|
|
31
|
+
{#snippet child({ props })}
|
|
32
|
+
<Button
|
|
33
|
+
{...props}
|
|
34
|
+
variant="ghost"
|
|
35
|
+
size="icon"
|
|
36
|
+
aria-label={label}
|
|
37
|
+
aria-pressed={pressed}
|
|
38
|
+
{onclick}
|
|
39
|
+
class={cn(
|
|
40
|
+
"size-11 rounded-full md:size-9 [&_svg:not([class*='size-'])]:size-5",
|
|
41
|
+
className,
|
|
42
|
+
)}
|
|
43
|
+
>
|
|
44
|
+
{@render children()}
|
|
45
|
+
</Button>
|
|
46
|
+
{/snippet}
|
|
47
|
+
</Tooltip.Trigger>
|
|
48
|
+
<Tooltip.Content portalProps={{ to: portalTarget }} sideOffset={6}>
|
|
49
|
+
{label}
|
|
50
|
+
{#if shortcut}
|
|
51
|
+
<kbd class="font-sans opacity-60">{shortcut}</kbd>
|
|
52
|
+
{/if}
|
|
53
|
+
</Tooltip.Content>
|
|
54
|
+
</Tooltip.Root>
|