Skip to content
File

Blob: src/client/components/admin/launcher-tile-picker.tsx

typescript103 lines
1import {
2 LAUNCHER_ICONS,
3 LAUNCHER_TINTS,
4 type LauncherIconName,
5 type LauncherTintName,
6} from "@/client/lib/launcher-icons";
7 
8interface LauncherTilePickerProps {
9 icon: LauncherIconName | "";
10 tint: LauncherTintName | "";
11 name: string;
12 onIconChange: (icon: LauncherIconName | "") => void;
13 onTintChange: (tint: LauncherTintName | "") => void;
14}
15 
16export const LauncherTilePicker = ({ icon, tint, name, onIconChange, onTintChange }: LauncherTilePickerProps) => {
17 const ResolvedIcon = icon ? LAUNCHER_ICONS[icon] : null;
18 const tintValue = tint ? LAUNCHER_TINTS[tint] : "oklch(0.30 0.005 265)";
19 const initial = (name.trim()[0] ?? "?").toUpperCase();
20 
21 return (
22 <div className="rounded-xl border border-zinc-800/60 bg-zinc-900/40 p-4">
23 <div className="mb-3 flex items-center justify-between gap-3">
24 <div>
25 <p className="text-sm font-medium text-zinc-100">Launcher tile</p>
26 <p className="mt-0.5 text-xs text-zinc-500">How this app appears on the signed-in landing page.</p>
27 </div>
28 <div
29 className="flex h-14 w-14 shrink-0 items-center justify-center rounded-2xl text-zinc-50 shadow-sm"
30 style={{ backgroundColor: tintValue }}
31 aria-hidden="true"
32 >
33 {ResolvedIcon ? (
34 <ResolvedIcon className="h-7 w-7" />
35 ) : (
36 <span className="font-display text-2xl font-semibold">{initial}</span>
37 )}
38 </div>
39 </div>
40 <fieldset className="mb-3 m-0 border-0 p-0">
41 <legend className="mb-1.5 block text-xs font-medium uppercase tracking-wider text-zinc-500">Icon</legend>
42 <div className="flex flex-wrap gap-1.5">
43 <button
44 type="button"
45 onClick={() => onIconChange("")}
46 aria-pressed={icon === ""}
47 className={`flex h-9 w-9 items-center justify-center rounded-lg border text-xs ${
48 icon === ""
49 ? "border-accent-500/60 bg-accent-500/10 text-accent-300"
50 : "border-zinc-700/60 bg-zinc-800/60 text-zinc-400 hover:border-zinc-600/60"
51 }`}
52 aria-label="No icon (use initial)"
53 >
54 {(name.trim()[0] ?? "·").toUpperCase()}
55 </button>
56 {Object.entries(LAUNCHER_ICONS).map(([key, Icon]) => (
57 <button
58 key={key}
59 type="button"
60 onClick={() => onIconChange(key as LauncherIconName)}
61 aria-pressed={icon === key}
62 className={`flex h-9 w-9 items-center justify-center rounded-lg border ${
63 icon === key
64 ? "border-accent-500/60 bg-accent-500/10 text-accent-300"
65 : "border-zinc-700/60 bg-zinc-800/60 text-zinc-400 hover:border-zinc-600/60 hover:text-zinc-200"
66 }`}
67 aria-label={key}
68 title={key}
69 >
70 <Icon className="h-4 w-4" />
71 </button>
72 ))}
73 </div>
74 </fieldset>
75 <fieldset className="m-0 border-0 p-0">
76 <legend className="mb-1.5 block text-xs font-medium uppercase tracking-wider text-zinc-500">Tint</legend>
77 <div className="flex flex-wrap gap-2">
78 {(Object.keys(LAUNCHER_TINTS) as LauncherTintName[]).map((key) => (
79 <button
80 key={key}
81 type="button"
82 onClick={() => onTintChange(key)}
83 aria-pressed={tint === key}
84 className={`flex items-center gap-2 rounded-lg border px-2.5 py-1.5 text-xs ${
85 tint === key
86 ? "border-accent-500/60 bg-accent-500/10 text-accent-300"
87 : "border-zinc-700/60 bg-zinc-800/60 text-zinc-400 hover:border-zinc-600/60 hover:text-zinc-200"
88 }`}
89 >
90 <span
91 className="h-3 w-3 rounded-full"
92 style={{ backgroundColor: LAUNCHER_TINTS[key] }}
93 aria-hidden="true"
94 />
95 {key}
96 </button>
97 ))}
98 </div>
99 </fieldset>
100 </div>
101 );
102};