Private
Public Access
Adds a 5th "Custom" option to the theme swatch grid alongside the existing 4 presets, opening a picker for all ~12 CSS custom properties (backgrounds, borders, text, three accent tiers, highlight, danger) plus a light/dark toggle for native control rendering. Persisted as a new users.custom_theme_colors JSONB column, validated server-side against exactly what a native <input type="color"> can ever produce. Colors survive switching to a preset and back, since there's no reason picking a preset for a moment should force redoing every color pick. Applied at runtime as inline custom properties on :root (presets stay static CSS) via a shared applyTheme() helper, which is also responsible for clearing those inline overrides when switching away -- otherwise they'd silently keep winning over whatever preset's own stylesheet values should apply next. Live preview on every color change; persists only on explicit "Save colors" (not per keystroke, since a color input fires continuously while dragging), and closing the modal without saving reverts the preview back to whatever's actually persisted. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
36 lines
1.8 KiB
Python
36 lines
1.8 KiB
Python
import uuid
|
|
from datetime import datetime
|
|
|
|
from sqlalchemy import Boolean, DateTime, String, func
|
|
from sqlalchemy.dialects.postgresql import JSONB
|
|
from sqlalchemy.orm import Mapped, mapped_column
|
|
|
|
from app.models.base import Base
|
|
|
|
|
|
class User(Base):
|
|
__tablename__ = "users"
|
|
|
|
id: Mapped[uuid.UUID] = mapped_column(primary_key=True, default=uuid.uuid4)
|
|
username: Mapped[str] = mapped_column(String(50), unique=True, index=True, nullable=False)
|
|
email: Mapped[str] = mapped_column(String(255), unique=True, index=True, nullable=False)
|
|
password_hash: Mapped[str] = mapped_column(String(255), nullable=False)
|
|
is_bot: Mapped[bool] = mapped_column(Boolean, default=False, nullable=False)
|
|
is_site_admin: Mapped[bool] = mapped_column(Boolean, default=False, nullable=False)
|
|
is_active: Mapped[bool] = mapped_column(Boolean, default=True, nullable=False)
|
|
display_name: Mapped[str | None] = mapped_column(String(50))
|
|
theme: Mapped[str | None] = mapped_column(String(20))
|
|
# Only meaningful when theme == "custom" -- kept even if the user
|
|
# switches to a preset and back, so switching away from custom is never
|
|
# destructive. Shape is CustomThemeColors (backend/app/schemas/user.py).
|
|
custom_theme_colors: Mapped[dict | None] = mapped_column(JSONB)
|
|
avatar_filename: Mapped[str | None] = mapped_column(String(64))
|
|
avatar_content_type: Mapped[str | None] = mapped_column(String(50))
|
|
# Manual override for the presence indicator -- when set, this user
|
|
# always shows as offline to everyone regardless of actual connection
|
|
# state (a "lurk" mode), independent of any individual room.
|
|
appear_offline: Mapped[bool] = mapped_column(Boolean, default=False, server_default="false", nullable=False)
|
|
created_at: Mapped[datetime] = mapped_column(
|
|
DateTime(timezone=True), server_default=func.now(), nullable=False
|
|
)
|