-
-
-
{instanceName}
-
-
-
-
-
-
- All repositories
-
-
-
- Search artifacts
-
-
-
- {groups.map((group) => (
-
- {group.repositories.map((repository) => (
-
- {repository.name}
- {repository.visibility === 'public' ? (
-
- ) : (
-
- )}
-
- ))}
-
- ))}
- {data && repositories.length === 0 ? (
-
- None visible to you
+
+
+
+
+
{instanceName}
+
+
+
+
+
+
+ All repositories
+
+
+
+ Search artifacts
+
- ) : null}
- {user?.role === 'admin' ?
: null}
+ {groups.map((group) => (
+
+ {group.repositories.map((repository) => (
+
+ {repository.name}
+ {repository.visibility === 'public' ? (
+
+ ) : (
+
+ )}
+
+ ))}
+
+ ))}
+ {data && repositories.length === 0 ? (
+
+ None visible to you
+
+ ) : null}
+
+ {user?.role === 'admin' ?
: null}
+
+ {user ? (
+
+
+
+ Account and tokens
+
+
+ ) : null}
+
- {user ? (
-
-
-
- Account and tokens
-
-
- ) : null}
-
+
+
Repository endpoint
+
+ {window.location.origin}/repository/
+
+
-
-
Repository endpoint
-
- {window.location.origin}/repository/
-
+
-
-
-
+
)
}
diff --git a/internal/frontend/src/components/copy-button.tsx b/internal/frontend/src/components/copy-button.tsx
index 82830b6..b52999b 100644
--- a/internal/frontend/src/components/copy-button.tsx
+++ b/internal/frontend/src/components/copy-button.tsx
@@ -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)
@@ -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 }) {
diff --git a/internal/frontend/src/components/welcome-editor.tsx b/internal/frontend/src/components/welcome-editor.tsx
index c6931d8..ab72cc4 100644
--- a/internal/frontend/src/components/welcome-editor.tsx
+++ b/internal/frontend/src/components/welcome-editor.tsx
@@ -49,13 +49,13 @@ export function WelcomeEditor({
-
-
+
+
{value.trim() ? (
) : (
-
+
Nothing to show. The landing page skips the hero when the welcome text is empty.
)}
diff --git a/internal/frontend/src/components/welcome-hero.tsx b/internal/frontend/src/components/welcome-hero.tsx
index 71889a6..2a5f4aa 100644
--- a/internal/frontend/src/components/welcome-hero.tsx
+++ b/internal/frontend/src/components/welcome-hero.tsx
@@ -8,12 +8,14 @@ export function WelcomeHero() {
if (!welcomeText.trim()) return null
return (
-
-
-
+
+
+
-
- {instanceName}
+
+
+ {instanceName}
+
diff --git a/internal/frontend/src/lib/utils.ts b/internal/frontend/src/lib/utils.ts
index 3649db4..0f84dbc 100644
--- a/internal/frontend/src/lib/utils.ts
+++ b/internal/frontend/src/lib/utils.ts
@@ -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 {
+ 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 {
diff --git a/internal/frontend/src/routes/repositories.tsx b/internal/frontend/src/routes/repositories.tsx
index 842795d..2f74d49 100644
--- a/internal/frontend/src/routes/repositories.tsx
+++ b/internal/frontend/src/routes/repositories.tsx
@@ -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 (
@@ -32,7 +32,7 @@ function RepositoryTable({ repositories }: { repositories: Repository[] }) {
{repository.name}
@@ -111,9 +111,15 @@ function RepositoryIndex() {
) : (
{groups.map((group) => (
-
+
{headings ? (
-
+
{group.name}
{group.description ? (
{group.description}