Simplify UI and add batch build endpoint
This commit is contained in:
12
frontend/index.html
Normal file
12
frontend/index.html
Normal file
@@ -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>GeViSet Web</title>
|
||||
</head>
|
||||
<body>
|
||||
<div id="root"></div>
|
||||
<script type="module" src="/src/main.tsx"></script>
|
||||
</body>
|
||||
</html>
|
||||
1731
frontend/package-lock.json
generated
Normal file
1731
frontend/package-lock.json
generated
Normal file
File diff suppressed because it is too large
Load Diff
22
frontend/package.json
Normal file
22
frontend/package.json
Normal file
@@ -0,0 +1,22 @@
|
||||
{
|
||||
"name": "geviset-web",
|
||||
"version": "0.1.0",
|
||||
"private": true,
|
||||
"type": "module",
|
||||
"scripts": {
|
||||
"dev": "vite",
|
||||
"build": "tsc -b && vite build",
|
||||
"preview": "vite preview"
|
||||
},
|
||||
"dependencies": {
|
||||
"react": "^18.3.1",
|
||||
"react-dom": "^18.3.1"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@types/react": "^18.3.1",
|
||||
"@types/react-dom": "^18.3.1",
|
||||
"@vitejs/plugin-react": "^4.3.2",
|
||||
"typescript": "^5.6.2",
|
||||
"vite": "^5.4.8"
|
||||
}
|
||||
}
|
||||
158
frontend/src/App.tsx
Normal file
158
frontend/src/App.tsx
Normal file
@@ -0,0 +1,158 @@
|
||||
import { useState } from "react";
|
||||
|
||||
const API_BASE = (() => {
|
||||
const override = import.meta.env.VITE_API_BASE;
|
||||
if (override) return override;
|
||||
if (window.location.port === "5173") {
|
||||
return `${window.location.protocol}//${window.location.hostname}:8002`;
|
||||
}
|
||||
return window.location.origin;
|
||||
})();
|
||||
const BUILD_STAMP = new Date().toISOString();
|
||||
|
||||
type Slot = {
|
||||
id: string;
|
||||
label: string;
|
||||
accept: string;
|
||||
file: File | null;
|
||||
setFile: (file: File | null) => void;
|
||||
required?: boolean;
|
||||
};
|
||||
|
||||
function formatSize(file: File) {
|
||||
const kb = file.size / 1024;
|
||||
if (kb < 1024) return `${kb.toFixed(1)} KB`;
|
||||
return `${(kb / 1024).toFixed(1)} MB`;
|
||||
}
|
||||
|
||||
export default function App() {
|
||||
const [status, setStatus] = useState<string | null>(null);
|
||||
const [busyLabel, setBusyLabel] = useState<string | null>(null);
|
||||
|
||||
const [baseFile, setBaseFile] = useState<File | null>(null);
|
||||
const [serversFile, setServersFile] = useState<File | null>(null);
|
||||
const [actionsFile, setActionsFile] = useState<File | null>(null);
|
||||
const [outputsFile, setOutputsFile] = useState<File | null>(null);
|
||||
const [keyboardsFile, setKeyboardsFile] = useState<File | null>(null);
|
||||
const [loggingFile, setLoggingFile] = useState<File | null>(null);
|
||||
|
||||
const slots: Slot[] = [
|
||||
{ id: "base", label: "Base .set", accept: ".set", file: baseFile, setFile: setBaseFile, required: true },
|
||||
{ id: "servers", label: "Servers", accept: ".xlsx", file: serversFile, setFile: setServersFile },
|
||||
{ id: "actions", label: "Actions", accept: ".xlsx", file: actionsFile, setFile: setActionsFile },
|
||||
{ id: "outputs", label: "Outputs", accept: ".xlsx", file: outputsFile, setFile: setOutputsFile },
|
||||
{ id: "keyboards", label: "Keyboards", accept: ".xlsx", file: keyboardsFile, setFile: setKeyboardsFile },
|
||||
{ id: "logging", label: "Logging", accept: ".xlsx", file: loggingFile, setFile: setLoggingFile },
|
||||
];
|
||||
|
||||
const handleBuild = async () => {
|
||||
if (!baseFile) {
|
||||
setStatus("Select a base .set file.");
|
||||
return;
|
||||
}
|
||||
const body = new FormData();
|
||||
body.append("base_set", baseFile);
|
||||
if (serversFile) body.append("servers", serversFile);
|
||||
if (actionsFile) body.append("actions", actionsFile);
|
||||
if (outputsFile) body.append("outputs", outputsFile);
|
||||
if (keyboardsFile) body.append("keyboards", keyboardsFile);
|
||||
if (loggingFile) body.append("logging", loggingFile);
|
||||
|
||||
setStatus("Building combined .set...");
|
||||
setBusyLabel("Building .set");
|
||||
const controller = new AbortController();
|
||||
const timeoutId = window.setTimeout(() => controller.abort(), 300000);
|
||||
try {
|
||||
const res = await fetch(`${API_BASE}/api/batch/build`, {
|
||||
method: "POST",
|
||||
body,
|
||||
signal: controller.signal,
|
||||
});
|
||||
if (!res.ok) {
|
||||
setStatus(await res.text());
|
||||
return;
|
||||
}
|
||||
const blob = await res.blob();
|
||||
const url = URL.createObjectURL(blob);
|
||||
const link = document.createElement("a");
|
||||
link.href = url;
|
||||
link.download = "combined.set";
|
||||
link.click();
|
||||
URL.revokeObjectURL(url);
|
||||
setStatus("Combined .set downloaded.");
|
||||
} catch (error: any) {
|
||||
if (error?.name === "AbortError") {
|
||||
setStatus("Build timed out.");
|
||||
} else {
|
||||
setStatus("Build failed.");
|
||||
}
|
||||
} finally {
|
||||
window.clearTimeout(timeoutId);
|
||||
setBusyLabel(null);
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="shell simple">
|
||||
<header className="topbar">
|
||||
<div className="topbar__brand">
|
||||
<div className="logo">GV</div>
|
||||
<div>
|
||||
<div className="topbar__title">GeViSet Builder</div>
|
||||
<div className="topbar__subtitle">Batch .set generator <EFBFBD> {BUILD_STAMP}</div>
|
||||
</div>
|
||||
</div>
|
||||
<div className="topbar__actions">
|
||||
<div className="statusbar">
|
||||
{busyLabel && <span className="spinner" />}
|
||||
{busyLabel ?? status ?? "Ready."}
|
||||
</div>
|
||||
</div>
|
||||
</header>
|
||||
|
||||
<main className="simple__body">
|
||||
<section className="card">
|
||||
<div className="card__header">
|
||||
<div>
|
||||
<h2>Build Combined .set</h2>
|
||||
<p>Select the base .set and any Excel files you want to apply. Only selected files are used.</p>
|
||||
</div>
|
||||
<button className="chip" onClick={handleBuild}>
|
||||
Build .set
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<div className="card__content">
|
||||
{slots.map((slot) => (
|
||||
<div key={slot.id} className="file-block">
|
||||
<label className="file-row">
|
||||
{slot.label}{slot.required ? " *" : ""}
|
||||
<input
|
||||
type="file"
|
||||
accept={slot.accept}
|
||||
onChange={(e) => {
|
||||
const file = e.target.files?.[0] || null;
|
||||
slot.setFile(file);
|
||||
e.target.value = "";
|
||||
}}
|
||||
/>
|
||||
</label>
|
||||
{slot.file ? (
|
||||
<div className="file-meta">
|
||||
<span className="file-name">{slot.file.name}</span>
|
||||
<span className="file-size">{formatSize(slot.file)}</span>
|
||||
<button className="chip chip--ghost" onClick={() => slot.setFile(null)}>
|
||||
Clear
|
||||
</button>
|
||||
</div>
|
||||
) : (
|
||||
<div className="file-meta file-meta--empty">Not selected</div>
|
||||
)}
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</section>
|
||||
</main>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
10
frontend/src/main.tsx
Normal file
10
frontend/src/main.tsx
Normal file
@@ -0,0 +1,10 @@
|
||||
import React from "react";
|
||||
import ReactDOM from "react-dom/client";
|
||||
import App from "./App";
|
||||
import "./styles.css";
|
||||
|
||||
ReactDOM.createRoot(document.getElementById("root")!).render(
|
||||
<React.StrictMode>
|
||||
<App />
|
||||
</React.StrictMode>
|
||||
);
|
||||
531
frontend/src/styles.css
Normal file
531
frontend/src/styles.css
Normal file
@@ -0,0 +1,531 @@
|
||||
@import url("https://fonts.googleapis.com/css2?family=IBM+Plex+Sans:wght@400;500;600&family=IBM+Plex+Mono:wght@400;500&display=swap");
|
||||
|
||||
:root {
|
||||
--bg: #0a0d12;
|
||||
--panel: #0e141d;
|
||||
--panel-2: #0b1118;
|
||||
--grid: #1c2634;
|
||||
--edge: #2a374a;
|
||||
--text: #e6edf6;
|
||||
--muted: #90a0b3;
|
||||
--accent: #4de3b8;
|
||||
--accent-2: #3aa0ff;
|
||||
--danger: #ff5d5d;
|
||||
--chip: #121a26;
|
||||
--chip-border: #2b394f;
|
||||
font-family: "IBM Plex Sans", sans-serif;
|
||||
color: var(--text);
|
||||
background-color: var(--bg);
|
||||
}
|
||||
|
||||
* {
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
body {
|
||||
margin: 0;
|
||||
min-height: 100vh;
|
||||
background:
|
||||
linear-gradient(180deg, rgba(18, 25, 35, 0.7) 0%, rgba(8, 11, 16, 0.9) 100%),
|
||||
repeating-linear-gradient(
|
||||
90deg,
|
||||
rgba(255, 255, 255, 0.02) 0px,
|
||||
rgba(255, 255, 255, 0.02) 1px,
|
||||
transparent 1px,
|
||||
transparent 28px
|
||||
);
|
||||
}
|
||||
|
||||
input,
|
||||
button {
|
||||
font-family: inherit;
|
||||
color: inherit;
|
||||
}
|
||||
|
||||
.shell {
|
||||
min-height: 100vh;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
.topbar {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
padding: 10px 18px;
|
||||
border-bottom: 1px solid var(--edge);
|
||||
background: linear-gradient(180deg, #101723 0%, #0a0d12 100%);
|
||||
box-shadow: inset 0 -1px 0 rgba(255, 255, 255, 0.03);
|
||||
}
|
||||
|
||||
.topbar__brand {
|
||||
display: flex;
|
||||
gap: 12px;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.logo {
|
||||
width: 36px;
|
||||
height: 36px;
|
||||
border-radius: 6px;
|
||||
display: grid;
|
||||
place-items: center;
|
||||
font-weight: 600;
|
||||
background: linear-gradient(135deg, var(--accent) 0%, var(--accent-2) 100%);
|
||||
color: #041017;
|
||||
letter-spacing: 0.04em;
|
||||
}
|
||||
|
||||
.topbar__title {
|
||||
font-size: 14px;
|
||||
font-weight: 600;
|
||||
text-transform: uppercase;
|
||||
letter-spacing: 0.12em;
|
||||
}
|
||||
|
||||
.topbar__subtitle {
|
||||
font-size: 11px;
|
||||
color: var(--muted);
|
||||
}
|
||||
|
||||
.topbar__actions {
|
||||
display: flex;
|
||||
gap: 10px;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.statusbar {
|
||||
font-size: 11px;
|
||||
color: var(--muted);
|
||||
padding: 4px 10px;
|
||||
border-radius: 4px;
|
||||
border: 1px solid var(--edge);
|
||||
background: rgba(10, 15, 22, 0.8);
|
||||
text-transform: uppercase;
|
||||
letter-spacing: 0.08em;
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
}
|
||||
|
||||
.spinner {
|
||||
width: 10px;
|
||||
height: 10px;
|
||||
border-radius: 50%;
|
||||
border: 2px solid rgba(77, 227, 184, 0.25);
|
||||
border-top-color: var(--accent);
|
||||
animation: spin 0.8s linear infinite;
|
||||
}
|
||||
|
||||
.shell__body {
|
||||
display: grid;
|
||||
grid-template-columns: 240px 1fr;
|
||||
min-height: 0;
|
||||
flex: 1;
|
||||
}
|
||||
|
||||
.simple__body {
|
||||
flex: 1;
|
||||
padding: 24px;
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: flex-start;
|
||||
}
|
||||
|
||||
.card {
|
||||
width: min(900px, 100%);
|
||||
background: var(--panel);
|
||||
border: 1px solid var(--edge);
|
||||
border-radius: 10px;
|
||||
padding: 18px;
|
||||
box-shadow: 0 20px 40px rgba(0, 0, 0, 0.35);
|
||||
}
|
||||
|
||||
.card__header {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
gap: 16px;
|
||||
border-bottom: 1px solid var(--edge);
|
||||
padding-bottom: 12px;
|
||||
}
|
||||
|
||||
.card__header h2 {
|
||||
margin: 0;
|
||||
font-size: 18px;
|
||||
}
|
||||
|
||||
.card__header p {
|
||||
margin: 6px 0 0;
|
||||
color: var(--muted);
|
||||
font-size: 12px;
|
||||
}
|
||||
|
||||
.card__content {
|
||||
margin-top: 16px;
|
||||
display: grid;
|
||||
gap: 14px;
|
||||
}
|
||||
|
||||
.file-block {
|
||||
display: grid;
|
||||
gap: 6px;
|
||||
}
|
||||
|
||||
.file-meta {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 10px;
|
||||
font-size: 12px;
|
||||
color: var(--muted);
|
||||
flex-wrap: wrap;
|
||||
}
|
||||
|
||||
.file-meta--empty {
|
||||
color: rgba(144, 160, 179, 0.7);
|
||||
font-style: italic;
|
||||
}
|
||||
|
||||
.file-name {
|
||||
color: var(--text);
|
||||
font-family: \"IBM Plex Mono\", monospace;
|
||||
}
|
||||
|
||||
.file-size {
|
||||
font-family: \"IBM Plex Mono\", monospace;
|
||||
}
|
||||
|
||||
.sidebar {
|
||||
border-right: 1px solid var(--edge);
|
||||
padding: 14px;
|
||||
background: var(--panel-2);
|
||||
display: grid;
|
||||
gap: 12px;
|
||||
align-content: start;
|
||||
}
|
||||
|
||||
.sidebar__section {
|
||||
padding: 10px;
|
||||
border-radius: 6px;
|
||||
background: rgba(12, 18, 26, 0.9);
|
||||
border: 1px solid var(--edge);
|
||||
align-self: start;
|
||||
}
|
||||
|
||||
.sidebar__title {
|
||||
font-size: 11px;
|
||||
text-transform: uppercase;
|
||||
letter-spacing: 0.16em;
|
||||
color: var(--muted);
|
||||
margin-bottom: 10px;
|
||||
}
|
||||
|
||||
.sidebar__hint {
|
||||
font-size: 10px;
|
||||
color: var(--muted);
|
||||
margin-top: 8px;
|
||||
line-height: 1.4;
|
||||
}
|
||||
|
||||
.stat {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
padding: 6px 0;
|
||||
border-bottom: 1px solid var(--edge);
|
||||
font-size: 12px;
|
||||
}
|
||||
|
||||
.stat:last-child {
|
||||
border-bottom: none;
|
||||
}
|
||||
|
||||
.stat strong {
|
||||
color: var(--accent);
|
||||
font-family: "IBM Plex Mono", monospace;
|
||||
}
|
||||
|
||||
.workspace {
|
||||
padding: 14px 16px;
|
||||
display: grid;
|
||||
gap: 12px;
|
||||
overflow: auto;
|
||||
}
|
||||
|
||||
.panel {
|
||||
padding: 12px;
|
||||
border-radius: 6px;
|
||||
background: var(--panel);
|
||||
border: 1px solid var(--edge);
|
||||
box-shadow: 0 10px 30px rgba(0, 0, 0, 0.35);
|
||||
}
|
||||
|
||||
.panel--dark {
|
||||
background: #0a0f16;
|
||||
}
|
||||
|
||||
.panel__header {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
gap: 10px;
|
||||
}
|
||||
|
||||
.panel__header h2 {
|
||||
margin: 0 0 2px;
|
||||
font-size: 14px;
|
||||
text-transform: uppercase;
|
||||
letter-spacing: 0.1em;
|
||||
}
|
||||
|
||||
.panel__header p {
|
||||
margin: 0;
|
||||
color: var(--muted);
|
||||
font-size: 11px;
|
||||
}
|
||||
|
||||
.panel__actions {
|
||||
display: flex;
|
||||
gap: 8px;
|
||||
align-items: center;
|
||||
flex-wrap: wrap;
|
||||
}
|
||||
|
||||
.tab {
|
||||
border: 1px solid var(--chip-border);
|
||||
background: var(--chip);
|
||||
color: var(--muted);
|
||||
padding: 5px 12px;
|
||||
border-radius: 4px;
|
||||
cursor: pointer;
|
||||
font-size: 11px;
|
||||
text-transform: uppercase;
|
||||
letter-spacing: 0.1em;
|
||||
}
|
||||
|
||||
.tab--active {
|
||||
color: var(--text);
|
||||
border-color: var(--accent);
|
||||
box-shadow: inset 0 0 0 1px rgba(77, 227, 184, 0.4);
|
||||
}
|
||||
|
||||
.chip {
|
||||
border: 1px solid var(--chip-border);
|
||||
background: var(--chip);
|
||||
color: var(--text);
|
||||
padding: 5px 10px;
|
||||
border-radius: 4px;
|
||||
cursor: pointer;
|
||||
font-size: 11px;
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
gap: 6px;
|
||||
text-transform: uppercase;
|
||||
letter-spacing: 0.08em;
|
||||
}
|
||||
|
||||
.chip input,
|
||||
.file-row input {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.chip--ghost {
|
||||
background: transparent;
|
||||
border-color: var(--edge);
|
||||
color: var(--muted);
|
||||
}
|
||||
|
||||
.chip--danger {
|
||||
border-color: rgba(255, 93, 93, 0.5);
|
||||
color: #ffd1d1;
|
||||
background: rgba(255, 93, 93, 0.15);
|
||||
}
|
||||
|
||||
.chip:disabled {
|
||||
opacity: 0.5;
|
||||
cursor: not-allowed;
|
||||
}
|
||||
|
||||
.file-row {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
padding: 6px 8px;
|
||||
border-radius: 4px;
|
||||
background: #0b121b;
|
||||
border: 1px solid var(--edge);
|
||||
font-size: 11px;
|
||||
cursor: pointer;
|
||||
margin-bottom: 6px;
|
||||
text-transform: uppercase;
|
||||
letter-spacing: 0.08em;
|
||||
}
|
||||
|
||||
.file-row--button {
|
||||
width: 100%;
|
||||
justify-content: flex-start;
|
||||
gap: 6px;
|
||||
color: var(--text);
|
||||
}
|
||||
|
||||
.search {
|
||||
border: 1px solid var(--edge);
|
||||
border-radius: 4px;
|
||||
padding: 5px 8px;
|
||||
min-width: 160px;
|
||||
background: #0b121b;
|
||||
font-size: 11px;
|
||||
}
|
||||
|
||||
.table {
|
||||
margin-top: 10px;
|
||||
display: grid;
|
||||
gap: 4px;
|
||||
font-family: "IBM Plex Mono", monospace;
|
||||
font-size: 11px;
|
||||
}
|
||||
|
||||
.table__head,
|
||||
.table__row {
|
||||
display: grid;
|
||||
gap: 4px;
|
||||
}
|
||||
|
||||
.table--servers .table__head,
|
||||
.table--servers .table__row {
|
||||
grid-template-columns: 1.1fr 1fr 0.8fr 0.5fr 0.45fr 0.45fr 0.5fr;
|
||||
}
|
||||
|
||||
.table--actions .table__head,
|
||||
.table--actions .table__row {
|
||||
grid-template-columns: 1.1fr 0.9fr 1.1fr 0.9fr 1.2fr;
|
||||
}
|
||||
|
||||
.table--keyboards .table__head,
|
||||
.table--keyboards .table__row {
|
||||
grid-template-columns: 1.2fr 1fr 0.6fr;
|
||||
}
|
||||
|
||||
.table--outputs .table__head,
|
||||
.table--outputs .table__row {
|
||||
grid-template-columns: 0.5fr 0.6fr 1.6fr;
|
||||
}
|
||||
|
||||
.table__cell {
|
||||
padding: 5px 6px;
|
||||
border-radius: 4px;
|
||||
background: #0b121b;
|
||||
color: inherit;
|
||||
border: 1px solid transparent;
|
||||
text-align: left;
|
||||
}
|
||||
|
||||
.table__cell--head {
|
||||
font-weight: 600;
|
||||
text-transform: uppercase;
|
||||
font-size: 10px;
|
||||
letter-spacing: 0.1em;
|
||||
cursor: pointer;
|
||||
color: var(--muted);
|
||||
background: transparent;
|
||||
border: 1px solid var(--edge);
|
||||
}
|
||||
|
||||
.table__cell--text {
|
||||
background: transparent;
|
||||
border: 1px dashed var(--edge);
|
||||
}
|
||||
|
||||
.table__cell--actions {
|
||||
display: flex;
|
||||
gap: 6px;
|
||||
align-items: center;
|
||||
flex-wrap: wrap;
|
||||
background: transparent;
|
||||
border: none;
|
||||
padding: 0;
|
||||
}
|
||||
|
||||
.input {
|
||||
border: 1px solid transparent;
|
||||
background: #0b121b;
|
||||
}
|
||||
|
||||
.input:focus {
|
||||
outline: none;
|
||||
border-color: var(--accent);
|
||||
box-shadow: 0 0 0 1px rgba(77, 227, 184, 0.25);
|
||||
}
|
||||
|
||||
.toggle {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.toggle input {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.toggle span {
|
||||
width: 30px;
|
||||
height: 16px;
|
||||
border-radius: 999px;
|
||||
background: #182232;
|
||||
position: relative;
|
||||
border: 1px solid var(--edge);
|
||||
}
|
||||
|
||||
.toggle span::after {
|
||||
content: "";
|
||||
width: 12px;
|
||||
height: 12px;
|
||||
position: absolute;
|
||||
top: 1px;
|
||||
left: 1px;
|
||||
border-radius: 50%;
|
||||
background: #d7e3f1;
|
||||
transition: transform 0.2s ease;
|
||||
}
|
||||
|
||||
.toggle input:checked + span {
|
||||
background: rgba(77, 227, 184, 0.3);
|
||||
}
|
||||
|
||||
.toggle input:checked + span::after {
|
||||
transform: translateX(14px);
|
||||
background: var(--accent);
|
||||
}
|
||||
|
||||
@media (max-width: 1024px) {
|
||||
.shell__body {
|
||||
grid-template-columns: 1fr;
|
||||
}
|
||||
|
||||
.sidebar {
|
||||
border-right: none;
|
||||
border-bottom: 1px solid var(--edge);
|
||||
grid-template-columns: repeat(auto-fit, minmax(200px, 1fr));
|
||||
}
|
||||
}
|
||||
|
||||
@media (max-width: 900px) {
|
||||
.table__head,
|
||||
.table__row {
|
||||
grid-template-columns: repeat(auto-fit, minmax(140px, 1fr));
|
||||
}
|
||||
|
||||
.topbar {
|
||||
flex-direction: column;
|
||||
align-items: flex-start;
|
||||
gap: 10px;
|
||||
}
|
||||
}
|
||||
|
||||
@keyframes spin {
|
||||
from {
|
||||
transform: rotate(0deg);
|
||||
}
|
||||
to {
|
||||
transform: rotate(360deg);
|
||||
}
|
||||
}
|
||||
1798
frontend/src/templates/templateRules.json
Normal file
1798
frontend/src/templates/templateRules.json
Normal file
File diff suppressed because it is too large
Load Diff
16
frontend/tsconfig.json
Normal file
16
frontend/tsconfig.json
Normal file
@@ -0,0 +1,16 @@
|
||||
{
|
||||
"compilerOptions": {
|
||||
"target": "ES2020",
|
||||
"useDefineForClassFields": true,
|
||||
"lib": ["ES2020", "DOM", "DOM.Iterable"],
|
||||
"module": "ESNext",
|
||||
"skipLibCheck": true,
|
||||
"moduleResolution": "bundler",
|
||||
"resolveJsonModule": true,
|
||||
"isolatedModules": true,
|
||||
"noEmit": true,
|
||||
"jsx": "react-jsx",
|
||||
"strict": true
|
||||
},
|
||||
"include": ["src"]
|
||||
}
|
||||
10
frontend/tsconfig.node.json
Normal file
10
frontend/tsconfig.node.json
Normal file
@@ -0,0 +1,10 @@
|
||||
{
|
||||
"compilerOptions": {
|
||||
"composite": true,
|
||||
"skipLibCheck": true,
|
||||
"module": "ESNext",
|
||||
"moduleResolution": "bundler",
|
||||
"allowSyntheticDefaultImports": true
|
||||
},
|
||||
"include": ["vite.config.ts"]
|
||||
}
|
||||
10
frontend/vite.config.ts
Normal file
10
frontend/vite.config.ts
Normal file
@@ -0,0 +1,10 @@
|
||||
import { defineConfig } from "vite";
|
||||
import react from "@vitejs/plugin-react";
|
||||
|
||||
export default defineConfig({
|
||||
plugins: [react()],
|
||||
server: {
|
||||
host: "0.0.0.0",
|
||||
port: 5173
|
||||
}
|
||||
});
|
||||
Reference in New Issue
Block a user