darkmode is now available
This commit is contained in:
@@ -50,7 +50,7 @@ export default function BlobPage() {
|
||||
}
|
||||
|
||||
if (isLoading) return <div className="p-6"><RepoListSkeleton /></div>
|
||||
if (isError || !blob) return <div className="p-6 text-sm text-[#DE350B]">File not found.</div>
|
||||
if (isError || !blob) return <div className="p-6 text-sm text-[var(--c-danger)]">File not found.</div>
|
||||
|
||||
const lines = blob.content.split('\n')
|
||||
const pathParts = filePath.split('/')
|
||||
@@ -60,18 +60,18 @@ export default function BlobPage() {
|
||||
|
||||
{/* Breadcrumb */}
|
||||
<div className="flex items-center gap-1 text-sm flex-wrap">
|
||||
<Link to="/repos" className="text-[#0052CC] hover:underline">Repositories</Link>
|
||||
<span className="text-[#5E6C84]">/</span>
|
||||
<Link to={`/repos/${owner}/${repoName}`} className="text-[#0052CC] hover:underline">{repoName}</Link>
|
||||
<Link to="/repos" className="text-[var(--c-brand)] hover:underline">Repositories</Link>
|
||||
<span className="text-[var(--c-muted)]">/</span>
|
||||
<Link to={`/repos/${owner}/${repoName}`} className="text-[var(--c-brand)] hover:underline">{repoName}</Link>
|
||||
{pathParts.map((seg, i) => {
|
||||
const partial = pathParts.slice(0, i + 1).join('/')
|
||||
const isLast = i === pathParts.length - 1
|
||||
return (
|
||||
<span key={partial} className="flex items-center gap-1">
|
||||
<span className="text-[#5E6C84]">/</span>
|
||||
<span className="text-[var(--c-muted)]">/</span>
|
||||
{isLast
|
||||
? <span className="font-semibold text-[#172B4D]">{seg}</span>
|
||||
: <Link to={`/repos/${owner}/${repoName}?path=${encodeURIComponent(partial)}&ref=${encodeURIComponent(branch)}`} className="text-[#0052CC] hover:underline">{seg}</Link>
|
||||
? <span className="font-semibold text-[var(--c-text)]">{seg}</span>
|
||||
: <Link to={`/repos/${owner}/${repoName}?path=${encodeURIComponent(partial)}&ref=${encodeURIComponent(branch)}`} className="text-[var(--c-brand)] hover:underline">{seg}</Link>
|
||||
}
|
||||
</span>
|
||||
)
|
||||
@@ -79,24 +79,24 @@ export default function BlobPage() {
|
||||
</div>
|
||||
|
||||
{/* File card */}
|
||||
<div className="border border-[#DFE1E6] rounded bg-white overflow-hidden">
|
||||
<div className="border border-[var(--c-border)] rounded bg-[var(--c-surface)] overflow-hidden">
|
||||
|
||||
{/* Toolbar */}
|
||||
<div className="flex items-center justify-between px-4 py-2.5 border-b border-[#DFE1E6] bg-[#FAFBFC] gap-3 flex-wrap">
|
||||
<div className="flex items-center justify-between px-4 py-2.5 border-b border-[var(--c-border)] bg-[var(--c-surface-raised)] gap-3 flex-wrap">
|
||||
<div className="flex items-center gap-2 text-sm">
|
||||
{/* Branch pill */}
|
||||
<span className="flex items-center gap-1 px-2 py-0.5 border border-[#DFE1E6] rounded text-xs text-[#5E6C84] bg-white">
|
||||
<span className="flex items-center gap-1 px-2 py-0.5 border border-[var(--c-border)] rounded text-xs text-[var(--c-muted)] bg-[var(--c-surface)]">
|
||||
<svg width="12" height="12" fill="none" stroke="currentColor" strokeWidth="1.5" viewBox="0 0 24 24">
|
||||
<path strokeLinecap="round" strokeLinejoin="round" d="M9.568 3H5.25A2.25 2.25 0 0 0 3 5.25v4.318c0 .597.237 1.17.659 1.591l9.581 9.581c.699.699 1.78.872 2.607.33a18.095 18.095 0 0 0 5.223-5.223c.542-.827.369-1.908-.33-2.607L11.16 3.66A2.25 2.25 0 0 0 9.568 3Z" />
|
||||
</svg>
|
||||
{branch}
|
||||
</span>
|
||||
<span className="text-[#5E6C84]">{repoName}</span>
|
||||
<span className="text-[#5E6C84]">/</span>
|
||||
<span className="font-medium text-[#172B4D]">{fileName}</span>
|
||||
<span className="text-[var(--c-muted)]">{repoName}</span>
|
||||
<span className="text-[var(--c-muted)]">/</span>
|
||||
<span className="font-medium text-[var(--c-text)]">{fileName}</span>
|
||||
<button
|
||||
onClick={() => navigator.clipboard.writeText(filePath)}
|
||||
className="text-[#5E6C84] hover:text-[#172B4D]"
|
||||
className="text-[var(--c-muted)] hover:text-[var(--c-text)]"
|
||||
title="Copy path"
|
||||
>
|
||||
<svg width="14" height="14" fill="none" stroke="currentColor" strokeWidth="1.5" viewBox="0 0 24 24">
|
||||
@@ -110,14 +110,14 @@ export default function BlobPage() {
|
||||
{isMarkdown && (
|
||||
<button
|
||||
onClick={() => setPreview(p => !p)}
|
||||
className={`px-3 py-1.5 text-xs font-medium rounded border ${preview ? 'border-[#0052CC] text-[#0052CC] bg-[#DEEBFF]' : 'border-[#DFE1E6] text-[#5E6C84] hover:bg-[#F4F5F7]'}`}
|
||||
className={`px-3 py-1.5 text-xs font-medium rounded border ${preview ? 'border-[var(--c-brand)] text-[var(--c-brand)] bg-[var(--c-brand-tint)]' : 'border-[var(--c-border)] text-[var(--c-muted)] hover:bg-[var(--c-surface-muted)]'}`}
|
||||
>
|
||||
{preview ? 'Source' : 'Preview'}
|
||||
</button>
|
||||
)}
|
||||
<button
|
||||
onClick={startEdit}
|
||||
className="px-3 py-1.5 text-xs font-medium border border-[#DFE1E6] rounded text-[#172B4D] hover:bg-[#F4F5F7] flex items-center gap-1.5"
|
||||
className="px-3 py-1.5 text-xs font-medium border border-[var(--c-border)] rounded text-[var(--c-text)] hover:bg-[var(--c-surface-muted)] flex items-center gap-1.5"
|
||||
>
|
||||
<svg width="12" height="12" fill="none" stroke="currentColor" strokeWidth="2" viewBox="0 0 24 24">
|
||||
<path strokeLinecap="round" strokeLinejoin="round" d="m16.862 4.487 1.687-1.688a1.875 1.875 0 1 1 2.652 2.652L10.582 16.07a4.5 4.5 0 0 1-1.897 1.13L6 18l.8-2.685a4.5 4.5 0 0 1 1.13-1.897l8.932-8.931Zm0 0L19.5 7.125" />
|
||||
@@ -126,7 +126,7 @@ export default function BlobPage() {
|
||||
</button>
|
||||
<button
|
||||
onClick={() => navigator.clipboard.writeText(blob.content)}
|
||||
className="px-3 py-1.5 text-xs font-medium border border-[#DFE1E6] rounded text-[#5E6C84] hover:bg-[#F4F5F7]"
|
||||
className="px-3 py-1.5 text-xs font-medium border border-[var(--c-border)] rounded text-[var(--c-muted)] hover:bg-[var(--c-surface-muted)]"
|
||||
>
|
||||
Copy
|
||||
</button>
|
||||
@@ -140,17 +140,17 @@ export default function BlobPage() {
|
||||
<textarea
|
||||
value={editContent}
|
||||
onChange={e => setEditContent(e.target.value)}
|
||||
className="w-full font-mono text-xs text-[#172B4D] bg-white p-4 resize-none focus:outline-none border-b border-[#DFE1E6]"
|
||||
className="w-full font-mono text-xs text-[var(--c-text)] bg-[var(--c-surface)] p-4 resize-none focus:outline-none border-b border-[var(--c-border)]"
|
||||
style={{ minHeight: Math.max(300, lines.length * 20) }}
|
||||
spellCheck={false}
|
||||
/>
|
||||
<div className="p-4 bg-[#FAFBFC] border-t border-[#DFE1E6] space-y-3">
|
||||
<div className="p-4 bg-[var(--c-surface-raised)] border-t border-[var(--c-border)] space-y-3">
|
||||
<div>
|
||||
<label className="block text-xs font-semibold text-[#172B4D] mb-1">Commit message</label>
|
||||
<label className="block text-xs font-semibold text-[var(--c-text)] mb-1">Commit message</label>
|
||||
<input
|
||||
value={commitMsg}
|
||||
onChange={e => setCommitMsg(e.target.value)}
|
||||
className="w-full border border-[#DFE1E6] rounded px-3 py-2 text-sm focus:outline-none focus:border-[#4C9AFF]"
|
||||
className="w-full border border-[var(--c-border)] rounded px-3 py-2 text-sm focus:outline-none focus:border-[var(--c-brand-focus)]"
|
||||
placeholder="Describe your changes…"
|
||||
/>
|
||||
</div>
|
||||
@@ -158,24 +158,24 @@ export default function BlobPage() {
|
||||
<button
|
||||
onClick={handleCommit}
|
||||
disabled={updateBlob.isPending || !commitMsg.trim()}
|
||||
className="px-4 py-2 rounded bg-[#0052CC] text-white text-sm font-medium hover:bg-[#0065FF] disabled:opacity-50"
|
||||
className="px-4 py-2 rounded bg-[var(--c-brand)] text-white text-sm font-medium hover:bg-[var(--c-brand-hover)] disabled:opacity-50"
|
||||
>
|
||||
{updateBlob.isPending ? 'Committing…' : 'Commit changes'}
|
||||
</button>
|
||||
<button onClick={cancelEdit} className="px-4 py-2 rounded border border-[#DFE1E6] text-sm text-[#172B4D] hover:bg-[#F4F5F7]">
|
||||
<button onClick={cancelEdit} className="px-4 py-2 rounded border border-[var(--c-border)] text-sm text-[var(--c-text)] hover:bg-[var(--c-surface-muted)]">
|
||||
Cancel
|
||||
</button>
|
||||
{updateBlob.isError && (
|
||||
<span className="text-xs text-[#DE350B]">{(updateBlob.error as Error)?.message}</span>
|
||||
<span className="text-xs text-[var(--c-danger)]">{(updateBlob.error as Error)?.message}</span>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
) : isMarkdown && preview ? (
|
||||
<div className="px-6 py-5 prose prose-sm max-w-none text-[#172B4D]
|
||||
prose-headings:text-[#172B4D] prose-headings:font-semibold prose-headings:border-b prose-headings:border-[#DFE1E6] prose-headings:pb-1
|
||||
prose-a:text-[#0052CC] prose-code:bg-[#F4F5F7] prose-code:px-1 prose-code:rounded
|
||||
prose-pre:bg-[#F4F5F7] prose-pre:border prose-pre:border-[#DFE1E6] prose-pre:rounded">
|
||||
<div className="px-6 py-5 prose prose-sm max-w-none text-[var(--c-text)]
|
||||
prose-headings:text-[var(--c-text)] prose-headings:font-semibold prose-headings:border-b prose-headings:border-[var(--c-border)] prose-headings:pb-1
|
||||
prose-a:text-[var(--c-brand)] prose-code:bg-[var(--c-surface-muted)] prose-code:px-1 prose-code:rounded
|
||||
prose-pre:bg-[var(--c-surface-muted)] prose-pre:border prose-pre:border-[var(--c-border)] prose-pre:rounded">
|
||||
<ReactMarkdown remarkPlugins={[remarkGfm]}>{blob.content}</ReactMarkdown>
|
||||
</div>
|
||||
) : (
|
||||
@@ -184,10 +184,10 @@ export default function BlobPage() {
|
||||
<tbody>
|
||||
{lines.map((line, i) => (
|
||||
<tr key={i} className="hover:bg-[#FFFBDD]">
|
||||
<td className="select-none text-right text-[#5E6C84] px-4 py-0.5 w-12 border-r border-[#DFE1E6] bg-[#FAFBFC] sticky left-0">
|
||||
<td className="select-none text-right text-[var(--c-muted)] px-4 py-0.5 w-12 border-r border-[var(--c-border)] bg-[var(--c-surface-raised)] sticky left-0">
|
||||
{i + 1}
|
||||
</td>
|
||||
<td className="px-4 py-0.5 text-[#172B4D] whitespace-pre">{line || ' '}</td>
|
||||
<td className="px-4 py-0.5 text-[var(--c-text)] whitespace-pre">{line || ' '}</td>
|
||||
</tr>
|
||||
))}
|
||||
</tbody>
|
||||
|
||||
@@ -22,29 +22,29 @@ export default function BranchesPage() {
|
||||
return (
|
||||
<div className="max-w-4xl mx-auto px-4 md:px-6 py-6">
|
||||
<div className="flex items-center gap-1 text-sm mb-4">
|
||||
<Link to={`/repos/${owner}/${repo}`} className="text-[#0052CC] hover:underline">{repo}</Link>
|
||||
<span className="text-[#5E6C84]">/</span>
|
||||
<span className="font-semibold text-[#172B4D]">Branches</span>
|
||||
<Link to={`/repos/${owner}/${repo}`} className="text-[var(--c-brand)] hover:underline">{repo}</Link>
|
||||
<span className="text-[var(--c-muted)]">/</span>
|
||||
<span className="font-semibold text-[var(--c-text)]">Branches</span>
|
||||
</div>
|
||||
|
||||
<h1 className="text-xl font-semibold text-[#172B4D] mb-4">Branches</h1>
|
||||
<h1 className="text-xl font-semibold text-[var(--c-text)] mb-4">Branches</h1>
|
||||
|
||||
{isLoading && <p className="text-sm text-[#5E6C84]">Loading branches…</p>}
|
||||
{isError && <p className="text-sm text-[#DE350B]">Failed to load branches.</p>}
|
||||
{isLoading && <p className="text-sm text-[var(--c-muted)]">Loading branches…</p>}
|
||||
{isError && <p className="text-sm text-[var(--c-danger)]">Failed to load branches.</p>}
|
||||
{!isLoading && !branches?.length && (
|
||||
<p className="text-sm text-[#5E6C84] py-8 text-center">No branches yet.</p>
|
||||
<p className="text-sm text-[var(--c-muted)] py-8 text-center">No branches yet.</p>
|
||||
)}
|
||||
|
||||
{branches && branches.length > 0 && (
|
||||
<div className="border border-[#DFE1E6] rounded overflow-hidden bg-white">
|
||||
<div className="border border-[var(--c-border)] rounded overflow-hidden bg-[var(--c-surface)]">
|
||||
{branches.map((branch, i) => (
|
||||
<div key={branch.name}
|
||||
className={`flex items-center gap-3 px-4 py-3 ${i > 0 ? 'border-t border-[#DFE1E6]' : ''} hover:bg-[#FAFBFC]`}>
|
||||
<svg width="14" height="14" fill="none" stroke="#5E6C84" strokeWidth="1.5" viewBox="0 0 24 24" className="shrink-0">
|
||||
className={`flex items-center gap-3 px-4 py-3 ${i > 0 ? 'border-t border-[var(--c-border)]' : ''} hover:bg-[var(--c-surface-raised)]`}>
|
||||
<svg width="14" height="14" fill="none" stroke="var(--c-muted)" strokeWidth="1.5" viewBox="0 0 24 24" className="shrink-0">
|
||||
<path strokeLinecap="round" strokeLinejoin="round" d="M3 13.5V6a2.25 2.25 0 0 1 2.25-2.25h.75a2.25 2.25 0 0 1 2.25 2.25v3.75A2.25 2.25 0 0 1 6 12H5.25A2.25 2.25 0 0 0 3 14.25v2.25A2.25 2.25 0 0 0 5.25 18.75H6a2.25 2.25 0 0 0 2.25-2.25V15m0 0a3 3 0 1 0 6 0 3 3 0 0 0-6 0Zm0 0h3" />
|
||||
</svg>
|
||||
<Link to={`/repos/${owner}/${repo}?ref=${branch.name}`}
|
||||
className="text-sm text-[#0052CC] hover:underline font-mono">
|
||||
className="text-sm text-[var(--c-brand)] hover:underline font-mono">
|
||||
{branch.name}
|
||||
</Link>
|
||||
</div>
|
||||
|
||||
@@ -27,32 +27,32 @@ export default function CommitsPage() {
|
||||
return (
|
||||
<div className="max-w-4xl mx-auto px-4 md:px-6 py-6">
|
||||
<div className="flex items-center gap-1 text-sm mb-4">
|
||||
<Link to={`/repos/${owner}/${repo}`} className="text-[#0052CC] hover:underline">{repo}</Link>
|
||||
<span className="text-[#5E6C84]">/</span>
|
||||
<span className="font-semibold text-[#172B4D]">Commits</span>
|
||||
<Link to={`/repos/${owner}/${repo}`} className="text-[var(--c-brand)] hover:underline">{repo}</Link>
|
||||
<span className="text-[var(--c-muted)]">/</span>
|
||||
<span className="font-semibold text-[var(--c-text)]">Commits</span>
|
||||
</div>
|
||||
|
||||
{isLoading && <p className="text-sm text-[#5E6C84]">Loading commits…</p>}
|
||||
{isError && <p className="text-sm text-[#DE350B]">Failed to load commits.</p>}
|
||||
{isLoading && <p className="text-sm text-[var(--c-muted)]">Loading commits…</p>}
|
||||
{isError && <p className="text-sm text-[var(--c-danger)]">Failed to load commits.</p>}
|
||||
{!isLoading && !commits?.length && (
|
||||
<p className="text-sm text-[#5E6C84] py-8 text-center">No commits yet. Push your first commit to get started.</p>
|
||||
<p className="text-sm text-[var(--c-muted)] py-8 text-center">No commits yet. Push your first commit to get started.</p>
|
||||
)}
|
||||
|
||||
{commits && commits.length > 0 && (
|
||||
<div className="border border-[#DFE1E6] rounded overflow-hidden bg-white">
|
||||
<div className="border border-[var(--c-border)] rounded overflow-hidden bg-[var(--c-surface)]">
|
||||
{commits.map((commit, i) => (
|
||||
<div key={commit.hash}
|
||||
className={`flex items-start gap-4 px-4 py-3 ${i > 0 ? 'border-t border-[#DFE1E6]' : ''} hover:bg-[#FAFBFC]`}>
|
||||
<div className="w-7 h-7 rounded-full bg-[#0052CC]/10 text-[#0052CC] flex items-center justify-center text-[10px] font-bold shrink-0 mt-0.5">
|
||||
className={`flex items-start gap-4 px-4 py-3 ${i > 0 ? 'border-t border-[var(--c-border)]' : ''} hover:bg-[var(--c-surface-raised)]`}>
|
||||
<div className="w-7 h-7 rounded-full bg-[var(--c-brand)]/10 text-[var(--c-brand)] flex items-center justify-center text-[10px] font-bold shrink-0 mt-0.5">
|
||||
{commit.author?.[0]?.toUpperCase()}
|
||||
</div>
|
||||
<div className="flex-1 min-w-0">
|
||||
<p className="text-sm font-medium text-[#172B4D] truncate">{commit.message}</p>
|
||||
<p className="text-xs text-[#5E6C84] mt-0.5">
|
||||
<p className="text-sm font-medium text-[var(--c-text)] truncate">{commit.message}</p>
|
||||
<p className="text-xs text-[var(--c-muted)] mt-0.5">
|
||||
{commit.author} · {new Date(commit.date).toLocaleDateString()}
|
||||
</p>
|
||||
</div>
|
||||
<code className="text-xs font-mono text-[#5E6C84] bg-[#F4F5F7] px-2 py-0.5 rounded shrink-0">
|
||||
<code className="text-xs font-mono text-[var(--c-muted)] bg-[var(--c-surface-muted)] px-2 py-0.5 rounded shrink-0">
|
||||
{commit.hash.slice(0, 7)}
|
||||
</code>
|
||||
</div>
|
||||
|
||||
@@ -32,12 +32,12 @@ export default function CreateRepoPage() {
|
||||
<div className="max-w-2xl mx-auto px-4 py-10">
|
||||
{/* Header */}
|
||||
<div className="flex items-center justify-between mb-6">
|
||||
<h1 className="text-xl font-semibold text-[#172B4D]">Create a new repository</h1>
|
||||
<Link to="/repos/import" className="text-sm text-[#0052CC] hover:underline">
|
||||
<h1 className="text-xl font-semibold text-[var(--c-text)]">Create a new repository</h1>
|
||||
<Link to="/repos/import" className="text-sm text-[var(--c-brand)] hover:underline">
|
||||
Import repository
|
||||
</Link>
|
||||
</div>
|
||||
<div className="border-t border-[#DFE1E6] mb-6" />
|
||||
<div className="border-t border-[var(--c-border)] mb-6" />
|
||||
|
||||
<form onSubmit={handleSubmit} className="space-y-5">
|
||||
|
||||
@@ -49,10 +49,10 @@ export default function CreateRepoPage() {
|
||||
placeholder="my-repository"
|
||||
required
|
||||
autoFocus
|
||||
className="w-full border border-[#DFE1E6] rounded px-3 py-2 text-sm focus:outline-none focus:border-[#4C9AFF] focus:ring-1 focus:ring-[#4C9AFF]"
|
||||
className="w-full border border-[var(--c-border)] rounded px-3 py-2 text-sm focus:outline-none focus:border-[var(--c-brand-focus)] focus:ring-1 focus:ring-[var(--c-brand-focus)]"
|
||||
/>
|
||||
{name && !/^[a-zA-Z0-9._-]+$/.test(name) && (
|
||||
<p className="text-xs text-[#DE350B] mt-1">Only letters, numbers, hyphens, underscores and dots are allowed.</p>
|
||||
<p className="text-xs text-[var(--c-danger)] mt-1">Only letters, numbers, hyphens, underscores and dots are allowed.</p>
|
||||
)}
|
||||
</Field>
|
||||
|
||||
@@ -63,11 +63,11 @@ export default function CreateRepoPage() {
|
||||
type="checkbox"
|
||||
checked={isPrivate}
|
||||
onChange={e => setIsPrivate(e.target.checked)}
|
||||
className="mt-0.5 w-4 h-4 accent-[#0052CC]"
|
||||
className="mt-0.5 w-4 h-4 accent-[var(--c-brand)]"
|
||||
/>
|
||||
<div>
|
||||
<span className="text-sm font-medium text-[#172B4D]">Private repository</span>
|
||||
<p className="text-xs text-[#0052CC] mt-0.5 leading-relaxed">
|
||||
<span className="text-sm font-medium text-[var(--c-text)]">Private repository</span>
|
||||
<p className="text-xs text-[var(--c-brand)] mt-0.5 leading-relaxed">
|
||||
{isPrivate
|
||||
? 'Uncheck to make this repository public. Public repositories typically contain open-source code and can be viewed by anyone.'
|
||||
: 'Check to make this repository private. Only invited collaborators can see and push to it.'}
|
||||
@@ -81,7 +81,7 @@ export default function CreateRepoPage() {
|
||||
<select
|
||||
value={initReadme}
|
||||
onChange={e => setInitReadme(e.target.value as 'none' | 'blank' | 'tutorial')}
|
||||
className="w-full border border-[#DFE1E6] rounded px-3 py-2 text-sm focus:outline-none focus:border-[#4C9AFF] bg-white"
|
||||
className="w-full border border-[var(--c-border)] rounded px-3 py-2 text-sm focus:outline-none focus:border-[var(--c-brand-focus)] bg-[var(--c-surface)]"
|
||||
>
|
||||
<option value="none">No</option>
|
||||
<option value="blank">Yes, blank</option>
|
||||
@@ -95,7 +95,7 @@ export default function CreateRepoPage() {
|
||||
value={defaultBranch}
|
||||
onChange={e => setDefaultBranch(e.target.value)}
|
||||
placeholder="e.g., 'main'"
|
||||
className="w-full border border-[#DFE1E6] rounded px-3 py-2 text-sm focus:outline-none focus:border-[#4C9AFF] focus:ring-1 focus:ring-[#4C9AFF]"
|
||||
className="w-full border border-[var(--c-border)] rounded px-3 py-2 text-sm focus:outline-none focus:border-[var(--c-brand-focus)] focus:ring-1 focus:ring-[var(--c-brand-focus)]"
|
||||
/>
|
||||
</Field>
|
||||
|
||||
@@ -104,7 +104,7 @@ export default function CreateRepoPage() {
|
||||
<select
|
||||
value={initGitignore ? 'yes' : 'no'}
|
||||
onChange={e => setInitGitignore(e.target.value === 'yes')}
|
||||
className="w-full border border-[#DFE1E6] rounded px-3 py-2 text-sm focus:outline-none focus:border-[#4C9AFF] bg-white"
|
||||
className="w-full border border-[var(--c-border)] rounded px-3 py-2 text-sm focus:outline-none focus:border-[var(--c-brand-focus)] bg-[var(--c-surface)]"
|
||||
>
|
||||
<option value="yes">Yes (recommended)</option>
|
||||
<option value="no">No</option>
|
||||
@@ -116,7 +116,7 @@ export default function CreateRepoPage() {
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => setShowAdvanced(s => !s)}
|
||||
className="flex items-center gap-1.5 text-sm text-[#0052CC] hover:underline font-medium"
|
||||
className="flex items-center gap-1.5 text-sm text-[var(--c-brand)] hover:underline font-medium"
|
||||
>
|
||||
<svg
|
||||
width="14" height="14" fill="none" stroke="currentColor" strokeWidth="2.5" viewBox="0 0 24 24"
|
||||
@@ -128,13 +128,13 @@ export default function CreateRepoPage() {
|
||||
</button>
|
||||
|
||||
{showAdvanced && (
|
||||
<div className="mt-4 space-y-4 border-t border-[#DFE1E6] pt-4">
|
||||
<div className="mt-4 space-y-4 border-t border-[var(--c-border)] pt-4">
|
||||
<Field label="Description">
|
||||
<textarea
|
||||
value={description}
|
||||
onChange={e => setDescription(e.target.value)}
|
||||
rows={4}
|
||||
className="w-full border border-[#DFE1E6] rounded px-3 py-2 text-sm focus:outline-none focus:border-[#4C9AFF] resize-y"
|
||||
className="w-full border border-[var(--c-border)] rounded px-3 py-2 text-sm focus:outline-none focus:border-[var(--c-brand-focus)] resize-y"
|
||||
placeholder="Describe your repository…"
|
||||
/>
|
||||
</Field>
|
||||
@@ -143,23 +143,23 @@ export default function CreateRepoPage() {
|
||||
</div>
|
||||
|
||||
{createRepo.isError && (
|
||||
<p className="text-xs text-[#DE350B]">
|
||||
<p className="text-xs text-[var(--c-danger)]">
|
||||
{createRepo.error instanceof Error ? createRepo.error.message : 'Failed to create repository.'}
|
||||
</p>
|
||||
)}
|
||||
|
||||
{/* Footer actions */}
|
||||
<div className="border-t border-[#DFE1E6] pt-5 flex items-center justify-end gap-3">
|
||||
<div className="border-t border-[var(--c-border)] pt-5 flex items-center justify-end gap-3">
|
||||
<Link
|
||||
to="/repos"
|
||||
className="px-4 py-2 rounded border border-[#DFE1E6] text-sm text-[#172B4D] hover:bg-[#F4F5F7] min-h-[36px] flex items-center"
|
||||
className="px-4 py-2 rounded border border-[var(--c-border)] text-sm text-[var(--c-text)] hover:bg-[var(--c-surface-muted)] min-h-[36px] flex items-center"
|
||||
>
|
||||
Cancel
|
||||
</Link>
|
||||
<button
|
||||
type="submit"
|
||||
disabled={createRepo.isPending || !name.trim()}
|
||||
className="px-4 py-2 rounded bg-[#0052CC] text-white text-sm font-medium hover:bg-[#0065FF] disabled:opacity-50 min-h-[36px]"
|
||||
className="px-4 py-2 rounded bg-[var(--c-brand)] text-white text-sm font-medium hover:bg-[var(--c-brand-hover)] disabled:opacity-50 min-h-[36px]"
|
||||
>
|
||||
{createRepo.isPending ? 'Creating…' : 'Create repository'}
|
||||
</button>
|
||||
@@ -172,8 +172,8 @@ export default function CreateRepoPage() {
|
||||
function Field({ label, required, children }: { label: string; required?: boolean; children: React.ReactNode }) {
|
||||
return (
|
||||
<div className="grid grid-cols-[180px_1fr] gap-4 items-start">
|
||||
<label className="text-sm text-[#172B4D] text-right pt-2 leading-tight">
|
||||
{label}{required && <span className="text-[#DE350B] ml-0.5">*</span>}
|
||||
<label className="text-sm text-[var(--c-text)] text-right pt-2 leading-tight">
|
||||
{label}{required && <span className="text-[var(--c-danger)] ml-0.5">*</span>}
|
||||
</label>
|
||||
<div>{children}</div>
|
||||
</div>
|
||||
|
||||
@@ -16,23 +16,23 @@ export default function DashboardPage() {
|
||||
|
||||
{/* Hero — only when no repos yet */}
|
||||
{!reposLoading && !hasRepos && isAuthenticated && (
|
||||
<div className="rounded-lg border border-[#DFE1E6] bg-white overflow-hidden">
|
||||
<div className="rounded-lg border border-[var(--c-border)] bg-[var(--c-surface)] overflow-hidden">
|
||||
<div className="flex items-center gap-8 p-8">
|
||||
<HeroIllustration />
|
||||
<div>
|
||||
<h1 className="text-xl font-semibold text-[#172B4D]">
|
||||
<h1 className="text-xl font-semibold text-[var(--c-text)]">
|
||||
Welcome to ForgeBucket{user?.username ? `, ${user.username}` : ''}!
|
||||
</h1>
|
||||
<p className="text-sm text-[#5E6C84] mt-2 max-w-md">
|
||||
<p className="text-sm text-[var(--c-muted)] mt-2 max-w-md">
|
||||
Get started by creating your first repository, pushing code, and collaborating through pull requests.
|
||||
</p>
|
||||
<div className="flex items-center gap-3 mt-5">
|
||||
<Link to="/repos"
|
||||
className="px-4 py-2 rounded bg-[#0052CC] text-white text-sm font-medium hover:bg-[#0065FF] min-h-[36px] flex items-center">
|
||||
className="px-4 py-2 rounded bg-[var(--c-brand)] text-white text-sm font-medium hover:bg-[var(--c-brand-hover)] min-h-[36px] flex items-center">
|
||||
Create repository
|
||||
</Link>
|
||||
<Link to="/explore"
|
||||
className="px-4 py-2 rounded border border-[#DFE1E6] text-sm text-[#172B4D] font-medium hover:bg-[#F4F5F7] min-h-[36px] flex items-center">
|
||||
className="px-4 py-2 rounded border border-[var(--c-border)] text-sm text-[var(--c-text)] font-medium hover:bg-[var(--c-surface-muted)] min-h-[36px] flex items-center">
|
||||
Explore
|
||||
</Link>
|
||||
</div>
|
||||
@@ -44,22 +44,22 @@ export default function DashboardPage() {
|
||||
{/* Recent repositories */}
|
||||
<section>
|
||||
<div className="flex items-center justify-between mb-3">
|
||||
<h2 className="text-sm font-semibold text-[#172B4D] flex items-center gap-2">
|
||||
<h2 className="text-sm font-semibold text-[var(--c-text)] flex items-center gap-2">
|
||||
Recent repositories
|
||||
<Link to="/repos"
|
||||
className="ml-1 w-5 h-5 rounded border border-[#DFE1E6] text-[#5E6C84] flex items-center justify-center hover:bg-[#F4F5F7] text-xs">
|
||||
className="ml-1 w-5 h-5 rounded border border-[var(--c-border)] text-[var(--c-muted)] flex items-center justify-center hover:bg-[var(--c-surface-muted)] text-xs">
|
||||
+
|
||||
</Link>
|
||||
</h2>
|
||||
<Link to="/repos" className="text-xs text-[#0052CC] hover:underline">View all</Link>
|
||||
<Link to="/repos" className="text-xs text-[var(--c-brand)] hover:underline">View all</Link>
|
||||
</div>
|
||||
|
||||
{reposLoading ? (
|
||||
<RepoListSkeleton />
|
||||
) : !repos?.length ? (
|
||||
<div className="border border-dashed border-[#DFE1E6] rounded p-6 text-center">
|
||||
<p className="text-sm text-[#5E6C84]">No repositories yet.</p>
|
||||
<Link to="/repos" className="text-xs text-[#0052CC] hover:underline mt-1 inline-block">
|
||||
<div className="border border-dashed border-[var(--c-border)] rounded p-6 text-center">
|
||||
<p className="text-sm text-[var(--c-muted)]">No repositories yet.</p>
|
||||
<Link to="/repos" className="text-xs text-[var(--c-brand)] hover:underline mt-1 inline-block">
|
||||
Create your first repository →
|
||||
</Link>
|
||||
</div>
|
||||
@@ -74,7 +74,7 @@ export default function DashboardPage() {
|
||||
{repos && repos.length > 0 && (
|
||||
<section>
|
||||
<div className="flex items-center justify-between mb-3">
|
||||
<h2 className="text-sm font-semibold text-[#172B4D]">Pull requests</h2>
|
||||
<h2 className="text-sm font-semibold text-[var(--c-text)]">Pull requests</h2>
|
||||
</div>
|
||||
<PullRequestSummary repos={repos.map(r => ({ owner: r.ownerName, name: r.name }))} />
|
||||
</section>
|
||||
@@ -93,8 +93,8 @@ function PullRequestSummary({ repos }: { repos: { owner: string; name: string }[
|
||||
|
||||
if (!open.length) {
|
||||
return (
|
||||
<div className="border border-[#DFE1E6] rounded p-6 text-center bg-white">
|
||||
<p className="text-sm text-[#5E6C84]">You have no open pull requests.</p>
|
||||
<div className="border border-[var(--c-border)] rounded p-6 text-center bg-[var(--c-surface)]">
|
||||
<p className="text-sm text-[var(--c-muted)]">You have no open pull requests.</p>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
@@ -105,14 +105,14 @@ function PullRequestSummary({ repos }: { repos: { owner: string; name: string }[
|
||||
<Link
|
||||
key={pr.id}
|
||||
to={`/repos/${first.owner}/${first.name}/pulls/${pr.id}`}
|
||||
className="flex items-center gap-3 p-4 border border-[#DFE1E6] rounded bg-white hover:border-[#4C9AFF] hover:bg-[#FAFBFC] transition-colors"
|
||||
className="flex items-center gap-3 p-4 border border-[var(--c-border)] rounded bg-[var(--c-surface)] hover:border-[var(--c-brand-focus)] hover:bg-[var(--c-surface-raised)] transition-colors"
|
||||
>
|
||||
<svg width="16" height="16" fill="none" stroke="#00875A" strokeWidth="1.5" viewBox="0 0 24 24" className="shrink-0">
|
||||
<svg width="16" height="16" fill="none" stroke="var(--c-success)" strokeWidth="1.5" viewBox="0 0 24 24" className="shrink-0">
|
||||
<path strokeLinecap="round" strokeLinejoin="round" d="M7.5 21 3 16.5m0 0L7.5 12M3 16.5h13.5m0-13.5L21 7.5m0 0L16.5 12M21 7.5H7.5" />
|
||||
</svg>
|
||||
<div className="flex-1 min-w-0">
|
||||
<p className="text-sm font-medium text-[#172B4D] truncate">{pr.title}</p>
|
||||
<p className="text-xs text-[#5E6C84] mt-0.5">
|
||||
<p className="text-sm font-medium text-[var(--c-text)] truncate">{pr.title}</p>
|
||||
<p className="text-xs text-[var(--c-muted)] mt-0.5">
|
||||
{first.name} · {pr.sourceBranch} → {pr.targetBranch}
|
||||
</p>
|
||||
</div>
|
||||
@@ -124,7 +124,7 @@ function PullRequestSummary({ repos }: { repos: { owner: string; name: string }[
|
||||
|
||||
function HeroIllustration() {
|
||||
return (
|
||||
<div className="shrink-0 w-32 h-32 bg-[#DEEBFF] rounded-lg flex items-center justify-center text-[#0052CC]">
|
||||
<div className="shrink-0 w-32 h-32 bg-[var(--c-brand-tint)] rounded-lg flex items-center justify-center text-[var(--c-brand)]">
|
||||
<svg width="64" height="64" fill="none" stroke="currentColor" strokeWidth="1" viewBox="0 0 24 24">
|
||||
<path strokeLinecap="round" strokeLinejoin="round" d="M3.75 9.776c.112-.017.227-.026.344-.026h15.812c.117 0 .232.009.344.026m-16.5 0a2.25 2.25 0 0 0-1.883 2.542l.857 6a2.25 2.25 0 0 0 2.227 1.932H19.05a2.25 2.25 0 0 0 2.227-1.932l.857-6a2.25 2.25 0 0 0-1.883-2.542m-16.5 0V6A2.25 2.25 0 0 1 6 3.75h3.879a1.5 1.5 0 0 1 1.06.44l2.122 2.12a1.5 1.5 0 0 0 1.06.44H18A2.25 2.25 0 0 1 20.25 9v.776" />
|
||||
</svg>
|
||||
|
||||
@@ -1,14 +1,14 @@
|
||||
export default function ExplorePage() {
|
||||
return (
|
||||
<div className="max-w-4xl mx-auto px-4 md:px-6 py-6 space-y-6">
|
||||
<h1 className="text-xl font-semibold text-[#172B4D]">Explore</h1>
|
||||
<div className="flex flex-col items-center justify-center py-16 border border-dashed border-[#DFE1E6] rounded text-center gap-3">
|
||||
<svg width="40" height="40" fill="none" stroke="#97A0AF" strokeWidth="1" viewBox="0 0 24 24">
|
||||
<h1 className="text-xl font-semibold text-[var(--c-text)]">Explore</h1>
|
||||
<div className="flex flex-col items-center justify-center py-16 border border-dashed border-[var(--c-border)] rounded text-center gap-3">
|
||||
<svg width="40" height="40" fill="none" stroke="var(--c-subtle)" strokeWidth="1" viewBox="0 0 24 24">
|
||||
<path strokeLinecap="round" strokeLinejoin="round" d="m21 21-5.197-5.197m0 0A7.5 7.5 0 1 0 5.196 5.196a7.5 7.5 0 0 0 10.607 10.607Z" />
|
||||
</svg>
|
||||
<div>
|
||||
<p className="text-sm font-medium text-[#172B4D]">Explore public repositories</p>
|
||||
<p className="text-xs text-[#5E6C84] mt-1">
|
||||
<p className="text-sm font-medium text-[var(--c-text)]">Explore public repositories</p>
|
||||
<p className="text-xs text-[var(--c-muted)] mt-1">
|
||||
Federated discovery across ForgeBucket instances — coming soon.
|
||||
</p>
|
||||
</div>
|
||||
|
||||
@@ -42,18 +42,18 @@ export default function ImportRepoPage() {
|
||||
<div className="max-w-2xl mx-auto px-4 py-10">
|
||||
{/* Header */}
|
||||
<div className="flex items-center justify-between mb-6">
|
||||
<h1 className="text-xl font-semibold text-[#172B4D]">Import existing code</h1>
|
||||
<Link to="/repos/new" className="text-sm text-[#0052CC] hover:underline">
|
||||
<h1 className="text-xl font-semibold text-[var(--c-text)]">Import existing code</h1>
|
||||
<Link to="/repos/new" className="text-sm text-[var(--c-brand)] hover:underline">
|
||||
Create new repository
|
||||
</Link>
|
||||
</div>
|
||||
<div className="border-t border-[#DFE1E6] mb-6" />
|
||||
<div className="border-t border-[var(--c-border)] mb-6" />
|
||||
|
||||
<form onSubmit={handleSubmit} className="space-y-6">
|
||||
|
||||
{/* Old repository section */}
|
||||
<div>
|
||||
<h2 className="text-sm font-semibold text-[#172B4D] mb-4">Old repository</h2>
|
||||
<h2 className="text-sm font-semibold text-[var(--c-text)] mb-4">Old repository</h2>
|
||||
<div className="space-y-4">
|
||||
<Field label="URL" required>
|
||||
<input
|
||||
@@ -62,7 +62,7 @@ export default function ImportRepoPage() {
|
||||
placeholder="https://github.com/user/repo.git"
|
||||
required
|
||||
autoFocus
|
||||
className="w-full border border-[#DFE1E6] rounded px-3 py-2 text-sm focus:outline-none focus:border-[#4C9AFF] focus:ring-1 focus:ring-[#4C9AFF]"
|
||||
className="w-full border border-[var(--c-border)] rounded px-3 py-2 text-sm focus:outline-none focus:border-[var(--c-brand-focus)] focus:ring-1 focus:ring-[var(--c-brand-focus)]"
|
||||
/>
|
||||
</Field>
|
||||
|
||||
@@ -72,9 +72,9 @@ export default function ImportRepoPage() {
|
||||
type="checkbox"
|
||||
checked={requiresAuth}
|
||||
onChange={e => setRequiresAuth(e.target.checked)}
|
||||
className="w-4 h-4 accent-[#0052CC]"
|
||||
className="w-4 h-4 accent-[var(--c-brand)]"
|
||||
/>
|
||||
<span className="text-sm text-[#172B4D]">Requires authorization</span>
|
||||
<span className="text-sm text-[var(--c-text)]">Requires authorization</span>
|
||||
</label>
|
||||
|
||||
{requiresAuth && (
|
||||
@@ -83,14 +83,14 @@ export default function ImportRepoPage() {
|
||||
value={authUser}
|
||||
onChange={e => setAuthUser(e.target.value)}
|
||||
placeholder="Username"
|
||||
className="w-full border border-[#DFE1E6] rounded px-3 py-2 text-sm focus:outline-none focus:border-[#4C9AFF]"
|
||||
className="w-full border border-[var(--c-border)] rounded px-3 py-2 text-sm focus:outline-none focus:border-[var(--c-brand-focus)]"
|
||||
/>
|
||||
<input
|
||||
type="password"
|
||||
value={authPass}
|
||||
onChange={e => setAuthPass(e.target.value)}
|
||||
placeholder="Password or access token"
|
||||
className="w-full border border-[#DFE1E6] rounded px-3 py-2 text-sm focus:outline-none focus:border-[#4C9AFF]"
|
||||
className="w-full border border-[var(--c-border)] rounded px-3 py-2 text-sm focus:outline-none focus:border-[var(--c-brand-focus)]"
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
@@ -98,11 +98,11 @@ export default function ImportRepoPage() {
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="border-t border-[#DFE1E6]" />
|
||||
<div className="border-t border-[var(--c-border)]" />
|
||||
|
||||
{/* New repository section */}
|
||||
<div>
|
||||
<h2 className="text-sm font-semibold text-[#172B4D] mb-4">New repository</h2>
|
||||
<h2 className="text-sm font-semibold text-[var(--c-text)] mb-4">New repository</h2>
|
||||
<div className="space-y-4">
|
||||
|
||||
<Field label="Repository name" required>
|
||||
@@ -111,7 +111,7 @@ export default function ImportRepoPage() {
|
||||
onChange={e => setName(e.target.value.replace(/[^a-zA-Z0-9._-]/g, '-'))}
|
||||
placeholder="my-repository"
|
||||
required
|
||||
className="w-full border border-[#DFE1E6] rounded px-3 py-2 text-sm focus:outline-none focus:border-[#4C9AFF] focus:ring-1 focus:ring-[#4C9AFF]"
|
||||
className="w-full border border-[var(--c-border)] rounded px-3 py-2 text-sm focus:outline-none focus:border-[var(--c-brand-focus)] focus:ring-1 focus:ring-[var(--c-brand-focus)]"
|
||||
/>
|
||||
</Field>
|
||||
|
||||
@@ -121,11 +121,11 @@ export default function ImportRepoPage() {
|
||||
type="checkbox"
|
||||
checked={isPrivate}
|
||||
onChange={e => setIsPrivate(e.target.checked)}
|
||||
className="mt-0.5 w-4 h-4 accent-[#0052CC]"
|
||||
className="mt-0.5 w-4 h-4 accent-[var(--c-brand)]"
|
||||
/>
|
||||
<div>
|
||||
<span className="text-sm font-medium text-[#172B4D]">Private repository</span>
|
||||
<p className="text-xs text-[#0052CC] mt-0.5 leading-relaxed">
|
||||
<span className="text-sm font-medium text-[var(--c-text)]">Private repository</span>
|
||||
<p className="text-xs text-[var(--c-brand)] mt-0.5 leading-relaxed">
|
||||
{isPrivate
|
||||
? 'Uncheck to make this repository public. Public repositories typically contain open-source code and can be viewed by anyone.'
|
||||
: 'Check to make this repository private. Only invited collaborators can see and push to it.'}
|
||||
@@ -139,7 +139,7 @@ export default function ImportRepoPage() {
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => setShowAdvanced(s => !s)}
|
||||
className="flex items-center gap-1.5 text-sm text-[#0052CC] hover:underline font-medium"
|
||||
className="flex items-center gap-1.5 text-sm text-[var(--c-brand)] hover:underline font-medium"
|
||||
>
|
||||
<svg
|
||||
width="14" height="14" fill="none" stroke="currentColor" strokeWidth="2.5" viewBox="0 0 24 24"
|
||||
@@ -151,13 +151,13 @@ export default function ImportRepoPage() {
|
||||
</button>
|
||||
|
||||
{showAdvanced && (
|
||||
<div className="mt-4 space-y-4 border-t border-[#DFE1E6] pt-4">
|
||||
<div className="mt-4 space-y-4 border-t border-[var(--c-border)] pt-4">
|
||||
<Field label="Description">
|
||||
<textarea
|
||||
value={description}
|
||||
onChange={e => setDescription(e.target.value)}
|
||||
rows={4}
|
||||
className="w-full border border-[#DFE1E6] rounded px-3 py-2 text-sm focus:outline-none focus:border-[#4C9AFF] resize-y"
|
||||
className="w-full border border-[var(--c-border)] rounded px-3 py-2 text-sm focus:outline-none focus:border-[var(--c-brand-focus)] resize-y"
|
||||
placeholder="Describe your repository…"
|
||||
/>
|
||||
</Field>
|
||||
@@ -168,23 +168,23 @@ export default function ImportRepoPage() {
|
||||
</div>
|
||||
|
||||
{importRepo.isError && (
|
||||
<p className="text-xs text-[#DE350B]">
|
||||
<p className="text-xs text-[var(--c-danger)]">
|
||||
{importRepo.error instanceof Error ? importRepo.error.message : 'Import failed.'}
|
||||
</p>
|
||||
)}
|
||||
|
||||
{/* Footer */}
|
||||
<div className="border-t border-[#DFE1E6] pt-5 flex items-center justify-end gap-3">
|
||||
<div className="border-t border-[var(--c-border)] pt-5 flex items-center justify-end gap-3">
|
||||
<Link
|
||||
to="/repos"
|
||||
className="px-4 py-2 rounded border border-[#DFE1E6] text-sm text-[#172B4D] hover:bg-[#F4F5F7] min-h-[36px] flex items-center"
|
||||
className="px-4 py-2 rounded border border-[var(--c-border)] text-sm text-[var(--c-text)] hover:bg-[var(--c-surface-muted)] min-h-[36px] flex items-center"
|
||||
>
|
||||
Cancel
|
||||
</Link>
|
||||
<button
|
||||
type="submit"
|
||||
disabled={importRepo.isPending || !url.trim() || !name.trim()}
|
||||
className="px-4 py-2 rounded bg-[#0052CC] text-white text-sm font-medium hover:bg-[#0065FF] disabled:opacity-50 min-h-[36px]"
|
||||
className="px-4 py-2 rounded bg-[var(--c-brand)] text-white text-sm font-medium hover:bg-[var(--c-brand-hover)] disabled:opacity-50 min-h-[36px]"
|
||||
>
|
||||
{importRepo.isPending ? 'Importing…' : 'Import repository'}
|
||||
</button>
|
||||
@@ -197,8 +197,8 @@ export default function ImportRepoPage() {
|
||||
function Field({ label, required, children }: { label: string; required?: boolean; children: React.ReactNode }) {
|
||||
return (
|
||||
<div className="grid grid-cols-[180px_1fr] gap-4 items-start">
|
||||
<label className="text-sm text-[#172B4D] text-right pt-2 leading-tight">
|
||||
{label}{required && <span className="text-[#DE350B] ml-0.5">*</span>}
|
||||
<label className="text-sm text-[var(--c-text)] text-right pt-2 leading-tight">
|
||||
{label}{required && <span className="text-[var(--c-danger)] ml-0.5">*</span>}
|
||||
</label>
|
||||
<div>{children}</div>
|
||||
</div>
|
||||
|
||||
@@ -34,44 +34,44 @@ export default function LoginPage() {
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="min-h-screen bg-[#F4F5F7] flex items-center justify-center p-4">
|
||||
<div className="min-h-screen bg-[var(--c-surface-muted)] flex items-center justify-center p-4">
|
||||
<div className="w-full max-w-sm">
|
||||
<div className="text-center mb-8">
|
||||
<h1 className="text-2xl font-bold text-[#172B4D]">ForgeBucket</h1>
|
||||
<p className="text-sm text-[#5E6C84] mt-1">Sign in to your account</p>
|
||||
<h1 className="text-2xl font-bold text-[var(--c-text)]">ForgeBucket</h1>
|
||||
<p className="text-sm text-[var(--c-muted)] mt-1">Sign in to your account</p>
|
||||
</div>
|
||||
|
||||
<div className="bg-white border border-[#DFE1E6] rounded-lg p-6 shadow-sm">
|
||||
<div className="bg-[var(--c-surface)] border border-[var(--c-border)] rounded-lg p-6 shadow-sm">
|
||||
<form onSubmit={handleSubmit} className="flex flex-col gap-4">
|
||||
<div>
|
||||
<label className="block text-xs font-semibold text-[#172B4D] mb-1">Username</label>
|
||||
<label className="block text-xs font-semibold text-[var(--c-text)] mb-1">Username</label>
|
||||
<input
|
||||
value={username} onChange={e => setUsername(e.target.value)}
|
||||
required autoFocus autoComplete="username"
|
||||
className="w-full border border-[#DFE1E6] rounded px-3 py-2 text-sm focus:outline-none focus:border-[#4C9AFF] focus:ring-1 focus:ring-[#4C9AFF]"
|
||||
className="w-full border border-[var(--c-border)] rounded px-3 py-2 text-sm focus:outline-none focus:border-[var(--c-brand-focus)] focus:ring-1 focus:ring-[var(--c-brand-focus)]"
|
||||
/>
|
||||
</div>
|
||||
<div>
|
||||
<label className="block text-xs font-semibold text-[#172B4D] mb-1">Password</label>
|
||||
<label className="block text-xs font-semibold text-[var(--c-text)] mb-1">Password</label>
|
||||
<input
|
||||
type="password" value={password} onChange={e => setPassword(e.target.value)}
|
||||
required autoComplete="current-password"
|
||||
className="w-full border border-[#DFE1E6] rounded px-3 py-2 text-sm focus:outline-none focus:border-[#4C9AFF] focus:ring-1 focus:ring-[#4C9AFF]"
|
||||
className="w-full border border-[var(--c-border)] rounded px-3 py-2 text-sm focus:outline-none focus:border-[var(--c-brand-focus)] focus:ring-1 focus:ring-[var(--c-brand-focus)]"
|
||||
/>
|
||||
</div>
|
||||
{error && <p className="text-xs text-[#DE350B] bg-[#FFEBE6] rounded px-3 py-2">{error}</p>}
|
||||
{error && <p className="text-xs text-[var(--c-danger)] bg-[var(--c-danger-tint)] rounded px-3 py-2">{error}</p>}
|
||||
<button
|
||||
type="submit" disabled={loading}
|
||||
className="w-full py-2.5 rounded bg-[#0052CC] text-white text-sm font-semibold hover:bg-[#0065FF] disabled:opacity-50 min-h-[44px]"
|
||||
className="w-full py-2.5 rounded bg-[var(--c-brand)] text-white text-sm font-semibold hover:bg-[var(--c-brand-hover)] disabled:opacity-50 min-h-[44px]"
|
||||
>
|
||||
{loading ? 'Signing in…' : 'Sign in'}
|
||||
</button>
|
||||
</form>
|
||||
</div>
|
||||
|
||||
<p className="text-center text-xs text-[#5E6C84] mt-4">
|
||||
<p className="text-center text-xs text-[var(--c-muted)] mt-4">
|
||||
No account?{' '}
|
||||
<Link to="/register" className="text-[#0052CC] hover:underline font-medium">Create one</Link>
|
||||
<Link to="/register" className="text-[var(--c-brand)] hover:underline font-medium">Create one</Link>
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -28,35 +28,35 @@ export default function PRDetailPage() {
|
||||
}
|
||||
|
||||
if (isError || !pr) {
|
||||
return <div className="p-6 text-sm text-[#DE350B]">Pull request not found.</div>
|
||||
return <div className="p-6 text-sm text-[var(--c-danger)]">Pull request not found.</div>
|
||||
}
|
||||
|
||||
const statusColor = pr.status === 'open'
|
||||
? 'bg-[#E3FCEF] text-[#006644] border-[#79F2C0]'
|
||||
: pr.status === 'merged'
|
||||
? 'bg-[#EAE6FF] text-[#403294] border-[#C0B6F2]'
|
||||
: 'bg-[#F4F5F7] text-[#5E6C84] border-[#DFE1E6]'
|
||||
: 'bg-[var(--c-surface-muted)] text-[var(--c-muted)] border-[var(--c-border)]'
|
||||
|
||||
return (
|
||||
<div className="max-w-5xl mx-auto px-4 md:px-6 py-6 space-y-6">
|
||||
{/* Breadcrumb */}
|
||||
<div className="flex items-center gap-1 text-sm flex-wrap">
|
||||
<Link to={`/repos/${owner}/${repo}`} className="text-[#0052CC] hover:underline">{repo}</Link>
|
||||
<span className="text-[#5E6C84]">/</span>
|
||||
<Link to={`/repos/${owner}/${repo}/pulls`} className="text-[#0052CC] hover:underline">Pull requests</Link>
|
||||
<span className="text-[#5E6C84]">/</span>
|
||||
<span className="text-[#172B4D]">#{pr.id}</span>
|
||||
<Link to={`/repos/${owner}/${repo}`} className="text-[var(--c-brand)] hover:underline">{repo}</Link>
|
||||
<span className="text-[var(--c-muted)]">/</span>
|
||||
<Link to={`/repos/${owner}/${repo}/pulls`} className="text-[var(--c-brand)] hover:underline">Pull requests</Link>
|
||||
<span className="text-[var(--c-muted)]">/</span>
|
||||
<span className="text-[var(--c-text)]">#{pr.id}</span>
|
||||
</div>
|
||||
|
||||
{/* Title + status */}
|
||||
<div>
|
||||
<div className="flex items-center gap-3 flex-wrap">
|
||||
<h1 className="text-xl font-semibold text-[#172B4D]">{pr.title}</h1>
|
||||
<h1 className="text-xl font-semibold text-[var(--c-text)]">{pr.title}</h1>
|
||||
<span className={cn('text-xs font-semibold px-2 py-0.5 rounded-full border', statusColor)}>
|
||||
{pr.status}
|
||||
</span>
|
||||
</div>
|
||||
<p className="text-xs text-[#5E6C84] mt-1">
|
||||
<p className="text-xs text-[var(--c-muted)] mt-1">
|
||||
#{pr.id} · <span className="font-mono">{pr.sourceBranch}</span>
|
||||
{' → '}
|
||||
<span className="font-mono">{pr.targetBranch}</span>
|
||||
@@ -65,14 +65,14 @@ export default function PRDetailPage() {
|
||||
|
||||
{/* Body */}
|
||||
{pr.body && (
|
||||
<div className="p-4 border border-[#DFE1E6] rounded text-sm text-[#172B4D] whitespace-pre-wrap">
|
||||
<div className="p-4 border border-[var(--c-border)] rounded text-sm text-[var(--c-text)] whitespace-pre-wrap">
|
||||
{pr.body}
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Diff placeholder */}
|
||||
<div>
|
||||
<h2 className="text-sm font-semibold text-[#172B4D] mb-3 flex items-center gap-2">
|
||||
<h2 className="text-sm font-semibold text-[var(--c-text)] mb-3 flex items-center gap-2">
|
||||
<svg width="16" height="16" fill="none" stroke="currentColor" strokeWidth="1.5" viewBox="0 0 24 24">
|
||||
<path strokeLinecap="round" strokeLinejoin="round" d="M19.5 14.25v-2.625a3.375 3.375 0 0 0-3.375-3.375h-1.5A1.125 1.125 0 0 1 13.5 7.125v-1.5a3.375 3.375 0 0 0-3.375-3.375H8.25m0 12.75h7.5m-7.5 3H12M10.5 2.25H5.625c-.621 0-1.125.504-1.125 1.125v17.25c0 .621.504 1.125 1.125 1.125h12.75c.621 0 1.125-.504 1.125-1.125V11.25a9 9 0 0 0-9-9Z" />
|
||||
</svg>
|
||||
|
||||
@@ -10,10 +10,10 @@ export default function PRsPage() {
|
||||
return (
|
||||
<div className="max-w-4xl mx-auto px-4 md:px-6 py-6">
|
||||
<div className="flex items-center justify-between mb-6">
|
||||
<h1 className="text-xl font-semibold text-[#172B4D]">Pull Requests</h1>
|
||||
<h1 className="text-xl font-semibold text-[var(--c-text)]">Pull Requests</h1>
|
||||
</div>
|
||||
|
||||
<div className="flex gap-1 mb-4 border-b border-[#DFE1E6]">
|
||||
<div className="flex gap-1 mb-4 border-b border-[var(--c-border)]">
|
||||
{(['open', 'merged', 'closed'] as PRStatus[]).map(s => (
|
||||
<button
|
||||
key={s}
|
||||
@@ -21,8 +21,8 @@ export default function PRsPage() {
|
||||
className={cn(
|
||||
'px-4 py-2 text-sm font-medium capitalize border-b-2 -mb-px transition-colors min-h-[44px]',
|
||||
status === s
|
||||
? 'border-[#0052CC] text-[#0052CC]'
|
||||
: 'border-transparent text-[#5E6C84] hover:text-[#172B4D]',
|
||||
? 'border-[var(--c-brand)] text-[var(--c-brand)]'
|
||||
: 'border-transparent text-[var(--c-muted)] hover:text-[var(--c-text)]',
|
||||
)}
|
||||
>
|
||||
{s}
|
||||
@@ -30,7 +30,7 @@ export default function PRsPage() {
|
||||
))}
|
||||
</div>
|
||||
|
||||
<p className="text-sm text-[#5E6C84] py-6 text-center">
|
||||
<p className="text-sm text-[var(--c-muted)] py-6 text-center">
|
||||
Navigate to a repository to view its pull requests.
|
||||
</p>
|
||||
</div>
|
||||
|
||||
@@ -1,16 +1,16 @@
|
||||
export default function PipelinesPage() {
|
||||
return (
|
||||
<div className="max-w-4xl mx-auto px-4 md:px-6 py-6 space-y-6">
|
||||
<h1 className="text-xl font-semibold text-[#172B4D]">Pipelines</h1>
|
||||
<div className="flex flex-col items-center justify-center py-16 border border-dashed border-[#DFE1E6] rounded text-center gap-3">
|
||||
<svg width="40" height="40" fill="none" stroke="#97A0AF" strokeWidth="1" viewBox="0 0 24 24">
|
||||
<h1 className="text-xl font-semibold text-[var(--c-text)]">Pipelines</h1>
|
||||
<div className="flex flex-col items-center justify-center py-16 border border-dashed border-[var(--c-border)] rounded text-center gap-3">
|
||||
<svg width="40" height="40" fill="none" stroke="var(--c-subtle)" strokeWidth="1" viewBox="0 0 24 24">
|
||||
<path strokeLinecap="round" strokeLinejoin="round" d="M5.25 5.653c0-.856.917-1.398 1.667-.986l11.54 6.347a1.125 1.125 0 0 1 0 1.972l-11.54 6.347a1.125 1.125 0 0 1-1.667-.986V5.653Z" />
|
||||
</svg>
|
||||
<div>
|
||||
<p className="text-sm font-medium text-[#172B4D]">No pipelines yet</p>
|
||||
<p className="text-xs text-[#5E6C84] mt-1 max-w-xs">
|
||||
<p className="text-sm font-medium text-[var(--c-text)]">No pipelines yet</p>
|
||||
<p className="text-xs text-[var(--c-muted)] mt-1 max-w-xs">
|
||||
Pipelines run automatically when you push to a repository.<br />
|
||||
Add a <code className="font-mono bg-[#F4F5F7] px-1 rounded">.forgebucket.yml</code> file to get started.
|
||||
Add a <code className="font-mono bg-[var(--c-surface-muted)] px-1 rounded">.forgebucket.yml</code> file to get started.
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -16,65 +16,65 @@ export default function ProfilePage() {
|
||||
|
||||
return (
|
||||
<div className="max-w-2xl mx-auto px-4 md:px-6 py-6 space-y-8">
|
||||
<h1 className="text-xl font-semibold text-[#172B4D]">Profile</h1>
|
||||
<h1 className="text-xl font-semibold text-[var(--c-text)]">Profile</h1>
|
||||
|
||||
{/* Avatar */}
|
||||
<div className="flex items-center gap-5">
|
||||
<div className="w-16 h-16 rounded-full bg-[#0052CC] flex items-center justify-center text-white text-2xl font-bold">
|
||||
<div className="w-16 h-16 rounded-full bg-[var(--c-brand)] flex items-center justify-center text-white text-2xl font-bold">
|
||||
{user?.username?.[0]?.toUpperCase()}
|
||||
</div>
|
||||
<div>
|
||||
<p className="text-sm font-semibold text-[#172B4D]">{user?.username}</p>
|
||||
<p className="text-xs text-[#5E6C84]">{user?.email}</p>
|
||||
<p className="text-sm font-semibold text-[var(--c-text)]">{user?.username}</p>
|
||||
<p className="text-xs text-[var(--c-muted)]">{user?.email}</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Edit form */}
|
||||
<section className="border border-[#DFE1E6] rounded-lg overflow-hidden">
|
||||
<div className="px-5 py-4 border-b border-[#DFE1E6] bg-[#FAFBFC]">
|
||||
<h2 className="text-sm font-semibold text-[#172B4D]">Edit profile</h2>
|
||||
<section className="border border-[var(--c-border)] rounded-lg overflow-hidden">
|
||||
<div className="px-5 py-4 border-b border-[var(--c-border)] bg-[var(--c-surface-raised)]">
|
||||
<h2 className="text-sm font-semibold text-[var(--c-text)]">Edit profile</h2>
|
||||
</div>
|
||||
<form onSubmit={handleSave} className="px-5 py-5 flex flex-col gap-5">
|
||||
<div>
|
||||
<label className="block text-xs font-semibold text-[#172B4D] mb-1">Display name</label>
|
||||
<label className="block text-xs font-semibold text-[var(--c-text)] mb-1">Display name</label>
|
||||
<input
|
||||
value={displayName}
|
||||
onChange={e => setDisplayName(e.target.value)}
|
||||
className="w-full border border-[#DFE1E6] rounded px-3 py-2 text-sm focus:outline-none focus:border-[#4C9AFF] focus:ring-1 focus:ring-[#4C9AFF]"
|
||||
className="w-full border border-[var(--c-border)] rounded px-3 py-2 text-sm focus:outline-none focus:border-[var(--c-brand-focus)] focus:ring-1 focus:ring-[var(--c-brand-focus)]"
|
||||
/>
|
||||
<p className="text-xs text-[#5E6C84] mt-1">This is your public display name.</p>
|
||||
<p className="text-xs text-[var(--c-muted)] mt-1">This is your public display name.</p>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<label className="block text-xs font-semibold text-[#172B4D] mb-1">Bio</label>
|
||||
<label className="block text-xs font-semibold text-[var(--c-text)] mb-1">Bio</label>
|
||||
<textarea
|
||||
value={bio}
|
||||
onChange={e => setBio(e.target.value)}
|
||||
rows={3}
|
||||
placeholder="Tell others a little about yourself"
|
||||
className="w-full border border-[#DFE1E6] rounded px-3 py-2 text-sm resize-none focus:outline-none focus:border-[#4C9AFF] focus:ring-1 focus:ring-[#4C9AFF]"
|
||||
className="w-full border border-[var(--c-border)] rounded px-3 py-2 text-sm resize-none focus:outline-none focus:border-[var(--c-brand-focus)] focus:ring-1 focus:ring-[var(--c-brand-focus)]"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<label className="block text-xs font-semibold text-[#172B4D] mb-1">Email</label>
|
||||
<label className="block text-xs font-semibold text-[var(--c-text)] mb-1">Email</label>
|
||||
<input
|
||||
value={user?.email ?? ''}
|
||||
disabled
|
||||
className="w-full border border-[#DFE1E6] rounded px-3 py-2 text-sm bg-[#F4F5F7] text-[#5E6C84] cursor-not-allowed"
|
||||
className="w-full border border-[var(--c-border)] rounded px-3 py-2 text-sm bg-[var(--c-surface-muted)] text-[var(--c-muted)] cursor-not-allowed"
|
||||
/>
|
||||
<p className="text-xs text-[#5E6C84] mt-1">Email changes are managed in Settings → Account.</p>
|
||||
<p className="text-xs text-[var(--c-muted)] mt-1">Email changes are managed in Settings → Account.</p>
|
||||
</div>
|
||||
|
||||
<div className="flex items-center gap-3">
|
||||
<button
|
||||
type="submit"
|
||||
className="px-4 py-2 rounded bg-[#0052CC] text-white text-sm font-medium hover:bg-[#0065FF] min-h-[44px]"
|
||||
className="px-4 py-2 rounded bg-[var(--c-brand)] text-white text-sm font-medium hover:bg-[var(--c-brand-hover)] min-h-[44px]"
|
||||
>
|
||||
Save changes
|
||||
</button>
|
||||
{saved && (
|
||||
<span className="text-xs text-[#00875A] font-medium">Saved!</span>
|
||||
<span className="text-xs text-[var(--c-success)] font-medium">Saved!</span>
|
||||
)}
|
||||
</div>
|
||||
</form>
|
||||
|
||||
@@ -37,41 +37,41 @@ export default function RegisterPage() {
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="min-h-screen bg-[#F4F5F7] flex items-center justify-center p-4">
|
||||
<div className="min-h-screen bg-[var(--c-surface-muted)] flex items-center justify-center p-4">
|
||||
<div className="w-full max-w-sm">
|
||||
<div className="text-center mb-8">
|
||||
<h1 className="text-2xl font-bold text-[#172B4D]">ForgeBucket</h1>
|
||||
<p className="text-sm text-[#5E6C84] mt-1">Create your account</p>
|
||||
<h1 className="text-2xl font-bold text-[var(--c-text)]">ForgeBucket</h1>
|
||||
<p className="text-sm text-[var(--c-muted)] mt-1">Create your account</p>
|
||||
</div>
|
||||
|
||||
<div className="bg-white border border-[#DFE1E6] rounded-lg p-6 shadow-sm">
|
||||
<div className="bg-[var(--c-surface)] border border-[var(--c-border)] rounded-lg p-6 shadow-sm">
|
||||
<form onSubmit={handleSubmit} className="flex flex-col gap-4">
|
||||
<div>
|
||||
<label className="block text-xs font-semibold text-[#172B4D] mb-1">Username</label>
|
||||
<label className="block text-xs font-semibold text-[var(--c-text)] mb-1">Username</label>
|
||||
<input value={username} onChange={e => setUsername(e.target.value)} required autoFocus
|
||||
className="w-full border border-[#DFE1E6] rounded px-3 py-2 text-sm focus:outline-none focus:border-[#4C9AFF] focus:ring-1 focus:ring-[#4C9AFF]" />
|
||||
className="w-full border border-[var(--c-border)] rounded px-3 py-2 text-sm focus:outline-none focus:border-[var(--c-brand-focus)] focus:ring-1 focus:ring-[var(--c-brand-focus)]" />
|
||||
</div>
|
||||
<div>
|
||||
<label className="block text-xs font-semibold text-[#172B4D] mb-1">Email</label>
|
||||
<label className="block text-xs font-semibold text-[var(--c-text)] mb-1">Email</label>
|
||||
<input type="email" value={email} onChange={e => setEmail(e.target.value)} required
|
||||
className="w-full border border-[#DFE1E6] rounded px-3 py-2 text-sm focus:outline-none focus:border-[#4C9AFF] focus:ring-1 focus:ring-[#4C9AFF]" />
|
||||
className="w-full border border-[var(--c-border)] rounded px-3 py-2 text-sm focus:outline-none focus:border-[var(--c-brand-focus)] focus:ring-1 focus:ring-[var(--c-brand-focus)]" />
|
||||
</div>
|
||||
<div>
|
||||
<label className="block text-xs font-semibold text-[#172B4D] mb-1">Password</label>
|
||||
<label className="block text-xs font-semibold text-[var(--c-text)] mb-1">Password</label>
|
||||
<input type="password" value={password} onChange={e => setPassword(e.target.value)} required
|
||||
className="w-full border border-[#DFE1E6] rounded px-3 py-2 text-sm focus:outline-none focus:border-[#4C9AFF] focus:ring-1 focus:ring-[#4C9AFF]" />
|
||||
className="w-full border border-[var(--c-border)] rounded px-3 py-2 text-sm focus:outline-none focus:border-[var(--c-brand-focus)] focus:ring-1 focus:ring-[var(--c-brand-focus)]" />
|
||||
</div>
|
||||
{error && <p className="text-xs text-[#DE350B] bg-[#FFEBE6] rounded px-3 py-2">{error}</p>}
|
||||
{error && <p className="text-xs text-[var(--c-danger)] bg-[var(--c-danger-tint)] rounded px-3 py-2">{error}</p>}
|
||||
<button type="submit" disabled={loading}
|
||||
className="w-full py-2.5 rounded bg-[#0052CC] text-white text-sm font-semibold hover:bg-[#0065FF] disabled:opacity-50 min-h-[44px]">
|
||||
className="w-full py-2.5 rounded bg-[var(--c-brand)] text-white text-sm font-semibold hover:bg-[var(--c-brand-hover)] disabled:opacity-50 min-h-[44px]">
|
||||
{loading ? 'Creating account…' : 'Create account'}
|
||||
</button>
|
||||
</form>
|
||||
</div>
|
||||
|
||||
<p className="text-center text-xs text-[#5E6C84] mt-4">
|
||||
<p className="text-center text-xs text-[var(--c-muted)] mt-4">
|
||||
Already have an account?{' '}
|
||||
<Link to="/login" className="text-[#0052CC] hover:underline font-medium">Sign in</Link>
|
||||
<Link to="/login" className="text-[var(--c-brand)] hover:underline font-medium">Sign in</Link>
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -27,46 +27,46 @@ export default function RepoIssuesPage() {
|
||||
return (
|
||||
<div className="max-w-4xl mx-auto px-4 md:px-6 py-6">
|
||||
<div className="flex items-center gap-1 text-sm mb-4">
|
||||
<Link to={`/repos/${owner}/${repo}`} className="text-[#0052CC] hover:underline">{repo}</Link>
|
||||
<span className="text-[#5E6C84]">/</span>
|
||||
<span className="font-semibold text-[#172B4D]">Issues</span>
|
||||
<Link to={`/repos/${owner}/${repo}`} className="text-[var(--c-brand)] hover:underline">{repo}</Link>
|
||||
<span className="text-[var(--c-muted)]">/</span>
|
||||
<span className="font-semibold text-[var(--c-text)]">Issues</span>
|
||||
</div>
|
||||
|
||||
<div className="flex items-center justify-between mb-4">
|
||||
<h1 className="text-xl font-semibold text-[#172B4D]">Issues</h1>
|
||||
<h1 className="text-xl font-semibold text-[var(--c-text)]">Issues</h1>
|
||||
<button onClick={() => setShowNew(true)}
|
||||
className="px-3 py-2 rounded bg-[#0052CC] text-white text-sm font-medium hover:bg-[#0065FF] min-h-[44px]">
|
||||
className="px-3 py-2 rounded bg-[var(--c-brand)] text-white text-sm font-medium hover:bg-[var(--c-brand-hover)] min-h-[44px]">
|
||||
New issue
|
||||
</button>
|
||||
</div>
|
||||
|
||||
{showNew && (
|
||||
<form onSubmit={handleCreate} className="mb-6 p-5 border border-[#4C9AFF] rounded bg-white space-y-3">
|
||||
<h2 className="text-sm font-semibold text-[#172B4D]">New Issue</h2>
|
||||
<form onSubmit={handleCreate} className="mb-6 p-5 border border-[var(--c-brand-focus)] rounded bg-[var(--c-surface)] space-y-3">
|
||||
<h2 className="text-sm font-semibold text-[var(--c-text)]">New Issue</h2>
|
||||
<input value={title} onChange={e => setTitle(e.target.value)} placeholder="Title" required
|
||||
className="w-full border border-[#DFE1E6] rounded px-3 py-2 text-sm focus:outline-none focus:border-[#4C9AFF]" />
|
||||
className="w-full border border-[var(--c-border)] rounded px-3 py-2 text-sm focus:outline-none focus:border-[var(--c-brand-focus)]" />
|
||||
<textarea value={body} onChange={e => setBody(e.target.value)} placeholder="Description (optional)" rows={4}
|
||||
className="w-full border border-[#DFE1E6] rounded px-3 py-2 text-sm resize-none focus:outline-none focus:border-[#4C9AFF]" />
|
||||
className="w-full border border-[var(--c-border)] rounded px-3 py-2 text-sm resize-none focus:outline-none focus:border-[var(--c-brand-focus)]" />
|
||||
<div className="flex gap-2">
|
||||
<button type="submit" disabled={createIssue.isPending || !title.trim()}
|
||||
className="px-4 py-2 rounded bg-[#0052CC] text-white text-sm hover:bg-[#0065FF] disabled:opacity-50 min-h-[44px]">
|
||||
className="px-4 py-2 rounded bg-[var(--c-brand)] text-white text-sm hover:bg-[var(--c-brand-hover)] disabled:opacity-50 min-h-[44px]">
|
||||
{createIssue.isPending ? 'Submitting…' : 'Submit'}
|
||||
</button>
|
||||
<button type="button" onClick={() => setShowNew(false)}
|
||||
className="px-4 py-2 rounded border border-[#DFE1E6] text-sm text-[#172B4D] hover:bg-[#F4F5F7] min-h-[44px]">
|
||||
className="px-4 py-2 rounded border border-[var(--c-border)] text-sm text-[var(--c-text)] hover:bg-[var(--c-surface-muted)] min-h-[44px]">
|
||||
Cancel
|
||||
</button>
|
||||
</div>
|
||||
</form>
|
||||
)}
|
||||
|
||||
<div className="flex gap-1 mb-4 border-b border-[#DFE1E6]">
|
||||
<div className="flex gap-1 mb-4 border-b border-[var(--c-border)]">
|
||||
{(['open', 'closed'] as IssueState[]).map(s => {
|
||||
const count = issues?.filter(i => i.state === s).length ?? 0
|
||||
return (
|
||||
<button key={s} onClick={() => setState(s)}
|
||||
className={cn('px-4 py-2 text-sm font-medium capitalize border-b-2 -mb-px min-h-[44px]',
|
||||
state === s ? 'border-[#0052CC] text-[#0052CC]' : 'border-transparent text-[#5E6C84] hover:text-[#172B4D]')}>
|
||||
state === s ? 'border-[var(--c-brand)] text-[var(--c-brand)]' : 'border-transparent text-[var(--c-muted)] hover:text-[var(--c-text)]')}>
|
||||
{s} {count > 0 && `(${count})`}
|
||||
</button>
|
||||
)
|
||||
@@ -74,21 +74,21 @@ export default function RepoIssuesPage() {
|
||||
</div>
|
||||
|
||||
{isLoading ? (
|
||||
<p className="text-sm text-[#5E6C84] py-4">Loading…</p>
|
||||
<p className="text-sm text-[var(--c-muted)] py-4">Loading…</p>
|
||||
) : !issues?.length ? (
|
||||
<p className="text-sm text-[#5E6C84] py-8 text-center">No {state} issues.</p>
|
||||
<p className="text-sm text-[var(--c-muted)] py-8 text-center">No {state} issues.</p>
|
||||
) : (
|
||||
<div className="flex flex-col gap-2">
|
||||
{issues.map(issue => (
|
||||
<div key={issue.id}
|
||||
className="flex items-start gap-3 p-4 border border-[#DFE1E6] rounded hover:bg-[#FAFBFC]">
|
||||
<svg width="16" height="16" viewBox="0 0 16 16" fill={issue.state === 'open' ? '#00875A' : '#5E6C84'}
|
||||
className="flex items-start gap-3 p-4 border border-[var(--c-border)] rounded hover:bg-[var(--c-surface-raised)]">
|
||||
<svg width="16" height="16" viewBox="0 0 16 16" fill={issue.state === 'open' ? 'var(--c-success)' : 'var(--c-muted)'}
|
||||
className="mt-0.5 shrink-0">
|
||||
<path d="M8 1.5a6.5 6.5 0 1 0 0 13 6.5 6.5 0 0 0 0-13ZM0 8a8 8 0 1 1 16 0A8 8 0 0 1 0 8Zm9 3a1 1 0 1 1-2 0 1 1 0 0 1 2 0Zm-.25-6.25a.75.75 0 0 0-1.5 0v3.5a.75.75 0 0 0 1.5 0v-3.5Z"/>
|
||||
</svg>
|
||||
<div className="flex-1 min-w-0">
|
||||
<p className="text-sm font-medium text-[#172B4D]">#{issue.number} {issue.title}</p>
|
||||
<p className="text-xs text-[#5E6C84] mt-0.5">
|
||||
<p className="text-sm font-medium text-[var(--c-text)]">#{issue.number} {issue.title}</p>
|
||||
<p className="text-xs text-[var(--c-muted)] mt-0.5">
|
||||
opened by {issue.authorName} · {new Date(issue.createdAt).toLocaleDateString()}
|
||||
</p>
|
||||
</div>
|
||||
@@ -97,7 +97,7 @@ export default function RepoIssuesPage() {
|
||||
? closeIssue.mutate(issue.number)
|
||||
: reopenIssue.mutate(issue.number)
|
||||
}
|
||||
className="text-xs px-3 py-1.5 rounded border border-[#DFE1E6] text-[#5E6C84] hover:bg-[#F4F5F7] shrink-0 min-h-[32px]">
|
||||
className="text-xs px-3 py-1.5 rounded border border-[var(--c-border)] text-[var(--c-muted)] hover:bg-[var(--c-surface-muted)] shrink-0 min-h-[32px]">
|
||||
{issue.state === 'open' ? 'Close' : 'Reopen'}
|
||||
</button>
|
||||
</div>
|
||||
|
||||
@@ -15,31 +15,31 @@ export default function RepoPRsPage() {
|
||||
return (
|
||||
<div className="max-w-4xl mx-auto px-4 md:px-6 py-6">
|
||||
<div className="flex items-center gap-1 text-sm mb-4">
|
||||
<Link to="/repos" className="text-[#0052CC] hover:underline">Repos</Link>
|
||||
<span className="text-[#5E6C84]">/</span>
|
||||
<Link to={`/repos/${owner}/${repo}`} className="text-[#0052CC] hover:underline">{repo}</Link>
|
||||
<span className="text-[#5E6C84]">/</span>
|
||||
<span className="font-semibold text-[#172B4D]">Pull requests</span>
|
||||
<Link to="/repos" className="text-[var(--c-brand)] hover:underline">Repos</Link>
|
||||
<span className="text-[var(--c-muted)]">/</span>
|
||||
<Link to={`/repos/${owner}/${repo}`} className="text-[var(--c-brand)] hover:underline">{repo}</Link>
|
||||
<span className="text-[var(--c-muted)]">/</span>
|
||||
<span className="font-semibold text-[var(--c-text)]">Pull requests</span>
|
||||
</div>
|
||||
|
||||
<div className="flex items-center justify-between mb-4">
|
||||
<h1 className="text-xl font-semibold text-[#172B4D]">Pull Requests</h1>
|
||||
<h1 className="text-xl font-semibold text-[var(--c-text)]">Pull Requests</h1>
|
||||
<Link
|
||||
to={`/repos/${owner}/${repo}/pulls/new`}
|
||||
className="px-3 py-2 rounded bg-[#0052CC] text-white text-sm font-medium hover:bg-[#0065FF] min-h-[44px] flex items-center"
|
||||
className="px-3 py-2 rounded bg-[var(--c-brand)] text-white text-sm font-medium hover:bg-[var(--c-brand-hover)] min-h-[44px] flex items-center"
|
||||
>
|
||||
New PR
|
||||
</Link>
|
||||
</div>
|
||||
|
||||
<div className="flex gap-1 mb-4 border-b border-[#DFE1E6]">
|
||||
<div className="flex gap-1 mb-4 border-b border-[var(--c-border)]">
|
||||
{(['open', 'merged', 'closed'] as PRStatus[]).map(s => {
|
||||
const count = prs?.filter(p => p.status === s).length ?? 0
|
||||
return (
|
||||
<button key={s} onClick={() => setStatus(s)}
|
||||
className={cn(
|
||||
'px-4 py-2 text-sm font-medium capitalize border-b-2 -mb-px min-h-[44px]',
|
||||
status === s ? 'border-[#0052CC] text-[#0052CC]' : 'border-transparent text-[#5E6C84] hover:text-[#172B4D]',
|
||||
status === s ? 'border-[var(--c-brand)] text-[var(--c-brand)]' : 'border-transparent text-[var(--c-muted)] hover:text-[var(--c-text)]',
|
||||
)}>
|
||||
{s} {count > 0 && <span className="ml-1 text-xs">({count})</span>}
|
||||
</button>
|
||||
@@ -48,9 +48,9 @@ export default function RepoPRsPage() {
|
||||
</div>
|
||||
|
||||
{isLoading ? <PRListSkeleton /> : isError ? (
|
||||
<p className="text-sm text-[#DE350B]">Failed to load pull requests.</p>
|
||||
<p className="text-sm text-[var(--c-danger)]">Failed to load pull requests.</p>
|
||||
) : !filtered.length ? (
|
||||
<p className="text-sm text-[#5E6C84] py-8 text-center">No {status} pull requests.</p>
|
||||
<p className="text-sm text-[var(--c-muted)] py-8 text-center">No {status} pull requests.</p>
|
||||
) : (
|
||||
<div className="flex flex-col gap-2">
|
||||
{filtered.map(pr => <PRRow key={pr.id} pr={pr} owner={owner} repo={repo} />)}
|
||||
@@ -65,19 +65,19 @@ function PRRow({ pr, owner, repo }: { pr: PullRequest; owner: string; repo: stri
|
||||
? 'bg-[#E3FCEF] text-[#006644] border-[#79F2C0]'
|
||||
: pr.status === 'merged'
|
||||
? 'bg-[#EAE6FF] text-[#403294] border-[#C0B6F2]'
|
||||
: 'bg-[#F4F5F7] text-[#5E6C84] border-[#DFE1E6]'
|
||||
: 'bg-[var(--c-surface-muted)] text-[var(--c-muted)] border-[var(--c-border)]'
|
||||
|
||||
return (
|
||||
<Link
|
||||
to={`/repos/${owner}/${repo}/pulls/${pr.id}`}
|
||||
className="flex items-start gap-3 p-4 border border-[#DFE1E6] rounded hover:border-[#4C9AFF] hover:bg-[#FAFBFC] transition-colors"
|
||||
className="flex items-start gap-3 p-4 border border-[var(--c-border)] rounded hover:border-[var(--c-brand-focus)] hover:bg-[var(--c-surface-raised)] transition-colors"
|
||||
>
|
||||
<span className={cn('text-[10px] font-semibold px-2 py-0.5 rounded-full border shrink-0 mt-0.5', statusColor)}>
|
||||
{pr.status}
|
||||
</span>
|
||||
<div className="flex-1 min-w-0">
|
||||
<p className="text-sm font-medium text-[#172B4D] truncate">{pr.title}</p>
|
||||
<p className="text-xs text-[#5E6C84] mt-0.5">
|
||||
<p className="text-sm font-medium text-[var(--c-text)] truncate">{pr.title}</p>
|
||||
<p className="text-xs text-[var(--c-muted)] mt-0.5">
|
||||
#{pr.id} · {pr.sourceBranch} → {pr.targetBranch}
|
||||
</p>
|
||||
</div>
|
||||
|
||||
@@ -35,7 +35,7 @@ export default function RepoPage() {
|
||||
}, [])
|
||||
|
||||
if (isLoading) return <div className="p-6"><RepoListSkeleton /></div>
|
||||
if (isError || !repo) return <div className="p-6 text-sm text-[#DE350B]">Repository not found.</div>
|
||||
if (isError || !repo) return <div className="p-6 text-sm text-[var(--c-danger)]">Repository not found.</div>
|
||||
|
||||
const branch = ref || repo.defaultBranch
|
||||
const cloneUrl = `${window.location.origin}/${owner}/${repoName}.git`
|
||||
@@ -51,26 +51,26 @@ export default function RepoPage() {
|
||||
{/* Header row */}
|
||||
<div className="flex items-start justify-between gap-4 flex-wrap">
|
||||
<div>
|
||||
<div className="flex items-center gap-2 text-sm text-[#5E6C84] mb-1">
|
||||
<Link to="/repos" className="hover:text-[#0052CC]">Repositories</Link>
|
||||
<div className="flex items-center gap-2 text-sm text-[var(--c-muted)] mb-1">
|
||||
<Link to="/repos" className="hover:text-[var(--c-brand)]">Repositories</Link>
|
||||
<span>/</span>
|
||||
<RepoAvatar ownerName={owner} name={repo.name} avatarUrl={repo.avatarUrl} size={20} />
|
||||
<span className="font-semibold text-[#172B4D]">{repo.name}</span>
|
||||
<span className="font-semibold text-[var(--c-text)]">{repo.name}</span>
|
||||
{repo.isPrivate && (
|
||||
<span className="text-[10px] font-medium px-1.5 py-0.5 rounded-full border border-[#DFE1E6] text-[#5E6C84]">
|
||||
<span className="text-[10px] font-medium px-1.5 py-0.5 rounded-full border border-[var(--c-border)] text-[var(--c-muted)]">
|
||||
Private
|
||||
</span>
|
||||
)}
|
||||
</div>
|
||||
{repo.description && (
|
||||
<p className="text-sm text-[#5E6C84]">{repo.description}</p>
|
||||
<p className="text-sm text-[var(--c-muted)]">{repo.description}</p>
|
||||
)}
|
||||
</div>
|
||||
|
||||
<div className="flex items-center gap-2 shrink-0">
|
||||
<Link
|
||||
to={`/repos/${owner}/${repoName}/pulls`}
|
||||
className="px-3 py-1.5 border border-[#DFE1E6] rounded text-sm text-[#172B4D] hover:bg-[#F4F5F7] font-medium"
|
||||
className="px-3 py-1.5 border border-[var(--c-border)] rounded text-sm text-[var(--c-text)] hover:bg-[var(--c-surface-muted)] font-medium"
|
||||
>
|
||||
Pull requests
|
||||
</Link>
|
||||
@@ -79,7 +79,7 @@ export default function RepoPage() {
|
||||
<div className="relative" ref={cloneRef}>
|
||||
<button
|
||||
onClick={() => setShowClone(s => !s)}
|
||||
className="flex items-center gap-1.5 px-3 py-1.5 rounded bg-[#0052CC] hover:bg-[#0065FF] text-white text-sm font-medium"
|
||||
className="flex items-center gap-1.5 px-3 py-1.5 rounded bg-[var(--c-brand)] hover:bg-[var(--c-brand-hover)] text-white text-sm font-medium"
|
||||
>
|
||||
<svg width="14" height="14" fill="none" stroke="currentColor" strokeWidth="2" viewBox="0 0 24 24">
|
||||
<path strokeLinecap="round" strokeLinejoin="round" d="M15.75 17.25v3.375c0 .621-.504 1.125-1.125 1.125h-9.75a1.125 1.125 0 0 1-1.125-1.125V7.875c0-.621.504-1.125 1.125-1.125H6.75a9.06 9.06 0 0 1 1.5.124m7.5 10.376h3.375c.621 0 1.125-.504 1.125-1.125V11.25c0-4.46-3.243-8.161-7.5-8.876a9.06 9.06 0 0 0-1.5-.124H9.375c-.621 0-1.125.504-1.125 1.125v3.5m7.5 10.375H9.375a1.125 1.125 0 0 1-1.125-1.125v-9.25m12 6.625v-1.875a3.375 3.375 0 0 0-3.375-3.375h-1.5a1.125 1.125 0 0 1-1.125-1.125v-1.5a3.375 3.375 0 0 0-3.375-3.375H9.75" />
|
||||
@@ -90,13 +90,13 @@ export default function RepoPage() {
|
||||
</svg>
|
||||
</button>
|
||||
{showClone && (
|
||||
<div className="absolute right-0 top-full mt-1 w-80 bg-white border border-[#DFE1E6] rounded-lg shadow-xl z-50 p-4">
|
||||
<p className="text-xs font-semibold text-[#5E6C84] uppercase tracking-wide mb-2">Clone over HTTP</p>
|
||||
<div className="flex items-center gap-2 bg-[#F4F5F7] border border-[#DFE1E6] rounded px-3 py-2">
|
||||
<code className="text-xs text-[#172B4D] flex-1 truncate">{cloneUrl}</code>
|
||||
<div className="absolute right-0 top-full mt-1 w-80 bg-[var(--c-surface)] border border-[var(--c-border)] rounded-lg shadow-xl z-50 p-4">
|
||||
<p className="text-xs font-semibold text-[var(--c-muted)] uppercase tracking-wide mb-2">Clone over HTTP</p>
|
||||
<div className="flex items-center gap-2 bg-[var(--c-surface-muted)] border border-[var(--c-border)] rounded px-3 py-2">
|
||||
<code className="text-xs text-[var(--c-text)] flex-1 truncate">{cloneUrl}</code>
|
||||
<button
|
||||
onClick={() => navigator.clipboard.writeText(cloneUrl)}
|
||||
className="text-[10px] text-[#0052CC] hover:underline shrink-0"
|
||||
className="text-[10px] text-[var(--c-brand)] hover:underline shrink-0"
|
||||
>
|
||||
Copy
|
||||
</button>
|
||||
@@ -116,7 +116,7 @@ export default function RepoPage() {
|
||||
<div className="relative" ref={branchRef}>
|
||||
<button
|
||||
onClick={() => setShowBranches(s => !s)}
|
||||
className="flex items-center gap-1.5 px-3 py-1.5 border border-[#DFE1E6] rounded text-sm text-[#172B4D] hover:bg-[#F4F5F7] font-medium bg-white"
|
||||
className="flex items-center gap-1.5 px-3 py-1.5 border border-[var(--c-border)] rounded text-sm text-[var(--c-text)] hover:bg-[var(--c-surface-muted)] font-medium bg-[var(--c-surface)]"
|
||||
>
|
||||
<svg width="14" height="14" fill="none" stroke="currentColor" strokeWidth="1.5" viewBox="0 0 24 24">
|
||||
<path strokeLinecap="round" strokeLinejoin="round" d="M9.568 3H5.25A2.25 2.25 0 0 0 3 5.25v4.318c0 .597.237 1.17.659 1.591l9.581 9.581c.699.699 1.78.872 2.607.33a18.095 18.095 0 0 0 5.223-5.223c.542-.827.369-1.908-.33-2.607L11.16 3.66A2.25 2.25 0 0 0 9.568 3Z" />
|
||||
@@ -127,28 +127,28 @@ export default function RepoPage() {
|
||||
</svg>
|
||||
</button>
|
||||
{showBranches && (
|
||||
<div className="absolute left-0 top-full mt-1 w-56 bg-white border border-[#DFE1E6] rounded-lg shadow-xl z-50 overflow-hidden">
|
||||
<div className="px-3 py-2 border-b border-[#DFE1E6] bg-[#F4F5F7]">
|
||||
<p className="text-xs font-semibold text-[#5E6C84]">Switch branch</p>
|
||||
<div className="absolute left-0 top-full mt-1 w-56 bg-[var(--c-surface)] border border-[var(--c-border)] rounded-lg shadow-xl z-50 overflow-hidden">
|
||||
<div className="px-3 py-2 border-b border-[var(--c-border)] bg-[var(--c-surface-muted)]">
|
||||
<p className="text-xs font-semibold text-[var(--c-muted)]">Switch branch</p>
|
||||
</div>
|
||||
<ul>
|
||||
{branches?.map(b => (
|
||||
<li key={b.name}>
|
||||
<button
|
||||
onClick={() => switchBranch(b.name)}
|
||||
className="w-full text-left px-3 py-2 text-sm hover:bg-[#F4F5F7] flex items-center gap-2"
|
||||
className="w-full text-left px-3 py-2 text-sm hover:bg-[var(--c-surface-muted)] flex items-center gap-2"
|
||||
>
|
||||
{b.name === branch && (
|
||||
<svg width="12" height="12" fill="none" stroke="#0052CC" strokeWidth="2.5" viewBox="0 0 24 24">
|
||||
<svg width="12" height="12" fill="none" stroke="var(--c-brand)" strokeWidth="2.5" viewBox="0 0 24 24">
|
||||
<path strokeLinecap="round" strokeLinejoin="round" d="m4.5 12.75 6 6 9-13.5" />
|
||||
</svg>
|
||||
)}
|
||||
<span className={b.name === branch ? 'text-[#0052CC] font-medium' : 'text-[#172B4D] ml-5'}>{b.name}</span>
|
||||
<span className={b.name === branch ? 'text-[var(--c-brand)] font-medium' : 'text-[var(--c-text)] ml-5'}>{b.name}</span>
|
||||
</button>
|
||||
</li>
|
||||
))}
|
||||
{!branches?.length && (
|
||||
<li className="px-3 py-2 text-xs text-[#5E6C84]">No branches found</li>
|
||||
<li className="px-3 py-2 text-xs text-[var(--c-muted)]">No branches found</li>
|
||||
)}
|
||||
</ul>
|
||||
</div>
|
||||
@@ -156,10 +156,10 @@ export default function RepoPage() {
|
||||
</div>
|
||||
|
||||
{/* Nav links */}
|
||||
<Link to={`/repos/${owner}/${repoName}/commits`} className="text-sm text-[#5E6C84] hover:text-[#172B4D] px-2 py-1">Commits</Link>
|
||||
<Link to={`/repos/${owner}/${repoName}/branches`} className="text-sm text-[#5E6C84] hover:text-[#172B4D] px-2 py-1">Branches</Link>
|
||||
<Link to={`/repos/${owner}/${repoName}/issues`} className="text-sm text-[#5E6C84] hover:text-[#172B4D] px-2 py-1">Issues</Link>
|
||||
<Link to={`/repos/${owner}/${repoName}/settings`} className="text-sm text-[#5E6C84] hover:text-[#172B4D] px-2 py-1 ml-auto">Settings</Link>
|
||||
<Link to={`/repos/${owner}/${repoName}/commits`} className="text-sm text-[var(--c-muted)] hover:text-[var(--c-text)] px-2 py-1">Commits</Link>
|
||||
<Link to={`/repos/${owner}/${repoName}/branches`} className="text-sm text-[var(--c-muted)] hover:text-[var(--c-text)] px-2 py-1">Branches</Link>
|
||||
<Link to={`/repos/${owner}/${repoName}/issues`} className="text-sm text-[var(--c-muted)] hover:text-[var(--c-text)] px-2 py-1">Issues</Link>
|
||||
<Link to={`/repos/${owner}/${repoName}/settings`} className="text-sm text-[var(--c-muted)] hover:text-[var(--c-text)] px-2 py-1 ml-auto">Settings</Link>
|
||||
</div>
|
||||
|
||||
<TreeBrowser owner={owner} repo={repoName} ref={branch} path={path} />
|
||||
@@ -180,17 +180,17 @@ function ReadmePreview({ owner, repo, ref }: { owner: string; repo: string; ref:
|
||||
if (!readmeEntry || !blob) return null
|
||||
|
||||
return (
|
||||
<div className="border border-[#DFE1E6] rounded bg-white overflow-hidden">
|
||||
<div className="px-4 py-2.5 border-b border-[#DFE1E6] bg-[#FAFBFC] flex items-center gap-2">
|
||||
<svg width="14" height="14" fill="none" stroke="#5E6C84" strokeWidth="1.5" viewBox="0 0 24 24">
|
||||
<div className="border border-[var(--c-border)] rounded bg-[var(--c-surface)] overflow-hidden">
|
||||
<div className="px-4 py-2.5 border-b border-[var(--c-border)] bg-[var(--c-surface-raised)] flex items-center gap-2">
|
||||
<svg width="14" height="14" fill="none" stroke="var(--c-muted)" strokeWidth="1.5" viewBox="0 0 24 24">
|
||||
<path strokeLinecap="round" strokeLinejoin="round" d="M19.5 14.25v-2.625a3.375 3.375 0 0 0-3.375-3.375h-1.5A1.125 1.125 0 0 1 13.5 7.125v-1.5a3.375 3.375 0 0 0-3.375-3.375H8.25m0 12.75h7.5m-7.5 3H12M10.5 2.25H5.625c-.621 0-1.125.504-1.125 1.125v17.25c0 .621.504 1.125 1.125 1.125h12.75c.621 0 1.125-.504 1.125-1.125V11.25a9 9 0 0 0-9-9Z" />
|
||||
</svg>
|
||||
<span className="text-sm font-semibold text-[#172B4D]">{readmeEntry.name}</span>
|
||||
<span className="text-sm font-semibold text-[var(--c-text)]">{readmeEntry.name}</span>
|
||||
</div>
|
||||
<div className="px-6 py-5 prose prose-sm max-w-none text-[#172B4D]
|
||||
prose-headings:text-[#172B4D] prose-headings:font-semibold prose-headings:border-b prose-headings:border-[#DFE1E6] prose-headings:pb-1
|
||||
prose-a:text-[#0052CC] prose-code:bg-[#F4F5F7] prose-code:px-1 prose-code:rounded prose-code:text-sm
|
||||
prose-pre:bg-[#F4F5F7] prose-pre:border prose-pre:border-[#DFE1E6] prose-pre:rounded">
|
||||
<div className="px-6 py-5 prose prose-sm max-w-none text-[var(--c-text)]
|
||||
prose-headings:text-[var(--c-text)] prose-headings:font-semibold prose-headings:border-b prose-headings:border-[var(--c-border)] prose-headings:pb-1
|
||||
prose-a:text-[var(--c-brand)] prose-code:bg-[var(--c-surface-muted)] prose-code:px-1 prose-code:rounded prose-code:text-sm
|
||||
prose-pre:bg-[var(--c-surface-muted)] prose-pre:border prose-pre:border-[var(--c-border)] prose-pre:rounded">
|
||||
<ReactMarkdown remarkPlugins={[remarkGfm]}>{blob.content}</ReactMarkdown>
|
||||
</div>
|
||||
</div>
|
||||
@@ -201,22 +201,22 @@ function GettingStarted({ repoName, branch, cloneUrl }: {
|
||||
repoName: string; branch: string; cloneUrl: string
|
||||
}) {
|
||||
return (
|
||||
<div className="border border-[#DFE1E6] rounded-lg overflow-hidden">
|
||||
<div className="px-5 py-4 bg-[#FAFBFC] border-b border-[#DFE1E6]">
|
||||
<h2 className="text-sm font-semibold text-[#172B4D]">Getting started</h2>
|
||||
<p className="text-xs text-[#5E6C84] mt-0.5">Push your first commit to get started.</p>
|
||||
<div className="border border-[var(--c-border)] rounded-lg overflow-hidden">
|
||||
<div className="px-5 py-4 bg-[var(--c-surface-raised)] border-b border-[var(--c-border)]">
|
||||
<h2 className="text-sm font-semibold text-[var(--c-text)]">Getting started</h2>
|
||||
<p className="text-xs text-[var(--c-muted)] mt-0.5">Push your first commit to get started.</p>
|
||||
</div>
|
||||
<div className="px-5 py-5 space-y-6 text-sm">
|
||||
<div>
|
||||
<p className="text-xs font-semibold text-[#5E6C84] uppercase tracking-wide mb-2">Clone over HTTP</p>
|
||||
<p className="text-xs font-semibold text-[var(--c-muted)] uppercase tracking-wide mb-2">Clone over HTTP</p>
|
||||
<CopyBlock value={cloneUrl} />
|
||||
</div>
|
||||
<div>
|
||||
<p className="text-xs font-semibold text-[#5E6C84] uppercase tracking-wide mb-2">…or push an existing repository</p>
|
||||
<p className="text-xs font-semibold text-[var(--c-muted)] uppercase tracking-wide mb-2">…or push an existing repository</p>
|
||||
<CopyBlock value={`git remote add origin ${cloneUrl}\ngit branch -M ${branch}\ngit push -u origin ${branch}`} multiline />
|
||||
</div>
|
||||
<div>
|
||||
<p className="text-xs font-semibold text-[#5E6C84] uppercase tracking-wide mb-2">…or create a new repository on the command line</p>
|
||||
<p className="text-xs font-semibold text-[var(--c-muted)] uppercase tracking-wide mb-2">…or create a new repository on the command line</p>
|
||||
<CopyBlock value={`echo "# ${repoName}" >> README.md\ngit init\ngit add README.md\ngit commit -m "first commit"\ngit branch -M ${branch}\ngit remote add origin ${cloneUrl}\ngit push -u origin ${branch}`} multiline />
|
||||
</div>
|
||||
</div>
|
||||
@@ -228,12 +228,12 @@ function CopyBlock({ value, multiline }: { value: string; multiline?: boolean })
|
||||
const copy = () => navigator.clipboard.writeText(value).catch(() => {})
|
||||
return (
|
||||
<div className="relative group">
|
||||
<pre className={`font-mono text-xs bg-[#F4F5F7] border border-[#DFE1E6] rounded px-4 py-3 overflow-x-auto text-[#172B4D] ${multiline ? 'whitespace-pre' : 'whitespace-nowrap'}`}>
|
||||
<pre className={`font-mono text-xs bg-[var(--c-surface-muted)] border border-[var(--c-border)] rounded px-4 py-3 overflow-x-auto text-[var(--c-text)] ${multiline ? 'whitespace-pre' : 'whitespace-nowrap'}`}>
|
||||
{value}
|
||||
</pre>
|
||||
<button
|
||||
onClick={copy}
|
||||
className="absolute top-2 right-2 px-2 py-1 rounded text-[10px] font-medium bg-white border border-[#DFE1E6] text-[#5E6C84] hover:text-[#172B4D] opacity-0 group-hover:opacity-100 transition-opacity"
|
||||
className="absolute top-2 right-2 px-2 py-1 rounded text-[10px] font-medium bg-[var(--c-surface)] border border-[var(--c-border)] text-[var(--c-muted)] hover:text-[var(--c-text)] opacity-0 group-hover:opacity-100 transition-opacity"
|
||||
>
|
||||
Copy
|
||||
</button>
|
||||
|
||||
@@ -91,28 +91,28 @@ export default function RepoSettingsPage() {
|
||||
return (
|
||||
<div className="flex h-full overflow-hidden">
|
||||
{/* Sidebar */}
|
||||
<aside className="w-60 shrink-0 border-r border-[#DFE1E6] bg-white hidden md:flex flex-col">
|
||||
<div className="p-3 border-b border-[#DFE1E6] space-y-2.5 shrink-0">
|
||||
<Link to={`/repos/${owner}/${repoName}`} className="flex items-center gap-1.5 text-sm text-[#172B4D] hover:text-[#0052CC] font-medium">
|
||||
<aside className="w-60 shrink-0 border-r border-[var(--c-border)] bg-[var(--c-surface)] hidden md:flex flex-col">
|
||||
<div className="p-3 border-b border-[var(--c-border)] space-y-2.5 shrink-0">
|
||||
<Link to={`/repos/${owner}/${repoName}`} className="flex items-center gap-1.5 text-sm text-[var(--c-text)] hover:text-[var(--c-brand)] font-medium">
|
||||
<svg width="14" height="14" fill="none" stroke="currentColor" strokeWidth="2" viewBox="0 0 24 24">
|
||||
<path strokeLinecap="round" strokeLinejoin="round" d="M10.5 19.5 3 12m0 0 7.5-7.5M3 12h18" />
|
||||
</svg>
|
||||
Repository settings
|
||||
</Link>
|
||||
<div className="relative">
|
||||
<svg className="absolute left-2.5 top-1/2 -translate-y-1/2 text-[#5E6C84] pointer-events-none" width="12" height="12" fill="none" stroke="currentColor" strokeWidth="2" viewBox="0 0 24 24">
|
||||
<svg className="absolute left-2.5 top-1/2 -translate-y-1/2 text-[var(--c-muted)] pointer-events-none" width="12" height="12" fill="none" stroke="currentColor" strokeWidth="2" viewBox="0 0 24 24">
|
||||
<path strokeLinecap="round" strokeLinejoin="round" d="m21 21-5.197-5.197m0 0A7.5 7.5 0 1 0 5.196 5.196a7.5 7.5 0 0 0 10.607 10.607Z" />
|
||||
</svg>
|
||||
<input value={sidebarSearch} onChange={e => setSidebarSearch(e.target.value)} placeholder="Jump to settings…" className="w-full text-xs bg-[#F4F5F7] border border-[#DFE1E6] rounded pl-7 pr-3 py-1.5 focus:outline-none focus:border-[#4C9AFF]" />
|
||||
<input value={sidebarSearch} onChange={e => setSidebarSearch(e.target.value)} placeholder="Jump to settings…" className="w-full text-xs bg-[var(--c-surface-muted)] border border-[var(--c-border)] rounded pl-7 pr-3 py-1.5 focus:outline-none focus:border-[var(--c-brand-focus)]" />
|
||||
</div>
|
||||
</div>
|
||||
<nav className="flex-1 overflow-y-auto py-2">
|
||||
{filtered.map(group => (
|
||||
<div key={group.group} className="mb-1">
|
||||
<p className="text-[10px] font-semibold text-[#5E6C84] uppercase tracking-wider px-4 py-1.5">{group.group}</p>
|
||||
<p className="text-[10px] font-semibold text-[var(--c-muted)] uppercase tracking-wider px-4 py-1.5">{group.group}</p>
|
||||
{group.items.map(item => (
|
||||
<button key={item.id} onClick={() => setSearchParams({ section: item.id })}
|
||||
className={`w-full text-left px-4 py-2 text-sm transition-colors border-l-[3px] ${section === item.id ? 'bg-[#DEEBFF] text-[#0052CC] font-medium border-[#0052CC]' : 'text-[#172B4D] hover:bg-[#F4F5F7] border-transparent'}`}>
|
||||
className={`w-full text-left px-4 py-2 text-sm transition-colors border-l-[3px] ${section === item.id ? 'bg-[var(--c-brand-tint)] text-[var(--c-brand)] font-medium border-[var(--c-brand)]' : 'text-[var(--c-text)] hover:bg-[var(--c-surface-muted)] border-transparent'}`}>
|
||||
{item.label}
|
||||
</button>
|
||||
))}
|
||||
@@ -123,8 +123,8 @@ export default function RepoSettingsPage() {
|
||||
|
||||
{/* Content */}
|
||||
<main className="flex-1 overflow-y-auto min-w-0">
|
||||
<div className="md:hidden p-3 border-b border-[#DFE1E6] bg-white">
|
||||
<select value={section} onChange={e => setSearchParams({ section: e.target.value })} className="w-full border border-[#DFE1E6] rounded px-3 py-2 text-sm focus:outline-none">
|
||||
<div className="md:hidden p-3 border-b border-[var(--c-border)] bg-[var(--c-surface)]">
|
||||
<select value={section} onChange={e => setSearchParams({ section: e.target.value })} className="w-full border border-[var(--c-border)] rounded px-3 py-2 text-sm focus:outline-none">
|
||||
{SIDEBAR.map(g => g.items.map(i => <option key={i.id} value={i.id}>{g.group} → {i.label}</option>))}
|
||||
</select>
|
||||
</div>
|
||||
@@ -260,19 +260,19 @@ function RepositoryDetailsSection({ owner, repo }: { owner: string; repo: string
|
||||
{/* Page header */}
|
||||
<div className="flex items-start justify-between gap-4 flex-wrap">
|
||||
<div>
|
||||
<div className="flex items-center gap-1 text-xs text-[#5E6C84] mb-1.5">
|
||||
<Link to={`/repos/${owner}/${repo}`} className="hover:text-[#0052CC]">{owner}</Link>
|
||||
<div className="flex items-center gap-1 text-xs text-[var(--c-muted)] mb-1.5">
|
||||
<Link to={`/repos/${owner}/${repo}`} className="hover:text-[var(--c-brand)]">{owner}</Link>
|
||||
<span>/</span>
|
||||
<Link to={`/repos/${owner}/${repo}`} className="hover:text-[#0052CC]">{repo}</Link>
|
||||
<Link to={`/repos/${owner}/${repo}`} className="hover:text-[var(--c-brand)]">{repo}</Link>
|
||||
</div>
|
||||
<h1 className="text-xl font-semibold text-[#172B4D]">Repository details</h1>
|
||||
<h1 className="text-xl font-semibold text-[var(--c-text)]">Repository details</h1>
|
||||
</div>
|
||||
|
||||
{/* Manage repository dropdown */}
|
||||
<div className="relative shrink-0" ref={manageRef}>
|
||||
<button
|
||||
onClick={() => setShowManage(s => !s)}
|
||||
className="flex items-center gap-1.5 px-3 py-1.5 border border-[#DFE1E6] rounded text-sm text-[#172B4D] hover:bg-[#F4F5F7] font-medium"
|
||||
className="flex items-center gap-1.5 px-3 py-1.5 border border-[var(--c-border)] rounded text-sm text-[var(--c-text)] hover:bg-[var(--c-surface-muted)] font-medium"
|
||||
>
|
||||
Manage repository
|
||||
<svg width="12" height="12" fill="none" stroke="currentColor" strokeWidth="2.5" viewBox="0 0 24 24">
|
||||
@@ -280,10 +280,10 @@ function RepositoryDetailsSection({ owner, repo }: { owner: string; repo: string
|
||||
</svg>
|
||||
</button>
|
||||
{showManage && (
|
||||
<div className="absolute right-0 top-full mt-1 w-52 bg-white border border-[#DFE1E6] rounded-lg shadow-xl z-20 overflow-hidden">
|
||||
<div className="absolute right-0 top-full mt-1 w-52 bg-[var(--c-surface)] border border-[var(--c-border)] rounded-lg shadow-xl z-20 overflow-hidden">
|
||||
<button
|
||||
onClick={() => { setShowManage(false); setShowAdvanced(true); setTimeout(() => document.getElementById('danger-zone')?.scrollIntoView({ behavior: 'smooth' }), 50) }}
|
||||
className="w-full flex items-center gap-2 px-4 py-3 text-sm text-[#DE350B] hover:bg-[#FFEBE6] text-left"
|
||||
className="w-full flex items-center gap-2 px-4 py-3 text-sm text-[var(--c-danger)] hover:bg-[var(--c-danger-tint)] text-left"
|
||||
>
|
||||
<svg width="14" height="14" fill="none" stroke="currentColor" strokeWidth="1.5" viewBox="0 0 24 24">
|
||||
<path strokeLinecap="round" strokeLinejoin="round" d="m14.74 9-.346 9m-4.788 0L9.26 9m9.968-3.21c.342.052.682.107 1.022.166m-1.022-.165L18.16 19.673a2.25 2.25 0 0 1-2.244 2.077H8.084a2.25 2.25 0 0 1-2.244-2.077L4.772 5.79m14.456 0a48.108 48.108 0 0 0-3.478-.397m-12 .562c.34-.059.68-.114 1.022-.165m0 0a48.11 48.11 0 0 1 3.478-.397m7.5 0v-.916c0-1.18-.91-2.164-2.09-2.201a51.964 51.964 0 0 0-3.32 0c-1.18.037-2.09 1.022-2.09 2.201v.916m7.5 0a48.667 48.667 0 0 0-7.5 0" />
|
||||
@@ -295,19 +295,19 @@ function RepositoryDetailsSection({ owner, repo }: { owner: string; repo: string
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="border-t border-[#DFE1E6]" />
|
||||
<div className="border-t border-[var(--c-border)]" />
|
||||
|
||||
<form onSubmit={handleSave} className="space-y-6">
|
||||
|
||||
{/* Avatar */}
|
||||
<div>
|
||||
<label className="block text-sm font-medium text-[#172B4D] mb-3">Avatar</label>
|
||||
<label className="block text-sm font-medium text-[var(--c-text)] mb-3">Avatar</label>
|
||||
<div className="flex items-center gap-4">
|
||||
{/* Clickable avatar */}
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => fileInputRef.current?.click()}
|
||||
className="rounded-lg overflow-hidden ring-2 ring-[#DFE1E6] hover:ring-[#4C9AFF] transition-all focus:outline-none"
|
||||
className="rounded-lg overflow-hidden ring-2 ring-[var(--c-border)] hover:ring-[var(--c-brand-focus)] transition-all focus:outline-none"
|
||||
title="Click to change avatar"
|
||||
>
|
||||
{avatarPreview ? (
|
||||
@@ -326,16 +326,16 @@ function RepositoryDetailsSection({ owner, repo }: { owner: string; repo: string
|
||||
type="button"
|
||||
onClick={() => fileInputRef.current?.click()}
|
||||
disabled={uploadAvatar.isPending}
|
||||
className="px-3 py-1.5 text-sm border border-[#DFE1E6] rounded text-[#172B4D] hover:bg-[#F4F5F7] disabled:opacity-50"
|
||||
className="px-3 py-1.5 text-sm border border-[var(--c-border)] rounded text-[var(--c-text)] hover:bg-[var(--c-surface-muted)] disabled:opacity-50"
|
||||
>
|
||||
{uploadAvatar.isPending ? 'Uploading…' : 'Change avatar'}
|
||||
</button>
|
||||
<p className="text-xs text-[#5E6C84] mt-1">JPEG, PNG, GIF or WebP · max 5 MB</p>
|
||||
<p className="text-xs text-[var(--c-muted)] mt-1">JPEG, PNG, GIF or WebP · max 5 MB</p>
|
||||
{uploadAvatar.isError && (
|
||||
<p className="text-xs text-[#DE350B] mt-1">{(uploadAvatar.error as Error).message}</p>
|
||||
<p className="text-xs text-[var(--c-danger)] mt-1">{(uploadAvatar.error as Error).message}</p>
|
||||
)}
|
||||
{uploadAvatar.isSuccess && !avatarPreview && (
|
||||
<p className="text-xs text-[#00875A] mt-1">Avatar updated.</p>
|
||||
<p className="text-xs text-[var(--c-success)] mt-1">Avatar updated.</p>
|
||||
)}
|
||||
</div>
|
||||
<input
|
||||
@@ -350,58 +350,58 @@ function RepositoryDetailsSection({ owner, repo }: { owner: string; repo: string
|
||||
|
||||
{/* Repository name */}
|
||||
<div>
|
||||
<label className="block text-sm font-medium text-[#172B4D] mb-1">
|
||||
Repository name <span className="text-[#DE350B]">*</span>
|
||||
<label className="block text-sm font-medium text-[var(--c-text)] mb-1">
|
||||
Repository name <span className="text-[var(--c-danger)]">*</span>
|
||||
</label>
|
||||
<input
|
||||
value={name}
|
||||
onChange={e => handleNameChange(e.target.value)}
|
||||
className={`w-full border rounded px-3 py-2 text-sm focus:outline-none focus:ring-1 ${nameError ? 'border-[#DE350B] focus:border-[#DE350B] focus:ring-[#DE350B]' : 'border-[#DFE1E6] focus:border-[#4C9AFF] focus:ring-[#4C9AFF]'}`}
|
||||
className={`w-full border rounded px-3 py-2 text-sm focus:outline-none focus:ring-1 ${nameError ? 'border-[var(--c-danger)] focus:border-[var(--c-danger)] focus:ring-[var(--c-danger)]' : 'border-[var(--c-border)] focus:border-[var(--c-brand-focus)] focus:ring-[var(--c-brand-focus)]'}`}
|
||||
/>
|
||||
{nameError
|
||||
? <p className="text-xs text-[#DE350B] mt-1">{nameError}</p>
|
||||
? <p className="text-xs text-[var(--c-danger)] mt-1">{nameError}</p>
|
||||
: name !== repoData.name
|
||||
? <p className="text-xs text-[#FF8B00] mt-1 flex items-center gap-1">
|
||||
? <p className="text-xs text-[var(--c-warning)] mt-1 flex items-center gap-1">
|
||||
<svg width="12" height="12" fill="none" stroke="currentColor" strokeWidth="2" viewBox="0 0 24 24"><path strokeLinecap="round" strokeLinejoin="round" d="M12 9v3.75m-9.303 3.376c-.866 1.5.217 3.374 1.948 3.374h14.71c1.73 0 2.813-1.874 1.948-3.374L13.949 3.378c-.866-1.5-3.032-1.5-3.898 0L2.697 16.126ZM12 15.75h.007v.008H12v-.008Z" /></svg>
|
||||
Renaming will change the clone URL — all existing git remotes will need to be updated.
|
||||
</p>
|
||||
: <p className="text-xs text-[#5E6C84] mt-1">Letters, numbers, hyphens, underscores, and dots only.</p>
|
||||
: <p className="text-xs text-[var(--c-muted)] mt-1">Letters, numbers, hyphens, underscores, and dots only.</p>
|
||||
}
|
||||
</div>
|
||||
|
||||
{/* Size (read-only) */}
|
||||
<div>
|
||||
<label className="block text-sm font-medium text-[#172B4D] mb-1">Size</label>
|
||||
<p className="text-sm text-[#172B4D]">{formatSize(repoData.size)}</p>
|
||||
<label className="block text-sm font-medium text-[var(--c-text)] mb-1">Size</label>
|
||||
<p className="text-sm text-[var(--c-text)]">{formatSize(repoData.size)}</p>
|
||||
</div>
|
||||
|
||||
{/* Description */}
|
||||
<div>
|
||||
<label className="block text-sm font-medium text-[#172B4D] mb-1">Description</label>
|
||||
<label className="block text-sm font-medium text-[var(--c-text)] mb-1">Description</label>
|
||||
<textarea
|
||||
value={description}
|
||||
onChange={e => setDescription(e.target.value)}
|
||||
rows={4}
|
||||
placeholder="Describe this repository…"
|
||||
className="w-full border border-[#DFE1E6] rounded px-3 py-2 text-sm focus:outline-none focus:border-[#4C9AFF] focus:ring-1 focus:ring-[#4C9AFF] resize-y"
|
||||
className="w-full border border-[var(--c-border)] rounded px-3 py-2 text-sm focus:outline-none focus:border-[var(--c-brand-focus)] focus:ring-1 focus:ring-[var(--c-brand-focus)] resize-y"
|
||||
/>
|
||||
</div>
|
||||
|
||||
{/* Access level */}
|
||||
<div>
|
||||
<label className="block text-sm font-medium text-[#172B4D] mb-2">Access level</label>
|
||||
<label className="block text-sm font-medium text-[var(--c-text)] mb-2">Access level</label>
|
||||
<label className="flex items-start gap-2.5 cursor-pointer">
|
||||
<input
|
||||
type="checkbox"
|
||||
checked={isPrivate}
|
||||
onChange={e => setIsPrivate(e.target.checked)}
|
||||
className="mt-0.5 w-4 h-4 accent-[#0052CC]"
|
||||
className="mt-0.5 w-4 h-4 accent-[var(--c-brand)]"
|
||||
/>
|
||||
<div>
|
||||
<span className="text-sm font-medium text-[#172B4D]">
|
||||
<span className="text-sm font-medium text-[var(--c-text)]">
|
||||
{isPrivate ? 'This is a private repository' : 'This is a public repository'}
|
||||
</span>
|
||||
<p className="text-xs text-[#5E6C84] mt-0.5">
|
||||
<p className="text-xs text-[var(--c-muted)] mt-0.5">
|
||||
{isPrivate
|
||||
? 'Only collaborators you invite can see and push to this repository.'
|
||||
: 'Anyone can view this repository. Make it private to restrict access.'}
|
||||
@@ -411,11 +411,11 @@ function RepositoryDetailsSection({ owner, repo }: { owner: string; repo: string
|
||||
</div>
|
||||
|
||||
{/* Advanced (collapsible) */}
|
||||
<div className="border border-[#DFE1E6] rounded-lg overflow-hidden">
|
||||
<div className="border border-[var(--c-border)] rounded-lg overflow-hidden">
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => setShowAdvanced(s => !s)}
|
||||
className="w-full flex items-center gap-2 px-4 py-3 text-sm font-medium text-[#172B4D] hover:bg-[#F4F5F7] text-left"
|
||||
className="w-full flex items-center gap-2 px-4 py-3 text-sm font-medium text-[var(--c-text)] hover:bg-[var(--c-surface-muted)] text-left"
|
||||
>
|
||||
<svg width="14" height="14" fill="none" stroke="currentColor" strokeWidth="2.5" viewBox="0 0 24 24"
|
||||
className={`transition-transform shrink-0 ${showAdvanced ? 'rotate-90' : ''}`}>
|
||||
@@ -425,47 +425,47 @@ function RepositoryDetailsSection({ owner, repo }: { owner: string; repo: string
|
||||
</button>
|
||||
|
||||
{showAdvanced && (
|
||||
<div className="border-t border-[#DFE1E6] px-4 py-5 space-y-6 bg-[#FAFBFC]">
|
||||
<div className="border-t border-[var(--c-border)] px-4 py-5 space-y-6 bg-[var(--c-surface-raised)]">
|
||||
{/* Default branch */}
|
||||
<div>
|
||||
<label className="block text-sm font-medium text-[#172B4D] mb-1">Default branch</label>
|
||||
<label className="block text-sm font-medium text-[var(--c-text)] mb-1">Default branch</label>
|
||||
<input
|
||||
value={defaultBranch}
|
||||
onChange={e => setDefaultBranch(e.target.value)}
|
||||
placeholder="main"
|
||||
className="w-full max-w-xs bg-white border border-[#DFE1E6] rounded px-3 py-2 text-sm focus:outline-none focus:border-[#4C9AFF]"
|
||||
className="w-full max-w-xs bg-[var(--c-surface)] border border-[var(--c-border)] rounded px-3 py-2 text-sm focus:outline-none focus:border-[var(--c-brand-focus)]"
|
||||
/>
|
||||
<p className="text-xs text-[#5E6C84] mt-1">The default branch used as the base for new pull requests.</p>
|
||||
<p className="text-xs text-[var(--c-muted)] mt-1">The default branch used as the base for new pull requests.</p>
|
||||
</div>
|
||||
|
||||
{/* Danger zone */}
|
||||
<div id="danger-zone" className="border border-[#FFEBE6] rounded-lg overflow-hidden">
|
||||
<div className="px-4 py-3 bg-[#FFEBE6]/60 border-b border-[#FFEBE6]">
|
||||
<h3 className="text-sm font-semibold text-[#BF2600]">Delete repository</h3>
|
||||
<div id="danger-zone" className="border border-[var(--c-danger-tint)] rounded-lg overflow-hidden">
|
||||
<div className="px-4 py-3 bg-[var(--c-danger-tint)]/60 border-b border-[var(--c-danger-tint)]">
|
||||
<h3 className="text-sm font-semibold text-[var(--c-danger-dark)]">Delete repository</h3>
|
||||
</div>
|
||||
<div className="px-4 py-4 space-y-3 bg-white">
|
||||
<p className="text-sm text-[#172B4D]">
|
||||
<div className="px-4 py-4 space-y-3 bg-[var(--c-surface)]">
|
||||
<p className="text-sm text-[var(--c-text)]">
|
||||
This is permanent and cannot be undone. All commits, branches, pull requests, issues, and settings will be permanently deleted.
|
||||
</p>
|
||||
<p className="text-xs text-[#5E6C84]">
|
||||
Type <code className="font-mono bg-[#F4F5F7] border border-[#DFE1E6] px-1.5 py-0.5 rounded text-[#172B4D]">{repo}</code> to confirm.
|
||||
<p className="text-xs text-[var(--c-muted)]">
|
||||
Type <code className="font-mono bg-[var(--c-surface-muted)] border border-[var(--c-border)] px-1.5 py-0.5 rounded text-[var(--c-text)]">{repo}</code> to confirm.
|
||||
</p>
|
||||
<input
|
||||
value={confirmDelete}
|
||||
onChange={e => setConfirmDelete(e.target.value)}
|
||||
placeholder={repo}
|
||||
className="w-full border border-[#DFE1E6] rounded px-3 py-2 text-sm focus:outline-none focus:border-[#DE350B]"
|
||||
className="w-full border border-[var(--c-border)] rounded px-3 py-2 text-sm focus:outline-none focus:border-[var(--c-danger)]"
|
||||
/>
|
||||
<button
|
||||
type="button"
|
||||
onClick={handleDelete}
|
||||
disabled={confirmDelete !== repo || deleteRepo.isPending}
|
||||
className="px-4 py-2 rounded bg-[#DE350B] text-white text-sm font-medium hover:bg-[#BF2600] disabled:opacity-40"
|
||||
className="px-4 py-2 rounded bg-[var(--c-danger)] text-white text-sm font-medium hover:bg-[var(--c-danger-dark)] disabled:opacity-40"
|
||||
>
|
||||
{deleteRepo.isPending ? 'Deleting…' : 'Delete repository'}
|
||||
</button>
|
||||
{deleteRepo.isError && (
|
||||
<p className="text-xs text-[#DE350B]">{(deleteRepo.error as Error).message}</p>
|
||||
<p className="text-xs text-[var(--c-danger)]">{(deleteRepo.error as Error).message}</p>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
@@ -474,12 +474,12 @@ function RepositoryDetailsSection({ owner, repo }: { owner: string; repo: string
|
||||
</div>
|
||||
|
||||
{/* Footer: error / saved / Discard / Save */}
|
||||
<div className="flex items-center justify-end gap-3 pt-2 border-t border-[#DFE1E6]">
|
||||
<div className="flex items-center justify-end gap-3 pt-2 border-t border-[var(--c-border)]">
|
||||
{updateRepo.isError && (
|
||||
<p className="text-xs text-[#DE350B] mr-auto">{(updateRepo.error as Error).message}</p>
|
||||
<p className="text-xs text-[var(--c-danger)] mr-auto">{(updateRepo.error as Error).message}</p>
|
||||
)}
|
||||
{saved && !updateRepo.isError && (
|
||||
<span className="text-xs text-[#00875A] font-medium mr-auto flex items-center gap-1">
|
||||
<span className="text-xs text-[var(--c-success)] font-medium mr-auto flex items-center gap-1">
|
||||
<svg width="12" height="12" fill="none" stroke="currentColor" strokeWidth="2.5" viewBox="0 0 24 24">
|
||||
<path strokeLinecap="round" strokeLinejoin="round" d="m4.5 12.75 6 6 9-13.5" />
|
||||
</svg>
|
||||
@@ -487,11 +487,11 @@ function RepositoryDetailsSection({ owner, repo }: { owner: string; repo: string
|
||||
</span>
|
||||
)}
|
||||
<button type="button" onClick={handleDiscard} disabled={!isDirty}
|
||||
className="px-4 py-2 rounded border border-[#DFE1E6] text-sm text-[#172B4D] hover:bg-[#F4F5F7] disabled:opacity-40 min-h-[36px]">
|
||||
className="px-4 py-2 rounded border border-[var(--c-border)] text-sm text-[var(--c-text)] hover:bg-[var(--c-surface-muted)] disabled:opacity-40 min-h-[36px]">
|
||||
Discard
|
||||
</button>
|
||||
<button type="submit" disabled={updateRepo.isPending || !isDirty || !!nameError}
|
||||
className="px-4 py-2 rounded bg-[#0052CC] text-white text-sm font-medium hover:bg-[#0065FF] disabled:opacity-50 min-h-[36px]">
|
||||
className="px-4 py-2 rounded bg-[var(--c-brand)] text-white text-sm font-medium hover:bg-[var(--c-brand-hover)] disabled:opacity-50 min-h-[36px]">
|
||||
{updateRepo.isPending ? 'Saving…' : 'Save changes'}
|
||||
</button>
|
||||
</div>
|
||||
@@ -506,14 +506,14 @@ function ComingSoon({ sectionId }: { sectionId: SectionId }) {
|
||||
const meta = SECTION_META[sectionId]
|
||||
return (
|
||||
<div className="max-w-2xl px-6 py-6">
|
||||
<h1 className="text-xl font-semibold text-[#172B4D] mb-1">{meta.title}</h1>
|
||||
<div className="mt-8 flex flex-col items-center justify-center py-16 border border-dashed border-[#DFE1E6] rounded-lg text-center bg-[#FAFBFC]">
|
||||
<svg width="40" height="40" fill="none" stroke="#97A0AF" strokeWidth="1.2" viewBox="0 0 24 24" className="mb-4">
|
||||
<h1 className="text-xl font-semibold text-[var(--c-text)] mb-1">{meta.title}</h1>
|
||||
<div className="mt-8 flex flex-col items-center justify-center py-16 border border-dashed border-[var(--c-border)] rounded-lg text-center bg-[var(--c-surface-raised)]">
|
||||
<svg width="40" height="40" fill="none" stroke="var(--c-subtle)" strokeWidth="1.2" viewBox="0 0 24 24" className="mb-4">
|
||||
<path strokeLinecap="round" strokeLinejoin="round" d="M10.5 6h9.75M10.5 6a1.5 1.5 0 1 1-3 0m3 0a1.5 1.5 0 1 0-3 0M3.75 6H7.5m3 12h9.75m-9.75 0a1.5 1.5 0 0 1-3 0m3 0a1.5 1.5 0 0 0-3 0m-3.75 0H7.5m9-6h3.75m-3.75 0a1.5 1.5 0 0 1-3 0m3 0a1.5 1.5 0 0 0-3 0m-9.75 0h9.75" />
|
||||
</svg>
|
||||
<h2 className="text-base font-semibold text-[#172B4D] mb-2">{meta.title}</h2>
|
||||
<p className="text-sm text-[#5E6C84] max-w-sm leading-relaxed">{meta.description}</p>
|
||||
<span className="mt-5 text-[10px] font-semibold uppercase tracking-wider text-white bg-[#97A0AF] px-2.5 py-1 rounded-full">Coming soon</span>
|
||||
<h2 className="text-base font-semibold text-[var(--c-text)] mb-2">{meta.title}</h2>
|
||||
<p className="text-sm text-[var(--c-muted)] max-w-sm leading-relaxed">{meta.description}</p>
|
||||
<span className="mt-5 text-[10px] font-semibold uppercase tracking-wider text-white bg-[var(--c-subtle)] px-2.5 py-1 rounded-full">Coming soon</span>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
|
||||
@@ -10,21 +10,21 @@ export default function ReposPage() {
|
||||
<div className="max-w-4xl mx-auto px-4 md:px-6 py-6">
|
||||
<div className="flex items-center justify-between mb-6">
|
||||
<div>
|
||||
<h1 className="text-xl font-semibold text-[#172B4D]">Repositories</h1>
|
||||
<h1 className="text-xl font-semibold text-[var(--c-text)]">Repositories</h1>
|
||||
{repos && (
|
||||
<p className="text-sm text-[#5E6C84] mt-0.5">{repos.length} repositor{repos.length === 1 ? 'y' : 'ies'}</p>
|
||||
<p className="text-sm text-[var(--c-muted)] mt-0.5">{repos.length} repositor{repos.length === 1 ? 'y' : 'ies'}</p>
|
||||
)}
|
||||
</div>
|
||||
<div className="flex items-center gap-2">
|
||||
<Link
|
||||
to="/repos/import"
|
||||
className="flex items-center gap-1.5 px-3 py-2 rounded border border-[#DFE1E6] text-sm text-[#172B4D] hover:bg-[#F4F5F7] font-medium min-h-[36px]"
|
||||
className="flex items-center gap-1.5 px-3 py-2 rounded border border-[var(--c-border)] text-sm text-[var(--c-text)] hover:bg-[var(--c-surface-muted)] font-medium min-h-[36px]"
|
||||
>
|
||||
Import
|
||||
</Link>
|
||||
<Link
|
||||
to="/repos/new"
|
||||
className="flex items-center gap-2 px-4 py-2 rounded bg-[#0052CC] text-white text-sm font-medium hover:bg-[#0065FF] min-h-[36px]"
|
||||
className="flex items-center gap-2 px-4 py-2 rounded bg-[var(--c-brand)] text-white text-sm font-medium hover:bg-[var(--c-brand-hover)] min-h-[36px]"
|
||||
>
|
||||
<svg width="14" height="14" fill="none" stroke="currentColor" strokeWidth="2.5" viewBox="0 0 24 24">
|
||||
<path strokeLinecap="round" strokeLinejoin="round" d="M12 4.5v15m7.5-7.5h-15" />
|
||||
@@ -40,12 +40,12 @@ export default function ReposPage() {
|
||||
<NotSignedIn />
|
||||
) : !repos?.length ? (
|
||||
<div className="py-16 text-center">
|
||||
<svg width="48" height="48" fill="none" stroke="#97A0AF" strokeWidth="1" viewBox="0 0 24 24" className="mx-auto mb-4">
|
||||
<svg width="48" height="48" fill="none" stroke="var(--c-subtle)" strokeWidth="1" viewBox="0 0 24 24" className="mx-auto mb-4">
|
||||
<path strokeLinecap="round" strokeLinejoin="round" d="M3.75 9.776c.112-.017.227-.026.344-.026h15.812c.117 0 .232.009.344.026m-16.5 0a2.25 2.25 0 0 0-1.883 2.542l.857 6a2.25 2.25 0 0 0 2.227 1.932H19.05a2.25 2.25 0 0 0 2.227-1.932l.857-6a2.25 2.25 0 0 0-1.883-2.542m-16.5 0V6A2.25 2.25 0 0 1 6 3.75h3.879a1.5 1.5 0 0 1 1.06.44l2.122 2.12a1.5 1.5 0 0 0 1.06.44H18A2.25 2.25 0 0 1 20.25 9v.776" />
|
||||
</svg>
|
||||
<p className="text-sm font-medium text-[#172B4D] mb-1">No repositories yet</p>
|
||||
<p className="text-xs text-[#5E6C84] mb-4">Create your first repository to get started.</p>
|
||||
<Link to="/repos/new" className="px-4 py-2 rounded bg-[#0052CC] text-white text-sm font-medium hover:bg-[#0065FF]">
|
||||
<p className="text-sm font-medium text-[var(--c-text)] mb-1">No repositories yet</p>
|
||||
<p className="text-xs text-[var(--c-muted)] mb-4">Create your first repository to get started.</p>
|
||||
<Link to="/repos/new" className="px-4 py-2 rounded bg-[var(--c-brand)] text-white text-sm font-medium hover:bg-[var(--c-brand-hover)]">
|
||||
Create repository
|
||||
</Link>
|
||||
</div>
|
||||
@@ -60,15 +60,15 @@ export default function ReposPage() {
|
||||
|
||||
function NotSignedIn() {
|
||||
return (
|
||||
<div className="flex flex-col items-center justify-center py-12 border border-dashed border-[#DFE1E6] rounded text-center gap-3">
|
||||
<svg width="36" height="36" fill="none" stroke="#97A0AF" strokeWidth="1.5" viewBox="0 0 24 24">
|
||||
<div className="flex flex-col items-center justify-center py-12 border border-dashed border-[var(--c-border)] rounded text-center gap-3">
|
||||
<svg width="36" height="36" fill="none" stroke="var(--c-subtle)" strokeWidth="1.5" viewBox="0 0 24 24">
|
||||
<path strokeLinecap="round" strokeLinejoin="round" d="M15.75 6a3.75 3.75 0 1 1-7.5 0 3.75 3.75 0 0 1 7.5 0ZM4.501 20.118a7.5 7.5 0 0 1 14.998 0A17.933 17.933 0 0 1 12 21.75c-2.676 0-5.216-.584-7.499-1.632Z" />
|
||||
</svg>
|
||||
<div>
|
||||
<p className="text-sm font-medium text-[#172B4D]">Sign in to see your repositories</p>
|
||||
<p className="text-xs text-[#5E6C84] mt-1">You need to be signed in to view and create repositories.</p>
|
||||
<p className="text-sm font-medium text-[var(--c-text)]">Sign in to see your repositories</p>
|
||||
<p className="text-xs text-[var(--c-muted)] mt-1">You need to be signed in to view and create repositories.</p>
|
||||
</div>
|
||||
<Link to="/login" className="px-4 py-2 rounded bg-[#0052CC] text-white text-sm font-medium hover:bg-[#0065FF] min-h-[44px] flex items-center">
|
||||
<Link to="/login" className="px-4 py-2 rounded bg-[var(--c-brand)] text-white text-sm font-medium hover:bg-[var(--c-brand-hover)] min-h-[44px] flex items-center">
|
||||
Sign in
|
||||
</Link>
|
||||
</div>
|
||||
|
||||
@@ -10,13 +10,13 @@ export default function SettingsPage() {
|
||||
|
||||
return (
|
||||
<div className="max-w-2xl mx-auto px-4 md:px-6 py-6 space-y-8">
|
||||
<h1 className="text-xl font-semibold text-[#172B4D]">Settings</h1>
|
||||
<h1 className="text-xl font-semibold text-[var(--c-text)]">Settings</h1>
|
||||
|
||||
{/* Account info */}
|
||||
<section className="border border-[#DFE1E6] rounded-lg overflow-hidden">
|
||||
<div className="px-5 py-4 border-b border-[#DFE1E6] bg-[#FAFBFC] flex items-center justify-between">
|
||||
<h2 className="text-sm font-semibold text-[#172B4D]">Account</h2>
|
||||
<Link to="/profile" className="text-xs text-[#0052CC] hover:underline">Edit profile →</Link>
|
||||
<section className="border border-[var(--c-border)] rounded-lg overflow-hidden">
|
||||
<div className="px-5 py-4 border-b border-[var(--c-border)] bg-[var(--c-surface-raised)] flex items-center justify-between">
|
||||
<h2 className="text-sm font-semibold text-[var(--c-text)]">Account</h2>
|
||||
<Link to="/profile" className="text-xs text-[var(--c-brand)] hover:underline">Edit profile →</Link>
|
||||
</div>
|
||||
<div className="px-5 py-4 flex flex-col gap-3">
|
||||
<Row label="Username" value={user?.username} />
|
||||
@@ -26,32 +26,32 @@ export default function SettingsPage() {
|
||||
</section>
|
||||
|
||||
{/* Change password */}
|
||||
<section className="border border-[#DFE1E6] rounded-lg overflow-hidden">
|
||||
<div className="px-5 py-4 border-b border-[#DFE1E6] bg-[#FAFBFC]">
|
||||
<h2 className="text-sm font-semibold text-[#172B4D]">Change password</h2>
|
||||
<section className="border border-[var(--c-border)] rounded-lg overflow-hidden">
|
||||
<div className="px-5 py-4 border-b border-[var(--c-border)] bg-[var(--c-surface-raised)]">
|
||||
<h2 className="text-sm font-semibold text-[var(--c-text)]">Change password</h2>
|
||||
</div>
|
||||
<form
|
||||
className="px-5 py-5 flex flex-col gap-4"
|
||||
onSubmit={e => { e.preventDefault(); setCurrentPw(''); setNewPw('') }}
|
||||
>
|
||||
<div>
|
||||
<label className="block text-xs font-semibold text-[#172B4D] mb-1">Current password</label>
|
||||
<label className="block text-xs font-semibold text-[var(--c-text)] mb-1">Current password</label>
|
||||
<input
|
||||
type="password" value={currentPw} onChange={e => setCurrentPw(e.target.value)}
|
||||
className="w-full border border-[#DFE1E6] rounded px-3 py-2 text-sm focus:outline-none focus:border-[#4C9AFF]"
|
||||
className="w-full border border-[var(--c-border)] rounded px-3 py-2 text-sm focus:outline-none focus:border-[var(--c-brand-focus)]"
|
||||
/>
|
||||
</div>
|
||||
<div>
|
||||
<label className="block text-xs font-semibold text-[#172B4D] mb-1">New password</label>
|
||||
<label className="block text-xs font-semibold text-[var(--c-text)] mb-1">New password</label>
|
||||
<input
|
||||
type="password" value={newPw} onChange={e => setNewPw(e.target.value)}
|
||||
className="w-full border border-[#DFE1E6] rounded px-3 py-2 text-sm focus:outline-none focus:border-[#4C9AFF]"
|
||||
className="w-full border border-[var(--c-border)] rounded px-3 py-2 text-sm focus:outline-none focus:border-[var(--c-brand-focus)]"
|
||||
/>
|
||||
</div>
|
||||
<div>
|
||||
<button
|
||||
type="submit"
|
||||
className="px-4 py-2 rounded bg-[#0052CC] text-white text-sm font-medium hover:bg-[#0065FF] min-h-[44px]"
|
||||
className="px-4 py-2 rounded bg-[var(--c-brand)] text-white text-sm font-medium hover:bg-[var(--c-brand-hover)] min-h-[44px]"
|
||||
>
|
||||
Update password
|
||||
</button>
|
||||
@@ -62,19 +62,19 @@ export default function SettingsPage() {
|
||||
<SSHKeySection />
|
||||
|
||||
{/* Danger zone */}
|
||||
<section className="border border-[#FFEBE6] rounded-lg overflow-hidden">
|
||||
<div className="px-5 py-4 border-b border-[#FFEBE6] bg-[#FFEBE6]/50">
|
||||
<h2 className="text-sm font-semibold text-[#BF2600]">Danger zone</h2>
|
||||
<section className="border border-[var(--c-danger-tint)] rounded-lg overflow-hidden">
|
||||
<div className="px-5 py-4 border-b border-[var(--c-danger-tint)] bg-[var(--c-danger-tint)]/50">
|
||||
<h2 className="text-sm font-semibold text-[var(--c-danger-dark)]">Danger zone</h2>
|
||||
</div>
|
||||
<div className="px-5 py-4 flex flex-col gap-3">
|
||||
<div className="flex items-center justify-between">
|
||||
<div>
|
||||
<p className="text-sm font-medium text-[#172B4D]">Sign out</p>
|
||||
<p className="text-xs text-[#5E6C84]">End your current session on this device.</p>
|
||||
<p className="text-sm font-medium text-[var(--c-text)]">Sign out</p>
|
||||
<p className="text-xs text-[var(--c-muted)]">End your current session on this device.</p>
|
||||
</div>
|
||||
<button
|
||||
onClick={logout}
|
||||
className="px-4 py-2 rounded border border-[#DE350B] text-[#DE350B] text-sm font-medium hover:bg-[#FFEBE6] min-h-[44px]"
|
||||
className="px-4 py-2 rounded border border-[var(--c-danger)] text-[var(--c-danger)] text-sm font-medium hover:bg-[var(--c-danger-tint)] min-h-[44px]"
|
||||
>
|
||||
Sign out
|
||||
</button>
|
||||
@@ -88,8 +88,8 @@ export default function SettingsPage() {
|
||||
function Row({ label, value }: { label: string; value?: string }) {
|
||||
return (
|
||||
<div className="flex items-center justify-between py-1">
|
||||
<span className="text-xs text-[#5E6C84] w-24 shrink-0">{label}</span>
|
||||
<span className="text-sm text-[#172B4D] font-medium">{value}</span>
|
||||
<span className="text-xs text-[var(--c-muted)] w-24 shrink-0">{label}</span>
|
||||
<span className="text-sm text-[var(--c-text)] font-medium">{value}</span>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
@@ -109,53 +109,53 @@ function SSHKeySection() {
|
||||
}
|
||||
|
||||
return (
|
||||
<section className="border border-[#DFE1E6] rounded-lg overflow-hidden">
|
||||
<div className="px-5 py-4 border-b border-[#DFE1E6] bg-[#FAFBFC] flex items-center justify-between">
|
||||
<h2 className="text-sm font-semibold text-[#172B4D]">SSH keys</h2>
|
||||
<section className="border border-[var(--c-border)] rounded-lg overflow-hidden">
|
||||
<div className="px-5 py-4 border-b border-[var(--c-border)] bg-[var(--c-surface-raised)] flex items-center justify-between">
|
||||
<h2 className="text-sm font-semibold text-[var(--c-text)]">SSH keys</h2>
|
||||
<button onClick={() => setShowAdd(s => !s)}
|
||||
className="text-xs text-[#0052CC] hover:underline font-medium">
|
||||
className="text-xs text-[var(--c-brand)] hover:underline font-medium">
|
||||
{showAdd ? 'Cancel' : '+ Add key'}
|
||||
</button>
|
||||
</div>
|
||||
|
||||
{showAdd && (
|
||||
<form onSubmit={handleAdd} className="px-5 py-4 border-b border-[#DFE1E6] space-y-3 bg-[#FAFBFC]">
|
||||
<form onSubmit={handleAdd} className="px-5 py-4 border-b border-[var(--c-border)] space-y-3 bg-[var(--c-surface-raised)]">
|
||||
<div>
|
||||
<label className="block text-xs font-semibold text-[#172B4D] mb-1">Title</label>
|
||||
<label className="block text-xs font-semibold text-[var(--c-text)] mb-1">Title</label>
|
||||
<input value={title} onChange={e => setTitle(e.target.value)} required placeholder="e.g. MacBook Pro"
|
||||
className="w-full border border-[#DFE1E6] rounded px-3 py-2 text-sm focus:outline-none focus:border-[#4C9AFF]" />
|
||||
className="w-full border border-[var(--c-border)] rounded px-3 py-2 text-sm focus:outline-none focus:border-[var(--c-brand-focus)]" />
|
||||
</div>
|
||||
<div>
|
||||
<label className="block text-xs font-semibold text-[#172B4D] mb-1">Public key</label>
|
||||
<label className="block text-xs font-semibold text-[var(--c-text)] mb-1">Public key</label>
|
||||
<textarea value={publicKey} onChange={e => setPublicKey(e.target.value)} required rows={3}
|
||||
placeholder="ssh-ed25519 AAAA… or ssh-rsa AAAA…"
|
||||
className="w-full border border-[#DFE1E6] rounded px-3 py-2 text-xs font-mono resize-none focus:outline-none focus:border-[#4C9AFF]" />
|
||||
className="w-full border border-[var(--c-border)] rounded px-3 py-2 text-xs font-mono resize-none focus:outline-none focus:border-[var(--c-brand-focus)]" />
|
||||
</div>
|
||||
{addKey.isError && (
|
||||
<p className="text-xs text-[#DE350B]">{addKey.error instanceof Error ? addKey.error.message : 'Error'}</p>
|
||||
<p className="text-xs text-[var(--c-danger)]">{addKey.error instanceof Error ? addKey.error.message : 'Error'}</p>
|
||||
)}
|
||||
<button type="submit" disabled={addKey.isPending}
|
||||
className="px-4 py-2 rounded bg-[#0052CC] text-white text-sm font-medium hover:bg-[#0065FF] disabled:opacity-50 min-h-[44px]">
|
||||
className="px-4 py-2 rounded bg-[var(--c-brand)] text-white text-sm font-medium hover:bg-[var(--c-brand-hover)] disabled:opacity-50 min-h-[44px]">
|
||||
{addKey.isPending ? 'Adding…' : 'Add SSH key'}
|
||||
</button>
|
||||
</form>
|
||||
)}
|
||||
|
||||
{!keys?.length ? (
|
||||
<div className="px-5 py-5 text-sm text-[#5E6C84]">No SSH keys added yet.</div>
|
||||
<div className="px-5 py-5 text-sm text-[var(--c-muted)]">No SSH keys added yet.</div>
|
||||
) : (
|
||||
<ul>
|
||||
{keys.map(key => (
|
||||
<li key={key.id} className="flex items-center gap-3 px-5 py-3 border-b border-[#DFE1E6] last:border-0">
|
||||
<svg width="16" height="16" fill="none" stroke="#5E6C84" strokeWidth="1.5" viewBox="0 0 24 24" className="shrink-0">
|
||||
<li key={key.id} className="flex items-center gap-3 px-5 py-3 border-b border-[var(--c-border)] last:border-0">
|
||||
<svg width="16" height="16" fill="none" stroke="var(--c-muted)" strokeWidth="1.5" viewBox="0 0 24 24" className="shrink-0">
|
||||
<path strokeLinecap="round" strokeLinejoin="round" d="M15.75 5.25a3 3 0 0 1 3 3m3 0a6 6 0 0 1-7.029 5.912c-.563-.097-1.159.026-1.563.43L10.5 17.25H8.25v2.25H6v2.25H2.25v-2.818c0-.597.237-1.17.659-1.591l6.499-6.499c.404-.404.527-1 .43-1.563A6 6 0 0 1 21.75 8.25Z" />
|
||||
</svg>
|
||||
<div className="flex-1 min-w-0">
|
||||
<p className="text-sm font-medium text-[#172B4D] truncate">{key.title}</p>
|
||||
<p className="text-xs font-mono text-[#5E6C84] truncate">{key.fingerprint}</p>
|
||||
<p className="text-sm font-medium text-[var(--c-text)] truncate">{key.title}</p>
|
||||
<p className="text-xs font-mono text-[var(--c-muted)] truncate">{key.fingerprint}</p>
|
||||
</div>
|
||||
<button onClick={() => deleteKey.mutate(key.id)}
|
||||
className="text-xs px-3 py-1.5 rounded border border-[#DE350B] text-[#DE350B] hover:bg-[#FFEBE6] shrink-0 min-h-[32px]">
|
||||
className="text-xs px-3 py-1.5 rounded border border-[var(--c-danger)] text-[var(--c-danger)] hover:bg-[var(--c-danger-tint)] shrink-0 min-h-[32px]">
|
||||
Delete
|
||||
</button>
|
||||
</li>
|
||||
|
||||
@@ -10,30 +10,30 @@ export default function StarredPage() {
|
||||
|
||||
return (
|
||||
<div className="max-w-4xl mx-auto px-4 md:px-6 py-6">
|
||||
<h1 className="text-xl font-semibold text-[#172B4D] mb-6">Starred repositories</h1>
|
||||
<h1 className="text-xl font-semibold text-[var(--c-text)] mb-6">Starred repositories</h1>
|
||||
|
||||
{!starredRepos.length ? (
|
||||
<div className="flex flex-col items-center justify-center py-16 border border-dashed border-[#DFE1E6] rounded text-center gap-3">
|
||||
<svg width="36" height="36" fill="none" stroke="#97A0AF" strokeWidth="1.5" viewBox="0 0 24 24">
|
||||
<div className="flex flex-col items-center justify-center py-16 border border-dashed border-[var(--c-border)] rounded text-center gap-3">
|
||||
<svg width="36" height="36" fill="none" stroke="var(--c-subtle)" strokeWidth="1.5" viewBox="0 0 24 24">
|
||||
<path strokeLinecap="round" strokeLinejoin="round" d="M11.48 3.499a.562.562 0 0 1 1.04 0l2.125 5.111a.563.563 0 0 0 .475.345l5.518.442c.499.04.701.663.321.988l-4.204 3.602a.563.563 0 0 0-.182.557l1.285 5.385a.562.562 0 0 1-.84.61l-4.725-2.885a.562.562 0 0 0-.586 0L6.982 20.54a.562.562 0 0 1-.84-.61l1.285-5.386a.562.562 0 0 0-.182-.557l-4.204-3.602a.562.562 0 0 1 .321-.988l5.518-.442a.563.563 0 0 0 .475-.345L11.48 3.5Z" />
|
||||
</svg>
|
||||
<div>
|
||||
<p className="text-sm font-medium text-[#172B4D]">No starred repositories</p>
|
||||
<p className="text-xs text-[#5E6C84] mt-1">Star repositories in the sidebar to find them here quickly.</p>
|
||||
<p className="text-sm font-medium text-[var(--c-text)]">No starred repositories</p>
|
||||
<p className="text-xs text-[var(--c-muted)] mt-1">Star repositories in the sidebar to find them here quickly.</p>
|
||||
</div>
|
||||
<Link to="/repos" className="text-xs text-[#0052CC] hover:underline">Browse repositories</Link>
|
||||
<Link to="/repos" className="text-xs text-[var(--c-brand)] hover:underline">Browse repositories</Link>
|
||||
</div>
|
||||
) : (
|
||||
<div className="flex flex-col gap-2">
|
||||
{starredRepos.map(r => (
|
||||
<Link key={r.id} to={`/repos/${r.ownerName}/${r.name}`}
|
||||
className="flex items-center gap-3 p-4 border border-[#DFE1E6] rounded bg-white hover:border-[#4C9AFF] hover:bg-[#FAFBFC] transition-colors">
|
||||
className="flex items-center gap-3 p-4 border border-[var(--c-border)] rounded bg-[var(--c-surface)] hover:border-[var(--c-brand-focus)] hover:bg-[var(--c-surface-raised)] transition-colors">
|
||||
<svg width="16" height="16" fill="#F79009" viewBox="0 0 24 24">
|
||||
<path d="M11.48 3.499a.562.562 0 0 1 1.04 0l2.125 5.111a.563.563 0 0 0 .475.345l5.518.442c.499.04.701.663.321.988l-4.204 3.602a.563.563 0 0 0-.182.557l1.285 5.385a.562.562 0 0 1-.84.61l-4.725-2.885a.562.562 0 0 0-.586 0L6.982 20.54a.562.562 0 0 1-.84-.61l1.285-5.386a.562.562 0 0 0-.182-.557l-4.204-3.602a.562.562 0 0 1 .321-.988l5.518-.442a.563.563 0 0 0 .475-.345L11.48 3.5Z"/>
|
||||
</svg>
|
||||
<div className="flex-1 min-w-0">
|
||||
<p className="text-sm font-semibold text-[#0052CC]">{r.ownerName}/{r.name}</p>
|
||||
{r.description && <p className="text-xs text-[#5E6C84] truncate mt-0.5">{r.description}</p>}
|
||||
<p className="text-sm font-semibold text-[var(--c-brand)]">{r.ownerName}/{r.name}</p>
|
||||
{r.description && <p className="text-xs text-[var(--c-muted)] truncate mt-0.5">{r.description}</p>}
|
||||
</div>
|
||||
</Link>
|
||||
))}
|
||||
|
||||
Reference in New Issue
Block a user