feat: add custom Pivoine Rose theme with Bootstrap 4 styling and SVG favicon
This commit is contained in:
@@ -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}
|
||||
|
||||
@@ -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
278
asciinema/theme/custom.css
Normal 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;
|
||||
}
|
||||
24
asciinema/theme/favicon.svg
Normal file
24
asciinema/theme/favicon.svg
Normal 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 |
Reference in New Issue
Block a user