feat(ui): overridable favicon/logo with branded default
Replace the ⚓ emoji with a real logo image used as the in-app brand mark and the favicon. Default bundled public/logo.svg is the HangmanLab mark recolored to the Foundry-Deck ember (#ff6a1a). Override at deploy time via HARBORFORGE_LOGO_URL (injected into runtime-config.js; getLogoUrl() + favicon swap), no rebuild needed. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -17,10 +17,14 @@ ENV FRONTEND_DEV_MODE=0
|
|||||||
# exists; /auth/config remains authoritative once the backend is up.
|
# exists; /auth/config remains authoritative once the backend is up.
|
||||||
ARG HARBORFORGE_OIDC_ONLY=false
|
ARG HARBORFORGE_OIDC_ONLY=false
|
||||||
ENV HARBORFORGE_OIDC_ONLY=${HARBORFORGE_OIDC_ONLY}
|
ENV HARBORFORGE_OIDC_ONLY=${HARBORFORGE_OIDC_ONLY}
|
||||||
|
# Optional deploy-time branding override: a URL the SPA uses for the
|
||||||
|
# logo + favicon. Empty → bundled /logo.svg default.
|
||||||
|
ARG HARBORFORGE_LOGO_URL=
|
||||||
|
ENV HARBORFORGE_LOGO_URL=${HARBORFORGE_LOGO_URL}
|
||||||
EXPOSE 3000
|
EXPOSE 3000
|
||||||
CMD ["sh", "-c", "\
|
CMD ["sh", "-c", "\
|
||||||
if [ \"$HARBORFORGE_OIDC_ONLY\" = \"true\" ]; then OO=true; else OO=false; fi; \
|
if [ \"$HARBORFORGE_OIDC_ONLY\" = \"true\" ]; then OO=true; else OO=false; fi; \
|
||||||
CFG=\"window.__HF_RUNTIME__={\\\"oidc_only\\\":$OO};\"; \
|
CFG=\"window.__HF_RUNTIME__={\\\"oidc_only\\\":$OO,\\\"logo_url\\\":\\\"$HARBORFORGE_LOGO_URL\\\"};\"; \
|
||||||
mkdir -p public; printf '%s' \"$CFG\" > public/runtime-config.js; \
|
mkdir -p public; printf '%s' \"$CFG\" > public/runtime-config.js; \
|
||||||
[ -d dist ] && printf '%s' \"$CFG\" > dist/runtime-config.js; \
|
[ -d dist ] && printf '%s' \"$CFG\" > dist/runtime-config.js; \
|
||||||
if [ \"$FRONTEND_DEV_MODE\" = \"1\" ]; then npm run dev -- --host 0.0.0.0 --port 3000 --strictPort; else serve -s dist -l 3000; fi"]
|
if [ \"$FRONTEND_DEV_MODE\" = \"1\" ]; then npm run dev -- --host 0.0.0.0 --port 3000 --strictPort; else serve -s dist -l 3000; fi"]
|
||||||
|
|||||||
@@ -2,7 +2,7 @@
|
|||||||
<html lang="en">
|
<html lang="en">
|
||||||
<head>
|
<head>
|
||||||
<meta charset="UTF-8" />
|
<meta charset="UTF-8" />
|
||||||
<link rel="icon" type="image/svg+xml" href="/vite.svg" />
|
<link id="hf-favicon" rel="icon" type="image/svg+xml" href="/logo.svg" />
|
||||||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||||
<title>HarborForge</title>
|
<title>HarborForge</title>
|
||||||
</head>
|
</head>
|
||||||
@@ -11,6 +11,13 @@
|
|||||||
<!-- Runtime config injected by the container entrypoint (deploy-time
|
<!-- Runtime config injected by the container entrypoint (deploy-time
|
||||||
HARBORFORGE_OIDC_ONLY). Absent in dev → app falls back to /auth/config. -->
|
HARBORFORGE_OIDC_ONLY). Absent in dev → app falls back to /auth/config. -->
|
||||||
<script src="/runtime-config.js"></script>
|
<script src="/runtime-config.js"></script>
|
||||||
|
<script>
|
||||||
|
// Optional deploy-time branding override (HARBORFORGE_LOGO_URL).
|
||||||
|
try {
|
||||||
|
var u = window.__HF_RUNTIME__ && window.__HF_RUNTIME__.logo_url;
|
||||||
|
if (u) document.getElementById('hf-favicon').href = u;
|
||||||
|
} catch (e) {}
|
||||||
|
</script>
|
||||||
<script type="module" src="/src/main.tsx"></script>
|
<script type="module" src="/src/main.tsx"></script>
|
||||||
</body>
|
</body>
|
||||||
</html>
|
</html>
|
||||||
|
|||||||
44
public/logo.svg
Normal file
44
public/logo.svg
Normal file
@@ -0,0 +1,44 @@
|
|||||||
|
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 733.000000 733.000000" role="img" aria-label="Hangman Lab">
|
||||||
|
<g transform="translate(0.000000,733.000000) scale(0.100000,-0.100000)"
|
||||||
|
fill="#ff6a1a" stroke="none">
|
||||||
|
<path d="M812 6860 c-66 -40 -73 -66 -70 -242 3 -141 5 -159 24 -185 43 -58
|
||||||
|
63 -63 262 -64 l182 -1 0 -2694 0 -2694 -290 0 c-314 0 -332 -3 -380 -54 -24
|
||||||
|
-27 -25 -32 -28 -199 -3 -195 4 -218 71 -250 32 -16 257 -17 3084 -17 2955 0
|
||||||
|
3049 1 3083 19 65 34 75 66 75 236 0 186 -16 224 -104 254 -25 8 -682 11
|
||||||
|
-2512 11 l-2479 0 0 1998 0 1997 698 697 697 697 670 -3 c369 -2 680 -4 693
|
||||||
|
-5 22 -1 22 -1 22 -269 l0 -268 84 -12 c47 -7 103 -19 125 -27 41 -14 41 -14
|
||||||
|
43 283 l3 297 631 1 c698 2 673 -1 714 67 18 28 20 51 20 187 0 173 -8 201
|
||||||
|
-72 240 -33 20 -56 20 -2623 20 -2567 0 -2590 0 -2623 -20z m1725 -500 c-1 -3
|
||||||
|
-183 -185 -404 -404 l-403 -399 0 405 0 406 405 -1 c223 -1 404 -4 402 -7z
|
||||||
|
M4283 5701 c-459 -125 -690 -624 -483 -1046 67 -138 196 -266 335 -335 281
|
||||||
|
-139 612 -93 837 118 235 219 297 575 152 870 -85 174 -235 306 -427 375 -111
|
||||||
|
40 -302 48 -414 18z m267 -211 c95 -15 201 -70 274 -144 273 -274 173 -723
|
||||||
|
-189 -851 -93 -33 -236 -35 -326 -5 -202 68 -340 249 -356 465 -18 242 153
|
||||||
|
471 394 529 71 18 119 19 203 6z M5945 4716 c-56 -25 -80 -61 -80 -122 0 -48
|
||||||
|
4 -57 36 -90 88 -88 219 -29 219 98 0 45 -34 96 -75 114 -42 17 -61 17 -100 0z
|
||||||
|
M5675 4466 c-60 -26 -73 -109 -26 -157 62 -61 161 -19 161 70 0 74 -68 118
|
||||||
|
-135 87z M3747 4397 c-10 -7 -226 -223 -479 -482 -307 -313 -467 -483 -479
|
||||||
|
-510 -35 -75 -17 -177 38 -227 39 -35 114 -61 160 -54 100 13 104 17 583 554
|
||||||
|
l145 162 3 -404 c3 -455 15 -342 -137 -1186 -76 -423 -101 -587 -101 -664 0
|
||||||
|
-190 182 -307 377 -242 62 21 136 95 152 152 6 22 29 188 52 369 57 466 109
|
||||||
|
839 118 848 4 4 52 6 106 5 l99 -3 67 -210 c38 -115 68 -220 69 -232 0 -12
|
||||||
|
-38 -152 -85 -311 -47 -159 -85 -302 -85 -317 0 -128 109 -225 255 -225 109 1
|
||||||
|
185 42 228 125 26 51 237 683 237 710 0 11 -54 182 -121 380 -121 360 -121
|
||||||
|
360 -134 605 -8 135 -14 292 -14 350 l0 105 152 -158 c84 -87 166 -167 182
|
||||||
|
-177 44 -29 98 -26 135 8 38 35 187 379 173 401 -5 8 -34 20 -64 26 -59 11
|
||||||
|
-38 -8 -393 362 -89 93 -100 97 -174 59 -59 -30 -161 -62 -229 -71 -52 -7 -53
|
||||||
|
-7 -53 -41 0 -44 -21 -84 -61 -118 -38 -32 -41 0 32 -455 l52 -325 -82 -78
|
||||||
|
c-45 -43 -85 -78 -89 -78 -4 0 -42 35 -84 78 l-77 77 34 215 c19 118 46 289
|
||||||
|
61 379 26 164 26 164 -5 188 -42 33 -54 58 -61 121 -5 54 -5 54 -92 87 -98 38
|
||||||
|
-184 89 -243 145 -76 70 -123 87 -168 57z M5504 4170 c-74 -30 -69 -170 6
|
||||||
|
-170 19 0 20 -7 20 -123 0 -122 0 -122 -54 -262 -30 -77 -97 -243 -150 -369
|
||||||
|
-106 -252 -113 -287 -73 -347 46 -70 38 -69 560 -69 327 0 475 3 490 11 69 35
|
||||||
|
107 109 88 172 -5 17 -62 142 -126 277 -225 470 -215 443 -215 586 0 114 2
|
||||||
|
124 18 124 57 0 72 101 23 151 -29 29 -29 29 -298 28 -147 0 -278 -4 -289 -9z
|
||||||
|
m376 -307 c1 -148 1 -148 81 -333 101 -231 98 -222 68 -216 -13 3 -116 22
|
||||||
|
-229 42 -112 19 -208 37 -212 40 -4 2 18 75 48 160 54 156 54 156 54 305 l0
|
||||||
|
149 95 0 95 0 0 -147z m-219 -593 c22 0 49 -42 49 -75 0 -46 -29 -75 -74 -75
|
||||||
|
-37 0 -76 36 -76 71 0 46 45 94 78 83 8 -2 18 -4 23 -4z m327 -126 c53 -59 -7
|
||||||
|
-144 -80 -113 -54 22 -65 83 -22 125 30 31 67 27 102 -12z"/>
|
||||||
|
</g>
|
||||||
|
</svg>
|
||||||
|
After Width: | Height: | Size: 3.0 KiB |
@@ -2,6 +2,7 @@ import { useState, useEffect } from 'react'
|
|||||||
import { Link, useLocation, useNavigate } from 'react-router-dom'
|
import { Link, useLocation, useNavigate } from 'react-router-dom'
|
||||||
import api from '@/services/api'
|
import api from '@/services/api'
|
||||||
import { useAuthConfig, oidcLinkHref } from '@/hooks/useAuthConfig'
|
import { useAuthConfig, oidcLinkHref } from '@/hooks/useAuthConfig'
|
||||||
|
import { getLogoUrl } from '@/runtime'
|
||||||
import type { User } from '@/types'
|
import type { User } from '@/types'
|
||||||
|
|
||||||
interface Props {
|
interface Props {
|
||||||
@@ -50,7 +51,7 @@ export default function Sidebar({ user, onLogout }: Props) {
|
|||||||
return (
|
return (
|
||||||
<nav className="sidebar">
|
<nav className="sidebar">
|
||||||
<div className="sidebar-header">
|
<div className="sidebar-header">
|
||||||
<h1>⚓ HarborForge</h1>
|
<h1><img src={getLogoUrl()} className="brand-logo" alt="" /> HarborForge</h1>
|
||||||
</div>
|
</div>
|
||||||
<ul className="nav-links">
|
<ul className="nav-links">
|
||||||
{links.map((l) => (
|
{links.map((l) => (
|
||||||
|
|||||||
@@ -144,7 +144,12 @@ input, textarea, select, button { font-family: inherit; }
|
|||||||
font-size: 1.3rem; letter-spacing: .12em;
|
font-size: 1.3rem; letter-spacing: .12em;
|
||||||
display: flex; align-items: center; gap: 8px;
|
display: flex; align-items: center; gap: 8px;
|
||||||
}
|
}
|
||||||
.sidebar-header h1::first-letter { color: var(--accent); }
|
/* Brand logo (overridable via HARBORFORGE_LOGO_URL / public/logo.svg) */
|
||||||
|
.brand-logo {
|
||||||
|
height: 1.15em; width: auto; vertical-align: -0.18em;
|
||||||
|
display: inline-block; flex: 0 0 auto;
|
||||||
|
}
|
||||||
|
.login-card h1 .brand-logo, .setup-header h1 .brand-logo { height: 1.4em; }
|
||||||
.nav-links { list-style: none; flex: 1; padding: 14px 12px; display: flex; flex-direction: column; gap: 2px; }
|
.nav-links { list-style: none; flex: 1; padding: 14px 12px; display: flex; flex-direction: column; gap: 2px; }
|
||||||
.nav-links li a {
|
.nav-links li a {
|
||||||
display: flex; align-items: center; gap: 10px;
|
display: flex; align-items: center; gap: 10px;
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
import React, { useState } from 'react'
|
import React, { useState } from 'react'
|
||||||
import { useAuthConfig, oidcLoginHref } from '@/hooks/useAuthConfig'
|
import { useAuthConfig, oidcLoginHref } from '@/hooks/useAuthConfig'
|
||||||
|
import { getLogoUrl } from '@/runtime'
|
||||||
|
|
||||||
interface Props {
|
interface Props {
|
||||||
onLogin: (username: string, password: string) => Promise<void>
|
onLogin: (username: string, password: string) => Promise<void>
|
||||||
@@ -44,7 +45,7 @@ export default function LoginPage({ onLogin }: Props) {
|
|||||||
return (
|
return (
|
||||||
<div className="login-page">
|
<div className="login-page">
|
||||||
<div className="login-card">
|
<div className="login-card">
|
||||||
<h1>⚓ HarborForge</h1>
|
<h1><img src={getLogoUrl()} className="brand-logo" alt="" /> HarborForge</h1>
|
||||||
<p className="subtitle">Agent/Human collaborative task management platform</p>
|
<p className="subtitle">Agent/Human collaborative task management platform</p>
|
||||||
|
|
||||||
{oidcError && <p className="error" style={{ marginBottom: 14 }}>{oidcError}</p>}
|
{oidcError && <p className="error" style={{ marginBottom: 14 }}>{oidcError}</p>}
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
import { useEffect, useRef, useState } from 'react'
|
import { useEffect, useRef, useState } from 'react'
|
||||||
import { useNavigate } from 'react-router-dom'
|
import { useNavigate } from 'react-router-dom'
|
||||||
|
import { getLogoUrl } from '@/runtime'
|
||||||
|
|
||||||
interface Props {
|
interface Props {
|
||||||
onToken: (token: string) => Promise<void>
|
onToken: (token: string) => Promise<void>
|
||||||
@@ -47,7 +48,7 @@ export default function OidcCallbackPage({ onToken }: Props) {
|
|||||||
return (
|
return (
|
||||||
<div className="login-page">
|
<div className="login-page">
|
||||||
<div className="login-card">
|
<div className="login-card">
|
||||||
<h1>⚓ HarborForge</h1>
|
<h1><img src={getLogoUrl()} className="brand-logo" alt="" /> HarborForge</h1>
|
||||||
<p className="subtitle">{msg}</p>
|
<p className="subtitle">{msg}</p>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
import { useState } from 'react'
|
import { useState } from 'react'
|
||||||
import axios from 'axios'
|
import axios from 'axios'
|
||||||
import { getRuntimeOidcOnly } from '@/runtime'
|
import { getRuntimeOidcOnly, getLogoUrl } from '@/runtime'
|
||||||
|
|
||||||
interface Props {
|
interface Props {
|
||||||
initialWizardPort: number | null
|
initialWizardPort: number | null
|
||||||
@@ -140,7 +140,7 @@ export default function SetupWizardPage({ initialWizardPort, onComplete }: Props
|
|||||||
<div className="setup-wizard">
|
<div className="setup-wizard">
|
||||||
<div className="setup-container">
|
<div className="setup-container">
|
||||||
<div className="setup-header">
|
<div className="setup-header">
|
||||||
<h1>⚓ HarborForge Setup Wizard</h1>
|
<h1><img src={getLogoUrl()} className="brand-logo" alt="" /> HarborForge Setup Wizard</h1>
|
||||||
<div className="setup-steps">
|
<div className="setup-steps">
|
||||||
{STEPS.map((s, i) => (
|
{STEPS.map((s, i) => (
|
||||||
<span key={i} className={`setup-step ${i === step ? 'active' : i < step ? 'done' : ''}`}>
|
<span key={i} className={`setup-step ${i === step ? 'active' : i < step ? 'done' : ''}`}>
|
||||||
|
|||||||
@@ -3,7 +3,7 @@
|
|||||||
// Available before the backend exists — used by the setup wizard.
|
// Available before the backend exists — used by the setup wizard.
|
||||||
declare global {
|
declare global {
|
||||||
interface Window {
|
interface Window {
|
||||||
__HF_RUNTIME__?: { oidc_only?: boolean }
|
__HF_RUNTIME__?: { oidc_only?: boolean; logo_url?: string }
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -13,4 +13,11 @@ export function getRuntimeOidcOnly(): boolean | null {
|
|||||||
return typeof v === 'boolean' ? v : null
|
return typeof v === 'boolean' ? v : null
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/** Brand logo URL: deploy-time override (HARBORFORGE_LOGO_URL) or the
|
||||||
|
* bundled default at /logo.svg. */
|
||||||
|
export function getLogoUrl(): string {
|
||||||
|
const u = typeof window !== 'undefined' ? window.__HF_RUNTIME__?.logo_url : undefined
|
||||||
|
return (typeof u === 'string' && u) ? u : '/logo.svg'
|
||||||
|
}
|
||||||
|
|
||||||
export {}
|
export {}
|
||||||
|
|||||||
Reference in New Issue
Block a user