1import type { ProjectIconColor, ProjectIconOverride } from "@t3tools/contracts";
2
3export type ProjectIconGlyph =
4 | { readonly kind: "emoji"; readonly emoji: string }
5 | { readonly kind: "monogram"; readonly text: string; readonly color: ProjectIconColor };
6
7/**
8 * Visible glyph count for sizing monogram text. Hermes has no Intl.Segmenter, so combining
9 * marks are folded into their base character instead of full grapheme segmentation.
10 */
11export function countGlyphs(text: string): number {
12 return Array.from(text.replace(/\p{M}/gu, "")).length;
13}
14
15/** Mirrors the automatic monogram web derives from a project name when it has no favicon. */
16export function projectMonogram(projectName: string): string {
17 const words =
18 projectName
19 .normalize("NFKC")
20 .trim()
21 .match(/[\p{L}\p{N}]+/gu) ?? [];
22 const firstWord = words[0];
23 if (!firstWord) return "PR";
24
25 const glyphs = Array.from(firstWord);
26 const first = glyphs[0] ?? "P";
27 const second =
28 glyphs.slice(1).find((glyph) => /\p{N}/u.test(glyph)) ??
29 (words.length > 1 ? Array.from(words.at(-1) ?? "")[0] : glyphs.at(-1)) ??
30 first;
31 return Array.from(`${first}${second}`.toUpperCase()).slice(0, 2).join("");
32}
33
34/**
35 * Picks what mobile draws for an assigned project icon. Mobile does not bundle
36 * the Lucide set, so a Lucide override keeps its color and falls back to the
37 * project's monogram instead of the folder glyph.
38 */
39export function resolveProjectIconGlyph(
40 projectIcon: ProjectIconOverride | null | undefined,
41 projectTitle: string,
42): ProjectIconGlyph | null {
43 switch (projectIcon?.kind) {
44 case "emoji":
45 return { kind: "emoji", emoji: projectIcon.emoji };
46 case "monogram":
47 return { kind: "monogram", text: projectIcon.text, color: projectIcon.color };
48 case "lucide":
49 return { kind: "monogram", text: projectMonogram(projectTitle), color: projectIcon.color };
50 case undefined:
51 return null;
52 }
53}
54
55const PROJECT_ICON_COLOR_CLASSES: Record<
56 ProjectIconColor,
57 { readonly text: string; readonly background: string }
58> = {
59 gray: { text: "text-gray-500", background: "bg-gray-500/15" },
60 red: { text: "text-red-500", background: "bg-red-500/15" },
61 orange: { text: "text-orange-500", background: "bg-orange-500/15" },
62 amber: { text: "text-amber-500", background: "bg-amber-500/15" },
63 yellow: { text: "text-yellow-500", background: "bg-yellow-500/15" },
64 lime: { text: "text-lime-500", background: "bg-lime-500/15" },
65 green: { text: "text-green-500", background: "bg-green-500/15" },
66 emerald: { text: "text-emerald-500", background: "bg-emerald-500/15" },
67 teal: { text: "text-teal-500", background: "bg-teal-500/15" },
68 cyan: { text: "text-cyan-500", background: "bg-cyan-500/15" },
69 sky: { text: "text-sky-500", background: "bg-sky-500/15" },
70 blue: { text: "text-blue-500", background: "bg-blue-500/15" },
71 indigo: { text: "text-indigo-500", background: "bg-indigo-500/15" },
72 violet: { text: "text-violet-500", background: "bg-violet-500/15" },
73 purple: { text: "text-purple-500", background: "bg-purple-500/15" },
74 fuchsia: { text: "text-fuchsia-500", background: "bg-fuchsia-500/15" },
75 pink: { text: "text-pink-500", background: "bg-pink-500/15" },
76 rose: { text: "text-rose-500", background: "bg-rose-500/15" },
77};
78
79export function projectIconColorClassNames(color: ProjectIconColor) {
80 return PROJECT_ICON_COLOR_CLASSES[color];
81}