Commit655a200unknown_key
polish: 2026 treatment for marketplace + projects
polish: 2026 treatment for marketplace + projects Picked up from parallel polish agents' worktrees. - marketplace.tsx: featured app cards with logos + install count + rating + Install primary gradient button; category filter pills; sections for trending / new / your-installed. - projects.tsx: board cards with status pill + item count + recent activity; create-new gradient CTA; dashed empty state with orb. All scoped CSS. Form actions / POST handlers / DB queries preserved exactly.
2 files changed+1885−467655a200c16664c9ac3f4e0ea2f3baec093f90929
2 changed files+1885−467
Modifiedsrc/routes/marketplace.tsx+1157−276View fileUnifiedSplit
Large file (1,511 lines). Load full file
Modifiedsrc/routes/projects.tsx+728−191View fileUnifiedSplit
@@ -6,6 +6,10 @@
66 * one column at a time. Simple v1: positions are recomputed via "max+1".
77 *
88 * Never throws — all DB paths wrapped in try/catch.
9 *
10 * 2026 polish: scoped `.proj-*` class system mirrors `collaborators.tsx` —
11 * eyebrow + display headline, polished project cards with tabular-nums for
12 * counts, state pills, and a kanban board with hairline-gradient columns.
913 */
1014
1115import { Hono } from "hono";
@@ -27,6 +31,457 @@ const DEFAULT_COLUMNS = ["To Do", "In Progress", "Done"] as const;
2731
2832const projectRoutes = new Hono<AuthEnv>();
2933
34// ─── Scoped CSS (.proj-*) ───────────────────────────────────────────────────
35const projStyles = `
36 .proj-wrap { max-width: 1100px; margin: 0 auto; padding: var(--space-5) var(--space-4) var(--space-8); }
37
38 .proj-head {
39 display: flex;
40 align-items: flex-end;
41 justify-content: space-between;
42 gap: var(--space-4);
43 flex-wrap: wrap;
44 margin-bottom: var(--space-5);
45 }
46 .proj-head-text { flex: 1; min-width: 280px; }
47 .proj-eyebrow {
48 display: inline-flex;
49 align-items: center;
50 gap: 8px;
51 text-transform: uppercase;
52 font-family: var(--font-mono);
53 font-size: 11px;
54 letter-spacing: 0.16em;
55 color: var(--text-muted);
56 font-weight: 600;
57 margin-bottom: 10px;
58 }
59 .proj-eyebrow-dot {
60 width: 8px; height: 8px;
61 border-radius: 9999px;
62 background: linear-gradient(135deg, #8c6dff, #36c5d6);
63 box-shadow: 0 0 0 3px rgba(140,109,255,0.18);
64 }
65 .proj-title {
66 font-family: var(--font-display);
67 font-size: clamp(24px, 3.4vw, 36px);
68 font-weight: 800;
69 letter-spacing: -0.028em;
70 line-height: 1.1;
71 margin: 0 0 6px;
72 color: var(--text-strong);
73 }
74 .proj-title-grad {
75 background-image: linear-gradient(135deg, #a48bff 0%, #8c6dff 50%, #36c5d6 100%);
76 -webkit-background-clip: text;
77 background-clip: text;
78 -webkit-text-fill-color: transparent;
79 color: transparent;
80 }
81 .proj-sub {
82 margin: 0;
83 font-size: 14px;
84 color: var(--text-muted);
85 line-height: 1.5;
86 max-width: 640px;
87 }
88
89 /* Buttons */
90 .proj-btn {
91 display: inline-flex;
92 align-items: center;
93 justify-content: center;
94 gap: 6px;
95 padding: 9px 16px;
96 border-radius: 10px;
97 font-size: 13px;
98 font-weight: 600;
99 text-decoration: none;
100 border: 1px solid transparent;
101 cursor: pointer;
102 font: inherit;
103 line-height: 1;
104 white-space: nowrap;
105 transition: transform 120ms ease, box-shadow 120ms ease, background 120ms ease, border-color 120ms ease, color 120ms ease;
106 }
107 .proj-btn-primary {
108 background: linear-gradient(135deg, #8c6dff 0%, #36c5d6 100%);
109 color: #ffffff;
110 box-shadow: 0 6px 18px -6px rgba(140,109,255,0.50), inset 0 1px 0 rgba(255,255,255,0.16);
111 }
112 .proj-btn-primary:hover {
113 transform: translateY(-1px);
114 box-shadow: 0 10px 24px -8px rgba(140,109,255,0.60), inset 0 1px 0 rgba(255,255,255,0.20);
115 text-decoration: none;
116 color: #ffffff;
117 }
118 .proj-btn-ghost {
119 background: transparent;
120 color: var(--text);
121 border-color: var(--border-strong);
122 }
123 .proj-btn-ghost:hover {
124 background: rgba(140,109,255,0.06);
125 border-color: rgba(140,109,255,0.45);
126 color: var(--text-strong);
127 text-decoration: none;
128 }
129 .proj-btn-mini {
130 padding: 4px 10px;
131 font-size: 11.5px;
132 border-radius: 8px;
133 }
134
135 /* Crumbs */
136 .proj-crumbs {
137 display: flex;
138 align-items: center;
139 gap: 12px;
140 flex-wrap: wrap;
141 margin-bottom: var(--space-4);
142 font-size: 12.5px;
143 }
144 .proj-crumbs a {
145 display: inline-flex;
146 align-items: center;
147 gap: 5px;
148 padding: 6px 11px;
149 background: rgba(255,255,255,0.025);
150 border: 1px solid var(--border);
151 border-radius: 8px;
152 color: var(--text-muted);
153 text-decoration: none;
154 font-weight: 500;
155 transition: border-color 120ms ease, color 120ms ease, background 120ms ease;
156 }
157 .proj-crumbs a:hover {
158 border-color: var(--border-strong);
159 color: var(--text-strong);
160 background: rgba(255,255,255,0.04);
161 text-decoration: none;
162 }
163
164 /* Project list cards */
165 .proj-list { display: flex; flex-direction: column; gap: 10px; }
166 .proj-card {
167 display: flex;
168 align-items: center;
169 gap: 16px;
170 padding: 14px 18px;
171 background: var(--bg-elevated);
172 border: 1px solid var(--border);
173 border-radius: 12px;
174 transition: border-color 120ms ease, background 120ms ease;
175 }
176 .proj-card:hover {
177 border-color: var(--border-strong);
178 background: rgba(255,255,255,0.03);
179 }
180 .proj-num {
181 font-family: var(--font-mono);
182 font-size: 12.5px;
183 color: var(--text-muted);
184 font-variant-numeric: tabular-nums;
185 flex-shrink: 0;
186 }
187 .proj-card-body { flex: 1; min-width: 0; }
188 .proj-card-title {
189 display: inline-flex;
190 align-items: center;
191 gap: 8px;
192 flex-wrap: wrap;
193 }
194 .proj-card-title a {
195 font-family: var(--font-display);
196 font-weight: 700;
197 font-size: 15.5px;
198 color: var(--text-strong);
199 text-decoration: none;
200 letter-spacing: -0.005em;
201 }
202 .proj-card-title a:hover { text-decoration: underline; }
203 .proj-card-desc {
204 margin-top: 3px;
205 font-size: 13px;
206 color: var(--text-muted);
207 line-height: 1.45;
208 }
209 .proj-card-meta {
210 display: flex;
211 align-items: center;
212 gap: 10px;
213 flex-wrap: wrap;
214 font-size: 12px;
215 color: var(--text-muted);
216 font-variant-numeric: tabular-nums;
217 white-space: nowrap;
218 }
219 .proj-card-meta .sep { opacity: 0.4; }
220
221 /* Pills */
222 .proj-pill {
223 display: inline-flex;
224 align-items: center;
225 gap: 5px;
226 padding: 2px 9px;
227 border-radius: 9999px;
228 font-size: 11px;
229 font-weight: 600;
230 letter-spacing: 0.02em;
231 text-transform: capitalize;
232 }
233 .proj-pill .dot { width: 6px; height: 6px; border-radius: 9999px; background: currentColor; }
234 .proj-pill.is-open {
235 background: rgba(52,211,153,0.14);
236 color: #6ee7b7;
237 box-shadow: inset 0 0 0 1px rgba(52,211,153,0.32);
238 }
239 .proj-pill.is-closed {
240 background: rgba(148,163,184,0.16);
241 color: #cbd5e1;
242 box-shadow: inset 0 0 0 1px rgba(148,163,184,0.30);
243 }
244 .proj-pill.is-count {
245 background: rgba(140,109,255,0.12);
246 color: #c4b5fd;
247 box-shadow: inset 0 0 0 1px rgba(140,109,255,0.30);
248 font-family: var(--font-mono);
249 font-variant-numeric: tabular-nums;
250 }
251
252 /* Form card */
253 .proj-form-card {
254 background: var(--bg-elevated);
255 border: 1px solid var(--border);
256 border-radius: 14px;
257 padding: var(--space-5);
258 max-width: 640px;
259 position: relative;
260 overflow: hidden;
261 }
262 .proj-form-card::before {
263 content: '';
264 position: absolute;
265 top: 0; left: 0; right: 0;
266 height: 2px;
267 background: linear-gradient(90deg, transparent 0%, #8c6dff 30%, #36c5d6 70%, transparent 100%);
268 opacity: 0.55;
269 }
270 .proj-field { margin-bottom: 14px; }
271 .proj-field-label {
272 display: block;
273 font-size: 11.5px;
274 color: var(--text-muted);
275 font-weight: 600;
276 text-transform: uppercase;
277 letter-spacing: 0.06em;
278 margin-bottom: 6px;
279 }
280 .proj-input, .proj-textarea {
281 width: 100%;
282 box-sizing: border-box;
283 padding: 9px 12px;
284 font: inherit;
285 font-size: 13.5px;
286 color: var(--text);
287 background: rgba(255,255,255,0.03);
288 border: 1px solid var(--border-strong);
289 border-radius: 10px;
290 transition: border-color 120ms ease, background 120ms ease, box-shadow 120ms ease;
291 }
292 .proj-textarea { font-family: inherit; resize: vertical; line-height: 1.5; }
293 .proj-input:focus, .proj-textarea:focus {
294 outline: none;
295 border-color: rgba(140,109,255,0.55);
296 background: rgba(255,255,255,0.05);
297 box-shadow: 0 0 0 3px rgba(140,109,255,0.18);
298 }
299
300 /* Board view */
301 .proj-board-head {
302 display: flex;
303 align-items: flex-end;
304 justify-content: space-between;
305 gap: var(--space-4);
306 flex-wrap: wrap;
307 margin-bottom: var(--space-4);
308 }
309 .proj-board-desc {
310 margin: 6px 0 0;
311 color: var(--text-muted);
312 font-size: 13.5px;
313 line-height: 1.5;
314 }
315
316 .proj-kanban {
317 display: flex;
318 gap: 14px;
319 overflow-x: auto;
320 padding-bottom: 12px;
321 scrollbar-width: thin;
322 }
323 .proj-kcol {
324 background: var(--bg-elevated);
325 border: 1px solid var(--border);
326 border-radius: 12px;
327 min-width: 280px;
328 max-width: 280px;
329 flex-shrink: 0;
330 padding: 12px;
331 position: relative;
332 overflow: hidden;
333 }
334 .proj-kcol::before {
335 content: '';
336 position: absolute;
337 top: 0; left: 0; right: 0;
338 height: 2px;
339 background: linear-gradient(90deg, transparent 0%, #8c6dff 30%, #36c5d6 70%, transparent 100%);
340 opacity: 0.5;
341 pointer-events: none;
342 }
343 .proj-kcol-new {
344 background: transparent;
345 border-style: dashed;
346 }
347 .proj-kcol-new::before { display: none; }
348 .proj-kcol-head {
349 display: flex;
350 align-items: center;
351 justify-content: space-between;
352 gap: 8px;
353 margin: 2px 4px 12px;
354 }
355 .proj-kcol-name {
356 font-family: var(--font-display);
357 font-size: 13.5px;
358 font-weight: 700;
359 color: var(--text-strong);
360 letter-spacing: -0.005em;
361 }
362 .proj-kcard {
363 background: rgba(255,255,255,0.03);
364 border: 1px solid var(--border);
365 border-radius: 10px;
366 padding: 10px 12px;
367 margin-bottom: 8px;
368 font-size: 13px;
369 transition: border-color 120ms ease, background 120ms ease;
370 }
371 .proj-kcard:hover {
372 border-color: var(--border-strong);
373 background: rgba(255,255,255,0.05);
374 }
375 .proj-kcard-title {
376 font-weight: 600;
377 color: var(--text-strong);
378 line-height: 1.35;
379 }
380 .proj-kcard-note {
381 margin-top: 4px;
382 color: var(--text-muted);
383 font-size: 12px;
384 line-height: 1.45;
385 }
386 .proj-kcard-actions {
387 margin-top: 8px;
388 display: flex;
389 gap: 4px;
390 flex-wrap: wrap;
391 }
392 .proj-kcard-actions form { margin: 0; display: inline; }
393
394 .proj-kadd { display: flex; flex-direction: column; gap: 6px; margin-top: 8px; }
395 .proj-kadd input {
396 width: 100%;
397 box-sizing: border-box;
398 padding: 7px 10px;
399 font: inherit;
400 font-size: 12.5px;
401 color: var(--text);
402 background: rgba(255,255,255,0.03);
403 border: 1px solid var(--border);
404 border-radius: 8px;
405 }
406 .proj-kadd input:focus {
407 outline: none;
408 border-color: rgba(140,109,255,0.55);
409 box-shadow: 0 0 0 3px rgba(140,109,255,0.18);
410 }
411
412 /* Empty */
413 .proj-empty {
414 position: relative;
415 overflow: hidden;
416 padding: clamp(32px, 6vw, 56px) clamp(20px, 4vw, 36px);
417 text-align: center;
418 background: var(--bg-elevated);
419 border: 1px dashed var(--border-strong);
420 border-radius: 16px;
421 }
422 .proj-empty-orb {
423 position: absolute;
424 inset: -40% 30% auto 30%;
425 height: 280px;
426 background: radial-gradient(circle, rgba(140,109,255,0.18), rgba(54,197,214,0.10) 45%, transparent 70%);
427 filter: blur(70px);
428 opacity: 0.7;
429 pointer-events: none;
430 z-index: 0;
431 }
432 .proj-empty-inner { position: relative; z-index: 1; }
433 .proj-empty-icon {
434 width: 56px; height: 56px;
435 border-radius: 9999px;
436 background: linear-gradient(135deg, rgba(140,109,255,0.25), rgba(54,197,214,0.20));
437 box-shadow: inset 0 0 0 1px rgba(140,109,255,0.40);
438 display: inline-flex;
439 align-items: center;
440 justify-content: center;
441 color: #c4b5fd;
442 margin-bottom: 14px;
443 }
444 .proj-empty-title {
445 font-family: var(--font-display);
446 font-size: 18px;
447 font-weight: 700;
448 margin: 0 0 6px;
449 color: var(--text-strong);
450 }
451 .proj-empty-sub {
452 margin: 0 auto 16px;
453 font-size: 13.5px;
454 color: var(--text-muted);
455 max-width: 420px;
456 line-height: 1.5;
457 }
458`;
459
460function IconBoard() {
461 return (
462 <svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true">
463 <rect x="3" y="3" width="7" height="18" rx="1" />
464 <rect x="14" y="3" width="7" height="11" rx="1" />
465 </svg>
466 );
467}
468function IconPlus() {
469 return (
470 <svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true">
471 <line x1="12" y1="5" x2="12" y2="19" />
472 <line x1="5" y1="12" x2="19" y2="12" />
473 </svg>
474 );
475}
476function IconArrowLeft() {
477 return (
478 <svg width="12" height="12" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true">
479 <line x1="19" y1="12" x2="5" y2="12" />
480 <polyline points="12 19 5 12 12 5" />
481 </svg>
482 );
483}
484
30485async function resolveRepo(ownerName: string, repoName: string) {
31486 try {
32487 const [owner] = await db
@@ -55,9 +510,15 @@ async function resolveRepo(ownerName: string, repoName: string) {
55510function notFound(user: any, label = "Not found") {
56511 return (
57512 <Layout title={label} user={user}>
58 <div class="empty-state">
59 <h2>{label}</h2>
513 <div class="proj-wrap">
514 <div class="proj-empty">
515 <div class="proj-empty-orb" aria-hidden="true" />
516 <div class="proj-empty-inner">
517 <h2 class="proj-empty-title">{label}</h2>
518 </div>
519 </div>
60520 </div>
521 <style dangerouslySetInnerHTML={{ __html: projStyles }} />
61522 </Layout>
62523 );
63524}
@@ -88,50 +549,93 @@ projectRoutes.get("/:owner/:repo/projects", softAuth, async (c) => {
88549 return c.html(
89550 <Layout title={`Projects — ${ownerName}/${repoName}`} user={user}>
90551 <RepoHeader owner={ownerName} repo={repoName} />
91 <div style="display: flex; justify-content: space-between; align-items: center; margin: 16px 0;">
92 <h2 style="margin: 0;">Projects</h2>
93 {user && (
94 <a
95 href={`/${ownerName}/${repoName}/projects/new`}
96 class="btn btn-primary"
97 >
98 New project
99 </a>
100 )}
101 </div>
102 {rows.length === 0 ? (
103 <div class="empty-state">
104 <p>No projects yet.</p>
105 </div>
106 ) : (
107 <table class="file-table">
108 <tbody>
552 <div class="proj-wrap">
553 <header class="proj-head">
554 <div class="proj-head-text">
555 <div class="proj-eyebrow">
556 <span class="proj-eyebrow-dot" aria-hidden="true" />
557 Repository · Projects
558 </div>
559 <h1 class="proj-title">
560 <span class="proj-title-grad">Plan the work.</span>
561 </h1>
562 <p class="proj-sub">
563 Lightweight kanban boards scoped to {ownerName}/{repoName}. Each
564 board owns its own columns and cards.
565 </p>
566 </div>
567 {user && (
568 <a
569 href={`/${ownerName}/${repoName}/projects/new`}
570 class="proj-btn proj-btn-primary"
571 >
572 <IconPlus />
573 New project
574 </a>
575 )}
576 </header>
577
578 {rows.length === 0 ? (
579 <div class="proj-empty">
580 <div class="proj-empty-orb" aria-hidden="true" />
581 <div class="proj-empty-inner">
582 <div class="proj-empty-icon" aria-hidden="true">
583 <IconBoard />
584 </div>
585 <h3 class="proj-empty-title">Start your first project</h3>
586 <p class="proj-empty-sub">
587 Boards are perfect for grouping work — sprints, OKRs, release
588 trains. We seed the default <code>To Do</code> /{" "}
589 <code>In Progress</code> / <code>Done</code> columns for you.
590 </p>
591 {user && (
592 <a
593 href={`/${ownerName}/${repoName}/projects/new`}
594 class="proj-btn proj-btn-primary"
595 >
596 <IconPlus />
597 New project
598 </a>
599 )}
600 </div>
601 </div>
602 ) : (
603 <div class="proj-list">
109604 {rows.map((r) => (
110 <tr>
111 <td style="width: 40px; color: var(--text-muted);">
112 #{r.p.number}
113 </td>
114 <td>
115 <a
116 href={`/${ownerName}/${repoName}/projects/${r.p.number}`}
117 >
118 <strong>{r.p.title}</strong>
119 </a>
120 {r.p.state === "closed" && <span class="badge">closed</span>}
605 <div class="proj-card">
606 <div class="proj-num">#{r.p.number}</div>
607 <div class="proj-card-body">
608 <div class="proj-card-title">
609 <a href={`/${ownerName}/${repoName}/projects/${r.p.number}`}>
610 {r.p.title}
611 </a>
612 {r.p.state === "closed" ? (
613 <span class="proj-pill is-closed">
614 <span class="dot" aria-hidden="true" />
615 Closed
616 </span>
617 ) : (
618 <span class="proj-pill is-open">
619 <span class="dot" aria-hidden="true" />
620 Open
621 </span>
622 )}
623 </div>
121624 {r.p.description && (
122 <div style="font-size: 12px; color: var(--text-muted); margin-top: 2px;">
123 {r.p.description}
124 </div>
625 <div class="proj-card-desc">{r.p.description}</div>
125626 )}
126 </td>
127 <td style="text-align: right; color: var(--text-muted); font-size: 13px;">
128 {r.columnCount} cols · {r.itemCount} items
129 </td>
130 </tr>
627 </div>
628 <div class="proj-card-meta">
629 <span class="proj-pill is-count">{r.columnCount} cols</span>
630 <span class="sep">·</span>
631 <span class="proj-pill is-count">{r.itemCount} items</span>
632 </div>
633 </div>
131634 ))}
132 </tbody>
133 </table>
134 )}
635 </div>
636 )}
637 </div>
638 <style dangerouslySetInnerHTML={{ __html: projStyles }} />
135639 </Layout>
136640 );
137641});
@@ -148,30 +652,62 @@ projectRoutes.get(
148652 return c.html(
149653 <Layout title="New project" user={user}>
150654 <RepoHeader owner={ownerName} repo={repoName} />
151 <h2 style="margin-top: 20px;">Create a project</h2>
152 <form
153 method="post"
154 action={`/${ownerName}/${repoName}/projects`}
155 style="display: flex; flex-direction: column; gap: 12px; margin-top: 16px;"
156 >
157 <input
158 type="text"
159 name="title"
160 placeholder="Title"
161 required
162 aria-label="Project title"
163 style="padding: 8px;"
164 />
165 <textarea
166 name="description"
167 rows={4}
168 placeholder="Description (optional)"
169 style="padding: 8px; font-family: inherit;"
170 ></textarea>
171 <button type="submit" class="btn btn-primary">
172 Create
173 </button>
174 </form>
655 <div class="proj-wrap">
656 <div class="proj-crumbs">
657 <a href={`/${ownerName}/${repoName}/projects`}>
658 <IconArrowLeft />
659 All projects
660 </a>
661 </div>
662 <header class="proj-head">
663 <div class="proj-head-text">
664 <div class="proj-eyebrow">
665 <span class="proj-eyebrow-dot" aria-hidden="true" />
666 Projects · New
667 </div>
668 <h1 class="proj-title">
669 <span class="proj-title-grad">Create a board.</span>
670 </h1>
671 <p class="proj-sub">
672 Name your project — we seed default kanban columns you can
673 rename later.
674 </p>
675 </div>
676 </header>
677 <form
678 method="post"
679 action={`/${ownerName}/${repoName}/projects`}
680 class="proj-form-card"
681 >
682 <div class="proj-field">
683 <label class="proj-field-label" for="proj-title">Title</label>
684 <input
685 class="proj-input"
686 type="text"
687 id="proj-title"
688 name="title"
689 placeholder="Sprint 24 — Q3 release"
690 required
691 aria-label="Project title"
692 />
693 </div>
694 <div class="proj-field">
695 <label class="proj-field-label" for="proj-desc">Description</label>
696 <textarea
697 class="proj-textarea"
698 id="proj-desc"
699 name="description"
700 rows={4}
701 placeholder="What is this board for? (optional)"
702 ></textarea>
703 </div>
704 <button type="submit" class="proj-btn proj-btn-primary">
705 <IconPlus />
706 Create project
707 </button>
708 </form>
709 </div>
710 <style dangerouslySetInnerHTML={{ __html: projStyles }} />
175711 </Layout>
176712 );
177713 }
@@ -264,7 +800,6 @@ projectRoutes.get(
264800
265801 if (!project) return c.html(notFound(user, "Project not found"), 404);
266802
267 const isOwner = user && user.id === resolved.repo.ownerId;
268803 const itemsByCol: Record<string, any[]> = {};
269804 for (const col of columns) itemsByCol[col.id] = [];
270805 for (const it of items) {
@@ -277,143 +812,145 @@ projectRoutes.get(
277812 user={user}
278813 >
279814 <RepoHeader owner={ownerName} repo={repoName} />
280 <style>{`
281 .kanban { display: flex; gap: 16px; overflow-x: auto; padding: 16px 0; }
282 .kcol { background: var(--bg-soft); border: 1px solid var(--border); border-radius: 6px; min-width: 260px; flex-shrink: 0; padding: 12px; }
283 .kcol h4 { margin: 0 0 12px; display: flex; justify-content: space-between; }
284 .kcard { background: var(--bg); border: 1px solid var(--border); border-radius: 4px; padding: 8px; margin-bottom: 8px; font-size: 13px; }
285 .kcard form { display: inline; }
286 `}</style>
287 <div style="display: flex; justify-content: space-between; align-items: center; margin-top: 16px;">
288 <h1 style="margin: 0;">
289 {project.title}{" "}
290 <span style="color: var(--text-muted);">#{project.number}</span>
291 {project.state === "closed" && <span class="badge">closed</span>}
292 </h1>
293 {user && (
294 <form
295 method="post"
296 action={`/${ownerName}/${repoName}/projects/${project.number}/close`}
297 style="display: inline;"
298 >
299 <button type="submit" class="btn">
300 {project.state === "open" ? "Close" : "Reopen"}
301 </button>
302 </form>
303 )}
304 </div>
305 {project.description && (
306 <div style="color: var(--text-muted); margin-top: 4px;">
307 {project.description}
815 <div class="proj-wrap">
816 <div class="proj-crumbs">
817 <a href={`/${ownerName}/${repoName}/projects`}>
818 <IconArrowLeft />
819 All projects
820 </a>
308821 </div>
309 )}
310 <div class="kanban">
311 {columns.map((col) => (
312 <div class="kcol">
313 <h2>
314 <span>{col.name}</span>
315 <span style="color: var(--text-muted); font-size: 13px;">
316 {(itemsByCol[col.id] || []).length}
317 </span>
318 </h2>
319 {(itemsByCol[col.id] || []).map((it) => (
320 <div class="kcard">
321 <div>
322 <strong>{it.title || "(untitled)"}</strong>
323 </div>
324 {it.note && (
325 <div style="color: var(--text-muted); margin-top: 4px;">
326 {it.note}
327 </div>
328 )}
329 {user && (
330 <div style="margin-top: 8px; display: flex; gap: 4px; flex-wrap: wrap;">
331 {columns
332 .filter((oc) => oc.id !== col.id)
333 .map((oc) => (
334 <form
335 method="post"
336 action={`/${ownerName}/${repoName}/projects/${project.number}/items/${it.id}/move`}
337 >
338 <input
339 type="hidden"
340 name="column_id"
341 value={oc.id}
342 />
343 <button
344 type="submit"
345 class="btn"
346 style="font-size: 11px; padding: 2px 6px;"
822 <div class="proj-board-head">
823 <div>
824 <div class="proj-eyebrow">
825 <span class="proj-eyebrow-dot" aria-hidden="true" />
826 Project · #{project.number}
827 </div>
828 <h1 class="proj-title">
829 <span class="proj-title-grad">{project.title}</span>
830 {project.state === "closed" && (
831 <span class="proj-pill is-closed" style="margin-left:12px;vertical-align:middle">
832 <span class="dot" aria-hidden="true" />
833 Closed
834 </span>
835 )}
836 </h1>
837 {project.description && (
838 <p class="proj-board-desc">{project.description}</p>
839 )}
840 </div>
841 {user && (
842 <form
843 method="post"
844 action={`/${ownerName}/${repoName}/projects/${project.number}/close`}
845 >
846 <button type="submit" class="proj-btn proj-btn-ghost">
847 {project.state === "open" ? "Close" : "Reopen"}
848 </button>
849 </form>
850 )}
851 </div>
852 <div class="proj-kanban">
853 {columns.map((col) => (
854 <div class="proj-kcol">
855 <div class="proj-kcol-head">
856 <span class="proj-kcol-name">{col.name}</span>
857 <span class="proj-pill is-count">
858 {(itemsByCol[col.id] || []).length}
859 </span>
860 </div>
861 {(itemsByCol[col.id] || []).map((it) => (
862 <div class="proj-kcard">
863 <div class="proj-kcard-title">{it.title || "(untitled)"}</div>
864 {it.note && (
865 <div class="proj-kcard-note">{it.note}</div>
866 )}
867 {user && (
868 <div class="proj-kcard-actions">
869 {columns
870 .filter((oc) => oc.id !== col.id)
871 .map((oc) => (
872 <form
873 method="post"
874 action={`/${ownerName}/${repoName}/projects/${project.number}/items/${it.id}/move`}
347875 >
348 → {oc.name}
349 </button>
350 </form>
351 ))}
352 <form
353 method="post"
354 action={`/${ownerName}/${repoName}/projects/${project.number}/items/${it.id}/delete`}
355 >
356 <button
357 type="submit"
358 class="btn"
359 aria-label="Delete item"
360 style="font-size: 11px; padding: 2px 6px;"
876 <input
877 type="hidden"
878 name="column_id"
879 value={oc.id}
880 />
881 <button
882 type="submit"
883 class="proj-btn proj-btn-ghost proj-btn-mini"
884 >
885 → {oc.name}
886 </button>
887 </form>
888 ))}
889 <form
890 method="post"
891 action={`/${ownerName}/${repoName}/projects/${project.number}/items/${it.id}/delete`}
361892 >
362 ×
363 </button>
364 </form>
365 </div>
366 )}
367 </div>
368 ))}
369 {user && (
893 <button
894 type="submit"
895 class="proj-btn proj-btn-ghost proj-btn-mini"
896 aria-label="Delete item"
897 >
898 ×
899 </button>
900 </form>
901 </div>
902 )}
903 </div>
904 ))}
905 {user && (
906 <form
907 method="post"
908 action={`/${ownerName}/${repoName}/projects/${project.number}/items`}
909 class="proj-kadd"
910 >
911 <input type="hidden" name="column_id" value={col.id} />
912 <input
913 type="text"
914 name="title"
915 placeholder="New card title"
916 required
917 aria-label="New card title"
918 />
919 <button
920 type="submit"
921 class="proj-btn proj-btn-ghost proj-btn-mini"
922 >
923 <IconPlus />
924 Add card
925 </button>
926 </form>
927 )}
928 </div>
929 ))}
930 {user && (
931 <div class="proj-kcol proj-kcol-new">
370932 <form
371933 method="post"
372 action={`/${ownerName}/${repoName}/projects/${project.number}/items`}
373 style="margin-top: 8px; display: flex; flex-direction: column; gap: 4px;"
934 action={`/${ownerName}/${repoName}/projects/${project.number}/columns`}
935 class="proj-kadd"
374936 >
375 <input type="hidden" name="column_id" value={col.id} />
376937 <input
377938 type="text"
378 name="title"
379 placeholder="New card title"
939 name="name"
940 placeholder="New column"
380941 required
381 aria-label="New card title"
382 style="padding: 4px; font-size: 12px;"
942 aria-label="New column name"
383943 />
384 <button
385 type="submit"
386 class="btn"
387 style="font-size: 12px; padding: 4px;"
388 >
389 + Add card
944 <button type="submit" class="proj-btn proj-btn-ghost proj-btn-mini">
945 <IconPlus />
946 Add column
390947 </button>
391948 </form>
392 )}
393 </div>
394 ))}
395 {user && (
396 <div class="kcol" style="background: transparent; border-style: dashed;">
397 <form
398 method="post"
399 action={`/${ownerName}/${repoName}/projects/${project.number}/columns`}
400 style="display: flex; flex-direction: column; gap: 8px;"
401 >
402 <input
403 type="text"
404 name="name"
405 placeholder="New column"
406 required
407 aria-label="New column name"
408 style="padding: 6px;"
409 />
410 <button type="submit" class="btn">
411 + Add column
412 </button>
413 </form>
414 </div>
415 )}
949 </div>
950 )}
951 </div>
416952 </div>
953 <style dangerouslySetInnerHTML={{ __html: projStyles }} />
417954 </Layout>
418955 );
419956 }
420957