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.
704 lines
17 KiB
CSS
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. */
|