diff --git a/CHANGELOG.md b/CHANGELOG.md index 9d51b42..b1b1cdc 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -7,6 +7,10 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ## [Unreleased] +### Added + +- Dark mode — toggle button in the header switches between light and dark palettes; preference persisted in `localStorage`; respects `prefers-color-scheme` on first visit; driven entirely by CSS custom properties on `[data-theme="dark"]` + ## [0.6.0] - 2026-05-26 ### Added diff --git a/README.md b/README.md index 977724b..0fa2769 100644 --- a/README.md +++ b/README.md @@ -48,6 +48,7 @@ The dashboard will be available at `/solid_stack` (or whatever path you choose). - **Job history view** — paginated list of all finished jobs with class name, queue, duration, and finished-at time; filterable by queue (click a badge), class substring, and time period; CSV export respects active filters - **Auto-refresh** — dashboard, jobs, processes, and history views poll automatically; pauses when the tab is hidden or a checkbox is checked; intervals configurable via `dashboard_refresh_interval` and `default_refresh_interval` - **Turbo Stream** job discard — removes the row inline without a full page reload +- **Dark mode** — toggle button in the header switches between light and dark palettes; preference persisted in `localStorage`; respects `prefers-color-scheme` on first visit ### Configuration diff --git a/ROADMAP.md b/ROADMAP.md index a8cf79a..bf32600 100644 --- a/ROADMAP.md +++ b/ROADMAP.md @@ -11,8 +11,7 @@ The path to v1.0.0 is staged: first achieve feature parity with `solid_queue_das > _Make the interface feel fast and operational, not just functional._ ### Added -- **Dark mode** — Stimulus theme controller toggles a `data-theme` attribute; CSS custom properties drive both light and dark palettes; preference persisted in `localStorage` -- **Empty-state improvements** — contextual empty states per section with actionable next steps +o- **Empty-state improvements** — contextual empty states per section with actionable next steps - **Inline notifications** — flash-style Turbo Stream feedback on bulk actions - **Responsive layout** — stats cards and tables adapt to narrow viewports - **CSS audit** — review all inline styles, consolidate utility classes, remove duplication, and enforce consistent use of CSS custom properties across all stylesheets diff --git a/app/assets/stylesheets/solid_stack_web/_02_layout.css b/app/assets/stylesheets/solid_stack_web/_02_layout.css index 9971954..d789bb9 100644 --- a/app/assets/stylesheets/solid_stack_web/_02_layout.css +++ b/app/assets/stylesheets/solid_stack_web/_02_layout.css @@ -17,6 +17,27 @@ height: 52px; } +.sqw-header__inner .sqw-nav { flex: 1; } + +.sqw-theme-toggle { + display: flex; + align-items: center; + justify-content: center; + width: 32px; + height: 32px; + padding: 0; + background: none; + border: 1px solid var(--border); + border-radius: var(--radius); + cursor: pointer; + font-size: 16px; + color: var(--text); + line-height: 1; + flex-shrink: 0; + transition: background 0.1s, border-color 0.1s; +} +.sqw-theme-toggle:hover { background: var(--bg); } + .sqw-header__logo { font-weight: 700; font-size: 16px; diff --git a/app/assets/stylesheets/solid_stack_web/_12_dark_mode.css b/app/assets/stylesheets/solid_stack_web/_12_dark_mode.css new file mode 100644 index 0000000..03bb0e4 --- /dev/null +++ b/app/assets/stylesheets/solid_stack_web/_12_dark_mode.css @@ -0,0 +1,22 @@ +[data-theme="dark"] { + --bg: #0d1117; + --surface: #161b22; + --border: #30363d; + --text: #e6edf3; + --muted: #8b949e; + --primary: #58a6ff; + --danger: #f85149; + --warning: #d29922; + --success: #3fb950; + --info: #39c5cf; + --purple: #bc8cff; + --shadow: 0 1px 3px rgba(0,0,0,.4); +} + +[data-theme="dark"] .sqw-flash--notice { background: #1b3a2b; color: #3fb950; border-color: #2d6a4f; } +[data-theme="dark"] .sqw-flash--alert { background: #3d1118; color: #f85149; border-color: #6a2030; } + +[data-theme="dark"] .sqw-theme-toggle { + border-color: var(--border); + color: var(--text); +} \ No newline at end of file diff --git a/app/javascript/solid_stack_web/application.js b/app/javascript/solid_stack_web/application.js index 3c5b90b..4b84d2c 100644 --- a/app/javascript/solid_stack_web/application.js +++ b/app/javascript/solid_stack_web/application.js @@ -4,9 +4,11 @@ import RefreshController from "solid_stack_web/refresh_controller" import SearchController from "solid_stack_web/search_controller" import SelectionController from "solid_stack_web/selection_controller" import SparklineTooltipController from "solid_stack_web/sparkline_tooltip_controller" +import ThemeController from "solid_stack_web/theme_controller" const application = Application.start() application.register("refresh", RefreshController) application.register("search", SearchController) application.register("selection", SelectionController) -application.register("sparkline-tooltip", SparklineTooltipController) \ No newline at end of file +application.register("sparkline-tooltip", SparklineTooltipController) +application.register("theme", ThemeController) \ No newline at end of file diff --git a/app/javascript/solid_stack_web/theme_controller.js b/app/javascript/solid_stack_web/theme_controller.js new file mode 100644 index 0000000..15f8e56 --- /dev/null +++ b/app/javascript/solid_stack_web/theme_controller.js @@ -0,0 +1,26 @@ +import { Controller } from "@hotwired/stimulus" + +export default class extends Controller { + static targets = ["toggle"] + + connect() { + const saved = localStorage.getItem("sqw-theme") + const preferred = window.matchMedia("(prefers-color-scheme: dark)").matches ? "dark" : "light" + this.apply(saved || preferred) + } + + toggle() { + const current = document.documentElement.getAttribute("data-theme") || "light" + const next = current === "dark" ? "light" : "dark" + localStorage.setItem("sqw-theme", next) + this.apply(next) + } + + apply(theme) { + document.documentElement.setAttribute("data-theme", theme) + if (this.hasToggleTarget) { + this.toggleTarget.textContent = theme === "dark" ? "☀" : "☽" + this.toggleTarget.setAttribute("aria-label", theme === "dark" ? "Switch to light mode" : "Switch to dark mode") + } + } +} \ No newline at end of file diff --git a/app/views/layouts/solid_stack_web/application.html.erb b/app/views/layouts/solid_stack_web/application.html.erb index 97d8c2d..6c72e06 100644 --- a/app/views/layouts/solid_stack_web/application.html.erb +++ b/app/views/layouts/solid_stack_web/application.html.erb @@ -10,7 +10,7 @@ <%= inline_styles %> <%= javascript_importmap_tags "solid_stack_web" %> - +
<%= link_to "Solid Stack", root_path, class: "sqw-header__logo" %> @@ -22,6 +22,8 @@ <%= link_to "Cable", cable_path, class: "sqw-nav__link#{" sqw-nav__link--active" if current_section == :cable}" %> +
diff --git a/config/importmap.rb b/config/importmap.rb index 8f4e779..3baec1d 100644 --- a/config/importmap.rb +++ b/config/importmap.rb @@ -5,3 +5,4 @@ pin "solid_stack_web/search_controller", to: "solid_stack_web/search_controller.js" pin "solid_stack_web/selection_controller", to: "solid_stack_web/selection_controller.js" pin "solid_stack_web/sparkline_tooltip_controller", to: "solid_stack_web/sparkline_tooltip_controller.js" +pin "solid_stack_web/theme_controller", to: "solid_stack_web/theme_controller.js"