ComponentsCSS
App Window
Desktop-app frame for product previews: window dots, an address pill or title, optional actions and your content.
deploys.brightline.example/web-shop
- Live
Add rollback button to deploy list
main · 4 min ago
- Preview
Cap search query at 800 ms
fix/search-timeout · 38 min ago
- Replaced
Move invoices to the new PDF renderer
main · 2 h ago
Installation
npx shadcn@latest add https://ui-collections.vercel.app/r/tide/app-window.jsonAlso installs the theme, cn(). Packages: clsx, tailwind-merge.
Usage
app-window-demo.tsx
import { AppWindow } from "@/registry/kit/ui/app-window";
import { Badge } from "@/registry/kit/ui/badge";
const deploys = [
{ branch: "main", commit: "Add rollback button to deploy list", when: "4 min ago", status: "Live" },
{ branch: "fix/search-timeout", commit: "Cap search query at 800 ms", when: "38 min ago", status: "Preview" },
{ branch: "main", commit: "Move invoices to the new PDF renderer", when: "2 h ago", status: "Replaced" },
];
export default function AppWindowDemo() {
return (
<AppWindow url="deploys.brightline.example/web-shop" className="w-full max-w-2xl">
<ul className="divide-y divide-line">
{deploys.map((d) => (
<li key={d.commit} className="flex items-center justify-between gap-4 px-5 py-3.5">
<div className="min-w-0">
<p className="truncate text-sm font-medium">{d.commit}</p>
<p className="truncate text-xs text-muted">
{d.branch} · {d.when}
</p>
</div>
<Badge variant={d.status === "Live" ? "success" : d.status === "Preview" ? "info" : "outline"}>{d.status}</Badge>
</li>
))}
</ul>
</AppWindow>
);
}API
urlstringText in the address pill.
titleReactNodeShown instead of the address, e.g. a file name.
actionsReactNodeControls on the right of the title bar.
bodyClassNamestringClasses for the content area.
Source
components/ui/app-window.tsx
import type { HTMLAttributes, ReactNode } from "react";
import { cn } from "@/registry/kit/lib/utils";
export interface AppWindowProps extends Omit<HTMLAttributes<HTMLDivElement>, "title"> {
/** Text in the address pill, e.g. "app.lumen.example/polaris". Omit for a plain title bar. */
url?: string;
/** Shown instead of the address pill, e.g. a file name. */
title?: ReactNode;
/** Controls on the right of the title bar. */
actions?: ReactNode;
/** Classes for the content area under the title bar. */
bodyClassName?: string;
}
/**
* A desktop-app frame for product previews: traffic-light dots, an address
* pill or title, optional actions and your content. It is a frame, not a
* scroller: put long content in an inner element that scrolls.
*/
export function AppWindow({ url, title, actions, bodyClassName, className, children, ...props }: AppWindowProps) {
return (
<div
className={cn("surface relative isolate flex min-w-0 flex-col overflow-hidden rounded-card text-left text-fg", className)}
{...props}
>
<div className="grid grid-cols-[auto_minmax(0,1fr)_auto] items-center gap-3 border-b border-line px-3.5 py-2.5">
<span aria-hidden className="flex gap-1.5">
<span className="size-2.5 rounded-full bg-danger/70" />
<span className="size-2.5 rounded-full bg-warning/70" />
<span className="size-2.5 rounded-full bg-success/70" />
</span>
<div className="flex min-w-0 justify-center">
{url ? (
<span className="surface-field flex h-7 max-w-full min-w-0 items-center gap-1.5 rounded-control px-3 text-xs text-muted sm:min-w-56">
<svg aria-hidden viewBox="0 0 24 24" className="size-3 shrink-0" fill="none" stroke="currentColor" strokeWidth="2.2">
<rect x="5" y="11" width="14" height="10" rx="2" />
<path d="M8 11V8a4 4 0 0 1 8 0v3" />
</svg>
<span className="truncate">{url}</span>
</span>
) : (
title && <span className="truncate text-xs font-medium text-muted">{title}</span>
)}
</div>
<div className="flex min-w-[2.75rem] items-center justify-end gap-2">{actions}</div>
</div>
<div className={cn("min-w-0 flex-1", bodyClassName)}>{children}</div>
</div>
);
}