Merge pull request #47 from refactoringhq/task/icon-picker-sezioni
feat: expand icon picker to 290 Phosphor icons with search and scroll
This commit is contained in:
382
design/icon-picker-sezioni.pen
Normal file
382
design/icon-picker-sezioni.pen
Normal file
@@ -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": {}
|
||||
}
|
||||
@@ -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<string, ComponentType<SVGAttributes<SVGSVGElement>>> = {
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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(
|
||||
<TypeCustomizePopover
|
||||
currentIcon={null}
|
||||
currentColor={null}
|
||||
onChangeIcon={onChangeIcon}
|
||||
onChangeColor={onChangeColor}
|
||||
onClose={onClose}
|
||||
/>
|
||||
)
|
||||
expect(screen.getByPlaceholderText('Search icons…')).toBeInTheDocument()
|
||||
})
|
||||
|
||||
it('filters icons by search query', () => {
|
||||
render(
|
||||
<TypeCustomizePopover
|
||||
currentIcon={null}
|
||||
currentColor={null}
|
||||
onChangeIcon={onChangeIcon}
|
||||
onChangeColor={onChangeColor}
|
||||
onClose={onClose}
|
||||
/>
|
||||
)
|
||||
|
||||
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(
|
||||
<TypeCustomizePopover
|
||||
currentIcon={null}
|
||||
currentColor={null}
|
||||
onChangeIcon={onChangeIcon}
|
||||
onChangeColor={onChangeColor}
|
||||
onClose={onClose}
|
||||
/>
|
||||
)
|
||||
|
||||
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(
|
||||
<TypeCustomizePopover
|
||||
@@ -56,8 +126,7 @@ describe('TypeCustomizePopover', () => {
|
||||
/>
|
||||
)
|
||||
|
||||
// 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(
|
||||
<TypeCustomizePopover
|
||||
currentIcon={null}
|
||||
@@ -106,9 +173,7 @@ describe('TypeCustomizePopover', () => {
|
||||
/>
|
||||
)
|
||||
|
||||
// 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()
|
||||
})
|
||||
})
|
||||
|
||||
@@ -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<IconProps> }[] = [
|
||||
{ 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<string, ComponentType<IconProps>> = 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<IconProps> {
|
||||
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 (
|
||||
<div
|
||||
className="bg-popover text-popover-foreground z-50 rounded-lg border shadow-md"
|
||||
style={{ width: 264, padding: 12 }}
|
||||
style={{ width: 280, padding: 12 }}
|
||||
onClick={(e) => e.stopPropagation()}
|
||||
onContextMenu={(e) => e.stopPropagation()}
|
||||
>
|
||||
{/* Color section */}
|
||||
<div className="font-mono-overline mb-2 text-muted-foreground">COLOR</div>
|
||||
<div className="flex gap-2 mb-3">
|
||||
<div className="flex gap-2 mb-3 flex-wrap">
|
||||
{ACCENT_COLORS.map((c) => (
|
||||
<button
|
||||
key={c.key}
|
||||
@@ -105,7 +58,7 @@ export function TypeCustomizePopover({
|
||||
"flex items-center justify-center rounded-full border-2 cursor-pointer transition-all",
|
||||
selectedColor === c.key ? "border-foreground scale-110" : "border-transparent hover:scale-105",
|
||||
)}
|
||||
style={{ width: 28, height: 28, backgroundColor: c.css, border: selectedColor === c.key ? '2px solid var(--foreground)' : '2px solid transparent' }}
|
||||
style={{ width: 24, height: 24, backgroundColor: c.css, border: selectedColor === c.key ? '2px solid var(--foreground)' : '2px solid transparent' }}
|
||||
onClick={() => handleColorClick(c.key)}
|
||||
title={c.label}
|
||||
/>
|
||||
@@ -114,23 +67,46 @@ export function TypeCustomizePopover({
|
||||
|
||||
{/* Icon section */}
|
||||
<div className="font-mono-overline mb-2 text-muted-foreground">ICON</div>
|
||||
<div className="flex flex-wrap gap-1" style={{ maxHeight: 200, overflowY: 'auto' }}>
|
||||
{ICON_OPTIONS.map(({ name, Icon }) => (
|
||||
<button
|
||||
key={name}
|
||||
className={cn(
|
||||
"flex items-center justify-center rounded cursor-pointer transition-colors",
|
||||
selectedIcon === name
|
||||
? "bg-primary/10 text-primary"
|
||||
: "text-muted-foreground hover:bg-accent hover:text-foreground",
|
||||
)}
|
||||
style={{ width: 30, height: 30 }}
|
||||
onClick={() => handleIconClick(name)}
|
||||
title={name}
|
||||
>
|
||||
<Icon size={16} />
|
||||
</button>
|
||||
))}
|
||||
|
||||
{/* Search input */}
|
||||
<div className="relative mb-2">
|
||||
<MagnifyingGlass
|
||||
size={14}
|
||||
className="absolute left-2 top-1/2 -translate-y-1/2 text-muted-foreground pointer-events-none"
|
||||
/>
|
||||
<input
|
||||
type="text"
|
||||
value={search}
|
||||
onChange={(e) => 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"
|
||||
/>
|
||||
</div>
|
||||
|
||||
{/* Icon grid */}
|
||||
<div className="flex flex-wrap gap-1 overflow-y-auto" style={{ maxHeight: 240 }}>
|
||||
{filteredIcons.length === 0 ? (
|
||||
<div className="w-full py-6 text-center text-[12px] text-muted-foreground">
|
||||
No icons found
|
||||
</div>
|
||||
) : (
|
||||
filteredIcons.map(({ name, Icon }) => (
|
||||
<button
|
||||
key={name}
|
||||
className={cn(
|
||||
"flex items-center justify-center rounded cursor-pointer transition-colors",
|
||||
selectedIcon === name
|
||||
? "bg-primary/10 text-primary"
|
||||
: "text-muted-foreground hover:bg-accent hover:text-foreground",
|
||||
)}
|
||||
style={{ width: 30, height: 30 }}
|
||||
onClick={() => handleIconClick(name)}
|
||||
title={name}
|
||||
>
|
||||
<Icon size={16} />
|
||||
</button>
|
||||
))
|
||||
)}
|
||||
</div>
|
||||
|
||||
{/* Done button */}
|
||||
|
||||
@@ -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;
|
||||
|
||||
339
src/utils/iconRegistry.ts
Normal file
339
src/utils/iconRegistry.ts
Normal file
@@ -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<IconProps> }
|
||||
|
||||
/**
|
||||
* 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<string, ComponentType<IconProps>> = 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<IconProps> {
|
||||
return (name && ICON_MAP[name]) || FileText
|
||||
}
|
||||
@@ -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<string, string> = Object.fromEntries(
|
||||
|
||||
Reference in New Issue
Block a user