/art v1: single featured piece, framed (look-overhaul testbed)

A calm "walk up to one piece on a quiet wall" page for the Daily Art engine. Bright,
modern, cream canvas (not a dim brown gallery) so the artwork's own colors are the pop
— the new look prototyped, scoped to /art so it doesn't touch the rest of the site yet.

- Fetches /api/art/today; large soft-shadowed frame (click -> lightbox), a museum
  "placard" (serif title, artist · date, medium, "from The Met · Public Domain (CC0)",
  View-at-museum link). Calm loading/empty states. Unlinked from the homepage (safe to
  iterate). Ships the (Codex-cleared) art backend too.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
jay
2026-06-21 15:48:58 -04:00
parent db967bb7fa
commit 9bfec573e2
+158
View File
@@ -0,0 +1,158 @@
<script>
import { onMount } from 'svelte';
import { getJSON } from '$lib/api.js';
let art = $state(null);
let state = $state('loading'); // loading | ready | empty
let zoom = $state(false);
onMount(async () => {
try {
art = await getJSON('/api/art/today');
state = art?.image_url ? 'ready' : 'empty';
} catch {
state = 'empty';
}
});
</script>
<svelte:head>
<title>Daily Art · upbeatBytes</title>
<meta name="description" content="A masterwork a day from the world's open museum collections — one piece, beautifully framed, on upbeatBytes." />
</svelte:head>
<div class="room">
<header class="bar">
<a class="brand" href="/">upbeat<span>Bytes</span></a>
<nav class="nav">
<a href="/">News</a>
<a href="/play">Games</a>
<a href="/art" aria-current="page">Art</a>
<span class="pill">No ads · No paywalls</span>
</nav>
</header>
<main class="gallery">
<div class="intro">
<h1>Daily Art</h1>
<p>A masterwork a day, from the world's open collections.</p>
</div>
{#if state === 'ready'}
<figure class="piece">
<button class="frame" onclick={() => (zoom = true)} aria-label="View larger">
<img src={art.image_url} alt={art.title} />
</button>
<figcaption class="placard">
<h2 class="title">{art.title}</h2>
<p class="who">{art.artist || 'Unknown artist'}{#if art.date_text} · {art.date_text}{/if}</p>
{#if art.medium}<p class="medium">{art.medium}</p>{/if}
<p class="credit">
from {art.museum}{#if art.license} · {art.license}{/if}
{#if art.source_url}<a class="more" href={art.source_url} target="_blank" rel="noopener">View at {art.museum}</a>{/if}
</p>
</figcaption>
</figure>
{:else if state === 'empty'}
<p class="note">The gallery's resting — a new piece is hung each morning. Check back soon.</p>
{:else}
<p class="note">Hanging today's piece…</p>
{/if}
</main>
<footer class="foot">upbeatBytes — no ads, no paywalls, no doomscrolling.</footer>
</div>
{#if zoom && art}
<button class="lightbox" onclick={() => (zoom = false)} aria-label="Close">
<img src={art.image_url} alt={art.title} />
<span class="lb-cap">{art.title} · {art.artist || ''}</span>
</button>
{/if}
<style>
/* --- look-overhaul testbed: bright, modern, calm. Scoped to /art for now. --- */
.room {
--canvas: #faf6ee; /* warm cream, not brown */
--surface: #ffffff;
--ink: #232a31; /* dark slate */
--muted: #707b86;
--line: #ece5d8;
--accent: #0a93c0; /* upbeatBytes blue, a touch brighter */
--accent-deep: #066c8e;
min-height: 100vh;
background: var(--canvas);
color: var(--ink);
font-family: ui-sans-serif, system-ui, -apple-system, "Segoe UI", Roboto, sans-serif;
display: flex;
flex-direction: column;
}
.bar {
display: flex; align-items: center; justify-content: space-between;
padding: 18px clamp(16px, 5vw, 56px);
max-width: 1100px; width: 100%; margin: 0 auto; box-sizing: border-box;
}
.brand { font-weight: 800; font-size: 1.25rem; letter-spacing: -0.02em; color: var(--ink); text-decoration: none; }
.brand span { color: var(--accent); }
.nav { display: flex; align-items: center; gap: clamp(12px, 2.5vw, 26px); }
.nav a { color: var(--muted); text-decoration: none; font-weight: 600; font-size: 0.95rem; }
.nav a:hover { color: var(--ink); }
.nav a[aria-current="page"] { color: var(--accent); }
.pill {
font-size: 0.76rem; font-weight: 600; color: var(--accent-deep);
background: #e7f4f9; border: 1px solid #cdeaf3; border-radius: 999px; padding: 5px 12px;
}
.gallery {
flex: 1; width: 100%; max-width: 1100px; margin: 0 auto;
padding: clamp(20px, 5vw, 56px); box-sizing: border-box;
display: flex; flex-direction: column; align-items: center;
}
.intro { text-align: center; margin-bottom: clamp(24px, 5vw, 48px); }
.intro h1 {
font-family: Georgia, "Iowan Old Style", "Times New Roman", serif;
font-size: clamp(2rem, 5vw, 3rem); margin: 0; letter-spacing: -0.01em;
}
.intro p { color: var(--muted); margin: 10px 0 0; font-size: 1.05rem; }
.piece { margin: 0; display: flex; flex-direction: column; align-items: center; max-width: 820px; }
.frame {
border: none; padding: 0; background: var(--surface); cursor: zoom-in;
border-radius: 18px; overflow: hidden; line-height: 0;
box-shadow: 0 18px 50px rgba(20, 30, 45, 0.14), 0 2px 8px rgba(20, 30, 45, 0.06);
transition: transform 0.25s ease, box-shadow 0.25s ease;
}
.frame:hover { transform: translateY(-3px); box-shadow: 0 26px 64px rgba(20, 30, 45, 0.18); }
.frame img { display: block; width: 100%; height: auto; max-height: 74vh; object-fit: contain; }
.placard { text-align: center; margin-top: clamp(20px, 4vw, 34px); max-width: 640px; }
.title {
font-family: Georgia, "Iowan Old Style", "Times New Roman", serif;
font-size: clamp(1.4rem, 3.5vw, 2rem); margin: 0; line-height: 1.2;
}
.who { margin: 8px 0 0; font-size: 1.05rem; color: var(--ink); }
.medium { margin: 4px 0 0; color: var(--muted); font-size: 0.95rem; font-style: italic; }
.credit { margin: 16px 0 0; color: var(--muted); font-size: 0.88rem; }
.more { display: inline-block; margin-left: 8px; color: var(--accent); font-weight: 600; text-decoration: none; }
.more:hover { color: var(--accent-deep); }
.note { color: var(--muted); font-size: 1.05rem; margin-top: 40px; }
.foot {
text-align: center; color: var(--muted); font-size: 0.84rem;
padding: 28px 16px 36px; border-top: 1px solid var(--line); margin-top: 24px;
}
.lightbox {
position: fixed; inset: 0; z-index: 50; border: none; cursor: zoom-out;
background: rgba(18, 22, 28, 0.92); display: flex; flex-direction: column;
align-items: center; justify-content: center; gap: 14px; padding: 4vmin;
}
.lightbox img { max-width: 96vw; max-height: 88vh; border-radius: 8px; box-shadow: 0 20px 60px rgba(0,0,0,0.5); }
.lb-cap { color: #e9e2d6; font-size: 0.9rem; }
@media (max-width: 560px) {
.pill { display: none; }
}
</style>