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
+9 -1
View File
@@ -4,7 +4,15 @@
<meta charset="UTF-8" /> <meta charset="UTF-8" />
<link rel="icon" type="image/svg+xml" href="/favicon.svg" /> <link rel="icon" type="image/svg+xml" href="/favicon.svg" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" /> <meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>frontend</title> <title>ForgeBucket</title>
<script>
// Apply dark class before first paint to avoid flash
(function(){
var stored = localStorage.getItem('fb_dark');
var dark = stored !== null ? stored === 'true' : window.matchMedia('(prefers-color-scheme: dark)').matches;
if (dark) document.documentElement.classList.add('dark');
})();
</script>
</head> </head>
<body> <body>
<div id="root"></div> <div id="root"></div>
+2 -2
View File
@@ -50,8 +50,8 @@ function CSRFBootstrap() {
function NotFound() { function NotFound() {
return ( return (
<div className="flex flex-col items-center justify-center h-full gap-3 text-center py-24"> <div className="flex flex-col items-center justify-center h-full gap-3 text-center py-24">
<p className="text-4xl font-bold text-[#DFE1E6]">404</p> <p className="text-4xl font-bold text-[var(--c-border)]">404</p>
<p className="text-sm text-[#5E6C84]">Page not found.</p> <p className="text-sm text-[var(--c-muted)]">Page not found.</p>
</div> </div>
) )
} }
@@ -13,19 +13,19 @@ interface PipelineWaterfallProps {
} }
const STATUS_COLOR: Record<Pipeline['status'], string> = { const STATUS_COLOR: Record<Pipeline['status'], string> = {
pending: 'bg-[#F4F5F7] border-[#DFE1E6] text-[#5E6C84]', pending: 'bg-[var(--c-surface-muted)] border-[var(--c-border)] text-[var(--c-muted)]',
running: 'bg-[#DEEBFF] border-[#4C9AFF] text-[#0052CC]', running: 'bg-[var(--c-brand-tint)] border-[var(--c-brand-focus)] text-[var(--c-brand)]',
success: 'bg-[#E3FCEF] border-[#79F2C0] text-[#006644]', success: 'bg-[#E3FCEF] border-[#79F2C0] text-[#006644]',
failure: 'bg-[#FFEBE6] border-[#FF8F73] text-[#BF2600]', failure: 'bg-[var(--c-danger-tint)] border-[#FF8F73] text-[var(--c-danger-dark)]',
cancelled: 'bg-[#F4F5F7] border-[#DFE1E6] text-[#5E6C84]', cancelled: 'bg-[var(--c-surface-muted)] border-[var(--c-border)] text-[var(--c-muted)]',
} }
const STATUS_DOT: Record<Pipeline['status'], string> = { const STATUS_DOT: Record<Pipeline['status'], string> = {
pending: 'bg-[#97A0AF]', pending: 'bg-[var(--c-subtle)]',
running: 'bg-[#0052CC] animate-pulse', running: 'bg-[var(--c-brand)] animate-pulse',
success: 'bg-[#00875A]', success: 'bg-[var(--c-success)]',
failure: 'bg-[#DE350B]', failure: 'bg-[var(--c-danger)]',
cancelled: 'bg-[#97A0AF]', cancelled: 'bg-[var(--c-subtle)]',
} }
const STATUS_LABEL: Record<Pipeline['status'], string> = { const STATUS_LABEL: Record<Pipeline['status'], string> = {
@@ -71,12 +71,12 @@ export function PipelineWaterfall({ pipeline, stages }: PipelineWaterfallProps)
const resolvedStages = stages ?? defaultStages(pipeline.status) const resolvedStages = stages ?? defaultStages(pipeline.status)
return ( return (
<div className="border border-[#DFE1E6] rounded p-4 bg-white"> <div className="border border-[var(--c-border)] rounded p-4 bg-[var(--c-surface)]">
{/* Pipeline header */} {/* Pipeline header */}
<div className="flex items-center justify-between mb-4"> <div className="flex items-center justify-between mb-4">
<div className="flex items-center gap-2"> <div className="flex items-center gap-2">
<span className={cn('w-2.5 h-2.5 rounded-full shrink-0', STATUS_DOT[pipeline.status])} /> <span className={cn('w-2.5 h-2.5 rounded-full shrink-0', STATUS_DOT[pipeline.status])} />
<span className="text-sm font-semibold text-[#172B4D]"> <span className="text-sm font-semibold text-[var(--c-text)]">
Pipeline #{pipeline.id} Pipeline #{pipeline.id}
</span> </span>
<span className={cn( <span className={cn(
@@ -86,7 +86,7 @@ export function PipelineWaterfall({ pipeline, stages }: PipelineWaterfallProps)
{STATUS_LABEL[pipeline.status]} {STATUS_LABEL[pipeline.status]}
</span> </span>
</div> </div>
<span className="text-xs text-[#5E6C84]">{pipeline.ref}</span> <span className="text-xs text-[var(--c-muted)]">{pipeline.ref}</span>
</div> </div>
{/* Waterfall stages */} {/* Waterfall stages */}
@@ -108,8 +108,8 @@ export function PipelineWaterfall({ pipeline, stages }: PipelineWaterfallProps)
{/* Connector arrow (not after last) */} {/* Connector arrow (not after last) */}
{i < resolvedStages.length - 1 && ( {i < resolvedStages.length - 1 && (
<div className="flex items-center px-1"> <div className="flex items-center px-1">
<div className="h-px w-4 bg-[#DFE1E6]" /> <div className="h-px w-4 bg-[var(--c-border)]" />
<svg width="6" height="8" viewBox="0 0 6 8" fill="#DFE1E6"> <svg width="6" height="8" viewBox="0 0 6 8" fill="var(--c-border)">
<path d="M0 0l6 4-6 4V0z" /> <path d="M0 0l6 4-6 4V0z" />
</svg> </svg>
</div> </div>
@@ -124,21 +124,21 @@ export function PipelineWaterfall({ pipeline, stages }: PipelineWaterfallProps)
function StatusIcon({ status }: { status: Pipeline['status'] }) { function StatusIcon({ status }: { status: Pipeline['status'] }) {
if (status === 'success') { if (status === 'success') {
return ( return (
<svg width="16" height="16" fill="none" stroke="#00875A" strokeWidth="2.5" viewBox="0 0 24 24"> <svg width="16" height="16" fill="none" stroke="var(--c-success)" strokeWidth="2.5" viewBox="0 0 24 24">
<path strokeLinecap="round" strokeLinejoin="round" d="m4.5 12.75 6 6 9-13.5" /> <path strokeLinecap="round" strokeLinejoin="round" d="m4.5 12.75 6 6 9-13.5" />
</svg> </svg>
) )
} }
if (status === 'failure') { if (status === 'failure') {
return ( return (
<svg width="16" height="16" fill="none" stroke="#DE350B" strokeWidth="2.5" viewBox="0 0 24 24"> <svg width="16" height="16" fill="none" stroke="var(--c-danger)" strokeWidth="2.5" viewBox="0 0 24 24">
<path strokeLinecap="round" strokeLinejoin="round" d="M6 18 18 6M6 6l12 12" /> <path strokeLinecap="round" strokeLinejoin="round" d="M6 18 18 6M6 6l12 12" />
</svg> </svg>
) )
} }
if (status === 'running') { if (status === 'running') {
return ( return (
<svg width="16" height="16" fill="none" stroke="#0052CC" strokeWidth="2" viewBox="0 0 24 24" className="animate-spin"> <svg width="16" height="16" fill="none" stroke="var(--c-brand)" strokeWidth="2" viewBox="0 0 24 24" className="animate-spin">
<path strokeLinecap="round" d="M12 3a9 9 0 1 0 9 9" /> <path strokeLinecap="round" d="M12 3a9 9 0 1 0 9 9" />
</svg> </svg>
) )
+14 -14
View File
@@ -16,7 +16,7 @@ interface DiffViewerProps {
export function DiffViewer({ files }: DiffViewerProps) { export function DiffViewer({ files }: DiffViewerProps) {
if (!files.length) { if (!files.length) {
return ( return (
<div className="text-center py-12 text-[#5E6C84] text-sm"> <div className="text-center py-12 text-[var(--c-muted)] text-sm">
No changes in this diff. No changes in this diff.
</div> </div>
) )
@@ -36,9 +36,9 @@ function FileDiffBlock({ file }: { file: FileDiff }) {
const lines = useMemo(() => parsePatch(file.patch), [file.patch]) const lines = useMemo(() => parsePatch(file.patch), [file.patch])
return ( return (
<div className="border border-[#DFE1E6] rounded overflow-hidden font-mono text-xs"> <div className="border border-[var(--c-border)] rounded overflow-hidden font-mono text-xs">
{/* File header */} {/* File header */}
<div className="flex items-center justify-between px-3 py-2 bg-[#F4F5F7] border-b border-[#DFE1E6] gap-2"> <div className="flex items-center justify-between px-3 py-2 bg-[var(--c-surface-muted)] border-b border-[var(--c-border)] gap-2">
<button <button
onClick={() => setCollapsed(c => !c)} onClick={() => setCollapsed(c => !c)}
className="flex items-center gap-2 text-left min-w-0" className="flex items-center gap-2 text-left min-w-0"
@@ -50,17 +50,17 @@ function FileDiffBlock({ file }: { file: FileDiff }) {
> >
<path strokeLinecap="round" strokeLinejoin="round" d="m19.5 8.25-7.5 7.5-7.5-7.5" /> <path strokeLinecap="round" strokeLinejoin="round" d="m19.5 8.25-7.5 7.5-7.5-7.5" />
</svg> </svg>
<span className="font-semibold text-[#172B4D] truncate">{file.path}</span> <span className="font-semibold text-[var(--c-text)] truncate">{file.path}</span>
{file.oldPath && file.oldPath !== file.path && ( {file.oldPath && file.oldPath !== file.path && (
<span className="text-[#5E6C84] text-[10px] shrink-0"> {file.oldPath}</span> <span className="text-[var(--c-muted)] text-[10px] shrink-0"> {file.oldPath}</span>
)} )}
</button> </button>
<div className="flex items-center gap-2 shrink-0 text-[11px]"> <div className="flex items-center gap-2 shrink-0 text-[11px]">
{file.additions > 0 && ( {file.additions > 0 && (
<span className="text-[#00875A] font-semibold">+{file.additions}</span> <span className="text-[var(--c-success)] font-semibold">+{file.additions}</span>
)} )}
{file.deletions > 0 && ( {file.deletions > 0 && (
<span className="text-[#DE350B] font-semibold">-{file.deletions}</span> <span className="text-[var(--c-danger)] font-semibold">-{file.deletions}</span>
)} )}
</div> </div>
</div> </div>
@@ -120,10 +120,10 @@ function parsePatch(patch: string): ParsedLine[] {
function DiffLine({ line }: { line: ParsedLine }) { function DiffLine({ line }: { line: ParsedLine }) {
if (line.type === 'hunk') { if (line.type === 'hunk') {
return ( return (
<tr className="bg-[#DEEBFF]"> <tr className="bg-[var(--c-brand-tint)]">
<td className="px-2 py-0.5 text-[#5E6C84] select-none w-10 text-right" /> <td className="px-2 py-0.5 text-[var(--c-muted)] select-none w-10 text-right" />
<td className="px-2 py-0.5 text-[#5E6C84] select-none w-10 text-right" /> <td className="px-2 py-0.5 text-[var(--c-muted)] select-none w-10 text-right" />
<td className="px-3 py-0.5 text-[#0052CC] font-semibold whitespace-pre">{line.content}</td> <td className="px-3 py-0.5 text-[var(--c-brand)] font-semibold whitespace-pre">{line.content}</td>
</tr> </tr>
) )
} }
@@ -131,14 +131,14 @@ function DiffLine({ line }: { line: ParsedLine }) {
const bg = line.type === 'added' const bg = line.type === 'added'
? 'bg-[#E3FCEF]' ? 'bg-[#E3FCEF]'
: line.type === 'removed' : line.type === 'removed'
? 'bg-[#FFEBE6]' ? 'bg-[var(--c-danger-tint)]'
: '' : ''
const gutter = line.type === 'added' const gutter = line.type === 'added'
? 'bg-[#ABF5D1] text-[#006644]' ? 'bg-[#ABF5D1] text-[#006644]'
: line.type === 'removed' : line.type === 'removed'
? 'bg-[#FFBDAD] text-[#BF2600]' ? 'bg-[#FFBDAD] text-[var(--c-danger-dark)]'
: 'bg-[#F4F5F7] text-[#5E6C84]' : 'bg-[var(--c-surface-muted)] text-[var(--c-muted)]'
const prefix = line.type === 'added' ? '+' : line.type === 'removed' ? '-' : ' ' const prefix = line.type === 'added' ? '+' : line.type === 'removed' ? '-' : ' '
@@ -48,7 +48,7 @@ export function MobileComment({ open, onClose, filePath, lineNumber, children }:
<div <div
ref={sheetRef} ref={sheetRef}
className={cn( className={cn(
'fixed bottom-0 left-0 right-0 z-50 bg-white rounded-t-2xl shadow-xl', 'fixed bottom-0 left-0 right-0 z-50 bg-[var(--c-surface)] rounded-t-2xl shadow-xl',
'transition-transform duration-300 ease-out', 'transition-transform duration-300 ease-out',
'pb-[env(safe-area-inset-bottom)]', 'pb-[env(safe-area-inset-bottom)]',
open ? 'translate-y-0' : 'translate-y-full', open ? 'translate-y-0' : 'translate-y-full',
@@ -59,18 +59,18 @@ export function MobileComment({ open, onClose, filePath, lineNumber, children }:
> >
{/* Handle */} {/* Handle */}
<div className="flex justify-center pt-3 pb-1"> <div className="flex justify-center pt-3 pb-1">
<div className="w-10 h-1 rounded-full bg-[#DFE1E6]" /> <div className="w-10 h-1 rounded-full bg-[var(--c-border)]" />
</div> </div>
{/* Header */} {/* Header */}
<div className="flex items-center justify-between px-4 py-3 border-b border-[#DFE1E6]"> <div className="flex items-center justify-between px-4 py-3 border-b border-[var(--c-border)]">
<div className="min-w-0"> <div className="min-w-0">
<p className="text-xs font-semibold text-[#172B4D] truncate">{filePath}</p> <p className="text-xs font-semibold text-[var(--c-text)] truncate">{filePath}</p>
<p className="text-xs text-[#5E6C84]">Line {lineNumber}</p> <p className="text-xs text-[var(--c-muted)]">Line {lineNumber}</p>
</div> </div>
<button <button
onClick={onClose} onClick={onClose}
className="flex items-center justify-center w-8 h-8 rounded hover:bg-[#F4F5F7] text-[#5E6C84]" className="flex items-center justify-center w-8 h-8 rounded hover:bg-[var(--c-surface-muted)] text-[var(--c-muted)]"
aria-label="Close" aria-label="Close"
> >
<svg width="16" height="16" fill="none" stroke="currentColor" strokeWidth="2" viewBox="0 0 24 24"> <svg width="16" height="16" fill="none" stroke="currentColor" strokeWidth="2" viewBox="0 0 24 24">
@@ -86,17 +86,17 @@ export function MobileComment({ open, onClose, filePath, lineNumber, children }:
autoFocus autoFocus
rows={4} rows={4}
placeholder="Leave a comment on this line…" placeholder="Leave a comment on this line…"
className="w-full border border-[#DFE1E6] rounded p-3 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 p-3 text-sm resize-none focus:outline-none focus:border-[var(--c-brand-focus)] focus:ring-1 focus:ring-[var(--c-brand-focus)]"
/> />
)} )}
<div className="flex justify-end gap-2 mt-3"> <div className="flex justify-end gap-2 mt-3">
<button <button
onClick={onClose} onClick={onClose}
className="px-4 py-2 text-sm rounded border border-[#DFE1E6] text-[#172B4D] hover:bg-[#F4F5F7] min-h-[44px]" className="px-4 py-2 text-sm rounded border border-[var(--c-border)] text-[var(--c-text)] hover:bg-[var(--c-surface-muted)] min-h-[44px]"
> >
Cancel Cancel
</button> </button>
<button className="px-4 py-2 text-sm rounded bg-[#0052CC] text-white hover:bg-[#0065FF] min-h-[44px]"> <button className="px-4 py-2 text-sm rounded bg-[var(--c-brand)] text-white hover:bg-[var(--c-brand-hover)] min-h-[44px]">
Save Save
</button> </button>
</div> </div>
+1 -1
View File
@@ -5,7 +5,7 @@ import { BottomTabBar } from './BottomTabBar'
export function AppShell() { export function AppShell() {
return ( return (
<div className="flex flex-col h-screen overflow-hidden bg-[#F4F5F7]"> <div className="flex flex-col h-screen overflow-hidden bg-[var(--c-surface-muted)]">
{/* Top header — full width, always visible */} {/* Top header — full width, always visible */}
<Header /> <Header />
@@ -57,7 +57,7 @@ export function BottomTabBar({ className }: BottomTabBarProps) {
return ( return (
<nav <nav
className={cn( className={cn(
'fixed bottom-0 left-0 right-0 h-14 bg-white border-t border-[#DFE1E6] pb-[env(safe-area-inset-bottom)]', 'fixed bottom-0 left-0 right-0 h-14 bg-[var(--c-surface)] border-t border-[var(--c-border)] pb-[env(safe-area-inset-bottom)]',
className, className,
)} )}
aria-label="Mobile navigation" aria-label="Mobile navigation"
@@ -72,7 +72,7 @@ export function BottomTabBar({ className }: BottomTabBarProps) {
cn( cn(
'flex flex-col items-center justify-center h-full gap-0.5 transition-colors', 'flex flex-col items-center justify-center h-full gap-0.5 transition-colors',
'min-w-[44px]', // WCAG touch target width 'min-w-[44px]', // WCAG touch target width
isActive ? 'text-[#0052CC]' : 'text-[#5E6C84]', isActive ? 'text-[var(--c-brand)]' : 'text-[var(--c-muted)]',
) )
} }
> >
+28 -9
View File
@@ -1,9 +1,11 @@
import { useState } from 'react' import { useState } from 'react'
import { Link, useNavigate } from 'react-router-dom' import { Link, useNavigate } from 'react-router-dom'
import { useAuth } from '../../contexts/AuthContext' import { useAuth } from '../../contexts/AuthContext'
import { useDarkMode } from '../../hooks/useDarkMode'
export function Header() { export function Header() {
const { user, isAuthenticated } = useAuth() const { user, isAuthenticated } = useAuth()
const { dark, toggle: toggleDark } = useDarkMode()
const [showCreate, setShowCreate] = useState(false) const [showCreate, setShowCreate] = useState(false)
const [search, setSearch] = useState('') const [search, setSearch] = useState('')
const navigate = useNavigate() const navigate = useNavigate()
@@ -20,7 +22,7 @@ export function Header() {
<header className="h-12 bg-[#1A2634] flex items-center gap-3 px-3 shrink-0 z-40 relative"> <header className="h-12 bg-[#1A2634] flex items-center gap-3 px-3 shrink-0 z-40 relative">
{/* Logo */} {/* Logo */}
<Link to="/" className="flex items-center gap-2 shrink-0"> <Link to="/" className="flex items-center gap-2 shrink-0">
<div className="w-7 h-7 rounded bg-[#0052CC] flex items-center justify-center text-white font-bold text-sm">F</div> <div className="w-7 h-7 rounded bg-[var(--c-brand)] flex items-center justify-center text-white font-bold text-sm">F</div>
<span className="text-white font-semibold text-sm hidden sm:block">ForgeBucket</span> <span className="text-white font-semibold text-sm hidden sm:block">ForgeBucket</span>
</Link> </Link>
@@ -35,7 +37,7 @@ export function Header() {
value={search} value={search}
onChange={e => setSearch(e.target.value)} onChange={e => setSearch(e.target.value)}
placeholder="Search repositories, issues, pull requests…" placeholder="Search repositories, issues, pull requests…"
className="w-full bg-white/10 text-white placeholder-white/40 text-xs rounded pl-8 pr-3 py-1.5 focus:outline-none focus:bg-white/15 focus:ring-1 focus:ring-white/30" className="w-full bg-[var(--c-surface)]/10 text-white placeholder-white/40 text-xs rounded pl-8 pr-3 py-1.5 focus:outline-none focus:bg-white/15 focus:ring-1 focus:ring-white/30"
/> />
</div> </div>
</form> </form>
@@ -46,7 +48,7 @@ export function Header() {
<div className="relative"> <div className="relative">
<button <button
onClick={() => setShowCreate(s => !s)} onClick={() => setShowCreate(s => !s)}
className="flex items-center gap-1 px-3 py-1.5 rounded bg-[#0052CC] hover:bg-[#0065FF] text-white text-xs font-semibold transition-colors" className="flex items-center gap-1 px-3 py-1.5 rounded bg-[var(--c-brand)] hover:bg-[var(--c-brand-hover)] text-white text-xs font-semibold transition-colors"
> >
<svg width="12" height="12" fill="none" stroke="currentColor" strokeWidth="2.5" viewBox="0 0 24 24"> <svg width="12" height="12" 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" /> <path strokeLinecap="round" strokeLinejoin="round" d="M12 4.5v15m7.5-7.5h-15" />
@@ -59,6 +61,23 @@ export function Header() {
</div> </div>
)} )}
{/* Dark mode toggle */}
<button
onClick={toggleDark}
title={dark ? 'Switch to light mode' : 'Switch to dark mode'}
className="w-8 h-8 rounded flex items-center justify-center text-white/60 hover:bg-white/10 hover:text-white transition-colors"
>
{dark ? (
<svg width="16" height="16" fill="none" stroke="currentColor" strokeWidth="1.5" viewBox="0 0 24 24">
<path strokeLinecap="round" strokeLinejoin="round" d="M12 3v2.25m6.364.386-1.591 1.591M21 12h-2.25m-.386 6.364-1.591-1.591M12 18.75V21m-4.773-4.227-1.591 1.591M5.25 12H3m4.227-4.773L5.636 5.636M15.75 12a3.75 3.75 0 1 1-7.5 0 3.75 3.75 0 0 1 7.5 0Z" />
</svg>
) : (
<svg width="16" height="16" fill="none" stroke="currentColor" strokeWidth="1.5" viewBox="0 0 24 24">
<path strokeLinecap="round" strokeLinejoin="round" d="M21.752 15.002A9.72 9.72 0 0 1 18 15.75c-5.385 0-9.75-4.365-9.75-9.75 0-1.33.266-2.597.748-3.752A9.753 9.753 0 0 0 3 11.25C3 16.635 7.365 21 12.75 21a9.753 9.753 0 0 0 9.002-5.998Z" />
</svg>
)}
</button>
{/* Notifications (placeholder) */} {/* Notifications (placeholder) */}
<button className="w-8 h-8 rounded flex items-center justify-center text-white/60 hover:bg-white/10 hover:text-white transition-colors"> <button className="w-8 h-8 rounded flex items-center justify-center text-white/60 hover:bg-white/10 hover:text-white transition-colors">
<svg width="16" height="16" fill="none" stroke="currentColor" strokeWidth="1.5" viewBox="0 0 24 24"> <svg width="16" height="16" fill="none" stroke="currentColor" strokeWidth="1.5" viewBox="0 0 24 24">
@@ -76,7 +95,7 @@ export function Header() {
{/* Avatar */} {/* Avatar */}
{isAuthenticated ? ( {isAuthenticated ? (
<Link to="/profile" className="w-7 h-7 rounded-full bg-[#0052CC] flex items-center justify-center text-white text-xs font-bold hover:ring-2 hover:ring-white/30 transition-all"> <Link to="/profile" className="w-7 h-7 rounded-full bg-[var(--c-brand)] flex items-center justify-center text-white text-xs font-bold hover:ring-2 hover:ring-white/30 transition-all">
{user?.username?.[0]?.toUpperCase()} {user?.username?.[0]?.toUpperCase()}
</Link> </Link>
) : ( ) : (
@@ -96,16 +115,16 @@ export function Header() {
function CreateMenu({ onClose }: { onClose: () => void }) { function CreateMenu({ onClose }: { onClose: () => void }) {
return ( return (
<div className="absolute right-0 top-full mt-1 w-56 bg-white rounded-lg shadow-xl border border-[#DFE1E6] z-50 overflow-hidden"> <div className="absolute right-0 top-full mt-1 w-56 bg-[var(--c-surface)] rounded-lg shadow-xl border border-[var(--c-border)] z-50 overflow-hidden">
<div className="px-4 py-2.5 border-b border-[#DFE1E6]"> <div className="px-4 py-2.5 border-b border-[var(--c-border)]">
<p className="text-xs font-semibold text-[#5E6C84] uppercase tracking-wide">Create</p> <p className="text-xs font-semibold text-[var(--c-muted)] uppercase tracking-wide">Create</p>
</div> </div>
<ul> <ul>
<li> <li>
<Link <Link
to="/repos/new" to="/repos/new"
onClick={onClose} onClick={onClose}
className="flex items-center gap-2.5 px-4 py-3 text-sm text-[#172B4D] hover:bg-[#F4F5F7]" className="flex items-center gap-2.5 px-4 py-3 text-sm text-[var(--c-text)] hover:bg-[var(--c-surface-muted)]"
> >
<svg width="16" height="16" fill="none" stroke="currentColor" strokeWidth="1.5" viewBox="0 0 24 24"> <svg width="16" height="16" fill="none" stroke="currentColor" strokeWidth="1.5" 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" /> <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" />
@@ -117,7 +136,7 @@ function CreateMenu({ onClose }: { onClose: () => void }) {
<Link <Link
to="/repos/import" to="/repos/import"
onClick={onClose} onClick={onClose}
className="flex items-center gap-2.5 px-4 py-3 text-sm text-[#172B4D] hover:bg-[#F4F5F7]" className="flex items-center gap-2.5 px-4 py-3 text-sm text-[var(--c-text)] hover:bg-[var(--c-surface-muted)]"
> >
<svg width="16" height="16" fill="none" stroke="currentColor" strokeWidth="1.5" viewBox="0 0 24 24"> <svg width="16" height="16" fill="none" stroke="currentColor" strokeWidth="1.5" viewBox="0 0 24 24">
<path strokeLinecap="round" strokeLinejoin="round" d="M3 16.5v2.25A2.25 2.25 0 0 0 5.25 21h13.5A2.25 2.25 0 0 0 21 18.75V16.5M16.5 12 12 16.5m0 0L7.5 12m4.5 4.5V3" /> <path strokeLinecap="round" strokeLinejoin="round" d="M3 16.5v2.25A2.25 2.25 0 0 0 5.25 21h13.5A2.25 2.25 0 0 0 21 18.75V16.5M16.5 12 12 16.5m0 0L7.5 12m4.5 4.5V3" />
+4 -4
View File
@@ -27,7 +27,7 @@ export function Sidebar({ className }: SidebarProps) {
{isAuthenticated && ( {isAuthenticated && (
<div className="px-3 py-3 border-b border-white/10"> <div className="px-3 py-3 border-b border-white/10">
<div className="flex items-center gap-2"> <div className="flex items-center gap-2">
<div className="w-6 h-6 rounded bg-[#0052CC] flex items-center justify-center text-white text-[10px] font-bold shrink-0"> <div className="w-6 h-6 rounded bg-[var(--c-brand)] flex items-center justify-center text-white text-[10px] font-bold shrink-0">
{user?.username?.[0]?.toUpperCase()} {user?.username?.[0]?.toUpperCase()}
</div> </div>
<span className="text-white text-xs font-semibold truncate">{user?.username}</span> <span className="text-white text-xs font-semibold truncate">{user?.username}</span>
@@ -108,7 +108,7 @@ function SidebarItem({ to, icon, label, end }: { to: string; icon: React.ReactNo
className={({ isActive }) => cn( className={({ isActive }) => cn(
'flex items-center gap-2.5 px-3 py-2 mx-1 rounded text-sm transition-colors min-h-[36px]', 'flex items-center gap-2.5 px-3 py-2 mx-1 rounded text-sm transition-colors min-h-[36px]',
isActive isActive
? 'bg-white/12 text-white font-medium' ? 'bg-[var(--c-surface)]/12 text-white font-medium'
: 'text-white/65 hover:bg-white/8 hover:text-white', : 'text-white/65 hover:bg-white/8 hover:text-white',
)} )}
> >
@@ -124,7 +124,7 @@ function RecentRepoItem({ ownerName, name, isActive, isStarred, onStar }: {
return ( return (
<div className={cn( <div className={cn(
'group flex items-center gap-2 mx-1 rounded transition-colors', 'group flex items-center gap-2 mx-1 rounded transition-colors',
isActive ? 'bg-white/12' : 'hover:bg-white/8', isActive ? 'bg-[var(--c-surface)]/12' : 'hover:bg-white/8',
)}> )}>
<Link to={`/repos/${ownerName}/${name}`} <Link to={`/repos/${ownerName}/${name}`}
className="flex items-center gap-2 flex-1 min-w-0 px-3 py-1.5"> className="flex items-center gap-2 flex-1 min-w-0 px-3 py-1.5">
@@ -160,7 +160,7 @@ function RepoSubNav({ owner, repo }: { owner: string; repo: string }) {
className={({ isActive }) => cn( className={({ isActive }) => cn(
'flex items-center gap-2 px-3 py-1.5 mx-1 rounded text-xs transition-colors', 'flex items-center gap-2 px-3 py-1.5 mx-1 rounded text-xs transition-colors',
isActive isActive
? 'bg-white/12 text-white' ? 'bg-[var(--c-surface)]/12 text-white'
: 'text-white/55 hover:bg-white/8 hover:text-white/90', : 'text-white/55 hover:bg-white/8 hover:text-white/90',
)}> )}>
<span className="shrink-0">{item.icon}</span> <span className="shrink-0">{item.icon}</span>
+5 -5
View File
@@ -12,25 +12,25 @@ export function RepoCard({ repo }: RepoCardProps) {
return ( return (
<Link <Link
to={`/repos/${repo.ownerName}/${repo.name}`} to={`/repos/${repo.ownerName}/${repo.name}`}
className="flex items-start gap-3 p-4 border border-[#DFE1E6] rounded hover:border-[#4C9AFF] hover:bg-[#FAFBFC] transition-colors group" 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 group"
> >
<RepoAvatar ownerName={repo.ownerName} name={repo.name} avatarUrl={repo.avatarUrl} size={36} /> <RepoAvatar ownerName={repo.ownerName} name={repo.name} avatarUrl={repo.avatarUrl} size={36} />
<div className="flex-1 min-w-0"> <div className="flex-1 min-w-0">
<div className="flex items-center gap-2 flex-wrap"> <div className="flex items-center gap-2 flex-wrap">
<span className="text-sm font-semibold text-[#0052CC] group-hover:underline truncate"> <span className="text-sm font-semibold text-[var(--c-brand)] group-hover:underline truncate">
{repo.name} {repo.name}
</span> </span>
{repo.isPrivate && ( {repo.isPrivate && (
<span className="text-[10px] font-medium px-1.5 py-0.5 rounded-full border border-[#DFE1E6] text-[#5E6C84] shrink-0"> <span className="text-[10px] font-medium px-1.5 py-0.5 rounded-full border border-[var(--c-border)] text-[var(--c-muted)] shrink-0">
Private Private
</span> </span>
)} )}
</div> </div>
{repo.description && ( {repo.description && (
<p className="text-xs text-[#5E6C84] mt-0.5 truncate">{repo.description}</p> <p className="text-xs text-[var(--c-muted)] mt-0.5 truncate">{repo.description}</p>
)} )}
<p className="text-xs text-[#5E6C84] mt-1"> <p className="text-xs text-[var(--c-muted)] mt-1">
Updated {ago} · {repo.defaultBranch} Updated {ago} · {repo.defaultBranch}
</p> </p>
</div> </div>
+16 -16
View File
@@ -35,9 +35,9 @@ export function TreeBrowser({ owner, repo, ref, path = '' }: TreeBrowserProps) {
const { data: entries, isLoading, isError } = useRepoTree(owner, repo, ref, path) const { data: entries, isLoading, isError } = useRepoTree(owner, repo, ref, path)
if (isLoading) return <TreeSkeleton /> if (isLoading) return <TreeSkeleton />
if (isError) return <p className="text-xs text-[#DE350B] p-4">Failed to load file tree.</p> if (isError) return <p className="text-xs text-[var(--c-danger)] p-4">Failed to load file tree.</p>
if (!entries?.length) return ( if (!entries?.length) return (
<div className="border border-dashed border-[#DFE1E6] rounded p-6 text-center text-xs text-[#5E6C84]"> <div className="border border-dashed border-[var(--c-border)] rounded p-6 text-center text-xs text-[var(--c-muted)]">
No files yet push your first commit to see them here. No files yet push your first commit to see them here.
</div> </div>
) )
@@ -47,19 +47,19 @@ export function TreeBrowser({ owner, repo, ref, path = '' }: TreeBrowserProps) {
const sorted = [...dirs, ...files] const sorted = [...dirs, ...files]
return ( return (
<div className="border border-[#DFE1E6] rounded overflow-hidden bg-white"> <div className="border border-[var(--c-border)] rounded overflow-hidden bg-[var(--c-surface)]">
{/* Path breadcrumb inside tree */} {/* Path breadcrumb inside tree */}
{path && ( {path && (
<div className="flex items-center gap-1 px-3 py-2 bg-[#F4F5F7] border-b border-[#DFE1E6] text-xs text-[#5E6C84]"> <div className="flex items-center gap-1 px-3 py-2 bg-[var(--c-surface-muted)] border-b border-[var(--c-border)] text-xs text-[var(--c-muted)]">
<Link to={`/repos/${owner}/${repo}`} className="hover:text-[#0052CC]">{repo}</Link> <Link to={`/repos/${owner}/${repo}`} className="hover:text-[var(--c-brand)]">{repo}</Link>
{path.split('/').map((seg, i, arr) => { {path.split('/').map((seg, i, arr) => {
const partial = arr.slice(0, i + 1).join('/') const partial = arr.slice(0, i + 1).join('/')
return ( return (
<span key={partial} className="flex items-center gap-1"> <span key={partial} className="flex items-center gap-1">
<span>/</span> <span>/</span>
{i < arr.length - 1 {i < arr.length - 1
? <Link to={`/repos/${owner}/${repo}?path=${partial}&ref=${ref}`} className="hover:text-[#0052CC]">{seg}</Link> ? <Link to={`/repos/${owner}/${repo}?path=${partial}&ref=${ref}`} className="hover:text-[var(--c-brand)]">{seg}</Link>
: <span className="text-[#172B4D] font-medium">{seg}</span> : <span className="text-[var(--c-text)] font-medium">{seg}</span>
} }
</span> </span>
) )
@@ -82,36 +82,36 @@ export function TreeBrowser({ owner, repo, ref, path = '' }: TreeBrowserProps) {
: `/repos/${owner}/${repo}/blob?ref=${encodeURIComponent(ref)}&path=${encodeURIComponent(entryPath)}` : `/repos/${owner}/${repo}/blob?ref=${encodeURIComponent(ref)}&path=${encodeURIComponent(entryPath)}`
return ( return (
<tr key={entry.hash} className="border-b border-[#DFE1E6] last:border-b-0 hover:bg-[#FAFBFC]"> <tr key={entry.hash} className="border-b border-[var(--c-border)] last:border-b-0 hover:bg-[var(--c-surface-raised)]">
{/* Name */} {/* Name */}
<td className="px-3 py-2"> <td className="px-3 py-2">
<div className="flex items-center gap-2"> <div className="flex items-center gap-2">
{isDir ? ( {isDir ? (
<svg width="16" height="16" fill="#0052CC" viewBox="0 0 24 24" className="shrink-0"> <svg width="16" height="16" fill="var(--c-brand)" viewBox="0 0 24 24" className="shrink-0">
<path d="M19.5 21h-15A2.25 2.25 0 0 1 2.25 18.75V6.75A2.25 2.25 0 0 1 4.5 4.5h4.086c.398 0 .779.158 1.06.44l1.415 1.414c.28.281.661.44 1.06.44H19.5A2.25 2.25 0 0 1 21.75 9v9.75A2.25 2.25 0 0 1 19.5 21Z" /> <path d="M19.5 21h-15A2.25 2.25 0 0 1 2.25 18.75V6.75A2.25 2.25 0 0 1 4.5 4.5h4.086c.398 0 .779.158 1.06.44l1.415 1.414c.28.281.661.44 1.06.44H19.5A2.25 2.25 0 0 1 21.75 9v9.75A2.25 2.25 0 0 1 19.5 21Z" />
</svg> </svg>
) : ( ) : (
<svg width="16" height="16" fill="none" stroke="#5E6C84" strokeWidth="1.5" viewBox="0 0 24 24" className="shrink-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="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" /> <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> </svg>
)} )}
<Link <Link
to={href} to={href}
className={isDir ? 'text-[#0052CC] hover:underline font-medium' : 'text-[#172B4D] hover:text-[#0052CC]'} className={isDir ? 'text-[var(--c-brand)] hover:underline font-medium' : 'text-[var(--c-text)] hover:text-[var(--c-brand)]'}
> >
{entry.name} {entry.name}
</Link> </Link>
{!isDir && entry.size > 0 && ( {!isDir && entry.size > 0 && (
<span className="text-[10px] text-[#5E6C84] hidden sm:inline">{formatSize(entry.size)}</span> <span className="text-[10px] text-[var(--c-muted)] hidden sm:inline">{formatSize(entry.size)}</span>
)} )}
</div> </div>
</td> </td>
{/* Commit message */} {/* Commit message */}
<td className="px-3 py-2 text-xs text-[#5E6C84] truncate max-w-0 hidden sm:table-cell"> <td className="px-3 py-2 text-xs text-[var(--c-muted)] truncate max-w-0 hidden sm:table-cell">
<span className="truncate block" title={entry.commitMsg}>{entry.commitMsg}</span> <span className="truncate block" title={entry.commitMsg}>{entry.commitMsg}</span>
</td> </td>
{/* Date */} {/* Date */}
<td className="px-3 py-2 text-xs text-[#5E6C84] whitespace-nowrap text-right"> <td className="px-3 py-2 text-xs text-[var(--c-muted)] whitespace-nowrap text-right">
{relativeTime(entry.commitDate)} {relativeTime(entry.commitDate)}
</td> </td>
</tr> </tr>
@@ -125,9 +125,9 @@ export function TreeBrowser({ owner, repo, ref, path = '' }: TreeBrowserProps) {
function TreeSkeleton() { function TreeSkeleton() {
return ( return (
<div className="border border-[#DFE1E6] rounded overflow-hidden bg-white"> <div className="border border-[var(--c-border)] rounded overflow-hidden bg-[var(--c-surface)]">
{Array.from({ length: 5 }).map((_, i) => ( {Array.from({ length: 5 }).map((_, i) => (
<div key={i} className="flex items-center gap-3 px-3 py-2.5 border-b border-[#DFE1E6] last:border-b-0"> <div key={i} className="flex items-center gap-3 px-3 py-2.5 border-b border-[var(--c-border)] last:border-b-0">
<Skeleton className="w-4 h-4 shrink-0" /> <Skeleton className="w-4 h-4 shrink-0" />
<Skeleton className={`h-4 ${i % 3 === 0 ? 'w-20' : i % 3 === 1 ? 'w-36' : 'w-28'}`} /> <Skeleton className={`h-4 ${i % 3 === 0 ? 'w-20' : i % 3 === 1 ? 'w-36' : 'w-28'}`} />
<Skeleton className="h-3 w-40 ml-auto hidden sm:block" /> <Skeleton className="h-3 w-40 ml-auto hidden sm:block" />
+16
View File
@@ -0,0 +1,16 @@
import { useEffect, useState } from 'react'
export function useDarkMode() {
const [dark, setDark] = useState(() => {
const stored = localStorage.getItem('fb_dark')
if (stored !== null) return stored === 'true'
return window.matchMedia('(prefers-color-scheme: dark)').matches
})
useEffect(() => {
document.documentElement.classList.toggle('dark', dark)
localStorage.setItem('fb_dark', String(dark))
}, [dark])
return { dark, toggle: () => setDark(d => !d) }
}
+59 -3
View File
@@ -1,15 +1,71 @@
@import "tailwindcss"; @import "tailwindcss";
/* Reset #root to full viewport — Tailwind handles all layout via utility classes */ /* Enable class-based dark mode for Tailwind v4 */
@variant dark (&:where(.dark, .dark *));
/* ── Semantic color tokens ───────────────────────────────────────────────── */
:root {
--c-text: #172B4D;
--c-muted: #5E6C84;
--c-subtle: #97A0AF;
--c-surface: #FFFFFF;
--c-surface-raised: #FAFBFC;
--c-surface-muted: #F4F5F7;
--c-border: #DFE1E6;
--c-brand: #0052CC;
--c-brand-hover: #0065FF;
--c-brand-focus: #4C9AFF;
--c-brand-tint: #DEEBFF;
--c-success: #00875A;
--c-danger: #DE350B;
--c-danger-dark: #BF2600;
--c-danger-tint: #FFEBE6;
--c-warning: #FF8B00;
}
.dark {
color-scheme: dark;
--c-text: #CBD5E1;
--c-muted: #94A3B8;
--c-subtle: #64748B;
--c-surface: #1E293B;
--c-surface-raised: #1A2535;
--c-surface-muted: #0F172A;
--c-border: #334155;
--c-brand: #3B82F6;
--c-brand-hover: #60A5FA;
--c-brand-focus: #93C5FD;
--c-brand-tint: #1E3A5F;
--c-success: #34D399;
--c-danger: #F87171;
--c-danger-dark: #EF4444;
--c-danger-tint: #451a1a;
--c-warning: #FBBF24;
}
body { body {
margin: 0; margin: 0;
font-family: system-ui, 'Segoe UI', Roboto, sans-serif; font-family: system-ui, 'Segoe UI', Roboto, sans-serif;
-webkit-font-smoothing: antialiased; -webkit-font-smoothing: antialiased;
-moz-osx-font-smoothing: grayscale; -moz-osx-font-smoothing: grayscale;
color: #172B4D; color: var(--c-text);
background: #FAFBFC; background: var(--c-surface-raised);
} }
#root { #root {
height: 100dvh; height: 100dvh;
} }
/* Native form elements in dark mode */
.dark input,
.dark textarea,
.dark select {
background-color: var(--c-surface);
color: var(--c-text);
border-color: var(--c-border);
color-scheme: dark;
}
.dark input::placeholder,
.dark textarea::placeholder {
color: var(--c-subtle);
}
+30 -30
View File
@@ -50,7 +50,7 @@ export default function BlobPage() {
} }
if (isLoading) return <div className="p-6"><RepoListSkeleton /></div> 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 lines = blob.content.split('\n')
const pathParts = filePath.split('/') const pathParts = filePath.split('/')
@@ -60,18 +60,18 @@ export default function BlobPage() {
{/* Breadcrumb */} {/* Breadcrumb */}
<div className="flex items-center gap-1 text-sm flex-wrap"> <div className="flex items-center gap-1 text-sm flex-wrap">
<Link to="/repos" className="text-[#0052CC] hover:underline">Repositories</Link> <Link to="/repos" className="text-[var(--c-brand)] hover:underline">Repositories</Link>
<span className="text-[#5E6C84]">/</span> <span className="text-[var(--c-muted)]">/</span>
<Link to={`/repos/${owner}/${repoName}`} className="text-[#0052CC] hover:underline">{repoName}</Link> <Link to={`/repos/${owner}/${repoName}`} className="text-[var(--c-brand)] hover:underline">{repoName}</Link>
{pathParts.map((seg, i) => { {pathParts.map((seg, i) => {
const partial = pathParts.slice(0, i + 1).join('/') const partial = pathParts.slice(0, i + 1).join('/')
const isLast = i === pathParts.length - 1 const isLast = i === pathParts.length - 1
return ( return (
<span key={partial} className="flex items-center gap-1"> <span key={partial} className="flex items-center gap-1">
<span className="text-[#5E6C84]">/</span> <span className="text-[var(--c-muted)]">/</span>
{isLast {isLast
? <span className="font-semibold text-[#172B4D]">{seg}</span> ? <span className="font-semibold text-[var(--c-text)]">{seg}</span>
: <Link to={`/repos/${owner}/${repoName}?path=${encodeURIComponent(partial)}&ref=${encodeURIComponent(branch)}`} className="text-[#0052CC] hover:underline">{seg}</Link> : <Link to={`/repos/${owner}/${repoName}?path=${encodeURIComponent(partial)}&ref=${encodeURIComponent(branch)}`} className="text-[var(--c-brand)] hover:underline">{seg}</Link>
} }
</span> </span>
) )
@@ -79,24 +79,24 @@ export default function BlobPage() {
</div> </div>
{/* File card */} {/* 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 */} {/* 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"> <div className="flex items-center gap-2 text-sm">
{/* Branch pill */} {/* 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"> <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" /> <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> </svg>
{branch} {branch}
</span> </span>
<span className="text-[#5E6C84]">{repoName}</span> <span className="text-[var(--c-muted)]">{repoName}</span>
<span className="text-[#5E6C84]">/</span> <span className="text-[var(--c-muted)]">/</span>
<span className="font-medium text-[#172B4D]">{fileName}</span> <span className="font-medium text-[var(--c-text)]">{fileName}</span>
<button <button
onClick={() => navigator.clipboard.writeText(filePath)} onClick={() => navigator.clipboard.writeText(filePath)}
className="text-[#5E6C84] hover:text-[#172B4D]" className="text-[var(--c-muted)] hover:text-[var(--c-text)]"
title="Copy path" title="Copy path"
> >
<svg width="14" height="14" fill="none" stroke="currentColor" strokeWidth="1.5" viewBox="0 0 24 24"> <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 && ( {isMarkdown && (
<button <button
onClick={() => setPreview(p => !p)} 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'} {preview ? 'Source' : 'Preview'}
</button> </button>
)} )}
<button <button
onClick={startEdit} 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"> <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" /> <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>
<button <button
onClick={() => navigator.clipboard.writeText(blob.content)} 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 Copy
</button> </button>
@@ -140,17 +140,17 @@ export default function BlobPage() {
<textarea <textarea
value={editContent} value={editContent}
onChange={e => setEditContent(e.target.value)} 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) }} style={{ minHeight: Math.max(300, lines.length * 20) }}
spellCheck={false} 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> <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 <input
value={commitMsg} value={commitMsg}
onChange={e => setCommitMsg(e.target.value)} 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…" placeholder="Describe your changes…"
/> />
</div> </div>
@@ -158,24 +158,24 @@ export default function BlobPage() {
<button <button
onClick={handleCommit} onClick={handleCommit}
disabled={updateBlob.isPending || !commitMsg.trim()} 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'} {updateBlob.isPending ? 'Committing…' : 'Commit changes'}
</button> </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 Cancel
</button> </button>
{updateBlob.isError && ( {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> </div>
</div> </div>
) : isMarkdown && preview ? ( ) : isMarkdown && preview ? (
<div className="px-6 py-5 prose prose-sm max-w-none text-[#172B4D] <div className="px-6 py-5 prose prose-sm max-w-none text-[var(--c-text)]
prose-headings:text-[#172B4D] prose-headings:font-semibold prose-headings:border-b prose-headings:border-[#DFE1E6] prose-headings:pb-1 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-[#0052CC] prose-code:bg-[#F4F5F7] prose-code:px-1 prose-code:rounded prose-a:text-[var(--c-brand)] prose-code:bg-[var(--c-surface-muted)] prose-code:px-1 prose-code:rounded
prose-pre:bg-[#F4F5F7] prose-pre:border prose-pre:border-[#DFE1E6] prose-pre: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> <ReactMarkdown remarkPlugins={[remarkGfm]}>{blob.content}</ReactMarkdown>
</div> </div>
) : ( ) : (
@@ -184,10 +184,10 @@ export default function BlobPage() {
<tbody> <tbody>
{lines.map((line, i) => ( {lines.map((line, i) => (
<tr key={i} className="hover:bg-[#FFFBDD]"> <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} {i + 1}
</td> </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> </tr>
))} ))}
</tbody> </tbody>
+11 -11
View File
@@ -22,29 +22,29 @@ export default function BranchesPage() {
return ( return (
<div className="max-w-4xl mx-auto px-4 md:px-6 py-6"> <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"> <div className="flex items-center gap-1 text-sm mb-4">
<Link to={`/repos/${owner}/${repo}`} className="text-[#0052CC] hover:underline">{repo}</Link> <Link to={`/repos/${owner}/${repo}`} className="text-[var(--c-brand)] hover:underline">{repo}</Link>
<span className="text-[#5E6C84]">/</span> <span className="text-[var(--c-muted)]">/</span>
<span className="font-semibold text-[#172B4D]">Branches</span> <span className="font-semibold text-[var(--c-text)]">Branches</span>
</div> </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>} {isLoading && <p className="text-sm text-[var(--c-muted)]">Loading branches</p>}
{isError && <p className="text-sm text-[#DE350B]">Failed to load branches.</p>} {isError && <p className="text-sm text-[var(--c-danger)]">Failed to load branches.</p>}
{!isLoading && !branches?.length && ( {!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 && ( {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) => ( {branches.map((branch, i) => (
<div key={branch.name} <div key={branch.name}
className={`flex items-center gap-3 px-4 py-3 ${i > 0 ? 'border-t border-[#DFE1E6]' : ''} hover:bg-[#FAFBFC]`}> 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="#5E6C84" strokeWidth="1.5" viewBox="0 0 24 24" className="shrink-0"> <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" /> <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> </svg>
<Link to={`/repos/${owner}/${repo}?ref=${branch.name}`} <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} {branch.name}
</Link> </Link>
</div> </div>
+12 -12
View File
@@ -27,32 +27,32 @@ export default function CommitsPage() {
return ( return (
<div className="max-w-4xl mx-auto px-4 md:px-6 py-6"> <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"> <div className="flex items-center gap-1 text-sm mb-4">
<Link to={`/repos/${owner}/${repo}`} className="text-[#0052CC] hover:underline">{repo}</Link> <Link to={`/repos/${owner}/${repo}`} className="text-[var(--c-brand)] hover:underline">{repo}</Link>
<span className="text-[#5E6C84]">/</span> <span className="text-[var(--c-muted)]">/</span>
<span className="font-semibold text-[#172B4D]">Commits</span> <span className="font-semibold text-[var(--c-text)]">Commits</span>
</div> </div>
{isLoading && <p className="text-sm text-[#5E6C84]">Loading commits</p>} {isLoading && <p className="text-sm text-[var(--c-muted)]">Loading commits</p>}
{isError && <p className="text-sm text-[#DE350B]">Failed to load commits.</p>} {isError && <p className="text-sm text-[var(--c-danger)]">Failed to load commits.</p>}
{!isLoading && !commits?.length && ( {!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 && ( {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) => ( {commits.map((commit, i) => (
<div key={commit.hash} <div key={commit.hash}
className={`flex items-start gap-4 px-4 py-3 ${i > 0 ? 'border-t border-[#DFE1E6]' : ''} hover:bg-[#FAFBFC]`}> 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-[#0052CC]/10 text-[#0052CC] flex items-center justify-center text-[10px] font-bold shrink-0 mt-0.5"> <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()} {commit.author?.[0]?.toUpperCase()}
</div> </div>
<div className="flex-1 min-w-0"> <div className="flex-1 min-w-0">
<p className="text-sm font-medium text-[#172B4D] truncate">{commit.message}</p> <p className="text-sm font-medium text-[var(--c-text)] truncate">{commit.message}</p>
<p className="text-xs text-[#5E6C84] mt-0.5"> <p className="text-xs text-[var(--c-muted)] mt-0.5">
{commit.author} · {new Date(commit.date).toLocaleDateString()} {commit.author} · {new Date(commit.date).toLocaleDateString()}
</p> </p>
</div> </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)} {commit.hash.slice(0, 7)}
</code> </code>
</div> </div>
+20 -20
View File
@@ -32,12 +32,12 @@ export default function CreateRepoPage() {
<div className="max-w-2xl mx-auto px-4 py-10"> <div className="max-w-2xl mx-auto px-4 py-10">
{/* Header */} {/* Header */}
<div className="flex items-center justify-between mb-6"> <div className="flex items-center justify-between mb-6">
<h1 className="text-xl font-semibold text-[#172B4D]">Create a new repository</h1> <h1 className="text-xl font-semibold text-[var(--c-text)]">Create a new repository</h1>
<Link to="/repos/import" className="text-sm text-[#0052CC] hover:underline"> <Link to="/repos/import" className="text-sm text-[var(--c-brand)] hover:underline">
Import repository Import repository
</Link> </Link>
</div> </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"> <form onSubmit={handleSubmit} className="space-y-5">
@@ -49,10 +49,10 @@ export default function CreateRepoPage() {
placeholder="my-repository" placeholder="my-repository"
required required
autoFocus 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) && ( {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> </Field>
@@ -63,11 +63,11 @@ export default function CreateRepoPage() {
type="checkbox" type="checkbox"
checked={isPrivate} checked={isPrivate}
onChange={e => setIsPrivate(e.target.checked)} 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> <div>
<span className="text-sm font-medium text-[#172B4D]">Private repository</span> <span className="text-sm font-medium text-[var(--c-text)]">Private repository</span>
<p className="text-xs text-[#0052CC] mt-0.5 leading-relaxed"> <p className="text-xs text-[var(--c-brand)] mt-0.5 leading-relaxed">
{isPrivate {isPrivate
? 'Uncheck to make this repository public. Public repositories typically contain open-source code and can be viewed by anyone.' ? '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.'} : 'Check to make this repository private. Only invited collaborators can see and push to it.'}
@@ -81,7 +81,7 @@ export default function CreateRepoPage() {
<select <select
value={initReadme} value={initReadme}
onChange={e => setInitReadme(e.target.value as 'none' | 'blank' | 'tutorial')} 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="none">No</option>
<option value="blank">Yes, blank</option> <option value="blank">Yes, blank</option>
@@ -95,7 +95,7 @@ export default function CreateRepoPage() {
value={defaultBranch} value={defaultBranch}
onChange={e => setDefaultBranch(e.target.value)} onChange={e => setDefaultBranch(e.target.value)}
placeholder="e.g., 'main'" 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> </Field>
@@ -104,7 +104,7 @@ export default function CreateRepoPage() {
<select <select
value={initGitignore ? 'yes' : 'no'} value={initGitignore ? 'yes' : 'no'}
onChange={e => setInitGitignore(e.target.value === 'yes')} 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="yes">Yes (recommended)</option>
<option value="no">No</option> <option value="no">No</option>
@@ -116,7 +116,7 @@ export default function CreateRepoPage() {
<button <button
type="button" type="button"
onClick={() => setShowAdvanced(s => !s)} 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 <svg
width="14" height="14" fill="none" stroke="currentColor" strokeWidth="2.5" viewBox="0 0 24 24" 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> </button>
{showAdvanced && ( {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"> <Field label="Description">
<textarea <textarea
value={description} value={description}
onChange={e => setDescription(e.target.value)} onChange={e => setDescription(e.target.value)}
rows={4} 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…" placeholder="Describe your repository…"
/> />
</Field> </Field>
@@ -143,23 +143,23 @@ export default function CreateRepoPage() {
</div> </div>
{createRepo.isError && ( {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.'} {createRepo.error instanceof Error ? createRepo.error.message : 'Failed to create repository.'}
</p> </p>
)} )}
{/* Footer actions */} {/* 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 <Link
to="/repos" 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 Cancel
</Link> </Link>
<button <button
type="submit" type="submit"
disabled={createRepo.isPending || !name.trim()} 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'} {createRepo.isPending ? 'Creating…' : 'Create repository'}
</button> </button>
@@ -172,8 +172,8 @@ export default function CreateRepoPage() {
function Field({ label, required, children }: { label: string; required?: boolean; children: React.ReactNode }) { function Field({ label, required, children }: { label: string; required?: boolean; children: React.ReactNode }) {
return ( return (
<div className="grid grid-cols-[180px_1fr] gap-4 items-start"> <div className="grid grid-cols-[180px_1fr] gap-4 items-start">
<label className="text-sm text-[#172B4D] text-right pt-2 leading-tight"> <label className="text-sm text-[var(--c-text)] text-right pt-2 leading-tight">
{label}{required && <span className="text-[#DE350B] ml-0.5">*</span>} {label}{required && <span className="text-[var(--c-danger)] ml-0.5">*</span>}
</label> </label>
<div>{children}</div> <div>{children}</div>
</div> </div>
+19 -19
View File
@@ -16,23 +16,23 @@ export default function DashboardPage() {
{/* Hero — only when no repos yet */} {/* Hero — only when no repos yet */}
{!reposLoading && !hasRepos && isAuthenticated && ( {!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"> <div className="flex items-center gap-8 p-8">
<HeroIllustration /> <HeroIllustration />
<div> <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}` : ''}! Welcome to ForgeBucket{user?.username ? `, ${user.username}` : ''}!
</h1> </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. Get started by creating your first repository, pushing code, and collaborating through pull requests.
</p> </p>
<div className="flex items-center gap-3 mt-5"> <div className="flex items-center gap-3 mt-5">
<Link to="/repos" <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 Create repository
</Link> </Link>
<Link to="/explore" <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 Explore
</Link> </Link>
</div> </div>
@@ -44,22 +44,22 @@ export default function DashboardPage() {
{/* Recent repositories */} {/* Recent repositories */}
<section> <section>
<div className="flex items-center justify-between mb-3"> <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 Recent repositories
<Link to="/repos" <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> </Link>
</h2> </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> </div>
{reposLoading ? ( {reposLoading ? (
<RepoListSkeleton /> <RepoListSkeleton />
) : !repos?.length ? ( ) : !repos?.length ? (
<div className="border border-dashed border-[#DFE1E6] rounded p-6 text-center"> <div className="border border-dashed border-[var(--c-border)] rounded p-6 text-center">
<p className="text-sm text-[#5E6C84]">No repositories yet.</p> <p className="text-sm text-[var(--c-muted)]">No repositories yet.</p>
<Link to="/repos" className="text-xs text-[#0052CC] hover:underline mt-1 inline-block"> <Link to="/repos" className="text-xs text-[var(--c-brand)] hover:underline mt-1 inline-block">
Create your first repository Create your first repository
</Link> </Link>
</div> </div>
@@ -74,7 +74,7 @@ export default function DashboardPage() {
{repos && repos.length > 0 && ( {repos && repos.length > 0 && (
<section> <section>
<div className="flex items-center justify-between mb-3"> <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> </div>
<PullRequestSummary repos={repos.map(r => ({ owner: r.ownerName, name: r.name }))} /> <PullRequestSummary repos={repos.map(r => ({ owner: r.ownerName, name: r.name }))} />
</section> </section>
@@ -93,8 +93,8 @@ function PullRequestSummary({ repos }: { repos: { owner: string; name: string }[
if (!open.length) { if (!open.length) {
return ( return (
<div className="border border-[#DFE1E6] rounded p-6 text-center bg-white"> <div className="border border-[var(--c-border)] rounded p-6 text-center bg-[var(--c-surface)]">
<p className="text-sm text-[#5E6C84]">You have no open pull requests.</p> <p className="text-sm text-[var(--c-muted)]">You have no open pull requests.</p>
</div> </div>
) )
} }
@@ -105,14 +105,14 @@ function PullRequestSummary({ repos }: { repos: { owner: string; name: string }[
<Link <Link
key={pr.id} key={pr.id}
to={`/repos/${first.owner}/${first.name}/pulls/${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" /> <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> </svg>
<div className="flex-1 min-w-0"> <div className="flex-1 min-w-0">
<p className="text-sm font-medium text-[#172B4D] truncate">{pr.title}</p> <p className="text-sm font-medium text-[var(--c-text)] truncate">{pr.title}</p>
<p className="text-xs text-[#5E6C84] mt-0.5"> <p className="text-xs text-[var(--c-muted)] mt-0.5">
{first.name} · {pr.sourceBranch} {pr.targetBranch} {first.name} · {pr.sourceBranch} {pr.targetBranch}
</p> </p>
</div> </div>
@@ -124,7 +124,7 @@ function PullRequestSummary({ repos }: { repos: { owner: string; name: string }[
function HeroIllustration() { function HeroIllustration() {
return ( 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"> <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" /> <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> </svg>
+5 -5
View File
@@ -1,14 +1,14 @@
export default function ExplorePage() { export default function ExplorePage() {
return ( return (
<div className="max-w-4xl mx-auto px-4 md:px-6 py-6 space-y-6"> <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> <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-[#DFE1E6] rounded text-center gap-3"> <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="#97A0AF" strokeWidth="1" viewBox="0 0 24 24"> <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" /> <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> </svg>
<div> <div>
<p className="text-sm font-medium text-[#172B4D]">Explore public repositories</p> <p className="text-sm font-medium text-[var(--c-text)]">Explore public repositories</p>
<p className="text-xs text-[#5E6C84] mt-1"> <p className="text-xs text-[var(--c-muted)] mt-1">
Federated discovery across ForgeBucket instances coming soon. Federated discovery across ForgeBucket instances coming soon.
</p> </p>
</div> </div>
+24 -24
View File
@@ -42,18 +42,18 @@ export default function ImportRepoPage() {
<div className="max-w-2xl mx-auto px-4 py-10"> <div className="max-w-2xl mx-auto px-4 py-10">
{/* Header */} {/* Header */}
<div className="flex items-center justify-between mb-6"> <div className="flex items-center justify-between mb-6">
<h1 className="text-xl font-semibold text-[#172B4D]">Import existing code</h1> <h1 className="text-xl font-semibold text-[var(--c-text)]">Import existing code</h1>
<Link to="/repos/new" className="text-sm text-[#0052CC] hover:underline"> <Link to="/repos/new" className="text-sm text-[var(--c-brand)] hover:underline">
Create new repository Create new repository
</Link> </Link>
</div> </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"> <form onSubmit={handleSubmit} className="space-y-6">
{/* Old repository section */} {/* Old repository section */}
<div> <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"> <div className="space-y-4">
<Field label="URL" required> <Field label="URL" required>
<input <input
@@ -62,7 +62,7 @@ export default function ImportRepoPage() {
placeholder="https://github.com/user/repo.git" placeholder="https://github.com/user/repo.git"
required required
autoFocus 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> </Field>
@@ -72,9 +72,9 @@ export default function ImportRepoPage() {
type="checkbox" type="checkbox"
checked={requiresAuth} checked={requiresAuth}
onChange={e => setRequiresAuth(e.target.checked)} 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> </label>
{requiresAuth && ( {requiresAuth && (
@@ -83,14 +83,14 @@ export default function ImportRepoPage() {
value={authUser} value={authUser}
onChange={e => setAuthUser(e.target.value)} onChange={e => setAuthUser(e.target.value)}
placeholder="Username" 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 <input
type="password" type="password"
value={authPass} value={authPass}
onChange={e => setAuthPass(e.target.value)} onChange={e => setAuthPass(e.target.value)}
placeholder="Password or access token" 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> </div>
)} )}
@@ -98,11 +98,11 @@ export default function ImportRepoPage() {
</div> </div>
</div> </div>
<div className="border-t border-[#DFE1E6]" /> <div className="border-t border-[var(--c-border)]" />
{/* New repository section */} {/* New repository section */}
<div> <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"> <div className="space-y-4">
<Field label="Repository name" required> <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, '-'))} onChange={e => setName(e.target.value.replace(/[^a-zA-Z0-9._-]/g, '-'))}
placeholder="my-repository" placeholder="my-repository"
required 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> </Field>
@@ -121,11 +121,11 @@ export default function ImportRepoPage() {
type="checkbox" type="checkbox"
checked={isPrivate} checked={isPrivate}
onChange={e => setIsPrivate(e.target.checked)} 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> <div>
<span className="text-sm font-medium text-[#172B4D]">Private repository</span> <span className="text-sm font-medium text-[var(--c-text)]">Private repository</span>
<p className="text-xs text-[#0052CC] mt-0.5 leading-relaxed"> <p className="text-xs text-[var(--c-brand)] mt-0.5 leading-relaxed">
{isPrivate {isPrivate
? 'Uncheck to make this repository public. Public repositories typically contain open-source code and can be viewed by anyone.' ? '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.'} : 'Check to make this repository private. Only invited collaborators can see and push to it.'}
@@ -139,7 +139,7 @@ export default function ImportRepoPage() {
<button <button
type="button" type="button"
onClick={() => setShowAdvanced(s => !s)} 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 <svg
width="14" height="14" fill="none" stroke="currentColor" strokeWidth="2.5" viewBox="0 0 24 24" 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> </button>
{showAdvanced && ( {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"> <Field label="Description">
<textarea <textarea
value={description} value={description}
onChange={e => setDescription(e.target.value)} onChange={e => setDescription(e.target.value)}
rows={4} 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…" placeholder="Describe your repository…"
/> />
</Field> </Field>
@@ -168,23 +168,23 @@ export default function ImportRepoPage() {
</div> </div>
{importRepo.isError && ( {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.'} {importRepo.error instanceof Error ? importRepo.error.message : 'Import failed.'}
</p> </p>
)} )}
{/* Footer */} {/* 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 <Link
to="/repos" 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 Cancel
</Link> </Link>
<button <button
type="submit" type="submit"
disabled={importRepo.isPending || !url.trim() || !name.trim()} 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'} {importRepo.isPending ? 'Importing…' : 'Import repository'}
</button> </button>
@@ -197,8 +197,8 @@ export default function ImportRepoPage() {
function Field({ label, required, children }: { label: string; required?: boolean; children: React.ReactNode }) { function Field({ label, required, children }: { label: string; required?: boolean; children: React.ReactNode }) {
return ( return (
<div className="grid grid-cols-[180px_1fr] gap-4 items-start"> <div className="grid grid-cols-[180px_1fr] gap-4 items-start">
<label className="text-sm text-[#172B4D] text-right pt-2 leading-tight"> <label className="text-sm text-[var(--c-text)] text-right pt-2 leading-tight">
{label}{required && <span className="text-[#DE350B] ml-0.5">*</span>} {label}{required && <span className="text-[var(--c-danger)] ml-0.5">*</span>}
</label> </label>
<div>{children}</div> <div>{children}</div>
</div> </div>
+12 -12
View File
@@ -34,44 +34,44 @@ export default function LoginPage() {
} }
return ( 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="w-full max-w-sm">
<div className="text-center mb-8"> <div className="text-center mb-8">
<h1 className="text-2xl font-bold text-[#172B4D]">ForgeBucket</h1> <h1 className="text-2xl font-bold text-[var(--c-text)]">ForgeBucket</h1>
<p className="text-sm text-[#5E6C84] mt-1">Sign in to your account</p> <p className="text-sm text-[var(--c-muted)] mt-1">Sign in to your account</p>
</div> </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"> <form onSubmit={handleSubmit} className="flex flex-col gap-4">
<div> <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 <input
value={username} onChange={e => setUsername(e.target.value)} value={username} onChange={e => setUsername(e.target.value)}
required autoFocus autoComplete="username" 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>
<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 <input
type="password" value={password} onChange={e => setPassword(e.target.value)} type="password" value={password} onChange={e => setPassword(e.target.value)}
required autoComplete="current-password" 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> </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 <button
type="submit" disabled={loading} 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'} {loading ? 'Signing in…' : 'Sign in'}
</button> </button>
</form> </form>
</div> </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?{' '} 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> </p>
</div> </div>
</div> </div>
+11 -11
View File
@@ -28,35 +28,35 @@ export default function PRDetailPage() {
} }
if (isError || !pr) { 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' const statusColor = pr.status === 'open'
? 'bg-[#E3FCEF] text-[#006644] border-[#79F2C0]' ? 'bg-[#E3FCEF] text-[#006644] border-[#79F2C0]'
: pr.status === 'merged' : pr.status === 'merged'
? 'bg-[#EAE6FF] text-[#403294] border-[#C0B6F2]' ? '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 ( return (
<div className="max-w-5xl mx-auto px-4 md:px-6 py-6 space-y-6"> <div className="max-w-5xl mx-auto px-4 md:px-6 py-6 space-y-6">
{/* Breadcrumb */} {/* Breadcrumb */}
<div className="flex items-center gap-1 text-sm flex-wrap"> <div className="flex items-center gap-1 text-sm flex-wrap">
<Link to={`/repos/${owner}/${repo}`} className="text-[#0052CC] hover:underline">{repo}</Link> <Link to={`/repos/${owner}/${repo}`} className="text-[var(--c-brand)] hover:underline">{repo}</Link>
<span className="text-[#5E6C84]">/</span> <span className="text-[var(--c-muted)]">/</span>
<Link to={`/repos/${owner}/${repo}/pulls`} className="text-[#0052CC] hover:underline">Pull requests</Link> <Link to={`/repos/${owner}/${repo}/pulls`} className="text-[var(--c-brand)] hover:underline">Pull requests</Link>
<span className="text-[#5E6C84]">/</span> <span className="text-[var(--c-muted)]">/</span>
<span className="text-[#172B4D]">#{pr.id}</span> <span className="text-[var(--c-text)]">#{pr.id}</span>
</div> </div>
{/* Title + status */} {/* Title + status */}
<div> <div>
<div className="flex items-center gap-3 flex-wrap"> <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)}> <span className={cn('text-xs font-semibold px-2 py-0.5 rounded-full border', statusColor)}>
{pr.status} {pr.status}
</span> </span>
</div> </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> #{pr.id} · <span className="font-mono">{pr.sourceBranch}</span>
{' → '} {' → '}
<span className="font-mono">{pr.targetBranch}</span> <span className="font-mono">{pr.targetBranch}</span>
@@ -65,14 +65,14 @@ export default function PRDetailPage() {
{/* Body */} {/* Body */}
{pr.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} {pr.body}
</div> </div>
)} )}
{/* Diff placeholder */} {/* Diff placeholder */}
<div> <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"> <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" /> <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> </svg>
+5 -5
View File
@@ -10,10 +10,10 @@ export default function PRsPage() {
return ( return (
<div className="max-w-4xl mx-auto px-4 md:px-6 py-6"> <div className="max-w-4xl mx-auto px-4 md:px-6 py-6">
<div className="flex items-center justify-between mb-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>
<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 => ( {(['open', 'merged', 'closed'] as PRStatus[]).map(s => (
<button <button
key={s} key={s}
@@ -21,8 +21,8 @@ export default function PRsPage() {
className={cn( className={cn(
'px-4 py-2 text-sm font-medium capitalize border-b-2 -mb-px transition-colors min-h-[44px]', 'px-4 py-2 text-sm font-medium capitalize border-b-2 -mb-px transition-colors min-h-[44px]',
status === s status === s
? 'border-[#0052CC] text-[#0052CC]' ? 'border-[var(--c-brand)] text-[var(--c-brand)]'
: 'border-transparent text-[#5E6C84] hover:text-[#172B4D]', : 'border-transparent text-[var(--c-muted)] hover:text-[var(--c-text)]',
)} )}
> >
{s} {s}
@@ -30,7 +30,7 @@ export default function PRsPage() {
))} ))}
</div> </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. Navigate to a repository to view its pull requests.
</p> </p>
</div> </div>
+6 -6
View File
@@ -1,16 +1,16 @@
export default function PipelinesPage() { export default function PipelinesPage() {
return ( return (
<div className="max-w-4xl mx-auto px-4 md:px-6 py-6 space-y-6"> <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> <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-[#DFE1E6] rounded text-center gap-3"> <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="#97A0AF" strokeWidth="1" viewBox="0 0 24 24"> <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" /> <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> </svg>
<div> <div>
<p className="text-sm font-medium text-[#172B4D]">No pipelines yet</p> <p className="text-sm font-medium text-[var(--c-text)]">No pipelines yet</p>
<p className="text-xs text-[#5E6C84] mt-1 max-w-xs"> <p className="text-xs text-[var(--c-muted)] mt-1 max-w-xs">
Pipelines run automatically when you push to a repository.<br /> 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> </p>
</div> </div>
</div> </div>
+17 -17
View File
@@ -16,65 +16,65 @@ export default function ProfilePage() {
return ( return (
<div className="max-w-2xl mx-auto px-4 md:px-6 py-6 space-y-8"> <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 */} {/* Avatar */}
<div className="flex items-center gap-5"> <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()} {user?.username?.[0]?.toUpperCase()}
</div> </div>
<div> <div>
<p className="text-sm font-semibold text-[#172B4D]">{user?.username}</p> <p className="text-sm font-semibold text-[var(--c-text)]">{user?.username}</p>
<p className="text-xs text-[#5E6C84]">{user?.email}</p> <p className="text-xs text-[var(--c-muted)]">{user?.email}</p>
</div> </div>
</div> </div>
{/* Edit form */} {/* Edit form */}
<section className="border border-[#DFE1E6] rounded-lg overflow-hidden"> <section className="border border-[var(--c-border)] rounded-lg overflow-hidden">
<div className="px-5 py-4 border-b border-[#DFE1E6] bg-[#FAFBFC]"> <div className="px-5 py-4 border-b border-[var(--c-border)] bg-[var(--c-surface-raised)]">
<h2 className="text-sm font-semibold text-[#172B4D]">Edit profile</h2> <h2 className="text-sm font-semibold text-[var(--c-text)]">Edit profile</h2>
</div> </div>
<form onSubmit={handleSave} className="px-5 py-5 flex flex-col gap-5"> <form onSubmit={handleSave} className="px-5 py-5 flex flex-col gap-5">
<div> <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 <input
value={displayName} value={displayName}
onChange={e => setDisplayName(e.target.value)} 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>
<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 <textarea
value={bio} value={bio}
onChange={e => setBio(e.target.value)} onChange={e => setBio(e.target.value)}
rows={3} rows={3}
placeholder="Tell others a little about yourself" 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>
<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 <input
value={user?.email ?? ''} value={user?.email ?? ''}
disabled 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>
<div className="flex items-center gap-3"> <div className="flex items-center gap-3">
<button <button
type="submit" 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 Save changes
</button> </button>
{saved && ( {saved && (
<span className="text-xs text-[#00875A] font-medium">Saved!</span> <span className="text-xs text-[var(--c-success)] font-medium">Saved!</span>
)} )}
</div> </div>
</form> </form>
+14 -14
View File
@@ -37,41 +37,41 @@ export default function RegisterPage() {
} }
return ( 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="w-full max-w-sm">
<div className="text-center mb-8"> <div className="text-center mb-8">
<h1 className="text-2xl font-bold text-[#172B4D]">ForgeBucket</h1> <h1 className="text-2xl font-bold text-[var(--c-text)]">ForgeBucket</h1>
<p className="text-sm text-[#5E6C84] mt-1">Create your account</p> <p className="text-sm text-[var(--c-muted)] mt-1">Create your account</p>
</div> </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"> <form onSubmit={handleSubmit} className="flex flex-col gap-4">
<div> <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 <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>
<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 <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>
<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 <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> </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} <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'} {loading ? 'Creating account…' : 'Create account'}
</button> </button>
</form> </form>
</div> </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?{' '} 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> </p>
</div> </div>
</div> </div>
+20 -20
View File
@@ -27,46 +27,46 @@ export default function RepoIssuesPage() {
return ( return (
<div className="max-w-4xl mx-auto px-4 md:px-6 py-6"> <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"> <div className="flex items-center gap-1 text-sm mb-4">
<Link to={`/repos/${owner}/${repo}`} className="text-[#0052CC] hover:underline">{repo}</Link> <Link to={`/repos/${owner}/${repo}`} className="text-[var(--c-brand)] hover:underline">{repo}</Link>
<span className="text-[#5E6C84]">/</span> <span className="text-[var(--c-muted)]">/</span>
<span className="font-semibold text-[#172B4D]">Issues</span> <span className="font-semibold text-[var(--c-text)]">Issues</span>
</div> </div>
<div className="flex items-center justify-between mb-4"> <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)} <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 New issue
</button> </button>
</div> </div>
{showNew && ( {showNew && (
<form onSubmit={handleCreate} className="mb-6 p-5 border border-[#4C9AFF] rounded bg-white space-y-3"> <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-[#172B4D]">New Issue</h2> <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 <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} <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"> <div className="flex gap-2">
<button type="submit" disabled={createIssue.isPending || !title.trim()} <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'} {createIssue.isPending ? 'Submitting…' : 'Submit'}
</button> </button>
<button type="button" onClick={() => setShowNew(false)} <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 Cancel
</button> </button>
</div> </div>
</form> </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 => { {(['open', 'closed'] as IssueState[]).map(s => {
const count = issues?.filter(i => i.state === s).length ?? 0 const count = issues?.filter(i => i.state === s).length ?? 0
return ( return (
<button key={s} onClick={() => setState(s)} <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]', 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})`} {s} {count > 0 && `(${count})`}
</button> </button>
) )
@@ -74,21 +74,21 @@ export default function RepoIssuesPage() {
</div> </div>
{isLoading ? ( {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 ? ( ) : !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"> <div className="flex flex-col gap-2">
{issues.map(issue => ( {issues.map(issue => (
<div key={issue.id} <div key={issue.id}
className="flex items-start gap-3 p-4 border border-[#DFE1E6] rounded hover:bg-[#FAFBFC]"> 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' ? '#00875A' : '#5E6C84'} <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"> 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"/> <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> </svg>
<div className="flex-1 min-w-0"> <div className="flex-1 min-w-0">
<p className="text-sm font-medium text-[#172B4D]">#{issue.number} {issue.title}</p> <p className="text-sm font-medium text-[var(--c-text)]">#{issue.number} {issue.title}</p>
<p className="text-xs text-[#5E6C84] mt-0.5"> <p className="text-xs text-[var(--c-muted)] mt-0.5">
opened by {issue.authorName} · {new Date(issue.createdAt).toLocaleDateString()} opened by {issue.authorName} · {new Date(issue.createdAt).toLocaleDateString()}
</p> </p>
</div> </div>
@@ -97,7 +97,7 @@ export default function RepoIssuesPage() {
? closeIssue.mutate(issue.number) ? closeIssue.mutate(issue.number)
: reopenIssue.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'} {issue.state === 'open' ? 'Close' : 'Reopen'}
</button> </button>
</div> </div>
+15 -15
View File
@@ -15,31 +15,31 @@ export default function RepoPRsPage() {
return ( return (
<div className="max-w-4xl mx-auto px-4 md:px-6 py-6"> <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"> <div className="flex items-center gap-1 text-sm mb-4">
<Link to="/repos" className="text-[#0052CC] hover:underline">Repos</Link> <Link to="/repos" className="text-[var(--c-brand)] hover:underline">Repos</Link>
<span className="text-[#5E6C84]">/</span> <span className="text-[var(--c-muted)]">/</span>
<Link to={`/repos/${owner}/${repo}`} className="text-[#0052CC] hover:underline">{repo}</Link> <Link to={`/repos/${owner}/${repo}`} className="text-[var(--c-brand)] hover:underline">{repo}</Link>
<span className="text-[#5E6C84]">/</span> <span className="text-[var(--c-muted)]">/</span>
<span className="font-semibold text-[#172B4D]">Pull requests</span> <span className="font-semibold text-[var(--c-text)]">Pull requests</span>
</div> </div>
<div className="flex items-center justify-between mb-4"> <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 <Link
to={`/repos/${owner}/${repo}/pulls/new`} 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 New PR
</Link> </Link>
</div> </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 => { {(['open', 'merged', 'closed'] as PRStatus[]).map(s => {
const count = prs?.filter(p => p.status === s).length ?? 0 const count = prs?.filter(p => p.status === s).length ?? 0
return ( return (
<button key={s} onClick={() => setStatus(s)} <button key={s} onClick={() => setStatus(s)}
className={cn( className={cn(
'px-4 py-2 text-sm font-medium capitalize border-b-2 -mb-px min-h-[44px]', '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>} {s} {count > 0 && <span className="ml-1 text-xs">({count})</span>}
</button> </button>
@@ -48,9 +48,9 @@ export default function RepoPRsPage() {
</div> </div>
{isLoading ? <PRListSkeleton /> : isError ? ( {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 ? ( ) : !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"> <div className="flex flex-col gap-2">
{filtered.map(pr => <PRRow key={pr.id} pr={pr} owner={owner} repo={repo} />)} {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]' ? 'bg-[#E3FCEF] text-[#006644] border-[#79F2C0]'
: pr.status === 'merged' : pr.status === 'merged'
? 'bg-[#EAE6FF] text-[#403294] border-[#C0B6F2]' ? '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 ( return (
<Link <Link
to={`/repos/${owner}/${repo}/pulls/${pr.id}`} 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)}> <span className={cn('text-[10px] font-semibold px-2 py-0.5 rounded-full border shrink-0 mt-0.5', statusColor)}>
{pr.status} {pr.status}
</span> </span>
<div className="flex-1 min-w-0"> <div className="flex-1 min-w-0">
<p className="text-sm font-medium text-[#172B4D] truncate">{pr.title}</p> <p className="text-sm font-medium text-[var(--c-text)] truncate">{pr.title}</p>
<p className="text-xs text-[#5E6C84] mt-0.5"> <p className="text-xs text-[var(--c-muted)] mt-0.5">
#{pr.id} · {pr.sourceBranch} {pr.targetBranch} #{pr.id} · {pr.sourceBranch} {pr.targetBranch}
</p> </p>
</div> </div>
+42 -42
View File
@@ -35,7 +35,7 @@ export default function RepoPage() {
}, []) }, [])
if (isLoading) return <div className="p-6"><RepoListSkeleton /></div> 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 branch = ref || repo.defaultBranch
const cloneUrl = `${window.location.origin}/${owner}/${repoName}.git` const cloneUrl = `${window.location.origin}/${owner}/${repoName}.git`
@@ -51,26 +51,26 @@ export default function RepoPage() {
{/* Header row */} {/* Header row */}
<div className="flex items-start justify-between gap-4 flex-wrap"> <div className="flex items-start justify-between gap-4 flex-wrap">
<div> <div>
<div className="flex items-center gap-2 text-sm text-[#5E6C84] mb-1"> <div className="flex items-center gap-2 text-sm text-[var(--c-muted)] mb-1">
<Link to="/repos" className="hover:text-[#0052CC]">Repositories</Link> <Link to="/repos" className="hover:text-[var(--c-brand)]">Repositories</Link>
<span>/</span> <span>/</span>
<RepoAvatar ownerName={owner} name={repo.name} avatarUrl={repo.avatarUrl} size={20} /> <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 && ( {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 Private
</span> </span>
)} )}
</div> </div>
{repo.description && ( {repo.description && (
<p className="text-sm text-[#5E6C84]">{repo.description}</p> <p className="text-sm text-[var(--c-muted)]">{repo.description}</p>
)} )}
</div> </div>
<div className="flex items-center gap-2 shrink-0"> <div className="flex items-center gap-2 shrink-0">
<Link <Link
to={`/repos/${owner}/${repoName}/pulls`} 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 Pull requests
</Link> </Link>
@@ -79,7 +79,7 @@ export default function RepoPage() {
<div className="relative" ref={cloneRef}> <div className="relative" ref={cloneRef}>
<button <button
onClick={() => setShowClone(s => !s)} 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"> <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" /> <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> </svg>
</button> </button>
{showClone && ( {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"> <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-[#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>
<div className="flex items-center gap-2 bg-[#F4F5F7] border border-[#DFE1E6] rounded px-3 py-2"> <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-[#172B4D] flex-1 truncate">{cloneUrl}</code> <code className="text-xs text-[var(--c-text)] flex-1 truncate">{cloneUrl}</code>
<button <button
onClick={() => navigator.clipboard.writeText(cloneUrl)} 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 Copy
</button> </button>
@@ -116,7 +116,7 @@ export default function RepoPage() {
<div className="relative" ref={branchRef}> <div className="relative" ref={branchRef}>
<button <button
onClick={() => setShowBranches(s => !s)} 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"> <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" /> <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> </svg>
</button> </button>
{showBranches && ( {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="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-[#DFE1E6] bg-[#F4F5F7]"> <div className="px-3 py-2 border-b border-[var(--c-border)] bg-[var(--c-surface-muted)]">
<p className="text-xs font-semibold text-[#5E6C84]">Switch branch</p> <p className="text-xs font-semibold text-[var(--c-muted)]">Switch branch</p>
</div> </div>
<ul> <ul>
{branches?.map(b => ( {branches?.map(b => (
<li key={b.name}> <li key={b.name}>
<button <button
onClick={() => switchBranch(b.name)} 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 && ( {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" /> <path strokeLinecap="round" strokeLinejoin="round" d="m4.5 12.75 6 6 9-13.5" />
</svg> </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> </button>
</li> </li>
))} ))}
{!branches?.length && ( {!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> </ul>
</div> </div>
@@ -156,10 +156,10 @@ export default function RepoPage() {
</div> </div>
{/* Nav links */} {/* 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}/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-[#5E6C84] hover:text-[#172B4D] px-2 py-1">Branches</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-[#5E6C84] hover:text-[#172B4D] px-2 py-1">Issues</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-[#5E6C84] hover:text-[#172B4D] px-2 py-1 ml-auto">Settings</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> </div>
<TreeBrowser owner={owner} repo={repoName} ref={branch} path={path} /> <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 if (!readmeEntry || !blob) return null
return ( return (
<div className="border border-[#DFE1E6] rounded bg-white overflow-hidden"> <div className="border border-[var(--c-border)] rounded bg-[var(--c-surface)] overflow-hidden">
<div className="px-4 py-2.5 border-b border-[#DFE1E6] bg-[#FAFBFC] flex items-center gap-2"> <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="#5E6C84" strokeWidth="1.5" viewBox="0 0 24 24"> <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" /> <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> </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>
<div className="px-6 py-5 prose prose-sm max-w-none text-[#172B4D] <div className="px-6 py-5 prose prose-sm max-w-none text-[var(--c-text)]
prose-headings:text-[#172B4D] prose-headings:font-semibold prose-headings:border-b prose-headings:border-[#DFE1E6] prose-headings:pb-1 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-[#0052CC] prose-code:bg-[#F4F5F7] prose-code:px-1 prose-code:rounded prose-code:text-sm 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-[#F4F5F7] prose-pre:border prose-pre:border-[#DFE1E6] prose-pre: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> <ReactMarkdown remarkPlugins={[remarkGfm]}>{blob.content}</ReactMarkdown>
</div> </div>
</div> </div>
@@ -201,22 +201,22 @@ function GettingStarted({ repoName, branch, cloneUrl }: {
repoName: string; branch: string; cloneUrl: string repoName: string; branch: string; cloneUrl: string
}) { }) {
return ( return (
<div className="border border-[#DFE1E6] rounded-lg overflow-hidden"> <div className="border border-[var(--c-border)] rounded-lg overflow-hidden">
<div className="px-5 py-4 bg-[#FAFBFC] border-b border-[#DFE1E6]"> <div className="px-5 py-4 bg-[var(--c-surface-raised)] border-b border-[var(--c-border)]">
<h2 className="text-sm font-semibold text-[#172B4D]">Getting started</h2> <h2 className="text-sm font-semibold text-[var(--c-text)]">Getting started</h2>
<p className="text-xs text-[#5E6C84] mt-0.5">Push your first commit to get started.</p> <p className="text-xs text-[var(--c-muted)] mt-0.5">Push your first commit to get started.</p>
</div> </div>
<div className="px-5 py-5 space-y-6 text-sm"> <div className="px-5 py-5 space-y-6 text-sm">
<div> <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} /> <CopyBlock value={cloneUrl} />
</div> </div>
<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 /> <CopyBlock value={`git remote add origin ${cloneUrl}\ngit branch -M ${branch}\ngit push -u origin ${branch}`} multiline />
</div> </div>
<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 /> <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>
</div> </div>
@@ -228,12 +228,12 @@ function CopyBlock({ value, multiline }: { value: string; multiline?: boolean })
const copy = () => navigator.clipboard.writeText(value).catch(() => {}) const copy = () => navigator.clipboard.writeText(value).catch(() => {})
return ( return (
<div className="relative group"> <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} {value}
</pre> </pre>
<button <button
onClick={copy} 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 Copy
</button> </button>
+64 -64
View File
@@ -91,28 +91,28 @@ export default function RepoSettingsPage() {
return ( return (
<div className="flex h-full overflow-hidden"> <div className="flex h-full overflow-hidden">
{/* Sidebar */} {/* Sidebar */}
<aside className="w-60 shrink-0 border-r border-[#DFE1E6] bg-white hidden md:flex flex-col"> <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-[#DFE1E6] space-y-2.5 shrink-0"> <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-[#172B4D] hover:text-[#0052CC] font-medium"> <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"> <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" /> <path strokeLinecap="round" strokeLinejoin="round" d="M10.5 19.5 3 12m0 0 7.5-7.5M3 12h18" />
</svg> </svg>
Repository settings Repository settings
</Link> </Link>
<div className="relative"> <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" /> <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> </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>
</div> </div>
<nav className="flex-1 overflow-y-auto py-2"> <nav className="flex-1 overflow-y-auto py-2">
{filtered.map(group => ( {filtered.map(group => (
<div key={group.group} className="mb-1"> <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 => ( {group.items.map(item => (
<button key={item.id} onClick={() => setSearchParams({ section: item.id })} <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} {item.label}
</button> </button>
))} ))}
@@ -123,8 +123,8 @@ export default function RepoSettingsPage() {
{/* Content */} {/* Content */}
<main className="flex-1 overflow-y-auto min-w-0"> <main className="flex-1 overflow-y-auto min-w-0">
<div className="md:hidden p-3 border-b border-[#DFE1E6] bg-white"> <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-[#DFE1E6] rounded px-3 py-2 text-sm focus:outline-none"> <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>))} {SIDEBAR.map(g => g.items.map(i => <option key={i.id} value={i.id}>{g.group} {i.label}</option>))}
</select> </select>
</div> </div>
@@ -260,19 +260,19 @@ function RepositoryDetailsSection({ owner, repo }: { owner: string; repo: string
{/* Page header */} {/* Page header */}
<div className="flex items-start justify-between gap-4 flex-wrap"> <div className="flex items-start justify-between gap-4 flex-wrap">
<div> <div>
<div className="flex items-center gap-1 text-xs text-[#5E6C84] mb-1.5"> <div className="flex items-center gap-1 text-xs text-[var(--c-muted)] mb-1.5">
<Link to={`/repos/${owner}/${repo}`} className="hover:text-[#0052CC]">{owner}</Link> <Link to={`/repos/${owner}/${repo}`} className="hover:text-[var(--c-brand)]">{owner}</Link>
<span>/</span> <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> </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> </div>
{/* Manage repository dropdown */} {/* Manage repository dropdown */}
<div className="relative shrink-0" ref={manageRef}> <div className="relative shrink-0" ref={manageRef}>
<button <button
onClick={() => setShowManage(s => !s)} 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 Manage repository
<svg width="12" height="12" fill="none" stroke="currentColor" strokeWidth="2.5" viewBox="0 0 24 24"> <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> </svg>
</button> </button>
{showManage && ( {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 <button
onClick={() => { setShowManage(false); setShowAdvanced(true); setTimeout(() => document.getElementById('danger-zone')?.scrollIntoView({ behavior: 'smooth' }), 50) }} 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"> <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" /> <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> </div>
<div className="border-t border-[#DFE1E6]" /> <div className="border-t border-[var(--c-border)]" />
<form onSubmit={handleSave} className="space-y-6"> <form onSubmit={handleSave} className="space-y-6">
{/* Avatar */} {/* Avatar */}
<div> <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"> <div className="flex items-center gap-4">
{/* Clickable avatar */} {/* Clickable avatar */}
<button <button
type="button" type="button"
onClick={() => fileInputRef.current?.click()} 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" title="Click to change avatar"
> >
{avatarPreview ? ( {avatarPreview ? (
@@ -326,16 +326,16 @@ function RepositoryDetailsSection({ owner, repo }: { owner: string; repo: string
type="button" type="button"
onClick={() => fileInputRef.current?.click()} onClick={() => fileInputRef.current?.click()}
disabled={uploadAvatar.isPending} 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'} {uploadAvatar.isPending ? 'Uploading…' : 'Change avatar'}
</button> </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 && ( {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 && ( {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> </div>
<input <input
@@ -350,58 +350,58 @@ function RepositoryDetailsSection({ owner, repo }: { owner: string; repo: string
{/* Repository name */} {/* Repository name */}
<div> <div>
<label className="block text-sm font-medium text-[#172B4D] mb-1"> <label className="block text-sm font-medium text-[var(--c-text)] mb-1">
Repository name <span className="text-[#DE350B]">*</span> Repository name <span className="text-[var(--c-danger)]">*</span>
</label> </label>
<input <input
value={name} value={name}
onChange={e => handleNameChange(e.target.value)} 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 {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 : 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> <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. Renaming will change the clone URL all existing git remotes will need to be updated.
</p> </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> </div>
{/* Size (read-only) */} {/* Size (read-only) */}
<div> <div>
<label className="block text-sm font-medium text-[#172B4D] mb-1">Size</label> <label className="block text-sm font-medium text-[var(--c-text)] mb-1">Size</label>
<p className="text-sm text-[#172B4D]">{formatSize(repoData.size)}</p> <p className="text-sm text-[var(--c-text)]">{formatSize(repoData.size)}</p>
</div> </div>
{/* Description */} {/* Description */}
<div> <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 <textarea
value={description} value={description}
onChange={e => setDescription(e.target.value)} onChange={e => setDescription(e.target.value)}
rows={4} rows={4}
placeholder="Describe this repository…" 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> </div>
{/* Access level */} {/* Access level */}
<div> <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"> <label className="flex items-start gap-2.5 cursor-pointer">
<input <input
type="checkbox" type="checkbox"
checked={isPrivate} checked={isPrivate}
onChange={e => setIsPrivate(e.target.checked)} 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> <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'} {isPrivate ? 'This is a private repository' : 'This is a public repository'}
</span> </span>
<p className="text-xs text-[#5E6C84] mt-0.5"> <p className="text-xs text-[var(--c-muted)] mt-0.5">
{isPrivate {isPrivate
? 'Only collaborators you invite can see and push to this repository.' ? 'Only collaborators you invite can see and push to this repository.'
: 'Anyone can view this repository. Make it private to restrict access.'} : 'Anyone can view this repository. Make it private to restrict access.'}
@@ -411,11 +411,11 @@ function RepositoryDetailsSection({ owner, repo }: { owner: string; repo: string
</div> </div>
{/* Advanced (collapsible) */} {/* Advanced (collapsible) */}
<div className="border border-[#DFE1E6] rounded-lg overflow-hidden"> <div className="border border-[var(--c-border)] rounded-lg overflow-hidden">
<button <button
type="button" type="button"
onClick={() => setShowAdvanced(s => !s)} 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" <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' : ''}`}> className={`transition-transform shrink-0 ${showAdvanced ? 'rotate-90' : ''}`}>
@@ -425,47 +425,47 @@ function RepositoryDetailsSection({ owner, repo }: { owner: string; repo: string
</button> </button>
{showAdvanced && ( {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 */} {/* Default branch */}
<div> <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 <input
value={defaultBranch} value={defaultBranch}
onChange={e => setDefaultBranch(e.target.value)} onChange={e => setDefaultBranch(e.target.value)}
placeholder="main" 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> </div>
{/* Danger zone */} {/* Danger zone */}
<div id="danger-zone" className="border border-[#FFEBE6] rounded-lg overflow-hidden"> <div id="danger-zone" className="border border-[var(--c-danger-tint)] rounded-lg overflow-hidden">
<div className="px-4 py-3 bg-[#FFEBE6]/60 border-b border-[#FFEBE6]"> <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-[#BF2600]">Delete repository</h3> <h3 className="text-sm font-semibold text-[var(--c-danger-dark)]">Delete repository</h3>
</div> </div>
<div className="px-4 py-4 space-y-3 bg-white"> <div className="px-4 py-4 space-y-3 bg-[var(--c-surface)]">
<p className="text-sm text-[#172B4D]"> <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. This is permanent and cannot be undone. All commits, branches, pull requests, issues, and settings will be permanently deleted.
</p> </p>
<p className="text-xs text-[#5E6C84]"> <p className="text-xs text-[var(--c-muted)]">
Type <code className="font-mono bg-[#F4F5F7] border border-[#DFE1E6] px-1.5 py-0.5 rounded text-[#172B4D]">{repo}</code> to confirm. 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> </p>
<input <input
value={confirmDelete} value={confirmDelete}
onChange={e => setConfirmDelete(e.target.value)} onChange={e => setConfirmDelete(e.target.value)}
placeholder={repo} 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 <button
type="button" type="button"
onClick={handleDelete} onClick={handleDelete}
disabled={confirmDelete !== repo || deleteRepo.isPending} 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'} {deleteRepo.isPending ? 'Deleting…' : 'Delete repository'}
</button> </button>
{deleteRepo.isError && ( {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>
</div> </div>
@@ -474,12 +474,12 @@ function RepositoryDetailsSection({ owner, repo }: { owner: string; repo: string
</div> </div>
{/* Footer: error / saved / Discard / Save */} {/* 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 && ( {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 && ( {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"> <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" /> <path strokeLinecap="round" strokeLinejoin="round" d="m4.5 12.75 6 6 9-13.5" />
</svg> </svg>
@@ -487,11 +487,11 @@ function RepositoryDetailsSection({ owner, repo }: { owner: string; repo: string
</span> </span>
)} )}
<button type="button" onClick={handleDiscard} disabled={!isDirty} <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 Discard
</button> </button>
<button type="submit" disabled={updateRepo.isPending || !isDirty || !!nameError} <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'} {updateRepo.isPending ? 'Saving…' : 'Save changes'}
</button> </button>
</div> </div>
@@ -506,14 +506,14 @@ function ComingSoon({ sectionId }: { sectionId: SectionId }) {
const meta = SECTION_META[sectionId] const meta = SECTION_META[sectionId]
return ( return (
<div className="max-w-2xl px-6 py-6"> <div className="max-w-2xl px-6 py-6">
<h1 className="text-xl font-semibold text-[#172B4D] mb-1">{meta.title}</h1> <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-[#DFE1E6] rounded-lg text-center bg-[#FAFBFC]"> <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="#97A0AF" strokeWidth="1.2" viewBox="0 0 24 24" className="mb-4"> <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" /> <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> </svg>
<h2 className="text-base font-semibold text-[#172B4D] mb-2">{meta.title}</h2> <h2 className="text-base font-semibold text-[var(--c-text)] mb-2">{meta.title}</h2>
<p className="text-sm text-[#5E6C84] max-w-sm leading-relaxed">{meta.description}</p> <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-[#97A0AF] px-2.5 py-1 rounded-full">Coming soon</span> <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>
</div> </div>
) )
+13 -13
View File
@@ -10,21 +10,21 @@ export default function ReposPage() {
<div className="max-w-4xl mx-auto px-4 md:px-6 py-6"> <div className="max-w-4xl mx-auto px-4 md:px-6 py-6">
<div className="flex items-center justify-between mb-6"> <div className="flex items-center justify-between mb-6">
<div> <div>
<h1 className="text-xl font-semibold text-[#172B4D]">Repositories</h1> <h1 className="text-xl font-semibold text-[var(--c-text)]">Repositories</h1>
{repos && ( {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>
<div className="flex items-center gap-2"> <div className="flex items-center gap-2">
<Link <Link
to="/repos/import" 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 Import
</Link> </Link>
<Link <Link
to="/repos/new" 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"> <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" /> <path strokeLinecap="round" strokeLinejoin="round" d="M12 4.5v15m7.5-7.5h-15" />
@@ -40,12 +40,12 @@ export default function ReposPage() {
<NotSignedIn /> <NotSignedIn />
) : !repos?.length ? ( ) : !repos?.length ? (
<div className="py-16 text-center"> <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" /> <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> </svg>
<p className="text-sm font-medium text-[#172B4D] mb-1">No repositories yet</p> <p className="text-sm font-medium text-[var(--c-text)] mb-1">No repositories yet</p>
<p className="text-xs text-[#5E6C84] mb-4">Create your first repository to get started.</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-[#0052CC] text-white text-sm font-medium hover:bg-[#0065FF]"> <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 Create repository
</Link> </Link>
</div> </div>
@@ -60,15 +60,15 @@ export default function ReposPage() {
function NotSignedIn() { function NotSignedIn() {
return ( return (
<div className="flex flex-col items-center justify-center py-12 border border-dashed border-[#DFE1E6] rounded text-center gap-3"> <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="#97A0AF" strokeWidth="1.5" viewBox="0 0 24 24"> <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" /> <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> </svg>
<div> <div>
<p className="text-sm font-medium text-[#172B4D]">Sign in to see your repositories</p> <p className="text-sm font-medium text-[var(--c-text)]">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-xs text-[var(--c-muted)] mt-1">You need to be signed in to view and create repositories.</p>
</div> </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 Sign in
</Link> </Link>
</div> </div>
+38 -38
View File
@@ -10,13 +10,13 @@ export default function SettingsPage() {
return ( return (
<div className="max-w-2xl mx-auto px-4 md:px-6 py-6 space-y-8"> <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 */} {/* Account info */}
<section className="border border-[#DFE1E6] rounded-lg overflow-hidden"> <section className="border border-[var(--c-border)] rounded-lg overflow-hidden">
<div className="px-5 py-4 border-b border-[#DFE1E6] bg-[#FAFBFC] flex items-center justify-between"> <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-[#172B4D]">Account</h2> <h2 className="text-sm font-semibold text-[var(--c-text)]">Account</h2>
<Link to="/profile" className="text-xs text-[#0052CC] hover:underline">Edit profile </Link> <Link to="/profile" className="text-xs text-[var(--c-brand)] hover:underline">Edit profile </Link>
</div> </div>
<div className="px-5 py-4 flex flex-col gap-3"> <div className="px-5 py-4 flex flex-col gap-3">
<Row label="Username" value={user?.username} /> <Row label="Username" value={user?.username} />
@@ -26,32 +26,32 @@ export default function SettingsPage() {
</section> </section>
{/* Change password */} {/* Change password */}
<section className="border border-[#DFE1E6] rounded-lg overflow-hidden"> <section className="border border-[var(--c-border)] rounded-lg overflow-hidden">
<div className="px-5 py-4 border-b border-[#DFE1E6] bg-[#FAFBFC]"> <div className="px-5 py-4 border-b border-[var(--c-border)] bg-[var(--c-surface-raised)]">
<h2 className="text-sm font-semibold text-[#172B4D]">Change password</h2> <h2 className="text-sm font-semibold text-[var(--c-text)]">Change password</h2>
</div> </div>
<form <form
className="px-5 py-5 flex flex-col gap-4" className="px-5 py-5 flex flex-col gap-4"
onSubmit={e => { e.preventDefault(); setCurrentPw(''); setNewPw('') }} onSubmit={e => { e.preventDefault(); setCurrentPw(''); setNewPw('') }}
> >
<div> <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 <input
type="password" value={currentPw} onChange={e => setCurrentPw(e.target.value)} 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>
<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 <input
type="password" value={newPw} onChange={e => setNewPw(e.target.value)} 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>
<div> <div>
<button <button
type="submit" 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 Update password
</button> </button>
@@ -62,19 +62,19 @@ export default function SettingsPage() {
<SSHKeySection /> <SSHKeySection />
{/* Danger zone */} {/* Danger zone */}
<section className="border border-[#FFEBE6] rounded-lg overflow-hidden"> <section className="border border-[var(--c-danger-tint)] rounded-lg overflow-hidden">
<div className="px-5 py-4 border-b border-[#FFEBE6] bg-[#FFEBE6]/50"> <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-[#BF2600]">Danger zone</h2> <h2 className="text-sm font-semibold text-[var(--c-danger-dark)]">Danger zone</h2>
</div> </div>
<div className="px-5 py-4 flex flex-col gap-3"> <div className="px-5 py-4 flex flex-col gap-3">
<div className="flex items-center justify-between"> <div className="flex items-center justify-between">
<div> <div>
<p className="text-sm font-medium text-[#172B4D]">Sign out</p> <p className="text-sm font-medium text-[var(--c-text)]">Sign out</p>
<p className="text-xs text-[#5E6C84]">End your current session on this device.</p> <p className="text-xs text-[var(--c-muted)]">End your current session on this device.</p>
</div> </div>
<button <button
onClick={logout} 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 Sign out
</button> </button>
@@ -88,8 +88,8 @@ export default function SettingsPage() {
function Row({ label, value }: { label: string; value?: string }) { function Row({ label, value }: { label: string; value?: string }) {
return ( return (
<div className="flex items-center justify-between py-1"> <div className="flex items-center justify-between py-1">
<span className="text-xs text-[#5E6C84] w-24 shrink-0">{label}</span> <span className="text-xs text-[var(--c-muted)] w-24 shrink-0">{label}</span>
<span className="text-sm text-[#172B4D] font-medium">{value}</span> <span className="text-sm text-[var(--c-text)] font-medium">{value}</span>
</div> </div>
) )
} }
@@ -109,53 +109,53 @@ function SSHKeySection() {
} }
return ( return (
<section className="border border-[#DFE1E6] rounded-lg overflow-hidden"> <section className="border border-[var(--c-border)] rounded-lg overflow-hidden">
<div className="px-5 py-4 border-b border-[#DFE1E6] bg-[#FAFBFC] flex items-center justify-between"> <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-[#172B4D]">SSH keys</h2> <h2 className="text-sm font-semibold text-[var(--c-text)]">SSH keys</h2>
<button onClick={() => setShowAdd(s => !s)} <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'} {showAdd ? 'Cancel' : '+ Add key'}
</button> </button>
</div> </div>
{showAdd && ( {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> <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" <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>
<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} <textarea value={publicKey} onChange={e => setPublicKey(e.target.value)} required rows={3}
placeholder="ssh-ed25519 AAAA… or ssh-rsa AAAA…" 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> </div>
{addKey.isError && ( {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} <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'} {addKey.isPending ? 'Adding…' : 'Add SSH key'}
</button> </button>
</form> </form>
)} )}
{!keys?.length ? ( {!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> <ul>
{keys.map(key => ( {keys.map(key => (
<li key={key.id} className="flex items-center gap-3 px-5 py-3 border-b border-[#DFE1E6] last:border-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="#5E6C84" strokeWidth="1.5" viewBox="0 0 24 24" className="shrink-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" /> <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> </svg>
<div className="flex-1 min-w-0"> <div className="flex-1 min-w-0">
<p className="text-sm font-medium text-[#172B4D] truncate">{key.title}</p> <p className="text-sm font-medium text-[var(--c-text)] truncate">{key.title}</p>
<p className="text-xs font-mono text-[#5E6C84] truncate">{key.fingerprint}</p> <p className="text-xs font-mono text-[var(--c-muted)] truncate">{key.fingerprint}</p>
</div> </div>
<button onClick={() => deleteKey.mutate(key.id)} <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 Delete
</button> </button>
</li> </li>
+9 -9
View File
@@ -10,30 +10,30 @@ export default function StarredPage() {
return ( return (
<div className="max-w-4xl mx-auto px-4 md:px-6 py-6"> <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 ? ( {!starredRepos.length ? (
<div className="flex flex-col items-center justify-center py-16 border border-dashed border-[#DFE1E6] rounded text-center gap-3"> <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="#97A0AF" strokeWidth="1.5" viewBox="0 0 24 24"> <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" /> <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> </svg>
<div> <div>
<p className="text-sm font-medium text-[#172B4D]">No starred repositories</p> <p className="text-sm font-medium text-[var(--c-text)]">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-xs text-[var(--c-muted)] mt-1">Star repositories in the sidebar to find them here quickly.</p>
</div> </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>
) : ( ) : (
<div className="flex flex-col gap-2"> <div className="flex flex-col gap-2">
{starredRepos.map(r => ( {starredRepos.map(r => (
<Link key={r.id} to={`/repos/${r.ownerName}/${r.name}`} <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"> <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"/> <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> </svg>
<div className="flex-1 min-w-0"> <div className="flex-1 min-w-0">
<p className="text-sm font-semibold text-[#0052CC]">{r.ownerName}/{r.name}</p> <p className="text-sm font-semibold text-[var(--c-brand)]">{r.ownerName}/{r.name}</p>
{r.description && <p className="text-xs text-[#5E6C84] truncate mt-0.5">{r.description}</p>} {r.description && <p className="text-xs text-[var(--c-muted)] truncate mt-0.5">{r.description}</p>}
</div> </div>
</Link> </Link>
))} ))}
+2 -2
View File
@@ -11,8 +11,8 @@ interface RepoAvatarProps {
// Consistent color per repo name (not random on each render) // Consistent color per repo name (not random on each render)
function hashColor(name: string): string { function hashColor(name: string): string {
const palette = [ const palette = [
'#0052CC', '#00875A', '#FF5630', '#FF8B00', 'var(--c-brand)', 'var(--c-success)', '#FF5630', 'var(--c-warning)',
'#6554C0', '#00B8D9', '#36B37E', '#253858', '#6554C0', '#00B8D9', '#36B37E', 'var(--c-text)',
] ]
let hash = 0 let hash = 0
for (let i = 0; i < name.length; i++) hash = (hash * 31 + name.charCodeAt(i)) | 0 for (let i = 0; i < name.length; i++) hash = (hash * 31 + name.charCodeAt(i)) | 0
+3 -3
View File
@@ -7,7 +7,7 @@ interface SkeletonProps {
export function Skeleton({ className }: SkeletonProps) { export function Skeleton({ className }: SkeletonProps) {
return ( return (
<div <div
className={cn('animate-pulse rounded bg-[#F4F5F7]', className)} className={cn('animate-pulse rounded bg-[var(--c-surface-muted)]', className)}
aria-hidden="true" aria-hidden="true"
/> />
) )
@@ -17,7 +17,7 @@ export function RepoListSkeleton() {
return ( return (
<div className="flex flex-col gap-3" aria-label="Loading repositories"> <div className="flex flex-col gap-3" aria-label="Loading repositories">
{Array.from({ length: 5 }).map((_, i) => ( {Array.from({ length: 5 }).map((_, i) => (
<div key={i} className="flex items-center gap-3 p-4 border border-[#DFE1E6] rounded"> <div key={i} className="flex items-center gap-3 p-4 border border-[var(--c-border)] rounded">
<Skeleton className="size-8 rounded-full shrink-0" /> <Skeleton className="size-8 rounded-full shrink-0" />
<div className="flex-1 flex flex-col gap-2"> <div className="flex-1 flex flex-col gap-2">
<Skeleton className="h-4 w-48" /> <Skeleton className="h-4 w-48" />
@@ -33,7 +33,7 @@ export function PRListSkeleton() {
return ( return (
<div className="flex flex-col gap-2" aria-label="Loading pull requests"> <div className="flex flex-col gap-2" aria-label="Loading pull requests">
{Array.from({ length: 6 }).map((_, i) => ( {Array.from({ length: 6 }).map((_, i) => (
<div key={i} className="flex items-center gap-3 p-4 border border-[#DFE1E6] rounded"> <div key={i} className="flex items-center gap-3 p-4 border border-[var(--c-border)] rounded">
<Skeleton className="h-5 w-12 rounded-full shrink-0" /> <Skeleton className="h-5 w-12 rounded-full shrink-0" />
<div className="flex-1 flex flex-col gap-2"> <div className="flex-1 flex flex-col gap-2">
<Skeleton className="h-4 w-64" /> <Skeleton className="h-4 w-64" />
+14 -14
View File
@@ -8,23 +8,23 @@ export const tokens = {
xxl: 48, // 48px xxl: 48, // 48px
}, },
color: { color: {
brand: '#0052CC', // Atlassian Blue brand: 'var(--c-brand)', // Atlassian Blue
brandHover: '#0065FF', brandHover: 'var(--c-brand-hover)',
surface: '#FFFFFF', surface: '#FFFFFF',
subtle: '#F4F5F7', subtle: 'var(--c-surface-muted)',
elevated: '#FAFBFC', elevated: 'var(--c-surface-raised)',
text: '#172B4D', text: 'var(--c-text)',
muted: '#5E6C84', muted: 'var(--c-muted)',
border: '#DFE1E6', border: 'var(--c-border)',
borderFocus: '#4C9AFF', borderFocus: 'var(--c-brand-focus)',
danger: '#DE350B', danger: 'var(--c-danger)',
dangerSubtle: '#FFEBE6', dangerSubtle: 'var(--c-danger-tint)',
success: '#00875A', success: 'var(--c-success)',
successSubtle: '#E3FCEF', successSubtle: '#E3FCEF',
warning: '#FF8B00', warning: 'var(--c-warning)',
warningSubtle: '#FFFAE6', warningSubtle: '#FFFAE6',
info: '#0052CC', info: 'var(--c-brand)',
infoSubtle: '#DEEBFF', infoSubtle: 'var(--c-brand-tint)',
}, },
sidebar: { sidebar: {
collapsed: 56, collapsed: 56,