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.
| Artifacts is a mode in the rail, the shell and the phone's tab bar in place of Docs: its home lists everything you can open by day or as cards under All, Yours and Shared with you, with search, filters and tiles to start a doc (slides, designs and dashboards say they are coming soon), and its sidebar holds favorites, spaces, Private, Shared, projects' docs and the trash. | 1 | /** |
| 2 | * An artifact's ⋯ menu, the same on Home's rows and cards and in its own | |
| 3 | * header: open in a new tab, favorite, share, rename, duplicate, move, | |
| 4 | * copy the link, export, save as a template, move to the trash. What the | |
| 5 | * viewer's role doesn't allow isn't offered. | |
| 6 | */ | |
| 7 | import type { Folio } from "@g1t/contracts"; | |
| 8 | import { ArrowRightLeft, Copy, Download, Ellipsis, ExternalLink, History, LayoutTemplate, Link2, PencilLine, Share2, Star, Trash2 } from "lucide-react"; | |
| 9 | import { type ReactNode, useState } from "react"; | |
| 10 | import { Link, useNavigate, useRevalidator } from "react-router"; | |
| 11 | ||
| 12 | import { canDo } from "../../lib/folios"; | |
| The site is built from its shared components: one Button with its variants and sizes, one Badge, Card for every panel, Alert for notices, Kbd for keys, Table for the security tables, and Avatar, Command, Progress, ScrollArea, ToggleGroup, Collapsible and Accordion in the kit; raw buttons, hand-drawn cards and pills and per-file button class lists are gone, outline buttons stand the same height as filled ones, and the phone's Ask g1t loses its tile. Every page was compared pixel by pixel before and after. | 13 | import { Button } from "../ui/button"; |
| Artifacts is a mode in the rail, the shell and the phone's tab bar in place of Docs: its home lists everything you can open by day or as cards under All, Yours and Shared with you, with search, filters and tiles to start a doc (slides, designs and dashboards say they are coming soon), and its sidebar holds favorites, spaces, Private, Shared, projects' docs and the trash. | 14 | import { DropdownMenu, DropdownMenuContent, DropdownMenuItem, DropdownMenuSeparator, DropdownMenuTrigger } from "../ui/dropdown-menu"; |
| 15 | import { foliosRequest, useFoliosData } from "./actions"; | |
| 16 | import { MoveDialog, RenameDialog, TemplateDialog } from "./dialogs"; | |
| 17 | import { ShareDialog } from "./share-dialog"; | |
| 18 | ||
| 19 | export function FolioMenu({ | |
| 20 | slug, | |
| 21 | folio, | |
| 22 | onError, | |
| 23 | onTrashed, | |
| 24 | page = false, | |
| 25 | trigger, | |
| 26 | }: { | |
| 27 | slug: string; | |
| 28 | folio: Folio; | |
| 29 | onError?: (message: string) => void; | |
| 30 | /** After it went to the trash (its own page goes back Home). */ | |
| 31 | onTrashed?: () => void; | |
| 32 | /** On its own page: History is offered, and "Open in new tab" isn't. */ | |
| 33 | page?: boolean; | |
| 34 | trigger?: ReactNode; | |
| 35 | }) { | |
| 36 | const layout = useFoliosData(); | |
| 37 | const navigate = useNavigate(); | |
| 38 | const { revalidate } = useRevalidator(); | |
| 39 | const [sharing, setSharing] = useState(false); | |
| 40 | const [renaming, setRenaming] = useState(false); | |
| 41 | const [moving, setMoving] = useState(false); | |
| 42 | const [templating, setTemplating] = useState(false); | |
| 43 | const editable = canDo(folio.viewer_role, "edit"); | |
| 44 | const fail = (message: string) => onError?.(message); | |
| 45 | const send = async <T,>(intent: string, body: Record<string, unknown>) => { | |
| 46 | const done = await foliosRequest<T>(slug, intent, { folio_id: folio.id, ...body }); | |
| 47 | if (done.ok) void revalidate(); | |
| 48 | else fail(done.error.message); | |
| 49 | return done; | |
| 50 | }; | |
| 51 | return ( | |
| 52 | <> | |
| 53 | <DropdownMenu> | |
| 54 | <DropdownMenuTrigger asChild> | |
| 55 | {trigger ?? ( | |
| The site is built from its shared components: one Button with its variants and sizes, one Badge, Card for every panel, Alert for notices, Kbd for keys, Table for the security tables, and Avatar, Command, Progress, ScrollArea, ToggleGroup, Collapsible and Accordion in the kit; raw buttons, hand-drawn cards and pills and per-file button class lists are gone, outline buttons stand the same height as filled ones, and the phone's Ask g1t loses its tile. Every page was compared pixel by pixel before and after. | 56 | <Button type="button" aria-label={`More for ${folio.title || "Untitled"}`} variant="ghost" size="icon-sm" className="text-faint max-md:size-10"> |
| Artifacts is a mode in the rail, the shell and the phone's tab bar in place of Docs: its home lists everything you can open by day or as cards under All, Yours and Shared with you, with search, filters and tiles to start a doc (slides, designs and dashboards say they are coming soon), and its sidebar holds favorites, spaces, Private, Shared, projects' docs and the trash. | 57 | <Ellipsis size={16} /> |
| The site is built from its shared components: one Button with its variants and sizes, one Badge, Card for every panel, Alert for notices, Kbd for keys, Table for the security tables, and Avatar, Command, Progress, ScrollArea, ToggleGroup, Collapsible and Accordion in the kit; raw buttons, hand-drawn cards and pills and per-file button class lists are gone, outline buttons stand the same height as filled ones, and the phone's Ask g1t loses its tile. Every page was compared pixel by pixel before and after. | 58 | </Button> |
| Artifacts is a mode in the rail, the shell and the phone's tab bar in place of Docs: its home lists everything you can open by day or as cards under All, Yours and Shared with you, with search, filters and tiles to start a doc (slides, designs and dashboards say they are coming soon), and its sidebar holds favorites, spaces, Private, Shared, projects' docs and the trash. | 59 | )} |
| 60 | </DropdownMenuTrigger> | |
| 61 | <DropdownMenuContent align="end" className="w-56"> | |
| 62 | {!page && ( | |
| 63 | <DropdownMenuItem asChild> | |
| 64 | <a href={folio.path} target="_blank" rel="noopener"> | |
| 65 | <ExternalLink size={14} /> Open in new tab | |
| 66 | </a> | |
| 67 | </DropdownMenuItem> | |
| 68 | )} | |
| 69 | <DropdownMenuItem onSelect={() => void send("favorite", { on: !folio.favorite })}> | |
| 70 | <Star size={14} className={folio.favorite ? "fill-current text-warn" : ""} /> {folio.favorite ? "Remove from Favorites" : "Add to Favorites"} | |
| 71 | </DropdownMenuItem> | |
| 72 | <DropdownMenuItem onSelect={() => setSharing(true)}> | |
| 73 | <Share2 size={14} /> Share… | |
| 74 | </DropdownMenuItem> | |
| 75 | {page && ( | |
| 76 | <DropdownMenuItem asChild> | |
| 77 | <Link to={`${folio.path}/history`}> | |
| 78 | <History size={14} /> History | |
| 79 | </Link> | |
| 80 | </DropdownMenuItem> | |
| 81 | )} | |
| 82 | <DropdownMenuSeparator /> | |
| 83 | {editable && ( | |
| 84 | <DropdownMenuItem onSelect={() => setRenaming(true)}> | |
| 85 | <PencilLine size={14} /> Rename | |
| 86 | </DropdownMenuItem> | |
| 87 | )} | |
| 88 | <DropdownMenuItem | |
| 89 | onSelect={async () => { | |
| 90 | const made = await send<Folio>("duplicate", {}); | |
| 91 | if (made.ok) navigate(made.value.path); | |
| 92 | }} | |
| 93 | > | |
| 94 | <Copy size={14} /> Duplicate | |
| 95 | </DropdownMenuItem> | |
| 96 | {editable && ( | |
| 97 | <DropdownMenuItem onSelect={() => setMoving(true)}> | |
| 98 | <ArrowRightLeft size={14} /> Move to… | |
| 99 | </DropdownMenuItem> | |
| 100 | )} | |
| 101 | <DropdownMenuItem | |
| 102 | onSelect={async () => { | |
| 103 | try { | |
| 104 | await navigator.clipboard.writeText(`${window.location.origin}${folio.path}`); | |
| 105 | } catch { | |
| 106 | fail("Your browser didn't allow copying."); | |
| 107 | } | |
| 108 | }} | |
| 109 | > | |
| 110 | <Link2 size={14} /> Copy link | |
| 111 | </DropdownMenuItem> | |
| 112 | <DropdownMenuItem asChild> | |
| 113 | <a href={`/${slug}/-/artifacts/export?folio=${encodeURIComponent(folio.id)}`}> | |
| 114 | <Download size={14} /> Export Markdown | |
| 115 | </a> | |
| 116 | </DropdownMenuItem> | |
| 117 | <DropdownMenuItem onSelect={() => setTemplating(true)}> | |
| 118 | <LayoutTemplate size={14} /> Save as template | |
| 119 | </DropdownMenuItem> | |
| 120 | {editable && ( | |
| 121 | <> | |
| 122 | <DropdownMenuSeparator /> | |
| 123 | <DropdownMenuItem | |
| 124 | onSelect={async () => { | |
| 125 | const done = await send("trash", {}); | |
| 126 | if (done.ok) onTrashed?.(); | |
| 127 | }} | |
| 128 | className="text-danger focus:text-danger" | |
| 129 | > | |
| 130 | <Trash2 size={14} /> Move to trash | |
| 131 | </DropdownMenuItem> | |
| 132 | </> | |
| 133 | )} | |
| 134 | </DropdownMenuContent> | |
| 135 | </DropdownMenu> | |
| 136 | <ShareDialog slug={slug} folio={folio} open={sharing} onOpenChange={setSharing} onChanged={() => void revalidate()} /> | |
| 137 | <RenameDialog open={renaming} onOpenChange={setRenaming} title={folio.title} onSave={(title) => send("update", { change: { title } })} /> | |
| 138 | <MoveDialog | |
| 139 | sidebar={layout?.sidebar} | |
| 140 | folio={folio} | |
| 141 | me={layout?.me.key ?? ""} | |
| 142 | open={moving} | |
| 143 | onOpenChange={setMoving} | |
| 144 | onMove={(spaceId, parentId) => send("move", { move: { space_id: spaceId, parent_id: parentId, before_id: null } })} | |
| 145 | /> | |
| 146 | <TemplateDialog open={templating} onOpenChange={setTemplating} title={folio.title || "Untitled"} widens={folio.general_access !== "workspace" && !(folio.space?.kind === "workspace" && folio.inherit)} onSave={(name, description) => send("save_template", { name, description })} /> | |
| 147 | </> | |
| 148 | ); | |
| 149 | } |