Skip to content
149 linesCodeBlameRaw

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 */
7import type { Folio } from "@g1t/contracts";
8import { ArrowRightLeft, Copy, Download, Ellipsis, ExternalLink, History, LayoutTemplate, Link2, PencilLine, Share2, Star, Trash2 } from "lucide-react";
9import { type ReactNode, useState } from "react";
10import { Link, useNavigate, useRevalidator } from "react-router";
11
12import { 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.13import { 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.14import { DropdownMenu, DropdownMenuContent, DropdownMenuItem, DropdownMenuSeparator, DropdownMenuTrigger } from "../ui/dropdown-menu";
15import { foliosRequest, useFoliosData } from "./actions";
16import { MoveDialog, RenameDialog, TemplateDialog } from "./dialogs";
17import { ShareDialog } from "./share-dialog";
18
19export 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}