chore(embedded): cut v0.0.16-beta
All checks were successful
Notify chart dev on beta cut / notify-chart-dev (push) Successful in 4s

Bake the standardized headers + archive bugfix + browse refactor
into the dev binary. Triggers notify-chart-dev → bumps tnd-zddc-chart
develop with appVersion=0.0.16-beta-<sha>.
This commit is contained in:
ZDDC 2026-05-03 22:22:13 -05:00
parent e67c1b2e06
commit 633411770c
9 changed files with 577 additions and 134 deletions

View file

@ -438,6 +438,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);
@ -1774,9 +1792,10 @@ body.help-open .app-header {
</svg>
<div class="header-title-group">
<span class="app-header__title">ZDDC Markdown</span>
<span class="build-timestamp"><span style="color:red;font-weight:bold">v0.0.16-beta · 2026-05-04 · 582db6d</span></span>
<span class="build-timestamp"><span style="color:red;font-weight:bold">v0.0.16-beta · 2026-05-04 · e67c1b2</span></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>
@ -1792,7 +1811,6 @@ body.help-open .app-header {
<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>
@ -1806,7 +1824,7 @@ body.help-open .app-header {
<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>
@ -1850,7 +1868,7 @@ body.help-open .app-header {
<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>
@ -2954,7 +2972,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');
@ -3754,12 +3772,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');
}
@ -4260,8 +4285,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;
@ -4286,12 +4311,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();
@ -6028,14 +6059,14 @@ function initializeFileNavResizer() {
* 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);
}
@ -6136,7 +6167,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

@ -218,6 +218,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);
@ -2113,7 +2131,7 @@ td[data-field="trackingNumber"] {
</svg>
<div class="header-title-group">
<span class="app-header__title">ZDDC Archive</span>
<span class="build-timestamp"><span style="color:red;font-weight:bold">v0.0.16-beta · 2026-05-04 · 582db6d</span></span>
<span class="build-timestamp"><span style="color:red;font-weight:bold">v0.0.16-beta · 2026-05-04 · e67c1b2</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>
@ -4444,8 +4462,10 @@ td[data-field="trackingNumber"] {
// Handle drops on grouping folders (for creating transmittals)
document.getElementById('groupingFoldersList').addEventListener('drop', handleDrop, false);
// Handle drops on the main app area (for adding directories)
document.getElementById('app').addEventListener('drop', handleAppDrop, false);
// Handle drops on the main app area (for adding directories).
// Note: the root element is id="appContainer" (id="app" was a
// stale reference that crashed dragDrop init in local mode).
document.getElementById('appContainer').addEventListener('drop', handleAppDrop, false);
}
// Prevent default behaviors
@ -7820,7 +7840,7 @@ window.app.modules.filtering = {
// Show unsupported browser message
function showUnsupportedBrowserMessage() {
const app = document.getElementById('app');
const app = document.getElementById('appContainer');
app.innerHTML = `
<div class="empty-state">
<div class="empty-state-content">

View file

@ -218,6 +218,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);
@ -722,22 +740,6 @@ body {
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 {
@ -894,10 +896,10 @@ body {
</svg>
<div class="header-title-group">
<span class="app-header__title">ZDDC Browse</span>
<span class="build-timestamp">v0.0.16-beta · 2026-05-04 · 582db6d</span>
<span class="build-timestamp"><span style="color:red;font-weight:bold">v0.0.16-beta · 2026-05-04 · e67c1b2</span></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>
@ -914,7 +916,7 @@ body {
<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>
@ -975,6 +977,87 @@ body {
<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>
/*!
@ -1616,6 +1699,53 @@ https://github.com/nodeca/pako/blob/main/LICENSE
}
}());
/**
* ZDDC shared help panel — open/close logic.
* Works with all four tools regardless of their module pattern.
* Expects: #help-btn, #help-panel, #help-panel-close in the DOM.
*/
(function () {
'use strict';
function init() {
var helpBtn = document.getElementById('help-btn');
var panel = document.getElementById('help-panel');
var closeBtn = document.getElementById('help-panel-close');
if (!helpBtn || !panel) { return; }
function isOpen() { return !panel.hidden; }
function openPanel() {
panel.hidden = false;
document.body.classList.add('help-open');
}
function closePanel() {
panel.hidden = true;
document.body.classList.remove('help-open');
}
helpBtn.addEventListener('click', function () {
if (isOpen()) { closePanel(); } else { openPanel(); }
});
if (closeBtn) {
closeBtn.addEventListener('click', closePanel);
}
document.addEventListener('keydown', function (e) {
if (e.key === 'Escape' && isOpen()) { closePanel(); }
});
}
if (document.readyState === 'loading') {
document.addEventListener('DOMContentLoaded', init);
} else {
init();
}
}());
/**
* ZDDC — shared preview helpers
*

View file

@ -218,6 +218,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);
@ -1376,10 +1394,10 @@ body.help-open .app-header {
</svg>
<div class="header-title-group">
<span class="app-header__title">ZDDC Classifier</span>
<span class="build-timestamp"><span style="color:red;font-weight:bold">v0.0.16-beta · 2026-05-04 · 582db6d</span></span>
<span class="build-timestamp"><span style="color:red;font-weight:bold">v0.0.16-beta · 2026-05-04 · e67c1b2</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" 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>
@ -1498,7 +1516,7 @@ body.help-open .app-header {
<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>
@ -1517,7 +1535,7 @@ body.help-open .app-header {
<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>
@ -2757,7 +2775,7 @@ body.help-open .app-header {
*/
function showBrowserWarning() {
const warning = document.getElementById('browserWarning');
const selectBtn = document.getElementById('selectDirectoryBtn');
const selectBtn = document.getElementById('addDirectoryBtn');
if (warning) {
warning.classList.remove('hidden');
}
@ -2777,8 +2795,8 @@ body.help-open .app-header {
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'),
@ -2812,8 +2830,8 @@ body.help-open .app-header {
*/
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();
@ -2975,7 +2993,7 @@ body.help-open .app-header {
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

@ -218,6 +218,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);
@ -867,11 +885,12 @@ body {
</svg>
<div class="header-title-group">
<span class="app-header__title">ZDDC</span>
<span class="build-timestamp"><span style="color:red;font-weight:bold">v0.0.16-beta · 2026-05-04 · 582db6d</span></span>
<span class="build-timestamp"><span style="color:red;font-weight:bold">v0.0.16-beta · 2026-05-04 · e67c1b2</span></span>
</div>
</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>
@ -936,6 +955,52 @@ body {
</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>
/**
* ZDDC — shared naming convention library
@ -1564,6 +1629,53 @@ body {
}
}());
/**
* ZDDC shared help panel — open/close logic.
* Works with all four tools regardless of their module pattern.
* Expects: #help-btn, #help-panel, #help-panel-close in the DOM.
*/
(function () {
'use strict';
function init() {
var helpBtn = document.getElementById('help-btn');
var panel = document.getElementById('help-panel');
var closeBtn = document.getElementById('help-panel-close');
if (!helpBtn || !panel) { return; }
function isOpen() { return !panel.hidden; }
function openPanel() {
panel.hidden = false;
document.body.classList.add('help-open');
}
function closePanel() {
panel.hidden = true;
document.body.classList.remove('help-open');
}
helpBtn.addEventListener('click', function () {
if (isOpen()) { closePanel(); } else { openPanel(); }
});
if (closeBtn) {
closeBtn.addEventListener('click', closePanel);
}
document.addEventListener('keydown', function (e) {
if (e.key === 'Escape' && isOpen()) { closePanel(); }
});
}
if (document.readyState === 'loading') {
document.addEventListener('DOMContentLoaded', init);
} else {
init();
}
}());
(function() {
'use strict';
// ZDDC landing page — project picker.

View file

@ -438,6 +438,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);
@ -1774,9 +1792,10 @@ body.help-open .app-header {
</svg>
<div class="header-title-group">
<span class="app-header__title">ZDDC Markdown</span>
<span class="build-timestamp"><span style="color:red;font-weight:bold">v0.0.16-beta · 2026-05-04 · 582db6d</span></span>
<span class="build-timestamp"><span style="color:red;font-weight:bold">v0.0.16-beta · 2026-05-04 · e67c1b2</span></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>
@ -1792,7 +1811,6 @@ body.help-open .app-header {
<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>
@ -1806,7 +1824,7 @@ body.help-open .app-header {
<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>
@ -1850,7 +1868,7 @@ body.help-open .app-header {
<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>
@ -2954,7 +2972,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');
@ -3754,12 +3772,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');
}
@ -4260,8 +4285,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;
@ -4286,12 +4311,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();
@ -6028,14 +6059,14 @@ function initializeFileNavResizer() {
* 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);
}
@ -6136,7 +6167,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

@ -222,6 +222,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);
@ -1041,37 +1059,6 @@ body.help-open .app-header {
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;
@ -2193,13 +2180,8 @@ dialog.modal--narrow {
</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">
@ -2210,14 +2192,22 @@ dialog.modal--narrow {
</svg>
<div class="header-title-group">
<span class="app-header__title">ZDDC Transmittal</span>
<span class="build-timestamp"><span style="color:red;font-weight:bold">v0.0.16-beta · 2026-05-04 · 582db6d</span></span>
<span class="build-timestamp"><span style="color:red;font-weight:bold">v0.0.16-beta · 2026-05-04 · e67c1b2</span></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">

View file

@ -1,8 +1,8 @@
# Generated by build.sh — do not edit. One <app>=<build label> per line.
archive=v0.0.16-beta · 2026-05-04 · 582db6d
transmittal=v0.0.16-beta · 2026-05-04 · 582db6d
classifier=v0.0.16-beta · 2026-05-04 · 582db6d
mdedit=v0.0.16-beta · 2026-05-04 · 582db6d
landing=v0.0.16-beta · 2026-05-04 · 582db6d
form=v0.0.16-beta · 2026-05-04 · 582db6d
browse=v0.0.16-beta · 2026-05-04 · 582db6d
archive=v0.0.16-beta · 2026-05-04 · e67c1b2
transmittal=v0.0.16-beta · 2026-05-04 · e67c1b2
classifier=v0.0.16-beta · 2026-05-04 · e67c1b2
mdedit=v0.0.16-beta · 2026-05-04 · e67c1b2
landing=v0.0.16-beta · 2026-05-04 · e67c1b2
form=v0.0.16-beta · 2026-05-04 · e67c1b2
browse=v0.0.16-beta · 2026-05-04 · e67c1b2

View file

@ -218,6 +218,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);
@ -723,11 +741,12 @@ body.help-open .app-header {
</svg>
<div class="header-title-group">
<span class="app-header__title" id="form-title">ZDDC Form</span>
<span class="build-timestamp"><span style="color:red;font-weight:bold">v0.0.16-beta · 2026-05-04 · 582db6d</span></span>
<span class="build-timestamp"><span style="color:red;font-weight:bold">v0.0.16-beta · 2026-05-04 · e67c1b2</span></span>
</div>
</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>
@ -739,6 +758,51 @@ body.help-open .app-header {
</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:
{
@ -838,6 +902,53 @@ body.help-open .app-header {
}
}());
/**
* ZDDC shared help panel — open/close logic.
* Works with all four tools regardless of their module pattern.
* Expects: #help-btn, #help-panel, #help-panel-close in the DOM.
*/
(function () {
'use strict';
function init() {
var helpBtn = document.getElementById('help-btn');
var panel = document.getElementById('help-panel');
var closeBtn = document.getElementById('help-panel-close');
if (!helpBtn || !panel) { return; }
function isOpen() { return !panel.hidden; }
function openPanel() {
panel.hidden = false;
document.body.classList.add('help-open');
}
function closePanel() {
panel.hidden = true;
document.body.classList.remove('help-open');
}
helpBtn.addEventListener('click', function () {
if (isOpen()) { closePanel(); } else { openPanel(); }
});
if (closeBtn) {
closeBtn.addEventListener('click', closePanel);
}
document.addEventListener('keydown', function (e) {
if (e.key === 'Escape' && isOpen()) { closePanel(); }
});
}
if (document.readyState === 'loading') {
document.addEventListener('DOMContentLoaded', init);
} else {
init();
}
}());
(function (global) {
'use strict';
if (global.formApp) {