feat: add automated community moderation
This commit is contained in:
@@ -26,17 +26,41 @@
|
||||
moderationReason: string | null;
|
||||
agent: AgentSummary;
|
||||
};
|
||||
type QueueItem = {
|
||||
id: string;
|
||||
source: 'scan' | 'report';
|
||||
contentType: 'comment' | 'profile_bio' | 'guide';
|
||||
contentId: string;
|
||||
bodyHtml: string;
|
||||
queuedAt: string;
|
||||
scanStatus: string | null;
|
||||
riskLevel: string;
|
||||
reason: string;
|
||||
category: string | null;
|
||||
provider: string | null;
|
||||
model: string | null;
|
||||
providerRequestId: string | null;
|
||||
reporterName: string | null;
|
||||
scores: Array<{ category: string; score: number }>;
|
||||
flags: string[];
|
||||
ruleMatches: Array<{ label: string; excerpt: string }>;
|
||||
targetType: string;
|
||||
targetTitle: string;
|
||||
targetHref: string;
|
||||
author: AgentSummary;
|
||||
};
|
||||
type CommunityItem = CommunityComment | CommunityBio;
|
||||
type ModerationAction = 'removeComment' | 'restoreComment' | 'hideBio' | 'restoreBio';
|
||||
type PageData = {
|
||||
tab: 'comments' | 'bios';
|
||||
tab: 'queue' | 'comments' | 'bios';
|
||||
status: string;
|
||||
search: string;
|
||||
summary: {
|
||||
queue: { total: number; automated: number; reported: number; priority: number };
|
||||
comments: { visible: number; removed: number };
|
||||
bios: { visible: number; hidden: number };
|
||||
};
|
||||
items: CommunityItem[];
|
||||
items: Array<CommunityItem | QueueItem>;
|
||||
pagination: any;
|
||||
permissions: { comments: boolean; profiles: boolean };
|
||||
};
|
||||
@@ -46,10 +70,15 @@
|
||||
let moderationAction = $state<ModerationAction | null>(null);
|
||||
let moderationReason = $state('');
|
||||
let confirming = $state(false);
|
||||
let queueReviewing = $state<QueueItem | null>(null);
|
||||
let queueDecision = $state<'approve' | 'reject'>('approve');
|
||||
let queueReason = $state('');
|
||||
let queueConfirming = $state(false);
|
||||
let handledResultToken = $state('');
|
||||
|
||||
const canModerateComments = $derived(data.permissions.comments);
|
||||
const canModerateProfiles = $derived(data.permissions.profiles);
|
||||
const isQueueItem = (item: CommunityItem | QueueItem): item is QueueItem => 'source' in item;
|
||||
const isComment = (item: CommunityItem): item is CommunityComment => 'author' in item;
|
||||
const itemAgent = (item: CommunityItem) => isComment(item) ? item.author : item.agent;
|
||||
const itemTimestamp = (item: CommunityItem) => isComment(item) ? item.createdAt : item.updatedAt;
|
||||
@@ -81,7 +110,14 @@
|
||||
: 'The bio will return to the Agent’s public profile.'
|
||||
);
|
||||
const statusItems = $derived(
|
||||
data.tab === 'comments'
|
||||
data.tab === 'queue'
|
||||
? [
|
||||
{ value: 'all', label: 'PENDING', count: data.summary.queue.total, description: 'All items requiring human review' },
|
||||
{ value: 'priority', label: 'PRIORITY', count: data.summary.queue.priority, description: 'High-risk or failed automated scans' },
|
||||
{ value: 'automated', label: 'AUTOMATED', count: data.summary.queue.automated, description: 'Items routed here by moderation scans' },
|
||||
{ value: 'reported', label: 'USER REPORTS', count: data.summary.queue.reported, description: 'Content flagged by community members' }
|
||||
]
|
||||
: data.tab === 'comments'
|
||||
? [
|
||||
{ value: 'all', label: 'ALL', count: data.summary.comments.visible + data.summary.comments.removed, description: 'All public and moderated comments' },
|
||||
{ value: 'visible', label: 'VISIBLE', count: data.summary.comments.visible, description: 'Comments currently visible to the community' },
|
||||
@@ -93,6 +129,22 @@
|
||||
{ value: 'hidden', label: 'HIDDEN', count: data.summary.bios.hidden, description: 'Bios hidden by Oversight' }
|
||||
]
|
||||
);
|
||||
const queueActionLabel = $derived(
|
||||
queueDecision === 'approve'
|
||||
? queueReviewing?.source === 'report' ? 'KEEP CONTENT' : queueReviewing?.contentType === 'comment' ? 'APPROVE & PUBLISH' : 'APPROVE REVISION'
|
||||
: queueReviewing?.source === 'report'
|
||||
? queueReviewing?.contentType === 'comment' ? 'REMOVE COMMENT' : `HIDE ${queueReviewing?.contentType === 'profile_bio' ? 'BIO' : 'GUIDE'}`
|
||||
: queueReviewing?.contentType === 'comment' ? 'REJECT & REMOVE' : 'REJECT REVISION'
|
||||
);
|
||||
const queueConsequence = $derived(
|
||||
queueDecision === 'approve'
|
||||
? queueReviewing?.source === 'report'
|
||||
? 'This report will be dismissed. Other reports for the same content will remain pending.'
|
||||
: 'This scan will be approved and the queued content will become public.'
|
||||
: queueReviewing?.source === 'report'
|
||||
? 'The content will be hidden or removed, and all pending reports for it will be resolved.'
|
||||
: 'This scan will be rejected. The queued revision will not be published.'
|
||||
);
|
||||
|
||||
function openModeration(item: CommunityItem) {
|
||||
reviewing = item;
|
||||
@@ -110,33 +162,53 @@
|
||||
confirming = false;
|
||||
}
|
||||
|
||||
function openQueueReview(item: QueueItem) {
|
||||
queueReviewing = item;
|
||||
queueDecision = 'approve';
|
||||
queueReason = '';
|
||||
queueConfirming = false;
|
||||
}
|
||||
|
||||
function closeQueueReview() {
|
||||
queueReviewing = null;
|
||||
queueReason = '';
|
||||
queueConfirming = false;
|
||||
}
|
||||
|
||||
$effect(() => {
|
||||
const token = String(form?.communityResultToken ?? '');
|
||||
if (!token || token === handledResultToken) return;
|
||||
handledResultToken = token;
|
||||
if (form?.communitySaved) closeModeration();
|
||||
if (form?.communitySaved) {
|
||||
closeModeration();
|
||||
closeQueueReview();
|
||||
}
|
||||
});
|
||||
</script>
|
||||
|
||||
<svelte:head><title>Community Oversight — ISAC</title></svelte:head>
|
||||
<svelte:window onkeydown={(event) => {
|
||||
if (event.key === 'Escape' && reviewing) closeModeration();
|
||||
if (event.key === 'Escape' && queueReviewing) closeQueueReview();
|
||||
}}/>
|
||||
|
||||
<div class="module-toolbar moderation-toolbar">
|
||||
<div>
|
||||
<p class="eyebrow">COMMUNITY OVERSIGHT / PUBLIC CONTENT CONTROL</p>
|
||||
<h2>COMMUNITY OVERSIGHT</h2>
|
||||
<p>Review public discussion and Agent bios. Moderation preserves evidence, requires a reason, and remains reversible.</p>
|
||||
<p>Resolve automated safety flags and community reports, then inspect the retained comment and biography history when needed.</p>
|
||||
</div>
|
||||
<div class="queue-readout">
|
||||
<span>{data.tab === 'comments' ? 'VISIBLE COMMENTS' : 'VISIBLE BIOS'}</span>
|
||||
<strong>{data.tab === 'comments' ? data.summary.comments.visible : data.summary.bios.visible}</strong>
|
||||
<small>PUBLIC RECORDS</small>
|
||||
<span>{data.tab === 'queue' ? 'PENDING REVIEW' : data.tab === 'comments' ? 'VISIBLE COMMENTS' : 'VISIBLE BIOS'}</span>
|
||||
<strong>{data.tab === 'queue' ? data.summary.queue.total : data.tab === 'comments' ? data.summary.comments.visible : data.summary.bios.visible}</strong>
|
||||
<small>{data.tab === 'queue' ? 'EXCEPTIONS ONLY' : 'PUBLIC RECORDS'}</small>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<nav class="community-tabs" aria-label="Community oversight channels">
|
||||
<a class:active={data.tab === 'queue'} aria-current={data.tab === 'queue' ? 'page' : undefined} href="/isac/community?tab=queue&status=all">
|
||||
<span>REVIEW QUEUE</span><b>{data.summary.queue.total}</b>
|
||||
</a>
|
||||
{#if canModerateComments}
|
||||
<a class:active={data.tab === 'comments'} aria-current={data.tab === 'comments' ? 'page' : undefined} href="/isac/community?tab=comments&status=all">
|
||||
<span>COMMENTS</span><b>{data.summary.comments.visible + data.summary.comments.removed}</b>
|
||||
@@ -154,14 +226,45 @@
|
||||
<form class="audit-filter community-filter">
|
||||
<input type="hidden" name="tab" value={data.tab}/>
|
||||
<input type="hidden" name="status" value={data.status}/>
|
||||
<input name="search" type="search" value={data.search} placeholder={data.tab === 'comments' ? 'AUTHOR, TARGET, OR COMMENT TEXT' : 'AGENT OR BIO TEXT'}/>
|
||||
<button class="secondary">SEARCH COMMUNITY →</button>
|
||||
<input name="search" type="search" value={data.search} placeholder={data.tab === 'queue' ? 'AUTHOR, TARGET, CATEGORY, OR CONTENT' : data.tab === 'comments' ? 'AUTHOR, TARGET, OR COMMENT TEXT' : 'AGENT OR BIO TEXT'}/>
|
||||
<button class="secondary">{data.tab === 'queue' ? 'SEARCH QUEUE →' : 'SEARCH COMMUNITY →'}</button>
|
||||
</form>
|
||||
|
||||
<RegistryPagination pagination={data.pagination}/>
|
||||
|
||||
<div class="community-registry">
|
||||
{#each data.items as item}
|
||||
{#if isQueueItem(item)}
|
||||
<article class="queue-item" class:priority={item.riskLevel === 'high' || item.scanStatus === 'error'}>
|
||||
<header>
|
||||
<AgentIdentityLink agent={item.author} meta="CONTENT AUTHOR" />
|
||||
<div class="community-state">
|
||||
<span>{date(item.queuedAt)}</span>
|
||||
<b class:visible={item.source === 'report'}>{item.source === 'scan' ? 'AUTOMATED SCAN' : 'USER REPORT'}</b>
|
||||
</div>
|
||||
</header>
|
||||
<div class="community-target">
|
||||
<span>{item.contentType.replace('_', ' ').toUpperCase()}</span>
|
||||
<a href={item.targetHref}>{item.targetTitle} →</a>
|
||||
</div>
|
||||
<div class="queue-evidence">
|
||||
<div class="queue-signal">
|
||||
<span class:priority={item.riskLevel === 'high'}>{item.riskLevel.toUpperCase()}</span>
|
||||
<strong>{item.category ? item.category.replace('_', ' ').toUpperCase() : item.scanStatus?.replace('_', ' ').toUpperCase()}</strong>
|
||||
</div>
|
||||
<p>{item.reason}</p>
|
||||
{#if item.reporterName}<small>REPORTED BY {item.reporterName}</small>{/if}
|
||||
{#if item.provider}<small>{item.provider.toUpperCase()} / {item.model ?? 'MODEL UNKNOWN'}</small>{/if}
|
||||
</div>
|
||||
<div class="community-markdown-preview">
|
||||
<MarkdownContent html={item.bodyHtml} compact emptyMessage="The queued content is no longer available."/>
|
||||
</div>
|
||||
<footer>
|
||||
<span>{item.source === 'scan' ? `SCAN ${item.id.slice(0,8).toUpperCase()}` : `REPORT ${item.id.slice(0,8).toUpperCase()}`}</span>
|
||||
<button class="secondary" onclick={() => openQueueReview(item)}>REVIEW EVIDENCE →</button>
|
||||
</footer>
|
||||
</article>
|
||||
{:else}
|
||||
<article class:moderated={item.status === 'removed' || item.status === 'hidden'}>
|
||||
<header>
|
||||
<AgentIdentityLink agent={itemAgent(item)} meta={isComment(item) ? 'COMMENT AUTHOR' : 'PROFILE OWNER'} />
|
||||
@@ -193,14 +296,106 @@
|
||||
{/if}
|
||||
</footer>
|
||||
</article>
|
||||
{/if}
|
||||
{:else}
|
||||
<div class="empty-dashboard">
|
||||
<strong>NO MATCHING {data.tab === 'comments' ? 'COMMENTS' : 'PROFILE BIOS'}</strong>
|
||||
<span>Adjust the status or search filters to inspect another part of the community record.</span>
|
||||
<strong>NO MATCHING {data.tab === 'queue' ? 'REVIEW EXCEPTIONS' : data.tab === 'comments' ? 'COMMENTS' : 'PROFILE BIOS'}</strong>
|
||||
<span>{data.tab === 'queue' ? 'Automated checks have not routed any matching content to human Oversight.' : 'Adjust the status or search filters to inspect another part of the community record.'}</span>
|
||||
</div>
|
||||
{/each}
|
||||
</div>
|
||||
|
||||
{#if queueReviewing}
|
||||
<div class="isac-modal-backdrop" role="presentation">
|
||||
<div class="isac-confirm-modal community-review-modal" role="dialog" aria-modal="true" aria-labelledby="queue-review-title">
|
||||
<div class="editor-title">
|
||||
<div>
|
||||
<p class="eyebrow">{queueReviewing.source === 'scan' ? 'AUTOMATED SAFETY FLAG' : 'COMMUNITY REPORT'} / {queueReviewing.contentType.replace('_', ' ').toUpperCase()}</p>
|
||||
<h3 id="queue-review-title">REVIEW CONTENT EXCEPTION</h3>
|
||||
</div>
|
||||
<button aria-label="Close content review" onclick={closeQueueReview}>×</button>
|
||||
</div>
|
||||
<div class="community-review-identity">
|
||||
<AgentIdentityLink agent={queueReviewing.author} meta="CONTENT AUTHOR" />
|
||||
<span>{date(queueReviewing.queuedAt)}</span>
|
||||
</div>
|
||||
<div class="community-target review-target">
|
||||
<span>{queueReviewing.contentType.replace('_', ' ').toUpperCase()}</span>
|
||||
<a href={queueReviewing.targetHref}>{queueReviewing.targetTitle} →</a>
|
||||
</div>
|
||||
<div class="queue-review-evidence">
|
||||
<div>
|
||||
<span>SOURCE</span><strong>{queueReviewing.source === 'scan' ? 'AUTOMATED SCAN' : 'USER REPORT'}</strong>
|
||||
</div>
|
||||
<div>
|
||||
<span>RISK / CATEGORY</span><strong>{queueReviewing.riskLevel.toUpperCase()} · {(queueReviewing.category ?? queueReviewing.scanStatus ?? 'REVIEW').replace('_', ' ').toUpperCase()}</strong>
|
||||
</div>
|
||||
{#if queueReviewing.provider}
|
||||
<div><span>PROVIDER / MODEL</span><strong>{queueReviewing.provider.toUpperCase()} · {queueReviewing.model ?? 'UNKNOWN'}</strong></div>
|
||||
{/if}
|
||||
{#if queueReviewing.providerRequestId}
|
||||
<div><span>REQUEST ID</span><strong>{queueReviewing.providerRequestId}</strong></div>
|
||||
{/if}
|
||||
{#if queueReviewing.reporterName}
|
||||
<div><span>REPORTER</span><strong>{queueReviewing.reporterName}</strong></div>
|
||||
{/if}
|
||||
</div>
|
||||
<div class="moderation-evidence queue-reason-evidence"><span>ROUTING REASON</span><p>{queueReviewing.reason}</p></div>
|
||||
{#if queueReviewing.flags.length || queueReviewing.ruleMatches.length || queueReviewing.scores.length}
|
||||
<div class="queue-signals">
|
||||
{#if queueReviewing.flags.length}<div><span>FLAGGED CATEGORIES</span><p>{queueReviewing.flags.join(' · ')}</p></div>{/if}
|
||||
{#if queueReviewing.ruleMatches.length}<div><span>LOCAL RULES</span>{#each queueReviewing.ruleMatches as match}<p><b>{match.label}</b>{match.excerpt ? ` — ${match.excerpt}` : ''}</p>{/each}</div>{/if}
|
||||
{#if queueReviewing.scores.length}<div><span>TOP PROVIDER SCORES</span>{#each queueReviewing.scores as score}<p><b>{score.category}</b> {(score.score * 100).toFixed(1)}%</p>{/each}</div>{/if}
|
||||
</div>
|
||||
{/if}
|
||||
<div class="community-review-content"><MarkdownContent html={queueReviewing.bodyHtml} emptyMessage="The queued content is no longer available."/></div>
|
||||
<div class="decision-tabs queue-decision-tabs" role="group" aria-label="Moderation decision">
|
||||
<button type="button" class:active={queueDecision === 'approve'} aria-pressed={queueDecision === 'approve'} onclick={() => { queueDecision = 'approve'; queueConfirming = false; }}>
|
||||
<b>✓</b><span>{queueReviewing.source === 'report' ? 'KEEP' : 'APPROVE'}<small>{queueReviewing.source === 'report' ? 'DISMISS THIS REPORT' : 'PUBLISH QUEUED CONTENT'}</small></span>
|
||||
</button>
|
||||
<button type="button" class:active={queueDecision === 'reject'} aria-pressed={queueDecision === 'reject'} onclick={() => { queueDecision = 'reject'; queueConfirming = false; }}>
|
||||
<b>×</b><span>{queueReviewing.source === 'report' ? 'REMOVE' : 'REJECT'}<small>{queueReviewing.source === 'report' ? 'RESOLVE ALL REPORTS' : 'KEEP CONTENT HIDDEN'}</small></span>
|
||||
</button>
|
||||
</div>
|
||||
{#if !queueConfirming}
|
||||
<label class="field moderation-reason">
|
||||
OVERSIGHT REASON — REQUIRED
|
||||
<textarea bind:value={queueReason} minlength="3" maxlength="500" required placeholder="Record the evidence and policy basis for this decision…"></textarea>
|
||||
</label>
|
||||
{#if form?.communityQueueError}<p class="error queue-error">{form.communityQueueError}</p>{/if}
|
||||
<div class="moderation-warning" class:rejecting={queueDecision === 'reject'}>
|
||||
<b>!</b><span>{queueConsequence} The review decision and reason will be added to the audit history.</span>
|
||||
</div>
|
||||
<div class="form-actions">
|
||||
<button type="button" class="secondary" onclick={closeQueueReview}>CANCEL</button>
|
||||
<button type="button" class="primary" disabled={queueReason.trim().length < 3} onclick={() => queueConfirming = true}>REVIEW ACTION →</button>
|
||||
</div>
|
||||
{:else}
|
||||
{@const queueReviewingId = queueReviewing.id}
|
||||
{@const queueReviewingSource = queueReviewing.source}
|
||||
<div class="community-final-confirm" role="alert">
|
||||
<div><span>FINAL CONFIRMATION</span><strong>{queueActionLabel}?</strong><p>{queueConsequence}</p></div>
|
||||
<dl><dt>OVERSIGHT REASON</dt><dd>{queueReason}</dd></dl>
|
||||
</div>
|
||||
<PendingForm action={queueDecision === 'approve' ? '?/approveQueue' : '?/rejectQueue'}>
|
||||
{#snippet children(pending)}
|
||||
<input type="hidden" name="id" value={queueReviewingId}/>
|
||||
<input type="hidden" name="source" value={queueReviewingSource}/>
|
||||
<input type="hidden" name="reason" value={queueReason}/>
|
||||
{#if form?.communityQueueError}<p class="error">{form.communityQueueError}</p>{/if}
|
||||
<div class="form-actions">
|
||||
<button type="button" class="secondary" disabled={pending} onclick={() => queueConfirming = false}>BACK</button>
|
||||
<button class={queueDecision === 'reject' ? 'danger' : 'primary'} disabled={pending} aria-busy={pending}>
|
||||
{pending ? 'PROCESSING…' : `CONFIRM ${queueActionLabel} →`}
|
||||
</button>
|
||||
</div>
|
||||
{/snippet}
|
||||
</PendingForm>
|
||||
{/if}
|
||||
</div>
|
||||
</div>
|
||||
{/if}
|
||||
|
||||
{#if reviewing && moderationAction}
|
||||
<div class="isac-modal-backdrop" role="presentation">
|
||||
<div class="isac-confirm-modal community-review-modal" role="dialog" aria-modal="true" aria-labelledby="community-review-title">
|
||||
@@ -256,15 +451,18 @@
|
||||
{/if}
|
||||
|
||||
<style>
|
||||
.community-tabs{height:auto;display:grid;grid-template-columns:repeat(2,minmax(0,1fr));margin:0 0 8px;border:1px solid var(--shd-border);background:#0d1011}
|
||||
.community-tabs{height:auto;display:grid;grid-template-columns:repeat(3,minmax(0,1fr));margin:0 0 8px;border:1px solid var(--shd-border);background:#0d1011}
|
||||
.community-tabs a{min-height:46px;display:grid;grid-template-columns:minmax(0,1fr) auto;align-items:center;gap:8px;padding:8px 11px;border-left:1px solid var(--shd-border);border-bottom:2px solid transparent;color:var(--shd-text-muted)}.community-tabs a:first-child{border-left:0}.community-tabs a span,.community-tabs a b{font-size:var(--shd-type-telemetry);font-weight:900;letter-spacing:.8px}.community-tabs a b{color:var(--shd-text-dim)}.community-tabs a:hover,.community-tabs a:focus-visible{background:var(--shd-surface-active);color:var(--shd-text)}.community-tabs a.active{border-bottom-color:var(--shd-orange);background:#211a0e;color:var(--shd-orange)}.community-tabs a.active b{color:var(--shd-orange)}
|
||||
.community-filter{grid-template-columns:minmax(0,1fr) auto}.community-registry{display:grid;grid-template-columns:repeat(2,minmax(0,1fr));gap:7px;margin-top:7px}.community-registry>article{min-width:0;display:grid;grid-template-rows:auto auto minmax(100px,1fr) auto;border:1px solid var(--shd-border);border-left:2px solid var(--shd-info);background:var(--shd-surface)}.community-registry>article.moderated{border-left-color:var(--shd-danger)}
|
||||
.community-registry article>header{min-height:55px;display:flex;align-items:center;justify-content:space-between;gap:10px;padding:8px 10px;border-bottom:1px solid var(--shd-border);background:var(--shd-surface-raised)}.community-registry article>header :global(.agent-identity){flex:1}.community-state{display:grid;justify-items:end;gap:4px}.community-state span,.community-state b{font-size:var(--shd-type-telemetry);font-weight:900;letter-spacing:.7px}.community-state span{color:var(--shd-text-muted)}.community-state b{padding:3px 5px;border:1px solid var(--shd-danger);color:var(--shd-danger)}.community-state b.visible{border-color:var(--shd-success);color:var(--shd-success)}
|
||||
.community-target{display:flex;align-items:center;justify-content:space-between;gap:8px;padding:7px 10px;border-bottom:1px solid var(--shd-border-soft);background:#0d1011}.community-target span,.community-target a{font-size:var(--shd-type-telemetry);font-weight:900;letter-spacing:.7px}.community-target span{color:var(--shd-text-muted)}.community-target a{overflow:hidden;color:var(--shd-orange);text-overflow:ellipsis;white-space:nowrap}.community-markdown-preview{max-height:150px;overflow:hidden;padding:11px;position:relative}.community-markdown-preview:after{position:absolute;right:0;bottom:0;left:0;height:25px;background:linear-gradient(transparent,var(--shd-surface));content:"";pointer-events:none}
|
||||
.community-registry>article.queue-item{grid-template-rows:auto auto auto minmax(100px,1fr) auto;border-left:1px solid var(--shd-info)}.community-registry>article.queue-item.priority{border-color:var(--shd-danger)}
|
||||
.queue-evidence{display:grid;gap:5px;padding:9px 10px;border-bottom:1px solid var(--shd-border);background:var(--shd-surface-raised)}.queue-evidence p{margin:0;color:var(--shd-text);line-height:1.4;overflow-wrap:anywhere}.queue-evidence small{color:var(--shd-text-muted);font-size:var(--shd-type-telemetry);font-weight:900;letter-spacing:.7px;overflow-wrap:anywhere}.queue-signal{display:flex;align-items:center;gap:7px}.queue-signal span,.queue-signal strong{padding:3px 5px;border:1px solid var(--shd-border-strong);color:var(--shd-text-muted);font-size:var(--shd-type-telemetry);font-weight:900;letter-spacing:.7px}.queue-signal span.priority{border-color:var(--shd-danger);color:var(--shd-danger)}
|
||||
.moderation-evidence{display:grid;gap:4px;margin:0 10px 9px;padding:8px;border:1px solid var(--shd-border);border-left:2px solid var(--shd-danger);background:#1b1111}.moderation-evidence span{color:var(--shd-danger);font-size:var(--shd-type-telemetry);font-weight:900;letter-spacing:.7px}.moderation-evidence p{margin:0;color:var(--shd-text-muted);font-size:var(--shd-type-supporting);line-height:1.4}
|
||||
.community-registry article>footer{display:flex;align-items:center;justify-content:space-between;gap:8px;padding:8px 10px;border-top:1px solid var(--shd-border);background:var(--shd-surface-raised)}.community-registry article>footer>span{color:var(--shd-text-dim);font-size:var(--shd-type-telemetry);font-weight:900;letter-spacing:.7px}.community-registry article>footer button{min-height:36px;padding:0 10px}
|
||||
.community-review-modal{width:min(900px,calc(100vw - 28px));max-height:calc(100vh - 28px);overflow:auto}.community-review-identity{display:flex;align-items:center;justify-content:space-between;gap:10px;padding:10px;border-bottom:1px solid var(--shd-border)}.community-review-identity :global(.agent-identity){flex:1}.community-review-identity>span{color:var(--shd-text-muted);font-size:var(--shd-type-telemetry);font-weight:900}.review-target{border-bottom:1px solid var(--shd-border)}.community-review-content{max-height:330px;overflow:auto;padding:14px;border-bottom:1px solid var(--shd-border);background:#0d1011}.community-review-content :global(.markdown-content){max-width:none}.moderation-reason{margin:10px}.moderation-reason textarea{min-height:90px}.community-final-confirm{display:grid;grid-template-columns:minmax(0,1fr) minmax(220px,.65fr);gap:9px;margin:10px;padding:10px;border:1px solid var(--shd-orange);background:#1b160d}.community-final-confirm>div{display:grid;gap:4px}.community-final-confirm span,.community-final-confirm dt{color:var(--shd-orange);font-size:var(--shd-type-telemetry);font-weight:900;letter-spacing:.8px}.community-final-confirm p,.community-final-confirm dd{margin:0;color:var(--shd-text-muted);font-size:var(--shd-type-supporting);line-height:1.4}.community-final-confirm dl{display:grid;align-content:start;gap:5px;margin:0;padding:8px;border:1px solid var(--shd-border);background:#0d1011}.community-review-modal>.moderation-warning,.community-review-modal>.form-actions,.community-review-modal :global(form > .form-actions),.community-review-modal :global(form > .error){margin-right:10px;margin-left:10px}
|
||||
.queue-review-evidence{display:grid;grid-template-columns:repeat(2,minmax(0,1fr));border-bottom:1px solid var(--shd-border)}.queue-review-evidence>div{min-width:0;display:grid;gap:3px;padding:8px 10px;border-right:1px solid var(--shd-border);border-top:1px solid var(--shd-border-soft)}.queue-review-evidence span,.queue-signals span{color:var(--shd-text-muted);font-size:var(--shd-type-telemetry);font-weight:900;letter-spacing:.7px}.queue-review-evidence strong{overflow-wrap:anywhere}.queue-reason-evidence{margin-top:10px}.queue-signals{display:grid;grid-template-columns:repeat(3,minmax(0,1fr));gap:1px;margin:0 10px 10px;border:1px solid var(--shd-border);background:var(--shd-border)}.queue-signals>div{min-width:0;padding:8px;background:#0d1011}.queue-signals p{margin:4px 0 0;color:var(--shd-text-muted);font-size:var(--shd-type-supporting);overflow-wrap:anywhere}.queue-decision-tabs{margin:10px}.queue-error{margin:0 10px}
|
||||
@media(max-width:900px){.community-registry{grid-template-columns:1fr}}
|
||||
@media(max-width:650px){.community-tabs{grid-template-columns:1fr}.community-tabs a{min-height:52px;border-top:1px solid var(--shd-border);border-left:0}.community-tabs a:first-child{border-top:0}.community-filter{grid-template-columns:1fr}.community-filter button{min-height:44px}.community-registry article>header,.community-registry article>footer,.community-review-identity,.community-target{align-items:stretch;flex-direction:column}.community-state{justify-items:start}.community-target a{white-space:normal}.community-registry article>footer button{width:100%;min-height:44px}.community-final-confirm{grid-template-columns:1fr}.community-review-modal>.form-actions,.community-review-modal :global(.form-actions){display:grid;grid-template-columns:1fr 1fr}.community-review-modal button{min-height:44px}}
|
||||
@media(max-width:650px){.community-tabs{grid-template-columns:1fr}.community-tabs a{min-height:52px;border-top:1px solid var(--shd-border);border-left:0}.community-tabs a:first-child{border-top:0}.community-filter{grid-template-columns:1fr}.community-filter button{min-height:44px}.community-registry article>header,.community-registry article>footer,.community-review-identity,.community-target{align-items:stretch;flex-direction:column}.community-state{justify-items:start}.community-target a{white-space:normal}.community-registry article>footer button{width:100%;min-height:44px}.community-final-confirm,.queue-review-evidence,.queue-signals{grid-template-columns:1fr}.community-review-modal>.form-actions,.community-review-modal :global(.form-actions){display:grid;grid-template-columns:1fr 1fr}.community-review-modal button{min-height:44px}}
|
||||
@media(prefers-reduced-motion:reduce){.community-tabs a,.community-registry button{transition:none}}
|
||||
</style>
|
||||
|
||||
Reference in New Issue
Block a user