Compare commits

...

8 Commits

38 changed files with 1109 additions and 281 deletions
+1
View File
@@ -0,0 +1 @@
*.gz filter=lfs diff=lfs merge=lfs -text
+34
View File
@@ -0,0 +1,34 @@
{
"version": "2.0.0",
"tasks": [
{
"label": "ESP-IDF: App Flash (preserve NVS & SPIFFS)",
"type": "shell",
"command": "${config:idf.toolsPath}/python_env/idf5.5_py3.14_env/bin/python ${config:idf.currentSetup}/tools/idf.py -p ${config:idf.port} app-flash && ${config:idf.toolsPath}/python_env/idf5.5_py3.14_env/bin/python -m esptool --port ${config:idf.port} run",
"options": {
"env": {
"IDF_PATH": "${config:idf.currentSetup}",
"IDF_TOOLS_PATH": "${config:idf.toolsPath}"
}
},
"group": {
"kind": "build",
"isDefault": false
},
"problemMatcher": []
},
{
"label": "ESP-IDF: Storage Flash (SPIFFS only)",
"type": "shell",
"command": "${config:idf.toolsPath}/python_env/idf5.5_py3.14_env/bin/python ${config:idf.currentSetup}/tools/idf.py -p ${config:idf.port} storage-flash && ${config:idf.toolsPath}/python_env/idf5.5_py3.14_env/bin/python -m esptool --port ${config:idf.port} run",
"options": {
"env": {
"IDF_PATH": "${config:idf.currentSetup}",
"IDF_TOOLS_PATH": "${config:idf.toolsPath}"
}
},
"group": "build",
"problemMatcher": []
}
]
}
@@ -174,23 +174,32 @@ esp_err_t api_light_status_handler(httpd_req_t *req)
// LED Configuration API
// ============================================================================
static int compare_segments_by_start(const void *a, const void *b)
{
const led_segment_t *seg_a = (const led_segment_t *)a;
const led_segment_t *seg_b = (const led_segment_t *)b;
return (int)seg_a->start - (int)seg_b->start;
}
esp_err_t api_wled_config_get_handler(httpd_req_t *req)
{
ESP_LOGI(TAG, "GET /api/wled/config");
extern led_segment_t segments[LED_SEGMENT_MAX_LEN];
extern size_t segment_count;
size_t required_size = sizeof(segments) * segment_count;
cJSON *json = cJSON_CreateObject();
persistence_manager_t pm;
if (persistence_manager_init(&pm, "led_config") == ESP_OK)
{
segment_count = persistence_manager_get_int(&pm, "segment_count", 0);
size_t required_size = sizeof(led_segment_t) * segment_count;
persistence_manager_get_blob(&pm, "segments", segments, required_size, NULL);
uint8_t segment_count = persistence_manager_get_int(&pm, "segment_count", 0);
persistence_manager_deinit(&pm);
qsort(segments, segment_count, sizeof(led_segment_t), compare_segments_by_start);
cJSON *segments_arr = cJSON_CreateArray();
for (uint8_t i = 0; i < segment_count; ++i)
{
@@ -278,6 +287,8 @@ esp_err_t api_wled_config_post_handler(httpd_req_t *req)
}
cJSON_Delete(json);
qsort(segments, segment_count, sizeof(led_segment_t), compare_segments_by_start);
persistence_manager_t pm;
if (persistence_manager_init(&pm, "led_config") == ESP_OK)
{
@@ -20,12 +20,15 @@ esp_err_t api_static_file_handler(httpd_req_t *req)
const char *uri = req->uri;
wifi_mode_t mode = 0;
esp_wifi_get_mode(&mode);
// Always serve captive.html in AP mode
// In AP mode, redirect root to SPA captive portal route
if (mode == WIFI_MODE_AP || mode == WIFI_MODE_APSTA)
{
if (strcmp(uri, "/") == 0 || strcmp(uri, "/index.html") == 0)
{
uri = "/captive.html";
httpd_resp_set_status(req, "302 Found");
httpd_resp_set_hdr(req, "Location", "/#/captive");
httpd_resp_send(req, NULL, 0);
return ESP_OK;
}
}
else
@@ -109,31 +112,8 @@ esp_err_t api_captive_portal_handler(httpd_req_t *req)
{
ESP_LOGI(TAG, "Captive portal detection: %s", req->uri);
// Serve captive.html directly (status 200, text/html)
const char *base_path = CONFIG_API_SERVER_STATIC_FILES_PATH;
char filepath[256];
snprintf(filepath, sizeof(filepath), "%s/captive.html", base_path);
FILE *f = fopen(filepath, "r");
if (!f)
{
ESP_LOGE(TAG, "captive.html not found: %s", filepath);
httpd_resp_set_status(req, "500 Internal Server Error");
httpd_resp_sendstr(req, "Captive portal not available");
return ESP_FAIL;
}
httpd_resp_set_type(req, "text/html");
char buf[512];
size_t read_bytes;
while ((read_bytes = fread(buf, 1, sizeof(buf), f)) > 0)
{
if (httpd_resp_send_chunk(req, buf, read_bytes) != ESP_OK)
{
fclose(f);
ESP_LOGE(TAG, "Failed to send captive chunk");
return ESP_FAIL;
}
}
fclose(f);
httpd_resp_send_chunk(req, NULL, 0);
httpd_resp_set_status(req, "302 Found");
httpd_resp_set_hdr(req, "Location", "/#/captive");
httpd_resp_send(req, NULL, 0);
return ESP_OK;
}
@@ -97,7 +97,11 @@ void wifi_manager_init()
s_wifi_event_group = xEventGroupCreate();
ESP_ERROR_CHECK(esp_netif_init());
ESP_ERROR_CHECK(esp_event_loop_create_default());
esp_err_t err = esp_event_loop_create_default();
if (err != ESP_OK && err != ESP_ERR_INVALID_STATE)
{
ESP_ERROR_CHECK(err);
}
// Default WiFi Station
esp_netif_create_default_wifi_sta();
@@ -4,5 +4,6 @@ idf_component_register(SRCS
INCLUDE_DIRS "include"
REQUIRES
mercedes
simulator
u8g2
)
@@ -1,6 +1,9 @@
#include "hermes/screensaver/clock_screensaver.h"
#include "simulator.h"
#include <ctime>
#include <cstring>
ClockScreensaver::ClockScreensaver(u8g2_t *u8g2) : m_u8g2(u8g2)
{
@@ -8,6 +11,14 @@ ClockScreensaver::ClockScreensaver(u8g2_t *u8g2) : m_u8g2(u8g2)
void ClockScreensaver::get_time_string(char *buffer, size_t bufferSize)
{
const char *sim_time = get_time();
if (sim_time != NULL)
{
strncpy(buffer, sim_time, bufferSize - 1);
buffer[bufferSize - 1] = '\0';
return;
}
time_t rawtime;
struct tm *timeinfo;
time(&rawtime);
@@ -399,6 +399,7 @@ void stop_simulation_task(void)
{
TaskHandle_t handle_to_delete = simulation_task_handle;
simulation_task_handle = NULL;
time = NULL;
xSemaphoreGive(simulation_mutex);
// Check if the task still exists before deleting it
+7
View File
@@ -11,6 +11,7 @@
#include <esp_log.h>
#include <freertos/FreeRTOS.h>
#include <freertos/task.h>
#include <esp_system.h>
#include <nvs_flash.h>
#include <sdkconfig.h>
@@ -39,6 +40,12 @@ void app_main(void)
gpio_set_level(WIFI_ANT_CONFIG, 1); // HIGH
#endif
esp_reset_reason_t reset_reason = esp_reset_reason();
if (reset_reason == ESP_RST_PANIC || reset_reason == ESP_RST_TASK_WDT || reset_reason == ESP_RST_INT_WDT)
{
ESP_LOGW("app_main", "Reboot after crash (reason: %d) — continuing normal init", reset_reason);
}
// Initialize NVS
esp_err_t err = nvs_flash_init();
if (err == ESP_ERR_NVS_NO_FREE_PAGES || err == ESP_ERR_NVS_NEW_VERSION_FOUND)
+4 -7
View File
@@ -24,13 +24,6 @@ CONFIG_LWIP_SNTP_UPDATE_DELAY=14400000
CONFIG_LWIP_SNTP_STARTUP_DELAY=y
CONFIG_LWIP_SNTP_MAXIMUM_STARTUP_DELAY=5000
# ESP PSRAM
CONFIG_SPIRAM=y
# SPI RAM config
CONFIG_SPIRAM_SPEED=80
CONFIG_SPIRAM_USE_CAPS_ALLOC=y
# HTTP Server WebSocket Support
CONFIG_HTTPD_WS_SUPPORT=y
@@ -39,8 +32,12 @@ CONFIG_MQTT_CLIENT_BROKER_URL="mqtts://mqtt.mars3142.dev:8883"
CONFIG_MQTT_CLIENT_USERNAME="system-control"
CONFIG_MQTT_CLIENT_PASSWORD="3jHLhNPLcn_dPrukrpMJ"
# System Event Task
CONFIG_ESP_SYSTEM_EVENT_TASK_STACK_SIZE=4096
# Compiler Options
CONFIG_COMPILER_DISABLE_DEFAULT_ERRORS=y
CONFIG_ESP_SYSTEM_USE_FRAME_POINTER=y
# Certificate Bundle
CONFIG_MBEDTLS_CERTIFICATE_BUNDLE=n
+7
View File
@@ -2,3 +2,10 @@
CONFIG_IDF_TARGET="esp32s3"
CONFIG_API_SERVER_HOSTNAME="system-client"
# ESP PSRAM
CONFIG_SPIRAM=y
# SPI RAM config
CONFIG_SPIRAM_SPEED=80
CONFIG_SPIRAM_USE_CAPS_ALLOC=y
+2 -6
View File
@@ -63,9 +63,5 @@
# Abenddämmerung (Violett → Blau)
80,45,95,0,115,250
60,40,100,0,110,250
45,35,95,0,105,250
30,30,85,0,100,250
25,30,75,0,95,250
20,25,65,0,90,250
15,20,50,0,85,250
45,35,78,0,100,250
20,25,55,0,88,250
1 # Nacht (Tiefblau/Dunkelblau)
63
64
65
66
67
Binary file not shown.
+1
View File
@@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 100 100"><text y=".9em" font-size="90">🚂</text></svg>

After

Width:  |  Height:  |  Size: 109 B

Binary file not shown.
Binary file not shown.
File diff suppressed because one or more lines are too long
+23
View File
@@ -0,0 +1,23 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0, maximum-scale=1.0, user-scalable=no">
<meta name="theme-color" content="#1a1a2e">
<meta name="apple-mobile-web-app-capable" content="yes">
<meta name="mobile-web-app-capable" content="yes">
<meta name="apple-mobile-web-app-status-bar-style" content="black-translucent">
<link rel="icon" type="image/svg+xml" href="/favicon.svg">
<title>System Control</title>
<script type="module" crossorigin src="/index-YHhIjoLo.js"></script>
<link rel="modulepreload" crossorigin href="/vendor-CwcuF_np.js">
<link rel="stylesheet" crossorigin href="/vendor-CbWpK_cD.css">
<link rel="stylesheet" crossorigin href="/index-BfY4NlvY.css">
</head>
<body>
<div id="app"></div>
</body>
</html>
+1
View File
@@ -0,0 +1 @@
<HTML><HEAD><TITLE>Success</TITLE></HEAD><BODY>Success</BODY></HTML>
Binary file not shown.
Binary file not shown.
File diff suppressed because one or more lines are too long
@@ -0,0 +1,11 @@
{
"enableAllProjectMcpServers": true,
"enabledMcpjsonServers": [
"svelte"
],
"permissions": {
"allow": [
"mcp__svelte__svelte-autofixer"
]
}
}
+319 -186
View File
@@ -10,10 +10,12 @@
"@picocss/pico": "^2.1.1",
"@tailwindcss/vite": "^4.1.18",
"gsap": "^3.13.0",
"svelte-french-toast": "^2.0.0-alpha.0",
"svelte-spa-router": "^4.0.2"
},
"devDependencies": {
"@sveltejs/vite-plugin-svelte": "^6.1.1",
"@tailwindcss/typography": "^0.5.19",
"autoprefixer": "^10.4.16",
"postcss": "^8.4.31",
"svelte": "^5.53.5",
@@ -879,7 +881,6 @@
"version": "1.0.8",
"resolved": "https://registry.npmjs.org/@sveltejs/acorn-typescript/-/acorn-typescript-1.0.8.tgz",
"integrity": "sha512-esgN+54+q0NjB0Y/4BomT9samII7jGwNy/2a3wNZbT2A2RpmXsXwUt24LvLhx6jUq2gVk4cWEvcRO6MFQbOfNA==",
"dev": true,
"license": "MIT",
"peerDependencies": {
"acorn": "^8.9.0"
@@ -925,18 +926,18 @@
}
},
"node_modules/@tailwindcss/node": {
"version": "4.1.18",
"resolved": "https://registry.npmjs.org/@tailwindcss/node/-/node-4.1.18.tgz",
"integrity": "sha512-DoR7U1P7iYhw16qJ49fgXUlry1t4CpXeErJHnQ44JgTSKMaZUdf17cfn5mHchfJ4KRBZRFA/Coo+MUF5+gOaCQ==",
"version": "4.2.2",
"resolved": "https://registry.npmjs.org/@tailwindcss/node/-/node-4.2.2.tgz",
"integrity": "sha512-pXS+wJ2gZpVXqFaUEjojq7jzMpTGf8rU6ipJz5ovJV6PUGmlJ+jvIwGrzdHdQ80Sg+wmQxUFuoW1UAAwHNEdFA==",
"license": "MIT",
"dependencies": {
"@jridgewell/remapping": "^2.3.4",
"enhanced-resolve": "^5.18.3",
"@jridgewell/remapping": "^2.3.5",
"enhanced-resolve": "^5.19.0",
"jiti": "^2.6.1",
"lightningcss": "1.30.2",
"lightningcss": "1.32.0",
"magic-string": "^0.30.21",
"source-map-js": "^1.2.1",
"tailwindcss": "4.1.18"
"tailwindcss": "4.2.2"
}
},
"node_modules/@tailwindcss/node/node_modules/jiti": {
@@ -949,38 +950,38 @@
}
},
"node_modules/@tailwindcss/node/node_modules/tailwindcss": {
"version": "4.1.18",
"resolved": "https://registry.npmjs.org/tailwindcss/-/tailwindcss-4.1.18.tgz",
"integrity": "sha512-4+Z+0yiYyEtUVCScyfHCxOYP06L5Ne+JiHhY2IjR2KWMIWhJOYZKLSGZaP5HkZ8+bY0cxfzwDE5uOmzFXyIwxw==",
"version": "4.2.2",
"resolved": "https://registry.npmjs.org/tailwindcss/-/tailwindcss-4.2.2.tgz",
"integrity": "sha512-KWBIxs1Xb6NoLdMVqhbhgwZf2PGBpPEiwOqgI4pFIYbNTfBXiKYyWoTsXgBQ9WFg/OlhnvHaY+AEpW7wSmFo2Q==",
"license": "MIT"
},
"node_modules/@tailwindcss/oxide": {
"version": "4.1.18",
"resolved": "https://registry.npmjs.org/@tailwindcss/oxide/-/oxide-4.1.18.tgz",
"integrity": "sha512-EgCR5tTS5bUSKQgzeMClT6iCY3ToqE1y+ZB0AKldj809QXk1Y+3jB0upOYZrn9aGIzPtUsP7sX4QQ4XtjBB95A==",
"version": "4.2.2",
"resolved": "https://registry.npmjs.org/@tailwindcss/oxide/-/oxide-4.2.2.tgz",
"integrity": "sha512-qEUA07+E5kehxYp9BVMpq9E8vnJuBHfJEC0vPC5e7iL/hw7HR61aDKoVoKzrG+QKp56vhNZe4qwkRmMC0zDLvg==",
"license": "MIT",
"engines": {
"node": ">= 10"
"node": ">= 20"
},
"optionalDependencies": {
"@tailwindcss/oxide-android-arm64": "4.1.18",
"@tailwindcss/oxide-darwin-arm64": "4.1.18",
"@tailwindcss/oxide-darwin-x64": "4.1.18",
"@tailwindcss/oxide-freebsd-x64": "4.1.18",
"@tailwindcss/oxide-linux-arm-gnueabihf": "4.1.18",
"@tailwindcss/oxide-linux-arm64-gnu": "4.1.18",
"@tailwindcss/oxide-linux-arm64-musl": "4.1.18",
"@tailwindcss/oxide-linux-x64-gnu": "4.1.18",
"@tailwindcss/oxide-linux-x64-musl": "4.1.18",
"@tailwindcss/oxide-wasm32-wasi": "4.1.18",
"@tailwindcss/oxide-win32-arm64-msvc": "4.1.18",
"@tailwindcss/oxide-win32-x64-msvc": "4.1.18"
"@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"
}
},
"node_modules/@tailwindcss/oxide-android-arm64": {
"version": "4.1.18",
"resolved": "https://registry.npmjs.org/@tailwindcss/oxide-android-arm64/-/oxide-android-arm64-4.1.18.tgz",
"integrity": "sha512-dJHz7+Ugr9U/diKJA0W6N/6/cjI+ZTAoxPf9Iz9BFRF2GzEX8IvXxFIi/dZBloVJX/MZGvRuFA9rqwdiIEZQ0Q==",
"version": "4.2.2",
"resolved": "https://registry.npmjs.org/@tailwindcss/oxide-android-arm64/-/oxide-android-arm64-4.2.2.tgz",
"integrity": "sha512-dXGR1n+P3B6748jZO/SvHZq7qBOqqzQ+yFrXpoOWWALWndF9MoSKAT3Q0fYgAzYzGhxNYOoysRvYlpixRBBoDg==",
"cpu": [
"arm64"
],
@@ -990,13 +991,13 @@
"android"
],
"engines": {
"node": ">= 10"
"node": ">= 20"
}
},
"node_modules/@tailwindcss/oxide-darwin-arm64": {
"version": "4.1.18",
"resolved": "https://registry.npmjs.org/@tailwindcss/oxide-darwin-arm64/-/oxide-darwin-arm64-4.1.18.tgz",
"integrity": "sha512-Gc2q4Qhs660bhjyBSKgq6BYvwDz4G+BuyJ5H1xfhmDR3D8HnHCmT/BSkvSL0vQLy/nkMLY20PQ2OoYMO15Jd0A==",
"version": "4.2.2",
"resolved": "https://registry.npmjs.org/@tailwindcss/oxide-darwin-arm64/-/oxide-darwin-arm64-4.2.2.tgz",
"integrity": "sha512-iq9Qjr6knfMpZHj55/37ouZeykwbDqF21gPFtfnhCCKGDcPI/21FKC9XdMO/XyBM7qKORx6UIhGgg6jLl7BZlg==",
"cpu": [
"arm64"
],
@@ -1006,13 +1007,13 @@
"darwin"
],
"engines": {
"node": ">= 10"
"node": ">= 20"
}
},
"node_modules/@tailwindcss/oxide-darwin-x64": {
"version": "4.1.18",
"resolved": "https://registry.npmjs.org/@tailwindcss/oxide-darwin-x64/-/oxide-darwin-x64-4.1.18.tgz",
"integrity": "sha512-FL5oxr2xQsFrc3X9o1fjHKBYBMD1QZNyc1Xzw/h5Qu4XnEBi3dZn96HcHm41c/euGV+GRiXFfh2hUCyKi/e+yw==",
"version": "4.2.2",
"resolved": "https://registry.npmjs.org/@tailwindcss/oxide-darwin-x64/-/oxide-darwin-x64-4.2.2.tgz",
"integrity": "sha512-BlR+2c3nzc8f2G639LpL89YY4bdcIdUmiOOkv2GQv4/4M0vJlpXEa0JXNHhCHU7VWOKWT/CjqHdTP8aUuDJkuw==",
"cpu": [
"x64"
],
@@ -1022,13 +1023,13 @@
"darwin"
],
"engines": {
"node": ">= 10"
"node": ">= 20"
}
},
"node_modules/@tailwindcss/oxide-freebsd-x64": {
"version": "4.1.18",
"resolved": "https://registry.npmjs.org/@tailwindcss/oxide-freebsd-x64/-/oxide-freebsd-x64-4.1.18.tgz",
"integrity": "sha512-Fj+RHgu5bDodmV1dM9yAxlfJwkkWvLiRjbhuO2LEtwtlYlBgiAT4x/j5wQr1tC3SANAgD+0YcmWVrj8R9trVMA==",
"version": "4.2.2",
"resolved": "https://registry.npmjs.org/@tailwindcss/oxide-freebsd-x64/-/oxide-freebsd-x64-4.2.2.tgz",
"integrity": "sha512-YUqUgrGMSu2CDO82hzlQ5qSb5xmx3RUrke/QgnoEx7KvmRJHQuZHZmZTLSuuHwFf0DJPybFMXMYf+WJdxHy/nQ==",
"cpu": [
"x64"
],
@@ -1038,13 +1039,13 @@
"freebsd"
],
"engines": {
"node": ">= 10"
"node": ">= 20"
}
},
"node_modules/@tailwindcss/oxide-linux-arm-gnueabihf": {
"version": "4.1.18",
"resolved": "https://registry.npmjs.org/@tailwindcss/oxide-linux-arm-gnueabihf/-/oxide-linux-arm-gnueabihf-4.1.18.tgz",
"integrity": "sha512-Fp+Wzk/Ws4dZn+LV2Nqx3IilnhH51YZoRaYHQsVq3RQvEl+71VGKFpkfHrLM/Li+kt5c0DJe/bHXK1eHgDmdiA==",
"version": "4.2.2",
"resolved": "https://registry.npmjs.org/@tailwindcss/oxide-linux-arm-gnueabihf/-/oxide-linux-arm-gnueabihf-4.2.2.tgz",
"integrity": "sha512-FPdhvsW6g06T9BWT0qTwiVZYE2WIFo2dY5aCSpjG/S/u1tby+wXoslXS0kl3/KXnULlLr1E3NPRRw0g7t2kgaQ==",
"cpu": [
"arm"
],
@@ -1054,77 +1055,89 @@
"linux"
],
"engines": {
"node": ">= 10"
"node": ">= 20"
}
},
"node_modules/@tailwindcss/oxide-linux-arm64-gnu": {
"version": "4.1.18",
"resolved": "https://registry.npmjs.org/@tailwindcss/oxide-linux-arm64-gnu/-/oxide-linux-arm64-gnu-4.1.18.tgz",
"integrity": "sha512-S0n3jboLysNbh55Vrt7pk9wgpyTTPD0fdQeh7wQfMqLPM/Hrxi+dVsLsPrycQjGKEQk85Kgbx+6+QnYNiHalnw==",
"version": "4.2.2",
"resolved": "https://registry.npmjs.org/@tailwindcss/oxide-linux-arm64-gnu/-/oxide-linux-arm64-gnu-4.2.2.tgz",
"integrity": "sha512-4og1V+ftEPXGttOO7eCmW7VICmzzJWgMx+QXAJRAhjrSjumCwWqMfkDrNu1LXEQzNAwz28NCUpucgQPrR4S2yw==",
"cpu": [
"arm64"
],
"libc": [
"glibc"
],
"license": "MIT",
"optional": true,
"os": [
"linux"
],
"engines": {
"node": ">= 10"
"node": ">= 20"
}
},
"node_modules/@tailwindcss/oxide-linux-arm64-musl": {
"version": "4.1.18",
"resolved": "https://registry.npmjs.org/@tailwindcss/oxide-linux-arm64-musl/-/oxide-linux-arm64-musl-4.1.18.tgz",
"integrity": "sha512-1px92582HkPQlaaCkdRcio71p8bc8i/ap5807tPRDK/uw953cauQBT8c5tVGkOwrHMfc2Yh6UuxaH4vtTjGvHg==",
"version": "4.2.2",
"resolved": "https://registry.npmjs.org/@tailwindcss/oxide-linux-arm64-musl/-/oxide-linux-arm64-musl-4.2.2.tgz",
"integrity": "sha512-oCfG/mS+/+XRlwNjnsNLVwnMWYH7tn/kYPsNPh+JSOMlnt93mYNCKHYzylRhI51X+TbR+ufNhhKKzm6QkqX8ag==",
"cpu": [
"arm64"
],
"libc": [
"musl"
],
"license": "MIT",
"optional": true,
"os": [
"linux"
],
"engines": {
"node": ">= 10"
"node": ">= 20"
}
},
"node_modules/@tailwindcss/oxide-linux-x64-gnu": {
"version": "4.1.18",
"resolved": "https://registry.npmjs.org/@tailwindcss/oxide-linux-x64-gnu/-/oxide-linux-x64-gnu-4.1.18.tgz",
"integrity": "sha512-v3gyT0ivkfBLoZGF9LyHmts0Isc8jHZyVcbzio6Wpzifg/+5ZJpDiRiUhDLkcr7f/r38SWNe7ucxmGW3j3Kb/g==",
"version": "4.2.2",
"resolved": "https://registry.npmjs.org/@tailwindcss/oxide-linux-x64-gnu/-/oxide-linux-x64-gnu-4.2.2.tgz",
"integrity": "sha512-rTAGAkDgqbXHNp/xW0iugLVmX62wOp2PoE39BTCGKjv3Iocf6AFbRP/wZT/kuCxC9QBh9Pu8XPkv/zCZB2mcMg==",
"cpu": [
"x64"
],
"libc": [
"glibc"
],
"license": "MIT",
"optional": true,
"os": [
"linux"
],
"engines": {
"node": ">= 10"
"node": ">= 20"
}
},
"node_modules/@tailwindcss/oxide-linux-x64-musl": {
"version": "4.1.18",
"resolved": "https://registry.npmjs.org/@tailwindcss/oxide-linux-x64-musl/-/oxide-linux-x64-musl-4.1.18.tgz",
"integrity": "sha512-bhJ2y2OQNlcRwwgOAGMY0xTFStt4/wyU6pvI6LSuZpRgKQwxTec0/3Scu91O8ir7qCR3AuepQKLU/kX99FouqQ==",
"version": "4.2.2",
"resolved": "https://registry.npmjs.org/@tailwindcss/oxide-linux-x64-musl/-/oxide-linux-x64-musl-4.2.2.tgz",
"integrity": "sha512-XW3t3qwbIwiSyRCggeO2zxe3KWaEbM0/kW9e8+0XpBgyKU4ATYzcVSMKteZJ1iukJ3HgHBjbg9P5YPRCVUxlnQ==",
"cpu": [
"x64"
],
"libc": [
"musl"
],
"license": "MIT",
"optional": true,
"os": [
"linux"
],
"engines": {
"node": ">= 10"
"node": ">= 20"
}
},
"node_modules/@tailwindcss/oxide-wasm32-wasi": {
"version": "4.1.18",
"resolved": "https://registry.npmjs.org/@tailwindcss/oxide-wasm32-wasi/-/oxide-wasm32-wasi-4.1.18.tgz",
"integrity": "sha512-LffYTvPjODiP6PT16oNeUQJzNVyJl1cjIebq/rWWBF+3eDst5JGEFSc5cWxyRCJ0Mxl+KyIkqRxk1XPEs9x8TA==",
"version": "4.2.2",
"resolved": "https://registry.npmjs.org/@tailwindcss/oxide-wasm32-wasi/-/oxide-wasm32-wasi-4.2.2.tgz",
"integrity": "sha512-eKSztKsmEsn1O5lJ4ZAfyn41NfG7vzCg496YiGtMDV86jz1q/irhms5O0VrY6ZwTUkFy/EKG3RfWgxSI3VbZ8Q==",
"bundleDependencies": [
"@napi-rs/wasm-runtime",
"@emnapi/core",
@@ -1139,21 +1152,79 @@
"license": "MIT",
"optional": true,
"dependencies": {
"@emnapi/core": "^1.7.1",
"@emnapi/runtime": "^1.7.1",
"@emnapi/core": "^1.8.1",
"@emnapi/runtime": "^1.8.1",
"@emnapi/wasi-threads": "^1.1.0",
"@napi-rs/wasm-runtime": "^1.1.0",
"@napi-rs/wasm-runtime": "^1.1.1",
"@tybys/wasm-util": "^0.10.1",
"tslib": "^2.4.0"
"tslib": "^2.8.1"
},
"engines": {
"node": ">=14.0.0"
}
},
"node_modules/@tailwindcss/oxide-wasm32-wasi/node_modules/@emnapi/core": {
"version": "1.8.1",
"inBundle": true,
"license": "MIT",
"optional": true,
"dependencies": {
"@emnapi/wasi-threads": "1.1.0",
"tslib": "^2.4.0"
}
},
"node_modules/@tailwindcss/oxide-wasm32-wasi/node_modules/@emnapi/runtime": {
"version": "1.8.1",
"inBundle": true,
"license": "MIT",
"optional": true,
"dependencies": {
"tslib": "^2.4.0"
}
},
"node_modules/@tailwindcss/oxide-wasm32-wasi/node_modules/@emnapi/wasi-threads": {
"version": "1.1.0",
"inBundle": true,
"license": "MIT",
"optional": true,
"dependencies": {
"tslib": "^2.4.0"
}
},
"node_modules/@tailwindcss/oxide-wasm32-wasi/node_modules/@napi-rs/wasm-runtime": {
"version": "1.1.1",
"inBundle": true,
"license": "MIT",
"optional": true,
"dependencies": {
"@emnapi/core": "^1.7.1",
"@emnapi/runtime": "^1.7.1",
"@tybys/wasm-util": "^0.10.1"
},
"funding": {
"type": "github",
"url": "https://github.com/sponsors/Brooooooklyn"
}
},
"node_modules/@tailwindcss/oxide-wasm32-wasi/node_modules/@tybys/wasm-util": {
"version": "0.10.1",
"inBundle": true,
"license": "MIT",
"optional": true,
"dependencies": {
"tslib": "^2.4.0"
}
},
"node_modules/@tailwindcss/oxide-wasm32-wasi/node_modules/tslib": {
"version": "2.8.1",
"inBundle": true,
"license": "0BSD",
"optional": true
},
"node_modules/@tailwindcss/oxide-win32-arm64-msvc": {
"version": "4.1.18",
"resolved": "https://registry.npmjs.org/@tailwindcss/oxide-win32-arm64-msvc/-/oxide-win32-arm64-msvc-4.1.18.tgz",
"integrity": "sha512-HjSA7mr9HmC8fu6bdsZvZ+dhjyGCLdotjVOgLA2vEqxEBZaQo9YTX4kwgEvPCpRh8o4uWc4J/wEoFzhEmjvPbA==",
"version": "4.2.2",
"resolved": "https://registry.npmjs.org/@tailwindcss/oxide-win32-arm64-msvc/-/oxide-win32-arm64-msvc-4.2.2.tgz",
"integrity": "sha512-qPmaQM4iKu5mxpsrWZMOZRgZv1tOZpUm+zdhhQP0VhJfyGGO3aUKdbh3gDZc/dPLQwW4eSqWGrrcWNBZWUWaXQ==",
"cpu": [
"arm64"
],
@@ -1163,13 +1234,13 @@
"win32"
],
"engines": {
"node": ">= 10"
"node": ">= 20"
}
},
"node_modules/@tailwindcss/oxide-win32-x64-msvc": {
"version": "4.1.18",
"resolved": "https://registry.npmjs.org/@tailwindcss/oxide-win32-x64-msvc/-/oxide-win32-x64-msvc-4.1.18.tgz",
"integrity": "sha512-bJWbyYpUlqamC8dpR7pfjA0I7vdF6t5VpUGMWRkXVE3AXgIZjYUYAK7II1GNaxR8J1SSrSrppRar8G++JekE3Q==",
"version": "4.2.2",
"resolved": "https://registry.npmjs.org/@tailwindcss/oxide-win32-x64-msvc/-/oxide-win32-x64-msvc-4.2.2.tgz",
"integrity": "sha512-1T/37VvI7WyH66b+vqHj/cLwnCxt7Qt3WFu5Q8hk65aOvlwAhs7rAp1VkulBJw/N4tMirXjVnylTR72uI0HGcA==",
"cpu": [
"x64"
],
@@ -1179,27 +1250,54 @@
"win32"
],
"engines": {
"node": ">= 10"
"node": ">= 20"
}
},
"node_modules/@tailwindcss/typography": {
"version": "0.5.19",
"resolved": "https://registry.npmjs.org/@tailwindcss/typography/-/typography-0.5.19.tgz",
"integrity": "sha512-w31dd8HOx3k9vPtcQh5QHP9GwKcgbMp87j58qi6xgiBnFFtKEAgCWnDw4qUT8aHwkCp8bKvb/KGKWWHedP0AAg==",
"dev": true,
"license": "MIT",
"dependencies": {
"postcss-selector-parser": "6.0.10"
},
"peerDependencies": {
"tailwindcss": ">=3.0.0 || insiders || >=4.0.0-alpha.20 || >=4.0.0-beta.1"
}
},
"node_modules/@tailwindcss/typography/node_modules/postcss-selector-parser": {
"version": "6.0.10",
"resolved": "https://registry.npmjs.org/postcss-selector-parser/-/postcss-selector-parser-6.0.10.tgz",
"integrity": "sha512-IQ7TZdoaqbT+LCpShg46jnZVlhWD2w6iQYAcYXfHARZ7X1t/UGhhceQDs5X0cGqKvYlHNOuv7Oa1xmb0oQuA3w==",
"dev": true,
"license": "MIT",
"dependencies": {
"cssesc": "^3.0.0",
"util-deprecate": "^1.0.2"
},
"engines": {
"node": ">=4"
}
},
"node_modules/@tailwindcss/vite": {
"version": "4.1.18",
"resolved": "https://registry.npmjs.org/@tailwindcss/vite/-/vite-4.1.18.tgz",
"integrity": "sha512-jVA+/UpKL1vRLg6Hkao5jldawNmRo7mQYrZtNHMIVpLfLhDml5nMRUo/8MwoX2vNXvnaXNNMedrMfMugAVX1nA==",
"version": "4.2.2",
"resolved": "https://registry.npmjs.org/@tailwindcss/vite/-/vite-4.2.2.tgz",
"integrity": "sha512-mEiF5HO1QqCLXoNEfXVA1Tzo+cYsrqV7w9Juj2wdUFyW07JRenqMG225MvPwr3ZD9N1bFQj46X7r33iHxLUW0w==",
"license": "MIT",
"dependencies": {
"@tailwindcss/node": "4.1.18",
"@tailwindcss/oxide": "4.1.18",
"tailwindcss": "4.1.18"
"@tailwindcss/node": "4.2.2",
"@tailwindcss/oxide": "4.2.2",
"tailwindcss": "4.2.2"
},
"peerDependencies": {
"vite": "^5.2.0 || ^6 || ^7"
"vite": "^5.2.0 || ^6 || ^7 || ^8"
}
},
"node_modules/@tailwindcss/vite/node_modules/tailwindcss": {
"version": "4.1.18",
"resolved": "https://registry.npmjs.org/tailwindcss/-/tailwindcss-4.1.18.tgz",
"integrity": "sha512-4+Z+0yiYyEtUVCScyfHCxOYP06L5Ne+JiHhY2IjR2KWMIWhJOYZKLSGZaP5HkZ8+bY0cxfzwDE5uOmzFXyIwxw==",
"version": "4.2.2",
"resolved": "https://registry.npmjs.org/tailwindcss/-/tailwindcss-4.2.2.tgz",
"integrity": "sha512-KWBIxs1Xb6NoLdMVqhbhgwZf2PGBpPEiwOqgI4pFIYbNTfBXiKYyWoTsXgBQ9WFg/OlhnvHaY+AEpW7wSmFo2Q==",
"license": "MIT"
},
"node_modules/@types/chai": {
@@ -1230,7 +1328,6 @@
"version": "2.0.7",
"resolved": "https://registry.npmjs.org/@types/trusted-types/-/trusted-types-2.0.7.tgz",
"integrity": "sha512-ScaPdn1dQczgbl0QFTeTOmVHFULt394XJgOQNoyVhZ6r2vLnMLJfBPd53SB52T/3G36VI1/g2MZaX0cwDuXsfw==",
"dev": true,
"license": "MIT"
},
"node_modules/@vitest/expect": {
@@ -1352,7 +1449,6 @@
"version": "8.15.0",
"resolved": "https://registry.npmjs.org/acorn/-/acorn-8.15.0.tgz",
"integrity": "sha512-NZyJarBfL7nWwIq+FDL6Zp/yHEhePMNnnJ0y3qfieCrmNvYct8uvtiV41UvlSe6apAfk0fY1FbWx+NwfmpvtTg==",
"dev": true,
"license": "MIT",
"bin": {
"acorn": "bin/acorn"
@@ -1422,7 +1518,6 @@
"version": "5.3.1",
"resolved": "https://registry.npmjs.org/aria-query/-/aria-query-5.3.1.tgz",
"integrity": "sha512-Z/ZeOgVl7bcSYZ/u/rh0fOpvEpq//LZmdbkXyc7syVzjPAhfOa9ebsdTSjEBDU4vs5nC98Kfduj1uFo0qyET3g==",
"dev": true,
"license": "Apache-2.0",
"engines": {
"node": ">= 0.4"
@@ -1439,9 +1534,9 @@
}
},
"node_modules/autoprefixer": {
"version": "10.4.24",
"resolved": "https://registry.npmjs.org/autoprefixer/-/autoprefixer-10.4.24.tgz",
"integrity": "sha512-uHZg7N9ULTVbutaIsDRoUkoS8/h3bdsmVJYZ5l3wv8Cp/6UIIoRDm90hZ+BwxUj/hGBEzLxdHNSKuFpn8WOyZw==",
"version": "10.5.0",
"resolved": "https://registry.npmjs.org/autoprefixer/-/autoprefixer-10.5.0.tgz",
"integrity": "sha512-FMhOoZV4+qR6aTUALKX2rEqGG+oyATvwBt9IIzVR5rMa2HRWPkxf+P+PAJLD1I/H5/II+HuZcBJYEFBpq39ong==",
"dev": true,
"funding": [
{
@@ -1459,8 +1554,8 @@
],
"license": "MIT",
"dependencies": {
"browserslist": "^4.28.1",
"caniuse-lite": "^1.0.30001766",
"browserslist": "^4.28.2",
"caniuse-lite": "^1.0.30001787",
"fraction.js": "^5.3.4",
"picocolors": "^1.1.1",
"postcss-value-parser": "^4.2.0"
@@ -1479,20 +1574,22 @@
"version": "4.1.0",
"resolved": "https://registry.npmjs.org/axobject-query/-/axobject-query-4.1.0.tgz",
"integrity": "sha512-qIj0G9wZbMGNLjLmg1PT6v2mE9AH2zlnADJD/2tC6E00hgmhUOfEB6greHPAfLRSufHqROIUTkw6E+M3lH0PTQ==",
"dev": true,
"license": "Apache-2.0",
"engines": {
"node": ">= 0.4"
}
},
"node_modules/baseline-browser-mapping": {
"version": "2.9.19",
"resolved": "https://registry.npmjs.org/baseline-browser-mapping/-/baseline-browser-mapping-2.9.19.tgz",
"integrity": "sha512-ipDqC8FrAl/76p2SSWKSI+H9tFwm7vYqXQrItCuiVPt26Km0jS+NzSsBWAaBusvSbQcfJG+JitdMm+wZAgTYqg==",
"version": "2.10.20",
"resolved": "https://registry.npmjs.org/baseline-browser-mapping/-/baseline-browser-mapping-2.10.20.tgz",
"integrity": "sha512-1AaXxEPfXT+GvTBJFuy4yXVHWJBXa4OdbIebGN/wX5DlsIkU0+wzGnd2lOzokSk51d5LUmqjgBLRLlypLUqInQ==",
"dev": true,
"license": "Apache-2.0",
"bin": {
"baseline-browser-mapping": "dist/cli.js"
"baseline-browser-mapping": "dist/cli.cjs"
},
"engines": {
"node": ">=6.0.0"
}
},
"node_modules/binary-extensions": {
@@ -1522,9 +1619,9 @@
}
},
"node_modules/browserslist": {
"version": "4.28.1",
"resolved": "https://registry.npmjs.org/browserslist/-/browserslist-4.28.1.tgz",
"integrity": "sha512-ZC5Bd0LgJXgwGqUknZY/vkUQ04r8NXnJZ3yYi4vDmSiZmC/pdSN0NbNRPxZpbtO4uAfDUAFffO8IZoM3Gj8IkA==",
"version": "4.28.2",
"resolved": "https://registry.npmjs.org/browserslist/-/browserslist-4.28.2.tgz",
"integrity": "sha512-48xSriZYYg+8qXna9kwqjIVzuQxi+KYWp2+5nCYnYKPTr0LvD89Jqk2Or5ogxz0NUMfIjhh2lIUX/LyX9B4oIg==",
"dev": true,
"funding": [
{
@@ -1542,11 +1639,11 @@
],
"license": "MIT",
"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"
"baseline-browser-mapping": "^2.10.12",
"caniuse-lite": "^1.0.30001782",
"electron-to-chromium": "^1.5.328",
"node-releases": "^2.0.36",
"update-browserslist-db": "^1.2.3"
},
"bin": {
"browserslist": "cli.js"
@@ -1576,9 +1673,9 @@
}
},
"node_modules/caniuse-lite": {
"version": "1.0.30001770",
"resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001770.tgz",
"integrity": "sha512-x/2CLQ1jHENRbHg5PSId2sXq1CIO1CISvwWAj027ltMVG2UNgW+w9oH2+HzgEIRFembL8bUlXtfbBHR1fCg2xw==",
"version": "1.0.30001788",
"resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001788.tgz",
"integrity": "sha512-6q8HFp+lOQtcf7wBK+uEenxymVWkGKkjFpCvw5W25cmMwEDU45p1xQFBQv8JDlMMry7eNxyBaR+qxgmTUZkIRQ==",
"dev": true,
"funding": [
{
@@ -1682,7 +1779,6 @@
"version": "2.1.1",
"resolved": "https://registry.npmjs.org/clsx/-/clsx-2.1.1.tgz",
"integrity": "sha512-eYm0QWBtUrBWZWG0d386OGAw16Z995PiOVo2B7bjWSbHedGl5e0ZWaq65kOGgUSNesEIDkB9ISbTg/JK9dhCZA==",
"dev": true,
"license": "MIT",
"engines": {
"node": ">=6"
@@ -1782,7 +1878,6 @@
"version": "5.6.4",
"resolved": "https://registry.npmjs.org/devalue/-/devalue-5.6.4.tgz",
"integrity": "sha512-Gp6rDldRsFh/7XuouDbxMH3Mx8GMCcgzIb1pDTvNyn8pZGQ22u+Wa+lGV9dQCltFQ7uVw0MhRyb8XDskNFOReA==",
"dev": true,
"license": "MIT"
},
"node_modules/didyoumean": {
@@ -1800,9 +1895,9 @@
"license": "MIT"
},
"node_modules/electron-to-chromium": {
"version": "1.5.286",
"resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.5.286.tgz",
"integrity": "sha512-9tfDXhJ4RKFNerfjdCcZfufu49vg620741MNs26a9+bhLThdB+plgMeou98CAaHu/WATj2iHOOHTp1hWtABj2A==",
"version": "1.5.340",
"resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.5.340.tgz",
"integrity": "sha512-908qahOGocRMinT2nM3ajCEM99H4iPdv84eagPP3FfZy/1ZGeOy2CZYzjhms81ckOPCXPlW7LkY4XpxD8r1DrA==",
"dev": true,
"license": "ISC"
},
@@ -1881,17 +1976,23 @@
"version": "1.2.2",
"resolved": "https://registry.npmjs.org/esm-env/-/esm-env-1.2.2.tgz",
"integrity": "sha512-Epxrv+Nr/CaL4ZcFGPJIYLWFom+YeV1DqMLHJoEd9SYRxNbaFruBwfEX/kkHUJf55j2+TUbmDcmuilbP1TmXHA==",
"dev": true,
"license": "MIT"
},
"node_modules/esrap": {
"version": "2.2.2",
"resolved": "https://registry.npmjs.org/esrap/-/esrap-2.2.2.tgz",
"integrity": "sha512-zA6497ha+qKvoWIK+WM9NAh5ni17sKZKhbS5B3PoYbBvaYHZWoS33zmFybmyqpn07RLUxSmn+RCls2/XF+d0oQ==",
"dev": true,
"version": "2.2.5",
"resolved": "https://registry.npmjs.org/esrap/-/esrap-2.2.5.tgz",
"integrity": "sha512-/yLB1538mag+dn0wsePTe8C0rDIjUOaJpMs2McodSzmM2msWcZsBSdRtg6HOBt0A/r82BN+Md3pgwSc/uWt2Ig==",
"license": "MIT",
"dependencies": {
"@jridgewell/sourcemap-codec": "^1.4.15"
},
"peerDependencies": {
"@typescript-eslint/types": "^8.2.0"
},
"peerDependenciesMeta": {
"@typescript-eslint/types": {
"optional": true
}
}
},
"node_modules/estree-walker": {
@@ -2151,7 +2252,6 @@
"version": "3.0.3",
"resolved": "https://registry.npmjs.org/is-reference/-/is-reference-3.0.3.tgz",
"integrity": "sha512-ixkJoqQvAP88E6wLydLGGqCJsrFUnqoH6HnaczB8XmDH1oaWU+xxdptvikTgaEhtZ53Ky6YXiBuUI2WXLMCwjw==",
"dev": true,
"license": "MIT",
"dependencies": {
"@types/estree": "^1.0.6"
@@ -2188,9 +2288,9 @@
}
},
"node_modules/lightningcss": {
"version": "1.30.2",
"resolved": "https://registry.npmjs.org/lightningcss/-/lightningcss-1.30.2.tgz",
"integrity": "sha512-utfs7Pr5uJyyvDETitgsaqSyjCb2qNRAtuqUeWIAKztsOYdcACf2KtARYXg2pSvhkt+9NfoaNY7fxjl6nuMjIQ==",
"version": "1.32.0",
"resolved": "https://registry.npmjs.org/lightningcss/-/lightningcss-1.32.0.tgz",
"integrity": "sha512-NXYBzinNrblfraPGyrbPoD19C1h9lfI/1mzgWYvXUTe414Gz/X1FD2XBZSZM7rRTrMA8JL3OtAaGifrIKhQ5yQ==",
"license": "MPL-2.0",
"dependencies": {
"detect-libc": "^2.0.3"
@@ -2203,23 +2303,23 @@
"url": "https://opencollective.com/parcel"
},
"optionalDependencies": {
"lightningcss-android-arm64": "1.30.2",
"lightningcss-darwin-arm64": "1.30.2",
"lightningcss-darwin-x64": "1.30.2",
"lightningcss-freebsd-x64": "1.30.2",
"lightningcss-linux-arm-gnueabihf": "1.30.2",
"lightningcss-linux-arm64-gnu": "1.30.2",
"lightningcss-linux-arm64-musl": "1.30.2",
"lightningcss-linux-x64-gnu": "1.30.2",
"lightningcss-linux-x64-musl": "1.30.2",
"lightningcss-win32-arm64-msvc": "1.30.2",
"lightningcss-win32-x64-msvc": "1.30.2"
"lightningcss-android-arm64": "1.32.0",
"lightningcss-darwin-arm64": "1.32.0",
"lightningcss-darwin-x64": "1.32.0",
"lightningcss-freebsd-x64": "1.32.0",
"lightningcss-linux-arm-gnueabihf": "1.32.0",
"lightningcss-linux-arm64-gnu": "1.32.0",
"lightningcss-linux-arm64-musl": "1.32.0",
"lightningcss-linux-x64-gnu": "1.32.0",
"lightningcss-linux-x64-musl": "1.32.0",
"lightningcss-win32-arm64-msvc": "1.32.0",
"lightningcss-win32-x64-msvc": "1.32.0"
}
},
"node_modules/lightningcss-android-arm64": {
"version": "1.30.2",
"resolved": "https://registry.npmjs.org/lightningcss-android-arm64/-/lightningcss-android-arm64-1.30.2.tgz",
"integrity": "sha512-BH9sEdOCahSgmkVhBLeU7Hc9DWeZ1Eb6wNS6Da8igvUwAe0sqROHddIlvU06q3WyXVEOYDZ6ykBZQnjTbmo4+A==",
"version": "1.32.0",
"resolved": "https://registry.npmjs.org/lightningcss-android-arm64/-/lightningcss-android-arm64-1.32.0.tgz",
"integrity": "sha512-YK7/ClTt4kAK0vo6w3X+Pnm0D2cf2vPHbhOXdoNti1Ga0al1P4TBZhwjATvjNwLEBCnKvjJc2jQgHXH0NEwlAg==",
"cpu": [
"arm64"
],
@@ -2237,9 +2337,9 @@
}
},
"node_modules/lightningcss-darwin-arm64": {
"version": "1.30.2",
"resolved": "https://registry.npmjs.org/lightningcss-darwin-arm64/-/lightningcss-darwin-arm64-1.30.2.tgz",
"integrity": "sha512-ylTcDJBN3Hp21TdhRT5zBOIi73P6/W0qwvlFEk22fkdXchtNTOU4Qc37SkzV+EKYxLouZ6M4LG9NfZ1qkhhBWA==",
"version": "1.32.0",
"resolved": "https://registry.npmjs.org/lightningcss-darwin-arm64/-/lightningcss-darwin-arm64-1.32.0.tgz",
"integrity": "sha512-RzeG9Ju5bag2Bv1/lwlVJvBE3q6TtXskdZLLCyfg5pt+HLz9BqlICO7LZM7VHNTTn/5PRhHFBSjk5lc4cmscPQ==",
"cpu": [
"arm64"
],
@@ -2257,9 +2357,9 @@
}
},
"node_modules/lightningcss-darwin-x64": {
"version": "1.30.2",
"resolved": "https://registry.npmjs.org/lightningcss-darwin-x64/-/lightningcss-darwin-x64-1.30.2.tgz",
"integrity": "sha512-oBZgKchomuDYxr7ilwLcyms6BCyLn0z8J0+ZZmfpjwg9fRVZIR5/GMXd7r9RH94iDhld3UmSjBM6nXWM2TfZTQ==",
"version": "1.32.0",
"resolved": "https://registry.npmjs.org/lightningcss-darwin-x64/-/lightningcss-darwin-x64-1.32.0.tgz",
"integrity": "sha512-U+QsBp2m/s2wqpUYT/6wnlagdZbtZdndSmut/NJqlCcMLTWp5muCrID+K5UJ6jqD2BFshejCYXniPDbNh73V8w==",
"cpu": [
"x64"
],
@@ -2277,9 +2377,9 @@
}
},
"node_modules/lightningcss-freebsd-x64": {
"version": "1.30.2",
"resolved": "https://registry.npmjs.org/lightningcss-freebsd-x64/-/lightningcss-freebsd-x64-1.30.2.tgz",
"integrity": "sha512-c2bH6xTrf4BDpK8MoGG4Bd6zAMZDAXS569UxCAGcA7IKbHNMlhGQ89eRmvpIUGfKWNVdbhSbkQaWhEoMGmGslA==",
"version": "1.32.0",
"resolved": "https://registry.npmjs.org/lightningcss-freebsd-x64/-/lightningcss-freebsd-x64-1.32.0.tgz",
"integrity": "sha512-JCTigedEksZk3tHTTthnMdVfGf61Fky8Ji2E4YjUTEQX14xiy/lTzXnu1vwiZe3bYe0q+SpsSH/CTeDXK6WHig==",
"cpu": [
"x64"
],
@@ -2297,9 +2397,9 @@
}
},
"node_modules/lightningcss-linux-arm-gnueabihf": {
"version": "1.30.2",
"resolved": "https://registry.npmjs.org/lightningcss-linux-arm-gnueabihf/-/lightningcss-linux-arm-gnueabihf-1.30.2.tgz",
"integrity": "sha512-eVdpxh4wYcm0PofJIZVuYuLiqBIakQ9uFZmipf6LF/HRj5Bgm0eb3qL/mr1smyXIS1twwOxNWndd8z0E374hiA==",
"version": "1.32.0",
"resolved": "https://registry.npmjs.org/lightningcss-linux-arm-gnueabihf/-/lightningcss-linux-arm-gnueabihf-1.32.0.tgz",
"integrity": "sha512-x6rnnpRa2GL0zQOkt6rts3YDPzduLpWvwAF6EMhXFVZXD4tPrBkEFqzGowzCsIWsPjqSK+tyNEODUBXeeVHSkw==",
"cpu": [
"arm"
],
@@ -2317,12 +2417,15 @@
}
},
"node_modules/lightningcss-linux-arm64-gnu": {
"version": "1.30.2",
"resolved": "https://registry.npmjs.org/lightningcss-linux-arm64-gnu/-/lightningcss-linux-arm64-gnu-1.30.2.tgz",
"integrity": "sha512-UK65WJAbwIJbiBFXpxrbTNArtfuznvxAJw4Q2ZGlU8kPeDIWEX1dg3rn2veBVUylA2Ezg89ktszWbaQnxD/e3A==",
"version": "1.32.0",
"resolved": "https://registry.npmjs.org/lightningcss-linux-arm64-gnu/-/lightningcss-linux-arm64-gnu-1.32.0.tgz",
"integrity": "sha512-0nnMyoyOLRJXfbMOilaSRcLH3Jw5z9HDNGfT/gwCPgaDjnx0i8w7vBzFLFR1f6CMLKF8gVbebmkUN3fa/kQJpQ==",
"cpu": [
"arm64"
],
"libc": [
"glibc"
],
"license": "MPL-2.0",
"optional": true,
"os": [
@@ -2337,12 +2440,15 @@
}
},
"node_modules/lightningcss-linux-arm64-musl": {
"version": "1.30.2",
"resolved": "https://registry.npmjs.org/lightningcss-linux-arm64-musl/-/lightningcss-linux-arm64-musl-1.30.2.tgz",
"integrity": "sha512-5Vh9dGeblpTxWHpOx8iauV02popZDsCYMPIgiuw97OJ5uaDsL86cnqSFs5LZkG3ghHoX5isLgWzMs+eD1YzrnA==",
"version": "1.32.0",
"resolved": "https://registry.npmjs.org/lightningcss-linux-arm64-musl/-/lightningcss-linux-arm64-musl-1.32.0.tgz",
"integrity": "sha512-UpQkoenr4UJEzgVIYpI80lDFvRmPVg6oqboNHfoH4CQIfNA+HOrZ7Mo7KZP02dC6LjghPQJeBsvXhJod/wnIBg==",
"cpu": [
"arm64"
],
"libc": [
"musl"
],
"license": "MPL-2.0",
"optional": true,
"os": [
@@ -2357,12 +2463,15 @@
}
},
"node_modules/lightningcss-linux-x64-gnu": {
"version": "1.30.2",
"resolved": "https://registry.npmjs.org/lightningcss-linux-x64-gnu/-/lightningcss-linux-x64-gnu-1.30.2.tgz",
"integrity": "sha512-Cfd46gdmj1vQ+lR6VRTTadNHu6ALuw2pKR9lYq4FnhvgBc4zWY1EtZcAc6EffShbb1MFrIPfLDXD6Xprbnni4w==",
"version": "1.32.0",
"resolved": "https://registry.npmjs.org/lightningcss-linux-x64-gnu/-/lightningcss-linux-x64-gnu-1.32.0.tgz",
"integrity": "sha512-V7Qr52IhZmdKPVr+Vtw8o+WLsQJYCTd8loIfpDaMRWGUZfBOYEJeyJIkqGIDMZPwPx24pUMfwSxxI8phr/MbOA==",
"cpu": [
"x64"
],
"libc": [
"glibc"
],
"license": "MPL-2.0",
"optional": true,
"os": [
@@ -2377,12 +2486,15 @@
}
},
"node_modules/lightningcss-linux-x64-musl": {
"version": "1.30.2",
"resolved": "https://registry.npmjs.org/lightningcss-linux-x64-musl/-/lightningcss-linux-x64-musl-1.30.2.tgz",
"integrity": "sha512-XJaLUUFXb6/QG2lGIW6aIk6jKdtjtcffUT0NKvIqhSBY3hh9Ch+1LCeH80dR9q9LBjG3ewbDjnumefsLsP6aiA==",
"version": "1.32.0",
"resolved": "https://registry.npmjs.org/lightningcss-linux-x64-musl/-/lightningcss-linux-x64-musl-1.32.0.tgz",
"integrity": "sha512-bYcLp+Vb0awsiXg/80uCRezCYHNg1/l3mt0gzHnWV9XP1W5sKa5/TCdGWaR/zBM2PeF/HbsQv/j2URNOiVuxWg==",
"cpu": [
"x64"
],
"libc": [
"musl"
],
"license": "MPL-2.0",
"optional": true,
"os": [
@@ -2397,9 +2509,9 @@
}
},
"node_modules/lightningcss-win32-arm64-msvc": {
"version": "1.30.2",
"resolved": "https://registry.npmjs.org/lightningcss-win32-arm64-msvc/-/lightningcss-win32-arm64-msvc-1.30.2.tgz",
"integrity": "sha512-FZn+vaj7zLv//D/192WFFVA0RgHawIcHqLX9xuWiQt7P0PtdFEVaxgF9rjM/IRYHQXNnk61/H/gb2Ei+kUQ4xQ==",
"version": "1.32.0",
"resolved": "https://registry.npmjs.org/lightningcss-win32-arm64-msvc/-/lightningcss-win32-arm64-msvc-1.32.0.tgz",
"integrity": "sha512-8SbC8BR40pS6baCM8sbtYDSwEVQd4JlFTOlaD3gWGHfThTcABnNDBda6eTZeqbofalIJhFx0qKzgHJmcPTnGdw==",
"cpu": [
"arm64"
],
@@ -2417,9 +2529,9 @@
}
},
"node_modules/lightningcss-win32-x64-msvc": {
"version": "1.30.2",
"resolved": "https://registry.npmjs.org/lightningcss-win32-x64-msvc/-/lightningcss-win32-x64-msvc-1.30.2.tgz",
"integrity": "sha512-5g1yc73p+iAkid5phb4oVFMB45417DkRevRbt/El/gKXJk4jid+vPFF/AXbxn05Aky8PapwzZrdJShv5C0avjw==",
"version": "1.32.0",
"resolved": "https://registry.npmjs.org/lightningcss-win32-x64-msvc/-/lightningcss-win32-x64-msvc-1.32.0.tgz",
"integrity": "sha512-Amq9B/SoZYdDi1kFrojnoqPLxYhQ4Wo5XiL8EVJrVsB8ARoC1PWW6VGtT0WKCemjy8aC+louJnjS7U18x3b06Q==",
"cpu": [
"x64"
],
@@ -2460,7 +2572,6 @@
"version": "3.0.0",
"resolved": "https://registry.npmjs.org/locate-character/-/locate-character-3.0.0.tgz",
"integrity": "sha512-SW13ws7BjaeJ6p7Q6CO2nchbYEc3X3J6WrmTTDto7yMPqVSZTUyY5Tjbid+Ab8gLnATtygYtiDIJGQRRn2ZOiA==",
"dev": true,
"license": "MIT"
},
"node_modules/loupe": {
@@ -2554,9 +2665,9 @@
}
},
"node_modules/node-releases": {
"version": "2.0.27",
"resolved": "https://registry.npmjs.org/node-releases/-/node-releases-2.0.27.tgz",
"integrity": "sha512-nmh3lCkYZ3grZvqcCH+fjmQ7X+H0OeZgP40OierEaAptX4XofMh5kwNbWh7lBduUzCcV/8kZ+NDLCwm2iorIlA==",
"version": "2.0.37",
"resolved": "https://registry.npmjs.org/node-releases/-/node-releases-2.0.37.tgz",
"integrity": "sha512-1h5gKZCF+pO/o3Iqt5Jp7wc9rH3eJJ0+nh/CIoiRwjRxde/hAHyLPXYN4V3CqKAbiZPSeJFSWHmJsbkicta0Eg==",
"dev": true,
"license": "MIT"
},
@@ -2664,9 +2775,9 @@
}
},
"node_modules/postcss": {
"version": "8.5.6",
"resolved": "https://registry.npmjs.org/postcss/-/postcss-8.5.6.tgz",
"integrity": "sha512-3Ybi1tAuwAP9s0r1UQ2J4n5Y0G05bJkpUIO0/bI9MhwmD70S5aTWbXGBwxHrelT+XM1k6dM0pk+SwNkpTRN7Pg==",
"version": "8.5.10",
"resolved": "https://registry.npmjs.org/postcss/-/postcss-8.5.10.tgz",
"integrity": "sha512-pMMHxBOZKFU6HgAZ4eyGnwXF/EvPGGqUr0MnZ5+99485wwW41kW91A4LOGxSHhgugZmSChL5AlElNdwlNgcnLQ==",
"funding": [
{
"type": "opencollective",
@@ -3084,10 +3195,9 @@
}
},
"node_modules/svelte": {
"version": "5.53.12",
"resolved": "https://registry.npmjs.org/svelte/-/svelte-5.53.12.tgz",
"integrity": "sha512-4x/uk4rQe/d7RhfvS8wemTfNjQ0bJbKvamIzRBfTe2eHHjzBZ7PZicUQrC2ryj83xxEacfA1zHKd1ephD1tAxA==",
"dev": true,
"version": "5.55.5",
"resolved": "https://registry.npmjs.org/svelte/-/svelte-5.55.5.tgz",
"integrity": "sha512-2uCs/LZ9us+AktdzYJM8OcxQ8qnPS1kpaO7syGT/MgO+6Qr1Ybl+TqPq+97u7PHqmmMlye5ZkoyXONy5mjjAbw==",
"license": "MIT",
"dependencies": {
"@jridgewell/remapping": "^2.3.4",
@@ -3101,7 +3211,7 @@
"clsx": "^2.1.1",
"devalue": "^5.6.4",
"esm-env": "^1.2.1",
"esrap": "^2.2.2",
"esrap": "^2.2.4",
"is-reference": "^3.0.3",
"locate-character": "^3.0.0",
"magic-string": "^0.30.11",
@@ -3111,6 +3221,18 @@
"node": ">=18"
}
},
"node_modules/svelte-french-toast": {
"version": "2.0.0-alpha.0",
"resolved": "https://registry.npmjs.org/svelte-french-toast/-/svelte-french-toast-2.0.0-alpha.0.tgz",
"integrity": "sha512-81wcVaY9UZ/0JuzLEizMSoIXqNbX7yhfTZavBuw94T3cnT2HmJ9O+qXY/c91h9FkeMwboo0KHZVmzOEQVTXDFg==",
"license": "MIT",
"dependencies": {
"svelte-writable-derived": "^3.1.1"
},
"peerDependencies": {
"svelte": "^5.0.0"
}
},
"node_modules/svelte-spa-router": {
"version": "4.0.2",
"resolved": "https://registry.npmjs.org/svelte-spa-router/-/svelte-spa-router-4.0.2.tgz",
@@ -3123,6 +3245,18 @@
"url": "https://github.com/sponsors/ItalyPaleAle"
}
},
"node_modules/svelte-writable-derived": {
"version": "3.1.1",
"resolved": "https://registry.npmjs.org/svelte-writable-derived/-/svelte-writable-derived-3.1.1.tgz",
"integrity": "sha512-w4LR6/bYZEuCs7SGr+M54oipk/UQKtiMadyOhW0PTwAtJ/Ai12QS77sLngEcfBx2q4H8ZBQucc9ktSA5sUGZWw==",
"license": "MIT",
"funding": {
"url": "https://ko-fi.com/pixievoltno1"
},
"peerDependencies": {
"svelte": "^3.2.1 || ^4.0.0-next.1 || ^5.0.0-next.94"
}
},
"node_modules/tailwindcss": {
"version": "3.4.19",
"resolved": "https://registry.npmjs.org/tailwindcss/-/tailwindcss-3.4.19.tgz",
@@ -3326,9 +3460,9 @@
"license": "MIT"
},
"node_modules/vite": {
"version": "7.3.1",
"resolved": "https://registry.npmjs.org/vite/-/vite-7.3.1.tgz",
"integrity": "sha512-w+N7Hifpc3gRjZ63vYBXA56dvvRlNWRczTdmCBBa+CotUzAPf5b7YMdMR/8CQoeYE5LX3W4wj6RYTgonm1b9DA==",
"version": "7.3.2",
"resolved": "https://registry.npmjs.org/vite/-/vite-7.3.2.tgz",
"integrity": "sha512-Bby3NOsna2jsjfLVOHKes8sGwgl4TT0E6vvpYgnAYDIF/tie7MRaFthmKuHx1NSXjiTueXH3do80FMQgvEktRg==",
"license": "MIT",
"dependencies": {
"esbuild": "^0.27.0",
@@ -3551,7 +3685,6 @@
"version": "1.1.4",
"resolved": "https://registry.npmjs.org/zimmerframe/-/zimmerframe-1.1.4.tgz",
"integrity": "sha512-B58NGBEoc8Y9MWWCQGl/gq9xBCe4IiKM0a2x7GZdQKOW5Exr8S1W24J6OgM1njK8xCRGvAJIL/MxXHf6SkmQKQ==",
"dev": true,
"license": "MIT"
}
}
+2
View File
@@ -9,6 +9,7 @@
},
"devDependencies": {
"@sveltejs/vite-plugin-svelte": "^6.1.1",
"@tailwindcss/typography": "^0.5.19",
"autoprefixer": "^10.4.16",
"postcss": "^8.4.31",
"svelte": "^5.53.5",
@@ -22,6 +23,7 @@
"@picocss/pico": "^2.1.1",
"@tailwindcss/vite": "^4.1.18",
"gsap": "^3.13.0",
"svelte-french-toast": "^2.0.0-alpha.0",
"svelte-spa-router": "^4.0.2"
}
}
+5 -4
View File
@@ -3,13 +3,13 @@
import Footer from './components/footer.svelte';
import Index from './routes/index.svelte';
import Captive from './routes/captive.svelte';
import Router, { location } from 'svelte-spa-router';
import Router from 'svelte-spa-router';
import { lang } from './i18n/store';
import { Toaster } from 'svelte-french-toast';
const routes = {
'/': Index,
'/control': Index,
'/config': Index,
'/captive': Captive,
// Fallback route
'*': Index
};
@@ -19,7 +19,8 @@
});
</script>
<div class="container mx-auto lg:max-w-2xl">
<Toaster/>
<div class="container mx-auto">
<Header />
<main class="py-8">
@@ -7,4 +7,4 @@
<LedConfiguration />
<SchemaConfiguration />
</div>
</div>
@@ -1,14 +1,118 @@
<script lang="ts">
import { onMount } from 'svelte';
import Card from '../common/card.svelte';
import Button from '../common/button.svelte';
import { t } from '../../i18n/store';
import SegmentRow from './segmentRow.svelte';
import { segmentStore, type Segment } from '../../stores/configSegmentStore';
import toast from 'svelte-french-toast';
let segments = $state<Segment[]>([]);
let nextId = $state(0);
onMount(() => {
const unsub = segmentStore.subscribe((data) => {
segments = data.map((s) => ({ ...s }));
nextId = data.length;
});
segmentStore.fetchSegments();
return unsub;
});
function addSegment() {
const id = `new-${nextId++}`;
segments = [...segments, { id, name: $t('wled.segment.name').replace('{num}', String(nextId)), start: 0, leds: 1 }];
}
function findOverlap(): [string, string] | null {
const sorted = [...segments].sort((a, b) => a.start - b.start);
for (let i = 0; i < sorted.length - 1; i++) {
const a = sorted[i];
const b = sorted[i + 1];
if (a.start + a.leds > b.start) {
return [a.name || `#${i + 1}`, b.name || `#${i + 2}`];
}
}
return null;
}
async function saveSegments() {
const overlap = findOverlap();
if (overlap) {
toast.error(`"${overlap[0]}" & "${overlap[1]}" überschneiden sich`);
return;
}
try {
await segmentStore.updateSegments(segments);
toast.success($t('wled.saved'));
} catch {
toast.error($t('wled.error.save'));
}
}
</script>
<Card title="wled.config.title">
<p class="text-sm -mt-3 mb-4 text-text-muted">{$t("wled.config.desc")}</p>
<p class="text-sm -mt-3 mb-4 text-text-muted">{$t('wled.config.desc')}</p>
<div class="flex justify-center items-center">
<div class="flex-1">Segmente</div>
<Button label={$t("wled.segment.add")} ariaLabel={$t("wled.segment.add")}></Button>
<div class="flex justify-center items-center mb-2">
<div class="flex-1 text-sm font-medium">{$t('wled.segments.title')}</div>
<Button label={$t('wled.segment.add')} ariaLabel={$t('wled.segment.add')} icon="" onClick={addSegment} />
</div>
<div class="overflow-y-auto max-h-64 scrollbar">
<table class="w-full table-fixed border-collapse text-left">
<colgroup>
<col />
<col class="w-24" />
<col class="w-24" />
<col class="w-12" />
</colgroup>
<thead class="sticky top-0 z-10 bg-card">
<tr class="border-b-2 border-border">
<th class="px-3 py-2 text-sm">{$t('wled.segment.name').replace('{num}', '')}</th>
<th class="px-3 py-2 text-sm text-center">{$t('wled.segment.start')}</th>
<th class="px-3 py-2 text-sm text-center">{$t('wled.segment.leds')}</th>
<th class="w-12"></th>
</tr>
</thead>
<tbody>
{#each segments as segment (segment.id)}
<SegmentRow
bind:name={segment.name}
bind:start={segment.start}
bind:leds={segment.leds}
onDelete={() => { segments = segments.filter((s) => s.id !== segment.id); }}
/>
{/each}
{#if segments.length === 0}
<tr>
<td colspan="3" class="px-3 py-4 text-sm text-center text-text-muted">
<div>{$t('wled.segments.empty.title')}</div>
<div class="text-xs mt-1">{$t('wled.segments.empty.hint')}</div>
</td>
</tr>
{/if}
</tbody>
</table>
</div>
<div class="flex justify-end mt-3">
<Button label={$t('btn.save')} ariaLabel={$t('btn.save')} icon="💾" onClick={saveSegments} />
</div>
</Card>
<style>
.scrollbar {
scrollbar-color: var(--primary) var(--border);
scrollbar-width: thin;
}
.scrollbar::-webkit-scrollbar {
width: 6px;
}
.scrollbar::-webkit-scrollbar-track {
background: var(--border);
border-radius: 3px;
}
.scrollbar::-webkit-scrollbar-thumb {
background: var(--primary);
border-radius: 3px;
}
</style>
@@ -1,7 +1,143 @@
<script lang="ts">
import Card from '../common/card.svelte';
import { t } from '../../i18n/store';
import DropDown from '../common/dropDown.svelte';
import Button from '../common/button.svelte';
import toast from 'svelte-french-toast';
import SchemaRow from './schemaRow.svelte';
import { schemaStore, type SchemaRow as SchemaRowData } from '../../stores/configSchemaStore';
import { controlStore } from '../../stores/controlStore';
import { onMount } from 'svelte';
const schemas = [
{ value: 'schema_01.csv', label: $t('schema.name.1') },
{ value: 'schema_02.csv', label: $t('schema.name.2') },
{ value: 'schema_03.csv', label: $t('schema.name.3') }
];
let activeSchema = $state($controlStore.schema ?? 'schema_01.csv');
let activeLabel = $derived(schemas.find((s) => s.value === activeSchema)?.label ?? activeSchema);
let userSelected = $state(false);
let currentSchema = $derived($controlStore.schema);
$effect(() => {
if (currentSchema && !userSelected) {
activeSchema = currentSchema;
schemaStore.fetchSchema(currentSchema).catch(() => {});
}
});
let rows = $state<SchemaRowData[]>([]);
onMount(() => {
return schemaStore.subscribe((data) => {
rows = data.map((r) => ({ ...r }));
});
});
function rowToTime(index: number): string {
const total = index * 30;
const h = Math.floor(total / 60);
const m = total % 60;
return `${h < 10 ? '0' : ''}${h}:${m < 10 ? '0' : ''}${m}`;
}
async function loadClick() {
try {
await schemaStore.fetchSchema(activeSchema);
toast.success($t('schema.loaded').replace('{file}', activeLabel));
} catch {
toast.error($t('schema.demo'));
}
}
async function saveClick() {
try {
await schemaStore.saveSchema(activeSchema, rows);
toast.success($t('schema.saved').replace('{file}', activeLabel));
} catch {
toast.error($t('error'));
}
}
</script>
<Card title="schema.editor.title">
<div class="space-y-2">
<div class="flex items-center justify-center cursor-pointer space-x-2">
<DropDown
id="active-schema"
options={schemas}
bind:value={activeSchema}
onchange={() => (userSelected = true)}
/>
<Button
label={$t('btn.load')}
ariaLabel={$t('btn.load')}
onClick={loadClick}
icon="🔄" />
<Button
label={$t('btn.save')}
ariaLabel={$t('btn.save')}
onClick={saveClick}
icon="💾" />
</div>
<div class="overflow-auto max-h-80 scrollbar">
<table class="w-full table-fixed border-collapse text-left">
<colgroup>
<col class="w-14" />
<col class="w-14" />
<col />
<col />
<col />
<col />
</colgroup>
<thead class="sticky top-0 z-10 bg-card">
<tr class="border-b-2 border-border">
<th class="px-3 py-2 text-sm">{$t('schema.header.time')}</th>
<th class="px-3 py-2 text-sm text-center">{$t('schema.header.color')}</th>
<th class="px-3 py-2 text-sm text-center">{$t('schema.header.red')}</th>
<th class="px-3 py-2 text-sm text-center">{$t('schema.header.green')}</th>
<th class="px-3 py-2 text-sm text-center">{$t('schema.header.blue')}</th>
<th class="px-3 py-2 text-sm text-center">{$t('schema.header.brightness')}</th>
</tr>
</thead>
<tbody>
{#each rows as row, i}
<SchemaRow
time={rowToTime(i)}
bind:r={row.r}
bind:g={row.g}
bind:b={row.b}
bind:brightness={row.brightness}
/>
{/each}
{#if rows.length === 0}
<tr>
<td colspan="6" class="px-3 py-4 text-sm text-center text-text-muted">
{$t('schema.loading')}
</td>
</tr>
{/if}
</tbody>
</table>
</div>
</div>
</Card>
<style>
.scrollbar {
scrollbar-color: var(--primary) var(--border);
scrollbar-width: thin;
}
.scrollbar::-webkit-scrollbar {
width: 6px;
}
.scrollbar::-webkit-scrollbar-track {
background: var(--border);
border-radius: 3px;
}
.scrollbar::-webkit-scrollbar-thumb {
background: var(--primary);
border-radius: 3px;
}
</style>
@@ -0,0 +1,65 @@
<script lang="ts">
interface Props {
time: string;
r: number;
g: number;
b: number;
brightness: number;
}
let { time, r = $bindable(), g = $bindable(), b = $bindable(), brightness = $bindable() }: Props = $props();
function clamp(v: number): number {
return Math.min(255, Math.max(0, Math.round(v)));
}
</script>
<tr class="group">
<td class="px-3 py-2 text-sm bg-bg-secondary group-hover:bg-accent border-y border-l border-border">{time}</td>
<td class="px-2 py-2 text-sm bg-bg-secondary group-hover:bg-accent border-y border-border">
<div class="h-6 rounded" style="background: rgb({r},{g},{b});"></div>
</td>
<td class="px-3 py-2 text-sm text-center bg-bg-secondary group-hover:bg-accent border-y border-border">
<input
type="number" min="0" max="255"
bind:value={r}
oninput={() => r = clamp(r)}
class="w-14 px-1 py-0.5 text-sm text-center rounded border border-border bg-input focus:outline-none focus:border-primary no-spinner"
/>
</td>
<td class="px-3 py-2 text-sm text-center bg-bg-secondary group-hover:bg-accent border-y border-border">
<input
type="number" min="0" max="255"
bind:value={g}
oninput={() => g = clamp(g)}
class="w-14 px-1 py-0.5 text-sm text-center rounded border border-border bg-input focus:outline-none focus:border-primary no-spinner"
/>
</td>
<td class="px-3 py-2 text-sm text-center bg-bg-secondary group-hover:bg-accent border-y border-border">
<input
type="number" min="0" max="255"
bind:value={b}
oninput={() => b = clamp(b)}
class="w-14 px-1 py-0.5 text-sm text-center rounded border border-border bg-input focus:outline-none focus:border-primary no-spinner"
/>
</td>
<td class="px-3 py-2 text-sm text-center bg-bg-secondary group-hover:bg-accent border-y border-r border-border">
<input
type="number" min="0" max="255"
bind:value={brightness}
oninput={() => brightness = clamp(brightness)}
class="w-14 px-1 py-0.5 text-sm text-center rounded border border-border bg-input focus:outline-none focus:border-primary no-spinner"
/>
</td>
</tr>
<style>
:global(input.no-spinner::-webkit-inner-spin-button),
:global(input.no-spinner::-webkit-outer-spin-button) {
-webkit-appearance: none;
margin: 0;
}
:global(input.no-spinner) {
-moz-appearance: textfield;
}
</style>
@@ -0,0 +1,54 @@
<script lang="ts">
interface Props {
name: string;
start: number;
leds: number;
onDelete: () => void;
}
let { name = $bindable(), start = $bindable(), leds = $bindable(), onDelete }: Props = $props();
</script>
<tr class="group">
<td class="px-3 py-2 text-sm bg-bg-secondary group-hover:bg-accent border-y border-l border-border">
<input
type="text"
bind:value={name}
class="w-full px-1 py-0.5 text-sm rounded border border-border bg-input focus:outline-none focus:border-primary"
/>
</td>
<td class="px-3 py-2 text-sm text-center bg-bg-secondary group-hover:bg-accent border-y border-border">
<input
type="number"
min="0"
bind:value={start}
class="w-16 px-1 py-0.5 text-sm text-right rounded border border-border bg-input focus:outline-none focus:border-primary no-spinner"
/>
</td>
<td class="px-3 py-2 text-sm text-center bg-bg-secondary group-hover:bg-accent border-y border-border">
<input
type="number"
min="1"
bind:value={leds}
class="w-16 px-1 py-0.5 text-sm text-right rounded border border-border bg-input focus:outline-none focus:border-primary no-spinner"
/>
</td>
<td class="px-1 py-2 text-sm text-center bg-bg-secondary group-hover:bg-accent border-y border-r border-border">
<button
onclick={onDelete}
aria-label="delete"
class="flex items-center justify-center cursor-pointer px-1 py-1 bg-card rounded-lg text-text-muted text-sm transition-all border-solid border-border border-x border-y hover:border-primary"
>🗑️</button>
</td>
</tr>
<style>
:global(input.no-spinner::-webkit-inner-spin-button),
:global(input.no-spinner::-webkit-outer-spin-button) {
-webkit-appearance: none;
margin: 0;
}
:global(input.no-spinner) {
-moz-appearance: textfield;
}
</style>
@@ -31,9 +31,9 @@
<footer class="px-0 py-4">
<hr class="border-0 mb-4 border-t-2 border-solid" />
<div class="flex justify-center items-center relative">
<div class="flex justify-between items-center">
<p class="m-0">© {romanYear} by mars3142</p>
<p class="absolute right-0 m-0 text-sm text-gray-500">
<p class="m-0 text-sm text-gray-500">
v{__APP_VERSION__} ({__COMMIT_HASH__})
</p>
</div>
+37 -19
View File
@@ -31,11 +31,13 @@
},
"segments": {
"title": "Segmente",
"empty": "Keine Segmente konfiguriert",
"empty.hint": "Klicke auf \"Segment hinzufügen\" um ein Segment zu erstellen"
"empty": {
"title": "Keine Segmente konfiguriert",
"hint": "Klicke auf \"Segment hinzufügen\" um ein Segment zu erstellen"
}
},
"segment": {
"add": " Segment hinzufügen",
"add": "Segment hinzufügen",
"name": "Segment {num}",
"leds": "Anzahl LEDs",
"start": "Start-LED",
@@ -86,7 +88,11 @@
"loading": "Schema wird geladen...",
"header": {
"time": "Zeit",
"color": "Farbe"
"color": "Farbe",
"red": "R",
"green": "G",
"blue": "B",
"brightness": "Helligkeit"
},
"loaded": "{file} erfolgreich geladen",
"saved": "{file} erfolgreich gespeichert!",
@@ -94,15 +100,19 @@
},
"scenes": {
"title": "Szenen",
"empty": "Keine Szenen definiert",
"empty.hint": "Erstelle Szenen unter Konfiguration",
"empty": {
"title": "Keine Szenen definiert",
"hint": "Erstelle Szenen unter Konfiguration"
},
"manage": {
"title": "Szenen verwalten",
"desc": "Erstelle und bearbeite Szenen für schnellen Zugriff"
},
"config": {
"empty": "Keine Szenen erstellt",
"empty.hint": "Klicke auf \"Neue Szene\" um eine Szene zu erstellen"
"empty": {
"title": "Keine Szenen erstellt",
"hint": "Klicke auf \"Neue Szene\" um eine Szene zu erstellen"
}
},
"activated": "\"{name}\" aktiviert",
"created": "Szene erstellt",
@@ -118,8 +128,10 @@
"devices": {
"external": "Externe Geräte",
"control": {
"empty": "Keine Geräte hinzugefügt",
"empty.hint": "Füge Geräte unter Konfiguration hinzu"
"empty": {
"title": "Keine Geräte hinzugefügt",
"hint": "Füge Geräte unter Konfiguration hinzu"
}
},
"new": {
"title": "Neue Geräte",
@@ -127,8 +139,10 @@
},
"searching": "Suche nach Geräten...",
"unpaired": {
"empty": "Keine neuen Geräte gefunden",
"empty.hint": "Drücke \"Geräte suchen\" um nach Matter-Geräten zu suchen"
"empty": {
"title": "Keine neuen Geräte gefunden",
"hint": "Drücke \"Geräte suchen\" um nach Matter-Geräten zu suchen"
}
},
"paired": {
"title": "Zugeordnete Geräte",
@@ -152,11 +166,15 @@
"config": {
"title": "WLAN Konfiguration"
},
"ssid": "WLAN Name (SSID)",
"ssid.placeholder": "Netzwerkname eingeben",
"password": "WLAN Passwort",
"password.short": "Passwort",
"password.placeholder": "Passwort eingeben",
"ssid": {
"title": "WLAN Name (SSID)",
"placeholder": "Netzwerkname eingeben"
},
"password": {
"title": "WLAN Passwort",
"short": "Passwort",
"placeholder": "Passwort eingeben"
},
"available": "Verfügbare Netzwerke",
"scan": {
"hint": "Nach Netzwerken suchen...",
@@ -212,8 +230,8 @@
},
"btn": {
"scan": "🔍 Suchen",
"save": "💾 Speichern",
"load": "🔄 Laden",
"save": "Speichern",
"load": "Laden",
"cancel": "Abbrechen",
"apply": "Übernehmen",
"new": {
+37 -19
View File
@@ -31,11 +31,13 @@
},
"segments": {
"title": "Segments",
"empty": "No segments configured",
"empty.hint": "Click \"Add Segment\" to create a segment"
"empty": {
"title": "No segments configured",
"hint": "Click \"Add Segment\" to create a segment"
}
},
"segment": {
"add": " Add Segment",
"add": "Add Segment",
"name": "Segment {num}",
"leds": "Number of LEDs",
"start": "Start LED",
@@ -86,7 +88,11 @@
"loading": "Loading schema...",
"header": {
"time": "Time",
"color": "Color"
"color": "Color",
"red": "R",
"green": "G",
"blue": "B",
"brightness": "Brightness"
},
"loaded": "{file} loaded successfully",
"saved": "{file} saved successfully!",
@@ -94,15 +100,19 @@
},
"scenes": {
"title": "Scenes",
"empty": "No scenes defined",
"empty.hint": "Create scenes in settings",
"empty": {
"title": "No scenes defined",
"hint": "Create scenes in settings"
},
"manage": {
"title": "Manage Scenes",
"desc": "Create and edit scenes for quick access"
},
"config": {
"empty": "No scenes created",
"empty.hint": "Click \"New Scene\" to create a scene"
"empty": {
"title": "No scenes created",
"hint": "Click \"New Scene\" to create a scene"
}
},
"activated": "\"{name}\" activated",
"created": "Scene created",
@@ -118,8 +128,10 @@
"devices": {
"external": "External Devices",
"control": {
"empty": "No devices added",
"empty.hint": "Add devices in settings"
"empty": {
"title": "No devices added",
"hint": "Add devices in settings"
}
},
"new": {
"title": "New Devices",
@@ -127,8 +139,10 @@
},
"searching": "Searching for devices...",
"unpaired": {
"empty": "No new devices found",
"empty.hint": "Press \"Scan devices\" to search for Matter devices"
"empty": {
"title": "No new devices found",
"hint": "Press \"Scan devices\" to search for Matter devices"
}
},
"paired": {
"title": "Paired Devices",
@@ -152,11 +166,15 @@
"config": {
"title": "WiFi Configuration"
},
"ssid": "WiFi Name (SSID)",
"ssid.placeholder": "Enter network name",
"password": "WiFi Password",
"password.short": "Password",
"password.placeholder": "Enter password",
"ssid": {
"title": "WiFi Name (SSID)",
"placeholder": "Enter network name"
},
"password": {
"title": "WiFi Password",
"short": "Password",
"placeholder": "Enter password"
},
"available": "Available Networks",
"scan": {
"hint": "Search for networks...",
@@ -212,8 +230,8 @@
},
"btn": {
"scan": "🔍 Scan",
"save": "💾 Save",
"load": "🔄 Load",
"save": "Save",
"load": "Load",
"cancel": "Cancel",
"apply": "Apply",
"new": {
+131
View File
@@ -0,0 +1,131 @@
<script lang="ts">
import { t } from '../i18n/store';
import Card from '../components/common/card.svelte';
let ssid = $state('');
let password = $state('');
let showPassword = $state(false);
let statusMessage = $state('');
let statusType = $state<'success' | 'error' | 'info' | ''>('');
let countdownInterval: ReturnType<typeof setInterval> | null = null;
let canConnect = $derived(ssid.trim().length > 0 && password.length > 0);
function togglePassword() {
showPassword = !showPassword;
}
function showStatus(message: string, type: 'success' | 'error' | 'info') {
statusMessage = message;
statusType = type;
if (type !== 'info') {
setTimeout(() => {
statusMessage = '';
statusType = '';
}, 5000);
}
}
async function saveWifi() {
if (!ssid.trim()) {
showStatus($t('wifi.error.ssid'), 'error');
return;
}
showStatus($t('common.loading'), 'info');
try {
const response = await fetch('/api/wifi/config', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ ssid: ssid.trim(), password })
});
if (response.ok) {
showStatus($t('wifi.saved'), 'success');
let countdown = 10;
if (countdownInterval) clearInterval(countdownInterval);
countdownInterval = setInterval(() => {
const text = $t('captive.connecting').replace('{seconds}', String(countdown));
showStatus(text, 'success');
countdown--;
if (countdown < 0) {
if (countdownInterval) clearInterval(countdownInterval);
showStatus($t('captive.done'), 'success');
}
}, 1000);
} else {
const errorData = await response.json().catch(() => ({}));
showStatus($t('error') + ': ' + (errorData.error || $t('wifi.error.save')), 'error');
}
} catch (err: unknown) {
const message = err instanceof Error ? err.message : String(err);
showStatus($t('error') + ': ' + message, 'error');
}
}
</script>
<Card title="captive.subtitle">
<div class="mb-4">
<label for="ssid" class="block mb-2 text-sm font-medium text-text-muted">
{$t('wifi.ssid.title')}
</label>
<input
type="text"
id="ssid"
bind:value={ssid}
placeholder={$t('wifi.ssid.placeholder')}
class="w-full px-4 py-3 border-2 border-border rounded-lg bg-input text-text text-base focus:outline-none focus:border-success transition-colors"
/>
</div>
<div class="mb-4">
<label for="password" class="block mb-2 text-sm font-medium text-text-muted">
{$t('wifi.password.title')}
</label>
<div class="relative flex items-center">
<input
type={showPassword ? 'text' : 'password'}
id="password"
bind:value={password}
placeholder={$t('wifi.password.placeholder')}
class="w-full px-4 py-3 pr-12 border-2 border-border rounded-lg bg-input text-text text-base focus:outline-none focus:border-success transition-colors"
/>
<button
type="button"
onclick={togglePassword}
aria-label="Toggle password visibility"
class="absolute right-3 text-xl text-text-muted cursor-pointer bg-transparent border-none p-1 transition-colors hover:text-text"
>
{showPassword ? '🙈' : '👁️'}
</button>
</div>
</div>
<div class="mt-5">
<button
onclick={saveWifi}
disabled={!canConnect}
class="w-full py-3 px-5 rounded-lg text-base font-semibold cursor-pointer transition-all flex items-center justify-center gap-2 min-h-12 touch-manipulation bg-success text-white hover:opacity-90 active:scale-[0.98] disabled:opacity-50 disabled:cursor-not-allowed disabled:bg-gray-400"
>
{$t('captive.connect')}
</button>
</div>
{#if statusMessage}
<div
class="mt-3 text-center rounded-lg px-4 py-3 text-sm border {statusType === 'success'
? 'bg-success/15 border-success text-success'
: statusType === 'error'
? 'bg-error/15 border-error text-error'
: 'bg-accent border-accent/50 text-text'}"
>
{statusMessage}
</div>
{/if}
<div class="bg-accent rounded-lg px-4 py-3 mt-5 text-sm text-text-muted">
<strong class="text-text">{$t('captive.note.title')}</strong>
{$t('captive.note.text')}
</div>
</Card>
@@ -0,0 +1,57 @@
import { writable } from 'svelte/store';
import { baseUrl } from '../utils/apiClient';
import { createLogger } from '../utils/logger';
const log = createLogger('configSchemaStore');
export interface SchemaRow {
r: number;
g: number;
b: number;
w: number;
brightness: number;
saturation: number;
}
function parseCSV(csv: string): SchemaRow[] {
return csv
.trim()
.split('\n')
.filter((line) => line.trim().length > 0)
.map((line) => {
const [r, g, b, w, brightness, saturation] = line.split(',').map(Number);
return { r, g, b, w, brightness, saturation };
});
}
function toCSV(rows: SchemaRow[]): string {
return rows.map((row) => `${row.r},${row.g},${row.b},${row.w},${row.brightness},${row.saturation}`).join('\n');
}
function createSchemaStore() {
const { subscribe, set } = writable<SchemaRow[]>([]);
async function fetchSchema(filename: string): Promise<void> {
log.debug('Loading schema', { filename });
const res = await fetch(`${baseUrl}/api/schema/${filename}`);
if (!res.ok) throw new Error(`Failed to load schema: ${res.status}`);
const text = await res.text();
set(parseCSV(text));
log.debug('Schema loaded', { filename });
}
async function saveSchema(filename: string, rows: SchemaRow[]): Promise<void> {
log.debug('Saving schema', { filename });
const res = await fetch(`${baseUrl}/api/schema/${filename}`, {
method: 'POST',
headers: { 'Content-Type': 'text/csv' },
body: toCSV(rows)
});
if (!res.ok) throw new Error(`Failed to save schema: ${res.status}`);
log.debug('Schema saved', { filename });
}
return { subscribe, fetchSchema, saveSchema };
}
export const schemaStore = createSchemaStore();
+4 -1
View File
@@ -1,5 +1,8 @@
import { vitePreprocess } from '@sveltejs/vite-plugin-svelte';
export default {
preprocess: vitePreprocess()
preprocess: vitePreprocess(),
vitePlugin: {
inspector: true
}
};
+3
View File
@@ -0,0 +1,3 @@
{
"$schema": "https://docs.renovatebot.com/renovate-schema.json"
}