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 = () => (
); // Signed-in landing surface: a tile grid of apps in the limic suite. // A tile click hard-navigates to the app's `url`; the app's own auth // flow (tessera OIDC, Cloudflare Access, etc.) takes over from there. // We don't force a `/authorize` handshake from the launcher itself // because the user might just be opening a homepage. export const Launcher = ({ greetingName }: LauncherProps) => { const { data: apps = null, error: fetchError } = useQuery({ queryKey: queryKeys.launcherApps(), queryFn: () => fetchJson("/api/launcher"), }); const error = fetchError instanceof Error ? `Could not load apps (${fetchError.message}).` : null; return (

{greetingName}.

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 .

) : null} {apps === null && !error ? ( // Reserve the same vertical rhythm so the page doesn't shift // when the API call resolves. Four skeleton tiles ≈ the median // suite size; the layout settles immediately if there are more. ) : null} {apps && apps.length > 0 ? (
{apps.map((app) => ( ))}
) : null}
); };