filegrc 0.13.1 → 0.13.3
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/package.json +4 -2
- package/src/collection-review.js +28 -8
- package/src/git.js +1970 -794
- package/src/web.js +174 -73
package/src/web.js
CHANGED
|
@@ -43,7 +43,7 @@ export function renderIndex(state = null) {
|
|
|
43
43
|
</head>
|
|
44
44
|
<body>
|
|
45
45
|
<a class="skip-link" href="#main">Skip to content</a>
|
|
46
|
-
<div id="app"><div class="loading">Loading workspace…</div></div>
|
|
46
|
+
<div id="app"><div class="loading" role="status" aria-live="polite" aria-busy="true"><span class="spinner" aria-hidden="true"></span><span>Loading workspace…</span></div></div>
|
|
47
47
|
${snapshot}
|
|
48
48
|
<script src="./filegrc-app.js" defer></script>
|
|
49
49
|
</body>
|
|
@@ -98,6 +98,44 @@ let mutationStateRefreshRequested = false;
|
|
|
98
98
|
let programSelectionGeneration = 0;
|
|
99
99
|
let expiredStateRefresh = null;
|
|
100
100
|
|
|
101
|
+
function loadingIndicator(label) {
|
|
102
|
+
return '<span class="spinner" aria-hidden="true"></span><span>' + esc(label) + '</span>';
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
function setLoadingContent(element, label) {
|
|
106
|
+
if (!element) return;
|
|
107
|
+
element.classList.add("is-loading");
|
|
108
|
+
element.setAttribute("aria-busy", "true");
|
|
109
|
+
element.innerHTML = loadingIndicator(label);
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
function clearLoadingContent(element, label = "") {
|
|
113
|
+
if (!element) return;
|
|
114
|
+
element.classList.remove("is-loading");
|
|
115
|
+
element.removeAttribute("aria-busy");
|
|
116
|
+
element.textContent = label;
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
function setButtonBusy(button, busy, label = "") {
|
|
120
|
+
if (!button) return;
|
|
121
|
+
if (busy) {
|
|
122
|
+
if (!button.dataset.loadingIdle) {
|
|
123
|
+
button.dataset.loadingIdle = button.innerHTML;
|
|
124
|
+
button.dataset.loadingWasDisabled = String(button.disabled);
|
|
125
|
+
}
|
|
126
|
+
button.disabled = true;
|
|
127
|
+
setLoadingContent(button, label);
|
|
128
|
+
return;
|
|
129
|
+
}
|
|
130
|
+
const idle = button.dataset.loadingIdle;
|
|
131
|
+
const wasDisabled = button.dataset.loadingWasDisabled === "true";
|
|
132
|
+
clearLoadingContent(button);
|
|
133
|
+
if (idle !== undefined) button.innerHTML = idle;
|
|
134
|
+
button.disabled = wasDisabled;
|
|
135
|
+
delete button.dataset.loadingIdle;
|
|
136
|
+
delete button.dataset.loadingWasDisabled;
|
|
137
|
+
}
|
|
138
|
+
|
|
101
139
|
start().catch((error) => {
|
|
102
140
|
root.innerHTML = '<main class="fatal"><h1>Could Not Load the Workspace</h1><pre></pre></main>';
|
|
103
141
|
root.querySelector("pre").textContent = error.stack || error.message;
|
|
@@ -188,7 +226,7 @@ function renderStateLoading(main, route, sections) {
|
|
|
188
226
|
const message = failed
|
|
189
227
|
? state.sectionErrors?.[failed] || "Reload the workspace and try again."
|
|
190
228
|
: "Records are ready. FileGRC is calculating the information needed for this page.";
|
|
191
|
-
main.innerHTML = '<div class="page"><section class="panel state-loading" role="status" aria-live="polite"><p class="kicker">' + (failed ? "Loading error" : "One moment") + '</p><h2>' + esc(title) + '</h2><p>' + esc(message) + '</p></section></div>';
|
|
229
|
+
main.innerHTML = '<div class="page"><section class="panel state-loading" role="' + (failed ? "alert" : "status") + '" aria-live="polite"' + (failed ? "" : ' aria-busy="true"') + '>' + (failed ? "" : '<span class="spinner spinner-large" aria-hidden="true"></span>') + '<div><p class="kicker">' + (failed ? "Loading error" : "One moment") + '</p><h2>' + esc(title) + '</h2><p>' + esc(message) + '</p></div></section></div>';
|
|
192
230
|
}
|
|
193
231
|
|
|
194
232
|
function loadStateForRoute() {
|
|
@@ -373,13 +411,17 @@ function topbar(route) {
|
|
|
373
411
|
const programSelect = programs.length > 1 && !state.readOnly
|
|
374
412
|
? '<label class="program-select"><span class="sr-only">Current Program</span><select data-program-select>' + programs.map(({ record }) => '<option value="' + esc(record.id) + '" ' + (record.id === state.selectedProgramId ? "selected" : "") + '>' + esc(record.title) + '</option>').join("") + '</select></label>'
|
|
375
413
|
: "";
|
|
376
|
-
|
|
414
|
+
const validationMark = validationLoading && !repositoryError ? '<span class="spinner" aria-hidden="true"></span>' : '<span class="status-dot ' + validationTone + '"></span>';
|
|
415
|
+
const repositoryBusy = repositoryLoading && !repositoryError || state.repository?.status === "syncing";
|
|
416
|
+
const repositoryMark = repositoryBusy ? '<span class="spinner" aria-hidden="true"></span>' : '<span class="status-dot ' + repositoryTone + '"></span>';
|
|
417
|
+
return '<button class="mobile-nav" type="button" aria-label="Open navigation" aria-controls="sidebar-navigation" aria-expanded="false">☰</button><div><small class="eyebrow">' + esc(state.workspace.organizationName) + '</small><h1>' + esc(titleCase(title)) + '</h1></div><div class="topbar-status">' + programSelect + topbarProgramReadiness() + '<label class="search topbar-search"><span aria-hidden="true">⌕</span><input data-global-search type="search" placeholder="Search records" aria-label="Search records"><kbd>/</kbd></label><a class="validation-chip' + (validationLoading && !repositoryError ? ' is-loading' : '') + '" href="#/repository"' + (validationLoading && !repositoryError ? ' aria-busy="true"' : '') + '>' + validationMark + validationLabel + '</a><a class="repo-chip' + (repositoryBusy ? ' is-loading' : '') + '" href="#/repository"' + (repositoryBusy ? ' aria-busy="true"' : '') + '>' + repositoryMark + esc(repositoryLabel) + '</a></div>';
|
|
377
418
|
}
|
|
378
419
|
|
|
379
420
|
function topbarProgramReadiness() {
|
|
380
421
|
if (state.sections?.program !== "complete") {
|
|
381
|
-
const
|
|
382
|
-
|
|
422
|
+
const loading = state.sections?.program !== "error";
|
|
423
|
+
const label = loading ? '<span class="spinner" aria-hidden="true"></span><span class="sr-only">Loading</span>' : "View";
|
|
424
|
+
return '<a class="topbar-readiness' + (loading ? ' is-loading' : '') + '" href="#/"' + (loading ? ' aria-busy="true"' : '') + '><span class="topbar-readiness-copy"><span>Program readiness</span><strong>' + label + '</strong></span><span class="progress" aria-hidden="true"><span style="width:0%"></span></span></a>';
|
|
383
425
|
}
|
|
384
426
|
const progress = dashboardProgramReadiness(state.programReadiness);
|
|
385
427
|
const detail = progress.complete + " of " + progress.total + " readiness items complete";
|
|
@@ -394,7 +436,7 @@ function repositoryStatusTone(status) {
|
|
|
394
436
|
|
|
395
437
|
function repositorySyncAlert() {
|
|
396
438
|
if (state.repository?.status === "syncing") {
|
|
397
|
-
return '<div class="repository-sync-alert syncing" role="status" aria-live="polite"><span class="
|
|
439
|
+
return '<div class="repository-sync-alert syncing is-loading" role="status" aria-live="polite" aria-busy="true"><span class="spinner" aria-hidden="true"></span><span><strong>Saved and committed locally.</strong> Git push is continuing in the background. Other changes unlock when synchronization finishes.</span><a href="#/repository">View status</a></div>';
|
|
398
440
|
}
|
|
399
441
|
const message = state.repository?.backgroundSyncError;
|
|
400
442
|
if (!message) return "";
|
|
@@ -938,15 +980,26 @@ function openCollectionReviewDialog(type) {
|
|
|
938
980
|
const sourceType = v4 ? "component" : "system";
|
|
939
981
|
const systems = resourcesOfType(sourceType).filter(({ record }) => record.status === "active");
|
|
940
982
|
const allowedDecisions = configuration.decisions || ["complete"];
|
|
941
|
-
const
|
|
942
|
-
|
|
983
|
+
const priorDecision = assessment.review?.decision;
|
|
984
|
+
const priorAuthoritativeSourceId = assessment.review?.authoritativeComponentId || assessment.review?.authoritativeSystemId || null;
|
|
985
|
+
const priorAuthoritativeSourceActive = priorDecision !== "externally-managed"
|
|
986
|
+
|| systems.some(({ record }) => record.id === priorAuthoritativeSourceId);
|
|
987
|
+
const priorDecisionStillValid = allowedDecisions.includes(priorDecision)
|
|
988
|
+
&& priorAuthoritativeSourceActive
|
|
989
|
+
&& (assessment.recordCount ? priorDecision !== "zero-population" : priorDecision !== "complete");
|
|
990
|
+
const preservedAuthoritativeSourceId = priorDecisionStillValid && priorDecision === "externally-managed"
|
|
991
|
+
? priorAuthoritativeSourceId
|
|
992
|
+
: null;
|
|
993
|
+
const defaultDecision = priorDecisionStillValid
|
|
994
|
+
? priorDecision
|
|
995
|
+
: !assessment.recordCount && allowedDecisions.includes("zero-population") ? "zero-population" : allowedDecisions[0];
|
|
943
996
|
const decisions = allowedDecisions.map((decision) => (
|
|
944
997
|
'<option value="' + esc(decision) + '" ' + (defaultDecision === decision ? "selected" : "") + '>' + esc(properCase(decision)) + '</option>'
|
|
945
998
|
)).join("");
|
|
946
999
|
const dialog = document.createElement("dialog");
|
|
947
1000
|
dialog.className = "commit-dialog event-dialog collection-review-dialog";
|
|
948
1001
|
dialog.setAttribute("aria-labelledby", "collection-review-dialog-title");
|
|
949
|
-
dialog.innerHTML = '<form><div class="dialog-head"><div><p class="kicker">Scope confirmation</p><h2 id="collection-review-dialog-title">Confirm ' + esc(configuration.title.toLowerCase()) + '</h2></div><button type="button" class="icon-button" aria-label="Close">×</button></div><p>' + esc(configuration.description) + '</p><section class="event-dialog-steps collection-review-checks"><strong>Before confirming</strong><ul>' + configuration.reviewPoints.map((point) => '<li>' + esc(point) + '</li>').join("") + '</ul></section><div class="form-grid"><label><span>Conclusion</span><select name="decision" required>' + decisions + '</select></label><label><span>Reviewer</span><select name="reviewerId" required><option value="">Select</option>' + people.map(({ record }) => '<option value="' + esc(record.id) + '" ' + ((assessment.review?.reviewedByIds || []).includes(record.id) ? "selected" : "") + '>' + esc(record.title) + '</option>').join("") + '</select></label><label><span>Reviewed on</span><input name="reviewedOn" type="date" required value="' + esc(
|
|
1002
|
+
dialog.innerHTML = '<form><div class="dialog-head"><div><p class="kicker">Scope confirmation</p><h2 id="collection-review-dialog-title">Confirm ' + esc(configuration.title.toLowerCase()) + '</h2></div><button type="button" class="icon-button" aria-label="Close">×</button></div><p>' + esc(configuration.description) + '</p><section class="event-dialog-steps collection-review-checks"><strong>Before confirming</strong><ul>' + configuration.reviewPoints.map((point) => '<li>' + esc(point) + '</li>').join("") + '</ul></section><div class="form-grid"><label><span>Conclusion</span><select name="decision" required>' + decisions + '</select></label><label><span>Reviewer</span><select name="reviewerId" required><option value="">Select</option>' + people.map(({ record }) => '<option value="' + esc(record.id) + '" ' + ((assessment.review?.reviewedByIds || []).includes(record.id) ? "selected" : "") + '>' + esc(record.title) + '</option>').join("") + '</select></label><label><span>Reviewed on</span><input name="reviewedOn" type="date" required value="' + esc(currentDate()) + '"></label><label data-authoritative-system><span>Authoritative ' + (v4 ? "Component" : "System") + '</span><select name="authoritativeSourceId"><option value="">Select</option>' + systems.map(({ record }) => '<option value="' + esc(record.id) + '" ' + (preservedAuthoritativeSourceId === record.id ? "selected" : "") + '>' + esc(record.title) + '</option>').join("") + '</select></label><label class="full"><span>Review notes</span><textarea name="rationale" rows="3" required placeholder="Note what you confirmed and any scope decision that needs context.">' + esc(assessment.review?.rationale || "") + '</textarea></label></div><div class="workflow-preview"><strong>What this saves</strong><p>Confirm ' + assessment.recordCount + ' current ' + esc(pluralize("record", assessment.recordCount)) + '. If the collection or material scope changes, FileGRC will ask for another review.</p></div><div class="dialog-error" role="alert"></div><div class="dialog-actions"><span class="save-status review-save-status" role="status" aria-live="polite"></span><button type="button" class="button" data-event="cancel">Cancel</button><button type="submit" class="button primary">Confirm and save</button></div></form>';
|
|
950
1003
|
document.body.append(dialog);
|
|
951
1004
|
dialog.showModal();
|
|
952
1005
|
const form = dialog.querySelector("form");
|
|
@@ -981,8 +1034,10 @@ function openCollectionReviewDialog(type) {
|
|
|
981
1034
|
expectedRevision: assessment.reviewRevision || undefined
|
|
982
1035
|
};
|
|
983
1036
|
form.querySelectorAll("button,input,select,textarea").forEach((control) => { control.disabled = true; });
|
|
1037
|
+
const submit = form.querySelector('button[type="submit"]');
|
|
1038
|
+
setButtonBusy(submit, true, "Saving…");
|
|
984
1039
|
try {
|
|
985
|
-
saveStatus
|
|
1040
|
+
setLoadingContent(saveStatus, "Validating and saving…");
|
|
986
1041
|
const prefetch = await repositoryPrefetch;
|
|
987
1042
|
if (prefetch?.error) throw prefetch.error;
|
|
988
1043
|
const response = await localFetch("/api/collection-review", {
|
|
@@ -991,14 +1046,15 @@ function openCollectionReviewDialog(type) {
|
|
|
991
1046
|
body: JSON.stringify({ ...payload, confirmed: true, prefetchToken: prefetch?.token })
|
|
992
1047
|
});
|
|
993
1048
|
if (!response.ok) throw new Error(await responseMessage(response));
|
|
994
|
-
saveStatus
|
|
1049
|
+
setLoadingContent(saveStatus, "Saved locally. Refreshing page…");
|
|
995
1050
|
applyMutationState(await response.json());
|
|
996
1051
|
dialog.close();
|
|
997
1052
|
render();
|
|
998
1053
|
} catch (requestError) {
|
|
999
|
-
saveStatus
|
|
1054
|
+
clearLoadingContent(saveStatus, "Not saved");
|
|
1000
1055
|
error.textContent = requestError.message;
|
|
1001
1056
|
} finally {
|
|
1057
|
+
setButtonBusy(submit, false);
|
|
1002
1058
|
form.querySelectorAll("button,input,select,textarea").forEach((control) => { control.disabled = false; });
|
|
1003
1059
|
}
|
|
1004
1060
|
});
|
|
@@ -1909,8 +1965,7 @@ async function openObligationRuleActivation(ruleId) {
|
|
|
1909
1965
|
const approvedByIds = [...form.querySelectorAll('input[name="approver"]:checked')].map(({ value }) => value);
|
|
1910
1966
|
if (!approvedByIds.length) return void (error.textContent = "Select at least one approver.");
|
|
1911
1967
|
const button = form.querySelector('button[type="submit"]');
|
|
1912
|
-
button
|
|
1913
|
-
button.textContent = "Activating…";
|
|
1968
|
+
setButtonBusy(button, true, "Activating…");
|
|
1914
1969
|
error.textContent = "";
|
|
1915
1970
|
try {
|
|
1916
1971
|
const saved = await localFetch("/api/obligation-rule-activations", {
|
|
@@ -1931,8 +1986,7 @@ async function openObligationRuleActivation(ruleId) {
|
|
|
1931
1986
|
render();
|
|
1932
1987
|
} catch (caught) {
|
|
1933
1988
|
error.textContent = caught.message;
|
|
1934
|
-
button
|
|
1935
|
-
button.textContent = "Activate schedule";
|
|
1989
|
+
setButtonBusy(button, false);
|
|
1936
1990
|
}
|
|
1937
1991
|
});
|
|
1938
1992
|
} catch (error) {
|
|
@@ -2068,6 +2122,8 @@ function openObligationEventDialog(trigger) {
|
|
|
2068
2122
|
const form = event.currentTarget;
|
|
2069
2123
|
if (!form.reportValidity()) return;
|
|
2070
2124
|
form.querySelectorAll("button,input,select").forEach((control) => { control.disabled = true; });
|
|
2125
|
+
const submit = form.querySelector('button[type="submit"]');
|
|
2126
|
+
setButtonBusy(submit, true, "Adding tasks…");
|
|
2071
2127
|
try {
|
|
2072
2128
|
const response = await localFetch("/api/obligation-events", {
|
|
2073
2129
|
method: "POST",
|
|
@@ -2092,6 +2148,7 @@ function openObligationEventDialog(trigger) {
|
|
|
2092
2148
|
history.replaceState(null, "", "#/stage/run");
|
|
2093
2149
|
render();
|
|
2094
2150
|
} catch (error) {
|
|
2151
|
+
setButtonBusy(submit, false);
|
|
2095
2152
|
form.querySelectorAll("button,input,select").forEach((control) => { control.disabled = false; });
|
|
2096
2153
|
form.querySelector(".dialog-error").textContent = error.message;
|
|
2097
2154
|
}
|
|
@@ -2124,6 +2181,8 @@ function openExternalReviewerGovernanceDialog() {
|
|
|
2124
2181
|
const error = dialog.querySelector(".dialog-error");
|
|
2125
2182
|
error.textContent = "";
|
|
2126
2183
|
form.querySelectorAll("button,input,textarea").forEach((control) => { control.disabled = true; });
|
|
2184
|
+
const submit = form.querySelector('[data-preview-governance]');
|
|
2185
|
+
setButtonBusy(submit, true, previewedPayload ? "Applying…" : "Preparing preview…");
|
|
2127
2186
|
try {
|
|
2128
2187
|
if (!previewedPayload) {
|
|
2129
2188
|
const response = await localFetch("/api/external-reviewer-governance/preview", {
|
|
@@ -2150,6 +2209,8 @@ function openExternalReviewerGovernanceDialog() {
|
|
|
2150
2209
|
} catch (requestError) {
|
|
2151
2210
|
error.textContent = requestError.message;
|
|
2152
2211
|
} finally {
|
|
2212
|
+
setButtonBusy(submit, false);
|
|
2213
|
+
submit.textContent = previewedPayload ? "Confirm and apply" : "Preview bundle";
|
|
2153
2214
|
form.querySelectorAll("button,input,textarea").forEach((control) => { control.disabled = false; });
|
|
2154
2215
|
}
|
|
2155
2216
|
});
|
|
@@ -2186,6 +2247,8 @@ function openNextAuditCycleDialog(prior) {
|
|
|
2186
2247
|
const error = dialog.querySelector(".dialog-error");
|
|
2187
2248
|
error.textContent = "";
|
|
2188
2249
|
form.querySelectorAll("button,input").forEach((control) => { control.disabled = true; });
|
|
2250
|
+
const submit = form.querySelector("[data-preview-cycle]");
|
|
2251
|
+
setButtonBusy(submit, true, previewedPayload ? "Creating…" : "Preparing preview…");
|
|
2189
2252
|
try {
|
|
2190
2253
|
if (!previewedPayload) {
|
|
2191
2254
|
const response = await localFetch("/api/audit-cycle/preview", {
|
|
@@ -2214,6 +2277,8 @@ function openNextAuditCycleDialog(prior) {
|
|
|
2214
2277
|
} catch (requestError) {
|
|
2215
2278
|
error.textContent = requestError.message;
|
|
2216
2279
|
} finally {
|
|
2280
|
+
setButtonBusy(submit, false);
|
|
2281
|
+
submit.textContent = previewedPayload ? "Confirm and create" : "Preview carry-forward";
|
|
2217
2282
|
form.querySelectorAll("button,input").forEach((control) => { control.disabled = false; });
|
|
2218
2283
|
}
|
|
2219
2284
|
});
|
|
@@ -2303,6 +2368,8 @@ function openApplicabilityReviewDialog(type, entries) {
|
|
|
2303
2368
|
])
|
|
2304
2369
|
};
|
|
2305
2370
|
form.querySelectorAll("button,input,select").forEach((control) => { control.disabled = true; });
|
|
2371
|
+
const submit = form.querySelector("[data-preview-review]");
|
|
2372
|
+
setButtonBusy(submit, true, previewedPayload ? "Saving…" : "Preparing preview…");
|
|
2306
2373
|
try {
|
|
2307
2374
|
if (!previewedPayload) {
|
|
2308
2375
|
const response = await localFetch("/api/applicability-review/preview", {
|
|
@@ -2316,7 +2383,7 @@ function openApplicabilityReviewDialog(type, entries) {
|
|
|
2316
2383
|
dialog.querySelector(".workflow-preview").innerHTML = '<strong>Review before saving</strong><p>' + preview.reviewedIds.length + ' ' + pluralize("decision", preview.reviewedIds.length) + ' will be saved with the reviewer, review date, and current scope recorded automatically.</p>';
|
|
2317
2384
|
dialog.querySelector("[data-preview-review]").textContent = "Confirm and save";
|
|
2318
2385
|
} else {
|
|
2319
|
-
saveStatus
|
|
2386
|
+
setLoadingContent(saveStatus, "Validating and saving…");
|
|
2320
2387
|
const prefetch = await repositoryPrefetch;
|
|
2321
2388
|
if (prefetch?.error) throw prefetch.error;
|
|
2322
2389
|
const response = await localFetch("/api/applicability-review", {
|
|
@@ -2325,14 +2392,17 @@ function openApplicabilityReviewDialog(type, entries) {
|
|
|
2325
2392
|
body: JSON.stringify({ ...previewedPayload, confirmed: true, prefetchToken: prefetch?.token })
|
|
2326
2393
|
});
|
|
2327
2394
|
if (!response.ok) throw new Error(await responseMessage(response));
|
|
2328
|
-
saveStatus
|
|
2395
|
+
setLoadingContent(saveStatus, "Saved locally. Refreshing page…");
|
|
2329
2396
|
applyMutationState(await response.json());
|
|
2330
2397
|
dialog.close();
|
|
2331
2398
|
render();
|
|
2332
2399
|
}
|
|
2333
2400
|
} catch (requestError) {
|
|
2401
|
+
clearLoadingContent(saveStatus, "Not saved");
|
|
2334
2402
|
error.textContent = requestError.message;
|
|
2335
2403
|
} finally {
|
|
2404
|
+
setButtonBusy(submit, false);
|
|
2405
|
+
submit.textContent = previewedPayload ? "Confirm and save" : "Preview decisions";
|
|
2336
2406
|
form.querySelectorAll("button,input,select").forEach((control) => { control.disabled = false; });
|
|
2337
2407
|
}
|
|
2338
2408
|
});
|
|
@@ -2377,8 +2447,7 @@ function renderAuditPacket(main, params = new URLSearchParams()) {
|
|
|
2377
2447
|
main.querySelector("#initialize-audit-work")?.addEventListener("click", async (event) => {
|
|
2378
2448
|
const button = event.currentTarget;
|
|
2379
2449
|
const error = main.querySelector(".audit-preparation-error");
|
|
2380
|
-
button
|
|
2381
|
-
button.textContent = "Initializing…";
|
|
2450
|
+
setButtonBusy(button, true, "Initializing…");
|
|
2382
2451
|
error.textContent = "";
|
|
2383
2452
|
try {
|
|
2384
2453
|
const response = await localFetch("/api/audit-preparation", {
|
|
@@ -2391,8 +2460,7 @@ function renderAuditPacket(main, params = new URLSearchParams()) {
|
|
|
2391
2460
|
render();
|
|
2392
2461
|
} catch (caught) {
|
|
2393
2462
|
error.textContent = caught.message;
|
|
2394
|
-
button
|
|
2395
|
-
button.textContent = "Initialize audit work";
|
|
2463
|
+
setButtonBusy(button, false);
|
|
2396
2464
|
}
|
|
2397
2465
|
});
|
|
2398
2466
|
main.querySelector("#packet-form").addEventListener("submit", async (event) => {
|
|
@@ -2401,8 +2469,7 @@ function renderAuditPacket(main, params = new URLSearchParams()) {
|
|
|
2401
2469
|
if (!form.reportValidity()) return;
|
|
2402
2470
|
const button = form.querySelector('button[type="submit"]');
|
|
2403
2471
|
const error = main.querySelector(".dialog-error");
|
|
2404
|
-
button
|
|
2405
|
-
button.textContent = "Generating…";
|
|
2472
|
+
setButtonBusy(button, true, "Generating…");
|
|
2406
2473
|
error.textContent = "";
|
|
2407
2474
|
try {
|
|
2408
2475
|
const response = await localFetch("/api/evidence-packet", {
|
|
@@ -2423,8 +2490,8 @@ function renderAuditPacket(main, params = new URLSearchParams()) {
|
|
|
2423
2490
|
} catch (caught) {
|
|
2424
2491
|
error.textContent = caught.message;
|
|
2425
2492
|
} finally {
|
|
2493
|
+
setButtonBusy(button, false);
|
|
2426
2494
|
button.disabled = state.readOnly;
|
|
2427
|
-
button.textContent = draft ? "Generate draft" : "Generate packet";
|
|
2428
2495
|
}
|
|
2429
2496
|
});
|
|
2430
2497
|
if (latestPacketResult && latestPacketState === state && latestPacketResult.packet.audit?.id === selected?.id) {
|
|
@@ -2696,7 +2763,7 @@ function renderDetail(main, type, id, params = new URLSearchParams()) {
|
|
|
2696
2763
|
const definition = state.model.resources[type];
|
|
2697
2764
|
if (!entry || !definition) return renderNotFound(main);
|
|
2698
2765
|
if (entry.detailsLoaded === false) {
|
|
2699
|
-
main.innerHTML = '<div class="page"><div class="detail-head"><div><div class="breadcrumbs header-breadcrumbs"><a href="#/resources/' + encodeURIComponent(type) + '">' + esc(titleCase(definition.pluralTitle)) + '</a><span>/</span><span>' + esc(entry.record.title) + '</span></div><h2>' + esc(titleCase(entry.record.title)) + '</h2></div></div><section class="panel detail-loading" role="status">Loading record
|
|
2766
|
+
main.innerHTML = '<div class="page"><div class="detail-head"><div><div class="breadcrumbs header-breadcrumbs"><a href="#/resources/' + encodeURIComponent(type) + '">' + esc(titleCase(definition.pluralTitle)) + '</a><span>/</span><span>' + esc(entry.record.title) + '</span></div><h2>' + esc(titleCase(entry.record.title)) + '</h2></div></div><section class="panel detail-loading is-loading" role="status" aria-live="polite" aria-busy="true">' + loadingIndicator("Loading record…") + '</section></div>';
|
|
2700
2767
|
loadResourceDetail(type, id);
|
|
2701
2768
|
return;
|
|
2702
2769
|
}
|
|
@@ -2768,8 +2835,10 @@ function renderDetail(main, type, id, params = new URLSearchParams()) {
|
|
|
2768
2835
|
? '<section class="panel detail-history-panel"><div class="panel-head"><h3>File History</h3></div><div class="history">' + entry.history.map((commit) => '<div><code>' + esc(commit.shortCommit) + '</code><span><strong>' + esc(commit.subject) + '</strong><small>' + esc(commit.author) + ' · ' + esc(formatLocalDateTime(commit.timestamp)) + '</small></span></div>').join("") + '</div></section>'
|
|
2769
2836
|
: "";
|
|
2770
2837
|
const participationPanel = entry.historyLoaded === false
|
|
2771
|
-
? '<section class="panel detail-support-panel" role="status"><div class="panel-head"><h3>Participation</h3></div><p class="muted">Loading participation and file history
|
|
2772
|
-
:
|
|
2838
|
+
? '<section class="panel detail-support-panel" role="status" aria-live="polite" aria-busy="true"><div class="panel-head"><h3>Participation</h3></div><p class="muted is-loading">' + loadingIndicator("Loading participation and file history…") + '</p></section>'
|
|
2839
|
+
: entry.historyError
|
|
2840
|
+
? '<section class="panel detail-support-panel" role="status" aria-live="polite"><div class="panel-head"><h3>Participation</h3></div><p class="muted">Participation and file history are unavailable. Reload the record to try again.</p></section>'
|
|
2841
|
+
: personParticipation(entry);
|
|
2773
2842
|
const supportPanels = renderDetailSupport({
|
|
2774
2843
|
hasRecordBody,
|
|
2775
2844
|
workflowPanel: workflowGuidance({ type, id, title: "Next steps" }),
|
|
@@ -2996,8 +3065,8 @@ async function loadResourceHistory(type, id) {
|
|
|
2996
3065
|
const key = (state.stateToken || "live") + "\0history\0" + type + "\0" + id;
|
|
2997
3066
|
if (resourceDetailRequests.has(key)) return resourceDetailRequests.get(key);
|
|
2998
3067
|
const request = (async () => {
|
|
3068
|
+
const token = state.stateToken;
|
|
2999
3069
|
try {
|
|
3000
|
-
const token = state.stateToken;
|
|
3001
3070
|
const tokenQuery = (token ? "?token=" + encodeURIComponent(token) + "&" : "?") + "history=only";
|
|
3002
3071
|
const response = await localFetch("/api/resource/" + encodeURIComponent(type) + "/" + encodeURIComponent(id) + tokenQuery);
|
|
3003
3072
|
if (response.status === 409 && token) {
|
|
@@ -3018,8 +3087,15 @@ async function loadResourceHistory(type, id) {
|
|
|
3018
3087
|
entry.historyLoaded = true;
|
|
3019
3088
|
const route = parseRoute();
|
|
3020
3089
|
if (route.name === "detail" && route.type === type && route.id === id) render();
|
|
3021
|
-
} catch {
|
|
3022
|
-
|
|
3090
|
+
} catch (error) {
|
|
3091
|
+
if (token && state.stateToken !== token) return;
|
|
3092
|
+
const entry = state.resources.find(({ record }) => record.type === type && record.id === id);
|
|
3093
|
+
if (entry) {
|
|
3094
|
+
entry.historyLoaded = true;
|
|
3095
|
+
entry.historyError = error.message;
|
|
3096
|
+
}
|
|
3097
|
+
const route = parseRoute();
|
|
3098
|
+
if (route.name === "detail" && route.type === type && route.id === id) render();
|
|
3023
3099
|
} finally {
|
|
3024
3100
|
for (const [requestKey, pending] of resourceDetailRequests) {
|
|
3025
3101
|
if (pending === request) resourceDetailRequests.delete(requestKey);
|
|
@@ -3412,8 +3488,8 @@ async function runRepositoryGitAction(action) {
|
|
|
3412
3488
|
status.textContent = "";
|
|
3413
3489
|
status.classList.remove("error");
|
|
3414
3490
|
}
|
|
3491
|
+
if (active) setButtonBusy(active, true, label);
|
|
3415
3492
|
buttons.forEach((button) => { button.disabled = true; });
|
|
3416
|
-
if (active) active.textContent = label;
|
|
3417
3493
|
try {
|
|
3418
3494
|
const response = await localFetch("/api/git/" + action, { method: "POST" });
|
|
3419
3495
|
if (!response.ok) throw new Error(await responseMessage(response));
|
|
@@ -3429,8 +3505,8 @@ async function runRepositoryGitAction(action) {
|
|
|
3429
3505
|
? "Synchronized " + result.shortCommit + " with " + result.upstream + "."
|
|
3430
3506
|
: "Pushed " + result.shortCommit + " to " + result.upstream + ".";
|
|
3431
3507
|
} catch (cause) {
|
|
3508
|
+
if (active) setButtonBusy(active, false);
|
|
3432
3509
|
buttons.forEach((button, index) => { button.disabled = disabled[index]; });
|
|
3433
|
-
if (active) active.textContent = action === "pull" ? "Check incoming commits" : action === "retry-sync" ? "Retry sync" : "Push";
|
|
3434
3510
|
if (status) {
|
|
3435
3511
|
status.textContent = cause.message;
|
|
3436
3512
|
status.classList.add("error");
|
|
@@ -3456,8 +3532,10 @@ function openCommitDialog() {
|
|
|
3456
3532
|
const form = event.currentTarget;
|
|
3457
3533
|
if (!form.reportValidity()) return;
|
|
3458
3534
|
const button = form.querySelector('button[type="submit"]');
|
|
3459
|
-
form.querySelectorAll("button,input")
|
|
3460
|
-
|
|
3535
|
+
const controls = [...form.querySelectorAll("button,input")];
|
|
3536
|
+
const disabled = controls.map((control) => control.disabled);
|
|
3537
|
+
setButtonBusy(button, true, busyLabel);
|
|
3538
|
+
controls.forEach((control) => { control.disabled = true; });
|
|
3461
3539
|
try {
|
|
3462
3540
|
const response = await localFetch("/api/commit", {
|
|
3463
3541
|
method: "POST",
|
|
@@ -3479,8 +3557,8 @@ function openCommitDialog() {
|
|
|
3479
3557
|
status.classList.toggle("error", !result.pushed && !result.pushSkipped);
|
|
3480
3558
|
}
|
|
3481
3559
|
} catch (error) {
|
|
3482
|
-
|
|
3483
|
-
|
|
3560
|
+
setButtonBusy(button, false);
|
|
3561
|
+
controls.forEach((control, index) => { control.disabled = disabled[index]; });
|
|
3484
3562
|
form.querySelector(".dialog-error").textContent = error.message;
|
|
3485
3563
|
}
|
|
3486
3564
|
});
|
|
@@ -4061,9 +4139,11 @@ function setOnboardingBusy(busy, label = "") {
|
|
|
4061
4139
|
onboardingBusy = busy;
|
|
4062
4140
|
onboardingDialog.querySelectorAll("button,input,select,textarea").forEach((control) => { control.disabled = busy; });
|
|
4063
4141
|
const next = onboardingDialog.querySelector('[data-onboarding="next"]');
|
|
4064
|
-
|
|
4142
|
+
const draft = onboardingDialog.querySelector('[data-onboarding="draft"]');
|
|
4065
4143
|
const skip = onboardingDialog.querySelector('[data-onboarding="skip"]');
|
|
4066
|
-
|
|
4144
|
+
const retry = onboardingDialog.querySelector(".onboarding-retry-sync");
|
|
4145
|
+
const active = label === "Saving draft…" ? draft : label === "Skipping…" ? skip : label === "Retrying sync…" ? retry : next;
|
|
4146
|
+
if (busy && active && label) setLoadingContent(active, label);
|
|
4067
4147
|
if (busy && onboardingStep === onboardingSteps().length - 1) {
|
|
4068
4148
|
onboardingStillWorkingTimer = setTimeout(() => {
|
|
4069
4149
|
const status = onboardingDialog?.querySelector(".onboarding-save-status");
|
|
@@ -4743,12 +4823,8 @@ function wireStructuredObjectEditors(dialog) {
|
|
|
4743
4823
|
dialog.querySelector("form").addEventListener("click", (event) => {
|
|
4744
4824
|
const bind = event.target.closest("[data-bind-review-revisions]");
|
|
4745
4825
|
if (bind) {
|
|
4746
|
-
const previousLabel = bind.textContent;
|
|
4747
4826
|
bindCurrentReviewRevisions(dialog, bind).catch((error) => {
|
|
4748
4827
|
dialog.querySelector(".dialog-error").textContent = error.message;
|
|
4749
|
-
}).finally(() => {
|
|
4750
|
-
bind.disabled = false;
|
|
4751
|
-
if (bind.textContent === "Binding…") bind.textContent = previousLabel;
|
|
4752
4828
|
});
|
|
4753
4829
|
return;
|
|
4754
4830
|
}
|
|
@@ -4824,23 +4900,28 @@ async function bindCurrentReviewRevisions(dialog, button) {
|
|
|
4824
4900
|
throw new Error("Select the retention schedule, Information Types, and operational scope before binding their current revisions.");
|
|
4825
4901
|
}
|
|
4826
4902
|
const ids = [...new Set(Object.values(selected).flat().concat(scheduleDocumentId, retentionUseReviewIds(dialog)).filter(Boolean))];
|
|
4827
|
-
button
|
|
4828
|
-
|
|
4829
|
-
|
|
4830
|
-
|
|
4831
|
-
|
|
4832
|
-
|
|
4833
|
-
|
|
4834
|
-
|
|
4835
|
-
|
|
4836
|
-
|
|
4837
|
-
|
|
4838
|
-
|
|
4839
|
-
|
|
4840
|
-
|
|
4903
|
+
setButtonBusy(button, true, "Binding…");
|
|
4904
|
+
let bound = false;
|
|
4905
|
+
try {
|
|
4906
|
+
const query = ids.map((id) => "id=" + encodeURIComponent(id)).join("&");
|
|
4907
|
+
const response = await localFetch("/api/review-revisions?" + query);
|
|
4908
|
+
if (!response.ok) throw new Error(await responseMessage(response));
|
|
4909
|
+
const { revisions } = await response.json();
|
|
4910
|
+
const editor = button.closest(".string-map-editor");
|
|
4911
|
+
const items = editor.querySelector(".string-map-items");
|
|
4912
|
+
items.replaceChildren();
|
|
4913
|
+
for (const [key, value] of Object.entries(revisions)) {
|
|
4914
|
+
items.append(editor.querySelector("template").content.cloneNode(true));
|
|
4915
|
+
const row = items.lastElementChild;
|
|
4916
|
+
row.querySelector("[data-map-key]").value = key;
|
|
4917
|
+
row.querySelector("[data-map-value]").value = value;
|
|
4918
|
+
}
|
|
4919
|
+
bound = true;
|
|
4920
|
+
dialog.querySelector(".dialog-error").textContent = "";
|
|
4921
|
+
} finally {
|
|
4922
|
+
setButtonBusy(button, false);
|
|
4923
|
+
if (bound) button.textContent = "Refresh current revisions";
|
|
4841
4924
|
}
|
|
4842
|
-
button.textContent = "Refresh current revisions";
|
|
4843
|
-
dialog.querySelector(".dialog-error").textContent = "";
|
|
4844
4925
|
}
|
|
4845
4926
|
|
|
4846
4927
|
function retentionUseReviewIds(dialog) {
|
|
@@ -5687,16 +5768,16 @@ function prefetchRepositoryForReview(status = null) {
|
|
|
5687
5768
|
if (state.repository?.mode !== "trunk" || state.repository?.developmentOverride) {
|
|
5688
5769
|
return Promise.resolve(null);
|
|
5689
5770
|
}
|
|
5690
|
-
if (status) status
|
|
5771
|
+
if (status) setLoadingContent(status, "Checking repository…");
|
|
5691
5772
|
return fetch("/api/git/prefetch", { method: "POST" })
|
|
5692
5773
|
.then(async (response) => {
|
|
5693
5774
|
if (!response.ok) throw new Error(await responseMessage(response));
|
|
5694
5775
|
const result = await response.json();
|
|
5695
|
-
if (status) status
|
|
5776
|
+
if (status) clearLoadingContent(status, result.status === "checked" ? "Repository checked" : "Ready to save");
|
|
5696
5777
|
return result;
|
|
5697
5778
|
})
|
|
5698
5779
|
.catch((error) => {
|
|
5699
|
-
if (status) status
|
|
5780
|
+
if (status) clearLoadingContent(status, "Repository check failed");
|
|
5700
5781
|
return null;
|
|
5701
5782
|
});
|
|
5702
5783
|
}
|
|
@@ -5745,7 +5826,10 @@ function setMutationBusy(dialog, busy, label, idleLabel) {
|
|
|
5745
5826
|
clearTimeout(dialog._stillWorkingTimer);
|
|
5746
5827
|
dialog._stillWorkingTimer = null;
|
|
5747
5828
|
dialog.dataset.mutationBusy = busy ? "true" : "false";
|
|
5829
|
+
const button = dialog.querySelector("#save-record,#save-content,#activate-policy") || dialog.querySelector('button[type="submit"]');
|
|
5830
|
+
if (busy && button) setButtonBusy(button, true, label);
|
|
5748
5831
|
dialog.querySelectorAll("button,input,select,textarea").forEach((control) => {
|
|
5832
|
+
if (control === button) return;
|
|
5749
5833
|
if (busy) {
|
|
5750
5834
|
control.dataset.mutationWasDisabled = control.disabled ? "true" : "false";
|
|
5751
5835
|
control.disabled = true;
|
|
@@ -5754,13 +5838,15 @@ function setMutationBusy(dialog, busy, label, idleLabel) {
|
|
|
5754
5838
|
delete control.dataset.mutationWasDisabled;
|
|
5755
5839
|
}
|
|
5756
5840
|
});
|
|
5757
|
-
|
|
5758
|
-
|
|
5841
|
+
if (!busy && button) {
|
|
5842
|
+
setButtonBusy(button, false);
|
|
5843
|
+
if (idleLabel) button.textContent = idleLabel;
|
|
5844
|
+
}
|
|
5759
5845
|
const status = dialog.querySelector(".save-status");
|
|
5760
|
-
if (status) status
|
|
5846
|
+
if (status) clearLoadingContent(status);
|
|
5761
5847
|
if (busy) {
|
|
5762
5848
|
dialog._stillWorkingTimer = setTimeout(() => {
|
|
5763
|
-
if (status) status
|
|
5849
|
+
if (status) setLoadingContent(status, "Still working. Recalculating readiness and repository state.");
|
|
5764
5850
|
}, 1_500);
|
|
5765
5851
|
}
|
|
5766
5852
|
}
|
|
@@ -5804,6 +5890,8 @@ async function responseMessage(response) {
|
|
|
5804
5890
|
try { return JSON.parse(source).error || source; } catch { return source; }
|
|
5805
5891
|
}
|
|
5806
5892
|
async function localFetch(url, options) {
|
|
5893
|
+
const requestProgramId = state?.selectedProgramId || null;
|
|
5894
|
+
const requestProgramGeneration = programSelectionGeneration;
|
|
5807
5895
|
const requestUrl = new URL(url, location.origin);
|
|
5808
5896
|
if (state?.selectedProgramId && !requestUrl.searchParams.has("programId")) {
|
|
5809
5897
|
requestUrl.searchParams.set("programId", state.selectedProgramId);
|
|
@@ -5826,17 +5914,24 @@ async function localFetch(url, options) {
|
|
|
5826
5914
|
const chip = synchronizing ? document.querySelector(".repo-chip") : null;
|
|
5827
5915
|
const previousChip = chip?.innerHTML;
|
|
5828
5916
|
let repositoryRefreshed = false;
|
|
5829
|
-
if (chip)
|
|
5917
|
+
if (chip) {
|
|
5918
|
+
chip.classList.add("is-loading");
|
|
5919
|
+
chip.setAttribute("aria-busy", "true");
|
|
5920
|
+
chip.innerHTML = loadingIndicator("Syncing");
|
|
5921
|
+
}
|
|
5830
5922
|
try {
|
|
5831
5923
|
const response = await fetch(scopedUrl, requestOptions);
|
|
5832
5924
|
if (synchronizing && !response.ok) {
|
|
5833
5925
|
try {
|
|
5834
|
-
const stateResponse = await fetch("/api/state?programId=" + encodeURIComponent(
|
|
5926
|
+
const stateResponse = await fetch("/api/state?programId=" + encodeURIComponent(requestProgramId || ""));
|
|
5835
5927
|
if (stateResponse.ok) {
|
|
5836
|
-
|
|
5928
|
+
const refreshedState = normalizeAppState(await stateResponse.json());
|
|
5929
|
+
if (programSelectionGeneration !== requestProgramGeneration || (state?.selectedProgramId || null) !== requestProgramId) return response;
|
|
5930
|
+
state = refreshedState;
|
|
5931
|
+
repositoryRefreshed = true;
|
|
5932
|
+
scheduleRepositorySyncPoll();
|
|
5837
5933
|
if (chip?.isConnected) {
|
|
5838
5934
|
chip.innerHTML = '<span class="status-dot ' + repositoryStatusTone(state.repository.status) + '"></span>' + esc(state.repository.label);
|
|
5839
|
-
repositoryRefreshed = true;
|
|
5840
5935
|
}
|
|
5841
5936
|
}
|
|
5842
5937
|
} catch {
|
|
@@ -5847,7 +5942,11 @@ async function localFetch(url, options) {
|
|
|
5847
5942
|
} catch {
|
|
5848
5943
|
throw new Error("The filegrc server is unavailable. Restart npm run serve, or pnpm dev in the monorepo, and try again.");
|
|
5849
5944
|
} finally {
|
|
5850
|
-
if (
|
|
5945
|
+
if (chip?.isConnected) {
|
|
5946
|
+
chip.classList.remove("is-loading");
|
|
5947
|
+
chip.removeAttribute("aria-busy");
|
|
5948
|
+
if (!repositoryRefreshed && previousChip) chip.innerHTML = previousChip;
|
|
5949
|
+
}
|
|
5851
5950
|
}
|
|
5852
5951
|
}
|
|
5853
5952
|
async function fetchJson(url, options) { const response = await localFetch(url, options); if (!response.ok) throw new Error(await responseMessage(response)); return response.json(); }
|
|
@@ -5856,9 +5955,11 @@ function esc(value) { return String(value ?? "").replace(/[&<>"']/g, (character)
|
|
|
5856
5955
|
|
|
5857
5956
|
export const APP_STYLES = String.raw`
|
|
5858
5957
|
:root{--ink:#151827;--muted:#5d6475;--line:#dfe3ef;--paper:#f6f7fb;--panel:#fff;--accent:#0000a5;--accent-soft:#eef1ff;--accent-light:#8aa1ff;--focus:#0000e0;--amber:#8a5200;--red:#a13a31;--sidebar:linear-gradient(135deg,#000070 0%,#000035 60%);--primary-gradient:linear-gradient(135deg,#000070 0%,#000035 60%);--surface-soft:#f2f4fa;--surface-muted:#eceff7;--field:#fff;--field-readonly:#eef0f6;--code-bg:#10162b;--code-ink:#e8ebff;--shadow:0 8px 28px rgba(0,0,53,.08);color-scheme:light dark;font-family:Inter,ui-sans-serif,system-ui,-apple-system,BlinkMacSystemFont,"Segoe UI",sans-serif;color:var(--ink);background:var(--paper);font-synthesis:none}
|
|
5859
|
-
*{box-sizing:border-box}body{margin:0;min-width:320px;background:var(--paper)}button,input,select,textarea{font:inherit}a{color:inherit}.skip-link{position:fixed;left:1rem;top:-4rem;z-index:100;padding:.7rem 1rem;background:#fff}.skip-link:focus{top:1rem}.loading,.fatal{padding:3rem}.shell{display:grid;grid-template-columns:248px 1fr;min-height:100vh}.sidebar{position:fixed;inset:0 auto 0 0;width:248px;background:var(--sidebar);color:#eef1ff;padding:25px 18px 18px;overflow:auto;z-index:20}.brand{display:flex;align-items:center;gap:12px;text-decoration:none;margin:0 7px 27px}.brand .mark{display:block;width:39px;height:39px;border-radius:10px}.brand strong,.brand small{display:block}.brand strong{color:#fff;font-size:18px}.brand small{font-size:13.2px;color:#c5cae2;margin-top:2px}.nav-home,.nav-items a{display:flex;justify-content:space-between;align-items:center;text-decoration:none;border-radius:7px;padding:8px 10px;font-size:15.6px;color:#d5d9ed}.nav-home{margin-bottom:9px}.nav-home:hover,.nav-items a:hover,.nav-home.current,.nav-items a.current{background:#202066;color:#fff}.nav-heading{width:100%;border:0;background:none;color:#b4bbdc;text-transform:uppercase;letter-spacing:.11em;font-size:12px;font-weight:750;display:flex;align-items:center;justify-content:space-between;padding:13px 10px 5px;cursor:pointer}.chevron{display:grid;place-items:center;width:14px;height:22px;font-size:0;line-height:1;transform:none}.chevron:before{content:"";width:6px;height:6px;border-right:1.5px solid currentColor;border-bottom:1.5px solid currentColor;transform:rotate(-45deg);transform-origin:center;transition:transform .15s}.nav-items{display:none}.nav-group.open .nav-items{display:block}.nav-items small{font-size:12px;color:#b8bed7}.side-foot{position:sticky;bottom:-18px;margin:25px -18px -18px;padding:17px 25px;background:#000024;border-top:1px solid #34345f;color:#cbd0e5;font-size:13.2px;display:flex;align-items:center;gap:8px}.status-dot{width:8px;height:8px;border-radius:50%;background:#9aa39f;display:inline-block;flex:0 0 auto}.status-dot.good,.badge.good{background:#6abf8c}.status-dot.warn,.badge.warn{background:#e9a445}.status-dot.bad,.badge.bad{background:#dc6c5d}.status-dot.neutral{background:#9aabff}.workspace{grid-column:2;min-width:0}.topbar{height:86px;background:rgba(255,255,255,.88);backdrop-filter:blur(10px);border-bottom:1px solid var(--line);padding:0 32px;display:flex;align-items:center;gap:23px;position:sticky;top:0;z-index:10}.topbar>div:first-of-type{min-width:190px}.topbar h1{font-size:20.4px;line-height:1.1;margin:3px 0 0}.eyebrow,.kicker{color:var(--accent);text-transform:uppercase;letter-spacing:.12em;font-weight:760;font-size:10.8px;margin:0}.search{height:39px;max-width:240px;flex:1;margin-left:auto;display:flex;align-items:center;gap:9px;background:#f2f4fa;border:1px solid #dfe3ef;border-radius:8px;padding:0 10px;color:#5d6475}.search input{border:0;outline:0;background:none;min-width:0;flex:1;font-size:15.6px}.search kbd{background:#fff;border:1px solid #dfe3ef;border-radius:4px;padding:1px 5px;font-size:12px}.mobile-sidebar-search{display:none}.repo-chip{display:flex;align-items:center;gap:8px;border:1px solid var(--line);border-radius:8px;padding:10px 12px;color:var(--muted);font-size:13.2px;white-space:nowrap;text-decoration:none}.mobile-nav{display:none}.page{padding:30px 34px 70px;max-width:1510px;margin:auto}.hero{color:#f8f9ff;background:linear-gradient(120deg,#000070,#000035);border-radius:13px;padding:28px 31px;display:flex;justify-content:space-between;align-items:end;min-height:158px;box-shadow:var(--shadow);position:relative;overflow:hidden}.hero:after{content:"";position:absolute;width:270px;height:270px;border:55px solid rgba(138,161,255,.1);border-radius:50%;right:-80px;top:-145px}.hero .kicker{color:#cbd3ff}.hero h2{font-family:Georgia,serif;font-weight:500;font-size:33.6px;margin:10px 0 8px;letter-spacing:-.02em}.hero p:not(.kicker){margin:0;color:#dde1f4;font-size:15.6px;max-width:650px}.hero-meta{display:flex;gap:15px;position:relative;z-index:1}.hero-meta span{font-size:12px;text-transform:uppercase;letter-spacing:.08em;color:#e6e8f7;border-left:1px solid #6874ab;padding-left:15px}.metrics{display:grid;grid-template-columns:repeat(4,1fr);gap:12px;margin:14px 0}.metric{background:#fff;border:1px solid var(--line);border-radius:10px;padding:16px 18px;box-shadow:0 2px 8px rgba(21,40,33,.025)}.metric-label{font-size:12px;text-transform:uppercase;letter-spacing:.08em;color:var(--muted);display:flex;align-items:center;gap:7px}.metric>strong{display:block;font-family:Georgia,serif;font-size:30px;font-weight:500;margin:8px 0 2px}.metric>small{font-size:12px;color:#697184}.dashboard-grid{display:grid;grid-template-columns:repeat(3,1fr);gap:14px}.panel{background:#fff;border:1px solid var(--line);border-radius:11px;padding:21px;min-width:0;box-shadow:0 2px 8px rgba(21,40,33,.025)}.span-2{grid-column:span 2}.panel-head{display:flex;align-items:start;justify-content:space-between;gap:15px;margin-bottom:18px}.panel-head h3{font-size:16.8px;margin:4px 0 0}.panel-head>a{font-size:13.2px;color:var(--accent);font-weight:700}.audit-progress{display:grid;grid-template-columns:105px 1fr;gap:11px 20px;align-items:end}.progress-number strong{font-family:Georgia,serif;font-size:36px;font-weight:500;display:block}.progress-number span{font-size:12px;color:var(--muted)}.progress{height:9px;background:#eceff7;border-radius:9px;overflow:hidden}.progress span{display:block;height:100%;background:linear-gradient(90deg,#0000a5,var(--accent-light));border-radius:9px}.progress-meta{grid-column:2;display:flex;justify-content:space-between;font-size:10.8px;text-transform:uppercase;letter-spacing:.08em;color:#5d6475}.due-list{display:grid}.due-list a{display:grid;grid-template-columns:60px 1fr;text-decoration:none;border-top:1px solid #e8ebf3;padding:10px 0;align-items:center}.due-list a:first-child{border:0;padding-top:0}.due-list time{font-size:12px;color:var(--accent);font-weight:750}.due-list strong,.due-list small{display:block}.due-list strong{font-size:13.2px}.due-list small{font-size:10.8px;color:var(--muted);margin-top:3px}.resource-bars{display:grid;gap:11px}.resource-bars a{display:grid;grid-template-columns:105px 1fr 20px;gap:9px;align-items:center;text-decoration:none;font-size:12px}.resource-bars i{height:5px;background:#e8ebf3;border-radius:5px;overflow:hidden}.resource-bars b{display:block;height:100%;background:#6676dd;border-radius:5px}.resource-bars strong{text-align:right}.catalog{display:grid;grid-template-columns:repeat(4,1fr);gap:7px}.catalog a{display:flex;justify-content:space-between;text-decoration:none;padding:9px 11px;background:#f2f4fa;border-radius:6px;font-size:12px}.catalog a:hover{background:var(--accent-soft)}.page-intro{display:flex;justify-content:space-between;align-items:end;margin-bottom:25px}.page-intro h2,.detail-head h2{font-family:Georgia,serif;font-size:37.2px;font-weight:500;margin:7px 0}.page-intro p:not(.kicker){color:var(--muted);max-width:700px;font-size:15.6px;margin:0}.button{border:1px solid #d0d5e3;background:#fff;border-radius:7px;padding:9px 13px;cursor:pointer;font-size:14.4px;font-weight:650}.button.primary{background:var(--accent);border-color:var(--accent);color:#fff}.button.danger{color:var(--red)}.list-tools{display:flex;align-items:center;gap:10px;margin-bottom:12px}.list-tools label{flex:1}.list-tools input,.list-tools select{width:100%;border:1px solid var(--line);border-radius:7px;background:#fff;padding:10px 12px;font-size:14.4px}.list-tools select{width:auto}.list-tools>span{color:var(--muted);font-size:12px}.record-table-wrap{background:#fff;border:1px solid var(--line);border-radius:10px;overflow:auto}.record-table{width:100%;border-collapse:collapse;font-size:13.2px}.record-table th{background:#f2f4fa;text-align:left;text-transform:uppercase;letter-spacing:.08em;color:#75817b;font-size:10.8px;padding:11px 14px;border-bottom:1px solid var(--line)}.record-table td{padding:13px 14px;border-bottom:1px solid #e8ebf3;vertical-align:top}.record-table tr:last-child td{border-bottom:0}.record-table code{font-size:10.8px;color:#5d6475}.record-title{display:block;color:var(--ink);font-weight:700;text-decoration:none}.record-table td>small{display:block;color:#6a7181;margin-top:3px}.record-table td[data-label="Description"]{min-width:260px;max-width:520px;color:var(--muted);line-height:1.45}.badge,.tag,.type-pill{display:inline-block;border-radius:99px;background:#eceff7;padding:3px 7px;font-size:10.8px;text-transform:uppercase;letter-spacing:.05em;white-space:nowrap}.tag{text-transform:none;margin:1px}.badge.status-active,.badge.status-approved,.badge.status-complete,.badge.status-passed,.badge.status-accepted{background:#ddefe5;color:#176143}.badge.status-open,.badge.status-high,.badge.status-critical,.badge.status-failed{background:#f5ded9;color:#8d352c}.badge.status-draft,.badge.status-planned,.badge.status-in-progress,.badge.status-medium{background:#f7e9cf;color:#855717}.breadcrumbs{display:flex;gap:8px;color:var(--muted);font-size:13.2px;margin-bottom:20px}.detail-head{display:flex;justify-content:space-between;align-items:end;margin-bottom:22px}.detail-head h2{margin-bottom:4px}.detail-head>div>code{font-size:12px;color:var(--muted)}.actions{display:flex;gap:7px}.detail-grid{display:grid;grid-template-columns:minmax(0,2fr) minmax(270px,1fr);gap:14px}.detail-grid aside{display:grid;gap:14px;align-content:start}.detail-main{padding:29px}.content-label{color:#75817b;text-transform:uppercase;letter-spacing:.08em;font-size:10.8px;border-bottom:1px solid var(--line);padding-bottom:13px;margin-bottom:23px}.markdown{max-width:790px}.markdown h1{font-family:Georgia,serif;font-size:34.8px;font-weight:500}.markdown h2{font-family:Georgia,serif;font-size:27.6px;font-weight:500;margin-top:1.8em}.markdown h3{font-size:18px;margin-top:1.7em}.markdown p,.markdown li{font-size:15.6px;line-height:1.65;color:#272c3b}.markdown code{background:#eef0f6;border-radius:3px;padding:1px 4px}.markdown pre{padding:15px;background:#10162b;color:#e8ebff;border-radius:7px;overflow:auto}.markdown blockquote{border-left:3px solid var(--accent-light);padding:4px 15px;color:var(--muted);margin-left:0}.table-wrap{overflow:auto}.markdown table{border-collapse:collapse;width:100%;font-size:13.2px}.markdown th,.markdown td{border:1px solid var(--line);padding:8px;text-align:left}.metadata{margin:0}.metadata>div{display:grid;grid-template-columns:105px 1fr;gap:10px;border-top:1px solid #e8ebf3;padding:10px 0}.metadata>div:first-child{border-top:0;padding-top:0}.metadata dt{font-size:10.8px;text-transform:uppercase;letter-spacing:.06em;color:#5d6475}.metadata dd{margin:0;font-size:13.2px;min-width:0}.compact-json{white-space:pre-wrap;font-size:10.8px}.git-panel>code{font-size:10.8px;word-break:break-all}.git-panel p{font-size:12px;color:var(--muted)}.relation{color:var(--accent);text-decoration:none}.history{display:grid}.history>div{display:grid;grid-template-columns:60px 1fr;gap:8px;padding:8px 0;border-top:1px solid #e8ebf3}.history>div:first-child{border-top:0}.history code{font-size:10.8px;color:var(--accent)}.history strong,.history small{display:block}.history strong{font-size:12px}.history small{font-size:10.8px;color:var(--muted);margin-top:2px}.empty{padding:25px;color:#697184;text-align:center;font-size:13.2px;background:#f4f5fa;border-radius:7px}.changes{padding-left:18px}.changes li{margin:8px 0}.diagnostics>div{display:grid;grid-template-columns:58px minmax(120px,180px) minmax(0,1fr);gap:10px;align-items:start;border-top:1px solid var(--line);padding:10px 0}.diagnostics p{margin:0;font-size:13.2px;overflow-wrap:anywhere}.diagnostics code{font-size:10.8px;overflow-wrap:anywhere}.editor,.search-results{width:min(760px,calc(100vw - 30px));border:0;border-radius:12px;padding:0;box-shadow:0 25px 80px rgba(0,0,24,.28)}dialog::backdrop{background:rgba(0,0,24,.55)}.editor form,.search-results{padding:23px}.dialog-head{display:flex;justify-content:space-between;align-items:start}.dialog-head h2{font-family:Georgia,serif;font-weight:500;margin:5px 0 0}.icon-button{border:0;background:#eceff7;width:32px;height:32px;border-radius:50%;font-size:26.4px;cursor:pointer}.editor form>p{font-size:13.2px;color:var(--muted)}.editor textarea{width:100%;height:440px;border:1px solid var(--line);border-radius:7px;background:#10162b;color:#e8ebff;padding:15px;font:13.2px/1.5 ui-monospace,SFMono-Regular,Menlo,monospace;tab-size:2}.dialog-actions{display:flex;justify-content:end;gap:8px;margin-top:14px}.dialog-error{color:var(--red);font-size:13.2px;min-height:18px;margin-top:7px}.result-list{display:grid;margin-top:17px;max-height:60vh;overflow:auto}.result-list a{display:block;text-decoration:none;padding:11px;border-top:1px solid var(--line)}.result-list strong,.result-list small{display:block}.result-list small{color:var(--muted);margin-top:3px}.muted{color:#737a8b}.sr-only{position:absolute;width:1px;height:1px;padding:0;margin:-1px;overflow:hidden;clip:rect(0,0,0,0);white-space:nowrap;border:0}
|
|
5958
|
+
*{box-sizing:border-box}body{margin:0;min-width:320px;background:var(--paper)}button,input,select,textarea{font:inherit}a{color:inherit}@keyframes spinner-rotate{to{transform:rotate(360deg)}}.spinner{display:inline-block;flex:0 0 auto;width:1em;height:1em;border:2px solid currentColor;border-right-color:transparent;border-radius:50%;animation:spinner-rotate .7s linear infinite}.spinner-large{width:30px;height:30px;border-width:3px}.loading,.detail-loading,.muted.is-loading,.save-status.is-loading,.onboarding-save-status.is-loading,.button.is-loading{display:flex;align-items:center;gap:9px}.loading{min-height:100vh;justify-content:center;color:var(--muted)}.state-loading{display:flex;align-items:flex-start;gap:16px}.state-loading>.spinner{margin-top:5px;color:var(--accent)}.state-loading h2{margin:5px 0 8px}.state-loading p:last-child{margin:0;color:var(--muted)}.skip-link{position:fixed;left:1rem;top:-4rem;z-index:100;padding:.7rem 1rem;background:#fff}.skip-link:focus{top:1rem}.loading,.fatal{padding:3rem}.shell{display:grid;grid-template-columns:248px 1fr;min-height:100vh}.sidebar{position:fixed;inset:0 auto 0 0;width:248px;background:var(--sidebar);color:#eef1ff;padding:25px 18px 18px;overflow:auto;z-index:20}.brand{display:flex;align-items:center;gap:12px;text-decoration:none;margin:0 7px 27px}.brand .mark{display:block;width:39px;height:39px;border-radius:10px}.brand strong,.brand small{display:block}.brand strong{color:#fff;font-size:18px}.brand small{font-size:13.2px;color:#c5cae2;margin-top:2px}.nav-home,.nav-items a{display:flex;justify-content:space-between;align-items:center;text-decoration:none;border-radius:7px;padding:8px 10px;font-size:15.6px;color:#d5d9ed}.nav-home{margin-bottom:9px}.nav-home:hover,.nav-items a:hover,.nav-home.current,.nav-items a.current{background:#202066;color:#fff}.nav-heading{width:100%;border:0;background:none;color:#b4bbdc;text-transform:uppercase;letter-spacing:.11em;font-size:12px;font-weight:750;display:flex;align-items:center;justify-content:space-between;padding:13px 10px 5px;cursor:pointer}.chevron{display:grid;place-items:center;width:14px;height:22px;font-size:0;line-height:1;transform:none}.chevron:before{content:"";width:6px;height:6px;border-right:1.5px solid currentColor;border-bottom:1.5px solid currentColor;transform:rotate(-45deg);transform-origin:center;transition:transform .15s}.nav-items{display:none}.nav-group.open .nav-items{display:block}.nav-items small{font-size:12px;color:#b8bed7}.side-foot{position:sticky;bottom:-18px;margin:25px -18px -18px;padding:17px 25px;background:#000024;border-top:1px solid #34345f;color:#cbd0e5;font-size:13.2px;display:flex;align-items:center;gap:8px}.status-dot{width:8px;height:8px;border-radius:50%;background:#9aa39f;display:inline-block;flex:0 0 auto}.status-dot.good,.badge.good{background:#6abf8c}.status-dot.warn,.badge.warn{background:#e9a445}.status-dot.bad,.badge.bad{background:#dc6c5d}.status-dot.neutral{background:#9aabff}.workspace{grid-column:2;min-width:0}.topbar{height:86px;background:rgba(255,255,255,.88);backdrop-filter:blur(10px);border-bottom:1px solid var(--line);padding:0 32px;display:flex;align-items:center;gap:23px;position:sticky;top:0;z-index:10}.topbar>div:first-of-type{min-width:190px}.topbar h1{font-size:20.4px;line-height:1.1;margin:3px 0 0}.eyebrow,.kicker{color:var(--accent);text-transform:uppercase;letter-spacing:.12em;font-weight:760;font-size:10.8px;margin:0}.search{height:39px;max-width:240px;flex:1;margin-left:auto;display:flex;align-items:center;gap:9px;background:#f2f4fa;border:1px solid #dfe3ef;border-radius:8px;padding:0 10px;color:#5d6475}.search input{border:0;outline:0;background:none;min-width:0;flex:1;font-size:15.6px}.search kbd{background:#fff;border:1px solid #dfe3ef;border-radius:4px;padding:1px 5px;font-size:12px}.mobile-sidebar-search{display:none}.repo-chip{display:flex;align-items:center;gap:8px;border:1px solid var(--line);border-radius:8px;padding:10px 12px;color:var(--muted);font-size:13.2px;white-space:nowrap;text-decoration:none}.mobile-nav{display:none}.page{padding:30px 34px 70px;max-width:1510px;margin:auto}.hero{color:#f8f9ff;background:linear-gradient(120deg,#000070,#000035);border-radius:13px;padding:28px 31px;display:flex;justify-content:space-between;align-items:end;min-height:158px;box-shadow:var(--shadow);position:relative;overflow:hidden}.hero:after{content:"";position:absolute;width:270px;height:270px;border:55px solid rgba(138,161,255,.1);border-radius:50%;right:-80px;top:-145px}.hero .kicker{color:#cbd3ff}.hero h2{font-family:Georgia,serif;font-weight:500;font-size:33.6px;margin:10px 0 8px;letter-spacing:-.02em}.hero p:not(.kicker){margin:0;color:#dde1f4;font-size:15.6px;max-width:650px}.hero-meta{display:flex;gap:15px;position:relative;z-index:1}.hero-meta span{font-size:12px;text-transform:uppercase;letter-spacing:.08em;color:#e6e8f7;border-left:1px solid #6874ab;padding-left:15px}.metrics{display:grid;grid-template-columns:repeat(4,1fr);gap:12px;margin:14px 0}.metric{background:#fff;border:1px solid var(--line);border-radius:10px;padding:16px 18px;box-shadow:0 2px 8px rgba(21,40,33,.025)}.metric-label{font-size:12px;text-transform:uppercase;letter-spacing:.08em;color:var(--muted);display:flex;align-items:center;gap:7px}.metric>strong{display:block;font-family:Georgia,serif;font-size:30px;font-weight:500;margin:8px 0 2px}.metric>small{font-size:12px;color:#697184}.dashboard-grid{display:grid;grid-template-columns:repeat(3,1fr);gap:14px}.panel{background:#fff;border:1px solid var(--line);border-radius:11px;padding:21px;min-width:0;box-shadow:0 2px 8px rgba(21,40,33,.025)}.span-2{grid-column:span 2}.panel-head{display:flex;align-items:start;justify-content:space-between;gap:15px;margin-bottom:18px}.panel-head h3{font-size:16.8px;margin:4px 0 0}.panel-head>a{font-size:13.2px;color:var(--accent);font-weight:700}.audit-progress{display:grid;grid-template-columns:105px 1fr;gap:11px 20px;align-items:end}.progress-number strong{font-family:Georgia,serif;font-size:36px;font-weight:500;display:block}.progress-number span{font-size:12px;color:var(--muted)}.progress{height:9px;background:#eceff7;border-radius:9px;overflow:hidden}.progress span{display:block;height:100%;background:linear-gradient(90deg,#0000a5,var(--accent-light));border-radius:9px}.progress-meta{grid-column:2;display:flex;justify-content:space-between;font-size:10.8px;text-transform:uppercase;letter-spacing:.08em;color:#5d6475}.due-list{display:grid}.due-list a{display:grid;grid-template-columns:60px 1fr;text-decoration:none;border-top:1px solid #e8ebf3;padding:10px 0;align-items:center}.due-list a:first-child{border:0;padding-top:0}.due-list time{font-size:12px;color:var(--accent);font-weight:750}.due-list strong,.due-list small{display:block}.due-list strong{font-size:13.2px}.due-list small{font-size:10.8px;color:var(--muted);margin-top:3px}.resource-bars{display:grid;gap:11px}.resource-bars a{display:grid;grid-template-columns:105px 1fr 20px;gap:9px;align-items:center;text-decoration:none;font-size:12px}.resource-bars i{height:5px;background:#e8ebf3;border-radius:5px;overflow:hidden}.resource-bars b{display:block;height:100%;background:#6676dd;border-radius:5px}.resource-bars strong{text-align:right}.catalog{display:grid;grid-template-columns:repeat(4,1fr);gap:7px}.catalog a{display:flex;justify-content:space-between;text-decoration:none;padding:9px 11px;background:#f2f4fa;border-radius:6px;font-size:12px}.catalog a:hover{background:var(--accent-soft)}.page-intro{display:flex;justify-content:space-between;align-items:end;margin-bottom:25px}.page-intro h2,.detail-head h2{font-family:Georgia,serif;font-size:37.2px;font-weight:500;margin:7px 0}.page-intro p:not(.kicker){color:var(--muted);max-width:700px;font-size:15.6px;margin:0}.button{border:1px solid #d0d5e3;background:#fff;border-radius:7px;padding:9px 13px;cursor:pointer;font-size:14.4px;font-weight:650}.button.primary{background:var(--accent);border-color:var(--accent);color:#fff}.button.danger{color:var(--red)}.list-tools{display:flex;align-items:center;gap:10px;margin-bottom:12px}.list-tools label{flex:1}.list-tools input,.list-tools select{width:100%;border:1px solid var(--line);border-radius:7px;background:#fff;padding:10px 12px;font-size:14.4px}.list-tools select{width:auto}.list-tools>span{color:var(--muted);font-size:12px}.record-table-wrap{background:#fff;border:1px solid var(--line);border-radius:10px;overflow:auto}.record-table{width:100%;border-collapse:collapse;font-size:13.2px}.record-table th{background:#f2f4fa;text-align:left;text-transform:uppercase;letter-spacing:.08em;color:#75817b;font-size:10.8px;padding:11px 14px;border-bottom:1px solid var(--line)}.record-table td{padding:13px 14px;border-bottom:1px solid #e8ebf3;vertical-align:top}.record-table tr:last-child td{border-bottom:0}.record-table code{font-size:10.8px;color:#5d6475}.record-title{display:block;color:var(--ink);font-weight:700;text-decoration:none}.record-table td>small{display:block;color:#6a7181;margin-top:3px}.record-table td[data-label="Description"]{min-width:260px;max-width:520px;color:var(--muted);line-height:1.45}.badge,.tag,.type-pill{display:inline-block;border-radius:99px;background:#eceff7;padding:3px 7px;font-size:10.8px;text-transform:uppercase;letter-spacing:.05em;white-space:nowrap}.tag{text-transform:none;margin:1px}.badge.status-active,.badge.status-approved,.badge.status-complete,.badge.status-passed,.badge.status-accepted{background:#ddefe5;color:#176143}.badge.status-open,.badge.status-high,.badge.status-critical,.badge.status-failed{background:#f5ded9;color:#8d352c}.badge.status-draft,.badge.status-planned,.badge.status-in-progress,.badge.status-medium{background:#f7e9cf;color:#855717}.breadcrumbs{display:flex;gap:8px;color:var(--muted);font-size:13.2px;margin-bottom:20px}.detail-head{display:flex;justify-content:space-between;align-items:end;margin-bottom:22px}.detail-head h2{margin-bottom:4px}.detail-head>div>code{font-size:12px;color:var(--muted)}.actions{display:flex;gap:7px}.detail-grid{display:grid;grid-template-columns:minmax(0,2fr) minmax(270px,1fr);gap:14px}.detail-grid aside{display:grid;gap:14px;align-content:start}.detail-main{padding:29px}.content-label{color:#75817b;text-transform:uppercase;letter-spacing:.08em;font-size:10.8px;border-bottom:1px solid var(--line);padding-bottom:13px;margin-bottom:23px}.markdown{max-width:790px}.markdown h1{font-family:Georgia,serif;font-size:34.8px;font-weight:500}.markdown h2{font-family:Georgia,serif;font-size:27.6px;font-weight:500;margin-top:1.8em}.markdown h3{font-size:18px;margin-top:1.7em}.markdown p,.markdown li{font-size:15.6px;line-height:1.65;color:#272c3b}.markdown code{background:#eef0f6;border-radius:3px;padding:1px 4px}.markdown pre{padding:15px;background:#10162b;color:#e8ebff;border-radius:7px;overflow:auto}.markdown blockquote{border-left:3px solid var(--accent-light);padding:4px 15px;color:var(--muted);margin-left:0}.table-wrap{overflow:auto}.markdown table{border-collapse:collapse;width:100%;font-size:13.2px}.markdown th,.markdown td{border:1px solid var(--line);padding:8px;text-align:left}.metadata{margin:0}.metadata>div{display:grid;grid-template-columns:105px 1fr;gap:10px;border-top:1px solid #e8ebf3;padding:10px 0}.metadata>div:first-child{border-top:0;padding-top:0}.metadata dt{font-size:10.8px;text-transform:uppercase;letter-spacing:.06em;color:#5d6475}.metadata dd{margin:0;font-size:13.2px;min-width:0}.compact-json{white-space:pre-wrap;font-size:10.8px}.git-panel>code{font-size:10.8px;word-break:break-all}.git-panel p{font-size:12px;color:var(--muted)}.relation{color:var(--accent);text-decoration:none}.history{display:grid}.history>div{display:grid;grid-template-columns:60px 1fr;gap:8px;padding:8px 0;border-top:1px solid #e8ebf3}.history>div:first-child{border-top:0}.history code{font-size:10.8px;color:var(--accent)}.history strong,.history small{display:block}.history strong{font-size:12px}.history small{font-size:10.8px;color:var(--muted);margin-top:2px}.empty{padding:25px;color:#697184;text-align:center;font-size:13.2px;background:#f4f5fa;border-radius:7px}.changes{padding-left:18px}.changes li{margin:8px 0}.diagnostics>div{display:grid;grid-template-columns:58px minmax(120px,180px) minmax(0,1fr);gap:10px;align-items:start;border-top:1px solid var(--line);padding:10px 0}.diagnostics p{margin:0;font-size:13.2px;overflow-wrap:anywhere}.diagnostics code{font-size:10.8px;overflow-wrap:anywhere}.editor,.search-results{width:min(760px,calc(100vw - 30px));border:0;border-radius:12px;padding:0;box-shadow:0 25px 80px rgba(0,0,24,.28)}dialog::backdrop{background:rgba(0,0,24,.55)}.editor form,.search-results{padding:23px}.dialog-head{display:flex;justify-content:space-between;align-items:start}.dialog-head h2{font-family:Georgia,serif;font-weight:500;margin:5px 0 0}.icon-button{border:0;background:#eceff7;width:32px;height:32px;border-radius:50%;font-size:26.4px;cursor:pointer}.editor form>p{font-size:13.2px;color:var(--muted)}.editor textarea{width:100%;height:440px;border:1px solid var(--line);border-radius:7px;background:#10162b;color:#e8ebff;padding:15px;font:13.2px/1.5 ui-monospace,SFMono-Regular,Menlo,monospace;tab-size:2px}.dialog-actions{display:flex;justify-content:end;gap:8px;margin-top:14px}.dialog-error{color:var(--red);font-size:13.2px;min-height:18px;margin-top:7px}.result-list{display:grid;margin-top:17px;max-height:60vh;overflow:auto}.result-list a{display:block;text-decoration:none;padding:11px;border-top:1px solid var(--line)}.result-list strong,.result-list small{display:block}.result-list small{color:var(--muted);margin-top:3px}.muted{color:#737a8b}.sr-only{position:absolute;width:1px;height:1px;padding:0;margin:-1px;overflow:hidden;clip:rect(0,0,0,0);white-space:nowrap;border:0}
|
|
5860
5959
|
.topbar-readiness{display:grid;grid-template-columns:minmax(0,1fr);gap:3px;flex:0 1 220px;min-width:155px;padding:5px 8px;border:1px solid transparent;border-radius:8px;color:var(--ink);text-decoration:none}.topbar-readiness:hover{border-color:var(--accent-light);background:var(--accent-soft)}.topbar-readiness-copy{display:grid;grid-template-columns:minmax(0,1fr) auto;gap:7px;align-items:center}.topbar-readiness-copy>span{overflow:hidden;color:var(--muted);font-size:9.6px;font-weight:700;text-overflow:ellipsis;text-transform:uppercase;letter-spacing:.08em;white-space:nowrap}.topbar-readiness-copy>strong{font-size:10.8px;line-height:1}.topbar-readiness>.progress{width:100%;height:5px}.topbar-status{display:flex;flex:1 1 auto;min-width:0;align-items:center;justify-content:flex-end;gap:8px;margin-left:auto}.program-select select{max-width:180px;border:1px solid var(--line);border-radius:8px;padding:9px 28px 9px 10px;background:var(--surface);color:var(--ink);font:inherit}.topbar-status .topbar-search{flex:0 1 240px;width:240px;min-width:140px;margin-left:0}.repo-chip,.validation-chip{display:flex;align-items:center;gap:8px;border:1px solid var(--line);border-radius:8px;padding:10px 12px;color:var(--muted);font-size:13.2px;white-space:nowrap;text-decoration:none}.repo-chip:hover,.validation-chip:hover{color:var(--ink);border-color:var(--accent-light)}
|
|
5861
|
-
.metadata>div{grid-template-columns:minmax(140px,1fr) minmax(0,2.5fr)}.metadata dt{min-width:0;overflow-wrap:anywhere}
|
|
5960
|
+
.metadata>div{grid-template-columns:minmax(140px,1fr) minmax(0,2.5fr)}.metadata dt{min-width:0;overflow-wrap:anywhere}.editor textarea{tab-size:2}
|
|
5961
|
+
.is-loading>.spinner,.state-loading>.spinner{align-self:center;margin-top:0}.state-loading,.repository-sync-alert.is-loading{align-items:center}.topbar-readiness.is-loading .topbar-readiness-copy>strong{display:flex;align-items:center;justify-content:center}
|
|
5962
|
+
@media(prefers-reduced-motion:reduce){.spinner{animation:none}}
|
|
5862
5963
|
.audit-progress-empty{padding:11px 13px;border-radius:8px;background:var(--surface-soft)}.audit-progress-empty strong,.audit-progress-empty span{display:block}.audit-progress-empty strong{font-size:13.2px}.audit-progress-empty span{margin-top:4px;color:var(--muted);font-size:10.8px}
|
|
5863
5964
|
.icon-button{position:relative;display:grid;place-items:center;padding:0;color:var(--ink);font-size:0}.icon-button:before,.icon-button:after{content:"";position:absolute;width:13px;height:2px;border-radius:2px;background:currentColor;transform:rotate(45deg)}.icon-button:after{transform:rotate(-45deg)}
|
|
5864
5965
|
html,body{height:100%;overflow:hidden}.shell{grid-template-columns:248px minmax(0,1fr);height:100vh;min-height:0}.sidebar{display:flex;flex-direction:column;height:100vh;overflow:hidden;overscroll-behavior:contain}.workspace{height:100vh;overflow-x:hidden;overflow-y:auto;overscroll-behavior:contain;-webkit-overflow-scrolling:touch}
|