Components
AsyncView
The four-state async renderer every data screen needs: placeholder → loading → success / error. You own the async work and pass the status; AsyncView picks the view, with lens-ready defaults.
Installation
npx @glasskit-ui/cli add async-viewInstall the SDK (it provides GlassViewport, useDpad and the stylesheet), then copy these files into your project:
npm install @glasskit-ui/react// components/lib/utils.tsexport type ClassValue = string | number | null | undefined | false;/** * Join truthy class names. Dependency-free on purpose: the lens components * style via bespoke semantic classes (no conflicting Tailwind utilities to * de-dupe), so this needs no clsx/tailwind-merge and resolves from anywhere * the registry is vendored. */export function cn(...inputs: ClassValue[]): string { return inputs.filter(Boolean).join(" ");}/** * Accessible name from a free-form `label` prop: the label itself when it's a * plain string, otherwise undefined (a ReactNode can't become an aria-label). */export function stringLabel(label: unknown): string | undefined { return typeof label === "string" ? label : undefined;}// components/glasskit/async-view.tsximport type { ReactNode } from "react";import { cn } from "../lib/utils";export type AsyncStatus = "idle" | "loading" | "success" | "error";/** * <AsyncView> — the four-state async renderer every data screen needs: * placeholder (idle) → loading → success/error. The consumer owns the * async work and passes the current `status`; AsyncView picks the view. * This is the spine's one styled-with-logic piece — the logic is the * state→view selection, nothing more. * * Sensible additive defaults: an emitted pulse for loading, a dim line * for error. Override any state via its slot. */export function AsyncView({ status, children, loading, error, placeholder, errorLabel = "Couldn’t load", className,}: { status: AsyncStatus; /** Success content. */ children?: ReactNode; loading?: ReactNode; error?: ReactNode; placeholder?: ReactNode; /** Default error message when no `error` slot is given. */ errorLabel?: ReactNode; className?: string;}) { if (status === "success") return <>{children}</>; let body: ReactNode; if (status === "loading") { body = loading ?? <Spinner />; } else if (status === "error") { body = error ?? <p className="t-body gk-async-error">{errorLabel}</p>; } else { body = placeholder ?? null; } return ( <div className={cn("gk-async", className)} role="status" aria-busy={status === "loading"} > {body} </div> );}/** The default loading indicator — three emitted dots pulsing in sequence. */export function Spinner({ label = "Loading" }: { label?: string }) { return ( <span className="gk-spinner" role="img" aria-label={label}> <span /> <span /> <span /> </span> );}Usage
<AsyncView status={status} error={<Cue>Couldn’t load</Cue>}> <Readout label="Heart rate" value={bpm} unit="BPM" /></AsyncView>Props
Prop
Type
WeatherTile
A glanceable weather complication: a condition glyph + big temperature, the condition, and an optional location / hi-lo line. A popping surface.
EmptyState
The nothing-here screen: optional glyph + title + hint + one action. The quiet sibling of ErrorState — nothing failed, there's just no content yet. Pairs with AsyncView's placeholder slot.