Redesign the UI shell and stabilize download/auth UX
Shift the frontend to a brutalist control-surface style, align tab/button states, and remove duplicated panels so the interface is clearer. Also add auth boot loading feedback and robust temp archive cleanup to prevent LZ4 download collisions from stale files.
This commit is contained in:
Binary file not shown.
@@ -1145,6 +1145,7 @@ func (s *Server) createTarLz4Temp(uid int64, rel, base string) (string, string,
|
||||
if _, err := exec.LookPath("lz4"); err != nil {
|
||||
return "", "", "", fmt.Errorf("lz4 format requires 'lz4' binary on server; choose zip or tar.gz in settings")
|
||||
}
|
||||
cleanupStaleTempArchives(1 * time.Hour)
|
||||
tarPath, err := s.createTarTemp(uid, rel, base)
|
||||
if err != nil {
|
||||
return "", "", "", err
|
||||
@@ -1157,6 +1158,7 @@ func (s *Server) createTarLz4Temp(uid int64, rel, base string) (string, string,
|
||||
}
|
||||
outPath := outTmp.Name()
|
||||
outTmp.Close()
|
||||
_ = os.Remove(outPath)
|
||||
|
||||
cmd := exec.Command("lz4", "-z", "-q", tarPath, outPath)
|
||||
if out, err := cmd.CombinedOutput(); err != nil {
|
||||
@@ -1536,6 +1538,7 @@ func createTarLz4FromLocalDir(root, baseName string) (string, string, string, er
|
||||
if _, err := exec.LookPath("lz4"); err != nil {
|
||||
return "", "", "", fmt.Errorf("lz4 format requires 'lz4' binary on server; choose zip or tar.gz in settings")
|
||||
}
|
||||
cleanupStaleTempArchives(1 * time.Hour)
|
||||
tarPath, err := createTarFromLocalDir(root)
|
||||
if err != nil {
|
||||
return "", "", "", err
|
||||
@@ -1548,6 +1551,7 @@ func createTarLz4FromLocalDir(root, baseName string) (string, string, string, er
|
||||
}
|
||||
outPath := outTmp.Name()
|
||||
outTmp.Close()
|
||||
_ = os.Remove(outPath)
|
||||
cmd := exec.Command("lz4", "-z", "-q", tarPath, outPath)
|
||||
if out, err := cmd.CombinedOutput(); err != nil {
|
||||
os.Remove(outPath)
|
||||
@@ -1556,6 +1560,35 @@ func createTarLz4FromLocalDir(root, baseName string) (string, string, string, er
|
||||
return outPath, baseName + ".tar.lz4", "application/x-lz4", nil
|
||||
}
|
||||
|
||||
func cleanupStaleTempArchives(maxIdle time.Duration) {
|
||||
tmpDir := os.TempDir()
|
||||
entries, err := os.ReadDir(tmpDir)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
cutoff := time.Now().Add(-maxIdle)
|
||||
for _, entry := range entries {
|
||||
name := entry.Name()
|
||||
if !strings.HasPrefix(name, "filez-") && !strings.HasPrefix(name, "filez-batch-") {
|
||||
continue
|
||||
}
|
||||
if !(strings.HasSuffix(name, ".tar.lz4") || strings.HasSuffix(name, ".tar") || strings.HasSuffix(name, ".tar.gz") || strings.HasSuffix(name, ".zip") || strings.HasSuffix(name, ".rar")) {
|
||||
continue
|
||||
}
|
||||
|
||||
info, err := entry.Info()
|
||||
if err != nil || info.IsDir() {
|
||||
continue
|
||||
}
|
||||
if info.ModTime().After(cutoff) {
|
||||
continue
|
||||
}
|
||||
|
||||
_ = os.Remove(filepath.Join(tmpDir, name))
|
||||
}
|
||||
}
|
||||
|
||||
func createRarFromLocalDir(root, baseName string) (string, string, string, error) {
|
||||
if _, err := exec.LookPath("rar"); err != nil {
|
||||
return "", "", "", fmt.Errorf("rar format requires 'rar' binary on server; choose zip or tar.gz in settings")
|
||||
|
||||
@@ -50,6 +50,7 @@ type ProtocolProfile = {
|
||||
}
|
||||
type ProtocolInfo = { ftp?: ProtocolProfile; ftps?: ProtocolProfile }
|
||||
type MarkdownRendererProps = { children?: string }
|
||||
type CachedUIPrefs = { username: string; theme: Theme; colorMode: ColorMode; archiveFormat: ArchiveFormat }
|
||||
|
||||
const AdminPanel = lazy(() => import('./lazy/AdminPanel'))
|
||||
const TransferSection = lazy(() => import('./lazy/TransferSection'))
|
||||
@@ -64,6 +65,7 @@ function resolveModuleValue<T>(mod: unknown): T | null {
|
||||
|
||||
const themeOptions: Theme[] = ['dracula', 'nord', 'monokai', 'solarized', 'github']
|
||||
const modeOptions: ColorMode[] = ['auto', 'light', 'dark']
|
||||
const archiveOptions: ArchiveFormat[] = ['zip', 'rar', 'tar.gz', 'lz4']
|
||||
const shareTTLPresets: Array<{ value: string; label: string }> = [
|
||||
{ value: '60', label: '1h' },
|
||||
{ value: '120', label: '2h' },
|
||||
@@ -92,6 +94,7 @@ const dict: Record<Lang, Record<string, string>> = {
|
||||
signIn: 'Sign in',
|
||||
signInGoogle: 'Continue with Google',
|
||||
signInAdmin: 'Sign in as admin',
|
||||
loading: 'Loading',
|
||||
username: 'Username',
|
||||
password: 'Password',
|
||||
logout: 'Logout',
|
||||
@@ -192,6 +195,7 @@ const dict: Record<Lang, Record<string, string>> = {
|
||||
signIn: 'Anmelden',
|
||||
signInGoogle: 'Mit Google fortfahren',
|
||||
signInAdmin: 'Als Admin anmelden',
|
||||
loading: 'Laden',
|
||||
username: 'Benutzername',
|
||||
password: 'Passwort',
|
||||
logout: 'Abmelden',
|
||||
@@ -292,6 +296,7 @@ const dict: Record<Lang, Record<string, string>> = {
|
||||
signIn: 'Войти',
|
||||
signInGoogle: 'Продолжить через Google',
|
||||
signInAdmin: 'Войти как админ',
|
||||
loading: 'Загрузка',
|
||||
username: 'Логин',
|
||||
password: 'Пароль',
|
||||
logout: 'Выйти',
|
||||
@@ -396,6 +401,54 @@ function detectMode(): Exclude<ColorMode, 'auto'> {
|
||||
return window.matchMedia('(prefers-color-scheme: dark)').matches ? 'dark' : 'light'
|
||||
}
|
||||
|
||||
const uiPrefsCookieName = 'filez_ui_prefs'
|
||||
|
||||
function cookieValue(name: string): string {
|
||||
const chunks = document.cookie ? document.cookie.split('; ') : []
|
||||
for (const chunk of chunks) {
|
||||
const idx = chunk.indexOf('=')
|
||||
if (idx <= 0) continue
|
||||
if (chunk.slice(0, idx) === name) return chunk.slice(idx + 1)
|
||||
}
|
||||
return ''
|
||||
}
|
||||
|
||||
function asTheme(v: string): Theme {
|
||||
return themeOptions.includes(v as Theme) ? (v as Theme) : 'dracula'
|
||||
}
|
||||
|
||||
function asColorMode(v: string): ColorMode {
|
||||
return modeOptions.includes(v as ColorMode) ? (v as ColorMode) : 'auto'
|
||||
}
|
||||
|
||||
function asArchiveFormat(v: string): ArchiveFormat {
|
||||
return archiveOptions.includes(v as ArchiveFormat) ? (v as ArchiveFormat) : 'zip'
|
||||
}
|
||||
|
||||
function readCachedUIPrefs(): CachedUIPrefs | null {
|
||||
const raw = cookieValue(uiPrefsCookieName)
|
||||
if (!raw) return null
|
||||
try {
|
||||
const parsed = JSON.parse(decodeURIComponent(raw)) as Partial<CachedUIPrefs>
|
||||
const username = String(parsed.username ?? '').trim().toLowerCase()
|
||||
if (!username) return null
|
||||
return {
|
||||
username,
|
||||
theme: asTheme(String(parsed.theme ?? '')),
|
||||
colorMode: asColorMode(String(parsed.colorMode ?? '')),
|
||||
archiveFormat: asArchiveFormat(String(parsed.archiveFormat ?? '')),
|
||||
}
|
||||
} catch {
|
||||
return null
|
||||
}
|
||||
}
|
||||
|
||||
function writeCachedUIPrefs(prefs: CachedUIPrefs) {
|
||||
const maxAge = 60 * 60 * 24 * 30
|
||||
const payload = encodeURIComponent(JSON.stringify(prefs))
|
||||
document.cookie = `${uiPrefsCookieName}=${payload}; Max-Age=${maxAge}; Path=/; SameSite=Lax`
|
||||
}
|
||||
|
||||
const imageExt = new Set(['png', 'jpg', 'jpeg', 'webp', 'gif', 'bmp', 'svg'])
|
||||
const codeExt = new Set(['ts', 'tsx', 'js', 'jsx', 'go', 'py', 'rs', 'java', 'c', 'cpp', 'css', 'html'])
|
||||
const docExt = new Set(['txt', 'md', 'pdf', 'doc', 'docx', 'odt', 'rtf'])
|
||||
@@ -497,8 +550,10 @@ export default function App() {
|
||||
const [route, setRoute] = useState<Route>(routeFromPath(window.location.pathname))
|
||||
const [lang, setLang] = useState<Lang>(detectLang())
|
||||
const [err, setErr] = useState('')
|
||||
const [bootstrapping, setBootstrapping] = useState(true)
|
||||
|
||||
const [user, setUser] = useState<User | null>(null)
|
||||
const [cachedUIPrefs, setCachedUIPrefs] = useState<CachedUIPrefs | null>(() => readCachedUIPrefs())
|
||||
const [admin, setAdmin] = useState<string | null>(null)
|
||||
const [files, setFiles] = useState<FileEntry[]>([])
|
||||
const [path, setPath] = useState('/')
|
||||
@@ -551,8 +606,11 @@ export default function App() {
|
||||
const driveHistoryRef = useRef<string[]>(['/'])
|
||||
const driveHistoryIndexRef = useRef(0)
|
||||
|
||||
const effectiveTheme = useMemo(() => user?.theme ?? 'dracula', [user?.theme])
|
||||
const effectiveMode = useMemo(() => (user?.colorMode && user.colorMode !== 'auto' ? user.colorMode : detectMode()), [user?.colorMode])
|
||||
const effectiveTheme = useMemo(() => user?.theme ?? cachedUIPrefs?.theme ?? 'dracula', [user?.theme, cachedUIPrefs?.theme])
|
||||
const effectiveMode = useMemo(() => {
|
||||
const mode = user?.colorMode ?? cachedUIPrefs?.colorMode ?? 'auto'
|
||||
return mode !== 'auto' ? mode : detectMode()
|
||||
}, [user?.colorMode, cachedUIPrefs?.colorMode])
|
||||
const t = (k: string) => dict[lang][k] ?? k
|
||||
|
||||
useEffect(() => {
|
||||
@@ -729,9 +787,18 @@ export default function App() {
|
||||
}, [])
|
||||
|
||||
const bootstrap = useCallback(async () => {
|
||||
setBootstrapping(true)
|
||||
try {
|
||||
const me = await api<User>('/api/auth/me')
|
||||
setUser(me)
|
||||
const nextPrefs: CachedUIPrefs = {
|
||||
username: me.username.toLowerCase(),
|
||||
theme: me.theme,
|
||||
colorMode: me.colorMode,
|
||||
archiveFormat: me.archiveFormat,
|
||||
}
|
||||
setCachedUIPrefs(nextPrefs)
|
||||
writeCachedUIPrefs(nextPrefs)
|
||||
const protocols = await api<ProtocolInfo>('/api/user/protocols')
|
||||
setProtocolInfo(protocols)
|
||||
const initial = routeFromPath(window.location.pathname) === 'drive' ? drivePathFromLocation() : '/'
|
||||
@@ -748,6 +815,8 @@ export default function App() {
|
||||
await loadUsers()
|
||||
} catch {
|
||||
setAdmin(null)
|
||||
} finally {
|
||||
setBootstrapping(false)
|
||||
}
|
||||
}, [loadFiles, loadUsers])
|
||||
|
||||
@@ -764,6 +833,14 @@ export default function App() {
|
||||
try {
|
||||
const me = await api<User>('/api/auth/login', { method: 'POST', body: JSON.stringify({ username, password }) })
|
||||
setUser(me)
|
||||
const nextPrefs: CachedUIPrefs = {
|
||||
username: me.username.toLowerCase(),
|
||||
theme: me.theme,
|
||||
colorMode: me.colorMode,
|
||||
archiveFormat: me.archiveFormat,
|
||||
}
|
||||
setCachedUIPrefs(nextPrefs)
|
||||
writeCachedUIPrefs(nextPrefs)
|
||||
const protocols = await api<ProtocolInfo>('/api/user/protocols')
|
||||
setProtocolInfo(protocols)
|
||||
setUsername('')
|
||||
@@ -860,7 +937,16 @@ export default function App() {
|
||||
async function savePref(theme: Theme, colorMode: ColorMode, archiveFormat: ArchiveFormat) {
|
||||
if (!user) return
|
||||
await api('/api/user/preferences', { method: 'POST', body: JSON.stringify({ theme, colorMode, archiveFormat }) })
|
||||
setUser({ ...user, theme, colorMode, archiveFormat })
|
||||
const nextUser = { ...user, theme, colorMode, archiveFormat }
|
||||
setUser(nextUser)
|
||||
const nextPrefs: CachedUIPrefs = {
|
||||
username: nextUser.username.toLowerCase(),
|
||||
theme: nextUser.theme,
|
||||
colorMode: nextUser.colorMode,
|
||||
archiveFormat: nextUser.archiveFormat,
|
||||
}
|
||||
setCachedUIPrefs(nextPrefs)
|
||||
writeCachedUIPrefs(nextPrefs)
|
||||
}
|
||||
|
||||
async function createUser(e: FormEvent) {
|
||||
@@ -1090,19 +1176,28 @@ export default function App() {
|
||||
})
|
||||
}
|
||||
|
||||
return (
|
||||
<main className="relative min-h-screen overflow-hidden bg-gradient-to-br from-background via-background to-muted/30 p-4 md:p-6">
|
||||
<div className="pointer-events-none absolute -left-24 top-8 h-72 w-72 rounded-full bg-primary/10 blur-3xl" />
|
||||
<div className="pointer-events-none absolute -right-24 bottom-0 h-72 w-72 rounded-full bg-primary/15 blur-3xl" />
|
||||
const authLoadingCard = (
|
||||
<Card className="control-shell mx-auto max-w-md bg-card/95">
|
||||
<CardContent className="flex min-h-[220px] flex-col items-center justify-center gap-3 p-6">
|
||||
<div className="h-9 w-9 animate-spin border-2 border-border border-t-primary" aria-hidden="true" />
|
||||
<p className="text-xs uppercase tracking-[0.18em] text-muted-foreground">{t('loading')}...</p>
|
||||
</CardContent>
|
||||
</Card>
|
||||
)
|
||||
|
||||
<div className="container relative z-10 space-y-5">
|
||||
<Card className="border-none bg-card/95 shadow-lg backdrop-blur">
|
||||
<CardContent className="flex flex-wrap items-center justify-between gap-3 p-4">
|
||||
return (
|
||||
<main className="relative min-h-screen overflow-hidden p-4 md:p-6">
|
||||
<div className="pointer-events-none absolute inset-0 bg-[linear-gradient(180deg,transparent_0%,hsl(var(--primary)/0.06)_100%)]" />
|
||||
<div className="pointer-events-none absolute inset-0 opacity-30 [background:repeating-linear-gradient(0deg,transparent_0,transparent_2px,hsla(0,0%,100%,0.03)_3px)]" />
|
||||
|
||||
<div className="relative z-10 mx-auto max-w-[1660px] space-y-5">
|
||||
<Card className="control-shell border-primary/35 bg-card/95">
|
||||
<CardContent className="flex flex-wrap items-center justify-between gap-3 p-4 md:p-5">
|
||||
<div className="flex items-center gap-3">
|
||||
<div className="rounded-xl bg-primary/10 p-2 text-primary"><HardDriveUpload className="h-5 w-5" /></div>
|
||||
<div className="border-2 border-primary bg-primary/15 p-2 text-primary"><HardDriveUpload className="h-5 w-5" /></div>
|
||||
<div>
|
||||
<p className="text-lg font-semibold">{t('brand')}</p>
|
||||
<p className="text-xs text-muted-foreground">{route === 'admin' ? '/admin' : route === 'drive' ? '/drive' : '/'}</p>
|
||||
<p className="font-display text-lg font-semibold">{t('brand')}</p>
|
||||
<p className="text-[11px] uppercase tracking-[0.18em] text-muted-foreground">{route === 'admin' ? '/admin' : route === 'drive' ? '/drive' : '/'}</p>
|
||||
</div>
|
||||
</div>
|
||||
<div className="flex items-center gap-2">
|
||||
@@ -1129,7 +1224,7 @@ export default function App() {
|
||||
</CardContent>
|
||||
</Card>
|
||||
|
||||
{err ? <Card className="border-destructive/40"><CardContent className="p-3 text-sm text-destructive">{err}</CardContent></Card> : null}
|
||||
{err ? <Card className="control-shell border-destructive/60"><CardContent className="p-3 text-sm font-medium uppercase tracking-wide text-destructive">{err}</CardContent></Card> : null}
|
||||
|
||||
{user ? (
|
||||
<Dialog open={settingsOpen} onOpenChange={setSettingsOpen}>
|
||||
@@ -1219,11 +1314,11 @@ export default function App() {
|
||||
|
||||
{route === 'landing' ? (
|
||||
<section className="grid gap-4 lg:grid-cols-[1.35fr_1fr]">
|
||||
<Card className="border-none bg-gradient-to-br from-card via-card to-primary/10 shadow-xl">
|
||||
<Card className="control-shell border-primary/40 bg-[linear-gradient(135deg,hsl(var(--card))_0%,hsl(var(--card)/0.8)_55%,hsl(var(--primary)/0.18)_100%)]">
|
||||
<CardHeader className="space-y-4">
|
||||
<Badge variant="secondary" className="w-fit">Production ready</Badge>
|
||||
<CardTitle className="max-w-2xl text-3xl leading-tight md:text-5xl">{t('heroTitle')}</CardTitle>
|
||||
<CardDescription className="max-w-xl text-base md:text-lg">{t('heroDesc')}</CardDescription>
|
||||
<Badge variant="secondary" className="w-fit border border-border bg-muted/30 uppercase tracking-[0.14em]">Control Node</Badge>
|
||||
<CardTitle className="font-display max-w-2xl text-3xl leading-tight md:text-6xl">{t('heroTitle')}</CardTitle>
|
||||
<CardDescription className="max-w-xl text-base text-muted-foreground md:text-lg">{t('heroDesc')}</CardDescription>
|
||||
</CardHeader>
|
||||
<CardContent className="space-y-6">
|
||||
<div className="flex gap-2">
|
||||
@@ -1231,17 +1326,17 @@ export default function App() {
|
||||
<Button size="lg" variant="outline" onClick={() => navigate('admin')}>{t('openAdmin')}</Button>
|
||||
</div>
|
||||
<div className="grid gap-3 sm:grid-cols-3">
|
||||
<div className="rounded-xl border bg-background/70 p-3 text-sm"><p className="font-semibold">Argon2id Auth</p><p className="text-xs text-muted-foreground">Hardened credentials and sessions</p></div>
|
||||
<div className="rounded-xl border bg-background/70 p-3 text-sm"><p className="font-semibold">Isolated Storage</p><p className="text-xs text-muted-foreground">Per-account file boundaries</p></div>
|
||||
<div className="rounded-xl border bg-background/70 p-3 text-sm"><p className="font-semibold">Share + Resume</p><p className="text-xs text-muted-foreground">Expiring links and byte-range downloads</p></div>
|
||||
<div className="border-2 border-border bg-background/80 p-3 text-sm"><p className="font-display text-base">Argon2id Auth</p><p className="text-xs uppercase tracking-wide text-muted-foreground">Hardened credentials and sessions</p></div>
|
||||
<div className="border-2 border-border bg-background/80 p-3 text-sm"><p className="font-display text-base">Isolated Storage</p><p className="text-xs uppercase tracking-wide text-muted-foreground">Per-account file boundaries</p></div>
|
||||
<div className="border-2 border-border bg-background/80 p-3 text-sm"><p className="font-display text-base">Share + Resume</p><p className="text-xs uppercase tracking-wide text-muted-foreground">Expiring links and byte-range downloads</p></div>
|
||||
</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
|
||||
<Card className="border-none bg-card/95 shadow-xl">
|
||||
<Card className="control-shell bg-card/95">
|
||||
<CardHeader>
|
||||
<CardTitle>{t('userLogin')}</CardTitle>
|
||||
<CardDescription>{t('registrationDisabled')}</CardDescription>
|
||||
<CardTitle className="font-display text-3xl">{t('userLogin')}</CardTitle>
|
||||
<CardDescription className="max-w-md uppercase tracking-wide">{t('registrationDisabled')}</CardDescription>
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
<form className="space-y-3" onSubmit={loginUser}>
|
||||
@@ -1255,8 +1350,8 @@ export default function App() {
|
||||
</section>
|
||||
) : route === 'drive' ? (
|
||||
user ? (
|
||||
<section className="grid gap-4 lg:grid-cols-[250px_1fr]">
|
||||
<Card className="h-fit border-none bg-card/95 shadow-lg">
|
||||
<section className="grid gap-4 lg:grid-cols-[280px_1fr]">
|
||||
<Card className="control-shell h-fit bg-card/95">
|
||||
<Suspense fallback={<CardContent className="p-4 text-sm text-muted-foreground">Loading...</CardContent>}>
|
||||
<TransferSection
|
||||
username={user.username}
|
||||
@@ -1281,10 +1376,10 @@ export default function App() {
|
||||
</Suspense>
|
||||
</Card>
|
||||
|
||||
<Card className={`border-none bg-card/95 shadow-lg ${dragActive ? 'ring-2 ring-primary/40' : ''}`}>
|
||||
<Card className={`control-shell bg-card/95 ${dragActive ? 'ring-2 ring-primary/50' : ''}`}>
|
||||
<CardHeader className="space-y-3 pb-2">
|
||||
<div className="flex items-center justify-between">
|
||||
<CardTitle>{t('drive')}</CardTitle>
|
||||
<CardTitle className="font-display text-3xl">{t('drive')}</CardTitle>
|
||||
<div className="flex items-center gap-2">
|
||||
{selectedCount > 0 ? (
|
||||
<Badge variant="outline">{selectedCount} {t('selected')}</Badge>
|
||||
@@ -1296,7 +1391,7 @@ export default function App() {
|
||||
<Search className="pointer-events-none absolute left-3 top-3 h-4 w-4 text-muted-foreground" />
|
||||
<Input ref={searchRef} className="h-11 rounded-xl pl-9" value={query} onChange={(e) => setQuery(e.target.value)} placeholder={t('search')} />
|
||||
</div>
|
||||
<div className="-mx-1 flex items-center gap-1 overflow-x-auto px-1 text-xs text-muted-foreground whitespace-nowrap">
|
||||
<div className="-mx-1 flex items-center gap-1 overflow-x-auto px-1 text-xs uppercase tracking-[0.14em] text-muted-foreground whitespace-nowrap">
|
||||
<Button
|
||||
size="sm"
|
||||
variant="ghost"
|
||||
@@ -1329,11 +1424,11 @@ export default function App() {
|
||||
)
|
||||
})}
|
||||
</div>
|
||||
<p className="text-[11px] text-muted-foreground">{t('shortcuts')}</p>
|
||||
<p className="text-[11px] uppercase tracking-[0.16em] text-muted-foreground">{t('shortcuts')}</p>
|
||||
</CardHeader>
|
||||
|
||||
<CardContent
|
||||
className="space-y-2"
|
||||
className="min-h-[420px] space-y-2"
|
||||
onMouseDown={(e) => {
|
||||
if (e.button !== 0) return
|
||||
if (isSelectionIgnoredTarget(e.target)) return
|
||||
@@ -1389,7 +1484,7 @@ export default function App() {
|
||||
/>
|
||||
<ContextMenu>
|
||||
<ContextMenuTrigger asChild>
|
||||
<div className="space-y-2">
|
||||
<div className="flex min-h-full flex-col gap-2">
|
||||
|
||||
<Dialog open={shareDialog} onOpenChange={(v) => setShareDialog(v)}>
|
||||
<DialogContent>
|
||||
@@ -1477,14 +1572,14 @@ export default function App() {
|
||||
</DialogContent>
|
||||
</Dialog>
|
||||
|
||||
<div className="hidden grid-cols-[1fr_110px_160px] rounded-xl border bg-muted/40 p-3 text-xs font-medium text-muted-foreground lg:grid">
|
||||
<div className="hidden grid-cols-[1fr_110px_160px] border-2 border-border bg-muted/40 p-3 text-xs font-semibold uppercase tracking-[0.14em] text-muted-foreground lg:grid">
|
||||
<button onClick={() => toggleSort('name')} className="text-left">{t('name')}</button>
|
||||
<button onClick={() => toggleSort('size')} className="text-left">{t('size')}</button>
|
||||
<button onClick={() => toggleSort('modTime')} className="text-left">{t('modified')}</button>
|
||||
</div>
|
||||
|
||||
{visibleFiles.length === 0 ? (
|
||||
<div className="rounded-xl border border-dashed p-10 text-center text-sm text-muted-foreground">{t('empty')}</div>
|
||||
<div className="border-2 border-dashed border-border p-10 text-center text-sm uppercase tracking-wide text-muted-foreground">{t('empty')}</div>
|
||||
) : (
|
||||
visibleFiles.map((f) => (
|
||||
<ContextMenu key={f.path}>
|
||||
@@ -1492,7 +1587,7 @@ export default function App() {
|
||||
<div
|
||||
data-file-row="true"
|
||||
draggable
|
||||
className={`group grid gap-2 rounded-xl border bg-background p-3 transition hover:shadow-sm lg:grid-cols-[1fr_110px_160px] lg:items-center ${selectedPaths[f.path] ? 'ring-2 ring-primary/45 bg-gradient-to-r from-primary/10 via-background to-background shadow-[0_0_22px_hsl(var(--primary)/0.18)]' : ''} ${dropFolderPath === f.path ? 'border-primary/70 bg-primary/10' : ''} ${draggingPaths.includes(f.path) ? 'opacity-70' : ''} ${paintSelect.active ? 'select-none' : ''}`}
|
||||
className={`group grid gap-2 border-2 border-border bg-background/90 p-3 transition hover:bg-muted/35 lg:grid-cols-[1fr_110px_160px] lg:items-center ${selectedPaths[f.path] ? 'ring-2 ring-primary/45 bg-gradient-to-r from-primary/12 via-background to-background shadow-[0_0_24px_hsl(var(--primary)/0.22)]' : ''} ${dropFolderPath === f.path ? 'border-primary/70 bg-primary/12' : ''} ${draggingPaths.includes(f.path) ? 'opacity-70' : ''} ${paintSelect.active ? 'select-none' : ''}`}
|
||||
onMouseDown={(e) => {
|
||||
if (e.button !== 0) return
|
||||
if (isSelectionIgnoredTarget(e.target)) return
|
||||
@@ -1566,7 +1661,7 @@ export default function App() {
|
||||
<div className="min-w-0">
|
||||
<div className="flex items-center gap-2">
|
||||
<KindIcon file={f} />
|
||||
<p className="truncate text-left text-sm font-medium hover:underline">{f.name}</p>
|
||||
<p className="truncate text-left text-sm font-semibold uppercase tracking-[0.08em] hover:underline">{f.name}</p>
|
||||
</div>
|
||||
<div className="mt-2 flex flex-wrap items-center gap-1">
|
||||
{(f.tags ?? []).map((tag) => (
|
||||
@@ -1600,6 +1695,7 @@ export default function App() {
|
||||
</ContextMenu>
|
||||
))
|
||||
)}
|
||||
<div className="flex-1" />
|
||||
</div>
|
||||
</ContextMenuTrigger>
|
||||
<ContextMenuContent>
|
||||
@@ -1618,10 +1714,12 @@ export default function App() {
|
||||
</CardContent>
|
||||
</Card>
|
||||
</section>
|
||||
) : bootstrapping ? (
|
||||
authLoadingCard
|
||||
) : (
|
||||
<Card className="mx-auto max-w-md border-none bg-card/95 shadow-lg">
|
||||
<Card className="control-shell mx-auto max-w-md bg-card/95">
|
||||
<CardHeader>
|
||||
<CardTitle>{t('userLogin')}</CardTitle>
|
||||
<CardTitle className="font-display text-3xl">{t('userLogin')}</CardTitle>
|
||||
<CardDescription>{t('registrationDisabled')}</CardDescription>
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
@@ -1655,11 +1753,13 @@ export default function App() {
|
||||
onDeleteUser={(id) => { void api('/api/admin/users/' + id, { method: 'DELETE' }).then(loadUsers) }}
|
||||
/>
|
||||
</Suspense>
|
||||
) : bootstrapping ? (
|
||||
authLoadingCard
|
||||
) : (
|
||||
<Card className="mx-auto max-w-md border-none bg-card/95 shadow-lg">
|
||||
<Card className="control-shell mx-auto max-w-md bg-card/95">
|
||||
<CardHeader>
|
||||
<CardTitle>{t('adminLogin')}</CardTitle>
|
||||
<CardDescription>{t('configuredFromEnv')}</CardDescription>
|
||||
<CardTitle className="font-display text-3xl">{t('adminLogin')}</CardTitle>
|
||||
<CardDescription className="uppercase tracking-[0.12em]">{t('configuredFromEnv')}</CardDescription>
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
<form className="space-y-3" onSubmit={loginAdmin}>
|
||||
@@ -1671,8 +1771,8 @@ export default function App() {
|
||||
</Card>
|
||||
)}
|
||||
|
||||
<footer className="py-2 text-center text-xs text-muted-foreground">
|
||||
<span className="inline-flex items-center gap-1"><Star className="h-3 w-3" /> FileZ</span>
|
||||
<footer className="py-2 text-center text-xs uppercase tracking-[0.2em] text-muted-foreground">
|
||||
<span className="inline-flex items-center gap-1"><Star className="h-3 w-3" /> FileZ Control Surface</span>
|
||||
</footer>
|
||||
</div>
|
||||
</main>
|
||||
|
||||
@@ -4,20 +4,20 @@ import { cva, type VariantProps } from 'class-variance-authority'
|
||||
import { cn } from '../../lib/utils'
|
||||
|
||||
const buttonVariants = cva(
|
||||
'inline-flex items-center justify-center whitespace-nowrap rounded-md text-sm font-medium transition-colors focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-primary/40 disabled:pointer-events-none disabled:opacity-50',
|
||||
'inline-flex items-center justify-center whitespace-nowrap rounded-none border-2 border-transparent text-sm font-semibold uppercase tracking-[0.08em] transition-colors focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-primary/40 disabled:pointer-events-none disabled:opacity-50',
|
||||
{
|
||||
variants: {
|
||||
variant: {
|
||||
default: 'bg-primary text-primary-foreground hover:bg-primary/90',
|
||||
outline: 'border border-input bg-background hover:bg-muted',
|
||||
secondary: 'bg-muted text-foreground hover:bg-muted/80',
|
||||
ghost: 'hover:bg-muted hover:text-foreground',
|
||||
destructive: 'bg-destructive text-destructive-foreground hover:bg-destructive/90',
|
||||
default: 'border-primary bg-primary text-primary-foreground hover:bg-primary/85',
|
||||
outline: 'border-input bg-background hover:bg-muted',
|
||||
secondary: 'border-border bg-muted text-foreground hover:bg-muted/70',
|
||||
ghost: 'border-transparent hover:border-border hover:bg-muted/40 hover:text-foreground',
|
||||
destructive: 'border-destructive bg-destructive text-destructive-foreground hover:bg-destructive/85',
|
||||
},
|
||||
size: {
|
||||
default: 'h-10 px-4 py-2',
|
||||
sm: 'h-9 rounded-md px-3',
|
||||
lg: 'h-11 rounded-md px-8',
|
||||
sm: 'h-8 px-3 text-xs',
|
||||
lg: 'h-11 px-8',
|
||||
},
|
||||
},
|
||||
defaultVariants: {
|
||||
|
||||
@@ -3,7 +3,7 @@ import * as React from 'react'
|
||||
import { cn } from '../../lib/utils'
|
||||
|
||||
export function Card({ className, ...props }: React.HTMLAttributes<HTMLDivElement>) {
|
||||
return <div className={cn('rounded-xl border bg-card text-card-foreground shadow-sm', className)} {...props} />
|
||||
return <div className={cn('rounded-none border-2 border-border bg-card text-card-foreground shadow-[6px_6px_0_hsl(var(--shadow-strong))]', className)} {...props} />
|
||||
}
|
||||
|
||||
export function CardHeader({ className, ...props }: React.HTMLAttributes<HTMLDivElement>) {
|
||||
|
||||
@@ -14,7 +14,7 @@ export const DialogOverlay = React.forwardRef<
|
||||
React.ElementRef<typeof DialogPrimitive.Overlay>,
|
||||
React.ComponentPropsWithoutRef<typeof DialogPrimitive.Overlay>
|
||||
>(({ className, ...props }, ref) => (
|
||||
<DialogPrimitive.Overlay ref={ref} className={cn('fixed inset-0 z-50 bg-black/40', className)} {...props} />
|
||||
<DialogPrimitive.Overlay ref={ref} className={cn('fixed inset-0 z-50 bg-black/70 backdrop-blur-[1px]', className)} {...props} />
|
||||
))
|
||||
DialogOverlay.displayName = DialogPrimitive.Overlay.displayName
|
||||
|
||||
@@ -27,7 +27,7 @@ export const DialogContent = React.forwardRef<
|
||||
<DialogPrimitive.Content
|
||||
ref={ref}
|
||||
className={cn(
|
||||
'fixed left-[50%] top-[50%] z-50 grid w-full max-w-lg translate-x-[-50%] translate-y-[-50%] gap-4 rounded-lg border bg-card p-6 shadow-lg',
|
||||
'fixed left-[50%] top-[50%] z-50 grid w-full max-w-lg translate-x-[-50%] translate-y-[-50%] gap-4 rounded-none border-2 border-border bg-card p-6 shadow-[8px_8px_0_hsl(var(--shadow-strong))]',
|
||||
className,
|
||||
)}
|
||||
{...props}
|
||||
|
||||
@@ -7,7 +7,7 @@ const Input = React.forwardRef<HTMLInputElement, React.ComponentProps<'input'>>(
|
||||
<input
|
||||
type={type}
|
||||
className={cn(
|
||||
'flex h-10 w-full rounded-md border border-input bg-background px-3 py-2 text-sm ring-offset-background file:border-0 file:bg-transparent file:text-sm file:font-medium placeholder:text-muted-foreground focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-primary/40 disabled:cursor-not-allowed disabled:opacity-50',
|
||||
'flex h-10 w-full rounded-none border-2 border-input bg-background px-3 py-2 text-sm ring-offset-background file:border-0 file:bg-transparent file:text-sm file:font-medium placeholder:text-muted-foreground focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-primary/40 disabled:cursor-not-allowed disabled:opacity-50',
|
||||
className,
|
||||
)}
|
||||
ref={ref}
|
||||
|
||||
@@ -14,7 +14,7 @@ export const SelectTrigger = React.forwardRef<
|
||||
<SelectPrimitive.Trigger
|
||||
ref={ref}
|
||||
className={cn(
|
||||
'flex h-10 w-full items-center justify-between rounded-md border border-input bg-background px-3 py-2 text-sm ring-offset-background placeholder:text-muted-foreground focus:outline-none focus:ring-2 focus:ring-primary/40 disabled:cursor-not-allowed disabled:opacity-50',
|
||||
'flex h-10 w-full items-center justify-between rounded-none border-2 border-input bg-background px-3 py-2 text-sm ring-offset-background placeholder:text-muted-foreground focus:outline-none focus:ring-2 focus:ring-primary/40 disabled:cursor-not-allowed disabled:opacity-50',
|
||||
className,
|
||||
)}
|
||||
{...props}
|
||||
@@ -34,7 +34,7 @@ export const SelectContent = React.forwardRef<
|
||||
<SelectPrimitive.Portal>
|
||||
<SelectPrimitive.Content
|
||||
ref={ref}
|
||||
className={cn('relative z-50 min-w-[8rem] overflow-hidden rounded-md border bg-card text-card-foreground shadow-md', className)}
|
||||
className={cn('relative z-50 min-w-[8rem] overflow-hidden rounded-none border-2 border-border bg-card text-card-foreground shadow-[4px_4px_0_hsl(var(--shadow-strong))]', className)}
|
||||
{...props}
|
||||
>
|
||||
<SelectPrimitive.Viewport className="p-1">{children}</SelectPrimitive.Viewport>
|
||||
@@ -49,7 +49,7 @@ export const SelectItem = React.forwardRef<
|
||||
>(({ className, children, ...props }, ref) => (
|
||||
<SelectPrimitive.Item
|
||||
ref={ref}
|
||||
className={cn('relative flex w-full cursor-default select-none items-center rounded-sm py-1.5 pl-8 pr-2 text-sm outline-none focus:bg-muted', className)}
|
||||
className={cn('relative flex w-full cursor-default select-none items-center rounded-none py-1.5 pl-8 pr-2 text-sm outline-none focus:bg-muted', className)}
|
||||
{...props}
|
||||
>
|
||||
<span className="absolute left-2 flex h-3.5 w-3.5 items-center justify-center">
|
||||
|
||||
@@ -11,7 +11,10 @@ export const TabsList = React.forwardRef<
|
||||
>(({ className, ...props }, ref) => (
|
||||
<TabsPrimitive.List
|
||||
ref={ref}
|
||||
className={cn('inline-flex h-10 items-center justify-center rounded-md bg-muted p-1 text-muted-foreground', className)}
|
||||
className={cn(
|
||||
'inline-flex h-10 items-center justify-center rounded-none border border-[#415273] bg-[#253149] p-1 text-[#95a4bf] shadow-[inset_0_0_0_1px_rgba(138,164,210,0.08)]',
|
||||
className,
|
||||
)}
|
||||
{...props}
|
||||
/>
|
||||
))
|
||||
@@ -24,7 +27,7 @@ export const TabsTrigger = React.forwardRef<
|
||||
<TabsPrimitive.Trigger
|
||||
ref={ref}
|
||||
className={cn(
|
||||
'inline-flex items-center justify-center whitespace-nowrap rounded-sm px-3 py-1.5 text-sm font-medium ring-offset-background transition-all focus:outline-none focus:ring-2 focus:ring-primary/40 data-[state=active]:bg-card data-[state=active]:text-foreground data-[state=active]:shadow-sm',
|
||||
'inline-flex items-center justify-center whitespace-nowrap rounded-none border border-transparent px-3 py-1.5 text-sm font-semibold uppercase tracking-[0.08em] text-[#99a8c4] transition-colors focus:outline-none focus:ring-2 focus:ring-[#6a84b5]/60 hover:bg-[#2a3750] hover:text-[#d2deef] data-[state=active]:border-[#566a8e] data-[state=active]:bg-[#303e58] data-[state=active]:text-[#e6eefb]',
|
||||
className,
|
||||
)}
|
||||
{...props}
|
||||
|
||||
@@ -8,7 +8,7 @@ const Textarea = React.forwardRef<HTMLTextAreaElement, TextareaProps>(({ classNa
|
||||
return (
|
||||
<textarea
|
||||
className={cn(
|
||||
'flex min-h-[80px] w-full rounded-md border border-input bg-background px-3 py-2 text-sm ring-offset-background placeholder:text-muted-foreground focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-primary/40 disabled:cursor-not-allowed disabled:opacity-50',
|
||||
'flex min-h-[80px] w-full rounded-none border-2 border-input bg-background px-3 py-2 text-sm ring-offset-background placeholder:text-muted-foreground focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-primary/40 disabled:cursor-not-allowed disabled:opacity-50',
|
||||
className,
|
||||
)}
|
||||
ref={ref}
|
||||
|
||||
@@ -4,39 +4,41 @@
|
||||
|
||||
@layer base {
|
||||
:root {
|
||||
--radius: 0.9rem;
|
||||
--radius: 0rem;
|
||||
|
||||
--background: 218 33% 97%;
|
||||
--foreground: 222 23% 16%;
|
||||
--card: 0 0% 100%;
|
||||
--card-foreground: 222 23% 16%;
|
||||
--background: 220 20% 9%;
|
||||
--foreground: 200 20% 92%;
|
||||
--card: 222 18% 12%;
|
||||
--card-foreground: 200 20% 92%;
|
||||
--popover: var(--card);
|
||||
--popover-foreground: var(--card-foreground);
|
||||
--border: 215 24% 86%;
|
||||
--input: 215 24% 86%;
|
||||
--primary: 217 87% 52%;
|
||||
--border: 213 14% 33%;
|
||||
--input: 213 14% 33%;
|
||||
--primary: 355 96% 59%;
|
||||
--primary-foreground: 0 0% 100%;
|
||||
--muted: 216 25% 93%;
|
||||
--muted-foreground: 220 12% 41%;
|
||||
--muted: 219 16% 16%;
|
||||
--muted-foreground: 214 15% 70%;
|
||||
--accent: var(--muted);
|
||||
--accent-foreground: var(--foreground);
|
||||
--destructive: 359 65% 55%;
|
||||
--destructive: 354 91% 52%;
|
||||
--destructive-foreground: 0 0% 100%;
|
||||
--shadow-strong: 220 30% 5%;
|
||||
}
|
||||
|
||||
.dark {
|
||||
--background: 223 28% 10%;
|
||||
--foreground: 214 29% 92%;
|
||||
--card: 223 24% 14%;
|
||||
--card-foreground: 214 29% 92%;
|
||||
--border: 223 17% 24%;
|
||||
--input: 223 17% 24%;
|
||||
--primary: 214 95% 72%;
|
||||
--primary-foreground: 223 28% 10%;
|
||||
--muted: 223 18% 18%;
|
||||
--muted-foreground: 217 13% 69%;
|
||||
--destructive: 358 70% 66%;
|
||||
--background: 222 20% 8%;
|
||||
--foreground: 210 24% 92%;
|
||||
--card: 220 16% 11%;
|
||||
--card-foreground: 210 24% 92%;
|
||||
--border: 213 14% 30%;
|
||||
--input: 213 14% 30%;
|
||||
--primary: 355 96% 59%;
|
||||
--primary-foreground: 0 0% 100%;
|
||||
--muted: 219 16% 14%;
|
||||
--muted-foreground: 214 14% 68%;
|
||||
--destructive: 354 91% 52%;
|
||||
--destructive-foreground: 0 0% 100%;
|
||||
--shadow-strong: 220 30% 4%;
|
||||
}
|
||||
|
||||
html[data-theme='dracula'] {
|
||||
@@ -123,7 +125,46 @@
|
||||
|
||||
body {
|
||||
@apply bg-background text-foreground antialiased;
|
||||
font-family: Manrope, 'Noto Sans', 'Segoe UI', sans-serif;
|
||||
font-family: 'Space Grotesk', 'IBM Plex Sans Condensed', 'Sora', sans-serif;
|
||||
letter-spacing: 0.01em;
|
||||
background-image:
|
||||
linear-gradient(180deg, hsl(var(--background)) 0%, hsl(222 26% 7%) 100%),
|
||||
linear-gradient(hsla(0 0% 100% / 0.045) 1px, transparent 1px),
|
||||
linear-gradient(90deg, hsla(0 0% 100% / 0.045) 1px, transparent 1px),
|
||||
radial-gradient(1200px 650px at 74% -8%, hsl(var(--primary) / 0.16), transparent 56%),
|
||||
radial-gradient(980px 560px at -8% 102%, hsl(196 100% 58% / 0.11), transparent 62%);
|
||||
background-size: auto, 42px 42px, 42px 42px, auto, auto;
|
||||
background-attachment: fixed;
|
||||
}
|
||||
|
||||
#root {
|
||||
min-height: 100vh;
|
||||
}
|
||||
|
||||
h1,
|
||||
h2,
|
||||
h3,
|
||||
.font-display {
|
||||
font-family: 'Archivo Black', 'Anton', 'Space Grotesk', sans-serif;
|
||||
letter-spacing: 0.02em;
|
||||
text-transform: uppercase;
|
||||
}
|
||||
|
||||
.control-shell {
|
||||
position: relative;
|
||||
overflow: hidden;
|
||||
border: 2px solid hsl(var(--border));
|
||||
background: linear-gradient(165deg, hsl(var(--card) / 0.96), hsl(var(--card) / 0.82));
|
||||
box-shadow: 8px 8px 0 hsl(var(--shadow-strong));
|
||||
}
|
||||
|
||||
.control-shell::after {
|
||||
content: '';
|
||||
position: absolute;
|
||||
inset: 0;
|
||||
pointer-events: none;
|
||||
background: linear-gradient(120deg, transparent 20%, hsl(var(--primary) / 0.07) 70%, transparent 100%);
|
||||
mix-blend-mode: screen;
|
||||
}
|
||||
|
||||
.markdown-preview {
|
||||
|
||||
@@ -47,12 +47,12 @@ export default function AdminPanel(props: Props) {
|
||||
} = props
|
||||
|
||||
return (
|
||||
<Card className="border-none bg-card/95 shadow-lg">
|
||||
<Card className="control-shell bg-card/95">
|
||||
<CardHeader>
|
||||
<div className="flex items-center justify-between">
|
||||
<div>
|
||||
<CardTitle>{t('admin')}</CardTitle>
|
||||
<CardDescription>{admin}</CardDescription>
|
||||
<CardTitle className="font-display text-3xl">{t('admin')}</CardTitle>
|
||||
<CardDescription className="uppercase tracking-[0.14em]">{admin}</CardDescription>
|
||||
</div>
|
||||
<Button variant="outline" onClick={onLogout}>
|
||||
<LogOut className="mr-2 h-4 w-4" /> {t('logout')}
|
||||
@@ -60,8 +60,8 @@ export default function AdminPanel(props: Props) {
|
||||
</div>
|
||||
</CardHeader>
|
||||
<CardContent className="grid gap-4 lg:grid-cols-2">
|
||||
<Card className="shadow-sm">
|
||||
<CardHeader><CardTitle className="text-base">{t('createUser')}</CardTitle></CardHeader>
|
||||
<Card className="border-2 border-border bg-background/70 shadow-[5px_5px_0_hsl(var(--shadow-strong))]">
|
||||
<CardHeader><CardTitle className="font-display text-2xl">{t('createUser')}</CardTitle></CardHeader>
|
||||
<CardContent>
|
||||
<form className="space-y-3" onSubmit={onCreateUser}>
|
||||
<Input value={newUsername} onChange={(e) => setNewUsername(e.target.value)} placeholder={t('username')} required />
|
||||
@@ -81,14 +81,14 @@ export default function AdminPanel(props: Props) {
|
||||
</CardContent>
|
||||
</Card>
|
||||
|
||||
<Card className="shadow-sm">
|
||||
<CardHeader><CardTitle className="text-base">{t('users')}</CardTitle></CardHeader>
|
||||
<Card className="border-2 border-border bg-background/70 shadow-[5px_5px_0_hsl(var(--shadow-strong))]">
|
||||
<CardHeader><CardTitle className="font-display text-2xl">{t('users')}</CardTitle></CardHeader>
|
||||
<CardContent className="space-y-2">
|
||||
{users.map((u) => (
|
||||
<div key={u.id} className="flex items-center justify-between rounded-md border p-2 text-sm">
|
||||
<div key={u.id} className="flex items-center justify-between border-2 border-border bg-background/85 p-2 text-sm">
|
||||
<div className="min-w-0">
|
||||
<p className="truncate font-medium">{u.username}</p>
|
||||
<p className="text-xs text-muted-foreground">#{u.id} · {u.theme}/{u.colorMode}</p>
|
||||
<p className="truncate font-semibold uppercase tracking-[0.1em]">{u.username}</p>
|
||||
<p className="text-[11px] uppercase tracking-[0.12em] text-muted-foreground">#{u.id} · {u.theme}/{u.colorMode}</p>
|
||||
</div>
|
||||
<Button size="sm" variant="destructive" onClick={() => onDeleteUser(u.id)}>{t('delete')}</Button>
|
||||
</div>
|
||||
|
||||
@@ -49,10 +49,10 @@ export default function TransferSection(props: Props) {
|
||||
} = props
|
||||
|
||||
return (
|
||||
<CardContent className="space-y-3 p-4">
|
||||
<CardContent className="space-y-3 p-4 md:p-5">
|
||||
<div>
|
||||
<p className="text-sm font-semibold">{username}</p>
|
||||
<p className="text-xs text-muted-foreground">{t('accountSubtitle')}</p>
|
||||
<p className="font-display text-xl font-semibold">{username}</p>
|
||||
<p className="text-[11px] uppercase tracking-[0.16em] text-muted-foreground">{t('accountSubtitle')}</p>
|
||||
</div>
|
||||
|
||||
<Button className="w-full justify-start" onClick={onUploadFile}>{t('upload')}</Button>
|
||||
@@ -73,24 +73,24 @@ export default function TransferSection(props: Props) {
|
||||
<Button onClick={onCreateFolder}>{t('newFolder')}</Button>
|
||||
</DialogContent>
|
||||
</Dialog>
|
||||
<div className="grid gap-1 rounded-lg bg-muted/60 p-2 text-xs">
|
||||
<button type="button" className={`rounded-md px-2 py-1 text-left ${view === 'all' ? 'bg-background font-semibold' : 'hover:bg-background/70'}`} onClick={() => setView('all')}>{t('allFiles')} ({filesCount})</button>
|
||||
<button type="button" className={`rounded-md px-2 py-1 text-left ${view === 'folders' ? 'bg-background font-semibold' : 'hover:bg-background/70'}`} onClick={() => setView('folders')}>{t('folders')}</button>
|
||||
<button type="button" className={`rounded-md px-2 py-1 text-left ${view === 'documents' ? 'bg-background font-semibold' : 'hover:bg-background/70'}`} onClick={() => setView('documents')}>{t('documents')}</button>
|
||||
<button type="button" className={`rounded-md px-2 py-1 text-left ${view === 'media' ? 'bg-background font-semibold' : 'hover:bg-background/70'}`} onClick={() => setView('media')}>{t('media')}</button>
|
||||
<button type="button" className={`rounded-md px-2 py-1 text-left ${view === 'archives' ? 'bg-background font-semibold' : 'hover:bg-background/70'}`} onClick={() => setView('archives')}>{t('archives')}</button>
|
||||
<button type="button" className={`rounded-md px-2 py-1 text-left ${view === 'tagged' ? 'bg-background font-semibold' : 'hover:bg-background/70'}`} onClick={() => setView('tagged')}>{t('tagged')}</button>
|
||||
<button type="button" className={`rounded-md px-2 py-1 text-left ${view === 'recent' ? 'bg-background font-semibold' : 'hover:bg-background/70'}`} onClick={() => setView('recent')}>{t('recent')}</button>
|
||||
<div className="grid gap-1 border-2 border-border bg-muted/50 p-2 text-xs uppercase tracking-[0.14em]">
|
||||
<button type="button" className={`border px-2 py-1 text-left ${view === 'all' ? 'border-border bg-background font-semibold' : 'border-transparent hover:border-border hover:bg-background/70'}`} onClick={() => setView('all')}>{t('allFiles')} ({filesCount})</button>
|
||||
<button type="button" className={`border px-2 py-1 text-left ${view === 'folders' ? 'border-border bg-background font-semibold' : 'border-transparent hover:border-border hover:bg-background/70'}`} onClick={() => setView('folders')}>{t('folders')}</button>
|
||||
<button type="button" className={`border px-2 py-1 text-left ${view === 'documents' ? 'border-border bg-background font-semibold' : 'border-transparent hover:border-border hover:bg-background/70'}`} onClick={() => setView('documents')}>{t('documents')}</button>
|
||||
<button type="button" className={`border px-2 py-1 text-left ${view === 'media' ? 'border-border bg-background font-semibold' : 'border-transparent hover:border-border hover:bg-background/70'}`} onClick={() => setView('media')}>{t('media')}</button>
|
||||
<button type="button" className={`border px-2 py-1 text-left ${view === 'archives' ? 'border-border bg-background font-semibold' : 'border-transparent hover:border-border hover:bg-background/70'}`} onClick={() => setView('archives')}>{t('archives')}</button>
|
||||
<button type="button" className={`border px-2 py-1 text-left ${view === 'tagged' ? 'border-border bg-background font-semibold' : 'border-transparent hover:border-border hover:bg-background/70'}`} onClick={() => setView('tagged')}>{t('tagged')}</button>
|
||||
<button type="button" className={`border px-2 py-1 text-left ${view === 'recent' ? 'border-border bg-background font-semibold' : 'border-transparent hover:border-border hover:bg-background/70'}`} onClick={() => setView('recent')}>{t('recent')}</button>
|
||||
</div>
|
||||
|
||||
<div className="space-y-2 rounded-lg bg-muted/60 p-2 text-xs">
|
||||
<p className="font-medium text-muted-foreground">{t('tags')}</p>
|
||||
<button type="button" className={`block w-full rounded-md px-2 py-1 text-left ${!activeTag ? 'bg-background font-semibold' : 'hover:bg-background/70'}`} onClick={() => setActiveTag('')}># all</button>
|
||||
<div className="space-y-2 border-2 border-border bg-muted/50 p-2 text-xs uppercase tracking-[0.12em]">
|
||||
<p className="font-semibold text-muted-foreground">{t('tags')}</p>
|
||||
<button type="button" className={`block w-full border px-2 py-1 text-left ${!activeTag ? 'border-border bg-background font-semibold' : 'border-transparent hover:border-border hover:bg-background/70'}`} onClick={() => setActiveTag('')}># all</button>
|
||||
{Object.entries(tagCounts)
|
||||
.sort((a, b) => b[1] - a[1])
|
||||
.slice(0, 8)
|
||||
.map(([tag, count]) => (
|
||||
<button type="button" key={tag} className={`block w-full rounded-md px-2 py-1 text-left ${activeTag === tag ? 'bg-background font-semibold' : 'hover:bg-background/70'}`} onClick={() => setActiveTag(tag)}>#{tag} ({count})</button>
|
||||
<button type="button" key={tag} className={`block w-full border px-2 py-1 text-left ${activeTag === tag ? 'border-border bg-background font-semibold' : 'border-transparent hover:border-border hover:bg-background/70'}`} onClick={() => setActiveTag(tag)}>#{tag} ({count})</button>
|
||||
))}
|
||||
</div>
|
||||
</CardContent>
|
||||
|
||||
Reference in New Issue
Block a user