Skip to content

feat: add dark mode with system preference detection#854

Closed
Alain-L wants to merge 8 commits intodalibo:masterfrom
Alain-L:feature/dark-mode
Closed

feat: add dark mode with system preference detection#854
Alain-L wants to merge 8 commits intodalibo:masterfrom
Alain-L:feature/dark-mode

Conversation

@Alain-L
Copy link

@Alain-L Alain-L commented Dec 12, 2025

Summary

  • Add dark mode support with toggle button, localStorage persistence, and system preference detection
  • Refactor SCSS to use Bootstrap CSS custom properties for native dark mode support
  • Add dark mode syntax highlighting for highlight.js

Features

  • 🌙 Toggle button in navbar (sun/moon icons)
  • 💾 Theme preference persisted in localStorage
  • 🖥️ Respects system prefers-color-scheme on first visit

Technical details

  • Replace hardcoded SCSS variables with Bootstrap CSS vars (--bs-body-bg, --bs-body-color, --bs-border-color, etc.)
  • useTheme composable handles theme state and DOM updates
  • Bootstrap dark mode via data-bs-theme attribute handles 100% of theming
  • Conditional import of stackoverflow-dark theme for highlight.js

For integrators

Just set data-bs-theme="dark" on your root element — pev2 adapts automatically.

Test plan

  • Unit tests pass (159 tests)
  • No visual regression in light mode (verified with Playwright)
  • Manual testing: toggle works, preference persists across refresh
  • Manual testing: system preference detection on fresh browser

Edit 2025-12-24: Updated description to reflect the simplified approach after Pierre's feedback.

@pgiraud
Copy link
Member

pgiraud commented Dec 19, 2025

I gave this PR a quick review.

I have he feeling that too many colors are set in our css files. In my opinion, setting the theme theme to dark using the data-bs-theme attribute in the <body> should do 95% of the job. This is probably a legacy from when we were using an older version of bootstrap. I think it would be better if we could first get rid of any custom colors and replace them by either bootstrap defined variables or classes. This would be a painful but useful job. I can probably help with that.

Also, the dark mode variables shouldn't be set in the example directory otherwise the colors won't be changed if PEV2 is integrated (ie. used outside the example application).

Could you please create a PR with only the playwright e2e tests (and without the husky files) ? I'd be happy to have this integrated sooner.

{ plan: testPlan.plan, query: testPlan.query, name: testPlan.name }
)

// Wait for plan view to render (has nav-link tabs)
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The " (has nav-link tabs)" part doesn't seem accurate with th e code below. Let's remove it.

@click="toggleTheme"
:title="theme === 'light' ? 'Switch to dark mode' : 'Switch to light mode'"
>
<FontAwesomeIcon :icon="theme === 'light' ? faMoon : faSun" />
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

In my opinion, this would be better the other way around. ie. the icon should show the current state.

@Alain-L
Copy link
Author

Alain-L commented Dec 19, 2025

Thanks for the review.

I've addressed your points:

  1. Bootstrap native dark mode: Removed the 265-line dark-mode.css that was overriding Bootstrap components. Now data-bs-theme="dark" should handle buttons, cards, forms, tables, etc. natively. Only ~90 lines remain for pev2-specific components (plan nodes, splitpanes, highlight.js).

  2. Styles in library: Moved dark mode styles from example/ to src/assets/scss/_dark-mode.scss so they're included in the library build.

  3. E2E tests: Removed from this PR. I'll create a dedicated PR for Playwright tests.

The branch has been force-pushed with these changes. Net result: -170 lines of CSS.

Happy to adjust anything based on your feedback.

Comment on lines 27 to 29
document.documentElement.setAttribute("data-theme", theme)
// Bootstrap 5 dark mode
document.documentElement.setAttribute("data-bs-theme", theme)
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think it would be more straight forward to rely on one attribute only: data-bs-theme. I don't see any additional value to have a pev2 custom attribute for that.

Imagine an app integrating PEV2 and using bootstrap. If it already has a theme switch, it wouldn't have to handle the extra specific data-theme value.

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks for the suggestion.

I've made the following changes:

  1. Single attribute: Now using only data-bs-theme instead of both data-theme and data-bs-theme. Apps integrating pev2 can control the theme using the standard Bootstrap attribute.

  2. Bootstrap-aligned palette: Replaced custom purple-tinted colors with Bootstrap's neutral grays (#212529, #343a40, etc.) for visual consistency.

  3. Semantic classes: Replaced bg-white/bg-light with bg-body/bg-body-tertiary which automatically adapt to light/dark mode. Also removed table-secondary from table headers.

The dark mode now relies almost entirely on Bootstrap's native theming.

@Alain-L
Copy link
Author

Alain-L commented Dec 24, 2025

Continued simplification:

Complete refactor from SCSS variables to Bootstrap CSS vars

Replaced all hardcoded SCSS variables ($gray-light, $text-color, $border-color, etc.) with Bootstrap CSS custom properties (--bs-body-bg, --bs-body-color, --bs-border-color, etc.).

Result: no need for _dark-mode.scss anymore. data-bs-theme="dark" now handles 100% of the theming for pev2 components.

3 commits:

  1. refactor(ui): use Bootstrap CSS variables - 12 SCSS files, variable replacements
  2. feat(example): add dark mode toggle - toggle in demo app only
  3. feat(ui): add dark mode syntax highlighting - conditional import of stackoverflow-dark for highlight.js (4 lines)

For integrators: just set data-bs-theme="dark" on the root element, pev2 adapts automatically.

Verified no regression in light mode with Playwright (master vs feature branch comparison).

@pgiraud
Copy link
Member

pgiraud commented Dec 25, 2025

Excellent work so far. Instead of adding a "master" into branch merge commit, could you please try to rebase your branch instead? I'd prefer we avoid merge commits.

alesage added 2 commits December 26, 2025 09:48
Replace hardcoded SCSS color variables with Bootstrap CSS variables
to enable theme customization (dark mode, high contrast, branding, etc.)

- Use semantic classes (bg-body, text-body-secondary, etc.)
- Replace $gray-light, $text-color, $line-color with CSS variables
- Update hatched background pattern to use --bs-tertiary-bg
- Add splitpanes theme overrides
- Replace btn-light with btn-outline-secondary in PlanNode
Add theme switcher button in navbar with system preference detection.
Theme preference is persisted in localStorage.
Import stackoverflow-dark theme for highlight.js when dark mode is active,
scoped to [data-bs-theme="dark"] selector.
@Alain-L
Copy link
Author

Alain-L commented Dec 26, 2025

Done! Rebased on master, no merge commit anymore.

3 clean commits:

  • 22f4a44 refactor(ui): use Bootstrap CSS variables for theming flexibility
  • 4b62f3e feat(example): add dark mode toggle
  • a93362c feat(ui): add dark mode syntax highlighting

Build and tests pass (159 tests).

alesage added 3 commits January 6, 2026 10:22
Replace emoji icons with FontAwesome sun/moon icons for consistency
with the rest of the example application.
Replace custom theme implementation with VueUse's useColorMode composable.
Reduces code while maintaining the same functionality.
@Alain-L
Copy link
Author

Alain-L commented Jan 6, 2026

Done — switched to FontAwesome icons, VueUse useColorMode, and inverted icon logic.

@pgiraud
Copy link
Member

pgiraud commented Jan 6, 2026

In the main view, I don't really like the new bordered node type.

Untitled

May I suggest this change?


diff --git c/src/components/PlanNode.vue i/src/components/PlanNode.vue                         
index aee7534..3f0d36b 100644                                                                  
--- c/src/components/PlanNode.vue                                                              
+++ i/src/components/PlanNode.vue                                                              
@@ -137,10 +137,10 @@ function centerCte() {                                                   
         <div class="card-body header no-focus-outline">                                       
           <header class="mb-0 d-flex justify-content-between">                                
             <h4                                                                               
-              class="text-body overflow-hidden btn btn-outline-secondary text-start py-0 px-1"
+              class="bg-body-tertiary overflow-hidden btn text-start py-0 px-1"               
               @click.prevent.stop="showDetails = !showDetails"                                
             >                                                                                 
-              <span class="text-secondary">                                                   
+              <span class="text-body-tertiary">                                               
                 <FontAwesomeIcon                                                              
                   fixed-width                                                                 
                   :icon="faChevronUp"                                                         

Also the reserved words like "as", "using", … now use the text-secondary but I feel like the difference with text-body is small. By using text-body-tertiary in place of text-secondary would make it look like what it was previously.

- Remove border from node type button (bg-body-tertiary instead of btn-outline-secondary)
- Use text-body-tertiary for chevron icon and reserved keywords (on, as, by, using, CTE)
@Alain-L
Copy link
Author

Alain-L commented Jan 6, 2026

You're right, the bordered button was a regression. Applied your suggestions to restore the original look:

  • Node type button: bg-body-tertiary instead of btn-outline-secondary
  • Reserved keywords: text-body-tertiary instead of text-secondary

@pgiraud
Copy link
Member

pgiraud commented Jan 6, 2026

Can you also change the "reserved keywords" color in the grid?

@Alain-L
Copy link
Author

Alain-L commented Jan 6, 2026

Done — applied to grid as well.

@pgiraud
Copy link
Member

pgiraud commented Jan 7, 2026

As discussed earlier, here are some possible enhancements (initial refactoring):

  • remove overridden .text-secondary rules in plan-node.scss and plan.scss,
  • clean the _variables.scss files and remove unrequired variables,
  • use text-body-x classes as much as possible (this makes the dark mode more proof to colors switch, by relying on opacity),
  • use tertiary in place of secondary when a lighter gray is required,
  • keep the light gray for the background color for the split panes.

@pgiraud
Copy link
Member

pgiraud commented Jan 16, 2026

Thanks for your contribution. Integrated in #866 .

@Alain-L
Copy link
Author

Alain-L commented Jan 16, 2026

Closing in favor of #866 which incorporates this work with additional CSS refactoring.

@Alain-L Alain-L closed this Jan 16, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants