From 0c5a8a24da271cf0a800b5908bc60faecb8d6f5b Mon Sep 17 00:00:00 2001 From: Elian Doran Date: Mon, 13 Apr 2026 14:40:46 +0300 Subject: [PATCH 01/54] refactor(options): create OptionsRowWithToggle --- .../widgets/type_widgets/options/advanced.tsx | 15 +++---- .../options/components/OptionsRow.tsx | 26 +++++++++++ .../src/widgets/type_widgets/options/llm.tsx | 17 ++++--- .../widgets/type_widgets/options/media.tsx | 45 +++++++++---------- .../widgets/type_widgets/options/other.tsx | 27 ++++------- .../type_widgets/options/spellcheck.tsx | 16 +++---- 6 files changed, 77 insertions(+), 69 deletions(-) diff --git a/apps/client/src/widgets/type_widgets/options/advanced.tsx b/apps/client/src/widgets/type_widgets/options/advanced.tsx index 619a1fb514..8f41d4c69f 100644 --- a/apps/client/src/widgets/type_widgets/options/advanced.tsx +++ b/apps/client/src/widgets/type_widgets/options/advanced.tsx @@ -8,9 +8,8 @@ import toast from "../../../services/toast"; import Button from "../../react/Button"; import Column from "../../react/Column"; import FormText from "../../react/FormText"; -import FormToggle from "../../react/FormToggle"; import { useTriliumOptionJson } from "../../react/hooks"; -import OptionsRow from "./components/OptionsRow"; +import { OptionsRowWithToggle } from "./components/OptionsRow"; import OptionsSection from "./components/OptionsSection"; export default function AdvancedSettings() { @@ -201,18 +200,14 @@ function ExperimentalOptions() { {t("experimental_features.disclaimer")} {filteredFeatures.map((feature) => ( - - toggleFeature(feature.id, enabled)} - /> - + currentValue={enabledFeatures.includes(feature.id)} + onChange={(enabled) => toggleFeature(feature.id, enabled)} + /> ))} ); diff --git a/apps/client/src/widgets/type_widgets/options/components/OptionsRow.tsx b/apps/client/src/widgets/type_widgets/options/components/OptionsRow.tsx index 0ff8b737b0..43ff7b5bc1 100644 --- a/apps/client/src/widgets/type_widgets/options/components/OptionsRow.tsx +++ b/apps/client/src/widgets/type_widgets/options/components/OptionsRow.tsx @@ -2,6 +2,7 @@ import "./OptionsRow.css"; import { cloneElement, VNode } from "preact"; +import FormToggle from "../../../react/FormToggle"; import { useUniqueName } from "../../../react/hooks"; interface OptionsRowProps { @@ -52,3 +53,28 @@ export function OptionsRowLink({ label, description, href }: OptionsRowLinkProps ); } + +interface OptionsRowWithToggleProps { + name: string; + label: string; + description?: string; + currentValue: boolean | null; + onChange: (newValue: boolean) => void; + disabled?: boolean; + helpPage?: string; +} + +export function OptionsRowWithToggle({ name, label, description, currentValue, onChange, disabled, helpPage }: OptionsRowWithToggleProps) { + return ( + + + + ); +} diff --git a/apps/client/src/widgets/type_widgets/options/llm.tsx b/apps/client/src/widgets/type_widgets/options/llm.tsx index 9da9f50ca6..ce709ef742 100644 --- a/apps/client/src/widgets/type_widgets/options/llm.tsx +++ b/apps/client/src/widgets/type_widgets/options/llm.tsx @@ -5,9 +5,8 @@ import { isExperimentalFeatureEnabled } from "../../../services/experimental_fea import { t } from "../../../services/i18n"; import ActionButton from "../../react/ActionButton"; import Button from "../../react/Button"; -import FormToggle from "../../react/FormToggle"; import { useTriliumOption, useTriliumOptionBool } from "../../react/hooks"; -import OptionsRow from "./components/OptionsRow"; +import OptionsRow, { OptionsRowWithToggle } from "./components/OptionsRow"; import OptionsSection from "./components/OptionsSection"; import AddProviderModal, { type LlmProviderConfig, PROVIDER_TYPES } from "./llm/AddProviderModal"; @@ -92,13 +91,13 @@ function McpSettings() { return ( - - - + {mcpEnabled && ( diff --git a/apps/client/src/widgets/type_widgets/options/media.tsx b/apps/client/src/widgets/type_widgets/options/media.tsx index ba90385016..a5af9343d0 100644 --- a/apps/client/src/widgets/type_widgets/options/media.tsx +++ b/apps/client/src/widgets/type_widgets/options/media.tsx @@ -5,10 +5,9 @@ import server from "../../../services/server"; import toast from "../../../services/toast"; import { isElectron } from "../../../services/utils"; import { FormTextBoxWithUnit } from "../../react/FormTextBox"; -import FormToggle from "../../react/FormToggle"; import { useTriliumOption, useTriliumOptionBool } from "../../react/hooks"; import Slider from "../../react/Slider"; -import OptionsRow from "./components/OptionsRow"; +import OptionsRow, { OptionsRowWithToggle } from "./components/OptionsRow"; import OptionsSection from "./components/OptionsSection"; import RelatedSettings from "./components/RelatedSettings"; @@ -29,21 +28,21 @@ function ImageSettings() { return ( - - - + - - - + - - - + - - - + currentValue={fuzzyEnabled} + onChange={setFuzzyEnabled} + /> - - - + currentValue={autocompleteFuzzy} + onChange={setAutocompleteFuzzy} + /> ); } diff --git a/apps/client/src/widgets/type_widgets/options/spellcheck.tsx b/apps/client/src/widgets/type_widgets/options/spellcheck.tsx index e91804e3f3..22ec3e8ba7 100644 --- a/apps/client/src/widgets/type_widgets/options/spellcheck.tsx +++ b/apps/client/src/widgets/type_widgets/options/spellcheck.tsx @@ -5,11 +5,10 @@ import { t } from "../../../services/i18n"; import { dynamicRequire, isElectron, restartDesktopApp } from "../../../services/utils"; import Button from "../../react/Button"; import FormText from "../../react/FormText"; -import FormToggle from "../../react/FormToggle"; import { useTriliumOption, useTriliumOptionBool } from "../../react/hooks"; import NoItems from "../../react/NoItems"; import CheckboxList from "./components/CheckboxList"; -import OptionsRow from "./components/OptionsRow"; +import OptionsRow, { OptionsRowWithToggle } from "./components/OptionsRow"; import OptionsSection from "./components/OptionsSection"; export default function SpellcheckSettings() { @@ -32,13 +31,12 @@ function ElectronSpellcheckSettings() { {t("spellcheck.restart-required")} - - - + + ); +} From c352d46a5b34fe58ff139a7fbaa0a2aa1f1c5608 Mon Sep 17 00:00:00 2001 From: Elian Doran Date: Mon, 13 Apr 2026 14:59:31 +0300 Subject: [PATCH 03/54] chore(options/advanced): use options row for database integrity check --- apps/client/src/translations/en/translation.json | 5 ++++- .../src/widgets/type_widgets/options/advanced.tsx | 12 ++++++------ 2 files changed, 10 insertions(+), 7 deletions(-) diff --git a/apps/client/src/translations/en/translation.json b/apps/client/src/translations/en/translation.json index 1943cc4b8b..31024c5a74 100644 --- a/apps/client/src/translations/en/translation.json +++ b/apps/client/src/translations/en/translation.json @@ -1121,6 +1121,8 @@ }, "consistency_checks": { "title": "Consistency Checks", + "find_and_fix_label": "Find and fix consistency issues", + "find_and_fix_description": "Scan for and automatically repair any data consistency issues in the database.", "find_and_fix_button": "Find and fix consistency issues", "finding_and_fixing_message": "Finding and fixing consistency issues...", "issues_fixed_message": "Any consistency issue which may have been found is now fixed." @@ -1144,7 +1146,8 @@ }, "database_integrity_check": { "title": "Database Integrity Check", - "description": "This will check that the database is not corrupted on the SQLite level. It might take some time, depending on the DB size.", + "check_integrity_label": "Check database integrity", + "check_integrity_description": "Verify that the database is not corrupted on the SQLite level.", "check_button": "Check database integrity", "checking_integrity": "Checking database integrity...", "integrity_check_succeeded": "Integrity check succeeded - no problems found.", diff --git a/apps/client/src/widgets/type_widgets/options/advanced.tsx b/apps/client/src/widgets/type_widgets/options/advanced.tsx index 62daed3dc1..2b15ff14fd 100644 --- a/apps/client/src/widgets/type_widgets/options/advanced.tsx +++ b/apps/client/src/widgets/type_widgets/options/advanced.tsx @@ -50,10 +50,9 @@ function AdvancedSyncOptions() { function DatabaseIntegrityOptions() { return ( - {t("database_integrity_check.description")} - - + + setShowModal(false)} + title={label} + fontFamily={fontFamily ?? ""} + fontSize={parseInt(fontSize ?? "100", 10)} + onFontFamilyChange={setFontFamily} + onFontSizeChange={(size) => setFontSize(String(size))} + getFontFamily={getFontFamily} + /> + + + ); +} + +const PREVIEW_TEXT = "The quick brown fox jumps over the lazy dog. 0123456789"; + +interface FontPickerModalProps { + show: boolean; + onHidden: () => void; + title: string; + fontFamily: string; + fontSize: number; + onFontFamilyChange: (value: string) => void; + onFontSizeChange: (value: number) => void; + getFontFamily: (value: string) => string | undefined; +} + +function FontPickerModal({ show, onHidden, title, fontFamily, fontSize, onFontFamilyChange, onFontSizeChange, getFontFamily }: FontPickerModalProps) { + return ( + - {FONT_FAMILIES.map(group => ( - - - {group.items.map(item => ( - onChange(item.value)} - checked={currentValue === item.value} - selected={currentValue === item.value} +
+
+ + {FONT_FAMILIES.map(group => ( + + + {group.items.map(item => ( + onFontFamilyChange(item.value)} + checked={fontFamily === item.value} + selected={fontFamily === item.value} + > + + {item.label ?? item.value} + + + ))} + + ))} + +
+ +
+
+ +
+ + {fontSize}% +
+
+ +
+ +
- - {item.label ?? item.value} - - - ))} - - ))} - + {PREVIEW_TEXT} +
+
+
+
+
); } From 368dd1adc4cb8c93f97d13998ffff301a99db14a Mon Sep 17 00:00:00 2001 From: Elian Doran Date: Mon, 13 Apr 2026 17:05:18 +0300 Subject: [PATCH 31/54] fix(options/appearance): font modal styles are affected --- apps/client/src/widgets/type_widgets/options/appearance.tsx | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/apps/client/src/widgets/type_widgets/options/appearance.tsx b/apps/client/src/widgets/type_widgets/options/appearance.tsx index e40daf3101..df46afa8ee 100644 --- a/apps/client/src/widgets/type_widgets/options/appearance.tsx +++ b/apps/client/src/widgets/type_widgets/options/appearance.tsx @@ -2,6 +2,7 @@ import "./appearance.css"; import { FontFamily, OptionNames } from "@triliumnext/commons"; import { Fragment } from "preact"; +import { createPortal } from "preact/compat"; import { useEffect, useState } from "preact/hooks"; import zoomService from "../../../components/zoom"; @@ -410,7 +411,7 @@ interface FontPickerModalProps { } function FontPickerModal({ show, onHidden, title, fontFamily, fontSize, onFontFamilyChange, onFontSizeChange, getFontFamily }: FontPickerModalProps) { - return ( + return createPortal( - + , + document.body ); } From 49878d64aa2e6e04fd17f8318b73a2b825ce2f7e Mon Sep 17 00:00:00 2001 From: Elian Doran Date: Mon, 13 Apr 2026 17:07:03 +0300 Subject: [PATCH 32/54] chore(options/appearance): integrate font description properly --- .../widgets/type_widgets/options/appearance.css | 5 +++++ .../widgets/type_widgets/options/appearance.tsx | 15 +++++++++------ 2 files changed, 14 insertions(+), 6 deletions(-) diff --git a/apps/client/src/widgets/type_widgets/options/appearance.css b/apps/client/src/widgets/type_widgets/options/appearance.css index 753a2715fd..8693b5bbdf 100644 --- a/apps/client/src/widgets/type_widgets/options/appearance.css +++ b/apps/client/src/widgets/type_widgets/options/appearance.css @@ -56,6 +56,11 @@ font-size: 12px; text-transform: uppercase; } + + .font-size-description { + color: var(--muted-text-color); + font-size: 12px; + } } .font-size-slider { diff --git a/apps/client/src/widgets/type_widgets/options/appearance.tsx b/apps/client/src/widgets/type_widgets/options/appearance.tsx index df46afa8ee..1f76dd37c7 100644 --- a/apps/client/src/widgets/type_widgets/options/appearance.tsx +++ b/apps/client/src/widgets/type_widgets/options/appearance.tsx @@ -328,8 +328,8 @@ function Fonts() { /> - - + + + <> + <> + - setShowModal(false)} - title={label} - fontFamily={fontFamily ?? ""} - fontSize={parseInt(fontSize ?? "100", 10)} - onFontFamilyChange={setFontFamily} - onFontSizeChange={(size) => setFontSize(String(size))} - getFontFamily={getFontFamily} - sizeDescription={sizeDescription} - /> - - + setShowModal(false)} + title={label} + fontFamily={fontFamily ?? ""} + fontSize={parseInt(fontSize ?? "100", 10)} + onFontFamilyChange={setFontFamily} + onFontSizeChange={(size) => setFontSize(String(size))} + getFontFamily={getFontFamily} + sizeDescription={sizeDescription} + /> + ); } From 00b85bb7bd5fead5d4fd9fbe25acce08d5d6ceab Mon Sep 17 00:00:00 2001 From: Elian Doran Date: Mon, 13 Apr 2026 17:18:20 +0300 Subject: [PATCH 34/54] feat(options/appearance): improve font buttons --- .../type_widgets/options/appearance.tsx | 16 ++++++--- apps/server/src/routes/api/fonts.ts | 27 +++----------- packages/commons/src/lib/options_interface.ts | 35 +++++++++++++++++++ 3 files changed, 51 insertions(+), 27 deletions(-) diff --git a/apps/client/src/widgets/type_widgets/options/appearance.tsx b/apps/client/src/widgets/type_widgets/options/appearance.tsx index bc14f302be..027667de56 100644 --- a/apps/client/src/widgets/type_widgets/options/appearance.tsx +++ b/apps/client/src/widgets/type_widgets/options/appearance.tsx @@ -1,6 +1,6 @@ import "./appearance.css"; -import { FontFamily, OptionNames } from "@triliumnext/commons"; +import { FontFamily, OptionNames, SYSTEM_MONOSPACE_FONT_STACK, SYSTEM_SANS_SERIF_FONT_STACK } from "@triliumnext/commons"; import { Fragment } from "preact"; import { createPortal } from "preact/compat"; import { useEffect, useState } from "preact/hooks"; @@ -330,7 +330,7 @@ function Fonts() { - + { - if (["theme", "system"].includes(value)) { - return undefined; + if (value === "theme") { + // Use inherited font from the current theme + return "inherit"; + } + if (value === "system") { + // Use the appropriate system font stack + return isMonospace ? SYSTEM_MONOSPACE_FONT_STACK : SYSTEM_SANS_SERIF_FONT_STACK; } return value; }; diff --git a/apps/server/src/routes/api/fonts.ts b/apps/server/src/routes/api/fonts.ts index dc24be874e..d9882a4101 100644 --- a/apps/server/src/routes/api/fonts.ts +++ b/apps/server/src/routes/api/fonts.ts @@ -1,23 +1,6 @@ import type { Request, Response } from "express"; import optionService from "../../services/options.js"; -import type { OptionMap } from "@triliumnext/commons"; - -const SYSTEM_SANS_SERIF = [ - "system-ui", - "-apple-system", - "BlinkMacSystemFont", - "Segoe UI", - "Cantarell", - "Ubuntu", - "Noto Sans", - "Helvetica", - "Arial", - "sans-serif", - "Apple Color Emoji", - "Segoe UI Emoji" -].join(","); - -const SYSTEM_MONOSPACE = ["ui-monospace", "SFMono-Regular", "SF Mono", "Consolas", "Source Code Pro", "Ubuntu Mono", "Menlo", "Liberation Mono", "monospace"].join(","); +import { SYSTEM_MONOSPACE_FONT_STACK, SYSTEM_SANS_SERIF_FONT_STACK, type OptionMap } from "@triliumnext/commons"; function getFontCss(req: Request, res: Response) { res.setHeader("Content-Type", "text/css"); @@ -44,19 +27,19 @@ function getFontFamily({ mainFontFamily, treeFontFamily, detailFontFamily, monos // System override if (mainFontFamily === "system") { - mainFontFamily = SYSTEM_SANS_SERIF; + mainFontFamily = SYSTEM_SANS_SERIF_FONT_STACK; } if (treeFontFamily === "system") { - treeFontFamily = SYSTEM_SANS_SERIF; + treeFontFamily = SYSTEM_SANS_SERIF_FONT_STACK; } if (detailFontFamily === "system") { - detailFontFamily = SYSTEM_SANS_SERIF; + detailFontFamily = SYSTEM_SANS_SERIF_FONT_STACK; } if (monospaceFontFamily === "system") { - monospaceFontFamily = SYSTEM_MONOSPACE; + monospaceFontFamily = SYSTEM_MONOSPACE_FONT_STACK; } // Apply the font override if not using theme fonts. diff --git a/packages/commons/src/lib/options_interface.ts b/packages/commons/src/lib/options_interface.ts index a00666677a..9f7db1d04b 100644 --- a/packages/commons/src/lib/options_interface.ts +++ b/packages/commons/src/lib/options_interface.ts @@ -14,6 +14,41 @@ type KeyboardShortcutsOptions = { export type FontFamily = "theme" | "serif" | "sans-serif" | "monospace" | string; +/** + * System sans-serif font stack for cross-platform compatibility. + * Used when the user selects "System default" for non-monospace fonts. + */ +export const SYSTEM_SANS_SERIF_FONT_STACK = [ + "system-ui", + "-apple-system", + "BlinkMacSystemFont", + "Segoe UI", + "Cantarell", + "Ubuntu", + "Noto Sans", + "Helvetica", + "Arial", + "sans-serif", + "Apple Color Emoji", + "Segoe UI Emoji" +].join(","); + +/** + * System monospace font stack for cross-platform compatibility. + * Used when the user selects "System default" for monospace fonts. + */ +export const SYSTEM_MONOSPACE_FONT_STACK = [ + "ui-monospace", + "SFMono-Regular", + "SF Mono", + "Consolas", + "Source Code Pro", + "Ubuntu Mono", + "Menlo", + "Liberation Mono", + "monospace" +].join(","); + export interface OptionDefinitions extends KeyboardShortcutsOptions { openNoteContexts: string; lastDailyBackupDate: string; From f9b0a88a4ec23447a2c44d65b4dcb738c6f36665 Mon Sep 17 00:00:00 2001 From: Elian Doran Date: Mon, 13 Apr 2026 17:19:40 +0300 Subject: [PATCH 35/54] fix(options/appearance): theme defined not previewed correctly --- .../src/widgets/type_widgets/options/appearance.tsx | 12 ++++++++++-- 1 file changed, 10 insertions(+), 2 deletions(-) diff --git a/apps/client/src/widgets/type_widgets/options/appearance.tsx b/apps/client/src/widgets/type_widgets/options/appearance.tsx index 027667de56..04f5477837 100644 --- a/apps/client/src/widgets/type_widgets/options/appearance.tsx +++ b/apps/client/src/widgets/type_widgets/options/appearance.tsx @@ -361,11 +361,19 @@ function Font({ label, sizeDescription, fontFamilyOption, fontSizeOption, disabl .find(item => item.value === fontFamily); const displayLabel = currentFont?.label ?? currentFont?.value ?? fontFamily ?? ""; + // Map option name to CSS variable + const themeCssVariable = { + mainFontFamily: "var(--main-font-family)", + treeFontFamily: "var(--tree-font-family)", + detailFontFamily: "var(--detail-font-family)", + monospaceFontFamily: "var(--monospace-font-family)" + }[fontFamilyOption] ?? "inherit"; + // Get the CSS font-family value for preview const getFontFamily = (value: string) => { if (value === "theme") { - // Use inherited font from the current theme - return "inherit"; + // Use the theme's CSS variable for this font option + return themeCssVariable; } if (value === "system") { // Use the appropriate system font stack From 19e07bc6fdeabdcd3704225b623e449ecd1035ed Mon Sep 17 00:00:00 2001 From: Elian Doran Date: Mon, 13 Apr 2026 17:21:57 +0300 Subject: [PATCH 36/54] chore(options/appearance): improve font description slightly --- apps/client/src/translations/en/translation.json | 9 +++++---- .../src/widgets/type_widgets/options/appearance.tsx | 6 ++++-- 2 files changed, 9 insertions(+), 6 deletions(-) diff --git a/apps/client/src/translations/en/translation.json b/apps/client/src/translations/en/translation.json index 952736d26f..d24a6c6c83 100644 --- a/apps/client/src/translations/en/translation.json +++ b/apps/client/src/translations/en/translation.json @@ -1192,13 +1192,14 @@ "theme_defined": "Theme defined", "fonts": "Fonts", "custom_fonts": "Use custom fonts", - "main_font": "General", + "main_font": "Interface text", "font_family": "Font family", "size": "Size", "preview": "Preview", - "note_tree_font": "Note tree", - "note_detail_font": "Note content", - "monospace_font": "Code notes & blocks", + "note_tree_font": "Note tree text", + "note_detail_font": "Document text", + "monospace_font": "Monospace text", + "monospace_font_description": "Used for code notes and code blocks", "size_relative_to_general": "Size is relative to the general font size", "not_all_fonts_available": "Not all listed fonts may be available on your system", "apply_changes": "Reload to apply changes", diff --git a/apps/client/src/widgets/type_widgets/options/appearance.tsx b/apps/client/src/widgets/type_widgets/options/appearance.tsx index 04f5477837..ccf8b142af 100644 --- a/apps/client/src/widgets/type_widgets/options/appearance.tsx +++ b/apps/client/src/widgets/type_widgets/options/appearance.tsx @@ -330,7 +330,7 @@ function Fonts() { - +
+ {description && {description}}
{displayLabel} From a62a7e351e186c27b2fae3282533abeab0be693a Mon Sep 17 00:00:00 2001 From: Elian Doran Date: Mon, 13 Apr 2026 17:23:25 +0300 Subject: [PATCH 37/54] feat(options/appearance): use toggles everywhere --- .../src/translations/en/translation.json | 10 +++--- .../type_widgets/options/appearance.tsx | 33 ++++++++++++------- 2 files changed, 27 insertions(+), 16 deletions(-) diff --git a/apps/client/src/translations/en/translation.json b/apps/client/src/translations/en/translation.json index d24a6c6c83..573ed5a69d 100644 --- a/apps/client/src/translations/en/translation.json +++ b/apps/client/src/translations/en/translation.json @@ -1248,11 +1248,11 @@ }, "ui-performance": { "title": "Performance", - "enable-motion": "Enable transitions and animations", - "enable-shadows": "Enable shadows", - "enable-backdrop-effects": "Enable background effects for menus, popups and panels", - "enable-smooth-scroll": "Enable smooth scrolling", - "app-restart-required": "(a restart of the application is required for the change to take effect)" + "enable-motion": "Transitions and animations", + "enable-shadows": "Shadows", + "enable-backdrop-effects": "Background effects for menus, popups and panels", + "enable-smooth-scroll": "Smooth scrolling", + "app-restart-required": "Requires app restart" }, "zoom_factor": { "title": "Zoom Factor (desktop build only)", diff --git a/apps/client/src/widgets/type_widgets/options/appearance.tsx b/apps/client/src/widgets/type_widgets/options/appearance.tsx index ccf8b142af..3be65ba14e 100644 --- a/apps/client/src/widgets/type_widgets/options/appearance.tsx +++ b/apps/client/src/widgets/type_widgets/options/appearance.tsx @@ -546,19 +546,25 @@ function Performance() { const [ backdropEffectsEnabled, setBackdropEffectsEnabled ] = useTriliumOptionBool("backdropEffectsEnabled"); return - - - {!isMobile() && } {isElectron() && } @@ -569,9 +575,12 @@ function Performance() { function SmoothScrollEnabledOption() { const [ smoothScrollEnabled, setSmoothScrollEnabled ] = useTriliumOptionBool("smoothScrollEnabled"); - return ; } @@ -605,9 +614,11 @@ function RibbonOptions() { return ( - ); From 92d011503d68825259411d1f4d9a8f59a3f724e5 Mon Sep 17 00:00:00 2001 From: Elian Doran Date: Mon, 13 Apr 2026 17:26:37 +0300 Subject: [PATCH 38/54] feat(options/appearance): improve content width section --- .../src/stylesheets/theme-next/pages.css | 7 +++++ .../type_widgets/options/appearance.tsx | 29 +++++++++---------- .../options/components/OptionsSection.tsx | 4 ++- 3 files changed, 23 insertions(+), 17 deletions(-) diff --git a/apps/client/src/stylesheets/theme-next/pages.css b/apps/client/src/stylesheets/theme-next/pages.css index 5348b706ac..047c4c91fb 100644 --- a/apps/client/src/stylesheets/theme-next/pages.css +++ b/apps/client/src/stylesheets/theme-next/pages.css @@ -238,6 +238,13 @@ body.desktop .options-section:not(.tn-no-card) { margin-inline-start: auto; } +.options-section-description { + color: var(--muted-text-color); + font-size: 13px; + margin-top: 0; + margin-bottom: 16px; +} + .options-section:not(.tn-no-card) h5 { font-size: var(--options-title-font-size); font-weight: bold; diff --git a/apps/client/src/widgets/type_widgets/options/appearance.tsx b/apps/client/src/widgets/type_widgets/options/appearance.tsx index 3be65ba14e..591b329d20 100644 --- a/apps/client/src/widgets/type_widgets/options/appearance.tsx +++ b/apps/client/src/widgets/type_widgets/options/appearance.tsx @@ -11,12 +11,10 @@ import server from "../../../services/server"; import { isElectron, isMobile, reloadFrontendApp, restartDesktopApp } from "../../../services/utils"; import { VerticalLayoutIcon } from "../../buttons/global_menu"; import Button from "../../react/Button"; -import Column from "../../react/Column"; import FormCheckbox from "../../react/FormCheckbox"; import FormGroup from "../../react/FormGroup"; import FormList, { FormListHeader, FormListItem } from "../../react/FormList"; import FormSelect from "../../react/FormSelect"; -import FormText from "../../react/FormText"; import { FormTextBoxWithUnit } from "../../react/FormTextBox"; import { useTriliumOption, useTriliumOptionBool } from "../../react/hooks"; import Icon from "../../react/Icon"; @@ -589,22 +587,21 @@ function MaxContentWidth() { const [centerContent, setCenterContent] = useTriliumOptionBool("centerContent"); return ( - - {t("max_content_width.default_description")} + + + + - - - - - - - + onChange={setCenterContent} + /> ); } diff --git a/apps/client/src/widgets/type_widgets/options/components/OptionsSection.tsx b/apps/client/src/widgets/type_widgets/options/components/OptionsSection.tsx index 18c86e7d2f..6a724ca545 100644 --- a/apps/client/src/widgets/type_widgets/options/components/OptionsSection.tsx +++ b/apps/client/src/widgets/type_widgets/options/components/OptionsSection.tsx @@ -5,6 +5,7 @@ import ActionButton from "../../../react/ActionButton"; interface OptionsSectionProps { title?: ComponentChildren; + description?: string; children: ComponentChildren; noCard?: boolean; style?: CSSProperties; @@ -12,7 +13,7 @@ interface OptionsSectionProps { helpUrl?: string; } -export default function OptionsSection({ title, children, noCard, className, helpUrl, ...rest }: OptionsSectionProps) { +export default function OptionsSection({ title, description, children, noCard, className, helpUrl, ...rest }: OptionsSectionProps) { return (
{(title || helpUrl) && ( @@ -27,6 +28,7 @@ export default function OptionsSection({ title, children, noCard, className, hel )}
)} + {description &&

{description}

} {children}
); From b9e8cd5697e90308a2c24adca839290f96cf0833 Mon Sep 17 00:00:00 2001 From: Elian Doran Date: Mon, 13 Apr 2026 17:29:32 +0300 Subject: [PATCH 39/54] chore(options/appearance): integrate application theme into first section --- .../src/translations/en/translation.json | 4 +- .../type_widgets/options/appearance.tsx | 93 +++++++++---------- 2 files changed, 45 insertions(+), 52 deletions(-) diff --git a/apps/client/src/translations/en/translation.json b/apps/client/src/translations/en/translation.json index 573ed5a69d..7b6a16c753 100644 --- a/apps/client/src/translations/en/translation.json +++ b/apps/client/src/translations/en/translation.json @@ -1231,8 +1231,8 @@ "edited_notes_message": "Edited Notes ribbon tab will automatically open on day notes" }, "theme": { - "title": "Application Theme", - "theme_label": "Theme", + "title": "User Interface", + "theme_label": "Application theme", "override_theme_fonts_label": "Override theme fonts", "auto_theme": "Legacy (Follow system color scheme)", "light_theme": "Legacy (Light)", diff --git a/apps/client/src/widgets/type_widgets/options/appearance.tsx b/apps/client/src/widgets/type_widgets/options/appearance.tsx index 591b329d20..c6de5ff760 100644 --- a/apps/client/src/widgets/type_widgets/options/appearance.tsx +++ b/apps/client/src/widgets/type_widgets/options/appearance.tsx @@ -93,8 +93,7 @@ const FONT_FAMILIES: FontGroup[] = [ export default function AppearanceSettings() { return (
- {!isMobile() && } - + {isElectron() && } @@ -114,35 +113,54 @@ export default function AppearanceSettings() { ); } -function LayoutOptions() { +function UserInterface() { + const [ theme, setTheme ] = useTriliumOption("theme", true); + const [ themes, setThemes ] = useState([]); const [ newLayout, setNewLayout ] = useTriliumOptionBool("newLayout"); const [ layoutOrientation, setLayoutOrientation ] = useTriliumOption("layoutOrientation", true); + useEffect(() => { + server.get("options/user-themes").then((userThemes) => { + setThemes([ + ...BUILTIN_THEMES, + ...userThemes + ]); + }); + }, []); + return ( - - - { - await setNewLayout(newValue === "new-layout"); - reloadFrontendApp(); - }} - values={[ - { key: "old-layout", text: t("settings_appearance.ui_old_layout"), illustration: }, - { key: "new-layout", text: t("settings_appearance.ui_new_layout"), illustration: } - ]} - /> - - - }, - { key: "horizontal", text: t("theme.layout-horizontal-title"), illustration: } - ]} + + + + {!isMobile() && <> + + { + await setNewLayout(newValue === "new-layout"); + reloadFrontendApp(); + }} + values={[ + { key: "old-layout", text: t("settings_appearance.ui_old_layout"), illustration: }, + { key: "new-layout", text: t("settings_appearance.ui_new_layout"), illustration: } + ]} + /> + + + }, + { key: "horizontal", text: t("theme.layout-horizontal-title"), illustration: } + ]} + /> + + } ); } @@ -286,31 +304,6 @@ function OrientationIllustration({ orientation }: { orientation: "vertical" | "h ); } -function ApplicationTheme() { - const [ theme, setTheme ] = useTriliumOption("theme", true); - const [ themes, setThemes ] = useState([]); - - useEffect(() => { - server.get("options/user-themes").then((userThemes) => { - setThemes([ - ...BUILTIN_THEMES, - ...userThemes - ]); - }); - }, []); - - return ( - - - - - - ); -} - function Fonts() { const [ overrideThemeFonts, setOverrideThemeFonts ] = useTriliumOptionBool("overrideThemeFonts"); const isEnabled = overrideThemeFonts === true; From b88eaaeeec96da021169aca37c2ed46e2eea37e2 Mon Sep 17 00:00:00 2001 From: Elian Doran Date: Mon, 13 Apr 2026 17:34:49 +0300 Subject: [PATCH 40/54] feat(options/appearance): group and add icons to theme selector --- .../src/translations/en/translation.json | 15 ++-- .../type_widgets/options/appearance.tsx | 82 +++++++++++++++---- 2 files changed, 74 insertions(+), 23 deletions(-) diff --git a/apps/client/src/translations/en/translation.json b/apps/client/src/translations/en/translation.json index 7b6a16c753..dad15c29f6 100644 --- a/apps/client/src/translations/en/translation.json +++ b/apps/client/src/translations/en/translation.json @@ -1234,12 +1234,15 @@ "title": "User Interface", "theme_label": "Application theme", "override_theme_fonts_label": "Override theme fonts", - "auto_theme": "Legacy (Follow system color scheme)", - "light_theme": "Legacy (Light)", - "dark_theme": "Legacy (Dark)", - "triliumnext": "Trilium (Follow system color scheme)", - "triliumnext-light": "Trilium (Light)", - "triliumnext-dark": "Trilium (Dark)", + "auto_theme": "Follow system color scheme", + "light_theme": "Light", + "dark_theme": "Dark", + "triliumnext": "Follow system color scheme", + "triliumnext-light": "Light", + "triliumnext-dark": "Dark", + "modern_themes": "Modern", + "legacy_themes": "Legacy", + "custom_themes": "Custom", "layout": "Layout", "layout-vertical-title": "Vertical", "layout-horizontal-title": "Horizontal", diff --git a/apps/client/src/widgets/type_widgets/options/appearance.tsx b/apps/client/src/widgets/type_widgets/options/appearance.tsx index c6de5ff760..cd2acfa13e 100644 --- a/apps/client/src/widgets/type_widgets/options/appearance.tsx +++ b/apps/client/src/widgets/type_widgets/options/appearance.tsx @@ -11,10 +11,10 @@ import server from "../../../services/server"; import { isElectron, isMobile, reloadFrontendApp, restartDesktopApp } from "../../../services/utils"; import { VerticalLayoutIcon } from "../../buttons/global_menu"; import Button from "../../react/Button"; +import Dropdown from "../../react/Dropdown"; import FormCheckbox from "../../react/FormCheckbox"; import FormGroup from "../../react/FormGroup"; import FormList, { FormListHeader, FormListItem } from "../../react/FormList"; -import FormSelect from "../../react/FormSelect"; import { FormTextBoxWithUnit } from "../../react/FormTextBox"; import { useTriliumOption, useTriliumOptionBool } from "../../react/hooks"; import Icon from "../../react/Icon"; @@ -31,16 +31,20 @@ const MIN_CONTENT_WIDTH = 640; interface Theme { val: string; title: string; + icon?: string; noteId?: string; } -const BUILTIN_THEMES: Theme[] = [ - { val: "next", title: t("theme.triliumnext") }, - { val: "next-light", title: t("theme.triliumnext-light") }, - { val: "next-dark", title: t("theme.triliumnext-dark") }, - { val: "auto", title: t("theme.auto_theme") }, - { val: "light", title: t("theme.light_theme") }, - { val: "dark", title: t("theme.dark_theme") } +const MODERN_THEMES: Theme[] = [ + { val: "next", title: t("theme.triliumnext"), icon: "bx bx-sun bx-flip-horizontal" }, + { val: "next-light", title: t("theme.triliumnext-light"), icon: "bx bx-sun" }, + { val: "next-dark", title: t("theme.triliumnext-dark"), icon: "bx bx-moon" } +]; + +const LEGACY_THEMES: Theme[] = [ + { val: "auto", title: t("theme.auto_theme"), icon: "bx bx-sun bx-flip-horizontal" }, + { val: "light", title: t("theme.light_theme"), icon: "bx bx-sun" }, + { val: "dark", title: t("theme.dark_theme"), icon: "bx bx-moon" } ]; interface FontFamilyEntry { @@ -115,26 +119,70 @@ export default function AppearanceSettings() { function UserInterface() { const [ theme, setTheme ] = useTriliumOption("theme", true); - const [ themes, setThemes ] = useState([]); + const [ customThemes, setCustomThemes ] = useState([]); const [ newLayout, setNewLayout ] = useTriliumOptionBool("newLayout"); const [ layoutOrientation, setLayoutOrientation ] = useTriliumOption("layoutOrientation", true); useEffect(() => { server.get("options/user-themes").then((userThemes) => { - setThemes([ - ...BUILTIN_THEMES, - ...userThemes - ]); + // Add placeholder icon for custom themes + setCustomThemes(userThemes.map(t => ({ ...t, icon: "bx bx-palette" }))); }); }, []); + // Find current theme for display + const allThemes = [...MODERN_THEMES, ...LEGACY_THEMES, ...customThemes]; + const currentTheme = allThemes.find(t => t.val === theme); + const currentThemeLabel = currentTheme?.title ?? theme ?? ""; + const currentThemeIcon = currentTheme?.icon ?? "bx bx-palette"; + return ( - + + + {currentThemeLabel} + } + > + + {MODERN_THEMES.map(t => ( + setTheme(t.val)} + > + {t.title} + + ))} + + {LEGACY_THEMES.map(t => ( + setTheme(t.val)} + > + {t.title} + + ))} + {customThemes.length > 0 && ( + <> + + {customThemes.map(t => ( + setTheme(t.val)} + > + {t.title} + + ))} + + )} + {!isMobile() && <> From 7180569357e049e4baa6efe1286eeea528b9e54f Mon Sep 17 00:00:00 2001 From: Elian Doran Date: Mon, 13 Apr 2026 17:37:39 +0300 Subject: [PATCH 41/54] feat(options/appearance): display icon for custom themes --- .../type_widgets/options/appearance.tsx | 73 ++++++++++++++----- 1 file changed, 55 insertions(+), 18 deletions(-) diff --git a/apps/client/src/widgets/type_widgets/options/appearance.tsx b/apps/client/src/widgets/type_widgets/options/appearance.tsx index cd2acfa13e..c0c8308b82 100644 --- a/apps/client/src/widgets/type_widgets/options/appearance.tsx +++ b/apps/client/src/widgets/type_widgets/options/appearance.tsx @@ -16,7 +16,7 @@ import FormCheckbox from "../../react/FormCheckbox"; import FormGroup from "../../react/FormGroup"; import FormList, { FormListHeader, FormListItem } from "../../react/FormList"; import { FormTextBoxWithUnit } from "../../react/FormTextBox"; -import { useTriliumOption, useTriliumOptionBool } from "../../react/hooks"; +import { useNote, useNoteIcon, useTriliumOption, useTriliumOptionBool } from "../../react/hooks"; import Icon from "../../react/Icon"; import Modal from "../../react/Modal"; import Slider from "../../react/Slider"; @@ -47,6 +47,51 @@ const LEGACY_THEMES: Theme[] = [ { val: "dark", title: t("theme.dark_theme"), icon: "bx bx-moon" } ]; +interface CustomThemeItemProps { + theme: Theme; + selected: boolean; + onClick: () => void; +} + +function CustomThemeItem({ theme, selected, onClick }: CustomThemeItemProps) { + const note = useNote(theme.noteId); + const noteIcon = useNoteIcon(note); + const icon = noteIcon ?? "bx bx-palette"; + + return ( + + {theme.title} + + ); +} + +interface CurrentThemeDisplayProps { + theme: Theme | undefined; + fallbackLabel: string; +} + +function CurrentThemeDisplay({ theme, fallbackLabel }: CurrentThemeDisplayProps) { + const note = useNote(theme?.noteId); + const noteIcon = useNoteIcon(note); + + // For built-in themes, use the icon from the theme object + // For custom themes, use the note icon or fallback to palette + const icon = theme?.icon ?? noteIcon ?? "bx bx-palette"; + const label = theme?.title ?? fallbackLabel; + + return ( + <> + + {label} + + ); +} + interface FontFamilyEntry { value: FontFamily; label?: string; @@ -125,25 +170,19 @@ function UserInterface() { useEffect(() => { server.get("options/user-themes").then((userThemes) => { - // Add placeholder icon for custom themes - setCustomThemes(userThemes.map(t => ({ ...t, icon: "bx bx-palette" }))); + setCustomThemes(userThemes); }); }, []); // Find current theme for display const allThemes = [...MODERN_THEMES, ...LEGACY_THEMES, ...customThemes]; const currentTheme = allThemes.find(t => t.val === theme); - const currentThemeLabel = currentTheme?.title ?? theme ?? ""; - const currentThemeIcon = currentTheme?.icon ?? "bx bx-palette"; return ( - - {currentThemeLabel} - } + text={} > {MODERN_THEMES.map(t => ( @@ -170,15 +209,13 @@ function UserInterface() { {customThemes.length > 0 && ( <> - {customThemes.map(t => ( - setTheme(t.val)} - > - {t.title} - + {customThemes.map(ct => ( + setTheme(ct.val)} + /> ))} )} From d4333400c41c926b376008b3c6dd403af5e036d1 Mon Sep 17 00:00:00 2001 From: Elian Doran Date: Mon, 13 Apr 2026 17:39:57 +0300 Subject: [PATCH 42/54] refactor(options/appearance): use simpler mechanism for theme note icon --- .../type_widgets/options/appearance.tsx | 86 ++++++------------- apps/server/src/routes/api/options.ts | 4 +- 2 files changed, 27 insertions(+), 63 deletions(-) diff --git a/apps/client/src/widgets/type_widgets/options/appearance.tsx b/apps/client/src/widgets/type_widgets/options/appearance.tsx index c0c8308b82..2a2cfdd390 100644 --- a/apps/client/src/widgets/type_widgets/options/appearance.tsx +++ b/apps/client/src/widgets/type_widgets/options/appearance.tsx @@ -16,7 +16,7 @@ import FormCheckbox from "../../react/FormCheckbox"; import FormGroup from "../../react/FormGroup"; import FormList, { FormListHeader, FormListItem } from "../../react/FormList"; import { FormTextBoxWithUnit } from "../../react/FormTextBox"; -import { useNote, useNoteIcon, useTriliumOption, useTriliumOptionBool } from "../../react/hooks"; +import { useTriliumOption, useTriliumOptionBool } from "../../react/hooks"; import Icon from "../../react/Icon"; import Modal from "../../react/Modal"; import Slider from "../../react/Slider"; @@ -47,51 +47,6 @@ const LEGACY_THEMES: Theme[] = [ { val: "dark", title: t("theme.dark_theme"), icon: "bx bx-moon" } ]; -interface CustomThemeItemProps { - theme: Theme; - selected: boolean; - onClick: () => void; -} - -function CustomThemeItem({ theme, selected, onClick }: CustomThemeItemProps) { - const note = useNote(theme.noteId); - const noteIcon = useNoteIcon(note); - const icon = noteIcon ?? "bx bx-palette"; - - return ( - - {theme.title} - - ); -} - -interface CurrentThemeDisplayProps { - theme: Theme | undefined; - fallbackLabel: string; -} - -function CurrentThemeDisplay({ theme, fallbackLabel }: CurrentThemeDisplayProps) { - const note = useNote(theme?.noteId); - const noteIcon = useNoteIcon(note); - - // For built-in themes, use the icon from the theme object - // For custom themes, use the note icon or fallback to palette - const icon = theme?.icon ?? noteIcon ?? "bx bx-palette"; - const label = theme?.title ?? fallbackLabel; - - return ( - <> - - {label} - - ); -} - interface FontFamilyEntry { value: FontFamily; label?: string; @@ -177,45 +132,52 @@ function UserInterface() { // Find current theme for display const allThemes = [...MODERN_THEMES, ...LEGACY_THEMES, ...customThemes]; const currentTheme = allThemes.find(t => t.val === theme); + const currentThemeIcon = currentTheme?.icon ?? "bx bx-palette"; + const currentThemeLabel = currentTheme?.title ?? theme ?? ""; return ( } + text={<> + + {currentThemeLabel} + } > - {MODERN_THEMES.map(t => ( + {MODERN_THEMES.map(th => ( setTheme(t.val)} + key={th.val} + icon={th.icon} + selected={theme === th.val} + onClick={() => setTheme(th.val)} > - {t.title} + {th.title} ))} - {LEGACY_THEMES.map(t => ( + {LEGACY_THEMES.map(th => ( setTheme(t.val)} + key={th.val} + icon={th.icon} + selected={theme === th.val} + onClick={() => setTheme(th.val)} > - {t.title} + {th.title} ))} {customThemes.length > 0 && ( <> {customThemes.map(ct => ( - setTheme(ct.val)} - /> + > + {ct.title} + ))} )} diff --git a/apps/server/src/routes/api/options.ts b/apps/server/src/routes/api/options.ts index 9be9ba0670..aecca72e91 100644 --- a/apps/server/src/routes/api/options.ts +++ b/apps/server/src/routes/api/options.ts @@ -14,6 +14,7 @@ interface UserTheme { val: string; // value of the theme, used in the URL title: string; // title of the theme, displayed in the UI noteId: string; // ID of the note containing the theme + icon: string; // icon class of the note } // options allowed to be updated directly in the Options dialog @@ -189,7 +190,8 @@ function getUserThemes() { ret.push({ val: value, title: note.title, - noteId: note.noteId + noteId: note.noteId, + icon: note.getIcon() }); } From 0f605ba99491479e6d2cc8cddce64e4a1388a201 Mon Sep 17 00:00:00 2001 From: Elian Doran Date: Mon, 13 Apr 2026 17:45:23 +0300 Subject: [PATCH 43/54] feat(options/appearance): improve desktop-specific options --- .../type_widgets/options/appearance.tsx | 41 +++++++++---------- .../options/components/OptionsRow.tsx | 6 +-- 2 files changed, 23 insertions(+), 24 deletions(-) diff --git a/apps/client/src/widgets/type_widgets/options/appearance.tsx b/apps/client/src/widgets/type_widgets/options/appearance.tsx index 2a2cfdd390..c375b05594 100644 --- a/apps/client/src/widgets/type_widgets/options/appearance.tsx +++ b/apps/client/src/widgets/type_widgets/options/appearance.tsx @@ -10,10 +10,7 @@ import { t } from "../../../services/i18n"; import server from "../../../services/server"; import { isElectron, isMobile, reloadFrontendApp, restartDesktopApp } from "../../../services/utils"; import { VerticalLayoutIcon } from "../../buttons/global_menu"; -import Button from "../../react/Button"; import Dropdown from "../../react/Dropdown"; -import FormCheckbox from "../../react/FormCheckbox"; -import FormGroup from "../../react/FormGroup"; import FormList, { FormListHeader, FormListItem } from "../../react/FormList"; import { FormTextBoxWithUnit } from "../../react/FormTextBox"; import { useTriliumOption, useTriliumOptionBool } from "../../react/hooks"; @@ -554,26 +551,28 @@ function ElectronIntegration() { /> - - - + - - - {t("electron_integration.background-effects")} - {" "} - - } - currentValue={backgroundEffects} onChange={setBackgroundEffects} - disabled={nativeTitleBarVisible} - /> - + {t("electron_integration.background-effects")} } + description={t("electron_integration.background-effects-description")} + currentValue={backgroundEffects} + onChange={setBackgroundEffects} + disabled={nativeTitleBarVisible} + /> -