From c5036e7bf000946d5fdfa6a082804e5484dd09d1 Mon Sep 17 00:00:00 2001 From: JB Date: Sat, 25 Jul 2026 21:25:15 -0400 Subject: [PATCH] Improve oversight change previews --- docs/DESIGN_SYSTEM.md | 11 +++ src/app.css | 2 +- .../components/ProposalChangePreview.svelte | 99 +++++++++++++++++++ .../isac/data-moderation/+page.server.ts | 8 +- src/routes/isac/data-moderation/+page.svelte | 8 +- 5 files changed, 124 insertions(+), 4 deletions(-) create mode 100644 src/lib/components/ProposalChangePreview.svelte diff --git a/docs/DESIGN_SYSTEM.md b/docs/DESIGN_SYSTEM.md index 7e183bb..f0522c3 100644 --- a/docs/DESIGN_SYSTEM.md +++ b/docs/DESIGN_SYSTEM.md @@ -717,6 +717,17 @@ must be separately saved and audit logged before approval, and material changes should be rejected for contributor revision. Collapse the decision controls and correction fields to one column on narrow screens. +Proposal evidence is reviewer-facing product data, not JSON-first diagnostics. +Open review in a wide, scroll-contained confirmation surface and present live +and proposed values side by side using the visual structure and terminology of +the source module's editor. Label the two states explicitly as `CURRENT LIVE / +BEFORE` and `PROPOSED / AFTER`; for additions, replace the empty live panel with +an explicit `NEW RECORD` state. Stack comparisons on narrow screens. Raw stored +payloads remain available in a collapsed technical-evidence disclosure for +troubleshooting and audit support, but must not be the primary review surface. +Each module should add a purpose-built preview as its editor matures; until then, +use compact labeled field cards rather than an exposed JSON document. + Data-navigation links with pending proposals use a dedicated four-column layout: module code, flexible label, square orange count cell, then navigation arrow. Never insert a count into the ordinary three-column link without changing its diff --git a/src/app.css b/src/app.css index 1d0d732..ee6c598 100644 --- a/src/app.css +++ b/src/app.css @@ -78,7 +78,7 @@ .avatar-picker{display:grid;grid-template-columns:repeat(4,1fr);gap:6px;margin-bottom:10px}.avatar-preset-form fieldset{height:100%}.avatar-preset{display:grid;gap:5px;width:100%;height:100%;padding:4px;background:#0b0e0f;border:1px solid #303536;color:#747c7e}.avatar-preset:hover,.avatar-preset:focus-visible{border-color:#8a672b;color:#f6a821}.avatar-preset.selected{border-color:#f6a821;color:#f6a821;box-shadow:inset 0 -2px #f6a821}.avatar-preset img{display:block;width:100%;aspect-ratio:1;object-fit:cover}.avatar-preset span{font-size:6px;line-height:1.25;text-transform:uppercase;letter-spacing:.4px}.avatar-upload{margin-top:8px} .image-drop-input{display:grid;grid-template-columns:64px 1fr auto;align-items:center;gap:10px;min-height:82px;padding:8px;border:1px dashed #4b5152;background:#0b0e0f;cursor:pointer;transition:border-color .15s,background .15s,transform .1s}.image-drop-input:hover,.image-drop-input:focus-visible,.image-drop-input.dragging{border-color:var(--shd-orange);background:#15130e;outline:none}.image-drop-input.dragging{transform:translateY(1px);box-shadow:inset 0 0 0 1px #f6a82155}.image-file-input{position:absolute!important;width:1px!important;height:1px!important;overflow:hidden!important;clip:rect(0 0 0 0)!important;white-space:nowrap!important}.image-upload-preview{display:grid;place-items:center;width:64px;height:64px;border:1px solid #343a3b;background:#070909;color:#62696b;font-size:6px;text-align:center}.image-upload-preview img{display:block;width:100%;height:100%;object-fit:contain}.image-drop-copy{display:grid;gap:4px;min-width:0}.image-drop-copy b{color:#c6cbcc;font-size:8px;letter-spacing:1px}.image-drop-copy span{overflow:hidden;color:#747c7e;font-size:7px;text-overflow:ellipsis;white-space:nowrap}.image-upload-actions{display:flex;gap:5px}.image-upload-actions button{min-width:92px;padding:8px}.image-upload-error{display:block;margin-top:5px;color:var(--shd-danger)}@media(max-width:700px){.image-drop-input{grid-template-columns:56px 1fr}.image-upload-preview{width:56px;height:56px}.image-upload-actions{grid-column:1/-1}.image-upload-actions button{flex:1}} .icon-upload-field .image-drop-input{grid-template-columns:1fr;gap:6px;text-align:center}.icon-upload-field .image-upload-preview{width:100%;height:72px}.icon-upload-field .image-upload-preview img{width:64px;height:64px}.icon-upload-field .image-upload-actions{display:grid;grid-template-columns:1fr 1fr;width:100%}.icon-upload-field .image-upload-actions button{min-width:0;padding:7px 3px;font-size:6px} -.proposal-submitted,.data-access-banner{display:flex;align-items:center;gap:12px;margin-bottom:9px;padding:9px 11px;border-left:3px solid var(--shd-success);background:#101713;color:#a6cbb0}.proposal-submitted b,.data-access-banner b{font-size:8px;letter-spacing:1px}.proposal-submitted span,.data-access-banner span{font-size:7px;color:#7e9d86}.data-access-banner{border-left-color:#657174;background:#111617;color:#aeb7b9}.data-access-banner.contributor{border-left-color:var(--shd-orange);background:#19150e;color:#d8bc87}.data-access-error{margin:-3px 0 9px}.data-readonly form[method=post] button[type=submit],.data-readonly form[method=post] button:not([type]){filter:grayscale(1);opacity:.45}.admin-sidebar nav a.has-pending{grid-template-columns:33px minmax(0,1fr) 18px 18px}.admin-sidebar nav a.has-pending em{grid-column:3;display:grid;place-items:center;min-width:18px;height:18px;padding:0 5px;border:1px solid #a26c19;background:#211a0e;color:#ffc45d;font-size:6px;font-style:normal;font-weight:900;text-align:center;transform:translateX(8px)}.admin-sidebar nav a.has-pending b{grid-column:4;justify-self:end}.admin-sidebar nav a.has-pending:hover em,.admin-sidebar nav a.has-pending.active em{border-color:var(--shd-orange);background:#2b2111;color:#fff1d0}.moderation-row{grid-template-columns:minmax(240px,1.5fr) minmax(140px,.8fr) 150px 80px 75px}.moderation-review{width:min(760px,100%)}.moderation-metadata{display:grid;grid-template-columns:repeat(3,1fr);gap:6px;margin:10px 0}.moderation-metadata span{display:grid;gap:4px;padding:8px;border:1px solid var(--shd-border);color:var(--shd-text-muted);font-size:6px}.moderation-metadata b{color:var(--shd-text);font-size:8px}.moderation-review details{margin:10px 0;border:1px solid var(--shd-border);background:#0b0e0f}.moderation-review summary{padding:9px;color:var(--shd-orange);font-size:7px;cursor:pointer}.moderation-review pre{max-height:260px;overflow:auto;margin:0;padding:10px;border-top:1px solid var(--shd-border);color:#9ca6a8;font-size:7px;white-space:pre-wrap}.decision-tabs{display:grid;grid-template-columns:1fr 1fr;gap:5px;margin:10px 0}.decision-tabs button{padding:9px;border:1px solid var(--shd-border);background:#111415;color:var(--shd-text-muted)}.decision-tabs button.active{border-color:var(--shd-orange);color:var(--shd-orange)}@media(max-width:800px){.moderation-row{grid-template-columns:1fr}.moderation-metadata{grid-template-columns:1fr}} +.proposal-submitted,.data-access-banner{display:flex;align-items:center;gap:12px;margin-bottom:9px;padding:9px 11px;border-left:3px solid var(--shd-success);background:#101713;color:#a6cbb0}.proposal-submitted b,.data-access-banner b{font-size:8px;letter-spacing:1px}.proposal-submitted span,.data-access-banner span{font-size:7px;color:#7e9d86}.data-access-banner{border-left-color:#657174;background:#111617;color:#aeb7b9}.data-access-banner.contributor{border-left-color:var(--shd-orange);background:#19150e;color:#d8bc87}.data-access-error{margin:-3px 0 9px}.data-readonly form[method=post] button[type=submit],.data-readonly form[method=post] button:not([type]){filter:grayscale(1);opacity:.45}.admin-sidebar nav a.has-pending{grid-template-columns:33px minmax(0,1fr) 18px 18px}.admin-sidebar nav a.has-pending em{grid-column:3;display:grid;place-items:center;min-width:18px;height:18px;padding:0 5px;border:1px solid #a26c19;background:#211a0e;color:#ffc45d;font-size:6px;font-style:normal;font-weight:900;text-align:center;transform:translateX(8px)}.admin-sidebar nav a.has-pending b{grid-column:4;justify-self:end}.admin-sidebar nav a.has-pending:hover em,.admin-sidebar nav a.has-pending.active em{border-color:var(--shd-orange);background:#2b2111;color:#fff1d0}.moderation-row{grid-template-columns:minmax(240px,1.5fr) minmax(140px,.8fr) 150px 80px 75px}.moderation-review{width:min(1120px,100%);max-height:calc(100vh - 32px);overflow:auto}.moderation-metadata{display:grid;grid-template-columns:repeat(4,1fr);gap:6px;margin:10px 0}.moderation-metadata span{display:grid;gap:4px;padding:8px;border:1px solid var(--shd-border);color:var(--shd-text-muted);font-size:6px}.moderation-metadata b{color:var(--shd-text);font-size:8px}.moderation-review details{margin:10px 0;border:1px solid var(--shd-border);background:#0b0e0f}.moderation-review summary{padding:9px;color:var(--shd-orange);font-size:7px;cursor:pointer}.moderation-review pre{max-height:260px;overflow:auto;margin:0;padding:10px;border-top:1px solid var(--shd-border);color:#9ca6a8;font-size:7px;white-space:pre-wrap}.proposal-change-preview{margin-top:10px}.proposal-preview-head{display:flex;align-items:center;justify-content:space-between;padding:8px 10px;border:1px solid var(--shd-border);background:#0d1011}.proposal-preview-head>div{display:grid;gap:3px}.proposal-preview-head span{font-size:7px;font-weight:900;letter-spacing:1px}.proposal-preview-head small{color:var(--shd-text-muted);font-size:6px}.proposal-preview-head>b{color:var(--shd-orange);font-size:7px}.proposal-compare-grid{display:grid;grid-template-columns:1fr 1fr;gap:7px;margin-top:7px}.proposal-editor-preview{min-width:0;border:1px solid var(--shd-border);background:#111415}.proposal-editor-preview.before{border-top:2px solid #596164}.proposal-editor-preview.after{border-top:2px solid var(--shd-orange)}.proposal-editor-preview>header{display:flex;align-items:center;justify-content:space-between;min-height:32px;padding:6px 9px;border-bottom:1px solid var(--shd-border);background:#0d1011}.proposal-editor-preview>header span,.proposal-editor-preview>header b{font-size:7px;letter-spacing:.9px}.proposal-editor-preview>header b{color:var(--shd-text-muted)}.proposal-editor-preview.after>header b{color:var(--shd-orange)}.proposal-attribute-grid{display:grid;grid-template-columns:repeat(4,minmax(0,1fr));gap:6px;padding:8px}.preview-field{min-width:0;display:grid;align-content:start;gap:5px}.preview-field.identity{grid-column:span 4}.preview-field>span{color:var(--shd-text-muted);font-size:6px;letter-spacing:.8px}.preview-field>strong,.preview-field.range>div{min-height:34px;display:flex;align-items:center;padding:7px 8px;border:1px solid #363b3c;background:#0d0f10;color:var(--shd-text);font-size:9px;overflow-wrap:anywhere}.preview-field.identity>strong{min-height:43px;font-size:13px}.preview-field.range{grid-column:span 2}.preview-field.range>div{display:grid;grid-template-columns:1fr auto 1fr;gap:6px}.preview-field.range strong:last-child{text-align:right}.preview-field.range i{color:#555d5f;font-size:5px;font-style:normal}.proposal-empty-state{min-height:180px;display:grid;place-content:center;gap:6px;text-align:center}.proposal-empty-state b{color:var(--shd-orange);font-size:9px;letter-spacing:1px}.proposal-empty-state span{color:var(--shd-text-muted);font-size:7px}.proposal-editor-preview.generic{margin-top:7px}.proposal-field-summary{display:grid;grid-template-columns:repeat(3,minmax(0,1fr));gap:6px;padding:8px}.proposal-field-summary>div{min-width:0;display:grid;align-content:start;gap:5px;padding:8px;border:1px solid #303536;background:#0d0f10}.proposal-field-summary span{color:var(--shd-text-muted);font-size:6px;letter-spacing:.8px}.proposal-field-summary strong{color:var(--shd-text);font-size:9px;overflow-wrap:anywhere}.proposal-field-summary small{max-height:42px;overflow:hidden;color:#687174;font-size:6px;overflow-wrap:anywhere}.decision-tabs{display:grid;grid-template-columns:1fr 1fr;gap:5px;margin:10px 0}.decision-tabs button{padding:9px;border:1px solid var(--shd-border);background:#111415;color:var(--shd-text-muted)}.decision-tabs button.active{border-color:var(--shd-orange);color:var(--shd-orange)}@media(max-width:800px){.moderation-row{grid-template-columns:1fr}.moderation-metadata,.proposal-compare-grid{grid-template-columns:1fr}.proposal-field-summary{grid-template-columns:1fr 1fr}}@media(max-width:520px){.proposal-attribute-grid,.proposal-field-summary{grid-template-columns:1fr}.preview-field.identity,.preview-field.range{grid-column:auto}} .account-tabs{width:100%;height:auto;margin:0 0 16px;display:grid;grid-template-columns:repeat(3,1fr);border:1px solid var(--shd-border)}.account-tabs button,.account-subtabs button{min-width:0;height:34px;padding:0 5px;border:0;border-left:1px solid var(--shd-border);border-bottom:2px solid transparent;background:#0d1011;color:#6f7779;font-size:7px;font-weight:900;letter-spacing:.8px;cursor:pointer;transition:var(--shd-transition)}.account-tabs button:first-child,.account-subtabs button:first-child{border-left:0}.account-tabs button:hover,.account-tabs button:focus-visible,.account-subtabs button:hover,.account-subtabs button:focus-visible{color:var(--shd-text);background:#171b1c}.account-tabs button.active,.account-subtabs button.active{border-bottom-color:var(--shd-orange);background:#211a0e;color:var(--shd-orange)}.account-tabs button:active,.account-subtabs button:active{transform:translateY(1px);filter:brightness(.85)}.account-tabs button:focus-visible,.account-subtabs button:focus-visible{position:relative;z-index:1;outline:2px solid var(--shd-orange);outline-offset:2px}.account-panel{min-height:290px}.account-subtabs{display:grid;grid-template-columns:1fr 1fr;margin:0 0 10px;border:1px solid var(--shd-border)}.account-subtabs button{height:31px}.access-subtabs{display:grid;grid-template-columns:repeat(4,1fr);margin:-2px 0 17px;border:1px solid var(--shd-border);background:#0b0e0f}.access-subtabs button{min-width:0;height:42px;display:grid;place-content:center;gap:3px;padding:4px;border:0;border-left:1px solid var(--shd-border);border-bottom:2px solid transparent;background:transparent;color:var(--shd-text-dim);cursor:pointer;transition:var(--shd-transition)}.access-subtabs button:first-child{border-left:0}.access-subtabs button span{font-size:6px;font-weight:900;letter-spacing:.7px}.access-subtabs button b{font-size:9px}.access-subtabs button:hover,.access-subtabs button:focus-visible{background:#171b1c;color:var(--shd-text)}.access-subtabs button.active{border-bottom-color:var(--shd-orange);background:#211a0e;color:var(--shd-orange)}.access-subtabs button:active{transform:translateY(1px);filter:brightness(.85)}.access-subtabs button:focus-visible{position:relative;z-index:1;outline:2px solid var(--shd-orange);outline-offset:2px}.account-retention{color:var(--shd-text-muted);font:12px/1.55 Georgia,serif}.danger-eyebrow{color:var(--shd-danger)} .account-alert.error-alert{background:#261513;border-color:#75443b;border-left-color:#df8069}.account-alert.error-alert b{color:#df8069}.account-alert.error-alert span{color:#d2aaa1} .danger-zone{margin-top:12px;border:1px solid #563734;background:#181112}.danger-zone summary{padding:10px;color:#df8069;cursor:pointer;font-size:8px;font-weight:bold;letter-spacing:1px}.danger-zone>p,.danger-zone>form{margin:0;padding:0 10px 10px}.danger-zone>p{color:#9f8580;font-size:8px;line-height:1.5}.danger-zone form{padding-bottom:10px} diff --git a/src/lib/components/ProposalChangePreview.svelte b/src/lib/components/ProposalChangePreview.svelte new file mode 100644 index 0000000..f3fbe9c --- /dev/null +++ b/src/lib/components/ProposalChangePreview.svelte @@ -0,0 +1,99 @@ + + +
+
+
CHANGE PREVIEWLIVE REGISTRY COMPARED WITH CONTRIBUTOR SUBMISSION
+ {currentSnapshot ? 'BEFORE / AFTER' : 'NEW RECORD'} +
+ + {#if module === 'attributes' && action === 'save'} +
+
+
CURRENT LIVE{currentSnapshot ? 'BEFORE' : 'NO LIVE RECORD'}
+ {#if currentSnapshot} +
+
NAME{shown(attributeValue('current', 'name'))}
+ {#each [['type','TYPE'],['category','CATEGORY'],['calculation_type','CALCULATION'],['unit','UNIT']] as field} +
{field[1]}{shown(attributeValue('current', field[0]))}
+ {/each} + {#each [['gear','GEAR'],['weapon','WEAPON'],['skill_chip','SKILL CHIP']] as source} +
{source[1]} RANGE · OPTIONAL
{shown(attributeValue('current', `${source[0]}_min`))}TO{shown(attributeValue('current', `${source[0]}_max`))}
+ {/each} +
OPTIONAL CAP{shown(attributeValue('current', 'cap_value'))}
+
+ {:else} +
NEW ATTRIBUTE DEFINITIONNo live registry record will be replaced.
+ {/if} +
+
+
PROPOSED DEFINITIONAFTER
+
+
NAME{shown(attributeValue('proposed', 'name'))}
+ {#each [['type','TYPE'],['category','CATEGORY'],['calculation_type','CALCULATION'],['unit','UNIT']] as field} +
{field[1]}{shown(attributeValue('proposed', field[0]))}
+ {/each} + {#each [['gear','GEAR'],['weapon','WEAPON'],['skill_chip','SKILL CHIP']] as source} +
{source[1]} RANGE · OPTIONAL
{shown(attributeValue('proposed', `${source[0]}_min`))}TO{shown(attributeValue('proposed', `${source[0]}_max`))}
+ {/each} +
OPTIONAL CAP{shown(attributeValue('proposed', 'cap_value'))}
+
+
+
+ {:else} +
+
PROPOSED {label(module)} RECORDAFTER
+
+ {#each entries as entry} +
+ {label(entry.name)} + {#if entry.kind === 'file'} + {entry.fileName || 'UPLOADED FILE'}{entry.type || 'FILE EVIDENCE'} + {:else if jsonValue(entry.value)} + {Array.isArray(jsonValue(entry.value)) ? `${jsonValue(entry.value).length} CONFIGURED ITEMS` : 'CONFIGURED DATA'} + {entry.value} + {:else} + {shown(entry.value)} + {/if} +
+ {/each} +
+
+ {/if} +
diff --git a/src/routes/isac/data-moderation/+page.server.ts b/src/routes/isac/data-moderation/+page.server.ts index 06fa1cc..3d30275 100644 --- a/src/routes/isac/data-moderation/+page.server.ts +++ b/src/routes/isac/data-moderation/+page.server.ts @@ -36,8 +36,14 @@ export const load: PageServerLoad = async ({ cookies, url }) => { LEFT JOIN users reviewer ON reviewer.id=p.reviewed_by LEFT JOIN user_profiles reviewer_profile ON reviewer_profile.user_id=reviewer.id ${where} ORDER BY p.created_at DESC LIMIT ? OFFSET ?`)).all(...args, pagination.perPage, pagination.offset); + const proposalsWithSnapshots = await Promise.all(proposals.map(async (proposal) => ({ + ...proposal, + current_snapshot: proposal.module === 'attributes' && proposal.entity_id + ? await (await database.prepare('SELECT * FROM stat_attributes WHERE id=?')).get(String(proposal.entity_id)) ?? null + : null + }))); const summaryRows = await (await database.prepare('SELECT status,COUNT(*) AS count FROM data_change_proposals GROUP BY status')).all(); - return { proposals, pagination, status, module, search, summary: Object.fromEntries(summaryRows.map((row) => [String(row.status), Number(row.count)])), modules }; + return { proposals: proposalsWithSnapshots, pagination, status, module, search, summary: Object.fromEntries(summaryRows.map((row) => [String(row.status), Number(row.count)])), modules }; }; export const actions: Actions = { diff --git a/src/routes/isac/data-moderation/+page.svelte b/src/routes/isac/data-moderation/+page.svelte index e0ecf8f..34fc844 100644 --- a/src/routes/isac/data-moderation/+page.svelte +++ b/src/routes/isac/data-moderation/+page.svelte @@ -3,6 +3,7 @@ import RegistryPagination from '$lib/components/RegistryPagination.svelte'; import KpiFilters from '$lib/components/KpiFilters.svelte'; import SearchableAssignment from '$lib/components/SearchableAssignment.svelte'; + import ProposalChangePreview from '$lib/components/ProposalChangePreview.svelte'; let { data, form } = $props(); let reviewing = $state(null); let decision = $state<'approve' | 'correct' | 'reject'>('approve'); @@ -69,8 +70,11 @@