3581 lines
201 KiB
HTML
3581 lines
201 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 — Projects</title>
|
||
<link rel="icon" type="image/svg+xml" href="data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHZpZXdCb3g9IjAgMCA2NCA2NCI+CiAgPHJlY3Qgd2lkdGg9IjY0IiBoZWlnaHQ9IjY0IiByeD0iMTIiIGZpbGw9IiMxZTNhNWYiLz4KICA8ZyBmaWxsPSIjZmZmIj4KICAgIDxyZWN0IHg9IjE0IiB5PSIxOCIgd2lkdGg9IjM2IiBoZWlnaHQ9IjciLz4KICAgIDxwb2x5Z29uIHBvaW50cz0iNDMsMjUgNTAsMjUgMjEsNDMgMTQsNDMiLz4KICAgIDxyZWN0IHg9IjE0IiB5PSI0MyIgd2lkdGg9IjM2IiBoZWlnaHQ9IjciLz4KICA8L2c+Cjwvc3ZnPgo=">
|
||
<style>
|
||
/* shared/fonts.css — base64-inlined woff2 for distinctive typography.
|
||
* Generated once from shared/fonts/*.woff2; do NOT edit by hand.
|
||
* Re-generate via the snippet in shared/fonts/README if you swap weights.
|
||
* "Ship the record player with the record": ZDDC tools render identically
|
||
* offline (file://) and online with no CDN dependency.
|
||
*/
|
||
|
||
@font-face {
|
||
font-family: 'IBM Plex Sans';
|
||
font-style: normal;
|
||
font-weight: 400;
|
||
font-display: swap;
|
||
src: url(data:font/woff2;base64,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) format('woff2');
|
||
}
|
||
|
||
@font-face {
|
||
font-family: 'IBM Plex Sans';
|
||
font-style: normal;
|
||
font-weight: 600;
|
||
font-display: swap;
|
||
src: url(data:font/woff2;base64,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) format('woff2');
|
||
}
|
||
|
||
@font-face {
|
||
font-family: 'Source Serif 4';
|
||
font-style: normal;
|
||
font-weight: 600;
|
||
font-display: swap;
|
||
src: url(data:font/woff2;base64,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) format('woff2');
|
||
}
|
||
|
||
/* ==========================================================================
|
||
ZDDC Shared Base — single source of truth for tokens and primitives
|
||
Included first by every tool's build.sh via ../shared/base.css
|
||
========================================================================== */
|
||
|
||
/* ── CSS custom properties ────────────────────────────────────────────────── */
|
||
:root {
|
||
/* Brand / accent (matches zddc.varasys.io website --accent) */
|
||
--primary: #2a5a8a;
|
||
--primary-hover: #1d4060;
|
||
--primary-active: #163352;
|
||
--primary-light: #e8f0f7;
|
||
|
||
/* Semantic colours */
|
||
--success: #28a745;
|
||
--warning: #d97706;
|
||
--danger: #dc3545;
|
||
--info: #17a2b8;
|
||
|
||
/* Backgrounds */
|
||
--bg: #ffffff;
|
||
--bg-secondary: #f8f9fa;
|
||
--bg-hover: #f0f4f8;
|
||
--bg-selected: var(--primary-light);
|
||
|
||
/* Text */
|
||
--text: #212529;
|
||
--text-muted: #6c757d;
|
||
--text-light: #ffffff;
|
||
|
||
/* Borders */
|
||
--border: #dee2e6;
|
||
--border-dark: #adb5bd;
|
||
|
||
/* Shape */
|
||
--radius: 4px;
|
||
|
||
/* Typography. --font-display covers headings (Source Serif 4 — a refined
|
||
transitional serif that reads as "engineering / document / serious"
|
||
without being academic). --font is body UI text (IBM Plex Sans —
|
||
distinctive engineering sans, with proper figures and tabular nums).
|
||
Both are base64-inlined via shared/fonts.css; system fallbacks kick in
|
||
when fonts.css isn't loaded (e.g. unbuilt component preview). --font-mono
|
||
stays as a system stack; engineering tools rarely benefit from a custom
|
||
mono and platform mono fonts are already excellent. */
|
||
--font-display: 'Source Serif 4', ui-serif, Charter, 'Iowan Old Style', Georgia, serif;
|
||
--font: 'IBM Plex Sans', -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
|
||
--font-mono: 'SF Mono', 'Fira Code', 'Consolas', 'Courier New', monospace;
|
||
}
|
||
|
||
/* ── Dark mode tokens ─────────────────────────────────────────────────────── */
|
||
/* Applied via: OS preference (auto) or [data-theme="dark"] on <html> */
|
||
/* The [data-theme="light"] selector locks light mode regardless of OS pref. */
|
||
@media (prefers-color-scheme: dark) {
|
||
:root:not([data-theme="light"]) {
|
||
--primary: #5fa8e0;
|
||
--primary-hover: #74b6e6;
|
||
--primary-active: #88c4ec;
|
||
--primary-light: #1a3550;
|
||
|
||
--bg: #1e1e1e;
|
||
--bg-secondary: #252526;
|
||
--bg-hover: #2d2d30;
|
||
--bg-selected: #1a3550;
|
||
|
||
--text: #d4d4d4;
|
||
--text-muted: #9d9d9d;
|
||
--text-light: #ffffff;
|
||
|
||
--border: #3e3e42;
|
||
--border-dark: #6e6e72;
|
||
}
|
||
}
|
||
|
||
/* Manual dark override — wins over media query */
|
||
[data-theme="dark"] {
|
||
--primary: #5fa8e0;
|
||
--primary-hover: #74b6e6;
|
||
--primary-active: #88c4ec;
|
||
--primary-light: #1a3550;
|
||
|
||
--bg: #1e1e1e;
|
||
--bg-secondary: #252526;
|
||
--bg-hover: #2d2d30;
|
||
--bg-selected: #1a3550;
|
||
|
||
--text: #d4d4d4;
|
||
--text-muted: #9d9d9d;
|
||
--text-light: #ffffff;
|
||
|
||
--border: #3e3e42;
|
||
--border-dark: #6e6e72;
|
||
}
|
||
|
||
/* ── Reset ────────────────────────────────────────────────────────────────── */
|
||
*, *::before, *::after {
|
||
box-sizing: border-box;
|
||
margin: 0;
|
||
padding: 0;
|
||
}
|
||
|
||
/* ── Base document ────────────────────────────────────────────────────────── */
|
||
html, body {
|
||
height: 100%;
|
||
font-family: var(--font);
|
||
font-size: 16px;
|
||
line-height: 1.5;
|
||
color: var(--text);
|
||
background-color: var(--bg-secondary);
|
||
}
|
||
|
||
/* ── Typography ───────────────────────────────────────────────────────────── */
|
||
h1, h2, h3, h4, h5, h6 {
|
||
font-family: var(--font-display);
|
||
font-weight: 600;
|
||
line-height: 1.2;
|
||
/* Source Serif 4 has subtle optical sizing; let the browser opt in
|
||
where supported (modern Chromium/Firefox). */
|
||
font-optical-sizing: auto;
|
||
}
|
||
|
||
/* Tracking numbers and other engineering identifiers should align in
|
||
columns when stacked vertically. Apply tabular figures wherever we
|
||
render structured numeric data. */
|
||
table, .tabular-nums, code {
|
||
font-variant-numeric: tabular-nums;
|
||
}
|
||
|
||
a {
|
||
color: var(--primary);
|
||
text-decoration: none;
|
||
}
|
||
|
||
a:hover {
|
||
text-decoration: underline;
|
||
}
|
||
|
||
/* ── Utility ──────────────────────────────────────────────────────────────── */
|
||
.hidden {
|
||
display: none !important;
|
||
}
|
||
|
||
.truncate {
|
||
white-space: nowrap;
|
||
overflow: hidden;
|
||
text-overflow: ellipsis;
|
||
}
|
||
|
||
/* ── Scrollbars (webkit) ──────────────────────────────────────────────────── */
|
||
::-webkit-scrollbar {
|
||
width: 7px;
|
||
height: 7px;
|
||
}
|
||
|
||
::-webkit-scrollbar-track {
|
||
background: var(--bg-secondary);
|
||
}
|
||
|
||
::-webkit-scrollbar-thumb {
|
||
background: #c1c1c1;
|
||
border-radius: 4px;
|
||
}
|
||
|
||
::-webkit-scrollbar-thumb:hover {
|
||
background: #a0a0a0;
|
||
}
|
||
|
||
/* ── Button primitive ─────────────────────────────────────────────────────── */
|
||
.btn {
|
||
display: inline-flex;
|
||
align-items: center;
|
||
gap: 0.25rem;
|
||
padding: 0.4rem 0.85rem;
|
||
font-family: var(--font);
|
||
font-size: 0.875rem;
|
||
font-weight: 500;
|
||
line-height: 1.4;
|
||
text-align: center;
|
||
text-decoration: none;
|
||
white-space: nowrap;
|
||
vertical-align: middle;
|
||
cursor: pointer;
|
||
border: 1px solid transparent;
|
||
border-radius: var(--radius);
|
||
transition: background 0.15s, box-shadow 0.15s, border-color 0.15s, color 0.15s;
|
||
background: var(--bg-secondary);
|
||
color: var(--text);
|
||
}
|
||
|
||
.btn:disabled,
|
||
.btn[disabled] {
|
||
opacity: 0.5;
|
||
cursor: not-allowed;
|
||
}
|
||
|
||
.btn:not(:disabled):hover {
|
||
box-shadow: 0 1px 4px rgba(0, 0, 0, 0.12);
|
||
}
|
||
|
||
.btn:not(:disabled):active {
|
||
box-shadow: none;
|
||
}
|
||
|
||
/* Variants */
|
||
.btn-primary {
|
||
background: var(--primary);
|
||
color: var(--text-light);
|
||
border-color: var(--primary);
|
||
}
|
||
|
||
.btn-primary:not(:disabled):hover {
|
||
background: var(--primary-hover);
|
||
border-color: var(--primary-hover);
|
||
color: var(--text-light);
|
||
}
|
||
|
||
.btn-primary:not(:disabled):active {
|
||
background: var(--primary-active);
|
||
border-color: var(--primary-active);
|
||
}
|
||
|
||
.btn-secondary {
|
||
background: var(--bg);
|
||
color: var(--text);
|
||
border-color: var(--border);
|
||
}
|
||
|
||
.btn-secondary:not(:disabled):hover {
|
||
background: var(--bg-secondary);
|
||
}
|
||
|
||
/* Subdued / de-emphasized variant.
|
||
Used on the "Use 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);
|
||
border-color: var(--success);
|
||
}
|
||
|
||
.btn-danger {
|
||
background: var(--danger);
|
||
color: var(--text-light);
|
||
border-color: var(--danger);
|
||
}
|
||
|
||
/* Sizes */
|
||
.btn-sm {
|
||
padding: 0.25rem 0.5rem;
|
||
font-size: 0.75rem;
|
||
}
|
||
|
||
.btn-lg {
|
||
padding: 0.6rem 1.4rem;
|
||
font-size: 1rem;
|
||
}
|
||
|
||
.btn-link {
|
||
background: transparent;
|
||
border-color: transparent;
|
||
color: var(--primary);
|
||
padding-left: 0;
|
||
padding-right: 0;
|
||
}
|
||
|
||
.btn-link:not(:disabled):hover {
|
||
text-decoration: underline;
|
||
box-shadow: none;
|
||
}
|
||
|
||
/* ── App header chrome ────────────────────────────────────────────────────── */
|
||
.app-header {
|
||
display: flex;
|
||
align-items: center;
|
||
justify-content: space-between;
|
||
padding: 0.35rem 1rem;
|
||
background: var(--bg-secondary);
|
||
border-bottom: 1px solid var(--border);
|
||
flex-shrink: 0;
|
||
/* Let the left / right groups wrap to a second row at narrow
|
||
viewports rather than overflowing the viewport edge. row-gap
|
||
gives a small breathing strip when wrapped. */
|
||
flex-wrap: wrap;
|
||
row-gap: 0.3rem;
|
||
}
|
||
|
||
/* Left and right groups inside .app-header. Both flex-row so their
|
||
children (logo, title, action button, theme icon, etc.) lay out
|
||
horizontally rather than stacking. Left side gets a slightly
|
||
larger gap because it carries the title group and an action
|
||
button; right side is just icon buttons. */
|
||
.header-left {
|
||
display: flex;
|
||
align-items: center;
|
||
gap: 0.75rem;
|
||
/* Allow the title to shrink (and ellipsize) before the action
|
||
buttons get pushed off-screen at narrow viewports. */
|
||
min-width: 0;
|
||
flex-wrap: wrap;
|
||
row-gap: 0.3rem;
|
||
}
|
||
|
||
.header-right {
|
||
display: flex;
|
||
align-items: center;
|
||
gap: 0.5rem;
|
||
flex-shrink: 0;
|
||
}
|
||
|
||
/* Title group (title + build label). Made shrinkable so narrow
|
||
viewports don't push the action buttons out of view; the title
|
||
itself ellipsizes via the rule below. */
|
||
.header-title-group {
|
||
display: flex;
|
||
align-items: baseline;
|
||
gap: 0.5rem;
|
||
min-width: 0;
|
||
flex-shrink: 1;
|
||
}
|
||
|
||
/* Tool name inside the header. Renders in the display serif so the
|
||
tool's identity reads as a document title, not a UI label.
|
||
overflow + ellipsis on min-width:0 lets the title compress
|
||
gracefully when there's no room. */
|
||
.app-header__title {
|
||
font-family: var(--font-display);
|
||
font-size: 18px;
|
||
font-weight: 600;
|
||
color: var(--text);
|
||
letter-spacing: 0;
|
||
white-space: nowrap;
|
||
overflow: hidden;
|
||
text-overflow: ellipsis;
|
||
min-width: 0;
|
||
}
|
||
|
||
/* Brand logo — sits left of the title in every tool's app-header.
|
||
Self-contained: the SVG provides its own dark blue rounded background,
|
||
so no extra wrapper styling is needed. */
|
||
.app-header__logo {
|
||
width: 26px;
|
||
height: 26px;
|
||
flex-shrink: 0;
|
||
display: block;
|
||
}
|
||
|
||
/* Page-load reveal. The header is the first thing a user sees — a
|
||
short staggered fade-in over ~360ms turns "instant pop-in" into a
|
||
subtle "the tool is composing itself for you" beat. Pure CSS, no
|
||
JS; respects prefers-reduced-motion. The stagger order (logo →
|
||
title → action buttons → right-side icons) mirrors the reading
|
||
order of the chrome itself. */
|
||
@keyframes zddc-header-rise {
|
||
from { opacity: 0; transform: translateY(-4px); }
|
||
to { opacity: 1; transform: translateY(0); }
|
||
}
|
||
|
||
.app-header__logo,
|
||
.header-title-group,
|
||
.header-left > .btn,
|
||
.header-right > * {
|
||
animation: zddc-header-rise 360ms cubic-bezier(0.2, 0.7, 0.2, 1) both;
|
||
}
|
||
|
||
.app-header__logo { animation-delay: 0ms; }
|
||
.header-title-group { animation-delay: 60ms; }
|
||
.header-left > .btn { animation-delay: 120ms; }
|
||
.header-right > *:nth-child(1) { animation-delay: 180ms; }
|
||
.header-right > *:nth-child(2) { animation-delay: 220ms; }
|
||
.header-right > *:nth-child(3) { animation-delay: 260ms; }
|
||
|
||
@media (prefers-reduced-motion: reduce) {
|
||
.app-header__logo,
|
||
.header-title-group,
|
||
.header-left > .btn,
|
||
.header-right > * {
|
||
animation: none;
|
||
}
|
||
}
|
||
|
||
/* ── Build timestamp ──────────────────────────────────────────────────────── */
|
||
.build-timestamp {
|
||
font-size: 0.55rem;
|
||
color: var(--text-muted);
|
||
opacity: 0.7;
|
||
font-weight: 300;
|
||
white-space: nowrap;
|
||
padding-top: 0.15rem;
|
||
}
|
||
|
||
/* Title + timestamp stacked vertically on the left side of the header */
|
||
.header-title-group {
|
||
display: flex;
|
||
flex-direction: column;
|
||
gap: 0;
|
||
line-height: 1;
|
||
}
|
||
|
||
/* ── Icon buttons (help, theme, refresh) ─────────────────────────────────── */
|
||
/* Square, centered — overrides the asymmetric text-button padding/line-height */
|
||
#help-btn,
|
||
#theme-btn,
|
||
#refreshHeaderBtn {
|
||
width: 2rem;
|
||
height: 2rem;
|
||
padding: 0;
|
||
line-height: 1;
|
||
display: inline-flex;
|
||
align-items: center;
|
||
justify-content: center;
|
||
font-size: 1rem;
|
||
}
|
||
|
||
/* The refresh ⟳ glyph renders slightly smaller than ◐ / ? — bump to match. */
|
||
#refreshHeaderBtn {
|
||
font-size: 1.1rem;
|
||
}
|
||
|
||
/* Toast CSS lives in shared/toast.css; loaded by every tool's build. */
|
||
|
||
/* ── Empty state ──────────────────────────────────────────────────────────── */
|
||
/* The "nothing's loaded yet" screen. By default, centers its inner
|
||
content in whatever space the parent gives it (works inside a flex
|
||
column). Tools that need to overlay an existing layout (archive,
|
||
classifier) add .empty-state--overlay; the screen pins below the
|
||
app header and on top of whatever underlying layout already exists.
|
||
Inner content uses BEM-ish .empty-state__inner with two variants:
|
||
plain (left-aligned, doc-style) and --centered (centered card). */
|
||
|
||
.empty-state {
|
||
flex: 1;
|
||
display: flex;
|
||
align-items: center;
|
||
justify-content: center;
|
||
padding: 2rem;
|
||
background: var(--bg);
|
||
}
|
||
|
||
.empty-state--overlay {
|
||
position: absolute;
|
||
top: 50px; /* clear the app-header */
|
||
left: 0;
|
||
right: 0;
|
||
bottom: 0;
|
||
z-index: 10;
|
||
flex: none;
|
||
}
|
||
|
||
.empty-state__inner {
|
||
max-width: 640px;
|
||
color: var(--text-muted);
|
||
line-height: 1.5;
|
||
}
|
||
|
||
.empty-state__inner h2 {
|
||
color: var(--text);
|
||
margin: 0 0 1rem;
|
||
font-size: 1.5rem;
|
||
}
|
||
|
||
.empty-state__inner p {
|
||
margin-bottom: 1rem;
|
||
}
|
||
|
||
.empty-state__inner ul,
|
||
.empty-state__inner ol {
|
||
margin: 1rem 0;
|
||
padding-left: 1.5rem;
|
||
}
|
||
|
||
.empty-state__inner li {
|
||
margin: 0.4rem 0;
|
||
}
|
||
|
||
.empty-state__inner .note {
|
||
font-size: 0.85rem;
|
||
font-style: italic;
|
||
}
|
||
|
||
/* Centered variant: tighter max-width + centered text. Used by tools
|
||
whose empty-state reads as a "welcome card" (archive, classifier)
|
||
rather than a doc-style page (browse). */
|
||
.empty-state__inner--centered {
|
||
max-width: 500px;
|
||
text-align: center;
|
||
padding: 2rem;
|
||
}
|
||
|
||
/* Bullet list inside an empty-state — keep the bullets left-aligned
|
||
even when the surrounding card is centered. */
|
||
.welcome-list {
|
||
text-align: left;
|
||
margin: 0.5rem auto;
|
||
max-width: 400px;
|
||
}
|
||
|
||
/* ── Theme and help icon buttons ─────────────────────────────────────────── */
|
||
#theme-btn,
|
||
#help-btn {
|
||
font-size: 1rem;
|
||
}
|
||
|
||
/* ── Help panel (shared slide-out drawer) ─────────────────────────────────── */
|
||
/* Used by all four tools. Toggle open/close via shared/help.js. */
|
||
|
||
.help-panel {
|
||
position: fixed;
|
||
top: 0;
|
||
right: 0;
|
||
width: min(420px, 85vw);
|
||
height: 100vh;
|
||
z-index: 1000;
|
||
background: var(--bg);
|
||
border-left: 1px solid var(--border);
|
||
box-shadow: -2px 0 12px rgba(0, 0, 0, 0.08);
|
||
display: flex;
|
||
flex-direction: column;
|
||
transform: translateX(100%);
|
||
transition: transform 0.25s ease;
|
||
}
|
||
|
||
.help-panel:not([hidden]) {
|
||
transform: translateX(0);
|
||
}
|
||
|
||
.help-panel[hidden] {
|
||
display: flex;
|
||
transform: translateX(100%);
|
||
pointer-events: none;
|
||
}
|
||
|
||
.help-panel__header {
|
||
display: flex;
|
||
align-items: center;
|
||
justify-content: space-between;
|
||
padding: 0.75rem 1rem;
|
||
border-bottom: 1px solid var(--border);
|
||
flex-shrink: 0;
|
||
background: var(--bg);
|
||
}
|
||
|
||
.help-panel__title {
|
||
font-size: 1rem;
|
||
font-weight: 700;
|
||
color: var(--text);
|
||
margin: 0;
|
||
}
|
||
|
||
.help-panel__close {
|
||
background: none;
|
||
border: none;
|
||
color: var(--text-muted);
|
||
font-size: 1.35rem;
|
||
cursor: pointer;
|
||
padding: 0.25rem 0.5rem;
|
||
border-radius: var(--radius);
|
||
line-height: 1;
|
||
transition: background 0.15s, color 0.15s;
|
||
}
|
||
|
||
.help-panel__close:hover {
|
||
color: var(--text);
|
||
background: var(--bg-secondary);
|
||
}
|
||
|
||
.help-panel__body {
|
||
flex: 1;
|
||
overflow-y: auto;
|
||
padding: 1rem 1rem 2rem;
|
||
font-size: 0.85rem;
|
||
line-height: 1.6;
|
||
color: var(--text);
|
||
}
|
||
|
||
.help-panel__body h3 {
|
||
font-size: 0.95rem;
|
||
font-weight: 700;
|
||
margin: 1.25rem 0 0.35rem;
|
||
color: var(--text);
|
||
border-bottom: 1px solid var(--border);
|
||
padding-bottom: 0.15rem;
|
||
}
|
||
|
||
.help-panel__body h3:first-child {
|
||
margin-top: 0;
|
||
}
|
||
|
||
.help-panel__body h4 {
|
||
font-size: 0.7rem;
|
||
font-weight: 700;
|
||
text-transform: uppercase;
|
||
letter-spacing: 0.06em;
|
||
margin: 1.25rem 0 0.3rem;
|
||
padding-left: 0.5rem;
|
||
border-left: 3px solid var(--border-dark);
|
||
color: var(--text-muted);
|
||
}
|
||
|
||
.help-panel__body p {
|
||
margin: 0 0 0.5rem;
|
||
}
|
||
|
||
.help-panel__body ol,
|
||
.help-panel__body ul {
|
||
padding-left: 1.5rem;
|
||
margin: 0.3rem 0 0.5rem;
|
||
}
|
||
|
||
.help-panel__body li {
|
||
margin-bottom: 0.3rem;
|
||
}
|
||
|
||
.help-panel__body dl {
|
||
margin: 0.3rem 0;
|
||
}
|
||
|
||
.help-panel__body dt {
|
||
font-weight: 600;
|
||
color: var(--text);
|
||
}
|
||
|
||
.help-panel__body dd {
|
||
margin: 0 0 0.5rem 1rem;
|
||
color: var(--text-muted);
|
||
}
|
||
|
||
.help-panel__body code {
|
||
font-family: var(--font-mono);
|
||
font-size: 0.8em;
|
||
background: var(--bg-secondary);
|
||
padding: 0.1em 0.3em;
|
||
border-radius: 3px;
|
||
}
|
||
|
||
.help-badge {
|
||
font-size: 0.7rem;
|
||
font-weight: 600;
|
||
padding: 0.1rem 0.35rem;
|
||
border-radius: var(--radius);
|
||
vertical-align: middle;
|
||
letter-spacing: 0.02em;
|
||
}
|
||
|
||
.help-badge--draft {
|
||
color: #2563eb;
|
||
background: #eff6ff;
|
||
}
|
||
|
||
.help-badge--published {
|
||
color: #7c3aed;
|
||
background: #f5f3ff;
|
||
}
|
||
|
||
/* Shrink main content when help panel is open */
|
||
body.help-open .app-header {
|
||
margin-right: min(420px, 85vw);
|
||
}
|
||
|
||
/* ── Column filter inputs (shared across archive, classifier, transmittal) ─── */
|
||
.column-filter {
|
||
display: block;
|
||
width: 100%;
|
||
box-sizing: border-box;
|
||
margin-top: 0.25rem;
|
||
padding: 0.2rem 0.4rem;
|
||
font-size: 0.8rem;
|
||
font-family: var(--font);
|
||
border: 1px solid var(--border);
|
||
border-radius: var(--radius);
|
||
background: var(--bg);
|
||
color: var(--text);
|
||
transition: border-color 0.15s;
|
||
}
|
||
|
||
.column-filter:focus {
|
||
border-color: var(--primary);
|
||
outline: none;
|
||
box-shadow: 0 0 0 1px rgba(42, 90, 138, 0.35);
|
||
}
|
||
|
||
.column-filter::placeholder {
|
||
color: var(--text-muted);
|
||
}
|
||
|
||
/* ── Narrow-viewport behavior ─────────────────────────────────────────────────
|
||
ZDDC tools are desktop-first (engineering workstations, large monitors),
|
||
but a baseline narrow rule keeps them usable on a tablet in landscape or
|
||
a window split next to a document. Three principled moves:
|
||
|
||
1. Smaller header padding so the chrome doesn't dominate the viewport.
|
||
2. The build-timestamp inside .header-title-group is hidden — it's a
|
||
traceability artifact, never an immediate-action element. (The full
|
||
label remains visible via the help panel and the "About" surface.)
|
||
3. .header-right gap tightens; the action button next to the title
|
||
drops to a 32x32 icon-only square via the .btn-square pattern (tools
|
||
that haven't adopted .btn-square just keep the text button — graceful).
|
||
|
||
Each tool is welcome to add its own narrow-mode rules in css/layout.css;
|
||
this block is the shared baseline. */
|
||
@media (max-width: 800px) {
|
||
.app-header {
|
||
padding: 0.3rem 0.6rem;
|
||
}
|
||
.app-header__title {
|
||
font-size: 16px;
|
||
}
|
||
.header-left {
|
||
gap: 0.5rem;
|
||
}
|
||
.header-right {
|
||
gap: 0.25rem;
|
||
}
|
||
/* Hide the build-timestamp on narrow viewports — it's reference info,
|
||
not a primary affordance, and steals horizontal space from the title.
|
||
Still reachable via the help panel and DOM. */
|
||
.header-title-group .build-timestamp {
|
||
display: none;
|
||
}
|
||
/* Action buttons that have an emoji-only or symbol-only label keep
|
||
their full width; text-labeled action buttons in the header shrink
|
||
to a more compact pad to fit. */
|
||
.header-left > .btn {
|
||
padding: 0.3rem 0.6rem;
|
||
font-size: 0.85rem;
|
||
}
|
||
}
|
||
|
||
/* Very narrow (phone-width). Stack the header-left children vertically so
|
||
the title and action button each get their own line; tools can override
|
||
this in their own CSS if they have a dedicated mobile layout. */
|
||
@media (max-width: 480px) {
|
||
.app-header {
|
||
align-items: flex-start;
|
||
flex-direction: column;
|
||
gap: 0.4rem;
|
||
}
|
||
.header-left,
|
||
.header-right {
|
||
width: 100%;
|
||
justify-content: space-between;
|
||
}
|
||
}
|
||
|
||
/* shared/toast.css — single-toast notification styles paired with
|
||
shared/toast.js. Uses BEM-ish .zddc-toast prefix to avoid collisions
|
||
with tool-local .toast classes; the old classifier rules can stay
|
||
alongside until this file is concatenated above them in the build. */
|
||
|
||
.zddc-toast {
|
||
position: fixed;
|
||
bottom: 2rem;
|
||
right: 2rem;
|
||
background: var(--bg);
|
||
color: var(--text);
|
||
padding: 0.875rem 1.25rem;
|
||
border-radius: var(--radius);
|
||
border: 1px solid var(--border);
|
||
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.15);
|
||
z-index: 9000;
|
||
max-width: 400px;
|
||
font-size: 0.875rem;
|
||
cursor: pointer;
|
||
animation: zddc-toast-in 0.3s ease-out;
|
||
}
|
||
|
||
.zddc-toast--success { border-left: 4px solid var(--success); }
|
||
.zddc-toast--error { border-left: 4px solid var(--danger); }
|
||
.zddc-toast--info { border-left: 4px solid var(--info); }
|
||
.zddc-toast--warning { border-left: 4px solid var(--warning); }
|
||
|
||
.zddc-toast--fade {
|
||
animation: zddc-toast-out 0.3s ease-out forwards;
|
||
}
|
||
|
||
@keyframes zddc-toast-in {
|
||
from { transform: translateX(100%); opacity: 0; }
|
||
to { transform: translateX(0); opacity: 1; }
|
||
}
|
||
|
||
@keyframes zddc-toast-out {
|
||
from { transform: translateX(0); opacity: 1; }
|
||
to { transform: translateX(100%); opacity: 0; }
|
||
}
|
||
|
||
/* shared/elevation.css — admin-elevation toggle in the tool header.
|
||
Renders only for users with admin scope (handled by elevation.js;
|
||
the placeholder is `.hidden` by default). When visible, sits left
|
||
of the theme button — sudo-style affordance for opting into admin
|
||
powers. */
|
||
|
||
.elevation-toggle {
|
||
display: inline-flex;
|
||
align-items: center;
|
||
gap: 0.3rem;
|
||
font-size: 0.78rem;
|
||
color: var(--text-muted);
|
||
user-select: none;
|
||
cursor: pointer;
|
||
padding: 0.15rem 0.45rem;
|
||
border: 1px solid var(--border);
|
||
border-radius: var(--radius);
|
||
background: var(--bg);
|
||
transition: background 0.12s, border-color 0.12s, color 0.12s;
|
||
}
|
||
|
||
.elevation-toggle:hover {
|
||
background: var(--bg-hover);
|
||
border-color: var(--border-dark);
|
||
}
|
||
|
||
.elevation-toggle input[type="checkbox"] {
|
||
margin: 0;
|
||
cursor: pointer;
|
||
accent-color: var(--danger);
|
||
}
|
||
|
||
.elevation-toggle__label {
|
||
cursor: pointer;
|
||
letter-spacing: 0.02em;
|
||
}
|
||
|
||
/* Active state — when elevation is ON, the toggle reads as "armed"
|
||
so the user can't miss that admin powers are currently live.
|
||
:has(:checked) lets us style the wrapper based on the inner
|
||
checkbox without JS. */
|
||
.elevation-toggle:has(input:checked) {
|
||
background: rgba(220, 53, 69, 0.12);
|
||
border-color: var(--danger);
|
||
color: var(--danger);
|
||
font-weight: 600;
|
||
}
|
||
|
||
/* Page-wide chrome when admin mode is active. The toggle alone is
|
||
easy to miss; these add an inescapable visual cue:
|
||
1. Thin red border around the entire viewport — peripheral-
|
||
vision reminder regardless of which tool / scroll position.
|
||
2. Sticky banner across the top with a one-click "Drop admin"
|
||
button so the user can disarm without hunting for the toggle.
|
||
Both rendered ONLY when the zddc-elevate cookie is set; the
|
||
shared/elevation.js init() syncs the body class on every page
|
||
load and tears it down when elevation is cleared.
|
||
|
||
Frame uses fixed positioning + pointer-events:none so it doesn't
|
||
reflow content or steal clicks. An inset outline on <body> was
|
||
tried first but overdrew content in tools whose root layout butts
|
||
right up to the viewport edge (browse split-pane, archive grid). */
|
||
body.is-elevated::after {
|
||
content: "";
|
||
position: fixed;
|
||
inset: 0;
|
||
border: 3px solid var(--danger, #dc3545);
|
||
pointer-events: none;
|
||
z-index: 9200; /* above banner (9100) so the frame paints on top */
|
||
}
|
||
|
||
.elevation-banner {
|
||
display: flex;
|
||
align-items: center;
|
||
gap: 0.75rem;
|
||
padding: 0.4rem 0.9rem;
|
||
background: rgba(220, 53, 69, 0.95);
|
||
color: #fff;
|
||
font-size: 0.85rem;
|
||
font-weight: 500;
|
||
letter-spacing: 0.01em;
|
||
position: sticky;
|
||
top: 0;
|
||
z-index: 9100; /* above modal-overlay (9000) so it's never hidden */
|
||
box-shadow: 0 1px 4px rgba(0, 0, 0, 0.18);
|
||
}
|
||
|
||
.elevation-banner__dot {
|
||
width: 0.5rem;
|
||
height: 0.5rem;
|
||
background: #fff;
|
||
border-radius: 50%;
|
||
box-shadow: 0 0 0 0 rgba(255, 255, 255, 0.7);
|
||
animation: elev-pulse 1.6s infinite;
|
||
flex-shrink: 0;
|
||
}
|
||
|
||
@keyframes elev-pulse {
|
||
0% { box-shadow: 0 0 0 0 rgba(255, 255, 255, 0.7); }
|
||
70% { box-shadow: 0 0 0 8px rgba(255, 255, 255, 0); }
|
||
100% { box-shadow: 0 0 0 0 rgba(255, 255, 255, 0); }
|
||
}
|
||
|
||
.elevation-banner__msg {
|
||
flex: 1 1 auto;
|
||
}
|
||
|
||
.elevation-banner__off {
|
||
background: rgba(255, 255, 255, 0.18);
|
||
border: 1px solid rgba(255, 255, 255, 0.7);
|
||
color: #fff;
|
||
padding: 0.18rem 0.65rem;
|
||
border-radius: var(--radius, 4px);
|
||
font-size: 0.78rem;
|
||
font-weight: 600;
|
||
letter-spacing: 0.02em;
|
||
cursor: pointer;
|
||
flex-shrink: 0;
|
||
}
|
||
.elevation-banner__off:hover {
|
||
background: rgba(255, 255, 255, 0.3);
|
||
}
|
||
|
||
/* shared/logo.css — paired with shared/logo.js. The wrapping anchor
|
||
inherits the logo's box and adds a subtle hover/focus affordance
|
||
so it reads as clickable without altering the logo's visual weight. */
|
||
|
||
.app-header__logo-link {
|
||
display: inline-flex;
|
||
align-items: center;
|
||
text-decoration: none;
|
||
border-radius: var(--radius);
|
||
transition: opacity 0.15s, box-shadow 0.15s;
|
||
}
|
||
|
||
.app-header__logo-link:hover .app-header__logo,
|
||
.app-header__logo-link:focus-visible .app-header__logo {
|
||
opacity: 0.82;
|
||
}
|
||
|
||
.app-header__logo-link:focus-visible {
|
||
outline: 2px solid var(--primary);
|
||
outline-offset: 2px;
|
||
}
|
||
|
||
/* Landing page layout */
|
||
body {
|
||
margin: 0;
|
||
font-family: var(--font);
|
||
font-size: 14px;
|
||
background: var(--bg-secondary);
|
||
color: var(--text);
|
||
}
|
||
|
||
.landing-main {
|
||
max-width: 880px;
|
||
margin: 32px auto;
|
||
padding: 0 16px 64px;
|
||
}
|
||
|
||
/* Welcome / hero */
|
||
.landing-hero {
|
||
margin: 0 0 24px;
|
||
padding: 0 4px;
|
||
}
|
||
.landing-hero h1 {
|
||
margin: 0 0 8px;
|
||
font-size: 1.5rem;
|
||
font-weight: 600;
|
||
color: var(--text);
|
||
}
|
||
.landing-hero-sub {
|
||
margin: 0;
|
||
color: var(--text-muted);
|
||
font-size: 0.95rem;
|
||
line-height: 1.5;
|
||
max-width: 60ch;
|
||
}
|
||
|
||
/* Access warning banner */
|
||
.access-warning-banner {
|
||
display: flex;
|
||
align-items: center;
|
||
justify-content: space-between;
|
||
padding: 10px 16px;
|
||
background: #fff3cd;
|
||
border: 1px solid #ffc107;
|
||
border-radius: var(--radius);
|
||
color: #664d03;
|
||
font-size: 0.875rem;
|
||
margin-bottom: 16px;
|
||
gap: 12px;
|
||
}
|
||
.access-warning-banner.hidden { display: none; }
|
||
.warning-dismiss-btn {
|
||
background: none;
|
||
border: none;
|
||
cursor: pointer;
|
||
color: #664d03;
|
||
font-size: 1rem;
|
||
padding: 0 4px;
|
||
flex-shrink: 0;
|
||
}
|
||
|
||
/* Cards (groups + projects) */
|
||
.landing-card {
|
||
background: var(--bg);
|
||
border: 1px solid var(--border);
|
||
border-radius: var(--radius);
|
||
overflow: visible;
|
||
box-shadow: 0 1px 2px rgba(0,0,0,0.04);
|
||
margin-bottom: 16px;
|
||
}
|
||
.landing-card:last-child { margin-bottom: 0; }
|
||
|
||
.landing-card-header {
|
||
display: flex;
|
||
align-items: center;
|
||
justify-content: space-between;
|
||
padding: 12px 16px;
|
||
border-bottom: 1px solid var(--border);
|
||
gap: 12px;
|
||
flex-wrap: wrap;
|
||
}
|
||
.landing-card-title {
|
||
display: flex;
|
||
align-items: baseline;
|
||
gap: 8px;
|
||
}
|
||
.landing-card-header h2 {
|
||
margin: 0;
|
||
font-size: 1rem;
|
||
font-weight: 600;
|
||
color: var(--text);
|
||
}
|
||
.landing-count {
|
||
color: var(--text-muted);
|
||
font-size: 0.875rem;
|
||
}
|
||
|
||
.landing-header-actions {
|
||
display: flex;
|
||
align-items: center;
|
||
gap: 8px;
|
||
flex-wrap: wrap;
|
||
}
|
||
|
||
/* ── Groups card ─────────────────────────────────────────────────────────── */
|
||
.groups-container { min-height: 0; }
|
||
.groups-empty {
|
||
padding: 16px 24px;
|
||
color: var(--text-muted);
|
||
font-size: 0.9rem;
|
||
line-height: 1.5;
|
||
}
|
||
.groups-table {
|
||
width: 100%;
|
||
border-collapse: collapse;
|
||
font-size: 0.9rem;
|
||
}
|
||
.groups-row {
|
||
cursor: pointer;
|
||
transition: background 0.08s;
|
||
}
|
||
.groups-row:hover { background: var(--bg-hover); }
|
||
.groups-row td {
|
||
padding: 8px 12px;
|
||
border-bottom: 1px solid var(--border);
|
||
vertical-align: middle;
|
||
}
|
||
.groups-row:last-child td { border-bottom: none; }
|
||
.groups-row-name {
|
||
font-weight: 500;
|
||
color: var(--text);
|
||
}
|
||
.groups-row-count {
|
||
color: var(--text-muted);
|
||
font-size: 0.85rem;
|
||
width: 8em;
|
||
white-space: nowrap;
|
||
}
|
||
.groups-row-actions {
|
||
width: 4.5em;
|
||
text-align: right;
|
||
white-space: nowrap;
|
||
}
|
||
.groups-btn-edit, .groups-btn-delete {
|
||
background: none;
|
||
border: 1px solid transparent;
|
||
border-radius: 3px;
|
||
cursor: pointer;
|
||
color: var(--text-muted);
|
||
font-size: 0.95rem;
|
||
padding: 2px 6px;
|
||
line-height: 1;
|
||
}
|
||
.groups-btn-edit:hover {
|
||
background: var(--bg-hover);
|
||
color: var(--text);
|
||
}
|
||
.groups-btn-delete:hover {
|
||
background: var(--bg-hover);
|
||
color: var(--danger, #c0392b);
|
||
}
|
||
|
||
/* ── Select-mode action bar ──────────────────────────────────────────────── */
|
||
.select-action-bar {
|
||
display: flex;
|
||
align-items: center;
|
||
justify-content: space-between;
|
||
gap: 12px;
|
||
padding: 10px 16px;
|
||
background: var(--bg-secondary);
|
||
border-bottom: 1px solid var(--border);
|
||
flex-wrap: wrap;
|
||
}
|
||
.select-action-bar.hidden { display: none; }
|
||
.select-action-bar__label {
|
||
display: flex;
|
||
align-items: center;
|
||
gap: 8px;
|
||
flex: 1 1 240px;
|
||
min-width: 0;
|
||
}
|
||
.select-action-bar__label > span {
|
||
font-weight: 500;
|
||
color: var(--text);
|
||
flex-shrink: 0;
|
||
}
|
||
.group-name-input {
|
||
flex: 1 1 0;
|
||
min-width: 120px;
|
||
padding: 5px 10px;
|
||
border: 1px solid var(--border);
|
||
border-radius: 3px;
|
||
font-size: 0.875rem;
|
||
background: var(--bg);
|
||
color: var(--text);
|
||
}
|
||
.group-name-input:focus {
|
||
outline: none;
|
||
border-color: var(--primary);
|
||
}
|
||
.select-action-bar__buttons {
|
||
display: flex;
|
||
gap: 6px;
|
||
flex-wrap: wrap;
|
||
}
|
||
|
||
/* ── Projects card ───────────────────────────────────────────────────────── */
|
||
.project-list-container {
|
||
min-height: 80px;
|
||
}
|
||
|
||
/* Empty / error states */
|
||
.project-list-empty {
|
||
padding: 32px 24px;
|
||
text-align: center;
|
||
color: var(--text-muted);
|
||
}
|
||
.project-list-empty h3 {
|
||
margin: 0 0 8px;
|
||
font-size: 1rem;
|
||
font-weight: 600;
|
||
color: var(--text);
|
||
}
|
||
.project-list-empty p {
|
||
margin: 4px 0;
|
||
font-size: 0.9rem;
|
||
line-height: 1.5;
|
||
}
|
||
.landing-empty-help {
|
||
color: var(--text-muted);
|
||
font-size: 0.85rem !important;
|
||
margin-top: 12px !important;
|
||
max-width: 50ch;
|
||
margin-left: auto !important;
|
||
margin-right: auto !important;
|
||
}
|
||
|
||
/* Loading state */
|
||
.project-list-loading {
|
||
padding: 32px 16px;
|
||
text-align: center;
|
||
color: var(--text-muted);
|
||
font-size: 0.875rem;
|
||
}
|
||
|
||
/* Project table */
|
||
.project-table {
|
||
width: 100%;
|
||
border-collapse: collapse;
|
||
font-size: 0.9rem;
|
||
}
|
||
.project-table thead {
|
||
background: var(--bg-secondary);
|
||
}
|
||
.project-table-headers th {
|
||
padding: 10px 12px;
|
||
text-align: left;
|
||
font-weight: 600;
|
||
color: var(--text);
|
||
border-bottom: 1px solid var(--border);
|
||
user-select: none;
|
||
}
|
||
.project-table-headers th[data-sort] {
|
||
cursor: pointer;
|
||
}
|
||
.project-table-headers th[data-sort]:hover {
|
||
background: var(--bg-hover);
|
||
}
|
||
.project-table-checkbox-col {
|
||
width: 32px;
|
||
padding: 8px 12px;
|
||
text-align: center;
|
||
}
|
||
.project-table-checkbox-col input[type="checkbox"] {
|
||
width: 16px;
|
||
height: 16px;
|
||
cursor: pointer;
|
||
accent-color: var(--primary);
|
||
margin: 0;
|
||
vertical-align: middle;
|
||
}
|
||
.project-table-name-col {
|
||
min-width: 140px;
|
||
}
|
||
.project-table-title-col {
|
||
width: 100%;
|
||
}
|
||
.sort-indicator {
|
||
color: var(--text-muted);
|
||
margin-left: 4px;
|
||
font-size: 0.75rem;
|
||
}
|
||
.sort-indicator.active {
|
||
color: var(--text);
|
||
}
|
||
|
||
.project-table-filters th {
|
||
padding: 6px 12px;
|
||
border-bottom: 1px solid var(--border);
|
||
background: var(--bg);
|
||
}
|
||
.project-table-filters .column-filter {
|
||
width: 100%;
|
||
box-sizing: border-box;
|
||
padding: 4px 8px;
|
||
border: 1px solid var(--border);
|
||
border-radius: 3px;
|
||
font-size: 0.85rem;
|
||
background: var(--bg);
|
||
color: var(--text);
|
||
}
|
||
.project-table-filters .column-filter:focus {
|
||
outline: none;
|
||
border-color: var(--primary);
|
||
}
|
||
.project-table-filters .column-filter.filter-active {
|
||
background: var(--bg-secondary);
|
||
border-color: var(--primary);
|
||
}
|
||
|
||
.project-table-row {
|
||
cursor: pointer;
|
||
transition: background 0.08s;
|
||
}
|
||
.project-table-row:hover {
|
||
background: var(--bg-hover);
|
||
}
|
||
.project-table-row.is-selected {
|
||
background: var(--bg-selected, rgba(0, 105, 217, 0.08));
|
||
}
|
||
.project-table-row.is-selected:hover {
|
||
background: var(--bg-selected-hover, rgba(0, 105, 217, 0.15));
|
||
}
|
||
.project-table-row td {
|
||
padding: 8px 12px;
|
||
border-bottom: 1px solid var(--border);
|
||
vertical-align: middle;
|
||
}
|
||
.project-table-no-title {
|
||
color: var(--text-muted);
|
||
}
|
||
.project-table-no-match {
|
||
padding: 24px 16px !important;
|
||
text-align: center;
|
||
color: var(--text-muted);
|
||
}
|
||
|
||
/* ── Project mode ──────────────────────────────────────────────────────── */
|
||
/* Activated when location.pathname is a single project segment (e.g.
|
||
/Project-1). Picker UI is hidden; this block surfaces the four
|
||
lifecycle-stage cards and MDL editing instructions. */
|
||
|
||
.project-title {
|
||
font-size: 1.6rem;
|
||
margin: 0 0 0.25rem;
|
||
font-weight: 600;
|
||
}
|
||
|
||
.project-title__subtle {
|
||
color: var(--text-muted);
|
||
font-weight: normal;
|
||
font-size: 0.9rem;
|
||
}
|
||
|
||
.lead {
|
||
color: var(--text-muted);
|
||
margin: 0.25rem 0 1.5rem;
|
||
}
|
||
|
||
.stages {
|
||
display: grid;
|
||
grid-template-columns: repeat(auto-fit, minmax(220px, 1fr));
|
||
gap: 0.85rem;
|
||
margin: 1rem 0 1.5rem;
|
||
}
|
||
|
||
.stage-card {
|
||
display: block;
|
||
padding: 1rem 1.1rem;
|
||
background: var(--bg);
|
||
border: 1px solid var(--border);
|
||
border-radius: var(--radius);
|
||
text-decoration: none;
|
||
color: var(--text);
|
||
transition: border-color 0.15s, box-shadow 0.15s, transform 0.05s;
|
||
cursor: pointer;
|
||
}
|
||
|
||
.stage-card:hover {
|
||
border-color: var(--primary);
|
||
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.08);
|
||
}
|
||
|
||
.stage-card:active {
|
||
transform: translateY(1px);
|
||
}
|
||
|
||
.stage-card h3 {
|
||
margin: 0 0 0.3rem;
|
||
font-size: 1rem;
|
||
color: var(--primary);
|
||
font-weight: 600;
|
||
}
|
||
|
||
.stage-card p {
|
||
margin: 0;
|
||
color: var(--text-muted);
|
||
font-size: 0.875rem;
|
||
}
|
||
|
||
/* MDL card variant: same outer styling as a stage card but contains
|
||
an interactive control (party <select> + Open button) instead of
|
||
navigating on click of the whole card. The :hover lift applies
|
||
regardless. */
|
||
.stage-card--mdl {
|
||
cursor: default;
|
||
}
|
||
.stage-card--mdl:active {
|
||
transform: none;
|
||
}
|
||
.stage-card__action {
|
||
display: flex;
|
||
gap: 0.5rem;
|
||
align-items: center;
|
||
margin-top: 0.65rem;
|
||
}
|
||
.mdl-party-select {
|
||
flex: 1 1 auto;
|
||
min-width: 0;
|
||
padding: 0.3rem 0.5rem;
|
||
font-family: var(--font);
|
||
font-size: 0.9rem;
|
||
border: 1px solid var(--border);
|
||
border-radius: var(--radius);
|
||
background: var(--bg-secondary);
|
||
color: var(--text);
|
||
}
|
||
.mdl-party-select:focus {
|
||
border-color: var(--primary);
|
||
outline: none;
|
||
box-shadow: 0 0 0 2px rgba(95, 168, 224, 0.25);
|
||
}
|
||
.mdl-party-select:disabled {
|
||
opacity: 0.55;
|
||
cursor: not-allowed;
|
||
}
|
||
.stage-card__hint {
|
||
margin: 0.65rem 0 0 !important;
|
||
font-size: 0.78rem !important;
|
||
color: var(--text-muted) !important;
|
||
line-height: 1.4;
|
||
}
|
||
.visually-hidden {
|
||
position: absolute;
|
||
width: 1px;
|
||
height: 1px;
|
||
padding: 0;
|
||
margin: -1px;
|
||
overflow: hidden;
|
||
clip: rect(0, 0, 0, 0);
|
||
white-space: nowrap;
|
||
border: 0;
|
||
}
|
||
|
||
.browse-link {
|
||
display: inline-block;
|
||
margin-top: 0.25rem;
|
||
color: var(--primary);
|
||
text-decoration: none;
|
||
cursor: pointer;
|
||
}
|
||
|
||
.browse-link:hover {
|
||
text-decoration: underline;
|
||
}
|
||
|
||
#projectView ol {
|
||
padding-left: 1.5rem;
|
||
}
|
||
|
||
#projectView ol li {
|
||
margin-bottom: 0.4rem;
|
||
}
|
||
|
||
#projectView code {
|
||
font-family: var(--font-mono);
|
||
background: var(--bg-secondary);
|
||
padding: 0.1em 0.35em;
|
||
border-radius: 3px;
|
||
font-size: 0.86em;
|
||
}
|
||
|
||
#projectView h2 {
|
||
font-size: 1.1rem;
|
||
margin: 2.25rem 0 0.5rem;
|
||
padding-bottom: 0.3rem;
|
||
border-bottom: 1px solid var(--border);
|
||
font-weight: 600;
|
||
}
|
||
|
||
.party-list {
|
||
padding-left: 1.5rem;
|
||
margin: 0.4rem 0 1rem;
|
||
}
|
||
|
||
.party-list li {
|
||
margin-bottom: 0.25rem;
|
||
}
|
||
|
||
.party-list a {
|
||
color: var(--primary);
|
||
text-decoration: none;
|
||
}
|
||
|
||
.party-list a:hover {
|
||
text-decoration: underline;
|
||
}
|
||
|
||
.party-list-none-yet {
|
||
color: var(--text-muted);
|
||
font-style: italic;
|
||
}
|
||
|
||
</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">ZDDC</span>
|
||
<span class="build-timestamp">v0.0.17</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>
|
||
|
||
<main id="landingMain" class="landing-main">
|
||
<!-- Picker mode (deployment root /). Project picker + groups. -->
|
||
<div id="pickerView">
|
||
<!-- Welcome / hero -->
|
||
<section class="landing-hero">
|
||
<h1>Welcome to the ZDDC Archive</h1>
|
||
<p class="landing-hero-sub">
|
||
Click a group or project below to open the archive. Use
|
||
<strong>+ New group</strong> to bundle a set of projects you open together.
|
||
</p>
|
||
</section>
|
||
|
||
<!-- Access warning banner (shown when URL ?projects= contains inaccessible items) -->
|
||
<div id="accessWarningBanner" class="access-warning-banner hidden" role="alert">
|
||
<span id="accessWarningText"></span>
|
||
<button class="warning-dismiss-btn" onclick="LandingApp.dismissWarning()" aria-label="Dismiss">×</button>
|
||
</div>
|
||
|
||
<!-- Groups card -->
|
||
<div class="landing-card">
|
||
<div class="landing-card-header">
|
||
<div class="landing-card-title">
|
||
<h2>Groups</h2>
|
||
<span id="groupCount" class="landing-count"></span>
|
||
</div>
|
||
<div class="landing-header-actions">
|
||
<button id="newGroupBtn" class="btn btn-secondary btn-sm" onclick="LandingApp.startCreateGroup()">+ New group</button>
|
||
</div>
|
||
</div>
|
||
<div id="groupsContainer" class="groups-container">
|
||
<!-- Populated by JS -->
|
||
</div>
|
||
</div>
|
||
|
||
<!-- Projects card -->
|
||
<div class="landing-card">
|
||
<!-- Action bar (only visible in select-mode) -->
|
||
<div id="selectActionBar" class="select-action-bar hidden">
|
||
<div class="select-action-bar__label">
|
||
<span id="selectModeTitle"></span>
|
||
<input id="groupNameInput" type="text" class="group-name-input" placeholder="Group name">
|
||
</div>
|
||
<div class="select-action-bar__buttons">
|
||
<button id="cancelSelectBtn" class="btn btn-secondary btn-sm" onclick="LandingApp.cancelSelect()">Cancel</button>
|
||
<button id="openSelectedBtn" class="btn btn-secondary btn-sm" onclick="LandingApp.openSelectedVisible()">Open selected</button>
|
||
<button id="saveGroupBtn" class="btn btn-primary btn-sm" onclick="LandingApp.saveGroup()">Save group</button>
|
||
</div>
|
||
</div>
|
||
|
||
<div class="landing-card-header">
|
||
<div class="landing-card-title">
|
||
<h2>Projects</h2>
|
||
<span id="projectCount" class="landing-count"></span>
|
||
</div>
|
||
</div>
|
||
|
||
<div id="projectListContainer" class="project-list-container">
|
||
<!-- Populated by JS -->
|
||
<div class="project-list-loading">Loading projects…</div>
|
||
</div>
|
||
</div>
|
||
</div><!-- /pickerView -->
|
||
|
||
<!-- Project mode (/<project>). Stage cards + MDL section. Shown
|
||
by landing.js when location.pathname is a single segment. -->
|
||
<div id="projectView" class="hidden">
|
||
<h1 id="projectTitle" class="project-title">
|
||
<span id="projectName"></span>
|
||
<span class="project-title__subtle">— project workspace</span>
|
||
</h1>
|
||
<p class="lead">Pick a lifecycle stage, or browse all files.</p>
|
||
|
||
<div class="stages">
|
||
<a class="stage-card" id="stageArchive">
|
||
<h3>Archive</h3>
|
||
<p>Permanent record of issued and received transmittals, organized by counterparty.</p>
|
||
</a>
|
||
<a class="stage-card" id="stageWorking">
|
||
<h3>Working</h3>
|
||
<p>Per-user drafting workspace. Your folder is private by default; you can grant access by editing its <code>.zddc</code> file.</p>
|
||
</a>
|
||
<a class="stage-card" id="stageStaging">
|
||
<h3>Staging</h3>
|
||
<p>Outbound transmittals being prepared for issue.</p>
|
||
</a>
|
||
<a class="stage-card" id="stageReviewing">
|
||
<h3>Reviewing</h3>
|
||
<p>Pending review responses — inbound submittals paired with their in-progress drafts.</p>
|
||
</a>
|
||
<!-- MDL card. Visually matches the four stage cards above but
|
||
is interactive rather than a plain link: pick a party from
|
||
the select, then Open. -->
|
||
<div class="stage-card stage-card--mdl" id="stageMdl">
|
||
<h3>Master Deliverables List</h3>
|
||
<p>The editable list of expected deliverables for each counterparty.</p>
|
||
<div class="stage-card__action">
|
||
<label class="visually-hidden" for="mdlPartySelect">Party</label>
|
||
<select id="mdlPartySelect" class="mdl-party-select" disabled>
|
||
<option value="">Loading parties…</option>
|
||
</select>
|
||
<button id="mdlOpenBtn" class="btn btn-primary btn-sm" disabled>Open MDL</button>
|
||
</div>
|
||
<p class="stage-card__hint" id="mdlHint">
|
||
The MDL view renders even when <code>archive/<party>/mdl/</code> doesn't yet exist —
|
||
you can start editing before any transmittals have been exchanged.
|
||
</p>
|
||
</div>
|
||
</div>
|
||
|
||
<p><a id="browseAllLink" class="browse-link">Browse all files →</a></p>
|
||
</div><!-- /projectView -->
|
||
</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">×</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
|
||
*
|
||
* Canonical implementation of all ZDDC filename, folder name, tracking number,
|
||
* revision, and status logic. Included in every tool's build via shared/zddc.js.
|
||
*
|
||
* Exposed as window.zddc (plain global) so it works with every tool's module
|
||
* pattern (archive globals, classifier IIFE, transmittal IIFE, mdedit globals).
|
||
*
|
||
* Public API
|
||
* ----------
|
||
* zddc.parseFilename(str) → ParsedFile | null
|
||
* zddc.parseFolder(str) → ParsedFolder | null
|
||
* zddc.parseRevision(str) → ParsedRevision
|
||
* zddc.formatFilename(parts) → string
|
||
* zddc.formatFolder(parts) → string
|
||
* zddc.compareRevisions(a, b) → number (-1 | 0 | 1)
|
||
* zddc.isValidStatus(str) → boolean
|
||
* zddc.STATUSES → string[]
|
||
*
|
||
* ParsedFile { trackingNumber, revision, status, title, extension }
|
||
* ParsedFolder { date, trackingNumber, status, title }
|
||
* ParsedRevision { base, modifier, modifierType, modifierNumber, isDraft, full }
|
||
*/
|
||
|
||
(function (root) {
|
||
'use strict';
|
||
|
||
// ── Valid status codes ───────────────────────────────────────────────────
|
||
|
||
/**
|
||
* Complete list of valid ZDDC document status codes.
|
||
* '---' denotes an unknown or not-yet-assigned status.
|
||
*/
|
||
var STATUSES = [
|
||
'---',
|
||
'IFA', 'IFB', 'IFC', 'IFD', 'IFI', 'IFP', 'IFR', 'IFU',
|
||
'REC',
|
||
'RSA', 'RSB', 'RSC', 'RSD', 'RSI',
|
||
'TBD',
|
||
];
|
||
|
||
var STATUS_SET = {};
|
||
for (var _i = 0; _i < STATUSES.length; _i++) {
|
||
STATUS_SET[STATUSES[_i]] = true;
|
||
}
|
||
|
||
function isValidStatus(str) {
|
||
return !!STATUS_SET[str];
|
||
}
|
||
|
||
// ── Filename parsing ─────────────────────────────────────────────────────
|
||
|
||
/**
|
||
* Canonical file regex.
|
||
* Matches: TRACKING_REVISION (STATUS) - TITLE.EXT
|
||
*
|
||
* Tracking number: no underscores, no whitespace.
|
||
* Revision: no whitespace, no parentheses.
|
||
* Status: anything inside parentheses (validated separately).
|
||
* Title: everything up to the last dot.
|
||
* Extension: after the last dot (lowercased by parseFilename).
|
||
*/
|
||
var FILE_RE = /^([^_\s]+)_([^\s()_]+)\s*\(([^)]+)\)\s*-\s*(\S.*\S|\S)\.\s*([^\s.]+)$/;
|
||
|
||
/**
|
||
* Parse a ZDDC filename.
|
||
*
|
||
* @param {string} filename
|
||
* @returns {{ trackingNumber: string, revision: string, status: string,
|
||
* title: string, extension: string, valid: boolean } | null}
|
||
* null only if filename is falsy.
|
||
* `valid` is true when all fields matched the ZDDC pattern.
|
||
*/
|
||
function parseFilename(filename) {
|
||
if (!filename) { return null; }
|
||
|
||
var match = filename.match(FILE_RE);
|
||
|
||
if (!match) {
|
||
var lastDot = filename.lastIndexOf('.');
|
||
return {
|
||
trackingNumber: '',
|
||
revision: '',
|
||
status: '',
|
||
title: lastDot > 0 ? filename.substring(0, lastDot) : filename,
|
||
extension: lastDot > 0 ? filename.substring(lastDot + 1).toLowerCase() : '',
|
||
valid: false,
|
||
};
|
||
}
|
||
|
||
return {
|
||
trackingNumber: match[1].trim(),
|
||
revision: match[2].trim(),
|
||
status: match[3].trim(),
|
||
title: match[4].trim(),
|
||
extension: match[5].toLowerCase(),
|
||
valid: true,
|
||
};
|
||
}
|
||
|
||
// ── Folder name parsing ──────────────────────────────────────────────────
|
||
|
||
/**
|
||
* Transmittal folder regex.
|
||
* Matches: YYYY-MM-DD_TRACKING (STATUS) - TITLE
|
||
*/
|
||
var FOLDER_RE = /^(\d{4}-\d{2}-\d{2})_([^_\s(]+)\s*\(([^)]+)\)\s*-\s*(.+)$/;
|
||
|
||
/**
|
||
* Parse a ZDDC transmittal folder name.
|
||
*
|
||
* @param {string} foldername
|
||
* @returns {{ date: string, trackingNumber: string, status: string,
|
||
* title: string, valid: boolean } | null}
|
||
* null only if foldername is falsy.
|
||
*/
|
||
function parseFolder(foldername) {
|
||
if (!foldername) { return null; }
|
||
|
||
var match = foldername.match(FOLDER_RE);
|
||
|
||
if (!match) {
|
||
return {
|
||
date: '',
|
||
trackingNumber: '',
|
||
status: '',
|
||
title: foldername,
|
||
valid: false,
|
||
};
|
||
}
|
||
|
||
return {
|
||
date: match[1],
|
||
trackingNumber: match[2].trim(),
|
||
status: match[3].trim(),
|
||
title: match[4].trim(),
|
||
valid: true,
|
||
};
|
||
}
|
||
|
||
// ── Revision parsing ─────────────────────────────────────────────────────
|
||
|
||
/**
|
||
* Modifier sub-regex: +LETTER DIGITS e.g. +C1, +B2, +N1, +Q1
|
||
* The draft prefix (~) may appear inside the modifier: A+~C1
|
||
*/
|
||
var MODIFIER_RE = /^\+(~?)([A-Za-z])(\d+)$/;
|
||
|
||
/**
|
||
* Parse a ZDDC revision string.
|
||
*
|
||
* Revision grammar:
|
||
* revision = ['~'] base ['+' ['~'] modifier_letter modifier_number]
|
||
* base = letter(s) | digit(s) | date(YYYY-MM-DD)
|
||
* modifier = letter + digits e.g. C1, B2, N1, Q1
|
||
*
|
||
* @param {string} revision
|
||
* @returns {{
|
||
* base: string,
|
||
* modifier: string, full modifier string e.g. '+C1', '' if none
|
||
* modifierType: string, modifier letter e.g. 'C', '' if none
|
||
* modifierNumber: number, modifier number e.g. 1, 0 if none
|
||
* modifierIsDraft: boolean,
|
||
* isDraft: boolean, true if base revision starts with ~
|
||
* full: string, original input
|
||
* }}
|
||
*/
|
||
function parseRevision(revision) {
|
||
var raw = (revision || '').toString();
|
||
|
||
// Split on '+' to separate base from optional modifier
|
||
var plusIdx = raw.indexOf('+');
|
||
var basePart = plusIdx === -1 ? raw : raw.substring(0, plusIdx);
|
||
var modifierPart = plusIdx === -1 ? '' : raw.substring(plusIdx);
|
||
|
||
// Draft flag on the base part
|
||
var isDraft = basePart.startsWith('~');
|
||
var base = isDraft ? basePart.substring(1) : basePart;
|
||
|
||
// Parse modifier
|
||
var modifier = '';
|
||
var modifierType = '';
|
||
var modifierNumber = 0;
|
||
var modifierIsDraft = false;
|
||
|
||
if (modifierPart) {
|
||
var mMatch = modifierPart.match(MODIFIER_RE);
|
||
if (mMatch) {
|
||
modifierIsDraft = mMatch[1] === '~';
|
||
modifierType = mMatch[2].toUpperCase();
|
||
modifierNumber = parseInt(mMatch[3], 10);
|
||
modifier = modifierPart;
|
||
} else {
|
||
// Unrecognised modifier — preserve as-is
|
||
modifier = modifierPart;
|
||
}
|
||
}
|
||
|
||
return {
|
||
base: base,
|
||
modifier: modifier,
|
||
modifierType: modifierType,
|
||
modifierNumber: modifierNumber,
|
||
modifierIsDraft: modifierIsDraft,
|
||
isDraft: isDraft,
|
||
full: raw,
|
||
};
|
||
}
|
||
|
||
// ── Revision comparison ──────────────────────────────────────────────────
|
||
|
||
/**
|
||
* Classify a base revision string into a sort tier:
|
||
* 0 = date (YYYY-MM-DD)
|
||
* 1 = letter(s) A, B, AA …
|
||
* 2 = number(s) 0, 1, 2, 1.5 …
|
||
* 3 = other
|
||
*/
|
||
function _baseTier(base) {
|
||
if (/^\d{4}-\d{2}-\d{2}$/.test(base)) { return 0; }
|
||
if (/^[A-Za-z]+$/.test(base)) { return 1; }
|
||
if (/^\d+(\.\d+)?$/.test(base)) { return 2; }
|
||
return 3;
|
||
}
|
||
|
||
/**
|
||
* Compare two base revision strings.
|
||
* Sort order: dates < letters < numbers < other.
|
||
*/
|
||
function _compareBase(a, b) {
|
||
var ta = _baseTier(a);
|
||
var tb = _baseTier(b);
|
||
if (ta !== tb) { return ta - tb; }
|
||
|
||
if (ta === 0) { return a < b ? -1 : a > b ? 1 : 0; } // date lexicographic = chronological
|
||
if (ta === 1) { return a.toUpperCase() < b.toUpperCase() ? -1 : a.toUpperCase() > b.toUpperCase() ? 1 : 0; }
|
||
if (ta === 2) { return parseFloat(a) - parseFloat(b); }
|
||
return a.localeCompare(b);
|
||
}
|
||
|
||
/**
|
||
* Compare two ZDDC revision strings for sort ordering.
|
||
*
|
||
* Canonical order (ascending = older → newer):
|
||
* ~A < A < A+B1 < A+C1 < A+~C2 < A+C2 < A+N1 < A+Q1
|
||
* < ~B < B < … < 0 < 1 < 2
|
||
*
|
||
* Rules:
|
||
* 1. Compare base revisions first (dates < letters < numbers).
|
||
* 2. For equal bases, draft (isDraft=true) comes before final.
|
||
* 3. For equal base+draft, no-modifier < has-modifier.
|
||
* 4. For equal base+draft+modifier presence:
|
||
* a. modifier draft comes before modifier final (modifierIsDraft).
|
||
* b. Sort modifier by type letter then by number.
|
||
*
|
||
* @param {string} a
|
||
* @param {string} b
|
||
* @returns {number} negative if a < b, 0 if equal, positive if a > b
|
||
*/
|
||
function compareRevisions(a, b) {
|
||
var pa = parseRevision(a);
|
||
var pb = parseRevision(b);
|
||
|
||
// 1. Base revision
|
||
var baseCmp = _compareBase(pa.base, pb.base);
|
||
if (baseCmp !== 0) { return baseCmp; }
|
||
|
||
// 2. Draft before final (for same base)
|
||
if (pa.isDraft !== pb.isDraft) { return pa.isDraft ? -1 : 1; }
|
||
|
||
// 3. No modifier before any modifier
|
||
var aHasMod = pa.modifier !== '';
|
||
var bHasMod = pb.modifier !== '';
|
||
if (aHasMod !== bHasMod) { return aHasMod ? 1 : -1; }
|
||
|
||
if (!aHasMod) { return 0; } // both have no modifier
|
||
|
||
// 4. Compare modifiers: type → number → draft (draft is a tie-breaker only)
|
||
// 4a. Modifier type letter (B < C < N < Q …)
|
||
if (pa.modifierType !== pb.modifierType) {
|
||
return pa.modifierType < pb.modifierType ? -1 : 1;
|
||
}
|
||
|
||
// 4b. Modifier number (1 < 2 …)
|
||
if (pa.modifierNumber !== pb.modifierNumber) {
|
||
return pa.modifierNumber - pb.modifierNumber;
|
||
}
|
||
|
||
// 4c. Draft of a modifier comes before the final modifier (same type+number)
|
||
if (pa.modifierIsDraft !== pb.modifierIsDraft) {
|
||
return pa.modifierIsDraft ? -1 : 1;
|
||
}
|
||
|
||
return 0;
|
||
}
|
||
|
||
// ── Filename / folder formatting ─────────────────────────────────────────
|
||
|
||
/**
|
||
* Build a ZDDC filename from its components.
|
||
*
|
||
* @param {{ trackingNumber: string, revision: string, status: string,
|
||
* title: string, extension: string }} parts
|
||
* @returns {string} e.g. "123456-EL-SPC-2623_A (IFR) - Specification.pdf"
|
||
*/
|
||
function formatFilename(parts) {
|
||
var tn = (parts.trackingNumber || '').trim();
|
||
var rev = (parts.revision || '').trim();
|
||
var st = (parts.status || '').trim();
|
||
var ttl = (parts.title || '').trim();
|
||
var ext = (parts.extension || '').replace(/^\./, '');
|
||
|
||
if (!tn || !rev || !st || !ttl) { return ''; }
|
||
|
||
var name = tn + '_' + rev + ' (' + st + ') - ' + ttl;
|
||
return ext ? name + '.' + ext : name;
|
||
}
|
||
|
||
/**
|
||
* Build a ZDDC transmittal folder name from its components.
|
||
*
|
||
* @param {{ date: string, trackingNumber: string, status: string,
|
||
* title: string }} parts
|
||
* @returns {string} e.g. "2025-10-31_123456-EM-SUB-0001 (IFR) - Title"
|
||
*/
|
||
function formatFolder(parts) {
|
||
var dt = (parts.date || '').trim();
|
||
var tn = (parts.trackingNumber || '').trim();
|
||
var st = (parts.status || '').trim();
|
||
var ttl = (parts.title || '').trim();
|
||
|
||
if (!dt || !tn || !st || !ttl) { return ''; }
|
||
|
||
return dt + '_' + tn + ' (' + st + ') - ' + ttl;
|
||
}
|
||
|
||
// ── Filename / extension splitting ───────────────────────────────────────
|
||
|
||
/**
|
||
* Split a filename into its base name and extension (no leading dot).
|
||
* Treats leading dot ('.gitignore') as no extension.
|
||
*
|
||
* @param {string} filename
|
||
* @returns {{ name: string, extension: string }}
|
||
*/
|
||
function splitExtension(filename) {
|
||
if (!filename) { return { name: '', extension: '' }; }
|
||
var lastDot = filename.lastIndexOf('.');
|
||
if (lastDot <= 0) { return { name: filename, extension: '' }; }
|
||
return {
|
||
name: filename.substring(0, lastDot),
|
||
extension: filename.substring(lastDot + 1).toLowerCase(),
|
||
};
|
||
}
|
||
|
||
/**
|
||
* Join a base name and extension. Tolerant of either form ('pdf' or '.pdf').
|
||
* Returns just the name when extension is empty.
|
||
*/
|
||
function joinExtension(name, extension) {
|
||
var ext = (extension || '').replace(/^\./, '');
|
||
return ext ? name + '.' + ext : name;
|
||
}
|
||
|
||
// ── Public API ───────────────────────────────────────────────────────────
|
||
|
||
root.zddc = {
|
||
STATUSES: STATUSES,
|
||
isValidStatus: isValidStatus,
|
||
parseFilename: parseFilename,
|
||
parseFolder: parseFolder,
|
||
parseRevision: parseRevision,
|
||
formatFilename: formatFilename,
|
||
formatFolder: formatFolder,
|
||
compareRevisions: compareRevisions,
|
||
splitExtension: splitExtension,
|
||
joinExtension: joinExtension,
|
||
};
|
||
|
||
}(typeof window !== 'undefined' ? window : this));
|
||
|
||
(function() {
|
||
'use strict';
|
||
|
||
// Escape a string for use in a RegExp (literal match)
|
||
function escapeRegex(str) {
|
||
return str.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
|
||
}
|
||
|
||
// Build regex pattern at parse time based on anchors
|
||
function compilePattern(raw, anchorStart, anchorEnd) {
|
||
var src = (anchorStart ? '^' : '') + raw + (anchorEnd ? '$' : '');
|
||
try {
|
||
return new RegExp(src, 'i');
|
||
} catch (e) {
|
||
// Invalid regex — escape and retry (always succeeds)
|
||
var safe = (anchorStart ? '^' : '') + escapeRegex(raw) + (anchorEnd ? '$' : '');
|
||
return new RegExp(safe, 'i');
|
||
}
|
||
}
|
||
|
||
// Parse a single token string into a node
|
||
function parseToken(token) {
|
||
var s = token;
|
||
var negate = false;
|
||
var anchorStart = false;
|
||
var anchorEnd = false;
|
||
|
||
if (s.charAt(0) === '!') {
|
||
negate = true;
|
||
s = s.slice(1);
|
||
}
|
||
if (s.charAt(0) === '^') {
|
||
anchorStart = true;
|
||
s = s.slice(1);
|
||
}
|
||
if (s.length > 0 && s.charAt(s.length - 1) === '$') {
|
||
anchorEnd = true;
|
||
s = s.slice(0, -1);
|
||
}
|
||
|
||
if (s === '') return null;
|
||
|
||
// bare * (possibly after stripping !) → wildcard-all or wildcard-none
|
||
if (s === '*' && !anchorStart && !anchorEnd) {
|
||
return negate ? null : { type: 'wildcard-all' };
|
||
}
|
||
|
||
var re = compilePattern(s, anchorStart, anchorEnd);
|
||
return { type: negate ? 'no-match' : 'match', re: re };
|
||
}
|
||
|
||
// Parse expression string into AST array
|
||
function parse(expression) {
|
||
if (!expression || typeof expression !== 'string') return [];
|
||
var trimmed = expression.trim();
|
||
if (trimmed === '') return [];
|
||
if (trimmed === '*') return [{ type: 'wildcard-all' }];
|
||
|
||
var ast = [];
|
||
var i = 0;
|
||
var len = trimmed.length;
|
||
|
||
while (i < len) {
|
||
var ch = trimmed.charAt(i);
|
||
|
||
if (ch === '(') {
|
||
var depth = 1;
|
||
var j = i + 1;
|
||
while (j < len && depth > 0) {
|
||
if (trimmed.charAt(j) === '(') depth++;
|
||
else if (trimmed.charAt(j) === ')') depth--;
|
||
j++;
|
||
}
|
||
var innerAst = parse(trimmed.slice(i + 1, j - 1));
|
||
if (innerAst.length === 1) {
|
||
ast.push(innerAst[0]);
|
||
} else if (innerAst.length > 1) {
|
||
for (var k = 0; k < innerAst.length; k++) ast.push(innerAst[k]);
|
||
}
|
||
i = j;
|
||
} else if (ch === '|') {
|
||
ast.push({ type: 'pipe' });
|
||
i++;
|
||
} else if (ch === ' ') {
|
||
i++;
|
||
} else {
|
||
var j = i;
|
||
while (j < len) {
|
||
var c = trimmed.charAt(j);
|
||
if (c === ' ' || c === '(' || c === '|' || c === ')') break;
|
||
j++;
|
||
}
|
||
var token = trimmed.slice(i, j);
|
||
if (token.length > 0) {
|
||
var node = parseToken(token);
|
||
if (node !== null) ast.push(node);
|
||
}
|
||
i = j;
|
||
}
|
||
}
|
||
|
||
// Group pipes into OR nodes
|
||
var hasPipe = false;
|
||
var branches = [[]];
|
||
for (var l = 0; l < ast.length; l++) {
|
||
if (ast[l].type === 'pipe') {
|
||
hasPipe = true;
|
||
branches.push([]);
|
||
} else {
|
||
branches[branches.length - 1].push(ast[l]);
|
||
}
|
||
}
|
||
branches = branches.filter(function(b) { return b.length > 0; });
|
||
|
||
if (!hasPipe) {
|
||
return ast.filter(function(n) { return n.type !== 'pipe'; });
|
||
}
|
||
|
||
var orNodes = branches.map(function(branch) {
|
||
if (branch.length === 1) return branch[0];
|
||
return { type: 'and', nodes: branch };
|
||
});
|
||
return [{ type: 'or', nodes: orNodes }];
|
||
}
|
||
|
||
// Check if a single node matches the value
|
||
function nodeMatches(node, value) {
|
||
switch (node.type) {
|
||
case 'wildcard-all': return true;
|
||
case 'match': return node.re.test(value);
|
||
case 'no-match': return !node.re.test(value);
|
||
case 'or':
|
||
for (var i = 0; i < node.nodes.length; i++) {
|
||
if (nodeMatches(node.nodes[i], value)) return true;
|
||
}
|
||
return false;
|
||
case 'and':
|
||
for (var i = 0; i < node.nodes.length; i++) {
|
||
if (!nodeMatches(node.nodes[i], value)) return false;
|
||
}
|
||
return true;
|
||
default: return false;
|
||
}
|
||
}
|
||
|
||
// Evaluate AST against value
|
||
function matches(value, ast) {
|
||
if (!ast || ast.length === 0) return true;
|
||
var v = String(value); // no forced lowercase — regex has 'i' flag
|
||
for (var i = 0; i < ast.length; i++) {
|
||
if (!nodeMatches(ast[i], v)) return false;
|
||
}
|
||
return true;
|
||
}
|
||
|
||
if (!window.zddc) {
|
||
throw new Error('shared/zddc-filter.js: window.zddc must be loaded first');
|
||
}
|
||
window.zddc.filter = { parse: parse, matches: matches };
|
||
})();
|
||
|
||
/**
|
||
* ZDDC shared theme toggle — light / dark / auto.
|
||
* Persists choice to localStorage under 'zddc-theme'.
|
||
* Works with all four tools regardless of their module pattern.
|
||
* Expects: #theme-btn in the DOM (optional — skips gracefully if absent).
|
||
*
|
||
* Theme cycle: auto → light → dark → auto …
|
||
* 'auto' honours the OS prefers-color-scheme media query (CSS handles it).
|
||
* 'light' sets data-theme="light" on <html> (overrides dark media query).
|
||
* 'dark' sets data-theme="dark" on <html>.
|
||
*/
|
||
(function () {
|
||
'use strict';
|
||
|
||
var STORAGE_KEY = 'zddc-theme';
|
||
var THEMES = ['auto', 'light', 'dark'];
|
||
|
||
var LABELS = {
|
||
auto: '◐',
|
||
light: '☀',
|
||
dark: '☾'
|
||
};
|
||
|
||
var TITLES = {
|
||
auto: 'Theme: auto (follows OS)',
|
||
light: 'Theme: light',
|
||
dark: 'Theme: dark'
|
||
};
|
||
|
||
function load() {
|
||
var stored = localStorage.getItem(STORAGE_KEY);
|
||
return THEMES.indexOf(stored) !== -1 ? stored : 'auto';
|
||
}
|
||
|
||
function apply(theme) {
|
||
if (theme === 'dark') {
|
||
document.documentElement.setAttribute('data-theme', 'dark');
|
||
} else if (theme === 'light') {
|
||
document.documentElement.setAttribute('data-theme', 'light');
|
||
} else {
|
||
document.documentElement.removeAttribute('data-theme');
|
||
}
|
||
}
|
||
|
||
function save(theme) {
|
||
try { localStorage.setItem(STORAGE_KEY, theme); } catch (e) {}
|
||
}
|
||
|
||
function updateButton(btn, theme) {
|
||
btn.textContent = LABELS[theme];
|
||
btn.title = TITLES[theme];
|
||
btn.setAttribute('aria-label', TITLES[theme]);
|
||
}
|
||
|
||
function next(theme) {
|
||
return THEMES[(THEMES.indexOf(theme) + 1) % THEMES.length];
|
||
}
|
||
|
||
function init() {
|
||
var current = load();
|
||
apply(current);
|
||
|
||
var btn = document.getElementById('theme-btn');
|
||
if (!btn) { return; }
|
||
|
||
updateButton(btn, current);
|
||
|
||
btn.addEventListener('click', function () {
|
||
current = next(current);
|
||
apply(current);
|
||
save(current);
|
||
updateButton(btn, current);
|
||
});
|
||
}
|
||
|
||
/* Apply theme immediately (before DOM ready) to avoid flash */
|
||
apply(load());
|
||
|
||
if (document.readyState === 'loading') {
|
||
document.addEventListener('DOMContentLoaded', init);
|
||
} else {
|
||
init();
|
||
}
|
||
}());
|
||
|
||
// shared/toast.js — non-blocking notification helper available to every
|
||
// tool via window.zddc.toast(msg, level, opts). Originated as classifier's
|
||
// local showToast (classifier/js/excel.js); promoted here so tools that
|
||
// today use alert() or silent console.error can switch to a uniform
|
||
// non-blocking surface.
|
||
//
|
||
// Usage:
|
||
// window.zddc.toast('Saved.', 'success');
|
||
// window.zddc.toast('Could not load: ' + err.message, 'error');
|
||
// window.zddc.toast('Note', 'info', { durationMs: 3000 });
|
||
//
|
||
// Levels: 'info' (default) | 'success' | 'warning' | 'error'.
|
||
// Each tool may also expose app.notify(msg, level) as a thin wrapper —
|
||
// see ARCHITECTURE.md for the convention.
|
||
(function () {
|
||
'use strict';
|
||
|
||
if (!window.zddc) window.zddc = {};
|
||
// Don't overwrite if a tool defined its own first.
|
||
if (typeof window.zddc.toast === 'function') return;
|
||
|
||
var DEFAULT_DURATION_MS = 5000;
|
||
var FADE_MS = 300;
|
||
|
||
function toast(message, level, opts) {
|
||
opts = opts || {};
|
||
var lvl = (level === 'success' || level === 'error' ||
|
||
level === 'warning') ? level : 'info';
|
||
|
||
// Single-toast policy: dismiss any existing toast immediately
|
||
// so the new one is always the most recent. Matches the
|
||
// classifier's prior behavior and avoids stack-of-toasts UX.
|
||
var existing = document.querySelector('.zddc-toast');
|
||
if (existing) existing.remove();
|
||
|
||
var el = document.createElement('div');
|
||
el.className = 'zddc-toast zddc-toast--' + lvl;
|
||
// ARIA: errors get assertive (interrupts SR queue), others polite.
|
||
el.setAttribute('role', lvl === 'error' ? 'alert' : 'status');
|
||
el.setAttribute('aria-live', lvl === 'error' ? 'assertive' : 'polite');
|
||
el.textContent = message == null ? '' : String(message);
|
||
document.body.appendChild(el);
|
||
|
||
var dur = typeof opts.durationMs === 'number' ?
|
||
opts.durationMs : DEFAULT_DURATION_MS;
|
||
var timer = setTimeout(function () {
|
||
el.classList.add('zddc-toast--fade');
|
||
setTimeout(function () {
|
||
if (el.parentNode) el.parentNode.removeChild(el);
|
||
}, FADE_MS);
|
||
}, dur);
|
||
|
||
// Click-to-dismiss. Useful for sticky errors the user wants gone.
|
||
el.addEventListener('click', function () {
|
||
clearTimeout(timer);
|
||
if (el.parentNode) el.parentNode.removeChild(el);
|
||
});
|
||
|
||
return el;
|
||
}
|
||
|
||
window.zddc.toast = toast;
|
||
|
||
// Route window.alert() calls into the toast helper. Every tool has
|
||
// accumulated some `alert(...)` sites for error reporting; rather
|
||
// than touch each one, intercept globally so they're non-blocking
|
||
// and ARIA-announced consistently. Native alert is preserved on
|
||
// window.alertNative for the rare case where a truly modal block
|
||
// is needed (e.g. before navigating away with unsaved changes).
|
||
if (typeof window.alert === 'function' && !window.alertNative) {
|
||
window.alertNative = window.alert.bind(window);
|
||
window.alert = function (msg) {
|
||
toast(String(msg == null ? '' : msg), 'error');
|
||
};
|
||
}
|
||
})();
|
||
|
||
// shared/logo.js — turn the inert <svg class="app-header__logo"> on
|
||
// every tool's header into a clickable link. The destination is the
|
||
// nearest "home" the user can sensibly back out to:
|
||
//
|
||
// file:// → no wrap (no server home)
|
||
// http(s)://host/ → wrap, href = /
|
||
// http(s)://host/<tool>.html (deployment root)→ wrap, href = /
|
||
// http(s)://host/<project>/... → wrap, href = /<project>
|
||
//
|
||
// When inside a project, the logo takes the user to the project
|
||
// landing (synthetic page with the four lifecycle-stage cards + MDL
|
||
// instructions). When at the deployment root, the logo points at /
|
||
// (the project picker). Offline, the logo stays decorative — there's
|
||
// no real "home" to go to.
|
||
//
|
||
// Mounts as a sibling-replacement on DOMContentLoaded: wraps the
|
||
// existing logo SVG in an <a>, preserving classes and attributes.
|
||
// Idempotent: re-mounting on an already-wrapped logo is a no-op.
|
||
//
|
||
// Tools that want to override (e.g. a deployment that pins logo to
|
||
// an external URL) can set window.zddc.logo.disabled = true before
|
||
// DOMContentLoaded and inject their own anchor.
|
||
(function () {
|
||
'use strict';
|
||
|
||
if (!window.zddc) window.zddc = {};
|
||
if (window.zddc.logo) return;
|
||
|
||
function projectSegment(pathname) {
|
||
var parts = pathname.split('/').filter(Boolean);
|
||
if (parts.length === 0) return null;
|
||
var first = parts[0];
|
||
// Tool HTMLs at the deployment root (index.html, archive.html
|
||
// with ?projects=...) don't carry a project segment.
|
||
if (first.indexOf('.') !== -1) return null;
|
||
return first;
|
||
}
|
||
|
||
function targetHref() {
|
||
if (typeof location === 'undefined') return null;
|
||
if (location.protocol !== 'http:' && location.protocol !== 'https:') {
|
||
return null;
|
||
}
|
||
if (window.zddc.logo && window.zddc.logo.disabled) return null;
|
||
var seg = projectSegment(location.pathname);
|
||
return seg ? '/' + encodeURIComponent(seg) : '/';
|
||
}
|
||
|
||
function mount() {
|
||
var logo = document.querySelector('.app-header__logo');
|
||
if (!logo) return;
|
||
// Already wrapped (template-supplied anchor, or a previous mount).
|
||
if (logo.parentElement && logo.parentElement.tagName === 'A' &&
|
||
logo.parentElement.classList.contains('app-header__logo-link')) {
|
||
return;
|
||
}
|
||
var href = targetHref();
|
||
if (!href) return;
|
||
var a = document.createElement('a');
|
||
a.href = href;
|
||
a.className = 'app-header__logo-link';
|
||
var label = href === '/' ? 'ZDDC home' : 'Project home';
|
||
a.title = label;
|
||
a.setAttribute('aria-label', label);
|
||
logo.parentNode.insertBefore(a, logo);
|
||
a.appendChild(logo);
|
||
}
|
||
|
||
window.zddc.logo = {
|
||
mount: mount,
|
||
// Test seam.
|
||
_projectSegment: projectSegment,
|
||
_targetHref: targetHref,
|
||
disabled: false,
|
||
};
|
||
|
||
if (document.readyState === 'loading') {
|
||
document.addEventListener('DOMContentLoaded', mount, { once: true });
|
||
} else {
|
||
mount();
|
||
}
|
||
})();
|
||
|
||
/**
|
||
* 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();
|
||
}
|
||
}());
|
||
|
||
// shared/elevation.js — admin elevation toggle.
|
||
//
|
||
// Sudo-style model: admins behave as normal users by default; clicking
|
||
// the header toggle elevates the session so admin escape hatches (WORM
|
||
// bypass, .zddc edit authority, profile admin scaffolds) start firing.
|
||
// State is carried in a `zddc-elevate=1` cookie that the server reads
|
||
// via handler.ACLMiddleware → zddc.Principal{Elevated}.
|
||
//
|
||
// Only renders the toggle when /.profile/access reports the caller has
|
||
// some admin scope — a non-admin sees nothing, which keeps the chrome
|
||
// quiet for the common case. The toggle fades in once access loads so
|
||
// non-admins never even see the affordance flash.
|
||
//
|
||
// Click flow: set/clear the cookie, then reload the page so the server
|
||
// sees the new state on the next render. The reload is intentional —
|
||
// admin scaffolds in tool HTML are server-rendered for some tools, so
|
||
// a soft state flip on the client alone wouldn't reach those.
|
||
(function () {
|
||
'use strict';
|
||
|
||
if (!window.zddc) window.zddc = {};
|
||
if (window.zddc.elevation) return;
|
||
|
||
var COOKIE_NAME = 'zddc-elevate';
|
||
|
||
function isElevated() {
|
||
var parts = document.cookie.split(';');
|
||
for (var i = 0; i < parts.length; i++) {
|
||
var kv = parts[i].trim().split('=');
|
||
if (kv[0] === COOKIE_NAME && kv[1] === '1') return true;
|
||
}
|
||
return false;
|
||
}
|
||
|
||
function setElevated(on) {
|
||
if (on) {
|
||
// SameSite=Lax blocks cross-site form-post / image-tag CSRF
|
||
// shapes. Max-Age caps the elevation window so a forgotten
|
||
// tab doesn't leave admin powers active indefinitely (sudo's
|
||
// 5-minute precedent informs the number — 30 minutes is a
|
||
// reasonable trade between annoyance and exposure).
|
||
document.cookie = COOKIE_NAME + '=1; Path=/; SameSite=Lax; Max-Age=1800';
|
||
} else {
|
||
document.cookie = COOKIE_NAME + '=; Path=/; SameSite=Lax; Max-Age=0';
|
||
}
|
||
}
|
||
|
||
async function fetchAccess() {
|
||
try {
|
||
var resp = await fetch('/.profile/access', {
|
||
headers: { 'Accept': 'application/json' },
|
||
credentials: 'same-origin',
|
||
cache: 'no-cache'
|
||
});
|
||
if (!resp.ok) return null;
|
||
return await resp.json();
|
||
} catch (_e) {
|
||
return null;
|
||
}
|
||
}
|
||
|
||
function render(host, elevated) {
|
||
host.classList.remove('hidden');
|
||
host.innerHTML =
|
||
'<input type="checkbox" id="elevation-checkbox"'
|
||
+ (elevated ? ' checked' : '') + '>'
|
||
+ '<label for="elevation-checkbox" class="elevation-toggle__label">'
|
||
+ 'Admin</label>';
|
||
var cb = host.querySelector('#elevation-checkbox');
|
||
cb.addEventListener('change', function () {
|
||
setElevated(cb.checked);
|
||
// Hard reload so server-rendered admin surfaces (profile
|
||
// page scaffolds, hidden-entry listings) catch up. URL
|
||
// and scroll state are preserved by the browser's normal
|
||
// back-forward cache rules.
|
||
window.location.reload();
|
||
});
|
||
}
|
||
|
||
// Page-wide affordances when elevation is active. The toggle alone
|
||
// is easy to miss — admin mode silently bypasses WORM and ACL
|
||
// restrictions, which produces surprising "I shouldn't have been
|
||
// able to do that" moments. A body class + a sticky banner with a
|
||
// one-click disable make the armed state unmistakable.
|
||
function applyArmedChrome(elevated) {
|
||
var b = document.body;
|
||
if (!b) return;
|
||
if (elevated) b.classList.add('is-elevated');
|
||
else b.classList.remove('is-elevated');
|
||
|
||
var banner = document.getElementById('elevation-banner');
|
||
if (elevated) {
|
||
if (!banner) {
|
||
banner = document.createElement('div');
|
||
banner.id = 'elevation-banner';
|
||
banner.className = 'elevation-banner';
|
||
banner.setAttribute('role', 'alert');
|
||
banner.innerHTML =
|
||
'<span class="elevation-banner__dot" aria-hidden="true"></span>'
|
||
+ '<span class="elevation-banner__msg">'
|
||
+ 'Admin mode is on — write access bypasses WORM and ACL safeguards.'
|
||
+ '</span>'
|
||
+ '<button type="button" class="elevation-banner__off" id="elevation-banner-off">'
|
||
+ 'Drop admin'
|
||
+ '</button>';
|
||
document.body.insertBefore(banner, document.body.firstChild);
|
||
var off = banner.querySelector('#elevation-banner-off');
|
||
if (off) off.addEventListener('click', function () {
|
||
setElevated(false);
|
||
window.location.reload();
|
||
});
|
||
}
|
||
} else if (banner) {
|
||
banner.parentNode.removeChild(banner);
|
||
}
|
||
}
|
||
|
||
async function init() {
|
||
// Body chrome applies on every page load whether or not the
|
||
// header has a toggle slot — the banner needs to surface in
|
||
// tools / pages that don't host the toggle (e.g. iframed
|
||
// classifier inside browse's grid mode), so the user can't
|
||
// accidentally write through an elevated context elsewhere.
|
||
applyArmedChrome(isElevated());
|
||
|
||
var host = document.getElementById('elevation-toggle');
|
||
if (!host) return; // tool doesn't include the slot yet — no-op
|
||
var access = await fetchAccess();
|
||
if (!access) return; // anonymous / endpoint missing — no-op
|
||
// Surface ONLY for users who have admin authority somewhere.
|
||
// /.profile/access ships `can_elevate` as an elevation-
|
||
// INDEPENDENT signal — true for any user named in any admin
|
||
// list, regardless of current cookie state. The other flags
|
||
// (is_super_admin, has_any_admin_scope) reflect EFFECTIVE
|
||
// authority and would be false for an un-elevated admin
|
||
// who hasn't toggled yet — so we can't gate on those.
|
||
if (!access.can_elevate) return;
|
||
render(host, isElevated());
|
||
}
|
||
|
||
if (document.readyState === 'loading') {
|
||
document.addEventListener('DOMContentLoaded', init);
|
||
} else {
|
||
init();
|
||
}
|
||
|
||
window.zddc.elevation = { isElevated: isElevated, setElevated: setElevated };
|
||
})();
|
||
|
||
(function() {
|
||
'use strict';
|
||
// ZDDC landing page — project picker.
|
||
//
|
||
// Two stacked sections:
|
||
// 1. Groups (saved bundles of projects) — click row to open, edit, or delete
|
||
// 2. Projects (live list from the server) — click row to open one project directly
|
||
//
|
||
// "Select-mode" (entered via "+ New group" or a group's edit ✏ button) shows
|
||
// checkboxes on each project row, a name input, and an action bar with
|
||
// Save / Open visible-checked / Cancel. In default (rest) mode there are no
|
||
// checkboxes; clicking anything just opens the archive.
|
||
//
|
||
// Storage: groups persist in localStorage under `zddc_landing_groups` as
|
||
// an array of { name: string, projects: string[] }. Old `zddc_landing_presets`
|
||
// entries are migrated once on init (project list only — filter/sort state
|
||
// from the old preset model is dropped).
|
||
|
||
// ── State ────────────────────────────────────────────────────────────────
|
||
|
||
var allProjects = []; // [{name, title, url}] from server
|
||
var groups = []; // [{name, projects: [name,...]}] from localStorage
|
||
var selected = new Set(); // checked project names (only used in select-mode)
|
||
var columnFilters = { pn: '', pt: '' };
|
||
var columnFilterASTs = { pn: null, pt: null };
|
||
var sortField = 'name'; // 'name' | 'title'
|
||
var sortDirection = 'asc';
|
||
var loadError = null; // user-facing error string
|
||
var loadErrorKind = null; // 'static' | 'auth' | 'non-json' | 'network'
|
||
|
||
// selectMode === null → default mode (click to open)
|
||
// selectMode === { kind: 'create' }
|
||
// selectMode === { kind: 'edit', originalName: '...' }
|
||
var selectMode = null;
|
||
|
||
var GROUPS_KEY = 'zddc_landing_groups';
|
||
var LEGACY_PRESETS_KEY = 'zddc_landing_presets';
|
||
var DEFAULT_SORT_FIELD = 'name';
|
||
var DEFAULT_SORT_DIRECTION = 'asc';
|
||
|
||
// ── URL state ────────────────────────────────────────────────────────────
|
||
// Only filters and sort persist in the URL. Selection (`?projects=`) used
|
||
// to live here for save-as-preset workflows; with click-to-open + named
|
||
// groups it adds noise and isn't shareable in any useful way (groups are
|
||
// localStorage-only per user).
|
||
|
||
function urlSerialize() {
|
||
var p = new URLSearchParams();
|
||
if (columnFilters.pn) p.set('pn', columnFilters.pn);
|
||
if (columnFilters.pt) p.set('pt', columnFilters.pt);
|
||
if (sortField !== DEFAULT_SORT_FIELD) p.set('sort', sortField);
|
||
if (sortDirection !== DEFAULT_SORT_DIRECTION) p.set('dir', sortDirection);
|
||
// Preserve channel selector from existing URL if present.
|
||
var v = new URLSearchParams(location.search).get('v');
|
||
if (v) p.set('v', v);
|
||
var qs = p.toString();
|
||
return qs ? '?' + qs : '';
|
||
}
|
||
|
||
function urlPush() {
|
||
var qs = urlSerialize();
|
||
if (qs === location.search) return;
|
||
try {
|
||
history.replaceState(null, '', location.pathname + qs);
|
||
} catch (e) { /* file:// protocol restrictions */ }
|
||
}
|
||
|
||
function urlRestore() {
|
||
var p = new URLSearchParams(location.search);
|
||
if (p.has('pn')) {
|
||
columnFilters.pn = p.get('pn');
|
||
columnFilterASTs.pn = parseFilterAST(columnFilters.pn);
|
||
}
|
||
if (p.has('pt')) {
|
||
columnFilters.pt = p.get('pt');
|
||
columnFilterASTs.pt = parseFilterAST(columnFilters.pt);
|
||
}
|
||
if (p.has('sort')) {
|
||
var s = p.get('sort');
|
||
if (s === 'name' || s === 'title') sortField = s;
|
||
}
|
||
if (p.has('dir')) {
|
||
var d = p.get('dir');
|
||
if (d === 'asc' || d === 'desc') sortDirection = d;
|
||
}
|
||
}
|
||
|
||
function parseFilterAST(text) {
|
||
if (!text) return null;
|
||
try { return zddc.filter.parse(text); } catch (e) { return null; }
|
||
}
|
||
|
||
// ── Server fetch ─────────────────────────────────────────────────────────
|
||
|
||
async function fetchProjects() {
|
||
var base = location.origin + location.pathname.replace(/\/[^\/]*$/, '/');
|
||
try {
|
||
var resp = await fetch(base, {
|
||
headers: { 'Accept': 'application/json' },
|
||
cache: 'no-cache',
|
||
credentials: 'same-origin'
|
||
});
|
||
if (!resp.ok) throw new Error('HTTP ' + resp.status);
|
||
|
||
var ctype = resp.headers.get('Content-Type') || '';
|
||
var body = await resp.text();
|
||
var trimmed = body.trim();
|
||
var looksLikeJson = trimmed.startsWith('[') || trimmed.startsWith('{');
|
||
if (!ctype.toLowerCase().includes('json') && !looksLikeJson) {
|
||
console.warn('Project-list endpoint returned non-JSON', {
|
||
requested: base,
|
||
finalUrl: resp.url,
|
||
redirected: resp.redirected,
|
||
contentType: ctype,
|
||
bodyStart: trimmed.slice(0, 200)
|
||
});
|
||
if (resp.redirected) {
|
||
loadErrorKind = 'auth';
|
||
throw new Error("The request was redirected to " + resp.url + ' — likely to an auth/login page. Sign in and reload.');
|
||
}
|
||
if (/<title>\s*Loading\s+ZDDC/i.test(trimmed) || /<title>\s*Loading\s+Archive/i.test(trimmed)) {
|
||
loadErrorKind = 'static';
|
||
throw new Error("This deployment doesn't expose a project list. The server is serving static stubs without a zddc-server backend.");
|
||
}
|
||
loadErrorKind = 'non-json';
|
||
throw new Error("The server at " + base + " returned HTML where a JSON project list was expected. Its zddc-server may be too old (no Accept: application/json dispatch on /), a reverse proxy is stripping the header, or the static site at the root has shadowed the API endpoint.");
|
||
}
|
||
|
||
var data = JSON.parse(body);
|
||
if (!Array.isArray(data)) throw new Error('Expected a JSON array of projects, got ' + typeof data);
|
||
// The root JSON is now a generic listing.FileInfo[] (same
|
||
// shape every other directory returns). Filter to
|
||
// directories (projects are folders), strip the trailing
|
||
// "/" the server adds to dir names, and pick up `title`
|
||
// (the per-project .zddc title:, populated by the
|
||
// server-side listing pipeline).
|
||
allProjects = data
|
||
.filter(function (p) { return p && p.is_dir; })
|
||
.map(function (p) {
|
||
var raw = String(p.name || '').replace(/\/$/, '');
|
||
return {
|
||
name: raw,
|
||
title: String(p.title || ''),
|
||
url: String(p.url || '')
|
||
};
|
||
})
|
||
.filter(function (p) {
|
||
if (!p.name) return false;
|
||
var c = p.name.charAt(0);
|
||
return c !== '.' && c !== '_';
|
||
});
|
||
return true;
|
||
} catch (e) {
|
||
loadError = e.message || String(e);
|
||
return false;
|
||
}
|
||
}
|
||
|
||
// ── Filter / sort ────────────────────────────────────────────────────────
|
||
|
||
function visibleProjects() {
|
||
var rows = allProjects.slice();
|
||
if (columnFilterASTs.pn && columnFilterASTs.pn.length > 0) {
|
||
rows = rows.filter(function(r) { return zddc.filter.matches(r.name, columnFilterASTs.pn); });
|
||
}
|
||
if (columnFilterASTs.pt && columnFilterASTs.pt.length > 0) {
|
||
rows = rows.filter(function(r) { return zddc.filter.matches(r.title || '', columnFilterASTs.pt); });
|
||
}
|
||
rows.sort(function(a, b) {
|
||
var av = (a[sortField] || '').toString();
|
||
var bv = (b[sortField] || '').toString();
|
||
if (sortField === 'title') {
|
||
if (!av && bv) return 1;
|
||
if (av && !bv) return -1;
|
||
}
|
||
var cmp = av.localeCompare(bv, undefined, { numeric: true, sensitivity: 'base' });
|
||
return cmp * (sortDirection === 'desc' ? -1 : 1);
|
||
});
|
||
return rows;
|
||
}
|
||
|
||
// ── Rendering ────────────────────────────────────────────────────────────
|
||
|
||
function render() {
|
||
renderActionBar();
|
||
renderGroups();
|
||
renderProjects();
|
||
renderProjectCount();
|
||
renderGroupCount();
|
||
}
|
||
|
||
function renderActionBar() {
|
||
var bar = document.getElementById('selectActionBar');
|
||
var title = document.getElementById('selectModeTitle');
|
||
var input = document.getElementById('groupNameInput');
|
||
var newBtn = document.getElementById('newGroupBtn');
|
||
if (!bar) return;
|
||
|
||
if (!selectMode) {
|
||
bar.classList.add('hidden');
|
||
if (newBtn) newBtn.disabled = false;
|
||
return;
|
||
}
|
||
|
||
bar.classList.remove('hidden');
|
||
if (newBtn) newBtn.disabled = true;
|
||
|
||
if (selectMode.kind === 'create') {
|
||
title.textContent = 'New group:';
|
||
if (document.activeElement !== input) input.value = '';
|
||
} else {
|
||
title.textContent = 'Editing:';
|
||
if (document.activeElement !== input) input.value = selectMode.originalName || '';
|
||
}
|
||
}
|
||
|
||
function renderGroups() {
|
||
var container = document.getElementById('groupsContainer');
|
||
if (!container) return;
|
||
if (groups.length === 0) {
|
||
container.innerHTML = '<div class="groups-empty">No saved groups yet. Use <strong>+ New group</strong> to bundle a set of projects.</div>';
|
||
return;
|
||
}
|
||
var html = '<table class="groups-table">';
|
||
html += '<tbody>';
|
||
for (var i = 0; i < groups.length; i++) {
|
||
var g = groups[i];
|
||
var n = escapeHtml(g.name);
|
||
var count = g.projects.length;
|
||
html += '<tr class="groups-row" data-name="' + n + '" onclick="LandingApp.openGroup(event)">';
|
||
html += '<td class="groups-row-name">' + n + '</td>';
|
||
html += '<td class="groups-row-count">' + count + ' project' + (count === 1 ? '' : 's') + '</td>';
|
||
html += '<td class="groups-row-actions">';
|
||
html += '<button class="groups-btn-edit" data-name="' + n + '" onclick="event.stopPropagation(); LandingApp.startEditGroup(event)" title="Edit group">✎</button>';
|
||
html += '<button class="groups-btn-delete" data-name="' + n + '" onclick="event.stopPropagation(); LandingApp.deleteGroup(event)" title="Delete group">×</button>';
|
||
html += '</td>';
|
||
html += '</tr>';
|
||
}
|
||
html += '</tbody></table>';
|
||
container.innerHTML = html;
|
||
}
|
||
|
||
function renderProjects() {
|
||
var container = document.getElementById('projectListContainer');
|
||
if (loadError) {
|
||
var heading, help;
|
||
if (loadErrorKind === 'static') {
|
||
heading = 'This server doesn\'t list projects';
|
||
help = 'You\'re on a static deployment (Caddy serving stubs) — there\'s no zddc-server backend here to enumerate projects. '
|
||
+ 'Open a project directly via its URL (e.g. <code>/<project>/Archive/</code>), or ask whoever sent you this link for the project URL they meant.';
|
||
} else if (loadErrorKind === 'auth') {
|
||
heading = 'Sign-in required';
|
||
help = 'The server bounced this request to an auth page. Sign in there, then reload this URL.';
|
||
} else {
|
||
heading = 'Couldn\'t load the project list';
|
||
help = 'Reload the page to try again. If this keeps happening, the server may be down or your link may be stale.';
|
||
}
|
||
container.innerHTML =
|
||
'<div class="project-list-empty">'
|
||
+ '<h3>' + escapeHtml(heading) + '</h3>'
|
||
+ '<p>' + escapeHtml(loadError) + '</p>'
|
||
+ '<p class="landing-empty-help">' + help + '</p>'
|
||
+ '</div>';
|
||
return;
|
||
}
|
||
if (allProjects.length === 0) {
|
||
container.innerHTML =
|
||
'<div class="project-list-empty">'
|
||
+ '<h3>No projects to show</h3>'
|
||
+ '<p>Either you don\'t have access to any projects on this server yet, or none have been set up.</p>'
|
||
+ '<p class="landing-empty-help">If someone shared this link with you, ask them which project administrator can grant your account access — and double-check that you\'re signed in with the same email they expected.</p>'
|
||
+ '</div>';
|
||
return;
|
||
}
|
||
|
||
var rows = visibleProjects();
|
||
var anyTitles = allProjects.some(function(p) { return p.title; });
|
||
var inSelect = !!selectMode;
|
||
var visibleSelected = inSelect ? rows.filter(function(r) { return selected.has(r.name); }).length : 0;
|
||
var headerCheckedState = !inSelect ? 'unchecked'
|
||
: visibleSelected === 0 ? 'unchecked'
|
||
: visibleSelected === rows.length ? 'checked' : 'indeterminate';
|
||
|
||
var html = '<table class="project-table' + (inSelect ? ' is-select-mode' : '') + '">';
|
||
html += '<thead>';
|
||
html += '<tr class="project-table-headers">';
|
||
if (inSelect) {
|
||
html += '<th class="project-table-checkbox-col">'
|
||
+ '<input type="checkbox" id="headerCheckbox" '
|
||
+ (headerCheckedState === 'checked' ? 'checked ' : '')
|
||
+ 'onclick="LandingApp.toggleHeaderCheckbox()" '
|
||
+ 'title="Check / uncheck all visible projects">'
|
||
+ '</th>';
|
||
}
|
||
html += '<th class="project-table-name-col" data-sort="name" onclick="LandingApp.toggleSort(\'name\')">'
|
||
+ 'Project number ' + sortIndicator('name')
|
||
+ '</th>';
|
||
if (anyTitles) {
|
||
html += '<th class="project-table-title-col" data-sort="title" onclick="LandingApp.toggleSort(\'title\')">'
|
||
+ 'Title ' + sortIndicator('title')
|
||
+ '</th>';
|
||
}
|
||
html += '</tr>';
|
||
html += '<tr class="project-table-filters">';
|
||
if (inSelect) html += '<th></th>';
|
||
html += '<th><input type="text" class="column-filter ' + (columnFilters.pn ? 'filter-active' : '') + '" '
|
||
+ 'data-column="pn" placeholder="filter…" '
|
||
+ 'value="' + escapeHtml(columnFilters.pn) + '" '
|
||
+ 'oninput="LandingApp.onColumnFilterInput(event)"></th>';
|
||
if (anyTitles) {
|
||
html += '<th><input type="text" class="column-filter ' + (columnFilters.pt ? 'filter-active' : '') + '" '
|
||
+ 'data-column="pt" placeholder="filter…" '
|
||
+ 'value="' + escapeHtml(columnFilters.pt) + '" '
|
||
+ 'oninput="LandingApp.onColumnFilterInput(event)"></th>';
|
||
}
|
||
html += '</tr>';
|
||
html += '</thead>';
|
||
|
||
var colspan = (inSelect ? 1 : 0) + 1 + (anyTitles ? 1 : 0);
|
||
if (rows.length === 0) {
|
||
html += '<tbody><tr><td colspan="' + colspan + '" class="project-table-no-match">'
|
||
+ 'No projects match the current filters.'
|
||
+ '</td></tr></tbody>';
|
||
} else {
|
||
html += '<tbody>';
|
||
for (var i = 0; i < rows.length; i++) {
|
||
var r = rows[i];
|
||
var isSel = inSelect && selected.has(r.name);
|
||
html += '<tr class="project-table-row' + (isSel ? ' is-selected' : '') + '" '
|
||
+ 'data-name="' + escapeHtml(r.name) + '" onclick="LandingApp.onProjectRowClick(event)">';
|
||
if (inSelect) {
|
||
html += '<td class="project-table-checkbox-col"><input type="checkbox" value="' + escapeHtml(r.name) + '"'
|
||
+ (isSel ? ' checked' : '')
|
||
+ ' onclick="event.stopPropagation(); LandingApp.toggleByCheckbox(event)"></td>';
|
||
}
|
||
html += '<td class="project-table-name-col">' + escapeHtml(r.name) + '</td>';
|
||
if (anyTitles) {
|
||
html += '<td class="project-table-title-col">' + (r.title ? escapeHtml(r.title) : '<span class="project-table-no-title">—</span>') + '</td>';
|
||
}
|
||
html += '</tr>';
|
||
}
|
||
html += '</tbody>';
|
||
}
|
||
html += '</table>';
|
||
container.innerHTML = html;
|
||
|
||
var headerCb = document.getElementById('headerCheckbox');
|
||
if (headerCb) headerCb.indeterminate = headerCheckedState === 'indeterminate';
|
||
}
|
||
|
||
function sortIndicator(field) {
|
||
if (sortField !== field) return '<span class="sort-indicator">↕</span>';
|
||
return '<span class="sort-indicator active">' + (sortDirection === 'asc' ? '▲' : '▼') + '</span>';
|
||
}
|
||
|
||
function renderProjectCount() {
|
||
var el = document.getElementById('projectCount');
|
||
if (!el) return;
|
||
if (loadError || allProjects.length === 0) { el.textContent = ''; return; }
|
||
var rows = visibleProjects();
|
||
var base = rows.length === allProjects.length
|
||
? '(' + allProjects.length + ')'
|
||
: '(' + rows.length + ' of ' + allProjects.length + ')';
|
||
if (selectMode) {
|
||
base = base + ' — ' + selected.size + ' checked';
|
||
}
|
||
el.textContent = base;
|
||
}
|
||
|
||
function renderGroupCount() {
|
||
var el = document.getElementById('groupCount');
|
||
if (!el) return;
|
||
el.textContent = groups.length === 0 ? '' : '(' + groups.length + ')';
|
||
}
|
||
|
||
// ── Events / actions ─────────────────────────────────────────────────────
|
||
|
||
function toggleSort(field) {
|
||
if (sortField === field) {
|
||
sortDirection = sortDirection === 'asc' ? 'desc' : 'asc';
|
||
} else {
|
||
sortField = field;
|
||
sortDirection = 'asc';
|
||
}
|
||
urlPush();
|
||
render();
|
||
}
|
||
|
||
function onColumnFilterInput(e) {
|
||
var col = e.target.getAttribute('data-column');
|
||
var val = e.target.value;
|
||
columnFilters[col] = val;
|
||
columnFilterASTs[col] = parseFilterAST(val);
|
||
urlPush();
|
||
renderProjects();
|
||
renderProjectCount();
|
||
var sel = document.querySelector('.column-filter[data-column="' + col + '"]');
|
||
if (sel) {
|
||
sel.focus();
|
||
sel.setSelectionRange(sel.value.length, sel.value.length);
|
||
}
|
||
}
|
||
|
||
function onProjectRowClick(e) {
|
||
var row = e.target.closest('.project-table-row');
|
||
if (!row) return;
|
||
var name = row.getAttribute('data-name');
|
||
if (!name) return;
|
||
if (selectMode) {
|
||
// In select-mode the row toggles its checkbox.
|
||
if (selected.has(name)) selected.delete(name);
|
||
else selected.add(name);
|
||
render();
|
||
} else {
|
||
// Default mode: click opens that single project directly.
|
||
openArchiveWith([name]);
|
||
}
|
||
}
|
||
|
||
function toggleByCheckbox(e) {
|
||
if (!selectMode) return;
|
||
var cb = e.target;
|
||
var name = cb.value;
|
||
if (cb.checked) selected.add(name);
|
||
else selected.delete(name);
|
||
render();
|
||
}
|
||
|
||
function toggleHeaderCheckbox() {
|
||
if (!selectMode) return;
|
||
var cb = document.getElementById('headerCheckbox');
|
||
if (!cb) return;
|
||
var rows = visibleProjects();
|
||
if (cb.checked) rows.forEach(function(r) { selected.add(r.name); });
|
||
else rows.forEach(function(r) { selected.delete(r.name); });
|
||
render();
|
||
}
|
||
|
||
// Navigation hook — tests replace this via LandingApp._setNavigate.
|
||
// (Patching window.location.href is unreliable in modern engines.)
|
||
var navigate = function(url) { location.href = url; };
|
||
|
||
function openArchiveWith(names) {
|
||
if (!names || names.length === 0) return;
|
||
var base = location.pathname.replace(/\/[^\/]*$/, '/');
|
||
var v = new URLSearchParams(location.search).get('v');
|
||
|
||
if (names.length === 1) {
|
||
// Single project → canonical project-subtree URL so the user
|
||
// can edit the address bar to swap archive.html for
|
||
// working/, staging/, reviewing/, etc. zddc-server's
|
||
// availability.go auto-serves the right tool at each.
|
||
// Multi-project (the `else` branch) keeps the ?projects=
|
||
// form because there's no single subtree root.
|
||
var url = base + encodeURIComponent(names[0]) + '/archive.html';
|
||
if (v) url += '?v=' + encodeURIComponent(v);
|
||
navigate(url);
|
||
return;
|
||
}
|
||
|
||
var params = ['projects=' + names.map(encodeURIComponent).join(',')];
|
||
if (v) params.push('v=' + encodeURIComponent(v));
|
||
navigate(base + 'archive.html?' + params.join('&'));
|
||
}
|
||
|
||
function openGroup(e) {
|
||
var row = e.target.closest('.groups-row');
|
||
if (!row) return;
|
||
var name = row.getAttribute('data-name');
|
||
var g = groups.find(function(x) { return x.name === name; });
|
||
if (!g) return;
|
||
// Drop projects the user no longer has access to (server-side ACL may
|
||
// have changed since the group was saved).
|
||
var accessible = new Set(allProjects.map(function(p) { return p.name; }));
|
||
var openable = g.projects.filter(function(p) { return accessible.has(p); });
|
||
if (openable.length === 0) {
|
||
showWarning('Group "' + name + '" has no projects you currently have access to.');
|
||
return;
|
||
}
|
||
if (openable.length < g.projects.length) {
|
||
// Open with what we can; warn but don't block.
|
||
console.warn('Skipping inaccessible projects in group', name, g.projects.filter(function(p) { return !accessible.has(p); }));
|
||
}
|
||
openArchiveWith(openable);
|
||
}
|
||
|
||
function dismissWarning() {
|
||
var el = document.getElementById('accessWarningBanner');
|
||
if (el) el.classList.add('hidden');
|
||
}
|
||
|
||
function showWarning(message) {
|
||
var el = document.getElementById('accessWarningBanner');
|
||
var txt = document.getElementById('accessWarningText');
|
||
if (!el || !txt) return;
|
||
txt.textContent = message;
|
||
el.classList.remove('hidden');
|
||
}
|
||
|
||
// ── Select-mode (create / edit groups) ───────────────────────────────────
|
||
|
||
function startCreateGroup() {
|
||
selectMode = { kind: 'create' };
|
||
selected = new Set();
|
||
render();
|
||
var input = document.getElementById('groupNameInput');
|
||
if (input) input.focus();
|
||
}
|
||
|
||
function startEditGroup(e) {
|
||
var btn = e.target.closest('.groups-btn-edit');
|
||
if (!btn) return;
|
||
var name = btn.getAttribute('data-name');
|
||
var g = groups.find(function(x) { return x.name === name; });
|
||
if (!g) return;
|
||
selectMode = { kind: 'edit', originalName: g.name };
|
||
selected = new Set(g.projects);
|
||
render();
|
||
var input = document.getElementById('groupNameInput');
|
||
if (input) input.focus();
|
||
}
|
||
|
||
function deleteGroup(e) {
|
||
var btn = e.target.closest('.groups-btn-delete');
|
||
if (!btn) return;
|
||
var name = btn.getAttribute('data-name');
|
||
if (!confirm('Delete group "' + name + '"?')) return;
|
||
groups = groups.filter(function(g) { return g.name !== name; });
|
||
persistGroups();
|
||
render();
|
||
}
|
||
|
||
function cancelSelect() {
|
||
selectMode = null;
|
||
selected = new Set();
|
||
render();
|
||
}
|
||
|
||
function saveGroup() {
|
||
if (!selectMode) return;
|
||
var input = document.getElementById('groupNameInput');
|
||
if (!input) return;
|
||
var name = (input.value || '').trim();
|
||
if (!name) {
|
||
input.focus();
|
||
return;
|
||
}
|
||
var projects = Array.from(selected).sort();
|
||
if (projects.length === 0) {
|
||
alert('Select at least one project before saving the group.');
|
||
return;
|
||
}
|
||
if (selectMode.kind === 'create') {
|
||
// Reject duplicate names so two groups can't share an identity.
|
||
if (groups.some(function(g) { return g.name === name; })) {
|
||
alert('A group named "' + name + '" already exists. Pick a different name or edit that group instead.');
|
||
return;
|
||
}
|
||
groups.push({ name: name, projects: projects });
|
||
} else {
|
||
// Editing: rename if name changed (and the new name doesn't collide).
|
||
if (name !== selectMode.originalName && groups.some(function(g) { return g.name === name; })) {
|
||
alert('A group named "' + name + '" already exists. Pick a different name.');
|
||
return;
|
||
}
|
||
groups = groups.map(function(g) {
|
||
return g.name === selectMode.originalName
|
||
? { name: name, projects: projects }
|
||
: g;
|
||
});
|
||
}
|
||
persistGroups();
|
||
selectMode = null;
|
||
selected = new Set();
|
||
render();
|
||
}
|
||
|
||
function openSelectedVisible() {
|
||
if (!selectMode) return;
|
||
// Rule: open only those that are currently visible (filtered in) AND
|
||
// checked. Filter-hidden but checked items are intentionally left out.
|
||
var visibleNames = new Set(visibleProjects().map(function(r) { return r.name; }));
|
||
var openable = Array.from(selected).filter(function(n) { return visibleNames.has(n); });
|
||
if (openable.length === 0) {
|
||
alert('No checked projects are currently visible. Adjust filters or check more projects to open.');
|
||
return;
|
||
}
|
||
openArchiveWith(openable);
|
||
}
|
||
|
||
// ── Persistence ──────────────────────────────────────────────────────────
|
||
|
||
function loadGroups() {
|
||
var raw;
|
||
try { raw = localStorage.getItem(GROUPS_KEY); }
|
||
catch (e) { raw = null; }
|
||
if (raw) {
|
||
try {
|
||
var parsed = JSON.parse(raw);
|
||
groups = Array.isArray(parsed) ? parsed.filter(isValidGroup) : [];
|
||
return;
|
||
} catch (e) { /* fall through to legacy */ }
|
||
}
|
||
// One-shot migration: convert old `zddc_landing_presets` (which carried
|
||
// filter+sort state alongside the project list) to plain groups.
|
||
try {
|
||
var legacy = localStorage.getItem(LEGACY_PRESETS_KEY);
|
||
if (!legacy) return;
|
||
var legacyParsed = JSON.parse(legacy);
|
||
if (!Array.isArray(legacyParsed)) return;
|
||
groups = legacyParsed.map(function(p) {
|
||
var projects = (p && p.state && Array.isArray(p.state.projects)) ? p.state.projects : [];
|
||
return { name: String(p && p.name || ''), projects: projects };
|
||
}).filter(isValidGroup);
|
||
persistGroups();
|
||
} catch (e) { groups = []; }
|
||
}
|
||
|
||
function isValidGroup(g) {
|
||
return g && typeof g.name === 'string' && g.name.length > 0
|
||
&& Array.isArray(g.projects);
|
||
}
|
||
|
||
function persistGroups() {
|
||
try { localStorage.setItem(GROUPS_KEY, JSON.stringify(groups)); }
|
||
catch (e) { /* private mode / quota */ }
|
||
}
|
||
|
||
// ── Project mode ─────────────────────────────────────────────────────────
|
||
//
|
||
// The same landing tool serves at /<project> as the project-workspace
|
||
// page. Mode is determined from location.pathname:
|
||
//
|
||
// / → 'picker' (existing behavior)
|
||
// /<single-segment> → 'project'
|
||
// /index.html → 'picker' (file:// + standalone-served root)
|
||
// anything else → 'picker' (best-effort fallback)
|
||
//
|
||
// Project mode shows the four canonical lifecycle-stage cards, a
|
||
// "browse all files" link, and a Master Deliverables List section
|
||
// with direct links to any parties currently in archive/. The party
|
||
// list is fetched from <project>/<archive>/?json=1; failures fall
|
||
// back to the static "no parties yet" copy.
|
||
|
||
function detectMode() {
|
||
if (typeof location === 'undefined') return 'picker';
|
||
var path = location.pathname || '/';
|
||
// Strip any trailing /index.html so the deployment-root case
|
||
// matches even on file:// or behind some servers.
|
||
var trimmed = path.replace(/\/index\.html$/, '/');
|
||
if (trimmed === '' || trimmed === '/') return 'picker';
|
||
// Single non-slash, non-dot segment → project root.
|
||
var parts = trimmed.split('/').filter(Boolean);
|
||
if (parts.length === 1 && parts[0].indexOf('.') === -1) {
|
||
return 'project';
|
||
}
|
||
return 'picker';
|
||
}
|
||
|
||
function projectFromPath() {
|
||
var parts = (location.pathname || '/').split('/').filter(Boolean);
|
||
return parts[0] || '';
|
||
}
|
||
|
||
// Render the project-workspace view: title, four stage links, MDL
|
||
// section. Stage hrefs use the no-trailing-slash form so the server
|
||
// routes them to each canonical default tool (browse for working/+
|
||
// reviewing/, transmittal for staging/, etc.). Browse-all and the
|
||
// archive deep link use the slash form to land on the directory listing.
|
||
async function renderProjectMode() {
|
||
var project = projectFromPath();
|
||
if (!project) return;
|
||
|
||
// Hide picker, show project view.
|
||
var picker = document.getElementById('pickerView');
|
||
var projectView = document.getElementById('projectView');
|
||
if (picker) picker.classList.add('hidden');
|
||
if (projectView) projectView.classList.remove('hidden');
|
||
|
||
document.title = project + ' — ZDDC';
|
||
var titleEl = document.getElementById('projectName');
|
||
if (titleEl) titleEl.textContent = project;
|
||
|
||
var p = encodeURIComponent(project);
|
||
var stages = [
|
||
{ id: 'stageArchive', href: '/' + p + '/archive' },
|
||
{ id: 'stageWorking', href: '/' + p + '/working' },
|
||
{ id: 'stageStaging', href: '/' + p + '/staging' },
|
||
{ id: 'stageReviewing', href: '/' + p + '/reviewing' },
|
||
];
|
||
for (var i = 0; i < stages.length; i++) {
|
||
var a = document.getElementById(stages[i].id);
|
||
if (a) a.setAttribute('href', stages[i].href);
|
||
}
|
||
|
||
var browseAll = document.getElementById('browseAllLink');
|
||
if (browseAll) {
|
||
browseAll.setAttribute('href', '/' + p + '/');
|
||
browseAll.textContent = 'Browse all files →';
|
||
}
|
||
|
||
// MDL card. Same shape as the stage cards above, but
|
||
// interactive: a <select> populated with party folders and an
|
||
// Open button that opens the chosen party's MDL. The view
|
||
// auto-renders at any archive/<party>/mdl/ URL even when the
|
||
// folder doesn't exist on disk (zddc-server commit 3fc3717),
|
||
// so we offer the operator-supplied party list directly AND
|
||
// a "type a new party name" affordance via a free-text last
|
||
// option.
|
||
var mdlSelect = document.getElementById('mdlPartySelect');
|
||
var mdlOpenBtn = document.getElementById('mdlOpenBtn');
|
||
var mdlHint = document.getElementById('mdlHint');
|
||
if (!mdlSelect || !mdlOpenBtn) return;
|
||
|
||
// Wire the Open button regardless of fetch outcome — even if
|
||
// party enumeration fails, an operator can still navigate by
|
||
// typing the party folder name in the URL bar.
|
||
function openSelectedMdl() {
|
||
var party = mdlSelect.value;
|
||
if (!party) return;
|
||
// No trailing slash: per the convention, the no-slash form
|
||
// serves the tables tool with the MDL view. The slash form
|
||
// would serve browse, which is not what the user wants when
|
||
// they click "Open MDL".
|
||
var url = '/' + p + '/archive/' + encodeURIComponent(party) + '/mdl';
|
||
window.location.assign(url);
|
||
}
|
||
mdlOpenBtn.addEventListener('click', openSelectedMdl);
|
||
mdlSelect.addEventListener('change', function () {
|
||
mdlOpenBtn.disabled = !mdlSelect.value;
|
||
});
|
||
// Enter inside the select also opens.
|
||
mdlSelect.addEventListener('keydown', function (e) {
|
||
if (e.key === 'Enter') {
|
||
e.preventDefault();
|
||
openSelectedMdl();
|
||
}
|
||
});
|
||
|
||
var parties = await fetchParties(p);
|
||
// Repopulate the select. mdlSelect starts with a single
|
||
// "Loading…" option; replace its contents either way.
|
||
mdlSelect.innerHTML = '';
|
||
if (parties == null) {
|
||
// Network error or unauthenticated. Leave the select
|
||
// disabled but visible; user can still navigate via URL.
|
||
var optErr = document.createElement('option');
|
||
optErr.value = '';
|
||
optErr.textContent = '(could not enumerate parties)';
|
||
mdlSelect.appendChild(optErr);
|
||
mdlSelect.disabled = true;
|
||
mdlOpenBtn.disabled = true;
|
||
return;
|
||
}
|
||
if (parties.length === 0) {
|
||
// No parties yet, but per the hint the URL still works for
|
||
// any party name. Give a placeholder option that disables
|
||
// Open until the user has typed something — except we
|
||
// don't have a text input. So just say "(none yet)" and
|
||
// disable. Operator can still navigate via the URL bar.
|
||
var optNone = document.createElement('option');
|
||
optNone.value = '';
|
||
optNone.textContent = '(no party folders yet)';
|
||
mdlSelect.appendChild(optNone);
|
||
mdlSelect.disabled = true;
|
||
mdlOpenBtn.disabled = true;
|
||
if (mdlHint) {
|
||
mdlHint.innerHTML =
|
||
'No <code>archive/<party>/</code> folders yet. The MDL view still '
|
||
+ 'auto-renders at any such URL, even before the folder exists — type a '
|
||
+ 'party name into the URL bar (or wait for the first transmittal) to start editing.';
|
||
}
|
||
return;
|
||
}
|
||
// Populate the select with each party.
|
||
var optPlaceholder = document.createElement('option');
|
||
optPlaceholder.value = '';
|
||
optPlaceholder.textContent = 'Choose a party…';
|
||
mdlSelect.appendChild(optPlaceholder);
|
||
for (var j = 0; j < parties.length; j++) {
|
||
var opt = document.createElement('option');
|
||
opt.value = parties[j].name;
|
||
opt.textContent = parties[j].name;
|
||
mdlSelect.appendChild(opt);
|
||
}
|
||
mdlSelect.disabled = false;
|
||
// Open button stays disabled until the user picks something.
|
||
mdlOpenBtn.disabled = true;
|
||
}
|
||
|
||
// Returns an array of {name, url} for each party folder in the
|
||
// project's archive/, sorted by name. Returns null if the listing
|
||
// can't be fetched (offline, 4xx, or non-JSON response). Returns
|
||
// [] if the listing succeeds but archive/ is empty / has no
|
||
// visible party folders.
|
||
async function fetchParties(projectURL) {
|
||
try {
|
||
var resp = await fetch('/' + projectURL + '/archive/', {
|
||
headers: { 'Accept': 'application/json' },
|
||
cache: 'no-cache',
|
||
credentials: 'same-origin'
|
||
});
|
||
if (!resp.ok) return null;
|
||
var ctype = resp.headers.get('Content-Type') || '';
|
||
if (!ctype.toLowerCase().includes('json')) return null;
|
||
var data = await resp.json();
|
||
if (!Array.isArray(data)) return null;
|
||
// Server emits directories with trailing "/" on the name.
|
||
// Filter to dirs only, strip the slash for display.
|
||
var out = [];
|
||
for (var i = 0; i < data.length; i++) {
|
||
var e = data[i];
|
||
if (!e.is_dir) continue;
|
||
var nm = String(e.name || '').replace(/\/$/, '');
|
||
if (!nm) continue;
|
||
if (nm.charAt(0) === '.' || nm.charAt(0) === '_') continue;
|
||
out.push({ name: nm, url: e.url || ('/' + projectURL + '/archive/' + encodeURIComponent(nm) + '/') });
|
||
}
|
||
out.sort(function (a, b) { return a.name < b.name ? -1 : a.name > b.name ? 1 : 0; });
|
||
return out;
|
||
} catch (e) {
|
||
return null;
|
||
}
|
||
}
|
||
|
||
// ── Bootstrap ────────────────────────────────────────────────────────────
|
||
|
||
async function init() {
|
||
if (detectMode() === 'project') {
|
||
await renderProjectMode();
|
||
return;
|
||
}
|
||
await initPicker();
|
||
}
|
||
|
||
async function initPicker() {
|
||
loadGroups();
|
||
urlRestore();
|
||
|
||
var ok = await fetchProjects();
|
||
if (ok) {
|
||
// No URL-restored selection in the new model, but warn about
|
||
// groups that reference inaccessible projects so the user knows
|
||
// why their group is shorter than expected when opened.
|
||
var accessibleNames = new Set(allProjects.map(function(p) { return p.name; }));
|
||
var ghostlyGroups = groups.filter(function(g) {
|
||
return g.projects.some(function(p) { return !accessibleNames.has(p); });
|
||
});
|
||
if (ghostlyGroups.length > 0) {
|
||
console.info('Some saved groups reference projects you no longer have access to; they will open with the accessible subset only.', ghostlyGroups.map(function(g) { return g.name; }));
|
||
}
|
||
}
|
||
|
||
render();
|
||
|
||
// Wire up keyboard shortcuts in the action-bar input: Enter saves,
|
||
// Escape cancels.
|
||
var input = document.getElementById('groupNameInput');
|
||
if (input) {
|
||
input.addEventListener('keydown', function(e) {
|
||
if (e.key === 'Enter') { e.preventDefault(); saveGroup(); }
|
||
else if (e.key === 'Escape') { e.preventDefault(); cancelSelect(); }
|
||
});
|
||
}
|
||
}
|
||
|
||
function escapeHtml(text) {
|
||
var div = document.createElement('div');
|
||
div.textContent = String(text == null ? '' : text);
|
||
return div.innerHTML;
|
||
}
|
||
|
||
document.addEventListener('DOMContentLoaded', init);
|
||
|
||
// Public API for inline handlers.
|
||
window.LandingApp = {
|
||
init: init,
|
||
toggleByCheckbox: toggleByCheckbox,
|
||
toggleHeaderCheckbox: toggleHeaderCheckbox,
|
||
toggleSort: toggleSort,
|
||
onColumnFilterInput: onColumnFilterInput,
|
||
onProjectRowClick: onProjectRowClick,
|
||
openGroup: openGroup,
|
||
startCreateGroup: startCreateGroup,
|
||
startEditGroup: startEditGroup,
|
||
deleteGroup: deleteGroup,
|
||
cancelSelect: cancelSelect,
|
||
saveGroup: saveGroup,
|
||
openSelectedVisible: openSelectedVisible,
|
||
dismissWarning: dismissWarning,
|
||
// Project-mode entry points (also tested directly).
|
||
detectMode: detectMode,
|
||
renderProjectMode: renderProjectMode,
|
||
// Test-only: override the navigation function (avoids the messy
|
||
// browser-locked-down state of window.location).
|
||
_setNavigate: function(fn) { navigate = fn; }
|
||
};
|
||
|
||
})();
|
||
|
||
</script>
|
||
</body>
|
||
</html>
|