Polish museum path puzzle UX for homelab testing.

Add celebration overlay with Ted image, puzzle sounds, larger artifact thumbnails, and fix desktop artifact tray scrolling.
This commit is contained in:
2026-06-11 10:10:05 -04:00
parent 7c68aa3bd3
commit 89af525f22
8 changed files with 196 additions and 40 deletions
+1
View File
@@ -15,6 +15,7 @@ dist-ssr
# Local media copies (generated by scripts/setup-dev-media.sh)
public/media/unlock.mp4
public/media/artifact-room-map.jpg
public/media/ted-celebrate.png
public/media/artifacts/
public/media/celebration-pop.mp3
public/media/celebration-horn.mp3
+1
View File
@@ -1,6 +1,7 @@
{
"unlockVideoUrl": "/media/unlock.mp4",
"mapImageUrl": "/media/artifact-room-map.jpg",
"tedCelebrateImageUrl": "/media/ted-celebrate.png",
"incorrectUrl": "/media/incorrect.mp3",
"unlockConfirmUrl": "/media/unlock-confirm.mp3",
"celebrationPopUrl": "/media/celebration-pop.mp3",
+8
View File
@@ -26,6 +26,14 @@ cp "$INCORRECT_SOURCE" "$DEST_DIR/incorrect.mp3"
cp "$CONFIRM_SOURCE" "$DEST_DIR/unlock-confirm.mp3"
python3 "$ROOT/scripts/extract-artifact-room-map.py" "$PDF_SOURCE" "$DEST_DIR/artifact-room-map.jpg"
python3 "$ROOT/scripts/extract-artifact-images.py" "$PDF_SOURCE" "$DEST_DIR/artifacts"
TED_CELEBRATE_SOURCE="${TED_CELEBRATE_SOURCE:-$SOURCE_DIR/ted-celebrate.png}"
if [[ -f "$TED_CELEBRATE_SOURCE" ]]; then
cp "$TED_CELEBRATE_SOURCE" "$DEST_DIR/ted-celebrate.png"
elif [[ -f "$ROOT/public/media/ted-celebrate.png" ]]; then
cp "$ROOT/public/media/ted-celebrate.png" "$DEST_DIR/ted-celebrate.png"
else
echo "Warning: ted-celebrate.png not found; puzzle 2 celebration image will be missing" >&2
fi
cp "$ROOT/public/media/config.json.example" "$DEST_DIR/config.json"
echo "Copied media to $DEST_DIR/"
+9
View File
@@ -26,10 +26,19 @@ cp "$INCORRECT_SOURCE" "$DEST/incorrect.mp3"
cp "$CONFIRM_SOURCE" "$DEST/unlock-confirm.mp3"
python3 "$ROOT/scripts/extract-artifact-room-map.py" "$PDF_SOURCE" "$DEST/artifact-room-map.jpg"
python3 "$ROOT/scripts/extract-artifact-images.py" "$PDF_SOURCE" "$DEST/artifacts"
TED_CELEBRATE_SOURCE="${TED_CELEBRATE_SOURCE:-$SOURCE_DIR/ted-celebrate.png}"
if [[ -f "$TED_CELEBRATE_SOURCE" ]]; then
cp "$TED_CELEBRATE_SOURCE" "$DEST/ted-celebrate.png"
elif [[ -f "$ROOT/public/media/ted-celebrate.png" ]]; then
cp "$ROOT/public/media/ted-celebrate.png" "$DEST/ted-celebrate.png"
else
echo "Warning: ted-celebrate.png not found; puzzle 2 celebration image will be missing" >&2
fi
cat >"$DEST/config.json" <<'EOF'
{
"unlockVideoUrl": "/media/unlock.mp4",
"mapImageUrl": "/media/artifact-room-map.jpg",
"tedCelebrateImageUrl": "/media/ted-celebrate.png",
"incorrectUrl": "/media/incorrect.mp3",
"unlockConfirmUrl": "/media/unlock-confirm.mp3",
"celebrationPopUrl": "/media/celebration-pop.mp3",
+126 -23
View File
@@ -487,11 +487,16 @@
.artifact-bank__grid {
display: grid;
grid-template-columns: repeat(auto-fill, minmax(6.5rem, 1fr));
gap: 0.65rem;
grid-template-columns: repeat(auto-fill, minmax(8.75rem, 1fr));
gap: 0.75rem;
}
@media (max-width: 52rem) {
.artifact-bank__grid {
max-height: 28rem;
overflow-y: auto;
}
}
.path-slots {
display: flex;
@@ -503,8 +508,8 @@
display: flex;
align-items: center;
gap: 0.65rem;
min-height: 4.5rem;
padding: 0.5rem 0.65rem;
min-height: 5.5rem;
padding: 0.55rem 0.75rem;
background: linear-gradient(180deg, #2a2218 0%, #1f1a14 100%);
border: 2px solid #4a3f32;
border-radius: 0.5rem;
@@ -561,7 +566,7 @@
}
.artifact-card--overlay {
width: 6.5rem;
width: 8.75rem;
opacity: 0.95;
box-shadow: 0 8px 24px rgba(0, 0, 0, 0.5);
cursor: grabbing;
@@ -575,8 +580,8 @@
}
.artifact-card__image {
width: 3.25rem;
height: 3.25rem;
width: 5rem;
height: 5rem;
object-fit: contain;
flex-shrink: 0;
background: #fff;
@@ -584,12 +589,17 @@
}
.path-slot .artifact-card__image {
width: 2.75rem;
height: 2.75rem;
width: 4rem;
height: 4rem;
}
.artifact-card--overlay .artifact-card__image {
width: 5rem;
height: 5rem;
}
.artifact-card__name {
font-size: 0.78rem;
font-size: 0.85rem;
line-height: 1.25;
color: #f4e4bc;
text-align: center;
@@ -605,17 +615,110 @@
margin-top: 0.25rem;
}
.museum-path__success {
margin: 0.25rem 0 0;
padding: 0.85rem 1.25rem;
font-family: Cinzel, Georgia, serif;
font-size: 1.1rem;
font-weight: 600;
letter-spacing: 0.04em;
color: #e8c547;
text-align: center;
background: rgba(92, 68, 28, 0.5);
border: 1px solid rgba(201, 162, 39, 0.55);
border-radius: 0.5rem;
animation: error-pop 0.25s ease-out;
.museum-path-panel--fade-out {
opacity: 0;
transform: scale(0.98);
pointer-events: none;
transition:
opacity 0.65s ease-out,
transform 0.65s ease-out;
}
.museum-path-celebration {
position: fixed;
inset: 0;
z-index: 200;
display: flex;
align-items: center;
justify-content: center;
overflow: hidden;
}
.museum-path-celebration--fade-in {
opacity: 0;
animation: museum-celebration-in 0.9s ease-out 0.35s forwards;
}
@keyframes museum-celebration-in {
from {
opacity: 0;
}
to {
opacity: 1;
}
}
.museum-path-celebration__bg {
position: absolute;
inset: 0;
width: 100%;
height: 100%;
object-fit: cover;
object-position: center top;
}
.museum-path-celebration__scrim {
position: absolute;
inset: 0;
background: linear-gradient(
180deg,
rgba(26, 21, 16, 0.35) 0%,
rgba(26, 21, 16, 0.55) 45%,
rgba(26, 21, 16, 0.72) 100%
);
}
.museum-path-celebration__content {
position: relative;
z-index: 1;
max-width: 36rem;
margin: 0 1.5rem;
padding: 2rem 2.25rem;
text-align: center;
background: rgba(45, 36, 25, 0.72);
border: 3px solid #c9a227;
border-radius: 1rem;
box-shadow:
inset 0 0 0 1px rgba(232, 197, 71, 0.35),
0 16px 48px rgba(0, 0, 0, 0.5);
animation: museum-celebration-content-in 0.8s ease-out 0.7s both;
}
@keyframes museum-celebration-content-in {
from {
opacity: 0;
transform: translateY(1.25rem) scale(0.96);
}
to {
opacity: 1;
transform: translateY(0) scale(1);
}
}
.museum-path-celebration__eyebrow {
margin: 0 0 0.65rem;
font-family: Cinzel, Georgia, serif;
font-size: clamp(1rem, 2.5vw, 1.2rem);
font-weight: 600;
letter-spacing: 0.14em;
text-transform: uppercase;
color: #e8c547;
}
.museum-path-celebration__title {
margin: 0 0 0.85rem;
font-family: Cinzel, Georgia, serif;
font-size: clamp(2rem, 6vw, 3rem);
font-weight: 700;
line-height: 1.15;
letter-spacing: 0.04em;
color: #f4e4bc;
text-shadow: 0 2px 12px rgba(0, 0, 0, 0.45);
}
.museum-path-celebration__subtitle {
margin: 0;
font-size: clamp(1rem, 2.5vw, 1.15rem);
line-height: 1.45;
color: #c9b896;
}
+22
View File
@@ -0,0 +1,22 @@
interface MuseumPathCelebrationProps {
imageUrl: string;
}
export function MuseumPathCelebration({ imageUrl }: MuseumPathCelebrationProps) {
return (
<div className="museum-path-celebration museum-path-celebration--fade-in" role="status">
<img
className="museum-path-celebration__bg"
src={encodeURI(imageUrl)}
alt=""
aria-hidden="true"
/>
<div className="museum-path-celebration__scrim" aria-hidden="true" />
<div className="museum-path-celebration__content">
<p className="museum-path-celebration__eyebrow">You did it!</p>
<h1 className="museum-path-celebration__title">Ted escaped the museum!</h1>
<p className="museum-path-celebration__subtitle">You found the right path through the displays.</p>
</div>
</div>
);
}
+16 -8
View File
@@ -13,12 +13,15 @@ import {
import { CSS } from '@dnd-kit/utilities';
import confetti from 'canvas-confetti';
import { useCallback, useMemo, useState } from 'react';
import { playCelebrationSounds, playIncorrectSound } from './mediaSounds';
import { CORRECT_PATH, SLOT_COUNT } from './puzzle2Config';
import {
artifactImageUrl,
useArtifactManifest,
type ArtifactEntry,
} from './useArtifactManifest';
import { MuseumPathCelebration } from './MuseumPathCelebration';
import { useMediaConfig } from './useMediaConfig';
type DropZoneId = 'bank' | `slot-${number}`;
@@ -153,6 +156,7 @@ function PathSlot({ index, artifact }: { index: number; artifact: ArtifactEntry
}
export function MuseumPathPuzzle() {
const mediaConfig = useMediaConfig();
const manifest = useArtifactManifest();
const [slots, setSlots] = useState<(string | null)[]>(() => Array(SLOT_COUNT).fill(null));
const [activeSlug, setActiveSlug] = useState<string | null>(null);
@@ -214,12 +218,14 @@ export function MuseumPathPuzzle() {
if (phase !== 'playing') return;
if (!isPathCorrect(slots)) {
playIncorrectSound(mediaConfig.incorrectUrl);
triggerJiggle();
return;
}
setShowError(false);
setPhase('solved');
playCelebrationSounds(mediaConfig);
const confettiColors = ['#e8c547', '#c9a227', '#f4e4bc', '#d4a84b'];
const duration = 2000;
@@ -245,7 +251,7 @@ export function MuseumPathPuzzle() {
}
};
frame();
}, [phase, slots, triggerJiggle]);
}, [mediaConfig, phase, slots, triggerJiggle]);
if (!manifest) {
return (
@@ -261,7 +267,14 @@ export function MuseumPathPuzzle() {
return (
<div className="museum-path-scene">
<div className="sarcophagus-panel museum-path-panel">
{phase === 'solved' && (
<MuseumPathCelebration imageUrl={mediaConfig.tedCelebrateImageUrl} />
)}
<div
className={`sarcophagus-panel museum-path-panel ${phase === 'solved' ? 'museum-path-panel--fade-out' : ''}`}
aria-hidden={phase === 'solved'}
>
<header className="lock-header museum-path__header">
<h1>Museum Path</h1>
<p className="lock-subtitle">
@@ -296,19 +309,14 @@ export function MuseumPathPuzzle() {
</p>
)}
{phase === 'solved' ? (
<p className="museum-path__success" role="status">
You found the path! Ted escaped the museum!
</p>
) : (
<button
type="button"
className={`unlock-btn museum-path__check ${jiggle ? 'unlock-btn--jiggle' : ''}`}
onClick={handleCheckPath}
disabled={phase === 'solved'}
>
CHECK PATH
</button>
)}
</div>
</div>
);
+4
View File
@@ -1,6 +1,7 @@
export interface MediaConfig {
unlockVideoUrl: string;
mapImageUrl: string;
tedCelebrateImageUrl: string;
incorrectUrl: string;
unlockConfirmUrl: string;
celebrationPopUrl: string;
@@ -11,6 +12,7 @@ export interface MediaConfig {
export const DEFAULT_MEDIA_CONFIG: MediaConfig = {
unlockVideoUrl: '/media/unlock.mp4',
mapImageUrl: '/media/artifact-room-map.jpg',
tedCelebrateImageUrl: '/media/ted-celebrate.png',
incorrectUrl: '/media/incorrect.mp3',
unlockConfirmUrl: '/media/unlock-confirm.mp3',
celebrationPopUrl: '/media/celebration-pop.mp3',
@@ -23,6 +25,8 @@ export function mergeMediaConfig(partial: Partial<MediaConfig> | null): MediaCon
return {
unlockVideoUrl: partial.unlockVideoUrl ?? DEFAULT_MEDIA_CONFIG.unlockVideoUrl,
mapImageUrl: partial.mapImageUrl ?? DEFAULT_MEDIA_CONFIG.mapImageUrl,
tedCelebrateImageUrl:
partial.tedCelebrateImageUrl ?? DEFAULT_MEDIA_CONFIG.tedCelebrateImageUrl,
incorrectUrl: partial.incorrectUrl ?? DEFAULT_MEDIA_CONFIG.incorrectUrl,
unlockConfirmUrl: partial.unlockConfirmUrl ?? DEFAULT_MEDIA_CONFIG.unlockConfirmUrl,
celebrationPopUrl: partial.celebrationPopUrl ?? DEFAULT_MEDIA_CONFIG.celebrationPopUrl,