From 36cd711302f9e500c42bb6b3a1115627d4f35eda Mon Sep 17 00:00:00 2001 From: 3dtours Date: Mon, 28 Sep 2026 22:05:55 +0700 Subject: [PATCH] web: the header wears the theme, and the rail says which build it is MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Two things the studio could not say about itself. The preset the header has open was painted in the dim text colour, the same grey as the page name beside it — so the one label up there that changes with the look read as chrome. It now takes the theme's own accent, the colour the brand mark and the open tab already carry, which means it follows both the light/dark mode and the accent group the reader picked, out of the CSS token and with no colour written into the component. And an image has no other mark on it: once the frontend tarball is loaded on the NAS as `:latest`, nothing on the box says which revision came off. The rail now names the build at its foot — quiet, mono, wrapping rather than widening the column, and gone under 860px where the rail is the phone's scrolling tab bar instead of a column. The name is stamped into the bundle at build time: vite.config reads VERSION from the environment (`docker compose build frontend --build-arg VERSION=$(git rev-parse --short HEAD)`) and falls back to the package version plus the minute it was built, so two builds of the same tree are never the same name. `ARG VERSION=` in the Dockerfile is the knob; the bare `npm run build` — dev server, check scripts — still stamps its own time, and the dev server passes nothing at all, so the line only draws when there is something to say. Verified: library-check.mjs — 37 steps, all passed, the two new ones reading the header off the running studio: the rail foot names the build (0.1.0+202609281504) and the preset chip's computed colour is the brand's accent, not a grey — rgb(206, 117, 9) amber, rgb(93, 24, 191) after switching to violet. scan-nav-check.mjs and roll-walk-check.mjs — all passed. frontend tsc --noEmit clean. Live 8090 on index-… matching dist/: /, /library and /app 200 with 0 console errors. ponytail: the name is the package version plus a caller-supplied tag, and nothing bumps the package version, so the tag is the whole identity — have the release job write it into package.json if the numbers ever need to mean something. --- docker/frontend/Dockerfile | 6 ++++++ docker/frontend/scripts/library-check.mjs | 20 ++++++++++++++++++++ docker/frontend/src/styles/app.css | 20 +++++++++++++++++++- docker/frontend/src/ui/ToolRail.tsx | 10 ++++++++++ docker/frontend/vite.config.ts | 11 +++++++++++ 5 files changed, 66 insertions(+), 1 deletion(-) diff --git a/docker/frontend/Dockerfile b/docker/frontend/Dockerfile index ba19e00..165d6e4 100644 --- a/docker/frontend/Dockerfile +++ b/docker/frontend/Dockerfile @@ -4,6 +4,12 @@ WORKDIR /app COPY package.json package-lock.json ./ RUN npm ci COPY . . +# The build's own name, shown at the foot of the studio's tab rail (vite.config +# reads it): `docker compose build frontend --build-arg +# VERSION=$(git rev-parse --short HEAD)` puts the commit in the page, which is +# how the image on the NAS says which revision it is. Left unset, the build +# stamps its own time. +ARG VERSION= # tsc --noEmit && vite build; the prebuild hook copies canvaskit.wasm into public/ RUN npm run build diff --git a/docker/frontend/scripts/library-check.mjs b/docker/frontend/scripts/library-check.mjs index 819eaea..cacfbd3 100644 --- a/docker/frontend/scripts/library-check.mjs +++ b/docker/frontend/scripts/library-check.mjs @@ -477,6 +477,26 @@ step('studio developed the frame from its handle', true, 'stage no longer on the // own store and drops the query, so a reload reopens it like any other photo. step('the address was handed back', !page.url().includes('lib='), `url=${page.url()}`); +// Two things a reader and an operator read straight off the header: the preset +// the studio has open wears the theme's accent (the same colour the brand and +// the open tab carry), and the rail's foot names the build this page came from. +await page.waitForSelector('.rail-version', { timeout: 15_000 }); +const chrome = await page.evaluate(() => ({ + version: document.querySelector('.rail-version')?.textContent?.trim() ?? '', + recipe: getComputedStyle(document.querySelector('.recipe-name')).color, + accent: getComputedStyle(document.querySelector('.brand em')).color, +})); +step( + 'the rail foot names the build the page came from', + /^\d+\.\d+\.\d+\+\S+$/.test(chrome.version), + chrome.version +); +step( + 'the preset chip in the header wears the theme accent, not a fixed grey', + chrome.recipe === chrome.accent, + `${chrome.recipe}, brand accent ${chrome.accent}` +); + const editsIn = () => page.evaluate(async () => { const db = await new Promise((resolve, reject) => { diff --git a/docker/frontend/src/styles/app.css b/docker/frontend/src/styles/app.css index db33028..627fba8 100644 --- a/docker/frontend/src/styles/app.css +++ b/docker/frontend/src/styles/app.css @@ -20,10 +20,13 @@ carry, so every screen says where the visitor is. */ .page-name { font-size: 12px; letter-spacing: 0.06em; text-transform: uppercase; color: var(--text-dim); } .header .spacer { flex: 1 1 auto; } +/* The preset the studio has open. It wears the theme's own accent rather than + the dim text colour: it is the one label up there that changes with the + look, and the same colour the open tab and the brand already carry. */ .recipe-name { font-family: var(--mono); font-size: 12px; - color: var(--text-dim); + color: var(--accent); border: 1px solid var(--border-soft); border-radius: 999px; padding: 3px 10px; @@ -65,6 +68,18 @@ .rail button .glyph { font-size: 17px; line-height: 1; } .rail button[aria-pressed="true"] { background: var(--accent-soft); color: var(--accent); border-color: var(--accent-line); } .rail button:hover { color: var(--text); } +/* The build behind the page. It sits at the foot of the column, quiet enough to + read past, and wraps rather than widening the rail when a name is long. */ +.rail-version { + margin-top: auto; + padding: 8px 2px 2px; + font-family: var(--mono); + font-size: 9px; + letter-spacing: 0.02em; + color: var(--text-dim); + text-align: center; + overflow-wrap: anywhere; +} /* --- panel columns ------------------------------------------------------- */ /* The panel is a cascade: the rail holds the tabs, then one column per open @@ -1259,6 +1274,9 @@ input[type="range"] { width: 100%; accent-color: var(--accent); } scrollbar-width: none; } .rail::-webkit-scrollbar { display: none; } + /* The column is a row here, and the row is the tabs: a build name at its foot + would sit past the end of a line that already scrolls. It goes. */ + .rail-version { display: none; } .rail button { flex: 0 0 auto; flex-direction: row; diff --git a/docker/frontend/src/ui/ToolRail.tsx b/docker/frontend/src/ui/ToolRail.tsx index cad11c1..b3c9e9f 100644 --- a/docker/frontend/src/ui/ToolRail.tsx +++ b/docker/frontend/src/ui/ToolRail.tsx @@ -7,6 +7,10 @@ import { useI18n } from '../i18n/I18nProvider'; // (see styles/app.css). export type TabId = 'presets' | 'favorited' | 'photos' | 'wb' | 'light' | 'fx' | 'hsl' | 'frame' | 'save' | 'create'; +// The build this bundle came from, put in by vite at build time and named by +// whoever built the image (see vite.config.ts). Empty under a dev server. +const VERSION = import.meta.env.VITE_VERSION ?? ''; + export const TABS: { id: TabId; label: MsgKey; glyph: string }[] = [ { id: 'presets', label: 'tab.presets', glyph: '◉' }, { id: 'favorited', label: 'tab.favorited', glyph: '★' }, @@ -67,6 +71,12 @@ export function ToolRail({ {t(entry.label)} ))} + {/* The build this page came off, at the foot of the rail: an image on the + NAS carries no other mark, so the version is how an operator tells the + one that is up from the one they just built. Stamped at build time + (vite.config), empty in a dev server, so the line only draws when + there is something to say. */} + {VERSION ? {VERSION} : null} ); } diff --git a/docker/frontend/vite.config.ts b/docker/frontend/vite.config.ts index abbf1f8..7fd770f 100644 --- a/docker/frontend/vite.config.ts +++ b/docker/frontend/vite.config.ts @@ -1,6 +1,17 @@ import { defineConfig } from 'vite'; import react from '@vitejs/plugin-react'; import { fileURLToPath, URL } from 'node:url'; +import pkg from './package.json'; + +// The build's own name, handed to the bundle as an env var and printed at the +// foot of the studio's tab rail. The caller names it (`VERSION=$(git rev-parse +// --short HEAD) docker compose build`, see Dockerfile); left alone, the build +// stamps the minute it ran, so two builds are never the same name and an +// operator can tell which image is up. +const version = `${pkg.version}+${process.env.VERSION || new Date().toISOString().slice(0, 16).replace(/[-:T]/g, '')}`; +// Vite hands the VITE_ ones to the page, and it reads this very `process.env` +// when it builds `import.meta.env` — so the name is set here and read there. +process.env.VITE_VERSION = version; // The app is shared source with the React Native app: `frameUtils.ts` and // `skiaImage.ts` import `@shopify/react-native-skia`, and the alias below swaps