feat: add library icons
This commit is contained in:
@@ -17,6 +17,7 @@ class Library(BigIntAuditBase, SlugKey):
|
|||||||
# Which structure to save the files in the filesystem (i.e {author_name}/{title}.{ext})
|
# Which structure to save the files in the filesystem (i.e {author_name}/{title}.{ext})
|
||||||
path_template: Mapped[str]
|
path_template: Mapped[str]
|
||||||
description: Mapped[Optional[str]]
|
description: Mapped[Optional[str]]
|
||||||
|
icon: Mapped[str] = mapped_column(default="library")
|
||||||
read_only: Mapped[bool] = mapped_column(nullable=False, default=False)
|
read_only: Mapped[bool] = mapped_column(nullable=False, default=False)
|
||||||
|
|
||||||
books: Mapped[list["Book"]] = relationship(back_populates="library")
|
books: Mapped[list["Book"]] = relationship(back_populates="library")
|
||||||
|
|||||||
@@ -8,6 +8,7 @@ class LibraryCreate(BaseModel):
|
|||||||
root_path: str
|
root_path: str
|
||||||
path_template: str | None = "{author}/{title}"
|
path_template: str | None = "{author}/{title}"
|
||||||
description: str | None = None
|
description: str | None = None
|
||||||
|
icon: str = "library"
|
||||||
read_only: bool = False
|
read_only: bool = False
|
||||||
|
|
||||||
@computed_field
|
@computed_field
|
||||||
@@ -21,6 +22,7 @@ class LibraryRead(BaseModel):
|
|||||||
root_path: str
|
root_path: str
|
||||||
path_template: str
|
path_template: str
|
||||||
description: str | None
|
description: str | None
|
||||||
|
icon: str
|
||||||
read_only: bool
|
read_only: bool
|
||||||
total: int | None = None
|
total: int | None = None
|
||||||
|
|
||||||
@@ -30,4 +32,5 @@ class LibraryUpdate(BaseModel):
|
|||||||
root_path: str | None
|
root_path: str | None
|
||||||
path_template: str | None
|
path_template: str | None
|
||||||
description: str | None
|
description: str | None
|
||||||
|
icon: str | None
|
||||||
read_only: bool | None
|
read_only: bool | None
|
||||||
|
|||||||
@@ -5,13 +5,14 @@
|
|||||||
import { Input } from '$lib/components/ui/input/index.js';
|
import { Input } from '$lib/components/ui/input/index.js';
|
||||||
import { Textarea } from '$lib/components/ui/textarea/index';
|
import { Textarea } from '$lib/components/ui/textarea/index';
|
||||||
import { Button } from '$lib/components/ui/button/index.js';
|
import { Button } from '$lib/components/ui/button/index.js';
|
||||||
|
import { IconPicker } from '$lib/components/ui/icon-picker/index.js';
|
||||||
import { toast } from 'svelte-sonner';
|
import { toast } from 'svelte-sonner';
|
||||||
import { goto, invalidate, invalidateAll } from '$app/navigation';
|
|
||||||
import { page } from '$app/state';
|
|
||||||
import { getLibraryState } from '$lib/state/library.svelte';
|
import { getLibraryState } from '$lib/state/library.svelte';
|
||||||
|
|
||||||
let { open = $bindable(false) } = $props();
|
let { open = $bindable(false) } = $props();
|
||||||
|
|
||||||
|
let selectedIcon = $state('library');
|
||||||
|
|
||||||
const libraryState = getLibraryState();
|
const libraryState = getLibraryState();
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
@@ -35,6 +36,7 @@
|
|||||||
const libraryName = createLibrary.fields.name.value();
|
const libraryName = createLibrary.fields.name.value();
|
||||||
|
|
||||||
form.reset();
|
form.reset();
|
||||||
|
selectedIcon = 'library';
|
||||||
open = false;
|
open = false;
|
||||||
toast.success(`Library '${libraryName}' created.`);
|
toast.success(`Library '${libraryName}' created.`);
|
||||||
|
|
||||||
@@ -48,14 +50,24 @@
|
|||||||
>
|
>
|
||||||
<Field.Set>
|
<Field.Set>
|
||||||
<Field.Group>
|
<Field.Group>
|
||||||
<!-- Library name -->
|
<!-- Library name and Icon -->
|
||||||
<Field.Field>
|
<div class="flex items-end gap-3">
|
||||||
<Field.Label for="name">Library name</Field.Label>
|
<div class="flex-1">
|
||||||
<Input {...createLibrary.fields.name.as('text')} />
|
<Field.Field>
|
||||||
{#each createLibrary.fields.name.issues() ?? [] as issue}
|
<Field.Label for="name">Library name</Field.Label>
|
||||||
<Field.Error>{issue.message}</Field.Error>
|
<Input {...createLibrary.fields.name.as('text')} />
|
||||||
{/each}
|
{#each createLibrary.fields.name.issues() ?? [] as issue}
|
||||||
</Field.Field>
|
<Field.Error>{issue.message}</Field.Error>
|
||||||
|
{/each}
|
||||||
|
</Field.Field>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="flex flex-col gap-2">
|
||||||
|
<Field.Label>Icon</Field.Label>
|
||||||
|
<IconPicker bind:value={selectedIcon} />
|
||||||
|
<input type="hidden" name="icon" value={selectedIcon} />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
<!-- Description -->
|
<!-- Description -->
|
||||||
<Field.Field>
|
<Field.Field>
|
||||||
|
|||||||
@@ -4,12 +4,15 @@
|
|||||||
import { useSidebar } from '$lib/components/ui/sidebar/index.js';
|
import { useSidebar } from '$lib/components/ui/sidebar/index.js';
|
||||||
import { Badge } from "$lib/components/ui/badge/index.js";
|
import { Badge } from "$lib/components/ui/badge/index.js";
|
||||||
import { getLibraryState, LibraryState } from '$lib/state/library.svelte';
|
import { getLibraryState, LibraryState } from '$lib/state/library.svelte';
|
||||||
|
import { LIBRARY_ICONS } from '$lib/components/ui/icon-picker/index.js';
|
||||||
import ChevronsUpDownIcon from '@lucide/svelte/icons/chevrons-up-down';
|
import ChevronsUpDownIcon from '@lucide/svelte/icons/chevrons-up-down';
|
||||||
import PlusIcon from '@lucide/svelte/icons/plus';
|
import PlusIcon from '@lucide/svelte/icons/plus';
|
||||||
import LibraryCreateForm from '../forms/library-create-form.svelte';
|
import LibraryCreateForm from '../forms/library-create-form.svelte';
|
||||||
|
|
||||||
const libraryState = getLibraryState();
|
const libraryState = getLibraryState();
|
||||||
const sidebar = useSidebar();
|
const sidebar = useSidebar();
|
||||||
|
|
||||||
|
const activeIcon = $derived(LIBRARY_ICONS[libraryState.activeLibrary?.icon ?? 'library'] ?? LIBRARY_ICONS['library']);
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<Sidebar.Menu>
|
<Sidebar.Menu>
|
||||||
@@ -22,11 +25,11 @@
|
|||||||
size="lg"
|
size="lg"
|
||||||
class="ml-2 data-[state=open]:bg-sidebar-accent data-[state=open]:text-sidebar-accent-foreground"
|
class="ml-2 data-[state=open]:bg-sidebar-accent data-[state=open]:text-sidebar-accent-foreground"
|
||||||
>
|
>
|
||||||
|
{@const ActiveIcon = activeIcon.component}
|
||||||
<div
|
<div
|
||||||
class="flex aspect-square size-8 items-center justify-center rounded-lg bg-sidebar-primary text-sidebar-primary-foreground"
|
class="flex aspect-square size-8 items-center justify-center rounded-lg bg-sidebar-primary text-sidebar-primary-foreground"
|
||||||
>
|
>
|
||||||
<p class="text-lg font-semibold">{libraryState.activeLibrary!.name[0]}</p>
|
<ActiveIcon class="size-4" />
|
||||||
<!-- <libraryState?.activeLibrary.logo class="size-4" /> -->
|
|
||||||
</div>
|
</div>
|
||||||
<div class="grid flex-1 text-left text-sm leading-tight">
|
<div class="grid flex-1 text-left text-sm leading-tight">
|
||||||
<span class="ml-1 truncate font-semibold">
|
<span class="ml-1 truncate font-semibold">
|
||||||
@@ -45,16 +48,16 @@
|
|||||||
>
|
>
|
||||||
<DropdownMenu.Label class="text-xs text-muted-foreground">Libraries</DropdownMenu.Label>
|
<DropdownMenu.Label class="text-xs text-muted-foreground">Libraries</DropdownMenu.Label>
|
||||||
{#each libraryState.libraries as library (library.name)}
|
{#each libraryState.libraries as library (library.name)}
|
||||||
|
{@const LibraryIcon = (LIBRARY_ICONS[library.icon ?? 'library'] ?? LIBRARY_ICONS['library']).component}
|
||||||
<DropdownMenu.Item onSelect={() => libraryState.setActive(library.id)} class="gap-2 p-2">
|
<DropdownMenu.Item onSelect={() => libraryState.setActive(library.id)} class="gap-2 p-2">
|
||||||
<div class="flex size-6 items-center justify-center rounded-md border">
|
<div class="flex size-6 items-center justify-center rounded-md border">
|
||||||
<p>{library.name[0]}</p>
|
<LibraryIcon class="size-3.5 shrink-0" />
|
||||||
<!-- <library.logo class="size-3.5 shrink-0" /> -->
|
|
||||||
</div>
|
</div>
|
||||||
{library.name}
|
{library.name}
|
||||||
<Badge
|
<Badge
|
||||||
variant="outline"
|
variant="outline"
|
||||||
class="font-semibold ml-auto">
|
class="font-semibold ml-auto">
|
||||||
{library.total}
|
{library.total ?? 0}
|
||||||
</Badge>
|
</Badge>
|
||||||
</DropdownMenu.Item>
|
</DropdownMenu.Item>
|
||||||
{/each}
|
{/each}
|
||||||
|
|||||||
@@ -0,0 +1,75 @@
|
|||||||
|
<script lang="ts">
|
||||||
|
import * as Popover from '$lib/components/ui/popover/index.js';
|
||||||
|
import * as Tooltip from '$lib/components/ui/tooltip/index.js';
|
||||||
|
import { Button } from '$lib/components/ui/button/index.js';
|
||||||
|
import { Input } from '$lib/components/ui/input/index.js';
|
||||||
|
import { ScrollArea } from '$lib/components/ui/scroll-area/index.js';
|
||||||
|
import { LIBRARY_ICONS } from './library-icons.js';
|
||||||
|
|
||||||
|
let {
|
||||||
|
value = $bindable('library')
|
||||||
|
}: {
|
||||||
|
value?: string;
|
||||||
|
} = $props();
|
||||||
|
|
||||||
|
let open = $state(false);
|
||||||
|
let search = $state('');
|
||||||
|
|
||||||
|
const filteredIcons = $derived.by(() => {
|
||||||
|
if (!search) return Object.entries(LIBRARY_ICONS);
|
||||||
|
const query = search.toLowerCase();
|
||||||
|
return Object.entries(LIBRARY_ICONS).filter(
|
||||||
|
([key, icon]) =>
|
||||||
|
key.includes(query) ||
|
||||||
|
icon.label.toLowerCase().includes(query) ||
|
||||||
|
icon.category.toLowerCase().includes(query)
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
function selectIcon(iconKey: string) {
|
||||||
|
value = iconKey;
|
||||||
|
open = false;
|
||||||
|
search = '';
|
||||||
|
}
|
||||||
|
|
||||||
|
const selectedIcon = $derived(LIBRARY_ICONS[value] ?? LIBRARY_ICONS['library']);
|
||||||
|
const SelectedIconComponent = $derived(selectedIcon.component);
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<Popover.Root bind:open>
|
||||||
|
<Popover.Trigger asChild>
|
||||||
|
{#snippet child({ props })}
|
||||||
|
<Button variant="outline" size="icon" class="h-9 w-9" {...props}>
|
||||||
|
<SelectedIconComponent class="size-4" />
|
||||||
|
</Button>
|
||||||
|
{/snippet}
|
||||||
|
</Popover.Trigger>
|
||||||
|
<Popover.Content class="w-64 p-3" align="start">
|
||||||
|
<Input
|
||||||
|
bind:value={search}
|
||||||
|
placeholder="Search icons..."
|
||||||
|
class="mb-3 h-8"
|
||||||
|
/>
|
||||||
|
<ScrollArea class="h-48">
|
||||||
|
<div class="grid grid-cols-6 gap-1">
|
||||||
|
{#each filteredIcons as [key, icon] (key)}
|
||||||
|
{@const IconComponent = icon.component}
|
||||||
|
<Tooltip.Root>
|
||||||
|
<Tooltip.Trigger>
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
class="flex size-8 items-center justify-center rounded-md hover:bg-accent {value === key ? 'bg-accent' : ''}"
|
||||||
|
onclick={() => selectIcon(key)}
|
||||||
|
>
|
||||||
|
<IconComponent class="size-4" />
|
||||||
|
</button>
|
||||||
|
</Tooltip.Trigger>
|
||||||
|
<Tooltip.Content>
|
||||||
|
<p>{icon.label}</p>
|
||||||
|
</Tooltip.Content>
|
||||||
|
</Tooltip.Root>
|
||||||
|
{/each}
|
||||||
|
</div>
|
||||||
|
</ScrollArea>
|
||||||
|
</Popover.Content>
|
||||||
|
</Popover.Root>
|
||||||
@@ -0,0 +1,4 @@
|
|||||||
|
import IconPicker from './icon-picker.svelte';
|
||||||
|
export { LIBRARY_ICONS, getIconComponent, type IconName } from './library-icons.js';
|
||||||
|
|
||||||
|
export { IconPicker };
|
||||||
@@ -0,0 +1,147 @@
|
|||||||
|
import {
|
||||||
|
Library,
|
||||||
|
BookOpen,
|
||||||
|
BookMarked,
|
||||||
|
Book,
|
||||||
|
Bookmark,
|
||||||
|
FolderOpen,
|
||||||
|
Code,
|
||||||
|
Terminal,
|
||||||
|
Cpu,
|
||||||
|
Server,
|
||||||
|
Database,
|
||||||
|
Binary,
|
||||||
|
Braces,
|
||||||
|
GraduationCap,
|
||||||
|
FlaskConical,
|
||||||
|
Microscope,
|
||||||
|
Atom,
|
||||||
|
Dna,
|
||||||
|
Brain,
|
||||||
|
FileText,
|
||||||
|
ScrollText,
|
||||||
|
Calculator,
|
||||||
|
Compass,
|
||||||
|
Ruler,
|
||||||
|
PencilRuler,
|
||||||
|
Sigma,
|
||||||
|
Image,
|
||||||
|
Palette,
|
||||||
|
Sparkles,
|
||||||
|
Zap,
|
||||||
|
Feather,
|
||||||
|
PenLine,
|
||||||
|
Quote,
|
||||||
|
Wand2,
|
||||||
|
Sword,
|
||||||
|
Crown,
|
||||||
|
Flame,
|
||||||
|
Shield,
|
||||||
|
Rocket,
|
||||||
|
Satellite,
|
||||||
|
Globe,
|
||||||
|
Search,
|
||||||
|
Eye,
|
||||||
|
Fingerprint,
|
||||||
|
Heart,
|
||||||
|
HeartHandshake,
|
||||||
|
Landmark,
|
||||||
|
Scroll,
|
||||||
|
Archive,
|
||||||
|
Clock,
|
||||||
|
Star,
|
||||||
|
Rainbow,
|
||||||
|
Baby,
|
||||||
|
Headphones,
|
||||||
|
Music
|
||||||
|
} from '@lucide/svelte';
|
||||||
|
import type { Component } from 'svelte';
|
||||||
|
|
||||||
|
export type IconName = keyof typeof LIBRARY_ICONS;
|
||||||
|
|
||||||
|
export const LIBRARY_ICONS: Record<string, { component: Component; label: string; category: string }> = {
|
||||||
|
// Generic
|
||||||
|
library: { component: Library, label: 'Library', category: 'Generic' },
|
||||||
|
'book-open': { component: BookOpen, label: 'Book Open', category: 'Generic' },
|
||||||
|
'book-marked': { component: BookMarked, label: 'Book Marked', category: 'Generic' },
|
||||||
|
book: { component: Book, label: 'Book', category: 'Generic' },
|
||||||
|
bookmark: { component: Bookmark, label: 'Bookmark', category: 'Generic' },
|
||||||
|
'folder-open': { component: FolderOpen, label: 'Folder Open', category: 'Generic' },
|
||||||
|
|
||||||
|
// Technical
|
||||||
|
code: { component: Code, label: 'Code', category: 'Technical' },
|
||||||
|
terminal: { component: Terminal, label: 'Terminal', category: 'Technical' },
|
||||||
|
cpu: { component: Cpu, label: 'CPU', category: 'Technical' },
|
||||||
|
server: { component: Server, label: 'Server', category: 'Technical' },
|
||||||
|
database: { component: Database, label: 'Database', category: 'Technical' },
|
||||||
|
binary: { component: Binary, label: 'Binary', category: 'Technical' },
|
||||||
|
braces: { component: Braces, label: 'Braces', category: 'Technical' },
|
||||||
|
|
||||||
|
// Academic
|
||||||
|
'graduation-cap': { component: GraduationCap, label: 'Graduation Cap', category: 'Academic' },
|
||||||
|
'flask-conical': { component: FlaskConical, label: 'Flask', category: 'Academic' },
|
||||||
|
microscope: { component: Microscope, label: 'Microscope', category: 'Academic' },
|
||||||
|
atom: { component: Atom, label: 'Atom', category: 'Academic' },
|
||||||
|
dna: { component: Dna, label: 'DNA', category: 'Academic' },
|
||||||
|
brain: { component: Brain, label: 'Brain', category: 'Academic' },
|
||||||
|
'file-text': { component: FileText, label: 'File Text', category: 'Academic' },
|
||||||
|
'scroll-text': { component: ScrollText, label: 'Scroll Text', category: 'Academic' },
|
||||||
|
|
||||||
|
// Math/Engineering
|
||||||
|
calculator: { component: Calculator, label: 'Calculator', category: 'Math' },
|
||||||
|
compass: { component: Compass, label: 'Compass', category: 'Math' },
|
||||||
|
ruler: { component: Ruler, label: 'Ruler', category: 'Math' },
|
||||||
|
'pencil-ruler': { component: PencilRuler, label: 'Pencil Ruler', category: 'Math' },
|
||||||
|
sigma: { component: Sigma, label: 'Sigma', category: 'Math' },
|
||||||
|
|
||||||
|
// Comics/Manga
|
||||||
|
image: { component: Image, label: 'Image', category: 'Comics' },
|
||||||
|
palette: { component: Palette, label: 'Palette', category: 'Comics' },
|
||||||
|
sparkles: { component: Sparkles, label: 'Sparkles', category: 'Comics' },
|
||||||
|
zap: { component: Zap, label: 'Zap', category: 'Comics' },
|
||||||
|
|
||||||
|
// Fiction
|
||||||
|
feather: { component: Feather, label: 'Feather', category: 'Fiction' },
|
||||||
|
'pen-line': { component: PenLine, label: 'Pen', category: 'Fiction' },
|
||||||
|
quote: { component: Quote, label: 'Quote', category: 'Fiction' },
|
||||||
|
|
||||||
|
// Fantasy
|
||||||
|
wand: { component: Wand2, label: 'Wand', category: 'Fantasy' },
|
||||||
|
sword: { component: Sword, label: 'Sword', category: 'Fantasy' },
|
||||||
|
crown: { component: Crown, label: 'Crown', category: 'Fantasy' },
|
||||||
|
flame: { component: Flame, label: 'Flame', category: 'Fantasy' },
|
||||||
|
shield: { component: Shield, label: 'Shield', category: 'Fantasy' },
|
||||||
|
|
||||||
|
// Sci-Fi
|
||||||
|
rocket: { component: Rocket, label: 'Rocket', category: 'Sci-Fi' },
|
||||||
|
satellite: { component: Satellite, label: 'Satellite', category: 'Sci-Fi' },
|
||||||
|
globe: { component: Globe, label: 'Globe', category: 'Sci-Fi' },
|
||||||
|
|
||||||
|
// Mystery
|
||||||
|
search: { component: Search, label: 'Search', category: 'Mystery' },
|
||||||
|
eye: { component: Eye, label: 'Eye', category: 'Mystery' },
|
||||||
|
fingerprint: { component: Fingerprint, label: 'Fingerprint', category: 'Mystery' },
|
||||||
|
|
||||||
|
// Romance
|
||||||
|
heart: { component: Heart, label: 'Heart', category: 'Romance' },
|
||||||
|
'heart-handshake': { component: HeartHandshake, label: 'Heart Handshake', category: 'Romance' },
|
||||||
|
|
||||||
|
// History
|
||||||
|
landmark: { component: Landmark, label: 'Landmark', category: 'History' },
|
||||||
|
scroll: { component: Scroll, label: 'Scroll', category: 'History' },
|
||||||
|
archive: { component: Archive, label: 'Archive', category: 'History' },
|
||||||
|
clock: { component: Clock, label: 'Clock', category: 'History' },
|
||||||
|
|
||||||
|
// Children's
|
||||||
|
star: { component: Star, label: 'Star', category: "Children's" },
|
||||||
|
rainbow: { component: Rainbow, label: 'Rainbow', category: "Children's" },
|
||||||
|
baby: { component: Baby, label: 'Baby', category: "Children's" },
|
||||||
|
|
||||||
|
// Audio
|
||||||
|
headphones: { component: Headphones, label: 'Headphones', category: 'Audio' },
|
||||||
|
music: { component: Music, label: 'Music', category: 'Audio' }
|
||||||
|
};
|
||||||
|
|
||||||
|
export function getIconComponent(name: string): Component {
|
||||||
|
return LIBRARY_ICONS[name]?.component ?? LIBRARY_ICONS['library'].component;
|
||||||
|
}
|
||||||
@@ -0,0 +1,19 @@
|
|||||||
|
import Root from "./popover.svelte";
|
||||||
|
import Close from "./popover-close.svelte";
|
||||||
|
import Content from "./popover-content.svelte";
|
||||||
|
import Trigger from "./popover-trigger.svelte";
|
||||||
|
import Portal from "./popover-portal.svelte";
|
||||||
|
|
||||||
|
export {
|
||||||
|
Root,
|
||||||
|
Content,
|
||||||
|
Trigger,
|
||||||
|
Close,
|
||||||
|
Portal,
|
||||||
|
//
|
||||||
|
Root as Popover,
|
||||||
|
Content as PopoverContent,
|
||||||
|
Trigger as PopoverTrigger,
|
||||||
|
Close as PopoverClose,
|
||||||
|
Portal as PopoverPortal,
|
||||||
|
};
|
||||||
@@ -0,0 +1,7 @@
|
|||||||
|
<script lang="ts">
|
||||||
|
import { Popover as PopoverPrimitive } from "bits-ui";
|
||||||
|
|
||||||
|
let { ref = $bindable(null), ...restProps }: PopoverPrimitive.CloseProps = $props();
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<PopoverPrimitive.Close bind:ref data-slot="popover-close" {...restProps} />
|
||||||
@@ -0,0 +1,31 @@
|
|||||||
|
<script lang="ts">
|
||||||
|
import { Popover as PopoverPrimitive } from "bits-ui";
|
||||||
|
import PopoverPortal from "./popover-portal.svelte";
|
||||||
|
import { cn, type WithoutChildrenOrChild } from "$lib/utils.js";
|
||||||
|
import type { ComponentProps } from "svelte";
|
||||||
|
|
||||||
|
let {
|
||||||
|
ref = $bindable(null),
|
||||||
|
class: className,
|
||||||
|
sideOffset = 4,
|
||||||
|
align = "center",
|
||||||
|
portalProps,
|
||||||
|
...restProps
|
||||||
|
}: PopoverPrimitive.ContentProps & {
|
||||||
|
portalProps?: WithoutChildrenOrChild<ComponentProps<typeof PopoverPortal>>;
|
||||||
|
} = $props();
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<PopoverPortal {...portalProps}>
|
||||||
|
<PopoverPrimitive.Content
|
||||||
|
bind:ref
|
||||||
|
data-slot="popover-content"
|
||||||
|
{sideOffset}
|
||||||
|
{align}
|
||||||
|
class={cn(
|
||||||
|
"bg-popover text-popover-foreground 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-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-end-2 data-[side=right]:slide-in-from-start-2 data-[side=top]:slide-in-from-bottom-2 z-50 w-72 origin-(--bits-popover-content-transform-origin) rounded-md border p-4 shadow-md outline-hidden",
|
||||||
|
className
|
||||||
|
)}
|
||||||
|
{...restProps}
|
||||||
|
/>
|
||||||
|
</PopoverPortal>
|
||||||
@@ -0,0 +1,7 @@
|
|||||||
|
<script lang="ts">
|
||||||
|
import { Popover as PopoverPrimitive } from "bits-ui";
|
||||||
|
|
||||||
|
let { ...restProps }: PopoverPrimitive.PortalProps = $props();
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<PopoverPrimitive.Portal {...restProps} />
|
||||||
@@ -0,0 +1,17 @@
|
|||||||
|
<script lang="ts">
|
||||||
|
import { cn } from "$lib/utils.js";
|
||||||
|
import { Popover as PopoverPrimitive } from "bits-ui";
|
||||||
|
|
||||||
|
let {
|
||||||
|
ref = $bindable(null),
|
||||||
|
class: className,
|
||||||
|
...restProps
|
||||||
|
}: PopoverPrimitive.TriggerProps = $props();
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<PopoverPrimitive.Trigger
|
||||||
|
bind:ref
|
||||||
|
data-slot="popover-trigger"
|
||||||
|
class={cn("", className)}
|
||||||
|
{...restProps}
|
||||||
|
/>
|
||||||
@@ -0,0 +1,7 @@
|
|||||||
|
<script lang="ts">
|
||||||
|
import { Popover as PopoverPrimitive } from "bits-ui";
|
||||||
|
|
||||||
|
let { open = $bindable(false), ...restProps }: PopoverPrimitive.RootProps = $props();
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<PopoverPrimitive.Root bind:open {...restProps} />
|
||||||
@@ -13,7 +13,8 @@ export const libraryCreateSchema = z.object({
|
|||||||
name: z.string().min(1, 'Library must have a name'),
|
name: z.string().min(1, 'Library must have a name'),
|
||||||
description: z.string().optional(),
|
description: z.string().optional(),
|
||||||
root_path: z.string().min(1, 'Must specify a root path'),
|
root_path: z.string().min(1, 'Must specify a root path'),
|
||||||
path_template: z.string().optional().default('/{author}/{title}')
|
path_template: z.string().optional().default('/{author}/{title}'),
|
||||||
|
icon: z.string().default('library')
|
||||||
});
|
});
|
||||||
|
|
||||||
export type LibraryQuerySchema = typeof libraryQuerySchema;
|
export type LibraryQuerySchema = typeof libraryQuerySchema;
|
||||||
|
|||||||
Reference in New Issue
Block a user