filegrc 0.5.1 → 0.6.0
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/README.md +14 -6
- package/model/index.js +4 -4
- package/model/v4.json +10052 -0
- package/package.json +1 -1
- package/src/agent.js +17 -5
- package/src/audit-preparation.js +17 -13
- package/src/audit-transition.js +7 -4
- package/src/batch-review.js +31 -8
- package/src/cli.js +43 -25
- package/src/collection-review.js +71 -25
- package/src/evidence-packet.js +76 -43
- package/src/external-reviewer.js +4 -4
- package/src/files.js +70 -6
- package/src/git.js +70 -13
- package/src/index.js +1 -0
- package/src/model-migration.js +631 -14
- package/src/obligations.js +14 -7
- package/src/program-path.js +49 -38
- package/src/program-readiness.js +94 -54
- package/src/program.js +52 -0
- package/src/reconciliation.js +2 -2
- package/src/server.js +50 -8
- package/src/setup.js +56 -21
- package/src/source-coverage.js +13 -9
- package/src/state.js +17 -14
- package/src/timing.js +6 -0
- package/src/validate.js +100 -9
- package/src/web.js +690 -139
- package/src/workflow.js +47 -25
package/src/web.js
CHANGED
|
@@ -81,12 +81,15 @@ let onboardingShade = null;
|
|
|
81
81
|
let onboardingStep = 0;
|
|
82
82
|
let onboardingDraft = null;
|
|
83
83
|
let onboardingBusy = false;
|
|
84
|
+
let onboardingSetupOnly = false;
|
|
84
85
|
let onboardingStillWorkingTimer = null;
|
|
85
86
|
let onboardingPendingDraft = false;
|
|
86
87
|
const resourceDetailRequests = new Map();
|
|
87
88
|
let resourceGuideCleanup = null;
|
|
88
89
|
let repositorySyncPollTimer = null;
|
|
89
90
|
let repositorySyncPollInFlight = false;
|
|
91
|
+
let mutationStateRefreshInFlight = false;
|
|
92
|
+
let mutationStateRefreshTimer = null;
|
|
90
93
|
|
|
91
94
|
start().catch((error) => {
|
|
92
95
|
root.innerHTML = '<main class="fatal"><h1>Could Not Load the Workspace</h1><pre></pre></main>';
|
|
@@ -101,7 +104,7 @@ async function start() {
|
|
|
101
104
|
window.addEventListener("scroll", positionCurrentOnboarding, true);
|
|
102
105
|
render();
|
|
103
106
|
scheduleRepositorySyncPoll();
|
|
104
|
-
if (!state.readOnly && rendererSettingsEntry()?.record.showOnboarding === true) {
|
|
107
|
+
if (!state.readOnly && rendererSettingsEntry()?.record.showOnboarding === true && !initialSetupSystem()) {
|
|
105
108
|
queueMicrotask(requestOnboarding);
|
|
106
109
|
}
|
|
107
110
|
}
|
|
@@ -207,6 +210,16 @@ function readinessStageForType(type) {
|
|
|
207
210
|
));
|
|
208
211
|
}
|
|
209
212
|
|
|
213
|
+
function activeProgram() {
|
|
214
|
+
return resourcesOfType("program").find(({ record }) => !["retired"].includes(record.status))?.record || state.workspace;
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
function reviewedRequirementIds() {
|
|
218
|
+
return new Set((activeProgram().requirementApplicability || [])
|
|
219
|
+
.filter(({ decision }) => ["applicable", "not-applicable"].includes(decision))
|
|
220
|
+
.map(({ requirementId }) => requirementId));
|
|
221
|
+
}
|
|
222
|
+
|
|
210
223
|
function renderSidebarUtility(utility, route, direct = false) {
|
|
211
224
|
const directClass = direct ? "nav-direct " : "";
|
|
212
225
|
if (utility === "obligation-board") return "";
|
|
@@ -272,27 +285,28 @@ function renderHome(main) {
|
|
|
272
285
|
'<div class="overview-grid"><section class="panel obligation-panel"><div class="panel-head"><div><p class="kicker">Policy obligations</p><h3>' + obligationHeading + '</h3></div><a href="#/stage/run">Open board</a></div>' + obligationPreview(previewObligations) + '</section>' +
|
|
273
286
|
'<section class="panel event-reminder-panel"><div class="panel-head"><div><p class="kicker">Event reminders</p><h3>Did Something Change?</h3></div><a href="#/stage/run?section=events">' + (acceptedEventTriggers.length ? "Trigger work" : "Review proposals") + '</a></div>' + eventReminderPreview(orderedPolicyEventTriggers(state.obligations.triggers).slice(0, 4)) + '</section>' +
|
|
274
287
|
auditPanel + '</div></div>';
|
|
275
|
-
main.querySelector("#resume-setup")?.addEventListener("click", requestOnboarding);
|
|
288
|
+
main.querySelector("#resume-setup")?.addEventListener("click", () => requestOnboarding({ setupOnly: Boolean(initialSetupSystem()) }));
|
|
289
|
+
}
|
|
290
|
+
|
|
291
|
+
function initialSetupSystem() {
|
|
292
|
+
const program = activeProgram();
|
|
293
|
+
return resourcesOfType("system").find(({ record }) => (program.systemIds || []).includes(record.id) && record.status !== "retired")?.record || null;
|
|
276
294
|
}
|
|
277
295
|
|
|
278
296
|
function initialSetupBanner() {
|
|
279
|
-
const
|
|
297
|
+
const program = activeProgram();
|
|
298
|
+
const system = initialSetupSystem();
|
|
280
299
|
if (!system) {
|
|
281
|
-
return '<section class="setup-banner"><div><p class="kicker">Setup
|
|
300
|
+
return '<section class="setup-banner"><div><p class="kicker">Setup</p><h3>Define the service in scope</h3><p>Name the service and choose the program goal.</p></div><button class="button primary" type="button" id="resume-setup">Start setup</button></section>';
|
|
282
301
|
}
|
|
283
|
-
const goal = programGoalFromKind(
|
|
302
|
+
const goal = programGoalFromKind(program.assuranceGoal);
|
|
284
303
|
const goalLabels = {
|
|
285
304
|
readiness: "Program Readiness",
|
|
286
305
|
"type-1": "SOC 2 Type 1",
|
|
287
306
|
"type-2": "SOC 2 Type 2"
|
|
288
307
|
};
|
|
289
|
-
const
|
|
290
|
-
|
|
291
|
-
: "Confirm the saved program goal: " + goalLabels[goal] + ".";
|
|
292
|
-
const completion = system.status === "planned"
|
|
293
|
-
? "Confirm the service scope to activate the planned service and continue to Step 1."
|
|
294
|
-
: "Confirm the service scope to close onboarding and continue to Step 1.";
|
|
295
|
-
return '<section class="setup-banner"><div><p class="kicker">Setup draft saved</p><h3>Review the initial service scope</h3><p>' + esc(system.title) + ' already has a saved service boundary.</p></div><ol><li>Review the saved service boundary.</li><li>' + esc(goalStep) + '</li><li>' + esc(completion) + '</li><li><button class="text-button" type="button" id="resume-setup">Resume setup</button></li></ol></section>';
|
|
308
|
+
const goalLabel = goal === "none" ? "Choose goal" : goalLabels[goal];
|
|
309
|
+
return '<section class="setup-banner"><div><p class="kicker">Setup draft</p><h3>' + esc(system.title) + '</h3><p>Review and confirm the initial scope.</p></div><div class="setup-draft-state"><span><small>Program goal</small><strong>' + esc(goalLabel) + '</strong></span><span><small>System</small><strong>' + esc(properCase(system.status)) + '</strong></span><button class="button primary" type="button" id="resume-setup">Resume setup</button></div></section>';
|
|
296
310
|
}
|
|
297
311
|
|
|
298
312
|
function readinessOverview() {
|
|
@@ -332,6 +346,10 @@ function renderStageOverview(main, stageId, params = new URLSearchParams()) {
|
|
|
332
346
|
main.innerHTML = '<div class="page stage-overview-page"><nav class="breadcrumbs"><a href="#/">Overview</a><span>/</span><span>' + esc(stage.title) + '</span></nav>' +
|
|
333
347
|
'<section class="stage-overview-hero"><div><p class="kicker">Step ' + esc(stage.number) + ' of 5</p><h2>' + esc(stage.title) + '</h2><p>' + esc(stage.summary) + '</p></div>' + stageProgressCard(progress) + '</section>' +
|
|
334
348
|
renderStagePageIndex(stage) + (stage.id === "controls" ? renderEvidenceReadiness() : "") + '</div>';
|
|
349
|
+
main.querySelector("[data-show-evidence-families]")?.addEventListener("click", (event) => {
|
|
350
|
+
main.querySelectorAll("[data-evidence-family-extra]").forEach((card) => { card.hidden = false; });
|
|
351
|
+
event.currentTarget.remove();
|
|
352
|
+
});
|
|
335
353
|
}
|
|
336
354
|
|
|
337
355
|
function workflowGuidance(options = {}) {
|
|
@@ -370,20 +388,54 @@ function collectionReviewPanel(type) {
|
|
|
370
388
|
if (!assessment) return "";
|
|
371
389
|
const configuration = assessment.configuration;
|
|
372
390
|
const current = assessment.status === "current";
|
|
391
|
+
const needsFirstRecord = collectionNeedsFirstRecord(type);
|
|
373
392
|
const reviewerNames = (assessment.review?.reviewedByIds || [])
|
|
374
393
|
.map((id) => state.resources.find(({ record }) => record.id === id)?.record.title || id);
|
|
394
|
+
const reviewNote = assessment.review?.rationale
|
|
395
|
+
? " Note: " + esc(assessment.review.rationale)
|
|
396
|
+
: "";
|
|
375
397
|
const reviewSummary = current
|
|
376
|
-
? '<p class="collection-review-result"><strong>' + esc(properCase(assessment.review.decision)) + '</strong><span>Reviewed ' + esc(formatCalendarDate(assessment.review.reviewedOn)) + (reviewerNames.length ? " by " + esc(reviewerNames.join(", ")) : "") + '
|
|
377
|
-
: '<p class="collection-review-result"><strong>' + (assessment.status === "stale" ? "Review again" : "Review required") + '</strong><span>' + esc(assessment.message) + '</span></p>';
|
|
378
|
-
|
|
398
|
+
? '<p class="collection-review-result"><strong>' + esc(properCase(assessment.review.decision)) + '</strong><span>Reviewed ' + esc(formatCalendarDate(assessment.review.reviewedOn)) + (reviewerNames.length ? " by " + esc(reviewerNames.join(", ")) : "") + "." + reviewNote + '</span></p>'
|
|
399
|
+
: '<p class="collection-review-result"><strong>' + (needsFirstRecord ? "Records required" : assessment.status === "stale" ? "Review again" : "Review required") + '</strong><span>' + esc(assessment.message) + '</span></p>';
|
|
400
|
+
const action = state.readOnly
|
|
401
|
+
? ""
|
|
402
|
+
: needsFirstRecord
|
|
403
|
+
? '<a class="button primary" href="#/resources/' + encodeURIComponent(type) + '?new=1">Add first ' + esc(state.model.resources[type].title.toLowerCase()) + '</a>'
|
|
404
|
+
: '<button class="button ' + (current ? "" : "primary") + '" type="button" data-review-collection="' + esc(type) + '">' + (current ? "Review again" : "Review and confirm") + '</button>';
|
|
405
|
+
return '<section class="collection-review-panel panel ' + (current ? "current" : "required") + '"><div class="collection-review-head"><div><p class="kicker">Scope confirmation</p><h3>' + esc(configuration.title) + '</h3><p>' + esc(configuration.description) + '</p></div><span class="badge ' + (current ? "good" : "warn") + '">' + (current ? "Reviewed" : needsFirstRecord ? "Records required" : assessment.status === "stale" ? "Stale" : "Review required") + '</span></div>' +
|
|
379
406
|
'<details ' + (current ? "" : "open") + '><summary>What to review</summary><ul>' + configuration.reviewPoints.map((point) => '<li>' + esc(point) + '</li>').join("") + '</ul></details>' +
|
|
380
|
-
'<div class="collection-review-foot">' + reviewSummary +
|
|
407
|
+
'<div class="collection-review-foot">' + reviewSummary + action + '</div></section>';
|
|
408
|
+
}
|
|
409
|
+
|
|
410
|
+
function collectionNeedsFirstRecord(type) {
|
|
411
|
+
const assessment = state.collectionReviews?.[type];
|
|
412
|
+
if (!assessment || assessment.recordCount) return false;
|
|
413
|
+
return !(assessment.configuration.decisions || []).some((decision) => (
|
|
414
|
+
decision === "zero-population" || decision === "externally-managed"
|
|
415
|
+
));
|
|
416
|
+
}
|
|
417
|
+
|
|
418
|
+
function resourceCreationAllowed(type) {
|
|
419
|
+
return type !== "program" || !resourcesOfType("program").some(({ record }) => record.status !== "retired");
|
|
420
|
+
}
|
|
421
|
+
|
|
422
|
+
function collectionEmptyState(type, definition) {
|
|
423
|
+
const assessment = state.collectionReviews?.[type];
|
|
424
|
+
if (!assessment) {
|
|
425
|
+
return definition.guidance?.emptyState || "No records exist. Use the page guidance to decide whether a record is required, then add only real program facts.";
|
|
426
|
+
}
|
|
427
|
+
return collectionNeedsFirstRecord(type)
|
|
428
|
+
? "No " + definition.pluralTitle.toLowerCase() + " have been added yet."
|
|
429
|
+
: "No records exist. Use the scope confirmation above to record an allowed empty-collection conclusion, or add the records management identified.";
|
|
381
430
|
}
|
|
382
431
|
|
|
383
|
-
function resourceReviewCriteria(type) {
|
|
432
|
+
function resourceReviewCriteria(type, collapsed = false) {
|
|
384
433
|
const reviewPoints = state.model.resources[type]?.guidance?.reviewPoints || [];
|
|
385
434
|
if (!reviewPoints.length) return "";
|
|
386
|
-
|
|
435
|
+
const content = '<strong>Review criteria</strong><ul>' + reviewPoints.map((point) => '<li>' + esc(point) + '</li>').join("") + '</ul>';
|
|
436
|
+
return collapsed
|
|
437
|
+
? '<details class="resource-review-criteria compact"><summary>Review criteria</summary><ul>' + reviewPoints.map((point) => '<li>' + esc(point) + '</li>').join("") + '</ul></details>'
|
|
438
|
+
: '<section class="resource-review-criteria">' + content + '</section>';
|
|
387
439
|
}
|
|
388
440
|
|
|
389
441
|
function recordWorkflowItems(type, id) {
|
|
@@ -409,7 +461,8 @@ function recordWorkflowCell(type, entry) {
|
|
|
409
461
|
if (!items.length) return '<span class="record-workflow-clear">No calculated action</span>';
|
|
410
462
|
const item = items[0];
|
|
411
463
|
const href = workflowItemHref(item) || "#/resource/" + encodeURIComponent(type) + "/" + encodeURIComponent(entry.record.id);
|
|
412
|
-
|
|
464
|
+
const title = type === "control" ? "Finish implementation" : item.title;
|
|
465
|
+
return '<a class="record-workflow-action" href="' + href + '"><span class="workflow-finding-status ' + esc(item.state) + '">' + esc(properCase(item.state)) + '</span><span><strong>' + esc(title) + '</strong><small>' + esc(stagePageItemDetail(item)) + (items.length > 1 ? " +" + (items.length - 1) + " more" : "") + '</small></span></a>';
|
|
413
466
|
}
|
|
414
467
|
|
|
415
468
|
function openCollectionReviewDialog(type) {
|
|
@@ -417,7 +470,9 @@ function openCollectionReviewDialog(type) {
|
|
|
417
470
|
if (!assessment) return;
|
|
418
471
|
const configuration = assessment.configuration;
|
|
419
472
|
const people = resourcesOfType("person").filter(({ record }) => record.status === "active");
|
|
420
|
-
const
|
|
473
|
+
const v4 = String(state.model.modelVersion) === "4";
|
|
474
|
+
const sourceType = v4 ? "component" : "system";
|
|
475
|
+
const systems = resourcesOfType(sourceType).filter(({ record }) => record.status === "active");
|
|
421
476
|
const allowedDecisions = configuration.decisions || ["complete"];
|
|
422
477
|
const defaultDecision = assessment.review?.decision
|
|
423
478
|
|| (!assessment.recordCount && allowedDecisions.includes("zero-population") ? "zero-population" : allowedDecisions[0]);
|
|
@@ -427,15 +482,17 @@ function openCollectionReviewDialog(type) {
|
|
|
427
482
|
const dialog = document.createElement("dialog");
|
|
428
483
|
dialog.className = "commit-dialog event-dialog collection-review-dialog";
|
|
429
484
|
dialog.setAttribute("aria-labelledby", "collection-review-dialog-title");
|
|
430
|
-
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(assessment.review?.reviewedOn || currentDate()) + '"></label><label data-authoritative-system><span>Authoritative System</span><select name="
|
|
485
|
+
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(assessment.review?.reviewedOn || 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) + '" ' + ((assessment.review?.authoritativeComponentId || assessment.review?.authoritativeSystemId) === 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" role="status"></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" data-preview-collection-review>Preview confirmation</button></div></form>';
|
|
431
486
|
document.body.append(dialog);
|
|
432
487
|
dialog.showModal();
|
|
433
488
|
const form = dialog.querySelector("form");
|
|
489
|
+
const saveStatus = dialog.querySelector(".review-save-status");
|
|
490
|
+
const repositoryPrefetch = prefetchRepositoryForReview(saveStatus);
|
|
434
491
|
const systemField = dialog.querySelector("[data-authoritative-system]");
|
|
435
492
|
const syncDecision = () => {
|
|
436
493
|
const external = form.elements.decision.value === "externally-managed";
|
|
437
494
|
systemField.hidden = !external;
|
|
438
|
-
form.elements.
|
|
495
|
+
form.elements.authoritativeSourceId.required = external;
|
|
439
496
|
};
|
|
440
497
|
syncDecision();
|
|
441
498
|
dialog.querySelector(".icon-button").addEventListener("click", () => dialog.close());
|
|
@@ -459,7 +516,7 @@ function openCollectionReviewDialog(type) {
|
|
|
459
516
|
rationale: form.elements.rationale.value.trim(),
|
|
460
517
|
reviewedByIds: [form.elements.reviewerId.value],
|
|
461
518
|
reviewedOn: form.elements.reviewedOn.value,
|
|
462
|
-
authoritativeSystemId: form.elements.
|
|
519
|
+
[v4 ? "authoritativeComponentId" : "authoritativeSystemId"]: form.elements.authoritativeSourceId.value || undefined,
|
|
463
520
|
expectedRevision: assessment.reviewRevision || undefined
|
|
464
521
|
};
|
|
465
522
|
form.querySelectorAll("button,input,select,textarea").forEach((control) => { control.disabled = true; });
|
|
@@ -476,12 +533,15 @@ function openCollectionReviewDialog(type) {
|
|
|
476
533
|
dialog.querySelector(".workflow-preview").innerHTML = '<strong>Review before saving</strong><p>Save this confirmation for ' + preview.assessment.recordCount + ' current ' + esc(pluralize("record", preview.assessment.recordCount)) + '. If the collection or material scope changes, FileGRC will ask for another review.</p>';
|
|
477
534
|
dialog.querySelector("[data-preview-collection-review]").textContent = "Confirm and save";
|
|
478
535
|
} else {
|
|
536
|
+
saveStatus.textContent = "Validating and saving…";
|
|
537
|
+
const prefetch = await repositoryPrefetch;
|
|
479
538
|
const response = await localFetch("/api/collection-review", {
|
|
480
539
|
method: "POST",
|
|
481
|
-
headers: { "content-type": "application/json" },
|
|
482
|
-
body: JSON.stringify({ ...previewedPayload, confirmed: true })
|
|
540
|
+
headers: { "content-type": "application/json", prefer: "respond-async" },
|
|
541
|
+
body: JSON.stringify({ ...previewedPayload, confirmed: true, prefetchToken: prefetch?.token })
|
|
483
542
|
});
|
|
484
543
|
if (!response.ok) throw new Error(await responseMessage(response));
|
|
544
|
+
saveStatus.textContent = "Saved locally. Refreshing page…";
|
|
485
545
|
applyMutationState(await response.json());
|
|
486
546
|
dialog.close();
|
|
487
547
|
render();
|
|
@@ -582,11 +642,18 @@ function renderEvidenceReadiness() {
|
|
|
582
642
|
?.find((stage) => stage.id === "controls")
|
|
583
643
|
?.items.filter((item) => item.id.startsWith("source-family-")) || [];
|
|
584
644
|
const completeCount = items.filter((item) => item.status === "complete").length;
|
|
585
|
-
const
|
|
586
|
-
|
|
587
|
-
|
|
588
|
-
|
|
589
|
-
|
|
645
|
+
const visibleCount = 4;
|
|
646
|
+
const componentSources = items.some((item) => Array.isArray(item.sourceComponentIds));
|
|
647
|
+
const sourceType = componentSources ? "component" : "system";
|
|
648
|
+
const sourceLabel = componentSources ? "Components" : "Systems";
|
|
649
|
+
const cards = items.map((item, index) => {
|
|
650
|
+
const componentSources = Array.isArray(item.sourceComponentIds);
|
|
651
|
+
const sourceType = componentSources ? "component" : "system";
|
|
652
|
+
const sourceLabel = componentSources ? "Component" : "System";
|
|
653
|
+
const sources = (item.sourceComponentIds || item.sourceSystemIds || []).map((id) => {
|
|
654
|
+
const source = state.resources.find(({ record }) => record.type === sourceType && record.id === id)?.record;
|
|
655
|
+
const sourceCheck = (item.sourceComponentChecks || item.sourceSystemChecks || []).find((check) => (check.sourceComponentId || check.sourceSystemId) === id);
|
|
656
|
+
const complete = sourceCheck?.complete ?? (item.completeSourceComponentIds || item.completeSourceSystemIds || []).includes(id);
|
|
590
657
|
const status = complete
|
|
591
658
|
? "Ready"
|
|
592
659
|
: Object.entries(sourceCheck?.checks || {})
|
|
@@ -594,23 +661,26 @@ function renderEvidenceReadiness() {
|
|
|
594
661
|
.map(([name]) => evidenceSourceCheckLabel(name))
|
|
595
662
|
.join(", ") || "Needs details";
|
|
596
663
|
return source
|
|
597
|
-
? '<a class="evidence-map-source ' + (complete ? "complete" : "incomplete") + '" href="#/resource/
|
|
664
|
+
? '<a class="evidence-map-source ' + (complete ? "complete" : "incomplete") + '" href="#/resource/' + sourceType + '/' + encodeURIComponent(id) + '">' + esc(source.title) + '<small>' + esc(status) + '</small></a>'
|
|
598
665
|
: "";
|
|
599
666
|
}).join("");
|
|
600
667
|
const sourceAction = sources
|
|
601
668
|
? sources
|
|
602
|
-
: '<a class="button" href="#/resources/
|
|
669
|
+
: '<a class="button" href="#/resources/' + sourceType + '?new=1">Add source ' + sourceLabel + '</a>';
|
|
603
670
|
const method = item.operationRecordTypes?.length
|
|
604
671
|
? "FileGRC records: " + item.operationRecordTypes.map(properCase).join(", ")
|
|
605
672
|
: properCase(item.evidenceForm || "External evidence");
|
|
606
|
-
return '<article class="evidence-map-card ' + esc(item.status) + '"><div class="evidence-map-card-head"><div><span class="badge ' + (item.status === "complete" ? "good" : "warn") + '">' + (item.status === "complete" ? "Mapped" : "Needs mapping") + '</span><h3>' + esc(item.title) + '</h3></div><small>' + esc(method) + '</small></div><p>' + esc(item.description || item.message) + '</p>' +
|
|
607
|
-
(item.sourceKinds?.length ? '<div class="evidence-map-expectation"><strong>Source role</strong><span>' + item.sourceKinds.map((kind) =>
|
|
673
|
+
return '<article class="evidence-map-card ' + esc(item.status) + '"' + (index >= visibleCount ? ' data-evidence-family-extra hidden' : "") + '><div class="evidence-map-card-head"><div><span class="badge ' + (item.status === "complete" ? "good" : "warn") + '">' + (item.status === "complete" ? "Mapped" : "Needs mapping") + '</span><h3>' + esc(item.title) + '</h3></div><small>' + esc(method) + '</small></div><p>' + esc(item.description || item.message) + '</p>' +
|
|
674
|
+
(item.sourceKinds?.length ? '<div class="evidence-map-expectation"><strong>Source role</strong><span>' + item.sourceKinds.map((kind) => esc(properCase(kind))).join(" or ") + '</span></div>' : "") +
|
|
608
675
|
(item.evidencePrompt ? '<div class="evidence-map-expectation"><strong>Expected evidence</strong><span>' + esc(item.evidencePrompt) + '</span></div>' : "") +
|
|
609
676
|
(item.timing ? '<div class="evidence-map-expectation"><strong>When</strong><span>' + esc(item.timing) + '</span></div>' : "") +
|
|
610
677
|
'<div class="evidence-map-links"><div><small>Controls</small><div class="evidence-map-references">' + (item.controlIds || []).map((id) => formatReference(id)).join("") + '</div></div><div><small>Authoritative sources</small><div class="evidence-map-sources">' + sourceAction + '</div></div></div><p class="evidence-map-status">' + esc(item.message) + '</p></article>';
|
|
611
678
|
}).join("");
|
|
679
|
+
const more = items.length > visibleCount
|
|
680
|
+
? '<button class="button evidence-map-more" type="button" data-show-evidence-families>Show ' + (items.length - visibleCount) + ' more evidence families</button>'
|
|
681
|
+
: "";
|
|
612
682
|
const empty = '<section class="evidence-map-empty"><p class="kicker">Evidence readiness</p><h3>Select the program controls first</h3><p>Control implementation checks are generated from the selected Controls and their authoritative evidence sources.</p><a class="button primary" href="#/resources/control">Review Controls</a></section>';
|
|
613
|
-
return '<section class="evidence-map"><div class="evidence-map-head"><div><p class="kicker">Control implementation</p><h2>' + completeCount + ' of ' + items.length + ' evidence ' + (items.length === 1 ? "family" : "families") + ' ready</h2><p>Connect each Control to the
|
|
683
|
+
return '<section class="evidence-map"><div class="evidence-map-head"><div><p class="kicker">Control implementation</p><h2>' + completeCount + ' of ' + items.length + ' evidence ' + (items.length === 1 ? "family" : "families") + ' ready</h2><p>Connect each Control to the ' + sourceLabel + ' that produce its evidence.</p></div><div class="evidence-map-actions"><a class="button" href="#/resources/' + sourceType + '">Review ' + sourceLabel + '</a><a class="button primary" href="#/resources/control">Review Controls</a></div></div>' + (cards || empty) + more + '</section>';
|
|
614
684
|
}
|
|
615
685
|
|
|
616
686
|
function evidenceSourceCheckLabel(name) {
|
|
@@ -663,6 +733,7 @@ function stagePageItemDetail(item) {
|
|
|
663
733
|
? String(item.message || "").match(/^Complete (\d+) checks before implementation:/)
|
|
664
734
|
: null;
|
|
665
735
|
if (controlChecks) return controlChecks[1] + " implementation checks remain. Open the Control to review them.";
|
|
736
|
+
if (/^Review .+ before this page can be ready\.$/i.test(item.message || "")) return "Confirm the current scope.";
|
|
666
737
|
return item.message || workflowItemDetail(item);
|
|
667
738
|
}
|
|
668
739
|
|
|
@@ -756,7 +827,7 @@ function stagePageItems(stage, destination) {
|
|
|
756
827
|
|
|
757
828
|
function operationProgress() {
|
|
758
829
|
const program = state.programReadiness;
|
|
759
|
-
const goal = program?.target?.goal ||
|
|
830
|
+
const goal = program?.target?.goal || activeProgram().assuranceGoal || "none";
|
|
760
831
|
const asOf = program?.asOf || currentDate();
|
|
761
832
|
const candidateStarted = goal === "soc-2-type-2"
|
|
762
833
|
? Boolean(program?.target?.candidateCoverage?.kind === "range" && program.target.candidateCoverage.startsOn <= asOf)
|
|
@@ -837,7 +908,7 @@ function sectionDestinations(section) {
|
|
|
837
908
|
destinations.push({ type, kind: "Record page", label: titleCase(definition.pluralTitle), href: "#/resources/" + encodeURIComponent(type), description: definition.description });
|
|
838
909
|
}
|
|
839
910
|
if (section.utility === "obligation-board") destinations.push({ utility: section.utility, kind: "Working page", label: "Work Queue", href: "#/stage/run", description: "Complete recurring work, Policy Event tasks, and assigned follow-up with its due windows and linked proof." });
|
|
840
|
-
if (section.utility === "audit-packet") destinations.push({ utility: section.utility, kind: "Working page", label: "Audit Evidence & Packet", href: "#/audit-packet", description: "Review filegrc and
|
|
911
|
+
if (section.utility === "audit-packet") destinations.push({ utility: section.utility, kind: "Working page", label: "Audit Evidence & Packet", href: "#/audit-packet", description: "Review filegrc Evidence and Evidence Artifacts, prepare fieldwork, and build the indexed packet." });
|
|
841
912
|
return destinations;
|
|
842
913
|
}
|
|
843
914
|
|
|
@@ -857,10 +928,10 @@ function renderExternalEvidenceSection() {
|
|
|
857
928
|
)).join("");
|
|
858
929
|
const createButton = state.readOnly
|
|
859
930
|
? ""
|
|
860
|
-
: '<button class="button primary" type="button" data-new-external-evidence>New
|
|
861
|
-
return '<section class="workflow-section external-evidence-section"><div class="section-head"><div><p class="kicker">Fixed artifacts and approved references</p><h2>
|
|
931
|
+
: '<button class="button primary" type="button" data-new-external-evidence>New Evidence Artifact</button>';
|
|
932
|
+
return '<section class="workflow-section external-evidence-section"><div class="section-head"><div><p class="kicker">Fixed artifacts and approved references</p><h2>Evidence Artifacts</h2><p>Add fixed artifacts only when they exist. Link each one to its source Component and the work it supports.</p></div><div class="page-actions">' +
|
|
862
933
|
createButton + '<a class="button" href="#/resources/evidence">View all</a></div></div><div class="external-evidence-list">' +
|
|
863
|
-
(recent || empty("No
|
|
934
|
+
(recent || empty("No Evidence Artifacts have been collected yet. Create one during operation only when a real artifact or approved external reference exists.")) +
|
|
864
935
|
'</div></section>';
|
|
865
936
|
}
|
|
866
937
|
|
|
@@ -1009,6 +1080,12 @@ function coverageEnd(coverage) {
|
|
|
1009
1080
|
}
|
|
1010
1081
|
|
|
1011
1082
|
function defaultClassificationId() {
|
|
1083
|
+
const classifications = resourcesOfType("classification")
|
|
1084
|
+
.filter(({ record }) => record.status === "active")
|
|
1085
|
+
.map(({ record }) => record.id);
|
|
1086
|
+
if (classifications.length) {
|
|
1087
|
+
return classifications.includes("internal") ? "internal" : classifications[0];
|
|
1088
|
+
}
|
|
1012
1089
|
const definitions = state.workspace.classificationDefinitions || {};
|
|
1013
1090
|
return Object.hasOwn(definitions, "internal") ? "internal" : Object.keys(definitions)[0] || "";
|
|
1014
1091
|
}
|
|
@@ -1071,7 +1148,7 @@ function obligationCompletionPlan(item) {
|
|
|
1071
1148
|
if (!currentPeopleForParties(item.ownerIds || []).length) {
|
|
1072
1149
|
return { type, blocked: "Assign current owner", href: "#/resource/obligation/" + encodeURIComponent(item.obligationId) };
|
|
1073
1150
|
}
|
|
1074
|
-
if (["access-review", "backup-test"].includes(type) && !(
|
|
1151
|
+
if (["access-review", "backup-test"].includes(type) && !(activeProgram().systemIds || []).some((id) => state.resources.some(({ record }) => record.id === id && record.status !== "retired"))) {
|
|
1075
1152
|
return { type, blocked: "Add system first", href: "#/resources/system?new=1" };
|
|
1076
1153
|
}
|
|
1077
1154
|
if (type === "vendor-review" && !resourcesOfType("vendor").some(({ record }) => record.status !== "terminated")) {
|
|
@@ -1123,7 +1200,7 @@ function obligationCompletionSeed(type, item, obligation) {
|
|
|
1123
1200
|
.filter((record) => record.status === "active" && !responsiblePeople.includes(record.id))
|
|
1124
1201
|
.map(({ id }) => id);
|
|
1125
1202
|
const reviewerPeople = independentPeople.length ? [independentPeople[0]] : [];
|
|
1126
|
-
const inScopeSystems = resourcesOfType("system").filter(({ record }) => (
|
|
1203
|
+
const inScopeSystems = resourcesOfType("system").filter(({ record }) => (activeProgram().systemIds || []).includes(record.id) && record.status !== "retired").map(({ record }) => record.id);
|
|
1127
1204
|
const activeVendors = resourcesOfType("vendor").filter(({ record }) => record.status !== "terminated").map(({ record }) => record.id);
|
|
1128
1205
|
const title = item.title + " · " + formatCalendarDate(item.dueWindowStart);
|
|
1129
1206
|
const common = { title };
|
|
@@ -1386,8 +1463,11 @@ function openApplicabilityReviewDialog(type, entries) {
|
|
|
1386
1463
|
const definition = state.model.resources[type];
|
|
1387
1464
|
const reviewPoints = definition?.guidance?.reviewPoints || [];
|
|
1388
1465
|
const people = resourcesOfType("person").filter(({ record }) => record.status === "active");
|
|
1466
|
+
const reviewedRequirements = reviewedRequirementIds();
|
|
1389
1467
|
const pending = entries.filter(({ record }) => (
|
|
1390
|
-
|
|
1468
|
+
type === "requirement" && String(state.model.modelVersion) === "4"
|
|
1469
|
+
? !reviewedRequirements.has(record.id)
|
|
1470
|
+
: !record.applicabilityReview
|
|
1391
1471
|
|| type === "requirement" && record.applicability === "undetermined"
|
|
1392
1472
|
));
|
|
1393
1473
|
const dialog = document.createElement("dialog");
|
|
@@ -1400,10 +1480,12 @@ function openApplicabilityReviewDialog(type, entries) {
|
|
|
1400
1480
|
const reviewChecks = reviewPoints.length
|
|
1401
1481
|
? '<section class="collection-review-checks"><strong>Before deciding</strong><ul>' + reviewPoints.map((point) => '<li>' + esc(point) + '</li>').join("") + '</ul></section>'
|
|
1402
1482
|
: "";
|
|
1403
|
-
dialog.innerHTML = '<form><div class="dialog-head"><div><p class="kicker">Batch review</p><h2 id="applicability-dialog-title">Review ' + esc(definition.pluralTitle) + '</h2></div><button type="button" class="icon-button" aria-label="Close">×</button></div><p>Record only decisions reviewed against the current service scope. Leave an item at Review later when management has not decided it.</p>' + reviewChecks + '<div class="form-grid review-context"><label><span>Reviewer</span><select name="reviewerId" required><option value="">Select</option>' + people.map(({ record }) => '<option value="' + esc(record.id) + '">' + esc(record.title) + '</option>').join("") + '</select></label><label><span>Reviewed on</span><input name="reviewedOn" type="date" required value="' + esc(currentDate()) + '"></label></div><div class="applicability-rows">' + (rows || empty("Every record already has a reviewed applicability decision.")) + '</div><div class="workflow-preview" role="status"></div><div class="dialog-error" role="alert"></div><div class="dialog-actions"><button type="button" class="button" data-event="cancel">Cancel</button><button type="submit" class="button primary" data-preview-review ' + (pending.length ? "" : "disabled") + '>Preview decisions</button></div></form>';
|
|
1483
|
+
dialog.innerHTML = '<form><div class="dialog-head"><div><p class="kicker">Batch review</p><h2 id="applicability-dialog-title">Review ' + esc(definition.pluralTitle) + '</h2></div><button type="button" class="icon-button" aria-label="Close">×</button></div><p>Record only decisions reviewed against the current service scope. Leave an item at Review later when management has not decided it.</p>' + reviewChecks + '<div class="form-grid review-context"><label><span>Reviewer</span><select name="reviewerId" required><option value="">Select</option>' + people.map(({ record }) => '<option value="' + esc(record.id) + '">' + esc(record.title) + '</option>').join("") + '</select></label><label><span>Reviewed on</span><input name="reviewedOn" type="date" required value="' + esc(currentDate()) + '"></label></div><div class="applicability-rows">' + (rows || empty("Every record already has a reviewed applicability decision.")) + '</div><div class="workflow-preview" role="status"></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" data-preview-review ' + (pending.length ? "" : "disabled") + '>Preview decisions</button></div></form>';
|
|
1404
1484
|
document.body.append(dialog);
|
|
1405
1485
|
dialog.showModal();
|
|
1406
1486
|
const form = dialog.querySelector("form");
|
|
1487
|
+
const saveStatus = dialog.querySelector(".review-save-status");
|
|
1488
|
+
const repositoryPrefetch = prefetchRepositoryForReview(saveStatus);
|
|
1407
1489
|
dialog.querySelector(".icon-button").addEventListener("click", () => dialog.close());
|
|
1408
1490
|
dialog.querySelector('[data-event="cancel"]').addEventListener("click", () => dialog.close());
|
|
1409
1491
|
dialog.addEventListener("close", () => dialog.remove());
|
|
@@ -1438,11 +1520,17 @@ function openApplicabilityReviewDialog(type, entries) {
|
|
|
1438
1520
|
error.textContent = "Select at least one decision.";
|
|
1439
1521
|
return;
|
|
1440
1522
|
}
|
|
1523
|
+
const decisionIds = new Set(decisions.map(({ id }) => id));
|
|
1441
1524
|
const payload = {
|
|
1442
1525
|
decisions,
|
|
1443
1526
|
reviewedByIds: [form.elements.reviewerId.value],
|
|
1444
1527
|
reviewedOn: form.elements.reviewedOn.value,
|
|
1445
|
-
expectedRevisions: Object.fromEntries(
|
|
1528
|
+
expectedRevisions: Object.fromEntries([
|
|
1529
|
+
...entries.filter((entry) => decisionIds.has(entry.record.id)).map((entry) => [entry.record.id, entry.revision]),
|
|
1530
|
+
...(type === "requirement" && String(state.model.modelVersion) === "4"
|
|
1531
|
+
? state.resources.filter(({ record }) => record.id === activeProgram().id).map((entry) => [entry.record.id, entry.revision])
|
|
1532
|
+
: [])
|
|
1533
|
+
])
|
|
1446
1534
|
};
|
|
1447
1535
|
form.querySelectorAll("button,input,select").forEach((control) => { control.disabled = true; });
|
|
1448
1536
|
try {
|
|
@@ -1455,15 +1543,18 @@ function openApplicabilityReviewDialog(type, entries) {
|
|
|
1455
1543
|
if (!response.ok) throw new Error(await responseMessage(response));
|
|
1456
1544
|
const preview = await response.json();
|
|
1457
1545
|
previewedPayload = payload;
|
|
1458
|
-
dialog.querySelector(".workflow-preview").innerHTML = '<strong>Review before saving</strong><p>' + preview.reviewedIds.length + '
|
|
1546
|
+
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>';
|
|
1459
1547
|
dialog.querySelector("[data-preview-review]").textContent = "Confirm and save";
|
|
1460
1548
|
} else {
|
|
1549
|
+
saveStatus.textContent = "Validating and saving…";
|
|
1550
|
+
const prefetch = await repositoryPrefetch;
|
|
1461
1551
|
const response = await localFetch("/api/applicability-review", {
|
|
1462
1552
|
method: "POST",
|
|
1463
|
-
headers: { "content-type": "application/json" },
|
|
1464
|
-
body: JSON.stringify({ ...previewedPayload, confirmed: true })
|
|
1553
|
+
headers: { "content-type": "application/json", prefer: "respond-async" },
|
|
1554
|
+
body: JSON.stringify({ ...previewedPayload, confirmed: true, prefetchToken: prefetch?.token })
|
|
1465
1555
|
});
|
|
1466
1556
|
if (!response.ok) throw new Error(await responseMessage(response));
|
|
1557
|
+
saveStatus.textContent = "Saved locally. Refreshing page…";
|
|
1467
1558
|
applyMutationState(await response.json());
|
|
1468
1559
|
dialog.close();
|
|
1469
1560
|
render();
|
|
@@ -1498,10 +1589,10 @@ function renderAuditPacket(main, params = new URLSearchParams()) {
|
|
|
1498
1589
|
["Repository", state.git.available ? state.git.clean ? "Clean revision" : state.git.changes.length + " uncommitted" : "Git unavailable", "#/repository", state.git.clean ? "good" : "warn"],
|
|
1499
1590
|
["Engagement", selected ? selected.title : "No audit record", "#/resources/audit", selected ? "good" : "warn"],
|
|
1500
1591
|
["filegrc Evidence", filegrcRecords.length + " operating " + pluralize("record", filegrcRecords.length), "#/stage/run", "neutral"],
|
|
1501
|
-
["
|
|
1592
|
+
["Evidence Artifacts", evidence.length + " " + pluralize("record", evidence.length), "#/resources/evidence", evidence.length ? "good" : "neutral"],
|
|
1502
1593
|
["Policy work", state.obligations.counts.overdue ? state.obligations.counts.overdue + " overdue" : state.obligations.counts.blocked ? state.obligations.counts.blocked + " blocked" : state.obligations.counts.due ? state.obligations.counts.due + " due" : state.obligations.counts.proposed ? state.obligations.counts.proposed + " proposals" : "No work due", "#/stage/run", state.obligations.counts.overdue || state.obligations.counts.blocked ? "bad" : state.obligations.counts.due ? "warn" : state.obligations.counts.proposed ? "neutral" : "good"]
|
|
1503
1594
|
];
|
|
1504
|
-
const evidencePaths = '<section class="panel audit-evidence-paths"><div class="panel-head"><div><p class="kicker">Evidence workflow</p><h3>Review both evidence paths</h3><p>Use the formal audit date or period. Each selected control may need one or both paths.</p></div></div><div class="audit-evidence-path-grid"><a href="#/stage/run"><span class="step-label">filegrc Evidence</span><h4>Review operating records</h4><p>Confirm the applicable Step 4 records are complete and linked to their Controls.</p></a><a href="#/resources/evidence"><span class="step-label">
|
|
1595
|
+
const evidencePaths = '<section class="panel audit-evidence-paths"><div class="panel-head"><div><p class="kicker">Evidence workflow</p><h3>Review both evidence paths</h3><p>Use the formal audit date or period. Each selected control may need one or both paths.</p></div></div><div class="audit-evidence-path-grid"><a href="#/stage/run"><span class="step-label">filegrc Evidence</span><h4>Review operating records</h4><p>Confirm the applicable Step 4 records are complete and linked to their Controls.</p></a><a href="#/resources/evidence"><span class="step-label">Evidence Artifacts</span><h4>Review imported or referenced proof</h4><p>Confirm each artifact is fixed, verified, and linked to its source Component and Controls.</p></a></div></section>';
|
|
1505
1596
|
const dateFields = typeOne
|
|
1506
1597
|
? '<label><span>As-of date</span><input type="date" name="start" required value="' + esc(start) + '"></label>'
|
|
1507
1598
|
: '<label><span>Period start</span><input type="date" name="start" required value="' + esc(start) + '"></label><label><span>Period end</span><input type="date" name="end" required value="' + esc(end) + '"></label>';
|
|
@@ -1602,10 +1693,10 @@ function renderPacketResults(container, result) {
|
|
|
1602
1693
|
container.innerHTML = '<section class="metrics packet-metrics">' +
|
|
1603
1694
|
metric("filegrc Evidence", packet.summary.filegrcRecords, packet.summary.records + " total packet records", "neutral") +
|
|
1604
1695
|
metric("Obligations", packet.summary.obligationOccurrences, packet.summary.eventRuns + " event workflows", "neutral") +
|
|
1605
|
-
metric("
|
|
1696
|
+
metric("Evidence Artifacts", packet.summary.evidence, packet.summary.policies + " policies · " + packet.summary.controls + " controls", "neutral") +
|
|
1606
1697
|
metric("Review items", packet.summary.gaps, packet.summary.errors + " errors · " + packet.summary.warnings + " warnings", packet.summary.errors ? "bad" : packet.summary.warnings ? "warn" : "good") +
|
|
1607
1698
|
'</section><section class="panel packet-output"><div class="panel-head"><div><p class="kicker">' + (ready ? "filegrc management checks passed" : "Draft packet") + '</p><h3>' + esc(result.output) + '</h3></div>' + (result.packetUrl ? '<a class="button primary" href="' + esc(result.packetUrl) + '" target="_blank" rel="noreferrer">Open index</a>' : "") + '</div><p>The directory contains ' + result.files.length + ' files. ' + (ready ? "Verify the checksums, reconcile external deliveries, and let the engagement team confirm evidence sufficiency." : "Do not deliver it until every error is resolved and each warning has been reviewed.") + '</p></section>' +
|
|
1608
|
-
'<div class="dashboard-grid"><section class="panel span-2"><div class="panel-head"><h3>Coverage Gaps and Warnings</h3></div>' + (packet.gaps.length ? '<div class="packet-gaps">' + packet.gaps.map((gap) => '<div><span class="badge ' + (gap.severity === "error" ? "bad" : "warn") + '">' + esc(properCase(gap.severity)) + '</span><p>' + esc(gap.message) + '</p></div>').join("") + '</div>' : empty("No packet gaps were detected.")) + '</section><section class="panel"><div class="panel-head"><h3>Included filegrc Evidence</h3></div>' + (packet.filegrcRecords.length ? '<div class="packet-list">' + packet.filegrcRecords.slice(0, 12).map((item) => '<a href="#/resource/' + encodeURIComponent(item.type) + '/' + encodeURIComponent(item.id) + '"><strong>' + esc(item.title) + '</strong><small>' + esc(properCase(item.type)) + ' · ' + esc(item.primaryDate) + '</small></a>').join("") + '</div>' : empty("No filegrc Evidence matched.")) + '</section><section class="panel"><div class="panel-head"><h3>Included
|
|
1699
|
+
'<div class="dashboard-grid"><section class="panel span-2"><div class="panel-head"><h3>Coverage Gaps and Warnings</h3></div>' + (packet.gaps.length ? '<div class="packet-gaps">' + packet.gaps.map((gap) => '<div><span class="badge ' + (gap.severity === "error" ? "bad" : "warn") + '">' + esc(properCase(gap.severity)) + '</span><p>' + esc(gap.message) + '</p></div>').join("") + '</div>' : empty("No packet gaps were detected.")) + '</section><section class="panel"><div class="panel-head"><h3>Included filegrc Evidence</h3></div>' + (packet.filegrcRecords.length ? '<div class="packet-list">' + packet.filegrcRecords.slice(0, 12).map((item) => '<a href="#/resource/' + encodeURIComponent(item.type) + '/' + encodeURIComponent(item.id) + '"><strong>' + esc(item.title) + '</strong><small>' + esc(properCase(item.type)) + ' · ' + esc(item.primaryDate) + '</small></a>').join("") + '</div>' : empty("No filegrc Evidence matched.")) + '</section><section class="panel"><div class="panel-head"><h3>Included Evidence Artifacts</h3></div>' + (packet.evidence.length ? '<div class="packet-list">' + packet.evidence.slice(0, 12).map((item) => '<a href="#/resource/evidence/' + encodeURIComponent(item.id) + '"><strong>' + esc(item.title) + '</strong><small>' + esc(properCase(item.status)) + ' · ' + esc(properCase(item.artifactKind)) + '</small></a>').join("") + '</div>' : empty("No Evidence Artifacts matched.")) + '</section></div>';
|
|
1609
1700
|
}
|
|
1610
1701
|
|
|
1611
1702
|
function obligationPreview(items) {
|
|
@@ -1699,10 +1790,12 @@ function renderList(main, type, params = new URLSearchParams()) {
|
|
|
1699
1790
|
const values = [...new Set(observed)].sort();
|
|
1700
1791
|
return { name, label: field.label || humanize(name), values };
|
|
1701
1792
|
}).filter(({ values }) => values.length > 1);
|
|
1702
|
-
const createButton = !state.readOnly && !definition.singleton ? '<button class="button primary" id="new-resource">New ' + esc(definition.title.toLowerCase()) + '</button>' : "";
|
|
1793
|
+
const createButton = !state.readOnly && !definition.singleton && !collectionNeedsFirstRecord(type) && resourceCreationAllowed(type) ? '<button class="button primary" id="new-resource">New ' + esc(definition.title.toLowerCase()) + '</button>' : "";
|
|
1794
|
+
const reviewedRequirements = reviewedRequirementIds();
|
|
1703
1795
|
const hasPendingApplicability = entries.some(({ record }) => (
|
|
1704
|
-
|
|
1705
|
-
|
|
1796
|
+
type === "requirement" && String(state.model.modelVersion) === "4"
|
|
1797
|
+
? !reviewedRequirements.has(record.id)
|
|
1798
|
+
: !record.applicabilityReview || type === "requirement" && record.applicability === "undetermined"
|
|
1706
1799
|
));
|
|
1707
1800
|
const applicabilityButton = !state.readOnly
|
|
1708
1801
|
&& ["requirement", "control", "commitment", "complementary-control"].includes(type)
|
|
@@ -1730,7 +1823,7 @@ function renderList(main, type, params = new URLSearchParams()) {
|
|
|
1730
1823
|
const start = (pageNumber - 1) * LIST_PAGE_SIZE;
|
|
1731
1824
|
const visible = filtered.slice(start, start + LIST_PAGE_SIZE);
|
|
1732
1825
|
main.querySelector("#result-count").textContent = filtered.length + (filtered.length === 1 ? " record" : " records");
|
|
1733
|
-
main.querySelector("#record-rows").innerHTML = filtered.length ? visible.map((entry) => '<tr><td data-label="' + esc(fieldLabel(type, "title")) + '" data-primary-field><a class="record-title" href="#/resource/' + encodeURIComponent(type) + '/' + encodeURIComponent(entry.record.id) + '">' + esc(entry.record.title) + '</a></td>' + fields.map((name) => '<td data-label="' + esc(fieldLabel(type, name)) + '">' + (name === "$operationTracking" ? controlOperationTracking(entry.record) : name === "$workQueueStatus" ? obligationWorkQueueStatus(entry.record) : formatValue(name === "status" ? displayStatus(entry.record) : entry.record[name], name, type)) + '</td>').join("") + '<td data-label="Next action">' + recordWorkflowCell(type, entry) + '</td><td data-label="Git file"><code>' + esc(entry.relativePath.replace(/^data\//, "")) + '</code></td></tr>').join("") : '<tr><td colspan="' + (fields.length + 3) + '">' + empty(entries.length ? "No records match this filter." :
|
|
1826
|
+
main.querySelector("#record-rows").innerHTML = filtered.length ? visible.map((entry) => '<tr><td data-label="' + esc(fieldLabel(type, "title")) + '" data-primary-field><a class="record-title" href="#/resource/' + encodeURIComponent(type) + '/' + encodeURIComponent(entry.record.id) + '">' + esc(entry.record.title) + '</a></td>' + fields.map((name) => '<td data-label="' + esc(fieldLabel(type, name)) + '">' + (name === "$operationTracking" ? controlOperationTracking(entry.record) : name === "$workQueueStatus" ? obligationWorkQueueStatus(entry.record) : formatValue(name === "status" ? displayStatus(entry.record) : entry.record[name], name, type, true)) + '</td>').join("") + '<td data-label="Next action">' + recordWorkflowCell(type, entry) + '</td><td data-label="Git file"><code>' + esc(entry.relativePath.replace(/^data\//, "")) + '</code></td></tr>').join("") : '<tr><td colspan="' + (fields.length + 3) + '">' + empty(entries.length ? "No records match this filter." : collectionEmptyState(type, definition)) + '</td></tr>';
|
|
1734
1827
|
pagination.hidden = totalPages === 1;
|
|
1735
1828
|
previous.disabled = pageNumber === 1;
|
|
1736
1829
|
next.disabled = pageNumber === totalPages;
|
|
@@ -1772,7 +1865,7 @@ function renderList(main, type, params = new URLSearchParams()) {
|
|
|
1772
1865
|
main.querySelector("#new-resource")?.addEventListener("click", () => openEditor(type));
|
|
1773
1866
|
main.querySelector("#review-applicability")?.addEventListener("click", () => openApplicabilityReviewDialog(type, entries));
|
|
1774
1867
|
main.querySelector("[data-review-collection]")?.addEventListener("click", () => openCollectionReviewDialog(type));
|
|
1775
|
-
if (params.get("new") === "1" && !state.readOnly && !definition.singleton) queueMicrotask(() => openEditor(type));
|
|
1868
|
+
if (params.get("new") === "1" && !state.readOnly && !definition.singleton && resourceCreationAllowed(type)) queueMicrotask(() => openEditor(type));
|
|
1776
1869
|
if (params.get("review") === "1" && !state.readOnly) {
|
|
1777
1870
|
queueMicrotask(() => main.querySelector("#review-applicability")?.click());
|
|
1778
1871
|
}
|
|
@@ -1878,10 +1971,16 @@ function renderDetail(main, type, id) {
|
|
|
1878
1971
|
main.querySelector("[data-evidence-file]")?.click();
|
|
1879
1972
|
});
|
|
1880
1973
|
main.querySelector("[data-evidence-file]")?.addEventListener("change", async (event) => {
|
|
1881
|
-
const
|
|
1974
|
+
const input = event.currentTarget;
|
|
1975
|
+
const file = input.files?.[0];
|
|
1882
1976
|
if (!file) return;
|
|
1883
|
-
if (!
|
|
1884
|
-
|
|
1977
|
+
if (!await confirmAction({
|
|
1978
|
+
kicker: "Attach evidence",
|
|
1979
|
+
title: file.name,
|
|
1980
|
+
message: "Confirm its classification, retention, and repository access are appropriate.",
|
|
1981
|
+
confirmLabel: "Attach"
|
|
1982
|
+
})) {
|
|
1983
|
+
input.value = "";
|
|
1885
1984
|
return;
|
|
1886
1985
|
}
|
|
1887
1986
|
try {
|
|
@@ -1894,12 +1993,19 @@ function renderDetail(main, type, id) {
|
|
|
1894
1993
|
applyMutationState(await response.json());
|
|
1895
1994
|
render();
|
|
1896
1995
|
} catch (error) {
|
|
1996
|
+
input.value = "";
|
|
1897
1997
|
showError(error.message);
|
|
1898
1998
|
}
|
|
1899
1999
|
});
|
|
1900
2000
|
main.querySelectorAll("[data-detach-evidence]").forEach((button) => button.addEventListener("click", async () => {
|
|
1901
2001
|
const attachment = button.dataset.detachEvidence;
|
|
1902
|
-
if (!
|
|
2002
|
+
if (!await confirmAction({
|
|
2003
|
+
kicker: "Remove attachment",
|
|
2004
|
+
title: attachment,
|
|
2005
|
+
message: "Deletes this local file from the Evidence Artifact.",
|
|
2006
|
+
confirmLabel: "Remove",
|
|
2007
|
+
danger: true
|
|
2008
|
+
})) return;
|
|
1903
2009
|
try {
|
|
1904
2010
|
const response = await localFetch(
|
|
1905
2011
|
"/api/evidence-attachments/" + encodeURIComponent(entry.record.id) + "/" + encodeURIComponent(attachment)
|
|
@@ -1916,7 +2022,13 @@ function renderDetail(main, type, id) {
|
|
|
1916
2022
|
main.querySelector("#add-record-content")?.addEventListener("click", () => openEditor(type, entry, { addRecordContent: true }));
|
|
1917
2023
|
main.querySelectorAll("[data-edit-content]").forEach((button) => button.addEventListener("click", () => openContentEditor(entry, button.dataset.editContent)));
|
|
1918
2024
|
main.querySelector("#delete-resource")?.addEventListener("click", async () => {
|
|
1919
|
-
if (!
|
|
2025
|
+
if (!await confirmAction({
|
|
2026
|
+
kicker: "Delete record",
|
|
2027
|
+
title: entry.record.title,
|
|
2028
|
+
message: "Deletes this record and its Markdown. Use this only for mistakes and uncommitted drafts.",
|
|
2029
|
+
confirmLabel: "Delete",
|
|
2030
|
+
danger: true
|
|
2031
|
+
})) return;
|
|
1920
2032
|
try {
|
|
1921
2033
|
const response = await localFetch("/api/resource/" + encodeURIComponent(type) + "/" + encodeURIComponent(id) + "?revision=" + encodeURIComponent(entry.revision), { method: "DELETE" });
|
|
1922
2034
|
if (!response.ok) return showError(await responseMessage(response));
|
|
@@ -2182,13 +2294,10 @@ function renderTrunkRepository(main) {
|
|
|
2182
2294
|
const lastSync = repository.lastSuccessfulSynchronization
|
|
2183
2295
|
? formatLocalDateTime(repository.lastSuccessfulSynchronization)
|
|
2184
2296
|
: "No successful sync recorded by this server";
|
|
2185
|
-
const override = repository.developmentOverride
|
|
2186
|
-
? '<div class="repository-override"><span class="status-dot warn"></span><div><strong>Development write override active</strong><p>Browser writes stay local. FileGRC will not fetch, commit, or push while this server uses <code>--allow-non-authoritative-writes</code>.</p></div></div>'
|
|
2187
|
-
: "";
|
|
2188
2297
|
const validationBody = state.validation.diagnostics.length
|
|
2189
2298
|
? '<div class="diagnostics">' + state.validation.diagnostics.map((item) => '<div><span class="badge ' + item.severity + '">' + esc(properCase(item.severity)) + '</span><code>' + esc(item.path) + '</code><p>' + esc(item.message) + '</p></div>').join("") + '</div>'
|
|
2190
2299
|
: empty("No validation problems.");
|
|
2191
|
-
main.innerHTML = '<div class="page"><div class="page-intro"><div><p class="kicker">Audit trail</p><h2>Repository State</h2><p>Browser saves use one authoritative branch. Record status represents draft, proposal, approval, and retirement; Git branches do not.</p><p class="repository-sync-status" role="status" aria-live="polite"></p></div><div class="page-actions">' + retryButton + onboardingButton + settingsLink + '<a class="button" href="#/resource/workspace/workspace">Workspace settings</a></div></div
|
|
2300
|
+
main.innerHTML = '<div class="page"><div class="page-intro"><div><p class="kicker">Audit trail</p><h2>Repository State</h2><p>Browser saves use one authoritative branch. Record status represents draft, proposal, approval, and retirement; Git branches do not.</p><p class="repository-sync-status" role="status" aria-live="polite"></p></div><div class="page-actions">' + retryButton + onboardingButton + settingsLink + '<a class="button" href="#/resource/workspace/workspace">Workspace settings</a></div></div><section class="panel repository-state-banner"><span class="status-dot ' + repositoryStatusTone(repository.status) + '"></span><div><p class="kicker">Repository status</p><h3>' + esc(repository.label) + '</h3><p>' + esc(repository.message) + '</p></div></section><div class="dashboard-grid"><section class="panel"><div class="panel-head"><h3>Configured Repository</h3></div><dl class="metadata"><div><dt>Branch</dt><dd>' + esc(repository.authoritativeBranch) + '</dd></div><div><dt>Remote</dt><dd>' + esc(repository.remote) + '</dd></div><div><dt>Checkout</dt><dd>' + esc(state.git.branch || (state.git.available ? "Detached HEAD" : "Unavailable")) + '</dd></div><div><dt>Upstream</dt><dd>' + esc(repository.upstream || "Not configured") + '</dd></div></dl></section><section class="panel"><div class="panel-head"><h3>Synchronization</h3></div><dl class="metadata"><div><dt>Current commit</dt><dd><code>' + esc(repository.currentCommit || "Unavailable") + '</code></dd></div><div><dt>Upstream commit</dt><dd><code>' + esc(repository.upstreamCommit || "Unavailable") + '</code></dd></div><div><dt>Ahead</dt><dd>' + esc(repository.ahead ?? "Unknown") + '</dd></div><div><dt>Behind</dt><dd>' + esc(repository.behind ?? "Unknown") + '</dd></div><div><dt>Last sync</dt><dd>' + esc(lastSync) + '</dd></div></dl></section><section class="panel"><div class="panel-head"><h3>Safety Checks</h3></div><dl class="metadata"><div><dt>Whole worktree</dt><dd>' + (repository.wholeWorktreeClean === null ? "Unavailable" : repository.wholeWorktreeClean ? "Clean" : "Has changes") + '</dd></div><div><dt>Git operation</dt><dd>' + esc(repository.operationInProgress || "None") + '</dd></div><div><dt>Pending scope</dt><dd>' + (repository.pendingCommitsFilegrcOnly === false ? "Includes external files" : repository.pendingCommits?.length ? "FileGRC only" : "None") + '</dd></div></dl></section><section class="panel span-2"><div class="panel-head"><h3>Pending FileGRC-only Commits</h3></div>' + pending + '</section><section class="panel span-2"><div class="panel-head"><h3>Validation</h3><span class="badge ' + (state.validation.ok ? "good" : "bad") + '">' + (state.validation.ok ? "Passing" : "Needs attention") + '</span></div>' + validationBody + '</section></div></div>';
|
|
2192
2301
|
main.querySelectorAll("[data-git-action]").forEach((button) => button.addEventListener("click", () => runRepositoryGitAction(button.dataset.gitAction)));
|
|
2193
2302
|
main.querySelector("#start-onboarding")?.addEventListener("click", requestOnboarding);
|
|
2194
2303
|
}
|
|
@@ -2376,13 +2485,14 @@ function rendererSettingsEntry() {
|
|
|
2376
2485
|
return state.resources.find(({ record }) => record.type === "renderer-settings");
|
|
2377
2486
|
}
|
|
2378
2487
|
|
|
2379
|
-
function requestOnboarding() {
|
|
2488
|
+
function requestOnboarding({ setupOnly = false } = {}) {
|
|
2380
2489
|
if (state.readOnly || onboardingDialog || !rendererSettingsEntry()) return;
|
|
2381
2490
|
if (parseRoute().name !== "home") {
|
|
2382
2491
|
history.replaceState(null, "", "#/");
|
|
2383
2492
|
render();
|
|
2384
2493
|
}
|
|
2385
|
-
onboardingStep = 0;
|
|
2494
|
+
onboardingStep = setupOnly ? onboardingSteps().length - 1 : 0;
|
|
2495
|
+
onboardingSetupOnly = setupOnly;
|
|
2386
2496
|
onboardingDraft = initialOnboardingDraft();
|
|
2387
2497
|
onboardingBusy = false;
|
|
2388
2498
|
onboardingShade = document.createElement("div");
|
|
@@ -2407,23 +2517,25 @@ function requestOnboarding() {
|
|
|
2407
2517
|
onboardingDialog = null;
|
|
2408
2518
|
onboardingDraft = null;
|
|
2409
2519
|
onboardingBusy = false;
|
|
2520
|
+
onboardingSetupOnly = false;
|
|
2410
2521
|
onboardingPendingDraft = false;
|
|
2411
2522
|
});
|
|
2412
2523
|
renderOnboardingStep();
|
|
2413
2524
|
}
|
|
2414
2525
|
|
|
2415
2526
|
function initialOnboardingDraft() {
|
|
2416
|
-
const
|
|
2527
|
+
const program = activeProgram();
|
|
2528
|
+
const system = initialSetupSystem();
|
|
2417
2529
|
const owner = resourcesOfType("person").find(({ record }) => record.status === "active")?.record;
|
|
2418
2530
|
return {
|
|
2419
|
-
systemId:
|
|
2420
|
-
serviceName:
|
|
2421
|
-
scope:
|
|
2422
|
-
ownerId:
|
|
2423
|
-
criticality:
|
|
2424
|
-
classificationId:
|
|
2425
|
-
internetExposed:
|
|
2426
|
-
programGoal: programGoalFromKind(
|
|
2531
|
+
systemId: system?.id || "",
|
|
2532
|
+
serviceName: system?.title || "",
|
|
2533
|
+
scope: system?.boundary || system?.description || "",
|
|
2534
|
+
ownerId: system?.ownerIds?.[0] || owner?.id || "",
|
|
2535
|
+
criticality: system?.criticality || "high",
|
|
2536
|
+
classificationId: system?.classificationId || defaultClassificationId(),
|
|
2537
|
+
internetExposed: system?.internetExposed === false ? "false" : "true",
|
|
2538
|
+
programGoal: programGoalFromKind(program.assuranceGoal)
|
|
2427
2539
|
};
|
|
2428
2540
|
}
|
|
2429
2541
|
|
|
@@ -2432,60 +2544,51 @@ function onboardingSteps() {
|
|
|
2432
2544
|
target: ".repo-chip",
|
|
2433
2545
|
kicker: "Mental model",
|
|
2434
2546
|
title: "Files are the program",
|
|
2435
|
-
body: "
|
|
2547
|
+
body: "Source files live under data/. The UI, CLI, and direct edits change the same files. Git records the history.",
|
|
2436
2548
|
points: [
|
|
2437
|
-
"
|
|
2438
|
-
"
|
|
2439
|
-
"
|
|
2549
|
+
"JSON holds fields. Markdown holds long-form content.",
|
|
2550
|
+
"Record status tracks approval.",
|
|
2551
|
+
"Trunk-mode browser saves validate, commit, and push."
|
|
2440
2552
|
]
|
|
2441
2553
|
};
|
|
2442
2554
|
const path = {
|
|
2443
2555
|
target: ".readiness-map",
|
|
2444
2556
|
kicker: "Program model",
|
|
2445
2557
|
title: "Follow the audit chain",
|
|
2446
|
-
body: "Follow
|
|
2447
|
-
points: [
|
|
2448
|
-
"Define the people, criteria, service, Systems, and providers in scope.",
|
|
2449
|
-
"Approve the policies, implement the controls, then operate them and retain dated proof.",
|
|
2450
|
-
"Create the audit engagement when a CPA firm is involved or a real customer deadline requires it."
|
|
2451
|
-
]
|
|
2558
|
+
body: "Follow five steps. Each page shows the next decision.",
|
|
2452
2559
|
};
|
|
2453
2560
|
const operation = {
|
|
2454
2561
|
target: ".obligation-panel",
|
|
2455
2562
|
kicker: "Program operation",
|
|
2456
|
-
title: "
|
|
2457
|
-
body: "
|
|
2563
|
+
title: "Run work and record changes",
|
|
2564
|
+
body: "Work Queue holds scheduled and assigned tasks. Policy Events add tasks when a listed change occurs.",
|
|
2458
2565
|
points: [
|
|
2459
|
-
"
|
|
2460
|
-
"
|
|
2461
|
-
"The browser and CLI use the same schedules, event rules, and completion checks."
|
|
2566
|
+
"Open a task to see its owner, due date, and proof.",
|
|
2567
|
+
"Record events after they happen."
|
|
2462
2568
|
]
|
|
2463
2569
|
};
|
|
2464
2570
|
const auditPath = {
|
|
2465
2571
|
target: null,
|
|
2466
2572
|
kicker: "Report goal and audit",
|
|
2467
|
-
title: "Choose
|
|
2468
|
-
body:
|
|
2469
|
-
"SOC 2 is an independent CPA report on controls relevant to the selected Trust Services Criteria.",
|
|
2470
|
-
"Most customer requests focus on Security. Add another category only when the service and customer need call for it. Choose a management goal now, then create an Audit record only after a real CPA engagement or customer deadline exists."
|
|
2471
|
-
],
|
|
2573
|
+
title: "Choose a goal",
|
|
2574
|
+
body: "SOC 2 is an independent CPA report on controls tied to selected Trust Services Criteria.",
|
|
2472
2575
|
sections: [
|
|
2473
2576
|
{
|
|
2474
2577
|
title: "Type 1",
|
|
2475
|
-
body: "
|
|
2578
|
+
body: "Design and implementation at a point in time. Optional before Type 2."
|
|
2476
2579
|
},
|
|
2477
2580
|
{
|
|
2478
2581
|
title: "Type 2",
|
|
2479
|
-
body: "
|
|
2582
|
+
body: "Operation across a period. Evidence and populations must cover it."
|
|
2480
2583
|
}
|
|
2481
2584
|
],
|
|
2482
|
-
afterSections: "FileGRC prepares the records and packet. The CPA
|
|
2585
|
+
afterSections: "FileGRC prepares the records and evidence packet. The CPA tests and reports."
|
|
2483
2586
|
};
|
|
2484
2587
|
const setup = {
|
|
2485
2588
|
target: null,
|
|
2486
2589
|
kicker: "Initial scope",
|
|
2487
|
-
title: "Describe the service
|
|
2488
|
-
body: "Create the first
|
|
2590
|
+
title: "Describe the service in scope",
|
|
2591
|
+
body: "Create the first System and choose the program goal."
|
|
2489
2592
|
};
|
|
2490
2593
|
return [
|
|
2491
2594
|
files,
|
|
@@ -2512,10 +2615,10 @@ function renderOnboardingStep() {
|
|
|
2512
2615
|
? onboardingSetupForm()
|
|
2513
2616
|
: description + explanation + afterSections;
|
|
2514
2617
|
const finalActions = onboardingStep === steps.length - 1
|
|
2515
|
-
? '<span class="onboarding-save-status" role="status" aria-live="polite"></span><button class="button" type="button" data-onboarding="draft">Save as planned</button><button class="button primary" type="button" data-onboarding="next">Confirm
|
|
2618
|
+
? '<span class="onboarding-save-status" role="status" aria-live="polite"></span><button class="button" type="button" data-onboarding="draft">Save as planned</button><button class="button primary" type="button" data-onboarding="next">Confirm scope</button>'
|
|
2516
2619
|
: '<button class="button primary" type="button" data-onboarding="next">Next</button>';
|
|
2517
|
-
onboardingDialog.innerHTML = '<div class="onboarding-progress" style="--onboarding-step-count:' + steps.length + '" aria-label="Onboarding step ' + (onboardingStep + 1) + ' of ' + steps.length + '">' + progress + '</div><div class="onboarding-scroll"><div class="onboarding-head"><p class="kicker">' + esc(step.kicker) + ' · ' + (onboardingStep + 1) + ' of ' + steps.length + '</p><h2 id="onboarding-title">' + esc(titleCase(step.title)) + '</h2></div>' + body + '<div class="dialog-error" role="alert"></div></div><div class="dialog-actions onboarding-actions"><button class="button text-button onboarding-skip" type="button" data-onboarding="skip">Skip onboarding</button>' + (onboardingStep ? '<button class="button" type="button" data-onboarding="back">Back</button>' : "") + finalActions + '</div>';
|
|
2518
|
-
onboardingDialog.querySelector('[data-onboarding="skip"]').addEventListener("click", cancelOnboarding);
|
|
2620
|
+
onboardingDialog.innerHTML = '<div class="onboarding-progress" style="--onboarding-step-count:' + steps.length + '" aria-label="Onboarding step ' + (onboardingStep + 1) + ' of ' + steps.length + '">' + progress + '</div><div class="onboarding-scroll"><div class="onboarding-head"><p class="kicker">' + esc(step.kicker) + ' · ' + (onboardingStep + 1) + ' of ' + steps.length + '</p><h2 id="onboarding-title">' + esc(titleCase(step.title)) + '</h2></div>' + body + '<div class="dialog-error" role="alert"></div></div><div class="dialog-actions onboarding-actions"><button class="button text-button onboarding-skip" type="button" data-onboarding="skip">' + (onboardingSetupOnly ? "Close" : "Skip onboarding") + '</button>' + (onboardingStep ? '<button class="button" type="button" data-onboarding="back">Back</button>' : "") + finalActions + '</div>';
|
|
2621
|
+
onboardingDialog.querySelector('[data-onboarding="skip"]').addEventListener("click", onboardingSetupOnly ? closeOnboarding : cancelOnboarding);
|
|
2519
2622
|
onboardingDialog.querySelector('[data-onboarding="back"]')?.addEventListener("click", () => {
|
|
2520
2623
|
captureOnboardingForm();
|
|
2521
2624
|
onboardingStep -= 1;
|
|
@@ -2548,16 +2651,21 @@ function renderOnboardingStep() {
|
|
|
2548
2651
|
|
|
2549
2652
|
function onboardingSetupForm() {
|
|
2550
2653
|
const people = resourcesOfType("person").filter(({ record }) => record.status === "active");
|
|
2551
|
-
const
|
|
2654
|
+
const resourceClassifications = resourcesOfType("classification")
|
|
2655
|
+
.filter(({ record }) => record.status === "active")
|
|
2656
|
+
.map(({ record }) => record.id);
|
|
2657
|
+
const classifications = resourceClassifications.length
|
|
2658
|
+
? resourceClassifications
|
|
2659
|
+
: Object.keys(state.workspace.classificationDefinitions || {});
|
|
2552
2660
|
if (onboardingDraft.classificationId && !classifications.includes(onboardingDraft.classificationId)) {
|
|
2553
2661
|
classifications.push(onboardingDraft.classificationId);
|
|
2554
2662
|
}
|
|
2555
2663
|
const gitStatus = state.repository?.mode === "trunk"
|
|
2556
|
-
? '<div class="onboarding-git-status ' + (state.repository.status === "synced" ? "" : "warning") + '"><span class="status-dot ' + repositoryStatusTone(state.repository.status) + '"></span><span><strong>' + esc(state.repository.label) + '</strong><small>' + esc(state.repository.status === "synced" ? "Completing onboarding will save its related
|
|
2664
|
+
? '<div class="onboarding-git-status ' + (state.repository.status === "synced" ? "" : "warning") + '"><span class="status-dot ' + repositoryStatusTone(state.repository.status) + '"></span><span><strong>' + esc(state.repository.label) + '</strong><small>' + esc(state.repository.status === "synced" ? "Completing onboarding will save its related Workspace, Program, System, Component, and renderer changes in one local commit, then push it in the background." : state.repository.message) + '</small></span></div>'
|
|
2557
2665
|
: state.git.available && state.git.branch
|
|
2558
2666
|
? '<div class="onboarding-git-status"><span class="status-dot good"></span><span><strong>Manual repository mode</strong><small>Setup changes will stay local until you commit and synchronize them.</small></span></div>'
|
|
2559
2667
|
: '<div class="onboarding-git-status warning"><span class="status-dot warn"></span><span><strong>Git setup needed</strong><small>Manual-mode writes still work, but Git history is unavailable until the repository is configured.</small></span></div>';
|
|
2560
|
-
return '<p class="onboarding-body">' + esc(onboardingSteps().at(-1).body) + '</p>' + gitStatus + '<form id="onboarding-setup" class="onboarding-form"><label class="wide"><span>Service name</span><input name="serviceName" required maxlength="200" value="' + esc(onboardingDraft.serviceName) + '" placeholder="Customer-facing application"></label><label class="wide"><span>Scope description</span><textarea name="scope" required maxlength="2000" placeholder="What the service does and which production boundary is in scope">' + esc(onboardingDraft.scope) + '</textarea></label><label><span>Accountable owner</span><select name="ownerId" required><option value="">Select</option>' + people.map(({ record }) => '<option value="' + esc(record.id) + '" ' + (record.id === onboardingDraft.ownerId ? "selected" : "") + '>' + esc(record.title) + '</option>').join("") + '</select></label><label><span>Business criticality</span><select name="criticality" required>' + ["low", "medium", "high", "critical"].map((value) => '<option value="' + value + '" ' + (value === onboardingDraft.criticality ? "selected" : "") + '>' + esc(properCase(value)) + '</option>').join("") + '</select></label><label><span>Highest data classification</span><select name="classificationId" required>' + classifications.map((value) => '<option value="' + esc(value) + '" ' + (value === onboardingDraft.classificationId ? "selected" : "") + '>' + esc(properCase(value)) + '</option>').join("") + '</select></label><label><span>Internet exposed</span><select name="internetExposed" required><option value="true" ' + (onboardingDraft.internetExposed === "true" ? "selected" : "") + '>Yes</option><option value="false" ' + (onboardingDraft.internetExposed === "false" ? "selected" : "") + '>No</option></select></label><label class="wide"><span>Program goal</span><select name="programGoal" required><option value="none" ' + (onboardingDraft.programGoal === "none" ? "selected" : "") + '>No
|
|
2668
|
+
return '<p class="onboarding-body">' + esc(onboardingSteps().at(-1).body) + '</p>' + gitStatus + '<form id="onboarding-setup" class="onboarding-form"><label class="wide"><span>Service name</span><input name="serviceName" required maxlength="200" value="' + esc(onboardingDraft.serviceName) + '" placeholder="Customer-facing application"></label><label class="wide"><span>Scope description</span><textarea name="scope" required maxlength="2000" placeholder="What the service does and which production boundary is in scope">' + esc(onboardingDraft.scope) + '</textarea></label><label><span>Accountable owner</span><select name="ownerId" required><option value="">Select</option>' + people.map(({ record }) => '<option value="' + esc(record.id) + '" ' + (record.id === onboardingDraft.ownerId ? "selected" : "") + '>' + esc(record.title) + '</option>').join("") + '</select></label><label><span>Business criticality</span><select name="criticality" required>' + ["low", "medium", "high", "critical"].map((value) => '<option value="' + value + '" ' + (value === onboardingDraft.criticality ? "selected" : "") + '>' + esc(properCase(value)) + '</option>').join("") + '</select></label><label><span>Highest data classification</span><select name="classificationId" required>' + classifications.map((value) => '<option value="' + esc(value) + '" ' + (value === onboardingDraft.classificationId ? "selected" : "") + '>' + esc(properCase(value)) + '</option>').join("") + '</select></label><label><span>Internet exposed</span><select name="internetExposed" required><option value="true" ' + (onboardingDraft.internetExposed === "true" ? "selected" : "") + '>Yes</option><option value="false" ' + (onboardingDraft.internetExposed === "false" ? "selected" : "") + '>No</option></select></label><label class="wide"><span>Program goal</span><select name="programGoal" required><option value="none" ' + (onboardingDraft.programGoal === "none" ? "selected" : "") + '>No assurance goal yet</option><option value="readiness" ' + (onboardingDraft.programGoal === "readiness" ? "selected" : "") + '>Program readiness</option><option value="type-1" ' + (onboardingDraft.programGoal === "type-1" ? "selected" : "") + '>SOC 2 Type 1</option><option value="type-2" ' + (onboardingDraft.programGoal === "type-2" ? "selected" : "") + '>SOC 2 Type 2</option></select><small>Sets management intent. Audits and report periods are separate.</small></label></form><p class="onboarding-write-note">Planned saves a draft. Confirm scope activates it. Both add it to the Program.</p>';
|
|
2561
2669
|
}
|
|
2562
2670
|
|
|
2563
2671
|
function captureOnboardingForm() {
|
|
@@ -2837,8 +2945,9 @@ function openEditor(type, entry = null, options = {}) {
|
|
|
2837
2945
|
const recordContentItem = recordContent ? entry?.content?.[recordContent.slot] : null;
|
|
2838
2946
|
const editorDescription = options.description
|
|
2839
2947
|
|| implementationEditorDescription(type)
|
|
2840
|
-
||
|
|
2841
|
-
|
|
2948
|
+
|| conciseResourceDescription(definition)
|
|
2949
|
+
|| "Add the facts known now.";
|
|
2950
|
+
dialog.innerHTML = '<form><div class="dialog-head"><div><p class="kicker">' + (entry ? "Edit record" : options.actionCompletion ? "Complete assigned work" : options.obligationCompletion ? "Record obligation work" : "Create record") + '</p><h2 id="resource-editor-title">' + esc(titleCase(entry?.record.title || record.title || definition.title)) + '</h2></div><button type="button" class="icon-button" data-editor-dismiss aria-label="Close">×</button></div><p>' + esc(editorDescription) + '</p>' + resourceReviewCriteria(type, true) + '<div class="form-grid">' + names.map((name) => editorField(type, name, fields[name], record[name], required.has(name) || conditionMatches(record, fields[name].requiredWhen), Boolean(entry), oneOf.has(name), activeOneOf.has(name))).join("") + '</div>' +
|
|
2842
2951
|
activeMarkdown.map((markdown) => {
|
|
2843
2952
|
const generated = !entry?.content?.[markdown.name];
|
|
2844
2953
|
const source = entry?.content?.[markdown.name]?.source
|
|
@@ -2857,6 +2966,8 @@ function openEditor(type, entry = null, options = {}) {
|
|
|
2857
2966
|
dialog.addEventListener("close", () => dialog.remove());
|
|
2858
2967
|
dialog.querySelectorAll("[data-editor-dismiss]").forEach((button) => button.addEventListener("click", () => dialog.close()));
|
|
2859
2968
|
wireEditorRequirements(dialog, record, fields, oneOfGroups, markdownDefinitions);
|
|
2969
|
+
wireStructuredObjectEditors(dialog);
|
|
2970
|
+
wireChoicePickers(dialog);
|
|
2860
2971
|
dialog.querySelector(".advanced-editor textarea").addEventListener("input", () => {
|
|
2861
2972
|
dialog.dataset.jsonDirty = "true";
|
|
2862
2973
|
dialog.querySelector("form").noValidate = true;
|
|
@@ -2942,22 +3053,79 @@ function openEditor(type, entry = null, options = {}) {
|
|
|
2942
3053
|
}
|
|
2943
3054
|
|
|
2944
3055
|
function implementationEditorDescription(type) {
|
|
3056
|
+
if (type === "person") {
|
|
3057
|
+
return "Record someone who owns, reviews, approves, or performs program work.";
|
|
3058
|
+
}
|
|
3059
|
+
if (type === "appointment") {
|
|
3060
|
+
return "Assign named authority to one person for a defined period.";
|
|
3061
|
+
}
|
|
3062
|
+
if (type === "program") {
|
|
3063
|
+
return "Choose the goal, scope, owners, criteria, controls, and risk method.";
|
|
3064
|
+
}
|
|
2945
3065
|
if (type === "system") {
|
|
2946
|
-
return "
|
|
3066
|
+
return "Define the service boundary, owners, information, and recovery needs.";
|
|
3067
|
+
}
|
|
3068
|
+
if (type === "component") {
|
|
3069
|
+
return "Connect this Component to its Systems and select any evidence roles.";
|
|
2947
3070
|
}
|
|
2948
3071
|
if (type === "control") {
|
|
2949
|
-
return "
|
|
3072
|
+
return "Describe the procedure, Systems, Components, and evidence sources.";
|
|
3073
|
+
}
|
|
3074
|
+
if (type === "evidence") {
|
|
3075
|
+
return "Add a retained export, report, screenshot, signed file, or approved external reference.";
|
|
3076
|
+
}
|
|
3077
|
+
if (type === "audit") {
|
|
3078
|
+
return "Record a real CPA engagement or a customer deadline that requires audit planning.";
|
|
3079
|
+
}
|
|
3080
|
+
if (type === "audit-request") {
|
|
3081
|
+
return "Track one auditor request, owner, due date, response, and evidence.";
|
|
3082
|
+
}
|
|
3083
|
+
if (type === "vendor") {
|
|
3084
|
+
return "Record a material external provider relationship.";
|
|
3085
|
+
}
|
|
3086
|
+
if (type === "classification") {
|
|
3087
|
+
return "Define one information handling level and its rank.";
|
|
3088
|
+
}
|
|
3089
|
+
if (type === "information-type") {
|
|
3090
|
+
return "Name an information category and its default handling level.";
|
|
3091
|
+
}
|
|
3092
|
+
if (type === "policy") {
|
|
3093
|
+
return "Define a program rule, its scope, owner, reviewer, and dates.";
|
|
3094
|
+
}
|
|
3095
|
+
if (type === "obligation") {
|
|
3096
|
+
return "Schedule recurring or event-driven work.";
|
|
3097
|
+
}
|
|
3098
|
+
if (type === "risk") {
|
|
3099
|
+
return "Track one threat or business impact and its treatment.";
|
|
3100
|
+
}
|
|
3101
|
+
if (type === "risk-assessment") {
|
|
3102
|
+
return "Evaluate a defined scope with the Program risk method.";
|
|
3103
|
+
}
|
|
3104
|
+
if (type === "finding") {
|
|
3105
|
+
return "Track a confirmed gap that needs its own remediation.";
|
|
3106
|
+
}
|
|
3107
|
+
if (type === "exception") {
|
|
3108
|
+
return "Record an approved, time-bound departure from a Policy or Control.";
|
|
3109
|
+
}
|
|
3110
|
+
if (type === "action-item") {
|
|
3111
|
+
return "Assign one dated follow-up task and its completion proof.";
|
|
2950
3112
|
}
|
|
2951
3113
|
return "";
|
|
2952
3114
|
}
|
|
2953
3115
|
|
|
3116
|
+
function conciseResourceDescription(definition) {
|
|
3117
|
+
const description = definition?.description?.trim() || "";
|
|
3118
|
+
return description.match(/^.*?[.!?](?:\s|$)/)?.[0]?.trim() || description;
|
|
3119
|
+
}
|
|
3120
|
+
|
|
2954
3121
|
function seedRecord(type, definition) {
|
|
2955
3122
|
const record = { id: createResourceId(type, "new", state.resources.map(({ record }) => record.id)), type, title: "" };
|
|
2956
3123
|
const fields = { ...state.model.commonFields, ...definition.fields };
|
|
2957
3124
|
for (const name of definition.required || []) {
|
|
2958
3125
|
const field = fields[name];
|
|
2959
3126
|
if (record[name] !== undefined) continue;
|
|
2960
|
-
if (field.
|
|
3127
|
+
if (field.default !== undefined) record[name] = structuredClone(field.default);
|
|
3128
|
+
else if (field.relation) {
|
|
2961
3129
|
const candidates = relationCandidates(field);
|
|
2962
3130
|
record[name] = field.type === "array" ? (candidates.length === 1 ? [candidates[0].record.id] : []) : (candidates.length === 1 ? candidates[0].record.id : "");
|
|
2963
3131
|
}
|
|
@@ -3011,12 +3179,18 @@ function renderRecordContentEditor(type, entry, options) {
|
|
|
3011
3179
|
if (!config) return "";
|
|
3012
3180
|
const item = entry?.content?.[config.slot];
|
|
3013
3181
|
const source = item?.source || "";
|
|
3014
|
-
const editor = '<label class="content-editor-field record-content-editor"><span>' + esc(config.label) + ' Markdown <small>optional</small></span><textarea data-record-content spellcheck="true" placeholder="
|
|
3182
|
+
const editor = '<label class="content-editor-field record-content-editor"><span>' + esc(config.label) + ' Markdown <small>optional</small></span><textarea data-record-content spellcheck="true" placeholder="' + esc(recordContentPlaceholder(type)) + '">' + esc(source) + '</textarea></label>';
|
|
3015
3183
|
if (config.mode === "default") return editor;
|
|
3016
3184
|
const open = item || options.addRecordContent;
|
|
3017
3185
|
return '<details class="record-content-details" ' + (open ? "open" : "") + '><summary>' + (item ? "Record Markdown" : "Add Record Markdown") + '</summary><p>Use this when the structured fields do not capture the full record.</p>' + editor + '</details>';
|
|
3018
3186
|
}
|
|
3019
3187
|
|
|
3188
|
+
function recordContentPlaceholder(type) {
|
|
3189
|
+
if (type === "system") return "Add architecture, dependencies, or boundary details.";
|
|
3190
|
+
if (type === "component") return "Add operating details or evidence retrieval steps.";
|
|
3191
|
+
return "Document the work, results, decisions, and follow-up.";
|
|
3192
|
+
}
|
|
3193
|
+
|
|
3020
3194
|
function editorField(type, name, field, value, required, editing, oneOfRequired = false, oneOfActive = oneOfRequired) {
|
|
3021
3195
|
const label = fieldLabel(type, name);
|
|
3022
3196
|
const requiredMark = required || field.requiredWhen || oneOfRequired
|
|
@@ -3034,21 +3208,22 @@ function editorField(type, name, field, value, required, editing, oneOfRequired
|
|
|
3034
3208
|
if (field.relation && field.type === "array") {
|
|
3035
3209
|
const candidates = relationCandidates(field);
|
|
3036
3210
|
control = candidates.length
|
|
3037
|
-
? '<div class="checkbox-list">' + candidates.map(({ record }) => '<label><input type="checkbox" value="' + esc(record.id) + '" ' + ((value || []).includes(record.id) ? "checked" : "") + '><span>' + esc(record.title) + '<small>' + esc(state.model.resources[record.type].title
|
|
3038
|
-
: required ? '<select><option value="">No
|
|
3211
|
+
? '<div class="checkbox-list">' + candidates.map(({ record }) => '<label><input type="checkbox" value="' + esc(record.id) + '" ' + ((value || []).includes(record.id) ? "checked" : "") + '><span>' + esc(record.title) + '<small>' + esc(state.model.resources[record.type].title) + '</small></span></label>').join("") + '</div>'
|
|
3212
|
+
: required ? '<select><option value="">No matching ' + esc(relationTypeLabel(field, true).toLowerCase()) + ' yet</option></select>' : '<div class="missing-options">No matching ' + esc(relationTypeLabel(field, true).toLowerCase()) + ' yet.</div>';
|
|
3039
3213
|
return fieldWrap(name, "relation-array", label, requiredMark, control, help, required);
|
|
3040
3214
|
}
|
|
3041
3215
|
if (field.relation) {
|
|
3042
3216
|
const candidates = relationCandidates(field);
|
|
3043
3217
|
control = candidates.length
|
|
3044
|
-
? '<select><option value="">Select
|
|
3045
|
-
: required ? '<select><option value="">No
|
|
3218
|
+
? '<select><option value="">Select ' + esc(relationTypeLabel(field).toLowerCase()) + '</option>' + candidates.map(({ record }) => '<option value="' + esc(record.id) + '" ' + (value === record.id ? "selected" : "") + '>' + esc(record.title) + ' · ' + esc(state.model.resources[record.type].title) + '</option>').join("") + '</select>'
|
|
3219
|
+
: required ? '<select><option value="">No matching ' + esc(relationTypeLabel(field, true).toLowerCase()) + ' yet</option></select>' : '<div class="missing-options">No matching ' + esc(relationTypeLabel(field, true).toLowerCase()) + ' yet.</div>';
|
|
3046
3220
|
return fieldWrap(name, "relation", label, requiredMark, control, help, required);
|
|
3047
3221
|
}
|
|
3048
3222
|
if (name === "classificationId") {
|
|
3049
|
-
const
|
|
3223
|
+
const classifications = resourcesOfType("classification").filter(({ record }) => record.status === "active");
|
|
3224
|
+
const values = classifications.length ? classifications.map(({ record }) => record.id) : Object.keys(state.workspace.classificationDefinitions || {});
|
|
3050
3225
|
control = '<select><option value="">Select</option>' + values.map((item) => '<option value="' + esc(item) + '" ' + (value === item ? "selected" : "") + '>' + esc(properCase(item)) + '</option>').join("") + '</select>';
|
|
3051
|
-
return fieldWrap(name, "string", label, requiredMark, control, "Defined by Workspace classificationDefinitions", required);
|
|
3226
|
+
return fieldWrap(name, "string", label, requiredMark, control, classifications.length ? "References an active Classification" : "Defined by Workspace classificationDefinitions", required);
|
|
3052
3227
|
}
|
|
3053
3228
|
if (field.type === "enum" || field.type === "rating" || field.type === "outcome") {
|
|
3054
3229
|
const values = field.values || (field.type === "rating" ? state.model.primitives.rating : state.model.primitives.outcome) || [];
|
|
@@ -3060,15 +3235,30 @@ function editorField(type, name, field, value, required, editing, oneOfRequired
|
|
|
3060
3235
|
return fieldWrap(name, "boolean", label, requiredMark, control, help, required);
|
|
3061
3236
|
}
|
|
3062
3237
|
if (field.type === "object") {
|
|
3238
|
+
const schema = state.model.objectTypes[field.objectType];
|
|
3239
|
+
if (schema?.additionalProperties === false) {
|
|
3240
|
+
control = '<div class="structured-object-fields" data-object-type="' + esc(field.objectType) + '">' + objectPropertyFields(schema, value || {}) + '</div>';
|
|
3241
|
+
return fieldWrap(name, "structured-object", label, requiredMark, control, "Fill only the details that apply.", required);
|
|
3242
|
+
}
|
|
3063
3243
|
control = '<textarea spellcheck="false" placeholder="{ }">' + esc(value === undefined ? "" : JSON.stringify(value, null, 2)) + '</textarea>';
|
|
3064
3244
|
return fieldWrap(name, "object", label, requiredMark, control, "JSON object", required);
|
|
3065
3245
|
}
|
|
3066
3246
|
if (field.type === "array") {
|
|
3247
|
+
if (field.items === "object") {
|
|
3248
|
+
const schema = state.model.objectTypes[field.itemObjectType];
|
|
3249
|
+
const items = Array.isArray(value) ? value : [];
|
|
3250
|
+
control = '<div class="object-array-items">' + items.map((item) => objectArrayItem(schema, item)).join("") + '</div><button class="button" type="button" data-add-object-item>Add ' + esc(humanize(field.itemObjectType || "item")) + '</button><template>' + objectArrayItem(schema, {}) + '</template>';
|
|
3251
|
+
return fieldWrap(name, "object-array", label, requiredMark, control, "Add one row for each relationship or scoped decision.", required);
|
|
3252
|
+
}
|
|
3253
|
+
if (name === "evidenceSourceKinds") {
|
|
3254
|
+
const selected = Array.isArray(value) ? value : [];
|
|
3255
|
+
const options = evidenceSourceRoleOptions();
|
|
3256
|
+
const summary = selected.length ? selected.length + " selected" : "Choose roles";
|
|
3257
|
+
control = '<details class="choice-picker"><summary><span data-choice-summary>' + esc(summary) + '</span></summary><div class="checkbox-list">' + options.map((option) => '<label><input type="checkbox" value="' + esc(option) + '" ' + (selected.includes(option) ? "checked" : "") + '><span>' + esc(properCase(option)) + '</span></label>').join("") + '</div></details>';
|
|
3258
|
+
return fieldWrap(name, "choice-array", label, requiredMark, control, "Select only roles this Component performs.", required);
|
|
3259
|
+
}
|
|
3067
3260
|
control = '<textarea placeholder="One value per line">' + esc((value || []).join("\n")) + '</textarea>';
|
|
3068
|
-
|
|
3069
|
-
? "One role per line. Use the roles required by the Controls this System supports, such as " + evidenceSourceRoleOptions().join(", ") + "."
|
|
3070
|
-
: "One value per line";
|
|
3071
|
-
return fieldWrap(name, "array", label, requiredMark, control, arrayHelp, required);
|
|
3261
|
+
return fieldWrap(name, "array", label, requiredMark, control, "One value per line", required);
|
|
3072
3262
|
}
|
|
3073
3263
|
if (["description", "statement", "scope", "rationale", "purpose"].some((part) => name.toLowerCase().includes(part))) {
|
|
3074
3264
|
control = '<textarea>' + esc(value ?? "") + '</textarea>';
|
|
@@ -3086,6 +3276,178 @@ function evidenceSourceRoleOptions() {
|
|
|
3086
3276
|
return [...new Set((state.model.evidenceSourceFamilies || []).flatMap(({ sourceKinds }) => sourceKinds || []))].sort();
|
|
3087
3277
|
}
|
|
3088
3278
|
|
|
3279
|
+
function wireChoicePickers(dialog) {
|
|
3280
|
+
dialog.querySelectorAll(".choice-picker").forEach((picker) => {
|
|
3281
|
+
const sync = () => {
|
|
3282
|
+
const count = picker.querySelectorAll('input[type="checkbox"]:checked').length;
|
|
3283
|
+
picker.querySelector("[data-choice-summary]").textContent = count ? count + " selected" : "Choose roles";
|
|
3284
|
+
};
|
|
3285
|
+
picker.addEventListener("change", sync);
|
|
3286
|
+
sync();
|
|
3287
|
+
});
|
|
3288
|
+
}
|
|
3289
|
+
|
|
3290
|
+
function objectPropertyFields(schema, value = {}) {
|
|
3291
|
+
const required = new Set(schema?.required || []);
|
|
3292
|
+
return Object.entries(schema?.properties || {}).map(([name, property]) => {
|
|
3293
|
+
const requiredNow = required.has(name) && !property.requiredWhen;
|
|
3294
|
+
const mark = required.has(name) || property.requiredWhen
|
|
3295
|
+
? '<span class="required-mark" ' + (requiredNow ? "" : "hidden") + '>Required</span>'
|
|
3296
|
+
: "";
|
|
3297
|
+
const stringMap = property.type === "object" && state.model.objectTypes[property.objectType]?.additionalProperties?.type === "string";
|
|
3298
|
+
let input;
|
|
3299
|
+
if (property.relation && property.type === "id") {
|
|
3300
|
+
const candidates = relationCandidates(property);
|
|
3301
|
+
input = '<select><option value="">Select ' + esc(relationTypeLabel(property).toLowerCase()) + '</option>' + candidates.map(({ record }) => '<option value="' + esc(record.id) + '" ' + (value[name] === record.id ? "selected" : "") + '>' + esc(record.title) + ' · ' + esc(state.model.resources[record.type].title) + '</option>').join("") + '</select>';
|
|
3302
|
+
} else if (property.relation && property.type === "array") {
|
|
3303
|
+
const candidates = relationCandidates(property);
|
|
3304
|
+
input = candidates.length
|
|
3305
|
+
? '<div class="checkbox-list">' + candidates.map(({ record }) => '<label><input type="checkbox" value="' + esc(record.id) + '" ' + ((value[name] || []).includes(record.id) ? "checked" : "") + '><span>' + esc(record.title) + '<small>' + esc(state.model.resources[record.type].title) + '</small></span></label>').join("") + '</div>'
|
|
3306
|
+
: '<div class="missing-options">No matching ' + esc(relationTypeLabel(property, true).toLowerCase()) + ' yet.</div>';
|
|
3307
|
+
} else if (property.type === "array" && property.values) {
|
|
3308
|
+
input = '<div class="checkbox-list">' + property.values.map((option) => '<label><input type="checkbox" value="' + esc(option) + '" ' + ((value[name] || []).includes(option) ? "checked" : "") + '><span>' + esc(properCase(option)) + '</span></label>').join("") + '</div>';
|
|
3309
|
+
} else if (property.type === "array") {
|
|
3310
|
+
input = '<textarea placeholder="One value per line">' + esc((value[name] || []).join("\n")) + '</textarea>';
|
|
3311
|
+
} else if (property.type === "enum" || property.type === "rating") {
|
|
3312
|
+
const options = property.values || (property.registry ? Object.keys(state.model[property.registry] || {}) : state.model.primitives[property.type] || []);
|
|
3313
|
+
input = '<select><option value="">Select</option>' + options.map((option) => '<option value="' + esc(option) + '" ' + (value[name] === option ? "selected" : "") + '>' + esc(properCase(option)) + '</option>').join("") + '</select>';
|
|
3314
|
+
} else if (property.type === "object") {
|
|
3315
|
+
const nested = state.model.objectTypes[property.objectType];
|
|
3316
|
+
input = nested?.additionalProperties === false
|
|
3317
|
+
? '<div class="structured-object-fields nested" data-object-type="' + esc(property.objectType) + '">' + objectPropertyFields(nested, value[name] || {}) + '</div>'
|
|
3318
|
+
: nested?.additionalProperties?.type === "string"
|
|
3319
|
+
? stringMapEditor(value[name] || {}, name)
|
|
3320
|
+
: '<textarea spellcheck="false" placeholder="{ }">' + esc(value[name] === undefined ? "" : JSON.stringify(value[name], null, 2)) + '</textarea>';
|
|
3321
|
+
} else if (property.type === "boolean") {
|
|
3322
|
+
input = '<select><option value="">Not set</option><option value="true" ' + (value[name] === true ? "selected" : "") + '>Yes</option><option value="false" ' + (value[name] === false ? "selected" : "") + '>No</option></select>';
|
|
3323
|
+
} else {
|
|
3324
|
+
const multiline = ["rationale", "description", "notes"].some((part) => name.toLowerCase().includes(part));
|
|
3325
|
+
input = multiline
|
|
3326
|
+
? '<textarea>' + esc(value[name] || "") + '</textarea>'
|
|
3327
|
+
: '<input type="' + (property.type === "date" ? "date" : property.type === "timestamp" ? "datetime-local" : ["integer", "number"].includes(property.type) ? "number" : "text") + '" value="' + esc(property.type === "timestamp" && value[name] ? String(value[name]).slice(0, 16) : value[name] ?? "") + '"' + (property.minimum !== undefined ? ' min="' + esc(property.minimum) + '"' : "") + '>';
|
|
3328
|
+
}
|
|
3329
|
+
return '<div class="object-property' + (stringMap ? " string-map-property" : "") + '" data-object-field="' + esc(name) + '" data-object-kind="' + esc(property.type) + '" data-object-required="' + (required.has(name) || property.requiredWhen ? "true" : "false") + '"' + (property.requiredWhen ? ' data-object-required-when="' + esc(JSON.stringify(property.requiredWhen)) + '"' : "") + (property.allowedWhen ? ' data-object-allowed-when="' + esc(JSON.stringify(property.allowedWhen)) + '"' : "") + '><span class="object-property-label">' + esc(humanize(name)) + mark + '</span>' + input + '</div>';
|
|
3330
|
+
}).join("");
|
|
3331
|
+
}
|
|
3332
|
+
|
|
3333
|
+
function objectArrayItem(schema, value = {}) {
|
|
3334
|
+
return '<fieldset class="object-array-item"><legend>' + esc(humanize(schema?.title || "Relationship")) + '</legend><div class="structured-object-fields">' + objectPropertyFields(schema, value) + '</div><button type="button" class="text-button" data-remove-object-item>Remove</button></fieldset>';
|
|
3335
|
+
}
|
|
3336
|
+
|
|
3337
|
+
function stringMapEditor(value = {}, name = "item") {
|
|
3338
|
+
const entries = Object.entries(value);
|
|
3339
|
+
const row = ([key = "", item = ""] = []) => '<div class="string-map-row"><input data-map-key value="' + esc(key) + '" placeholder="Name"><input data-map-value value="' + esc(item) + '" placeholder="Value"><button type="button" class="text-button" data-remove-string-map aria-label="Remove ' + esc(humanize(name).toLowerCase()) + '">Remove</button></div>';
|
|
3340
|
+
return '<div class="string-map-editor"><div class="string-map-items">' + (entries.length ? entries.map(row).join("") : row()) + '</div><button type="button" class="button" data-add-string-map>Add ' + esc(humanize(name).replace(/s$/, "").toLowerCase()) + '</button><template>' + row() + '</template></div>';
|
|
3341
|
+
}
|
|
3342
|
+
|
|
3343
|
+
function wireStructuredObjectEditors(dialog) {
|
|
3344
|
+
dialog.querySelectorAll('[data-kind="object-array"]').forEach((group) => {
|
|
3345
|
+
group.querySelector('[data-add-object-item]')?.addEventListener("click", () => {
|
|
3346
|
+
const template = group.querySelector("template");
|
|
3347
|
+
group.querySelector(".object-array-items").append(template.content.cloneNode(true));
|
|
3348
|
+
refresh(group.querySelector(".object-array-items > .object-array-item:last-child > .structured-object-fields"));
|
|
3349
|
+
});
|
|
3350
|
+
group.addEventListener("click", (event) => {
|
|
3351
|
+
const remove = event.target.closest("[data-remove-object-item]");
|
|
3352
|
+
if (!remove) return;
|
|
3353
|
+
remove.closest(".object-array-item")?.remove();
|
|
3354
|
+
});
|
|
3355
|
+
});
|
|
3356
|
+
dialog.querySelector("form").addEventListener("click", (event) => {
|
|
3357
|
+
const add = event.target.closest("[data-add-string-map]");
|
|
3358
|
+
if (add) {
|
|
3359
|
+
const editor = add.closest(".string-map-editor");
|
|
3360
|
+
editor.querySelector(".string-map-items").append(editor.querySelector("template").content.cloneNode(true));
|
|
3361
|
+
editor.querySelector(".string-map-row:last-child [data-map-key]").focus();
|
|
3362
|
+
return;
|
|
3363
|
+
}
|
|
3364
|
+
const remove = event.target.closest("[data-remove-string-map]");
|
|
3365
|
+
if (!remove) return;
|
|
3366
|
+
const editor = remove.closest(".string-map-editor");
|
|
3367
|
+
const rows = editor.querySelectorAll(".string-map-row");
|
|
3368
|
+
if (rows.length > 1) remove.closest(".string-map-row").remove();
|
|
3369
|
+
else rows[0].querySelectorAll("input").forEach((input) => { input.value = ""; });
|
|
3370
|
+
});
|
|
3371
|
+
const refresh = (container) => {
|
|
3372
|
+
const fields = [...container.querySelectorAll(":scope > [data-object-field]")];
|
|
3373
|
+
const values = Object.fromEntries(fields.map((field) => [field.dataset.objectField, readObjectProperty(field)]));
|
|
3374
|
+
const hasValue = Object.values(values).some((value) => value !== undefined && (!Array.isArray(value) || value.length) && (typeof value !== "object" || Array.isArray(value) || Object.keys(value).length));
|
|
3375
|
+
const group = container.closest("[data-field-group]");
|
|
3376
|
+
const active = Boolean(container.closest(".object-array-item")) || group?.dataset.required === "true" || hasValue;
|
|
3377
|
+
for (const field of fields) {
|
|
3378
|
+
const allowedWhen = field.dataset.objectAllowedWhen ? JSON.parse(field.dataset.objectAllowedWhen) : null;
|
|
3379
|
+
const requiredWhen = field.dataset.objectRequiredWhen ? JSON.parse(field.dataset.objectRequiredWhen) : null;
|
|
3380
|
+
field.hidden = Boolean(allowedWhen) && !conditionMatches(values, allowedWhen);
|
|
3381
|
+
const required = active && !field.hidden && (field.dataset.objectRequired === "true") && (!requiredWhen || conditionMatches(values, requiredWhen));
|
|
3382
|
+
field.querySelector(".required-mark")?.toggleAttribute("hidden", !required);
|
|
3383
|
+
const checkboxes = [...field.querySelectorAll(":scope > .checkbox-list input[type=checkbox]")];
|
|
3384
|
+
if (checkboxes.length) {
|
|
3385
|
+
const selected = checkboxes.findIndex((checkbox) => checkbox.checked);
|
|
3386
|
+
checkboxes.forEach((checkbox, index) => { checkbox.required = required && index === (selected < 0 ? 0 : selected); });
|
|
3387
|
+
} else {
|
|
3388
|
+
const map = field.querySelector(":scope > .string-map-editor");
|
|
3389
|
+
if (map) {
|
|
3390
|
+
[...map.querySelectorAll(".string-map-row")].forEach((row, index) => {
|
|
3391
|
+
const key = row.querySelector("[data-map-key]");
|
|
3392
|
+
const value = row.querySelector("[data-map-value]");
|
|
3393
|
+
const rowRequired = required && index === 0 || key.value.trim() || value.value.trim();
|
|
3394
|
+
key.required = Boolean(rowRequired);
|
|
3395
|
+
value.required = Boolean(rowRequired);
|
|
3396
|
+
});
|
|
3397
|
+
continue;
|
|
3398
|
+
}
|
|
3399
|
+
const control = field.querySelector(":scope > input, :scope > select, :scope > textarea");
|
|
3400
|
+
if (control) control.required = required;
|
|
3401
|
+
}
|
|
3402
|
+
}
|
|
3403
|
+
};
|
|
3404
|
+
dialog.querySelectorAll(".structured-object-fields").forEach(refresh);
|
|
3405
|
+
dialog.querySelector("form").addEventListener("input", (event) => {
|
|
3406
|
+
const container = event.target.closest(".structured-object-fields");
|
|
3407
|
+
if (container) refresh(container);
|
|
3408
|
+
});
|
|
3409
|
+
dialog.querySelector("form").addEventListener("change", (event) => {
|
|
3410
|
+
const container = event.target.closest(".structured-object-fields");
|
|
3411
|
+
if (container) refresh(container);
|
|
3412
|
+
});
|
|
3413
|
+
}
|
|
3414
|
+
|
|
3415
|
+
function readObjectProperty(field) {
|
|
3416
|
+
if (field.hidden) return undefined;
|
|
3417
|
+
const kind = field.dataset.objectKind;
|
|
3418
|
+
if (kind === "array") {
|
|
3419
|
+
const checkboxes = [...field.querySelectorAll(":scope > .checkbox-list input[type=checkbox]")];
|
|
3420
|
+
if (checkboxes.length) return checkboxes.filter((checkbox) => checkbox.checked).map((checkbox) => checkbox.value);
|
|
3421
|
+
return (field.querySelector(":scope > textarea")?.value || "").split(/\n|,/).map((item) => item.trim()).filter(Boolean);
|
|
3422
|
+
}
|
|
3423
|
+
if (kind === "object") {
|
|
3424
|
+
const nested = field.querySelector(":scope > .structured-object-fields");
|
|
3425
|
+
if (nested) return readStructuredObject(nested);
|
|
3426
|
+
const map = field.querySelector(":scope > .string-map-editor");
|
|
3427
|
+
if (map) return readStringMap(map);
|
|
3428
|
+
const raw = field.querySelector(":scope > textarea")?.value.trim();
|
|
3429
|
+
return raw ? JSON.parse(raw) : undefined;
|
|
3430
|
+
}
|
|
3431
|
+
const raw = field.querySelector(":scope > input, :scope > select, :scope > textarea")?.value ?? "";
|
|
3432
|
+
if (raw === "") return undefined;
|
|
3433
|
+
if (kind === "boolean") return raw === "true";
|
|
3434
|
+
if (["integer", "number"].includes(kind)) return Number(raw);
|
|
3435
|
+
if (kind === "timestamp") return new Date(raw).toISOString();
|
|
3436
|
+
return raw;
|
|
3437
|
+
}
|
|
3438
|
+
|
|
3439
|
+
function readStringMap(container) {
|
|
3440
|
+
return Object.fromEntries([...container.querySelectorAll(".string-map-row")]
|
|
3441
|
+
.map((row) => [row.querySelector("[data-map-key]").value.trim(), row.querySelector("[data-map-value]").value.trim()])
|
|
3442
|
+
.filter(([key, value]) => key && value));
|
|
3443
|
+
}
|
|
3444
|
+
|
|
3445
|
+
function readStructuredObject(container) {
|
|
3446
|
+
return Object.fromEntries([...container.querySelectorAll(":scope > [data-object-field]")]
|
|
3447
|
+
.map((field) => [field.dataset.objectField, readObjectProperty(field)])
|
|
3448
|
+
.filter(([, value]) => value !== undefined && (!Array.isArray(value) || value.length) && (typeof value !== "object" || Array.isArray(value) || Object.keys(value).length)));
|
|
3449
|
+
}
|
|
3450
|
+
|
|
3089
3451
|
function fieldWrap(name, kind, label, requiredMark, control, help, required) {
|
|
3090
3452
|
const labelId = "field-label-" + name;
|
|
3091
3453
|
let labelledControl = control.replace(/^<([a-z]+)/, '<$1 aria-labelledby="' + esc(labelId) + '"');
|
|
@@ -3211,18 +3573,28 @@ function readGuidedRecord(dialog, base, fields) {
|
|
|
3211
3573
|
}
|
|
3212
3574
|
const kind = group.dataset.kind;
|
|
3213
3575
|
let value;
|
|
3214
|
-
if (kind === "relation-array") value = [...group.querySelectorAll('input[type="checkbox"]:checked')].map((input) => input.value);
|
|
3576
|
+
if (kind === "relation-array" || kind === "choice-array") value = [...group.querySelectorAll('input[type="checkbox"]:checked')].map((input) => input.value);
|
|
3215
3577
|
else {
|
|
3216
3578
|
const control = group.querySelector("input,select,textarea");
|
|
3217
3579
|
const raw = control?.value ?? "";
|
|
3218
3580
|
if (kind === "array") value = raw.split(/\n|,/).map((item) => item.trim()).filter(Boolean);
|
|
3581
|
+
else if (kind === "object-array") {
|
|
3582
|
+
value = [...group.querySelectorAll(".object-array-items > .object-array-item")]
|
|
3583
|
+
.map((item) => readStructuredObject(item.querySelector(":scope > .structured-object-fields")))
|
|
3584
|
+
.filter((item) => Object.keys(item).length);
|
|
3585
|
+
}
|
|
3586
|
+
else if (kind === "structured-object") value = readStructuredObject(group.querySelector(":scope > .structured-object-fields"));
|
|
3219
3587
|
else if (kind === "object") value = raw.trim() ? JSON.parse(raw) : undefined;
|
|
3220
3588
|
else if (kind === "boolean") value = raw === "" ? undefined : raw === "true";
|
|
3221
3589
|
else if (kind === "integer") value = raw === "" ? undefined : Number(raw);
|
|
3222
3590
|
else if (kind === "number") value = raw === "" ? undefined : Number(raw);
|
|
3223
3591
|
else value = raw;
|
|
3224
3592
|
}
|
|
3225
|
-
|
|
3593
|
+
const empty = value === ""
|
|
3594
|
+
|| value === undefined
|
|
3595
|
+
|| Array.isArray(value) && !value.length
|
|
3596
|
+
|| value && typeof value === "object" && !Array.isArray(value) && !Object.keys(value).length;
|
|
3597
|
+
if (empty && group.dataset.required !== "true") delete record[name];
|
|
3226
3598
|
else record[name] = value;
|
|
3227
3599
|
}
|
|
3228
3600
|
return record;
|
|
@@ -3232,8 +3604,20 @@ function relationCandidates(field) {
|
|
|
3232
3604
|
return state.resources.filter(({ record }) => field.relation.includes("*") || field.relation.includes(record.type));
|
|
3233
3605
|
}
|
|
3234
3606
|
|
|
3607
|
+
function relationTypeLabel(field, plural = false) {
|
|
3608
|
+
if (field.relation.includes("*")) return plural ? "Resources" : "resource";
|
|
3609
|
+
if (field.relation.length > 3) return plural ? "Resources" : "resource";
|
|
3610
|
+
const labels = field.relation.map((type) => (
|
|
3611
|
+
plural ? state.model.resources[type]?.pluralTitle : state.model.resources[type]?.title
|
|
3612
|
+
) || humanize(type));
|
|
3613
|
+
if (labels.length < 2) return labels.join("");
|
|
3614
|
+
if (labels.length === 2) return labels.join(" or ");
|
|
3615
|
+
return labels.slice(0, -1).join(", ") + ", or " + labels.at(-1);
|
|
3616
|
+
}
|
|
3617
|
+
|
|
3235
3618
|
function relationHelp(field) {
|
|
3236
3619
|
if (field.relation.includes("*")) return "References any resource";
|
|
3620
|
+
if (field.relation.length > 3) return "References supported records";
|
|
3237
3621
|
const labels = field.relation.map((type) => state.model.resources[type]?.pluralTitle || type);
|
|
3238
3622
|
if (labels.length < 2) return "References " + labels.join("");
|
|
3239
3623
|
if (labels.length === 2) return "References " + labels.join(" or ");
|
|
@@ -3333,7 +3717,14 @@ function globalSearch(query) {
|
|
|
3333
3717
|
dialog.innerHTML = '<div class="dialog-head"><div><p class="kicker">Search</p><h2 id="search-results-title">' + esc(query) + '</h2></div><button class="icon-button" aria-label="Close">×</button></div><div class="result-list"></div><nav class="pagination search-pagination" aria-label="Search result pages" hidden><button class="button" type="button" data-search-page="previous">Previous</button><span class="page-status" aria-live="polite"></span><button class="button" type="button" data-search-page="next">Next</button></nav>';
|
|
3334
3718
|
document.body.append(dialog);
|
|
3335
3719
|
dialog.showModal();
|
|
3336
|
-
|
|
3720
|
+
const clearSearch = () => {
|
|
3721
|
+
const search = root.querySelector("#global-search");
|
|
3722
|
+
if (search?.value.trim() === query) search.value = "";
|
|
3723
|
+
};
|
|
3724
|
+
dialog.querySelector(".icon-button").onclick = () => {
|
|
3725
|
+
clearSearch();
|
|
3726
|
+
dialog.close();
|
|
3727
|
+
};
|
|
3337
3728
|
const results = dialog.querySelector(".result-list");
|
|
3338
3729
|
const pagination = dialog.querySelector(".search-pagination");
|
|
3339
3730
|
const previous = pagination.querySelector('[data-search-page="previous"]');
|
|
@@ -3344,7 +3735,10 @@ function globalSearch(query) {
|
|
|
3344
3735
|
const start = (pageNumber - 1) * SEARCH_PAGE_SIZE;
|
|
3345
3736
|
const visible = matches.slice(start, start + SEARCH_PAGE_SIZE);
|
|
3346
3737
|
results.innerHTML = visible.length ? visible.map(({ record }) => '<a href="#/resource/' + encodeURIComponent(record.type) + '/' + encodeURIComponent(record.id) + '"><strong>' + esc(record.title) + '</strong><small>' + esc(state.model.resources[record.type].title) + '</small></a>').join("") : empty("No matching records.");
|
|
3347
|
-
results.querySelectorAll("a").forEach((link) => link.onclick = () =>
|
|
3738
|
+
results.querySelectorAll("a").forEach((link) => link.onclick = () => {
|
|
3739
|
+
clearSearch();
|
|
3740
|
+
dialog.close();
|
|
3741
|
+
});
|
|
3348
3742
|
pagination.hidden = totalPages === 1;
|
|
3349
3743
|
previous.disabled = pageNumber === 1;
|
|
3350
3744
|
next.disabled = pageNumber === totalPages;
|
|
@@ -3363,7 +3757,10 @@ function globalSearch(query) {
|
|
|
3363
3757
|
results.scrollTop = 0;
|
|
3364
3758
|
});
|
|
3365
3759
|
renderResults();
|
|
3366
|
-
dialog.addEventListener("close", () =>
|
|
3760
|
+
dialog.addEventListener("close", () => {
|
|
3761
|
+
clearSearch();
|
|
3762
|
+
dialog.remove();
|
|
3763
|
+
});
|
|
3367
3764
|
}
|
|
3368
3765
|
|
|
3369
3766
|
function auditProgress(audit) {
|
|
@@ -3538,9 +3935,15 @@ function conditionMatchesValues(condition, valueFor) {
|
|
|
3538
3935
|
function conditionValueMatches(actual, expected) {
|
|
3539
3936
|
return Array.isArray(expected) ? expected.includes(actual) : actual === expected;
|
|
3540
3937
|
}
|
|
3541
|
-
function formatValue(value, field, type) {
|
|
3938
|
+
function formatValue(value, field, type, compact = false) {
|
|
3542
3939
|
if (value === undefined || value === null || value === "") return '<span class="muted">Not set</span>';
|
|
3543
3940
|
const definition = fieldDefinition(type, field);
|
|
3941
|
+
if (type === "program" && field === "requirementApplicability" && Array.isArray(value)) {
|
|
3942
|
+
const reviewed = value.filter(({ decision }) => ["applicable", "not-applicable"].includes(decision)).length;
|
|
3943
|
+
const pending = value.length - reviewed;
|
|
3944
|
+
const label = reviewed + " reviewed" + (pending ? " · " + pending + " to review" : "");
|
|
3945
|
+
return '<a class="tag relation" href="#/resources/requirement' + (pending ? "?review=1" : "") + '">' + esc(label) + '</a>';
|
|
3946
|
+
}
|
|
3544
3947
|
if (definition?.type === "date") return esc(formatCalendarDate(value));
|
|
3545
3948
|
if (definition?.type === "timestamp") return esc(formatLocalDateTime(value));
|
|
3546
3949
|
if (type === "obligation" && field === "status") {
|
|
@@ -3548,7 +3951,12 @@ function formatValue(value, field, type) {
|
|
|
3548
3951
|
return '<span class="badge ' + (value === "active" ? "neutral" : "status-" + esc(String(value))) + '">' + esc(label) + '</span>';
|
|
3549
3952
|
}
|
|
3550
3953
|
if (field === "status" || field.endsWith("Rating") || field === "severity" || field === "outcome") return '<span class="badge status-' + esc(String(value)) + '">' + esc(properCase(value)) + '</span>';
|
|
3551
|
-
if (Array.isArray(value)
|
|
3954
|
+
if (Array.isArray(value) && definition?.items === "object") return formatObjectArray(value, definition.itemObjectType, compact);
|
|
3955
|
+
if (Array.isArray(value)) {
|
|
3956
|
+
if (!value.length) return '<span class="muted">None</span>';
|
|
3957
|
+
if (field === "evidenceSourceKinds") return value.map((item) => '<span class="tag choice-tag">' + esc(properCase(item)) + '</span>').join(" ");
|
|
3958
|
+
return value.map((item) => formatReference(item)).join(" ");
|
|
3959
|
+
}
|
|
3552
3960
|
if (field === "sourceReference" && typeof value === "object") {
|
|
3553
3961
|
const href = safeExternalUrl(value.url);
|
|
3554
3962
|
if (href) return '<a class="external-source" href="' + esc(href) + '" target="_blank" rel="noopener noreferrer"><span><strong>' + esc(value.title || "Official source") + '</strong><small>' + esc(href) + '</small></span><b aria-hidden="true">↗</b></a>';
|
|
@@ -3560,6 +3968,33 @@ function formatValue(value, field, type) {
|
|
|
3560
3968
|
if (definition?.type === "enum") return esc(properCase(value));
|
|
3561
3969
|
return esc(String(value));
|
|
3562
3970
|
}
|
|
3971
|
+
|
|
3972
|
+
function formatObjectArray(items, objectType, compact = false) {
|
|
3973
|
+
if (!items.length) return '<span class="muted">None</span>';
|
|
3974
|
+
const schema = state.model.objectTypes[objectType];
|
|
3975
|
+
if (!schema) return '<span class="muted">' + items.length + ' ' + pluralize("item", items.length) + '</span>';
|
|
3976
|
+
return '<div class="object-value-list ' + (compact ? "compact" : "") + '">' + items.map((item) => {
|
|
3977
|
+
const notes = [];
|
|
3978
|
+
const facts = Object.entries(schema.properties || {}).flatMap(([name, property]) => {
|
|
3979
|
+
const value = item[name];
|
|
3980
|
+
if (value === undefined || value === null || value === "" || Array.isArray(value) && !value.length) return [];
|
|
3981
|
+
if (["rationale", "description", "notes"].includes(name)) {
|
|
3982
|
+
notes.push(String(value));
|
|
3983
|
+
return [];
|
|
3984
|
+
}
|
|
3985
|
+
const label = humanize(name.replace(/Ids?$/, ""));
|
|
3986
|
+
let display;
|
|
3987
|
+
if (property.relation && property.type === "id") display = formatReference(value);
|
|
3988
|
+
else if (property.relation && property.type === "array") display = value.map((id) => formatReference(id)).join(" ");
|
|
3989
|
+
else if (Array.isArray(value)) display = esc(value.map((entry) => properCase(entry)).join(", "));
|
|
3990
|
+
else if (property.type === "enum") display = esc(properCase(value));
|
|
3991
|
+
else if (property.type === "date") display = esc(formatCalendarDate(value));
|
|
3992
|
+
else display = esc(String(value));
|
|
3993
|
+
return ['<span><b>' + esc(label) + '</b>' + display + '</span>'];
|
|
3994
|
+
});
|
|
3995
|
+
return '<div class="object-value"><span class="object-value-facts">' + facts.join("") + '</span>' + (!compact && notes.length ? '<small>' + esc(notes.join(" ")) + '</small>' : "") + '</div>';
|
|
3996
|
+
}).join("") + '</div>';
|
|
3997
|
+
}
|
|
3563
3998
|
function controlOperationTracking(control) {
|
|
3564
3999
|
const obligations = resourcesOfType("obligation")
|
|
3565
4000
|
.map(({ record }) => record)
|
|
@@ -3643,13 +4078,97 @@ function pluralize(noun, count) {
|
|
|
3643
4078
|
if (/[^aeiou]y$/i.test(noun)) return noun.slice(0, -1) + "ies";
|
|
3644
4079
|
return noun + "s";
|
|
3645
4080
|
}
|
|
3646
|
-
function renderNotFound(main) {
|
|
4081
|
+
function renderNotFound(main) {
|
|
4082
|
+
const route = parseRoute();
|
|
4083
|
+
const definition = route.type ? state.model.resources[route.type] : null;
|
|
4084
|
+
const recordMissing = route.name === "detail" && definition;
|
|
4085
|
+
const title = recordMissing ? definition.title + " Not Found" : "Page Not Found";
|
|
4086
|
+
const message = recordMissing ? "This record may have been renamed or deleted." : "This page does not exist.";
|
|
4087
|
+
const collectionLink = recordMissing
|
|
4088
|
+
? '<a class="button primary" href="#/resources/' + encodeURIComponent(route.type) + '">Back to ' + esc(titleCase(definition.pluralTitle)) + '</a>'
|
|
4089
|
+
: "";
|
|
4090
|
+
main.innerHTML = '<div class="page"><section class="panel not-found"><p class="kicker">Not found</p><h2>' + esc(title) + '</h2><p>' + esc(message) + '</p><div class="page-actions">' + collectionLink + '<a class="button ' + (recordMissing ? "" : "primary") + '" href="#/">Program overview</a></div></section></div>';
|
|
4091
|
+
}
|
|
3647
4092
|
function applyMutationState(result) {
|
|
3648
|
-
if (
|
|
3649
|
-
|
|
4093
|
+
if (result?.state) {
|
|
4094
|
+
state = result.state;
|
|
4095
|
+
} else if (result?.stateRefresh) {
|
|
4096
|
+
applyFastMutationPatch(result);
|
|
4097
|
+
scheduleMutationStateRefresh();
|
|
4098
|
+
} else {
|
|
4099
|
+
throw new Error("The save response did not include the current workspace state.");
|
|
4100
|
+
}
|
|
3650
4101
|
scheduleRepositorySyncPoll(result.synchronization);
|
|
3651
4102
|
}
|
|
3652
4103
|
|
|
4104
|
+
function applyFastMutationPatch(result) {
|
|
4105
|
+
if (result.operation === "collection-review" && result.assessment?.resourceType) {
|
|
4106
|
+
state.collectionReviews[result.assessment.resourceType] = result.assessment;
|
|
4107
|
+
}
|
|
4108
|
+
for (const record of result.changes?.update || []) {
|
|
4109
|
+
const entry = state.resources.find(({ record: current }) => current.id === record.id);
|
|
4110
|
+
if (entry) entry.record = record;
|
|
4111
|
+
}
|
|
4112
|
+
for (const record of result.changes?.create || []) {
|
|
4113
|
+
if (!state.resources.some(({ record: current }) => current.id === record.id)) {
|
|
4114
|
+
state.resources.push({ record, content: {}, history: [], detailsLoaded: false });
|
|
4115
|
+
}
|
|
4116
|
+
}
|
|
4117
|
+
if (result.synchronization) {
|
|
4118
|
+
state.repository = {
|
|
4119
|
+
...state.repository,
|
|
4120
|
+
status: result.synchronization.status,
|
|
4121
|
+
label: result.synchronization.status === "syncing" ? "Syncing" : state.repository.label,
|
|
4122
|
+
currentCommit: result.synchronization.commit || state.repository.currentCommit,
|
|
4123
|
+
writesAllowed: result.synchronization.status !== "syncing",
|
|
4124
|
+
backgroundSynchronization: result.synchronization.status === "syncing" ? result.synchronization : null
|
|
4125
|
+
};
|
|
4126
|
+
}
|
|
4127
|
+
state.readOnly = true;
|
|
4128
|
+
}
|
|
4129
|
+
|
|
4130
|
+
function scheduleMutationStateRefresh(delay = 0) {
|
|
4131
|
+
if (mutationStateRefreshTimer || mutationStateRefreshInFlight) return;
|
|
4132
|
+
mutationStateRefreshTimer = setTimeout(refreshMutationState, delay);
|
|
4133
|
+
}
|
|
4134
|
+
|
|
4135
|
+
async function refreshMutationState() {
|
|
4136
|
+
mutationStateRefreshTimer = null;
|
|
4137
|
+
if (mutationStateRefreshInFlight) return;
|
|
4138
|
+
mutationStateRefreshInFlight = true;
|
|
4139
|
+
let retry = false;
|
|
4140
|
+
try {
|
|
4141
|
+
const response = await fetch("/api/state");
|
|
4142
|
+
if (!response.ok) throw new Error(await responseMessage(response));
|
|
4143
|
+
state = await response.json();
|
|
4144
|
+
render();
|
|
4145
|
+
scheduleRepositorySyncPoll();
|
|
4146
|
+
} catch {
|
|
4147
|
+
retry = true;
|
|
4148
|
+
} finally {
|
|
4149
|
+
mutationStateRefreshInFlight = false;
|
|
4150
|
+
}
|
|
4151
|
+
if (retry) scheduleMutationStateRefresh(1_000);
|
|
4152
|
+
}
|
|
4153
|
+
|
|
4154
|
+
function prefetchRepositoryForReview(status) {
|
|
4155
|
+
if (state.repository?.mode !== "trunk" || state.repository?.developmentOverride) {
|
|
4156
|
+
return Promise.resolve(null);
|
|
4157
|
+
}
|
|
4158
|
+
status.textContent = "Checking repository…";
|
|
4159
|
+
return fetch("/api/git/prefetch", { method: "POST" })
|
|
4160
|
+
.then(async (response) => {
|
|
4161
|
+
if (!response.ok) throw new Error(await responseMessage(response));
|
|
4162
|
+
const result = await response.json();
|
|
4163
|
+
status.textContent = result.status === "checked" ? "Repository checked" : "Ready to save";
|
|
4164
|
+
return result;
|
|
4165
|
+
})
|
|
4166
|
+
.catch(() => {
|
|
4167
|
+
status.textContent = "Repository will be checked when you save";
|
|
4168
|
+
return null;
|
|
4169
|
+
});
|
|
4170
|
+
}
|
|
4171
|
+
|
|
3653
4172
|
function scheduleRepositorySyncPoll(synchronization = state.repository?.backgroundSynchronization) {
|
|
3654
4173
|
const syncing = synchronization?.status === "syncing"
|
|
3655
4174
|
|| state.repository?.status === "syncing";
|
|
@@ -3724,6 +4243,30 @@ function showError(message) {
|
|
|
3724
4243
|
dialog.querySelectorAll("button").forEach((button) => button.addEventListener("click", () => dialog.close()));
|
|
3725
4244
|
dialog.addEventListener("close", () => dialog.remove());
|
|
3726
4245
|
}
|
|
4246
|
+
|
|
4247
|
+
function confirmAction({ kicker, title, message, confirmLabel = "Confirm", danger = false }) {
|
|
4248
|
+
const dialog = document.createElement("dialog");
|
|
4249
|
+
dialog.className = "alert-dialog confirmation-dialog";
|
|
4250
|
+
dialog.setAttribute("aria-labelledby", "confirmation-dialog-title");
|
|
4251
|
+
dialog.setAttribute("aria-describedby", "confirmation-dialog-message");
|
|
4252
|
+
dialog.innerHTML = '<form><div class="dialog-head"><div><p class="kicker">' + esc(kicker) + '</p><h2 id="confirmation-dialog-title">' + esc(title) + '</h2></div><button type="button" class="icon-button" data-confirm-dismiss aria-label="Close">×</button></div><p id="confirmation-dialog-message">' + esc(message) + '</p><div class="dialog-actions"><button type="button" class="button" data-confirm-dismiss>Cancel</button><button type="submit" class="button ' + (danger ? "danger-action" : "primary") + '">' + esc(confirmLabel) + '</button></div></form>';
|
|
4253
|
+
document.body.append(dialog);
|
|
4254
|
+
return new Promise((resolve) => {
|
|
4255
|
+
let confirmed = false;
|
|
4256
|
+
dialog.querySelector("form").addEventListener("submit", (event) => {
|
|
4257
|
+
event.preventDefault();
|
|
4258
|
+
confirmed = true;
|
|
4259
|
+
dialog.close();
|
|
4260
|
+
});
|
|
4261
|
+
dialog.querySelectorAll("[data-confirm-dismiss]").forEach((button) => button.addEventListener("click", () => dialog.close()));
|
|
4262
|
+
dialog.addEventListener("close", () => {
|
|
4263
|
+
dialog.remove();
|
|
4264
|
+
resolve(confirmed);
|
|
4265
|
+
}, { once: true });
|
|
4266
|
+
dialog.showModal();
|
|
4267
|
+
});
|
|
4268
|
+
}
|
|
4269
|
+
|
|
3727
4270
|
async function responseMessage(response) {
|
|
3728
4271
|
const source = await response.text();
|
|
3729
4272
|
try { return JSON.parse(source).error || source; } catch { return source; }
|
|
@@ -3732,7 +4275,7 @@ async function localFetch(url, options) {
|
|
|
3732
4275
|
const method = String(options?.method || "GET").toUpperCase();
|
|
3733
4276
|
const synchronizing = state?.repository?.mode === "trunk"
|
|
3734
4277
|
&& ["POST", "PUT", "DELETE"].includes(method)
|
|
3735
|
-
&&
|
|
4278
|
+
&& !["/api/evidence-packet", "/api/git/prefetch"].includes(url);
|
|
3736
4279
|
const chip = synchronizing ? document.querySelector(".repo-chip") : null;
|
|
3737
4280
|
const previousChip = chip?.innerHTML;
|
|
3738
4281
|
let repositoryRefreshed = false;
|
|
@@ -3777,7 +4320,7 @@ html,body{height:100%;overflow:hidden}.shell{grid-template-columns:248px minmax(
|
|
|
3777
4320
|
.nav-stage>.nav-items>a.nav-direct{display:grid;grid-template-columns:minmax(0,1fr) var(--nav-control-width);gap:6px;align-items:center;width:100%;padding:6px 6px 6px 7px;font-size:13.2px}
|
|
3778
4321
|
.nav-heading-row{display:grid;grid-template-columns:minmax(0,1fr) 24px;gap:2px;align-items:stretch}.nav-heading-row>.nav-heading{display:grid;grid-template-columns:24px minmax(0,1fr);gap:6px;align-items:center;width:100%;padding:7px 6px;border-radius:7px;color:#d5d9ed;text-align:left;text-transform:none;letter-spacing:0;text-decoration:none}.nav-heading-row>.nav-heading:hover,.nav-heading-row>.nav-heading.current{background:rgba(255,255,255,.1);color:#fff}.nav-toggle{display:grid;place-items:center;width:24px;height:auto;min-height:100%;padding:0;border:0;border-radius:6px;background:none;color:#aeb6d8;cursor:pointer}.nav-toggle:hover{background:rgba(255,255,255,.1);color:#fff}.nav-chevron{display:block;width:12px;height:12px;place-self:center;fill:none;stroke:currentColor;stroke-width:1.5;stroke-linecap:round;stroke-linejoin:round;transition:transform .15s}.nav-subheading-row{display:grid;grid-template-columns:minmax(0,1fr) 22px;gap:2px;align-items:center;width:100%;padding:0;border:0;border-radius:6px;background:none;color:#919bc4;cursor:pointer}.nav-subheading-row:hover{background:rgba(255,255,255,.1);color:#fff}.nav-subheading-row>.nav-subheading{display:flex;align-items:center;min-width:0;padding:7px;color:inherit;text-align:left;text-transform:uppercase;letter-spacing:.09em;font-size:9.6px;font-weight:780}.nav-group.open>.nav-heading-row .nav-chevron,.nav-group.open>.nav-subheading-row .nav-chevron{transform:rotate(90deg)}
|
|
3779
4322
|
.stage-overview-hero{display:grid;grid-template-columns:minmax(0,1fr) 300px;gap:28px;align-items:center;padding:26px 28px;background:var(--panel);border:1px solid var(--line);border-radius:12px;box-shadow:var(--shadow)}.stage-overview-hero h2,.group-overview-head h2{font:500 37.2px Georgia,serif;margin:7px 0}.stage-overview-hero>div>p:not(.kicker),.group-overview-head>div>p:not(.kicker){max-width:760px;color:var(--muted);font-size:14.4px;line-height:1.55;margin:0}.stage-progress-card{padding:15px 17px;background:var(--paper);border:1px solid var(--line);border-radius:9px}.stage-progress-card>div:first-child{display:flex;align-items:center;justify-content:space-between;margin-bottom:11px}.stage-progress-card>div:first-child>strong{font:500 33.6px Georgia,serif}.stage-progress-card p{color:var(--muted);font-size:10.8px;line-height:1.45;margin:9px 0 0}.badge.neutral{background:#e5e8f2;color:#555e73}.stage-overview-layout{display:grid;grid-template-columns:320px minmax(0,1fr);gap:15px;margin-top:15px;align-items:start}.stage-plan ol,.group-plan ol{display:grid;gap:12px;padding-left:20px;margin:0}.stage-plan li,.group-plan li{padding-left:4px;color:var(--ink);font-size:13.2px;line-height:1.5}.stage-groups{min-width:0}.stage-groups>.section-head{margin:4px 0 13px}.stage-group-grid{display:grid;grid-template-columns:repeat(2,minmax(0,1fr));gap:10px}.stage-group-card{position:relative;display:block;min-height:138px;padding:18px 38px 17px 18px;background:var(--panel);border:1px solid var(--line);border-radius:10px;text-decoration:none;box-shadow:0 2px 8px rgba(21,40,33,.025)}.stage-group-card:hover{border-color:var(--accent-light)}.stage-group-card h3{font-size:15.6px;margin:0 0 7px}.stage-group-card p{color:var(--muted);font-size:12px;line-height:1.5;margin:0}.stage-group-card small{display:block;color:var(--accent);font-size:9.6px;font-weight:700;margin-top:12px}.stage-group-arrow{position:absolute;right:16px;top:16px;color:var(--accent);font-size:24px}.group-overview-head{display:flex;justify-content:space-between;align-items:end;gap:25px;margin-bottom:15px}.stage-status-link{display:grid;grid-template-columns:auto auto;align-items:center;gap:4px 12px;min-width:155px;padding:12px 14px;background:var(--panel);border:1px solid var(--line);border-radius:9px;text-decoration:none}.stage-status-link>strong{font:500 28.8px Georgia,serif;text-align:right}.stage-status-link>small{grid-column:1/-1;color:var(--muted);font-size:9.6px;text-align:right}.relationship-note{display:grid;grid-template-columns:250px minmax(0,1fr);gap:20px;align-items:center;margin-bottom:15px;padding:17px 20px;background:var(--accent-soft);border:1px solid #cbd3ff;border-radius:10px}.relationship-note h3{font-size:15.6px;margin:5px 0 0}.relationship-note>p{color:var(--muted);font-size:12px;line-height:1.55;margin:0}.relationship-note code{font-size:10.8px}.group-plan{margin-bottom:24px}.group-related-links{display:flex;align-items:center;gap:8px;margin-top:18px;padding-top:14px;border-top:1px solid var(--line)}.group-related-links>span{color:var(--muted);font-size:10.8px;margin-right:auto}.group-destinations>.section-head{margin-bottom:12px}.group-destination-grid{display:grid;grid-template-columns:repeat(2,minmax(0,1fr));gap:11px}.group-destination-card{display:grid;grid-template-columns:minmax(0,1fr) 100px;gap:16px;min-height:150px;padding:18px;background:var(--panel);border:1px solid var(--line);border-radius:10px;text-decoration:none;box-shadow:0 2px 8px rgba(21,40,33,.025)}.group-destination-card:hover{border-color:var(--accent-light)}.group-destination-card h3{font-size:15.6px;margin:5px 0 7px}.group-destination-card p:not(.kicker){color:var(--muted);font-size:10.8px;line-height:1.5;margin:0}.destination-rollup{align-self:center;text-align:right}.destination-rollup strong,.destination-rollup small{display:block}.destination-rollup strong{font:500 32.4px Georgia,serif}.destination-rollup small{color:var(--muted);font-size:9.6px;line-height:1.35;margin-top:3px}
|
|
3780
|
-
.collection-review-panel{margin:16px 0 22px}.collection-review-panel.required{border-color:#d8bd78}.collection-review-panel.current{border-color:#b9dac6}.collection-review-head{display:flex;align-items:flex-start;justify-content:space-between;gap:24px}.collection-review-head h3{margin:4px 0 6px}.collection-review-head p:not(.kicker){max-width:900px;margin:0;color:var(--muted);font-size:11px;line-height:1.5}.collection-review-panel details{margin-top:13px;padding:10px 12px;border:1px solid var(--line);border-radius:7px;background:var(--surface-soft)}.collection-review-panel summary{cursor:pointer;font-size:11px;font-weight:750}.collection-review-panel ul,.collection-review-checks ul{margin:9px 0 0;padding-left:20px;color:var(--muted);font-size:10.4px;line-height:1.55}.collection-review-foot{display:flex;align-items:center;justify-content:space-between;gap:20px;margin-top:13px}.collection-review-result{display:flex;align-items:baseline;gap:8px;margin:0}.collection-review-result strong{font-size:11px}.collection-review-result span{color:var(--muted);font-size:10.4px}.collection-review-checks{margin:13px 0;padding:11px 13px;border:1px solid var(--line);border-radius:7px;background:var(--surface-soft)}.collection-review-checks>strong{font-size:11px}.event-dialog-steps.collection-review-checks{margin:15px 0 0;padding:10px;border:0}.collection-review-dialog textarea{box-sizing:border-box;width:100%;resize:vertical}.resource-review-criteria{margin:16px 0 22px;padding:13px 15px;border:1px solid var(--line);border-radius:8px;background:var(--panel)}.resource-review-criteria>strong{font-size:11px}.resource-review-criteria ul{display:grid;grid-template-columns:repeat(2,minmax(0,1fr));gap:5px 28px;margin:9px 0 0;padding-left:20px;color:var(--muted);font-size:10.4px;line-height:1.5}.commit-dialog .resource-review-criteria{margin:12px 0;background:var(--surface-soft)}.record-workflow-action{display:grid;grid-template-columns:auto minmax(140px,1fr);gap:7px;align-items:start;min-width:240px;text-decoration:none}.record-workflow-action strong,.record-workflow-action small{display:block}.record-workflow-action strong{font-size:10.4px}.record-workflow-action small{margin-top:2px;color:var(--muted);font-size:9.2px;line-height:1.35}.record-workflow-clear{color:var(--muted);font-size:10px;white-space:nowrap}.workflow-guidance{margin:16px 0 22px}.workflow-guidance .panel-head{align-items:flex-start;margin-bottom:12px}.workflow-guidance .panel-head h3{margin:4px 0}.workflow-guidance .panel-head p:not(.kicker){margin:4px 0 0;color:var(--muted);font-size:11px}.workflow-findings{display:grid;grid-template-columns:repeat(2,minmax(0,1fr));gap:7px}.workflow-findings>a,.workflow-findings>div{display:grid;grid-template-columns:auto minmax(0,1fr);gap:9px;align-items:start;padding:10px 11px;border:1px solid var(--line);border-radius:7px;background:var(--surface-soft);text-decoration:none}.workflow-findings strong,.workflow-findings small{display:block}.workflow-findings strong{font-size:11px}.workflow-findings small{margin-top:3px;color:var(--muted);font-size:9.6px;line-height:1.4}.workflow-finding-status{min-width:62px;padding:3px 5px;border-radius:99px;background:var(--accent-soft);color:var(--accent);font-size:8.4px;font-weight:750;text-align:center;text-transform:uppercase;letter-spacing:.04em}.workflow-finding-status.overdue,.workflow-finding-status.blocked{background:#f5ded9;color:#8d352c}.workflow-finding-status.ready,.workflow-finding-status.due,.workflow-finding-status.open{background:#f7e9cf;color:#855717}.workflow-finding-status.complete{background:#ddefe5;color:#176143}.workflow-guidance-more{margin:10px 0 0;color:var(--muted);font-size:9.6px}.context-workflow{display:flex;align-items:center;justify-content:space-between;gap:24px;margin:-8px 0 18px;padding:17px 19px;background:var(--accent-soft);border:1px solid #cbd3ff;border-radius:10px}.context-workflow h3{font-size:16px;margin:4px 0 5px}.context-workflow p:not(.kicker){color:var(--muted);font-size:12px;line-height:1.5;margin:0;max-width:850px}.context-workflow .button{white-space:nowrap}.evidence-map{display:grid;gap:12px;margin-top:18px}.evidence-map-head{display:flex;align-items:center;justify-content:space-between;gap:24px;padding:20px 22px;background:var(--accent-soft);border:1px solid #cbd3ff;border-radius:11px}.evidence-map-head>div:first-child{max-width:760px}.evidence-map-head h2{font:500 24px Georgia,serif;margin:5px 0 7px}.evidence-map-head p:not(.kicker){color:var(--muted);font-size:12px;line-height:1.55;margin:0}.evidence-map-actions{display:flex;gap:8px}.evidence-map-card{padding:18px 20px;background:var(--panel);border:1px solid var(--line);border-radius:10px;box-shadow:0 2px 8px rgba(21,40,33,.025)}.evidence-map-card.complete{border-color:#b9dac6}.evidence-map-card-head{display:flex;align-items:start;justify-content:space-between;gap:20px}.evidence-map-card-head h3{font-size:16px;margin:7px 0 0}.evidence-map-card-head>small{color:var(--muted);font-size:10px;text-align:right}.evidence-map-card>p{color:var(--muted);font-size:12px;line-height:1.55;margin:12px 0}.evidence-map-expectation{display:grid;grid-template-columns:120px minmax(0,1fr);gap:12px;padding:8px 0;border-top:1px solid var(--line);font-size:11px;line-height:1.5}.evidence-map-expectation strong{color:var(--muted);font-size:9.6px;text-transform:uppercase;letter-spacing:.06em}.evidence-map-expectation code{background:var(--paper);border-radius:4px;padding:2px 5px}.evidence-map-links{display:grid;grid-template-columns:minmax(220px,1fr) minmax(280px,1.2fr);gap:20px;margin-top:13px;padding-top:13px;border-top:1px solid var(--line)}.evidence-map-links>div>small{display:block;color:var(--muted);font-size:9.6px;font-weight:700;text-transform:uppercase;letter-spacing:.06em;margin-bottom:7px}.evidence-map-references,.evidence-map-sources{display:flex;flex-wrap:wrap;gap:5px}.evidence-map-source{display:flex;align-items:center;gap:7px;padding:7px 9px;background:var(--paper);border:1px solid var(--line);border-radius:7px;font-size:11px;text-decoration:none}.evidence-map-source.complete{border-color:#b9dac6;background:#edf7f1}.evidence-map-source small{color:var(--muted);font-size:9px}.evidence-map-status{padding:9px 11px;background:var(--paper);border-radius:7px}.evidence-map-empty{padding:24px;background:var(--panel);border:1px solid var(--line);border-radius:10px}.evidence-map-empty h3{margin:6px 0}.evidence-map-empty p:not(.kicker){color:var(--muted);font-size:12px;margin:0 0 14px}.stage-pages{margin-top:24px}.stage-page-grid{display:grid;grid-template-columns:repeat(2,minmax(0,1fr));gap:10px}.stage-page-card{position:relative;display:flex;flex-direction:column;min-width:0;padding:18px;background:var(--panel);border:1px solid var(--line);border-radius:10px;box-shadow:0 2px 8px rgba(21,40,33,.025);transition:border-color .15s,box-shadow .15s}.stage-page-card:hover{border-color:var(--accent-light);box-shadow:0 5px 16px rgba(21,40,33,.07)}.stage-page-card.complete{border-color:#b9dac6}.stage-page-card-link{position:absolute;inset:0;z-index:1;border-radius:10px}.stage-page-card-link:focus-visible{outline:2px solid var(--focus);outline-offset:2px}.stage-page-card-head{display:flex;align-items:center;justify-content:space-between;gap:18px}.stage-page-card-head h3{font-size:15.6px;line-height:1.35;margin:4px 0 0}.stage-page-card-head>div>small{display:block;color:var(--accent);font-size:9.6px;font-weight:700}.stage-page-card>p{color:var(--muted);font-size:12px;line-height:1.5;margin:13px 0 0}.stage-page-tasks{position:relative;z-index:2;display:grid;gap:6px;margin-top:13px}.stage-page-tasks>a{display:grid;grid-template-columns:auto minmax(0,1fr);gap:8px;align-items:start;padding:9px;border:1px solid var(--line);border-radius:7px;background:var(--surface-soft);text-decoration:none}.stage-page-tasks>a:hover{border-color:var(--accent-light)}.stage-page-tasks strong,.stage-page-tasks small{display:block}.stage-page-tasks strong{font-size:10.8px}.stage-page-tasks small{margin-top:2px;color:var(--muted);font-size:9.4px;line-height:1.35}.stage-page-tasks-more{color:var(--muted);font-size:9.4px}.stage-page-rollup{display:flex;flex:0 0 104px;flex-direction:column;justify-content:center;text-align:right}.stage-page-rollup strong,.stage-page-rollup small{display:block}.stage-page-rollup strong{font:500 24px Georgia,serif}.stage-page-rollup small{color:var(--muted);font-size:9.6px;line-height:1.35;margin-top:2px}.stage-page-card-foot{display:flex;align-items:center;justify-content:space-between;gap:10px;margin-top:auto;padding-top:15px}.stage-page-completion-state{color:var(--muted);font-size:10.8px}.stage-page-completion-state.complete{color:#176143;font-weight:700}.stage-page-open{color:var(--accent);font-size:12px;font-weight:700}.work-queue-section{margin-top:28px}.work-queue-section>.section-head{align-items:end}
|
|
4323
|
+
.collection-review-panel{margin:16px 0 22px}.collection-review-panel.required{border-color:#d8bd78}.collection-review-panel.current{border-color:#b9dac6}.collection-review-head{display:flex;align-items:flex-start;justify-content:space-between;gap:24px}.collection-review-head h3{margin:4px 0 6px}.collection-review-head p:not(.kicker){max-width:900px;margin:0;color:var(--muted);font-size:11px;line-height:1.5}.collection-review-panel details{margin-top:13px;padding:10px 12px;border:1px solid var(--line);border-radius:7px;background:var(--surface-soft)}.collection-review-panel summary{cursor:pointer;font-size:11px;font-weight:750}.collection-review-panel ul,.collection-review-checks ul{margin:9px 0 0;padding-left:20px;color:var(--muted);font-size:10.4px;line-height:1.55}.collection-review-foot{display:flex;align-items:center;justify-content:space-between;gap:20px;margin-top:13px}.collection-review-result{display:flex;align-items:baseline;gap:8px;margin:0}.collection-review-result strong{font-size:11px}.collection-review-result span{color:var(--muted);font-size:10.4px}.collection-review-checks{margin:13px 0;padding:11px 13px;border:1px solid var(--line);border-radius:7px;background:var(--surface-soft)}.collection-review-checks>strong{font-size:11px}.event-dialog-steps.collection-review-checks{margin:15px 0 0;padding:10px;border:0}.collection-review-dialog textarea{box-sizing:border-box;width:100%;resize:vertical}.review-save-status{margin-right:auto;color:var(--muted);font-size:11px}.resource-review-criteria{margin:16px 0 22px;padding:13px 15px;border:1px solid var(--line);border-radius:8px;background:var(--panel)}.resource-review-criteria>strong{font-size:11px}.resource-review-criteria ul{display:grid;grid-template-columns:repeat(2,minmax(0,1fr));gap:5px 28px;margin:9px 0 0;padding-left:20px;color:var(--muted);font-size:10.4px;line-height:1.5}.commit-dialog .resource-review-criteria{margin:12px 0;background:var(--surface-soft)}.record-workflow-action{display:grid;grid-template-columns:auto minmax(140px,1fr);gap:7px;align-items:start;min-width:240px;text-decoration:none}.record-workflow-action strong,.record-workflow-action small{display:block}.record-workflow-action strong{font-size:10.4px}.record-workflow-action small{margin-top:2px;color:var(--muted);font-size:9.2px;line-height:1.35}.record-workflow-clear{color:var(--muted);font-size:10px;white-space:nowrap}.workflow-guidance{margin:16px 0 22px}.workflow-guidance .panel-head{align-items:flex-start;margin-bottom:12px}.workflow-guidance .panel-head h3{margin:4px 0}.workflow-guidance .panel-head p:not(.kicker){margin:4px 0 0;color:var(--muted);font-size:11px}.workflow-findings{display:grid;grid-template-columns:repeat(2,minmax(0,1fr));gap:7px}.workflow-findings>a,.workflow-findings>div{display:grid;grid-template-columns:auto minmax(0,1fr);gap:9px;align-items:start;padding:10px 11px;border:1px solid var(--line);border-radius:7px;background:var(--surface-soft);text-decoration:none}.workflow-findings strong,.workflow-findings small{display:block}.workflow-findings strong{font-size:11px}.workflow-findings small{margin-top:3px;color:var(--muted);font-size:9.6px;line-height:1.4}.workflow-finding-status{min-width:62px;padding:3px 5px;border-radius:99px;background:var(--accent-soft);color:var(--accent);font-size:8.4px;font-weight:750;text-align:center;text-transform:uppercase;letter-spacing:.04em}.workflow-finding-status.overdue,.workflow-finding-status.blocked{background:#f5ded9;color:#8d352c}.workflow-finding-status.ready,.workflow-finding-status.due,.workflow-finding-status.open{background:#f7e9cf;color:#855717}.workflow-finding-status.complete{background:#ddefe5;color:#176143}.workflow-guidance-more{margin:10px 0 0;color:var(--muted);font-size:9.6px}.context-workflow{display:flex;align-items:center;justify-content:space-between;gap:24px;margin:-8px 0 18px;padding:17px 19px;background:var(--accent-soft);border:1px solid #cbd3ff;border-radius:10px}.context-workflow h3{font-size:16px;margin:4px 0 5px}.context-workflow p:not(.kicker){color:var(--muted);font-size:12px;line-height:1.5;margin:0;max-width:850px}.context-workflow .button{white-space:nowrap}.evidence-map{display:grid;gap:12px;margin-top:18px}.evidence-map-head{display:flex;align-items:center;justify-content:space-between;gap:24px;padding:20px 22px;background:var(--accent-soft);border:1px solid #cbd3ff;border-radius:11px}.evidence-map-head>div:first-child{max-width:760px}.evidence-map-head h2{font:500 24px Georgia,serif;margin:5px 0 7px}.evidence-map-head p:not(.kicker){color:var(--muted);font-size:12px;line-height:1.55;margin:0}.evidence-map-actions{display:flex;gap:8px}.evidence-map-card{padding:18px 20px;background:var(--panel);border:1px solid var(--line);border-radius:10px;box-shadow:0 2px 8px rgba(21,40,33,.025)}.evidence-map-card.complete{border-color:#b9dac6}.evidence-map-card-head{display:flex;align-items:start;justify-content:space-between;gap:20px}.evidence-map-card-head h3{font-size:16px;margin:7px 0 0}.evidence-map-card-head>small{color:var(--muted);font-size:10px;text-align:right}.evidence-map-card>p{color:var(--muted);font-size:12px;line-height:1.55;margin:12px 0}.evidence-map-expectation{display:grid;grid-template-columns:120px minmax(0,1fr);gap:12px;padding:8px 0;border-top:1px solid var(--line);font-size:11px;line-height:1.5}.evidence-map-expectation strong{color:var(--muted);font-size:9.6px;text-transform:uppercase;letter-spacing:.06em}.evidence-map-expectation code{background:var(--paper);border-radius:4px;padding:2px 5px}.evidence-map-links{display:grid;grid-template-columns:minmax(220px,1fr) minmax(280px,1.2fr);gap:20px;margin-top:13px;padding-top:13px;border-top:1px solid var(--line)}.evidence-map-links>div>small{display:block;color:var(--muted);font-size:9.6px;font-weight:700;text-transform:uppercase;letter-spacing:.06em;margin-bottom:7px}.evidence-map-references,.evidence-map-sources{display:flex;flex-wrap:wrap;gap:5px}.evidence-map-source{display:flex;align-items:center;gap:7px;padding:7px 9px;background:var(--paper);border:1px solid var(--line);border-radius:7px;font-size:11px;text-decoration:none}.evidence-map-source.complete{border-color:#b9dac6;background:#edf7f1}.evidence-map-source small{color:var(--muted);font-size:9px}.evidence-map-status{padding:9px 11px;background:var(--paper);border-radius:7px}.evidence-map-empty{padding:24px;background:var(--panel);border:1px solid var(--line);border-radius:10px}.evidence-map-empty h3{margin:6px 0}.evidence-map-empty p:not(.kicker){color:var(--muted);font-size:12px;margin:0 0 14px}.stage-pages{margin-top:24px}.stage-page-grid{display:grid;grid-template-columns:repeat(2,minmax(0,1fr));gap:10px}.stage-page-card{position:relative;display:flex;flex-direction:column;min-width:0;padding:18px;background:var(--panel);border:1px solid var(--line);border-radius:10px;box-shadow:0 2px 8px rgba(21,40,33,.025);transition:border-color .15s,box-shadow .15s}.stage-page-card:hover{border-color:var(--accent-light);box-shadow:0 5px 16px rgba(21,40,33,.07)}.stage-page-card.complete{border-color:#b9dac6}.stage-page-card-link{position:absolute;inset:0;z-index:1;border-radius:10px}.stage-page-card-link:focus-visible{outline:2px solid var(--focus);outline-offset:2px}.stage-page-card-head{display:flex;align-items:center;justify-content:space-between;gap:18px}.stage-page-card-head h3{font-size:15.6px;line-height:1.35;margin:4px 0 0}.stage-page-card-head>div>small{display:block;color:var(--accent);font-size:9.6px;font-weight:700}.stage-page-card>p{color:var(--muted);font-size:12px;line-height:1.5;margin:13px 0 0}.stage-page-tasks{position:relative;z-index:2;display:grid;gap:6px;margin-top:13px}.stage-page-tasks>a{display:grid;grid-template-columns:auto minmax(0,1fr);gap:8px;align-items:start;padding:9px;border:1px solid var(--line);border-radius:7px;background:var(--surface-soft);text-decoration:none}.stage-page-tasks>a:hover{border-color:var(--accent-light)}.stage-page-tasks strong,.stage-page-tasks small{display:block}.stage-page-tasks strong{font-size:10.8px}.stage-page-tasks small{margin-top:2px;color:var(--muted);font-size:9.4px;line-height:1.35}.stage-page-tasks-more{color:var(--muted);font-size:9.4px}.stage-page-rollup{display:flex;flex:0 0 104px;flex-direction:column;justify-content:center;text-align:right}.stage-page-rollup strong,.stage-page-rollup small{display:block}.stage-page-rollup strong{font:500 24px Georgia,serif}.stage-page-rollup small{color:var(--muted);font-size:9.6px;line-height:1.35;margin-top:2px}.stage-page-card-foot{display:flex;align-items:center;justify-content:space-between;gap:10px;margin-top:auto;padding-top:15px}.stage-page-completion-state{color:var(--muted);font-size:10.8px}.stage-page-completion-state.complete{color:#176143;font-weight:700}.stage-page-open{color:var(--accent);font-size:12px;font-weight:700}.work-queue-section{margin-top:28px}.work-queue-section>.section-head{align-items:end}
|
|
3781
4324
|
.workflow-findings>a:hover{border-color:var(--accent-light);box-shadow:0 3px 9px rgba(21,40,33,.05)}.workflow-findings>a:focus-visible{outline:2px solid var(--focus);outline-offset:2px}.stage-page-card-head{align-items:flex-start}.stage-page-completion-state{flex:0 0 auto;max-width:150px;padding:5px 8px;border-radius:99px;background:#f7e9cf;color:#855717;font-size:9.6px;font-weight:750;line-height:1.25;text-align:right}.stage-page-completion-state.complete{background:#ddefe5;color:#176143}.obligation-card.workflow-target{outline:2px solid var(--focus);outline-offset:3px}.obligation-card-foot{flex-wrap:wrap}.obligation-card-foot .obligation-links{flex:1 1 120px}
|
|
3782
4325
|
.evidence-attachments .panel-head{align-items:flex-start}.evidence-attachments .panel-head h3{margin:0}.evidence-attachments .panel-head p{margin:4px 0 0;color:var(--muted);font-size:10px}.evidence-attachments ul{list-style:none;margin:0;padding:0}.evidence-attachments li{display:flex;align-items:center;justify-content:space-between;gap:10px;padding:9px 0;border-top:1px solid var(--line)}.evidence-attachments li:first-child{border-top:0}.evidence-attachments strong,.evidence-attachments small{display:block}.evidence-attachments strong{font-size:11px}.evidence-attachments small{margin-top:2px;color:var(--muted);font-size:9px;overflow-wrap:anywhere}.evidence-attachments .attachment-empty{display:block;color:var(--muted);font-size:10px;line-height:1.45}.danger-text{color:var(--red)}
|
|
3783
4326
|
.button{text-decoration:none}
|
|
@@ -3787,7 +4330,7 @@ html,body{height:100%;overflow:hidden}.shell{grid-template-columns:248px minmax(
|
|
|
3787
4330
|
.overview-grid>.panel{padding:15px}.overview-grid .panel-head{margin-bottom:10px}.overview-grid .audit-progress{gap:7px 20px}.overview-grid .progress-number strong{font-size:31.2px}.overview-grid .audit-engagement{padding:8px 11px}
|
|
3788
4331
|
.nav-close,.nav-scrim{display:none}.pagination{display:flex;align-items:center;justify-content:center;gap:12px;margin-top:14px}.pagination[hidden]{display:none}.page-status{color:var(--muted);font-size:12px;min-width:150px;text-align:center}.button:disabled{cursor:not-allowed;opacity:.45}.search-pagination{padding-top:2px}
|
|
3789
4332
|
.list-tools{flex-wrap:wrap}.list-tools label{min-width:220px}.list-header-tools{flex:1;justify-content:flex-end;margin:0 0 0 28px}.list-header-tools label{flex:1 1 220px;max-width:360px}.list-header-tools select{max-width:190px}.list-header-tools .button{white-space:nowrap}
|
|
3790
|
-
.setup-banner{margin:14px 0;background:#eef1ff;border:1px solid #ccd4ff;border-radius:11px;padding:19px 22px;display:grid;grid-template-columns:1fr
|
|
4333
|
+
.setup-banner{margin:14px 0;background:#eef1ff;border:1px solid #ccd4ff;border-radius:11px;padding:19px 22px;display:grid;grid-template-columns:1fr auto;gap:25px;align-items:center}.setup-banner h3{margin:5px 0 6px;font-size:18px}.setup-banner p:not(.kicker){margin:0;color:var(--muted);font-size:13.2px;line-height:1.5}.setup-banner>.button{justify-self:end}.setup-draft-state{display:flex;align-items:center;justify-content:flex-end;gap:22px}.setup-draft-state span{min-width:100px}.setup-draft-state small,.setup-draft-state strong{display:block}.setup-draft-state small{color:var(--muted);font-size:9.6px;text-transform:uppercase;letter-spacing:.06em}.setup-draft-state strong{margin-top:3px;font-size:12px}.due-list time.overdue{color:var(--red)}.content-label{display:flex;align-items:center;justify-content:space-between;gap:12px}.text-button{border:0;background:none;color:var(--accent);font-size:10.8px;text-transform:uppercase;letter-spacing:.06em;font-weight:750;cursor:pointer;white-space:nowrap}.tag{white-space:normal;overflow-wrap:anywhere;max-width:100%}.editor{max-height:calc(100vh - 30px);overflow:auto}.form-grid{display:grid;grid-template-columns:repeat(2,minmax(0,1fr));gap:14px;margin:20px 0}.form-field>.field-label,.content-editor-field>span{display:flex;align-items:center;justify-content:space-between;gap:8px;color:#3e4557;font-size:12px;font-weight:700;margin-bottom:6px}.required-mark{font-size:9.6px;color:var(--accent);text-transform:uppercase;letter-spacing:.06em}.form-field input,.form-field select,.editor .form-field textarea{width:100%;height:auto;min-height:40px;border:1px solid var(--line);border-radius:7px;background:#fff;color:var(--ink);padding:9px 10px;font:14.4px/1.4 inherit}.editor .form-field textarea{height:82px}.form-field input[readonly]{background:#eef0f6;color:#5d6475}.form-field>small{display:block;color:#6a7181;font-size:10.8px;margin-top:5px}.checkbox-list{display:grid;gap:5px;max-height:145px;overflow:auto;border:1px solid var(--line);border-radius:7px;padding:7px}.checkbox-list label{display:flex;align-items:center;gap:8px;padding:5px;border-radius:5px}.checkbox-list input{width:16px;min-height:16px;padding:0;flex:0 0 auto}.checkbox-list label:hover{background:#f2f4fa}.checkbox-list span,.checkbox-list small{display:block;font-size:12px}.checkbox-list small{color:var(--muted);margin-top:2px}.choice-picker{border:1px solid var(--line);border-radius:7px;background:var(--field)}.choice-picker summary{min-height:40px;padding:10px;cursor:pointer;font-size:13.2px}.choice-picker .checkbox-list{max-height:220px;border:0;border-top:1px solid var(--line);border-radius:0}.structured-object-fields{display:grid;grid-template-columns:repeat(2,minmax(0,1fr));gap:9px;padding:10px;border:1px solid var(--line);border-radius:8px;background:var(--surface-soft)}.structured-object-fields.nested{grid-column:1/-1;background:var(--panel)}.object-property{min-width:0}.object-property-label{display:flex;align-items:center;justify-content:space-between;gap:6px;margin-bottom:4px;font-size:11px;font-weight:700}.string-map-editor,.string-map-items{display:grid;gap:7px}.string-map-row{display:grid;grid-template-columns:minmax(0,1fr) minmax(0,1fr) auto;gap:6px;align-items:center}.object-array-items{display:grid;gap:10px;margin-bottom:8px}.object-array-item{display:grid;gap:9px;border:1px solid var(--line);border-radius:8px;padding:10px}.object-array-item legend{padding:0 5px;color:var(--muted);font-size:11px;font-weight:750}.object-array-item>.structured-object-fields{padding:0;border:0;background:none}.missing-options{padding:11px;border:1px dashed #d7c8a9;background:#fbf5e9;color:#795b23;border-radius:7px;font-size:12px}.content-editor-field{display:block;margin:17px 0}.editor .content-editor-field textarea,.editor .markdown-source{height:260px;background:#10162b;color:#e8ebff;font:13.2px/1.5 ui-monospace,SFMono-Regular,Menlo,monospace}.advanced-editor{border-top:1px solid var(--line);margin-top:18px;padding-top:13px}.advanced-editor summary{cursor:pointer;color:var(--accent);font-size:13.2px;font-weight:750}.advanced-editor p{font-size:12px;color:var(--muted)}.editor .advanced-editor>textarea{height:320px}.alert-dialog{width:min(520px,calc(100vw - 30px));border:0;border-radius:12px;padding:23px;box-shadow:0 25px 80px rgba(0,0,24,.28)}.alert-dialog>p{font-size:14.4px;line-height:1.55;color:var(--muted)}.metadata dd{overflow-wrap:anywhere}
|
|
3791
4334
|
.record-content-action{display:flex;justify-content:flex-start;margin-top:20px}.record-content-details{border-top:1px solid var(--line);margin-top:18px;padding-top:13px}.record-content-details summary{cursor:pointer;color:var(--accent);font-size:13.2px;font-weight:750}.record-content-details>p{color:var(--muted);font-size:12px}.record-content-editor>span small{color:var(--muted);font-size:10.8px;font-weight:500}
|
|
3792
4335
|
.program-setup{grid-template-columns:minmax(250px,.75fr) minmax(440px,1.4fr)}.setup-steps{display:grid;grid-template-columns:repeat(2,minmax(0,1fr));gap:7px}.setup-steps a{display:grid;grid-template-columns:24px minmax(0,1fr);gap:9px;align-items:start;padding:10px;border:1px solid var(--line);border-radius:8px;background:var(--panel);color:var(--ink);text-decoration:none}.setup-steps a:hover{border-color:var(--accent-light)}.setup-steps a>span:first-child{display:grid;place-items:center;width:24px;height:24px;border-radius:50%;background:var(--accent-soft);color:var(--accent);font-size:12px}.setup-steps a.done>span:first-child{background:#dcefe4;color:#125733}.setup-steps strong,.setup-steps small{display:block}.setup-steps strong{font-size:12px}.setup-steps small{margin-top:3px;color:var(--muted);font-size:9.6px;line-height:1.4;font-weight:500}
|
|
3793
4336
|
.readiness-map{margin:14px 0;background:var(--panel);border:1px solid var(--line);border-radius:11px;padding:20px 22px;box-shadow:0 2px 8px rgba(21,40,33,.025)}.readiness-map-head{display:grid;grid-template-columns:minmax(220px,1fr) minmax(320px,420px);gap:28px;align-items:center;margin-bottom:17px}.readiness-map-head h3{font-size:18px;margin:5px 0 0}.readiness-progress-summary{display:grid;grid-template-columns:minmax(0,1fr) auto;gap:8px 14px;align-items:center}.readiness-progress-summary>div{display:grid;grid-template-columns:minmax(0,1fr) auto auto;gap:3px 8px;align-items:baseline}.readiness-progress-summary>div>span{color:var(--muted);font-size:9.6px;font-weight:700;text-transform:uppercase;letter-spacing:.08em}.readiness-progress-summary>div>strong{font-size:9.6px;font-weight:700;line-height:1.2}.readiness-progress-summary>div>.badge{font-size:8.4px}.readiness-progress-summary .progress,.readiness-progress-summary small{grid-column:1/-1}.readiness-progress-summary small{color:var(--muted);font-size:9.6px}.readiness-progress-summary>.button{white-space:nowrap}.readiness-flow{display:grid;grid-template-columns:repeat(3,minmax(0,1fr));gap:7px}.readiness-flow a{display:grid;grid-template-columns:23px minmax(0,1fr);column-gap:8px;align-content:start;min-width:0;padding:11px;border:1px solid var(--line);border-radius:8px;background:var(--surface-soft);text-decoration:none}.readiness-flow a:hover{border-color:var(--accent-light);background:var(--accent-soft)}.readiness-flow a>span{grid-row:1/4;display:grid;place-items:center;width:23px;height:23px;border-radius:50%;background:var(--primary-gradient);color:#fff;font-size:9.6px;font-weight:800}.readiness-flow strong{font-size:12px;line-height:1.25}.readiness-flow small{grid-column:2;color:var(--muted);font-size:9.6px;line-height:1.4;margin-top:3px}.readiness-state{grid-column:2;justify-self:start;margin-top:8px;padding:3px 6px;border-radius:99px;background:var(--surface-muted);color:var(--muted);font-size:8.4px;line-height:1.2}.readiness-state.good{background:#dcefe4;color:#125733}.readiness-state.warn{background:#f6e8c9;color:#79500f}.readiness-state.bad{background:#f7dfdc;color:#873027}.audit-engagement{display:grid;grid-template-columns:minmax(210px,1fr) minmax(260px,1.25fr) auto;gap:20px;align-items:center;padding:14px 15px;border-radius:8px;background:var(--surface-soft)}.audit-engagement strong{font-size:13.2px}.audit-engagement p,.audit-engagement li{color:var(--muted);font-size:10.8px;line-height:1.5}.audit-engagement p{margin:5px 0 0}.audit-engagement ul{margin:0;padding-left:18px}.audit-engagement .button{white-space:nowrap;text-decoration:none}.resource-directory{display:grid;grid-template-columns:repeat(3,minmax(0,1fr));gap:10px}.resource-directory>section{min-width:0;padding:12px;border-radius:8px;background:var(--surface-soft)}.resource-directory h4{margin:0 0 7px;color:var(--muted);font-size:10.8px;text-transform:uppercase;letter-spacing:.08em}.resource-directory a{display:flex;justify-content:space-between;gap:10px;padding:5px 0;border-top:1px solid var(--line);font-size:10.8px;text-decoration:none}.resource-directory a:first-of-type{border-top:0}.resource-directory a:hover span{color:var(--accent)}.resource-directory a strong{color:var(--muted);font-size:9.6px}.record-prose{max-width:790px}.record-prose section{padding:0 0 20px}.record-prose section+section{padding-top:20px;border-top:1px solid var(--line)}.record-prose h3{margin:0 0 7px;color:var(--muted);font-size:10.8px;text-transform:uppercase;letter-spacing:.08em}.record-prose p{margin:0;font-size:16.8px;line-height:1.65;white-space:pre-wrap}.connections-panel .panel-head>span{display:grid;place-items:center;min-width:22px;height:22px;border-radius:99px;background:var(--surface-muted);color:var(--muted);font-size:9.6px}.connections{display:grid}.connections a{display:block;padding:9px 0;border-top:1px solid var(--line);text-decoration:none}.connections a:first-child{padding-top:0;border-top:0}.connections strong,.connections small{display:block}.connections strong{font-size:12px}.connections small{margin-top:3px;color:var(--muted);font-size:9.6px;line-height:1.4}.connections a:hover strong{color:var(--accent)}.connections-more{margin:9px 0 0;color:var(--muted);font-size:9.6px;line-height:1.4}.external-source{display:flex;align-items:flex-start;justify-content:space-between;gap:8px;color:var(--accent);text-decoration:none}.external-source span,.external-source strong,.external-source small{display:block}.external-source strong{font-size:12px;line-height:1.35}.external-source small{margin-top:3px;color:var(--muted);font-size:9.6px;line-height:1.35;overflow-wrap:anywhere}.external-source b{font-size:13.2px}.external-source:hover strong{text-decoration:underline}
|
|
@@ -3797,16 +4340,18 @@ html,body{height:100%;overflow:hidden}.shell{grid-template-columns:248px minmax(
|
|
|
3797
4340
|
.onboarding-save-status{color:var(--muted);font-size:10.8px;line-height:1.35}.onboarding-save-status:empty{display:none}.onboarding-save-status:not(:empty){order:-1;flex-basis:100%;margin-bottom:4px}.onboarding-retry-sync{margin-top:9px}.detail-loading{color:var(--muted)}
|
|
3798
4341
|
.save-status{min-height:16px;color:var(--muted);font-size:10.8px;line-height:1.35}
|
|
3799
4342
|
.page-intro,.detail-head{align-items:center;margin-bottom:12px}.actions{align-items:center}.detail-head>div:first-child{min-width:0}.detail-head h2{margin:7px 0}.detail-head .header-breadcrumbs{margin:0;font-size:10.8px;line-height:normal;min-height:11px;align-items:center}.header-breadcrumbs span:last-child{overflow:hidden;text-overflow:ellipsis;white-space:nowrap;max-width:60ch}
|
|
4343
|
+
.not-found{max-width:620px;padding:28px}.not-found h2{font-family:Georgia,serif;font-size:37.2px;font-weight:500;margin:7px 0}.not-found>p:not(.kicker){margin:0;color:var(--muted);font-size:15.6px}.not-found .page-actions{justify-content:flex-start;margin-top:22px}
|
|
4344
|
+
.button.danger-action{background:var(--red);border-color:var(--red);color:#fff}
|
|
3800
4345
|
@media(max-width:1200px){.readiness-flow{grid-template-columns:repeat(3,minmax(0,1fr))}.audit-engagement{grid-template-columns:1fr 1fr}.audit-engagement .button{grid-column:1/-1;justify-self:start}}
|
|
3801
4346
|
@media(max-width:1100px){.search{display:none}.topbar-status{margin-left:auto}.metrics{grid-template-columns:repeat(2,1fr)}.dashboard-grid,.organization-grid{grid-template-columns:repeat(2,1fr)}.catalog{grid-template-columns:repeat(3,1fr)}.span-2{grid-column:span 2}.resource-directory{grid-template-columns:repeat(2,minmax(0,1fr))}}
|
|
3802
|
-
@media(max-width:760px){.shell{display:block}.sidebar{transform:translateX(-100%);transition:.2s;box-shadow:8px 0 30px rgba(0,0,0,.2)}.sidebar.shown{transform:translateX(0)}.workspace{min-width:0}.mobile-nav{display:block;border:0;background:none;font-size:24px}.topbar{height:72px;padding:0 16px}.topbar>div:first-of-type{min-width:0}.topbar-status{display:none}.search{display:flex;max-width:none}.search kbd,.topbar .eyebrow{display:none}.page{padding:20px 15px 60px}.hero{display:block;padding:23px}.hero-meta{margin-top:22px;flex-wrap:wrap}.metrics,.dashboard-grid,.organization-grid{grid-template-columns:1fr}.span-2{grid-column:auto}.catalog{grid-template-columns:repeat(2,1fr)}.detail-grid{grid-template-columns:1fr}.page-intro,.detail-head{display:block}.page-intro>.button,.actions{margin-top:15px}.page-intro>.list-header-tools{justify-content:flex-start;margin:15px 0 0}.list-header-tools label{max-width:none}.record-table{min-width:720px}.readiness-map{padding:17px}.readiness-map-head{grid-template-columns:1fr;gap:8px}.readiness-flow{grid-template-columns:repeat(2,minmax(0,1fr))}.audit-engagement{grid-template-columns:1fr}.audit-engagement .button{grid-column:auto}.resource-directory{grid-template-columns:1fr}}
|
|
4347
|
+
@media(max-width:760px){.shell{display:block}.sidebar{transform:translateX(-100%);transition:.2s;box-shadow:8px 0 30px rgba(0,0,0,.2)}.sidebar.shown{transform:translateX(0)}.workspace{min-width:0}.mobile-nav{display:block;border:0;background:none;font-size:24px}.topbar{height:72px;padding:0 16px}.topbar>div:first-of-type{flex:1;min-width:0}.topbar h1{overflow:hidden;text-overflow:ellipsis;white-space:nowrap}.topbar-status{display:none}.search{display:flex;flex:0 1 180px;min-width:110px;max-width:none}.search kbd,.topbar .eyebrow{display:none}.page{padding:20px 15px 60px}.hero{display:block;padding:23px}.hero-meta{margin-top:22px;flex-wrap:wrap}.metrics,.dashboard-grid,.organization-grid{grid-template-columns:1fr}.span-2{grid-column:auto}.catalog{grid-template-columns:repeat(2,1fr)}.detail-grid{grid-template-columns:1fr}.page-intro,.detail-head{display:block}.page-intro>.button,.actions{margin-top:15px}.page-intro>.list-header-tools{justify-content:flex-start;margin:15px 0 0}.list-header-tools label{max-width:none}.record-table{min-width:720px}.readiness-map{padding:17px}.readiness-map-head{grid-template-columns:1fr;gap:8px}.readiness-flow{grid-template-columns:repeat(2,minmax(0,1fr))}.audit-engagement{grid-template-columns:1fr}.audit-engagement .button{grid-column:auto}.resource-directory{grid-template-columns:1fr}}
|
|
3803
4348
|
@media(max-width:760px){.setup-banner,.page-guide,.stage-overview-hero,.relationship-note,.group-destination-card,.stage-page-grid,.evidence-map-expectation,.evidence-map-links,.external-evidence-list{grid-template-columns:1fr}.context-workflow,.evidence-map-head{align-items:stretch;flex-direction:column}.evidence-map-actions{flex-wrap:wrap}.page-guide>div{border-left:0;border-top:1px solid var(--line)}.page-guide>div:first-child{border-top:0}.group-overview-head{display:block}.stage-progress-card,.stage-status-link{margin-top:15px}.destination-rollup{text-align:left}.form-grid{grid-template-columns:1fr}.record-table{min-width:0}.record-table thead{display:none}.record-table,.record-table tbody,.record-table tr{display:block}.record-table tr{padding:8px 12px;border-bottom:1px solid var(--line)}.record-table tr:last-child{border-bottom:0}.record-table td:not([data-label]){display:block}.record-table td[data-label]{display:grid;grid-template-columns:105px minmax(0,1fr);gap:10px;border:0;padding:7px 0;align-items:start}.record-table td[data-label]::before{content:attr(data-label);color:#75817b;text-transform:uppercase;letter-spacing:.07em;font-size:9.6px;font-weight:700}.record-table td[data-primary-field]{display:block;padding:8px 0 10px}.record-table td[data-primary-field]::before{display:none}.content-label{align-items:flex-start}.editor form{padding:18px}.diagnostics>div{grid-template-columns:58px minmax(0,1fr)}.diagnostics p{grid-column:1/-1}.changes code{overflow-wrap:anywhere}.onboarding-dialog{max-height:56vh}}
|
|
3804
4349
|
@media(max-width:760px){.guide-review ul,.resource-review-criteria ul{grid-template-columns:1fr}.collection-review-head,.collection-review-foot{align-items:stretch;flex-direction:column}.record-workflow-action{min-width:0}}
|
|
3805
4350
|
@media(max-width:520px){.onboarding-form,.onboarding-sections,.setup-steps{grid-template-columns:1fr}.onboarding-form label.wide{grid-column:auto}.onboarding-actions{flex-wrap:wrap}.onboarding-skip{width:100%;order:3;margin:3px 0 0}.readiness-flow{grid-template-columns:1fr}.obligation-card-foot{align-items:flex-start;flex-direction:column}.obligation-action{align-self:flex-start}}
|
|
3806
4351
|
@media(min-width:761px){.detail-grid{grid-template-columns:minmax(270px,1fr) minmax(0,2fr)}.detail-grid aside{grid-column:1;grid-row:1}.detail-main{grid-column:2;grid-row:1}}
|
|
3807
4352
|
@media(min-width:761px){.detail-grid.detail-grid-structured{grid-template-columns:1fr}.detail-grid-structured aside{grid-column:1;grid-row:1;grid-template-columns:repeat(auto-fit,minmax(320px,1fr))}.detail-grid-structured aside>.panel{align-self:start}}
|
|
3808
4353
|
@media(max-width:760px){.sidebar{visibility:hidden;transition:transform .2s,visibility 0s .2s}.sidebar.shown{visibility:visible;transition-delay:0s}.nav-close{display:grid;place-items:center;position:absolute;top:25px;right:18px;width:34px;height:34px;border:1px solid #5966a4;border-radius:50%;background:#11174a;color:#eef1ff;font-size:24px;cursor:pointer}.nav-scrim{display:block;position:fixed;inset:0;border:0;background:rgba(0,0,24,.38);opacity:0;pointer-events:none;transition:opacity .2s;z-index:15}.sidebar.shown+.nav-scrim{opacity:1;pointer-events:auto}.pagination{justify-content:space-between;gap:8px}.page-status{min-width:0}}
|
|
3809
|
-
@media(max-width:760px){.topbar{height:56px}.nav-close{font-size:0}.nav-close:before,.nav-close:after{content:"";position:absolute;width:13px;height:2px;border-radius:2px;background:currentColor;transform:rotate(45deg)}.nav-close:after{transform:rotate(-45deg)}}
|
|
4354
|
+
@media(max-width:760px){.topbar{height:56px}.topbar>div:first-of-type{display:none}.search{flex:1;min-width:0}.nav-close{font-size:0}.nav-close:before,.nav-close:after{content:"";position:absolute;width:13px;height:2px;border-radius:2px;background:currentColor;transform:rotate(45deg)}.nav-close:after{transform:rotate(-45deg)}}
|
|
3810
4355
|
|
|
3811
4356
|
.connection-group+.connection-group{margin-top:15px;padding-top:13px;border-top:1px solid var(--line)}.connection-group h4{margin:0 0 8px;color:var(--muted);font-size:9.6px;text-transform:uppercase;letter-spacing:.08em}
|
|
3812
4357
|
body,button,input,select,textarea,dialog{color:var(--ink)}
|
|
@@ -3881,6 +4426,8 @@ dialog::backdrop{background:rgba(0,0,24,.62)}
|
|
|
3881
4426
|
.obligation-card-foot{display:flex;align-items:flex-end;justify-content:space-between;gap:9px;margin-top:10px}.obligation-card-foot .obligation-links{margin-top:0;min-width:0}.obligation-action{flex:0 0 auto;border:0;border-radius:6px;background:var(--accent-soft);color:var(--accent);padding:7px 9px;font-family:inherit;font-size:10.8px;font-weight:700;line-height:1;text-decoration:none;cursor:pointer}.obligation-action:hover{filter:brightness(1.08)}.obligation-action.blocked{background:var(--surface-muted);color:var(--muted)}.obligation-more{width:100%;margin-top:9px}.workflow-section{scroll-margin-top:92px}
|
|
3882
4427
|
.event-dialog label{display:block;margin-top:13px}.event-dialog label[hidden]{display:none}.event-dialog label>span{display:block;font-size:12px;font-weight:720;margin-bottom:6px}.event-dialog input,.event-dialog select,.event-dialog textarea{width:100%;min-height:40px;border:1px solid var(--line);border-radius:7px;background:var(--field);color:var(--ink);padding:9px 10px;font-size:14.4px}.event-dialog textarea{resize:vertical}.commit-dialog .form-grid label.full{grid-column:1/-1}.workflow-preview{margin-top:14px;padding:0 12px;border-radius:7px;background:var(--surface-soft)}.workflow-preview:not(:empty){padding-top:10px;padding-bottom:10px}.workflow-preview strong,.workflow-preview p{display:block;margin:0}.workflow-preview p{margin-top:5px;color:var(--muted);font-size:12px;line-height:1.5}.event-dialog-steps{display:grid;gap:6px;margin-top:15px;padding:10px;background:var(--surface-soft);border-radius:7px}.event-dialog-steps strong,.event-dialog-steps small{display:block}.event-dialog-steps strong{font-size:12px}.event-dialog-steps small{font-size:9.6px;color:var(--muted);margin-top:2px}
|
|
3883
4428
|
.applicability-dialog{width:min(980px,calc(100vw - 30px));max-height:calc(100vh - 32px);border:0;border-radius:12px;padding:0;background:var(--panel);color:var(--ink);box-shadow:0 25px 80px rgba(0,0,24,.28)}.applicability-dialog form{padding:23px}.applicability-dialog form>p{color:var(--muted);font-size:13.2px}.review-context label>span{display:block;font-size:12px;font-weight:720;margin-bottom:6px}.review-context input,.review-context select,.applicability-row input,.applicability-row select{width:100%;min-height:38px;border:1px solid var(--line);border-radius:7px;background:var(--field);color:var(--ink);padding:8px 9px;font-size:13.2px}.review-context label.full{grid-column:1/-1}.applicability-rows{display:grid;gap:7px;max-height:46vh;overflow:auto;margin-top:16px;padding-right:4px}.applicability-row{display:grid;grid-template-columns:minmax(210px,1fr) 180px minmax(240px,1.3fr);gap:9px;align-items:center;padding:9px;border:1px solid var(--line);border-radius:8px}.applicability-row strong,.applicability-row small{display:block}.applicability-row small{margin-top:3px;color:var(--muted);font-size:10.8px}
|
|
4429
|
+
.object-value-list{display:grid;gap:7px}.object-value{min-width:0;padding:7px 8px;border:1px solid var(--line);border-radius:7px;background:var(--surface-soft)}.object-value-facts{display:flex;flex-wrap:wrap;gap:5px 10px}.object-value-facts>span{display:flex;align-items:baseline;gap:5px;min-width:0;font-size:11px}.object-value-facts b{color:var(--muted);font-size:8.8px;text-transform:uppercase;letter-spacing:.05em}.object-value>small{display:block;margin-top:6px;color:var(--muted);font-size:10px;line-height:1.45}.object-value-list.compact .object-value{padding:0;border:0;background:none}.object-value-list.compact .object-value-facts{display:grid;gap:5px}.object-value-list.compact .object-value-facts>span{display:grid;gap:2px;font-size:10px}
|
|
4430
|
+
.resource-review-criteria.compact{margin:12px 0;padding:10px 12px;background:var(--surface-soft)}.resource-review-criteria.compact summary{cursor:pointer;font-size:11px;font-weight:750}.resource-review-criteria.compact ul{margin-bottom:0}.evidence-map-more{justify-self:center;margin:2px 0 8px}
|
|
3884
4431
|
.packet-builder form{display:grid;grid-template-columns:repeat(3,minmax(0,1fr)) auto;gap:12px;align-items:end}.packet-builder label>span{display:block;font-size:10.8px;font-weight:720;margin-bottom:6px}.packet-builder input,.packet-builder select{width:100%;min-height:40px;border:1px solid var(--line);border-radius:7px;background:var(--field);color:var(--ink);padding:9px 10px;font-size:13.2px}.packet-note,.packet-output>p{font-size:12px;color:var(--muted);margin:12px 0 0}.packet-output{margin:14px 0}.packet-output h3{overflow-wrap:anywhere}.packet-gaps{display:grid}.packet-gaps>div{display:grid;grid-template-columns:58px 1fr;gap:10px;border-top:1px solid var(--line);padding:10px 0}.packet-gaps>div:first-child{border-top:0}.packet-gaps p{font-size:12px;margin:0}.packet-list{display:grid}.packet-list a{display:block;text-decoration:none;border-top:1px solid var(--line);padding:9px 0}.packet-list a:first-child{border-top:0}.packet-list strong,.packet-list small{display:block}.packet-list strong{font-size:12px}.packet-list small{font-size:9.6px;color:var(--muted);margin-top:2px}
|
|
3885
4432
|
.packet-preflight{display:grid;grid-template-columns:repeat(5,minmax(0,1fr));gap:8px;margin-bottom:12px}.packet-preflight a{display:flex;align-items:flex-start;gap:9px;padding:10px 12px;border:1px solid var(--line);border-radius:8px;background:var(--panel);text-decoration:none}.packet-preflight .status-dot{margin-top:4px}.packet-preflight small,.packet-preflight strong{display:block}.packet-preflight small{color:var(--muted);font-size:9.6px;text-transform:uppercase;letter-spacing:.07em}.packet-preflight strong{margin-top:3px;font-size:12px}.audit-evidence-paths{margin-bottom:12px}.audit-evidence-paths .panel-head p:not(.kicker){margin:4px 0 0;color:var(--muted);font-size:10.8px}.audit-evidence-path-grid{display:grid;grid-template-columns:repeat(2,minmax(0,1fr));gap:8px;margin-top:12px}.audit-evidence-path-grid>a{display:block;padding:14px;border:1px solid var(--line);border-radius:8px;background:var(--surface-soft);text-decoration:none}.audit-evidence-path-grid h4{margin:7px 0 5px;font-size:13.2px}.audit-evidence-path-grid p{margin:0;color:var(--muted);font-size:10.8px;line-height:1.55}
|
|
3886
4433
|
.audit-preparation{margin-bottom:12px}.audit-preparation .panel-head{align-items:flex-start}.audit-preparation .panel-head h3{margin:3px 0}.audit-preparation .panel-head p:not(.kicker){margin:4px 0 0;color:var(--muted);font-size:10.8px}.preparation-progress{height:5px;margin:12px 0 0;border-radius:99px;background:var(--surface-muted);overflow:hidden}.preparation-progress span{display:block;height:100%;border-radius:inherit;background:var(--primary-gradient)}.audit-preparation-note{margin:9px 0 0;color:var(--muted);font-size:10.8px;line-height:1.5}.preparation-stages{display:grid;grid-template-columns:repeat(2,minmax(0,1fr));gap:8px;margin-top:12px}.preparation-stage{border:1px solid var(--line);border-radius:8px;background:var(--surface-soft);overflow:hidden}.preparation-stage summary{display:flex;justify-content:space-between;gap:12px;align-items:center;padding:11px 12px;cursor:pointer;list-style:none}.preparation-stage summary::-webkit-details-marker{display:none}.preparation-stage summary span,.preparation-stage summary strong,.preparation-stage summary small{display:block}.preparation-stage summary strong{font-size:12px}.preparation-stage summary small{margin-top:3px;color:var(--muted);font-size:9.6px;line-height:1.4}.preparation-stage summary b{flex:none;color:var(--muted);font-size:9.6px;font-weight:650}.preparation-items{border-top:1px solid var(--line);background:var(--panel)}.preparation-items>a,.preparation-items>div{display:grid;grid-template-columns:22px minmax(0,1fr);gap:9px;padding:10px 12px;border-top:1px solid var(--line);text-decoration:none}.preparation-items>:first-child{border-top:0}.preparation-items strong,.preparation-items small{display:block}.preparation-items strong{font-size:10.8px}.preparation-items small{margin-top:3px;color:var(--muted);font-size:9.6px;line-height:1.45}.preparation-status{display:grid;place-items:center;width:20px;height:20px;border-radius:50%;background:var(--surface-muted);color:var(--muted);font-size:10.8px;font-weight:800}.preparation-status.complete{background:#dcefe4;color:#125733}.preparation-status.action{background:#f7dfdc;color:#873027}.preparation-status.later{background:#f6e8c9;color:#79500f}.preparation-status.external,.preparation-status.info{background:var(--accent-soft);color:var(--accent)}.audit-preparation-error:empty{display:none}
|
|
@@ -3890,7 +4437,11 @@ dialog::backdrop{background:rgba(0,0,24,.62)}
|
|
|
3890
4437
|
@media(max-width:900px){.overview-grid{grid-template-columns:1fr}.overview-grid>.audit-panel{grid-column:auto}}
|
|
3891
4438
|
@media(max-width:520px){.event-reminder-preview,.policy-event-list,.packet-builder form,.packet-preflight,.audit-evidence-path-grid{grid-template-columns:1fr}.policy-event-row:nth-child(n) .policy-event-tooltip{right:auto;left:0}.packet-metrics{grid-template-columns:1fr}.obligation-card-head{display:block}.obligation-card-head strong{display:block;text-align:left;margin-top:3px}}
|
|
3892
4439
|
@media(max-width:520px){.policy-event-feedback{grid-template-columns:auto minmax(0,1fr) auto}.policy-event-feedback>.button{grid-column:2}.policy-event-feedback>.icon-button{grid-column:3;grid-row:1}}
|
|
4440
|
+
@media(max-width:520px){.applicability-dialog form{padding:18px}.applicability-row{grid-template-columns:1fr}.applicability-rows{max-height:42vh}}
|
|
3893
4441
|
@media(max-width:760px){.workflow-findings{grid-template-columns:1fr}}
|
|
4442
|
+
.choice-tag{overflow-wrap:normal;font-size:10px}.form-field[data-kind="structured-object"],.string-map-property{grid-column:1/-1}.string-map-row .text-button{text-transform:none;letter-spacing:0;color:var(--muted);font-size:11px}
|
|
4443
|
+
@media(max-width:760px){.setup-banner>.button{justify-self:start}.setup-draft-state{justify-content:flex-start;flex-wrap:wrap;gap:12px 22px}.setup-draft-state .button{width:100%}}
|
|
4444
|
+
@media(max-width:760px){.string-map-row{grid-template-columns:1fr 1fr}.string-map-row .text-button{grid-column:1/-1;justify-self:start}}
|
|
3894
4445
|
|
|
3895
4446
|
@media(prefers-color-scheme:dark){
|
|
3896
4447
|
:root{--ink:#f4f5ff;--muted:#b8bfd3;--line:#343d5c;--paper:#000;--panel:#141a2e;--accent:#aab7ff;--accent-soft:#252e52;--accent-light:#9aabff;--focus:#bdc7ff;--amber:#ffd08a;--red:#ffaaa0;--surface-soft:#1b2238;--surface-muted:#252d48;--field:#11172a;--field-readonly:#1c2338;--shadow:0 12px 34px rgba(0,0,0,.3)}
|