[chore] used codex to create a base layout for src

This commit is contained in:
2026-02-26 17:55:52 +01:00
parent 74c9ded46b
commit f804d55ce2
12 changed files with 416 additions and 135 deletions

21
src/app/dom.ts Normal file
View File

@@ -0,0 +1,21 @@
export type GreetDom = {
form: HTMLFormElement;
input: HTMLInputElement;
message: HTMLElement;
};
export function getMainContainer(): HTMLElement | null {
return document.querySelector(".container");
}
export function getGreetDom(): GreetDom | null {
const form = document.querySelector<HTMLFormElement>("#greet-form");
const input = document.querySelector<HTMLInputElement>("#greet-input");
const message = document.querySelector<HTMLElement>("#greet-msg");
if (!form || !input || !message) {
return null;
}
return { form, input, message };
}

33
src/app/init.ts Normal file
View File

@@ -0,0 +1,33 @@
import { initCountdownController } from "../features/countdown/controller";
import { invokeCommand } from "../shared/tauri/invoke";
import { getGreetDom, getMainContainer } from "./dom";
async function greet(name: string, messageEl: HTMLElement): Promise<void> {
try {
const message = await invokeCommand<string>("greet", { name });
messageEl.textContent = message;
} catch (error) {
messageEl.textContent = `greet error: ${String(error)}`;
}
}
function bindGreetForm(): void {
const greetDom = getGreetDom();
if (!greetDom) {
return;
}
greetDom.form.addEventListener("submit", (event) => {
event.preventDefault();
void greet(greetDom.input.value, greetDom.message);
});
}
export async function initApp(): Promise<void> {
bindGreetForm();
const container = getMainContainer();
if (container) {
await initCountdownController(container);
}
}

View File

@@ -0,0 +1,26 @@
import { invokeCommand } from "../../shared/tauri/invoke";
import type { CountdownCommand, CountdownSnapshotDto } from "./types";
export async function fetchCountdownSnapshot(): Promise<CountdownSnapshotDto> {
return invokeCommand<CountdownSnapshotDto>("countdown_snapshot");
}
async function invokeCountdownCommand(command: CountdownCommand): Promise<void> {
await invokeCommand<void>(command);
}
export async function startCountdown(): Promise<void> {
await invokeCountdownCommand("countdown_start");
}
export async function pauseCountdown(): Promise<void> {
await invokeCountdownCommand("countdown_pause");
}
export async function resumeCountdown(): Promise<void> {
await invokeCountdownCommand("countdown_resume");
}
export async function resetCountdown(): Promise<void> {
await invokeCountdownCommand("countdown_reset");
}

View File

@@ -0,0 +1,48 @@
import {
fetchCountdownSnapshot,
pauseCountdown,
resetCountdown,
resumeCountdown,
startCountdown,
} from "./api";
import { createCountdownView } from "./view";
export async function initCountdownController(container: HTMLElement): Promise<void> {
const view = createCountdownView(container);
const refreshSnapshot = async (): Promise<void> => {
try {
const snapshot = await fetchCountdownSnapshot();
view.setSnapshot(snapshot);
} catch (error) {
view.setError(`snapshot error: ${String(error)}`);
}
};
const runAction = async (action: () => Promise<void>): Promise<void> => {
try {
await action();
await refreshSnapshot();
} catch (error) {
view.setError(`command error: ${String(error)}`);
}
};
view.onStart(() => {
void runAction(startCountdown);
});
view.onPause(() => {
void runAction(pauseCountdown);
});
view.onResume(() => {
void runAction(resumeCountdown);
});
view.onReset(() => {
void runAction(resetCountdown);
});
view.onRefresh(() => {
void refreshSnapshot();
});
await refreshSnapshot();
}

View File

@@ -0,0 +1,21 @@
export type CountdownState = "Idle" | "Running" | "Paused" | "Finished";
export type CountdownDuration = {
secs: number;
nanos: number;
};
export type CountdownSnapshotDto = {
id: number;
label: string;
duration: CountdownDuration;
state: CountdownState;
start_epoch_ms: number | null;
target_epoch_ms: number | null;
};
export type CountdownCommand =
| "countdown_start"
| "countdown_pause"
| "countdown_resume"
| "countdown_reset";

View File

@@ -0,0 +1,77 @@
import { formatDuration } from "../../shared/time/format";
import type { CountdownSnapshotDto } from "./types";
export type CountdownView = {
onStart: (handler: () => void) => void;
onPause: (handler: () => void) => void;
onResume: (handler: () => void) => void;
onReset: (handler: () => void) => void;
onRefresh: (handler: () => void) => void;
setSnapshot: (snapshot: CountdownSnapshotDto) => void;
setError: (message: string) => void;
};
function makeButton(label: string): HTMLButtonElement {
const button = document.createElement("button");
button.type = "button";
button.textContent = label;
return button;
}
export function createCountdownView(container: HTMLElement): CountdownView {
const panel = document.createElement("section");
panel.className = "countdown-panel";
const title = document.createElement("h2");
title.textContent = "Countdown Controls";
const actions = document.createElement("div");
actions.className = "countdown-actions";
const startButton = makeButton("Start");
const pauseButton = makeButton("Pause");
const resumeButton = makeButton("Resume");
const resetButton = makeButton("Reset");
const refreshButton = makeButton("Refresh");
actions.append(startButton, pauseButton, resumeButton, resetButton, refreshButton);
const summary = document.createElement("p");
summary.className = "countdown-summary";
summary.textContent = "Waiting for snapshot...";
const error = document.createElement("p");
error.className = "countdown-error";
const snapshot = document.createElement("pre");
snapshot.className = "countdown-snapshot";
panel.append(title, actions, summary, error, snapshot);
container.appendChild(panel);
return {
onStart(handler) {
startButton.addEventListener("click", handler);
},
onPause(handler) {
pauseButton.addEventListener("click", handler);
},
onResume(handler) {
resumeButton.addEventListener("click", handler);
},
onReset(handler) {
resetButton.addEventListener("click", handler);
},
onRefresh(handler) {
refreshButton.addEventListener("click", handler);
},
setSnapshot(value) {
error.textContent = "";
summary.textContent = `State: ${value.state} | Remaining: ${formatDuration(value.duration)}`;
snapshot.textContent = JSON.stringify(value, null, 2);
},
setError(message) {
error.textContent = message;
},
};
}

View File

@@ -1,22 +1,6 @@
import { invoke } from "@tauri-apps/api/core";
let greetInputEl: HTMLInputElement | null;
let greetMsgEl: HTMLElement | null;
async function greet() {
if (greetMsgEl && greetInputEl) {
// Learn more about Tauri commands at https://tauri.app/develop/calling-rust/
greetMsgEl.textContent = await invoke("greet", {
name: greetInputEl.value,
});
}
}
import "./styles.css";
import { initApp } from "./app/init";
window.addEventListener("DOMContentLoaded", () => {
greetInputEl = document.querySelector("#greet-input");
greetMsgEl = document.querySelector("#greet-msg");
document.querySelector("#greet-form")?.addEventListener("submit", (e) => {
e.preventDefault();
greet();
});
void initApp();
});

View File

@@ -0,0 +1,8 @@
import { invoke } from "@tauri-apps/api/core";
export async function invokeCommand<T>(
command: string,
payload?: Record<string, unknown>
): Promise<T> {
return invoke<T>(command, payload);
}

17
src/shared/time/format.ts Normal file
View File

@@ -0,0 +1,17 @@
type DurationLike = {
secs?: number;
nanos?: number;
};
export function formatDuration(value: DurationLike): string {
const totalSeconds = Math.max(0, Math.floor(value.secs ?? 0));
const hours = Math.floor(totalSeconds / 3600)
.toString()
.padStart(2, "0");
const minutes = Math.floor((totalSeconds % 3600) / 60)
.toString()
.padStart(2, "0");
const seconds = (totalSeconds % 60).toString().padStart(2, "0");
return `${hours}:${minutes}:${seconds}`;
}

View File

@@ -1,116 +1,2 @@
.logo.vite:hover {
filter: drop-shadow(0 0 2em #747bff);
}
.logo.typescript:hover {
filter: drop-shadow(0 0 2em #2d79c7);
}
:root {
font-family: Inter, Avenir, Helvetica, Arial, sans-serif;
font-size: 16px;
line-height: 24px;
font-weight: 400;
color: #0f0f0f;
background-color: #f6f6f6;
font-synthesis: none;
text-rendering: optimizeLegibility;
-webkit-font-smoothing: antialiased;
-moz-osx-font-smoothing: grayscale;
-webkit-text-size-adjust: 100%;
}
.container {
margin: 0;
padding-top: 10vh;
display: flex;
flex-direction: column;
justify-content: center;
text-align: center;
}
.logo {
height: 6em;
padding: 1.5em;
will-change: filter;
transition: 0.75s;
}
.logo.tauri:hover {
filter: drop-shadow(0 0 2em #24c8db);
}
.row {
display: flex;
justify-content: center;
}
a {
font-weight: 500;
color: #646cff;
text-decoration: inherit;
}
a:hover {
color: #535bf2;
}
h1 {
text-align: center;
}
input,
button {
border-radius: 8px;
border: 1px solid transparent;
padding: 0.6em 1.2em;
font-size: 1em;
font-weight: 500;
font-family: inherit;
color: #0f0f0f;
background-color: #ffffff;
transition: border-color 0.25s;
box-shadow: 0 2px 2px rgba(0, 0, 0, 0.2);
}
button {
cursor: pointer;
}
button:hover {
border-color: #396cd8;
}
button:active {
border-color: #396cd8;
background-color: #e8e8e8;
}
input,
button {
outline: none;
}
#greet-input {
margin-right: 5px;
}
@media (prefers-color-scheme: dark) {
:root {
color: #f6f6f6;
background-color: #2f2f2f;
}
a:hover {
color: #24c8db;
}
input,
button {
color: #ffffff;
background-color: #0f0f0f98;
}
button:active {
background-color: #0f0f0f69;
}
}
@import "./styles/base.css";
@import "./styles/countdown.css";

117
src/styles/base.css Normal file
View File

@@ -0,0 +1,117 @@
.logo.vite:hover {
filter: drop-shadow(0 0 2em #747bff);
}
.logo.typescript:hover {
filter: drop-shadow(0 0 2em #2d79c7);
}
:root {
font-family: Inter, Avenir, Helvetica, Arial, sans-serif;
font-size: 16px;
line-height: 24px;
font-weight: 400;
color: #0f0f0f;
background-color: #f6f6f6;
font-synthesis: none;
text-rendering: optimizeLegibility;
-webkit-font-smoothing: antialiased;
-moz-osx-font-smoothing: grayscale;
-webkit-text-size-adjust: 100%;
}
.container {
margin: 0;
padding-top: 10vh;
display: flex;
flex-direction: column;
justify-content: center;
text-align: center;
}
.logo {
height: 6em;
padding: 1.5em;
will-change: filter;
transition: 0.75s;
}
.logo.tauri:hover {
filter: drop-shadow(0 0 2em #24c8db);
}
.row {
display: flex;
justify-content: center;
}
a {
font-weight: 500;
color: #646cff;
text-decoration: inherit;
}
a:hover {
color: #535bf2;
}
h1 {
text-align: center;
}
input,
button {
border-radius: 8px;
border: 1px solid transparent;
padding: 0.6em 1.2em;
font-size: 1em;
font-weight: 500;
font-family: inherit;
color: #0f0f0f;
background-color: #ffffff;
transition: border-color 0.25s;
box-shadow: 0 2px 2px rgba(0, 0, 0, 0.2);
}
button {
cursor: pointer;
}
button:hover {
border-color: #396cd8;
}
button:active {
border-color: #396cd8;
background-color: #e8e8e8;
}
input,
button {
outline: none;
}
#greet-input {
margin-right: 5px;
}
@media (prefers-color-scheme: dark) {
:root {
color: #f6f6f6;
background-color: #2f2f2f;
}
a:hover {
color: #24c8db;
}
input,
button {
color: #ffffff;
background-color: #0f0f0f98;
}
button:active {
background-color: #0f0f0f69;
}
}

43
src/styles/countdown.css Normal file
View File

@@ -0,0 +1,43 @@
.countdown-panel {
margin: 2rem auto 0;
max-width: 780px;
width: min(90vw, 780px);
text-align: left;
}
.countdown-actions {
display: flex;
flex-wrap: wrap;
gap: 0.5rem;
margin: 0.75rem 0;
}
.countdown-summary {
margin: 0.5rem 0;
}
.countdown-error {
min-height: 1.25rem;
color: #b00020;
margin: 0.25rem 0;
}
.countdown-snapshot {
margin: 0;
padding: 0.75rem;
border-radius: 8px;
border: 1px solid rgba(0, 0, 0, 0.15);
background: rgba(255, 255, 255, 0.65);
overflow: auto;
}
@media (prefers-color-scheme: dark) {
.countdown-error {
color: #ff8a80;
}
.countdown-snapshot {
border-color: rgba(255, 255, 255, 0.25);
background: rgba(15, 15, 15, 0.6);
}
}