feat: plakat redesign phase 3 (windows on desktop, sheets on mobile) (#76)

- Tapping an object on the desk opens a window (desktop) or a bottom sheet
  (mobile) instead of flipping: projects, now, servers, reading, about, contact
- Desktop windows in poster style (2px ink rule, hard offset shadow), open
  from the tapped object, thrown by the title bar with momentum and elastic
  viewport edges, stack by focus, minimize into a tray, Escape closes the top
  window, focus moves to close and returns to the trigger
- Mobile sheets via vaul 1.1.2: drag down to dismiss, overlay, focus trap
- Project window shows live/source links from Directus when present
- Hero "Write me" opens the contact window; still a plain #contact link
  without JavaScript
- Shared current-book loader for desk, deck and reading window
- Texts in de/en, i18n key check extended

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
denshooter
2026-10-15 23:49:35 +02:00
committed by GitHub
parent 62f2fb7742
commit 0fa10315b2
19 changed files with 1413 additions and 113 deletions
+21 -19
View File
@@ -12,6 +12,8 @@ import {
type MotionValue,
} from "framer-motion";
import ObjectFace from "./ObjectFace";
import { useOpenWindow } from "../fenster/WindowProvider";
import { labelFor, windowFor } from "./windowFor";
import { BOARD_SIZE, BOARD_SPOTS, type TischLabels, type TischObject } from "./types";
interface Handle {
@@ -138,7 +140,6 @@ interface BoardItemProps {
function BoardItem({ obj, labels, spot, z, tableRef, onFront, register }: BoardItemProps) {
const reduce = useReducedMotion();
const [flipped, setFlipped] = useState(false);
const [lifted, setLifted] = useState(false);
// Framer meldet nach einem Wurf trotzdem einen Tap; der soll die Karte nicht umdrehen
const dragged = useRef(false);
@@ -151,12 +152,17 @@ function BoardItem({ obj, labels, spot, z, tableRef, onFront, register }: BoardI
const rotate = useTransform(() => base.get() + (reduce ? 0 : tilt.get()));
const { w, h } = BOARD_SIZE[obj.kind];
const canFlip = obj.kind === "project";
const openWindow = useOpenWindow();
const elRef = useRef<HTMLDivElement | null>(null);
const openIt = () => openWindow?.(windowFor(obj), elRef.current);
const radius = { sticker: "rounded-full", photo: "rounded-md", book: "rounded-[6px_14px_14px_6px]" }[obj.kind as string] ?? "rounded-2xl";
return (
<motion.div
ref={(el) => register(el ? { el, x, y, base, spotRotation: spot.r } : null)}
ref={(el) => {
elRef.current = el;
register(el ? { el, x, y, base, spotRotation: spot.r } : null);
}}
drag
dragConstraints={tableRef}
dragElastic={0.18}
@@ -171,21 +177,17 @@ function BoardItem({ obj, labels, spot, z, tableRef, onFront, register }: BoardI
setLifted(true);
}}
onDragEnd={() => setLifted(false)}
onTap={canFlip ? () => !dragged.current && setFlipped((f) => !f) : undefined}
onKeyDown={
canFlip
? (e) => {
if (e.key === " ") {
e.preventDefault();
setFlipped((f) => !f);
}
}
: undefined
}
tabIndex={canFlip ? 0 : undefined}
role={canFlip ? "button" : undefined}
aria-pressed={canFlip ? flipped : undefined}
aria-label={canFlip && obj.kind === "project" ? obj.title : undefined}
onTap={() => !dragged.current && openIt()}
onKeyDown={(e) => {
if (e.key === " ") {
e.preventDefault();
openIt();
}
}}
tabIndex={0}
role="button"
aria-haspopup="dialog"
aria-label={labelFor(obj, labels)}
className={`absolute cursor-grab touch-none select-none active:cursor-grabbing ${radius} transition-shadow duration-200 ease-out ${
lifted ? "shadow-[14px_16px_0_rgb(var(--plakat-ink))]" : "shadow-[6px_6px_0_rgb(var(--plakat-ink))]"
}`}
@@ -200,7 +202,7 @@ function BoardItem({ obj, labels, spot, z, tableRef, onFront, register }: BoardI
zIndex: z,
}}
>
<ObjectFace obj={obj} labels={labels} flipped={flipped} />
<ObjectFace obj={obj} labels={labels} />
</motion.div>
);
}