Added dark mode

This commit is contained in:
defnax 2026-08-27 19:50:58 +02:00
parent df9cd7d24e
commit 5c6e2f738e
8 changed files with 1005 additions and 2 deletions

View File

@ -0,0 +1,39 @@
const m = require('mithril');
const theme = require('theme');
const choices = [
{ value: 'system', icon: 'fa-adjust', title: 'System', description: 'Follow your device appearance setting.' },
{ value: 'light', icon: 'fa-sun', title: 'Light', description: 'Always use the light appearance.' },
{ value: 'dark', icon: 'fa-moon', title: 'Dark', description: 'Always use the dark appearance.' },
];
const Appearance = {
view: () => m('.widget.config-appearance', [
m('.widget__heading', m('h3', 'Appearance')),
m('.widget__body', [
m('p.config-appearance__intro', 'Choose how RetroShare WebUI looks on this device.'),
m('fieldset.theme-choice', [
m('legend', 'Theme'),
choices.map((choice) => {
const selected = theme.getMode() === choice.value;
return m('label.theme-choice__option', { class: selected ? 'is-selected' : '' }, [
m('input[type=radio][name=webui-theme]', {
value: choice.value,
checked: selected,
onchange: () => theme.setMode(choice.value),
}),
m(`i.fas.${choice.icon}[aria-hidden=true]`),
m('.theme-choice__copy', [
m('strong', choice.title),
m('span', choice.description),
]),
selected ? m('i.fas.fa-check.theme-choice__check[aria-hidden=true]') : null,
]);
}),
]),
m('p.config-appearance__note', 'This preference is stored only in this browser.'),
]),
]),
};
module.exports = Appearance;

View File

@ -2,6 +2,7 @@ const m = require('mithril');
const widget = require('widgets');
const sections = {
appearance: require('config/config_appearance'),
network: require('config/config_network'),
node: require('config/config_node'),
services: require('config/config_services'),

View File

@ -1,4 +1,5 @@
const m = require('mithril');
const theme = require('theme');
const login = require('login');
const rs = require('rswebui');
@ -115,6 +116,7 @@ const navbar = () => {
},
},
[
m(theme.ThemeToggle),
m(
'.nav-menu__status',
{
@ -297,6 +299,7 @@ const MobileNavigation = () => {
}, [m(navIcon[name]), m('span', name.charAt(0).toUpperCase() + name.slice(1))])
)),
m('.mobile-more-sheet__actions', [
m(theme.ThemeToggle),
m('button[type=button]', { onclick: () => window.location.reload(true) }, [m('i.fas.fa-sync-alt'), ' Reload']),
m('button[type=button]', { onclick: () => rs.logout() }, [m('i.fas.fa-sign-out-alt'), ' Logout']),
]),
@ -378,7 +381,7 @@ const Layout = () => {
m.route(document.getElementById('main'), '/', {
'/': {
render: () => m(login),
render: () => [m('.login-theme-control', m(theme.ThemeToggle)), m(login)],
},
'/home': {
render: () => m(Layout, m(home)),

View File

@ -0,0 +1,870 @@
/* Runtime light/dark theme. Keep this file last in main.scss so these semantic
colour rules can also cover legacy page styles while they are migrated. */
:root {
color-scheme: light;
--rs-bg: #eef3f6;
--rs-surface: #ffffff;
--rs-surface-raised: #ffffff;
--rs-surface-muted: #f8fafc;
--rs-surface-hover: #f1f5f9;
--rs-text: #1e293b;
--rs-text-strong: #0f172a;
--rs-text-muted: #64748b;
--rs-border: #cbd5e1;
--rs-border-soft: #e2e8f0;
--rs-accent: #3ba4d7;
--rs-accent-soft: #e0f2fe;
--rs-overlay: rgba(15, 23, 42, 0.45);
--rs-shadow: rgba(15, 23, 42, 0.16);
}
:root[data-theme='dark'] {
color-scheme: dark;
--rs-bg: #0f172a;
--rs-surface: #172033;
--rs-surface-raised: #1e293b;
--rs-surface-muted: #111c2e;
--rs-surface-hover: #263449;
--rs-text: #dbe5f1;
--rs-text-strong: #f8fafc;
--rs-text-muted: #94a3b8;
--rs-border: #475569;
--rs-border-soft: #334155;
--rs-accent: #38bdf8;
--rs-accent-soft: #0c4a6e;
--rs-overlay: rgba(2, 6, 23, 0.72);
--rs-shadow: rgba(2, 6, 23, 0.55);
}
body {
background: var(--rs-bg);
color: var(--rs-text);
}
.theme-toggle {
display: inline-flex;
align-items: center;
justify-content: center;
gap: .45rem;
min-height: 2.25rem;
padding: .4rem .65rem;
border: 1px solid var(--rs-border-soft);
border-radius: .5rem;
background: var(--rs-surface-muted);
color: var(--rs-text);
box-shadow: none;
text-transform: none;
&:hover { background: var(--rs-surface-hover); }
&--compact { width: 2.5rem; padding: .4rem; }
}
.login-theme-control {
position: fixed;
top: max(1rem, env(safe-area-inset-top));
right: 1rem;
z-index: 20;
}
.config-appearance__intro {
margin: .25rem 0 1rem;
color: var(--rs-text-muted);
}
.theme-choice {
display: grid;
width: min(100%, 46rem);
margin: 0;
padding: 0;
gap: .65rem;
border: 0;
legend {
margin-bottom: .65rem;
color: var(--rs-text-strong);
font-weight: 700;
}
&__option {
display: grid;
grid-template-columns: auto 1.4rem minmax(0, 1fr) auto;
align-items: center;
min-height: 4.25rem;
padding: .75rem 1rem;
gap: .8rem;
border: 1px solid var(--rs-border-soft);
border-radius: .65rem;
background: var(--rs-surface-muted);
color: var(--rs-text);
cursor: pointer;
&:hover { background: var(--rs-surface-hover); }
&.is-selected {
border-color: var(--rs-accent);
box-shadow: 0 0 0 1px var(--rs-accent);
}
> i { color: var(--rs-text-muted); text-align: center; }
}
&__copy {
display: flex;
flex-direction: column;
gap: .2rem;
strong { color: var(--rs-text-strong); }
span { color: var(--rs-text-muted); font-size: .875rem; }
}
&__check { color: var(--rs-accent) !important; }
}
.config-appearance__note {
margin-top: 1rem;
color: var(--rs-text-muted);
font-size: .8rem;
}
.nav-menu .theme-toggle {
width: 100%;
color: #cbd5e1;
background: rgba(255, 255, 255, .06);
border-color: rgba(255, 255, 255, .12);
}
.nav-menu.collapsed .theme-toggle {
width: 2.5rem;
span { display: none; }
}
:root[data-theme='dark'] {
#main,
.content,
.main-container,
.tab-content,
.node-panel { color: var(--rs-text); }
.tab-content { background-color: var(--rs-bg); }
.widget,
.sidebar,
.card,
.modal,
.modal-content,
.popup,
.dropdown-content,
.collection,
.mobile-bottom-nav,
.mobile-more-sheet,
.mobile-status-sheet {
background-color: var(--rs-surface);
color: var(--rs-text);
border-color: var(--rs-border-soft);
}
h1, h2, h3, h4, h5, h6,
label, legend, p, li, td, th, strong { color: inherit; }
a:not(.nav-menu .item) { color: var(--rs-accent); }
small, .date, .subtitle, .description { color: var(--rs-text-muted); }
hr,
table th,
table tr,
.widget__heading { border-color: var(--rs-border-soft); }
table { color: var(--rs-text); }
table th { color: var(--rs-text-strong); }
input:not([type='checkbox']):not([type='radio']):not([type='range']),
textarea,
select,
[contenteditable='true'] {
background-color: var(--rs-surface-muted) !important;
color: var(--rs-text-strong) !important;
border-color: var(--rs-border) !important;
}
input::placeholder,
textarea::placeholder { color: var(--rs-text-muted); opacity: 1; }
input:disabled,
textarea:disabled,
select:disabled {
background-color: #1b2638 !important;
color: #7f8da3 !important;
}
.sidebar a,
.sidebarquickview a { color: var(--rs-text-muted); }
.sidebar a:hover,
.sidebarquickview a:hover,
.selected-sidebar-link,
.selected-sidebarquickview-link { color: var(--rs-text-strong); }
.selected-sidebar-link,
.selected-sidebarquickview-link,
.active,
.selected { background-color: var(--rs-accent-soft); }
.mobile-bottom-nav {
border-color: var(--rs-border-soft);
box-shadow: 0 -4px 14px var(--rs-shadow);
}
.mobile-bottom-nav__item { color: var(--rs-text-muted); }
.mobile-bottom-nav__item.active { color: var(--rs-accent); }
.mobile-bottom-nav__item .nav-unread-badge { border-color: var(--rs-surface); }
.mobile-more-overlay,
.mobile-status-overlay { background: var(--rs-overlay); }
.mobile-more-sheet__links a,
.mobile-status-sheet__item {
background: var(--rs-surface-muted);
color: var(--rs-text);
border-color: var(--rs-border-soft);
}
.mobile-more-sheet__links a.active {
background: var(--rs-accent-soft);
color: #7dd3fc;
}
.mobile-status-sheet__item span,
.mobile-status-sheet__item small,
.mobile-status-sheet__version { color: var(--rs-text-muted); }
.tooltip { color: var(--rs-text); }
.tooltiptext { background: #020617; color: #e2e8f0; }
/* Common legacy utility colours used throughout page-specific Sass. */
[style*='background: #fff'],
[style*='background: #ffffff'],
[style*='background-color: #fff'],
[style*='background-color: #ffffff'] { background-color: var(--rs-surface) !important; }
[style*='background: #f8fafc'],
[style*='background-color: #f8fafc'],
[style*='background: #f1f5f9'],
[style*='background-color: #f1f5f9'] { background-color: var(--rs-surface-muted) !important; }
[style*='color: #0f172a'],
[style*='color: #1e293b'],
[style*='color: #334155'] { color: var(--rs-text-strong) !important; }
[style*='color: #475569'],
[style*='color: #64748b'],
[style*='color: #94a3b8'] { color: var(--rs-text-muted) !important; }
@media (max-width: 700px) {
.sidebar { background: var(--rs-surface) !important; }
.sidebar-drawer .sidebar {
background: var(--rs-surface) !important;
border-color: var(--rs-border-soft) !important;
box-shadow: 8px 0 24px var(--rs-shadow);
}
.sidebar-drawer .sidebar a { color: var(--rs-text) !important; }
.sidebar-drawer .sidebar .selected-sidebar-link {
background: var(--rs-accent-soft);
color: var(--rs-text-strong) !important;
}
.sidebar-mobile-toggle {
background: var(--rs-surface);
color: var(--rs-text-strong);
}
}
}
@media (prefers-reduced-motion: reduce) {
:root { scroll-behavior: auto; }
*, *::before, *::after {
animation-duration: .01ms !important;
animation-iteration-count: 1 !important;
transition-duration: .01ms !important;
}
}
/* Page shells use highly specific legacy declarations (many with !important),
so keep their dark treatment explicit instead of depending on generic cards. */
:root[data-theme='dark'] {
.network-container,
.people-container,
.chat-hub-container {
background: var(--rs-bg) !important;
color: var(--rs-text) !important;
}
.network-left-pane,
.people-left-pane,
.chat-hub-left-pane,
.network-tabs,
.chat-hub-header-bar,
.chat-hub-tabs-container,
.chat-hub-rightbar {
background: var(--rs-surface) !important;
color: var(--rs-text) !important;
border-color: var(--rs-border-soft) !important;
box-shadow: none !important;
}
.network-right-pane,
.people-right-pane,
.chat-hub-right-pane,
.network-tab-content,
.chat-hub-tab-content,
.chat-hub-messages,
.chat-hub-conversation-main {
background: var(--rs-bg) !important;
color: var(--rs-text) !important;
}
.own-profile-card,
.chat-own-profile-card,
.people-sidebar-header {
background: var(--rs-surface-muted) !important;
color: var(--rs-text) !important;
border-color: var(--rs-border-soft) !important;
}
.profile-name,
.friend-name,
.room-name,
.chat-name,
.chat-header-name,
.detail-title h2,
.detail-section .info-value,
.participant-name,
.identity-name {
color: var(--rs-text-strong) !important;
}
.friend-status,
.room-topic,
.chat-last-msg,
.chat-time,
.rooms-section-title,
.chat-header-topic,
.detail-subtitle,
.detail-section .info-label,
.rightbar-title {
color: var(--rs-text-muted) !important;
}
.friend-list-item:hover,
.chat-item:hover,
.chat-room-list-item:hover,
.chat-hub-rightbar .user:hover,
.network-tabs .tab-btn:hover,
.chat-hub-tabs .tab-btn:hover {
background: var(--rs-surface-hover) !important;
color: var(--rs-text-strong) !important;
}
.friend-list-item.selected,
.chat-item.selected,
.chat-room-list-item.selected {
background: #0c4a6e !important;
}
.friend-list-item.selected .friend-name,
.chat-item.selected .chat-name,
.chat-room-list-item.selected .room-name {
color: #bae6fd !important;
}
.friend-avatar .status-dot,
.chat-avatar-wrapper .status-dot,
.profile-avatar-wrapper .status-dot {
border-color: var(--rs-surface) !important;
}
.network-tabs .tab-btn,
.chat-hub-tabs .tab-btn,
.chat-hub-rightbar .user {
color: var(--rs-text-muted) !important;
}
.network-tabs .tab-btn.active,
.chat-hub-tabs .tab-btn.active {
color: var(--rs-accent) !important;
background: transparent !important;
border-bottom-color: var(--rs-accent) !important;
}
.detail-header { border-color: var(--rs-border-soft) !important; }
.detail-section,
.participant-card,
.identity-card,
.people-context-menu,
.profile-presence-menu,
.rightbar-context-menu,
.chat-msg-context-menu {
background: var(--rs-surface-raised) !important;
color: var(--rs-text) !important;
border-color: var(--rs-border-soft) !important;
box-shadow: 0 8px 24px var(--rs-shadow) !important;
}
.detail-section h3 {
color: var(--rs-text-strong) !important;
border-color: var(--rs-border-soft) !important;
}
.chat-hub-messages .message.incoming {
background: var(--rs-surface-raised) !important;
color: var(--rs-text) !important;
border-color: var(--rs-border-soft) !important;
}
.chat-hub-messages .message.outgoing {
background: #075985 !important;
color: #f8fafc !important;
}
.chat-hub-input-area {
background: var(--rs-surface) !important;
border-color: var(--rs-border-soft) !important;
}
.chat-hub-input-area textarea.chat-hub-textarea {
background: var(--rs-surface-muted) !important;
color: var(--rs-text-strong) !important;
border-color: var(--rs-border) !important;
}
.statistics-page { color: var(--rs-text) !important; }
.statistics-header h1,
.traffic-panel__heading h2 { color: var(--rs-text-strong) !important; }
.statistics-header p,
.traffic-panel__heading p,
.statistics-note { color: var(--rs-text-muted) !important; }
.traffic-panel {
background: var(--rs-surface) !important;
color: var(--rs-text) !important;
border-color: var(--rs-border-soft) !important;
box-shadow: 0 3px 14px var(--rs-shadow) !important;
}
.traffic-pie__track { stroke: var(--rs-border-soft); }
.traffic-pie__value { fill: var(--rs-text-strong); }
.traffic-pie__caption { fill: var(--rs-text-muted); }
.traffic-table { color: var(--rs-text) !important; }
.traffic-table th,
.traffic-table td { border-color: var(--rs-border-soft) !important; }
}
/* Remaining feature-specific and phone-only surfaces. */
:root[data-theme='dark'] {
.mobile-pane-header,
.chat-header-bar,
.chat-identity-select-container,
.network-chat-view .chat-input-area,
.side-bar,
.mail-box-content,
.mail-mobile-nav-toggle,
.mail-nav-drawer {
background: var(--rs-surface) !important;
color: var(--rs-text) !important;
border-color: var(--rs-border-soft) !important;
}
.mobile-pane-header strong,
.chat-header-name { color: var(--rs-text-strong) !important; }
.chat-header-location,
.tunnel-label,
.chatting-as-label { color: var(--rs-text-muted) !important; }
.network-chat-view,
.network-chat-view .chat-messages {
background: var(--rs-bg) !important;
color: var(--rs-text) !important;
}
.network-chat-view .chat-bubble-container.incoming .chat-bubble {
background: var(--rs-surface-raised) !important;
color: var(--rs-text) !important;
border-color: var(--rs-border) !important;
}
.network-chat-view .chat-bubble-container.outgoing .chat-bubble {
background: #075985 !important;
color: #f8fafc !important;
}
.network-chat-view .chat-bubble-container .chat-sender,
.network-chat-view .chat-bubble-container .chat-time {
color: var(--rs-text-muted) !important;
}
.network-chat-view textarea.chat-textarea,
.network-chat-view .chat-input-area textarea.chat-textarea,
.network-chat-view .chat-input-area textarea.chat-textarea:focus {
background: var(--rs-surface-muted) !important;
color: var(--rs-text-strong) !important;
border-color: var(--rs-border) !important;
box-shadow: none !important;
}
.network-chat-view .chat-attach-status {
background: var(--rs-surface-muted) !important;
color: var(--rs-text) !important;
border-color: var(--rs-border-soft) !important;
}
.location-card {
background: var(--rs-surface-raised) !important;
color: var(--rs-text) !important;
border-color: var(--rs-border-soft) !important;
box-shadow: none !important;
}
.location-card .loc-header { border-color: var(--rs-border-soft) !important; }
.location-card .loc-name,
.location-card .loc-val { color: var(--rs-text-strong) !important; }
.location-card .loc-label { color: var(--rs-text-muted) !important; }
.location-card .loc-status.offline {
background: var(--rs-surface-hover) !important;
color: var(--rs-text-muted) !important;
}
.webhelp {
background: var(--rs-surface-raised) !important;
color: var(--rs-text) !important;
border-color: var(--rs-border) !important;
}
.webhelp:hover { background: var(--rs-surface-hover) !important; }
.board-view-container,
.board-grid,
.posts { color: var(--rs-text) !important; }
.board-toolbar,
.board-card--compact,
.board-card--card,
.board-grid__empty,
.board-comments,
.board-comment__content,
.board-comment-composer,
.board-post-voting {
background: var(--rs-surface) !important;
color: var(--rs-text) !important;
border-color: var(--rs-border-soft) !important;
box-shadow: none !important;
}
.board-card--compact:hover,
.board-card--card:hover { background: var(--rs-surface-hover) !important; }
.board-card--compact .board-card__placeholder-wrapper,
.board-card--card .board-card__placeholder-wrapper,
.board-card--compact .board-card__image-container,
.board-card--card .board-card__image-container {
background: var(--rs-surface-muted) !important;
}
.board-card--compact .board-card__title,
.board-card--card .board-card__title,
.board-card--compact .board-card__meta b,
.board-card__vote-score,
.board-grid__empty-title,
.board-comments { color: var(--rs-text-strong) !important; }
.board-card--compact .board-card__meta,
.board-card--card .board-card__meta,
.board-card--card .board-card__notes,
.board-card__placeholder-content,
.board-grid__empty-desc { color: var(--rs-text-muted) !important; }
.board-card--card .board-card__notes,
.board-card__vote-pill,
.board-card--card .board-card__comments-btn,
.board-card--card .board-card__notes-btn,
.board-toolbar__count-badge,
.board-toolbar__view-toggle {
background: var(--rs-surface-muted) !important;
color: var(--rs-text) !important;
border-color: var(--rs-border) !important;
}
.board-toolbar__search-input,
.board-toolbar__voter select {
background: var(--rs-surface-muted) !important;
color: var(--rs-text-strong) !important;
border-color: var(--rs-border) !important;
}
.table-pagination-container table.mails tr.msgbody,
.msg-view .msg-view-nav,
.msg-view__header,
.msg-view__body {
background: var(--rs-surface) !important;
color: var(--rs-text) !important;
border-color: var(--rs-border-soft) !important;
box-shadow: none !important;
}
.table-pagination-container table.mails tr.msgbody.unread {
background: #10283b !important;
border-color: #075985 !important;
border-left-color: var(--rs-accent) !important;
}
.table-pagination-container table.mails tr.msgbody td.cell-from div span,
.msg-view__header h3 { color: var(--rs-text-strong) !important; }
.table-pagination-container table.mails tr.msgbody td.cell-subject {
color: var(--rs-text) !important;
}
.table-pagination-container table.mails tr.msgbody td.cell-date,
.table-pagination-container table.mails tr.msgbody td.cell-attachment {
color: var(--rs-text-muted) !important;
}
table.mails tr:hover,
table.mails tr.unread { background: var(--rs-surface-hover) !important; }
table.mails > tr:hover { background: var(--rs-surface) !important; }
}
/* Network graph and the shared People/Chats segmented navigation. */
:root[data-theme='dark'] {
.people-sidebar-header .segmented-control {
background: var(--rs-surface-muted) !important;
border: 1px solid var(--rs-border-soft);
}
.people-sidebar-header .segmented-control button.segment-tab {
background: transparent !important;
color: var(--rs-text-muted) !important;
box-shadow: none !important;
}
.people-sidebar-header .segmented-control button.segment-tab:hover {
background: var(--rs-surface-hover) !important;
color: var(--rs-text-strong) !important;
}
.people-sidebar-header .segmented-control button.segment-tab.active {
background: var(--rs-accent-soft) !important;
color: #bae6fd !important;
box-shadow: inset 0 0 0 1px #075985 !important;
}
.people-sidebar-header .segmented-control .segment-badge {
background: var(--rs-border-soft) !important;
color: var(--rs-text) !important;
}
.people-sidebar-header .searchbar-wrapper input.searchbar-input,
.people-sidebar-header .sub-filter-row select.filter-select {
background: var(--rs-surface-muted) !important;
color: var(--rs-text-strong) !important;
border-color: var(--rs-border) !important;
}
.network-graph { color: var(--rs-text) !important; }
.network-graph__toolbar label,
.network-graph__legend { color: var(--rs-text-muted) !important; }
.network-graph__search {
background: var(--rs-surface-muted) !important;
color: var(--rs-text) !important;
border-color: var(--rs-border) !important;
}
.network-graph__search input {
background: transparent !important;
color: var(--rs-text-strong) !important;
}
.network-graph__canvas {
background: var(--rs-surface) !important;
border-color: var(--rs-border-soft) !important;
}
.network-graph__edges line {
stroke: #64748b !important;
opacity: .72;
}
.network-graph__node circle { stroke: var(--rs-surface) !important; }
.network-graph__node text {
fill: var(--rs-text-strong) !important;
stroke: var(--rs-surface) !important;
}
.network-graph__node.is-match circle { stroke: #fb923c !important; }
}
/* Mail composer contains inline light-theme styles, so these require explicit
important overrides until the composer is migrated to semantic classes. */
:root[data-theme='dark'] {
.mail-compose-toolbar,
.mail-emoji-picker-popover,
.mail-attachments-bar {
background: var(--rs-surface-raised) !important;
color: var(--rs-text) !important;
border-color: var(--rs-border) !important;
}
.mail-compose-toolbar .toolbar-divider {
background: var(--rs-border) !important;
}
.mail-compose-toolbar button.mail-tool-btn {
background: transparent !important;
color: var(--rs-text-muted) !important;
border-color: transparent !important;
box-shadow: none !important;
}
.mail-compose-toolbar button.mail-tool-btn:hover {
background: var(--rs-surface-hover) !important;
color: var(--rs-text-strong) !important;
}
.mail-compose-toolbar button.mail-compose-send-btn {
background: #0284c7 !important;
color: #fff !important;
}
.mail-emoji-picker-popover .emoji-search-bar,
.mail-emoji-picker-popover .emoji-cat-bar {
background: var(--rs-surface-muted) !important;
border-color: var(--rs-border-soft) !important;
}
.mail-emoji-picker-popover .emoji-cat-bar button {
color: var(--rs-text) !important;
}
.mail-emoji-picker-popover .emoji-cat-bar button:hover,
.mail-emoji-picker-popover .emoji-cat-bar button[style*='background: #ffffff'] {
background: var(--rs-surface-hover) !important;
}
.mail-attachment-chip {
background: var(--rs-surface-muted) !important;
color: var(--rs-text) !important;
border-color: var(--rs-border) !important;
}
.board-pagination {
background: var(--rs-surface-muted) !important;
border-color: var(--rs-border) !important;
}
.board-pagination__btn {
background: var(--rs-surface-raised) !important;
color: var(--rs-accent) !important;
border-color: var(--rs-border) !important;
}
.board-pagination__btn i { color: var(--rs-accent) !important; }
.board-pagination__btn:hover:not(:disabled) {
background: #075985 !important;
color: #fff !important;
border-color: var(--rs-accent) !important;
}
.board-pagination__btn:hover:not(:disabled) i { color: #fff !important; }
.board-pagination__btn:disabled {
background: var(--rs-surface-muted) !important;
color: var(--rs-text-muted) !important;
border-color: var(--rs-border-soft) !important;
}
.board-pagination__btn:disabled i { color: var(--rs-text-muted) !important; }
.board-pagination__label { color: var(--rs-text-strong) !important; }
.modal-content.create-board-post-modal,
.create-board-post {
background: var(--rs-surface) !important;
color: var(--rs-text) !important;
}
.create-board-post__heading h3 { color: var(--rs-text-strong) !important; }
.create-board-post__heading p,
.create-board-post__placeholder { color: var(--rs-text-muted) !important; }
.create-board-post__modes { border-color: var(--rs-border) !important; }
.create-board-post__modes button {
background: transparent !important;
color: var(--rs-text-muted) !important;
}
.create-board-post__modes button:hover {
background: var(--rs-surface-hover) !important;
color: var(--rs-text-strong) !important;
}
.create-board-post__modes button.active {
background: var(--rs-accent-soft) !important;
color: #7dd3fc !important;
border-bottom-color: var(--rs-accent) !important;
}
.create-board-post__preview {
background: var(--rs-surface-muted) !important;
border-color: var(--rs-border) !important;
}
.create-board-post__file-button {
background: var(--rs-surface-raised) !important;
color: var(--rs-text) !important;
border-color: var(--rs-border) !important;
}
.create-board-post__file-button:hover { background: var(--rs-surface-hover) !important; }
.create-board-post__author label { color: var(--rs-text) !important; }
.create-board-post__author select.network-style-select {
background: var(--rs-surface-muted) !important;
color: var(--rs-text-strong) !important;
border-color: var(--rs-border) !important;
}
.modal-content.create-channel-post-modal,
.create-channel-post-form {
background: var(--rs-surface) !important;
color: var(--rs-text) !important;
}
.create-channel-post-form__heading h3 { color: var(--rs-text-strong) !important; }
.create-channel-post-form__heading p,
.create-channel-post-form__thumbnail small,
.create-channel-post-form__attachments small {
color: var(--rs-text-muted) !important;
}
.create-channel-post-form__thumbnail,
.create-channel-post-form__attachments,
.channel-post-thumbnail-preview {
background: var(--rs-surface-muted) !important;
color: var(--rs-text) !important;
border-color: var(--rs-border) !important;
}
.create-channel-post-form__thumbnail-label,
.create-channel-post-form__attachments > label:first-child {
color: var(--rs-text) !important;
}
.channel-post-thumbnail-preview__placeholder {
background: var(--rs-surface-raised) !important;
color: var(--rs-text-muted) !important;
}
.channel-post-thumbnail-preview__placeholder span {
color: var(--rs-text-strong) !important;
}
.create-channel-post-form__attachment-list {
background: var(--rs-surface-raised) !important;
border-color: var(--rs-border) !important;
}
.create-channel-post-form__attachment-item { border-color: var(--rs-border-soft) !important; }
.create-channel-post-form__attachment-info span { color: var(--rs-text) !important; }
}

View File

@ -24,3 +24,6 @@
// 7. Small screen / touch adaptations. Must stay last: it overrides the
// layouts defined above without relying on !important.
@use 'responsive';
// 8. Runtime light/dark theme overrides. Must stay last.
@use 'theme';

76
webui-src/app/theme.js Normal file
View File

@ -0,0 +1,76 @@
const m = require('mithril');
const STORAGE_KEY = 'retroshare-webui-theme';
const MODES = ['system', 'light', 'dark'];
let mode = readMode();
function readMode() {
try {
const stored = window.localStorage.getItem(STORAGE_KEY);
return MODES.includes(stored) ? stored : 'system';
} catch (_) {
return 'system';
}
}
function systemTheme() {
return window.matchMedia && window.matchMedia('(prefers-color-scheme: dark)').matches
? 'dark'
: 'light';
}
function apply() {
const effectiveTheme = mode === 'system' ? systemTheme() : mode;
document.documentElement.dataset.theme = effectiveTheme;
document.documentElement.dataset.themePreference = mode;
document.documentElement.style.colorScheme = effectiveTheme;
}
function setMode(nextMode) {
if (!MODES.includes(nextMode)) return;
mode = nextMode;
try {
window.localStorage.setItem(STORAGE_KEY, mode);
} catch (_) {
// The theme still works for this session when storage is unavailable.
}
apply();
m.redraw();
}
function nextMode() {
setMode(MODES[(MODES.indexOf(mode) + 1) % MODES.length]);
}
function icon() {
if (mode === 'dark') return 'fa-moon';
if (mode === 'light') return 'fa-sun';
return 'fa-adjust';
}
const ThemeToggle = {
view: (vnode) => m('button.theme-toggle[type=button]', {
class: vnode.attrs.compact ? 'theme-toggle--compact' : '',
title: `Theme: ${mode}. Click to change.`,
'aria-label': `Theme: ${mode}. Click to use ${MODES[(MODES.indexOf(mode) + 1) % MODES.length]} mode.`,
onclick: nextMode,
}, [
m(`i.fas.${icon()}[aria-hidden=true]`),
vnode.attrs.compact ? null : m('span', `Theme: ${mode.charAt(0).toUpperCase()}${mode.slice(1)}`),
]),
};
apply();
if (window.matchMedia) {
const colorScheme = window.matchMedia('(prefers-color-scheme: dark)');
const updateSystemTheme = () => {
if (mode === 'system') {
apply();
m.redraw();
}
};
if (colorScheme.addEventListener) colorScheme.addEventListener('change', updateSystemTheme);
else if (colorScheme.addListener) colorScheme.addListener(updateSystemTheme);
}
module.exports = { ThemeToggle, getMode: () => mode, setMode };

View File

@ -3,6 +3,17 @@
<head>
<meta charset="utf-8" />
<meta name="viewport" content="width=device-width, initial-scale=1" />
<meta name="color-scheme" content="light dark" />
<script>
(function () {
var mode = 'system';
try { mode = localStorage.getItem('retroshare-webui-theme') || 'system'; } catch (_) {}
var dark = mode === 'dark' || (mode === 'system' && window.matchMedia && window.matchMedia('(prefers-color-scheme: dark)').matches);
document.documentElement.dataset.theme = dark ? 'dark' : 'light';
document.documentElement.dataset.themePreference = mode;
document.documentElement.style.colorScheme = dark ? 'dark' : 'light';
}());
</script>
<link rel="stylesheet" href="styles.css" />
<link href="images/retroshare.svg" rel="icon" data-n-head="true" type="image/svg" />
<title>RetroShare</title>

File diff suppressed because one or more lines are too long