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

This commit is contained in:
ZDDC 2026-05-21 17:10:23 -05:00
parent 86d667309d
commit b1ef81077e
7 changed files with 110 additions and 69 deletions

View file

@ -2582,7 +2582,7 @@ td[data-field="trackingNumber"] {
</svg> </svg>
<div class="header-title-group"> <div class="header-title-group">
<span class="app-header__title">ZDDC Archive</span> <span class="app-header__title">ZDDC Archive</span>
<span class="build-timestamp"><span style="color:red;font-weight:bold">v0.0.22-beta · 2026-05-21 16:29:59 · 90a3102</span></span> <span class="build-timestamp"><span style="color:red;font-weight:bold">v0.0.22-beta · 2026-05-21 22:10:16 · 86d6673</span></span>
</div> </div>
<button id="addDirectoryBtn" class="btn btn-primary">Use Local Directory</button> <button id="addDirectoryBtn" class="btn btn-primary">Use Local Directory</button>
<button id="refreshHeaderBtn" class="btn btn-secondary hidden" title="Refresh Data"></button> <button id="refreshHeaderBtn" class="btn btn-secondary hidden" title="Refresh Data"></button>

View file

@ -1224,21 +1224,6 @@ body {
/* .hidden lives in shared/base.css; no per-tool override needed. */ /* .hidden lives in shared/base.css; no per-tool override needed. */
/* Status bar — shows transient errors/info */
.status-bar {
padding: 0.4rem 1rem;
background: var(--bg-secondary);
border-top: 1px solid var(--border);
font-size: 0.85rem;
color: var(--text-muted);
min-height: 1.6rem;
line-height: 1.6rem;
flex-shrink: 0;
}
.status-bar--error { color: #b00020; }
.status-bar--info { color: var(--primary); }
/* Read-only banner for the YAML editor — surfaced by preview-yaml.js /* Read-only banner for the YAML editor — surfaced by preview-yaml.js
when the listing's `writable` bit was false. CodeMirror's readOnly when the listing's `writable` bit was false. CodeMirror's readOnly
mode has no built-in visual signal beyond the disabled caret, so a mode has no built-in visual signal beyond the disabled caret, so a
@ -1502,6 +1487,11 @@ body {
.preview-pane__body { .preview-pane__body {
flex: 1; flex: 1;
min-height: 0; /* critical: lets the flex child shrink to fit
the viewport instead of growing to its
content's natural size (which clips the
YAML editor's bottom when there are many
lines, even with the editor's own scroll) */
overflow: auto; overflow: auto;
display: flex; display: flex;
flex-direction: column; flex-direction: column;
@ -1833,21 +1823,6 @@ body {
color: var(--text-muted); 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) ──────── */ /* ── Markdown plugin (right-pane internals when a .md is selected) ──────── */
/* CSS-Grid shell mirroring mdedit's layout: sidebar on the LEFT /* CSS-Grid shell mirroring mdedit's layout: sidebar on the LEFT
(front matter top + TOC bottom), content on the RIGHT (informational (front matter top + TOC bottom), content on the RIGHT (informational
@ -2369,7 +2344,7 @@ body {
</svg> </svg>
<div class="header-title-group"> <div class="header-title-group">
<span class="app-header__title">ZDDC Browse</span> <span class="app-header__title">ZDDC Browse</span>
<span class="build-timestamp"><span style="color:red;font-weight:bold">v0.0.22-beta · 2026-05-21 16:30:00 · 90a3102</span></span> <span class="build-timestamp"><span style="color:red;font-weight:bold">v0.0.22-beta · 2026-05-21 22:10:16 · 86d6673</span></span>
</div> </div>
<button id="addDirectoryBtn" class="btn btn-primary">Use Local Directory</button> <button id="addDirectoryBtn" class="btn btn-primary">Use Local Directory</button>
<button id="refreshHeaderBtn" class="btn btn-secondary hidden" title="Refresh listing" aria-label="Refresh listing"></button> <button id="refreshHeaderBtn" class="btn btn-secondary hidden" title="Refresh listing" aria-label="Refresh listing"></button>
@ -2445,8 +2420,6 @@ body {
</div> </div>
</main> </main>
<div id="statusBar" class="status-bar"></div>
<!-- Help Panel --> <!-- Help Panel -->
<aside id="help-panel" class="help-panel" hidden aria-labelledby="help-panel-title"> <aside id="help-panel" class="help-panel" hidden aria-labelledby="help-panel-title">
<div class="help-panel__header"> <div class="help-panel__header">
@ -10501,19 +10474,17 @@ var e=function(t,n){return e=Object.setPrototypeOf||{__proto__:[]}instanceof Arr
var REVIEW_OFFSET_DAYS = 7; var REVIEW_OFFSET_DAYS = 7;
var RESPONSE_OFFSET_DAYS = 14; var RESPONSE_OFFSET_DAYS = 14;
// Notifications go through the shared toast helper — there's no
// persistent footer strip in browse anymore.
function statusInfo(msg) { function statusInfo(msg) {
var el = document.getElementById('statusBar'); if (msg && window.zddc && typeof window.zddc.toast === 'function') {
if (!el) return; window.zddc.toast(msg, 'info');
el.textContent = msg || ''; }
el.classList.remove('status-bar--error');
el.classList.add('status-bar--info');
} }
function statusError(msg) { function statusError(msg) {
var el = document.getElementById('statusBar'); if (msg && window.zddc && typeof window.zddc.toast === 'function') {
if (!el) return; window.zddc.toast(msg, 'error');
el.textContent = msg || ''; }
el.classList.remove('status-bar--info');
el.classList.add('status-bar--error');
} }
// Compute today + N days as a YYYY-MM-DD string. // Compute today + N days as a YYYY-MM-DD string.
@ -11563,18 +11534,21 @@ var e=function(t,n){return e=Object.setPrototypeOf||{__proto__:[]}instanceof Arr
// call time, not at IIFE-eval time. // call time, not at IIFE-eval time.
function previewMod() { return window.app.modules.preview; } function previewMod() { return window.app.modules.preview; }
// Notifications route through the shared toast helper (shared/
// toast.js) — there's no persistent footer strip in browse. Same
// signatures as before so the 70+ existing call sites work
// unchanged; statusClear is a no-op (toasts fade on their own and
// single-toast policy guarantees only the latest is visible).
function status(msg, kind) { function status(msg, kind) {
var el = document.getElementById('statusBar'); if (!msg) return;
if (!el) return; if (!window.zddc || typeof window.zddc.toast !== 'function') return;
el.textContent = msg || ''; var level = kind === 'error' ? 'error' : 'info';
el.classList.remove('status-bar--error', 'status-bar--info'); window.zddc.toast(msg, level);
if (kind === 'error') el.classList.add('status-bar--error');
if (kind === 'info') el.classList.add('status-bar--info');
} }
function statusError(msg) { status(msg, 'error'); } function statusError(msg) { status(msg, 'error'); }
function statusInfo(msg) { status(msg, 'info'); } function statusInfo(msg) { status(msg, 'info'); }
function statusClear() { status('', null); } function statusClear() { /* no-op — toasts fade on their own */ }
async function pickLocalDir() { async function pickLocalDir() {
if (typeof window.showDirectoryPicker !== 'function') { if (typeof window.showDirectoryPicker !== 'function') {

View file

@ -1793,7 +1793,7 @@ body.is-elevated::after {
</svg> </svg>
<div class="header-title-group"> <div class="header-title-group">
<span class="app-header__title">ZDDC Classifier</span> <span class="app-header__title">ZDDC Classifier</span>
<span class="build-timestamp"><span style="color:red;font-weight:bold">v0.0.22-beta · 2026-05-21 16:29:59 · 90a3102</span></span> <span class="build-timestamp"><span style="color:red;font-weight:bold">v0.0.22-beta · 2026-05-21 22:10:16 · 86d6673</span></span>
</div> </div>
<button id="addDirectoryBtn" class="btn btn-primary">Use Local Directory</button> <button id="addDirectoryBtn" class="btn btn-primary">Use 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> <button id="refreshHeaderBtn" class="btn btn-secondary hidden" title="Refresh and rescan directory" aria-label="Refresh" style="font-size:1.1rem;"></button>
@ -7896,10 +7896,10 @@ X.B(E,Y);return E}return J}())
const { file, index } = modifiedFiles[i]; const { file, index } = modifiedFiles[i];
try { try {
// Add small delay between operations to prevent race conditions // No inter-operation delay: saveFile() is fully awaited and
if (i > 0) { // each iteration renames a distinct file, so the saves are
await new Promise(resolve => setTimeout(resolve, 200)); // already serialized. (The old 200ms sleep papered over an
} // earlier missing-await bug, since fixed.)
// Validate before saving // Validate before saving
const newFilename = computeNewFilename(file, index); const newFilename = computeNewFilename(file, index);
@ -7942,8 +7942,8 @@ X.B(E,Y);return E}return J}())
errorCount++; errorCount++;
errors.push(`${zddc.joinExtension(file.originalFilename, file.extension)}: ${saveErr.message}`); errors.push(`${zddc.joinExtension(file.originalFilename, file.extension)}: ${saveErr.message}`);
// Add delay after errors to let filesystem stabilize // No post-error delay: each file is independent, so an
await new Promise(resolve => setTimeout(resolve, 300)); // error on one doesn't require "settling" before the next.
} }
} catch (err) { } catch (err) {
console.error(`Error processing file ${index}:`, err); console.error(`Error processing file ${index}:`, err);

View file

@ -1536,7 +1536,7 @@ body {
</svg> </svg>
<div class="header-title-group"> <div class="header-title-group">
<span class="app-header__title">ZDDC</span> <span class="app-header__title">ZDDC</span>
<span class="build-timestamp"><span style="color:red;font-weight:bold">v0.0.22-beta · 2026-05-21 16:29:59 · 90a3102</span></span> <span class="build-timestamp"><span style="color:red;font-weight:bold">v0.0.22-beta · 2026-05-21 22:10:16 · 86d6673</span></span>
</div> </div>
</div> </div>
<div class="header-right"> <div class="header-right">

View file

@ -2635,7 +2635,7 @@ dialog.modal--narrow {
</svg> </svg>
<div class="header-title-group"> <div class="header-title-group">
<span class="app-header__title">ZDDC Transmittal</span> <span class="app-header__title">ZDDC Transmittal</span>
<span class="build-timestamp"><span style="color:red;font-weight:bold">v0.0.22-beta · 2026-05-21 16:29:59 · 90a3102</span></span> <span class="build-timestamp"><span style="color:red;font-weight:bold">v0.0.22-beta · 2026-05-21 22:10:16 · 86d6673</span></span>
</div> </div>
<span id="no-js-notice" class="text-gray-400 text-xs italic">JavaScript not available</span> <span id="no-js-notice" class="text-gray-400 text-xs italic">JavaScript not available</span>
<!-- Publish split-button (Transmittal-specific primary action; <!-- Publish split-button (Transmittal-specific primary action;

View file

@ -1,8 +1,8 @@
# Generated by build.sh — do not edit. One <app>=<build label> per line. # Generated by build.sh — do not edit. One <app>=<build label> per line.
archive=v0.0.22-beta · 2026-05-21 16:29:59 · 90a3102 archive=v0.0.22-beta · 2026-05-21 22:10:16 · 86d6673
transmittal=v0.0.22-beta · 2026-05-21 16:29:59 · 90a3102 transmittal=v0.0.22-beta · 2026-05-21 22:10:16 · 86d6673
classifier=v0.0.22-beta · 2026-05-21 16:29:59 · 90a3102 classifier=v0.0.22-beta · 2026-05-21 22:10:16 · 86d6673
landing=v0.0.22-beta · 2026-05-21 16:29:59 · 90a3102 landing=v0.0.22-beta · 2026-05-21 22:10:16 · 86d6673
form=v0.0.22-beta · 2026-05-21 16:29:59 · 90a3102 form=v0.0.22-beta · 2026-05-21 22:10:16 · 86d6673
tables=v0.0.22-beta · 2026-05-21 16:29:59 · 90a3102 tables=v0.0.22-beta · 2026-05-21 22:10:16 · 86d6673
browse=v0.0.22-beta · 2026-05-21 16:30:00 · 90a3102 browse=v0.0.22-beta · 2026-05-21 22:10:16 · 86d6673

View file

@ -1534,7 +1534,7 @@ body.is-elevated::after {
</svg> </svg>
<div class="header-title-group"> <div class="header-title-group">
<span class="app-header__title" id="table-title">ZDDC Table</span> <span class="app-header__title" id="table-title">ZDDC Table</span>
<span class="build-timestamp"><span style="color:red;font-weight:bold">v0.0.22-dev · 2026-05-21 18:21:55 · a6cb847-dirty</span></span> <span class="build-timestamp"><span style="color:red;font-weight:bold">v0.0.22-beta · 2026-05-21 22:10:16 · 86d6673</span></span>
</div> </div>
</div> </div>
<div class="header-right"> <div class="header-right">
@ -4422,6 +4422,16 @@ body.is-elevated::after {
const propSchema = propertySchemaFor(col); const propSchema = propertySchemaFor(col);
// Read-only cells (schema readOnly:true — e.g. the folder-bound
// originator the server derives from the party folder, or
// server-managed audit fields) can't be edited: any value the
// user typed would be overwritten on write. Suppress edit entry
// entirely; selection still works for keyboard navigation, same
// as the $-prefixed synthesized columns above.
if (propSchema && propSchema.readOnly) {
return;
}
// Complex-type cells (nested object, generic array, oneOf) // Complex-type cells (nested object, generic array, oneOf)
// can't be inline-edited cleanly — punt to the row's form // can't be inline-edited cleanly — punt to the row's form
// editor in a side panel / new page. Phase 2 ships the // editor in a side panel / new page. Phase 2 ships the
@ -5690,8 +5700,28 @@ body.is-elevated::after {
const newEtag = (resp.headers.get('ETag') || '').replace(/"/g, ''); const newEtag = (resp.headers.get('ETag') || '').replace(/"/g, '');
row.yamlUrl = location; row.yamlUrl = location;
row.url = location ? location + '.html' : row.url; row.url = location ? location + '.html' : row.url;
// Re-fetch the just-written row so server-derived fields
// surface immediately: folder-bound originator, the composed
// tracking number's components, and audit stamps. The local
// `merged` lacks these (e.g. originator is read-only and
// never typed). Fall back to merged if the GET fails.
row.data = merged; row.data = merged;
row.etag = newEtag || null; if (location) {
try {
const back = await fetch(location, { credentials: 'same-origin' });
if (back.ok) {
const text = await back.text();
if (text && text.trim() && window.jsyaml) {
row.data = window.jsyaml.load(text) || merged;
}
const fetchedEtag = (back.headers.get('ETag') || '').replace(/"/g, '');
if (fetchedEtag) row.etag = fetchedEtag;
}
} catch (e) {
console.warn('[tables] post-create re-fetch failed; using local merge', e);
}
}
if (!row.etag) row.etag = newEtag || null;
row.isNew = false; row.isNew = false;
// Move the drafts entry (was keyed on the synthetic id) to // Move the drafts entry (was keyed on the synthetic id) to
// the new url, then clear it (data has the merged values). // the new url, then clear it (data has the merged values).
@ -5731,6 +5761,20 @@ body.is-elevated::after {
return { status: 'forbidden' }; return { status: 'forbidden' };
} }
if (resp.status === 409) {
// The composed tracking number collides with an existing
// row (the server rejects duplicates). Surface it on the
// sequence cell — the usual disambiguator — rather than the
// generic errored state, so the user knows to bump a
// component instead of retrying the same values.
let msg = 'Duplicate tracking number — change a component (e.g. sequence).';
try { const t = await resp.text(); if (t && t.trim()) msg = t.trim(); } catch (_) { /* ignore */ }
clearCellInvalid(rowId);
markCellInvalid(rowId, 'sequence', msg);
setRowState(rowId, 'invalid');
return { status: 'duplicate', message: msg };
}
console.warn('[tables] createRow returned', resp.status); console.warn('[tables] createRow returned', resp.status);
setRowState(rowId, 'errored'); setRowState(rowId, 'errored');
return { status: 'http-error', code: resp.status }; return { status: 'http-error', code: resp.status };
@ -7320,6 +7364,29 @@ body.is-elevated::after {
fs.appendChild(childWidget.el); fs.appendChild(childWidget.el);
} }
// Cross-field mirror: a field with `ui:mirrorFrom: <sibling>`
// shows the live value of that sibling. Used by the project-
// rollup forms so the read-only `originator` reflects the
// selected Package (party) — the party folder is the
// originator's source of truth. Display-only: the server is
// still authoritative via the cascade's folder_fields.
for (let i = 0; i < ordered.length; i++) {
const name = ordered[i];
const mirrorFrom = ui && ui[name] && ui[name]['ui:mirrorFrom'];
if (!mirrorFrom || !children[name] || !children[mirrorFrom]) {
continue;
}
const targetInput = children[name].el.querySelector('input, select, textarea');
const sourceInput = children[mirrorFrom].el.querySelector('input, select, textarea');
if (!targetInput || !sourceInput) {
continue;
}
const sync = function () { targetInput.value = sourceInput.value; };
sourceInput.addEventListener('input', sync);
sourceInput.addEventListener('change', sync);
sync(); // initialize from any pre-filled party value
}
return { return {
el: fs, el: fs,
path: path, path: path,