From 84e61464c70bcae02318fbf1d1c6cc2c4edeb9fa Mon Sep 17 00:00:00 2001 From: Johan Lundberg Date: Mon, 16 Feb 2026 12:08:19 +0100 Subject: [PATCH] feat: add Porchlight branding with logo, favicon, and redesigned CSS theme Rebrand from FastAPI OIDC OP to Porchlight with warm amber/gold identity: - Add doorway-with-light SVG logo and 32x32 PNG favicon - Rewrite style.css with full design system (color tokens, spacing scale, typography scale, section cards, button variants, dark mode) - Update base template with site header, logo, and favicon - Update all page titles and FastAPI app title to Porchlight --- src/fastapi_oidc_op/app.py | 2 +- src/fastapi_oidc_op/static/favicon.png | Bin 0 -> 236 bytes src/fastapi_oidc_op/static/logo.svg | 14 + src/fastapi_oidc_op/static/style.css | 355 ++++++++++++++---- src/fastapi_oidc_op/templates/base.html | 9 +- src/fastapi_oidc_op/templates/login.html | 2 +- .../templates/manage/credentials.html | 2 +- tests/test_app.py | 2 +- 8 files changed, 310 insertions(+), 76 deletions(-) create mode 100644 src/fastapi_oidc_op/static/favicon.png create mode 100644 src/fastapi_oidc_op/static/logo.svg diff --git a/src/fastapi_oidc_op/app.py b/src/fastapi_oidc_op/app.py index e91bd44..144798d 100644 --- a/src/fastapi_oidc_op/app.py +++ b/src/fastapi_oidc_op/app.py @@ -68,7 +68,7 @@ def create_app(settings: Settings | None = None) -> FastAPI: settings = Settings() # type: ignore[call-arg] app = FastAPI( - title="FastAPI OIDC OP", + title="Porchlight", version="0.1.0", docs_url="/docs" if settings.debug else None, redoc_url=None, diff --git a/src/fastapi_oidc_op/static/favicon.png b/src/fastapi_oidc_op/static/favicon.png new file mode 100644 index 0000000000000000000000000000000000000000..8c53fd6e4bb38ce542c58c38a7d2e05409a3366c GIT binary patch literal 236 zcmeAS@N?(olHy`uVBq!ia0vp^3LwnE1|*BCs=ffJ&7LlfAr*7pPPOH0P~d63+Ndoc zRMvk&(ne`_=SMC*i-|Khw%RLr$3Ifq_I|VOOA}7fX3t4KmbT9n;hJm1Hu=+G)A`X2 zDQj=Lo^#J)OyS$x-lFzQ<#Yw??TN{_07O2hKX zlTTj9sJzcP^vCw=?Pt%fNLcYf~4abrFkFv#pu-490EF@!PC{xWt~$(69Ce?TIB!$ literal 0 HcmV?d00001 diff --git a/src/fastapi_oidc_op/static/logo.svg b/src/fastapi_oidc_op/static/logo.svg new file mode 100644 index 0000000..c4767fa --- /dev/null +++ b/src/fastapi_oidc_op/static/logo.svg @@ -0,0 +1,14 @@ + + + + + + + + + + + + + + diff --git a/src/fastapi_oidc_op/static/style.css b/src/fastapi_oidc_op/static/style.css index cbc09f0..b6ae1de 100644 --- a/src/fastapi_oidc_op/static/style.css +++ b/src/fastapi_oidc_op/static/style.css @@ -1,30 +1,67 @@ +/* ========================================================================== + Porchlight — Default Theme + ========================================================================== */ + +/* ---------- Design tokens ---------- */ + :root { - --bg: #fdfdfd; - --fg: #1a1a1a; - --accent: #2563eb; + /* Colors — light mode */ + --bg: #fafaf9; + --fg: #1c1917; + --fg-muted: #78716c; + --accent: #d97706; + --accent-hover: #b45309; --accent-fg: #fff; - --border: #d1d5db; + --surface: #f5f5f4; + --border: #d6d3d1; --error-bg: #fef2f2; - --error-fg: #991b1b; + --error-fg: #dc2626; --success-bg: #f0fdf4; - --success-fg: #166534; + --success-fg: #16a34a; --radius: 0.375rem; + + /* Spacing scale (4px base) */ + --sp-1: 0.25rem; /* 4px */ + --sp-2: 0.5rem; /* 8px */ + --sp-3: 0.75rem; /* 12px */ + --sp-4: 1rem; /* 16px */ + --sp-5: 1.25rem; /* 20px */ + --sp-6: 1.5rem; /* 24px */ + --sp-8: 2rem; /* 32px */ + --sp-10: 2.5rem; /* 40px */ + --sp-12: 3rem; /* 48px */ + + /* Typography */ + --font-family: system-ui, -apple-system, sans-serif; + --font-size-sm: 0.875rem; + --font-size-base: 1rem; + --font-size-lg: 1.125rem; + --font-size-xl: 1.25rem; + --font-size-2xl: 1.5rem; + --font-size-3xl: 1.875rem; + --line-height: 1.6; + --line-height-tight: 1.25; } @media (prefers-color-scheme: dark) { :root { - --bg: #111; - --fg: #e5e5e5; - --accent: #60a5fa; - --accent-fg: #111; - --border: #404040; - --error-bg: #450a0a; - --error-fg: #fca5a5; - --success-bg: #052e16; - --success-fg: #86efac; + --bg: #1c1917; + --fg: #fafaf9; + --fg-muted: #a8a29e; + --accent: #f59e0b; + --accent-hover: #fbbf24; + --accent-fg: #1c1917; + --surface: #292524; + --border: #44403c; + --error-bg: #451a1a; + --error-fg: #f87171; + --success-bg: #14532d; + --success-fg: #4ade80; } } +/* ---------- Reset ---------- */ + *, *::before, *::after { @@ -32,23 +69,247 @@ } body { - font-family: system-ui, -apple-system, sans-serif; + font-family: var(--font-family); + font-size: var(--font-size-base); + line-height: var(--line-height); background: var(--bg); color: var(--fg); - max-width: 40rem; - margin: 2rem auto; - padding: 0 1rem; - line-height: 1.6; + margin: 0; + padding: 0; } +/* ---------- Layout ---------- */ + +.site-header { + max-width: 40rem; + margin: 0 auto; + padding: var(--sp-4) var(--sp-4) 0; +} + +.site-header a { + display: inline-flex; + align-items: center; + gap: var(--sp-2); + text-decoration: none; + color: var(--fg); +} + +.site-header a:hover { + opacity: 0.8; +} + +.site-logo { + width: 2rem; + height: 2rem; + flex-shrink: 0; +} + +.site-title { + font-size: var(--font-size-xl); + font-weight: 600; + letter-spacing: -0.01em; +} + +main { + max-width: 40rem; + margin: 0 auto; + padding: var(--sp-6) var(--sp-4) var(--sp-12); +} + +/* ---------- Typography ---------- */ + +h1 { + font-size: var(--font-size-3xl); + font-weight: 700; + line-height: var(--line-height-tight); + margin: 0 0 var(--sp-6); +} + +h2 { + font-size: var(--font-size-2xl); + font-weight: 600; + line-height: var(--line-height-tight); + margin: 0 0 var(--sp-4); +} + +h3 { + font-size: var(--font-size-xl); + font-weight: 600; + line-height: var(--line-height-tight); + margin: 0 0 var(--sp-3); +} + +p { + margin: 0 0 var(--sp-4); +} + +small { + font-size: var(--font-size-sm); + color: var(--fg-muted); +} + +/* ---------- Links ---------- */ + +a { + color: var(--accent); + text-decoration-thickness: 1px; + text-underline-offset: 2px; +} + +a:hover { + color: var(--accent-hover); +} + +/* ---------- Sections / Cards ---------- */ + +section { + background: var(--surface); + border: 1px solid var(--border); + border-radius: var(--radius); + padding: var(--sp-6); + margin-bottom: var(--sp-6); +} + +section h2 { + margin-top: 0; +} + +section h2:first-child { + margin-top: 0; +} + +/* ---------- Forms ---------- */ + +label { + display: block; + margin-bottom: var(--sp-1); + font-weight: 500; + font-size: var(--font-size-sm); +} + +input[type="text"], +input[type="password"], +input[type="email"] { + display: block; + width: 100%; + padding: var(--sp-2) var(--sp-3); + border: 1px solid var(--border); + border-radius: var(--radius); + background: var(--bg); + color: var(--fg); + font-family: var(--font-family); + font-size: var(--font-size-base); + line-height: var(--line-height); + margin-bottom: var(--sp-4); + transition: border-color 0.15s ease; +} + +input[type="text"]:focus, +input[type="password"]:focus, +input[type="email"]:focus { + border-color: var(--accent); + outline: none; + box-shadow: 0 0 0 2px color-mix(in srgb, var(--accent) 25%, transparent); +} + +/* ---------- Buttons ---------- */ + +button, +.btn { + display: inline-flex; + align-items: center; + justify-content: center; + gap: var(--sp-2); + padding: var(--sp-2) var(--sp-4); + border: none; + border-radius: var(--radius); + background: var(--accent); + color: var(--accent-fg); + font-family: var(--font-family); + font-size: var(--font-size-base); + font-weight: 500; + cursor: pointer; + transition: background-color 0.15s ease; + text-decoration: none; + line-height: var(--line-height-tight); +} + +button:hover, +.btn:hover { + background: var(--accent-hover); +} + +button:active, +.btn:active { + transform: translateY(1px); +} + +/* Secondary button */ +.btn-secondary { + background: transparent; + color: var(--fg); + border: 1px solid var(--border); +} + +.btn-secondary:hover { + background: var(--surface); + border-color: var(--fg-muted); +} + +/* Danger button */ +.btn-danger { + background: var(--error-fg); + color: #fff; +} + +.btn-danger:hover { + opacity: 0.9; +} + +/* ---------- Alerts ---------- */ + +[role="alert"] { + background: var(--error-bg); + color: var(--error-fg); + padding: var(--sp-3) var(--sp-4); + border-radius: var(--radius); + border: 1px solid color-mix(in srgb, var(--error-fg) 20%, transparent); + margin-bottom: var(--sp-4); + font-size: var(--font-size-sm); +} + +[role="status"] { + background: var(--success-bg); + color: var(--success-fg); + padding: var(--sp-3) var(--sp-4); + border-radius: var(--radius); + border: 1px solid color-mix(in srgb, var(--success-fg) 20%, transparent); + margin-bottom: var(--sp-4); + font-size: var(--font-size-sm); +} + +/* ---------- Lists ---------- */ + +ul { + padding-left: var(--sp-5); + margin: 0 0 var(--sp-4); +} + +li { + margin-bottom: var(--sp-2); +} + +/* ---------- Accessibility ---------- */ + .skip-link { position: absolute; left: -9999px; top: 0; background: var(--accent); color: var(--accent-fg); - padding: 0.5rem 1rem; + padding: var(--sp-2) var(--sp-4); z-index: 100; + font-weight: 500; } .skip-link:focus { @@ -60,56 +321,6 @@ body { outline-offset: 2px; } -label { - display: block; - margin-bottom: 0.25rem; - font-weight: 500; -} - -input[type="text"], -input[type="password"], -input[type="email"] { - display: block; - width: 100%; - padding: 0.5rem; - border: 1px solid var(--border); - border-radius: var(--radius); - background: var(--bg); - color: var(--fg); - font-size: 1rem; - margin-bottom: 1rem; -} - -button { - padding: 0.5rem 1rem; - border: none; - border-radius: var(--radius); - background: var(--accent); - color: var(--accent-fg); - font-size: 1rem; - cursor: pointer; -} - -button:hover { - opacity: 0.9; -} - -[role="alert"] { - background: var(--error-bg); - color: var(--error-fg); - padding: 0.75rem 1rem; - border-radius: var(--radius); - margin-bottom: 1rem; -} - -[role="status"] { - background: var(--success-bg); - color: var(--success-fg); - padding: 0.75rem 1rem; - border-radius: var(--radius); - margin-bottom: 1rem; -} - .sr-only { position: absolute; width: 1px; @@ -122,6 +333,8 @@ button:hover { border: 0; } +/* ---------- Reduced motion ---------- */ + @media (prefers-reduced-motion: reduce) { *, *::before, diff --git a/src/fastapi_oidc_op/templates/base.html b/src/fastapi_oidc_op/templates/base.html index f20bc8c..ecd9e33 100644 --- a/src/fastapi_oidc_op/templates/base.html +++ b/src/fastapi_oidc_op/templates/base.html @@ -3,11 +3,18 @@ - {% block title %}FastAPI OIDC OP{% endblock %} + {% block title %}Porchlight{% endblock %} + +
{% block content %}{% endblock %}
diff --git a/src/fastapi_oidc_op/templates/login.html b/src/fastapi_oidc_op/templates/login.html index 795d7f6..6725ddf 100644 --- a/src/fastapi_oidc_op/templates/login.html +++ b/src/fastapi_oidc_op/templates/login.html @@ -1,6 +1,6 @@ {% extends "base.html" %} -{% block title %}Login — FastAPI OIDC OP{% endblock %} +{% block title %}Sign in — Porchlight{% endblock %} {% block content %}

Sign in

diff --git a/src/fastapi_oidc_op/templates/manage/credentials.html b/src/fastapi_oidc_op/templates/manage/credentials.html index 2afa4d6..ca73824 100644 --- a/src/fastapi_oidc_op/templates/manage/credentials.html +++ b/src/fastapi_oidc_op/templates/manage/credentials.html @@ -1,6 +1,6 @@ {% extends "base.html" %} -{% block title %}Credentials — FastAPI OIDC OP{% endblock %} +{% block title %}Credentials — Porchlight{% endblock %} {% block content %}

Credentials

diff --git a/tests/test_app.py b/tests/test_app.py index 796a3e9..3204b5c 100644 --- a/tests/test_app.py +++ b/tests/test_app.py @@ -12,7 +12,7 @@ async def test_app_has_title(client: AsyncClient) -> None: response = await client.get("/openapi.json") assert response.status_code == 200 data = response.json() - assert data["info"]["title"] == "FastAPI OIDC OP" + assert data["info"]["title"] == "Porchlight" async def test_app_has_repos_on_state(client: AsyncClient) -> None: