diff --git a/offlineu_core.py b/offlineu_core.py
index bb7e247..68df164 100644
--- a/offlineu_core.py
+++ b/offlineu_core.py
@@ -58,8 +58,11 @@ VIDEO_SIZE_BOUNDS = {'video_width': (200, 4000), 'video_height': (120, 3000)}
# Allowed values for every setting except accent_color (validated separately
# as a hex string). Anything outside these sets is rejected/ignored on save.
SETTINGS_CHOICES = {
- 'theme': {'dark', 'light'},
- 'font_family': {'system', 'sans', 'serif', 'monospace'},
+ 'theme': {
+ 'dark', 'light', 'dracula', 'tokyo_night', 'catppuccin_mocha', 'ayu_dark',
+ 'github_dark', 'atom_one_dark', 'houston', 'night_owl', 'dainty', 'matcha',
+ },
+ 'font_family': {'system', 'sans', 'serif', 'monospace', 'monaspace'},
'font_size': {'small', 'medium', 'large', 'xlarge'},
'layout_width': {'normal', 'wide', 'full'},
'density': {'comfortable', 'compact'},
@@ -67,6 +70,89 @@ SETTINGS_CHOICES = {
'corner_radius': {'sharp', 'rounded', 'pill'},
}
+# Display names for the theme dropdown - only needed where the raw key
+# (e.g. 'tokyo_night') isn't already a clean label via .capitalize().
+THEME_DISPLAY_NAMES = {
+ 'dark': 'Dark', 'light': 'Light', 'dracula': 'Dracula',
+ 'tokyo_night': 'Tokyo Night', 'catppuccin_mocha': 'Catppuccin Mocha',
+ 'ayu_dark': 'Ayu Dark', 'github_dark': 'GitHub Dark',
+ 'atom_one_dark': 'Atom One Dark', 'houston': 'Houston',
+ 'night_owl': 'Night Owl', 'dainty': 'Dainty', 'matcha': 'Matcha',
+}
+
+# Full color palette per theme. 'accent' here is only the *default* accent
+# offered when a theme is first selected - the accent_color setting is what
+# actually drives --accent afterward, so it stays independently editable.
+# Sourced from each theme's official palette (Dracula, Tokyo Night,
+# Catppuccin Mocha, Ayu, GitHub Dark, Atom One Dark, Houston all verified
+# against upstream repos/specs). Dainty and Matcha don't have one single
+# fixed official hex set (Dainty is a theme *generator*; Matcha's exact
+# source values weren't available) - built in the spirit of their
+# documented look (refined/minimal, earthy green-and-gray) rather than
+# claimed as an exact match.
+THEME_PALETTES = {
+ 'dark': {
+ 'bg-primary': '#1a1a1a', 'bg-secondary': '#2d2d2d', 'bg-tertiary': '#3d3d3d',
+ 'bg-tertiary-hover': '#404040', 'text-primary': '#e0e0e0', 'text-muted': '#999999',
+ 'border-color': '#555555', 'accent': '#007acc', 'accent-hover': '#005a9e',
+ },
+ 'light': {
+ 'bg-primary': '#f2f2f2', 'bg-secondary': '#ffffff', 'bg-tertiary': '#eeeeee',
+ 'bg-tertiary-hover': '#e2e2e2', 'text-primary': '#222222', 'text-muted': '#666666',
+ 'border-color': '#cccccc', 'accent': '#007acc', 'accent-hover': '#005a9e',
+ },
+ 'dracula': {
+ 'bg-primary': '#282a36', 'bg-secondary': '#343746', 'bg-tertiary': '#44475a',
+ 'bg-tertiary-hover': '#4d5066', 'text-primary': '#f8f8f2', 'text-muted': '#6272a4',
+ 'border-color': '#44475a', 'accent': '#bd93f9', 'accent-hover': '#a672f0',
+ },
+ 'tokyo_night': {
+ 'bg-primary': '#1a1b26', 'bg-secondary': '#1f2335', 'bg-tertiary': '#283457',
+ 'bg-tertiary-hover': '#364a73', 'text-primary': '#c0caf5', 'text-muted': '#565f89',
+ 'border-color': '#292e42', 'accent': '#7aa2f7', 'accent-hover': '#6183bb',
+ },
+ 'catppuccin_mocha': {
+ 'bg-primary': '#1e1e2e', 'bg-secondary': '#313244', 'bg-tertiary': '#45475a',
+ 'bg-tertiary-hover': '#585b70', 'text-primary': '#cdd6f4', 'text-muted': '#a6adc8',
+ 'border-color': '#585b70', 'accent': '#cba6f7', 'accent-hover': '#b48ee0',
+ },
+ 'ayu_dark': {
+ 'bg-primary': '#0a0e14', 'bg-secondary': '#131721', 'bg-tertiary': '#1f2430',
+ 'bg-tertiary-hover': '#272d3b', 'text-primary': '#b3b1ad', 'text-muted': '#626d7a',
+ 'border-color': '#1f2430', 'accent': '#e6b450', 'accent-hover': '#ffb454',
+ },
+ 'github_dark': {
+ 'bg-primary': '#0d1117', 'bg-secondary': '#161b22', 'bg-tertiary': '#21262d',
+ 'bg-tertiary-hover': '#30363d', 'text-primary': '#e6edf3', 'text-muted': '#8b949e',
+ 'border-color': '#30363d', 'accent': '#2f81f7', 'accent-hover': '#1f6feb',
+ },
+ 'atom_one_dark': {
+ 'bg-primary': '#282c34', 'bg-secondary': '#2c313a', 'bg-tertiary': '#3b414d',
+ 'bg-tertiary-hover': '#454b59', 'text-primary': '#abb2bf', 'text-muted': '#5c6370',
+ 'border-color': '#3b414d', 'accent': '#61afef', 'accent-hover': '#528bff',
+ },
+ 'houston': {
+ 'bg-primary': '#17191e', 'bg-secondary': '#2b2d33', 'bg-tertiary': '#34363d',
+ 'bg-tertiary-hover': '#3d3f47', 'text-primary': '#eef0f9', 'text-muted': '#79808f',
+ 'border-color': '#2b2d33', 'accent': '#acafff', 'accent-hover': '#54b9ff',
+ },
+ 'night_owl': {
+ 'bg-primary': '#011627', 'bg-secondary': '#001122', 'bg-tertiary': '#0b2942',
+ 'bg-tertiary-hover': '#123a5c', 'text-primary': '#d6deeb', 'text-muted': '#5f7e97',
+ 'border-color': '#102a44', 'accent': '#82aaff', 'accent-hover': '#6690e0',
+ },
+ 'dainty': {
+ 'bg-primary': '#1c1c22', 'bg-secondary': '#232329', 'bg-tertiary': '#2c2c34',
+ 'bg-tertiary-hover': '#35353e', 'text-primary': '#e8e6f0', 'text-muted': '#8a8894',
+ 'border-color': '#38383f', 'accent': '#c9a0dc', 'accent-hover': '#b285c7',
+ },
+ 'matcha': {
+ 'bg-primary': '#1e2320', 'bg-secondary': '#262b27', 'bg-tertiary': '#333a34',
+ 'bg-tertiary-hover': '#3d4539', 'text-primary': '#d6ddd2', 'text-muted': '#7d8a7c',
+ 'border-color': '#3d443e', 'accent': '#a3c585', 'accent-hover': '#8fb86e',
+ },
+}
+
# How each choice resolves to an actual CSS value. Keeping this server-side
# (rather than duplicating the mapping in JS) means the client just applies
# whatever /api/settings hands back.
@@ -75,6 +161,10 @@ FONT_FAMILY_CSS = {
'sans': "Arial, Helvetica, sans-serif",
'serif': "Georgia, 'Times New Roman', serif",
'monospace': "'Courier New', Consolas, monospace",
+ # Monaspace is GitHub's monospace font superfamily, not a color theme -
+ # falls back to Consolas/monospace if the font isn't installed locally,
+ # same as any other web font declaration.
+ 'monaspace': "'Monaspace Neon', 'Monaspace Argon', 'Cascadia Code', Consolas, monospace",
}
FONT_SIZE_CSS = {'small': '14px', 'medium': '16px', 'large': '18px', 'xlarge': '20px'}
LAYOUT_WIDTH_CSS = {'normal': '1000px', 'wide': '1600px', 'full': '100%'}
@@ -113,6 +203,13 @@ def save_settings(new_settings: Dict[str, Any]) -> Dict[str, Any]:
for key, value in new_settings.items():
if key == 'accent_color' and isinstance(value, str) and HEX_COLOR_RE.match(value):
current[key] = value
+ elif key == 'theme' and value in SETTINGS_CHOICES['theme']:
+ current['theme'] = value
+ # Switching to a named preset also adopts its signature accent,
+ # unless this same request is also setting accent_color
+ # explicitly (in which case that wins).
+ if 'accent_color' not in new_settings:
+ current['accent_color'] = THEME_PALETTES[value]['accent']
elif key == 'library_path' and isinstance(value, str):
value = value.strip()
if value and not os.path.isdir(value):
@@ -140,8 +237,17 @@ def save_settings(new_settings: Dict[str, Any]) -> Dict[str, Any]:
def settings_css_vars(settings: Dict[str, Any]) -> Dict[str, str]:
"""Resolve a settings dict into the actual CSS custom-property values."""
+ palette = THEME_PALETTES.get(settings['theme'], THEME_PALETTES['dark'])
return {
'--accent': settings['accent_color'],
+ '--accent-hover': palette['accent-hover'],
+ '--bg-primary': palette['bg-primary'],
+ '--bg-secondary': palette['bg-secondary'],
+ '--bg-tertiary': palette['bg-tertiary'],
+ '--bg-tertiary-hover': palette['bg-tertiary-hover'],
+ '--text-primary': palette['text-primary'],
+ '--text-muted': palette['text-muted'],
+ '--border-color': palette['border-color'],
'--font-family': FONT_FAMILY_CSS[settings['font_family']],
'--font-size-base': FONT_SIZE_CSS[settings['font_size']],
'--container-max-width': LAYOUT_WIDTH_CSS[settings['layout_width']],
@@ -737,12 +843,17 @@ def browse_library():
def settings_page():
"""Render the display-settings page."""
current = load_settings()
+ named_presets = sorted(
+ (t for t in SETTINGS_CHOICES['theme'] if t not in ('dark', 'light')),
+ key=lambda t: THEME_DISPLAY_NAMES[t]
+ )
return render_template(
'settings.html',
settings=current,
default_library_path=LIBRARY_PATH,
- theme_choices=sorted(SETTINGS_CHOICES['theme']),
- font_family_choices=['system', 'sans', 'serif', 'monospace'],
+ theme_choices=['dark', 'light'] + named_presets,
+ theme_display_names=THEME_DISPLAY_NAMES,
+ font_family_choices=['system', 'sans', 'serif', 'monospace', 'monaspace'],
font_size_choices=['small', 'medium', 'large', 'xlarge'],
layout_width_choices=['normal', 'wide', 'full'],
density_choices=['comfortable', 'compact'],
diff --git a/templates/settings.html b/templates/settings.html
index 66d5cb1..01ebcf2 100644
--- a/templates/settings.html
+++ b/templates/settings.html
@@ -233,7 +233,7 @@
@@ -276,7 +276,7 @@