diff --git a/design/icon-picker-sezioni.pen b/design/icon-picker-sezioni.pen new file mode 100644 index 00000000..a359f74f --- /dev/null +++ b/design/icon-picker-sezioni.pen @@ -0,0 +1,382 @@ +{ + "version": "2.8", + "children": [ + { + "type": "frame", + "id": "ips01", + "x": 0, + "y": 0, + "name": "Icon Picker โ€” Full Grid (280+ icons)", + "theme": { "Mode": "Light" }, + "clip": true, + "width": 320, + "height": 480, + "fill": "$--background", + "layout": "vertical", + "padding": [12, 12], + "gap": 8, + "children": [ + { + "type": "text", + "id": "ips02", + "content": "COLOR", + "fontSize": 10, + "fontWeight": 400, + "fontFamily": "IBM Plex Mono", + "textTransform": "uppercase", + "fill": "$--muted-foreground" + }, + { + "type": "frame", + "id": "ips03", + "name": "colorRow", + "layout": "horizontal", + "gap": 8, + "children": [ + { "type": "ellipse", "id": "ips03a", "fill": "#E53E3E", "width": 24, "height": 24 }, + { "type": "ellipse", "id": "ips03b", "fill": "#D9730D", "width": 24, "height": 24 }, + { "type": "ellipse", "id": "ips03c", "fill": "#D69E2E", "width": 24, "height": 24 }, + { "type": "ellipse", "id": "ips03d", "fill": "#38A169", "width": 24, "height": 24 }, + { "type": "ellipse", "id": "ips03e", "fill": "#155DFF", "width": 24, "height": 24 }, + { "type": "ellipse", "id": "ips03f", "fill": "#805AD5", "width": 24, "height": 24 }, + { "type": "ellipse", "id": "ips03g", "fill": "#319795", "width": 24, "height": 24 }, + { "type": "ellipse", "id": "ips03h", "fill": "#D53F8C", "width": 24, "height": 24 } + ] + }, + { + "type": "text", + "id": "ips04", + "content": "ICON", + "fontSize": 10, + "fontWeight": 400, + "fontFamily": "IBM Plex Mono", + "textTransform": "uppercase", + "fill": "$--muted-foreground" + }, + { + "type": "frame", + "id": "ips05", + "name": "searchField", + "width": "fill_container", + "height": 28, + "fill": "$--background", + "stroke": { "align": "inside", "thickness": 1, "fill": "$--border" }, + "cornerRadius": 4, + "layout": "horizontal", + "alignItems": "center", + "padding": [0, 8], + "gap": 6, + "children": [ + { + "type": "text", + "id": "ips05a", + "content": "๐Ÿ”", + "fontSize": 12 + }, + { + "type": "text", + "id": "ips05b", + "content": "Search iconsโ€ฆ", + "fontSize": 12, + "fill": "$--muted-foreground" + } + ] + }, + { + "type": "frame", + "id": "ips06", + "name": "iconGrid", + "width": "fill_container", + "height": 300, + "layout": "horizontal", + "wrap": true, + "gap": 4, + "overflow": "scroll", + "children": [ + { "type": "frame", "id": "ig01", "width": 30, "height": 30, "cornerRadius": 4, "fill": "$--muted", "children": [{ "type": "text", "id": "igl01", "content": "๐Ÿ“„", "fontSize": 14 }] }, + { "type": "frame", "id": "ig02", "width": 30, "height": 30, "cornerRadius": 4, "fill": "transparent", "children": [{ "type": "text", "id": "igl02", "content": "๐Ÿ”ง", "fontSize": 14 }] }, + { "type": "frame", "id": "ig03", "width": 30, "height": 30, "cornerRadius": 4, "fill": "transparent", "children": [{ "type": "text", "id": "igl03", "content": "โš—๏ธ", "fontSize": 14 }] }, + { "type": "frame", "id": "ig04", "width": 30, "height": 30, "cornerRadius": 4, "fill": "transparent", "children": [{ "type": "text", "id": "igl04", "content": "๐ŸŽฏ", "fontSize": 14 }] }, + { "type": "frame", "id": "ig05", "width": 30, "height": 30, "cornerRadius": 4, "fill": "transparent", "children": [{ "type": "text", "id": "igl05", "content": "๐Ÿ ", "fontSize": 14 }] }, + { "type": "frame", "id": "ig06", "width": 30, "height": 30, "cornerRadius": 4, "fill": "transparent", "children": [{ "type": "text", "id": "igl06", "content": "โญ", "fontSize": 14 }] }, + { "type": "frame", "id": "ig07", "width": 30, "height": 30, "cornerRadius": 4, "fill": "transparent", "children": [{ "type": "text", "id": "igl07", "content": "๐Ÿ“š", "fontSize": 14 }] }, + { "type": "frame", "id": "ig08", "width": 30, "height": 30, "cornerRadius": 4, "fill": "transparent", "children": [{ "type": "text", "id": "igl08", "content": "โœˆ๏ธ", "fontSize": 14 }] }, + { + "type": "text", + "id": "ig09", + "content": "โ€ฆ 280+ icons in scrollable grid", + "fontSize": 10, + "fill": "$--muted-foreground", + "width": "fill_container", + "textAlign": "center" + } + ] + }, + { + "type": "frame", + "id": "ips07", + "name": "doneRow", + "width": "fill_container", + "layout": "horizontal", + "justifyContent": "flex-end", + "children": [ + { + "type": "text", + "id": "ips07a", + "content": "Done", + "fontSize": 12, + "fontWeight": 500, + "fill": "$--muted-foreground" + } + ] + } + ] + }, + { + "type": "frame", + "id": "ips10", + "x": 360, + "y": 0, + "name": "Icon Picker โ€” Search 'book' filtered", + "theme": { "Mode": "Light" }, + "clip": true, + "width": 320, + "height": 340, + "fill": "$--background", + "layout": "vertical", + "padding": [12, 12], + "gap": 8, + "children": [ + { + "type": "text", + "id": "ips11", + "content": "COLOR", + "fontSize": 10, + "fontWeight": 400, + "fontFamily": "IBM Plex Mono", + "textTransform": "uppercase", + "fill": "$--muted-foreground" + }, + { + "type": "frame", + "id": "ips12", + "name": "colorRow", + "layout": "horizontal", + "gap": 8, + "children": [ + { "type": "ellipse", "id": "ips12a", "fill": "#E53E3E", "width": 24, "height": 24 }, + { "type": "ellipse", "id": "ips12b", "fill": "#D9730D", "width": 24, "height": 24 }, + { "type": "ellipse", "id": "ips12c", "fill": "#D69E2E", "width": 24, "height": 24 }, + { "type": "ellipse", "id": "ips12d", "fill": "#38A169", "width": 24, "height": 24 }, + { "type": "ellipse", "id": "ips12e", "fill": "#155DFF", "width": 24, "height": 24 }, + { "type": "ellipse", "id": "ips12f", "fill": "#805AD5", "width": 24, "height": 24 }, + { "type": "ellipse", "id": "ips12g", "fill": "#319795", "width": 24, "height": 24 }, + { "type": "ellipse", "id": "ips12h", "fill": "#D53F8C", "width": 24, "height": 24 } + ] + }, + { + "type": "text", + "id": "ips13", + "content": "ICON", + "fontSize": 10, + "fontWeight": 400, + "fontFamily": "IBM Plex Mono", + "textTransform": "uppercase", + "fill": "$--muted-foreground" + }, + { + "type": "frame", + "id": "ips14", + "name": "searchField", + "width": "fill_container", + "height": 28, + "fill": "$--background", + "stroke": { "align": "inside", "thickness": 1, "fill": "#155DFF" }, + "cornerRadius": 4, + "layout": "horizontal", + "alignItems": "center", + "padding": [0, 8], + "gap": 6, + "children": [ + { + "type": "text", + "id": "ips14a", + "content": "๐Ÿ”", + "fontSize": 12 + }, + { + "type": "text", + "id": "ips14b", + "content": "book", + "fontSize": 12, + "fill": "$--foreground" + } + ] + }, + { + "type": "frame", + "id": "ips15", + "name": "filteredIconGrid", + "width": "fill_container", + "layout": "horizontal", + "wrap": true, + "gap": 4, + "children": [ + { "type": "frame", "id": "fg01", "width": 30, "height": 30, "cornerRadius": 4, "fill": "rgba(21,93,255,0.1)", "children": [{ "type": "text", "id": "fgl01", "content": "๐Ÿ“–", "fontSize": 14 }] }, + { "type": "frame", "id": "fg02", "width": 30, "height": 30, "cornerRadius": 4, "fill": "transparent", "children": [{ "type": "text", "id": "fgl02", "content": "๐Ÿ“•", "fontSize": 14 }] }, + { "type": "frame", "id": "fg03", "width": 30, "height": 30, "cornerRadius": 4, "fill": "transparent", "children": [{ "type": "text", "id": "fgl03", "content": "๐Ÿ”–", "fontSize": 14 }] }, + { "type": "frame", "id": "fg04", "width": 30, "height": 30, "cornerRadius": 4, "fill": "transparent", "children": [{ "type": "text", "id": "fgl04", "content": "๐Ÿ“š", "fontSize": 14 }] }, + { + "type": "text", + "id": "fg05", + "content": "book, book-bookmark, book-open, bookmark, books", + "fontSize": 10, + "fill": "$--muted-foreground", + "width": "fill_container" + } + ] + }, + { + "type": "frame", + "id": "ips16", + "name": "doneRow", + "width": "fill_container", + "layout": "horizontal", + "justifyContent": "flex-end", + "children": [ + { + "type": "text", + "id": "ips16a", + "content": "Done", + "fontSize": 12, + "fontWeight": 500, + "fill": "$--muted-foreground" + } + ] + } + ] + }, + { + "type": "frame", + "id": "ips20", + "x": 720, + "y": 0, + "name": "Icon Picker โ€” Empty State (no results)", + "theme": { "Mode": "Light" }, + "clip": true, + "width": 320, + "height": 300, + "fill": "$--background", + "layout": "vertical", + "padding": [12, 12], + "gap": 8, + "children": [ + { + "type": "text", + "id": "ips21", + "content": "COLOR", + "fontSize": 10, + "fontWeight": 400, + "fontFamily": "IBM Plex Mono", + "textTransform": "uppercase", + "fill": "$--muted-foreground" + }, + { + "type": "frame", + "id": "ips22", + "name": "colorRow", + "layout": "horizontal", + "gap": 8, + "children": [ + { "type": "ellipse", "id": "ips22a", "fill": "#E53E3E", "width": 24, "height": 24 }, + { "type": "ellipse", "id": "ips22b", "fill": "#D9730D", "width": 24, "height": 24 }, + { "type": "ellipse", "id": "ips22c", "fill": "#D69E2E", "width": 24, "height": 24 }, + { "type": "ellipse", "id": "ips22d", "fill": "#38A169", "width": 24, "height": 24 }, + { "type": "ellipse", "id": "ips22e", "fill": "#155DFF", "width": 24, "height": 24 }, + { "type": "ellipse", "id": "ips22f", "fill": "#805AD5", "width": 24, "height": 24 }, + { "type": "ellipse", "id": "ips22g", "fill": "#319795", "width": 24, "height": 24 }, + { "type": "ellipse", "id": "ips22h", "fill": "#D53F8C", "width": 24, "height": 24 } + ] + }, + { + "type": "text", + "id": "ips23", + "content": "ICON", + "fontSize": 10, + "fontWeight": 400, + "fontFamily": "IBM Plex Mono", + "textTransform": "uppercase", + "fill": "$--muted-foreground" + }, + { + "type": "frame", + "id": "ips24", + "name": "searchField", + "width": "fill_container", + "height": 28, + "fill": "$--background", + "stroke": { "align": "inside", "thickness": 1, "fill": "#155DFF" }, + "cornerRadius": 4, + "layout": "horizontal", + "alignItems": "center", + "padding": [0, 8], + "gap": 6, + "children": [ + { + "type": "text", + "id": "ips24a", + "content": "๐Ÿ”", + "fontSize": 12 + }, + { + "type": "text", + "id": "ips24b", + "content": "zzzznotfound", + "fontSize": 12, + "fill": "$--foreground" + } + ] + }, + { + "type": "frame", + "id": "ips25", + "name": "emptyState", + "width": "fill_container", + "height": 80, + "layout": "vertical", + "alignItems": "center", + "justifyContent": "center", + "children": [ + { + "type": "text", + "id": "ips25a", + "content": "No icons found", + "fontSize": 12, + "fill": "$--muted-foreground", + "textAlign": "center" + } + ] + }, + { + "type": "frame", + "id": "ips26", + "name": "doneRow", + "width": "fill_container", + "layout": "horizontal", + "justifyContent": "flex-end", + "children": [ + { + "type": "text", + "id": "ips26a", + "content": "Done", + "fontSize": 12, + "fontWeight": 500, + "fill": "$--muted-foreground" + } + ] + } + ] + } + ], + "variables": {} +} diff --git a/src/components/NoteItem.tsx b/src/components/NoteItem.tsx index 234fa121..7cfb01b2 100644 --- a/src/components/NoteItem.tsx +++ b/src/components/NoteItem.tsx @@ -6,7 +6,7 @@ import { Users, CalendarBlank, Tag, FileText, StackSimple, } from '@phosphor-icons/react' import { getTypeColor, getTypeLightColor } from '../utils/typeColors' -import { resolveIcon } from './TypeCustomizePopover' +import { resolveIcon } from '../utils/iconRegistry' import { relativeDate, getDisplayDate } from '../utils/noteListHelpers' const TYPE_ICON_MAP: Record>> = { diff --git a/src/components/Sidebar.tsx b/src/components/Sidebar.tsx index 7ebdc53c..2d3f04b1 100644 --- a/src/components/Sidebar.tsx +++ b/src/components/Sidebar.tsx @@ -1,6 +1,7 @@ import { useState, useMemo, useRef, useEffect, useCallback, memo } from 'react' import type { VaultEntry, SidebarSelection } from '../types' -import { resolveIcon, TypeCustomizePopover } from './TypeCustomizePopover' +import { resolveIcon } from '../utils/iconRegistry' +import { TypeCustomizePopover } from './TypeCustomizePopover' import { useSectionVisibility } from '../hooks/useSectionVisibility' import { DndContext, closestCenter, KeyboardSensor, PointerSensor, diff --git a/src/components/TypeCustomizePopover.test.tsx b/src/components/TypeCustomizePopover.test.tsx index ddd345ff..83fb2363 100644 --- a/src/components/TypeCustomizePopover.test.tsx +++ b/src/components/TypeCustomizePopover.test.tsx @@ -1,17 +1,19 @@ import { describe, it, expect, vi, beforeEach } from 'vitest' import { render, screen, fireEvent } from '@testing-library/react' -import { TypeCustomizePopover, resolveIcon, ICON_OPTIONS } from './TypeCustomizePopover' +import { TypeCustomizePopover } from './TypeCustomizePopover' +import { resolveIcon, ICON_OPTIONS } from '../utils/iconRegistry' describe('resolveIcon', () => { it('returns the correct icon component for known name', () => { const Icon = resolveIcon('wrench') expect(Icon).toBeDefined() - expect(Icon).not.toBe(ICON_OPTIONS[0].Icon) // should not be fallback (file-text) + // wrench should not be the default fallback (file-text) + const fileTextIcon = resolveIcon('file-text') + expect(Icon).not.toBe(fileTextIcon) }) it('returns FileText fallback for null', () => { const Icon = resolveIcon(null) - // FileText is the default expect(Icon).toBeDefined() }) @@ -21,6 +23,23 @@ describe('resolveIcon', () => { }) }) +describe('ICON_OPTIONS', () => { + it('contains 200+ icons', () => { + expect(ICON_OPTIONS.length).toBeGreaterThanOrEqual(200) + }) + + it('has unique names', () => { + const names = ICON_OPTIONS.map((o) => o.name) + expect(new Set(names).size).toBe(names.length) + }) + + it('uses kebab-case names', () => { + for (const option of ICON_OPTIONS) { + expect(option.name).toMatch(/^[a-z][a-z0-9-]*$/) + } + }) +}) + describe('TypeCustomizePopover', () => { const onChangeIcon = vi.fn() const onChangeColor = vi.fn() @@ -45,6 +64,57 @@ describe('TypeCustomizePopover', () => { expect(screen.getByText('Done')).toBeInTheDocument() }) + it('renders search input', () => { + render( + + ) + expect(screen.getByPlaceholderText('Search iconsโ€ฆ')).toBeInTheDocument() + }) + + it('filters icons by search query', () => { + render( + + ) + + const searchInput = screen.getByPlaceholderText('Search iconsโ€ฆ') + fireEvent.change(searchInput, { target: { value: 'book' } }) + + // Should show book-related icons + expect(screen.getByTitle('book')).toBeInTheDocument() + expect(screen.getByTitle('book-open')).toBeInTheDocument() + // Should not show unrelated icons + expect(screen.queryByTitle('wrench')).not.toBeInTheDocument() + }) + + it('shows empty state when no icons match search', () => { + render( + + ) + + const searchInput = screen.getByPlaceholderText('Search iconsโ€ฆ') + fireEvent.change(searchInput, { target: { value: 'zzzznonexistent' } }) + + expect(screen.getByText('No icons found')).toBeInTheDocument() + }) + it('calls onChangeColor when a color is clicked', () => { render( { /> ) - // Click the first color button (by title) - const colorButtons = screen.getAllByTitle(/red|blue|green|purple|yellow|orange/i) + const colorButtons = screen.getAllByTitle(/red|blue|green|purple|yellow|orange|teal|pink/i) fireEvent.click(colorButtons[0]) expect(onChangeColor).toHaveBeenCalled() @@ -74,9 +143,7 @@ describe('TypeCustomizePopover', () => { /> ) - // Click the wrench icon fireEvent.click(screen.getByTitle('wrench')) - expect(onChangeIcon).toHaveBeenCalledWith('wrench') }) @@ -95,7 +162,7 @@ describe('TypeCustomizePopover', () => { expect(onClose).toHaveBeenCalled() }) - it('renders all icon options', () => { + it('renders all color options including teal and pink', () => { render( { /> ) - // Should have buttons for each icon option - for (const option of ICON_OPTIONS.slice(0, 5)) { - expect(screen.getByTitle(option.name)).toBeInTheDocument() - } + expect(screen.getByTitle('Teal')).toBeInTheDocument() + expect(screen.getByTitle('Pink')).toBeInTheDocument() }) }) diff --git a/src/components/TypeCustomizePopover.tsx b/src/components/TypeCustomizePopover.tsx index 222a456d..37781806 100644 --- a/src/components/TypeCustomizePopover.tsx +++ b/src/components/TypeCustomizePopover.tsx @@ -1,63 +1,13 @@ -import { useState, type ComponentType } from 'react' -import { - FileText, Wrench, Flask, Target, ArrowsClockwise, Users, CalendarBlank, - Tag, StackSimple, BookOpen, CookingPot, Heart, Star, House, Lightbulb, - Briefcase, Gear, Cube, Leaf, MusicNote, Camera, Airplane, GameController, - PaintBrush, ShoppingCart, GraduationCap, Trophy, ChatCircle, Notebook, - MapPin, Code, Barbell, PawPrint, Pill, Knife, - type IconProps, -} from '@phosphor-icons/react' +import { useState, useMemo } from 'react' +import { MagnifyingGlass } from '@phosphor-icons/react' +import { ICON_OPTIONS, type IconEntry } from '../utils/iconRegistry' import { ACCENT_COLORS } from '../utils/typeColors' import { cn } from '@/lib/utils' -/** Curated Phosphor icons (normal weight) for type customization */ -// eslint-disable-next-line react-refresh/only-export-components -- constant co-located with component -export const ICON_OPTIONS: { name: string; Icon: ComponentType }[] = [ - { name: 'file-text', Icon: FileText }, - { name: 'wrench', Icon: Wrench }, - { name: 'flask', Icon: Flask }, - { name: 'target', Icon: Target }, - { name: 'arrows-clockwise', Icon: ArrowsClockwise }, - { name: 'users', Icon: Users }, - { name: 'calendar-blank', Icon: CalendarBlank }, - { name: 'tag', Icon: Tag }, - { name: 'stack-simple', Icon: StackSimple }, - { name: 'book-open', Icon: BookOpen }, - { name: 'cooking-pot', Icon: CookingPot }, - { name: 'heart', Icon: Heart }, - { name: 'star', Icon: Star }, - { name: 'house', Icon: House }, - { name: 'lightbulb', Icon: Lightbulb }, - { name: 'briefcase', Icon: Briefcase }, - { name: 'gear', Icon: Gear }, - { name: 'cube', Icon: Cube }, - { name: 'leaf', Icon: Leaf }, - { name: 'music-note', Icon: MusicNote }, - { name: 'camera', Icon: Camera }, - { name: 'airplane', Icon: Airplane }, - { name: 'game-controller', Icon: GameController }, - { name: 'paint-brush', Icon: PaintBrush }, - { name: 'shopping-cart', Icon: ShoppingCart }, - { name: 'graduation-cap', Icon: GraduationCap }, - { name: 'trophy', Icon: Trophy }, - { name: 'chat-circle', Icon: ChatCircle }, - { name: 'notebook', Icon: Notebook }, - { name: 'map-pin', Icon: MapPin }, - { name: 'code', Icon: Code }, - { name: 'barbell', Icon: Barbell }, - { name: 'paw-print', Icon: PawPrint }, - { name: 'pill', Icon: Pill }, - { name: 'knife', Icon: Knife }, -] - -const ICON_MAP: Record> = Object.fromEntries( - ICON_OPTIONS.map((o) => [o.name, o.Icon]), -) - -/** Resolves a Phosphor icon name to its component, with fallback to FileText */ -// eslint-disable-next-line react-refresh/only-export-components -- utility co-located with component -export function resolveIcon(name: string | null): ComponentType { - return (name && ICON_MAP[name]) || FileText +function filterIcons(icons: IconEntry[], query: string): IconEntry[] { + if (!query) return icons + const lower = query.toLowerCase() + return icons.filter((o) => o.name.includes(lower)) } interface TypeCustomizePopoverProps { @@ -77,6 +27,9 @@ export function TypeCustomizePopover({ }: TypeCustomizePopoverProps) { const [selectedColor, setSelectedColor] = useState(currentColor) const [selectedIcon, setSelectedIcon] = useState(currentIcon) + const [search, setSearch] = useState('') + + const filteredIcons = useMemo(() => filterIcons(ICON_OPTIONS, search), [search]) const handleColorClick = (key: string) => { setSelectedColor(key) @@ -91,13 +44,13 @@ export function TypeCustomizePopover({ return (
e.stopPropagation()} onContextMenu={(e) => e.stopPropagation()} > {/* Color section */}
COLOR
-
+
{ACCENT_COLORS.map((c) => ( - ))} + + {/* Search input */} +
+ + setSearch(e.target.value)} + placeholder="Search iconsโ€ฆ" + className="w-full rounded border border-border bg-background pl-7 pr-2 py-1 text-[12px] text-foreground placeholder:text-muted-foreground outline-none focus:border-primary" + /> +
+ + {/* Icon grid */} +
+ {filteredIcons.length === 0 ? ( +
+ No icons found +
+ ) : ( + filteredIcons.map(({ name, Icon }) => ( + + )) + )}
{/* Done button */} diff --git a/src/index.css b/src/index.css index 08680379..ba455d1a 100644 --- a/src/index.css +++ b/src/index.css @@ -78,6 +78,10 @@ --accent-purple-light: rgba(128, 90, 213, 0.1); --accent-red-light: rgba(229, 62, 62, 0.1); --accent-yellow-light: rgba(214, 158, 46, 0.1); + --accent-teal: #319795; + --accent-teal-light: rgba(49, 151, 149, 0.1); + --accent-pink: #D53F8C; + --accent-pink-light: rgba(213, 63, 140, 0.1); --border-primary: #E9E9E7; --border-subtle: #E9E9E7; --border-input: #E9E9E7; diff --git a/src/utils/iconRegistry.ts b/src/utils/iconRegistry.ts new file mode 100644 index 00000000..2c247dc7 --- /dev/null +++ b/src/utils/iconRegistry.ts @@ -0,0 +1,339 @@ +import type { ComponentType } from 'react' +import { + Acorn, AddressBook, Airplane, Alarm, Anchor, Aperture, AppWindow, Archive, Armchair, Article, + ArrowsClockwise, Atom, Baby, Backpack, Bag, Balloon, Bandaids, Bank, Barbell, Barcode, + Barn, Baseball, Basketball, Bed, Bell, Bicycle, Binoculars, Bird, Blueprint, Boat, + Bone, Book, BookBookmark, BookOpen, Bookmark, Books, BowlFood, Brain, Bread, Bridge, + Briefcase, Broadcast, Broom, Browser, Bug, Building, BuildingOffice, Buildings, Bus, Butterfly, + Cactus, Cake, Calculator, Calendar, CalendarBlank, CalendarCheck, Camera, Campfire, Car, Cat, + Certificate, Chair, Champagne, ChartBar, ChartDonut, ChartLine, ChartPie, Chat, ChatCircle, ChatDots, + Check, CheckCircle, ChefHat, Church, Circuitry, City, Clipboard, ClipboardText, Clock, Cloud, + CloudSun, Clover, Code, Coffee, Coin, Coins, Compass, Confetti, Cookie, CookingPot, + Couch, CreditCard, Crown, Cube, Database, Desktop, Detective, Diamond, Dna, Dog, + Door, Drop, Envelope, Eye, Eyeglasses, Factory, Fan, Farm, Feather, File, + FileCode, FileText, FilmReel, Fingerprint, Fire, FirstAid, Fish, Flag, Flame, Flashlight, + Flask, Flower, FlowerLotus, Folder, FolderOpen, Football, ForkKnife, Function, Funnel, GameController, + Gauge, Gavel, Gear, Ghost, Gift, GitBranch, Globe, GraduationCap, Guitar, Hammer, + Hand, Handshake, Headphones, Headset, Heart, Heartbeat, Horse, Hospital, Hourglass, House, + IceCream, IdentificationBadge, Image, Infinity as InfinityIcon, Island, Joystick, Kanban, Key, Keyboard, Knife, + Ladder, Lamp, Laptop, Leaf, Lifebuoy, Lightbulb, Lighthouse, Lightning, Link, List, + ListChecks, Lock, MagicWand, Magnet, MagnifyingGlass, Mailbox, MapPin, MaskHappy, Medal, Megaphone, + Meteor, Microphone, Microscope, Money, Monitor, Moon, Motorcycle, Mountains, MusicNote, Needle, + Network, Newspaper, Note, Notebook, Notepad, Package, PaintBrush, Palette, PaperPlane, Paperclip, + Park, PawPrint, Peace, Pen, Pencil, Person, Phone, Pi, PiggyBank, Pill, + Pizza, Planet, Plant, Playlist, Plug, PottedPlant, Prescription, Presentation, Printer, PushPin, + PuzzlePiece, QrCode, Quotes, Rabbit, Radio, Rainbow, Recycle, Robot, Rocket, Ruler, + Sailboat, Scales, Scissors, Scroll, Seal, SecurityCamera, Shield, ShieldCheck, ShootingStar, ShoppingBag, + ShoppingCart, Signature, Smiley, Sneaker, Snowflake, SoccerBall, Sparkle, Stack, StackSimple, Star, + Stethoscope, Storefront, Strategy, Student, Suitcase, Sun, Sword, TShirt, Tag, Target, + Television, Tent, Terminal, Ticket, Timer, Toolbox, Train, Translate, Trash, Tree, + TreeStructure, Trophy, Truck, Umbrella, User, UserCircle, Users, Vault, VinylRecord, Wallet, + Warning, Watch, Waves, Wind, Wine, Wrench, + type IconProps, +} from '@phosphor-icons/react' + +export type { IconProps } +export type IconEntry = { name: string; Icon: ComponentType } + +/** + * Curated set of ~290 Phosphor icons for type/section customization. + * Excludes: brand logos, UI-only icons (arrows, carets, alignment), redundant variants. + * Names are kebab-case, matching the format stored in vault frontmatter. + */ +export const ICON_OPTIONS: IconEntry[] = [ + { name: 'acorn', Icon: Acorn }, + { name: 'address-book', Icon: AddressBook }, + { name: 'airplane', Icon: Airplane }, + { name: 'alarm', Icon: Alarm }, + { name: 'anchor', Icon: Anchor }, + { name: 'aperture', Icon: Aperture }, + { name: 'app-window', Icon: AppWindow }, + { name: 'archive', Icon: Archive }, + { name: 'armchair', Icon: Armchair }, + { name: 'article', Icon: Article }, + { name: 'arrows-clockwise', Icon: ArrowsClockwise }, + { name: 'atom', Icon: Atom }, + { name: 'baby', Icon: Baby }, + { name: 'backpack', Icon: Backpack }, + { name: 'bag', Icon: Bag }, + { name: 'balloon', Icon: Balloon }, + { name: 'bandaids', Icon: Bandaids }, + { name: 'bank', Icon: Bank }, + { name: 'barbell', Icon: Barbell }, + { name: 'barcode', Icon: Barcode }, + { name: 'barn', Icon: Barn }, + { name: 'baseball', Icon: Baseball }, + { name: 'basketball', Icon: Basketball }, + { name: 'bed', Icon: Bed }, + { name: 'bell', Icon: Bell }, + { name: 'bicycle', Icon: Bicycle }, + { name: 'binoculars', Icon: Binoculars }, + { name: 'bird', Icon: Bird }, + { name: 'blueprint', Icon: Blueprint }, + { name: 'boat', Icon: Boat }, + { name: 'bone', Icon: Bone }, + { name: 'book', Icon: Book }, + { name: 'book-bookmark', Icon: BookBookmark }, + { name: 'book-open', Icon: BookOpen }, + { name: 'bookmark', Icon: Bookmark }, + { name: 'books', Icon: Books }, + { name: 'bowl-food', Icon: BowlFood }, + { name: 'brain', Icon: Brain }, + { name: 'bread', Icon: Bread }, + { name: 'bridge', Icon: Bridge }, + { name: 'briefcase', Icon: Briefcase }, + { name: 'broadcast', Icon: Broadcast }, + { name: 'broom', Icon: Broom }, + { name: 'browser', Icon: Browser }, + { name: 'bug', Icon: Bug }, + { name: 'building', Icon: Building }, + { name: 'building-office', Icon: BuildingOffice }, + { name: 'buildings', Icon: Buildings }, + { name: 'bus', Icon: Bus }, + { name: 'butterfly', Icon: Butterfly }, + { name: 'cactus', Icon: Cactus }, + { name: 'cake', Icon: Cake }, + { name: 'calculator', Icon: Calculator }, + { name: 'calendar', Icon: Calendar }, + { name: 'calendar-blank', Icon: CalendarBlank }, + { name: 'calendar-check', Icon: CalendarCheck }, + { name: 'camera', Icon: Camera }, + { name: 'campfire', Icon: Campfire }, + { name: 'car', Icon: Car }, + { name: 'cat', Icon: Cat }, + { name: 'certificate', Icon: Certificate }, + { name: 'chair', Icon: Chair }, + { name: 'champagne', Icon: Champagne }, + { name: 'chart-bar', Icon: ChartBar }, + { name: 'chart-donut', Icon: ChartDonut }, + { name: 'chart-line', Icon: ChartLine }, + { name: 'chart-pie', Icon: ChartPie }, + { name: 'chat', Icon: Chat }, + { name: 'chat-circle', Icon: ChatCircle }, + { name: 'chat-dots', Icon: ChatDots }, + { name: 'check', Icon: Check }, + { name: 'check-circle', Icon: CheckCircle }, + { name: 'chef-hat', Icon: ChefHat }, + { name: 'church', Icon: Church }, + { name: 'circuitry', Icon: Circuitry }, + { name: 'city', Icon: City }, + { name: 'clipboard', Icon: Clipboard }, + { name: 'clipboard-text', Icon: ClipboardText }, + { name: 'clock', Icon: Clock }, + { name: 'cloud', Icon: Cloud }, + { name: 'cloud-sun', Icon: CloudSun }, + { name: 'clover', Icon: Clover }, + { name: 'code', Icon: Code }, + { name: 'coffee', Icon: Coffee }, + { name: 'coin', Icon: Coin }, + { name: 'coins', Icon: Coins }, + { name: 'compass', Icon: Compass }, + { name: 'confetti', Icon: Confetti }, + { name: 'cookie', Icon: Cookie }, + { name: 'cooking-pot', Icon: CookingPot }, + { name: 'couch', Icon: Couch }, + { name: 'credit-card', Icon: CreditCard }, + { name: 'crown', Icon: Crown }, + { name: 'cube', Icon: Cube }, + { name: 'database', Icon: Database }, + { name: 'desktop', Icon: Desktop }, + { name: 'detective', Icon: Detective }, + { name: 'diamond', Icon: Diamond }, + { name: 'dna', Icon: Dna }, + { name: 'dog', Icon: Dog }, + { name: 'door', Icon: Door }, + { name: 'drop', Icon: Drop }, + { name: 'envelope', Icon: Envelope }, + { name: 'eye', Icon: Eye }, + { name: 'eyeglasses', Icon: Eyeglasses }, + { name: 'factory', Icon: Factory }, + { name: 'fan', Icon: Fan }, + { name: 'farm', Icon: Farm }, + { name: 'feather', Icon: Feather }, + { name: 'file', Icon: File }, + { name: 'file-code', Icon: FileCode }, + { name: 'file-text', Icon: FileText }, + { name: 'film-reel', Icon: FilmReel }, + { name: 'fingerprint', Icon: Fingerprint }, + { name: 'fire', Icon: Fire }, + { name: 'first-aid', Icon: FirstAid }, + { name: 'fish', Icon: Fish }, + { name: 'flag', Icon: Flag }, + { name: 'flame', Icon: Flame }, + { name: 'flashlight', Icon: Flashlight }, + { name: 'flask', Icon: Flask }, + { name: 'flower', Icon: Flower }, + { name: 'flower-lotus', Icon: FlowerLotus }, + { name: 'folder', Icon: Folder }, + { name: 'folder-open', Icon: FolderOpen }, + { name: 'football', Icon: Football }, + { name: 'fork-knife', Icon: ForkKnife }, + { name: 'function', Icon: Function }, + { name: 'funnel', Icon: Funnel }, + { name: 'game-controller', Icon: GameController }, + { name: 'gauge', Icon: Gauge }, + { name: 'gavel', Icon: Gavel }, + { name: 'gear', Icon: Gear }, + { name: 'ghost', Icon: Ghost }, + { name: 'gift', Icon: Gift }, + { name: 'git-branch', Icon: GitBranch }, + { name: 'globe', Icon: Globe }, + { name: 'graduation-cap', Icon: GraduationCap }, + { name: 'guitar', Icon: Guitar }, + { name: 'hammer', Icon: Hammer }, + { name: 'hand', Icon: Hand }, + { name: 'handshake', Icon: Handshake }, + { name: 'headphones', Icon: Headphones }, + { name: 'headset', Icon: Headset }, + { name: 'heart', Icon: Heart }, + { name: 'heartbeat', Icon: Heartbeat }, + { name: 'horse', Icon: Horse }, + { name: 'hospital', Icon: Hospital }, + { name: 'hourglass', Icon: Hourglass }, + { name: 'house', Icon: House }, + { name: 'ice-cream', Icon: IceCream }, + { name: 'identification-badge', Icon: IdentificationBadge }, + { name: 'image', Icon: Image }, + { name: 'infinity', Icon: InfinityIcon }, + { name: 'island', Icon: Island }, + { name: 'joystick', Icon: Joystick }, + { name: 'kanban', Icon: Kanban }, + { name: 'key', Icon: Key }, + { name: 'keyboard', Icon: Keyboard }, + { name: 'knife', Icon: Knife }, + { name: 'ladder', Icon: Ladder }, + { name: 'lamp', Icon: Lamp }, + { name: 'laptop', Icon: Laptop }, + { name: 'leaf', Icon: Leaf }, + { name: 'lifebuoy', Icon: Lifebuoy }, + { name: 'lightbulb', Icon: Lightbulb }, + { name: 'lighthouse', Icon: Lighthouse }, + { name: 'lightning', Icon: Lightning }, + { name: 'link', Icon: Link }, + { name: 'list', Icon: List }, + { name: 'list-checks', Icon: ListChecks }, + { name: 'lock', Icon: Lock }, + { name: 'magic-wand', Icon: MagicWand }, + { name: 'magnet', Icon: Magnet }, + { name: 'magnifying-glass', Icon: MagnifyingGlass }, + { name: 'mailbox', Icon: Mailbox }, + { name: 'map-pin', Icon: MapPin }, + { name: 'mask-happy', Icon: MaskHappy }, + { name: 'medal', Icon: Medal }, + { name: 'megaphone', Icon: Megaphone }, + { name: 'meteor', Icon: Meteor }, + { name: 'microphone', Icon: Microphone }, + { name: 'microscope', Icon: Microscope }, + { name: 'money', Icon: Money }, + { name: 'monitor', Icon: Monitor }, + { name: 'moon', Icon: Moon }, + { name: 'motorcycle', Icon: Motorcycle }, + { name: 'mountains', Icon: Mountains }, + { name: 'music-note', Icon: MusicNote }, + { name: 'needle', Icon: Needle }, + { name: 'network', Icon: Network }, + { name: 'newspaper', Icon: Newspaper }, + { name: 'note', Icon: Note }, + { name: 'notebook', Icon: Notebook }, + { name: 'notepad', Icon: Notepad }, + { name: 'package', Icon: Package }, + { name: 'paint-brush', Icon: PaintBrush }, + { name: 'palette', Icon: Palette }, + { name: 'paper-plane', Icon: PaperPlane }, + { name: 'paperclip', Icon: Paperclip }, + { name: 'park', Icon: Park }, + { name: 'paw-print', Icon: PawPrint }, + { name: 'peace', Icon: Peace }, + { name: 'pen', Icon: Pen }, + { name: 'pencil', Icon: Pencil }, + { name: 'person', Icon: Person }, + { name: 'phone', Icon: Phone }, + { name: 'pi', Icon: Pi }, + { name: 'piggy-bank', Icon: PiggyBank }, + { name: 'pill', Icon: Pill }, + { name: 'pizza', Icon: Pizza }, + { name: 'planet', Icon: Planet }, + { name: 'plant', Icon: Plant }, + { name: 'playlist', Icon: Playlist }, + { name: 'plug', Icon: Plug }, + { name: 'potted-plant', Icon: PottedPlant }, + { name: 'prescription', Icon: Prescription }, + { name: 'presentation', Icon: Presentation }, + { name: 'printer', Icon: Printer }, + { name: 'push-pin', Icon: PushPin }, + { name: 'puzzle-piece', Icon: PuzzlePiece }, + { name: 'qr-code', Icon: QrCode }, + { name: 'quotes', Icon: Quotes }, + { name: 'rabbit', Icon: Rabbit }, + { name: 'radio', Icon: Radio }, + { name: 'rainbow', Icon: Rainbow }, + { name: 'recycle', Icon: Recycle }, + { name: 'robot', Icon: Robot }, + { name: 'rocket', Icon: Rocket }, + { name: 'ruler', Icon: Ruler }, + { name: 'sailboat', Icon: Sailboat }, + { name: 'scales', Icon: Scales }, + { name: 'scissors', Icon: Scissors }, + { name: 'scroll', Icon: Scroll }, + { name: 'seal', Icon: Seal }, + { name: 'security-camera', Icon: SecurityCamera }, + { name: 'shield', Icon: Shield }, + { name: 'shield-check', Icon: ShieldCheck }, + { name: 'shooting-star', Icon: ShootingStar }, + { name: 'shopping-bag', Icon: ShoppingBag }, + { name: 'shopping-cart', Icon: ShoppingCart }, + { name: 'signature', Icon: Signature }, + { name: 'smiley', Icon: Smiley }, + { name: 'sneaker', Icon: Sneaker }, + { name: 'snowflake', Icon: Snowflake }, + { name: 'soccer-ball', Icon: SoccerBall }, + { name: 'sparkle', Icon: Sparkle }, + { name: 'stack', Icon: Stack }, + { name: 'stack-simple', Icon: StackSimple }, + { name: 'star', Icon: Star }, + { name: 'stethoscope', Icon: Stethoscope }, + { name: 'storefront', Icon: Storefront }, + { name: 'strategy', Icon: Strategy }, + { name: 'student', Icon: Student }, + { name: 'suitcase', Icon: Suitcase }, + { name: 'sun', Icon: Sun }, + { name: 'sword', Icon: Sword }, + { name: 't-shirt', Icon: TShirt }, + { name: 'tag', Icon: Tag }, + { name: 'target', Icon: Target }, + { name: 'television', Icon: Television }, + { name: 'tent', Icon: Tent }, + { name: 'terminal', Icon: Terminal }, + { name: 'ticket', Icon: Ticket }, + { name: 'timer', Icon: Timer }, + { name: 'toolbox', Icon: Toolbox }, + { name: 'train', Icon: Train }, + { name: 'translate', Icon: Translate }, + { name: 'trash', Icon: Trash }, + { name: 'tree', Icon: Tree }, + { name: 'tree-structure', Icon: TreeStructure }, + { name: 'trophy', Icon: Trophy }, + { name: 'truck', Icon: Truck }, + { name: 'umbrella', Icon: Umbrella }, + { name: 'user', Icon: User }, + { name: 'user-circle', Icon: UserCircle }, + { name: 'users', Icon: Users }, + { name: 'vault', Icon: Vault }, + { name: 'vinyl-record', Icon: VinylRecord }, + { name: 'wallet', Icon: Wallet }, + { name: 'warning', Icon: Warning }, + { name: 'watch', Icon: Watch }, + { name: 'waves', Icon: Waves }, + { name: 'wind', Icon: Wind }, + { name: 'wine', Icon: Wine }, + { name: 'wrench', Icon: Wrench }, +] + +const ICON_MAP: Record> = Object.fromEntries( + ICON_OPTIONS.map((o) => [o.name, o.Icon]), +) + +/** Resolves a Phosphor icon name to its component, with fallback to FileText */ +export function resolveIcon(name: string | null): ComponentType { + return (name && ICON_MAP[name]) || FileText +} diff --git a/src/utils/typeColors.ts b/src/utils/typeColors.ts index 34e2433d..4c494ae2 100644 --- a/src/utils/typeColors.ts +++ b/src/utils/typeColors.ts @@ -36,6 +36,8 @@ export const ACCENT_COLORS: { key: string; label: string; css: string; cssLight: { key: 'green', label: 'Green', css: 'var(--accent-green)', cssLight: 'var(--accent-green-light)' }, { key: 'blue', label: 'Blue', css: 'var(--accent-blue)', cssLight: 'var(--accent-blue-light)' }, { key: 'purple', label: 'Purple', css: 'var(--accent-purple)', cssLight: 'var(--accent-purple-light)' }, + { key: 'teal', label: 'Teal', css: 'var(--accent-teal)', cssLight: 'var(--accent-teal-light)' }, + { key: 'pink', label: 'Pink', css: 'var(--accent-pink)', cssLight: 'var(--accent-pink-light)' }, ] const COLOR_KEY_TO_CSS: Record = Object.fromEntries(