diff --git a/Dockerfile b/Dockerfile
index c6a5f5f..ffa10c0 100644
--- a/Dockerfile
+++ b/Dockerfile
@@ -9,7 +9,9 @@ COPY package.json package-lock.json ./
RUN npm ci
COPY . .
ARG BASE_PATH=/
-RUN BASE_PATH="$BASE_PATH" npm run build
+# .git isn't in the build context, so pass the version in: --build-arg APP_VERSION=v1.2.0
+ARG APP_VERSION=dev
+RUN BASE_PATH="$BASE_PATH" APP_VERSION="$APP_VERSION" npm run build
FROM nginxinc/nginx-unprivileged:1.29-alpine AS serve
ARG BASE_PATH=/
diff --git a/src/App.css b/src/App.css
index c246761..02bf4bf 100644
--- a/src/App.css
+++ b/src/App.css
@@ -31,6 +31,8 @@
--track: #dbe2ec;
--grid-line: rgba(15, 27, 42, 0.07);
--scrim: rgba(15, 27, 42, 0.42);
+ --libreble-ble: #c2410c;
+ --libreble-led: #f26b1d;
}
@media (prefers-color-scheme: dark) {
:root {
@@ -58,6 +60,8 @@
--track: #1c2836;
--grid-line: rgba(231, 238, 246, 0.08);
--scrim: rgba(5, 9, 14, 0.62);
+ --libreble-ble: #ffa259;
+ --libreble-led: #ff8a3d;
}
}
/* explicit toggle overrides — must win over the media query in both directions */
@@ -86,6 +90,8 @@
--track: #dbe2ec;
--grid-line: rgba(15, 27, 42, 0.07);
--scrim: rgba(15, 27, 42, 0.42);
+ --libreble-ble: #c2410c;
+ --libreble-led: #f26b1d;
}
:root[data-theme='dark'] {
--bg: #0b1017;
@@ -112,6 +118,8 @@
--track: #1c2836;
--grid-line: rgba(231, 238, 246, 0.08);
--scrim: rgba(5, 9, 14, 0.62);
+ --libreble-ble: #ffa259;
+ --libreble-led: #ff8a3d;
}
* {
@@ -1244,17 +1252,46 @@ body {
color: var(--accent-2);
}
-.footnote {
+/* libreble footer: one centered line — "Part of libreble" · GitHub · Report an issue · version */
+.app-footer {
margin-top: 34px;
padding-top: 16px;
border-top: 1px solid var(--border);
color: var(--ink-3);
- font-size: 12px;
- line-height: 1.6;
+ font-size: 11.5px;
+ line-height: 1.5;
}
-.footnote b {
- color: var(--ink-2);
- font-weight: 600;
+.app-footer p {
+ margin: 0;
+ display: flex;
+ flex-wrap: wrap;
+ align-items: center;
+ justify-content: center;
+ gap: 4px 12px;
+}
+.app-footer a {
+ display: inline-flex;
+ align-items: center;
+ gap: 4px;
+ color: inherit;
+ text-decoration: none;
+}
+.app-footer a:hover {
+ color: var(--ink);
+}
+.app-footer svg {
+ width: 14px;
+ height: 14px;
+ flex: none;
+}
+.libreble-name {
+ font-weight: 500;
+}
+.libreble-ble {
+ color: var(--libreble-ble);
+}
+.libreble-led {
+ fill: var(--libreble-led);
}
/* ===================== app-shell extras (beyond the prototype) ===================== */
diff --git a/src/App.tsx b/src/App.tsx
index e4b4d97..2859e9b 100644
--- a/src/App.tsx
+++ b/src/App.tsx
@@ -8,11 +8,12 @@ import { WebBluetoothTransport, isWebBluetoothAvailable } from './ble/webBluetoo
import { useToolClient } from './hooks/useToolClient';
import { useTheme } from './theme';
import { Icon, IconSprite } from './Icons';
-import { GUIDE, NOMINAL_TABLE } from './guide';
+import { NOMINAL_TABLE } from './guide';
import Dashboard from './Dashboard';
import { AccessoryPage, GuideAccessoryGrid, GuideMaterialGrid, MaterialPage } from './Guide';
import type { GuideProps } from './Guide';
import Diagnostics from './Diagnostics';
+import AppFooter from './AppFooter';
type TransportKind = 'ble' | 'mock';
@@ -174,11 +175,7 @@ export default function App() {
} />
-
+
);
}
diff --git a/src/AppFooter.tsx b/src/AppFooter.tsx
new file mode 100644
index 0000000..18f12f5
--- /dev/null
+++ b/src/AppFooter.tsx
@@ -0,0 +1,64 @@
+const REPO = 'https://github.com/libreble/collet';
+/** A clean tag build ("v1.2.0") links to its release; anything else is shown as plain text. */
+const RELEASE = /^v\d+\.\d+\.\d+$/.test(__APP_VERSION__)
+ ? `${REPO}/releases/tag/${__APP_VERSION__}`
+ : null;
+
+export default function AppFooter() {
+ return (
+
+ );
+}
+
+/** The libreble "open beacon": an LED inside two rings opening to the top right. */
+function LibrebleMark() {
+ return (
+
+ );
+}
+
+function GitHubMark() {
+ return (
+
+ );
+}
diff --git a/src/vite-env.d.ts b/src/vite-env.d.ts
new file mode 100644
index 0000000..2632f11
--- /dev/null
+++ b/src/vite-env.d.ts
@@ -0,0 +1,4 @@
+///
+
+/** Build version for the footer — see appVersion() in vite.config.ts. */
+declare const __APP_VERSION__: string;
diff --git a/vite.config.ts b/vite.config.ts
index a3c4ce7..5288e65 100644
--- a/vite.config.ts
+++ b/vite.config.ts
@@ -1,3 +1,4 @@
+import { execSync } from 'node:child_process';
import { copyFileSync } from 'node:fs';
import { resolve } from 'node:path';
import { defineConfig, type Plugin } from 'vite';
@@ -29,8 +30,24 @@ const base = `/${(process.env.BASE_PATH ?? '/collet/').replace(/^\/+|\/+$/g, '')
'/',
);
+// Shown in the footer. Tag builds in CI use the tag (v1.2.0); elsewhere `git describe`
+// (v1.2.0-3-gabc1234, or a bare sha on a shallow clone); `APP_VERSION` overrides; no git → "dev".
+function appVersion(): string {
+ if (process.env.APP_VERSION) return process.env.APP_VERSION;
+ if (process.env.GITHUB_REF_TYPE === 'tag' && process.env.GITHUB_REF_NAME)
+ return process.env.GITHUB_REF_NAME;
+ try {
+ return execSync('git describe --tags --always', { stdio: ['ignore', 'pipe', 'ignore'] })
+ .toString()
+ .trim();
+ } catch {
+ return 'dev';
+ }
+}
+
export default defineConfig({
base,
+ define: { __APP_VERSION__: JSON.stringify(appVersion()) },
plugins: [
react(),
VitePWA({