5et Initial React Skin
This commit is contained in:
1 parent
b236390afe
commit
0eb340e1c2
17 files changed
+5079
No files matched your search
+52
@@ -0,0 +1,52 @@
|
|||||||
|
# react/.gitignore
|
||||||
|
.DS_STORE
|
||||||
|
node_modules
|
||||||
|
scripts/flow/*/.flowconfig
|
||||||
|
.flowconfig
|
||||||
|
*~
|
||||||
|
*.pyc
|
||||||
|
.grunt
|
||||||
|
_SpecRunner.html
|
||||||
|
__benchmarks__
|
||||||
|
build/
|
||||||
|
remote-repo/
|
||||||
|
coverage/
|
||||||
|
.module-cache
|
||||||
|
fixtures/dom/public/react-dom.js
|
||||||
|
fixtures/dom/public/react.js
|
||||||
|
test/the-files-to-test.generated.js
|
||||||
|
*.log*
|
||||||
|
chrome-user-data
|
||||||
|
*.sublime-project
|
||||||
|
*.sublime-workspace
|
||||||
|
.idea
|
||||||
|
*.iml
|
||||||
|
.vscode
|
||||||
|
*.swp
|
||||||
|
*.swo
|
||||||
|
/tmp
|
||||||
|
/.worktrees
|
||||||
|
.claude/*.local.*
|
||||||
|
|
||||||
|
packages/react-devtools-core/dist
|
||||||
|
packages/react-devtools-extensions/chrome/build
|
||||||
|
packages/react-devtools-extensions/chrome/*.crx
|
||||||
|
packages/react-devtools-extensions/chrome/*.pem
|
||||||
|
packages/react-devtools-extensions/firefox/build
|
||||||
|
packages/react-devtools-extensions/firefox/*.xpi
|
||||||
|
packages/react-devtools-extensions/firefox/*.pem
|
||||||
|
packages/react-devtools-extensions/shared/build
|
||||||
|
packages/react-devtools-extensions/.tempUserDataDir
|
||||||
|
packages/react-devtools-fusebox/dist
|
||||||
|
packages/react-devtools-inline/dist
|
||||||
|
packages/react-devtools-shell/dist
|
||||||
|
packages/react-devtools-timeline/dist
|
||||||
|
|
||||||
|
# 5et-d/.gitignore
|
||||||
|
5etools-src/
|
||||||
|
dist/
|
||||||
|
dist-electron/
|
||||||
|
release/
|
||||||
|
5etools*.zip
|
||||||
|
*.md
|
||||||
|
!README.md
|
||||||
@@ -0,0 +1,71 @@
|
|||||||
|
# 5ET-D Desktop App
|
||||||
|
|
||||||
|
Current stage:
|
||||||
|
|
||||||
|
- Build 0 complete: Electron boots directly into local 5etools wiki.
|
||||||
|
- Stage 1 in progress: React shell navigation + light visual reskin pass, while preserving wiki behavior.
|
||||||
|
|
||||||
|
## Prerequisites
|
||||||
|
|
||||||
|
- Node.js 20+
|
||||||
|
- `5etools-src` directory in this repository root:
|
||||||
|
- `./5etools-src`
|
||||||
|
|
||||||
|
Optional override:
|
||||||
|
|
||||||
|
- Set `FIVEETOOLS_SRC_DIR` to point at a different `5etools-src` path.
|
||||||
|
- Set `FIVEETOOLS_DESKTOP_RESKIN=0` to disable injected wiki styling for strict baseline comparisons.
|
||||||
|
- Set `FIVEETOOLS_DESKTOP_FORCE_NIGHT=0` to keep 5etools day theme while using desktop reskin.
|
||||||
|
|
||||||
|
## Install
|
||||||
|
|
||||||
|
```bash
|
||||||
|
npm install
|
||||||
|
```
|
||||||
|
|
||||||
|
## Run (dev)
|
||||||
|
|
||||||
|
```bash
|
||||||
|
npm run dev
|
||||||
|
```
|
||||||
|
|
||||||
|
`npm run dev` uses a portless workflow (`vite build --watch`) for environments where local TCP binding is restricted.
|
||||||
|
|
||||||
|
Optional HMR mode (requires local port binding):
|
||||||
|
|
||||||
|
```bash
|
||||||
|
npm run dev:web
|
||||||
|
```
|
||||||
|
|
||||||
|
## Run (build once + launch)
|
||||||
|
|
||||||
|
```bash
|
||||||
|
npm run start
|
||||||
|
```
|
||||||
|
|
||||||
|
## Package installers
|
||||||
|
|
||||||
|
```bash
|
||||||
|
npm run dist
|
||||||
|
```
|
||||||
|
|
||||||
|
This command installs `electron-builder` on demand via `npx` for packaging, rather than during every local dependency install.
|
||||||
|
|
||||||
|
## Notes
|
||||||
|
|
||||||
|
- The app starts an internal local HTTP server for wiki content.
|
||||||
|
- This avoids `file://` loading limitations while keeping content fully local.
|
||||||
|
- Electron launch scripts clear `ELECTRON_RUN_AS_NODE` automatically, so app boot is stable even if that variable is set in your shell environment.
|
||||||
|
- SQLite is intentionally deferred until post-Build 0 to avoid native module/toolchain issues while the wiki bootstrap is stabilized.
|
||||||
|
- Build identity is set to:
|
||||||
|
- Product name: `5ET-D`
|
||||||
|
- App ID: `com.notBoris.5et-desktop`
|
||||||
|
|
||||||
|
## Troubleshooting
|
||||||
|
|
||||||
|
- `app.whenReady` undefined or Electron behaving like Node:
|
||||||
|
- check `echo $ELECTRON_RUN_AS_NODE`
|
||||||
|
- launcher scripts already clear this variable before app start
|
||||||
|
- `listen EPERM ...5173`:
|
||||||
|
- use `npm run dev` (portless mode)
|
||||||
|
- only use `npm run dev:web` when local TCP binding is available
|
||||||
+12
@@ -0,0 +1,12 @@
|
|||||||
|
<!doctype html>
|
||||||
|
<html lang="en">
|
||||||
|
<head>
|
||||||
|
<meta charset="UTF-8" />
|
||||||
|
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||||
|
<title>5ET-D</title>
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
<div id="root"></div>
|
||||||
|
<script type="module" src="/src/renderer/main.tsx"></script>
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
Generated
+3402
File diff suppressed because it is too large.
Load diff
@@ -0,0 +1,69 @@
|
|||||||
|
{
|
||||||
|
"name": "5et-d-desktop",
|
||||||
|
"version": "0.1.0",
|
||||||
|
"private": true,
|
||||||
|
"description": "5ET-D desktop shell for local 5etools wiki",
|
||||||
|
"main": "dist-electron/main/main.js",
|
||||||
|
"scripts": {
|
||||||
|
"clean": "rimraf dist dist-electron release",
|
||||||
|
"build:renderer": "vite build",
|
||||||
|
"build:main": "tsc -p tsconfig.main.json",
|
||||||
|
"build": "npm run clean && npm run build:renderer && npm run build:main",
|
||||||
|
"dev:renderer": "vite build --watch",
|
||||||
|
"dev:renderer:web": "vite --host 127.0.0.1 --port 5173 --strictPort",
|
||||||
|
"dev:main": "tsc -p tsconfig.main.json --watch",
|
||||||
|
"dev:electron": "wait-on file:dist/index.html file:dist-electron/main/main.js && node scripts/run-electron.cjs .",
|
||||||
|
"dev:electron:web": "wait-on tcp:127.0.0.1:5173 file:dist-electron/main/main.js && VITE_DEV_SERVER_URL=http://127.0.0.1:5173 node scripts/run-electron.cjs .",
|
||||||
|
"dev": "concurrently -k \"npm:dev:main\" \"npm:dev:renderer\" \"npm:dev:electron\"",
|
||||||
|
"dev:web": "concurrently -k \"npm:dev:main\" \"npm:dev:renderer:web\" \"npm:dev:electron:web\"",
|
||||||
|
"start": "npm run build && node scripts/run-electron.cjs .",
|
||||||
|
"pack": "npm run build && npx electron-builder --dir",
|
||||||
|
"dist": "npm run build && npx electron-builder"
|
||||||
|
},
|
||||||
|
"dependencies": {
|
||||||
|
"electron": "40.6.0",
|
||||||
|
"react": "19.2.4",
|
||||||
|
"react-dom": "19.2.4"
|
||||||
|
},
|
||||||
|
"devDependencies": {
|
||||||
|
"@types/node": "latest",
|
||||||
|
"@types/react": "latest",
|
||||||
|
"@types/react-dom": "latest",
|
||||||
|
"@vitejs/plugin-react": "5.1.4",
|
||||||
|
"concurrently": "9.2.1",
|
||||||
|
"rimraf": "6.1.3",
|
||||||
|
"typescript": "5.9.3",
|
||||||
|
"vite": "7.3.1",
|
||||||
|
"wait-on": "9.0.4"
|
||||||
|
},
|
||||||
|
"build": {
|
||||||
|
"appId": "com.notBoris.5et-desktop",
|
||||||
|
"productName": "5ET-D",
|
||||||
|
"directories": {
|
||||||
|
"output": "release"
|
||||||
|
},
|
||||||
|
"files": [
|
||||||
|
"dist/**/*",
|
||||||
|
"dist-electron/**/*",
|
||||||
|
"package.json"
|
||||||
|
],
|
||||||
|
"extraResources": [
|
||||||
|
{
|
||||||
|
"from": "5etools-src",
|
||||||
|
"to": "5etools-src",
|
||||||
|
"filter": [
|
||||||
|
"**/*"
|
||||||
|
]
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"mac": {
|
||||||
|
"target": "dmg"
|
||||||
|
},
|
||||||
|
"win": {
|
||||||
|
"target": "nsis"
|
||||||
|
},
|
||||||
|
"linux": {
|
||||||
|
"target": "AppImage"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
Executable
+33
@@ -0,0 +1,33 @@
|
|||||||
|
#!/usr/bin/env node
|
||||||
|
|
||||||
|
const { spawn } = require('node:child_process');
|
||||||
|
|
||||||
|
const electronBinary = require('electron');
|
||||||
|
const args = process.argv.slice(2);
|
||||||
|
|
||||||
|
const env = { ...process.env };
|
||||||
|
delete env.ELECTRON_RUN_AS_NODE;
|
||||||
|
|
||||||
|
const child = spawn(electronBinary, args, {
|
||||||
|
stdio: 'inherit',
|
||||||
|
env
|
||||||
|
});
|
||||||
|
|
||||||
|
child.on('error', (err) => {
|
||||||
|
console.error('Failed to launch Electron:', err);
|
||||||
|
process.exit(1);
|
||||||
|
});
|
||||||
|
|
||||||
|
child.on('exit', (code, signal) => {
|
||||||
|
if (signal) {
|
||||||
|
console.error(`Electron exited via signal: ${signal}`);
|
||||||
|
process.exit(1);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (code && code !== 0) {
|
||||||
|
console.error(`Electron exited with code: ${code}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
process.exit(code ?? 0);
|
||||||
|
});
|
||||||
@@ -0,0 +1,82 @@
|
|||||||
|
import path from "node:path";
|
||||||
|
import { app, BrowserWindow, ipcMain } from "electron";
|
||||||
|
import { startWikiServer, type WikiServer } from "./wikiServer";
|
||||||
|
|
||||||
|
let mainWindow: BrowserWindow | null = null;
|
||||||
|
let wikiServer: WikiServer | null = null;
|
||||||
|
|
||||||
|
const DEV_RENDERER_URL = process.env.VITE_DEV_SERVER_URL;
|
||||||
|
|
||||||
|
function isDevMode(): boolean {
|
||||||
|
return !app.isPackaged;
|
||||||
|
}
|
||||||
|
|
||||||
|
async function createMainWindow(): Promise<void> {
|
||||||
|
if (!wikiServer) {
|
||||||
|
throw new Error("Wiki server was not initialized before creating the window");
|
||||||
|
}
|
||||||
|
|
||||||
|
mainWindow = new BrowserWindow({
|
||||||
|
width: 1440,
|
||||||
|
height: 900,
|
||||||
|
minWidth: 1024,
|
||||||
|
minHeight: 700,
|
||||||
|
title: "5etools Desktop",
|
||||||
|
webPreferences: {
|
||||||
|
contextIsolation: true,
|
||||||
|
nodeIntegration: false,
|
||||||
|
sandbox: false,
|
||||||
|
preload: path.join(__dirname, "../preload/preload.js")
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
if (isDevMode() && DEV_RENDERER_URL) {
|
||||||
|
await mainWindow.loadURL(DEV_RENDERER_URL);
|
||||||
|
} else {
|
||||||
|
await mainWindow.loadFile(path.join(__dirname, "../../dist/index.html"));
|
||||||
|
}
|
||||||
|
|
||||||
|
mainWindow.on("closed", () => {
|
||||||
|
mainWindow = null;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
async function bootstrap(): Promise<void> {
|
||||||
|
wikiServer = await startWikiServer();
|
||||||
|
|
||||||
|
ipcMain.handle("wiki:get-url", async () => {
|
||||||
|
if (!wikiServer) {
|
||||||
|
throw new Error("Wiki server is not available");
|
||||||
|
}
|
||||||
|
|
||||||
|
return wikiServer.url;
|
||||||
|
});
|
||||||
|
|
||||||
|
await createMainWindow();
|
||||||
|
|
||||||
|
app.on("activate", async () => {
|
||||||
|
if (BrowserWindow.getAllWindows().length === 0) {
|
||||||
|
await createMainWindow();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
app.whenReady().then(() => {
|
||||||
|
bootstrap().catch((err) => {
|
||||||
|
console.error("Failed to bootstrap app", err);
|
||||||
|
app.exit(1);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
app.on("window-all-closed", () => {
|
||||||
|
if (process.platform !== "darwin") {
|
||||||
|
app.quit();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
app.on("before-quit", async () => {
|
||||||
|
if (wikiServer) {
|
||||||
|
await wikiServer.close();
|
||||||
|
wikiServer = null;
|
||||||
|
}
|
||||||
|
});
|
||||||
@@ -0,0 +1,908 @@
|
|||||||
|
import fs from "node:fs";
|
||||||
|
import http from "node:http";
|
||||||
|
import path from "node:path";
|
||||||
|
import { URL } from "node:url";
|
||||||
|
import { app } from "electron";
|
||||||
|
|
||||||
|
const SHOULD_INJECT_RESKIN = process.env.FIVEETOOLS_DESKTOP_RESKIN !== "0";
|
||||||
|
const SHOULD_FORCE_NIGHT_THEME = process.env.FIVEETOOLS_DESKTOP_FORCE_NIGHT !== "0";
|
||||||
|
const SHOULD_USE_EXTERNAL_IMAGE_FALLBACK = process.env.FIVEETOOLS_DESKTOP_EXTERNAL_IMAGES !== "0";
|
||||||
|
const DEFAULT_EXTERNAL_IMAGE_ROOT = "https://5e.tools/img/";
|
||||||
|
const EXTERNAL_IMAGE_ROOT = SHOULD_USE_EXTERNAL_IMAGE_FALLBACK
|
||||||
|
? normalizeExternalBaseUrl(process.env.FIVEETOOLS_DESKTOP_IMG_BASE_URL || DEFAULT_EXTERNAL_IMAGE_ROOT)
|
||||||
|
: null;
|
||||||
|
|
||||||
|
const MIME_TYPES: Record<string, string> = {
|
||||||
|
".avif": "image/avif",
|
||||||
|
".css": "text/css; charset=utf-8",
|
||||||
|
".eot": "application/vnd.ms-fontobject",
|
||||||
|
".gif": "image/gif",
|
||||||
|
".html": "text/html; charset=utf-8",
|
||||||
|
".ico": "image/x-icon",
|
||||||
|
".jpeg": "image/jpeg",
|
||||||
|
".jpg": "image/jpeg",
|
||||||
|
".js": "text/javascript; charset=utf-8",
|
||||||
|
".json": "application/json; charset=utf-8",
|
||||||
|
".map": "application/json; charset=utf-8",
|
||||||
|
".mjs": "text/javascript; charset=utf-8",
|
||||||
|
".mp3": "audio/mpeg",
|
||||||
|
".mp4": "video/mp4",
|
||||||
|
".otf": "font/otf",
|
||||||
|
".pdf": "application/pdf",
|
||||||
|
".png": "image/png",
|
||||||
|
".svg": "image/svg+xml",
|
||||||
|
".ttf": "font/ttf",
|
||||||
|
".txt": "text/plain; charset=utf-8",
|
||||||
|
".wasm": "application/wasm",
|
||||||
|
".webm": "video/webm",
|
||||||
|
".webp": "image/webp",
|
||||||
|
".woff": "font/woff",
|
||||||
|
".woff2": "font/woff2",
|
||||||
|
".webmanifest": "application/manifest+json; charset=utf-8",
|
||||||
|
".xml": "application/xml; charset=utf-8"
|
||||||
|
};
|
||||||
|
|
||||||
|
const DESKTOP_RESKIN_BOOTSTRAP = `
|
||||||
|
(() => {
|
||||||
|
${EXTERNAL_IMAGE_ROOT ? `globalThis.DEPLOYED_IMG_ROOT = ${JSON.stringify(EXTERNAL_IMAGE_ROOT)};` : ""}
|
||||||
|
|
||||||
|
const root = document.documentElement;
|
||||||
|
root.classList.add("desktop-reskin");
|
||||||
|
|
||||||
|
${
|
||||||
|
SHOULD_FORCE_NIGHT_THEME
|
||||||
|
? `
|
||||||
|
root.classList.add("ve-night-mode", "ve-night-mode--standard");
|
||||||
|
try {
|
||||||
|
window.localStorage.setItem("StyleSwitcher_style", "night");
|
||||||
|
} catch {}
|
||||||
|
`
|
||||||
|
: ""
|
||||||
|
}
|
||||||
|
|
||||||
|
const doToastInfo = (content) => {
|
||||||
|
try {
|
||||||
|
if (globalThis.JqueryUtil?.doToast) {
|
||||||
|
globalThis.JqueryUtil.doToast({ type: "info", content });
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
} catch {}
|
||||||
|
|
||||||
|
console.info("[5ET-D]", content);
|
||||||
|
};
|
||||||
|
|
||||||
|
const closeDropdown = (li, toggle) => {
|
||||||
|
if (typeof NavBar !== "undefined" && typeof NavBar._closeDropdownElement === "function") {
|
||||||
|
NavBar._closeDropdownElement(li);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
li.classList.remove("open");
|
||||||
|
toggle.setAttribute("aria-expanded", "false");
|
||||||
|
};
|
||||||
|
|
||||||
|
const addCharactersMenu = () => {
|
||||||
|
const navbar = document.getElementById("navbar");
|
||||||
|
if (!navbar || navbar.querySelector(".desktop-nav-characters")) return;
|
||||||
|
|
||||||
|
const li = document.createElement("li");
|
||||||
|
li.setAttribute("role", "presentation");
|
||||||
|
li.className = "dropdown dropdown--navbar page__nav-hidden-mobile page__btn-nav-root desktop-nav-characters";
|
||||||
|
|
||||||
|
const toggle = document.createElement("a");
|
||||||
|
toggle.className = "ve-dropdown-toggle";
|
||||||
|
toggle.href = "#";
|
||||||
|
toggle.setAttribute("role", "button");
|
||||||
|
toggle.setAttribute("aria-haspopup", "true");
|
||||||
|
toggle.setAttribute("aria-expanded", "false");
|
||||||
|
toggle.innerHTML = 'Characters <span class="caret"></span>';
|
||||||
|
|
||||||
|
toggle.onclick = (event) => {
|
||||||
|
if (typeof NavBar !== "undefined" && typeof NavBar._handleDropdownClick === "function") {
|
||||||
|
NavBar._handleDropdownClick(toggle, event, false);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
event.preventDefault();
|
||||||
|
event.stopPropagation();
|
||||||
|
|
||||||
|
const isOpen = li.classList.contains("open");
|
||||||
|
[...navbar.querySelectorAll(":scope > li.dropdown--navbar.open")]
|
||||||
|
.forEach((it) => it.classList.remove("open"));
|
||||||
|
|
||||||
|
li.classList.toggle("open", !isOpen);
|
||||||
|
toggle.setAttribute("aria-expanded", String(!isOpen));
|
||||||
|
};
|
||||||
|
|
||||||
|
li.onmouseenter = function () {
|
||||||
|
if (typeof NavBar !== "undefined" && typeof NavBar._handleItemMouseEnter === "function") {
|
||||||
|
NavBar._handleItemMouseEnter(this);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const menu = document.createElement("ul");
|
||||||
|
menu.setAttribute("role", "menu");
|
||||||
|
menu.className = "ve-dropdown-menu ve-dropdown-menu--top";
|
||||||
|
menu.onclick = (event) => event.stopPropagation();
|
||||||
|
|
||||||
|
const addMenuItem = (label, onClick) => {
|
||||||
|
const menuItem = document.createElement("li");
|
||||||
|
menuItem.setAttribute("role", "presentation");
|
||||||
|
|
||||||
|
const link = document.createElement("a");
|
||||||
|
link.href = "#";
|
||||||
|
link.textContent = label;
|
||||||
|
link.onclick = (event) => {
|
||||||
|
event.preventDefault();
|
||||||
|
event.stopPropagation();
|
||||||
|
onClick();
|
||||||
|
closeDropdown(li, toggle);
|
||||||
|
};
|
||||||
|
|
||||||
|
menuItem.append(link);
|
||||||
|
menu.append(menuItem);
|
||||||
|
};
|
||||||
|
|
||||||
|
addMenuItem("New", () => doToastInfo("Characters: New is coming in a later build."));
|
||||||
|
addMenuItem("Load from JSON", () => doToastInfo("Characters: Load from JSON is coming in a later build."));
|
||||||
|
addMenuItem("My Characters", () => doToastInfo("Characters: My Characters is coming in a later build."));
|
||||||
|
|
||||||
|
li.append(toggle, menu);
|
||||||
|
|
||||||
|
const playerToggle = [...navbar.querySelectorAll(":scope > li.dropdown--navbar > a.ve-dropdown-toggle")]
|
||||||
|
.find((it) => it.textContent?.trim().startsWith("Player"));
|
||||||
|
const playerLi = playerToggle?.parentElement;
|
||||||
|
const helpLi = navbar.querySelector(':scope > li[data-page^="https://wiki.tercept.net/en/5eTools"]');
|
||||||
|
|
||||||
|
if (playerLi?.nextSibling) {
|
||||||
|
navbar.insertBefore(li, playerLi.nextSibling);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (playerLi) {
|
||||||
|
navbar.append(li);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (helpLi) {
|
||||||
|
navbar.insertBefore(li, helpLi);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
navbar.append(li);
|
||||||
|
};
|
||||||
|
|
||||||
|
window.addEventListener("DOMContentLoaded", addCharactersMenu);
|
||||||
|
window.addEventListener("load", addCharactersMenu);
|
||||||
|
})();
|
||||||
|
`;
|
||||||
|
const DESKTOP_RESKIN_STYLE = `
|
||||||
|
html.desktop-reskin {
|
||||||
|
--desktop-shell-gutter: 14px;
|
||||||
|
--desktop-topbar-pad-x: 18px;
|
||||||
|
}
|
||||||
|
|
||||||
|
html.desktop-reskin body {
|
||||||
|
background:
|
||||||
|
radial-gradient(circle at 13% -2%, rgba(77, 140, 222, 0.3), transparent 40%),
|
||||||
|
radial-gradient(circle at 86% -4%, rgba(241, 176, 92, 0.24), transparent 38%),
|
||||||
|
linear-gradient(180deg, #0c1422 0%, #0a1019 100%) !important;
|
||||||
|
color: #dce7f6;
|
||||||
|
}
|
||||||
|
|
||||||
|
html.desktop-reskin .page__header {
|
||||||
|
margin: 10px var(--desktop-shell-gutter) 0 !important;
|
||||||
|
padding: 0 !important;
|
||||||
|
border: 1px solid rgba(109, 164, 223, 0.32) !important;
|
||||||
|
border-bottom: 0 !important;
|
||||||
|
border-radius: 18px 18px 0 0;
|
||||||
|
background: linear-gradient(115deg, rgba(17, 29, 47, 0.98), rgba(24, 38, 58, 0.96) 64%, rgba(87, 58, 32, 0.88));
|
||||||
|
backdrop-filter: blur(14px);
|
||||||
|
box-shadow: 0 10px 24px rgba(0, 0, 0, 0.24);
|
||||||
|
}
|
||||||
|
|
||||||
|
html.desktop-reskin .page__header .container {
|
||||||
|
width: 100% !important;
|
||||||
|
max-width: none !important;
|
||||||
|
padding: 10px var(--desktop-topbar-pad-x) !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
html.desktop-reskin .page__header .ve-flex-v-baseline {
|
||||||
|
flex-wrap: wrap;
|
||||||
|
row-gap: 4px;
|
||||||
|
}
|
||||||
|
|
||||||
|
html.desktop-reskin .page__title {
|
||||||
|
margin: 0 !important;
|
||||||
|
color: #ebf4ff !important;
|
||||||
|
font-size: clamp(2.8rem, 4.6vw, 7rem);
|
||||||
|
line-height: 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
html.desktop-reskin .page__subtitle {
|
||||||
|
margin: 0 0 0 14px !important;
|
||||||
|
color: #bccce0 !important;
|
||||||
|
font-size: clamp(1.2rem, 1.75vw, 3.1rem);
|
||||||
|
}
|
||||||
|
|
||||||
|
html.desktop-reskin #navigation.page__nav {
|
||||||
|
margin: 0 var(--desktop-shell-gutter) 12px !important;
|
||||||
|
padding: 0 !important;
|
||||||
|
max-width: none !important;
|
||||||
|
width: auto !important;
|
||||||
|
border: 1px solid rgba(109, 164, 223, 0.32) !important;
|
||||||
|
border-top: 0 !important;
|
||||||
|
border-radius: 0 0 18px 18px;
|
||||||
|
background: linear-gradient(180deg, rgba(12, 21, 35, 0.96), rgba(8, 15, 27, 0.98));
|
||||||
|
box-shadow: 0 12px 26px rgba(0, 0, 0, 0.28);
|
||||||
|
position: relative;
|
||||||
|
z-index: 3000 !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
html.desktop-reskin #navigation.page__nav .page__nav-inner {
|
||||||
|
width: 100% !important;
|
||||||
|
margin: 0 !important;
|
||||||
|
padding: 10px var(--desktop-topbar-pad-x) !important;
|
||||||
|
overflow: visible;
|
||||||
|
}
|
||||||
|
|
||||||
|
html.desktop-reskin #navbar {
|
||||||
|
background: transparent !important;
|
||||||
|
border-bottom: 0 !important;
|
||||||
|
box-shadow: none;
|
||||||
|
display: flex;
|
||||||
|
align-items: stretch;
|
||||||
|
flex-wrap: wrap;
|
||||||
|
gap: 4px;
|
||||||
|
min-width: 0;
|
||||||
|
max-width: 100%;
|
||||||
|
overflow: visible;
|
||||||
|
position: relative;
|
||||||
|
z-index: 3001 !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
html.desktop-reskin #navbar > li {
|
||||||
|
float: none !important;
|
||||||
|
flex: 0 0 auto;
|
||||||
|
min-width: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
html.desktop-reskin #navbar > li > a {
|
||||||
|
border: 1px solid rgba(107, 159, 214, 0.28);
|
||||||
|
background: rgba(19, 31, 49, 0.9);
|
||||||
|
border-radius: 8px;
|
||||||
|
margin: 0 !important;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
height: 100%;
|
||||||
|
transition: background-color 120ms ease, border-color 120ms ease;
|
||||||
|
}
|
||||||
|
|
||||||
|
html.desktop-reskin #navbar > li.page__btn-nav-root > a {
|
||||||
|
border-color: rgba(116, 174, 233, 0.44) !important;
|
||||||
|
background: linear-gradient(180deg, rgba(22, 37, 58, 0.94), rgba(14, 24, 39, 0.97)) !important;
|
||||||
|
box-shadow: inset 0 1px 0 rgba(162, 214, 255, 0.12);
|
||||||
|
}
|
||||||
|
|
||||||
|
html.desktop-reskin #navbar > li.dropdown--navbar > a.ve-dropdown-toggle {
|
||||||
|
border-color: rgba(116, 174, 233, 0.44) !important;
|
||||||
|
background: linear-gradient(180deg, rgba(22, 37, 58, 0.94), rgba(14, 24, 39, 0.97)) !important;
|
||||||
|
box-shadow: inset 0 1px 0 rgba(162, 214, 255, 0.12);
|
||||||
|
}
|
||||||
|
|
||||||
|
html.desktop-reskin #navbar .navbar-brand,
|
||||||
|
html.desktop-reskin #navbar > li > a,
|
||||||
|
html.desktop-reskin #navbar > li > a .caret {
|
||||||
|
color: #e6f1ff !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
html.desktop-reskin #navbar > li > a:hover {
|
||||||
|
background: rgba(65, 121, 191, 0.32);
|
||||||
|
border-color: rgba(118, 184, 247, 0.56);
|
||||||
|
}
|
||||||
|
|
||||||
|
html.desktop-reskin #navbar > li.page__btn-nav-root > a:hover,
|
||||||
|
html.desktop-reskin #navbar > li.page__btn-nav-root > a:focus {
|
||||||
|
background: linear-gradient(145deg, rgba(52, 102, 161, 0.78), rgba(31, 86, 142, 0.82)) !important;
|
||||||
|
border-color: rgba(136, 202, 255, 0.84) !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
html.desktop-reskin #navbar > li.dropdown--navbar > a.ve-dropdown-toggle:hover,
|
||||||
|
html.desktop-reskin #navbar > li.dropdown--navbar > a.ve-dropdown-toggle:focus {
|
||||||
|
background: linear-gradient(145deg, rgba(52, 102, 161, 0.78), rgba(31, 86, 142, 0.82)) !important;
|
||||||
|
border-color: rgba(136, 202, 255, 0.84) !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
html.desktop-reskin #navbar > li.active > a,
|
||||||
|
html.desktop-reskin #navbar > li.open > a {
|
||||||
|
background: linear-gradient(140deg, rgba(33, 126, 209, 0.9), rgba(25, 95, 167, 0.94));
|
||||||
|
border-color: rgba(123, 196, 255, 0.86);
|
||||||
|
color: #f4f9ff !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
html.desktop-reskin #navbar > li.dropdown--navbar.open > a.ve-dropdown-toggle,
|
||||||
|
html.desktop-reskin #navbar > li.dropdown--navbar.active > a.ve-dropdown-toggle {
|
||||||
|
background: linear-gradient(140deg, rgba(34, 127, 210, 0.92), rgba(25, 95, 167, 0.96)) !important;
|
||||||
|
border-color: rgba(139, 207, 255, 0.92) !important;
|
||||||
|
box-shadow: inset 0 1px 0 rgba(229, 245, 255, 0.22);
|
||||||
|
}
|
||||||
|
|
||||||
|
html.desktop-reskin #navbar > li.page__btn-nav-root.active > a,
|
||||||
|
html.desktop-reskin #navbar > li.page__btn-nav-root.open > a {
|
||||||
|
background: linear-gradient(140deg, rgba(34, 127, 210, 0.92), rgba(25, 95, 167, 0.96)) !important;
|
||||||
|
border-color: rgba(139, 207, 255, 0.92) !important;
|
||||||
|
box-shadow: inset 0 1px 0 rgba(229, 245, 255, 0.22);
|
||||||
|
}
|
||||||
|
|
||||||
|
html.desktop-reskin #navbar > li.dropdown--navbar > a.ve-dropdown-toggle > .caret {
|
||||||
|
border-top-color: #c9e6ff !important;
|
||||||
|
opacity: 0.95;
|
||||||
|
}
|
||||||
|
|
||||||
|
html.desktop-reskin #navbar > .omni__wrp-input:not(.omni__wrp-input--scrolled) {
|
||||||
|
margin-left: auto !important;
|
||||||
|
flex: 1 1 320px;
|
||||||
|
min-width: 220px;
|
||||||
|
max-width: 460px;
|
||||||
|
}
|
||||||
|
|
||||||
|
html.desktop-reskin #navbar > .omni__wrp-input:not(.omni__wrp-input--scrolled) .omni__input {
|
||||||
|
min-width: 0 !important;
|
||||||
|
width: 100%;
|
||||||
|
border-right: 1px solid rgba(102, 155, 210, 0.42) !important;
|
||||||
|
border-top-right-radius: 12px !important;
|
||||||
|
border-bottom-right-radius: 12px !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
html.desktop-reskin #navbar > .omni__wrp-input:not(.omni__wrp-input--scrolled) .omni__submit {
|
||||||
|
display: none !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
html.desktop-reskin #navbar > .omni__wrp-input:not(.omni__wrp-input--scrolled) .omni__btn-clear {
|
||||||
|
right: 12px !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
@media (max-width: 1400px) {
|
||||||
|
html.desktop-reskin #navbar > .omni__wrp-input:not(.omni__wrp-input--scrolled) {
|
||||||
|
flex-basis: 260px;
|
||||||
|
min-width: 190px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@media (max-width: 1180px) {
|
||||||
|
html.desktop-reskin {
|
||||||
|
--desktop-topbar-pad-x: 12px;
|
||||||
|
}
|
||||||
|
|
||||||
|
html.desktop-reskin #navbar {
|
||||||
|
gap: 3px;
|
||||||
|
}
|
||||||
|
|
||||||
|
html.desktop-reskin #navbar > .omni__wrp-input:not(.omni__wrp-input--scrolled) {
|
||||||
|
margin-left: 0 !important;
|
||||||
|
flex-basis: 100%;
|
||||||
|
max-width: 100%;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
html.desktop-reskin #navbar .form-control,
|
||||||
|
html.desktop-reskin #navbar input[type="search"] {
|
||||||
|
background: rgba(8, 14, 24, 0.96) !important;
|
||||||
|
border: 1px solid rgba(102, 155, 210, 0.42) !important;
|
||||||
|
color: #e4effb !important;
|
||||||
|
border-radius: 12px !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
html.desktop-reskin #navbar .form-control::placeholder,
|
||||||
|
html.desktop-reskin #navbar input[type="search"]::placeholder {
|
||||||
|
color: #96adc8 !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
html.desktop-reskin #navbar .btn {
|
||||||
|
border-radius: 12px !important;
|
||||||
|
border-color: rgba(102, 155, 210, 0.42) !important;
|
||||||
|
background: rgba(14, 24, 40, 0.95) !important;
|
||||||
|
color: #d9e8fb !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
html.desktop-reskin #navbar .omni__submit,
|
||||||
|
html.desktop-reskin #navbar .omni__submit.ve-btn {
|
||||||
|
display: none !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
html.desktop-reskin #navbar .dropdown-menu,
|
||||||
|
html.desktop-reskin #navbar .ve-dropdown-menu {
|
||||||
|
border: 1px solid rgba(102, 155, 210, 0.42) !important;
|
||||||
|
border-radius: 12px !important;
|
||||||
|
background: linear-gradient(180deg, rgba(13, 22, 37, 0.98), rgba(9, 17, 30, 0.99)) !important;
|
||||||
|
padding: 4px !important;
|
||||||
|
box-shadow: 0 12px 24px rgba(0, 0, 0, 0.34);
|
||||||
|
z-index: 3200 !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
html.desktop-reskin #navbar .ve-dropdown-menu--top {
|
||||||
|
margin-top: 4px !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
html.desktop-reskin #navbar .ve-dropdown-menu--side {
|
||||||
|
margin-left: 4px !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
html.desktop-reskin #navbar .dropdown-menu > li > a,
|
||||||
|
html.desktop-reskin #navbar .ve-dropdown-menu > li > a,
|
||||||
|
html.desktop-reskin #navbar .ve-dropdown-menu > li > span {
|
||||||
|
color: #dce9fa !important;
|
||||||
|
border-radius: 8px;
|
||||||
|
padding: 6px 12px !important;
|
||||||
|
transition: background-color 100ms ease, color 100ms ease;
|
||||||
|
}
|
||||||
|
|
||||||
|
html.desktop-reskin #navbar .dropdown-menu > li > a:hover,
|
||||||
|
html.desktop-reskin #navbar .dropdown-menu > li > a:focus,
|
||||||
|
html.desktop-reskin #navbar .ve-dropdown-menu > li > a:hover,
|
||||||
|
html.desktop-reskin #navbar .ve-dropdown-menu > li > a:focus,
|
||||||
|
html.desktop-reskin #navbar .ve-dropdown-menu > li > span:hover,
|
||||||
|
html.desktop-reskin #navbar .ve-dropdown-menu > li > span:focus {
|
||||||
|
background: rgba(58, 108, 170, 0.34) !important;
|
||||||
|
color: #f2f8ff !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
html.desktop-reskin #navbar .dropdown-menu > li.active > a,
|
||||||
|
html.desktop-reskin #navbar .ve-dropdown-menu > li.active > a,
|
||||||
|
html.desktop-reskin #navbar .ve-dropdown-menu > li.active > span {
|
||||||
|
background: linear-gradient(140deg, rgba(36, 124, 202, 0.92), rgba(28, 95, 161, 0.94)) !important;
|
||||||
|
color: #f5fbff !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
html.desktop-reskin #navbar .dropdown-menu > li.disabled > a,
|
||||||
|
html.desktop-reskin #navbar .ve-dropdown-menu > li.disabled > a,
|
||||||
|
html.desktop-reskin #navbar .ve-dropdown-menu > li.disabled > span {
|
||||||
|
color: #8aa2bd !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
html.desktop-reskin #navbar .ve-dropdown-divider,
|
||||||
|
html.desktop-reskin #navbar .dropdown-menu .divider {
|
||||||
|
margin: 6px 4px !important;
|
||||||
|
height: 1px;
|
||||||
|
background: rgba(106, 156, 213, 0.42) !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
html.desktop-reskin #navbar .ve-dropdown-ext-link > .glyphicon {
|
||||||
|
color: #9ec9f3 !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
html.desktop-reskin .input-xs,
|
||||||
|
html.desktop-reskin .form-control,
|
||||||
|
html.desktop-reskin select,
|
||||||
|
html.desktop-reskin input,
|
||||||
|
html.desktop-reskin textarea {
|
||||||
|
border-radius: 8px !important;
|
||||||
|
border-color: rgba(115, 170, 226, 0.45) !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
html.desktop-reskin .btn,
|
||||||
|
html.desktop-reskin .ve-btn {
|
||||||
|
border-radius: 9px !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
html.desktop-reskin .omni__wrp-output {
|
||||||
|
position: relative;
|
||||||
|
z-index: 3400 !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
html.desktop-reskin .omni__output {
|
||||||
|
border: 1px solid rgba(114, 171, 232, 0.45) !important;
|
||||||
|
border-radius: 12px !important;
|
||||||
|
background: linear-gradient(180deg, rgba(15, 25, 41, 0.98), rgba(9, 17, 29, 0.99)) !important;
|
||||||
|
box-shadow: 0 16px 30px rgba(0, 0, 0, 0.42);
|
||||||
|
color: #dce9f8 !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
html.desktop-reskin .omni__output .ve-btn,
|
||||||
|
html.desktop-reskin .omni__output .btn {
|
||||||
|
border: 1px solid rgba(112, 167, 225, 0.42) !important;
|
||||||
|
background: rgba(22, 36, 56, 0.92) !important;
|
||||||
|
color: #dce9f8 !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
html.desktop-reskin .omni__output .ve-btn.active,
|
||||||
|
html.desktop-reskin .omni__output .btn.active,
|
||||||
|
html.desktop-reskin .omni__output .ve-btn:hover,
|
||||||
|
html.desktop-reskin .omni__output .btn:hover {
|
||||||
|
background: rgba(53, 95, 145, 0.58) !important;
|
||||||
|
border-color: rgba(139, 204, 255, 0.72) !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
html.desktop-reskin .omni__row-result {
|
||||||
|
padding: 3px 6px;
|
||||||
|
border-radius: 8px;
|
||||||
|
border-bottom: 1px solid rgba(105, 151, 205, 0.3);
|
||||||
|
color: #d7e6f7 !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
html.desktop-reskin .omni__row-result:nth-child(odd) {
|
||||||
|
background: rgba(43, 71, 105, 0.38) !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
html.desktop-reskin .omni__row-result:hover {
|
||||||
|
background: rgba(63, 103, 151, 0.52) !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
html.desktop-reskin .omni__lnk-name {
|
||||||
|
color: #8ec9ff !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
html.desktop-reskin .omni__disp-srd,
|
||||||
|
html.desktop-reskin .omni__disp-source-marker,
|
||||||
|
html.desktop-reskin .omni__wrp-page {
|
||||||
|
color: #aecaec !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
html.desktop-reskin .omni__wrp-paginate {
|
||||||
|
margin-top: 4px;
|
||||||
|
padding-top: 6px;
|
||||||
|
border-top: 1px solid rgba(107, 156, 211, 0.35);
|
||||||
|
}
|
||||||
|
|
||||||
|
html.desktop-reskin .omni__paginate-ctrl {
|
||||||
|
color: #c8def8;
|
||||||
|
}
|
||||||
|
|
||||||
|
html.desktop-reskin .omni__paginate-ctrl:hover {
|
||||||
|
color: #8dc9ff !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
html.desktop-reskin .list,
|
||||||
|
html.desktop-reskin .list-display-only,
|
||||||
|
html.desktop-reskin .lst__row,
|
||||||
|
html.desktop-reskin .ui-list__row,
|
||||||
|
html.desktop-reskin .list li {
|
||||||
|
color: #dce8f8;
|
||||||
|
}
|
||||||
|
|
||||||
|
html.desktop-reskin .lst__row,
|
||||||
|
html.desktop-reskin .ui-list__row,
|
||||||
|
html.desktop-reskin .list li,
|
||||||
|
html.desktop-reskin .lst__row-border,
|
||||||
|
html.desktop-reskin .lst__row-inner,
|
||||||
|
html.desktop-reskin .veapp__list-row {
|
||||||
|
border-radius: 10px;
|
||||||
|
}
|
||||||
|
|
||||||
|
html.desktop-reskin .lst__row-border,
|
||||||
|
html.desktop-reskin .lst__row-inner,
|
||||||
|
html.desktop-reskin .veapp__list-row,
|
||||||
|
html.desktop-reskin .lst__wrp-cells {
|
||||||
|
border-color: rgba(112, 165, 222, 0.42) !important;
|
||||||
|
color: #dce8f8 !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
html.desktop-reskin .list .lst__row:nth-child(odd) .lst__row-border,
|
||||||
|
html.desktop-reskin .list .lst__row:nth-child(odd) .lst__row-inner,
|
||||||
|
html.desktop-reskin .list .lst__row:nth-child(odd).lst__row-border,
|
||||||
|
html.desktop-reskin .list .lst__row:nth-child(odd).veapp__list-row {
|
||||||
|
background: rgba(34, 57, 86, 0.36);
|
||||||
|
}
|
||||||
|
|
||||||
|
html.desktop-reskin .list .lst__row:nth-child(even) .lst__row-border,
|
||||||
|
html.desktop-reskin .list .lst__row:nth-child(even) .lst__row-inner,
|
||||||
|
html.desktop-reskin .list .lst__row:nth-child(even).lst__row-border,
|
||||||
|
html.desktop-reskin .list .lst__row:nth-child(even).veapp__list-row {
|
||||||
|
background: rgba(18, 30, 48, 0.38);
|
||||||
|
}
|
||||||
|
|
||||||
|
html.desktop-reskin .list .lst__row:hover .lst__row-border,
|
||||||
|
html.desktop-reskin .list .lst__row:hover .lst__row-inner,
|
||||||
|
html.desktop-reskin .list .lst__row:hover.lst__row-border,
|
||||||
|
html.desktop-reskin .list .lst__row:hover.veapp__list-row {
|
||||||
|
background: rgba(64, 105, 151, 0.48) !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
html.desktop-reskin .lst__row a,
|
||||||
|
html.desktop-reskin .veapp__list-row a,
|
||||||
|
html.desktop-reskin .lst__wrp-cells a {
|
||||||
|
color: inherit;
|
||||||
|
}
|
||||||
|
|
||||||
|
html.desktop-reskin .lst__form-top > *,
|
||||||
|
html.desktop-reskin .lst__form-top .form-control,
|
||||||
|
html.desktop-reskin .lst__form-top .btn,
|
||||||
|
html.desktop-reskin .lst__form-top .ve-btn {
|
||||||
|
border-color: rgba(112, 167, 225, 0.48) !important;
|
||||||
|
background: rgba(15, 25, 40, 0.94) !important;
|
||||||
|
color: #dce8f8 !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
html.desktop-reskin .lst__form-top .form-control::placeholder {
|
||||||
|
color: #99b2cf !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
html.desktop-reskin .fltr__pill,
|
||||||
|
html.desktop-reskin .fltr__mini-pill,
|
||||||
|
html.desktop-reskin .fltr__btn_nest {
|
||||||
|
border-color: rgba(109, 163, 221, 0.45) !important;
|
||||||
|
background: rgba(20, 32, 50, 0.92) !important;
|
||||||
|
color: #dce8f8 !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
html.desktop-reskin .fltr__pill[data-state="yes"],
|
||||||
|
html.desktop-reskin .fltr__mini-pill[data-state="yes"] {
|
||||||
|
background: linear-gradient(140deg, rgba(38, 120, 196, 0.9), rgba(27, 89, 151, 0.94)) !important;
|
||||||
|
border-color: rgba(131, 196, 255, 0.84) !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
html.desktop-reskin .fltr__pill[data-state="no"],
|
||||||
|
html.desktop-reskin .fltr__mini-pill[data-state="no"] {
|
||||||
|
background: linear-gradient(140deg, rgba(136, 55, 55, 0.92), rgba(109, 44, 44, 0.94)) !important;
|
||||||
|
border-color: rgba(206, 132, 132, 0.7) !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
html.desktop-reskin .wrp-stats-table {
|
||||||
|
border-top: 1px solid rgba(114, 171, 232, 0.5) !important;
|
||||||
|
border-bottom: 1px solid rgba(114, 171, 232, 0.5) !important;
|
||||||
|
background: rgba(9, 15, 25, 0.5);
|
||||||
|
}
|
||||||
|
|
||||||
|
html.desktop-reskin .table,
|
||||||
|
html.desktop-reskin table {
|
||||||
|
color: #dce8f8;
|
||||||
|
}
|
||||||
|
|
||||||
|
html.desktop-reskin .table > thead > tr > th,
|
||||||
|
html.desktop-reskin .table > tbody > tr > th,
|
||||||
|
html.desktop-reskin .table > tfoot > tr > th,
|
||||||
|
html.desktop-reskin .table > thead > tr > td,
|
||||||
|
html.desktop-reskin .table > tbody > tr > td,
|
||||||
|
html.desktop-reskin .table > tfoot > tr > td,
|
||||||
|
html.desktop-reskin table > thead > tr > th,
|
||||||
|
html.desktop-reskin table > tbody > tr > th,
|
||||||
|
html.desktop-reskin table > tfoot > tr > th,
|
||||||
|
html.desktop-reskin table > thead > tr > td,
|
||||||
|
html.desktop-reskin table > tbody > tr > td,
|
||||||
|
html.desktop-reskin table > tfoot > tr > td {
|
||||||
|
border-color: rgba(104, 154, 209, 0.42) !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
html.desktop-reskin .table > thead > tr > th,
|
||||||
|
html.desktop-reskin table > thead > tr > th {
|
||||||
|
background: rgba(33, 59, 92, 0.62);
|
||||||
|
color: #e8f2ff;
|
||||||
|
}
|
||||||
|
|
||||||
|
html.desktop-reskin .table-striped > tbody > tr:nth-of-type(odd),
|
||||||
|
html.desktop-reskin .stripe-odd-table > tbody > tr:nth-child(odd),
|
||||||
|
html.desktop-reskin .stripe-even-table > tbody > tr:nth-child(even),
|
||||||
|
html.desktop-reskin .stripe-odd:nth-child(odd),
|
||||||
|
html.desktop-reskin .stripe-even:nth-child(even) {
|
||||||
|
background: rgba(42, 70, 104, 0.38) !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
html.desktop-reskin .hwin,
|
||||||
|
html.desktop-reskin .ve-window,
|
||||||
|
html.desktop-reskin .modal__inner {
|
||||||
|
border: 1px solid rgba(115, 170, 226, 0.35) !important;
|
||||||
|
border-radius: 12px !important;
|
||||||
|
box-shadow: 0 16px 28px rgba(0, 0, 0, 0.35);
|
||||||
|
}
|
||||||
|
`;
|
||||||
|
|
||||||
|
export interface WikiServer {
|
||||||
|
readonly rootDir: string;
|
||||||
|
readonly url: string;
|
||||||
|
close: () => Promise<void>;
|
||||||
|
}
|
||||||
|
|
||||||
|
function getWikiRootCandidates(): string[] {
|
||||||
|
const envDir = process.env.FIVEETOOLS_SRC_DIR;
|
||||||
|
|
||||||
|
return [
|
||||||
|
envDir,
|
||||||
|
path.resolve(app.getAppPath(), "..", "5etools-src"),
|
||||||
|
path.resolve(app.getAppPath(), "5etools-src"),
|
||||||
|
path.resolve(process.cwd(), "..", "5etools-src"),
|
||||||
|
path.resolve(process.cwd(), "5etools-src"),
|
||||||
|
path.resolve(process.resourcesPath, "5etools-src")
|
||||||
|
].filter((it): it is string => Boolean(it));
|
||||||
|
}
|
||||||
|
|
||||||
|
function resolveWikiRootDir(): string {
|
||||||
|
const candidates = getWikiRootCandidates();
|
||||||
|
|
||||||
|
for (const candidate of candidates) {
|
||||||
|
const indexPath = path.join(candidate, "5etools.html");
|
||||||
|
const dataPath = path.join(candidate, "data");
|
||||||
|
|
||||||
|
if (fs.existsSync(indexPath) && fs.existsSync(dataPath)) {
|
||||||
|
return candidate;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const formatted = candidates.map((it) => `- ${it}`).join("\n");
|
||||||
|
throw new Error(`Unable to locate 5etools-src directory. Checked:\n${formatted}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
function getMimeType(filePath: string): string {
|
||||||
|
const ext = path.extname(filePath).toLowerCase();
|
||||||
|
return MIME_TYPES[ext] ?? "application/octet-stream";
|
||||||
|
}
|
||||||
|
|
||||||
|
function normalizeExternalBaseUrl(url: string): string {
|
||||||
|
const trimmed = url.trim();
|
||||||
|
return `${trimmed.replace(/\/+$/, "")}/`;
|
||||||
|
}
|
||||||
|
|
||||||
|
function getRequestPath(requestUrl: string): string {
|
||||||
|
const parsed = new URL(requestUrl, "http://127.0.0.1");
|
||||||
|
return decodeURIComponent(parsed.pathname);
|
||||||
|
}
|
||||||
|
|
||||||
|
function getExternalImageUrl(requestUrl: string): URL | null {
|
||||||
|
if (!EXTERNAL_IMAGE_ROOT) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
let requestPath: string;
|
||||||
|
try {
|
||||||
|
requestPath = getRequestPath(requestUrl);
|
||||||
|
} catch {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!requestPath.startsWith("/img/")) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
const relativePath = requestPath.slice("/img/".length);
|
||||||
|
if (!relativePath) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
return new URL(relativePath, EXTERNAL_IMAGE_ROOT);
|
||||||
|
}
|
||||||
|
|
||||||
|
async function pTryServeExternalImage(requestUrl: string, res: http.ServerResponse): Promise<boolean> {
|
||||||
|
const externalImageUrl = getExternalImageUrl(requestUrl);
|
||||||
|
if (!externalImageUrl) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
const response = await fetch(externalImageUrl.toString());
|
||||||
|
if (!response.ok) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
const responseBuffer = Buffer.from(await response.arrayBuffer());
|
||||||
|
const contentType = response.headers.get("content-type") ?? getMimeType(externalImageUrl.pathname);
|
||||||
|
|
||||||
|
res.writeHead(200, {
|
||||||
|
"Content-Type": contentType,
|
||||||
|
"Cache-Control": "public, max-age=3600"
|
||||||
|
});
|
||||||
|
res.end(responseBuffer);
|
||||||
|
return true;
|
||||||
|
} catch {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function injectDesktopReskin(html: string): string {
|
||||||
|
if (html.includes("desktop-reskin-style")) {
|
||||||
|
return html;
|
||||||
|
}
|
||||||
|
|
||||||
|
const injection = [
|
||||||
|
`<script id="desktop-reskin-boot">${DESKTOP_RESKIN_BOOTSTRAP}</script>`,
|
||||||
|
`<style id="desktop-reskin-style">${DESKTOP_RESKIN_STYLE}</style>`
|
||||||
|
].join("");
|
||||||
|
|
||||||
|
if (!/<\/head>/i.test(html)) {
|
||||||
|
return `${injection}${html}`;
|
||||||
|
}
|
||||||
|
|
||||||
|
return html.replace(/<\/head>/i, `${injection}</head>`);
|
||||||
|
}
|
||||||
|
|
||||||
|
function resolveRequestPath(rootDir: string, requestUrl: string): string {
|
||||||
|
const reqPath = getRequestPath(requestUrl);
|
||||||
|
const normalizedReqPath = reqPath === "/" ? "/5etools.html" : reqPath;
|
||||||
|
|
||||||
|
const candidate = path.resolve(rootDir, `.${normalizedReqPath}`);
|
||||||
|
const rootResolved = path.resolve(rootDir);
|
||||||
|
const relative = path.relative(rootResolved, candidate);
|
||||||
|
|
||||||
|
if (relative.startsWith("..") || path.isAbsolute(relative)) {
|
||||||
|
throw new Error("Blocked path traversal attempt");
|
||||||
|
}
|
||||||
|
|
||||||
|
if (fs.existsSync(candidate) && fs.statSync(candidate).isDirectory()) {
|
||||||
|
const indexPath = path.join(candidate, "index.html");
|
||||||
|
if (fs.existsSync(indexPath)) {
|
||||||
|
return indexPath;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return candidate;
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function startWikiServer(): Promise<WikiServer> {
|
||||||
|
const rootDir = resolveWikiRootDir();
|
||||||
|
|
||||||
|
return await new Promise<WikiServer>((resolve, reject) => {
|
||||||
|
const server = http.createServer((req, res) => {
|
||||||
|
const url = req.url ?? "/";
|
||||||
|
|
||||||
|
let filePath: string;
|
||||||
|
try {
|
||||||
|
filePath = resolveRequestPath(rootDir, url);
|
||||||
|
} catch {
|
||||||
|
res.writeHead(403, { "Content-Type": "text/plain; charset=utf-8" });
|
||||||
|
res.end("Forbidden");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
fs.readFile(filePath, (err, data) => {
|
||||||
|
if (err) {
|
||||||
|
if (err.code === "ENOENT") {
|
||||||
|
void pTryServeExternalImage(url, res).then((isServed) => {
|
||||||
|
if (isServed) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
res.writeHead(404, { "Content-Type": "text/plain; charset=utf-8" });
|
||||||
|
res.end("Not Found");
|
||||||
|
});
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
res.writeHead(500, { "Content-Type": "text/plain; charset=utf-8" });
|
||||||
|
res.end("Internal Server Error");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const extension = path.extname(filePath).toLowerCase();
|
||||||
|
const transformedBody =
|
||||||
|
extension === ".html" && SHOULD_INJECT_RESKIN ? injectDesktopReskin(data.toString("utf8")) : data;
|
||||||
|
|
||||||
|
res.writeHead(200, {
|
||||||
|
"Content-Type": getMimeType(filePath),
|
||||||
|
"Cache-Control": "no-store"
|
||||||
|
});
|
||||||
|
|
||||||
|
if (typeof transformedBody === "string") {
|
||||||
|
res.end(transformedBody, "utf8");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
res.end(transformedBody);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
server.once("error", reject);
|
||||||
|
|
||||||
|
server.listen(0, "127.0.0.1", () => {
|
||||||
|
const address = server.address();
|
||||||
|
if (!address || typeof address === "string") {
|
||||||
|
reject(new Error("Failed to resolve local wiki server address"));
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const url = `http://127.0.0.1:${address.port}`;
|
||||||
|
|
||||||
|
resolve({
|
||||||
|
rootDir,
|
||||||
|
url,
|
||||||
|
close: () =>
|
||||||
|
new Promise<void>((closeResolve, closeReject) => {
|
||||||
|
server.close((closeErr) => {
|
||||||
|
if (closeErr) {
|
||||||
|
closeReject(closeErr);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
closeResolve();
|
||||||
|
});
|
||||||
|
})
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
@@ -0,0 +1,5 @@
|
|||||||
|
import { contextBridge, ipcRenderer } from "electron";
|
||||||
|
|
||||||
|
contextBridge.exposeInMainWorld("desktopApi", {
|
||||||
|
getWikiUrl: () => ipcRenderer.invoke("wiki:get-url") as Promise<string>
|
||||||
|
});
|
||||||
@@ -0,0 +1,91 @@
|
|||||||
|
import { useEffect, useMemo, useState } from "react";
|
||||||
|
import { WIKI_NAV_SECTIONS } from "./wikiNav";
|
||||||
|
|
||||||
|
const DEFAULT_PAGE = "/5etools.html";
|
||||||
|
|
||||||
|
export default function App() {
|
||||||
|
const [wikiUrl, setWikiUrl] = useState<string>("");
|
||||||
|
const [error, setError] = useState<string>("");
|
||||||
|
const [currentPath, setCurrentPath] = useState<string>(DEFAULT_PAGE);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
window.desktopApi
|
||||||
|
.getWikiUrl()
|
||||||
|
.then((url) => setWikiUrl(url))
|
||||||
|
.catch((err: unknown) => {
|
||||||
|
const message = err instanceof Error ? err.message : "Unknown error";
|
||||||
|
setError(message);
|
||||||
|
});
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
const frameSrc = useMemo(() => {
|
||||||
|
if (!wikiUrl) {
|
||||||
|
return "";
|
||||||
|
}
|
||||||
|
|
||||||
|
return new URL(currentPath, `${wikiUrl}/`).toString();
|
||||||
|
}, [currentPath, wikiUrl]);
|
||||||
|
|
||||||
|
if (error) {
|
||||||
|
return (
|
||||||
|
<main className="status-wrap">
|
||||||
|
<section className="status-panel">
|
||||||
|
<h1>5etools Desktop failed to start</h1>
|
||||||
|
<p>{error}</p>
|
||||||
|
</section>
|
||||||
|
</main>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!wikiUrl) {
|
||||||
|
return (
|
||||||
|
<main className="status-wrap">
|
||||||
|
<section className="status-panel">
|
||||||
|
<h1>Starting 5etools Desktop</h1>
|
||||||
|
<p>Loading local 5etools wiki...</p>
|
||||||
|
</section>
|
||||||
|
</main>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<main className="shell" aria-label="5etools Desktop Wiki Shell">
|
||||||
|
<aside className="sidebar">
|
||||||
|
<header className="sidebar-brand">
|
||||||
|
<p className="sidebar-kicker">5etools Desktop</p>
|
||||||
|
<h1 className="sidebar-title">Compendium</h1>
|
||||||
|
<p className="sidebar-copy">5etools Desktop by <b>notBoris</b>.</p>
|
||||||
|
<p className="sidebar-copy-subtitle">This application is not affiliated with the 5etools Project. 5etools® is a Registered Trademark of The Norr Group Ltd. All rights reserved.</p>
|
||||||
|
</header>
|
||||||
|
|
||||||
|
<nav className="sidebar-nav" aria-label="Wiki Sections">
|
||||||
|
{WIKI_NAV_SECTIONS.map((section) => (
|
||||||
|
<section key={section.title} className="nav-section">
|
||||||
|
<h2>{section.title}</h2>
|
||||||
|
<ul>
|
||||||
|
{section.items.map((item) => (
|
||||||
|
<li key={item.path}>
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
className={item.path === currentPath ? "nav-item is-active" : "nav-item"}
|
||||||
|
onClick={() => setCurrentPath(item.path)}
|
||||||
|
>
|
||||||
|
<span className="nav-item-label">{item.label}</span>
|
||||||
|
<span className="nav-item-desc">{item.description}</span>
|
||||||
|
</button>
|
||||||
|
</li>
|
||||||
|
))}
|
||||||
|
</ul>
|
||||||
|
</section>
|
||||||
|
))}
|
||||||
|
</nav>
|
||||||
|
</aside>
|
||||||
|
|
||||||
|
<section className="workspace">
|
||||||
|
<section className="frame-shell frame-shell--full">
|
||||||
|
<iframe className="wiki-frame" src={frameSrc} title="5etools Wiki" />
|
||||||
|
</section>
|
||||||
|
</section>
|
||||||
|
</main>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -0,0 +1,10 @@
|
|||||||
|
import React from "react";
|
||||||
|
import { createRoot } from "react-dom/client";
|
||||||
|
import App from "./App";
|
||||||
|
import "./styles.css";
|
||||||
|
|
||||||
|
createRoot(document.getElementById("root") as HTMLElement).render(
|
||||||
|
<React.StrictMode>
|
||||||
|
<App />
|
||||||
|
</React.StrictMode>
|
||||||
|
);
|
||||||
@@ -0,0 +1,250 @@
|
|||||||
|
:root {
|
||||||
|
--bg-0: #070d15;
|
||||||
|
--bg-1: #0f1a2c;
|
||||||
|
--bg-2: #17243a;
|
||||||
|
--panel-0: rgba(10, 16, 28, 0.86);
|
||||||
|
--panel-1: rgba(23, 36, 58, 0.88);
|
||||||
|
--line: rgba(130, 178, 228, 0.24);
|
||||||
|
--line-strong: rgba(130, 178, 228, 0.5);
|
||||||
|
--text-0: #f4f8fd;
|
||||||
|
--text-1: #c2d0e4;
|
||||||
|
--text-2: #90a3bf;
|
||||||
|
--accent: #60c4ff;
|
||||||
|
--accent-strong: #8fd8ff;
|
||||||
|
--accent-warm: #e8b273;
|
||||||
|
--shadow: 0 16px 42px rgba(0, 0, 0, 0.42);
|
||||||
|
color: var(--text-0);
|
||||||
|
background: var(--bg-0);
|
||||||
|
font-family: "Avenir Next", "Trebuchet MS", "Segoe UI", sans-serif;
|
||||||
|
}
|
||||||
|
|
||||||
|
*,
|
||||||
|
*::before,
|
||||||
|
*::after {
|
||||||
|
box-sizing: border-box;
|
||||||
|
}
|
||||||
|
|
||||||
|
html,
|
||||||
|
body,
|
||||||
|
#root {
|
||||||
|
width: 100%;
|
||||||
|
height: 100%;
|
||||||
|
margin: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
body {
|
||||||
|
overflow: hidden;
|
||||||
|
background:
|
||||||
|
radial-gradient(circle at 15% -5%, rgba(62, 117, 194, 0.45), transparent 42%),
|
||||||
|
radial-gradient(circle at 86% 0%, rgba(235, 167, 86, 0.4), transparent 40%),
|
||||||
|
linear-gradient(165deg, var(--bg-0) 0%, #0b1322 100%);
|
||||||
|
}
|
||||||
|
|
||||||
|
.shell {
|
||||||
|
width: 100%;
|
||||||
|
height: 100%;
|
||||||
|
display: grid;
|
||||||
|
grid-template-columns: 320px 1fr;
|
||||||
|
}
|
||||||
|
|
||||||
|
.sidebar {
|
||||||
|
border-right: 1px solid var(--line);
|
||||||
|
background: linear-gradient(180deg, var(--panel-0) 0%, rgba(9, 14, 24, 0.88) 100%);
|
||||||
|
backdrop-filter: blur(18px);
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
min-height: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.sidebar-brand {
|
||||||
|
padding: 24px 20px 16px;
|
||||||
|
border-bottom: 1px solid var(--line);
|
||||||
|
}
|
||||||
|
|
||||||
|
.sidebar-kicker {
|
||||||
|
margin: 0 0 6px;
|
||||||
|
color: var(--accent-warm);
|
||||||
|
font-size: 0.78rem;
|
||||||
|
letter-spacing: 0.16em;
|
||||||
|
text-transform: uppercase;
|
||||||
|
}
|
||||||
|
|
||||||
|
.sidebar-title {
|
||||||
|
margin: 0;
|
||||||
|
font-size: 1.45rem;
|
||||||
|
letter-spacing: 0.01em;
|
||||||
|
}
|
||||||
|
|
||||||
|
.sidebar-copy {
|
||||||
|
margin: 10px 0 0;
|
||||||
|
color: var(--text-1);
|
||||||
|
font-size: 0.9rem;
|
||||||
|
line-height: 1.4;
|
||||||
|
}
|
||||||
|
|
||||||
|
.sidebar-copy-subtitle {
|
||||||
|
margin: 5px 0 0;
|
||||||
|
color: var(--text-1);
|
||||||
|
font-size: 0.6rem;
|
||||||
|
line-height: 1.4;
|
||||||
|
}
|
||||||
|
|
||||||
|
.sidebar-nav {
|
||||||
|
padding: 16px 14px 24px;
|
||||||
|
overflow: auto;
|
||||||
|
}
|
||||||
|
|
||||||
|
.nav-section + .nav-section {
|
||||||
|
margin-top: 18px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.nav-section h2 {
|
||||||
|
margin: 0 0 10px;
|
||||||
|
padding: 0 8px;
|
||||||
|
color: var(--text-2);
|
||||||
|
font-size: 0.72rem;
|
||||||
|
text-transform: uppercase;
|
||||||
|
letter-spacing: 0.14em;
|
||||||
|
font-weight: 700;
|
||||||
|
}
|
||||||
|
|
||||||
|
.nav-section ul {
|
||||||
|
margin: 0;
|
||||||
|
padding: 0;
|
||||||
|
list-style: none;
|
||||||
|
display: grid;
|
||||||
|
gap: 6px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.nav-item {
|
||||||
|
width: 100%;
|
||||||
|
border: 1px solid transparent;
|
||||||
|
background: rgba(24, 40, 63, 0.45);
|
||||||
|
border-radius: 12px;
|
||||||
|
padding: 10px 12px;
|
||||||
|
color: var(--text-1);
|
||||||
|
text-align: left;
|
||||||
|
cursor: pointer;
|
||||||
|
transition:
|
||||||
|
transform 120ms ease,
|
||||||
|
border-color 120ms ease,
|
||||||
|
background-color 120ms ease;
|
||||||
|
}
|
||||||
|
|
||||||
|
.nav-item:hover {
|
||||||
|
transform: translateX(2px);
|
||||||
|
border-color: var(--line-strong);
|
||||||
|
background: rgba(31, 54, 83, 0.75);
|
||||||
|
}
|
||||||
|
|
||||||
|
.nav-item.is-active {
|
||||||
|
border-color: rgba(120, 205, 255, 0.75);
|
||||||
|
background: linear-gradient(140deg, rgba(63, 132, 207, 0.38), rgba(36, 75, 116, 0.82));
|
||||||
|
}
|
||||||
|
|
||||||
|
.nav-item-label {
|
||||||
|
display: block;
|
||||||
|
font-size: 0.92rem;
|
||||||
|
font-weight: 650;
|
||||||
|
color: var(--text-0);
|
||||||
|
}
|
||||||
|
|
||||||
|
.nav-item-desc {
|
||||||
|
margin-top: 3px;
|
||||||
|
display: block;
|
||||||
|
font-size: 0.76rem;
|
||||||
|
color: var(--text-2);
|
||||||
|
}
|
||||||
|
|
||||||
|
.workspace {
|
||||||
|
min-width: 0;
|
||||||
|
min-height: 0;
|
||||||
|
padding: 14px;
|
||||||
|
display: grid;
|
||||||
|
grid-template-rows: 1fr;
|
||||||
|
gap: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.frame-shell {
|
||||||
|
min-height: 0;
|
||||||
|
border: 1px solid var(--line);
|
||||||
|
border-radius: 16px;
|
||||||
|
overflow: hidden;
|
||||||
|
box-shadow: var(--shadow);
|
||||||
|
background: rgba(0, 0, 0, 0.35);
|
||||||
|
animation: frame-enter 220ms ease;
|
||||||
|
}
|
||||||
|
|
||||||
|
.frame-shell--full {
|
||||||
|
height: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
|
.wiki-frame {
|
||||||
|
width: 100%;
|
||||||
|
height: 100%;
|
||||||
|
border: 0;
|
||||||
|
display: block;
|
||||||
|
}
|
||||||
|
|
||||||
|
.status-wrap {
|
||||||
|
width: 100%;
|
||||||
|
height: 100%;
|
||||||
|
display: grid;
|
||||||
|
place-items: center;
|
||||||
|
padding: 24px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.status-panel {
|
||||||
|
width: min(680px, 100%);
|
||||||
|
border: 1px solid var(--line-strong);
|
||||||
|
border-radius: 12px;
|
||||||
|
padding: 20px;
|
||||||
|
background: linear-gradient(145deg, #18273f 0%, #101726 100%);
|
||||||
|
box-shadow: var(--shadow);
|
||||||
|
}
|
||||||
|
|
||||||
|
.status-panel h1 {
|
||||||
|
margin: 0 0 8px;
|
||||||
|
font-size: 1.15rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.status-panel p {
|
||||||
|
margin: 0;
|
||||||
|
color: var(--text-1);
|
||||||
|
}
|
||||||
|
|
||||||
|
@keyframes frame-enter {
|
||||||
|
from {
|
||||||
|
opacity: 0.5;
|
||||||
|
transform: translateY(8px);
|
||||||
|
}
|
||||||
|
|
||||||
|
to {
|
||||||
|
opacity: 1;
|
||||||
|
transform: translateY(0);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@media (width <= 1200px) {
|
||||||
|
.shell {
|
||||||
|
grid-template-columns: 268px 1fr;
|
||||||
|
}
|
||||||
|
|
||||||
|
.nav-item-desc {
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@media (width <= 900px) {
|
||||||
|
.shell {
|
||||||
|
grid-template-columns: 1fr;
|
||||||
|
}
|
||||||
|
|
||||||
|
.sidebar {
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.workspace {
|
||||||
|
padding: 12px;
|
||||||
|
}
|
||||||
|
}
|
||||||
Vendored
+9
@@ -0,0 +1,9 @@
|
|||||||
|
/// <reference types="vite/client" />
|
||||||
|
|
||||||
|
interface DesktopApi {
|
||||||
|
getWikiUrl: () => Promise<string>;
|
||||||
|
}
|
||||||
|
|
||||||
|
interface Window {
|
||||||
|
desktopApi: DesktopApi;
|
||||||
|
}
|
||||||
@@ -0,0 +1,41 @@
|
|||||||
|
export interface WikiNavItem {
|
||||||
|
label: string;
|
||||||
|
path: string;
|
||||||
|
description: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface WikiNavSection {
|
||||||
|
title: string;
|
||||||
|
items: WikiNavItem[];
|
||||||
|
}
|
||||||
|
|
||||||
|
export const WIKI_NAV_SECTIONS: WikiNavSection[] = [
|
||||||
|
{
|
||||||
|
title: "Core",
|
||||||
|
items: [
|
||||||
|
{ label: "Home", path: "/5etools.html", description: "Main dashboard and tools" },
|
||||||
|
{ label: "Search", path: "/search.html", description: "Global content search" },
|
||||||
|
{ label: "Quick Reference", path: "/quickreference.html", description: "Fast rules lookup" }
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: "Character",
|
||||||
|
items: [
|
||||||
|
{ label: "Classes", path: "/classes.html", description: "Class and subclass details" },
|
||||||
|
{ label: "Backgrounds", path: "/backgrounds.html", description: "Origin and backstory options" },
|
||||||
|
{ label: "Species", path: "/races.html", description: "Species and lineages" },
|
||||||
|
{ label: "Feats", path: "/feats.html", description: "Character feat selection" },
|
||||||
|
{ label: "Optional Features", path: "/optionalfeatures.html", description: "Expanded build options" }
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: "Compendium",
|
||||||
|
items: [
|
||||||
|
{ label: "Spells", path: "/spells.html", description: "Spell catalog and filters" },
|
||||||
|
{ label: "Items", path: "/items.html", description: "Equipment and magic items" },
|
||||||
|
{ label: "Bestiary", path: "/bestiary.html", description: "Creatures and stat blocks" },
|
||||||
|
{ label: "Rules", path: "/variantrules.html", description: "System and optional rules" },
|
||||||
|
{ label: "Tables", path: "/tables.html", description: "Reference tables and charts" }
|
||||||
|
]
|
||||||
|
}
|
||||||
|
];
|
||||||
@@ -0,0 +1,16 @@
|
|||||||
|
{
|
||||||
|
"compilerOptions": {
|
||||||
|
"target": "ES2022",
|
||||||
|
"lib": ["ES2022", "DOM", "DOM.Iterable"],
|
||||||
|
"module": "ESNext",
|
||||||
|
"moduleResolution": "Bundler",
|
||||||
|
"jsx": "react-jsx",
|
||||||
|
"strict": true,
|
||||||
|
"noEmit": true,
|
||||||
|
"skipLibCheck": true,
|
||||||
|
"resolveJsonModule": true,
|
||||||
|
"allowSyntheticDefaultImports": true,
|
||||||
|
"types": ["vite/client"]
|
||||||
|
},
|
||||||
|
"include": ["src/renderer", "src/renderer/vite-env.d.ts"]
|
||||||
|
}
|
||||||
@@ -0,0 +1,16 @@
|
|||||||
|
{
|
||||||
|
"compilerOptions": {
|
||||||
|
"target": "ES2022",
|
||||||
|
"lib": ["ES2022"],
|
||||||
|
"module": "CommonJS",
|
||||||
|
"moduleResolution": "Node",
|
||||||
|
"strict": true,
|
||||||
|
"esModuleInterop": true,
|
||||||
|
"skipLibCheck": true,
|
||||||
|
"resolveJsonModule": true,
|
||||||
|
"outDir": "dist-electron",
|
||||||
|
"rootDir": "src",
|
||||||
|
"types": ["node", "electron"]
|
||||||
|
},
|
||||||
|
"include": ["src/main/**/*.ts", "src/preload/**/*.ts"]
|
||||||
|
}
|
||||||
@@ -0,0 +1,12 @@
|
|||||||
|
import { defineConfig } from "vite";
|
||||||
|
import react from "@vitejs/plugin-react";
|
||||||
|
|
||||||
|
export default defineConfig({
|
||||||
|
base: "./",
|
||||||
|
plugins: [react()],
|
||||||
|
server: {
|
||||||
|
host: "127.0.0.1",
|
||||||
|
port: 5173,
|
||||||
|
strictPort: true
|
||||||
|
}
|
||||||
|
});
|
||||||
Reference in new issue
Block a user