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 | * A space's settings, as a new space asks for them and its settings page | |
| 3 | * changes them: name, icon, address, who it is for, what they may do, | |
| 4 | * how agents change what is in it, and the projects it is about. | |
| 5 | */ | |
| 6 | import { DOC_AGENT_MODE_LABELS, DOC_ROLE_LABELS, DOC_SPACE_KIND_LABELS, type DocAgentMode, type DocRole, type DocSpace, type DocSpaceKind } from "@g1t/contracts"; | |
| 7 | import { type FormEvent, useState } from "react"; | |
| 8 | ||
| 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. | 9 | import { ErrorText } from "../ui"; |
| 10 | 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. | 11 | import { SelectField } from "../ui/select"; |
| 12 | ||
| 13 | export type SpaceFormValue = { | |
| 14 | name: string; | |
| 15 | icon: string; | |
| 16 | slug: string; | |
| 17 | description: string; | |
| 18 | kind: DocSpaceKind; | |
| 19 | team: string; | |
| 20 | default_role: DocRole; | |
| 21 | agent_mode: DocAgentMode; | |
| 22 | projects: string; | |
| 23 | }; | |
| 24 | ||
| 25 | export function initialSpace(space?: DocSpace | null): SpaceFormValue { | |
| 26 | return { | |
| 27 | name: space?.name ?? "", | |
| 28 | icon: space?.icon ?? "", | |
| 29 | slug: space?.slug ?? "", | |
| 30 | description: space?.description ?? "", | |
| 31 | kind: space?.kind ?? "workspace", | |
| 32 | team: space?.team ?? "", | |
| 33 | default_role: space?.default_role ?? "edit", | |
| 34 | agent_mode: space?.agent_mode ?? "suggest", | |
| 35 | projects: (space?.projects ?? []).join(", "), | |
| 36 | }; | |
| 37 | } | |
| 38 | ||
| 39 | const FIELD = "h-9 w-full rounded-md border border-line bg-bg px-3 text-sm text-fg outline-none placeholder:text-faint focus:border-accent/60"; | |
| 40 | ||
| 41 | function Row({ label, hint, children }: { label: string; hint?: string; children: React.ReactNode }) { | |
| 42 | return ( | |
| 43 | <div className="grid gap-1.5 sm:grid-cols-[12rem_minmax(0,1fr)] sm:gap-6"> | |
| 44 | <div> | |
| 45 | <div className="text-sm font-medium">{label}</div> | |
| 46 | {hint && <p className="mt-0.5 text-xs leading-relaxed text-faint">{hint}</p>} | |
| 47 | </div> | |
| 48 | <div className="min-w-0">{children}</div> | |
| 49 | </div> | |
| 50 | ); | |
| 51 | } | |
| 52 | ||
| 53 | export function SpaceForm({ | |
| 54 | initial, | |
| 55 | teams, | |
| 56 | isDefault = false, | |
| 57 | submitLabel, | |
| 58 | busy, | |
| 59 | error, | |
| 60 | onSubmit, | |
| 61 | }: { | |
| 62 | initial: SpaceFormValue; | |
| 63 | teams: { slug: string; name: string }[]; | |
| 64 | isDefault?: boolean; | |
| 65 | submitLabel: string; | |
| 66 | busy: boolean; | |
| 67 | error: string | null; | |
| 68 | onSubmit: (value: SpaceFormValue) => void; | |
| 69 | }) { | |
| 70 | const [v, setV] = useState(initial); | |
| 71 | const set = <K extends keyof SpaceFormValue>(key: K, value: SpaceFormValue[K]) => setV((was) => ({ ...was, [key]: value })); | |
| 72 | const submit = (e: FormEvent) => { | |
| 73 | e.preventDefault(); | |
| 74 | onSubmit(v); | |
| 75 | }; | |
| 76 | const roles = (["view", "comment", "edit"] as DocRole[]).map((r) => ({ value: r, label: DOC_ROLE_LABELS[r] })); | |
| 77 | return ( | |
| 78 | <form onSubmit={submit} className="space-y-6"> | |
| 79 | <Row label="Name"> | |
| 80 | <div className="flex gap-2"> | |
| 81 | <input aria-label="Icon (an emoji)" value={v.icon} onChange={(e) => set("icon", e.target.value)} placeholder="📚" maxLength={8} className={`${FIELD} w-14 text-center`} /> | |
| 82 | <input aria-label="Name" required value={v.name} onChange={(e) => set("name", e.target.value)} placeholder="Engineering" maxLength={80} className={FIELD} /> | |
| 83 | </div> | |
| 84 | </Row> | |
| 85 | <Row label="Description" hint="One line on what belongs here."> | |
| 86 | <input aria-label="Description" value={v.description} onChange={(e) => set("description", e.target.value)} maxLength={300} placeholder="Specs, runbooks and decisions for the platform team" className={FIELD} /> | |
| 87 | </Row> | |
| 88 | <Row label="Address" hint="In the space's links. Left empty, it comes from the name."> | |
| 89 | <input aria-label="Address" value={v.slug} onChange={(e) => set("slug", e.target.value.toLowerCase())} placeholder="engineering" pattern="[a-z0-9](?:[a-z0-9-]{0,38}[a-z0-9])?" className={`${FIELD} font-mono`} /> | |
| 90 | </Row> | |
| 91 | <Row label="Who it's for" hint={isDefault ? "The General space is always the whole workspace's." : "Members-only spaces are only their members'. Not even owners see them unless added."}> | |
| 92 | <SelectField | |
| 93 | aria-label="Who it's for" | |
| 94 | value={v.kind} | |
| 95 | disabled={isDefault} | |
| 96 | onValueChange={(kind) => set("kind", kind as DocSpaceKind)} | |
| 97 | options={(["workspace", "team", "private"] as DocSpaceKind[]).filter((k) => k !== "team" || teams.length > 0).map((k) => ({ value: k, label: DOC_SPACE_KIND_LABELS[k] }))} | |
| 98 | className="h-9 w-full" | |
| 99 | /> | |
| 100 | </Row> | |
| 101 | {v.kind === "team" && ( | |
| 102 | <Row label="Team"> | |
| 103 | <SelectField aria-label="Team" value={v.team} onValueChange={(team) => set("team", team)} placeholder="Choose a team" options={teams.map((t) => ({ value: t.slug, label: t.name }))} className="h-9 w-full" /> | |
| 104 | </Row> | |
| 105 | )} | |
| 106 | {v.kind !== "private" && ( | |
| 107 | <Row label={v.kind === "team" ? "The team can" : "Everyone can"} hint="Members you add below can be given more."> | |
| 108 | <SelectField aria-label="What they can do" value={v.default_role} onValueChange={(r) => set("default_role", r as DocRole)} options={roles} className="h-9 w-full" /> | |
| 109 | </Row> | |
| 110 | )} | |
| 111 | <Row label="Agents in this space" hint="An agent never does more than the person it works for. With Suggest, its changes wait for a person to accept them."> | |
| 112 | <SelectField | |
| 113 | aria-label="Agents in this space" | |
| 114 | value={v.agent_mode} | |
| 115 | onValueChange={(m) => set("agent_mode", m as DocAgentMode)} | |
| 116 | options={(["suggest", "edit"] as DocAgentMode[]).map((m) => ({ | |
| 117 | value: m, | |
| 118 | label: DOC_AGENT_MODE_LABELS[m], | |
| 119 | description: m === "suggest" ? "Tracked changes you accept or reject (recommended)" : "Agents edit directly; every edit is in the history", | |
| 120 | }))} | |
| 121 | className="h-9 w-full" | |
| 122 | /> | |
| 123 | </Row> | |
| 124 | <Row label="Projects" hint="owner/name, comma separated. Artifacts can be filtered by project."> | |
| 125 | <input aria-label="Projects" value={v.projects} onChange={(e) => set("projects", e.target.value)} placeholder="acme/web, acme/api" className={`${FIELD} font-mono`} /> | |
| 126 | </Row> | |
| 127 | {error && <ErrorText>{error}</ErrorText>} | |
| 128 | <div className="flex justify-end"> | |
| 129 | <Button type="submit" variant="accent" disabled={busy}> | |
| 130 | {busy ? "Saving…" : submitLabel} | |
| 131 | </Button> | |
| 132 | </div> | |
| 133 | </form> | |
| 134 | ); | |
| 135 | } | |
| 136 | ||
| 137 | /** The form's value as the service takes it. */ | |
| 138 | export function spaceInput(v: SpaceFormValue) { | |
| 139 | return { | |
| 140 | name: v.name.trim(), | |
| 141 | icon: v.icon.trim() || null, | |
| 142 | slug: v.slug.trim() || null, | |
| 143 | description: v.description.trim() || null, | |
| 144 | kind: v.kind, | |
| 145 | team: v.kind === "team" ? v.team : null, | |
| 146 | default_role: v.kind === "private" ? null : v.default_role, | |
| 147 | agent_mode: v.agent_mode, | |
| 148 | projects: v.projects | |
| 149 | .split(/[\s,]+/) | |
| 150 | .map((p) => p.trim()) | |
| 151 | .filter(Boolean), | |
| 152 | }; | |
| 153 | } |