Three things on the public website:
1) Cut alpha and beta channel builds for all five tools, so each tool
now has stable + beta + alpha actually published — previously
beta and alpha were vapor for archive (which had been freshened
earlier) and missing entirely for the others. The intro page's
tool cards now point at real artifacts on every channel.
2) New website/releases/index.html — a generated index of every
version + channel of every tool, with stable/beta/alpha pill
links per tool and a "Pin to version" row of every concrete
v0.0.X build. Regenerated by build.sh's new build_releases_index
function (reads the filesystem so it is always consistent with
what is actually under releases/). Linked from the intro page nav
(Releases), from the bottom of the Try the tools section
("Browse all versions"), and from the Learn more list.
reference.html's nav gets the same Releases link.
3) Folded website/zddc-server.html into website/index.html as a new
inline section ("zddc-server (optional)") below the tool cards.
The earlier separate page is removed; the broken Server nav link
that pointed at it is gone too. The new section leads with the
dual-mode insight (the tools work locally on a folder OR via any
web server, including the optional zddc-server) and frames
zddc-server as a small Go binary that adds things a generic web
server cannot: ACL via .zddc files, virtual .archive URL space,
per-request access logging, mundane glue. The What is it?
paragraph also mentions the dual-mode story up front so users
reading top-to-bottom get the framing before they hit the cards.
Also caught two stale _latest.html refs missed by the earlier
rename sweep: 8 tool links in reference.html and a comment line in
CLAUDE.md. Verified with a full link audit — every relative href in
index.html, reference.html, and releases/index.html now resolves to
an existing file under website/.
ARCHITECTURE.md doc-ownership table updated: zddc-server.html row
removed; new row added for the regenerated releases/index.html.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
1029 lines
29 KiB
HTML
1029 lines
29 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 Archive — Projects</title>
|
|
<style>
|
|
/* ==========================================================================
|
|
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: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Oxygen, Ubuntu, Cantarell, 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: #4a90c4;
|
|
--primary-hover: #5ba3d9;
|
|
--primary-active: #6ab5e8;
|
|
--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: #4a90c4;
|
|
--primary-hover: #5ba3d9;
|
|
--primary-active: #6ab5e8;
|
|
--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-weight: 600;
|
|
line-height: 1.2;
|
|
}
|
|
|
|
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);
|
|
}
|
|
|
|
.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;
|
|
}
|
|
|
|
/* Tool name inside the header */
|
|
.app-header__title {
|
|
font-size: 17px;
|
|
font-weight: 600;
|
|
color: var(--text);
|
|
letter-spacing: 0.01em;
|
|
white-space: nowrap;
|
|
}
|
|
|
|
/* ── 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;
|
|
}
|
|
|
|
/* Toast CSS lives in classifier/css/base.css — only that tool uses toasts. */
|
|
|
|
/* ── 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);
|
|
}
|
|
|
|
/* Landing page layout */
|
|
body {
|
|
margin: 0;
|
|
font-family: var(--font);
|
|
font-size: 14px;
|
|
background: var(--bg-secondary);
|
|
color: var(--text);
|
|
}
|
|
|
|
.landing-main {
|
|
max-width: 640px;
|
|
margin: 32px auto;
|
|
padding: 0 16px;
|
|
}
|
|
|
|
/* 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;
|
|
}
|
|
|
|
/* Main card */
|
|
.landing-card {
|
|
background: var(--bg);
|
|
border: 1px solid var(--border);
|
|
border-radius: var(--radius);
|
|
overflow: hidden;
|
|
}
|
|
|
|
.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-header h2 {
|
|
margin: 0;
|
|
font-size: 1rem;
|
|
font-weight: 600;
|
|
color: var(--text);
|
|
}
|
|
|
|
.landing-header-actions {
|
|
display: flex;
|
|
align-items: center;
|
|
gap: 8px;
|
|
flex-wrap: wrap;
|
|
}
|
|
|
|
/* Project list */
|
|
.project-list {
|
|
padding: 8px 0;
|
|
min-height: 80px;
|
|
}
|
|
|
|
.project-list-empty {
|
|
padding: 32px 16px;
|
|
text-align: center;
|
|
color: var(--text-muted);
|
|
font-size: 0.875rem;
|
|
}
|
|
|
|
.project-item {
|
|
display: flex;
|
|
align-items: center;
|
|
padding: 8px 16px;
|
|
gap: 10px;
|
|
cursor: pointer;
|
|
border-radius: 0;
|
|
transition: background 0.1s;
|
|
}
|
|
.project-item:hover { background: var(--bg-hover); }
|
|
.project-item input[type="checkbox"] {
|
|
width: 16px;
|
|
height: 16px;
|
|
cursor: pointer;
|
|
flex-shrink: 0;
|
|
accent-color: var(--primary);
|
|
}
|
|
.project-item-name {
|
|
font-size: 0.9375rem;
|
|
color: var(--text);
|
|
user-select: none;
|
|
}
|
|
|
|
/* Footer */
|
|
.landing-card-footer {
|
|
display: flex;
|
|
justify-content: space-between;
|
|
align-items: center;
|
|
padding: 12px 16px;
|
|
border-top: 1px solid var(--border);
|
|
gap: 8px;
|
|
}
|
|
|
|
/* Preset menu */
|
|
.preset-control {
|
|
position: relative;
|
|
}
|
|
.preset-menu {
|
|
position: absolute;
|
|
top: 100%;
|
|
right: 0;
|
|
margin-top: 4px;
|
|
background: var(--bg);
|
|
border: 1px solid var(--border);
|
|
border-radius: var(--radius);
|
|
box-shadow: 0 4px 12px rgba(0,0,0,0.12);
|
|
min-width: 200px;
|
|
z-index: 100;
|
|
padding: 4px 0;
|
|
}
|
|
.preset-menu.hidden { display: none; }
|
|
.preset-menu-item {
|
|
display: flex;
|
|
align-items: center;
|
|
justify-content: space-between;
|
|
padding: 8px 12px;
|
|
cursor: pointer;
|
|
font-size: 0.875rem;
|
|
gap: 8px;
|
|
}
|
|
.preset-menu-item:hover { background: var(--bg-hover); }
|
|
.preset-menu-item-name {
|
|
flex: 1;
|
|
overflow: hidden;
|
|
text-overflow: ellipsis;
|
|
white-space: nowrap;
|
|
}
|
|
.preset-delete-btn {
|
|
background: none;
|
|
border: none;
|
|
cursor: pointer;
|
|
color: var(--text-muted);
|
|
font-size: 0.875rem;
|
|
padding: 0 2px;
|
|
flex-shrink: 0;
|
|
}
|
|
.preset-delete-btn:hover { color: var(--danger); }
|
|
.preset-menu-empty {
|
|
padding: 8px 12px;
|
|
color: var(--text-muted);
|
|
font-size: 0.875rem;
|
|
}
|
|
.preset-menu-divider {
|
|
border: none;
|
|
border-top: 1px solid var(--border);
|
|
margin: 4px 0;
|
|
}
|
|
|
|
/* Loading state */
|
|
.project-list-loading {
|
|
padding: 32px 16px;
|
|
text-align: center;
|
|
color: var(--text-muted);
|
|
font-size: 0.875rem;
|
|
}
|
|
|
|
</style>
|
|
</head>
|
|
<body>
|
|
<header class="app-header">
|
|
<div class="header-left">
|
|
<span class="app-header__title">ZDDC Archive</span>
|
|
<span class="build-timestamp"><span style="color:red;font-weight:bold">beta · 2026-04-28 · 67f794e</span></span>
|
|
</div>
|
|
<div class="header-right">
|
|
<button id="theme-btn" class="btn btn-secondary" title="Theme: auto (follows OS)" aria-label="Theme: auto (follows OS)">◐</button>
|
|
</div>
|
|
</header>
|
|
|
|
<main class="landing-main">
|
|
<!-- 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>
|
|
|
|
<div class="landing-card">
|
|
<div class="landing-card-header">
|
|
<h2>Select Projects</h2>
|
|
<div class="landing-header-actions">
|
|
<!-- Presets dropdown -->
|
|
<div class="preset-control">
|
|
<button id="presetMenuBtn" class="btn btn-secondary btn-sm" onclick="LandingApp.togglePresetMenu()">▾ Presets</button>
|
|
<div id="presetMenu" class="preset-menu hidden"></div>
|
|
</div>
|
|
<button class="btn btn-secondary btn-sm" onclick="LandingApp.selectAll()">Select All</button>
|
|
<button class="btn btn-secondary btn-sm" onclick="LandingApp.selectNone()">Select None</button>
|
|
</div>
|
|
</div>
|
|
|
|
<div id="projectList" class="project-list">
|
|
<!-- Populated by JS -->
|
|
</div>
|
|
|
|
<div class="landing-card-footer">
|
|
<button id="savePresetBtn" class="btn btn-secondary" onclick="LandingApp.savePreset()">Save Preset…</button>
|
|
<button id="openArchiveBtn" class="btn btn-primary" onclick="LandingApp.openArchive()">Open Archive →</button>
|
|
</div>
|
|
</div>
|
|
</main>
|
|
|
|
<script>
|
|
/**
|
|
* 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();
|
|
}
|
|
}());
|
|
|
|
(function() {
|
|
'use strict';
|
|
|
|
var accessibleProjects = []; // [{name, url}, ...] from server
|
|
var presets = []; // [{name, projects[]}, ...] from localStorage
|
|
var PRESETS_KEY = 'zddc_landing_presets';
|
|
|
|
// ── Initialise ──────────────────────────────────────────────────────────
|
|
|
|
async function init() {
|
|
loadPresets();
|
|
|
|
var urlProjects = getUrlProjects();
|
|
|
|
var projectList = document.getElementById('projectList');
|
|
projectList.innerHTML = '<div class="project-list-loading">Loading projects…<\/div>';
|
|
|
|
try {
|
|
var resp = await fetch(location.origin + location.pathname.replace(/\/[^\/]*$/, '/'), {
|
|
headers: { 'Accept': 'application/json' }
|
|
});
|
|
if (!resp.ok) throw new Error('HTTP ' + resp.status);
|
|
accessibleProjects = await resp.json();
|
|
} catch (e) {
|
|
projectList.innerHTML = '<div class="project-list-empty">Could not load project list: ' + escapeHtml(e.message) + '<\/div>';
|
|
return;
|
|
}
|
|
|
|
// Warn about URL projects that are not accessible
|
|
if (urlProjects.size > 0) {
|
|
var missing = Array.from(urlProjects).filter(function(p) {
|
|
return !accessibleProjects.some(function(ap) { return ap.name === p; });
|
|
});
|
|
if (missing.length > 0) {
|
|
showWarning('This link includes projects you don\'t have access to: ' + missing.map(escapeHtml).join(', '));
|
|
}
|
|
}
|
|
|
|
renderProjects(urlProjects);
|
|
renderPresetMenu();
|
|
|
|
// Close preset menu on outside click
|
|
document.addEventListener('click', function(e) {
|
|
var menu = document.getElementById('presetMenu');
|
|
var btn = document.getElementById('presetMenuBtn');
|
|
if (menu && !menu.classList.contains('hidden') && !menu.contains(e.target) && e.target !== btn) {
|
|
menu.classList.add('hidden');
|
|
}
|
|
});
|
|
}
|
|
|
|
function getUrlProjects() {
|
|
var params = new URLSearchParams(location.search);
|
|
var val = params.get('projects');
|
|
if (!val) return new Set();
|
|
return new Set(val.split(',').map(function(p) { return p.trim(); }).filter(Boolean));
|
|
}
|
|
|
|
// ── Rendering ────────────────────────────────────────────────────────────
|
|
|
|
function renderProjects(preCheck) {
|
|
var container = document.getElementById('projectList');
|
|
if (accessibleProjects.length === 0) {
|
|
container.innerHTML = '<div class="project-list-empty">No projects available.<\/div>';
|
|
return;
|
|
}
|
|
var html = accessibleProjects.map(function(p) {
|
|
var checked = (preCheck.size === 0 || preCheck.has(p.name)) ? ' checked' : '';
|
|
return '<div class="project-item" onclick="LandingApp.toggleProject(this)">' +
|
|
'<input type="checkbox" value="' + escapeHtml(p.name) + '"' + checked + ' onclick="event.stopPropagation()">' +
|
|
'<span class="project-item-name">' + escapeHtml(p.name) + '<\/span>' +
|
|
'<\/div>';
|
|
}).join('');
|
|
container.innerHTML = html;
|
|
}
|
|
|
|
function renderPresetMenu() {
|
|
var menu = document.getElementById('presetMenu');
|
|
if (!menu) return;
|
|
if (presets.length === 0) {
|
|
menu.innerHTML = '<div class="preset-menu-empty">No presets saved.<\/div>';
|
|
return;
|
|
}
|
|
menu.innerHTML = presets.map(function(preset) {
|
|
return '<div class="preset-menu-item">' +
|
|
'<span class="preset-menu-item-name" onclick="LandingApp.applyPreset(' + JSON.stringify(preset.name) + ')">' +
|
|
escapeHtml(preset.name) + '<\/span>' +
|
|
'<button class="preset-delete-btn" onclick="LandingApp.deletePreset(' + JSON.stringify(preset.name) + ')" title="Delete preset">×<\/button>' +
|
|
'<\/div>';
|
|
}).join('');
|
|
}
|
|
|
|
// ── Actions ──────────────────────────────────────────────────────────────
|
|
|
|
function selectAll() {
|
|
document.querySelectorAll('#projectList input[type="checkbox"]').forEach(function(cb) {
|
|
cb.checked = true;
|
|
});
|
|
}
|
|
|
|
function selectNone() {
|
|
document.querySelectorAll('#projectList input[type="checkbox"]').forEach(function(cb) {
|
|
cb.checked = false;
|
|
});
|
|
}
|
|
|
|
function toggleProject(row) {
|
|
var cb = row.querySelector('input[type="checkbox"]');
|
|
if (cb) cb.checked = !cb.checked;
|
|
}
|
|
|
|
function openArchive() {
|
|
var checked = Array.from(document.querySelectorAll('#projectList input[type="checkbox"]:checked'))
|
|
.map(function(cb) { return cb.value; });
|
|
if (checked.length === 0) {
|
|
alert('Select at least one project to open.');
|
|
return;
|
|
}
|
|
var base = location.pathname.replace(/\/[^\/]*$/, '/');
|
|
location.href = base + 'archive.html?projects=' + checked.map(encodeURIComponent).join(',');
|
|
}
|
|
|
|
function savePreset() {
|
|
var name = prompt('Enter a name for this preset:');
|
|
if (!name || !name.trim()) return;
|
|
name = name.trim();
|
|
var checked = Array.from(document.querySelectorAll('#projectList input[type="checkbox"]:checked'))
|
|
.map(function(cb) { return cb.value; });
|
|
// Replace existing preset with same name
|
|
presets = presets.filter(function(p) { return p.name !== name; });
|
|
presets.push({ name: name, projects: checked });
|
|
savePresets();
|
|
renderPresetMenu();
|
|
}
|
|
|
|
function togglePresetMenu() {
|
|
var menu = document.getElementById('presetMenu');
|
|
if (menu) menu.classList.toggle('hidden');
|
|
}
|
|
|
|
function applyPreset(name) {
|
|
var preset = presets.find(function(p) { return p.name === name; });
|
|
if (!preset) return;
|
|
var projectSet = new Set(preset.projects);
|
|
document.querySelectorAll('#projectList input[type="checkbox"]').forEach(function(cb) {
|
|
cb.checked = projectSet.has(cb.value);
|
|
});
|
|
document.getElementById('presetMenu').classList.add('hidden');
|
|
}
|
|
|
|
function deletePreset(name) {
|
|
presets = presets.filter(function(p) { return p.name !== name; });
|
|
savePresets();
|
|
renderPresetMenu();
|
|
}
|
|
|
|
function dismissWarning() {
|
|
var el = document.getElementById('accessWarningBanner');
|
|
if (el) el.classList.add('hidden');
|
|
}
|
|
|
|
// ── Warning ───────────────────────────────────────────────────────────────
|
|
|
|
function showWarning(message) {
|
|
var el = document.getElementById('accessWarningBanner');
|
|
var txt = document.getElementById('accessWarningText');
|
|
if (!el || !txt) return;
|
|
txt.textContent = message;
|
|
el.classList.remove('hidden');
|
|
}
|
|
|
|
// ── Persistence ───────────────────────────────────────────────────────────
|
|
|
|
function loadPresets() {
|
|
try {
|
|
var raw = localStorage.getItem(PRESETS_KEY);
|
|
presets = raw ? JSON.parse(raw) : [];
|
|
if (!Array.isArray(presets)) presets = [];
|
|
} catch (e) {
|
|
presets = [];
|
|
}
|
|
}
|
|
|
|
function savePresets() {
|
|
try {
|
|
localStorage.setItem(PRESETS_KEY, JSON.stringify(presets));
|
|
} catch (e) { /* quota exceeded or private browsing */ }
|
|
}
|
|
|
|
// ── Utilities ─────────────────────────────────────────────────────────────
|
|
|
|
function escapeHtml(text) {
|
|
var div = document.createElement('div');
|
|
div.textContent = String(text);
|
|
return div.innerHTML;
|
|
}
|
|
|
|
// ── Bootstrap ─────────────────────────────────────────────────────────────
|
|
|
|
document.addEventListener('DOMContentLoaded', init);
|
|
|
|
window.LandingApp = {
|
|
init: init,
|
|
selectAll: selectAll,
|
|
selectNone: selectNone,
|
|
toggleProject: toggleProject,
|
|
openArchive: openArchive,
|
|
savePreset: savePreset,
|
|
togglePresetMenu: togglePresetMenu,
|
|
applyPreset: applyPreset,
|
|
deletePreset: deletePreset,
|
|
dismissWarning: dismissWarning
|
|
};
|
|
|
|
})();
|
|
|
|
</script>
|
|
</body>
|
|
</html>
|