import * as React from "react"; import { IconBold, IconCheck, IconChevronDown, IconItalic, IconLink, IconList, IconSearch, IconStrikethrough, } from "@tabler/icons-react"; import { cn, Kbd, motion } from "@colineapp/ui"; import { LiquidGlass, LiquidGlassTabLabel, useLiquidGlassHighlight, useLiquidGlassTabs, type LiquidGlassHandle, } from "@colineapp/liquid-glass/react"; import { useDraggable } from "./stage"; /* The package's surfaces, built the way its README says to. Every glass surface here is a positioned, rounded element with first and nothing painted over it. */ // Ease-out cubic, the curve the glass materializes on. const EASE = [0.33, 1, 0.68, 1] as const; // ── Pill tabs ─────────────────────────────────────────── export function GlassTabs({ options, value, onChange, className, label, }: { options: string[]; value: string; onChange: (value: string) => void; className?: string; label: string; }) { const track = React.useRef(null); useLiquidGlassTabs(track, { appearance: "class" }); const index = options.indexOf(value); return (
{ const step = event.key === "ArrowRight" ? 1 : event.key === "ArrowLeft" ? -1 : 0; if (!step) return; event.preventDefault(); const next = options[(index + step + options.length) % options.length]; if (next === undefined) return; onChange(next); track.current?.querySelector(`[data-tab="${next}"]`)?.focus(); }} > {options.map((option) => ( ))}
); } // ── Search field ──────────────────────────────────────── export function GlassSearch({ className, placeholder = "Search" }: { className?: string; placeholder?: string }) { const [value, setValue] = React.useState(""); return ( ); } // ── Toolbar ───────────────────────────────────────────── const TOOLS = [ { id: "bold", label: "Bold", icon: IconBold }, { id: "italic", label: "Italic", icon: IconItalic }, { id: "strike", label: "Strikethrough", icon: IconStrikethrough }, { id: "link", label: "Link", icon: IconLink }, { id: "list", label: "List", icon: IconList }, ] as const; export function GlassToolbar({ className }: { className?: string }) { const [on, setOn] = React.useState>(() => new Set(["bold"])); return (
{TOOLS.map(({ id, label, icon: Icon }) => { const active = on.has(id); return ( ); })}
); } // ── Menu ──────────────────────────────────────────────── const SORTS = ["Date added", "Name", "Size", "Kind"]; /** * Glass menu, as iOS 27's: it pours out of its button and covers it, the button * becoming the menu, and closing it draws back into the button as a drop. */ export function GlassMenu({ className }: { className?: string }) { const [open, setOpen] = React.useState(false); const [sort, setSort] = React.useState(SORTS[0]!); const root = React.useRef(null); const button = React.useRef(null); const glass = React.useRef(null); // While it draws back, pressing the button pours it straight back out. const closing = React.useRef(false); const close = React.useCallback(() => { if (closing.current) return; closing.current = true; void glass.current?.drawBack().then((gone) => { if (!gone || !closing.current) return; closing.current = false; setOpen(false); }); }, []); const reopen = () => { closing.current = false; if (button.current) void glass.current?.pourFrom(button.current, { cover: true }); }; React.useEffect(() => { if (!open) return; const onDown = (event: PointerEvent) => { if (!root.current?.contains(event.target as Node)) close(); }; const onKey = (event: KeyboardEvent) => { if (event.key === "Escape") close(); }; window.addEventListener("pointerdown", onDown); window.addEventListener("keydown", onKey); return () => { window.removeEventListener("pointerdown", onDown); window.removeEventListener("keydown", onKey); }; }, [open, close]); return (
{open ? ( { setSort(item); close(); }} /> ) : null}
); } function SortMenu({ button, glass, sort, onChoose, }: { button: React.RefObject; glass: React.RefObject; sort: string; onChoose: (item: string) => void; }) { const menu = React.useRef(null); // A pill under the row you point at, stretching toward the next one. useLiquidGlassHighlight(menu); // Placed below its button, it pours out of it and over it. React.useLayoutEffect(() => { if (button.current) void glass.current?.pourFrom(button.current, { cover: true }); }, [button, glass]); return (
{SORTS.map((item) => ( ))}
); } // ── Dialog ────────────────────────────────────────────── /** A glass dialog inside its stage: floating="dialog", materializing as it zooms in. */ export function GlassDialog({ className }: { className?: string }) { const [open, setOpen] = React.useState(false); const [leaving, setLeaving] = React.useState(false); const [hover, setHover] = React.useState(false); const glass = React.useRef(null); const trigger = React.useRef(null); const first = React.useRef(null); const close = React.useCallback(() => { setLeaving(true); void glass.current?.dematerialize({ scale: 0.98 }).then(() => { setOpen(false); setLeaving(false); trigger.current?.focus(); }); }, []); React.useLayoutEffect(() => { if (!open || leaving) return; void glass.current?.materialize({ scale: 0.965 }); first.current?.focus(); }, [open, leaving]); return (
<> {open ? (
event.key === "Escape" && close()}>
) : null}
); } // ── Card ──────────────────────────────────────────────── /** Regular glass over text: drag it and the rim bends the letters it crosses. */ export function GlassCard({ initial = { x: 0.5, y: 0.5 } }: { initial?: { x: number; y: number } }) { const drag = useDraggable({ initial }); return (
Drag me over the text The rim bends each letter as it crosses.
); }