Tighten request board layout

This commit is contained in:
Andraxion 2026-06-27 02:03:43 -04:00
parent db3e080640
commit 496df474b8
3 changed files with 622 additions and 672 deletions

View file

@ -454,7 +454,11 @@ function createLauncherRequestId() {
}
function normalizeLauncherRequestStatus(value) {
return String(value || "").trim().toLowerCase() === "active" ? "active" : "pending";
const normalized = String(value || "").trim().toLowerCase();
if (normalized === "active" || normalized === "implemented") {
return normalized;
}
return "pending";
}
function normalizeLauncherRequestTags(value) {

View file

@ -21,6 +21,7 @@ type WorldDefaultPayload = {
type LaunchState = "ready" | "opening" | "opened" | "blocked" | "error";
type BoardTab = "news" | "requests";
type LauncherWindowMode = "public" | "admin";
type LauncherRequestStatus = "pending" | "active" | "implemented";
type LauncherRequestAnalysisRouting = {
summary?: string;
@ -38,7 +39,7 @@ type LauncherRequest = {
id: string;
sourceSubmissionId?: string;
title: string;
status: "pending" | "active";
status: LauncherRequestStatus;
category: string;
tags: string[];
sourceText: string;
@ -145,6 +146,38 @@ function readLauncherWindowMode(): LauncherWindowMode {
return searchParams.get("admin") === "requests" ? "admin" : "public";
}
function normalizeCommaSeparatedList(value: string): string[] {
return value
.split(",")
.map((entry) => entry.trim())
.filter(Boolean);
}
function SaveIcon() {
return (
<svg viewBox="0 0 24 24" aria-hidden="true">
<path d="M5 4h11l3 3v13H5z" fill="none" stroke="currentColor" strokeWidth="1.8" strokeLinejoin="round" />
<path d="M8 4h7v5H8zM8 14h8v5H8z" fill="none" stroke="currentColor" strokeWidth="1.8" strokeLinejoin="round" />
</svg>
);
}
function CheckIcon() {
return (
<svg viewBox="0 0 24 24" aria-hidden="true">
<path d="M5 12.5l4.2 4.2L19 7" fill="none" stroke="currentColor" strokeWidth="2.2" strokeLinecap="round" strokeLinejoin="round" />
</svg>
);
}
function PlayIcon() {
return (
<svg viewBox="0 0 24 24" aria-hidden="true">
<path d="M8 6l10 6-10 6z" fill="currentColor" />
</svg>
);
}
async function resolveDefaultWorldId(): Promise<string> {
const response = await fetch("/api/world-default");
if (!response.ok) {
@ -193,137 +226,6 @@ function cloneLauncherRequest(requestEntry: LauncherRequest): LauncherRequest {
return JSON.parse(JSON.stringify(requestEntry)) as LauncherRequest;
}
function getPrimaryAnalysisItem(requestEntry: LauncherRequest): LauncherRequestAnalysisItem | null {
const items = Array.isArray(requestEntry.analysis?.items) ? requestEntry.analysis?.items : [];
return items.length > 0 ? items[0] : null;
}
function formatConfidence(value: number | null | undefined): string {
if (!Number.isFinite(Number(value))) {
return "Unscored";
}
return `${Math.round(Number(value) * 100)}%`;
}
function escapePopupHtml(value: string): string {
return String(value || "")
.replace(/&/g, "&amp;")
.replace(/</g, "&lt;")
.replace(/>/g, "&gt;")
.replace(/"/g, "&quot;")
.replace(/'/g, "&#39;");
}
function openReviewDetailsPopup(requestEntry: LauncherRequest): void {
const popup = window.open("", `worldshaper-review-${requestEntry.id}`, "popup=yes,width=840,height=760,resizable=yes,scrollbars=yes");
if (!popup) {
return;
}
const routing = requestEntry.analysis?.routing;
const items = Array.isArray(requestEntry.analysis?.items) ? requestEntry.analysis.items : [];
const renderedItems = items.map((item, index) => {
const reviewOptions = Array.isArray(item?.reviewOptions) ? item.reviewOptions : [];
return `
<section class="review-card">
<div class="review-kicker">Review Item ${index + 1}</div>
<h2>${escapePopupHtml(String(item?.title || `Request ${index + 1}`))}</h2>
<div class="review-meta">
<span>${escapePopupHtml(String(item?.primaryCategory || "Unsorted"))}</span>
<span>${escapePopupHtml(String(item?.statusRecommendation || "needs_review"))}</span>
<span>${escapePopupHtml(formatConfidence(item?.confidence))}</span>
</div>
<div class="review-block">
<h3>Review Rationale</h3>
<p>${escapePopupHtml(String(item?.reviewRationale || "No structured review rationale was returned."))}</p>
</div>
<div class="review-block">
<h3>Parsed Interpretation</h3>
<p>${escapePopupHtml(String(item?.parsedInterpretation || ""))}</p>
</div>
<div class="review-block">
<h3>Implementation Approach</h3>
<p>${escapePopupHtml(String(item?.implementationApproach || ""))}</p>
</div>
<div class="review-block">
<h3>Possible Options</h3>
${reviewOptions.length > 0
? `<ul>${reviewOptions.map((option) => `<li>${escapePopupHtml(String(option || ""))}</li>`).join("")}</ul>`
: "<p>No structured options were returned.</p>"}
</div>
<div class="review-block">
<h3>Notes</h3>
<p>${escapePopupHtml(String(item?.notes || "No extra notes."))}</p>
</div>
</section>
`;
}).join("");
popup.document.open();
popup.document.write(`<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8" />
<title>Worldshaper Review Details</title>
<style>
body { margin: 0; font-family: Georgia, "Segoe UI", sans-serif; background: #08111f; color: #e8f2ff; }
main { max-width: 920px; margin: 0 auto; padding: 24px; display: grid; gap: 16px; }
.hero { padding: 18px 20px; border: 1px solid #365782; border-radius: 14px; background: linear-gradient(180deg, rgba(17,32,63,.96), rgba(10,19,38,.98)); }
.hero h1 { margin: 0 0 8px; font-size: 28px; }
.hero p { margin: 0; color: #b8cfee; line-height: 1.6; white-space: pre-wrap; }
.review-card { padding: 18px 20px; border: 1px solid #365782; border-radius: 14px; background: rgba(17,32,63,.88); display: grid; gap: 12px; }
.review-kicker { color: #ffd166; font-size: 11px; font-weight: 700; text-transform: uppercase; letter-spacing: .08em; }
.review-card h2, .review-block h3 { margin: 0; }
.review-meta { display: flex; flex-wrap: wrap; gap: 8px; }
.review-meta span { padding: 4px 8px; border: 1px solid #365782; border-radius: 999px; background: rgba(8,16,31,.75); font-size: 12px; }
.review-block { display: grid; gap: 6px; }
.review-block p, .review-block ul { margin: 0; color: #d7e7ff; line-height: 1.6; white-space: pre-wrap; }
.review-block ul { padding-left: 18px; }
</style>
</head>
<body>
<main>
<section class="hero">
<h1>${escapePopupHtml(requestEntry.title)}</h1>
<p>${escapePopupHtml(requestEntry.sourceText)}</p>
</section>
${routing ? `
<section class="review-card">
<div class="review-kicker">Routing Summary</div>
<h2>${escapePopupHtml(String(routing.summary || "KB routing context"))}</h2>
<div class="review-meta">
<span>Ambiguity: ${escapePopupHtml(String(routing.ambiguity || "medium"))}</span>
${(Array.isArray(routing.suggestedTags) ? routing.suggestedTags : []).map((tag) => `<span>${escapePopupHtml(tag)}</span>`).join("")}
</div>
<div class="review-block">
<h3>Rationale</h3>
<p>${escapePopupHtml(String(routing.rationale || "No routing rationale was stored."))}</p>
</div>
<div class="review-block">
<h3>Matched Terms</h3>
${(Array.isArray(routing.matchedTerms) && routing.matchedTerms.length > 0)
? `<ul>${routing.matchedTerms.map((term) => `<li>${escapePopupHtml(String(term || ""))}</li>`).join("")}</ul>`
: "<p>No explicit terminology matches were stored.</p>"}
</div>
<div class="review-block">
<h3>Likely Systems</h3>
${(Array.isArray(routing.suggestedSystems) && routing.suggestedSystems.length > 0)
? `<ul>${routing.suggestedSystems.map((systemId) => `<li>${escapePopupHtml(String(systemId || ""))}</li>`).join("")}</ul>`
: "<p>No likely systems were stored.</p>"}
</div>
<div class="review-block">
<h3>Possible Directions</h3>
${(Array.isArray(routing.possibleDirections) && routing.possibleDirections.length > 0)
? `<ul>${routing.possibleDirections.map((direction) => `<li>${escapePopupHtml(String(direction || ""))}</li>`).join("")}</ul>`
: "<p>No alternate directions were stored.</p>"}
</div>
</section>
` : ""}
${renderedItems || '<section class="review-card"><h2>No Review Items</h2><p>This request does not have structured review details yet.</p></section>'}
</main>
</body>
</html>`);
popup.document.close();
}
function formatRequestTimestamp(value: string): string {
const parsed = Date.parse(String(value || ""));
if (!Number.isFinite(parsed)) {
@ -337,10 +239,6 @@ function formatRequestTimestamp(value: string): string {
}).format(parsed);
}
function formatRequestStatusLabel(status: "pending" | "active"): string {
return status === "active" ? "Active" : "Pending";
}
function normalizeAnalysisState(value: string | undefined): string {
return String(value || "").trim().toLowerCase();
}
@ -363,6 +261,9 @@ function formatAnalysisStateLabel(value: string | undefined): string {
}
function getRequestDisplayStateLabel(requestEntry: LauncherRequest): string {
if (requestEntry.status === "implemented") {
return "Implemented";
}
if (requestEntry.status === "active") {
return "Active";
}
@ -383,6 +284,9 @@ function getRequestDisplayStateLabel(requestEntry: LauncherRequest): string {
}
function getRequestDisplayStateClassName(requestEntry: LauncherRequest): string {
if (requestEntry.status === "implemented") {
return "implemented";
}
if (requestEntry.status === "active") {
return "active";
}
@ -504,6 +408,7 @@ function WorldshaperLauncher() {
const [adminAuthSubmitting, setAdminAuthSubmitting] = useState(false);
const [adminPasswordError, setAdminPasswordError] = useState("");
const [selectedAdminRequestId, setSelectedAdminRequestId] = useState("");
const [selectedAdminAnalysisIndex, setSelectedAdminAnalysisIndex] = useState(0);
const [adminEditorDraft, setAdminEditorDraft] = useState<LauncherRequest | null>(null);
const [adminSaving, setAdminSaving] = useState(false);
const [recentSaveEvents, setRecentSaveEvents] = useState<RecentSaveEvent[]>([]);
@ -671,6 +576,10 @@ function WorldshaperLauncher() {
setAdminEditorDraft(cloneLauncherRequest(requests[0]));
}, [adminPanelOpen, adminAccessGranted, requests, selectedAdminRequestId, adminEditorDraft]);
useEffect(() => {
setSelectedAdminAnalysisIndex(0);
}, [selectedAdminRequestId]);
async function handleLaunch(): Promise<void> {
setError("");
const nextWorldId = worldId || DEFAULT_EDITOR_WORLD_ID_FALLBACK;
@ -766,6 +675,7 @@ function WorldshaperLauncher() {
function handleSelectAdminRequest(requestId: string): void {
const nextRequest = requests.find((entry) => entry.id === requestId);
setSelectedAdminRequestId(requestId);
setSelectedAdminAnalysisIndex(0);
setAdminEditorDraft(nextRequest ? cloneLauncherRequest(nextRequest) : null);
setAdminNotice("");
setAdminPasswordError("");
@ -1053,6 +963,7 @@ function WorldshaperLauncher() {
const requestCount = requests.length;
const pendingRequestCount = requests.filter((entry) => entry.status === "pending").length;
const activeRequestCount = requests.filter((entry) => entry.status === "active").length;
const implementedRequestCount = requests.filter((entry) => entry.status === "implemented").length;
const queuedPendingRequestCount = requests.filter(isQueuedPendingRequest).length;
const needsReviewRequestCount = requests.filter(isNeedsReviewRequest).length;
const requestTags = (allowedRequestTags.length > 0
@ -1063,6 +974,12 @@ function WorldshaperLauncher() {
.map((entry) => String(entry || "").trim())
.filter(Boolean),
))).sort((a, b) => a.localeCompare(b));
const requestTagFilterOptions = requestTags
.map((tag) => ({
tag,
count: requests.filter((entry) => entry.status !== "pending" && entry.tags.includes(tag)).length,
}))
.filter((entry) => entry.count > 0);
const filteredRequests = requests.filter((entry) => {
if (requestFilter === "status:pending") {
return entry.status === "pending";
@ -1076,18 +993,22 @@ function WorldshaperLauncher() {
if (requestFilter === "status:active") {
return entry.status === "active";
}
if (requestFilter === "status:implemented") {
return entry.status === "implemented";
}
if (requestFilter.startsWith("tag:")) {
const tag = requestFilter.slice(4);
return entry.status === "active" && entry.tags.includes(tag);
return entry.status !== "pending" && entry.tags.includes(tag);
}
return true;
});
const boardTitle = adminWindowMode ? "Worldshaper Admin" : "Worldshaper Board";
const boardHint = adminWindowMode
? `${queuedPendingRequestCount} queued, ${needsReviewRequestCount} review, ${activeRequestCount} active`
? `${queuedPendingRequestCount} queued, ${needsReviewRequestCount} review, ${activeRequestCount} active, ${implementedRequestCount} implemented`
: (activeBoardTab === "news"
? "Latest announcements"
: `${queuedPendingRequestCount} queued, ${needsReviewRequestCount} review, ${activeRequestCount} active`);
: `${queuedPendingRequestCount} queued, ${needsReviewRequestCount} review, ${activeRequestCount} active, ${implementedRequestCount} implemented`);
const selectedAnalysisItem = adminEditorDraft?.analysis?.items?.[selectedAdminAnalysisIndex] || null;
return (
<main
@ -1197,7 +1118,7 @@ function WorldshaperLauncher() {
<div className="changelog-splash-kicker">{adminWindowMode ? "Protected Review Workspace" : "Shared Request Board"}</div>
<div className="changelog-splash-title" id="launcher-requests-title">{adminWindowMode ? "Admin Review Console" : "Requests"}</div>
<div className="changelog-splash-meta">
{requestCount} saved request{requestCount === 1 ? "" : "s"}: {queuedPendingRequestCount} queued, {needsReviewRequestCount} in review, and {activeRequestCount} active.
{requestCount} saved request{requestCount === 1 ? "" : "s"}: {queuedPendingRequestCount} queued, {needsReviewRequestCount} in review, {activeRequestCount} active, and {implementedRequestCount} implemented.
</div>
<div className="launcher-request-hero-actions">
<div className="launcher-request-toolbar-buttons">
@ -1233,8 +1154,9 @@ function WorldshaperLauncher() {
<option value="status:queued">Queued For Analysis</option>
<option value="status:review">Needs Review</option>
<option value="status:active">Active Requests</option>
{requestTags.map((tag) => (
<option key={tag} value={`tag:${tag}`}>{tag}</option>
<option value="status:implemented">Implemented Requests</option>
{requestTagFilterOptions.map(({ tag, count }) => (
<option key={tag} value={`tag:${tag}`}>({count}) {tag}</option>
))}
</select>
</label>
@ -1292,6 +1214,7 @@ function WorldshaperLauncher() {
<span>{needsReviewRequestCount} review</span>
<span>{pendingRequestCount} pending</span>
<span>{activeRequestCount} active</span>
<span>{implementedRequestCount} implemented</span>
</div>
</div>
<div className="launcher-request-admin-actions">
@ -1316,16 +1239,15 @@ function WorldshaperLauncher() {
{adminPasswordError ? <p className="launcher-request-error">{adminPasswordError}</p> : null}
{logsError ? <p className="launcher-request-error">{logsError}</p> : null}
<div className="launcher-request-admin-grid">
<div className="launcher-request-admin-sidebar">
<section className="launcher-request-admin-card">
<div className="launcher-request-admin-card-head">
<h4 className="launcher-request-admin-card-title">Request Management</h4>
<div className="launcher-request-admin-card-hint">Select a request to review or edit it.</div>
<div className="launcher-request-admin-card-hint">Select a request to load it on the right.</div>
</div>
<div className="launcher-request-admin-request-list">
{requests.map((requestEntry) => {
const isMutating = requestMutatingId === requestEntry.id;
const analysisState = formatAnalysisStateLabel(requestEntry.analysis?.state);
const reviewItem = getPrimaryAnalysisItem(requestEntry);
const isSelected = requestEntry.id === selectedAdminRequestId;
return (
<article
@ -1336,17 +1258,15 @@ function WorldshaperLauncher() {
<div className="launcher-request-admin-request-copy">
<div className="launcher-request-admin-request-title">{requestEntry.title}</div>
<div className="launcher-request-admin-request-meta">
<span>{formatRequestStatusLabel(requestEntry.status)}</span>
<span>{requestEntry.category}</span>
<span>{analysisState}</span>
<span>{formatConfidence(reviewItem?.confidence ?? requestEntry.analysis?.confidence)}</span>
<span>{getRequestDisplayStateLabel(requestEntry)}</span>
<span>{formatRequestTimestamp(requestEntry.updatedAt)}</span>
</div>
{reviewItem?.reviewRationale ? (
<div className="launcher-request-admin-request-rationale">{reviewItem.reviewRationale}</div>
) : null}
{!reviewItem?.reviewRationale && requestEntry.analysis?.routing?.summary ? (
<div className="launcher-request-admin-request-rationale">{requestEntry.analysis.routing.summary}</div>
{requestEntry.tags.length > 0 ? (
<div className="launcher-request-tags">
{requestEntry.tags.slice(0, 3).map((tag) => (
<span key={`${requestEntry.id}-${tag}`} className="launcher-request-tag">{tag}</span>
))}
</div>
) : null}
</div>
<button
@ -1358,6 +1278,7 @@ function WorldshaperLauncher() {
}}
disabled={isMutating}
aria-label={`Delete ${requestEntry.title}`}
title="Delete request"
>
X
</button>
@ -1366,82 +1287,104 @@ function WorldshaperLauncher() {
})}
</div>
</section>
<section className="launcher-request-admin-card launcher-request-admin-editor-card">
<section className="launcher-request-admin-card">
<div className="launcher-request-admin-card-head">
<h4 className="launcher-request-admin-card-title">Review Editor</h4>
<div className="launcher-request-admin-card-hint">Edit request fields, review details, and approval state.</div>
<h4 className="launcher-request-admin-card-title">Recent Logs</h4>
<div className="launcher-request-admin-card-hint">Newest events first.</div>
</div>
<div className="launcher-request-admin-log-list">
{logsLoading && recentSaveEvents.length === 0 ? (
<div className="launcher-request-empty">Loading admin logs...</div>
) : null}
{!logsLoading && recentSaveEvents.length === 0 ? (
<div className="launcher-request-empty">No admin logs have been recorded yet.</div>
) : null}
{recentSaveEvents.map((eventEntry, index) => (
<article key={`log-${eventEntry.at || index}-${eventEntry.type || "event"}`} className="launcher-request-admin-log-row">
<div className="launcher-request-admin-log-head">
<div className="launcher-request-admin-log-title">{formatEventLabel(eventEntry)}</div>
<div className="launcher-request-admin-log-time">{formatRequestTimestamp(String(eventEntry.at || ""))}</div>
</div>
<div className="launcher-request-admin-log-detail">{formatEventDetail(eventEntry) || "No extra details recorded."}</div>
</article>
))}
</div>
</section>
</div>
<section className="launcher-request-admin-card launcher-request-admin-detail-card">
{!adminEditorDraft ? (
<div className="launcher-request-empty">Select a request from the list to review it.</div>
) : (
<div className="launcher-request-admin-editor">
<div className="launcher-request-admin-editor-grid">
<label className="launcher-request-admin-field">
<span className="launcher-request-filter-label">Title</span>
<input
type="text"
className="launcher-request-filter-select"
value={adminEditorDraft.title}
onChange={(event) => updateAdminDraft((current) => ({ ...current, title: event.target.value }))}
/>
</label>
<label className="launcher-request-admin-field">
<span className="launcher-request-filter-label">Category</span>
<input
type="text"
className="launcher-request-filter-select"
value={adminEditorDraft.category}
onChange={(event) => updateAdminDraft((current) => ({ ...current, category: event.target.value }))}
/>
</label>
<div className="launcher-request-admin-detail">
<div className="launcher-request-admin-detail-top">
<div className="launcher-request-admin-detail-copy">
<div className="launcher-request-admin-kicker">Selected Request</div>
<h4 className="launcher-request-admin-title">{adminEditorDraft.title}</h4>
</div>
<label className="launcher-request-admin-field">
<span className="launcher-request-filter-label">Original Submission</span>
<textarea
className="launcher-request-textarea launcher-request-admin-textarea"
value={adminEditorDraft.sourceText}
onChange={(event) => updateAdminDraft((current) => ({ ...current, sourceText: event.target.value }))}
/>
</label>
<label className="launcher-request-admin-field">
<span className="launcher-request-filter-label">Request Summary</span>
<textarea
className="launcher-request-textarea launcher-request-admin-textarea-sm"
value={adminEditorDraft.summary}
onChange={(event) => updateAdminDraft((current) => ({ ...current, summary: event.target.value }))}
/>
</label>
<label className="launcher-request-admin-field">
<span className="launcher-request-filter-label">Request Implementation Notes</span>
<textarea
className="launcher-request-textarea launcher-request-admin-textarea-sm"
value={adminEditorDraft.implementationNotes}
onChange={(event) => updateAdminDraft((current) => ({ ...current, implementationNotes: event.target.value }))}
/>
</label>
<label className="launcher-request-admin-field">
<span className="launcher-request-filter-label">Request Tags</span>
<div className="launcher-request-admin-tag-grid">
{requestTags.map((tag) => {
const isActive = adminEditorDraft.tags.includes(tag);
return (
<button
key={`draft-tag-${tag}`}
type="button"
className={`launcher-request-tag launcher-request-admin-tag-toggle ${isActive ? "is-active" : ""}`}
onClick={() => updateAdminDraft((current) => ({
...current,
tags: isActive
? current.tags.filter((entry) => entry !== tag)
: [...current.tags, tag].sort((left, right) => left.localeCompare(right)),
}))}
<div className="launcher-request-admin-detail-controls">
<label className="launcher-request-admin-field launcher-request-admin-field-inline">
<span className="launcher-request-filter-label">Request State</span>
<select
className="launcher-request-filter-select"
value={adminEditorDraft.status}
onChange={(event) => updateAdminDraft((current) => ({ ...current, status: event.target.value as LauncherRequestStatus }))}
>
{tag}
</button>
);
})}
</div>
<option value="pending">Pending</option>
<option value="active">Active</option>
<option value="implemented">Implemented</option>
</select>
</label>
{(adminEditorDraft.analysis?.items?.length || 0) > 1 ? (
<label className="launcher-request-admin-field launcher-request-admin-field-inline">
<span className="launcher-request-filter-label">Analysis Item</span>
<select
className="launcher-request-filter-select"
value={String(selectedAdminAnalysisIndex)}
onChange={(event) => setSelectedAdminAnalysisIndex(Number(event.target.value) || 0)}
>
{(adminEditorDraft.analysis?.items || []).map((item, itemIndex) => (
<option key={`analysis-tab-${itemIndex}`} value={itemIndex}>
{item.title || `Request ${itemIndex + 1}`}
</option>
))}
</select>
</label>
) : null}
<div className="launcher-request-admin-icon-actions">
<button
type="button"
className="launcher-request-admin-icon-btn"
onClick={() => void handleSaveAdminRequest()}
disabled={adminSaving || requeueingMode !== ""}
title="Save all edits to the selected request without changing its review state."
aria-label="Save request"
>
<SaveIcon />
</button>
<button
type="button"
className="launcher-request-admin-icon-btn is-success"
onClick={() => void handleApproveAdminRequest()}
disabled={adminSaving || requeueingMode !== ""}
title="Approve the current reviewed item and promote it into the active request list."
aria-label="Approve request"
>
<CheckIcon />
</button>
<button
type="button"
className="launcher-request-admin-icon-btn is-success"
onClick={() => void handleRequeueAnalysis("draft")}
disabled={adminSaving || requeueingMode !== ""}
title="Submit the current edited draft back through the analyzer for a fresh manual review pass."
aria-label="Manual submission"
>
<PlayIcon />
</button>
</div>
</div>
</div>
<div className="launcher-request-admin-query-grid">
<section className="launcher-request-admin-analysis-item">
<div className="launcher-request-admin-analysis-head">
<div>
@ -1483,7 +1426,7 @@ function WorldshaperLauncher() {
...(current.analysis || {}),
routing: {
...(current.analysis?.routing || {}),
suggestedTags: event.target.value.split(",").map((entry) => entry.trim()).filter(Boolean),
suggestedTags: normalizeCommaSeparatedList(event.target.value),
},
},
}))}
@ -1493,7 +1436,7 @@ function WorldshaperLauncher() {
<label className="launcher-request-admin-field">
<span className="launcher-request-filter-label">Routing Summary</span>
<textarea
className="launcher-request-textarea launcher-request-admin-textarea launcher-request-admin-textarea-sm"
className="launcher-request-textarea launcher-request-admin-textarea-sm"
value={String(adminEditorDraft.analysis?.routing?.summary || "")}
onChange={(event) => updateAdminDraft((current) => ({
...current,
@ -1510,7 +1453,7 @@ function WorldshaperLauncher() {
<label className="launcher-request-admin-field">
<span className="launcher-request-filter-label">Routing Rationale</span>
<textarea
className="launcher-request-textarea launcher-request-admin-textarea launcher-request-admin-textarea-sm"
className="launcher-request-textarea launcher-request-admin-textarea-sm"
value={String(adminEditorDraft.analysis?.routing?.rationale || "")}
onChange={(event) => updateAdminDraft((current) => ({
...current,
@ -1524,10 +1467,11 @@ function WorldshaperLauncher() {
}))}
/>
</label>
<div className="launcher-request-admin-editor-grid">
<label className="launcher-request-admin-field">
<span className="launcher-request-filter-label">Matched Terms</span>
<textarea
className="launcher-request-textarea launcher-request-admin-textarea launcher-request-admin-textarea-sm"
className="launcher-request-textarea launcher-request-admin-textarea-xs"
value={Array.isArray(adminEditorDraft.analysis?.routing?.matchedTerms) ? adminEditorDraft.analysis?.routing?.matchedTerms?.join("\n") : ""}
onChange={(event) => updateAdminDraft((current) => ({
...current,
@ -1544,7 +1488,7 @@ function WorldshaperLauncher() {
<label className="launcher-request-admin-field">
<span className="launcher-request-filter-label">Likely Systems</span>
<textarea
className="launcher-request-textarea launcher-request-admin-textarea launcher-request-admin-textarea-sm"
className="launcher-request-textarea launcher-request-admin-textarea-xs"
value={Array.isArray(adminEditorDraft.analysis?.routing?.suggestedSystems) ? adminEditorDraft.analysis?.routing?.suggestedSystems?.join("\n") : ""}
onChange={(event) => updateAdminDraft((current) => ({
...current,
@ -1558,10 +1502,11 @@ function WorldshaperLauncher() {
}))}
/>
</label>
</div>
<label className="launcher-request-admin-field">
<span className="launcher-request-filter-label">Possible Directions</span>
<textarea
className="launcher-request-textarea launcher-request-admin-textarea launcher-request-admin-textarea-sm"
className="launcher-request-textarea launcher-request-admin-textarea-sm"
value={Array.isArray(adminEditorDraft.analysis?.routing?.possibleDirections) ? adminEditorDraft.analysis?.routing?.possibleDirections?.join("\n") : ""}
onChange={(event) => updateAdminDraft((current) => ({
...current,
@ -1576,29 +1521,51 @@ function WorldshaperLauncher() {
/>
</label>
</section>
{(adminEditorDraft.analysis?.items || []).map((item, itemIndex) => (
<section key={`analysis-item-${itemIndex}`} className="launcher-request-admin-analysis-item">
<section className="launcher-request-admin-analysis-item">
<div className="launcher-request-admin-analysis-head">
<div>
<div className="launcher-request-admin-kicker">Review Item {itemIndex + 1}</div>
<div className="launcher-request-admin-request-title">{item.title || `Request ${itemIndex + 1}`}</div>
<div className="launcher-request-admin-kicker">Analysis</div>
<div className="launcher-request-admin-request-title">{selectedAnalysisItem?.title || "No structured analysis item yet"}</div>
</div>
<button
type="button"
className="launcher-secondary-btn"
onClick={() => openReviewDetailsPopup(adminEditorDraft)}
>
Open Review Popup
</button>
</div>
<div className="launcher-request-admin-editor-grid">
<label className="launcher-request-admin-field">
<span className="launcher-request-filter-label">Title</span>
<input
type="text"
className="launcher-request-filter-select"
value={adminEditorDraft.title}
onChange={(event) => updateAdminDraft((current) => ({ ...current, title: event.target.value }))}
/>
</label>
<label className="launcher-request-admin-field">
<span className="launcher-request-filter-label">Request Tags</span>
<input
type="text"
className="launcher-request-filter-select"
value={adminEditorDraft.tags.join(", ")}
onChange={(event) => updateAdminDraft((current) => ({ ...current, tags: normalizeCommaSeparatedList(event.target.value) }))}
/>
</label>
</div>
<label className="launcher-request-admin-field">
<span className="launcher-request-filter-label">Original Submission</span>
<textarea
className="launcher-request-textarea launcher-request-admin-textarea-sm"
value={adminEditorDraft.sourceText}
onChange={(event) => updateAdminDraft((current) => ({ ...current, sourceText: event.target.value }))}
/>
</label>
{selectedAnalysisItem ? (
<>
<div className="launcher-request-admin-editor-grid">
<label className="launcher-request-admin-field">
<span className="launcher-request-filter-label">Item Title</span>
<input
type="text"
className="launcher-request-filter-select"
value={String(item.title || "")}
onChange={(event) => updateAdminDraftItem(itemIndex, (current) => ({ ...current, title: event.target.value }))}
value={String(selectedAnalysisItem.title || "")}
onChange={(event) => updateAdminDraftItem(selectedAdminAnalysisIndex, (current) => ({ ...current, title: event.target.value }))}
/>
</label>
<label className="launcher-request-admin-field">
@ -1606,16 +1573,16 @@ function WorldshaperLauncher() {
<input
type="text"
className="launcher-request-filter-select"
value={String(item.primaryCategory || "")}
onChange={(event) => updateAdminDraftItem(itemIndex, (current) => ({ ...current, primaryCategory: event.target.value }))}
value={String(selectedAnalysisItem.primaryCategory || "")}
onChange={(event) => updateAdminDraftItem(selectedAdminAnalysisIndex, (current) => ({ ...current, primaryCategory: event.target.value }))}
/>
</label>
<label className="launcher-request-admin-field">
<span className="launcher-request-filter-label">Recommendation</span>
<select
className="launcher-request-filter-select"
value={String(item.statusRecommendation || "needs_review")}
onChange={(event) => updateAdminDraftItem(itemIndex, (current) => ({ ...current, statusRecommendation: event.target.value }))}
value={String(selectedAnalysisItem.statusRecommendation || "needs_review")}
onChange={(event) => updateAdminDraftItem(selectedAdminAnalysisIndex, (current) => ({ ...current, statusRecommendation: event.target.value }))}
>
<option value="needs_review">Needs Review</option>
<option value="active">Active</option>
@ -1623,12 +1590,29 @@ function WorldshaperLauncher() {
<option value="duplicate">Duplicate</option>
</select>
</label>
<label className="launcher-request-admin-field">
<span className="launcher-request-filter-label">Confidence</span>
<input
type="number"
min="0"
max="1"
step="0.01"
className="launcher-request-filter-select"
value={Number.isFinite(Number(selectedAnalysisItem.confidence)) ? String(selectedAnalysisItem.confidence) : ""}
onChange={(event) => updateAdminDraftItem(selectedAdminAnalysisIndex, (current) => ({
...current,
confidence: event.target.value === "" ? null : Number(event.target.value),
}))}
/>
</label>
</div>
<div className="launcher-request-admin-editor-grid">
<label className="launcher-request-admin-field">
<span className="launcher-request-filter-label">Problem Type</span>
<select
className="launcher-request-filter-select"
value={String(item.problemType || "unknown")}
onChange={(event) => updateAdminDraftItem(itemIndex, (current) => ({ ...current, problemType: event.target.value }))}
value={String(selectedAnalysisItem.problemType || "unknown")}
onChange={(event) => updateAdminDraftItem(selectedAdminAnalysisIndex, (current) => ({ ...current, problemType: event.target.value }))}
>
<option value="feature">Feature</option>
<option value="bug">Bug</option>
@ -1639,153 +1623,73 @@ function WorldshaperLauncher() {
<option value="unknown">Unknown</option>
</select>
</label>
<label className="launcher-request-admin-field">
<span className="launcher-request-filter-label">Confidence</span>
<input
type="number"
min="0"
max="1"
step="0.01"
className="launcher-request-filter-select"
value={Number.isFinite(Number(item.confidence)) ? String(item.confidence) : ""}
onChange={(event) => updateAdminDraftItem(itemIndex, (current) => ({
...current,
confidence: event.target.value === "" ? null : Number(event.target.value),
}))}
/>
</label>
</div>
<label className="launcher-request-admin-field">
<span className="launcher-request-filter-label">Standardized Tags</span>
<div className="launcher-request-admin-tag-grid">
{requestTags.map((tag) => {
const isActive = Array.isArray(item.tags) && item.tags.includes(tag);
return (
<button
key={`item-${itemIndex}-tag-${tag}`}
type="button"
className={`launcher-request-tag launcher-request-admin-tag-toggle ${isActive ? "is-active" : ""}`}
onClick={() => updateAdminDraftItem(itemIndex, (current) => {
const currentTags = Array.isArray(current.tags) ? current.tags : [];
return {
<input
type="text"
className="launcher-request-filter-select"
value={Array.isArray(selectedAnalysisItem.tags) ? selectedAnalysisItem.tags.join(", ") : ""}
onChange={(event) => updateAdminDraftItem(selectedAdminAnalysisIndex, (current) => ({
...current,
tags: isActive
? currentTags.filter((entry) => entry !== tag)
: [...currentTags, tag].sort((left, right) => left.localeCompare(right)),
};
})}
>
{tag}
</button>
);
})}
</div>
</label>
<label className="launcher-request-admin-field">
<span className="launcher-request-filter-label">Review Rationale</span>
<textarea
className="launcher-request-textarea launcher-request-admin-textarea"
value={String(item.reviewRationale || "")}
onChange={(event) => updateAdminDraftItem(itemIndex, (current) => ({ ...current, reviewRationale: event.target.value }))}
/>
</label>
<label className="launcher-request-admin-field">
<span className="launcher-request-filter-label">Possible Options</span>
<textarea
className="launcher-request-textarea launcher-request-admin-textarea launcher-request-admin-textarea-sm"
value={Array.isArray(item.reviewOptions) ? item.reviewOptions.join("\n") : ""}
onChange={(event) => updateAdminDraftItem(itemIndex, (current) => ({
...current,
reviewOptions: event.target.value.split(/\r?\n/).map((entry) => entry.trim()).filter(Boolean),
tags: normalizeCommaSeparatedList(event.target.value),
}))}
/>
</label>
</div>
<label className="launcher-request-admin-field">
<span className="launcher-request-filter-label">Parsed Interpretation</span>
<textarea
className="launcher-request-textarea launcher-request-admin-textarea"
value={String(item.parsedInterpretation || "")}
onChange={(event) => updateAdminDraftItem(itemIndex, (current) => ({ ...current, parsedInterpretation: event.target.value }))}
className="launcher-request-textarea launcher-request-admin-textarea-sm"
value={String(selectedAnalysisItem.parsedInterpretation || "")}
onChange={(event) => updateAdminDraftItem(selectedAdminAnalysisIndex, (current) => ({ ...current, parsedInterpretation: event.target.value }))}
/>
</label>
<label className="launcher-request-admin-field">
<span className="launcher-request-filter-label">Implementation Approach</span>
<textarea
className="launcher-request-textarea launcher-request-admin-textarea"
value={String(item.implementationApproach || "")}
onChange={(event) => updateAdminDraftItem(itemIndex, (current) => ({ ...current, implementationApproach: event.target.value }))}
className="launcher-request-textarea launcher-request-admin-textarea-sm"
value={String(selectedAnalysisItem.implementationApproach || "")}
onChange={(event) => updateAdminDraftItem(selectedAdminAnalysisIndex, (current) => ({ ...current, implementationApproach: event.target.value }))}
/>
</label>
<div className="launcher-request-admin-editor-grid">
<label className="launcher-request-admin-field">
<span className="launcher-request-filter-label">Review Rationale</span>
<textarea
className="launcher-request-textarea launcher-request-admin-textarea-xs"
value={String(selectedAnalysisItem.reviewRationale || "")}
onChange={(event) => updateAdminDraftItem(selectedAdminAnalysisIndex, (current) => ({ ...current, reviewRationale: event.target.value }))}
/>
</label>
<label className="launcher-request-admin-field">
<span className="launcher-request-filter-label">Notes</span>
<span className="launcher-request-filter-label">Possible Options</span>
<textarea
className="launcher-request-textarea launcher-request-admin-textarea launcher-request-admin-textarea-sm"
value={String(item.notes || "")}
onChange={(event) => updateAdminDraftItem(itemIndex, (current) => ({ ...current, notes: event.target.value }))}
className="launcher-request-textarea launcher-request-admin-textarea-xs"
value={Array.isArray(selectedAnalysisItem.reviewOptions) ? selectedAnalysisItem.reviewOptions.join("\n") : ""}
onChange={(event) => updateAdminDraftItem(selectedAdminAnalysisIndex, (current) => ({
...current,
reviewOptions: event.target.value.split(/\r?\n/).map((entry) => entry.trim()).filter(Boolean),
}))}
/>
</label>
</div>
<label className="launcher-request-admin-field">
<span className="launcher-request-filter-label">Notes</span>
<textarea
className="launcher-request-textarea launcher-request-admin-textarea-xs"
value={String(selectedAnalysisItem.notes || "")}
onChange={(event) => updateAdminDraftItem(selectedAdminAnalysisIndex, (current) => ({ ...current, notes: event.target.value }))}
/>
</label>
</>
) : (
<div className="launcher-request-empty">This request does not have a structured analysis item yet.</div>
)}
</section>
))}
<div className="launcher-request-admin-actions">
<button
type="button"
className="launcher-primary-btn"
onClick={() => void handleSaveAdminRequest()}
disabled={adminSaving || requeueingMode !== ""}
>
{adminSaving ? "Saving..." : "Save Review Changes"}
</button>
<button
type="button"
className="launcher-secondary-btn"
onClick={() => void handleRequeueAnalysis("saved")}
disabled={adminSaving || requeueingMode !== ""}
>
{requeueingMode === "saved" ? "Re-running Saved Review..." : "Review Saved Request"}
</button>
<button
type="button"
className="launcher-secondary-btn"
onClick={() => void handleRequeueAnalysis("draft")}
disabled={adminSaving || requeueingMode !== ""}
>
{requeueingMode === "draft" ? "Submitting Draft Review..." : "Review Edited Draft"}
</button>
<button
type="button"
className="launcher-secondary-btn"
onClick={() => void handleApproveAdminRequest()}
disabled={adminSaving || requeueingMode !== ""}
>
Approve Request
</button>
</div>
</div>
)}
</section>
<section className="launcher-request-admin-card">
<div className="launcher-request-admin-card-head">
<h4 className="launcher-request-admin-card-title">Recent Logs</h4>
<div className="launcher-request-admin-card-hint">Newest events first.</div>
</div>
<div className="launcher-request-admin-log-list">
{logsLoading && recentSaveEvents.length === 0 ? (
<div className="launcher-request-empty">Loading admin logs...</div>
) : null}
{!logsLoading && recentSaveEvents.length === 0 ? (
<div className="launcher-request-empty">No admin logs have been recorded yet.</div>
) : null}
{recentSaveEvents.map((eventEntry, index) => (
<article key={`log-${eventEntry.at || index}-${eventEntry.type || "event"}`} className="launcher-request-admin-log-row">
<div className="launcher-request-admin-log-head">
<div className="launcher-request-admin-log-title">{formatEventLabel(eventEntry)}</div>
<div className="launcher-request-admin-log-time">{formatRequestTimestamp(String(eventEntry.at || ""))}</div>
</div>
<div className="launcher-request-admin-log-detail">{formatEventDetail(eventEntry) || "No extra details recorded."}</div>
</article>
))}
</div>
</section>
</div>
</>
)}
@ -1845,13 +1749,9 @@ function WorldshaperLauncher() {
) : null}
{!requestsLoading ? filteredRequests.map((requestEntry) => {
const isExpanded = expandedRequestIds.includes(requestEntry.id);
const isActiveRequest = requestEntry.status === "active";
const isActiveRequest = requestEntry.status === "active" || requestEntry.status === "implemented";
const requestDisplayState = getRequestDisplayStateLabel(requestEntry);
const requestDisplayStateClassName = getRequestDisplayStateClassName(requestEntry);
const reviewItem = getPrimaryAnalysisItem(requestEntry);
const analysisStateLabel = requestEntry.status === "pending"
? formatAnalysisStateLabel(requestEntry.analysis?.state)
: "";
return (
<section
key={requestEntry.id}
@ -1865,7 +1765,6 @@ function WorldshaperLauncher() {
</div>
<div className="launcher-request-entry-title-block">
<h3 className="launcher-request-entry-title">{requestEntry.title}</h3>
<div className="launcher-request-entry-category">{requestEntry.category}</div>
</div>
</div>
</div>
@ -1876,18 +1775,6 @@ function WorldshaperLauncher() {
))}
</div>
) : null}
<div className="launcher-request-entry-text">
{requestEntry.status === "active" ? requestEntry.summary : requestEntry.sourceText}
</div>
{requestEntry.status === "pending" && reviewItem?.reviewRationale ? (
<div className="launcher-request-entry-review-note">
Review reason: {reviewItem.reviewRationale}
</div>
) : null}
<div className="launcher-request-entry-meta">
{requestEntry.status === "pending" ? `${analysisStateLabel} | ` : ""}
{formatRequestTimestamp(requestEntry.updatedAt || requestEntry.createdAt)}
</div>
{isActiveRequest && isExpanded ? (
<div className="launcher-request-expanded">
<div className="launcher-request-expanded-block">

View file

@ -234,11 +234,11 @@ body {
min-height: 100%;
display: grid;
grid-template-rows: auto minmax(0, 1fr) auto;
gap: 12px;
gap: 10px;
}
.changelog-splash-hero {
padding: 14px 16px;
padding: 10px 12px;
border: 1px solid #4f79af;
border-radius: 12px;
background:
@ -249,25 +249,25 @@ body {
.changelog-splash-kicker {
color: #ffd166;
font-size: 10px;
font-size: 9px;
font-weight: 800;
text-transform: uppercase;
letter-spacing: 0.08em;
margin-bottom: 6px;
margin-bottom: 4px;
}
.changelog-splash-title {
color: #eef6ff;
font-size: 22px;
font-size: 19px;
font-weight: 800;
line-height: 1.1;
margin-bottom: 6px;
margin-bottom: 4px;
}
.changelog-splash-meta {
color: #a9c2ec;
font-size: 12px;
line-height: 1.45;
font-size: 11px;
line-height: 1.35;
}
.changelog-splash-list {
@ -281,9 +281,9 @@ body {
.launcher-request-hero-actions {
display: grid;
grid-template-columns: minmax(0, 1fr) auto;
gap: 12px;
gap: 10px;
align-items: end;
margin-top: 16px;
margin-top: 10px;
}
.launcher-request-toolbar-buttons {
@ -325,8 +325,8 @@ body {
.launcher-request-admin-panel {
display: grid;
gap: 12px;
padding: 14px;
gap: 10px;
padding: 10px;
border: 1px solid #476d9d;
border-radius: 12px;
background:
@ -402,6 +402,12 @@ body {
gap: 10px;
}
.launcher-request-admin-sidebar {
display: grid;
gap: 12px;
min-height: 0;
}
.launcher-request-admin-notice {
margin: 0;
color: #a8e6c2;
@ -411,21 +417,21 @@ body {
.launcher-request-admin-grid {
display: grid;
grid-template-columns: minmax(0, 1fr) minmax(0, 1fr);
grid-template-columns: 320px minmax(0, 1fr);
gap: 12px;
min-height: 0;
}
.launcher-shell-admin .launcher-request-admin-grid {
grid-template-columns: 360px minmax(0, 1fr);
grid-template-columns: 320px minmax(0, 1fr);
}
.launcher-request-admin-card {
display: grid;
grid-template-rows: auto minmax(0, 1fr);
gap: 10px;
gap: 8px;
min-height: 0;
padding: 12px;
padding: 10px;
border: 1px solid #365782;
border-radius: 12px;
background: rgba(8, 16, 31, 0.74);
@ -447,7 +453,7 @@ body {
.launcher-request-admin-card-hint {
color: #9fb8e5;
font-size: 11px;
font-size: 10px;
line-height: 1.4;
}
@ -463,14 +469,14 @@ body {
.launcher-shell-admin .launcher-request-admin-request-list,
.launcher-shell-admin .launcher-request-admin-log-list {
max-height: 42dvh;
max-height: 31dvh;
}
.launcher-request-admin-request-row,
.launcher-request-admin-log-row {
display: grid;
gap: 8px;
padding: 10px 12px;
gap: 6px;
padding: 8px 10px;
border: 1px solid #365782;
border-radius: 10px;
background: rgba(17, 32, 63, 0.82);
@ -491,19 +497,13 @@ body {
.launcher-request-admin-request-copy {
min-width: 0;
display: grid;
gap: 5px;
}
.launcher-request-admin-request-rationale {
color: #d7e7ff;
font-size: 12px;
line-height: 1.45;
gap: 4px;
}
.launcher-request-admin-request-title,
.launcher-request-admin-log-title {
color: #eef6ff;
font-size: 13px;
font-size: 12px;
font-weight: 700;
line-height: 1.35;
}
@ -539,26 +539,84 @@ body {
.launcher-request-admin-log-detail {
color: #d7e7ff;
font-size: 12px;
font-size: 11px;
line-height: 1.5;
white-space: pre-wrap;
}
.launcher-request-admin-editor-card {
grid-column: 1 / -1;
}
.launcher-request-admin-editor {
display: grid;
gap: 12px;
.launcher-request-admin-detail-card {
min-height: 0;
overflow: auto;
max-height: 58dvh;
padding-right: 4px;
}
.launcher-shell-admin .launcher-request-admin-editor {
max-height: 68dvh;
.launcher-request-admin-detail {
display: grid;
gap: 10px;
min-height: 0;
overflow: hidden;
}
.launcher-request-admin-detail-top {
display: flex;
justify-content: space-between;
align-items: flex-start;
gap: 12px;
}
.launcher-request-admin-detail-copy {
min-width: 0;
}
.launcher-request-admin-detail-controls {
display: flex;
flex-wrap: wrap;
justify-content: flex-end;
align-items: flex-end;
gap: 10px;
}
.launcher-request-admin-field-inline {
min-width: 160px;
}
.launcher-request-admin-icon-actions {
display: flex;
gap: 8px;
}
.launcher-request-admin-icon-btn {
width: 42px;
height: 42px;
display: inline-flex;
align-items: center;
justify-content: center;
border: 1px solid #365782;
border-radius: 12px;
background: rgba(17, 32, 63, 0.9);
color: #e7ecf3;
cursor: pointer;
}
.launcher-request-admin-icon-btn svg {
width: 18px;
height: 18px;
}
.launcher-request-admin-icon-btn.is-success {
border-color: rgba(110, 255, 173, 0.52);
background: linear-gradient(135deg, rgba(33, 146, 86, 0.88) 0%, rgba(24, 108, 123, 0.88) 100%);
color: #f3fff8;
}
.launcher-request-admin-icon-btn:disabled {
opacity: 0.56;
cursor: default;
}
.launcher-request-admin-query-grid {
display: grid;
grid-template-columns: repeat(2, minmax(0, 1fr));
gap: 10px;
min-height: 0;
}
.launcher-request-admin-editor-grid {
@ -585,11 +643,12 @@ body {
.launcher-request-admin-analysis-item {
display: grid;
gap: 10px;
padding: 12px;
gap: 8px;
padding: 10px;
border: 1px solid #365782;
border-radius: 12px;
background: rgba(8, 16, 31, 0.72);
align-content: start;
}
.launcher-request-admin-analysis-head {
@ -604,7 +663,11 @@ body {
}
.launcher-request-admin-textarea-sm {
min-height: 74px;
min-height: 58px;
}
.launcher-request-admin-textarea-xs {
min-height: 46px;
}
.launcher-request-composer-label {
@ -635,15 +698,15 @@ body {
min-height: 0;
overflow: auto;
display: grid;
gap: 10px;
gap: 8px;
padding-right: 4px;
}
.launcher-request-entry {
position: relative;
display: grid;
gap: 10px;
padding: 12px 14px;
gap: 7px;
padding: 9px 11px;
border: 1px solid #365782;
border-radius: 12px;
background: rgba(17, 32, 63, 0.84);
@ -700,6 +763,12 @@ body {
color: #b7f0d5;
}
.launcher-request-status-pill.is-implemented {
border-color: #4a8f60;
background: rgba(23, 88, 53, 0.86);
color: #d6ffe3;
}
.launcher-request-status-pill.is-pending {
border-color: #9c8140;
background: rgba(93, 70, 19, 0.78);
@ -733,21 +802,14 @@ body {
.launcher-request-entry-title-block {
min-width: 0;
display: grid;
gap: 3px;
display: block;
}
.launcher-request-entry-title {
margin: 0;
color: #eef6ff;
font-size: 14px;
line-height: 1.25;
}
.launcher-request-entry-category {
color: #9fb8e5;
font-size: 11px;
line-height: 1.35;
font-size: 13px;
line-height: 1.2;
}
.launcher-request-delete-btn {
@ -771,28 +833,15 @@ body {
}
.launcher-request-tag {
padding: 4px 8px;
padding: 3px 7px;
border: 1px solid #365782;
border-radius: 999px;
background: rgba(25, 48, 87, 0.72);
color: #d7e7ff;
font-size: 11px;
font-size: 10px;
line-height: 1;
}
.launcher-request-entry-text {
color: #d7e7ff;
font-size: 13px;
line-height: 1.5;
white-space: pre-wrap;
}
.launcher-request-entry-review-note {
color: #ffd5b0;
font-size: 12px;
line-height: 1.45;
}
.launcher-request-entry-meta,
.launcher-request-empty {
color: #9fb8e5;
@ -839,7 +888,7 @@ body {
}
.changelog-splash-section {
padding: 12px 14px;
padding: 10px 12px;
border: 1px solid #365782;
border-radius: 12px;
background: rgba(17, 32, 63, 0.84);
@ -887,10 +936,10 @@ body {
.launcher-primary-btn,
.launcher-secondary-btn {
min-height: 46px;
padding: 12px 16px;
min-height: 40px;
padding: 10px 14px;
border-radius: 12px;
font-size: 0.96rem;
font-size: 0.92rem;
font-weight: 700;
}
@ -2034,6 +2083,10 @@ button.danger:not(:disabled):hover {
grid-template-columns: 1fr;
}
.launcher-request-admin-query-grid {
grid-template-columns: 1fr;
}
.launcher-request-admin-request-row {
grid-template-columns: 1fr;
}
@ -2042,6 +2095,12 @@ button.danger:not(:disabled):hover {
grid-template-columns: 1fr;
}
.launcher-request-admin-detail-top,
.launcher-request-admin-detail-controls {
display: grid;
justify-content: stretch;
}
.launcher-request-admin-analysis-head {
grid-template-columns: 1fr;
display: grid;