ZDDC/browse/css/tree.css
ZDDC b34edcecac feat(browse): markdown editor — editable YAML front matter + DOCX/HTML/PDF download buttons
Two improvements to browse's preview-markdown plugin so it can replace
the standalone mdedit tool:

1. **YAML front-matter editing.** The FM pane above the outline used to
   render a read-only <dl> of parsed keys — sparse and unusable when
   the file had no envelope yet. It's now a dedicated <textarea> that's
   always present. On load, parseFrontMatter() splits the `---\n…\n---`
   envelope off the body: the body feeds Toast UI Editor, the envelope
   feeds the textarea. On save, assembleContent() recombines them.
   Dirty tracking covers both halves via a SHA-256 of the assembled
   bytes. The shell mirrors mdedit's old layout (FM textarea top,
   outline below) but the FM pane is now always functional, eliminating
   the "empty pane over the TOC" problem.

2. **Download as DOCX / HTML / PDF.** When the file handle is HTTP-
   backed (server mode) and the file is a .md, three buttons appear in
   the info header next to Save. Clicking one fetches the server's
   ?convert=<fmt> endpoint and triggers a browser download with a
   clean filename (foo.md → foo.docx). Auto-saves the buffer first if
   dirty so the converted bytes reflect what's on screen.

Helper at window.zddc.source.downloadConverted (shared/zddc-source.js)
so other tools — archive, transmittal — can reuse the same flow later.
Friendly error messages map HTTP 503 / 422 / 504 to actionable toasts.
2026-05-13 10:32:38 -05:00

704 lines
17 KiB
CSS

/* ── Layout ──────────────────────────────────────────────────────────────── */
html, body {
margin: 0;
padding: 0;
height: 100%;
overflow: hidden;
font-family: var(--font);
color: var(--text);
background-color: var(--bg);
}
#appMain {
position: relative;
height: calc(100vh - 2.65rem); /* clear .app-header */
display: flex;
flex-direction: column;
overflow: hidden;
}
.browse-root {
display: flex;
flex-direction: column;
flex: 1;
height: 100%;
overflow: hidden;
background: var(--bg);
}
/* ── Toolbar ─────────────────────────────────────────────────────────────── */
.browse-toolbar {
display: flex;
align-items: center;
gap: 0.75rem;
padding: 0.4rem 1rem;
background: var(--bg-secondary);
border-bottom: 1px solid var(--border);
flex-shrink: 0;
}
.view-mode-toggle {
display: inline-flex;
gap: 0;
border: 1px solid var(--border);
border-radius: var(--radius);
overflow: hidden;
}
.view-mode-toggle .btn {
border-radius: 0;
border: none;
border-right: 1px solid var(--border);
}
.view-mode-toggle .btn:last-child {
border-right: none;
}
.view-mode-toggle .btn[aria-selected="true"] {
background: var(--primary);
color: var(--text-light);
}
/* Breadcrumbs */
.breadcrumbs {
flex: 1;
display: flex;
align-items: center;
flex-wrap: wrap;
gap: 0.15rem 0.4rem;
font-size: 0.85rem;
color: var(--text-muted);
min-width: 0;
}
.breadcrumbs a,
.breadcrumbs button {
color: var(--text-muted);
background: none;
border: 0;
padding: 0.1rem 0.3rem;
border-radius: var(--radius);
cursor: pointer;
text-decoration: none;
font: inherit;
}
.breadcrumbs a:hover,
.breadcrumbs button:hover {
color: var(--text);
background: var(--bg-hover);
}
.breadcrumbs .bc-sep {
color: var(--text-muted);
user-select: none;
}
.breadcrumbs .bc-current {
color: var(--text);
font-weight: 600;
padding: 0.1rem 0.3rem;
}
.bc-home-icon {
width: 1em;
height: 1em;
vertical-align: -0.15em;
}
.toolbar__count {
font-size: 0.8rem;
color: var(--text-muted);
white-space: nowrap;
}
/* ── Two-pane browse view ────────────────────────────────────────────────── */
.browse-view {
display: flex;
flex: 1;
overflow: hidden;
min-height: 0;
}
.pane {
overflow: hidden;
background: var(--bg);
display: flex;
flex-direction: column;
}
.tree-pane {
width: 360px;
min-width: 200px;
max-width: 60%;
border-right: 1px solid var(--border);
flex-shrink: 0;
}
.tree-pane__body {
flex: 1;
overflow: auto;
padding: 0.25rem 0;
font-size: 0.875rem;
}
/* Pane resizer — 4px grab handle between tree and preview */
.pane-resizer {
width: 4px;
background: transparent;
cursor: col-resize;
flex-shrink: 0;
position: relative;
z-index: 1;
}
.pane-resizer:hover,
.pane-resizer.is-dragging {
background: var(--primary);
}
.preview-pane {
flex: 1;
min-width: 0;
}
.preview-pane__header {
display: flex;
align-items: center;
gap: 0.5rem;
padding: 0.4rem 0.75rem;
background: var(--bg-secondary);
border-bottom: 1px solid var(--border);
flex-shrink: 0;
min-height: 2.1rem;
}
.preview-pane__title {
flex: 1;
font-size: 0.9rem;
font-weight: 500;
color: var(--text);
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
min-width: 0;
}
.preview-pane__meta {
font-size: 0.8rem;
color: var(--text-muted);
white-space: nowrap;
}
.preview-pane__body {
flex: 1;
overflow: auto;
display: flex;
flex-direction: column;
background: var(--bg);
}
/* The body's children fill the available space. Plugins inject
different content here — img, iframe, pre, custom markdown editor. */
.preview-pane__body > * {
flex: 1;
min-height: 0;
}
.preview-empty {
display: flex;
align-items: center;
justify-content: center;
color: var(--text-muted);
font-size: 0.95rem;
padding: 2rem;
text-align: center;
}
.preview-pane__body img.preview-image {
max-width: 100%;
max-height: 100%;
object-fit: contain;
margin: auto;
display: block;
flex: none; /* avoid flex sizing interfering with object-fit */
}
.preview-pane__body iframe.preview-iframe {
width: 100%;
height: 100%;
border: none;
}
.preview-pane__body pre.preview-text {
padding: 1rem;
font-family: var(--font-mono);
font-size: 0.85rem;
white-space: pre-wrap;
word-wrap: break-word;
margin: 0;
overflow: auto;
background: var(--bg);
color: var(--text);
}
/* ── Tree (vertical, file-explorer style) ───────────────────────────────── */
.tree-row {
display: flex;
align-items: center;
gap: 0.25rem;
padding: 0.15rem 0.5rem;
cursor: pointer;
user-select: none;
border-radius: 0;
color: var(--text);
}
.tree-row:hover {
background: var(--bg-hover);
}
.tree-row.is-selected {
background: var(--bg-selected);
color: var(--text);
}
.tree-row.is-selected .tree-name__label {
color: var(--text);
}
.tree-name__chevron {
display: inline-block;
width: 1rem;
text-align: center;
color: var(--text-muted);
flex-shrink: 0;
font-family: monospace;
font-size: 0.65rem;
}
.tree-row[data-isdir="true"] .tree-name__chevron::before,
.tree-row[data-iszip="true"] .tree-name__chevron::before {
content: "▸";
}
.tree-row[data-isdir="true"].expanded .tree-name__chevron::before,
.tree-row[data-iszip="true"].expanded .tree-name__chevron::before {
content: "▾";
}
.tree-name__chevron--leaf::before {
content: "";
}
.tree-name__icon {
flex-shrink: 0;
font-size: 0.95rem;
}
.tree-name__label {
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
color: var(--text);
}
.tree-row[data-isdir="true"] .tree-name__label,
.tree-row[data-iszip="true"] .tree-name__label {
font-weight: 500;
}
/* ── Drag-drop upload overlay ─────────────────────────────────────────────── */
/* Shown only while a drag is active over the page AND the current scope
accepts uploads. Pointer-events:none below dragover so the underlying
drop event still reaches the document handlers. */
.upload-overlay {
position: fixed;
inset: 0;
z-index: 50;
pointer-events: none;
background: rgba(42, 90, 138, 0.18);
backdrop-filter: blur(2px);
-webkit-backdrop-filter: blur(2px);
display: flex;
align-items: center;
justify-content: center;
opacity: 0;
transition: opacity 0.12s ease;
}
.upload-overlay.is-active {
opacity: 1;
}
.upload-overlay__panel {
background: var(--bg);
border: 2px dashed var(--primary);
border-radius: var(--radius);
padding: 1.5rem 2.25rem;
text-align: center;
box-shadow: 0 6px 24px rgba(0, 0, 0, 0.18);
pointer-events: none;
color: var(--text);
max-width: 80vw;
}
.upload-overlay__icon {
font-size: 2.5rem;
line-height: 1;
color: var(--primary);
}
.upload-overlay__title {
font-family: var(--font-display);
font-size: 1.15rem;
font-weight: 600;
margin-top: 0.5rem;
}
.upload-overlay__path {
margin-top: 0.35rem;
font-family: var(--font-mono);
font-size: 0.82rem;
color: var(--text-muted);
word-break: break-all;
}
/* Virtual rows: synthesized client-side for folders that aren't on
disk yet (canonical project folders). Rendered muted so the user
reads them as "available but empty" rather than ordinary entries.
Hover/select states still apply; the hint sits to the right of the
label. */
.tree-row--virtual .tree-name__icon,
.tree-row--virtual .tree-name__label {
opacity: 0.65;
}
.tree-name__hint {
margin-left: 0.5rem;
font-size: 0.78rem;
color: var(--text-muted);
font-style: italic;
}
/* ── Grid view (Phase C) ─────────────────────────────────────────────────── */
.grid-view {
flex: 1;
overflow: auto;
background: var(--bg);
padding: 0;
}
.grid-empty {
padding: 3rem;
text-align: center;
color: var(--text-muted);
}
/* ── Status bar ──────────────────────────────────────────────────────────── */
.status-bar {
padding: 0.4rem 1rem;
background: var(--bg-secondary);
border-top: 1px solid var(--border);
font-size: 0.8rem;
color: var(--text-muted);
min-height: 1.6rem;
flex-shrink: 0;
}
.status-bar.is-error { color: var(--danger); }
.status-bar.is-info { color: var(--text); }
/* ── Markdown plugin (right-pane internals when a .md is selected) ──────── */
/* CSS-Grid shell mirroring mdedit's layout: sidebar on the LEFT
(front matter top + TOC bottom), content on the RIGHT (informational
header above the Toast UI editor). The grid gives every cell a
definite size, which Toast UI needs to compute its scroll regions
correctly. */
.md-shell {
display: grid;
grid-template-rows: 1fr;
grid-template-columns: 280px 1fr; /* JS overrides on resize */
grid-template-areas: "sidebar content";
height: 100%;
min-height: 0;
background: var(--bg);
overflow: hidden;
}
/* Sidebar (col 1): two stacked sections — Front matter (top, fixed
default 180 px, drag-resizable) and TOC (bottom, takes the rest). */
.md-shell__sidebar {
grid-area: sidebar;
display: grid;
grid-template-rows: 180px 1fr; /* JS overrides on resize */
min-height: 0;
overflow: hidden;
border-right: 1px solid var(--border);
background: var(--bg);
position: relative;
}
/* Vertical sidebar/content resizer. Sits absolutely on the column
boundary so it doesn't occupy a grid track. */
.md-shell__resizer {
grid-area: sidebar;
align-self: stretch;
justify-self: end;
width: 6px;
margin-right: -3px;
cursor: col-resize;
background: transparent;
z-index: 2;
transition: background 0.12s;
}
.md-shell__resizer:hover,
.md-shell__resizer.is-dragging,
.md-shell__resizer:focus-visible {
background: var(--primary);
outline: none;
}
/* Horizontal resizer between front-matter and TOC inside the sidebar.
Spans both rows by placement, then absolutely positioned to overlay
the grid-row boundary. */
.md-shell__fmresizer {
grid-column: 1;
grid-row: 1;
align-self: end;
justify-self: stretch;
height: 6px;
margin-bottom: -3px;
cursor: row-resize;
background: transparent;
z-index: 2;
transition: background 0.12s;
}
.md-shell__fmresizer:hover,
.md-shell__fmresizer.is-dragging,
.md-shell__fmresizer:focus-visible {
background: var(--primary);
outline: none;
}
/* Content (col 2): informational header above the Toast UI editor. */
.md-shell__content {
grid-area: content;
display: grid;
grid-template-rows: auto 1fr;
min-width: 0;
min-height: 0;
overflow: hidden;
}
/* Informational header above the editor: file name on the left, then
dirty marker, status, source hint, save button. Reads as a header
for the content panel — file metadata at a glance. */
.md-shell__infohdr {
display: flex;
align-items: center;
gap: 0.5rem;
padding: 0.4rem 0.75rem;
background: var(--bg-secondary);
border-bottom: 1px solid var(--border);
font-size: 0.85rem;
}
.md-shell__title {
flex: 1;
font-family: var(--font-display);
font-size: 1rem;
font-weight: 600;
color: var(--text);
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
min-width: 0;
}
.md-shell__dirty {
color: var(--text-muted);
font-size: 0.85rem;
min-width: 5.5rem;
text-align: right;
}
.md-shell__status {
color: var(--text-muted);
font-size: 0.85rem;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
max-width: 14rem;
}
.md-shell__source {
color: var(--text-muted);
font-size: 0.75rem;
font-style: italic;
padding: 0.15rem 0.4rem;
border-radius: var(--radius);
background: var(--bg);
border: 1px solid var(--border);
}
.md-shell__download {
/* Slightly tighter than the Save button so a row of three doesn't
crowd the title. The base .btn styles still drive padding/color. */
font-variant-numeric: tabular-nums;
letter-spacing: 0.02em;
}
.md-shell__download[disabled] {
opacity: 0.55;
cursor: progress;
}
/* Editor host: a single grid cell with overflow:hidden so Toast UI's
internal scrollers handle the content. */
.md-shell__editor {
min-width: 0;
min-height: 0;
overflow: hidden;
}
.md-side {
display: grid;
grid-template-rows: auto 1fr;
min-height: 0;
overflow: hidden;
}
.md-side--toc {
border-top: 1px solid var(--border);
}
.md-side__header {
padding: 0.35rem 0.75rem;
background: var(--bg-secondary);
border-bottom: 1px solid var(--border);
font-size: 0.72rem;
font-weight: 600;
text-transform: uppercase;
letter-spacing: 0.06em;
color: var(--text-muted);
}
.md-side__body {
overflow-y: auto;
min-height: 0;
padding: 0.3rem 0;
font-size: 0.85rem;
line-height: 1.45;
}
/* ── Outline list ───────────────────────────────────────────────────────── */
.md-toc__empty {
color: var(--text-muted);
font-style: italic;
padding: 0.5rem 0.75rem;
margin: 0;
font-size: 0.82rem;
}
.md-toc__list {
list-style: none;
margin: 0;
padding: 0;
}
.md-toc__item {
margin: 0;
padding: 0.22rem 0.75rem;
color: var(--text);
cursor: pointer;
border-left: 2px solid transparent;
transition: background 0.1s, border-color 0.1s, color 0.1s;
/* Truncate long headings rather than wrap; the title attribute
carries the full text. */
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}
.md-toc__item:hover {
background: var(--bg-secondary);
border-left-color: var(--primary);
}
.md-toc__item:focus-visible {
outline: 2px solid var(--primary);
outline-offset: -2px;
}
.md-toc__item--l1 { padding-left: 0.75rem; font-weight: 600; }
.md-toc__item--l2 { padding-left: 1.4rem; }
.md-toc__item--l3 { padding-left: 2.05rem; font-size: 0.82rem; }
.md-toc__item--l4 { padding-left: 2.7rem; font-size: 0.8rem; color: var(--text-muted); }
.md-toc__item--l5 { padding-left: 3.35rem; font-size: 0.78rem; color: var(--text-muted); }
.md-toc__item--l6 { padding-left: 4rem; font-size: 0.78rem; color: var(--text-muted); }
/* Flash on click — applied to the heading element in the editor pane.
The class is scoped to .md-toc__flash so it doesn't paint outside
this plugin. */
.md-toc__flash {
background-color: rgba(95, 168, 224, 0.25) !important;
transition: background-color 0.3s ease;
}
/* ── Front matter editor ────────────────────────────────────────────────── */
.md-fm__body {
/* Body cell owns the textarea; sized by the sidebar's grid row. */
padding: 0;
display: block;
overflow: hidden;
}
.md-fm__textarea {
width: 100%;
height: 100%;
box-sizing: border-box;
margin: 0;
padding: 0.4rem 0.6rem;
border: 0;
background: transparent;
color: var(--text);
font-family: var(--font-mono, ui-monospace, SFMono-Regular, Consolas, monospace);
font-size: 0.8rem;
line-height: 1.45;
resize: none;
outline: none;
white-space: pre;
overflow: auto;
tab-size: 2;
}
.md-fm__textarea::placeholder {
color: var(--text-muted);
font-style: italic;
}
.md-fm__textarea:focus {
background: var(--surface-2, rgba(0, 0, 0, 0.025));
}
.md-fm__textarea[readonly] {
color: var(--text-muted);
cursor: not-allowed;
}
/* ── Sort control ────────────────────────────────────────────────────────── */
.sort-control {
display: inline-flex;
align-items: center;
gap: 0.35rem;
font-size: 0.8rem;
color: var(--text-muted);
white-space: nowrap;
}
.sort-control__label {
user-select: none;
}
.sort-control__select {
font-family: var(--font);
font-size: 0.8rem;
padding: 0.2rem 0.4rem;
border: 1px solid var(--border);
border-radius: var(--radius);
background: var(--bg);
color: var(--text);
cursor: pointer;
}
.sort-control__select:focus {
outline: 2px solid var(--primary);
outline-offset: -1px;
}
/* Older .md-fm-section / .fm-list / .md-toc-resizer rules were replaced
by the .md-shell BEM block above. */