feat: add custom Pivoine Rose theme with Bootstrap 4 styling and SVG favicon
This commit is contained in:
@@ -8,6 +8,7 @@ services:
|
|||||||
volumes:
|
volumes:
|
||||||
- asciinema_data:/var/opt/asciinema
|
- asciinema_data:/var/opt/asciinema
|
||||||
- ./custom.exs:/opt/app/etc/custom.exs:ro
|
- ./custom.exs:/opt/app/etc/custom.exs:ro
|
||||||
|
- ./theme:/opt/app/etc/theme:ro
|
||||||
environment:
|
environment:
|
||||||
SECRET_KEY_BASE: ${ASCIINEMA_SECRET_KEY}
|
SECRET_KEY_BASE: ${ASCIINEMA_SECRET_KEY}
|
||||||
URL_HOST: ${ASCIINEMA_TRAEFIK_HOST}
|
URL_HOST: ${ASCIINEMA_TRAEFIK_HOST}
|
||||||
|
|||||||
@@ -15,3 +15,58 @@ config :asciinema, Asciinema.Emails.Mailer,
|
|||||||
verify: :verify_none,
|
verify: :verify_none,
|
||||||
versions: [:"tlsv1.2", :"tlsv1.3"]
|
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