diff --git a/.build/Dockerfile b/.build/Dockerfile index 1806c4c..2b83f38 100644 --- a/.build/Dockerfile +++ b/.build/Dockerfile @@ -10,6 +10,7 @@ ENV VITE_MAP_ZOOM=__VITE_MAP_ZOOM__ ENV VITE_DISABLED_TABS=__VITE_DISABLED_TABS__ ENV VITE_ENABLED_THEMES=__VITE_ENABLED_THEMES__ ENV VITE_APP_NAME=__VITE_APP_NAME__ +ENV VITE_SKIP_SPLASH=__VITE_SKIP_SPLASH__ RUN npm run build FROM caddy:2-alpine diff --git a/.build/docker-entrypoint.sh b/.build/docker-entrypoint.sh index 4cf2325..a08b8e5 100644 --- a/.build/docker-entrypoint.sh +++ b/.build/docker-entrypoint.sh @@ -9,6 +9,7 @@ MAP_ZOOM="${VITE_MAP_ZOOM:-}" # Optional branding/customization — empty means default behaviour (all tabs, hidden themes hidden). DISABLED_TABS="${VITE_DISABLED_TABS:-}" ENABLED_THEMES="${VITE_ENABLED_THEMES:-}" +SKIP_SPLASH="${VITE_SKIP_SPLASH:-}" # APP_NAME is substituted verbatim; avoid '&' and '|' (sed replacement metachar and delimiter). APP_NAME="${VITE_APP_NAME:-BEACON}" @@ -20,6 +21,7 @@ find /srv -name '*.js' -exec sed -i \ -e "s|__VITE_DISABLED_TABS__|${DISABLED_TABS}|g" \ -e "s|__VITE_ENABLED_THEMES__|${ENABLED_THEMES}|g" \ -e "s|__VITE_APP_NAME__|${APP_NAME}|g" \ + -e "s|__VITE_SKIP_SPLASH__|${SKIP_SPLASH}|g" \ {} + exec "$@" diff --git a/.env.example b/.env.example index 69075ed..91b7213 100644 --- a/.env.example +++ b/.env.example @@ -22,3 +22,6 @@ # Custom top-left app name (default: BEACON). Avoid the & and | characters. # VITE_APP_NAME=Beacon - MeshMapper + +# Skip the once-per-session load splash. Unset = shown. +# VITE_SKIP_SPLASH=true diff --git a/docker/.env.example b/docker/.env.example index 32f0f0a..1ae20a3 100644 --- a/docker/.env.example +++ b/docker/.env.example @@ -22,3 +22,6 @@ VITE_ENABLED_THEMES= # Custom app name shown in the top-left wordmark. Default: BEACON. Avoid the & and | characters. VITE_APP_NAME= + +# Skip the once-per-session load splash. Set to true to skip; leave empty to show it. +VITE_SKIP_SPLASH= diff --git a/docker/docker-compose.yml b/docker/docker-compose.yml index 840b255..2efed27 100644 --- a/docker/docker-compose.yml +++ b/docker/docker-compose.yml @@ -24,4 +24,6 @@ services: - VITE_DISABLED_TABS=${VITE_DISABLED_TABS:-} - VITE_ENABLED_THEMES=${VITE_ENABLED_THEMES:-} - VITE_APP_NAME=${VITE_APP_NAME:-BEACON} + # Skip the once-per-session load splash; unset = shown + - VITE_SKIP_SPLASH=${VITE_SKIP_SPLASH:-} restart: unless-stopped diff --git a/src/components/SplashScreen.tsx b/src/components/SplashScreen.tsx index 6230e23..20a1be1 100644 --- a/src/components/SplashScreen.tsx +++ b/src/components/SplashScreen.tsx @@ -1,11 +1,13 @@ // Page-load splash: pulsing BEACON wordmark over a themed backdrop, shown once -// per browser session, then faded out and removed from the DOM. +// per browser session, then faded out and removed from the DOM. Set VITE_SKIP_SPLASH +// to skip it entirely for a deployment. // // To remove the splash entirely: delete this file and remove its import + // line from src/App.tsx. Nothing else references it. import { useState, useEffect } from "react"; import { BeaconLogo } from "./BeaconLogo"; +import { SKIP_SPLASH } from "../lib/constants"; const SPLASH_KEY = "beacon-splash-shown"; const VISIBLE_MS = 2000; @@ -15,6 +17,7 @@ export function SplashScreen() { // Synchronous gate: decided before first paint so StrictMode's double-mount // (and any same-session reload) never re-shows it. const [render, setRender] = useState(() => { + if (SKIP_SPLASH) return false; try { if (typeof sessionStorage === "undefined") return false; return sessionStorage.getItem(SPLASH_KEY) !== "1"; diff --git a/src/lib/constants.ts b/src/lib/constants.ts index 35666cc..40e0881 100644 --- a/src/lib/constants.ts +++ b/src/lib/constants.ts @@ -31,6 +31,12 @@ export function filterEnabledTabs(tabs: readonly string[], disabledRaw: string | return tabs.filter((t) => !disabled.has(t.toLowerCase())); } +// Truthy env flag: "1"/"true"/"yes"/"on" (case-insensitive). Anything else — empty, unset, or an +// un-substituted sentinel — is false, so the gated behaviour only turns on when explicitly asked for. +export function parseEnvBool(raw: string | undefined): boolean { + return ["1", "true", "yes", "on"].includes((raw ?? "").trim().toLowerCase()); +} + // The themes offered in the picker. When VITE_ENABLED_THEMES lists ids it's an EXCLUSIVE allowlist — // only those show (e.g. a branded deployment that wants just its own themes). Otherwise every // non-hidden theme shows and `hidden` ones (like the MeshMapper pair) stay out. @@ -55,3 +61,6 @@ export const ENABLED_THEME_IDS = new Set( // "||" (not "??") so an empty sed substitution falls back to the default too. export const APP_NAME = import.meta.env.VITE_APP_NAME || "BEACON"; export const GITHUB_URL = "https://github.com/MeshCore-Beacon"; + +// Skip the once-per-session load splash entirely (e.g. an embedded/branded deployment). +export const SKIP_SPLASH = parseEnvBool(import.meta.env.VITE_SKIP_SPLASH); diff --git a/tests/lib/constants.test.ts b/tests/lib/constants.test.ts index 57f2955..328fdd6 100644 --- a/tests/lib/constants.test.ts +++ b/tests/lib/constants.test.ts @@ -1,5 +1,5 @@ import { describe, it, expect } from "vitest"; -import { TABS, parseEnvList, filterEnabledTabs, selectableThemes } from "../../src/lib/constants"; +import { TABS, parseEnvList, filterEnabledTabs, parseEnvBool, selectableThemes } from "../../src/lib/constants"; describe("parseEnvList", () => { it("returns [] for undefined or empty", () => { @@ -37,6 +37,16 @@ describe("filterEnabledTabs", () => { }); }); +describe("parseEnvBool", () => { + it("is true for common truthy strings (case-insensitive)", () => { + for (const v of ["1", "true", "TRUE", "Yes", "on", " on "]) expect(parseEnvBool(v)).toBe(true); + }); + + it("is false for unset, empty, sentinel, or other values", () => { + for (const v of [undefined, "", "0", "false", "no", "__VITE_SKIP_SPLASH__"]) expect(parseEnvBool(v)).toBe(false); + }); +}); + describe("selectableThemes", () => { const THEMES = [ { id: "neutral-blue", name: "Neutral Blue" },