From 9fe69641c5a61f5fd48d6bbc6c56168a3a007b61 Mon Sep 17 00:00:00 2001 From: patrick Date: Mon, 10 Aug 2026 23:58:02 -0400 Subject: [PATCH] feat: add library icons --- backend/src/chitai/database/models/library.py | 1 + backend/src/chitai/schemas/library.py | 3 + .../forms/library-create-form.svelte | 32 ++-- .../components/layout/library-switcher.svelte | 15 +- .../ui/icon-picker/icon-picker.svelte | 75 +++++++++ .../lib/components/ui/icon-picker/index.ts | 4 + .../ui/icon-picker/library-icons.ts | 147 ++++++++++++++++++ .../src/lib/components/ui/popover/index.ts | 19 +++ .../ui/popover/popover-close.svelte | 7 + .../ui/popover/popover-content.svelte | 31 ++++ .../ui/popover/popover-portal.svelte | 7 + .../ui/popover/popover-trigger.svelte | 17 ++ .../lib/components/ui/popover/popover.svelte | 7 + frontend/src/lib/schema/library.ts | 3 +- 14 files changed, 351 insertions(+), 17 deletions(-) create mode 100644 frontend/src/lib/components/ui/icon-picker/icon-picker.svelte create mode 100644 frontend/src/lib/components/ui/icon-picker/index.ts create mode 100644 frontend/src/lib/components/ui/icon-picker/library-icons.ts create mode 100644 frontend/src/lib/components/ui/popover/index.ts create mode 100644 frontend/src/lib/components/ui/popover/popover-close.svelte create mode 100644 frontend/src/lib/components/ui/popover/popover-content.svelte create mode 100644 frontend/src/lib/components/ui/popover/popover-portal.svelte create mode 100644 frontend/src/lib/components/ui/popover/popover-trigger.svelte create mode 100644 frontend/src/lib/components/ui/popover/popover.svelte diff --git a/backend/src/chitai/database/models/library.py b/backend/src/chitai/database/models/library.py index 049a7f7..45b2b31 100644 --- a/backend/src/chitai/database/models/library.py +++ b/backend/src/chitai/database/models/library.py @@ -17,6 +17,7 @@ class Library(BigIntAuditBase, SlugKey): # Which structure to save the files in the filesystem (i.e {author_name}/{title}.{ext}) path_template: Mapped[str] description: Mapped[Optional[str]] + icon: Mapped[str] = mapped_column(default="library") read_only: Mapped[bool] = mapped_column(nullable=False, default=False) books: Mapped[list["Book"]] = relationship(back_populates="library") diff --git a/backend/src/chitai/schemas/library.py b/backend/src/chitai/schemas/library.py index 0cfcc2e..36232f5 100644 --- a/backend/src/chitai/schemas/library.py +++ b/backend/src/chitai/schemas/library.py @@ -8,6 +8,7 @@ class LibraryCreate(BaseModel): root_path: str path_template: str | None = "{author}/{title}" description: str | None = None + icon: str = "library" read_only: bool = False @computed_field @@ -21,6 +22,7 @@ class LibraryRead(BaseModel): root_path: str path_template: str description: str | None + icon: str read_only: bool total: int | None = None @@ -30,4 +32,5 @@ class LibraryUpdate(BaseModel): root_path: str | None path_template: str | None description: str | None + icon: str | None read_only: bool | None diff --git a/frontend/src/lib/components/forms/library-create-form.svelte b/frontend/src/lib/components/forms/library-create-form.svelte index 4f67174..70817d5 100644 --- a/frontend/src/lib/components/forms/library-create-form.svelte +++ b/frontend/src/lib/components/forms/library-create-form.svelte @@ -5,13 +5,14 @@ import { Input } from '$lib/components/ui/input/index.js'; import { Textarea } from '$lib/components/ui/textarea/index'; 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 { goto, invalidate, invalidateAll } from '$app/navigation'; - import { page } from '$app/state'; import { getLibraryState } from '$lib/state/library.svelte'; let { open = $bindable(false) } = $props(); + let selectedIcon = $state('library'); + const libraryState = getLibraryState(); @@ -35,6 +36,7 @@ const libraryName = createLibrary.fields.name.value(); form.reset(); + selectedIcon = 'library'; open = false; toast.success(`Library '${libraryName}' created.`); @@ -48,14 +50,24 @@ > - - - Library name - - {#each createLibrary.fields.name.issues() ?? [] as issue} - {issue.message} - {/each} - + +
+
+ + Library name + + {#each createLibrary.fields.name.issues() ?? [] as issue} + {issue.message} + {/each} + +
+ +
+ Icon + + +
+
diff --git a/frontend/src/lib/components/layout/library-switcher.svelte b/frontend/src/lib/components/layout/library-switcher.svelte index 91ca44c..d202494 100644 --- a/frontend/src/lib/components/layout/library-switcher.svelte +++ b/frontend/src/lib/components/layout/library-switcher.svelte @@ -4,12 +4,15 @@ import { useSidebar } from '$lib/components/ui/sidebar/index.js'; import { Badge } from "$lib/components/ui/badge/index.js"; 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 PlusIcon from '@lucide/svelte/icons/plus'; import LibraryCreateForm from '../forms/library-create-form.svelte'; const libraryState = getLibraryState(); const sidebar = useSidebar(); + + const activeIcon = $derived(LIBRARY_ICONS[libraryState.activeLibrary?.icon ?? 'library'] ?? LIBRARY_ICONS['library']); @@ -22,11 +25,11 @@ size="lg" class="ml-2 data-[state=open]:bg-sidebar-accent data-[state=open]:text-sidebar-accent-foreground" > + {@const ActiveIcon = activeIcon.component}
-

{libraryState.activeLibrary!.name[0]}

- +
@@ -45,16 +48,16 @@ > Libraries {#each libraryState.libraries as library (library.name)} + {@const LibraryIcon = (LIBRARY_ICONS[library.icon ?? 'library'] ?? LIBRARY_ICONS['library']).component} libraryState.setActive(library.id)} class="gap-2 p-2">
-

{library.name[0]}

- +
{library.name} - - {library.total} + {library.total ?? 0}
{/each} diff --git a/frontend/src/lib/components/ui/icon-picker/icon-picker.svelte b/frontend/src/lib/components/ui/icon-picker/icon-picker.svelte new file mode 100644 index 0000000..9d236db --- /dev/null +++ b/frontend/src/lib/components/ui/icon-picker/icon-picker.svelte @@ -0,0 +1,75 @@ + + + + + {#snippet child({ props })} + + {/snippet} + + + + +
+ {#each filteredIcons as [key, icon] (key)} + {@const IconComponent = icon.component} + + + + + +

{icon.label}

+
+
+ {/each} +
+
+
+
diff --git a/frontend/src/lib/components/ui/icon-picker/index.ts b/frontend/src/lib/components/ui/icon-picker/index.ts new file mode 100644 index 0000000..b00e218 --- /dev/null +++ b/frontend/src/lib/components/ui/icon-picker/index.ts @@ -0,0 +1,4 @@ +import IconPicker from './icon-picker.svelte'; +export { LIBRARY_ICONS, getIconComponent, type IconName } from './library-icons.js'; + +export { IconPicker }; diff --git a/frontend/src/lib/components/ui/icon-picker/library-icons.ts b/frontend/src/lib/components/ui/icon-picker/library-icons.ts new file mode 100644 index 0000000..16d35d3 --- /dev/null +++ b/frontend/src/lib/components/ui/icon-picker/library-icons.ts @@ -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 = { + // 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; +} diff --git a/frontend/src/lib/components/ui/popover/index.ts b/frontend/src/lib/components/ui/popover/index.ts new file mode 100644 index 0000000..b79d12e --- /dev/null +++ b/frontend/src/lib/components/ui/popover/index.ts @@ -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, +}; diff --git a/frontend/src/lib/components/ui/popover/popover-close.svelte b/frontend/src/lib/components/ui/popover/popover-close.svelte new file mode 100644 index 0000000..c360925 --- /dev/null +++ b/frontend/src/lib/components/ui/popover/popover-close.svelte @@ -0,0 +1,7 @@ + + + diff --git a/frontend/src/lib/components/ui/popover/popover-content.svelte b/frontend/src/lib/components/ui/popover/popover-content.svelte new file mode 100644 index 0000000..3d79f3c --- /dev/null +++ b/frontend/src/lib/components/ui/popover/popover-content.svelte @@ -0,0 +1,31 @@ + + + + + diff --git a/frontend/src/lib/components/ui/popover/popover-portal.svelte b/frontend/src/lib/components/ui/popover/popover-portal.svelte new file mode 100644 index 0000000..dd8265f --- /dev/null +++ b/frontend/src/lib/components/ui/popover/popover-portal.svelte @@ -0,0 +1,7 @@ + + + diff --git a/frontend/src/lib/components/ui/popover/popover-trigger.svelte b/frontend/src/lib/components/ui/popover/popover-trigger.svelte new file mode 100644 index 0000000..586323c --- /dev/null +++ b/frontend/src/lib/components/ui/popover/popover-trigger.svelte @@ -0,0 +1,17 @@ + + + diff --git a/frontend/src/lib/components/ui/popover/popover.svelte b/frontend/src/lib/components/ui/popover/popover.svelte new file mode 100644 index 0000000..6b1aa5f --- /dev/null +++ b/frontend/src/lib/components/ui/popover/popover.svelte @@ -0,0 +1,7 @@ + + + diff --git a/frontend/src/lib/schema/library.ts b/frontend/src/lib/schema/library.ts index 62e7a61..72b6463 100644 --- a/frontend/src/lib/schema/library.ts +++ b/frontend/src/lib/schema/library.ts @@ -13,7 +13,8 @@ export const libraryCreateSchema = z.object({ name: z.string().min(1, 'Library must have a name'), description: z.string().optional(), 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;