diff --git a/asciinema/compose.yaml b/asciinema/compose.yaml index a4e74c3..7e68922 100644 --- a/asciinema/compose.yaml +++ b/asciinema/compose.yaml @@ -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} diff --git a/asciinema/custom.exs b/asciinema/custom.exs index dd1b726..a1cc669 100644 --- a/asciinema/custom.exs +++ b/asciinema/custom.exs @@ -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 = """ + + + """ + + case conn.resp_body do + body when is_binary(body) -> + new_body = String.replace(body, "", "#{custom_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 diff --git a/asciinema/theme/custom.css b/asciinema/theme/custom.css new file mode 100644 index 0000000..9d092f1 --- /dev/null +++ b/asciinema/theme/custom.css @@ -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; +} diff --git a/asciinema/theme/favicon.svg b/asciinema/theme/favicon.svg new file mode 100644 index 0000000..2a96a8b --- /dev/null +++ b/asciinema/theme/favicon.svg @@ -0,0 +1,24 @@ + + + + + + + + + + + + + + + + + + + + + + + +