From ca754a4dfa69c5a63925cedad95fb7dabe63545c Mon Sep 17 00:00:00 2001 From: phortx Date: Mon, 25 May 2026 11:10:40 +0200 Subject: [PATCH 01/11] backup frontend --- bun.lock => temp-frontend/bun.lock | 0 eslint.config.js => temp-frontend/eslint.config.js | 0 index.html => temp-frontend/index.html | 0 package.json => temp-frontend/package.json | 0 {public => temp-frontend/public}/favicon.svg | 0 {public => temp-frontend/public}/icons.svg | 0 {src => temp-frontend/src}/App.tsx | 0 {src => temp-frontend/src}/api/open5e.test.ts | 0 {src => temp-frontend/src}/api/open5e.ts | 0 {src => temp-frontend/src}/components/AddCreatureForm.test.tsx | 0 {src => temp-frontend/src}/components/AddCreatureForm.tsx | 0 {src => temp-frontend/src}/components/CreatureList.test.tsx | 0 {src => temp-frontend/src}/components/CreatureList.tsx | 0 {src => temp-frontend/src}/components/EncounterToolbar.tsx | 0 {src => temp-frontend/src}/components/FloatingNextTurn.tsx | 0 {src => temp-frontend/src}/components/Footer.tsx | 0 {src => temp-frontend/src}/components/HealthBar.test.tsx | 0 {src => temp-frontend/src}/components/HealthBar.tsx | 0 {src => temp-frontend/src}/components/HpControls.tsx | 0 {src => temp-frontend/src}/components/MonsterAutocomplete.tsx | 0 {src => temp-frontend/src}/components/StatBlockPanel.tsx | 0 {src => temp-frontend/src}/components/TurnControls.test.tsx | 0 {src => temp-frontend/src}/components/TurnControls.tsx | 0 {src => temp-frontend/src}/hooks/useEncounter.test.ts | 0 {src => temp-frontend/src}/hooks/useEncounter.ts | 0 {src => temp-frontend/src}/hooks/useEncounterSettings.test.ts | 0 {src => temp-frontend/src}/hooks/useEncounterSettings.ts | 0 {src => temp-frontend/src}/hooks/useTurnTracker.test.ts | 0 {src => temp-frontend/src}/hooks/useTurnTracker.ts | 0 {src => temp-frontend/src}/index.css | 0 {src => temp-frontend/src}/main.tsx | 0 {src => temp-frontend/src}/test/setup.ts | 0 {src => temp-frontend/src}/types/creature.ts | 0 {src => temp-frontend/src}/types/encounterSettings.ts | 0 {src => temp-frontend/src}/types/statBlock.ts | 0 {src => temp-frontend/src}/types/turnState.ts | 0 {src => temp-frontend/src}/types/viewMode.ts | 0 {src => temp-frontend/src}/utils/formatInGameTime.test.ts | 0 {src => temp-frontend/src}/utils/formatInGameTime.ts | 0 {src => temp-frontend/src}/utils/formatModifier.test.ts | 0 {src => temp-frontend/src}/utils/formatModifier.ts | 0 {src => temp-frontend/src}/utils/liquidColors.ts | 0 {src => temp-frontend/src}/utils/rollInitiative.test.ts | 0 {src => temp-frontend/src}/utils/rollInitiative.ts | 0 tsconfig.app.json => temp-frontend/tsconfig.app.json | 0 tsconfig.json => temp-frontend/tsconfig.json | 0 tsconfig.node.json => temp-frontend/tsconfig.node.json | 0 vite.config.ts => temp-frontend/vite.config.ts | 0 vitest.config.ts => temp-frontend/vitest.config.ts | 0 49 files changed, 0 insertions(+), 0 deletions(-) rename bun.lock => temp-frontend/bun.lock (100%) rename eslint.config.js => temp-frontend/eslint.config.js (100%) rename index.html => temp-frontend/index.html (100%) rename package.json => temp-frontend/package.json (100%) rename {public => temp-frontend/public}/favicon.svg (100%) rename {public => temp-frontend/public}/icons.svg (100%) rename {src => temp-frontend/src}/App.tsx (100%) rename {src => temp-frontend/src}/api/open5e.test.ts (100%) rename {src => temp-frontend/src}/api/open5e.ts (100%) rename {src => temp-frontend/src}/components/AddCreatureForm.test.tsx (100%) rename {src => temp-frontend/src}/components/AddCreatureForm.tsx (100%) rename {src => temp-frontend/src}/components/CreatureList.test.tsx (100%) rename {src => temp-frontend/src}/components/CreatureList.tsx (100%) rename {src => temp-frontend/src}/components/EncounterToolbar.tsx (100%) rename {src => temp-frontend/src}/components/FloatingNextTurn.tsx (100%) rename {src => temp-frontend/src}/components/Footer.tsx (100%) rename {src => temp-frontend/src}/components/HealthBar.test.tsx (100%) rename {src => temp-frontend/src}/components/HealthBar.tsx (100%) rename {src => temp-frontend/src}/components/HpControls.tsx (100%) rename {src => temp-frontend/src}/components/MonsterAutocomplete.tsx (100%) rename {src => temp-frontend/src}/components/StatBlockPanel.tsx (100%) rename {src => temp-frontend/src}/components/TurnControls.test.tsx (100%) rename {src => temp-frontend/src}/components/TurnControls.tsx (100%) rename {src => temp-frontend/src}/hooks/useEncounter.test.ts (100%) rename {src => temp-frontend/src}/hooks/useEncounter.ts (100%) rename {src => temp-frontend/src}/hooks/useEncounterSettings.test.ts (100%) rename {src => temp-frontend/src}/hooks/useEncounterSettings.ts (100%) rename {src => temp-frontend/src}/hooks/useTurnTracker.test.ts (100%) rename {src => temp-frontend/src}/hooks/useTurnTracker.ts (100%) rename {src => temp-frontend/src}/index.css (100%) rename {src => temp-frontend/src}/main.tsx (100%) rename {src => temp-frontend/src}/test/setup.ts (100%) rename {src => temp-frontend/src}/types/creature.ts (100%) rename {src => temp-frontend/src}/types/encounterSettings.ts (100%) rename {src => temp-frontend/src}/types/statBlock.ts (100%) rename {src => temp-frontend/src}/types/turnState.ts (100%) rename {src => temp-frontend/src}/types/viewMode.ts (100%) rename {src => temp-frontend/src}/utils/formatInGameTime.test.ts (100%) rename {src => temp-frontend/src}/utils/formatInGameTime.ts (100%) rename {src => temp-frontend/src}/utils/formatModifier.test.ts (100%) rename {src => temp-frontend/src}/utils/formatModifier.ts (100%) rename {src => temp-frontend/src}/utils/liquidColors.ts (100%) rename {src => temp-frontend/src}/utils/rollInitiative.test.ts (100%) rename {src => temp-frontend/src}/utils/rollInitiative.ts (100%) rename tsconfig.app.json => temp-frontend/tsconfig.app.json (100%) rename tsconfig.json => temp-frontend/tsconfig.json (100%) rename tsconfig.node.json => temp-frontend/tsconfig.node.json (100%) rename vite.config.ts => temp-frontend/vite.config.ts (100%) rename vitest.config.ts => temp-frontend/vitest.config.ts (100%) diff --git a/bun.lock b/temp-frontend/bun.lock similarity index 100% rename from bun.lock rename to temp-frontend/bun.lock diff --git a/eslint.config.js b/temp-frontend/eslint.config.js similarity index 100% rename from eslint.config.js rename to temp-frontend/eslint.config.js diff --git a/index.html b/temp-frontend/index.html similarity index 100% rename from index.html rename to temp-frontend/index.html diff --git a/package.json b/temp-frontend/package.json similarity index 100% rename from package.json rename to temp-frontend/package.json diff --git a/public/favicon.svg b/temp-frontend/public/favicon.svg similarity index 100% rename from public/favicon.svg rename to temp-frontend/public/favicon.svg diff --git a/public/icons.svg b/temp-frontend/public/icons.svg similarity index 100% rename from public/icons.svg rename to temp-frontend/public/icons.svg diff --git a/src/App.tsx b/temp-frontend/src/App.tsx similarity index 100% rename from src/App.tsx rename to temp-frontend/src/App.tsx diff --git a/src/api/open5e.test.ts b/temp-frontend/src/api/open5e.test.ts similarity index 100% rename from src/api/open5e.test.ts rename to temp-frontend/src/api/open5e.test.ts diff --git a/src/api/open5e.ts b/temp-frontend/src/api/open5e.ts similarity index 100% rename from src/api/open5e.ts rename to temp-frontend/src/api/open5e.ts diff --git a/src/components/AddCreatureForm.test.tsx b/temp-frontend/src/components/AddCreatureForm.test.tsx similarity index 100% rename from src/components/AddCreatureForm.test.tsx rename to temp-frontend/src/components/AddCreatureForm.test.tsx diff --git a/src/components/AddCreatureForm.tsx b/temp-frontend/src/components/AddCreatureForm.tsx similarity index 100% rename from src/components/AddCreatureForm.tsx rename to temp-frontend/src/components/AddCreatureForm.tsx diff --git a/src/components/CreatureList.test.tsx b/temp-frontend/src/components/CreatureList.test.tsx similarity index 100% rename from src/components/CreatureList.test.tsx rename to temp-frontend/src/components/CreatureList.test.tsx diff --git a/src/components/CreatureList.tsx b/temp-frontend/src/components/CreatureList.tsx similarity index 100% rename from src/components/CreatureList.tsx rename to temp-frontend/src/components/CreatureList.tsx diff --git a/src/components/EncounterToolbar.tsx b/temp-frontend/src/components/EncounterToolbar.tsx similarity index 100% rename from src/components/EncounterToolbar.tsx rename to temp-frontend/src/components/EncounterToolbar.tsx diff --git a/src/components/FloatingNextTurn.tsx b/temp-frontend/src/components/FloatingNextTurn.tsx similarity index 100% rename from src/components/FloatingNextTurn.tsx rename to temp-frontend/src/components/FloatingNextTurn.tsx diff --git a/src/components/Footer.tsx b/temp-frontend/src/components/Footer.tsx similarity index 100% rename from src/components/Footer.tsx rename to temp-frontend/src/components/Footer.tsx diff --git a/src/components/HealthBar.test.tsx b/temp-frontend/src/components/HealthBar.test.tsx similarity index 100% rename from src/components/HealthBar.test.tsx rename to temp-frontend/src/components/HealthBar.test.tsx diff --git a/src/components/HealthBar.tsx b/temp-frontend/src/components/HealthBar.tsx similarity index 100% rename from src/components/HealthBar.tsx rename to temp-frontend/src/components/HealthBar.tsx diff --git a/src/components/HpControls.tsx b/temp-frontend/src/components/HpControls.tsx similarity index 100% rename from src/components/HpControls.tsx rename to temp-frontend/src/components/HpControls.tsx diff --git a/src/components/MonsterAutocomplete.tsx b/temp-frontend/src/components/MonsterAutocomplete.tsx similarity index 100% rename from src/components/MonsterAutocomplete.tsx rename to temp-frontend/src/components/MonsterAutocomplete.tsx diff --git a/src/components/StatBlockPanel.tsx b/temp-frontend/src/components/StatBlockPanel.tsx similarity index 100% rename from src/components/StatBlockPanel.tsx rename to temp-frontend/src/components/StatBlockPanel.tsx diff --git a/src/components/TurnControls.test.tsx b/temp-frontend/src/components/TurnControls.test.tsx similarity index 100% rename from src/components/TurnControls.test.tsx rename to temp-frontend/src/components/TurnControls.test.tsx diff --git a/src/components/TurnControls.tsx b/temp-frontend/src/components/TurnControls.tsx similarity index 100% rename from src/components/TurnControls.tsx rename to temp-frontend/src/components/TurnControls.tsx diff --git a/src/hooks/useEncounter.test.ts b/temp-frontend/src/hooks/useEncounter.test.ts similarity index 100% rename from src/hooks/useEncounter.test.ts rename to temp-frontend/src/hooks/useEncounter.test.ts diff --git a/src/hooks/useEncounter.ts b/temp-frontend/src/hooks/useEncounter.ts similarity index 100% rename from src/hooks/useEncounter.ts rename to temp-frontend/src/hooks/useEncounter.ts diff --git a/src/hooks/useEncounterSettings.test.ts b/temp-frontend/src/hooks/useEncounterSettings.test.ts similarity index 100% rename from src/hooks/useEncounterSettings.test.ts rename to temp-frontend/src/hooks/useEncounterSettings.test.ts diff --git a/src/hooks/useEncounterSettings.ts b/temp-frontend/src/hooks/useEncounterSettings.ts similarity index 100% rename from src/hooks/useEncounterSettings.ts rename to temp-frontend/src/hooks/useEncounterSettings.ts diff --git a/src/hooks/useTurnTracker.test.ts b/temp-frontend/src/hooks/useTurnTracker.test.ts similarity index 100% rename from src/hooks/useTurnTracker.test.ts rename to temp-frontend/src/hooks/useTurnTracker.test.ts diff --git a/src/hooks/useTurnTracker.ts b/temp-frontend/src/hooks/useTurnTracker.ts similarity index 100% rename from src/hooks/useTurnTracker.ts rename to temp-frontend/src/hooks/useTurnTracker.ts diff --git a/src/index.css b/temp-frontend/src/index.css similarity index 100% rename from src/index.css rename to temp-frontend/src/index.css diff --git a/src/main.tsx b/temp-frontend/src/main.tsx similarity index 100% rename from src/main.tsx rename to temp-frontend/src/main.tsx diff --git a/src/test/setup.ts b/temp-frontend/src/test/setup.ts similarity index 100% rename from src/test/setup.ts rename to temp-frontend/src/test/setup.ts diff --git a/src/types/creature.ts b/temp-frontend/src/types/creature.ts similarity index 100% rename from src/types/creature.ts rename to temp-frontend/src/types/creature.ts diff --git a/src/types/encounterSettings.ts b/temp-frontend/src/types/encounterSettings.ts similarity index 100% rename from src/types/encounterSettings.ts rename to temp-frontend/src/types/encounterSettings.ts diff --git a/src/types/statBlock.ts b/temp-frontend/src/types/statBlock.ts similarity index 100% rename from src/types/statBlock.ts rename to temp-frontend/src/types/statBlock.ts diff --git a/src/types/turnState.ts b/temp-frontend/src/types/turnState.ts similarity index 100% rename from src/types/turnState.ts rename to temp-frontend/src/types/turnState.ts diff --git a/src/types/viewMode.ts b/temp-frontend/src/types/viewMode.ts similarity index 100% rename from src/types/viewMode.ts rename to temp-frontend/src/types/viewMode.ts diff --git a/src/utils/formatInGameTime.test.ts b/temp-frontend/src/utils/formatInGameTime.test.ts similarity index 100% rename from src/utils/formatInGameTime.test.ts rename to temp-frontend/src/utils/formatInGameTime.test.ts diff --git a/src/utils/formatInGameTime.ts b/temp-frontend/src/utils/formatInGameTime.ts similarity index 100% rename from src/utils/formatInGameTime.ts rename to temp-frontend/src/utils/formatInGameTime.ts diff --git a/src/utils/formatModifier.test.ts b/temp-frontend/src/utils/formatModifier.test.ts similarity index 100% rename from src/utils/formatModifier.test.ts rename to temp-frontend/src/utils/formatModifier.test.ts diff --git a/src/utils/formatModifier.ts b/temp-frontend/src/utils/formatModifier.ts similarity index 100% rename from src/utils/formatModifier.ts rename to temp-frontend/src/utils/formatModifier.ts diff --git a/src/utils/liquidColors.ts b/temp-frontend/src/utils/liquidColors.ts similarity index 100% rename from src/utils/liquidColors.ts rename to temp-frontend/src/utils/liquidColors.ts diff --git a/src/utils/rollInitiative.test.ts b/temp-frontend/src/utils/rollInitiative.test.ts similarity index 100% rename from src/utils/rollInitiative.test.ts rename to temp-frontend/src/utils/rollInitiative.test.ts diff --git a/src/utils/rollInitiative.ts b/temp-frontend/src/utils/rollInitiative.ts similarity index 100% rename from src/utils/rollInitiative.ts rename to temp-frontend/src/utils/rollInitiative.ts diff --git a/tsconfig.app.json b/temp-frontend/tsconfig.app.json similarity index 100% rename from tsconfig.app.json rename to temp-frontend/tsconfig.app.json diff --git a/tsconfig.json b/temp-frontend/tsconfig.json similarity index 100% rename from tsconfig.json rename to temp-frontend/tsconfig.json diff --git a/tsconfig.node.json b/temp-frontend/tsconfig.node.json similarity index 100% rename from tsconfig.node.json rename to temp-frontend/tsconfig.node.json diff --git a/vite.config.ts b/temp-frontend/vite.config.ts similarity index 100% rename from vite.config.ts rename to temp-frontend/vite.config.ts diff --git a/vitest.config.ts b/temp-frontend/vitest.config.ts similarity index 100% rename from vitest.config.ts rename to temp-frontend/vitest.config.ts From 3f705452dbf36fa8a1b1764b312c88d0c99bac06 Mon Sep 17 00:00:00 2001 From: phortx Date: Mon, 25 May 2026 13:48:56 +0200 Subject: [PATCH 02/11] feat: initialize spring boot --- .gitattributes | 3 + .gitignore | 40 ++- .tool-versions | 2 + build.gradle.kts | 60 +++++ compose.yaml | 9 + gradle/wrapper/gradle-wrapper.jar | Bin 0 -> 48966 bytes gradle/wrapper/gradle-wrapper.properties | 7 + gradlew | 248 ++++++++++++++++++ gradlew.bat | 93 +++++++ settings.gradle.kts | 1 + .../mithril_forge/MithrilForgeApplication.kt | 11 + src/main/resources/application.yaml | 3 + .../MithrilForgeApplicationTests.kt | 15 ++ .../TestcontainersConfiguration.kt | 16 ++ src/test/resources/docker-java.properties | 1 + 15 files changed, 508 insertions(+), 1 deletion(-) create mode 100644 .gitattributes create mode 100644 build.gradle.kts create mode 100644 compose.yaml create mode 100644 gradle/wrapper/gradle-wrapper.jar create mode 100644 gradle/wrapper/gradle-wrapper.properties create mode 100755 gradlew create mode 100644 gradlew.bat create mode 100644 settings.gradle.kts create mode 100644 src/main/kotlin/de/entropy_labs/mithril_forge/MithrilForgeApplication.kt create mode 100644 src/main/resources/application.yaml create mode 100644 src/test/kotlin/de/entropy_labs/mithril_forge/MithrilForgeApplicationTests.kt create mode 100644 src/test/kotlin/de/entropy_labs/mithril_forge/TestcontainersConfiguration.kt create mode 100644 src/test/resources/docker-java.properties diff --git a/.gitattributes b/.gitattributes new file mode 100644 index 0000000..8af972c --- /dev/null +++ b/.gitattributes @@ -0,0 +1,3 @@ +/gradlew text eol=lf +*.bat text eol=crlf +*.jar binary diff --git a/.gitignore b/.gitignore index a0fab83..70b0429 100644 --- a/.gitignore +++ b/.gitignore @@ -1,3 +1,42 @@ +HELP.md +.gradle +build/ +!gradle/wrapper/gradle-wrapper.jar +!**/src/main/**/build/ +!**/src/test/**/build/ + +### STS ### +.apt_generated +.classpath +.factorypath +.project +.settings +.springBeans +.sts4-cache +bin/ +!**/src/main/**/bin/ +!**/src/test/**/bin/ + +### IntelliJ IDEA ### +.idea +*.iws +*.iml +*.ipr +out/ +!**/src/main/**/out/ +!**/src/test/**/out/ + +### NetBeans ### +/nbproject/private/ +/nbbuild/ +/dist/ +/nbdist/ +/.nb-gradle/ + +### Kotlin ### +.kotlin + + # Logs logs *.log @@ -15,7 +54,6 @@ dist-ssr # Editor directories and files .vscode/* !.vscode/extensions.json -.idea .DS_Store *.suo *.ntvs* diff --git a/.tool-versions b/.tool-versions index 0bf6966..790917b 100644 --- a/.tool-versions +++ b/.tool-versions @@ -1 +1,3 @@ bun 1.3.13 +kotlin 2.3.21 +java graalvm-community-24.0.1 diff --git a/build.gradle.kts b/build.gradle.kts new file mode 100644 index 0000000..fddb722 --- /dev/null +++ b/build.gradle.kts @@ -0,0 +1,60 @@ +plugins { + kotlin("jvm") version "2.2.21" + kotlin("plugin.spring") version "2.2.21" + id("org.springframework.boot") version "4.0.6" + id("io.spring.dependency-management") version "1.1.7" +} + +group = "de.entropy-labs" +version = "0.0.1-SNAPSHOT" + +java { + toolchain { + languageVersion = JavaLanguageVersion.of(24) + } +} + +repositories { + mavenCentral() +} + +extra["sentryVersion"] = "8.27.0" +extra["testcontainersVersion"] = "1.21.3" + +dependencies { + implementation("org.springframework.boot:spring-boot-starter-jdbc") + implementation("org.springframework.boot:spring-boot-starter-session-jdbc") + implementation("org.springframework.boot:spring-boot-starter-webmvc") + implementation("io.sentry:sentry-spring-boot-4-starter") + implementation("org.jetbrains.kotlin:kotlin-reflect") + implementation("org.springdoc:springdoc-openapi-starter-webmvc-ui:3.0.2") + implementation("tools.jackson.module:jackson-module-kotlin") + developmentOnly("org.springframework.boot:spring-boot-devtools") + developmentOnly("org.springframework.boot:spring-boot-docker-compose") + runtimeOnly("org.postgresql:postgresql") + testImplementation("org.springframework.boot:spring-boot-starter-jdbc-test") + testImplementation("org.springframework.boot:spring-boot-starter-session-jdbc-test") + testImplementation("org.springframework.boot:spring-boot-starter-webmvc-test") + testImplementation("org.springframework.boot:spring-boot-testcontainers") + testImplementation("org.testcontainers:junit-jupiter") + testImplementation("org.testcontainers:postgresql") + testImplementation("org.jetbrains.kotlin:kotlin-test-junit5") + testRuntimeOnly("org.junit.platform:junit-platform-launcher") +} + +dependencyManagement { + imports { + mavenBom("io.sentry:sentry-bom:${property("sentryVersion")}") + mavenBom("org.testcontainers:testcontainers-bom:${property("testcontainersVersion")}") + } +} + +kotlin { + compilerOptions { + freeCompilerArgs.addAll("-Xjsr305=strict", "-Xannotation-default-target=param-property") + } +} + +tasks.withType { + useJUnitPlatform() +} diff --git a/compose.yaml b/compose.yaml new file mode 100644 index 0000000..7c8044f --- /dev/null +++ b/compose.yaml @@ -0,0 +1,9 @@ +services: + postgres: + image: 'postgres:latest' + environment: + - 'POSTGRES_DB=mydatabase' + - 'POSTGRES_PASSWORD=secret' + - 'POSTGRES_USER=myuser' + ports: + - '5432' diff --git a/gradle/wrapper/gradle-wrapper.jar b/gradle/wrapper/gradle-wrapper.jar new file mode 100644 index 0000000000000000000000000000000000000000..d997cfc60f4cff0e7451d19d49a82fa986695d07 GIT binary patch literal 48966 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

`Lem|V$A>eR0uN8Y&sdjtu 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

1bvi{DS7E@q1{$_8->K{_OWv2 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 literal 0 HcmV?d00001 diff --git a/gradle/wrapper/gradle-wrapper.properties b/gradle/wrapper/gradle-wrapper.properties new file mode 100644 index 0000000..c61a118 --- /dev/null +++ b/gradle/wrapper/gradle-wrapper.properties @@ -0,0 +1,7 @@ +distributionBase=GRADLE_USER_HOME +distributionPath=wrapper/dists +distributionUrl=https\://services.gradle.org/distributions/gradle-9.4.1-bin.zip +networkTimeout=10000 +validateDistributionUrl=true +zipStoreBase=GRADLE_USER_HOME +zipStorePath=wrapper/dists diff --git a/gradlew b/gradlew new file mode 100755 index 0000000..739907d --- /dev/null +++ b/gradlew @@ -0,0 +1,248 @@ +#!/bin/sh + +# +# Copyright © 2015 the original authors. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# https://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# +# SPDX-License-Identifier: Apache-2.0 +# + +############################################################################## +# +# Gradle start up script for POSIX generated by Gradle. +# +# Important for running: +# +# (1) You need a POSIX-compliant shell to run this script. If your /bin/sh is +# noncompliant, but you have some other compliant shell such as ksh or +# bash, then to run this script, type that shell name before the whole +# command line, like: +# +# ksh Gradle +# +# Busybox and similar reduced shells will NOT work, because this script +# requires all of these POSIX shell features: +# * functions; +# * expansions «$var», «${var}», «${var:-default}», «${var+SET}», +# «${var#prefix}», «${var%suffix}», and «$( cmd )»; +# * compound commands having a testable exit status, especially «case»; +# * various built-in commands including «command», «set», and «ulimit». +# +# Important for patching: +# +# (2) This script targets any POSIX shell, so it avoids extensions provided +# by Bash, Ksh, etc; in particular arrays are avoided. +# +# The "traditional" practice of packing multiple parameters into a +# space-separated string is a well documented source of bugs and security +# problems, so this is (mostly) avoided, by progressively accumulating +# options in "$@", and eventually passing that to Java. +# +# Where the inherited environment variables (DEFAULT_JVM_OPTS, JAVA_OPTS, +# and GRADLE_OPTS) rely on word-splitting, this is performed explicitly; +# see the in-line comments for details. +# +# There are tweaks for specific operating systems such as AIX, CygWin, +# Darwin, MinGW, and NonStop. +# +# (3) This script is generated from the Groovy template +# https://github.com/gradle/gradle/blob/2d6327017519d23b96af35865dc997fcb544fb40/platforms/jvm/plugins-application/src/main/resources/org/gradle/api/internal/plugins/unixStartScript.txt +# within the Gradle project. +# +# You can find Gradle at https://github.com/gradle/gradle/. +# +############################################################################## + +# Attempt to set APP_HOME + +# Resolve links: $0 may be a link +app_path=$0 + +# Need this for daisy-chained symlinks. +while + APP_HOME=${app_path%"${app_path##*/}"} # leaves a trailing /; empty if no leading path + [ -h "$app_path" ] +do + ls=$( ls -ld "$app_path" ) + link=${ls#*' -> '} + case $link in #( + /*) app_path=$link ;; #( + *) app_path=$APP_HOME$link ;; + esac +done + +# This is normally unused +# shellcheck disable=SC2034 +APP_BASE_NAME=${0##*/} +# Discard cd standard output in case $CDPATH is set (https://github.com/gradle/gradle/issues/25036) +APP_HOME=$( cd -P "${APP_HOME:-./}" > /dev/null && printf '%s\n' "$PWD" ) || exit + +# Use the maximum available, or set MAX_FD != -1 to use that value. +MAX_FD=maximum + +warn () { + echo "$*" +} >&2 + +die () { + echo + echo "$*" + echo + exit 1 +} >&2 + +# OS specific support (must be 'true' or 'false'). +cygwin=false +msys=false +darwin=false +nonstop=false +case "$( uname )" in #( + CYGWIN* ) cygwin=true ;; #( + Darwin* ) darwin=true ;; #( + MSYS* | MINGW* ) msys=true ;; #( + NONSTOP* ) nonstop=true ;; +esac + + + +# Determine the Java command to use to start the JVM. +if [ -n "$JAVA_HOME" ] ; then + if [ -x "$JAVA_HOME/jre/sh/java" ] ; then + # IBM's JDK on AIX uses strange locations for the executables + JAVACMD=$JAVA_HOME/jre/sh/java + else + JAVACMD=$JAVA_HOME/bin/java + fi + if [ ! -x "$JAVACMD" ] ; then + die "ERROR: JAVA_HOME is set to an invalid directory: $JAVA_HOME + +Please set the JAVA_HOME variable in your environment to match the +location of your Java installation." + fi +else + JAVACMD=java + if ! command -v java >/dev/null 2>&1 + then + die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. + +Please set the JAVA_HOME variable in your environment to match the +location of your Java installation." + fi +fi + +# Increase the maximum file descriptors if we can. +if ! "$cygwin" && ! "$darwin" && ! "$nonstop" ; then + case $MAX_FD in #( + max*) + # In POSIX sh, ulimit -H is undefined. That's why the result is checked to see if it worked. + # shellcheck disable=SC2039,SC3045 + MAX_FD=$( ulimit -H -n ) || + warn "Could not query maximum file descriptor limit" + esac + case $MAX_FD in #( + '' | soft) :;; #( + *) + # In POSIX sh, ulimit -n is undefined. That's why the result is checked to see if it worked. + # shellcheck disable=SC2039,SC3045 + ulimit -n "$MAX_FD" || + warn "Could not set maximum file descriptor limit to $MAX_FD" + esac +fi + +# Collect all arguments for the java command, stacking in reverse order: +# * args from the command line +# * the main class name +# * -classpath +# * -D...appname settings +# * --module-path (only if needed) +# * DEFAULT_JVM_OPTS, JAVA_OPTS, and GRADLE_OPTS environment variables. + +# For Cygwin or MSYS, switch paths to Windows format before running java +if "$cygwin" || "$msys" ; then + APP_HOME=$( cygpath --path --mixed "$APP_HOME" ) + + JAVACMD=$( cygpath --unix "$JAVACMD" ) + + # Now convert the arguments - kludge to limit ourselves to /bin/sh + for arg do + if + case $arg in #( + -*) false ;; # don't mess with options #( + /?*) t=${arg#/} t=/${t%%/*} # looks like a POSIX filepath + [ -e "$t" ] ;; #( + *) false ;; + esac + then + arg=$( cygpath --path --ignore --mixed "$arg" ) + fi + # Roll the args list around exactly as many times as the number of + # args, so each arg winds up back in the position where it started, but + # possibly modified. + # + # NB: a `for` loop captures its iteration list before it begins, so + # changing the positional parameters here affects neither the number of + # iterations, nor the values presented in `arg`. + shift # remove old arg + set -- "$@" "$arg" # push replacement arg + done +fi + + +# Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. +DEFAULT_JVM_OPTS='"-Xmx64m" "-Xms64m"' + +# Collect all arguments for the java command: +# * DEFAULT_JVM_OPTS, JAVA_OPTS, and optsEnvironmentVar are not allowed to contain shell fragments, +# and any embedded shellness will be escaped. +# * For example: A user cannot expect ${Hostname} to be expanded, as it is an environment variable and will be +# treated as '${Hostname}' itself on the command line. + +set -- \ + "-Dorg.gradle.appname=$APP_BASE_NAME" \ + -jar "$APP_HOME/gradle/wrapper/gradle-wrapper.jar" \ + "$@" + +# Stop when "xargs" is not available. +if ! command -v xargs >/dev/null 2>&1 +then + die "xargs is not available" +fi + +# Use "xargs" to parse quoted args. +# +# With -n1 it outputs one arg per line, with the quotes and backslashes removed. +# +# In Bash we could simply go: +# +# readarray ARGS < <( xargs -n1 <<<"$var" ) && +# set -- "${ARGS[@]}" "$@" +# +# but POSIX shell has neither arrays nor command substitution, so instead we +# post-process each arg (as a line of input to sed) to backslash-escape any +# character that might be a shell metacharacter, then use eval to reverse +# that process (while maintaining the separation between arguments), and wrap +# the whole thing up as a single "set" statement. +# +# This will of course break if any of these variables contains a newline or +# an unmatched quote. +# + +eval "set -- $( + printf '%s\n' "$DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS" | + xargs -n1 | + sed ' s~[^-[:alnum:]+,./:=@_]~\\&~g; ' | + tr '\n' ' ' + )" '"$@"' + +exec "$JAVACMD" "$@" diff --git a/gradlew.bat b/gradlew.bat new file mode 100644 index 0000000..c4bdd3a --- /dev/null +++ b/gradlew.bat @@ -0,0 +1,93 @@ +@rem +@rem Copyright 2015 the original author or authors. +@rem +@rem Licensed under the Apache License, Version 2.0 (the "License"); +@rem you may not use this file except in compliance with the License. +@rem You may obtain a copy of the License at +@rem +@rem https://www.apache.org/licenses/LICENSE-2.0 +@rem +@rem Unless required by applicable law or agreed to in writing, software +@rem distributed under the License is distributed on an "AS IS" BASIS, +@rem WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +@rem See the License for the specific language governing permissions and +@rem limitations under the License. +@rem +@rem SPDX-License-Identifier: Apache-2.0 +@rem + +@if "%DEBUG%"=="" @echo off +@rem ########################################################################## +@rem +@rem Gradle startup script for Windows +@rem +@rem ########################################################################## + +@rem Set local scope for the variables with windows NT shell +if "%OS%"=="Windows_NT" setlocal + +set DIRNAME=%~dp0 +if "%DIRNAME%"=="" set DIRNAME=. +@rem This is normally unused +set APP_BASE_NAME=%~n0 +set APP_HOME=%DIRNAME% + +@rem Resolve any "." and ".." in APP_HOME to make it shorter. +for %%i in ("%APP_HOME%") do set APP_HOME=%%~fi + +@rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. +set DEFAULT_JVM_OPTS="-Xmx64m" "-Xms64m" + +@rem Find java.exe +if defined JAVA_HOME goto findJavaFromJavaHome + +set JAVA_EXE=java.exe +%JAVA_EXE% -version >NUL 2>&1 +if %ERRORLEVEL% equ 0 goto execute + +echo. 1>&2 +echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. 1>&2 +echo. 1>&2 +echo Please set the JAVA_HOME variable in your environment to match the 1>&2 +echo location of your Java installation. 1>&2 + +goto fail + +:findJavaFromJavaHome +set JAVA_HOME=%JAVA_HOME:"=% +set JAVA_EXE=%JAVA_HOME%/bin/java.exe + +if exist "%JAVA_EXE%" goto execute + +echo. 1>&2 +echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME% 1>&2 +echo. 1>&2 +echo Please set the JAVA_HOME variable in your environment to match the 1>&2 +echo location of your Java installation. 1>&2 + +goto fail + +:execute +@rem Setup the command line + + + +@rem Execute Gradle +"%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -jar "%APP_HOME%\gradle\wrapper\gradle-wrapper.jar" %* + +:end +@rem End local scope for the variables with windows NT shell +if %ERRORLEVEL% equ 0 goto mainEnd + +:fail +rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of +rem the _cmd.exe /c_ return code! +set EXIT_CODE=%ERRORLEVEL% +if %EXIT_CODE% equ 0 set EXIT_CODE=1 +if not ""=="%GRADLE_EXIT_CONSOLE%" exit %EXIT_CODE% +exit /b %EXIT_CODE% + +:mainEnd +if "%OS%"=="Windows_NT" endlocal + +:omega diff --git a/settings.gradle.kts b/settings.gradle.kts new file mode 100644 index 0000000..d6f12bb --- /dev/null +++ b/settings.gradle.kts @@ -0,0 +1 @@ +rootProject.name = "mithril-forge" diff --git a/src/main/kotlin/de/entropy_labs/mithril_forge/MithrilForgeApplication.kt b/src/main/kotlin/de/entropy_labs/mithril_forge/MithrilForgeApplication.kt new file mode 100644 index 0000000..7200a5a --- /dev/null +++ b/src/main/kotlin/de/entropy_labs/mithril_forge/MithrilForgeApplication.kt @@ -0,0 +1,11 @@ +package de.entropy_labs.mithril_forge + +import org.springframework.boot.autoconfigure.SpringBootApplication +import org.springframework.boot.runApplication + +@SpringBootApplication +class MithrilForgeApplication + +fun main(args: Array) { + runApplication(*args) +} diff --git a/src/main/resources/application.yaml b/src/main/resources/application.yaml new file mode 100644 index 0000000..14f6655 --- /dev/null +++ b/src/main/resources/application.yaml @@ -0,0 +1,3 @@ +spring: + application: + name: mithril-forge diff --git a/src/test/kotlin/de/entropy_labs/mithril_forge/MithrilForgeApplicationTests.kt b/src/test/kotlin/de/entropy_labs/mithril_forge/MithrilForgeApplicationTests.kt new file mode 100644 index 0000000..2055bc2 --- /dev/null +++ b/src/test/kotlin/de/entropy_labs/mithril_forge/MithrilForgeApplicationTests.kt @@ -0,0 +1,15 @@ +package de.entropy_labs.mithril_forge + +import org.junit.jupiter.api.Test +import org.springframework.boot.test.context.SpringBootTest +import org.springframework.context.annotation.Import + +@SpringBootTest +@Import(TestcontainersConfiguration::class) +class MithrilForgeApplicationTests { + + @Test + fun contextLoads() { + } + +} diff --git a/src/test/kotlin/de/entropy_labs/mithril_forge/TestcontainersConfiguration.kt b/src/test/kotlin/de/entropy_labs/mithril_forge/TestcontainersConfiguration.kt new file mode 100644 index 0000000..2ed4353 --- /dev/null +++ b/src/test/kotlin/de/entropy_labs/mithril_forge/TestcontainersConfiguration.kt @@ -0,0 +1,16 @@ +package de.entropy_labs.mithril_forge + +import org.springframework.boot.test.context.TestConfiguration +import org.springframework.boot.testcontainers.service.connection.ServiceConnection +import org.springframework.context.annotation.Bean +import org.testcontainers.containers.PostgreSQLContainer +import org.testcontainers.utility.DockerImageName + +@TestConfiguration(proxyBeanMethods = false) +class TestcontainersConfiguration { + + @Bean + @ServiceConnection + fun postgresContainer(): PostgreSQLContainer<*> = + PostgreSQLContainer(DockerImageName.parse("postgres:latest")) +} diff --git a/src/test/resources/docker-java.properties b/src/test/resources/docker-java.properties new file mode 100644 index 0000000..d06ebb9 --- /dev/null +++ b/src/test/resources/docker-java.properties @@ -0,0 +1 @@ +api.version=1.44 From d597b300be1e2108af3901f713095f170d6d1406 Mon Sep 17 00:00:00 2001 From: phortx Date: Mon, 25 May 2026 16:42:32 +0200 Subject: [PATCH 03/11] refactor: frontend and backend --- .gitignore | 4 ++++ .tool-versions | 1 + build.gradle.kts => backend/build.gradle.kts | 0 .../de/entropy_labs/mithril_forge/MithrilForgeApplication.kt | 0 {src => backend/src}/main/resources/application.yaml | 0 .../mithril_forge/MithrilForgeApplicationTests.kt | 0 .../entropy_labs/mithril_forge/TestcontainersConfiguration.kt | 0 {src => backend/src}/test/resources/docker-java.properties | 0 {temp-frontend => frontend}/bun.lock | 0 {temp-frontend => frontend}/eslint.config.js | 0 {temp-frontend => frontend}/index.html | 0 {temp-frontend => frontend}/package.json | 0 {temp-frontend => frontend}/public/favicon.svg | 0 {temp-frontend => frontend}/public/icons.svg | 0 {temp-frontend => frontend}/src/App.tsx | 0 {temp-frontend => frontend}/src/api/open5e.test.ts | 0 {temp-frontend => frontend}/src/api/open5e.ts | 0 .../src/components/AddCreatureForm.test.tsx | 0 .../src/components/AddCreatureForm.tsx | 0 .../src/components/CreatureList.test.tsx | 0 {temp-frontend => frontend}/src/components/CreatureList.tsx | 0 .../src/components/EncounterToolbar.tsx | 0 .../src/components/FloatingNextTurn.tsx | 0 {temp-frontend => frontend}/src/components/Footer.tsx | 0 {temp-frontend => frontend}/src/components/HealthBar.test.tsx | 0 {temp-frontend => frontend}/src/components/HealthBar.tsx | 0 {temp-frontend => frontend}/src/components/HpControls.tsx | 0 .../src/components/MonsterAutocomplete.tsx | 0 {temp-frontend => frontend}/src/components/StatBlockPanel.tsx | 0 .../src/components/TurnControls.test.tsx | 0 {temp-frontend => frontend}/src/components/TurnControls.tsx | 0 {temp-frontend => frontend}/src/hooks/useEncounter.test.ts | 0 {temp-frontend => frontend}/src/hooks/useEncounter.ts | 0 .../src/hooks/useEncounterSettings.test.ts | 0 {temp-frontend => frontend}/src/hooks/useEncounterSettings.ts | 0 {temp-frontend => frontend}/src/hooks/useTurnTracker.test.ts | 0 {temp-frontend => frontend}/src/hooks/useTurnTracker.ts | 0 {temp-frontend => frontend}/src/index.css | 0 {temp-frontend => frontend}/src/main.tsx | 0 {temp-frontend => frontend}/src/test/setup.ts | 0 {temp-frontend => frontend}/src/types/creature.ts | 0 {temp-frontend => frontend}/src/types/encounterSettings.ts | 0 {temp-frontend => frontend}/src/types/statBlock.ts | 0 {temp-frontend => frontend}/src/types/turnState.ts | 0 {temp-frontend => frontend}/src/types/viewMode.ts | 0 .../src/utils/formatInGameTime.test.ts | 0 {temp-frontend => frontend}/src/utils/formatInGameTime.ts | 0 {temp-frontend => frontend}/src/utils/formatModifier.test.ts | 0 {temp-frontend => frontend}/src/utils/formatModifier.ts | 0 {temp-frontend => frontend}/src/utils/liquidColors.ts | 0 {temp-frontend => frontend}/src/utils/rollInitiative.test.ts | 0 {temp-frontend => frontend}/src/utils/rollInitiative.ts | 0 {temp-frontend => frontend}/tsconfig.app.json | 0 {temp-frontend => frontend}/tsconfig.json | 0 {temp-frontend => frontend}/tsconfig.node.json | 0 {temp-frontend => frontend}/vite.config.ts | 0 {temp-frontend => frontend}/vitest.config.ts | 0 settings.gradle.kts | 2 ++ 58 files changed, 7 insertions(+) rename build.gradle.kts => backend/build.gradle.kts (100%) rename {src => backend/src}/main/kotlin/de/entropy_labs/mithril_forge/MithrilForgeApplication.kt (100%) rename {src => backend/src}/main/resources/application.yaml (100%) rename {src => backend/src}/test/kotlin/de/entropy_labs/mithril_forge/MithrilForgeApplicationTests.kt (100%) rename {src => backend/src}/test/kotlin/de/entropy_labs/mithril_forge/TestcontainersConfiguration.kt (100%) rename {src => backend/src}/test/resources/docker-java.properties (100%) rename {temp-frontend => frontend}/bun.lock (100%) rename {temp-frontend => frontend}/eslint.config.js (100%) rename {temp-frontend => frontend}/index.html (100%) rename {temp-frontend => frontend}/package.json (100%) rename {temp-frontend => frontend}/public/favicon.svg (100%) rename {temp-frontend => frontend}/public/icons.svg (100%) rename {temp-frontend => frontend}/src/App.tsx (100%) rename {temp-frontend => frontend}/src/api/open5e.test.ts (100%) rename {temp-frontend => frontend}/src/api/open5e.ts (100%) rename {temp-frontend => frontend}/src/components/AddCreatureForm.test.tsx (100%) rename {temp-frontend => frontend}/src/components/AddCreatureForm.tsx (100%) rename {temp-frontend => frontend}/src/components/CreatureList.test.tsx (100%) rename {temp-frontend => frontend}/src/components/CreatureList.tsx (100%) rename {temp-frontend => frontend}/src/components/EncounterToolbar.tsx (100%) rename {temp-frontend => frontend}/src/components/FloatingNextTurn.tsx (100%) rename {temp-frontend => frontend}/src/components/Footer.tsx (100%) rename {temp-frontend => frontend}/src/components/HealthBar.test.tsx (100%) rename {temp-frontend => frontend}/src/components/HealthBar.tsx (100%) rename {temp-frontend => frontend}/src/components/HpControls.tsx (100%) rename {temp-frontend => frontend}/src/components/MonsterAutocomplete.tsx (100%) rename {temp-frontend => frontend}/src/components/StatBlockPanel.tsx (100%) rename {temp-frontend => frontend}/src/components/TurnControls.test.tsx (100%) rename {temp-frontend => frontend}/src/components/TurnControls.tsx (100%) rename {temp-frontend => frontend}/src/hooks/useEncounter.test.ts (100%) rename {temp-frontend => frontend}/src/hooks/useEncounter.ts (100%) rename {temp-frontend => frontend}/src/hooks/useEncounterSettings.test.ts (100%) rename {temp-frontend => frontend}/src/hooks/useEncounterSettings.ts (100%) rename {temp-frontend => frontend}/src/hooks/useTurnTracker.test.ts (100%) rename {temp-frontend => frontend}/src/hooks/useTurnTracker.ts (100%) rename {temp-frontend => frontend}/src/index.css (100%) rename {temp-frontend => frontend}/src/main.tsx (100%) rename {temp-frontend => frontend}/src/test/setup.ts (100%) rename {temp-frontend => frontend}/src/types/creature.ts (100%) rename {temp-frontend => frontend}/src/types/encounterSettings.ts (100%) rename {temp-frontend => frontend}/src/types/statBlock.ts (100%) rename {temp-frontend => frontend}/src/types/turnState.ts (100%) rename {temp-frontend => frontend}/src/types/viewMode.ts (100%) rename {temp-frontend => frontend}/src/utils/formatInGameTime.test.ts (100%) rename {temp-frontend => frontend}/src/utils/formatInGameTime.ts (100%) rename {temp-frontend => frontend}/src/utils/formatModifier.test.ts (100%) rename {temp-frontend => frontend}/src/utils/formatModifier.ts (100%) rename {temp-frontend => frontend}/src/utils/liquidColors.ts (100%) rename {temp-frontend => frontend}/src/utils/rollInitiative.test.ts (100%) rename {temp-frontend => frontend}/src/utils/rollInitiative.ts (100%) rename {temp-frontend => frontend}/tsconfig.app.json (100%) rename {temp-frontend => frontend}/tsconfig.json (100%) rename {temp-frontend => frontend}/tsconfig.node.json (100%) rename {temp-frontend => frontend}/vite.config.ts (100%) rename {temp-frontend => frontend}/vitest.config.ts (100%) diff --git a/.gitignore b/.gitignore index 70b0429..fa07165 100644 --- a/.gitignore +++ b/.gitignore @@ -50,6 +50,10 @@ node_modules dist dist-ssr *.local +.vite + +# Frontend build output bundled into Spring Boot static resources +backend/src/main/resources/static/ # Editor directories and files .vscode/* diff --git a/.tool-versions b/.tool-versions index 790917b..7718dee 100644 --- a/.tool-versions +++ b/.tool-versions @@ -1,3 +1,4 @@ bun 1.3.13 +nodejs 25.9.0 kotlin 2.3.21 java graalvm-community-24.0.1 diff --git a/build.gradle.kts b/backend/build.gradle.kts similarity index 100% rename from build.gradle.kts rename to backend/build.gradle.kts diff --git a/src/main/kotlin/de/entropy_labs/mithril_forge/MithrilForgeApplication.kt b/backend/src/main/kotlin/de/entropy_labs/mithril_forge/MithrilForgeApplication.kt similarity index 100% rename from src/main/kotlin/de/entropy_labs/mithril_forge/MithrilForgeApplication.kt rename to backend/src/main/kotlin/de/entropy_labs/mithril_forge/MithrilForgeApplication.kt diff --git a/src/main/resources/application.yaml b/backend/src/main/resources/application.yaml similarity index 100% rename from src/main/resources/application.yaml rename to backend/src/main/resources/application.yaml diff --git a/src/test/kotlin/de/entropy_labs/mithril_forge/MithrilForgeApplicationTests.kt b/backend/src/test/kotlin/de/entropy_labs/mithril_forge/MithrilForgeApplicationTests.kt similarity index 100% rename from src/test/kotlin/de/entropy_labs/mithril_forge/MithrilForgeApplicationTests.kt rename to backend/src/test/kotlin/de/entropy_labs/mithril_forge/MithrilForgeApplicationTests.kt diff --git a/src/test/kotlin/de/entropy_labs/mithril_forge/TestcontainersConfiguration.kt b/backend/src/test/kotlin/de/entropy_labs/mithril_forge/TestcontainersConfiguration.kt similarity index 100% rename from src/test/kotlin/de/entropy_labs/mithril_forge/TestcontainersConfiguration.kt rename to backend/src/test/kotlin/de/entropy_labs/mithril_forge/TestcontainersConfiguration.kt diff --git a/src/test/resources/docker-java.properties b/backend/src/test/resources/docker-java.properties similarity index 100% rename from src/test/resources/docker-java.properties rename to backend/src/test/resources/docker-java.properties diff --git a/temp-frontend/bun.lock b/frontend/bun.lock similarity index 100% rename from temp-frontend/bun.lock rename to frontend/bun.lock diff --git a/temp-frontend/eslint.config.js b/frontend/eslint.config.js similarity index 100% rename from temp-frontend/eslint.config.js rename to frontend/eslint.config.js diff --git a/temp-frontend/index.html b/frontend/index.html similarity index 100% rename from temp-frontend/index.html rename to frontend/index.html diff --git a/temp-frontend/package.json b/frontend/package.json similarity index 100% rename from temp-frontend/package.json rename to frontend/package.json diff --git a/temp-frontend/public/favicon.svg b/frontend/public/favicon.svg similarity index 100% rename from temp-frontend/public/favicon.svg rename to frontend/public/favicon.svg diff --git a/temp-frontend/public/icons.svg b/frontend/public/icons.svg similarity index 100% rename from temp-frontend/public/icons.svg rename to frontend/public/icons.svg diff --git a/temp-frontend/src/App.tsx b/frontend/src/App.tsx similarity index 100% rename from temp-frontend/src/App.tsx rename to frontend/src/App.tsx diff --git a/temp-frontend/src/api/open5e.test.ts b/frontend/src/api/open5e.test.ts similarity index 100% rename from temp-frontend/src/api/open5e.test.ts rename to frontend/src/api/open5e.test.ts diff --git a/temp-frontend/src/api/open5e.ts b/frontend/src/api/open5e.ts similarity index 100% rename from temp-frontend/src/api/open5e.ts rename to frontend/src/api/open5e.ts diff --git a/temp-frontend/src/components/AddCreatureForm.test.tsx b/frontend/src/components/AddCreatureForm.test.tsx similarity index 100% rename from temp-frontend/src/components/AddCreatureForm.test.tsx rename to frontend/src/components/AddCreatureForm.test.tsx diff --git a/temp-frontend/src/components/AddCreatureForm.tsx b/frontend/src/components/AddCreatureForm.tsx similarity index 100% rename from temp-frontend/src/components/AddCreatureForm.tsx rename to frontend/src/components/AddCreatureForm.tsx diff --git a/temp-frontend/src/components/CreatureList.test.tsx b/frontend/src/components/CreatureList.test.tsx similarity index 100% rename from temp-frontend/src/components/CreatureList.test.tsx rename to frontend/src/components/CreatureList.test.tsx diff --git a/temp-frontend/src/components/CreatureList.tsx b/frontend/src/components/CreatureList.tsx similarity index 100% rename from temp-frontend/src/components/CreatureList.tsx rename to frontend/src/components/CreatureList.tsx diff --git a/temp-frontend/src/components/EncounterToolbar.tsx b/frontend/src/components/EncounterToolbar.tsx similarity index 100% rename from temp-frontend/src/components/EncounterToolbar.tsx rename to frontend/src/components/EncounterToolbar.tsx diff --git a/temp-frontend/src/components/FloatingNextTurn.tsx b/frontend/src/components/FloatingNextTurn.tsx similarity index 100% rename from temp-frontend/src/components/FloatingNextTurn.tsx rename to frontend/src/components/FloatingNextTurn.tsx diff --git a/temp-frontend/src/components/Footer.tsx b/frontend/src/components/Footer.tsx similarity index 100% rename from temp-frontend/src/components/Footer.tsx rename to frontend/src/components/Footer.tsx diff --git a/temp-frontend/src/components/HealthBar.test.tsx b/frontend/src/components/HealthBar.test.tsx similarity index 100% rename from temp-frontend/src/components/HealthBar.test.tsx rename to frontend/src/components/HealthBar.test.tsx diff --git a/temp-frontend/src/components/HealthBar.tsx b/frontend/src/components/HealthBar.tsx similarity index 100% rename from temp-frontend/src/components/HealthBar.tsx rename to frontend/src/components/HealthBar.tsx diff --git a/temp-frontend/src/components/HpControls.tsx b/frontend/src/components/HpControls.tsx similarity index 100% rename from temp-frontend/src/components/HpControls.tsx rename to frontend/src/components/HpControls.tsx diff --git a/temp-frontend/src/components/MonsterAutocomplete.tsx b/frontend/src/components/MonsterAutocomplete.tsx similarity index 100% rename from temp-frontend/src/components/MonsterAutocomplete.tsx rename to frontend/src/components/MonsterAutocomplete.tsx diff --git a/temp-frontend/src/components/StatBlockPanel.tsx b/frontend/src/components/StatBlockPanel.tsx similarity index 100% rename from temp-frontend/src/components/StatBlockPanel.tsx rename to frontend/src/components/StatBlockPanel.tsx diff --git a/temp-frontend/src/components/TurnControls.test.tsx b/frontend/src/components/TurnControls.test.tsx similarity index 100% rename from temp-frontend/src/components/TurnControls.test.tsx rename to frontend/src/components/TurnControls.test.tsx diff --git a/temp-frontend/src/components/TurnControls.tsx b/frontend/src/components/TurnControls.tsx similarity index 100% rename from temp-frontend/src/components/TurnControls.tsx rename to frontend/src/components/TurnControls.tsx diff --git a/temp-frontend/src/hooks/useEncounter.test.ts b/frontend/src/hooks/useEncounter.test.ts similarity index 100% rename from temp-frontend/src/hooks/useEncounter.test.ts rename to frontend/src/hooks/useEncounter.test.ts diff --git a/temp-frontend/src/hooks/useEncounter.ts b/frontend/src/hooks/useEncounter.ts similarity index 100% rename from temp-frontend/src/hooks/useEncounter.ts rename to frontend/src/hooks/useEncounter.ts diff --git a/temp-frontend/src/hooks/useEncounterSettings.test.ts b/frontend/src/hooks/useEncounterSettings.test.ts similarity index 100% rename from temp-frontend/src/hooks/useEncounterSettings.test.ts rename to frontend/src/hooks/useEncounterSettings.test.ts diff --git a/temp-frontend/src/hooks/useEncounterSettings.ts b/frontend/src/hooks/useEncounterSettings.ts similarity index 100% rename from temp-frontend/src/hooks/useEncounterSettings.ts rename to frontend/src/hooks/useEncounterSettings.ts diff --git a/temp-frontend/src/hooks/useTurnTracker.test.ts b/frontend/src/hooks/useTurnTracker.test.ts similarity index 100% rename from temp-frontend/src/hooks/useTurnTracker.test.ts rename to frontend/src/hooks/useTurnTracker.test.ts diff --git a/temp-frontend/src/hooks/useTurnTracker.ts b/frontend/src/hooks/useTurnTracker.ts similarity index 100% rename from temp-frontend/src/hooks/useTurnTracker.ts rename to frontend/src/hooks/useTurnTracker.ts diff --git a/temp-frontend/src/index.css b/frontend/src/index.css similarity index 100% rename from temp-frontend/src/index.css rename to frontend/src/index.css diff --git a/temp-frontend/src/main.tsx b/frontend/src/main.tsx similarity index 100% rename from temp-frontend/src/main.tsx rename to frontend/src/main.tsx diff --git a/temp-frontend/src/test/setup.ts b/frontend/src/test/setup.ts similarity index 100% rename from temp-frontend/src/test/setup.ts rename to frontend/src/test/setup.ts diff --git a/temp-frontend/src/types/creature.ts b/frontend/src/types/creature.ts similarity index 100% rename from temp-frontend/src/types/creature.ts rename to frontend/src/types/creature.ts diff --git a/temp-frontend/src/types/encounterSettings.ts b/frontend/src/types/encounterSettings.ts similarity index 100% rename from temp-frontend/src/types/encounterSettings.ts rename to frontend/src/types/encounterSettings.ts diff --git a/temp-frontend/src/types/statBlock.ts b/frontend/src/types/statBlock.ts similarity index 100% rename from temp-frontend/src/types/statBlock.ts rename to frontend/src/types/statBlock.ts diff --git a/temp-frontend/src/types/turnState.ts b/frontend/src/types/turnState.ts similarity index 100% rename from temp-frontend/src/types/turnState.ts rename to frontend/src/types/turnState.ts diff --git a/temp-frontend/src/types/viewMode.ts b/frontend/src/types/viewMode.ts similarity index 100% rename from temp-frontend/src/types/viewMode.ts rename to frontend/src/types/viewMode.ts diff --git a/temp-frontend/src/utils/formatInGameTime.test.ts b/frontend/src/utils/formatInGameTime.test.ts similarity index 100% rename from temp-frontend/src/utils/formatInGameTime.test.ts rename to frontend/src/utils/formatInGameTime.test.ts diff --git a/temp-frontend/src/utils/formatInGameTime.ts b/frontend/src/utils/formatInGameTime.ts similarity index 100% rename from temp-frontend/src/utils/formatInGameTime.ts rename to frontend/src/utils/formatInGameTime.ts diff --git a/temp-frontend/src/utils/formatModifier.test.ts b/frontend/src/utils/formatModifier.test.ts similarity index 100% rename from temp-frontend/src/utils/formatModifier.test.ts rename to frontend/src/utils/formatModifier.test.ts diff --git a/temp-frontend/src/utils/formatModifier.ts b/frontend/src/utils/formatModifier.ts similarity index 100% rename from temp-frontend/src/utils/formatModifier.ts rename to frontend/src/utils/formatModifier.ts diff --git a/temp-frontend/src/utils/liquidColors.ts b/frontend/src/utils/liquidColors.ts similarity index 100% rename from temp-frontend/src/utils/liquidColors.ts rename to frontend/src/utils/liquidColors.ts diff --git a/temp-frontend/src/utils/rollInitiative.test.ts b/frontend/src/utils/rollInitiative.test.ts similarity index 100% rename from temp-frontend/src/utils/rollInitiative.test.ts rename to frontend/src/utils/rollInitiative.test.ts diff --git a/temp-frontend/src/utils/rollInitiative.ts b/frontend/src/utils/rollInitiative.ts similarity index 100% rename from temp-frontend/src/utils/rollInitiative.ts rename to frontend/src/utils/rollInitiative.ts diff --git a/temp-frontend/tsconfig.app.json b/frontend/tsconfig.app.json similarity index 100% rename from temp-frontend/tsconfig.app.json rename to frontend/tsconfig.app.json diff --git a/temp-frontend/tsconfig.json b/frontend/tsconfig.json similarity index 100% rename from temp-frontend/tsconfig.json rename to frontend/tsconfig.json diff --git a/temp-frontend/tsconfig.node.json b/frontend/tsconfig.node.json similarity index 100% rename from temp-frontend/tsconfig.node.json rename to frontend/tsconfig.node.json diff --git a/temp-frontend/vite.config.ts b/frontend/vite.config.ts similarity index 100% rename from temp-frontend/vite.config.ts rename to frontend/vite.config.ts diff --git a/temp-frontend/vitest.config.ts b/frontend/vitest.config.ts similarity index 100% rename from temp-frontend/vitest.config.ts rename to frontend/vitest.config.ts diff --git a/settings.gradle.kts b/settings.gradle.kts index d6f12bb..3a6b6e0 100644 --- a/settings.gradle.kts +++ b/settings.gradle.kts @@ -1 +1,3 @@ rootProject.name = "mithril-forge" + +include("backend") From 60b4611be2255adaa411f753696cd2563d551372 Mon Sep 17 00:00:00 2001 From: phortx Date: Mon, 25 May 2026 16:57:00 +0200 Subject: [PATCH 04/11] fix: vitest setup --- frontend/vitest.config.ts | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/frontend/vitest.config.ts b/frontend/vitest.config.ts index 9f20b0c..4785500 100644 --- a/frontend/vitest.config.ts +++ b/frontend/vitest.config.ts @@ -7,5 +7,10 @@ export default defineConfig({ environment: 'jsdom', setupFiles: ['./src/test/setup.ts'], globals: true, + // Node 25 enables experimental Web Storage by default and installs a + // non-configurable broken `localStorage` on globalThis before jsdom can + // attach its own. Disable it so jsdom's localStorage wins. + // See: https://github.com/vitest-dev/vitest/issues/8757 + execArgv: ['--no-experimental-webstorage'], }, }) From ec6c745fc0e1ca4928ba7d7b1f4bf65ec09785b3 Mon Sep 17 00:00:00 2001 From: phortx Date: Mon, 25 May 2026 18:28:38 +0200 Subject: [PATCH 05/11] fix: only bun, no node --- .github/workflows/ci.yml | 3 - .tool-versions | 1 - bunfig.toml | 2 + frontend/bun.lock | 137 ++---------------- frontend/bunfig.toml | 2 + frontend/package.json | 21 ++- frontend/src/api/open5e.test.ts | 47 +++--- frontend/src/api/open5e.ts | 4 + .../src/components/AddCreatureForm.test.tsx | 36 ++--- frontend/src/components/CreatureList.test.tsx | 20 +-- frontend/src/components/TurnControls.test.tsx | 12 +- frontend/src/test/setup.ts | 28 +++- frontend/src/types/bun-test-globals.d.ts | 11 ++ frontend/tsconfig.app.json | 2 +- frontend/tsconfig.node.json | 2 +- frontend/vitest.config.ts | 16 -- 16 files changed, 127 insertions(+), 217 deletions(-) create mode 100644 bunfig.toml create mode 100644 frontend/bunfig.toml create mode 100644 frontend/src/types/bun-test-globals.d.ts delete mode 100644 frontend/vitest.config.ts diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 055dd0d..758fca9 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -16,9 +16,6 @@ concurrency: group: pages cancel-in-progress: false -env: - FORCE_JAVASCRIPT_ACTIONS_TO_NODE24: true - jobs: ci: name: Lint, Typecheck & Build diff --git a/.tool-versions b/.tool-versions index 7718dee..790917b 100644 --- a/.tool-versions +++ b/.tool-versions @@ -1,4 +1,3 @@ bun 1.3.13 -nodejs 25.9.0 kotlin 2.3.21 java graalvm-community-24.0.1 diff --git a/bunfig.toml b/bunfig.toml new file mode 100644 index 0000000..298cddb --- /dev/null +++ b/bunfig.toml @@ -0,0 +1,2 @@ +[test] +preload = ["./frontend/src/test/setup.ts"] diff --git a/frontend/bun.lock b/frontend/bun.lock index 8be421d..aa48dfb 100644 --- a/frontend/bun.lock +++ b/frontend/bun.lock @@ -12,12 +12,13 @@ }, "devDependencies": { "@eslint/js": "^9.39.4", + "@happy-dom/global-registrator": "^20.0.0", "@tailwindcss/vite": "^4.2.2", "@testing-library/dom": "^10.4.1", "@testing-library/jest-dom": "^6.9.1", "@testing-library/react": "^16.3.2", "@testing-library/user-event": "^14.6.1", - "@types/node": "^24.12.0", + "@types/bun": "^1.3.0", "@types/react": "^19.2.14", "@types/react-dom": "^19.2.3", "@vitejs/plugin-react": "^6.0.1", @@ -25,24 +26,16 @@ "eslint-plugin-react-hooks": "^7.0.1", "eslint-plugin-react-refresh": "^0.5.2", "globals": "^17.4.0", - "jsdom": "^29.0.1", "tailwindcss": "^4.2.2", "typescript": "~5.9.3", "typescript-eslint": "^8.57.0", "vite": "^8.0.1", - "vitest": "^4.1.2", }, }, }, "packages": { "@adobe/css-tools": ["@adobe/css-tools@4.4.4", "", {}, "sha512-Elp+iwUx5rN5+Y8xLt5/GRoG20WGoDCQ/1Fb+1LiGtvwbDavuSk0jhD/eZdckHAuzcDzccnkv+rEjyWfRx18gg=="], - "@asamuzakjp/css-color": ["@asamuzakjp/css-color@5.1.1", "", { "dependencies": { "@csstools/css-calc": "^3.1.1", "@csstools/css-color-parser": "^4.0.2", "@csstools/css-parser-algorithms": "^4.0.0", "@csstools/css-tokenizer": "^4.0.0", "lru-cache": "^11.2.7" } }, "sha512-iGWN8E45Ws0XWx3D44Q1t6vX2LqhCKcwfmwBYCDsFrYFS6m4q/Ks61L2veETaLv+ckDC6+dTETJoaAAb7VjLiw=="], - - "@asamuzakjp/dom-selector": ["@asamuzakjp/dom-selector@7.0.4", "", { "dependencies": { "@asamuzakjp/nwsapi": "^2.3.9", "bidi-js": "^1.0.3", "css-tree": "^3.2.1", "is-potential-custom-element-name": "^1.0.1", "lru-cache": "^11.2.7" } }, "sha512-jXR6x4AcT3eIrS2fSNAwJpwirOkGcd+E7F7CP3zjdTqz9B/2huHOL8YJZBgekKwLML+u7qB/6P1LXQuMScsx0w=="], - - "@asamuzakjp/nwsapi": ["@asamuzakjp/nwsapi@2.3.9", "", {}, "sha512-n8GuYSrI9bF7FFZ/SjhwevlHc8xaVlb/7HmHelnc/PZXBD2ZR49NnN9sMMuDdEGPeeRQ5d0hqlSlEpgCX3Wl0Q=="], - "@babel/code-frame": ["@babel/code-frame@7.29.0", "", { "dependencies": { "@babel/helper-validator-identifier": "^7.28.5", "js-tokens": "^4.0.0", "picocolors": "^1.1.1" } }, "sha512-9NhCeYjq9+3uxgdtp20LSiJXJvN0FeCtNGpJxuMFZ1Kv3cWUNb6DOhJwUvcVCzKGR66cw4njwM6hrJLqgOwbcw=="], "@babel/compat-data": ["@babel/compat-data@7.29.0", "", {}, "sha512-T1NCJqT/j9+cn8fvkt7jtwbLBfLC/1y1c7NtCeXFRgzGTsafi68MRv8yzkYSapBnFA6L3U2VSc02ciDzoAJhJg=="], @@ -77,20 +70,6 @@ "@babel/types": ["@babel/types@7.29.0", "", { "dependencies": { "@babel/helper-string-parser": "^7.27.1", "@babel/helper-validator-identifier": "^7.28.5" } }, "sha512-LwdZHpScM4Qz8Xw2iKSzS+cfglZzJGvofQICy7W7v4caru4EaAmyUuO6BGrbyQ2mYV11W0U8j5mBhd14dd3B0A=="], - "@bramus/specificity": ["@bramus/specificity@2.4.2", "", { "dependencies": { "css-tree": "^3.0.0" }, "bin": { "specificity": "bin/cli.js" } }, "sha512-ctxtJ/eA+t+6q2++vj5j7FYX3nRu311q1wfYH3xjlLOsczhlhxAg2FWNUXhpGvAw3BWo1xBcvOV6/YLc2r5FJw=="], - - "@csstools/color-helpers": ["@csstools/color-helpers@6.0.2", "", {}, "sha512-LMGQLS9EuADloEFkcTBR3BwV/CGHV7zyDxVRtVDTwdI2Ca4it0CCVTT9wCkxSgokjE5Ho41hEPgb8OEUwoXr6Q=="], - - "@csstools/css-calc": ["@csstools/css-calc@3.1.1", "", { "peerDependencies": { "@csstools/css-parser-algorithms": "^4.0.0", "@csstools/css-tokenizer": "^4.0.0" } }, "sha512-HJ26Z/vmsZQqs/o3a6bgKslXGFAungXGbinULZO3eMsOyNJHeBBZfup5FiZInOghgoM4Hwnmw+OgbJCNg1wwUQ=="], - - "@csstools/css-color-parser": ["@csstools/css-color-parser@4.0.2", "", { "dependencies": { "@csstools/color-helpers": "^6.0.2", "@csstools/css-calc": "^3.1.1" }, "peerDependencies": { "@csstools/css-parser-algorithms": "^4.0.0", "@csstools/css-tokenizer": "^4.0.0" } }, "sha512-0GEfbBLmTFf0dJlpsNU7zwxRIH0/BGEMuXLTCvFYxuL1tNhqzTbtnFICyJLTNK4a+RechKP75e7w42ClXSnJQw=="], - - "@csstools/css-parser-algorithms": ["@csstools/css-parser-algorithms@4.0.0", "", { "peerDependencies": { "@csstools/css-tokenizer": "^4.0.0" } }, "sha512-+B87qS7fIG3L5h3qwJ/IFbjoVoOe/bpOdh9hAjXbvx0o8ImEmUsGXN0inFOnk2ChCFgqkkGFQ+TpM5rbhkKe4w=="], - - "@csstools/css-syntax-patches-for-csstree": ["@csstools/css-syntax-patches-for-csstree@1.1.2", "", { "peerDependencies": { "css-tree": "^3.2.1" }, "optionalPeers": ["css-tree"] }, "sha512-5GkLzz4prTIpoyeUiIu3iV6CSG3Plo7xRVOFPKI7FVEJ3mZ0A8SwK0XU3Gl7xAkiQ+mDyam+NNp875/C5y+jSA=="], - - "@csstools/css-tokenizer": ["@csstools/css-tokenizer@4.0.0", "", {}, "sha512-QxULHAm7cNu72w97JUNCBFODFaXpbDg+dP8b/oWFAZ2MTRppA3U00Y2L1HqaS4J6yBqxwa/Y3nMBaxVKbB/NsA=="], - "@emnapi/core": ["@emnapi/core@1.9.1", "", { "dependencies": { "@emnapi/wasi-threads": "1.2.0", "tslib": "^2.4.0" } }, "sha512-mukuNALVsoix/w1BJwFzwXBN/dHeejQtuVzcDsfOEsdpCumXb/E9j8w11h5S54tT1xhifGfbbSm/ICrObRb3KA=="], "@emnapi/runtime": ["@emnapi/runtime@1.9.1", "", { "dependencies": { "tslib": "^2.4.0" } }, "sha512-VYi5+ZVLhpgK4hQ0TAjiQiZ6ol0oe4mBx7mVv7IflsiEp0OWoVsp/+f9Vc1hOhE0TtkORVrI1GvzyreqpgWtkA=="], @@ -115,7 +94,7 @@ "@eslint/plugin-kit": ["@eslint/plugin-kit@0.4.1", "", { "dependencies": { "@eslint/core": "^0.17.0", "levn": "^0.4.1" } }, "sha512-43/qtrDUokr7LJqoF2c3+RInu/t4zfrpYdoSDfYyhg52rwLV6TnOvdG4fXm7IkSB3wErkcmJS9iEhjVtOSEjjA=="], - "@exodus/bytes": ["@exodus/bytes@1.15.0", "", { "peerDependencies": { "@noble/hashes": "^1.8.0 || ^2.0.0" }, "optionalPeers": ["@noble/hashes"] }, "sha512-UY0nlA+feH81UGSHv92sLEPLCeZFjXOuHhrIo0HQydScuQc8s0A7kL/UdgwgDq8g8ilksmuoF35YVTNphV2aBQ=="], + "@happy-dom/global-registrator": ["@happy-dom/global-registrator@20.9.0", "", { "dependencies": { "@types/node": ">=20.0.0", "happy-dom": "^20.9.0" } }, "sha512-lBW6/m5BIFl3pMuWPNN0lIOYw9LMCmPfix53ExS3FBi4E+NELEljQ3xH6aAV9IYiQRfn9YIIgzzMrD0vIcD7tw=="], "@humanfs/core": ["@humanfs/core@0.19.1", "", {}, "sha512-5DyQ4+1JEUzejeK1JGICcideyfUbGixgS9jNgex5nqkW+cY7WZhxBigmieN5Qnw9ZosSNVC9KQKyb+GUaGyKUA=="], @@ -171,8 +150,6 @@ "@rolldown/pluginutils": ["@rolldown/pluginutils@1.0.0-rc.7", "", {}, "sha512-qujRfC8sFVInYSPPMLQByRh7zhwkGFS4+tyMQ83srV1qrxL4g8E2tyxVVyxd0+8QeBM1mIk9KbWxkegRr76XzA=="], - "@standard-schema/spec": ["@standard-schema/spec@1.1.0", "", {}, "sha512-l2aFy5jALhniG5HgqrD6jXLi/rUWrKvqN/qJx6yoJsgKhblVd+iqqU4RCXavm/jPityDo5TCvKMnpjKnOriy0w=="], - "@tailwindcss/node": ["@tailwindcss/node@4.2.2", "", { "dependencies": { "@jridgewell/remapping": "^2.3.5", "enhanced-resolve": "^5.19.0", "jiti": "^2.6.1", "lightningcss": "1.32.0", "magic-string": "^0.30.21", "source-map-js": "^1.2.1", "tailwindcss": "4.2.2" } }, "sha512-pXS+wJ2gZpVXqFaUEjojq7jzMpTGf8rU6ipJz5ovJV6PUGmlJ+jvIwGrzdHdQ80Sg+wmQxUFuoW1UAAwHNEdFA=="], "@tailwindcss/oxide": ["@tailwindcss/oxide@4.2.2", "", { "optionalDependencies": { "@tailwindcss/oxide-android-arm64": "4.2.2", "@tailwindcss/oxide-darwin-arm64": "4.2.2", "@tailwindcss/oxide-darwin-x64": "4.2.2", "@tailwindcss/oxide-freebsd-x64": "4.2.2", "@tailwindcss/oxide-linux-arm-gnueabihf": "4.2.2", "@tailwindcss/oxide-linux-arm64-gnu": "4.2.2", "@tailwindcss/oxide-linux-arm64-musl": "4.2.2", "@tailwindcss/oxide-linux-x64-gnu": "4.2.2", "@tailwindcss/oxide-linux-x64-musl": "4.2.2", "@tailwindcss/oxide-wasm32-wasi": "4.2.2", "@tailwindcss/oxide-win32-arm64-msvc": "4.2.2", "@tailwindcss/oxide-win32-x64-msvc": "4.2.2" } }, "sha512-qEUA07+E5kehxYp9BVMpq9E8vnJuBHfJEC0vPC5e7iL/hw7HR61aDKoVoKzrG+QKp56vhNZe4qwkRmMC0zDLvg=="], @@ -215,9 +192,7 @@ "@types/aria-query": ["@types/aria-query@5.0.4", "", {}, "sha512-rfT93uj5s0PRL7EzccGMs3brplhcrghnDoV26NqKhCAS1hVo+WdNsPvE/yb6ilfr5hi2MEk6d5EWJTKdxg8jVw=="], - "@types/chai": ["@types/chai@5.2.3", "", { "dependencies": { "@types/deep-eql": "*", "assertion-error": "^2.0.1" } }, "sha512-Mw558oeA9fFbv65/y4mHtXDs9bPnFMZAL/jxdPFUpOHHIXX91mcgEHbS5Lahr+pwZFR8A7GQleRWeI6cGFC2UA=="], - - "@types/deep-eql": ["@types/deep-eql@4.0.2", "", {}, "sha512-c9h9dVVMigMPc4bwTvC5dxqtqJZwQPePsWjPlpSOnojbor6pGqdk541lfA7AqFQr5pB1BRdq0juY9db81BwyFw=="], + "@types/bun": ["@types/bun@1.3.14", "", { "dependencies": { "bun-types": "1.3.14" } }, "sha512-h1hFqFVcvAvD9j9K7ZW7vd82aSA+rTdznZa+5bwvCwqSB1jmmfLcbIWhOLx1/+boy/xmjgCs/OMUL8hRJSmnPw=="], "@types/estree": ["@types/estree@1.0.8", "", {}, "sha512-dWHzHa2WqEXI/O1E9OjrocMTKJl2mSrEolh1Iomrv6U+JuNwaHXsXx9bLu5gG7BUWFIN0skIQJQ/L1rIex4X6w=="], @@ -229,6 +204,10 @@ "@types/react-dom": ["@types/react-dom@19.2.3", "", { "peerDependencies": { "@types/react": "^19.2.0" } }, "sha512-jp2L/eY6fn+KgVVQAOqYItbF0VY/YApe5Mz2F0aykSO8gx31bYCZyvSeYxCHKvzHG5eZjc+zyaS5BrBWya2+kQ=="], + "@types/whatwg-mimetype": ["@types/whatwg-mimetype@3.0.2", "", {}, "sha512-c2AKvDT8ToxLIOUlN51gTiHXflsfIFisS4pO7pDPoKouJCESkhZnEy623gwP9laCy5lnLDAw1vAzu2vM2YLOrA=="], + + "@types/ws": ["@types/ws@8.18.1", "", { "dependencies": { "@types/node": "*" } }, "sha512-ThVF6DCVhA8kUGy+aazFQ4kXQ7E1Ty7A3ypFOe0IcJV8O/M511G99AW24irKrW56Wt44yG9+ij8FaqoBGkuBXg=="], + "@typescript-eslint/eslint-plugin": ["@typescript-eslint/eslint-plugin@8.58.0", "", { "dependencies": { "@eslint-community/regexpp": "^4.12.2", "@typescript-eslint/scope-manager": "8.58.0", "@typescript-eslint/type-utils": "8.58.0", "@typescript-eslint/utils": "8.58.0", "@typescript-eslint/visitor-keys": "8.58.0", "ignore": "^7.0.5", "natural-compare": "^1.4.0", "ts-api-utils": "^2.5.0" }, "peerDependencies": { "@typescript-eslint/parser": "^8.58.0", "eslint": "^8.57.0 || ^9.0.0 || ^10.0.0", "typescript": ">=4.8.4 <6.1.0" } }, "sha512-RLkVSiNuUP1C2ROIWfqX+YcUfLaSnxGE/8M+Y57lopVwg9VTYYfhuz15Yf1IzCKgZj6/rIbYTmJCUSqr76r0Wg=="], "@typescript-eslint/parser": ["@typescript-eslint/parser@8.58.0", "", { "dependencies": { "@typescript-eslint/scope-manager": "8.58.0", "@typescript-eslint/types": "8.58.0", "@typescript-eslint/typescript-estree": "8.58.0", "@typescript-eslint/visitor-keys": "8.58.0", "debug": "^4.4.3" }, "peerDependencies": { "eslint": "^8.57.0 || ^9.0.0 || ^10.0.0", "typescript": ">=4.8.4 <6.1.0" } }, "sha512-rLoGZIf9afaRBYsPUMtvkDWykwXwUPL60HebR4JgTI8mxfFe2cQTu3AGitANp4b9B2QlVru6WzjgB2IzJKiCSA=="], @@ -251,20 +230,6 @@ "@vitejs/plugin-react": ["@vitejs/plugin-react@6.0.1", "", { "dependencies": { "@rolldown/pluginutils": "1.0.0-rc.7" }, "peerDependencies": { "@rolldown/plugin-babel": "^0.1.7 || ^0.2.0", "babel-plugin-react-compiler": "^1.0.0", "vite": "^8.0.0" }, "optionalPeers": ["@rolldown/plugin-babel", "babel-plugin-react-compiler"] }, "sha512-l9X/E3cDb+xY3SWzlG1MOGt2usfEHGMNIaegaUGFsLkb3RCn/k8/TOXBcab+OndDI4TBtktT8/9BwwW8Vi9KUQ=="], - "@vitest/expect": ["@vitest/expect@4.1.2", "", { "dependencies": { "@standard-schema/spec": "^1.1.0", "@types/chai": "^5.2.2", "@vitest/spy": "4.1.2", "@vitest/utils": "4.1.2", "chai": "^6.2.2", "tinyrainbow": "^3.1.0" } }, "sha512-gbu+7B0YgUJ2nkdsRJrFFW6X7NTP44WlhiclHniUhxADQJH5Szt9mZ9hWnJPJ8YwOK5zUOSSlSvyzRf0u1DSBQ=="], - - "@vitest/mocker": ["@vitest/mocker@4.1.2", "", { "dependencies": { "@vitest/spy": "4.1.2", "estree-walker": "^3.0.3", "magic-string": "^0.30.21" }, "peerDependencies": { "msw": "^2.4.9", "vite": "^6.0.0 || ^7.0.0 || ^8.0.0" }, "optionalPeers": ["msw", "vite"] }, "sha512-Ize4iQtEALHDttPRCmN+FKqOl2vxTiNUhzobQFFt/BM1lRUTG7zRCLOykG/6Vo4E4hnUdfVLo5/eqKPukcWW7Q=="], - - "@vitest/pretty-format": ["@vitest/pretty-format@4.1.2", "", { "dependencies": { "tinyrainbow": "^3.1.0" } }, "sha512-dwQga8aejqeuB+TvXCMzSQemvV9hNEtDDpgUKDzOmNQayl2OG241PSWeJwKRH3CiC+sESrmoFd49rfnq7T4RnA=="], - - "@vitest/runner": ["@vitest/runner@4.1.2", "", { "dependencies": { "@vitest/utils": "4.1.2", "pathe": "^2.0.3" } }, "sha512-Gr+FQan34CdiYAwpGJmQG8PgkyFVmARK8/xSijia3eTFgVfpcpztWLuP6FttGNfPLJhaZVP/euvujeNYar36OQ=="], - - "@vitest/snapshot": ["@vitest/snapshot@4.1.2", "", { "dependencies": { "@vitest/pretty-format": "4.1.2", "@vitest/utils": "4.1.2", "magic-string": "^0.30.21", "pathe": "^2.0.3" } }, "sha512-g7yfUmxYS4mNxk31qbOYsSt2F4m1E02LFqO53Xpzg3zKMhLAPZAjjfyl9e6z7HrW6LvUdTwAQR3HHfLjpko16A=="], - - "@vitest/spy": ["@vitest/spy@4.1.2", "", {}, "sha512-DU4fBnbVCJGNBwVA6xSToNXrkZNSiw59H8tcuUspVMsBDBST4nfvsPsEHDHGtWRRnqBERBQu7TrTKskmjqTXKA=="], - - "@vitest/utils": ["@vitest/utils@4.1.2", "", { "dependencies": { "@vitest/pretty-format": "4.1.2", "convert-source-map": "^2.0.0", "tinyrainbow": "^3.1.0" } }, "sha512-xw2/TiX82lQHA06cgbqRKFb5lCAy3axQ4H4SoUFhUsg+wztiet+co86IAMDtF6Vm1hc7J6j09oh/rgDn+JdKIQ=="], - "acorn": ["acorn@8.16.0", "", { "bin": { "acorn": "bin/acorn" } }, "sha512-UVJyE9MttOsBQIDKw1skb9nAwQuR5wuGD3+82K6JgJlm/Y+KI92oNsMNGZCYdDsVtRHSak0pcV5Dno5+4jh9sw=="], "acorn-jsx": ["acorn-jsx@5.3.2", "", { "peerDependencies": { "acorn": "^6.0.0 || ^7.0.0 || ^8.0.0" } }, "sha512-rq9s+JNhf0IChjtDXxllJ7g41oZk5SlXtp0LHwyA5cejwn7vKmKp4pPri6YEePv2PU65sAsegbXtIinmDFDXgQ=="], @@ -279,24 +244,20 @@ "aria-query": ["aria-query@5.3.0", "", { "dependencies": { "dequal": "^2.0.3" } }, "sha512-b0P0sZPKtyu8HkeRAfCq0IfURZK+SuwMjY1UXGBU27wpAiTwQAIlq56IbIO+ytk/JjS1fMR14ee5WBBfKi5J6A=="], - "assertion-error": ["assertion-error@2.0.1", "", {}, "sha512-Izi8RQcffqCeNVgFigKli1ssklIbpHnCYc6AknXGYoB6grJqyeby7jv12JUQgmTAnIDnbck1uxksT4dzN3PWBA=="], - "balanced-match": ["balanced-match@1.0.2", "", {}, "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw=="], "baseline-browser-mapping": ["baseline-browser-mapping@2.10.12", "", { "bin": { "baseline-browser-mapping": "dist/cli.cjs" } }, "sha512-qyq26DxfY4awP2gIRXhhLWfwzwI+N5Nxk6iQi8EFizIaWIjqicQTE4sLnZZVdeKPRcVNoJOkkpfzoIYuvCKaIQ=="], - "bidi-js": ["bidi-js@1.0.3", "", { "dependencies": { "require-from-string": "^2.0.2" } }, "sha512-RKshQI1R3YQ+n9YJz2QQ147P66ELpa1FQEg20Dk8oW9t2KgLbpDLLp9aGZ7y8WHSshDknG0bknqGw5/tyCs5tw=="], - "brace-expansion": ["brace-expansion@1.1.13", "", { "dependencies": { "balanced-match": "^1.0.0", "concat-map": "0.0.1" } }, "sha512-9ZLprWS6EENmhEOpjCYW2c8VkmOvckIJZfkr7rBW6dObmfgJ/L1GpSYW5Hpo9lDz4D1+n0Ckz8rU7FwHDQiG/w=="], "browserslist": ["browserslist@4.28.1", "", { "dependencies": { "baseline-browser-mapping": "^2.9.0", "caniuse-lite": "^1.0.30001759", "electron-to-chromium": "^1.5.263", "node-releases": "^2.0.27", "update-browserslist-db": "^1.2.0" }, "bin": { "browserslist": "cli.js" } }, "sha512-ZC5Bd0LgJXgwGqUknZY/vkUQ04r8NXnJZ3yYi4vDmSiZmC/pdSN0NbNRPxZpbtO4uAfDUAFffO8IZoM3Gj8IkA=="], + "bun-types": ["bun-types@1.3.14", "", { "dependencies": { "@types/node": "*" } }, "sha512-4N0ig0fEomHt5R0KCFWjovxow98rIoRwKolrYdCcknNwMekCXRnWEUvgu5soYV8QXtVsrUD8B95MBOZGPvr6KQ=="], + "callsites": ["callsites@3.1.0", "", {}, "sha512-P8BjAsXvZS+VIDUI11hHCQEv74YT67YUi5JJFNWIqL235sBmjX4+qx9Muvls5ivyNENctx46xQLQ3aTuE7ssaQ=="], "caniuse-lite": ["caniuse-lite@1.0.30001782", "", {}, "sha512-dZcaJLJeDMh4rELYFw1tvSn1bhZWYFOt468FcbHHxx/Z/dFidd1I6ciyFdi3iwfQCyOjqo9upF6lGQYtMiJWxw=="], - "chai": ["chai@6.2.2", "", {}, "sha512-NUPRluOfOiTKBKvWPtSD4PhFvWCqOi0BGStNWs57X9js7XGTprSmFoz5F0tWhR4WPjNeR9jXqdC7/UpSJTnlRg=="], - "chalk": ["chalk@4.1.2", "", { "dependencies": { "ansi-styles": "^4.1.0", "supports-color": "^7.1.0" } }, "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA=="], "color-convert": ["color-convert@2.0.1", "", { "dependencies": { "color-name": "~1.1.4" } }, "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ=="], @@ -309,18 +270,12 @@ "cross-spawn": ["cross-spawn@7.0.6", "", { "dependencies": { "path-key": "^3.1.0", "shebang-command": "^2.0.0", "which": "^2.0.1" } }, "sha512-uV2QOWP2nWzsy2aMp8aRibhi9dlzF5Hgh5SHaB9OiTGEyDTiJJyx0uy51QXdyWbtAHNua4XJzUKca3OzKUd3vA=="], - "css-tree": ["css-tree@3.2.1", "", { "dependencies": { "mdn-data": "2.27.1", "source-map-js": "^1.2.1" } }, "sha512-X7sjQzceUhu1u7Y/ylrRZFU2FS6LRiFVp6rKLPg23y3x3c3DOKAwuXGDp+PAGjh6CSnCjYeAul8pcT8bAl+lSA=="], - "css.escape": ["css.escape@1.5.1", "", {}, "sha512-YUifsXXuknHlUsmlgyY0PKzgPOr7/FjCePfHNt0jxm83wHZi44VDMQ7/fGNkjY3/jV1MC+1CmZbaHzugyeRtpg=="], "csstype": ["csstype@3.2.3", "", {}, "sha512-z1HGKcYy2xA8AGQfwrn0PAy+PB7X/GSj3UVJW9qKyn43xWa+gl5nXmU4qqLMRzWVLFC8KusUX8T/0kCiOYpAIQ=="], - "data-urls": ["data-urls@7.0.0", "", { "dependencies": { "whatwg-mimetype": "^5.0.0", "whatwg-url": "^16.0.0" } }, "sha512-23XHcCF+coGYevirZceTVD7NdJOqVn+49IHyxgszm+JIiHLoB2TkmPtsYkNWT1pvRSGkc35L6NHs0yHkN2SumA=="], - "debug": ["debug@4.4.3", "", { "dependencies": { "ms": "^2.1.3" } }, "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA=="], - "decimal.js": ["decimal.js@10.6.0", "", {}, "sha512-YpgQiITW3JXGntzdUmyUR1V812Hn8T1YVXhCu+wO3OpS4eU9l4YdD3qjyiKdV6mvV29zapkMeD390UVEf2lkUg=="], - "deep-is": ["deep-is@0.1.4", "", {}, "sha512-oIPzksmTg4/MriiaYGO+okXDT7ztn/w3Eptv/+gSIdMdKsJo0u4CfYNFJPy+4SKMuCqGw2wxnA+URMg3t8a/bQ=="], "dequal": ["dequal@2.0.3", "", {}, "sha512-0je+qPKHEMohvfRTCEo3CrPG6cAzAYgmzKyxRiYSSDkS6eGJdyVJm7WaYA5ECaAD9wLB2T4EEeymA5aFVcYXCA=="], @@ -333,9 +288,7 @@ "enhanced-resolve": ["enhanced-resolve@5.20.1", "", { "dependencies": { "graceful-fs": "^4.2.4", "tapable": "^2.3.0" } }, "sha512-Qohcme7V1inbAfvjItgw0EaxVX5q2rdVEZHRBrEQdRZTssLDGsL8Lwrznl8oQ/6kuTJONLaDcGjkNP247XEhcA=="], - "entities": ["entities@6.0.1", "", {}, "sha512-aN97NXWF6AWBTahfVOIrB/NShkzi5H7F9r1s9mD3cDj4Ko5f2qhhVoYMibXF7GlLveb/D2ioWay8lxI97Ven3g=="], - - "es-module-lexer": ["es-module-lexer@2.0.0", "", {}, "sha512-5POEcUuZybH7IdmGsD8wlf0AI55wMecM9rVBTI/qEAy2c1kTOm3DjFYjrBdI2K3BaJjJYfYFeRtM0t9ssnRuxw=="], + "entities": ["entities@7.0.1", "", {}, "sha512-TWrgLOFUQTH994YUyl1yT4uyavY5nNB5muff+RtWaqNVCAK408b5ZnnbNAUEWLTCpum9w6arT70i1XdQ4UeOPA=="], "escalade": ["escalade@3.2.0", "", {}, "sha512-WUj2qlxaQtO4g6Pq5c29GTcWGDyd8itL8zTlipgECz3JesAiiOKotd8JU6otB3PACgG6xkJUyVhboMS+bje/jA=="], @@ -359,12 +312,8 @@ "estraverse": ["estraverse@5.3.0", "", {}, "sha512-MMdARuVEQziNTeJD8DgMqmhwR11BRQ/cBP+pLtYdSTnf3MIO8fFeiINEbX36ZdNlfU/7A9f3gUw49B3oQsvwBA=="], - "estree-walker": ["estree-walker@3.0.3", "", { "dependencies": { "@types/estree": "^1.0.0" } }, "sha512-7RUKfXgSMMkzt6ZuXmqapOurLGPPfgj6l9uRZ7lRGolvk0y2yocc35LdcxKC5PQZdn2DMqioAQ2NoWcrTKmm6g=="], - "esutils": ["esutils@2.0.3", "", {}, "sha512-kVscqXk4OCp68SZ0dkgEKVi6/8ij300KBWTJq32P/dYeWTSwK41WyTxalN1eRmA5Z9UU/LX9D7FWSmV9SAYx6g=="], - "expect-type": ["expect-type@1.3.0", "", {}, "sha512-knvyeauYhqjOYvQ66MznSMs83wmHrCycNEN6Ao+2AeYEfxUIkuiVxdEa1qlGEPK+We3n0THiDciYSsCcgW/DoA=="], - "fast-deep-equal": ["fast-deep-equal@3.1.3", "", {}, "sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q=="], "fast-json-stable-stringify": ["fast-json-stable-stringify@2.1.0", "", {}, "sha512-lhd/wF+Lk98HZoTCtlVraHtfh5XYijIjalXck7saUtuanSDyLMxnHhSXEDJqHxD7msR8D0uCmqlkwjCV8xvwHw=="], @@ -391,14 +340,14 @@ "graceful-fs": ["graceful-fs@4.2.11", "", {}, "sha512-RbJ5/jmFcNNCcDV5o9eTnBLJ/HszWV0P73bc+Ff4nS/rJj+YaS6IGyiOL0VoBYX+l1Wrl3k63h/KrH+nhJ0XvQ=="], + "happy-dom": ["happy-dom@20.9.0", "", { "dependencies": { "@types/node": ">=20.0.0", "@types/whatwg-mimetype": "^3.0.2", "@types/ws": "^8.18.1", "entities": "^7.0.1", "whatwg-mimetype": "^3.0.0", "ws": "^8.18.3" } }, "sha512-GZZ9mKe8r646NUAf/zemnGbjYh4Bt8/MqASJY+pSm5ZDtc3YQox+4gsLI7yi1hba6o+eCsGxpHn5+iEVn31/FQ=="], + "has-flag": ["has-flag@4.0.0", "", {}, "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ=="], "hermes-estree": ["hermes-estree@0.25.1", "", {}, "sha512-0wUoCcLp+5Ev5pDW2OriHC2MJCbwLwuRx+gAqMTOkGKJJiBCLjtrvy4PWUGn6MIVefecRpzoOZ/UV6iGdOr+Cw=="], "hermes-parser": ["hermes-parser@0.25.1", "", { "dependencies": { "hermes-estree": "0.25.1" } }, "sha512-6pEjquH3rqaI6cYAXYPcz9MS4rY6R4ngRgrgfDshRptUZIc3lw0MCIJIGDj9++mfySOuPTHB4nrSW99BCvOPIA=="], - "html-encoding-sniffer": ["html-encoding-sniffer@6.0.0", "", { "dependencies": { "@exodus/bytes": "^1.6.0" } }, "sha512-CV9TW3Y3f8/wT0BRFc1/KAVQ3TUHiXmaAb6VW9vtiMFf7SLoMd1PdAc4W3KFOFETBJUb90KatHqlsZMWV+R9Gg=="], - "ignore": ["ignore@5.3.2", "", {}, "sha512-hsBTNUqQTDwkWtcdYI2i06Y/nUBEsNEDJKjWdigLvegy8kDuJAS8uRlpkkcQpyEXL0Z/pjDy5HBmMjRCJ2gq+g=="], "import-fresh": ["import-fresh@3.3.1", "", { "dependencies": { "parent-module": "^1.0.0", "resolve-from": "^4.0.0" } }, "sha512-TR3KfrTZTYLPB6jUjfx6MF9WcWrHL9su5TObK4ZkYgBdWKPOFoSoQIdEuTuR82pmtxH2spWG9h6etwfr1pLBqQ=="], @@ -411,8 +360,6 @@ "is-glob": ["is-glob@4.0.3", "", { "dependencies": { "is-extglob": "^2.1.1" } }, "sha512-xelSayHH36ZgE7ZWhli7pW34hNbNl8Ojv5KVmkJD4hBdD3th8Tfk9vYasLM+mXWOZhFkgZfxhLSnrwRr4elSSg=="], - "is-potential-custom-element-name": ["is-potential-custom-element-name@1.0.1", "", {}, "sha512-bCYeRA2rVibKZd+s2625gGnGF/t7DSqDs4dP7CrLA1m7jKWz6pps0LpYLJN8Q64HtmPKJ1hrN3nzPNKFEKOUiQ=="], - "isexe": ["isexe@2.0.0", "", {}, "sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw=="], "jiti": ["jiti@2.6.1", "", { "bin": { "jiti": "lib/jiti-cli.mjs" } }, "sha512-ekilCSN1jwRvIbgeg/57YFh8qQDNbwDb9xT/qu2DAHbFFZUicIl4ygVaAvzveMhMVr3LnpSKTNnwt8PoOfmKhQ=="], @@ -421,8 +368,6 @@ "js-yaml": ["js-yaml@4.1.1", "", { "dependencies": { "argparse": "^2.0.1" }, "bin": { "js-yaml": "bin/js-yaml.js" } }, "sha512-qQKT4zQxXl8lLwBtHMWwaTcGfFOZviOJet3Oy/xmGk2gZH677CJM9EvtfdSkgWcATZhj/55JZ0rmy3myCT5lsA=="], - "jsdom": ["jsdom@29.0.1", "", { "dependencies": { "@asamuzakjp/css-color": "^5.0.1", "@asamuzakjp/dom-selector": "^7.0.3", "@bramus/specificity": "^2.4.2", "@csstools/css-syntax-patches-for-csstree": "^1.1.1", "@exodus/bytes": "^1.15.0", "css-tree": "^3.2.1", "data-urls": "^7.0.0", "decimal.js": "^10.6.0", "html-encoding-sniffer": "^6.0.0", "is-potential-custom-element-name": "^1.0.1", "lru-cache": "^11.2.7", "parse5": "^8.0.0", "saxes": "^6.0.0", "symbol-tree": "^3.2.4", "tough-cookie": "^6.0.1", "undici": "^7.24.5", "w3c-xmlserializer": "^5.0.0", "webidl-conversions": "^8.0.1", "whatwg-mimetype": "^5.0.0", "whatwg-url": "^16.0.1", "xml-name-validator": "^5.0.0" }, "peerDependencies": { "canvas": "^3.0.0" }, "optionalPeers": ["canvas"] }, "sha512-z6JOK5gRO7aMybVq/y/MlIpKh8JIi68FBKMUtKkK2KH/wMSRlCxQ682d08LB9fYXplyY/UXG8P4XXTScmdjApg=="], - "jsesc": ["jsesc@3.1.0", "", { "bin": { "jsesc": "bin/jsesc" } }, "sha512-/sM3dO2FOzXjKQhJuo0Q173wf2KOo8t4I8vHy6lF9poUp7bKT0/NHE8fPX23PwfhnykfqnC2xRxOnVw5XuGIaA=="], "json-buffer": ["json-buffer@3.0.1", "", {}, "sha512-4bV5BfR2mqfQTJm+V5tPPdf+ZpuhiIvTuAB5g8kcrXOZpTT/QwwVRWBywX1ozr6lEuPdbHxwaJlm9G6mI2sfSQ=="], @@ -467,7 +412,7 @@ "lodash.merge": ["lodash.merge@4.6.2", "", {}, "sha512-0KpjqXRVvrYyCsX1swR/XTK0va6VQkQM6MNo7PqW77ByjAhoARA8EfrP1N4+KlKj8YS0ZUCtRT/YUuhyYDujIQ=="], - "lru-cache": ["lru-cache@11.2.7", "", {}, "sha512-aY/R+aEsRelme17KGQa/1ZSIpLpNYYrhcrepKTZgE+W3WM16YMCaPwOHLHsmopZHELU0Ojin1lPVxKR0MihncA=="], + "lru-cache": ["lru-cache@5.1.1", "", { "dependencies": { "yallist": "^3.0.2" } }, "sha512-KpNARQA3Iwv+jTA0utUVVbrh+Jlrr1Fv0e56GGzAFOXN7dk/FviaDW8LHmK52DlcH4WP2n6gI8vN1aesBFgo9w=="], "lucide-react": ["lucide-react@1.7.0", "", { "peerDependencies": { "react": "^16.5.1 || ^17.0.0 || ^18.0.0 || ^19.0.0" } }, "sha512-yI7BeItCLZJTXikmK4KNUGCKoGzSvbKlfCvw44bU4fXAL6v3gYS4uHD1jzsLkfwODYwI6Drw5Tu9Z5ulDe0TSg=="], @@ -475,8 +420,6 @@ "magic-string": ["magic-string@0.30.21", "", { "dependencies": { "@jridgewell/sourcemap-codec": "^1.5.5" } }, "sha512-vd2F4YUyEXKGcLHoq+TEyCjxueSeHnFxyyjNp80yg0XV4vUhnDer/lvvlqM/arB5bXQN5K2/3oinyCRyx8T2CQ=="], - "mdn-data": ["mdn-data@2.27.1", "", {}, "sha512-9Yubnt3e8A0OKwxYSXyhLymGW4sCufcLG6VdiDdUGVkPhpqLxlvP5vl1983gQjJl3tqbrM731mjaZaP68AgosQ=="], - "min-indent": ["min-indent@1.0.1", "", {}, "sha512-I9jwMn07Sy/IwOj3zVkVik2JTvgpaykDZEigL6Rx6N9LbMywwUSMtxET+7lVoDLLd3O3IXwJwvuuns8UB/HeAg=="], "minimatch": ["minimatch@3.1.5", "", { "dependencies": { "brace-expansion": "^1.1.7" } }, "sha512-VgjWUsnnT6n+NUk6eZq77zeFdpW2LWDzP6zFGrCbHXiYNul5Dzqk2HHQ5uFH2DNW5Xbp8+jVzaeNt94ssEEl4w=="], @@ -489,8 +432,6 @@ "node-releases": ["node-releases@2.0.36", "", {}, "sha512-TdC8FSgHz8Mwtw9g5L4gR/Sh9XhSP/0DEkQxfEFXOpiul5IiHgHan2VhYYb6agDSfp4KuvltmGApc8HMgUrIkA=="], - "obug": ["obug@2.1.1", "", {}, "sha512-uTqF9MuPraAQ+IsnPf366RG4cP9RtUi7MLO1N3KEc+wb0a6yKpeL0lmk2IB1jY5KHPAlTc6T/JRdC/YqxHNwkQ=="], - "optionator": ["optionator@0.9.4", "", { "dependencies": { "deep-is": "^0.1.3", "fast-levenshtein": "^2.0.6", "levn": "^0.4.1", "prelude-ls": "^1.2.1", "type-check": "^0.4.0", "word-wrap": "^1.2.5" } }, "sha512-6IpQ7mKUxRcZNLIObR0hz7lxsapSSIYNZJwXPGeF0mTVqGKFIXj1DQcMoT22S3ROcLyY/rz0PWaWZ9ayWmad9g=="], "p-limit": ["p-limit@3.1.0", "", { "dependencies": { "yocto-queue": "^0.1.0" } }, "sha512-TYOanM3wGwNGsZN2cVTYPArw454xnXj5qmWF1bEoAc4+cU/ol7GVh7odevjp1FNHduHc3KZMcFduxU5Xc6uJRQ=="], @@ -499,14 +440,10 @@ "parent-module": ["parent-module@1.0.1", "", { "dependencies": { "callsites": "^3.0.0" } }, "sha512-GQ2EWRpQV8/o+Aw8YqtfZZPfNRWZYkbidE9k5rpl/hC3vtHHBfGm2Ifi6qWV+coDGkrUKZAxE3Lot5kcsRlh+g=="], - "parse5": ["parse5@8.0.0", "", { "dependencies": { "entities": "^6.0.0" } }, "sha512-9m4m5GSgXjL4AjumKzq1Fgfp3Z8rsvjRNbnkVwfu2ImRqE5D0LnY2QfDen18FSY9C573YU5XxSapdHZTZ2WolA=="], - "path-exists": ["path-exists@4.0.0", "", {}, "sha512-ak9Qy5Q7jYb2Wwcey5Fpvg2KoAc/ZIhLSLOSBmRmygPsGwkVVt0fZa0qrtMz+m6tJTAHfZQ8FnmB4MG4LWy7/w=="], "path-key": ["path-key@3.1.1", "", {}, "sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q=="], - "pathe": ["pathe@2.0.3", "", {}, "sha512-WUjGcAqP1gQacoQe+OBJsFA7Ld4DyXuUIjZ5cc75cLHvJ7dtNsTugphxIADwspS+AraAUePCKrSVtPLFj/F88w=="], - "picocolors": ["picocolors@1.1.1", "", {}, "sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA=="], "picomatch": ["picomatch@4.0.4", "", {}, "sha512-QP88BAKvMam/3NxH6vj2o21R6MjxZUAd6nlwAS/pnGvN9IVLocLHxGYIzFhg6fUQ+5th6P4dv4eW9jX3DSIj7A=="], @@ -527,14 +464,10 @@ "redent": ["redent@3.0.0", "", { "dependencies": { "indent-string": "^4.0.0", "strip-indent": "^3.0.0" } }, "sha512-6tDA8g98We0zd0GvVeMT9arEOnTw9qM03L9cJXaCjrip1OO764RDBLBfrB4cwzNGDj5OA5ioymC9GkizgWJDUg=="], - "require-from-string": ["require-from-string@2.0.2", "", {}, "sha512-Xf0nWe6RseziFMu+Ap9biiUbmplq6S9/p+7w7YXP/JBHhrUDDUhwa+vANyubuqfZWTveU//DYVGsDG7RKL/vEw=="], - "resolve-from": ["resolve-from@4.0.0", "", {}, "sha512-pb/MYmXstAkysRFx8piNI1tGFNQIFA3vkE3Gq4EuA1dF6gHp/+vgZqsCGJapvy8N3Q+4o7FwvquPJcnZ7RYy4g=="], "rolldown": ["rolldown@1.0.0-rc.12", "", { "dependencies": { "@oxc-project/types": "=0.122.0", "@rolldown/pluginutils": "1.0.0-rc.12" }, "optionalDependencies": { "@rolldown/binding-android-arm64": "1.0.0-rc.12", "@rolldown/binding-darwin-arm64": "1.0.0-rc.12", "@rolldown/binding-darwin-x64": "1.0.0-rc.12", "@rolldown/binding-freebsd-x64": "1.0.0-rc.12", "@rolldown/binding-linux-arm-gnueabihf": "1.0.0-rc.12", "@rolldown/binding-linux-arm64-gnu": "1.0.0-rc.12", "@rolldown/binding-linux-arm64-musl": "1.0.0-rc.12", "@rolldown/binding-linux-ppc64-gnu": "1.0.0-rc.12", "@rolldown/binding-linux-s390x-gnu": "1.0.0-rc.12", "@rolldown/binding-linux-x64-gnu": "1.0.0-rc.12", "@rolldown/binding-linux-x64-musl": "1.0.0-rc.12", "@rolldown/binding-openharmony-arm64": "1.0.0-rc.12", "@rolldown/binding-wasm32-wasi": "1.0.0-rc.12", "@rolldown/binding-win32-arm64-msvc": "1.0.0-rc.12", "@rolldown/binding-win32-x64-msvc": "1.0.0-rc.12" }, "bin": { "rolldown": "bin/cli.mjs" } }, "sha512-yP4USLIMYrwpPHEFB5JGH1uxhcslv6/hL0OyvTuY+3qlOSJvZ7ntYnoWpehBxufkgN0cvXxppuTu5hHa/zPh+A=="], - "saxes": ["saxes@6.0.0", "", { "dependencies": { "xmlchars": "^2.2.0" } }, "sha512-xAg7SOnEhrm5zI3puOOKyy1OMcMlIJZYNJY7xLBwSze0UjhPLnWfj2GF2EpT0jmzaJKIWKHLsaSSajf35bcYnA=="], - "scheduler": ["scheduler@0.27.0", "", {}, "sha512-eNv+WrVbKu1f3vbYJT/xtiF5syA5HPIMtf9IgY/nKg0sWqzAUEvqY/xm7OcZc/qafLx/iO9FgOmeSAp4v5ti/Q=="], "semver": ["semver@6.3.1", "", { "bin": { "semver": "bin/semver.js" } }, "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA=="], @@ -543,42 +476,20 @@ "shebang-regex": ["shebang-regex@3.0.0", "", {}, "sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A=="], - "siginfo": ["siginfo@2.0.0", "", {}, "sha512-ybx0WO1/8bSBLEWXZvEd7gMW3Sn3JFlW3TvX1nREbDLRNQNaeNN8WK0meBwPdAaOI7TtRRRJn/Es1zhrrCHu7g=="], - "source-map-js": ["source-map-js@1.2.1", "", {}, "sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA=="], - "stackback": ["stackback@0.0.2", "", {}, "sha512-1XMJE5fQo1jGH6Y/7ebnwPOBEkIEnT4QF32d5R1+VXdXveM0IBMJt8zfaxX1P3QhVwrYe+576+jkANtSS2mBbw=="], - - "std-env": ["std-env@4.0.0", "", {}, "sha512-zUMPtQ/HBY3/50VbpkupYHbRroTRZJPRLvreamgErJVys0ceuzMkD44J/QjqhHjOzK42GQ3QZIeFG1OYfOtKqQ=="], - "strip-indent": ["strip-indent@3.0.0", "", { "dependencies": { "min-indent": "^1.0.0" } }, "sha512-laJTa3Jb+VQpaC6DseHhF7dXVqHTfJPCRDaEbid/drOhgitgYku/letMUqOXFoWV0zIIUbjpdH2t+tYj4bQMRQ=="], "strip-json-comments": ["strip-json-comments@3.1.1", "", {}, "sha512-6fPc+R4ihwqP6N/aIv2f1gMH8lOVtWQHoqC4yK6oSDVVocumAsfCqjkXnqiYMhmMwS/mEHLp7Vehlt3ql6lEig=="], "supports-color": ["supports-color@7.2.0", "", { "dependencies": { "has-flag": "^4.0.0" } }, "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw=="], - "symbol-tree": ["symbol-tree@3.2.4", "", {}, "sha512-9QNk5KwDF+Bvz+PyObkmSYjI5ksVUYtjW7AU22r2NKcfLJcXp96hkDWU3+XndOsUb+AQ9QhfzfCT2O+CNWT5Tw=="], - "tailwindcss": ["tailwindcss@4.2.2", "", {}, "sha512-KWBIxs1Xb6NoLdMVqhbhgwZf2PGBpPEiwOqgI4pFIYbNTfBXiKYyWoTsXgBQ9WFg/OlhnvHaY+AEpW7wSmFo2Q=="], "tapable": ["tapable@2.3.2", "", {}, "sha512-1MOpMXuhGzGL5TTCZFItxCc0AARf1EZFQkGqMm7ERKj8+Hgr5oLvJOVFcC+lRmR8hCe2S3jC4T5D7Vg/d7/fhA=="], - "tinybench": ["tinybench@2.9.0", "", {}, "sha512-0+DUvqWMValLmha6lr4kD8iAMK1HzV0/aKnCtWb9v9641TnP/MFb7Pc2bxoxQjTXAErryXVgUOfv2YqNllqGeg=="], - - "tinyexec": ["tinyexec@1.0.4", "", {}, "sha512-u9r3uZC0bdpGOXtlxUIdwf9pkmvhqJdrVCH9fapQtgy/OeTTMZ1nqH7agtvEfmGui6e1XxjcdrlxvxJvc3sMqw=="], - "tinyglobby": ["tinyglobby@0.2.15", "", { "dependencies": { "fdir": "^6.5.0", "picomatch": "^4.0.3" } }, "sha512-j2Zq4NyQYG5XMST4cbs02Ak8iJUdxRM0XI5QyxXuZOzKOINmWurp3smXu3y5wDcJrptwpSjgXHzIQxR0omXljQ=="], - "tinyrainbow": ["tinyrainbow@3.1.0", "", {}, "sha512-Bf+ILmBgretUrdJxzXM0SgXLZ3XfiaUuOj/IKQHuTXip+05Xn+uyEYdVg0kYDipTBcLrCVyUzAPz7QmArb0mmw=="], - - "tldts": ["tldts@7.0.27", "", { "dependencies": { "tldts-core": "^7.0.27" }, "bin": { "tldts": "bin/cli.js" } }, "sha512-I4FZcVFcqCRuT0ph6dCDpPuO4Xgzvh+spkcTr1gK7peIvxWauoloVO0vuy1FQnijT63ss6AsHB6+OIM4aXHbPg=="], - - "tldts-core": ["tldts-core@7.0.27", "", {}, "sha512-YQ7uPjgWUibIK6DW5lrKujGwUKhLevU4hcGbP5O6TcIUb+oTjJYJVWPS4nZsIHrEEEG6myk/oqAJUEQmpZrHsg=="], - - "tough-cookie": ["tough-cookie@6.0.1", "", { "dependencies": { "tldts": "^7.0.5" } }, "sha512-LktZQb3IeoUWB9lqR5EWTHgW/VTITCXg4D21M+lvybRVdylLrRMnqaIONLVb5mav8vM19m44HIcGq4qASeu2Qw=="], - - "tr46": ["tr46@6.0.0", "", { "dependencies": { "punycode": "^2.3.1" } }, "sha512-bLVMLPtstlZ4iMQHpFHTR7GAGj2jxi8Dg0s2h2MafAE4uSWF98FC/3MomU51iQAMf8/qDUbKWf5GxuvvVcXEhw=="], - "ts-api-utils": ["ts-api-utils@2.5.0", "", { "peerDependencies": { "typescript": ">=4.8.4" } }, "sha512-OJ/ibxhPlqrMM0UiNHJ/0CKQkoKF243/AEmplt3qpRgkW8VG7IfOS41h7V8TjITqdByHzrjcS/2si+y4lIh8NA=="], "tslib": ["tslib@2.8.1", "", {}, "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w=="], @@ -589,8 +500,6 @@ "typescript-eslint": ["typescript-eslint@8.58.0", "", { "dependencies": { "@typescript-eslint/eslint-plugin": "8.58.0", "@typescript-eslint/parser": "8.58.0", "@typescript-eslint/typescript-estree": "8.58.0", "@typescript-eslint/utils": "8.58.0" }, "peerDependencies": { "eslint": "^8.57.0 || ^9.0.0 || ^10.0.0", "typescript": ">=4.8.4 <6.1.0" } }, "sha512-e2TQzKfaI85fO+F3QywtX+tCTsu/D3WW5LVU6nz8hTFKFZ8yBJ6mSYRpXqdR3mFjPWmO0eWsTa5f+UpAOe/FMA=="], - "undici": ["undici@7.24.6", "", {}, "sha512-Xi4agocCbRzt0yYMZGMA6ApD7gvtUFaxm4ZmeacWI4cZxaF6C+8I8QfofC20NAePiB/IcvZmzkJ7XPa471AEtA=="], - "undici-types": ["undici-types@7.16.0", "", {}, "sha512-Zz+aZWSj8LE6zoxD+xrjh4VfkIG8Ya6LvYkZqtUQGJPZjYl53ypCaUwWqo7eI0x66KBGeRo+mlBEkMSeSZ38Nw=="], "update-browserslist-db": ["update-browserslist-db@1.2.3", "", { "dependencies": { "escalade": "^3.2.0", "picocolors": "^1.1.1" }, "peerDependencies": { "browserslist": ">= 4.21.0" }, "bin": { "update-browserslist-db": "cli.js" } }, "sha512-Js0m9cx+qOgDxo0eMiFGEueWztz+d4+M3rGlmKPT+T4IS/jP4ylw3Nwpu6cpTTP8R1MAC1kF4VbdLt3ARf209w=="], @@ -601,25 +510,13 @@ "vite": ["vite@8.0.3", "", { "dependencies": { "lightningcss": "^1.32.0", "picomatch": "^4.0.4", "postcss": "^8.5.8", "rolldown": "1.0.0-rc.12", "tinyglobby": "^0.2.15" }, "optionalDependencies": { "fsevents": "~2.3.3" }, "peerDependencies": { "@types/node": "^20.19.0 || >=22.12.0", "@vitejs/devtools": "^0.1.0", "esbuild": "^0.27.0", "jiti": ">=1.21.0", "less": "^4.0.0", "sass": "^1.70.0", "sass-embedded": "^1.70.0", "stylus": ">=0.54.8", "sugarss": "^5.0.0", "terser": "^5.16.0", "tsx": "^4.8.1", "yaml": "^2.4.2" }, "optionalPeers": ["@types/node", "@vitejs/devtools", "esbuild", "jiti", "less", "sass", "sass-embedded", "stylus", "sugarss", "terser", "tsx", "yaml"], "bin": { "vite": "bin/vite.js" } }, "sha512-B9ifbFudT1TFhfltfaIPgjo9Z3mDynBTJSUYxTjOQruf/zHH+ezCQKcoqO+h7a9Pw9Nm/OtlXAiGT1axBgwqrQ=="], - "vitest": ["vitest@4.1.2", "", { "dependencies": { "@vitest/expect": "4.1.2", "@vitest/mocker": "4.1.2", "@vitest/pretty-format": "4.1.2", "@vitest/runner": "4.1.2", "@vitest/snapshot": "4.1.2", "@vitest/spy": "4.1.2", "@vitest/utils": "4.1.2", "es-module-lexer": "^2.0.0", "expect-type": "^1.3.0", "magic-string": "^0.30.21", "obug": "^2.1.1", "pathe": "^2.0.3", "picomatch": "^4.0.3", "std-env": "^4.0.0-rc.1", "tinybench": "^2.9.0", "tinyexec": "^1.0.2", "tinyglobby": "^0.2.15", "tinyrainbow": "^3.1.0", "vite": "^6.0.0 || ^7.0.0 || ^8.0.0", "why-is-node-running": "^2.3.0" }, "peerDependencies": { "@edge-runtime/vm": "*", "@opentelemetry/api": "^1.9.0", "@types/node": "^20.0.0 || ^22.0.0 || >=24.0.0", "@vitest/browser-playwright": "4.1.2", "@vitest/browser-preview": "4.1.2", "@vitest/browser-webdriverio": "4.1.2", "@vitest/ui": "4.1.2", "happy-dom": "*", "jsdom": "*" }, "optionalPeers": ["@edge-runtime/vm", "@opentelemetry/api", "@types/node", "@vitest/browser-playwright", "@vitest/browser-preview", "@vitest/browser-webdriverio", "@vitest/ui", "happy-dom", "jsdom"], "bin": { "vitest": "vitest.mjs" } }, "sha512-xjR1dMTVHlFLh98JE3i/f/WePqJsah4A0FK9cc8Ehp9Udk0AZk6ccpIZhh1qJ/yxVWRZ+Q54ocnD8TXmkhspGg=="], - - "w3c-xmlserializer": ["w3c-xmlserializer@5.0.0", "", { "dependencies": { "xml-name-validator": "^5.0.0" } }, "sha512-o8qghlI8NZHU1lLPrpi2+Uq7abh4GGPpYANlalzWxyWteJOCsr/P+oPBA49TOLu5FTZO4d3F9MnWJfiMo4BkmA=="], - - "webidl-conversions": ["webidl-conversions@8.0.1", "", {}, "sha512-BMhLD/Sw+GbJC21C/UgyaZX41nPt8bUTg+jWyDeg7e7YN4xOM05YPSIXceACnXVtqyEw/LMClUQMtMZ+PGGpqQ=="], - - "whatwg-mimetype": ["whatwg-mimetype@5.0.0", "", {}, "sha512-sXcNcHOC51uPGF0P/D4NVtrkjSU2fNsm9iog4ZvZJsL3rjoDAzXZhkm2MWt1y+PUdggKAYVoMAIYcs78wJ51Cw=="], - - "whatwg-url": ["whatwg-url@16.0.1", "", { "dependencies": { "@exodus/bytes": "^1.11.0", "tr46": "^6.0.0", "webidl-conversions": "^8.0.1" } }, "sha512-1to4zXBxmXHV3IiSSEInrreIlu02vUOvrhxJJH5vcxYTBDAx51cqZiKdyTxlecdKNSjj8EcxGBxNf6Vg+945gw=="], + "whatwg-mimetype": ["whatwg-mimetype@3.0.0", "", {}, "sha512-nt+N2dzIutVRxARx1nghPKGv1xHikU7HKdfafKkLNLindmPU/ch3U31NOCGGA/dmPcmb1VlofO0vnKAcsm0o/Q=="], "which": ["which@2.0.2", "", { "dependencies": { "isexe": "^2.0.0" }, "bin": { "node-which": "./bin/node-which" } }, "sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA=="], - "why-is-node-running": ["why-is-node-running@2.3.0", "", { "dependencies": { "siginfo": "^2.0.0", "stackback": "0.0.2" }, "bin": { "why-is-node-running": "cli.js" } }, "sha512-hUrmaWBdVDcxvYqnyh09zunKzROWjbZTiNy8dBEjkS7ehEDQibXJ7XvlmtbwuTclUiIyN+CyXQD4Vmko8fNm8w=="], - "word-wrap": ["word-wrap@1.2.5", "", {}, "sha512-BN22B5eaMMI9UMtjrGd5g5eCYPpCPDUy0FJXbYsaT5zYxjFOckS53SQDE3pWkVoWpHXVb3BrYcEN4Twa55B5cA=="], - "xml-name-validator": ["xml-name-validator@5.0.0", "", {}, "sha512-EvGK8EJ3DhaHfbRlETOWAS5pO9MZITeauHKJyb8wyajUfQUenkIg2MvLDTZ4T/TgIcm3HU0TFBgWWboAZ30UHg=="], - - "xmlchars": ["xmlchars@2.2.0", "", {}, "sha512-JZnDKK8B0RCDw84FNdDAIpZK+JuJw+s7Lz8nksI7SIuU3UXJJslUthsi+uWBUYOwPFwW7W7PRLRfUKpxjtjFCw=="], + "ws": ["ws@8.21.0", "", { "peerDependencies": { "bufferutil": "^4.0.1", "utf-8-validate": ">=5.0.2" }, "optionalPeers": ["bufferutil", "utf-8-validate"] }, "sha512-Vsp28b7DRcimFQvrqu2Wek3z1iYxDCWqHYB8Qsnk/S4RfaCQzPGPyBNuVjJV3cd6UiKtUtp6sNM77gWvzcCH+g=="], "yallist": ["yallist@3.1.1", "", {}, "sha512-a4UGQaWPH59mOXUYnAG2ewncQS4i4F43Tv3JoAM+s2VDAmS9NsK8GpDMLrCHPksFT7h3K6TOoUNn2pb7RoXx4g=="], @@ -629,8 +526,6 @@ "zod-validation-error": ["zod-validation-error@4.0.2", "", { "peerDependencies": { "zod": "^3.25.0 || ^4.0.0" } }, "sha512-Q6/nZLe6jxuU80qb/4uJ4t5v2VEZ44lzQjPDhYJNztRQ4wyWc6VF3D3Kb/fAuPetZQnhS3hnajCf9CsWesghLQ=="], - "@babel/helper-compilation-targets/lru-cache": ["lru-cache@5.1.1", "", { "dependencies": { "yallist": "^3.0.2" } }, "sha512-KpNARQA3Iwv+jTA0utUVVbrh+Jlrr1Fv0e56GGzAFOXN7dk/FviaDW8LHmK52DlcH4WP2n6gI8vN1aesBFgo9w=="], - "@eslint-community/eslint-utils/eslint-visitor-keys": ["eslint-visitor-keys@3.4.3", "", {}, "sha512-wpc+LXeiyiisxPlEkUzU6svyS1frIO3Mgxj1fdy7Pm8Ygzguax2N3Fa/D/ag1WqbOprdI+uY6wMUl8/a2G+iag=="], "@eslint/eslintrc/globals": ["globals@14.0.0", "", {}, "sha512-oahGvuMGQlPw/ivIYBjVSrWAfWLBeku5tpPE2fOPLi+WHffIWbuh2tCjhyQhTBPMf5E9jDEH4FOmTYgYwbKwtQ=="], diff --git a/frontend/bunfig.toml b/frontend/bunfig.toml new file mode 100644 index 0000000..3fc87eb --- /dev/null +++ b/frontend/bunfig.toml @@ -0,0 +1,2 @@ +[test] +preload = ["./src/test/setup.ts"] diff --git a/frontend/package.json b/frontend/package.json index 4ac8766..64d1bbe 100644 --- a/frontend/package.json +++ b/frontend/package.json @@ -4,13 +4,13 @@ "version": "0.0.0", "type": "module", "scripts": { - "dev": "vite", - "build": "tsc -b && vite build", - "lint": "eslint .", - "typecheck": "tsc -b --noEmit", - "preview": "vite preview", - "test": "vitest run", - "test:watch": "vitest" + "dev": "bun --bun vite", + "build": "bun --bun tsc -b && bun --bun vite build", + "lint": "bun --bun eslint .", + "typecheck": "bun --bun tsc -b --noEmit", + "preview": "bun --bun vite preview", + "test": "bun test", + "test:watch": "bun test --watch" }, "dependencies": { "lucide-react": "^1.7.0", @@ -20,12 +20,13 @@ }, "devDependencies": { "@eslint/js": "^9.39.4", + "@happy-dom/global-registrator": "^20.0.0", "@tailwindcss/vite": "^4.2.2", "@testing-library/dom": "^10.4.1", "@testing-library/jest-dom": "^6.9.1", "@testing-library/react": "^16.3.2", "@testing-library/user-event": "^14.6.1", - "@types/node": "^24.12.0", + "@types/bun": "^1.3.0", "@types/react": "^19.2.14", "@types/react-dom": "^19.2.3", "@vitejs/plugin-react": "^6.0.1", @@ -33,11 +34,9 @@ "eslint-plugin-react-hooks": "^7.0.1", "eslint-plugin-react-refresh": "^0.5.2", "globals": "^17.4.0", - "jsdom": "^29.0.1", "tailwindcss": "^4.2.2", "typescript": "~5.9.3", "typescript-eslint": "^8.57.0", - "vite": "^8.0.1", - "vitest": "^4.1.2" + "vite": "^8.0.1" } } diff --git a/frontend/src/api/open5e.test.ts b/frontend/src/api/open5e.test.ts index b7b5aaf..5ac4e74 100644 --- a/frontend/src/api/open5e.test.ts +++ b/frontend/src/api/open5e.test.ts @@ -1,4 +1,5 @@ -import { abilityModifier } from './open5e' +import { afterEach, beforeEach, describe, expect, it, jest } from 'bun:test' +import { abilityModifier, clearMonsterCache, getCachedMonster, getMonster, searchMonsters } from './open5e' import type { MonsterData } from '../types/statBlock' describe('abilityModifier', () => { @@ -72,22 +73,19 @@ function makeMonster(overrides: Partial = {}): MonsterData { } } +const originalFetch = globalThis.fetch + describe('searchMonsters', () => { - let searchMonsters: typeof import('./open5e').searchMonsters - let getCachedMonster: typeof import('./open5e').getCachedMonster - const mockFetch = vi.fn() + const mockFetch = jest.fn() - beforeEach(async () => { - vi.resetModules() - vi.stubGlobal('fetch', mockFetch) + beforeEach(() => { + clearMonsterCache() + globalThis.fetch = mockFetch as unknown as typeof fetch mockFetch.mockReset() - const mod = await import('./open5e') - searchMonsters = mod.searchMonsters - getCachedMonster = mod.getCachedMonster }) afterEach(() => { - vi.unstubAllGlobals() + globalThis.fetch = originalFetch }) it('returns empty array for query shorter than 2 chars', async () => { @@ -111,7 +109,7 @@ describe('searchMonsters', () => { const result = await searchMonsters('goblin') - expect(mockFetch).toHaveBeenCalledOnce() + expect(mockFetch).toHaveBeenCalledTimes(1) expect(mockFetch.mock.calls[0][0]).toContain('/monsters/') expect(mockFetch.mock.calls[0][0]).toContain('search=goblin') expect(result).toEqual([ @@ -147,21 +145,16 @@ describe('searchMonsters', () => { }) describe('getMonster', () => { - let getMonster: typeof import('./open5e').getMonster - let getCachedMonster: typeof import('./open5e').getCachedMonster - const mockFetch = vi.fn() + const mockFetch = jest.fn() - beforeEach(async () => { - vi.resetModules() - vi.stubGlobal('fetch', mockFetch) + beforeEach(() => { + clearMonsterCache() + globalThis.fetch = mockFetch as unknown as typeof fetch mockFetch.mockReset() - const mod = await import('./open5e') - getMonster = mod.getMonster - getCachedMonster = mod.getCachedMonster }) afterEach(() => { - vi.unstubAllGlobals() + globalThis.fetch = originalFetch }) it('fetches monster by slug and returns data', async () => { @@ -173,7 +166,7 @@ describe('getMonster', () => { const result = await getMonster('goblin') - expect(mockFetch).toHaveBeenCalledOnce() + expect(mockFetch).toHaveBeenCalledTimes(1) expect(mockFetch.mock.calls[0][0]).toContain('/monsters/goblin/') expect(result).toEqual(goblin) }) @@ -188,7 +181,7 @@ describe('getMonster', () => { await getMonster('goblin') const result = await getMonster('goblin') - expect(mockFetch).toHaveBeenCalledOnce() + expect(mockFetch).toHaveBeenCalledTimes(1) expect(result).toEqual(goblin) }) @@ -212,10 +205,8 @@ describe('getMonster', () => { }) describe('getCachedMonster', () => { - it('returns undefined for unknown slug', async () => { - vi.resetModules() - const { getCachedMonster } = await import('./open5e') - + it('returns undefined for unknown slug', () => { + clearMonsterCache() expect(getCachedMonster('unknown')).toBeUndefined() }) }) diff --git a/frontend/src/api/open5e.ts b/frontend/src/api/open5e.ts index ffc330e..4c335d9 100644 --- a/frontend/src/api/open5e.ts +++ b/frontend/src/api/open5e.ts @@ -54,6 +54,10 @@ export function getCachedMonster(slug: string): MonsterData | undefined { return monsterCache.get(slug) } +export function clearMonsterCache(): void { + monsterCache.clear() +} + export function abilityModifier(score: number): number { return Math.floor((score - 10) / 2) } diff --git a/frontend/src/components/AddCreatureForm.test.tsx b/frontend/src/components/AddCreatureForm.test.tsx index b852d23..2de3aa1 100644 --- a/frontend/src/components/AddCreatureForm.test.tsx +++ b/frontend/src/components/AddCreatureForm.test.tsx @@ -4,7 +4,7 @@ import { AddCreatureForm } from './AddCreatureForm' describe('AddCreatureForm', () => { it('renders name, type, modifier and max hp inputs with add button', () => { - render() + render() expect(screen.getByLabelText('Name')).toBeInTheDocument() expect(screen.getByLabelText('Init Mod')).toBeInTheDocument() @@ -17,7 +17,7 @@ describe('AddCreatureForm', () => { it('calls onAdd with name, modifier, creature type, maxHp, and ac on submit', async () => { const user = userEvent.setup() - const onAdd = vi.fn() + const onAdd = jest.fn() render() await user.type(screen.getByLabelText('Name'), 'Goblin') @@ -32,7 +32,7 @@ describe('AddCreatureForm', () => { it('resets fields after submit', async () => { const user = userEvent.setup() - render() + render() await user.type(screen.getByLabelText('Name'), 'Goblin') await user.clear(screen.getByLabelText('Init Mod')) @@ -46,7 +46,7 @@ describe('AddCreatureForm', () => { it('does not call onAdd when name is empty', async () => { const user = userEvent.setup() - const onAdd = vi.fn() + const onAdd = jest.fn() render() await user.click(screen.getByRole('button', { name: /Add/ })) @@ -56,7 +56,7 @@ describe('AddCreatureForm', () => { it('does not call onAdd when name is only whitespace', async () => { const user = userEvent.setup() - const onAdd = vi.fn() + const onAdd = jest.fn() render() await user.type(screen.getByLabelText('Name'), ' ') @@ -67,7 +67,7 @@ describe('AddCreatureForm', () => { it('trims whitespace from name', async () => { const user = userEvent.setup() - const onAdd = vi.fn() + const onAdd = jest.fn() render() await user.type(screen.getByLabelText('Name'), ' Goblin ') @@ -77,7 +77,7 @@ describe('AddCreatureForm', () => { }) it('defaults modifier to 0 and maxHp to 10', () => { - render() + render() expect(screen.getByLabelText('Init Mod')).toHaveValue(0) expect(screen.getByLabelText('Max HP')).toHaveValue(10) @@ -85,7 +85,7 @@ describe('AddCreatureForm', () => { it('supports negative modifiers', async () => { const user = userEvent.setup() - const onAdd = vi.fn() + const onAdd = jest.fn() render() await user.type(screen.getByLabelText('Name'), 'Zombie') @@ -99,7 +99,7 @@ describe('AddCreatureForm', () => { it('allows switching creature type to party', async () => { const user = userEvent.setup() - const onAdd = vi.fn() + const onAdd = jest.fn() render() await user.type(screen.getByLabelText('Name'), 'Cleric') @@ -111,7 +111,7 @@ describe('AddCreatureForm', () => { describe('quantity stepper', () => { it('shows quantity control for enemy type', () => { - render() + render() expect(screen.getByText('Qty')).toBeInTheDocument() expect(screen.getByText('1')).toBeInTheDocument() @@ -119,7 +119,7 @@ describe('AddCreatureForm', () => { it('hides quantity control for party type', async () => { const user = userEvent.setup() - render() + render() await user.click(screen.getByText('Party')) @@ -128,7 +128,7 @@ describe('AddCreatureForm', () => { it('increments and decrements quantity', async () => { const user = userEvent.setup() - render() + render() const [minusBtn, plusBtn] = screen.getAllByRole('button').filter( (btn) => btn.querySelector('svg') && btn.closest('[class*="border-forge-leather"]'), @@ -143,7 +143,7 @@ describe('AddCreatureForm', () => { }) it('does not go below 1', () => { - render() + render() const minusBtn = screen.getAllByRole('button').find( (btn) => btn.querySelector('svg') && btn.closest('[class*="border-forge-leather"]') && (btn as HTMLButtonElement).disabled, @@ -154,7 +154,7 @@ describe('AddCreatureForm', () => { it('calls onAdd multiple times with numbered names when qty > 1', async () => { const user = userEvent.setup() - const onAdd = vi.fn() + const onAdd = jest.fn() render() await user.type(screen.getByLabelText('Name'), 'Goblin') @@ -175,7 +175,7 @@ describe('AddCreatureForm', () => { it('does not number name when qty is 1', async () => { const user = userEvent.setup() - const onAdd = vi.fn() + const onAdd = jest.fn() render() await user.type(screen.getByLabelText('Name'), 'Goblin') @@ -187,7 +187,7 @@ describe('AddCreatureForm', () => { it('resets quantity after submit', async () => { const user = userEvent.setup() - render() + render() const plusBtn = screen.getAllByRole('button').filter( (btn) => btn.querySelector('svg') && btn.closest('[class*="border-forge-leather"]'), @@ -203,7 +203,7 @@ describe('AddCreatureForm', () => { it('shows count on Add button when qty > 1', async () => { const user = userEvent.setup() - render() + render() const plusBtn = screen.getAllByRole('button').filter( (btn) => btn.querySelector('svg') && btn.closest('[class*="border-forge-leather"]'), @@ -216,7 +216,7 @@ describe('AddCreatureForm', () => { it('resets quantity when switching to party', async () => { const user = userEvent.setup() - render() + render() const plusBtn = screen.getAllByRole('button').filter( (btn) => btn.querySelector('svg') && btn.closest('[class*="border-forge-leather"]'), diff --git a/frontend/src/components/CreatureList.test.tsx b/frontend/src/components/CreatureList.test.tsx index 5441df7..5a9a252 100644 --- a/frontend/src/components/CreatureList.test.tsx +++ b/frontend/src/components/CreatureList.test.tsx @@ -7,13 +7,13 @@ const defaultProps = { activeCreatureId: null as string | null, viewMode: 'dm' as const, statVisibility: 'all' as const, - onRemove: vi.fn(), - onRollInitiative: vi.fn(), - onUpdateInitiative: vi.fn(), - onToggleCreatureType: vi.fn(), - onDamage: vi.fn(), - onHeal: vi.fn(), - onSetTempHp: vi.fn(), + onRemove: jest.fn(), + onRollInitiative: jest.fn(), + onUpdateInitiative: jest.fn(), + onToggleCreatureType: jest.fn(), + onDamage: jest.fn(), + onHeal: jest.fn(), + onSetTempHp: jest.fn(), } const mockCreatures: Creature[] = [ @@ -24,7 +24,7 @@ const mockCreatures: Creature[] = [ describe('CreatureList', () => { beforeEach(() => { - vi.clearAllMocks() + jest.clearAllMocks() }) it('shows empty state when no creatures', () => { @@ -82,7 +82,7 @@ describe('CreatureList', () => { it('calls onRemove with correct id when creature is dead and expanded', async () => { const user = userEvent.setup() - const onRemove = vi.fn() + const onRemove = jest.fn() const deadCreatures: Creature[] = [ { id: '1', name: 'Goblin', initiativeModifier: 2, initiative: 15, creatureType: 'enemy', maxHp: 20, hp: 0, tempHp: 0, monsterSlug: null, ac: 10 }, ] @@ -147,7 +147,7 @@ describe('CreatureList', () => { it('allows manual initiative editing', async () => { const user = userEvent.setup() - const onUpdateInitiative = vi.fn() + const onUpdateInitiative = jest.fn() render( { beforeEach(() => { - vi.clearAllMocks() + jest.clearAllMocks() }) it('shows Start Encounter button when not started', () => { @@ -25,12 +25,12 @@ describe('TurnControls', () => { it('calls onStart when Start Encounter is clicked', async () => { const user = userEvent.setup() - const onStart = vi.fn() + const onStart = jest.fn() render() await user.click(screen.getByRole('button', { name: 'Start Encounter' })) - expect(onStart).toHaveBeenCalledOnce() + expect(onStart).toHaveBeenCalledTimes(1) }) it('shows round number when started', () => { diff --git a/frontend/src/test/setup.ts b/frontend/src/test/setup.ts index a9d0dd3..7fffcb3 100644 --- a/frontend/src/test/setup.ts +++ b/frontend/src/test/setup.ts @@ -1 +1,27 @@ -import '@testing-library/jest-dom/vitest' +import { GlobalRegistrator } from '@happy-dom/global-registrator' +import { afterEach, expect } from 'bun:test' +import * as matchers from '@testing-library/jest-dom/matchers' + +GlobalRegistrator.register() +expect.extend(matchers as Parameters[0]) + +// usehooks-ts' `useLocalStorage` dispatches a custom "local-storage" event on +// `window` to synchronise multiple hooks of the same key within one window. +// Across tests, hooks from earlier tests linger (React roots aren't unmounted +// until GC and @testing-library's cleanup() destabilises happy-dom). When a +// later test calls setItem, those zombies receive the event and — if their +// captured `sortedCreatures` happens to be `[]` — their useEffect writes +// `setTurnState(null)` back into localStorage, corrupting the current test. +// We don't need cross-hook sync in tests (one window, one hook per key), so +// suppressing this custom event removes the pollution channel. +const origDispatchEvent = window.dispatchEvent.bind(window) +window.dispatchEvent = (event: Event): boolean => { + if (event.type === 'local-storage') return true + return origDispatchEvent(event) +} + +afterEach(() => { + document.body.innerHTML = '' + localStorage.clear() +}) + diff --git a/frontend/src/types/bun-test-globals.d.ts b/frontend/src/types/bun-test-globals.d.ts new file mode 100644 index 0000000..3a0006c --- /dev/null +++ b/frontend/src/types/bun-test-globals.d.ts @@ -0,0 +1,11 @@ +/// + +import type { TestingLibraryMatchers } from '@testing-library/jest-dom/matchers' + +declare module 'bun:test' { + // eslint-disable-next-line @typescript-eslint/no-empty-object-type + interface Matchers extends TestingLibraryMatchers {} + // eslint-disable-next-line @typescript-eslint/no-empty-object-type + interface AsymmetricMatchersContaining + extends TestingLibraryMatchers {} +} diff --git a/frontend/tsconfig.app.json b/frontend/tsconfig.app.json index 714f25f..c189f8e 100644 --- a/frontend/tsconfig.app.json +++ b/frontend/tsconfig.app.json @@ -5,7 +5,7 @@ "useDefineForClassFields": true, "lib": ["ES2023", "DOM", "DOM.Iterable"], "module": "ESNext", - "types": ["vite/client", "vitest/globals"], + "types": ["vite/client", "bun", "@testing-library/jest-dom"], "skipLibCheck": true, /* Bundler mode */ diff --git a/frontend/tsconfig.node.json b/frontend/tsconfig.node.json index 8a67f62..1b151e8 100644 --- a/frontend/tsconfig.node.json +++ b/frontend/tsconfig.node.json @@ -4,7 +4,7 @@ "target": "ES2023", "lib": ["ES2023"], "module": "ESNext", - "types": ["node"], + "types": ["bun"], "skipLibCheck": true, /* Bundler mode */ diff --git a/frontend/vitest.config.ts b/frontend/vitest.config.ts deleted file mode 100644 index 4785500..0000000 --- a/frontend/vitest.config.ts +++ /dev/null @@ -1,16 +0,0 @@ -import { defineConfig } from 'vitest/config' -import react from '@vitejs/plugin-react' - -export default defineConfig({ - plugins: [react()], - test: { - environment: 'jsdom', - setupFiles: ['./src/test/setup.ts'], - globals: true, - // Node 25 enables experimental Web Storage by default and installs a - // non-configurable broken `localStorage` on globalThis before jsdom can - // attach its own. Disable it so jsdom's localStorage wins. - // See: https://github.com/vitest-dev/vitest/issues/8757 - execArgv: ['--no-experimental-webstorage'], - }, -}) From e6610e3991b88035d65dfa787f1d03c9044d5ce0 Mon Sep 17 00:00:00 2001 From: phortx Date: Mon, 25 May 2026 19:06:11 +0200 Subject: [PATCH 06/11] feat: add just --- frontend/vite.config.ts | 14 ++++++ justfile | 94 +++++++++++++++++++++++++++++++++++++++++ 2 files changed, 108 insertions(+) create mode 100644 justfile diff --git a/frontend/vite.config.ts b/frontend/vite.config.ts index b4d4cc7..c2188c3 100644 --- a/frontend/vite.config.ts +++ b/frontend/vite.config.ts @@ -2,7 +2,21 @@ import { defineConfig } from 'vite' import react from '@vitejs/plugin-react' import tailwindcss from '@tailwindcss/vite' +// Backend dev port (Spring Boot default). Override via VITE_BACKEND_URL. +const BACKEND_URL = process.env.VITE_BACKEND_URL ?? 'http://localhost:8080' + export default defineConfig({ plugins: [react(), tailwindcss()], base: process.env.GITHUB_ACTIONS ? '/mithril-forge/' : '/', + server: { + port: 5173, + strictPort: true, + proxy: { + // App API + '/api': { target: BACKEND_URL, changeOrigin: true }, + // OpenAPI / Swagger UI for convenient debugging on the dev port + '/v3/api-docs': { target: BACKEND_URL, changeOrigin: true }, + '/swagger-ui': { target: BACKEND_URL, changeOrigin: true }, + }, + }, }) diff --git a/justfile b/justfile new file mode 100644 index 0000000..407fb78 --- /dev/null +++ b/justfile @@ -0,0 +1,94 @@ +# Mithril Forge — task runner +# Run `just` to see all available recipes. +# +# Required tooling: bun, ./gradlew, docker +# Postgres comes up automatically via Spring Boot Docker Compose Support on `bootRun`. + +set shell := ["bash", "-cu"] + +# Default: list all recipes +default: + @just --list + +# --- Development --------------------------------------------------------- + +# Start backend + frontend in parallel (Postgres auto-managed by Spring Boot) +# Ctrl+C cleanly shuts down both processes via SIGTERM propagation. +dev: + #!/usr/bin/env bash + set -euo pipefail + trap 'echo "Shutting down..."; kill 0' EXIT INT TERM + ./gradlew :backend:bootRun --console=plain & + (cd frontend && bun run dev) & + wait + +# Backend only +dev-backend: + ./gradlew :backend:bootRun + +# Frontend only (assumes backend already on :8080) +dev-frontend: + cd frontend && bun run dev + +# --- Build --------------------------------------------------------------- + +# Build the deployable Single-JAR (frontend bundled into backend) +build: + ./gradlew :backend:bootJar + +# Clean build from scratch +build-clean: + ./gradlew :backend:clean :backend:bootJar + +# --- Testing ------------------------------------------------------------- + +# Run all tests (backend + frontend) +test: test-backend test-frontend + +test-backend: + ./gradlew :backend:test + +test-frontend: + cd frontend && bun run test + +# --- Quality ------------------------------------------------------------- + +lint: + cd frontend && bun run lint + +typecheck: + cd frontend && bun run typecheck + +# Run everything CI checks locally +check: lint typecheck test + +# --- Database ------------------------------------------------------------ + +# Manually start the Postgres dev container (usually unnecessary — Spring +# Boot Docker Compose Support brings it up on `bootRun`). +db-up: + docker compose up -d postgres + +db-down: + docker compose down + +# Wipe volumes and restart fresh +db-reset: + docker compose down -v + docker compose up -d postgres + +# --- Utilities ----------------------------------------------------------- + +# Install dependencies for both sides +install: + cd frontend && bun install + ./gradlew --version + +# Wipe everything build-related +clean: + ./gradlew :backend:clean + rm -rf frontend/dist frontend/node_modules frontend/.vite + +# Run the built JAR locally to test the production bundle +run-jar: build + java -jar backend/build/libs/backend-0.0.1-SNAPSHOT.jar From dd44112e9a264dfef7ca422ba56522c1e85c45c7 Mon Sep 17 00:00:00 2001 From: phortx Date: Mon, 25 May 2026 19:12:13 +0200 Subject: [PATCH 07/11] feat: build backend and frontend in one jar --- backend/build.gradle.kts | 56 ++++++++++++++++++++++++++++++++++++++++ 1 file changed, 56 insertions(+) diff --git a/backend/build.gradle.kts b/backend/build.gradle.kts index fddb722..75536af 100644 --- a/backend/build.gradle.kts +++ b/backend/build.gradle.kts @@ -58,3 +58,59 @@ kotlin { tasks.withType { useJUnitPlatform() } + +// --- Frontend Build Integration --- + +val frontendDir = layout.projectDirectory.dir("../frontend") +val frontendDist = frontendDir.dir("dist") +val staticResources = layout.projectDirectory.dir("src/main/resources/static") + +val bunInstall by tasks.registering(Exec::class) { + group = "frontend" + description = "Install frontend dependencies (frozen lockfile)" + workingDir = frontendDir.asFile + commandLine("bun", "install", "--frozen-lockfile") + inputs.file(frontendDir.file("package.json")) + inputs.file(frontendDir.file("bun.lock")) + outputs.dir(frontendDir.dir("node_modules")) +} + +val bunBuild by tasks.registering(Exec::class) { + group = "frontend" + description = "Build frontend with Vite/Bun" + dependsOn(bunInstall) + workingDir = frontendDir.asFile + commandLine("bun", "run", "build") + inputs.dir(frontendDir.dir("src")) + inputs.dir(frontendDir.dir("public")) + inputs.file(frontendDir.file("index.html")) + inputs.file(frontendDir.file("vite.config.ts")) + inputs.file(frontendDir.file("tsconfig.json")) + inputs.file(frontendDir.file("package.json")) + outputs.dir(frontendDist) +} + +val copyFrontend by tasks.registering(Copy::class) { + group = "frontend" + description = "Copy Vite dist into Spring Boot static resources" + dependsOn(bunBuild) + doFirst { delete(staticResources) } // verhindert stale chunks nach Renames + from(frontendDist) + into(staticResources) +} + +// Nur bootJar (Production) bekommt Frontend; bootRun bleibt schlank für Dev +tasks.named("bootJar") { dependsOn(copyFrontend) } + +tasks.named("clean") { + delete(staticResources, frontendDist) +} + +// Frontend-Tests via Gradle (optional, parallel zu bun run test im justfile) +val bunTest by tasks.registering(Exec::class) { + group = "verification" + description = "Run frontend tests via Bun" + dependsOn(bunInstall) + workingDir = frontendDir.asFile + commandLine("bun", "run", "test") +} From e1b79ba0758a0547bd7b569260b044daf83bb80a Mon Sep 17 00:00:00 2001 From: phortx Date: Mon, 25 May 2026 19:19:33 +0200 Subject: [PATCH 08/11] feat: connect frontend and backend --- .../mithril_forge/StatusController.kt | 16 +++++++++++ .../mithril_forge/config/WebMvcConfig.kt | 17 +++++++++++ backend/src/main/resources/application.yaml | 17 +++++++++++ .../mithril_forge/StatusControllerTest.kt | 28 +++++++++++++++++++ 4 files changed, 78 insertions(+) create mode 100644 backend/src/main/kotlin/de/entropy_labs/mithril_forge/StatusController.kt create mode 100644 backend/src/main/kotlin/de/entropy_labs/mithril_forge/config/WebMvcConfig.kt create mode 100644 backend/src/test/kotlin/de/entropy_labs/mithril_forge/StatusControllerTest.kt diff --git a/backend/src/main/kotlin/de/entropy_labs/mithril_forge/StatusController.kt b/backend/src/main/kotlin/de/entropy_labs/mithril_forge/StatusController.kt new file mode 100644 index 0000000..162de57 --- /dev/null +++ b/backend/src/main/kotlin/de/entropy_labs/mithril_forge/StatusController.kt @@ -0,0 +1,16 @@ +package de.entropy_labs.mithril_forge + +import org.springframework.web.bind.annotation.GetMapping +import org.springframework.web.bind.annotation.RequestMapping +import org.springframework.web.bind.annotation.RestController + +@RestController +@RequestMapping("/api") +class StatusController { + + @GetMapping("/status") + fun status(): Map = mapOf( + "status" to "ok", + "app" to "mithril-forge", + ) +} diff --git a/backend/src/main/kotlin/de/entropy_labs/mithril_forge/config/WebMvcConfig.kt b/backend/src/main/kotlin/de/entropy_labs/mithril_forge/config/WebMvcConfig.kt new file mode 100644 index 0000000..bbc2f0a --- /dev/null +++ b/backend/src/main/kotlin/de/entropy_labs/mithril_forge/config/WebMvcConfig.kt @@ -0,0 +1,17 @@ +package de.entropy_labs.mithril_forge.config + +import org.springframework.context.annotation.Configuration +import org.springframework.web.servlet.config.annotation.ViewControllerRegistry +import org.springframework.web.servlet.config.annotation.WebMvcConfigurer + +/** + * SPA-Routing: forwards all non-API, non-static paths to index.html + * so that React Router deep links work when served from the JAR. + */ +@Configuration +class WebMvcConfig : WebMvcConfigurer { + override fun addViewControllers(registry: ViewControllerRegistry) { + // Paths without a dot (i.e. not asset files) → forward to index.html + registry.addViewController("/{path:[^.]*}").setViewName("forward:/index.html") + } +} diff --git a/backend/src/main/resources/application.yaml b/backend/src/main/resources/application.yaml index 14f6655..9bb99f8 100644 --- a/backend/src/main/resources/application.yaml +++ b/backend/src/main/resources/application.yaml @@ -1,3 +1,20 @@ spring: application: name: mithril-forge + docker: + compose: + file: ../compose.yaml # compose.yaml liegt im Root, Backend ist Unterordner + enabled: true + datasource: + url: jdbc:postgresql://localhost:5432/mydatabase + username: myuser + password: secret + devtools: + restart: + exclude: static/** # kein Restart-Loop wenn Frontend-Build static/ befüllt + +sentry: + enabled: false # kein DSN konfiguriert, Sentry im Dev stumm schalten + +server: + port: 8080 diff --git a/backend/src/test/kotlin/de/entropy_labs/mithril_forge/StatusControllerTest.kt b/backend/src/test/kotlin/de/entropy_labs/mithril_forge/StatusControllerTest.kt new file mode 100644 index 0000000..0ec6226 --- /dev/null +++ b/backend/src/test/kotlin/de/entropy_labs/mithril_forge/StatusControllerTest.kt @@ -0,0 +1,28 @@ +package de.entropy_labs.mithril_forge + +import org.junit.jupiter.api.Test +import org.springframework.beans.factory.annotation.Autowired +import org.springframework.boot.webmvc.test.autoconfigure.AutoConfigureMockMvc +import org.springframework.boot.test.context.SpringBootTest +import org.springframework.context.annotation.Import +import org.springframework.test.web.servlet.MockMvc +import org.springframework.test.web.servlet.get + +@SpringBootTest +@AutoConfigureMockMvc +@Import(TestcontainersConfiguration::class) +class StatusControllerTest { + + @Autowired + lateinit var mockMvc: MockMvc + + @Test + fun `GET api-status returns ok`() { + mockMvc.get("/api/status") + .andExpect { + status { isOk() } + jsonPath("$.status") { value("ok") } + jsonPath("$.app") { value("mithril-forge") } + } + } +} From c4f0eda6da6973d401e154e3c013b851c017b8a3 Mon Sep 17 00:00:00 2001 From: phortx Date: Mon, 25 May 2026 19:22:01 +0200 Subject: [PATCH 09/11] feat: update github actions --- .github/workflows/ci.yml | 68 ++++++++++++++++++++-------------------- backend/build.gradle.kts | 1 + 2 files changed, 35 insertions(+), 34 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 758fca9..37bf9e8 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -1,4 +1,4 @@ -name: CI / Deploy +name: CI on: push: @@ -6,57 +6,57 @@ on: pull_request: branches: [main] -permissions: - contents: read - pages: write - id-token: write - -# Only one deployment at a time; don't cancel in-progress runs on main concurrency: - group: pages - cancel-in-progress: false + group: ci-${{ github.ref }} + cancel-in-progress: true jobs: - ci: - name: Lint, Typecheck & Build + build: + name: Build & Test runs-on: ubuntu-latest steps: - name: Checkout uses: actions/checkout@v4 + - name: Setup GraalVM (Java 24) + uses: graalvm/setup-graalvm@v1 + with: + java-version: '24' + distribution: 'graalvm-community' + cache: 'gradle' + - name: Setup Bun uses: oven-sh/setup-bun@v2 + with: + bun-version: '1.3.13' - - name: Install dependencies + - name: Install frontend dependencies + working-directory: frontend run: bun install --frozen-lockfile - - name: Lint + - name: Frontend — Lint + working-directory: frontend run: bun run lint - - name: Typecheck + - name: Frontend — Typecheck + working-directory: frontend run: bun run typecheck - - name: Build - run: bun run build + - name: Frontend — Test + working-directory: frontend + run: bun run test - - name: Upload Pages artifact - if: github.ref == 'refs/heads/main' - uses: actions/upload-pages-artifact@v3 - with: - path: dist + - name: Backend — Test + run: ./gradlew :backend:test - deploy: - name: Deploy to GitHub Pages - needs: ci - if: github.ref == 'refs/heads/main' - runs-on: ubuntu-latest - - environment: - name: github-pages - url: ${{ steps.deployment.outputs.page_url }} + - name: Build Single JAR (frontend embedded) + run: ./gradlew :backend:bootJar - steps: - - name: Deploy - id: deployment - uses: actions/deploy-pages@v4 + - name: Upload JAR artifact + if: github.ref == 'refs/heads/main' + uses: actions/upload-artifact@v4 + with: + name: mithril-forge-jar + path: backend/build/libs/*.jar + retention-days: 30 diff --git a/backend/build.gradle.kts b/backend/build.gradle.kts index 75536af..edbb1ee 100644 --- a/backend/build.gradle.kts +++ b/backend/build.gradle.kts @@ -81,6 +81,7 @@ val bunBuild by tasks.registering(Exec::class) { dependsOn(bunInstall) workingDir = frontendDir.asFile commandLine("bun", "run", "build") + environment.remove("GITHUB_ACTIONS") // prevent wrong Vite base-path (/mithril-forge/ vs /) inputs.dir(frontendDir.dir("src")) inputs.dir(frontendDir.dir("public")) inputs.file(frontendDir.file("index.html")) From 27122b3dc08f2c3f00953e287eeff1c1b67dad82 Mon Sep 17 00:00:00 2001 From: phortx Date: Mon, 25 May 2026 19:26:45 +0200 Subject: [PATCH 10/11] chore: update markdown files --- CLAUDE.md | 237 ++++++++++++++++++++++++++++++++++++++---------------- README.md | 92 ++++++++++++++++++--- 2 files changed, 246 insertions(+), 83 deletions(-) diff --git a/CLAUDE.md b/CLAUDE.md index 198124d..58c614b 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -1,111 +1,206 @@ # Mithril Forge — D&D Encounter Tracker -Web-based encounter tracker with two views: DM (full control) and Player (read-only for TV display). Hosted as a static site on GitHub Pages. +Web-based encounter tracker with two views: DM (full control) and Player (read-only for TV display). Runs as a monorepo: React frontend + Spring Boot backend, served as a single JAR in production. ## Project Overview -A DM runs this on their MacBook with two browser windows: one for the DM view (local screen) and one for the Player view (TV via screen mirroring). State syncs between tabs via localStorage's native `storage` event — no backend, no polling. +A DM runs this on their MacBook with two browser windows: one for the DM view (local screen) and one for the Player view (TV via screen mirroring). Active encounter state syncs between tabs via localStorage. Persistent data (encounter templates, future features) is stored in PostgreSQL via the backend. + +## Monorepo Structure + +``` +mithril-forge/ +├── backend/ # Spring Boot (Kotlin) +│ ├── build.gradle.kts +│ └── src/ +│ ├── main/kotlin/de/entropy_labs/mithril_forge/ +│ │ ├── MithrilForgeApplication.kt +│ │ ├── StatusController.kt +│ │ └── config/WebMvcConfig.kt +│ └── main/resources/ +│ ├── application.yaml +│ └── static/ # gitignored — populated by bootJar build only +├── frontend/ # React 19 + Vite + Bun +│ ├── src/ +│ │ ├── api/ # Open5e + (future) backend API +│ │ ├── components/ # React components (co-located *.test.tsx) +│ │ ├── hooks/ # Custom hooks (co-located *.test.ts) +│ │ ├── types/ # TypeScript type definitions +│ │ ├── utils/ # Utility functions (co-located *.test.ts) +│ │ ├── test/ # Test setup (setup.ts) +│ │ ├── App.tsx # Root component with DM/Player view toggle +│ │ └── main.tsx # Entry point +│ ├── vite.config.ts # Dev proxy: /api → :8080 +│ ├── vitest.config.ts +│ ├── tsconfig.json, tsconfig.app.json, tsconfig.node.json +│ ├── eslint.config.js +│ └── package.json +├── gradle/wrapper/ +├── gradlew, gradlew.bat +├── settings.gradle.kts # include("backend") +├── compose.yaml # Postgres (Docker) +├── justfile # Task runner (primary interface) +├── .tool-versions # bun 1.3.13, java graalvm-community-24.0.1, kotlin 2.3.21 +└── .github/workflows/ci.yml # Lint + typecheck + tests + bootJar → JAR artifact +``` ## Tech Stack -- **Runtime/Build:** Bun -- **Framework:** React 19 with TypeScript -- **Bundler:** Vite -- **Styling:** Tailwind CSS v4 with custom `@theme` block (fantasy color palette, fonts) -- **Icons:** lucide-react -- **State Sync:** `usehooks-ts` `useLocalStorage` hook (reactive cross-tab sync via `storage` event) -- **Testing:** Vitest + React Testing Library (`@testing-library/react`, `jest-dom`, `user-event`), jsdom environment -- **Monster Data:** Open5e SRD API (`src/api/open5e.ts`) -- **Deployment:** Static build artifact on GitHub Pages via GitHub Actions -- **Target:** Desktop/Landscape only (MacBook + TV) +### Frontend +- **React 19** with TypeScript 5 +- **Vite 8** (bundler + dev server) +- **Bun 1.3.13** (runtime + package manager) +- **Tailwind CSS v4** with custom `@theme` block (fantasy color palette, fonts) +- **lucide-react** (icons) +- **usehooks-ts** `useLocalStorage` (cross-tab reactive state via `storage` event) +- **Bun Test + React Testing Library** (`jest-dom`, `user-event`), happy-dom environment +- **Open5e SRD API** (`src/api/open5e.ts`) for monster data + +### Backend +- **Kotlin 2.3.21**, **Java GraalVM Community 24** +- **Spring Boot 4.0.6** (Web MVC, JDBC, Session JDBC, DevTools, Docker Compose Support) +- **PostgreSQL** database, **Flyway** migrations +- **Spring JDBC** for data access +- **SpringDoc OpenAPI 3** (Swagger UI at `/swagger-ui.html`) +- **Testcontainers** (PostgreSQL) for integration tests +- **Sentry** Spring Boot starter (currently disabled) + +## Commands + +### Primary: just (task runner) + +```bash +just dev # Backend + Frontend parallel (Postgres auto via Spring Boot Docker Compose) +just dev-backend # Backend only (:8080) +just dev-frontend # Frontend only (:5173, expects backend on :8080) + +just build # Single-JAR (frontend embedded in backend) +just build-clean # clean + Single-JAR + +just test # All tests (backend + frontend) +just test-backend # ./gradlew :backend:test (Testcontainers) +just test-frontend # cd frontend && bun run test + +just lint # cd frontend && bun run lint +just typecheck # cd frontend && bun run typecheck +just check # lint + typecheck + test + +just db-up # Postgres container manually (usually unnecessary) +just db-down # docker compose down +just db-reset # Wipe volumes + restart + +just install # bun install + gradlew --version +just clean # Remove backend/build/, frontend/dist/, frontend/node_modules/ +just run-jar # Build JAR + run locally (production smoke test) +``` + +### Frontend (from `frontend/`) + +```bash +bun run dev # Vite dev server with HMR +bun run build # Production build +bun run preview # Serve production build locally +bun run lint # ESLint +bun run typecheck # tsc --noEmit +bun run test # Tests once +bun run test:watch # Tests in watch mode +``` + +### Backend (from repo root) + +```bash +./gradlew :backend:bootRun # Start backend (dev, no frontend build) +./gradlew :backend:bootJar # Single-JAR with embedded frontend +./gradlew :backend:test # Backend tests (Testcontainers) +./gradlew :backend:clean # Clean backend build artifacts +``` ## Architecture -### State Management -All encounter state lives in localStorage. The `useLocalStorage` hook from `usehooks-ts` handles persistence and cross-tab reactivity automatically. No BroadcastChannel, no custom sync logic. +### State Strategy (Hybrid) -Three hooks manage distinct state slices: +Active encounter state (creatures, initiative, HP, turns) lives in localStorage. The `useLocalStorage` hook from `usehooks-ts` handles persistence and cross-tab reactivity via `storage` events. Do not replace this with BroadcastChannel, polling, or WebSocket for encounter state. + +Persistent data (encounter templates, future features) will be stored in PostgreSQL via the backend REST API. + +Three frontend hooks manage distinct localStorage slices: - `useEncounter` — creatures, persisted to `'mithril-forge-encounter'` - `useTurnTracker` — active turn, round, persisted to `'mithril-forge-turn'` -- `useEncounterSettings` — stat visibility and other DM settings, persisted to `'mithril-forge-settings'` +- `useEncounterSettings` — stat visibility and DM settings, persisted to `'mithril-forge-settings'` + +### Dev Proxy + +Vite proxies `/api/*` → `http://localhost:8080` in dev. In production the frontend is served by Spring Boot from the same origin — no CORS is needed in either environment. + +### SPA Routing + +`WebMvcConfig.kt` forwards all paths without a dot (i.e. not asset files) to `index.html`, so React Router deep links work in the production JAR. + +### Production Build + +`bootJar` runs the Vite build first (via a Gradle task), copies `frontend/dist/` into `backend/src/main/resources/static/` (gitignored), then assembles the JAR. The result is a single self-contained JAR that serves both the API and the frontend. ### Views + A single app with a toggle to switch between DM and Player mode. Both views read from the same localStorage state. Only the DM view can write/modify state. ### Creature Types - **Player Characters** — manual entry (name, HP, AC, initiative modifier) -- **Enemies** — manual entry or via Open5e monster search (auto-populates name, AC, HP, initiative modifier) +- **Enemies** — manual entry or via Open5e monster search (auto-populates stats) - **Pets/Summons** — not yet implemented ### Stat Visibility (DM-controlled) + The DM can set one of three modes for the Player view: - `all` — all creatures' stats visible - `party-only` — only party members' HP/AC visible (default) - `hidden` — no stats visible in Player view -## Commands - -```bash -bun install # Install dependencies -bun run dev # Start dev server -bun run build # Production build -bun run preview # Preview production build -bun run lint # Run linter -bun run typecheck # Run TypeScript type checking -bun run test # Run tests once -bun run test:watch # Run tests in watch mode -``` +The Player view must never show enemy HP, AC, or stat blocks regardless of this setting. -## Project Structure +## Coding Conventions -``` -src/ - api/ # External API integrations (Open5e monster data) - components/ # React components (co-located *.test.tsx files) - hooks/ # Custom hooks (co-located *.test.ts files) - types/ # TypeScript type definitions - utils/ # Utility functions (co-located *.test.ts files) - test/ # Test setup (setup.ts imports jest-dom matchers) - App.tsx # Root component with DM/Player view toggle - main.tsx # Entry point - index.css # Global styles: Tailwind import + custom fantasy theme -``` +### Frontend +- Functional React components with hooks +- Shared types in `src/types/` +- One responsibility per component +- TypeScript strict mode (`noUnusedLocals`, `noUnusedParameters`) +- Named exports everywhere; only `App.tsx` uses a default export +- Co-locate test files next to their source (`Foo.test.tsx` beside `Foo.tsx`) +- Relative imports only — no path aliases, no barrel exports +- No Prettier — formatting enforced by ESLint only +- English UI — all user-facing text in English -Root config files: `vite.config.ts`, `vitest.config.ts`, `tsconfig.json`, `tsconfig.app.json`, `tsconfig.node.json`, `eslint.config.js` +### Backend +- Kotlin idioms: data classes, extension functions, functional style where it reads naturally +- Spring conventions: `@RestController`, `@Service`, `@Repository` layering +- Flyway for all schema changes — never modify the database by hand +- JDBC for data access (no JPA/ORM) +- Testcontainers for tests that need a real database +- Package structure: `de.entropy_labs.mithril_forge` ## Key Design Decisions -- **No backend.** Everything runs client-side. State persists in localStorage. -- **Cross-tab sync via storage event.** The `useLocalStorage` hook from `usehooks-ts` fires on `storage` events from other tabs. This is the only sync mechanism — do not add BroadcastChannel, polling, or WebSocket. -- **Player view hides sensitive data.** The Player view must never show enemy HP, AC, or stat blocks. Only show: initiative order, names, death status, active turn, round counter, and timer. Stat visibility is DM-controlled via `StatVisibility` setting. +- **Cross-tab sync via storage event.** The `useLocalStorage` hook fires on `storage` events from other tabs. This is the only sync mechanism for active encounter state — do not add BroadcastChannel, polling, or WebSocket. +- **Player view hides sensitive data.** Only show: initiative order, names, death status, active turn, round counter, timer. Enemy HP, AC, stat blocks are always hidden from the Player view. - **Conditions are display-only tags.** No automatic expiration or turn tracking for conditions. -- **English UI.** All user-facing text is in English. - **Fantasy aesthetic.** Dark tones, parchment elements, thematic fonts (Cinzel, Cinzel Decorative, Crimson Pro). Implemented via Tailwind custom theme + CSS animations. -- **Open5e for monsters.** Enemy creatures can be looked up from the SRD via Open5e API. Stat blocks are only visible in DM view. -- **Relative imports only.** No path aliases, no barrel exports. +- **Open5e for monster data.** Enemy creatures can be looked up from the SRD via the Open5e API. Stat blocks are only visible in DM view. +- **No CORS needed.** Dev: Vite proxy. Production: same origin (Spring Boot serves frontend). +- **Target: Desktop/Landscape only.** MacBook + TV. No mobile support planned. -## Coding Conventions +## Current Phase -- Use functional React components with hooks -- Define shared types in `src/types/` -- Keep components focused — one responsibility per component -- Use TypeScript strict mode (+ `noUnusedLocals`, `noUnusedParameters`) -- Prefer named exports; only `App.tsx` uses a default export -- Co-locate test files next to their source files (`Foo.test.tsx` beside `Foo.tsx`) -- No Prettier — formatting enforced by ESLint only +**v0.4 — Backend Integration.** Implemented and working: +1. Monorepo structure (frontend/ + backend/) +2. Spring Boot 4.0.6 backend (Kotlin, PostgreSQL, Flyway) +3. Single-JAR production build (frontend embedded via bootJar) +4. Dev orchestration via justfile (parallel frontend + backend) +5. Vite dev proxy (/api → :8080) +6. SPA routing in production (WebMvcConfig) +7. CI: Lint + typecheck + FE tests + BE tests + bootJar → JAR artifact (no GitHub Pages) +8. All v0.1–v0.3 features intact: encounter UI, HP/AC tracking, Open5e search, stat blocks, test suite -## Current Phase +**Next milestone:** Encounter Templates API — save/load named encounter configurations via backend REST endpoints and PostgreSQL. -**v0.2 — Core Features.** Implemented and working: -1. Tech stack setup (Bun, React, TypeScript, Vite, Tailwind CSS v4) -2. GitHub Actions + Pages deployment -3. Encounter UI: add creatures, roll/set initiative, sorted order, active turn, next turn, round counter -4. localStorage persistence and cross-tab sync -5. Full fantasy aesthetic (dark theme, liquid health orbs, animated glows, parchment panels) -6. HP/AC tracking: damage, healing, temp HP, color-coded liquid health bar -7. Stat blocks: StatBlockPanel with full Open5e SRD data (DM view only) -8. Monster database: Open5e search with autocomplete, auto-populates creature stats -9. Stat visibility control (all / party-only / hidden) for Player view -10. Test suite: Vitest + RTL covering hooks, components, utils, and API - -Features NOT yet implemented: conditions, concentration, pets/summons with owner linking, D&D Beyond integration, mobile support. +Features NOT yet implemented: encounter templates, conditions, concentration, pets/summons with owner linking, D&D Beyond integration, mobile support. diff --git a/README.md b/README.md index 56543cb..fcdfaa6 100644 --- a/README.md +++ b/README.md @@ -5,9 +5,9 @@ **D&D Encounter Tracker — Right in Your Browser** Track initiative, HP, and more across two screens.\ -One for the DM. One for the players. No install. No account. No cost. +One for the DM. One for the players. No account. No cost. -[**Open Mithril Forge**](https://phortx.github.io/mithril-forge/) · [Report a Bug](https://github.com/phortx/mithril-forge/issues) · [Support on Ko-fi](https://ko-fi.com/phortx) +[Report a Bug](https://github.com/phortx/mithril-forge/issues) · [Support on Ko-fi](https://ko-fi.com/phortx) @@ -17,7 +17,7 @@ One for the DM. One for the players. No install. No account. No cost. Managing initiative with paper cards gets unwieldy fast — especially in larger fights where you want to track HP, conditions, and stats properly. Mithril Forge replaces that with a browser-based tracker designed for the tabletop. -**Just open it and go.** There is nothing to install, no account to create, and no subscription to pay for. All data stays in your browser's local storage — private, instant, and entirely yours. +**Just open it and go.** There is nothing to install, no account to create, and no subscription to pay for. All encounter state stays in your browser's local storage — private, instant, and entirely yours. ### How It Works @@ -26,7 +26,7 @@ Open two browser windows on the same machine: - **DM view** on your laptop — full stat blocks, HP, all controls - **Player view** on the TV — initiative order, names, active turn -State syncs between the windows automatically via localStorage. No backend, no server, no cloud. +The active encounter state (creatures, initiative, HP, turns) syncs between the two windows automatically via localStorage. Encounter templates and persistent data are stored in PostgreSQL via the backend. --- @@ -81,7 +81,7 @@ State syncs between the windows automatically via localStorage. No backend, no s ### v0.1 — Proof of Concept - [x] Tech stack setup (Bun, React, TypeScript, Vite) -- [x] GitHub Actions + Pages deployment +- [x] GitHub Actions CI - [x] Add creatures, roll/set initiative, sorted order - [x] Active turn highlight, Next Turn, round counter - [x] localStorage persistence & cross-tab sync @@ -104,7 +104,16 @@ State syncs between the windows automatically via localStorage. No backend, no s - [x] Auto-fill creature stats from SRD data - [x] Stat block side panel (DM only) +### v0.4 — Backend Integration +- [x] Spring Boot backend (Kotlin, PostgreSQL) +- [x] Monorepo structure (frontend/ + backend/) +- [x] Single-JAR production build (frontend embedded in backend) +- [x] Dev orchestration via justfile (parallel frontend + backend) +- [x] Vite dev proxy (/api → :8080, no CORS needed) +- [x] SPA routing in production via WebMvcConfig + ### Future +- [ ] Encounter templates (save/load via backend API) - [ ] Condition tags - [ ] Concentration toggle with visual indicator - [ ] Pets/Summons with owner linkage and shared initiative @@ -118,33 +127,92 @@ State syncs between the windows automatically via localStorage. No backend, no s ### Prerequisites -- [Bun](https://bun.sh) >= 1.0 +| Tool | Version | +|---|---| +| [Bun](https://bun.sh) | 1.3.13 | +| [Java (GraalVM Community)](https://www.graalvm.org) | 24 | +| [Docker](https://www.docker.com) | any recent | +| [just](https://github.com/casey/just) | any recent | + +Recommended: use [mise](https://mise.jdx.dev) or [asdf](https://asdf-vm.com) with the `.tool-versions` file at the repo root to pin exact versions. ### Setup ```bash -bun install +just install # bun install + verify gradlew ``` ### Commands +#### Via just (recommended) + | Command | Description | |---|---| -| `bun run dev` | Start local dev server with HMR | -| `bun run build` | Type-check + production build | +| `just dev` | Start backend + frontend in parallel (Postgres auto-managed) | +| `just dev-backend` | Backend only on :8080 | +| `just dev-frontend` | Frontend only on :5173 (expects backend on :8080) | +| `just build` | Single-JAR build (frontend embedded in backend) | +| `just build-clean` | Clean + Single-JAR build | +| `just test` | All tests (backend + frontend) | +| `just test-backend` | Backend tests only (Testcontainers) | +| `just test-frontend` | Frontend tests only | +| `just lint` | ESLint (frontend) | +| `just typecheck` | TypeScript type check (frontend) | +| `just check` | lint + typecheck + test | +| `just db-up` | Start Postgres container manually | +| `just db-down` | Stop Postgres container | +| `just db-reset` | Wipe volumes + restart Postgres | +| `just install` | Install all dependencies | +| `just clean` | Remove all build artifacts | +| `just run-jar` | Build + run JAR locally (production smoke test) | + +#### Frontend (from `frontend/`) + +| Command | Description | +|---|---| +| `bun run dev` | Vite dev server with HMR | +| `bun run build` | Production build | | `bun run preview` | Serve the production build locally | -| `bun run lint` | Run ESLint | -| `bun run typecheck` | Run TypeScript type checker | +| `bun run lint` | ESLint | +| `bun run typecheck` | TypeScript type checker | +| `bun run test` | Run tests once | +| `bun run test:watch` | Run tests in watch mode | + +#### Backend (from repo root) + +| Command | Description | +|---|---| +| `./gradlew :backend:bootRun` | Start backend (dev, no frontend build) | +| `./gradlew :backend:bootJar` | Build Single-JAR with embedded frontend | +| `./gradlew :backend:test` | Backend tests (Testcontainers) | +| `./gradlew :backend:clean` | Clean backend build artifacts | ### Tech Stack +**Frontend** + | | | |---|---| -| Runtime | Bun | +| Runtime | Bun 1.3.13 | | Framework | React 19 + TypeScript 5 | | Bundler | Vite 8 | | Styling | Tailwind CSS v4 | | State sync | `useLocalStorage` from usehooks-ts | +| Icons | lucide-react | +| Testing | Bun Test + React Testing Library + jest-dom + happy-dom | + +**Backend** + +| | | +|---|---| +| Language | Kotlin 2.3.21 | +| Runtime | Java GraalVM Community 24 | +| Framework | Spring Boot 4.0.6 | +| Database | PostgreSQL | +| Migrations | Flyway | +| Data access | Spring JDBC | +| API docs | SpringDoc OpenAPI (Swagger UI at `/swagger-ui.html`) | +| Testing | Testcontainers (PostgreSQL) | --- From 90dc60df63149820a0fc607b8a4bdc7722ce4ff4 Mon Sep 17 00:00:00 2001 From: phortx Date: Mon, 25 May 2026 19:35:02 +0200 Subject: [PATCH 11/11] fix: build --- backend/build.gradle.kts | 3 +++ 1 file changed, 3 insertions(+) diff --git a/backend/build.gradle.kts b/backend/build.gradle.kts index edbb1ee..b92a1ab 100644 --- a/backend/build.gradle.kts +++ b/backend/build.gradle.kts @@ -103,6 +103,9 @@ val copyFrontend by tasks.registering(Copy::class) { // Nur bootJar (Production) bekommt Frontend; bootRun bleibt schlank für Dev tasks.named("bootJar") { dependsOn(copyFrontend) } +// processResources liest static/ — muss nach copyFrontend laufen +tasks.named("processResources") { dependsOn(copyFrontend) } + tasks.named("clean") { delete(staticResources, frontendDist) }