Skip to content
153 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 * 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 */
6import { DOC_AGENT_MODE_LABELS, DOC_ROLE_LABELS, DOC_SPACE_KIND_LABELS, type DocAgentMode, type DocRole, type DocSpace, type DocSpaceKind } from "@g1t/contracts";
7import { 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.9import { ErrorText } from "../ui";
10import { 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.11import { SelectField } from "../ui/select";
12
13export 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
25export 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
39const 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
41function 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
53export 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. */
138export 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}