ZDDC/tables/template.html
ZDDC 1721b4b1db feat(tables): explicit Save button + clearer dirty-row marker
Three triggers for flushing pending edits:
  - Save button in the toolbar — shown only when ≥1 row is dirty,
    label reads "Save (N unsaved)". Disappears after a clean settle.
  - Ctrl+S (Cmd+S) anywhere on the page, capturing-phase so it beats
    the browser's "Save Page As" default.
  - focusout of #table-root with a relatedTarget outside the grid —
    catches "edit cell, click a header link, expect it to save".

The row-blur trigger stays — moving between rows still flushes. The
new triggers fill the gap when the user edits one row and then leaves
the grid entirely without first navigating to another row.

Dirty marker gets a 4px (was 3px) left swatch AND a faint blue
background tint on the row, so "unsaved" reads as a row state rather
than a small marker on the edge.

editor.setDraft / clearDraftField notify save.onDraftsChanged,
which refreshes the Save button + reapplies the dirty class.
saveRow on 200/201/202 also refreshes the button so it disappears
the moment its row settles.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-19 08:38:35 -05:00

196 lines
9.4 KiB
HTML

<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>ZDDC Table</title>
<link rel="icon" type="image/svg+xml" href="{{FAVICON}}">
<style>
{{CSS_PLACEHOLDER}}
</style>
</head>
<body>
<header class="app-header">
<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">
<rect x="14" y="18" width="36" height="7"/>
<polygon points="43,25 50,25 21,43 14,43"/>
<rect x="14" y="43" width="36" height="7"/>
</g>
</svg>
<div class="header-title-group">
<span class="app-header__title" id="table-title">ZDDC Table</span>
<span class="build-timestamp">{{BUILD_LABEL}}</span>
</div>
</div>
<div class="header-right">
<!-- Elevation toggle slot. shared/elevation.js fills it
when /.profile/access reports the user has admin
authority; stays empty + hidden for non-admins so
the chrome is quiet for the common case. -->
<span id="elevation-toggle" class="elevation-toggle hidden"
title="Opt into your admin powers for the next 30 minutes (sudo-style)."></span>
<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>
<!-- Table mode: shown for /<dir>/table.html requests. -->
<main id="table-mode" class="table-main" hidden>
<div id="table-description" class="table-description" hidden></div>
<div id="table-status" class="table-status" hidden></div>
<div class="table-toolbar" id="table-toolbar">
<div class="table-toolbar__left">
<span id="table-rowcount" class="table-rowcount" aria-live="polite"></span>
<button type="button" id="table-clear-filters" class="btn btn-secondary btn-sm" hidden>Clear filters</button>
</div>
<div class="table-toolbar__right">
<button type="button" id="table-save" class="btn btn-primary btn-sm" hidden>Save</button>
<button type="button" id="table-export-csv" class="btn btn-secondary btn-sm" hidden>Export CSV</button>
<a id="table-add-row" class="btn btn-primary btn-sm" hidden>+ Add row</a>
</div>
</div>
<div class="table-scroll">
<table id="table-root" class="zddc-table" aria-describedby="table-description">
<thead></thead>
<tbody></tbody>
</table>
</div>
<div id="table-empty" class="table-empty" hidden>No rows match the current filters.</div>
</main>
<!-- Form mode: shown for /<dir>/form.html and /<dir>/<id>.yaml.html
requests. Same bundle ships both modes so a row's "+ Add row"
and click-to-edit reuse the table tool's spec, validator, and
file-IO instead of duplicating them in a separate form HTML. -->
<main id="form-mode" class="form-main" hidden>
<div id="form-status" class="form-status" hidden></div>
<form id="form-root" class="form-root" novalidate></form>
<div class="form-actions">
<button type="button" id="submit-btn" class="btn btn-primary">Submit</button>
</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 Table</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 table?</h3>
<p>The directory you opened — say <code>archive/Acme/mdl/</code>
<em>is</em> the table. <code>table.yaml</code> describes the
columns; <code>form.yaml</code> describes the row-edit form
schema; every other <code>.yaml</code> file in the directory
is one row. Copying the directory anywhere takes the whole
table (spec + form + every row) with it.</p>
<h3>Editing cells</h3>
<p>Click a cell to select it. Then:</p>
<dl>
<dt><kbd></kbd> / <kbd></kbd> / <kbd></kbd> / <kbd></kbd></dt>
<dd>Move selection. Hold <kbd>Shift</kbd> to extend a range.</dd>
<dt><kbd>Tab</kbd> / <kbd>Shift+Tab</kbd></dt>
<dd>Move right / left, wrap to next / previous row.</dd>
<dt><kbd>Enter</kbd> / <kbd>F2</kbd> / double-click / typing</dt>
<dd>Enter edit mode. Typing replaces the cell value; the
others keep it.</dd>
<dt><kbd>Enter</kbd> in edit mode</dt>
<dd>Commit and move down.</dd>
<dt><kbd>Tab</kbd> in edit mode</dt>
<dd>Commit and move right.</dd>
<dt><kbd>Esc</kbd></dt>
<dd>Cancel the edit; restore the prior value.</dd>
<dt><kbd>Delete</kbd> / <kbd>Backspace</kbd></dt>
<dd>Clear every cell in the current selection.</dd>
<dt><kbd>Ctrl+D</kbd> / <kbd>Ctrl+R</kbd></dt>
<dd>Fill the top row down / left column right through the
selected range.</dd>
<dt><kbd>Ctrl+C</kbd> / <kbd>Ctrl+V</kbd></dt>
<dd>Copy / paste — interoperates with Excel and Google
Sheets via tab-separated values.</dd>
<dt><kbd>Ctrl+Z</kbd></dt>
<dd>Undo the last edit (one history per session).</dd>
</dl>
<p>Edits save automatically when you move to a different row.
A small left-edge swatch on the row indicates state:
<strong>blue</strong> = unsaved, <strong>amber</strong> = the
server flagged a validation error, <strong>orange</strong> =
someone else changed this row since you loaded it (you'll
get a prompt with <em>Use mine</em> / <em>Reload</em>).</p>
<h3>Sorting</h3>
<p>Click a column header to sort by that column. Click again to
toggle direction. <kbd>Shift</kbd>-click another header to
add a secondary sort key.</p>
<h3>Filtering</h3>
<p>Type in the box under a column header to filter rows whose
value contains your text (case-insensitive). Same filter UI
for every column.</p>
<h3>Customizing the columns</h3>
<p>The default Master Deliverables List has columns for every
component of a tracking number
(<code>originator</code>, <code>phase</code>,
<code>project</code>, <code>area</code>,
<code>discipline</code>, <code>type</code>,
<code>sequence</code>, <code>suffix</code>) plus deliverable
metadata. To customize, drop your own
<code>table.yaml</code> (and matching
<code>form.yaml</code>) into this directory:</p>
<pre><code>archive/&lt;party&gt;/mdl/
table.yaml ← columns + sort/filter defaults
form.yaml ← per-row schema (JSON Schema)
&lt;id&gt;.yaml ... ← rows</code></pre>
<p>Operator-supplied files override the embedded defaults.
Hide a column by omitting it from <code>columns:</code>;
add a column by appending one (and adding the matching
property in <code>form.yaml</code>'s
<code>schema.properties</code>). The same pattern works
for any directory — <code>&lt;dir&gt;/table.html</code>
is automatically a table whenever
<code>&lt;dir&gt;/table.yaml</code> exists.</p>
<h3>Permissions</h3>
<p>Whether a row is editable depends on the cascading
<code>.zddc</code> permissions for the directory. Rows
in <code>Issued</code> or <code>Received</code> archives
are read-only by design (WORM).</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 table context here on render. Shape:
{
"title": "Optional page title override",
"description": "Optional description shown above the table",
"columns": [{field, title, width?, format?, filter?, sort?, enum?}],
"rows": [{url, data, editable}],
"defaults": {sort?: [{field, dir}], filter?: {field: value}}
}
-->
<script id="table-context" type="application/json">{}</script>
<!--
Form mode context — server injects this for /<dir>/form.html and
/<dir>/<id>.yaml.html. Empty in table-mode renders.
-->
<script id="form-context" type="application/json">{}</script>
<script>
{{JS_PLACEHOLDER}}
</script>
</body>
</html>