g1t/apps/web/app/components/pin-button.tsx
Pick any line to see why it is the way it is: the commit, the pull request and issue it came from, and what the agent was thinking.
| Navigation: the workspace's own page with tabs, pins and recent in the sidebar | 1 | /** |
| 2 | * Pinning a project, so it stays at the top of the workspace's sidebar: | |
| 3 | * a button on the project's header and on each row of the Projects page. | |
| 4 | * Posts to the workspace's pins route (routes/workspace/pins.ts); while it | |
| 5 | * is on its way, it shows as done. | |
| 6 | */ | |
| 7 | import { Pin } from "lucide-react"; | |
| 8 | import { useFetcher } from "react-router"; | |
| 9 | ||
| 10 | import { cn } from "../lib/cn"; | |
| Pin button: its hint is the site's tooltip, not the browser's title | 11 | import { Tooltip, TooltipContent, TooltipTrigger } from "./ui/tooltip"; |
| Navigation: the workspace's own page with tabs, pins and recent in the sidebar | 12 | |
| 13 | export function PinButton({ | |
| 14 | workspace, | |
| 15 | slug, | |
| 16 | name, | |
| 17 | pinned, | |
| 18 | compact, | |
| Previews build from any branch; Agent in place of Ask AI; pin from the sidebar | 19 | small, |
| Navigation: the workspace's own page with tabs, pins and recent in the sidebar | 20 | className, |
| 21 | }: { | |
| 22 | workspace: string; | |
| 23 | slug: string; | |
| 24 | /** For what the button says to a screen reader. */ | |
| 25 | name: string; | |
| 26 | pinned: boolean; | |
| 27 | /** Only the pin, as on a row; the header says it in words too. */ | |
| 28 | compact?: boolean; | |
| Previews build from any branch; Agent in place of Ask AI; pin from the sidebar | 29 | /** Row-sized, for the sidebar. */ |
| 30 | small?: boolean; | |
| Navigation: the workspace's own page with tabs, pins and recent in the sidebar | 31 | className?: string; |
| 32 | }) { | |
| 33 | const fetcher = useFetcher<{ error: string | null }>({ key: `pin:${workspace}/${slug}` }); | |
| 34 | const asked = fetcher.formData?.get("intent"); | |
| 35 | const shown = asked === "pin" ? true : asked === "unpin" ? false : pinned; | |
| 36 | const label = shown ? `Unpin ${name}` : `Pin ${name}`; | |
| 37 | const error = fetcher.state === "idle" ? fetcher.data?.error : null; | |
| 38 | return ( | |
| 39 | <fetcher.Form method="post" action={`/${workspace}/-/pins`} className={cn("relative flex", className)}> | |
| 40 | <input type="hidden" name="intent" value={shown ? "unpin" : "pin"} /> | |
| 41 | <input type="hidden" name="slug" value={slug} /> | |
| Pin button: its hint is the site's tooltip, not the browser's title | 42 | <Tooltip> |
| 43 | <TooltipTrigger asChild> | |
| 44 | <button | |
| 45 | type="submit" | |
| 46 | aria-label={label} | |
| 47 | aria-pressed={shown} | |
| 48 | className={cn( | |
| 49 | "inline-flex shrink-0 items-center justify-center gap-1.5 rounded-md text-[0.8125rem] transition-colors outline-none focus-visible:ring-2 focus-visible:ring-accent", | |
| 50 | small | |
| 51 | ? "relative z-10 size-6 hover:bg-raised" | |
| 52 | : compact | |
| 53 | ? "relative z-10 size-8 hover:bg-raised" | |
| 54 | : "h-8 border border-line px-2.5 text-fg/80 hover:border-line-strong hover:bg-surface hover:text-fg", | |
| 55 | shown ? "text-accent" : compact || small ? "text-faint hover:text-fg" : "", | |
| 56 | error && "text-danger", | |
| 57 | )} | |
| 58 | > | |
| 59 | <Pin size={small ? 12 : 14} className={shown ? "fill-current" : undefined} /> | |
| 60 | {!compact && !small && <span className="hidden sm:inline">{shown ? "Unpin" : "Pin"}</span>} | |
| 61 | </button> | |
| 62 | </TooltipTrigger> | |
| 63 | <TooltipContent>{error ?? (shown ? "Unpin from the sidebar" : "Pin to the sidebar")}</TooltipContent> | |
| 64 | </Tooltip> | |
| Navigation: the workspace's own page with tabs, pins and recent in the sidebar | 65 | </fetcher.Form> |
| 66 | ); | |
| 67 | } |