From 26ebfad3a21a7c94e07a3c41cc64cb63e8aa55dd Mon Sep 17 00:00:00 2001 From: Gabriel Kaszewski Date: Fri, 19 Jun 2026 00:12:42 +0200 Subject: [PATCH] add SPA config UI, wire media/rss adapters, event-driven layout push - React SPA: dashboard, data sources CRUD, widgets CRUD, layout builder, presets. TanStack Router + Query, shadcn/ui, Vite proxy to :3000 - wire media + rss adapters into polling loop, remove xtb source type - media adapter: read username/password from headers, proper subsonic auth - event handler: subscribe to LayoutChanged, push screen update to clients - fix clippy warnings across workspace (Default impls, collapsible ifs, redundant closures, is_none_or, unused imports) --- Cargo.lock | 10 + Cargo.toml | 2 + crates/adapters/config-memory/src/lib.rs | 89 +- crates/adapters/config-sqlite/src/lib.rs | 26 +- .../src/repository/data_sources.rs | 23 +- .../config-sqlite/src/repository/layout.rs | 16 +- .../config-sqlite/src/repository/mod.rs | 12 +- .../config-sqlite/src/repository/presets.rs | 35 +- .../config-sqlite/src/repository/widgets.rs | 14 +- .../src/serialization/data_source.rs | 33 +- .../config-sqlite/src/serialization/layout.rs | 66 +- .../config-sqlite/src/serialization/mod.rs | 2 +- .../config-sqlite/src/serialization/preset.rs | 6 +- .../config-sqlite/src/serialization/widget.rs | 46 +- .../config-sqlite/tests/config_store_tests.rs | 73 +- crates/adapters/display-terminal/src/lib.rs | 24 +- crates/adapters/http-api/src/lib.rs | 4 +- .../http-api/src/routes/data_sources.rs | 93 +- crates/adapters/http-api/src/routes/layout.rs | 41 +- crates/adapters/http-api/src/routes/mod.rs | 48 +- .../adapters/http-api/src/routes/presets.rs | 84 +- .../adapters/http-api/src/routes/widgets.rs | 89 +- crates/adapters/http-api/tests/api_tests.rs | 100 +- crates/adapters/http-json/src/lib.rs | 22 +- .../http-json/tests/http_json_tests.rs | 31 +- crates/adapters/media/Cargo.toml | 2 + crates/adapters/media/src/error.rs | 2 + crates/adapters/media/src/lib.rs | 61 +- crates/adapters/media/tests/media_tests.rs | 27 +- crates/adapters/rss/src/lib.rs | 17 +- crates/adapters/rss/src/parser.rs | 16 +- crates/adapters/rss/tests/parser_tests.rs | 22 +- crates/adapters/tcp-client/src/lib.rs | 15 +- crates/adapters/tcp-server/src/broadcaster.rs | 29 +- crates/adapters/tcp-server/src/event_bus.rs | 4 +- crates/adapters/tcp-server/src/lib.rs | 4 +- crates/adapters/tcp-server/src/server.rs | 10 +- crates/api-types/src/data_source.rs | 11 +- crates/api-types/src/layout.rs | 61 +- crates/api-types/src/lib.rs | 8 +- crates/api-types/src/preset.rs | 4 +- crates/api-types/src/widget.rs | 33 +- crates/application/src/config_service.rs | 165 ++- crates/application/src/data_projection.rs | 16 +- .../application/tests/config_service_tests.rs | 47 +- .../tests/data_projection_tests.rs | 33 +- crates/application/tests/support/mod.rs | 32 +- crates/bootstrap/Cargo.toml | 2 + crates/bootstrap/src/config.rs | 6 +- crates/bootstrap/src/event_handler.rs | 53 + crates/bootstrap/src/main.rs | 23 +- crates/bootstrap/src/polling.rs | 76 +- crates/client-application/src/client_app.rs | 25 +- .../tests/client_app_tests.rs | 32 +- crates/client-desktop/src/main.rs | 33 +- crates/client-domain/src/bounding_box.rs | 14 +- crates/client-domain/src/layout_engine.rs | 46 +- crates/client-domain/src/lib.rs | 4 +- crates/client-domain/src/ports/display.rs | 8 +- crates/client-domain/src/ports/mod.rs | 2 +- crates/client-domain/src/render_tree.rs | 4 +- .../tests/layout_engine_tests.rs | 99 +- .../client-domain/tests/render_tree_tests.rs | 48 +- crates/domain/src/entities/data_source.rs | 1 - crates/domain/src/entities/mod.rs | 8 +- crates/domain/src/entities/widget_config.rs | 5 +- crates/domain/src/lib.rs | 18 +- crates/domain/src/ports/broadcast.rs | 2 +- crates/domain/src/ports/config_repository.rs | 46 +- crates/domain/src/ports/data_source_port.rs | 2 +- crates/domain/src/ports/event.rs | 2 +- crates/domain/src/ports/mod.rs | 4 +- crates/domain/src/value_objects/layout.rs | 6 +- crates/domain/src/value_objects/mod.rs | 10 +- crates/domain/src/value_objects/value.rs | 5 +- .../domain/src/value_objects/widget_state.rs | 2 +- crates/domain/tests/data_source_tests.rs | 14 +- crates/domain/tests/key_mapping_tests.rs | 15 +- crates/domain/tests/layout_tests.rs | 18 +- crates/domain/tests/value_tests.rs | 33 +- crates/domain/tests/widget_tests.rs | 76 +- crates/protocol/src/frame.rs | 4 +- crates/protocol/src/lib.rs | 16 +- crates/protocol/src/wire.rs | 50 +- crates/protocol/tests/conversion_tests.rs | 40 +- crates/protocol/tests/round_trip_tests.rs | 56 +- docs/k-frame-spa-handoff.md | 147 ++ spa/.gitignore | 24 + spa/.prettierignore | 7 + spa/.prettierrc | 11 + spa/README.md | 21 + spa/bun.lock | 1292 +++++++++++++++++ spa/components.json | 25 + spa/eslint.config.js | 22 + spa/index.html | 13 + spa/package.json | 57 + spa/public/vite.svg | 1 + spa/src/api/client.ts | 28 + spa/src/api/data-sources.ts | 50 + spa/src/api/layout.ts | 22 + spa/src/api/presets.ts | 39 + spa/src/api/types.ts | 59 + spa/src/api/widgets.ts | 49 + spa/src/assets/react.svg | 1 + spa/src/components/app-shell.tsx | 78 + spa/src/components/theme-provider.tsx | 230 +++ spa/src/components/ui/accordion.tsx | 79 + spa/src/components/ui/alert-dialog.tsx | 197 +++ spa/src/components/ui/alert.tsx | 76 + spa/src/components/ui/aspect-ratio.tsx | 11 + spa/src/components/ui/avatar.tsx | 110 ++ spa/src/components/ui/badge.tsx | 49 + spa/src/components/ui/breadcrumb.tsx | 122 ++ spa/src/components/ui/button-group.tsx | 83 ++ spa/src/components/ui/button.tsx | 67 + spa/src/components/ui/calendar.tsx | 222 +++ spa/src/components/ui/card.tsx | 103 ++ spa/src/components/ui/carousel.tsx | 240 +++ spa/src/components/ui/chart.tsx | 373 +++++ spa/src/components/ui/checkbox.tsx | 31 + spa/src/components/ui/collapsible.tsx | 33 + spa/src/components/ui/combobox.tsx | 299 ++++ spa/src/components/ui/command.tsx | 193 +++ spa/src/components/ui/context-menu.tsx | 261 ++++ spa/src/components/ui/dialog.tsx | 168 +++ spa/src/components/ui/direction.tsx | 22 + spa/src/components/ui/drawer.tsx | 132 ++ spa/src/components/ui/dropdown-menu.tsx | 269 ++++ spa/src/components/ui/empty.tsx | 104 ++ spa/src/components/ui/field.tsx | 238 +++ spa/src/components/ui/hover-card.tsx | 42 + spa/src/components/ui/input-group.tsx | 154 ++ spa/src/components/ui/input-otp.tsx | 87 ++ spa/src/components/ui/input.tsx | 19 + spa/src/components/ui/item.tsx | 196 +++ spa/src/components/ui/kbd.tsx | 26 + spa/src/components/ui/label.tsx | 22 + spa/src/components/ui/menubar.tsx | 280 ++++ spa/src/components/ui/native-select.tsx | 61 + spa/src/components/ui/navigation-menu.tsx | 164 +++ spa/src/components/ui/pagination.tsx | 129 ++ spa/src/components/ui/popover.tsx | 87 ++ spa/src/components/ui/progress.tsx | 31 + spa/src/components/ui/radio-group.tsx | 42 + spa/src/components/ui/resizable.tsx | 50 + spa/src/components/ui/scroll-area.tsx | 53 + spa/src/components/ui/select.tsx | 192 +++ spa/src/components/ui/separator.tsx | 26 + spa/src/components/ui/sheet.tsx | 145 ++ spa/src/components/ui/sidebar.tsx | 700 +++++++++ spa/src/components/ui/skeleton.tsx | 13 + spa/src/components/ui/slider.tsx | 59 + spa/src/components/ui/sonner.tsx | 47 + spa/src/components/ui/spinner.tsx | 10 + spa/src/components/ui/switch.tsx | 33 + spa/src/components/ui/table.tsx | 114 ++ spa/src/components/ui/tabs.tsx | 90 ++ spa/src/components/ui/textarea.tsx | 18 + spa/src/components/ui/toggle-group.tsx | 89 ++ spa/src/components/ui/toggle.tsx | 45 + spa/src/components/ui/tooltip.tsx | 57 + spa/src/hooks/use-mobile.ts | 19 + spa/src/index.css | 130 ++ spa/src/lib/utils.ts | 6 + spa/src/main.tsx | 26 + spa/src/pages/dashboard.tsx | 75 + spa/src/pages/data-sources.tsx | 388 +++++ spa/src/pages/layout-builder.tsx | 547 +++++++ spa/src/pages/presets.tsx | 196 +++ spa/src/pages/widgets.tsx | 344 +++++ spa/src/router.tsx | 66 + spa/tsconfig.app.json | 29 + spa/tsconfig.json | 12 + spa/tsconfig.node.json | 24 + spa/vite.config.ts | 22 + 175 files changed, 12338 insertions(+), 801 deletions(-) create mode 100644 crates/bootstrap/src/event_handler.rs create mode 100644 docs/k-frame-spa-handoff.md create mode 100644 spa/.gitignore create mode 100644 spa/.prettierignore create mode 100644 spa/.prettierrc create mode 100644 spa/README.md create mode 100644 spa/bun.lock create mode 100644 spa/components.json create mode 100644 spa/eslint.config.js create mode 100644 spa/index.html create mode 100644 spa/package.json create mode 100644 spa/public/vite.svg create mode 100644 spa/src/api/client.ts create mode 100644 spa/src/api/data-sources.ts create mode 100644 spa/src/api/layout.ts create mode 100644 spa/src/api/presets.ts create mode 100644 spa/src/api/types.ts create mode 100644 spa/src/api/widgets.ts create mode 100644 spa/src/assets/react.svg create mode 100644 spa/src/components/app-shell.tsx create mode 100644 spa/src/components/theme-provider.tsx create mode 100644 spa/src/components/ui/accordion.tsx create mode 100644 spa/src/components/ui/alert-dialog.tsx create mode 100644 spa/src/components/ui/alert.tsx create mode 100644 spa/src/components/ui/aspect-ratio.tsx create mode 100644 spa/src/components/ui/avatar.tsx create mode 100644 spa/src/components/ui/badge.tsx create mode 100644 spa/src/components/ui/breadcrumb.tsx create mode 100644 spa/src/components/ui/button-group.tsx create mode 100644 spa/src/components/ui/button.tsx create mode 100644 spa/src/components/ui/calendar.tsx create mode 100644 spa/src/components/ui/card.tsx create mode 100644 spa/src/components/ui/carousel.tsx create mode 100644 spa/src/components/ui/chart.tsx create mode 100644 spa/src/components/ui/checkbox.tsx create mode 100644 spa/src/components/ui/collapsible.tsx create mode 100644 spa/src/components/ui/combobox.tsx create mode 100644 spa/src/components/ui/command.tsx create mode 100644 spa/src/components/ui/context-menu.tsx create mode 100644 spa/src/components/ui/dialog.tsx create mode 100644 spa/src/components/ui/direction.tsx create mode 100644 spa/src/components/ui/drawer.tsx create mode 100644 spa/src/components/ui/dropdown-menu.tsx create mode 100644 spa/src/components/ui/empty.tsx create mode 100644 spa/src/components/ui/field.tsx create mode 100644 spa/src/components/ui/hover-card.tsx create mode 100644 spa/src/components/ui/input-group.tsx create mode 100644 spa/src/components/ui/input-otp.tsx create mode 100644 spa/src/components/ui/input.tsx create mode 100644 spa/src/components/ui/item.tsx create mode 100644 spa/src/components/ui/kbd.tsx create mode 100644 spa/src/components/ui/label.tsx create mode 100644 spa/src/components/ui/menubar.tsx create mode 100644 spa/src/components/ui/native-select.tsx create mode 100644 spa/src/components/ui/navigation-menu.tsx create mode 100644 spa/src/components/ui/pagination.tsx create mode 100644 spa/src/components/ui/popover.tsx create mode 100644 spa/src/components/ui/progress.tsx create mode 100644 spa/src/components/ui/radio-group.tsx create mode 100644 spa/src/components/ui/resizable.tsx create mode 100644 spa/src/components/ui/scroll-area.tsx create mode 100644 spa/src/components/ui/select.tsx create mode 100644 spa/src/components/ui/separator.tsx create mode 100644 spa/src/components/ui/sheet.tsx create mode 100644 spa/src/components/ui/sidebar.tsx create mode 100644 spa/src/components/ui/skeleton.tsx create mode 100644 spa/src/components/ui/slider.tsx create mode 100644 spa/src/components/ui/sonner.tsx create mode 100644 spa/src/components/ui/spinner.tsx create mode 100644 spa/src/components/ui/switch.tsx create mode 100644 spa/src/components/ui/table.tsx create mode 100644 spa/src/components/ui/tabs.tsx create mode 100644 spa/src/components/ui/textarea.tsx create mode 100644 spa/src/components/ui/toggle-group.tsx create mode 100644 spa/src/components/ui/toggle.tsx create mode 100644 spa/src/components/ui/tooltip.tsx create mode 100644 spa/src/hooks/use-mobile.ts create mode 100644 spa/src/index.css create mode 100644 spa/src/lib/utils.ts create mode 100644 spa/src/main.tsx create mode 100644 spa/src/pages/dashboard.tsx create mode 100644 spa/src/pages/data-sources.tsx create mode 100644 spa/src/pages/layout-builder.tsx create mode 100644 spa/src/pages/presets.tsx create mode 100644 spa/src/pages/widgets.tsx create mode 100644 spa/src/router.tsx create mode 100644 spa/tsconfig.app.json create mode 100644 spa/tsconfig.json create mode 100644 spa/tsconfig.node.json create mode 100644 spa/vite.config.ts diff --git a/Cargo.lock b/Cargo.lock index 16cd04b..e9efe08 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -166,6 +166,8 @@ dependencies = [ "dotenvy", "http-api", "http-json", + "media-adapter", + "rss-adapter", "tcp-server", "tokio", "tracing", @@ -1102,12 +1104,20 @@ dependencies = [ "digest", ] +[[package]] +name = "md5" +version = "0.7.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "490cc448043f947bae3cbee9c203358d62dbee0db12107a74be5c30ccfd09771" + [[package]] name = "media-adapter" version = "0.1.0" dependencies = [ "axum", "domain", + "fastrand", + "md5", "reqwest", "serde_json", "thiserror", diff --git a/Cargo.toml b/Cargo.toml index 794e2f9..482f611 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -36,6 +36,8 @@ display-terminal = { path = "crates/adapters/display-terminal" } config-sqlite = { path = "crates/adapters/config-sqlite" } http-json = { path = "crates/adapters/http-json" } http-api = { path = "crates/adapters/http-api" } +media-adapter = { path = "crates/adapters/media" } +rss-adapter = { path = "crates/adapters/rss" } axum = { version = "0.8", features = ["macros"] } tower-http = { version = "0.6", features = ["cors"] } api-types = { path = "crates/api-types" } diff --git a/crates/adapters/config-memory/src/lib.rs b/crates/adapters/config-memory/src/lib.rs index befbe52..9c1191b 100644 --- a/crates/adapters/config-memory/src/lib.rs +++ b/crates/adapters/config-memory/src/lib.rs @@ -1,10 +1,9 @@ +use domain::{ + ConfigRepository, DataSource, DataSourceId, Layout, LayoutPreset, LayoutPresetId, WidgetConfig, + WidgetId, +}; use std::collections::HashMap; use std::sync::RwLock; -use domain::{ - ConfigRepository, - DataSource, DataSourceId, Layout, LayoutPreset, LayoutPresetId, - WidgetConfig, WidgetId, -}; #[derive(Debug, thiserror::Error)] pub enum MemoryConfigError { @@ -19,8 +18,8 @@ pub struct MemoryConfigStore { presets: RwLock>, } -impl MemoryConfigStore { - pub fn new() -> Self { +impl Default for MemoryConfigStore { + fn default() -> Self { Self { widgets: RwLock::new(HashMap::new()), data_sources: RwLock::new(HashMap::new()), @@ -30,82 +29,130 @@ impl MemoryConfigStore { } } +impl MemoryConfigStore { + pub fn new() -> Self { + Self::default() + } +} + impl ConfigRepository for MemoryConfigStore { type Error = MemoryConfigError; async fn get_widget(&self, id: WidgetId) -> Result, Self::Error> { - let guard = self.widgets.read().map_err(|_| MemoryConfigError::LockPoisoned)?; + let guard = self + .widgets + .read() + .map_err(|_| MemoryConfigError::LockPoisoned)?; Ok(guard.get(&id).cloned()) } async fn list_widgets(&self) -> Result, Self::Error> { - let guard = self.widgets.read().map_err(|_| MemoryConfigError::LockPoisoned)?; + let guard = self + .widgets + .read() + .map_err(|_| MemoryConfigError::LockPoisoned)?; Ok(guard.values().cloned().collect()) } async fn save_widget(&self, config: &WidgetConfig) -> Result<(), Self::Error> { - let mut guard = self.widgets.write().map_err(|_| MemoryConfigError::LockPoisoned)?; + let mut guard = self + .widgets + .write() + .map_err(|_| MemoryConfigError::LockPoisoned)?; guard.insert(config.id, config.clone()); Ok(()) } async fn delete_widget(&self, id: WidgetId) -> Result<(), Self::Error> { - let mut guard = self.widgets.write().map_err(|_| MemoryConfigError::LockPoisoned)?; + let mut guard = self + .widgets + .write() + .map_err(|_| MemoryConfigError::LockPoisoned)?; guard.remove(&id); Ok(()) } async fn get_data_source(&self, id: DataSourceId) -> Result, Self::Error> { - let guard = self.data_sources.read().map_err(|_| MemoryConfigError::LockPoisoned)?; + let guard = self + .data_sources + .read() + .map_err(|_| MemoryConfigError::LockPoisoned)?; Ok(guard.get(&id).cloned()) } async fn list_data_sources(&self) -> Result, Self::Error> { - let guard = self.data_sources.read().map_err(|_| MemoryConfigError::LockPoisoned)?; + let guard = self + .data_sources + .read() + .map_err(|_| MemoryConfigError::LockPoisoned)?; Ok(guard.values().cloned().collect()) } async fn save_data_source(&self, source: &DataSource) -> Result<(), Self::Error> { - let mut guard = self.data_sources.write().map_err(|_| MemoryConfigError::LockPoisoned)?; + let mut guard = self + .data_sources + .write() + .map_err(|_| MemoryConfigError::LockPoisoned)?; guard.insert(source.id, source.clone()); Ok(()) } async fn delete_data_source(&self, id: DataSourceId) -> Result<(), Self::Error> { - let mut guard = self.data_sources.write().map_err(|_| MemoryConfigError::LockPoisoned)?; + let mut guard = self + .data_sources + .write() + .map_err(|_| MemoryConfigError::LockPoisoned)?; guard.remove(&id); Ok(()) } async fn get_layout(&self) -> Result, Self::Error> { - let guard = self.layout.read().map_err(|_| MemoryConfigError::LockPoisoned)?; + let guard = self + .layout + .read() + .map_err(|_| MemoryConfigError::LockPoisoned)?; Ok(guard.clone()) } async fn save_layout(&self, layout: &Layout) -> Result<(), Self::Error> { - let mut guard = self.layout.write().map_err(|_| MemoryConfigError::LockPoisoned)?; + let mut guard = self + .layout + .write() + .map_err(|_| MemoryConfigError::LockPoisoned)?; *guard = Some(layout.clone()); Ok(()) } async fn get_preset(&self, id: LayoutPresetId) -> Result, Self::Error> { - let guard = self.presets.read().map_err(|_| MemoryConfigError::LockPoisoned)?; + let guard = self + .presets + .read() + .map_err(|_| MemoryConfigError::LockPoisoned)?; Ok(guard.get(&id).cloned()) } async fn list_presets(&self) -> Result, Self::Error> { - let guard = self.presets.read().map_err(|_| MemoryConfigError::LockPoisoned)?; + let guard = self + .presets + .read() + .map_err(|_| MemoryConfigError::LockPoisoned)?; Ok(guard.values().cloned().collect()) } async fn save_preset(&self, preset: &LayoutPreset) -> Result<(), Self::Error> { - let mut guard = self.presets.write().map_err(|_| MemoryConfigError::LockPoisoned)?; + let mut guard = self + .presets + .write() + .map_err(|_| MemoryConfigError::LockPoisoned)?; guard.insert(preset.id, preset.clone()); Ok(()) } async fn delete_preset(&self, id: LayoutPresetId) -> Result<(), Self::Error> { - let mut guard = self.presets.write().map_err(|_| MemoryConfigError::LockPoisoned)?; + let mut guard = self + .presets + .write() + .map_err(|_| MemoryConfigError::LockPoisoned)?; guard.remove(&id); Ok(()) } diff --git a/crates/adapters/config-sqlite/src/lib.rs b/crates/adapters/config-sqlite/src/lib.rs index 693f588..f0bc7e1 100644 --- a/crates/adapters/config-sqlite/src/lib.rs +++ b/crates/adapters/config-sqlite/src/lib.rs @@ -1,6 +1,6 @@ pub mod error; -mod serialization; mod repository; +mod serialization; use sqlx::SqlitePool; @@ -27,8 +27,10 @@ impl SqliteConfigStore { data_source_id INTEGER NOT NULL, mappings TEXT NOT NULL, max_data_size INTEGER NOT NULL - )" - ).execute(&self.pool).await?; + )", + ) + .execute(&self.pool) + .await?; sqlx::query( "CREATE TABLE IF NOT EXISTS data_sources ( @@ -37,23 +39,29 @@ impl SqliteConfigStore { source_type TEXT NOT NULL, poll_interval_secs INTEGER NOT NULL, config TEXT NOT NULL - )" - ).execute(&self.pool).await?; + )", + ) + .execute(&self.pool) + .await?; sqlx::query( "CREATE TABLE IF NOT EXISTS layout ( id INTEGER PRIMARY KEY CHECK (id = 1), data TEXT NOT NULL - )" - ).execute(&self.pool).await?; + )", + ) + .execute(&self.pool) + .await?; sqlx::query( "CREATE TABLE IF NOT EXISTS presets ( id INTEGER PRIMARY KEY, name TEXT NOT NULL, layout_data TEXT NOT NULL - )" - ).execute(&self.pool).await?; + )", + ) + .execute(&self.pool) + .await?; Ok(()) } diff --git a/crates/adapters/config-sqlite/src/repository/data_sources.rs b/crates/adapters/config-sqlite/src/repository/data_sources.rs index 78a1fc9..3f45cf3 100644 --- a/crates/adapters/config-sqlite/src/repository/data_sources.rs +++ b/crates/adapters/config-sqlite/src/repository/data_sources.rs @@ -1,10 +1,13 @@ -use domain::{DataSource, DataSourceId}; use crate::SqliteConfigStore; use crate::error::SqliteConfigError; use crate::serialization::data_source as ser; +use domain::{DataSource, DataSourceId}; impl SqliteConfigStore { - pub(crate) async fn get_data_source_impl(&self, id: DataSourceId) -> Result, SqliteConfigError> { + pub(crate) async fn get_data_source_impl( + &self, + id: DataSourceId, + ) -> Result, SqliteConfigError> { let row = sqlx::query("SELECT * FROM data_sources WHERE id = ?") .bind(id as i64) .fetch_optional(&self.pool) @@ -17,16 +20,21 @@ impl SqliteConfigStore { } } - pub(crate) async fn list_data_sources_impl(&self) -> Result, SqliteConfigError> { + pub(crate) async fn list_data_sources_impl( + &self, + ) -> Result, SqliteConfigError> { let rows = sqlx::query("SELECT * FROM data_sources") .fetch_all(&self.pool) .await .map_err(SqliteConfigError::Sql)?; - rows.iter().map(|r| ser::data_source_from_row(r)).collect() + rows.iter().map(ser::data_source_from_row).collect() } - pub(crate) async fn save_data_source_impl(&self, source: &DataSource) -> Result<(), SqliteConfigError> { + pub(crate) async fn save_data_source_impl( + &self, + source: &DataSource, + ) -> Result<(), SqliteConfigError> { let config_json = ser::data_source_config_to_json(&source.config)?; let type_str = ser::data_source_type_to_str(&source.source_type); @@ -46,7 +54,10 @@ impl SqliteConfigStore { Ok(()) } - pub(crate) async fn delete_data_source_impl(&self, id: DataSourceId) -> Result<(), SqliteConfigError> { + pub(crate) async fn delete_data_source_impl( + &self, + id: DataSourceId, + ) -> Result<(), SqliteConfigError> { sqlx::query("DELETE FROM data_sources WHERE id = ?") .bind(id as i64) .execute(&self.pool) diff --git a/crates/adapters/config-sqlite/src/repository/layout.rs b/crates/adapters/config-sqlite/src/repository/layout.rs index a4149a3..5f7d8a0 100644 --- a/crates/adapters/config-sqlite/src/repository/layout.rs +++ b/crates/adapters/config-sqlite/src/repository/layout.rs @@ -1,8 +1,8 @@ -use sqlx::Row; -use domain::Layout; use crate::SqliteConfigStore; use crate::error::SqliteConfigError; use crate::serialization::layout as ser; +use domain::Layout; +use sqlx::Row; impl SqliteConfigStore { pub(crate) async fn get_layout_impl(&self) -> Result, SqliteConfigError> { @@ -23,13 +23,11 @@ impl SqliteConfigStore { pub(crate) async fn save_layout_impl(&self, layout: &Layout) -> Result<(), SqliteConfigError> { let json = ser::layout_to_json(layout)?; - sqlx::query( - "INSERT OR REPLACE INTO layout (id, data) VALUES (1, ?)" - ) - .bind(&json) - .execute(&self.pool) - .await - .map_err(SqliteConfigError::Sql)?; + sqlx::query("INSERT OR REPLACE INTO layout (id, data) VALUES (1, ?)") + .bind(&json) + .execute(&self.pool) + .await + .map_err(SqliteConfigError::Sql)?; Ok(()) } diff --git a/crates/adapters/config-sqlite/src/repository/mod.rs b/crates/adapters/config-sqlite/src/repository/mod.rs index 6753196..53982b4 100644 --- a/crates/adapters/config-sqlite/src/repository/mod.rs +++ b/crates/adapters/config-sqlite/src/repository/mod.rs @@ -1,16 +1,14 @@ -mod widgets; mod data_sources; mod layout; mod presets; +mod widgets; -use domain::{ - ConfigRepository, - DataSource, DataSourceId, - Layout, LayoutPreset, LayoutPresetId, - WidgetConfig, WidgetId, -}; use crate::SqliteConfigStore; use crate::error::SqliteConfigError; +use domain::{ + ConfigRepository, DataSource, DataSourceId, Layout, LayoutPreset, LayoutPresetId, WidgetConfig, + WidgetId, +}; impl ConfigRepository for SqliteConfigStore { type Error = SqliteConfigError; diff --git a/crates/adapters/config-sqlite/src/repository/presets.rs b/crates/adapters/config-sqlite/src/repository/presets.rs index 44e7e5a..6a5dfd7 100644 --- a/crates/adapters/config-sqlite/src/repository/presets.rs +++ b/crates/adapters/config-sqlite/src/repository/presets.rs @@ -1,10 +1,13 @@ -use domain::{LayoutPreset, LayoutPresetId}; use crate::SqliteConfigStore; use crate::error::SqliteConfigError; use crate::serialization::{layout as layout_ser, preset as ser}; +use domain::{LayoutPreset, LayoutPresetId}; impl SqliteConfigStore { - pub(crate) async fn get_preset_impl(&self, id: LayoutPresetId) -> Result, SqliteConfigError> { + pub(crate) async fn get_preset_impl( + &self, + id: LayoutPresetId, + ) -> Result, SqliteConfigError> { let row = sqlx::query("SELECT * FROM presets WHERE id = ?") .bind(id as i64) .fetch_optional(&self.pool) @@ -23,26 +26,30 @@ impl SqliteConfigStore { .await .map_err(SqliteConfigError::Sql)?; - rows.iter().map(|r| ser::preset_from_row(r)).collect() + rows.iter().map(ser::preset_from_row).collect() } - pub(crate) async fn save_preset_impl(&self, preset: &LayoutPreset) -> Result<(), SqliteConfigError> { + pub(crate) async fn save_preset_impl( + &self, + preset: &LayoutPreset, + ) -> Result<(), SqliteConfigError> { let layout_json = layout_ser::layout_to_json(&preset.layout)?; - sqlx::query( - "INSERT OR REPLACE INTO presets (id, name, layout_data) VALUES (?, ?, ?)" - ) - .bind(preset.id as i64) - .bind(&preset.name) - .bind(&layout_json) - .execute(&self.pool) - .await - .map_err(SqliteConfigError::Sql)?; + sqlx::query("INSERT OR REPLACE INTO presets (id, name, layout_data) VALUES (?, ?, ?)") + .bind(preset.id as i64) + .bind(&preset.name) + .bind(&layout_json) + .execute(&self.pool) + .await + .map_err(SqliteConfigError::Sql)?; Ok(()) } - pub(crate) async fn delete_preset_impl(&self, id: LayoutPresetId) -> Result<(), SqliteConfigError> { + pub(crate) async fn delete_preset_impl( + &self, + id: LayoutPresetId, + ) -> Result<(), SqliteConfigError> { sqlx::query("DELETE FROM presets WHERE id = ?") .bind(id as i64) .execute(&self.pool) diff --git a/crates/adapters/config-sqlite/src/repository/widgets.rs b/crates/adapters/config-sqlite/src/repository/widgets.rs index fdd3ca7..48fcc74 100644 --- a/crates/adapters/config-sqlite/src/repository/widgets.rs +++ b/crates/adapters/config-sqlite/src/repository/widgets.rs @@ -1,10 +1,13 @@ -use domain::{WidgetConfig, WidgetId}; use crate::SqliteConfigStore; use crate::error::SqliteConfigError; use crate::serialization::widget as ser; +use domain::{WidgetConfig, WidgetId}; impl SqliteConfigStore { - pub(crate) async fn get_widget_impl(&self, id: WidgetId) -> Result, SqliteConfigError> { + pub(crate) async fn get_widget_impl( + &self, + id: WidgetId, + ) -> Result, SqliteConfigError> { let row = sqlx::query("SELECT * FROM widgets WHERE id = ?") .bind(id as i64) .fetch_optional(&self.pool) @@ -23,10 +26,13 @@ impl SqliteConfigStore { .await .map_err(SqliteConfigError::Sql)?; - rows.iter().map(|r| ser::widget_from_row(r)).collect() + rows.iter().map(ser::widget_from_row).collect() } - pub(crate) async fn save_widget_impl(&self, config: &WidgetConfig) -> Result<(), SqliteConfigError> { + pub(crate) async fn save_widget_impl( + &self, + config: &WidgetConfig, + ) -> Result<(), SqliteConfigError> { let mappings_json = ser::mappings_to_json(&config.mappings)?; let hint_str = ser::display_hint_to_str(&config.display_hint); diff --git a/crates/adapters/config-sqlite/src/serialization/data_source.rs b/crates/adapters/config-sqlite/src/serialization/data_source.rs index 3fb2587..5378c3b 100644 --- a/crates/adapters/config-sqlite/src/serialization/data_source.rs +++ b/crates/adapters/config-sqlite/src/serialization/data_source.rs @@ -1,14 +1,13 @@ -use std::time::Duration; +use crate::error::SqliteConfigError; +use domain::{DataSource, DataSourceConfig, DataSourceType}; use sqlx::Row; use sqlx::sqlite::SqliteRow; -use domain::{DataSource, DataSourceConfig, DataSourceType}; -use crate::error::SqliteConfigError; +use std::time::Duration; pub fn data_source_type_to_str(t: &DataSourceType) -> &'static str { match t { DataSourceType::Weather => "weather", DataSourceType::Media => "media", - DataSourceType::Xtb => "xtb", DataSourceType::Rss => "rss", DataSourceType::HttpJson => "http_json", DataSourceType::Webhook => "webhook", @@ -19,11 +18,12 @@ fn data_source_type_from_str(s: &str) -> Result Ok(DataSourceType::Weather), "media" => Ok(DataSourceType::Media), - "xtb" => Ok(DataSourceType::Xtb), "rss" => Ok(DataSourceType::Rss), "http_json" => Ok(DataSourceType::HttpJson), "webhook" => Ok(DataSourceType::Webhook), - _ => Err(SqliteConfigError::Serialization(format!("unknown source type: {s}"))), + _ => Err(SqliteConfigError::Serialization(format!( + "unknown source type: {s}" + ))), } } @@ -37,20 +37,27 @@ pub fn data_source_config_to_json(config: &DataSourceConfig) -> Result Result { - let v: serde_json::Value = serde_json::from_str(json) - .map_err(|e| SqliteConfigError::Serialization(e.to_string()))?; + let v: serde_json::Value = + serde_json::from_str(json).map_err(|e| SqliteConfigError::Serialization(e.to_string()))?; let url = v["url"].as_str().map(String::from); let api_key = v["api_key"].as_str().map(String::from); let headers = match v["headers"].as_array() { - Some(arr) => arr.iter().filter_map(|h| { - let pair = h.as_array()?; - Some((pair[0].as_str()?.into(), pair[1].as_str()?.into())) - }).collect(), + Some(arr) => arr + .iter() + .filter_map(|h| { + let pair = h.as_array()?; + Some((pair[0].as_str()?.into(), pair[1].as_str()?.into())) + }) + .collect(), None => vec![], }; - Ok(DataSourceConfig { url, headers, api_key }) + Ok(DataSourceConfig { + url, + headers, + api_key, + }) } pub fn data_source_from_row(row: &SqliteRow) -> Result { diff --git a/crates/adapters/config-sqlite/src/serialization/layout.rs b/crates/adapters/config-sqlite/src/serialization/layout.rs index 47eba61..16402c1 100644 --- a/crates/adapters/config-sqlite/src/serialization/layout.rs +++ b/crates/adapters/config-sqlite/src/serialization/layout.rs @@ -1,5 +1,5 @@ -use domain::{ContainerNode, Direction, Layout, LayoutChild, LayoutNode, Sizing}; use crate::error::SqliteConfigError; +use domain::{ContainerNode, Direction, Layout, LayoutChild, LayoutNode, Sizing}; pub fn layout_to_json(layout: &Layout) -> Result { let v = node_to_json(&layout.root); @@ -7,8 +7,8 @@ pub fn layout_to_json(layout: &Layout) -> Result { } pub fn layout_from_json(json: &str) -> Result { - let v: serde_json::Value = serde_json::from_str(json) - .map_err(|e| SqliteConfigError::Serialization(e.to_string()))?; + let v: serde_json::Value = + serde_json::from_str(json).map_err(|e| SqliteConfigError::Serialization(e.to_string()))?; let root = node_from_json(&v)?; Ok(Layout { root }) } @@ -17,16 +17,20 @@ fn node_to_json(node: &LayoutNode) -> serde_json::Value { match node { LayoutNode::Leaf(id) => serde_json::json!({ "type": "leaf", "widget_id": id }), LayoutNode::Container(c) => { - let children: Vec = c.children.iter().map(|ch| { - let sizing = match &ch.sizing { - Sizing::Fixed(px) => serde_json::json!({ "type": "fixed", "value": px }), - Sizing::Flex(w) => serde_json::json!({ "type": "flex", "value": w }), - }; - serde_json::json!({ - "sizing": sizing, - "node": node_to_json(&ch.node), + let children: Vec = c + .children + .iter() + .map(|ch| { + let sizing = match &ch.sizing { + Sizing::Fixed(px) => serde_json::json!({ "type": "fixed", "value": px }), + Sizing::Flex(w) => serde_json::json!({ "type": "flex", "value": w }), + }; + serde_json::json!({ + "sizing": sizing, + "node": node_to_json(&ch.node), + }) }) - }).collect(); + .collect(); serde_json::json!({ "type": "container", @@ -44,25 +48,44 @@ fn node_from_json(v: &serde_json::Value) -> Result { - let id = v["widget_id"].as_u64().ok_or_else(|| err("missing widget_id"))? as u16; + let id = v["widget_id"] + .as_u64() + .ok_or_else(|| err("missing widget_id"))? as u16; Ok(LayoutNode::Leaf(id)) } "container" => { - let direction = match v["direction"].as_str().ok_or_else(|| err("missing direction"))? { + let direction = match v["direction"] + .as_str() + .ok_or_else(|| err("missing direction"))? + { "row" => Direction::Row, "column" => Direction::Column, d => return Err(err(&format!("unknown direction: {d}"))), }; let gap = v["gap"].as_u64().unwrap_or(0) as u8; let padding = v["padding"].as_u64().unwrap_or(0) as u8; - let children = v["children"].as_array() + let children = v["children"] + .as_array() .ok_or_else(|| err("missing children"))? .iter() .map(|ch| { let sizing_v = &ch["sizing"]; - let sizing = match sizing_v["type"].as_str().ok_or_else(|| err("missing sizing type"))? { - "fixed" => Sizing::Fixed(sizing_v["value"].as_u64().ok_or_else(|| err("missing fixed value"))? as u16), - "flex" => Sizing::Flex(sizing_v["value"].as_u64().ok_or_else(|| err("missing flex value"))? as u8), + let sizing = match sizing_v["type"] + .as_str() + .ok_or_else(|| err("missing sizing type"))? + { + "fixed" => Sizing::Fixed( + sizing_v["value"] + .as_u64() + .ok_or_else(|| err("missing fixed value"))? + as u16, + ), + "flex" => Sizing::Flex( + sizing_v["value"] + .as_u64() + .ok_or_else(|| err("missing flex value"))? + as u8, + ), s => return Err(err(&format!("unknown sizing: {s}"))), }; let node = node_from_json(&ch["node"])?; @@ -70,7 +93,12 @@ fn node_from_json(v: &serde_json::Value) -> Result, _>>()?; - Ok(LayoutNode::Container(ContainerNode { direction, gap, padding, children })) + Ok(LayoutNode::Container(ContainerNode { + direction, + gap, + padding, + children, + })) } t => Err(err(&format!("unknown node type: {t}"))), } diff --git a/crates/adapters/config-sqlite/src/serialization/mod.rs b/crates/adapters/config-sqlite/src/serialization/mod.rs index 37f98f1..f2dab06 100644 --- a/crates/adapters/config-sqlite/src/serialization/mod.rs +++ b/crates/adapters/config-sqlite/src/serialization/mod.rs @@ -1,4 +1,4 @@ -pub mod widget; pub mod data_source; pub mod layout; pub mod preset; +pub mod widget; diff --git a/crates/adapters/config-sqlite/src/serialization/preset.rs b/crates/adapters/config-sqlite/src/serialization/preset.rs index f6e2848..833d813 100644 --- a/crates/adapters/config-sqlite/src/serialization/preset.rs +++ b/crates/adapters/config-sqlite/src/serialization/preset.rs @@ -1,8 +1,8 @@ +use super::layout::layout_from_json; +use crate::error::SqliteConfigError; +use domain::LayoutPreset; use sqlx::Row; use sqlx::sqlite::SqliteRow; -use domain::LayoutPreset; -use crate::error::SqliteConfigError; -use super::layout::layout_from_json; pub fn preset_from_row(row: &SqliteRow) -> Result { let id: i64 = row.get("id"); diff --git a/crates/adapters/config-sqlite/src/serialization/widget.rs b/crates/adapters/config-sqlite/src/serialization/widget.rs index ed1be70..78f636f 100644 --- a/crates/adapters/config-sqlite/src/serialization/widget.rs +++ b/crates/adapters/config-sqlite/src/serialization/widget.rs @@ -1,7 +1,7 @@ +use crate::error::SqliteConfigError; +use domain::{DisplayHint, KeyMapping, WidgetConfig}; use sqlx::Row; use sqlx::sqlite::SqliteRow; -use domain::{DisplayHint, KeyMapping, WidgetConfig}; -use crate::error::SqliteConfigError; pub fn display_hint_to_str(hint: &DisplayHint) -> &'static str { match hint { @@ -16,32 +16,44 @@ fn display_hint_from_str(s: &str) -> Result { "icon_value" => Ok(DisplayHint::IconValue), "text_block" => Ok(DisplayHint::TextBlock), "key_value" => Ok(DisplayHint::KeyValue), - _ => Err(SqliteConfigError::Serialization(format!("unknown display hint: {s}"))), + _ => Err(SqliteConfigError::Serialization(format!( + "unknown display hint: {s}" + ))), } } pub fn mappings_to_json(mappings: &[KeyMapping]) -> Result { - let entries: Vec = mappings.iter().map(|m| { - serde_json::json!({ - "source_path": m.source_path, - "target_key": m.target_key, + let entries: Vec = mappings + .iter() + .map(|m| { + serde_json::json!({ + "source_path": m.source_path, + "target_key": m.target_key, + }) }) - }).collect(); + .collect(); serde_json::to_string(&entries).map_err(|e| SqliteConfigError::Serialization(e.to_string())) } fn mappings_from_json(json: &str) -> Result, SqliteConfigError> { - let entries: Vec = serde_json::from_str(json) - .map_err(|e| SqliteConfigError::Serialization(e.to_string()))?; + let entries: Vec = + serde_json::from_str(json).map_err(|e| SqliteConfigError::Serialization(e.to_string()))?; - entries.iter().map(|v| { - Ok(KeyMapping { - source_path: v["source_path"].as_str() - .ok_or_else(|| SqliteConfigError::Serialization("missing source_path".into()))?.into(), - target_key: v["target_key"].as_str() - .ok_or_else(|| SqliteConfigError::Serialization("missing target_key".into()))?.into(), + entries + .iter() + .map(|v| { + Ok(KeyMapping { + source_path: v["source_path"] + .as_str() + .ok_or_else(|| SqliteConfigError::Serialization("missing source_path".into()))? + .into(), + target_key: v["target_key"] + .as_str() + .ok_or_else(|| SqliteConfigError::Serialization("missing target_key".into()))? + .into(), + }) }) - }).collect() + .collect() } pub fn widget_from_row(row: &SqliteRow) -> Result { diff --git a/crates/adapters/config-sqlite/tests/config_store_tests.rs b/crates/adapters/config-sqlite/tests/config_store_tests.rs index e17e323..9077315 100644 --- a/crates/adapters/config-sqlite/tests/config_store_tests.rs +++ b/crates/adapters/config-sqlite/tests/config_store_tests.rs @@ -1,11 +1,9 @@ -use std::time::Duration; -use domain::{ - ConfigRepository, DisplayHint, KeyMapping, WidgetConfig, - DataSource, DataSourceConfig, DataSourceType, - Layout, LayoutNode, ContainerNode, LayoutChild, Direction, Sizing, - LayoutPreset, -}; use config_sqlite::SqliteConfigStore; +use domain::{ + ConfigRepository, ContainerNode, DataSource, DataSourceConfig, DataSourceType, Direction, + DisplayHint, KeyMapping, Layout, LayoutChild, LayoutNode, LayoutPreset, Sizing, WidgetConfig, +}; +use std::time::Duration; async fn test_store() -> SqliteConfigStore { SqliteConfigStore::new("sqlite::memory:").await.unwrap() @@ -18,8 +16,14 @@ fn weather_widget() -> WidgetConfig { display_hint: DisplayHint::IconValue, data_source_id: 10, mappings: vec![ - KeyMapping { source_path: "$.temp".into(), target_key: "temperature".into() }, - KeyMapping { source_path: "$.icon".into(), target_key: "icon".into() }, + KeyMapping { + source_path: "$.temp".into(), + target_key: "temperature".into(), + }, + KeyMapping { + source_path: "$.icon".into(), + target_key: "icon".into(), + }, ], max_data_size: 2048, } @@ -46,8 +50,14 @@ fn test_layout() -> Layout { gap: 4, padding: 2, children: vec![ - LayoutChild { sizing: Sizing::Flex(1), node: LayoutNode::Leaf(1) }, - LayoutChild { sizing: Sizing::Fixed(80), node: LayoutNode::Leaf(2) }, + LayoutChild { + sizing: Sizing::Flex(1), + node: LayoutNode::Leaf(1), + }, + LayoutChild { + sizing: Sizing::Fixed(80), + node: LayoutNode::Leaf(2), + }, ], }), } @@ -78,14 +88,17 @@ async fn get_nonexistent_widget_returns_none() { async fn list_widgets_returns_all() { let store = test_store().await; store.save_widget(&weather_widget()).await.unwrap(); - store.save_widget(&WidgetConfig { - id: 2, - name: "portfolio".into(), - display_hint: DisplayHint::KeyValue, - data_source_id: 20, - mappings: vec![], - max_data_size: 1024, - }).await.unwrap(); + store + .save_widget(&WidgetConfig { + id: 2, + name: "portfolio".into(), + display_hint: DisplayHint::KeyValue, + data_source_id: 20, + mappings: vec![], + max_data_size: 1024, + }) + .await + .unwrap(); let widgets = store.list_widgets().await.unwrap(); assert_eq!(widgets.len(), 2); @@ -172,12 +185,22 @@ async fn save_and_retrieve_preset() { #[tokio::test] async fn list_and_delete_presets() { let store = test_store().await; - store.save_preset(&LayoutPreset { - id: 1, name: "a".into(), layout: test_layout(), - }).await.unwrap(); - store.save_preset(&LayoutPreset { - id: 2, name: "b".into(), layout: test_layout(), - }).await.unwrap(); + store + .save_preset(&LayoutPreset { + id: 1, + name: "a".into(), + layout: test_layout(), + }) + .await + .unwrap(); + store + .save_preset(&LayoutPreset { + id: 2, + name: "b".into(), + layout: test_layout(), + }) + .await + .unwrap(); assert_eq!(store.list_presets().await.unwrap().len(), 2); diff --git a/crates/adapters/display-terminal/src/lib.rs b/crates/adapters/display-terminal/src/lib.rs index 5d65bdc..d36bab6 100644 --- a/crates/adapters/display-terminal/src/lib.rs +++ b/crates/adapters/display-terminal/src/lib.rs @@ -1,5 +1,6 @@ use client_domain::{BoundingBox, DisplayPort}; +#[derive(Default)] pub struct TerminalDisplay; impl TerminalDisplay { @@ -12,12 +13,24 @@ impl DisplayPort for TerminalDisplay { type Error = std::io::Error; fn clear_region(&mut self, bounds: BoundingBox) -> Result<(), Self::Error> { - println!("[CLEAR] ({}, {}) {}x{}", bounds.x, bounds.y, bounds.width, bounds.height); + println!( + "[CLEAR] ({}, {}) {}x{}", + bounds.x, bounds.y, bounds.width, bounds.height + ); Ok(()) } - fn draw_text(&mut self, text: &str, x: u16, y: u16, bounds: BoundingBox) -> Result<(), Self::Error> { - println!("[TEXT] ({x}, {y}) in {}x{}: \"{text}\"", bounds.width, bounds.height); + fn draw_text( + &mut self, + text: &str, + x: u16, + y: u16, + bounds: BoundingBox, + ) -> Result<(), Self::Error> { + println!( + "[TEXT] ({x}, {y}) in {}x{}: \"{text}\"", + bounds.width, bounds.height + ); Ok(()) } @@ -27,7 +40,10 @@ impl DisplayPort for TerminalDisplay { } fn fill_background(&mut self, bounds: BoundingBox) -> Result<(), Self::Error> { - println!("[BG] ({}, {}) {}x{}", bounds.x, bounds.y, bounds.width, bounds.height); + println!( + "[BG] ({}, {}) {}x{}", + bounds.x, bounds.y, bounds.width, bounds.height + ); Ok(()) } diff --git a/crates/adapters/http-api/src/lib.rs b/crates/adapters/http-api/src/lib.rs index a118de8..4564a2e 100644 --- a/crates/adapters/http-api/src/lib.rs +++ b/crates/adapters/http-api/src/lib.rs @@ -1,9 +1,9 @@ mod routes; -use std::sync::Arc; use axum::Router; -use tower_http::cors::CorsLayer; use domain::{ConfigRepository, EventPublisher}; +use std::sync::Arc; +use tower_http::cors::CorsLayer; pub struct AppState { pub config: Arc, diff --git a/crates/adapters/http-api/src/routes/data_sources.rs b/crates/adapters/http-api/src/routes/data_sources.rs index 26746a5..abe6f2a 100644 --- a/crates/adapters/http-api/src/routes/data_sources.rs +++ b/crates/adapters/http-api/src/routes/data_sources.rs @@ -1,54 +1,107 @@ +use crate::AppState; +use api_types::DataSourceDto; +use application::ConfigService; use axum::{ extract::{Path, State}, http::StatusCode, response::Json, }; use domain::{ConfigRepository, EventPublisher}; -use application::ConfigService; -use crate::AppState; -use api_types::DataSourceDto; type S = State>; -pub async fn list_data_sources(State(state): S) -> Result>, StatusCode> -where C: ConfigRepository, C::Error: std::fmt::Debug, E: EventPublisher, E::Error: std::fmt::Debug, +pub async fn list_data_sources( + State(state): S, +) -> Result>, StatusCode> +where + C: ConfigRepository, + C::Error: std::fmt::Debug, + E: EventPublisher, + E::Error: std::fmt::Debug, { - let sources = state.config.list_data_sources().await.map_err(|_| StatusCode::INTERNAL_SERVER_ERROR)?; + let sources = state + .config + .list_data_sources() + .await + .map_err(|_| StatusCode::INTERNAL_SERVER_ERROR)?; Ok(Json(sources.iter().map(DataSourceDto::from).collect())) } -pub async fn get_data_source(State(state): S, Path(id): Path) -> Result, StatusCode> -where C: ConfigRepository, C::Error: std::fmt::Debug, E: EventPublisher, E::Error: std::fmt::Debug, +pub async fn get_data_source( + State(state): S, + Path(id): Path, +) -> Result, StatusCode> +where + C: ConfigRepository, + C::Error: std::fmt::Debug, + E: EventPublisher, + E::Error: std::fmt::Debug, { - let source = state.config.get_data_source(id).await.map_err(|_| StatusCode::INTERNAL_SERVER_ERROR)?; + let source = state + .config + .get_data_source(id) + .await + .map_err(|_| StatusCode::INTERNAL_SERVER_ERROR)?; match source { Some(s) => Ok(Json(DataSourceDto::from(&s))), None => Err(StatusCode::NOT_FOUND), } } -pub async fn create_data_source(State(state): S, Json(body): Json) -> Result -where C: ConfigRepository, C::Error: std::fmt::Debug, E: EventPublisher, E::Error: std::fmt::Debug, +pub async fn create_data_source( + State(state): S, + Json(body): Json, +) -> Result +where + C: ConfigRepository, + C::Error: std::fmt::Debug, + E: EventPublisher, + E::Error: std::fmt::Debug, { - let source = body.into_domain().map_err(|e| (StatusCode::BAD_REQUEST, e))?; + let source = body + .into_domain() + .map_err(|e| (StatusCode::BAD_REQUEST, e))?; let svc = ConfigService::new(state.config.as_ref(), state.events.as_ref()); - svc.create_data_source(source).await.map_err(|e| (StatusCode::INTERNAL_SERVER_ERROR, format!("{e}")))?; + svc.create_data_source(source) + .await + .map_err(|e| (StatusCode::INTERNAL_SERVER_ERROR, format!("{e}")))?; Ok(StatusCode::CREATED) } -pub async fn update_data_source(State(state): S, Path(_id): Path, Json(body): Json) -> Result -where C: ConfigRepository, C::Error: std::fmt::Debug, E: EventPublisher, E::Error: std::fmt::Debug, +pub async fn update_data_source( + State(state): S, + Path(_id): Path, + Json(body): Json, +) -> Result +where + C: ConfigRepository, + C::Error: std::fmt::Debug, + E: EventPublisher, + E::Error: std::fmt::Debug, { - let source = body.into_domain().map_err(|e| (StatusCode::BAD_REQUEST, e))?; + let source = body + .into_domain() + .map_err(|e| (StatusCode::BAD_REQUEST, e))?; let svc = ConfigService::new(state.config.as_ref(), state.events.as_ref()); - svc.update_data_source(source).await.map_err(|e| (StatusCode::INTERNAL_SERVER_ERROR, format!("{e}")))?; + svc.update_data_source(source) + .await + .map_err(|e| (StatusCode::INTERNAL_SERVER_ERROR, format!("{e}")))?; Ok(StatusCode::OK) } -pub async fn delete_data_source(State(state): S, Path(id): Path) -> Result -where C: ConfigRepository, C::Error: std::fmt::Debug, E: EventPublisher, E::Error: std::fmt::Debug, +pub async fn delete_data_source( + State(state): S, + Path(id): Path, +) -> Result +where + C: ConfigRepository, + C::Error: std::fmt::Debug, + E: EventPublisher, + E::Error: std::fmt::Debug, { let svc = ConfigService::new(state.config.as_ref(), state.events.as_ref()); - svc.delete_data_source(id).await.map_err(|_| StatusCode::INTERNAL_SERVER_ERROR)?; + svc.delete_data_source(id) + .await + .map_err(|_| StatusCode::INTERNAL_SERVER_ERROR)?; Ok(StatusCode::NO_CONTENT) } diff --git a/crates/adapters/http-api/src/routes/layout.rs b/crates/adapters/http-api/src/routes/layout.rs index ed5514f..f4e4c53 100644 --- a/crates/adapters/http-api/src/routes/layout.rs +++ b/crates/adapters/http-api/src/routes/layout.rs @@ -1,27 +1,42 @@ -use axum::{ - extract::State, - http::StatusCode, - response::Json, -}; -use domain::{ConfigRepository, EventPublisher}; -use application::ConfigService; use crate::AppState; use api_types::LayoutDto; +use application::ConfigService; +use axum::{extract::State, http::StatusCode, response::Json}; +use domain::{ConfigRepository, EventPublisher}; type S = State>; pub async fn get_layout(State(state): S) -> Result>, StatusCode> -where C: ConfigRepository, C::Error: std::fmt::Debug, E: EventPublisher, E::Error: std::fmt::Debug, +where + C: ConfigRepository, + C::Error: std::fmt::Debug, + E: EventPublisher, + E::Error: std::fmt::Debug, { - let layout = state.config.get_layout().await.map_err(|_| StatusCode::INTERNAL_SERVER_ERROR)?; + let layout = state + .config + .get_layout() + .await + .map_err(|_| StatusCode::INTERNAL_SERVER_ERROR)?; Ok(Json(layout.as_ref().map(LayoutDto::from))) } -pub async fn update_layout(State(state): S, Json(body): Json) -> Result -where C: ConfigRepository, C::Error: std::fmt::Debug, E: EventPublisher, E::Error: std::fmt::Debug, +pub async fn update_layout( + State(state): S, + Json(body): Json, +) -> Result +where + C: ConfigRepository, + C::Error: std::fmt::Debug, + E: EventPublisher, + E::Error: std::fmt::Debug, { - let layout = body.into_domain().map_err(|e| (StatusCode::BAD_REQUEST, e))?; + let layout = body + .into_domain() + .map_err(|e| (StatusCode::BAD_REQUEST, e))?; let svc = ConfigService::new(state.config.as_ref(), state.events.as_ref()); - svc.update_layout(layout).await.map_err(|e| (StatusCode::INTERNAL_SERVER_ERROR, format!("{e}")))?; + svc.update_layout(layout) + .await + .map_err(|e| (StatusCode::INTERNAL_SERVER_ERROR, format!("{e}")))?; Ok(StatusCode::OK) } diff --git a/crates/adapters/http-api/src/routes/mod.rs b/crates/adapters/http-api/src/routes/mod.rs index 646e662..f47b64f 100644 --- a/crates/adapters/http-api/src/routes/mod.rs +++ b/crates/adapters/http-api/src/routes/mod.rs @@ -1,12 +1,12 @@ -mod widgets; mod data_sources; mod layout; mod presets; +mod widgets; -use axum::Router; -use axum::routing::{get, post, put, delete}; -use domain::{ConfigRepository, EventPublisher}; use crate::AppState; +use axum::Router; +use axum::routing::{get, post}; +use domain::{ConfigRepository, EventPublisher}; pub fn api_routes() -> Router> where @@ -16,12 +16,38 @@ where E::Error: std::fmt::Debug + Send, { Router::new() - .route("/widgets", get(widgets::list_widgets::).post(widgets::create_widget::)) - .route("/widgets/{id}", get(widgets::get_widget::).put(widgets::update_widget::).delete(widgets::delete_widget::)) - .route("/data-sources", get(data_sources::list_data_sources::).post(data_sources::create_data_source::)) - .route("/data-sources/{id}", get(data_sources::get_data_source::).put(data_sources::update_data_source::).delete(data_sources::delete_data_source::)) - .route("/layout", get(layout::get_layout::).put(layout::update_layout::)) - .route("/presets", get(presets::list_presets::).post(presets::create_preset::)) - .route("/presets/{id}", get(presets::get_preset::).delete(presets::delete_preset::)) + .route( + "/widgets", + get(widgets::list_widgets::).post(widgets::create_widget::), + ) + .route( + "/widgets/{id}", + get(widgets::get_widget::) + .put(widgets::update_widget::) + .delete(widgets::delete_widget::), + ) + .route( + "/data-sources", + get(data_sources::list_data_sources::) + .post(data_sources::create_data_source::), + ) + .route( + "/data-sources/{id}", + get(data_sources::get_data_source::) + .put(data_sources::update_data_source::) + .delete(data_sources::delete_data_source::), + ) + .route( + "/layout", + get(layout::get_layout::).put(layout::update_layout::), + ) + .route( + "/presets", + get(presets::list_presets::).post(presets::create_preset::), + ) + .route( + "/presets/{id}", + get(presets::get_preset::).delete(presets::delete_preset::), + ) .route("/presets/{id}/load", post(presets::load_preset::)) } diff --git a/crates/adapters/http-api/src/routes/presets.rs b/crates/adapters/http-api/src/routes/presets.rs index 979fa3a..e756b05 100644 --- a/crates/adapters/http-api/src/routes/presets.rs +++ b/crates/adapters/http-api/src/routes/presets.rs @@ -1,53 +1,101 @@ +use crate::AppState; +use api_types::{CreatePresetDto, PresetDto}; +use application::ConfigService; use axum::{ extract::{Path, State}, http::StatusCode, response::Json, }; use domain::{ConfigRepository, EventPublisher}; -use application::ConfigService; -use crate::AppState; -use api_types::{PresetDto, CreatePresetDto}; type S = State>; pub async fn list_presets(State(state): S) -> Result>, StatusCode> -where C: ConfigRepository, C::Error: std::fmt::Debug, E: EventPublisher, E::Error: std::fmt::Debug, +where + C: ConfigRepository, + C::Error: std::fmt::Debug, + E: EventPublisher, + E::Error: std::fmt::Debug, { - let presets = state.config.list_presets().await.map_err(|_| StatusCode::INTERNAL_SERVER_ERROR)?; + let presets = state + .config + .list_presets() + .await + .map_err(|_| StatusCode::INTERNAL_SERVER_ERROR)?; Ok(Json(presets.iter().map(PresetDto::from).collect())) } -pub async fn get_preset(State(state): S, Path(id): Path) -> Result, StatusCode> -where C: ConfigRepository, C::Error: std::fmt::Debug, E: EventPublisher, E::Error: std::fmt::Debug, +pub async fn get_preset( + State(state): S, + Path(id): Path, +) -> Result, StatusCode> +where + C: ConfigRepository, + C::Error: std::fmt::Debug, + E: EventPublisher, + E::Error: std::fmt::Debug, { - let preset = state.config.get_preset(id).await.map_err(|_| StatusCode::INTERNAL_SERVER_ERROR)?; + let preset = state + .config + .get_preset(id) + .await + .map_err(|_| StatusCode::INTERNAL_SERVER_ERROR)?; match preset { Some(p) => Ok(Json(PresetDto::from(&p))), None => Err(StatusCode::NOT_FOUND), } } -pub async fn create_preset(State(state): S, Json(body): Json) -> Result -where C: ConfigRepository, C::Error: std::fmt::Debug, E: EventPublisher, E::Error: std::fmt::Debug, +pub async fn create_preset( + State(state): S, + Json(body): Json, +) -> Result +where + C: ConfigRepository, + C::Error: std::fmt::Debug, + E: EventPublisher, + E::Error: std::fmt::Debug, { - let preset = body.into_domain().map_err(|e| (StatusCode::BAD_REQUEST, e))?; + let preset = body + .into_domain() + .map_err(|e| (StatusCode::BAD_REQUEST, e))?; let svc = ConfigService::new(state.config.as_ref(), state.events.as_ref()); - svc.save_preset(preset).await.map_err(|e| (StatusCode::INTERNAL_SERVER_ERROR, format!("{e}")))?; + svc.save_preset(preset) + .await + .map_err(|e| (StatusCode::INTERNAL_SERVER_ERROR, format!("{e}")))?; Ok(StatusCode::CREATED) } -pub async fn delete_preset(State(state): S, Path(id): Path) -> Result -where C: ConfigRepository, C::Error: std::fmt::Debug, E: EventPublisher, E::Error: std::fmt::Debug, +pub async fn delete_preset( + State(state): S, + Path(id): Path, +) -> Result +where + C: ConfigRepository, + C::Error: std::fmt::Debug, + E: EventPublisher, + E::Error: std::fmt::Debug, { let svc = ConfigService::new(state.config.as_ref(), state.events.as_ref()); - svc.delete_preset(id).await.map_err(|_| StatusCode::INTERNAL_SERVER_ERROR)?; + svc.delete_preset(id) + .await + .map_err(|_| StatusCode::INTERNAL_SERVER_ERROR)?; Ok(StatusCode::NO_CONTENT) } -pub async fn load_preset(State(state): S, Path(id): Path) -> Result -where C: ConfigRepository, C::Error: std::fmt::Debug, E: EventPublisher, E::Error: std::fmt::Debug, +pub async fn load_preset( + State(state): S, + Path(id): Path, +) -> Result +where + C: ConfigRepository, + C::Error: std::fmt::Debug, + E: EventPublisher, + E::Error: std::fmt::Debug, { let svc = ConfigService::new(state.config.as_ref(), state.events.as_ref()); - svc.load_preset(id).await.map_err(|e| (StatusCode::INTERNAL_SERVER_ERROR, format!("{e}")))?; + svc.load_preset(id) + .await + .map_err(|e| (StatusCode::INTERNAL_SERVER_ERROR, format!("{e}")))?; Ok(StatusCode::OK) } diff --git a/crates/adapters/http-api/src/routes/widgets.rs b/crates/adapters/http-api/src/routes/widgets.rs index 19c6aff..6b69565 100644 --- a/crates/adapters/http-api/src/routes/widgets.rs +++ b/crates/adapters/http-api/src/routes/widgets.rs @@ -1,54 +1,105 @@ +use crate::AppState; +use api_types::{CreateWidgetDto, WidgetDto}; +use application::ConfigService; use axum::{ extract::{Path, State}, http::StatusCode, response::Json, }; use domain::{ConfigRepository, EventPublisher}; -use application::ConfigService; -use crate::AppState; -use api_types::{WidgetDto, CreateWidgetDto}; type S = State>; pub async fn list_widgets(State(state): S) -> Result>, StatusCode> -where C: ConfigRepository, C::Error: std::fmt::Debug, E: EventPublisher, E::Error: std::fmt::Debug, +where + C: ConfigRepository, + C::Error: std::fmt::Debug, + E: EventPublisher, + E::Error: std::fmt::Debug, { - let widgets = state.config.list_widgets().await.map_err(|_| StatusCode::INTERNAL_SERVER_ERROR)?; + let widgets = state + .config + .list_widgets() + .await + .map_err(|_| StatusCode::INTERNAL_SERVER_ERROR)?; Ok(Json(widgets.iter().map(WidgetDto::from).collect())) } -pub async fn get_widget(State(state): S, Path(id): Path) -> Result, StatusCode> -where C: ConfigRepository, C::Error: std::fmt::Debug, E: EventPublisher, E::Error: std::fmt::Debug, +pub async fn get_widget( + State(state): S, + Path(id): Path, +) -> Result, StatusCode> +where + C: ConfigRepository, + C::Error: std::fmt::Debug, + E: EventPublisher, + E::Error: std::fmt::Debug, { - let widget = state.config.get_widget(id).await.map_err(|_| StatusCode::INTERNAL_SERVER_ERROR)?; + let widget = state + .config + .get_widget(id) + .await + .map_err(|_| StatusCode::INTERNAL_SERVER_ERROR)?; match widget { Some(w) => Ok(Json(WidgetDto::from(&w))), None => Err(StatusCode::NOT_FOUND), } } -pub async fn create_widget(State(state): S, Json(body): Json) -> Result -where C: ConfigRepository, C::Error: std::fmt::Debug, E: EventPublisher, E::Error: std::fmt::Debug, +pub async fn create_widget( + State(state): S, + Json(body): Json, +) -> Result +where + C: ConfigRepository, + C::Error: std::fmt::Debug, + E: EventPublisher, + E::Error: std::fmt::Debug, { - let widget = body.into_domain().map_err(|e| (StatusCode::BAD_REQUEST, e))?; + let widget = body + .into_domain() + .map_err(|e| (StatusCode::BAD_REQUEST, e))?; let svc = ConfigService::new(state.config.as_ref(), state.events.as_ref()); - svc.create_widget(widget).await.map_err(|e| (StatusCode::INTERNAL_SERVER_ERROR, format!("{e}")))?; + svc.create_widget(widget) + .await + .map_err(|e| (StatusCode::INTERNAL_SERVER_ERROR, format!("{e}")))?; Ok(StatusCode::CREATED) } -pub async fn update_widget(State(state): S, Path(_id): Path, Json(body): Json) -> Result -where C: ConfigRepository, C::Error: std::fmt::Debug, E: EventPublisher, E::Error: std::fmt::Debug, +pub async fn update_widget( + State(state): S, + Path(_id): Path, + Json(body): Json, +) -> Result +where + C: ConfigRepository, + C::Error: std::fmt::Debug, + E: EventPublisher, + E::Error: std::fmt::Debug, { - let widget = body.into_domain().map_err(|e| (StatusCode::BAD_REQUEST, e))?; + let widget = body + .into_domain() + .map_err(|e| (StatusCode::BAD_REQUEST, e))?; let svc = ConfigService::new(state.config.as_ref(), state.events.as_ref()); - svc.update_widget(widget).await.map_err(|e| (StatusCode::INTERNAL_SERVER_ERROR, format!("{e}")))?; + svc.update_widget(widget) + .await + .map_err(|e| (StatusCode::INTERNAL_SERVER_ERROR, format!("{e}")))?; Ok(StatusCode::OK) } -pub async fn delete_widget(State(state): S, Path(id): Path) -> Result -where C: ConfigRepository, C::Error: std::fmt::Debug, E: EventPublisher, E::Error: std::fmt::Debug, +pub async fn delete_widget( + State(state): S, + Path(id): Path, +) -> Result +where + C: ConfigRepository, + C::Error: std::fmt::Debug, + E: EventPublisher, + E::Error: std::fmt::Debug, { let svc = ConfigService::new(state.config.as_ref(), state.events.as_ref()); - svc.delete_widget(id).await.map_err(|_| StatusCode::INTERNAL_SERVER_ERROR)?; + svc.delete_widget(id) + .await + .map_err(|_| StatusCode::INTERNAL_SERVER_ERROR)?; Ok(StatusCode::NO_CONTENT) } diff --git a/crates/adapters/http-api/tests/api_tests.rs b/crates/adapters/http-api/tests/api_tests.rs index ee89238..bb24740 100644 --- a/crates/adapters/http-api/tests/api_tests.rs +++ b/crates/adapters/http-api/tests/api_tests.rs @@ -1,10 +1,10 @@ -use std::sync::Arc; use axum::body::Body; use axum::http::{Request, StatusCode}; -use tower::ServiceExt; use config_memory::MemoryConfigStore; -use tcp_server::TcpEventBus; use http_api::{AppState, router}; +use std::sync::Arc; +use tcp_server::TcpEventBus; +use tower::ServiceExt; fn test_app() -> axum::Router { let config = Arc::new(MemoryConfigStore::new()); @@ -38,13 +38,22 @@ async fn create_and_get_widget() { "mappings": [{"source_path": "$.temp", "target_key": "temperature"}] }"#; - let resp = app.clone().oneshot(json_request("POST", "/api/widgets", Some(body))).await.unwrap(); + let resp = app + .clone() + .oneshot(json_request("POST", "/api/widgets", Some(body))) + .await + .unwrap(); assert_eq!(resp.status(), StatusCode::CREATED); - let resp = app.oneshot(json_request("GET", "/api/widgets/1", None)).await.unwrap(); + let resp = app + .oneshot(json_request("GET", "/api/widgets/1", None)) + .await + .unwrap(); assert_eq!(resp.status(), StatusCode::OK); - let body = axum::body::to_bytes(resp.into_body(), usize::MAX).await.unwrap(); + let body = axum::body::to_bytes(resp.into_body(), usize::MAX) + .await + .unwrap(); let json: serde_json::Value = serde_json::from_slice(&body).unwrap(); assert_eq!(json["name"], "weather"); assert_eq!(json["display_hint"], "icon_value"); @@ -58,11 +67,22 @@ async fn list_widgets() { let w1 = r#"{"id":1,"name":"a","display_hint":"icon_value","data_source_id":1,"mappings":[]}"#; let w2 = r#"{"id":2,"name":"b","display_hint":"key_value","data_source_id":2,"mappings":[]}"#; - app.clone().oneshot(json_request("POST", "/api/widgets", Some(w1))).await.unwrap(); - app.clone().oneshot(json_request("POST", "/api/widgets", Some(w2))).await.unwrap(); + app.clone() + .oneshot(json_request("POST", "/api/widgets", Some(w1))) + .await + .unwrap(); + app.clone() + .oneshot(json_request("POST", "/api/widgets", Some(w2))) + .await + .unwrap(); - let resp = app.oneshot(json_request("GET", "/api/widgets", None)).await.unwrap(); - let body = axum::body::to_bytes(resp.into_body(), usize::MAX).await.unwrap(); + let resp = app + .oneshot(json_request("GET", "/api/widgets", None)) + .await + .unwrap(); + let body = axum::body::to_bytes(resp.into_body(), usize::MAX) + .await + .unwrap(); let json: Vec = serde_json::from_slice(&body).unwrap(); assert_eq!(json.len(), 2); } @@ -71,13 +91,24 @@ async fn list_widgets() { async fn delete_widget() { let app = test_app(); - let body = r#"{"id":1,"name":"a","display_hint":"icon_value","data_source_id":1,"mappings":[]}"#; - app.clone().oneshot(json_request("POST", "/api/widgets", Some(body))).await.unwrap(); + let body = + r#"{"id":1,"name":"a","display_hint":"icon_value","data_source_id":1,"mappings":[]}"#; + app.clone() + .oneshot(json_request("POST", "/api/widgets", Some(body))) + .await + .unwrap(); - let resp = app.clone().oneshot(json_request("DELETE", "/api/widgets/1", None)).await.unwrap(); + let resp = app + .clone() + .oneshot(json_request("DELETE", "/api/widgets/1", None)) + .await + .unwrap(); assert_eq!(resp.status(), StatusCode::NO_CONTENT); - let resp = app.oneshot(json_request("GET", "/api/widgets/1", None)).await.unwrap(); + let resp = app + .oneshot(json_request("GET", "/api/widgets/1", None)) + .await + .unwrap(); assert_eq!(resp.status(), StatusCode::NOT_FOUND); } @@ -95,13 +126,22 @@ async fn create_and_get_data_source() { "headers": [] }"#; - let resp = app.clone().oneshot(json_request("POST", "/api/data-sources", Some(body))).await.unwrap(); + let resp = app + .clone() + .oneshot(json_request("POST", "/api/data-sources", Some(body))) + .await + .unwrap(); assert_eq!(resp.status(), StatusCode::CREATED); - let resp = app.oneshot(json_request("GET", "/api/data-sources/10", None)).await.unwrap(); + let resp = app + .oneshot(json_request("GET", "/api/data-sources/10", None)) + .await + .unwrap(); assert_eq!(resp.status(), StatusCode::OK); - let body = axum::body::to_bytes(resp.into_body(), usize::MAX).await.unwrap(); + let body = axum::body::to_bytes(resp.into_body(), usize::MAX) + .await + .unwrap(); let json: serde_json::Value = serde_json::from_slice(&body).unwrap(); assert_eq!(json["name"], "weather_api"); assert_eq!(json["poll_interval_secs"], 300); @@ -124,13 +164,22 @@ async fn update_and_get_layout() { } }"#; - let resp = app.clone().oneshot(json_request("PUT", "/api/layout", Some(body))).await.unwrap(); + let resp = app + .clone() + .oneshot(json_request("PUT", "/api/layout", Some(body))) + .await + .unwrap(); assert_eq!(resp.status(), StatusCode::OK); - let resp = app.oneshot(json_request("GET", "/api/layout", None)).await.unwrap(); + let resp = app + .oneshot(json_request("GET", "/api/layout", None)) + .await + .unwrap(); assert_eq!(resp.status(), StatusCode::OK); - let body = axum::body::to_bytes(resp.into_body(), usize::MAX).await.unwrap(); + let body = axum::body::to_bytes(resp.into_body(), usize::MAX) + .await + .unwrap(); let json: serde_json::Value = serde_json::from_slice(&body).unwrap(); assert_eq!(json["root"]["type"], "container"); assert_eq!(json["root"]["direction"], "row"); @@ -141,9 +190,16 @@ async fn update_and_get_layout() { async fn get_nonexistent_returns_404() { let app = test_app(); - let resp = app.clone().oneshot(json_request("GET", "/api/widgets/99", None)).await.unwrap(); + let resp = app + .clone() + .oneshot(json_request("GET", "/api/widgets/99", None)) + .await + .unwrap(); assert_eq!(resp.status(), StatusCode::NOT_FOUND); - let resp = app.oneshot(json_request("GET", "/api/data-sources/99", None)).await.unwrap(); + let resp = app + .oneshot(json_request("GET", "/api/data-sources/99", None)) + .await + .unwrap(); assert_eq!(resp.status(), StatusCode::NOT_FOUND); } diff --git a/crates/adapters/http-json/src/lib.rs b/crates/adapters/http-json/src/lib.rs index 7c3540b..5f60b8c 100644 --- a/crates/adapters/http-json/src/lib.rs +++ b/crates/adapters/http-json/src/lib.rs @@ -14,26 +14,32 @@ pub enum HttpJsonError { Parse(String), } -impl HttpJsonAdapter { - pub fn new() -> Self { +impl Default for HttpJsonAdapter { + fn default() -> Self { Self { client: reqwest::Client::new(), } } } +impl HttpJsonAdapter { + pub fn new() -> Self { + Self::default() + } +} + fn json_to_value(json: serde_json::Value) -> Value { match json { serde_json::Value::Null => Value::Null, serde_json::Value::Bool(b) => Value::Bool(b), serde_json::Value::Number(n) => Value::Number(n.as_f64().unwrap_or(0.0)), serde_json::Value::String(s) => Value::String(s), - serde_json::Value::Array(arr) => { - Value::Array(arr.into_iter().map(json_to_value).collect()) - } - serde_json::Value::Object(map) => { - Value::Object(map.into_iter().map(|(k, v)| (k, json_to_value(v))).collect()) - } + serde_json::Value::Array(arr) => Value::Array(arr.into_iter().map(json_to_value).collect()), + serde_json::Value::Object(map) => Value::Object( + map.into_iter() + .map(|(k, v)| (k, json_to_value(v))) + .collect(), + ), } } diff --git a/crates/adapters/http-json/tests/http_json_tests.rs b/crates/adapters/http-json/tests/http_json_tests.rs index 81fc3e3..a843489 100644 --- a/crates/adapters/http-json/tests/http_json_tests.rs +++ b/crates/adapters/http-json/tests/http_json_tests.rs @@ -1,19 +1,23 @@ -use std::time::Duration; -use axum::{Router, routing::get, response::Json}; +use axum::{Router, response::Json, routing::get}; use domain::{DataSource, DataSourceConfig, DataSourcePort, DataSourceType, Value}; use http_json::HttpJsonAdapter; +use std::time::Duration; async fn start_fake_api() -> String { let app = Router::new() - .route("/weather", get(|| async { - Json(serde_json::json!({ - "main": {"temp": 5.4, "humidity": 80}, - "weather": [{"icon": "cloud_rain"}] - })) - })) - .route("/simple", get(|| async { - Json(serde_json::json!({"value": "hello"})) - })) + .route( + "/weather", + get(|| async { + Json(serde_json::json!({ + "main": {"temp": 5.4, "humidity": 80}, + "weather": [{"icon": "cloud_rain"}] + })) + }), + ) + .route( + "/simple", + get(|| async { Json(serde_json::json!({"value": "hello"})) }), + ) .route("/not-json", get(|| async { "plain text" })); let listener = tokio::net::TcpListener::bind("127.0.0.1:0").await.unwrap(); @@ -46,10 +50,7 @@ async fn polls_url_and_returns_nested_json_as_value() { let result = adapter.poll(&source).await.unwrap(); - assert_eq!( - result.get_path("$.main.temp"), - Some(&Value::Number(5.4)) - ); + assert_eq!(result.get_path("$.main.temp"), Some(&Value::Number(5.4))); assert_eq!( result.get_path("$.main.humidity"), Some(&Value::Number(80.0)) diff --git a/crates/adapters/media/Cargo.toml b/crates/adapters/media/Cargo.toml index b28915c..b0f12ac 100644 --- a/crates/adapters/media/Cargo.toml +++ b/crates/adapters/media/Cargo.toml @@ -8,6 +8,8 @@ domain.workspace = true reqwest.workspace = true serde_json.workspace = true thiserror.workspace = true +md5 = "0.7" +fastrand = "2" [dev-dependencies] tokio.workspace = true diff --git a/crates/adapters/media/src/error.rs b/crates/adapters/media/src/error.rs index 98eec85..a6f25a6 100644 --- a/crates/adapters/media/src/error.rs +++ b/crates/adapters/media/src/error.rs @@ -4,6 +4,8 @@ pub enum MediaError { Request(#[from] reqwest::Error), #[error("no url configured")] NoUrl, + #[error("missing field in headers: {0}")] + MissingField(&'static str), #[error("parse: {0}")] Parse(String), } diff --git a/crates/adapters/media/src/lib.rs b/crates/adapters/media/src/lib.rs index c29db1f..4da148c 100644 --- a/crates/adapters/media/src/lib.rs +++ b/crates/adapters/media/src/lib.rs @@ -2,33 +2,61 @@ mod error; pub use error::MediaError; -use std::collections::BTreeMap; use domain::{DataSource, DataSourcePort, Value}; +use std::collections::BTreeMap; pub struct MediaAdapter { client: reqwest::Client, } -impl MediaAdapter { - pub fn new() -> Self { +impl Default for MediaAdapter { + fn default() -> Self { Self { client: reqwest::Client::new(), } } } +impl MediaAdapter { + pub fn new() -> Self { + Self::default() + } +} + +fn find_header<'a>(headers: &'a [(String, String)], key: &str) -> Option<&'a str> { + headers + .iter() + .find(|(k, _)| k.eq_ignore_ascii_case(key)) + .map(|(_, v)| v.as_str()) +} + +fn subsonic_token(password: &str, salt: &str) -> String { + format!("{:x}", md5::compute(format!("{password}{salt}"))) +} + impl DataSourcePort for MediaAdapter { type Error = MediaError; async fn poll(&self, source: &DataSource) -> Result { let base_url = source.config.url.as_ref().ok_or(MediaError::NoUrl)?; - let api_key = source.config.api_key.as_deref().unwrap_or(""); + let username = find_header(&source.config.headers, "username") + .ok_or(MediaError::MissingField("username"))?; + let password = find_header(&source.config.headers, "password") + .ok_or(MediaError::MissingField("password"))?; + + let salt: String = (0..12).map(|_| fastrand::alphanumeric()).collect(); + let token = subsonic_token(password, &salt); let url = format!( - "{base_url}/rest/getNowPlaying.view?u=kframe&t={api_key}&s=salt&v=1.16.1&c=kframe&f=json" + "{base_url}/rest/getNowPlaying.view?u={username}&t={token}&s={salt}&v=1.16.1&c=kframe&f=json" ); - let resp = self.client.get(&url).send().await.map_err(MediaError::Request)?; + let resp = self + .client + .get(&url) + .send() + .await + .map_err(MediaError::Request)?; let json: serde_json::Value = resp.json().await.map_err(MediaError::Request)?; let entries = json["subsonic-response"]["nowPlaying"]["entry"] @@ -45,15 +73,18 @@ impl DataSourcePort for MediaAdapter { let entry = &entries[0]; let mut result = BTreeMap::new(); result.insert("playing".into(), Value::Bool(true)); - result.insert("title".into(), Value::String( - entry["title"].as_str().unwrap_or("Unknown").into() - )); - result.insert("artist".into(), Value::String( - entry["artist"].as_str().unwrap_or("Unknown").into() - )); - result.insert("album".into(), Value::String( - entry["album"].as_str().unwrap_or("Unknown").into() - )); + result.insert( + "title".into(), + Value::String(entry["title"].as_str().unwrap_or("Unknown").into()), + ); + result.insert( + "artist".into(), + Value::String(entry["artist"].as_str().unwrap_or("Unknown").into()), + ); + result.insert( + "album".into(), + Value::String(entry["album"].as_str().unwrap_or("Unknown").into()), + ); if let Some(duration) = entry["duration"].as_u64() { result.insert("duration".into(), Value::Number(duration as f64)); diff --git a/crates/adapters/media/tests/media_tests.rs b/crates/adapters/media/tests/media_tests.rs index 56bbeee..ae3a935 100644 --- a/crates/adapters/media/tests/media_tests.rs +++ b/crates/adapters/media/tests/media_tests.rs @@ -1,6 +1,6 @@ -use std::time::Duration; use domain::{DataSource, DataSourceConfig, DataSourcePort, DataSourceType, Value}; use media_adapter::MediaAdapter; +use std::time::Duration; fn subsonic_response(playing: bool) -> serde_json::Value { if playing { @@ -28,10 +28,10 @@ fn subsonic_response(playing: bool) -> serde_json::Value { } async fn start_fake_subsonic(playing: bool) -> String { - let app = axum::Router::new() - .route("/rest/getNowPlaying.view", axum::routing::get(move || async move { - axum::response::Json(subsonic_response(playing)) - })); + let app = axum::Router::new().route( + "/rest/getNowPlaying.view", + axum::routing::get(move || async move { axum::response::Json(subsonic_response(playing)) }), + ); let listener = tokio::net::TcpListener::bind("127.0.0.1:0").await.unwrap(); let addr = listener.local_addr().unwrap(); @@ -47,8 +47,11 @@ fn make_source(url: String) -> DataSource { poll_interval: Duration::from_secs(5), config: DataSourceConfig { url: Some(url), - headers: vec![], - api_key: Some("testtoken".into()), + headers: vec![ + ("username".into(), "test".into()), + ("password".into(), "testpass".into()), + ], + api_key: None, }, } } @@ -62,8 +65,14 @@ async fn returns_now_playing_info() { let result = adapter.poll(&source).await.unwrap(); assert_eq!(result.get_path("$.playing"), Some(&Value::Bool(true))); - assert_eq!(result.get_path("$.title"), Some(&Value::String("Believer".into()))); - assert_eq!(result.get_path("$.artist"), Some(&Value::String("Imagine Dragons".into()))); + assert_eq!( + result.get_path("$.title"), + Some(&Value::String("Believer".into())) + ); + assert_eq!( + result.get_path("$.artist"), + Some(&Value::String("Imagine Dragons".into())) + ); } #[tokio::test] diff --git a/crates/adapters/rss/src/lib.rs b/crates/adapters/rss/src/lib.rs index f27c39f..4830cad 100644 --- a/crates/adapters/rss/src/lib.rs +++ b/crates/adapters/rss/src/lib.rs @@ -10,21 +10,32 @@ pub struct RssAdapter { client: reqwest::Client, } -impl RssAdapter { - pub fn new() -> Self { +impl Default for RssAdapter { + fn default() -> Self { Self { client: reqwest::Client::new(), } } } +impl RssAdapter { + pub fn new() -> Self { + Self::default() + } +} + impl DataSourcePort for RssAdapter { type Error = RssError; async fn poll(&self, source: &DataSource) -> Result { let url = source.config.url.as_ref().ok_or(RssError::NoUrl)?; - let resp = self.client.get(url).send().await.map_err(RssError::Request)?; + let resp = self + .client + .get(url) + .send() + .await + .map_err(RssError::Request)?; let xml = resp.text().await.map_err(RssError::Request)?; parser::parse_rss(&xml) diff --git a/crates/adapters/rss/src/parser.rs b/crates/adapters/rss/src/parser.rs index a8072db..34727b3 100644 --- a/crates/adapters/rss/src/parser.rs +++ b/crates/adapters/rss/src/parser.rs @@ -29,10 +29,10 @@ pub fn parse_rss(xml: &str) -> Result { } Ok(Event::End(e)) => { let tag = String::from_utf8_lossy(e.name().as_ref()).to_string(); - if tag == "item" { - if let Some(item) = current_item.take() { - items.push(Value::Object(item)); - } + if tag == "item" + && let Some(item) = current_item.take() + { + items.push(Value::Object(item)); } current_tag.clear(); } @@ -52,10 +52,10 @@ pub fn parse_rss(xml: &str) -> Result { } Ok(Event::CData(e)) => { let text = String::from_utf8_lossy(&e).to_string(); - if !current_tag.is_empty() { - if let Some(item) = current_item.as_mut() { - item.insert(current_tag.clone(), Value::String(text)); - } + if !current_tag.is_empty() + && let Some(item) = current_item.as_mut() + { + item.insert(current_tag.clone(), Value::String(text)); } } Ok(Event::Eof) => break, diff --git a/crates/adapters/rss/tests/parser_tests.rs b/crates/adapters/rss/tests/parser_tests.rs index 8163a3d..b4b8aa1 100644 --- a/crates/adapters/rss/tests/parser_tests.rs +++ b/crates/adapters/rss/tests/parser_tests.rs @@ -1,5 +1,5 @@ use domain::Value; -use rss_adapter::{parse_rss, RssError}; +use rss_adapter::{RssError, parse_rss}; const SAMPLE_RSS: &str = r#" @@ -23,8 +23,20 @@ const SAMPLE_RSS: &str = r#" fn parses_rss_into_value() { let result = parse_rss(SAMPLE_RSS).unwrap(); - assert_eq!(result.get_path("$.title"), Some(&Value::String("Test Feed".into()))); - assert_eq!(result.get_path("$.items[0].title"), Some(&Value::String("First Article".into()))); - assert_eq!(result.get_path("$.items[1].title"), Some(&Value::String("Second Article".into()))); - assert_eq!(result.get_path("$.items[0].description"), Some(&Value::String("Description of first article".into()))); + assert_eq!( + result.get_path("$.title"), + Some(&Value::String("Test Feed".into())) + ); + assert_eq!( + result.get_path("$.items[0].title"), + Some(&Value::String("First Article".into())) + ); + assert_eq!( + result.get_path("$.items[1].title"), + Some(&Value::String("Second Article".into())) + ); + assert_eq!( + result.get_path("$.items[0].description"), + Some(&Value::String("Description of first article".into())) + ); } diff --git a/crates/adapters/tcp-client/src/lib.rs b/crates/adapters/tcp-client/src/lib.rs index 9fcc2e3..2e2f299 100644 --- a/crates/adapters/tcp-client/src/lib.rs +++ b/crates/adapters/tcp-client/src/lib.rs @@ -1,8 +1,8 @@ +use client_domain::NetworkPort; +use protocol::MAX_FRAME_SIZE; use std::io::{Read, Write}; use std::net::TcpStream; use std::time::Duration; -use client_domain::NetworkPort; -use protocol::MAX_FRAME_SIZE; #[derive(Debug, thiserror::Error)] pub enum TcpClientError { @@ -14,13 +14,14 @@ pub enum TcpClientError { FrameTooLarge(usize), } +#[derive(Default)] pub struct StdTcpClient { stream: Option, } impl StdTcpClient { pub fn new() -> Self { - Self { stream: None } + Self::default() } } @@ -30,7 +31,9 @@ impl NetworkPort for StdTcpClient { fn connect(&mut self, addr: &str) -> Result<(), Self::Error> { let stream = TcpStream::connect(addr).map_err(TcpClientError::Io)?; stream.set_nonblocking(true).map_err(TcpClientError::Io)?; - stream.set_read_timeout(Some(Duration::from_millis(10))).map_err(TcpClientError::Io)?; + stream + .set_read_timeout(Some(Duration::from_millis(10))) + .map_err(TcpClientError::Io)?; self.stream = Some(stream); Ok(()) } @@ -63,7 +66,9 @@ impl NetworkPort for StdTcpClient { let mut payload = vec![0u8; len]; stream.set_nonblocking(false).map_err(TcpClientError::Io)?; - stream.read_exact(&mut payload).map_err(TcpClientError::Io)?; + stream + .read_exact(&mut payload) + .map_err(TcpClientError::Io)?; stream.set_nonblocking(true).map_err(TcpClientError::Io)?; Ok(Some(payload)) diff --git a/crates/adapters/tcp-server/src/broadcaster.rs b/crates/adapters/tcp-server/src/broadcaster.rs index d90716e..cf4fb5c 100644 --- a/crates/adapters/tcp-server/src/broadcaster.rs +++ b/crates/adapters/tcp-server/src/broadcaster.rs @@ -1,12 +1,7 @@ -use tokio::sync::broadcast; -use domain::{ - BroadcastPort, Layout, WidgetId, WidgetState, -}; -use protocol::{ - ServerMessage, WidgetDescriptor, WireDisplayHint, WireLayoutNode, - encode, -}; use crate::error::TcpServerError; +use domain::{BroadcastPort, Layout, WidgetId, WidgetState}; +use protocol::{ServerMessage, WidgetDescriptor, WireDisplayHint, WireLayoutNode, encode}; +use tokio::sync::broadcast; pub struct TcpBroadcaster { tx: broadcast::Sender>, @@ -37,13 +32,14 @@ impl BroadcastPort for TcpBroadcaster { widgets: &[(WidgetId, WidgetState)], ) -> Result<(), Self::Error> { let wire_layout: WireLayoutNode = (&layout.root).into(); - let wire_widgets: Vec = widgets.iter().map(|(id, state)| { - WidgetDescriptor { + let wire_widgets: Vec = widgets + .iter() + .map(|(id, state)| WidgetDescriptor { id: *id, display_hint: WireDisplayHint::IconValue, state: state.into(), - } - }).collect(); + }) + .collect(); let msg = ServerMessage::ScreenUpdate { layout: wire_layout, @@ -58,13 +54,14 @@ impl BroadcastPort for TcpBroadcaster { &self, updates: &[(WidgetId, WidgetState)], ) -> Result<(), Self::Error> { - let wire_widgets: Vec = updates.iter().map(|(id, state)| { - WidgetDescriptor { + let wire_widgets: Vec = updates + .iter() + .map(|(id, state)| WidgetDescriptor { id: *id, display_hint: WireDisplayHint::IconValue, state: state.into(), - } - }).collect(); + }) + .collect(); let msg = ServerMessage::DataUpdate { widgets: wire_widgets, diff --git a/crates/adapters/tcp-server/src/event_bus.rs b/crates/adapters/tcp-server/src/event_bus.rs index e88f6d1..fd48e14 100644 --- a/crates/adapters/tcp-server/src/event_bus.rs +++ b/crates/adapters/tcp-server/src/event_bus.rs @@ -1,6 +1,6 @@ -use tokio::sync::broadcast; -use domain::{EventPublisher, DomainEvent}; use crate::error::TcpServerError; +use domain::{DomainEvent, EventPublisher}; +use tokio::sync::broadcast; pub struct TcpEventBus { tx: broadcast::Sender, diff --git a/crates/adapters/tcp-server/src/lib.rs b/crates/adapters/tcp-server/src/lib.rs index ab1f12d..5bd425e 100644 --- a/crates/adapters/tcp-server/src/lib.rs +++ b/crates/adapters/tcp-server/src/lib.rs @@ -1,9 +1,9 @@ -mod error; mod broadcaster; +mod error; mod event_bus; mod server; -pub use error::TcpServerError; pub use broadcaster::TcpBroadcaster; +pub use error::TcpServerError; pub use event_bus::TcpEventBus; pub use server::run_tcp_server; diff --git a/crates/adapters/tcp-server/src/server.rs b/crates/adapters/tcp-server/src/server.rs index 33b1b0d..c30c981 100644 --- a/crates/adapters/tcp-server/src/server.rs +++ b/crates/adapters/tcp-server/src/server.rs @@ -1,10 +1,10 @@ -use std::sync::Arc; -use tokio::net::TcpListener; -use tokio::sync::broadcast; -use tokio::io::AsyncWriteExt; -use tracing::{info, warn}; use crate::broadcaster::TcpBroadcaster; use crate::error::TcpServerError; +use std::sync::Arc; +use tokio::io::AsyncWriteExt; +use tokio::net::TcpListener; +use tokio::sync::broadcast; +use tracing::{info, warn}; pub async fn run_tcp_server( addr: &str, diff --git a/crates/api-types/src/data_source.rs b/crates/api-types/src/data_source.rs index b4c67da..636ac57 100644 --- a/crates/api-types/src/data_source.rs +++ b/crates/api-types/src/data_source.rs @@ -1,6 +1,6 @@ -use serde::{Serialize, Deserialize}; -use std::time::Duration; use domain::*; +use serde::{Deserialize, Serialize}; +use std::time::Duration; #[derive(Serialize, Deserialize)] pub struct DataSourceDto { @@ -21,11 +21,12 @@ impl From<&DataSource> for DataSourceDto { source_type: match ds.source_type { DataSourceType::Weather => "weather", DataSourceType::Media => "media", - DataSourceType::Xtb => "xtb", + DataSourceType::Rss => "rss", DataSourceType::HttpJson => "http_json", DataSourceType::Webhook => "webhook", - }.into(), + } + .into(), poll_interval_secs: ds.poll_interval.as_secs(), url: ds.config.url.clone(), api_key: ds.config.api_key.clone(), @@ -39,7 +40,7 @@ impl DataSourceDto { let source_type = match self.source_type.as_str() { "weather" => DataSourceType::Weather, "media" => DataSourceType::Media, - "xtb" => DataSourceType::Xtb, + "rss" => DataSourceType::Rss, "http_json" => DataSourceType::HttpJson, "webhook" => DataSourceType::Webhook, diff --git a/crates/api-types/src/layout.rs b/crates/api-types/src/layout.rs index fad084c..8f5ec75 100644 --- a/crates/api-types/src/layout.rs +++ b/crates/api-types/src/layout.rs @@ -1,5 +1,5 @@ -use serde::{Serialize, Deserialize}; use domain::*; +use serde::{Deserialize, Serialize}; #[derive(Serialize, Deserialize)] pub struct SizingDto { @@ -41,30 +41,41 @@ impl From<&LayoutNode> for LayoutNodeDto { LayoutNode::Leaf(id) => Self { node_type: "leaf".into(), widget_id: Some(*id), - direction: None, gap: None, padding: None, children: None, + direction: None, + gap: None, + padding: None, + children: None, }, LayoutNode::Container(c) => Self { node_type: "container".into(), widget_id: None, - direction: Some(match c.direction { - Direction::Row => "row", - Direction::Column => "column", - }.into()), + direction: Some( + match c.direction { + Direction::Row => "row", + Direction::Column => "column", + } + .into(), + ), gap: Some(c.gap), padding: Some(c.padding), - children: Some(c.children.iter().map(|ch| LayoutChildDto { - sizing: SizingDto { - sizing_type: match ch.sizing { - Sizing::Fixed(_) => "fixed".into(), - Sizing::Flex(_) => "flex".into(), - }, - value: match ch.sizing { - Sizing::Fixed(v) => v, - Sizing::Flex(v) => v as u16, - }, - }, - node: (&ch.node).into(), - }).collect()), + children: Some( + c.children + .iter() + .map(|ch| LayoutChildDto { + sizing: SizingDto { + sizing_type: match ch.sizing { + Sizing::Fixed(_) => "fixed".into(), + Sizing::Flex(_) => "flex".into(), + }, + value: match ch.sizing { + Sizing::Fixed(v) => v, + Sizing::Flex(v) => v as u16, + }, + }, + node: (&ch.node).into(), + }) + .collect(), + ), }, } } @@ -83,7 +94,9 @@ impl LayoutNodeDto { "column" => Direction::Column, d => return Err(format!("unknown direction: {d}")), }; - let children = self.children.ok_or("missing children")? + let children = self + .children + .ok_or("missing children")? .into_iter() .map(|ch| { let sizing = match ch.sizing.sizing_type.as_str() { @@ -110,12 +123,16 @@ impl LayoutNodeDto { impl From<&Layout> for LayoutDto { fn from(l: &Layout) -> Self { - Self { root: (&l.root).into() } + Self { + root: (&l.root).into(), + } } } impl LayoutDto { pub fn into_domain(self) -> Result { - Ok(Layout { root: self.root.into_domain()? }) + Ok(Layout { + root: self.root.into_domain()?, + }) } } diff --git a/crates/api-types/src/lib.rs b/crates/api-types/src/lib.rs index 3d945ed..7ac4c8d 100644 --- a/crates/api-types/src/lib.rs +++ b/crates/api-types/src/lib.rs @@ -1,9 +1,9 @@ -pub mod widget; pub mod data_source; pub mod layout; pub mod preset; +pub mod widget; -pub use widget::{KeyMappingDto, WidgetDto, CreateWidgetDto}; pub use data_source::DataSourceDto; -pub use layout::{LayoutDto, LayoutNodeDto, LayoutChildDto, SizingDto}; -pub use preset::{PresetDto, CreatePresetDto}; +pub use layout::{LayoutChildDto, LayoutDto, LayoutNodeDto, SizingDto}; +pub use preset::{CreatePresetDto, PresetDto}; +pub use widget::{CreateWidgetDto, KeyMappingDto, WidgetDto}; diff --git a/crates/api-types/src/preset.rs b/crates/api-types/src/preset.rs index 62f46a7..d5ff3e3 100644 --- a/crates/api-types/src/preset.rs +++ b/crates/api-types/src/preset.rs @@ -1,6 +1,6 @@ -use serde::{Serialize, Deserialize}; -use domain::*; use crate::layout::LayoutDto; +use domain::*; +use serde::{Deserialize, Serialize}; #[derive(Serialize, Deserialize)] pub struct PresetDto { diff --git a/crates/api-types/src/widget.rs b/crates/api-types/src/widget.rs index cd9bbc4..d15292b 100644 --- a/crates/api-types/src/widget.rs +++ b/crates/api-types/src/widget.rs @@ -1,5 +1,5 @@ -use serde::{Serialize, Deserialize}; use domain::*; +use serde::{Deserialize, Serialize}; #[derive(Serialize, Deserialize)] pub struct KeyMappingDto { @@ -28,7 +28,9 @@ pub struct CreateWidgetDto { pub max_data_size: u16, } -fn default_max_data_size() -> u16 { 2048 } +fn default_max_data_size() -> u16 { + 2048 +} impl From<&WidgetConfig> for WidgetDto { fn from(w: &WidgetConfig) -> Self { @@ -39,12 +41,17 @@ impl From<&WidgetConfig> for WidgetDto { DisplayHint::IconValue => "icon_value", DisplayHint::TextBlock => "text_block", DisplayHint::KeyValue => "key_value", - }.into(), + } + .into(), data_source_id: w.data_source_id, - mappings: w.mappings.iter().map(|m| KeyMappingDto { - source_path: m.source_path.clone(), - target_key: m.target_key.clone(), - }).collect(), + mappings: w + .mappings + .iter() + .map(|m| KeyMappingDto { + source_path: m.source_path.clone(), + target_key: m.target_key.clone(), + }) + .collect(), max_data_size: w.max_data_size, } } @@ -63,10 +70,14 @@ impl CreateWidgetDto { name: self.name, display_hint: hint, data_source_id: self.data_source_id, - mappings: self.mappings.into_iter().map(|m| KeyMapping { - source_path: m.source_path, - target_key: m.target_key, - }).collect(), + mappings: self + .mappings + .into_iter() + .map(|m| KeyMapping { + source_path: m.source_path, + target_key: m.target_key, + }) + .collect(), max_data_size: self.max_data_size, }) } diff --git a/crates/application/src/config_service.rs b/crates/application/src/config_service.rs index 806a0b0..1cd6cc5 100644 --- a/crates/application/src/config_service.rs +++ b/crates/application/src/config_service.rs @@ -1,10 +1,8 @@ -use std::fmt; use domain::{ - ConfigRepository, EventPublisher, DomainEvent, - WidgetConfig, WidgetId, - DataSource, DataSourceId, DataSourceValidationError, - Layout, LayoutPreset, LayoutPresetId, + ConfigRepository, DataSource, DataSourceId, DataSourceValidationError, DomainEvent, + EventPublisher, Layout, LayoutPreset, LayoutPresetId, WidgetConfig, WidgetId, }; +use std::fmt; pub struct ConfigService<'a, C, E> { config: &'a C, @@ -34,78 +32,173 @@ where Self { config, events } } - pub async fn create_widget(&self, widget: WidgetConfig) -> Result<(), ConfigError> { - self.config.save_widget(&widget).await.map_err(ConfigError::Repository)?; - self.events.publish(DomainEvent::WidgetCreated { id: widget.id }).await.map_err(ConfigError::Event)?; + pub async fn create_widget( + &self, + widget: WidgetConfig, + ) -> Result<(), ConfigError> { + self.config + .save_widget(&widget) + .await + .map_err(ConfigError::Repository)?; + self.events + .publish(DomainEvent::WidgetCreated { id: widget.id }) + .await + .map_err(ConfigError::Event)?; Ok(()) } - pub async fn update_widget(&self, widget: WidgetConfig) -> Result<(), ConfigError> { - self.config.save_widget(&widget).await.map_err(ConfigError::Repository)?; - self.events.publish(DomainEvent::WidgetUpdated { id: widget.id }).await.map_err(ConfigError::Event)?; + pub async fn update_widget( + &self, + widget: WidgetConfig, + ) -> Result<(), ConfigError> { + self.config + .save_widget(&widget) + .await + .map_err(ConfigError::Repository)?; + self.events + .publish(DomainEvent::WidgetUpdated { id: widget.id }) + .await + .map_err(ConfigError::Event)?; Ok(()) } pub async fn delete_widget(&self, id: WidgetId) -> Result<(), ConfigError> { - self.config.delete_widget(id).await.map_err(ConfigError::Repository)?; - self.events.publish(DomainEvent::WidgetDeleted { id }).await.map_err(ConfigError::Event)?; + self.config + .delete_widget(id) + .await + .map_err(ConfigError::Repository)?; + self.events + .publish(DomainEvent::WidgetDeleted { id }) + .await + .map_err(ConfigError::Event)?; Ok(()) } - pub async fn create_data_source(&self, source: DataSource) -> Result<(), ConfigError> { + pub async fn create_data_source( + &self, + source: DataSource, + ) -> Result<(), ConfigError> { let errors = source.validate(); if !errors.is_empty() { return Err(ConfigError::Validation(errors)); } - self.config.save_data_source(&source).await.map_err(ConfigError::Repository)?; - self.events.publish(DomainEvent::DataSourceAdded { id: source.id }).await.map_err(ConfigError::Event)?; + self.config + .save_data_source(&source) + .await + .map_err(ConfigError::Repository)?; + self.events + .publish(DomainEvent::DataSourceAdded { id: source.id }) + .await + .map_err(ConfigError::Event)?; Ok(()) } - pub async fn update_data_source(&self, source: DataSource) -> Result<(), ConfigError> { + pub async fn update_data_source( + &self, + source: DataSource, + ) -> Result<(), ConfigError> { let errors = source.validate(); if !errors.is_empty() { return Err(ConfigError::Validation(errors)); } - self.config.save_data_source(&source).await.map_err(ConfigError::Repository)?; - self.events.publish(DomainEvent::DataSourceUpdated { id: source.id }).await.map_err(ConfigError::Event)?; + self.config + .save_data_source(&source) + .await + .map_err(ConfigError::Repository)?; + self.events + .publish(DomainEvent::DataSourceUpdated { id: source.id }) + .await + .map_err(ConfigError::Event)?; Ok(()) } - pub async fn delete_data_source(&self, id: DataSourceId) -> Result<(), ConfigError> { - self.config.delete_data_source(id).await.map_err(ConfigError::Repository)?; - self.events.publish(DomainEvent::DataSourceRemoved { id }).await.map_err(ConfigError::Event)?; + pub async fn delete_data_source( + &self, + id: DataSourceId, + ) -> Result<(), ConfigError> { + self.config + .delete_data_source(id) + .await + .map_err(ConfigError::Repository)?; + self.events + .publish(DomainEvent::DataSourceRemoved { id }) + .await + .map_err(ConfigError::Event)?; Ok(()) } - pub async fn update_layout(&self, layout: Layout) -> Result<(), ConfigError> { - self.config.save_layout(&layout).await.map_err(ConfigError::Repository)?; - self.events.publish(DomainEvent::LayoutChanged { layout }).await.map_err(ConfigError::Event)?; + pub async fn update_layout( + &self, + layout: Layout, + ) -> Result<(), ConfigError> { + self.config + .save_layout(&layout) + .await + .map_err(ConfigError::Repository)?; + self.events + .publish(DomainEvent::LayoutChanged { layout }) + .await + .map_err(ConfigError::Event)?; Ok(()) } - pub async fn save_preset(&self, preset: LayoutPreset) -> Result<(), ConfigError> { - self.config.save_preset(&preset).await.map_err(ConfigError::Repository)?; - self.events.publish(DomainEvent::LayoutPresetSaved { id: preset.id }).await.map_err(ConfigError::Event)?; + pub async fn save_preset( + &self, + preset: LayoutPreset, + ) -> Result<(), ConfigError> { + self.config + .save_preset(&preset) + .await + .map_err(ConfigError::Repository)?; + self.events + .publish(DomainEvent::LayoutPresetSaved { id: preset.id }) + .await + .map_err(ConfigError::Event)?; Ok(()) } - pub async fn load_preset(&self, id: LayoutPresetId) -> Result<(), ConfigError> { - let preset = self.config.get_preset(id).await + pub async fn load_preset( + &self, + id: LayoutPresetId, + ) -> Result<(), ConfigError> { + let preset = self + .config + .get_preset(id) + .await .map_err(ConfigError::Repository)? .ok_or(ConfigError::NotFound)?; - self.events.publish(DomainEvent::LayoutPresetLoaded { id }).await.map_err(ConfigError::Event)?; + self.events + .publish(DomainEvent::LayoutPresetLoaded { id }) + .await + .map_err(ConfigError::Event)?; - self.config.save_layout(&preset.layout).await.map_err(ConfigError::Repository)?; - self.events.publish(DomainEvent::LayoutChanged { layout: preset.layout }).await.map_err(ConfigError::Event)?; + self.config + .save_layout(&preset.layout) + .await + .map_err(ConfigError::Repository)?; + self.events + .publish(DomainEvent::LayoutChanged { + layout: preset.layout, + }) + .await + .map_err(ConfigError::Event)?; Ok(()) } - pub async fn delete_preset(&self, id: LayoutPresetId) -> Result<(), ConfigError> { - self.config.delete_preset(id).await.map_err(ConfigError::Repository)?; - self.events.publish(DomainEvent::LayoutPresetDeleted { id }).await.map_err(ConfigError::Event)?; + pub async fn delete_preset( + &self, + id: LayoutPresetId, + ) -> Result<(), ConfigError> { + self.config + .delete_preset(id) + .await + .map_err(ConfigError::Repository)?; + self.events + .publish(DomainEvent::LayoutPresetDeleted { id }) + .await + .map_err(ConfigError::Event)?; Ok(()) } } diff --git a/crates/application/src/data_projection.rs b/crates/application/src/data_projection.rs index ce0c894..deb526f 100644 --- a/crates/application/src/data_projection.rs +++ b/crates/application/src/data_projection.rs @@ -1,15 +1,18 @@ -use std::collections::HashMap; use domain::{DataSourceId, Value, WidgetConfig, WidgetId, WidgetState}; +use std::collections::HashMap; +#[derive(Default)] pub struct DataProjection { current: HashMap, } impl DataProjection { pub fn new() -> Self { - Self { - current: HashMap::new(), - } + Self::default() + } + + pub fn get_state(&self, widget_id: WidgetId) -> Option<&WidgetState> { + self.current.get(&widget_id) } pub fn apply_poll_result( @@ -27,9 +30,10 @@ impl DataProjection { let new_state = config.extract(raw); - let is_changed = self.current + let is_changed = self + .current .get(&config.id) - .map_or(true, |prev| *prev != new_state); + .is_none_or(|prev| *prev != new_state); if is_changed { self.current.insert(config.id, new_state.clone()); diff --git a/crates/application/tests/config_service_tests.rs b/crates/application/tests/config_service_tests.rs index 304c7dd..f193ae0 100644 --- a/crates/application/tests/config_service_tests.rs +++ b/crates/application/tests/config_service_tests.rs @@ -1,13 +1,12 @@ mod support; -use std::time::Duration; -use domain::{ - ConfigRepository, DisplayHint, DomainEvent, KeyMapping, WidgetConfig, - DataSource, DataSourceConfig, DataSourceType, - Layout, LayoutNode, ContainerNode, LayoutChild, Direction, Sizing, - LayoutPreset, -}; use application::ConfigService; +use domain::{ + ConfigRepository, ContainerNode, DataSource, DataSourceConfig, DataSourceType, Direction, + DisplayHint, DomainEvent, KeyMapping, Layout, LayoutChild, LayoutNode, LayoutPreset, Sizing, + WidgetConfig, +}; +use std::time::Duration; use support::{InMemoryConfigRepository, InMemoryEventPublisher}; #[tokio::test] @@ -21,9 +20,10 @@ async fn create_widget_persists_and_emits_event() { "weather".into(), DisplayHint::IconValue, 1, - vec![ - KeyMapping { source_path: "$.temp".into(), target_key: "temperature".into() }, - ], + vec![KeyMapping { + source_path: "$.temp".into(), + target_key: "temperature".into(), + }], ); service.create_widget(config).await.unwrap(); @@ -99,8 +99,14 @@ async fn update_layout_persists_and_emits_event() { gap: 4, padding: 2, children: vec![ - LayoutChild { sizing: Sizing::Flex(1), node: LayoutNode::Leaf(1) }, - LayoutChild { sizing: Sizing::Flex(1), node: LayoutNode::Leaf(2) }, + LayoutChild { + sizing: Sizing::Flex(1), + node: LayoutNode::Leaf(1), + }, + LayoutChild { + sizing: Sizing::Flex(1), + node: LayoutNode::Leaf(2), + }, ], }), }; @@ -111,7 +117,10 @@ async fn update_layout_persists_and_emits_event() { assert_eq!(stored, Some(layout)); assert_eq!(events.emitted().len(), 1); - assert!(matches!(events.emitted()[0], DomainEvent::LayoutChanged { .. })); + assert!(matches!( + events.emitted()[0], + DomainEvent::LayoutChanged { .. } + )); } #[tokio::test] @@ -125,9 +134,10 @@ async fn load_preset_replaces_active_layout() { direction: Direction::Column, gap: 0, padding: 0, - children: vec![ - LayoutChild { sizing: Sizing::Flex(1), node: LayoutNode::Leaf(5) }, - ], + children: vec![LayoutChild { + sizing: Sizing::Flex(1), + node: LayoutNode::Leaf(5), + }], }), }; @@ -146,6 +156,9 @@ async fn load_preset_replaces_active_layout() { let emitted = events.emitted(); assert_eq!(emitted.len(), 2); - assert!(matches!(emitted[0], DomainEvent::LayoutPresetLoaded { id: 1 })); + assert!(matches!( + emitted[0], + DomainEvent::LayoutPresetLoaded { id: 1 } + )); assert!(matches!(emitted[1], DomainEvent::LayoutChanged { .. })); } diff --git a/crates/application/tests/data_projection_tests.rs b/crates/application/tests/data_projection_tests.rs index 4797a6a..4c6d869 100644 --- a/crates/application/tests/data_projection_tests.rs +++ b/crates/application/tests/data_projection_tests.rs @@ -1,8 +1,6 @@ -use std::collections::BTreeMap; -use domain::{ - DisplayHint, KeyMapping, Value, WidgetConfig, WidgetId, WidgetState, -}; use application::DataProjection; +use domain::{DisplayHint, KeyMapping, Value, WidgetConfig, WidgetId, WidgetState}; +use std::collections::BTreeMap; fn weather_widget() -> WidgetConfig { WidgetConfig::new( @@ -11,8 +9,14 @@ fn weather_widget() -> WidgetConfig { DisplayHint::IconValue, 10, vec![ - KeyMapping { source_path: "$.temp".into(), target_key: "temperature".into() }, - KeyMapping { source_path: "$.icon".into(), target_key: "icon".into() }, + KeyMapping { + source_path: "$.temp".into(), + target_key: "temperature".into(), + }, + KeyMapping { + source_path: "$.icon".into(), + target_key: "icon".into(), + }, ], ) } @@ -33,7 +37,10 @@ fn apply_poll_result_detects_new_widget_state() { assert_eq!(changed.len(), 1); assert_eq!(changed[0].0, 1); - assert_eq!(changed[0].1.data.get("temperature"), Some(&Value::Number(5.4))); + assert_eq!( + changed[0].1.data.get("temperature"), + Some(&Value::Number(5.4)) + ); } #[test] @@ -56,7 +63,10 @@ fn apply_poll_result_detects_changed_value() { let changed = projection.apply_poll_result(10, &weather_response(6.1), &widgets); assert_eq!(changed.len(), 1); - assert_eq!(changed[0].1.data.get("temperature"), Some(&Value::Number(6.1))); + assert_eq!( + changed[0].1.data.get("temperature"), + Some(&Value::Number(6.1)) + ); } #[test] @@ -69,9 +79,10 @@ fn apply_poll_result_only_updates_widgets_bound_to_source() { "portfolio".into(), DisplayHint::KeyValue, 20, - vec![ - KeyMapping { source_path: "$.value".into(), target_key: "amount".into() }, - ], + vec![KeyMapping { + source_path: "$.value".into(), + target_key: "amount".into(), + }], ), ]; diff --git a/crates/application/tests/support/mod.rs b/crates/application/tests/support/mod.rs index 41fc30c..4027bf5 100644 --- a/crates/application/tests/support/mod.rs +++ b/crates/application/tests/support/mod.rs @@ -1,10 +1,9 @@ -use std::sync::Mutex; -use std::collections::HashMap; use domain::{ - ConfigRepository, EventPublisher, - DataSource, DataSourceId, Layout, LayoutPreset, LayoutPresetId, - WidgetConfig, WidgetId, DomainEvent, + ConfigRepository, DataSource, DataSourceId, DomainEvent, EventPublisher, Layout, LayoutPreset, + LayoutPresetId, WidgetConfig, WidgetId, }; +use std::collections::HashMap; +use std::sync::Mutex; pub struct InMemoryConfigRepository { widgets: Mutex>, @@ -45,7 +44,10 @@ impl ConfigRepository for InMemoryConfigRepository { } async fn save_widget(&self, config: &WidgetConfig) -> Result<(), Self::Error> { - self.widgets.lock().unwrap().insert(config.id, config.clone()); + self.widgets + .lock() + .unwrap() + .insert(config.id, config.clone()); Ok(()) } @@ -59,11 +61,20 @@ impl ConfigRepository for InMemoryConfigRepository { } async fn list_data_sources(&self) -> Result, Self::Error> { - Ok(self.data_sources.lock().unwrap().values().cloned().collect()) + Ok(self + .data_sources + .lock() + .unwrap() + .values() + .cloned() + .collect()) } async fn save_data_source(&self, source: &DataSource) -> Result<(), Self::Error> { - self.data_sources.lock().unwrap().insert(source.id, source.clone()); + self.data_sources + .lock() + .unwrap() + .insert(source.id, source.clone()); Ok(()) } @@ -90,7 +101,10 @@ impl ConfigRepository for InMemoryConfigRepository { } async fn save_preset(&self, preset: &LayoutPreset) -> Result<(), Self::Error> { - self.presets.lock().unwrap().insert(preset.id, preset.clone()); + self.presets + .lock() + .unwrap() + .insert(preset.id, preset.clone()); Ok(()) } diff --git a/crates/bootstrap/Cargo.toml b/crates/bootstrap/Cargo.toml index 4abfde2..ff80512 100644 --- a/crates/bootstrap/Cargo.toml +++ b/crates/bootstrap/Cargo.toml @@ -10,6 +10,8 @@ config-sqlite.workspace = true tcp-server.workspace = true http-api.workspace = true http-json.workspace = true +media-adapter.workspace = true +rss-adapter.workspace = true tokio.workspace = true anyhow.workspace = true tracing.workspace = true diff --git a/crates/bootstrap/src/config.rs b/crates/bootstrap/src/config.rs index 5a52e60..c3a0393 100644 --- a/crates/bootstrap/src/config.rs +++ b/crates/bootstrap/src/config.rs @@ -12,10 +12,8 @@ impl ServerConfig { Self { database_url: env::var("KFRAME_DATABASE_URL") .unwrap_or_else(|_| "sqlite:kframe.db?mode=rwc".into()), - tcp_addr: env::var("KFRAME_TCP_ADDR") - .unwrap_or_else(|_| "0.0.0.0:2699".into()), - http_addr: env::var("KFRAME_HTTP_ADDR") - .unwrap_or_else(|_| "0.0.0.0:3000".into()), + tcp_addr: env::var("KFRAME_TCP_ADDR").unwrap_or_else(|_| "0.0.0.0:2699".into()), + http_addr: env::var("KFRAME_HTTP_ADDR").unwrap_or_else(|_| "0.0.0.0:3000".into()), poll_interval_secs: env::var("KFRAME_POLL_INTERVAL_SECS") .ok() .and_then(|v| v.parse().ok()) diff --git a/crates/bootstrap/src/event_handler.rs b/crates/bootstrap/src/event_handler.rs new file mode 100644 index 0000000..83d7d65 --- /dev/null +++ b/crates/bootstrap/src/event_handler.rs @@ -0,0 +1,53 @@ +use application::DataProjection; +use config_sqlite::SqliteConfigStore; +use domain::{BroadcastPort, ConfigRepository, DomainEvent}; +use std::sync::Arc; +use tcp_server::{TcpBroadcaster, TcpEventBus}; +use tokio::sync::Mutex; +use tracing::{error, info, warn}; + +pub async fn run( + event_bus: Arc, + config: Arc, + broadcaster: Arc, + projection: Arc>, +) { + let mut rx = event_bus.subscribe(); + + loop { + match rx.recv().await { + Ok(DomainEvent::LayoutChanged { layout }) => { + let widgets = match config.list_widgets().await { + Ok(w) => w, + Err(e) => { + error!(error = %e, "failed to fetch widgets for screen update"); + continue; + } + }; + + let proj = projection.lock().await; + let widget_states: Vec<_> = widgets + .iter() + .filter_map(|w| proj.get_state(w.id).map(|s| (w.id, s.clone()))) + .collect(); + + if let Err(e) = broadcaster + .push_screen_update(&layout, &widget_states) + .await + { + error!(error = %e, "failed to push screen update"); + } + + info!("layout changed, pushed screen update to clients"); + } + Ok(_) => {} + Err(tokio::sync::broadcast::error::RecvError::Lagged(n)) => { + warn!(skipped = n, "event handler lagged, missed events"); + } + Err(tokio::sync::broadcast::error::RecvError::Closed) => { + error!("event bus closed"); + break; + } + } + } +} diff --git a/crates/bootstrap/src/main.rs b/crates/bootstrap/src/main.rs index 8a76008..7787b1e 100644 --- a/crates/bootstrap/src/main.rs +++ b/crates/bootstrap/src/main.rs @@ -1,14 +1,15 @@ mod config; +mod event_handler; mod polling; -use std::sync::Arc; use anyhow::Result; use application::DataProjection; use config_sqlite::SqliteConfigStore; -use tcp_server::{TcpBroadcaster, TcpEventBus, run_tcp_server}; use http_api::AppState; +use std::sync::Arc; +use tcp_server::{TcpBroadcaster, TcpEventBus, run_tcp_server}; use tokio::sync::Mutex; -use tracing::{info, error}; +use tracing::{error, info}; #[tokio::main] async fn main() -> Result<()> { @@ -53,5 +54,19 @@ async fn main() -> Result<()> { info!("K-Frame server running"); - polling::run(config_store, broadcaster, projection, cfg.poll_interval_secs).await + let ev_bus = event_bus.clone(); + let ev_config = config_store.clone(); + let ev_bc = broadcaster.clone(); + let ev_proj = projection.clone(); + tokio::spawn(async move { + event_handler::run(ev_bus, ev_config, ev_bc, ev_proj).await; + }); + + polling::run( + config_store, + broadcaster, + projection, + cfg.poll_interval_secs, + ) + .await } diff --git a/crates/bootstrap/src/polling.rs b/crates/bootstrap/src/polling.rs index b13b9ad..d91a985 100644 --- a/crates/bootstrap/src/polling.rs +++ b/crates/bootstrap/src/polling.rs @@ -1,16 +1,17 @@ +use anyhow::Result; +use application::DataProjection; +use config_sqlite::SqliteConfigStore; +use domain::{ + BroadcastPort, ConfigRepository, DataSource, DataSourcePort, DataSourceType, Value, WidgetState, +}; +use http_json::HttpJsonAdapter; +use media_adapter::MediaAdapter; +use rss_adapter::RssAdapter; use std::sync::Arc; use std::time::Duration; -use anyhow::Result; -use domain::{ - ConfigRepository, BroadcastPort, DataSourcePort, DataSourceType, - DataSource, Value, WidgetState, -}; -use application::DataProjection; -use http_json::HttpJsonAdapter; use tcp_server::TcpBroadcaster; -use config_sqlite::SqliteConfigStore; use tokio::sync::Mutex; -use tracing::{info, warn, debug}; +use tracing::{debug, info, warn}; pub async fn run( config: Arc, @@ -19,6 +20,8 @@ pub async fn run( poll_interval_secs: u64, ) -> Result<()> { let http_adapter = HttpJsonAdapter::new(); + let media_adapter = MediaAdapter::new(); + let rss_adapter = RssAdapter::new(); let interval = Duration::from_secs(poll_interval_secs); info!(interval_secs = poll_interval_secs, "polling loop started"); @@ -26,11 +29,17 @@ pub async fn run( loop { tokio::time::sleep(interval).await; - let sources = config.list_data_sources().await + let sources = config + .list_data_sources() + .await .map_err(|e| anyhow::anyhow!("{e}"))?; - let widgets = config.list_widgets().await + let widgets = config + .list_widgets() + .await .map_err(|e| anyhow::anyhow!("{e}"))?; - let layout = config.get_layout().await + let layout = config + .get_layout() + .await .map_err(|e| anyhow::anyhow!("{e}"))?; if sources.is_empty() || widgets.is_empty() { @@ -41,13 +50,14 @@ pub async fn run( let mut all_changed: Vec<(u16, WidgetState)> = Vec::new(); for source in &sources { - let result = match poll_source(&http_adapter, source).await { - Ok(v) => v, - Err(e) => { - warn!(source = %source.name, error = %e, "poll failed"); - continue; - } - }; + let result = + match poll_source(&http_adapter, &media_adapter, &rss_adapter, source).await { + Ok(v) => v, + Err(e) => { + warn!(source = %source.name, error = %e, "poll failed"); + continue; + } + }; let mut proj = projection.lock().await; let changed = proj.apply_poll_result(source.id, &result, &widgets); @@ -56,7 +66,9 @@ pub async fn run( if !all_changed.is_empty() { if let Some(l) = &layout { - broadcaster.push_screen_update(l, &all_changed).await + broadcaster + .push_screen_update(l, &all_changed) + .await .map_err(|e| anyhow::anyhow!("{e}"))?; } info!(count = all_changed.len(), "pushed widget updates"); @@ -66,15 +78,25 @@ pub async fn run( async fn poll_source( http_adapter: &HttpJsonAdapter, + media_adapter: &MediaAdapter, + rss_adapter: &RssAdapter, source: &DataSource, ) -> Result { match source.source_type { - DataSourceType::HttpJson | DataSourceType::Weather => { - http_adapter.poll(source).await - .map_err(|e| anyhow::anyhow!("{e}")) - } - _ => { - Err(anyhow::anyhow!("unsupported source type: {:?}", source.source_type)) - } + DataSourceType::HttpJson | DataSourceType::Weather => http_adapter + .poll(source) + .await + .map_err(|e| anyhow::anyhow!("{e}")), + DataSourceType::Media => media_adapter + .poll(source) + .await + .map_err(|e| anyhow::anyhow!("{e}")), + DataSourceType::Rss => rss_adapter + .poll(source) + .await + .map_err(|e| anyhow::anyhow!("{e}")), + DataSourceType::Webhook => Err(anyhow::anyhow!( + "webhook sources are push-based, not polled" + )), } } diff --git a/crates/client-application/src/client_app.rs b/crates/client-application/src/client_app.rs index 56f12de..3b4a12b 100644 --- a/crates/client-application/src/client_app.rs +++ b/crates/client-application/src/client_app.rs @@ -1,9 +1,7 @@ -use std::collections::HashMap; -use domain::LayoutNode; use client_domain::{BoundingBox, LayoutEngine, RenderTree}; -use protocol::{ - ServerMessage, WidgetDescriptor, WireDisplayHint, WireWidgetState, WireLayoutNode, -}; +use domain::LayoutNode; +use protocol::{ServerMessage, WidgetDescriptor, WireDisplayHint, WireLayoutNode, WireWidgetState}; +use std::collections::HashMap; pub struct ClientApp { screen: BoundingBox, @@ -33,9 +31,7 @@ impl ClientApp { ServerMessage::ScreenUpdate { layout, widgets } => { self.handle_screen_update(layout, widgets) } - ServerMessage::DataUpdate { widgets } => { - self.handle_data_update(widgets) - } + ServerMessage::DataUpdate { widgets } => self.handle_data_update(widgets), ServerMessage::Heartbeat => Vec::new(), } } @@ -50,7 +46,8 @@ impl ClientApp { self.widget_states.clear(); for w in &widgets { - self.widget_states.insert(w.id, (w.display_hint.clone(), w.state.clone())); + self.widget_states + .insert(w.id, (w.display_hint.clone(), w.state.clone())); } let repaints = self.build_repaints_for_all(&new_tree); @@ -58,10 +55,7 @@ impl ClientApp { repaints } - fn handle_data_update( - &mut self, - widgets: Vec, - ) -> Vec { + fn handle_data_update(&mut self, widgets: Vec) -> Vec { let tree = match &self.render_tree { Some(t) => t, None => return Vec::new(), @@ -70,9 +64,10 @@ impl ClientApp { let mut repaints = Vec::new(); for w in widgets { - let changed = self.widget_states + let changed = self + .widget_states .get(&w.id) - .map_or(true, |(_, prev_state)| *prev_state != w.state); + .is_none_or(|(_, prev_state)| *prev_state != w.state); if changed { if let Some(bounds) = tree.get_widget_bounds(w.id) { diff --git a/crates/client-application/tests/client_app_tests.rs b/crates/client-application/tests/client_app_tests.rs index bf185ae..5c51181 100644 --- a/crates/client-application/tests/client_app_tests.rs +++ b/crates/client-application/tests/client_app_tests.rs @@ -1,9 +1,8 @@ use client_application::{ClientApp, RepaintCommand}; use client_domain::BoundingBox; use protocol::{ - ServerMessage, WidgetDescriptor, - WireDisplayHint, WireLayoutNode, WireContainerNode, WireLayoutChild, - WireDirection, WireSizing, WireWidgetState, WireKeyValue, WireValue, + ServerMessage, WidgetDescriptor, WireContainerNode, WireDirection, WireDisplayHint, + WireKeyValue, WireLayoutChild, WireLayoutNode, WireSizing, WireValue, WireWidgetState, }; fn screen() -> BoundingBox { @@ -15,9 +14,10 @@ fn weather_descriptor(id: u16, temp: &str) -> WidgetDescriptor { id, display_hint: WireDisplayHint::IconValue, state: WireWidgetState { - data: vec![ - WireKeyValue { key: "temperature".into(), value: WireValue::String(temp.into()) }, - ], + data: vec![WireKeyValue { + key: "temperature".into(), + value: WireValue::String(temp.into()), + }], error: None, }, } @@ -29,8 +29,14 @@ fn two_widget_layout() -> WireLayoutNode { gap: 0, padding: 0, children: vec![ - WireLayoutChild { sizing: WireSizing::Flex(1), node: WireLayoutNode::Leaf(1) }, - WireLayoutChild { sizing: WireSizing::Flex(1), node: WireLayoutNode::Leaf(2) }, + WireLayoutChild { + sizing: WireSizing::Flex(1), + node: WireLayoutNode::Leaf(1), + }, + WireLayoutChild { + sizing: WireSizing::Flex(1), + node: WireLayoutNode::Leaf(2), + }, ], }) } @@ -116,8 +122,14 @@ fn second_screen_update_repaints_all_widgets_with_new_layout() { gap: 0, padding: 0, children: vec![ - WireLayoutChild { sizing: WireSizing::Flex(1), node: WireLayoutNode::Leaf(1) }, - WireLayoutChild { sizing: WireSizing::Flex(1), node: WireLayoutNode::Leaf(2) }, + WireLayoutChild { + sizing: WireSizing::Flex(1), + node: WireLayoutNode::Leaf(1), + }, + WireLayoutChild { + sizing: WireSizing::Flex(1), + node: WireLayoutNode::Leaf(2), + }, ], }); diff --git a/crates/client-desktop/src/main.rs b/crates/client-desktop/src/main.rs index 6db2f8a..5fa7738 100644 --- a/crates/client-desktop/src/main.rs +++ b/crates/client-desktop/src/main.rs @@ -1,11 +1,11 @@ -use std::thread; -use std::sync::mpsc; -use std::time::Duration; -use client_domain::{BoundingBox, DisplayPort, NetworkPort}; use client_application::ClientApp; -use tcp_client::StdTcpClient; +use client_domain::{BoundingBox, DisplayPort, NetworkPort}; use display_terminal::TerminalDisplay; use protocol::decode_server_message; +use std::sync::mpsc; +use std::thread; +use std::time::Duration; +use tcp_client::StdTcpClient; fn main() { let screen = BoundingBox::screen(240, 320); @@ -35,12 +35,12 @@ fn main() { } match net.receive() { - Ok(Some(payload)) => { - match decode_server_message(&payload) { - Ok(msg) => { let _ = tx.send(msg); } - Err(e) => println!("[NET] Decode error: {e}"), + Ok(Some(payload)) => match decode_server_message(&payload) { + Ok(msg) => { + let _ = tx.send(msg); } - } + Err(e) => println!("[NET] Decode error: {e}"), + }, Ok(None) => { thread::sleep(Duration::from_millis(50)); } @@ -67,11 +67,14 @@ fn main() { for kv in &cmd.state.data { if let protocol::WireValue::String(s) = &kv.value { - display.draw_text( - &format!("{}: {s}", kv.key), - cmd.bounds.x, cmd.bounds.y, - cmd.bounds, - ).unwrap(); + display + .draw_text( + &format!("{}: {s}", kv.key), + cmd.bounds.x, + cmd.bounds.y, + cmd.bounds, + ) + .unwrap(); } } } diff --git a/crates/client-domain/src/bounding_box.rs b/crates/client-domain/src/bounding_box.rs index d67d36c..525f72d 100644 --- a/crates/client-domain/src/bounding_box.rs +++ b/crates/client-domain/src/bounding_box.rs @@ -8,10 +8,20 @@ pub struct BoundingBox { impl BoundingBox { pub fn new(x: u16, y: u16, width: u16, height: u16) -> Self { - Self { x, y, width, height } + Self { + x, + y, + width, + height, + } } pub fn screen(width: u16, height: u16) -> Self { - Self { x: 0, y: 0, width, height } + Self { + x: 0, + y: 0, + width, + height, + } } } diff --git a/crates/client-domain/src/layout_engine.rs b/crates/client-domain/src/layout_engine.rs index d938592..bde2606 100644 --- a/crates/client-domain/src/layout_engine.rs +++ b/crates/client-domain/src/layout_engine.rs @@ -1,6 +1,6 @@ -use std::collections::HashMap; -use domain::{LayoutNode, ContainerNode, Direction, Sizing}; use crate::{BoundingBox, RenderTree}; +use domain::{ContainerNode, Direction, LayoutNode, Sizing}; +use std::collections::HashMap; pub struct LayoutEngine; @@ -11,11 +11,7 @@ impl LayoutEngine { RenderTree { widget_bounds } } - fn compute_node( - node: &LayoutNode, - bounds: BoundingBox, - out: &mut HashMap, - ) { + fn compute_node(node: &LayoutNode, bounds: BoundingBox, out: &mut HashMap) { match node { LayoutNode::Leaf(id) => { out.insert(*id, bounds); @@ -48,16 +44,22 @@ impl LayoutEngine { let total_gap = container.gap as u16 * (children.len() as u16).saturating_sub(1); let available = total_axis.saturating_sub(total_gap); - let fixed_total: u16 = children.iter().map(|c| match c.sizing { - Sizing::Fixed(px) => px, - Sizing::Flex(_) => 0, - }).sum(); + let fixed_total: u16 = children + .iter() + .map(|c| match c.sizing { + Sizing::Fixed(px) => px, + Sizing::Flex(_) => 0, + }) + .sum(); let flex_space = available.saturating_sub(fixed_total); - let flex_total: u16 = children.iter().map(|c| match c.sizing { - Sizing::Flex(w) => w as u16, - Sizing::Fixed(_) => 0, - }).sum(); + let flex_total: u16 = children + .iter() + .map(|c| match c.sizing { + Sizing::Flex(w) => w as u16, + Sizing::Fixed(_) => 0, + }) + .sum(); let mut offset = 0u16; @@ -74,19 +76,9 @@ impl LayoutEngine { }; let child_bounds = if is_row { - BoundingBox::new( - inner.x + offset, - inner.y, - child_size, - inner.height, - ) + BoundingBox::new(inner.x + offset, inner.y, child_size, inner.height) } else { - BoundingBox::new( - inner.x, - inner.y + offset, - inner.width, - child_size, - ) + BoundingBox::new(inner.x, inner.y + offset, inner.width, child_size) }; Self::compute_node(&child.node, child_bounds, out); diff --git a/crates/client-domain/src/lib.rs b/crates/client-domain/src/lib.rs index 8419a63..9a38c4d 100644 --- a/crates/client-domain/src/lib.rs +++ b/crates/client-domain/src/lib.rs @@ -1,9 +1,9 @@ mod bounding_box; mod layout_engine; -mod render_tree; pub mod ports; +mod render_tree; pub use bounding_box::BoundingBox; pub use layout_engine::LayoutEngine; +pub use ports::{ClientConfig, DisplayPort, NetworkPort, StoragePort}; pub use render_tree::RenderTree; -pub use ports::{DisplayPort, NetworkPort, StoragePort, ClientConfig}; diff --git a/crates/client-domain/src/ports/display.rs b/crates/client-domain/src/ports/display.rs index 4dd7df9..68867af 100644 --- a/crates/client-domain/src/ports/display.rs +++ b/crates/client-domain/src/ports/display.rs @@ -4,7 +4,13 @@ pub trait DisplayPort { type Error; fn clear_region(&mut self, bounds: BoundingBox) -> Result<(), Self::Error>; - fn draw_text(&mut self, text: &str, x: u16, y: u16, bounds: BoundingBox) -> Result<(), Self::Error>; + fn draw_text( + &mut self, + text: &str, + x: u16, + y: u16, + bounds: BoundingBox, + ) -> Result<(), Self::Error>; fn draw_icon(&mut self, icon: &str, x: u16, y: u16) -> Result<(), Self::Error>; fn fill_background(&mut self, bounds: BoundingBox) -> Result<(), Self::Error>; fn flush(&mut self) -> Result<(), Self::Error>; diff --git a/crates/client-domain/src/ports/mod.rs b/crates/client-domain/src/ports/mod.rs index 948ef16..9e5b042 100644 --- a/crates/client-domain/src/ports/mod.rs +++ b/crates/client-domain/src/ports/mod.rs @@ -4,4 +4,4 @@ mod storage; pub use display::DisplayPort; pub use network::NetworkPort; -pub use storage::{StoragePort, ClientConfig}; +pub use storage::{ClientConfig, StoragePort}; diff --git a/crates/client-domain/src/render_tree.rs b/crates/client-domain/src/render_tree.rs index 3fc0ad2..54b03bb 100644 --- a/crates/client-domain/src/render_tree.rs +++ b/crates/client-domain/src/render_tree.rs @@ -1,6 +1,6 @@ -use std::collections::HashMap; -use domain::WidgetId; use crate::BoundingBox; +use domain::WidgetId; +use std::collections::HashMap; pub struct RenderTree { pub widget_bounds: HashMap, diff --git a/crates/client-domain/tests/layout_engine_tests.rs b/crates/client-domain/tests/layout_engine_tests.rs index d6b5cf3..7a611f6 100644 --- a/crates/client-domain/tests/layout_engine_tests.rs +++ b/crates/client-domain/tests/layout_engine_tests.rs @@ -1,7 +1,5 @@ -use domain::{ - LayoutNode, ContainerNode, LayoutChild, Direction, Sizing, -}; use client_domain::{BoundingBox, LayoutEngine, RenderTree}; +use domain::{ContainerNode, Direction, LayoutChild, LayoutNode, Sizing}; fn screen() -> BoundingBox { BoundingBox::screen(240, 320) @@ -73,9 +71,18 @@ fn row_splits_width_among_equal_flex_children() { let layout = row(vec![leaf(1), leaf(2), leaf(3)]); let tree = LayoutEngine::compute(&layout, screen()); - assert_eq!(tree.get_widget_bounds(1), Some(&BoundingBox::new(0, 0, 80, 320))); - assert_eq!(tree.get_widget_bounds(2), Some(&BoundingBox::new(80, 0, 80, 320))); - assert_eq!(tree.get_widget_bounds(3), Some(&BoundingBox::new(160, 0, 80, 320))); + assert_eq!( + tree.get_widget_bounds(1), + Some(&BoundingBox::new(0, 0, 80, 320)) + ); + assert_eq!( + tree.get_widget_bounds(2), + Some(&BoundingBox::new(80, 0, 80, 320)) + ); + assert_eq!( + tree.get_widget_bounds(3), + Some(&BoundingBox::new(160, 0, 80, 320)) + ); } #[test] @@ -83,8 +90,14 @@ fn column_splits_height_among_equal_flex_children() { let layout = column(vec![leaf(1), leaf(2)]); let tree = LayoutEngine::compute(&layout, screen()); - assert_eq!(tree.get_widget_bounds(1), Some(&BoundingBox::new(0, 0, 240, 160))); - assert_eq!(tree.get_widget_bounds(2), Some(&BoundingBox::new(0, 160, 240, 160))); + assert_eq!( + tree.get_widget_bounds(1), + Some(&BoundingBox::new(0, 0, 240, 160)) + ); + assert_eq!( + tree.get_widget_bounds(2), + Some(&BoundingBox::new(0, 160, 240, 160)) + ); } #[test] @@ -92,8 +105,14 @@ fn fixed_and_flex_children_coexist() { let layout = row(vec![leaf_fixed(1, 40), leaf(2)]); let tree = LayoutEngine::compute(&layout, screen()); - assert_eq!(tree.get_widget_bounds(1), Some(&BoundingBox::new(0, 0, 40, 320))); - assert_eq!(tree.get_widget_bounds(2), Some(&BoundingBox::new(40, 0, 200, 320))); + assert_eq!( + tree.get_widget_bounds(1), + Some(&BoundingBox::new(0, 0, 40, 320)) + ); + assert_eq!( + tree.get_widget_bounds(2), + Some(&BoundingBox::new(40, 0, 200, 320)) + ); } #[test] @@ -102,8 +121,14 @@ fn gap_is_subtracted_before_distributing_space() { let layout = row_with_gap(10, vec![leaf(1), leaf(2)]); let tree = LayoutEngine::compute(&layout, screen()); - assert_eq!(tree.get_widget_bounds(1), Some(&BoundingBox::new(0, 0, 115, 320))); - assert_eq!(tree.get_widget_bounds(2), Some(&BoundingBox::new(125, 0, 115, 320))); + assert_eq!( + tree.get_widget_bounds(1), + Some(&BoundingBox::new(0, 0, 115, 320)) + ); + assert_eq!( + tree.get_widget_bounds(2), + Some(&BoundingBox::new(125, 0, 115, 320)) + ); } #[test] @@ -112,7 +137,10 @@ fn padding_insets_available_area() { let layout = row_with_padding(10, vec![leaf(1)]); let tree = LayoutEngine::compute(&layout, screen()); - assert_eq!(tree.get_widget_bounds(1), Some(&BoundingBox::new(10, 10, 220, 300))); + assert_eq!( + tree.get_widget_bounds(1), + Some(&BoundingBox::new(10, 10, 220, 300)) + ); } #[test] @@ -125,9 +153,18 @@ fn nested_containers_compute_correctly() { let layout = row(vec![leaf(1), inner_col]); let tree = LayoutEngine::compute(&layout, screen()); - assert_eq!(tree.get_widget_bounds(1), Some(&BoundingBox::new(0, 0, 120, 320))); - assert_eq!(tree.get_widget_bounds(2), Some(&BoundingBox::new(120, 0, 120, 160))); - assert_eq!(tree.get_widget_bounds(3), Some(&BoundingBox::new(120, 160, 120, 160))); + assert_eq!( + tree.get_widget_bounds(1), + Some(&BoundingBox::new(0, 0, 120, 320)) + ); + assert_eq!( + tree.get_widget_bounds(2), + Some(&BoundingBox::new(120, 0, 120, 160)) + ); + assert_eq!( + tree.get_widget_bounds(3), + Some(&BoundingBox::new(120, 160, 120, 160)) + ); } #[test] @@ -138,14 +175,32 @@ fn weighted_flex_distributes_proportionally() { gap: 0, padding: 0, children: vec![ - LayoutChild { sizing: Sizing::Flex(1), node: LayoutNode::Leaf(1) }, - LayoutChild { sizing: Sizing::Flex(2), node: LayoutNode::Leaf(2) }, - LayoutChild { sizing: Sizing::Flex(1), node: LayoutNode::Leaf(3) }, + LayoutChild { + sizing: Sizing::Flex(1), + node: LayoutNode::Leaf(1), + }, + LayoutChild { + sizing: Sizing::Flex(2), + node: LayoutNode::Leaf(2), + }, + LayoutChild { + sizing: Sizing::Flex(1), + node: LayoutNode::Leaf(3), + }, ], }); let tree = LayoutEngine::compute(&layout, screen()); - assert_eq!(tree.get_widget_bounds(1), Some(&BoundingBox::new(0, 0, 60, 320))); - assert_eq!(tree.get_widget_bounds(2), Some(&BoundingBox::new(60, 0, 120, 320))); - assert_eq!(tree.get_widget_bounds(3), Some(&BoundingBox::new(180, 0, 60, 320))); + assert_eq!( + tree.get_widget_bounds(1), + Some(&BoundingBox::new(0, 0, 60, 320)) + ); + assert_eq!( + tree.get_widget_bounds(2), + Some(&BoundingBox::new(60, 0, 120, 320)) + ); + assert_eq!( + tree.get_widget_bounds(3), + Some(&BoundingBox::new(180, 0, 60, 320)) + ); } diff --git a/crates/client-domain/tests/render_tree_tests.rs b/crates/client-domain/tests/render_tree_tests.rs index ca0bd77..a399e00 100644 --- a/crates/client-domain/tests/render_tree_tests.rs +++ b/crates/client-domain/tests/render_tree_tests.rs @@ -1,7 +1,5 @@ -use domain::{ - LayoutNode, ContainerNode, LayoutChild, Direction, Sizing, -}; use client_domain::{BoundingBox, LayoutEngine}; +use domain::{ContainerNode, Direction, LayoutChild, LayoutNode, Sizing}; fn screen() -> BoundingBox { BoundingBox::screen(240, 320) @@ -14,8 +12,14 @@ fn diff_detects_moved_widget_after_layout_change() { gap: 0, padding: 0, children: vec![ - LayoutChild { sizing: Sizing::Flex(1), node: LayoutNode::Leaf(1) }, - LayoutChild { sizing: Sizing::Flex(1), node: LayoutNode::Leaf(2) }, + LayoutChild { + sizing: Sizing::Flex(1), + node: LayoutNode::Leaf(1), + }, + LayoutChild { + sizing: Sizing::Flex(1), + node: LayoutNode::Leaf(2), + }, ], }); @@ -24,8 +28,14 @@ fn diff_detects_moved_widget_after_layout_change() { gap: 0, padding: 0, children: vec![ - LayoutChild { sizing: Sizing::Flex(1), node: LayoutNode::Leaf(1) }, - LayoutChild { sizing: Sizing::Flex(1), node: LayoutNode::Leaf(2) }, + LayoutChild { + sizing: Sizing::Flex(1), + node: LayoutNode::Leaf(1), + }, + LayoutChild { + sizing: Sizing::Flex(1), + node: LayoutNode::Leaf(2), + }, ], }); @@ -44,8 +54,14 @@ fn diff_returns_empty_for_identical_layouts() { gap: 0, padding: 0, children: vec![ - LayoutChild { sizing: Sizing::Flex(1), node: LayoutNode::Leaf(1) }, - LayoutChild { sizing: Sizing::Flex(1), node: LayoutNode::Leaf(2) }, + LayoutChild { + sizing: Sizing::Flex(1), + node: LayoutNode::Leaf(1), + }, + LayoutChild { + sizing: Sizing::Flex(1), + node: LayoutNode::Leaf(2), + }, ], }); @@ -61,18 +77,20 @@ fn diff_detects_added_and_removed_widgets() { direction: Direction::Row, gap: 0, padding: 0, - children: vec![ - LayoutChild { sizing: Sizing::Flex(1), node: LayoutNode::Leaf(1) }, - ], + children: vec![LayoutChild { + sizing: Sizing::Flex(1), + node: LayoutNode::Leaf(1), + }], }); let layout_b = LayoutNode::Container(ContainerNode { direction: Direction::Row, gap: 0, padding: 0, - children: vec![ - LayoutChild { sizing: Sizing::Flex(1), node: LayoutNode::Leaf(2) }, - ], + children: vec![LayoutChild { + sizing: Sizing::Flex(1), + node: LayoutNode::Leaf(2), + }], }); let tree_a = LayoutEngine::compute(&layout_a, screen()); diff --git a/crates/domain/src/entities/data_source.rs b/crates/domain/src/entities/data_source.rs index 0cfbf15..40ac6be 100644 --- a/crates/domain/src/entities/data_source.rs +++ b/crates/domain/src/entities/data_source.rs @@ -6,7 +6,6 @@ pub type DataSourceId = u16; pub enum DataSourceType { Weather, Media, - Xtb, Rss, HttpJson, Webhook, diff --git a/crates/domain/src/entities/mod.rs b/crates/domain/src/entities/mod.rs index 1e6b981..38672ad 100644 --- a/crates/domain/src/entities/mod.rs +++ b/crates/domain/src/entities/mod.rs @@ -1,7 +1,9 @@ -mod widget_config; mod data_source; mod layout_preset; +mod widget_config; -pub use widget_config::{WidgetConfig, WidgetId}; -pub use data_source::{DataSource, DataSourceId, DataSourceType, DataSourceConfig, DataSourceValidationError}; +pub use data_source::{ + DataSource, DataSourceConfig, DataSourceId, DataSourceType, DataSourceValidationError, +}; pub use layout_preset::{LayoutPreset, LayoutPresetId}; +pub use widget_config::{WidgetConfig, WidgetId}; diff --git a/crates/domain/src/entities/widget_config.rs b/crates/domain/src/entities/widget_config.rs index 8e5e6c1..c1f5d03 100644 --- a/crates/domain/src/entities/widget_config.rs +++ b/crates/domain/src/entities/widget_config.rs @@ -1,5 +1,5 @@ -use std::collections::BTreeMap; use crate::value_objects::{DisplayHint, KeyMapping, Value, WidgetState}; +use std::collections::BTreeMap; pub type WidgetId = u16; pub type DataSourceId = u16; @@ -58,7 +58,8 @@ impl WidgetConfig { fn truncate_value(value: Value, max_bytes: usize) -> Value { match value { Value::String(s) if s.len() > max_bytes => { - let truncated: String = s.char_indices() + let truncated: String = s + .char_indices() .take_while(|(i, _)| *i < max_bytes) .map(|(_, c)| c) .collect(); diff --git a/crates/domain/src/lib.rs b/crates/domain/src/lib.rs index c635da8..3b2556c 100644 --- a/crates/domain/src/lib.rs +++ b/crates/domain/src/lib.rs @@ -1,19 +1,17 @@ #![allow(async_fn_in_trait)] pub mod entities; -pub mod value_objects; pub mod events; pub mod ports; +pub mod value_objects; pub use entities::{ - WidgetConfig, WidgetId, - DataSource, DataSourceId, DataSourceType, DataSourceConfig, DataSourceValidationError, - LayoutPreset, LayoutPresetId, -}; -pub use value_objects::{ - Value, KeyMapping, - WidgetState, WidgetError, DisplayHint, - Layout, LayoutNode, LayoutChild, ContainerNode, Direction, Sizing, LayoutValidationError, + DataSource, DataSourceConfig, DataSourceId, DataSourceType, DataSourceValidationError, + LayoutPreset, LayoutPresetId, WidgetConfig, WidgetId, }; pub use events::DomainEvent; -pub use ports::{ConfigRepository, DataSourcePort, BroadcastPort, EventPublisher}; +pub use ports::{BroadcastPort, ConfigRepository, DataSourcePort, EventPublisher}; +pub use value_objects::{ + ContainerNode, Direction, DisplayHint, KeyMapping, Layout, LayoutChild, LayoutNode, + LayoutValidationError, Sizing, Value, WidgetError, WidgetState, +}; diff --git a/crates/domain/src/ports/broadcast.rs b/crates/domain/src/ports/broadcast.rs index 6d194e5..80d3be5 100644 --- a/crates/domain/src/ports/broadcast.rs +++ b/crates/domain/src/ports/broadcast.rs @@ -1,6 +1,6 @@ -use std::future::Future; use crate::entities::WidgetId; use crate::value_objects::{Layout, WidgetState}; +use std::future::Future; pub trait BroadcastPort { type Error; diff --git a/crates/domain/src/ports/config_repository.rs b/crates/domain/src/ports/config_repository.rs index 07d0293..df2a4a0 100644 --- a/crates/domain/src/ports/config_repository.rs +++ b/crates/domain/src/ports/config_repository.rs @@ -1,27 +1,53 @@ -use std::future::Future; use crate::entities::{ DataSource, DataSourceId, LayoutPreset, LayoutPresetId, WidgetConfig, WidgetId, }; use crate::value_objects::Layout; +use std::future::Future; pub trait ConfigRepository { type Error; - fn get_widget(&self, id: WidgetId) -> impl Future, Self::Error>> + Send; + fn get_widget( + &self, + id: WidgetId, + ) -> impl Future, Self::Error>> + Send; fn list_widgets(&self) -> impl Future, Self::Error>> + Send; - fn save_widget(&self, config: &WidgetConfig) -> impl Future> + Send; + fn save_widget( + &self, + config: &WidgetConfig, + ) -> impl Future> + Send; fn delete_widget(&self, id: WidgetId) -> impl Future> + Send; - fn get_data_source(&self, id: DataSourceId) -> impl Future, Self::Error>> + Send; - fn list_data_sources(&self) -> impl Future, Self::Error>> + Send; - fn save_data_source(&self, source: &DataSource) -> impl Future> + Send; - fn delete_data_source(&self, id: DataSourceId) -> impl Future> + Send; + fn get_data_source( + &self, + id: DataSourceId, + ) -> impl Future, Self::Error>> + Send; + fn list_data_sources( + &self, + ) -> impl Future, Self::Error>> + Send; + fn save_data_source( + &self, + source: &DataSource, + ) -> impl Future> + Send; + fn delete_data_source( + &self, + id: DataSourceId, + ) -> impl Future> + Send; fn get_layout(&self) -> impl Future, Self::Error>> + Send; fn save_layout(&self, layout: &Layout) -> impl Future> + Send; - fn get_preset(&self, id: LayoutPresetId) -> impl Future, Self::Error>> + Send; + fn get_preset( + &self, + id: LayoutPresetId, + ) -> impl Future, Self::Error>> + Send; fn list_presets(&self) -> impl Future, Self::Error>> + Send; - fn save_preset(&self, preset: &LayoutPreset) -> impl Future> + Send; - fn delete_preset(&self, id: LayoutPresetId) -> impl Future> + Send; + fn save_preset( + &self, + preset: &LayoutPreset, + ) -> impl Future> + Send; + fn delete_preset( + &self, + id: LayoutPresetId, + ) -> impl Future> + Send; } diff --git a/crates/domain/src/ports/data_source_port.rs b/crates/domain/src/ports/data_source_port.rs index 7205696..f02d29c 100644 --- a/crates/domain/src/ports/data_source_port.rs +++ b/crates/domain/src/ports/data_source_port.rs @@ -1,6 +1,6 @@ -use std::future::Future; use crate::entities::DataSource; use crate::value_objects::Value; +use std::future::Future; pub trait DataSourcePort { type Error; diff --git a/crates/domain/src/ports/event.rs b/crates/domain/src/ports/event.rs index 82daeaa..e951f19 100644 --- a/crates/domain/src/ports/event.rs +++ b/crates/domain/src/ports/event.rs @@ -1,5 +1,5 @@ -use std::future::Future; use crate::events::DomainEvent; +use std::future::Future; pub trait EventPublisher { type Error; diff --git a/crates/domain/src/ports/mod.rs b/crates/domain/src/ports/mod.rs index db2331f..ad3e440 100644 --- a/crates/domain/src/ports/mod.rs +++ b/crates/domain/src/ports/mod.rs @@ -1,9 +1,9 @@ +mod broadcast; mod config_repository; mod data_source_port; -mod broadcast; mod event; +pub use broadcast::BroadcastPort; pub use config_repository::ConfigRepository; pub use data_source_port::DataSourcePort; -pub use broadcast::BroadcastPort; pub use event::EventPublisher; diff --git a/crates/domain/src/value_objects/layout.rs b/crates/domain/src/value_objects/layout.rs index d9b6bfd..2109a61 100644 --- a/crates/domain/src/value_objects/layout.rs +++ b/crates/domain/src/value_objects/layout.rs @@ -1,5 +1,5 @@ -use std::collections::BTreeSet; use crate::entities::WidgetId; +use std::collections::BTreeSet; #[derive(Debug, Clone, PartialEq)] pub enum Sizing { @@ -81,7 +81,9 @@ impl Layout { fn collect_ids(node: &LayoutNode, ids: &mut BTreeSet) { match node { - LayoutNode::Leaf(id) => { ids.insert(*id); } + LayoutNode::Leaf(id) => { + ids.insert(*id); + } LayoutNode::Container(c) => { for child in &c.children { Self::collect_ids(&child.node, ids); diff --git a/crates/domain/src/value_objects/mod.rs b/crates/domain/src/value_objects/mod.rs index 70a2b31..fec5287 100644 --- a/crates/domain/src/value_objects/mod.rs +++ b/crates/domain/src/value_objects/mod.rs @@ -1,11 +1,11 @@ -mod value; mod key_mapping; -mod widget_state; mod layout; +mod value; +mod widget_state; -pub use value::Value; pub use key_mapping::KeyMapping; -pub use widget_state::{WidgetState, WidgetError, DisplayHint}; pub use layout::{ - Layout, LayoutNode, LayoutChild, ContainerNode, Direction, Sizing, LayoutValidationError, + ContainerNode, Direction, Layout, LayoutChild, LayoutNode, LayoutValidationError, Sizing, }; +pub use value::Value; +pub use widget_state::{DisplayHint, WidgetError, WidgetState}; diff --git a/crates/domain/src/value_objects/value.rs b/crates/domain/src/value_objects/value.rs index 79428fc..f6eac90 100644 --- a/crates/domain/src/value_objects/value.rs +++ b/crates/domain/src/value_objects/value.rs @@ -17,10 +17,7 @@ impl Value { Value::Number(_) => 8, Value::String(s) => s.len(), Value::Array(arr) => arr.iter().map(|v| v.estimated_size()).sum(), - Value::Object(map) => map - .iter() - .map(|(k, v)| k.len() + v.estimated_size()) - .sum(), + Value::Object(map) => map.iter().map(|(k, v)| k.len() + v.estimated_size()).sum(), } } diff --git a/crates/domain/src/value_objects/widget_state.rs b/crates/domain/src/value_objects/widget_state.rs index 56cab12..b723b81 100644 --- a/crates/domain/src/value_objects/widget_state.rs +++ b/crates/domain/src/value_objects/widget_state.rs @@ -1,5 +1,5 @@ -use std::collections::BTreeMap; use super::Value; +use std::collections::BTreeMap; #[derive(Debug, Clone, PartialEq)] pub struct WidgetState { diff --git a/crates/domain/tests/data_source_tests.rs b/crates/domain/tests/data_source_tests.rs index 8ea0e3a..ef4971a 100644 --- a/crates/domain/tests/data_source_tests.rs +++ b/crates/domain/tests/data_source_tests.rs @@ -1,5 +1,5 @@ -use std::time::Duration; use domain::{DataSource, DataSourceConfig, DataSourceType, DataSourceValidationError}; +use std::time::Duration; fn make_source(source_type: DataSourceType, url: Option<&str>, poll: Duration) -> DataSource { DataSource { @@ -38,14 +38,22 @@ fn webhook_with_zero_interval_is_valid() { #[test] fn poll_based_source_requires_nonzero_interval() { - let source = make_source(DataSourceType::Weather, Some("https://api.weather.com"), Duration::ZERO); + let source = make_source( + DataSourceType::Weather, + Some("https://api.weather.com"), + Duration::ZERO, + ); let errors = source.validate(); assert!(errors.contains(&DataSourceValidationError::PollIntervalRequired)); } #[test] fn valid_poll_source_has_no_errors() { - let source = make_source(DataSourceType::Rss, Some("https://feed.example.com"), Duration::from_secs(300)); + let source = make_source( + DataSourceType::Rss, + Some("https://feed.example.com"), + Duration::from_secs(300), + ); let errors = source.validate(); assert!(errors.is_empty()); } diff --git a/crates/domain/tests/key_mapping_tests.rs b/crates/domain/tests/key_mapping_tests.rs index 8c33e12..1afdd81 100644 --- a/crates/domain/tests/key_mapping_tests.rs +++ b/crates/domain/tests/key_mapping_tests.rs @@ -1,5 +1,5 @@ -use std::collections::BTreeMap; use domain::{KeyMapping, Value}; +use std::collections::BTreeMap; #[test] fn extracts_value_at_path_and_renames_key() { @@ -8,11 +8,10 @@ fn extracts_value_at_path_and_renames_key() { target_key: "temperature".into(), }; - let raw = Value::Object(BTreeMap::from([ - ("main".into(), Value::Object(BTreeMap::from([ - ("temp".into(), Value::Number(5.4)), - ]))), - ])); + let raw = Value::Object(BTreeMap::from([( + "main".into(), + Value::Object(BTreeMap::from([("temp".into(), Value::Number(5.4))])), + )])); let result = mapping.extract(&raw); assert_eq!(result, Some(("temperature".into(), Value::Number(5.4)))); @@ -25,9 +24,7 @@ fn returns_none_when_path_does_not_match() { target_key: "value".into(), }; - let raw = Value::Object(BTreeMap::from([ - ("other".into(), Value::Number(1.0)), - ])); + let raw = Value::Object(BTreeMap::from([("other".into(), Value::Number(1.0))])); assert_eq!(mapping.extract(&raw), None); } diff --git a/crates/domain/tests/layout_tests.rs b/crates/domain/tests/layout_tests.rs index ce65196..441d58a 100644 --- a/crates/domain/tests/layout_tests.rs +++ b/crates/domain/tests/layout_tests.rs @@ -1,8 +1,8 @@ -use std::collections::BTreeSet; use domain::{ ContainerNode, Direction, Layout, LayoutChild, LayoutNode, LayoutValidationError, Sizing, WidgetId, }; +use std::collections::BTreeSet; fn leaf(id: WidgetId) -> LayoutChild { LayoutChild { @@ -22,14 +22,18 @@ fn row(children: Vec) -> LayoutNode { #[test] fn validate_returns_empty_when_all_widgets_exist() { - let layout = Layout { root: row(vec![leaf(1), leaf(2)]) }; + let layout = Layout { + root: row(vec![leaf(1), leaf(2)]), + }; let known = BTreeSet::from([1, 2]); assert!(layout.validate(&known).is_empty()); } #[test] fn validate_reports_unknown_widget_ids() { - let layout = Layout { root: row(vec![leaf(1), leaf(99)]) }; + let layout = Layout { + root: row(vec![leaf(1), leaf(99)]), + }; let known = BTreeSet::from([1]); let errors = layout.validate(&known); assert_eq!(errors, vec![LayoutValidationError::UnknownWidget(99)]); @@ -49,7 +53,9 @@ fn validate_checks_nested_containers() { sizing: Sizing::Flex(1), node: row(vec![leaf(1), leaf(42)]), }; - let layout = Layout { root: row(vec![inner, leaf(2)]) }; + let layout = Layout { + root: row(vec![inner, leaf(2)]), + }; let known = BTreeSet::from([1, 2]); let errors = layout.validate(&known); assert_eq!(errors, vec![LayoutValidationError::UnknownWidget(42)]); @@ -61,6 +67,8 @@ fn widget_ids_collects_all_leaf_ids() { sizing: Sizing::Flex(1), node: row(vec![leaf(3)]), }; - let layout = Layout { root: row(vec![leaf(1), inner, leaf(2)]) }; + let layout = Layout { + root: row(vec![leaf(1), inner, leaf(2)]), + }; assert_eq!(layout.widget_ids(), BTreeSet::from([1, 2, 3])); } diff --git a/crates/domain/tests/value_tests.rs b/crates/domain/tests/value_tests.rs index c2c5ae2..5a54a16 100644 --- a/crates/domain/tests/value_tests.rs +++ b/crates/domain/tests/value_tests.rs @@ -1,5 +1,5 @@ -use std::collections::BTreeMap; use domain::Value; +use std::collections::BTreeMap; #[test] fn estimated_size_of_string_is_its_byte_length() { @@ -29,37 +29,31 @@ fn estimated_size_of_nested_structure_sums_recursively() { #[test] fn estimated_size_of_array_sums_elements() { - let v = Value::Array(vec![ - Value::String("abc".into()), - Value::Number(1.0), - ]); + let v = Value::Array(vec![Value::String("abc".into()), Value::Number(1.0)]); assert_eq!(v.estimated_size(), 11); } #[test] fn get_path_returns_none_for_missing_key() { - let data = Value::Object(BTreeMap::from([ - ("main".into(), Value::Number(1.0)), - ])); + let data = Value::Object(BTreeMap::from([("main".into(), Value::Number(1.0))])); assert_eq!(data.get_path("$.missing"), None); } #[test] fn get_path_returns_none_when_traversing_non_object() { - let data = Value::Object(BTreeMap::from([ - ("temp".into(), Value::Number(5.4)), - ])); + let data = Value::Object(BTreeMap::from([("temp".into(), Value::Number(5.4))])); assert_eq!(data.get_path("$.temp.nested"), None); } #[test] fn get_path_accesses_array_by_index() { - let data = Value::Object(BTreeMap::from([ - ("items".into(), Value::Array(vec![ + let data = Value::Object(BTreeMap::from([( + "items".into(), + Value::Array(vec![ Value::String("first".into()), Value::String("second".into()), - ])), - ])); + ]), + )])); assert_eq!( data.get_path("$.items[1]"), Some(&Value::String("second".into())) @@ -68,10 +62,9 @@ fn get_path_accesses_array_by_index() { #[test] fn get_path_traverses_nested_object() { - let data = Value::Object(BTreeMap::from([ - ("main".into(), Value::Object(BTreeMap::from([ - ("temp".into(), Value::Number(5.4)), - ]))), - ])); + let data = Value::Object(BTreeMap::from([( + "main".into(), + Value::Object(BTreeMap::from([("temp".into(), Value::Number(5.4))])), + )])); assert_eq!(data.get_path("$.main.temp"), Some(&Value::Number(5.4))); } diff --git a/crates/domain/tests/widget_tests.rs b/crates/domain/tests/widget_tests.rs index 2d3934f..3668d5b 100644 --- a/crates/domain/tests/widget_tests.rs +++ b/crates/domain/tests/widget_tests.rs @@ -1,5 +1,5 @@ -use std::collections::BTreeMap; use domain::{DisplayHint, KeyMapping, Value, WidgetConfig}; +use std::collections::BTreeMap; #[test] fn extract_applies_all_mappings_to_produce_widget_state() { @@ -9,27 +9,39 @@ fn extract_applies_all_mappings_to_produce_widget_state() { display_hint: DisplayHint::IconValue, data_source_id: 1, mappings: vec![ - KeyMapping { source_path: "$.main.temp".into(), target_key: "temperature".into() }, - KeyMapping { source_path: "$.weather[0].icon".into(), target_key: "icon".into() }, + KeyMapping { + source_path: "$.main.temp".into(), + target_key: "temperature".into(), + }, + KeyMapping { + source_path: "$.weather[0].icon".into(), + target_key: "icon".into(), + }, ], max_data_size: 2048, }; let raw = Value::Object(BTreeMap::from([ - ("main".into(), Value::Object(BTreeMap::from([ - ("temp".into(), Value::Number(5.4)), - ]))), - ("weather".into(), Value::Array(vec![ - Value::Object(BTreeMap::from([ - ("icon".into(), Value::String("cloud_rain".into())), - ])), - ])), + ( + "main".into(), + Value::Object(BTreeMap::from([("temp".into(), Value::Number(5.4))])), + ), + ( + "weather".into(), + Value::Array(vec![Value::Object(BTreeMap::from([( + "icon".into(), + Value::String("cloud_rain".into()), + )]))]), + ), ])); let state = config.extract(&raw); assert_eq!(state.data.get("temperature"), Some(&Value::Number(5.4))); - assert_eq!(state.data.get("icon"), Some(&Value::String("cloud_rain".into()))); + assert_eq!( + state.data.get("icon"), + Some(&Value::String("cloud_rain".into())) + ); assert_eq!(state.error, None); } @@ -41,15 +53,14 @@ fn extract_truncates_string_values_exceeding_max_data_size() { name: "news".into(), display_hint: DisplayHint::TextBlock, data_source_id: 1, - mappings: vec![ - KeyMapping { source_path: "$.text".into(), target_key: "body".into() }, - ], + mappings: vec![KeyMapping { + source_path: "$.text".into(), + target_key: "body".into(), + }], max_data_size: 100, }; - let raw = Value::Object(BTreeMap::from([ - ("text".into(), Value::String(long_text)), - ])); + let raw = Value::Object(BTreeMap::from([("text".into(), Value::String(long_text))])); let state = config.extract(&raw); match state.data.get("body") { @@ -66,9 +77,18 @@ fn extract_respects_max_data_size_across_total_state() { display_hint: DisplayHint::TextBlock, data_source_id: 1, mappings: vec![ - KeyMapping { source_path: "$.a".into(), target_key: "a".into() }, - KeyMapping { source_path: "$.b".into(), target_key: "b".into() }, - KeyMapping { source_path: "$.c".into(), target_key: "c".into() }, + KeyMapping { + source_path: "$.a".into(), + target_key: "a".into(), + }, + KeyMapping { + source_path: "$.b".into(), + target_key: "b".into(), + }, + KeyMapping { + source_path: "$.c".into(), + target_key: "c".into(), + }, ], max_data_size: 50, }; @@ -92,15 +112,19 @@ fn extract_skips_mappings_that_dont_match() { display_hint: DisplayHint::IconValue, data_source_id: 1, mappings: vec![ - KeyMapping { source_path: "$.temp".into(), target_key: "temperature".into() }, - KeyMapping { source_path: "$.missing".into(), target_key: "gone".into() }, + KeyMapping { + source_path: "$.temp".into(), + target_key: "temperature".into(), + }, + KeyMapping { + source_path: "$.missing".into(), + target_key: "gone".into(), + }, ], max_data_size: 2048, }; - let raw = Value::Object(BTreeMap::from([ - ("temp".into(), Value::Number(5.4)), - ])); + let raw = Value::Object(BTreeMap::from([("temp".into(), Value::Number(5.4))])); let state = config.extract(&raw); diff --git a/crates/protocol/src/frame.rs b/crates/protocol/src/frame.rs index 7791618..7e13fc7 100644 --- a/crates/protocol/src/frame.rs +++ b/crates/protocol/src/frame.rs @@ -1,5 +1,5 @@ -use serde::{Serialize, Deserialize}; -use super::wire::{WireLayoutNode, WireWidgetState, WireDisplayHint}; +use super::wire::{WireDisplayHint, WireLayoutNode, WireWidgetState}; +use serde::{Deserialize, Serialize}; #[derive(Debug, Clone, PartialEq, Serialize, Deserialize)] pub struct WidgetDescriptor { diff --git a/crates/protocol/src/lib.rs b/crates/protocol/src/lib.rs index 2f9cacf..4efa771 100644 --- a/crates/protocol/src/lib.rs +++ b/crates/protocol/src/lib.rs @@ -1,13 +1,11 @@ -mod wire; mod frame; +mod wire; -pub use wire::{ - WireValue, WireWidgetState, WireWidgetError, WireDisplayHint, - WireLayoutNode, WireContainerNode, WireLayoutChild, WireDirection, WireSizing, - WireKeyValue, -}; pub use frame::{ - ServerMessage, ClientMessage, WidgetDescriptor, - encode, decode_server_message, encode_client, decode_client_message, - MAX_FRAME_SIZE, + ClientMessage, MAX_FRAME_SIZE, ServerMessage, WidgetDescriptor, decode_client_message, + decode_server_message, encode, encode_client, +}; +pub use wire::{ + WireContainerNode, WireDirection, WireDisplayHint, WireKeyValue, WireLayoutChild, + WireLayoutNode, WireSizing, WireValue, WireWidgetError, WireWidgetState, }; diff --git a/crates/protocol/src/wire.rs b/crates/protocol/src/wire.rs index 81ee3db..199e186 100644 --- a/crates/protocol/src/wire.rs +++ b/crates/protocol/src/wire.rs @@ -1,9 +1,9 @@ -use std::collections::BTreeMap; -use serde::{Serialize, Deserialize}; use domain::value_objects::{ - ContainerNode, Direction, DisplayHint, LayoutChild, LayoutNode, Sizing, Value, - WidgetError, WidgetState, + ContainerNode, Direction, DisplayHint, LayoutChild, LayoutNode, Sizing, Value, WidgetError, + WidgetState, }; +use serde::{Deserialize, Serialize}; +use std::collections::BTreeMap; #[derive(Debug, Clone, PartialEq, Serialize, Deserialize)] pub enum WireValue { @@ -84,10 +84,14 @@ pub struct WireWidgetState { impl From<&WidgetState> for WireWidgetState { fn from(s: &WidgetState) -> Self { WireWidgetState { - data: s.data.iter().map(|(k, v)| WireKeyValue { - key: k.clone(), - value: v.into(), - }).collect(), + data: s + .data + .iter() + .map(|(k, v)| WireKeyValue { + key: k.clone(), + value: v.into(), + }) + .collect(), error: s.error.as_ref().map(Into::into), } } @@ -96,7 +100,11 @@ impl From<&WidgetState> for WireWidgetState { impl From for WidgetState { fn from(w: WireWidgetState) -> Self { WidgetState { - data: w.data.into_iter().map(|kv| (kv.key, kv.value.into())).collect(), + data: w + .data + .into_iter() + .map(|kv| (kv.key, kv.value.into())) + .collect(), error: w.error.map(Into::into), } } @@ -205,10 +213,14 @@ impl From<&LayoutNode> for WireLayoutNode { direction: (&c.direction).into(), gap: c.gap, padding: c.padding, - children: c.children.iter().map(|ch| WireLayoutChild { - sizing: (&ch.sizing).into(), - node: (&ch.node).into(), - }).collect(), + children: c + .children + .iter() + .map(|ch| WireLayoutChild { + sizing: (&ch.sizing).into(), + node: (&ch.node).into(), + }) + .collect(), }), } } @@ -222,10 +234,14 @@ impl From for LayoutNode { direction: c.direction.into(), gap: c.gap, padding: c.padding, - children: c.children.into_iter().map(|ch| LayoutChild { - sizing: ch.sizing.into(), - node: ch.node.into(), - }).collect(), + children: c + .children + .into_iter() + .map(|ch| LayoutChild { + sizing: ch.sizing.into(), + node: ch.node.into(), + }) + .collect(), }), } } diff --git a/crates/protocol/tests/conversion_tests.rs b/crates/protocol/tests/conversion_tests.rs index 93b91eb..5e0198d 100644 --- a/crates/protocol/tests/conversion_tests.rs +++ b/crates/protocol/tests/conversion_tests.rs @@ -1,24 +1,24 @@ -use std::collections::BTreeMap; use domain::{ - Value, WidgetState, WidgetError, DisplayHint, - LayoutNode, ContainerNode, LayoutChild, Direction, Sizing, + ContainerNode, Direction, DisplayHint, LayoutChild, LayoutNode, Sizing, Value, WidgetError, + WidgetState, }; use protocol::{ - WireValue, WireWidgetState, WireWidgetError, WireDisplayHint, - WireLayoutNode, WireContainerNode, WireLayoutChild, WireDirection, WireSizing, - WireKeyValue, + WireContainerNode, WireDirection, WireDisplayHint, WireKeyValue, WireLayoutChild, + WireLayoutNode, WireSizing, WireValue, WireWidgetError, WireWidgetState, }; +use std::collections::BTreeMap; #[test] fn value_converts_to_wire_and_back() { - let original = Value::Object(BTreeMap::from([ - ("items".into(), Value::Array(vec![ + let original = Value::Object(BTreeMap::from([( + "items".into(), + Value::Array(vec![ Value::String("hello".into()), Value::Number(42.0), Value::Bool(true), Value::Null, - ])), - ])); + ]), + )])); let wire: WireValue = (&original).into(); let roundtripped: Value = wire.into(); @@ -28,9 +28,7 @@ fn value_converts_to_wire_and_back() { #[test] fn widget_state_with_error_converts_to_wire_and_back() { let original = WidgetState { - data: BTreeMap::from([ - ("temp".into(), Value::Number(5.4)), - ]), + data: BTreeMap::from([("temp".into(), Value::Number(5.4))]), error: Some(WidgetError::SourceUnavailable), }; @@ -56,12 +54,10 @@ fn layout_tree_converts_to_wire_and_back() { direction: Direction::Column, gap: 2, padding: 0, - children: vec![ - LayoutChild { - sizing: Sizing::Flex(1), - node: LayoutNode::Leaf(2), - }, - ], + children: vec![LayoutChild { + sizing: Sizing::Flex(1), + node: LayoutNode::Leaf(2), + }], }), }, ], @@ -74,7 +70,11 @@ fn layout_tree_converts_to_wire_and_back() { #[test] fn display_hint_converts_to_wire_and_back() { - for hint in [DisplayHint::IconValue, DisplayHint::TextBlock, DisplayHint::KeyValue] { + for hint in [ + DisplayHint::IconValue, + DisplayHint::TextBlock, + DisplayHint::KeyValue, + ] { let wire: WireDisplayHint = (&hint).into(); let roundtripped: DisplayHint = wire.into(); assert_eq!(hint, roundtripped); diff --git a/crates/protocol/tests/round_trip_tests.rs b/crates/protocol/tests/round_trip_tests.rs index 49d2509..63dd8aa 100644 --- a/crates/protocol/tests/round_trip_tests.rs +++ b/crates/protocol/tests/round_trip_tests.rs @@ -1,8 +1,7 @@ use protocol::{ - ServerMessage, ClientMessage, WidgetDescriptor, - WireDisplayHint, WireLayoutNode, WireContainerNode, WireLayoutChild, - WireDirection, WireSizing, WireWidgetState, WireKeyValue, WireValue, - encode, decode_server_message, encode_client, decode_client_message, + ClientMessage, ServerMessage, WidgetDescriptor, WireContainerNode, WireDirection, + WireDisplayHint, WireKeyValue, WireLayoutChild, WireLayoutNode, WireSizing, WireValue, + WireWidgetState, decode_client_message, decode_server_message, encode, encode_client, }; #[test] @@ -23,19 +22,23 @@ fn screen_update_round_trips() { }, ], }), - widgets: vec![ - WidgetDescriptor { - id: 1, - display_hint: WireDisplayHint::IconValue, - state: WireWidgetState { - data: vec![ - WireKeyValue { key: "temperature".into(), value: WireValue::String("5.4°C".into()) }, - WireKeyValue { key: "icon".into(), value: WireValue::String("cloud_rain".into()) }, - ], - error: None, - }, + widgets: vec![WidgetDescriptor { + id: 1, + display_hint: WireDisplayHint::IconValue, + state: WireWidgetState { + data: vec![ + WireKeyValue { + key: "temperature".into(), + value: WireValue::String("5.4°C".into()), + }, + WireKeyValue { + key: "icon".into(), + value: WireValue::String("cloud_rain".into()), + }, + ], + error: None, }, - ], + }], }; let frame = encode(&msg).unwrap(); @@ -47,18 +50,17 @@ fn screen_update_round_trips() { #[test] fn data_update_round_trips() { let msg = ServerMessage::DataUpdate { - widgets: vec![ - WidgetDescriptor { - id: 3, - display_hint: WireDisplayHint::TextBlock, - state: WireWidgetState { - data: vec![ - WireKeyValue { key: "body".into(), value: WireValue::String("Breaking news...".into()) }, - ], - error: None, - }, + widgets: vec![WidgetDescriptor { + id: 3, + display_hint: WireDisplayHint::TextBlock, + state: WireWidgetState { + data: vec![WireKeyValue { + key: "body".into(), + value: WireValue::String("Breaking news...".into()), + }], + error: None, }, - ], + }], }; let frame = encode(&msg).unwrap(); diff --git a/docs/k-frame-spa-handoff.md b/docs/k-frame-spa-handoff.md new file mode 100644 index 0000000..69d06ee --- /dev/null +++ b/docs/k-frame-spa-handoff.md @@ -0,0 +1,147 @@ +# K-Frame SPA Handoff + +## What is K-Frame + +IoT dashboard system. Server aggregates data from configurable sources and pushes to ESP32 display clients via TCP. The server is fully functional — SQLite config, REST API, TCP broadcasting, data source polling. ESP32 firmware works end-to-end (display renders widgets). Now needs a config UI. + +## What this session should build + +A React SPA (config/admin UI) for the K-Frame server. The SPA is at `/mnt/drive/dev/k-frame/spa/` — fresh Vite + React 19 + shadcn/ui + TanStack Router + TanStack Query setup. Currently shows a placeholder page. + +## Existing artifacts to read first + +- **Design spec**: `docs/superpowers/specs/2026-06-18-k-frame-design.md` +- **Domain glossary**: `CONTEXT.md` +- **ADRs**: `docs/adr/0001-event-driven-cqrs.md`, `0002-static-dispatch-over-trait-objects.md`, `0003-postcard-over-flatbuffers.md` +- **API types (DTO definitions)**: `crates/api-types/src/` — widget.rs, data_source.rs, layout.rs, preset.rs. These define the exact JSON shapes the API accepts/returns. + +## REST API (server runs on :3000) + +All endpoints return/accept JSON. + +| Method | Path | Description | +|--------|------|-------------| +| GET | /api/widgets | List all widgets | +| POST | /api/widgets | Create widget | +| GET | /api/widgets/{id} | Get widget | +| PUT | /api/widgets/{id} | Update widget | +| DELETE | /api/widgets/{id} | Delete widget | +| GET | /api/data-sources | List all data sources | +| POST | /api/data-sources | Create data source | +| GET | /api/data-sources/{id} | Get data source | +| PUT | /api/data-sources/{id} | Update data source | +| DELETE | /api/data-sources/{id} | Delete data source | +| GET | /api/layout | Get current layout (nullable) | +| PUT | /api/layout | Update layout | +| GET | /api/presets | List presets | +| POST | /api/presets | Create preset | +| GET | /api/presets/{id} | Get preset | +| DELETE | /api/presets/{id} | Delete preset | +| POST | /api/presets/{id}/load | Load preset as active layout | + +### Key JSON shapes + +**Widget** (create/update): +```json +{ + "id": 1, + "name": "weather", + "display_hint": "icon_value", + "data_source_id": 10, + "mappings": [ + {"source_path": "$.main.temp", "target_key": "temperature"}, + {"source_path": "$.weather[0].icon", "target_key": "icon"} + ], + "max_data_size": 2048 +} +``` +`display_hint` values: `"icon_value"`, `"text_block"`, `"key_value"` + +**Data Source** (create/update): +```json +{ + "id": 10, + "name": "weather", + "source_type": "weather", + "poll_interval_secs": 300, + "url": "https://api.openweathermap.org/...", + "api_key": "xxx", + "headers": [] +} +``` +`source_type` values: `"weather"`, `"media"`, `"rss"`, `"http_json"`, `"webhook"` + +**Layout**: +```json +{ + "root": { + "type": "container", + "direction": "row", + "gap": 4, + "padding": 2, + "children": [ + { + "sizing": {"type": "flex", "value": 1}, + "node": {"type": "leaf", "widget_id": 1} + }, + { + "sizing": {"type": "fixed", "value": 80}, + "node": {"type": "leaf", "widget_id": 2} + } + ] + } +} +``` +Nodes are recursive — containers can nest. + +**Preset**: +```json +{"id": 1, "name": "dashboard", "layout": { "root": { ... } }} +``` + +## Pages to build + +1. **Dashboard** — overview of connected clients, active data sources, current layout. Landing page. +2. **Data Sources** — CRUD list. Form: name, source_type (select), URL, API key, poll interval, headers. +3. **Widgets** — CRUD list. Form: name, display_hint (select), data_source_id (select from existing sources), key mappings (dynamic list of source_path + target_key pairs), max_data_size. +4. **Layout Builder** — visual tree editor. Add containers (row/column), nest them, place widgets as leaves, set sizing (fixed/flex), gap, padding. This is the most complex page. +5. **Presets** — save current layout as preset, load presets, delete presets. + +## SPA tech stack (already set up) + +- React 19 + TypeScript +- Vite 8 +- shadcn/ui (components already installed in `src/components/ui/`) +- TanStack Router (not yet configured with routes) +- TanStack Query (not yet configured with provider) +- Tailwind CSS 4 +- Bun (lockfile is bun.lock) + +## Server integration + +The SPA's built files need to be served by the Axum server. Two approaches: +1. **Dev**: SPA runs on Vite dev server (:5173), proxies API calls to :3000. Add proxy config to `vite.config.ts`. +2. **Prod**: `bun run build` outputs to `spa/dist/`, Axum serves these as static files. Need to add static file serving to the http-api adapter. + +The Vite proxy setup is needed first so development works. + +## User preferences + +- Concise, no filler +- No mocking — test against real API +- Clean code, modules, no huge files +- shadcn components for all UI elements +- TanStack Router for routing, TanStack Query for data fetching +- No Co-Authored-By in commits + +## What NOT to change + +- No changes to Rust crates (domain, application, adapters, etc.) +- No changes to the ESP32 firmware +- API is stable — build against it as-is + +## Suggested skills + +- `superpowers:brainstorming` — for designing the layout builder UX (most complex page) +- `frontend-design` — for visual design direction, making it not look like a default template +- `shadcn` — for component usage, composition, and styling patterns diff --git a/spa/.gitignore b/spa/.gitignore new file mode 100644 index 0000000..a547bf3 --- /dev/null +++ b/spa/.gitignore @@ -0,0 +1,24 @@ +# Logs +logs +*.log +npm-debug.log* +yarn-debug.log* +yarn-error.log* +pnpm-debug.log* +lerna-debug.log* + +node_modules +dist +dist-ssr +*.local + +# Editor directories and files +.vscode/* +!.vscode/extensions.json +.idea +.DS_Store +*.suo +*.ntvs* +*.njsproj +*.sln +*.sw? diff --git a/spa/.prettierignore b/spa/.prettierignore new file mode 100644 index 0000000..0b4a1db --- /dev/null +++ b/spa/.prettierignore @@ -0,0 +1,7 @@ +node_modules/ +coverage/ +.pnpm-store/ +pnpm-lock.yaml +package-lock.json +pnpm-lock.yaml +yarn.lock diff --git a/spa/.prettierrc b/spa/.prettierrc new file mode 100644 index 0000000..9000bfa --- /dev/null +++ b/spa/.prettierrc @@ -0,0 +1,11 @@ +{ + "endOfLine": "lf", + "semi": false, + "singleQuote": false, + "tabWidth": 2, + "trailingComma": "es5", + "printWidth": 80, + "plugins": ["prettier-plugin-tailwindcss"], + "tailwindStylesheet": "src/index.css", + "tailwindFunctions": ["cn", "cva"] +} diff --git a/spa/README.md b/spa/README.md new file mode 100644 index 0000000..811328a --- /dev/null +++ b/spa/README.md @@ -0,0 +1,21 @@ +# React + TypeScript + Vite + shadcn/ui + +This is a template for a new Vite project with React, TypeScript, and shadcn/ui. + +## Adding components + +To add components to your app, run the following command: + +```bash +npx shadcn@latest add button +``` + +This will place the ui components in the `src/components` directory. + +## Using components + +To use the components in your app, import them as follows: + +```tsx +import { Button } from "@/components/ui/button" +``` diff --git a/spa/bun.lock b/spa/bun.lock new file mode 100644 index 0000000..b28cd86 --- /dev/null +++ b/spa/bun.lock @@ -0,0 +1,1292 @@ +{ + "lockfileVersion": 1, + "workspaces": { + "": { + "name": "vite-app", + "dependencies": { + "@base-ui/react": "^1.6.0", + "@fontsource-variable/geist": "^5.2.9", + "@tailwindcss/vite": "^4", + "@tanstack/react-query": "^5.101.0", + "@tanstack/react-router": "^1.170.16", + "class-variance-authority": "^0.7.1", + "clsx": "^2.1.1", + "cmdk": "^1.1.1", + "date-fns": "^4.4.0", + "embla-carousel-react": "^8.6.0", + "input-otp": "^1.4.2", + "lucide-react": "^1.21.0", + "next-themes": "^0.4.6", + "radix-ui": "^1.6.0", + "react": "^19.2.6", + "react-day-picker": "^10.0.1", + "react-dom": "^19.2.6", + "react-resizable-panels": "^4.11.2", + "recharts": "3.8.0", + "shadcn": "^4.11.0", + "sonner": "^2.0.7", + "tailwind-merge": "^3.6.0", + "tailwindcss": "^4", + "tw-animate-css": "^1.4.0", + "vaul": "^1.1.2", + }, + "devDependencies": { + "@eslint/js": "^10", + "@types/node": "^24", + "@types/react": "^19", + "@types/react-dom": "^19", + "@vitejs/plugin-react": "^6", + "eslint": "^10", + "eslint-plugin-react-hooks": "^7.1.1", + "eslint-plugin-react-refresh": "^0.5.2", + "globals": "^17", + "prettier": "^3.8.3", + "prettier-plugin-tailwindcss": "^0.8.0", + "typescript": "~6", + "typescript-eslint": "^8", + "vite": "^8", + }, + }, + }, + "packages": { + "@babel/code-frame": ["@babel/code-frame@7.29.7", "", { "dependencies": { "@babel/helper-validator-identifier": "^7.29.7", "js-tokens": "^4.0.0", "picocolors": "^1.1.1" } }, "sha512-Aup7aUOfpbAUg2ROOJN6Iw5f9DMBlzu0mIkm/malLQFN/YQgO48wCj0Kxa3sEHJvPVFg7siR+qRInwXd2qhQKw=="], + + "@babel/compat-data": ["@babel/compat-data@7.29.7", "", {}, "sha512-locTkQyKvwIEgBzVrn8693ebc97F2U8ZHjbXwDXJ5Fn2TCpNwTlKcaKLkdHop5c/icOFE7qt7Q9JC5hnKNa6Gg=="], + + "@babel/core": ["@babel/core@7.29.7", "", { "dependencies": { "@babel/code-frame": "^7.29.7", "@babel/generator": "^7.29.7", "@babel/helper-compilation-targets": "^7.29.7", "@babel/helper-module-transforms": "^7.29.7", "@babel/helpers": "^7.29.7", "@babel/parser": "^7.29.7", "@babel/template": "^7.29.7", "@babel/traverse": "^7.29.7", "@babel/types": "^7.29.7", "@jridgewell/remapping": "^2.3.5", "convert-source-map": "^2.0.0", "debug": "^4.1.0", "gensync": "^1.0.0-beta.2", "json5": "^2.2.3", "semver": "^6.3.1" } }, "sha512-RgHBCvtjbOK2gXSNBNIkNoEc9qoVEtau3hj8gEqKQuL3HZAibKarWFEI3Lfm6EYKkLalOh8eSrj9b+ch9H/VBA=="], + + "@babel/generator": ["@babel/generator@7.29.7", "", { "dependencies": { "@babel/parser": "^7.29.7", "@babel/types": "^7.29.7", "@jridgewell/gen-mapping": "^0.3.12", "@jridgewell/trace-mapping": "^0.3.28", "jsesc": "^3.0.2" } }, "sha512-DkXD5OJQaAQIdZ1bt3UZdEnHAn9Imd3IVBdX03UFe+ony9Ojw5pzr9YVKGDY1jt+Gcn/FnGkNf8r+Vj5NOJWtQ=="], + + "@babel/helper-annotate-as-pure": ["@babel/helper-annotate-as-pure@7.29.7", "", { "dependencies": { "@babel/types": "^7.29.7" } }, "sha512-OoK6239jHPuSQOoS0kfTVKn0b/rVTk0seKq4Gd2UMLtmOVLjDC0ki3e+c90Trqv2gMfvJFqkiljrr568+qddiw=="], + + "@babel/helper-compilation-targets": ["@babel/helper-compilation-targets@7.29.7", "", { "dependencies": { "@babel/compat-data": "^7.29.7", "@babel/helper-validator-option": "^7.29.7", "browserslist": "^4.24.0", "lru-cache": "^5.1.1", "semver": "^6.3.1" } }, "sha512-wem6WaBj4NaVYVdNhLPPVacES6ZJ+KBBfSkTMD3YZxbP3rm3Di85tJU5ljaUNhaOynt+Aj0xruhYuzQBt8n71g=="], + + "@babel/helper-create-class-features-plugin": ["@babel/helper-create-class-features-plugin@7.29.7", "", { "dependencies": { "@babel/helper-annotate-as-pure": "^7.29.7", "@babel/helper-member-expression-to-functions": "^7.29.7", "@babel/helper-optimise-call-expression": "^7.29.7", "@babel/helper-replace-supers": "^7.29.7", "@babel/helper-skip-transparent-expression-wrappers": "^7.29.7", "@babel/traverse": "^7.29.7", "semver": "^6.3.1" }, "peerDependencies": { "@babel/core": "^7.0.0" } }, "sha512-IY3ZD9Tmooqr3TUhc3DUWxiuo8xx1DWLhd5M7hQ+ZWJamqM2BbalrBJb2MisSLoYorOj75U03qULCxQTY9r3hg=="], + + "@babel/helper-globals": ["@babel/helper-globals@7.29.7", "", {}, "sha512-3nQVUAtvkKH9zahfWgw96Jc/uFOmjACE1kQz82E2lqWmHBgjzbNlsC22nuQTfahmWeQtTq5nQ/4Nnd2A1wj4zA=="], + + "@babel/helper-member-expression-to-functions": ["@babel/helper-member-expression-to-functions@7.29.7", "", { "dependencies": { "@babel/traverse": "^7.29.7", "@babel/types": "^7.29.7" } }, "sha512-j+7JYmk1JYDtACIGj0QJqqWZjoUpMoEikQGADMaHgCMCSDqd2+P32rfcibUNrGOMWrlzK1WJBdxrB3JJQZwWtg=="], + + "@babel/helper-module-imports": ["@babel/helper-module-imports@7.29.7", "", { "dependencies": { "@babel/traverse": "^7.29.7", "@babel/types": "^7.29.7" } }, "sha512-ejHwrQQYcm9xnTivShn2IDOlIzInN34AXskvq9QicvCtEzq1Vzclu/tKF8Jq1Cg8JG2GL6/EmjgsCT7lXepE3g=="], + + "@babel/helper-module-transforms": ["@babel/helper-module-transforms@7.29.7", "", { "dependencies": { "@babel/helper-module-imports": "^7.29.7", "@babel/helper-validator-identifier": "^7.29.7", "@babel/traverse": "^7.29.7" }, "peerDependencies": { "@babel/core": "^7.0.0" } }, "sha512-UPUVSyXbOh627KiCIGQSgwWzGeBKLkaJ9PJEdrngIwMSzxLR4jS4+f1f1jb7VzBbg8nFLaYotvVPFCTqdrmTAg=="], + + "@babel/helper-optimise-call-expression": ["@babel/helper-optimise-call-expression@7.29.7", "", { "dependencies": { "@babel/types": "^7.29.7" } }, "sha512-+kmGVjcT9RGYzoDwdwEqEvGgKe3BYq+O1iGzjFubaNgZHwYHP6lsF2Yghf4kEuv9BV7tYDZ913aBW9am6YKong=="], + + "@babel/helper-plugin-utils": ["@babel/helper-plugin-utils@7.29.7", "", {}, "sha512-G7sHYigPY17oO5SYWnfD/0MTBwVR781S/JI643e/JhUYgVgWE/61SoW3NH9KWUKyKq5LVh3npif99Wkt6j86Jw=="], + + "@babel/helper-replace-supers": ["@babel/helper-replace-supers@7.29.7", "", { "dependencies": { "@babel/helper-member-expression-to-functions": "^7.29.7", "@babel/helper-optimise-call-expression": "^7.29.7", "@babel/traverse": "^7.29.7" }, "peerDependencies": { "@babel/core": "^7.0.0" } }, "sha512-atfGXWSeCiF4DnKZIfmJfQRkSw9b9gNNXR1kqKjbhG4pGYCOnkp8OcTB8E3NXjBu8NpheSnOeNKz8KT7UNFTmQ=="], + + "@babel/helper-skip-transparent-expression-wrappers": ["@babel/helper-skip-transparent-expression-wrappers@7.29.7", "", { "dependencies": { "@babel/traverse": "^7.29.7", "@babel/types": "^7.29.7" } }, "sha512-brcMGQaVzIeUb+6/bs1Av0f8YuNNjKY2JyvfRCsFuFsdKccEQ5Ges2y74D74NZ1Rz8lKJ9ksJkfqwQFJ/iNEyQ=="], + + "@babel/helper-string-parser": ["@babel/helper-string-parser@7.29.7", "", {}, "sha512-Pb5ijPrZ89GDH8223L4UP8i6QApWxs04RbPQJTeWDV0/keR2E36MeKnyr6LYmUUvqRRI+Iv87SuF1W6ErINzYw=="], + + "@babel/helper-validator-identifier": ["@babel/helper-validator-identifier@7.29.7", "", {}, "sha512-qehxGkRj55h/ff8EMaJ+cYhyaKlHIxqYDn682wQD7RNp9UujOQsHog2uS0r2vzr4pW+sXf90NeeayjcNaX3fFg=="], + + "@babel/helper-validator-option": ["@babel/helper-validator-option@7.29.7", "", {}, "sha512-N9ZErrD+yW5geCDtBqnOoxmR8+tNKiGuxKlDpuJxfsqpa2dFcexaziGAE/qoHLiDDreVNMupxGmSoNlyvsA3gw=="], + + "@babel/helpers": ["@babel/helpers@7.29.7", "", { "dependencies": { "@babel/template": "^7.29.7", "@babel/types": "^7.29.7" } }, "sha512-1k2lAGRMfHTcwuNYcCNUmaUffmQv8KWMfh2iJUUeRlwlwH4FdNG7mfPI10NPfLHJFThE4Tyr4mv7kTNZOiPuBg=="], + + "@babel/parser": ["@babel/parser@7.29.7", "", { "dependencies": { "@babel/types": "^7.29.7" }, "bin": "./bin/babel-parser.js" }, "sha512-hnORnjP/1P/zFEndoeX+n+t1RwWRJiJpM/jO7FW32Kn9r5+sJB2JWOdYo4L6k78j15eCwY3Gm/7364B1EMwtNg=="], + + "@babel/plugin-syntax-jsx": ["@babel/plugin-syntax-jsx@7.29.7", "", { "dependencies": { "@babel/helper-plugin-utils": "^7.29.7" }, "peerDependencies": { "@babel/core": "^7.0.0-0" } }, "sha512-TSu8+mHCoEaaCDEZ0I3+6mvTBYR4PCxQwf2z9/r5Tbztv6NaLR3B9thGTTxX2WGuGHJqRiAbKPeGTJ5XWXVg6A=="], + + "@babel/plugin-syntax-typescript": ["@babel/plugin-syntax-typescript@7.29.7", "", { "dependencies": { "@babel/helper-plugin-utils": "^7.29.7" }, "peerDependencies": { "@babel/core": "^7.0.0-0" } }, "sha512-ngr+82Sh0xMz25TPCZi+nC2iTzjfCdWS2ONXTp/PtSCHCgaCNBpdMqgvJ2ccdLlClVZ7sisIgB914j/JFe+RZA=="], + + "@babel/plugin-transform-modules-commonjs": ["@babel/plugin-transform-modules-commonjs@7.29.7", "", { "dependencies": { "@babel/helper-module-transforms": "^7.29.7", "@babel/helper-plugin-utils": "^7.29.7" }, "peerDependencies": { "@babel/core": "^7.0.0-0" } }, "sha512-j0vCldybPC5b5dwCQOJ21uKtHzt7hxLygJTg9eF1ScfaikEDNfzn94XoW5Fi+seBR0nCyL23xaBFFkq7dTM8XQ=="], + + "@babel/plugin-transform-typescript": ["@babel/plugin-transform-typescript@7.29.7", "", { "dependencies": { "@babel/helper-annotate-as-pure": "^7.29.7", "@babel/helper-create-class-features-plugin": "^7.29.7", "@babel/helper-plugin-utils": "^7.29.7", "@babel/helper-skip-transparent-expression-wrappers": "^7.29.7", "@babel/plugin-syntax-typescript": "^7.29.7" }, "peerDependencies": { "@babel/core": "^7.0.0-0" } }, "sha512-jK52h8LaLc7JarhQV2ofeFMts4H7vnOXnqZNA6fYglBTZewRBE51KWt3BUltW1P+KoPsYkHoJeXePuz4zo2LMw=="], + + "@babel/preset-typescript": ["@babel/preset-typescript@7.29.7", "", { "dependencies": { "@babel/helper-plugin-utils": "^7.29.7", "@babel/helper-validator-option": "^7.29.7", "@babel/plugin-syntax-jsx": "^7.29.7", "@babel/plugin-transform-modules-commonjs": "^7.29.7", "@babel/plugin-transform-typescript": "^7.29.7" }, "peerDependencies": { "@babel/core": "^7.0.0-0" } }, "sha512-/Foi8vKY2EVbed/1eZx0gJEEwHAIxogrySI7rULcRIvhZzbvoE/b5qG5Ghc0WKAFKOHA9SD1x7RsFlOYdutIiQ=="], + + "@babel/runtime": ["@babel/runtime@7.29.7", "", {}, "sha512-Nq8OhGWiZIZGV6hLHoyAKLLcJihP/xFeBMGJoUrxTX2psI8dCifzLhZISFb+VWS3wFMRDmCGw5R+dOySCqPLhw=="], + + "@babel/template": ["@babel/template@7.29.7", "", { "dependencies": { "@babel/code-frame": "^7.29.7", "@babel/parser": "^7.29.7", "@babel/types": "^7.29.7" } }, "sha512-puq+Gf35oI24FeN11LkoUQFqv9uwNeWpxXZi/Ji3rRIoKAzKnxRaZ+Gkj0vKS9ZCiTESfng1N9LyOyXvo+m+Gg=="], + + "@babel/traverse": ["@babel/traverse@7.29.7", "", { "dependencies": { "@babel/code-frame": "^7.29.7", "@babel/generator": "^7.29.7", "@babel/helper-globals": "^7.29.7", "@babel/parser": "^7.29.7", "@babel/template": "^7.29.7", "@babel/types": "^7.29.7", "debug": "^4.3.1" } }, "sha512-EhlfNQtZ+NK22w5BM61ciuiq1m58ed33Wr1Xan//ZRTy6hgjnwyCffRYwzsGXdASJSUJ1guZILsErh1eQcl+zw=="], + + "@babel/types": ["@babel/types@7.29.7", "", { "dependencies": { "@babel/helper-string-parser": "^7.29.7", "@babel/helper-validator-identifier": "^7.29.7" } }, "sha512-4zBIxpPzowiZpusoFkyGVwakdRJUyuH5PxQ/PrqghfdFWWasvnCdPfQXHrenDai+gyLARulZjZowCOj6fjT4pA=="], + + "@base-ui/react": ["@base-ui/react@1.6.0", "", { "dependencies": { "@babel/runtime": "^7.29.2", "@base-ui/utils": "0.3.1", "@floating-ui/react-dom": "^2.1.8", "@floating-ui/utils": "^0.2.11", "use-sync-external-store": "^1.6.0" }, "peerDependencies": { "@date-fns/tz": "^1.2.0", "@types/react": "^17 || ^18 || ^19", "date-fns": "^4.0.0", "react": "^17 || ^18 || ^19", "react-dom": "^17 || ^18 || ^19" }, "optionalPeers": ["@date-fns/tz", "@types/react", "date-fns"] }, "sha512-/jzjTWJYXhRFO45Bev9lc3cHbmjzCMpUqbMZ2AgKy/z25mY9B6shGSNcXcjQar9n5doM0KYW1W8fcFv2jZBuMw=="], + + "@base-ui/utils": ["@base-ui/utils@0.3.1", "", { "dependencies": { "@babel/runtime": "^7.29.2", "@floating-ui/utils": "^0.2.11", "reselect": "^5.2.0", "use-sync-external-store": "^1.6.0" }, "peerDependencies": { "@types/react": "^17 || ^18 || ^19", "react": "^17 || ^18 || ^19", "react-dom": "^17 || ^18 || ^19" }, "optionalPeers": ["@types/react"] }, "sha512-gFFiltORVmW/N6IILTGxizP3PBpVpysqML1ALY5Vk0mH+7faVkCknOU31goYHN5Aoek2dkjxva1XOD2Ce9WuIg=="], + + "@date-fns/tz": ["@date-fns/tz@1.5.0", "", {}, "sha512-lwYN/vDPeNRULcepoE/LO2Pgx+7/RV+S9ARfbc9lr2DtGkOD7pAiruHvbR1RX3Qyf6ja47EWJDMsNK5vK08DJg=="], + + "@dotenvx/dotenvx": ["@dotenvx/dotenvx@1.73.1", "", { "dependencies": { "commander": "^11.1.0", "conf": "^10.2.0", "dotenv": "^17.2.1", "eciesjs": "^0.4.10", "enquirer": "^2.4.1", "env-paths": "^2.2.1", "execa": "^5.1.1", "fdir": "^6.2.0", "ignore": "^5.3.0", "object-treeify": "1.1.33", "open": "^8.4.2", "picomatch": "^4.0.4", "systeminformation": "^5.22.11", "undici": "^7.11.0", "which": "^4.0.0", "yocto-spinner": "^1.1.0" }, "bin": { "dotenvx": "src/cli/dotenvx.js" } }, "sha512-AWf8edAsSpINqJbtaZsaiV3PO+vw5pS4wJFl6ahYNdnp3gfFYeMxRCdsA9042bKP4UPCs8yr5FikSFlvkmFVag=="], + + "@ecies/ciphers": ["@ecies/ciphers@0.2.6", "", { "peerDependencies": { "@noble/ciphers": "^1.0.0" } }, "sha512-patgsRPKGkhhoBjETV4XxD0En4ui5fbX0hzayqI3M8tvNMGUoUvmyYAIWwlxBc1KX5cturfqByYdj5bYGRpN9g=="], + + "@emnapi/core": ["@emnapi/core@1.10.0", "", { "dependencies": { "@emnapi/wasi-threads": "1.2.1", "tslib": "^2.4.0" } }, "sha512-yq6OkJ4p82CAfPl0u9mQebQHKPJkY7WrIuk205cTYnYe+k2Z8YBh11FrbRG/H6ihirqcacOgl2BIO8oyMQLeXw=="], + + "@emnapi/runtime": ["@emnapi/runtime@1.10.0", "", { "dependencies": { "tslib": "^2.4.0" } }, "sha512-ewvYlk86xUoGI0zQRNq/mC+16R1QeDlKQy21Ki3oSYXNgLb45GV1P6A0M+/s6nyCuNDqe5VpaY84BzXGwVbwFA=="], + + "@emnapi/wasi-threads": ["@emnapi/wasi-threads@1.2.1", "", { "dependencies": { "tslib": "^2.4.0" } }, "sha512-uTII7OYF+/Mes/MrcIOYp5yOtSMLBWSIoLPpcgwipoiKbli6k322tcoFsxoIIxPDqW01SQGAgko4EzZi2BNv2w=="], + + "@eslint-community/eslint-utils": ["@eslint-community/eslint-utils@4.9.1", "", { "dependencies": { "eslint-visitor-keys": "^3.4.3" }, "peerDependencies": { "eslint": "^6.0.0 || ^7.0.0 || >=8.0.0" } }, "sha512-phrYmNiYppR7znFEdqgfWHXR6NCkZEK7hwWDHZUjit/2/U0r6XvkDl0SYnoM51Hq7FhCGdLDT6zxCCOY1hexsQ=="], + + "@eslint-community/regexpp": ["@eslint-community/regexpp@4.12.2", "", {}, "sha512-EriSTlt5OC9/7SXkRSCAhfSxxoSUgBm33OH+IkwbdpgoqsSsUg7y3uh+IICI/Qg4BBWr3U2i39RpmycbxMq4ew=="], + + "@eslint/config-array": ["@eslint/config-array@0.23.5", "", { "dependencies": { "@eslint/object-schema": "^3.0.5", "debug": "^4.3.1", "minimatch": "^10.2.4" } }, "sha512-Y3kKLvC1dvTOT+oGlqNQ1XLqK6D1HU2YXPc52NmAlJZbMMWDzGYXMiPRJ8TYD39muD/OTjlZmNJ4ib7dvSrMBA=="], + + "@eslint/config-helpers": ["@eslint/config-helpers@0.6.0", "", { "dependencies": { "@eslint/core": "^1.2.1" } }, "sha512-ii6Bw9jJ2zi2cWA2Z+9/QZ/+3DX6kwaV5Q986D/CdP3Lap3w/pgQZ373FV7byY/i7L4IRH/G43I5dz1ClsCbpA=="], + + "@eslint/core": ["@eslint/core@1.2.1", "", { "dependencies": { "@types/json-schema": "^7.0.15" } }, "sha512-MwcE1P+AZ4C6DWlpin/OmOA54mmIZ/+xZuJiQd4SyB29oAJjN30UW9wkKNptW2ctp4cEsvhlLY/CsQ1uoHDloQ=="], + + "@eslint/js": ["@eslint/js@10.0.1", "", { "peerDependencies": { "eslint": "^10.0.0" }, "optionalPeers": ["eslint"] }, "sha512-zeR9k5pd4gxjZ0abRoIaxdc7I3nDktoXZk2qOv9gCNWx3mVwEn32VRhyLaRsDiJjTs0xq/T8mfPtyuXu7GWBcA=="], + + "@eslint/object-schema": ["@eslint/object-schema@3.0.5", "", {}, "sha512-vqTaUEgxzm+YDSdElad6PiRoX4t8VGDjCtt05zn4nU810UIx/uNEV7/lZJ6KwFThKZOzOxzXy48da+No7HZaMw=="], + + "@eslint/plugin-kit": ["@eslint/plugin-kit@0.7.2", "", { "dependencies": { "@eslint/core": "^1.2.1", "levn": "^0.4.1" } }, "sha512-+CNAzxglkrpNf/kKywqQfk74QjtceuOE7Qm+AF8miRvPF/wmmK5+OJOgVh3AVTT3RP2mH3+FOaxlE5v72owk0A=="], + + "@floating-ui/core": ["@floating-ui/core@1.7.5", "", { "dependencies": { "@floating-ui/utils": "^0.2.11" } }, "sha512-1Ih4WTWyw0+lKyFMcBHGbb5U5FtuHJuujoyyr5zTaWS5EYMeT6Jb2AuDeftsCsEuchO+mM2ij5+q9crhydzLhQ=="], + + "@floating-ui/dom": ["@floating-ui/dom@1.7.6", "", { "dependencies": { "@floating-ui/core": "^1.7.5", "@floating-ui/utils": "^0.2.11" } }, "sha512-9gZSAI5XM36880PPMm//9dfiEngYoC6Am2izES1FF406YFsjvyBMmeJ2g4SAju3xWwtuynNRFL2s9hgxpLI5SQ=="], + + "@floating-ui/react-dom": ["@floating-ui/react-dom@2.1.8", "", { "dependencies": { "@floating-ui/dom": "^1.7.6" }, "peerDependencies": { "react": ">=16.8.0", "react-dom": ">=16.8.0" } }, "sha512-cC52bHwM/n/CxS87FH0yWdngEZrjdtLW/qVruo68qg+prK7ZQ4YGdut2GyDVpoGeAYe/h899rVeOVm6Oi40k2A=="], + + "@floating-ui/utils": ["@floating-ui/utils@0.2.11", "", {}, "sha512-RiB/yIh78pcIxl6lLMG0CgBXAZ2Y0eVHqMPYugu+9U0AeT6YBeiJpf7lbdJNIugFP5SIjwNRgo4DhR1Qxi26Gg=="], + + "@fontsource-variable/geist": ["@fontsource-variable/geist@5.2.9", "", {}, "sha512-TP+QSBG3wxKGPE33CbMy/L0Nu3qvJ6Fy81Yc4LnQ95xH+i+cfEp8fyU8/kfV14YwszxIFPhnoMTbjL71waVpyQ=="], + + "@hono/node-server": ["@hono/node-server@1.19.14", "", { "peerDependencies": { "hono": "^4" } }, "sha512-GwtvgtXxnWsucXvbQXkRgqksiH2Qed37H9xHZocE5sA3N8O8O8/8FA3uclQXxXVzc9XBZuEOMK7+r02FmSpHtw=="], + + "@humanfs/core": ["@humanfs/core@0.19.2", "", { "dependencies": { "@humanfs/types": "^0.15.0" } }, "sha512-UhXNm+CFMWcbChXywFwkmhqjs3PRCmcSa/hfBgLIb7oQ5HNb1wS0icWsGtSAUNgefHeI+eBrA8I1fxmbHsGdvA=="], + + "@humanfs/node": ["@humanfs/node@0.16.8", "", { "dependencies": { "@humanfs/core": "^0.19.2", "@humanfs/types": "^0.15.0", "@humanwhocodes/retry": "^0.4.0" } }, "sha512-gE1eQNZ3R++kTzFUpdGlpmy8kDZD/MLyHqDwqjkVQI0JMdI1D51sy1H958PNXYkM2rAac7e5/CnIKZrHtPh3BQ=="], + + "@humanfs/types": ["@humanfs/types@0.15.0", "", {}, "sha512-ZZ1w0aoQkwuUuC7Yf+7sdeaNfqQiiLcSRbfI08oAxqLtpXQr9AIVX7Ay7HLDuiLYAaFPu8oBYNq/QIi9URHJ3Q=="], + + "@humanwhocodes/module-importer": ["@humanwhocodes/module-importer@1.0.1", "", {}, "sha512-bxveV4V8v5Yb4ncFTT3rPSgZBOpCkjfK0y4oVVVJwIuDVBRMDXrPyXRL988i5ap9m9bnyEEjWfm5WkBmtffLfA=="], + + "@humanwhocodes/retry": ["@humanwhocodes/retry@0.4.3", "", {}, "sha512-bV0Tgo9K4hfPCek+aMAn81RppFKv2ySDQeMoSZuvTASywNTnVJCArCZE2FWqpvIatKu7VMRLWlR1EazvVhDyhQ=="], + + "@jridgewell/gen-mapping": ["@jridgewell/gen-mapping@0.3.13", "", { "dependencies": { "@jridgewell/sourcemap-codec": "^1.5.0", "@jridgewell/trace-mapping": "^0.3.24" } }, "sha512-2kkt/7niJ6MgEPxF0bYdQ6etZaA+fQvDcLKckhy1yIQOzaoKjBBjSj63/aLVjYE3qhRt5dvM+uUyfCg6UKCBbA=="], + + "@jridgewell/remapping": ["@jridgewell/remapping@2.3.5", "", { "dependencies": { "@jridgewell/gen-mapping": "^0.3.5", "@jridgewell/trace-mapping": "^0.3.24" } }, "sha512-LI9u/+laYG4Ds1TDKSJW2YPrIlcVYOwi2fUC6xB43lueCjgxV4lffOCZCtYFiH6TNOX+tQKXx97T4IKHbhyHEQ=="], + + "@jridgewell/resolve-uri": ["@jridgewell/resolve-uri@3.1.2", "", {}, "sha512-bRISgCIjP20/tbWSPWMEi54QVPRZExkuD9lJL+UIxUKtwVJA8wW1Trb1jMs1RFXo1CBTNZ/5hpC9QvmKWdopKw=="], + + "@jridgewell/sourcemap-codec": ["@jridgewell/sourcemap-codec@1.5.5", "", {}, "sha512-cYQ9310grqxueWbl+WuIUIaiUaDcj7WOq5fVhEljNVgRfOUhY9fy2zTvfoqWsnebh8Sl70VScFbICvJnLKB0Og=="], + + "@jridgewell/trace-mapping": ["@jridgewell/trace-mapping@0.3.31", "", { "dependencies": { "@jridgewell/resolve-uri": "^3.1.0", "@jridgewell/sourcemap-codec": "^1.4.14" } }, "sha512-zzNR+SdQSDJzc8joaeP8QQoCQr8NuYx2dIIytl1QeBEZHJ9uW6hebsrYgbz8hJwUQao3TWCMtmfV8Nu1twOLAw=="], + + "@modelcontextprotocol/sdk": ["@modelcontextprotocol/sdk@1.29.0", "", { "dependencies": { "@hono/node-server": "^1.19.9", "ajv": "^8.17.1", "ajv-formats": "^3.0.1", "content-type": "^1.0.5", "cors": "^2.8.5", "cross-spawn": "^7.0.5", "eventsource": "^3.0.2", "eventsource-parser": "^3.0.0", "express": "^5.2.1", "express-rate-limit": "^8.2.1", "hono": "^4.11.4", "jose": "^6.1.3", "json-schema-typed": "^8.0.2", "pkce-challenge": "^5.0.0", "raw-body": "^3.0.0", "zod": "^3.25 || ^4.0", "zod-to-json-schema": "^3.25.1" }, "peerDependencies": { "@cfworker/json-schema": "^4.1.1" }, "optionalPeers": ["@cfworker/json-schema"] }, "sha512-zo37mZA9hJWpULgkRpowewez1y6ML5GsXJPY8FI0tBBCd77HEvza4jDqRKOXgHNn867PVGCyTdzqpz0izu5ZjQ=="], + + "@napi-rs/wasm-runtime": ["@napi-rs/wasm-runtime@1.1.5", "", { "dependencies": { "@tybys/wasm-util": "^0.10.2" }, "peerDependencies": { "@emnapi/core": "^1.7.1", "@emnapi/runtime": "^1.7.1" } }, "sha512-AWPoBRJ9tsnVhor4sjO7rkni+7p+2IAEFj6cx06UgP10jkQHqay/36uRV/bFkgrh18D9vb4cr8Q0Pthskgzy+Q=="], + + "@noble/ciphers": ["@noble/ciphers@1.3.0", "", {}, "sha512-2I0gnIVPtfnMw9ee9h1dJG7tp81+8Ob3OJb3Mv37rx5L40/b0i7djjCVvGOVqc9AEIQyvyu1i6ypKdFw8R8gQw=="], + + "@noble/curves": ["@noble/curves@1.9.7", "", { "dependencies": { "@noble/hashes": "1.8.0" } }, "sha512-gbKGcRUYIjA3/zCCNaWDciTMFI0dCkvou3TL8Zmy5Nc7sJ47a0jtOeZoTaMxkuqRo9cRhjOdZJXegxYE5FN/xw=="], + + "@noble/hashes": ["@noble/hashes@1.8.0", "", {}, "sha512-jCs9ldd7NwzpgXDIf6P3+NrHh9/sD6CQdxHyjQI+h/6rDNo88ypBxxz45UDuZHz9r3tNz7N/VInSVoVdtXEI4A=="], + + "@nodelib/fs.scandir": ["@nodelib/fs.scandir@2.1.5", "", { "dependencies": { "@nodelib/fs.stat": "2.0.5", "run-parallel": "^1.1.9" } }, "sha512-vq24Bq3ym5HEQm2NKCr3yXDwjc7vTsEThRDnkp2DK9p1uqLR+DHurm/NOTo0KG7HYHU7eppKZj3MyqYuMBf62g=="], + + "@nodelib/fs.stat": ["@nodelib/fs.stat@2.0.5", "", {}, "sha512-RkhPPp2zrqDAQA/2jNhnztcPAlv64XdhIp7a7454A5ovI7Bukxgt7MX7udwAu3zg1DcpPU0rz3VV1SeaqvY4+A=="], + + "@nodelib/fs.walk": ["@nodelib/fs.walk@1.2.8", "", { "dependencies": { "@nodelib/fs.scandir": "2.1.5", "fastq": "^1.6.0" } }, "sha512-oGB+UxlgWcgQkgwo8GcEGwemoTFt3FIO9ababBmaGwXIoBKZ+GTy0pP185beGg7Llih/NSHSV2XAs1lnznocSg=="], + + "@oxc-project/types": ["@oxc-project/types@0.133.0", "", {}, "sha512-KzkdCd6Uxqnf6l3HOw1xfatAlUURA0g14cvBYFyJ5SaNOQbOUvBr9PKArcPcrNIeRsBdgcUzOGrhKveVpvOIGA=="], + + "@radix-ui/number": ["@radix-ui/number@1.1.2", "", {}, "sha512-ceTwaxc4I5IOi97DgCotl3pqiyRGvffcc0oOsE2dQYaJOFIDsDt4VWG6xEbg1QePv9QWausCEIppud/tJ1wNig=="], + + "@radix-ui/primitive": ["@radix-ui/primitive@1.1.4", "", {}, "sha512-7AdCK9PQyiljKoBDbN8OuctCbd/esdwZPQ8RtOE3SsyQtUpiPb+ND75q0jEhC1m1ecBI0MFNeLJvwIh9iKHRcQ=="], + + "@radix-ui/react-accessible-icon": ["@radix-ui/react-accessible-icon@1.1.10", "", { "dependencies": { "@radix-ui/react-visually-hidden": "1.2.6" }, "peerDependencies": { "@types/react": "*", "@types/react-dom": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react", "@types/react-dom"] }, "sha512-TraSwZUqTcVbiDV2/RXzAXC7aeVVXchq0daPFZE7zAxYFaMzjOUggLOfQH9KFLgRizuwVKZO/crveV1eeO3/ZQ=="], + + "@radix-ui/react-accordion": ["@radix-ui/react-accordion@1.2.14", "", { "dependencies": { "@radix-ui/primitive": "1.1.4", "@radix-ui/react-collapsible": "1.1.14", "@radix-ui/react-collection": "1.1.10", "@radix-ui/react-compose-refs": "1.1.3", "@radix-ui/react-context": "1.1.4", "@radix-ui/react-direction": "1.1.2", "@radix-ui/react-id": "1.1.2", "@radix-ui/react-primitive": "2.1.6", "@radix-ui/react-use-controllable-state": "1.2.3" }, "peerDependencies": { "@types/react": "*", "@types/react-dom": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react", "@types/react-dom"] }, "sha512-iE8YB9nmTBH8zd73ofBISZ8JCzgMoMkATJr7qDwa6u5F1+7mTM81V6fa71jgZ65rpjVpecDf1vSnwIFP9Ly1zw=="], + + "@radix-ui/react-alert-dialog": ["@radix-ui/react-alert-dialog@1.1.17", "", { "dependencies": { "@radix-ui/primitive": "1.1.4", "@radix-ui/react-compose-refs": "1.1.3", "@radix-ui/react-context": "1.1.4", "@radix-ui/react-dialog": "1.1.17", "@radix-ui/react-primitive": "2.1.6" }, "peerDependencies": { "@types/react": "*", "@types/react-dom": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react", "@types/react-dom"] }, "sha512-563ygGeyWPrxyVCNp7OV4rE2aIXhFPknpFyo4wbDlcyMMPZ6ySh+zC5WTvY0ZFLgPTg/QB6tA8PyDQyJ2b4cPg=="], + + "@radix-ui/react-arrow": ["@radix-ui/react-arrow@1.1.10", "", { "dependencies": { "@radix-ui/react-primitive": "2.1.6" }, "peerDependencies": { "@types/react": "*", "@types/react-dom": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react", "@types/react-dom"] }, "sha512-j2VTDz1vgCsmuG0k5lBfOcM8n5JPFqZBcMryasFjHYMhwxYL5SRUV5lMSUpRdNtw3D/Sv8pzJtrlAgkssYSsQQ=="], + + "@radix-ui/react-aspect-ratio": ["@radix-ui/react-aspect-ratio@1.1.10", "", { "dependencies": { "@radix-ui/react-primitive": "2.1.6" }, "peerDependencies": { "@types/react": "*", "@types/react-dom": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react", "@types/react-dom"] }, "sha512-kbI7NrqhDeuytYrq7JjAsoXczvL8wgj2tc1MyaYWm+50bMKHCHQtVWCryslx4cCpmCTTkBcwQckE4CmmGV2haQ=="], + + "@radix-ui/react-avatar": ["@radix-ui/react-avatar@1.2.0", "", { "dependencies": { "@radix-ui/react-context": "1.1.4", "@radix-ui/react-primitive": "2.1.6", "@radix-ui/react-use-callback-ref": "1.1.2", "@radix-ui/react-use-is-hydrated": "0.1.1", "@radix-ui/react-use-layout-effect": "1.1.2" }, "peerDependencies": { "@types/react": "*", "@types/react-dom": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react", "@types/react-dom"] }, "sha512-am/CwltXtmtdtP+5FbYblYDnMa/zuKcMJP1i3/SJMDXXfj2mG+BTqLH2wucqeyyiQMursUtg/5cK+Nh2pCaSOA=="], + + "@radix-ui/react-checkbox": ["@radix-ui/react-checkbox@1.3.5", "", { "dependencies": { "@radix-ui/primitive": "1.1.4", "@radix-ui/react-compose-refs": "1.1.3", "@radix-ui/react-context": "1.1.4", "@radix-ui/react-presence": "1.1.6", "@radix-ui/react-primitive": "2.1.6", "@radix-ui/react-use-controllable-state": "1.2.3", "@radix-ui/react-use-previous": "1.1.2", "@radix-ui/react-use-size": "1.1.2" }, "peerDependencies": { "@types/react": "*", "@types/react-dom": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react", "@types/react-dom"] }, "sha512-pREzrmNnVwGvYaBoM64huTRK7B3lrTRuwj8A9nwhPiEtMb+yudiWh6zWAqEtP0Dzd5+iBa1Ki7V1pCxV8ExMdA=="], + + "@radix-ui/react-collapsible": ["@radix-ui/react-collapsible@1.1.14", "", { "dependencies": { "@radix-ui/primitive": "1.1.4", "@radix-ui/react-compose-refs": "1.1.3", "@radix-ui/react-context": "1.1.4", "@radix-ui/react-id": "1.1.2", "@radix-ui/react-presence": "1.1.6", "@radix-ui/react-primitive": "2.1.6", "@radix-ui/react-use-controllable-state": "1.2.3", "@radix-ui/react-use-layout-effect": "1.1.2" }, "peerDependencies": { "@types/react": "*", "@types/react-dom": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react", "@types/react-dom"] }, "sha512-9bT+FvifX1FK2Mj6UEsTdyu0cN3JaA3KdfhaBao+ONrYFy/pyOy3TU1TNw7iOk1o+0hOEq67RojlUUmoFGwxyA=="], + + "@radix-ui/react-collection": ["@radix-ui/react-collection@1.1.10", "", { "dependencies": { "@radix-ui/react-compose-refs": "1.1.3", "@radix-ui/react-context": "1.1.4", "@radix-ui/react-primitive": "2.1.6", "@radix-ui/react-slot": "1.3.0" }, "peerDependencies": { "@types/react": "*", "@types/react-dom": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react", "@types/react-dom"] }, "sha512-IVVz4EvBcKjrzKgof714qDnz/SzQAkLA2Emh5edlHbgcE6fNd3Un6CJLlaYcnm8N4JmAtzQgse4dOKxcD2yc9g=="], + + "@radix-ui/react-compose-refs": ["@radix-ui/react-compose-refs@1.1.3", "", { "peerDependencies": { "@types/react": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react"] }, "sha512-rYOP8OMnuuPMQF1uhPVlGNcCDlkokKqGFE3JcxFViIkAXP7EvFWUliJAstrapypaBLJNHbZL6jGhbVDGTwmVhA=="], + + "@radix-ui/react-context": ["@radix-ui/react-context@1.1.4", "", { "peerDependencies": { "@types/react": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react"] }, "sha512-QwH4PO5urrbO+FaGd5Aglg+YJgWTyyuZ3g/6mKvsqraLkglDdckw9JafgL5McL5VEJ6EPNduPaT3ZE9BttDAqg=="], + + "@radix-ui/react-context-menu": ["@radix-ui/react-context-menu@2.3.1", "", { "dependencies": { "@radix-ui/primitive": "1.1.4", "@radix-ui/react-context": "1.1.4", "@radix-ui/react-menu": "2.1.18", "@radix-ui/react-primitive": "2.1.6", "@radix-ui/react-use-controllable-state": "1.2.3" }, "peerDependencies": { "@types/react": "*", "@types/react-dom": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react", "@types/react-dom"] }, "sha512-XbrxS68W5dyiE4fAb96yvJwSVU5x66B20A99sD5Mk3xSWK/LqeOnx6TZnim1KieMjXS/CTFq8reOAjWxas2G8Q=="], + + "@radix-ui/react-dialog": ["@radix-ui/react-dialog@1.1.17", "", { "dependencies": { "@radix-ui/primitive": "1.1.4", "@radix-ui/react-compose-refs": "1.1.3", "@radix-ui/react-context": "1.1.4", "@radix-ui/react-dismissable-layer": "1.1.13", "@radix-ui/react-focus-guards": "1.1.4", "@radix-ui/react-focus-scope": "1.1.10", "@radix-ui/react-id": "1.1.2", "@radix-ui/react-portal": "1.1.12", "@radix-ui/react-presence": "1.1.6", "@radix-ui/react-primitive": "2.1.6", "@radix-ui/react-slot": "1.3.0", "@radix-ui/react-use-controllable-state": "1.2.3", "aria-hidden": "^1.2.4", "react-remove-scroll": "^2.7.2" }, "peerDependencies": { "@types/react": "*", "@types/react-dom": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react", "@types/react-dom"] }, "sha512-TDTYmpdq8dI2+Xgvgj9AJ8Ghqq+Eph/TRVEdaFQPDItIY+6QSkU7MJMeevw1568Yw/2Ijz8BTphPSP2XejKphw=="], + + "@radix-ui/react-direction": ["@radix-ui/react-direction@1.1.2", "", { "peerDependencies": { "@types/react": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react"] }, "sha512-C3vFhbyi4SW3PmbAi6Awpu4OzJtd0MxGurvSsYtr7p7nM8RNB3VAF3CUmnp2j50knpkrRcB7+ycVXzgLgF6yNA=="], + + "@radix-ui/react-dismissable-layer": ["@radix-ui/react-dismissable-layer@1.1.13", "", { "dependencies": { "@radix-ui/primitive": "1.1.4", "@radix-ui/react-compose-refs": "1.1.3", "@radix-ui/react-primitive": "2.1.6", "@radix-ui/react-use-callback-ref": "1.1.2", "@radix-ui/react-use-escape-keydown": "1.1.2" }, "peerDependencies": { "@types/react": "*", "@types/react-dom": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react", "@types/react-dom"] }, "sha512-2v+zNAWWe0ySxgC0D0yeXMPQ23xZVgXZTerTz+JKlmdRj6gfTqmCcR29jb6d290DezXPGgruHWDX/vYUebtErg=="], + + "@radix-ui/react-dropdown-menu": ["@radix-ui/react-dropdown-menu@2.1.18", "", { "dependencies": { "@radix-ui/primitive": "1.1.4", "@radix-ui/react-compose-refs": "1.1.3", "@radix-ui/react-context": "1.1.4", "@radix-ui/react-id": "1.1.2", "@radix-ui/react-menu": "2.1.18", "@radix-ui/react-primitive": "2.1.6", "@radix-ui/react-use-controllable-state": "1.2.3" }, "peerDependencies": { "@types/react": "*", "@types/react-dom": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react", "@types/react-dom"] }, "sha512-PZGV82gFk0WltDRI//SsG28ZIjlo9ANTmoNYg0jLNzXXiDsAy5PkOOYQaVD1pPxY6t7gxffb1QMD6qaUvsBZdw=="], + + "@radix-ui/react-focus-guards": ["@radix-ui/react-focus-guards@1.1.4", "", { "peerDependencies": { "@types/react": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react"] }, "sha512-cot/aB/mOm0IYVYTTmQcEEK1M48lZWi8FlYe5nDPQQ8NYZUlXEFgncJ9p2Kzer3RKSrY7cTTpEMLZKNo9QoP5Q=="], + + "@radix-ui/react-focus-scope": ["@radix-ui/react-focus-scope@1.1.10", "", { "dependencies": { "@radix-ui/react-compose-refs": "1.1.3", "@radix-ui/react-primitive": "2.1.6", "@radix-ui/react-use-callback-ref": "1.1.2" }, "peerDependencies": { "@types/react": "*", "@types/react-dom": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react", "@types/react-dom"] }, "sha512-Fas/lXQqhVvqwAb64s5RFeHiHYElZ6SUQbZaNd6EkfhP/Al7wTIQ9WIR4QVX475tlu5yFCEdDcJH6/UwsZjMWw=="], + + "@radix-ui/react-form": ["@radix-ui/react-form@0.1.10", "", { "dependencies": { "@radix-ui/primitive": "1.1.4", "@radix-ui/react-compose-refs": "1.1.3", "@radix-ui/react-context": "1.1.4", "@radix-ui/react-id": "1.1.2", "@radix-ui/react-label": "2.1.10", "@radix-ui/react-primitive": "2.1.6" }, "peerDependencies": { "@types/react": "*", "@types/react-dom": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react", "@types/react-dom"] }, "sha512-1NfuvctVtX4sU3Mmq/IdrR8UunxiCMiVg3A5UENKhFzxUBeOyaQQ+lmaQaV7Tc8cqvBKsJL3/KGBsixK0D8WFg=="], + + "@radix-ui/react-hover-card": ["@radix-ui/react-hover-card@1.1.17", "", { "dependencies": { "@radix-ui/primitive": "1.1.4", "@radix-ui/react-compose-refs": "1.1.3", "@radix-ui/react-context": "1.1.4", "@radix-ui/react-dismissable-layer": "1.1.13", "@radix-ui/react-popper": "1.3.1", "@radix-ui/react-portal": "1.1.12", "@radix-ui/react-presence": "1.1.6", "@radix-ui/react-primitive": "2.1.6", "@radix-ui/react-use-controllable-state": "1.2.3" }, "peerDependencies": { "@types/react": "*", "@types/react-dom": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react", "@types/react-dom"] }, "sha512-GjZQIEANVkuuWeztlKz6QEHe31ZX2iDfHzcTMCQVZXC0JyQrgfKWSC+LOOEw6aVV64zyjzobIzSA4AU4eKWrHA=="], + + "@radix-ui/react-id": ["@radix-ui/react-id@1.1.2", "", { "dependencies": { "@radix-ui/react-use-layout-effect": "1.1.2" }, "peerDependencies": { "@types/react": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react"] }, "sha512-orBC88futVpqCmhX1p4cvquNHsELQ+w+vBJnuj3ftETI5bJb0bZn3Tqu3SWN2IOcPycTnMGnhwoermvISt72sA=="], + + "@radix-ui/react-label": ["@radix-ui/react-label@2.1.10", "", { "dependencies": { "@radix-ui/react-primitive": "2.1.6" }, "peerDependencies": { "@types/react": "*", "@types/react-dom": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react", "@types/react-dom"] }, "sha512-ib0zvq2ZsAqKm5tRnqGJn3vOxSgIts5ToxsXT0q1S/GfLD1Zj7UOEnkw8u2w6sRmn47djpQWuSU1DCL1R29/yw=="], + + "@radix-ui/react-menu": ["@radix-ui/react-menu@2.1.18", "", { "dependencies": { "@radix-ui/primitive": "1.1.4", "@radix-ui/react-collection": "1.1.10", "@radix-ui/react-compose-refs": "1.1.3", "@radix-ui/react-context": "1.1.4", "@radix-ui/react-direction": "1.1.2", "@radix-ui/react-dismissable-layer": "1.1.13", "@radix-ui/react-focus-guards": "1.1.4", "@radix-ui/react-focus-scope": "1.1.10", "@radix-ui/react-id": "1.1.2", "@radix-ui/react-popper": "1.3.1", "@radix-ui/react-portal": "1.1.12", "@radix-ui/react-presence": "1.1.6", "@radix-ui/react-primitive": "2.1.6", "@radix-ui/react-roving-focus": "1.1.13", "@radix-ui/react-slot": "1.3.0", "@radix-ui/react-use-callback-ref": "1.1.2", "aria-hidden": "^1.2.4", "react-remove-scroll": "^2.7.2" }, "peerDependencies": { "@types/react": "*", "@types/react-dom": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react", "@types/react-dom"] }, "sha512-lj8Rxjtn6zJq1oSbE/uDtAwCbB9BnxgHD+8MwJMuTh6u1dPamYhW9iuELr/Z8d0D/UysFblYYHeBPwi7T4k0YQ=="], + + "@radix-ui/react-menubar": ["@radix-ui/react-menubar@1.1.18", "", { "dependencies": { "@radix-ui/primitive": "1.1.4", "@radix-ui/react-collection": "1.1.10", "@radix-ui/react-compose-refs": "1.1.3", "@radix-ui/react-context": "1.1.4", "@radix-ui/react-direction": "1.1.2", "@radix-ui/react-id": "1.1.2", "@radix-ui/react-menu": "2.1.18", "@radix-ui/react-primitive": "2.1.6", "@radix-ui/react-roving-focus": "1.1.13", "@radix-ui/react-use-controllable-state": "1.2.3" }, "peerDependencies": { "@types/react": "*", "@types/react-dom": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react", "@types/react-dom"] }, "sha512-hX7EGx/oFq6DPY27GQuP/2wP48GHf5LG6r06VgNJlG+znmDS8OfopZcRcGly3L4lsB9FqpmLx6JQSE9P3BUpyw=="], + + "@radix-ui/react-navigation-menu": ["@radix-ui/react-navigation-menu@1.2.16", "", { "dependencies": { "@radix-ui/primitive": "1.1.4", "@radix-ui/react-collection": "1.1.10", "@radix-ui/react-compose-refs": "1.1.3", "@radix-ui/react-context": "1.1.4", "@radix-ui/react-direction": "1.1.2", "@radix-ui/react-dismissable-layer": "1.1.13", "@radix-ui/react-id": "1.1.2", "@radix-ui/react-presence": "1.1.6", "@radix-ui/react-primitive": "2.1.6", "@radix-ui/react-use-callback-ref": "1.1.2", "@radix-ui/react-use-controllable-state": "1.2.3", "@radix-ui/react-use-layout-effect": "1.1.2", "@radix-ui/react-use-previous": "1.1.2", "@radix-ui/react-visually-hidden": "1.2.6" }, "peerDependencies": { "@types/react": "*", "@types/react-dom": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react", "@types/react-dom"] }, "sha512-nJ0SkrSQgudyYhMiYeHA1ayLVuduEJCFLan1RZZN7c9kqzzCFLaU9kuy81uNtqzweM9YaQPgWzxi9MwQ9jZ04g=="], + + "@radix-ui/react-one-time-password-field": ["@radix-ui/react-one-time-password-field@0.1.10", "", { "dependencies": { "@radix-ui/number": "1.1.2", "@radix-ui/primitive": "1.1.4", "@radix-ui/react-collection": "1.1.10", "@radix-ui/react-compose-refs": "1.1.3", "@radix-ui/react-context": "1.1.4", "@radix-ui/react-direction": "1.1.2", "@radix-ui/react-primitive": "2.1.6", "@radix-ui/react-roving-focus": "1.1.13", "@radix-ui/react-use-controllable-state": "1.2.3", "@radix-ui/react-use-effect-event": "0.0.3", "@radix-ui/react-use-is-hydrated": "0.1.1", "@radix-ui/react-use-layout-effect": "1.1.2" }, "peerDependencies": { "@types/react": "*", "@types/react-dom": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react", "@types/react-dom"] }, "sha512-GHkcJ+WVj91At+OvUVTD4R3W0/wxw9t/sG5xFUBYXaCbtWiooZX5Md376QjJqgH4VsVyXrbVNHO2O4NYcmjfVg=="], + + "@radix-ui/react-password-toggle-field": ["@radix-ui/react-password-toggle-field@0.1.5", "", { "dependencies": { "@radix-ui/primitive": "1.1.4", "@radix-ui/react-compose-refs": "1.1.3", "@radix-ui/react-context": "1.1.4", "@radix-ui/react-id": "1.1.2", "@radix-ui/react-primitive": "2.1.6", "@radix-ui/react-use-controllable-state": "1.2.3", "@radix-ui/react-use-effect-event": "0.0.3", "@radix-ui/react-use-is-hydrated": "0.1.1" }, "peerDependencies": { "@types/react": "*", "@types/react-dom": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react", "@types/react-dom"] }, "sha512-fVuA82u0b/fClpbEJv8yp1nU9eSvoSEOERsU/hhf3FXGPIvkmE7oEaHEu8poowoXO39/Va7zq2E0TUcYr1dBRg=="], + + "@radix-ui/react-popover": ["@radix-ui/react-popover@1.1.17", "", { "dependencies": { "@radix-ui/primitive": "1.1.4", "@radix-ui/react-compose-refs": "1.1.3", "@radix-ui/react-context": "1.1.4", "@radix-ui/react-dismissable-layer": "1.1.13", "@radix-ui/react-focus-guards": "1.1.4", "@radix-ui/react-focus-scope": "1.1.10", "@radix-ui/react-id": "1.1.2", "@radix-ui/react-popper": "1.3.1", "@radix-ui/react-portal": "1.1.12", "@radix-ui/react-presence": "1.1.6", "@radix-ui/react-primitive": "2.1.6", "@radix-ui/react-slot": "1.3.0", "@radix-ui/react-use-controllable-state": "1.2.3", "aria-hidden": "^1.2.4", "react-remove-scroll": "^2.7.2" }, "peerDependencies": { "@types/react": "*", "@types/react-dom": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react", "@types/react-dom"] }, "sha512-/YSAOdJ7YJvdn7bn5sdSx2egW+SKY+u7O5RyAVs94Ymrg2fg5QTSFPMRkzvhGyFuE4/qsmPBdrwYoZMZh/4f+g=="], + + "@radix-ui/react-popper": ["@radix-ui/react-popper@1.3.1", "", { "dependencies": { "@floating-ui/react-dom": "^2.0.0", "@radix-ui/react-arrow": "1.1.10", "@radix-ui/react-compose-refs": "1.1.3", "@radix-ui/react-context": "1.1.4", "@radix-ui/react-primitive": "2.1.6", "@radix-ui/react-use-callback-ref": "1.1.2", "@radix-ui/react-use-layout-effect": "1.1.2", "@radix-ui/react-use-rect": "1.1.2", "@radix-ui/react-use-size": "1.1.2", "@radix-ui/rect": "1.1.2" }, "peerDependencies": { "@types/react": "*", "@types/react-dom": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react", "@types/react-dom"] }, "sha512-bhnq/0DEPTi2lsOD3J5rTL65qUKHbKbhqHsmN9TMiclSXpipi651ooUKPPp6G5lF/WiHBdn1s0Wuqsn+myVAvw=="], + + "@radix-ui/react-portal": ["@radix-ui/react-portal@1.1.12", "", { "dependencies": { "@radix-ui/react-primitive": "2.1.6", "@radix-ui/react-use-layout-effect": "1.1.2" }, "peerDependencies": { "@types/react": "*", "@types/react-dom": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react", "@types/react-dom"] }, "sha512-m309havGzsjLHHaIX50G5PlvRs3xkgPCsGk/5PTvYm8D5q33yG0J7w/712PTOhid7NTaFETtnSXjngHQavvhVw=="], + + "@radix-ui/react-presence": ["@radix-ui/react-presence@1.1.6", "", { "dependencies": { "@radix-ui/react-use-layout-effect": "1.1.2" }, "peerDependencies": { "@types/react": "*", "@types/react-dom": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react", "@types/react-dom"] }, "sha512-zdTk4PlUO0E18HnZ3wYbW0KkJJxWCdiNYp6g6X1PtONFhxVkg01vliTJAmwIszU6mHiyBOoW9P0rAugl5/hULQ=="], + + "@radix-ui/react-primitive": ["@radix-ui/react-primitive@2.1.6", "", { "dependencies": { "@radix-ui/react-slot": "1.3.0" }, "peerDependencies": { "@types/react": "*", "@types/react-dom": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react", "@types/react-dom"] }, "sha512-wetd0QI77DbvrPpTAvH1SqOxsYF2wZe5TNxqwOd5Ty4XDpV3dpV0s8K/1MGMJBeY5o7lg8ub5VIt1Ub+yVen6g=="], + + "@radix-ui/react-progress": ["@radix-ui/react-progress@1.1.10", "", { "dependencies": { "@radix-ui/react-context": "1.1.4", "@radix-ui/react-primitive": "2.1.6" }, "peerDependencies": { "@types/react": "*", "@types/react-dom": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react", "@types/react-dom"] }, "sha512-JYzEg60lk79PwKM27WZyKd7PW8O4OM5jOaFfRPfOyeXmMw7tLJh5kSj+CEjVTehszuwml/AdCzPGMXBTGf4BBw=="], + + "@radix-ui/react-radio-group": ["@radix-ui/react-radio-group@1.4.1", "", { "dependencies": { "@radix-ui/primitive": "1.1.4", "@radix-ui/react-compose-refs": "1.1.3", "@radix-ui/react-context": "1.1.4", "@radix-ui/react-direction": "1.1.2", "@radix-ui/react-presence": "1.1.6", "@radix-ui/react-primitive": "2.1.6", "@radix-ui/react-roving-focus": "1.1.13", "@radix-ui/react-use-controllable-state": "1.2.3", "@radix-ui/react-use-previous": "1.1.2", "@radix-ui/react-use-size": "1.1.2" }, "peerDependencies": { "@types/react": "*", "@types/react-dom": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react", "@types/react-dom"] }, "sha512-/SSxZdKEo2Eo29FFRKd06EfFDYp8HryKg0WYg7QLXaydPzl52YfSvCH2a3QDBRdtcuwACroJT8UVjQVgOJ7P9A=="], + + "@radix-ui/react-roving-focus": ["@radix-ui/react-roving-focus@1.1.13", "", { "dependencies": { "@radix-ui/primitive": "1.1.4", "@radix-ui/react-collection": "1.1.10", "@radix-ui/react-compose-refs": "1.1.3", "@radix-ui/react-context": "1.1.4", "@radix-ui/react-direction": "1.1.2", "@radix-ui/react-id": "1.1.2", "@radix-ui/react-primitive": "2.1.6", "@radix-ui/react-use-callback-ref": "1.1.2", "@radix-ui/react-use-controllable-state": "1.2.3" }, "peerDependencies": { "@types/react": "*", "@types/react-dom": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react", "@types/react-dom"] }, "sha512-9gkwneI0guf8JDmrFxPjJF6Ozzgioyw+/lonYNCwefS9ZHA05er0BVHiXr+LbWGHxUfczvMY6G1oiZZi1VzjRw=="], + + "@radix-ui/react-scroll-area": ["@radix-ui/react-scroll-area@1.2.12", "", { "dependencies": { "@radix-ui/number": "1.1.2", "@radix-ui/primitive": "1.1.4", "@radix-ui/react-compose-refs": "1.1.3", "@radix-ui/react-context": "1.1.4", "@radix-ui/react-direction": "1.1.2", "@radix-ui/react-presence": "1.1.6", "@radix-ui/react-primitive": "2.1.6", "@radix-ui/react-use-callback-ref": "1.1.2", "@radix-ui/react-use-layout-effect": "1.1.2" }, "peerDependencies": { "@types/react": "*", "@types/react-dom": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react", "@types/react-dom"] }, "sha512-xuafVzQiTCLsyEjakowTdG3OgTXsmO7IdCiO77otIa+z44xoLNs9Do5eg7POFumIOCjtG6djfm6RKUKpUa/csA=="], + + "@radix-ui/react-select": ["@radix-ui/react-select@2.3.1", "", { "dependencies": { "@radix-ui/number": "1.1.2", "@radix-ui/primitive": "1.1.4", "@radix-ui/react-collection": "1.1.10", "@radix-ui/react-compose-refs": "1.1.3", "@radix-ui/react-context": "1.1.4", "@radix-ui/react-direction": "1.1.2", "@radix-ui/react-dismissable-layer": "1.1.13", "@radix-ui/react-focus-guards": "1.1.4", "@radix-ui/react-focus-scope": "1.1.10", "@radix-ui/react-id": "1.1.2", "@radix-ui/react-popper": "1.3.1", "@radix-ui/react-portal": "1.1.12", "@radix-ui/react-presence": "1.1.6", "@radix-ui/react-primitive": "2.1.6", "@radix-ui/react-slot": "1.3.0", "@radix-ui/react-use-callback-ref": "1.1.2", "@radix-ui/react-use-controllable-state": "1.2.3", "@radix-ui/react-use-layout-effect": "1.1.2", "@radix-ui/react-use-previous": "1.1.2", "@radix-ui/react-visually-hidden": "1.2.6", "aria-hidden": "^1.2.4", "react-remove-scroll": "^2.7.2" }, "peerDependencies": { "@types/react": "*", "@types/react-dom": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react", "@types/react-dom"] }, "sha512-w6eDvY78LE9ZUiNnXCA1QVK8RYN7k9galFv09kjVydJqBAgHd7Y9A6h0UJ/6DCZNGZMZrB2ohcSW1Bo9d8+wWA=="], + + "@radix-ui/react-separator": ["@radix-ui/react-separator@1.1.10", "", { "dependencies": { "@radix-ui/react-primitive": "2.1.6" }, "peerDependencies": { "@types/react": "*", "@types/react-dom": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react", "@types/react-dom"] }, "sha512-Y6K6jLQCVfCnTL2MEtGxDLffkhNfEfHsEg3Wa8JU+IWdn3EWbLXd3OuOfQRN7p/W/cUce1WyTk3QeuAoDBzN9g=="], + + "@radix-ui/react-slider": ["@radix-ui/react-slider@1.4.1", "", { "dependencies": { "@radix-ui/number": "1.1.2", "@radix-ui/primitive": "1.1.4", "@radix-ui/react-collection": "1.1.10", "@radix-ui/react-compose-refs": "1.1.3", "@radix-ui/react-context": "1.1.4", "@radix-ui/react-direction": "1.1.2", "@radix-ui/react-primitive": "2.1.6", "@radix-ui/react-use-controllable-state": "1.2.3", "@radix-ui/react-use-layout-effect": "1.1.2", "@radix-ui/react-use-previous": "1.1.2", "@radix-ui/react-use-size": "1.1.2" }, "peerDependencies": { "@types/react": "*", "@types/react-dom": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react", "@types/react-dom"] }, "sha512-r91WSpQucNGFKAIxT8FT0H0zyjd5tJlqObLp7LOMV4z49KoDCwjy01w3vDOU4e1wxhF9IgjYco7SB6byOW7Buw=="], + + "@radix-ui/react-slot": ["@radix-ui/react-slot@1.3.0", "", { "dependencies": { "@radix-ui/react-compose-refs": "1.1.3" }, "peerDependencies": { "@types/react": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react"] }, "sha512-MojKku4U/miO8Av4Dkb+ctMAQx7JmY96LmtDQlAarCRtd7rN52QCSzBF+XAvr5S6coSVj9HEPBgHAHKEJVk/WA=="], + + "@radix-ui/react-switch": ["@radix-ui/react-switch@1.3.1", "", { "dependencies": { "@radix-ui/primitive": "1.1.4", "@radix-ui/react-compose-refs": "1.1.3", "@radix-ui/react-context": "1.1.4", "@radix-ui/react-primitive": "2.1.6", "@radix-ui/react-use-controllable-state": "1.2.3", "@radix-ui/react-use-previous": "1.1.2", "@radix-ui/react-use-size": "1.1.2" }, "peerDependencies": { "@types/react": "*", "@types/react-dom": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react", "@types/react-dom"] }, "sha512-55bQtCnOB0BohomSHi6qvQXpJEEqUGDm6hRrM0Bph5OXwhSegqkd8IqgBAQkM1IlgUlWZIxpxRcpOEfRIgimyw=="], + + "@radix-ui/react-tabs": ["@radix-ui/react-tabs@1.1.15", "", { "dependencies": { "@radix-ui/primitive": "1.1.4", "@radix-ui/react-context": "1.1.4", "@radix-ui/react-direction": "1.1.2", "@radix-ui/react-id": "1.1.2", "@radix-ui/react-presence": "1.1.6", "@radix-ui/react-primitive": "2.1.6", "@radix-ui/react-roving-focus": "1.1.13", "@radix-ui/react-use-controllable-state": "1.2.3" }, "peerDependencies": { "@types/react": "*", "@types/react-dom": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react", "@types/react-dom"] }, "sha512-kxc9gI6/HfcU4nfMMVS3AmQK414kbU1IE6UCJmMmxjhO3cRPXOyYnmvyKD+ODt7q56nRq9l7Wovi6uaGwKgMlg=="], + + "@radix-ui/react-toast": ["@radix-ui/react-toast@1.2.17", "", { "dependencies": { "@radix-ui/primitive": "1.1.4", "@radix-ui/react-collection": "1.1.10", "@radix-ui/react-compose-refs": "1.1.3", "@radix-ui/react-context": "1.1.4", "@radix-ui/react-dismissable-layer": "1.1.13", "@radix-ui/react-portal": "1.1.12", "@radix-ui/react-presence": "1.1.6", "@radix-ui/react-primitive": "2.1.6", "@radix-ui/react-use-callback-ref": "1.1.2", "@radix-ui/react-use-controllable-state": "1.2.3", "@radix-ui/react-use-layout-effect": "1.1.2", "@radix-ui/react-visually-hidden": "1.2.6" }, "peerDependencies": { "@types/react": "*", "@types/react-dom": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react", "@types/react-dom"] }, "sha512-uL4kyyWy000pPL43fGGCV5qT6ZchCWEQZOSlkYiPwPt8Hy1iW38RjeptIvz1/SZesrW6Vn58Ct3sV7tfEfiAbw=="], + + "@radix-ui/react-toggle": ["@radix-ui/react-toggle@1.1.12", "", { "dependencies": { "@radix-ui/primitive": "1.1.4", "@radix-ui/react-primitive": "2.1.6", "@radix-ui/react-use-controllable-state": "1.2.3" }, "peerDependencies": { "@types/react": "*", "@types/react-dom": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react", "@types/react-dom"] }, "sha512-AsAVsYNZIlRBsci7BhE+QyQeKd1h6TffJYt+lF0QQkd5OpQ3klfIByPsCb4G0h/Fq6PJwh1FYNluzBFYzhk4+w=="], + + "@radix-ui/react-toggle-group": ["@radix-ui/react-toggle-group@1.1.13", "", { "dependencies": { "@radix-ui/primitive": "1.1.4", "@radix-ui/react-context": "1.1.4", "@radix-ui/react-direction": "1.1.2", "@radix-ui/react-primitive": "2.1.6", "@radix-ui/react-roving-focus": "1.1.13", "@radix-ui/react-toggle": "1.1.12", "@radix-ui/react-use-controllable-state": "1.2.3" }, "peerDependencies": { "@types/react": "*", "@types/react-dom": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react", "@types/react-dom"] }, "sha512-Xb9PLtlvU66F36LiKba6dFswu6V2mDkgidO4fNSbQHQwmZ9ObxMIO17MN/LJ4aWJecVuSVLAHPZjyeMzJrgeiA=="], + + "@radix-ui/react-toolbar": ["@radix-ui/react-toolbar@1.1.13", "", { "dependencies": { "@radix-ui/primitive": "1.1.4", "@radix-ui/react-context": "1.1.4", "@radix-ui/react-direction": "1.1.2", "@radix-ui/react-primitive": "2.1.6", "@radix-ui/react-roving-focus": "1.1.13", "@radix-ui/react-separator": "1.1.10", "@radix-ui/react-toggle-group": "1.1.13" }, "peerDependencies": { "@types/react": "*", "@types/react-dom": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react", "@types/react-dom"] }, "sha512-Za1l4f6fzTkGgz/iynAMN8iaqiKff2wm2/QwiLmHPtDQreWEBrvSimgQFIekxMUdRPhILM7xdIXxuS/o/DGZag=="], + + "@radix-ui/react-tooltip": ["@radix-ui/react-tooltip@1.2.10", "", { "dependencies": { "@radix-ui/primitive": "1.1.4", "@radix-ui/react-compose-refs": "1.1.3", "@radix-ui/react-context": "1.1.4", "@radix-ui/react-dismissable-layer": "1.1.13", "@radix-ui/react-id": "1.1.2", "@radix-ui/react-popper": "1.3.1", "@radix-ui/react-portal": "1.1.12", "@radix-ui/react-presence": "1.1.6", "@radix-ui/react-primitive": "2.1.6", "@radix-ui/react-slot": "1.3.0", "@radix-ui/react-use-controllable-state": "1.2.3", "@radix-ui/react-visually-hidden": "1.2.6" }, "peerDependencies": { "@types/react": "*", "@types/react-dom": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react", "@types/react-dom"] }, "sha512-NlNe8D0dWEpVfXFli90IO6X07Josx/b1iu98tDnx9Xv0HT4wLIL+m2VOheMHhK7qbp2HoTBqALEFzGyZs/levw=="], + + "@radix-ui/react-use-callback-ref": ["@radix-ui/react-use-callback-ref@1.1.2", "", { "peerDependencies": { "@types/react": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react"] }, "sha512-xCso9j1/u8sEgP1RNHjFrXJLApL8LiqOkI1R4ywuN00rxWdYg4oQXuwKLS3i0j5NWLromUD27/4nlxj2UFVvIw=="], + + "@radix-ui/react-use-controllable-state": ["@radix-ui/react-use-controllable-state@1.2.3", "", { "dependencies": { "@radix-ui/react-use-effect-event": "0.0.3", "@radix-ui/react-use-layout-effect": "1.1.2" }, "peerDependencies": { "@types/react": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react"] }, "sha512-PLzC90MS+ReootmjC597dvopoelpZ8Q61HJkDXZSExitIq7PL55vHNnesAHwguHK0aPfBnpdNzQtv1uliaqQrA=="], + + "@radix-ui/react-use-effect-event": ["@radix-ui/react-use-effect-event@0.0.3", "", { "dependencies": { "@radix-ui/react-use-layout-effect": "1.1.2" }, "peerDependencies": { "@types/react": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react"] }, "sha512-6c8ZqvPTWILEKnyVkP53EGRCcpnJiKTC21sS/6R1GF5xKyHJJWQEPfkqlcgUkdRQivd6tb23abUwe4ngWmY0JA=="], + + "@radix-ui/react-use-escape-keydown": ["@radix-ui/react-use-escape-keydown@1.1.2", "", { "dependencies": { "@radix-ui/react-use-callback-ref": "1.1.2" }, "peerDependencies": { "@types/react": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react"] }, "sha512-2uVLvLjgO7NZCWw01/FdqRwmA42J0BcjPMUCA+koFEOAb+zjqIP7SiFz/7zWPrKnVmSqr76Omq2ALyCuX4dhLw=="], + + "@radix-ui/react-use-is-hydrated": ["@radix-ui/react-use-is-hydrated@0.1.1", "", { "peerDependencies": { "@types/react": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react"] }, "sha512-qwOiz4Tjo8CNnrOLAYUMXeZwDzXgXpvK4TKQPmWLECM9XoWvA6+0Z2/7Ag3A4ivjS4ovbLJPbskkxioFyBhr8A=="], + + "@radix-ui/react-use-layout-effect": ["@radix-ui/react-use-layout-effect@1.1.2", "", { "peerDependencies": { "@types/react": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react"] }, "sha512-jrBWOxZITuGcnjRCM2t2U5ZPkCLxD+Ym6DjfssS5haTj2iiak/DOb64JeN6OdLfLgptb6/e2kKR+ZuTrGoZTPA=="], + + "@radix-ui/react-use-previous": ["@radix-ui/react-use-previous@1.1.2", "", { "peerDependencies": { "@types/react": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react"] }, "sha512-IGBQPtRFdhN6MQ8dbegVmBq1LVZluya3F1jWY+puIcQC3MHctRwTDSBWCkL/3ZcnMJLTMJ++Z+ktmvg0F89iCw=="], + + "@radix-ui/react-use-rect": ["@radix-ui/react-use-rect@1.1.2", "", { "dependencies": { "@radix-ui/rect": "1.1.2" }, "peerDependencies": { "@types/react": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react"] }, "sha512-d8a+bBY/FxikNPlgJJoaBHZX+zKVbWHYJGTLnLvveQgFSTntkGdEKv3JDtHrMS0DNYpllz2nRsTLGLKYttbpmw=="], + + "@radix-ui/react-use-size": ["@radix-ui/react-use-size@1.1.2", "", { "dependencies": { "@radix-ui/react-use-layout-effect": "1.1.2" }, "peerDependencies": { "@types/react": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react"] }, "sha512-giWQp+4mxjBPt4KZ0MmyuykFNWfbDxKt4x+fPkRYmgRFJSbCZFzUglvMb/Kjn38tm10YP4ufiQZDx3zna4LU6w=="], + + "@radix-ui/react-visually-hidden": ["@radix-ui/react-visually-hidden@1.2.6", "", { "dependencies": { "@radix-ui/react-primitive": "2.1.6" }, "peerDependencies": { "@types/react": "*", "@types/react-dom": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react", "@types/react-dom"] }, "sha512-jCE0WljWifTI4niIMCll06kGpsJTAPiZVU9H4WR1N6qW7At9ystHbN7dDB+we2xH535roFHj7qKS+RGj0FMDWQ=="], + + "@radix-ui/rect": ["@radix-ui/rect@1.1.2", "", {}, "sha512-xnXE7wG13PI+cxieVssYXlQJuYVRhH9NBoxt3KNwzghDIA69GMm7d4wXRouHIYjE+KvS6U/MsMO73NdS2MH9ZA=="], + + "@reduxjs/toolkit": ["@reduxjs/toolkit@2.12.0", "", { "dependencies": { "@standard-schema/spec": "^1.0.0", "@standard-schema/utils": "^0.3.0", "immer": "^11.0.0", "redux": "^5.0.1", "redux-thunk": "^3.1.0", "reselect": "^5.1.0" }, "peerDependencies": { "react": "^16.9.0 || ^17.0.0 || ^18 || ^19", "react-redux": "^7.2.1 || ^8.1.3 || ^9.0.0" }, "optionalPeers": ["react", "react-redux"] }, "sha512-KiT+RzZbp6mQET+Mg+h2c97+9j1sNflUxQkIHI7Yuzf6Peu+OYpmkn6nbHWmLLWj+1ZODUJFwGZ7gx3L9R9EOw=="], + + "@rolldown/binding-android-arm64": ["@rolldown/binding-android-arm64@1.0.3", "", { "os": "android", "cpu": "arm64" }, "sha512-454rs7jHngixp/NMxd5srYD57OnzSlZ/eFTETjORQHLwJG1lRtmNOJcBerZlfu4GjKqeq8aCCIQrMdHyhI51Hw=="], + + "@rolldown/binding-darwin-arm64": ["@rolldown/binding-darwin-arm64@1.0.3", "", { "os": "darwin", "cpu": "arm64" }, "sha512-PcAhP+ynjURNyy8SKGl5DQP94aGuB/7JrXJb/t7P+hanXvQVMWzUvRRhBAcg/lNRadBhoUPqSoP4xw5tR/KBEA=="], + + "@rolldown/binding-darwin-x64": ["@rolldown/binding-darwin-x64@1.0.3", "", { "os": "darwin", "cpu": "x64" }, "sha512-9YpfeUvSE2RS7wysJ81uOZkXJz7f7Q55H2Gvp3VEw/EsahqDtrphrZ0EwDLK5vvKOzaCrBsjF8JmnMLcUt78Gg=="], + + "@rolldown/binding-freebsd-x64": ["@rolldown/binding-freebsd-x64@1.0.3", "", { "os": "freebsd", "cpu": "x64" }, "sha512-yB1IlAsSNHncV6SCTL27/MVGR5htvQsoGxIv5KMGXALp+Ll1wYsn+x98M9MW7qa+NdSbvrrY7ANI4wLJ0n1e6g=="], + + "@rolldown/binding-linux-arm-gnueabihf": ["@rolldown/binding-linux-arm-gnueabihf@1.0.3", "", { "os": "linux", "cpu": "arm" }, "sha512-Yi30IVAAfLUCy2MseFjbB1jAMDl1VMCAas5StnYp8da9+CKvMd2H2cbEjWcw5NPaPqzvYkVIaF1nNUG+b7u/sw=="], + + "@rolldown/binding-linux-arm64-gnu": ["@rolldown/binding-linux-arm64-gnu@1.0.3", "", { "os": "linux", "cpu": "arm64" }, "sha512-jsO7R8To+AdlYgUmN5sHSCZbfhtMBkO0WUx8iORQnPcMMdgr7qM2DQmMwgabs3GhNztdmoKkMKQFHD6DTMCIQw=="], + + "@rolldown/binding-linux-arm64-musl": ["@rolldown/binding-linux-arm64-musl@1.0.3", "", { "os": "linux", "cpu": "arm64" }, "sha512-VWkUHwWriDciit80wleYwKILoR/KMvxh/IdwS/paX+ZgpuRpCrKLUdadJbc0NpBEiyhpYawsJ73j9aCvOH+f7Q=="], + + "@rolldown/binding-linux-ppc64-gnu": ["@rolldown/binding-linux-ppc64-gnu@1.0.3", "", { "os": "linux", "cpu": "ppc64" }, "sha512-5f1laC0SlIR0yDbFCd8acUhvJIag6N3zC5P7oUPN6wX0aOma+uKJ0wBDH5aq7I1PVI2ttTlhJwzwRIBnLiSGEg=="], + + "@rolldown/binding-linux-s390x-gnu": ["@rolldown/binding-linux-s390x-gnu@1.0.3", "", { "os": "linux", "cpu": "s390x" }, "sha512-Iq4ko0r4XsgbrF/LunNgHtAGLRRVE2kXonAXQ/MV0mC6jQpMOhW1SvtZja2EhC/kd05++bP78dsqBeIQyYJ6Yg=="], + + "@rolldown/binding-linux-x64-gnu": ["@rolldown/binding-linux-x64-gnu@1.0.3", "", { "os": "linux", "cpu": "x64" }, "sha512-B8m6tD5+/N5FeNQFbKlLA/2yVq9ycQP1SeedyEYYKWBNR3ZQbkvIUcNnDNM03lO1l5F2roiiFJGgvoLLyZXtSg=="], + + "@rolldown/binding-linux-x64-musl": ["@rolldown/binding-linux-x64-musl@1.0.3", "", { "os": "linux", "cpu": "x64" }, "sha512-pSdpdUJHkuCxun9LE7jvgUB9qsRgaiyNNCX7m/AvHTcq67AiT/Yhoxvw5zPfhrM8k/BfP8ce/hMOpthKDpEUow=="], + + "@rolldown/binding-openharmony-arm64": ["@rolldown/binding-openharmony-arm64@1.0.3", "", { "os": "none", "cpu": "arm64" }, "sha512-OXXS3RKJgX2uLwM+gYyuH5omcH8fL1LJs96pZGgtetVCahON57+d4SJHzTgZiOjxgGkSnpXpOsWuPDGAKAigEg=="], + + "@rolldown/binding-wasm32-wasi": ["@rolldown/binding-wasm32-wasi@1.0.3", "", { "dependencies": { "@emnapi/core": "1.10.0", "@emnapi/runtime": "1.10.0", "@napi-rs/wasm-runtime": "^1.1.4" }, "cpu": "none" }, "sha512-JTtb8BWFynicNSoPrehsCzBtOKjZ6jhMiPFEmOiuXg1Fl8dn2KHQob+GuPSGR0dryQa1PQJbzjF3dqO/whhjLg=="], + + "@rolldown/binding-win32-arm64-msvc": ["@rolldown/binding-win32-arm64-msvc@1.0.3", "", { "os": "win32", "cpu": "arm64" }, "sha512-gEdFFEN70A/jxb2svrWsN3aDL7OUtmvlOy+6fa2jxG8K0wQ1ZbdeLGnidov6Yu5/733dI5ySfzFlQ/cb0bSz1g=="], + + "@rolldown/binding-win32-x64-msvc": ["@rolldown/binding-win32-x64-msvc@1.0.3", "", { "os": "win32", "cpu": "x64" }, "sha512-eXB7CHuaQdqmJcc3koCNtNPmT/bj2gc999kUFgBxG8Ac0NdgXc4rkCHhqrgrhN3zddvvvrgzj1e90SuSfmyIXA=="], + + "@rolldown/pluginutils": ["@rolldown/pluginutils@1.0.1", "", {}, "sha512-2j9bGt5Jh8hj+vPtgzPtl72j0yRxHAyumoo6TNfAjsLB04UtpSvPbPcDcBMxz7n+9CYB0c1GxQFxYRg2jimqGw=="], + + "@sec-ant/readable-stream": ["@sec-ant/readable-stream@0.4.1", "", {}, "sha512-831qok9r2t8AlxLko40y2ebgSDhenenCatLVeW/uBtnHPyhHOvG0C7TvfgecV+wHzIm5KUICgzmVpWS+IMEAeg=="], + + "@sindresorhus/merge-streams": ["@sindresorhus/merge-streams@4.0.0", "", {}, "sha512-tlqY9xq5ukxTUZBmoOp+m61cqwQD5pHJtFY3Mn8CA8ps6yghLH/Hw8UPdqg4OLmFW3IFlcXnQNmo/dh8HzXYIQ=="], + + "@standard-schema/spec": ["@standard-schema/spec@1.1.0", "", {}, "sha512-l2aFy5jALhniG5HgqrD6jXLi/rUWrKvqN/qJx6yoJsgKhblVd+iqqU4RCXavm/jPityDo5TCvKMnpjKnOriy0w=="], + + "@standard-schema/utils": ["@standard-schema/utils@0.3.0", "", {}, "sha512-e7Mew686owMaPJVNNLs55PUvgz371nKgwsc4vxE49zsODpJEnxgxRo2y/OKrqueavXgZNMDVj3DdHFlaSAeU8g=="], + + "@tailwindcss/node": ["@tailwindcss/node@4.3.1", "", { "dependencies": { "@jridgewell/remapping": "^2.3.5", "enhanced-resolve": "5.21.6", "jiti": "^2.7.0", "lightningcss": "1.32.0", "magic-string": "^0.30.21", "source-map-js": "^1.2.1", "tailwindcss": "4.3.1" } }, "sha512-6NDaqRoAMSXD1mr/RXu0HBvNE9a2n5tHPsxu9XHLws8o4Twes5rBM2205SUUiJ9goAtadrN6xTGX0UDEwp/N4A=="], + + "@tailwindcss/oxide": ["@tailwindcss/oxide@4.3.1", "", { "optionalDependencies": { "@tailwindcss/oxide-android-arm64": "4.3.1", "@tailwindcss/oxide-darwin-arm64": "4.3.1", "@tailwindcss/oxide-darwin-x64": "4.3.1", "@tailwindcss/oxide-freebsd-x64": "4.3.1", "@tailwindcss/oxide-linux-arm-gnueabihf": "4.3.1", "@tailwindcss/oxide-linux-arm64-gnu": "4.3.1", "@tailwindcss/oxide-linux-arm64-musl": "4.3.1", "@tailwindcss/oxide-linux-x64-gnu": "4.3.1", "@tailwindcss/oxide-linux-x64-musl": "4.3.1", "@tailwindcss/oxide-wasm32-wasi": "4.3.1", "@tailwindcss/oxide-win32-arm64-msvc": "4.3.1", "@tailwindcss/oxide-win32-x64-msvc": "4.3.1" } }, "sha512-yVPyo8RNkabVr3O2EhHEE0Rewu7YKzc1DhIqfL46LKveFrmu9XbDazNOJY7/GRuvw1h6u3utWnR29H/p5JPlgA=="], + + "@tailwindcss/oxide-android-arm64": ["@tailwindcss/oxide-android-arm64@4.3.1", "", { "os": "android", "cpu": "arm64" }, "sha512-SVlyf61g374l5cHyg8x9kf5xmLcOaxvOTsbsqDnSsDJaKOEFZ7GCvi84VAVGpxojYOs1+3K6M0UjXfqPU8vmOQ=="], + + "@tailwindcss/oxide-darwin-arm64": ["@tailwindcss/oxide-darwin-arm64@4.3.1", "", { "os": "darwin", "cpu": "arm64" }, "sha512-hVnWLwv+e/l7c4WKyVtHVrIPvYdqWHjRB3MDIqARynzFtnQg85kmQEFCbV9Ja0VVx4xXTIiDWY60Y7iz/iNoDA=="], + + "@tailwindcss/oxide-darwin-x64": ["@tailwindcss/oxide-darwin-x64@4.3.1", "", { "os": "darwin", "cpu": "x64" }, "sha512-Cf7abu0WVgbhU7ANgPUnSAvm7nCvMweusHb8FnaHlLfv/Caq4GYaEZg7ZImzzmjx4lIAfuS8q+eLIS7A7IzxIg=="], + + "@tailwindcss/oxide-freebsd-x64": ["@tailwindcss/oxide-freebsd-x64@4.3.1", "", { "os": "freebsd", "cpu": "x64" }, "sha512-ZZqzX2Y+GXtXXfqSfpJhDm60OoZfvLHLCgm+J7NVqgHHJjG/m9ugZI77RwTsVd4fnBJuCFP6Ae6kTJb71UdS8g=="], + + "@tailwindcss/oxide-linux-arm-gnueabihf": ["@tailwindcss/oxide-linux-arm-gnueabihf@4.3.1", "", { "os": "linux", "cpu": "arm" }, "sha512-/Ah/xik0LaMYfv9DZ0S/t4pBlBNYOcqtRwusjgovHkvT8ixueWCLyJjsaF5kQIckjb4IT8Q6K6p/iPmZMixYgg=="], + + "@tailwindcss/oxide-linux-arm64-gnu": ["@tailwindcss/oxide-linux-arm64-gnu@4.3.1", "", { "os": "linux", "cpu": "arm64" }, "sha512-gqdFoVJlw444GvpnheZLHmvTzSxI/cOUUh2KSNejQjTcYkW062SVD+En0rUgD+QV91bz1XGIGtt1HJd48xUGbQ=="], + + "@tailwindcss/oxide-linux-arm64-musl": ["@tailwindcss/oxide-linux-arm64-musl@4.3.1", "", { "os": "linux", "cpu": "arm64" }, "sha512-Bwv9KwOvE0VKa86xPFif9b9c3Y1NxOV1P0gLti/IYaWEsQYZXDlxfGEtA8mdDZ7SG3wyNXAWYT5SIn3giL57oA=="], + + "@tailwindcss/oxide-linux-x64-gnu": ["@tailwindcss/oxide-linux-x64-gnu@4.3.1", "", { "os": "linux", "cpu": "x64" }, "sha512-Ymi8O8T15HYQdOUWUtTI6ldN0neHP85FC+Qz32xTcZ7iJXtem/x8ITev0o1e9e5rkqj4lONZfTRLvkmin1+tKg=="], + + "@tailwindcss/oxide-linux-x64-musl": ["@tailwindcss/oxide-linux-x64-musl@4.3.1", "", { "os": "linux", "cpu": "x64" }, "sha512-M+P/91qJ6uILLw4k2G93GMDRAXj61SMvFQYt39AqvUqYgExXpLL5aepfns7sj4HiAQeolirQF9E0lzRvdf4zPQ=="], + + "@tailwindcss/oxide-wasm32-wasi": ["@tailwindcss/oxide-wasm32-wasi@4.3.1", "", { "dependencies": { "@emnapi/core": "^1.10.0", "@emnapi/runtime": "^1.10.0", "@emnapi/wasi-threads": "^1.2.1", "@napi-rs/wasm-runtime": "^1.1.4", "@tybys/wasm-util": "^0.10.2", "tslib": "^2.8.1" }, "cpu": "none" }, "sha512-zsM8uOeqvVGHsAXsJxsT28ttosFahLJKCLOTUBqRAtKnVgGSRitds9T432QiT8b77Yga7JIBkulIRRlJPtYhRA=="], + + "@tailwindcss/oxide-win32-arm64-msvc": ["@tailwindcss/oxide-win32-arm64-msvc@4.3.1", "", { "os": "win32", "cpu": "arm64" }, "sha512-aiNvSq9BsVk8V513lDKlrCFAgf8qBMPZTpgEhInL+NwQqs97mYmupVMrPrgBBSL8Pv/0zXu9MrMF9rMun1ZeNg=="], + + "@tailwindcss/oxide-win32-x64-msvc": ["@tailwindcss/oxide-win32-x64-msvc@4.3.1", "", { "os": "win32", "cpu": "x64" }, "sha512-xDEyu1rg290472FEGaKHnzyDyh5QH+AlWvsU5hMoMtPpzmKlRI0jaYKCgSHDYtaQWZOYbMaduSyCwFwY4n1HmA=="], + + "@tailwindcss/vite": ["@tailwindcss/vite@4.3.1", "", { "dependencies": { "@tailwindcss/node": "4.3.1", "@tailwindcss/oxide": "4.3.1", "tailwindcss": "4.3.1" }, "peerDependencies": { "vite": "^5.2.0 || ^6 || ^7 || ^8" } }, "sha512-hItDHuIIlEV61R+faXu66s1K36aTurO/Qw0e45Vskz57gXl9pWOT6eg3zmcEui6CZXddbN7zd41bwmvag4JGwQ=="], + + "@tanstack/history": ["@tanstack/history@1.162.0", "", {}, "sha512-79pf/RkhteYZTRgcR4F9kbk84P2N8rugQJswxfIqovlbRiT3yI7eBE+5QorIrZaOKktsgzRlXh1l/du/xpl4iA=="], + + "@tanstack/query-core": ["@tanstack/query-core@5.101.0", "", {}, "sha512-cQetA74EB+seWySv1TTKr828TnP0u39m6LykwDXIo84SNortpDkp30TMEjkqtYCNP9c40uT/iwl6MLiufEt0Ow=="], + + "@tanstack/react-query": ["@tanstack/react-query@5.101.0", "", { "dependencies": { "@tanstack/query-core": "5.101.0" }, "peerDependencies": { "react": "^18 || ^19" } }, "sha512-rLlJXSpkqfizLWgkR5+eLeIk0MvTx/meEIR7LRjxic+qxiQP8zVjq7BqQkiCMNLQBlLfuOLqqr6KO5GtrDlmSg=="], + + "@tanstack/react-router": ["@tanstack/react-router@1.170.16", "", { "dependencies": { "@tanstack/history": "1.162.0", "@tanstack/react-store": "^0.9.3", "@tanstack/router-core": "1.171.13", "isbot": "^5.1.22" }, "peerDependencies": { "react": ">=18.0.0 || >=19.0.0", "react-dom": ">=18.0.0 || >=19.0.0" } }, "sha512-w6eq1IJklujs1tESazaK/FxH0+H2l8vm/QPuu1cD3oRW/ubgKneQpd7b64ti/8gUyEimzimJQZDmJr6YHfP5+g=="], + + "@tanstack/react-store": ["@tanstack/react-store@0.9.3", "", { "dependencies": { "@tanstack/store": "0.9.3", "use-sync-external-store": "^1.6.0" }, "peerDependencies": { "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0", "react-dom": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0" } }, "sha512-y2iHd/N9OkoQbFJLUX1T9vbc2O9tjH0pQRgTcx1/Nz4IlwLvkgpuglXUx+mXt0g5ZDFrEeDnONPqkbfxXJKwRg=="], + + "@tanstack/router-core": ["@tanstack/router-core@1.171.13", "", { "dependencies": { "@tanstack/history": "1.162.0", "cookie-es": "^3.0.0", "seroval": "^1.5.4", "seroval-plugins": "^1.5.4" } }, "sha512-+NOwEj1kO/6IGmpHRIZHasYxYWpyBQGNIZAST9aNrk9Q3YlU9SgqVnl1pbLa9qAKfeNdXQIRve0RQb/0kyDeDA=="], + + "@tanstack/store": ["@tanstack/store@0.9.3", "", {}, "sha512-8reSzl/qGWGGVKhBoxXPMWzATSbZLZFWhwBAFO9NAyp0TxzfBP0mIrGb8CP8KrQTmvzXlR/vFPPUrHTLBGyFyw=="], + + "@ts-morph/common": ["@ts-morph/common@0.27.0", "", { "dependencies": { "fast-glob": "^3.3.3", "minimatch": "^10.0.1", "path-browserify": "^1.0.1" } }, "sha512-Wf29UqxWDpc+i61k3oIOzcUfQt79PIT9y/MWfAGlrkjg6lBC1hwDECLXPVJAhWjiGbfBCxZd65F/LIZF3+jeJQ=="], + + "@tybys/wasm-util": ["@tybys/wasm-util@0.10.2", "", { "dependencies": { "tslib": "^2.4.0" } }, "sha512-RoBvJ2X0wuKlWFIjrwffGw1IqZHKQqzIchKaadZZfnNpsAYp2mM0h36JtPCjNDAHGgYez/15uMBpfGwchhiMgg=="], + + "@types/d3-array": ["@types/d3-array@3.2.2", "", {}, "sha512-hOLWVbm7uRza0BYXpIIW5pxfrKe0W+D5lrFiAEYR+pb6w3N2SwSMaJbXdUfSEv+dT4MfHBLtn5js0LAWaO6otw=="], + + "@types/d3-color": ["@types/d3-color@3.1.3", "", {}, "sha512-iO90scth9WAbmgv7ogoq57O9YpKmFBbmoEoCHDB2xMBY0+/KVrqAaCDyCE16dUspeOvIxFFRI+0sEtqDqy2b4A=="], + + "@types/d3-ease": ["@types/d3-ease@3.0.2", "", {}, "sha512-NcV1JjO5oDzoK26oMzbILE6HW7uVXOHLQvHshBUW4UMdZGfiY6v5BeQwh9a9tCzv+CeefZQHJt5SRgK154RtiA=="], + + "@types/d3-interpolate": ["@types/d3-interpolate@3.0.4", "", { "dependencies": { "@types/d3-color": "*" } }, "sha512-mgLPETlrpVV1YRJIglr4Ez47g7Yxjl1lj7YKsiMCb27VJH9W8NVM6Bb9d8kkpG/uAQS5AmbA48q2IAolKKo1MA=="], + + "@types/d3-path": ["@types/d3-path@3.1.1", "", {}, "sha512-VMZBYyQvbGmWyWVea0EHs/BwLgxc+MKi1zLDCONksozI4YJMcTt8ZEuIR4Sb1MMTE8MMW49v0IwI5+b7RmfWlg=="], + + "@types/d3-scale": ["@types/d3-scale@4.0.9", "", { "dependencies": { "@types/d3-time": "*" } }, "sha512-dLmtwB8zkAeO/juAMfnV+sItKjlsw2lKdZVVy6LRr0cBmegxSABiLEpGVmSJJ8O08i4+sGR6qQtb6WtuwJdvVw=="], + + "@types/d3-shape": ["@types/d3-shape@3.1.8", "", { "dependencies": { "@types/d3-path": "*" } }, "sha512-lae0iWfcDeR7qt7rA88BNiqdvPS5pFVPpo5OfjElwNaT2yyekbM0C9vK+yqBqEmHr6lDkRnYNoTBYlAgJa7a4w=="], + + "@types/d3-time": ["@types/d3-time@3.0.4", "", {}, "sha512-yuzZug1nkAAaBlBBikKZTgzCeA+k1uy4ZFwWANOfKw5z5LRhV0gNA7gNkKm7HoK+HRN0wX3EkxGk0fpbWhmB7g=="], + + "@types/d3-timer": ["@types/d3-timer@3.0.2", "", {}, "sha512-Ps3T8E8dZDam6fUyNiMkekK3XUsaUEik+idO9/YjPtfj2qruF8tFBXS7XhtE4iIXBLxhmLjP3SXpLhVf21I9Lw=="], + + "@types/esrecurse": ["@types/esrecurse@4.3.1", "", {}, "sha512-xJBAbDifo5hpffDBuHl0Y8ywswbiAp/Wi7Y/GtAgSlZyIABppyurxVueOPE8LUQOxdlgi6Zqce7uoEpqNTeiUw=="], + + "@types/estree": ["@types/estree@1.0.9", "", {}, "sha512-GhdPgy1el4/ImP05X05Uw4cw2/M93BCUmnEvWZNStlCzEKME4Fkk+YpoA5OiHNQmoS7Cafb8Xa3Pya8m1Qrzeg=="], + + "@types/json-schema": ["@types/json-schema@7.0.15", "", {}, "sha512-5+fP8P8MFNC+AyZCDxrB2pkZFPGzqQWUzpSeuuVLvm8VMcorNYavBqoFcxK8bQz4Qsbn4oUEEem4wDLfcysGHA=="], + + "@types/node": ["@types/node@24.13.2", "", { "dependencies": { "undici-types": "~7.18.0" } }, "sha512-fRa09kZTgu8o71KFcDjUFuc7F+dEbZYZmkI0mg5YBTRs0yMKjYHsq/c0urDKeDb+D5qVgXOdFcuu+DZPKOITwA=="], + + "@types/react": ["@types/react@19.2.17", "", { "dependencies": { "csstype": "^3.2.2" } }, "sha512-MXfmqaVPEVgkBT/aY0aGCkRWWtByiYQXo3xdQ8r5RzuFrPiRn8Gar2tQdXSUQ2GKV3bkXckek89V8wQBY2Q/Aw=="], + + "@types/react-dom": ["@types/react-dom@19.2.3", "", { "peerDependencies": { "@types/react": "^19.2.0" } }, "sha512-jp2L/eY6fn+KgVVQAOqYItbF0VY/YApe5Mz2F0aykSO8gx31bYCZyvSeYxCHKvzHG5eZjc+zyaS5BrBWya2+kQ=="], + + "@types/use-sync-external-store": ["@types/use-sync-external-store@0.0.6", "", {}, "sha512-zFDAD+tlpf2r4asuHEj0XH6pY6i0g5NeAHPn+15wk3BV6JA69eERFXC1gyGThDkVa1zCyKr5jox1+2LbV/AMLg=="], + + "@types/validate-npm-package-name": ["@types/validate-npm-package-name@4.0.2", "", {}, "sha512-lrpDziQipxCEeK5kWxvljWYhUvOiB2A9izZd9B2AFarYAkqZshb4lPbRs7zKEic6eGtH8V/2qJW+dPp9OtF6bw=="], + + "@typescript-eslint/eslint-plugin": ["@typescript-eslint/eslint-plugin@8.61.1", "", { "dependencies": { "@eslint-community/regexpp": "^4.12.2", "@typescript-eslint/scope-manager": "8.61.1", "@typescript-eslint/type-utils": "8.61.1", "@typescript-eslint/utils": "8.61.1", "@typescript-eslint/visitor-keys": "8.61.1", "ignore": "^7.0.5", "natural-compare": "^1.4.0", "ts-api-utils": "^2.5.0" }, "peerDependencies": { "@typescript-eslint/parser": "^8.61.1", "eslint": "^8.57.0 || ^9.0.0 || ^10.0.0", "typescript": ">=4.8.4 <6.1.0" } }, "sha512-ZPlVl3PB3et/59Ne0fv/sci6ZXz4T4Hp4nTJ56i/Y0gR89ARb+KphojTq6j+56E5PIezmOIOOWyY+aWQFd+IkQ=="], + + "@typescript-eslint/parser": ["@typescript-eslint/parser@8.61.1", "", { "dependencies": { "@typescript-eslint/scope-manager": "8.61.1", "@typescript-eslint/types": "8.61.1", "@typescript-eslint/typescript-estree": "8.61.1", "@typescript-eslint/visitor-keys": "8.61.1", "debug": "^4.4.3" }, "peerDependencies": { "eslint": "^8.57.0 || ^9.0.0 || ^10.0.0", "typescript": ">=4.8.4 <6.1.0" } }, "sha512-PJ5vePq5/ognBbrIcoC5+SHO5dfpeLPzP9FpLkzWrguoYQEeeSjlJpVwOpo1JRSTEi7dRcwNy4h4dzV70PqHcg=="], + + "@typescript-eslint/project-service": ["@typescript-eslint/project-service@8.61.1", "", { "dependencies": { "@typescript-eslint/tsconfig-utils": "^8.61.1", "@typescript-eslint/types": "^8.61.1", "debug": "^4.4.3" }, "peerDependencies": { "typescript": ">=4.8.4 <6.1.0" } }, "sha512-PrC4JYGmR241lYnfhmKGTXkFqv8+ymbTFgSAY0fVXpY82/QkMw5TZPl+vGzuDDU2QYJk9fIDOBTntF+yDv9LEA=="], + + "@typescript-eslint/scope-manager": ["@typescript-eslint/scope-manager@8.61.1", "", { "dependencies": { "@typescript-eslint/types": "8.61.1", "@typescript-eslint/visitor-keys": "8.61.1" } }, "sha512-L2bdIeoQS8FlKAvONAr20w6OcLXeB+qiDKbAooS9A0Ben+iSIkBef0FxqwKWYqt5sa0i4KJtxVyVmhMylKzF5w=="], + + "@typescript-eslint/tsconfig-utils": ["@typescript-eslint/tsconfig-utils@8.61.1", "", { "peerDependencies": { "typescript": ">=4.8.4 <6.1.0" } }, "sha512-UN/H4di+OO7EWx2ovME+8t31YO+KVnK0RRKEHR3kOt21/Ay8BOq3M1OMvWs5vNiqcFCYGYoxK3MXPZzmMUE+yg=="], + + "@typescript-eslint/type-utils": ["@typescript-eslint/type-utils@8.61.1", "", { "dependencies": { "@typescript-eslint/types": "8.61.1", "@typescript-eslint/typescript-estree": "8.61.1", "@typescript-eslint/utils": "8.61.1", "debug": "^4.4.3", "ts-api-utils": "^2.5.0" }, "peerDependencies": { "eslint": "^8.57.0 || ^9.0.0 || ^10.0.0", "typescript": ">=4.8.4 <6.1.0" } }, "sha512-GYRicKmVK0C4fsKgaACaknOUAq9Oa2kwsjnpFhFcS/5p4Ht5IP9OVLbgIgcK4SRk92nVHFluurg1lumD9dBcLw=="], + + "@typescript-eslint/types": ["@typescript-eslint/types@8.61.1", "", {}, "sha512-G+CRlPqLv7Bz1IZVs03x5K59F1veqL0EJUROAdGhKsEq8qOiRiZbI+HUojPq5l0fEGOKModD9br6lObhB8zkoA=="], + + "@typescript-eslint/typescript-estree": ["@typescript-eslint/typescript-estree@8.61.1", "", { "dependencies": { "@typescript-eslint/project-service": "8.61.1", "@typescript-eslint/tsconfig-utils": "8.61.1", "@typescript-eslint/types": "8.61.1", "@typescript-eslint/visitor-keys": "8.61.1", "debug": "^4.4.3", "minimatch": "^10.2.2", "semver": "^7.7.3", "tinyglobby": "^0.2.15", "ts-api-utils": "^2.5.0" }, "peerDependencies": { "typescript": ">=4.8.4 <6.1.0" } }, "sha512-u+oQD3BqYWPc8YV9Zab4vaJElJuwOLPRc10Jm1o/qS+6Qwen14HCWwx0Seo4LnSn2wxea2Ik8DxPt2/FHmuhrg=="], + + "@typescript-eslint/utils": ["@typescript-eslint/utils@8.61.1", "", { "dependencies": { "@eslint-community/eslint-utils": "^4.9.1", "@typescript-eslint/scope-manager": "8.61.1", "@typescript-eslint/types": "8.61.1", "@typescript-eslint/typescript-estree": "8.61.1" }, "peerDependencies": { "eslint": "^8.57.0 || ^9.0.0 || ^10.0.0", "typescript": ">=4.8.4 <6.1.0" } }, "sha512-1+P/3Dj6jvtybE1q0HQ6yBt/gq+oKJyLdEv4HdnqasaEXRSYCAsD59mXEVQnM/ULNdQxbX77tdG4jPRjIS6knA=="], + + "@typescript-eslint/visitor-keys": ["@typescript-eslint/visitor-keys@8.61.1", "", { "dependencies": { "@typescript-eslint/types": "8.61.1", "eslint-visitor-keys": "^5.0.0" } }, "sha512-6fJ9MHWtK14C1DSkiMlHUSOmrVebL7150xZJBlJiL62jjhIA4JmOq6flwBgDxIdBKKdoiZRel+dfPD5MLfny3w=="], + + "@vitejs/plugin-react": ["@vitejs/plugin-react@6.0.2", "", { "dependencies": { "@rolldown/pluginutils": "^1.0.0" }, "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-DlSMqo4WhThw4vB8Mpn0Woe9J+Jfq1geJ61AKW0QEgLzGMNwtIMdxbDUzLxcun8W7NbJO0e2Jg/Nxm3cCSVzzg=="], + + "accepts": ["accepts@2.0.0", "", { "dependencies": { "mime-types": "^3.0.0", "negotiator": "^1.0.0" } }, "sha512-5cvg6CtKwfgdmVqY1WIiXKc3Q1bkRqGLi+2W/6ao+6Y7gu/RCwRuAhGEzh5B4KlszSuTLgZYuqFqo5bImjNKng=="], + + "acorn": ["acorn@8.17.0", "", { "bin": { "acorn": "bin/acorn" } }, "sha512-xRQbDb9BnwDafYNn6Vwl839DYVjqXYb1XVGtWAZ1kcDc6iwAL4hg3B1dZlRiuENFeO2H53gFG3in621AdERVAg=="], + + "acorn-jsx": ["acorn-jsx@5.3.2", "", { "peerDependencies": { "acorn": "^6.0.0 || ^7.0.0 || ^8.0.0" } }, "sha512-rq9s+JNhf0IChjtDXxllJ7g41oZk5SlXtp0LHwyA5cejwn7vKmKp4pPri6YEePv2PU65sAsegbXtIinmDFDXgQ=="], + + "agent-base": ["agent-base@7.1.4", "", {}, "sha512-MnA+YT8fwfJPgBx3m60MNqakm30XOkyIoH1y6huTQvC0PwZG7ki8NacLBcrPbNoo8vEZy7Jpuk7+jMO+CUovTQ=="], + + "ajv": ["ajv@6.15.0", "", { "dependencies": { "fast-deep-equal": "^3.1.1", "fast-json-stable-stringify": "^2.0.0", "json-schema-traverse": "^0.4.1", "uri-js": "^4.2.2" } }, "sha512-fgFx7Hfoq60ytK2c7DhnF8jIvzYgOMxfugjLOSMHjLIPgenqa7S7oaagATUq99mV6IYvN2tRmC0wnTYX6iPbMw=="], + + "ajv-formats": ["ajv-formats@3.0.1", "", { "dependencies": { "ajv": "^8.0.0" } }, "sha512-8iUql50EUR+uUcdRQ3HDqa6EVyo3docL8g5WJ3FNcWmu62IbkGUue/pEyLBW8VGKKucTPgqeks4fIU1DA4yowQ=="], + + "ansi-colors": ["ansi-colors@4.1.3", "", {}, "sha512-/6w/C21Pm1A7aZitlI5Ni/2J6FFQN8i1Cvz3kHABAAbw93v/NlvKdVOqz7CCWz/3iv/JplRSEEZ83XION15ovw=="], + + "ansi-regex": ["ansi-regex@6.2.2", "", {}, "sha512-Bq3SmSpyFHaWjPk8If9yc6svM8c56dB5BAtW4Qbw5jHTwwXXcTLoRMkpDJp6VL0XzlWaCHTXrkFURMYmD0sLqg=="], + + "argparse": ["argparse@2.0.1", "", {}, "sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q=="], + + "aria-hidden": ["aria-hidden@1.2.6", "", { "dependencies": { "tslib": "^2.0.0" } }, "sha512-ik3ZgC9dY/lYVVM++OISsaYDeg1tb0VtP5uL3ouh1koGOaUMDPpbFIei4JkFimWUFPn90sbMNMXQAIVOlnYKJA=="], + + "ast-types": ["ast-types@0.16.1", "", { "dependencies": { "tslib": "^2.0.1" } }, "sha512-6t10qk83GOG8p0vKmaCr8eiilZwO171AvbROMtvvNiwrTly62t+7XkA8RdIIVbpMhCASAsxgAzdRSwh6nw/5Dg=="], + + "atomically": ["atomically@1.7.0", "", {}, "sha512-Xcz9l0z7y9yQ9rdDaxlmaI4uJHf/T8g9hOEzJcsEqX2SjCj4J20uK7+ldkDHMbpJDK76wF7xEIgxc/vSlsfw5w=="], + + "balanced-match": ["balanced-match@4.0.4", "", {}, "sha512-BLrgEcRTwX2o6gGxGOCNyMvGSp35YofuYzw9h1IMTRmKqttAZZVU67bdb9Pr2vUHA8+j3i2tJfjO6C6+4myGTA=="], + + "baseline-browser-mapping": ["baseline-browser-mapping@2.10.38", "", { "bin": { "baseline-browser-mapping": "dist/cli.cjs" } }, "sha512-31/02mVB4yuQU6adKk5SlY6m+mxDwUq5KZkyYgnLrrKl7TEm1+3PyDtDBz2kOv/wxZz41GHsvV1A/u6RmiyBvw=="], + + "body-parser": ["body-parser@2.3.0", "", { "dependencies": { "bytes": "^3.1.2", "content-type": "^2.0.0", "debug": "^4.4.3", "http-errors": "^2.0.1", "iconv-lite": "^0.7.2", "on-finished": "^2.4.1", "qs": "^6.15.2", "raw-body": "^3.0.2", "type-is": "^2.1.0" } }, "sha512-2cGmJupaNgg+QUwVLAucDuWuoMZ6EX9iHDRswZ5lsNYEmwPaRknMPCLZz07yTzVq/83p4o/wzbDZbBrTvGGTIw=="], + + "brace-expansion": ["brace-expansion@5.0.6", "", { "dependencies": { "balanced-match": "^4.0.2" } }, "sha512-kLpxurY4Z4r9sgMsyG0Z9uzsBlgiU/EFKhj/h91/8yHu0edo7XuixOIH3VcJ8kkxs6/jPzoI6U9Vj3WqbMQ94g=="], + + "braces": ["braces@3.0.3", "", { "dependencies": { "fill-range": "^7.1.1" } }, "sha512-yQbXgO/OSZVD2IsiLlro+7Hf6Q18EJrKSEsdoMzKePKXct3gvD8oLcOQdIzGupr5Fj+EDe8gO/lxc1BzfMpxvA=="], + + "browserslist": ["browserslist@4.28.2", "", { "dependencies": { "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" } }, "sha512-48xSriZYYg+8qXna9kwqjIVzuQxi+KYWp2+5nCYnYKPTr0LvD89Jqk2Or5ogxz0NUMfIjhh2lIUX/LyX9B4oIg=="], + + "bundle-name": ["bundle-name@4.1.0", "", { "dependencies": { "run-applescript": "^7.0.0" } }, "sha512-tjwM5exMg6BGRI+kNmTntNsvdZS1X8BFYS6tnJ2hdH0kVxM6/eVZ2xy+FqStSWvYmtfFMDLIxurorHwDKfDz5Q=="], + + "bytes": ["bytes@3.1.2", "", {}, "sha512-/Nf7TyzTx6S3yRJObOAV7956r8cr2+Oj8AC5dt8wSP3BQAoeX58NoHyCU8P8zGkNXStjTSi6fzO6F0pBdcYbEg=="], + + "call-bind-apply-helpers": ["call-bind-apply-helpers@1.0.2", "", { "dependencies": { "es-errors": "^1.3.0", "function-bind": "^1.1.2" } }, "sha512-Sp1ablJ0ivDkSzjcaJdxEunN5/XvksFJ2sMBFfq6x0ryhQV/2b/KwFe21cMpmHtPOSij8K99/wSfoEuTObmuMQ=="], + + "call-bound": ["call-bound@1.0.4", "", { "dependencies": { "call-bind-apply-helpers": "^1.0.2", "get-intrinsic": "^1.3.0" } }, "sha512-+ys997U96po4Kx/ABpBCqhA9EuxJaQWDQg7295H4hBphv3IZg0boBKuwYpt4YXp6MZ5AmZQnU/tyMTlRpaSejg=="], + + "callsites": ["callsites@3.1.0", "", {}, "sha512-P8BjAsXvZS+VIDUI11hHCQEv74YT67YUi5JJFNWIqL235sBmjX4+qx9Muvls5ivyNENctx46xQLQ3aTuE7ssaQ=="], + + "caniuse-lite": ["caniuse-lite@1.0.30001799", "", {}, "sha512-hG1bReV+OUU+MOqK4t/ZWI0tZOyz3rqS9XuhOUz1cIcbwBKjOyJEJuw9ER5JuNyqxNk8u/JUVbGibBOL1yrjFw=="], + + "chalk": ["chalk@5.6.2", "", {}, "sha512-7NzBL0rN6fMUW+f7A6Io4h40qQlG+xGmtMxfbnH/K7TAtt8JQWVQK+6g0UXKMeVJoyV5EkkNsErQ8pVD3bLHbA=="], + + "class-variance-authority": ["class-variance-authority@0.7.1", "", { "dependencies": { "clsx": "^2.1.1" } }, "sha512-Ka+9Trutv7G8M6WT6SeiRWz792K5qEqIGEGzXKhAE6xOWAY6pPH8U+9IY3oCMv6kqTmLsv7Xh/2w2RigkePMsg=="], + + "cli-cursor": ["cli-cursor@5.0.0", "", { "dependencies": { "restore-cursor": "^5.0.0" } }, "sha512-aCj4O5wKyszjMmDT4tZj93kxyydN/K5zPWSCe6/0AV/AA1pqe5ZBIw0a2ZfPQV7lL5/yb5HsUreJ6UFAF1tEQw=="], + + "cli-spinners": ["cli-spinners@2.9.2", "", {}, "sha512-ywqV+5MmyL4E7ybXgKys4DugZbX0FC6LnwrhjuykIjnK9k8OQacQ7axGKnjDXWNhns0xot3bZI5h55H8yo9cJg=="], + + "clsx": ["clsx@2.1.1", "", {}, "sha512-eYm0QWBtUrBWZWG0d386OGAw16Z995PiOVo2B7bjWSbHedGl5e0ZWaq65kOGgUSNesEIDkB9ISbTg/JK9dhCZA=="], + + "cmdk": ["cmdk@1.1.1", "", { "dependencies": { "@radix-ui/react-compose-refs": "^1.1.1", "@radix-ui/react-dialog": "^1.1.6", "@radix-ui/react-id": "^1.1.0", "@radix-ui/react-primitive": "^2.0.2" }, "peerDependencies": { "react": "^18 || ^19 || ^19.0.0-rc", "react-dom": "^18 || ^19 || ^19.0.0-rc" } }, "sha512-Vsv7kFaXm+ptHDMZ7izaRsP70GgrW9NBNGswt9OZaVBLlE0SNpDq8eu/VGXyF9r7M0azK3Wy7OlYXsuyYLFzHg=="], + + "code-block-writer": ["code-block-writer@13.0.3", "", {}, "sha512-Oofo0pq3IKnsFtuHqSF7TqBfr71aeyZDVJ0HpmqB7FBM2qEigL0iPONSCZSO9pE9dZTAxANe5XHG9Uy0YMv8cg=="], + + "commander": ["commander@14.0.3", "", {}, "sha512-H+y0Jo/T1RZ9qPP4Eh1pkcQcLRglraJaSLoyOtHxu6AapkjWVCy2Sit1QQ4x3Dng8qDlSsZEet7g5Pq06MvTgw=="], + + "conf": ["conf@10.2.0", "", { "dependencies": { "ajv": "^8.6.3", "ajv-formats": "^2.1.1", "atomically": "^1.7.0", "debounce-fn": "^4.0.0", "dot-prop": "^6.0.1", "env-paths": "^2.2.1", "json-schema-typed": "^7.0.3", "onetime": "^5.1.2", "pkg-up": "^3.1.0", "semver": "^7.3.5" } }, "sha512-8fLl9F04EJqjSqH+QjITQfJF8BrOVaYr1jewVgSRAEWePfxT0sku4w2hrGQ60BC/TNLGQ2pgxNlTbWQmMPFvXg=="], + + "content-disposition": ["content-disposition@1.1.0", "", {}, "sha512-5jRCH9Z/+DRP7rkvY83B+yGIGX96OYdJmzngqnw2SBSxqCFPd0w2km3s5iawpGX8krnwSGmF0FW5Nhr0Hfai3g=="], + + "content-type": ["content-type@1.0.5", "", {}, "sha512-nTjqfcBFEipKdXCv4YDQWCfmcLZKm81ldF0pAopTvyrFGVbcR6P/VAAd5G7N+0tTr8QqiU0tFadD6FK4NtJwOA=="], + + "convert-source-map": ["convert-source-map@2.0.0", "", {}, "sha512-Kvp459HrV2FEJ1CAsi1Ku+MY3kasH19TFykTz2xWmMeq6bk2NU3XXvfJ+Q61m0xktWwt+1HSYf3JZsTms3aRJg=="], + + "cookie": ["cookie@0.7.2", "", {}, "sha512-yki5XnKuf750l50uGTllt6kKILY4nQ1eNIQatoXEByZ5dWgnKqbnqmTrBE5B4N7lrMJKQ2ytWMiTO2o0v6Ew/w=="], + + "cookie-es": ["cookie-es@3.1.1", "", {}, "sha512-UaXxwISYJPTr9hwQxMFYZ7kNhSXboMXP+Z3TRX6f1/NyaGPfuNUZOWP1pUEb75B2HjfklIYLVRfWiFZJyC6Npg=="], + + "cookie-signature": ["cookie-signature@1.2.2", "", {}, "sha512-D76uU73ulSXrD1UXF4KE2TMxVVwhsnCgfAyTg9k8P6KGZjlXKrOLe4dJQKI3Bxi5wjesZoFXJWElNWBjPZMbhg=="], + + "cors": ["cors@2.8.6", "", { "dependencies": { "object-assign": "^4", "vary": "^1" } }, "sha512-tJtZBBHA6vjIAaF6EnIaq6laBBP9aq/Y3ouVJjEfoHbRBcHBAHYcMh/w8LDrk2PvIMMq8gmopa5D4V8RmbrxGw=="], + + "cosmiconfig": ["cosmiconfig@9.0.2", "", { "dependencies": { "env-paths": "^2.2.1", "import-fresh": "^3.3.0", "js-yaml": "^4.1.0", "parse-json": "^5.2.0" }, "peerDependencies": { "typescript": ">=4.9.5" }, "optionalPeers": ["typescript"] }, "sha512-gtTZxTDau1wL7Y7zifc2dd8jHSK/k6BTx/2Xp/BpdlAdnlYWFVt7qhJqgwi7637yRwRQ3qL4ZidbB4I8tA5VOg=="], + + "cross-spawn": ["cross-spawn@7.0.6", "", { "dependencies": { "path-key": "^3.1.0", "shebang-command": "^2.0.0", "which": "^2.0.1" } }, "sha512-uV2QOWP2nWzsy2aMp8aRibhi9dlzF5Hgh5SHaB9OiTGEyDTiJJyx0uy51QXdyWbtAHNua4XJzUKca3OzKUd3vA=="], + + "cssesc": ["cssesc@3.0.0", "", { "bin": { "cssesc": "bin/cssesc" } }, "sha512-/Tb/JcjK111nNScGob5MNtsntNM1aCNUDipB/TkwZFhyDrrE47SOx/18wF2bbjgc3ZzCSKW1T5nt5EbFoAz/Vg=="], + + "csstype": ["csstype@3.2.3", "", {}, "sha512-z1HGKcYy2xA8AGQfwrn0PAy+PB7X/GSj3UVJW9qKyn43xWa+gl5nXmU4qqLMRzWVLFC8KusUX8T/0kCiOYpAIQ=="], + + "d3-array": ["d3-array@3.2.4", "", { "dependencies": { "internmap": "1 - 2" } }, "sha512-tdQAmyA18i4J7wprpYq8ClcxZy3SC31QMeByyCFyRt7BVHdREQZ5lpzoe5mFEYZUWe+oq8HBvk9JjpibyEV4Jg=="], + + "d3-color": ["d3-color@3.1.0", "", {}, "sha512-zg/chbXyeBtMQ1LbD/WSoW2DpC3I0mpmPdW+ynRTj/x2DAWYrIY7qeZIHidozwV24m4iavr15lNwIwLxRmOxhA=="], + + "d3-ease": ["d3-ease@3.0.1", "", {}, "sha512-wR/XK3D3XcLIZwpbvQwQ5fK+8Ykds1ip7A2Txe0yxncXSdq1L9skcG7blcedkOX+ZcgxGAmLX1FrRGbADwzi0w=="], + + "d3-format": ["d3-format@3.1.2", "", {}, "sha512-AJDdYOdnyRDV5b6ArilzCPPwc1ejkHcoyFarqlPqT7zRYjhavcT3uSrqcMvsgh2CgoPbK3RCwyHaVyxYcP2Arg=="], + + "d3-interpolate": ["d3-interpolate@3.0.1", "", { "dependencies": { "d3-color": "1 - 3" } }, "sha512-3bYs1rOD33uo8aqJfKP3JWPAibgw8Zm2+L9vBKEHJ2Rg+viTR7o5Mmv5mZcieN+FRYaAOWX5SJATX6k1PWz72g=="], + + "d3-path": ["d3-path@3.1.0", "", {}, "sha512-p3KP5HCf/bvjBSSKuXid6Zqijx7wIfNW+J/maPs+iwR35at5JCbLUT0LzF1cnjbCHWhqzQTIN2Jpe8pRebIEFQ=="], + + "d3-scale": ["d3-scale@4.0.2", "", { "dependencies": { "d3-array": "2.10.0 - 3", "d3-format": "1 - 3", "d3-interpolate": "1.2.0 - 3", "d3-time": "2.1.1 - 3", "d3-time-format": "2 - 4" } }, "sha512-GZW464g1SH7ag3Y7hXjf8RoUuAFIqklOAq3MRl4OaWabTFJY9PN/E1YklhXLh+OQ3fM9yS2nOkCoS+WLZ6kvxQ=="], + + "d3-shape": ["d3-shape@3.2.0", "", { "dependencies": { "d3-path": "^3.1.0" } }, "sha512-SaLBuwGm3MOViRq2ABk3eLoxwZELpH6zhl3FbAoJ7Vm1gofKx6El1Ib5z23NUEhF9AsGl7y+dzLe5Cw2AArGTA=="], + + "d3-time": ["d3-time@3.1.0", "", { "dependencies": { "d3-array": "2 - 3" } }, "sha512-VqKjzBLejbSMT4IgbmVgDjpkYrNWUYJnbCGo874u7MMKIWsILRX+OpX/gTk8MqjpT1A/c6HY2dCA77ZN0lkQ2Q=="], + + "d3-time-format": ["d3-time-format@4.1.0", "", { "dependencies": { "d3-time": "1 - 3" } }, "sha512-dJxPBlzC7NugB2PDLwo9Q8JiTR3M3e4/XANkreKSUxF8vvXKqm1Yfq4Q5dl8budlunRVlUUaDUgFt7eA8D6NLg=="], + + "d3-timer": ["d3-timer@3.0.1", "", {}, "sha512-ndfJ/JxxMd3nw31uyKoY2naivF+r29V+Lc0svZxe1JvvIRmi8hUsrMvdOwgS1o6uBHmiz91geQ0ylPP0aj1VUA=="], + + "data-uri-to-buffer": ["data-uri-to-buffer@4.0.1", "", {}, "sha512-0R9ikRb668HB7QDxT1vkpuUBtqc53YyAwMwGeUFKRojY/NWKvdZ+9UYtRfGmhqNbRkTSVpMbmyhXipFFv2cb/A=="], + + "date-fns": ["date-fns@4.4.0", "", {}, "sha512-+1UMbeh68lH1SegH83CGWwpb6OHHbpSgr3+s5Eww5M4CAgswBpoWS0AjTOfEJ33HiYKz1hdj/KTFprzXHmq/6w=="], + + "debounce-fn": ["debounce-fn@4.0.0", "", { "dependencies": { "mimic-fn": "^3.0.0" } }, "sha512-8pYCQiL9Xdcg0UPSD3d+0KMlOjp+KGU5EPwYddgzQ7DATsg4fuUDjQtsYLmWjnk2obnNHgV3vE2Y4jejSOJVBQ=="], + + "debug": ["debug@4.4.3", "", { "dependencies": { "ms": "^2.1.3" } }, "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA=="], + + "decimal.js-light": ["decimal.js-light@2.5.1", "", {}, "sha512-qIMFpTMZmny+MMIitAB6D7iVPEorVw6YQRWkvarTkT4tBeSLLiHzcwj6q0MmYSFCiVpiqPJTJEYIrpcPzVEIvg=="], + + "dedent": ["dedent@1.7.2", "", { "peerDependencies": { "babel-plugin-macros": "^3.1.0" }, "optionalPeers": ["babel-plugin-macros"] }, "sha512-WzMx3mW98SN+zn3hgemf4OzdmyNhhhKz5Ay0pUfQiMQ3e1g+xmTJWp/pKdwKVXhdSkAEGIIzqeuWrL3mV/AXbA=="], + + "deep-is": ["deep-is@0.1.4", "", {}, "sha512-oIPzksmTg4/MriiaYGO+okXDT7ztn/w3Eptv/+gSIdMdKsJo0u4CfYNFJPy+4SKMuCqGw2wxnA+URMg3t8a/bQ=="], + + "deepmerge": ["deepmerge@4.3.1", "", {}, "sha512-3sUqbMEc77XqpdNO7FRyRog+eW3ph+GYCbj+rK+uYyRMuwsVy0rMiVtPn+QJlKFvWP/1PYpapqYn0Me2knFn+A=="], + + "default-browser": ["default-browser@5.5.0", "", { "dependencies": { "bundle-name": "^4.1.0", "default-browser-id": "^5.0.0" } }, "sha512-H9LMLr5zwIbSxrmvikGuI/5KGhZ8E2zH3stkMgM5LpOWDutGM2JZaj460Udnf1a+946zc7YBgrqEWwbk7zHvGw=="], + + "default-browser-id": ["default-browser-id@5.0.1", "", {}, "sha512-x1VCxdX4t+8wVfd1so/9w+vQ4vx7lKd2Qp5tDRutErwmR85OgmfX7RlLRMWafRMY7hbEiXIbudNrjOAPa/hL8Q=="], + + "define-lazy-prop": ["define-lazy-prop@3.0.0", "", {}, "sha512-N+MeXYoqr3pOgn8xfyRPREN7gHakLYjhsHhWGT3fWAiL4IkAt0iDw14QiiEm2bE30c5XX5q0FtAA3CK5f9/BUg=="], + + "depd": ["depd@2.0.0", "", {}, "sha512-g7nH6P6dyDioJogAAGprGpCtVImJhpPk/roCzdb3fIh61/s/nPsfR6onyMwkCAR/OlC3yBC0lESvUoQEAssIrw=="], + + "detect-libc": ["detect-libc@2.1.2", "", {}, "sha512-Btj2BOOO83o3WyH59e8MgXsxEQVcarkUOpEYrubB0urwnN10yQ364rsiByU11nZlqWYZm05i/of7io4mzihBtQ=="], + + "detect-node-es": ["detect-node-es@1.1.0", "", {}, "sha512-ypdmJU/TbBby2Dxibuv7ZLW3Bs1QEmM7nHjEANfohJLvE0XVujisn1qPJcZxg+qDucsr+bP6fLD1rPS3AhJ7EQ=="], + + "diff": ["diff@8.0.4", "", {}, "sha512-DPi0FmjiSU5EvQV0++GFDOJ9ASQUVFh5kD+OzOnYdi7n3Wpm9hWWGfB/O2blfHcMVTL5WkQXSnRiK9makhrcnw=="], + + "dot-prop": ["dot-prop@6.0.1", "", { "dependencies": { "is-obj": "^2.0.0" } }, "sha512-tE7ztYzXHIeyvc7N+hR3oi7FIbf/NIjVP9hmAt3yMXzrQ072/fpjGLx2GxNxGxUl5V73MEqYzioOMoVhGMJ5cA=="], + + "dotenv": ["dotenv@17.4.2", "", {}, "sha512-nI4U3TottKAcAD9LLud4Cb7b2QztQMUEfHbvhTH09bqXTxnSie8WnjPALV/WMCrJZ6UV/qHJ6L03OqO3LcdYZw=="], + + "dunder-proto": ["dunder-proto@1.0.1", "", { "dependencies": { "call-bind-apply-helpers": "^1.0.1", "es-errors": "^1.3.0", "gopd": "^1.2.0" } }, "sha512-KIN/nDJBQRcXw0MLVhZE9iQHmG68qAVIBg9CqmUYjmQIhgij9U5MFvrqkUL5FbtyyzZuOeOt0zdeRe4UY7ct+A=="], + + "eciesjs": ["eciesjs@0.4.18", "", { "dependencies": { "@ecies/ciphers": "^0.2.5", "@noble/ciphers": "^1.3.0", "@noble/curves": "^1.9.7", "@noble/hashes": "^1.8.0" } }, "sha512-wG99Zcfcys9fZux7Cft8BAX/YrOJLJSZ3jyYPfhZHqN2E+Ffx+QXBDsv3gubEgPtV6dTzJMSQUwk1H98/t/0wQ=="], + + "ee-first": ["ee-first@1.1.1", "", {}, "sha512-WMwm9LhRUo+WUaRN+vRuETqG89IgZphVSNkdFgeb6sS/E4OrDIN7t48CAewSHXc6C8lefD8KKfr5vY61brQlow=="], + + "electron-to-chromium": ["electron-to-chromium@1.5.375", "", {}, "sha512-ZWP5eB4BVPW/ZYo9252hQZHZ5XavtsTgpbhcmMmRwymavC5AsLWQWBPaKMeNd2LW0KGby5HPXvj7+sr4ta5j/Q=="], + + "embla-carousel": ["embla-carousel@8.6.0", "", {}, "sha512-SjWyZBHJPbqxHOzckOfo8lHisEaJWmwd23XppYFYVh10bU66/Pn5tkVkbkCMZVdbUE5eTCI2nD8OyIP4Z+uwkA=="], + + "embla-carousel-react": ["embla-carousel-react@8.6.0", "", { "dependencies": { "embla-carousel": "8.6.0", "embla-carousel-reactive-utils": "8.6.0" }, "peerDependencies": { "react": "^16.8.0 || ^17.0.1 || ^18.0.0 || ^19.0.0 || ^19.0.0-rc" } }, "sha512-0/PjqU7geVmo6F734pmPqpyHqiM99olvyecY7zdweCw+6tKEXnrE90pBiBbMMU8s5tICemzpQ3hi5EpxzGW+JA=="], + + "embla-carousel-reactive-utils": ["embla-carousel-reactive-utils@8.6.0", "", { "peerDependencies": { "embla-carousel": "8.6.0" } }, "sha512-fMVUDUEx0/uIEDM0Mz3dHznDhfX+znCCDCeIophYb1QGVM7YThSWX+wz11zlYwWFOr74b4QLGg0hrGPJeG2s4A=="], + + "emoji-regex": ["emoji-regex@10.6.0", "", {}, "sha512-toUI84YS5YmxW219erniWD0CIVOo46xGKColeNQRgOzDorgBi1v4D71/OFzgD9GO2UGKIv1C3Sp8DAn0+j5w7A=="], + + "encodeurl": ["encodeurl@2.0.0", "", {}, "sha512-Q0n9HRi4m6JuGIV1eFlmvJB7ZEVxu93IrMyiMsGC0lrMJMWzRgx6WGquyfQgZVb31vhGgXnfmPNNXmxnOkRBrg=="], + + "enhanced-resolve": ["enhanced-resolve@5.21.6", "", { "dependencies": { "graceful-fs": "^4.2.4", "tapable": "^2.3.3" } }, "sha512-aNnGCvbJ/RIyWo1IuhNdVjnNF+EjH9wpzpNHt+ci/m9He9LJvUN8wrCcXjp9cWsGNAuvSpVFTx/vraAFQ8qGjQ=="], + + "enquirer": ["enquirer@2.4.1", "", { "dependencies": { "ansi-colors": "^4.1.1", "strip-ansi": "^6.0.1" } }, "sha512-rRqJg/6gd538VHvR3PSrdRBb/1Vy2YfzHqzvbhGIQpDRKIa4FgV/54b5Q1xYSxOOwKvjXweS26E0Q+nAMwp2pQ=="], + + "env-paths": ["env-paths@2.2.1", "", {}, "sha512-+h1lkLKhZMTYjog1VEpJNG7NZJWcuc2DDk/qsqSTRRCOXiLjeQ1d1/udrUGhqMxUgAlwKNZ0cf2uqan5GLuS2A=="], + + "error-ex": ["error-ex@1.3.4", "", { "dependencies": { "is-arrayish": "^0.2.1" } }, "sha512-sqQamAnR14VgCr1A618A3sGrygcpK+HEbenA/HiEAkkUwcZIIB/tgWqHFxWgOyDh4nB4JCRimh79dR5Ywc9MDQ=="], + + "es-define-property": ["es-define-property@1.0.1", "", {}, "sha512-e3nRfgfUZ4rNGL232gUgX06QNyyez04KdjFrF+LTRoOXmrOgFKDg4BCdsjW8EnT69eqdYGmRpJwiPVYNrCaW3g=="], + + "es-errors": ["es-errors@1.3.0", "", {}, "sha512-Zf5H2Kxt2xjTvbJvP2ZWLEICxA6j+hAmMzIlypy4xcBg1vKVnx89Wy0GbS+kf5cwCVFFzdCFh2XSCFNULS6csw=="], + + "es-object-atoms": ["es-object-atoms@1.1.2", "", { "dependencies": { "es-errors": "^1.3.0" } }, "sha512-HWcBoN6NileqtSydK2FqHbS/LoDd2pqrnQHLyJzBj4kOp/ky2MWMN694xOfkK8/SnUsW2DH7EfyVlydKCsm1Zw=="], + + "es-toolkit": ["es-toolkit@1.47.1", "", {}, "sha512-5RAqEwf4P4E17p+W75KLOWw/nOvKZzSQpxM32IpI2KZLaVonjTrZ0Ai5ghMaVI9eKC2p8eoQgcBdkEDgzFk6+Q=="], + + "escalade": ["escalade@3.2.0", "", {}, "sha512-WUj2qlxaQtO4g6Pq5c29GTcWGDyd8itL8zTlipgECz3JesAiiOKotd8JU6otB3PACgG6xkJUyVhboMS+bje/jA=="], + + "escape-html": ["escape-html@1.0.3", "", {}, "sha512-NiSupZ4OeuGwr68lGIeym/ksIZMJodUGOSCZ/FSnTxcrekbvqrgdUxlJOMpijaKZVjAJrWrGs/6Jy8OMuyj9ow=="], + + "escape-string-regexp": ["escape-string-regexp@4.0.0", "", {}, "sha512-TtpcNJ3XAzx3Gq8sWRzJaVajRs0uVxA2YAkdb1jm2YkPz4G6egUFAyA3n5vtEIZefPk5Wa4UXbKuS5fKkJWdgA=="], + + "eslint": ["eslint@10.5.0", "", { "dependencies": { "@eslint-community/eslint-utils": "^4.8.0", "@eslint-community/regexpp": "^4.12.2", "@eslint/config-array": "^0.23.5", "@eslint/config-helpers": "^0.6.0", "@eslint/core": "^1.2.1", "@eslint/plugin-kit": "^0.7.2", "@humanfs/node": "^0.16.6", "@humanwhocodes/module-importer": "^1.0.1", "@humanwhocodes/retry": "^0.4.2", "@types/estree": "^1.0.6", "ajv": "^6.14.0", "cross-spawn": "^7.0.6", "debug": "^4.3.2", "escape-string-regexp": "^4.0.0", "eslint-scope": "^9.1.2", "eslint-visitor-keys": "^5.0.1", "espree": "^11.2.0", "esquery": "^1.7.0", "esutils": "^2.0.2", "fast-deep-equal": "^3.1.3", "file-entry-cache": "^8.0.0", "find-up": "^5.0.0", "glob-parent": "^6.0.2", "ignore": "^5.2.0", "imurmurhash": "^0.1.4", "is-glob": "^4.0.0", "json-stable-stringify-without-jsonify": "^1.0.1", "minimatch": "^10.2.4", "natural-compare": "^1.4.0", "optionator": "^0.9.3" }, "peerDependencies": { "jiti": "*" }, "optionalPeers": ["jiti"], "bin": { "eslint": "bin/eslint.js" } }, "sha512-1y+7C+vi12bUK1IpZeaV3gsH9fHLBmPvYmPx42pvT/E9yG0IC8g3PUZZgp0+JLJl7ZDK0flc2gc+Aw9dpCvIsQ=="], + + "eslint-plugin-react-hooks": ["eslint-plugin-react-hooks@7.1.1", "", { "dependencies": { "@babel/core": "^7.24.4", "@babel/parser": "^7.24.4", "hermes-parser": "^0.25.1", "zod": "^3.25.0 || ^4.0.0", "zod-validation-error": "^3.5.0 || ^4.0.0" }, "peerDependencies": { "eslint": "^3.0.0 || ^4.0.0 || ^5.0.0 || ^6.0.0 || ^7.0.0 || ^8.0.0-0 || ^9.0.0 || ^10.0.0" } }, "sha512-f2I7Gw6JbvCexzIInuSbZpfdQ44D7iqdWX01FKLvrPgqxoE7oMj8clOfto8U6vYiz4yd5oKu39rRSVOe1zRu0g=="], + + "eslint-plugin-react-refresh": ["eslint-plugin-react-refresh@0.5.3", "", { "peerDependencies": { "eslint": "^9 || ^10" } }, "sha512-5EMmLCV98Pi4o/f/3DP/v/tNqLHMIc9I8LKClNDWhZ9JTho89/kQcitCXQBMG7sAfVRK0Ie3T2EDOzp1YXYiVA=="], + + "eslint-scope": ["eslint-scope@9.1.2", "", { "dependencies": { "@types/esrecurse": "^4.3.1", "@types/estree": "^1.0.8", "esrecurse": "^4.3.0", "estraverse": "^5.2.0" } }, "sha512-xS90H51cKw0jltxmvmHy2Iai1LIqrfbw57b79w/J7MfvDfkIkFZ+kj6zC3BjtUwh150HsSSdxXZcsuv72miDFQ=="], + + "eslint-visitor-keys": ["eslint-visitor-keys@5.0.1", "", {}, "sha512-tD40eHxA35h0PEIZNeIjkHoDR4YjjJp34biM0mDvplBe//mB+IHCqHDGV7pxF+7MklTvighcCPPZC7ynWyjdTA=="], + + "espree": ["espree@11.2.0", "", { "dependencies": { "acorn": "^8.16.0", "acorn-jsx": "^5.3.2", "eslint-visitor-keys": "^5.0.1" } }, "sha512-7p3DrVEIopW1B1avAGLuCSh1jubc01H2JHc8B4qqGblmg5gI9yumBgACjWo4JlIc04ufug4xJ3SQI8HkS/Rgzw=="], + + "esprima": ["esprima@4.0.1", "", { "bin": { "esparse": "./bin/esparse.js", "esvalidate": "./bin/esvalidate.js" } }, "sha512-eGuFFw7Upda+g4p+QHvnW0RyTX/SVeJBDM/gCtMARO0cLuT2HcEKnTPvhjV6aGeqrCB/sbNop0Kszm0jsaWU4A=="], + + "esquery": ["esquery@1.7.0", "", { "dependencies": { "estraverse": "^5.1.0" } }, "sha512-Ap6G0WQwcU/LHsvLwON1fAQX9Zp0A2Y6Y/cJBl9r/JbW90Zyg4/zbG6zzKa2OTALELarYHmKu0GhpM5EO+7T0g=="], + + "esrecurse": ["esrecurse@4.3.0", "", { "dependencies": { "estraverse": "^5.2.0" } }, "sha512-KmfKL3b6G+RXvP8N1vr3Tq1kL/oCFgn2NYXEtqP8/L3pKapUA4G8cFVaoF3SU323CD4XypR/ffioHmkti6/Tag=="], + + "estraverse": ["estraverse@5.3.0", "", {}, "sha512-MMdARuVEQziNTeJD8DgMqmhwR11BRQ/cBP+pLtYdSTnf3MIO8fFeiINEbX36ZdNlfU/7A9f3gUw49B3oQsvwBA=="], + + "esutils": ["esutils@2.0.3", "", {}, "sha512-kVscqXk4OCp68SZ0dkgEKVi6/8ij300KBWTJq32P/dYeWTSwK41WyTxalN1eRmA5Z9UU/LX9D7FWSmV9SAYx6g=="], + + "etag": ["etag@1.8.1", "", {}, "sha512-aIL5Fx7mawVa300al2BnEE4iNvo1qETxLrPI/o05L7z6go7fCw1J6EQmbK4FmJ2AS7kgVF/KEZWufBfdClMcPg=="], + + "eventemitter3": ["eventemitter3@5.0.4", "", {}, "sha512-mlsTRyGaPBjPedk6Bvw+aqbsXDtoAyAzm5MO7JgU+yVRyMQ5O8bD4Kcci7BS85f93veegeCPkL8R4GLClnjLFw=="], + + "eventsource": ["eventsource@3.0.7", "", { "dependencies": { "eventsource-parser": "^3.0.1" } }, "sha512-CRT1WTyuQoD771GW56XEZFQ/ZoSfWid1alKGDYMmkt2yl8UXrVR4pspqWNEcqKvVIzg6PAltWjxcSSPrboA4iA=="], + + "eventsource-parser": ["eventsource-parser@3.1.0", "", {}, "sha512-kJezFj9YFAMLeORyi7aCLxLbD5/qWMQnoMVlVPyHIll7lgRJCc3JVln9Vgl9nwQi0YkMnhdGTMNn7CkRRAptMg=="], + + "execa": ["execa@9.6.1", "", { "dependencies": { "@sindresorhus/merge-streams": "^4.0.0", "cross-spawn": "^7.0.6", "figures": "^6.1.0", "get-stream": "^9.0.0", "human-signals": "^8.0.1", "is-plain-obj": "^4.1.0", "is-stream": "^4.0.1", "npm-run-path": "^6.0.0", "pretty-ms": "^9.2.0", "signal-exit": "^4.1.0", "strip-final-newline": "^4.0.0", "yoctocolors": "^2.1.1" } }, "sha512-9Be3ZoN4LmYR90tUoVu2te2BsbzHfhJyfEiAVfz7N5/zv+jduIfLrV2xdQXOHbaD6KgpGdO9PRPM1Y4Q9QkPkA=="], + + "express": ["express@5.2.1", "", { "dependencies": { "accepts": "^2.0.0", "body-parser": "^2.2.1", "content-disposition": "^1.0.0", "content-type": "^1.0.5", "cookie": "^0.7.1", "cookie-signature": "^1.2.1", "debug": "^4.4.0", "depd": "^2.0.0", "encodeurl": "^2.0.0", "escape-html": "^1.0.3", "etag": "^1.8.1", "finalhandler": "^2.1.0", "fresh": "^2.0.0", "http-errors": "^2.0.0", "merge-descriptors": "^2.0.0", "mime-types": "^3.0.0", "on-finished": "^2.4.1", "once": "^1.4.0", "parseurl": "^1.3.3", "proxy-addr": "^2.0.7", "qs": "^6.14.0", "range-parser": "^1.2.1", "router": "^2.2.0", "send": "^1.1.0", "serve-static": "^2.2.0", "statuses": "^2.0.1", "type-is": "^2.0.1", "vary": "^1.1.2" } }, "sha512-hIS4idWWai69NezIdRt2xFVofaF4j+6INOpJlVOLDO8zXGpUVEVzIYk12UUi2JzjEzWL3IOAxcTubgz9Po0yXw=="], + + "express-rate-limit": ["express-rate-limit@8.5.2", "", { "dependencies": { "ip-address": "^10.2.0" }, "peerDependencies": { "express": ">= 4.11" } }, "sha512-5Kb34ipNX694DH48vN9irak1Qx30nb0PLYHXfJgw4YEjiC3ZEmZJhwOp+VfiCYwFzvFTdB9QkArYS5kXa2cx2A=="], + + "fast-deep-equal": ["fast-deep-equal@3.1.3", "", {}, "sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q=="], + + "fast-glob": ["fast-glob@3.3.3", "", { "dependencies": { "@nodelib/fs.stat": "^2.0.2", "@nodelib/fs.walk": "^1.2.3", "glob-parent": "^5.1.2", "merge2": "^1.3.0", "micromatch": "^4.0.8" } }, "sha512-7MptL8U0cqcFdzIzwOTHoilX9x5BrNqye7Z/LuC7kCMRio1EMSyqRK3BEAUD7sXRq4iT4AzTVuZdhgQ2TCvYLg=="], + + "fast-json-stable-stringify": ["fast-json-stable-stringify@2.1.0", "", {}, "sha512-lhd/wF+Lk98HZoTCtlVraHtfh5XYijIjalXck7saUtuanSDyLMxnHhSXEDJqHxD7msR8D0uCmqlkwjCV8xvwHw=="], + + "fast-levenshtein": ["fast-levenshtein@2.0.6", "", {}, "sha512-DCXu6Ifhqcks7TZKY3Hxp3y6qphY5SJZmrWMDrKcERSOXWQdMhU9Ig/PYrzyw/ul9jOIyh0N4M0tbC5hodg8dw=="], + + "fast-uri": ["fast-uri@3.1.2", "", {}, "sha512-rVjf7ArG3LTk+FS6Yw81V1DLuZl1bRbNrev6Tmd/9RaroeeRRJhAt7jg/6YFxbvAQXUCavSoZhPPj6oOx+5KjQ=="], + + "fastq": ["fastq@1.20.1", "", { "dependencies": { "reusify": "^1.0.4" } }, "sha512-GGToxJ/w1x32s/D2EKND7kTil4n8OVk/9mycTc4VDza13lOvpUZTGX3mFSCtV9ksdGBVzvsyAVLM6mHFThxXxw=="], + + "fdir": ["fdir@6.5.0", "", { "peerDependencies": { "picomatch": "^3 || ^4" }, "optionalPeers": ["picomatch"] }, "sha512-tIbYtZbucOs0BRGqPJkshJUYdL+SDH7dVM8gjy+ERp3WAUjLEFJE+02kanyHtwjWOnwrKYBiwAmM0p4kLJAnXg=="], + + "fetch-blob": ["fetch-blob@3.2.0", "", { "dependencies": { "node-domexception": "^1.0.0", "web-streams-polyfill": "^3.0.3" } }, "sha512-7yAQpD2UMJzLi1Dqv7qFYnPbaPx7ZfFK6PiIxQ4PfkGPyNyl2Ugx+a/umUonmKqjhM4DnfbMvdX6otXq83soQQ=="], + + "figures": ["figures@6.1.0", "", { "dependencies": { "is-unicode-supported": "^2.0.0" } }, "sha512-d+l3qxjSesT4V7v2fh+QnmFnUWv9lSpjarhShNTgBOfA0ttejbQUAlHLitbjkoRiDulW0OPoQPYIGhIC8ohejg=="], + + "file-entry-cache": ["file-entry-cache@8.0.0", "", { "dependencies": { "flat-cache": "^4.0.0" } }, "sha512-XXTUwCvisa5oacNGRP9SfNtYBNAMi+RPwBFmblZEF7N7swHYQS6/Zfk7SRwx4D5j3CH211YNRco1DEMNVfZCnQ=="], + + "fill-range": ["fill-range@7.1.1", "", { "dependencies": { "to-regex-range": "^5.0.1" } }, "sha512-YsGpe3WHLK8ZYi4tWDg2Jy3ebRz2rXowDxnld4bkQB00cc/1Zw9AWnC0i9ztDJitivtQvaI9KaLyKrc+hBW0yg=="], + + "finalhandler": ["finalhandler@2.1.1", "", { "dependencies": { "debug": "^4.4.0", "encodeurl": "^2.0.0", "escape-html": "^1.0.3", "on-finished": "^2.4.1", "parseurl": "^1.3.3", "statuses": "^2.0.1" } }, "sha512-S8KoZgRZN+a5rNwqTxlZZePjT/4cnm0ROV70LedRHZ0p8u9fRID0hJUZQpkKLzro8LfmC8sx23bY6tVNxv8pQA=="], + + "find-up": ["find-up@5.0.0", "", { "dependencies": { "locate-path": "^6.0.0", "path-exists": "^4.0.0" } }, "sha512-78/PXT1wlLLDgTzDs7sjq9hzz0vXD+zn+7wypEe4fXQxCmdmqfGsEPQxmiCSQI3ajFV91bVSsvNtrJRiW6nGng=="], + + "flat-cache": ["flat-cache@4.0.1", "", { "dependencies": { "flatted": "^3.2.9", "keyv": "^4.5.4" } }, "sha512-f7ccFPK3SXFHpx15UIGyRJ/FJQctuKZ0zVuN3frBo4HnK3cay9VEW0R6yPYFHC0AgqhukPzKjq22t5DmAyqGyw=="], + + "flatted": ["flatted@3.4.2", "", {}, "sha512-PjDse7RzhcPkIJwy5t7KPWQSZ9cAbzQXcafsetQoD7sOJRQlGikNbx7yZp2OotDnJyrDcbyRq3Ttb18iYOqkxA=="], + + "formdata-polyfill": ["formdata-polyfill@4.0.10", "", { "dependencies": { "fetch-blob": "^3.1.2" } }, "sha512-buewHzMvYL29jdeQTVILecSaZKnt/RJWjoZCF5OW60Z67/GmSLBkOFM7qh1PI3zFNtJbaZL5eQu1vLfazOwj4g=="], + + "forwarded": ["forwarded@0.2.0", "", {}, "sha512-buRG0fpBtRHSTCOASe6hD258tEubFoRLb4ZNA6NxMVHNw2gOcwHo9wyablzMzOA5z9xA9L1KNjk/Nt6MT9aYow=="], + + "fresh": ["fresh@2.0.0", "", {}, "sha512-Rx/WycZ60HOaqLKAi6cHRKKI7zxWbJ31MhntmtwMoaTeF7XFH9hhBp8vITaMidfljRQ6eYWCKkaTK+ykVJHP2A=="], + + "fs-extra": ["fs-extra@11.3.5", "", { "dependencies": { "graceful-fs": "^4.2.0", "jsonfile": "^6.0.1", "universalify": "^2.0.0" } }, "sha512-eKpRKAovdpZtR1WopLHxlBWvAgPny3c4gX1G5Jhwmmw4XJj0ifSD5qB5TOo8hmA0wlRKDAOAhEE1yVPgs6Fgcg=="], + + "fsevents": ["fsevents@2.3.3", "", { "os": "darwin" }, "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw=="], + + "function-bind": ["function-bind@1.1.2", "", {}, "sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA=="], + + "fuzzysort": ["fuzzysort@3.1.0", "", {}, "sha512-sR9BNCjBg6LNgwvxlBd0sBABvQitkLzoVY9MYYROQVX/FvfJ4Mai9LsGhDgd8qYdds0bY77VzYd5iuB+v5rwQQ=="], + + "gensync": ["gensync@1.0.0-beta.2", "", {}, "sha512-3hN7NaskYvMDLQY55gnW3NQ+mesEAepTqlg+VEbj7zzqEMBVNhzcGYYeqFo/TlYz6eQiFcp1HcsCZO+nGgS8zg=="], + + "get-east-asian-width": ["get-east-asian-width@1.6.0", "", {}, "sha512-QRbvDIbx6YklUe6RxeTeleMR0yv3cYH6PsPZHcnVn7xv7zO1BHN8r0XETu8n6Ye3Q+ahtSarc3WgtNWmehIBfA=="], + + "get-intrinsic": ["get-intrinsic@1.3.0", "", { "dependencies": { "call-bind-apply-helpers": "^1.0.2", "es-define-property": "^1.0.1", "es-errors": "^1.3.0", "es-object-atoms": "^1.1.1", "function-bind": "^1.1.2", "get-proto": "^1.0.1", "gopd": "^1.2.0", "has-symbols": "^1.1.0", "hasown": "^2.0.2", "math-intrinsics": "^1.1.0" } }, "sha512-9fSjSaos/fRIVIp+xSJlE6lfwhES7LNtKaCBIamHsjr2na1BiABJPo0mOjjz8GJDURarmCPGqaiVg5mfjb98CQ=="], + + "get-nonce": ["get-nonce@1.0.1", "", {}, "sha512-FJhYRoDaiatfEkUK8HKlicmu/3SGFD51q3itKDGoSTysQJBnfOcxU5GxnhE1E6soB76MbT0MBtnKJuXyAx+96Q=="], + + "get-own-enumerable-keys": ["get-own-enumerable-keys@1.0.0", "", {}, "sha512-PKsK2FSrQCyxcGHsGrLDcK0lx+0Ke+6e8KFFozA9/fIQLhQzPaRvJFdcz7+Axg3jUH/Mq+NI4xa5u/UT2tQskA=="], + + "get-proto": ["get-proto@1.0.1", "", { "dependencies": { "dunder-proto": "^1.0.1", "es-object-atoms": "^1.0.0" } }, "sha512-sTSfBjoXBp89JvIKIefqw7U2CCebsc74kiY6awiGogKtoSGbgjYE/G/+l9sF3MWFPNc9IcoOC4ODfKHfxFmp0g=="], + + "get-stream": ["get-stream@9.0.1", "", { "dependencies": { "@sec-ant/readable-stream": "^0.4.1", "is-stream": "^4.0.1" } }, "sha512-kVCxPF3vQM/N0B1PmoqVUqgHP+EeVjmZSQn+1oCRPxd2P21P2F19lIgbR3HBosbB1PUhOAoctJnfEn2GbN2eZA=="], + + "glob-parent": ["glob-parent@6.0.2", "", { "dependencies": { "is-glob": "^4.0.3" } }, "sha512-XxwI8EOhVQgWp6iDL+3b0r86f4d6AX6zSU55HfB4ydCEuXLXc5FcYeOu+nnGftS4TEju/11rt4KJPTMgbfmv4A=="], + + "globals": ["globals@17.6.0", "", {}, "sha512-sepffkT8stwnIYbsMBpoCHJuJM5l98FUF2AnE07hfvE0m/qp3R586hw4jF4uadbhvg1ooIdzuu7CsfD2jzCaNA=="], + + "gopd": ["gopd@1.2.0", "", {}, "sha512-ZUKRh6/kUFoAiTAtTYPZJ3hw9wNxx+BIBOijnlG9PnrJsCcSjs1wyyD6vJpaYtgnzDrKYRSqf3OO6Rfa93xsRg=="], + + "graceful-fs": ["graceful-fs@4.2.11", "", {}, "sha512-RbJ5/jmFcNNCcDV5o9eTnBLJ/HszWV0P73bc+Ff4nS/rJj+YaS6IGyiOL0VoBYX+l1Wrl3k63h/KrH+nhJ0XvQ=="], + + "has-symbols": ["has-symbols@1.1.0", "", {}, "sha512-1cDNdwJ2Jaohmb3sg4OmKaMBwuC48sYni5HUw2DvsC8LjGTLK9h+eb1X6RyuOHe4hT0ULCW68iomhjUoKUqlPQ=="], + + "hasown": ["hasown@2.0.4", "", { "dependencies": { "function-bind": "^1.1.2" } }, "sha512-T2UbfbBEF32wiepXIsMlTW9+dDYC6wMh/t/vYA4tuOMKqWz/n3vr1NFSxQiyP+zk2mXsoMA/i/7qV6LKut1t1A=="], + + "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=="], + + "hono": ["hono@4.12.26", "", {}, "sha512-uyZtpnYxM9CmQ7QsQknM4zN8EftNqhON1qYeIKM0Se67CCEe2c44xyGURwB0axX2fBDu1dqHrHAc1hmNT8ITkw=="], + + "http-errors": ["http-errors@2.0.1", "", { "dependencies": { "depd": "~2.0.0", "inherits": "~2.0.4", "setprototypeof": "~1.2.0", "statuses": "~2.0.2", "toidentifier": "~1.0.1" } }, "sha512-4FbRdAX+bSdmo4AUFuS0WNiPz8NgFt+r8ThgNWmlrjQjt1Q7ZR9+zTlce2859x4KSXrwIsaeTqDoKQmtP8pLmQ=="], + + "https-proxy-agent": ["https-proxy-agent@7.0.6", "", { "dependencies": { "agent-base": "^7.1.2", "debug": "4" } }, "sha512-vK9P5/iUfdl95AI+JVyUuIcVtd4ofvtrOr3HNtM2yxC9bnMbEdp3x01OhQNnjb8IJYi38VlTE3mBXwcfvywuSw=="], + + "human-signals": ["human-signals@8.0.1", "", {}, "sha512-eKCa6bwnJhvxj14kZk5NCPc6Hb6BdsU9DZcOnmQKSnO1VKrfV0zCvtttPZUsBvjmNDn8rpcJfpwSYnHBjc95MQ=="], + + "iconv-lite": ["iconv-lite@0.7.2", "", { "dependencies": { "safer-buffer": ">= 2.1.2 < 3.0.0" } }, "sha512-im9DjEDQ55s9fL4EYzOAv0yMqmMBSZp6G0VvFyTMPKWxiSBHUj9NW/qqLmXUwXrrM7AvqSlTCfvqRb0cM8yYqw=="], + + "ignore": ["ignore@5.3.2", "", {}, "sha512-hsBTNUqQTDwkWtcdYI2i06Y/nUBEsNEDJKjWdigLvegy8kDuJAS8uRlpkkcQpyEXL0Z/pjDy5HBmMjRCJ2gq+g=="], + + "immer": ["immer@10.2.0", "", {}, "sha512-d/+XTN3zfODyjr89gM3mPq1WNX2B8pYsu7eORitdwyA2sBubnTl3laYlBk4sXY5FUa5qTZGBDPJICVbvqzjlbw=="], + + "import-fresh": ["import-fresh@3.3.1", "", { "dependencies": { "parent-module": "^1.0.0", "resolve-from": "^4.0.0" } }, "sha512-TR3KfrTZTYLPB6jUjfx6MF9WcWrHL9su5TObK4ZkYgBdWKPOFoSoQIdEuTuR82pmtxH2spWG9h6etwfr1pLBqQ=="], + + "imurmurhash": ["imurmurhash@0.1.4", "", {}, "sha512-JmXMZ6wuvDmLiHEml9ykzqO6lwFbof0GG4IkcGaENdCRDDmMVnny7s5HsIgHCbaq0w2MyPhDqkhTUgS2LU2PHA=="], + + "inherits": ["inherits@2.0.4", "", {}, "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ=="], + + "input-otp": ["input-otp@1.4.2", "", { "peerDependencies": { "react": "^16.8 || ^17.0 || ^18.0 || ^19.0.0 || ^19.0.0-rc", "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0.0 || ^19.0.0-rc" } }, "sha512-l3jWwYNvrEa6NTCt7BECfCm48GvwuZzkoeG3gBL2w4CHeOXW3eKFmf9UNYkNfYc3mxMrthMnxjIE07MT0zLBQA=="], + + "internmap": ["internmap@2.0.3", "", {}, "sha512-5Hh7Y1wQbvY5ooGgPbDaL5iYLAPzMTUrjMulskHLH6wnv/A+1q5rgEaiuqEjB+oxGXIVZs1FF+R/KPN3ZSQYYg=="], + + "ip-address": ["ip-address@10.2.0", "", {}, "sha512-/+S6j4E9AHvW9SWMSEY9Xfy66O5PWvVEJ08O0y5JGyEKQpojb0K0GKpz/v5HJ/G0vi3D2sjGK78119oXZeE0qA=="], + + "ipaddr.js": ["ipaddr.js@1.9.1", "", {}, "sha512-0KI/607xoxSToH7GjN1FfSbLoU0+btTicjsQSWQlh/hZykN8KpmMf7uYwPW3R+akZ6R/w18ZlXSHBYXiYUPO3g=="], + + "is-arrayish": ["is-arrayish@0.2.1", "", {}, "sha512-zz06S8t0ozoDXMG+ube26zeCTNXcKIPJZJi8hBrF4idCLms4CG9QtK7qBl1boi5ODzFpjswb5JPmHCbMpjaYzg=="], + + "is-docker": ["is-docker@2.2.1", "", { "bin": { "is-docker": "cli.js" } }, "sha512-F+i2BKsFrH66iaUFc0woD8sLy8getkwTwtOBjvs56Cx4CgJDeKQeqfz8wAYiSb8JOprWhHH5p77PbmYCvvUuXQ=="], + + "is-extglob": ["is-extglob@2.1.1", "", {}, "sha512-SbKbANkN603Vi4jEZv49LeVJMn4yGwsbzZworEoyEiutsN3nJYdbO36zfhGJ6QEDpOZIFkDtnq5JRxmvl3jsoQ=="], + + "is-glob": ["is-glob@4.0.3", "", { "dependencies": { "is-extglob": "^2.1.1" } }, "sha512-xelSayHH36ZgE7ZWhli7pW34hNbNl8Ojv5KVmkJD4hBdD3th8Tfk9vYasLM+mXWOZhFkgZfxhLSnrwRr4elSSg=="], + + "is-in-ssh": ["is-in-ssh@1.0.0", "", {}, "sha512-jYa6Q9rH90kR1vKB6NM7qqd1mge3Fx4Dhw5TVlK1MUBqhEOuCagrEHMevNuCcbECmXZ0ThXkRm+Ymr51HwEPAw=="], + + "is-inside-container": ["is-inside-container@1.0.0", "", { "dependencies": { "is-docker": "^3.0.0" }, "bin": { "is-inside-container": "cli.js" } }, "sha512-KIYLCCJghfHZxqjYBE7rEy0OBuTd5xCHS7tHVgvCLkx7StIoaxwNW3hCALgEUjFfeRk+MG/Qxmp/vtETEF3tRA=="], + + "is-interactive": ["is-interactive@2.0.0", "", {}, "sha512-qP1vozQRI+BMOPcjFzrjXuQvdak2pHNUMZoeG2eRbiSqyvbEf/wQtEOTOX1guk6E3t36RkaqiSt8A/6YElNxLQ=="], + + "is-number": ["is-number@7.0.0", "", {}, "sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng=="], + + "is-obj": ["is-obj@3.0.0", "", {}, "sha512-IlsXEHOjtKhpN8r/tRFj2nDyTmHvcfNeu/nrRIcXE17ROeatXchkojffa1SpdqW4cr/Fj6QkEf/Gn4zf6KKvEQ=="], + + "is-plain-obj": ["is-plain-obj@4.1.0", "", {}, "sha512-+Pgi+vMuUNkJyExiMBt5IlFoMyKnr5zhJ4Uspz58WOhBF5QoIZkFyNHIbBAtHwzVAgk5RtndVNsDRN61/mmDqg=="], + + "is-promise": ["is-promise@4.0.0", "", {}, "sha512-hvpoI6korhJMnej285dSg6nu1+e6uxs7zG3BYAm5byqDsgJNWwxzM6z6iZiAgQR4TJ30JmBTOwqZUw3WlyH3AQ=="], + + "is-regexp": ["is-regexp@3.1.0", "", {}, "sha512-rbku49cWloU5bSMI+zaRaXdQHXnthP6DZ/vLnfdSKyL4zUzuWnomtOEiZZOd+ioQ+avFo/qau3KPTc7Fjy1uPA=="], + + "is-stream": ["is-stream@4.0.1", "", {}, "sha512-Dnz92NInDqYckGEUJv689RbRiTSEHCQ7wOVeALbkOz999YpqT46yMRIGtSNl2iCL1waAZSx40+h59NV/EwzV/A=="], + + "is-unicode-supported": ["is-unicode-supported@2.1.0", "", {}, "sha512-mE00Gnza5EEB3Ds0HfMyllZzbBrmLOX3vfWoj9A9PEnTfratQ/BcaJOuMhnkhjXvb2+FkY3VuHqtAGpTPmglFQ=="], + + "is-wsl": ["is-wsl@2.2.0", "", { "dependencies": { "is-docker": "^2.0.0" } }, "sha512-fKzAra0rGJUUBwGBgNkHZuToZcn+TtXHpeCgmkMJMMYx1sQDYaCSyjJBSCa2nH1DGm7s3n1oBnohoVTBaN7Lww=="], + + "isbot": ["isbot@5.1.43", "", {}, "sha512-drJhFmibra4LO6Wd7D3Oi6UICRK9244vSZkmxzhlZP0TTdwCA2ueK4PEkUkzPYeuqug9+cqqdWPgihjk5+83Cg=="], + + "isexe": ["isexe@2.0.0", "", {}, "sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw=="], + + "jiti": ["jiti@2.7.0", "", { "bin": { "jiti": "lib/jiti-cli.mjs" } }, "sha512-AC/7JofJvZGrrneWNaEnJeOLUx+JlGt7tNa0wZiRPT4MY1wmfKjt2+6O2p2uz2+skll8OZZmJMNqeke7kKbNgQ=="], + + "jose": ["jose@6.2.3", "", {}, "sha512-YYVDInQKFJfR/xa3ojUTl8c2KoTwiL1R5Wg9YCydwH0x0B9grbzlg5HC7mMjCtUJjbQ/YnGEZIhI5tCgfTb4Hw=="], + + "js-tokens": ["js-tokens@4.0.0", "", {}, "sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ=="], + + "js-yaml": ["js-yaml@4.2.0", "", { "dependencies": { "argparse": "^2.0.1" }, "bin": { "js-yaml": "bin/js-yaml.js" } }, "sha512-ePWsvanv0DWuDRsW8dnt+R4jQ31SCRCQ7hhNcPXZPsoBZiemuZNYGf7adZdqX2D86j6rvKp3RpCxVTSb8WQlOw=="], + + "jsesc": ["jsesc@3.1.0", "", { "bin": { "jsesc": "bin/jsesc" } }, "sha512-/sM3dO2FOzXjKQhJuo0Q173wf2KOo8t4I8vHy6lF9poUp7bKT0/NHE8fPX23PwfhnykfqnC2xRxOnVw5XuGIaA=="], + + "json-buffer": ["json-buffer@3.0.1", "", {}, "sha512-4bV5BfR2mqfQTJm+V5tPPdf+ZpuhiIvTuAB5g8kcrXOZpTT/QwwVRWBywX1ozr6lEuPdbHxwaJlm9G6mI2sfSQ=="], + + "json-parse-even-better-errors": ["json-parse-even-better-errors@2.3.1", "", {}, "sha512-xyFwyhro/JEof6Ghe2iz2NcXoj2sloNsWr/XsERDK/oiPCfaNhl5ONfp+jQdAZRQQ0IJWNzH9zIZF7li91kh2w=="], + + "json-schema-traverse": ["json-schema-traverse@0.4.1", "", {}, "sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg=="], + + "json-schema-typed": ["json-schema-typed@8.0.2", "", {}, "sha512-fQhoXdcvc3V28x7C7BMs4P5+kNlgUURe2jmUT1T//oBRMDrqy1QPelJimwZGo7Hg9VPV3EQV5Bnq4hbFy2vetA=="], + + "json-stable-stringify-without-jsonify": ["json-stable-stringify-without-jsonify@1.0.1", "", {}, "sha512-Bdboy+l7tA3OGW6FjyFHWkP5LuByj1Tk33Ljyq0axyzdk9//JSi2u3fP1QSmd1KNwq6VOKYGlAu87CisVir6Pw=="], + + "json5": ["json5@2.2.3", "", { "bin": { "json5": "lib/cli.js" } }, "sha512-XmOWe7eyHYH14cLdVPoyg+GOH3rYX++KpzrylJwSW98t3Nk+U8XOl8FWKOgwtzdb8lXGf6zYwDUzeHMWfxasyg=="], + + "jsonfile": ["jsonfile@6.2.1", "", { "dependencies": { "universalify": "^2.0.0" }, "optionalDependencies": { "graceful-fs": "^4.1.6" } }, "sha512-zwOTdL3rFQ/lRdBnntKVOX6k5cKJwEc1HdilT71BWEu7J41gXIB2MRp+vxduPSwZJPWBxEzv4yH1wYLJGUHX4Q=="], + + "keyv": ["keyv@4.5.4", "", { "dependencies": { "json-buffer": "3.0.1" } }, "sha512-oxVHkHR/EJf2CNXnWxRLW6mg7JyCCUcG0DtEGmL2ctUo1PNTin1PUil+r/+4r5MpVgC/fn1kjsx7mjSujKqIpw=="], + + "kleur": ["kleur@4.1.5", "", {}, "sha512-o+NO+8WrRiQEE4/7nwRJhN1HWpVmJm511pBHUxPLtp0BUISzlBplORYSmTclCnJvQq2tKu/sgl3xVpkc7ZWuQQ=="], + + "levn": ["levn@0.4.1", "", { "dependencies": { "prelude-ls": "^1.2.1", "type-check": "~0.4.0" } }, "sha512-+bT2uH4E5LGE7h/n3evcS/sQlJXCpIp6ym8OWJ5eV6+67Dsql/LaaT7qJBAt2rzfoa/5QBGBhxDix1dMt2kQKQ=="], + + "lightningcss": ["lightningcss@1.32.0", "", { "dependencies": { "detect-libc": "^2.0.3" }, "optionalDependencies": { "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" } }, "sha512-NXYBzinNrblfraPGyrbPoD19C1h9lfI/1mzgWYvXUTe414Gz/X1FD2XBZSZM7rRTrMA8JL3OtAaGifrIKhQ5yQ=="], + + "lightningcss-android-arm64": ["lightningcss-android-arm64@1.32.0", "", { "os": "android", "cpu": "arm64" }, "sha512-YK7/ClTt4kAK0vo6w3X+Pnm0D2cf2vPHbhOXdoNti1Ga0al1P4TBZhwjATvjNwLEBCnKvjJc2jQgHXH0NEwlAg=="], + + "lightningcss-darwin-arm64": ["lightningcss-darwin-arm64@1.32.0", "", { "os": "darwin", "cpu": "arm64" }, "sha512-RzeG9Ju5bag2Bv1/lwlVJvBE3q6TtXskdZLLCyfg5pt+HLz9BqlICO7LZM7VHNTTn/5PRhHFBSjk5lc4cmscPQ=="], + + "lightningcss-darwin-x64": ["lightningcss-darwin-x64@1.32.0", "", { "os": "darwin", "cpu": "x64" }, "sha512-U+QsBp2m/s2wqpUYT/6wnlagdZbtZdndSmut/NJqlCcMLTWp5muCrID+K5UJ6jqD2BFshejCYXniPDbNh73V8w=="], + + "lightningcss-freebsd-x64": ["lightningcss-freebsd-x64@1.32.0", "", { "os": "freebsd", "cpu": "x64" }, "sha512-JCTigedEksZk3tHTTthnMdVfGf61Fky8Ji2E4YjUTEQX14xiy/lTzXnu1vwiZe3bYe0q+SpsSH/CTeDXK6WHig=="], + + "lightningcss-linux-arm-gnueabihf": ["lightningcss-linux-arm-gnueabihf@1.32.0", "", { "os": "linux", "cpu": "arm" }, "sha512-x6rnnpRa2GL0zQOkt6rts3YDPzduLpWvwAF6EMhXFVZXD4tPrBkEFqzGowzCsIWsPjqSK+tyNEODUBXeeVHSkw=="], + + "lightningcss-linux-arm64-gnu": ["lightningcss-linux-arm64-gnu@1.32.0", "", { "os": "linux", "cpu": "arm64" }, "sha512-0nnMyoyOLRJXfbMOilaSRcLH3Jw5z9HDNGfT/gwCPgaDjnx0i8w7vBzFLFR1f6CMLKF8gVbebmkUN3fa/kQJpQ=="], + + "lightningcss-linux-arm64-musl": ["lightningcss-linux-arm64-musl@1.32.0", "", { "os": "linux", "cpu": "arm64" }, "sha512-UpQkoenr4UJEzgVIYpI80lDFvRmPVg6oqboNHfoH4CQIfNA+HOrZ7Mo7KZP02dC6LjghPQJeBsvXhJod/wnIBg=="], + + "lightningcss-linux-x64-gnu": ["lightningcss-linux-x64-gnu@1.32.0", "", { "os": "linux", "cpu": "x64" }, "sha512-V7Qr52IhZmdKPVr+Vtw8o+WLsQJYCTd8loIfpDaMRWGUZfBOYEJeyJIkqGIDMZPwPx24pUMfwSxxI8phr/MbOA=="], + + "lightningcss-linux-x64-musl": ["lightningcss-linux-x64-musl@1.32.0", "", { "os": "linux", "cpu": "x64" }, "sha512-bYcLp+Vb0awsiXg/80uCRezCYHNg1/l3mt0gzHnWV9XP1W5sKa5/TCdGWaR/zBM2PeF/HbsQv/j2URNOiVuxWg=="], + + "lightningcss-win32-arm64-msvc": ["lightningcss-win32-arm64-msvc@1.32.0", "", { "os": "win32", "cpu": "arm64" }, "sha512-8SbC8BR40pS6baCM8sbtYDSwEVQd4JlFTOlaD3gWGHfThTcABnNDBda6eTZeqbofalIJhFx0qKzgHJmcPTnGdw=="], + + "lightningcss-win32-x64-msvc": ["lightningcss-win32-x64-msvc@1.32.0", "", { "os": "win32", "cpu": "x64" }, "sha512-Amq9B/SoZYdDi1kFrojnoqPLxYhQ4Wo5XiL8EVJrVsB8ARoC1PWW6VGtT0WKCemjy8aC+louJnjS7U18x3b06Q=="], + + "lines-and-columns": ["lines-and-columns@1.2.4", "", {}, "sha512-7ylylesZQ/PV29jhEDl3Ufjo6ZX7gCqJr5F7PKrqc93v7fzSymt1BpwEU8nAUXs8qzzvqhbjhK5QZg6Mt/HkBg=="], + + "locate-path": ["locate-path@6.0.0", "", { "dependencies": { "p-locate": "^5.0.0" } }, "sha512-iPZK6eYjbxRu3uB4/WZ3EsEIMJFMqAoopl3R+zuq0UjcAm/MO6KCweDgPfP3elTztoKP3KtnVHxTn2NHBSDVUw=="], + + "log-symbols": ["log-symbols@6.0.0", "", { "dependencies": { "chalk": "^5.3.0", "is-unicode-supported": "^1.3.0" } }, "sha512-i24m8rpwhmPIS4zscNzK6MSEhk0DUWa/8iYQWxhffV8jkI4Phvs3F+quL5xvS0gdQR0FyTCMMH33Y78dDTzzIw=="], + + "lru-cache": ["lru-cache@5.1.1", "", { "dependencies": { "yallist": "^3.0.2" } }, "sha512-KpNARQA3Iwv+jTA0utUVVbrh+Jlrr1Fv0e56GGzAFOXN7dk/FviaDW8LHmK52DlcH4WP2n6gI8vN1aesBFgo9w=="], + + "lucide-react": ["lucide-react@1.21.0", "", { "peerDependencies": { "react": "^16.5.1 || ^17.0.0 || ^18.0.0 || ^19.0.0" } }, "sha512-reEZMXq8Qdd5jg5XYkQ5TR1fB/GiQ7ih4vcrthYDtgjSDwh0i6/YLiGjsWsIwgN49gpAnd4J2elSNzncMEEUUQ=="], + + "magic-string": ["magic-string@0.30.21", "", { "dependencies": { "@jridgewell/sourcemap-codec": "^1.5.5" } }, "sha512-vd2F4YUyEXKGcLHoq+TEyCjxueSeHnFxyyjNp80yg0XV4vUhnDer/lvvlqM/arB5bXQN5K2/3oinyCRyx8T2CQ=="], + + "math-intrinsics": ["math-intrinsics@1.1.0", "", {}, "sha512-/IXtbwEk5HTPyEwyKX6hGkYXxM9nbj64B+ilVJnC/R6B0pH5G4V3b0pVbL7DBj4tkhBAppbQUlf6F6Xl9LHu1g=="], + + "media-typer": ["media-typer@1.1.0", "", {}, "sha512-aisnrDP4GNe06UcKFnV5bfMNPBUw4jsLGaWwWfnH3v02GnBuXX2MCVn5RbrWo0j3pczUilYblq7fQ7Nw2t5XKw=="], + + "merge-descriptors": ["merge-descriptors@2.0.0", "", {}, "sha512-Snk314V5ayFLhp3fkUREub6WtjBfPdCPY1Ln8/8munuLuiYhsABgBVWsozAG+MWMbVEvcdcpbi9R7ww22l9Q3g=="], + + "merge-stream": ["merge-stream@2.0.0", "", {}, "sha512-abv/qOcuPfk3URPfDzmZU1LKmuw8kT+0nIHvKrKgFrwifol/doWcdA4ZqsWQ8ENrFKkd67Mfpo/LovbIUsbt3w=="], + + "merge2": ["merge2@1.4.1", "", {}, "sha512-8q7VEgMJW4J8tcfVPy8g09NcQwZdbwFEqhe/WZkoIzjn/3TGDwtOCYtXGxA3O8tPzpczCCDgv+P2P5y00ZJOOg=="], + + "micromatch": ["micromatch@4.0.8", "", { "dependencies": { "braces": "^3.0.3", "picomatch": "^2.3.1" } }, "sha512-PXwfBhYu0hBCPw8Dn0E+WDYb7af3dSLVWKi3HGv84IdF4TyFoC0ysxFd0Goxw7nSv4T/PzEJQxsYsEiFCKo2BA=="], + + "mime-db": ["mime-db@1.54.0", "", {}, "sha512-aU5EJuIN2WDemCcAp2vFBfp/m4EAhWJnUNSSw0ixs7/kXbd6Pg64EmwJkNdFhB8aWt1sH2CTXrLxo/iAGV3oPQ=="], + + "mime-types": ["mime-types@3.0.2", "", { "dependencies": { "mime-db": "^1.54.0" } }, "sha512-Lbgzdk0h4juoQ9fCKXW4by0UJqj+nOOrI9MJ1sSj4nI8aI2eo1qmvQEie4VD1glsS250n15LsWsYtCugiStS5A=="], + + "mimic-fn": ["mimic-fn@3.1.0", "", {}, "sha512-Ysbi9uYW9hFyfrThdDEQuykN4Ey6BuwPD2kpI5ES/nFTDn/98yxYNLZJcgUAKPT/mcrLLKaGzJR9YVxJrIdASQ=="], + + "mimic-function": ["mimic-function@5.0.1", "", {}, "sha512-VP79XUPxV2CigYP3jWwAUFSku2aKqBH7uTAapFWCBqutsbmDo96KY5o8uh6U+/YSIn5OxJnXp73beVkpqMIGhA=="], + + "minimatch": ["minimatch@10.2.5", "", { "dependencies": { "brace-expansion": "^5.0.5" } }, "sha512-MULkVLfKGYDFYejP07QOurDLLQpcjk7Fw+7jXS2R2czRQzR56yHRveU5NDJEOviH+hETZKSkIk5c+T23GjFUMg=="], + + "minimist": ["minimist@1.2.8", "", {}, "sha512-2yyAR8qBkN3YuheJanUpWC5U3bb5osDywNB8RzDVlDwDHbocAJveqqj1u8+SVD7jkWT4yvsHCpWqqWqAxb0zCA=="], + + "ms": ["ms@2.1.3", "", {}, "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA=="], + + "nanoid": ["nanoid@3.3.13", "", { "bin": { "nanoid": "bin/nanoid.cjs" } }, "sha512-sPdqC6ByMVVGvF1ynvvMo0/o+oD1VX7DaHhijt1bFgjvBkHBib4t49GoNDhf2NDta4oeUNlaGbSt5K7qjZ955Q=="], + + "natural-compare": ["natural-compare@1.4.0", "", {}, "sha512-OWND8ei3VtNC9h7V60qff3SVobHr996CTwgxubgyQYEpg290h9J0buyECNNJexkFm5sOajh5G116RYA1c8ZMSw=="], + + "negotiator": ["negotiator@1.0.0", "", {}, "sha512-8Ofs/AUQh8MaEcrlq5xOX0CQ9ypTF5dl78mjlMNfOK08fzpgTHQRQPBxcPlEtIw0yRpws+Zo/3r+5WRby7u3Gg=="], + + "next-themes": ["next-themes@0.4.6", "", { "peerDependencies": { "react": "^16.8 || ^17 || ^18 || ^19 || ^19.0.0-rc", "react-dom": "^16.8 || ^17 || ^18 || ^19 || ^19.0.0-rc" } }, "sha512-pZvgD5L0IEvX5/9GWyHMf3m8BKiVQwsCMHfoFosXtXBMnaS0ZnIJ9ST4b4NqLVKDEm8QBxoNNGNaBv2JNF6XNA=="], + + "node-domexception": ["node-domexception@1.0.0", "", {}, "sha512-/jKZoMpw0F8GRwl4/eLROPA3cfcXtLApP0QzLmUT/HuPCZWyB7IY9ZrMeKw2O/nFIqPQB3PVM9aYm0F312AXDQ=="], + + "node-fetch": ["node-fetch@3.3.2", "", { "dependencies": { "data-uri-to-buffer": "^4.0.0", "fetch-blob": "^3.1.4", "formdata-polyfill": "^4.0.10" } }, "sha512-dRB78srN/l6gqWulah9SrxeYnxeddIG30+GOqK/9OlLVyLg3HPnr6SqOWTWOXKRwC2eGYCkZ59NNuSgvSrpgOA=="], + + "node-releases": ["node-releases@2.0.48", "", {}, "sha512-1uz8041X6LoI6ZSdZacM9lVY28vuzDlSKitnpbSNK0RfKoIJkX29NBPVEFXhnuSuEOA9Ww0xnPJ+ILWbGAv8DA=="], + + "npm-run-path": ["npm-run-path@6.0.0", "", { "dependencies": { "path-key": "^4.0.0", "unicorn-magic": "^0.3.0" } }, "sha512-9qny7Z9DsQU8Ou39ERsPU4OZQlSTP47ShQzuKZ6PRXpYLtIFgl/DEBYEXKlvcEa+9tHVcK8CF81Y2V72qaZhWA=="], + + "object-assign": ["object-assign@4.1.1", "", {}, "sha512-rJgTQnkUnH1sFw8yT6VSU3zD3sWmu6sZhIseY8VX+GRu3P6F7Fu+JNDoXfklElbLJSnc3FUQHVe4cU5hj+BcUg=="], + + "object-inspect": ["object-inspect@1.13.4", "", {}, "sha512-W67iLl4J2EXEGTbfeHCffrjDfitvLANg0UlX3wFUUSTx92KXRFegMHUVgSqE+wvhAbi4WqjGg9czysTV2Epbew=="], + + "object-treeify": ["object-treeify@1.1.33", "", {}, "sha512-EFVjAYfzWqWsBMRHPMAXLCDIJnpMhdWAqR7xG6M6a2cs6PMFpl/+Z20w9zDW4vkxOFfddegBKq9Rehd0bxWE7A=="], + + "on-finished": ["on-finished@2.4.1", "", { "dependencies": { "ee-first": "1.1.1" } }, "sha512-oVlzkg3ENAhCk2zdv7IJwd/QUD4z2RxRwpkcGY8psCVcCYZNq4wYnVWALHM+brtuJjePWiYF/ClmuDr8Ch5+kg=="], + + "once": ["once@1.4.0", "", { "dependencies": { "wrappy": "1" } }, "sha512-lNaJgI+2Q5URQBkccEKHTQOPaXdUxnZZElQTZY0MFUAuaEqe1E+Nyvgdz/aIyNi6Z9MzO5dv1H8n58/GELp3+w=="], + + "onetime": ["onetime@5.1.2", "", { "dependencies": { "mimic-fn": "^2.1.0" } }, "sha512-kbpaSSGJTWdAY5KPVeMOKXSrPtr8C8C7wodJbcsd51jRnmD+GZu8Y0VoU6Dm5Z4vWr0Ig/1NKuWRKf7j5aaYSg=="], + + "open": ["open@11.0.0", "", { "dependencies": { "default-browser": "^5.4.0", "define-lazy-prop": "^3.0.0", "is-in-ssh": "^1.0.0", "is-inside-container": "^1.0.0", "powershell-utils": "^0.1.0", "wsl-utils": "^0.3.0" } }, "sha512-smsWv2LzFjP03xmvFoJ331ss6h+jixfA4UUV/Bsiyuu4YJPfN+FIQGOIiv4w9/+MoHkfkJ22UIaQWRVFRfH6Vw=="], + + "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=="], + + "ora": ["ora@8.2.0", "", { "dependencies": { "chalk": "^5.3.0", "cli-cursor": "^5.0.0", "cli-spinners": "^2.9.2", "is-interactive": "^2.0.0", "is-unicode-supported": "^2.0.0", "log-symbols": "^6.0.0", "stdin-discarder": "^0.2.2", "string-width": "^7.2.0", "strip-ansi": "^7.1.0" } }, "sha512-weP+BZ8MVNnlCm8c0Qdc1WSWq4Qn7I+9CJGm7Qali6g44e/PUzbjNqJX5NJ9ljlNMosfJvg1fKEGILklK9cwnw=="], + + "p-limit": ["p-limit@3.1.0", "", { "dependencies": { "yocto-queue": "^0.1.0" } }, "sha512-TYOanM3wGwNGsZN2cVTYPArw454xnXj5qmWF1bEoAc4+cU/ol7GVh7odevjp1FNHduHc3KZMcFduxU5Xc6uJRQ=="], + + "p-locate": ["p-locate@5.0.0", "", { "dependencies": { "p-limit": "^3.0.2" } }, "sha512-LaNjtRWUBY++zB5nE/NwcaoMylSPk+S+ZHNB1TzdbMJMny6dynpAGt7X/tl/QYq3TIeE6nxHppbo2LGymrG5Pw=="], + + "p-try": ["p-try@2.2.0", "", {}, "sha512-R4nPAVTAU0B9D35/Gk3uJf/7XYbQcyohSKdvAxIRSNghFl4e71hVoGnBNQz9cWaXxO2I10KTC+3jMdvvoKw6dQ=="], + + "parent-module": ["parent-module@1.0.1", "", { "dependencies": { "callsites": "^3.0.0" } }, "sha512-GQ2EWRpQV8/o+Aw8YqtfZZPfNRWZYkbidE9k5rpl/hC3vtHHBfGm2Ifi6qWV+coDGkrUKZAxE3Lot5kcsRlh+g=="], + + "parse-json": ["parse-json@5.2.0", "", { "dependencies": { "@babel/code-frame": "^7.0.0", "error-ex": "^1.3.1", "json-parse-even-better-errors": "^2.3.0", "lines-and-columns": "^1.1.6" } }, "sha512-ayCKvm/phCGxOkYRSCM82iDwct8/EonSEgCSxWxD7ve6jHggsFl4fZVQBPRNgQoKiuV/odhFrGzQXZwbifC8Rg=="], + + "parse-ms": ["parse-ms@4.0.0", "", {}, "sha512-TXfryirbmq34y8QBwgqCVLi+8oA3oWx2eAnSn62ITyEhEYaWRlVZ2DvMM9eZbMs/RfxPu/PK/aBLyGj4IrqMHw=="], + + "parseurl": ["parseurl@1.3.3", "", {}, "sha512-CiyeOxFT/JZyN5m0z9PfXw4SCBJ6Sygz1Dpl0wqjlhDEGGBP1GnsUVEL0p63hoG1fcj3fHynXi9NYO4nWOL+qQ=="], + + "path-browserify": ["path-browserify@1.0.1", "", {}, "sha512-b7uo2UCUOYZcnF/3ID0lulOJi/bafxa1xPe7ZPsammBSpjSWQkjNxlt635YGS2MiR9GjvuXCtz2emr3jbsz98g=="], + + "path-exists": ["path-exists@4.0.0", "", {}, "sha512-ak9Qy5Q7jYb2Wwcey5Fpvg2KoAc/ZIhLSLOSBmRmygPsGwkVVt0fZa0qrtMz+m6tJTAHfZQ8FnmB4MG4LWy7/w=="], + + "path-key": ["path-key@3.1.1", "", {}, "sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q=="], + + "path-to-regexp": ["path-to-regexp@8.4.2", "", {}, "sha512-qRcuIdP69NPm4qbACK+aDogI5CBDMi1jKe0ry5rSQJz8JVLsC7jV8XpiJjGRLLol3N+R5ihGYcrPLTno6pAdBA=="], + + "picocolors": ["picocolors@1.1.1", "", {}, "sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA=="], + + "picomatch": ["picomatch@4.0.4", "", {}, "sha512-QP88BAKvMam/3NxH6vj2o21R6MjxZUAd6nlwAS/pnGvN9IVLocLHxGYIzFhg6fUQ+5th6P4dv4eW9jX3DSIj7A=="], + + "pkce-challenge": ["pkce-challenge@5.0.1", "", {}, "sha512-wQ0b/W4Fr01qtpHlqSqspcj3EhBvimsdh0KlHhH8HRZnMsEa0ea2fTULOXOS9ccQr3om+GcGRk4e+isrZWV8qQ=="], + + "pkg-up": ["pkg-up@3.1.0", "", { "dependencies": { "find-up": "^3.0.0" } }, "sha512-nDywThFk1i4BQK4twPQ6TA4RT8bDY96yeuCVBWL3ePARCiEKDRSrNGbFIgUJpLp+XeIR65v8ra7WuJOFUBtkMA=="], + + "postcss": ["postcss@8.5.15", "", { "dependencies": { "nanoid": "^3.3.12", "picocolors": "^1.1.1", "source-map-js": "^1.2.1" } }, "sha512-FfR8sjd4em2T6fb3I2MwAJU7HWVMr9zba+enmQeeWFfCbm+UOC/0X4DS8XtpUTMwWMGbjKYP7xjfNekzyGmB3A=="], + + "postcss-selector-parser": ["postcss-selector-parser@7.1.4", "", { "dependencies": { "cssesc": "^3.0.0", "util-deprecate": "^1.0.2" } }, "sha512-HeP7D2wyhkR+XaK6v4W8oRF62Dsz4flyuczALJp61GckGm42u1saSSJ/0auvcBqxs3jMRFEcPK34At/0JBKdOg=="], + + "powershell-utils": ["powershell-utils@0.1.0", "", {}, "sha512-dM0jVuXJPsDN6DvRpea484tCUaMiXWjuCn++HGTqUWzGDjv5tZkEZldAJ/UMlqRYGFrD/etByo4/xOuC/snX2A=="], + + "prelude-ls": ["prelude-ls@1.2.1", "", {}, "sha512-vkcDPrRZo1QZLbn5RLGPpg/WmIQ65qoWWhcGKf/b5eplkkarX0m9z8ppCat4mlOqUsWpyNuYgO3VRyrYHSzX5g=="], + + "prettier": ["prettier@3.8.4", "", { "bin": { "prettier": "bin/prettier.cjs" } }, "sha512-N2MylSdi48+5N/6S5j+maeHbUSIzzZ5uOcX5Hm4QpV8Dkb1HFjfAKTKX6yNPJQD9AhcT3ifHNB66tWTTJDi11Q=="], + + "prettier-plugin-tailwindcss": ["prettier-plugin-tailwindcss@0.8.0", "", { "peerDependencies": { "@ianvs/prettier-plugin-sort-imports": "*", "@prettier/plugin-hermes": "*", "@prettier/plugin-oxc": "*", "@prettier/plugin-pug": "*", "@shopify/prettier-plugin-liquid": "*", "@trivago/prettier-plugin-sort-imports": "*", "@zackad/prettier-plugin-twig": "*", "prettier": "^3.0", "prettier-plugin-astro": "*", "prettier-plugin-css-order": "*", "prettier-plugin-jsdoc": "*", "prettier-plugin-marko": "*", "prettier-plugin-multiline-arrays": "*", "prettier-plugin-organize-attributes": "*", "prettier-plugin-organize-imports": "*", "prettier-plugin-sort-imports": "*", "prettier-plugin-svelte": "*" }, "optionalPeers": ["@ianvs/prettier-plugin-sort-imports", "@prettier/plugin-hermes", "@prettier/plugin-oxc", "@prettier/plugin-pug", "@shopify/prettier-plugin-liquid", "@trivago/prettier-plugin-sort-imports", "@zackad/prettier-plugin-twig", "prettier-plugin-astro", "prettier-plugin-css-order", "prettier-plugin-jsdoc", "prettier-plugin-marko", "prettier-plugin-multiline-arrays", "prettier-plugin-organize-attributes", "prettier-plugin-organize-imports", "prettier-plugin-sort-imports", "prettier-plugin-svelte"] }, "sha512-V8ITGH87yuBDF6JpEZTOVlUz/saAwqb8f3HRgUj8Lh+tGCcrmorhsLpYqzygwFwK0PE2Ib6Mv3M7T/uE2tZV1g=="], + + "pretty-ms": ["pretty-ms@9.3.0", "", { "dependencies": { "parse-ms": "^4.0.0" } }, "sha512-gjVS5hOP+M3wMm5nmNOucbIrqudzs9v/57bWRHQWLYklXqoXKrVfYW2W9+glfGsqtPgpiz5WwyEEB+ksXIx3gQ=="], + + "prompts": ["prompts@2.4.2", "", { "dependencies": { "kleur": "^3.0.3", "sisteransi": "^1.0.5" } }, "sha512-NxNv/kLguCA7p3jE8oL2aEBsrJWgAakBpgmgK6lpPWV+WuOmY6r2/zbAVnP+T8bQlA0nzHXSJSJW0Hq7ylaD2Q=="], + + "proxy-addr": ["proxy-addr@2.0.7", "", { "dependencies": { "forwarded": "0.2.0", "ipaddr.js": "1.9.1" } }, "sha512-llQsMLSUDUPT44jdrU/O37qlnifitDP+ZwrmmZcoSKyLKvtZxpyV0n2/bD/N4tBAAZ/gJEdZU7KMraoK1+XYAg=="], + + "punycode": ["punycode@2.3.1", "", {}, "sha512-vYt7UD1U9Wg6138shLtLOvdAu+8DsC/ilFtEVHcH+wydcSpNE20AfSOduf6MkRFahL5FY7X1oU7nKVZFtfq8Fg=="], + + "qs": ["qs@6.15.2", "", { "dependencies": { "side-channel": "^1.1.0" } }, "sha512-Rzq0KEyX/w/tEybncDgdkZrJgVUsUMk3xjh3t5bv3S1HTAtg+uOYt72+ZfwiQwKdysThkTBdL/rTi6HDmX9Ddw=="], + + "queue-microtask": ["queue-microtask@1.2.3", "", {}, "sha512-NuaNSa6flKT5JaSYQzJok04JzTL1CA6aGhv5rfLW3PgqA+M2ChpZQnAC8h8i4ZFkBS8X5RqkDBHA7r4hej3K9A=="], + + "radix-ui": ["radix-ui@1.6.0", "", { "dependencies": { "@radix-ui/primitive": "1.1.4", "@radix-ui/react-accessible-icon": "1.1.10", "@radix-ui/react-accordion": "1.2.14", "@radix-ui/react-alert-dialog": "1.1.17", "@radix-ui/react-arrow": "1.1.10", "@radix-ui/react-aspect-ratio": "1.1.10", "@radix-ui/react-avatar": "1.2.0", "@radix-ui/react-checkbox": "1.3.5", "@radix-ui/react-collapsible": "1.1.14", "@radix-ui/react-collection": "1.1.10", "@radix-ui/react-compose-refs": "1.1.3", "@radix-ui/react-context": "1.1.4", "@radix-ui/react-context-menu": "2.3.1", "@radix-ui/react-dialog": "1.1.17", "@radix-ui/react-direction": "1.1.2", "@radix-ui/react-dismissable-layer": "1.1.13", "@radix-ui/react-dropdown-menu": "2.1.18", "@radix-ui/react-focus-guards": "1.1.4", "@radix-ui/react-focus-scope": "1.1.10", "@radix-ui/react-form": "0.1.10", "@radix-ui/react-hover-card": "1.1.17", "@radix-ui/react-label": "2.1.10", "@radix-ui/react-menu": "2.1.18", "@radix-ui/react-menubar": "1.1.18", "@radix-ui/react-navigation-menu": "1.2.16", "@radix-ui/react-one-time-password-field": "0.1.10", "@radix-ui/react-password-toggle-field": "0.1.5", "@radix-ui/react-popover": "1.1.17", "@radix-ui/react-popper": "1.3.1", "@radix-ui/react-portal": "1.1.12", "@radix-ui/react-presence": "1.1.6", "@radix-ui/react-primitive": "2.1.6", "@radix-ui/react-progress": "1.1.10", "@radix-ui/react-radio-group": "1.4.1", "@radix-ui/react-roving-focus": "1.1.13", "@radix-ui/react-scroll-area": "1.2.12", "@radix-ui/react-select": "2.3.1", "@radix-ui/react-separator": "1.1.10", "@radix-ui/react-slider": "1.4.1", "@radix-ui/react-slot": "1.3.0", "@radix-ui/react-switch": "1.3.1", "@radix-ui/react-tabs": "1.1.15", "@radix-ui/react-toast": "1.2.17", "@radix-ui/react-toggle": "1.1.12", "@radix-ui/react-toggle-group": "1.1.13", "@radix-ui/react-toolbar": "1.1.13", "@radix-ui/react-tooltip": "1.2.10", "@radix-ui/react-use-callback-ref": "1.1.2", "@radix-ui/react-use-controllable-state": "1.2.3", "@radix-ui/react-use-effect-event": "0.0.3", "@radix-ui/react-use-escape-keydown": "1.1.2", "@radix-ui/react-use-is-hydrated": "0.1.1", "@radix-ui/react-use-layout-effect": "1.1.2", "@radix-ui/react-use-size": "1.1.2", "@radix-ui/react-visually-hidden": "1.2.6" }, "peerDependencies": { "@types/react": "*", "@types/react-dom": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react", "@types/react-dom"] }, "sha512-EUEC70O03EgxWMP5aoqfBZ6iLC5bczFagGy7zhSYRt8o5DP7IWNiP3ywetse3L9b8843ExB0OGWZvgbYVJuNeg=="], + + "range-parser": ["range-parser@1.2.1", "", {}, "sha512-Hrgsx+orqoygnmhFbKaHE6c296J+HTAQXoxEF6gNupROmmGJRoyzfG3ccAveqCBrwr/2yxQ5BVd/GTl5agOwSg=="], + + "raw-body": ["raw-body@3.0.2", "", { "dependencies": { "bytes": "~3.1.2", "http-errors": "~2.0.1", "iconv-lite": "~0.7.0", "unpipe": "~1.0.0" } }, "sha512-K5zQjDllxWkf7Z5xJdV0/B0WTNqx6vxG70zJE4N0kBs4LovmEYWJzQGxC9bS9RAKu3bgM40lrd5zoLJ12MQ5BA=="], + + "react": ["react@19.2.7", "", {}, "sha512-HNe9WslTbXmFK8o8cmwgAeJFSBvt1bPdHCVKtaaV+WlAN36mpT4hcRpwbf3fY56ar2oIXzsBpOAiIRHAdY0OlQ=="], + + "react-day-picker": ["react-day-picker@10.0.1", "", { "dependencies": { "@date-fns/tz": "^1.4.1", "date-fns": "^4.1.0" }, "peerDependencies": { "@types/react": ">=16.8.0", "react": ">=16.8.0" }, "optionalPeers": ["@types/react"] }, "sha512-eNh6BlwcYInWaJtRv18mXQ06Ys/H6rdTZAnTaSdOYJuTpwP1JMCHNd1FDRadA+gbeinq+psdULN5Xnowy9mV8w=="], + + "react-dom": ["react-dom@19.2.7", "", { "dependencies": { "scheduler": "^0.27.0" }, "peerDependencies": { "react": "^19.2.7" } }, "sha512-t0BRVXvbiE/o20Hfw669rLbMCDWtYZLvmJigy2f0MxsXF+71pxhR3xOkspmsO8h3ZlNzyibAmtCa3l4lYKk6gQ=="], + + "react-is": ["react-is@19.2.7", "", {}, "sha512-kZFnouyVv7eP/Phmrlo9FK+zcAdriZJvzxXHF1Sl1P377WSGe2G/JxVolhTrB/jeV47lKImhNUsijjHAAbcl/A=="], + + "react-redux": ["react-redux@9.3.0", "", { "dependencies": { "@types/use-sync-external-store": "^0.0.6", "use-sync-external-store": "^1.4.0" }, "peerDependencies": { "@types/react": "^18.2.25 || ^19", "react": "^18.0 || ^19", "redux": "^5.0.0" }, "optionalPeers": ["@types/react", "redux"] }, "sha512-KQopgqFo/p/fgmAs5qz6p5RWaNAzq40WAu7fJIXnQpYxFPbJYtsJPWvGeF2rOBaY/kEuV77AVsX8TsQzKm+A/g=="], + + "react-remove-scroll": ["react-remove-scroll@2.7.2", "", { "dependencies": { "react-remove-scroll-bar": "^2.3.7", "react-style-singleton": "^2.2.3", "tslib": "^2.1.0", "use-callback-ref": "^1.3.3", "use-sidecar": "^1.1.3" }, "peerDependencies": { "@types/react": "*", "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react"] }, "sha512-Iqb9NjCCTt6Hf+vOdNIZGdTiH1QSqr27H/Ek9sv/a97gfueI/5h1s3yRi1nngzMUaOOToin5dI1dXKdXiF+u0Q=="], + + "react-remove-scroll-bar": ["react-remove-scroll-bar@2.3.8", "", { "dependencies": { "react-style-singleton": "^2.2.2", "tslib": "^2.0.0" }, "peerDependencies": { "@types/react": "*", "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0" }, "optionalPeers": ["@types/react"] }, "sha512-9r+yi9+mgU33AKcj6IbT9oRCO78WriSj6t/cF8DWBZJ9aOGPOTEDvdUDz1FwKim7QXWwmHqtdHnRJfhAxEG46Q=="], + + "react-resizable-panels": ["react-resizable-panels@4.11.2", "", { "peerDependencies": { "react": "^18.0.0 || ^19.0.0", "react-dom": "^18.0.0 || ^19.0.0" } }, "sha512-+kfFbDZ8mygc7g0vxOcDzCVGuwiIUOnILqPoUHo6/uP+Mmyx6HzZU+kj1aOPDlktXuobYbr6BtQekvJwHRX4Eg=="], + + "react-style-singleton": ["react-style-singleton@2.2.3", "", { "dependencies": { "get-nonce": "^1.0.0", "tslib": "^2.0.0" }, "peerDependencies": { "@types/react": "*", "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react"] }, "sha512-b6jSvxvVnyptAiLjbkWLE/lOnR4lfTtDAl+eUC7RZy+QQWc6wRzIV2CE6xBuMmDxc2qIihtDCZD5NPOFl7fRBQ=="], + + "recast": ["recast@0.23.11", "", { "dependencies": { "ast-types": "^0.16.1", "esprima": "~4.0.0", "source-map": "~0.6.1", "tiny-invariant": "^1.3.3", "tslib": "^2.0.1" } }, "sha512-YTUo+Flmw4ZXiWfQKGcwwc11KnoRAYgzAE2E7mXKCjSviTKShtxBsN6YUUBB2gtaBzKzeKunxhUwNHQuRryhWA=="], + + "recharts": ["recharts@3.8.0", "", { "dependencies": { "@reduxjs/toolkit": "^1.9.0 || 2.x.x", "clsx": "^2.1.1", "decimal.js-light": "^2.5.1", "es-toolkit": "^1.39.3", "eventemitter3": "^5.0.1", "immer": "^10.1.1", "react-redux": "8.x.x || 9.x.x", "reselect": "5.1.1", "tiny-invariant": "^1.3.3", "use-sync-external-store": "^1.2.2", "victory-vendor": "^37.0.2" }, "peerDependencies": { "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0", "react-dom": "^16.0.0 || ^17.0.0 || ^18.0.0 || ^19.0.0", "react-is": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0" } }, "sha512-Z/m38DX3L73ExO4Tpc9/iZWHmHnlzWG4njQbxsF5aSjwqmHNDDIm0rdEBArkwsBvR8U6EirlEHiQNYWCVh9sGQ=="], + + "redux": ["redux@5.0.1", "", {}, "sha512-M9/ELqF6fy8FwmkpnF0S3YKOqMyoWJ4+CS5Efg2ct3oY9daQvd/Pc71FpGZsVsbl3Cpb+IIcjBDUnnyBdQbq4w=="], + + "redux-thunk": ["redux-thunk@3.1.0", "", { "peerDependencies": { "redux": "^5.0.0" } }, "sha512-NW2r5T6ksUKXCabzhL9z+h206HQw/NJkcLm1GPImRQ8IzfXwRGqjVhKJGauHirT0DAuyy6hjdnMZaRoAcy0Klw=="], + + "require-from-string": ["require-from-string@2.0.2", "", {}, "sha512-Xf0nWe6RseziFMu+Ap9biiUbmplq6S9/p+7w7YXP/JBHhrUDDUhwa+vANyubuqfZWTveU//DYVGsDG7RKL/vEw=="], + + "reselect": ["reselect@5.1.1", "", {}, "sha512-K/BG6eIky/SBpzfHZv/dd+9JBFiS4SWV7FIujVyJRux6e45+73RaUHXLmIR1f7WOMaQ0U1km6qwklRQxpJJY0w=="], + + "resolve-from": ["resolve-from@4.0.0", "", {}, "sha512-pb/MYmXstAkysRFx8piNI1tGFNQIFA3vkE3Gq4EuA1dF6gHp/+vgZqsCGJapvy8N3Q+4o7FwvquPJcnZ7RYy4g=="], + + "restore-cursor": ["restore-cursor@5.1.0", "", { "dependencies": { "onetime": "^7.0.0", "signal-exit": "^4.1.0" } }, "sha512-oMA2dcrw6u0YfxJQXm342bFKX/E4sG9rbTzO9ptUcR/e8A33cHuvStiYOwH7fszkZlZ1z/ta9AAoPk2F4qIOHA=="], + + "reusify": ["reusify@1.1.0", "", {}, "sha512-g6QUff04oZpHs0eG5p83rFLhHeV00ug/Yf9nZM6fLeUrPguBTkTQOdpAWWspMh55TZfVQDPaN3NQJfbVRAxdIw=="], + + "rolldown": ["rolldown@1.0.3", "", { "dependencies": { "@oxc-project/types": "=0.133.0", "@rolldown/pluginutils": "^1.0.0" }, "optionalDependencies": { "@rolldown/binding-android-arm64": "1.0.3", "@rolldown/binding-darwin-arm64": "1.0.3", "@rolldown/binding-darwin-x64": "1.0.3", "@rolldown/binding-freebsd-x64": "1.0.3", "@rolldown/binding-linux-arm-gnueabihf": "1.0.3", "@rolldown/binding-linux-arm64-gnu": "1.0.3", "@rolldown/binding-linux-arm64-musl": "1.0.3", "@rolldown/binding-linux-ppc64-gnu": "1.0.3", "@rolldown/binding-linux-s390x-gnu": "1.0.3", "@rolldown/binding-linux-x64-gnu": "1.0.3", "@rolldown/binding-linux-x64-musl": "1.0.3", "@rolldown/binding-openharmony-arm64": "1.0.3", "@rolldown/binding-wasm32-wasi": "1.0.3", "@rolldown/binding-win32-arm64-msvc": "1.0.3", "@rolldown/binding-win32-x64-msvc": "1.0.3" }, "bin": { "rolldown": "./bin/cli.mjs" } }, "sha512-i00lAJ2ks1BYr7rjNjKC7BcqAS7nVfiT3QX1SI5aY+AFHblCmaUf9OE9dbdzDvW6dJxbi2ZCZiy9v3CcwOiX3g=="], + + "router": ["router@2.2.0", "", { "dependencies": { "debug": "^4.4.0", "depd": "^2.0.0", "is-promise": "^4.0.0", "parseurl": "^1.3.3", "path-to-regexp": "^8.0.0" } }, "sha512-nLTrUKm2UyiL7rlhapu/Zl45FwNgkZGaCpZbIHajDYgwlJCOzLSk+cIPAnsEqV955GjILJnKbdQC1nVPz+gAYQ=="], + + "run-applescript": ["run-applescript@7.1.0", "", {}, "sha512-DPe5pVFaAsinSaV6QjQ6gdiedWDcRCbUuiQfQa2wmWV7+xC9bGulGI8+TdRmoFkAPaBXk8CrAbnlY2ISniJ47Q=="], + + "run-parallel": ["run-parallel@1.2.0", "", { "dependencies": { "queue-microtask": "^1.2.2" } }, "sha512-5l4VyZR86LZ/lDxZTR6jqL8AFE2S0IFLMP26AbjsLVADxHdhB/c0GUsH+y39UfCi3dzz8OlQuPmnaJOMoDHQBA=="], + + "safer-buffer": ["safer-buffer@2.1.2", "", {}, "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg=="], + + "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=="], + + "send": ["send@1.2.1", "", { "dependencies": { "debug": "^4.4.3", "encodeurl": "^2.0.0", "escape-html": "^1.0.3", "etag": "^1.8.1", "fresh": "^2.0.0", "http-errors": "^2.0.1", "mime-types": "^3.0.2", "ms": "^2.1.3", "on-finished": "^2.4.1", "range-parser": "^1.2.1", "statuses": "^2.0.2" } }, "sha512-1gnZf7DFcoIcajTjTwjwuDjzuz4PPcY2StKPlsGAQ1+YH20IRVrBaXSWmdjowTJ6u8Rc01PoYOGHXfP1mYcZNQ=="], + + "seroval": ["seroval@1.5.4", "", {}, "sha512-46uFvgrXTVxZcUorgSSRZ4y+ieqLLQRMlG4bnCZKW3qI6BZm7Rg4ntMW4p1mILEEBZWrFlcpp0AyIIlM6jD9iw=="], + + "seroval-plugins": ["seroval-plugins@1.5.4", "", { "peerDependencies": { "seroval": "^1.0" } }, "sha512-S0xQPhUTefAhNvNWFg0c1J8qJArHt5KdtJ/cFAofo06KD1MVSeFWyl4iiu+ApDIuw0WhjpOfCdgConOfAnLgkw=="], + + "serve-static": ["serve-static@2.2.1", "", { "dependencies": { "encodeurl": "^2.0.0", "escape-html": "^1.0.3", "parseurl": "^1.3.3", "send": "^1.2.0" } }, "sha512-xRXBn0pPqQTVQiC8wyQrKs2MOlX24zQ0POGaj0kultvoOCstBQM5yvOhAVSUwOMjQtTvsPWoNCHfPGwaaQJhTw=="], + + "setprototypeof": ["setprototypeof@1.2.0", "", {}, "sha512-E5LDX7Wrp85Kil5bhZv46j8jOeboKq5JMmYM3gVGdGH8xFpPWXUMsNrlODCrkoxMEeNi/XZIwuRvY4XNwYMJpw=="], + + "shadcn": ["shadcn@4.11.0", "", { "dependencies": { "@babel/core": "^7.28.0", "@babel/parser": "^7.28.0", "@babel/plugin-transform-typescript": "^7.28.0", "@babel/preset-typescript": "^7.27.1", "@dotenvx/dotenvx": "^1.48.4", "@modelcontextprotocol/sdk": "^1.26.0", "@types/validate-npm-package-name": "^4.0.2", "browserslist": "^4.26.2", "commander": "^14.0.0", "cosmiconfig": "^9.0.0", "dedent": "^1.6.0", "deepmerge": "^4.3.1", "diff": "^8.0.2", "execa": "^9.6.0", "fast-glob": "^3.3.3", "fs-extra": "^11.3.1", "fuzzysort": "^3.1.0", "https-proxy-agent": "^7.0.6", "kleur": "^4.1.5", "node-fetch": "^3.3.2", "open": "^11.0.0", "ora": "^8.2.0", "postcss": "^8.5.6", "postcss-selector-parser": "^7.1.0", "prompts": "^2.4.2", "recast": "^0.23.11", "stringify-object": "^5.0.0", "tailwind-merge": "^3.0.1", "ts-morph": "^26.0.0", "tsconfig-paths": "^4.2.0", "validate-npm-package-name": "^7.0.1", "zod": "^3.24.1", "zod-to-json-schema": "^3.24.6" }, "bin": { "shadcn": "dist/index.js" } }, "sha512-UV0cchFea9hO7poV1CuEP0wvmYjpAqcxCKdy23bndl2Du2ARtDs8A4xdzfhUjDBeOW1nNpJ6lXmsEpsply2SfQ=="], + + "shebang-command": ["shebang-command@2.0.0", "", { "dependencies": { "shebang-regex": "^3.0.0" } }, "sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA=="], + + "shebang-regex": ["shebang-regex@3.0.0", "", {}, "sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A=="], + + "side-channel": ["side-channel@1.1.1", "", { "dependencies": { "es-errors": "^1.3.0", "object-inspect": "^1.13.4", "side-channel-list": "^1.0.1", "side-channel-map": "^1.0.1", "side-channel-weakmap": "^1.0.2" } }, "sha512-6x6dK6zJdpTzF4sQeNYxwtvBzf6Eg4GtlesS94HOvTudUeyK2WXAaIfmDgsyslYrRBeFIlsi54AYsFGUuhmvrQ=="], + + "side-channel-list": ["side-channel-list@1.0.1", "", { "dependencies": { "es-errors": "^1.3.0", "object-inspect": "^1.13.4" } }, "sha512-mjn/0bi/oUURjc5Xl7IaWi/OJJJumuoJFQJfDDyO46+hBWsfaVM65TBHq2eoZBhzl9EchxOijpkbRC8SVBQU0w=="], + + "side-channel-map": ["side-channel-map@1.0.1", "", { "dependencies": { "call-bound": "^1.0.2", "es-errors": "^1.3.0", "get-intrinsic": "^1.2.5", "object-inspect": "^1.13.3" } }, "sha512-VCjCNfgMsby3tTdo02nbjtM/ewra6jPHmpThenkTYh8pG9ucZ/1P8So4u4FGBek/BjpOVsDCMoLA/iuBKIFXRA=="], + + "side-channel-weakmap": ["side-channel-weakmap@1.0.2", "", { "dependencies": { "call-bound": "^1.0.2", "es-errors": "^1.3.0", "get-intrinsic": "^1.2.5", "object-inspect": "^1.13.3", "side-channel-map": "^1.0.1" } }, "sha512-WPS/HvHQTYnHisLo9McqBHOJk2FkHO/tlpvldyrnem4aeQp4hai3gythswg6p01oSoTl58rcpiFAjF2br2Ak2A=="], + + "signal-exit": ["signal-exit@4.1.0", "", {}, "sha512-bzyZ1e88w9O1iNJbKnOlvYTrWPDl46O1bG0D3XInv+9tkPrxrN8jUUTiFlDkkmKWgn1M6CfIA13SuGqOa9Korw=="], + + "sisteransi": ["sisteransi@1.0.5", "", {}, "sha512-bLGGlR1QxBcynn2d5YmDX4MGjlZvy2MRBDRNHLJ8VI6l6+9FUiyTFNJ0IveOSP0bcXgVDPRcfGqA0pjaqUpfVg=="], + + "sonner": ["sonner@2.0.7", "", { "peerDependencies": { "react": "^18.0.0 || ^19.0.0 || ^19.0.0-rc", "react-dom": "^18.0.0 || ^19.0.0 || ^19.0.0-rc" } }, "sha512-W6ZN4p58k8aDKA4XPcx2hpIQXBRAgyiWVkYhT7CvK6D3iAu7xjvVyhQHg2/iaKJZ1XVJ4r7XuwGL+WGEK37i9w=="], + + "source-map": ["source-map@0.6.1", "", {}, "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g=="], + + "source-map-js": ["source-map-js@1.2.1", "", {}, "sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA=="], + + "statuses": ["statuses@2.0.2", "", {}, "sha512-DvEy55V3DB7uknRo+4iOGT5fP1slR8wQohVdknigZPMpMstaKJQWhwiYBACJE3Ul2pTnATihhBYnRhZQHGBiRw=="], + + "stdin-discarder": ["stdin-discarder@0.2.2", "", {}, "sha512-UhDfHmA92YAlNnCfhmq0VeNL5bDbiZGg7sZ2IvPsXubGkiNa9EC+tUTsjBRsYUAz87btI6/1wf4XoVvQ3uRnmQ=="], + + "string-width": ["string-width@7.2.0", "", { "dependencies": { "emoji-regex": "^10.3.0", "get-east-asian-width": "^1.0.0", "strip-ansi": "^7.1.0" } }, "sha512-tsaTIkKW9b4N+AEj+SVA+WhJzV7/zMhcSu78mLKWSk7cXMOSHsBKFWUs0fWwq8QyK3MgJBQRX6Gbi4kYbdvGkQ=="], + + "stringify-object": ["stringify-object@5.0.0", "", { "dependencies": { "get-own-enumerable-keys": "^1.0.0", "is-obj": "^3.0.0", "is-regexp": "^3.1.0" } }, "sha512-zaJYxz2FtcMb4f+g60KsRNFOpVMUyuJgA51Zi5Z1DOTC3S59+OQiVOzE9GZt0x72uBGWKsQIuBKeF9iusmKFsg=="], + + "strip-ansi": ["strip-ansi@7.2.0", "", { "dependencies": { "ansi-regex": "^6.2.2" } }, "sha512-yDPMNjp4WyfYBkHnjIRLfca1i6KMyGCtsVgoKe/z1+6vukgaENdgGBZt+ZmKPc4gavvEZ5OgHfHdrazhgNyG7w=="], + + "strip-bom": ["strip-bom@3.0.0", "", {}, "sha512-vavAMRXOgBVNF6nyEEmL3DBK19iRpDcoIwW+swQ+CbGiu7lju6t+JklA1MHweoWtadgt4ISVUsXLyDq34ddcwA=="], + + "strip-final-newline": ["strip-final-newline@4.0.0", "", {}, "sha512-aulFJcD6YK8V1G7iRB5tigAP4TsHBZZrOV8pjV++zdUwmeV8uzbY7yn6h9MswN62adStNZFuCIx4haBnRuMDaw=="], + + "systeminformation": ["systeminformation@5.31.7", "", { "os": "!aix", "bin": { "systeminformation": "lib/cli.js" } }, "sha512-/8NC53e5nP9nmhn42/ncdOkyJnOoue/Vy+tJOyUGd1Yv66G069wK4rrziwhrqDETgk78CudTQupw5z19S5uoZw=="], + + "tailwind-merge": ["tailwind-merge@3.6.0", "", {}, "sha512-uxL7qAVQriqRQPAyK3pj66VqskWqoZ37PW94jwOTwNfq/z9oyu1V+eqrZqtR2+fCiXdYOZe/Modt8GtvqNzu+w=="], + + "tailwindcss": ["tailwindcss@4.3.1", "", {}, "sha512-hk+TB1m+K8CYNrP6rjQaq/Y+4Zylwpa87mLYBKCunwnnQ9p+fHb7kmSfGqyEJoxF/O6CDyABWVFEafNSYKll+Q=="], + + "tapable": ["tapable@2.3.3", "", {}, "sha512-uxc/zpqFg6x7C8vOE7lh6Lbda8eEL9zmVm/PLeTPBRhh1xCgdWaQ+J1CUieGpIfm2HdtsUpRv+HshiasBMcc6A=="], + + "tiny-invariant": ["tiny-invariant@1.3.3", "", {}, "sha512-+FbBPE1o9QAYvviau/qC5SE3caw21q3xkvWKBtja5vgqOWIHHJ3ioaq1VPfn/Szqctz2bU/oYeKd9/z5BL+PVg=="], + + "tinyglobby": ["tinyglobby@0.2.17", "", { "dependencies": { "fdir": "^6.5.0", "picomatch": "^4.0.4" } }, "sha512-wXR/dYpcqKmfWpEdZjiKJOwCNFndD0DMnrW/cYjVGttEkBfVgcLFHoNrlj47mjOVic9yyNu65alsgF4NQyTa2g=="], + + "to-regex-range": ["to-regex-range@5.0.1", "", { "dependencies": { "is-number": "^7.0.0" } }, "sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ=="], + + "toidentifier": ["toidentifier@1.0.1", "", {}, "sha512-o5sSPKEkg/DIQNmH43V0/uerLrpzVedkUh8tGNvaeXpfpuwjKenlSox/2O/BTlZUtEe+JG7s5YhEz608PlAHRA=="], + + "ts-api-utils": ["ts-api-utils@2.5.0", "", { "peerDependencies": { "typescript": ">=4.8.4" } }, "sha512-OJ/ibxhPlqrMM0UiNHJ/0CKQkoKF243/AEmplt3qpRgkW8VG7IfOS41h7V8TjITqdByHzrjcS/2si+y4lIh8NA=="], + + "ts-morph": ["ts-morph@26.0.0", "", { "dependencies": { "@ts-morph/common": "~0.27.0", "code-block-writer": "^13.0.3" } }, "sha512-ztMO++owQnz8c/gIENcM9XfCEzgoGphTv+nKpYNM1bgsdOVC/jRZuEBf6N+mLLDNg68Kl+GgUZfOySaRiG1/Ug=="], + + "tsconfig-paths": ["tsconfig-paths@4.2.0", "", { "dependencies": { "json5": "^2.2.2", "minimist": "^1.2.6", "strip-bom": "^3.0.0" } }, "sha512-NoZ4roiN7LnbKn9QqE1amc9DJfzvZXxF4xDavcOWt1BPkdx+m+0gJuPM+S0vCe7zTJMYUP0R8pO2XMr+Y8oLIg=="], + + "tslib": ["tslib@2.8.1", "", {}, "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w=="], + + "tw-animate-css": ["tw-animate-css@1.4.0", "", {}, "sha512-7bziOlRqH0hJx80h/3mbicLW7o8qLsH5+RaLR2t+OHM3D0JlWGODQKQ4cxbK7WlvmUxpcj6Kgu6EKqjrGFe3QQ=="], + + "type-check": ["type-check@0.4.0", "", { "dependencies": { "prelude-ls": "^1.2.1" } }, "sha512-XleUoc9uwGXqjWwXaUTZAmzMcFZ5858QA2vvx1Ur5xIcixXIP+8LnFDgRplU30us6teqdlskFfu+ae4K79Ooew=="], + + "type-is": ["type-is@2.1.0", "", { "dependencies": { "content-type": "^2.0.0", "media-typer": "^1.1.0", "mime-types": "^3.0.0" } }, "sha512-faYHw0anBbc/kWF3zFTEnxSFOAGUX9GFbOBthvDdLsIlEoWOFOtS0zgCiQYwIskL9iGXZL3kAXD8OoZ4GmMATA=="], + + "typescript": ["typescript@6.0.3", "", { "bin": { "tsc": "bin/tsc", "tsserver": "bin/tsserver" } }, "sha512-y2TvuxSZPDyQakkFRPZHKFm+KKVqIisdg9/CZwm9ftvKXLP8NRWj38/ODjNbr43SsoXqNuAisEf1GdCxqWcdBw=="], + + "typescript-eslint": ["typescript-eslint@8.61.1", "", { "dependencies": { "@typescript-eslint/eslint-plugin": "8.61.1", "@typescript-eslint/parser": "8.61.1", "@typescript-eslint/typescript-estree": "8.61.1", "@typescript-eslint/utils": "8.61.1" }, "peerDependencies": { "eslint": "^8.57.0 || ^9.0.0 || ^10.0.0", "typescript": ">=4.8.4 <6.1.0" } }, "sha512-V7PayAfJokV3pEHgN7/v03D1SpujhRfQtYLbLIiBfDDncdg4PAiRBfoS4cnCANK4jmAPncczi59QO3afiXUlNw=="], + + "undici": ["undici@7.28.0", "", {}, "sha512-cRZYrTDwWznlnRiPjggAGxZXanty6M8RV1ff8Wm4LWXBp7/IG8v5DnOm74DtUBp9OONpK75YlPnIjQqX0dBDtA=="], + + "undici-types": ["undici-types@7.18.2", "", {}, "sha512-AsuCzffGHJybSaRrmr5eHr81mwJU3kjw6M+uprWvCXiNeN9SOGwQ3Jn8jb8m3Z6izVgknn1R0FTCEAP2QrLY/w=="], + + "unicorn-magic": ["unicorn-magic@0.3.0", "", {}, "sha512-+QBBXBCvifc56fsbuxZQ6Sic3wqqc3WWaqxs58gvJrcOuN83HGTCwz3oS5phzU9LthRNE9VrJCFCLUgHeeFnfA=="], + + "universalify": ["universalify@2.0.1", "", {}, "sha512-gptHNQghINnc/vTGIk0SOFGFNXw7JVrlRUtConJRlvaw6DuX0wO5Jeko9sWrMBhh+PsYAZ7oXAiOnf/UKogyiw=="], + + "unpipe": ["unpipe@1.0.0", "", {}, "sha512-pjy2bYhSsufwWlKwPc+l3cN7+wuJlK6uz0YdJEOlQDbl6jo/YlPi4mb8agUkVC8BF7V8NuzeyPNqRksA3hztKQ=="], + + "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=="], + + "uri-js": ["uri-js@4.4.1", "", { "dependencies": { "punycode": "^2.1.0" } }, "sha512-7rKUyy33Q1yc98pQ1DAmLtwX109F7TIfWlW1Ydo8Wl1ii1SeHieeh0HHfPeL2fMXK6z0s8ecKs9frCuLJvndBg=="], + + "use-callback-ref": ["use-callback-ref@1.3.3", "", { "dependencies": { "tslib": "^2.0.0" }, "peerDependencies": { "@types/react": "*", "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react"] }, "sha512-jQL3lRnocaFtu3V00JToYz/4QkNWswxijDaCVNZRiRTO3HQDLsdu1ZtmIUvV4yPp+rvWm5j0y0TG/S61cuijTg=="], + + "use-sidecar": ["use-sidecar@1.1.3", "", { "dependencies": { "detect-node-es": "^1.1.0", "tslib": "^2.0.0" }, "peerDependencies": { "@types/react": "*", "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react"] }, "sha512-Fedw0aZvkhynoPYlA5WXrMCAMm+nSWdZt6lzJQ7Ok8S6Q+VsHmHpRWndVRJ8Be0ZbkfPc5LRYH+5XrzXcEeLRQ=="], + + "use-sync-external-store": ["use-sync-external-store@1.6.0", "", { "peerDependencies": { "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0" } }, "sha512-Pp6GSwGP/NrPIrxVFAIkOQeyw8lFenOHijQWkUTrDvrF4ALqylP2C/KCkeS9dpUM3KvYRQhna5vt7IL95+ZQ9w=="], + + "util-deprecate": ["util-deprecate@1.0.2", "", {}, "sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw=="], + + "validate-npm-package-name": ["validate-npm-package-name@7.0.2", "", {}, "sha512-hVDIBwsRruT73PbK7uP5ebUt+ezEtCmzZz3F59BSr2F6OVFnJ/6h8liuvdLrQ88Xmnk6/+xGGuq+pG9WwTuy3A=="], + + "vary": ["vary@1.1.2", "", {}, "sha512-BNGbWLfd0eUPabhkXUVm0j8uuvREyTh5ovRa/dyow/BqAbZJyC+5fU+IzQOzmAKzYqYRAISoRhdQr3eIZ/PXqg=="], + + "vaul": ["vaul@1.1.2", "", { "dependencies": { "@radix-ui/react-dialog": "^1.1.1" }, "peerDependencies": { "react": "^16.8 || ^17.0 || ^18.0 || ^19.0.0 || ^19.0.0-rc", "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0.0 || ^19.0.0-rc" } }, "sha512-ZFkClGpWyI2WUQjdLJ/BaGuV6AVQiJ3uELGk3OYtP+B6yCO7Cmn9vPFXVJkRaGkOJu3m8bQMgtyzNHixULceQA=="], + + "victory-vendor": ["victory-vendor@37.3.6", "", { "dependencies": { "@types/d3-array": "^3.0.3", "@types/d3-ease": "^3.0.0", "@types/d3-interpolate": "^3.0.1", "@types/d3-scale": "^4.0.2", "@types/d3-shape": "^3.1.0", "@types/d3-time": "^3.0.0", "@types/d3-timer": "^3.0.0", "d3-array": "^3.1.6", "d3-ease": "^3.0.1", "d3-interpolate": "^3.0.1", "d3-scale": "^4.0.2", "d3-shape": "^3.1.0", "d3-time": "^3.0.0", "d3-timer": "^3.0.1" } }, "sha512-SbPDPdDBYp+5MJHhBCAyI7wKM3d5ivekigc2Dk2s7pgbZ9wIgIBYGVw4zGHBml/qTFbexrofXW6Gu4noGxrOwQ=="], + + "vite": ["vite@8.0.16", "", { "dependencies": { "lightningcss": "^1.32.0", "picomatch": "^4.0.4", "postcss": "^8.5.15", "rolldown": "1.0.3", "tinyglobby": "^0.2.17" }, "optionalDependencies": { "fsevents": "~2.3.3" }, "peerDependencies": { "@types/node": "^20.19.0 || >=22.12.0", "@vitejs/devtools": "^0.1.18", "esbuild": "^0.27.0 || ^0.28.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-h9bXPmJichP5fLmVQo3PyaGSDE2n3aPuomeAlVRm0JLmt4rY6zmPKd59HYI4LNW8oTK7tlTsuC7l/m7awx9Jcw=="], + + "web-streams-polyfill": ["web-streams-polyfill@3.3.3", "", {}, "sha512-d2JWLCivmZYTSIoge9MsgFCZrt571BikcWGYkjC1khllbTeDlGqZ2D8vD8E/lJa8WGWbb7Plm8/XJYV7IJHZZw=="], + + "which": ["which@2.0.2", "", { "dependencies": { "isexe": "^2.0.0" }, "bin": { "node-which": "./bin/node-which" } }, "sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA=="], + + "word-wrap": ["word-wrap@1.2.5", "", {}, "sha512-BN22B5eaMMI9UMtjrGd5g5eCYPpCPDUy0FJXbYsaT5zYxjFOckS53SQDE3pWkVoWpHXVb3BrYcEN4Twa55B5cA=="], + + "wrappy": ["wrappy@1.0.2", "", {}, "sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ=="], + + "wsl-utils": ["wsl-utils@0.3.1", "", { "dependencies": { "is-wsl": "^3.1.0", "powershell-utils": "^0.1.0" } }, "sha512-g/eziiSUNBSsdDJtCLB8bdYEUMj4jR7AGeUo96p/3dTafgjHhpF4RiCFPiRILwjQoDXx5MqkBr4fwWtR3Ky4Wg=="], + + "yallist": ["yallist@3.1.1", "", {}, "sha512-a4UGQaWPH59mOXUYnAG2ewncQS4i4F43Tv3JoAM+s2VDAmS9NsK8GpDMLrCHPksFT7h3K6TOoUNn2pb7RoXx4g=="], + + "yocto-queue": ["yocto-queue@0.1.0", "", {}, "sha512-rVksvsnNCdJ/ohGc6xgPwyN8eheCxsiLM8mxuE/t/mOVqJewPuO1miLpTHQiRgTKCLexL4MeAFVagts7HmNZ2Q=="], + + "yocto-spinner": ["yocto-spinner@1.2.0", "", { "dependencies": { "yoctocolors": "^2.1.1" } }, "sha512-Yw0hUB6UA3o4YUgKy3oSe9a4cxoaZ9sBfYDw+JSxo6Id0KoJGoxzPA24qqUXYKBWABs/zDSGTz9kww7t3F0XGw=="], + + "yoctocolors": ["yoctocolors@2.1.2", "", {}, "sha512-CzhO+pFNo8ajLM2d2IW/R93ipy99LWjtwblvC1RsoSUMZgyLbYFr221TnSNT7GjGdYui6P459mw9JH/g/zW2ug=="], + + "zod": ["zod@4.4.3", "", {}, "sha512-ytENFjIJFl2UwYglde2jchW2Hwm4GJFLDiSXWdTrJQBIN9Fcyp7n4DhxJEiWNAJMV1/BqWfW/kkg71UDcHJyTQ=="], + + "zod-to-json-schema": ["zod-to-json-schema@3.25.2", "", { "peerDependencies": { "zod": "^3.25.28 || ^4" } }, "sha512-O/PgfnpT1xKSDeQYSCfRI5Gy3hPf91mKVDuYLUHZJMiDFptvP41MSnWofm8dnCm0256ZNfZIM7DSzuSMAFnjHA=="], + + "zod-validation-error": ["zod-validation-error@4.0.2", "", { "peerDependencies": { "zod": "^3.25.0 || ^4.0.0" } }, "sha512-Q6/nZLe6jxuU80qb/4uJ4t5v2VEZ44lzQjPDhYJNztRQ4wyWc6VF3D3Kb/fAuPetZQnhS3hnajCf9CsWesghLQ=="], + + "@base-ui/utils/reselect": ["reselect@5.2.0", "", {}, "sha512-AgZ3UOZm3YndfrJ4OYjgrT7bmCm/1iqkjvEfH/oYjzh6PD2qw4QuT3jjnXIrpdt4MTpMXclMT3lXbmRY+XRakw=="], + + "@dotenvx/dotenvx/commander": ["commander@11.1.0", "", {}, "sha512-yPVavfyCcRhmorC7rWlkHn15b4wDVgVmBA7kV4QVBsF7kv/9TKJAbAXVTxvTnwP8HHKjRCJDClKbciiYS7p0DQ=="], + + "@dotenvx/dotenvx/execa": ["execa@5.1.1", "", { "dependencies": { "cross-spawn": "^7.0.3", "get-stream": "^6.0.0", "human-signals": "^2.1.0", "is-stream": "^2.0.0", "merge-stream": "^2.0.0", "npm-run-path": "^4.0.1", "onetime": "^5.1.2", "signal-exit": "^3.0.3", "strip-final-newline": "^2.0.0" } }, "sha512-8uSpZZocAZRBAPIEINJj3Lo9HyGitllczc27Eh5YYojjMFMn8yHMDMaUHE2Jqfq05D/wucwI4JGURyXt1vchyg=="], + + "@dotenvx/dotenvx/open": ["open@8.4.2", "", { "dependencies": { "define-lazy-prop": "^2.0.0", "is-docker": "^2.1.1", "is-wsl": "^2.2.0" } }, "sha512-7x81NCL719oNbsq/3mh+hVrAWmFuEYUqrq/Iw3kUzH8ReypT9QQ0BLoJS7/G9k6N81XjW4qHWtjWwe/9eLy1EQ=="], + + "@dotenvx/dotenvx/which": ["which@4.0.0", "", { "dependencies": { "isexe": "^3.1.1" }, "bin": { "node-which": "bin/which.js" } }, "sha512-GlaYyEb07DPxYCKhKzplCWBJtvxZcZMrL+4UkrTSJHHPyZU4mYYTv3qaOe77H7EODLSSopAUFAc6W8U4yqvscg=="], + + "@eslint-community/eslint-utils/eslint-visitor-keys": ["eslint-visitor-keys@3.4.3", "", {}, "sha512-wpc+LXeiyiisxPlEkUzU6svyS1frIO3Mgxj1fdy7Pm8Ygzguax2N3Fa/D/ag1WqbOprdI+uY6wMUl8/a2G+iag=="], + + "@modelcontextprotocol/sdk/ajv": ["ajv@8.20.0", "", { "dependencies": { "fast-deep-equal": "^3.1.3", "fast-uri": "^3.0.1", "json-schema-traverse": "^1.0.0", "require-from-string": "^2.0.2" } }, "sha512-Thbli+OlOj+iMPYFBVBfJ3OmCAnaSyNn4M1vz9T6Gka5Jt9ba/HIR56joy65tY6kx/FCF5VXNB819Y7/GUrBGA=="], + + "@reduxjs/toolkit/immer": ["immer@11.1.8", "", {}, "sha512-/tbkHMW7y10Lx6i1crLjD4/OhNkRG+Fo7byZHtah0547nIeXYcpIXaUh0IAQY6gO5459qpGGYapcEOHtFXkIuA=="], + + "@tailwindcss/oxide-wasm32-wasi/@emnapi/core": ["@emnapi/core@1.10.0", "", { "dependencies": { "@emnapi/wasi-threads": "1.2.1", "tslib": "^2.4.0" }, "bundled": true }, "sha512-yq6OkJ4p82CAfPl0u9mQebQHKPJkY7WrIuk205cTYnYe+k2Z8YBh11FrbRG/H6ihirqcacOgl2BIO8oyMQLeXw=="], + + "@tailwindcss/oxide-wasm32-wasi/@emnapi/runtime": ["@emnapi/runtime@1.10.0", "", { "dependencies": { "tslib": "^2.4.0" }, "bundled": true }, "sha512-ewvYlk86xUoGI0zQRNq/mC+16R1QeDlKQy21Ki3oSYXNgLb45GV1P6A0M+/s6nyCuNDqe5VpaY84BzXGwVbwFA=="], + + "@tailwindcss/oxide-wasm32-wasi/@emnapi/wasi-threads": ["@emnapi/wasi-threads@1.2.1", "", { "dependencies": { "tslib": "^2.4.0" }, "bundled": true }, "sha512-uTII7OYF+/Mes/MrcIOYp5yOtSMLBWSIoLPpcgwipoiKbli6k322tcoFsxoIIxPDqW01SQGAgko4EzZi2BNv2w=="], + + "@tailwindcss/oxide-wasm32-wasi/@napi-rs/wasm-runtime": ["@napi-rs/wasm-runtime@1.1.5", "", { "dependencies": { "@tybys/wasm-util": "^0.10.2" }, "peerDependencies": { "@emnapi/core": "^1.7.1", "@emnapi/runtime": "^1.7.1" }, "bundled": true }, "sha512-AWPoBRJ9tsnVhor4sjO7rkni+7p+2IAEFj6cx06UgP10jkQHqay/36uRV/bFkgrh18D9vb4cr8Q0Pthskgzy+Q=="], + + "@tailwindcss/oxide-wasm32-wasi/@tybys/wasm-util": ["@tybys/wasm-util@0.10.2", "", { "dependencies": { "tslib": "^2.4.0" }, "bundled": true }, "sha512-RoBvJ2X0wuKlWFIjrwffGw1IqZHKQqzIchKaadZZfnNpsAYp2mM0h36JtPCjNDAHGgYez/15uMBpfGwchhiMgg=="], + + "@tailwindcss/oxide-wasm32-wasi/tslib": ["tslib@2.8.1", "", { "bundled": true }, "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w=="], + + "@typescript-eslint/eslint-plugin/ignore": ["ignore@7.0.5", "", {}, "sha512-Hs59xBNfUIunMFgWAbGX5cq6893IbWg4KnrjbYwX3tx0ztorVgTDA6B2sxf8ejHJ4wz8BqGUMYlnzNBer5NvGg=="], + + "@typescript-eslint/typescript-estree/semver": ["semver@7.8.4", "", { "bin": { "semver": "bin/semver.js" } }, "sha512-rUCObTnP32Q08R2uuIrt7r9PlEonuTmtuXYcW6s5kjdlj3xbnwe+21yXptAUYcMAABLkYYTtnmzb3w3EDZfueA=="], + + "ajv-formats/ajv": ["ajv@8.20.0", "", { "dependencies": { "fast-deep-equal": "^3.1.3", "fast-uri": "^3.0.1", "json-schema-traverse": "^1.0.0", "require-from-string": "^2.0.2" } }, "sha512-Thbli+OlOj+iMPYFBVBfJ3OmCAnaSyNn4M1vz9T6Gka5Jt9ba/HIR56joy65tY6kx/FCF5VXNB819Y7/GUrBGA=="], + + "body-parser/content-type": ["content-type@2.0.0", "", {}, "sha512-j/O/d7GcZCyNl7/hwZAb606rzqkyvaDctLmckbxLzHvFBzTJHuGEdodATcP3yIRoDrLHkIATJuvzbFlp/ki2cQ=="], + + "conf/ajv": ["ajv@8.20.0", "", { "dependencies": { "fast-deep-equal": "^3.1.3", "fast-uri": "^3.0.1", "json-schema-traverse": "^1.0.0", "require-from-string": "^2.0.2" } }, "sha512-Thbli+OlOj+iMPYFBVBfJ3OmCAnaSyNn4M1vz9T6Gka5Jt9ba/HIR56joy65tY6kx/FCF5VXNB819Y7/GUrBGA=="], + + "conf/ajv-formats": ["ajv-formats@2.1.1", "", { "dependencies": { "ajv": "^8.0.0" } }, "sha512-Wx0Kx52hxE7C18hkMEggYlEifqWZtYaRgouJor+WMdPnQyEK13vgEWyVNup7SoeeoLMsr4kf5h6dOW11I15MUA=="], + + "conf/json-schema-typed": ["json-schema-typed@7.0.3", "", {}, "sha512-7DE8mpG+/fVw+dTpjbxnx47TaMnDfOI1jwft9g1VybltZCduyRQPJPvc+zzKY9WPHxhPWczyFuYa6I8Mw4iU5A=="], + + "conf/semver": ["semver@7.8.4", "", { "bin": { "semver": "bin/semver.js" } }, "sha512-rUCObTnP32Q08R2uuIrt7r9PlEonuTmtuXYcW6s5kjdlj3xbnwe+21yXptAUYcMAABLkYYTtnmzb3w3EDZfueA=="], + + "dot-prop/is-obj": ["is-obj@2.0.0", "", {}, "sha512-drqDG3cbczxxEJRoOXcOjtdp1J/lyp1mNn0xaznRs8+muBhgQcrnbspox5X5fOw0HnMnbfDzvnEMEtqDEJEo8w=="], + + "enquirer/strip-ansi": ["strip-ansi@6.0.1", "", { "dependencies": { "ansi-regex": "^5.0.1" } }, "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A=="], + + "fast-glob/glob-parent": ["glob-parent@5.1.2", "", { "dependencies": { "is-glob": "^4.0.1" } }, "sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow=="], + + "is-inside-container/is-docker": ["is-docker@3.0.0", "", { "bin": { "is-docker": "cli.js" } }, "sha512-eljcgEDlEns/7AXFosB5K/2nCM4P7FQPkGc/DWLy5rmFEWvZayGrik1d9/QIY5nJ4f9YsVvBkA6kJpHn9rISdQ=="], + + "log-symbols/is-unicode-supported": ["is-unicode-supported@1.3.0", "", {}, "sha512-43r2mRvz+8JRIKnWJ+3j8JtjRKZ6GmjzfaE/qiBJnikNnYv/6bagRJ1kUhNk8R5EX/GkobD+r+sfxCPJsiKBLQ=="], + + "micromatch/picomatch": ["picomatch@2.3.2", "", {}, "sha512-V7+vQEJ06Z+c5tSye8S+nHUfI51xoXIXjHQ99cQtKUkQqqO1kO/KCJUfZXuB47h/YBlDhah2H3hdUGXn8ie0oA=="], + + "npm-run-path/path-key": ["path-key@4.0.0", "", {}, "sha512-haREypq7xkM7ErfgIyA0z+Bj4AGKlMSdlQE2jvJo6huWD1EdkKYV+G/T4nq0YEF2vgTT8kqMFKo1uHn950r4SQ=="], + + "onetime/mimic-fn": ["mimic-fn@2.1.0", "", {}, "sha512-OqbOk5oEQeAZ8WXWydlu9HJjz9WVdEIvamMCcXmuqUYjTknH/sqsWvhQ3vgwKFRR1HpjvNBKQ37nbJgYzGqGcg=="], + + "pkg-up/find-up": ["find-up@3.0.0", "", { "dependencies": { "locate-path": "^3.0.0" } }, "sha512-1yD6RmLI1XBfxugvORwlck6f75tYL+iR0jqwsOrOxMZyGYqUuDhJ0l4AXdO1iX/FTs9cBAMEk1gWSEx1kSbylg=="], + + "prompts/kleur": ["kleur@3.0.3", "", {}, "sha512-eTIzlVOSUR+JxdDFepEYcBMtZ9Qqdef+rnzWdRZuMbOywu5tO2w2N7rqjoANZ5k9vywhL6Br1VRjUIgTQx4E8w=="], + + "restore-cursor/onetime": ["onetime@7.0.0", "", { "dependencies": { "mimic-function": "^5.0.0" } }, "sha512-VXJjc87FScF88uafS3JllDgvAm+c/Slfz06lorj2uAY34rlUu0Nt+v8wreiImcrgAjjIHp1rXpTDlLOGw29WwQ=="], + + "shadcn/zod": ["zod@3.25.76", "", {}, "sha512-gzUt/qt81nXsFGKIFcC3YnfEAx5NkunCfnDlvuBSSFS02bcXu4Lmea0AFIUwbLWxWPx3d9p8S5QoaujKcNQxcQ=="], + + "type-is/content-type": ["content-type@2.0.0", "", {}, "sha512-j/O/d7GcZCyNl7/hwZAb606rzqkyvaDctLmckbxLzHvFBzTJHuGEdodATcP3yIRoDrLHkIATJuvzbFlp/ki2cQ=="], + + "wsl-utils/is-wsl": ["is-wsl@3.1.1", "", { "dependencies": { "is-inside-container": "^1.0.0" } }, "sha512-e6rvdUCiQCAuumZslxRJWR/Doq4VpPR82kqclvcS0efgt430SlGIk05vdCN58+VrzgtIcfNODjozVielycD4Sw=="], + + "@dotenvx/dotenvx/execa/get-stream": ["get-stream@6.0.1", "", {}, "sha512-ts6Wi+2j3jQjqi70w5AlN8DFnkSwC+MqmxEzdEALB2qXZYV3X/b1CTfgPLGJNMeAWxdPfU8FO1ms3NUfaHCPYg=="], + + "@dotenvx/dotenvx/execa/human-signals": ["human-signals@2.1.0", "", {}, "sha512-B4FFZ6q/T2jhhksgkbEW3HBvWIfDW85snkQgawt07S7J5QXTk6BkNV+0yAeZrM5QpMAdYlocGoljn0sJ/WQkFw=="], + + "@dotenvx/dotenvx/execa/is-stream": ["is-stream@2.0.1", "", {}, "sha512-hFoiJiTl63nn+kstHGBtewWSKnQLpyb155KHheA1l39uvtO9nWIop1p3udqPcUd/xbF1VLMO4n7OI6p7RbngDg=="], + + "@dotenvx/dotenvx/execa/npm-run-path": ["npm-run-path@4.0.1", "", { "dependencies": { "path-key": "^3.0.0" } }, "sha512-S48WzZW777zhNIrn7gxOlISNAqi9ZC/uQFnRdbeIHhZhCA6UqpkOT8T1G7BvfdgP4Er8gF4sUbaS0i7QvIfCWw=="], + + "@dotenvx/dotenvx/execa/signal-exit": ["signal-exit@3.0.7", "", {}, "sha512-wnD2ZE+l+SPC/uoS0vXeE9L1+0wuaMqKlfz9AMUo38JsyLSBWSFcHR1Rri62LZc12vLr1gb3jl7iwQhgwpAbGQ=="], + + "@dotenvx/dotenvx/execa/strip-final-newline": ["strip-final-newline@2.0.0", "", {}, "sha512-BrpvfNAE3dcvq7ll3xVumzjKjZQ5tI1sEUIKr3Uoks0XUl45St3FlatVqef9prk4jRDzhW6WZg+3bk93y6pLjA=="], + + "@dotenvx/dotenvx/open/define-lazy-prop": ["define-lazy-prop@2.0.0", "", {}, "sha512-Ds09qNh8yw3khSjiJjiUInaGX9xlqZDY7JVryGxdxV7NPeuqQfplOpQ66yJFZut3jLa5zOwkXw1g9EI2uKh4Og=="], + + "@dotenvx/dotenvx/which/isexe": ["isexe@3.1.5", "", {}, "sha512-6B3tLtFqtQS4ekarvLVMZ+X+VlvQekbe4taUkf/rhVO3d/h0M2rfARm/pXLcPEsjjMsFgrFgSrhQIxcSVrBz8w=="], + + "@modelcontextprotocol/sdk/ajv/json-schema-traverse": ["json-schema-traverse@1.0.0", "", {}, "sha512-NM8/P9n3XjXhIZn1lLhkFaACTOURQXjWhV4BA/RnOv8xvgqtqpAX9IO4mRQxSx1Rlo4tqzeqb0sOlruaOy3dug=="], + + "ajv-formats/ajv/json-schema-traverse": ["json-schema-traverse@1.0.0", "", {}, "sha512-NM8/P9n3XjXhIZn1lLhkFaACTOURQXjWhV4BA/RnOv8xvgqtqpAX9IO4mRQxSx1Rlo4tqzeqb0sOlruaOy3dug=="], + + "conf/ajv/json-schema-traverse": ["json-schema-traverse@1.0.0", "", {}, "sha512-NM8/P9n3XjXhIZn1lLhkFaACTOURQXjWhV4BA/RnOv8xvgqtqpAX9IO4mRQxSx1Rlo4tqzeqb0sOlruaOy3dug=="], + + "enquirer/strip-ansi/ansi-regex": ["ansi-regex@5.0.1", "", {}, "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ=="], + + "pkg-up/find-up/locate-path": ["locate-path@3.0.0", "", { "dependencies": { "p-locate": "^3.0.0", "path-exists": "^3.0.0" } }, "sha512-7AO748wWnIhNqAuaty2ZWHkQHRSNfPVIsPIfwEOWO22AmaoVrWavlOcMR5nzTLNYvp36X220/maaRsrec1G65A=="], + + "pkg-up/find-up/locate-path/p-locate": ["p-locate@3.0.0", "", { "dependencies": { "p-limit": "^2.0.0" } }, "sha512-x+12w/To+4GFfgJhBEpiDcLozRJGegY+Ei7/z0tSLkMmxGZNybVMSfWj9aJn8Z5Fc7dBUNJOOVgPv2H7IwulSQ=="], + + "pkg-up/find-up/locate-path/path-exists": ["path-exists@3.0.0", "", {}, "sha512-bpC7GYwiDYQ4wYLe+FA8lhRjhQCMcQGuSgGGqDkg/QerRWw9CmGRT0iSOVRSZJ29NMLZgIzqaljJ63oaL4NIJQ=="], + + "pkg-up/find-up/locate-path/p-locate/p-limit": ["p-limit@2.3.0", "", { "dependencies": { "p-try": "^2.0.0" } }, "sha512-//88mFWSJx8lxCzwdAABTJL2MyWB12+eIY7MDL2SqLmAkeKU9qxRvWuSyTjm3FUmpBEMuFfckAIqEaVGUDxb6w=="], + } +} diff --git a/spa/components.json b/spa/components.json new file mode 100644 index 0000000..5c23ec4 --- /dev/null +++ b/spa/components.json @@ -0,0 +1,25 @@ +{ + "$schema": "https://ui.shadcn.com/schema.json", + "style": "radix-nova", + "rsc": false, + "tsx": true, + "tailwind": { + "config": "", + "css": "src/index.css", + "baseColor": "neutral", + "cssVariables": true, + "prefix": "" + }, + "iconLibrary": "lucide", + "rtl": false, + "aliases": { + "components": "@/components", + "utils": "@/lib/utils", + "ui": "@/components/ui", + "lib": "@/lib", + "hooks": "@/hooks" + }, + "menuColor": "default", + "menuAccent": "subtle", + "registries": {} +} diff --git a/spa/eslint.config.js b/spa/eslint.config.js new file mode 100644 index 0000000..ef614d2 --- /dev/null +++ b/spa/eslint.config.js @@ -0,0 +1,22 @@ +import js from '@eslint/js' +import globals from 'globals' +import reactHooks from 'eslint-plugin-react-hooks' +import reactRefresh from 'eslint-plugin-react-refresh' +import tseslint from 'typescript-eslint' +import { defineConfig, globalIgnores } from 'eslint/config' + +export default defineConfig([ + globalIgnores(['dist']), + { + files: ['**/*.{ts,tsx}'], + extends: [ + js.configs.recommended, + tseslint.configs.recommended, + reactHooks.configs.flat.recommended, + reactRefresh.configs.vite, + ], + languageOptions: { + globals: globals.browser, + }, + }, +]) diff --git a/spa/index.html b/spa/index.html new file mode 100644 index 0000000..f7a3d71 --- /dev/null +++ b/spa/index.html @@ -0,0 +1,13 @@ + + + + + + + K-Frame + + +
+ + + diff --git a/spa/package.json b/spa/package.json new file mode 100644 index 0000000..37aaf58 --- /dev/null +++ b/spa/package.json @@ -0,0 +1,57 @@ +{ + "name": "spa", + "private": true, + "version": "0.0.1", + "type": "module", + "scripts": { + "dev": "vite", + "build": "tsc -b && vite build", + "lint": "eslint .", + "format": "prettier --write \"**/*.{ts,tsx}\"", + "typecheck": "tsc --noEmit", + "preview": "vite preview" + }, + "dependencies": { + "@base-ui/react": "^1.6.0", + "@fontsource-variable/geist": "^5.2.9", + "@tailwindcss/vite": "^4", + "@tanstack/react-query": "^5.101.0", + "@tanstack/react-router": "^1.170.16", + "class-variance-authority": "^0.7.1", + "clsx": "^2.1.1", + "cmdk": "^1.1.1", + "date-fns": "^4.4.0", + "embla-carousel-react": "^8.6.0", + "input-otp": "^1.4.2", + "lucide-react": "^1.21.0", + "next-themes": "^0.4.6", + "radix-ui": "^1.6.0", + "react": "^19.2.6", + "react-day-picker": "^10.0.1", + "react-dom": "^19.2.6", + "react-resizable-panels": "^4.11.2", + "recharts": "3.8.0", + "shadcn": "^4.11.0", + "sonner": "^2.0.7", + "tailwind-merge": "^3.6.0", + "tailwindcss": "^4", + "tw-animate-css": "^1.4.0", + "vaul": "^1.1.2" + }, + "devDependencies": { + "@eslint/js": "^10", + "@types/node": "^24", + "@types/react": "^19", + "@types/react-dom": "^19", + "@vitejs/plugin-react": "^6", + "eslint": "^10", + "eslint-plugin-react-hooks": "^7.1.1", + "eslint-plugin-react-refresh": "^0.5.2", + "globals": "^17", + "prettier": "^3.8.3", + "prettier-plugin-tailwindcss": "^0.8.0", + "typescript": "~6", + "typescript-eslint": "^8", + "vite": "^8" + } +} diff --git a/spa/public/vite.svg b/spa/public/vite.svg new file mode 100644 index 0000000..e7b8dfb --- /dev/null +++ b/spa/public/vite.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/spa/src/api/client.ts b/spa/src/api/client.ts new file mode 100644 index 0000000..7c39830 --- /dev/null +++ b/spa/src/api/client.ts @@ -0,0 +1,28 @@ +const BASE = "/api" + +async function request(path: string, init?: RequestInit): Promise { + const res = await fetch(`${BASE}${path}`, { + ...init, + headers: { + "Content-Type": "application/json", + ...init?.headers, + }, + }) + if (!res.ok) { + const text = await res.text().catch(() => res.statusText) + throw new Error(`${res.status}: ${text}`) + } + if (res.status === 204) return undefined as T + const text = await res.text() + if (!text) return null as T + return JSON.parse(text) +} + +export const api = { + get: (path: string) => request(path), + post: (path: string, body: unknown) => + request(path, { method: "POST", body: JSON.stringify(body) }), + put: (path: string, body: unknown) => + request(path, { method: "PUT", body: JSON.stringify(body) }), + delete: (path: string) => request(path, { method: "DELETE" }), +} diff --git a/spa/src/api/data-sources.ts b/spa/src/api/data-sources.ts new file mode 100644 index 0000000..834aeb2 --- /dev/null +++ b/spa/src/api/data-sources.ts @@ -0,0 +1,50 @@ +import { useMutation, useQuery, useQueryClient } from "@tanstack/react-query" +import { api } from "./client" +import type { DataSource } from "./types" + +const KEYS = { + all: ["data-sources"] as const, + one: (id: number) => ["data-sources", id] as const, +} + +export function useDataSources() { + return useQuery({ + queryKey: KEYS.all, + queryFn: () => api.get("/data-sources"), + }) +} + +export function useDataSource(id: number) { + return useQuery({ + queryKey: KEYS.one(id), + queryFn: () => api.get(`/data-sources/${id}`), + }) +} + +export function useCreateDataSource() { + const qc = useQueryClient() + return useMutation({ + mutationFn: (ds: DataSource) => api.post("/data-sources", ds), + onSuccess: () => qc.invalidateQueries({ queryKey: KEYS.all }), + }) +} + +export function useUpdateDataSource() { + const qc = useQueryClient() + return useMutation({ + mutationFn: (ds: DataSource) => + api.put(`/data-sources/${ds.id}`, ds), + onSuccess: (_, ds) => { + qc.invalidateQueries({ queryKey: KEYS.all }) + qc.invalidateQueries({ queryKey: KEYS.one(ds.id) }) + }, + }) +} + +export function useDeleteDataSource() { + const qc = useQueryClient() + return useMutation({ + mutationFn: (id: number) => api.delete(`/data-sources/${id}`), + onSuccess: () => qc.invalidateQueries({ queryKey: KEYS.all }), + }) +} diff --git a/spa/src/api/layout.ts b/spa/src/api/layout.ts new file mode 100644 index 0000000..37083b4 --- /dev/null +++ b/spa/src/api/layout.ts @@ -0,0 +1,22 @@ +import { useMutation, useQuery, useQueryClient } from "@tanstack/react-query" +import { api } from "./client" +import type { Layout } from "./types" + +const KEYS = { + current: ["layout"] as const, +} + +export function useLayout() { + return useQuery({ + queryKey: KEYS.current, + queryFn: () => api.get("/layout"), + }) +} + +export function useUpdateLayout() { + const qc = useQueryClient() + return useMutation({ + mutationFn: (layout: Layout) => api.put("/layout", layout), + onSuccess: () => qc.invalidateQueries({ queryKey: KEYS.current }), + }) +} diff --git a/spa/src/api/presets.ts b/spa/src/api/presets.ts new file mode 100644 index 0000000..9175dbd --- /dev/null +++ b/spa/src/api/presets.ts @@ -0,0 +1,39 @@ +import { useMutation, useQuery, useQueryClient } from "@tanstack/react-query" +import { api } from "./client" +import type { Preset } from "./types" + +const KEYS = { + all: ["presets"] as const, + one: (id: number) => ["presets", id] as const, +} + +export function usePresets() { + return useQuery({ + queryKey: KEYS.all, + queryFn: () => api.get("/presets"), + }) +} + +export function useCreatePreset() { + const qc = useQueryClient() + return useMutation({ + mutationFn: (p: Preset) => api.post("/presets", p), + onSuccess: () => qc.invalidateQueries({ queryKey: KEYS.all }), + }) +} + +export function useDeletePreset() { + const qc = useQueryClient() + return useMutation({ + mutationFn: (id: number) => api.delete(`/presets/${id}`), + onSuccess: () => qc.invalidateQueries({ queryKey: KEYS.all }), + }) +} + +export function useLoadPreset() { + const qc = useQueryClient() + return useMutation({ + mutationFn: (id: number) => api.post(`/presets/${id}/load`, {}), + onSuccess: () => qc.invalidateQueries({ queryKey: ["layout"] }), + }) +} diff --git a/spa/src/api/types.ts b/spa/src/api/types.ts new file mode 100644 index 0000000..573aa97 --- /dev/null +++ b/spa/src/api/types.ts @@ -0,0 +1,59 @@ +export type DisplayHint = "icon_value" | "text_block" | "key_value" +export type SourceType = "weather" | "media" | "rss" | "http_json" | "webhook" +export type SizingType = "fixed" | "flex" +export type Direction = "row" | "column" + +export interface KeyMapping { + source_path: string + target_key: string +} + +export interface Widget { + id: number + name: string + display_hint: DisplayHint + data_source_id: number + mappings: KeyMapping[] + max_data_size: number +} + +export type CreateWidget = Widget + +export interface DataSource { + id: number + name: string + source_type: SourceType + poll_interval_secs: number + url: string | null + api_key: string | null + headers: [string, string][] +} + +export interface Sizing { + type: SizingType + value: number +} + +export interface LayoutNode { + type: "leaf" | "container" + widget_id?: number + direction?: Direction + gap?: number + padding?: number + children?: LayoutChild[] +} + +export interface LayoutChild { + sizing: Sizing + node: LayoutNode +} + +export interface Layout { + root: LayoutNode +} + +export interface Preset { + id: number + name: string + layout: Layout +} diff --git a/spa/src/api/widgets.ts b/spa/src/api/widgets.ts new file mode 100644 index 0000000..283afaa --- /dev/null +++ b/spa/src/api/widgets.ts @@ -0,0 +1,49 @@ +import { useMutation, useQuery, useQueryClient } from "@tanstack/react-query" +import { api } from "./client" +import type { Widget } from "./types" + +const KEYS = { + all: ["widgets"] as const, + one: (id: number) => ["widgets", id] as const, +} + +export function useWidgets() { + return useQuery({ + queryKey: KEYS.all, + queryFn: () => api.get("/widgets"), + }) +} + +export function useWidget(id: number) { + return useQuery({ + queryKey: KEYS.one(id), + queryFn: () => api.get(`/widgets/${id}`), + }) +} + +export function useCreateWidget() { + const qc = useQueryClient() + return useMutation({ + mutationFn: (w: Widget) => api.post("/widgets", w), + onSuccess: () => qc.invalidateQueries({ queryKey: KEYS.all }), + }) +} + +export function useUpdateWidget() { + const qc = useQueryClient() + return useMutation({ + mutationFn: (w: Widget) => api.put(`/widgets/${w.id}`, w), + onSuccess: (_, w) => { + qc.invalidateQueries({ queryKey: KEYS.all }) + qc.invalidateQueries({ queryKey: KEYS.one(w.id) }) + }, + }) +} + +export function useDeleteWidget() { + const qc = useQueryClient() + return useMutation({ + mutationFn: (id: number) => api.delete(`/widgets/${id}`), + onSuccess: () => qc.invalidateQueries({ queryKey: KEYS.all }), + }) +} diff --git a/spa/src/assets/react.svg b/spa/src/assets/react.svg new file mode 100644 index 0000000..6c87de9 --- /dev/null +++ b/spa/src/assets/react.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/spa/src/components/app-shell.tsx b/spa/src/components/app-shell.tsx new file mode 100644 index 0000000..7d52b51 --- /dev/null +++ b/spa/src/components/app-shell.tsx @@ -0,0 +1,78 @@ +import { Link, useRouterState } from "@tanstack/react-router" +import { + SidebarProvider, + Sidebar, + SidebarContent, + SidebarHeader, + SidebarMenu, + SidebarMenuItem, + SidebarMenuButton, + SidebarInset, + SidebarTrigger, +} from "@/components/ui/sidebar" +import { Separator } from "@/components/ui/separator" +import { Toaster } from "@/components/ui/sonner" +import { + LayoutDashboard, + Database, + Box, + Layers, + Save, +} from "lucide-react" + +const NAV = [ + { to: "/", label: "Dashboard", icon: LayoutDashboard }, + { to: "/data-sources", label: "Data Sources", icon: Database }, + { to: "/widgets", label: "Widgets", icon: Box }, + { to: "/layout", label: "Layout", icon: Layers }, + { to: "/presets", label: "Presets", icon: Save }, +] as const + +export function AppShell({ children }: { children: React.ReactNode }) { + const { location } = useRouterState() + + return ( + + + + K-Frame + + + + {NAV.map((item) => { + const active = + item.to === "/" + ? location.pathname === "/" + : location.pathname.startsWith(item.to) + return ( + + + + + {item.label} + + + + ) + })} + + + + +
+ + + + {NAV.find((n) => + n.to === "/" + ? location.pathname === "/" + : location.pathname.startsWith(n.to), + )?.label ?? ""} + +
+
{children}
+
+ +
+ ) +} diff --git a/spa/src/components/theme-provider.tsx b/spa/src/components/theme-provider.tsx new file mode 100644 index 0000000..1349a0c --- /dev/null +++ b/spa/src/components/theme-provider.tsx @@ -0,0 +1,230 @@ +/* eslint-disable react-refresh/only-export-components */ +import * as React from "react" + +type Theme = "dark" | "light" | "system" +type ResolvedTheme = "dark" | "light" + +type ThemeProviderProps = { + children: React.ReactNode + defaultTheme?: Theme + storageKey?: string + disableTransitionOnChange?: boolean +} + +type ThemeProviderState = { + theme: Theme + setTheme: (theme: Theme) => void +} + +const COLOR_SCHEME_QUERY = "(prefers-color-scheme: dark)" +const THEME_VALUES: Theme[] = ["dark", "light", "system"] + +const ThemeProviderContext = React.createContext< + ThemeProviderState | undefined +>(undefined) + +function isTheme(value: string | null): value is Theme { + if (value === null) { + return false + } + + return THEME_VALUES.includes(value as Theme) +} + +function getSystemTheme(): ResolvedTheme { + if (window.matchMedia(COLOR_SCHEME_QUERY).matches) { + return "dark" + } + + return "light" +} + +function disableTransitionsTemporarily() { + const style = document.createElement("style") + style.appendChild( + document.createTextNode( + "*,*::before,*::after{-webkit-transition:none!important;transition:none!important}" + ) + ) + document.head.appendChild(style) + + return () => { + window.getComputedStyle(document.body) + requestAnimationFrame(() => { + requestAnimationFrame(() => { + style.remove() + }) + }) + } +} + +function isEditableTarget(target: EventTarget | null) { + if (!(target instanceof HTMLElement)) { + return false + } + + if (target.isContentEditable) { + return true + } + + const editableParent = target.closest( + "input, textarea, select, [contenteditable='true']" + ) + if (editableParent) { + return true + } + + return false +} + +export function ThemeProvider({ + children, + defaultTheme = "system", + storageKey = "theme", + disableTransitionOnChange = true, + ...props +}: ThemeProviderProps) { + const [theme, setThemeState] = React.useState(() => { + const storedTheme = localStorage.getItem(storageKey) + if (isTheme(storedTheme)) { + return storedTheme + } + + return defaultTheme + }) + + const setTheme = React.useCallback( + (nextTheme: Theme) => { + localStorage.setItem(storageKey, nextTheme) + setThemeState(nextTheme) + }, + [storageKey] + ) + + const applyTheme = React.useCallback( + (nextTheme: Theme) => { + const root = document.documentElement + const resolvedTheme = + nextTheme === "system" ? getSystemTheme() : nextTheme + const restoreTransitions = disableTransitionOnChange + ? disableTransitionsTemporarily() + : null + + root.classList.remove("light", "dark") + root.classList.add(resolvedTheme) + + if (restoreTransitions) { + restoreTransitions() + } + }, + [disableTransitionOnChange] + ) + + React.useEffect(() => { + applyTheme(theme) + + if (theme !== "system") { + return undefined + } + + const mediaQuery = window.matchMedia(COLOR_SCHEME_QUERY) + const handleChange = () => { + applyTheme("system") + } + + mediaQuery.addEventListener("change", handleChange) + + return () => { + mediaQuery.removeEventListener("change", handleChange) + } + }, [theme, applyTheme]) + + React.useEffect(() => { + const handleKeyDown = (event: KeyboardEvent) => { + if (event.repeat) { + return + } + + if (event.metaKey || event.ctrlKey || event.altKey) { + return + } + + if (isEditableTarget(event.target)) { + return + } + + if (event.key.toLowerCase() !== "d") { + return + } + + setThemeState((currentTheme) => { + const nextTheme = + currentTheme === "dark" + ? "light" + : currentTheme === "light" + ? "dark" + : getSystemTheme() === "dark" + ? "light" + : "dark" + + localStorage.setItem(storageKey, nextTheme) + return nextTheme + }) + } + + window.addEventListener("keydown", handleKeyDown) + + return () => { + window.removeEventListener("keydown", handleKeyDown) + } + }, [storageKey]) + + React.useEffect(() => { + const handleStorageChange = (event: StorageEvent) => { + if (event.storageArea !== localStorage) { + return + } + + if (event.key !== storageKey) { + return + } + + if (isTheme(event.newValue)) { + setThemeState(event.newValue) + return + } + + setThemeState(defaultTheme) + } + + window.addEventListener("storage", handleStorageChange) + + return () => { + window.removeEventListener("storage", handleStorageChange) + } + }, [defaultTheme, storageKey]) + + const value = React.useMemo( + () => ({ + theme, + setTheme, + }), + [theme, setTheme] + ) + + return ( + + {children} + + ) +} + +export const useTheme = () => { + const context = React.useContext(ThemeProviderContext) + + if (context === undefined) { + throw new Error("useTheme must be used within a ThemeProvider") + } + + return context +} diff --git a/spa/src/components/ui/accordion.tsx b/spa/src/components/ui/accordion.tsx new file mode 100644 index 0000000..fcfee5c --- /dev/null +++ b/spa/src/components/ui/accordion.tsx @@ -0,0 +1,79 @@ +import * as React from "react" +import { Accordion as AccordionPrimitive } from "radix-ui" + +import { cn } from "@/lib/utils" +import { ChevronDownIcon, ChevronUpIcon } from "lucide-react" + +function Accordion({ + className, + ...props +}: React.ComponentProps) { + return ( + + ) +} + +function AccordionItem({ + className, + ...props +}: React.ComponentProps) { + return ( + + ) +} + +function AccordionTrigger({ + className, + children, + ...props +}: React.ComponentProps) { + return ( + + + {children} + + + + + ) +} + +function AccordionContent({ + className, + children, + ...props +}: React.ComponentProps) { + return ( + +
+ {children} +
+
+ ) +} + +export { Accordion, AccordionItem, AccordionTrigger, AccordionContent } diff --git a/spa/src/components/ui/alert-dialog.tsx b/spa/src/components/ui/alert-dialog.tsx new file mode 100644 index 0000000..b8171d5 --- /dev/null +++ b/spa/src/components/ui/alert-dialog.tsx @@ -0,0 +1,197 @@ +import * as React from "react" +import { AlertDialog as AlertDialogPrimitive } from "radix-ui" + +import { cn } from "@/lib/utils" +import { Button } from "@/components/ui/button" + +function AlertDialog({ + ...props +}: React.ComponentProps) { + return +} + +function AlertDialogTrigger({ + ...props +}: React.ComponentProps) { + return ( + + ) +} + +function AlertDialogPortal({ + ...props +}: React.ComponentProps) { + return ( + + ) +} + +function AlertDialogOverlay({ + className, + ...props +}: React.ComponentProps) { + return ( + + ) +} + +function AlertDialogContent({ + className, + size = "default", + ...props +}: React.ComponentProps & { + size?: "default" | "sm" +}) { + return ( + + + + + ) +} + +function AlertDialogHeader({ + className, + ...props +}: React.ComponentProps<"div">) { + return ( +
+ ) +} + +function AlertDialogFooter({ + className, + ...props +}: React.ComponentProps<"div">) { + return ( +
+ ) +} + +function AlertDialogMedia({ + className, + ...props +}: React.ComponentProps<"div">) { + return ( +
+ ) +} + +function AlertDialogTitle({ + className, + ...props +}: React.ComponentProps) { + return ( + + ) +} + +function AlertDialogDescription({ + className, + ...props +}: React.ComponentProps) { + return ( + + ) +} + +function AlertDialogAction({ + className, + variant = "default", + size = "default", + ...props +}: React.ComponentProps & + Pick, "variant" | "size">) { + return ( + + ) +} + +function AlertDialogCancel({ + className, + variant = "outline", + size = "default", + ...props +}: React.ComponentProps & + Pick, "variant" | "size">) { + return ( + + ) +} + +export { + AlertDialog, + AlertDialogAction, + AlertDialogCancel, + AlertDialogContent, + AlertDialogDescription, + AlertDialogFooter, + AlertDialogHeader, + AlertDialogMedia, + AlertDialogOverlay, + AlertDialogPortal, + AlertDialogTitle, + AlertDialogTrigger, +} diff --git a/spa/src/components/ui/alert.tsx b/spa/src/components/ui/alert.tsx new file mode 100644 index 0000000..1fe3176 --- /dev/null +++ b/spa/src/components/ui/alert.tsx @@ -0,0 +1,76 @@ +import * as React from "react" +import { cva, type VariantProps } from "class-variance-authority" + +import { cn } from "@/lib/utils" + +const alertVariants = cva( + "group/alert relative grid w-full gap-0.5 rounded-lg border px-2.5 py-2 text-left text-sm has-data-[slot=alert-action]:relative has-data-[slot=alert-action]:pr-18 has-[>svg]:grid-cols-[auto_1fr] has-[>svg]:gap-x-2 *:[svg]:row-span-2 *:[svg]:translate-y-0.5 *:[svg]:text-current *:[svg:not([class*='size-'])]:size-4", + { + variants: { + variant: { + default: "bg-card text-card-foreground", + destructive: + "bg-card text-destructive *:data-[slot=alert-description]:text-destructive/90 *:[svg]:text-current", + }, + }, + defaultVariants: { + variant: "default", + }, + } +) + +function Alert({ + className, + variant, + ...props +}: React.ComponentProps<"div"> & VariantProps) { + return ( +
+ ) +} + +function AlertTitle({ className, ...props }: React.ComponentProps<"div">) { + return ( +
svg]/alert:col-start-2 [&_a]:underline [&_a]:underline-offset-3 [&_a]:hover:text-foreground", + className + )} + {...props} + /> + ) +} + +function AlertDescription({ + className, + ...props +}: React.ComponentProps<"div">) { + return ( +
+ ) +} + +function AlertAction({ className, ...props }: React.ComponentProps<"div">) { + return ( +
+ ) +} + +export { Alert, AlertTitle, AlertDescription, AlertAction } diff --git a/spa/src/components/ui/aspect-ratio.tsx b/spa/src/components/ui/aspect-ratio.tsx new file mode 100644 index 0000000..57e38fa --- /dev/null +++ b/spa/src/components/ui/aspect-ratio.tsx @@ -0,0 +1,11 @@ +"use client" + +import { AspectRatio as AspectRatioPrimitive } from "radix-ui" + +function AspectRatio({ + ...props +}: React.ComponentProps) { + return +} + +export { AspectRatio } diff --git a/spa/src/components/ui/avatar.tsx b/spa/src/components/ui/avatar.tsx new file mode 100644 index 0000000..99f3ed2 --- /dev/null +++ b/spa/src/components/ui/avatar.tsx @@ -0,0 +1,110 @@ +import * as React from "react" +import { Avatar as AvatarPrimitive } from "radix-ui" + +import { cn } from "@/lib/utils" + +function Avatar({ + className, + size = "default", + ...props +}: React.ComponentProps & { + size?: "default" | "sm" | "lg" +}) { + return ( + + ) +} + +function AvatarImage({ + className, + ...props +}: React.ComponentProps) { + return ( + + ) +} + +function AvatarFallback({ + className, + ...props +}: React.ComponentProps) { + return ( + + ) +} + +function AvatarBadge({ className, ...props }: React.ComponentProps<"span">) { + return ( + svg]:hidden", + "group-data-[size=default]/avatar:size-2.5 group-data-[size=default]/avatar:[&>svg]:size-2", + "group-data-[size=lg]/avatar:size-3 group-data-[size=lg]/avatar:[&>svg]:size-2", + className + )} + {...props} + /> + ) +} + +function AvatarGroup({ className, ...props }: React.ComponentProps<"div">) { + return ( +
+ ) +} + +function AvatarGroupCount({ + className, + ...props +}: React.ComponentProps<"div">) { + return ( +
svg]:size-4 group-has-data-[size=lg]/avatar-group:[&>svg]:size-5 group-has-data-[size=sm]/avatar-group:[&>svg]:size-3", + className + )} + {...props} + /> + ) +} + +export { + Avatar, + AvatarImage, + AvatarFallback, + AvatarGroup, + AvatarGroupCount, + AvatarBadge, +} diff --git a/spa/src/components/ui/badge.tsx b/spa/src/components/ui/badge.tsx new file mode 100644 index 0000000..cacff11 --- /dev/null +++ b/spa/src/components/ui/badge.tsx @@ -0,0 +1,49 @@ +import * as React from "react" +import { cva, type VariantProps } from "class-variance-authority" +import { Slot } from "radix-ui" + +import { cn } from "@/lib/utils" + +const badgeVariants = cva( + "group/badge inline-flex h-5 w-fit shrink-0 items-center justify-center gap-1 overflow-hidden rounded-4xl border border-transparent px-2 py-0.5 text-xs font-medium whitespace-nowrap transition-all focus-visible:border-ring focus-visible:ring-[3px] focus-visible:ring-ring/50 has-data-[icon=inline-end]:pr-1.5 has-data-[icon=inline-start]:pl-1.5 aria-invalid:border-destructive aria-invalid:ring-destructive/20 dark:aria-invalid:ring-destructive/40 [&>svg]:pointer-events-none [&>svg]:size-3!", + { + variants: { + variant: { + default: "bg-primary text-primary-foreground [a]:hover:bg-primary/80", + secondary: + "bg-secondary text-secondary-foreground [a]:hover:bg-secondary/80", + destructive: + "bg-destructive/10 text-destructive focus-visible:ring-destructive/20 dark:bg-destructive/20 dark:focus-visible:ring-destructive/40 [a]:hover:bg-destructive/20", + outline: + "border-border text-foreground [a]:hover:bg-muted [a]:hover:text-muted-foreground", + ghost: + "hover:bg-muted hover:text-muted-foreground dark:hover:bg-muted/50", + link: "text-primary underline-offset-4 hover:underline", + }, + }, + defaultVariants: { + variant: "default", + }, + } +) + +function Badge({ + className, + variant = "default", + asChild = false, + ...props +}: React.ComponentProps<"span"> & + VariantProps & { asChild?: boolean }) { + const Comp = asChild ? Slot.Root : "span" + + return ( + + ) +} + +export { Badge, badgeVariants } diff --git a/spa/src/components/ui/breadcrumb.tsx b/spa/src/components/ui/breadcrumb.tsx new file mode 100644 index 0000000..db9afc0 --- /dev/null +++ b/spa/src/components/ui/breadcrumb.tsx @@ -0,0 +1,122 @@ +import * as React from "react" +import { Slot } from "radix-ui" + +import { cn } from "@/lib/utils" +import { ChevronRightIcon, MoreHorizontalIcon } from "lucide-react" + +function Breadcrumb({ className, ...props }: React.ComponentProps<"nav">) { + return ( +