diff --git a/remarkbox/__init__.py b/remarkbox/__init__.py index 6551a7e..ab9811b 100644 --- a/remarkbox/__init__.py +++ b/remarkbox/__init__.py @@ -99,6 +99,8 @@ def load_entry_points(group_name): def load_jinja2_themes(config): """Automatically load any entry_point registered Remarkbox theme.""" themes = load_entry_points("remarkbox.themes") + theme_defaults = {} + for theme_name, theme_module in themes.items(): theme_module_name = theme_module.__name__ # teach Jinja2 about the template dir in the theme package. @@ -111,6 +113,12 @@ def load_jinja2_themes(config): "{}:static/theme/{}".format(theme_module_name, theme_name), cache_max_age=3600, ) + # Collect theme's default mode if defined + if hasattr(theme_module, 'default_theme_mode'): + theme_defaults[theme_name] = theme_module.default_theme_mode + + # Store theme defaults in config registry for later access + config.registry.settings['theme_defaults'] = theme_defaults return config @@ -492,6 +500,30 @@ def main(global_config, **settings): def add_mathjax(request): return "true" if request.namespace.mathjax else "false" + def add_theme_mode(request): + """ + Return theme mode 'light' or 'dark'. + Priority: user preference > query params > theme default > 'light'. + """ + # If user is authenticated and has a preference + if request.user and request.user.authenticated and request.user.theme_mode != 'auto': + return request.user.theme_mode + + # Check if there's a mode parameter (for embeds or overrides) + param_mode = request.params.get("mode") + if param_mode in ("light", "dark"): + return param_mode + + # Use theme's default mode if available + if request.theme: + theme_defaults = request.registry.settings.get('theme_defaults', {}) + theme_default = theme_defaults.get(request.theme) + if theme_default in ("light", "dark"): + return theme_default + + # Final fallback to light + return "light" + # register functions to app config as request methods. # each request instance will run these functions and attach results. # cache result with `reify=True` to prevent multiple db lookups. @@ -546,6 +578,7 @@ def main(global_config, **settings): config.add_request_method(add_page_offset, "page_offset", reify=True) config.add_request_method(add_node_order, "node_order", reify=True) config.add_request_method(add_mathjax, "mathjax", reify=True) + config.add_request_method(add_theme_mode, "theme_mode", reify=True) # all of the web application routes. config.include(".routes") diff --git a/remarkbox/models/user.py b/remarkbox/models/user.py index 3865e45..1e194d3 100644 --- a/remarkbox/models/user.py +++ b/remarkbox/models/user.py @@ -114,6 +114,12 @@ class User(RBase, Base): default="daily", nullable=False, ) + # Theme mode preference: 'auto', 'light', or 'dark'. 'auto' respects parent site. + theme_mode = Column( + Enum('auto', 'light', 'dark', name='theme_mode_enum'), + default='auto', + nullable=False, + ) # example: cus_12345678AbCdEF but may be null. stripe_id = Column(Unicode(18), unique=True, nullable=True) diff --git a/remarkbox/scripts/alembic/versions/add_theme_mode_to_user.py b/remarkbox/scripts/alembic/versions/add_theme_mode_to_user.py new file mode 100644 index 0000000..724cc09 --- /dev/null +++ b/remarkbox/scripts/alembic/versions/add_theme_mode_to_user.py @@ -0,0 +1,34 @@ +"""add theme_mode column to rb_user + +Revision ID: b8f3c9d4e5a1 +Revises: 14a6a35940c7 +Create Date: 2025-10-11 00:00:00.000000 + +""" + +# revision identifiers, used by Alembic. +revision = 'b8f3c9d4e5a1' +down_revision = '14a6a35940c7' +branch_labels = None +depends_on = None + +from alembic import op +import sqlalchemy as sa + + +def upgrade(): + op.add_column( + 'rb_user', + sa.Column( + 'theme_mode', + sa.Enum('auto', 'light', 'dark', name='theme_mode_enum'), + nullable=False, + server_default='auto', + ), + ) + + +def downgrade(): + op.drop_column('rb_user', 'theme_mode') + # Also drop the enum type + op.execute('DROP TYPE theme_mode_enum') diff --git a/remarkbox/static/css/common.css b/remarkbox/static/css/common.css index 40bf713..ebc86be 100644 --- a/remarkbox/static/css/common.css +++ b/remarkbox/static/css/common.css @@ -1,10 +1,11 @@ -/* Variables */ +/* Variables - Light Mode (default) */ :root { --color: #222222; --color-muted: #3f3f3f; --color-code: #000000; + --background: #ffffff; --background-faint: #fafafa; --primary: #d40000; /* Remarkbox logo color */ @@ -19,6 +20,28 @@ --line-height: 1.6; } +/* Dark Mode Variables */ +:root.dark-mode { + --color: #e8e8e8; + --color-muted: #b0b0b0; + + --color-code: #e8e8e8; + + --background: #1a1a1a; + --background-faint: #2a2a2a; + + --primary: #ff5555; /* Brighter red for dark mode */ + --primary-hover: #ff7777; + --primary-inverse: #1a1a1a; + + --border: #444444; + --border-faint: #333333; + + --color-mod: #ff5555; + + --line-height: 1.6; +} + body { overflow-x: hidden; line-height: var(--line-height); @@ -26,6 +49,7 @@ body { font-family: "Raleway", "HelveticaNeue", "Helvetica Neue", Helvetica, Arial, sans-serif; color: var(--color); + background-color: var(--background); } h1, diff --git a/remarkbox/templates/base.j2 b/remarkbox/templates/base.j2 index 766d105..10f8a1c 100644 --- a/remarkbox/templates/base.j2 +++ b/remarkbox/templates/base.j2 @@ -1,7 +1,7 @@ {%- import 'snippets/snippets.j2' as snippets with context -%} {% include 'snippets/owner-key-comment.j2' %} - +