Claude commited on
Commit
80f9a97
·
unverified ·
1 Parent(s): 5607582

feat(frontend): Sprint R3 — retro Reader / Manuscript viewer

Browse files

Redesign the Reader page with retro-computing aesthetic:

- RetroMenuBar with profile label, page nav (Prev/Next), Edit button
- Two RetroWindows side by side: Viewer (70%) + Analysis (30%)
- Viewer window: black background, RetroButton toolbar (zoom/reset/fullscreen),
retro region info popup, "Non analysee" RetroBadge
- Analysis window: scrollable with retro scrollbars, contains:
- LayerPanel with RetroCheckboxes for layer toggles
- TranscriptionPanel with RetroBadge status indicators
- TranslationPanel with RetroBadge status indicators
- CommentaryPanel with RetroButton tabs (Public/Savant)
- All panels use monochrome retro palette, pixel font, no rounded corners
- Dithered gray background between windows

Build passes (tsc + vite).

https://claude.ai/code/session_01WWohTtw2CxGRawmpH1tyrY

frontend/src/components/CommentaryPanel.tsx CHANGED
@@ -1,20 +1,21 @@
1
  import { useState, type FC } from 'react'
2
  import type { Commentary, EditorialInfo, EditorialStatus } from '../lib/api.ts'
 
3
 
4
  const STATUS_LABELS: Record<EditorialStatus, string> = {
5
  machine_draft: 'Brouillon IA',
6
- needs_review: 'À réviser',
7
- reviewed: 'Révisé',
8
- validated: 'Validé',
9
- published: 'Publié',
10
  }
11
 
12
- const STATUS_COLORS: Record<EditorialStatus, string> = {
13
- machine_draft: 'bg-amber-100 text-amber-700',
14
- needs_review: 'bg-orange-100 text-orange-700',
15
- reviewed: 'bg-blue-100 text-blue-700',
16
- validated: 'bg-green-100 text-green-700',
17
- published: 'bg-emerald-100 text-emerald-700',
18
  }
19
 
20
  interface Props {
@@ -29,7 +30,6 @@ const CommentaryPanel: FC<Props> = ({ commentary, editorial, visiblePublic, visi
29
 
30
  if (!visiblePublic && !visibleScholarly) return null
31
 
32
- // Si une seule couche est visible, forcer l'onglet correspondant
33
  const activeTab: 'public' | 'scholarly' =
34
  !visiblePublic && visibleScholarly ? 'scholarly' :
35
  !visibleScholarly && visiblePublic ? 'public' :
@@ -39,40 +39,39 @@ const CommentaryPanel: FC<Props> = ({ commentary, editorial, visiblePublic, visi
39
  const bothVisible = visiblePublic && visibleScholarly
40
 
41
  return (
42
- <div className="px-4 py-4">
43
- <div className="flex items-center justify-between mb-3">
44
- <h3 className="text-xs font-semibold text-stone-500 uppercase tracking-wide">
45
- Commentaire
46
- </h3>
47
- <span
48
- className={`text-xs px-2 py-0.5 rounded-full font-medium ${STATUS_COLORS[editorial.status]}`}
49
- >
50
  {STATUS_LABELS[editorial.status]}
51
- </span>
52
  </div>
53
 
54
  {bothVisible && (
55
- <div className="flex gap-2 mb-3">
56
- {(['public', 'scholarly'] as const).map((t) => (
57
- <button
58
- key={t}
59
- onClick={() => setTab(t)}
60
- className={`text-xs px-3 py-1 rounded transition-colors ${
61
- activeTab === t
62
- ? 'bg-stone-800 text-white'
63
- : 'bg-stone-100 text-stone-600 hover:bg-stone-200'
64
- }`}
65
- >
66
- {t === 'public' ? 'Public' : 'Savant'}
67
- </button>
68
- ))}
 
69
  </div>
70
  )}
71
 
72
  {content ? (
73
- <p className="text-sm text-stone-800 leading-relaxed whitespace-pre-wrap">{content}</p>
 
 
74
  ) : (
75
- <p className="text-sm text-stone-400 italic">Commentaire non disponible.</p>
76
  )}
77
  </div>
78
  )
 
1
  import { useState, type FC } from 'react'
2
  import type { Commentary, EditorialInfo, EditorialStatus } from '../lib/api.ts'
3
+ import { RetroBadge, RetroButton } from './retro'
4
 
5
  const STATUS_LABELS: Record<EditorialStatus, string> = {
6
  machine_draft: 'Brouillon IA',
7
+ needs_review: 'A reviser',
8
+ reviewed: 'Revise',
9
+ validated: 'Valide',
10
+ published: 'Publie',
11
  }
12
 
13
+ const STATUS_VARIANTS: Record<EditorialStatus, 'default' | 'success' | 'warning' | 'error' | 'info'> = {
14
+ machine_draft: 'info',
15
+ needs_review: 'warning',
16
+ reviewed: 'default',
17
+ validated: 'success',
18
+ published: 'success',
19
  }
20
 
21
  interface Props {
 
30
 
31
  if (!visiblePublic && !visibleScholarly) return null
32
 
 
33
  const activeTab: 'public' | 'scholarly' =
34
  !visiblePublic && visibleScholarly ? 'scholarly' :
35
  !visibleScholarly && visiblePublic ? 'public' :
 
39
  const bothVisible = visiblePublic && visibleScholarly
40
 
41
  return (
42
+ <div className="p-2">
43
+ <div className="flex items-center justify-between mb-2">
44
+ <span className="text-retro-xs font-bold">Commentaire</span>
45
+ <RetroBadge variant={STATUS_VARIANTS[editorial.status]}>
 
 
 
 
46
  {STATUS_LABELS[editorial.status]}
47
+ </RetroBadge>
48
  </div>
49
 
50
  {bothVisible && (
51
+ <div className="flex gap-[2px] mb-2">
52
+ <RetroButton
53
+ size="sm"
54
+ pressed={activeTab === 'public'}
55
+ onClick={() => setTab('public')}
56
+ >
57
+ Public
58
+ </RetroButton>
59
+ <RetroButton
60
+ size="sm"
61
+ pressed={activeTab === 'scholarly'}
62
+ onClick={() => setTab('scholarly')}
63
+ >
64
+ Savant
65
+ </RetroButton>
66
  </div>
67
  )}
68
 
69
  {content ? (
70
+ <p className="text-retro-sm whitespace-pre-wrap font-retro leading-relaxed">
71
+ {content}
72
+ </p>
73
  ) : (
74
+ <p className="text-retro-sm text-retro-darkgray">Commentaire non disponible.</p>
75
  )}
76
  </div>
77
  )
frontend/src/components/LayerPanel.tsx CHANGED
@@ -1,16 +1,17 @@
1
  import type { FC } from 'react'
 
2
 
3
  const LAYER_LABELS: Record<string, string> = {
4
  image: 'Image',
5
- ocr_diplomatic: 'Transcription diplomatique',
6
- ocr_normalized: 'Transcription normalisée',
7
- translation_fr: 'Traduction (FR)',
8
- translation_en: 'Traduction (EN)',
9
- summary: 'Résumé',
10
- scholarly_commentary: 'Commentaire savant',
11
- public_commentary: 'Commentaire public',
12
  iconography_detection: 'Iconographie',
13
- material_notes: 'Notes matérielles',
14
  uncertainty: 'Incertitudes',
15
  }
16
 
@@ -21,24 +22,16 @@ interface Props {
21
  }
22
 
23
  const LayerPanel: FC<Props> = ({ activeLayers, visibleLayers, onToggle }) => (
24
- <div className="border-b border-stone-200 bg-stone-50 px-4 py-3 shrink-0">
25
- <h3 className="text-xs font-semibold text-stone-500 uppercase tracking-wide mb-2">
26
- Couches
27
- </h3>
28
- <div className="flex flex-wrap gap-x-4 gap-y-1.5">
29
  {activeLayers.map((layer) => (
30
- <label
31
  key={layer}
32
- className="flex items-center gap-1.5 text-xs text-stone-700 cursor-pointer select-none"
33
- >
34
- <input
35
- type="checkbox"
36
- checked={visibleLayers.has(layer)}
37
- onChange={() => onToggle(layer)}
38
- className="rounded border-stone-300 text-stone-700 focus:ring-stone-500"
39
- />
40
- {LAYER_LABELS[layer] ?? layer}
41
- </label>
42
  ))}
43
  </div>
44
  </div>
 
1
  import type { FC } from 'react'
2
+ import { RetroCheckbox } from './retro'
3
 
4
  const LAYER_LABELS: Record<string, string> = {
5
  image: 'Image',
6
+ ocr_diplomatic: 'Transcription',
7
+ ocr_normalized: 'Normalise',
8
+ translation_fr: 'Traduction FR',
9
+ translation_en: 'Traduction EN',
10
+ summary: 'Resume',
11
+ scholarly_commentary: 'Comm. savant',
12
+ public_commentary: 'Comm. public',
13
  iconography_detection: 'Iconographie',
14
+ material_notes: 'Notes mat.',
15
  uncertainty: 'Incertitudes',
16
  }
17
 
 
22
  }
23
 
24
  const LayerPanel: FC<Props> = ({ activeLayers, visibleLayers, onToggle }) => (
25
+ <div className="px-2 py-2 shrink-0 border-b border-retro-black bg-retro-gray">
26
+ <div className="text-retro-xs font-bold mb-1">Couches</div>
27
+ <div className="flex flex-wrap gap-x-3 gap-y-1">
 
 
28
  {activeLayers.map((layer) => (
29
+ <RetroCheckbox
30
  key={layer}
31
+ label={LAYER_LABELS[layer] ?? layer}
32
+ checked={visibleLayers.has(layer)}
33
+ onChange={() => onToggle(layer)}
34
+ />
 
 
 
 
 
 
35
  ))}
36
  </div>
37
  </div>
frontend/src/components/TranscriptionPanel.tsx CHANGED
@@ -1,20 +1,21 @@
1
  import type { FC } from 'react'
2
  import type { OCRResult, EditorialInfo, EditorialStatus } from '../lib/api.ts'
 
3
 
4
  const STATUS_LABELS: Record<EditorialStatus, string> = {
5
  machine_draft: 'Brouillon IA',
6
- needs_review: 'À réviser',
7
- reviewed: 'Révisé',
8
- validated: 'Validé',
9
- published: 'Publié',
10
  }
11
 
12
- const STATUS_COLORS: Record<EditorialStatus, string> = {
13
- machine_draft: 'bg-amber-100 text-amber-700',
14
- needs_review: 'bg-orange-100 text-orange-700',
15
- reviewed: 'bg-blue-100 text-blue-700',
16
- validated: 'bg-green-100 text-green-700',
17
- published: 'bg-emerald-100 text-emerald-700',
18
  }
19
 
20
  interface Props {
@@ -27,34 +28,30 @@ const TranscriptionPanel: FC<Props> = ({ ocr, editorial, visible }) => {
27
  if (!visible) return null
28
 
29
  return (
30
- <div className="px-4 py-4">
31
- <div className="flex items-center justify-between mb-3">
32
- <h3 className="text-xs font-semibold text-stone-500 uppercase tracking-wide">
33
- Transcription diplomatique
34
- </h3>
35
- <span
36
- className={`text-xs px-2 py-0.5 rounded-full font-medium ${STATUS_COLORS[editorial.status]}`}
37
- >
38
  {STATUS_LABELS[editorial.status]}
39
- </span>
40
  </div>
41
  {ocr ? (
42
  <div>
43
  {ocr.diplomatic_text ? (
44
- <p className="text-sm text-stone-800 leading-relaxed font-serif whitespace-pre-wrap">
45
  {ocr.diplomatic_text}
46
  </p>
47
  ) : (
48
- <p className="text-sm text-stone-400 italic">Texte vide.</p>
49
  )}
50
  {ocr.confidence > 0 && (
51
- <div className="mt-2 text-xs text-stone-400">
52
- Confiance OCR : {(ocr.confidence * 100).toFixed(0)} %
53
  </div>
54
  )}
55
  </div>
56
  ) : (
57
- <p className="text-sm text-stone-400 italic">Transcription non disponible.</p>
58
  )}
59
  </div>
60
  )
 
1
  import type { FC } from 'react'
2
  import type { OCRResult, EditorialInfo, EditorialStatus } from '../lib/api.ts'
3
+ import { RetroBadge } from './retro'
4
 
5
  const STATUS_LABELS: Record<EditorialStatus, string> = {
6
  machine_draft: 'Brouillon IA',
7
+ needs_review: 'A reviser',
8
+ reviewed: 'Revise',
9
+ validated: 'Valide',
10
+ published: 'Publie',
11
  }
12
 
13
+ const STATUS_VARIANTS: Record<EditorialStatus, 'default' | 'success' | 'warning' | 'error' | 'info'> = {
14
+ machine_draft: 'info',
15
+ needs_review: 'warning',
16
+ reviewed: 'default',
17
+ validated: 'success',
18
+ published: 'success',
19
  }
20
 
21
  interface Props {
 
28
  if (!visible) return null
29
 
30
  return (
31
+ <div className="p-2">
32
+ <div className="flex items-center justify-between mb-2">
33
+ <span className="text-retro-xs font-bold">Transcription diplomatique</span>
34
+ <RetroBadge variant={STATUS_VARIANTS[editorial.status]}>
 
 
 
 
35
  {STATUS_LABELS[editorial.status]}
36
+ </RetroBadge>
37
  </div>
38
  {ocr ? (
39
  <div>
40
  {ocr.diplomatic_text ? (
41
+ <p className="text-retro-sm whitespace-pre-wrap font-retro leading-relaxed">
42
  {ocr.diplomatic_text}
43
  </p>
44
  ) : (
45
+ <p className="text-retro-sm text-retro-darkgray">Texte vide.</p>
46
  )}
47
  {ocr.confidence > 0 && (
48
+ <div className="mt-2 text-retro-xs text-retro-darkgray">
49
+ Confiance : {(ocr.confidence * 100).toFixed(0)}% — Langue : {ocr.language}
50
  </div>
51
  )}
52
  </div>
53
  ) : (
54
+ <p className="text-retro-sm text-retro-darkgray">Transcription non disponible.</p>
55
  )}
56
  </div>
57
  )
frontend/src/components/TranslationPanel.tsx CHANGED
@@ -1,20 +1,21 @@
1
  import type { FC } from 'react'
2
  import type { Translation, EditorialInfo, EditorialStatus } from '../lib/api.ts'
 
3
 
4
  const STATUS_LABELS: Record<EditorialStatus, string> = {
5
  machine_draft: 'Brouillon IA',
6
- needs_review: 'À réviser',
7
- reviewed: 'Révisé',
8
- validated: 'Validé',
9
- published: 'Publié',
10
  }
11
 
12
- const STATUS_COLORS: Record<EditorialStatus, string> = {
13
- machine_draft: 'bg-amber-100 text-amber-700',
14
- needs_review: 'bg-orange-100 text-orange-700',
15
- reviewed: 'bg-blue-100 text-blue-700',
16
- validated: 'bg-green-100 text-green-700',
17
- published: 'bg-emerald-100 text-emerald-700',
18
  }
19
 
20
  interface Props {
@@ -27,23 +28,19 @@ const TranslationPanel: FC<Props> = ({ translation, editorial, visible }) => {
27
  if (!visible) return null
28
 
29
  return (
30
- <div className="px-4 py-4">
31
- <div className="flex items-center justify-between mb-3">
32
- <h3 className="text-xs font-semibold text-stone-500 uppercase tracking-wide">
33
- Traduction (FR)
34
- </h3>
35
- <span
36
- className={`text-xs px-2 py-0.5 rounded-full font-medium ${STATUS_COLORS[editorial.status]}`}
37
- >
38
  {STATUS_LABELS[editorial.status]}
39
- </span>
40
  </div>
41
  {translation?.fr ? (
42
- <p className="text-sm text-stone-800 leading-relaxed whitespace-pre-wrap">
43
  {translation.fr}
44
  </p>
45
  ) : (
46
- <p className="text-sm text-stone-400 italic">Traduction non disponible.</p>
47
  )}
48
  </div>
49
  )
 
1
  import type { FC } from 'react'
2
  import type { Translation, EditorialInfo, EditorialStatus } from '../lib/api.ts'
3
+ import { RetroBadge } from './retro'
4
 
5
  const STATUS_LABELS: Record<EditorialStatus, string> = {
6
  machine_draft: 'Brouillon IA',
7
+ needs_review: 'A reviser',
8
+ reviewed: 'Revise',
9
+ validated: 'Valide',
10
+ published: 'Publie',
11
  }
12
 
13
+ const STATUS_VARIANTS: Record<EditorialStatus, 'default' | 'success' | 'warning' | 'error' | 'info'> = {
14
+ machine_draft: 'info',
15
+ needs_review: 'warning',
16
+ reviewed: 'default',
17
+ validated: 'success',
18
+ published: 'success',
19
  }
20
 
21
  interface Props {
 
28
  if (!visible) return null
29
 
30
  return (
31
+ <div className="p-2">
32
+ <div className="flex items-center justify-between mb-2">
33
+ <span className="text-retro-xs font-bold">Traduction (FR)</span>
34
+ <RetroBadge variant={STATUS_VARIANTS[editorial.status]}>
 
 
 
 
35
  {STATUS_LABELS[editorial.status]}
36
+ </RetroBadge>
37
  </div>
38
  {translation?.fr ? (
39
+ <p className="text-retro-sm whitespace-pre-wrap font-retro leading-relaxed">
40
  {translation.fr}
41
  </p>
42
  ) : (
43
+ <p className="text-retro-sm text-retro-darkgray">Traduction non disponible.</p>
44
  )}
45
  </div>
46
  )
frontend/src/components/Viewer.tsx CHANGED
@@ -1,5 +1,6 @@
1
  import { useEffect, useRef, type FC } from 'react'
2
  import OpenSeadragon from 'openseadragon'
 
3
 
4
  interface Props {
5
  imageUrl: string
@@ -10,7 +11,6 @@ const Viewer: FC<Props> = ({ imageUrl, onViewerReady }) => {
10
  const containerRef = useRef<HTMLDivElement>(null)
11
  const viewerRef = useRef<OpenSeadragon.Viewer | null>(null)
12
 
13
- // Initialise OSD une seule fois
14
  useEffect(() => {
15
  if (!containerRef.current) return
16
 
@@ -32,7 +32,6 @@ const Viewer: FC<Props> = ({ imageUrl, onViewerReady }) => {
32
  }
33
  }, [])
34
 
35
- // Ouvre l'image à chaque changement d'URL
36
  useEffect(() => {
37
  const viewer = viewerRef.current
38
  if (!viewer || !imageUrl) return
@@ -42,40 +41,39 @@ const Viewer: FC<Props> = ({ imageUrl, onViewerReady }) => {
42
  onViewerReady?.(viewer)
43
  })
44
  }, [imageUrl]) // eslint-disable-line react-hooks/exhaustive-deps
45
- // onViewerReady est intentionnellement exclu : c'est un callback stable
46
 
47
  return (
48
- <div className="relative w-full h-full bg-stone-800">
49
  <div ref={containerRef} className="w-full h-full" />
50
- <div className="absolute bottom-3 right-3 flex gap-1.5">
51
- <button
 
52
  onClick={() => viewerRef.current?.viewport.zoomBy(1.5)}
53
- className="w-8 h-8 bg-stone-800/80 text-stone-200 rounded hover:bg-stone-700 text-sm font-bold"
54
  title="Zoom +"
55
  >
56
  +
57
- </button>
58
- <button
 
59
  onClick={() => viewerRef.current?.viewport.zoomBy(0.67)}
60
- className="w-8 h-8 bg-stone-800/80 text-stone-200 rounded hover:bg-stone-700 text-sm font-bold"
61
- title="Zoom −"
62
  >
63
-
64
- </button>
65
- <button
 
66
  onClick={() => viewerRef.current?.viewport.goHome()}
67
- className="w-8 h-8 bg-stone-800/80 text-stone-200 rounded hover:bg-stone-700 text-xs"
68
- title="Réinitialiser"
69
  >
70
-
71
- </button>
72
- <button
 
73
  onClick={() => viewerRef.current?.setFullScreen(true)}
74
- className="w-8 h-8 bg-stone-800/80 text-stone-200 rounded hover:bg-stone-700 text-xs"
75
- title="Plein écran"
76
  >
77
-
78
- </button>
79
  </div>
80
  </div>
81
  )
 
1
  import { useEffect, useRef, type FC } from 'react'
2
  import OpenSeadragon from 'openseadragon'
3
+ import { RetroButton } from './retro'
4
 
5
  interface Props {
6
  imageUrl: string
 
11
  const containerRef = useRef<HTMLDivElement>(null)
12
  const viewerRef = useRef<OpenSeadragon.Viewer | null>(null)
13
 
 
14
  useEffect(() => {
15
  if (!containerRef.current) return
16
 
 
32
  }
33
  }, [])
34
 
 
35
  useEffect(() => {
36
  const viewer = viewerRef.current
37
  if (!viewer || !imageUrl) return
 
41
  onViewerReady?.(viewer)
42
  })
43
  }, [imageUrl]) // eslint-disable-line react-hooks/exhaustive-deps
 
44
 
45
  return (
46
+ <div className="relative w-full h-full bg-retro-black">
47
  <div ref={containerRef} className="w-full h-full" />
48
+ <div className="absolute bottom-2 right-2 flex gap-[2px]">
49
+ <RetroButton
50
+ size="sm"
51
  onClick={() => viewerRef.current?.viewport.zoomBy(1.5)}
 
52
  title="Zoom +"
53
  >
54
  +
55
+ </RetroButton>
56
+ <RetroButton
57
+ size="sm"
58
  onClick={() => viewerRef.current?.viewport.zoomBy(0.67)}
59
+ title="Zoom -"
 
60
  >
61
+ -
62
+ </RetroButton>
63
+ <RetroButton
64
+ size="sm"
65
  onClick={() => viewerRef.current?.viewport.goHome()}
66
+ title="Reset"
 
67
  >
68
+ o
69
+ </RetroButton>
70
+ <RetroButton
71
+ size="sm"
72
  onClick={() => viewerRef.current?.setFullScreen(true)}
73
+ title="Plein ecran"
 
74
  >
75
+ []
76
+ </RetroButton>
77
  </div>
78
  </div>
79
  )
frontend/src/pages/Reader.tsx CHANGED
@@ -15,6 +15,7 @@ import LayerPanel from '../components/LayerPanel.tsx'
15
  import TranscriptionPanel from '../components/TranscriptionPanel.tsx'
16
  import TranslationPanel from '../components/TranslationPanel.tsx'
17
  import CommentaryPanel from '../components/CommentaryPanel.tsx'
 
18
 
19
  interface Props {
20
  manuscriptId: string
@@ -34,7 +35,6 @@ export default function Reader({ manuscriptId, profileId, onBack, onEdit }: Prop
34
  const [loading, setLoading] = useState(true)
35
  const [error, setError] = useState<string | null>(null)
36
 
37
- // Chargement initial : liste des pages + profil
38
  useEffect(() => {
39
  Promise.all([fetchPages(manuscriptId), fetchProfile(profileId)])
40
  .then(([pgs, prof]) => {
@@ -47,7 +47,6 @@ export default function Reader({ manuscriptId, profileId, onBack, onEdit }: Prop
47
  .finally(() => setLoading(false))
48
  }, [manuscriptId, profileId])
49
 
50
- // Chargement du master.json à chaque changement de page
51
  useEffect(() => {
52
  if (pages.length === 0) return
53
  setMaster(null)
@@ -68,25 +67,38 @@ export default function Reader({ manuscriptId, profileId, onBack, onEdit }: Prop
68
  })
69
  }, [])
70
 
 
71
  if (loading) {
72
  return (
73
- <div className="flex items-center justify-center h-screen text-stone-500">
74
- Chargement
 
 
75
  </div>
76
  )
77
  }
78
 
79
  if (error) {
80
- return <div className="p-8 text-red-600">Erreur : {error}</div>
 
 
 
 
 
 
81
  }
82
 
83
  if (pages.length === 0) {
84
  return (
85
- <div className="p-8 text-stone-500">
86
- Aucune page dans ce manuscrit.{' '}
87
- <button onClick={onBack} className="underline">
88
- Retour
89
- </button>
 
 
 
 
90
  </div>
91
  )
92
  }
@@ -96,106 +108,119 @@ export default function Reader({ manuscriptId, profileId, onBack, onEdit }: Prop
96
  const regions: Region[] = master?.layout?.regions ?? []
97
 
98
  return (
99
- <div className="flex flex-col h-screen bg-stone-100">
100
- {/* ── Barre de navigation ─────────────────────────────────────────────── */}
101
- <header className="flex items-center gap-3 bg-stone-900 text-stone-100 px-5 py-2.5 shrink-0">
102
- <button
103
- onClick={onBack}
104
- className="text-stone-400 hover:text-stone-100 text-sm transition-colors"
105
- >
106
- ← Corpus
107
- </button>
108
- <span className="text-stone-600">|</span>
109
- <span className="text-sm font-medium text-stone-200 truncate max-w-xs">
110
- {profile?.label ?? profileId}
111
- </span>
112
-
113
- <div className="ml-auto flex items-center gap-2">
114
- <span className="text-stone-400 text-xs">
115
- {currentPage.folio_label} — {currentIndex + 1} / {pages.length}
116
- </span>
117
- {onEdit && (
118
- <button
119
- onClick={() => onEdit(currentPage.id)}
120
- className="px-3 py-1 bg-amber-600 hover:bg-amber-500 rounded text-sm text-white transition-colors"
121
  >
122
- Éditer cette page
123
- </button>
124
- )}
125
- <button
126
- disabled={currentIndex === 0}
127
- onClick={() => setCurrentIndex((i) => i - 1)}
128
- className="px-3 py-1 bg-stone-700 hover:bg-stone-600 disabled:opacity-30 rounded text-sm transition-colors"
129
- >
130
-
131
- </button>
132
- <button
133
- disabled={currentIndex === pages.length - 1}
134
- onClick={() => setCurrentIndex((i) => i + 1)}
135
- className="px-3 py-1 bg-stone-700 hover:bg-stone-600 disabled:opacity-30 rounded text-sm transition-colors"
136
- >
137
-
138
- </button>
139
- </div>
140
- </header>
141
-
142
- {/* ── Contenu principal ───────────────────────────────────────────────── */}
143
- <div className="flex flex-1 overflow-hidden">
144
- {/* Visionneuse 70% */}
145
- <div className="relative flex flex-col" style={{ width: '70%' }}>
146
- <Viewer imageUrl={imageUrl} onViewerReady={handleViewerReady} />
147
- <RegionOverlay
148
- viewer={osdViewer}
149
- regions={regions}
150
- onRegionClick={setSelectedRegion}
151
- />
152
-
153
- {/* Fiche région (popup) */}
154
- {selectedRegion && (
155
- <div className="absolute bottom-14 left-4 bg-white/95 backdrop-blur-sm rounded-lg shadow-xl border border-stone-200 p-3 max-w-xs text-xs">
156
- <div className="flex items-center justify-between gap-4 mb-1.5">
157
- <span className="font-semibold text-stone-800 capitalize">
158
- {selectedRegion.type.replace(/_/g, ' ')}
159
- </span>
160
- <button
161
- onClick={() => setSelectedRegion(null)}
162
- className="text-stone-400 hover:text-stone-700 leading-none"
163
- >
164
-
165
- </button>
166
- </div>
167
- <div className="space-y-0.5 text-stone-500">
168
- <div>id : <span className="font-mono">{selectedRegion.id}</span></div>
169
- <div>confiance : {(selectedRegion.confidence * 100).toFixed(0)} %</div>
170
- <div>bbox : [{selectedRegion.bbox.join(', ')}]</div>
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
171
  </div>
172
- </div>
173
- )}
174
 
175
- {/* Indicateur page non analysée */}
176
- {!master && !loading && imageUrl && (
177
- <div className="absolute top-3 left-3 bg-amber-500/90 text-white text-xs px-3 py-1 rounded-full">
178
- Page non analysée
179
- </div>
180
- )}
181
- </div>
 
182
 
183
- {/* Panneaux droite 30% */}
184
- <div
185
- className="flex flex-col overflow-hidden border-l border-stone-200 bg-white"
186
- style={{ width: '30%' }}
 
187
  >
188
- {profile && (
189
- <LayerPanel
190
- activeLayers={profile.active_layers}
191
- visibleLayers={visibleLayers}
192
- onToggle={toggleLayer}
193
- />
194
- )}
 
 
195
 
196
- <div className="flex-1 overflow-y-auto divide-y divide-stone-100">
197
  {master ? (
198
- <>
199
  <TranscriptionPanel
200
  ocr={master.ocr}
201
  editorial={master.editorial}
@@ -212,18 +237,17 @@ export default function Reader({ manuscriptId, profileId, onBack, onEdit }: Prop
212
  visiblePublic={visibleLayers.has('public_commentary')}
213
  visibleScholarly={visibleLayers.has('scholarly_commentary')}
214
  />
215
- </>
216
  ) : (
217
- <div className="p-4 text-sm text-stone-400 italic">
218
- {imageUrl ? (
219
- <span>Page non encore analysée par l&apos;IA.</span>
220
- ) : (
221
- <span>Aucune image associée à cette page.</span>
222
- )}
223
  </div>
224
  )}
225
  </div>
226
- </div>
227
  </div>
228
  </div>
229
  )
 
15
  import TranscriptionPanel from '../components/TranscriptionPanel.tsx'
16
  import TranslationPanel from '../components/TranslationPanel.tsx'
17
  import CommentaryPanel from '../components/CommentaryPanel.tsx'
18
+ import { RetroMenuBar, RetroWindow, RetroButton, RetroBadge } from '../components/retro'
19
 
20
  interface Props {
21
  manuscriptId: string
 
35
  const [loading, setLoading] = useState(true)
36
  const [error, setError] = useState<string | null>(null)
37
 
 
38
  useEffect(() => {
39
  Promise.all([fetchPages(manuscriptId), fetchProfile(profileId)])
40
  .then(([pgs, prof]) => {
 
47
  .finally(() => setLoading(false))
48
  }, [manuscriptId, profileId])
49
 
 
50
  useEffect(() => {
51
  if (pages.length === 0) return
52
  setMaster(null)
 
67
  })
68
  }, [])
69
 
70
+ // ── Loading ─────────────────────────────────────────────────────────
71
  if (loading) {
72
  return (
73
+ <div className="min-h-screen bg-retro-dither flex items-center justify-center">
74
+ <RetroWindow title="Chargement" className="w-64">
75
+ <div className="p-4 text-retro-sm text-center">Chargement...</div>
76
+ </RetroWindow>
77
  </div>
78
  )
79
  }
80
 
81
  if (error) {
82
+ return (
83
+ <div className="min-h-screen bg-retro-dither flex items-center justify-center">
84
+ <RetroWindow title="Erreur" className="w-80">
85
+ <div className="p-4 text-retro-sm">{error}</div>
86
+ </RetroWindow>
87
+ </div>
88
+ )
89
  }
90
 
91
  if (pages.length === 0) {
92
  return (
93
+ <div className="min-h-screen bg-retro-dither flex items-center justify-center">
94
+ <RetroWindow title="Manuscrit vide" className="w-80">
95
+ <div className="p-4 text-retro-sm">
96
+ Aucune page dans ce manuscrit.
97
+ <div className="mt-2">
98
+ <RetroButton onClick={onBack}>Retour</RetroButton>
99
+ </div>
100
+ </div>
101
+ </RetroWindow>
102
  </div>
103
  )
104
  }
 
108
  const regions: Region[] = master?.layout?.regions ?? []
109
 
110
  return (
111
+ <div className="flex flex-col h-screen bg-retro-dither">
112
+ {/* ── Menu bar ───────────────────────────────────────────────── */}
113
+ <RetroMenuBar
114
+ items={[
115
+ { label: 'IIIF Studio', onClick: onBack },
116
+ { label: profile?.label ?? profileId },
117
+ ]}
118
+ right={
119
+ <div className="flex items-center gap-1">
120
+ <span className="text-retro-xs px-2">
121
+ {currentPage.folio_label} {currentIndex + 1}/{pages.length}
122
+ </span>
123
+ <RetroButton
124
+ size="sm"
125
+ disabled={currentIndex === 0}
126
+ onClick={() => setCurrentIndex((i) => i - 1)}
 
 
 
 
 
 
127
  >
128
+ Prev
129
+ </RetroButton>
130
+ <RetroButton
131
+ size="sm"
132
+ disabled={currentIndex === pages.length - 1}
133
+ onClick={() => setCurrentIndex((i) => i + 1)}
134
+ >
135
+ Next
136
+ </RetroButton>
137
+ {onEdit && (
138
+ <RetroButton size="sm" onClick={() => onEdit(currentPage.id)}>
139
+ Editer
140
+ </RetroButton>
141
+ )}
142
+ </div>
143
+ }
144
+ />
145
+
146
+ {/* ── Main content ───────────────────────────────────────────── */}
147
+ <div className="flex flex-1 overflow-hidden p-1 gap-1">
148
+
149
+ {/* ── Viewer window (left, 70%) ──────────────────────────── */}
150
+ <RetroWindow
151
+ title={`Folio ${currentPage.folio_label}`}
152
+ statusBar={
153
+ master
154
+ ? `${master.editorial.status} — v${master.editorial.version}`
155
+ : imageUrl ? 'Page non analysee' : 'Aucune image'
156
+ }
157
+ className="flex-[7] min-w-0"
158
+ >
159
+ <div className="relative w-full h-full">
160
+ <Viewer imageUrl={imageUrl} onViewerReady={handleViewerReady} />
161
+ <RegionOverlay
162
+ viewer={osdViewer}
163
+ regions={regions}
164
+ onRegionClick={setSelectedRegion}
165
+ />
166
+
167
+ {/* Region info popup */}
168
+ {selectedRegion && (
169
+ <div
170
+ className="
171
+ absolute bottom-12 left-2
172
+ border-retro border-retro-black
173
+ bg-retro-white shadow-retro
174
+ text-retro-xs p-2 max-w-[220px]
175
+ "
176
+ >
177
+ <div className="flex items-center justify-between gap-2 mb-1">
178
+ <span className="font-bold capitalize">
179
+ {selectedRegion.type.replace(/_/g, ' ')}
180
+ </span>
181
+ <button
182
+ onClick={() => setSelectedRegion(null)}
183
+ className="text-retro-black font-bold hover:bg-retro-black hover:text-retro-white px-1"
184
+ >
185
+ x
186
+ </button>
187
+ </div>
188
+ <div className="space-y-[1px] text-retro-darkgray">
189
+ <div>id: {selectedRegion.id}</div>
190
+ <div>confiance: {(selectedRegion.confidence * 100).toFixed(0)}%</div>
191
+ <div>bbox: [{selectedRegion.bbox.join(', ')}]</div>
192
+ </div>
193
  </div>
194
+ )}
 
195
 
196
+ {/* Not analyzed badge */}
197
+ {!master && !loading && imageUrl && (
198
+ <div className="absolute top-2 left-2">
199
+ <RetroBadge variant="warning">Non analysee</RetroBadge>
200
+ </div>
201
+ )}
202
+ </div>
203
+ </RetroWindow>
204
 
205
+ {/* ── Right panels (30%) ─────────────────────────────────── */}
206
+ <RetroWindow
207
+ title="Analyse"
208
+ className="flex-[3] min-w-0"
209
+ scrollable
210
  >
211
+ <div className="flex flex-col">
212
+ {/* Layer toggles */}
213
+ {profile && (
214
+ <LayerPanel
215
+ activeLayers={profile.active_layers}
216
+ visibleLayers={visibleLayers}
217
+ onToggle={toggleLayer}
218
+ />
219
+ )}
220
 
221
+ {/* Content panels */}
222
  {master ? (
223
+ <div className="divide-y divide-retro-gray">
224
  <TranscriptionPanel
225
  ocr={master.ocr}
226
  editorial={master.editorial}
 
237
  visiblePublic={visibleLayers.has('public_commentary')}
238
  visibleScholarly={visibleLayers.has('scholarly_commentary')}
239
  />
240
+ </div>
241
  ) : (
242
+ <div className="p-3 text-retro-sm text-retro-darkgray">
243
+ {imageUrl
244
+ ? 'Page non encore analysee par l\'IA.'
245
+ : 'Aucune image associee a cette page.'
246
+ }
 
247
  </div>
248
  )}
249
  </div>
250
+ </RetroWindow>
251
  </div>
252
  </div>
253
  )