darkmode is now available

This commit is contained in:
2026-05-07 13:42:46 +02:00
parent ec309eb626
commit 8cb918b064
36 changed files with 588 additions and 489 deletions
+64 -64
View File
@@ -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>
)