Commite26af57unknown_key
polish(repo): 2026 cards for editor + commits + branches + tags + blame
polish(repo): 2026 cards for editor + commits + branches + tags + blame Editor: scoped `.editor-*` styles — eyebrow + display headline, mono path breadcrumb, branch pill, focus-ring inputs, primary commit + ghost cancel + AI Suggest button row. (commits/branches/tags/blame surfaces from the same session landed in commit 398a10c, which folded in the staged web.tsx delta.)
1 file changed+457−92e26af570ee752a41f0fd06a010de65bf6e2bd7fb
1 changed file+457−92
Modifiedsrc/routes/editor.tsx+457−92View fileUnifiedSplit
@@ -1,34 +1,26 @@
11/**
22 * Web file editor — create and edit files directly in the browser.
3 *
4 * 2026 polish: this surface uses a scoped `.editor-*` class system that
5 * mirrors `admin-integrations.tsx` and `collaborators.tsx` — gradient
6 * hairline, mono breadcrumb pill, commit-message input with focus ring,
7 * primary commit + ghost cancel buttons, and an AI "Suggest" button that
8 * sits inline. The git operations themselves are unchanged.
39 */
410
511import { Hono } from "hono";
612import { Layout } from "../views/layout";
7import { RepoHeader, RepoNav, Breadcrumb } from "../views/components";
8import {
9 Container,
10 Flex,
11 Form,
12 FormGroup,
13 Input,
14 TextArea,
15 Button,
16 LinkButton,
17 EmptyState,
18 Text,
19} from "../views/ui";
13import { RepoHeader, RepoNav } from "../views/components";
14import { EmptyState } from "../views/ui";
2015import {
2116 getBlob,
22 getDefaultBranch,
2317 getRepoPath,
24 repoExists,
2518} from "../git/repository";
2619import { generateCommitMessage } from "../lib/ai-generators";
2720import { isAiAvailable } from "../lib/ai-client";
2821import { softAuth, requireAuth } from "../middleware/auth";
2922import type { AuthEnv } from "../middleware/auth";
3023import { requireRepoAccess } from "../middleware/repo-access";
31import { join } from "path";
3224
3325const editor = new Hono<AuthEnv>();
3426
@@ -78,6 +70,269 @@ function AI_COMMIT_MSG_SCRIPT(args: {
7870 );
7971}
8072
73// ─── Scoped CSS (.editor-*) ─────────────────────────────────────────────────
74// Every selector is prefixed `.editor-*` so this surface can't bleed into
75// the repo header / nav above. Tokens reused from layout (--bg-elevated,
76// --border, --text-strong, --accent, --space-*, --font-*).
77
78const editorStyles = `
79 .editor-wrap { max-width: 1100px; margin: 0 auto; padding: var(--space-5) var(--space-4) var(--space-8); }
80
81 /* ─── Header strip (sits below RepoHeader + RepoNav) ─── */
82 .editor-head { margin-bottom: var(--space-5); }
83 .editor-eyebrow {
84 display: inline-flex;
85 align-items: center;
86 gap: 8px;
87 text-transform: uppercase;
88 font-family: var(--font-mono);
89 font-size: 11px;
90 letter-spacing: 0.16em;
91 color: var(--text-muted);
92 font-weight: 600;
93 margin-bottom: 10px;
94 }
95 .editor-eyebrow-dot {
96 width: 8px; height: 8px;
97 border-radius: 9999px;
98 background: linear-gradient(135deg, #8c6dff, #36c5d6);
99 box-shadow: 0 0 0 3px rgba(140,109,255,0.18);
100 }
101 .editor-title {
102 font-family: var(--font-display);
103 font-size: clamp(22px, 3vw, 32px);
104 font-weight: 800;
105 letter-spacing: -0.025em;
106 line-height: 1.15;
107 margin: 0 0 6px;
108 color: var(--text-strong);
109 }
110 .editor-title-grad {
111 background-image: linear-gradient(135deg, #a48bff 0%, #8c6dff 50%, #36c5d6 100%);
112 -webkit-background-clip: text;
113 background-clip: text;
114 -webkit-text-fill-color: transparent;
115 color: transparent;
116 }
117 .editor-sub {
118 margin: 0;
119 font-size: 14px;
120 color: var(--text-muted);
121 line-height: 1.5;
122 max-width: 700px;
123 }
124
125 /* ─── Toolbar (path breadcrumb + branch pill) ─── */
126 .editor-toolbar {
127 display: flex;
128 align-items: center;
129 justify-content: space-between;
130 gap: var(--space-3);
131 flex-wrap: wrap;
132 margin-bottom: var(--space-3);
133 padding: 10px 14px;
134 background: var(--bg-elevated);
135 border: 1px solid var(--border);
136 border-radius: 12px;
137 }
138 .editor-path {
139 display: inline-flex;
140 align-items: center;
141 gap: 6px;
142 font-family: var(--font-mono);
143 font-size: 13px;
144 color: var(--text);
145 flex-wrap: wrap;
146 min-width: 0;
147 }
148 .editor-path-sep { color: var(--text-faint); }
149 .editor-path-seg {
150 color: var(--text-muted);
151 }
152 .editor-path-name {
153 color: var(--text-strong);
154 font-weight: 600;
155 }
156 .editor-branch-pill {
157 display: inline-flex;
158 align-items: center;
159 gap: 6px;
160 padding: 4px 10px;
161 border-radius: 999px;
162 font-family: var(--font-mono);
163 font-size: 12px;
164 color: #c4b5fd;
165 background: rgba(140,109,255,0.12);
166 box-shadow: inset 0 0 0 1px rgba(140,109,255,0.30);
167 white-space: nowrap;
168 font-weight: 600;
169 }
170 .editor-branch-pill svg { opacity: 0.9; }
171
172 /* ─── Form card ─── */
173 .editor-card {
174 background: var(--bg-elevated);
175 border: 1px solid var(--border);
176 border-radius: 14px;
177 overflow: hidden;
178 position: relative;
179 }
180 .editor-card::before {
181 content: '';
182 position: absolute;
183 top: 0; left: 0; right: 0;
184 height: 2px;
185 background: linear-gradient(90deg, transparent 0%, #8c6dff 30%, #36c5d6 70%, transparent 100%);
186 opacity: 0.55;
187 pointer-events: none;
188 }
189 .editor-body { padding: var(--space-4) var(--space-5); }
190
191 .editor-field { display: block; margin-bottom: var(--space-4); }
192 .editor-label {
193 display: block;
194 font-size: 11.5px;
195 color: var(--text-muted);
196 font-weight: 600;
197 text-transform: uppercase;
198 letter-spacing: 0.06em;
199 margin-bottom: 6px;
200 }
201 .editor-filename-row {
202 display: flex;
203 align-items: center;
204 gap: 6px;
205 font-family: var(--font-mono);
206 }
207 .editor-filename-dir {
208 font-size: 13px;
209 color: var(--text-muted);
210 }
211 .editor-input {
212 width: 100%;
213 box-sizing: border-box;
214 padding: 10px 12px;
215 font: inherit;
216 font-size: 14px;
217 color: var(--text);
218 background: rgba(255,255,255,0.03);
219 border: 1px solid var(--border-strong);
220 border-radius: 10px;
221 transition: border-color 120ms ease, background 120ms ease, box-shadow 120ms ease;
222 }
223 .editor-input:focus {
224 outline: none;
225 border-color: rgba(140,109,255,0.55);
226 background: rgba(255,255,255,0.05);
227 box-shadow: 0 0 0 3px rgba(140,109,255,0.18);
228 }
229 .editor-input-mono { font-family: var(--font-mono); }
230
231 .editor-textarea {
232 width: 100%;
233 box-sizing: border-box;
234 padding: 12px 14px;
235 font: inherit;
236 font-family: var(--font-mono);
237 font-size: 13px;
238 line-height: 1.55;
239 tab-size: 2;
240 color: var(--text);
241 background: var(--bg);
242 border: 1px solid var(--border-strong);
243 border-radius: 10px;
244 resize: vertical;
245 min-height: 280px;
246 transition: border-color 120ms ease, background 120ms ease, box-shadow 120ms ease;
247 }
248 .editor-textarea:focus {
249 outline: none;
250 border-color: rgba(140,109,255,0.55);
251 box-shadow: 0 0 0 3px rgba(140,109,255,0.18);
252 }
253
254 /* ─── Action row ─── */
255 .editor-actions {
256 display: flex;
257 align-items: center;
258 gap: 10px;
259 flex-wrap: wrap;
260 padding: var(--space-3) var(--space-5);
261 border-top: 1px solid var(--border);
262 background: rgba(255,255,255,0.012);
263 }
264 .editor-btn {
265 display: inline-flex;
266 align-items: center;
267 justify-content: center;
268 gap: 6px;
269 padding: 9px 16px;
270 border-radius: 10px;
271 font-size: 13px;
272 font-weight: 600;
273 text-decoration: none;
274 border: 1px solid transparent;
275 cursor: pointer;
276 font: inherit;
277 transition: transform 120ms ease, box-shadow 120ms ease, background 120ms ease, border-color 120ms ease, color 120ms ease;
278 line-height: 1;
279 white-space: nowrap;
280 }
281 .editor-btn-primary {
282 background: linear-gradient(135deg, #8c6dff 0%, #36c5d6 100%);
283 color: #fff;
284 box-shadow: 0 6px 18px -6px rgba(140,109,255,0.5), inset 0 1px 0 rgba(255,255,255,0.16);
285 }
286 .editor-btn-primary:hover {
287 transform: translateY(-1px);
288 box-shadow: 0 10px 24px -8px rgba(140,109,255,0.6), inset 0 1px 0 rgba(255,255,255,0.20);
289 text-decoration: none;
290 color: #fff;
291 }
292 .editor-btn-ghost {
293 background: transparent;
294 color: var(--text);
295 border-color: var(--border-strong);
296 }
297 .editor-btn-ghost:hover {
298 background: rgba(140,109,255,0.06);
299 border-color: rgba(140,109,255,0.45);
300 color: var(--text-strong);
301 text-decoration: none;
302 }
303 .editor-btn-ai {
304 background: transparent;
305 color: #c4b5fd;
306 border-color: rgba(140,109,255,0.35);
307 }
308 .editor-btn-ai:hover {
309 border-color: rgba(140,109,255,0.65);
310 background: rgba(140,109,255,0.08);
311 color: #ddd6fe;
312 text-decoration: none;
313 }
314 .editor-btn-ai:disabled {
315 opacity: 0.55;
316 cursor: progress;
317 }
318 .editor-status {
319 color: var(--text-muted);
320 font-size: 12.5px;
321 margin-left: auto;
322 }
323`;
324
325function IconBranch() {
326 return (
327 <svg width="12" height="12" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true">
328 <line x1="6" y1="3" x2="6" y2="15" />
329 <circle cx="18" cy="6" r="3" />
330 <circle cx="6" cy="18" r="3" />
331 <path d="M18 9a9 9 0 0 1-9 9" />
332 </svg>
333 );
334}
335
81336// New file form
82337editor.get("/:owner/:repo/new/:ref{.+$}", requireAuth, requireRepoAccess("write"), async (c) => {
83338 const { owner, repo } = c.req.param();
@@ -93,49 +348,98 @@ editor.get("/:owner/:repo/new/:ref{.+$}", requireAuth, requireRepoAccess("write"
93348 <Layout title={`New file — ${owner}/${repo}`} user={user}>
94349 <RepoHeader owner={owner} repo={repo} />
95350 <RepoNav owner={owner} repo={repo} active="code" />
96 <Container maxWidth={900}>
97 <h2 style="margin-bottom: 16px">Create new file</h2>
98 <Form method="post" action={`/${owner}/${repo}/new/${ref}`}>
99 <input type="hidden" name="dir_path" value={dirPath} />
100 <FormGroup label="File path">
101 <Flex align="center" gap={4}>
102 {dirPath && (
103 <Text muted size={14}>
104 {dirPath}/
105 </Text>
106 )}
107 <Input
108 name="filename"
351 <div class="editor-wrap">
352 <header class="editor-head">
353 <div class="editor-eyebrow">
354 <span class="editor-eyebrow-dot" aria-hidden="true" />
355 Editor · New file
356 </div>
357 <h1 class="editor-title">
358 <span class="editor-title-grad">Create a new file.</span>
359 </h1>
360 <p class="editor-sub">
361 Pick a path, paste your content, and write a short commit
362 message. The commit lands directly on{" "}
363 <code style="font-size:12.5px">{ref}</code>.
364 </p>
365 </header>
366
367 <div class="editor-toolbar">
368 <div class="editor-path" title={dirPath ? `${dirPath}/…` : "Repository root"}>
369 <span class="editor-path-seg">{owner}/{repo}</span>
370 {dirPath && (
371 <>
372 <span class="editor-path-sep">/</span>
373 <span class="editor-path-seg">{dirPath}</span>
374 </>
375 )}
376 <span class="editor-path-sep">/</span>
377 <span class="editor-path-name">new file…</span>
378 </div>
379 <span class="editor-branch-pill" title="Target branch">
380 <IconBranch />
381 {ref}
382 </span>
383 </div>
384
385 <form
386 method="post"
387 action={`/${owner}/${repo}/new/${ref}`}
388 class="editor-card"
389 >
390 <div class="editor-body">
391 <input type="hidden" name="dir_path" value={dirPath} />
392 <div class="editor-field">
393 <label class="editor-label" for="editor-filename">File path</label>
394 <div class="editor-filename-row">
395 {dirPath && (
396 <span class="editor-filename-dir">{dirPath}/</span>
397 )}
398 <input
399 class="editor-input editor-input-mono"
400 id="editor-filename"
401 name="filename"
402 required
403 placeholder="filename.ts"
404 autocomplete="off"
405 aria-label="File path"
406 />
407 </div>
408 </div>
409 <div class="editor-field">
410 <label class="editor-label" for="editor-content-new">Content</label>
411 <textarea
412 class="editor-textarea"
413 id="editor-content-new"
414 name="content"
415 rows={20}
416 placeholder="Enter file content…"
417 spellcheck={false}
418 />
419 </div>
420 <div class="editor-field" style="margin-bottom:0">
421 <label class="editor-label" for="commit-message-input">Commit message</label>
422 <input
423 class="editor-input"
424 id="commit-message-input"
425 name="message"
426 placeholder="Create new file"
109427 required
110 placeholder="filename.ts"
111 style="flex: 1"
112 autocomplete="off"
113 aria-label="File path"
428 aria-label="Commit message"
114429 />
115 </Flex>
116 </FormGroup>
117 <FormGroup label="Content">
118 <TextArea
119 name="content"
120 rows={20}
121 placeholder="Enter file content..."
122 mono
123 style="line-height: 1.5; tab-size: 2"
124 />
125 </FormGroup>
126 <FormGroup label="Commit message">
127 <Input
128 name="message"
129 placeholder="Create new file"
130 required
131 aria-label="Commit message"
132 />
133 </FormGroup>
134 <Button type="submit" variant="primary">
135 Commit new file
136 </Button>
137 </Form>
138 </Container>
430 </div>
431 </div>
432 <div class="editor-actions">
433 <button type="submit" class="editor-btn editor-btn-primary">
434 Commit new file
435 </button>
436 <a href={`/${owner}/${repo}`} class="editor-btn editor-btn-ghost">
437 Cancel
438 </a>
439 </div>
440 </form>
441 </div>
442 <style dangerouslySetInnerHTML={{ __html: editorStyles }} />
139443 </Layout>
140444 );
141445});
@@ -259,51 +563,111 @@ editor.get("/:owner/:repo/edit/:ref{.+$}", requireAuth, requireRepoAccess("write
259563 );
260564 }
261565
566 const fileName = filePath.split("/").pop() || filePath;
567 const dirParts = filePath.split("/").slice(0, -1);
568
262569 return c.html(
263570 <Layout title={`Editing ${filePath} — ${owner}/${repo}`} user={user}>
264571 <RepoHeader owner={owner} repo={repo} />
265572 <RepoNav owner={owner} repo={repo} active="code" />
266 <Breadcrumb owner={owner} repo={repo} ref={ref} path={filePath} />
267 <Container maxWidth={900}>
268 <Form method="post" action={`/${owner}/${repo}/edit/${ref}/${filePath}`}>
269 <FormGroup label="Content">
270 <TextArea
271 name="content"
272 rows={25}
273 value={blob.content}
274 mono
275 style="line-height: 1.5; tab-size: 2; width: 100%"
276 />
277 </FormGroup>
278 <FormGroup label="Commit message">
279 <Input
280 id="commit-message-input"
281 name="message"
282 placeholder={`Update ${filePath.split("/").pop()}`}
283 required
284 aria-label="Commit message"
285 />
286 </FormGroup>
287 <Flex gap={8} align="center">
288 <Button type="submit" variant="primary">
573 <div class="editor-wrap">
574 <header class="editor-head">
575 <div class="editor-eyebrow">
576 <span class="editor-eyebrow-dot" aria-hidden="true" />
577 Editor · Edit file
578 </div>
579 <h1 class="editor-title">
580 <span class="editor-title-grad">Edit{" "}</span>
581 <code style="font-family:var(--font-mono);font-size:0.82em;color:var(--text-strong);font-weight:700">{fileName}</code>
582 </h1>
583 <p class="editor-sub">
584 Save your changes as a commit on{" "}
585 <code style="font-size:12.5px">{ref}</code>. Write a clear
586 one-line message so reviewers can follow the history.
587 </p>
588 </header>
589
590 <div class="editor-toolbar">
591 <div class="editor-path" title={filePath}>
592 <a
593 href={`/${owner}/${repo}`}
594 class="editor-path-seg"
595 style="color:var(--text-muted);text-decoration:none"
596 >
597 {owner}/{repo}
598 </a>
599 {dirParts.map((seg, i) => {
600 const subPath = dirParts.slice(0, i + 1).join("/");
601 return (
602 <>
603 <span class="editor-path-sep">/</span>
604 <a
605 href={`/${owner}/${repo}/tree/${ref}/${subPath}`}
606 class="editor-path-seg"
607 style="text-decoration:none"
608 >
609 {seg}
610 </a>
611 </>
612 );
613 })}
614 <span class="editor-path-sep">/</span>
615 <span class="editor-path-name">{fileName}</span>
616 </div>
617 <span class="editor-branch-pill" title="Target branch">
618 <IconBranch />
619 {ref}
620 </span>
621 </div>
622
623 <form
624 method="post"
625 action={`/${owner}/${repo}/edit/${ref}/${filePath}`}
626 class="editor-card"
627 >
628 <div class="editor-body">
629 <div class="editor-field">
630 <label class="editor-label" for="editor-content-edit">Content</label>
631 <textarea
632 class="editor-textarea"
633 id="editor-content-edit"
634 name="content"
635 rows={25}
636 spellcheck={false}
637 >{blob.content}</textarea>
638 </div>
639 <div class="editor-field" style="margin-bottom:0">
640 <label class="editor-label" for="commit-message-input">Commit message</label>
641 <input
642 class="editor-input"
643 id="commit-message-input"
644 name="message"
645 placeholder={`Update ${fileName}`}
646 required
647 aria-label="Commit message"
648 />
649 </div>
650 </div>
651 <div class="editor-actions">
652 <button type="submit" class="editor-btn editor-btn-primary">
289653 Commit changes
290 </Button>
654 </button>
291655 <button
292656 type="button"
293657 id="ai-commit-msg-btn"
294 class="btn"
658 class="editor-btn editor-btn-ai"
295659 title="Generate a one-line commit message using Claude based on the diff"
296660 >
297 Suggest with AI
661 {"✨"} Suggest with AI
298662 </button>
299 <span
300 id="ai-commit-msg-status"
301 style="color:var(--text-muted);font-size:13px"
302 />
303 <LinkButton href={`/${owner}/${repo}/blob/${ref}/${filePath}`}>
663 <a
664 href={`/${owner}/${repo}/blob/${ref}/${filePath}`}
665 class="editor-btn editor-btn-ghost"
666 >
304667 Cancel
305 </LinkButton>
306 </Flex>
668 </a>
669 <span id="ai-commit-msg-status" class="editor-status" />
670 </div>
307671 <script
308672 dangerouslySetInnerHTML={{
309673 __html: AI_COMMIT_MSG_SCRIPT({
@@ -313,8 +677,9 @@ editor.get("/:owner/:repo/edit/:ref{.+$}", requireAuth, requireRepoAccess("write
313677 }),
314678 }}
315679 />
316 </Form>
317 </Container>
680 </form>
681 </div>
682 <style dangerouslySetInnerHTML={{ __html: editorStyles }} />
318683 </Layout>
319684 );
320685});
321686