First real commit. Setting up for full CI/CD and testing pipeline
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>Fast_Reader</title>
|
||||
</head>
|
||||
<body>
|
||||
<div id="app"></div>
|
||||
<script type="module" src="/src/main.ts"></script>
|
||||
</body>
|
||||
</html>
|
||||
1173
frontend/package-lock.json
generated
Normal file
1173
frontend/package-lock.json
generated
Normal file
File diff suppressed because it is too large
Load Diff
17
frontend/package.json
Normal file
17
frontend/package.json
Normal file
@@ -0,0 +1,17 @@
|
||||
{
|
||||
"name": "fast_reader-frontend",
|
||||
"private": true,
|
||||
"version": "0.1.0",
|
||||
"type": "module",
|
||||
"scripts": {
|
||||
"build": "tsc && vite build",
|
||||
"dev": "vite"
|
||||
},
|
||||
"devDependencies": {
|
||||
"typescript": "^5.8.3",
|
||||
"vite": "^6.2.0"
|
||||
},
|
||||
"allowScripts": {
|
||||
"esbuild@0.25.12": true
|
||||
}
|
||||
}
|
||||
431
frontend/src/app.ts
Normal file
431
frontend/src/app.ts
Normal file
@@ -0,0 +1,431 @@
|
||||
import {
|
||||
PrepareText,
|
||||
GetSettings,
|
||||
SaveSettings,
|
||||
ResetSettings,
|
||||
} from "../wailsjs/go/app/App";
|
||||
|
||||
interface Word {
|
||||
text: string;
|
||||
prefix: string;
|
||||
focus: string;
|
||||
suffix: string;
|
||||
}
|
||||
|
||||
interface AppSettings {
|
||||
readingSpeed: number;
|
||||
fontSize: number;
|
||||
theme: string;
|
||||
}
|
||||
|
||||
const root = document.querySelector<HTMLDivElement>("#app")!;
|
||||
|
||||
root.innerHTML = `
|
||||
<main class="layout">
|
||||
<header>
|
||||
<div>
|
||||
<h1>Fast Reader Training</h1>
|
||||
<p class="subtitle">Focus on one word at a time.</p>
|
||||
</div>
|
||||
<span class="version">v0.1.0</span>
|
||||
</header>
|
||||
|
||||
<section class="input-section">
|
||||
<label for="text-input">Your text</label>
|
||||
<textarea id="text-input"
|
||||
placeholder="Paste or type the text you want to read..."
|
||||
></textarea>
|
||||
<div class="input-footer">
|
||||
<span id="word-count">0 words</span>
|
||||
<button id="load-demo" class="secondary">Load demo text</button>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<section class="reader">
|
||||
<div class="reader-top">
|
||||
<span id="reading-status">Ready to read</span>
|
||||
<span id="current-position">0 / 0</span>
|
||||
</div>
|
||||
|
||||
<div class="word-display" id="word-display">
|
||||
<span class="word-prefix"></span><span class="word-focus">F</span><span class="word-suffix">lashRead</span>
|
||||
</div>
|
||||
|
||||
<div class="progress-track">
|
||||
<div id="progress-bar"></div>
|
||||
</div>
|
||||
|
||||
<div class="controls">
|
||||
<button id="reset" class="secondary" title="Reset (R)">↺</button>
|
||||
<button id="play" class="primary" title="Play / Pause (Space)">▶ Start reading</button>
|
||||
<button id="skip" class="secondary" title="Next word (→)">→</button>
|
||||
</div>
|
||||
<div class="settings">
|
||||
<div class="setting-group">
|
||||
<label for="speed">Reading speed</label>
|
||||
<div class="speed-control">
|
||||
<input
|
||||
id="speed"
|
||||
type="range"
|
||||
min="100"
|
||||
max="1000"
|
||||
step="25"
|
||||
value="300"
|
||||
/>
|
||||
<strong id="speed-value">300 WPM</strong>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="setting-group">
|
||||
<label for="font-size">Word font size</label>
|
||||
<div class="speed-control">
|
||||
<input
|
||||
id="font-size"
|
||||
type="range"
|
||||
min="24"
|
||||
max="96"
|
||||
step="2"
|
||||
value="48"
|
||||
/>
|
||||
<strong id="font-size-value">48 px</strong>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="setting-group">
|
||||
<label for="theme">Theme</label>
|
||||
<select id="theme">
|
||||
<option value="dark">Dark</option>
|
||||
<option value="light">Light</option>
|
||||
</select>
|
||||
</div>
|
||||
|
||||
<div class="settings-footer">
|
||||
<span id="settings-status" role="status"></span>
|
||||
<button id="reset-settings" class="secondary">
|
||||
Reset settings
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<footer>
|
||||
<span>Space: Play/Pause</span>
|
||||
<span>← →: Navigate</span>
|
||||
<span>R: Reset</span>
|
||||
</footer>
|
||||
</main>
|
||||
`;
|
||||
|
||||
const textInput = document.querySelector<HTMLTextAreaElement>("#text-input")!;
|
||||
const wordCount = document.querySelector<HTMLSpanElement>("#word-count")!;
|
||||
const display = document.querySelector<HTMLDivElement>("#word-display")!;
|
||||
const prefixEl = display.querySelector<HTMLSpanElement>(".word-prefix")!;
|
||||
const focusEl = display.querySelector<HTMLSpanElement>(".word-focus")!;
|
||||
const suffixEl = display.querySelector<HTMLSpanElement>(".word-suffix")!;
|
||||
const playButton = document.querySelector<HTMLButtonElement>("#play")!;
|
||||
const resetButton = document.querySelector<HTMLButtonElement>("#reset")!;
|
||||
const skipButton = document.querySelector<HTMLButtonElement>("#skip")!;
|
||||
const speedInput = document.querySelector<HTMLInputElement>("#speed")!;
|
||||
const speedValue = document.querySelector<HTMLElement>("#speed-value")!;
|
||||
const progressBar = document.querySelector<HTMLDivElement>("#progress-bar")!;
|
||||
const positionLabel =
|
||||
document.querySelector<HTMLSpanElement>("#current-position")!;
|
||||
const statusLabel = document.querySelector<HTMLSpanElement>("#reading-status")!;
|
||||
const demoButton = document.querySelector<HTMLButtonElement>("#load-demo")!;
|
||||
const fontSizeInput = document.querySelector<HTMLInputElement>("#font-size")!;
|
||||
|
||||
const fontSizeValue = document.querySelector<HTMLElement>("#font-size-value")!;
|
||||
|
||||
const themeInput = document.querySelector<HTMLSelectElement>("#theme")!;
|
||||
|
||||
const resetSettingsButton =
|
||||
document.querySelector<HTMLButtonElement>("#reset-settings")!;
|
||||
|
||||
const settingsStatus =
|
||||
document.querySelector<HTMLSpanElement>("#settings-status")!;
|
||||
|
||||
let words: Word[] = [];
|
||||
let currentIndex = 0;
|
||||
let isPlaying = false;
|
||||
let timer: number | undefined;
|
||||
let wpm = 300;
|
||||
let fontSize = 48;
|
||||
let theme = "dark";
|
||||
|
||||
let currentSettings: AppSettings = {
|
||||
readingSpeed: 300,
|
||||
fontSize: 48,
|
||||
theme: "dark",
|
||||
};
|
||||
|
||||
function applySettings(settings: AppSettings) {
|
||||
currentSettings = settings;
|
||||
|
||||
wpm = settings.readingSpeed;
|
||||
fontSize = settings.fontSize;
|
||||
theme = settings.theme;
|
||||
|
||||
speedInput.value = String(wpm);
|
||||
speedValue.textContent = `${wpm} WPM`;
|
||||
|
||||
fontSizeInput.value = String(fontSize);
|
||||
fontSizeValue.textContent = `${fontSize} px`;
|
||||
|
||||
themeInput.value = theme;
|
||||
|
||||
document.documentElement.dataset.theme = theme;
|
||||
display.style.fontSize = `${fontSize}px`;
|
||||
}
|
||||
|
||||
async function persistSettings() {
|
||||
currentSettings = {
|
||||
readingSpeed: wpm,
|
||||
fontSize,
|
||||
theme,
|
||||
};
|
||||
|
||||
settingsStatus.textContent = "Saving...";
|
||||
|
||||
try {
|
||||
await SaveSettings(currentSettings);
|
||||
settingsStatus.textContent = "Settings saved";
|
||||
} catch (error) {
|
||||
console.error("Could not save settings:", error);
|
||||
settingsStatus.textContent = "Could not save settings";
|
||||
}
|
||||
}
|
||||
|
||||
async function loadSettings() {
|
||||
try {
|
||||
const saved = await GetSettings();
|
||||
applySettings(saved);
|
||||
} catch (error) {
|
||||
console.error("Could not load settings:", error);
|
||||
settingsStatus.textContent = "Using default settings";
|
||||
}
|
||||
}
|
||||
|
||||
function updateWordCount() {
|
||||
const count = textInput.value.trim()
|
||||
? textInput.value.trim().split(/\s+/).length
|
||||
: 0;
|
||||
|
||||
wordCount.textContent = `${count} words`;
|
||||
}
|
||||
|
||||
function renderWord() {
|
||||
if (words.length === 0) {
|
||||
prefixEl.textContent = "";
|
||||
focusEl.textContent = "F";
|
||||
suffixEl.textContent = "lashRead";
|
||||
positionLabel.textContent = "0 / 0";
|
||||
progressBar.style.width = "0%";
|
||||
return;
|
||||
}
|
||||
|
||||
const word = words[currentIndex];
|
||||
|
||||
prefixEl.textContent = word.prefix;
|
||||
focusEl.textContent = word.focus;
|
||||
suffixEl.textContent = word.suffix;
|
||||
|
||||
positionLabel.textContent = `${currentIndex + 1} / ${words.length}`;
|
||||
|
||||
progressBar.style.width = `${((currentIndex + 1) / words.length) * 100}%`;
|
||||
}
|
||||
|
||||
function stopPlayback() {
|
||||
isPlaying = false;
|
||||
|
||||
if (timer !== undefined) {
|
||||
window.clearTimeout(timer);
|
||||
timer = undefined;
|
||||
}
|
||||
|
||||
playButton.textContent = "▶ Start reading";
|
||||
statusLabel.textContent = "Paused";
|
||||
}
|
||||
|
||||
function advance() {
|
||||
if (!isPlaying) return;
|
||||
|
||||
if (currentIndex >= words.length - 1) {
|
||||
stopPlayback();
|
||||
statusLabel.textContent = "Finished reading";
|
||||
return;
|
||||
}
|
||||
|
||||
const currentWord = words[currentIndex].text;
|
||||
|
||||
currentIndex++;
|
||||
renderWord();
|
||||
|
||||
const delay = getDuration(currentWord);
|
||||
timer = window.setTimeout(advance, delay);
|
||||
}
|
||||
|
||||
function getDuration(word: string): number {
|
||||
let duration = 60000 / wpm;
|
||||
|
||||
if (/[.!?]$/.test(word)) {
|
||||
duration += 300;
|
||||
} else if (/[,;:]$/.test(word)) {
|
||||
duration += 150;
|
||||
}
|
||||
|
||||
if ([...word].length >= 10) {
|
||||
duration += 50;
|
||||
}
|
||||
|
||||
return duration;
|
||||
}
|
||||
|
||||
async function startPlayback() {
|
||||
if (words.length === 0) {
|
||||
const text = textInput.value.trim();
|
||||
|
||||
if (!text) {
|
||||
textInput.focus();
|
||||
return;
|
||||
}
|
||||
|
||||
words = await PrepareText(text);
|
||||
currentIndex = 0;
|
||||
renderWord();
|
||||
}
|
||||
|
||||
if (currentIndex >= words.length - 1) {
|
||||
currentIndex = 0;
|
||||
renderWord();
|
||||
}
|
||||
|
||||
isPlaying = true;
|
||||
playButton.textContent = "⏸ Pause";
|
||||
statusLabel.textContent = "Reading";
|
||||
|
||||
timer = window.setTimeout(advance, getDuration(words[currentIndex].text));
|
||||
}
|
||||
|
||||
playButton.addEventListener("click", () => {
|
||||
if (isPlaying) {
|
||||
stopPlayback();
|
||||
} else {
|
||||
void startPlayback();
|
||||
}
|
||||
});
|
||||
|
||||
resetButton.addEventListener("click", () => {
|
||||
stopPlayback();
|
||||
currentIndex = 0;
|
||||
renderWord();
|
||||
statusLabel.textContent = "Ready to read";
|
||||
});
|
||||
|
||||
skipButton.addEventListener("click", () => {
|
||||
if (currentIndex < words.length - 1) {
|
||||
currentIndex++;
|
||||
renderWord();
|
||||
}
|
||||
});
|
||||
|
||||
speedInput.addEventListener("input", () => {
|
||||
wpm = Number(speedInput.value);
|
||||
speedValue.textContent = `${wpm} WPM`;
|
||||
});
|
||||
|
||||
speedInput.addEventListener("change", () => {
|
||||
void persistSettings();
|
||||
});
|
||||
fontSizeInput.addEventListener("input", () => {
|
||||
fontSize = Number(fontSizeInput.value);
|
||||
|
||||
fontSizeValue.textContent = `${fontSize} px`;
|
||||
display.style.fontSize = `${fontSize}px`;
|
||||
});
|
||||
|
||||
fontSizeInput.addEventListener("change", () => {
|
||||
void persistSettings();
|
||||
});
|
||||
|
||||
themeInput.addEventListener("change", () => {
|
||||
theme = themeInput.value;
|
||||
document.documentElement.dataset.theme = theme;
|
||||
|
||||
void persistSettings();
|
||||
});
|
||||
|
||||
textInput.addEventListener("input", () => {
|
||||
stopPlayback();
|
||||
words = [];
|
||||
currentIndex = 0;
|
||||
updateWordCount();
|
||||
renderWord();
|
||||
statusLabel.textContent = "Ready to read";
|
||||
});
|
||||
|
||||
demoButton.addEventListener("click", () => {
|
||||
textInput.value =
|
||||
"The quick brown fox jumps over the lazy dog. " +
|
||||
"Speed reading helps you focus on one word at a time, " +
|
||||
"reducing the need for eye movement across a page. " +
|
||||
"Experiment with different speeds and discover what works best for you.";
|
||||
|
||||
updateWordCount();
|
||||
words = [];
|
||||
currentIndex = 0;
|
||||
stopPlayback();
|
||||
renderWord();
|
||||
});
|
||||
resetSettingsButton.addEventListener("click", async () => {
|
||||
try {
|
||||
settingsStatus.textContent = "Resetting...";
|
||||
|
||||
const defaults = await ResetSettings();
|
||||
applySettings(defaults);
|
||||
|
||||
settingsStatus.textContent = "Settings reset";
|
||||
} catch (error) {
|
||||
console.error("Could not reset settings:", error);
|
||||
settingsStatus.textContent = "Could not reset settings";
|
||||
}
|
||||
});
|
||||
|
||||
document.addEventListener("keydown", (event) => {
|
||||
const target = event.target as HTMLElement;
|
||||
|
||||
if (target.tagName === "TEXTAREA" || target.tagName === "INPUT") {
|
||||
return;
|
||||
}
|
||||
|
||||
switch (event.key) {
|
||||
case " ":
|
||||
event.preventDefault();
|
||||
playButton.click();
|
||||
break;
|
||||
|
||||
case "ArrowRight":
|
||||
skipButton.click();
|
||||
break;
|
||||
|
||||
case "ArrowLeft":
|
||||
if (currentIndex > 0) {
|
||||
currentIndex--;
|
||||
renderWord();
|
||||
}
|
||||
break;
|
||||
|
||||
case "r":
|
||||
case "R":
|
||||
resetButton.click();
|
||||
break;
|
||||
|
||||
case "Escape":
|
||||
stopPlayback();
|
||||
break;
|
||||
}
|
||||
});
|
||||
|
||||
updateWordCount();
|
||||
renderWord();
|
||||
|
||||
void loadSettings();
|
||||
2
frontend/src/main.ts
Normal file
2
frontend/src/main.ts
Normal file
@@ -0,0 +1,2 @@
|
||||
import "./styles/style.css";
|
||||
import "./app";
|
||||
306
frontend/src/styles/style.css
Normal file
306
frontend/src/styles/style.css
Normal file
@@ -0,0 +1,306 @@
|
||||
:root {
|
||||
font-family:
|
||||
Inter,
|
||||
system-ui,
|
||||
-apple-system,
|
||||
BlinkMacSystemFont,
|
||||
"Segoe UI",
|
||||
sans-serif;
|
||||
|
||||
font-synthesis: none;
|
||||
text-rendering: optimizeLegibility;
|
||||
-webkit-font-smoothing: antialiased;
|
||||
|
||||
--background: #121218;
|
||||
--text: #e8e8ed;
|
||||
--surface: #1c1c24;
|
||||
--border: #30303a;
|
||||
--muted: #92929e;
|
||||
--accent: #8b7cf6;
|
||||
--red: #ff4c55;
|
||||
--secondary-background: #292932;
|
||||
--secondary-border: #3a3a45;
|
||||
--secondary-text: #d0d0da;
|
||||
--progress-track: #32323c;
|
||||
|
||||
color: var(--text);
|
||||
background: var(--background);
|
||||
}
|
||||
|
||||
:root[data-theme="light"] {
|
||||
--background: #f4f4f8;
|
||||
--text: #24242d;
|
||||
--surface: #ffffff;
|
||||
--border: #dedee7;
|
||||
--muted: #707080;
|
||||
--accent: #6958df;
|
||||
--red: #d93645;
|
||||
--secondary-background: #ededf3;
|
||||
--secondary-border: #d5d5df;
|
||||
--secondary-text: #33333e;
|
||||
--progress-track: #dedee7;
|
||||
}
|
||||
|
||||
* {
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
body {
|
||||
margin: 0;
|
||||
min-width: 600px;
|
||||
min-height: 100vh;
|
||||
background: var(--background);
|
||||
color: var(--text);
|
||||
}
|
||||
|
||||
button,
|
||||
textarea,
|
||||
input {
|
||||
font: inherit;
|
||||
}
|
||||
|
||||
.layout {
|
||||
max-width: 1000px;
|
||||
margin: 0 auto;
|
||||
padding: 36px 48px;
|
||||
}
|
||||
|
||||
header {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
margin-bottom: 32px;
|
||||
}
|
||||
|
||||
h1 {
|
||||
font-size: 28px;
|
||||
font-weight: 700;
|
||||
letter-spacing: -0.8px;
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
.subtitle {
|
||||
margin: 6px 0 0;
|
||||
color: var(--muted);
|
||||
font-size: 14px;
|
||||
}
|
||||
|
||||
.version {
|
||||
color: var(--muted);
|
||||
font-size: 12px;
|
||||
border: 1px solid var(--border);
|
||||
padding: 5px 10px;
|
||||
border-radius: 20px;
|
||||
}
|
||||
|
||||
.input-section {
|
||||
margin-bottom: 24px;
|
||||
}
|
||||
|
||||
.input-section label,
|
||||
.settings label {
|
||||
display: block;
|
||||
font-size: 13px;
|
||||
font-weight: 600;
|
||||
margin-bottom: 10px;
|
||||
}
|
||||
|
||||
textarea {
|
||||
display: block;
|
||||
width: 100%;
|
||||
height: 130px;
|
||||
resize: vertical;
|
||||
padding: 14px;
|
||||
color: var(--text);
|
||||
background: var(--surface);
|
||||
border: 1px solid var(--border);
|
||||
border-radius: 10px;
|
||||
outline: none;
|
||||
line-height: 1.6;
|
||||
font-size: 14px;
|
||||
}
|
||||
|
||||
textarea:focus {
|
||||
border-color: var(--accent);
|
||||
}
|
||||
|
||||
.input-footer {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
margin-top: 8px;
|
||||
}
|
||||
|
||||
#word-count {
|
||||
font-size: 12px;
|
||||
color: var(--muted);
|
||||
}
|
||||
|
||||
button {
|
||||
border: 0;
|
||||
border-radius: 8px;
|
||||
cursor: pointer;
|
||||
transition: opacity 0.15s;
|
||||
}
|
||||
|
||||
button:hover {
|
||||
opacity: 0.85;
|
||||
}
|
||||
|
||||
.primary {
|
||||
background: var(--accent);
|
||||
color: white;
|
||||
padding: 12px 28px;
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
.secondary {
|
||||
color: var(--secondary-text);
|
||||
background: var(--secondary-background);
|
||||
border: 1px solid var(--secondary-border);
|
||||
padding: 10px 16px;
|
||||
}
|
||||
|
||||
.reader {
|
||||
background: var(--surface);
|
||||
border: 1px solid var(--border);
|
||||
border-radius: 14px;
|
||||
padding: 24px;
|
||||
}
|
||||
|
||||
.reader-top {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
color: var(--muted);
|
||||
font-size: 12px;
|
||||
}
|
||||
|
||||
.word-display {
|
||||
display: flex;
|
||||
align-items: baseline;
|
||||
justify-content: center;
|
||||
min-height: 180px;
|
||||
padding: 48px 0;
|
||||
font-size: clamp(36px, 6vw, 64px);
|
||||
font-weight: 500;
|
||||
letter-spacing: 0.5px;
|
||||
font-variant-ligatures: none;
|
||||
}
|
||||
|
||||
.word-prefix,
|
||||
.word-focus,
|
||||
.word-suffix {
|
||||
white-space: pre;
|
||||
}
|
||||
|
||||
.word-focus {
|
||||
color: var(--red);
|
||||
font-weight: 700;
|
||||
}
|
||||
|
||||
.progress-track {
|
||||
height: 4px;
|
||||
border-radius: 4px;
|
||||
overflow: hidden;
|
||||
background: var(--progress-track);
|
||||
}
|
||||
|
||||
#progress-bar {
|
||||
height: 100%;
|
||||
width: 0;
|
||||
background: var(--accent);
|
||||
transition: width 0.08s linear;
|
||||
}
|
||||
|
||||
.controls {
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
gap: 12px;
|
||||
margin: 24px 0 32px;
|
||||
}
|
||||
|
||||
.settings {
|
||||
max-width: 500px;
|
||||
margin: 0 auto;
|
||||
}
|
||||
|
||||
.speed-control {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 20px;
|
||||
}
|
||||
|
||||
input[type="range"] {
|
||||
flex: 1;
|
||||
accent-color: var(--accent);
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
#speed-value {
|
||||
min-width: 80px;
|
||||
font-size: 13px;
|
||||
text-align: right;
|
||||
}
|
||||
|
||||
footer {
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
flex-wrap: wrap;
|
||||
gap: 24px;
|
||||
margin-top: 24px;
|
||||
color: var(--muted);
|
||||
font-size: 12px;
|
||||
}
|
||||
|
||||
@media (max-width: 700px) {
|
||||
.layout {
|
||||
padding: 24px 18px;
|
||||
}
|
||||
|
||||
.reader {
|
||||
padding: 16px;
|
||||
}
|
||||
|
||||
.word-display {
|
||||
min-height: 140px;
|
||||
padding: 36px 0;
|
||||
}
|
||||
}
|
||||
|
||||
.setting-group {
|
||||
margin-bottom: 20px;
|
||||
}
|
||||
|
||||
.settings select {
|
||||
width: 100%;
|
||||
padding: 10px 12px;
|
||||
color: var(--text);
|
||||
background: var(--surface);
|
||||
border: 1px solid var(--border);
|
||||
border-radius: 8px;
|
||||
outline: none;
|
||||
color-scheme: light dark;
|
||||
}
|
||||
|
||||
.settings select option {
|
||||
color: var(--text);
|
||||
background-color: var(--surface);
|
||||
}
|
||||
|
||||
.settings select:focus {
|
||||
border-color: var(--accent);
|
||||
}
|
||||
|
||||
.settings-footer {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
gap: 16px;
|
||||
margin-top: 24px;
|
||||
}
|
||||
|
||||
#settings-status {
|
||||
color: var(--muted);
|
||||
font-size: 12px;
|
||||
}
|
||||
22
frontend/tsconfig.json
Normal file
22
frontend/tsconfig.json
Normal file
@@ -0,0 +1,22 @@
|
||||
{
|
||||
"compilerOptions": {
|
||||
"target": "ES2022",
|
||||
"useDefineForClassFields": true,
|
||||
"module": "ESNext",
|
||||
"lib": [
|
||||
"ES2022",
|
||||
"DOM",
|
||||
"DOM.Iterable"
|
||||
],
|
||||
"skipLibCheck": true,
|
||||
"moduleResolution": "Bundler",
|
||||
"allowImportingTsExtensions": false,
|
||||
"verbatimModuleSyntax": true,
|
||||
"moduleDetection": "force",
|
||||
"noEmit": true,
|
||||
"strict": true
|
||||
},
|
||||
"include": [
|
||||
"src"
|
||||
]
|
||||
}
|
||||
Reference in New Issue
Block a user