Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
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
1 change: 1 addition & 0 deletions CLAUDE.md
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@
- For parameterized strings use `StringUtils.format(Strings.KEY, arg0, arg1)` with `{0}`, `{1}` placeholders.
- Keys use UPPER_SNAKE_CASE grouped by feature prefix (e.g. `AI_CHAT_*`).
- Only `src/nls/root/strings.js` (English) needs manual edits — other locales are auto-translated by GitHub Actions.
- **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`.
- Never compare `$(el).text()` against English strings for logic — use data attributes or CSS classes instead.

## Phoenix MCP (Desktop App Testing)
Expand Down
18 changes: 18 additions & 0 deletions docs/API-Reference/search/FindUtils.md
Original file line number Diff line number Diff line change
Expand Up @@ -107,6 +107,24 @@ enable/disable instant search
## isInstantSearchDisabled() ⇒ <code>boolean</code>
if instant search is disabled, this will return true we can only do instant search through worker

**Kind**: global function
<a name="setIndexingSuspended"></a>

## setIndexingSuspended(suspended)
Set whether indexing has been suspended due to cache size limit

**Kind**: global function

| Param | Type | Description |
| --- | --- | --- |
| suspended | <code>boolean</code> | true if indexing was suspended |

<a name="isIndexingSuspended"></a>

## isIndexingSuspended() ⇒ <code>boolean</code>
Check if indexing was suspended due to cache size limit.
When true, Find in Files should not perform searches.

**Kind**: global function
<a name="isWorkerSearchInProgress"></a>

Expand Down
11 changes: 9 additions & 2 deletions src-mdviewer/CLAUDE-markdown-viewer.md
Original file line number Diff line number Diff line change
Expand Up @@ -21,11 +21,18 @@ Test Runner Window
- `src-mdviewer/src/bridge.js` — postMessage bridge between Phoenix and md iframe. Handles file switching, content sync, keyboard shortcuts, edit mode.
- `src-mdviewer/src/core/doc-cache.js` — Document DOM cache with LRU eviction for file switching.
- `src-mdviewer/src/components/editor.js` — Contenteditable WYSIWYG editing, Turndown HTML→Markdown conversion.
- `src-mdviewer/src/components/embedded-toolbar.js` — Reader/edit toggle, cursor sync, format buttons.
- `src-mdviewer/src/components/embedded-toolbar.js` — Reader/edit toggle, cursor sync, theme toggle, format buttons.
- `src-mdviewer/src/components/format-bar.js` — Floating format bar on text selection (bold, italic, underline, link).
- `src-mdviewer/src/components/link-popover.js` — Link popover for editing/removing links in edit mode.
- `src-mdviewer/src/components/viewer.js` — Reader mode click handling, link interception, copy buttons.
- `src/extensionsIntegrated/Phoenix-live-preview/MarkdownSync.js` — Phoenix-side sync: CM↔iframe content, cursor, scroll, selection.
- `src/extensionsIntegrated/Phoenix-live-preview/MarkdownSync.js` — Phoenix-side sync: CM↔iframe content, cursor, scroll, selection, theme.

### Translations / i18n
- The md viewer iframe has its **own** i18n system separate from Phoenix's `src/nls/` strings.
- Root strings (English) are in `src-mdviewer/src/locales/en.json` — edit this file for new/changed strings.
- Other locale files in `src-mdviewer/src/locales/` are **auto-translated by GitHub Actions** — do not edit them manually.
- Use `t("key.subkey")` and `tp("key", { param })` from `src-mdviewer/src/core/i18n.js` for string lookups.
- Phoenix-side strings (e.g. preference descriptions) still go in `src/nls/root/strings.js` as usual.

## Communication: postMessage (reliable in both directions)

Expand Down
10 changes: 5 additions & 5 deletions src-mdviewer/index.html
Original file line number Diff line number Diff line change
Expand Up @@ -5,12 +5,12 @@
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<meta name="color-scheme" content="light dark" />
<meta http-equiv="Content-Security-Policy"
content="default-src 'self';
script-src 'self';
style-src 'self' 'unsafe-inline';
content="default-src 'self' http://localhost:* https://localhost:* phtauri: https://phtauri.localhost https://*.phcode.dev;
script-src 'self' http://localhost:* https://localhost:* phtauri: https://phtauri.localhost https://*.phcode.dev;
style-src 'self' 'unsafe-inline' http://localhost:* https://localhost:* phtauri: https://phtauri.localhost https://*.phcode.dev;
img-src 'self' blob: data: phtauri: https: http:;
font-src 'self' data:;
connect-src 'self';" />
font-src 'self' data: http://localhost:* https://localhost:* phtauri: https://phtauri.localhost https://*.phcode.dev;
connect-src 'self' http://localhost:* https://localhost:* ws://localhost:* wss://localhost:* phtauri: https://phtauri.localhost https://*.phcode.dev;" />
<script type="module" src="/src/embedded-main.js"></script>
</head>
<body>
Expand Down
16 changes: 11 additions & 5 deletions src-mdviewer/src/bridge.js
Original file line number Diff line number Diff line change
Expand Up @@ -806,11 +806,17 @@ function handleReloadFile(data) {

// --- Theme, edit mode, locale ---

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

function handleSetEditMode(data) {
Expand Down
39 changes: 33 additions & 6 deletions src-mdviewer/src/components/embedded-toolbar.js
Original file line number Diff line number Diff line change
Expand Up @@ -32,7 +32,9 @@ import {
Link2Off,
Printer,
Image as ImageIcon,
Upload
Upload,
Sun,
Moon
} from "lucide";
import { on, emit } from "../core/events.js";
import { getState, setState } from "../core/state.js";
Expand All @@ -45,11 +47,11 @@ let collapseLevel = 0; // 0=expanded, 1=blocks, 2=blocks+lists, 3=all

// Width thresholds for progressive collapse
const THRESHOLD_BLOCKS = 640; // collapse block elements + image first
const THRESHOLD_LISTS = 520; // then lists
const THRESHOLD_TEXT = 500; // finally text formatting (all dropdowns collapsed)
const THRESHOLD_LISTS = 590; // then lists
const THRESHOLD_TEXT = 590; // finally text formatting (all dropdowns collapsed)

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

export function initEmbeddedToolbar() {
toolbar = document.getElementById("toolbar");
Expand All @@ -58,6 +60,7 @@ export function initEmbeddedToolbar() {
render();

on("state:editMode", () => render());
on("state:theme", () => render());
on("editor:selection-state", updateFormatState);
on("state:locale", () => render());
}
Expand All @@ -80,8 +83,12 @@ function render() {
}

function renderReadMode() {
const isDark = getState().theme === "dark";
toolbar.innerHTML = `<div class="embedded-toolbar">
<div class="toolbar-spacer"></div>
<button class="toolbar-btn theme-toggle-btn" id="emb-theme-toggle" data-tooltip="${t("toolbar.theme") || "Toggle theme"}">
<i data-lucide="${isDark ? "sun" : "moon"}"></i>
</button>
<button class="toolbar-btn print-btn" id="emb-print-btn" data-tooltip="${t("toolbar.print") || "Print"}">
<i data-lucide="printer"></i>
</button>
Expand All @@ -96,9 +103,9 @@ function renderReadMode() {
</div>`;

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

wireThemeToggle();
wireCursorSyncButton();
wirePrintButton();

Expand Down Expand Up @@ -192,9 +199,13 @@ function renderEditMode(level) {
${imageSection}
</div>`;

const isDark = getState().theme === "dark";
toolbar.innerHTML = `<div class="embedded-toolbar">
${formatRow}
<div class="toolbar-spacer"></div>
<button class="toolbar-btn theme-toggle-btn" id="emb-theme-toggle" data-tooltip="${t("toolbar.theme") || "Toggle theme"}">
<i data-lucide="${isDark ? "sun" : "moon"}"></i>
</button>
<button class="toolbar-btn print-btn" id="emb-print-btn" data-tooltip="${t("toolbar.print") || "Print"}">
<i data-lucide="printer"></i>
</button>
Expand All @@ -209,12 +220,12 @@ function renderEditMode(level) {
</div>`;

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

wireFormatButtons();
wireBlockTypeSelect();
wireDropdowns();
wireThemeToggle();
wireCursorSyncButton();
wirePrintButton();
wireDoneButton();
Expand Down Expand Up @@ -307,6 +318,22 @@ function wireCursorSyncButton() {
}
}

function wireThemeToggle() {
const toggleBtn = document.getElementById("emb-theme-toggle");
if (toggleBtn) {
toggleBtn.addEventListener("click", () => {
const current = getState().theme || "light";
const newTheme = current === "light" ? "dark" : "light";
// Send to parent (Phoenix) for persistence
window.parent.postMessage({
type: "MDVIEWR_EVENT",
eventName: "mdviewrThemeToggle",
theme: newTheme
}, "*");
});
}
}

function wirePrintButton() {
const printBtn = document.getElementById("emb-print-btn");
if (printBtn) {
Expand Down
4 changes: 4 additions & 0 deletions src-mdviewer/src/core/doc-cache.js
Original file line number Diff line number Diff line change
Expand Up @@ -176,6 +176,10 @@ export function saveActiveScrollPos() {
const entry = cache.get(activeFilePath);
if (!entry) return;

// Don't overwrite scroll position if viewer is hidden (e.g. panel closed)
// — hidden elements report scrollTop = 0 which would destroy the saved value.
if (!viewerContainer.offsetParent && viewerContainer.scrollTop === 0) return;

entry.scrollPos = viewerContainer.scrollTop;

// Also save source line for reload scenarios (DOM rebuilt, pixel pos unreliable)
Expand Down
2 changes: 1 addition & 1 deletion src-mdviewer/src/locales/en.json
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
{
"toolbar": {
"open": "Open File",
"theme": "Toggle Theme",
"theme": "Toggle Dark/Light Theme",
"toc": "Table of Contents",
"search": "Search",
"focus": "Focus Mode",
Expand Down
29 changes: 26 additions & 3 deletions src/extensionsIntegrated/Phoenix-live-preview/MarkdownSync.js
Original file line number Diff line number Diff line change
Expand Up @@ -132,6 +132,13 @@
case "mdviewrCursorSyncToggle":
_cursorSyncEnabled = !!data.enabled;
break;
case "mdviewrThemeToggle":
sendThemeOverride(data.theme);
// Persist via StateManager (accessed through main.js callback)
if (_onThemeToggle) {
_onThemeToggle(data.theme);
}
break;
case "mdviewrImageUploadRequest":
_handleImageUploadFromIframe(data);
break;
Expand Down Expand Up @@ -409,6 +416,10 @@
iframeWindow.postMessage(msg, "*");
}

// User's explicit theme choice (null = use editor theme)
let _themeOverride = null;
let _onThemeToggle = null;

function _sendTheme() {
if (!_active || !_iframeReady) {
return;
Expand All @@ -418,14 +429,24 @@
return;
}

const currentTheme = ThemeManager.getCurrentTheme();
const isDark = currentTheme && currentTheme.dark;
let theme;
if (_themeOverride) {
theme = _themeOverride;
} else {
const currentTheme = ThemeManager.getCurrentTheme();
theme = (currentTheme && currentTheme.dark) ? "dark" : "light";

Check warning on line 437 in src/extensionsIntegrated/Phoenix-live-preview/MarkdownSync.js

View check run for this annotation

SonarQubeCloud / SonarCloud Code Analysis

Prefer using an optional chain expression instead, as it's more concise and easier to read.

See more on https://sonarcloud.io/project/issues?id=phcode-dev_phoenix&issues=AZ1s7ixIGCWvKhGOCIJ1&open=AZ1s7ixIGCWvKhGOCIJ1&pullRequest=2803
}
iframeWindow.postMessage({
type: "MDVIEWR_SET_THEME",
theme: isDark ? "dark" : "light"
theme: theme
}, "*");
}

function sendThemeOverride(theme) {
_themeOverride = theme;
_sendTheme();
}

function _sendLocale() {
if (!_active || !_iframeReady) {
return;
Expand Down Expand Up @@ -1098,4 +1119,6 @@
exports.setEditMode = setEditMode;
exports.setIframeReadyHandler = setIframeReadyHandler;
exports.setCursorSyncEnabled = setCursorSyncEnabled;
exports.sendThemeOverride = sendThemeOverride;
exports.setThemeToggleHandler = function(handler) { _onThemeToggle = handler; };
});
12 changes: 12 additions & 0 deletions src/extensionsIntegrated/Phoenix-live-preview/main.js
Original file line number Diff line number Diff line change
Expand Up @@ -83,6 +83,10 @@ define(function (require, exports, module) {

const StateManager = PreferencesManager.stateManager;
const STATE_CUSTOM_SERVER_BANNER_ACK = "customServerBannerDone";
const PREF_MD_THEME = "mdViewerTheme";
PreferencesManager.definePreference(PREF_MD_THEME, "string", "light", {
description: Strings.MD_VIEWER_THEME_DESCRIPTION
});
let customServerModalBar;

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

// Markdown theme toggle — persist user choice
MarkdownSync.setThemeToggleHandler((theme) => {
PreferencesManager.set(PREF_MD_THEME, theme);
});

$panel.find(".live-preview-settings-banner-btn").on("click", ()=>{
CommandManager.execute(Commands.FILE_LIVE_FILE_PREVIEW_SETTINGS);
Metrics.countEvent(Metrics.EVENT_TYPE.LIVE_PREVIEW, "settingsBtnBanner", "click");
Expand Down Expand Up @@ -919,6 +928,9 @@ define(function (require, exports, module) {

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

Expand Down
1 change: 1 addition & 0 deletions src/nls/root/strings.js
Original file line number Diff line number Diff line change
Expand Up @@ -161,6 +161,7 @@ define({
"LIVE_DEV_OPEN_ERROR_TITLE": "Error Opening Live Preview in {0}",
"LIVE_DEV_OPEN_ERROR_MESSAGE": "Make sure that {0} browser is installed and try again.",
"LIVE_DEV_CLICK_TO_PIN_UNPIN": "Pin or Unpin Preview Page",
"MD_VIEWER_THEME_DESCRIPTION": "Theme for the Markdown viewer (light or dark)",
"LIVE_DEV_STATUS_TIP_SYNC_ERROR": "Live Preview (not updating due to syntax error)",
"LIVE_DEV_SETTINGS": "Live Preview Settings\u2026",
"LIVE_DEV_SETTINGS_BANNER": "Set up a custom server to live preview `<b>{0}</b>` and other server-rendered files (PHP, JSP, etc.)",
Expand Down
4 changes: 4 additions & 0 deletions test/spec/md-editor-edit-integ-test.js
Original file line number Diff line number Diff line change
Expand Up @@ -210,6 +210,8 @@ define(function (require, exports, module) {

// In reader mode: checkboxes should be disabled
await _enterReaderMode();
await awaitsFor(() => _getCheckboxes().length > 0,
"checkboxes to appear in reader mode");
let checkboxes = _getCheckboxes();
expect(checkboxes.length).toBeGreaterThan(0);
for (const cb of checkboxes) {
Expand All @@ -218,6 +220,8 @@ define(function (require, exports, module) {

// In edit mode: checkboxes should be enabled
await _enterEditMode();
await awaitsFor(() => _getCheckboxes().length > 0,
"checkboxes to appear in edit mode");
checkboxes = _getCheckboxes();
expect(checkboxes.length).toBeGreaterThan(0);
for (const cb of checkboxes) {
Expand Down
7 changes: 4 additions & 3 deletions test/spec/md-editor-integ-test.js
Original file line number Diff line number Diff line change
Expand Up @@ -774,12 +774,13 @@ define(function (require, exports, module) {
// Verify edit mode preserved
await _assertMdEditMode(true);

// Verify scroll position preserved (wider tolerance for CI)
// Verify scroll position preserved (wider tolerance for CI).
// Wait longer as theme/content sync after reopen can cause transient reflows.
await awaitsFor(() => {
const scroll = _getViewerScrollTop();
return scroll > 10 && Math.abs(scroll - scrollBefore) < 150;
}, "scroll position to be preserved after panel reopen", 5000);
}, 15000);
}, "scroll position to be preserved after panel reopen", 8000);
}, 20000);

it("should reload button re-render current file with fresh DOM preserving scroll and edit mode", async function () {
await _openMdFileAndWaitForPreview("long.md");
Expand Down
Loading