Spaces:
Build error
Build error
Claude commited on
feat(frontend): Sprint R2 — retro Home / Desktop
Browse filesTransform the Home page into a retro desktop environment:
- RetroMenuBar at top with IIIF Studio branding + search + admin
- Dithered gray background (bg-retro-dither)
- Corpus list in a RetroWindow with selection highlighting (navy blue)
- Profile-based icon glyphs for each corpus type
- Manuscripts sub-window appears on corpus with multiple manuscripts
- Bottom dock with RetroIcon grid for quick corpus access
- Loading/error states displayed in centered RetroWindow dialogs
Restyle SearchBar: inset well input, navy-blue hover on results.
Restyle AdminNav: uses RetroButton component.
Remove RetroDemo page (no longer needed).
Build passes (tsc + vite).
https://claude.ai/code/session_01WWohTtw2CxGRawmpH1tyrY
- frontend/src/App.tsx +0 -6
- frontend/src/components/AdminNav.tsx +5 -6
- frontend/src/components/SearchBar.tsx +35 -15
- frontend/src/pages/Home.tsx +157 -70
- frontend/src/pages/RetroDemo.tsx +0 -198
frontend/src/App.tsx
CHANGED
|
@@ -3,14 +3,12 @@ import Admin from './pages/Admin.tsx'
|
|
| 3 |
import Editor from './pages/Editor.tsx'
|
| 4 |
import Home from './pages/Home.tsx'
|
| 5 |
import Reader from './pages/Reader.tsx'
|
| 6 |
-
import RetroDemo from './pages/RetroDemo.tsx'
|
| 7 |
|
| 8 |
type View =
|
| 9 |
| { name: 'home' }
|
| 10 |
| { name: 'reader'; manuscriptId: string; profileId: string }
|
| 11 |
| { name: 'admin' }
|
| 12 |
| { name: 'editor'; pageId: string }
|
| 13 |
-
| { name: 'retro-demo' }
|
| 14 |
|
| 15 |
export default function App() {
|
| 16 |
const [view, setView] = useState<View>({ name: 'home' })
|
|
@@ -39,10 +37,6 @@ export default function App() {
|
|
| 39 |
)
|
| 40 |
}
|
| 41 |
|
| 42 |
-
if (view.name === 'retro-demo') {
|
| 43 |
-
return <RetroDemo onBack={() => setView({ name: 'home' })} />
|
| 44 |
-
}
|
| 45 |
-
|
| 46 |
return (
|
| 47 |
<Home
|
| 48 |
onOpenManuscript={(manuscriptId, profileId) =>
|
|
|
|
| 3 |
import Editor from './pages/Editor.tsx'
|
| 4 |
import Home from './pages/Home.tsx'
|
| 5 |
import Reader from './pages/Reader.tsx'
|
|
|
|
| 6 |
|
| 7 |
type View =
|
| 8 |
| { name: 'home' }
|
| 9 |
| { name: 'reader'; manuscriptId: string; profileId: string }
|
| 10 |
| { name: 'admin' }
|
| 11 |
| { name: 'editor'; pageId: string }
|
|
|
|
| 12 |
|
| 13 |
export default function App() {
|
| 14 |
const [view, setView] = useState<View>({ name: 'home' })
|
|
|
|
| 37 |
)
|
| 38 |
}
|
| 39 |
|
|
|
|
|
|
|
|
|
|
|
|
|
| 40 |
return (
|
| 41 |
<Home
|
| 42 |
onOpenManuscript={(manuscriptId, profileId) =>
|
frontend/src/components/AdminNav.tsx
CHANGED
|
@@ -1,14 +1,13 @@
|
|
|
|
|
|
|
|
| 1 |
interface AdminNavProps {
|
| 2 |
onClick: () => void
|
| 3 |
}
|
| 4 |
|
| 5 |
export default function AdminNav({ onClick }: AdminNavProps) {
|
| 6 |
return (
|
| 7 |
-
<
|
| 8 |
-
|
| 9 |
-
|
| 10 |
-
>
|
| 11 |
-
Administration
|
| 12 |
-
</button>
|
| 13 |
)
|
| 14 |
}
|
|
|
|
| 1 |
+
import { RetroButton } from './retro'
|
| 2 |
+
|
| 3 |
interface AdminNavProps {
|
| 4 |
onClick: () => void
|
| 5 |
}
|
| 6 |
|
| 7 |
export default function AdminNav({ onClick }: AdminNavProps) {
|
| 8 |
return (
|
| 9 |
+
<RetroButton size="sm" onClick={onClick}>
|
| 10 |
+
Admin
|
| 11 |
+
</RetroButton>
|
|
|
|
|
|
|
|
|
|
| 12 |
)
|
| 13 |
}
|
frontend/src/components/SearchBar.tsx
CHANGED
|
@@ -41,7 +41,6 @@ export default function SearchBar({ onSelectResult }: Props) {
|
|
| 41 |
}
|
| 42 |
}, [query, runSearch])
|
| 43 |
|
| 44 |
-
// Close dropdown on outside click
|
| 45 |
useEffect(() => {
|
| 46 |
const handler = (e: MouseEvent) => {
|
| 47 |
if (containerRef.current && !containerRef.current.contains(e.target as Node)) {
|
|
@@ -53,27 +52,45 @@ export default function SearchBar({ onSelectResult }: Props) {
|
|
| 53 |
}, [])
|
| 54 |
|
| 55 |
return (
|
| 56 |
-
<div ref={containerRef} className="relative w-
|
| 57 |
<div className="relative">
|
| 58 |
<input
|
| 59 |
type="search"
|
| 60 |
value={query}
|
| 61 |
onChange={(e) => setQuery(e.target.value)}
|
| 62 |
onFocus={() => results.length > 0 && setOpen(true)}
|
| 63 |
-
placeholder="Rechercher
|
| 64 |
-
className="
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 65 |
/>
|
| 66 |
{loading && (
|
| 67 |
-
<span className="absolute right-2
|
| 68 |
-
|
| 69 |
</span>
|
| 70 |
)}
|
| 71 |
</div>
|
| 72 |
|
| 73 |
{open && (
|
| 74 |
-
<div
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 75 |
{results.length === 0 ? (
|
| 76 |
-
<div className="px-
|
|
|
|
|
|
|
| 77 |
) : (
|
| 78 |
<ul>
|
| 79 |
{results.map((r) => (
|
|
@@ -83,17 +100,20 @@ export default function SearchBar({ onSelectResult }: Props) {
|
|
| 83 |
setOpen(false)
|
| 84 |
onSelectResult?.(r)
|
| 85 |
}}
|
| 86 |
-
className="
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 87 |
>
|
| 88 |
<div className="flex items-center justify-between gap-2">
|
| 89 |
-
<span className="font-
|
| 90 |
-
|
| 91 |
-
|
| 92 |
-
<span className="text-xs text-stone-400 shrink-0">
|
| 93 |
-
score : {r.score}
|
| 94 |
</span>
|
| 95 |
</div>
|
| 96 |
-
<div className="text-xs
|
| 97 |
{r.excerpt}
|
| 98 |
</div>
|
| 99 |
</button>
|
|
|
|
| 41 |
}
|
| 42 |
}, [query, runSearch])
|
| 43 |
|
|
|
|
| 44 |
useEffect(() => {
|
| 45 |
const handler = (e: MouseEvent) => {
|
| 46 |
if (containerRef.current && !containerRef.current.contains(e.target as Node)) {
|
|
|
|
| 52 |
}, [])
|
| 53 |
|
| 54 |
return (
|
| 55 |
+
<div ref={containerRef} className="relative w-64">
|
| 56 |
<div className="relative">
|
| 57 |
<input
|
| 58 |
type="search"
|
| 59 |
value={query}
|
| 60 |
onChange={(e) => setQuery(e.target.value)}
|
| 61 |
onFocus={() => results.length > 0 && setOpen(true)}
|
| 62 |
+
placeholder="Rechercher..."
|
| 63 |
+
className="
|
| 64 |
+
w-full px-2 py-[2px]
|
| 65 |
+
text-retro-sm font-retro
|
| 66 |
+
bg-retro-white text-retro-black
|
| 67 |
+
border border-retro-black
|
| 68 |
+
shadow-retro-well
|
| 69 |
+
placeholder:text-retro-darkgray
|
| 70 |
+
"
|
| 71 |
/>
|
| 72 |
{loading && (
|
| 73 |
+
<span className="absolute right-2 top-1/2 -translate-y-1/2 text-retro-darkgray text-retro-xs">
|
| 74 |
+
...
|
| 75 |
</span>
|
| 76 |
)}
|
| 77 |
</div>
|
| 78 |
|
| 79 |
{open && (
|
| 80 |
+
<div
|
| 81 |
+
className="
|
| 82 |
+
absolute top-full left-0 right-0 mt-[2px]
|
| 83 |
+
bg-retro-white
|
| 84 |
+
border-retro border-retro-black
|
| 85 |
+
shadow-retro-lg
|
| 86 |
+
z-50 max-h-60
|
| 87 |
+
overflow-y-auto retro-scroll
|
| 88 |
+
"
|
| 89 |
+
>
|
| 90 |
{results.length === 0 ? (
|
| 91 |
+
<div className="px-2 py-2 text-retro-xs text-retro-darkgray">
|
| 92 |
+
Aucun resultat.
|
| 93 |
+
</div>
|
| 94 |
) : (
|
| 95 |
<ul>
|
| 96 |
{results.map((r) => (
|
|
|
|
| 100 |
setOpen(false)
|
| 101 |
onSelectResult?.(r)
|
| 102 |
}}
|
| 103 |
+
className="
|
| 104 |
+
w-full text-left px-2 py-[3px]
|
| 105 |
+
text-retro-sm font-retro
|
| 106 |
+
hover:bg-retro-select hover:text-retro-select-text
|
| 107 |
+
border-b border-retro-gray last:border-0
|
| 108 |
+
"
|
| 109 |
>
|
| 110 |
<div className="flex items-center justify-between gap-2">
|
| 111 |
+
<span className="font-bold">{r.folio_label}</span>
|
| 112 |
+
<span className="text-retro-xs opacity-60">
|
| 113 |
+
{r.score}
|
|
|
|
|
|
|
| 114 |
</span>
|
| 115 |
</div>
|
| 116 |
+
<div className="text-retro-xs truncate opacity-70">
|
| 117 |
{r.excerpt}
|
| 118 |
</div>
|
| 119 |
</button>
|
frontend/src/pages/Home.tsx
CHANGED
|
@@ -1,6 +1,7 @@
|
|
| 1 |
import { useEffect, useState } from 'react'
|
| 2 |
import AdminNav from '../components/AdminNav.tsx'
|
| 3 |
import SearchBar from '../components/SearchBar.tsx'
|
|
|
|
| 4 |
import {
|
| 5 |
fetchCorpora,
|
| 6 |
fetchManuscripts,
|
|
@@ -8,6 +9,14 @@ import {
|
|
| 8 |
type Manuscript,
|
| 9 |
} from '../lib/api.ts'
|
| 10 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 11 |
interface Props {
|
| 12 |
onOpenManuscript: (manuscriptId: string, profileId: string) => void
|
| 13 |
onOpenPage?: (pageId: string) => void
|
|
@@ -20,6 +29,7 @@ export default function Home({ onOpenManuscript, onOpenPage, onAdmin }: Props) {
|
|
| 20 |
const [error, setError] = useState<string | null>(null)
|
| 21 |
const [manuscripts, setManuscripts] = useState<Record<string, Manuscript[]>>({})
|
| 22 |
const [expanding, setExpanding] = useState<string | null>(null)
|
|
|
|
| 23 |
|
| 24 |
useEffect(() => {
|
| 25 |
fetchCorpora()
|
|
@@ -29,7 +39,8 @@ export default function Home({ onOpenManuscript, onOpenPage, onAdmin }: Props) {
|
|
| 29 |
}, [])
|
| 30 |
|
| 31 |
const handleCorpusClick = async (corpus: Corpus) => {
|
| 32 |
-
|
|
|
|
| 33 |
const cached = manuscripts[corpus.id]
|
| 34 |
if (cached) {
|
| 35 |
if (cached.length === 1) onOpenManuscript(cached[0].id, corpus.profile_id)
|
|
@@ -42,95 +53,171 @@ export default function Home({ onOpenManuscript, onOpenPage, onAdmin }: Props) {
|
|
| 42 |
setManuscripts((prev) => ({ ...prev, [corpus.id]: ms }))
|
| 43 |
if (ms.length === 1) onOpenManuscript(ms[0].id, corpus.profile_id)
|
| 44 |
} catch {
|
| 45 |
-
//
|
| 46 |
} finally {
|
| 47 |
setExpanding(null)
|
| 48 |
}
|
| 49 |
}
|
| 50 |
|
|
|
|
| 51 |
if (loading) {
|
| 52 |
return (
|
| 53 |
-
<div className="flex items-center justify-center
|
| 54 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
| 55 |
</div>
|
| 56 |
)
|
| 57 |
}
|
| 58 |
|
|
|
|
| 59 |
if (error) {
|
| 60 |
return (
|
| 61 |
-
<div className="
|
| 62 |
-
Erreur
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 63 |
</div>
|
| 64 |
)
|
| 65 |
}
|
| 66 |
|
|
|
|
|
|
|
| 67 |
return (
|
| 68 |
-
<div className="min-h-screen bg-
|
| 69 |
-
|
| 70 |
-
|
| 71 |
-
|
| 72 |
-
|
| 73 |
-
|
| 74 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 75 |
</div>
|
| 76 |
-
|
| 77 |
-
|
| 78 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 79 |
</div>
|
| 80 |
-
</
|
| 81 |
-
|
| 82 |
-
<main className="max-w-3xl mx-auto py-10 px-8">
|
| 83 |
-
<h2 className="text-sm font-semibold text-stone-500 uppercase tracking-wide mb-6">
|
| 84 |
-
Corpus disponibles
|
| 85 |
-
</h2>
|
| 86 |
-
|
| 87 |
-
{corpora.length === 0 ? (
|
| 88 |
-
<p className="text-stone-400 text-sm">
|
| 89 |
-
Aucun corpus enregistré. Créez-en un via{' '}
|
| 90 |
-
<code className="bg-stone-200 px-1 rounded text-xs">POST /api/v1/corpora</code>.
|
| 91 |
-
</p>
|
| 92 |
-
) : (
|
| 93 |
-
<ul className="space-y-3">
|
| 94 |
-
{corpora.map((corpus) => (
|
| 95 |
-
<li key={corpus.id}>
|
| 96 |
-
<button
|
| 97 |
-
onClick={() => void handleCorpusClick(corpus)}
|
| 98 |
-
className="w-full text-left bg-white border border-stone-200 rounded-lg px-6 py-4 hover:border-stone-400 hover:shadow-sm transition-all"
|
| 99 |
-
>
|
| 100 |
-
<div className="font-medium text-stone-900">{corpus.title}</div>
|
| 101 |
-
<div className="text-xs text-stone-400 mt-1">
|
| 102 |
-
Profil : {corpus.profile_id} · Slug : {corpus.slug}
|
| 103 |
-
</div>
|
| 104 |
-
</button>
|
| 105 |
-
|
| 106 |
-
{expanding === corpus.id && (
|
| 107 |
-
<div className="mt-2 ml-4 text-xs text-stone-400">Chargement…</div>
|
| 108 |
-
)}
|
| 109 |
-
|
| 110 |
-
{manuscripts[corpus.id] && manuscripts[corpus.id].length > 1 && (
|
| 111 |
-
<ul className="mt-2 ml-4 space-y-1">
|
| 112 |
-
{manuscripts[corpus.id].map((ms) => (
|
| 113 |
-
<li key={ms.id}>
|
| 114 |
-
<button
|
| 115 |
-
onClick={() => onOpenManuscript(ms.id, corpus.profile_id)}
|
| 116 |
-
className="text-sm text-stone-600 hover:text-stone-900 hover:underline text-left"
|
| 117 |
-
>
|
| 118 |
-
{ms.title}
|
| 119 |
-
{ms.total_pages > 0 && (
|
| 120 |
-
<span className="text-stone-400 ml-1">
|
| 121 |
-
({ms.total_pages} pages)
|
| 122 |
-
</span>
|
| 123 |
-
)}
|
| 124 |
-
</button>
|
| 125 |
-
</li>
|
| 126 |
-
))}
|
| 127 |
-
</ul>
|
| 128 |
-
)}
|
| 129 |
-
</li>
|
| 130 |
-
))}
|
| 131 |
-
</ul>
|
| 132 |
-
)}
|
| 133 |
-
</main>
|
| 134 |
</div>
|
| 135 |
)
|
| 136 |
}
|
|
|
|
| 1 |
import { useEffect, useState } from 'react'
|
| 2 |
import AdminNav from '../components/AdminNav.tsx'
|
| 3 |
import SearchBar from '../components/SearchBar.tsx'
|
| 4 |
+
import { RetroMenuBar, RetroWindow, RetroIcon } from '../components/retro'
|
| 5 |
import {
|
| 6 |
fetchCorpora,
|
| 7 |
fetchManuscripts,
|
|
|
|
| 9 |
type Manuscript,
|
| 10 |
} from '../lib/api.ts'
|
| 11 |
|
| 12 |
+
/** Map profile IDs to desktop icon glyphs */
|
| 13 |
+
const PROFILE_GLYPHS: Record<string, string> = {
|
| 14 |
+
'medieval-illuminated': '\u{1F4DC}',
|
| 15 |
+
'medieval-textual': '\u{1F4D6}',
|
| 16 |
+
'early-modern-print': '\u{1F5A8}',
|
| 17 |
+
'modern-handwritten': '\u{270D}',
|
| 18 |
+
}
|
| 19 |
+
|
| 20 |
interface Props {
|
| 21 |
onOpenManuscript: (manuscriptId: string, profileId: string) => void
|
| 22 |
onOpenPage?: (pageId: string) => void
|
|
|
|
| 29 |
const [error, setError] = useState<string | null>(null)
|
| 30 |
const [manuscripts, setManuscripts] = useState<Record<string, Manuscript[]>>({})
|
| 31 |
const [expanding, setExpanding] = useState<string | null>(null)
|
| 32 |
+
const [selectedCorpus, setSelectedCorpus] = useState<Corpus | null>(null)
|
| 33 |
|
| 34 |
useEffect(() => {
|
| 35 |
fetchCorpora()
|
|
|
|
| 39 |
}, [])
|
| 40 |
|
| 41 |
const handleCorpusClick = async (corpus: Corpus) => {
|
| 42 |
+
setSelectedCorpus(corpus)
|
| 43 |
+
|
| 44 |
const cached = manuscripts[corpus.id]
|
| 45 |
if (cached) {
|
| 46 |
if (cached.length === 1) onOpenManuscript(cached[0].id, corpus.profile_id)
|
|
|
|
| 53 |
setManuscripts((prev) => ({ ...prev, [corpus.id]: ms }))
|
| 54 |
if (ms.length === 1) onOpenManuscript(ms[0].id, corpus.profile_id)
|
| 55 |
} catch {
|
| 56 |
+
// silent
|
| 57 |
} finally {
|
| 58 |
setExpanding(null)
|
| 59 |
}
|
| 60 |
}
|
| 61 |
|
| 62 |
+
// ── Loading state ───────────────────────────────────────────────────
|
| 63 |
if (loading) {
|
| 64 |
return (
|
| 65 |
+
<div className="min-h-screen bg-retro-dither flex items-center justify-center">
|
| 66 |
+
<RetroWindow title="IIIF Studio" className="w-72">
|
| 67 |
+
<div className="p-4 text-retro-sm text-center">
|
| 68 |
+
Chargement...
|
| 69 |
+
</div>
|
| 70 |
+
</RetroWindow>
|
| 71 |
</div>
|
| 72 |
)
|
| 73 |
}
|
| 74 |
|
| 75 |
+
// ── Error state ─────────────────────────────────────────────────────
|
| 76 |
if (error) {
|
| 77 |
return (
|
| 78 |
+
<div className="min-h-screen bg-retro-dither flex items-center justify-center">
|
| 79 |
+
<RetroWindow title="Erreur" className="w-80">
|
| 80 |
+
<div className="p-4 text-retro-sm">
|
| 81 |
+
<p className="font-bold mb-2">Erreur de connexion</p>
|
| 82 |
+
<p className="text-retro-xs">{error}</p>
|
| 83 |
+
</div>
|
| 84 |
+
</RetroWindow>
|
| 85 |
</div>
|
| 86 |
)
|
| 87 |
}
|
| 88 |
|
| 89 |
+
const selectedMs = selectedCorpus ? manuscripts[selectedCorpus.id] : undefined
|
| 90 |
+
|
| 91 |
return (
|
| 92 |
+
<div className="min-h-screen bg-retro-dither flex flex-col">
|
| 93 |
+
{/* ── Menu bar (top of screen) ─────────────────────────────── */}
|
| 94 |
+
<RetroMenuBar
|
| 95 |
+
items={[
|
| 96 |
+
{ label: 'IIIF Studio' },
|
| 97 |
+
{ label: 'Fichier' },
|
| 98 |
+
{ label: 'Corpus' },
|
| 99 |
+
{ label: 'Aide' },
|
| 100 |
+
]}
|
| 101 |
+
right={
|
| 102 |
+
<div className="flex items-center gap-1">
|
| 103 |
+
<SearchBar onSelectResult={onOpenPage ? (r) => onOpenPage(r.page_id) : undefined} />
|
| 104 |
+
<AdminNav onClick={onAdmin} />
|
| 105 |
+
</div>
|
| 106 |
+
}
|
| 107 |
+
/>
|
| 108 |
+
|
| 109 |
+
{/* ── Desktop area ─────────────────────────────────────────── */}
|
| 110 |
+
<div className="flex-1 flex flex-col">
|
| 111 |
+
|
| 112 |
+
{/* ── Corpus list window (center) ──────────────────────── */}
|
| 113 |
+
<div className="flex-1 flex items-start justify-center p-6 gap-4">
|
| 114 |
+
<RetroWindow
|
| 115 |
+
title="Corpus disponibles"
|
| 116 |
+
statusBar={`${corpora.length} corpus enregistre${corpora.length > 1 ? 's' : ''}`}
|
| 117 |
+
className="w-full max-w-2xl"
|
| 118 |
+
scrollable
|
| 119 |
+
>
|
| 120 |
+
{corpora.length === 0 ? (
|
| 121 |
+
<div className="p-4 text-retro-sm text-retro-darkgray">
|
| 122 |
+
Aucun corpus enregistre. Creez-en un via Administration.
|
| 123 |
+
</div>
|
| 124 |
+
) : (
|
| 125 |
+
<div className="divide-y divide-retro-gray">
|
| 126 |
+
{corpora.map((corpus) => (
|
| 127 |
+
<div key={corpus.id}>
|
| 128 |
+
<button
|
| 129 |
+
onClick={() => void handleCorpusClick(corpus)}
|
| 130 |
+
className={`
|
| 131 |
+
w-full text-left px-3 py-[6px] flex items-center gap-3
|
| 132 |
+
text-retro-sm font-retro
|
| 133 |
+
${selectedCorpus?.id === corpus.id
|
| 134 |
+
? 'bg-retro-select text-retro-select-text'
|
| 135 |
+
: 'hover:bg-retro-select hover:text-retro-select-text'
|
| 136 |
+
}
|
| 137 |
+
`}
|
| 138 |
+
>
|
| 139 |
+
<span className="text-[18px] leading-none shrink-0">
|
| 140 |
+
{PROFILE_GLYPHS[corpus.profile_id] || '\u{1F4C1}'}
|
| 141 |
+
</span>
|
| 142 |
+
<div className="flex-1 min-w-0">
|
| 143 |
+
<div className="font-bold truncate">{corpus.title}</div>
|
| 144 |
+
<div className={`text-retro-xs truncate ${
|
| 145 |
+
selectedCorpus?.id === corpus.id ? 'opacity-70' : 'text-retro-darkgray'
|
| 146 |
+
}`}>
|
| 147 |
+
{corpus.profile_id} — {corpus.slug}
|
| 148 |
+
</div>
|
| 149 |
+
</div>
|
| 150 |
+
</button>
|
| 151 |
+
|
| 152 |
+
{expanding === corpus.id && (
|
| 153 |
+
<div className="px-3 py-1 text-retro-xs text-retro-darkgray bg-retro-light">
|
| 154 |
+
Chargement...
|
| 155 |
+
</div>
|
| 156 |
+
)}
|
| 157 |
+
</div>
|
| 158 |
+
))}
|
| 159 |
+
</div>
|
| 160 |
+
)}
|
| 161 |
+
</RetroWindow>
|
| 162 |
+
|
| 163 |
+
{/* ── Manuscripts sub-window (appears when a corpus has multiple) ── */}
|
| 164 |
+
{selectedMs && selectedMs.length > 1 && selectedCorpus && (
|
| 165 |
+
<RetroWindow
|
| 166 |
+
title={`Manuscrits — ${selectedCorpus.title}`}
|
| 167 |
+
onClose={() => setSelectedCorpus(null)}
|
| 168 |
+
statusBar={`${selectedMs.length} manuscrit${selectedMs.length > 1 ? 's' : ''}`}
|
| 169 |
+
className="w-80"
|
| 170 |
+
scrollable
|
| 171 |
+
>
|
| 172 |
+
<div className="divide-y divide-retro-gray">
|
| 173 |
+
{selectedMs.map((ms) => (
|
| 174 |
+
<button
|
| 175 |
+
key={ms.id}
|
| 176 |
+
onClick={() => onOpenManuscript(ms.id, selectedCorpus.profile_id)}
|
| 177 |
+
className="
|
| 178 |
+
w-full text-left px-3 py-[6px]
|
| 179 |
+
text-retro-sm font-retro
|
| 180 |
+
hover:bg-retro-select hover:text-retro-select-text
|
| 181 |
+
"
|
| 182 |
+
>
|
| 183 |
+
<div className="font-bold truncate">{ms.title}</div>
|
| 184 |
+
{ms.total_pages > 0 && (
|
| 185 |
+
<div className="text-retro-xs text-retro-darkgray">
|
| 186 |
+
{ms.total_pages} pages
|
| 187 |
+
</div>
|
| 188 |
+
)}
|
| 189 |
+
</button>
|
| 190 |
+
))}
|
| 191 |
+
</div>
|
| 192 |
+
</RetroWindow>
|
| 193 |
+
)}
|
| 194 |
</div>
|
| 195 |
+
|
| 196 |
+
{/* ── Desktop icons (bottom dock) ────────────────────────── */}
|
| 197 |
+
<div
|
| 198 |
+
className="
|
| 199 |
+
shrink-0 flex items-end justify-center gap-1 px-4 py-3
|
| 200 |
+
border-t-retro border-retro-black
|
| 201 |
+
bg-retro-gray
|
| 202 |
+
shadow-retro-outset
|
| 203 |
+
"
|
| 204 |
+
>
|
| 205 |
+
{corpora.map((corpus) => (
|
| 206 |
+
<RetroIcon
|
| 207 |
+
key={corpus.id}
|
| 208 |
+
glyph={PROFILE_GLYPHS[corpus.profile_id] || '\u{1F4C1}'}
|
| 209 |
+
label={corpus.slug}
|
| 210 |
+
selected={selectedCorpus?.id === corpus.id}
|
| 211 |
+
onClick={() => void handleCorpusClick(corpus)}
|
| 212 |
+
/>
|
| 213 |
+
))}
|
| 214 |
+
{corpora.length === 0 && (
|
| 215 |
+
<div className="text-retro-xs text-retro-darkgray py-2">
|
| 216 |
+
Aucun corpus
|
| 217 |
+
</div>
|
| 218 |
+
)}
|
| 219 |
</div>
|
| 220 |
+
</div>
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 221 |
</div>
|
| 222 |
)
|
| 223 |
}
|
frontend/src/pages/RetroDemo.tsx
DELETED
|
@@ -1,198 +0,0 @@
|
|
| 1 |
-
import { useState } from 'react'
|
| 2 |
-
import {
|
| 3 |
-
RetroWindow,
|
| 4 |
-
RetroButton,
|
| 5 |
-
RetroMenuBar,
|
| 6 |
-
RetroIcon,
|
| 7 |
-
RetroCheckbox,
|
| 8 |
-
RetroInput,
|
| 9 |
-
RetroTextarea,
|
| 10 |
-
RetroSelect,
|
| 11 |
-
RetroBadge,
|
| 12 |
-
} from '../components/retro'
|
| 13 |
-
|
| 14 |
-
export default function RetroDemo({ onBack }: { onBack: () => void }) {
|
| 15 |
-
const [checked1, setChecked1] = useState(true)
|
| 16 |
-
const [checked2, setChecked2] = useState(false)
|
| 17 |
-
const [selectedIcon, setSelectedIcon] = useState<string | null>(null)
|
| 18 |
-
const [inputVal, setInputVal] = useState('')
|
| 19 |
-
const [textareaVal, setTextareaVal] = useState('Explicit liber primus incipit secundus...')
|
| 20 |
-
const [selectVal, setSelectVal] = useState('medieval-illuminated')
|
| 21 |
-
|
| 22 |
-
return (
|
| 23 |
-
<div className="min-h-screen bg-retro-dither">
|
| 24 |
-
{/* ── Menu Bar ─────────────────────────────────────────── */}
|
| 25 |
-
<RetroMenuBar
|
| 26 |
-
items={[
|
| 27 |
-
{ label: 'IIIF Studio', onClick: onBack },
|
| 28 |
-
{ label: 'Fichier' },
|
| 29 |
-
{ label: 'Edition' },
|
| 30 |
-
{ label: 'Aide' },
|
| 31 |
-
{ label: 'Disabled', disabled: true },
|
| 32 |
-
]}
|
| 33 |
-
right={
|
| 34 |
-
<span className="text-retro-xs text-retro-darkgray px-2">
|
| 35 |
-
Design System Demo
|
| 36 |
-
</span>
|
| 37 |
-
}
|
| 38 |
-
/>
|
| 39 |
-
|
| 40 |
-
<div className="p-4 flex flex-col gap-4 max-w-4xl mx-auto">
|
| 41 |
-
{/* ── Section: Buttons ──────────────────────────────── */}
|
| 42 |
-
<RetroWindow title="RetroButton" statusBar="Boutons avec bevel 3D outset/inset">
|
| 43 |
-
<div className="p-3 flex flex-wrap gap-2 items-center">
|
| 44 |
-
<RetroButton>Normal</RetroButton>
|
| 45 |
-
<RetroButton size="sm">Small</RetroButton>
|
| 46 |
-
<RetroButton pressed>Pressed</RetroButton>
|
| 47 |
-
<RetroButton disabled>Disabled</RetroButton>
|
| 48 |
-
<RetroButton onClick={onBack}>Retour Accueil</RetroButton>
|
| 49 |
-
</div>
|
| 50 |
-
</RetroWindow>
|
| 51 |
-
|
| 52 |
-
{/* ── Section: Window variants ─────────────────────── */}
|
| 53 |
-
<div className="flex gap-4">
|
| 54 |
-
<RetroWindow
|
| 55 |
-
title="Fenetre active"
|
| 56 |
-
onClose={() => {}}
|
| 57 |
-
statusBar="Active window"
|
| 58 |
-
className="flex-1"
|
| 59 |
-
active
|
| 60 |
-
>
|
| 61 |
-
<div className="p-3 text-retro-sm">
|
| 62 |
-
Contenu de la fenetre avec barre de titre noire.
|
| 63 |
-
Le bouton [x] ferme la fenetre.
|
| 64 |
-
</div>
|
| 65 |
-
</RetroWindow>
|
| 66 |
-
|
| 67 |
-
<RetroWindow
|
| 68 |
-
title="Fenetre inactive"
|
| 69 |
-
onClose={() => {}}
|
| 70 |
-
statusBar="Inactive window"
|
| 71 |
-
className="flex-1"
|
| 72 |
-
active={false}
|
| 73 |
-
>
|
| 74 |
-
<div className="p-3 text-retro-sm">
|
| 75 |
-
Une fenetre non focusee a une barre de titre grise.
|
| 76 |
-
</div>
|
| 77 |
-
</RetroWindow>
|
| 78 |
-
</div>
|
| 79 |
-
|
| 80 |
-
{/* ── Section: Scrollable window ───────────────────── */}
|
| 81 |
-
<RetroWindow title="Scrollable Window" scrollable className="h-[150px]">
|
| 82 |
-
<div className="p-3 text-retro-sm">
|
| 83 |
-
{Array.from({ length: 20 }, (_, i) => (
|
| 84 |
-
<p key={i}>Ligne {i + 1} — Lorem ipsum dolor sit amet, consectetur adipiscing elit.</p>
|
| 85 |
-
))}
|
| 86 |
-
</div>
|
| 87 |
-
</RetroWindow>
|
| 88 |
-
|
| 89 |
-
{/* ── Section: Icons ───────────────────────────────── */}
|
| 90 |
-
<RetroWindow title="RetroIcon — Desktop Icons" statusBar="Cliquez pour selectionner">
|
| 91 |
-
<div className="p-4 flex flex-wrap gap-2 bg-retro-dither-light">
|
| 92 |
-
{[
|
| 93 |
-
{ glyph: '\u{1F4DC}', label: 'Beatus' },
|
| 94 |
-
{ glyph: '\u{1F4D6}', label: 'Cartulaire' },
|
| 95 |
-
{ glyph: '\u{1F4C4}', label: 'Charte' },
|
| 96 |
-
{ glyph: '\u{1F5BC}', label: 'Enluminures' },
|
| 97 |
-
{ glyph: '\u{1F4BE}', label: 'Export' },
|
| 98 |
-
{ glyph: '\u{2699}', label: 'Config' },
|
| 99 |
-
].map((icon) => (
|
| 100 |
-
<RetroIcon
|
| 101 |
-
key={icon.label}
|
| 102 |
-
glyph={icon.glyph}
|
| 103 |
-
label={icon.label}
|
| 104 |
-
selected={selectedIcon === icon.label}
|
| 105 |
-
onClick={() => setSelectedIcon(icon.label)}
|
| 106 |
-
/>
|
| 107 |
-
))}
|
| 108 |
-
</div>
|
| 109 |
-
</RetroWindow>
|
| 110 |
-
|
| 111 |
-
{/* ── Section: Form controls ───────────────────────── */}
|
| 112 |
-
<RetroWindow title="Form Controls" statusBar="Inputs, textareas, selects, checkboxes">
|
| 113 |
-
<div className="p-3 flex flex-col gap-3">
|
| 114 |
-
<div className="flex gap-4">
|
| 115 |
-
<div className="flex-1">
|
| 116 |
-
<RetroInput
|
| 117 |
-
label="Identifiant du corpus"
|
| 118 |
-
placeholder="ex: beatus-lat8878"
|
| 119 |
-
value={inputVal}
|
| 120 |
-
onChange={(e) => setInputVal(e.target.value)}
|
| 121 |
-
/>
|
| 122 |
-
</div>
|
| 123 |
-
<div className="flex-1">
|
| 124 |
-
<RetroSelect
|
| 125 |
-
label="Profil de corpus"
|
| 126 |
-
value={selectVal}
|
| 127 |
-
onChange={(e) => setSelectVal(e.target.value)}
|
| 128 |
-
options={[
|
| 129 |
-
{ value: 'medieval-illuminated', label: 'Medieval Illuminated' },
|
| 130 |
-
{ value: 'medieval-textual', label: 'Medieval Textual' },
|
| 131 |
-
{ value: 'early-modern-print', label: 'Early Modern Print' },
|
| 132 |
-
{ value: 'modern-handwritten', label: 'Modern Handwritten' },
|
| 133 |
-
]}
|
| 134 |
-
/>
|
| 135 |
-
</div>
|
| 136 |
-
</div>
|
| 137 |
-
|
| 138 |
-
<RetroTextarea
|
| 139 |
-
label="Transcription diplomatique"
|
| 140 |
-
value={textareaVal}
|
| 141 |
-
onChange={(e) => setTextareaVal(e.target.value)}
|
| 142 |
-
rows={4}
|
| 143 |
-
/>
|
| 144 |
-
|
| 145 |
-
<div className="flex gap-4">
|
| 146 |
-
<RetroCheckbox
|
| 147 |
-
label="OCR diplomatique"
|
| 148 |
-
checked={checked1}
|
| 149 |
-
onChange={setChecked1}
|
| 150 |
-
/>
|
| 151 |
-
<RetroCheckbox
|
| 152 |
-
label="Iconographie"
|
| 153 |
-
checked={checked2}
|
| 154 |
-
onChange={setChecked2}
|
| 155 |
-
/>
|
| 156 |
-
<RetroCheckbox
|
| 157 |
-
label="Disabled"
|
| 158 |
-
checked={false}
|
| 159 |
-
onChange={() => {}}
|
| 160 |
-
disabled
|
| 161 |
-
/>
|
| 162 |
-
</div>
|
| 163 |
-
</div>
|
| 164 |
-
</RetroWindow>
|
| 165 |
-
|
| 166 |
-
{/* ── Section: Badges ──────────────────────────────── */}
|
| 167 |
-
<RetroWindow title="RetroBadge — Status Indicators">
|
| 168 |
-
<div className="p-3 flex flex-wrap gap-2 items-center">
|
| 169 |
-
<RetroBadge>default</RetroBadge>
|
| 170 |
-
<RetroBadge variant="success">validated</RetroBadge>
|
| 171 |
-
<RetroBadge variant="warning">needs_review</RetroBadge>
|
| 172 |
-
<RetroBadge variant="error">failed</RetroBadge>
|
| 173 |
-
<RetroBadge variant="info">machine_draft</RetroBadge>
|
| 174 |
-
</div>
|
| 175 |
-
</RetroWindow>
|
| 176 |
-
|
| 177 |
-
{/* ── Section: Typography ──────────────────────────── */}
|
| 178 |
-
<RetroWindow title="Typography & Utilities">
|
| 179 |
-
<div className="p-3 flex flex-col gap-2">
|
| 180 |
-
<p className="text-retro-2xl font-bold">Heading 2XL — IIIF Studio</p>
|
| 181 |
-
<p className="text-retro-xl font-bold">Heading XL — Beatus de Saint-Sever</p>
|
| 182 |
-
<p className="text-retro-lg font-semibold">Heading LG — Folio 13r</p>
|
| 183 |
-
<p className="text-retro-base">Body base — Explicit liber primus incipit secundus</p>
|
| 184 |
-
<p className="text-retro-sm">Body SM — Metadata and labels</p>
|
| 185 |
-
<p className="text-retro-xs text-retro-darkgray">Caption XS — Timestamps and fine print</p>
|
| 186 |
-
<hr className="border-retro-darkgray" />
|
| 187 |
-
<div className="flex gap-4">
|
| 188 |
-
<div className="p-2 bg-retro-dither text-retro-xs">bg-retro-dither</div>
|
| 189 |
-
<div className="p-2 bg-retro-dither-dark text-retro-xs text-retro-white">bg-retro-dither-dark</div>
|
| 190 |
-
<div className="p-2 bg-retro-dither-light text-retro-xs">bg-retro-dither-light</div>
|
| 191 |
-
<div className="p-2 bg-retro-lines text-retro-xs">bg-retro-lines</div>
|
| 192 |
-
</div>
|
| 193 |
-
</div>
|
| 194 |
-
</RetroWindow>
|
| 195 |
-
</div>
|
| 196 |
-
</div>
|
| 197 |
-
)
|
| 198 |
-
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|