From d3cf0c6151be8951530d2ab4626ba8288ab2c2aa Mon Sep 17 00:00:00 2001 From: Eric Wagoner Date: Sun, 2 Aug 2026 17:21:14 -0400 Subject: [PATCH] Web UI: navigation, Juniper-derived design system, accessibility floor MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit One shared stylesheet (static/app.css) replaces the two ad-hoc style blocks, with tokens drawn from the mascot drawing: sky background, cream game-board cards inside confident outlines with flat offset shadows, hair-purple for brand and actions, shirt-green for go, bow-tie orange for danger, pipe-fitting gold for trim, jeans navy for chrome — and the rainbow game path as a stripe under the header, the one loud element. Accent colors split into object and *-ink variants so text on light surfaces holds AA contrast. Navigation: both pages share a header with brand-home link, a Primary nav with aria-current, and tally counts that link to the lists they count (dashboard tallies deep-link into the review catalog; review tallies jump to their sections). Accessibility: skip link, landmark nav, polite live region for stage activity, status role on banners, the dropzone is a real button, candidate rows are focusable and activate with Enter/Space, focus-visible ring throughout, reduced motion respected. Co-Authored-By: Claude Fable 5 --- CLAUDE.md | 2 +- src/bggpipe/static/app.css | 346 +++++++++++++++++++++++++++ src/bggpipe/templates/dashboard.html | 152 ++---------- src/bggpipe/templates/review.html | 269 +++------------------ src/bggpipe/webreview.py | 1 + tests/test_web_dashboard.py | 29 +++ 6 files changed, 432 insertions(+), 367 deletions(-) create mode 100644 src/bggpipe/static/app.css diff --git a/CLAUDE.md b/CLAUDE.md index 6e71a8e..16d5b25 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -27,7 +27,7 @@ Full design lives in `bgg-shelf-pipeline-spec.md` (read it before changing pipel ## Layout -- `src/bggpipe/` — `cli.py` (typer app), one module per stage (`extract`, `resolve`, `review` + `webreview`, `diff`, `upload`, `enrich`), plus `bgg_client.py` (rate-limited XML API2 client that caches responses to `data/bgg_cache/`), `jobs.py` (single-slot background stage runner for the web UI), `normalize.py` (title normalization), `models.py` (dataclasses), `config.py`, `fsio.py` (atomic writes), `init_wizard.py` (first-run setup). +- `src/bggpipe/` — `cli.py` (typer app), one module per stage (`extract`, `resolve`, `review` + `webreview`, `diff`, `upload`, `enrich`); `templates/` + `static/app.css` are the web UI (one shared stylesheet is the design system — its tokens derive from the mascot art), plus `bgg_client.py` (rate-limited XML API2 client that caches responses to `data/bgg_cache/`), `jobs.py` (single-slot background stage runner for the web UI), `normalize.py` (title normalization), `models.py` (dataclasses), `config.py`, `fsio.py` (atomic writes), `init_wizard.py` (first-run setup). - `scripts/` — `write_stub_fixtures.py` / `write_photo_fixtures.py` generate synthetic fixtures; `record_fixtures.py` re-records real API responses once a token exists. - `tests/fixtures/bgg_cache/` — stub XML fixtures the offline tests run against. - `data/` — pipeline state (CSV/JSON artifacts are committed; caches are not — see Git). diff --git a/src/bggpipe/static/app.css b/src/bggpipe/static/app.css new file mode 100644 index 0000000..c241a87 --- /dev/null +++ b/src/bggpipe/static/app.css @@ -0,0 +1,346 @@ +/* bggpipe design system — one stylesheet, two pages (dashboard, review). + * + * The visual world is Juniper's mascot drawing: a sky-blue day, flat cel + * color inside confident dark outlines, a cream game board with a rainbow + * path, gold pipe fittings, purple hair, a spring-green shirt, an orange + * bow tie, navy jeans. Tokens map those to roles — purple is brand and + * action, green means go, bow-tie orange means danger, gold is trim, navy + * is ink and dark chrome. The one loud element is the rainbow path stripe + * under the header; everything else stays flat and outlined. + * + * Contrast rule: --accent/--go/--stop have *-ink variants for text on + * light surfaces; the base variants fill objects. + */ + +:root { + --sky: #cbe7f7; /* the drawing's daytime background */ + --sky-deep: #b3d9ef; + --board: #fbf5e6; /* cream game-board card */ + --board-edge: #e9dfc6; + --ink: #1f2433; /* outline navy-black */ + --ink-soft: #52607a; + --navy: #2c3a5c; /* jeans: dark chrome, header, log */ + --navy-deep: #232e49; + --accent: #8330c2; /* hair purple: brand + actions */ + --accent-ink: #6b27a0; + --go: #2f8a45; /* shirt green, darkened for white text */ + --go-ink: #256e37; + --stop: #d94a26; /* bow-tie orange-red */ + --stop-ink: #b03a1c; + --gold: #f2c04b; /* pipe fittings: rings, trim, hovers */ + --gold-ink: #8a6414; + --ticket: #fdf3d2; /* reshoot work-orders */ + --focus: #8330c2; + --path: linear-gradient(90deg, + #f767b8, #f79a3e, #f2c04b, #6fce6f, #5aa7f0, #9a5be0); + --font-display: ui-rounded, "Hiragino Maru Gothic ProN", "Arial Rounded MT Bold", var(--font-body); + --font-body: system-ui, -apple-system, "Segoe UI", sans-serif; + --font-mono: ui-monospace, "SF Mono", Menlo, monospace; + --radius: 8px; + --radius-lg: 12px; + --line: 2px solid var(--ink); + --shadow-card: 3px 3px 0 rgba(31, 36, 51, .18); + --shadow-raised: 5px 5px 0 rgba(31, 36, 51, .22); +} + +/* -- base ------------------------------------------------------------- */ +* { box-sizing: border-box; } +html { background: var(--sky); } +body { + margin: 0; + font-family: var(--font-body); + color: var(--ink); + background: + radial-gradient(ellipse at 50% -20%, rgba(255,255,255,.5), transparent 55%), + var(--sky); + min-height: 100vh; +} +main { max-width: 62rem; margin: 0 auto; padding: 1.4rem 1.2rem 6rem; } +h2 { + color: var(--ink); + font-family: var(--font-display); + font-weight: 700; font-size: 1.1rem; letter-spacing: .02em; + margin: 2rem 0 .8rem; +} +h2 .count { color: var(--ink-soft); font-size: .85rem; font-weight: 400; } +:focus-visible { outline: 3px solid var(--focus); outline-offset: 2px; } + +/* -- header + navigation ---------------------------------------------- */ +header { + position: sticky; top: 0; z-index: 5; + background: var(--navy); + color: #fff; + padding: .45rem 1.2rem; + display: flex; align-items: center; gap: 1.2rem; flex-wrap: wrap; + border-bottom: 4px solid transparent; + border-image: var(--path) 1; +} +.brand { display: flex; align-items: center; gap: .6rem; text-decoration: none; color: inherit; } +.brand img { + width: 38px; height: 38px; border-radius: 50%; + border: 2px solid var(--gold); object-fit: cover; display: block; + background: var(--sky); +} +.wordmark { + font-family: var(--font-display); + font-size: 1.3rem; font-weight: 700; letter-spacing: .01em; +} +.wordmark small { opacity: .8; font-family: var(--font-body); font-weight: 400; font-size: .75rem; margin-left: .5rem; } +nav[aria-label="Primary"] { display: flex; gap: .9rem; font-size: .9rem; } +nav[aria-label="Primary"] a { + color: #fff; text-decoration: none; opacity: .85; + padding: .1rem 0; border-bottom: 2px solid transparent; +} +nav[aria-label="Primary"] a:hover { opacity: 1; } +nav[aria-label="Primary"] a[aria-current="page"] { + opacity: 1; border-bottom-color: var(--gold); font-weight: 600; +} +.skip { + position: absolute; left: -999px; top: 0; z-index: 10; + background: var(--board); color: var(--ink); + padding: .5rem 1rem; border-radius: 0 0 var(--radius) 0; +} +.skip:focus { left: 0; } +#tally { font-size: .85rem; opacity: .95; display: flex; gap: 1rem; flex-wrap: wrap; } +#tally b { color: var(--gold); font-weight: 700; } +#tally a { color: inherit; text-decoration: none; border-bottom: 1px dotted rgba(255,255,255,.5); } +#tally a:hover { border-bottom-color: var(--gold); } +.keyhelp { margin-left: auto; font-size: .75rem; opacity: .85; } +kbd { + font-family: var(--font-mono); + font-size: .72rem; + background: var(--board); + color: var(--ink); + border: 1px solid var(--board-edge); + border-bottom-width: 2px; + border-radius: 4px; + padding: 0 .35em; + display: inline-block; min-width: 1.4em; text-align: center; +} +header kbd { background: rgba(255,255,255,.16); color: #fff; border-color: rgba(255,255,255,.3); } + +/* -- banners ----------------------------------------------------------- */ +#banner { max-width: 62rem; margin: 0 auto; padding: 0 1.2rem; } +.banner { + border-radius: var(--radius); padding: .6rem .9rem; margin-top: .9rem; + font-size: .85rem; line-height: 1.4; + border: var(--line); + background: var(--board); +} +.banner.error { border-color: var(--stop); color: var(--stop-ink); background: #fbe3da; } +.banner.warn { border-color: var(--gold-ink); color: var(--gold-ink); background: var(--ticket); } + +/* -- buttons ----------------------------------------------------------- */ +button, .linkbtn { + font: inherit; font-size: .85rem; + border: var(--line); background: #fff; color: var(--ink); + border-radius: var(--radius); padding: .3rem .8rem; cursor: pointer; + text-decoration: none; display: inline-block; + box-shadow: 2px 2px 0 rgba(31, 36, 51, .18); +} +button:hover, .linkbtn:hover { background: var(--board); } +button:active, .linkbtn:active { transform: translate(1px, 1px); box-shadow: 1px 1px 0 rgba(31,36,51,.18); } +button:disabled { opacity: .45; cursor: default; box-shadow: none; } +button.primary { background: var(--go); border-color: var(--ink); color: #fff; } +button.primary:hover { background: var(--go-ink); } +button.danger { color: var(--stop-ink); border-color: var(--stop); background: #fff; } + +/* -- decision cards (review) ------------------------------------------ */ +.card { + background: var(--board); + border: var(--line); + border-radius: var(--radius-lg); + box-shadow: var(--shadow-card); + padding: 1rem 1.1rem; + margin-bottom: 1rem; + display: flex; gap: 1.1rem; + scroll-margin-top: 5rem; +} +.card.active { + border-color: var(--accent); + box-shadow: var(--shadow-raised); +} +@media (prefers-reduced-motion: no-preference) { + .card { transition: box-shadow .15s ease, border-color .15s ease; } + button, .linkbtn { transition: transform .06s ease, box-shadow .06s ease; } +} +.shots { flex: 0 0 180px; display: flex; flex-direction: column; gap: .5rem; } +.shots img { width: 100%; border-radius: 6px; border: var(--line); display: block; } +.shots .noshot { + color: var(--ink-soft); font-size: .8rem; border: 2px dashed var(--board-edge); + border-radius: 6px; padding: 1.2rem .6rem; text-align: center; +} +.body { flex: 1; min-width: 0; } +.title { font-size: 1.15rem; font-weight: 700; font-family: var(--font-display); margin: 0 0 .15rem; } +.status { font-size: .72rem; text-transform: uppercase; letter-spacing: .08em; color: var(--ink-soft); } +.cues { margin: .5rem 0 .7rem; display: flex; flex-wrap: wrap; gap: .35rem; } +.cue { + font-size: .74rem; background: #fff; border: 1px solid var(--board-edge); + border-radius: 999px; padding: .1rem .6rem; color: var(--ink); +} +.cue b { font-weight: 600; color: var(--ink-soft); } +.cands { list-style: none; margin: 0; padding: 0; } +.cands li { + display: flex; align-items: center; gap: .6rem; + padding: .4rem .5rem; border-radius: var(--radius); cursor: pointer; + border: 2px solid transparent; +} +.cands li:hover, .cands li:focus-visible { background: #fff; border-color: var(--accent); } +.thumb { width: 42px; height: 42px; border-radius: 6px; object-fit: cover; border: 1px solid var(--board-edge); flex: 0 0 42px; } +.thumb.ph { + display: flex; align-items: center; justify-content: center; + background: var(--accent); color: #fff; + font-family: var(--font-display); font-size: 1.2rem; +} +.cname { font-weight: 600; } +.cmeta { color: var(--ink-soft); font-size: .8rem; margin-left: .4rem; } +.rowactions { margin-top: .7rem; display: flex; gap: 1rem; align-items: center; flex-wrap: wrap; font-size: .85rem; } +.rowactions button { + font: inherit; border: var(--line); background: #fff; + border-radius: var(--radius); padding: .25rem .7rem; cursor: pointer; +} +.rowactions button.reject { color: var(--stop-ink); border-color: var(--stop); } +.rowactions input[type=text] { + font: inherit; width: 8.5em; padding: .25rem .5rem; + border: 2px solid var(--board-edge); border-radius: var(--radius); +} + +/* -- reshoot work-orders: tickets, not cards --------------------------- */ +.ticket { + background: var(--ticket); + border: 2px dashed var(--gold-ink); + border-radius: var(--radius); + padding: .8rem 1rem; + margin-bottom: .8rem; + display: flex; gap: 1rem; align-items: flex-start; + scroll-margin-top: 5rem; +} +.ticket.active { border-style: solid; box-shadow: var(--shadow-raised); } +.ticket .stencil { + writing-mode: vertical-rl; text-orientation: mixed; + font-family: var(--font-mono); + font-size: .7rem; letter-spacing: .35em; font-weight: 700; + color: var(--gold-ink); text-transform: uppercase; + border-right: 1px solid var(--gold-ink); padding-right: .5rem; +} +.ticket img { width: 130px; border-radius: 4px; border: 1px solid var(--gold-ink); } +.ticket .loc { font-weight: 600; margin-bottom: .25rem; } +.ticket .partial { font-family: var(--font-mono); font-size: .85rem; } +.ticket .notes { color: var(--gold-ink); font-size: .85rem; margin-top: .2rem; } +.ticket button { + font: inherit; font-size: .8rem; margin-top: .5rem; + background: none; border: 1px solid var(--gold-ink); color: var(--gold-ink); + border-radius: var(--radius); padding: .2rem .6rem; cursor: pointer; + box-shadow: none; +} + +/* -- all-done celebration ---------------------------------------------- */ +.done { + background: var(--board); border: var(--line); border-radius: var(--radius-lg); + padding: 1.6rem; text-align: center; box-shadow: var(--shadow-raised); +} +.done .piper { + height: 165px; margin-bottom: .4rem; + border-radius: var(--radius-lg); border: var(--line); + background: var(--sky); +} +.done h2 { color: var(--ink); margin-top: 0; } +.done .nums { display: flex; justify-content: center; gap: 2rem; margin: 1rem 0; } +.done .nums div { font-size: 1.6rem; font-weight: 700; font-family: var(--font-display); } +.done .nums span { display: block; font-size: .72rem; text-transform: uppercase; letter-spacing: .08em; color: var(--ink-soft); } +.done code { + font-family: var(--font-mono); + background: var(--navy); color: #fff; + padding: .3rem .8rem; border-radius: var(--radius); + display: inline-block; white-space: nowrap; +} +.done .waiting { + color: var(--gold-ink); + max-width: 34rem; margin: 1rem auto .2rem; line-height: 1.5; +} +.done .next { margin: .8rem 0 .2rem; } + +/* -- catalog: read-only status ledger ---------------------------------- */ +.catalog { background: var(--board); border: var(--line); border-radius: var(--radius-lg); padding: .4rem 1rem; box-shadow: var(--shadow-card); } +.catalog table { width: 100%; border-collapse: collapse; font-size: .85rem; } +.catalog td { padding: .38rem .5rem; border-top: 1px solid var(--board-edge); vertical-align: top; } +.catalog tr:first-child td { border-top: none; } +.catalog tr:hover td { background: #fff; } +.catalog .t { font-weight: 600; max-width: 22rem; } +.catalog .meta { color: var(--ink-soft); } +.catalog a { + color: inherit; + text-decoration: underline dotted; + text-underline-offset: 2px; +} +.catalog a:hover { color: var(--accent-ink); } +.chip { + font-size: .68rem; text-transform: uppercase; letter-spacing: .06em; + border-radius: 999px; padding: .1rem .55rem; white-space: nowrap; + border: 1px solid currentColor; +} +.chip.ok { background: #e2f2e4; color: var(--go-ink); } +.chip.wait { background: var(--ticket); color: var(--gold-ink); } +.chip.no { background: #fbe3da; color: var(--stop-ink); } +.chip.open { background: #ece5f7; color: var(--accent-ink); } +.chip.merged { background: #e3ecf3; color: var(--navy); } + +/* -- merge notices: slim, undoable ------------------------------------- */ +.card.merge { padding: .55rem 1rem; align-items: center; } +.card.merge .body { display: flex; align-items: center; gap: .8rem; flex-wrap: wrap; } +.card.merge .arrow { color: var(--ink-soft); } +.card.merge button { + font: inherit; font-size: .8rem; margin-left: auto; + background: none; border: 1px solid var(--board-edge); + border-radius: var(--radius); padding: .2rem .6rem; cursor: pointer; + box-shadow: none; +} +.card.merge button:hover { border-color: var(--stop); color: var(--stop-ink); } + +/* -- dashboard: stage cards, dropzone, activity log --------------------- */ +.stages { display: grid; grid-template-columns: repeat(auto-fit, minmax(17rem, 1fr)); gap: .9rem; } +.stage { + background: var(--board); border: var(--line); border-radius: var(--radius-lg); + box-shadow: var(--shadow-card); + padding: .9rem 1rem; + display: flex; flex-direction: column; gap: .5rem; +} +.stage .top { display: flex; align-items: baseline; gap: .55rem; } +.stage .num { + font-family: var(--font-mono); + color: var(--accent-ink); font-size: .8rem; font-weight: 700; +} +.stage .name { font-weight: 700; font-family: var(--font-display); } +.stage .facts { color: var(--ink-soft); font-size: .84rem; line-height: 1.45; flex: 1; } +.stage .facts b { color: var(--ink); } +.stage .act { display: flex; gap: .5rem; align-items: center; flex-wrap: wrap; } +.stage input[type=number] { + font: inherit; width: 4.5em; padding: .25rem .4rem; + border: 2px solid var(--board-edge); border-radius: var(--radius); +} +#dropzone { + display: block; width: 100%; + background: var(--ticket); + border: 2px dashed var(--gold-ink); + border-radius: var(--radius); + padding: 1.4rem; + text-align: center; color: var(--gold-ink); + font: inherit; cursor: pointer; + box-shadow: none; +} +#dropzone.hot, #dropzone:hover { border-style: solid; background: #fdeebb; } +#joblog { + background: var(--navy); color: #eaf1fb; + font-family: var(--font-mono); font-size: .78rem; + border: var(--line); border-radius: var(--radius-lg); + padding: .8rem 1rem; + max-height: 20rem; overflow-y: auto; white-space: pre-wrap; +} +#jobstate { font-size: .85rem; color: var(--ink); margin-bottom: .4rem; } +#jobstate .running { color: var(--accent-ink); font-weight: 600; } +#jobstate .failed { color: var(--stop-ink); font-weight: 600; } + +@media (max-width: 700px) { + .card, .ticket { flex-direction: column; } + .shots { flex-basis: auto; } +} diff --git a/src/bggpipe/templates/dashboard.html b/src/bggpipe/templates/dashboard.html index 0571dda..eb36408 100644 --- a/src/bggpipe/templates/dashboard.html +++ b/src/bggpipe/templates/dashboard.html @@ -3,141 +3,36 @@ -bggpipe +bggpipe — dashboard - + +
- + +
-
-

Photos

-
drop shelf photos here, or click to choose - -
+
+

Photos

+ + -

Pipeline

+

Pipeline

-

Activity

-
idle
-
(stage output appears here)
+

Activity

+
idle
+
(stage output appears here)