mirror of
https://github.com/smartfrigde/armcord.git
synced 2026-08-19 06:03:22 +00:00
Compare commits
5 commits
642533f800
...
758b97e63e
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
758b97e63e | ||
|
|
0f7ac5449c | ||
|
|
bd3f8de6bc | ||
|
|
5d6cc64db7 | ||
|
|
d0dda79fbf |
10 changed files with 457 additions and 159 deletions
File diff suppressed because one or more lines are too long
|
|
@ -46,6 +46,7 @@
|
||||||
"electron": "43.2.0",
|
"electron": "43.2.0",
|
||||||
"electron-builder": "26.8.1",
|
"electron-builder": "26.8.1",
|
||||||
"lucide-solid": "^1.16.0",
|
"lucide-solid": "^1.16.0",
|
||||||
|
"monaco-editor": "0.52.2",
|
||||||
"rolldown": "1.0.3",
|
"rolldown": "1.0.3",
|
||||||
"rollup-plugin-copy": "^3.5.0",
|
"rollup-plugin-copy": "^3.5.0",
|
||||||
"solid-js": "^1.9.14",
|
"solid-js": "^1.9.14",
|
||||||
|
|
|
||||||
8
pnpm-lock.yaml
generated
8
pnpm-lock.yaml
generated
|
|
@ -84,6 +84,9 @@ importers:
|
||||||
lucide-solid:
|
lucide-solid:
|
||||||
specifier: ^1.16.0
|
specifier: ^1.16.0
|
||||||
version: 1.16.0(solid-js@1.9.14)
|
version: 1.16.0(solid-js@1.9.14)
|
||||||
|
monaco-editor:
|
||||||
|
specifier: 0.52.2
|
||||||
|
version: 0.52.2
|
||||||
rolldown:
|
rolldown:
|
||||||
specifier: 1.0.3
|
specifier: 1.0.3
|
||||||
version: 1.0.3
|
version: 1.0.3
|
||||||
|
|
@ -2104,6 +2107,9 @@ packages:
|
||||||
mlly@1.8.2:
|
mlly@1.8.2:
|
||||||
resolution: {integrity: sha512-d+ObxMQFmbt10sretNDytwt85VrbkhhUA/JBGm1MPaWJ65Cl4wOgLaB1NYvJSZ0Ef03MMEU/0xpPMXUIQ29UfA==}
|
resolution: {integrity: sha512-d+ObxMQFmbt10sretNDytwt85VrbkhhUA/JBGm1MPaWJ65Cl4wOgLaB1NYvJSZ0Ef03MMEU/0xpPMXUIQ29UfA==}
|
||||||
|
|
||||||
|
monaco-editor@0.52.2:
|
||||||
|
resolution: {integrity: sha512-GEQWEZmfkOGLdd3XK8ryrfWz3AIP8YymVXiPHEdewrUq7mh0qrKrfHLNCXcbB6sTnMLnOZ3ztSiKcciFUkIJwQ==}
|
||||||
|
|
||||||
ms@2.1.3:
|
ms@2.1.3:
|
||||||
resolution: {integrity: sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==}
|
resolution: {integrity: sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==}
|
||||||
|
|
||||||
|
|
@ -4845,6 +4851,8 @@ snapshots:
|
||||||
pkg-types: 1.3.1
|
pkg-types: 1.3.1
|
||||||
ufo: 1.6.4
|
ufo: 1.6.4
|
||||||
|
|
||||||
|
monaco-editor@0.52.2: {}
|
||||||
|
|
||||||
ms@2.1.3: {}
|
ms@2.1.3: {}
|
||||||
|
|
||||||
neo-async@2.6.2: {}
|
neo-async@2.6.2: {}
|
||||||
|
|
|
||||||
|
|
@ -34,6 +34,8 @@ export default defineConfig([
|
||||||
{ src: "src/**/**/*.js", dest: "ts-out/js/" },
|
{ src: "src/**/**/*.js", dest: "ts-out/js/" },
|
||||||
{ src: "package.json", dest: "ts-out/" },
|
{ src: "package.json", dest: "ts-out/" },
|
||||||
{ src: "assets/**/**", dest: "ts-out/assets/" },
|
{ 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" },
|
||||||
],
|
],
|
||||||
}),
|
}),
|
||||||
],
|
],
|
||||||
|
|
|
||||||
88
src/common/sanitization.ts
Normal file
88
src/common/sanitization.ts
Normal file
|
|
@ -0,0 +1,88 @@
|
||||||
|
/** 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);
|
||||||
|
}
|
||||||
|
|
@ -6,20 +6,23 @@
|
||||||
<link rel="stylesheet" href="legcord://assets/css/editor.css">
|
<link rel="stylesheet" href="legcord://assets/css/editor.css">
|
||||||
</head>
|
</head>
|
||||||
<body>
|
<body>
|
||||||
<script type="text/javascript" src="legcord://assets/js/monacoLoader.js"></script>
|
<script type="text/javascript" src="./monaco/vs/loader.js"></script>
|
||||||
<div id="editorCode"></div>
|
<div id="editorCode"></div>
|
||||||
<script>
|
<script>
|
||||||
const cssCode = cssEditor.get;
|
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({
|
require.config({
|
||||||
paths: { vs: "https://cdn.jsdelivr.net/npm/monaco-editor/min/vs" }
|
paths: { vs }
|
||||||
});
|
});
|
||||||
|
|
||||||
window.MonacoEnvironment = {
|
window.MonacoEnvironment = {
|
||||||
getWorkerUrl: (_workerId, _label) => `data:text/javascript;charset=utf-8,${encodeURIComponent(`
|
getWorkerUrl: () =>
|
||||||
self.MonacoEnvironment = {
|
`data:text/javascript;charset=utf-8,${encodeURIComponent(`
|
||||||
baseUrl: 'https://cdn.jsdelivr.net/npm/monaco-editor/min/'
|
self.MonacoEnvironment = { baseUrl: ${JSON.stringify(monacoRoot)} };
|
||||||
};
|
importScripts(${JSON.stringify(`${vs}/base/worker/workerMain.js`)});
|
||||||
importScripts('https://cdn.jsdelivr.net/npm/monaco-editor/min/vs/base/worker/workerMain.js');`)}`
|
`)}`,
|
||||||
};
|
};
|
||||||
|
|
||||||
// Monaco init
|
// Monaco init
|
||||||
|
|
@ -49,8 +52,6 @@
|
||||||
cssEditor.set(editor.getValue());
|
cssEditor.set(editor.getValue());
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
</script>
|
</script>
|
||||||
</body>
|
</body>
|
||||||
</html>
|
</html>
|
||||||
|
|
@ -14,7 +14,7 @@ export function openCssEditor(file: string) {
|
||||||
preload: path.join(import.meta.dirname, "cssEditor", "preload.mjs"),
|
preload: path.join(import.meta.dirname, "cssEditor", "preload.mjs"),
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
cssWindow.loadURL("legcord://html/editor.html");
|
void cssWindow.loadFile(path.join(import.meta.dirname, "html", "editor.html"));
|
||||||
|
|
||||||
ipcMain.on("editor-setCSS", (_event, css: string) => {
|
ipcMain.on("editor-setCSS", (_event, css: string) => {
|
||||||
fs.writeFileSync(file, css);
|
fs.writeFileSync(file, css);
|
||||||
|
|
|
||||||
|
|
@ -19,6 +19,13 @@ import { navigateTo } from "../common/dom.js";
|
||||||
import { forceQuit, setForceQuit } from "../common/forceQuit.js";
|
import { forceQuit, setForceQuit } from "../common/forceQuit.js";
|
||||||
import { handleCommands, passedValidArgument } from "../common/handleCommands.js";
|
import { handleCommands, passedValidArgument } from "../common/handleCommands.js";
|
||||||
import { getLang } from "../common/lang.js";
|
import { getLang } from "../common/lang.js";
|
||||||
|
import {
|
||||||
|
isBlockedLocalhostWebSocket,
|
||||||
|
isDiscordIcsBlobUrl,
|
||||||
|
isDiscordPopoutUrl,
|
||||||
|
isTelemetryBlockedUrl,
|
||||||
|
isYouTubeEmbedOrProxyFrame,
|
||||||
|
} from "../common/sanitization.js";
|
||||||
import { injectThemesMain } from "../common/themes.js";
|
import { injectThemesMain } from "../common/themes.js";
|
||||||
import {
|
import {
|
||||||
DEFAULT_WINDOW_HEIGHT,
|
DEFAULT_WINDOW_HEIGHT,
|
||||||
|
|
@ -199,10 +206,7 @@ function doAfterDefiningTheWindow(passedWindow: BrowserWindow): void {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
frame.once("dom-ready", async () => {
|
frame.once("dom-ready", async () => {
|
||||||
if (
|
if (isYouTubeEmbedOrProxyFrame(frame.url)) {
|
||||||
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"));
|
await frame.executeJavaScript(readFileSync(path.join(__dirname, "assets/js/adguard.js"), "utf-8"));
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
@ -211,18 +215,14 @@ function doAfterDefiningTheWindow(passedWindow: BrowserWindow): void {
|
||||||
// Allow about:blank (used by Vencord & Equicord QuickCss popup)
|
// Allow about:blank (used by Vencord & Equicord QuickCss popup)
|
||||||
if (url === "about:blank") return { action: "allow" };
|
if (url === "about:blank") return { action: "allow" };
|
||||||
// Saving ics files on future events
|
// Saving ics files on future events
|
||||||
if (url.startsWith("blob:https://discord.com/")) {
|
if (isDiscordIcsBlobUrl(url)) {
|
||||||
return {
|
return {
|
||||||
action: "allow",
|
action: "allow",
|
||||||
overrideBrowserWindowOptions: { show: false },
|
overrideBrowserWindowOptions: { show: false },
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
// Allow Discord stream popout
|
// Allow Discord stream popout
|
||||||
if (
|
if (isDiscordPopoutUrl(url))
|
||||||
url === "https://discord.com/popout" ||
|
|
||||||
url === "https://canary.discord.com/popout" ||
|
|
||||||
url === "https://ptb.discord.com/popout"
|
|
||||||
)
|
|
||||||
return {
|
return {
|
||||||
action: "allow",
|
action: "allow",
|
||||||
overrideBrowserWindowOptions: {
|
overrideBrowserWindowOptions: {
|
||||||
|
|
@ -267,21 +267,8 @@ function doAfterDefiningTheWindow(passedWindow: BrowserWindow): void {
|
||||||
|
|
||||||
registerCustomHandler();
|
registerCustomHandler();
|
||||||
|
|
||||||
const blockedPatterns = [
|
|
||||||
/https:\/\/.*\/api\/v\d+\/science/,
|
|
||||||
/https:\/\/sentry\.io\/.*/,
|
|
||||||
/https:\/\/.*\.nel\.cloudflare\.com\/.*/,
|
|
||||||
];
|
|
||||||
passedWindow.webContents.session.webRequest.onBeforeRequest((details, callback) => {
|
passedWindow.webContents.session.webRequest.onBeforeRequest((details, callback) => {
|
||||||
if (blockedPatterns.some((pattern) => pattern.test(details.url))) {
|
if (isTelemetryBlockedUrl(details.url) || isBlockedLocalhostWebSocket(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({ cancel: true });
|
||||||
}
|
}
|
||||||
return callback({});
|
return callback({});
|
||||||
|
|
|
||||||
|
|
@ -41,17 +41,60 @@ export const AboutPopup = (props: { close: () => void }) => {
|
||||||
<ModalBody>
|
<ModalBody>
|
||||||
<div class={classes.aboutContainer}>
|
<div class={classes.aboutContainer}>
|
||||||
<div class={classes.aboutHeader}>
|
<div class={classes.aboutHeader}>
|
||||||
<h2 class={classes.aboutTitle}>Legcord</h2>
|
<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>
|
||||||
<p class={classes.aboutDescription}>
|
<p class={classes.aboutDescription}>
|
||||||
Legcord is a free and open-source Discord client that offers a personalized experience with
|
A free and open-source Discord client crafted by the community, for the community —
|
||||||
advanced features, better performance, and a modern design. Developed by the community, for
|
delivering advanced features, better performance, and a modern design.
|
||||||
the community.
|
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* Quick Actions */}
|
|
||||||
<div class={classes.quickActions}>
|
<div class={classes.quickActions}>
|
||||||
<h3 class={classes.quickActionsTitle}>Quick Actions</h3>
|
<h3 class={classes.sectionTitle}>Quick Actions</h3>
|
||||||
<div class={classes.quickActionsList}>
|
<div class={classes.quickActionsList}>
|
||||||
<button
|
<button
|
||||||
type="button"
|
type="button"
|
||||||
|
|
@ -68,13 +111,30 @@ export const AboutPopup = (props: { close: () => void }) => {
|
||||||
stroke-width="2"
|
stroke-width="2"
|
||||||
stroke-linecap="round"
|
stroke-linecap="round"
|
||||||
stroke-linejoin="round"
|
stroke-linejoin="round"
|
||||||
|
aria-hidden="true"
|
||||||
>
|
>
|
||||||
<title>View Source Code</title>
|
|
||||||
<polyline points="16 18 22 12 16 6" />
|
<polyline points="16 18 22 12 16 6" />
|
||||||
<polyline points="8 6 2 12 8 18" />
|
<polyline points="8 6 2 12 8 18" />
|
||||||
</svg>
|
</svg>
|
||||||
</span>
|
</span>
|
||||||
View Source Code
|
<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>
|
||||||
</button>
|
</button>
|
||||||
<button
|
<button
|
||||||
type="button"
|
type="button"
|
||||||
|
|
@ -91,13 +151,30 @@ export const AboutPopup = (props: { close: () => void }) => {
|
||||||
stroke-width="2"
|
stroke-width="2"
|
||||||
stroke-linecap="round"
|
stroke-linecap="round"
|
||||||
stroke-linejoin="round"
|
stroke-linejoin="round"
|
||||||
|
aria-hidden="true"
|
||||||
>
|
>
|
||||||
<title>Donate</title>
|
|
||||||
<line x1="12" y1="1" x2="12" y2="23" />
|
<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" />
|
<path d="M17 5H9.5a3.5 3.5 0 0 0 0 7h5a3.5 3.5 0 0 1 0 7H6" />
|
||||||
</svg>
|
</svg>
|
||||||
</span>
|
</span>
|
||||||
Donate
|
<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>
|
||||||
</button>
|
</button>
|
||||||
<button
|
<button
|
||||||
type="button"
|
type="button"
|
||||||
|
|
@ -105,19 +182,44 @@ export const AboutPopup = (props: { close: () => void }) => {
|
||||||
onClick={() => window.open("https://discord.gg/JatCnhKufc", "_blank")}
|
onClick={() => window.open("https://discord.gg/JatCnhKufc", "_blank")}
|
||||||
>
|
>
|
||||||
<span class={classes.quickActionButtonIcon}>
|
<span class={classes.quickActionButtonIcon}>
|
||||||
<svg width="20" height="20" viewBox="0 0 24 24" fill="currentColor" stroke="none">
|
<svg
|
||||||
<title>Join Discord Server</title>
|
width="20"
|
||||||
|
height="20"
|
||||||
|
viewBox="0 0 24 24"
|
||||||
|
fill="currentColor"
|
||||||
|
stroke="none"
|
||||||
|
aria-hidden="true"
|
||||||
|
>
|
||||||
<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" />
|
<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>
|
</svg>
|
||||||
</span>
|
</span>
|
||||||
Join Discord Server
|
<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>
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* Contributors Section */}
|
|
||||||
<div class={classes.contributorsSection}>
|
<div class={classes.contributorsSection}>
|
||||||
<h3 class={classes.contributorsTitle}>Contributors</h3>
|
<h3 class={classes.sectionTitle}>Contributors</h3>
|
||||||
|
{!loading() && !error() && (
|
||||||
|
<span class={classes.contributorBadge}>{contributors().length} contributors</span>
|
||||||
|
)}
|
||||||
{loading() && <div class={classes.loadingState}>Loading contributors...</div>}
|
{loading() && <div class={classes.loadingState}>Loading contributors...</div>}
|
||||||
{error() && <div class={classes.errorState}>Failed to load contributors: {error()}</div>}
|
{error() && <div class={classes.errorState}>Failed to load contributors: {error()}</div>}
|
||||||
{!loading() && !error() && (
|
{!loading() && !error() && (
|
||||||
|
|
@ -147,6 +249,22 @@ export const AboutPopup = (props: { close: () => void }) => {
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
</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 © Legcord 2024-2026 · OSL-3.0</span>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</ModalBody>
|
</ModalBody>
|
||||||
</ModalRoot>
|
</ModalRoot>
|
||||||
|
|
|
||||||
|
|
@ -93,75 +93,75 @@
|
||||||
min-height: auto;
|
min-height: auto;
|
||||||
}
|
}
|
||||||
|
|
||||||
.aboutHeader {
|
.aboutLogo {
|
||||||
text-align: center;
|
display: flex;
|
||||||
margin-bottom: 32px;
|
justify-content: center;
|
||||||
padding: 24px;
|
margin-bottom: 20px;
|
||||||
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);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.aboutTitle {
|
.aboutLogo svg {
|
||||||
font-size: 32px;
|
opacity: 0.95;
|
||||||
font-weight: 700;
|
}
|
||||||
margin: 0 0 20px 0;
|
|
||||||
color: var(--header-primary);
|
.aboutHeader {
|
||||||
letter-spacing: -0.5px;
|
text-align: center;
|
||||||
|
margin-bottom: 24px;
|
||||||
|
padding: 32px 24px 24px;
|
||||||
|
background: linear-gradient(180deg, var(--background-secondary-alt) 0%, var(--background-secondary) 100%);
|
||||||
|
border: 1px solid var(--background-modifier-accent);
|
||||||
|
border-radius: 12px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.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;
|
||||||
}
|
}
|
||||||
|
|
||||||
.aboutDescription {
|
.aboutDescription {
|
||||||
font-size: 16px;
|
font-size: 14px;
|
||||||
line-height: 1.6;
|
line-height: 1.5;
|
||||||
color: var(--text-muted);
|
color: var(--text-muted);
|
||||||
margin: 0 0 24px 0;
|
margin: 0;
|
||||||
max-width: 600px;
|
padding: 0 8px;
|
||||||
|
max-width: 520px;
|
||||||
margin-left: auto;
|
margin-left: auto;
|
||||||
margin-right: auto;
|
margin-right: auto;
|
||||||
font-weight: 400;
|
font-weight: 400;
|
||||||
}
|
}
|
||||||
|
|
||||||
.aboutVersion {
|
.sectionTitle {
|
||||||
font-size: 14px;
|
font-size: 16px;
|
||||||
color: var(--text-muted);
|
font-weight: 600;
|
||||||
margin: 0;
|
margin: 0 0 16px 0;
|
||||||
padding: 10px 18px;
|
color: var(--header-primary);
|
||||||
background: var(--background-tertiary);
|
letter-spacing: -0.2px;
|
||||||
border-radius: 8px;
|
text-transform: uppercase;
|
||||||
display: inline-block;
|
opacity: 0.7;
|
||||||
font-family: var(--font-mono);
|
|
||||||
border: 1px solid var(--background-modifier-accent);
|
|
||||||
font-weight: 500;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.quickActions {
|
.quickActions {
|
||||||
background: var(--background-secondary);
|
margin-bottom: 24px;
|
||||||
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 {
|
.quickActionsList {
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
gap: 12px;
|
gap: 8px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.quickActionButton {
|
.quickActionButton {
|
||||||
background: var(--background-secondary-alt);
|
background: var(--background-secondary);
|
||||||
border: 1px solid var(--background-modifier-accent);
|
border: 1px solid var(--background-modifier-accent);
|
||||||
border-radius: 8px;
|
border-radius: 10px;
|
||||||
padding: 12px 16px;
|
padding: 12px 16px;
|
||||||
font-size: 14px;
|
font-size: 14px;
|
||||||
font-weight: 500;
|
font-weight: 500;
|
||||||
|
|
@ -171,73 +171,110 @@
|
||||||
align-items: center;
|
align-items: center;
|
||||||
text-align: left;
|
text-align: left;
|
||||||
width: 100%;
|
width: 100%;
|
||||||
transition: all 0.15s ease;
|
transition: all 0.2s ease;
|
||||||
box-shadow: 0 1px 3px rgba(0, 0, 0, 0.1);
|
|
||||||
position: relative;
|
position: relative;
|
||||||
overflow: hidden;
|
overflow: hidden;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.quickActionButton::before {
|
||||||
|
content: "";
|
||||||
|
position: absolute;
|
||||||
|
inset: 0;
|
||||||
|
background: var(--brand-experiment);
|
||||||
|
opacity: 0;
|
||||||
|
transition: opacity 0.2s ease;
|
||||||
|
}
|
||||||
|
|
||||||
.quickActionButton:hover {
|
.quickActionButton:hover {
|
||||||
background: var(--background-tertiary);
|
border-color: var(--brand-experiment);
|
||||||
border-color: var(--background-modifier-hover);
|
transform: translateX(4px);
|
||||||
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.15);
|
}
|
||||||
transform: translateY(-1px);
|
|
||||||
|
.quickActionButton:hover::before {
|
||||||
|
opacity: 0.04;
|
||||||
}
|
}
|
||||||
|
|
||||||
.quickActionButton:active {
|
.quickActionButton:active {
|
||||||
transform: translateY(0);
|
transform: translateX(2px);
|
||||||
box-shadow: 0 1px 3px rgba(0, 0, 0, 0.1);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.quickActionButtonIcon {
|
.quickActionButtonIcon {
|
||||||
margin-right: 12px;
|
margin-right: 12px;
|
||||||
font-size: 18px;
|
|
||||||
color: var(--interactive-normal);
|
|
||||||
transition: color 0.15s ease;
|
|
||||||
display: flex;
|
display: flex;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
justify-content: center;
|
justify-content: center;
|
||||||
width: 20px;
|
width: 36px;
|
||||||
height: 20px;
|
height: 36px;
|
||||||
|
border-radius: 8px;
|
||||||
|
background: var(--background-tertiary);
|
||||||
|
color: var(--interactive-normal);
|
||||||
|
flex-shrink: 0;
|
||||||
|
transition: all 0.2s ease;
|
||||||
}
|
}
|
||||||
|
|
||||||
.quickActionButton:hover .quickActionButtonIcon {
|
.quickActionButton:hover .quickActionButtonIcon {
|
||||||
color: var(--interactive-hover);
|
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);
|
||||||
}
|
}
|
||||||
|
|
||||||
.contributorsSection {
|
.contributorsSection {
|
||||||
padding: 20px;
|
padding: 20px;
|
||||||
background: var(--background-secondary);
|
background: var(--background-secondary);
|
||||||
border: 1px solid var(--background-accent);
|
border: 1px solid var(--background-modifier-accent);
|
||||||
border-radius: 12px;
|
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 {
|
.contributorsGrid {
|
||||||
display: grid;
|
display: grid;
|
||||||
grid-template-columns: repeat(auto-fill, minmax(200px, 1fr));
|
grid-template-columns: repeat(auto-fill, minmax(160px, 1fr));
|
||||||
gap: 16px;
|
gap: 10px;
|
||||||
padding: 0;
|
padding: 0;
|
||||||
max-width: 100%;
|
max-width: 100%;
|
||||||
width: 100%;
|
width: 100%;
|
||||||
justify-items: center;
|
max-height: 280px;
|
||||||
max-height: 40vh;
|
|
||||||
overflow-y: auto;
|
overflow-y: auto;
|
||||||
scrollbar-width: thin;
|
scrollbar-width: thin;
|
||||||
scrollbar-color: var(--background-modifier-accent) transparent;
|
scrollbar-color: var(--background-modifier-accent) transparent;
|
||||||
}
|
}
|
||||||
|
|
||||||
.contributorsGrid::-webkit-scrollbar {
|
.contributorsGrid::-webkit-scrollbar {
|
||||||
width: 6px;
|
width: 4px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.contributorsGrid::-webkit-scrollbar-track {
|
.contributorsGrid::-webkit-scrollbar-track {
|
||||||
|
|
@ -246,7 +283,7 @@
|
||||||
|
|
||||||
.contributorsGrid::-webkit-scrollbar-thumb {
|
.contributorsGrid::-webkit-scrollbar-thumb {
|
||||||
background: var(--background-modifier-accent);
|
background: var(--background-modifier-accent);
|
||||||
border-radius: 3px;
|
border-radius: 2px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.contributorsGrid::-webkit-scrollbar-thumb:hover {
|
.contributorsGrid::-webkit-scrollbar-thumb:hover {
|
||||||
|
|
@ -256,27 +293,23 @@
|
||||||
.contributorCard {
|
.contributorCard {
|
||||||
background: var(--background-primary);
|
background: var(--background-primary);
|
||||||
border: 1px solid var(--background-modifier-accent);
|
border: 1px solid var(--background-modifier-accent);
|
||||||
border-radius: 12px;
|
border-radius: 10px;
|
||||||
padding: 20px;
|
padding: 14px 12px;
|
||||||
text-align: center;
|
text-align: center;
|
||||||
transition: all 0.15s ease;
|
transition: all 0.2s ease;
|
||||||
min-width: 180px;
|
|
||||||
max-width: 100%;
|
|
||||||
box-sizing: border-box;
|
box-sizing: border-box;
|
||||||
width: 100%;
|
width: 100%;
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
justify-content: center;
|
justify-content: center;
|
||||||
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.08);
|
|
||||||
position: relative;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.contributorCard:hover {
|
.contributorCard:hover {
|
||||||
background: var(--background-secondary);
|
background: var(--background-secondary-alt);
|
||||||
border-color: var(--background-modifier-hover);
|
border-color: var(--brand-experiment);
|
||||||
box-shadow: 0 4px 16px rgba(0, 0, 0, 0.12);
|
|
||||||
transform: translateY(-2px);
|
transform: translateY(-2px);
|
||||||
|
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.15);
|
||||||
}
|
}
|
||||||
|
|
||||||
.contributorLink {
|
.contributorLink {
|
||||||
|
|
@ -290,46 +323,98 @@
|
||||||
}
|
}
|
||||||
|
|
||||||
.contributorAvatar {
|
.contributorAvatar {
|
||||||
width: 64px;
|
width: 48px;
|
||||||
height: 64px;
|
height: 48px;
|
||||||
border-radius: 50%;
|
border-radius: 50%;
|
||||||
margin-bottom: 16px;
|
margin-bottom: 10px;
|
||||||
max-width: 100%;
|
max-width: 100%;
|
||||||
border: 3px solid var(--background-modifier-accent);
|
border: 2px solid var(--background-modifier-accent);
|
||||||
object-fit: cover;
|
object-fit: cover;
|
||||||
transition: all 0.15s ease;
|
transition: all 0.2s ease;
|
||||||
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.contributorCard:hover .contributorAvatar {
|
.contributorCard:hover .contributorAvatar {
|
||||||
border-color: var(--brand-experiment);
|
border-color: var(--brand-experiment);
|
||||||
transform: scale(1.05);
|
transform: scale(1.1);
|
||||||
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.15);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.contributorName {
|
.contributorName {
|
||||||
font-weight: 600;
|
font-weight: 600;
|
||||||
font-size: 16px;
|
font-size: 13px;
|
||||||
margin-bottom: 8px;
|
margin-bottom: 4px;
|
||||||
color: var(--header-primary);
|
color: var(--header-primary);
|
||||||
word-break: break-word;
|
word-break: break-word;
|
||||||
overflow-wrap: break-word;
|
overflow-wrap: break-word;
|
||||||
text-align: center;
|
text-align: center;
|
||||||
transition: color 0.15s ease;
|
transition: color 0.2s ease;
|
||||||
letter-spacing: -0.2px;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.contributorCard:hover .contributorName {
|
.contributorCard:hover .contributorName {
|
||||||
color: var(--interactive-hover);
|
color: var(--brand-experiment);
|
||||||
}
|
}
|
||||||
|
|
||||||
.contributorInfo {
|
.contributorInfo {
|
||||||
font-size: 13px;
|
font-size: 11px;
|
||||||
color: var(--text-muted);
|
color: var(--text-muted);
|
||||||
margin: 0;
|
margin: 0;
|
||||||
text-align: center;
|
text-align: center;
|
||||||
font-weight: 400;
|
font-weight: 400;
|
||||||
line-height: 1.4;
|
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;
|
||||||
}
|
}
|
||||||
|
|
||||||
.loadingState,
|
.loadingState,
|
||||||
|
|
@ -348,6 +433,25 @@
|
||||||
border-radius: 8px;
|
border-radius: 8px;
|
||||||
margin: 16px;
|
margin: 16px;
|
||||||
}
|
}
|
||||||
.modal {
|
|
||||||
max-height: 500px;
|
.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;
|
||||||
}
|
}
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue