Skip to content

Commit c1adff5

Browse files
Fix/honest preview title page (#23)
* Fix title page preview to show only explicit frontmatter content
1 parent 0dbc95c commit c1adff5

File tree

9 files changed

+207
-31
lines changed

9 files changed

+207
-31
lines changed

RELEASES.md

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,10 @@
55
### New features
66
- **Document sharing** — Share documents via encrypted URL links. Content is compressed (pako deflate) and encrypted (AES-256-GCM) client-side, then embedded in the URL fragment which never leaves the browser. Two modes: link-only (encryption key in URL) and password-protected (PBKDF2 key derivation). Configurable link expiry (1h to 30 days). Import shared documents with Replace All or Merge options. Optional URL shortening via is.gd.
77

8+
### Changed
9+
- **Honest title page preview** — Live preview now shows title page only from explicit YAML frontmatter instead of auto-generating phantom titles and dates from H1 headings and `new Date()`. When no frontmatter is present, a clickable placeholder guides users to add metadata. DOCX/PDF/HTML export retains auto-fallback behavior for professional output.
10+
- **Document Options** — "Export Options" renamed to "Document Options". Toggles (Title Page, ToC, Header, Footer) now affect both live preview and export, making the editor truly WYSIWYG.
11+
812
---
913

1014
## 1.3.1

index.html

Lines changed: 53 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -402,7 +402,9 @@
402402
padding: 2px;
403403
color: #9ca3af;
404404
border-radius: 3px;
405-
transition: color 0.1s, background 0.1s;
405+
transition:
406+
color 0.1s,
407+
background 0.1s;
406408
}
407409
.explorer-item .item-actions button:hover {
408410
color: #ef4444;
@@ -698,7 +700,7 @@
698700
<div id="settingsPanel" class="settings-panel bg-gray-900 border-t border-gray-800">
699701
<div class="px-4 py-3 flex flex-wrap items-center gap-6">
700702
<span class="text-xs text-gray-500 uppercase tracking-wider font-medium"
701-
>Export Options</span
703+
>Document Options</span
702704
>
703705

704706
<label class="flex items-center gap-2 cursor-pointer group">
@@ -746,18 +748,50 @@
746748
</header>
747749

748750
<main class="flex-1 flex flex-col lg:flex-row min-h-0 relative">
749-
<div id="explorerPanel" class="hidden lg:flex flex-col min-h-0 bg-white border-r border-gray-200 overflow-hidden absolute left-0 top-0 bottom-0 z-30 shadow-lg" style="width: 0; min-width: 0; pointer-events: none; transition: width 0.2s ease, min-width 0.2s ease, box-shadow 0.2s ease;">
750-
<div class="px-3 py-2 bg-white border-b border-gray-200 flex items-center justify-between shrink-0">
751+
<div
752+
id="explorerPanel"
753+
class="hidden lg:flex flex-col min-h-0 bg-white border-r border-gray-200 overflow-hidden absolute left-0 top-0 bottom-0 z-30 shadow-lg"
754+
style="
755+
width: 0;
756+
min-width: 0;
757+
pointer-events: none;
758+
transition:
759+
width 0.2s ease,
760+
min-width 0.2s ease,
761+
box-shadow 0.2s ease;
762+
"
763+
>
764+
<div
765+
class="px-3 py-2 bg-white border-b border-gray-200 flex items-center justify-between shrink-0"
766+
>
751767
<span class="text-xs font-medium text-gray-500 uppercase tracking-wider">Files</span>
752768
<div class="flex items-center gap-1">
753-
<button id="addDocBtn" class="text-gray-400 hover:text-kyotu-orange transition-colors p-0.5" title="New document">
769+
<button
770+
id="addDocBtn"
771+
class="text-gray-400 hover:text-kyotu-orange transition-colors p-0.5"
772+
title="New document"
773+
>
754774
<svg class="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">
755-
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M12 4v16m8-8H4"/>
775+
<path
776+
stroke-linecap="round"
777+
stroke-linejoin="round"
778+
stroke-width="2"
779+
d="M12 4v16m8-8H4"
780+
/>
756781
</svg>
757782
</button>
758-
<button id="explorerClose" class="text-gray-400 hover:text-gray-600 transition-colors p-0.5" title="Close (Ctrl+B)">
783+
<button
784+
id="explorerClose"
785+
class="text-gray-400 hover:text-gray-600 transition-colors p-0.5"
786+
title="Close (Ctrl+B)"
787+
>
759788
<svg class="w-3.5 h-3.5" fill="none" stroke="currentColor" viewBox="0 0 24 24">
760-
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M6 18L18 6M6 6l12 12"/>
789+
<path
790+
stroke-linecap="round"
791+
stroke-linejoin="round"
792+
stroke-width="2"
793+
d="M6 18L18 6M6 6l12 12"
794+
/>
761795
</svg>
762796
</button>
763797
</div>
@@ -770,9 +804,18 @@
770804
class="px-4 py-2.5 bg-white border-b border-gray-200 flex items-center justify-between shrink-0"
771805
>
772806
<div class="flex items-center gap-2">
773-
<button id="explorerToggle" class="opacity-50 hover:opacity-80 text-gray-500 transition-all hidden lg:block" title="Toggle file explorer (Ctrl+B)">
807+
<button
808+
id="explorerToggle"
809+
class="opacity-50 hover:opacity-80 text-gray-500 transition-all hidden lg:block"
810+
title="Toggle file explorer (Ctrl+B)"
811+
>
774812
<svg class="w-3.5 h-3.5" fill="none" stroke="currentColor" viewBox="0 0 24 24">
775-
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M3 7v10a2 2 0 002 2h14a2 2 0 002-2V9a2 2 0 00-2-2h-6l-2-2H5a2 2 0 00-2 2z"/>
813+
<path
814+
stroke-linecap="round"
815+
stroke-linejoin="round"
816+
stroke-width="2"
817+
d="M3 7v10a2 2 0 002 2h14a2 2 0 002-2V9a2 2 0 00-2-2h-6l-2-2H5a2 2 0 00-2 2z"
818+
/>
776819
</svg>
777820
</button>
778821
<div

src/html-preview.js

Lines changed: 30 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -84,16 +84,42 @@ function generateTitlePage(metadata, theme, options = {}) {
8484
return html;
8585
}
8686

87+
function generateTitlePagePlaceholder() {
88+
return `<div class="title-page-placeholder" data-action="insert-frontmatter">
89+
<div class="title-page-placeholder-icon">
90+
<svg width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round">
91+
<path d="M12 5v14m-7-7h14"/>
92+
</svg>
93+
</div>
94+
<div class="title-page-placeholder-text">Click to add title page</div>
95+
<div class="title-page-placeholder-hint">title &middot; subtitle &middot; author &middot; date</div>
96+
</div>`;
97+
}
98+
8799
export function generateHTMLPreview(elements, metadata, themeIdOrObject = "kyotu", options = {}) {
88100
const theme = typeof themeIdOrObject === "string" ? getTheme(themeIdOrObject) : themeIdOrObject;
89101

90102
let html = "";
91-
if (metadata.title || metadata.subtitle || metadata.author || metadata.date) {
92-
html += generateTitlePage(metadata, theme, options);
93-
if (options.pagedMode) {
94-
html += '<div class="title-page-break"></div>';
103+
const showTitlePage = options.showTitlePage !== false;
104+
105+
if (showTitlePage) {
106+
const isUserPreview = !!options.interactive || !!options.pagedMode;
107+
const explicit = metadata._explicitFields;
108+
const hasExplicitContent =
109+
explicit && (explicit.title || explicit.subtitle || explicit.author || explicit.date);
110+
111+
if (hasExplicitContent) {
112+
html += generateTitlePage(isUserPreview ? explicit : metadata, theme, options);
113+
if (options.pagedMode) {
114+
html += '<div class="title-page-break"></div>';
115+
}
116+
} else if (!isUserPreview && (metadata.title || metadata.date)) {
117+
html += generateTitlePage(metadata, theme, options);
118+
} else if (options.interactive) {
119+
html += generateTitlePagePlaceholder();
95120
}
96121
}
122+
97123
const interactive = !!options.interactive;
98124
for (const el of elements) html += elementToHTML(el, theme, interactive);
99125
return html;

src/main.js

Lines changed: 36 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -186,6 +186,7 @@ Object.keys(toggles).forEach((id) => {
186186
})
187187
);
188188
} catch {}
189+
updatePreview();
189190
});
190191
});
191192

@@ -627,10 +628,18 @@ async function updatePreview() {
627628
}
628629

629630
if (previewMode === "pages") {
630-
const previewOptions = { logoDataUrl, pagedMode: true };
631+
const previewOptions = {
632+
logoDataUrl,
633+
pagedMode: true,
634+
showTitlePage: exportOptions.showTitlePage,
635+
};
631636
await updatePreviewPaged(elements, metadata, theme, previewOptions);
632637
} else {
633-
const previewOptions = { logoDataUrl, interactive: true };
638+
const previewOptions = {
639+
logoDataUrl,
640+
interactive: true,
641+
showTitlePage: exportOptions.showTitlePage,
642+
};
634643
preview.classList.remove("page-mode");
635644
await renderPreview(preview, elements, metadata, theme, previewOptions);
636645
}
@@ -1088,6 +1097,31 @@ initScrollSync(markdownInput, preview);
10881097
initFormattingToolbar(markdownInput);
10891098
initIncludeAutocomplete(markdownInput, () => allDocuments);
10901099
initDiagramActions(preview);
1100+
1101+
preview.addEventListener("click", (e) => {
1102+
const placeholder = e.target.closest("[data-action='insert-frontmatter']");
1103+
if (!placeholder) return;
1104+
insertFrontmatterTemplate();
1105+
});
1106+
1107+
function insertFrontmatterTemplate() {
1108+
const current = markdownInput.value;
1109+
if (current.trimStart().startsWith("---")) {
1110+
markdownInput.focus();
1111+
const firstNewline = current.indexOf("\n");
1112+
markdownInput.setSelectionRange(firstNewline + 1, firstNewline + 1);
1113+
markdownInput.scrollTop = 0;
1114+
return;
1115+
}
1116+
const template = `---\ntitle: ""\nsubtitle: ""\nauthor: ""\ndate: ""\n---\n\n`;
1117+
markdownInput.value = template + current;
1118+
markdownInput.focus();
1119+
const cursorPos = template.indexOf('title: "') + 'title: "'.length;
1120+
markdownInput.setSelectionRange(cursorPos, cursorPos);
1121+
markdownInput.scrollTop = 0;
1122+
markdownInput.dispatchEvent(new Event("input"));
1123+
}
1124+
10911125
updateSyncToggleUI();
10921126

10931127
function updateSyncToggleUI() {

src/parser.js

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -27,10 +27,12 @@ export function parseMarkdown(content) {
2727
const lines = content.split("\n");
2828
let metadata = {};
2929
let bodyLines = lines;
30+
let hasExplicitFrontmatter = false;
3031

3132
if (lines[0] === "---") {
3233
const endIndex = lines.findIndex((l, i) => i > 0 && l === "---");
3334
if (endIndex > 0) {
35+
hasExplicitFrontmatter = true;
3436
const yamlLines = lines.slice(1, endIndex);
3537
yamlLines.forEach((line) => {
3638
const match = line.match(/^([\w-]+):\s*"?([^"]*)"?$/);
@@ -40,6 +42,13 @@ export function parseMarkdown(content) {
4042
}
4143
}
4244

45+
const explicitFields = {
46+
title: metadata.title || null,
47+
subtitle: metadata.subtitle || null,
48+
author: metadata.author || null,
49+
date: metadata.date || null,
50+
};
51+
4352
if (!metadata.title) {
4453
const firstH1 = bodyLines.find((l) => l.match(/^# /));
4554
metadata.title = firstH1 ? firstH1.replace(/^# /, "") : "Document";
@@ -64,6 +73,9 @@ export function parseMarkdown(content) {
6473
metadata.date = `${months[now.getMonth()]} ${now.getFullYear()}`;
6574
}
6675

76+
metadata._hasExplicitFrontmatter = hasExplicitFrontmatter;
77+
metadata._explicitFields = explicitFields;
78+
6779
return { metadata, body: bodyLines.join("\n") };
6880
}
6981

src/preview-styles.js

Lines changed: 36 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -230,6 +230,42 @@ export function generatePreviewStyles(theme, selectors = "#preview, #tePreview")
230230
page-break-after: always;
231231
display: block;
232232
}
233+
:is(${selectors}) .title-page-placeholder {
234+
border: 2px dashed #${c.muted}80;
235+
border-radius: 8px;
236+
padding: 2rem;
237+
margin-bottom: 2rem;
238+
text-align: center;
239+
cursor: pointer;
240+
transition: border-color 0.2s, background 0.2s;
241+
background: transparent;
242+
}
243+
:is(${selectors}) .title-page-placeholder:hover {
244+
border-color: #${c.primary};
245+
background: #${c.primary}08;
246+
}
247+
:is(${selectors}) .title-page-placeholder-icon {
248+
color: #${c.muted};
249+
margin-bottom: 0.5rem;
250+
display: flex;
251+
justify-content: center;
252+
transition: color 0.2s;
253+
}
254+
:is(${selectors}) .title-page-placeholder:hover .title-page-placeholder-icon {
255+
color: #${c.primary};
256+
}
257+
:is(${selectors}) .title-page-placeholder-text {
258+
font-family: ${bodyFont};
259+
font-size: 14px;
260+
font-weight: 500;
261+
color: #${c.secondary};
262+
}
263+
:is(${selectors}) .title-page-placeholder-hint {
264+
font-family: ${bodyFont};
265+
font-size: 12px;
266+
color: #${c.muted};
267+
margin-top: 0.25rem;
268+
}
233269
`;
234270
}
235271

tests/features/preview/headings.feature

Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -34,3 +34,28 @@ Feature: Heading rendering in preview
3434
Then the preview should contain text "My Report"
3535
And the preview should contain text "Executive Summary"
3636
And the preview should contain text "Test Author"
37+
38+
Scenario: Preview without frontmatter shows placeholder instead of phantom title page
39+
Given the editor contains:
40+
"""
41+
# My Document
42+
Some content here.
43+
"""
44+
When the preview renders
45+
Then the preview should contain a ".title-page-placeholder" element
46+
And the preview should not contain a ".title-page" element
47+
48+
Scenario: Title page toggle OFF hides title page and placeholder from preview
49+
Given the editor contains:
50+
"""
51+
---
52+
title: "My Report"
53+
author: "Author"
54+
---
55+
# Introduction
56+
Content.
57+
"""
58+
When the title page toggle is turned off
59+
And the preview renders
60+
Then the preview should not contain a ".title-page" element
61+
And the preview should not contain a ".title-page-placeholder" element

tests/steps/common.steps.js

Lines changed: 1 addition & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -2,25 +2,11 @@ import { createBdd } from "playwright-bdd";
22
import { test as base } from "playwright-bdd";
33
import { chromium } from "playwright-core";
44

5-
/**
6-
* Custom test fixture that launches a fresh Chromium instance per test.
7-
* Required for gVisor/container environments where --single-process mode
8-
* crashes after browser context cleanup.
9-
*
10-
* In CI environments with proper Chromium support, the standard
11-
* Playwright browser management works fine.
12-
*/
135
export const test = base.extend({
146
page: async ({}, use) => {
157
const browser = await chromium.launch({
168
headless: true,
17-
args: [
18-
"--no-sandbox",
19-
"--disable-setuid-sandbox",
20-
"--disable-gpu",
21-
"--single-process",
22-
"--no-zygote",
23-
],
9+
args: ["--no-sandbox", "--disable-setuid-sandbox", "--disable-gpu"],
2410
});
2511
const context = await browser.newContext({
2612
viewport: { width: 1280, height: 720 },

tests/steps/preview.steps.js

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -102,6 +102,16 @@ Then("the preview paragraph should not contain an {string} element", async ({ pa
102102
expect(count).toBe(0);
103103
});
104104

105+
Then("the preview should not contain a {string} element", async ({ page }, selector) => {
106+
const count = await page.locator(`#preview ${selector}`).count();
107+
expect(count).toBe(0);
108+
});
109+
110+
When("the title page toggle is turned off", async ({ page }) => {
111+
await page.click("#settingsBtn");
112+
await page.click("#toggleTitlePage");
113+
});
114+
105115
// --- Table ---
106116

107117
Then("the first row should contain {string} cells", async ({ page }, cellTag) => {

0 commit comments

Comments
 (0)