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.
| Share the editor settings and Claude Code deploy permissions | 1 | /** |
| Projects: what a workspace builds and runs, first on every page | 2 | * A project's or a workspace's secrets and variables, as one list in the |
| Secrets and variables: one list, rows per environment, for workflows and deployments | 3 | * way Vercel lists environment variables: each row is a key, its type |
| 4 | * (Secret or Config), the environments it applies to and who reads it. | |
| 5 | * Adding and editing happen in a side panel, opened by `?add` or | |
| 6 | * `?edit=<id>` so the page works without scripts. | |
| Share the editor settings and Claude Code deploy permissions | 7 | */ |
| Secrets and variables: one list, rows per environment, for workflows and deployments | 8 | import { Lock, Pencil, Plus, Search, SlidersHorizontal, Trash2, X } from "lucide-react"; |
| 9 | import { useMemo, useState } from "react"; | |
| Merge branch 'worktree-agent-a985c01f43538af01' | 10 | import { Form, Link, useLocation } from "react-router"; |
| Share the editor settings and Claude Code deploy permissions | 11 | |
| Secrets and variables: one list, rows per environment, for workflows and deployments | 12 | import type { Setting } from "@g1t/contracts"; |
| Share the editor settings and Claude Code deploy permissions | 13 | |
| 14 | import type { SecretsAction, SecretsData } from "../lib/secrets.server"; | |
| Merge branch 'worktree-agent-a985c01f43538af01' | 15 | import { ButtonLink, EmptyState, ErrorText, SubmitButton, TimeAgo } from "./ui"; |
| 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. | 16 | import { Card } from "./ui/card"; |
| Workspace names and icons, and a component kit for every control | 17 | import { CheckboxOption } from "./ui/checkbox"; |
| Merge branch 'worktree-agent-ae1299e92e4462012' | 18 | import { Hint } from "./ui/hint"; |
| Workspace names and icons, and a component kit for every control | 19 | import { RadioCard, RadioGroup, RadioOption } from "./ui/radio-group"; |
| 20 | import { Select, SelectContent, SelectItem, SelectSeparator, SelectTrigger, SelectValue } from "./ui/select"; | |
| Share the editor settings and Claude Code deploy permissions | 21 | |
| Secrets and variables: one list, rows per environment, for workflows and deployments | 22 | /** The environments every deployment knows; workflow jobs may name others. */ |
| 23 | const KNOWN_ENVIRONMENTS = ["production", "preview"]; | |
| 24 | ||
| 25 | const READERS: Record<string, string> = { workflows: "Workflows", deployments: "Deployments" }; | |
| 26 | ||
| 27 | function environmentsLabel(environments: string[]): string { | |
| 28 | if (environments.length === 0) return "All environments"; | |
| 29 | return environments.map((env) => env.charAt(0).toUpperCase() + env.slice(1)).join(", "); | |
| Share the editor settings and Claude Code deploy permissions | 30 | } |
| 31 | ||
| Secrets and variables: one list, rows per environment, for workflows and deployments | 32 | const SELECT = |
| 33 | "rounded-md border border-line bg-bg px-3 py-2 text-sm outline-none transition-colors hover:border-line-strong focus:border-accent-dim"; | |
| 34 | ||
| 35 | export function SecretsPanel({ | |
| 36 | data, | |
| 37 | action, | |
| Share the editor settings and Claude Code deploy permissions | 38 | scope, |
| 39 | manage, | |
| 40 | }: { | |
| Secrets and variables: one list, rows per environment, for workflows and deployments | 41 | data: SecretsData; |
| 42 | action: SecretsAction | undefined; | |
| Projects: what a workspace builds and runs, first on every page | 43 | scope: "project" | "workspace"; |
| Share the editor settings and Claude Code deploy permissions | 44 | manage: boolean; |
| 45 | }) { | |
| Secrets and variables: one list, rows per environment, for workflows and deployments | 46 | const location = useLocation(); |
| 47 | const params = new URLSearchParams(location.search); | |
| 48 | const editing = params.get("edit"); | |
| 49 | const adding = params.has("add"); | |
| 50 | const row = editing ? data.rows.find((r) => r.id === editing && r.scope === scope) : undefined; | |
| 51 | const [query, setQuery] = useState(""); | |
| 52 | const [type, setType] = useState("all"); | |
| 53 | const [environment, setEnvironment] = useState("all"); | |
| 54 | const environments = useMemo( | |
| 55 | () => [...new Set([...KNOWN_ENVIRONMENTS, ...data.rows.flatMap((r) => r.environments)])], | |
| 56 | [data.rows], | |
| 57 | ); | |
| 58 | const shown = data.rows.filter( | |
| 59 | (r) => | |
| 60 | (!query || r.name.toLowerCase().includes(query.toLowerCase()) || r.note?.toLowerCase().includes(query.toLowerCase())) && | |
| 61 | (type === "all" || r.kind === type) && | |
| 62 | (environment === "all" || r.environments.length === 0 || r.environments.includes(environment)), | |
| 63 | ); | |
| 64 | ||
| Share the editor settings and Claude Code deploy permissions | 65 | return ( |
| Secrets and variables: one list, rows per environment, for workflows and deployments | 66 | <div className="max-w-5xl"> |
| 67 | <header className="flex flex-wrap items-start justify-between gap-4"> | |
| 68 | <div> | |
| 69 | <h2 className="text-lg font-semibold tracking-tight">Secrets and variables</h2> | |
| 70 | <p className="mt-1 max-w-2xl text-sm text-muted"> | |
| 71 | One list for everything that reads them. Each row says which environments it applies to and whether{" "} | |
| 72 | <strong className="font-medium text-fg">workflows</strong> (as <code className="text-fg">secrets.KEY</code>{" "} | |
| 73 | and <code className="text-fg">vars.KEY</code>), <strong className="font-medium text-fg">deployments</strong>{" "} | |
| 74 | (the build's environment and the running app's <code className="text-fg">env.KEY</code>), or both read it. | |
| 75 | {scope === "workspace" | |
| Projects: what a workspace builds and runs, first on every page | 76 | ? " Every project, or the ones you link, reads the workspace's; a project's own row of the same key wins." |
| 77 | : " Rows from the workspace are shown too; adding the same key here replaces them for this project."}{" "} | |
| Secrets and variables: one list, rows per environment, for workflows and deployments | 78 | <a href="https://docs.g1t.sh/guides/secrets-and-variables/" className="text-fg hover:underline"> |
| 79 | How they are read | |
| 80 | </a> | |
| 81 | </p> | |
| 82 | </div> | |
| 83 | {manage && ( | |
| 84 | <ButtonLink to="?add" variant="accent"> | |
| 85 | <Plus size={14} /> | |
| 86 | Add | |
| 87 | </ButtonLink> | |
| Share the editor settings and Claude Code deploy permissions | 88 | )} |
| Secrets and variables: one list, rows per environment, for workflows and deployments | 89 | </header> |
| 90 | ||
| 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. | 91 | <Card asChild radius="lg" className="mt-4 px-4 py-2.5 text-xs text-muted"> |
| 92 | <p> | |
| 93 | Built in: workflows get <code className="text-fg">secrets.G1T_TOKEN</code>, the workspace's own token for | |
| 94 | the run, with <code className="text-fg">secrets.GITHUB_TOKEN</code> as its alias. Agents | |
| 95 | and the merge queue never read secrets or variables, and runs for people outside the workspace get no secrets. | |
| 96 | </p> | |
| 97 | </Card> | |
| Secrets and variables: one list, rows per environment, for workflows and deployments | 98 | |
| 99 | <div className="mt-5 flex flex-wrap gap-2"> | |
| 100 | <label className="relative min-w-56 grow"> | |
| 101 | <Search size={14} className="pointer-events-none absolute top-1/2 left-3 -translate-y-1/2 text-faint" /> | |
| 102 | <input | |
| 103 | value={query} | |
| 104 | onChange={(e) => setQuery(e.target.value)} | |
| 105 | placeholder="Search keys and notes" | |
| 106 | aria-label="Search" | |
| 107 | className={`${SELECT} w-full pl-9`} | |
| 108 | /> | |
| 109 | </label> | |
| Workspace names and icons, and a component kit for every control | 110 | <Select value={type} onValueChange={setType}> |
| 111 | <SelectTrigger aria-label="Type" className="h-auto w-auto py-2"> | |
| 112 | <SelectValue /> | |
| 113 | </SelectTrigger> | |
| 114 | <SelectContent align="end"> | |
| 115 | <SelectItem value="all">All types</SelectItem> | |
| 116 | <SelectSeparator /> | |
| 117 | <SelectItem value="secret" icon={<Lock />}>Secret</SelectItem> | |
| 118 | <SelectItem value="variable" icon={<SlidersHorizontal />}>Config</SelectItem> | |
| 119 | </SelectContent> | |
| 120 | </Select> | |
| 121 | <Select value={environment} onValueChange={setEnvironment}> | |
| 122 | <SelectTrigger aria-label="Environment" className="h-auto w-auto py-2"> | |
| 123 | <SelectValue /> | |
| 124 | </SelectTrigger> | |
| 125 | <SelectContent align="end"> | |
| 126 | <SelectItem value="all">All environments</SelectItem> | |
| 127 | {environments.length > 0 && <SelectSeparator />} | |
| 128 | {environments.map((env) => ( | |
| 129 | <SelectItem key={env} value={env}> | |
| 130 | {environmentsLabel([env])} | |
| 131 | </SelectItem> | |
| 132 | ))} | |
| 133 | </SelectContent> | |
| 134 | </Select> | |
| Secrets and variables: one list, rows per environment, for workflows and deployments | 135 | </div> |
| 136 | ||
| 137 | <ErrorText>{data.error}</ErrorText> | |
| 138 | {!editing && !adding && <div className="mt-2"><ErrorText>{action?.error}</ErrorText></div>} | |
| 139 | ||
| Share the editor settings and Claude Code deploy permissions | 140 | <div className="mt-4"> |
| Secrets and variables: one list, rows per environment, for workflows and deployments | 141 | {data.rows.length === 0 ? ( |
| 142 | <EmptyState title="No secrets or variables yet"> | |
| 143 | Add one, or paste a <code>.env</code> file into Add to bring many at once. | |
| 144 | </EmptyState> | |
| 145 | ) : shown.length === 0 ? ( | |
| 146 | <EmptyState title="Nothing matches" /> | |
| Share the editor settings and Claude Code deploy permissions | 147 | ) : ( |
| 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. | 148 | <Card asChild className="overflow-hidden"> |
| 149 | <ul> | |
| 150 | {shown.map((r) => ( | |
| 151 | <Row key={r.id} row={r} inherited={r.scope !== scope} manage={manage} /> | |
| 152 | ))} | |
| 153 | </ul> | |
| 154 | </Card> | |
| Share the editor settings and Claude Code deploy permissions | 155 | )} |
| 156 | </div> | |
| Secrets and variables: one list, rows per environment, for workflows and deployments | 157 | |
| 158 | {manage && (adding || row) && ( | |
| Merge branch 'worktree-agent-a985c01f43538af01' | 159 | // Keyed to the row, so going from one row's edit to another's starts from that row. |
| 160 | <Drawer key={row?.id ?? "add"} row={row} scope={scope} projects={data.projects} error={action?.error} /> | |
| Secrets and variables: one list, rows per environment, for workflows and deployments | 161 | )} |
| 162 | </div> | |
| 163 | ); | |
| 164 | } | |
| 165 | ||
| 166 | function Row({ row, inherited, manage }: { row: Setting; inherited: boolean; manage: boolean }) { | |
| 167 | const secret = row.kind === "secret"; | |
| 168 | return ( | |
| 169 | <li className="grid grid-cols-[minmax(0,1.4fr)_minmax(0,1fr)_auto] items-center gap-x-4 gap-y-1 border-t border-line px-4 py-3 text-sm first:border-t-0 md:grid-cols-[minmax(0,1.4fr)_minmax(0,1fr)_minmax(0,1fr)_6rem_6rem_auto]"> | |
| 170 | <div className="min-w-0"> | |
| 171 | <p className="truncate font-mono text-[0.8125rem]">{row.name}</p> | |
| 172 | {row.note && <p className="truncate text-xs text-faint">{row.note}</p>} | |
| 173 | {!secret && row.value != null && <p className="truncate font-mono text-xs text-muted">{row.value}</p>} | |
| 174 | </div> | |
| 175 | <span className="truncate text-muted">{environmentsLabel(row.environments)}</span> | |
| 176 | <span className="hidden truncate text-xs text-muted md:block"> | |
| 177 | {row.availableTo.map((r) => READERS[r] ?? r).join(" · ")} | |
| 178 | </span> | |
| 179 | <span className="hidden items-center gap-1.5 text-xs text-muted md:flex"> | |
| 180 | {secret ? <Lock size={13} /> : <SlidersHorizontal size={13} />} | |
| 181 | {secret ? "Secret" : "Config"} | |
| 182 | </span> | |
| 183 | <span className="hidden text-xs text-faint md:block"> | |
| 184 | <TimeAgo at={row.updatedAt} /> | |
| 185 | </span> | |
| 186 | <span className="flex items-center justify-end gap-1"> | |
| 187 | {inherited ? ( | |
| 188 | <span className="rounded-full px-2 py-px text-xs text-muted ring-1 ring-line">Workspace</span> | |
| 189 | ) : ( | |
| 190 | <> | |
| Projects: what a workspace builds and runs, first on every page | 191 | {row.projects.length > 0 && ( |
| Merge branch 'worktree-agent-ae1299e92e4462012' | 192 | <Hint label={row.projects.join(", ")}> |
| 193 | <span className="mr-1 text-xs text-faint"> | |
| 194 | {row.projects.length} {row.projects.length === 1 ? "project" : "projects"} | |
| 195 | <span className="sr-only">: {row.projects.join(", ")}</span> | |
| 196 | </span> | |
| 197 | </Hint> | |
| Secrets and variables: one list, rows per environment, for workflows and deployments | 198 | )} |
| 199 | {manage && ( | |
| 200 | <> | |
| 201 | <Link | |
| 202 | to={`?edit=${row.id}`} | |
| 203 | aria-label={`Edit ${row.name}`} | |
| Phones reach every page: each mode's menu from the top bar or by tapping the tab you're on, Docs first in the avatar sheet, a project's pages as a swipeable strip; dialogs as bottom sheets above the keyboard, safe areas, menus kept on screen, 40px targets, touch-visible controls, save bars above the tab bar | 204 | className="rounded-md p-1.5 text-faint max-sm:p-2.5 transition-colors hover:bg-raised hover:text-fg" |
| Secrets and variables: one list, rows per environment, for workflows and deployments | 205 | > |
| 206 | <Pencil size={14} /> | |
| 207 | </Link> | |
| 208 | <Form method="post"> | |
| 209 | <input type="hidden" name="intent" value="delete" /> | |
| 210 | <input type="hidden" name="id" value={row.id} /> | |
| 211 | <input type="hidden" name="name" value={row.name} /> | |
| Merge branch 'worktree-agent-a985c01f43538af01' | 212 | <SubmitButton |
| 213 | icon | |
| 214 | match={{ intent: "delete", id: row.id }} | |
| Secrets and variables: one list, rows per environment, for workflows and deployments | 215 | aria-label={`Remove ${row.name}`} |
| 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. | 216 | variant="ghost" |
| 217 | size="inline" | |
| 218 | className="p-1.5 text-faint max-sm:p-2.5 hover:text-danger" | |
| Secrets and variables: one list, rows per environment, for workflows and deployments | 219 | > |
| 220 | <Trash2 size={14} /> | |
| Merge branch 'worktree-agent-a985c01f43538af01' | 221 | </SubmitButton> |
| Secrets and variables: one list, rows per environment, for workflows and deployments | 222 | </Form> |
| 223 | </> | |
| Share the editor settings and Claude Code deploy permissions | 224 | )} |
| Secrets and variables: one list, rows per environment, for workflows and deployments | 225 | </> |
| 226 | )} | |
| 227 | </span> | |
| 228 | </li> | |
| Share the editor settings and Claude Code deploy permissions | 229 | ); |
| 230 | } | |
| 231 | ||
| Secrets and variables: one list, rows per environment, for workflows and deployments | 232 | function Drawer({ |
| 233 | row, | |
| Share the editor settings and Claude Code deploy permissions | 234 | scope, |
| Projects: what a workspace builds and runs, first on every page | 235 | projects, |
| Secrets and variables: one list, rows per environment, for workflows and deployments | 236 | error, |
| Share the editor settings and Claude Code deploy permissions | 237 | }: { |
| Secrets and variables: one list, rows per environment, for workflows and deployments | 238 | row: Setting | undefined; |
| Projects: what a workspace builds and runs, first on every page | 239 | scope: "project" | "workspace"; |
| 240 | projects: string[]; | |
| Secrets and variables: one list, rows per environment, for workflows and deployments | 241 | error: string | undefined; |
| Share the editor settings and Claude Code deploy permissions | 242 | }) { |
| Secrets and variables: one list, rows per environment, for workflows and deployments | 243 | const editing = !!row; |
| 244 | const [type, setType] = useState<"secret" | "config">(row?.kind === "variable" ? "config" : "secret"); | |
| 245 | const [some, setSome] = useState(!!row && row.environments.length > 0); | |
| Projects: what a workspace builds and runs, first on every page | 246 | const [reach, setReach] = useState(row && row.projects.length > 0 ? "some" : "all"); |
| Secrets and variables: one list, rows per environment, for workflows and deployments | 247 | const custom = row?.environments.filter((env) => !KNOWN_ENVIRONMENTS.includes(env)) ?? []; |
| 248 | const field = | |
| 249 | "w-full rounded-md border border-line bg-bg px-3 py-2 text-sm outline-none transition-colors placeholder:text-faint hover:border-line-strong focus:border-accent-dim"; | |
| Share the editor settings and Claude Code deploy permissions | 250 | return ( |
| Secrets and variables: one list, rows per environment, for workflows and deployments | 251 | <div className="fixed inset-0 z-50 flex justify-end bg-black/50" role="dialog" aria-modal="true" aria-label={editing ? "Edit" : "Add"}> |
| 252 | <Link to="?" aria-label="Close" className="grow" /> | |
| Phones reach every page: each mode's menu from the top bar or by tapping the tab you're on, Docs first in the avatar sheet, a project's pages as a swipeable strip; dialogs as bottom sheets above the keyboard, safe areas, menus kept on screen, 40px targets, touch-visible controls, save bars above the tab bar | 253 | <Form method="post" className="flex h-full w-full max-w-xl flex-col border-l border-line bg-bg pt-[env(safe-area-inset-top)] pb-[env(safe-area-inset-bottom)] shadow-2xl"> |
| Secrets and variables: one list, rows per environment, for workflows and deployments | 254 | <div className="flex items-center justify-between border-b border-line px-6 py-4"> |
| 255 | <h3 className="font-semibold">{editing ? `Edit ${row.name}` : "Add a secret or variable"}</h3> | |
| Phones reach every page: each mode's menu from the top bar or by tapping the tab you're on, Docs first in the avatar sheet, a project's pages as a swipeable strip; dialogs as bottom sheets above the keyboard, safe areas, menus kept on screen, 40px targets, touch-visible controls, save bars above the tab bar | 256 | <Link to="?" aria-label="Close" className="flex items-center justify-center rounded-md p-1.5 text-faint hover:bg-raised hover:text-fg max-sm:size-10"> |
| Secrets and variables: one list, rows per environment, for workflows and deployments | 257 | <X size={16} /> |
| 258 | </Link> | |
| 259 | </div> | |
| 260 | <div className="grow space-y-6 overflow-y-auto px-6 py-5"> | |
| 261 | <input type="hidden" name="intent" value="save" /> | |
| 262 | {row && <input type="hidden" name="id" value={row.id} />} | |
| 263 | ||
| 264 | <fieldset> | |
| 265 | <legend className="mb-2 text-sm font-medium text-muted">Type</legend> | |
| Workspace names and icons, and a component kit for every control | 266 | <RadioGroup |
| 267 | name="type" | |
| 268 | value={type} | |
| 269 | onValueChange={(value) => setType(value as typeof type)} | |
| 270 | aria-label="Type" | |
| 271 | className="gap-3 sm:grid-cols-2" | |
| 272 | > | |
| Secrets and variables: one list, rows per environment, for workflows and deployments | 273 | {( |
| 274 | [ | |
| Workspace names and icons, and a component kit for every control | 275 | ["secret", "Secret", "You can't read it again after saving. For passwords, API keys and tokens.", <Lock key="i" />], |
| 276 | ["config", "Config", "Readable by members after saving. For values that are not sensitive.", <SlidersHorizontal key="i" />], | |
| Secrets and variables: one list, rows per environment, for workflows and deployments | 277 | ] as const |
| Workspace names and icons, and a component kit for every control | 278 | ).map(([value, title, text, icon]) => ( |
| Secrets and variables: one list, rows per environment, for workflows and deployments | 279 | // A secret's value is sealed: it can never become config. |
| Workspace names and icons, and a component kit for every control | 280 | <RadioCard |
| 281 | key={value} | |
| 282 | value={value} | |
| 283 | title={title} | |
| 284 | description={text} | |
| 285 | icon={icon} | |
| 286 | disabled={value === "config" && row?.kind === "secret"} | |
| 287 | /> | |
| 288 | ))} | |
| 289 | </RadioGroup> | |
| Secrets and variables: one list, rows per environment, for workflows and deployments | 290 | {row?.kind === "variable" && ( |
| 291 | <p className="mt-2 text-xs text-faint">Config can become a secret; a secret cannot become config.</p> | |
| 292 | )} | |
| 293 | </fieldset> | |
| 294 | ||
| 295 | <label className="block"> | |
| 296 | <span className="mb-1.5 block text-sm font-medium text-muted">Key</span> | |
| 297 | {editing ? ( | |
| 298 | <input name="key" value={row.name} readOnly className={`${field} font-mono text-muted`} /> | |
| 299 | ) : ( | |
| 300 | <textarea | |
| 301 | name="key" | |
| 302 | required | |
| 303 | rows={1} | |
| 304 | placeholder="CLIENT_KEY, or paste a .env file" | |
| 305 | autoComplete="off" | |
| 306 | spellCheck={false} | |
| 307 | className={`${field} min-h-10 font-mono`} | |
| 308 | /> | |
| 309 | )} | |
| 310 | </label> | |
| 311 | ||
| 312 | <label className="block"> | |
| 313 | <span className="mb-1.5 block text-sm font-medium text-muted">Value</span> | |
| 314 | <textarea | |
| 315 | name="value" | |
| 316 | rows={type === "secret" ? 3 : 2} | |
| 317 | defaultValue={row?.kind === "variable" ? (row.value ?? "") : ""} | |
| 318 | placeholder={ | |
| 319 | editing && row.kind === "secret" ? "Leave empty to keep the current value" : "Enter a value" | |
| 320 | } | |
| 321 | autoComplete="off" | |
| 322 | spellCheck={false} | |
| 323 | className={`${field} font-mono`} | |
| 324 | /> | |
| 325 | </label> | |
| 326 | ||
| 327 | <label className="block"> | |
| 328 | <span className="mb-1.5 block text-sm font-medium text-muted">Note (optional)</span> | |
| 329 | <input name="note" defaultValue={row?.note ?? ""} placeholder="Where to rotate it, or who to ask" className={field} /> | |
| 330 | </label> | |
| 331 | ||
| 332 | <fieldset> | |
| 333 | <legend className="mb-2 text-sm font-medium text-muted">Environments</legend> | |
| 334 | <div className="space-y-2 text-sm"> | |
| Workspace names and icons, and a component kit for every control | 335 | <RadioGroup name="scope" value={some ? "some" : "all"} onValueChange={(value) => setSome(value === "some")} aria-label="Environments"> |
| 336 | <RadioOption value="all" label="All environments" /> | |
| 337 | <RadioOption value="some" label="Only some" /> | |
| 338 | </RadioGroup> | |
| Secrets and variables: one list, rows per environment, for workflows and deployments | 339 | {some && ( |
| 340 | <div className="ml-6 space-y-2"> | |
| 341 | {KNOWN_ENVIRONMENTS.map((env) => ( | |
| Workspace names and icons, and a component kit for every control | 342 | <CheckboxOption |
| 343 | key={env} | |
| 344 | name="env" | |
| 345 | value={env} | |
| 346 | defaultChecked={row?.environments.includes(env)} | |
| 347 | label={environmentsLabel([env])} | |
| 348 | /> | |
| Secrets and variables: one list, rows per environment, for workflows and deployments | 349 | ))} |
| 350 | <input | |
| 351 | name="envCustom" | |
| 352 | defaultValue={custom.join(", ")} | |
| 353 | placeholder="Others, comma-separated: staging, qa" | |
| 354 | className={field} | |
| 355 | /> | |
| 356 | <p className="text-xs text-faint"> | |
| 357 | Deployments are production and preview; a workflow job reads the row for its{" "} | |
| 358 | <code>environment:</code>, and rows for all environments otherwise. | |
| 359 | </p> | |
| 360 | </div> | |
| 361 | )} | |
| 362 | </div> | |
| 363 | </fieldset> | |
| 364 | ||
| 365 | <fieldset> | |
| 366 | <legend className="mb-2 text-sm font-medium text-muted">Available to</legend> | |
| 367 | <div className="space-y-2 text-sm"> | |
| 368 | {( | |
| 369 | [ | |
| 370 | ["workflows", "Workflows", "secrets.KEY or vars.KEY in GitHub Actions workflows"], | |
| 371 | ["deployments", "Deployments", "The build's environment, and env.KEY in the running app"], | |
| 372 | ] as const | |
| 373 | ).map(([value, title, text]) => ( | |
| Workspace names and icons, and a component kit for every control | 374 | <CheckboxOption |
| 375 | key={value} | |
| 376 | name="availableTo" | |
| 377 | value={value} | |
| 378 | defaultChecked={row ? row.availableTo.includes(value) : true} | |
| 379 | label={title} | |
| 380 | description={text} | |
| 381 | /> | |
| Secrets and variables: one list, rows per environment, for workflows and deployments | 382 | ))} |
| 383 | </div> | |
| 384 | </fieldset> | |
| 385 | ||
| 386 | {scope === "workspace" && ( | |
| 387 | <fieldset> | |
| Projects: what a workspace builds and runs, first on every page | 388 | <legend className="mb-2 text-sm font-medium text-muted">Projects</legend> |
| Secrets and variables: one list, rows per environment, for workflows and deployments | 389 | <div className="space-y-2 text-sm"> |
| Workspace names and icons, and a component kit for every control | 390 | <RadioGroup name="reach" value={reach} onValueChange={(value) => setReach(value as typeof reach)} aria-label="Projects"> |
| 391 | <RadioOption value="all" label="Every project" /> | |
| 392 | <RadioOption value="some" label="Only these" /> | |
| 393 | </RadioGroup> | |
| Secrets and variables: one list, rows per environment, for workflows and deployments | 394 | {reach === "some" && ( |
| Workspace names and icons, and a component kit for every control | 395 | <div className="ml-6 grid max-h-48 gap-1.5 overflow-y-auto p-0.5 sm:grid-cols-2"> |
| Projects: what a workspace builds and runs, first on every page | 396 | {projects.map((name) => ( |
| Workspace names and icons, and a component kit for every control | 397 | <CheckboxOption |
| 398 | key={name} | |
| 399 | name="project" | |
| 400 | value={name} | |
| 401 | defaultChecked={row?.projects.includes(name)} | |
| 402 | label={name} | |
| 403 | className="items-center" | |
| 404 | labelClassName="font-mono text-xs" | |
| 405 | /> | |
| Secrets and variables: one list, rows per environment, for workflows and deployments | 406 | ))} |
| 407 | </div> | |
| 408 | )} | |
| 409 | </div> | |
| 410 | </fieldset> | |
| 411 | )} | |
| 412 | <ErrorText>{error}</ErrorText> | |
| 413 | </div> | |
| 414 | <div className="flex items-center justify-between gap-4 border-t border-line px-6 py-4"> | |
| 415 | <p className="text-xs text-faint">{editing ? "" : "Paste .env contents into Key to add many."}</p> | |
| Merge branch 'worktree-agent-a985c01f43538af01' | 416 | <SubmitButton match={{ intent: "save" }} pending="Saving…"> |
| Secrets and variables: one list, rows per environment, for workflows and deployments | 417 | Save |
| Merge branch 'worktree-agent-a985c01f43538af01' | 418 | </SubmitButton> |
| Secrets and variables: one list, rows per environment, for workflows and deployments | 419 | </div> |
| 420 | </Form> | |
| Share the editor settings and Claude Code deploy permissions | 421 | </div> |
| 422 | ); | |
| 423 | } |
This file's history is long; its oldest lines are credited to the oldest commit read.