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