mirror of
https://github.com/SoPat712/allstarr.git
synced 2026-10-08 14:05:02 -04:00
feat(ui): add in-app GitHub issue drafting
This commit is contained in:
6 files changed
+549
-3
No files matched your search
@@ -0,0 +1,5 @@
|
||||
blank_issues_enabled: false
|
||||
contact_links:
|
||||
- name: Allstarr Documentation
|
||||
url: https://github.com/SoPat712/allstarr#readme
|
||||
about: Check the setup and usage docs before filing a new issue.
|
||||
@@ -84,6 +84,7 @@
|
||||
<button class="sidebar-link" type="button" data-tab="kept">Kept Downloads</button>
|
||||
<button class="sidebar-link" type="button" data-tab="scrobbling">Scrobbling</button>
|
||||
<button class="sidebar-link" type="button" data-tab="config">Configuration</button>
|
||||
<button class="sidebar-link" type="button" data-tab="report-issues">Report Issues</button>
|
||||
<button class="sidebar-link" type="button" data-tab="endpoints">API Analytics</button>
|
||||
</nav>
|
||||
<div class="sidebar-footer">
|
||||
@@ -102,6 +103,7 @@
|
||||
<div class="tab" data-tab="kept">Kept Downloads</div>
|
||||
<div class="tab" data-tab="scrobbling">Scrobbling</div>
|
||||
<div class="tab" data-tab="config">Configuration</div>
|
||||
<div class="tab" data-tab="report-issues">Report Issues</div>
|
||||
<div class="tab" data-tab="endpoints">API Analytics</div>
|
||||
</div>
|
||||
|
||||
@@ -895,6 +897,81 @@
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Report Issues Tab -->
|
||||
<div class="tab-content" id="tab-report-issues">
|
||||
<div class="card">
|
||||
<h2>Report Issues</h2>
|
||||
<div class="guidance-banner info mb-16">
|
||||
<span>ℹ️</span>
|
||||
<div class="guidance-content">
|
||||
<div class="guidance-title">Draft a GitHub issue from inside Allstarr.</div>
|
||||
<div class="guidance-detail">Allstarr includes only safe diagnostics here. Sensitive values stay redacted, and the final submit still happens on GitHub.</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="report-issue-layout">
|
||||
<div class="report-issue-panel">
|
||||
<div class="form-group">
|
||||
<label for="issue-report-type">Report Type</label>
|
||||
<select id="issue-report-type">
|
||||
<option value="bug">Bug Report</option>
|
||||
<option value="feature">Feature Request</option>
|
||||
</select>
|
||||
</div>
|
||||
|
||||
<div class="form-group">
|
||||
<label for="issue-report-title">Title</label>
|
||||
<input type="text" id="issue-report-title" placeholder="Short summary of the issue">
|
||||
</div>
|
||||
|
||||
<div class="form-group">
|
||||
<label for="issue-report-primary" id="issue-report-primary-label">Describe the bug</label>
|
||||
<textarea id="issue-report-primary" rows="5"
|
||||
placeholder="What happened? What looked wrong?"></textarea>
|
||||
</div>
|
||||
|
||||
<div class="form-group">
|
||||
<label for="issue-report-secondary" id="issue-report-secondary-label">To Reproduce</label>
|
||||
<textarea id="issue-report-secondary" rows="5"
|
||||
placeholder="List the steps needed to reproduce the issue"></textarea>
|
||||
</div>
|
||||
|
||||
<div class="form-group">
|
||||
<label for="issue-report-tertiary" id="issue-report-tertiary-label">Expected behavior</label>
|
||||
<textarea id="issue-report-tertiary" rows="4"
|
||||
placeholder="What did you expect to happen instead?"></textarea>
|
||||
</div>
|
||||
|
||||
<div class="form-group">
|
||||
<label for="issue-report-context" id="issue-report-context-label">Additional context</label>
|
||||
<textarea id="issue-report-context" rows="4"
|
||||
placeholder="Anything else that might help, including screenshots or surrounding context"></textarea>
|
||||
</div>
|
||||
|
||||
<div class="card-actions-row">
|
||||
<button class="primary" type="button" id="open-github-issue-btn">Open Bug Report on GitHub</button>
|
||||
<button type="button" id="copy-issue-report-btn">Copy Report</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="report-preview-panel">
|
||||
<div class="guidance-banner compact">
|
||||
Safe diagnostics only: version, backend type, active tab, client browser, and redacted config state.
|
||||
</div>
|
||||
|
||||
<div class="form-group">
|
||||
<label for="issue-report-preview">GitHub Issue Preview</label>
|
||||
<textarea id="issue-report-preview" rows="22" readonly></textarea>
|
||||
</div>
|
||||
|
||||
<div class="report-preview-help" id="issue-report-preview-help">
|
||||
GitHub drafts that exceed the URL size limit will open with a shorter body. The full report will also be copied to your clipboard.
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- API Analytics Tab -->
|
||||
<div class="tab-content" id="tab-endpoints">
|
||||
<div class="card">
|
||||
|
||||
@@ -72,6 +72,7 @@ function applyAuthorizationScope() {
|
||||
"kept",
|
||||
"scrobbling",
|
||||
"config",
|
||||
"report-issues",
|
||||
"endpoints",
|
||||
];
|
||||
|
||||
|
||||
@@ -0,0 +1,405 @@
|
||||
import { showToast } from "./utils.js";
|
||||
|
||||
const GITHUB_NEW_ISSUE_URL = "https://github.com/SoPat712/allstarr/issues/new";
|
||||
const MAX_PREFILL_URL_LENGTH = 6500;
|
||||
const ISSUE_TEMPLATES = {
|
||||
bug: {
|
||||
template: "bug-report.md",
|
||||
titlePrefix: "[BUG] ",
|
||||
openLabel: "Open Bug Report on GitHub",
|
||||
primaryLabel: "Describe the bug",
|
||||
primaryPlaceholder: "What happened? What looked wrong?",
|
||||
secondaryLabel: "To Reproduce",
|
||||
secondaryPlaceholder: "List the steps needed to reproduce the issue",
|
||||
tertiaryLabel: "Expected behavior",
|
||||
tertiaryPlaceholder: "What did you expect to happen instead?",
|
||||
contextLabel: "Additional context",
|
||||
contextPlaceholder:
|
||||
"Anything else that might help, including screenshots or surrounding context",
|
||||
},
|
||||
feature: {
|
||||
template: "feature-request.md",
|
||||
titlePrefix: "[FEATURE] ",
|
||||
openLabel: "Open Feature Request on GitHub",
|
||||
primaryLabel: "Problem to solve",
|
||||
primaryPlaceholder: "What problem are you trying to solve?",
|
||||
secondaryLabel: "Solution you'd like",
|
||||
secondaryPlaceholder: "What should Allstarr do instead?",
|
||||
tertiaryLabel: "Alternatives considered",
|
||||
tertiaryPlaceholder: "What alternatives or workarounds have you considered?",
|
||||
contextLabel: "Additional context",
|
||||
contextPlaceholder:
|
||||
"Extra examples, mockups, or screenshots that explain the request",
|
||||
},
|
||||
};
|
||||
const DIAGNOSTIC_SOURCE_IDS = [
|
||||
"sidebar-version",
|
||||
"backend-type",
|
||||
"spotify-status",
|
||||
"jellyfin-url",
|
||||
];
|
||||
|
||||
function getElement(id) {
|
||||
return document.getElementById(id);
|
||||
}
|
||||
|
||||
function normalizeText(value, fallback = "Unavailable") {
|
||||
const normalized = String(value ?? "").trim();
|
||||
if (!normalized || normalized === "-" || /^loading/i.test(normalized)) {
|
||||
return fallback;
|
||||
}
|
||||
|
||||
return normalized;
|
||||
}
|
||||
|
||||
function getIssueType() {
|
||||
return getElement("issue-report-type")?.value === "feature" ? "feature" : "bug";
|
||||
}
|
||||
|
||||
function getIssueConfig(type = getIssueType()) {
|
||||
return ISSUE_TEMPLATES[type] || ISSUE_TEMPLATES.bug;
|
||||
}
|
||||
|
||||
function sanitizeTitle(title, type) {
|
||||
const prefix = getIssueConfig(type).titlePrefix;
|
||||
const trimmed = String(title ?? "").trim();
|
||||
if (!trimmed) {
|
||||
return prefix + (type === "feature" ? "Please add a short request title" : "Please add a short bug title");
|
||||
}
|
||||
|
||||
if (trimmed.toUpperCase().startsWith(prefix.trim())) {
|
||||
return trimmed;
|
||||
}
|
||||
|
||||
return prefix + trimmed;
|
||||
}
|
||||
|
||||
function formatCurrentTab() {
|
||||
const activeTab =
|
||||
document.querySelector(".sidebar-link.active") ||
|
||||
document.querySelector(".tab.active");
|
||||
const text = activeTab?.textContent?.trim();
|
||||
return normalizeText(text, "Unknown");
|
||||
}
|
||||
|
||||
function getRedactedUrlState() {
|
||||
const jellyfinUrl = normalizeText(getElement("jellyfin-url")?.textContent, "");
|
||||
return jellyfinUrl ? "Configured (redacted)" : "Not configured";
|
||||
}
|
||||
|
||||
function getDiagnostics() {
|
||||
const timezone =
|
||||
Intl.DateTimeFormat().resolvedOptions().timeZone || "Unavailable";
|
||||
|
||||
return {
|
||||
version: normalizeText(getElement("sidebar-version")?.textContent),
|
||||
backendType: normalizeText(
|
||||
getElement("backend-type")?.textContent ||
|
||||
getElement("config-backend-type")?.textContent,
|
||||
),
|
||||
spotifyStatus: normalizeText(getElement("spotify-status")?.textContent),
|
||||
jellyfinUrl: getRedactedUrlState(),
|
||||
currentTab: formatCurrentTab(),
|
||||
browser: normalizeText(window.navigator?.userAgent),
|
||||
generatedAt: new Date().toISOString(),
|
||||
timezone,
|
||||
};
|
||||
}
|
||||
|
||||
function getReportState() {
|
||||
const type = getIssueType();
|
||||
return {
|
||||
type,
|
||||
titleInput: String(getElement("issue-report-title")?.value ?? "").trim(),
|
||||
primary: String(getElement("issue-report-primary")?.value ?? "").trim(),
|
||||
secondary: String(getElement("issue-report-secondary")?.value ?? "").trim(),
|
||||
tertiary: String(getElement("issue-report-tertiary")?.value ?? "").trim(),
|
||||
context: String(getElement("issue-report-context")?.value ?? "").trim(),
|
||||
};
|
||||
}
|
||||
|
||||
function renderIssueBody(state, includeDiagnostics = true) {
|
||||
const diagnostics = getDiagnostics();
|
||||
const diagnosticsMarkdown = [
|
||||
"<details>",
|
||||
"<summary>Safe diagnostics from Allstarr</summary>",
|
||||
"",
|
||||
"- Sensitive values stay redacted in this block.",
|
||||
`- Allstarr Version: ${diagnostics.version}`,
|
||||
`- Backend Type: ${diagnostics.backendType}`,
|
||||
`- Spotify Status: ${diagnostics.spotifyStatus}`,
|
||||
`- Jellyfin URL: ${diagnostics.jellyfinUrl}`,
|
||||
`- Current Tab: ${diagnostics.currentTab}`,
|
||||
`- Browser: ${diagnostics.browser}`,
|
||||
`- Generated At (UTC): ${diagnostics.generatedAt}`,
|
||||
`- Browser Time Zone: ${diagnostics.timezone}`,
|
||||
"",
|
||||
"</details>",
|
||||
].join("\n");
|
||||
|
||||
if (state.type === "feature") {
|
||||
const sections = [
|
||||
[
|
||||
"**Is your feature request related to a problem? Please describe.**",
|
||||
state.primary || "_Please describe the problem you want to solve._",
|
||||
],
|
||||
[
|
||||
"**Describe the solution you'd like**",
|
||||
state.secondary || "_Please describe the solution you want._",
|
||||
],
|
||||
[
|
||||
"**Describe alternatives you've considered**",
|
||||
state.tertiary || "_Please describe alternatives or workarounds you've considered._",
|
||||
],
|
||||
[
|
||||
"**Additional context**",
|
||||
state.context || "_Add any other context, screenshots, or examples here._",
|
||||
],
|
||||
];
|
||||
|
||||
if (includeDiagnostics) {
|
||||
sections.push(["**Environment**", diagnosticsMarkdown]);
|
||||
}
|
||||
|
||||
return sections.map(([heading, content]) => `${heading}\n${content}`).join("\n\n");
|
||||
}
|
||||
|
||||
const sections = [
|
||||
[
|
||||
"**Describe the bug**",
|
||||
state.primary || "_Please describe the bug._",
|
||||
],
|
||||
[
|
||||
"**To Reproduce**",
|
||||
state.secondary ||
|
||||
"_Please list the steps needed to reproduce the issue._",
|
||||
],
|
||||
[
|
||||
"**Expected behavior**",
|
||||
state.tertiary || "_Please describe what you expected to happen._",
|
||||
],
|
||||
[
|
||||
"**Additional context**",
|
||||
state.context || "_Add any other context, screenshots, or examples here._",
|
||||
],
|
||||
];
|
||||
|
||||
if (includeDiagnostics) {
|
||||
sections.push(["**Details**", diagnosticsMarkdown]);
|
||||
}
|
||||
|
||||
return sections.map(([heading, content]) => `${heading}\n${content}`).join("\n\n");
|
||||
}
|
||||
|
||||
function buildIssuePayload() {
|
||||
const state = getReportState();
|
||||
const config = getIssueConfig(state.type);
|
||||
const title = sanitizeTitle(state.titleInput, state.type);
|
||||
const fullBody = renderIssueBody(state, true);
|
||||
|
||||
const fullUrl = new URL(GITHUB_NEW_ISSUE_URL);
|
||||
fullUrl.searchParams.set("template", config.template);
|
||||
fullUrl.searchParams.set("title", title);
|
||||
fullUrl.searchParams.set("body", fullBody);
|
||||
|
||||
if (fullUrl.toString().length <= MAX_PREFILL_URL_LENGTH) {
|
||||
return {
|
||||
title,
|
||||
fullBody,
|
||||
url: fullUrl.toString(),
|
||||
truncated: false,
|
||||
};
|
||||
}
|
||||
|
||||
const shortenedBody = [
|
||||
renderIssueBody(state, false),
|
||||
"> Full safe diagnostics were copied to your clipboard by Allstarr.",
|
||||
"> Paste them below if GitHub opens with a shorter draft.",
|
||||
].join("\n\n");
|
||||
|
||||
const shortenedUrl = new URL(GITHUB_NEW_ISSUE_URL);
|
||||
shortenedUrl.searchParams.set("template", config.template);
|
||||
shortenedUrl.searchParams.set("title", title);
|
||||
shortenedUrl.searchParams.set("body", shortenedBody);
|
||||
|
||||
return {
|
||||
title,
|
||||
fullBody,
|
||||
url: shortenedUrl.toString(),
|
||||
truncated: true,
|
||||
};
|
||||
}
|
||||
|
||||
async function copyTextToClipboard(text) {
|
||||
if (!text) {
|
||||
return false;
|
||||
}
|
||||
|
||||
if (navigator.clipboard?.writeText) {
|
||||
try {
|
||||
await navigator.clipboard.writeText(text);
|
||||
return true;
|
||||
} catch {
|
||||
// Fall back to a hidden textarea if direct clipboard access fails.
|
||||
}
|
||||
}
|
||||
|
||||
const helper = document.createElement("textarea");
|
||||
helper.value = text;
|
||||
helper.setAttribute("readonly", "");
|
||||
helper.style.position = "absolute";
|
||||
helper.style.left = "-9999px";
|
||||
document.body.appendChild(helper);
|
||||
helper.select();
|
||||
|
||||
let copied = false;
|
||||
try {
|
||||
copied = document.execCommand("copy");
|
||||
} catch {
|
||||
copied = false;
|
||||
}
|
||||
|
||||
document.body.removeChild(helper);
|
||||
return copied;
|
||||
}
|
||||
|
||||
async function copyIssueReport({ silent = false } = {}) {
|
||||
const payload = buildIssuePayload();
|
||||
const copied = await copyTextToClipboard(`${payload.title}\n\n${payload.fullBody}`);
|
||||
|
||||
if (!silent) {
|
||||
showToast(
|
||||
copied
|
||||
? "Issue draft copied to clipboard"
|
||||
: "Could not copy the report. You can still copy it from the preview.",
|
||||
copied ? "success" : "warning",
|
||||
4000,
|
||||
);
|
||||
}
|
||||
|
||||
return copied;
|
||||
}
|
||||
|
||||
function validateTitle() {
|
||||
const titleInput = getElement("issue-report-title");
|
||||
if (!titleInput?.value?.trim()) {
|
||||
titleInput?.focus();
|
||||
showToast("Add a short title before opening the GitHub draft.", "warning");
|
||||
return false;
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
async function openGithubIssueDraft() {
|
||||
if (!validateTitle()) {
|
||||
return;
|
||||
}
|
||||
|
||||
const copied = await copyIssueReport({ silent: true });
|
||||
const payload = buildIssuePayload();
|
||||
const openedWindow = window.open(payload.url, "_blank", "noopener,noreferrer");
|
||||
|
||||
if (!openedWindow) {
|
||||
window.location.href = payload.url;
|
||||
}
|
||||
|
||||
const message = payload.truncated
|
||||
? "Opened a shorter GitHub draft and copied the full report to your clipboard."
|
||||
: copied
|
||||
? "Opened the GitHub draft and copied the report to your clipboard."
|
||||
: "Opened the GitHub draft. If anything is missing, use Copy Report.";
|
||||
showToast(message, payload.truncated ? "warning" : "success", 5000);
|
||||
}
|
||||
|
||||
function updateIssueReporterCopy() {
|
||||
const type = getIssueType();
|
||||
const config = getIssueConfig(type);
|
||||
|
||||
getElement("issue-report-primary-label").textContent = config.primaryLabel;
|
||||
getElement("issue-report-primary").placeholder = config.primaryPlaceholder;
|
||||
getElement("issue-report-secondary-label").textContent = config.secondaryLabel;
|
||||
getElement("issue-report-secondary").placeholder = config.secondaryPlaceholder;
|
||||
getElement("issue-report-tertiary-label").textContent = config.tertiaryLabel;
|
||||
getElement("issue-report-tertiary").placeholder = config.tertiaryPlaceholder;
|
||||
getElement("issue-report-context-label").textContent = config.contextLabel;
|
||||
getElement("issue-report-context").placeholder = config.contextPlaceholder;
|
||||
getElement("open-github-issue-btn").textContent = config.openLabel;
|
||||
getElement("issue-report-title").placeholder =
|
||||
type === "feature"
|
||||
? "Short summary of the feature request"
|
||||
: "Short summary of the issue";
|
||||
}
|
||||
|
||||
export function refreshIssueReportPreview() {
|
||||
const preview = getElement("issue-report-preview");
|
||||
const previewHelp = getElement("issue-report-preview-help");
|
||||
if (!preview || !previewHelp) {
|
||||
return;
|
||||
}
|
||||
|
||||
updateIssueReporterCopy();
|
||||
|
||||
const payload = buildIssuePayload();
|
||||
preview.value = `${payload.title}\n\n${payload.fullBody}`;
|
||||
previewHelp.textContent = payload.truncated
|
||||
? "This report is long enough that Allstarr will open GitHub with a shorter draft and copy the full report to your clipboard."
|
||||
: "This draft fits in a normal GitHub issue URL. Allstarr will still copy the full report to your clipboard when you open it.";
|
||||
}
|
||||
|
||||
export function initIssueReporter() {
|
||||
const typeSelect = getElement("issue-report-type");
|
||||
const titleInput = getElement("issue-report-title");
|
||||
const primaryInput = getElement("issue-report-primary");
|
||||
const secondaryInput = getElement("issue-report-secondary");
|
||||
const tertiaryInput = getElement("issue-report-tertiary");
|
||||
const contextInput = getElement("issue-report-context");
|
||||
const copyButton = getElement("copy-issue-report-btn");
|
||||
const openButton = getElement("open-github-issue-btn");
|
||||
|
||||
if (
|
||||
!typeSelect ||
|
||||
!titleInput ||
|
||||
!primaryInput ||
|
||||
!secondaryInput ||
|
||||
!tertiaryInput ||
|
||||
!contextInput ||
|
||||
!copyButton ||
|
||||
!openButton
|
||||
) {
|
||||
return;
|
||||
}
|
||||
|
||||
[typeSelect, titleInput, primaryInput, secondaryInput, tertiaryInput, contextInput].forEach(
|
||||
(input) => {
|
||||
input.addEventListener("input", refreshIssueReportPreview);
|
||||
input.addEventListener("change", refreshIssueReportPreview);
|
||||
},
|
||||
);
|
||||
|
||||
copyButton.addEventListener("click", () => {
|
||||
copyIssueReport();
|
||||
});
|
||||
openButton.addEventListener("click", () => {
|
||||
openGithubIssueDraft();
|
||||
});
|
||||
|
||||
const diagnosticsObserver = new MutationObserver(() => {
|
||||
refreshIssueReportPreview();
|
||||
});
|
||||
DIAGNOSTIC_SOURCE_IDS.forEach((id) => {
|
||||
const source = getElement(id);
|
||||
if (!source) {
|
||||
return;
|
||||
}
|
||||
|
||||
diagnosticsObserver.observe(source, {
|
||||
childList: true,
|
||||
subtree: true,
|
||||
characterData: true,
|
||||
});
|
||||
});
|
||||
|
||||
window.addEventListener("hashchange", refreshIssueReportPreview);
|
||||
refreshIssueReportPreview();
|
||||
}
|
||||
@@ -37,6 +37,7 @@ import { initAuthSession } from "./auth-session.js";
|
||||
import { initActionDispatcher } from "./action-dispatcher.js";
|
||||
import { initNavigationView } from "./views/navigation-view.js";
|
||||
import { initScrobblingView } from "./views/scrobbling-view.js";
|
||||
import { initIssueReporter } from "./issue-reporter.js";
|
||||
|
||||
let cookieDateInitialized = false;
|
||||
let restartRequired = false;
|
||||
@@ -137,6 +138,8 @@ initPlaylistAdmin({
|
||||
fetchJellyfinPlaylists: dashboard.fetchJellyfinPlaylists,
|
||||
});
|
||||
|
||||
initIssueReporter();
|
||||
|
||||
const authSession = initAuthSession({
|
||||
stopDashboardRefresh: dashboard.stopDashboardRefresh,
|
||||
loadDashboardData: dashboard.loadDashboardData,
|
||||
|
||||
@@ -780,7 +780,8 @@ button.danger:hover {
|
||||
}
|
||||
|
||||
input,
|
||||
select {
|
||||
select,
|
||||
textarea {
|
||||
background: var(--bg-tertiary);
|
||||
border: 1px solid var(--border);
|
||||
color: var(--text-primary);
|
||||
@@ -790,15 +791,24 @@ select {
|
||||
}
|
||||
|
||||
input:focus,
|
||||
select:focus {
|
||||
select:focus,
|
||||
textarea:focus {
|
||||
outline: none;
|
||||
border-color: var(--accent);
|
||||
}
|
||||
|
||||
input::placeholder {
|
||||
input::placeholder,
|
||||
textarea::placeholder {
|
||||
color: var(--text-secondary);
|
||||
}
|
||||
|
||||
textarea {
|
||||
width: 100%;
|
||||
resize: vertical;
|
||||
line-height: 1.5;
|
||||
font-family: inherit;
|
||||
}
|
||||
|
||||
.form-row {
|
||||
display: grid;
|
||||
grid-template-columns: 1fr 1fr 120px auto;
|
||||
@@ -1045,6 +1055,14 @@ input::placeholder {
|
||||
max-height: none;
|
||||
}
|
||||
|
||||
.report-issue-layout {
|
||||
grid-template-columns: 1fr;
|
||||
}
|
||||
|
||||
.report-preview-panel textarea {
|
||||
min-height: 360px;
|
||||
}
|
||||
|
||||
.support-badge {
|
||||
right: 12px;
|
||||
bottom: 12px;
|
||||
@@ -1067,6 +1085,43 @@ input::placeholder {
|
||||
display: block;
|
||||
}
|
||||
|
||||
.report-issue-layout {
|
||||
display: grid;
|
||||
grid-template-columns: minmax(0, 1fr) minmax(320px, 0.95fr);
|
||||
gap: 20px;
|
||||
align-items: start;
|
||||
}
|
||||
|
||||
.report-issue-panel,
|
||||
.report-preview-panel {
|
||||
display: grid;
|
||||
gap: 16px;
|
||||
}
|
||||
|
||||
.report-issue-panel .form-group,
|
||||
.report-preview-panel .form-group {
|
||||
margin-bottom: 0;
|
||||
}
|
||||
|
||||
.report-issue-panel label,
|
||||
.report-preview-panel label {
|
||||
display: block;
|
||||
margin-bottom: 6px;
|
||||
color: var(--text-secondary);
|
||||
}
|
||||
|
||||
.report-preview-panel textarea {
|
||||
min-height: 520px;
|
||||
font-family: ui-monospace, Menlo, Monaco, "Cascadia Mono", "Segoe UI Mono",
|
||||
monospace;
|
||||
font-size: 0.84rem;
|
||||
}
|
||||
|
||||
.report-preview-help {
|
||||
color: var(--text-secondary);
|
||||
font-size: 0.84rem;
|
||||
}
|
||||
|
||||
/* Utility classes to reduce inline styles in index.html */
|
||||
.hidden {
|
||||
display: none;
|
||||
|
||||
Reference in new issue
Block a user