feat: add custom Pivoine Rose theme with Bootstrap 4 styling and SVG favicon

This commit is contained in:
2025-11-09 08:34:02 +01:00
parent 73b4fec389
commit 428fd70ac3
4 changed files with 358 additions and 0 deletions

View File

@@ -8,6 +8,7 @@ services:
volumes:
- asciinema_data:/var/opt/asciinema
- ./custom.exs:/opt/app/etc/custom.exs:ro
- ./theme:/opt/app/etc/theme:ro
environment:
SECRET_KEY_BASE: ${ASCIINEMA_SECRET_KEY}
URL_HOST: ${ASCIINEMA_TRAEFIK_HOST}

View File

@@ -15,3 +15,58 @@ config :asciinema, Asciinema.Emails.Mailer,
verify: :verify_none,
versions: [:"tlsv1.2", :"tlsv1.3"]
]
# Custom theme configuration - inject custom CSS and favicon
defmodule AsciinemaWeb.CustomThemePlug do
@moduledoc """
Plug to inject custom CSS and favicon into HTML responses
"""
import Plug.Conn
def init(opts), do: opts
def call(conn, _opts) do
register_before_send(conn, fn conn ->
if html_response?(conn) do
inject_custom_theme(conn)
else
conn
end
end)
end
defp html_response?(conn) do
case get_resp_header(conn, "content-type") do
[content_type | _] -> String.contains?(content_type, "text/html")
[] -> false
end
end
defp inject_custom_theme(conn) do
custom_head = """
<link rel="stylesheet" href="/theme/custom.css">
<link rel="icon" type="image/svg+xml" href="/theme/favicon.svg">
"""
case conn.resp_body do
body when is_binary(body) ->
new_body = String.replace(body, "</head>", "#{custom_head}</head>")
%{conn | resp_body: new_body}
_ ->
conn
end
end
end
# Configure Phoenix endpoint to serve custom theme files
config :asciinema, AsciinemaWeb.Endpoint,
# Serve theme files from /opt/app/etc/theme
static_dirs: %{
at: "/theme",
from: "/opt/app/etc/theme",
gzip: false
}
# Inject the custom theme Plug into the endpoint
config :asciinema, AsciinemaWeb.Endpoint,
plug: AsciinemaWeb.CustomThemePlug

278
asciinema/theme/custom.css Normal file
View File

@@ -0,0 +1,278 @@
/* Pivoine Rose Custom Theme for Asciinema Server */
/* Primary Color: #CE275B (Pivoine Rose) */
/* Background: Gray tones */
:root {
--pivoine-rose: #CE275B;
--pivoine-rose-dark: #A61E47;
--pivoine-rose-light: #E63368;
--gray-50: #FAFAFA;
--gray-100: #F5F5F5;
--gray-200: #EEEEEE;
--gray-300: #E0E0E0;
--gray-400: #BDBDBD;
--gray-500: #9E9E9E;
--gray-600: #757575;
--gray-700: #616161;
--gray-800: #424242;
--gray-900: #212121;
}
/* Override Bootstrap primary color */
.btn-primary,
.badge-primary,
.bg-primary,
.text-primary,
a.text-primary:hover,
a.text-primary:focus {
background-color: var(--pivoine-rose) !important;
border-color: var(--pivoine-rose) !important;
color: white !important;
}
.btn-primary:hover,
.btn-primary:focus,
.btn-primary:active {
background-color: var(--pivoine-rose-dark) !important;
border-color: var(--pivoine-rose-dark) !important;
}
/* Links */
a {
color: var(--pivoine-rose);
}
a:hover,
a:focus {
color: var(--pivoine-rose-dark);
}
/* Body and backgrounds */
body {
background-color: var(--gray-100);
color: var(--gray-900);
}
/* Header/Navbar */
.navbar {
background-color: var(--gray-800) !important;
border-bottom: 3px solid var(--pivoine-rose);
}
.navbar-dark .navbar-brand {
color: var(--pivoine-rose) !important;
font-weight: 600;
}
.navbar-dark .navbar-brand:hover {
color: var(--pivoine-rose-light) !important;
}
.navbar-dark .navbar-nav .nav-link {
color: var(--gray-200) !important;
}
.navbar-dark .navbar-nav .nav-link:hover {
color: var(--pivoine-rose) !important;
}
/* Cards and containers */
.card,
.list-group-item {
background-color: white;
border-color: var(--gray-300);
box-shadow: 0 1px 3px rgba(0, 0, 0, 0.1);
}
.card-header,
.list-group-item.active {
background-color: var(--gray-800);
border-color: var(--gray-700);
color: white;
}
/* Main content area */
.container,
.container-fluid {
background-color: var(--gray-50);
padding: 2rem 1rem;
}
main {
background-color: white;
border-radius: 4px;
padding: 2rem;
box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1);
}
/* Buttons */
.btn {
border-radius: 4px;
font-weight: 500;
transition: all 0.2s ease;
}
.btn-outline-primary {
color: var(--pivoine-rose);
border-color: var(--pivoine-rose);
}
.btn-outline-primary:hover {
background-color: var(--pivoine-rose);
border-color: var(--pivoine-rose);
color: white;
}
.btn-secondary {
background-color: var(--gray-600);
border-color: var(--gray-600);
}
.btn-secondary:hover {
background-color: var(--gray-700);
border-color: var(--gray-700);
}
/* Forms */
.form-control {
border-color: var(--gray-300);
border-radius: 4px;
}
.form-control:focus {
border-color: var(--pivoine-rose);
box-shadow: 0 0 0 0.2rem rgba(206, 39, 91, 0.25);
}
/* Alerts */
.alert-primary {
background-color: rgba(206, 39, 91, 0.1);
border-color: var(--pivoine-rose);
color: var(--pivoine-rose-dark);
}
/* Tables */
.table {
background-color: white;
}
.table thead th {
background-color: var(--gray-100);
border-bottom: 2px solid var(--pivoine-rose);
color: var(--gray-800);
}
.table-striped tbody tr:nth-of-type(odd) {
background-color: var(--gray-50);
}
/* Pagination */
.pagination .page-item.active .page-link {
background-color: var(--pivoine-rose);
border-color: var(--pivoine-rose);
}
.pagination .page-link {
color: var(--pivoine-rose);
}
.pagination .page-link:hover {
color: var(--pivoine-rose-dark);
background-color: var(--gray-100);
}
/* Player/Terminal */
.asciinema-player-wrapper,
.asciinema-terminal {
border-radius: 4px;
box-shadow: 0 4px 6px rgba(0, 0, 0, 0.1);
}
/* Footer */
footer {
background-color: var(--gray-800);
color: var(--gray-400);
border-top: 3px solid var(--pivoine-rose);
}
footer a {
color: var(--gray-300);
}
footer a:hover {
color: var(--pivoine-rose);
}
/* Badges */
.badge {
border-radius: 3px;
font-weight: 500;
}
.badge-secondary {
background-color: var(--gray-600);
}
/* Dropdowns */
.dropdown-menu {
border-color: var(--gray-300);
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.15);
}
.dropdown-item:hover,
.dropdown-item:focus {
background-color: var(--gray-100);
color: var(--pivoine-rose);
}
/* Loading/Progress */
.progress-bar {
background-color: var(--pivoine-rose);
}
/* Custom recording list */
.recording-item {
transition: transform 0.2s ease, box-shadow 0.2s ease;
}
.recording-item:hover {
transform: translateY(-2px);
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.15);
}
/* Logo styling */
.navbar-brand svg,
.navbar-brand img {
filter: brightness(0) saturate(100%) invert(28%) sepia(91%) saturate(2059%) hue-rotate(325deg) brightness(89%) contrast(92%);
transition: filter 0.2s ease;
}
.navbar-brand:hover svg,
.navbar-brand:hover img {
filter: brightness(0) saturate(100%) invert(36%) sepia(96%) saturate(2373%) hue-rotate(329deg) brightness(96%) contrast(87%);
}
/* Responsive adjustments */
@media (max-width: 768px) {
main {
padding: 1rem;
}
.container,
.container-fluid {
padding: 1rem 0.5rem;
}
}
/* Smooth animations */
* {
transition-property: background-color, border-color, color;
transition-duration: 0.15s;
transition-timing-function: ease-in-out;
}
button,
.btn,
a {
transition-property: all;
}

View File

@@ -0,0 +1,24 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 100 100">
<!-- Placeholder Favicon - Pivoine Rose themed -->
<defs>
<linearGradient id="grad" x1="0%" y1="0%" x2="100%" y2="100%">
<stop offset="0%" style="stop-color:#E63368;stop-opacity:1" />
<stop offset="100%" style="stop-color:#CE275B;stop-opacity:1" />
</linearGradient>
</defs>
<!-- Background circle -->
<circle cx="50" cy="50" r="48" fill="url(#grad)" stroke="#A61E47" stroke-width="2"/>
<!-- Terminal symbol/Play button hybrid -->
<g fill="#FFFFFF">
<!-- Terminal prompt bracket -->
<path d="M 30 35 L 25 40 L 30 45" stroke="#FFFFFF" stroke-width="3" fill="none" stroke-linecap="round"/>
<!-- Play triangle (for asciinema recording) -->
<path d="M 45 35 L 45 65 L 70 50 Z" fill="#FFFFFF"/>
</g>
<!-- Optional dot for recording indicator -->
<circle cx="72" cy="30" r="6" fill="#FFFFFF" opacity="0.9"/>
</svg>

After

Width:  |  Height:  |  Size: 932 B