ZDDC/website/releases/landing_beta.html
ZDDC 91d6e61e22 feat(web): releases index, alpha+beta channel builds, inline server section
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>
2026-04-28 10:00:10 -05:00

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">&times;</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">&times;<\/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>