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 | * The small dialogs an artifact's ⋯ menu opens, from Home's rows and from | |
| 3 | * its own page: rename, move (to a space or Private, inside a doc), and | |
| 4 | * save as a template. | |
| 5 | */ | |
| 6 | import { FOLIO_MAX_TITLE, type Folio, type FoliosSidebar, type Result } from "@g1t/contracts"; | |
| 7 | import { useEffect, useMemo, useState } from "react"; | |
| 8 | ||
| 9 | import { buildTree, canDo, flatten } 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. | 10 | import { ErrorText } from "../ui"; |
| 11 | 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. | 12 | import { Dialog, DialogContent, DialogDescription, DialogFooter, DialogHeader, DialogTitle } from "../ui/dialog"; |
| 13 | import { SelectField } from "../ui/select"; | |
| 14 | ||
| 15 | const FIELD = "h-9 w-full rounded-md border border-line bg-bg px-3 text-sm outline-none focus:border-accent/60"; | |
| 16 | ||
| 17 | export function RenameDialog({ open, onOpenChange, title, onSave }: { open: boolean; onOpenChange: (o: boolean) => void; title: string; onSave: (title: string) => Promise<Result<unknown>> }) { | |
| 18 | const [value, setValue] = useState(title); | |
| 19 | const [error, setError] = useState<string | null>(null); | |
| 20 | const [busy, setBusy] = useState(false); | |
| 21 | useEffect(() => { | |
| 22 | if (open) { | |
| 23 | setValue(title); | |
| 24 | setError(null); | |
| 25 | } | |
| 26 | }, [open, title]); | |
| 27 | return ( | |
| 28 | <Dialog open={open} onOpenChange={onOpenChange}> | |
| 29 | <DialogContent> | |
| 30 | <form | |
| 31 | onSubmit={async (e) => { | |
| 32 | e.preventDefault(); | |
| 33 | setBusy(true); | |
| 34 | const done = await onSave(value.trim()); | |
| 35 | setBusy(false); | |
| 36 | if (done.ok) onOpenChange(false); | |
| 37 | else setError(done.error.message); | |
| 38 | }} | |
| 39 | > | |
| 40 | <DialogHeader> | |
| 41 | <DialogTitle>Rename</DialogTitle> | |
| 42 | <DialogDescription>Its link keeps working: the address ends in its id.</DialogDescription> | |
| 43 | </DialogHeader> | |
| 44 | <div className="mt-4 space-y-3"> | |
| 45 | <input aria-label="Title" autoFocus maxLength={FOLIO_MAX_TITLE} placeholder="Untitled" value={value} onChange={(e) => setValue(e.target.value)} className={FIELD} /> | |
| 46 | {error && <ErrorText>{error}</ErrorText>} | |
| 47 | </div> | |
| 48 | <DialogFooter className="mt-4"> | |
| 49 | <Button type="submit" variant="accent" disabled={busy}> | |
| 50 | {busy ? "Saving…" : "Rename"} | |
| 51 | </Button> | |
| 52 | </DialogFooter> | |
| 53 | </form> | |
| 54 | </DialogContent> | |
| 55 | </Dialog> | |
| 56 | ); | |
| 57 | } | |
| 58 | ||
| 59 | /** | |
| 60 | * Where an artifact can go: the top of a space the viewer can edit, or of | |
| 61 | * their own Private, or inside a doc there. Never inside itself or | |
| 62 | * anything under it. | |
| 63 | */ | |
| 64 | export function MoveDialog({ | |
| 65 | sidebar, | |
| 66 | folio, | |
| 67 | me, | |
| 68 | open, | |
| 69 | onOpenChange, | |
| 70 | onMove, | |
| 71 | }: { | |
| 72 | sidebar: FoliosSidebar | null | undefined; | |
| 73 | folio: Pick<Folio, "id" | "title" | "space" | "parent_id" | "owner">; | |
| 74 | /** The viewer's member key: only an artifact's owner can move it to their Private. */ | |
| 75 | me: string; | |
| 76 | open: boolean; | |
| 77 | onOpenChange: (o: boolean) => void; | |
| 78 | onMove: (spaceId: string | null, parentId: string | null) => Promise<Result<unknown>>; | |
| 79 | }) { | |
| 80 | const mine = `user:${folio.owner.id}` === me; | |
| 81 | const places = useMemo(() => { | |
| 82 | const out: { value: string; label: string; tree: ReturnType<typeof buildTree> }[] = []; | |
| 83 | if (mine) out.push({ value: "private", label: "Private (just you)", tree: buildTree(sidebar?.private_tree ?? []) }); | |
| 84 | for (const s of sidebar?.spaces ?? []) if (canDo(s.viewer_role, "edit")) out.push({ value: s.id, label: s.name, tree: buildTree(s.tree) }); | |
| 85 | return out; | |
| 86 | }, [sidebar, mine]); | |
| 87 | const [place, setPlace] = useState(folio.space?.id ?? "private"); | |
| 88 | const [parent, setParent] = useState(folio.parent_id ?? ""); | |
| 89 | const [error, setError] = useState<string | null>(null); | |
| 90 | const [busy, setBusy] = useState(false); | |
| 91 | useEffect(() => { | |
| 92 | if (!open) return; | |
| 93 | setPlace(folio.space?.id ?? (mine ? "private" : (places[0]?.value ?? ""))); | |
| 94 | setParent(folio.parent_id ?? ""); | |
| 95 | setError(null); | |
| 96 | }, [open, folio.space?.id, folio.parent_id, mine, places]); | |
| 97 | const docs = useMemo(() => { | |
| 98 | const tree = flatten(places.find((p) => p.value === place)?.tree ?? []); | |
| 99 | const hidden = new Set<string>([folio.id]); | |
| 100 | for (const item of tree) if (item.parent_id && hidden.has(item.parent_id)) hidden.add(item.id); | |
| 101 | return tree.filter((i) => i.kind === "doc" && !hidden.has(i.id)); | |
| 102 | }, [places, place, folio.id]); | |
| 103 | return ( | |
| 104 | <Dialog open={open} onOpenChange={onOpenChange}> | |
| 105 | <DialogContent> | |
| 106 | <DialogHeader> | |
| 107 | <DialogTitle>Move “{folio.title || "Untitled"}”</DialogTitle> | |
| 108 | <DialogDescription>Everything inside it moves with it. In a space, it follows the space's access unless it's set to only people invited.</DialogDescription> | |
| 109 | </DialogHeader> | |
| 110 | {places.length === 0 ? ( | |
| 111 | <p className="text-sm text-muted">There's nowhere you can move it: you can't add to any space, and only its owner can move it to their Private.</p> | |
| 112 | ) : ( | |
| 113 | <div className="space-y-3"> | |
| 114 | <SelectField | |
| 115 | aria-label="Where" | |
| 116 | value={place} | |
| 117 | onValueChange={(v) => { | |
| 118 | setPlace(v); | |
| 119 | setParent(""); | |
| 120 | }} | |
| 121 | options={places.map((p) => ({ value: p.value, label: p.label }))} | |
| 122 | className="h-9 w-full" | |
| 123 | /> | |
| 124 | <SelectField | |
| 125 | aria-label="Inside" | |
| 126 | value={parent} | |
| 127 | onValueChange={setParent} | |
| 128 | options={[{ value: "", label: "At the top" }, ...docs.map((p) => ({ value: p.id, label: `${" ".repeat(p.depth)}${p.icon ?? ""} ${p.title || "Untitled"}`.trim() }))]} | |
| 129 | className="h-9 w-full" | |
| 130 | /> | |
| 131 | {error && <ErrorText>{error}</ErrorText>} | |
| 132 | </div> | |
| 133 | )} | |
| 134 | <DialogFooter> | |
| 135 | <Button | |
| 136 | type="button" | |
| 137 | variant="accent" | |
| 138 | disabled={busy || places.length === 0} | |
| 139 | onClick={async () => { | |
| 140 | setBusy(true); | |
| 141 | const done = await onMove(place === "private" ? null : place, parent || null); | |
| 142 | setBusy(false); | |
| 143 | if (done.ok) onOpenChange(false); | |
| 144 | else setError(done.error.message); | |
| 145 | }} | |
| 146 | > | |
| 147 | {busy ? "Moving…" : "Move"} | |
| 148 | </Button> | |
| 149 | </DialogFooter> | |
| 150 | </DialogContent> | |
| 151 | </Dialog> | |
| 152 | ); | |
| 153 | } | |
| 154 | ||
| 155 | /** Save as a template. From something not everyone can read, it says so first: the template is everyone's (plan section 4.3 rule 8). */ | |
| 156 | export function TemplateDialog({ open, onOpenChange, title, widens, onSave }: { open: boolean; onOpenChange: (o: boolean) => void; title: string; widens: boolean; onSave: (name: string, description: string) => Promise<Result<unknown>> }) { | |
| 157 | const [name, setName] = useState(title); | |
| 158 | const [description, setDescription] = useState(""); | |
| 159 | const [error, setError] = useState<string | null>(null); | |
| 160 | useEffect(() => { | |
| 161 | if (open) { | |
| 162 | setName(title); | |
| 163 | setError(null); | |
| 164 | } | |
| 165 | }, [open, title]); | |
| 166 | return ( | |
| 167 | <Dialog open={open} onOpenChange={onOpenChange}> | |
| 168 | <DialogContent> | |
| 169 | <DialogHeader> | |
| 170 | <DialogTitle>Save as a template</DialogTitle> | |
| 171 | <DialogDescription>Everyone in the workspace will be able to use this template.</DialogDescription> | |
| 172 | </DialogHeader> | |
| 173 | <div className="space-y-3"> | |
| 174 | {widens && <p className="rounded-md border border-warn/30 bg-warn/10 px-3 py-2 text-xs leading-relaxed text-warn">Not everyone in the workspace can open this now. Its content will be in the template, which everyone can use.</p>} | |
| 175 | <input aria-label="Name" value={name} onChange={(e) => setName(e.target.value)} className={FIELD} /> | |
| 176 | <input aria-label="What it's for" placeholder="What it's for" value={description} onChange={(e) => setDescription(e.target.value)} className={FIELD} /> | |
| 177 | {error && <ErrorText>{error}</ErrorText>} | |
| 178 | </div> | |
| 179 | <DialogFooter> | |
| 180 | <Button | |
| 181 | type="button" | |
| 182 | variant="accent" | |
| 183 | onClick={async () => { | |
| 184 | const done = await onSave(name, description); | |
| 185 | if (done.ok) onOpenChange(false); | |
| 186 | else setError(done.error.message); | |
| 187 | }} | |
| 188 | > | |
| 189 | Save template | |
| 190 | </Button> | |
| 191 | </DialogFooter> | |
| 192 | </DialogContent> | |
| 193 | </Dialog> | |
| 194 | ); | |
| 195 | } |