1- import type { ProjectIconColor } from "@t3tools/contracts" ;
21import type { EnvironmentProject } from "@t3tools/client-runtime/state/shell" ;
32import {
43 getProjectFaviconResourceKey ,
54 isProjectFaviconFallbackUrl ,
65} from "@t3tools/shared/projectFavicon" ;
7- import {
8- BotIcon ,
9- BookOpenIcon ,
10- BracesIcon ,
11- CircuitBoardIcon ,
12- CloudCogIcon ,
13- Code2Icon ,
14- DatabaseIcon ,
15- FlaskConicalIcon ,
16- FolderCodeIcon ,
17- Gamepad2Icon ,
18- Globe2Icon ,
19- ImageIcon ,
20- Layers3Icon ,
21- MonitorIcon ,
22- MusicIcon ,
23- PackageIcon ,
24- ServerIcon ,
25- ShieldCheckIcon ,
26- ShoppingBagIcon ,
27- SmartphoneIcon ,
28- TerminalIcon ,
29- VideoIcon ,
30- } from "lucide-react" ;
6+ import { FolderCodeIcon } from "lucide-react" ;
317import type { IconName } from "lucide-react/dynamic" ;
328import type { ComponentType } from "react" ;
339import { lazy , Suspense , useState } from "react" ;
3410import { useAtomValue } from "@effect/atom-react" ;
3511import { projectFaviconUrlAtom } from "../state/assets" ;
36- import { selectProjectIcon , type ProjectIconName } from "../projectIconModel " ;
12+ import { deriveProjectIdentity } from "../projectIdentity " ;
3713import { projectIconColorClassName } from "../projectIconColors" ;
3814import { cn } from "~/lib/utils" ;
3915
@@ -45,56 +21,6 @@ function DynamicProjectIconFallback() {
4521 return < FolderCodeIcon className = "size-full text-[inherit]" /> ;
4622}
4723
48- const PROJECT_ICONS : Record < ProjectIconName , ComponentType < { className ?: string } > > = {
49- ai : BotIcon ,
50- book : BookOpenIcon ,
51- braces : BracesIcon ,
52- circuit : CircuitBoardIcon ,
53- cloud : CloudCogIcon ,
54- code : Code2Icon ,
55- database : DatabaseIcon ,
56- desktop : MonitorIcon ,
57- "folder-code" : FolderCodeIcon ,
58- game : Gamepad2Icon ,
59- image : ImageIcon ,
60- layers : Layers3Icon ,
61- mobile : SmartphoneIcon ,
62- music : MusicIcon ,
63- package : PackageIcon ,
64- security : ShieldCheckIcon ,
65- server : ServerIcon ,
66- shopping : ShoppingBagIcon ,
67- terminal : TerminalIcon ,
68- test : FlaskConicalIcon ,
69- video : VideoIcon ,
70- web : Globe2Icon ,
71- } ;
72-
73- const PROJECT_ICON_COLOR_BY_NAME : Record < ProjectIconName , ProjectIconColor > = {
74- ai : "violet" ,
75- book : "amber" ,
76- braces : "purple" ,
77- circuit : "teal" ,
78- cloud : "sky" ,
79- code : "blue" ,
80- database : "cyan" ,
81- desktop : "indigo" ,
82- "folder-code" : "orange" ,
83- game : "emerald" ,
84- image : "pink" ,
85- layers : "fuchsia" ,
86- mobile : "lime" ,
87- music : "fuchsia" ,
88- package : "orange" ,
89- security : "teal" ,
90- server : "blue" ,
91- shopping : "rose" ,
92- terminal : "green" ,
93- test : "yellow" ,
94- video : "red" ,
95- web : "sky" ,
96- } ;
97-
9824// The slice of a project that decides its icon. Every surface must pass the
9925// project record itself (or a snapshot spread from it) so the saved title, favicon
10026// and icon override always travel together. Passing a display label as the title
@@ -103,7 +29,6 @@ export type ProjectFaviconProject = Pick<
10329 EnvironmentProject ,
10430 "environmentId" | "workspaceRoot" | "title" | "faviconPath" | "projectIcon"
10531> ;
106-
10732export function ProjectFavicon ( input : {
10833 project : ProjectFaviconProject ;
10934 className ?: string | undefined ;
@@ -118,7 +43,13 @@ export function ProjectFavicon(input: {
11843 } ) ,
11944 ) ;
12045 if ( project . projectIcon ?. kind === "emoji" ) {
121- return < ProjectFaviconFallback className = { input . className } emoji = { project . projectIcon . emoji } /> ;
46+ return (
47+ < ProjectFaviconFallback
48+ className = { input . className }
49+ icon = { FolderCodeIcon }
50+ emoji = { project . projectIcon . emoji }
51+ />
52+ ) ;
12253 }
12354 if ( project . projectIcon ?. kind === "lucide" ) {
12455 const colorClassName = projectIconColorClassName ( project . projectIcon . color ) ;
@@ -139,25 +70,14 @@ export function ProjectFavicon(input: {
13970 </ span >
14071 ) ;
14172 }
142- const automaticIconName = input . fallbackIcon
143- ? null
144- : selectProjectIcon ( project . title , project . workspaceRoot ) ;
145- const FallbackIcon =
146- input . fallbackIcon ??
147- ( automaticIconName ?. kind === "lucide" ? PROJECT_ICONS [ automaticIconName . icon ] : undefined ) ;
148- const fallbackEmoji = automaticIconName ?. kind === "emoji" ? automaticIconName . emoji : undefined ;
149- const fallbackColorClassName =
150- automaticIconName ?. kind === "lucide"
151- ? projectIconColorClassName ( PROJECT_ICON_COLOR_BY_NAME [ automaticIconName . icon ] )
152- : undefined ;
73+ const FallbackIcon = input . fallbackIcon ?? FolderCodeIcon ;
15374
15475 if ( ! src || isProjectFaviconFallbackUrl ( src ) ) {
15576 return (
15677 < ProjectFaviconFallback
15778 className = { input . className }
158- colorClassName = { fallbackColorClassName }
15979 icon = { FallbackIcon }
160- emoji = { fallbackEmoji }
80+ projectName = { project . title }
16181 />
16282 ) ;
16383 }
@@ -174,23 +94,65 @@ export function ProjectFavicon(input: {
17494 src = { src }
17595 className = { input . className }
17696 fallbackIcon = { FallbackIcon }
177- fallbackEmoji = { fallbackEmoji }
178- fallbackColorClassName = { fallbackColorClassName }
97+ fallbackProjectName = { project . title }
17998 />
18099 ) ;
181100}
182101
183102function ProjectFaviconFallback ( {
184103 className,
185- colorClassName,
186104 icon : Icon ,
187105 emoji,
106+ projectName,
188107} : {
189108 readonly className ?: string | undefined ;
190- readonly colorClassName ?: string | undefined ;
191- readonly icon ?: ComponentType < { className ?: string } > | undefined ;
109+ readonly icon : ComponentType < { className ?: string } > ;
192110 readonly emoji ?: string | undefined ;
111+ readonly projectName ?: string | undefined ;
193112} ) {
113+ if ( projectName && projectName . trim ( ) . length > 0 ) {
114+ const identity = deriveProjectIdentity ( projectName ) ;
115+ return (
116+ < svg
117+ aria-hidden = "true"
118+ viewBox = "0 0 16 16"
119+ className = { cn (
120+ "size-4 shrink-0 overflow-hidden rounded-[25%] font-mono select-none" ,
121+ className ,
122+ ) }
123+ style = { {
124+ backgroundColor : identity . background ,
125+ backgroundImage : `linear-gradient(145deg, ${ identity . highlight } , ${ identity . background } 72%)` ,
126+ } }
127+ >
128+ < text
129+ x = "8"
130+ y = "10.8"
131+ textAnchor = "middle"
132+ fill = "white"
133+ className = "font-mono"
134+ fontSize = "8.25"
135+ fontWeight = "700"
136+ textLength = "12"
137+ lengthAdjust = "spacingAndGlyphs"
138+ textRendering = "geometricPrecision"
139+ >
140+ { identity . monogram }
141+ </ text >
142+ < rect
143+ x = "0.25"
144+ y = "0.25"
145+ width = "15.5"
146+ height = "15.5"
147+ rx = "3.75"
148+ fill = "none"
149+ strokeWidth = "0.5"
150+ className = "stroke-black/10 dark:stroke-white/10"
151+ />
152+ </ svg >
153+ ) ;
154+ }
155+
194156 if ( emoji ) {
195157 return (
196158 < span
@@ -205,22 +167,19 @@ function ProjectFaviconFallback({
205167 ) ;
206168 }
207169
208- if ( ! Icon ) return null ;
209- return < Icon className = { cn ( "size-3.5 shrink-0 text-icon-muted" , colorClassName , className ) } /> ;
170+ return < Icon className = { cn ( "size-3.5 shrink-0 text-icon-muted" , className ) } /> ;
210171}
211172
212173function ProjectFaviconImage ( {
213174 src,
214175 className,
215176 fallbackIcon : FallbackIcon ,
216- fallbackEmoji,
217- fallbackColorClassName,
177+ fallbackProjectName,
218178} : {
219179 readonly src : string ;
220180 readonly className ?: string | undefined ;
221- readonly fallbackIcon ?: ComponentType < { className ?: string } > | undefined ;
222- readonly fallbackEmoji ?: string | undefined ;
223- readonly fallbackColorClassName ?: string | undefined ;
181+ readonly fallbackIcon : ComponentType < { className ?: string } > ;
182+ readonly fallbackProjectName ?: string | undefined ;
224183} ) {
225184 const [ displayedSrc , setDisplayedSrc ] = useState < string | null > ( ( ) =>
226185 src . startsWith ( "data:image/" ) ? src : null ,
@@ -235,9 +194,8 @@ function ProjectFaviconImage({
235194 { displayedSrc === null ? (
236195 < ProjectFaviconFallback
237196 className = { className }
238- colorClassName = { fallbackColorClassName }
239197 icon = { FallbackIcon }
240- emoji = { fallbackEmoji }
198+ projectName = { fallbackProjectName }
241199 />
242200 ) : null }
243201 { displayedSrc ? (
0 commit comments