Compare commits

..

No commits in common. "758b97e63e909779b8f8555845098dcfb23a31d8" and "642533f8007a37067d612c03d8f3871c9d52b68a" have entirely different histories.

10 changed files with 159 additions and 457 deletions

File diff suppressed because one or more lines are too long

View file

@ -46,7 +46,6 @@
"electron": "43.2.0",
"electron-builder": "26.8.1",
"lucide-solid": "^1.16.0",
"monaco-editor": "0.52.2",
"rolldown": "1.0.3",
"rollup-plugin-copy": "^3.5.0",
"solid-js": "^1.9.14",

8
pnpm-lock.yaml generated
View file

@ -84,9 +84,6 @@ importers:
lucide-solid:
specifier: ^1.16.0
version: 1.16.0(solid-js@1.9.14)
monaco-editor:
specifier: 0.52.2
version: 0.52.2
rolldown:
specifier: 1.0.3
version: 1.0.3
@ -2107,9 +2104,6 @@ packages:
mlly@1.8.2:
resolution: {integrity: sha512-d+ObxMQFmbt10sretNDytwt85VrbkhhUA/JBGm1MPaWJ65Cl4wOgLaB1NYvJSZ0Ef03MMEU/0xpPMXUIQ29UfA==}
monaco-editor@0.52.2:
resolution: {integrity: sha512-GEQWEZmfkOGLdd3XK8ryrfWz3AIP8YymVXiPHEdewrUq7mh0qrKrfHLNCXcbB6sTnMLnOZ3ztSiKcciFUkIJwQ==}
ms@2.1.3:
resolution: {integrity: sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==}
@ -4851,8 +4845,6 @@ snapshots:
pkg-types: 1.3.1
ufo: 1.6.4
monaco-editor@0.52.2: {}
ms@2.1.3: {}
neo-async@2.6.2: {}

View file

@ -34,8 +34,6 @@ export default defineConfig([
{ src: "src/**/**/*.js", dest: "ts-out/js/" },
{ src: "package.json", dest: "ts-out/" },
{ src: "assets/**/**", dest: "ts-out/assets/" },
// Monaco AMD tree for the offline Quick CSS editor (next to editor.html)
{ src: "node_modules/monaco-editor/min/vs/**/*", dest: "ts-out/html/monaco/vs" },
],
}),
],

View file

@ -1,88 +0,0 @@
/** True when hostname is exactly `domain` or a subdomain of it (case-insensitive). */
export function hostnameMatches(hostname: string, domain: string): boolean {
const host = hostname.toLowerCase();
const target = domain.toLowerCase();
return host === target || host.endsWith(`.${target}`);
}
export function tryParseUrl(urlString: string): URL | null {
try {
return new URL(urlString);
} catch {
return null;
}
}
/** True when the frame is a YouTube embed (or Discord Activities YouTube proxy). */
export function isYouTubeEmbedOrProxyFrame(frameUrl: string): boolean {
const url = tryParseUrl(frameUrl);
if (!url) return false;
if (hostnameMatches(url.hostname, "youtube.com") && url.pathname.includes("/embed/")) {
return true;
}
// Discord Activities may proxy YouTube through *.discordsays.com (youtube.com appears in the path/query)
if (
hostnameMatches(url.hostname, "discordsays.com") &&
(url.pathname.includes("youtube.com") || url.search.includes("youtube.com"))
) {
return true;
}
return false;
}
/** Telemetry / noise hosts and Discord science endpoints we cancel in webRequest. */
export function isTelemetryBlockedUrl(requestUrl: string): boolean {
const url = tryParseUrl(requestUrl);
if (url?.protocol !== "https:") return false;
if (/^\/api\/v\d+\/science(?:\/|$)/.test(url.pathname)) return true;
if (hostnameMatches(url.hostname, "sentry.io")) return true;
if (hostnameMatches(url.hostname, "nel.cloudflare.com")) return true;
return false;
}
/**
* Blob downloads for Discord calendar (.ics) use `blob:https://discord.com/<uuid>`.
* Parse the blob payload URL so `discord.com.evil` / userinfo tricks cannot match.
*/
export function isDiscordIcsBlobUrl(urlString: string): boolean {
const blobUrl = tryParseUrl(urlString);
if (blobUrl?.protocol !== "blob:") return false;
const inner = tryParseUrl(blobUrl.pathname);
if (!inner || (inner.protocol !== "https:" && inner.protocol !== "http:")) return false;
return hostnameMatches(inner.hostname, "discord.com");
}
/** Discord stream popout windows (stable / canary / PTB). */
export function isDiscordPopoutUrl(urlString: string): boolean {
const url = tryParseUrl(urlString);
if (url?.protocol !== "https:" || url.pathname !== "/popout") return false;
const host = url.hostname.toLowerCase();
return host === "discord.com" || host === "canary.discord.com" || host === "ptb.discord.com";
}
const DEFAULT_ALLOWED_LOCALHOST_WS_PORTS = new Set([1211, 1112, 6888]);
/**
* Block stray localhost WebSocket probes, except known Legcord/local RPC ports.
* Uses URL parsing instead of substring checks on the raw request URL.
*/
export function isBlockedLocalhostWebSocket(
requestUrl: string,
allowedPorts: ReadonlySet<number> = DEFAULT_ALLOWED_LOCALHOST_WS_PORTS,
): boolean {
const url = tryParseUrl(requestUrl);
if (!url || (url.protocol !== "ws:" && url.protocol !== "wss:")) return false;
if (url.hostname !== "127.0.0.1" && url.hostname !== "localhost") return false;
const port = url.port ? Number(url.port) : url.protocol === "wss:" ? 443 : 80;
if (!Number.isFinite(port)) return true;
return !allowedPorts.has(port);
}

View file

@ -6,23 +6,20 @@
<link rel="stylesheet" href="legcord://assets/css/editor.css">
</head>
<body>
<script type="text/javascript" src="./monaco/vs/loader.js"></script>
<script type="text/javascript" src="legcord://assets/js/monacoLoader.js"></script>
<div id="editorCode"></div>
<script>
const cssCode = cssEditor.get;
const monacoRoot = new URL("./monaco/", window.location.href).href;
const vs = new URL("./monaco/vs", window.location.href).href;
require.config({
paths: { vs }
paths: { vs: "https://cdn.jsdelivr.net/npm/monaco-editor/min/vs" }
});
window.MonacoEnvironment = {
getWorkerUrl: () =>
`data:text/javascript;charset=utf-8,${encodeURIComponent(`
self.MonacoEnvironment = { baseUrl: ${JSON.stringify(monacoRoot)} };
importScripts(${JSON.stringify(`${vs}/base/worker/workerMain.js`)});
`)}`,
getWorkerUrl: (_workerId, _label) => `data:text/javascript;charset=utf-8,${encodeURIComponent(`
self.MonacoEnvironment = {
baseUrl: 'https://cdn.jsdelivr.net/npm/monaco-editor/min/'
};
importScripts('https://cdn.jsdelivr.net/npm/monaco-editor/min/vs/base/worker/workerMain.js');`)}`
};
// Monaco init
@ -52,6 +49,8 @@
cssEditor.set(editor.getValue());
});
}
</script>
</body>
</html>
</html>

View file

@ -14,7 +14,7 @@ export function openCssEditor(file: string) {
preload: path.join(import.meta.dirname, "cssEditor", "preload.mjs"),
},
});
void cssWindow.loadFile(path.join(import.meta.dirname, "html", "editor.html"));
cssWindow.loadURL("legcord://html/editor.html");
ipcMain.on("editor-setCSS", (_event, css: string) => {
fs.writeFileSync(file, css);

View file

@ -19,13 +19,6 @@ import { navigateTo } from "../common/dom.js";
import { forceQuit, setForceQuit } from "../common/forceQuit.js";
import { handleCommands, passedValidArgument } from "../common/handleCommands.js";
import { getLang } from "../common/lang.js";
import {
isBlockedLocalhostWebSocket,
isDiscordIcsBlobUrl,
isDiscordPopoutUrl,
isTelemetryBlockedUrl,
isYouTubeEmbedOrProxyFrame,
} from "../common/sanitization.js";
import { injectThemesMain } from "../common/themes.js";
import {
DEFAULT_WINDOW_HEIGHT,
@ -206,7 +199,10 @@ function doAfterDefiningTheWindow(passedWindow: BrowserWindow): void {
return;
}
frame.once("dom-ready", async () => {
if (isYouTubeEmbedOrProxyFrame(frame.url)) {
if (
frame.url.includes("youtube.com/embed/") ||
(frame.url.includes("discordsays") && frame.url.includes("youtube.com"))
) {
await frame.executeJavaScript(readFileSync(path.join(__dirname, "assets/js/adguard.js"), "utf-8"));
}
});
@ -215,14 +211,18 @@ function doAfterDefiningTheWindow(passedWindow: BrowserWindow): void {
// Allow about:blank (used by Vencord & Equicord QuickCss popup)
if (url === "about:blank") return { action: "allow" };
// Saving ics files on future events
if (isDiscordIcsBlobUrl(url)) {
if (url.startsWith("blob:https://discord.com/")) {
return {
action: "allow",
overrideBrowserWindowOptions: { show: false },
};
}
// Allow Discord stream popout
if (isDiscordPopoutUrl(url))
if (
url === "https://discord.com/popout" ||
url === "https://canary.discord.com/popout" ||
url === "https://ptb.discord.com/popout"
)
return {
action: "allow",
overrideBrowserWindowOptions: {
@ -267,8 +267,21 @@ function doAfterDefiningTheWindow(passedWindow: BrowserWindow): void {
registerCustomHandler();
const blockedPatterns = [
/https:\/\/.*\/api\/v\d+\/science/,
/https:\/\/sentry\.io\/.*/,
/https:\/\/.*\.nel\.cloudflare\.com\/.*/,
];
passedWindow.webContents.session.webRequest.onBeforeRequest((details, callback) => {
if (isTelemetryBlockedUrl(details.url) || isBlockedLocalhostWebSocket(details.url)) {
if (blockedPatterns.some((pattern) => pattern.test(details.url))) {
return callback({ cancel: true });
}
if (
details.url.includes("ws://127.0.0.1:") &&
!details.url.includes("127.0.0.1:1211") &&
!details.url.includes("127.0.0.1:1112") &&
!details.url.includes("127.0.0.1:6888")
) {
return callback({ cancel: true });
}
return callback({});

View file

@ -41,60 +41,17 @@ export const AboutPopup = (props: { close: () => void }) => {
<ModalBody>
<div class={classes.aboutContainer}>
<div class={classes.aboutHeader}>
<div class={classes.aboutLogo}>
<svg
width="200"
height="90"
viewBox="0 0 2249 1020"
xmlns="http://www.w3.org/2000/svg"
aria-hidden="true"
>
<path
fill-rule="evenodd"
fill="#111725"
d="m102.2 705v-478.6h66.8v478.6zm375.5-196.1q-1.2-29-12.2-49-10.5-20.6-29.6-31.6-19.2-11.6-44.1-11.6-27.9 0-48.8 14.2-20.3 14.2-31.9 40-7.9 16.9-10.4 38zm-166 85.2q12.7 27 34.8 41.9 22.6 14.8 51.7 14.8 51.6 0 87-40.6l35.5 39.3q-22.7 27.8-55.2 43.2-32.5 14.9-72.6 14.9-47.6 0-83-22-35.4-21.9-55.1-60.6-19.8-39.3-19.8-90.9 0-51.6 19.8-90.3 19.7-39.4 55.1-61.3 35.4-21.9 81.3-22.6 54.6 0 87.7 25.2 33 24.5 47 69.6 13.9 45.2 9.8 107.1h-233.7q3.1 17.6 9.7 32.3zm565.9-231.6v308.3q0 49.7-19.7 85.8-19.8 36.1-55.8 56.1-36 19.4-84.7 19.4-36 0-67.9-11.6-32-12.3-60.4-33.6l28.4-50.9q22.1 18 45.3 28.3 23.2 9.7 51.7 9.7 29.6 0 51.1-12.2 22-12.9 33.6-35.5 12.2-22.6 12.2-53.6v-41.6q-13.3 23.7-34.2 38.4-27.9 19.4-67.9 19.4-41.8 0-73.2-20.7-31.3-21.3-48.8-58.7-17.4-37.4-17.4-86.4 0-48.4 16.9-84.5 17.4-36.8 48.2-57.4 31.3-21.3 71.9-21.3 40.7-0.6 69.1 19.4 21.6 14.3 35.4 38.3v-55.1zm-155 54.2q-26.1 0-46.4 14.2-20.3 13.5-31.9 38-11.1 24.5-11.6 56.1 0.5 31.6 11.6 56.2 11.6 24.5 31.3 38.7 20.3 13.5 47 13.5 26.2 0 45.9-13.5 20.3-14.2 31.3-38.7 11.6-24.6 11.6-56.2 0-32.2-11.6-56.1-11-24.5-31.3-38-19.7-14.2-45.9-14.2z"
/>
<path
fill-rule="evenodd"
fill="#9538e6"
d="m1218.3 412.8l-36.5 47.7q-16.3-18.7-38.4-29-22-10.3-49.9-10.3-26.7 0-47.6 14.2-20.9 14.2-32.5 40-11.6 25.1-11.6 58.7 0 33.5 11.6 59.3 11.6 25.2 32.5 40 20.9 14.2 47.6 14.2 29 0 52.3-10.3 23.2-11 38.9-31.6l37.1 40.6q-20.9 29-54.6 45.2-33 16.1-76.6 16.1-45.8 0-81.3-22-34.8-21.9-54.5-60.6-19.8-39.3-19.8-90.9 0-51.6 19.8-90.3 19.7-39.4 54.5-61.3 35.5-22.6 81.3-22.6 41.8 0 73.7 14.2 32.6 13.6 54 38.7zm186.9-52.9q47.6 0 83.6 21.9 36.6 22 56.9 61.3 20.3 38.7 20.3 90.3 0 51.6-20.3 91-20.3 39.3-56.9 61.2-36 22-83.6 22-47.6 0-84.2-22-36.6-21.9-56.9-61.2-20.3-39.4-20.3-91 0-51.6 20.3-90.3 20.3-39.3 56.9-61.3 36.6-21.9 84.2-21.9zm0 60q-27.9 0-49.3 14.8-21.5 14.2-33.7 40-11.6 25.8-11.6 59.4 0 34.2 11.6 60 12.2 25.8 33.7 40.6 21.4 14.2 49.3 14.2 27.9 0 48.8-14.2 21.4-14.8 33-40.6 12.2-25.8 12.2-60 0-33.6-12.2-59.4-11.6-25.8-33-40-20.9-14.8-48.8-14.8zm397.6-60v67.1q-33.1-1.9-57.5 12.9-23.8 14.2-37.1 40-10.1 20.3-12.2 45.4v179.7h-66.8v-342.5h66.8v67.7q14.6-32.2 39.4-49.6 28.5-20.7 67.4-20.7zm166.5-0.6q43 0 72.6 21.3 22.7 15.5 37.1 41.5v-195.7h66.8v478.6h-66.8v-58.5q-14.3 25.4-36.5 41.1-29.6 20.6-72 20.6-44.1 0-77.8-21.9-33.1-22.6-51.7-61.9-18.5-40-18.5-91.6 0-51.6 18.5-90.3 18.6-39.4 51.7-61.3 33.1-21.9 76.6-21.9zm15.1 60q-27.8 0-49.3 14.8-21.5 14.8-33.7 41.3-11.6 25.8-12.2 59.3 0.6 33.6 12.2 59.4 12.2 25.8 33.7 40.6 21.5 14.8 49.3 14.8 27.9 0 49.4-14.8 21.4-14.8 33.1-40.6 12.1-25.8 12.1-59.4 0-34.2-12.1-60-11.7-25.8-33.1-40.6-21.5-14.8-49.4-14.8z"
/>
<path
fill="#cdc6fd"
stroke="#111725"
stroke-miterlimit="10"
stroke-width="20"
d="m528.7 775.2l-52.3 27.2c-28.7 14.9-44.2 26.8-34.2 44.9 9 18.7 27.5 12.9 56.2-2l52.4-27.3"
/>
<path
fill="#cdc6fd"
stroke="#111725"
stroke-miterlimit="10"
stroke-width="20"
d="m598.2 910l-52.4 27.2c-28.6 14.9-44.1 26.8-34.1 44.9 9 18.6 27.5 12.8 56.2-2.1l52.4-27.2"
/>
<path
fill="#9538e6"
stroke="#111725"
stroke-miterlimit="10"
stroke-width="20"
d="m495.6 757.8c1.7-1 27.6-14.6 30.6-16.2 34.9-18.6 94.6-32.2 140.1-4.9 25.2 15.2 38.5 36.2 48.6 54.4 9.3 19.4 18.4 41.8 16.2 71.3-4 53.1-49.6 94.2-84.9 112.1-2.9 1.5-29 14.9-30.8 15.7z"
/>
</svg>
</div>
<span class={classes.aboutVersion}>
{window.legcord?.version === "0.0.0"
? "Dev Build"
: `v${window.legcord?.version ?? "1.3.0"}`}
</span>
<h2 class={classes.aboutTitle}>Legcord</h2>
<p class={classes.aboutDescription}>
A free and open-source Discord client crafted by the community, for the community
delivering advanced features, better performance, and a modern design.
Legcord is a free and open-source Discord client that offers a personalized experience with
advanced features, better performance, and a modern design. Developed by the community, for
the community.
</p>
</div>
{/* Quick Actions */}
<div class={classes.quickActions}>
<h3 class={classes.sectionTitle}>Quick Actions</h3>
<h3 class={classes.quickActionsTitle}>Quick Actions</h3>
<div class={classes.quickActionsList}>
<button
type="button"
@ -111,30 +68,13 @@ export const AboutPopup = (props: { close: () => void }) => {
stroke-width="2"
stroke-linecap="round"
stroke-linejoin="round"
aria-hidden="true"
>
<title>View Source Code</title>
<polyline points="16 18 22 12 16 6" />
<polyline points="8 6 2 12 8 18" />
</svg>
</span>
<span class={classes.quickActionButtonLabel}>
<span class={classes.quickActionButtonTitle}>View Source Code</span>
<span class={classes.quickActionButtonDesc}>github.com/Legcord/Legcord</span>
</span>
<svg
class={classes.quickActionButtonArrow}
width="16"
height="16"
viewBox="0 0 24 24"
fill="none"
stroke="currentColor"
stroke-width="2"
stroke-linecap="round"
stroke-linejoin="round"
aria-hidden="true"
>
<polyline points="9 18 15 12 9 6" />
</svg>
View Source Code
</button>
<button
type="button"
@ -151,30 +91,13 @@ export const AboutPopup = (props: { close: () => void }) => {
stroke-width="2"
stroke-linecap="round"
stroke-linejoin="round"
aria-hidden="true"
>
<title>Donate</title>
<line x1="12" y1="1" x2="12" y2="23" />
<path d="M17 5H9.5a3.5 3.5 0 0 0 0 7h5a3.5 3.5 0 0 1 0 7H6" />
</svg>
</span>
<span class={classes.quickActionButtonLabel}>
<span class={classes.quickActionButtonTitle}>Donate</span>
<span class={classes.quickActionButtonDesc}>Support the project</span>
</span>
<svg
class={classes.quickActionButtonArrow}
width="16"
height="16"
viewBox="0 0 24 24"
fill="none"
stroke="currentColor"
stroke-width="2"
stroke-linecap="round"
stroke-linejoin="round"
aria-hidden="true"
>
<polyline points="9 18 15 12 9 6" />
</svg>
Donate
</button>
<button
type="button"
@ -182,44 +105,19 @@ export const AboutPopup = (props: { close: () => void }) => {
onClick={() => window.open("https://discord.gg/JatCnhKufc", "_blank")}
>
<span class={classes.quickActionButtonIcon}>
<svg
width="20"
height="20"
viewBox="0 0 24 24"
fill="currentColor"
stroke="none"
aria-hidden="true"
>
<svg width="20" height="20" viewBox="0 0 24 24" fill="currentColor" stroke="none">
<title>Join Discord Server</title>
<path d="M20.317 4.37a19.791 19.791 0 0 0-4.885-1.515a.074.074 0 0 0-.079.037c-.21.375-.444.864-.608 1.25a18.27 18.27 0 0 0-5.487 0a12.64 12.64 0 0 0-.617-1.25a.077.077 0 0 0-.079-.037A19.736 19.736 0 0 0 3.677 4.37a.07.07 0 0 0-.032.027C.533 9.046-.32 13.58.099 18.057a.082.082 0 0 0 .031.057a19.9 19.9 0 0 0 5.993 3.03a.078.078 0 0 0 .084-.028a14.09 14.09 0 0 0 1.226-1.994a.076.076 0 0 0-.041-.106a13.107 13.107 0 0 1-1.872-.892a.077.077 0 0 1-.008-.128a10.2 10.2 0 0 0 .372-.292a.074.074 0 0 1 .077-.01c3.928 1.793 8.18 1.793 12.062 0a.074.074 0 0 1 .078.01c.12.098.246.198.373.292a.077.077 0 0 1-.006.127a12.299 12.299 0 0 1-1.873.892a.077.077 0 0 0-.041.107c.36.698.772 1.362 1.225 1.993a.076.076 0 0 0 .084.028a19.839 19.839 0 0 0 6.002-3.03a.077.077 0 0 0 .032-.054c.5-5.177-.838-9.674-3.549-13.66a.061.061 0 0 0-.031-.03zM8.02 15.33c-1.183 0-2.157-1.085-2.157-2.419c0-1.333.956-2.419 2.157-2.419c1.21 0 2.176 1.096 2.157 2.42c0 1.333-.956 2.418-2.157 2.418zm7.975 0c-1.183 0-2.157-1.085-2.157-2.419c0-1.333.955-2.419 2.157-2.419c1.21 0 2.176 1.096 2.157 2.42c0 1.333-.946 2.418-2.157 2.418z" />
</svg>
</span>
<span class={classes.quickActionButtonLabel}>
<span class={classes.quickActionButtonTitle}>Join Discord Server</span>
<span class={classes.quickActionButtonDesc}>Connect with the community</span>
</span>
<svg
class={classes.quickActionButtonArrow}
width="16"
height="16"
viewBox="0 0 24 24"
fill="none"
stroke="currentColor"
stroke-width="2"
stroke-linecap="round"
stroke-linejoin="round"
aria-hidden="true"
>
<polyline points="9 18 15 12 9 6" />
</svg>
Join Discord Server
</button>
</div>
</div>
{/* Contributors Section */}
<div class={classes.contributorsSection}>
<h3 class={classes.sectionTitle}>Contributors</h3>
{!loading() && !error() && (
<span class={classes.contributorBadge}>{contributors().length} contributors</span>
)}
<h3 class={classes.contributorsTitle}>Contributors</h3>
{loading() && <div class={classes.loadingState}>Loading contributors...</div>}
{error() && <div class={classes.errorState}>Failed to load contributors: {error()}</div>}
{!loading() && !error() && (
@ -249,22 +147,6 @@ export const AboutPopup = (props: { close: () => void }) => {
</div>
)}
</div>
<div class={classes.techSection}>
<h3 class={classes.sectionTitle}>Technologies</h3>
<div class={classes.techList}>
<span class={classes.techTag}>Electron</span>
<span class={classes.techTag}>SolidJS</span>
<span class={classes.techTag}>TypeScript</span>
<span class={classes.techTag}>Rolldown</span>
<span class={classes.techTag}>shelter</span>
<span class={classes.techTag}>Biome</span>
</div>
</div>
<div class={classes.aboutFooter}>
<span>Copyright &copy; Legcord 2024-2026 &middot; OSL-3.0</span>
</div>
</div>
</ModalBody>
</ModalRoot>

View file

@ -93,75 +93,75 @@
min-height: auto;
}
.aboutLogo {
display: flex;
justify-content: center;
margin-bottom: 20px;
}
.aboutLogo svg {
opacity: 0.95;
}
.aboutHeader {
text-align: center;
margin-bottom: 24px;
padding: 32px 24px 24px;
background: linear-gradient(180deg, var(--background-secondary-alt) 0%, var(--background-secondary) 100%);
margin-bottom: 32px;
padding: 24px;
background: var(--background-secondary);
border: 1px solid var(--background-modifier-accent);
border-radius: 12px;
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.08);
}
.aboutVersion {
font-size: 13px;
color: var(--text-muted);
margin: 0 0 16px 0;
padding: 6px 14px;
background: var(--background-tertiary);
border-radius: 20px;
display: inline-block;
font-family: var(--font-mono);
border: 1px solid var(--background-modifier-accent);
font-weight: 500;
letter-spacing: 0.3px;
.aboutTitle {
font-size: 32px;
font-weight: 700;
margin: 0 0 20px 0;
color: var(--header-primary);
letter-spacing: -0.5px;
}
.aboutDescription {
font-size: 14px;
line-height: 1.5;
font-size: 16px;
line-height: 1.6;
color: var(--text-muted);
margin: 0;
padding: 0 8px;
max-width: 520px;
margin: 0 0 24px 0;
max-width: 600px;
margin-left: auto;
margin-right: auto;
font-weight: 400;
}
.sectionTitle {
font-size: 16px;
font-weight: 600;
margin: 0 0 16px 0;
color: var(--header-primary);
letter-spacing: -0.2px;
text-transform: uppercase;
opacity: 0.7;
.aboutVersion {
font-size: 14px;
color: var(--text-muted);
margin: 0;
padding: 10px 18px;
background: var(--background-tertiary);
border-radius: 8px;
display: inline-block;
font-family: var(--font-mono);
border: 1px solid var(--background-modifier-accent);
font-weight: 500;
}
.quickActions {
margin-bottom: 24px;
background: var(--background-secondary);
border: 1px solid var(--background-modifier-accent);
border-radius: 12px;
padding: 20px;
margin: 0 0 32px 0;
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.08);
}
.quickActionsTitle {
font-size: 20px;
font-weight: 600;
margin: 0 0 20px 0;
color: var(--header-primary);
letter-spacing: -0.3px;
}
.quickActionsList {
display: flex;
flex-direction: column;
gap: 8px;
gap: 12px;
}
.quickActionButton {
background: var(--background-secondary);
background: var(--background-secondary-alt);
border: 1px solid var(--background-modifier-accent);
border-radius: 10px;
border-radius: 8px;
padding: 12px 16px;
font-size: 14px;
font-weight: 500;
@ -171,110 +171,73 @@
align-items: center;
text-align: left;
width: 100%;
transition: all 0.2s ease;
transition: all 0.15s ease;
box-shadow: 0 1px 3px rgba(0, 0, 0, 0.1);
position: relative;
overflow: hidden;
}
.quickActionButton::before {
content: "";
position: absolute;
inset: 0;
background: var(--brand-experiment);
opacity: 0;
transition: opacity 0.2s ease;
}
.quickActionButton:hover {
border-color: var(--brand-experiment);
transform: translateX(4px);
}
.quickActionButton:hover::before {
opacity: 0.04;
background: var(--background-tertiary);
border-color: var(--background-modifier-hover);
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.15);
transform: translateY(-1px);
}
.quickActionButton:active {
transform: translateX(2px);
transform: translateY(0);
box-shadow: 0 1px 3px rgba(0, 0, 0, 0.1);
}
.quickActionButtonIcon {
margin-right: 12px;
font-size: 18px;
color: var(--interactive-normal);
transition: color 0.15s ease;
display: flex;
align-items: center;
justify-content: center;
width: 36px;
height: 36px;
border-radius: 8px;
background: var(--background-tertiary);
color: var(--interactive-normal);
flex-shrink: 0;
transition: all 0.2s ease;
width: 20px;
height: 20px;
}
.quickActionButton:hover .quickActionButtonIcon {
color: var(--brand-experiment);
background: var(--background-secondary-alt);
}
.quickActionButtonLabel {
display: flex;
flex-direction: column;
gap: 2px;
flex: 1;
min-width: 0;
}
.quickActionButtonTitle {
font-size: 14px;
font-weight: 600;
color: var(--header-primary);
}
.quickActionButtonDesc {
font-size: 12px;
color: var(--text-muted);
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
}
.quickActionButtonArrow {
margin-left: 8px;
color: var(--text-muted);
flex-shrink: 0;
opacity: 0;
transition: all 0.2s ease;
}
.quickActionButton:hover .quickActionButtonArrow {
opacity: 1;
transform: translateX(2px);
color: var(--brand-experiment);
color: var(--interactive-hover);
}
.contributorsSection {
padding: 20px;
background: var(--background-secondary);
border: 1px solid var(--background-modifier-accent);
border: 1px solid var(--background-accent);
border-radius: 12px;
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.08);
}
.contributorsTitle {
font-size: 20px;
font-weight: 600;
margin: 0 0 24px 0;
color: var(--header-primary);
text-align: center;
letter-spacing: -0.3px;
}
.contributorsGrid {
display: grid;
grid-template-columns: repeat(auto-fill, minmax(160px, 1fr));
gap: 10px;
grid-template-columns: repeat(auto-fill, minmax(200px, 1fr));
gap: 16px;
padding: 0;
max-width: 100%;
width: 100%;
max-height: 280px;
justify-items: center;
max-height: 40vh;
overflow-y: auto;
scrollbar-width: thin;
scrollbar-color: var(--background-modifier-accent) transparent;
}
.contributorsGrid::-webkit-scrollbar {
width: 4px;
width: 6px;
}
.contributorsGrid::-webkit-scrollbar-track {
@ -283,7 +246,7 @@
.contributorsGrid::-webkit-scrollbar-thumb {
background: var(--background-modifier-accent);
border-radius: 2px;
border-radius: 3px;
}
.contributorsGrid::-webkit-scrollbar-thumb:hover {
@ -293,23 +256,27 @@
.contributorCard {
background: var(--background-primary);
border: 1px solid var(--background-modifier-accent);
border-radius: 10px;
padding: 14px 12px;
border-radius: 12px;
padding: 20px;
text-align: center;
transition: all 0.2s ease;
transition: all 0.15s ease;
min-width: 180px;
max-width: 100%;
box-sizing: border-box;
width: 100%;
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.08);
position: relative;
}
.contributorCard:hover {
background: var(--background-secondary-alt);
border-color: var(--brand-experiment);
background: var(--background-secondary);
border-color: var(--background-modifier-hover);
box-shadow: 0 4px 16px rgba(0, 0, 0, 0.12);
transform: translateY(-2px);
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.15);
}
.contributorLink {
@ -323,98 +290,46 @@
}
.contributorAvatar {
width: 48px;
height: 48px;
width: 64px;
height: 64px;
border-radius: 50%;
margin-bottom: 10px;
margin-bottom: 16px;
max-width: 100%;
border: 2px solid var(--background-modifier-accent);
border: 3px solid var(--background-modifier-accent);
object-fit: cover;
transition: all 0.2s ease;
transition: all 0.15s ease;
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1);
}
.contributorCard:hover .contributorAvatar {
border-color: var(--brand-experiment);
transform: scale(1.1);
transform: scale(1.05);
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.15);
}
.contributorName {
font-weight: 600;
font-size: 13px;
margin-bottom: 4px;
font-size: 16px;
margin-bottom: 8px;
color: var(--header-primary);
word-break: break-word;
overflow-wrap: break-word;
text-align: center;
transition: color 0.2s ease;
transition: color 0.15s ease;
letter-spacing: -0.2px;
}
.contributorCard:hover .contributorName {
color: var(--brand-experiment);
color: var(--interactive-hover);
}
.contributorInfo {
font-size: 11px;
font-size: 13px;
color: var(--text-muted);
margin: 0;
text-align: center;
font-weight: 400;
line-height: 1.3;
}
.contributorBadge {
display: inline-block;
font-size: 12px;
font-weight: 500;
color: var(--text-muted);
background: var(--background-tertiary);
border: 1px solid var(--background-modifier-accent);
border-radius: 20px;
padding: 3px 12px;
margin-bottom: 16px;
letter-spacing: 0.2px;
}
.techSection {
margin-top: 24px;
padding: 20px;
background: var(--background-secondary);
border: 1px solid var(--background-modifier-accent);
border-radius: 12px;
}
.techList {
display: flex;
flex-wrap: wrap;
gap: 8px;
}
.techTag {
display: inline-block;
font-size: 12px;
font-weight: 500;
color: var(--text-normal);
background: var(--background-primary);
border: 1px solid var(--background-modifier-accent);
border-radius: 6px;
padding: 4px 10px;
transition: all 0.15s ease;
}
.techTag:hover {
border-color: var(--brand-experiment);
color: var(--brand-experiment);
transform: translateY(-1px);
}
.aboutFooter {
margin-top: 24px;
text-align: center;
padding: 8px 0 4px;
font-size: 12px;
color: var(--text-muted);
opacity: 0.7;
letter-spacing: 0.1px;
line-height: 1.4;
}
.loadingState,
@ -433,25 +348,6 @@
border-radius: 8px;
margin: 16px;
}
.loadingState::after {
content: "";
display: block;
width: 24px;
height: 24px;
margin: 16px auto 0;
border: 3px solid var(--background-modifier-accent);
border-top-color: var(--brand-experiment);
border-radius: 50%;
animation: spin 0.8s linear infinite;
}
@keyframes spin {
to {
transform: rotate(360deg);
}
}
.modal {
max-height: 640px;
max-height: 500px;
}