filegrc 0.13.2 → 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 +1 -1
- package/src/web.js +160 -70
package/package.json
CHANGED
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 "";
|
|
@@ -992,8 +1034,10 @@ function openCollectionReviewDialog(type) {
|
|
|
992
1034
|
expectedRevision: assessment.reviewRevision || undefined
|
|
993
1035
|
};
|
|
994
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…");
|
|
995
1039
|
try {
|
|
996
|
-
saveStatus
|
|
1040
|
+
setLoadingContent(saveStatus, "Validating and saving…");
|
|
997
1041
|
const prefetch = await repositoryPrefetch;
|
|
998
1042
|
if (prefetch?.error) throw prefetch.error;
|
|
999
1043
|
const response = await localFetch("/api/collection-review", {
|
|
@@ -1002,14 +1046,15 @@ function openCollectionReviewDialog(type) {
|
|
|
1002
1046
|
body: JSON.stringify({ ...payload, confirmed: true, prefetchToken: prefetch?.token })
|
|
1003
1047
|
});
|
|
1004
1048
|
if (!response.ok) throw new Error(await responseMessage(response));
|
|
1005
|
-
saveStatus
|
|
1049
|
+
setLoadingContent(saveStatus, "Saved locally. Refreshing page…");
|
|
1006
1050
|
applyMutationState(await response.json());
|
|
1007
1051
|
dialog.close();
|
|
1008
1052
|
render();
|
|
1009
1053
|
} catch (requestError) {
|
|
1010
|
-
saveStatus
|
|
1054
|
+
clearLoadingContent(saveStatus, "Not saved");
|
|
1011
1055
|
error.textContent = requestError.message;
|
|
1012
1056
|
} finally {
|
|
1057
|
+
setButtonBusy(submit, false);
|
|
1013
1058
|
form.querySelectorAll("button,input,select,textarea").forEach((control) => { control.disabled = false; });
|
|
1014
1059
|
}
|
|
1015
1060
|
});
|
|
@@ -1920,8 +1965,7 @@ async function openObligationRuleActivation(ruleId) {
|
|
|
1920
1965
|
const approvedByIds = [...form.querySelectorAll('input[name="approver"]:checked')].map(({ value }) => value);
|
|
1921
1966
|
if (!approvedByIds.length) return void (error.textContent = "Select at least one approver.");
|
|
1922
1967
|
const button = form.querySelector('button[type="submit"]');
|
|
1923
|
-
button
|
|
1924
|
-
button.textContent = "Activating…";
|
|
1968
|
+
setButtonBusy(button, true, "Activating…");
|
|
1925
1969
|
error.textContent = "";
|
|
1926
1970
|
try {
|
|
1927
1971
|
const saved = await localFetch("/api/obligation-rule-activations", {
|
|
@@ -1942,8 +1986,7 @@ async function openObligationRuleActivation(ruleId) {
|
|
|
1942
1986
|
render();
|
|
1943
1987
|
} catch (caught) {
|
|
1944
1988
|
error.textContent = caught.message;
|
|
1945
|
-
button
|
|
1946
|
-
button.textContent = "Activate schedule";
|
|
1989
|
+
setButtonBusy(button, false);
|
|
1947
1990
|
}
|
|
1948
1991
|
});
|
|
1949
1992
|
} catch (error) {
|
|
@@ -2079,6 +2122,8 @@ function openObligationEventDialog(trigger) {
|
|
|
2079
2122
|
const form = event.currentTarget;
|
|
2080
2123
|
if (!form.reportValidity()) return;
|
|
2081
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…");
|
|
2082
2127
|
try {
|
|
2083
2128
|
const response = await localFetch("/api/obligation-events", {
|
|
2084
2129
|
method: "POST",
|
|
@@ -2103,6 +2148,7 @@ function openObligationEventDialog(trigger) {
|
|
|
2103
2148
|
history.replaceState(null, "", "#/stage/run");
|
|
2104
2149
|
render();
|
|
2105
2150
|
} catch (error) {
|
|
2151
|
+
setButtonBusy(submit, false);
|
|
2106
2152
|
form.querySelectorAll("button,input,select").forEach((control) => { control.disabled = false; });
|
|
2107
2153
|
form.querySelector(".dialog-error").textContent = error.message;
|
|
2108
2154
|
}
|
|
@@ -2135,6 +2181,8 @@ function openExternalReviewerGovernanceDialog() {
|
|
|
2135
2181
|
const error = dialog.querySelector(".dialog-error");
|
|
2136
2182
|
error.textContent = "";
|
|
2137
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…");
|
|
2138
2186
|
try {
|
|
2139
2187
|
if (!previewedPayload) {
|
|
2140
2188
|
const response = await localFetch("/api/external-reviewer-governance/preview", {
|
|
@@ -2161,6 +2209,8 @@ function openExternalReviewerGovernanceDialog() {
|
|
|
2161
2209
|
} catch (requestError) {
|
|
2162
2210
|
error.textContent = requestError.message;
|
|
2163
2211
|
} finally {
|
|
2212
|
+
setButtonBusy(submit, false);
|
|
2213
|
+
submit.textContent = previewedPayload ? "Confirm and apply" : "Preview bundle";
|
|
2164
2214
|
form.querySelectorAll("button,input,textarea").forEach((control) => { control.disabled = false; });
|
|
2165
2215
|
}
|
|
2166
2216
|
});
|
|
@@ -2197,6 +2247,8 @@ function openNextAuditCycleDialog(prior) {
|
|
|
2197
2247
|
const error = dialog.querySelector(".dialog-error");
|
|
2198
2248
|
error.textContent = "";
|
|
2199
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…");
|
|
2200
2252
|
try {
|
|
2201
2253
|
if (!previewedPayload) {
|
|
2202
2254
|
const response = await localFetch("/api/audit-cycle/preview", {
|
|
@@ -2225,6 +2277,8 @@ function openNextAuditCycleDialog(prior) {
|
|
|
2225
2277
|
} catch (requestError) {
|
|
2226
2278
|
error.textContent = requestError.message;
|
|
2227
2279
|
} finally {
|
|
2280
|
+
setButtonBusy(submit, false);
|
|
2281
|
+
submit.textContent = previewedPayload ? "Confirm and create" : "Preview carry-forward";
|
|
2228
2282
|
form.querySelectorAll("button,input").forEach((control) => { control.disabled = false; });
|
|
2229
2283
|
}
|
|
2230
2284
|
});
|
|
@@ -2314,6 +2368,8 @@ function openApplicabilityReviewDialog(type, entries) {
|
|
|
2314
2368
|
])
|
|
2315
2369
|
};
|
|
2316
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…");
|
|
2317
2373
|
try {
|
|
2318
2374
|
if (!previewedPayload) {
|
|
2319
2375
|
const response = await localFetch("/api/applicability-review/preview", {
|
|
@@ -2327,7 +2383,7 @@ function openApplicabilityReviewDialog(type, entries) {
|
|
|
2327
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>';
|
|
2328
2384
|
dialog.querySelector("[data-preview-review]").textContent = "Confirm and save";
|
|
2329
2385
|
} else {
|
|
2330
|
-
saveStatus
|
|
2386
|
+
setLoadingContent(saveStatus, "Validating and saving…");
|
|
2331
2387
|
const prefetch = await repositoryPrefetch;
|
|
2332
2388
|
if (prefetch?.error) throw prefetch.error;
|
|
2333
2389
|
const response = await localFetch("/api/applicability-review", {
|
|
@@ -2336,14 +2392,17 @@ function openApplicabilityReviewDialog(type, entries) {
|
|
|
2336
2392
|
body: JSON.stringify({ ...previewedPayload, confirmed: true, prefetchToken: prefetch?.token })
|
|
2337
2393
|
});
|
|
2338
2394
|
if (!response.ok) throw new Error(await responseMessage(response));
|
|
2339
|
-
saveStatus
|
|
2395
|
+
setLoadingContent(saveStatus, "Saved locally. Refreshing page…");
|
|
2340
2396
|
applyMutationState(await response.json());
|
|
2341
2397
|
dialog.close();
|
|
2342
2398
|
render();
|
|
2343
2399
|
}
|
|
2344
2400
|
} catch (requestError) {
|
|
2401
|
+
clearLoadingContent(saveStatus, "Not saved");
|
|
2345
2402
|
error.textContent = requestError.message;
|
|
2346
2403
|
} finally {
|
|
2404
|
+
setButtonBusy(submit, false);
|
|
2405
|
+
submit.textContent = previewedPayload ? "Confirm and save" : "Preview decisions";
|
|
2347
2406
|
form.querySelectorAll("button,input,select").forEach((control) => { control.disabled = false; });
|
|
2348
2407
|
}
|
|
2349
2408
|
});
|
|
@@ -2388,8 +2447,7 @@ function renderAuditPacket(main, params = new URLSearchParams()) {
|
|
|
2388
2447
|
main.querySelector("#initialize-audit-work")?.addEventListener("click", async (event) => {
|
|
2389
2448
|
const button = event.currentTarget;
|
|
2390
2449
|
const error = main.querySelector(".audit-preparation-error");
|
|
2391
|
-
button
|
|
2392
|
-
button.textContent = "Initializing…";
|
|
2450
|
+
setButtonBusy(button, true, "Initializing…");
|
|
2393
2451
|
error.textContent = "";
|
|
2394
2452
|
try {
|
|
2395
2453
|
const response = await localFetch("/api/audit-preparation", {
|
|
@@ -2402,8 +2460,7 @@ function renderAuditPacket(main, params = new URLSearchParams()) {
|
|
|
2402
2460
|
render();
|
|
2403
2461
|
} catch (caught) {
|
|
2404
2462
|
error.textContent = caught.message;
|
|
2405
|
-
button
|
|
2406
|
-
button.textContent = "Initialize audit work";
|
|
2463
|
+
setButtonBusy(button, false);
|
|
2407
2464
|
}
|
|
2408
2465
|
});
|
|
2409
2466
|
main.querySelector("#packet-form").addEventListener("submit", async (event) => {
|
|
@@ -2412,8 +2469,7 @@ function renderAuditPacket(main, params = new URLSearchParams()) {
|
|
|
2412
2469
|
if (!form.reportValidity()) return;
|
|
2413
2470
|
const button = form.querySelector('button[type="submit"]');
|
|
2414
2471
|
const error = main.querySelector(".dialog-error");
|
|
2415
|
-
button
|
|
2416
|
-
button.textContent = "Generating…";
|
|
2472
|
+
setButtonBusy(button, true, "Generating…");
|
|
2417
2473
|
error.textContent = "";
|
|
2418
2474
|
try {
|
|
2419
2475
|
const response = await localFetch("/api/evidence-packet", {
|
|
@@ -2434,8 +2490,8 @@ function renderAuditPacket(main, params = new URLSearchParams()) {
|
|
|
2434
2490
|
} catch (caught) {
|
|
2435
2491
|
error.textContent = caught.message;
|
|
2436
2492
|
} finally {
|
|
2493
|
+
setButtonBusy(button, false);
|
|
2437
2494
|
button.disabled = state.readOnly;
|
|
2438
|
-
button.textContent = draft ? "Generate draft" : "Generate packet";
|
|
2439
2495
|
}
|
|
2440
2496
|
});
|
|
2441
2497
|
if (latestPacketResult && latestPacketState === state && latestPacketResult.packet.audit?.id === selected?.id) {
|
|
@@ -2707,7 +2763,7 @@ function renderDetail(main, type, id, params = new URLSearchParams()) {
|
|
|
2707
2763
|
const definition = state.model.resources[type];
|
|
2708
2764
|
if (!entry || !definition) return renderNotFound(main);
|
|
2709
2765
|
if (entry.detailsLoaded === false) {
|
|
2710
|
-
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>';
|
|
2711
2767
|
loadResourceDetail(type, id);
|
|
2712
2768
|
return;
|
|
2713
2769
|
}
|
|
@@ -2779,8 +2835,10 @@ function renderDetail(main, type, id, params = new URLSearchParams()) {
|
|
|
2779
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>'
|
|
2780
2836
|
: "";
|
|
2781
2837
|
const participationPanel = entry.historyLoaded === false
|
|
2782
|
-
? '<section class="panel detail-support-panel" role="status"><div class="panel-head"><h3>Participation</h3></div><p class="muted">Loading participation and file history
|
|
2783
|
-
:
|
|
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);
|
|
2784
2842
|
const supportPanels = renderDetailSupport({
|
|
2785
2843
|
hasRecordBody,
|
|
2786
2844
|
workflowPanel: workflowGuidance({ type, id, title: "Next steps" }),
|
|
@@ -3007,8 +3065,8 @@ async function loadResourceHistory(type, id) {
|
|
|
3007
3065
|
const key = (state.stateToken || "live") + "\0history\0" + type + "\0" + id;
|
|
3008
3066
|
if (resourceDetailRequests.has(key)) return resourceDetailRequests.get(key);
|
|
3009
3067
|
const request = (async () => {
|
|
3068
|
+
const token = state.stateToken;
|
|
3010
3069
|
try {
|
|
3011
|
-
const token = state.stateToken;
|
|
3012
3070
|
const tokenQuery = (token ? "?token=" + encodeURIComponent(token) + "&" : "?") + "history=only";
|
|
3013
3071
|
const response = await localFetch("/api/resource/" + encodeURIComponent(type) + "/" + encodeURIComponent(id) + tokenQuery);
|
|
3014
3072
|
if (response.status === 409 && token) {
|
|
@@ -3029,8 +3087,15 @@ async function loadResourceHistory(type, id) {
|
|
|
3029
3087
|
entry.historyLoaded = true;
|
|
3030
3088
|
const route = parseRoute();
|
|
3031
3089
|
if (route.name === "detail" && route.type === type && route.id === id) render();
|
|
3032
|
-
} catch {
|
|
3033
|
-
|
|
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();
|
|
3034
3099
|
} finally {
|
|
3035
3100
|
for (const [requestKey, pending] of resourceDetailRequests) {
|
|
3036
3101
|
if (pending === request) resourceDetailRequests.delete(requestKey);
|
|
@@ -3423,8 +3488,8 @@ async function runRepositoryGitAction(action) {
|
|
|
3423
3488
|
status.textContent = "";
|
|
3424
3489
|
status.classList.remove("error");
|
|
3425
3490
|
}
|
|
3491
|
+
if (active) setButtonBusy(active, true, label);
|
|
3426
3492
|
buttons.forEach((button) => { button.disabled = true; });
|
|
3427
|
-
if (active) active.textContent = label;
|
|
3428
3493
|
try {
|
|
3429
3494
|
const response = await localFetch("/api/git/" + action, { method: "POST" });
|
|
3430
3495
|
if (!response.ok) throw new Error(await responseMessage(response));
|
|
@@ -3440,8 +3505,8 @@ async function runRepositoryGitAction(action) {
|
|
|
3440
3505
|
? "Synchronized " + result.shortCommit + " with " + result.upstream + "."
|
|
3441
3506
|
: "Pushed " + result.shortCommit + " to " + result.upstream + ".";
|
|
3442
3507
|
} catch (cause) {
|
|
3508
|
+
if (active) setButtonBusy(active, false);
|
|
3443
3509
|
buttons.forEach((button, index) => { button.disabled = disabled[index]; });
|
|
3444
|
-
if (active) active.textContent = action === "pull" ? "Check incoming commits" : action === "retry-sync" ? "Retry sync" : "Push";
|
|
3445
3510
|
if (status) {
|
|
3446
3511
|
status.textContent = cause.message;
|
|
3447
3512
|
status.classList.add("error");
|
|
@@ -3467,8 +3532,10 @@ function openCommitDialog() {
|
|
|
3467
3532
|
const form = event.currentTarget;
|
|
3468
3533
|
if (!form.reportValidity()) return;
|
|
3469
3534
|
const button = form.querySelector('button[type="submit"]');
|
|
3470
|
-
form.querySelectorAll("button,input")
|
|
3471
|
-
|
|
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; });
|
|
3472
3539
|
try {
|
|
3473
3540
|
const response = await localFetch("/api/commit", {
|
|
3474
3541
|
method: "POST",
|
|
@@ -3490,8 +3557,8 @@ function openCommitDialog() {
|
|
|
3490
3557
|
status.classList.toggle("error", !result.pushed && !result.pushSkipped);
|
|
3491
3558
|
}
|
|
3492
3559
|
} catch (error) {
|
|
3493
|
-
|
|
3494
|
-
|
|
3560
|
+
setButtonBusy(button, false);
|
|
3561
|
+
controls.forEach((control, index) => { control.disabled = disabled[index]; });
|
|
3495
3562
|
form.querySelector(".dialog-error").textContent = error.message;
|
|
3496
3563
|
}
|
|
3497
3564
|
});
|
|
@@ -4072,9 +4139,11 @@ function setOnboardingBusy(busy, label = "") {
|
|
|
4072
4139
|
onboardingBusy = busy;
|
|
4073
4140
|
onboardingDialog.querySelectorAll("button,input,select,textarea").forEach((control) => { control.disabled = busy; });
|
|
4074
4141
|
const next = onboardingDialog.querySelector('[data-onboarding="next"]');
|
|
4075
|
-
|
|
4142
|
+
const draft = onboardingDialog.querySelector('[data-onboarding="draft"]');
|
|
4076
4143
|
const skip = onboardingDialog.querySelector('[data-onboarding="skip"]');
|
|
4077
|
-
|
|
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);
|
|
4078
4147
|
if (busy && onboardingStep === onboardingSteps().length - 1) {
|
|
4079
4148
|
onboardingStillWorkingTimer = setTimeout(() => {
|
|
4080
4149
|
const status = onboardingDialog?.querySelector(".onboarding-save-status");
|
|
@@ -4754,12 +4823,8 @@ function wireStructuredObjectEditors(dialog) {
|
|
|
4754
4823
|
dialog.querySelector("form").addEventListener("click", (event) => {
|
|
4755
4824
|
const bind = event.target.closest("[data-bind-review-revisions]");
|
|
4756
4825
|
if (bind) {
|
|
4757
|
-
const previousLabel = bind.textContent;
|
|
4758
4826
|
bindCurrentReviewRevisions(dialog, bind).catch((error) => {
|
|
4759
4827
|
dialog.querySelector(".dialog-error").textContent = error.message;
|
|
4760
|
-
}).finally(() => {
|
|
4761
|
-
bind.disabled = false;
|
|
4762
|
-
if (bind.textContent === "Binding…") bind.textContent = previousLabel;
|
|
4763
4828
|
});
|
|
4764
4829
|
return;
|
|
4765
4830
|
}
|
|
@@ -4835,23 +4900,28 @@ async function bindCurrentReviewRevisions(dialog, button) {
|
|
|
4835
4900
|
throw new Error("Select the retention schedule, Information Types, and operational scope before binding their current revisions.");
|
|
4836
4901
|
}
|
|
4837
4902
|
const ids = [...new Set(Object.values(selected).flat().concat(scheduleDocumentId, retentionUseReviewIds(dialog)).filter(Boolean))];
|
|
4838
|
-
button
|
|
4839
|
-
|
|
4840
|
-
|
|
4841
|
-
|
|
4842
|
-
|
|
4843
|
-
|
|
4844
|
-
|
|
4845
|
-
|
|
4846
|
-
|
|
4847
|
-
|
|
4848
|
-
|
|
4849
|
-
|
|
4850
|
-
|
|
4851
|
-
|
|
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";
|
|
4852
4924
|
}
|
|
4853
|
-
button.textContent = "Refresh current revisions";
|
|
4854
|
-
dialog.querySelector(".dialog-error").textContent = "";
|
|
4855
4925
|
}
|
|
4856
4926
|
|
|
4857
4927
|
function retentionUseReviewIds(dialog) {
|
|
@@ -5698,16 +5768,16 @@ function prefetchRepositoryForReview(status = null) {
|
|
|
5698
5768
|
if (state.repository?.mode !== "trunk" || state.repository?.developmentOverride) {
|
|
5699
5769
|
return Promise.resolve(null);
|
|
5700
5770
|
}
|
|
5701
|
-
if (status) status
|
|
5771
|
+
if (status) setLoadingContent(status, "Checking repository…");
|
|
5702
5772
|
return fetch("/api/git/prefetch", { method: "POST" })
|
|
5703
5773
|
.then(async (response) => {
|
|
5704
5774
|
if (!response.ok) throw new Error(await responseMessage(response));
|
|
5705
5775
|
const result = await response.json();
|
|
5706
|
-
if (status) status
|
|
5776
|
+
if (status) clearLoadingContent(status, result.status === "checked" ? "Repository checked" : "Ready to save");
|
|
5707
5777
|
return result;
|
|
5708
5778
|
})
|
|
5709
5779
|
.catch((error) => {
|
|
5710
|
-
if (status) status
|
|
5780
|
+
if (status) clearLoadingContent(status, "Repository check failed");
|
|
5711
5781
|
return null;
|
|
5712
5782
|
});
|
|
5713
5783
|
}
|
|
@@ -5756,7 +5826,10 @@ function setMutationBusy(dialog, busy, label, idleLabel) {
|
|
|
5756
5826
|
clearTimeout(dialog._stillWorkingTimer);
|
|
5757
5827
|
dialog._stillWorkingTimer = null;
|
|
5758
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);
|
|
5759
5831
|
dialog.querySelectorAll("button,input,select,textarea").forEach((control) => {
|
|
5832
|
+
if (control === button) return;
|
|
5760
5833
|
if (busy) {
|
|
5761
5834
|
control.dataset.mutationWasDisabled = control.disabled ? "true" : "false";
|
|
5762
5835
|
control.disabled = true;
|
|
@@ -5765,13 +5838,15 @@ function setMutationBusy(dialog, busy, label, idleLabel) {
|
|
|
5765
5838
|
delete control.dataset.mutationWasDisabled;
|
|
5766
5839
|
}
|
|
5767
5840
|
});
|
|
5768
|
-
|
|
5769
|
-
|
|
5841
|
+
if (!busy && button) {
|
|
5842
|
+
setButtonBusy(button, false);
|
|
5843
|
+
if (idleLabel) button.textContent = idleLabel;
|
|
5844
|
+
}
|
|
5770
5845
|
const status = dialog.querySelector(".save-status");
|
|
5771
|
-
if (status) status
|
|
5846
|
+
if (status) clearLoadingContent(status);
|
|
5772
5847
|
if (busy) {
|
|
5773
5848
|
dialog._stillWorkingTimer = setTimeout(() => {
|
|
5774
|
-
if (status) status
|
|
5849
|
+
if (status) setLoadingContent(status, "Still working. Recalculating readiness and repository state.");
|
|
5775
5850
|
}, 1_500);
|
|
5776
5851
|
}
|
|
5777
5852
|
}
|
|
@@ -5815,6 +5890,8 @@ async function responseMessage(response) {
|
|
|
5815
5890
|
try { return JSON.parse(source).error || source; } catch { return source; }
|
|
5816
5891
|
}
|
|
5817
5892
|
async function localFetch(url, options) {
|
|
5893
|
+
const requestProgramId = state?.selectedProgramId || null;
|
|
5894
|
+
const requestProgramGeneration = programSelectionGeneration;
|
|
5818
5895
|
const requestUrl = new URL(url, location.origin);
|
|
5819
5896
|
if (state?.selectedProgramId && !requestUrl.searchParams.has("programId")) {
|
|
5820
5897
|
requestUrl.searchParams.set("programId", state.selectedProgramId);
|
|
@@ -5837,17 +5914,24 @@ async function localFetch(url, options) {
|
|
|
5837
5914
|
const chip = synchronizing ? document.querySelector(".repo-chip") : null;
|
|
5838
5915
|
const previousChip = chip?.innerHTML;
|
|
5839
5916
|
let repositoryRefreshed = false;
|
|
5840
|
-
if (chip)
|
|
5917
|
+
if (chip) {
|
|
5918
|
+
chip.classList.add("is-loading");
|
|
5919
|
+
chip.setAttribute("aria-busy", "true");
|
|
5920
|
+
chip.innerHTML = loadingIndicator("Syncing");
|
|
5921
|
+
}
|
|
5841
5922
|
try {
|
|
5842
5923
|
const response = await fetch(scopedUrl, requestOptions);
|
|
5843
5924
|
if (synchronizing && !response.ok) {
|
|
5844
5925
|
try {
|
|
5845
|
-
const stateResponse = await fetch("/api/state?programId=" + encodeURIComponent(
|
|
5926
|
+
const stateResponse = await fetch("/api/state?programId=" + encodeURIComponent(requestProgramId || ""));
|
|
5846
5927
|
if (stateResponse.ok) {
|
|
5847
|
-
|
|
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();
|
|
5848
5933
|
if (chip?.isConnected) {
|
|
5849
5934
|
chip.innerHTML = '<span class="status-dot ' + repositoryStatusTone(state.repository.status) + '"></span>' + esc(state.repository.label);
|
|
5850
|
-
repositoryRefreshed = true;
|
|
5851
5935
|
}
|
|
5852
5936
|
}
|
|
5853
5937
|
} catch {
|
|
@@ -5858,7 +5942,11 @@ async function localFetch(url, options) {
|
|
|
5858
5942
|
} catch {
|
|
5859
5943
|
throw new Error("The filegrc server is unavailable. Restart npm run serve, or pnpm dev in the monorepo, and try again.");
|
|
5860
5944
|
} finally {
|
|
5861
|
-
if (
|
|
5945
|
+
if (chip?.isConnected) {
|
|
5946
|
+
chip.classList.remove("is-loading");
|
|
5947
|
+
chip.removeAttribute("aria-busy");
|
|
5948
|
+
if (!repositoryRefreshed && previousChip) chip.innerHTML = previousChip;
|
|
5949
|
+
}
|
|
5862
5950
|
}
|
|
5863
5951
|
}
|
|
5864
5952
|
async function fetchJson(url, options) { const response = await localFetch(url, options); if (!response.ok) throw new Error(await responseMessage(response)); return response.json(); }
|
|
@@ -5867,9 +5955,11 @@ function esc(value) { return String(value ?? "").replace(/[&<>"']/g, (character)
|
|
|
5867
5955
|
|
|
5868
5956
|
export const APP_STYLES = String.raw`
|
|
5869
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}
|
|
5870
|
-
*{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}
|
|
5871
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)}
|
|
5872
|
-
.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}}
|
|
5873
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}
|
|
5874
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)}
|
|
5875
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}
|