diff --git a/internal/frontend/dist/index.html b/internal/frontend/dist/index.html index 439810a..8d8fc78 100644 --- a/internal/frontend/dist/index.html +++ b/internal/frontend/dist/index.html @@ -11,10 +11,10 @@ const dark = stored ? stored === 'dark' : window.matchMedia('(prefers-color-scheme: dark)').matches document.documentElement.classList.toggle('dark', dark) - + - - + +
diff --git a/internal/frontend/src/components/app-shell.tsx b/internal/frontend/src/components/app-shell.tsx index c734e1d..0d11c79 100644 --- a/internal/frontend/src/components/app-shell.tsx +++ b/internal/frontend/src/components/app-shell.tsx @@ -1,5 +1,7 @@ import { + createContext, useCallback, + useContext, useEffect, useRef, useState, @@ -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 (
@@ -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 ( cn( 'flex items-center gap-2 rounded-xl px-3 py-2 text-[13px] transition-colors', @@ -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'], @@ -117,71 +126,74 @@ function RailContent({ onNavigate }: { onNavigate?: () => void }) { const headings = needsHeadings(groups) return ( -
- - - {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}