feat!: playlists
This commit is contained in:
@@ -1,7 +1,17 @@
|
||||
<script lang="ts">
|
||||
import { page } from '$app/stores';
|
||||
import * as Sidebar from '$lib/components/ui/sidebar';
|
||||
import { Home, Library, ListMusic, Music2, Settings } from 'lucide-svelte';
|
||||
import Home from 'virtual:icons/lucide/home';
|
||||
import Library from 'virtual:icons/lucide/library';
|
||||
import ListMusic from 'virtual:icons/lucide/list-music';
|
||||
import Music2 from 'virtual:icons/lucide/music-2';
|
||||
import Settings from 'virtual:icons/lucide/settings';
|
||||
import CirclePlus from 'virtual:icons/lucide/circle-plus';
|
||||
import { cn } from '$lib/utils';
|
||||
import CreatePlaylistDialog from './CreatePlaylistDialog.svelte';
|
||||
import { getLibraryState } from '$lib/library.svelte';
|
||||
|
||||
const library = getLibraryState();
|
||||
</script>
|
||||
|
||||
<Sidebar.Root collapsible="icon">
|
||||
@@ -64,6 +74,25 @@
|
||||
</a>
|
||||
{/snippet}
|
||||
</Sidebar.MenuButton>
|
||||
<Sidebar.MenuSub>
|
||||
<Sidebar.MenuSubItem>
|
||||
<Sidebar.MenuSubButton class="cursor-pointer">
|
||||
{#snippet child({ props })}
|
||||
<CreatePlaylistDialog class={cn(props.class ?? '', 'w-full')}>
|
||||
<CirclePlus class="!text-inherit" />
|
||||
<span>Create Playlist</span>
|
||||
</CreatePlaylistDialog>
|
||||
{/snippet}
|
||||
</Sidebar.MenuSubButton>
|
||||
</Sidebar.MenuSubItem>
|
||||
{#each library.playlists as playlist}
|
||||
<Sidebar.MenuSubItem>
|
||||
<Sidebar.MenuSubButton class="cursor-pointer">
|
||||
{playlist.name}
|
||||
</Sidebar.MenuSubButton>
|
||||
</Sidebar.MenuSubItem>
|
||||
{/each}
|
||||
</Sidebar.MenuSub>
|
||||
</Sidebar.MenuItem>
|
||||
</Sidebar.Menu>
|
||||
</Sidebar.Group>
|
||||
|
||||
69
src/lib/components/groove/CreatePlaylistDialog.svelte
Normal file
69
src/lib/components/groove/CreatePlaylistDialog.svelte
Normal file
@@ -0,0 +1,69 @@
|
||||
<script lang="ts">
|
||||
import type { Snippet } from 'svelte';
|
||||
import * as Dialog from '$lib/components/ui/dialog';
|
||||
import { enhance } from '$app/forms';
|
||||
import { Input } from '$lib/components/ui/input';
|
||||
import { Button } from '$lib/components/ui/button';
|
||||
import CirclePlus from 'virtual:icons/lucide/circle-plus';
|
||||
import type { SubmitFunction } from '../../../routes/playlists/$types';
|
||||
|
||||
interface Props {
|
||||
children?: Snippet;
|
||||
[key: string]: any;
|
||||
}
|
||||
|
||||
let { children, ...props }: Props = $props();
|
||||
|
||||
let name = $state('');
|
||||
let submitting = $state(false);
|
||||
let isOpen = $state(false);
|
||||
|
||||
const createPlaylist: SubmitFunction = async ({ cancel }) => {
|
||||
if (submitting) {
|
||||
cancel();
|
||||
return;
|
||||
}
|
||||
|
||||
submitting = true;
|
||||
isOpen = false;
|
||||
|
||||
return async ({ update }) => {
|
||||
await update({
|
||||
invalidateAll: true,
|
||||
reset: true
|
||||
});
|
||||
|
||||
submitting = false;
|
||||
};
|
||||
};
|
||||
</script>
|
||||
|
||||
<Dialog.Root bind:open={isOpen}>
|
||||
<Dialog.Trigger {...props}>
|
||||
{@render children?.()}
|
||||
</Dialog.Trigger>
|
||||
<Dialog.Content>
|
||||
<Dialog.Header>
|
||||
<Dialog.Title>Name your Playlist</Dialog.Title>
|
||||
<Dialog.Description>Make it memorable</Dialog.Description>
|
||||
</Dialog.Header>
|
||||
|
||||
<form method="POST" action="/playlists?/create" use:enhance={createPlaylist}>
|
||||
<Input
|
||||
name="name"
|
||||
placeholder="My awesome playlist"
|
||||
minlength={1}
|
||||
maxlength={24}
|
||||
bind:value={name}
|
||||
/>
|
||||
<Button
|
||||
type="submit"
|
||||
class="mt-2 w-full"
|
||||
disabled={submitting || name.length < 1 || name.length > 24}
|
||||
>
|
||||
<CirclePlus />
|
||||
Create
|
||||
</Button>
|
||||
</form>
|
||||
</Dialog.Content>
|
||||
</Dialog.Root>
|
||||
@@ -52,10 +52,6 @@
|
||||
};
|
||||
};
|
||||
|
||||
async function skipToQueueIndex(index: number) {
|
||||
console.log(`SKIP TO QUEUE INDEX ${index}`);
|
||||
}
|
||||
|
||||
const submitPlayerAction: SubmitFunction = async () => {
|
||||
return async ({ update, result }) => {
|
||||
await update({
|
||||
|
||||
115
src/lib/components/groove/PlaylistListing.svelte
Normal file
115
src/lib/components/groove/PlaylistListing.svelte
Normal file
@@ -0,0 +1,115 @@
|
||||
<script lang="ts">
|
||||
import type { Track } from '$lib/proto/library';
|
||||
import dayjs from 'dayjs';
|
||||
import { Button } from '$lib/components/ui/button';
|
||||
import Trash2 from 'virtual:icons/lucide/trash-2';
|
||||
import { enhance } from '$app/forms';
|
||||
import Play from 'virtual:icons/lucide/play';
|
||||
import type { SubmitFunction } from '../../../routes/playlists/[id]/$types';
|
||||
import { getCoverUrl } from '$lib/covers';
|
||||
import { getLibraryState } from '$lib/library.svelte';
|
||||
|
||||
interface Props {
|
||||
id: number;
|
||||
name: string;
|
||||
tracks: Track[];
|
||||
}
|
||||
|
||||
let { id, name, tracks }: Props = $props();
|
||||
let totalDuration = $derived<number>(
|
||||
tracks.reduce((acc, track) => acc + Number(track.duration), 0)
|
||||
);
|
||||
|
||||
const library = getLibraryState();
|
||||
|
||||
let coverImages = $derived.by(() => {
|
||||
if (tracks.length === 0) {
|
||||
return [];
|
||||
}
|
||||
|
||||
if (tracks.length >= 4) {
|
||||
return tracks.slice(0, 4).map((t) => getCoverUrl(t.hash));
|
||||
}
|
||||
|
||||
if (tracks.length === 1) {
|
||||
return [getCoverUrl(tracks[0].hash)];
|
||||
}
|
||||
|
||||
if (tracks.length === 2) {
|
||||
const x = getCoverUrl(tracks[0].hash);
|
||||
const y = getCoverUrl(tracks[1].hash);
|
||||
return [x, y, y, x];
|
||||
}
|
||||
|
||||
let _covers: string[] = [];
|
||||
|
||||
let i = 0;
|
||||
while (_covers.length < 4) {
|
||||
_covers.push(getCoverUrl(tracks[i].hash));
|
||||
i = (i + 1) % tracks.length;
|
||||
}
|
||||
|
||||
return _covers;
|
||||
});
|
||||
|
||||
const playPlaylist: SubmitFunction = async () => {
|
||||
return async ({ update }) => {
|
||||
await update({
|
||||
invalidateAll: false,
|
||||
reset: false
|
||||
});
|
||||
};
|
||||
};
|
||||
|
||||
// TODO: Refactor
|
||||
const deletePlaylist: SubmitFunction = async ({ action }) => {
|
||||
const playlistId = parseInt(action.pathname.split('/playlists/')[1].split('/')[0]);
|
||||
|
||||
return async ({ update, result }) => {
|
||||
await update({
|
||||
invalidateAll: false,
|
||||
reset: false
|
||||
});
|
||||
|
||||
if (result.type === 'success') {
|
||||
library.playlists = library.playlists.filter((p) => p.id !== playlistId);
|
||||
}
|
||||
};
|
||||
};
|
||||
</script>
|
||||
|
||||
<div class="overflow-hidden rounded-md border">
|
||||
<div class="relative grid" class:grid-cols-2={coverImages.length > 1}>
|
||||
{#each coverImages as cover}
|
||||
<img class="aspect-square" src={cover} alt="" />
|
||||
{:else}
|
||||
<div class="w-full aspect-square animate-pulse bg-secondary"></div>
|
||||
{/each}
|
||||
</div>
|
||||
<div class="space-y-2 p-4">
|
||||
<div class="relative space-y-2">
|
||||
<h3 class="font-medium leading-none">{name}</h3>
|
||||
<div>
|
||||
<p class="text-xs text-muted-foreground">
|
||||
{dayjs(totalDuration, 'milliseconds').format('mm:ss')}
|
||||
</p>
|
||||
<p class="text-xs text-muted-foreground">
|
||||
{tracks.length} track{tracks.length !== 1 ? 's' : ''}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
<div class="flex w-full justify-between gap-1">
|
||||
<form method="POST" action="/playlists/{id}?/play" use:enhance={playPlaylist}>
|
||||
<Button type="submit" variant="outline" size="icon">
|
||||
<Play />
|
||||
</Button>
|
||||
</form>
|
||||
|
||||
<form method="POST" action="/playlists/{id}?/delete" use:enhance={deletePlaylist}>
|
||||
<Button type="submit" variant="outline" size="icon">
|
||||
<Trash2 />
|
||||
</Button>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@@ -2,9 +2,9 @@
|
||||
import { deserialize, enhance } from '$app/forms';
|
||||
import { getCoverUrl } from '$lib/covers';
|
||||
import { getPlayerState } from '$lib/player.svelte';
|
||||
import { flip } from 'svelte/animate';
|
||||
import type { SubmitFunction } from '../../../routes/player/$types';
|
||||
import type { Track } from '$lib/proto/library';
|
||||
import { ScrollArea } from '$lib/components/ui/scroll-area';
|
||||
|
||||
const player = getPlayerState();
|
||||
|
||||
@@ -63,37 +63,35 @@
|
||||
}
|
||||
</script>
|
||||
|
||||
<form
|
||||
class="flex h-full flex-col gap-1 overflow-y-auto"
|
||||
method="POST"
|
||||
use:enhance={submitPlayerAction}
|
||||
>
|
||||
{#each player.queue as track, i}
|
||||
<button
|
||||
type="submit"
|
||||
class="flex flex-row items-center gap-2 rounded-lg px-3 py-2 transition-all hover:bg-secondary"
|
||||
formaction="/player?/skip-to-queue-index&index={i}"
|
||||
draggable={true}
|
||||
ondragstart={onDragStart}
|
||||
ondrop={onDrop}
|
||||
ondragover={(e) => e.preventDefault()}
|
||||
data-queue-index={i}
|
||||
>
|
||||
<div class="min-w-8 overflow-hidden rounded-md">
|
||||
<img src={getCoverUrl(track.hash)} class="aspect-square size-8" alt="Cover" />
|
||||
</div>
|
||||
<div class="flex flex-col overflow-hidden">
|
||||
<p
|
||||
class="w-full self-start overflow-hidden text-ellipsis text-nowrap text-left text-sm text-foreground/80"
|
||||
>
|
||||
{track.name}
|
||||
</p>
|
||||
<p class="w-full self-start overflow-hidden text-left text-xs text-muted-foreground">
|
||||
{track.artistName}
|
||||
</p>
|
||||
</div>
|
||||
</button>
|
||||
{/each}
|
||||
<form class="flex w-full flex-col overflow-y-hidden" method="POST" use:enhance={submitPlayerAction}>
|
||||
<ScrollArea class="flex h-full w-full flex-col pr-3">
|
||||
{#each player.queue as track, i}
|
||||
<button
|
||||
type="submit"
|
||||
class="flex w-full flex-row items-center gap-2 rounded-lg px-3 py-2 transition-all hover:bg-secondary"
|
||||
formaction="/player?/skip-to-queue-index&index={i}"
|
||||
draggable={true}
|
||||
ondragstart={onDragStart}
|
||||
ondrop={onDrop}
|
||||
ondragover={(e) => e.preventDefault()}
|
||||
data-queue-index={i}
|
||||
>
|
||||
<div class="min-w-8 overflow-hidden rounded-md">
|
||||
<img src={getCoverUrl(track.hash)} class="aspect-square size-8" alt="Cover" />
|
||||
</div>
|
||||
<div class="flex flex-col overflow-hidden">
|
||||
<p
|
||||
class="w-full self-start overflow-hidden text-ellipsis text-nowrap text-left text-sm text-foreground/80"
|
||||
>
|
||||
{track.name}
|
||||
</p>
|
||||
<p class="w-full self-start overflow-hidden text-left text-xs text-muted-foreground">
|
||||
{track.artistName}
|
||||
</p>
|
||||
</div>
|
||||
</button>
|
||||
{/each}
|
||||
</ScrollArea>
|
||||
</form>
|
||||
|
||||
<style>
|
||||
|
||||
24
src/lib/components/ui/accordion/accordion-content.svelte
Normal file
24
src/lib/components/ui/accordion/accordion-content.svelte
Normal file
@@ -0,0 +1,24 @@
|
||||
<script lang="ts">
|
||||
import { Accordion as AccordionPrimitive, type WithoutChild } from "bits-ui";
|
||||
import { cn } from "$lib/utils.js";
|
||||
|
||||
let {
|
||||
ref = $bindable(null),
|
||||
class: className,
|
||||
children,
|
||||
...restProps
|
||||
}: WithoutChild<AccordionPrimitive.ContentProps> = $props();
|
||||
</script>
|
||||
|
||||
<AccordionPrimitive.Content
|
||||
bind:ref
|
||||
class={cn(
|
||||
"data-[state=closed]:animate-accordion-up data-[state=open]:animate-accordion-down overflow-hidden text-sm",
|
||||
className
|
||||
)}
|
||||
{...restProps}
|
||||
>
|
||||
<div class="pb-4 pt-0">
|
||||
{@render children?.()}
|
||||
</div>
|
||||
</AccordionPrimitive.Content>
|
||||
12
src/lib/components/ui/accordion/accordion-item.svelte
Normal file
12
src/lib/components/ui/accordion/accordion-item.svelte
Normal file
@@ -0,0 +1,12 @@
|
||||
<script lang="ts">
|
||||
import { Accordion as AccordionPrimitive } from "bits-ui";
|
||||
import { cn } from "$lib/utils.js";
|
||||
|
||||
let {
|
||||
ref = $bindable(null),
|
||||
class: className,
|
||||
...restProps
|
||||
}: AccordionPrimitive.ItemProps = $props();
|
||||
</script>
|
||||
|
||||
<AccordionPrimitive.Item bind:ref class={cn("border-b", className)} {...restProps} />
|
||||
31
src/lib/components/ui/accordion/accordion-trigger.svelte
Normal file
31
src/lib/components/ui/accordion/accordion-trigger.svelte
Normal file
@@ -0,0 +1,31 @@
|
||||
<script lang="ts">
|
||||
import { Accordion as AccordionPrimitive, type WithoutChild } from "bits-ui";
|
||||
import ChevronDown from "lucide-svelte/icons/chevron-down";
|
||||
import { cn } from "$lib/utils.js";
|
||||
|
||||
let {
|
||||
ref = $bindable(null),
|
||||
class: className,
|
||||
level = 3,
|
||||
children,
|
||||
...restProps
|
||||
}: WithoutChild<AccordionPrimitive.TriggerProps> & {
|
||||
level?: AccordionPrimitive.HeaderProps["level"];
|
||||
} = $props();
|
||||
</script>
|
||||
|
||||
<AccordionPrimitive.Header {level} class="flex">
|
||||
<AccordionPrimitive.Trigger
|
||||
bind:ref
|
||||
class={cn(
|
||||
"flex flex-1 items-center justify-between py-4 text-sm font-medium transition-all hover:underline [&[data-state=open]>svg]:rotate-180",
|
||||
className
|
||||
)}
|
||||
{...restProps}
|
||||
>
|
||||
{@render children?.()}
|
||||
<ChevronDown
|
||||
class="text-muted-foreground size-4 shrink-0 transition-transform duration-200"
|
||||
/>
|
||||
</AccordionPrimitive.Trigger>
|
||||
</AccordionPrimitive.Header>
|
||||
17
src/lib/components/ui/accordion/index.ts
Normal file
17
src/lib/components/ui/accordion/index.ts
Normal file
@@ -0,0 +1,17 @@
|
||||
import { Accordion as AccordionPrimitive } from "bits-ui";
|
||||
import Content from "./accordion-content.svelte";
|
||||
import Item from "./accordion-item.svelte";
|
||||
import Trigger from "./accordion-trigger.svelte";
|
||||
|
||||
const Root = AccordionPrimitive.Root;
|
||||
export {
|
||||
Root,
|
||||
Content,
|
||||
Item,
|
||||
Trigger,
|
||||
//
|
||||
Root as Accordion,
|
||||
Content as AccordionContent,
|
||||
Item as AccordionItem,
|
||||
Trigger as AccordionTrigger,
|
||||
};
|
||||
16
src/lib/components/ui/card/card-content.svelte
Normal file
16
src/lib/components/ui/card/card-content.svelte
Normal file
@@ -0,0 +1,16 @@
|
||||
<script lang="ts">
|
||||
import type { WithElementRef } from "bits-ui";
|
||||
import type { HTMLAttributes } from "svelte/elements";
|
||||
import { cn } from "$lib/utils.js";
|
||||
|
||||
let {
|
||||
ref = $bindable(null),
|
||||
class: className,
|
||||
children,
|
||||
...restProps
|
||||
}: WithElementRef<HTMLAttributes<HTMLDivElement>> = $props();
|
||||
</script>
|
||||
|
||||
<div bind:this={ref} class={cn("p-6", className)} {...restProps}>
|
||||
{@render children?.()}
|
||||
</div>
|
||||
16
src/lib/components/ui/card/card-description.svelte
Normal file
16
src/lib/components/ui/card/card-description.svelte
Normal file
@@ -0,0 +1,16 @@
|
||||
<script lang="ts">
|
||||
import type { WithElementRef } from "bits-ui";
|
||||
import type { HTMLAttributes } from "svelte/elements";
|
||||
import { cn } from "$lib/utils.js";
|
||||
|
||||
let {
|
||||
ref = $bindable(null),
|
||||
class: className,
|
||||
children,
|
||||
...restProps
|
||||
}: WithElementRef<HTMLAttributes<HTMLParagraphElement>> = $props();
|
||||
</script>
|
||||
|
||||
<p bind:this={ref} class={cn("text-muted-foreground text-sm", className)} {...restProps}>
|
||||
{@render children?.()}
|
||||
</p>
|
||||
16
src/lib/components/ui/card/card-footer.svelte
Normal file
16
src/lib/components/ui/card/card-footer.svelte
Normal file
@@ -0,0 +1,16 @@
|
||||
<script lang="ts">
|
||||
import type { WithElementRef } from "bits-ui";
|
||||
import type { HTMLAttributes } from "svelte/elements";
|
||||
import { cn } from "$lib/utils.js";
|
||||
|
||||
let {
|
||||
ref = $bindable(null),
|
||||
class: className,
|
||||
children,
|
||||
...restProps
|
||||
}: WithElementRef<HTMLAttributes<HTMLDivElement>> = $props();
|
||||
</script>
|
||||
|
||||
<div bind:this={ref} class={cn("flex items-center p-6 pt-0", className)} {...restProps}>
|
||||
{@render children?.()}
|
||||
</div>
|
||||
16
src/lib/components/ui/card/card-header.svelte
Normal file
16
src/lib/components/ui/card/card-header.svelte
Normal file
@@ -0,0 +1,16 @@
|
||||
<script lang="ts">
|
||||
import type { WithElementRef } from "bits-ui";
|
||||
import type { HTMLAttributes } from "svelte/elements";
|
||||
import { cn } from "$lib/utils.js";
|
||||
|
||||
let {
|
||||
ref = $bindable(null),
|
||||
class: className,
|
||||
children,
|
||||
...restProps
|
||||
}: WithElementRef<HTMLAttributes<HTMLDivElement>> = $props();
|
||||
</script>
|
||||
|
||||
<div bind:this={ref} class={cn("flex flex-col space-y-1.5 p-6 pb-0", className)} {...restProps}>
|
||||
{@render children?.()}
|
||||
</div>
|
||||
25
src/lib/components/ui/card/card-title.svelte
Normal file
25
src/lib/components/ui/card/card-title.svelte
Normal file
@@ -0,0 +1,25 @@
|
||||
<script lang="ts">
|
||||
import type { WithElementRef } from "bits-ui";
|
||||
import type { HTMLAttributes } from "svelte/elements";
|
||||
import { cn } from "$lib/utils.js";
|
||||
|
||||
let {
|
||||
ref = $bindable(null),
|
||||
class: className,
|
||||
level = 3,
|
||||
children,
|
||||
...restProps
|
||||
}: WithElementRef<HTMLAttributes<HTMLDivElement>> & {
|
||||
level?: 1 | 2 | 3 | 4 | 5 | 6;
|
||||
} = $props();
|
||||
</script>
|
||||
|
||||
<div
|
||||
role="heading"
|
||||
aria-level={level}
|
||||
bind:this={ref}
|
||||
class={cn("font-semibold leading-none tracking-tight", className)}
|
||||
{...restProps}
|
||||
>
|
||||
{@render children?.()}
|
||||
</div>
|
||||
20
src/lib/components/ui/card/card.svelte
Normal file
20
src/lib/components/ui/card/card.svelte
Normal file
@@ -0,0 +1,20 @@
|
||||
<script lang="ts">
|
||||
import type { WithElementRef } from "bits-ui";
|
||||
import type { HTMLAttributes } from "svelte/elements";
|
||||
import { cn } from "$lib/utils.js";
|
||||
|
||||
let {
|
||||
ref = $bindable(null),
|
||||
class: className,
|
||||
children,
|
||||
...restProps
|
||||
}: WithElementRef<HTMLAttributes<HTMLDivElement>> = $props();
|
||||
</script>
|
||||
|
||||
<div
|
||||
bind:this={ref}
|
||||
class={cn("bg-card text-card-foreground rounded-xl border shadow", className)}
|
||||
{...restProps}
|
||||
>
|
||||
{@render children?.()}
|
||||
</div>
|
||||
22
src/lib/components/ui/card/index.ts
Normal file
22
src/lib/components/ui/card/index.ts
Normal file
@@ -0,0 +1,22 @@
|
||||
import Root from "./card.svelte";
|
||||
import Content from "./card-content.svelte";
|
||||
import Description from "./card-description.svelte";
|
||||
import Footer from "./card-footer.svelte";
|
||||
import Header from "./card-header.svelte";
|
||||
import Title from "./card-title.svelte";
|
||||
|
||||
export {
|
||||
Root,
|
||||
Content,
|
||||
Description,
|
||||
Footer,
|
||||
Header,
|
||||
Title,
|
||||
//
|
||||
Root as Card,
|
||||
Content as CardContent,
|
||||
Description as CardDescription,
|
||||
Footer as CardFooter,
|
||||
Header as CardHeader,
|
||||
Title as CardTitle,
|
||||
};
|
||||
38
src/lib/components/ui/dialog/dialog-content.svelte
Normal file
38
src/lib/components/ui/dialog/dialog-content.svelte
Normal file
@@ -0,0 +1,38 @@
|
||||
<script lang="ts">
|
||||
import { Dialog as DialogPrimitive, type WithoutChildrenOrChild } from "bits-ui";
|
||||
import X from "lucide-svelte/icons/x";
|
||||
import type { Snippet } from "svelte";
|
||||
import * as Dialog from "./index.js";
|
||||
import { cn } from "$lib/utils.js";
|
||||
|
||||
let {
|
||||
ref = $bindable(null),
|
||||
class: className,
|
||||
portalProps,
|
||||
children,
|
||||
...restProps
|
||||
}: WithoutChildrenOrChild<DialogPrimitive.ContentProps> & {
|
||||
portalProps?: DialogPrimitive.PortalProps;
|
||||
children: Snippet;
|
||||
} = $props();
|
||||
</script>
|
||||
|
||||
<Dialog.Portal {...portalProps}>
|
||||
<Dialog.Overlay />
|
||||
<DialogPrimitive.Content
|
||||
bind:ref
|
||||
class={cn(
|
||||
"bg-background data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 data-[state=closed]:slide-out-to-left-1/2 data-[state=closed]:slide-out-to-top-[48%] data-[state=open]:slide-in-from-left-1/2 data-[state=open]:slide-in-from-top-[48%] fixed left-[50%] top-[50%] z-50 grid w-full max-w-lg translate-x-[-50%] translate-y-[-50%] gap-4 border p-6 shadow-lg duration-200 sm:rounded-lg",
|
||||
className
|
||||
)}
|
||||
{...restProps}
|
||||
>
|
||||
{@render children?.()}
|
||||
<DialogPrimitive.Close
|
||||
class="ring-offset-background focus:ring-ring data-[state=open]:bg-accent data-[state=open]:text-muted-foreground absolute right-4 top-4 rounded-sm opacity-70 transition-opacity hover:opacity-100 focus:outline-none focus:ring-2 focus:ring-offset-2 disabled:pointer-events-none"
|
||||
>
|
||||
<X class="size-4" />
|
||||
<span class="sr-only">Close</span>
|
||||
</DialogPrimitive.Close>
|
||||
</DialogPrimitive.Content>
|
||||
</Dialog.Portal>
|
||||
16
src/lib/components/ui/dialog/dialog-description.svelte
Normal file
16
src/lib/components/ui/dialog/dialog-description.svelte
Normal file
@@ -0,0 +1,16 @@
|
||||
<script lang="ts">
|
||||
import { Dialog as DialogPrimitive } from "bits-ui";
|
||||
import { cn } from "$lib/utils.js";
|
||||
|
||||
let {
|
||||
ref = $bindable(null),
|
||||
class: className,
|
||||
...restProps
|
||||
}: DialogPrimitive.DescriptionProps = $props();
|
||||
</script>
|
||||
|
||||
<DialogPrimitive.Description
|
||||
bind:ref
|
||||
class={cn("text-muted-foreground text-sm", className)}
|
||||
{...restProps}
|
||||
/>
|
||||
20
src/lib/components/ui/dialog/dialog-footer.svelte
Normal file
20
src/lib/components/ui/dialog/dialog-footer.svelte
Normal file
@@ -0,0 +1,20 @@
|
||||
<script lang="ts">
|
||||
import type { WithElementRef } from "bits-ui";
|
||||
import type { HTMLAttributes } from "svelte/elements";
|
||||
import { cn } from "$lib/utils.js";
|
||||
|
||||
let {
|
||||
ref = $bindable(null),
|
||||
class: className,
|
||||
children,
|
||||
...restProps
|
||||
}: WithElementRef<HTMLAttributes<HTMLDivElement>> = $props();
|
||||
</script>
|
||||
|
||||
<div
|
||||
bind:this={ref}
|
||||
class={cn("flex flex-col-reverse sm:flex-row sm:justify-end sm:space-x-2", className)}
|
||||
{...restProps}
|
||||
>
|
||||
{@render children?.()}
|
||||
</div>
|
||||
20
src/lib/components/ui/dialog/dialog-header.svelte
Normal file
20
src/lib/components/ui/dialog/dialog-header.svelte
Normal file
@@ -0,0 +1,20 @@
|
||||
<script lang="ts">
|
||||
import type { HTMLAttributes } from "svelte/elements";
|
||||
import type { WithElementRef } from "bits-ui";
|
||||
import { cn } from "$lib/utils.js";
|
||||
|
||||
let {
|
||||
ref = $bindable(null),
|
||||
class: className,
|
||||
children,
|
||||
...restProps
|
||||
}: WithElementRef<HTMLAttributes<HTMLDivElement>> = $props();
|
||||
</script>
|
||||
|
||||
<div
|
||||
bind:this={ref}
|
||||
class={cn("flex flex-col space-y-1.5 text-center sm:text-left", className)}
|
||||
{...restProps}
|
||||
>
|
||||
{@render children?.()}
|
||||
</div>
|
||||
19
src/lib/components/ui/dialog/dialog-overlay.svelte
Normal file
19
src/lib/components/ui/dialog/dialog-overlay.svelte
Normal file
@@ -0,0 +1,19 @@
|
||||
<script lang="ts">
|
||||
import { Dialog as DialogPrimitive } from "bits-ui";
|
||||
import { cn } from "$lib/utils.js";
|
||||
|
||||
let {
|
||||
ref = $bindable(null),
|
||||
class: className,
|
||||
...restProps
|
||||
}: DialogPrimitive.OverlayProps = $props();
|
||||
</script>
|
||||
|
||||
<DialogPrimitive.Overlay
|
||||
bind:ref
|
||||
class={cn(
|
||||
"data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 fixed inset-0 z-50 bg-black/80",
|
||||
className
|
||||
)}
|
||||
{...restProps}
|
||||
/>
|
||||
16
src/lib/components/ui/dialog/dialog-title.svelte
Normal file
16
src/lib/components/ui/dialog/dialog-title.svelte
Normal file
@@ -0,0 +1,16 @@
|
||||
<script lang="ts">
|
||||
import { Dialog as DialogPrimitive } from "bits-ui";
|
||||
import { cn } from "$lib/utils.js";
|
||||
|
||||
let {
|
||||
ref = $bindable(null),
|
||||
class: className,
|
||||
...restProps
|
||||
}: DialogPrimitive.TitleProps = $props();
|
||||
</script>
|
||||
|
||||
<DialogPrimitive.Title
|
||||
bind:ref
|
||||
class={cn("text-lg font-semibold leading-none tracking-tight", className)}
|
||||
{...restProps}
|
||||
/>
|
||||
37
src/lib/components/ui/dialog/index.ts
Normal file
37
src/lib/components/ui/dialog/index.ts
Normal file
@@ -0,0 +1,37 @@
|
||||
import { Dialog as DialogPrimitive } from "bits-ui";
|
||||
|
||||
import Title from "./dialog-title.svelte";
|
||||
import Footer from "./dialog-footer.svelte";
|
||||
import Header from "./dialog-header.svelte";
|
||||
import Overlay from "./dialog-overlay.svelte";
|
||||
import Content from "./dialog-content.svelte";
|
||||
import Description from "./dialog-description.svelte";
|
||||
|
||||
const Root: typeof DialogPrimitive.Root = DialogPrimitive.Root;
|
||||
const Trigger: typeof DialogPrimitive.Trigger = DialogPrimitive.Trigger;
|
||||
const Close: typeof DialogPrimitive.Close = DialogPrimitive.Close;
|
||||
const Portal: typeof DialogPrimitive.Portal = DialogPrimitive.Portal;
|
||||
|
||||
export {
|
||||
Root,
|
||||
Title,
|
||||
Portal,
|
||||
Footer,
|
||||
Header,
|
||||
Trigger,
|
||||
Overlay,
|
||||
Content,
|
||||
Description,
|
||||
Close,
|
||||
//
|
||||
Root as Dialog,
|
||||
Title as DialogTitle,
|
||||
Portal as DialogPortal,
|
||||
Footer as DialogFooter,
|
||||
Header as DialogHeader,
|
||||
Trigger as DialogTrigger,
|
||||
Overlay as DialogOverlay,
|
||||
Content as DialogContent,
|
||||
Description as DialogDescription,
|
||||
Close as DialogClose,
|
||||
};
|
||||
Reference in New Issue
Block a user