Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 3 additions & 3 deletions internal/frontend/dist/index.html
Original file line number Diff line number Diff line change
Expand Up @@ -11,10 +11,10 @@
const dark = stored ? stored === 'dark' : window.matchMedia('(prefers-color-scheme: dark)').matches
document.documentElement.classList.toggle('dark', dark)
</script>
<script type="module" crossorigin src="/assets/index-CRO8UY-q.js"></script>
<script type="module" crossorigin src="/assets/index-D7We2GBj.js"></script>
<link rel="modulepreload" crossorigin href="/assets/vendor-react-CRwv3FO_.js">
<link rel="modulepreload" crossorigin href="/assets/vendor-icons-C5NvIIAT.js">
<link rel="stylesheet" crossorigin href="/assets/index-DC5HCQzn.css">
<link rel="modulepreload" crossorigin href="/assets/vendor-icons-D8F5X9EB.js">
<link rel="stylesheet" crossorigin href="/assets/index-IFvVeuLA.css">
</head>
<body>
<div id="root"></div>
Expand Down
134 changes: 73 additions & 61 deletions internal/frontend/src/components/app-shell.tsx
Original file line number Diff line number Diff line change
@@ -1,5 +1,7 @@
import {
createContext,
useCallback,
useContext,
useEffect,
useRef,
useState,
Expand Down Expand Up @@ -47,6 +49,10 @@ const managePages = [
{ to: '/admin/maintenance', label: 'Maintenance', icon: Wrench },
]

// The mobile sheet closes when a rail link is followed, but only then. Closing
// on any click inside the rail would swallow the Manage fold toggle.
const RailNavigate = createContext<() => void>(() => {})

function RailSection({ label, children }: { label: string; children: ReactNode }) {
return (
<div className="space-y-1 px-3 py-3">
Expand All @@ -57,10 +63,13 @@ function RailSection({ label, children }: { label: string; children: ReactNode }
}

function RailLink({ to, end, children }: { to: string; end?: boolean; children: ReactNode }) {
const navigated = useContext(RailNavigate)

return (
<NavLink
to={to}
end={end}
onClick={navigated}
className={({ isActive }) =>
cn(
'flex items-center gap-2 rounded-xl px-3 py-2 text-[13px] transition-colors',
Expand Down Expand Up @@ -104,7 +113,7 @@ function ManageSection() {
)
}

function RailContent({ onNavigate }: { onNavigate?: () => void }) {
function RailContent({ onNavigate = () => {} }: { onNavigate?: () => void }) {
const { instanceName, user } = useSession()
const { data } = useQuery({
queryKey: ['repositories'],
Expand All @@ -117,71 +126,74 @@ function RailContent({ onNavigate }: { onNavigate?: () => void }) {
const headings = needsHeadings(groups)

return (
<div className="flex h-full flex-col" onClick={onNavigate}>
<Link
to="/"
className="mx-3 mt-4 flex items-center gap-2.5 rounded-xl px-3 py-2.5 transition-colors hover:bg-accent/70"
>
<Logo className="size-5" />
<span className="truncate font-semibold tracking-tight">{instanceName}</span>
</Link>

<div className="flex-1 overflow-y-auto scrollbar-thin pb-4">
<RailSection label="Browse">
<RailLink to="/" end>
<Library className="size-3.5 shrink-0 text-faint" />
<span className="flex-1 truncate">All repositories</span>
</RailLink>
<RailLink to="/search">
<Search className="size-3.5 shrink-0 text-faint" />
<span className="flex-1 truncate">Search artifacts</span>
</RailLink>
</RailSection>

{groups.map((group) => (
<RailSection
key={group.id ?? 'uncategorized'}
label={headings ? group.name : 'Repositories'}
>
{group.repositories.map((repository) => (
<RailLink key={repository.id} to={`/repositories/${repository.name}`}>
<span className="flex-1 truncate font-mono text-[13px]">{repository.name}</span>
{repository.visibility === 'public' ? (
<Globe className="size-3.5 shrink-0 text-faint" aria-label="Public" />
) : (
<Lock className="size-3.5 shrink-0 text-faint" aria-label="Private" />
)}
</RailLink>
))}
</RailSection>
))}
{data && repositories.length === 0 ? (
<RailSection label="Repositories">
<p className="px-3 py-2 text-[13px] text-faint">None visible to you</p>
<RailNavigate.Provider value={onNavigate}>
<div className="flex h-full flex-col">
<Link
to="/"
onClick={onNavigate}
className="mx-3 mt-4 flex items-center gap-2.5 rounded-xl px-3 py-2.5 transition-colors hover:bg-accent/70"
>
<Logo className="size-5" />
<span className="truncate font-semibold tracking-tight">{instanceName}</span>
</Link>

<div className="flex-1 overflow-y-auto scrollbar-thin pb-4">
<RailSection label="Browse">
<RailLink to="/" end>
<Library className="size-3.5 shrink-0 text-faint" />
<span className="flex-1 truncate">All repositories</span>
</RailLink>
<RailLink to="/search">
<Search className="size-3.5 shrink-0 text-faint" />
<span className="flex-1 truncate">Search artifacts</span>
</RailLink>
</RailSection>
) : null}

{user?.role === 'admin' ? <ManageSection /> : null}
{groups.map((group) => (
<RailSection
key={group.id ?? 'uncategorized'}
label={headings ? group.name : 'Repositories'}
>
{group.repositories.map((repository) => (
<RailLink key={repository.id} to={`/repositories/${repository.name}`}>
<span className="flex-1 truncate font-mono text-[13px]">{repository.name}</span>
{repository.visibility === 'public' ? (
<Globe className="size-3.5 shrink-0 text-faint" aria-label="Public" />
) : (
<Lock className="size-3.5 shrink-0 text-faint" aria-label="Private" />
)}
</RailLink>
))}
</RailSection>
))}
{data && repositories.length === 0 ? (
<RailSection label="Repositories">
<p className="px-3 py-2 text-[13px] text-faint">None visible to you</p>
</RailSection>
) : null}

{user?.role === 'admin' ? <ManageSection /> : null}

{user ? (
<RailSection label="You">
<RailLink to="/account">
<KeyRound className="size-3.5 shrink-0 text-faint" />
<span className="flex-1 truncate">Account and tokens</span>
</RailLink>
</RailSection>
) : null}
</div>

{user ? (
<RailSection label="You">
<RailLink to="/account">
<KeyRound className="size-3.5 shrink-0 text-faint" />
<span className="flex-1 truncate">Account and tokens</span>
</RailLink>
</RailSection>
) : null}
</div>
<div className="mx-3 rounded-2xl bg-tint px-4 py-3.5 text-tint-foreground">
<p className="text-xs font-medium opacity-80">Repository endpoint</p>
<p className="mt-1 break-all font-mono text-[11px] opacity-70">
{window.location.origin}/repository/
</p>
</div>

<div className="mx-3 rounded-2xl bg-tint px-4 py-3.5 text-tint-foreground">
<p className="text-xs font-medium opacity-80">Repository endpoint</p>
<p className="mt-1 break-all font-mono text-[11px] opacity-70">
{window.location.origin}/repository/
</p>
<BuildStamp />
</div>

<BuildStamp />
</div>
</RailNavigate.Provider>
)
}

Expand Down
7 changes: 2 additions & 5 deletions internal/frontend/src/components/copy-button.tsx
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import { useEffect, useState } from 'react'
import { Check, Copy } from 'lucide-react'
import type { RepositoryFormat } from '@/lib/api'
import { cn } from '@/lib/utils'
import { cn, copyText } from '@/lib/utils'

function useCopied(): [boolean, (value: string) => void] {
const [copied, setCopied] = useState(false)
Expand All @@ -12,10 +12,7 @@ function useCopied(): [boolean, (value: string) => void] {
return () => window.clearTimeout(timer)
}, [copied])

return [
copied,
(value: string) => void navigator.clipboard.writeText(value).then(() => setCopied(true)),
]
return [copied, (value: string) => void copyText(value).then(setCopied)]
}

export function CopyButton({ value, className }: { value: string; className?: string }) {
Expand Down
6 changes: 3 additions & 3 deletions internal/frontend/src/components/welcome-editor.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -49,13 +49,13 @@ export function WelcomeEditor({
</TabsContent>

<TabsContent value="preview" className="mt-0">
<div className="relative overflow-hidden rounded-2xl bg-panel px-6 py-6 text-panel-foreground">
<BrandSplotch className="left-0 top-0 opacity-70 dark:opacity-40" />
<div className="relative overflow-hidden rounded-2xl bg-muted px-6 py-6 text-foreground">
<BrandSplotch className="left-full top-0 opacity-20 dark:opacity-25" />
<div className="relative">
{value.trim() ? (
<Markdown source={value} className="max-w-2xl" />
) : (
<p className="text-[13px] text-panel-muted">
<p className="text-[13px] text-muted-foreground">
Nothing to show. The landing page skips the hero when the welcome text is empty.
</p>
)}
Expand Down
12 changes: 7 additions & 5 deletions internal/frontend/src/components/welcome-hero.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -8,12 +8,14 @@ export function WelcomeHero() {
if (!welcomeText.trim()) return null

return (
<section className="relative overflow-hidden rounded-3xl bg-panel px-7 py-8 text-panel-foreground sm:px-10 sm:py-11">
<BrandSplotch className="left-0 top-0 opacity-70 dark:opacity-40" />
<div className="relative space-y-6">
<section className="relative overflow-hidden rounded-3xl bg-muted px-7 py-8 text-foreground sm:px-10 sm:py-11">
<BrandSplotch className="left-full top-0 opacity-15 dark:opacity-25" />
<div className="relative space-y-5">
<div className="flex items-center gap-2.5">
<Logo className="size-7 text-panel-foreground" />
<span className="text-[13px] font-medium text-panel-muted">{instanceName}</span>
<Logo className="size-6 text-foreground" />
<span className="text-[11px] font-medium uppercase tracking-[0.14em] text-muted-foreground">
{instanceName}
</span>
</div>
<Markdown source={welcomeText} className="max-w-2xl" />
</div>
Expand Down
31 changes: 31 additions & 0 deletions internal/frontend/src/lib/utils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,37 @@ export function plural(count: number, singular: string, many = `${singular}s`):
return `${count} ${count === 1 ? singular : many}`
}

// navigator.clipboard only exists in a secure context, and an Arca instance is
// commonly reached over plain HTTP on an internal host, so keep the old
// execCommand path as the fallback.
export async function copyText(text: string): Promise<boolean> {
if (navigator.clipboard?.writeText) {
try {
await navigator.clipboard.writeText(text)
return true
} catch {
// Permission denied or no user gesture, try the textarea instead.
}
}

const area = document.createElement('textarea')
area.value = text
area.setAttribute('readonly', '')
area.style.position = 'fixed'
area.style.top = '0'
area.style.opacity = '0'
document.body.append(area)
area.select()

try {
return document.execCommand('copy')
} catch {
return false
} finally {
area.remove()
}
}

const UNITS = ['B', 'KB', 'MB', 'GB', 'TB']

export function formatBytes(bytes: number): string {
Expand Down
14 changes: 10 additions & 4 deletions internal/frontend/src/routes/repositories.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@ import { WelcomeHero } from '@/components/welcome-hero'
import { api, type Repository } from '@/lib/api'
import { categoriesQuery, groupRepositories, needsHeadings } from '@/lib/categories'
import { useSession } from '@/lib/session'
import { formatRelative, plural } from '@/lib/utils'
import { cn, formatRelative, plural } from '@/lib/utils'

function RepositoryTable({ repositories }: { repositories: Repository[] }) {
return (
Expand All @@ -32,7 +32,7 @@ function RepositoryTable({ repositories }: { repositories: Repository[] }) {
<TableCell className="py-3">
<Link
to={`/repositories/${repository.name}`}
className="font-mono text-sm font-medium text-foreground hover:text-primary"
className="whitespace-nowrap font-mono text-sm font-medium text-foreground hover:text-primary"
>
{repository.name}
</Link>
Expand Down Expand Up @@ -111,9 +111,15 @@ function RepositoryIndex() {
) : (
<div className="space-y-8">
{groups.map((group) => (
<section key={group.id ?? 'uncategorized'} className="space-y-3">
<section
key={group.id ?? 'uncategorized'}
className={cn(
'space-y-3',
headings && 'lg:grid lg:grid-cols-[10rem_minmax(0,1fr)] lg:gap-6 lg:space-y-0',
)}
>
{headings ? (
<div className="space-y-0.5">
<div className="space-y-0.5 lg:sticky lg:top-20 lg:self-start lg:pt-2.5">
<h2 className="text-sm font-semibold">{group.name}</h2>
{group.description ? (
<p className="text-xs text-muted-foreground">{group.description}</p>
Expand Down
Loading