Skip to content

Commit 23e259c

Browse files
committed
feat(mdviewer): dark/light theme toggle, typography, toolbar collapse
- Add theme toggle button (sun/moon) in md viewer embedded toolbar, visible in both edit and reader modes - Theme persisted via PreferencesManager preference (mdViewerTheme) - Communication: iframe sends mdviewrThemeToggle → MarkdownSync persists and sends MDVIEWR_SET_THEME back to apply - Content max-width changed to 90ch for paper-like readability - Responsive padding: 70px on wider viewports, 24px on narrow - Collapse all toolbar format groups below 590px panel width - Update CLAUDE.md and CLAUDE-markdown-viewer.md to document md viewer's own i18n system (src-mdviewer/src/locales/en.json)
1 parent 86c79d9 commit 23e259c

8 files changed

Lines changed: 90 additions & 17 deletions

File tree

CLAUDE.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,7 @@
2020
- For parameterized strings use `StringUtils.format(Strings.KEY, arg0, arg1)` with `{0}`, `{1}` placeholders.
2121
- Keys use UPPER_SNAKE_CASE grouped by feature prefix (e.g. `AI_CHAT_*`).
2222
- Only `src/nls/root/strings.js` (English) needs manual edits — other locales are auto-translated by GitHub Actions.
23+
- **Exception — Markdown viewer iframe** (`src-mdviewer/`): Has its own i18n system. Strings go in `src-mdviewer/src/locales/en.json` (root), not `src/nls/`. Other locale files in that folder are auto-translated by GitHub Actions. Use `t("key")` / `tp("key", { param })` from `src-mdviewer/src/core/i18n.js`.
2324
- Never compare `$(el).text()` against English strings for logic — use data attributes or CSS classes instead.
2425

2526
## Phoenix MCP (Desktop App Testing)

src-mdviewer/CLAUDE-markdown-viewer.md

Lines changed: 9 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -21,11 +21,18 @@ Test Runner Window
2121
- `src-mdviewer/src/bridge.js` — postMessage bridge between Phoenix and md iframe. Handles file switching, content sync, keyboard shortcuts, edit mode.
2222
- `src-mdviewer/src/core/doc-cache.js` — Document DOM cache with LRU eviction for file switching.
2323
- `src-mdviewer/src/components/editor.js` — Contenteditable WYSIWYG editing, Turndown HTML→Markdown conversion.
24-
- `src-mdviewer/src/components/embedded-toolbar.js` — Reader/edit toggle, cursor sync, format buttons.
24+
- `src-mdviewer/src/components/embedded-toolbar.js` — Reader/edit toggle, cursor sync, theme toggle, format buttons.
2525
- `src-mdviewer/src/components/format-bar.js` — Floating format bar on text selection (bold, italic, underline, link).
2626
- `src-mdviewer/src/components/link-popover.js` — Link popover for editing/removing links in edit mode.
2727
- `src-mdviewer/src/components/viewer.js` — Reader mode click handling, link interception, copy buttons.
28-
- `src/extensionsIntegrated/Phoenix-live-preview/MarkdownSync.js` — Phoenix-side sync: CM↔iframe content, cursor, scroll, selection.
28+
- `src/extensionsIntegrated/Phoenix-live-preview/MarkdownSync.js` — Phoenix-side sync: CM↔iframe content, cursor, scroll, selection, theme.
29+
30+
### Translations / i18n
31+
- The md viewer iframe has its **own** i18n system separate from Phoenix's `src/nls/` strings.
32+
- Root strings (English) are in `src-mdviewer/src/locales/en.json` — edit this file for new/changed strings.
33+
- Other locale files in `src-mdviewer/src/locales/` are **auto-translated by GitHub Actions** — do not edit them manually.
34+
- Use `t("key.subkey")` and `tp("key", { param })` from `src-mdviewer/src/core/i18n.js` for string lookups.
35+
- Phoenix-side strings (e.g. preference descriptions) still go in `src/nls/root/strings.js` as usual.
2936

3037
## Communication: postMessage (reliable in both directions)
3138

src-mdviewer/src/bridge.js

Lines changed: 7 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -806,11 +806,13 @@ function handleReloadFile(data) {
806806

807807
// --- Theme, edit mode, locale ---
808808

809-
function handleSetTheme(_data) {
810-
// Force light theme for a paper-like appearance regardless of editor theme.
811-
// The theme infrastructure is preserved for future use.
812-
// Theme is set in index.html (data-theme="light") so no action needed here.
813-
// Avoid setting attributes/styles to prevent reflows that reset scroll position.
809+
function handleSetTheme(data) {
810+
const { theme } = data;
811+
// Skip if already applied to avoid reflows that can reset scroll position
812+
if (document.documentElement.getAttribute("data-theme") === theme) return;
813+
document.documentElement.setAttribute("data-theme", theme);
814+
document.documentElement.style.colorScheme = theme === "dark" ? "dark" : "light";
815+
setState({ theme });
814816
}
815817

816818
function handleSetEditMode(data) {

src-mdviewer/src/components/embedded-toolbar.js

Lines changed: 33 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -32,7 +32,9 @@ import {
3232
Link2Off,
3333
Printer,
3434
Image as ImageIcon,
35-
Upload
35+
Upload,
36+
Sun,
37+
Moon
3638
} from "lucide";
3739
import { on, emit } from "../core/events.js";
3840
import { getState, setState } from "../core/state.js";
@@ -45,11 +47,11 @@ let collapseLevel = 0; // 0=expanded, 1=blocks, 2=blocks+lists, 3=all
4547

4648
// Width thresholds for progressive collapse
4749
const THRESHOLD_BLOCKS = 640; // collapse block elements + image first
48-
const THRESHOLD_LISTS = 520; // then lists
49-
const THRESHOLD_TEXT = 500; // finally text formatting (all dropdowns collapsed)
50+
const THRESHOLD_LISTS = 590; // then lists
51+
const THRESHOLD_TEXT = 590; // finally text formatting (all dropdowns collapsed)
5052

5153
const allIcons = { Bold, Italic, Strikethrough, Underline, Code, Link, List, ListOrdered,
52-
ListChecks, Quote, Minus, Table, FileCode, ChevronDown, Type, MoreHorizontal, Pencil, BookOpen, Link2, Link2Off, Printer, Image: ImageIcon, Upload };
54+
ListChecks, Quote, Minus, Table, FileCode, ChevronDown, Type, MoreHorizontal, Pencil, BookOpen, Link2, Link2Off, Printer, Image: ImageIcon, Upload, Sun, Moon };
5355

5456
export function initEmbeddedToolbar() {
5557
toolbar = document.getElementById("toolbar");
@@ -58,6 +60,7 @@ export function initEmbeddedToolbar() {
5860
render();
5961

6062
on("state:editMode", () => render());
63+
on("state:theme", () => render());
6164
on("editor:selection-state", updateFormatState);
6265
on("state:locale", () => render());
6366
}
@@ -80,8 +83,12 @@ function render() {
8083
}
8184

8285
function renderReadMode() {
86+
const isDark = getState().theme === "dark";
8387
toolbar.innerHTML = `<div class="embedded-toolbar">
8488
<div class="toolbar-spacer"></div>
89+
<button class="toolbar-btn theme-toggle-btn" id="emb-theme-toggle" data-tooltip="${t("toolbar.theme") || "Toggle theme"}">
90+
<i data-lucide="${isDark ? "sun" : "moon"}"></i>
91+
</button>
8592
<button class="toolbar-btn print-btn" id="emb-print-btn" data-tooltip="${t("toolbar.print") || "Print"}">
8693
<i data-lucide="printer"></i>
8794
</button>
@@ -96,9 +103,9 @@ function renderReadMode() {
96103
</div>`;
97104

98105
createIcons({ icons: allIcons, attrs: { class: "" } });
99-
// Remove data-lucide from replaced SVGs to prevent warnings on subsequent createIcons calls
100106
toolbar.querySelectorAll("svg[data-lucide]").forEach(svg => svg.removeAttribute("data-lucide"));
101107

108+
wireThemeToggle();
102109
wireCursorSyncButton();
103110
wirePrintButton();
104111

@@ -192,9 +199,13 @@ function renderEditMode(level) {
192199
${imageSection}
193200
</div>`;
194201

202+
const isDark = getState().theme === "dark";
195203
toolbar.innerHTML = `<div class="embedded-toolbar">
196204
${formatRow}
197205
<div class="toolbar-spacer"></div>
206+
<button class="toolbar-btn theme-toggle-btn" id="emb-theme-toggle" data-tooltip="${t("toolbar.theme") || "Toggle theme"}">
207+
<i data-lucide="${isDark ? "sun" : "moon"}"></i>
208+
</button>
198209
<button class="toolbar-btn print-btn" id="emb-print-btn" data-tooltip="${t("toolbar.print") || "Print"}">
199210
<i data-lucide="printer"></i>
200211
</button>
@@ -209,12 +220,12 @@ function renderEditMode(level) {
209220
</div>`;
210221

211222
createIcons({ icons: allIcons, attrs: { class: "" } });
212-
// Remove data-lucide from replaced SVGs to prevent warnings on subsequent createIcons calls
213223
toolbar.querySelectorAll("svg[data-lucide]").forEach(svg => svg.removeAttribute("data-lucide"));
214224

215225
wireFormatButtons();
216226
wireBlockTypeSelect();
217227
wireDropdowns();
228+
wireThemeToggle();
218229
wireCursorSyncButton();
219230
wirePrintButton();
220231
wireDoneButton();
@@ -307,6 +318,22 @@ function wireCursorSyncButton() {
307318
}
308319
}
309320

321+
function wireThemeToggle() {
322+
const toggleBtn = document.getElementById("emb-theme-toggle");
323+
if (toggleBtn) {
324+
toggleBtn.addEventListener("click", () => {
325+
const current = getState().theme || "light";
326+
const newTheme = current === "light" ? "dark" : "light";
327+
// Send to parent (Phoenix) for persistence
328+
window.parent.postMessage({
329+
type: "MDVIEWR_EVENT",
330+
eventName: "mdviewrThemeToggle",
331+
theme: newTheme
332+
}, "*");
333+
});
334+
}
335+
}
336+
310337
function wirePrintButton() {
311338
const printBtn = document.getElementById("emb-print-btn");
312339
if (printBtn) {

src-mdviewer/src/locales/en.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
{
22
"toolbar": {
33
"open": "Open File",
4-
"theme": "Toggle Theme",
4+
"theme": "Toggle Dark/Light Theme",
55
"toc": "Table of Contents",
66
"search": "Search",
77
"focus": "Focus Mode",

src/extensionsIntegrated/Phoenix-live-preview/MarkdownSync.js

Lines changed: 26 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -132,6 +132,13 @@ define(function (require, exports, module) {
132132
case "mdviewrCursorSyncToggle":
133133
_cursorSyncEnabled = !!data.enabled;
134134
break;
135+
case "mdviewrThemeToggle":
136+
sendThemeOverride(data.theme);
137+
// Persist via StateManager (accessed through main.js callback)
138+
if (_onThemeToggle) {
139+
_onThemeToggle(data.theme);
140+
}
141+
break;
135142
case "mdviewrImageUploadRequest":
136143
_handleImageUploadFromIframe(data);
137144
break;
@@ -409,6 +416,10 @@ define(function (require, exports, module) {
409416
iframeWindow.postMessage(msg, "*");
410417
}
411418

419+
// User's explicit theme choice (null = use editor theme)
420+
let _themeOverride = null;
421+
let _onThemeToggle = null;
422+
412423
function _sendTheme() {
413424
if (!_active || !_iframeReady) {
414425
return;
@@ -418,14 +429,24 @@ define(function (require, exports, module) {
418429
return;
419430
}
420431

421-
const currentTheme = ThemeManager.getCurrentTheme();
422-
const isDark = currentTheme && currentTheme.dark;
432+
let theme;
433+
if (_themeOverride) {
434+
theme = _themeOverride;
435+
} else {
436+
const currentTheme = ThemeManager.getCurrentTheme();
437+
theme = (currentTheme && currentTheme.dark) ? "dark" : "light";
438+
}
423439
iframeWindow.postMessage({
424440
type: "MDVIEWR_SET_THEME",
425-
theme: isDark ? "dark" : "light"
441+
theme: theme
426442
}, "*");
427443
}
428444

445+
function sendThemeOverride(theme) {
446+
_themeOverride = theme;
447+
_sendTheme();
448+
}
449+
429450
function _sendLocale() {
430451
if (!_active || !_iframeReady) {
431452
return;
@@ -1098,4 +1119,6 @@ define(function (require, exports, module) {
10981119
exports.setEditMode = setEditMode;
10991120
exports.setIframeReadyHandler = setIframeReadyHandler;
11001121
exports.setCursorSyncEnabled = setCursorSyncEnabled;
1122+
exports.sendThemeOverride = sendThemeOverride;
1123+
exports.setThemeToggleHandler = function(handler) { _onThemeToggle = handler; };
11011124
});

src/extensionsIntegrated/Phoenix-live-preview/main.js

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -83,6 +83,10 @@ define(function (require, exports, module) {
8383

8484
const StateManager = PreferencesManager.stateManager;
8585
const STATE_CUSTOM_SERVER_BANNER_ACK = "customServerBannerDone";
86+
const PREF_MD_THEME = "mdViewerTheme";
87+
PreferencesManager.definePreference(PREF_MD_THEME, "string", "light", {
88+
description: Strings.MD_VIEWER_THEME_DESCRIPTION
89+
});
8690
let customServerModalBar;
8791

8892
const isBrowser = !Phoenix.isNativeApp;
@@ -788,6 +792,11 @@ define(function (require, exports, module) {
788792
$modeBtn = $panel.find("#livePreviewModeBtn");
789793
$previewBtn = $panel.find("#previewModeLivePreviewButton");
790794

795+
// Markdown theme toggle — persist user choice
796+
MarkdownSync.setThemeToggleHandler((theme) => {
797+
PreferencesManager.set(PREF_MD_THEME, theme);
798+
});
799+
791800
$panel.find(".live-preview-settings-banner-btn").on("click", ()=>{
792801
CommandManager.execute(Commands.FILE_LIVE_FILE_PREVIEW_SETTINGS);
793802
Metrics.countEvent(Metrics.EVENT_TYPE.LIVE_PREVIEW, "settingsBtnBanner", "click");
@@ -919,6 +928,9 @@ define(function (require, exports, module) {
919928

920929
_isMdviewrActive = true;
921930
MarkdownSync.activate(currentDoc, $iframe, baseURL);
931+
// Apply persisted theme preference
932+
const savedTheme = PreferencesManager.get(PREF_MD_THEME) || "light";
933+
MarkdownSync.sendThemeOverride(savedTheme);
922934
// Sync preview mode and edit mode for reuse case where iframe is already ready
923935
_updateLPControlsForMdviewer();
924936

src/nls/root/strings.js

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -161,6 +161,7 @@ define({
161161
"LIVE_DEV_OPEN_ERROR_TITLE": "Error Opening Live Preview in {0}",
162162
"LIVE_DEV_OPEN_ERROR_MESSAGE": "Make sure that {0} browser is installed and try again.",
163163
"LIVE_DEV_CLICK_TO_PIN_UNPIN": "Pin or Unpin Preview Page",
164+
"MD_VIEWER_THEME_DESCRIPTION": "Theme for the Markdown viewer (light or dark)",
164165
"LIVE_DEV_STATUS_TIP_SYNC_ERROR": "Live Preview (not updating due to syntax error)",
165166
"LIVE_DEV_SETTINGS": "Live Preview Settings\u2026",
166167
"LIVE_DEV_SETTINGS_BANNER": "Set up a custom server to live preview `<b>{0}</b>` and other server-rendered files (PHP, JSP, etc.)",

0 commit comments

Comments
 (0)