feat: custom icons

This commit is contained in:
2026-03-15 15:13:41 +01:00
parent 449d2cb382
commit 202462bad7
7 changed files with 166 additions and 5 deletions

View File

@@ -17,11 +17,12 @@ import {
SidebarRail, SidebarRail,
SidebarTrigger, SidebarTrigger,
} from '@/components/ui/sidebar' } from '@/components/ui/sidebar'
import { Plus, Settings, Triangle } from 'lucide-react' import { Plus, Settings } from 'lucide-react'
import { Tooltip, TooltipContent, TooltipTrigger } from '@/components/ui/tooltip' import { Tooltip, TooltipContent, TooltipTrigger } from '@/components/ui/tooltip'
import { useSidebar } from '@/components/ui/sidebar' import { useSidebar } from '@/components/ui/sidebar'
import { usePlatform } from './KosmosContext' import { usePlatform } from './KosmosContext'
import { getRecollectionIcon } from '@/lib/iconMap' import { getRecollectionIcon } from '@/lib/iconMap'
import { RecollectionsIcon } from '@/lib/icons'
import { NewRecollectionDialog } from './NewRecollectionDialog' import { NewRecollectionDialog } from './NewRecollectionDialog'
import { SettingsDialog } from './SettingsDialog' import { SettingsDialog } from './SettingsDialog'
import type { Recollection } from './types' import type { Recollection } from './types'
@@ -132,7 +133,7 @@ export function KosmosSidebar() {
<SidebarMenuItem> <SidebarMenuItem>
<SidebarMenuButton asChild tooltip="Recollections" isActive={location.pathname === '/recollections' && !selectedRecollectionId}> <SidebarMenuButton asChild tooltip="Recollections" isActive={location.pathname === '/recollections' && !selectedRecollectionId}>
<Link to="/recollections"> <Link to="/recollections">
<Triangle className="size-4" /> <RecollectionsIcon className="size-4 shrink-0" />
<span>Recollections</span> <span>Recollections</span>
</Link> </Link>
</SidebarMenuButton> </SidebarMenuButton>

View File

@@ -5,7 +5,8 @@
import React from 'react' import React from 'react'
import { Link, NavLink, useParams } from 'react-router-dom' import { Link, NavLink, useParams } from 'react-router-dom'
import { usePlatform } from '@/app/kosmos/KosmosContext' import { usePlatform } from '@/app/kosmos/KosmosContext'
import { ArrowLeft, BookOpen, Workflow } from 'lucide-react' import { ArrowLeft } from 'lucide-react'
import { FluxIcon, LogosIcon } from '@/lib/icons'
import { useRecollectionMenubar } from './RecollectionMenubarContext' import { useRecollectionMenubar } from './RecollectionMenubarContext'
export function RecollectionMenubar() { export function RecollectionMenubar() {
@@ -40,7 +41,7 @@ export function RecollectionMenubar() {
`flex items-center gap-1.5 rounded-sm px-2.5 py-1 text-sm outline-none focus:bg-accent focus:text-accent-foreground hover:bg-accent hover:text-accent-foreground ${isActive ? 'bg-accent font-medium text-foreground' : 'text-muted-foreground'}` `flex items-center gap-1.5 rounded-sm px-2.5 py-1 text-sm outline-none focus:bg-accent focus:text-accent-foreground hover:bg-accent hover:text-accent-foreground ${isActive ? 'bg-accent font-medium text-foreground' : 'text-muted-foreground'}`
} }
> >
<BookOpen className="size-3.5" /> <LogosIcon className="size-3.5" />
Logos Logos
</NavLink> </NavLink>
<NavLink <NavLink
@@ -49,7 +50,7 @@ export function RecollectionMenubar() {
`flex items-center gap-1.5 rounded-sm px-2.5 py-1 text-sm outline-none focus:bg-accent focus:text-accent-foreground hover:bg-accent hover:text-accent-foreground ${isActive ? 'bg-accent font-medium text-foreground' : 'text-muted-foreground'}` `flex items-center gap-1.5 rounded-sm px-2.5 py-1 text-sm outline-none focus:bg-accent focus:text-accent-foreground hover:bg-accent hover:text-accent-foreground ${isActive ? 'bg-accent font-medium text-foreground' : 'text-muted-foreground'}`
} }
> >
<Workflow className="size-3.5" /> <FluxIcon className="size-3.5" />
Flux Flux
</NavLink> </NavLink>
</nav> </nav>

View File

@@ -0,0 +1,46 @@
# Icon registry
Custom SVG icons as React components. Icons use `fill="currentColor"` so they match text color in light/dark themes.
**Usage:** `import { RecollectionsIcon } from '@/lib/icons'` then `<RecollectionsIcon className="size-4" />`.
---
## Adding a new custom icon
You do **not** need a separate `.svg` file. Everything lives in the registry.
### 1. Get path data from your SVG
From the SVG you have:
- **viewBox** e.g. `"0 0 24 24"` from `<svg viewBox="...">`
- **paths** the `d` attribute of each `<path>`. One or more strings.
Example: `<path d="M12 2L2 7v10l10 5 10-5V7L12 2z"/>` → use `'M12 2L2 7v10l10 5 10-5V7L12 2z'`.
### 2. Edit `registry.tsx` only
1. **Add an entry to `ICONS`** (same shape as `recollections`):
```ts
myFeature: {
viewBox: '0 0 24 24',
paths: ['M12 2L2 7v10l10 5 10-5V7L12 2z'],
},
```
2. **Export a component** (one line):
```ts
export const MyFeatureIcon = createIcon('myFeature')
```
### 3. Use it
```tsx
import { MyFeatureIcon } from '@/lib/icons'
<MyFeatureIcon className="size-4" />
```
Thats it. No new files, no public SVG. All icons are defined in `registry.tsx`.

View File

@@ -0,0 +1,35 @@
/**
* Base component for registry SVG icons. Renders an inline <svg> with one or more <path> elements.
* Use this when defining a new icon so all icons share the same props and behavior.
*/
import React from 'react'
import type { IconProps } from './types'
export type SvgIconData = {
viewBox: string
paths: string[]
}
type SvgIconProps = IconProps & {
viewBox: string
paths: string[]
}
export function SvgIcon({ viewBox, paths, className, title, fill = 'currentColor' }: SvgIconProps) {
return (
<svg
xmlns="http://www.w3.org/2000/svg"
viewBox={viewBox}
fill={fill}
className={className}
aria-hidden={!title}
{...(title ? { 'aria-label': title } : {})}
>
{title && <title>{title}</title>}
{paths.map((d, i) => (
<path key={i} d={d} />
))}
</svg>
)
}

View File

@@ -0,0 +1,15 @@
/**
* Custom SVG icon registry. Import icons as components.
*
* import { RecollectionsIcon } from '@/lib/icons'
* <RecollectionsIcon className="size-4" />
*
* To add a new icon: edit registry.tsx (add to ICONS + export a createIcon('key') component).
* See README.md in this folder.
*/
export { SvgIcon } from './SvgIcon'
export type { IconProps } from './types'
export type { SvgIconData } from './SvgIcon'
export { RecollectionsIcon, FluxIcon, LogosIcon } from './registry'

View File

@@ -0,0 +1,50 @@
/**
* Custom SVG icon registry. All icons are defined here.
*
* To add a new icon:
* 1. Add a new entry to ICONS (viewBox + paths; paths = array of <path d="..."> values).
* 2. Export a component: export const MyIcon = createIcon('myIcon')
*
* Usage: import { RecollectionsIcon } from '@/lib/icons'
*/
import React from 'react'
import { SvgIcon } from './SvgIcon'
import type { IconProps } from './types'
export type IconDefinition = {
viewBox: string
paths: string[]
}
const ICONS: Record<string, IconDefinition> = {
recollections: {
viewBox: '0 0 136.94762 136.94878',
paths: [
'm 66.371161,0.03390048 c -2.311523,0.07281 -4.641626,0.264753 -6.982424,0.578125 C 21.999631,5.6183495 -4.3606806,40.0826 0.59967533,77.47726 5.5599054,114.87286 39.993587,141.27485 77.394601,136.36007 114.85985,131.43673 141.3174,96.93037 136.34772,59.4714 131.68966,24.35368 101.04394,-1.0582045 66.371161,0.03390048 Z M 61.566472,3.2760895 c -0.0095,0.001 -0.01971,0.003 -0.0293,0.004 -0.0098,9.2e-4 -0.01964,9.9e-4 -0.0293,0.002 z m 9.699219,9.5449205 c 19.50377,0.55671 35.127149,11.15304 44.126949,26.2207 6.93841,11.61643 9.68597,25.81012 6.91406,40.09375 C 120.71152,73.34663 118.17437,68.0619 114.67585,63.45187 106.8245,53.10609 94.721751,46.13507 80.130921,42.52023 l -0.27148,-0.0684 -0.27734,-0.0352 c -28.151269,-3.7155 -55.054642,8.83634 -64.988286,36.95117 -0.02494,0.0706 -0.02478,0.14601 -0.04883,0.2168 C 11.655501,63.71409 15.42168,46.23136 25.22272,33.90491 l 0.01367,-0.0156 0.01367,-0.0176 C 34.377229,22.19422 47.860414,14.74021 62.603576,13.22132 l 0.03711,-0.004 0.03711,-0.004 c 2.936655,-0.34652 5.801645,-0.47211 8.587895,-0.39258 z m -1.29102,41.87109 c 12.95901,0.70222 25.06263,6.49265 32.783209,15.08008 7.28112,8.09864 10.79834,18.33767 8.07226,30 C 101.03587,90.14454 89.910281,84.99651 75.345771,82.73117 l -0.19531,-0.0312 -0.19922,-0.0156 c -20.733068,-1.55643 -35.022716,5.335 -48.671879,17.58594 -0.06457,-0.37677 -0.142507,-0.53509 -0.21289,-0.99805 l -0.002,-0.0215 -0.0039,-0.0215 c -1.539122,-9.56158 0.856493,-19.33174 6.644531,-27.0957 l 0.0059,-0.008 0.0059,-0.006 C 40.835342,61.14748 50.93512,56.87803 64.613388,54.7094 c 1.796703,-0.10041 3.587583,-0.11367 5.361323,-0.0176 z m 1.43946,40.8086 c 12.76679,0.43942 22.46301,6.41284 32.015619,15.60351 -8.516209,7.46779 -18.131509,11.6796 -29.921869,12.62305 l -0.0156,0.002 -0.0137,0.002 C 57.880343,125.06798 44.881286,121.11357 33.449284,111.37969 41.537216,101.9635 52.025551,97.38866 65.0528,95.70782 c 2.205241,-0.20547 4.323551,-0.27717 6.361331,-0.20703 z m 2.89648,38.19336 c 9.8e-4,-2.3e-4 0.003,1.6e-4 0.004,0 0,0 0.002,0 0.002,0 0.001,-1.1e-4 0.003,-1e-5 0.004,0 l -0.0117,0.002 z',
],
},
flux: {
viewBox: '0 0 128.21893 138.56171',
paths: [
'M 65.374983,8.1088677e-4 60.701153,0.03011089 60.415993,4.6961249 c -0.55179,9.0557191 -2.83786,11.9493291 -6.79687,14.5195291 -3.95902,2.5702 -10.54806,4.20232 -18.3418,7.4082 -34.98520956,14.39284 -47.28717,62.07586 -21.55664,90.648436 0,0 0.002,0.002 0.002,0.002 0,0 0.002,0.002 0.002,0.002 10e-4,10e-4 9.6e-4,0.002 0.002,0.004 10.83353,12.16108 26.49706,20.323 43.10742,21.14844 1.82408,0.0913 3.65036,0.13596 5.47656,0.13281 h 0.0508 l 0.0488,-0.002 C 100.64938,137.72435 128.68569,105.7552 128.213,68.397434 128.00565,52.017994 121.01832,35.173324 109.838,22.170874 98.657673,9.1684149 82.994953,-0.10807511 65.375113,9.5388677e-4 Z M 72.351543,14.047684 c 3.97414,0.85717 7.73706,2.00996 8.76562,3.47266 l 0.0332,0.0488 0.0351,0.0469 c 6.89636,9.33072 6.88878,19.61502 2.61328,28.80664 -4.27549,9.19163 -13.07048,16.8718 -23.43164,19.5332 -19.35646,4.97273 -27.00478,5.36045 -43.54101,18.77539 l -0.27149,0.22071 -0.23828,0.25586 c -1.01013,1.08488 -1.80757,2.18133 -2.68164,3.27148 -0.86509,-4.20543 -1.08073,-8.40113 -0.47461,-13.26562 1.90196,-15.26728 13.08901,-31.08978 27.28125,-36.76563 6.67107,-2.66751 15.44131,-4.10549 23.06445,-10.32226 h 0.002 v -0.002 c 4.55186,-3.71381 7.379,-8.78935 8.84375,-14.07617 z m 26.69531,15.76172 c 3.560237,3.98608 6.818267,8.37635 8.771477,11.92773 l 0.006,0.0117 0.008,0.0117 c 7.49286,13.45031 9.35625,29.3145 5.18554,44.13672 -3.99106,14.176786 -13.477257,26.166346 -26.355467,33.312496 -13.35902,7.26239 -26.86378,8.03142 -41.45117,3.71289 -11.9737,-4.11608 -18.89818,-9.93158 -25.79883,-19.92578 5.60813,-9.268456 17.76283,-18.626576 27.17774,-20.341796 10.91193,-1.9874 22.02927,-5.01594 31.47461,-10.92578 9.44534,-5.90984 17.12434,-14.98149 20.23828,-27.86914 1.05706,-4.37454 1.23242,-9.22164 0.74414,-14.05078 z',
],
},
logos: {
viewBox: '0 0 146.33972 145.15062',
paths: [
'M 73.027332,7.1405223e-8 69.925772,5.16211 c -5.52098,9.188434 -14.41144,26.253674 -19.48828,32.402344 0.0491,-0.0595 -2.98075,2.63874 -6.69531,5.3418 -3.71456,2.70305 -8.45619,5.96709 -13.31055,9.22265 -9.70871,6.51113 -20.01065,13.07222 -23.4648407,15.12305 L -8.724555e-6,71.388674 6.8222613,75.761714 c 9.0907907,5.82534 18.1443907,11.70934 27.1582007,17.65235 l 0.0156,0.01 0.0176,0.0117 c 4.00269,2.59317 8.36085,5.13445 11.86328,7.523436 0.23407,0.45064 0.38789,0.80708 0.73633,1.46094 0.71978,1.35064 1.66548,3.08636 2.76367,5.08594 2.19639,3.99914 5.00416,9.04144 7.78711,14.00195 2.78296,4.96051 5.53921,9.83667 7.63282,13.50195 1.0468,1.83264 1.92645,3.36087 2.57031,4.46094 0.32193,0.55003 0.58219,0.98887 0.79297,1.33398 0.10539,0.17256 0.19404,0.31506 0.30273,0.48047 0.0543,0.0827 0.10819,0.16699 0.2168,0.31446 0.1086,0.14746 -0.44724,0.078 1.28515,1.23437 l 3.47071,2.31641 3.78711,-3.91993 13.84961,-23.21679 c 0.91939,-1.54094 2.60442,-4.51911 4.34179,-7.35938 0.86869,-1.42013 1.74513,-2.79264 2.47266,-3.83593 0.58635,-0.84084 1.23011,-1.53521 1.26758,-1.61524 0,0 0.002,-0.002 0.002,-0.002 0,0 0,-0.002 0,-0.002 0,0 0,-0.002 0,-0.002 0,0 -0.002,0 -0.002,0 0,0 -0.002,0 -0.002,0 0,0 -0.002,0 -0.002,0 C 110.91313,95.490664 128.18052,86.436874 141.2792,76.263754 l 5.06054,-3.92969 -5.04296,-3.95312 c -9.38527,-7.35596 -20.29484,-13.54071 -29.4336,-19.82227 -2.33755,-1.60623 -5.3246,-3.37974 -8.0625,-5.19531 -2.73789,-1.81558 -5.222368,-3.83146 -5.923828,-4.70703 -0.44105,-0.55116 -2.07282,-3.15619 -3.80664,-6.27539 -1.7342,-3.11992 -3.75432,-6.92382 -5.76953,-10.69141 -2.01521,-3.76759 -4.02165,-7.49617 -5.78711,-10.55078 -1.76546,-3.05461 -2.58933,-5.01617 -4.98242,-7.140627 z M 74.419912,23.173824 c 1.17627,1.99532 2.38635,4.05673 3.68555,6.39649 1.02962,1.85427 1.28165,2.47838 2.24805,4.29297 -1.93131,-0.43835 -3.97357,-0.82076 -6.42578,-1.04297 l -0.48633,-0.0449 -0.48633,0.0508 c -1.65299,0.17332 -3.16247,0.42313 -4.60938,0.7207 2.03534,-3.47033 4.0086,-6.8998 6.07422,-10.37305 z m 0.084,22.8711 c 12.48904,0.43314 21.30922,9.51036 23.13672,22.39648 1.01116,7.46859 -0.96496,15.02931 -5.50196,21.04688 v 0.002 c -4.21095,5.59226 -8.86516,8.02542 -15.82031,9.11719 -8.31129,0.0281 -14.27689,-2.81072 -18.55664,-7.17188 -4.33772,-4.42023 -6.90239,-10.56445 -7.40625,-16.96289 -1.00248,-12.73032 5.43459,-25.61273 21.60352,-28.39844 0.86423,-0.0494 1.71231,-0.0582 2.54492,-0.0293 z m -35.95703,16.11719 c -1.30582,5.40995 -1.66749,11.01933 -0.61133,16.82031 0.0652,0.70615 0.22574,1.26763 0.35937,1.87109 -0.73651,-0.50535 -1.53019,-1.01175 -2.17187,-1.47265 -1.38484,-0.99469 -2.10979,-1.58445 -3.06446,-2.20508 l -0.0195,-0.0117 -0.8789,-0.56055 -0.0781,-0.0449 c -2.38147,-1.40078 -5.03264,-3.28099 -7.61328,-5.03906 1.79854,-1.25798 3.57872,-2.53921 5.39258,-3.77539 2.85143,-1.94332 5.79752,-3.70317 8.68555,-5.58203 z m 71.302728,0.62304 c 2.39205,1.59547 4.80964,3.252 7.11719,4.59375 2.03836,1.18513 4.75208,3.12784 7.25976,4.88672 -3.47791,2.39487 -7.00474,4.84452 -10.39257,7.03125 l -0.002,0.002 -0.002,0.002 c -1.52473,0.98648 -2.99987,1.93804 -4.50195,2.91992 1.66778,-6.01121 2.23579,-12.37241 0.52148,-19.43555 z M 66.058592,110.64257 c 4.81115,1.23001 9.49114,1.35292 13.97852,0.56836 -0.76396,1.32958 -1.60165,2.70012 -2.29492,3.93946 -1.46148,2.59299 -3.03458,5.16218 -4.57422,7.8125 -1.75297,-2.96478 -2.93487,-4.85528 -5.01758,-8.53321 -0.76423,-1.34958 -1.36576,-2.48098 -2.0918,-3.78711 z',
],
},
}
function createIcon(key: keyof typeof ICONS) {
const { viewBox, paths } = ICONS[key]
return function IconComponent(props: IconProps) {
return <SvgIcon viewBox={viewBox} paths={paths} {...props} />
}
}
export const RecollectionsIcon = createIcon('recollections')
export const FluxIcon = createIcon('flux')
export const LogosIcon = createIcon('logos')

View File

@@ -0,0 +1,13 @@
/**
* Shared props for SVG icon components in the icon registry.
* All icons inherit text color via fill="currentColor" by default.
*/
export type IconProps = {
/** Optional class name (e.g. for size: "size-4", "w-5 h-5") */
className?: string
/** Optional title for accessibility; omit for decorative icons (use aria-hidden on parent) */
title?: string
/** Override fill; default is "currentColor" */
fill?: string
}