chore(headers): standardize across all 7 tools

Bring every tool's header in line with archive's pattern:

  [logo] [title] [version] [Add Local Directory] [⟳] ............... [◐] [?]
  ------------- header-left ---------------       ----- header-right -

Changes per tool:

* browse: rename "Select Directory" → "Add Local Directory"; add the
  red-non-stable wrap to the build label (was missing); add a help
  panel + bundle shared/help.js.

* classifier: rename selectDirectoryBtn → addDirectoryBtn,
  refreshBtn → refreshHeaderBtn for consistency. Update all JS
  callers and welcome-screen copy to the new label.

* mdedit: same id rename. Move the previously-in-pane refresh
  button into the header. Stop renaming the dir button to
  "Directory: <name>" once a folder is loaded — instead use the
  shared btn--subtle variant to de-emphasize while keeping the
  standard label.

* transmittal: convert non-standard <div class="app-header"> with
  spacer/icons containers to <header class="app-header"> with the
  canonical header-left/header-right pair. Move the publish split-
  button into header-left (Transmittal-specific primary action).
  Remove dead .app-header__spacer/__icons/header-icon-btn CSS now
  that nothing references those classes.

* landing, form: add help-btn + help-panel + bundle shared/help.js.
  Each panel is tool-specific (project picker docs for landing,
  schema-driven form docs for form).

Cross-cutting:

* shared/base.css: promote .btn--subtle from browse/css/tree.css
  so any tool with an online mode can de-emphasize Add Local
  Directory consistently.

Verified all 7 tools in headless Chromium: header structure correct,
build label red on non-stable cuts, help panel opens + closes via
button + Esc.
This commit is contained in:
ZDDC 2026-05-03 22:17:02 -05:00
parent a7e84dae15
commit bbb75a87af
17 changed files with 277 additions and 101 deletions

View file

@ -34,6 +34,7 @@ concat_files \
"../shared/zddc.js" \
"../shared/zddc-filter.js" \
"../shared/theme.js" \
"../shared/help.js" \
"../shared/preview-lib.js" \
"js/init.js" \
"js/loader.js" \
@ -51,8 +52,13 @@ tool=browse
compute_build_label "$tool" "$@"
# Replace template placeholders with concatenated CSS/JS + label.
# Non-stable build labels (alpha/beta/dev-dirty) are wrapped in a red
# span — same convention as every other tool (compute_build_label
# sets $is_red=1 for non-stable cuts). Keeps the visual cue
# consistent across tool headers.
awk -v css_file="$css_temp" -v js_file="$js_temp" \
-v build_label="$build_label" -v favicon="$favicon_data_uri" '
-v build_label="$build_label" -v is_red="$is_red" \
-v favicon="$favicon_data_uri" '
/\{\{CSS_PLACEHOLDER\}\}/ {
while ((getline line < css_file) > 0) print line
close(css_file); next
@ -61,8 +67,15 @@ awk -v css_file="$css_temp" -v js_file="$js_temp" \
while ((getline line < js_file) > 0) print line
close(js_file); next
}
{
/\{\{BUILD_LABEL\}\}/ {
if (is_red == "1") {
gsub(/\{\{BUILD_LABEL\}\}/, "<span style=\"color:red;font-weight:bold\">" build_label "</span>")
} else {
gsub(/\{\{BUILD_LABEL\}\}/, build_label)
}
print; next
}
{
gsub(/\{\{FAVICON\}\}/, favicon)
print
}

View file

@ -142,22 +142,6 @@
outline-offset: -1px;
}
/* Subtle button variant used for "Select Directory" when the page
is server-backed (the user usually doesn't need to switch to a
local folder; we keep the option visible but quiet). */
.btn.btn--subtle {
background: transparent;
color: var(--text-muted);
border-color: var(--border);
box-shadow: none;
font-weight: normal;
}
.btn.btn--subtle:hover {
color: var(--text);
background: var(--bg-hover, rgba(0,0,0,0.04));
}
/* Table — folders + files in a tree */
.browse-table {

View file

@ -24,8 +24,8 @@
<span class="app-header__title">ZDDC Browse</span>
<span class="build-timestamp">{{BUILD_LABEL}}</span>
</div>
<button id="addDirectoryBtn" class="btn btn-primary">Select Directory</button>
<button id="refreshHeaderBtn" class="btn btn-secondary hidden" title="Refresh listing" aria-label="Refresh listing"></button>
<button id="addDirectoryBtn" class="btn btn-primary">Add Local Directory</button>
<button id="refreshHeaderBtn" class="btn btn-secondary hidden" title="Refresh listing" aria-label="Refresh listing" style="font-size:1.1rem;"></button>
</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>
@ -42,7 +42,7 @@
<ul>
<li><b>Online</b> — when this page is served by zddc-server, the
listing for the current directory loads automatically.</li>
<li><b>Local</b> — click <i>Select Directory</i> to pick any folder
<li><b>Local</b> — click <i>Add Local Directory</i> to pick any folder
on your computer (Chromium-based browsers).</li>
</ul>
<p>Once loaded: click a folder to expand it, <b>shift-click</b>
@ -103,6 +103,87 @@
<div id="statusBar" class="status-bar"></div>
<!-- Help Panel -->
<aside id="help-panel" class="help-panel" hidden aria-labelledby="help-panel-title">
<div class="help-panel__header">
<h2 id="help-panel-title" class="help-panel__title">Help — ZDDC Browse</h2>
<button type="button" class="help-panel__close" id="help-panel-close" aria-label="Close">&times;</button>
</div>
<div class="help-panel__body">
<h3>What is Browse?</h3>
<p>Browse is a directory listing for ZDDC archives — and any directory. It works in two modes:</p>
<dl>
<dt>Online</dt>
<dd>When the page is served by zddc-server, the listing for the current
URL directory loads automatically. Breadcrumbs link to ancestor folders.</dd>
<dt>Local</dt>
<dd>Click <strong>Add Local Directory</strong> to pick any folder on your
computer. Local mode requires a Chromium-based browser (File System
Access API).</dd>
</dl>
<h3>Tree navigation</h3>
<dl>
<dt>Click a folder</dt>
<dd>Toggle expand/collapse on that folder.</dd>
<dt>Shift-click a folder</dt>
<dd>Recursive expand or collapse — applies to the whole subtree.</dd>
<dt>Click a file</dt>
<dd>Open in the preview popup. Modifier-click (Ctrl/Cmd) or middle-click
opens in a new tab.</dd>
<dt>ZIP files</dt>
<dd>Behave as folders — click to inspect contents inline. JSZip is
bundled, so this works offline.</dd>
<dt>Column headers</dt>
<dd>Click to sort; click again to reverse.</dd>
<dt>Refresh</dt>
<dd>Re-fetches the current directory listing — works for both
local (re-enumerates the FS handle) and online (re-fetches the JSON).</dd>
</dl>
<h3>Filter rows</h3>
<p>Two filter rows live in the table header:</p>
<dl>
<dt>📄 file row</dt>
<dd>Filter by file name (left input) and/or extension (Type input).
File matches stay visible together with their ancestor folders, so
the path to each hit is always shown.</dd>
<dt>📁 folder row</dt>
<dd>Filter by folder name. Matching folders show with their entire
subtree. Combined with file filter: file must also be inside a
matching folder's subtree (intersection).</dd>
</dl>
<p>Filter syntax (shared across all ZDDC tools):</p>
<dl>
<dt><code>term</code></dt>
<dd>Contains "term" (case-insensitive)</dd>
<dt><code>!term</code></dt>
<dd>Does not contain</dd>
<dt><code>^term</code></dt>
<dd>Starts with</dd>
<dt><code>term$</code></dt>
<dd>Ends with</dd>
<dt><code>a b</code></dt>
<dd>Both (AND)</dd>
<dt><code>a | b</code></dt>
<dd>Either (OR)</dd>
<dt><code>el.*spc</code></dt>
<dd>Regex — any-char + any-sequence</dd>
</dl>
<h3>Header buttons</h3>
<dl>
<dt>Add Local Directory</dt>
<dd>Pick a folder from your computer. Works in both modes; in online
mode it's de-emphasized but still available.</dd>
<dt>⟳ Refresh</dt>
<dd>Re-load the current directory listing.</dd>
<dt>◐ Theme</dt>
<dd>Cycle auto / light / dark.</dd>
</dl>
</div>
</aside>
<script>
{{JS_PLACEHOLDER}}
</script>

View file

@ -60,7 +60,7 @@
*/
function showBrowserWarning() {
const warning = document.getElementById('browserWarning');
const selectBtn = document.getElementById('selectDirectoryBtn');
const selectBtn = document.getElementById('addDirectoryBtn');
if (warning) {
warning.classList.remove('hidden');
}
@ -80,8 +80,8 @@
mainApp: document.getElementById('mainApp'),
// Header buttons
selectDirectoryBtn: document.getElementById('selectDirectoryBtn'),
refreshBtn: document.getElementById('refreshBtn'),
addDirectoryBtn: document.getElementById('addDirectoryBtn'),
refreshHeaderBtn: document.getElementById('refreshHeaderBtn'),
saveAllBtn: document.getElementById('saveAllBtn'),
cancelAllBtn: document.getElementById('cancelAllBtn'),
exportHashesBtn: document.getElementById('exportHashesBtn'),
@ -115,8 +115,8 @@
*/
function setupEventListeners() {
// Directory selection
app.dom.selectDirectoryBtn.addEventListener('click', handleSelectDirectory);
app.dom.refreshBtn.addEventListener('click', handleRefresh);
app.dom.addDirectoryBtn.addEventListener('click', handleSelectDirectory);
app.dom.refreshHeaderBtn.addEventListener('click', handleRefresh);
// Drag and drop on welcome screen
setupWelcomeDragDrop();
@ -278,7 +278,7 @@
await app.modules.scanner.scanDirectory(dirHandle);
// Show refresh button now that a directory is loaded
if (app.dom.refreshBtn) { app.dom.refreshBtn.classList.remove('hidden'); }
if (app.dom.refreshHeaderBtn) { app.dom.refreshHeaderBtn.classList.remove('hidden'); }
}
/**

View file

@ -29,8 +29,8 @@
<span class="app-header__title">ZDDC Classifier</span>
<span class="build-timestamp">{{BUILD_LABEL}}</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" aria-label="Refresh" style="font-size:1.1rem;"></button>
<button id="addDirectoryBtn" class="btn btn-primary">Add Local Directory</button>
<button id="refreshHeaderBtn" class="btn btn-secondary hidden" title="Refresh and rescan directory" aria-label="Refresh" style="font-size:1.1rem;"></button>
</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>
@ -149,7 +149,7 @@
<li>Rename one file or all modified files at once</li>
</ul>
<p>Click <strong>Select Directory</strong> to begin.</p>
<p>Click <strong>Add Local Directory</strong> to begin.</p>
<p class="note">This application works entirely in your browser. No data is transmitted to any server.</p>
</div>
@ -168,7 +168,7 @@
<h3>Getting Started</h3>
<ol>
<li>Click <strong>Select Directory</strong> to open a folder containing files to rename.</li>
<li>Click <strong>Add Local Directory</strong> to open a folder containing files to rename.</li>
<li>The folder tree on the left shows all sub-folders. Click a folder to load its files.</li>
<li>Edit cells in the spreadsheet to set the new filename components.</li>
<li>Click <strong>Save All</strong> (or save individual rows) to rename the files on disk.</li>

View file

@ -24,6 +24,7 @@ concat_files \
concat_files \
"../shared/theme.js" \
"../shared/help.js" \
"js/app.js" \
"js/context.js" \
"js/util.js" \

View file

@ -27,6 +27,7 @@
</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>
<button id="help-btn" class="btn btn-secondary" title="Help" aria-label="Help">?</button>
</div>
</header>
@ -38,6 +39,51 @@
</div>
</main>
<!-- Help Panel -->
<aside id="help-panel" class="help-panel" hidden aria-labelledby="help-panel-title">
<div class="help-panel__header">
<h2 id="help-panel-title" class="help-panel__title">Help — ZDDC Form</h2>
<button type="button" class="help-panel__close" id="help-panel-close" aria-label="Close">&times;</button>
</div>
<div class="help-panel__body">
<h3>What is this form?</h3>
<p>This is a schema-driven form rendered by zddc-server. Every
<code>&lt;name&gt;.form.yaml</code> file in the archive becomes an
editable form at <code>&lt;path&gt;/&lt;name&gt;.form.html</code>.
Submissions are saved as <code>&lt;name&gt;/&lt;id&gt;.yaml</code>
files alongside the schema, and re-render with their data filled in
when revisited.</p>
<h3>Filling in the form</h3>
<dl>
<dt>Required fields</dt>
<dd>Marked with an asterisk in their label. Submitting with a
required field empty re-renders the form with an inline error.</dd>
<dt>Validation</dt>
<dd>Server-side via JSON Schema 2020-12 (subset). Client-side
hints (<code>required</code>, <code>min</code>, <code>max</code>,
<code>pattern</code>) are added where the schema specifies them.</dd>
<dt>Submit</dt>
<dd>POSTs to the same URL the form was loaded from. On success the
browser navigates to the saved submission's URL. On failure the
form re-renders with errors inline at each invalid field.</dd>
</dl>
<h3>Editing existing submissions</h3>
<p>Open the saved submission's URL — the form re-renders with its
current data and any errors. Submitting overwrites the same file.
History is in git via your normal commit cycle.</p>
<h3>Header buttons</h3>
<dl>
<dt>◐ Theme</dt>
<dd>Cycle auto / light / dark.</dd>
<dt>? Help</dt>
<dd>This panel. Press <kbd>Esc</kbd> to close.</dd>
</dl>
</div>
</aside>
<!--
Server injects the form context here on render. Shape:
{

View file

@ -26,6 +26,7 @@ concat_files \
"../shared/zddc.js" \
"../shared/zddc-filter.js" \
"../shared/theme.js" \
"../shared/help.js" \
"js/landing.js" \
> "$js_raw"

View file

@ -27,6 +27,7 @@
</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>
<button id="help-btn" class="btn btn-secondary" title="Help" aria-label="Help">?</button>
</div>
</header>
@ -91,6 +92,52 @@
</div>
</main>
<!-- Help Panel -->
<aside id="help-panel" class="help-panel" hidden aria-labelledby="help-panel-title">
<div class="help-panel__header">
<h2 id="help-panel-title" class="help-panel__title">Help — ZDDC</h2>
<button type="button" class="help-panel__close" id="help-panel-close" aria-label="Close">&times;</button>
</div>
<div class="help-panel__body">
<h3>What is this page?</h3>
<p>This is the ZDDC archive landing page — a project picker. It lists every
project (top-level directory) you have access to on this server, plus any
<strong>groups</strong> you've defined for opening multiple projects at once.</p>
<h3>Projects</h3>
<p>Click a project to open it. The project's archive view (list of folders +
files, with all the standard ZDDC tools available inside) loads in the same
tab. Use back/forward to navigate between projects and the picker.</p>
<h3>Groups</h3>
<p>A group bundles a set of projects you commonly open together. Click
<strong>+ New group</strong>, give it a name, click projects to include
them, then save. Opening a group opens all its projects in one go.</p>
<dl>
<dt>Save group</dt>
<dd>Persist the selection as a named group on this server (visible to
other users with access to the same projects).</dd>
<dt>Open selected</dt>
<dd>Open the currently-checked projects without saving as a group.</dd>
<dt>Cancel</dt>
<dd>Exit select mode without saving.</dd>
</dl>
<h3>Access</h3>
<p>Projects and groups are filtered by your account's permissions.
If a URL references a project you don't have access to, a warning banner
appears and the inaccessible items are skipped silently.</p>
<h3>Header buttons</h3>
<dl>
<dt>◐ Theme</dt>
<dd>Cycle auto / light / dark.</dd>
<dt>? Help</dt>
<dd>This panel. Press <kbd>Esc</kbd> to close.</dd>
</dl>
</div>
</aside>
<script>
{{JS_PLACEHOLDER}}
</script>

View file

@ -37,7 +37,7 @@ const SCRATCHPAD_WELCOME = [
'Use this **Scratchpad** for quick notes. Download it any time with the ⬇',
'button on the Scratchpad row in the file list.',
'',
'Click **Select Directory** above to open a folder of Markdown files,',
'Click **Add Local Directory** above to open a folder of Markdown files,',
'or just start typing here.',
'',
].join('\n');

View file

@ -6,14 +6,14 @@
* Set up all event listeners for the application
*/
function setupEventListeners() {
// Select directory button
const selectDirectoryBtn = document.getElementById('select-directory');
// Add Local Directory button (was id="select-directory" / "refresh-directory")
const selectDirectoryBtn = document.getElementById('addDirectoryBtn');
if (selectDirectoryBtn) {
selectDirectoryBtn.addEventListener('click', openDirectory);
}
// Refresh directory button
const refreshDirectoryBtn = document.getElementById('refresh-directory');
// Refresh button (now in header, was in file-nav pane)
const refreshDirectoryBtn = document.getElementById('refreshHeaderBtn');
if (refreshDirectoryBtn) {
refreshDirectoryBtn.addEventListener('click', refreshDirectory);
}

View file

@ -171,12 +171,19 @@ async function openDirectory() {
* @param {string} directoryName - Name of the selected directory
*/
function updateDirectoryStatus(directoryName) {
const selectDirectoryBtn = document.getElementById('select-directory');
// Standardized header pattern (across all ZDDC tools): the button
// keeps the label "Add Local Directory"; de-emphasize it once a
// directory is loaded (the user can still click to pick another)
// by applying the shared btn--subtle variant. The directory name
// is shown in the file-nav pane, not on the button.
const selectDirectoryBtn = document.getElementById('addDirectoryBtn');
if (selectDirectoryBtn) {
selectDirectoryBtn.textContent = `Directory: ${directoryName}`;
selectDirectoryBtn.classList.remove('btn-primary');
selectDirectoryBtn.classList.add('btn--subtle');
selectDirectoryBtn.title = `Loaded: ${directoryName} — click to switch`;
}
const refreshBtn = document.getElementById('refresh-directory');
const refreshBtn = document.getElementById('refreshHeaderBtn');
if (refreshBtn) {
refreshBtn.classList.remove('hidden');
}
@ -677,8 +684,8 @@ async function loadServerDirectory() {
// Only enter server-source mode if the host actually serves JSON directory
// listings (zddc-server / Caddy). On a plain static host the probe fails
// and we must leave "Select Directory" visible so the user can still load
// local files.
// and we must leave "Add Local Directory" visible so the user can still
// load local files.
try {
const resp = await fetch(baseUrl, { headers: { 'Accept': 'application/json' }, cache: 'no-cache' });
if (!resp.ok) return;
@ -703,12 +710,18 @@ async function loadServerDirectory() {
entries: {},
};
// Surface refresh, hide write-only controls. "Select Directory" stays
// visible so the user can switch to a local folder at any time.
const refreshBtn = document.getElementById('refresh-directory');
// Surface refresh, hide write-only controls. "Add Local Directory"
// stays visible (de-emphasized via btn--subtle) so the user can
// switch to a local folder at any time.
const refreshBtn = document.getElementById('refreshHeaderBtn');
if (refreshBtn) refreshBtn.classList.remove('hidden');
const newFileRootBtn = document.getElementById('new-file-root');
if (newFileRootBtn) newFileRootBtn.classList.add('hidden');
const addDirBtn = document.getElementById('addDirectoryBtn');
if (addDirBtn) {
addDirBtn.classList.remove('btn-primary');
addDirBtn.classList.add('btn--subtle');
}
const stats = await readServerDirectory(baseUrl, fileTree, 0);
renderFileTree();

View file

@ -32,7 +32,7 @@ document.addEventListener('DOMContentLoaded', function () {
* Initialize UI based on File System API availability
*/
function initializeApiAvailability() {
const selectDirectoryBtn = document.getElementById('select-directory');
const selectDirectoryBtn = document.getElementById('addDirectoryBtn');
const welcomeHint = document.getElementById('welcome-hint');
const welcomeFirefox = document.getElementById('welcome-firefox');

View file

@ -29,7 +29,8 @@
<span class="app-header__title">ZDDC Markdown</span>
<span class="build-timestamp">{{BUILD_LABEL}}</span>
</div>
<button id="select-directory" class="btn btn-primary" title="Select a Directory">Select Directory</button>
<button id="addDirectoryBtn" class="btn btn-primary" title="Add a local directory">Add Local Directory</button>
<button id="refreshHeaderBtn" class="btn btn-secondary hidden" title="Refresh directory" aria-label="Refresh" style="font-size:1.1rem;"></button>
</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>
@ -45,7 +46,6 @@
<span>Files</span>
<div class="flex items-center gap-1">
<button id="new-file-root" class="btn btn-secondary btn-sm hidden" title="New file in root directory">+</button>
<button id="refresh-directory" class="btn btn-secondary btn-sm hidden" title="Refresh directory"></button>
</div>
</div>
</div>
@ -59,7 +59,7 @@
<div class="pane content-pane flex-1 relative flex flex-col bg-white dark:bg-gray-900 overflow-hidden" id="main-content">
<div id="welcome-screen" class="welcome-screen hidden flex-col items-center justify-center h-full text-gray-500 dark:text-gray-400 text-center p-6">
<p id="welcome-hint" class="text-sm">Click <strong>Scratchpad</strong> in the file list to start editing,<br>or <strong>Select Directory</strong> to work with files.</p>
<p id="welcome-hint" class="text-sm">Click <strong>Scratchpad</strong> in the file list to start editing,<br>or <strong>Add Local Directory</strong> to work with files.</p>
<p id="welcome-firefox" class="text-sm text-amber-600 hidden mt-2">Your browser doesn't support the File System API.<br>Use <strong>Scratchpad</strong> to edit markdown and download as a file.</p>
</div>
@ -103,7 +103,7 @@
<h3>Getting Started</h3>
<ol>
<li>Click <strong>Select Directory</strong> to open a folder. The file tree on the left will populate with all files in that folder.</li>
<li>Click <strong>Add Local Directory</strong> to open a folder. The file tree on the left will populate with all files in that folder.</li>
<li>Click any Markdown file (<code>.md</code>) in the tree to open it in the editor.</li>
<li>Use the <strong>Scratchpad</strong> entry (always visible at the top of the tree) for temporary notes without saving to disk.</li>
</ol>

View file

@ -210,6 +210,24 @@ a:hover {
background: var(--bg-secondary);
}
/* Subdued / de-emphasized variant.
Used on the "Add Local Directory" button when a tool is operating
in server (online) mode the local-dir affordance is still
available but visually quieter, since the typical user already
has the directory loaded from the server. */
.btn.btn--subtle {
background: transparent;
color: var(--text-muted);
border-color: var(--border);
box-shadow: none;
font-weight: normal;
}
.btn.btn--subtle:not(:disabled):hover {
color: var(--text);
background: var(--bg-secondary);
}
.btn-success {
background: var(--success);
color: var(--text-light);

View file

@ -372,37 +372,6 @@
box-sizing: content-box;
}
.app-header__spacer {
flex: 1;
}
.app-header__icons {
display: flex;
align-items: center;
gap: 0.5rem;
}
.header-icon-btn {
display: inline-flex;
align-items: center;
justify-content: center;
width: 28px;
height: 28px;
border-radius: 0.25rem;
border: none;
background: transparent;
color: var(--text-muted);
cursor: pointer;
padding: 0;
text-decoration: none;
transition: color 0.15s, background 0.15s;
}
.header-icon-btn:hover {
color: var(--primary-hover);
background: var(--primary-light);
}
/* ── Fixed footer status bar at viewport bottom ───────── */
.page-footer {
position: fixed;

View file

@ -27,13 +27,8 @@ conventions at https://codeberg.org/VARASYS/ZDDC#file-naming-convention.
</head>
<body class="font-sans text-gray-900">
<div class="app-header print:hidden" data-no-disable="true">
<div class="split-button" id="bottom-menu" hidden>
<button id="bottom-toggle" type="button" class="btn btn-primary split-button__toggle" data-no-disable="true" aria-haspopup="true" aria-expanded="false">&#x25BE;</button>
<button id="bottom-primary" type="button" class="btn btn-primary" data-no-disable="true">Publish</button>
<div class="dropdown-menu hidden" role="menu" id="bottom-dropdown"></div>
</div>
<span id="no-js-notice" class="text-gray-400 text-xs italic">JavaScript not available</span>
<header class="app-header print:hidden" data-no-disable="true">
<div class="header-left">
<svg class="app-header__logo" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 64 64" aria-hidden="true">
<rect width="64" height="64" rx="12" fill="#1e3a5f"/>
<g fill="#fff">
@ -46,12 +41,20 @@ conventions at https://codeberg.org/VARASYS/ZDDC#file-naming-convention.
<span class="app-header__title">ZDDC Transmittal</span>
<span class="build-timestamp">{{BUILD_LABEL}}</span>
</div>
<div class="app-header__spacer"></div>
<div class="app-header__icons">
<span id="no-js-notice" class="text-gray-400 text-xs italic">JavaScript not available</span>
<!-- Publish split-button (Transmittal-specific primary action;
other tools have "Add Local Directory" here instead) -->
<div class="split-button" id="bottom-menu" hidden>
<button id="bottom-toggle" type="button" class="btn btn-primary split-button__toggle" data-no-disable="true" aria-haspopup="true" aria-expanded="false">&#x25BE;</button>
<button id="bottom-primary" type="button" class="btn btn-primary" data-no-disable="true">Publish</button>
<div class="dropdown-menu hidden" role="menu" id="bottom-dropdown"></div>
</div>
</div>
<div class="header-right">
<button type="button" id="theme-btn" class="btn btn-secondary" title="Theme: auto (follows OS)" aria-label="Theme: auto (follows OS)"></button>
<button type="button" id="help-btn" class="btn btn-secondary" aria-label="Help" title="Help">?</button>
</div>
</div>
</header>
<div class="page-container">
<form id="transmittal-form">
<input type="hidden" id="mode" value="edit">