[chore] used codex to create a base layout for src
This commit is contained in:
21
src/app/dom.ts
Normal file
21
src/app/dom.ts
Normal 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
33
src/app/init.ts
Normal 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);
|
||||
}
|
||||
}
|
||||
26
src/features/countdown/api.ts
Normal file
26
src/features/countdown/api.ts
Normal 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");
|
||||
}
|
||||
48
src/features/countdown/controller.ts
Normal file
48
src/features/countdown/controller.ts
Normal 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();
|
||||
}
|
||||
21
src/features/countdown/types.ts
Normal file
21
src/features/countdown/types.ts
Normal 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";
|
||||
77
src/features/countdown/view.ts
Normal file
77
src/features/countdown/view.ts
Normal 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;
|
||||
},
|
||||
};
|
||||
}
|
||||
22
src/main.ts
22
src/main.ts
@@ -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();
|
||||
});
|
||||
|
||||
8
src/shared/tauri/invoke.ts
Normal file
8
src/shared/tauri/invoke.ts
Normal 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
17
src/shared/time/format.ts
Normal 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}`;
|
||||
}
|
||||
118
src/styles.css
118
src/styles.css
@@ -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
117
src/styles/base.css
Normal 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
43
src/styles/countdown.css
Normal 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);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user