Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
31 commits
Select commit Hold shift + click to select a range
bb5446a
refactor: migrate basic components to bootstrap theme variables
Aug 3, 2026
d882a63
refactor: migrate feature components to bootstrap theme variables
Aug 3, 2026
5f42429
fix: make icons and outline buttons readable in dark mode
Aug 3, 2026
b5d9bb7
feat: add dark mode toggle to topbar
Aug 3, 2026
6715d62
style: align theme toggle dimensions with project selector
Aug 3, 2026
b14a658
style: make theme toggle square
Aug 3, 2026
1640173
fix: theme remaining bootstrap variants and topbar spacing
Aug 3, 2026
4b48c23
feat: add dark mode support for quill editor and placeholder tiles
Aug 3, 2026
c9e588d
fix: apply theme at app level so logged-out pages respect it
Aug 3, 2026
8274c64
feat: theme alerts, setup wizard, and remaining modal surfaces
Aug 3, 2026
ea82ff9
style: theme custom scrollbars for dark mode
Aug 3, 2026
5dffc1b
refactor: move theme toggle into avatar dropdown
Aug 5, 2026
5c9ce43
feat: invert pdf page canvas in dark mode
Aug 5, 2026
400ee08
refactor: extract applyTheme to shared util
Aug 5, 2026
9ae1d7a
style: remove duplicate btn-light override
Aug 5, 2026
2b2551f
fix: reapply theme when user settings load
Aug 5, 2026
fca5dae
Merge remote-tracking branch 'origin/dev' into feat-212-dark-mode
Aug 5, 2026
d14666b
refactor: derive theme state from store and simplify overrides
Aug 5, 2026
faec2d2
fix: tie color-scheme to the selected theme so native controls match
Aug 5, 2026
b6330b6
fix: disable pdf inversion when printing
Aug 6, 2026
84b3ea0
style: lighten annotator icons and use outline variants
Aug 7, 2026
0a8771d
fix: give pdf page separator an explicit colour and width
Aug 7, 2026
0c660c6
feat: add per-document light mode toggle to pdf toolbar
Aug 7, 2026
d380c43
style: use care yellow as the dark mode accent colour
Aug 7, 2026
b3f9061
fix: keep toast close button visible in dark mode
Aug 7, 2026
7666e20
refactor: centralise dark mode accent colours and persist pdf theme c…
Aug 7, 2026
5e1123e
refactor: keep topbar theming in its own component
Aug 7, 2026
dfeab50
style: make annotation highlights visible over dark pdf pages
Aug 9, 2026
6319f4e
style: deepen the dark mode accent colour
Aug 9, 2026
a82a260
style: reduce the blue cast on dark pdf pages
Aug 9, 2026
95010ef
feat: make pdf theme independent of the app theme
Aug 9, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
17 changes: 17 additions & 0 deletions frontend/src/App.vue
Original file line number Diff line number Diff line change
Expand Up @@ -41,6 +41,7 @@
<script>
import Toast from "@/basic/Toast.vue";
import TopBar from "@/basic/navigation/Topbar.vue";
import { applyTheme } from "@/assets/utils";
import Loader from "@/basic/Loading.vue";
import {createTable} from "@/store/utils";
import axios from "axios";
Expand Down Expand Up @@ -191,6 +192,15 @@
}
},
watch: {
"$store.state.settings": {
handler() {
const saved = this.$store.getters["settings/getValue"]("app.theme.mode");
if (saved) {
applyTheme(saved);
}
},
deep: true,
},
$route(to, from) {
if (to.meta && to.meta.checkLogin) {
this.runCheckLoginFlow();
Expand Down Expand Up @@ -232,6 +242,7 @@
this.connect();
},
async mounted() {
this.initTheme();
if (this.$route.meta.checkLogin) {
await this.runCheckLoginFlow();
}
Expand All @@ -242,6 +253,12 @@
}
},
methods: {
initTheme() {
const saved = this.$store.getters["settings/getValue"]("app.theme.mode");
const cached = localStorage.getItem("care.theme");
const osPrefersDark = window.matchMedia("(prefers-color-scheme: dark)").matches;
applyTheme(saved || cached || (osPrefersDark ? "dark" : "light"));
},
async runCheckLoginFlow() {
const response = await axios.get(getServerURL() + "/auth/check", {
withCredentials: true,
Expand Down Expand Up @@ -292,7 +309,7 @@
this.$refs.consentModal.open();
}
return;
}

Check warning on line 312 in frontend/src/App.vue

View workflow job for this annotation

GitHub Actions / lint

This file has 353 lines of code. Keep files below 300 lines where practical

if (this.shouldForceTwoFactorSetup) {
if (this.$refs.consentModal?.isVisible()) {
Expand Down
201 changes: 197 additions & 4 deletions frontend/src/assets/styles/global.css
Original file line number Diff line number Diff line change
@@ -1,11 +1,11 @@
.scrollable-error-container {
color: firebrick;
color: var(--bs-danger-text-emphasis);
max-height: 180px;
overflow-y: auto;
padding: 10px;
border: 1px solid #ffcdd2;
border-radius: 4px;
background-color: #ffebee;
border: 1px solid var(--bs-danger-border-subtle);
border-radius: 4px !important;
background-color: var(--bs-danger-bg-subtle);

> p {
margin-bottom: 0.5rem;
Expand All @@ -21,3 +21,196 @@
line-height: 1.4;
}
}

/* ===== Dark mode =====
Bootstrap switches themes when data-bs-theme="dark" is set on <html>. Most of
CARE follows automatically, but three things need help here:
1. A few Bootstrap classes have a fixed colour built in and stay light.
2. Dark mode uses CARE's logo yellow as the accent instead of Bootstrap's
blue, with dark text on top since the yellow is too light for white.
Bootstrap sets button and form-control colours per component, so each
family needs its own override rather than one variable change.
3. Quill, the PDF viewer and bootstrap-vue-3 come from other libraries and
do not follow the theme at all. */

html[data-bs-theme="dark"] {
--care-accent: #e8cd7d;
--care-accent-hover: #dcbf68;
--care-accent-active: #cfb055;
--care-on-accent: #212529;
}

/* Tell the browser which theme is showing. Bootstrap declares
color-scheme: light dark, which lets the browser pick scrollbar and native
control colours from the operating system rather than from CARE. The html
prefix beats Bootstrap's own rule on specificity. */
html[data-bs-theme="light"] {
color-scheme: light;
}

html[data-bs-theme="dark"] {
color-scheme: dark;
}

/* --- Bootstrap classes with a fixed colour --- */

[data-bs-theme="dark"] .btn-outline-secondary,
[data-bs-theme="dark"] .btn-outline-dark {
--bs-btn-color: var(--bs-body-color);
--bs-btn-border-color: var(--bs-border-color);
--bs-btn-hover-bg: var(--bs-secondary-bg);
--bs-btn-hover-color: var(--bs-body-color);
--bs-btn-hover-border-color: var(--bs-border-color);
}

[data-bs-theme="dark"] .btn-light {
--bs-btn-bg: var(--bs-secondary-bg);
--bs-btn-color: var(--bs-body-color);
--bs-btn-border-color: var(--bs-border-color);
--bs-btn-hover-bg: var(--bs-tertiary-bg);
--bs-btn-hover-color: var(--bs-body-color);
--bs-btn-hover-border-color: var(--bs-border-color);
}

[data-bs-theme="dark"] .btn-dark {
--bs-btn-bg: var(--bs-secondary-bg);
--bs-btn-color: var(--bs-body-color);
--bs-btn-border-color: var(--bs-border-color);
}

[data-bs-theme="dark"] .text-dark {
color: var(--bs-body-color) !important;
}

/* Disabled controls lose more contrast against dark than against white. */
[data-bs-theme="dark"] .btn:disabled,
[data-bs-theme="dark"] .nav-link.disabled {
opacity: 0.85;
}

/* --- Accent colour --- */

[data-bs-theme="dark"] .btn-primary {
--bs-btn-bg: var(--care-accent);
--bs-btn-border-color: var(--care-accent);
--bs-btn-color: var(--care-on-accent);
--bs-btn-hover-bg: var(--care-accent-hover);
--bs-btn-hover-border-color: var(--care-accent-hover);
--bs-btn-hover-color: var(--care-on-accent);
--bs-btn-active-bg: var(--care-accent-active);
--bs-btn-active-border-color: var(--care-accent-active);
--bs-btn-active-color: var(--care-on-accent);
}


[data-bs-theme="dark"] .page-item.active .page-link {
background-color: var(--care-accent);
border-color: var(--care-accent);
color: var(--care-on-accent);
}

[data-bs-theme="dark"] .form-check-input:checked {
background-color: var(--care-accent);
border-color: var(--care-accent);
}

[data-bs-theme="dark"] .form-check-input:focus {
border-color: var(--care-accent);
box-shadow: 0 0 0 0.25rem rgba(232, 205, 125, 0.25);
}

/* The switch knob is a background image, so each state needs its own SVG. */
[data-bs-theme="dark"] .form-switch .form-check-input:focus {
--bs-form-switch-bg: url("data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' viewBox='-4 -4 8 8'%3e%3ccircle r='3' fill='%23e8cd7d'/%3e%3c/svg%3e");
}

[data-bs-theme="dark"] .form-switch .form-check-input:checked,
[data-bs-theme="dark"] .form-switch .form-check-input:checked:focus {
--bs-form-switch-bg: url("data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' viewBox='-4 -4 8 8'%3e%3ccircle r='3' fill='%23212529'/%3e%3c/svg%3e");
}

/* --- Quill editor --- */

[data-bs-theme="dark"] .ql-toolbar.ql-snow,
[data-bs-theme="dark"] .ql-container.ql-snow {
border-color: var(--bs-border-color);
}

[data-bs-theme="dark"] .ql-toolbar.ql-snow {
background-color: var(--bs-tertiary-bg);
}

[data-bs-theme="dark"] .ql-container.ql-snow,
[data-bs-theme="dark"] .ql-editor {
background-color: var(--bs-body-bg);
color: var(--bs-body-color);
}

[data-bs-theme="dark"] .ql-editor.ql-blank::before {
color: var(--bs-secondary-color);
}

/* Quill draws its toolbar icons as SVG, so they need stroke and fill. */
[data-bs-theme="dark"] .ql-snow .ql-stroke {
stroke: var(--bs-body-color);
}

[data-bs-theme="dark"] .ql-snow .ql-fill {
fill: var(--bs-body-color);
}

[data-bs-theme="dark"] .ql-snow .ql-picker {
color: var(--bs-body-color);
}

[data-bs-theme="dark"] .ql-snow .ql-picker-options {
background-color: var(--bs-body-bg);
border-color: var(--bs-border-color);
}

/* --- Toasts --- */

/* bootstrap-vue-3 hardcodes light toast colours, so the close button must not be
inverted to white the way Bootstrap does for dark backgrounds. */
[data-bs-theme="dark"] .b-toast .btn-close {
filter: none;
}

/* --- PDF viewer --- */

/* A black drop shadow is invisible on dark, so define the edge with a border. */
[data-bs-theme="dark"] .pdf-toolbar {
border: 1px solid var(--bs-border-color);
}

/* Invert the rendered page, then tint it cool so it sits with the rest of the
dark UI rather than looking warm. Skipped when the reader has switched an
individual document back to light from the toolbar. */
.scrolling-page.pdf-dark canvas.pdf-page {
filter: invert(0.88) grayscale(0.4) sepia(0.15) hue-rotate(185deg) saturate(2.2);
}

/* Printing a dark page wastes ink and reads badly, so never invert for print. */
@media print {
canvas.pdf-page {
filter: none !important;
}
}

/* A pure white page glares against the dark UI when the reader switches an
individual document back to light, so soften it. Brightness scales all
channels equally, which keeps figure colours accurate. */
[data-bs-theme="dark"] .scrolling-page:not(.pdf-dark) canvas.pdf-page {
filter: brightness(0.93);
}

/* Annotation highlights are SVG rects blended with multiply, which works like a
marker pen on white paper but produces almost nothing over a dark page.
Screen lightens instead of darkens, so the band stays visible. */
.scrolling-page.pdf-dark svg.highlight-layer {
mix-blend-mode: screen !important;
}

.scrolling-page.pdf-dark .svg-highlight {
opacity: 0.4 !important;
}
15 changes: 14 additions & 1 deletion frontend/src/assets/utils.js
Original file line number Diff line number Diff line change
Expand Up @@ -369,4 +369,17 @@ export async function extractTextFromPDF(pdfDocument) {
}

return fullText;
}
}

/**
* Applies a colour theme to the document root and caches it locally so the
* correct theme is used on first paint before user settings load.
*
* @param mode the theme to apply, "light" or "dark"
* @returns {void}
*/
export function applyTheme(mode) {
document.documentElement.setAttribute("data-bs-theme", mode);
localStorage.setItem("care.theme", mode);
}

10 changes: 5 additions & 5 deletions frontend/src/auth/SetupWizard.vue
Original file line number Diff line number Diff line change
Expand Up @@ -316,7 +316,7 @@
}
}
if (config.data && Array.isArray(config.data.allSettings)) {
this.allSettingsFromDb = config.data.allSettings;

Check warning on line 319 in frontend/src/auth/SetupWizard.vue

View workflow job for this annotation

GitHub Actions / lint

This file has 643 lines of code. Keep files below 300 lines where practical
}
},
methods: {
Expand Down Expand Up @@ -636,12 +636,12 @@
left: 0;
right: 0;
height: 2px;
background-color: #ccc;
background-color: var(--bs-secondary-bg, #ccc);
}

.stepper > div {
z-index: 1;
background-color: white;
background-color: var(--bs-body-bg, white);
padding: 0 5px;
}

Expand All @@ -655,13 +655,13 @@
border-radius: 50%;
align-items: center;
justify-content: center;
border: 1px solid #6c6b6b;
border: 1px solid var(--bs-border-color, #6c6b6b);
}

.stepper > div.active::before {
color: white;
background-color: #0d6efd;
border-color: #0d6efd;
background-color: var(--bs-primary, #0d6efd);
border-color: var(--bs-primary, #0d6efd);
}

.feedback-invalid {
Expand Down
8 changes: 4 additions & 4 deletions frontend/src/auth/TwoFactorSelect.vue
Original file line number Diff line number Diff line change
Expand Up @@ -232,13 +232,13 @@ form {
}

.method-card:hover {
background-color: #f8f9fa;
border-color: #0d6efd;
background-color: var(--bs-tertiary-bg, #f8f9fa);
border-color: var(--bs-primary, #0d6efd);
}

.method-card.selected {
border-color: #0d6efd;
background-color: #e7f1ff;
border-color: var(--bs-primary, #0d6efd);
background-color: var(--bs-primary-bg-subtle, #e7f1ff);
}
</style>

2 changes: 1 addition & 1 deletion frontend/src/auth/TwoFactorSettingsModal.vue
Original file line number Diff line number Diff line change
Expand Up @@ -323,7 +323,7 @@
} else {
await this.disableEmail2FA();
}
},

Check warning on line 326 in frontend/src/auth/TwoFactorSettingsModal.vue

View workflow job for this annotation

GitHub Actions / lint

This file has 639 lines of code. Keep files below 300 lines where practical
async enableEmail2FA() {
if (!this.hasEmail) {
this.eventBus.emit("toast", {
Expand Down Expand Up @@ -659,7 +659,7 @@
}

.totp-setup-section {
background-color: #f8f9fa;
background-color: var(--bs-tertiary-bg, #f8f9fa);
padding: 16px;
border-radius: 8px;
border: 1px solid #dee2e6;
Expand Down
Loading
Loading