Spaces:
Build error
feat(frontend): Sprint R1 — retro design system foundation
Browse filesDesign system inspired by Xerox Star, Smalltalk-80, and classic Mac:
- Tailwind config: retro color palette (monochrome), pixel-perfect
shadows (outset/inset bevel, hard drop shadows), bitmap font tokens
- Global CSS: IBM Plex Mono font, retro scrollbars (Win 3.1 style),
dithered background patterns, inverted selection, dotted focus rings
- 9 primitive components in src/components/retro/:
RetroWindow (title bar + status bar + close button),
RetroButton (3D bevel outset/inset), RetroMenuBar (classic Mac menu),
RetroIcon (desktop icon with label), RetroCheckbox (pixel checkbox),
RetroInput, RetroTextarea, RetroSelect (all with inset wells),
RetroBadge (monochrome status indicators)
- RetroDemo page to showcase all components (temporary, removed in R2)
- Barrel export index.ts for clean imports
No existing components modified. Build passes (tsc + vite).
https://claude.ai/code/session_01WWohTtw2CxGRawmpH1tyrY
- frontend/src/App.tsx +6 -0
- frontend/src/components/retro/RetroBadge.tsx +38 -0
- frontend/src/components/retro/RetroButton.tsx +46 -0
- frontend/src/components/retro/RetroCheckbox.tsx +48 -0
- frontend/src/components/retro/RetroIcon.tsx +63 -0
- frontend/src/components/retro/RetroInput.tsx +30 -0
- frontend/src/components/retro/RetroMenuBar.tsx +55 -0
- frontend/src/components/retro/RetroSelect.tsx +38 -0
- frontend/src/components/retro/RetroTextarea.tsx +32 -0
- frontend/src/components/retro/RetroWindow.tsx +96 -0
- frontend/src/components/retro/index.ts +9 -0
- frontend/src/index.css +123 -0
- frontend/src/pages/RetroDemo.tsx +198 -0
- frontend/tailwind.config.js +47 -1
|
@@ -3,12 +3,14 @@ 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 |
|
| 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,6 +39,10 @@ export default function App() {
|
|
| 37 |
)
|
| 38 |
}
|
| 39 |
|
|
|
|
|
|
|
|
|
|
|
|
|
| 40 |
return (
|
| 41 |
<Home
|
| 42 |
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 |
+
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 |
)
|
| 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) =>
|
|
@@ -0,0 +1,38 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import type { ReactNode } from 'react'
|
| 2 |
+
|
| 3 |
+
type Variant = 'default' | 'success' | 'warning' | 'error' | 'info'
|
| 4 |
+
|
| 5 |
+
interface Props {
|
| 6 |
+
children: ReactNode
|
| 7 |
+
variant?: Variant
|
| 8 |
+
className?: string
|
| 9 |
+
}
|
| 10 |
+
|
| 11 |
+
const variantStyles: Record<Variant, string> = {
|
| 12 |
+
default: 'bg-retro-gray text-retro-black',
|
| 13 |
+
success: 'bg-retro-black text-retro-white',
|
| 14 |
+
warning: 'bg-retro-white text-retro-black border-dashed',
|
| 15 |
+
error: 'bg-retro-white text-retro-black font-bold',
|
| 16 |
+
info: 'bg-retro-light text-retro-black',
|
| 17 |
+
}
|
| 18 |
+
|
| 19 |
+
export default function RetroBadge({
|
| 20 |
+
children,
|
| 21 |
+
variant = 'default',
|
| 22 |
+
className = '',
|
| 23 |
+
}: Props) {
|
| 24 |
+
return (
|
| 25 |
+
<span
|
| 26 |
+
className={`
|
| 27 |
+
inline-block
|
| 28 |
+
px-2 py-[1px]
|
| 29 |
+
text-retro-xs font-retro
|
| 30 |
+
border border-retro-black
|
| 31 |
+
${variantStyles[variant]}
|
| 32 |
+
${className}
|
| 33 |
+
`}
|
| 34 |
+
>
|
| 35 |
+
{children}
|
| 36 |
+
</span>
|
| 37 |
+
)
|
| 38 |
+
}
|
|
@@ -0,0 +1,46 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import type { ReactNode, ButtonHTMLAttributes } from 'react'
|
| 2 |
+
|
| 3 |
+
interface Props extends ButtonHTMLAttributes<HTMLButtonElement> {
|
| 4 |
+
children: ReactNode
|
| 5 |
+
/** Render as a small compact button */
|
| 6 |
+
size?: 'sm' | 'md'
|
| 7 |
+
/** If true, renders in "pressed" state */
|
| 8 |
+
pressed?: boolean
|
| 9 |
+
}
|
| 10 |
+
|
| 11 |
+
export default function RetroButton({
|
| 12 |
+
children,
|
| 13 |
+
size = 'md',
|
| 14 |
+
pressed = false,
|
| 15 |
+
className = '',
|
| 16 |
+
disabled,
|
| 17 |
+
...rest
|
| 18 |
+
}: Props) {
|
| 19 |
+
const padding = size === 'sm' ? 'px-2 py-[1px]' : 'px-3 py-[3px]'
|
| 20 |
+
const fontSize = size === 'sm' ? 'text-retro-xs' : 'text-retro-sm'
|
| 21 |
+
|
| 22 |
+
return (
|
| 23 |
+
<button
|
| 24 |
+
className={`
|
| 25 |
+
${padding} ${fontSize}
|
| 26 |
+
font-retro font-medium
|
| 27 |
+
bg-retro-gray
|
| 28 |
+
border border-retro-black
|
| 29 |
+
${pressed
|
| 30 |
+
? 'shadow-retro-inset'
|
| 31 |
+
: 'shadow-retro-outset active:shadow-retro-inset'
|
| 32 |
+
}
|
| 33 |
+
${disabled
|
| 34 |
+
? 'text-retro-darkgray cursor-not-allowed'
|
| 35 |
+
: 'text-retro-black hover:bg-retro-light cursor-pointer'
|
| 36 |
+
}
|
| 37 |
+
select-none
|
| 38 |
+
${className}
|
| 39 |
+
`}
|
| 40 |
+
disabled={disabled}
|
| 41 |
+
{...rest}
|
| 42 |
+
>
|
| 43 |
+
{children}
|
| 44 |
+
</button>
|
| 45 |
+
)
|
| 46 |
+
}
|
|
@@ -0,0 +1,48 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
interface Props {
|
| 2 |
+
/** Label text next to the checkbox */
|
| 3 |
+
label: string
|
| 4 |
+
/** Controlled checked state */
|
| 5 |
+
checked: boolean
|
| 6 |
+
/** Change handler */
|
| 7 |
+
onChange: (checked: boolean) => void
|
| 8 |
+
/** Disabled state */
|
| 9 |
+
disabled?: boolean
|
| 10 |
+
/** Extra CSS classes on the wrapper */
|
| 11 |
+
className?: string
|
| 12 |
+
}
|
| 13 |
+
|
| 14 |
+
export default function RetroCheckbox({
|
| 15 |
+
label,
|
| 16 |
+
checked,
|
| 17 |
+
onChange,
|
| 18 |
+
disabled = false,
|
| 19 |
+
className = '',
|
| 20 |
+
}: Props) {
|
| 21 |
+
return (
|
| 22 |
+
<label
|
| 23 |
+
onClick={() => { if (!disabled) onChange(!checked) }}
|
| 24 |
+
className={`
|
| 25 |
+
inline-flex items-center gap-[6px]
|
| 26 |
+
text-retro-sm font-retro
|
| 27 |
+
${disabled ? 'text-retro-darkgray cursor-not-allowed' : 'cursor-pointer'}
|
| 28 |
+
select-none
|
| 29 |
+
${className}
|
| 30 |
+
`}
|
| 31 |
+
>
|
| 32 |
+
<span
|
| 33 |
+
className={`
|
| 34 |
+
inline-flex items-center justify-center
|
| 35 |
+
w-[13px] h-[13px]
|
| 36 |
+
border border-retro-black
|
| 37 |
+
bg-retro-white
|
| 38 |
+
shadow-retro-well
|
| 39 |
+
text-[10px] leading-none font-bold
|
| 40 |
+
shrink-0
|
| 41 |
+
`}
|
| 42 |
+
>
|
| 43 |
+
{checked && <span className="text-retro-black">x</span>}
|
| 44 |
+
</span>
|
| 45 |
+
{label}
|
| 46 |
+
</label>
|
| 47 |
+
)
|
| 48 |
+
}
|
|
@@ -0,0 +1,63 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
interface Props {
|
| 2 |
+
/** Label displayed below the icon */
|
| 3 |
+
label: string
|
| 4 |
+
/** Emoji or text character used as the icon glyph */
|
| 5 |
+
glyph: string
|
| 6 |
+
/** Click handler */
|
| 7 |
+
onClick?: () => void
|
| 8 |
+
/** If true, renders in selected/highlighted state */
|
| 9 |
+
selected?: boolean
|
| 10 |
+
/** Extra CSS classes */
|
| 11 |
+
className?: string
|
| 12 |
+
}
|
| 13 |
+
|
| 14 |
+
export default function RetroIcon({
|
| 15 |
+
label,
|
| 16 |
+
glyph,
|
| 17 |
+
onClick,
|
| 18 |
+
selected = false,
|
| 19 |
+
className = '',
|
| 20 |
+
}: Props) {
|
| 21 |
+
return (
|
| 22 |
+
<button
|
| 23 |
+
onClick={onClick}
|
| 24 |
+
className={`
|
| 25 |
+
flex flex-col items-center gap-1
|
| 26 |
+
p-2 w-[80px]
|
| 27 |
+
cursor-pointer select-none
|
| 28 |
+
${className}
|
| 29 |
+
`}
|
| 30 |
+
onDoubleClick={onClick}
|
| 31 |
+
>
|
| 32 |
+
{/* Icon box */}
|
| 33 |
+
<div
|
| 34 |
+
className={`
|
| 35 |
+
w-[48px] h-[48px]
|
| 36 |
+
flex items-center justify-center
|
| 37 |
+
border border-retro-black
|
| 38 |
+
text-[24px] leading-none
|
| 39 |
+
${selected
|
| 40 |
+
? 'bg-retro-black text-retro-white'
|
| 41 |
+
: 'bg-retro-white text-retro-black hover:bg-retro-light'
|
| 42 |
+
}
|
| 43 |
+
shadow-retro-outset
|
| 44 |
+
`}
|
| 45 |
+
>
|
| 46 |
+
{glyph}
|
| 47 |
+
</div>
|
| 48 |
+
{/* Label */}
|
| 49 |
+
<span
|
| 50 |
+
className={`
|
| 51 |
+
text-retro-xs text-center leading-tight
|
| 52 |
+
max-w-full break-words
|
| 53 |
+
${selected
|
| 54 |
+
? 'bg-retro-select text-retro-select-text px-1'
|
| 55 |
+
: 'text-retro-black'
|
| 56 |
+
}
|
| 57 |
+
`}
|
| 58 |
+
>
|
| 59 |
+
{label}
|
| 60 |
+
</span>
|
| 61 |
+
</button>
|
| 62 |
+
)
|
| 63 |
+
}
|
|
@@ -0,0 +1,30 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import type { InputHTMLAttributes } from 'react'
|
| 2 |
+
|
| 3 |
+
interface Props extends InputHTMLAttributes<HTMLInputElement> {
|
| 4 |
+
/** Optional label rendered above the input */
|
| 5 |
+
label?: string
|
| 6 |
+
}
|
| 7 |
+
|
| 8 |
+
export default function RetroInput({ label, className = '', ...rest }: Props) {
|
| 9 |
+
return (
|
| 10 |
+
<div className="flex flex-col gap-[2px]">
|
| 11 |
+
{label && (
|
| 12 |
+
<label className="text-retro-xs font-retro font-medium text-retro-black">
|
| 13 |
+
{label}
|
| 14 |
+
</label>
|
| 15 |
+
)}
|
| 16 |
+
<input
|
| 17 |
+
className={`
|
| 18 |
+
px-2 py-[3px]
|
| 19 |
+
text-retro-sm font-retro
|
| 20 |
+
bg-retro-white text-retro-black
|
| 21 |
+
border border-retro-black
|
| 22 |
+
shadow-retro-well
|
| 23 |
+
placeholder:text-retro-darkgray
|
| 24 |
+
${className}
|
| 25 |
+
`}
|
| 26 |
+
{...rest}
|
| 27 |
+
/>
|
| 28 |
+
</div>
|
| 29 |
+
)
|
| 30 |
+
}
|
|
@@ -0,0 +1,55 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import type { ReactNode } from 'react'
|
| 2 |
+
|
| 3 |
+
export interface MenuItem {
|
| 4 |
+
label: string
|
| 5 |
+
onClick?: () => void
|
| 6 |
+
disabled?: boolean
|
| 7 |
+
}
|
| 8 |
+
|
| 9 |
+
interface Props {
|
| 10 |
+
/** Left-aligned items (logo, menus) */
|
| 11 |
+
items?: MenuItem[]
|
| 12 |
+
/** Right-aligned content (search, status) */
|
| 13 |
+
right?: ReactNode
|
| 14 |
+
/** Extra CSS classes */
|
| 15 |
+
className?: string
|
| 16 |
+
}
|
| 17 |
+
|
| 18 |
+
export default function RetroMenuBar({ items = [], right, className = '' }: Props) {
|
| 19 |
+
return (
|
| 20 |
+
<div
|
| 21 |
+
className={`
|
| 22 |
+
flex items-center
|
| 23 |
+
bg-retro-gray
|
| 24 |
+
border-b-retro border-retro-black
|
| 25 |
+
shadow-retro-outset
|
| 26 |
+
px-1 py-[2px]
|
| 27 |
+
select-none shrink-0
|
| 28 |
+
${className}
|
| 29 |
+
`}
|
| 30 |
+
>
|
| 31 |
+
{items.map((item, i) => (
|
| 32 |
+
<button
|
| 33 |
+
key={i}
|
| 34 |
+
onClick={item.onClick}
|
| 35 |
+
disabled={item.disabled}
|
| 36 |
+
className={`
|
| 37 |
+
px-3 py-[2px]
|
| 38 |
+
text-retro-sm font-retro font-medium
|
| 39 |
+
${item.disabled
|
| 40 |
+
? 'text-retro-darkgray cursor-not-allowed'
|
| 41 |
+
: 'text-retro-black hover:bg-retro-black hover:text-retro-white cursor-pointer'
|
| 42 |
+
}
|
| 43 |
+
`}
|
| 44 |
+
>
|
| 45 |
+
{item.label}
|
| 46 |
+
</button>
|
| 47 |
+
))}
|
| 48 |
+
{right && (
|
| 49 |
+
<div className="ml-auto flex items-center gap-1">
|
| 50 |
+
{right}
|
| 51 |
+
</div>
|
| 52 |
+
)}
|
| 53 |
+
</div>
|
| 54 |
+
)
|
| 55 |
+
}
|
|
@@ -0,0 +1,38 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import type { SelectHTMLAttributes } from 'react'
|
| 2 |
+
|
| 3 |
+
interface Props extends SelectHTMLAttributes<HTMLSelectElement> {
|
| 4 |
+
/** Optional label rendered above the select */
|
| 5 |
+
label?: string
|
| 6 |
+
/** Options: array of {value, label} */
|
| 7 |
+
options: { value: string; label: string }[]
|
| 8 |
+
}
|
| 9 |
+
|
| 10 |
+
export default function RetroSelect({ label, options, className = '', ...rest }: Props) {
|
| 11 |
+
return (
|
| 12 |
+
<div className="flex flex-col gap-[2px]">
|
| 13 |
+
{label && (
|
| 14 |
+
<label className="text-retro-xs font-retro font-medium text-retro-black">
|
| 15 |
+
{label}
|
| 16 |
+
</label>
|
| 17 |
+
)}
|
| 18 |
+
<select
|
| 19 |
+
className={`
|
| 20 |
+
px-2 py-[3px]
|
| 21 |
+
text-retro-sm font-retro
|
| 22 |
+
bg-retro-white text-retro-black
|
| 23 |
+
border border-retro-black
|
| 24 |
+
shadow-retro-well
|
| 25 |
+
cursor-pointer
|
| 26 |
+
${className}
|
| 27 |
+
`}
|
| 28 |
+
{...rest}
|
| 29 |
+
>
|
| 30 |
+
{options.map((opt) => (
|
| 31 |
+
<option key={opt.value} value={opt.value}>
|
| 32 |
+
{opt.label}
|
| 33 |
+
</option>
|
| 34 |
+
))}
|
| 35 |
+
</select>
|
| 36 |
+
</div>
|
| 37 |
+
)
|
| 38 |
+
}
|
|
@@ -0,0 +1,32 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import type { TextareaHTMLAttributes } from 'react'
|
| 2 |
+
|
| 3 |
+
interface Props extends TextareaHTMLAttributes<HTMLTextAreaElement> {
|
| 4 |
+
/** Optional label rendered above the textarea */
|
| 5 |
+
label?: string
|
| 6 |
+
}
|
| 7 |
+
|
| 8 |
+
export default function RetroTextarea({ label, className = '', ...rest }: Props) {
|
| 9 |
+
return (
|
| 10 |
+
<div className="flex flex-col gap-[2px]">
|
| 11 |
+
{label && (
|
| 12 |
+
<label className="text-retro-xs font-retro font-medium text-retro-black">
|
| 13 |
+
{label}
|
| 14 |
+
</label>
|
| 15 |
+
)}
|
| 16 |
+
<textarea
|
| 17 |
+
className={`
|
| 18 |
+
px-2 py-[3px]
|
| 19 |
+
text-retro-sm font-retro
|
| 20 |
+
bg-retro-white text-retro-black
|
| 21 |
+
border border-retro-black
|
| 22 |
+
shadow-retro-well
|
| 23 |
+
placeholder:text-retro-darkgray
|
| 24 |
+
retro-scroll
|
| 25 |
+
resize-y
|
| 26 |
+
${className}
|
| 27 |
+
`}
|
| 28 |
+
{...rest}
|
| 29 |
+
/>
|
| 30 |
+
</div>
|
| 31 |
+
)
|
| 32 |
+
}
|
|
@@ -0,0 +1,96 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import type { ReactNode } from 'react'
|
| 2 |
+
|
| 3 |
+
interface Props {
|
| 4 |
+
/** Window title shown in the title bar */
|
| 5 |
+
title: string
|
| 6 |
+
/** Optional status text displayed in the bottom status bar */
|
| 7 |
+
statusBar?: string
|
| 8 |
+
/** Called when the close button is clicked (omit to hide the button) */
|
| 9 |
+
onClose?: () => void
|
| 10 |
+
/** Extra CSS classes for the outer container */
|
| 11 |
+
className?: string
|
| 12 |
+
/** Content rendered inside the window body */
|
| 13 |
+
children: ReactNode
|
| 14 |
+
/** If true, window body is scrollable with retro scrollbars */
|
| 15 |
+
scrollable?: boolean
|
| 16 |
+
/** If true, the window is rendered as "active" (darker title bar) */
|
| 17 |
+
active?: boolean
|
| 18 |
+
}
|
| 19 |
+
|
| 20 |
+
export default function RetroWindow({
|
| 21 |
+
title,
|
| 22 |
+
statusBar,
|
| 23 |
+
onClose,
|
| 24 |
+
className = '',
|
| 25 |
+
children,
|
| 26 |
+
scrollable = false,
|
| 27 |
+
active = true,
|
| 28 |
+
}: Props) {
|
| 29 |
+
return (
|
| 30 |
+
<div
|
| 31 |
+
className={`
|
| 32 |
+
flex flex-col
|
| 33 |
+
border-retro border-retro-black bg-retro-gray
|
| 34 |
+
shadow-retro
|
| 35 |
+
${className}
|
| 36 |
+
`}
|
| 37 |
+
>
|
| 38 |
+
{/* ── Title bar ──────────────────────────────────────────── */}
|
| 39 |
+
<div
|
| 40 |
+
className={`
|
| 41 |
+
flex items-center gap-2 px-2 py-[3px]
|
| 42 |
+
select-none shrink-0
|
| 43 |
+
${active ? 'bg-retro-black text-retro-white' : 'bg-retro-darkgray text-retro-white'}
|
| 44 |
+
`}
|
| 45 |
+
>
|
| 46 |
+
{onClose && (
|
| 47 |
+
<button
|
| 48 |
+
onClick={onClose}
|
| 49 |
+
className="
|
| 50 |
+
w-[14px] h-[14px] flex items-center justify-center
|
| 51 |
+
border border-retro-white bg-retro-gray text-retro-black
|
| 52 |
+
text-[9px] leading-none font-bold
|
| 53 |
+
hover:bg-retro-white active:bg-retro-darkgray
|
| 54 |
+
"
|
| 55 |
+
aria-label="Fermer"
|
| 56 |
+
>
|
| 57 |
+
x
|
| 58 |
+
</button>
|
| 59 |
+
)}
|
| 60 |
+
<span className="flex-1 text-retro-xs font-bold truncate tracking-wide">
|
| 61 |
+
{title}
|
| 62 |
+
</span>
|
| 63 |
+
</div>
|
| 64 |
+
|
| 65 |
+
{/* ── Content area ───────────────────────────────────────── */}
|
| 66 |
+
<div
|
| 67 |
+
className={`
|
| 68 |
+
flex-1 bg-retro-white
|
| 69 |
+
border-t-0
|
| 70 |
+
m-[3px] mt-0
|
| 71 |
+
shadow-retro-well
|
| 72 |
+
${scrollable ? 'overflow-auto retro-scroll' : 'overflow-hidden'}
|
| 73 |
+
`}
|
| 74 |
+
>
|
| 75 |
+
{children}
|
| 76 |
+
</div>
|
| 77 |
+
|
| 78 |
+
{/* ── Status bar (optional) ──────────────────────────────── */}
|
| 79 |
+
{statusBar !== undefined && (
|
| 80 |
+
<div
|
| 81 |
+
className="
|
| 82 |
+
px-2 py-[2px]
|
| 83 |
+
text-retro-xs text-retro-black
|
| 84 |
+
border-t border-retro-darkgray
|
| 85 |
+
bg-retro-gray
|
| 86 |
+
shadow-retro-well
|
| 87 |
+
mx-[3px] mb-[3px]
|
| 88 |
+
truncate shrink-0
|
| 89 |
+
"
|
| 90 |
+
>
|
| 91 |
+
{statusBar}
|
| 92 |
+
</div>
|
| 93 |
+
)}
|
| 94 |
+
</div>
|
| 95 |
+
)
|
| 96 |
+
}
|
|
@@ -0,0 +1,9 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
export { default as RetroWindow } from './RetroWindow'
|
| 2 |
+
export { default as RetroButton } from './RetroButton'
|
| 3 |
+
export { default as RetroMenuBar } from './RetroMenuBar'
|
| 4 |
+
export { default as RetroIcon } from './RetroIcon'
|
| 5 |
+
export { default as RetroCheckbox } from './RetroCheckbox'
|
| 6 |
+
export { default as RetroInput } from './RetroInput'
|
| 7 |
+
export { default as RetroTextarea } from './RetroTextarea'
|
| 8 |
+
export { default as RetroSelect } from './RetroSelect'
|
| 9 |
+
export { default as RetroBadge } from './RetroBadge'
|
|
@@ -1,3 +1,126 @@
|
|
|
|
|
|
|
|
|
|
|
| 1 |
@tailwind base;
|
| 2 |
@tailwind components;
|
| 3 |
@tailwind utilities;
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
/* ── Google Font: IBM Plex Mono (must precede @tailwind) ───────────── */
|
| 2 |
+
@import url('https://fonts.googleapis.com/css2?family=IBM+Plex+Mono:ital,wght@0,400;0,500;0,600;0,700;1,400&display=swap');
|
| 3 |
+
|
| 4 |
@tailwind base;
|
| 5 |
@tailwind components;
|
| 6 |
@tailwind utilities;
|
| 7 |
+
|
| 8 |
+
/* ══════════════════════════════════════════════════════════════════════
|
| 9 |
+
IIIF Studio — Retro Design System (global styles)
|
| 10 |
+
Inspired by Xerox Star, Smalltalk-80, GEOS, classic Mac
|
| 11 |
+
══════════════════════════════════════════════════════════════════════ */
|
| 12 |
+
|
| 13 |
+
/* ── Base reset for retro look ─────────────────────────────────────── */
|
| 14 |
+
@layer base {
|
| 15 |
+
html {
|
| 16 |
+
font-family: 'IBM Plex Mono', 'Courier New', Courier, monospace;
|
| 17 |
+
font-size: 13px;
|
| 18 |
+
line-height: 1.5;
|
| 19 |
+
-webkit-font-smoothing: none;
|
| 20 |
+
-moz-osx-font-smoothing: unset;
|
| 21 |
+
background-color: #c0c0c0;
|
| 22 |
+
color: #000000;
|
| 23 |
+
}
|
| 24 |
+
|
| 25 |
+
body {
|
| 26 |
+
margin: 0;
|
| 27 |
+
padding: 0;
|
| 28 |
+
min-height: 100vh;
|
| 29 |
+
}
|
| 30 |
+
|
| 31 |
+
/* Disable subpixel antialiasing for that crisp pixel look */
|
| 32 |
+
* {
|
| 33 |
+
text-rendering: optimizeSpeed;
|
| 34 |
+
}
|
| 35 |
+
|
| 36 |
+
/* Focus ring: dotted black outline (classic retro) */
|
| 37 |
+
*:focus-visible {
|
| 38 |
+
outline: 1px dotted #000000;
|
| 39 |
+
outline-offset: 1px;
|
| 40 |
+
}
|
| 41 |
+
|
| 42 |
+
/* Selection: classic inverted */
|
| 43 |
+
::selection {
|
| 44 |
+
background-color: #000080;
|
| 45 |
+
color: #ffffff;
|
| 46 |
+
}
|
| 47 |
+
}
|
| 48 |
+
|
| 49 |
+
/* ── Retro scrollbar (Windows 3.1 style) ───────────────────────────── */
|
| 50 |
+
@layer components {
|
| 51 |
+
/* Webkit-based browsers */
|
| 52 |
+
.retro-scroll::-webkit-scrollbar {
|
| 53 |
+
width: 16px;
|
| 54 |
+
height: 16px;
|
| 55 |
+
}
|
| 56 |
+
|
| 57 |
+
.retro-scroll::-webkit-scrollbar-track {
|
| 58 |
+
background:
|
| 59 |
+
repeating-conic-gradient(
|
| 60 |
+
#c0c0c0 0% 25%, #dfdfdf 0% 50%
|
| 61 |
+
) 50% / 2px 2px;
|
| 62 |
+
}
|
| 63 |
+
|
| 64 |
+
.retro-scroll::-webkit-scrollbar-thumb {
|
| 65 |
+
background-color: #c0c0c0;
|
| 66 |
+
border: 1px solid #000000;
|
| 67 |
+
box-shadow:
|
| 68 |
+
inset -1px -1px 0 #808080,
|
| 69 |
+
inset 1px 1px 0 #ffffff;
|
| 70 |
+
}
|
| 71 |
+
|
| 72 |
+
.retro-scroll::-webkit-scrollbar-button {
|
| 73 |
+
background-color: #c0c0c0;
|
| 74 |
+
border: 1px solid #000000;
|
| 75 |
+
box-shadow:
|
| 76 |
+
inset -1px -1px 0 #808080,
|
| 77 |
+
inset 1px 1px 0 #ffffff;
|
| 78 |
+
display: block;
|
| 79 |
+
height: 16px;
|
| 80 |
+
width: 16px;
|
| 81 |
+
}
|
| 82 |
+
|
| 83 |
+
.retro-scroll::-webkit-scrollbar-corner {
|
| 84 |
+
background-color: #c0c0c0;
|
| 85 |
+
}
|
| 86 |
+
|
| 87 |
+
/* Firefox */
|
| 88 |
+
.retro-scroll {
|
| 89 |
+
scrollbar-width: auto;
|
| 90 |
+
scrollbar-color: #c0c0c0 #dfdfdf;
|
| 91 |
+
}
|
| 92 |
+
}
|
| 93 |
+
|
| 94 |
+
/* ── Desktop dithered background ───────────────────────────────────── */
|
| 95 |
+
@layer utilities {
|
| 96 |
+
.bg-retro-dither {
|
| 97 |
+
background-color: #c0c0c0;
|
| 98 |
+
background-image: url("data:image/svg+xml,%3Csvg width='4' height='4' viewBox='0 0 4 4' xmlns='http://www.w3.org/2000/svg'%3E%3Crect x='0' y='0' width='1' height='1' fill='%23a0a0a0'/%3E%3Crect x='2' y='2' width='1' height='1' fill='%23a0a0a0'/%3E%3C/svg%3E");
|
| 99 |
+
background-repeat: repeat;
|
| 100 |
+
}
|
| 101 |
+
|
| 102 |
+
.bg-retro-dither-dark {
|
| 103 |
+
background-color: #808080;
|
| 104 |
+
background-image: url("data:image/svg+xml,%3Csvg width='4' height='4' viewBox='0 0 4 4' xmlns='http://www.w3.org/2000/svg'%3E%3Crect x='0' y='0' width='1' height='1' fill='%23606060'/%3E%3Crect x='2' y='2' width='1' height='1' fill='%23606060'/%3E%3C/svg%3E");
|
| 105 |
+
background-repeat: repeat;
|
| 106 |
+
}
|
| 107 |
+
|
| 108 |
+
.bg-retro-dither-light {
|
| 109 |
+
background-color: #dfdfdf;
|
| 110 |
+
background-image: url("data:image/svg+xml,%3Csvg width='4' height='4' viewBox='0 0 4 4' xmlns='http://www.w3.org/2000/svg'%3E%3Crect x='0' y='0' width='1' height='1' fill='%23c8c8c8'/%3E%3Crect x='2' y='2' width='1' height='1' fill='%23c8c8c8'/%3E%3C/svg%3E");
|
| 111 |
+
background-repeat: repeat;
|
| 112 |
+
}
|
| 113 |
+
|
| 114 |
+
/* Horizontal lines pattern */
|
| 115 |
+
.bg-retro-lines {
|
| 116 |
+
background-color: #c0c0c0;
|
| 117 |
+
background-image: repeating-linear-gradient(
|
| 118 |
+
0deg,
|
| 119 |
+
transparent,
|
| 120 |
+
transparent 1px,
|
| 121 |
+
#a0a0a0 1px,
|
| 122 |
+
#a0a0a0 2px
|
| 123 |
+
);
|
| 124 |
+
background-size: 100% 4px;
|
| 125 |
+
}
|
| 126 |
+
}
|
|
@@ -0,0 +1,198 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 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 |
+
}
|
|
@@ -2,7 +2,53 @@
|
|
| 2 |
export default {
|
| 3 |
content: ['./index.html', './src/**/*.{js,ts,jsx,tsx}'],
|
| 4 |
theme: {
|
| 5 |
-
extend: {
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 6 |
},
|
| 7 |
plugins: [],
|
| 8 |
}
|
|
|
|
| 2 |
export default {
|
| 3 |
content: ['./index.html', './src/**/*.{js,ts,jsx,tsx}'],
|
| 4 |
theme: {
|
| 5 |
+
extend: {
|
| 6 |
+
/* ── Retro palette ─────────────────────────────────────────── */
|
| 7 |
+
colors: {
|
| 8 |
+
retro: {
|
| 9 |
+
white: '#ffffff',
|
| 10 |
+
light: '#dfdfdf',
|
| 11 |
+
gray: '#c0c0c0',
|
| 12 |
+
darkgray: '#808080',
|
| 13 |
+
black: '#000000',
|
| 14 |
+
/* Accent for selected / active items */
|
| 15 |
+
select: '#000080',
|
| 16 |
+
'select-text': '#ffffff',
|
| 17 |
+
},
|
| 18 |
+
},
|
| 19 |
+
|
| 20 |
+
/* ── Retro fonts ───────────────────────────────────────────── */
|
| 21 |
+
fontFamily: {
|
| 22 |
+
retro: ['"IBM Plex Mono"', '"Courier New"', 'Courier', 'monospace'],
|
| 23 |
+
mono: ['"IBM Plex Mono"', '"Courier New"', 'Courier', 'monospace'],
|
| 24 |
+
},
|
| 25 |
+
|
| 26 |
+
/* ── Hard drop shadows (no blur) ───────────────────────────── */
|
| 27 |
+
boxShadow: {
|
| 28 |
+
'retro': '2px 2px 0px 0px #000000',
|
| 29 |
+
'retro-lg': '3px 3px 0px 0px #000000',
|
| 30 |
+
/* Outset bevel (button at rest) */
|
| 31 |
+
'retro-outset': 'inset -1px -1px 0 #808080, inset 1px 1px 0 #ffffff',
|
| 32 |
+
/* Inset bevel (button pressed) */
|
| 33 |
+
'retro-inset': 'inset 1px 1px 0 #808080, inset -1px -1px 0 #ffffff',
|
| 34 |
+
/* Window inner area */
|
| 35 |
+
'retro-well': 'inset 1px 1px 0 #808080, inset -1px -1px 0 #dfdfdf',
|
| 36 |
+
},
|
| 37 |
+
|
| 38 |
+
/* ── Spacing / sizing tokens ───────────────────────────────── */
|
| 39 |
+
borderWidth: {
|
| 40 |
+
'retro': '2px',
|
| 41 |
+
},
|
| 42 |
+
|
| 43 |
+
fontSize: {
|
| 44 |
+
'retro-xs': ['11px', { lineHeight: '16px' }],
|
| 45 |
+
'retro-sm': ['12px', { lineHeight: '18px' }],
|
| 46 |
+
'retro-base': ['13px', { lineHeight: '20px' }],
|
| 47 |
+
'retro-lg': ['15px', { lineHeight: '22px' }],
|
| 48 |
+
'retro-xl': ['18px', { lineHeight: '26px' }],
|
| 49 |
+
'retro-2xl': ['22px', { lineHeight: '30px' }],
|
| 50 |
+
},
|
| 51 |
+
},
|
| 52 |
},
|
| 53 |
plugins: [],
|
| 54 |
}
|