mirror of
https://github.com/alexandrev/xslt-lab.git
synced 2026-09-21 04:43:16 +00:00
Add user feedback improvements: server error reporting, Clarity analytics, usage survey
- Auto-report link on 5xx/network errors: pre-fills GitHub issue with version, error, and stylesheet snippet - Microsoft Clarity: conditional on VITE_CLARITY_ID env var, loaded after page load - Usage survey: appears after 3 successful transforms, fires GA4 event, one-shot (stored in localStorage) - 27 tests passing (10 new tests covering buildBugReportUrl, server error detection, survey lifecycle)
This commit is contained in:
@@ -103,6 +103,17 @@
|
|||||||
}
|
}
|
||||||
</style>
|
</style>
|
||||||
<script type="module" src="/env.js"></script>
|
<script type="module" src="/env.js"></script>
|
||||||
|
<script>
|
||||||
|
window.addEventListener("load", function () {
|
||||||
|
var clarityId = window.env && window.env.VITE_CLARITY_ID;
|
||||||
|
if (!clarityId) return;
|
||||||
|
(function (c, l, a, r, i, t, y) {
|
||||||
|
c[a] = c[a] || function () { (c[a].q = c[a].q || []).push(arguments); };
|
||||||
|
t = l.createElement(r); t.async = 1; t.src = "https://www.clarity.ms/tag/" + i;
|
||||||
|
y = l.getElementsByTagName(r)[0]; y.parentNode.insertBefore(t, y);
|
||||||
|
})(window, document, "clarity", "script", clarityId);
|
||||||
|
}, { once: true });
|
||||||
|
</script>
|
||||||
<script>
|
<script>
|
||||||
const loadAdsense = () => {
|
const loadAdsense = () => {
|
||||||
const client = window.env && window.env.VITE_ADSENSE_CLIENT;
|
const client = window.env && window.env.VITE_ADSENSE_CLIENT;
|
||||||
|
|||||||
@@ -17,6 +17,7 @@ import {
|
|||||||
const MonacoEditor = lazy(() => import("@monaco-editor/react"));
|
const MonacoEditor = lazy(() => import("@monaco-editor/react"));
|
||||||
const FeedbackWidget = lazy(() => import("./components/FeedbackWidget"));
|
const FeedbackWidget = lazy(() => import("./components/FeedbackWidget"));
|
||||||
const BuyMeACoffee = lazy(() => import("./components/BuyMeACoffee"));
|
const BuyMeACoffee = lazy(() => import("./components/BuyMeACoffee"));
|
||||||
|
const UsageSurvey = lazy(() => import("./components/UsageSurvey"));
|
||||||
|
|
||||||
function runWhenIdle(callback, timeout = 2000) {
|
function runWhenIdle(callback, timeout = 2000) {
|
||||||
if (typeof window === "undefined") {
|
if (typeof window === "undefined") {
|
||||||
@@ -115,6 +116,7 @@ function defaultWorkspaceStatus() {
|
|||||||
duration: null,
|
duration: null,
|
||||||
error: "",
|
error: "",
|
||||||
errorLines: [],
|
errorLines: [],
|
||||||
|
isServerError: false,
|
||||||
traceEntries: [],
|
traceEntries: [],
|
||||||
traceText: "",
|
traceText: "",
|
||||||
showRawTrace: false,
|
showRawTrace: false,
|
||||||
@@ -122,6 +124,26 @@ function defaultWorkspaceStatus() {
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export function buildBugReportUrl(version, error, xslt) {
|
||||||
|
const trimmedXslt = xslt ? xslt.slice(0, 500) : "";
|
||||||
|
const body = [
|
||||||
|
"## Bug report",
|
||||||
|
"",
|
||||||
|
`**XSLT version:** ${version || "default"}`,
|
||||||
|
`**Error:** ${error || ""}`,
|
||||||
|
"",
|
||||||
|
"**Stylesheet (first 500 chars):**",
|
||||||
|
"```xml",
|
||||||
|
trimmedXslt,
|
||||||
|
"```",
|
||||||
|
].join("\n");
|
||||||
|
const title = `Server error: ${(error || "").slice(0, 80)}`;
|
||||||
|
return (
|
||||||
|
"https://github.com/alexandrev/xslt-lab/issues/new" +
|
||||||
|
`?labels=bug&title=${encodeURIComponent(title)}&body=${encodeURIComponent(body)}`
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
function looksLikeHtml(text) {
|
function looksLikeHtml(text) {
|
||||||
if (!text || typeof text !== "string") return false;
|
if (!text || typeof text !== "string") return false;
|
||||||
const trimmed = text.trim();
|
const trimmed = text.trim();
|
||||||
@@ -261,6 +283,10 @@ export default function App() {
|
|||||||
const [paramsCollapsed, setParamsCollapsed] = useState(false);
|
const [paramsCollapsed, setParamsCollapsed] = useState(false);
|
||||||
const [errorCollapsed, setErrorCollapsed] = useState(false);
|
const [errorCollapsed, setErrorCollapsed] = useState(false);
|
||||||
const [ethicalAdsReady, setEthicalAdsReady] = useState(false);
|
const [ethicalAdsReady, setEthicalAdsReady] = useState(false);
|
||||||
|
const [transformCount, setTransformCount] = useState(0);
|
||||||
|
const [surveyDone, setSurveyDone] = useState(() => {
|
||||||
|
try { return localStorage.getItem("xsp_survey_done") === "1"; } catch { return false; }
|
||||||
|
});
|
||||||
const workspaceImportRef = useRef(null);
|
const workspaceImportRef = useRef(null);
|
||||||
const resultResizeState = useRef({ startY: 0, startHeight: MIN_RESULT_HEIGHT });
|
const resultResizeState = useRef({ startY: 0, startHeight: MIN_RESULT_HEIGHT });
|
||||||
const paramResizeState = useRef({ startX: 0, startWidth: DEFAULT_PARAM_WIDTH });
|
const paramResizeState = useRef({ startX: 0, startWidth: DEFAULT_PARAM_WIDTH });
|
||||||
@@ -466,6 +492,7 @@ export default function App() {
|
|||||||
duration,
|
duration,
|
||||||
error,
|
error,
|
||||||
errorLines,
|
errorLines,
|
||||||
|
isServerError,
|
||||||
traceEntries,
|
traceEntries,
|
||||||
traceText,
|
traceText,
|
||||||
showRawTrace,
|
showRawTrace,
|
||||||
@@ -979,6 +1006,7 @@ export default function App() {
|
|||||||
updateWorkspaceStatus(tabId, {
|
updateWorkspaceStatus(tabId, {
|
||||||
error: txt || res.statusText,
|
error: txt || res.statusText,
|
||||||
errorLines: lines,
|
errorLines: lines,
|
||||||
|
isServerError: res.status >= 500,
|
||||||
duration: null,
|
duration: null,
|
||||||
result: "",
|
result: "",
|
||||||
traceEntries: [],
|
traceEntries: [],
|
||||||
@@ -990,11 +1018,13 @@ export default function App() {
|
|||||||
}
|
}
|
||||||
const data = await res.json();
|
const data = await res.json();
|
||||||
const defaultView = looksLikeHtml(data.result) ? "render" : "source";
|
const defaultView = looksLikeHtml(data.result) ? "render" : "source";
|
||||||
|
setTransformCount((prev) => prev + 1);
|
||||||
updateWorkspaceStatus(tabId, {
|
updateWorkspaceStatus(tabId, {
|
||||||
result: data.result,
|
result: data.result,
|
||||||
duration: data.duration_ms,
|
duration: data.duration_ms,
|
||||||
error: "",
|
error: "",
|
||||||
errorLines: [],
|
errorLines: [],
|
||||||
|
isServerError: false,
|
||||||
showRawTrace: false,
|
showRawTrace: false,
|
||||||
resultView: defaultView,
|
resultView: defaultView,
|
||||||
});
|
});
|
||||||
@@ -1012,6 +1042,7 @@ export default function App() {
|
|||||||
updateWorkspaceStatus(tabId, {
|
updateWorkspaceStatus(tabId, {
|
||||||
error: txt,
|
error: txt,
|
||||||
errorLines: parseErrorLines(txt),
|
errorLines: parseErrorLines(txt),
|
||||||
|
isServerError: true,
|
||||||
result: "",
|
result: "",
|
||||||
duration: null,
|
duration: null,
|
||||||
traceEntries: [],
|
traceEntries: [],
|
||||||
@@ -1784,6 +1815,16 @@ export default function App() {
|
|||||||
+{(errorLines || []).length - MAX_ERROR_LINES} more…
|
+{(errorLines || []).length - MAX_ERROR_LINES} more…
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
{isServerError && (
|
||||||
|
<a
|
||||||
|
className="error-report-link"
|
||||||
|
href={buildBugReportUrl(activeTab?.version, error, activeTab?.xslt)}
|
||||||
|
target="_blank"
|
||||||
|
rel="noopener noreferrer"
|
||||||
|
>
|
||||||
|
Report this bug on GitHub
|
||||||
|
</a>
|
||||||
|
)}
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
{error && errorCollapsed && (
|
{error && errorCollapsed && (
|
||||||
@@ -1976,6 +2017,14 @@ export default function App() {
|
|||||||
<Suspense fallback={null}>
|
<Suspense fallback={null}>
|
||||||
<BuyMeACoffee />
|
<BuyMeACoffee />
|
||||||
<FeedbackWidget />
|
<FeedbackWidget />
|
||||||
|
{!surveyDone && transformCount >= 3 && (
|
||||||
|
<UsageSurvey
|
||||||
|
onDismiss={() => {
|
||||||
|
setSurveyDone(true);
|
||||||
|
try { localStorage.setItem("xsp_survey_done", "1"); } catch {}
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
</Suspense>
|
</Suspense>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
+106
-2
@@ -1,6 +1,6 @@
|
|||||||
import { render, screen, waitFor, cleanup, fireEvent } from "@testing-library/react";
|
import { render, screen, waitFor, cleanup, fireEvent, act } from "@testing-library/react";
|
||||||
import { afterEach, beforeEach, describe, expect, it, vi } from "vitest";
|
import { afterEach, beforeEach, describe, expect, it, vi } from "vitest";
|
||||||
import App from "./App";
|
import App, { buildBugReportUrl } from "./App";
|
||||||
|
|
||||||
vi.mock("@monaco-editor/react", () => ({
|
vi.mock("@monaco-editor/react", () => ({
|
||||||
default: () => <div data-testid="monaco-editor" />,
|
default: () => <div data-testid="monaco-editor" />,
|
||||||
@@ -53,6 +53,110 @@ describe("App bootstrap", () => {
|
|||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
describe("buildBugReportUrl", () => {
|
||||||
|
it("includes version and error in the URL", () => {
|
||||||
|
const url = buildBugReportUrl("2.0", "Connection refused", "<xsl:stylesheet/>");
|
||||||
|
expect(url).toContain("labels=bug");
|
||||||
|
expect(url).toContain(encodeURIComponent("2.0"));
|
||||||
|
expect(url).toContain(encodeURIComponent("Connection refused"));
|
||||||
|
});
|
||||||
|
|
||||||
|
it("truncates xslt to 500 chars in the body", () => {
|
||||||
|
const longXslt = "x".repeat(600);
|
||||||
|
const url = buildBugReportUrl("1.0", "err", longXslt);
|
||||||
|
const body = decodeURIComponent(url.split("body=")[1]);
|
||||||
|
expect(body).toContain("x".repeat(500));
|
||||||
|
expect(body).not.toContain("x".repeat(501));
|
||||||
|
});
|
||||||
|
|
||||||
|
it("handles missing arguments gracefully", () => {
|
||||||
|
const url = buildBugReportUrl(undefined, undefined, undefined);
|
||||||
|
expect(url).toContain("github.com/alexandrev/xslt-lab/issues/new");
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe("server error reporting", () => {
|
||||||
|
it("shows report link on 5xx response", async () => {
|
||||||
|
vi.stubGlobal(
|
||||||
|
"fetch",
|
||||||
|
vi.fn(() =>
|
||||||
|
Promise.resolve({
|
||||||
|
ok: false,
|
||||||
|
status: 500,
|
||||||
|
statusText: "Internal Server Error",
|
||||||
|
json: async () => ({ error: "Saxon died" }),
|
||||||
|
}),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
render(<App />);
|
||||||
|
fireEvent.pointerDown(window);
|
||||||
|
await waitFor(() =>
|
||||||
|
expect(screen.getByText(/report this bug/i)).toBeInTheDocument(),
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("does not show report link on 400 XSLT syntax error", async () => {
|
||||||
|
vi.stubGlobal(
|
||||||
|
"fetch",
|
||||||
|
vi.fn(() =>
|
||||||
|
Promise.resolve({
|
||||||
|
ok: false,
|
||||||
|
status: 400,
|
||||||
|
statusText: "Bad Request",
|
||||||
|
json: async () => ({ error: "XSLT syntax error at line 3" }),
|
||||||
|
}),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
render(<App />);
|
||||||
|
fireEvent.pointerDown(window);
|
||||||
|
await waitFor(() =>
|
||||||
|
expect(screen.getByText(/xslt syntax error/i)).toBeInTheDocument(),
|
||||||
|
);
|
||||||
|
expect(screen.queryByText(/report this bug/i)).not.toBeInTheDocument();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe("usage survey", () => {
|
||||||
|
it("does not show survey before 3 successful transforms", async () => {
|
||||||
|
render(<App />);
|
||||||
|
fireEvent.pointerDown(window);
|
||||||
|
await waitFor(() => expect(fetch).toHaveBeenCalled());
|
||||||
|
expect(screen.queryByText(/what are you using this for/i)).not.toBeInTheDocument();
|
||||||
|
});
|
||||||
|
|
||||||
|
it("shows survey after 3 successful transforms", async () => {
|
||||||
|
render(<App />);
|
||||||
|
fireEvent.pointerDown(window);
|
||||||
|
// 1st transform on mount
|
||||||
|
await waitFor(() => expect(fetch).toHaveBeenCalledTimes(1), { timeout: 2000 });
|
||||||
|
// Toggle trace twice to trigger 2 more transforms
|
||||||
|
const traceCheckbox = screen.getByRole("checkbox", { name: /enable internal variables/i });
|
||||||
|
await act(async () => { fireEvent.click(traceCheckbox); });
|
||||||
|
await waitFor(() => expect(fetch).toHaveBeenCalledTimes(2), { timeout: 2000 });
|
||||||
|
await act(async () => { fireEvent.click(traceCheckbox); });
|
||||||
|
await waitFor(() => expect(fetch).toHaveBeenCalledTimes(3), { timeout: 2000 });
|
||||||
|
await waitFor(() =>
|
||||||
|
expect(screen.getByText(/what are you using this for/i)).toBeInTheDocument(),
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("dismisses survey when ✕ is clicked", async () => {
|
||||||
|
render(<App />);
|
||||||
|
fireEvent.pointerDown(window);
|
||||||
|
await waitFor(() => expect(fetch).toHaveBeenCalledTimes(1), { timeout: 2000 });
|
||||||
|
const traceCheckbox = screen.getByRole("checkbox", { name: /enable internal variables/i });
|
||||||
|
await act(async () => { fireEvent.click(traceCheckbox); });
|
||||||
|
await waitFor(() => expect(fetch).toHaveBeenCalledTimes(2), { timeout: 2000 });
|
||||||
|
await act(async () => { fireEvent.click(traceCheckbox); });
|
||||||
|
await waitFor(() => expect(fetch).toHaveBeenCalledTimes(3), { timeout: 2000 });
|
||||||
|
await waitFor(() =>
|
||||||
|
expect(screen.getByText(/what are you using this for/i)).toBeInTheDocument(),
|
||||||
|
);
|
||||||
|
fireEvent.click(screen.getByRole("button", { name: /dismiss survey/i }));
|
||||||
|
expect(screen.queryByText(/what are you using this for/i)).not.toBeInTheDocument();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
describe("version selector", () => {
|
describe("version selector", () => {
|
||||||
it("renders XSLT 1.0, 2.0 and 3.0 options", async () => {
|
it("renders XSLT 1.0, 2.0 and 3.0 options", async () => {
|
||||||
render(<App />);
|
render(<App />);
|
||||||
|
|||||||
@@ -0,0 +1,41 @@
|
|||||||
|
const SURVEY_OPTIONS = [
|
||||||
|
{ id: "learning", label: "Learning XSLT" },
|
||||||
|
{ id: "work", label: "Work / integration" },
|
||||||
|
{ id: "debugging", label: "Debugging" },
|
||||||
|
{ id: "other", label: "Other" },
|
||||||
|
];
|
||||||
|
|
||||||
|
export default function UsageSurvey({ onDismiss }) {
|
||||||
|
const handleSelect = (id) => {
|
||||||
|
if (typeof window.gtag === "function") {
|
||||||
|
window.gtag("event", "usage_survey", { use_case: id });
|
||||||
|
}
|
||||||
|
onDismiss();
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="usage-survey" role="complementary" aria-label="Quick survey">
|
||||||
|
<span className="usage-survey-q">What are you using this for?</span>
|
||||||
|
<div className="usage-survey-options">
|
||||||
|
{SURVEY_OPTIONS.map((opt) => (
|
||||||
|
<button
|
||||||
|
key={opt.id}
|
||||||
|
type="button"
|
||||||
|
className="usage-survey-btn"
|
||||||
|
onClick={() => handleSelect(opt.id)}
|
||||||
|
>
|
||||||
|
{opt.label}
|
||||||
|
</button>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
className="usage-survey-dismiss"
|
||||||
|
onClick={onDismiss}
|
||||||
|
aria-label="Dismiss survey"
|
||||||
|
>
|
||||||
|
✕
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -0,0 +1,46 @@
|
|||||||
|
import { render, screen, fireEvent, cleanup } from "@testing-library/react";
|
||||||
|
import { afterEach, describe, expect, it, vi } from "vitest";
|
||||||
|
import UsageSurvey from "./UsageSurvey";
|
||||||
|
|
||||||
|
afterEach(cleanup);
|
||||||
|
|
||||||
|
describe("UsageSurvey", () => {
|
||||||
|
it("renders all four options", () => {
|
||||||
|
render(<UsageSurvey onDismiss={() => {}} />);
|
||||||
|
expect(screen.getByText("Learning XSLT")).toBeInTheDocument();
|
||||||
|
expect(screen.getByText("Work / integration")).toBeInTheDocument();
|
||||||
|
expect(screen.getByText("Debugging")).toBeInTheDocument();
|
||||||
|
expect(screen.getByText("Other")).toBeInTheDocument();
|
||||||
|
});
|
||||||
|
|
||||||
|
it("calls onDismiss when an option is clicked", () => {
|
||||||
|
const onDismiss = vi.fn();
|
||||||
|
render(<UsageSurvey onDismiss={onDismiss} />);
|
||||||
|
fireEvent.click(screen.getByRole("button", { name: "Learning XSLT" }));
|
||||||
|
expect(onDismiss).toHaveBeenCalledOnce();
|
||||||
|
});
|
||||||
|
|
||||||
|
it("calls onDismiss when the dismiss button is clicked", () => {
|
||||||
|
const onDismiss = vi.fn();
|
||||||
|
render(<UsageSurvey onDismiss={onDismiss} />);
|
||||||
|
fireEvent.click(screen.getByRole("button", { name: /dismiss survey/i }));
|
||||||
|
expect(onDismiss).toHaveBeenCalledOnce();
|
||||||
|
});
|
||||||
|
|
||||||
|
it("fires a gtag event with the selected use_case", () => {
|
||||||
|
const gtag = vi.fn();
|
||||||
|
window.gtag = gtag;
|
||||||
|
render(<UsageSurvey onDismiss={() => {}} />);
|
||||||
|
fireEvent.click(screen.getByRole("button", { name: "Work / integration" }));
|
||||||
|
expect(gtag).toHaveBeenCalledWith("event", "usage_survey", { use_case: "work" });
|
||||||
|
delete window.gtag;
|
||||||
|
});
|
||||||
|
|
||||||
|
it("does not throw when gtag is not defined", () => {
|
||||||
|
delete window.gtag;
|
||||||
|
render(<UsageSurvey onDismiss={() => {}} />);
|
||||||
|
expect(() =>
|
||||||
|
fireEvent.click(screen.getByRole("button", { name: "Debugging" })),
|
||||||
|
).not.toThrow();
|
||||||
|
});
|
||||||
|
});
|
||||||
@@ -1479,3 +1479,119 @@ a:focus-visible {
|
|||||||
:root[data-theme="dark"] .feedback-link:hover {
|
:root[data-theme="dark"] .feedback-link:hover {
|
||||||
background: #4a3916;
|
background: #4a3916;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* ── Error report link ── */
|
||||||
|
.error-report-link {
|
||||||
|
display: inline-block;
|
||||||
|
margin: 6px 8px 4px;
|
||||||
|
font-size: 0.78rem;
|
||||||
|
color: #c0392b;
|
||||||
|
text-decoration: underline;
|
||||||
|
cursor: pointer;
|
||||||
|
}
|
||||||
|
|
||||||
|
.error-report-link:hover {
|
||||||
|
color: #922b21;
|
||||||
|
}
|
||||||
|
|
||||||
|
:root[data-theme="dark"] .error-report-link {
|
||||||
|
color: #ff7675;
|
||||||
|
}
|
||||||
|
|
||||||
|
:root[data-theme="dark"] .error-report-link:hover {
|
||||||
|
color: #fab1a0;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* ── Usage survey banner ── */
|
||||||
|
.usage-survey {
|
||||||
|
position: fixed;
|
||||||
|
bottom: 24px;
|
||||||
|
left: 50%;
|
||||||
|
transform: translateX(-50%);
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 10px;
|
||||||
|
background: #fff;
|
||||||
|
border: 1px solid #cfdcf4;
|
||||||
|
border-radius: 12px;
|
||||||
|
padding: 10px 14px;
|
||||||
|
box-shadow: 0 4px 16px rgba(33, 66, 108, 0.14);
|
||||||
|
z-index: 100;
|
||||||
|
flex-wrap: wrap;
|
||||||
|
max-width: 90vw;
|
||||||
|
}
|
||||||
|
|
||||||
|
.usage-survey-q {
|
||||||
|
font-size: 0.85rem;
|
||||||
|
font-weight: 600;
|
||||||
|
color: #21426c;
|
||||||
|
white-space: nowrap;
|
||||||
|
}
|
||||||
|
|
||||||
|
.usage-survey-options {
|
||||||
|
display: flex;
|
||||||
|
gap: 6px;
|
||||||
|
flex-wrap: wrap;
|
||||||
|
}
|
||||||
|
|
||||||
|
.usage-survey-btn {
|
||||||
|
padding: 4px 10px;
|
||||||
|
border: 1px solid #cfdcf4;
|
||||||
|
border-radius: 20px;
|
||||||
|
background: #f0f5ff;
|
||||||
|
color: #21426c;
|
||||||
|
font-size: 0.8rem;
|
||||||
|
cursor: pointer;
|
||||||
|
white-space: nowrap;
|
||||||
|
}
|
||||||
|
|
||||||
|
.usage-survey-btn:hover {
|
||||||
|
background: #007acc;
|
||||||
|
color: #fff;
|
||||||
|
border-color: #007acc;
|
||||||
|
}
|
||||||
|
|
||||||
|
.usage-survey-dismiss {
|
||||||
|
margin-left: 4px;
|
||||||
|
padding: 2px 6px;
|
||||||
|
border: none;
|
||||||
|
background: transparent;
|
||||||
|
color: #536084;
|
||||||
|
font-size: 0.85rem;
|
||||||
|
cursor: pointer;
|
||||||
|
border-radius: 4px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.usage-survey-dismiss:hover {
|
||||||
|
background: #eef3ff;
|
||||||
|
}
|
||||||
|
|
||||||
|
:root[data-theme="dark"] .usage-survey {
|
||||||
|
background: #0f141b;
|
||||||
|
border-color: #2b3645;
|
||||||
|
box-shadow: 0 4px 16px rgba(0, 0, 0, 0.4);
|
||||||
|
}
|
||||||
|
|
||||||
|
:root[data-theme="dark"] .usage-survey-q {
|
||||||
|
color: #e3e8ef;
|
||||||
|
}
|
||||||
|
|
||||||
|
:root[data-theme="dark"] .usage-survey-btn {
|
||||||
|
background: #1a2332;
|
||||||
|
color: #b0bfcf;
|
||||||
|
border-color: #2b3645;
|
||||||
|
}
|
||||||
|
|
||||||
|
:root[data-theme="dark"] .usage-survey-btn:hover {
|
||||||
|
background: #6bb6ff;
|
||||||
|
color: #0f141b;
|
||||||
|
border-color: #6bb6ff;
|
||||||
|
}
|
||||||
|
|
||||||
|
:root[data-theme="dark"] .usage-survey-dismiss {
|
||||||
|
color: #7a8fa8;
|
||||||
|
}
|
||||||
|
|
||||||
|
:root[data-theme="dark"] .usage-survey-dismiss:hover {
|
||||||
|
background: #1a2332;
|
||||||
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user