fix(mdedit): two-line ZDDC tree display + dark-mode editor contrast

Two issues from one session:

* File tree: ZDDC-conforming filenames render as a single line
  even though the JS already produced two-div markup (filename-main +
  filename-secondary). Cause: .tree-row__label was display:flex
  (row-direction), so the two divs laid out side-by-side. Fix: wrap
  each label's text in a new .tree-row__name span styled
  flex-direction:column. Both file and folder code paths use the
  same wrapper now; non-ZDDC entries collapse to a single
  .filename-main line so typography stays consistent across the tree.
  Tested by injecting a ZDDC filename into a mock directory and
  asserting filename-secondary's bounding-box top is below
  filename-main's bottom.

* Toast UI Editor was unreadable in dark mode. Toast UI ships with
  light-only chrome; its .toastui-editor-md-container has color #222
  on a transparent bg, so when mdedit's dark theme rendered the
  surrounding pane in #1e1e1e the editor text fell on near-black
  background → effectively invisible. Fix: add CSS overrides in
  mdedit/css/editor.css that target the editor's load-bearing
  surfaces (md-container, md-preview, ww-container, ProseMirror,
  toolbar, mode-switch tabs, popups) and apply var(--bg) /
  var(--text). Toolbar icons get a filter:invert(0.85) hue-rotate
  to flip the sprite-baked dark glyphs. Both manual override
  (data-theme="dark") and OS-pref auto fallback (prefers-color-scheme)
  are covered. Tested by computing contrast ratios on every editor
  surface in dark mode — all came in at 10:1+ (well above WCAG AA's
  4.5:1).

Embedded snapshots refreshed to current main HEAD's dev build label.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
ZDDC 2026-05-01 21:09:46 -05:00
parent 6cc0d2ae27
commit 8c2e65e4a2
10 changed files with 6463 additions and 28 deletions

View file

@ -337,13 +337,25 @@
flex: 1;
min-width: 0;
overflow: hidden;
white-space: nowrap;
text-overflow: ellipsis;
display: flex;
align-items: center;
gap: 0.25rem;
}
/* The text wrapper inside a tree-row label. For ZDDC-conforming files and
folders, this wraps two stacked <div>s (filename-main + filename-secondary)
so the row reads top-to-bottom as title + metadata same shape the archive
tool uses for its transmittal-folder list. For non-ZDDC entries it just
contains a single line. flex column makes the two-line case work; min-width:0
lets each line truncate independently. */
.tree-row__name {
display: flex;
flex-direction: column;
min-width: 0;
flex: 1;
line-height: 1.25;
}
/* ── New-file modal ─────────────────────────────────────────────────────────── */
.modal-overlay {
position: fixed;

View file

@ -23,3 +23,97 @@
.toastui-editor-main .toastui-editor-md-preview {
height: 100% !important;
}
/* Toast UI Editor dark-theme overrides
Toast UI ships with light-mode chrome and edit surfaces by default. In
mdedit's dark mode the editor's text (#222) falls onto the transparent
md-container, which inherits var(--bg) dark = #1e1e1e effectively
black-on-black. Override the load-bearing surfaces with mdedit's tokens
so the editor harmonises with the rest of the chrome.
The selectors target both manual override (data-theme="dark") and the
OS-pref auto fallback (prefers-color-scheme + no data-theme="light"). */
/* Manual dark override */
[data-theme="dark"] .toastui-editor-defaultUI,
[data-theme="dark"] .toastui-editor-md-container,
[data-theme="dark"] .toastui-editor-md-preview,
[data-theme="dark"] .toastui-editor-ww-container,
[data-theme="dark"] .toastui-editor-mode-switch,
[data-theme="dark"] .toastui-editor-main,
[data-theme="dark"] .ProseMirror {
background-color: var(--bg);
color: var(--text);
}
[data-theme="dark"] .toastui-editor-defaultUI-toolbar {
background-color: var(--bg-secondary);
border-bottom-color: var(--border);
}
[data-theme="dark"] .toastui-editor-md-splitter {
background-color: var(--border);
}
[data-theme="dark"] .toastui-editor-toolbar-icons {
/* Toast UI's icons are sprite-baked dark; invert flips them to light. */
filter: invert(0.85) hue-rotate(180deg);
}
[data-theme="dark"] .toastui-editor-toolbar-divider {
background-color: var(--border);
}
[data-theme="dark"] .toastui-editor-mode-switch {
border-top-color: var(--border);
}
[data-theme="dark"] .toastui-editor-mode-switch .tab-item {
color: var(--text-muted);
}
[data-theme="dark"] .toastui-editor-mode-switch .tab-item.active {
color: var(--text);
background-color: var(--bg);
}
[data-theme="dark"] .toastui-editor-popup,
[data-theme="dark"] .toastui-editor-context-menu {
background-color: var(--bg-secondary);
color: var(--text);
border-color: var(--border);
}
/* OS-pref auto fallback (matches every selector above) */
@media (prefers-color-scheme: dark) {
:root:not([data-theme="light"]) .toastui-editor-defaultUI,
:root:not([data-theme="light"]) .toastui-editor-md-container,
:root:not([data-theme="light"]) .toastui-editor-md-preview,
:root:not([data-theme="light"]) .toastui-editor-ww-container,
:root:not([data-theme="light"]) .toastui-editor-mode-switch,
:root:not([data-theme="light"]) .toastui-editor-main,
:root:not([data-theme="light"]) .ProseMirror {
background-color: var(--bg);
color: var(--text);
}
:root:not([data-theme="light"]) .toastui-editor-defaultUI-toolbar {
background-color: var(--bg-secondary);
border-bottom-color: var(--border);
}
:root:not([data-theme="light"]) .toastui-editor-md-splitter {
background-color: var(--border);
}
:root:not([data-theme="light"]) .toastui-editor-toolbar-icons {
filter: invert(0.85) hue-rotate(180deg);
}
:root:not([data-theme="light"]) .toastui-editor-toolbar-divider {
background-color: var(--border);
}
:root:not([data-theme="light"]) .toastui-editor-mode-switch {
border-top-color: var(--border);
}
:root:not([data-theme="light"]) .toastui-editor-mode-switch .tab-item {
color: var(--text-muted);
}
:root:not([data-theme="light"]) .toastui-editor-mode-switch .tab-item.active {
color: var(--text);
background-color: var(--bg);
}
:root:not([data-theme="light"]) .toastui-editor-popup,
:root:not([data-theme="light"]) .toastui-editor-context-menu {
background-color: var(--bg-secondary);
color: var(--text);
border-color: var(--border);
}
}

6205
mdedit/dist/mdedit.html vendored Normal file

File diff suppressed because one or more lines are too long

View file

@ -101,7 +101,7 @@ function renderFileTree() {
const scratchLabel = document.createElement('span');
scratchLabel.className = 'tree-row__label';
scratchLabel.innerHTML = '<div class="filename-main">📝 Scratchpad</div><div class="filename-secondary">Quick notes — no directory needed</div>';
scratchLabel.innerHTML = '<span class="tree-row__name"><div class="filename-main">📝 Scratchpad</div><div class="filename-secondary">Quick notes — no directory needed</div></span>';
scratchpadElement.appendChild(scratchLabel);
const scratchActions = document.createElement('div');
@ -169,7 +169,10 @@ function renderFileTree() {
const meta = `${parsedFolder.trackingNumber} (${parsedFolder.status}) — ${parsedFolder.date}`;
dirName.innerHTML = `<div class="filename-main">📁 ${escapeHtml(parsedFolder.title)}</div><div class="filename-secondary">${escapeHtml(meta)}</div>`;
} else {
dirName.textContent = `📁 ${name}`;
// Non-ZDDC folder: still wrap in filename-main so
// typography matches the two-line entries (same font
// size + weight; just no secondary line).
dirName.innerHTML = `<div class="filename-main">📁 ${escapeHtml(name)}</div>`;
}
const dirLabel = document.createElement('span');
@ -209,24 +212,30 @@ function renderFileTree() {
const fileIcon = getFileTypeIcon(name);
let fileNameDisplay;
// Build the inner two-line text inside a tree-row__name
// wrapper (column-flex). ZDDC-conforming filenames split
// into title + meta; "Title - filename.ext" pattern uses
// the dash as the same split. Plain names get a single
// line via filename-main only — same wrapper, just no
// secondary div, so the layout stays consistent.
let fileNameInner;
const parsed = zddc.parseFilename(name);
if (parsed && parsed.valid) {
const titleDisplay = escapeHtml(parsed.title);
const metaDisplay = escapeHtml(`${parsed.trackingNumber}_${parsed.revision} (${parsed.status})`);
fileNameDisplay = `<div class="filename-main">${fileIcon} ${titleDisplay}</div><div class="filename-secondary">${metaDisplay}</div>`;
fileNameInner = `<div class="filename-main">${fileIcon} ${titleDisplay}</div><div class="filename-secondary">${metaDisplay}</div>`;
} else if (name.includes(' - ')) {
const dashIdx = name.lastIndexOf(' - ');
const secondary = escapeHtml(name.substring(0, dashIdx));
const primary = escapeHtml(name.substring(dashIdx + 3).replace(/\.[^.]+$/, ''));
fileNameDisplay = `<div class="filename-main">${fileIcon} ${primary}</div><div class="filename-secondary">${secondary}</div>`;
fileNameInner = `<div class="filename-main">${fileIcon} ${primary}</div><div class="filename-secondary">${secondary}</div>`;
} else {
fileNameDisplay = `<span>${fileIcon} ${escapeHtml(name)}</span>`;
fileNameInner = `<div class="filename-main">${fileIcon} ${escapeHtml(name)}</div>`;
}
const fileLabel = document.createElement('span');
fileLabel.className = 'tree-row__label';
fileLabel.innerHTML = fileNameDisplay;
fileLabel.innerHTML = `<span class="tree-row__name">${fileNameInner}</span>`;
const fileActions = createActionButtons(filePath, 'file');

View file

@ -2113,7 +2113,7 @@ td[data-field="trackingNumber"] {
</svg>
<div class="header-title-group">
<span class="app-header__title">ZDDC Archive</span>
<span class="build-timestamp">v0.0.8</span>
<span class="build-timestamp"><span style="color:red;font-weight:bold">v0.0.9-alpha · 2026-05-02 02:07:03 · 17b0a4d-dirty</span></span>
</div>
<button id="addDirectoryBtn" class="btn btn-primary">Add Local Directory</button>
<button id="refreshHeaderBtn" class="btn btn-secondary hidden" title="Refresh Data" style="font-size:1.1rem;"></button>

View file

@ -1376,7 +1376,7 @@ body.help-open .app-header {
</svg>
<div class="header-title-group">
<span class="app-header__title">ZDDC Classifier</span>
<span class="build-timestamp">v0.0.8</span>
<span class="build-timestamp"><span style="color:red;font-weight:bold">v0.0.9-alpha · 2026-05-02 02:07:03 · 17b0a4d-dirty</span></span>
</div>
<button id="selectDirectoryBtn" class="btn btn-primary">Select Directory</button>
<button id="refreshBtn" class="btn btn-secondary hidden" title="Refresh and rescan directory">Refresh</button>

View file

@ -866,7 +866,7 @@ body {
</g>
</svg>
<span class="app-header__title">ZDDC Archive</span>
<span class="build-timestamp">v0.0.8</span>
<span class="build-timestamp"><span style="color:red;font-weight:bold">v0.0.9-alpha · 2026-05-02 02:07:03 · 17b0a4d-dirty</span></span>
</div>
<div class="header-right">
<button id="theme-btn" class="btn btn-secondary" title="Theme: auto (follows OS)" aria-label="Theme: auto (follows OS)"></button>

View file

@ -1066,13 +1066,25 @@ body.help-open .app-header {
flex: 1;
min-width: 0;
overflow: hidden;
white-space: nowrap;
text-overflow: ellipsis;
display: flex;
align-items: center;
gap: 0.25rem;
}
/* The text wrapper inside a tree-row label. For ZDDC-conforming files and
folders, this wraps two stacked <div>s (filename-main + filename-secondary)
so the row reads top-to-bottom as title + metadata — same shape the archive
tool uses for its transmittal-folder list. For non-ZDDC entries it just
contains a single line. flex column makes the two-line case work; min-width:0
lets each line truncate independently. */
.tree-row__name {
display: flex;
flex-direction: column;
min-width: 0;
flex: 1;
line-height: 1.25;
}
/* ── New-file modal ─────────────────────────────────────────────────────────── */
.modal-overlay {
position: fixed;
@ -1147,6 +1159,100 @@ body.help-open .app-header {
height: 100% !important;
}
/* ── Toast UI Editor — dark-theme overrides ───────────────────────────────
Toast UI ships with light-mode chrome and edit surfaces by default. In
mdedit's dark mode the editor's text (#222) falls onto the transparent
md-container, which inherits var(--bg) dark = #1e1e1e → effectively
black-on-black. Override the load-bearing surfaces with mdedit's tokens
so the editor harmonises with the rest of the chrome.
The selectors target both manual override (data-theme="dark") and the
OS-pref auto fallback (prefers-color-scheme + no data-theme="light"). */
/* Manual dark override */
[data-theme="dark"] .toastui-editor-defaultUI,
[data-theme="dark"] .toastui-editor-md-container,
[data-theme="dark"] .toastui-editor-md-preview,
[data-theme="dark"] .toastui-editor-ww-container,
[data-theme="dark"] .toastui-editor-mode-switch,
[data-theme="dark"] .toastui-editor-main,
[data-theme="dark"] .ProseMirror {
background-color: var(--bg);
color: var(--text);
}
[data-theme="dark"] .toastui-editor-defaultUI-toolbar {
background-color: var(--bg-secondary);
border-bottom-color: var(--border);
}
[data-theme="dark"] .toastui-editor-md-splitter {
background-color: var(--border);
}
[data-theme="dark"] .toastui-editor-toolbar-icons {
/* Toast UI's icons are sprite-baked dark; invert flips them to light. */
filter: invert(0.85) hue-rotate(180deg);
}
[data-theme="dark"] .toastui-editor-toolbar-divider {
background-color: var(--border);
}
[data-theme="dark"] .toastui-editor-mode-switch {
border-top-color: var(--border);
}
[data-theme="dark"] .toastui-editor-mode-switch .tab-item {
color: var(--text-muted);
}
[data-theme="dark"] .toastui-editor-mode-switch .tab-item.active {
color: var(--text);
background-color: var(--bg);
}
[data-theme="dark"] .toastui-editor-popup,
[data-theme="dark"] .toastui-editor-context-menu {
background-color: var(--bg-secondary);
color: var(--text);
border-color: var(--border);
}
/* OS-pref auto fallback (matches every selector above) */
@media (prefers-color-scheme: dark) {
:root:not([data-theme="light"]) .toastui-editor-defaultUI,
:root:not([data-theme="light"]) .toastui-editor-md-container,
:root:not([data-theme="light"]) .toastui-editor-md-preview,
:root:not([data-theme="light"]) .toastui-editor-ww-container,
:root:not([data-theme="light"]) .toastui-editor-mode-switch,
:root:not([data-theme="light"]) .toastui-editor-main,
:root:not([data-theme="light"]) .ProseMirror {
background-color: var(--bg);
color: var(--text);
}
:root:not([data-theme="light"]) .toastui-editor-defaultUI-toolbar {
background-color: var(--bg-secondary);
border-bottom-color: var(--border);
}
:root:not([data-theme="light"]) .toastui-editor-md-splitter {
background-color: var(--border);
}
:root:not([data-theme="light"]) .toastui-editor-toolbar-icons {
filter: invert(0.85) hue-rotate(180deg);
}
:root:not([data-theme="light"]) .toastui-editor-toolbar-divider {
background-color: var(--border);
}
:root:not([data-theme="light"]) .toastui-editor-mode-switch {
border-top-color: var(--border);
}
:root:not([data-theme="light"]) .toastui-editor-mode-switch .tab-item {
color: var(--text-muted);
}
:root:not([data-theme="light"]) .toastui-editor-mode-switch .tab-item.active {
color: var(--text);
background-color: var(--bg);
}
:root:not([data-theme="light"]) .toastui-editor-popup,
:root:not([data-theme="light"]) .toastui-editor-context-menu {
background-color: var(--bg-secondary);
color: var(--text);
border-color: var(--border);
}
}
/* Table of Contents styles */
.toc-pane {
height: 100%;
@ -1668,7 +1774,7 @@ body.help-open .app-header {
</svg>
<div class="header-title-group">
<span class="app-header__title">ZDDC Markdown</span>
<span class="build-timestamp">v0.0.8</span>
<span class="build-timestamp"><span style="color:red;font-weight:bold">v0.0.9-alpha · 2026-05-02 02:07:03 · 17b0a4d-dirty</span></span>
</div>
<button id="select-directory" class="btn btn-primary" title="Select a Directory">Select Directory</button>
</div>
@ -4332,7 +4438,7 @@ function renderFileTree() {
const scratchLabel = document.createElement('span');
scratchLabel.className = 'tree-row__label';
scratchLabel.innerHTML = '<div class="filename-main">📝 Scratchpad</div><div class="filename-secondary">Quick notes — no directory needed</div>';
scratchLabel.innerHTML = '<span class="tree-row__name"><div class="filename-main">📝 Scratchpad</div><div class="filename-secondary">Quick notes — no directory needed</div></span>';
scratchpadElement.appendChild(scratchLabel);
const scratchActions = document.createElement('div');
@ -4400,7 +4506,10 @@ function renderFileTree() {
const meta = `${parsedFolder.trackingNumber} (${parsedFolder.status}) — ${parsedFolder.date}`;
dirName.innerHTML = `<div class="filename-main">📁 ${escapeHtml(parsedFolder.title)}</div><div class="filename-secondary">${escapeHtml(meta)}</div>`;
} else {
dirName.textContent = `📁 ${name}`;
// Non-ZDDC folder: still wrap in filename-main so
// typography matches the two-line entries (same font
// size + weight; just no secondary line).
dirName.innerHTML = `<div class="filename-main">📁 ${escapeHtml(name)}</div>`;
}
const dirLabel = document.createElement('span');
@ -4440,24 +4549,30 @@ function renderFileTree() {
const fileIcon = getFileTypeIcon(name);
let fileNameDisplay;
// Build the inner two-line text inside a tree-row__name
// wrapper (column-flex). ZDDC-conforming filenames split
// into title + meta; "Title - filename.ext" pattern uses
// the dash as the same split. Plain names get a single
// line via filename-main only — same wrapper, just no
// secondary div, so the layout stays consistent.
let fileNameInner;
const parsed = zddc.parseFilename(name);
if (parsed && parsed.valid) {
const titleDisplay = escapeHtml(parsed.title);
const metaDisplay = escapeHtml(`${parsed.trackingNumber}_${parsed.revision} (${parsed.status})`);
fileNameDisplay = `<div class="filename-main">${fileIcon} ${titleDisplay}</div><div class="filename-secondary">${metaDisplay}</div>`;
fileNameInner = `<div class="filename-main">${fileIcon} ${titleDisplay}</div><div class="filename-secondary">${metaDisplay}</div>`;
} else if (name.includes(' - ')) {
const dashIdx = name.lastIndexOf(' - ');
const secondary = escapeHtml(name.substring(0, dashIdx));
const primary = escapeHtml(name.substring(dashIdx + 3).replace(/\.[^.]+$/, ''));
fileNameDisplay = `<div class="filename-main">${fileIcon} ${primary}</div><div class="filename-secondary">${secondary}</div>`;
fileNameInner = `<div class="filename-main">${fileIcon} ${primary}</div><div class="filename-secondary">${secondary}</div>`;
} else {
fileNameDisplay = `<span>${fileIcon} ${escapeHtml(name)}</span>`;
fileNameInner = `<div class="filename-main">${fileIcon} ${escapeHtml(name)}</div>`;
}
const fileLabel = document.createElement('span');
fileLabel.className = 'tree-row__label';
fileLabel.innerHTML = fileNameDisplay;
fileLabel.innerHTML = `<span class="tree-row__name">${fileNameInner}</span>`;
const fileActions = createActionButtons(filePath, 'file');

View file

@ -2210,7 +2210,7 @@ dialog.modal--narrow {
</svg>
<div class="header-title-group">
<span class="app-header__title">ZDDC Transmittal</span>
<span class="build-timestamp">v0.0.8</span>
<span class="build-timestamp"><span style="color:red;font-weight:bold">v0.0.9-alpha · 2026-05-02 02:07:03 · 17b0a4d-dirty</span></span>
</div>
<div class="app-header__spacer"></div>
<div class="app-header__icons">

View file

@ -1,6 +1,6 @@
# Generated by build.sh — do not edit. One <app>=<build label> per line.
archive=v0.0.8
transmittal=v0.0.8
classifier=v0.0.8
mdedit=v0.0.8
landing=v0.0.8
archive=v0.0.9-alpha · 2026-05-02 02:07:03 · 17b0a4d-dirty
transmittal=v0.0.9-alpha · 2026-05-02 02:07:03 · 17b0a4d-dirty
classifier=v0.0.9-alpha · 2026-05-02 02:07:03 · 17b0a4d-dirty
mdedit=v0.0.9-alpha · 2026-05-02 02:07:03 · 17b0a4d-dirty
landing=v0.0.9-alpha · 2026-05-02 02:07:03 · 17b0a4d-dirty