import { useQuery } from "@tanstack/react-query"; import { isLauncherIconName, isLauncherTintName, LAUNCHER_ICONS, LAUNCHER_TINTS, type LauncherIconName, type LauncherTintName, } from "@/client/lib/launcher-icons"; import { fetchJson, queryKeys } from "@/client/lib/query"; import { Skeleton } from "@/client/components/ui/skeleton"; interface LauncherApp { id: string; name: string; url: string; icon: LauncherIconName | null; tint: LauncherTintName | null; } const initialFromName = (name: string): string => (name.trim()[0] ?? "?").toUpperCase(); const safeHost = (uri: string): string | null => { try { return new URL(uri).host; } catch { return null; } }; const Tile = ({ app }: { app: LauncherApp }) => { const Icon = app.icon && isLauncherIconName(app.icon) ? LAUNCHER_ICONS[app.icon] : null; // Default for tiles without a tint: low-chroma zinc so they read as // "tessera-suite default" rather than mimicking any specific brand. const tint = app.tint && isLauncherTintName(app.tint) ? LAUNCHER_TINTS[app.tint] : "oklch(0.30 0.005 265)"; // Worker filters URLs to http(s); this guard keeps a row that slipped // through pre-validation from crashing the page. const host = safeHost(app.url); return ( {app.name} {host ? {host} : null} ); }; interface LauncherProps { greetingName: string; } const SKELETON_TILE_COUNT = 4; const LauncherTileGridSkeleton = () => (
); export const LauncherSkeleton = () => (Pick an app to get to work.
{error ? ({error}
) : null} {apps && apps.length === 0 ? (No launcher tiles are registered yet. An admin can add one at{" "} /admin/launcher-apps .