File
Blob: src/client/components/admin/launcher-tile-picker.tsx
| 1 | import { |
| 2 | LAUNCHER_ICONS, |
| 3 | LAUNCHER_TINTS, |
| 4 | type LauncherIconName, |
| 5 | type LauncherTintName, |
| 6 | } from "@/client/lib/launcher-icons"; |
| 7 | |
| 8 | interface LauncherTilePickerProps { |
| 9 | icon: LauncherIconName | ""; |
| 10 | tint: LauncherTintName | ""; |
| 11 | name: string; |
| 12 | onIconChange: (icon: LauncherIconName | "") => void; |
| 13 | onTintChange: (tint: LauncherTintName | "") => void; |
| 14 | } |
| 15 | |
| 16 | export 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 | }; |