From 6f5b862234ff1c8d199170487acb64fef468f912 Mon Sep 17 00:00:00 2001 From: lashman Date: Sat, 14 Mar 2026 19:04:35 +0200 Subject: [PATCH] pipeline cards, context menus, presets, settings overhaul rewrote pipeline as draggable card strip with per-rule config popovers, added right-click menus to pipeline cards, sidebar tree, and file list, preset import/export with BRU format support, new rules (hash, swap, truncate, sanitize, padding, randomize, text editor, folder name, transliterate), settings dialog with all sections, overlay collision containment, tooltips on icon buttons, empty pipeline default --- Cargo.lock | 437 +- crates/nomina-app/Cargo.toml | 2 + crates/nomina-app/capabilities/default.json | 15 +- .../nomina-app/gen/schemas/capabilities.json | 2 +- .../nomina-app/src/commands/context_menu.rs | 128 + crates/nomina-app/src/commands/files.rs | 52 + crates/nomina-app/src/commands/mod.rs | 2 + crates/nomina-app/src/commands/presets.rs | 24 + crates/nomina-app/src/commands/rename.rs | 139 +- crates/nomina-app/src/commands/updates.rs | 75 + crates/nomina-app/src/main.rs | 11 +- crates/nomina-app/tauri.conf.json | 9 +- crates/nomina-core/Cargo.toml | 8 +- crates/nomina-core/src/bru.rs | 208 + crates/nomina-core/src/lib.rs | 3 + crates/nomina-core/src/pipeline.rs | 47 +- crates/nomina-core/src/rules/add.rs | 103 +- crates/nomina-core/src/rules/case.rs | 178 + crates/nomina-core/src/rules/date.rs | 2 +- crates/nomina-core/src/rules/extension.rs | 15 + crates/nomina-core/src/rules/folder_name.rs | 127 + crates/nomina-core/src/rules/hash.rs | 142 + crates/nomina-core/src/rules/mod.rs | 20 +- crates/nomina-core/src/rules/move_parts.rs | 212 +- crates/nomina-core/src/rules/numbering.rs | 98 +- crates/nomina-core/src/rules/padding.rs | 124 + crates/nomina-core/src/rules/randomize.rs | 123 + crates/nomina-core/src/rules/regex.rs | 20 +- crates/nomina-core/src/rules/remove.rs | 297 +- crates/nomina-core/src/rules/replace.rs | 310 +- crates/nomina-core/src/rules/sanitize.rs | 324 + crates/nomina-core/src/rules/swap.rs | 116 + crates/nomina-core/src/rules/text_editor.rs | 82 + crates/nomina-core/src/rules/transliterate.rs | 99 + crates/nomina-core/src/rules/truncate.rs | 110 + crates/nomina-core/src/scanner.rs | 15 +- crates/nomina-core/src/undo.rs | 8 +- ui/components.json | 25 + ui/package-lock.json | 5778 ++++++++++++++++- ui/package.json | 18 + ui/src/App.tsx | 15 +- ui/src/components/browser/FileList.tsx | 951 ++- ui/src/components/layout/AppShell.tsx | 155 +- ui/src/components/layout/ResizeEdges.tsx | 95 + ui/src/components/layout/Sidebar.tsx | 571 +- ui/src/components/layout/StatusBar.tsx | 102 +- ui/src/components/layout/Toolbar.tsx | 255 +- ui/src/components/pipeline/PipelineStrip.tsx | 694 ++ .../components/pipeline/configs/AddConfig.tsx | 95 + .../pipeline/configs/CaseConfig.tsx | 65 + .../pipeline/configs/DateConfig.tsx | 86 + .../pipeline/configs/ExtensionConfig.tsx | 98 + .../pipeline/configs/FolderNameConfig.tsx | 38 + .../pipeline/configs/HashConfig.tsx | 53 + .../pipeline/configs/MovePartsConfig.tsx | 83 + .../pipeline/configs/NumberingConfig.tsx | 80 + .../pipeline/configs/PaddingConfig.tsx | 36 + .../pipeline/configs/RandomizeConfig.tsx | 52 + .../pipeline/configs/RegexConfig.tsx | 47 + .../pipeline/configs/RemoveConfig.tsx | 113 + .../pipeline/configs/ReplaceConfig.tsx | 65 + .../pipeline/configs/SanitizeConfig.tsx | 53 + .../pipeline/configs/SwapConfig.tsx | 45 + .../pipeline/configs/TextEditorConfig.tsx | 85 + .../pipeline/configs/TransliterateConfig.tsx | 10 + .../pipeline/configs/TruncateConfig.tsx | 41 + ui/src/components/presets/PresetsDialog.tsx | 306 + ui/src/components/rules/AddTab.tsx | 102 - ui/src/components/rules/CaseTab.tsx | 69 - ui/src/components/rules/ExtensionTab.tsx | 71 - ui/src/components/rules/NumberingTab.tsx | 136 - ui/src/components/rules/RegexTab.tsx | 74 - ui/src/components/rules/RemoveTab.tsx | 123 - ui/src/components/rules/ReplaceTab.tsx | 82 - ui/src/components/rules/RulePanel.tsx | 113 - ui/src/components/settings/SettingsDialog.tsx | 1132 ++++ ui/src/components/ui/badge.tsx | 49 + ui/src/components/ui/button.tsx | 67 + ui/src/components/ui/checkbox.tsx | 30 + ui/src/components/ui/context-menu.tsx | 266 + ui/src/components/ui/dialog.tsx | 166 + ui/src/components/ui/dropdown-menu.tsx | 272 + ui/src/components/ui/input.tsx | 19 + ui/src/components/ui/number-input.tsx | 196 + ui/src/components/ui/popover.tsx | 106 + ui/src/components/ui/scroll-area.tsx | 55 + ui/src/components/ui/segmented-control.tsx | 71 + ui/src/components/ui/separator.tsx | 28 + ui/src/components/ui/sonner.tsx | 20 + ui/src/components/ui/switch.tsx | 31 + ui/src/components/ui/tooltip.tsx | 57 + ui/src/hooks/useAnnounce.ts | 19 + ui/src/hooks/useDebounce.ts | 12 - ui/src/hooks/useKeyboardShortcuts.ts | 14 +- ui/src/hooks/useTheme.ts | 109 + ui/src/hooks/useWindowState.ts | 123 + ui/src/index.css | 471 +- ui/src/lib/portal.ts | 6 + ui/src/lib/utils.ts | 6 + ui/src/stores/fileStore.ts | 20 +- ui/src/stores/ruleStore.ts | 185 +- ui/src/stores/settingsStore.ts | 249 +- ui/src/types/rules.ts | 267 +- ui/tsconfig.json | 6 +- ui/vite.config.ts | 7 + 105 files changed, 17257 insertions(+), 1369 deletions(-) create mode 100644 crates/nomina-app/src/commands/context_menu.rs create mode 100644 crates/nomina-app/src/commands/updates.rs create mode 100644 crates/nomina-core/src/bru.rs create mode 100644 crates/nomina-core/src/rules/folder_name.rs create mode 100644 crates/nomina-core/src/rules/hash.rs create mode 100644 crates/nomina-core/src/rules/padding.rs create mode 100644 crates/nomina-core/src/rules/randomize.rs create mode 100644 crates/nomina-core/src/rules/sanitize.rs create mode 100644 crates/nomina-core/src/rules/swap.rs create mode 100644 crates/nomina-core/src/rules/text_editor.rs create mode 100644 crates/nomina-core/src/rules/transliterate.rs create mode 100644 crates/nomina-core/src/rules/truncate.rs create mode 100644 ui/components.json create mode 100644 ui/src/components/layout/ResizeEdges.tsx create mode 100644 ui/src/components/pipeline/PipelineStrip.tsx create mode 100644 ui/src/components/pipeline/configs/AddConfig.tsx create mode 100644 ui/src/components/pipeline/configs/CaseConfig.tsx create mode 100644 ui/src/components/pipeline/configs/DateConfig.tsx create mode 100644 ui/src/components/pipeline/configs/ExtensionConfig.tsx create mode 100644 ui/src/components/pipeline/configs/FolderNameConfig.tsx create mode 100644 ui/src/components/pipeline/configs/HashConfig.tsx create mode 100644 ui/src/components/pipeline/configs/MovePartsConfig.tsx create mode 100644 ui/src/components/pipeline/configs/NumberingConfig.tsx create mode 100644 ui/src/components/pipeline/configs/PaddingConfig.tsx create mode 100644 ui/src/components/pipeline/configs/RandomizeConfig.tsx create mode 100644 ui/src/components/pipeline/configs/RegexConfig.tsx create mode 100644 ui/src/components/pipeline/configs/RemoveConfig.tsx create mode 100644 ui/src/components/pipeline/configs/ReplaceConfig.tsx create mode 100644 ui/src/components/pipeline/configs/SanitizeConfig.tsx create mode 100644 ui/src/components/pipeline/configs/SwapConfig.tsx create mode 100644 ui/src/components/pipeline/configs/TextEditorConfig.tsx create mode 100644 ui/src/components/pipeline/configs/TransliterateConfig.tsx create mode 100644 ui/src/components/pipeline/configs/TruncateConfig.tsx create mode 100644 ui/src/components/presets/PresetsDialog.tsx delete mode 100644 ui/src/components/rules/AddTab.tsx delete mode 100644 ui/src/components/rules/CaseTab.tsx delete mode 100644 ui/src/components/rules/ExtensionTab.tsx delete mode 100644 ui/src/components/rules/NumberingTab.tsx delete mode 100644 ui/src/components/rules/RegexTab.tsx delete mode 100644 ui/src/components/rules/RemoveTab.tsx delete mode 100644 ui/src/components/rules/ReplaceTab.tsx delete mode 100644 ui/src/components/rules/RulePanel.tsx create mode 100644 ui/src/components/settings/SettingsDialog.tsx create mode 100644 ui/src/components/ui/badge.tsx create mode 100644 ui/src/components/ui/button.tsx create mode 100644 ui/src/components/ui/checkbox.tsx create mode 100644 ui/src/components/ui/context-menu.tsx create mode 100644 ui/src/components/ui/dialog.tsx create mode 100644 ui/src/components/ui/dropdown-menu.tsx create mode 100644 ui/src/components/ui/input.tsx create mode 100644 ui/src/components/ui/number-input.tsx create mode 100644 ui/src/components/ui/popover.tsx create mode 100644 ui/src/components/ui/scroll-area.tsx create mode 100644 ui/src/components/ui/segmented-control.tsx create mode 100644 ui/src/components/ui/separator.tsx create mode 100644 ui/src/components/ui/sonner.tsx create mode 100644 ui/src/components/ui/switch.tsx create mode 100644 ui/src/components/ui/tooltip.tsx create mode 100644 ui/src/hooks/useAnnounce.ts delete mode 100644 ui/src/hooks/useDebounce.ts create mode 100644 ui/src/hooks/useTheme.ts create mode 100644 ui/src/hooks/useWindowState.ts create mode 100644 ui/src/lib/portal.ts create mode 100644 ui/src/lib/utils.ts diff --git a/Cargo.lock b/Cargo.lock index 464c48c..8787864 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -396,6 +396,16 @@ dependencies = [ "version_check", ] +[[package]] +name = "core-foundation" +version = "0.9.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "91e195e091a93c46f7102ec7818a2aa394e1e1771c3ab4825963fa03e45afb8f" +dependencies = [ + "core-foundation-sys", + "libc", +] + [[package]] name = "core-foundation" version = "0.10.1" @@ -419,9 +429,9 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "064badf302c3194842cf2c5d61f56cc88e54a759313879cdf03abdd27d0c3b97" dependencies = [ "bitflags 2.11.0", - "core-foundation", + "core-foundation 0.10.1", "core-graphics-types", - "foreign-types", + "foreign-types 0.5.0", "libc", ] @@ -432,7 +442,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "3d44a101f213f6c4cdc1853d4b78aef6db6bdfa3468798cc1d9912f4735013eb" dependencies = [ "bitflags 2.11.0", - "core-foundation", + "core-foundation 0.10.1", "libc", ] @@ -626,6 +636,12 @@ dependencies = [ "syn 2.0.117", ] +[[package]] +name = "deunicode" +version = "1.6.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "abd57806937c9cc163efc8ea3910e00a62e2aeb0b8119f1793a978088f8f6b04" + [[package]] name = "digest" version = "0.10.7" @@ -918,6 +934,15 @@ version = "0.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "77ce24cb58228fbb8aa041425bb1050850ac19177686ea6e0f41a70416f56fdb" +[[package]] +name = "foreign-types" +version = "0.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f6f339eb8adc052cd2ca78910fda869aefa38d22d5cb648e6485e4d3fc06f3b1" +dependencies = [ + "foreign-types-shared 0.1.1", +] + [[package]] name = "foreign-types" version = "0.5.0" @@ -925,7 +950,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d737d9aa519fb7b749cbc3b962edcf310a8dd1f4b67c91c4f83975dbdd17d965" dependencies = [ "foreign-types-macros", - "foreign-types-shared", + "foreign-types-shared 0.3.1", ] [[package]] @@ -939,6 +964,12 @@ dependencies = [ "syn 2.0.117", ] +[[package]] +name = "foreign-types-shared" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "00b0228411908ca8685dba7fc2cdd70ec9990a6e753e89b6ac91a84c40fbaf4b" + [[package]] name = "foreign-types-shared" version = "0.3.1" @@ -1348,6 +1379,25 @@ dependencies = [ "syn 2.0.117", ] +[[package]] +name = "h2" +version = "0.4.13" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2f44da3a8150a6703ed5d34e164b875fd14c2cdab9af1252a9a1020bde2bdc54" +dependencies = [ + "atomic-waker", + "bytes", + "fnv", + "futures-core", + "futures-sink", + "http", + "indexmap 2.13.0", + "slab", + "tokio", + "tokio-util", + "tracing", +] + [[package]] name = "hashbrown" version = "0.12.3" @@ -1458,6 +1508,7 @@ dependencies = [ "bytes", "futures-channel", "futures-core", + "h2", "http", "http-body", "httparse", @@ -1469,6 +1520,38 @@ dependencies = [ "want", ] +[[package]] +name = "hyper-rustls" +version = "0.27.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e3c93eb611681b207e1fe55d5a71ecf91572ec8a6705cdb6857f7d8d5242cf58" +dependencies = [ + "http", + "hyper", + "hyper-util", + "rustls", + "rustls-pki-types", + "tokio", + "tokio-rustls", + "tower-service", +] + +[[package]] +name = "hyper-tls" +version = "0.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "70206fc6890eaca9fde8a0bf71caa2ddfc9fe045ac9e5c70df101a7dbde866e0" +dependencies = [ + "bytes", + "http-body-util", + "hyper", + "hyper-util", + "native-tls", + "tokio", + "tokio-native-tls", + "tower-service", +] + [[package]] name = "hyper-util" version = "0.1.20" @@ -1487,9 +1570,11 @@ dependencies = [ "percent-encoding", "pin-project-lite", "socket2", + "system-configuration", "tokio", "tower-service", "tracing", + "windows-registry", ] [[package]] @@ -1822,9 +1907,9 @@ dependencies = [ [[package]] name = "kamadak-exif" -version = "0.5.5" +version = "0.6.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ef4fc70d0ab7e5b6bafa30216a6b48705ea964cdfc29c050f2412295eba58077" +checksum = "1130d80c7374efad55a117d715a3af9368f0fa7a2c54573afc15a188cd984837" dependencies = [ "mutate_once", ] @@ -1985,6 +2070,16 @@ version = "0.1.10" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "2532096657941c2fea9c289d370a250971c689d4f143798ff67113ec042024a5" +[[package]] +name = "md-5" +version = "0.10.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d89e7ee0cfbedfc4da3340218492196241d89eefb6dab27de5df917a6d2e78cf" +dependencies = [ + "cfg-if", + "digest", +] + [[package]] name = "memchr" version = "2.8.0" @@ -2054,6 +2149,23 @@ version = "0.1.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "13d2233c9842d08cfe13f9eac96e207ca6a2ea10b80259ebe8ad0268be27d2af" +[[package]] +name = "native-tls" +version = "0.2.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "465500e14ea162429d264d44189adc38b199b62b1c21eea9f69e4b73cb03bbf2" +dependencies = [ + "libc", + "log", + "openssl", + "openssl-probe", + "openssl-sys", + "schannel", + "security-framework", + "security-framework-sys", + "tempfile", +] + [[package]] name = "natord" version = "1.0.9" @@ -2112,6 +2224,7 @@ dependencies = [ "env_logger", "log", "nomina-core", + "reqwest 0.12.28", "serde", "serde_json", "tauri", @@ -2120,6 +2233,7 @@ dependencies = [ "tauri-plugin-shell", "tokio", "uuid", + "winreg", ] [[package]] @@ -2127,17 +2241,23 @@ name = "nomina-core" version = "0.1.0" dependencies = [ "chrono", + "deunicode", "filetime", "glob", "kamadak-exif", "log", + "md-5", "natord", + "rand 0.8.5", "rayon", "regex", "serde", "serde_json", + "sha1", + "sha2", "tempfile", "thiserror 2.0.18", + "unicode-normalization", "uuid", "walkdir", ] @@ -2326,6 +2446,50 @@ dependencies = [ "pathdiff", ] +[[package]] +name = "openssl" +version = "0.10.76" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "951c002c75e16ea2c65b8c7e4d3d51d5530d8dfa7d060b4776828c88cfb18ecf" +dependencies = [ + "bitflags 2.11.0", + "cfg-if", + "foreign-types 0.3.2", + "libc", + "once_cell", + "openssl-macros", + "openssl-sys", +] + +[[package]] +name = "openssl-macros" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a948666b637a0f465e8564c73e89d4dde00d72d4d473cc972f390fc3dcee7d9c" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.117", +] + +[[package]] +name = "openssl-probe" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7c87def4c32ab89d880effc9e097653c8da5d6ef28e6b539d313baaacfbafcbe" + +[[package]] +name = "openssl-sys" +version = "0.9.112" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "57d55af3b3e226502be1526dfdba67ab0e9c96fc293004e79576b2b9edb0dbdb" +dependencies = [ + "cc", + "libc", + "pkg-config", + "vcpkg", +] + [[package]] name = "option-ext" version = "0.2.0" @@ -2977,6 +3141,46 @@ version = "0.8.10" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "dc897dd8d9e8bd1ed8cdad82b5966c3e0ecae09fb1907d58efaa013543185d0a" +[[package]] +name = "reqwest" +version = "0.12.28" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "eddd3ca559203180a307f12d114c268abf583f59b03cb906fd0b3ff8646c1147" +dependencies = [ + "base64 0.22.1", + "bytes", + "encoding_rs", + "futures-core", + "h2", + "http", + "http-body", + "http-body-util", + "hyper", + "hyper-rustls", + "hyper-tls", + "hyper-util", + "js-sys", + "log", + "mime", + "native-tls", + "percent-encoding", + "pin-project-lite", + "rustls-pki-types", + "serde", + "serde_json", + "serde_urlencoded", + "sync_wrapper", + "tokio", + "tokio-native-tls", + "tower", + "tower-http", + "tower-service", + "url", + "wasm-bindgen", + "wasm-bindgen-futures", + "web-sys", +] + [[package]] name = "reqwest" version = "0.13.2" @@ -3035,6 +3239,20 @@ dependencies = [ "windows-sys 0.60.2", ] +[[package]] +name = "ring" +version = "0.17.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a4689e6c2294d81e88dc6261c768b63bc4fcdb852be6d1352498b114f61383b7" +dependencies = [ + "cc", + "cfg-if", + "getrandom 0.2.17", + "libc", + "untrusted", + "windows-sys 0.52.0", +] + [[package]] name = "rustc-hash" version = "2.1.1" @@ -3063,12 +3281,51 @@ dependencies = [ "windows-sys 0.61.2", ] +[[package]] +name = "rustls" +version = "0.23.37" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "758025cb5fccfd3bc2fd74708fd4682be41d99e5dff73c377c0646c6012c73a4" +dependencies = [ + "once_cell", + "rustls-pki-types", + "rustls-webpki", + "subtle", + "zeroize", +] + +[[package]] +name = "rustls-pki-types" +version = "1.14.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "be040f8b0a225e40375822a563fa9524378b9d63112f53e19ffff34df5d33fdd" +dependencies = [ + "zeroize", +] + +[[package]] +name = "rustls-webpki" +version = "0.103.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d7df23109aa6c1567d1c575b9952556388da57401e4ace1d15f79eedad0d8f53" +dependencies = [ + "ring", + "rustls-pki-types", + "untrusted", +] + [[package]] name = "rustversion" version = "1.0.22" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b39cdef0fa800fc44525c84ccb54a029961a8215f9619753635a9c0d2538d46d" +[[package]] +name = "ryu" +version = "1.0.23" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9774ba4a74de5f7b1c1451ed6cd5285a32eddb5cccb8cc655a4e50009e06477f" + [[package]] name = "same-file" version = "1.0.6" @@ -3078,6 +3335,15 @@ dependencies = [ "winapi-util", ] +[[package]] +name = "schannel" +version = "0.1.29" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "91c1b7e4904c873ef0710c1f407dde2e6287de2bebc1bbbf7d430bb7cbffd939" +dependencies = [ + "windows-sys 0.61.2", +] + [[package]] name = "schemars" version = "0.8.22" @@ -3135,6 +3401,29 @@ version = "1.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "94143f37725109f92c262ed2cf5e59bce7498c01bcc1502d7b9afe439a4e9f49" +[[package]] +name = "security-framework" +version = "3.7.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b7f4bc775c73d9a02cde8bf7b2ec4c9d12743edf609006c7facc23998404cd1d" +dependencies = [ + "bitflags 2.11.0", + "core-foundation 0.10.1", + "core-foundation-sys", + "libc", + "security-framework-sys", +] + +[[package]] +name = "security-framework-sys" +version = "2.17.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6ce2691df843ecc5d231c0b14ece2acc3efb62c0a398c7e1d875f3983ce020e3" +dependencies = [ + "core-foundation-sys", + "libc", +] + [[package]] name = "selectors" version = "0.24.0" @@ -3277,6 +3566,18 @@ dependencies = [ "serde_core", ] +[[package]] +name = "serde_urlencoded" +version = "0.7.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d3491c14715ca2294c4d6a88f15e84739788c1d030eed8c110436aafdaa2f3fd" +dependencies = [ + "form_urlencoded", + "itoa", + "ryu", + "serde", +] + [[package]] name = "serde_with" version = "3.18.0" @@ -3349,6 +3650,17 @@ dependencies = [ "stable_deref_trait", ] +[[package]] +name = "sha1" +version = "0.10.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e3bf829a2d51ab4a5ddf1352d8470c140cadc8301b2ae1789db023f01cedd6ba" +dependencies = [ + "cfg-if", + "cpufeatures", + "digest", +] + [[package]] name = "sha2" version = "0.10.9" @@ -3557,6 +3869,12 @@ version = "0.11.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7da8b5736845d9f2fcb837ea5d9e2628564b3b043a70948a3f0b778838c5fb4f" +[[package]] +name = "subtle" +version = "2.6.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "13c2bddecc57b384dee18652358fb23172facb8a2c51ccc10d74c157bdea3292" + [[package]] name = "swift-rs" version = "1.0.7" @@ -3610,6 +3928,27 @@ dependencies = [ "syn 2.0.117", ] +[[package]] +name = "system-configuration" +version = "0.7.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a13f3d0daba03132c0aa9767f98351b3488edc2c100cda2d2ec2b04f3d8d3c8b" +dependencies = [ + "bitflags 2.11.0", + "core-foundation 0.9.4", + "system-configuration-sys", +] + +[[package]] +name = "system-configuration-sys" +version = "0.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8e1d1b10ced5ca923a1fcb8d03e96b8d3268065d724548c0211415ff6ac6bac4" +dependencies = [ + "core-foundation-sys", + "libc", +] + [[package]] name = "system-deps" version = "6.2.2" @@ -3631,7 +3970,7 @@ checksum = "6e06d52c379e63da659a483a958110bbde891695a0ecb53e48cc7786d5eda7bb" dependencies = [ "bitflags 2.11.0", "block2", - "core-foundation", + "core-foundation 0.10.1", "core-graphics", "crossbeam-channel", "dispatch2", @@ -3708,7 +4047,7 @@ dependencies = [ "percent-encoding", "plist", "raw-window-handle", - "reqwest", + "reqwest 0.13.2", "serde", "serde_json", "serde_repr", @@ -4075,6 +4414,21 @@ dependencies = [ "zerovec", ] +[[package]] +name = "tinyvec" +version = "1.10.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bfa5fdc3bce6191a1dbc8c02d5c8bffcf557bafa17c124c5264a458f1b0613fa" +dependencies = [ + "tinyvec_macros", +] + +[[package]] +name = "tinyvec_macros" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1f3ccbac311fea05f86f61904b462b55fb3df8837a366dfc601a0161d0532f20" + [[package]] name = "tokio" version = "1.50.0" @@ -4103,6 +4457,26 @@ dependencies = [ "syn 2.0.117", ] +[[package]] +name = "tokio-native-tls" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bbae76ab933c85776efabc971569dd6119c580d8f5d448769dec1764bf796ef2" +dependencies = [ + "native-tls", + "tokio", +] + +[[package]] +name = "tokio-rustls" +version = "0.26.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1729aa945f29d91ba541258c8df89027d5792d85a8841fb65e8bf0f4ede4ef61" +dependencies = [ + "rustls", + "tokio", +] + [[package]] name = "tokio-util" version = "0.7.18" @@ -4372,6 +4746,15 @@ version = "1.0.24" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e6e4313cd5fcd3dad5cafa179702e2b244f760991f45397d14d4ebf38247da75" +[[package]] +name = "unicode-normalization" +version = "0.1.25" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5fd4f6878c9cb28d874b009da9e8d183b5abc80117c40bbd187a1fde336be6e8" +dependencies = [ + "tinyvec", +] + [[package]] name = "unicode-segmentation" version = "1.12.0" @@ -4384,6 +4767,12 @@ version = "0.2.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ebc1c04c71510c7f702b52b7c350734c9ff1295c464a03335b00bb84fc54f853" +[[package]] +name = "untrusted" +version = "0.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8ecb6da28b8a351d773b68d5825ac39017e680750f980f3a1a85cd8dd28a47c1" + [[package]] name = "url" version = "2.5.8" @@ -4439,6 +4828,12 @@ dependencies = [ "wasm-bindgen", ] +[[package]] +name = "vcpkg" +version = "0.2.15" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "accd4ea62f7bb7a82fe23066fb0957d48ef677f6eeb8215f372f52e48bb32426" + [[package]] name = "version-compare" version = "0.2.1" @@ -4877,6 +5272,17 @@ dependencies = [ "windows-link 0.1.3", ] +[[package]] +name = "windows-registry" +version = "0.6.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "02752bf7fbdcce7f2a27a742f798510f3e5ad88dbe84871e5168e2120c3d5720" +dependencies = [ + "windows-link 0.2.1", + "windows-result 0.4.1", + "windows-strings 0.5.1", +] + [[package]] name = "windows-result" version = "0.3.4" @@ -4922,6 +5328,15 @@ dependencies = [ "windows-targets 0.42.2", ] +[[package]] +name = "windows-sys" +version = "0.52.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "282be5f36a8ce781fad8c8ae18fa3f9beff57ec1b52cb3de0789201425d9a33d" +dependencies = [ + "windows-targets 0.52.6", +] + [[package]] name = "windows-sys" version = "0.59.0" @@ -5404,6 +5819,12 @@ dependencies = [ "synstructure", ] +[[package]] +name = "zeroize" +version = "1.8.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b97154e67e32c85465826e8bcc1c59429aaaf107c1e4a9e53c8d8ccd5eff88d0" + [[package]] name = "zerotrie" version = "0.2.3" diff --git a/crates/nomina-app/Cargo.toml b/crates/nomina-app/Cargo.toml index a5d290b..f893735 100644 --- a/crates/nomina-app/Cargo.toml +++ b/crates/nomina-app/Cargo.toml @@ -17,7 +17,9 @@ log = "0.4" env_logger = "0.11" directories = "6" anyhow = "1" +winreg = "0.55" tokio = { version = "1", features = ["full"] } +reqwest = { version = "0.12", features = ["json"] } [build-dependencies] tauri-build = { version = "2", features = [] } diff --git a/crates/nomina-app/capabilities/default.json b/crates/nomina-app/capabilities/default.json index afa084b..fd2b53d 100644 --- a/crates/nomina-app/capabilities/default.json +++ b/crates/nomina-app/capabilities/default.json @@ -4,7 +4,20 @@ "windows": ["main"], "permissions": [ "core:default", + "core:window:allow-minimize", + "core:window:allow-toggle-maximize", + "core:window:allow-close", + "core:window:allow-start-dragging", + "core:window:allow-set-size", + "core:window:allow-set-position", + "core:window:allow-outer-position", + "core:window:allow-outer-size", + "core:window:allow-is-maximized", + "core:window:allow-maximize", + "core:window:allow-set-always-on-top", + "core:window:allow-request-user-attention", "dialog:default", - "shell:default" + "shell:default", + "shell:allow-open" ] } diff --git a/crates/nomina-app/gen/schemas/capabilities.json b/crates/nomina-app/gen/schemas/capabilities.json index 1b48039..17c20f8 100644 --- a/crates/nomina-app/gen/schemas/capabilities.json +++ b/crates/nomina-app/gen/schemas/capabilities.json @@ -1 +1 @@ -{"default":{"identifier":"default","description":"Default permissions for Nomina","local":true,"windows":["main"],"permissions":["core:default","dialog:default","shell:default"]}} \ No newline at end of file +{"default":{"identifier":"default","description":"Default permissions for Nomina","local":true,"windows":["main"],"permissions":["core:default","core:window:allow-minimize","core:window:allow-toggle-maximize","core:window:allow-close","core:window:allow-start-dragging","core:window:allow-set-size","core:window:allow-set-position","core:window:allow-outer-position","core:window:allow-outer-size","core:window:allow-is-maximized","core:window:allow-maximize","core:window:allow-set-always-on-top","core:window:allow-request-user-attention","dialog:default","shell:default","shell:allow-open"]}} \ No newline at end of file diff --git a/crates/nomina-app/src/commands/context_menu.rs b/crates/nomina-app/src/commands/context_menu.rs new file mode 100644 index 0000000..3de9f31 --- /dev/null +++ b/crates/nomina-app/src/commands/context_menu.rs @@ -0,0 +1,128 @@ +use std::path::PathBuf; + +#[cfg(target_os = "windows")] +use winreg::enums::*; +#[cfg(target_os = "windows")] +use winreg::RegKey; + +#[tauri::command] +pub fn get_launch_args() -> Vec { + std::env::args().skip(1).collect() +} + +#[tauri::command] +pub fn register_context_menu() -> Result<(), String> { + #[cfg(target_os = "windows")] + { + let exe = std::env::current_exe() + .map_err(|e| format!("Failed to get exe path: {}", e))?; + let exe_str = exe.to_string_lossy().to_string(); + let icon = format!("{},0", exe_str); + + let hkcu = RegKey::predef(HKEY_CURRENT_USER); + let base = r"Software\Classes"; + + // Context menu for files: *\shell\Nomina + let (key, _) = hkcu + .create_subkey(format!(r"{}\*\shell\Nomina", base)) + .map_err(|e| format!("Registry error: {}", e))?; + key.set_value("", &"Edit in Nomina") + .map_err(|e| format!("Registry error: {}", e))?; + key.set_value("Icon", &icon) + .map_err(|e| format!("Registry error: {}", e))?; + + let (cmd, _) = hkcu + .create_subkey(format!(r"{}\*\shell\Nomina\command", base)) + .map_err(|e| format!("Registry error: {}", e))?; + cmd.set_value("", &format!("\"{}\" \"%1\"", exe_str)) + .map_err(|e| format!("Registry error: {}", e))?; + + // Context menu for folders: Directory\shell\Nomina + let (key, _) = hkcu + .create_subkey(format!(r"{}\Directory\shell\Nomina", base)) + .map_err(|e| format!("Registry error: {}", e))?; + key.set_value("", &"Edit in Nomina") + .map_err(|e| format!("Registry error: {}", e))?; + key.set_value("Icon", &icon) + .map_err(|e| format!("Registry error: {}", e))?; + + let (cmd, _) = hkcu + .create_subkey(format!(r"{}\Directory\shell\Nomina\command", base)) + .map_err(|e| format!("Registry error: {}", e))?; + cmd.set_value("", &format!("\"{}\" \"%1\"", exe_str)) + .map_err(|e| format!("Registry error: {}", e))?; + + // Context menu for folder background: Directory\Background\shell\Nomina + let (key, _) = hkcu + .create_subkey(format!(r"{}\Directory\Background\shell\Nomina", base)) + .map_err(|e| format!("Registry error: {}", e))?; + key.set_value("", &"Edit in Nomina") + .map_err(|e| format!("Registry error: {}", e))?; + key.set_value("Icon", &icon) + .map_err(|e| format!("Registry error: {}", e))?; + + let (cmd, _) = hkcu + .create_subkey(format!(r"{}\Directory\Background\shell\Nomina\command", base)) + .map_err(|e| format!("Registry error: {}", e))?; + // %V gives the current folder path when right-clicking background + cmd.set_value("", &format!("\"{}\" \"%V\"", exe_str)) + .map_err(|e| format!("Registry error: {}", e))?; + + Ok(()) + } + + #[cfg(not(target_os = "windows"))] + Err("Context menu registration is only supported on Windows".to_string()) +} + +#[tauri::command] +pub fn unregister_context_menu() -> Result<(), String> { + #[cfg(target_os = "windows")] + { + let hkcu = RegKey::predef(HKEY_CURRENT_USER); + let base = r"Software\Classes"; + + let _ = hkcu.delete_subkey_all(format!(r"{}\*\shell\Nomina", base)); + let _ = hkcu.delete_subkey_all(format!(r"{}\Directory\shell\Nomina", base)); + let _ = hkcu.delete_subkey_all(format!(r"{}\Directory\Background\shell\Nomina", base)); + + Ok(()) + } + + #[cfg(not(target_os = "windows"))] + Err("Context menu registration is only supported on Windows".to_string()) +} + +/// Given CLI args (paths), figure out what folder to open and which files to select. +/// Returns (folder_path, selected_file_paths). +#[tauri::command] +pub fn resolve_launch_paths(args: Vec) -> Option<(String, Vec)> { + if args.is_empty() { + return None; + } + + let paths: Vec = args.iter().map(PathBuf::from).filter(|p| p.exists()).collect(); + if paths.is_empty() { + return None; + } + + // If single directory passed (from background click or folder right-click), open it + if paths.len() == 1 && paths[0].is_dir() { + let folder = paths[0].to_string_lossy().to_string(); + return Some((folder, vec![])); + } + + // For files (or mix), use the parent of the first path as the folder + // and collect all paths as the selection + let folder = paths[0] + .parent() + .map(|p| p.to_string_lossy().to_string()) + .unwrap_or_default(); + + if folder.is_empty() { + return None; + } + + let selected: Vec = paths.iter().map(|p| p.to_string_lossy().to_string()).collect(); + Some((folder, selected)) +} diff --git a/crates/nomina-app/src/commands/files.rs b/crates/nomina-app/src/commands/files.rs index dc71ea1..e692e33 100644 --- a/crates/nomina-app/src/commands/files.rs +++ b/crates/nomina-app/src/commands/files.rs @@ -48,3 +48,55 @@ pub async fn get_file_metadata(path: String) -> Result { accessed: meta.accessed().ok().map(|t| chrono::DateTime::::from(t)), }) } + +#[tauri::command] +pub async fn reveal_in_explorer(path: String) -> Result<(), String> { + let p = PathBuf::from(&path); + + #[cfg(target_os = "windows")] + { + if p.is_dir() { + std::process::Command::new("explorer") + .arg(&path) + .spawn() + .map_err(|e| e.to_string())?; + } else { + std::process::Command::new("explorer") + .args(["/select,", &path]) + .spawn() + .map_err(|e| e.to_string())?; + } + } + + #[cfg(target_os = "macos")] + { + if p.is_dir() { + std::process::Command::new("open") + .arg(&path) + .spawn() + .map_err(|e| e.to_string())?; + } else { + std::process::Command::new("open") + .args(["-R", &path]) + .spawn() + .map_err(|e| e.to_string())?; + } + } + + #[cfg(target_os = "linux")] + { + let target = if p.is_dir() { + path.clone() + } else { + p.parent() + .map(|p| p.to_string_lossy().to_string()) + .unwrap_or(path.clone()) + }; + std::process::Command::new("xdg-open") + .arg(&target) + .spawn() + .map_err(|e| e.to_string())?; + } + + Ok(()) +} diff --git a/crates/nomina-app/src/commands/mod.rs b/crates/nomina-app/src/commands/mod.rs index 6ab0167..3bb814b 100644 --- a/crates/nomina-app/src/commands/mod.rs +++ b/crates/nomina-app/src/commands/mod.rs @@ -2,3 +2,5 @@ pub mod files; pub mod rename; pub mod presets; pub mod undo; +pub mod context_menu; +pub mod updates; diff --git a/crates/nomina-app/src/commands/presets.rs b/crates/nomina-app/src/commands/presets.rs index e17f4eb..415646e 100644 --- a/crates/nomina-app/src/commands/presets.rs +++ b/crates/nomina-app/src/commands/presets.rs @@ -1,5 +1,6 @@ use std::path::PathBuf; +use nomina_core::bru; use nomina_core::preset::NominaPreset; #[tauri::command] @@ -19,6 +20,11 @@ pub async fn load_preset(path: String) -> Result { NominaPreset::load(&PathBuf::from(path)).map_err(|e| e.to_string()) } +#[tauri::command] +pub async fn delete_preset(path: String) -> Result<(), String> { + std::fs::remove_file(&path).map_err(|e| e.to_string()) +} + #[tauri::command] pub async fn list_presets() -> Result, String> { let dir = get_presets_dir(); @@ -70,3 +76,21 @@ fn sanitize_filename(name: &str) -> String { }) .collect() } + +#[tauri::command] +pub async fn export_preset(source_path: String, dest_path: String) -> Result<(), String> { + std::fs::copy(&source_path, &dest_path).map_err(|e| e.to_string())?; + Ok(()) +} + +#[tauri::command] +pub async fn import_preset(path: String) -> Result { + let p = PathBuf::from(&path); + let ext = p.extension().and_then(|e| e.to_str()).unwrap_or("").to_lowercase(); + + match ext.as_str() { + "nomina" => NominaPreset::load(&p).map_err(|e| e.to_string()), + "bru" => bru::parse_bru_file(&p).map_err(|e| e.to_string()), + _ => Err(format!("Unsupported preset format: .{}", ext)), + } +} diff --git a/crates/nomina-app/src/commands/rename.rs b/crates/nomina-app/src/commands/rename.rs index 204b467..946047a 100644 --- a/crates/nomina-app/src/commands/rename.rs +++ b/crates/nomina-app/src/commands/rename.rs @@ -29,30 +29,62 @@ fn build_rule(cfg: &RuleConfig) -> Option> { if !cfg.enabled { return None; } - let val = &cfg.config; + // Re-inject `enabled` - #[serde(flatten)] on RuleConfig consumes it + // before individual rule structs can see it + let mut val = cfg.config.clone(); + if let serde_json::Value::Object(ref mut map) = val { + map.insert("enabled".to_string(), serde_json::Value::Bool(cfg.enabled)); + } match cfg.rule_type.as_str() { - "replace" => serde_json::from_value::(val.clone()) + "replace" => serde_json::from_value::(val) .ok() .map(|r| Box::new(r) as Box), - "regex" => serde_json::from_value::(val.clone()) + "regex" => serde_json::from_value::(val) .ok() .map(|r| Box::new(r) as Box), - "remove" => serde_json::from_value::(val.clone()) + "remove" => serde_json::from_value::(val) .ok() .map(|r| Box::new(r) as Box), - "add" => serde_json::from_value::(val.clone()) + "add" => serde_json::from_value::(val) .ok() .map(|r| Box::new(r) as Box), - "case" => serde_json::from_value::(val.clone()) + "case" => serde_json::from_value::(val) .ok() .map(|r| Box::new(r) as Box), - "numbering" => serde_json::from_value::(val.clone()) + "numbering" => serde_json::from_value::(val) .ok() .map(|r| Box::new(r) as Box), - "date" => serde_json::from_value::(val.clone()) + "date" => serde_json::from_value::(val) .ok() .map(|r| Box::new(r) as Box), - "move_parts" => serde_json::from_value::(val.clone()) + "move_parts" => serde_json::from_value::(val) + .ok() + .map(|r| Box::new(r) as Box), + "text_editor" => serde_json::from_value::(val) + .ok() + .map(|r| Box::new(r) as Box), + "hash" => serde_json::from_value::(val) + .ok() + .map(|r| Box::new(r) as Box), + "folder_name" => serde_json::from_value::(val) + .ok() + .map(|r| Box::new(r) as Box), + "transliterate" => serde_json::from_value::(val) + .ok() + .map(|r| Box::new(r) as Box), + "padding" => serde_json::from_value::(val) + .ok() + .map(|r| Box::new(r) as Box), + "truncate" => serde_json::from_value::(val) + .ok() + .map(|r| Box::new(r) as Box), + "randomize" => serde_json::from_value::(val) + .ok() + .map(|r| Box::new(r) as Box), + "swap" => serde_json::from_value::(val) + .ok() + .map(|r| Box::new(r) as Box), + "sanitize" => serde_json::from_value::(val) .ok() .map(|r| Box::new(r) as Box), _ => None, @@ -64,12 +96,20 @@ pub async fn preview_rename( rules: Vec, directory: String, filters: Option, + selected_paths: Option>, ) -> Result, String> { let scanner = FileScanner::new( PathBuf::from(&directory), filters.unwrap_or_default(), ); - let files = scanner.scan(); + let all_files = scanner.scan(); + + let files = if let Some(ref paths) = selected_paths { + let file_map: std::collections::HashMap = all_files.into_iter().map(|f| (f.path.clone(), f)).collect(); + paths.iter().filter_map(|p| file_map.get(&PathBuf::from(p)).cloned()).collect() + } else { + all_files + }; let mut pipeline = Pipeline::new(); for cfg in &rules { @@ -77,7 +117,11 @@ pub async fn preview_rename( pipeline.add_step(rule, cfg.step_mode.clone()); } if cfg.rule_type == "extension" { - if let Ok(ext_rule) = serde_json::from_value(cfg.config.clone()) { + let mut ext_val = cfg.config.clone(); + if let serde_json::Value::Object(ref mut map) = ext_val { + map.insert("enabled".to_string(), serde_json::Value::Bool(cfg.enabled)); + } + if let Ok(ext_rule) = serde_json::from_value(ext_val) { pipeline.extension_rule = Some(ext_rule); } } @@ -87,12 +131,40 @@ pub async fn preview_rename( } #[tauri::command] -pub async fn execute_rename(operations: Vec) -> Result { - let valid: Vec<&PreviewResult> = operations +pub async fn execute_rename( + operations: Vec, + create_backup: Option, + undo_limit: Option, + skip_read_only: Option, + conflict_strategy: Option, + backup_path: Option, +) -> Result { + let skip_ro = skip_read_only.unwrap_or(true); + let strategy = conflict_strategy.unwrap_or_else(|| "suffix".to_string()); + + let mut valid: Vec<&PreviewResult> = operations .iter() .filter(|op| !op.has_conflict && !op.has_error && op.original_name != op.new_name) .collect(); + // skip read-only files if enabled + if skip_ro { + valid.retain(|op| { + if let Ok(meta) = std::fs::metadata(&op.original_path) { + !meta.permissions().readonly() + } else { + true + } + }); + } + + // sort deepest paths first so children are renamed before parents + valid.sort_by(|a, b| { + let depth_a = a.original_path.components().count(); + let depth_b = b.original_path.components().count(); + depth_b.cmp(&depth_a) + }); + // validation pass for op in &valid { if !op.original_path.exists() { @@ -100,6 +172,32 @@ pub async fn execute_rename(operations: Vec) -> Result) -> Result) -> Result Result { + let current = env!("CARGO_PKG_VERSION").to_string(); + let api_url = "https://git.lashman.live/api/v1/repos/lashman/nomina/releases?limit=1"; + + let client = reqwest::Client::builder() + .timeout(std::time::Duration::from_secs(10)) + .build() + .map_err(|e| e.to_string())?; + + let resp = client + .get(api_url) + .header("Accept", "application/json") + .send() + .await + .map_err(|e| format!("Network error: {}", e))?; + + if !resp.status().is_success() { + return Err(format!("Server returned {}", resp.status())); + } + + let releases: Vec = resp + .json() + .await + .map_err(|e| format!("Parse error: {}", e))?; + + let latest = releases.first().ok_or("No releases found")?; + let tag = latest["tag_name"] + .as_str() + .unwrap_or("0.0.0") + .trim_start_matches('v') + .to_string(); + let url = latest["html_url"] + .as_str() + .unwrap_or("") + .to_string(); + + let available = version_newer(&tag, ¤t); + + Ok(UpdateInfo { + available, + current_version: current, + latest_version: tag, + url, + }) +} + +fn version_newer(latest: &str, current: &str) -> bool { + let parse = |s: &str| -> Vec { + s.split('.').filter_map(|p| p.parse().ok()).collect() + }; + let l = parse(latest); + let c = parse(current); + for i in 0..l.len().max(c.len()) { + let lv = l.get(i).copied().unwrap_or(0); + let cv = c.get(i).copied().unwrap_or(0); + if lv > cv { + return true; + } + if lv < cv { + return false; + } + } + false +} diff --git a/crates/nomina-app/src/main.rs b/crates/nomina-app/src/main.rs index 1693310..2b22b6f 100644 --- a/crates/nomina-app/src/main.rs +++ b/crates/nomina-app/src/main.rs @@ -1,4 +1,4 @@ -#![cfg_attr(not(debug_assertions), windows_subsystem = "windows")] +#![windows_subsystem = "windows"] mod commands; @@ -9,15 +9,24 @@ fn main() { .invoke_handler(tauri::generate_handler![ commands::files::scan_directory, commands::files::get_file_metadata, + commands::files::reveal_in_explorer, commands::rename::preview_rename, commands::rename::execute_rename, commands::presets::save_preset, commands::presets::load_preset, commands::presets::list_presets, + commands::presets::delete_preset, + commands::presets::export_preset, + commands::presets::import_preset, commands::undo::undo_last, commands::undo::undo_batch, commands::undo::get_undo_history, commands::undo::clear_undo_history, + commands::context_menu::get_launch_args, + commands::context_menu::register_context_menu, + commands::context_menu::unregister_context_menu, + commands::context_menu::resolve_launch_paths, + commands::updates::check_for_updates, ]) .run(tauri::generate_context!()) .expect("error running nomina"); diff --git a/crates/nomina-app/tauri.conf.json b/crates/nomina-app/tauri.conf.json index 32a3914..5c1e464 100644 --- a/crates/nomina-app/tauri.conf.json +++ b/crates/nomina-app/tauri.conf.json @@ -6,8 +6,8 @@ "build": { "frontendDist": "../../ui/dist", "devUrl": "http://localhost:5173", - "beforeDevCommand": "cd ../../ui && npm run dev", - "beforeBuildCommand": "cd ../../ui && npm run build" + "beforeDevCommand": { "script": "npm run dev", "cwd": "../../ui" }, + "beforeBuildCommand": { "script": "npm run build", "cwd": "../../ui" } }, "app": { "windows": [ @@ -18,7 +18,10 @@ "minWidth": 900, "minHeight": 600, "resizable": true, - "fullscreen": false + "fullscreen": false, + "decorations": false, + "shadow": false, + "transparent": true } ], "security": { diff --git a/crates/nomina-core/Cargo.toml b/crates/nomina-core/Cargo.toml index 9aa3a4a..f8b4c92 100644 --- a/crates/nomina-core/Cargo.toml +++ b/crates/nomina-core/Cargo.toml @@ -17,7 +17,13 @@ thiserror = "2" glob = "0.3" natord = "1" log = "0.4" -kamadak-exif = "0.5" +kamadak-exif = "0.6" +unicode-normalization = "0.1" +sha2 = "0.10" +md-5 = "0.10" +sha1 = "0.10" +deunicode = "1" +rand = "0.8" [dev-dependencies] tempfile = "3" diff --git a/crates/nomina-core/src/bru.rs b/crates/nomina-core/src/bru.rs new file mode 100644 index 0000000..5cd8edf --- /dev/null +++ b/crates/nomina-core/src/bru.rs @@ -0,0 +1,208 @@ +use std::collections::HashMap; +use std::path::Path; + +use crate::NominaError; +use crate::preset::{NominaPreset, PresetRule}; + +/// Parse a Bulk Rename Utility (.bru) preset file into a NominaPreset. +/// BRU files use an INI-like format with sections for each operation. +pub fn parse_bru_file(path: &Path) -> crate::Result { + let data = std::fs::read_to_string(path).map_err(|e| NominaError::Filesystem { + path: path.to_path_buf(), + source: e, + })?; + + let sections = parse_ini(&data); + let mut rules = Vec::new(); + + // RegEx(1) section + if let Some(sec) = sections.get("regexmatch(1)") { + let pattern = sec.get("match").cloned().unwrap_or_default(); + let replace = sec.get("replace").cloned().unwrap_or_default(); + let case = sec.get("casesensitive").map(|v| v == "1").unwrap_or(false); + if !pattern.is_empty() { + rules.push(make_rule("regex", serde_json::json!({ + "pattern": pattern, + "replacement": replace, + "case_sensitive": case, + "global": true, + "target": "Name", + }))); + } + } + + // Replace sections - BRU supports multiple replace slots + for i in 1..=14 { + let key = if i == 1 { "replace(1)".to_string() } else { format!("replace({})", i) }; + if let Some(sec) = sections.get(&key) { + let search = sec.get("find").or(sec.get("search")).cloned().unwrap_or_default(); + let replace = sec.get("replace").cloned().unwrap_or_default(); + let case = sec.get("casesensitive").or(sec.get("matchcase")).map(|v| v == "1").unwrap_or(false); + if !search.is_empty() { + rules.push(make_rule("replace", serde_json::json!({ + "search": search, + "replace_with": replace, + "match_case": case, + "use_regex": false, + "target": "Name", + }))); + } + } + } + + // Remove section + if let Some(sec) = sections.get("remove(1)") { + let first_n: usize = sec.get("first").and_then(|v| v.parse().ok()).unwrap_or(0); + let last_n: usize = sec.get("last").and_then(|v| v.parse().ok()).unwrap_or(0); + let from: usize = sec.get("from").and_then(|v| v.parse().ok()).unwrap_or(0); + let to: usize = sec.get("to").and_then(|v| v.parse().ok()).unwrap_or(0); + if first_n > 0 || last_n > 0 || from != to { + rules.push(make_rule("remove", serde_json::json!({ + "first_n": first_n, + "last_n": last_n, + "from": from, + "to": to, + "crop_before": "", + "crop_after": "", + "remove_pattern": "", + "collapse_chars": false, + "trim": { "spaces": false, "dots": false, "dashes": false, "underscores": false }, + }))); + } + } + + // Add / Prefix / Suffix section + if let Some(sec) = sections.get("add(1)") { + let prefix = sec.get("prefix").cloned().unwrap_or_default(); + let suffix = sec.get("suffix").cloned().unwrap_or_default(); + let insert = sec.get("insert").cloned().unwrap_or_default(); + let at_pos: usize = sec.get("atpos").or(sec.get("at")).and_then(|v| v.parse().ok()).unwrap_or(0); + if !prefix.is_empty() || !suffix.is_empty() || !insert.is_empty() { + rules.push(make_rule("add", serde_json::json!({ + "prefix": prefix, + "suffix": suffix, + "insert": insert, + "position": at_pos, + "inserts": [], + }))); + } + } + + // Case section + if let Some(sec) = sections.get("case(1)") { + let mode_num: u32 = sec.get("type").or(sec.get("mode")).and_then(|v| v.parse().ok()).unwrap_or(0); + let mode = match mode_num { + 1 => "Lower", + 2 => "Upper", + 3 => "Title", + 4 => "Sentence", + 5 => "Invert", + _ => "Same", + }; + if mode != "Same" { + rules.push(make_rule("case", serde_json::json!({ + "mode": mode, + "target": "Name", + }))); + } + } + + // Numbering section + if let Some(sec) = sections.get("number(1)") { + let mode_num: u32 = sec.get("mode").and_then(|v| v.parse().ok()).unwrap_or(0); + if mode_num > 0 { + let start: i64 = sec.get("start").and_then(|v| v.parse().ok()).unwrap_or(1); + let step: i64 = sec.get("incr").or(sec.get("step")).and_then(|v| v.parse().ok()).unwrap_or(1); + let pad: usize = sec.get("pad").and_then(|v| v.parse().ok()).unwrap_or(0); + let mode = match mode_num { + 1 => "Prefix", + 2 => "Suffix", + 3 => "Both", + _ => "Prefix", + }; + rules.push(make_rule("numbering", serde_json::json!({ + "mode": mode, + "start": start, + "step": step, + "padding": pad, + "separator": "", + "format": "Decimal", + }))); + } + } + + // Extension section + if let Some(sec) = sections.get("extension(1)") { + let mode_num: u32 = sec.get("type").or(sec.get("mode")).and_then(|v| v.parse().ok()).unwrap_or(0); + let new_ext = sec.get("new").or(sec.get("extension")).cloned().unwrap_or_default(); + let mode = match mode_num { + 1 => "Lower", + 2 => "Upper", + 3 => "Title", + 4 if !new_ext.is_empty() => "Fixed", + 5 => "Remove", + _ => "Same", + }; + if mode != "Same" { + let mut json = serde_json::json!({ + "mode": mode, + "new_extension": new_ext, + "mapping": [], + }); + if mode == "Fixed" { + json["new_extension"] = serde_json::Value::String(new_ext); + } + rules.push(make_rule("extension", json)); + } + } + + let name = path.file_stem() + .map(|s| s.to_string_lossy().to_string()) + .unwrap_or_else(|| "Imported BRU preset".to_string()); + + Ok(NominaPreset { + version: 1, + name, + description: "Imported from Bulk Rename Utility".to_string(), + created: chrono::Utc::now().to_rfc3339(), + rules, + filters: None, + }) +} + +fn make_rule(rule_type: &str, config: serde_json::Value) -> PresetRule { + let mut map = config.as_object().cloned().unwrap_or_default(); + map.insert("type".to_string(), serde_json::Value::String(rule_type.to_string())); + map.insert("step_mode".to_string(), serde_json::Value::String("Simultaneous".to_string())); + map.insert("enabled".to_string(), serde_json::Value::Bool(true)); + + serde_json::from_value(serde_json::Value::Object(map)).unwrap() +} + +/// Parse INI-like BRU format into section -> key/value maps. +/// BRU uses `[SectionName]` headers and `Key=Value` pairs. +/// Section names are normalized to lowercase for easier lookup. +fn parse_ini(text: &str) -> HashMap> { + let mut sections: HashMap> = HashMap::new(); + let mut current_section = String::new(); + + for line in text.lines() { + let trimmed = line.trim(); + if trimmed.is_empty() || trimmed.starts_with(';') || trimmed.starts_with('#') { + continue; + } + if trimmed.starts_with('[') && trimmed.ends_with(']') { + current_section = trimmed[1..trimmed.len() - 1].trim().to_lowercase(); + sections.entry(current_section.clone()).or_default(); + } else if let Some((key, value)) = trimmed.split_once('=') { + if !current_section.is_empty() { + sections + .entry(current_section.clone()) + .or_default() + .insert(key.trim().to_lowercase(), value.trim().to_string()); + } + } + } + + sections +} diff --git a/crates/nomina-core/src/lib.rs b/crates/nomina-core/src/lib.rs index 7acd391..0a38d7a 100644 --- a/crates/nomina-core/src/lib.rs +++ b/crates/nomina-core/src/lib.rs @@ -3,6 +3,7 @@ pub mod pipeline; pub mod filter; pub mod metadata; pub mod preset; +pub mod bru; pub mod undo; pub mod scanner; @@ -54,6 +55,7 @@ pub struct RenameContext { pub size: u64, pub created: Option>, pub modified: Option>, + pub accessed: Option>, pub date_taken: Option>, pub parent_folder: String, } @@ -69,6 +71,7 @@ impl RenameContext { size: 0, created: None, modified: None, + accessed: None, date_taken: None, parent_folder: String::new(), } diff --git a/crates/nomina-core/src/pipeline.rs b/crates/nomina-core/src/pipeline.rs index af90fab..c00101d 100644 --- a/crates/nomina-core/src/pipeline.rs +++ b/crates/nomina-core/src/pipeline.rs @@ -47,6 +47,7 @@ impl Pipeline { size: file.size, created: file.created, modified: file.modified, + accessed: file.accessed, date_taken: None, parent_folder: file .path @@ -91,27 +92,11 @@ impl Pipeline { fn apply_rules(&self, stem: &str, ctx: &RenameContext) -> String { let mut working = stem.to_string(); - // collect simultaneous rules - let simultaneous: Vec<&PipelineStep> = self - .steps - .iter() - .filter(|s| s.mode == StepMode::Simultaneous && s.rule.is_enabled()) - .collect(); - - // apply simultaneous rules (last one wins for the full output) - if !simultaneous.is_empty() { - let mut sim_result = stem.to_string(); - for step in &simultaneous { - sim_result = step.rule.apply(stem, ctx); - } - working = sim_result; - } - - // apply sequential rules in order for step in &self.steps { - if step.mode == StepMode::Sequential && step.rule.is_enabled() { - working = step.rule.apply(&working, ctx); + if !step.rule.is_enabled() { + continue; } + working = step.rule.apply(&working, ctx); } working @@ -126,6 +111,18 @@ impl Pipeline { } for r in &mut results { + // check for empty filename (stem is everything before the last dot) + let stem = if let Some(dot_pos) = r.new_name.rfind('.') { + &r.new_name[..dot_pos] + } else { + &r.new_name[..] + }; + if stem.is_empty() { + r.has_error = true; + r.error_message = Some("Empty filename - add a rule to set a new name".into()); + continue; + } + if let Some(&count) = name_counts.get(&r.new_name.to_lowercase()) { if count > 1 { r.has_conflict = true; @@ -177,6 +174,10 @@ mod tests { match_case: true, first_only: false, enabled: true, + use_regex: false, + scope_start: None, + scope_end: None, + occurrence: None, }), StepMode::Simultaneous, ); @@ -197,6 +198,10 @@ mod tests { match_case: true, first_only: false, enabled: true, + use_regex: false, + scope_start: None, + scope_end: None, + occurrence: None, }), StepMode::Sequential, ); @@ -224,6 +229,10 @@ mod tests { match_case: true, first_only: false, enabled: false, + use_regex: false, + scope_start: None, + scope_end: None, + occurrence: None, }), StepMode::Simultaneous, ); diff --git a/crates/nomina-core/src/rules/add.rs b/crates/nomina-core/src/rules/add.rs index 3dadea3..2da0d53 100644 --- a/crates/nomina-core/src/rules/add.rs +++ b/crates/nomina-core/src/rules/add.rs @@ -2,6 +2,12 @@ use serde::{Deserialize, Serialize}; use crate::{RenameContext, RenameRule}; +#[derive(Debug, Clone, Serialize, Deserialize)] +pub struct InsertOp { + pub position: usize, + pub text: String, +} + #[derive(Debug, Clone, Serialize, Deserialize)] pub struct AddRule { pub prefix: String, @@ -10,6 +16,23 @@ pub struct AddRule { pub insert_at: usize, pub word_space: bool, pub enabled: bool, + #[serde(default)] + pub inserts: Vec, +} + +fn expand_vars(template: &str, ctx: &RenameContext) -> String { + let date_str = ctx + .modified + .map(|d| d.format("%Y-%m-%d").to_string()) + .unwrap_or_default(); + + template + .replace("{name}", &ctx.original_name) + .replace("{ext}", &ctx.extension) + .replace("{folder}", &ctx.parent_folder) + .replace("{date}", &date_str) + .replace("{index}", &ctx.index.to_string()) + .replace("{total}", &ctx.total.to_string()) } impl AddRule { @@ -21,39 +44,60 @@ impl AddRule { insert_at: 0, word_space: false, enabled: true, + inserts: Vec::new(), } } } impl RenameRule for AddRule { - fn apply(&self, filename: &str, _context: &RenameContext) -> String { + fn apply(&self, filename: &str, context: &RenameContext) -> String { + let prefix = expand_vars(&self.prefix, context); + let suffix = expand_vars(&self.suffix, context); + let insert = expand_vars(&self.insert, context); + let mut result = filename.to_string(); - if !self.insert.is_empty() { + if !insert.is_empty() { let chars: Vec = result.chars().collect(); let pos = self.insert_at.min(chars.len()); let before: String = chars[..pos].iter().collect(); let after: String = chars[pos..].iter().collect(); if self.word_space { - result = format!("{} {} {}", before.trim_end(), self.insert, after.trim_start()); + result = format!("{} {} {}", before.trim_end(), insert, after.trim_start()); } else { - result = format!("{}{}{}", before, self.insert, after); + result = format!("{}{}{}", before, insert, after); } } - if !self.prefix.is_empty() { - if self.word_space { - result = format!("{} {}", self.prefix, result); - } else { - result = format!("{}{}", self.prefix, result); + if !self.inserts.is_empty() { + let mut ops: Vec<&InsertOp> = self.inserts.iter().collect(); + ops.sort_by(|a, b| b.position.cmp(&a.position)); + for op in ops { + let text = expand_vars(&op.text, context); + if text.is_empty() { + continue; + } + let chars: Vec = result.chars().collect(); + let pos = op.position.min(chars.len()); + let before: String = chars[..pos].iter().collect(); + let after: String = chars[pos..].iter().collect(); + result = format!("{}{}{}", before, text, after); } } - if !self.suffix.is_empty() { + if !prefix.is_empty() { if self.word_space { - result = format!("{} {}", result, self.suffix); + result = format!("{} {}", prefix, result); } else { - result = format!("{}{}", result, self.suffix); + result = format!("{}{}", prefix, result); + } + } + + if !suffix.is_empty() { + if self.word_space { + result = format!("{} {}", result, suffix); + } else { + result = format!("{}{}", result, suffix); } } @@ -107,4 +151,39 @@ mod tests { let ctx = RenameContext::dummy(0); assert_eq!(rule.apply("abcd", &ctx), "ab-x-cd"); } + + #[test] + fn expand_name_var() { + let rule = AddRule { + prefix: "{name}_copy".into(), + ..AddRule::new() + }; + let mut ctx = RenameContext::dummy(0); + ctx.original_name = "photo".into(); + assert_eq!(rule.apply("file", &ctx), "photo_copyfile"); + } + + #[test] + fn expand_index_total() { + let rule = AddRule { + suffix: "_{index}_of_{total}".into(), + ..AddRule::new() + }; + let mut ctx = RenameContext::dummy(3); + ctx.total = 10; + assert_eq!(rule.apply("file", &ctx), "file_3_of_10"); + } + + #[test] + fn multi_inserts_reverse_order() { + let rule = AddRule { + inserts: vec![ + InsertOp { position: 1, text: "X".into() }, + InsertOp { position: 3, text: "Y".into() }, + ], + ..AddRule::new() + }; + let ctx = RenameContext::dummy(0); + assert_eq!(rule.apply("abcd", &ctx), "aXbcYd"); + } } diff --git a/crates/nomina-core/src/rules/case.rs b/crates/nomina-core/src/rules/case.rs index 0985ff8..0b6bbcc 100644 --- a/crates/nomina-core/src/rules/case.rs +++ b/crates/nomina-core/src/rules/case.rs @@ -11,6 +11,12 @@ pub enum CaseMode { Sentence, Invert, Random, + CamelCase, + PascalCase, + SnakeCase, + KebabCase, + DotCase, + SmartTitle, } #[derive(Debug, Clone, Serialize, Deserialize)] @@ -38,6 +44,62 @@ impl CaseRule { } } +fn split_words(s: &str) -> Vec { + let mut words = Vec::new(); + let mut current = String::new(); + + let chars: Vec = s.chars().collect(); + for i in 0..chars.len() { + let c = chars[i]; + if !c.is_alphanumeric() { + if !current.is_empty() { + words.push(std::mem::take(&mut current)); + } + continue; + } + + let should_split = if i > 0 && chars[i - 1].is_alphanumeric() { + if c.is_uppercase() { + // don't split between consecutive uppercase unless next is lowercase + // e.g. "XMLParser" -> split before 'P' not before 'M' or 'L' + let prev_upper = chars[i - 1].is_uppercase(); + if prev_upper { + i + 1 < chars.len() && chars[i + 1].is_lowercase() + } else { + true + } + } else { + false + } + } else { + false + }; + + if should_split && !current.is_empty() { + words.push(std::mem::take(&mut current)); + } + current.push(c); + } + if !current.is_empty() { + words.push(current); + } + words +} + +const SMALL_WORDS: &[&str] = &[ + "a", "an", "the", "and", "but", "or", "for", "nor", + "in", "on", "at", "to", "by", "of", "up", "as", + "is", "it", "if", "so", "no", "not", "yet", +]; + +fn capitalize(s: &str) -> String { + let mut chars = s.chars(); + match chars.next() { + None => String::new(), + Some(c) => c.to_uppercase().to_string() + chars.as_str(), + } +} + impl RenameRule for CaseRule { fn apply(&self, filename: &str, _context: &RenameContext) -> String { match self.mode { @@ -99,6 +161,73 @@ impl RenameRule for CaseRule { }) .collect() } + CaseMode::CamelCase => { + let words = split_words(filename); + words + .iter() + .enumerate() + .map(|(i, w)| { + if i == 0 { + w.to_lowercase() + } else { + capitalize(&w.to_lowercase()) + } + }) + .collect() + } + CaseMode::PascalCase => { + split_words(filename) + .iter() + .map(|w| capitalize(&w.to_lowercase())) + .collect() + } + CaseMode::SnakeCase => { + split_words(filename) + .iter() + .map(|w| w.to_lowercase()) + .collect::>() + .join("_") + } + CaseMode::KebabCase => { + split_words(filename) + .iter() + .map(|w| w.to_lowercase()) + .collect::>() + .join("-") + } + CaseMode::DotCase => { + split_words(filename) + .iter() + .map(|w| w.to_lowercase()) + .collect::>() + .join(".") + } + CaseMode::SmartTitle => { + let exceptions = self.exception_words(); + filename + .split_inclusive(|c: char| !c.is_alphanumeric()) + .enumerate() + .map(|(i, word)| { + let trimmed = word.trim(); + let lower = trimmed.to_lowercase(); + if i > 0 + && (exceptions.contains(&lower) + || SMALL_WORDS.contains(&lower.as_str())) + { + word.to_lowercase() + } else { + let mut chars = word.chars(); + match chars.next() { + None => String::new(), + Some(c) => { + c.to_uppercase().to_string() + + &chars.as_str().to_lowercase() + } + } + } + }) + .collect() + } } } @@ -146,4 +275,53 @@ mod tests { let ctx = RenameContext::dummy(0); assert_eq!(rule.apply("Hello", &ctx), "hELLO"); } + + #[test] + fn camel_case() { + let rule = CaseRule { mode: CaseMode::CamelCase, ..CaseRule::new() }; + let ctx = RenameContext::dummy(0); + assert_eq!(rule.apply("some_file-name", &ctx), "someFileName"); + assert_eq!(rule.apply("hello world", &ctx), "helloWorld"); + assert_eq!(rule.apply("XMLParser", &ctx), "xmlParser"); + } + + #[test] + fn pascal_case() { + let rule = CaseRule { mode: CaseMode::PascalCase, ..CaseRule::new() }; + let ctx = RenameContext::dummy(0); + assert_eq!(rule.apply("some_file-name", &ctx), "SomeFileName"); + assert_eq!(rule.apply("hello world", &ctx), "HelloWorld"); + } + + #[test] + fn snake_case() { + let rule = CaseRule { mode: CaseMode::SnakeCase, ..CaseRule::new() }; + let ctx = RenameContext::dummy(0); + assert_eq!(rule.apply("camelCase", &ctx), "camel_case"); + assert_eq!(rule.apply("XMLParser", &ctx), "xml_parser"); + assert_eq!(rule.apply("some-file name", &ctx), "some_file_name"); + } + + #[test] + fn kebab_case() { + let rule = CaseRule { mode: CaseMode::KebabCase, ..CaseRule::new() }; + let ctx = RenameContext::dummy(0); + assert_eq!(rule.apply("camelCase", &ctx), "camel-case"); + assert_eq!(rule.apply("some_file name", &ctx), "some-file-name"); + } + + #[test] + fn dot_case() { + let rule = CaseRule { mode: CaseMode::DotCase, ..CaseRule::new() }; + let ctx = RenameContext::dummy(0); + assert_eq!(rule.apply("camelCase", &ctx), "camel.case"); + } + + #[test] + fn smart_title() { + let rule = CaseRule { mode: CaseMode::SmartTitle, ..CaseRule::new() }; + let ctx = RenameContext::dummy(0); + assert_eq!(rule.apply("the lord of the rings", &ctx), "The Lord of the Rings"); + assert_eq!(rule.apply("a tale of two cities", &ctx), "A Tale of Two Cities"); + } } diff --git a/crates/nomina-core/src/rules/date.rs b/crates/nomina-core/src/rules/date.rs index 2689a7b..78434d8 100644 --- a/crates/nomina-core/src/rules/date.rs +++ b/crates/nomina-core/src/rules/date.rs @@ -75,7 +75,7 @@ impl DateRule { match self.source { DateSource::Created => context.created, DateSource::Modified => context.modified, - DateSource::Accessed => None, + DateSource::Accessed => context.accessed, DateSource::ExifTaken | DateSource::ExifDigitized => context.date_taken, DateSource::Current => Some(chrono::Utc::now()), } diff --git a/crates/nomina-core/src/rules/extension.rs b/crates/nomina-core/src/rules/extension.rs index 5e6a17b..7f37960 100644 --- a/crates/nomina-core/src/rules/extension.rs +++ b/crates/nomina-core/src/rules/extension.rs @@ -18,6 +18,10 @@ pub struct ExtensionRule { pub mode: ExtensionMode, pub fixed_value: String, pub enabled: bool, + #[serde(default)] + pub mapping: Option>, + #[serde(default)] + pub multi_extension: bool, } impl ExtensionRule { @@ -26,10 +30,21 @@ impl ExtensionRule { mode: ExtensionMode::Same, fixed_value: String::new(), enabled: true, + mapping: None, + multi_extension: false, } } pub fn transform_extension(&self, ext: &str) -> String { + if let Some(ref mappings) = self.mapping { + let ext_lower = ext.to_lowercase(); + for (from, to) in mappings { + if ext_lower == from.to_lowercase() { + return to.clone(); + } + } + } + match self.mode { ExtensionMode::Same => ext.to_string(), ExtensionMode::Lower => ext.to_lowercase(), diff --git a/crates/nomina-core/src/rules/folder_name.rs b/crates/nomina-core/src/rules/folder_name.rs new file mode 100644 index 0000000..c9b224f --- /dev/null +++ b/crates/nomina-core/src/rules/folder_name.rs @@ -0,0 +1,127 @@ +use serde::{Deserialize, Serialize}; + +use crate::{RenameContext, RenameRule}; + +#[derive(Debug, Clone, Default, Serialize, Deserialize, PartialEq)] +pub enum FolderMode { + #[default] + None, + Prefix, + Suffix, + Replace, +} + +fn default_level() -> usize { + 1 +} + +#[derive(Debug, Clone, Serialize, Deserialize)] +pub struct FolderNameRule { + pub mode: FolderMode, + #[serde(default = "default_level")] + pub level: usize, + pub separator: String, + pub enabled: bool, +} + +impl FolderNameRule { + pub fn new() -> Self { + Self { + mode: FolderMode::None, + level: 1, + separator: String::new(), + enabled: true, + } + } + + fn get_folder_name(&self, context: &RenameContext) -> Option { + if self.level == 1 { + if context.parent_folder.is_empty() { + return None; + } + return Some(context.parent_folder.clone()); + } + + context + .path + .ancestors() + .nth(self.level) + .and_then(|p| p.file_name()) + .map(|n| n.to_string_lossy().to_string()) + } +} + +impl RenameRule for FolderNameRule { + fn apply(&self, filename: &str, context: &RenameContext) -> String { + if self.mode == FolderMode::None { + return filename.to_string(); + } + + let folder = match self.get_folder_name(context) { + Some(f) => f, + None => return filename.to_string(), + }; + + match self.mode { + FolderMode::None => unreachable!(), + FolderMode::Prefix => format!("{}{}{}", folder, self.separator, filename), + FolderMode::Suffix => format!("{}{}{}", filename, self.separator, folder), + FolderMode::Replace => folder, + } + } + + fn display_name(&self) -> &str { + "Folder Name" + } + + fn rule_type(&self) -> &str { + "folder_name" + } + + fn is_enabled(&self) -> bool { + self.enabled + } +} + +#[cfg(test)] +mod tests { + use super::*; + use std::path::PathBuf; + + #[test] + fn prefix_with_parent() { + let rule = FolderNameRule { + mode: FolderMode::Prefix, + separator: " - ".into(), + ..FolderNameRule::new() + }; + let mut ctx = RenameContext::dummy(0); + ctx.parent_folder = "Photos".into(); + assert_eq!(rule.apply("sunset", &ctx), "Photos - sunset"); + } + + #[test] + fn suffix_mode() { + let rule = FolderNameRule { + mode: FolderMode::Suffix, + separator: "_".into(), + ..FolderNameRule::new() + }; + let mut ctx = RenameContext::dummy(0); + ctx.parent_folder = "2024".into(); + assert_eq!(rule.apply("photo", &ctx), "photo_2024"); + } + + #[test] + fn grandparent_level() { + let rule = FolderNameRule { + mode: FolderMode::Prefix, + level: 2, + separator: "_".into(), + ..FolderNameRule::new() + }; + let mut ctx = RenameContext::dummy(0); + ctx.path = PathBuf::from("/home/user/docs/sub/file.txt"); + assert_eq!(rule.apply("file", &ctx), "docs_file"); + } +} diff --git a/crates/nomina-core/src/rules/hash.rs b/crates/nomina-core/src/rules/hash.rs new file mode 100644 index 0000000..0f781d2 --- /dev/null +++ b/crates/nomina-core/src/rules/hash.rs @@ -0,0 +1,142 @@ +use serde::{Deserialize, Serialize}; +use sha2::Digest; + +use crate::{RenameContext, RenameRule}; + +#[derive(Debug, Clone, Default, Serialize, Deserialize, PartialEq)] +pub enum HashMode { + #[default] + None, + Prefix, + Suffix, + Replace, +} + +#[derive(Debug, Clone, Default, Serialize, Deserialize, PartialEq)] +pub enum HashAlgorithm { + MD5, + SHA1, + #[default] + SHA256, +} + +#[derive(Debug, Clone, Serialize, Deserialize)] +pub struct HashRule { + pub mode: HashMode, + pub algorithm: HashAlgorithm, + pub length: usize, + pub separator: String, + pub uppercase: bool, + pub enabled: bool, +} + +impl HashRule { + pub fn new() -> Self { + Self { + mode: HashMode::None, + algorithm: HashAlgorithm::SHA256, + length: 0, + separator: "_".into(), + uppercase: false, + enabled: true, + } + } + + fn compute_hash(&self, data: &[u8]) -> String { + let raw = match self.algorithm { + HashAlgorithm::MD5 => { + let mut h = md5::Md5::new(); + h.update(data); + format!("{:x}", h.finalize()) + } + HashAlgorithm::SHA1 => { + let mut h = sha1::Sha1::new(); + h.update(data); + format!("{:x}", h.finalize()) + } + HashAlgorithm::SHA256 => { + let mut h = sha2::Sha256::new(); + h.update(data); + format!("{:x}", h.finalize()) + } + }; + + let truncated = if self.length > 0 && self.length < raw.len() { + &raw[..self.length] + } else { + &raw + }; + + if self.uppercase { + truncated.to_uppercase() + } else { + truncated.to_string() + } + } +} + +impl RenameRule for HashRule { + fn apply(&self, filename: &str, context: &RenameContext) -> String { + if self.mode == HashMode::None { + return filename.to_string(); + } + + let data = match std::fs::read(&context.path) { + Ok(d) => d, + Err(_) => return filename.to_string(), + }; + + let hash = self.compute_hash(&data); + + match self.mode { + HashMode::None => filename.to_string(), + HashMode::Prefix => format!("{}{}{}", hash, self.separator, filename), + HashMode::Suffix => format!("{}{}{}", filename, self.separator, hash), + HashMode::Replace => hash, + } + } + + fn display_name(&self) -> &str { + "Hash" + } + + fn rule_type(&self) -> &str { + "hash" + } + + fn is_enabled(&self) -> bool { + self.enabled + } +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn mode_none_returns_unchanged() { + let rule = HashRule::new(); + let ctx = RenameContext::dummy(0); + assert_eq!(rule.apply("photo.jpg", &ctx), "photo.jpg"); + } + + #[test] + fn file_read_fails_returns_unchanged() { + let mut rule = HashRule::new(); + rule.mode = HashMode::Prefix; + let ctx = RenameContext::dummy(0); + // dummy context points to a nonexistent file, so read fails + assert_eq!(rule.apply("photo.jpg", &ctx), "photo.jpg"); + } + + #[test] + fn suffix_mode_read_fail_unchanged() { + let mut rule = HashRule::new(); + rule.mode = HashMode::Suffix; + rule.algorithm = HashAlgorithm::MD5; + rule.length = 8; + rule.uppercase = true; + let ctx = RenameContext::dummy(0); + assert_eq!(rule.apply("doc.pdf", &ctx), "doc.pdf"); + } +} diff --git a/crates/nomina-core/src/rules/mod.rs b/crates/nomina-core/src/rules/mod.rs index 8355b21..41b4807 100644 --- a/crates/nomina-core/src/rules/mod.rs +++ b/crates/nomina-core/src/rules/mod.rs @@ -7,13 +7,31 @@ pub mod numbering; pub mod date; pub mod move_parts; pub mod extension; +pub mod text_editor; +pub mod hash; +pub mod folder_name; +pub mod transliterate; +pub mod padding; +pub mod truncate; +pub mod randomize; +pub mod swap; +pub mod sanitize; pub use replace::ReplaceRule; pub use self::regex::RegexRule; pub use remove::RemoveRule; -pub use add::AddRule; +pub use add::{AddRule, InsertOp}; pub use case::{CaseMode, CaseRule}; pub use numbering::{NumberBase, NumberMode, NumberingRule}; pub use date::{DateMode, DateRule, DateSource}; pub use move_parts::{MovePartsRule, MoveTarget}; pub use extension::{ExtensionMode, ExtensionRule}; +pub use text_editor::TextEditorRule; +pub use hash::HashRule; +pub use folder_name::FolderNameRule; +pub use transliterate::TransliterateRule; +pub use padding::PaddingRule; +pub use truncate::TruncateRule; +pub use randomize::RandomizeRule; +pub use swap::SwapRule; +pub use sanitize::SanitizeRule; diff --git a/crates/nomina-core/src/rules/move_parts.rs b/crates/nomina-core/src/rules/move_parts.rs index e7f1fbc..cebb632 100644 --- a/crates/nomina-core/src/rules/move_parts.rs +++ b/crates/nomina-core/src/rules/move_parts.rs @@ -1,3 +1,4 @@ +use regex::Regex; use serde::{Deserialize, Serialize}; use crate::{RenameContext, RenameRule}; @@ -10,6 +11,19 @@ pub enum MoveTarget { End, } +#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)] +pub enum SelectionMode { + Chars, + Words, + Regex, +} + +impl Default for SelectionMode { + fn default() -> Self { + SelectionMode::Chars + } +} + #[derive(Debug, Clone, Serialize, Deserialize)] pub struct MovePartsRule { pub source_from: usize, @@ -18,6 +32,16 @@ pub struct MovePartsRule { pub separator: String, pub copy_mode: bool, pub enabled: bool, + #[serde(default)] + pub selection_mode: SelectionMode, + #[serde(default)] + pub regex_pattern: Option, + #[serde(default)] + pub regex_group: usize, + #[serde(default)] + pub swap_with_from: Option, + #[serde(default)] + pub swap_with_length: Option, } impl MovePartsRule { @@ -29,12 +53,62 @@ impl MovePartsRule { separator: String::new(), copy_mode: false, enabled: true, + selection_mode: SelectionMode::Chars, + regex_pattern: None, + regex_group: 0, + swap_with_from: None, + swap_with_length: None, } } } +fn split_words(s: &str) -> Vec<(usize, usize)> { + let mut spans = Vec::new(); + let chars: Vec = s.chars().collect(); + let mut i = 0; + while i < chars.len() { + if chars[i].is_whitespace() || matches!(chars[i], '_' | '-' | '.') { + i += 1; + continue; + } + let start = i; + while i < chars.len() && !chars[i].is_whitespace() && !matches!(chars[i], '_' | '-' | '.') { + i += 1; + } + spans.push((start, i)); + } + spans +} + impl RenameRule for MovePartsRule { fn apply(&self, filename: &str, _context: &RenameContext) -> String { + // swap mode + if let (Some(sw_from), Some(sw_len)) = (self.swap_with_from, self.swap_with_length) { + return self.apply_swap(filename, sw_from, sw_len); + } + + match self.selection_mode { + SelectionMode::Chars => self.apply_chars(filename), + SelectionMode::Words => self.apply_words(filename), + SelectionMode::Regex => self.apply_regex(filename), + } + } + + fn display_name(&self) -> &str { + "Move/Copy" + } + + fn rule_type(&self) -> &str { + "move_parts" + } + + fn is_enabled(&self) -> bool { + self.enabled + } +} + +impl MovePartsRule { + fn apply_chars(&self, filename: &str) -> String { if self.target == MoveTarget::None || self.source_length == 0 { return filename.to_string(); } @@ -59,8 +133,132 @@ impl RenameRule for MovePartsRule { r }; + self.place_at_target(&extracted, &remaining) + } + + fn apply_words(&self, filename: &str) -> String { + if self.target == MoveTarget::None || self.source_length == 0 { + return filename.to_string(); + } + + let words = split_words(filename); + if self.source_from >= words.len() { + return filename.to_string(); + } + + let word_end = (self.source_from + self.source_length).min(words.len()); + let byte_start = words[self.source_from].0; + let byte_end = words[word_end - 1].1; + + let chars: Vec = filename.chars().collect(); + let extracted: String = chars[byte_start..byte_end].iter().collect(); + + let remaining: String = if self.copy_mode { + filename.to_string() + } else { + // remove extracted span, trimming adjacent separators + let mut r = String::new(); + let mut skip_start = byte_start; + let mut skip_end = byte_end; + // extend skip to eat one adjacent separator + if skip_end < chars.len() && (chars[skip_end].is_whitespace() || matches!(chars[skip_end], '_' | '-' | '.')) { + skip_end += 1; + } else if skip_start > 0 && (chars[skip_start - 1].is_whitespace() || matches!(chars[skip_start - 1], '_' | '-' | '.')) { + skip_start -= 1; + } + for (i, c) in chars.iter().enumerate() { + if i < skip_start || i >= skip_end { + r.push(*c); + } + } + r + }; + + self.place_at_target(&extracted, &remaining) + } + + fn apply_regex(&self, filename: &str) -> String { + let pat = match &self.regex_pattern { + Some(p) if !p.is_empty() => p, + _ => return filename.to_string(), + }; + let re = match Regex::new(pat) { + Ok(r) => r, + Err(_) => return filename.to_string(), + }; + let caps = match re.captures(filename) { + Some(c) => c, + None => return filename.to_string(), + }; + let full_match = match caps.get(0) { + Some(m) => m, + None => return filename.to_string(), + }; + let extracted = match caps.get(self.regex_group) { + Some(m) => m.as_str().to_string(), + None => return filename.to_string(), + }; + + if self.target == MoveTarget::None { + return filename.to_string(); + } + + // remove the full match from filename + let remaining = if self.copy_mode { + filename.to_string() + } else { + let mut r = String::new(); + r.push_str(&filename[..full_match.start()]); + r.push_str(&filename[full_match.end()..]); + r + }; + + self.place_at_target(&extracted, &remaining) + } + + fn apply_swap(&self, filename: &str, sw_from: usize, sw_len: usize) -> String { + let chars: Vec = filename.chars().collect(); + let len = chars.len(); + + let a_start = self.source_from; + let a_end = (a_start + self.source_length).min(len); + let b_start = sw_from; + let b_end = (b_start + sw_len).min(len); + + if a_start >= len || b_start >= len || self.source_length == 0 || sw_len == 0 { + return filename.to_string(); + } + + // ranges must not overlap + if a_start < b_end && b_start < a_end { + return filename.to_string(); + } + + // ensure first < second + let (r1_start, r1_end, r2_start, r2_end) = if a_start < b_start { + (a_start, a_end, b_start, b_end) + } else { + (b_start, b_end, a_start, a_end) + }; + + let part1: String = chars[r1_start..r1_end].iter().collect(); + let part2: String = chars[r2_start..r2_end].iter().collect(); + + let mut result = String::new(); + let before: String = chars[..r1_start].iter().collect(); + let mid: String = chars[r1_end..r2_start].iter().collect(); + let after: String = chars[r2_end..].iter().collect(); + result.push_str(&before); + result.push_str(&part2); + result.push_str(&mid); + result.push_str(&part1); + result.push_str(&after); + result + } + + fn place_at_target(&self, extracted: &str, remaining: &str) -> String { match &self.target { - MoveTarget::None => filename.to_string(), + MoveTarget::None => remaining.to_string(), MoveTarget::Start => { if self.separator.is_empty() { format!("{}{}", extracted, remaining) @@ -84,18 +282,6 @@ impl RenameRule for MovePartsRule { } } } - - fn display_name(&self) -> &str { - "Move/Copy" - } - - fn rule_type(&self) -> &str { - "move_parts" - } - - fn is_enabled(&self) -> bool { - self.enabled - } } #[cfg(test)] diff --git a/crates/nomina-core/src/rules/numbering.rs b/crates/nomina-core/src/rules/numbering.rs index 8824772..41625dc 100644 --- a/crates/nomina-core/src/rules/numbering.rs +++ b/crates/nomina-core/src/rules/numbering.rs @@ -18,6 +18,7 @@ pub enum NumberBase { Octal, Binary, Alpha, + Roman, } #[derive(Debug, Clone, Serialize, Deserialize)] @@ -32,6 +33,29 @@ pub struct NumberingRule { pub per_folder: bool, pub insert_at: usize, pub enabled: bool, + #[serde(default)] + pub custom_format: Option, + #[serde(default)] + pub reverse: bool, +} + +fn to_roman(mut n: i64) -> Option { + if n <= 0 { + return None; + } + let table = [ + (1000, "M"), (900, "CM"), (500, "D"), (400, "CD"), + (100, "C"), (90, "XC"), (50, "L"), (40, "XL"), + (10, "X"), (9, "IX"), (5, "V"), (4, "IV"), (1, "I"), + ]; + let mut result = String::new(); + for &(val, sym) in &table { + while n >= val { + result.push_str(sym); + n -= val; + } + } + Some(result) } impl NumberingRule { @@ -47,6 +71,8 @@ impl NumberingRule { per_folder: false, insert_at: 0, enabled: true, + custom_format: None, + reverse: false, } } @@ -71,6 +97,9 @@ impl NumberingRule { } result } + NumberBase::Roman => { + return to_roman(n).unwrap_or_else(|| format!("{}", n)); + } }; if self.base != NumberBase::Alpha && s.len() < self.padding { @@ -87,7 +116,11 @@ impl RenameRule for NumberingRule { return filename.to_string(); } - let idx = context.index as i64; + let idx = if self.reverse { + context.total as i64 - 1 - context.index as i64 + } else { + context.index as i64 + }; let n = if self.break_at > 0 { self.start + (idx % self.break_at as i64) * self.increment } else { @@ -95,6 +128,10 @@ impl RenameRule for NumberingRule { }; let num = self.format_number(n); + if let Some(ref fmt) = self.custom_format { + return fmt.replace("{n}", &num); + } + match self.mode { NumberMode::None => filename.to_string(), NumberMode::Prefix => format!("{}{}{}", num, self.separator, filename), @@ -173,4 +210,63 @@ mod tests { let ctx = RenameContext::dummy(0); assert_eq!(rule.apply("file", &ctx), "file_a"); } + + #[test] + fn roman_numbering() { + let rule = NumberingRule { + mode: NumberMode::Prefix, + start: 1, + increment: 1, + separator: "_".into(), + base: NumberBase::Roman, + ..NumberingRule::new() + }; + assert_eq!(rule.apply("track", &RenameContext::dummy(0)), "I_track"); + assert_eq!(rule.apply("track", &RenameContext::dummy(3)), "IV_track"); + } + + #[test] + fn roman_zero_falls_back() { + let rule = NumberingRule { + mode: NumberMode::Prefix, + start: 0, + increment: 1, + separator: "_".into(), + base: NumberBase::Roman, + ..NumberingRule::new() + }; + assert_eq!(rule.apply("x", &RenameContext::dummy(0)), "0_x"); + } + + #[test] + fn custom_format() { + let rule = NumberingRule { + mode: NumberMode::Prefix, + start: 1, + increment: 1, + padding: 3, + custom_format: Some("File_{n}".into()), + ..NumberingRule::new() + }; + let ctx = RenameContext::dummy(2); + assert_eq!(rule.apply("ignored", &ctx), "File_003"); + } + + #[test] + fn reverse_numbering() { + let rule = NumberingRule { + mode: NumberMode::Prefix, + start: 1, + increment: 1, + padding: 1, + separator: "_".into(), + reverse: true, + ..NumberingRule::new() + }; + let mut ctx = RenameContext::dummy(0); + ctx.total = 5; + assert_eq!(rule.apply("a", &ctx), "5_a"); + ctx.index = 4; + assert_eq!(rule.apply("a", &ctx), "1_a"); + } } diff --git a/crates/nomina-core/src/rules/padding.rs b/crates/nomina-core/src/rules/padding.rs new file mode 100644 index 0000000..1eff195 --- /dev/null +++ b/crates/nomina-core/src/rules/padding.rs @@ -0,0 +1,124 @@ +use regex::Regex; +use serde::{Deserialize, Serialize}; + +use crate::{RenameContext, RenameRule}; + +fn default_width() -> usize { + 3 +} + +fn default_pad_char() -> char { + '0' +} + +#[derive(Debug, Clone, Serialize, Deserialize)] +pub struct PaddingRule { + #[serde(default = "default_width")] + pub width: usize, + #[serde(default = "default_pad_char")] + pub pad_char: char, + #[serde(default)] + pub pad_all: bool, + pub enabled: bool, +} + +impl PaddingRule { + pub fn new() -> Self { + Self { + width: default_width(), + pad_char: default_pad_char(), + pad_all: true, + enabled: true, + } + } +} + +impl RenameRule for PaddingRule { + fn apply(&self, filename: &str, _context: &RenameContext) -> String { + let re = match Regex::new(r"\d+") { + Ok(r) => r, + Err(_) => return filename.to_string(), + }; + + let mut result = String::new(); + let mut last = 0; + let mut count = 0; + + for m in re.find_iter(filename) { + count += 1; + result.push_str(&filename[last..m.start()]); + + if self.pad_all || count == 1 { + let num_str = m.as_str(); + if num_str.len() < self.width { + let pad_count = self.width - num_str.len(); + for _ in 0..pad_count { + result.push(self.pad_char); + } + } + result.push_str(num_str); + } else { + result.push_str(m.as_str()); + } + + last = m.end(); + } + + result.push_str(&filename[last..]); + result + } + + fn display_name(&self) -> &str { + "Padding" + } + + fn rule_type(&self) -> &str { + "padding" + } + + fn is_enabled(&self) -> bool { + self.enabled + } +} + +#[cfg(test)] +mod tests { + use super::*; + + fn rule(width: usize, pad_all: bool) -> PaddingRule { + PaddingRule { + width, + pad_char: '0', + pad_all, + enabled: true, + } + } + + #[test] + fn pad_single_number() { + let r = rule(3, true); + let ctx = RenameContext::dummy(0); + assert_eq!(r.apply("file1", &ctx), "file001"); + } + + #[test] + fn pad_multiple_numbers() { + let r = rule(3, true); + let ctx = RenameContext::dummy(0); + assert_eq!(r.apply("img2_v10", &ctx), "img002_v010"); + } + + #[test] + fn pad_first_only() { + let r = rule(3, false); + let ctx = RenameContext::dummy(0); + assert_eq!(r.apply("file1", &ctx), "file001"); + } + + #[test] + fn already_wide_enough() { + let r = rule(3, true); + let ctx = RenameContext::dummy(0); + assert_eq!(r.apply("file100", &ctx), "file100"); + } +} diff --git a/crates/nomina-core/src/rules/randomize.rs b/crates/nomina-core/src/rules/randomize.rs new file mode 100644 index 0000000..86332bb --- /dev/null +++ b/crates/nomina-core/src/rules/randomize.rs @@ -0,0 +1,123 @@ +use rand::Rng; +use serde::{Deserialize, Serialize}; +use uuid::Uuid; + +use crate::{RenameContext, RenameRule}; + +#[derive(Debug, Clone, Serialize, Deserialize, Default, PartialEq)] +pub enum RandomMode { + #[default] + Replace, + Prefix, + Suffix, +} + +#[derive(Debug, Clone, Serialize, Deserialize, Default, PartialEq)] +pub enum RandomFormat { + #[default] + Hex, + Alpha, + AlphaNum, + UUID, +} + +fn default_length() -> usize { + 8 +} + +#[derive(Debug, Clone, Serialize, Deserialize)] +pub struct RandomizeRule { + pub mode: RandomMode, + pub format: RandomFormat, + #[serde(default = "default_length")] + pub length: usize, + pub separator: String, + pub enabled: bool, +} + +impl RandomizeRule { + pub fn new() -> Self { + Self { + mode: RandomMode::Replace, + format: RandomFormat::Hex, + length: 8, + separator: String::from("_"), + enabled: true, + } + } + + fn generate_random(&self) -> String { + if self.format == RandomFormat::UUID { + return Uuid::new_v4().to_string(); + } + + let mut rng = rand::thread_rng(); + let chars: &[u8] = match self.format { + RandomFormat::Hex => b"0123456789abcdef", + RandomFormat::Alpha => b"abcdefghijklmnopqrstuvwxyz", + RandomFormat::AlphaNum => b"abcdefghijklmnopqrstuvwxyz0123456789", + RandomFormat::UUID => unreachable!(), + }; + + (0..self.length) + .map(|_| chars[rng.gen_range(0..chars.len())] as char) + .collect() + } +} + +impl RenameRule for RandomizeRule { + fn apply(&self, filename: &str, _context: &RenameContext) -> String { + let random = self.generate_random(); + match self.mode { + RandomMode::Replace => random, + RandomMode::Prefix => format!("{}{}{}", random, self.separator, filename), + RandomMode::Suffix => format!("{}{}{}", filename, self.separator, random), + } + } + + fn display_name(&self) -> &str { + "Randomize" + } + + fn rule_type(&self) -> &str { + "randomize" + } + + fn is_enabled(&self) -> bool { + self.enabled + } +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn replace_mode_length() { + let rule = RandomizeRule { + mode: RandomMode::Replace, + format: RandomFormat::Hex, + length: 12, + separator: String::new(), + enabled: true, + }; + let ctx = RenameContext::dummy(0); + let result = rule.apply("anything", &ctx); + assert_eq!(result.len(), 12); + } + + #[test] + fn prefix_mode_contains_filename() { + let rule = RandomizeRule { + mode: RandomMode::Prefix, + format: RandomFormat::Alpha, + length: 6, + separator: "-".into(), + enabled: true, + }; + let ctx = RenameContext::dummy(0); + let result = rule.apply("photo", &ctx); + assert!(result.contains("photo")); + assert!(result.ends_with("photo")); + } +} diff --git a/crates/nomina-core/src/rules/regex.rs b/crates/nomina-core/src/rules/regex.rs index 80cfdf2..d6cb1a9 100644 --- a/crates/nomina-core/src/rules/regex.rs +++ b/crates/nomina-core/src/rules/regex.rs @@ -9,6 +9,8 @@ pub struct RegexRule { pub replace_with: String, pub case_insensitive: bool, pub enabled: bool, + #[serde(default)] + pub match_limit: Option, } impl RegexRule { @@ -18,9 +20,19 @@ impl RegexRule { replace_with: String::new(), case_insensitive: false, enabled: true, + match_limit: None, } } + pub fn validate(&self) -> Result<(), String> { + let pat = if self.case_insensitive { + format!("(?i){}", self.pattern) + } else { + self.pattern.clone() + }; + Regex::new(&pat).map(|_| ()).map_err(|e| e.to_string()) + } + fn build_regex(&self) -> std::result::Result { let pat = if self.case_insensitive { format!("(?i){}", self.pattern) @@ -40,7 +52,10 @@ impl RenameRule for RegexRule { return filename.to_string(); } match self.build_regex() { - Ok(re) => re.replace_all(filename, self.replace_with.as_str()).into_owned(), + Ok(re) => match self.match_limit { + Some(n) => re.replacen(filename, n, self.replace_with.as_str()).into_owned(), + None => re.replace_all(filename, self.replace_with.as_str()).into_owned(), + }, Err(_) => filename.to_string(), } } @@ -69,6 +84,7 @@ mod tests { replace_with: "NUM".into(), case_insensitive: false, enabled: true, + match_limit: None, }; let ctx = RenameContext::dummy(0); assert_eq!(rule.apply("file123", &ctx), "fileNUM"); @@ -81,6 +97,7 @@ mod tests { replace_with: "${2}_${1}".into(), case_insensitive: false, enabled: true, + match_limit: None, }; let ctx = RenameContext::dummy(0); assert_eq!(rule.apply("hello-world", &ctx), "world_hello"); @@ -93,6 +110,7 @@ mod tests { replace_with: "x".into(), case_insensitive: false, enabled: true, + match_limit: None, }; let ctx = RenameContext::dummy(0); assert_eq!(rule.apply("test", &ctx), "test"); diff --git a/crates/nomina-core/src/rules/remove.rs b/crates/nomina-core/src/rules/remove.rs index e5045da..e1f83a3 100644 --- a/crates/nomina-core/src/rules/remove.rs +++ b/crates/nomina-core/src/rules/remove.rs @@ -1,9 +1,12 @@ +use regex::Regex; use serde::{Deserialize, Serialize}; +use unicode_normalization::UnicodeNormalization; use crate::{RenameContext, RenameRule}; -#[derive(Debug, Clone, Serialize, Deserialize)] +#[derive(Debug, Clone, Serialize, Deserialize, Default)] pub enum RemoveMode { + #[default] Chars, Words, } @@ -29,16 +32,78 @@ impl Default for TrimOptions { } } +const WORD_SEPARATORS: &[char] = &[' ', '_', '-', '.']; + +fn split_words(s: &str) -> Vec<(String, String)> { + // Returns pairs of (word, trailing_separator). + // The last word may have an empty separator. + let mut words: Vec<(String, String)> = Vec::new(); + let mut word = String::new(); + let mut sep = String::new(); + let mut in_sep = false; + + for c in s.chars() { + if WORD_SEPARATORS.contains(&c) { + if !in_sep && !word.is_empty() { + in_sep = true; + sep.push(c); + } else if in_sep { + sep.push(c); + } else { + // leading separator before any word + sep.push(c); + } + } else { + if in_sep { + words.push((std::mem::take(&mut word), std::mem::take(&mut sep))); + in_sep = false; + } else if !sep.is_empty() { + // leading separators - attach to first word + words.push((String::new(), std::mem::take(&mut sep))); + } + word.push(c); + } + } + // push whatever is left + if !word.is_empty() || !sep.is_empty() { + words.push((word, sep)); + } + words +} + +fn join_words(parts: &[(String, String)]) -> String { + let mut out = String::new(); + for (w, s) in parts { + out.push_str(w); + out.push_str(s); + } + out +} + +fn strip_accents(s: &str) -> String { + s.nfkd() + .filter(|c| !unicode_normalization::char::is_combining_mark(*c)) + .collect() +} + #[derive(Debug, Clone, Serialize, Deserialize)] pub struct RemoveRule { pub first_n: usize, pub last_n: usize, pub from: usize, pub to: usize, + #[serde(default)] pub mode: RemoveMode, pub crop_before: Option, pub crop_after: Option, + #[serde(default)] pub trim: TrimOptions, + #[serde(default)] + pub collapse_chars: Option, + #[serde(default)] + pub remove_pattern: Option, + #[serde(default)] + pub allow_empty: bool, pub enabled: bool, } @@ -53,6 +118,9 @@ impl RemoveRule { crop_before: None, crop_after: None, trim: TrimOptions::default(), + collapse_chars: None, + remove_pattern: None, + allow_empty: false, enabled: true, } } @@ -78,30 +146,92 @@ impl RenameRule for RemoveRule { let s: String = result.iter().collect(); let mut result = s; - // remove first N - if self.first_n > 0 && self.first_n < result.chars().count() { - result = result.chars().skip(self.first_n).collect(); - } + match self.mode { + RemoveMode::Chars => { + if self.first_n > 0 { + let count = result.chars().count(); + if self.first_n < count || self.allow_empty { + result = result.chars().skip(self.first_n).collect(); + } + } - // remove last N - if self.last_n > 0 { - let count = result.chars().count(); - if self.last_n < count { - result = result.chars().take(count - self.last_n).collect(); - } - } + if self.last_n > 0 { + let count = result.chars().count(); + if self.last_n < count || self.allow_empty { + let take = count.saturating_sub(self.last_n); + result = result.chars().take(take).collect(); + } + } - // remove from..to range - if self.to > self.from && self.from < result.chars().count() { - let chars: Vec = result.chars().collect(); - let to = self.to.min(chars.len()); - let mut s = String::new(); - for (i, c) in chars.iter().enumerate() { - if i < self.from || i >= to { - s.push(*c); + if self.to > self.from && self.from < result.chars().count() { + let chars: Vec = result.chars().collect(); + let to = self.to.min(chars.len()); + let mut s = String::new(); + for (i, c) in chars.iter().enumerate() { + if i < self.from || i >= to { + s.push(*c); + } + } + result = s; } } - result = s; + RemoveMode::Words => { + let mut parts = split_words(&result); + + // count only non-empty words + let word_indices: Vec = parts + .iter() + .enumerate() + .filter(|(_, (w, _))| !w.is_empty()) + .map(|(i, _)| i) + .collect(); + + if self.first_n > 0 && (self.first_n < word_indices.len() || self.allow_empty) { + // remove first N words and their trailing separators + for &idx in word_indices.iter().take(self.first_n) { + parts[idx].0.clear(); + parts[idx].1.clear(); + } + } + + // recompute after removal + let word_indices: Vec = parts + .iter() + .enumerate() + .filter(|(_, (w, _))| !w.is_empty()) + .map(|(i, _)| i) + .collect(); + + if self.last_n > 0 && (self.last_n < word_indices.len() || self.allow_empty) { + let start = word_indices.len() - self.last_n; + for &idx in &word_indices[start..] { + // remove separator before this word (from previous part) + if idx > 0 { + parts[idx - 1].1.clear(); + } + parts[idx].0.clear(); + parts[idx].1.clear(); + } + } + + // recompute for from/to + let word_indices: Vec = parts + .iter() + .enumerate() + .filter(|(_, (w, _))| !w.is_empty()) + .map(|(i, _)| i) + .collect(); + + if self.to > self.from && self.from < word_indices.len() { + let to = self.to.min(word_indices.len()); + for &idx in &word_indices[self.from..to] { + parts[idx].0.clear(); + parts[idx].1.clear(); + } + } + + result = join_words(&parts); + } } // trim options @@ -120,6 +250,43 @@ impl RenameRule for RemoveRule { .filter(|c| c.is_alphanumeric() || c.is_whitespace() || *c == '-' || *c == '_' || *c == '.') .collect(); } + if self.trim.accents { + result = strip_accents(&result); + } + + // collapse runs of specified chars + if let Some(ref chars) = self.collapse_chars { + let set: Vec = chars.chars().collect(); + let mut collapsed = String::new(); + let mut prev_was_target = false; + let mut prev_char: Option = None; + for c in result.chars() { + if set.contains(&c) { + if prev_was_target && prev_char == Some(c) { + continue; + } + if prev_was_target && set.len() > 1 { + // different target char in a run - still collapse + continue; + } + prev_was_target = true; + prev_char = Some(c); + collapsed.push(c); + } else { + prev_was_target = false; + prev_char = Some(c); + collapsed.push(c); + } + } + result = collapsed; + } + + // remove regex pattern matches + if let Some(ref pat) = self.remove_pattern { + if let Ok(re) = Regex::new(pat) { + result = re.replace_all(&result, "").into_owned(); + } + } result } @@ -170,4 +337,92 @@ mod tests { let ctx = RenameContext::dummy(0); assert_eq!(rule.apply("prefix-content", &ctx), "-content"); } + + #[test] + fn words_first_n() { + let rule = RemoveRule { + first_n: 1, + mode: RemoveMode::Words, + ..RemoveRule::new() + }; + let ctx = RenameContext::dummy(0); + assert_eq!(rule.apply("hello_world-foo bar", &ctx), "world-foo bar"); + } + + #[test] + fn words_last_n() { + let rule = RemoveRule { + last_n: 1, + mode: RemoveMode::Words, + ..RemoveRule::new() + }; + let ctx = RenameContext::dummy(0); + assert_eq!(rule.apply("hello_world_end", &ctx), "hello_world"); + } + + #[test] + fn words_from_to() { + let rule = RemoveRule { + from: 1, + to: 2, + mode: RemoveMode::Words, + ..RemoveRule::new() + }; + let ctx = RenameContext::dummy(0); + assert_eq!(rule.apply("aaa-bbb-ccc", &ctx), "aaa-ccc"); + } + + #[test] + fn trim_accents() { + let rule = RemoveRule { + trim: TrimOptions { + accents: true, + ..TrimOptions::default() + }, + ..RemoveRule::new() + }; + let ctx = RenameContext::dummy(0); + assert_eq!(rule.apply("cafe\u{0301}", &ctx), "cafe"); + assert_eq!(rule.apply("\u{00e9}t\u{00e9}", &ctx), "ete"); + } + + #[test] + fn collapse_chars_single() { + let rule = RemoveRule { + collapse_chars: Some("-".into()), + ..RemoveRule::new() + }; + let ctx = RenameContext::dummy(0); + assert_eq!(rule.apply("file---name", &ctx), "file-name"); + } + + #[test] + fn collapse_chars_multi() { + let rule = RemoveRule { + collapse_chars: Some("-_".into()), + ..RemoveRule::new() + }; + let ctx = RenameContext::dummy(0); + assert_eq!(rule.apply("file-_-name", &ctx), "file-name"); + } + + #[test] + fn remove_pattern_basic() { + let rule = RemoveRule { + remove_pattern: Some(r"\d+".into()), + ..RemoveRule::new() + }; + let ctx = RenameContext::dummy(0); + assert_eq!(rule.apply("file123name456", &ctx), "filename"); + } + + #[test] + fn remove_pattern_complex() { + let rule = RemoveRule { + remove_pattern: Some(r"\s*\(copy\)".into()), + ..RemoveRule::new() + }; + let ctx = RenameContext::dummy(0); + assert_eq!(rule.apply("document (copy)", &ctx), "document"); + } } diff --git a/crates/nomina-core/src/rules/replace.rs b/crates/nomina-core/src/rules/replace.rs index 33d3410..7426aa9 100644 --- a/crates/nomina-core/src/rules/replace.rs +++ b/crates/nomina-core/src/rules/replace.rs @@ -1,3 +1,4 @@ +use regex::Regex; use serde::{Deserialize, Serialize}; use crate::{RenameContext, RenameRule}; @@ -9,6 +10,14 @@ pub struct ReplaceRule { pub match_case: bool, pub first_only: bool, pub enabled: bool, + #[serde(default)] + pub use_regex: bool, + #[serde(default)] + pub scope_start: Option, + #[serde(default)] + pub scope_end: Option, + #[serde(default)] + pub occurrence: Option, } impl ReplaceRule { @@ -19,37 +28,116 @@ impl ReplaceRule { match_case: true, first_only: false, enabled: true, + use_regex: false, + scope_start: None, + scope_end: None, + occurrence: None, } } -} -impl RenameRule for ReplaceRule { - fn apply(&self, filename: &str, _context: &RenameContext) -> String { - if self.search.is_empty() { - return filename.to_string(); + fn effective_limit(&self) -> Option { + if let Some(n) = self.occurrence { + Some(n) + } else if self.first_only { + Some(1) + } else { + None } + } + + fn replace_inner(&self, text: &str) -> String { + if self.use_regex { + self.replace_regex(text) + } else { + self.replace_literal(text) + } + } + + fn replace_regex(&self, text: &str) -> String { + let pat = if self.match_case { + self.search.clone() + } else { + format!("(?i){}", self.search) + }; + let re = match Regex::new(&pat) { + Ok(r) => r, + Err(_) => return text.to_string(), + }; + + let limit = self.effective_limit(); + let mut result = String::new(); + let mut last = 0; + let mut count = 0; + + for m in re.find_iter(text) { + count += 1; + match limit { + Some(n) if count < n => { + result.push_str(&text[last..m.end()]); + last = m.end(); + } + Some(n) if count == n => { + result.push_str(&text[last..m.start()]); + let replaced = re.replace(m.as_str(), self.replace_with.as_str()); + result.push_str(&replaced); + result.push_str(&text[m.end()..]); + return result; + } + Some(_) => { + // past target, shouldn't happen since we return above + break; + } + None => { + result.push_str(&text[last..m.start()]); + let replaced = re.replace(m.as_str(), self.replace_with.as_str()); + result.push_str(&replaced); + last = m.end(); + } + } + } + result.push_str(&text[last..]); + result + } + + fn replace_literal(&self, text: &str) -> String { + let limit = self.effective_limit(); if self.match_case { - if self.first_only { - filename.replacen(&self.search, &self.replace_with, 1) - } else { - filename.replace(&self.search, &self.replace_with) + match limit { + Some(n) => self.replace_nth_literal(text, &self.search, n), + None => text.replace(&self.search, &self.replace_with), } } else { let lower_search = self.search.to_lowercase(); let mut result = String::new(); - let mut remaining = filename; + let mut remaining = text; + let mut count = 0; loop { let lower_remaining = remaining.to_lowercase(); match lower_remaining.find(&lower_search) { Some(pos) => { - result.push_str(&remaining[..pos]); - result.push_str(&self.replace_with); - remaining = &remaining[pos + self.search.len()..]; - if self.first_only { - result.push_str(remaining); - break; + count += 1; + match limit { + Some(n) if count < n => { + result.push_str(&remaining[..pos + self.search.len()]); + remaining = &remaining[pos + self.search.len()..]; + } + Some(n) if count == n => { + result.push_str(&remaining[..pos]); + result.push_str(&self.replace_with); + result.push_str(&remaining[pos + self.search.len()..]); + return result; + } + Some(_) => { + result.push_str(remaining); + return result; + } + None => { + result.push_str(&remaining[..pos]); + result.push_str(&self.replace_with); + remaining = &remaining[pos + self.search.len()..]; + } } } None => { @@ -62,6 +150,58 @@ impl RenameRule for ReplaceRule { } } + fn replace_nth_literal(&self, text: &str, search: &str, n: usize) -> String { + let mut result = String::new(); + let mut remaining = text; + let mut count = 0; + + loop { + match remaining.find(search) { + Some(pos) => { + count += 1; + if count == n { + result.push_str(&remaining[..pos]); + result.push_str(&self.replace_with); + result.push_str(&remaining[pos + search.len()..]); + return result; + } + result.push_str(&remaining[..pos + search.len()]); + remaining = &remaining[pos + search.len()..]; + } + None => { + result.push_str(remaining); + break; + } + } + } + result + } +} + +impl RenameRule for ReplaceRule { + fn apply(&self, filename: &str, _context: &RenameContext) -> String { + if self.search.is_empty() { + return filename.to_string(); + } + + let has_scope = self.scope_start.is_some() || self.scope_end.is_some(); + if has_scope { + let chars: Vec = filename.chars().collect(); + let start = self.scope_start.unwrap_or(0).min(chars.len()); + let end = self.scope_end.unwrap_or(chars.len()).min(chars.len()); + if start >= end { + return filename.to_string(); + } + let prefix: String = chars[..start].iter().collect(); + let scoped: String = chars[start..end].iter().collect(); + let suffix: String = chars[end..].iter().collect(); + let replaced = self.replace_inner(&scoped); + format!("{}{}{}", prefix, replaced, suffix) + } else { + self.replace_inner(filename) + } + } + fn display_name(&self) -> &str { "Replace" } @@ -79,6 +219,20 @@ impl RenameRule for ReplaceRule { mod tests { use super::*; + fn base_rule(search: &str, replace_with: &str) -> ReplaceRule { + ReplaceRule { + search: search.into(), + replace_with: replace_with.into(), + match_case: true, + first_only: false, + enabled: true, + use_regex: false, + scope_start: None, + scope_end: None, + occurrence: None, + } + } + #[test] fn basic_replace() { let rule = ReplaceRule { @@ -87,6 +241,10 @@ mod tests { match_case: true, first_only: false, enabled: true, + use_regex: false, + scope_start: None, + scope_end: None, + occurrence: None, }; let ctx = RenameContext::dummy(0); assert_eq!(rule.apply("IMG_001", &ctx), "photo-001"); @@ -100,6 +258,10 @@ mod tests { match_case: false, first_only: false, enabled: true, + use_regex: false, + scope_start: None, + scope_end: None, + occurrence: None, }; let ctx = RenameContext::dummy(0); assert_eq!(rule.apply("IMG_001", &ctx), "photo-001"); @@ -113,6 +275,10 @@ mod tests { match_case: true, first_only: true, enabled: true, + use_regex: false, + scope_start: None, + scope_end: None, + occurrence: None, }; let ctx = RenameContext::dummy(0); assert_eq!(rule.apply("aaa", &ctx), "baa"); @@ -126,8 +292,120 @@ mod tests { match_case: true, first_only: false, enabled: true, + use_regex: false, + scope_start: None, + scope_end: None, + occurrence: None, }; let ctx = RenameContext::dummy(0); assert_eq!(rule.apply("test", &ctx), "test"); } + + #[test] + fn regex_replace() { + let mut rule = base_rule(r"\d+", "NUM"); + rule.use_regex = true; + let ctx = RenameContext::dummy(0); + assert_eq!(rule.apply("file123-456", &ctx), "fileNUM-NUM"); + } + + #[test] + fn regex_first_only() { + let mut rule = base_rule(r"\d+", "NUM"); + rule.use_regex = true; + rule.first_only = true; + let ctx = RenameContext::dummy(0); + assert_eq!(rule.apply("file123-456", &ctx), "fileNUM-456"); + } + + #[test] + fn regex_invalid_pattern() { + let mut rule = base_rule(r"[invalid", "x"); + rule.use_regex = true; + let ctx = RenameContext::dummy(0); + assert_eq!(rule.apply("test", &ctx), "test"); + } + + #[test] + fn regex_case_insensitive() { + let mut rule = base_rule("hello", "HI"); + rule.use_regex = true; + rule.match_case = false; + let ctx = RenameContext::dummy(0); + assert_eq!(rule.apply("HELLO world", &ctx), "HI world"); + } + + #[test] + fn scope_basic() { + let mut rule = base_rule("a", "X"); + rule.scope_start = Some(2); + rule.scope_end = Some(5); + let ctx = RenameContext::dummy(0); + // "abcabc" - scope chars 2..5 = "cab" - replace 'a' -> 'X' = "cXb" + assert_eq!(rule.apply("abcabc", &ctx), "abcXbc"); + } + + #[test] + fn scope_no_start() { + let mut rule = base_rule("a", "X"); + rule.scope_end = Some(3); + let ctx = RenameContext::dummy(0); + // scope 0..3 = "abc" -> "Xbc", rest = "abc" + assert_eq!(rule.apply("abcabc", &ctx), "Xbcabc"); + } + + #[test] + fn scope_no_end() { + let mut rule = base_rule("a", "X"); + rule.scope_start = Some(3); + let ctx = RenameContext::dummy(0); + // prefix = "abc", scope 3.. = "abc" -> "Xbc" + assert_eq!(rule.apply("abcabc", &ctx), "abcXbc"); + } + + #[test] + fn occurrence_nth() { + let mut rule = base_rule("a", "X"); + rule.occurrence = Some(2); + let ctx = RenameContext::dummy(0); + assert_eq!(rule.apply("ababa", &ctx), "abXba"); + } + + #[test] + fn occurrence_overrides_first_only() { + let mut rule = base_rule("a", "X"); + rule.first_only = true; + rule.occurrence = Some(3); + let ctx = RenameContext::dummy(0); + // occurrence takes priority - replace 3rd 'a' + assert_eq!(rule.apply("ababa", &ctx), "ababX"); + } + + #[test] + fn occurrence_beyond_matches() { + let mut rule = base_rule("a", "X"); + rule.occurrence = Some(10); + let ctx = RenameContext::dummy(0); + assert_eq!(rule.apply("aaa", &ctx), "aaa"); + } + + #[test] + fn scope_with_regex() { + let mut rule = base_rule(r"\d+", "N"); + rule.use_regex = true; + rule.scope_start = Some(0); + rule.scope_end = Some(5); + let ctx = RenameContext::dummy(0); + // "abc12345" scope 0..5 = "abc12" -> "abcN", suffix = "345" + assert_eq!(rule.apply("abc12345", &ctx), "abcN345"); + } + + #[test] + fn occurrence_with_case_insensitive() { + let mut rule = base_rule("a", "X"); + rule.match_case = false; + rule.occurrence = Some(2); + let ctx = RenameContext::dummy(0); + assert_eq!(rule.apply("aAbAa", &ctx), "aXbAa"); + } } diff --git a/crates/nomina-core/src/rules/sanitize.rs b/crates/nomina-core/src/rules/sanitize.rs new file mode 100644 index 0000000..7035755 --- /dev/null +++ b/crates/nomina-core/src/rules/sanitize.rs @@ -0,0 +1,324 @@ +use serde::{Deserialize, Serialize}; +use unicode_normalization::UnicodeNormalization; + +use crate::{RenameContext, RenameRule}; + +#[derive(Debug, Clone, Serialize, Deserialize, Default, PartialEq)] +pub enum SpaceMode { + #[default] + None, + Underscores, + Dashes, + Dots, +} + +#[derive(Debug, Clone, Serialize, Deserialize)] +pub struct SanitizeRule { + #[serde(default = "default_true")] + pub illegal_chars: bool, + #[serde(default)] + pub spaces_to: SpaceMode, + #[serde(default)] + pub normalize_unicode: bool, + #[serde(default)] + pub strip_zero_width: bool, + #[serde(default)] + pub strip_diacritics: bool, + #[serde(default)] + pub collapse_whitespace: bool, + #[serde(default = "default_true")] + pub trim_dots_spaces: bool, + pub enabled: bool, +} + +fn default_true() -> bool { + true +} + +const ZERO_WIDTH: &[char] = &[ + '\u{200B}', // zero-width space + '\u{200C}', // zero-width non-joiner + '\u{200D}', // zero-width joiner + '\u{FEFF}', // BOM / zero-width no-break space + '\u{00AD}', // soft hyphen +]; + +impl SanitizeRule { + pub fn new() -> Self { + Self { + illegal_chars: true, + spaces_to: SpaceMode::None, + normalize_unicode: false, + strip_zero_width: false, + strip_diacritics: false, + collapse_whitespace: false, + trim_dots_spaces: true, + enabled: true, + } + } + + fn is_illegal(c: char) -> bool { + matches!(c, '<' | '>' | ':' | '"' | '/' | '\\' | '|' | '?' | '*') + || (c as u32) <= 0x1F + } +} + +impl RenameRule for SanitizeRule { + fn apply(&self, filename: &str, _context: &RenameContext) -> String { + let mut result = filename.to_string(); + + if self.illegal_chars { + result = result.chars().filter(|c| !Self::is_illegal(*c)).collect(); + } + + if self.spaces_to != SpaceMode::None { + let replacement = match self.spaces_to { + SpaceMode::Underscores => '_', + SpaceMode::Dashes => '-', + SpaceMode::Dots => '.', + SpaceMode::None => unreachable!(), + }; + result = result.chars().map(|c| if c == ' ' { replacement } else { c }).collect(); + } + + if self.normalize_unicode { + result = result.nfc().collect::(); + } + + if self.strip_zero_width { + result = result.chars().filter(|c| !ZERO_WIDTH.contains(c)).collect(); + } + + if self.strip_diacritics { + result = result + .nfkd() + .filter(|c| !unicode_normalization::char::is_combining_mark(*c)) + .collect(); + } + + if self.collapse_whitespace { + let mut collapsed = String::with_capacity(result.len()); + let mut prev: Option = Option::None; + for c in result.chars() { + let dominated = matches!(c, ' ' | '_' | '-'); + if dominated { + if let Some(p) = prev { + if matches!(p, ' ' | '_' | '-') { + continue; + } + } + } + collapsed.push(c); + prev = Some(c); + } + result = collapsed; + } + + if self.trim_dots_spaces { + let trimmed = result.trim_matches(|c: char| c == '.' || c == ' '); + result = trimmed.to_string(); + } + + result + } + + fn display_name(&self) -> &str { + "Sanitize" + } + + fn rule_type(&self) -> &str { + "sanitize" + } + + fn is_enabled(&self) -> bool { + self.enabled + } +} + +#[cfg(test)] +mod tests { + use super::*; + + fn ctx() -> RenameContext { + RenameContext::dummy(0) + } + + fn base() -> SanitizeRule { + SanitizeRule { + illegal_chars: false, + spaces_to: SpaceMode::None, + normalize_unicode: false, + strip_zero_width: false, + strip_diacritics: false, + collapse_whitespace: false, + trim_dots_spaces: false, + enabled: true, + } + } + + #[test] + fn illegal_chars_removed() { + let rule = SanitizeRule { + illegal_chars: true, + ..base() + }; + assert_eq!(rule.apply("file:test", &ctx()), "filenametest"); + assert_eq!(rule.apply("a\"b/c\\d|e?f*g", &ctx()), "abcdefg"); + } + + #[test] + fn control_chars_removed() { + let rule = SanitizeRule { + illegal_chars: true, + ..base() + }; + let input = format!("file{}name", '\x01'); + assert_eq!(rule.apply(&input, &ctx()), "filename"); + } + + #[test] + fn spaces_to_underscores() { + let rule = SanitizeRule { + spaces_to: SpaceMode::Underscores, + ..base() + }; + assert_eq!(rule.apply("my file name", &ctx()), "my_file_name"); + } + + #[test] + fn spaces_to_dashes() { + let rule = SanitizeRule { + spaces_to: SpaceMode::Dashes, + ..base() + }; + assert_eq!(rule.apply("my file name", &ctx()), "my-file-name"); + } + + #[test] + fn spaces_to_dots() { + let rule = SanitizeRule { + spaces_to: SpaceMode::Dots, + ..base() + }; + assert_eq!(rule.apply("my file name", &ctx()), "my.file.name"); + } + + #[test] + fn normalize_unicode_nfc() { + let rule = SanitizeRule { + normalize_unicode: true, + ..base() + }; + // e + combining acute -> precomposed + let input = "caf\u{0065}\u{0301}"; + let output = rule.apply(input, &ctx()); + assert_eq!(output, "caf\u{00E9}"); + } + + #[test] + fn strip_zero_width_chars() { + let rule = SanitizeRule { + strip_zero_width: true, + ..base() + }; + let input = "fi\u{200B}le\u{FEFF}name\u{00AD}"; + assert_eq!(rule.apply(&input, &ctx()), "filename"); + } + + #[test] + fn strip_diacritics_basic() { + let rule = SanitizeRule { + strip_diacritics: true, + ..base() + }; + assert_eq!(rule.apply("caf\u{00E9}", &ctx()), "cafe"); + assert_eq!(rule.apply("\u{00FC}ber", &ctx()), "uber"); + } + + #[test] + fn strip_diacritics_combining() { + let rule = SanitizeRule { + strip_diacritics: true, + ..base() + }; + assert_eq!(rule.apply("cafe\u{0301}", &ctx()), "cafe"); + } + + #[test] + fn collapse_whitespace_spaces() { + let rule = SanitizeRule { + collapse_whitespace: true, + ..base() + }; + assert_eq!(rule.apply("file name", &ctx()), "file name"); + } + + #[test] + fn collapse_whitespace_underscores() { + let rule = SanitizeRule { + collapse_whitespace: true, + ..base() + }; + assert_eq!(rule.apply("file___name", &ctx()), "file_name"); + } + + #[test] + fn collapse_whitespace_dashes() { + let rule = SanitizeRule { + collapse_whitespace: true, + ..base() + }; + assert_eq!(rule.apply("file---name", &ctx()), "file-name"); + } + + #[test] + fn collapse_whitespace_mixed() { + let rule = SanitizeRule { + collapse_whitespace: true, + ..base() + }; + assert_eq!(rule.apply("file -_name", &ctx()), "file name"); + } + + #[test] + fn trim_dots_and_spaces() { + let rule = SanitizeRule { + trim_dots_spaces: true, + ..base() + }; + assert_eq!(rule.apply(" file.txt ", &ctx()), "file.txt"); + assert_eq!(rule.apply("..file.txt..", &ctx()), "file.txt"); + assert_eq!(rule.apply(". .file. .", &ctx()), "file"); + } + + #[test] + fn all_together() { + let rule = SanitizeRule { + illegal_chars: true, + spaces_to: SpaceMode::Underscores, + normalize_unicode: false, + strip_zero_width: true, + strip_diacritics: false, + collapse_whitespace: true, + trim_dots_spaces: true, + enabled: true, + }; + let input = " .txt.."; + let output = rule.apply(input, &ctx()); + assert_eq!(output, "_my_filename.txt"); + } + + #[test] + fn passthrough_when_nothing_enabled() { + let rule = base(); + assert_eq!(rule.apply("anything goes ", &ctx()), "anything goes "); + } + + #[test] + fn display_and_type() { + let rule = SanitizeRule::new(); + assert_eq!(rule.display_name(), "Sanitize"); + assert_eq!(rule.rule_type(), "sanitize"); + assert!(rule.is_enabled()); + } +} diff --git a/crates/nomina-core/src/rules/swap.rs b/crates/nomina-core/src/rules/swap.rs new file mode 100644 index 0000000..4c7954f --- /dev/null +++ b/crates/nomina-core/src/rules/swap.rs @@ -0,0 +1,116 @@ +use serde::{Deserialize, Serialize}; + +use crate::{RenameContext, RenameRule}; + +#[derive(Debug, Clone, Serialize, Deserialize, Default, PartialEq)] +pub enum SwapOccurrence { + #[default] + First, + Last, +} + +#[derive(Debug, Clone, Serialize, Deserialize)] +pub struct SwapRule { + pub delimiter: String, + pub occurrence: SwapOccurrence, + #[serde(default)] + pub new_delimiter: Option, + pub enabled: bool, +} + +impl SwapRule { + pub fn new() -> Self { + Self { + delimiter: ", ".into(), + occurrence: SwapOccurrence::First, + new_delimiter: None, + enabled: true, + } + } +} + +impl RenameRule for SwapRule { + fn apply(&self, filename: &str, _context: &RenameContext) -> String { + if self.delimiter.is_empty() { + return filename.to_string(); + } + + let pos = match self.occurrence { + SwapOccurrence::First => filename.find(&self.delimiter), + SwapOccurrence::Last => filename.rfind(&self.delimiter), + }; + + let pos = match pos { + Some(p) => p, + None => return filename.to_string(), + }; + + let left = &filename[..pos]; + let right = &filename[pos + self.delimiter.len()..]; + let joiner = self.new_delimiter.as_deref().unwrap_or(&self.delimiter); + + format!("{}{}{}", right, joiner, left) + } + + fn display_name(&self) -> &str { + "Swap" + } + + fn rule_type(&self) -> &str { + "swap" + } + + fn is_enabled(&self) -> bool { + self.enabled + } +} + +#[cfg(test)] +mod tests { + use super::*; + + fn ctx() -> RenameContext { + RenameContext::dummy(0) + } + + #[test] + fn swap_comma_space() { + let rule = SwapRule::new(); + assert_eq!(rule.apply("LastName, FirstName", &ctx()), "FirstName, LastName"); + } + + #[test] + fn swap_with_new_delimiter() { + let rule = SwapRule { + new_delimiter: Some(" ".into()), + ..SwapRule::new() + }; + assert_eq!(rule.apply("LastName, FirstName", &ctx()), "FirstName LastName"); + } + + #[test] + fn swap_first_occurrence() { + let rule = SwapRule { + delimiter: "-".into(), + occurrence: SwapOccurrence::First, + ..SwapRule::new() + }; + assert_eq!(rule.apply("a-b-c", &ctx()), "b-c-a"); + } + + #[test] + fn swap_last_occurrence() { + let rule = SwapRule { + delimiter: "-".into(), + occurrence: SwapOccurrence::Last, + ..SwapRule::new() + }; + assert_eq!(rule.apply("a-b-c", &ctx()), "c-a-b"); + } + + #[test] + fn no_delimiter_found() { + let rule = SwapRule::new(); + assert_eq!(rule.apply("nodelimiter", &ctx()), "nodelimiter"); + } +} diff --git a/crates/nomina-core/src/rules/text_editor.rs b/crates/nomina-core/src/rules/text_editor.rs new file mode 100644 index 0000000..07750f8 --- /dev/null +++ b/crates/nomina-core/src/rules/text_editor.rs @@ -0,0 +1,82 @@ +use serde::{Deserialize, Serialize}; + +use crate::{RenameContext, RenameRule}; + +#[derive(Debug, Clone, Serialize, Deserialize)] +pub struct TextEditorRule { + #[serde(default)] + pub names: Vec, + pub enabled: bool, +} + +impl TextEditorRule { + pub fn new() -> Self { + Self { + names: Vec::new(), + enabled: true, + } + } +} + +impl RenameRule for TextEditorRule { + fn apply(&self, filename: &str, context: &RenameContext) -> String { + if let Some(name) = self.names.get(context.index) { + if !name.is_empty() { + return name.clone(); + } + } + filename.to_string() + } + + fn display_name(&self) -> &str { + "Text Editor" + } + + fn rule_type(&self) -> &str { + "text_editor" + } + + fn is_enabled(&self) -> bool { + self.enabled + } +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn maps_by_index() { + let rule = TextEditorRule { + names: vec!["alpha".into(), "beta".into(), "gamma".into()], + enabled: true, + }; + let mut ctx = RenameContext::dummy(0); + assert_eq!(rule.apply("file1", &ctx), "alpha"); + ctx.index = 1; + assert_eq!(rule.apply("file2", &ctx), "beta"); + ctx.index = 2; + assert_eq!(rule.apply("file3", &ctx), "gamma"); + } + + #[test] + fn out_of_bounds_keeps_original() { + let rule = TextEditorRule { + names: vec!["only".into()], + enabled: true, + }; + let mut ctx = RenameContext::dummy(0); + ctx.index = 5; + assert_eq!(rule.apply("original", &ctx), "original"); + } + + #[test] + fn empty_line_keeps_original() { + let rule = TextEditorRule { + names: vec!["".into(), "renamed".into()], + enabled: true, + }; + let ctx = RenameContext::dummy(0); + assert_eq!(rule.apply("original", &ctx), "original"); + } +} diff --git a/crates/nomina-core/src/rules/transliterate.rs b/crates/nomina-core/src/rules/transliterate.rs new file mode 100644 index 0000000..d2005ec --- /dev/null +++ b/crates/nomina-core/src/rules/transliterate.rs @@ -0,0 +1,99 @@ +use deunicode::deunicode; +use serde::{Deserialize, Serialize}; + +use crate::{RenameContext, RenameRule}; + +#[derive(Debug, Clone, Serialize, Deserialize)] +pub struct TransliterateRule { + pub enabled: bool, +} + +impl TransliterateRule { + pub fn new() -> Self { + Self { enabled: true } + } +} + +impl RenameRule for TransliterateRule { + fn apply(&self, filename: &str, _context: &RenameContext) -> String { + deunicode(filename) + } + + fn display_name(&self) -> &str { + "Transliterate" + } + + fn rule_type(&self) -> &str { + "transliterate" + } + + fn is_enabled(&self) -> bool { + self.enabled + } +} + +#[cfg(test)] +mod tests { + use super::*; + + fn rule() -> TransliterateRule { + TransliterateRule::new() + } + + #[test] + fn ascii_unchanged() { + let r = rule(); + let ctx = RenameContext::dummy(0); + assert_eq!(r.apply("hello.txt", &ctx), "hello.txt"); + } + + #[test] + fn accented_chars() { + let r = rule(); + let ctx = RenameContext::dummy(0); + assert_eq!(r.apply("caf\u{e9}", &ctx), "cafe"); + } + + #[test] + fn combining_accent() { + let r = rule(); + let ctx = RenameContext::dummy(0); + assert_eq!(r.apply("cafe\u{0301}", &ctx), "cafe"); + } + + #[test] + fn cyrillic() { + let r = rule(); + let ctx = RenameContext::dummy(0); + assert_eq!(r.apply("\u{41c}\u{43e}\u{441}\u{43a}\u{432}\u{430}", &ctx), "Moskva"); + } + + #[test] + fn cjk() { + let r = rule(); + let ctx = RenameContext::dummy(0); + let result = r.apply("\u{4e16}\u{754c}", &ctx); + assert!(!result.is_empty()); + assert!(result.is_ascii()); + } + + #[test] + fn german_umlauts() { + let r = rule(); + let ctx = RenameContext::dummy(0); + assert_eq!(r.apply("\u{fc}ber", &ctx), "uber"); + } + + #[test] + fn disabled() { + let r = TransliterateRule { enabled: false }; + assert!(!r.is_enabled()); + } + + #[test] + fn display_and_type() { + let r = rule(); + assert_eq!(r.display_name(), "Transliterate"); + assert_eq!(r.rule_type(), "transliterate"); + } +} diff --git a/crates/nomina-core/src/rules/truncate.rs b/crates/nomina-core/src/rules/truncate.rs new file mode 100644 index 0000000..31db02d --- /dev/null +++ b/crates/nomina-core/src/rules/truncate.rs @@ -0,0 +1,110 @@ +use serde::{Deserialize, Serialize}; + +use crate::{RenameContext, RenameRule}; + +fn default_max() -> usize { + 50 +} + +#[derive(Debug, Clone, Serialize, Deserialize, Default, PartialEq)] +pub enum TruncateFrom { + Start, + #[default] + End, +} + +#[derive(Debug, Clone, Serialize, Deserialize)] +pub struct TruncateRule { + #[serde(default = "default_max")] + pub max_length: usize, + #[serde(default)] + pub from: TruncateFrom, + #[serde(default)] + pub suffix: String, + pub enabled: bool, +} + +impl TruncateRule { + pub fn new() -> Self { + Self { + max_length: 50, + from: TruncateFrom::End, + suffix: String::new(), + enabled: true, + } + } +} + +impl RenameRule for TruncateRule { + fn apply(&self, filename: &str, _context: &RenameContext) -> String { + let char_count = filename.chars().count(); + if char_count <= self.max_length { + return filename.to_string(); + } + + let suffix_len = self.suffix.chars().count(); + let keep = self.max_length.saturating_sub(suffix_len); + + match self.from { + TruncateFrom::End => { + let truncated: String = filename.chars().take(keep).collect(); + format!("{}{}", truncated, self.suffix) + } + TruncateFrom::Start => { + let truncated: String = filename.chars().skip(char_count - keep).collect(); + format!("{}{}", self.suffix, truncated) + } + } + } + + fn display_name(&self) -> &str { + "Truncate" + } + + fn rule_type(&self) -> &str { + "truncate" + } + + fn is_enabled(&self) -> bool { + self.enabled + } +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn no_truncation_when_short() { + let rule = TruncateRule { + max_length: 10, + ..TruncateRule::new() + }; + let ctx = RenameContext::dummy(0); + assert_eq!(rule.apply("short", &ctx), "short"); + } + + #[test] + fn truncate_from_end_with_suffix() { + let rule = TruncateRule { + max_length: 8, + from: TruncateFrom::End, + suffix: "...".into(), + enabled: true, + }; + let ctx = RenameContext::dummy(0); + assert_eq!(rule.apply("a]very_long_filename", &ctx), "a]ver..."); + } + + #[test] + fn truncate_from_start() { + let rule = TruncateRule { + max_length: 7, + from: TruncateFrom::Start, + suffix: "~".into(), + enabled: true, + }; + let ctx = RenameContext::dummy(0); + assert_eq!(rule.apply("some_long_name", &ctx), "~g_name"); + } +} diff --git a/crates/nomina-core/src/scanner.rs b/crates/nomina-core/src/scanner.rs index 85f906c..f9e9107 100644 --- a/crates/nomina-core/src/scanner.rs +++ b/crates/nomina-core/src/scanner.rs @@ -88,18 +88,25 @@ fn split_filename(name: &str) -> (String, String) { } fn is_hidden_file(path: &Path) -> bool { + if path.file_name() + .map(|n| n.to_string_lossy().starts_with('.')) + .unwrap_or(false) + { + return true; + } + #[cfg(target_os = "windows")] { use std::os::windows::fs::MetadataExt; if let Ok(meta) = std::fs::metadata(path) { const FILE_ATTRIBUTE_HIDDEN: u32 = 0x2; - return meta.file_attributes() & FILE_ATTRIBUTE_HIDDEN != 0; + if meta.file_attributes() & FILE_ATTRIBUTE_HIDDEN != 0 { + return true; + } } } - path.file_name() - .map(|n| n.to_string_lossy().starts_with('.')) - .unwrap_or(false) + false } fn file_created(meta: &std::fs::Metadata) -> Option> { diff --git a/crates/nomina-core/src/undo.rs b/crates/nomina-core/src/undo.rs index 02e7b44..855e128 100644 --- a/crates/nomina-core/src/undo.rs +++ b/crates/nomina-core/src/undo.rs @@ -25,7 +25,7 @@ pub struct UndoEntry { pub renamed_path: PathBuf, } -const MAX_UNDO_BATCHES: usize = 50; +pub const DEFAULT_MAX_UNDO_BATCHES: usize = 50; impl UndoLog { pub fn new() -> Self { @@ -64,8 +64,12 @@ impl UndoLog { } pub fn add_batch(&mut self, batch: UndoBatch) { + self.add_batch_with_limit(batch, DEFAULT_MAX_UNDO_BATCHES); + } + + pub fn add_batch_with_limit(&mut self, batch: UndoBatch, max: usize) { self.entries.push(batch); - while self.entries.len() > MAX_UNDO_BATCHES { + while self.entries.len() > max { self.entries.remove(0); } } diff --git a/ui/components.json b/ui/components.json new file mode 100644 index 0000000..cd5b0af --- /dev/null +++ b/ui/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": "tabler", + "rtl": false, + "aliases": { + "components": "@/components", + "utils": "@/lib/utils", + "ui": "@/components/ui", + "lib": "@/lib", + "hooks": "@/hooks" + }, + "menuColor": "default", + "menuAccent": "subtle", + "registries": {} +} diff --git a/ui/package-lock.json b/ui/package-lock.json index 2507b9d..97da41a 100644 --- a/ui/package-lock.json +++ b/ui/package-lock.json @@ -1,22 +1,39 @@ { - "name": "ui", - "version": "1.0.0", + "name": "nomina-ui", + "version": "0.1.0", "lockfileVersion": 3, "requires": true, "packages": { "": { - "name": "ui", - "version": "1.0.0", - "license": "ISC", + "name": "nomina-ui", + "version": "0.1.0", "dependencies": { + "@dnd-kit/core": "^6.3.1", + "@dnd-kit/sortable": "^10.0.0", + "@dnd-kit/utilities": "^3.2.2", + "@fontsource-variable/geist": "^5.2.8", + "@tabler/icons-react": "^3.40.0", "@tanstack/react-virtual": "^3.13.22", + "@tauri-apps/plugin-dialog": "^2.6.0", + "@tauri-apps/plugin-shell": "^2.3.5", + "class-variance-authority": "^0.7.1", + "clsx": "^2.1.1", + "framer-motion": "^12.36.0", + "overlayscrollbars": "^2.14.0", + "overlayscrollbars-react": "^0.5.6", + "radix-ui": "^1.4.3", "react": "^19.2.4", "react-dom": "^19.2.4", + "shadcn": "^4.0.6", + "sonner": "^2.0.7", + "tailwind-merge": "^3.5.0", + "tw-animate-css": "^1.4.0", "zustand": "^5.0.11" }, "devDependencies": { "@tailwindcss/vite": "^4.2.1", "@tauri-apps/api": "^2.10.1", + "@types/node": "^25.5.0", "@types/react": "^19.2.14", "@types/react-dom": "^19.2.3", "@vitejs/plugin-react": "^4.7.0", @@ -25,11 +42,31 @@ "vite": "^6.4.1" } }, + "node_modules/@antfu/ni": { + "version": "25.0.0", + "resolved": "https://registry.npmjs.org/@antfu/ni/-/ni-25.0.0.tgz", + "integrity": "sha512-9q/yCljni37pkMr4sPrI3G4jqdIk074+iukc5aFJl7kmDCCsiJrbZ6zKxnES1Gwg+i9RcDZwvktl23puGslmvA==", + "license": "MIT", + "dependencies": { + "ansis": "^4.0.0", + "fzf": "^0.5.2", + "package-manager-detector": "^1.3.0", + "tinyexec": "^1.0.1" + }, + "bin": { + "na": "bin/na.mjs", + "nci": "bin/nci.mjs", + "ni": "bin/ni.mjs", + "nlx": "bin/nlx.mjs", + "nr": "bin/nr.mjs", + "nun": "bin/nun.mjs", + "nup": "bin/nup.mjs" + } + }, "node_modules/@babel/code-frame": { "version": "7.29.0", "resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.29.0.tgz", "integrity": "sha512-9NhCeYjq9+3uxgdtp20LSiJXJvN0FeCtNGpJxuMFZ1Kv3cWUNb6DOhJwUvcVCzKGR66cw4njwM6hrJLqgOwbcw==", - "dev": true, "license": "MIT", "dependencies": { "@babel/helper-validator-identifier": "^7.28.5", @@ -44,7 +81,6 @@ "version": "7.29.0", "resolved": "https://registry.npmjs.org/@babel/compat-data/-/compat-data-7.29.0.tgz", "integrity": "sha512-T1NCJqT/j9+cn8fvkt7jtwbLBfLC/1y1c7NtCeXFRgzGTsafi68MRv8yzkYSapBnFA6L3U2VSc02ciDzoAJhJg==", - "dev": true, "license": "MIT", "engines": { "node": ">=6.9.0" @@ -54,7 +90,6 @@ "version": "7.29.0", "resolved": "https://registry.npmjs.org/@babel/core/-/core-7.29.0.tgz", "integrity": "sha512-CGOfOJqWjg2qW/Mb6zNsDm+u5vFQ8DxXfbM09z69p5Z6+mE1ikP2jUXw+j42Pf1XTYED2Rni5f95npYeuwMDQA==", - "dev": true, "license": "MIT", "dependencies": { "@babel/code-frame": "^7.29.0", @@ -85,7 +120,6 @@ "version": "7.29.1", "resolved": "https://registry.npmjs.org/@babel/generator/-/generator-7.29.1.tgz", "integrity": "sha512-qsaF+9Qcm2Qv8SRIMMscAvG4O3lJ0F1GuMo5HR/Bp02LopNgnZBC/EkbevHFeGs4ls/oPz9v+Bsmzbkbe+0dUw==", - "dev": true, "license": "MIT", "dependencies": { "@babel/parser": "^7.29.0", @@ -98,11 +132,22 @@ "node": ">=6.9.0" } }, + "node_modules/@babel/helper-annotate-as-pure": { + "version": "7.27.3", + "resolved": "https://registry.npmjs.org/@babel/helper-annotate-as-pure/-/helper-annotate-as-pure-7.27.3.tgz", + "integrity": "sha512-fXSwMQqitTGeHLBC08Eq5yXz2m37E4pJX1qAU1+2cNedz/ifv/bVXft90VeSav5nFO61EcNgwr0aJxbyPaWBPg==", + "license": "MIT", + "dependencies": { + "@babel/types": "^7.27.3" + }, + "engines": { + "node": ">=6.9.0" + } + }, "node_modules/@babel/helper-compilation-targets": { "version": "7.28.6", "resolved": "https://registry.npmjs.org/@babel/helper-compilation-targets/-/helper-compilation-targets-7.28.6.tgz", "integrity": "sha512-JYtls3hqi15fcx5GaSNL7SCTJ2MNmjrkHXg4FSpOA/grxK8KwyZ5bubHsCq8FXCkua6xhuaaBit+3b7+VZRfcA==", - "dev": true, "license": "MIT", "dependencies": { "@babel/compat-data": "^7.28.6", @@ -115,21 +160,53 @@ "node": ">=6.9.0" } }, + "node_modules/@babel/helper-create-class-features-plugin": { + "version": "7.28.6", + "resolved": "https://registry.npmjs.org/@babel/helper-create-class-features-plugin/-/helper-create-class-features-plugin-7.28.6.tgz", + "integrity": "sha512-dTOdvsjnG3xNT9Y0AUg1wAl38y+4Rl4sf9caSQZOXdNqVn+H+HbbJ4IyyHaIqNR6SW9oJpA/RuRjsjCw2IdIow==", + "license": "MIT", + "dependencies": { + "@babel/helper-annotate-as-pure": "^7.27.3", + "@babel/helper-member-expression-to-functions": "^7.28.5", + "@babel/helper-optimise-call-expression": "^7.27.1", + "@babel/helper-replace-supers": "^7.28.6", + "@babel/helper-skip-transparent-expression-wrappers": "^7.27.1", + "@babel/traverse": "^7.28.6", + "semver": "^6.3.1" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0" + } + }, "node_modules/@babel/helper-globals": { "version": "7.28.0", "resolved": "https://registry.npmjs.org/@babel/helper-globals/-/helper-globals-7.28.0.tgz", "integrity": "sha512-+W6cISkXFa1jXsDEdYA8HeevQT/FULhxzR99pxphltZcVaugps53THCeiWA8SguxxpSp3gKPiuYfSWopkLQ4hw==", - "dev": true, "license": "MIT", "engines": { "node": ">=6.9.0" } }, + "node_modules/@babel/helper-member-expression-to-functions": { + "version": "7.28.5", + "resolved": "https://registry.npmjs.org/@babel/helper-member-expression-to-functions/-/helper-member-expression-to-functions-7.28.5.tgz", + "integrity": "sha512-cwM7SBRZcPCLgl8a7cY0soT1SptSzAlMH39vwiRpOQkJlh53r5hdHwLSCZpQdVLT39sZt+CRpNwYG4Y2v77atg==", + "license": "MIT", + "dependencies": { + "@babel/traverse": "^7.28.5", + "@babel/types": "^7.28.5" + }, + "engines": { + "node": ">=6.9.0" + } + }, "node_modules/@babel/helper-module-imports": { "version": "7.28.6", "resolved": "https://registry.npmjs.org/@babel/helper-module-imports/-/helper-module-imports-7.28.6.tgz", "integrity": "sha512-l5XkZK7r7wa9LucGw9LwZyyCUscb4x37JWTPz7swwFE/0FMQAGpiWUZn8u9DzkSBWEcK25jmvubfpw2dnAMdbw==", - "dev": true, "license": "MIT", "dependencies": { "@babel/traverse": "^7.28.6", @@ -143,7 +220,6 @@ "version": "7.28.6", "resolved": "https://registry.npmjs.org/@babel/helper-module-transforms/-/helper-module-transforms-7.28.6.tgz", "integrity": "sha512-67oXFAYr2cDLDVGLXTEABjdBJZ6drElUSI7WKp70NrpyISso3plG9SAGEF6y7zbha/wOzUByWWTJvEDVNIUGcA==", - "dev": true, "license": "MIT", "dependencies": { "@babel/helper-module-imports": "^7.28.6", @@ -157,21 +233,61 @@ "@babel/core": "^7.0.0" } }, + "node_modules/@babel/helper-optimise-call-expression": { + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/helper-optimise-call-expression/-/helper-optimise-call-expression-7.27.1.tgz", + "integrity": "sha512-URMGH08NzYFhubNSGJrpUEphGKQwMQYBySzat5cAByY1/YgIRkULnIy3tAMeszlL/so2HbeilYloUmSpd7GdVw==", + "license": "MIT", + "dependencies": { + "@babel/types": "^7.27.1" + }, + "engines": { + "node": ">=6.9.0" + } + }, "node_modules/@babel/helper-plugin-utils": { "version": "7.28.6", "resolved": "https://registry.npmjs.org/@babel/helper-plugin-utils/-/helper-plugin-utils-7.28.6.tgz", "integrity": "sha512-S9gzZ/bz83GRysI7gAD4wPT/AI3uCnY+9xn+Mx/KPs2JwHJIz1W8PZkg2cqyt3RNOBM8ejcXhV6y8Og7ly/Dug==", - "dev": true, "license": "MIT", "engines": { "node": ">=6.9.0" } }, + "node_modules/@babel/helper-replace-supers": { + "version": "7.28.6", + "resolved": "https://registry.npmjs.org/@babel/helper-replace-supers/-/helper-replace-supers-7.28.6.tgz", + "integrity": "sha512-mq8e+laIk94/yFec3DxSjCRD2Z0TAjhVbEJY3UQrlwVo15Lmt7C2wAUbK4bjnTs4APkwsYLTahXRraQXhb1WCg==", + "license": "MIT", + "dependencies": { + "@babel/helper-member-expression-to-functions": "^7.28.5", + "@babel/helper-optimise-call-expression": "^7.27.1", + "@babel/traverse": "^7.28.6" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0" + } + }, + "node_modules/@babel/helper-skip-transparent-expression-wrappers": { + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/helper-skip-transparent-expression-wrappers/-/helper-skip-transparent-expression-wrappers-7.27.1.tgz", + "integrity": "sha512-Tub4ZKEXqbPjXgWLl2+3JpQAYBJ8+ikpQ2Ocj/q/r0LwE3UhENh7EUabyHjz2kCEsrRY83ew2DQdHluuiDQFzg==", + "license": "MIT", + "dependencies": { + "@babel/traverse": "^7.27.1", + "@babel/types": "^7.27.1" + }, + "engines": { + "node": ">=6.9.0" + } + }, "node_modules/@babel/helper-string-parser": { "version": "7.27.1", "resolved": "https://registry.npmjs.org/@babel/helper-string-parser/-/helper-string-parser-7.27.1.tgz", "integrity": "sha512-qMlSxKbpRlAridDExk92nSobyDdpPijUq2DW6oDnUqd0iOGxmQjyqhMIihI9+zv4LPyZdRje2cavWPbCbWm3eA==", - "dev": true, "license": "MIT", "engines": { "node": ">=6.9.0" @@ -181,7 +297,6 @@ "version": "7.28.5", "resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.28.5.tgz", "integrity": "sha512-qSs4ifwzKJSV39ucNjsvc6WVHs6b7S03sOh2OcHF9UHfVPqWWALUsNUVzhSBiItjRZoLHx7nIarVjqKVusUZ1Q==", - "dev": true, "license": "MIT", "engines": { "node": ">=6.9.0" @@ -191,7 +306,6 @@ "version": "7.27.1", "resolved": "https://registry.npmjs.org/@babel/helper-validator-option/-/helper-validator-option-7.27.1.tgz", "integrity": "sha512-YvjJow9FxbhFFKDSuFnVCe2WxXk1zWc22fFePVNEaWJEu8IrZVlda6N0uHwzZrUM1il7NC9Mlp4MaJYbYd9JSg==", - "dev": true, "license": "MIT", "engines": { "node": ">=6.9.0" @@ -201,7 +315,6 @@ "version": "7.28.6", "resolved": "https://registry.npmjs.org/@babel/helpers/-/helpers-7.28.6.tgz", "integrity": "sha512-xOBvwq86HHdB7WUDTfKfT/Vuxh7gElQ+Sfti2Cy6yIWNW05P8iUslOVcZ4/sKbE+/jQaukQAdz/gf3724kYdqw==", - "dev": true, "license": "MIT", "dependencies": { "@babel/template": "^7.28.6", @@ -215,7 +328,6 @@ "version": "7.29.0", "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.29.0.tgz", "integrity": "sha512-IyDgFV5GeDUVX4YdF/3CPULtVGSXXMLh1xVIgdCgxApktqnQV0r7/8Nqthg+8YLGaAtdyIlo2qIdZrbCv4+7ww==", - "dev": true, "license": "MIT", "dependencies": { "@babel/types": "^7.29.0" @@ -227,6 +339,52 @@ "node": ">=6.0.0" } }, + "node_modules/@babel/plugin-syntax-jsx": { + "version": "7.28.6", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-jsx/-/plugin-syntax-jsx-7.28.6.tgz", + "integrity": "sha512-wgEmr06G6sIpqr8YDwA2dSRTE3bJ+V0IfpzfSY3Lfgd7YWOaAdlykvJi13ZKBt8cZHfgH1IXN+CL656W3uUa4w==", + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.28.6" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-syntax-typescript": { + "version": "7.28.6", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-typescript/-/plugin-syntax-typescript-7.28.6.tgz", + "integrity": "sha512-+nDNmQye7nlnuuHDboPbGm00Vqg3oO8niRRL27/4LYHUsHYh0zJ1xWOz0uRwNFmM1Avzk8wZbc6rdiYhomzv/A==", + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.28.6" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-modules-commonjs": { + "version": "7.28.6", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-modules-commonjs/-/plugin-transform-modules-commonjs-7.28.6.tgz", + "integrity": "sha512-jppVbf8IV9iWWwWTQIxJMAJCWBuuKx71475wHwYytrRGQ2CWiDvYlADQno3tcYpS/T2UUWFQp3nVtYfK/YBQrA==", + "license": "MIT", + "dependencies": { + "@babel/helper-module-transforms": "^7.28.6", + "@babel/helper-plugin-utils": "^7.28.6" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, "node_modules/@babel/plugin-transform-react-jsx-self": { "version": "7.27.1", "resolved": "https://registry.npmjs.org/@babel/plugin-transform-react-jsx-self/-/plugin-transform-react-jsx-self-7.27.1.tgz", @@ -259,11 +417,48 @@ "@babel/core": "^7.0.0-0" } }, + "node_modules/@babel/plugin-transform-typescript": { + "version": "7.28.6", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-typescript/-/plugin-transform-typescript-7.28.6.tgz", + "integrity": "sha512-0YWL2RFxOqEm9Efk5PvreamxPME8OyY0wM5wh5lHjF+VtVhdneCWGzZeSqzOfiobVqQaNCd2z0tQvnI9DaPWPw==", + "license": "MIT", + "dependencies": { + "@babel/helper-annotate-as-pure": "^7.27.3", + "@babel/helper-create-class-features-plugin": "^7.28.6", + "@babel/helper-plugin-utils": "^7.28.6", + "@babel/helper-skip-transparent-expression-wrappers": "^7.27.1", + "@babel/plugin-syntax-typescript": "^7.28.6" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/preset-typescript": { + "version": "7.28.5", + "resolved": "https://registry.npmjs.org/@babel/preset-typescript/-/preset-typescript-7.28.5.tgz", + "integrity": "sha512-+bQy5WOI2V6LJZpPVxY+yp66XdZ2yifu0Mc1aP5CQKgjn4QM5IN2i5fAZ4xKop47pr8rpVhiAeu+nDQa12C8+g==", + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.27.1", + "@babel/helper-validator-option": "^7.27.1", + "@babel/plugin-syntax-jsx": "^7.27.1", + "@babel/plugin-transform-modules-commonjs": "^7.27.1", + "@babel/plugin-transform-typescript": "^7.28.5" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, "node_modules/@babel/template": { "version": "7.28.6", "resolved": "https://registry.npmjs.org/@babel/template/-/template-7.28.6.tgz", "integrity": "sha512-YA6Ma2KsCdGb+WC6UpBVFJGXL58MDA6oyONbjyF/+5sBgxY/dwkhLogbMT2GXXyU84/IhRw/2D1Os1B/giz+BQ==", - "dev": true, "license": "MIT", "dependencies": { "@babel/code-frame": "^7.28.6", @@ -278,7 +473,6 @@ "version": "7.29.0", "resolved": "https://registry.npmjs.org/@babel/traverse/-/traverse-7.29.0.tgz", "integrity": "sha512-4HPiQr0X7+waHfyXPZpWPfWL/J7dcN1mx9gL6WdQVMbPnF3+ZhSMs8tCxN7oHddJE9fhNE7+lxdnlyemKfJRuA==", - "dev": true, "license": "MIT", "dependencies": { "@babel/code-frame": "^7.29.0", @@ -297,7 +491,6 @@ "version": "7.29.0", "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.29.0.tgz", "integrity": "sha512-LwdZHpScM4Qz8Xw2iKSzS+cfglZzJGvofQICy7W7v4caru4EaAmyUuO6BGrbyQ2mYV11W0U8j5mBhd14dd3B0A==", - "dev": true, "license": "MIT", "dependencies": { "@babel/helper-string-parser": "^7.27.1", @@ -307,6 +500,203 @@ "node": ">=6.9.0" } }, + "node_modules/@dnd-kit/accessibility": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/@dnd-kit/accessibility/-/accessibility-3.1.1.tgz", + "integrity": "sha512-2P+YgaXF+gRsIihwwY1gCsQSYnu9Zyj2py8kY5fFvUM1qm2WA2u639R6YNVfU4GWr+ZM5mqEsfHZZLoRONbemw==", + "license": "MIT", + "dependencies": { + "tslib": "^2.0.0" + }, + "peerDependencies": { + "react": ">=16.8.0" + } + }, + "node_modules/@dnd-kit/core": { + "version": "6.3.1", + "resolved": "https://registry.npmjs.org/@dnd-kit/core/-/core-6.3.1.tgz", + "integrity": "sha512-xkGBRQQab4RLwgXxoqETICr6S5JlogafbhNsidmrkVv2YRs5MLwpjoF2qpiGjQt8S9AoxtIV603s0GIUpY5eYQ==", + "license": "MIT", + "dependencies": { + "@dnd-kit/accessibility": "^3.1.1", + "@dnd-kit/utilities": "^3.2.2", + "tslib": "^2.0.0" + }, + "peerDependencies": { + "react": ">=16.8.0", + "react-dom": ">=16.8.0" + } + }, + "node_modules/@dnd-kit/sortable": { + "version": "10.0.0", + "resolved": "https://registry.npmjs.org/@dnd-kit/sortable/-/sortable-10.0.0.tgz", + "integrity": "sha512-+xqhmIIzvAYMGfBYYnbKuNicfSsk4RksY2XdmJhT+HAC01nix6fHCztU68jooFiMUB01Ky3F0FyOvhG/BZrWkg==", + "license": "MIT", + "dependencies": { + "@dnd-kit/utilities": "^3.2.2", + "tslib": "^2.0.0" + }, + "peerDependencies": { + "@dnd-kit/core": "^6.3.0", + "react": ">=16.8.0" + } + }, + "node_modules/@dnd-kit/utilities": { + "version": "3.2.2", + "resolved": "https://registry.npmjs.org/@dnd-kit/utilities/-/utilities-3.2.2.tgz", + "integrity": "sha512-+MKAJEOfaBe5SmV6t34p80MMKhjvUz0vRrvVJbPT0WElzaOJ/1xs+D+KDv+tD/NE5ujfrChEcshd4fLn0wpiqg==", + "license": "MIT", + "dependencies": { + "tslib": "^2.0.0" + }, + "peerDependencies": { + "react": ">=16.8.0" + } + }, + "node_modules/@dotenvx/dotenvx": { + "version": "1.55.0", + "resolved": "https://registry.npmjs.org/@dotenvx/dotenvx/-/dotenvx-1.55.0.tgz", + "integrity": "sha512-JOHoUw2dyHPSoVJwfH5yf3j8yIBtqYDhpcaFv9QKrS30KPme025yA+uJ9q9FTphnr6QkEOMUk/B7I3nmwHkamg==", + "license": "BSD-3-Clause", + "dependencies": { + "commander": "^11.1.0", + "dotenv": "^17.2.1", + "eciesjs": "^0.4.10", + "execa": "^5.1.1", + "fdir": "^6.2.0", + "ignore": "^5.3.0", + "object-treeify": "1.1.33", + "picomatch": "^4.0.2", + "which": "^4.0.0" + }, + "bin": { + "dotenvx": "src/cli/dotenvx.js" + }, + "funding": { + "url": "https://dotenvx.com" + } + }, + "node_modules/@dotenvx/dotenvx/node_modules/commander": { + "version": "11.1.0", + "resolved": "https://registry.npmjs.org/commander/-/commander-11.1.0.tgz", + "integrity": "sha512-yPVavfyCcRhmorC7rWlkHn15b4wDVgVmBA7kV4QVBsF7kv/9TKJAbAXVTxvTnwP8HHKjRCJDClKbciiYS7p0DQ==", + "license": "MIT", + "engines": { + "node": ">=16" + } + }, + "node_modules/@dotenvx/dotenvx/node_modules/execa": { + "version": "5.1.1", + "resolved": "https://registry.npmjs.org/execa/-/execa-5.1.1.tgz", + "integrity": "sha512-8uSpZZocAZRBAPIEINJj3Lo9HyGitllczc27Eh5YYojjMFMn8yHMDMaUHE2Jqfq05D/wucwI4JGURyXt1vchyg==", + "license": "MIT", + "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" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sindresorhus/execa?sponsor=1" + } + }, + "node_modules/@dotenvx/dotenvx/node_modules/get-stream": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/get-stream/-/get-stream-6.0.1.tgz", + "integrity": "sha512-ts6Wi+2j3jQjqi70w5AlN8DFnkSwC+MqmxEzdEALB2qXZYV3X/b1CTfgPLGJNMeAWxdPfU8FO1ms3NUfaHCPYg==", + "license": "MIT", + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/@dotenvx/dotenvx/node_modules/human-signals": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/human-signals/-/human-signals-2.1.0.tgz", + "integrity": "sha512-B4FFZ6q/T2jhhksgkbEW3HBvWIfDW85snkQgawt07S7J5QXTk6BkNV+0yAeZrM5QpMAdYlocGoljn0sJ/WQkFw==", + "license": "Apache-2.0", + "engines": { + "node": ">=10.17.0" + } + }, + "node_modules/@dotenvx/dotenvx/node_modules/is-stream": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/is-stream/-/is-stream-2.0.1.tgz", + "integrity": "sha512-hFoiJiTl63nn+kstHGBtewWSKnQLpyb155KHheA1l39uvtO9nWIop1p3udqPcUd/xbF1VLMO4n7OI6p7RbngDg==", + "license": "MIT", + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/@dotenvx/dotenvx/node_modules/npm-run-path": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/npm-run-path/-/npm-run-path-4.0.1.tgz", + "integrity": "sha512-S48WzZW777zhNIrn7gxOlISNAqi9ZC/uQFnRdbeIHhZhCA6UqpkOT8T1G7BvfdgP4Er8gF4sUbaS0i7QvIfCWw==", + "license": "MIT", + "dependencies": { + "path-key": "^3.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/@dotenvx/dotenvx/node_modules/onetime": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/onetime/-/onetime-5.1.2.tgz", + "integrity": "sha512-kbpaSSGJTWdAY5KPVeMOKXSrPtr8C8C7wodJbcsd51jRnmD+GZu8Y0VoU6Dm5Z4vWr0Ig/1NKuWRKf7j5aaYSg==", + "license": "MIT", + "dependencies": { + "mimic-fn": "^2.1.0" + }, + "engines": { + "node": ">=6" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/@dotenvx/dotenvx/node_modules/signal-exit": { + "version": "3.0.7", + "resolved": "https://registry.npmjs.org/signal-exit/-/signal-exit-3.0.7.tgz", + "integrity": "sha512-wnD2ZE+l+SPC/uoS0vXeE9L1+0wuaMqKlfz9AMUo38JsyLSBWSFcHR1Rri62LZc12vLr1gb3jl7iwQhgwpAbGQ==", + "license": "ISC" + }, + "node_modules/@dotenvx/dotenvx/node_modules/strip-final-newline": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/strip-final-newline/-/strip-final-newline-2.0.0.tgz", + "integrity": "sha512-BrpvfNAE3dcvq7ll3xVumzjKjZQ5tI1sEUIKr3Uoks0XUl45St3FlatVqef9prk4jRDzhW6WZg+3bk93y6pLjA==", + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/@ecies/ciphers": { + "version": "0.2.5", + "resolved": "https://registry.npmjs.org/@ecies/ciphers/-/ciphers-0.2.5.tgz", + "integrity": "sha512-GalEZH4JgOMHYYcYmVqnFirFsjZHeoGMDt9IxEnM9F7GRUUyUksJ7Ou53L83WHJq3RWKD3AcBpo0iQh0oMpf8A==", + "license": "MIT", + "engines": { + "bun": ">=1", + "deno": ">=2", + "node": ">=16" + }, + "peerDependencies": { + "@noble/ciphers": "^1.0.0" + } + }, "node_modules/@esbuild/aix-ppc64": { "version": "0.25.12", "resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.25.12.tgz", @@ -749,11 +1139,152 @@ "node": ">=18" } }, + "node_modules/@floating-ui/core": { + "version": "1.7.5", + "resolved": "https://registry.npmjs.org/@floating-ui/core/-/core-1.7.5.tgz", + "integrity": "sha512-1Ih4WTWyw0+lKyFMcBHGbb5U5FtuHJuujoyyr5zTaWS5EYMeT6Jb2AuDeftsCsEuchO+mM2ij5+q9crhydzLhQ==", + "license": "MIT", + "dependencies": { + "@floating-ui/utils": "^0.2.11" + } + }, + "node_modules/@floating-ui/dom": { + "version": "1.7.6", + "resolved": "https://registry.npmjs.org/@floating-ui/dom/-/dom-1.7.6.tgz", + "integrity": "sha512-9gZSAI5XM36880PPMm//9dfiEngYoC6Am2izES1FF406YFsjvyBMmeJ2g4SAju3xWwtuynNRFL2s9hgxpLI5SQ==", + "license": "MIT", + "dependencies": { + "@floating-ui/core": "^1.7.5", + "@floating-ui/utils": "^0.2.11" + } + }, + "node_modules/@floating-ui/react-dom": { + "version": "2.1.8", + "resolved": "https://registry.npmjs.org/@floating-ui/react-dom/-/react-dom-2.1.8.tgz", + "integrity": "sha512-cC52bHwM/n/CxS87FH0yWdngEZrjdtLW/qVruo68qg+prK7ZQ4YGdut2GyDVpoGeAYe/h899rVeOVm6Oi40k2A==", + "license": "MIT", + "dependencies": { + "@floating-ui/dom": "^1.7.6" + }, + "peerDependencies": { + "react": ">=16.8.0", + "react-dom": ">=16.8.0" + } + }, + "node_modules/@floating-ui/utils": { + "version": "0.2.11", + "resolved": "https://registry.npmjs.org/@floating-ui/utils/-/utils-0.2.11.tgz", + "integrity": "sha512-RiB/yIh78pcIxl6lLMG0CgBXAZ2Y0eVHqMPYugu+9U0AeT6YBeiJpf7lbdJNIugFP5SIjwNRgo4DhR1Qxi26Gg==", + "license": "MIT" + }, + "node_modules/@fontsource-variable/geist": { + "version": "5.2.8", + "resolved": "https://registry.npmjs.org/@fontsource-variable/geist/-/geist-5.2.8.tgz", + "integrity": "sha512-cJ6m9e+8MQ5dCYJsLylfZrgBh6KkG4bOLckB35Tr9J/EqdkEM6QllH5PxqP1dhTvFup+HtMRPuz9xOjxXJggxw==", + "license": "OFL-1.1", + "funding": { + "url": "https://github.com/sponsors/ayuhito" + } + }, + "node_modules/@hono/node-server": { + "version": "1.19.11", + "resolved": "https://registry.npmjs.org/@hono/node-server/-/node-server-1.19.11.tgz", + "integrity": "sha512-dr8/3zEaB+p0D2n/IUrlPF1HZm586qgJNXK1a9fhg/PzdtkK7Ksd5l312tJX2yBuALqDYBlG20QEbayqPyxn+g==", + "license": "MIT", + "engines": { + "node": ">=18.14.1" + }, + "peerDependencies": { + "hono": "^4" + } + }, + "node_modules/@inquirer/ansi": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/@inquirer/ansi/-/ansi-1.0.2.tgz", + "integrity": "sha512-S8qNSZiYzFd0wAcyG5AXCvUHC5Sr7xpZ9wZ2py9XR88jUz8wooStVx5M6dRzczbBWjic9NP7+rY0Xi7qqK/aMQ==", + "license": "MIT", + "engines": { + "node": ">=18" + } + }, + "node_modules/@inquirer/confirm": { + "version": "5.1.21", + "resolved": "https://registry.npmjs.org/@inquirer/confirm/-/confirm-5.1.21.tgz", + "integrity": "sha512-KR8edRkIsUayMXV+o3Gv+q4jlhENF9nMYUZs9PA2HzrXeHI8M5uDag70U7RJn9yyiMZSbtF5/UexBtAVtZGSbQ==", + "license": "MIT", + "dependencies": { + "@inquirer/core": "^10.3.2", + "@inquirer/type": "^3.0.10" + }, + "engines": { + "node": ">=18" + }, + "peerDependencies": { + "@types/node": ">=18" + }, + "peerDependenciesMeta": { + "@types/node": { + "optional": true + } + } + }, + "node_modules/@inquirer/core": { + "version": "10.3.2", + "resolved": "https://registry.npmjs.org/@inquirer/core/-/core-10.3.2.tgz", + "integrity": "sha512-43RTuEbfP8MbKzedNqBrlhhNKVwoK//vUFNW3Q3vZ88BLcrs4kYpGg+B2mm5p2K/HfygoCxuKwJJiv8PbGmE0A==", + "license": "MIT", + "dependencies": { + "@inquirer/ansi": "^1.0.2", + "@inquirer/figures": "^1.0.15", + "@inquirer/type": "^3.0.10", + "cli-width": "^4.1.0", + "mute-stream": "^2.0.0", + "signal-exit": "^4.1.0", + "wrap-ansi": "^6.2.0", + "yoctocolors-cjs": "^2.1.3" + }, + "engines": { + "node": ">=18" + }, + "peerDependencies": { + "@types/node": ">=18" + }, + "peerDependenciesMeta": { + "@types/node": { + "optional": true + } + } + }, + "node_modules/@inquirer/figures": { + "version": "1.0.15", + "resolved": "https://registry.npmjs.org/@inquirer/figures/-/figures-1.0.15.tgz", + "integrity": "sha512-t2IEY+unGHOzAaVM5Xx6DEWKeXlDDcNPeDyUpsRc6CUhBfU3VQOEl+Vssh7VNp1dR8MdUJBWhuObjXCsVpjN5g==", + "license": "MIT", + "engines": { + "node": ">=18" + } + }, + "node_modules/@inquirer/type": { + "version": "3.0.10", + "resolved": "https://registry.npmjs.org/@inquirer/type/-/type-3.0.10.tgz", + "integrity": "sha512-BvziSRxfz5Ov8ch0z/n3oijRSEcEsHnhggm4xFZe93DHcUCTlutlq9Ox4SVENAfcRD22UQq7T/atg9Wr3k09eA==", + "license": "MIT", + "engines": { + "node": ">=18" + }, + "peerDependencies": { + "@types/node": ">=18" + }, + "peerDependenciesMeta": { + "@types/node": { + "optional": true + } + } + }, "node_modules/@jridgewell/gen-mapping": { "version": "0.3.13", "resolved": "https://registry.npmjs.org/@jridgewell/gen-mapping/-/gen-mapping-0.3.13.tgz", "integrity": "sha512-2kkt/7niJ6MgEPxF0bYdQ6etZaA+fQvDcLKckhy1yIQOzaoKjBBjSj63/aLVjYE3qhRt5dvM+uUyfCg6UKCBbA==", - "dev": true, "license": "MIT", "dependencies": { "@jridgewell/sourcemap-codec": "^1.5.0", @@ -764,7 +1295,6 @@ "version": "2.3.5", "resolved": "https://registry.npmjs.org/@jridgewell/remapping/-/remapping-2.3.5.tgz", "integrity": "sha512-LI9u/+laYG4Ds1TDKSJW2YPrIlcVYOwi2fUC6xB43lueCjgxV4lffOCZCtYFiH6TNOX+tQKXx97T4IKHbhyHEQ==", - "dev": true, "license": "MIT", "dependencies": { "@jridgewell/gen-mapping": "^0.3.5", @@ -775,7 +1305,6 @@ "version": "3.1.2", "resolved": "https://registry.npmjs.org/@jridgewell/resolve-uri/-/resolve-uri-3.1.2.tgz", "integrity": "sha512-bRISgCIjP20/tbWSPWMEi54QVPRZExkuD9lJL+UIxUKtwVJA8wW1Trb1jMs1RFXo1CBTNZ/5hpC9QvmKWdopKw==", - "dev": true, "license": "MIT", "engines": { "node": ">=6.0.0" @@ -785,20 +1314,1669 @@ "version": "1.5.5", "resolved": "https://registry.npmjs.org/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.5.5.tgz", "integrity": "sha512-cYQ9310grqxueWbl+WuIUIaiUaDcj7WOq5fVhEljNVgRfOUhY9fy2zTvfoqWsnebh8Sl70VScFbICvJnLKB0Og==", - "dev": true, "license": "MIT" }, "node_modules/@jridgewell/trace-mapping": { "version": "0.3.31", "resolved": "https://registry.npmjs.org/@jridgewell/trace-mapping/-/trace-mapping-0.3.31.tgz", "integrity": "sha512-zzNR+SdQSDJzc8joaeP8QQoCQr8NuYx2dIIytl1QeBEZHJ9uW6hebsrYgbz8hJwUQao3TWCMtmfV8Nu1twOLAw==", - "dev": true, "license": "MIT", "dependencies": { "@jridgewell/resolve-uri": "^3.1.0", "@jridgewell/sourcemap-codec": "^1.4.14" } }, + "node_modules/@modelcontextprotocol/sdk": { + "version": "1.27.1", + "resolved": "https://registry.npmjs.org/@modelcontextprotocol/sdk/-/sdk-1.27.1.tgz", + "integrity": "sha512-sr6GbP+4edBwFndLbM60gf07z0FQ79gaExpnsjMGePXqFcSSb7t6iscpjk9DhFhwd+mTEQrzNafGP8/iGGFYaA==", + "license": "MIT", + "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" + }, + "engines": { + "node": ">=18" + }, + "peerDependencies": { + "@cfworker/json-schema": "^4.1.1", + "zod": "^3.25 || ^4.0" + }, + "peerDependenciesMeta": { + "@cfworker/json-schema": { + "optional": true + }, + "zod": { + "optional": false + } + } + }, + "node_modules/@mswjs/interceptors": { + "version": "0.41.3", + "resolved": "https://registry.npmjs.org/@mswjs/interceptors/-/interceptors-0.41.3.tgz", + "integrity": "sha512-cXu86tF4VQVfwz8W1SPbhoRyHJkti6mjH/XJIxp40jhO4j2k1m4KYrEykxqWPkFF3vrK4rgQppBh//AwyGSXPA==", + "license": "MIT", + "dependencies": { + "@open-draft/deferred-promise": "^2.2.0", + "@open-draft/logger": "^0.3.0", + "@open-draft/until": "^2.0.0", + "is-node-process": "^1.2.0", + "outvariant": "^1.4.3", + "strict-event-emitter": "^0.5.1" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/@noble/ciphers": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/@noble/ciphers/-/ciphers-1.3.0.tgz", + "integrity": "sha512-2I0gnIVPtfnMw9ee9h1dJG7tp81+8Ob3OJb3Mv37rx5L40/b0i7djjCVvGOVqc9AEIQyvyu1i6ypKdFw8R8gQw==", + "license": "MIT", + "engines": { + "node": "^14.21.3 || >=16" + }, + "funding": { + "url": "https://paulmillr.com/funding/" + } + }, + "node_modules/@noble/curves": { + "version": "1.9.7", + "resolved": "https://registry.npmjs.org/@noble/curves/-/curves-1.9.7.tgz", + "integrity": "sha512-gbKGcRUYIjA3/zCCNaWDciTMFI0dCkvou3TL8Zmy5Nc7sJ47a0jtOeZoTaMxkuqRo9cRhjOdZJXegxYE5FN/xw==", + "license": "MIT", + "dependencies": { + "@noble/hashes": "1.8.0" + }, + "engines": { + "node": "^14.21.3 || >=16" + }, + "funding": { + "url": "https://paulmillr.com/funding/" + } + }, + "node_modules/@noble/hashes": { + "version": "1.8.0", + "resolved": "https://registry.npmjs.org/@noble/hashes/-/hashes-1.8.0.tgz", + "integrity": "sha512-jCs9ldd7NwzpgXDIf6P3+NrHh9/sD6CQdxHyjQI+h/6rDNo88ypBxxz45UDuZHz9r3tNz7N/VInSVoVdtXEI4A==", + "license": "MIT", + "engines": { + "node": "^14.21.3 || >=16" + }, + "funding": { + "url": "https://paulmillr.com/funding/" + } + }, + "node_modules/@nodelib/fs.scandir": { + "version": "2.1.5", + "resolved": "https://registry.npmjs.org/@nodelib/fs.scandir/-/fs.scandir-2.1.5.tgz", + "integrity": "sha512-vq24Bq3ym5HEQm2NKCr3yXDwjc7vTsEThRDnkp2DK9p1uqLR+DHurm/NOTo0KG7HYHU7eppKZj3MyqYuMBf62g==", + "license": "MIT", + "dependencies": { + "@nodelib/fs.stat": "2.0.5", + "run-parallel": "^1.1.9" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/@nodelib/fs.stat": { + "version": "2.0.5", + "resolved": "https://registry.npmjs.org/@nodelib/fs.stat/-/fs.stat-2.0.5.tgz", + "integrity": "sha512-RkhPPp2zrqDAQA/2jNhnztcPAlv64XdhIp7a7454A5ovI7Bukxgt7MX7udwAu3zg1DcpPU0rz3VV1SeaqvY4+A==", + "license": "MIT", + "engines": { + "node": ">= 8" + } + }, + "node_modules/@nodelib/fs.walk": { + "version": "1.2.8", + "resolved": "https://registry.npmjs.org/@nodelib/fs.walk/-/fs.walk-1.2.8.tgz", + "integrity": "sha512-oGB+UxlgWcgQkgwo8GcEGwemoTFt3FIO9ababBmaGwXIoBKZ+GTy0pP185beGg7Llih/NSHSV2XAs1lnznocSg==", + "license": "MIT", + "dependencies": { + "@nodelib/fs.scandir": "2.1.5", + "fastq": "^1.6.0" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/@open-draft/deferred-promise": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/@open-draft/deferred-promise/-/deferred-promise-2.2.0.tgz", + "integrity": "sha512-CecwLWx3rhxVQF6V4bAgPS5t+So2sTbPgAzafKkVizyi7tlwpcFpdFqq+wqF2OwNBmqFuu6tOyouTuxgpMfzmA==", + "license": "MIT" + }, + "node_modules/@open-draft/logger": { + "version": "0.3.0", + "resolved": "https://registry.npmjs.org/@open-draft/logger/-/logger-0.3.0.tgz", + "integrity": "sha512-X2g45fzhxH238HKO4xbSr7+wBS8Fvw6ixhTDuvLd5mqh6bJJCFAPwU9mPDxbcrRtfxv4u5IHCEH77BmxvXmmxQ==", + "license": "MIT", + "dependencies": { + "is-node-process": "^1.2.0", + "outvariant": "^1.4.0" + } + }, + "node_modules/@open-draft/until": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/@open-draft/until/-/until-2.1.0.tgz", + "integrity": "sha512-U69T3ItWHvLwGg5eJ0n3I62nWuE6ilHlmz7zM0npLBRvPRd7e6NYmg54vvRtP5mZG7kZqZCFVdsTWo7BPtBujg==", + "license": "MIT" + }, + "node_modules/@radix-ui/number": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/@radix-ui/number/-/number-1.1.1.tgz", + "integrity": "sha512-MkKCwxlXTgz6CFoJx3pCwn07GKp36+aZyu/u2Ln2VrA5DcdyCZkASEDBTd8x5whTQQL5CiYf4prXKLcgQdv29g==", + "license": "MIT" + }, + "node_modules/@radix-ui/primitive": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/@radix-ui/primitive/-/primitive-1.1.3.tgz", + "integrity": "sha512-JTF99U/6XIjCBo0wqkU5sK10glYe27MRRsfwoiq5zzOEZLHU3A3KCMa5X/azekYRCJ0HlwI0crAXS/5dEHTzDg==", + "license": "MIT" + }, + "node_modules/@radix-ui/react-accessible-icon": { + "version": "1.1.7", + "resolved": "https://registry.npmjs.org/@radix-ui/react-accessible-icon/-/react-accessible-icon-1.1.7.tgz", + "integrity": "sha512-XM+E4WXl0OqUJFovy6GjmxxFyx9opfCAIUku4dlKRd5YEPqt4kALOkQOp0Of6reHuUkJuiPBEc5k0o4z4lTC8A==", + "license": "MIT", + "dependencies": { + "@radix-ui/react-visually-hidden": "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" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-accordion": { + "version": "1.2.12", + "resolved": "https://registry.npmjs.org/@radix-ui/react-accordion/-/react-accordion-1.2.12.tgz", + "integrity": "sha512-T4nygeh9YE9dLRPhAHSeOZi7HBXo+0kYIPJXayZfvWOWA0+n3dESrZbjfDPUABkUNym6Hd+f2IR113To8D2GPA==", + "license": "MIT", + "dependencies": { + "@radix-ui/primitive": "1.1.3", + "@radix-ui/react-collapsible": "1.1.12", + "@radix-ui/react-collection": "1.1.7", + "@radix-ui/react-compose-refs": "1.1.2", + "@radix-ui/react-context": "1.1.2", + "@radix-ui/react-direction": "1.1.1", + "@radix-ui/react-id": "1.1.1", + "@radix-ui/react-primitive": "2.1.3", + "@radix-ui/react-use-controllable-state": "1.2.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" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-alert-dialog": { + "version": "1.1.15", + "resolved": "https://registry.npmjs.org/@radix-ui/react-alert-dialog/-/react-alert-dialog-1.1.15.tgz", + "integrity": "sha512-oTVLkEw5GpdRe29BqJ0LSDFWI3qu0vR1M0mUkOQWDIUnY/QIkLpgDMWuKxP94c2NAC2LGcgVhG1ImF3jkZ5wXw==", + "license": "MIT", + "dependencies": { + "@radix-ui/primitive": "1.1.3", + "@radix-ui/react-compose-refs": "1.1.2", + "@radix-ui/react-context": "1.1.2", + "@radix-ui/react-dialog": "1.1.15", + "@radix-ui/react-primitive": "2.1.3", + "@radix-ui/react-slot": "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" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-arrow": { + "version": "1.1.7", + "resolved": "https://registry.npmjs.org/@radix-ui/react-arrow/-/react-arrow-1.1.7.tgz", + "integrity": "sha512-F+M1tLhO+mlQaOWspE8Wstg+z6PwxwRd8oQ8IXceWz92kfAmalTRf0EjrouQeo7QssEPfCn05B4Ihs1K9WQ/7w==", + "license": "MIT", + "dependencies": { + "@radix-ui/react-primitive": "2.1.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" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-aspect-ratio": { + "version": "1.1.7", + "resolved": "https://registry.npmjs.org/@radix-ui/react-aspect-ratio/-/react-aspect-ratio-1.1.7.tgz", + "integrity": "sha512-Yq6lvO9HQyPwev1onK1daHCHqXVLzPhSVjmsNjCa2Zcxy2f7uJD2itDtxknv6FzAKCwD1qQkeVDmX/cev13n/g==", + "license": "MIT", + "dependencies": { + "@radix-ui/react-primitive": "2.1.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" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-avatar": { + "version": "1.1.10", + "resolved": "https://registry.npmjs.org/@radix-ui/react-avatar/-/react-avatar-1.1.10.tgz", + "integrity": "sha512-V8piFfWapM5OmNCXTzVQY+E1rDa53zY+MQ4Y7356v4fFz6vqCyUtIz2rUD44ZEdwg78/jKmMJHj07+C/Z/rcog==", + "license": "MIT", + "dependencies": { + "@radix-ui/react-context": "1.1.2", + "@radix-ui/react-primitive": "2.1.3", + "@radix-ui/react-use-callback-ref": "1.1.1", + "@radix-ui/react-use-is-hydrated": "0.1.0", + "@radix-ui/react-use-layout-effect": "1.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" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-checkbox": { + "version": "1.3.3", + "resolved": "https://registry.npmjs.org/@radix-ui/react-checkbox/-/react-checkbox-1.3.3.tgz", + "integrity": "sha512-wBbpv+NQftHDdG86Qc0pIyXk5IR3tM8Vd0nWLKDcX8nNn4nXFOFwsKuqw2okA/1D/mpaAkmuyndrPJTYDNZtFw==", + "license": "MIT", + "dependencies": { + "@radix-ui/primitive": "1.1.3", + "@radix-ui/react-compose-refs": "1.1.2", + "@radix-ui/react-context": "1.1.2", + "@radix-ui/react-presence": "1.1.5", + "@radix-ui/react-primitive": "2.1.3", + "@radix-ui/react-use-controllable-state": "1.2.2", + "@radix-ui/react-use-previous": "1.1.1", + "@radix-ui/react-use-size": "1.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" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-collapsible": { + "version": "1.1.12", + "resolved": "https://registry.npmjs.org/@radix-ui/react-collapsible/-/react-collapsible-1.1.12.tgz", + "integrity": "sha512-Uu+mSh4agx2ib1uIGPP4/CKNULyajb3p92LsVXmH2EHVMTfZWpll88XJ0j4W0z3f8NK1eYl1+Mf/szHPmcHzyA==", + "license": "MIT", + "dependencies": { + "@radix-ui/primitive": "1.1.3", + "@radix-ui/react-compose-refs": "1.1.2", + "@radix-ui/react-context": "1.1.2", + "@radix-ui/react-id": "1.1.1", + "@radix-ui/react-presence": "1.1.5", + "@radix-ui/react-primitive": "2.1.3", + "@radix-ui/react-use-controllable-state": "1.2.2", + "@radix-ui/react-use-layout-effect": "1.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" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-collection": { + "version": "1.1.7", + "resolved": "https://registry.npmjs.org/@radix-ui/react-collection/-/react-collection-1.1.7.tgz", + "integrity": "sha512-Fh9rGN0MoI4ZFUNyfFVNU4y9LUz93u9/0K+yLgA2bwRojxM8JU1DyvvMBabnZPBgMWREAJvU2jjVzq+LrFUglw==", + "license": "MIT", + "dependencies": { + "@radix-ui/react-compose-refs": "1.1.2", + "@radix-ui/react-context": "1.1.2", + "@radix-ui/react-primitive": "2.1.3", + "@radix-ui/react-slot": "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" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-compose-refs": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/@radix-ui/react-compose-refs/-/react-compose-refs-1.1.2.tgz", + "integrity": "sha512-z4eqJvfiNnFMHIIvXP3CY57y2WJs5g2v3X0zm9mEJkrkNv4rDxu+sg9Jh8EkXyeqBkB7SOcboo9dMVqhyrACIg==", + "license": "MIT", + "peerDependencies": { + "@types/react": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-context": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/@radix-ui/react-context/-/react-context-1.1.2.tgz", + "integrity": "sha512-jCi/QKUM2r1Ju5a3J64TH2A5SpKAgh0LpknyqdQ4m6DCV0xJ2HG1xARRwNGPQfi1SLdLWZ1OJz6F4OMBBNiGJA==", + "license": "MIT", + "peerDependencies": { + "@types/react": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-context-menu": { + "version": "2.2.16", + "resolved": "https://registry.npmjs.org/@radix-ui/react-context-menu/-/react-context-menu-2.2.16.tgz", + "integrity": "sha512-O8morBEW+HsVG28gYDZPTrT9UUovQUlJue5YO836tiTJhuIWBm/zQHc7j388sHWtdH/xUZurK9olD2+pcqx5ww==", + "license": "MIT", + "dependencies": { + "@radix-ui/primitive": "1.1.3", + "@radix-ui/react-context": "1.1.2", + "@radix-ui/react-menu": "2.1.16", + "@radix-ui/react-primitive": "2.1.3", + "@radix-ui/react-use-callback-ref": "1.1.1", + "@radix-ui/react-use-controllable-state": "1.2.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" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-dialog": { + "version": "1.1.15", + "resolved": "https://registry.npmjs.org/@radix-ui/react-dialog/-/react-dialog-1.1.15.tgz", + "integrity": "sha512-TCglVRtzlffRNxRMEyR36DGBLJpeusFcgMVD9PZEzAKnUs1lKCgX5u9BmC2Yg+LL9MgZDugFFs1Vl+Jp4t/PGw==", + "license": "MIT", + "dependencies": { + "@radix-ui/primitive": "1.1.3", + "@radix-ui/react-compose-refs": "1.1.2", + "@radix-ui/react-context": "1.1.2", + "@radix-ui/react-dismissable-layer": "1.1.11", + "@radix-ui/react-focus-guards": "1.1.3", + "@radix-ui/react-focus-scope": "1.1.7", + "@radix-ui/react-id": "1.1.1", + "@radix-ui/react-portal": "1.1.9", + "@radix-ui/react-presence": "1.1.5", + "@radix-ui/react-primitive": "2.1.3", + "@radix-ui/react-slot": "1.2.3", + "@radix-ui/react-use-controllable-state": "1.2.2", + "aria-hidden": "^1.2.4", + "react-remove-scroll": "^2.6.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" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-direction": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/@radix-ui/react-direction/-/react-direction-1.1.1.tgz", + "integrity": "sha512-1UEWRX6jnOA2y4H5WczZ44gOOjTEmlqv1uNW4GAJEO5+bauCBhv8snY65Iw5/VOS/ghKN9gr2KjnLKxrsvoMVw==", + "license": "MIT", + "peerDependencies": { + "@types/react": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-dismissable-layer": { + "version": "1.1.11", + "resolved": "https://registry.npmjs.org/@radix-ui/react-dismissable-layer/-/react-dismissable-layer-1.1.11.tgz", + "integrity": "sha512-Nqcp+t5cTB8BinFkZgXiMJniQH0PsUt2k51FUhbdfeKvc4ACcG2uQniY/8+h1Yv6Kza4Q7lD7PQV0z0oicE0Mg==", + "license": "MIT", + "dependencies": { + "@radix-ui/primitive": "1.1.3", + "@radix-ui/react-compose-refs": "1.1.2", + "@radix-ui/react-primitive": "2.1.3", + "@radix-ui/react-use-callback-ref": "1.1.1", + "@radix-ui/react-use-escape-keydown": "1.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" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-dropdown-menu": { + "version": "2.1.16", + "resolved": "https://registry.npmjs.org/@radix-ui/react-dropdown-menu/-/react-dropdown-menu-2.1.16.tgz", + "integrity": "sha512-1PLGQEynI/3OX/ftV54COn+3Sud/Mn8vALg2rWnBLnRaGtJDduNW/22XjlGgPdpcIbiQxjKtb7BkcjP00nqfJw==", + "license": "MIT", + "dependencies": { + "@radix-ui/primitive": "1.1.3", + "@radix-ui/react-compose-refs": "1.1.2", + "@radix-ui/react-context": "1.1.2", + "@radix-ui/react-id": "1.1.1", + "@radix-ui/react-menu": "2.1.16", + "@radix-ui/react-primitive": "2.1.3", + "@radix-ui/react-use-controllable-state": "1.2.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" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-focus-guards": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/@radix-ui/react-focus-guards/-/react-focus-guards-1.1.3.tgz", + "integrity": "sha512-0rFg/Rj2Q62NCm62jZw0QX7a3sz6QCQU0LpZdNrJX8byRGaGVTqbrW9jAoIAHyMQqsNpeZ81YgSizOt5WXq0Pw==", + "license": "MIT", + "peerDependencies": { + "@types/react": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-focus-scope": { + "version": "1.1.7", + "resolved": "https://registry.npmjs.org/@radix-ui/react-focus-scope/-/react-focus-scope-1.1.7.tgz", + "integrity": "sha512-t2ODlkXBQyn7jkl6TNaw/MtVEVvIGelJDCG41Okq/KwUsJBwQ4XVZsHAVUkK4mBv3ewiAS3PGuUWuY2BoK4ZUw==", + "license": "MIT", + "dependencies": { + "@radix-ui/react-compose-refs": "1.1.2", + "@radix-ui/react-primitive": "2.1.3", + "@radix-ui/react-use-callback-ref": "1.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" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-form": { + "version": "0.1.8", + "resolved": "https://registry.npmjs.org/@radix-ui/react-form/-/react-form-0.1.8.tgz", + "integrity": "sha512-QM70k4Zwjttifr5a4sZFts9fn8FzHYvQ5PiB19O2HsYibaHSVt9fH9rzB0XZo/YcM+b7t/p7lYCT/F5eOeF5yQ==", + "license": "MIT", + "dependencies": { + "@radix-ui/primitive": "1.1.3", + "@radix-ui/react-compose-refs": "1.1.2", + "@radix-ui/react-context": "1.1.2", + "@radix-ui/react-id": "1.1.1", + "@radix-ui/react-label": "2.1.7", + "@radix-ui/react-primitive": "2.1.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" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-hover-card": { + "version": "1.1.15", + "resolved": "https://registry.npmjs.org/@radix-ui/react-hover-card/-/react-hover-card-1.1.15.tgz", + "integrity": "sha512-qgTkjNT1CfKMoP0rcasmlH2r1DAiYicWsDsufxl940sT2wHNEWWv6FMWIQXWhVdmC1d/HYfbhQx60KYyAtKxjg==", + "license": "MIT", + "dependencies": { + "@radix-ui/primitive": "1.1.3", + "@radix-ui/react-compose-refs": "1.1.2", + "@radix-ui/react-context": "1.1.2", + "@radix-ui/react-dismissable-layer": "1.1.11", + "@radix-ui/react-popper": "1.2.8", + "@radix-ui/react-portal": "1.1.9", + "@radix-ui/react-presence": "1.1.5", + "@radix-ui/react-primitive": "2.1.3", + "@radix-ui/react-use-controllable-state": "1.2.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" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-id": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/@radix-ui/react-id/-/react-id-1.1.1.tgz", + "integrity": "sha512-kGkGegYIdQsOb4XjsfM97rXsiHaBwco+hFI66oO4s9LU+PLAC5oJ7khdOVFxkhsmlbpUqDAvXw11CluXP+jkHg==", + "license": "MIT", + "dependencies": { + "@radix-ui/react-use-layout-effect": "1.1.1" + }, + "peerDependencies": { + "@types/react": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-label": { + "version": "2.1.7", + "resolved": "https://registry.npmjs.org/@radix-ui/react-label/-/react-label-2.1.7.tgz", + "integrity": "sha512-YT1GqPSL8kJn20djelMX7/cTRp/Y9w5IZHvfxQTVHrOqa2yMl7i/UfMqKRU5V7mEyKTrUVgJXhNQPVCG8PBLoQ==", + "license": "MIT", + "dependencies": { + "@radix-ui/react-primitive": "2.1.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" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-menu": { + "version": "2.1.16", + "resolved": "https://registry.npmjs.org/@radix-ui/react-menu/-/react-menu-2.1.16.tgz", + "integrity": "sha512-72F2T+PLlphrqLcAotYPp0uJMr5SjP5SL01wfEspJbru5Zs5vQaSHb4VB3ZMJPimgHHCHG7gMOeOB9H3Hdmtxg==", + "license": "MIT", + "dependencies": { + "@radix-ui/primitive": "1.1.3", + "@radix-ui/react-collection": "1.1.7", + "@radix-ui/react-compose-refs": "1.1.2", + "@radix-ui/react-context": "1.1.2", + "@radix-ui/react-direction": "1.1.1", + "@radix-ui/react-dismissable-layer": "1.1.11", + "@radix-ui/react-focus-guards": "1.1.3", + "@radix-ui/react-focus-scope": "1.1.7", + "@radix-ui/react-id": "1.1.1", + "@radix-ui/react-popper": "1.2.8", + "@radix-ui/react-portal": "1.1.9", + "@radix-ui/react-presence": "1.1.5", + "@radix-ui/react-primitive": "2.1.3", + "@radix-ui/react-roving-focus": "1.1.11", + "@radix-ui/react-slot": "1.2.3", + "@radix-ui/react-use-callback-ref": "1.1.1", + "aria-hidden": "^1.2.4", + "react-remove-scroll": "^2.6.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" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-menubar": { + "version": "1.1.16", + "resolved": "https://registry.npmjs.org/@radix-ui/react-menubar/-/react-menubar-1.1.16.tgz", + "integrity": "sha512-EB1FktTz5xRRi2Er974AUQZWg2yVBb1yjip38/lgwtCVRd3a+maUoGHN/xs9Yv8SY8QwbSEb+YrxGadVWbEutA==", + "license": "MIT", + "dependencies": { + "@radix-ui/primitive": "1.1.3", + "@radix-ui/react-collection": "1.1.7", + "@radix-ui/react-compose-refs": "1.1.2", + "@radix-ui/react-context": "1.1.2", + "@radix-ui/react-direction": "1.1.1", + "@radix-ui/react-id": "1.1.1", + "@radix-ui/react-menu": "2.1.16", + "@radix-ui/react-primitive": "2.1.3", + "@radix-ui/react-roving-focus": "1.1.11", + "@radix-ui/react-use-controllable-state": "1.2.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" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-navigation-menu": { + "version": "1.2.14", + "resolved": "https://registry.npmjs.org/@radix-ui/react-navigation-menu/-/react-navigation-menu-1.2.14.tgz", + "integrity": "sha512-YB9mTFQvCOAQMHU+C/jVl96WmuWeltyUEpRJJky51huhds5W2FQr1J8D/16sQlf0ozxkPK8uF3niQMdUwZPv5w==", + "license": "MIT", + "dependencies": { + "@radix-ui/primitive": "1.1.3", + "@radix-ui/react-collection": "1.1.7", + "@radix-ui/react-compose-refs": "1.1.2", + "@radix-ui/react-context": "1.1.2", + "@radix-ui/react-direction": "1.1.1", + "@radix-ui/react-dismissable-layer": "1.1.11", + "@radix-ui/react-id": "1.1.1", + "@radix-ui/react-presence": "1.1.5", + "@radix-ui/react-primitive": "2.1.3", + "@radix-ui/react-use-callback-ref": "1.1.1", + "@radix-ui/react-use-controllable-state": "1.2.2", + "@radix-ui/react-use-layout-effect": "1.1.1", + "@radix-ui/react-use-previous": "1.1.1", + "@radix-ui/react-visually-hidden": "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" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-one-time-password-field": { + "version": "0.1.8", + "resolved": "https://registry.npmjs.org/@radix-ui/react-one-time-password-field/-/react-one-time-password-field-0.1.8.tgz", + "integrity": "sha512-ycS4rbwURavDPVjCb5iS3aG4lURFDILi6sKI/WITUMZ13gMmn/xGjpLoqBAalhJaDk8I3UbCM5GzKHrnzwHbvg==", + "license": "MIT", + "dependencies": { + "@radix-ui/number": "1.1.1", + "@radix-ui/primitive": "1.1.3", + "@radix-ui/react-collection": "1.1.7", + "@radix-ui/react-compose-refs": "1.1.2", + "@radix-ui/react-context": "1.1.2", + "@radix-ui/react-direction": "1.1.1", + "@radix-ui/react-primitive": "2.1.3", + "@radix-ui/react-roving-focus": "1.1.11", + "@radix-ui/react-use-controllable-state": "1.2.2", + "@radix-ui/react-use-effect-event": "0.0.2", + "@radix-ui/react-use-is-hydrated": "0.1.0", + "@radix-ui/react-use-layout-effect": "1.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" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-password-toggle-field": { + "version": "0.1.3", + "resolved": "https://registry.npmjs.org/@radix-ui/react-password-toggle-field/-/react-password-toggle-field-0.1.3.tgz", + "integrity": "sha512-/UuCrDBWravcaMix4TdT+qlNdVwOM1Nck9kWx/vafXsdfj1ChfhOdfi3cy9SGBpWgTXwYCuboT/oYpJy3clqfw==", + "license": "MIT", + "dependencies": { + "@radix-ui/primitive": "1.1.3", + "@radix-ui/react-compose-refs": "1.1.2", + "@radix-ui/react-context": "1.1.2", + "@radix-ui/react-id": "1.1.1", + "@radix-ui/react-primitive": "2.1.3", + "@radix-ui/react-use-controllable-state": "1.2.2", + "@radix-ui/react-use-effect-event": "0.0.2", + "@radix-ui/react-use-is-hydrated": "0.1.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" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-popover": { + "version": "1.1.15", + "resolved": "https://registry.npmjs.org/@radix-ui/react-popover/-/react-popover-1.1.15.tgz", + "integrity": "sha512-kr0X2+6Yy/vJzLYJUPCZEc8SfQcf+1COFoAqauJm74umQhta9M7lNJHP7QQS3vkvcGLQUbWpMzwrXYwrYztHKA==", + "license": "MIT", + "dependencies": { + "@radix-ui/primitive": "1.1.3", + "@radix-ui/react-compose-refs": "1.1.2", + "@radix-ui/react-context": "1.1.2", + "@radix-ui/react-dismissable-layer": "1.1.11", + "@radix-ui/react-focus-guards": "1.1.3", + "@radix-ui/react-focus-scope": "1.1.7", + "@radix-ui/react-id": "1.1.1", + "@radix-ui/react-popper": "1.2.8", + "@radix-ui/react-portal": "1.1.9", + "@radix-ui/react-presence": "1.1.5", + "@radix-ui/react-primitive": "2.1.3", + "@radix-ui/react-slot": "1.2.3", + "@radix-ui/react-use-controllable-state": "1.2.2", + "aria-hidden": "^1.2.4", + "react-remove-scroll": "^2.6.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" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-popper": { + "version": "1.2.8", + "resolved": "https://registry.npmjs.org/@radix-ui/react-popper/-/react-popper-1.2.8.tgz", + "integrity": "sha512-0NJQ4LFFUuWkE7Oxf0htBKS6zLkkjBH+hM1uk7Ng705ReR8m/uelduy1DBo0PyBXPKVnBA6YBlU94MBGXrSBCw==", + "license": "MIT", + "dependencies": { + "@floating-ui/react-dom": "^2.0.0", + "@radix-ui/react-arrow": "1.1.7", + "@radix-ui/react-compose-refs": "1.1.2", + "@radix-ui/react-context": "1.1.2", + "@radix-ui/react-primitive": "2.1.3", + "@radix-ui/react-use-callback-ref": "1.1.1", + "@radix-ui/react-use-layout-effect": "1.1.1", + "@radix-ui/react-use-rect": "1.1.1", + "@radix-ui/react-use-size": "1.1.1", + "@radix-ui/rect": "1.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" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-portal": { + "version": "1.1.9", + "resolved": "https://registry.npmjs.org/@radix-ui/react-portal/-/react-portal-1.1.9.tgz", + "integrity": "sha512-bpIxvq03if6UNwXZ+HTK71JLh4APvnXntDc6XOX8UVq4XQOVl7lwok0AvIl+b8zgCw3fSaVTZMpAPPagXbKmHQ==", + "license": "MIT", + "dependencies": { + "@radix-ui/react-primitive": "2.1.3", + "@radix-ui/react-use-layout-effect": "1.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" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-presence": { + "version": "1.1.5", + "resolved": "https://registry.npmjs.org/@radix-ui/react-presence/-/react-presence-1.1.5.tgz", + "integrity": "sha512-/jfEwNDdQVBCNvjkGit4h6pMOzq8bHkopq458dPt2lMjx+eBQUohZNG9A7DtO/O5ukSbxuaNGXMjHicgwy6rQQ==", + "license": "MIT", + "dependencies": { + "@radix-ui/react-compose-refs": "1.1.2", + "@radix-ui/react-use-layout-effect": "1.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" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-primitive": { + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/@radix-ui/react-primitive/-/react-primitive-2.1.3.tgz", + "integrity": "sha512-m9gTwRkhy2lvCPe6QJp4d3G1TYEUHn/FzJUtq9MjH46an1wJU+GdoGC5VLof8RX8Ft/DlpshApkhswDLZzHIcQ==", + "license": "MIT", + "dependencies": { + "@radix-ui/react-slot": "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" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-progress": { + "version": "1.1.7", + "resolved": "https://registry.npmjs.org/@radix-ui/react-progress/-/react-progress-1.1.7.tgz", + "integrity": "sha512-vPdg/tF6YC/ynuBIJlk1mm7Le0VgW6ub6J2UWnTQ7/D23KXcPI1qy+0vBkgKgd38RCMJavBXpB83HPNFMTb0Fg==", + "license": "MIT", + "dependencies": { + "@radix-ui/react-context": "1.1.2", + "@radix-ui/react-primitive": "2.1.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" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-radio-group": { + "version": "1.3.8", + "resolved": "https://registry.npmjs.org/@radix-ui/react-radio-group/-/react-radio-group-1.3.8.tgz", + "integrity": "sha512-VBKYIYImA5zsxACdisNQ3BjCBfmbGH3kQlnFVqlWU4tXwjy7cGX8ta80BcrO+WJXIn5iBylEH3K6ZTlee//lgQ==", + "license": "MIT", + "dependencies": { + "@radix-ui/primitive": "1.1.3", + "@radix-ui/react-compose-refs": "1.1.2", + "@radix-ui/react-context": "1.1.2", + "@radix-ui/react-direction": "1.1.1", + "@radix-ui/react-presence": "1.1.5", + "@radix-ui/react-primitive": "2.1.3", + "@radix-ui/react-roving-focus": "1.1.11", + "@radix-ui/react-use-controllable-state": "1.2.2", + "@radix-ui/react-use-previous": "1.1.1", + "@radix-ui/react-use-size": "1.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" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-roving-focus": { + "version": "1.1.11", + "resolved": "https://registry.npmjs.org/@radix-ui/react-roving-focus/-/react-roving-focus-1.1.11.tgz", + "integrity": "sha512-7A6S9jSgm/S+7MdtNDSb+IU859vQqJ/QAtcYQcfFC6W8RS4IxIZDldLR0xqCFZ6DCyrQLjLPsxtTNch5jVA4lA==", + "license": "MIT", + "dependencies": { + "@radix-ui/primitive": "1.1.3", + "@radix-ui/react-collection": "1.1.7", + "@radix-ui/react-compose-refs": "1.1.2", + "@radix-ui/react-context": "1.1.2", + "@radix-ui/react-direction": "1.1.1", + "@radix-ui/react-id": "1.1.1", + "@radix-ui/react-primitive": "2.1.3", + "@radix-ui/react-use-callback-ref": "1.1.1", + "@radix-ui/react-use-controllable-state": "1.2.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" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-scroll-area": { + "version": "1.2.10", + "resolved": "https://registry.npmjs.org/@radix-ui/react-scroll-area/-/react-scroll-area-1.2.10.tgz", + "integrity": "sha512-tAXIa1g3sM5CGpVT0uIbUx/U3Gs5N8T52IICuCtObaos1S8fzsrPXG5WObkQN3S6NVl6wKgPhAIiBGbWnvc97A==", + "license": "MIT", + "dependencies": { + "@radix-ui/number": "1.1.1", + "@radix-ui/primitive": "1.1.3", + "@radix-ui/react-compose-refs": "1.1.2", + "@radix-ui/react-context": "1.1.2", + "@radix-ui/react-direction": "1.1.1", + "@radix-ui/react-presence": "1.1.5", + "@radix-ui/react-primitive": "2.1.3", + "@radix-ui/react-use-callback-ref": "1.1.1", + "@radix-ui/react-use-layout-effect": "1.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" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-select": { + "version": "2.2.6", + "resolved": "https://registry.npmjs.org/@radix-ui/react-select/-/react-select-2.2.6.tgz", + "integrity": "sha512-I30RydO+bnn2PQztvo25tswPH+wFBjehVGtmagkU78yMdwTwVf12wnAOF+AeP8S2N8xD+5UPbGhkUfPyvT+mwQ==", + "license": "MIT", + "dependencies": { + "@radix-ui/number": "1.1.1", + "@radix-ui/primitive": "1.1.3", + "@radix-ui/react-collection": "1.1.7", + "@radix-ui/react-compose-refs": "1.1.2", + "@radix-ui/react-context": "1.1.2", + "@radix-ui/react-direction": "1.1.1", + "@radix-ui/react-dismissable-layer": "1.1.11", + "@radix-ui/react-focus-guards": "1.1.3", + "@radix-ui/react-focus-scope": "1.1.7", + "@radix-ui/react-id": "1.1.1", + "@radix-ui/react-popper": "1.2.8", + "@radix-ui/react-portal": "1.1.9", + "@radix-ui/react-primitive": "2.1.3", + "@radix-ui/react-slot": "1.2.3", + "@radix-ui/react-use-callback-ref": "1.1.1", + "@radix-ui/react-use-controllable-state": "1.2.2", + "@radix-ui/react-use-layout-effect": "1.1.1", + "@radix-ui/react-use-previous": "1.1.1", + "@radix-ui/react-visually-hidden": "1.2.3", + "aria-hidden": "^1.2.4", + "react-remove-scroll": "^2.6.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" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-separator": { + "version": "1.1.7", + "resolved": "https://registry.npmjs.org/@radix-ui/react-separator/-/react-separator-1.1.7.tgz", + "integrity": "sha512-0HEb8R9E8A+jZjvmFCy/J4xhbXy3TV+9XSnGJ3KvTtjlIUy/YQ/p6UYZvi7YbeoeXdyU9+Y3scizK6hkY37baA==", + "license": "MIT", + "dependencies": { + "@radix-ui/react-primitive": "2.1.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" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-slider": { + "version": "1.3.6", + "resolved": "https://registry.npmjs.org/@radix-ui/react-slider/-/react-slider-1.3.6.tgz", + "integrity": "sha512-JPYb1GuM1bxfjMRlNLE+BcmBC8onfCi60Blk7OBqi2MLTFdS+8401U4uFjnwkOr49BLmXxLC6JHkvAsx5OJvHw==", + "license": "MIT", + "dependencies": { + "@radix-ui/number": "1.1.1", + "@radix-ui/primitive": "1.1.3", + "@radix-ui/react-collection": "1.1.7", + "@radix-ui/react-compose-refs": "1.1.2", + "@radix-ui/react-context": "1.1.2", + "@radix-ui/react-direction": "1.1.1", + "@radix-ui/react-primitive": "2.1.3", + "@radix-ui/react-use-controllable-state": "1.2.2", + "@radix-ui/react-use-layout-effect": "1.1.1", + "@radix-ui/react-use-previous": "1.1.1", + "@radix-ui/react-use-size": "1.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" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-slot": { + "version": "1.2.3", + "resolved": "https://registry.npmjs.org/@radix-ui/react-slot/-/react-slot-1.2.3.tgz", + "integrity": "sha512-aeNmHnBxbi2St0au6VBVC7JXFlhLlOnvIIlePNniyUNAClzmtAUEY8/pBiK3iHjufOlwA+c20/8jngo7xcrg8A==", + "license": "MIT", + "dependencies": { + "@radix-ui/react-compose-refs": "1.1.2" + }, + "peerDependencies": { + "@types/react": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-switch": { + "version": "1.2.6", + "resolved": "https://registry.npmjs.org/@radix-ui/react-switch/-/react-switch-1.2.6.tgz", + "integrity": "sha512-bByzr1+ep1zk4VubeEVViV592vu2lHE2BZY5OnzehZqOOgogN80+mNtCqPkhn2gklJqOpxWgPoYTSnhBCqpOXQ==", + "license": "MIT", + "dependencies": { + "@radix-ui/primitive": "1.1.3", + "@radix-ui/react-compose-refs": "1.1.2", + "@radix-ui/react-context": "1.1.2", + "@radix-ui/react-primitive": "2.1.3", + "@radix-ui/react-use-controllable-state": "1.2.2", + "@radix-ui/react-use-previous": "1.1.1", + "@radix-ui/react-use-size": "1.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" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-tabs": { + "version": "1.1.13", + "resolved": "https://registry.npmjs.org/@radix-ui/react-tabs/-/react-tabs-1.1.13.tgz", + "integrity": "sha512-7xdcatg7/U+7+Udyoj2zodtI9H/IIopqo+YOIcZOq1nJwXWBZ9p8xiu5llXlekDbZkca79a/fozEYQXIA4sW6A==", + "license": "MIT", + "dependencies": { + "@radix-ui/primitive": "1.1.3", + "@radix-ui/react-context": "1.1.2", + "@radix-ui/react-direction": "1.1.1", + "@radix-ui/react-id": "1.1.1", + "@radix-ui/react-presence": "1.1.5", + "@radix-ui/react-primitive": "2.1.3", + "@radix-ui/react-roving-focus": "1.1.11", + "@radix-ui/react-use-controllable-state": "1.2.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" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-toast": { + "version": "1.2.15", + "resolved": "https://registry.npmjs.org/@radix-ui/react-toast/-/react-toast-1.2.15.tgz", + "integrity": "sha512-3OSz3TacUWy4WtOXV38DggwxoqJK4+eDkNMl5Z/MJZaoUPaP4/9lf81xXMe1I2ReTAptverZUpbPY4wWwWyL5g==", + "license": "MIT", + "dependencies": { + "@radix-ui/primitive": "1.1.3", + "@radix-ui/react-collection": "1.1.7", + "@radix-ui/react-compose-refs": "1.1.2", + "@radix-ui/react-context": "1.1.2", + "@radix-ui/react-dismissable-layer": "1.1.11", + "@radix-ui/react-portal": "1.1.9", + "@radix-ui/react-presence": "1.1.5", + "@radix-ui/react-primitive": "2.1.3", + "@radix-ui/react-use-callback-ref": "1.1.1", + "@radix-ui/react-use-controllable-state": "1.2.2", + "@radix-ui/react-use-layout-effect": "1.1.1", + "@radix-ui/react-visually-hidden": "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" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-toggle": { + "version": "1.1.10", + "resolved": "https://registry.npmjs.org/@radix-ui/react-toggle/-/react-toggle-1.1.10.tgz", + "integrity": "sha512-lS1odchhFTeZv3xwHH31YPObmJn8gOg7Lq12inrr0+BH/l3Tsq32VfjqH1oh80ARM3mlkfMic15n0kg4sD1poQ==", + "license": "MIT", + "dependencies": { + "@radix-ui/primitive": "1.1.3", + "@radix-ui/react-primitive": "2.1.3", + "@radix-ui/react-use-controllable-state": "1.2.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" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-toggle-group": { + "version": "1.1.11", + "resolved": "https://registry.npmjs.org/@radix-ui/react-toggle-group/-/react-toggle-group-1.1.11.tgz", + "integrity": "sha512-5umnS0T8JQzQT6HbPyO7Hh9dgd82NmS36DQr+X/YJ9ctFNCiiQd6IJAYYZ33LUwm8M+taCz5t2ui29fHZc4Y6Q==", + "license": "MIT", + "dependencies": { + "@radix-ui/primitive": "1.1.3", + "@radix-ui/react-context": "1.1.2", + "@radix-ui/react-direction": "1.1.1", + "@radix-ui/react-primitive": "2.1.3", + "@radix-ui/react-roving-focus": "1.1.11", + "@radix-ui/react-toggle": "1.1.10", + "@radix-ui/react-use-controllable-state": "1.2.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" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-toolbar": { + "version": "1.1.11", + "resolved": "https://registry.npmjs.org/@radix-ui/react-toolbar/-/react-toolbar-1.1.11.tgz", + "integrity": "sha512-4ol06/1bLoFu1nwUqzdD4Y5RZ9oDdKeiHIsntug54Hcr1pgaHiPqHFEaXI1IFP/EsOfROQZ8Mig9VTIRza6Tjg==", + "license": "MIT", + "dependencies": { + "@radix-ui/primitive": "1.1.3", + "@radix-ui/react-context": "1.1.2", + "@radix-ui/react-direction": "1.1.1", + "@radix-ui/react-primitive": "2.1.3", + "@radix-ui/react-roving-focus": "1.1.11", + "@radix-ui/react-separator": "1.1.7", + "@radix-ui/react-toggle-group": "1.1.11" + }, + "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" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-tooltip": { + "version": "1.2.8", + "resolved": "https://registry.npmjs.org/@radix-ui/react-tooltip/-/react-tooltip-1.2.8.tgz", + "integrity": "sha512-tY7sVt1yL9ozIxvmbtN5qtmH2krXcBCfjEiCgKGLqunJHvgvZG2Pcl2oQ3kbcZARb1BGEHdkLzcYGO8ynVlieg==", + "license": "MIT", + "dependencies": { + "@radix-ui/primitive": "1.1.3", + "@radix-ui/react-compose-refs": "1.1.2", + "@radix-ui/react-context": "1.1.2", + "@radix-ui/react-dismissable-layer": "1.1.11", + "@radix-ui/react-id": "1.1.1", + "@radix-ui/react-popper": "1.2.8", + "@radix-ui/react-portal": "1.1.9", + "@radix-ui/react-presence": "1.1.5", + "@radix-ui/react-primitive": "2.1.3", + "@radix-ui/react-slot": "1.2.3", + "@radix-ui/react-use-controllable-state": "1.2.2", + "@radix-ui/react-visually-hidden": "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" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-use-callback-ref": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/@radix-ui/react-use-callback-ref/-/react-use-callback-ref-1.1.1.tgz", + "integrity": "sha512-FkBMwD+qbGQeMu1cOHnuGB6x4yzPjho8ap5WtbEJ26umhgqVXbhekKUQO+hZEL1vU92a3wHwdp0HAcqAUF5iDg==", + "license": "MIT", + "peerDependencies": { + "@types/react": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-use-controllable-state": { + "version": "1.2.2", + "resolved": "https://registry.npmjs.org/@radix-ui/react-use-controllable-state/-/react-use-controllable-state-1.2.2.tgz", + "integrity": "sha512-BjasUjixPFdS+NKkypcyyN5Pmg83Olst0+c6vGov0diwTEo6mgdqVR6hxcEgFuh4QrAs7Rc+9KuGJ9TVCj0Zzg==", + "license": "MIT", + "dependencies": { + "@radix-ui/react-use-effect-event": "0.0.2", + "@radix-ui/react-use-layout-effect": "1.1.1" + }, + "peerDependencies": { + "@types/react": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-use-effect-event": { + "version": "0.0.2", + "resolved": "https://registry.npmjs.org/@radix-ui/react-use-effect-event/-/react-use-effect-event-0.0.2.tgz", + "integrity": "sha512-Qp8WbZOBe+blgpuUT+lw2xheLP8q0oatc9UpmiemEICxGvFLYmHm9QowVZGHtJlGbS6A6yJ3iViad/2cVjnOiA==", + "license": "MIT", + "dependencies": { + "@radix-ui/react-use-layout-effect": "1.1.1" + }, + "peerDependencies": { + "@types/react": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-use-escape-keydown": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/@radix-ui/react-use-escape-keydown/-/react-use-escape-keydown-1.1.1.tgz", + "integrity": "sha512-Il0+boE7w/XebUHyBjroE+DbByORGR9KKmITzbR7MyQ4akpORYP/ZmbhAr0DG7RmmBqoOnZdy2QlvajJ2QA59g==", + "license": "MIT", + "dependencies": { + "@radix-ui/react-use-callback-ref": "1.1.1" + }, + "peerDependencies": { + "@types/react": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-use-is-hydrated": { + "version": "0.1.0", + "resolved": "https://registry.npmjs.org/@radix-ui/react-use-is-hydrated/-/react-use-is-hydrated-0.1.0.tgz", + "integrity": "sha512-U+UORVEq+cTnRIaostJv9AGdV3G6Y+zbVd+12e18jQ5A3c0xL03IhnHuiU4UV69wolOQp5GfR58NW/EgdQhwOA==", + "license": "MIT", + "dependencies": { + "use-sync-external-store": "^1.5.0" + }, + "peerDependencies": { + "@types/react": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-use-layout-effect": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/@radix-ui/react-use-layout-effect/-/react-use-layout-effect-1.1.1.tgz", + "integrity": "sha512-RbJRS4UWQFkzHTTwVymMTUv8EqYhOp8dOOviLj2ugtTiXRaRQS7GLGxZTLL1jWhMeoSCf5zmcZkqTl9IiYfXcQ==", + "license": "MIT", + "peerDependencies": { + "@types/react": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-use-previous": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/@radix-ui/react-use-previous/-/react-use-previous-1.1.1.tgz", + "integrity": "sha512-2dHfToCj/pzca2Ck724OZ5L0EVrr3eHRNsG/b3xQJLA2hZpVCS99bLAX+hm1IHXDEnzU6by5z/5MIY794/a8NQ==", + "license": "MIT", + "peerDependencies": { + "@types/react": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-use-rect": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/@radix-ui/react-use-rect/-/react-use-rect-1.1.1.tgz", + "integrity": "sha512-QTYuDesS0VtuHNNvMh+CjlKJ4LJickCMUAqjlE3+j8w+RlRpwyX3apEQKGFzbZGdo7XNG1tXa+bQqIE7HIXT2w==", + "license": "MIT", + "dependencies": { + "@radix-ui/rect": "1.1.1" + }, + "peerDependencies": { + "@types/react": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-use-size": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/@radix-ui/react-use-size/-/react-use-size-1.1.1.tgz", + "integrity": "sha512-ewrXRDTAqAXlkl6t/fkXWNAhFX9I+CkKlw6zjEwk86RSPKwZr3xpBRso655aqYafwtnbpHLj6toFzmd6xdVptQ==", + "license": "MIT", + "dependencies": { + "@radix-ui/react-use-layout-effect": "1.1.1" + }, + "peerDependencies": { + "@types/react": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-visually-hidden": { + "version": "1.2.3", + "resolved": "https://registry.npmjs.org/@radix-ui/react-visually-hidden/-/react-visually-hidden-1.2.3.tgz", + "integrity": "sha512-pzJq12tEaaIhqjbzpCuv/OypJY/BPavOofm+dbab+MHLajy277+1lLm6JFcGgF5eskJ6mquGirhXY2GD/8u8Ug==", + "license": "MIT", + "dependencies": { + "@radix-ui/react-primitive": "2.1.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" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, + "node_modules/@radix-ui/rect": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/@radix-ui/rect/-/rect-1.1.1.tgz", + "integrity": "sha512-HPwpGIzkl28mWyZqG52jiqDJ12waP11Pa1lGoiyUkIEuMLBP0oeK/C89esbXrxsky5we7dfd8U58nm0SgAWpVw==", + "license": "MIT" + }, "node_modules/@rolldown/pluginutils": { "version": "1.0.0-beta.27", "resolved": "https://registry.npmjs.org/@rolldown/pluginutils/-/pluginutils-1.0.0-beta.27.tgz", @@ -1156,6 +3334,50 @@ "win32" ] }, + "node_modules/@sec-ant/readable-stream": { + "version": "0.4.1", + "resolved": "https://registry.npmjs.org/@sec-ant/readable-stream/-/readable-stream-0.4.1.tgz", + "integrity": "sha512-831qok9r2t8AlxLko40y2ebgSDhenenCatLVeW/uBtnHPyhHOvG0C7TvfgecV+wHzIm5KUICgzmVpWS+IMEAeg==", + "license": "MIT" + }, + "node_modules/@sindresorhus/merge-streams": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/@sindresorhus/merge-streams/-/merge-streams-4.0.0.tgz", + "integrity": "sha512-tlqY9xq5ukxTUZBmoOp+m61cqwQD5pHJtFY3Mn8CA8ps6yghLH/Hw8UPdqg4OLmFW3IFlcXnQNmo/dh8HzXYIQ==", + "license": "MIT", + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/@tabler/icons": { + "version": "3.40.0", + "resolved": "https://registry.npmjs.org/@tabler/icons/-/icons-3.40.0.tgz", + "integrity": "sha512-V/Q4VgNPKubRTiLdmWjV/zscYcj5IIk+euicUtaVVqF6luSC9rDngYWgST5/yh3Mrg/mYUwRv1YVTk71Jp0twQ==", + "license": "MIT", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/codecalm" + } + }, + "node_modules/@tabler/icons-react": { + "version": "3.40.0", + "resolved": "https://registry.npmjs.org/@tabler/icons-react/-/icons-react-3.40.0.tgz", + "integrity": "sha512-oO5+6QCnna4a//mYubx4euZfECtzQZFDGsDMIdzZUhbdyBCT+3bRVFBPueGIcemWld4Vb/0UQ39C/cmGfGylAg==", + "license": "MIT", + "dependencies": { + "@tabler/icons": "3.40.0" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/codecalm" + }, + "peerDependencies": { + "react": ">= 16" + } + }, "node_modules/@tailwindcss/node": { "version": "4.2.1", "resolved": "https://registry.npmjs.org/@tailwindcss/node/-/node-4.2.1.tgz", @@ -1459,13 +3681,41 @@ "version": "2.10.1", "resolved": "https://registry.npmjs.org/@tauri-apps/api/-/api-2.10.1.tgz", "integrity": "sha512-hKL/jWf293UDSUN09rR69hrToyIXBb8CjGaWC7gfinvnQrBVvnLr08FeFi38gxtugAVyVcTa5/FD/Xnkb1siBw==", - "dev": true, "license": "Apache-2.0 OR MIT", "funding": { "type": "opencollective", "url": "https://opencollective.com/tauri" } }, + "node_modules/@tauri-apps/plugin-dialog": { + "version": "2.6.0", + "resolved": "https://registry.npmjs.org/@tauri-apps/plugin-dialog/-/plugin-dialog-2.6.0.tgz", + "integrity": "sha512-q4Uq3eY87TdcYzXACiYSPhmpBA76shgmQswGkSVio4C82Sz2W4iehe9TnKYwbq7weHiL88Yw19XZm7v28+Micg==", + "license": "MIT OR Apache-2.0", + "dependencies": { + "@tauri-apps/api": "^2.8.0" + } + }, + "node_modules/@tauri-apps/plugin-shell": { + "version": "2.3.5", + "resolved": "https://registry.npmjs.org/@tauri-apps/plugin-shell/-/plugin-shell-2.3.5.tgz", + "integrity": "sha512-jewtULhiQ7lI7+owCKAjc8tYLJr92U16bPOeAa472LHJdgaibLP83NcfAF2e+wkEcA53FxKQAZ7byDzs2eeizg==", + "license": "MIT OR Apache-2.0", + "dependencies": { + "@tauri-apps/api": "^2.10.1" + } + }, + "node_modules/@ts-morph/common": { + "version": "0.27.0", + "resolved": "https://registry.npmjs.org/@ts-morph/common/-/common-0.27.0.tgz", + "integrity": "sha512-Wf29UqxWDpc+i61k3oIOzcUfQt79PIT9y/MWfAGlrkjg6lBC1hwDECLXPVJAhWjiGbfBCxZd65F/LIZF3+jeJQ==", + "license": "MIT", + "dependencies": { + "fast-glob": "^3.3.3", + "minimatch": "^10.0.1", + "path-browserify": "^1.0.1" + } + }, "node_modules/@types/babel__core": { "version": "7.20.5", "resolved": "https://registry.npmjs.org/@types/babel__core/-/babel__core-7.20.5.tgz", @@ -1518,6 +3768,16 @@ "dev": true, "license": "MIT" }, + "node_modules/@types/node": { + "version": "25.5.0", + "resolved": "https://registry.npmjs.org/@types/node/-/node-25.5.0.tgz", + "integrity": "sha512-jp2P3tQMSxWugkCUKLRPVUpGaL5MVFwF8RDuSRztfwgN1wmqJeMSbKlnEtQqU8UrhTmzEmZdu2I6v2dpp7XIxw==", + "devOptional": true, + "license": "MIT", + "dependencies": { + "undici-types": "~7.18.0" + } + }, "node_modules/@types/react": { "version": "19.2.14", "resolved": "https://registry.npmjs.org/@types/react/-/react-19.2.14.tgz", @@ -1532,12 +3792,24 @@ "version": "19.2.3", "resolved": "https://registry.npmjs.org/@types/react-dom/-/react-dom-19.2.3.tgz", "integrity": "sha512-jp2L/eY6fn+KgVVQAOqYItbF0VY/YApe5Mz2F0aykSO8gx31bYCZyvSeYxCHKvzHG5eZjc+zyaS5BrBWya2+kQ==", - "dev": true, + "devOptional": true, "license": "MIT", "peerDependencies": { "@types/react": "^19.2.0" } }, + "node_modules/@types/statuses": { + "version": "2.0.6", + "resolved": "https://registry.npmjs.org/@types/statuses/-/statuses-2.0.6.tgz", + "integrity": "sha512-xMAgYwceFhRA2zY+XbEA7mxYbA093wdiW8Vu6gZPGWy9cmOyU9XesH1tNcEWsKFd5Vzrqx5T3D38PWx1FIIXkA==", + "license": "MIT" + }, + "node_modules/@types/validate-npm-package-name": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/@types/validate-npm-package-name/-/validate-npm-package-name-4.0.2.tgz", + "integrity": "sha512-lrpDziQipxCEeK5kWxvljWYhUvOiB2A9izZd9B2AFarYAkqZshb4lPbRs7zKEic6eGtH8V/2qJW+dPp9OtF6bw==", + "license": "MIT" + }, "node_modules/@vitejs/plugin-react": { "version": "4.7.0", "resolved": "https://registry.npmjs.org/@vitejs/plugin-react/-/plugin-react-4.7.0.tgz", @@ -1559,11 +3831,140 @@ "vite": "^4.2.0 || ^5.0.0 || ^6.0.0 || ^7.0.0" } }, + "node_modules/accepts": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/accepts/-/accepts-2.0.0.tgz", + "integrity": "sha512-5cvg6CtKwfgdmVqY1WIiXKc3Q1bkRqGLi+2W/6ao+6Y7gu/RCwRuAhGEzh5B4KlszSuTLgZYuqFqo5bImjNKng==", + "license": "MIT", + "dependencies": { + "mime-types": "^3.0.0", + "negotiator": "^1.0.0" + }, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/agent-base": { + "version": "7.1.4", + "resolved": "https://registry.npmjs.org/agent-base/-/agent-base-7.1.4.tgz", + "integrity": "sha512-MnA+YT8fwfJPgBx3m60MNqakm30XOkyIoH1y6huTQvC0PwZG7ki8NacLBcrPbNoo8vEZy7Jpuk7+jMO+CUovTQ==", + "license": "MIT", + "engines": { + "node": ">= 14" + } + }, + "node_modules/ajv": { + "version": "8.18.0", + "resolved": "https://registry.npmjs.org/ajv/-/ajv-8.18.0.tgz", + "integrity": "sha512-PlXPeEWMXMZ7sPYOHqmDyCJzcfNrUr3fGNKtezX14ykXOEIvyK81d+qydx89KY5O71FKMPaQ2vBfBFI5NHR63A==", + "license": "MIT", + "dependencies": { + "fast-deep-equal": "^3.1.3", + "fast-uri": "^3.0.1", + "json-schema-traverse": "^1.0.0", + "require-from-string": "^2.0.2" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/epoberezkin" + } + }, + "node_modules/ajv-formats": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/ajv-formats/-/ajv-formats-3.0.1.tgz", + "integrity": "sha512-8iUql50EUR+uUcdRQ3HDqa6EVyo3docL8g5WJ3FNcWmu62IbkGUue/pEyLBW8VGKKucTPgqeks4fIU1DA4yowQ==", + "license": "MIT", + "dependencies": { + "ajv": "^8.0.0" + }, + "peerDependencies": { + "ajv": "^8.0.0" + }, + "peerDependenciesMeta": { + "ajv": { + "optional": true + } + } + }, + "node_modules/ansi-regex": { + "version": "6.2.2", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-6.2.2.tgz", + "integrity": "sha512-Bq3SmSpyFHaWjPk8If9yc6svM8c56dB5BAtW4Qbw5jHTwwXXcTLoRMkpDJp6VL0XzlWaCHTXrkFURMYmD0sLqg==", + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/chalk/ansi-regex?sponsor=1" + } + }, + "node_modules/ansi-styles": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", + "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", + "license": "MIT", + "dependencies": { + "color-convert": "^2.0.1" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, + "node_modules/ansis": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/ansis/-/ansis-4.2.0.tgz", + "integrity": "sha512-HqZ5rWlFjGiV0tDm3UxxgNRqsOTniqoKZu0pIAfh7TZQMGuZK+hH0drySty0si0QXj1ieop4+SkSfPZBPPkHig==", + "license": "ISC", + "engines": { + "node": ">=14" + } + }, + "node_modules/argparse": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/argparse/-/argparse-2.0.1.tgz", + "integrity": "sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q==", + "license": "Python-2.0" + }, + "node_modules/aria-hidden": { + "version": "1.2.6", + "resolved": "https://registry.npmjs.org/aria-hidden/-/aria-hidden-1.2.6.tgz", + "integrity": "sha512-ik3ZgC9dY/lYVVM++OISsaYDeg1tb0VtP5uL3ouh1koGOaUMDPpbFIei4JkFimWUFPn90sbMNMXQAIVOlnYKJA==", + "license": "MIT", + "dependencies": { + "tslib": "^2.0.0" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/ast-types": { + "version": "0.16.1", + "resolved": "https://registry.npmjs.org/ast-types/-/ast-types-0.16.1.tgz", + "integrity": "sha512-6t10qk83GOG8p0vKmaCr8eiilZwO171AvbROMtvvNiwrTly62t+7XkA8RdIIVbpMhCASAsxgAzdRSwh6nw/5Dg==", + "license": "MIT", + "dependencies": { + "tslib": "^2.0.1" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/balanced-match": { + "version": "4.0.4", + "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-4.0.4.tgz", + "integrity": "sha512-BLrgEcRTwX2o6gGxGOCNyMvGSp35YofuYzw9h1IMTRmKqttAZZVU67bdb9Pr2vUHA8+j3i2tJfjO6C6+4myGTA==", + "license": "MIT", + "engines": { + "node": "18 || 20 || >=22" + } + }, "node_modules/baseline-browser-mapping": { "version": "2.10.7", "resolved": "https://registry.npmjs.org/baseline-browser-mapping/-/baseline-browser-mapping-2.10.7.tgz", "integrity": "sha512-1ghYO3HnxGec0TCGBXiDLVns4eCSx4zJpxnHrlqFQajmhfKMQBzUGDdkMK7fUW7PTHTeLf+j87aTuKuuwWzMGw==", - "dev": true, "license": "Apache-2.0", "bin": { "baseline-browser-mapping": "dist/cli.cjs" @@ -1572,11 +3973,58 @@ "node": ">=6.0.0" } }, + "node_modules/body-parser": { + "version": "2.2.2", + "resolved": "https://registry.npmjs.org/body-parser/-/body-parser-2.2.2.tgz", + "integrity": "sha512-oP5VkATKlNwcgvxi0vM0p/D3n2C3EReYVX+DNYs5TjZFn/oQt2j+4sVJtSMr18pdRr8wjTcBl6LoV+FUwzPmNA==", + "license": "MIT", + "dependencies": { + "bytes": "^3.1.2", + "content-type": "^1.0.5", + "debug": "^4.4.3", + "http-errors": "^2.0.0", + "iconv-lite": "^0.7.0", + "on-finished": "^2.4.1", + "qs": "^6.14.1", + "raw-body": "^3.0.1", + "type-is": "^2.0.1" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, + "node_modules/brace-expansion": { + "version": "5.0.4", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-5.0.4.tgz", + "integrity": "sha512-h+DEnpVvxmfVefa4jFbCf5HdH5YMDXRsmKflpf1pILZWRFlTbJpxeU55nJl4Smt5HQaGzg1o6RHFPJaOqnmBDg==", + "license": "MIT", + "dependencies": { + "balanced-match": "^4.0.2" + }, + "engines": { + "node": "18 || 20 || >=22" + } + }, + "node_modules/braces": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/braces/-/braces-3.0.3.tgz", + "integrity": "sha512-yQbXgO/OSZVD2IsiLlro+7Hf6Q18EJrKSEsdoMzKePKXct3gvD8oLcOQdIzGupr5Fj+EDe8gO/lxc1BzfMpxvA==", + "license": "MIT", + "dependencies": { + "fill-range": "^7.1.1" + }, + "engines": { + "node": ">=8" + } + }, "node_modules/browserslist": { "version": "4.28.1", "resolved": "https://registry.npmjs.org/browserslist/-/browserslist-4.28.1.tgz", "integrity": "sha512-ZC5Bd0LgJXgwGqUknZY/vkUQ04r8NXnJZ3yYi4vDmSiZmC/pdSN0NbNRPxZpbtO4uAfDUAFffO8IZoM3Gj8IkA==", - "dev": true, "funding": [ { "type": "opencollective", @@ -1606,11 +4054,72 @@ "node": "^6 || ^7 || ^8 || ^9 || ^10 || ^11 || ^12 || >=13.7" } }, + "node_modules/bundle-name": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/bundle-name/-/bundle-name-4.1.0.tgz", + "integrity": "sha512-tjwM5exMg6BGRI+kNmTntNsvdZS1X8BFYS6tnJ2hdH0kVxM6/eVZ2xy+FqStSWvYmtfFMDLIxurorHwDKfDz5Q==", + "license": "MIT", + "dependencies": { + "run-applescript": "^7.0.0" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/bytes": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/bytes/-/bytes-3.1.2.tgz", + "integrity": "sha512-/Nf7TyzTx6S3yRJObOAV7956r8cr2+Oj8AC5dt8wSP3BQAoeX58NoHyCU8P8zGkNXStjTSi6fzO6F0pBdcYbEg==", + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/call-bind-apply-helpers": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/call-bind-apply-helpers/-/call-bind-apply-helpers-1.0.2.tgz", + "integrity": "sha512-Sp1ablJ0ivDkSzjcaJdxEunN5/XvksFJ2sMBFfq6x0ryhQV/2b/KwFe21cMpmHtPOSij8K99/wSfoEuTObmuMQ==", + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0", + "function-bind": "^1.1.2" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/call-bound": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/call-bound/-/call-bound-1.0.4.tgz", + "integrity": "sha512-+ys997U96po4Kx/ABpBCqhA9EuxJaQWDQg7295H4hBphv3IZg0boBKuwYpt4YXp6MZ5AmZQnU/tyMTlRpaSejg==", + "license": "MIT", + "dependencies": { + "call-bind-apply-helpers": "^1.0.2", + "get-intrinsic": "^1.3.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/callsites": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/callsites/-/callsites-3.1.0.tgz", + "integrity": "sha512-P8BjAsXvZS+VIDUI11hHCQEv74YT67YUi5JJFNWIqL235sBmjX4+qx9Muvls5ivyNENctx46xQLQ3aTuE7ssaQ==", + "license": "MIT", + "engines": { + "node": ">=6" + } + }, "node_modules/caniuse-lite": { "version": "1.0.30001778", "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001778.tgz", "integrity": "sha512-PN7uxFL+ExFJO61aVmP1aIEG4i9whQd4eoSCebav62UwDyp5OHh06zN4jqKSMePVgxHifCw1QJxdRkA1Pisekg==", - "dev": true, "funding": [ { "type": "opencollective", @@ -1627,13 +4136,316 @@ ], "license": "CC-BY-4.0" }, + "node_modules/chalk": { + "version": "5.6.2", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-5.6.2.tgz", + "integrity": "sha512-7NzBL0rN6fMUW+f7A6Io4h40qQlG+xGmtMxfbnH/K7TAtt8JQWVQK+6g0UXKMeVJoyV5EkkNsErQ8pVD3bLHbA==", + "license": "MIT", + "engines": { + "node": "^12.17.0 || ^14.13 || >=16.0.0" + }, + "funding": { + "url": "https://github.com/chalk/chalk?sponsor=1" + } + }, + "node_modules/class-variance-authority": { + "version": "0.7.1", + "resolved": "https://registry.npmjs.org/class-variance-authority/-/class-variance-authority-0.7.1.tgz", + "integrity": "sha512-Ka+9Trutv7G8M6WT6SeiRWz792K5qEqIGEGzXKhAE6xOWAY6pPH8U+9IY3oCMv6kqTmLsv7Xh/2w2RigkePMsg==", + "license": "Apache-2.0", + "dependencies": { + "clsx": "^2.1.1" + }, + "funding": { + "url": "https://polar.sh/cva" + } + }, + "node_modules/cli-cursor": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/cli-cursor/-/cli-cursor-5.0.0.tgz", + "integrity": "sha512-aCj4O5wKyszjMmDT4tZj93kxyydN/K5zPWSCe6/0AV/AA1pqe5ZBIw0a2ZfPQV7lL5/yb5HsUreJ6UFAF1tEQw==", + "license": "MIT", + "dependencies": { + "restore-cursor": "^5.0.0" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/cli-spinners": { + "version": "2.9.2", + "resolved": "https://registry.npmjs.org/cli-spinners/-/cli-spinners-2.9.2.tgz", + "integrity": "sha512-ywqV+5MmyL4E7ybXgKys4DugZbX0FC6LnwrhjuykIjnK9k8OQacQ7axGKnjDXWNhns0xot3bZI5h55H8yo9cJg==", + "license": "MIT", + "engines": { + "node": ">=6" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/cli-width": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/cli-width/-/cli-width-4.1.0.tgz", + "integrity": "sha512-ouuZd4/dm2Sw5Gmqy6bGyNNNe1qt9RpmxveLSO7KcgsTnU7RXfsw+/bukWGo1abgBiMAic068rclZsO4IWmmxQ==", + "license": "ISC", + "engines": { + "node": ">= 12" + } + }, + "node_modules/cliui": { + "version": "8.0.1", + "resolved": "https://registry.npmjs.org/cliui/-/cliui-8.0.1.tgz", + "integrity": "sha512-BSeNnyus75C4//NQ9gQt1/csTXyo/8Sb+afLAkzAptFuMsod9HFokGNudZpi/oQV73hnVK+sR+5PVRMd+Dr7YQ==", + "license": "ISC", + "dependencies": { + "string-width": "^4.2.0", + "strip-ansi": "^6.0.1", + "wrap-ansi": "^7.0.0" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/cliui/node_modules/ansi-regex": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz", + "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==", + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/cliui/node_modules/emoji-regex": { + "version": "8.0.0", + "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz", + "integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==", + "license": "MIT" + }, + "node_modules/cliui/node_modules/string-width": { + "version": "4.2.3", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz", + "integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==", + "license": "MIT", + "dependencies": { + "emoji-regex": "^8.0.0", + "is-fullwidth-code-point": "^3.0.0", + "strip-ansi": "^6.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/cliui/node_modules/strip-ansi": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz", + "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==", + "license": "MIT", + "dependencies": { + "ansi-regex": "^5.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/cliui/node_modules/wrap-ansi": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-7.0.0.tgz", + "integrity": "sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q==", + "license": "MIT", + "dependencies": { + "ansi-styles": "^4.0.0", + "string-width": "^4.1.0", + "strip-ansi": "^6.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/wrap-ansi?sponsor=1" + } + }, + "node_modules/clsx": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/clsx/-/clsx-2.1.1.tgz", + "integrity": "sha512-eYm0QWBtUrBWZWG0d386OGAw16Z995PiOVo2B7bjWSbHedGl5e0ZWaq65kOGgUSNesEIDkB9ISbTg/JK9dhCZA==", + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/code-block-writer": { + "version": "13.0.3", + "resolved": "https://registry.npmjs.org/code-block-writer/-/code-block-writer-13.0.3.tgz", + "integrity": "sha512-Oofo0pq3IKnsFtuHqSF7TqBfr71aeyZDVJ0HpmqB7FBM2qEigL0iPONSCZSO9pE9dZTAxANe5XHG9Uy0YMv8cg==", + "license": "MIT" + }, + "node_modules/color-convert": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", + "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", + "license": "MIT", + "dependencies": { + "color-name": "~1.1.4" + }, + "engines": { + "node": ">=7.0.0" + } + }, + "node_modules/color-name": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", + "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==", + "license": "MIT" + }, + "node_modules/commander": { + "version": "14.0.3", + "resolved": "https://registry.npmjs.org/commander/-/commander-14.0.3.tgz", + "integrity": "sha512-H+y0Jo/T1RZ9qPP4Eh1pkcQcLRglraJaSLoyOtHxu6AapkjWVCy2Sit1QQ4x3Dng8qDlSsZEet7g5Pq06MvTgw==", + "license": "MIT", + "engines": { + "node": ">=20" + } + }, + "node_modules/content-disposition": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/content-disposition/-/content-disposition-1.0.1.tgz", + "integrity": "sha512-oIXISMynqSqm241k6kcQ5UwttDILMK4BiurCfGEREw6+X9jkkpEe5T9FZaApyLGGOnFuyMWZpdolTXMtvEJ08Q==", + "license": "MIT", + "engines": { + "node": ">=18" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, + "node_modules/content-type": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/content-type/-/content-type-1.0.5.tgz", + "integrity": "sha512-nTjqfcBFEipKdXCv4YDQWCfmcLZKm81ldF0pAopTvyrFGVbcR6P/VAAd5G7N+0tTr8QqiU0tFadD6FK4NtJwOA==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, "node_modules/convert-source-map": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/convert-source-map/-/convert-source-map-2.0.0.tgz", "integrity": "sha512-Kvp459HrV2FEJ1CAsi1Ku+MY3kasH19TFykTz2xWmMeq6bk2NU3XXvfJ+Q61m0xktWwt+1HSYf3JZsTms3aRJg==", - "dev": true, "license": "MIT" }, + "node_modules/cookie": { + "version": "0.7.2", + "resolved": "https://registry.npmjs.org/cookie/-/cookie-0.7.2.tgz", + "integrity": "sha512-yki5XnKuf750l50uGTllt6kKILY4nQ1eNIQatoXEByZ5dWgnKqbnqmTrBE5B4N7lrMJKQ2ytWMiTO2o0v6Ew/w==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/cookie-signature": { + "version": "1.2.2", + "resolved": "https://registry.npmjs.org/cookie-signature/-/cookie-signature-1.2.2.tgz", + "integrity": "sha512-D76uU73ulSXrD1UXF4KE2TMxVVwhsnCgfAyTg9k8P6KGZjlXKrOLe4dJQKI3Bxi5wjesZoFXJWElNWBjPZMbhg==", + "license": "MIT", + "engines": { + "node": ">=6.6.0" + } + }, + "node_modules/cors": { + "version": "2.8.6", + "resolved": "https://registry.npmjs.org/cors/-/cors-2.8.6.tgz", + "integrity": "sha512-tJtZBBHA6vjIAaF6EnIaq6laBBP9aq/Y3ouVJjEfoHbRBcHBAHYcMh/w8LDrk2PvIMMq8gmopa5D4V8RmbrxGw==", + "license": "MIT", + "dependencies": { + "object-assign": "^4", + "vary": "^1" + }, + "engines": { + "node": ">= 0.10" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, + "node_modules/cosmiconfig": { + "version": "9.0.1", + "resolved": "https://registry.npmjs.org/cosmiconfig/-/cosmiconfig-9.0.1.tgz", + "integrity": "sha512-hr4ihw+DBqcvrsEDioRO31Z17x71pUYoNe/4h6Z0wB72p7MU7/9gH8Q3s12NFhHPfYBBOV3qyfUxmr/Yn3shnQ==", + "license": "MIT", + "dependencies": { + "env-paths": "^2.2.1", + "import-fresh": "^3.3.0", + "js-yaml": "^4.1.0", + "parse-json": "^5.2.0" + }, + "engines": { + "node": ">=14" + }, + "funding": { + "url": "https://github.com/sponsors/d-fischer" + }, + "peerDependencies": { + "typescript": ">=4.9.5" + }, + "peerDependenciesMeta": { + "typescript": { + "optional": true + } + } + }, + "node_modules/cross-spawn": { + "version": "7.0.6", + "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.6.tgz", + "integrity": "sha512-uV2QOWP2nWzsy2aMp8aRibhi9dlzF5Hgh5SHaB9OiTGEyDTiJJyx0uy51QXdyWbtAHNua4XJzUKca3OzKUd3vA==", + "license": "MIT", + "dependencies": { + "path-key": "^3.1.0", + "shebang-command": "^2.0.0", + "which": "^2.0.1" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/cross-spawn/node_modules/isexe": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/isexe/-/isexe-2.0.0.tgz", + "integrity": "sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==", + "license": "ISC" + }, + "node_modules/cross-spawn/node_modules/which": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/which/-/which-2.0.2.tgz", + "integrity": "sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA==", + "license": "ISC", + "dependencies": { + "isexe": "^2.0.0" + }, + "bin": { + "node-which": "bin/node-which" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/cssesc": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/cssesc/-/cssesc-3.0.0.tgz", + "integrity": "sha512-/Tb/JcjK111nNScGob5MNtsntNM1aCNUDipB/TkwZFhyDrrE47SOx/18wF2bbjgc3ZzCSKW1T5nt5EbFoAz/Vg==", + "license": "MIT", + "bin": { + "cssesc": "bin/cssesc" + }, + "engines": { + "node": ">=4" + } + }, "node_modules/csstype": { "version": "3.2.3", "resolved": "https://registry.npmjs.org/csstype/-/csstype-3.2.3.tgz", @@ -1641,11 +4453,19 @@ "devOptional": true, "license": "MIT" }, + "node_modules/data-uri-to-buffer": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/data-uri-to-buffer/-/data-uri-to-buffer-4.0.1.tgz", + "integrity": "sha512-0R9ikRb668HB7QDxT1vkpuUBtqc53YyAwMwGeUFKRojY/NWKvdZ+9UYtRfGmhqNbRkTSVpMbmyhXipFFv2cb/A==", + "license": "MIT", + "engines": { + "node": ">= 12" + } + }, "node_modules/debug": { "version": "4.4.3", "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.3.tgz", "integrity": "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA==", - "dev": true, "license": "MIT", "dependencies": { "ms": "^2.1.3" @@ -1659,6 +4479,78 @@ } } }, + "node_modules/dedent": { + "version": "1.7.2", + "resolved": "https://registry.npmjs.org/dedent/-/dedent-1.7.2.tgz", + "integrity": "sha512-WzMx3mW98SN+zn3hgemf4OzdmyNhhhKz5Ay0pUfQiMQ3e1g+xmTJWp/pKdwKVXhdSkAEGIIzqeuWrL3mV/AXbA==", + "license": "MIT", + "peerDependencies": { + "babel-plugin-macros": "^3.1.0" + }, + "peerDependenciesMeta": { + "babel-plugin-macros": { + "optional": true + } + } + }, + "node_modules/deepmerge": { + "version": "4.3.1", + "resolved": "https://registry.npmjs.org/deepmerge/-/deepmerge-4.3.1.tgz", + "integrity": "sha512-3sUqbMEc77XqpdNO7FRyRog+eW3ph+GYCbj+rK+uYyRMuwsVy0rMiVtPn+QJlKFvWP/1PYpapqYn0Me2knFn+A==", + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/default-browser": { + "version": "5.5.0", + "resolved": "https://registry.npmjs.org/default-browser/-/default-browser-5.5.0.tgz", + "integrity": "sha512-H9LMLr5zwIbSxrmvikGuI/5KGhZ8E2zH3stkMgM5LpOWDutGM2JZaj460Udnf1a+946zc7YBgrqEWwbk7zHvGw==", + "license": "MIT", + "dependencies": { + "bundle-name": "^4.1.0", + "default-browser-id": "^5.0.0" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/default-browser-id": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/default-browser-id/-/default-browser-id-5.0.1.tgz", + "integrity": "sha512-x1VCxdX4t+8wVfd1so/9w+vQ4vx7lKd2Qp5tDRutErwmR85OgmfX7RlLRMWafRMY7hbEiXIbudNrjOAPa/hL8Q==", + "license": "MIT", + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/define-lazy-prop": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/define-lazy-prop/-/define-lazy-prop-3.0.0.tgz", + "integrity": "sha512-N+MeXYoqr3pOgn8xfyRPREN7gHakLYjhsHhWGT3fWAiL4IkAt0iDw14QiiEm2bE30c5XX5q0FtAA3CK5f9/BUg==", + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/depd": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/depd/-/depd-2.0.0.tgz", + "integrity": "sha512-g7nH6P6dyDioJogAAGprGpCtVImJhpPk/roCzdb3fIh61/s/nPsfR6onyMwkCAR/OlC3yBC0lESvUoQEAssIrw==", + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, "node_modules/detect-libc": { "version": "2.1.2", "resolved": "https://registry.npmjs.org/detect-libc/-/detect-libc-2.1.2.tgz", @@ -1669,13 +4561,91 @@ "node": ">=8" } }, + "node_modules/detect-node-es": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/detect-node-es/-/detect-node-es-1.1.0.tgz", + "integrity": "sha512-ypdmJU/TbBby2Dxibuv7ZLW3Bs1QEmM7nHjEANfohJLvE0XVujisn1qPJcZxg+qDucsr+bP6fLD1rPS3AhJ7EQ==", + "license": "MIT" + }, + "node_modules/diff": { + "version": "8.0.3", + "resolved": "https://registry.npmjs.org/diff/-/diff-8.0.3.tgz", + "integrity": "sha512-qejHi7bcSD4hQAZE0tNAawRK1ZtafHDmMTMkrrIGgSLl7hTnQHmKCeB45xAcbfTqK2zowkM3j3bHt/4b/ARbYQ==", + "license": "BSD-3-Clause", + "engines": { + "node": ">=0.3.1" + } + }, + "node_modules/dotenv": { + "version": "17.3.1", + "resolved": "https://registry.npmjs.org/dotenv/-/dotenv-17.3.1.tgz", + "integrity": "sha512-IO8C/dzEb6O3F9/twg6ZLXz164a2fhTnEWb95H23Dm4OuN+92NmEAlTrupP9VW6Jm3sO26tQlqyvyi4CsnY9GA==", + "license": "BSD-2-Clause", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://dotenvx.com" + } + }, + "node_modules/dunder-proto": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/dunder-proto/-/dunder-proto-1.0.1.tgz", + "integrity": "sha512-KIN/nDJBQRcXw0MLVhZE9iQHmG68qAVIBg9CqmUYjmQIhgij9U5MFvrqkUL5FbtyyzZuOeOt0zdeRe4UY7ct+A==", + "license": "MIT", + "dependencies": { + "call-bind-apply-helpers": "^1.0.1", + "es-errors": "^1.3.0", + "gopd": "^1.2.0" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/eciesjs": { + "version": "0.4.18", + "resolved": "https://registry.npmjs.org/eciesjs/-/eciesjs-0.4.18.tgz", + "integrity": "sha512-wG99Zcfcys9fZux7Cft8BAX/YrOJLJSZ3jyYPfhZHqN2E+Ffx+QXBDsv3gubEgPtV6dTzJMSQUwk1H98/t/0wQ==", + "license": "MIT", + "dependencies": { + "@ecies/ciphers": "^0.2.5", + "@noble/ciphers": "^1.3.0", + "@noble/curves": "^1.9.7", + "@noble/hashes": "^1.8.0" + }, + "engines": { + "bun": ">=1", + "deno": ">=2", + "node": ">=16" + } + }, + "node_modules/ee-first": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/ee-first/-/ee-first-1.1.1.tgz", + "integrity": "sha512-WMwm9LhRUo+WUaRN+vRuETqG89IgZphVSNkdFgeb6sS/E4OrDIN7t48CAewSHXc6C8lefD8KKfr5vY61brQlow==", + "license": "MIT" + }, "node_modules/electron-to-chromium": { "version": "1.5.313", "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.5.313.tgz", "integrity": "sha512-QBMrTWEf00GXZmJyx2lbYD45jpI3TUFnNIzJ5BBc8piGUDwMPa1GV6HJWTZVvY/eiN3fSopl7NRbgGp9sZ9LTA==", - "dev": true, "license": "ISC" }, + "node_modules/emoji-regex": { + "version": "10.6.0", + "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-10.6.0.tgz", + "integrity": "sha512-toUI84YS5YmxW219erniWD0CIVOo46xGKColeNQRgOzDorgBi1v4D71/OFzgD9GO2UGKIv1C3Sp8DAn0+j5w7A==", + "license": "MIT" + }, + "node_modules/encodeurl": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/encodeurl/-/encodeurl-2.0.0.tgz", + "integrity": "sha512-Q0n9HRi4m6JuGIV1eFlmvJB7ZEVxu93IrMyiMsGC0lrMJMWzRgx6WGquyfQgZVb31vhGgXnfmPNNXmxnOkRBrg==", + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, "node_modules/enhanced-resolve": { "version": "5.20.0", "resolved": "https://registry.npmjs.org/enhanced-resolve/-/enhanced-resolve-5.20.0.tgz", @@ -1690,6 +4660,54 @@ "node": ">=10.13.0" } }, + "node_modules/env-paths": { + "version": "2.2.1", + "resolved": "https://registry.npmjs.org/env-paths/-/env-paths-2.2.1.tgz", + "integrity": "sha512-+h1lkLKhZMTYjog1VEpJNG7NZJWcuc2DDk/qsqSTRRCOXiLjeQ1d1/udrUGhqMxUgAlwKNZ0cf2uqan5GLuS2A==", + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/error-ex": { + "version": "1.3.4", + "resolved": "https://registry.npmjs.org/error-ex/-/error-ex-1.3.4.tgz", + "integrity": "sha512-sqQamAnR14VgCr1A618A3sGrygcpK+HEbenA/HiEAkkUwcZIIB/tgWqHFxWgOyDh4nB4JCRimh79dR5Ywc9MDQ==", + "license": "MIT", + "dependencies": { + "is-arrayish": "^0.2.1" + } + }, + "node_modules/es-define-property": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/es-define-property/-/es-define-property-1.0.1.tgz", + "integrity": "sha512-e3nRfgfUZ4rNGL232gUgX06QNyyez04KdjFrF+LTRoOXmrOgFKDg4BCdsjW8EnT69eqdYGmRpJwiPVYNrCaW3g==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/es-errors": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/es-errors/-/es-errors-1.3.0.tgz", + "integrity": "sha512-Zf5H2Kxt2xjTvbJvP2ZWLEICxA6j+hAmMzIlypy4xcBg1vKVnx89Wy0GbS+kf5cwCVFFzdCFh2XSCFNULS6csw==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/es-object-atoms": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/es-object-atoms/-/es-object-atoms-1.1.1.tgz", + "integrity": "sha512-FGgH2h8zKNim9ljj7dankFPcICIK9Cp5bm+c2gQSYePhpaG5+esrLODihIorn+Pe6FGJzWhXQotPv73jTaldXA==", + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0" + }, + "engines": { + "node": ">= 0.4" + } + }, "node_modules/esbuild": { "version": "0.25.12", "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.25.12.tgz", @@ -1736,17 +4754,198 @@ "version": "3.2.0", "resolved": "https://registry.npmjs.org/escalade/-/escalade-3.2.0.tgz", "integrity": "sha512-WUj2qlxaQtO4g6Pq5c29GTcWGDyd8itL8zTlipgECz3JesAiiOKotd8JU6otB3PACgG6xkJUyVhboMS+bje/jA==", - "dev": true, "license": "MIT", "engines": { "node": ">=6" } }, + "node_modules/escape-html": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/escape-html/-/escape-html-1.0.3.tgz", + "integrity": "sha512-NiSupZ4OeuGwr68lGIeym/ksIZMJodUGOSCZ/FSnTxcrekbvqrgdUxlJOMpijaKZVjAJrWrGs/6Jy8OMuyj9ow==", + "license": "MIT" + }, + "node_modules/esprima": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/esprima/-/esprima-4.0.1.tgz", + "integrity": "sha512-eGuFFw7Upda+g4p+QHvnW0RyTX/SVeJBDM/gCtMARO0cLuT2HcEKnTPvhjV6aGeqrCB/sbNop0Kszm0jsaWU4A==", + "license": "BSD-2-Clause", + "bin": { + "esparse": "bin/esparse.js", + "esvalidate": "bin/esvalidate.js" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/etag": { + "version": "1.8.1", + "resolved": "https://registry.npmjs.org/etag/-/etag-1.8.1.tgz", + "integrity": "sha512-aIL5Fx7mawVa300al2BnEE4iNvo1qETxLrPI/o05L7z6go7fCw1J6EQmbK4FmJ2AS7kgVF/KEZWufBfdClMcPg==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/eventsource": { + "version": "3.0.7", + "resolved": "https://registry.npmjs.org/eventsource/-/eventsource-3.0.7.tgz", + "integrity": "sha512-CRT1WTyuQoD771GW56XEZFQ/ZoSfWid1alKGDYMmkt2yl8UXrVR4pspqWNEcqKvVIzg6PAltWjxcSSPrboA4iA==", + "license": "MIT", + "dependencies": { + "eventsource-parser": "^3.0.1" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/eventsource-parser": { + "version": "3.0.6", + "resolved": "https://registry.npmjs.org/eventsource-parser/-/eventsource-parser-3.0.6.tgz", + "integrity": "sha512-Vo1ab+QXPzZ4tCa8SwIHJFaSzy4R6SHf7BY79rFBDf0idraZWAkYrDjDj8uWaSm3S2TK+hJ7/t1CEmZ7jXw+pg==", + "license": "MIT", + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/execa": { + "version": "9.6.1", + "resolved": "https://registry.npmjs.org/execa/-/execa-9.6.1.tgz", + "integrity": "sha512-9Be3ZoN4LmYR90tUoVu2te2BsbzHfhJyfEiAVfz7N5/zv+jduIfLrV2xdQXOHbaD6KgpGdO9PRPM1Y4Q9QkPkA==", + "license": "MIT", + "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" + }, + "engines": { + "node": "^18.19.0 || >=20.5.0" + }, + "funding": { + "url": "https://github.com/sindresorhus/execa?sponsor=1" + } + }, + "node_modules/express": { + "version": "5.2.1", + "resolved": "https://registry.npmjs.org/express/-/express-5.2.1.tgz", + "integrity": "sha512-hIS4idWWai69NezIdRt2xFVofaF4j+6INOpJlVOLDO8zXGpUVEVzIYk12UUi2JzjEzWL3IOAxcTubgz9Po0yXw==", + "license": "MIT", + "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" + }, + "engines": { + "node": ">= 18" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, + "node_modules/express-rate-limit": { + "version": "8.3.1", + "resolved": "https://registry.npmjs.org/express-rate-limit/-/express-rate-limit-8.3.1.tgz", + "integrity": "sha512-D1dKN+cmyPWuvB+G2SREQDzPY1agpBIcTa9sJxOPMCNeH3gwzhqJRDWCXW3gg0y//+LQ/8j52JbMROWyrKdMdw==", + "license": "MIT", + "dependencies": { + "ip-address": "10.1.0" + }, + "engines": { + "node": ">= 16" + }, + "funding": { + "url": "https://github.com/sponsors/express-rate-limit" + }, + "peerDependencies": { + "express": ">= 4.11" + } + }, + "node_modules/fast-deep-equal": { + "version": "3.1.3", + "resolved": "https://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-3.1.3.tgz", + "integrity": "sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==", + "license": "MIT" + }, + "node_modules/fast-glob": { + "version": "3.3.3", + "resolved": "https://registry.npmjs.org/fast-glob/-/fast-glob-3.3.3.tgz", + "integrity": "sha512-7MptL8U0cqcFdzIzwOTHoilX9x5BrNqye7Z/LuC7kCMRio1EMSyqRK3BEAUD7sXRq4iT4AzTVuZdhgQ2TCvYLg==", + "license": "MIT", + "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" + }, + "engines": { + "node": ">=8.6.0" + } + }, + "node_modules/fast-uri": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/fast-uri/-/fast-uri-3.1.0.tgz", + "integrity": "sha512-iPeeDKJSWf4IEOasVVrknXpaBV0IApz/gp7S2bb7Z4Lljbl2MGJRqInZiUrQwV16cpzw/D3S5j5Julj/gT52AA==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/fastify" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/fastify" + } + ], + "license": "BSD-3-Clause" + }, + "node_modules/fastq": { + "version": "1.20.1", + "resolved": "https://registry.npmjs.org/fastq/-/fastq-1.20.1.tgz", + "integrity": "sha512-GGToxJ/w1x32s/D2EKND7kTil4n8OVk/9mycTc4VDza13lOvpUZTGX3mFSCtV9ksdGBVzvsyAVLM6mHFThxXxw==", + "license": "ISC", + "dependencies": { + "reusify": "^1.0.4" + } + }, "node_modules/fdir": { "version": "6.5.0", "resolved": "https://registry.npmjs.org/fdir/-/fdir-6.5.0.tgz", "integrity": "sha512-tIbYtZbucOs0BRGqPJkshJUYdL+SDH7dVM8gjy+ERp3WAUjLEFJE+02kanyHtwjWOnwrKYBiwAmM0p4kLJAnXg==", - "dev": true, "license": "MIT", "engines": { "node": ">=12.0.0" @@ -1760,6 +4959,148 @@ } } }, + "node_modules/fetch-blob": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/fetch-blob/-/fetch-blob-3.2.0.tgz", + "integrity": "sha512-7yAQpD2UMJzLi1Dqv7qFYnPbaPx7ZfFK6PiIxQ4PfkGPyNyl2Ugx+a/umUonmKqjhM4DnfbMvdX6otXq83soQQ==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/jimmywarting" + }, + { + "type": "paypal", + "url": "https://paypal.me/jimmywarting" + } + ], + "license": "MIT", + "dependencies": { + "node-domexception": "^1.0.0", + "web-streams-polyfill": "^3.0.3" + }, + "engines": { + "node": "^12.20 || >= 14.13" + } + }, + "node_modules/figures": { + "version": "6.1.0", + "resolved": "https://registry.npmjs.org/figures/-/figures-6.1.0.tgz", + "integrity": "sha512-d+l3qxjSesT4V7v2fh+QnmFnUWv9lSpjarhShNTgBOfA0ttejbQUAlHLitbjkoRiDulW0OPoQPYIGhIC8ohejg==", + "license": "MIT", + "dependencies": { + "is-unicode-supported": "^2.0.0" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/fill-range": { + "version": "7.1.1", + "resolved": "https://registry.npmjs.org/fill-range/-/fill-range-7.1.1.tgz", + "integrity": "sha512-YsGpe3WHLK8ZYi4tWDg2Jy3ebRz2rXowDxnld4bkQB00cc/1Zw9AWnC0i9ztDJitivtQvaI9KaLyKrc+hBW0yg==", + "license": "MIT", + "dependencies": { + "to-regex-range": "^5.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/finalhandler": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/finalhandler/-/finalhandler-2.1.1.tgz", + "integrity": "sha512-S8KoZgRZN+a5rNwqTxlZZePjT/4cnm0ROV70LedRHZ0p8u9fRID0hJUZQpkKLzro8LfmC8sx23bY6tVNxv8pQA==", + "license": "MIT", + "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" + }, + "engines": { + "node": ">= 18.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, + "node_modules/formdata-polyfill": { + "version": "4.0.10", + "resolved": "https://registry.npmjs.org/formdata-polyfill/-/formdata-polyfill-4.0.10.tgz", + "integrity": "sha512-buewHzMvYL29jdeQTVILecSaZKnt/RJWjoZCF5OW60Z67/GmSLBkOFM7qh1PI3zFNtJbaZL5eQu1vLfazOwj4g==", + "license": "MIT", + "dependencies": { + "fetch-blob": "^3.1.2" + }, + "engines": { + "node": ">=12.20.0" + } + }, + "node_modules/forwarded": { + "version": "0.2.0", + "resolved": "https://registry.npmjs.org/forwarded/-/forwarded-0.2.0.tgz", + "integrity": "sha512-buRG0fpBtRHSTCOASe6hD258tEubFoRLb4ZNA6NxMVHNw2gOcwHo9wyablzMzOA5z9xA9L1KNjk/Nt6MT9aYow==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/framer-motion": { + "version": "12.36.0", + "resolved": "https://registry.npmjs.org/framer-motion/-/framer-motion-12.36.0.tgz", + "integrity": "sha512-4PqYHAT7gev0ke0wos+PyrcFxI0HScjm3asgU8nSYa8YzJFuwgIvdj3/s3ZaxLq0bUSboIn19A2WS/MHwLCvfw==", + "license": "MIT", + "dependencies": { + "motion-dom": "^12.36.0", + "motion-utils": "^12.36.0", + "tslib": "^2.4.0" + }, + "peerDependencies": { + "@emotion/is-prop-valid": "*", + "react": "^18.0.0 || ^19.0.0", + "react-dom": "^18.0.0 || ^19.0.0" + }, + "peerDependenciesMeta": { + "@emotion/is-prop-valid": { + "optional": true + }, + "react": { + "optional": true + }, + "react-dom": { + "optional": true + } + } + }, + "node_modules/fresh": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/fresh/-/fresh-2.0.0.tgz", + "integrity": "sha512-Rx/WycZ60HOaqLKAi6cHRKKI7zxWbJ31MhntmtwMoaTeF7XFH9hhBp8vITaMidfljRQ6eYWCKkaTK+ykVJHP2A==", + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/fs-extra": { + "version": "11.3.4", + "resolved": "https://registry.npmjs.org/fs-extra/-/fs-extra-11.3.4.tgz", + "integrity": "sha512-CTXd6rk/M3/ULNQj8FBqBWHYBVYybQ3VPBw0xGKFe3tuH7ytT6ACnvzpIQ3UZtB8yvUKC2cXn1a+x+5EVQLovA==", + "license": "MIT", + "dependencies": { + "graceful-fs": "^4.2.0", + "jsonfile": "^6.0.1", + "universalify": "^2.0.0" + }, + "engines": { + "node": ">=14.14" + } + }, "node_modules/fsevents": { "version": "2.3.3", "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.3.tgz", @@ -1775,23 +5116,514 @@ "node": "^8.16.0 || ^10.6.0 || >=11.0.0" } }, + "node_modules/function-bind": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.2.tgz", + "integrity": "sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA==", + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/fuzzysort": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/fuzzysort/-/fuzzysort-3.1.0.tgz", + "integrity": "sha512-sR9BNCjBg6LNgwvxlBd0sBABvQitkLzoVY9MYYROQVX/FvfJ4Mai9LsGhDgd8qYdds0bY77VzYd5iuB+v5rwQQ==", + "license": "MIT" + }, + "node_modules/fzf": { + "version": "0.5.2", + "resolved": "https://registry.npmjs.org/fzf/-/fzf-0.5.2.tgz", + "integrity": "sha512-Tt4kuxLXFKHy8KT40zwsUPUkg1CrsgY25FxA2U/j/0WgEDCk3ddc/zLTCCcbSHX9FcKtLuVaDGtGE/STWC+j3Q==", + "license": "BSD-3-Clause" + }, "node_modules/gensync": { "version": "1.0.0-beta.2", "resolved": "https://registry.npmjs.org/gensync/-/gensync-1.0.0-beta.2.tgz", "integrity": "sha512-3hN7NaskYvMDLQY55gnW3NQ+mesEAepTqlg+VEbj7zzqEMBVNhzcGYYeqFo/TlYz6eQiFcp1HcsCZO+nGgS8zg==", - "dev": true, "license": "MIT", "engines": { "node": ">=6.9.0" } }, + "node_modules/get-caller-file": { + "version": "2.0.5", + "resolved": "https://registry.npmjs.org/get-caller-file/-/get-caller-file-2.0.5.tgz", + "integrity": "sha512-DyFP3BM/3YHTQOCUL/w0OZHR0lpKeGrxotcHWcqNEdnltqFwXVfhEBQ94eIo34AfQpo0rGki4cyIiftY06h2Fg==", + "license": "ISC", + "engines": { + "node": "6.* || 8.* || >= 10.*" + } + }, + "node_modules/get-east-asian-width": { + "version": "1.5.0", + "resolved": "https://registry.npmjs.org/get-east-asian-width/-/get-east-asian-width-1.5.0.tgz", + "integrity": "sha512-CQ+bEO+Tva/qlmw24dCejulK5pMzVnUOFOijVogd3KQs07HnRIgp8TGipvCCRT06xeYEbpbgwaCxglFyiuIcmA==", + "license": "MIT", + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/get-intrinsic": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/get-intrinsic/-/get-intrinsic-1.3.0.tgz", + "integrity": "sha512-9fSjSaos/fRIVIp+xSJlE6lfwhES7LNtKaCBIamHsjr2na1BiABJPo0mOjjz8GJDURarmCPGqaiVg5mfjb98CQ==", + "license": "MIT", + "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" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/get-nonce": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/get-nonce/-/get-nonce-1.0.1.tgz", + "integrity": "sha512-FJhYRoDaiatfEkUK8HKlicmu/3SGFD51q3itKDGoSTysQJBnfOcxU5GxnhE1E6soB76MbT0MBtnKJuXyAx+96Q==", + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/get-own-enumerable-keys": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/get-own-enumerable-keys/-/get-own-enumerable-keys-1.0.0.tgz", + "integrity": "sha512-PKsK2FSrQCyxcGHsGrLDcK0lx+0Ke+6e8KFFozA9/fIQLhQzPaRvJFdcz7+Axg3jUH/Mq+NI4xa5u/UT2tQskA==", + "license": "MIT", + "engines": { + "node": ">=14.16" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/get-proto": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/get-proto/-/get-proto-1.0.1.tgz", + "integrity": "sha512-sTSfBjoXBp89JvIKIefqw7U2CCebsc74kiY6awiGogKtoSGbgjYE/G/+l9sF3MWFPNc9IcoOC4ODfKHfxFmp0g==", + "license": "MIT", + "dependencies": { + "dunder-proto": "^1.0.1", + "es-object-atoms": "^1.0.0" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/get-stream": { + "version": "9.0.1", + "resolved": "https://registry.npmjs.org/get-stream/-/get-stream-9.0.1.tgz", + "integrity": "sha512-kVCxPF3vQM/N0B1PmoqVUqgHP+EeVjmZSQn+1oCRPxd2P21P2F19lIgbR3HBosbB1PUhOAoctJnfEn2GbN2eZA==", + "license": "MIT", + "dependencies": { + "@sec-ant/readable-stream": "^0.4.1", + "is-stream": "^4.0.1" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/glob-parent": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-5.1.2.tgz", + "integrity": "sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow==", + "license": "ISC", + "dependencies": { + "is-glob": "^4.0.1" + }, + "engines": { + "node": ">= 6" + } + }, + "node_modules/gopd": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/gopd/-/gopd-1.2.0.tgz", + "integrity": "sha512-ZUKRh6/kUFoAiTAtTYPZJ3hw9wNxx+BIBOijnlG9PnrJsCcSjs1wyyD6vJpaYtgnzDrKYRSqf3OO6Rfa93xsRg==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, "node_modules/graceful-fs": { "version": "4.2.11", "resolved": "https://registry.npmjs.org/graceful-fs/-/graceful-fs-4.2.11.tgz", "integrity": "sha512-RbJ5/jmFcNNCcDV5o9eTnBLJ/HszWV0P73bc+Ff4nS/rJj+YaS6IGyiOL0VoBYX+l1Wrl3k63h/KrH+nhJ0XvQ==", - "dev": true, "license": "ISC" }, + "node_modules/graphql": { + "version": "16.13.1", + "resolved": "https://registry.npmjs.org/graphql/-/graphql-16.13.1.tgz", + "integrity": "sha512-gGgrVCoDKlIZ8fIqXBBb0pPKqDgki0Z/FSKNiQzSGj2uEYHr1tq5wmBegGwJx6QB5S5cM0khSBpi/JFHMCvsmQ==", + "license": "MIT", + "engines": { + "node": "^12.22.0 || ^14.16.0 || ^16.0.0 || >=17.0.0" + } + }, + "node_modules/has-symbols": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/has-symbols/-/has-symbols-1.1.0.tgz", + "integrity": "sha512-1cDNdwJ2Jaohmb3sg4OmKaMBwuC48sYni5HUw2DvsC8LjGTLK9h+eb1X6RyuOHe4hT0ULCW68iomhjUoKUqlPQ==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/hasown": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/hasown/-/hasown-2.0.2.tgz", + "integrity": "sha512-0hJU9SCPvmMzIBdZFqNPXWa6dqh7WdH0cII9y+CyS8rG3nL48Bclra9HmKhVVUHyPWNH5Y7xDwAB7bfgSjkUMQ==", + "license": "MIT", + "dependencies": { + "function-bind": "^1.1.2" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/headers-polyfill": { + "version": "4.0.3", + "resolved": "https://registry.npmjs.org/headers-polyfill/-/headers-polyfill-4.0.3.tgz", + "integrity": "sha512-IScLbePpkvO846sIwOtOTDjutRMWdXdJmXdMvk6gCBHxFO8d+QKOQedyZSxFTTFYRSmlgSTDtXqqq4pcenBXLQ==", + "license": "MIT" + }, + "node_modules/hono": { + "version": "4.12.7", + "resolved": "https://registry.npmjs.org/hono/-/hono-4.12.7.tgz", + "integrity": "sha512-jq9l1DM0zVIvsm3lv9Nw9nlJnMNPOcAtsbsgiUhWcFzPE99Gvo6yRTlszSLLYacMeQ6quHD6hMfId8crVHvexw==", + "license": "MIT", + "engines": { + "node": ">=16.9.0" + } + }, + "node_modules/http-errors": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/http-errors/-/http-errors-2.0.1.tgz", + "integrity": "sha512-4FbRdAX+bSdmo4AUFuS0WNiPz8NgFt+r8ThgNWmlrjQjt1Q7ZR9+zTlce2859x4KSXrwIsaeTqDoKQmtP8pLmQ==", + "license": "MIT", + "dependencies": { + "depd": "~2.0.0", + "inherits": "~2.0.4", + "setprototypeof": "~1.2.0", + "statuses": "~2.0.2", + "toidentifier": "~1.0.1" + }, + "engines": { + "node": ">= 0.8" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, + "node_modules/https-proxy-agent": { + "version": "7.0.6", + "resolved": "https://registry.npmjs.org/https-proxy-agent/-/https-proxy-agent-7.0.6.tgz", + "integrity": "sha512-vK9P5/iUfdl95AI+JVyUuIcVtd4ofvtrOr3HNtM2yxC9bnMbEdp3x01OhQNnjb8IJYi38VlTE3mBXwcfvywuSw==", + "license": "MIT", + "dependencies": { + "agent-base": "^7.1.2", + "debug": "4" + }, + "engines": { + "node": ">= 14" + } + }, + "node_modules/human-signals": { + "version": "8.0.1", + "resolved": "https://registry.npmjs.org/human-signals/-/human-signals-8.0.1.tgz", + "integrity": "sha512-eKCa6bwnJhvxj14kZk5NCPc6Hb6BdsU9DZcOnmQKSnO1VKrfV0zCvtttPZUsBvjmNDn8rpcJfpwSYnHBjc95MQ==", + "license": "Apache-2.0", + "engines": { + "node": ">=18.18.0" + } + }, + "node_modules/iconv-lite": { + "version": "0.7.2", + "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.7.2.tgz", + "integrity": "sha512-im9DjEDQ55s9fL4EYzOAv0yMqmMBSZp6G0VvFyTMPKWxiSBHUj9NW/qqLmXUwXrrM7AvqSlTCfvqRb0cM8yYqw==", + "license": "MIT", + "dependencies": { + "safer-buffer": ">= 2.1.2 < 3.0.0" + }, + "engines": { + "node": ">=0.10.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, + "node_modules/ignore": { + "version": "5.3.2", + "resolved": "https://registry.npmjs.org/ignore/-/ignore-5.3.2.tgz", + "integrity": "sha512-hsBTNUqQTDwkWtcdYI2i06Y/nUBEsNEDJKjWdigLvegy8kDuJAS8uRlpkkcQpyEXL0Z/pjDy5HBmMjRCJ2gq+g==", + "license": "MIT", + "engines": { + "node": ">= 4" + } + }, + "node_modules/import-fresh": { + "version": "3.3.1", + "resolved": "https://registry.npmjs.org/import-fresh/-/import-fresh-3.3.1.tgz", + "integrity": "sha512-TR3KfrTZTYLPB6jUjfx6MF9WcWrHL9su5TObK4ZkYgBdWKPOFoSoQIdEuTuR82pmtxH2spWG9h6etwfr1pLBqQ==", + "license": "MIT", + "dependencies": { + "parent-module": "^1.0.0", + "resolve-from": "^4.0.0" + }, + "engines": { + "node": ">=6" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/inherits": { + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.4.tgz", + "integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==", + "license": "ISC" + }, + "node_modules/ip-address": { + "version": "10.1.0", + "resolved": "https://registry.npmjs.org/ip-address/-/ip-address-10.1.0.tgz", + "integrity": "sha512-XXADHxXmvT9+CRxhXg56LJovE+bmWnEWB78LB83VZTprKTmaC5QfruXocxzTZ2Kl0DNwKuBdlIhjL8LeY8Sf8Q==", + "license": "MIT", + "engines": { + "node": ">= 12" + } + }, + "node_modules/ipaddr.js": { + "version": "1.9.1", + "resolved": "https://registry.npmjs.org/ipaddr.js/-/ipaddr.js-1.9.1.tgz", + "integrity": "sha512-0KI/607xoxSToH7GjN1FfSbLoU0+btTicjsQSWQlh/hZykN8KpmMf7uYwPW3R+akZ6R/w18ZlXSHBYXiYUPO3g==", + "license": "MIT", + "engines": { + "node": ">= 0.10" + } + }, + "node_modules/is-arrayish": { + "version": "0.2.1", + "resolved": "https://registry.npmjs.org/is-arrayish/-/is-arrayish-0.2.1.tgz", + "integrity": "sha512-zz06S8t0ozoDXMG+ube26zeCTNXcKIPJZJi8hBrF4idCLms4CG9QtK7qBl1boi5ODzFpjswb5JPmHCbMpjaYzg==", + "license": "MIT" + }, + "node_modules/is-docker": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/is-docker/-/is-docker-3.0.0.tgz", + "integrity": "sha512-eljcgEDlEns/7AXFosB5K/2nCM4P7FQPkGc/DWLy5rmFEWvZayGrik1d9/QIY5nJ4f9YsVvBkA6kJpHn9rISdQ==", + "license": "MIT", + "bin": { + "is-docker": "cli.js" + }, + "engines": { + "node": "^12.20.0 || ^14.13.1 || >=16.0.0" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/is-extglob": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/is-extglob/-/is-extglob-2.1.1.tgz", + "integrity": "sha512-SbKbANkN603Vi4jEZv49LeVJMn4yGwsbzZworEoyEiutsN3nJYdbO36zfhGJ6QEDpOZIFkDtnq5JRxmvl3jsoQ==", + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/is-fullwidth-code-point": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-3.0.0.tgz", + "integrity": "sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg==", + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/is-glob": { + "version": "4.0.3", + "resolved": "https://registry.npmjs.org/is-glob/-/is-glob-4.0.3.tgz", + "integrity": "sha512-xelSayHH36ZgE7ZWhli7pW34hNbNl8Ojv5KVmkJD4hBdD3th8Tfk9vYasLM+mXWOZhFkgZfxhLSnrwRr4elSSg==", + "license": "MIT", + "dependencies": { + "is-extglob": "^2.1.1" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/is-in-ssh": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/is-in-ssh/-/is-in-ssh-1.0.0.tgz", + "integrity": "sha512-jYa6Q9rH90kR1vKB6NM7qqd1mge3Fx4Dhw5TVlK1MUBqhEOuCagrEHMevNuCcbECmXZ0ThXkRm+Ymr51HwEPAw==", + "license": "MIT", + "engines": { + "node": ">=20" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/is-inside-container": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/is-inside-container/-/is-inside-container-1.0.0.tgz", + "integrity": "sha512-KIYLCCJghfHZxqjYBE7rEy0OBuTd5xCHS7tHVgvCLkx7StIoaxwNW3hCALgEUjFfeRk+MG/Qxmp/vtETEF3tRA==", + "license": "MIT", + "dependencies": { + "is-docker": "^3.0.0" + }, + "bin": { + "is-inside-container": "cli.js" + }, + "engines": { + "node": ">=14.16" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/is-interactive": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/is-interactive/-/is-interactive-2.0.0.tgz", + "integrity": "sha512-qP1vozQRI+BMOPcjFzrjXuQvdak2pHNUMZoeG2eRbiSqyvbEf/wQtEOTOX1guk6E3t36RkaqiSt8A/6YElNxLQ==", + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/is-node-process": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/is-node-process/-/is-node-process-1.2.0.tgz", + "integrity": "sha512-Vg4o6/fqPxIjtxgUH5QLJhwZ7gW5diGCVlXpuUfELC62CuxM1iHcRe51f2W1FDy04Ai4KJkagKjx3XaqyfRKXw==", + "license": "MIT" + }, + "node_modules/is-number": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/is-number/-/is-number-7.0.0.tgz", + "integrity": "sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng==", + "license": "MIT", + "engines": { + "node": ">=0.12.0" + } + }, + "node_modules/is-obj": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/is-obj/-/is-obj-3.0.0.tgz", + "integrity": "sha512-IlsXEHOjtKhpN8r/tRFj2nDyTmHvcfNeu/nrRIcXE17ROeatXchkojffa1SpdqW4cr/Fj6QkEf/Gn4zf6KKvEQ==", + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/is-plain-obj": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/is-plain-obj/-/is-plain-obj-4.1.0.tgz", + "integrity": "sha512-+Pgi+vMuUNkJyExiMBt5IlFoMyKnr5zhJ4Uspz58WOhBF5QoIZkFyNHIbBAtHwzVAgk5RtndVNsDRN61/mmDqg==", + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/is-promise": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/is-promise/-/is-promise-4.0.0.tgz", + "integrity": "sha512-hvpoI6korhJMnej285dSg6nu1+e6uxs7zG3BYAm5byqDsgJNWwxzM6z6iZiAgQR4TJ30JmBTOwqZUw3WlyH3AQ==", + "license": "MIT" + }, + "node_modules/is-regexp": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/is-regexp/-/is-regexp-3.1.0.tgz", + "integrity": "sha512-rbku49cWloU5bSMI+zaRaXdQHXnthP6DZ/vLnfdSKyL4zUzuWnomtOEiZZOd+ioQ+avFo/qau3KPTc7Fjy1uPA==", + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/is-stream": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/is-stream/-/is-stream-4.0.1.tgz", + "integrity": "sha512-Dnz92NInDqYckGEUJv689RbRiTSEHCQ7wOVeALbkOz999YpqT46yMRIGtSNl2iCL1waAZSx40+h59NV/EwzV/A==", + "license": "MIT", + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/is-unicode-supported": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/is-unicode-supported/-/is-unicode-supported-2.1.0.tgz", + "integrity": "sha512-mE00Gnza5EEB3Ds0HfMyllZzbBrmLOX3vfWoj9A9PEnTfratQ/BcaJOuMhnkhjXvb2+FkY3VuHqtAGpTPmglFQ==", + "license": "MIT", + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/is-wsl": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/is-wsl/-/is-wsl-3.1.1.tgz", + "integrity": "sha512-e6rvdUCiQCAuumZslxRJWR/Doq4VpPR82kqclvcS0efgt430SlGIk05vdCN58+VrzgtIcfNODjozVielycD4Sw==", + "license": "MIT", + "dependencies": { + "is-inside-container": "^1.0.0" + }, + "engines": { + "node": ">=16" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/isexe": { + "version": "3.1.5", + "resolved": "https://registry.npmjs.org/isexe/-/isexe-3.1.5.tgz", + "integrity": "sha512-6B3tLtFqtQS4ekarvLVMZ+X+VlvQekbe4taUkf/rhVO3d/h0M2rfARm/pXLcPEsjjMsFgrFgSrhQIxcSVrBz8w==", + "license": "BlueOak-1.0.0", + "engines": { + "node": ">=18" + } + }, "node_modules/jiti": { "version": "2.6.1", "resolved": "https://registry.npmjs.org/jiti/-/jiti-2.6.1.tgz", @@ -1802,18 +5634,37 @@ "jiti": "lib/jiti-cli.mjs" } }, + "node_modules/jose": { + "version": "6.2.1", + "resolved": "https://registry.npmjs.org/jose/-/jose-6.2.1.tgz", + "integrity": "sha512-jUaKr1yrbfaImV7R2TN/b3IcZzsw38/chqMpo2XJ7i2F8AfM/lA4G1goC3JVEwg0H7UldTmSt3P68nt31W7/mw==", + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/panva" + } + }, "node_modules/js-tokens": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-4.0.0.tgz", "integrity": "sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ==", - "dev": true, "license": "MIT" }, + "node_modules/js-yaml": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-4.1.1.tgz", + "integrity": "sha512-qQKT4zQxXl8lLwBtHMWwaTcGfFOZviOJet3Oy/xmGk2gZH677CJM9EvtfdSkgWcATZhj/55JZ0rmy3myCT5lsA==", + "license": "MIT", + "dependencies": { + "argparse": "^2.0.1" + }, + "bin": { + "js-yaml": "bin/js-yaml.js" + } + }, "node_modules/jsesc": { "version": "3.1.0", "resolved": "https://registry.npmjs.org/jsesc/-/jsesc-3.1.0.tgz", "integrity": "sha512-/sM3dO2FOzXjKQhJuo0Q173wf2KOo8t4I8vHy6lF9poUp7bKT0/NHE8fPX23PwfhnykfqnC2xRxOnVw5XuGIaA==", - "dev": true, "license": "MIT", "bin": { "jsesc": "bin/jsesc" @@ -1822,11 +5673,28 @@ "node": ">=6" } }, + "node_modules/json-parse-even-better-errors": { + "version": "2.3.1", + "resolved": "https://registry.npmjs.org/json-parse-even-better-errors/-/json-parse-even-better-errors-2.3.1.tgz", + "integrity": "sha512-xyFwyhro/JEof6Ghe2iz2NcXoj2sloNsWr/XsERDK/oiPCfaNhl5ONfp+jQdAZRQQ0IJWNzH9zIZF7li91kh2w==", + "license": "MIT" + }, + "node_modules/json-schema-traverse": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-1.0.0.tgz", + "integrity": "sha512-NM8/P9n3XjXhIZn1lLhkFaACTOURQXjWhV4BA/RnOv8xvgqtqpAX9IO4mRQxSx1Rlo4tqzeqb0sOlruaOy3dug==", + "license": "MIT" + }, + "node_modules/json-schema-typed": { + "version": "8.0.2", + "resolved": "https://registry.npmjs.org/json-schema-typed/-/json-schema-typed-8.0.2.tgz", + "integrity": "sha512-fQhoXdcvc3V28x7C7BMs4P5+kNlgUURe2jmUT1T//oBRMDrqy1QPelJimwZGo7Hg9VPV3EQV5Bnq4hbFy2vetA==", + "license": "BSD-2-Clause" + }, "node_modules/json5": { "version": "2.2.3", "resolved": "https://registry.npmjs.org/json5/-/json5-2.2.3.tgz", "integrity": "sha512-XmOWe7eyHYH14cLdVPoyg+GOH3rYX++KpzrylJwSW98t3Nk+U8XOl8FWKOgwtzdb8lXGf6zYwDUzeHMWfxasyg==", - "dev": true, "license": "MIT", "bin": { "json5": "lib/cli.js" @@ -1835,6 +5703,27 @@ "node": ">=6" } }, + "node_modules/jsonfile": { + "version": "6.2.0", + "resolved": "https://registry.npmjs.org/jsonfile/-/jsonfile-6.2.0.tgz", + "integrity": "sha512-FGuPw30AdOIUTRMC2OMRtQV+jkVj2cfPqSeWXv1NEAJ1qZ5zb1X6z1mFhbfOB/iy3ssJCD+3KuZ8r8C3uVFlAg==", + "license": "MIT", + "dependencies": { + "universalify": "^2.0.0" + }, + "optionalDependencies": { + "graceful-fs": "^4.1.6" + } + }, + "node_modules/kleur": { + "version": "4.1.5", + "resolved": "https://registry.npmjs.org/kleur/-/kleur-4.1.5.tgz", + "integrity": "sha512-o+NO+8WrRiQEE4/7nwRJhN1HWpVmJm511pBHUxPLtp0BUISzlBplORYSmTclCnJvQq2tKu/sgl3xVpkc7ZWuQQ==", + "license": "MIT", + "engines": { + "node": ">=6" + } + }, "node_modules/lightningcss": { "version": "1.31.1", "resolved": "https://registry.npmjs.org/lightningcss/-/lightningcss-1.31.1.tgz", @@ -2096,11 +5985,44 @@ "url": "https://opencollective.com/parcel" } }, + "node_modules/lines-and-columns": { + "version": "1.2.4", + "resolved": "https://registry.npmjs.org/lines-and-columns/-/lines-and-columns-1.2.4.tgz", + "integrity": "sha512-7ylylesZQ/PV29jhEDl3Ufjo6ZX7gCqJr5F7PKrqc93v7fzSymt1BpwEU8nAUXs8qzzvqhbjhK5QZg6Mt/HkBg==", + "license": "MIT" + }, + "node_modules/log-symbols": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/log-symbols/-/log-symbols-6.0.0.tgz", + "integrity": "sha512-i24m8rpwhmPIS4zscNzK6MSEhk0DUWa/8iYQWxhffV8jkI4Phvs3F+quL5xvS0gdQR0FyTCMMH33Y78dDTzzIw==", + "license": "MIT", + "dependencies": { + "chalk": "^5.3.0", + "is-unicode-supported": "^1.3.0" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/log-symbols/node_modules/is-unicode-supported": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/is-unicode-supported/-/is-unicode-supported-1.3.0.tgz", + "integrity": "sha512-43r2mRvz+8JRIKnWJ+3j8JtjRKZ6GmjzfaE/qiBJnikNnYv/6bagRJ1kUhNk8R5EX/GkobD+r+sfxCPJsiKBLQ==", + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, "node_modules/lru-cache": { "version": "5.1.1", "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-5.1.1.tgz", "integrity": "sha512-KpNARQA3Iwv+jTA0utUVVbrh+Jlrr1Fv0e56GGzAFOXN7dk/FviaDW8LHmK52DlcH4WP2n6gI8vN1aesBFgo9w==", - "dev": true, "license": "ISC", "dependencies": { "yallist": "^3.0.2" @@ -2116,18 +6038,237 @@ "@jridgewell/sourcemap-codec": "^1.5.5" } }, + "node_modules/math-intrinsics": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/math-intrinsics/-/math-intrinsics-1.1.0.tgz", + "integrity": "sha512-/IXtbwEk5HTPyEwyKX6hGkYXxM9nbj64B+ilVJnC/R6B0pH5G4V3b0pVbL7DBj4tkhBAppbQUlf6F6Xl9LHu1g==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/media-typer": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/media-typer/-/media-typer-1.1.0.tgz", + "integrity": "sha512-aisnrDP4GNe06UcKFnV5bfMNPBUw4jsLGaWwWfnH3v02GnBuXX2MCVn5RbrWo0j3pczUilYblq7fQ7Nw2t5XKw==", + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/merge-descriptors": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/merge-descriptors/-/merge-descriptors-2.0.0.tgz", + "integrity": "sha512-Snk314V5ayFLhp3fkUREub6WtjBfPdCPY1Ln8/8munuLuiYhsABgBVWsozAG+MWMbVEvcdcpbi9R7ww22l9Q3g==", + "license": "MIT", + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/merge-stream": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/merge-stream/-/merge-stream-2.0.0.tgz", + "integrity": "sha512-abv/qOcuPfk3URPfDzmZU1LKmuw8kT+0nIHvKrKgFrwifol/doWcdA4ZqsWQ8ENrFKkd67Mfpo/LovbIUsbt3w==", + "license": "MIT" + }, + "node_modules/merge2": { + "version": "1.4.1", + "resolved": "https://registry.npmjs.org/merge2/-/merge2-1.4.1.tgz", + "integrity": "sha512-8q7VEgMJW4J8tcfVPy8g09NcQwZdbwFEqhe/WZkoIzjn/3TGDwtOCYtXGxA3O8tPzpczCCDgv+P2P5y00ZJOOg==", + "license": "MIT", + "engines": { + "node": ">= 8" + } + }, + "node_modules/micromatch": { + "version": "4.0.8", + "resolved": "https://registry.npmjs.org/micromatch/-/micromatch-4.0.8.tgz", + "integrity": "sha512-PXwfBhYu0hBCPw8Dn0E+WDYb7af3dSLVWKi3HGv84IdF4TyFoC0ysxFd0Goxw7nSv4T/PzEJQxsYsEiFCKo2BA==", + "license": "MIT", + "dependencies": { + "braces": "^3.0.3", + "picomatch": "^2.3.1" + }, + "engines": { + "node": ">=8.6" + } + }, + "node_modules/micromatch/node_modules/picomatch": { + "version": "2.3.1", + "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-2.3.1.tgz", + "integrity": "sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA==", + "license": "MIT", + "engines": { + "node": ">=8.6" + }, + "funding": { + "url": "https://github.com/sponsors/jonschlinkert" + } + }, + "node_modules/mime-db": { + "version": "1.54.0", + "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.54.0.tgz", + "integrity": "sha512-aU5EJuIN2WDemCcAp2vFBfp/m4EAhWJnUNSSw0ixs7/kXbd6Pg64EmwJkNdFhB8aWt1sH2CTXrLxo/iAGV3oPQ==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/mime-types": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-3.0.2.tgz", + "integrity": "sha512-Lbgzdk0h4juoQ9fCKXW4by0UJqj+nOOrI9MJ1sSj4nI8aI2eo1qmvQEie4VD1glsS250n15LsWsYtCugiStS5A==", + "license": "MIT", + "dependencies": { + "mime-db": "^1.54.0" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, + "node_modules/mimic-fn": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/mimic-fn/-/mimic-fn-2.1.0.tgz", + "integrity": "sha512-OqbOk5oEQeAZ8WXWydlu9HJjz9WVdEIvamMCcXmuqUYjTknH/sqsWvhQ3vgwKFRR1HpjvNBKQ37nbJgYzGqGcg==", + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/mimic-function": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/mimic-function/-/mimic-function-5.0.1.tgz", + "integrity": "sha512-VP79XUPxV2CigYP3jWwAUFSku2aKqBH7uTAapFWCBqutsbmDo96KY5o8uh6U+/YSIn5OxJnXp73beVkpqMIGhA==", + "license": "MIT", + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/minimatch": { + "version": "10.2.4", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-10.2.4.tgz", + "integrity": "sha512-oRjTw/97aTBN0RHbYCdtF1MQfvusSIBQM0IZEgzl6426+8jSC0nF1a/GmnVLpfB9yyr6g6FTqWqiZVbxrtaCIg==", + "license": "BlueOak-1.0.0", + "dependencies": { + "brace-expansion": "^5.0.2" + }, + "engines": { + "node": "18 || 20 || >=22" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/minimist": { + "version": "1.2.8", + "resolved": "https://registry.npmjs.org/minimist/-/minimist-1.2.8.tgz", + "integrity": "sha512-2yyAR8qBkN3YuheJanUpWC5U3bb5osDywNB8RzDVlDwDHbocAJveqqj1u8+SVD7jkWT4yvsHCpWqqWqAxb0zCA==", + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/motion-dom": { + "version": "12.36.0", + "resolved": "https://registry.npmjs.org/motion-dom/-/motion-dom-12.36.0.tgz", + "integrity": "sha512-Ep1pq8P88rGJ75om8lTCA13zqd7ywPGwCqwuWwin6BKc0hMLkVfcS6qKlRqEo2+t0DwoUcgGJfXwaiFn4AOcQA==", + "license": "MIT", + "dependencies": { + "motion-utils": "^12.36.0" + } + }, + "node_modules/motion-utils": { + "version": "12.36.0", + "resolved": "https://registry.npmjs.org/motion-utils/-/motion-utils-12.36.0.tgz", + "integrity": "sha512-eHWisygbiwVvf6PZ1vhaHCLamvkSbPIeAYxWUuL3a2PD/TROgE7FvfHWTIH4vMl798QLfMw15nRqIaRDXTlYRg==", + "license": "MIT" + }, "node_modules/ms": { "version": "2.1.3", "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==", - "dev": true, "license": "MIT" }, + "node_modules/msw": { + "version": "2.12.10", + "resolved": "https://registry.npmjs.org/msw/-/msw-2.12.10.tgz", + "integrity": "sha512-G3VUymSE0/iegFnuipujpwyTM2GuZAKXNeerUSrG2+Eg391wW63xFs5ixWsK9MWzr1AGoSkYGmyAzNgbR3+urw==", + "hasInstallScript": true, + "license": "MIT", + "dependencies": { + "@inquirer/confirm": "^5.0.0", + "@mswjs/interceptors": "^0.41.2", + "@open-draft/deferred-promise": "^2.2.0", + "@types/statuses": "^2.0.6", + "cookie": "^1.0.2", + "graphql": "^16.12.0", + "headers-polyfill": "^4.0.2", + "is-node-process": "^1.2.0", + "outvariant": "^1.4.3", + "path-to-regexp": "^6.3.0", + "picocolors": "^1.1.1", + "rettime": "^0.10.1", + "statuses": "^2.0.2", + "strict-event-emitter": "^0.5.1", + "tough-cookie": "^6.0.0", + "type-fest": "^5.2.0", + "until-async": "^3.0.2", + "yargs": "^17.7.2" + }, + "bin": { + "msw": "cli/index.js" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/mswjs" + }, + "peerDependencies": { + "typescript": ">= 4.8.x" + }, + "peerDependenciesMeta": { + "typescript": { + "optional": true + } + } + }, + "node_modules/msw/node_modules/cookie": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/cookie/-/cookie-1.1.1.tgz", + "integrity": "sha512-ei8Aos7ja0weRpFzJnEA9UHJ/7XQmqglbRwnf2ATjcB9Wq874VKH9kfjjirM6UhU2/E5fFYadylyhFldcqSidQ==", + "license": "MIT", + "engines": { + "node": ">=18" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, + "node_modules/mute-stream": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/mute-stream/-/mute-stream-2.0.0.tgz", + "integrity": "sha512-WWdIxpyjEn+FhQJQQv9aQAYlHoNVdzIzUySNV1gHUPDSdZJ3yZn7pAAbQcV7B56Mvu881q9FZV+0Vx2xC44VWA==", + "license": "ISC", + "engines": { + "node": "^18.17.0 || >=20.5.0" + } + }, "node_modules/nanoid": { "version": "3.3.11", "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.11.tgz", "integrity": "sha512-N8SpfPUnUp1bK+PMYW8qSWdl9U+wwNWI4QKxOYDy9JAro3WMX7p2OeVRF9v+347pnakNevPmiHhNmZ2HbFA76w==", - "dev": true, "funding": [ { "type": "github", @@ -2142,25 +6283,306 @@ "node": "^10 || ^12 || ^13.7 || ^14 || >=15.0.1" } }, + "node_modules/negotiator": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/negotiator/-/negotiator-1.0.0.tgz", + "integrity": "sha512-8Ofs/AUQh8MaEcrlq5xOX0CQ9ypTF5dl78mjlMNfOK08fzpgTHQRQPBxcPlEtIw0yRpws+Zo/3r+5WRby7u3Gg==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/node-domexception": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/node-domexception/-/node-domexception-1.0.0.tgz", + "integrity": "sha512-/jKZoMpw0F8GRwl4/eLROPA3cfcXtLApP0QzLmUT/HuPCZWyB7IY9ZrMeKw2O/nFIqPQB3PVM9aYm0F312AXDQ==", + "deprecated": "Use your platform's native DOMException instead", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/jimmywarting" + }, + { + "type": "github", + "url": "https://paypal.me/jimmywarting" + } + ], + "license": "MIT", + "engines": { + "node": ">=10.5.0" + } + }, + "node_modules/node-fetch": { + "version": "3.3.2", + "resolved": "https://registry.npmjs.org/node-fetch/-/node-fetch-3.3.2.tgz", + "integrity": "sha512-dRB78srN/l6gqWulah9SrxeYnxeddIG30+GOqK/9OlLVyLg3HPnr6SqOWTWOXKRwC2eGYCkZ59NNuSgvSrpgOA==", + "license": "MIT", + "dependencies": { + "data-uri-to-buffer": "^4.0.0", + "fetch-blob": "^3.1.4", + "formdata-polyfill": "^4.0.10" + }, + "engines": { + "node": "^12.20.0 || ^14.13.1 || >=16.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/node-fetch" + } + }, "node_modules/node-releases": { "version": "2.0.36", "resolved": "https://registry.npmjs.org/node-releases/-/node-releases-2.0.36.tgz", "integrity": "sha512-TdC8FSgHz8Mwtw9g5L4gR/Sh9XhSP/0DEkQxfEFXOpiul5IiHgHan2VhYYb6agDSfp4KuvltmGApc8HMgUrIkA==", - "dev": true, + "license": "MIT" + }, + "node_modules/npm-run-path": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/npm-run-path/-/npm-run-path-6.0.0.tgz", + "integrity": "sha512-9qny7Z9DsQU8Ou39ERsPU4OZQlSTP47ShQzuKZ6PRXpYLtIFgl/DEBYEXKlvcEa+9tHVcK8CF81Y2V72qaZhWA==", + "license": "MIT", + "dependencies": { + "path-key": "^4.0.0", + "unicorn-magic": "^0.3.0" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/npm-run-path/node_modules/path-key": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/path-key/-/path-key-4.0.0.tgz", + "integrity": "sha512-haREypq7xkM7ErfgIyA0z+Bj4AGKlMSdlQE2jvJo6huWD1EdkKYV+G/T4nq0YEF2vgTT8kqMFKo1uHn950r4SQ==", + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/object-assign": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/object-assign/-/object-assign-4.1.1.tgz", + "integrity": "sha512-rJgTQnkUnH1sFw8yT6VSU3zD3sWmu6sZhIseY8VX+GRu3P6F7Fu+JNDoXfklElbLJSnc3FUQHVe4cU5hj+BcUg==", + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/object-inspect": { + "version": "1.13.4", + "resolved": "https://registry.npmjs.org/object-inspect/-/object-inspect-1.13.4.tgz", + "integrity": "sha512-W67iLl4J2EXEGTbfeHCffrjDfitvLANg0UlX3wFUUSTx92KXRFegMHUVgSqE+wvhAbi4WqjGg9czysTV2Epbew==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/object-treeify": { + "version": "1.1.33", + "resolved": "https://registry.npmjs.org/object-treeify/-/object-treeify-1.1.33.tgz", + "integrity": "sha512-EFVjAYfzWqWsBMRHPMAXLCDIJnpMhdWAqR7xG6M6a2cs6PMFpl/+Z20w9zDW4vkxOFfddegBKq9Rehd0bxWE7A==", + "license": "MIT", + "engines": { + "node": ">= 10" + } + }, + "node_modules/on-finished": { + "version": "2.4.1", + "resolved": "https://registry.npmjs.org/on-finished/-/on-finished-2.4.1.tgz", + "integrity": "sha512-oVlzkg3ENAhCk2zdv7IJwd/QUD4z2RxRwpkcGY8psCVcCYZNq4wYnVWALHM+brtuJjePWiYF/ClmuDr8Ch5+kg==", + "license": "MIT", + "dependencies": { + "ee-first": "1.1.1" + }, + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/once": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/once/-/once-1.4.0.tgz", + "integrity": "sha512-lNaJgI+2Q5URQBkccEKHTQOPaXdUxnZZElQTZY0MFUAuaEqe1E+Nyvgdz/aIyNi6Z9MzO5dv1H8n58/GELp3+w==", + "license": "ISC", + "dependencies": { + "wrappy": "1" + } + }, + "node_modules/onetime": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/onetime/-/onetime-7.0.0.tgz", + "integrity": "sha512-VXJjc87FScF88uafS3JllDgvAm+c/Slfz06lorj2uAY34rlUu0Nt+v8wreiImcrgAjjIHp1rXpTDlLOGw29WwQ==", + "license": "MIT", + "dependencies": { + "mimic-function": "^5.0.0" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/open": { + "version": "11.0.0", + "resolved": "https://registry.npmjs.org/open/-/open-11.0.0.tgz", + "integrity": "sha512-smsWv2LzFjP03xmvFoJ331ss6h+jixfA4UUV/Bsiyuu4YJPfN+FIQGOIiv4w9/+MoHkfkJ22UIaQWRVFRfH6Vw==", + "license": "MIT", + "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" + }, + "engines": { + "node": ">=20" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/ora": { + "version": "8.2.0", + "resolved": "https://registry.npmjs.org/ora/-/ora-8.2.0.tgz", + "integrity": "sha512-weP+BZ8MVNnlCm8c0Qdc1WSWq4Qn7I+9CJGm7Qali6g44e/PUzbjNqJX5NJ9ljlNMosfJvg1fKEGILklK9cwnw==", + "license": "MIT", + "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" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/outvariant": { + "version": "1.4.3", + "resolved": "https://registry.npmjs.org/outvariant/-/outvariant-1.4.3.tgz", + "integrity": "sha512-+Sl2UErvtsoajRDKCE5/dBz4DIvHXQQnAxtQTF04OJxY0+DyZXSo5P5Bb7XYWOh81syohlYL24hbDwxedPUJCA==", + "license": "MIT" + }, + "node_modules/overlayscrollbars": { + "version": "2.14.0", + "resolved": "https://registry.npmjs.org/overlayscrollbars/-/overlayscrollbars-2.14.0.tgz", + "integrity": "sha512-RjV0pqc79kYhQLC3vTcLRb5GLpI1n6qh0Oua3g+bGH4EgNOJHVBGP7u0zZtxoAa0dkHlAqTTSYRb9MMmxNLjig==", + "license": "MIT" + }, + "node_modules/overlayscrollbars-react": { + "version": "0.5.6", + "resolved": "https://registry.npmjs.org/overlayscrollbars-react/-/overlayscrollbars-react-0.5.6.tgz", + "integrity": "sha512-E5To04bL5brn9GVCZ36SnfGanxa2I2MDkWoa4Cjo5wol7l+diAgi4DBc983V7l2nOk/OLJ6Feg4kySspQEGDBw==", + "license": "MIT", + "peerDependencies": { + "overlayscrollbars": "^2.0.0", + "react": ">=16.8.0" + } + }, + "node_modules/package-manager-detector": { + "version": "1.6.0", + "resolved": "https://registry.npmjs.org/package-manager-detector/-/package-manager-detector-1.6.0.tgz", + "integrity": "sha512-61A5ThoTiDG/C8s8UMZwSorAGwMJ0ERVGj2OjoW5pAalsNOg15+iQiPzrLJ4jhZ1HJzmC2PIHT2oEiH3R5fzNA==", + "license": "MIT" + }, + "node_modules/parent-module": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/parent-module/-/parent-module-1.0.1.tgz", + "integrity": "sha512-GQ2EWRpQV8/o+Aw8YqtfZZPfNRWZYkbidE9k5rpl/hC3vtHHBfGm2Ifi6qWV+coDGkrUKZAxE3Lot5kcsRlh+g==", + "license": "MIT", + "dependencies": { + "callsites": "^3.0.0" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/parse-json": { + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/parse-json/-/parse-json-5.2.0.tgz", + "integrity": "sha512-ayCKvm/phCGxOkYRSCM82iDwct8/EonSEgCSxWxD7ve6jHggsFl4fZVQBPRNgQoKiuV/odhFrGzQXZwbifC8Rg==", + "license": "MIT", + "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" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/parse-ms": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/parse-ms/-/parse-ms-4.0.0.tgz", + "integrity": "sha512-TXfryirbmq34y8QBwgqCVLi+8oA3oWx2eAnSn62ITyEhEYaWRlVZ2DvMM9eZbMs/RfxPu/PK/aBLyGj4IrqMHw==", + "license": "MIT", + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/parseurl": { + "version": "1.3.3", + "resolved": "https://registry.npmjs.org/parseurl/-/parseurl-1.3.3.tgz", + "integrity": "sha512-CiyeOxFT/JZyN5m0z9PfXw4SCBJ6Sygz1Dpl0wqjlhDEGGBP1GnsUVEL0p63hoG1fcj3fHynXi9NYO4nWOL+qQ==", + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/path-browserify": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/path-browserify/-/path-browserify-1.0.1.tgz", + "integrity": "sha512-b7uo2UCUOYZcnF/3ID0lulOJi/bafxa1xPe7ZPsammBSpjSWQkjNxlt635YGS2MiR9GjvuXCtz2emr3jbsz98g==", + "license": "MIT" + }, + "node_modules/path-key": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/path-key/-/path-key-3.1.1.tgz", + "integrity": "sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q==", + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/path-to-regexp": { + "version": "6.3.0", + "resolved": "https://registry.npmjs.org/path-to-regexp/-/path-to-regexp-6.3.0.tgz", + "integrity": "sha512-Yhpw4T9C6hPpgPeA28us07OJeqZ5EzQTkbfwuhsUg0c237RomFoETJgmp2sa3F/41gfLE6G5cqcYwznmeEeOlQ==", "license": "MIT" }, "node_modules/picocolors": { "version": "1.1.1", "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.1.1.tgz", "integrity": "sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==", - "dev": true, "license": "ISC" }, "node_modules/picomatch": { "version": "4.0.3", "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.3.tgz", "integrity": "sha512-5gTmgEY/sqK6gFXLIsQNH19lWb4ebPDLA4SdLP7dsWkIXHWlG66oPuVvXSGFPppYZz8ZDZq0dYYrbHfBCVUb1Q==", - "dev": true, "license": "MIT", "engines": { "node": ">=12" @@ -2169,11 +6591,19 @@ "url": "https://github.com/sponsors/jonschlinkert" } }, + "node_modules/pkce-challenge": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/pkce-challenge/-/pkce-challenge-5.0.1.tgz", + "integrity": "sha512-wQ0b/W4Fr01qtpHlqSqspcj3EhBvimsdh0KlHhH8HRZnMsEa0ea2fTULOXOS9ccQr3om+GcGRk4e+isrZWV8qQ==", + "license": "MIT", + "engines": { + "node": ">=16.20.0" + } + }, "node_modules/postcss": { "version": "8.5.8", "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.5.8.tgz", "integrity": "sha512-OW/rX8O/jXnm82Ey1k44pObPtdblfiuWnrd8X7GJ7emImCOstunGbXUpp7HdBrFQX6rJzn3sPT397Wp5aCwCHg==", - "dev": true, "funding": [ { "type": "opencollective", @@ -2198,6 +6628,217 @@ "node": "^10 || ^12 || >=14" } }, + "node_modules/postcss-selector-parser": { + "version": "7.1.1", + "resolved": "https://registry.npmjs.org/postcss-selector-parser/-/postcss-selector-parser-7.1.1.tgz", + "integrity": "sha512-orRsuYpJVw8LdAwqqLykBj9ecS5/cRHlI5+nvTo8LcCKmzDmqVORXtOIYEEQuL9D4BxtA1lm5isAqzQZCoQ6Eg==", + "license": "MIT", + "dependencies": { + "cssesc": "^3.0.0", + "util-deprecate": "^1.0.2" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/powershell-utils": { + "version": "0.1.0", + "resolved": "https://registry.npmjs.org/powershell-utils/-/powershell-utils-0.1.0.tgz", + "integrity": "sha512-dM0jVuXJPsDN6DvRpea484tCUaMiXWjuCn++HGTqUWzGDjv5tZkEZldAJ/UMlqRYGFrD/etByo4/xOuC/snX2A==", + "license": "MIT", + "engines": { + "node": ">=20" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/pretty-ms": { + "version": "9.3.0", + "resolved": "https://registry.npmjs.org/pretty-ms/-/pretty-ms-9.3.0.tgz", + "integrity": "sha512-gjVS5hOP+M3wMm5nmNOucbIrqudzs9v/57bWRHQWLYklXqoXKrVfYW2W9+glfGsqtPgpiz5WwyEEB+ksXIx3gQ==", + "license": "MIT", + "dependencies": { + "parse-ms": "^4.0.0" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/prompts": { + "version": "2.4.2", + "resolved": "https://registry.npmjs.org/prompts/-/prompts-2.4.2.tgz", + "integrity": "sha512-NxNv/kLguCA7p3jE8oL2aEBsrJWgAakBpgmgK6lpPWV+WuOmY6r2/zbAVnP+T8bQlA0nzHXSJSJW0Hq7ylaD2Q==", + "license": "MIT", + "dependencies": { + "kleur": "^3.0.3", + "sisteransi": "^1.0.5" + }, + "engines": { + "node": ">= 6" + } + }, + "node_modules/prompts/node_modules/kleur": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/kleur/-/kleur-3.0.3.tgz", + "integrity": "sha512-eTIzlVOSUR+JxdDFepEYcBMtZ9Qqdef+rnzWdRZuMbOywu5tO2w2N7rqjoANZ5k9vywhL6Br1VRjUIgTQx4E8w==", + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/proxy-addr": { + "version": "2.0.7", + "resolved": "https://registry.npmjs.org/proxy-addr/-/proxy-addr-2.0.7.tgz", + "integrity": "sha512-llQsMLSUDUPT44jdrU/O37qlnifitDP+ZwrmmZcoSKyLKvtZxpyV0n2/bD/N4tBAAZ/gJEdZU7KMraoK1+XYAg==", + "license": "MIT", + "dependencies": { + "forwarded": "0.2.0", + "ipaddr.js": "1.9.1" + }, + "engines": { + "node": ">= 0.10" + } + }, + "node_modules/qs": { + "version": "6.15.0", + "resolved": "https://registry.npmjs.org/qs/-/qs-6.15.0.tgz", + "integrity": "sha512-mAZTtNCeetKMH+pSjrb76NAM8V9a05I9aBZOHztWy/UqcJdQYNsf59vrRKWnojAT9Y+GbIvoTBC++CPHqpDBhQ==", + "license": "BSD-3-Clause", + "dependencies": { + "side-channel": "^1.1.0" + }, + "engines": { + "node": ">=0.6" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/queue-microtask": { + "version": "1.2.3", + "resolved": "https://registry.npmjs.org/queue-microtask/-/queue-microtask-1.2.3.tgz", + "integrity": "sha512-NuaNSa6flKT5JaSYQzJok04JzTL1CA6aGhv5rfLW3PgqA+M2ChpZQnAC8h8i4ZFkBS8X5RqkDBHA7r4hej3K9A==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "license": "MIT" + }, + "node_modules/radix-ui": { + "version": "1.4.3", + "resolved": "https://registry.npmjs.org/radix-ui/-/radix-ui-1.4.3.tgz", + "integrity": "sha512-aWizCQiyeAenIdUbqEpXgRA1ya65P13NKn/W8rWkcN0OPkRDxdBVLWnIEDsS2RpwCK2nobI7oMUSmexzTDyAmA==", + "license": "MIT", + "dependencies": { + "@radix-ui/primitive": "1.1.3", + "@radix-ui/react-accessible-icon": "1.1.7", + "@radix-ui/react-accordion": "1.2.12", + "@radix-ui/react-alert-dialog": "1.1.15", + "@radix-ui/react-arrow": "1.1.7", + "@radix-ui/react-aspect-ratio": "1.1.7", + "@radix-ui/react-avatar": "1.1.10", + "@radix-ui/react-checkbox": "1.3.3", + "@radix-ui/react-collapsible": "1.1.12", + "@radix-ui/react-collection": "1.1.7", + "@radix-ui/react-compose-refs": "1.1.2", + "@radix-ui/react-context": "1.1.2", + "@radix-ui/react-context-menu": "2.2.16", + "@radix-ui/react-dialog": "1.1.15", + "@radix-ui/react-direction": "1.1.1", + "@radix-ui/react-dismissable-layer": "1.1.11", + "@radix-ui/react-dropdown-menu": "2.1.16", + "@radix-ui/react-focus-guards": "1.1.3", + "@radix-ui/react-focus-scope": "1.1.7", + "@radix-ui/react-form": "0.1.8", + "@radix-ui/react-hover-card": "1.1.15", + "@radix-ui/react-label": "2.1.7", + "@radix-ui/react-menu": "2.1.16", + "@radix-ui/react-menubar": "1.1.16", + "@radix-ui/react-navigation-menu": "1.2.14", + "@radix-ui/react-one-time-password-field": "0.1.8", + "@radix-ui/react-password-toggle-field": "0.1.3", + "@radix-ui/react-popover": "1.1.15", + "@radix-ui/react-popper": "1.2.8", + "@radix-ui/react-portal": "1.1.9", + "@radix-ui/react-presence": "1.1.5", + "@radix-ui/react-primitive": "2.1.3", + "@radix-ui/react-progress": "1.1.7", + "@radix-ui/react-radio-group": "1.3.8", + "@radix-ui/react-roving-focus": "1.1.11", + "@radix-ui/react-scroll-area": "1.2.10", + "@radix-ui/react-select": "2.2.6", + "@radix-ui/react-separator": "1.1.7", + "@radix-ui/react-slider": "1.3.6", + "@radix-ui/react-slot": "1.2.3", + "@radix-ui/react-switch": "1.2.6", + "@radix-ui/react-tabs": "1.1.13", + "@radix-ui/react-toast": "1.2.15", + "@radix-ui/react-toggle": "1.1.10", + "@radix-ui/react-toggle-group": "1.1.11", + "@radix-ui/react-toolbar": "1.1.11", + "@radix-ui/react-tooltip": "1.2.8", + "@radix-ui/react-use-callback-ref": "1.1.1", + "@radix-ui/react-use-controllable-state": "1.2.2", + "@radix-ui/react-use-effect-event": "0.0.2", + "@radix-ui/react-use-escape-keydown": "1.1.1", + "@radix-ui/react-use-is-hydrated": "0.1.0", + "@radix-ui/react-use-layout-effect": "1.1.1", + "@radix-ui/react-use-size": "1.1.1", + "@radix-ui/react-visually-hidden": "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" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, + "node_modules/range-parser": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/range-parser/-/range-parser-1.2.1.tgz", + "integrity": "sha512-Hrgsx+orqoygnmhFbKaHE6c296J+HTAQXoxEF6gNupROmmGJRoyzfG3ccAveqCBrwr/2yxQ5BVd/GTl5agOwSg==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/raw-body": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/raw-body/-/raw-body-3.0.2.tgz", + "integrity": "sha512-K5zQjDllxWkf7Z5xJdV0/B0WTNqx6vxG70zJE4N0kBs4LovmEYWJzQGxC9bS9RAKu3bgM40lrd5zoLJ12MQ5BA==", + "license": "MIT", + "dependencies": { + "bytes": "~3.1.2", + "http-errors": "~2.0.1", + "iconv-lite": "~0.7.0", + "unpipe": "~1.0.0" + }, + "engines": { + "node": ">= 0.10" + } + }, "node_modules/react": { "version": "19.2.4", "resolved": "https://registry.npmjs.org/react/-/react-19.2.4.tgz", @@ -2229,6 +6870,150 @@ "node": ">=0.10.0" } }, + "node_modules/react-remove-scroll": { + "version": "2.7.2", + "resolved": "https://registry.npmjs.org/react-remove-scroll/-/react-remove-scroll-2.7.2.tgz", + "integrity": "sha512-Iqb9NjCCTt6Hf+vOdNIZGdTiH1QSqr27H/Ek9sv/a97gfueI/5h1s3yRi1nngzMUaOOToin5dI1dXKdXiF+u0Q==", + "license": "MIT", + "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" + }, + "engines": { + "node": ">=10" + }, + "peerDependencies": { + "@types/react": "*", + "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, + "node_modules/react-remove-scroll-bar": { + "version": "2.3.8", + "resolved": "https://registry.npmjs.org/react-remove-scroll-bar/-/react-remove-scroll-bar-2.3.8.tgz", + "integrity": "sha512-9r+yi9+mgU33AKcj6IbT9oRCO78WriSj6t/cF8DWBZJ9aOGPOTEDvdUDz1FwKim7QXWwmHqtdHnRJfhAxEG46Q==", + "license": "MIT", + "dependencies": { + "react-style-singleton": "^2.2.2", + "tslib": "^2.0.0" + }, + "engines": { + "node": ">=10" + }, + "peerDependencies": { + "@types/react": "*", + "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, + "node_modules/react-style-singleton": { + "version": "2.2.3", + "resolved": "https://registry.npmjs.org/react-style-singleton/-/react-style-singleton-2.2.3.tgz", + "integrity": "sha512-b6jSvxvVnyptAiLjbkWLE/lOnR4lfTtDAl+eUC7RZy+QQWc6wRzIV2CE6xBuMmDxc2qIihtDCZD5NPOFl7fRBQ==", + "license": "MIT", + "dependencies": { + "get-nonce": "^1.0.0", + "tslib": "^2.0.0" + }, + "engines": { + "node": ">=10" + }, + "peerDependencies": { + "@types/react": "*", + "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, + "node_modules/recast": { + "version": "0.23.11", + "resolved": "https://registry.npmjs.org/recast/-/recast-0.23.11.tgz", + "integrity": "sha512-YTUo+Flmw4ZXiWfQKGcwwc11KnoRAYgzAE2E7mXKCjSviTKShtxBsN6YUUBB2gtaBzKzeKunxhUwNHQuRryhWA==", + "license": "MIT", + "dependencies": { + "ast-types": "^0.16.1", + "esprima": "~4.0.0", + "source-map": "~0.6.1", + "tiny-invariant": "^1.3.3", + "tslib": "^2.0.1" + }, + "engines": { + "node": ">= 4" + } + }, + "node_modules/require-directory": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/require-directory/-/require-directory-2.1.1.tgz", + "integrity": "sha512-fGxEI7+wsG9xrvdjsrlmL22OMTTiHRwAMroiEeMgq8gzoLC/PQr7RsRDSTLUg/bZAZtF+TVIkHc6/4RIKrui+Q==", + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/require-from-string": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/require-from-string/-/require-from-string-2.0.2.tgz", + "integrity": "sha512-Xf0nWe6RseziFMu+Ap9biiUbmplq6S9/p+7w7YXP/JBHhrUDDUhwa+vANyubuqfZWTveU//DYVGsDG7RKL/vEw==", + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/resolve-from": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/resolve-from/-/resolve-from-4.0.0.tgz", + "integrity": "sha512-pb/MYmXstAkysRFx8piNI1tGFNQIFA3vkE3Gq4EuA1dF6gHp/+vgZqsCGJapvy8N3Q+4o7FwvquPJcnZ7RYy4g==", + "license": "MIT", + "engines": { + "node": ">=4" + } + }, + "node_modules/restore-cursor": { + "version": "5.1.0", + "resolved": "https://registry.npmjs.org/restore-cursor/-/restore-cursor-5.1.0.tgz", + "integrity": "sha512-oMA2dcrw6u0YfxJQXm342bFKX/E4sG9rbTzO9ptUcR/e8A33cHuvStiYOwH7fszkZlZ1z/ta9AAoPk2F4qIOHA==", + "license": "MIT", + "dependencies": { + "onetime": "^7.0.0", + "signal-exit": "^4.1.0" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/rettime": { + "version": "0.10.1", + "resolved": "https://registry.npmjs.org/rettime/-/rettime-0.10.1.tgz", + "integrity": "sha512-uyDrIlUEH37cinabq0AX4QbgV4HbFZ/gqoiunWQ1UqBtRvTTytwhNYjE++pO/MjPTZL5KQCf2bEoJ/BJNVQ5Kw==", + "license": "MIT" + }, + "node_modules/reusify": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/reusify/-/reusify-1.1.0.tgz", + "integrity": "sha512-g6QUff04oZpHs0eG5p83rFLhHeV00ug/Yf9nZM6fLeUrPguBTkTQOdpAWWspMh55TZfVQDPaN3NQJfbVRAxdIw==", + "license": "MIT", + "engines": { + "iojs": ">=1.0.0", + "node": ">=0.10.0" + } + }, "node_modules/rollup": { "version": "4.59.0", "resolved": "https://registry.npmjs.org/rollup/-/rollup-4.59.0.tgz", @@ -2274,6 +7059,73 @@ "fsevents": "~2.3.2" } }, + "node_modules/router": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/router/-/router-2.2.0.tgz", + "integrity": "sha512-nLTrUKm2UyiL7rlhapu/Zl45FwNgkZGaCpZbIHajDYgwlJCOzLSk+cIPAnsEqV955GjILJnKbdQC1nVPz+gAYQ==", + "license": "MIT", + "dependencies": { + "debug": "^4.4.0", + "depd": "^2.0.0", + "is-promise": "^4.0.0", + "parseurl": "^1.3.3", + "path-to-regexp": "^8.0.0" + }, + "engines": { + "node": ">= 18" + } + }, + "node_modules/router/node_modules/path-to-regexp": { + "version": "8.3.0", + "resolved": "https://registry.npmjs.org/path-to-regexp/-/path-to-regexp-8.3.0.tgz", + "integrity": "sha512-7jdwVIRtsP8MYpdXSwOS0YdD0Du+qOoF/AEPIt88PcCFrZCzx41oxku1jD88hZBwbNUIEfpqvuhjFaMAqMTWnA==", + "license": "MIT", + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, + "node_modules/run-applescript": { + "version": "7.1.0", + "resolved": "https://registry.npmjs.org/run-applescript/-/run-applescript-7.1.0.tgz", + "integrity": "sha512-DPe5pVFaAsinSaV6QjQ6gdiedWDcRCbUuiQfQa2wmWV7+xC9bGulGI8+TdRmoFkAPaBXk8CrAbnlY2ISniJ47Q==", + "license": "MIT", + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/run-parallel": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/run-parallel/-/run-parallel-1.2.0.tgz", + "integrity": "sha512-5l4VyZR86LZ/lDxZTR6jqL8AFE2S0IFLMP26AbjsLVADxHdhB/c0GUsH+y39UfCi3dzz8OlQuPmnaJOMoDHQBA==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "license": "MIT", + "dependencies": { + "queue-microtask": "^1.2.2" + } + }, + "node_modules/safer-buffer": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/safer-buffer/-/safer-buffer-2.1.2.tgz", + "integrity": "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==", + "license": "MIT" + }, "node_modules/scheduler": { "version": "0.27.0", "resolved": "https://registry.npmjs.org/scheduler/-/scheduler-0.27.0.tgz", @@ -2284,22 +7136,366 @@ "version": "6.3.1", "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.1.tgz", "integrity": "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==", - "dev": true, "license": "ISC", "bin": { "semver": "bin/semver.js" } }, + "node_modules/send": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/send/-/send-1.2.1.tgz", + "integrity": "sha512-1gnZf7DFcoIcajTjTwjwuDjzuz4PPcY2StKPlsGAQ1+YH20IRVrBaXSWmdjowTJ6u8Rc01PoYOGHXfP1mYcZNQ==", + "license": "MIT", + "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" + }, + "engines": { + "node": ">= 18" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, + "node_modules/serve-static": { + "version": "2.2.1", + "resolved": "https://registry.npmjs.org/serve-static/-/serve-static-2.2.1.tgz", + "integrity": "sha512-xRXBn0pPqQTVQiC8wyQrKs2MOlX24zQ0POGaj0kultvoOCstBQM5yvOhAVSUwOMjQtTvsPWoNCHfPGwaaQJhTw==", + "license": "MIT", + "dependencies": { + "encodeurl": "^2.0.0", + "escape-html": "^1.0.3", + "parseurl": "^1.3.3", + "send": "^1.2.0" + }, + "engines": { + "node": ">= 18" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, + "node_modules/setprototypeof": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/setprototypeof/-/setprototypeof-1.2.0.tgz", + "integrity": "sha512-E5LDX7Wrp85Kil5bhZv46j8jOeboKq5JMmYM3gVGdGH8xFpPWXUMsNrlODCrkoxMEeNi/XZIwuRvY4XNwYMJpw==", + "license": "ISC" + }, + "node_modules/shadcn": { + "version": "4.0.6", + "resolved": "https://registry.npmjs.org/shadcn/-/shadcn-4.0.6.tgz", + "integrity": "sha512-20M3ZsQPas+0QLVzrfrya2V3nryQiIPQsnJaOJg+Ylk24GtJa1vXGOLB7xwKuvWoMzM0YSLuCaoibW/NuHdDyA==", + "license": "MIT", + "dependencies": { + "@antfu/ni": "^25.0.0", + "@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", + "msw": "^2.10.4", + "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" + } + }, + "node_modules/shebang-command": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/shebang-command/-/shebang-command-2.0.0.tgz", + "integrity": "sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA==", + "license": "MIT", + "dependencies": { + "shebang-regex": "^3.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/shebang-regex": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/shebang-regex/-/shebang-regex-3.0.0.tgz", + "integrity": "sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A==", + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/side-channel": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/side-channel/-/side-channel-1.1.0.tgz", + "integrity": "sha512-ZX99e6tRweoUXqR+VBrslhda51Nh5MTQwou5tnUDgbtyM0dBgmhEDtWGP/xbKn6hqfPRHujUNwz5fy/wbbhnpw==", + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0", + "object-inspect": "^1.13.3", + "side-channel-list": "^1.0.0", + "side-channel-map": "^1.0.1", + "side-channel-weakmap": "^1.0.2" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/side-channel-list": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/side-channel-list/-/side-channel-list-1.0.0.tgz", + "integrity": "sha512-FCLHtRD/gnpCiCHEiJLOwdmFP+wzCmDEkc9y7NsYxeF4u7Btsn1ZuwgwJGxImImHicJArLP4R0yX4c2KCrMrTA==", + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0", + "object-inspect": "^1.13.3" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/side-channel-map": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/side-channel-map/-/side-channel-map-1.0.1.tgz", + "integrity": "sha512-VCjCNfgMsby3tTdo02nbjtM/ewra6jPHmpThenkTYh8pG9ucZ/1P8So4u4FGBek/BjpOVsDCMoLA/iuBKIFXRA==", + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.2", + "es-errors": "^1.3.0", + "get-intrinsic": "^1.2.5", + "object-inspect": "^1.13.3" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/side-channel-weakmap": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/side-channel-weakmap/-/side-channel-weakmap-1.0.2.tgz", + "integrity": "sha512-WPS/HvHQTYnHisLo9McqBHOJk2FkHO/tlpvldyrnem4aeQp4hai3gythswg6p01oSoTl58rcpiFAjF2br2Ak2A==", + "license": "MIT", + "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" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/signal-exit": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/signal-exit/-/signal-exit-4.1.0.tgz", + "integrity": "sha512-bzyZ1e88w9O1iNJbKnOlvYTrWPDl46O1bG0D3XInv+9tkPrxrN8jUUTiFlDkkmKWgn1M6CfIA13SuGqOa9Korw==", + "license": "ISC", + "engines": { + "node": ">=14" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/sisteransi": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/sisteransi/-/sisteransi-1.0.5.tgz", + "integrity": "sha512-bLGGlR1QxBcynn2d5YmDX4MGjlZvy2MRBDRNHLJ8VI6l6+9FUiyTFNJ0IveOSP0bcXgVDPRcfGqA0pjaqUpfVg==", + "license": "MIT" + }, + "node_modules/sonner": { + "version": "2.0.7", + "resolved": "https://registry.npmjs.org/sonner/-/sonner-2.0.7.tgz", + "integrity": "sha512-W6ZN4p58k8aDKA4XPcx2hpIQXBRAgyiWVkYhT7CvK6D3iAu7xjvVyhQHg2/iaKJZ1XVJ4r7XuwGL+WGEK37i9w==", + "license": "MIT", + "peerDependencies": { + "react": "^18.0.0 || ^19.0.0 || ^19.0.0-rc", + "react-dom": "^18.0.0 || ^19.0.0 || ^19.0.0-rc" + } + }, + "node_modules/source-map": { + "version": "0.6.1", + "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz", + "integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==", + "license": "BSD-3-Clause", + "engines": { + "node": ">=0.10.0" + } + }, "node_modules/source-map-js": { "version": "1.2.1", "resolved": "https://registry.npmjs.org/source-map-js/-/source-map-js-1.2.1.tgz", "integrity": "sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA==", - "dev": true, "license": "BSD-3-Clause", "engines": { "node": ">=0.10.0" } }, + "node_modules/statuses": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/statuses/-/statuses-2.0.2.tgz", + "integrity": "sha512-DvEy55V3DB7uknRo+4iOGT5fP1slR8wQohVdknigZPMpMstaKJQWhwiYBACJE3Ul2pTnATihhBYnRhZQHGBiRw==", + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/stdin-discarder": { + "version": "0.2.2", + "resolved": "https://registry.npmjs.org/stdin-discarder/-/stdin-discarder-0.2.2.tgz", + "integrity": "sha512-UhDfHmA92YAlNnCfhmq0VeNL5bDbiZGg7sZ2IvPsXubGkiNa9EC+tUTsjBRsYUAz87btI6/1wf4XoVvQ3uRnmQ==", + "license": "MIT", + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/strict-event-emitter": { + "version": "0.5.1", + "resolved": "https://registry.npmjs.org/strict-event-emitter/-/strict-event-emitter-0.5.1.tgz", + "integrity": "sha512-vMgjE/GGEPEFnhFub6pa4FmJBRBVOLpIII2hvCZ8Kzb7K0hlHo7mQv6xYrBvCL2LtAIBwFUK8wvuJgTVSQ5MFQ==", + "license": "MIT" + }, + "node_modules/string-width": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-7.2.0.tgz", + "integrity": "sha512-tsaTIkKW9b4N+AEj+SVA+WhJzV7/zMhcSu78mLKWSk7cXMOSHsBKFWUs0fWwq8QyK3MgJBQRX6Gbi4kYbdvGkQ==", + "license": "MIT", + "dependencies": { + "emoji-regex": "^10.3.0", + "get-east-asian-width": "^1.0.0", + "strip-ansi": "^7.1.0" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/stringify-object": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/stringify-object/-/stringify-object-5.0.0.tgz", + "integrity": "sha512-zaJYxz2FtcMb4f+g60KsRNFOpVMUyuJgA51Zi5Z1DOTC3S59+OQiVOzE9GZt0x72uBGWKsQIuBKeF9iusmKFsg==", + "license": "BSD-2-Clause", + "dependencies": { + "get-own-enumerable-keys": "^1.0.0", + "is-obj": "^3.0.0", + "is-regexp": "^3.1.0" + }, + "engines": { + "node": ">=14.16" + }, + "funding": { + "url": "https://github.com/yeoman/stringify-object?sponsor=1" + } + }, + "node_modules/strip-ansi": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-7.2.0.tgz", + "integrity": "sha512-yDPMNjp4WyfYBkHnjIRLfca1i6KMyGCtsVgoKe/z1+6vukgaENdgGBZt+ZmKPc4gavvEZ5OgHfHdrazhgNyG7w==", + "license": "MIT", + "dependencies": { + "ansi-regex": "^6.2.2" + }, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/chalk/strip-ansi?sponsor=1" + } + }, + "node_modules/strip-bom": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/strip-bom/-/strip-bom-3.0.0.tgz", + "integrity": "sha512-vavAMRXOgBVNF6nyEEmL3DBK19iRpDcoIwW+swQ+CbGiu7lju6t+JklA1MHweoWtadgt4ISVUsXLyDq34ddcwA==", + "license": "MIT", + "engines": { + "node": ">=4" + } + }, + "node_modules/strip-final-newline": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/strip-final-newline/-/strip-final-newline-4.0.0.tgz", + "integrity": "sha512-aulFJcD6YK8V1G7iRB5tigAP4TsHBZZrOV8pjV++zdUwmeV8uzbY7yn6h9MswN62adStNZFuCIx4haBnRuMDaw==", + "license": "MIT", + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/tagged-tag": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/tagged-tag/-/tagged-tag-1.0.0.tgz", + "integrity": "sha512-yEFYrVhod+hdNyx7g5Bnkkb0G6si8HJurOoOEgC8B/O0uXLHlaey/65KRv6cuWBNhBgHKAROVpc7QyYqE5gFng==", + "license": "MIT", + "engines": { + "node": ">=20" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/tailwind-merge": { + "version": "3.5.0", + "resolved": "https://registry.npmjs.org/tailwind-merge/-/tailwind-merge-3.5.0.tgz", + "integrity": "sha512-I8K9wewnVDkL1NTGoqWmVEIlUcB9gFriAEkXkfCjX5ib8ezGxtR3xD7iZIxrfArjEsH7F1CHD4RFUtxefdqV/A==", + "license": "MIT", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/dcastil" + } + }, "node_modules/tailwindcss": { "version": "4.2.1", "resolved": "https://registry.npmjs.org/tailwindcss/-/tailwindcss-4.2.1.tgz", @@ -2321,6 +7517,21 @@ "url": "https://opencollective.com/webpack" } }, + "node_modules/tiny-invariant": { + "version": "1.3.3", + "resolved": "https://registry.npmjs.org/tiny-invariant/-/tiny-invariant-1.3.3.tgz", + "integrity": "sha512-+FbBPE1o9QAYvviau/qC5SE3caw21q3xkvWKBtja5vgqOWIHHJ3ioaq1VPfn/Szqctz2bU/oYeKd9/z5BL+PVg==", + "license": "MIT" + }, + "node_modules/tinyexec": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/tinyexec/-/tinyexec-1.0.4.tgz", + "integrity": "sha512-u9r3uZC0bdpGOXtlxUIdwf9pkmvhqJdrVCH9fapQtgy/OeTTMZ1nqH7agtvEfmGui6e1XxjcdrlxvxJvc3sMqw==", + "license": "MIT", + "engines": { + "node": ">=18" + } + }, "node_modules/tinyglobby": { "version": "0.2.15", "resolved": "https://registry.npmjs.org/tinyglobby/-/tinyglobby-0.2.15.tgz", @@ -2338,11 +7549,130 @@ "url": "https://github.com/sponsors/SuperchupuDev" } }, + "node_modules/tldts": { + "version": "7.0.25", + "resolved": "https://registry.npmjs.org/tldts/-/tldts-7.0.25.tgz", + "integrity": "sha512-keinCnPbwXEUG3ilrWQZU+CqcTTzHq9m2HhoUP2l7Xmi8l1LuijAXLpAJ5zRW+ifKTNscs4NdCkfkDCBYm352w==", + "license": "MIT", + "dependencies": { + "tldts-core": "^7.0.25" + }, + "bin": { + "tldts": "bin/cli.js" + } + }, + "node_modules/tldts-core": { + "version": "7.0.25", + "resolved": "https://registry.npmjs.org/tldts-core/-/tldts-core-7.0.25.tgz", + "integrity": "sha512-ZjCZK0rppSBu7rjHYDYsEaMOIbbT+nWF57hKkv4IUmZWBNrBWBOjIElc0mKRgLM8bm7x/BBlof6t2gi/Oq/Asw==", + "license": "MIT" + }, + "node_modules/to-regex-range": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/to-regex-range/-/to-regex-range-5.0.1.tgz", + "integrity": "sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ==", + "license": "MIT", + "dependencies": { + "is-number": "^7.0.0" + }, + "engines": { + "node": ">=8.0" + } + }, + "node_modules/toidentifier": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/toidentifier/-/toidentifier-1.0.1.tgz", + "integrity": "sha512-o5sSPKEkg/DIQNmH43V0/uerLrpzVedkUh8tGNvaeXpfpuwjKenlSox/2O/BTlZUtEe+JG7s5YhEz608PlAHRA==", + "license": "MIT", + "engines": { + "node": ">=0.6" + } + }, + "node_modules/tough-cookie": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/tough-cookie/-/tough-cookie-6.0.1.tgz", + "integrity": "sha512-LktZQb3IeoUWB9lqR5EWTHgW/VTITCXg4D21M+lvybRVdylLrRMnqaIONLVb5mav8vM19m44HIcGq4qASeu2Qw==", + "license": "BSD-3-Clause", + "dependencies": { + "tldts": "^7.0.5" + }, + "engines": { + "node": ">=16" + } + }, + "node_modules/ts-morph": { + "version": "26.0.0", + "resolved": "https://registry.npmjs.org/ts-morph/-/ts-morph-26.0.0.tgz", + "integrity": "sha512-ztMO++owQnz8c/gIENcM9XfCEzgoGphTv+nKpYNM1bgsdOVC/jRZuEBf6N+mLLDNg68Kl+GgUZfOySaRiG1/Ug==", + "license": "MIT", + "dependencies": { + "@ts-morph/common": "~0.27.0", + "code-block-writer": "^13.0.3" + } + }, + "node_modules/tsconfig-paths": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/tsconfig-paths/-/tsconfig-paths-4.2.0.tgz", + "integrity": "sha512-NoZ4roiN7LnbKn9QqE1amc9DJfzvZXxF4xDavcOWt1BPkdx+m+0gJuPM+S0vCe7zTJMYUP0R8pO2XMr+Y8oLIg==", + "license": "MIT", + "dependencies": { + "json5": "^2.2.2", + "minimist": "^1.2.6", + "strip-bom": "^3.0.0" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/tslib": { + "version": "2.8.1", + "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.8.1.tgz", + "integrity": "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==", + "license": "0BSD" + }, + "node_modules/tw-animate-css": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/tw-animate-css/-/tw-animate-css-1.4.0.tgz", + "integrity": "sha512-7bziOlRqH0hJx80h/3mbicLW7o8qLsH5+RaLR2t+OHM3D0JlWGODQKQ4cxbK7WlvmUxpcj6Kgu6EKqjrGFe3QQ==", + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/Wombosvideo" + } + }, + "node_modules/type-fest": { + "version": "5.4.4", + "resolved": "https://registry.npmjs.org/type-fest/-/type-fest-5.4.4.tgz", + "integrity": "sha512-JnTrzGu+zPV3aXIUhnyWJj4z/wigMsdYajGLIYakqyOW1nPllzXEJee0QQbHj+CTIQtXGlAjuK0UY+2xTyjVAw==", + "license": "(MIT OR CC0-1.0)", + "dependencies": { + "tagged-tag": "^1.0.0" + }, + "engines": { + "node": ">=20" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/type-is": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/type-is/-/type-is-2.0.1.tgz", + "integrity": "sha512-OZs6gsjF4vMp32qrCbiVSkrFmXtG/AZhY3t0iAMrMBiAZyV9oALtXO8hsrHbMXF9x6L3grlFuwW2oAz7cav+Gw==", + "license": "MIT", + "dependencies": { + "content-type": "^1.0.5", + "media-typer": "^1.1.0", + "mime-types": "^3.0.0" + }, + "engines": { + "node": ">= 0.6" + } + }, "node_modules/typescript": { "version": "5.9.3", "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.9.3.tgz", "integrity": "sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw==", - "dev": true, + "devOptional": true, "license": "Apache-2.0", "bin": { "tsc": "bin/tsc", @@ -2352,11 +7682,56 @@ "node": ">=14.17" } }, + "node_modules/undici-types": { + "version": "7.18.2", + "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-7.18.2.tgz", + "integrity": "sha512-AsuCzffGHJybSaRrmr5eHr81mwJU3kjw6M+uprWvCXiNeN9SOGwQ3Jn8jb8m3Z6izVgknn1R0FTCEAP2QrLY/w==", + "devOptional": true, + "license": "MIT" + }, + "node_modules/unicorn-magic": { + "version": "0.3.0", + "resolved": "https://registry.npmjs.org/unicorn-magic/-/unicorn-magic-0.3.0.tgz", + "integrity": "sha512-+QBBXBCvifc56fsbuxZQ6Sic3wqqc3WWaqxs58gvJrcOuN83HGTCwz3oS5phzU9LthRNE9VrJCFCLUgHeeFnfA==", + "license": "MIT", + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/universalify": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/universalify/-/universalify-2.0.1.tgz", + "integrity": "sha512-gptHNQghINnc/vTGIk0SOFGFNXw7JVrlRUtConJRlvaw6DuX0wO5Jeko9sWrMBhh+PsYAZ7oXAiOnf/UKogyiw==", + "license": "MIT", + "engines": { + "node": ">= 10.0.0" + } + }, + "node_modules/unpipe": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/unpipe/-/unpipe-1.0.0.tgz", + "integrity": "sha512-pjy2bYhSsufwWlKwPc+l3cN7+wuJlK6uz0YdJEOlQDbl6jo/YlPi4mb8agUkVC8BF7V8NuzeyPNqRksA3hztKQ==", + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/until-async": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/until-async/-/until-async-3.0.2.tgz", + "integrity": "sha512-IiSk4HlzAMqTUseHHe3VhIGyuFmN90zMTpD3Z3y8jeQbzLIq500MVM7Jq2vUAnTKAFPJrqwkzr6PoTcPhGcOiw==", + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/kettanaito" + } + }, "node_modules/update-browserslist-db": { "version": "1.2.3", "resolved": "https://registry.npmjs.org/update-browserslist-db/-/update-browserslist-db-1.2.3.tgz", "integrity": "sha512-Js0m9cx+qOgDxo0eMiFGEueWztz+d4+M3rGlmKPT+T4IS/jP4ylw3Nwpu6cpTTP8R1MAC1kF4VbdLt3ARf209w==", - "dev": true, "funding": [ { "type": "opencollective", @@ -2383,6 +7758,82 @@ "browserslist": ">= 4.21.0" } }, + "node_modules/use-callback-ref": { + "version": "1.3.3", + "resolved": "https://registry.npmjs.org/use-callback-ref/-/use-callback-ref-1.3.3.tgz", + "integrity": "sha512-jQL3lRnocaFtu3V00JToYz/4QkNWswxijDaCVNZRiRTO3HQDLsdu1ZtmIUvV4yPp+rvWm5j0y0TG/S61cuijTg==", + "license": "MIT", + "dependencies": { + "tslib": "^2.0.0" + }, + "engines": { + "node": ">=10" + }, + "peerDependencies": { + "@types/react": "*", + "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, + "node_modules/use-sidecar": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/use-sidecar/-/use-sidecar-1.1.3.tgz", + "integrity": "sha512-Fedw0aZvkhynoPYlA5WXrMCAMm+nSWdZt6lzJQ7Ok8S6Q+VsHmHpRWndVRJ8Be0ZbkfPc5LRYH+5XrzXcEeLRQ==", + "license": "MIT", + "dependencies": { + "detect-node-es": "^1.1.0", + "tslib": "^2.0.0" + }, + "engines": { + "node": ">=10" + }, + "peerDependencies": { + "@types/react": "*", + "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, + "node_modules/use-sync-external-store": { + "version": "1.6.0", + "resolved": "https://registry.npmjs.org/use-sync-external-store/-/use-sync-external-store-1.6.0.tgz", + "integrity": "sha512-Pp6GSwGP/NrPIrxVFAIkOQeyw8lFenOHijQWkUTrDvrF4ALqylP2C/KCkeS9dpUM3KvYRQhna5vt7IL95+ZQ9w==", + "license": "MIT", + "peerDependencies": { + "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0" + } + }, + "node_modules/util-deprecate": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/util-deprecate/-/util-deprecate-1.0.2.tgz", + "integrity": "sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw==", + "license": "MIT" + }, + "node_modules/validate-npm-package-name": { + "version": "7.0.2", + "resolved": "https://registry.npmjs.org/validate-npm-package-name/-/validate-npm-package-name-7.0.2.tgz", + "integrity": "sha512-hVDIBwsRruT73PbK7uP5ebUt+ezEtCmzZz3F59BSr2F6OVFnJ/6h8liuvdLrQ88Xmnk6/+xGGuq+pG9WwTuy3A==", + "license": "ISC", + "engines": { + "node": "^20.17.0 || >=22.9.0" + } + }, + "node_modules/vary": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/vary/-/vary-1.1.2.tgz", + "integrity": "sha512-BNGbWLfd0eUPabhkXUVm0j8uuvREyTh5ovRa/dyow/BqAbZJyC+5fU+IzQOzmAKzYqYRAISoRhdQr3eIZ/PXqg==", + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, "node_modules/vite": { "version": "6.4.1", "resolved": "https://registry.npmjs.org/vite/-/vite-6.4.1.tgz", @@ -2458,13 +7909,232 @@ } } }, + "node_modules/web-streams-polyfill": { + "version": "3.3.3", + "resolved": "https://registry.npmjs.org/web-streams-polyfill/-/web-streams-polyfill-3.3.3.tgz", + "integrity": "sha512-d2JWLCivmZYTSIoge9MsgFCZrt571BikcWGYkjC1khllbTeDlGqZ2D8vD8E/lJa8WGWbb7Plm8/XJYV7IJHZZw==", + "license": "MIT", + "engines": { + "node": ">= 8" + } + }, + "node_modules/which": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/which/-/which-4.0.0.tgz", + "integrity": "sha512-GlaYyEb07DPxYCKhKzplCWBJtvxZcZMrL+4UkrTSJHHPyZU4mYYTv3qaOe77H7EODLSSopAUFAc6W8U4yqvscg==", + "license": "ISC", + "dependencies": { + "isexe": "^3.1.1" + }, + "bin": { + "node-which": "bin/which.js" + }, + "engines": { + "node": "^16.13.0 || >=18.0.0" + } + }, + "node_modules/wrap-ansi": { + "version": "6.2.0", + "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-6.2.0.tgz", + "integrity": "sha512-r6lPcBGxZXlIcymEu7InxDMhdW0KDxpLgoFLcguasxCaJ/SOIZwINatK9KY/tf+ZrlywOKU0UDj3ATXUBfxJXA==", + "license": "MIT", + "dependencies": { + "ansi-styles": "^4.0.0", + "string-width": "^4.1.0", + "strip-ansi": "^6.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/wrap-ansi/node_modules/ansi-regex": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz", + "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==", + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/wrap-ansi/node_modules/emoji-regex": { + "version": "8.0.0", + "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz", + "integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==", + "license": "MIT" + }, + "node_modules/wrap-ansi/node_modules/string-width": { + "version": "4.2.3", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz", + "integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==", + "license": "MIT", + "dependencies": { + "emoji-regex": "^8.0.0", + "is-fullwidth-code-point": "^3.0.0", + "strip-ansi": "^6.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/wrap-ansi/node_modules/strip-ansi": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz", + "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==", + "license": "MIT", + "dependencies": { + "ansi-regex": "^5.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/wrappy": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz", + "integrity": "sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ==", + "license": "ISC" + }, + "node_modules/wsl-utils": { + "version": "0.3.1", + "resolved": "https://registry.npmjs.org/wsl-utils/-/wsl-utils-0.3.1.tgz", + "integrity": "sha512-g/eziiSUNBSsdDJtCLB8bdYEUMj4jR7AGeUo96p/3dTafgjHhpF4RiCFPiRILwjQoDXx5MqkBr4fwWtR3Ky4Wg==", + "license": "MIT", + "dependencies": { + "is-wsl": "^3.1.0", + "powershell-utils": "^0.1.0" + }, + "engines": { + "node": ">=20" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/y18n": { + "version": "5.0.8", + "resolved": "https://registry.npmjs.org/y18n/-/y18n-5.0.8.tgz", + "integrity": "sha512-0pfFzegeDWJHJIAmTLRP2DwHjdF5s7jo9tuztdQxAhINCdvS+3nGINqPd00AphqJR/0LhANUS6/+7SCb98YOfA==", + "license": "ISC", + "engines": { + "node": ">=10" + } + }, "node_modules/yallist": { "version": "3.1.1", "resolved": "https://registry.npmjs.org/yallist/-/yallist-3.1.1.tgz", "integrity": "sha512-a4UGQaWPH59mOXUYnAG2ewncQS4i4F43Tv3JoAM+s2VDAmS9NsK8GpDMLrCHPksFT7h3K6TOoUNn2pb7RoXx4g==", - "dev": true, "license": "ISC" }, + "node_modules/yargs": { + "version": "17.7.2", + "resolved": "https://registry.npmjs.org/yargs/-/yargs-17.7.2.tgz", + "integrity": "sha512-7dSzzRQ++CKnNI/krKnYRV7JKKPUXMEh61soaHKg9mrWEhzFWhFnxPxGl+69cD1Ou63C13NUPCnmIcrvqCuM6w==", + "license": "MIT", + "dependencies": { + "cliui": "^8.0.1", + "escalade": "^3.1.1", + "get-caller-file": "^2.0.5", + "require-directory": "^2.1.1", + "string-width": "^4.2.3", + "y18n": "^5.0.5", + "yargs-parser": "^21.1.1" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/yargs-parser": { + "version": "21.1.1", + "resolved": "https://registry.npmjs.org/yargs-parser/-/yargs-parser-21.1.1.tgz", + "integrity": "sha512-tVpsJW7DdjecAiFpbIB1e3qxIQsE6NoPc5/eTdrbbIC4h0LVsWhnoa3g+m2HclBIujHzsxZ4VJVA+GUuc2/LBw==", + "license": "ISC", + "engines": { + "node": ">=12" + } + }, + "node_modules/yargs/node_modules/ansi-regex": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz", + "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==", + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/yargs/node_modules/emoji-regex": { + "version": "8.0.0", + "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz", + "integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==", + "license": "MIT" + }, + "node_modules/yargs/node_modules/string-width": { + "version": "4.2.3", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz", + "integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==", + "license": "MIT", + "dependencies": { + "emoji-regex": "^8.0.0", + "is-fullwidth-code-point": "^3.0.0", + "strip-ansi": "^6.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/yargs/node_modules/strip-ansi": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz", + "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==", + "license": "MIT", + "dependencies": { + "ansi-regex": "^5.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/yoctocolors": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/yoctocolors/-/yoctocolors-2.1.2.tgz", + "integrity": "sha512-CzhO+pFNo8ajLM2d2IW/R93ipy99LWjtwblvC1RsoSUMZgyLbYFr221TnSNT7GjGdYui6P459mw9JH/g/zW2ug==", + "license": "MIT", + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/yoctocolors-cjs": { + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/yoctocolors-cjs/-/yoctocolors-cjs-2.1.3.tgz", + "integrity": "sha512-U/PBtDf35ff0D8X8D0jfdzHYEPFxAI7jJlxZXwCSez5M3190m+QobIfh+sWDWSHMCWWJN2AWamkegn6vr6YBTw==", + "license": "MIT", + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/zod": { + "version": "3.25.76", + "resolved": "https://registry.npmjs.org/zod/-/zod-3.25.76.tgz", + "integrity": "sha512-gzUt/qt81nXsFGKIFcC3YnfEAx5NkunCfnDlvuBSSFS02bcXu4Lmea0AFIUwbLWxWPx3d9p8S5QoaujKcNQxcQ==", + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/colinhacks" + } + }, + "node_modules/zod-to-json-schema": { + "version": "3.25.1", + "resolved": "https://registry.npmjs.org/zod-to-json-schema/-/zod-to-json-schema-3.25.1.tgz", + "integrity": "sha512-pM/SU9d3YAggzi6MtR4h7ruuQlqKtad8e9S0fmxcMi+ueAK5Korys/aWcV9LIIHTVbj01NdzxcnXSN+O74ZIVA==", + "license": "ISC", + "peerDependencies": { + "zod": "^3.25 || ^4" + } + }, "node_modules/zustand": { "version": "5.0.11", "resolved": "https://registry.npmjs.org/zustand/-/zustand-5.0.11.tgz", diff --git a/ui/package.json b/ui/package.json index 2338baf..8032f90 100644 --- a/ui/package.json +++ b/ui/package.json @@ -9,14 +9,32 @@ "preview": "vite preview" }, "dependencies": { + "@dnd-kit/core": "^6.3.1", + "@dnd-kit/sortable": "^10.0.0", + "@dnd-kit/utilities": "^3.2.2", + "@fontsource-variable/geist": "^5.2.8", + "@tabler/icons-react": "^3.40.0", "@tanstack/react-virtual": "^3.13.22", + "@tauri-apps/plugin-dialog": "^2.6.0", + "@tauri-apps/plugin-shell": "^2.3.5", + "class-variance-authority": "^0.7.1", + "clsx": "^2.1.1", + "framer-motion": "^12.36.0", + "overlayscrollbars": "^2.14.0", + "overlayscrollbars-react": "^0.5.6", + "radix-ui": "^1.4.3", "react": "^19.2.4", "react-dom": "^19.2.4", + "shadcn": "^4.0.6", + "sonner": "^2.0.7", + "tailwind-merge": "^3.5.0", + "tw-animate-css": "^1.4.0", "zustand": "^5.0.11" }, "devDependencies": { "@tailwindcss/vite": "^4.2.1", "@tauri-apps/api": "^2.10.1", + "@types/node": "^25.5.0", "@types/react": "^19.2.14", "@types/react-dom": "^19.2.3", "@vitejs/plugin-react": "^4.7.0", diff --git a/ui/src/App.tsx b/ui/src/App.tsx index 85eb002..12be875 100644 --- a/ui/src/App.tsx +++ b/ui/src/App.tsx @@ -1,7 +1,16 @@ -import { AppShell } from "./components/layout/AppShell"; -import { useKeyboardShortcuts } from "./hooks/useKeyboardShortcuts"; +import { AppShell } from "@/components/layout/AppShell"; +import { useKeyboardShortcuts } from "@/hooks/useKeyboardShortcuts"; +import { useTheme } from "@/hooks/useTheme"; +import { useWindowState } from "@/hooks/useWindowState"; +import { TooltipProvider } from "@/components/ui/tooltip"; export default function App() { useKeyboardShortcuts(); - return ; + useTheme(); + useWindowState(); + return ( + + + + ); } diff --git a/ui/src/components/browser/FileList.tsx b/ui/src/components/browser/FileList.tsx index 5370f7a..a9d9c1e 100644 --- a/ui/src/components/browser/FileList.tsx +++ b/ui/src/components/browser/FileList.tsx @@ -1,6 +1,40 @@ -import { useRef } from "react"; +import { useRef, useState, useCallback, useMemo, useEffect } from "react"; import { useVirtualizer } from "@tanstack/react-virtual"; -import { useFileStore } from "../../stores/fileStore"; +import { useOverlayScrollbars } from "overlayscrollbars-react"; +import { invoke } from "@tauri-apps/api/core"; +import { useFileStore } from "@/stores/fileStore"; +import { useSettingsStore } from "@/stores/settingsStore"; +import { Checkbox } from "@/components/ui/checkbox"; +import { + ContextMenu, + ContextMenuContent, + ContextMenuItem, + ContextMenuSeparator, + ContextMenuTrigger, +} from "@/components/ui/context-menu"; +import { + IconCheck, + IconAlertTriangle, + IconX, + IconFileDescription, + IconArrowRight, + IconSelect, + IconDeselect, + IconCopy, + IconClipboard, + IconEye, + IconEyeOff, + IconArrowsSort, + IconFolder, + IconFile, + IconArrowUp, + IconChevronUp, + IconChevronDown, + IconExternalLink, +} from "@tabler/icons-react"; +import { cn } from "@/lib/utils"; +import { motion, AnimatePresence } from "framer-motion"; +import { announce } from "@/hooks/useAnnounce"; function formatSize(bytes: number): string { if (bytes < 1024) return `${bytes} B`; @@ -9,119 +43,844 @@ function formatSize(bytes: number): string { return `${(bytes / (1024 * 1024 * 1024)).toFixed(1)} GB`; } +function formatDateAbsolute(iso: string | null): string { + if (!iso) return "-"; + const d = new Date(iso); + const pad = (n: number) => String(n).padStart(2, "0"); + return `${d.getFullYear()}-${pad(d.getMonth() + 1)}-${pad(d.getDate())} ${pad(d.getHours())}:${pad(d.getMinutes())}`; +} + +function formatDateRelative(iso: string | null): string { + if (!iso) return "-"; + const d = new Date(iso); + const now = Date.now(); + const diff = now - d.getTime(); + const secs = Math.floor(diff / 1000); + if (secs < 60) return "just now"; + const mins = Math.floor(secs / 60); + if (mins < 60) return `${mins}m ago`; + const hrs = Math.floor(mins / 60); + if (hrs < 24) return `${hrs}h ago`; + const days = Math.floor(hrs / 24); + if (days < 30) return `${days}d ago`; + const months = Math.floor(days / 30); + if (months < 12) return `${months}mo ago`; + return `${Math.floor(months / 12)}y ago`; +} + +const DEFAULT_COL_WIDTHS = { + original: 0, // flex + renamed: 0, // flex + size: 72, + date: 120, + status: 80, +}; + export function FileList() { - const files = useFileStore((s) => s.files); + const allEntries = useFileStore((s) => s.files); const previewResults = useFileStore((s) => s.previewResults); const selectedFiles = useFileStore((s) => s.selectedFiles); const toggleFileSelection = useFileStore((s) => s.toggleFileSelection); + const currentPath = useFileStore((s) => s.currentPath); + const scanDirectory = useFileStore((s) => s.scanDirectory); + const zoom = useSettingsStore((s) => s.zoom); + const doubleClickAction = useSettingsStore((s) => s.doubleClickAction); + const showFullPath = useSettingsStore((s) => s.showFullPath); + const zebraStriping = useSettingsStore((s) => s.zebraStriping); + const dateFormat = useSettingsStore((s) => s.dateFormat); const parentRef = useRef(null); + const [initOverlayScrollbars] = useOverlayScrollbars({ + options: { scrollbars: { autoHide: "move", autoHideDelay: 600 } }, + defer: true, + }); + useEffect(() => { + if (parentRef.current) initOverlayScrollbars(parentRef.current); + }, [initOverlayScrollbars]); + const [contextFile, setContextFile] = useState(null); + const lastClickedIdx = useRef(-1); - const previewMap = new Map(previewResults.map((r) => [r.original_path, r])); + type SortKey = "original" | "renamed" | "size" | "status" | "date" | "type"; + type SortDir = "asc" | "desc"; + const defaultSort = useSettingsStore((s) => s.defaultSortOrder); + const locale = useSettingsStore((s) => s.locale); + const sortLocale = locale === "system" ? undefined : locale; + const sortKeyMap: Record = { name: "original", size: "size", date: "date", type: "type" }; + const [sortKey, setSortKey] = useState(sortKeyMap[defaultSort] ?? null); + const [sortDir, setSortDir] = useState("asc"); + + // column widths (fixed columns only; original/renamed use flex) + const [colWidths, setColWidths] = useState({ ...DEFAULT_COL_WIDTHS }); + const resizingCol = useRef<{ col: string; startX: number; startW: number } | null>(null); + + function toggleSort(key: SortKey) { + if (sortKey === key) { + if (sortDir === "asc") { + setSortDir("desc"); + announce(`Sorted by ${key}, descending`); + } else { + setSortKey(null); + setSortDir("asc"); + announce("Sort cleared"); + } + } else { + setSortKey(key); + setSortDir("asc"); + announce(`Sorted by ${key}, ascending`); + } + } + + const onResizePointerDown = useCallback((e: React.PointerEvent, col: string) => { + e.stopPropagation(); + e.preventDefault(); + resizingCol.current = { col, startX: e.clientX, startW: (colWidths as any)[col] }; + (e.target as HTMLElement).setPointerCapture(e.pointerId); + }, [colWidths]); + + const onResizePointerMove = useCallback((e: React.PointerEvent) => { + if (!resizingCol.current) return; + const { col, startX, startW } = resizingCol.current; + const z = useSettingsStore.getState().zoom; + const delta = (e.clientX - startX) / z; + const newW = Math.max(40, startW + delta); + setColWidths((prev) => ({ ...prev, [col]: newW })); + }, []); + + const onResizePointerUp = useCallback(() => { + resizingCol.current = null; + }, []); + + // marquee + const dragRef = useRef(false); + const didDragRef = useRef(false); + const startRef = useRef({ x: 0, y: 0 }); + const [marqueeBox, setMarqueeBox] = useState<{ x: number; y: number; w: number; h: number } | null>(null); + + const previewMap = useMemo( + () => new Map(previewResults.map((r) => [r.original_path, r])), + [previewResults], + ); + + // separate folders and files, folders first + const folders = useMemo(() => allEntries.filter((f) => f.is_dir), [allEntries]); + const unsortedFiles = useMemo(() => allEntries.filter((f) => !f.is_dir), [allEntries]); + + const sortFn = useCallback((a: typeof allEntries[0], b: typeof allEntries[0]) => { + if (!sortKey) return 0; + const dir = sortDir === "asc" ? 1 : -1; + switch (sortKey) { + case "original": + return dir * a.name.localeCompare(b.name, sortLocale, { sensitivity: "base" }); + case "renamed": { + const aPreview = previewMap.get(a.path); + const bPreview = previewMap.get(b.path); + return dir * (aPreview?.new_name || a.name).localeCompare(bPreview?.new_name || b.name, sortLocale, { sensitivity: "base" }); + } + case "size": + return dir * (a.size - b.size); + case "date": { + const aTime = a.modified ? new Date(a.modified).getTime() : 0; + const bTime = b.modified ? new Date(b.modified).getTime() : 0; + return dir * (aTime - bTime); + } + case "type": + return dir * (a.extension || "").localeCompare(b.extension || "", sortLocale, { sensitivity: "base" }); + case "status": { + const aPreview = previewMap.get(a.path); + const bPreview = previewMap.get(b.path); + const aScore = aPreview?.has_conflict ? 3 : aPreview?.has_error ? 2 : (aPreview && aPreview.new_name !== aPreview.original_name) ? 1 : 0; + const bScore = bPreview?.has_conflict ? 3 : bPreview?.has_error ? 2 : (bPreview && bPreview.new_name !== bPreview.original_name) ? 1 : 0; + return dir * (aScore - bScore); + } + default: + return 0; + } + }, [sortKey, sortDir, previewMap, sortLocale]); + + const sortedFolders = useMemo(() => { + if (!sortKey) return folders; + return [...folders].sort(sortFn); + }, [folders, sortFn, sortKey]); + + const files = useMemo(() => { + if (!sortKey) return unsortedFiles; + return [...unsortedFiles].sort(sortFn); + }, [unsortedFiles, sortFn, sortKey]); + + const setSortedFilePaths = useFileStore((s) => s.setSortedFilePaths); + const prevFileCountRef = useRef(0); + useEffect(() => { + const allPaths = [...sortedFolders, ...files].map((f) => f.path); + setSortedFilePaths(allPaths); + const total = sortedFolders.length + files.length; + if (total !== prevFileCountRef.current && total > 0) { + announce(`${files.length} files and ${sortedFolders.length} folders loaded`); + } + prevFileCountRef.current = total; + }, [sortedFolders, files, setSortedFilePaths]); + + // build display rows: parent nav + folders + files + const hasParent = currentPath && currentPath.length > 3; // more than just "C:\" + const parentRow = hasParent ? 1 : 0; + const totalRows = parentRow + sortedFolders.length + files.length; + + function getEntry(displayIndex: number) { + if (displayIndex < parentRow) return null; // parent row + const idx = displayIndex - parentRow; + if (idx < sortedFolders.length) return sortedFolders[idx]; + return files[idx - sortedFolders.length]; + } + + const compactMode = useSettingsStore((s) => s.compactMode); + const rowHeight = compactMode ? 26 : 32; const rowVirtualizer = useVirtualizer({ - count: files.length, + count: totalRows, getScrollElement: () => parentRef.current, - estimateSize: () => 28, + estimateSize: () => rowHeight, overscan: 20, }); - if (files.length === 0) { + const formatDate = useCallback( + (iso: string | null) => dateFormat === "relative" ? formatDateRelative(iso) : formatDateAbsolute(iso), + [dateFormat], + ); + + function handleFileDoubleClick(path: string) { + if (doubleClickAction === "open") { + invoke("reveal_in_explorer", { path }).catch(() => {}); + } + } + + function navigateUp() { + if (!currentPath) return; + const normalized = currentPath.replace(/\\/g, "/").replace(/\/$/, ""); + const parent = normalized.replace(/\/[^/]+$/, ""); + if (!parent) return; + // ensure drive roots keep trailing backslash (e.g. "D:\") + let result = parent.replace(/\//g, "\\"); + if (/^[A-Za-z]:$/.test(result)) result += "\\"; + scanDirectory(result); + } + + function navigateToFolder(path: string) { + scanDirectory(path); + } + + const onPointerDown = useCallback((e: React.PointerEvent) => { + if (e.button !== 0) return; + const target = e.target as HTMLElement; + if (target.closest("button") || target.closest("label") || target.closest("input") || target.tagName === "BUTTON") return; + if (target.closest("[role='checkbox']")) return; + + dragRef.current = true; + didDragRef.current = false; + startRef.current = { x: e.clientX, y: e.clientY }; + setMarqueeBox(null); + (e.target as HTMLElement).setPointerCapture(e.pointerId); + }, []); + + const onPointerMove = useCallback((e: React.PointerEvent) => { + if (!dragRef.current) return; + const sx = startRef.current.x; + const sy = startRef.current.y; + const cx = e.clientX; + const cy = e.clientY; + const w = Math.abs(cx - sx); + const h = Math.abs(cy - sy); + if (w > 4 || h > 4) { + didDragRef.current = true; + // account for root padding and zoom - fixed positioning is relative + // to the nearest transformed ancestor (the window-frame) + const pad = document.documentElement.classList.contains("maximized") ? 0 : 20; + setMarqueeBox({ + x: (Math.min(sx, cx) - pad) / zoom, + y: (Math.min(sy, cy) - pad) / zoom, + w: w / zoom, + h: h / zoom, + }); + } + }, [zoom]); + + const onPointerUp = useCallback((e: React.PointerEvent) => { + if (!dragRef.current) return; + dragRef.current = false; + + const sx = startRef.current.x; + const sy = startRef.current.y; + const x1 = Math.min(sx, e.clientX); + const y1 = Math.min(sy, e.clientY); + const x2 = Math.max(sx, e.clientX); + const y2 = Math.max(sy, e.clientY); + + setMarqueeBox(null); + + if (!didDragRef.current) return; // handled by onClick instead + + const scrollEl = parentRef.current; + if (!scrollEl) return; + + const newSelected = new Set(); + const rows = scrollEl.querySelectorAll("[data-file-path], [data-folder-path]"); + rows.forEach((row) => { + const r = row.getBoundingClientRect(); + if (r.left < x2 && r.right > x1 && r.top < y2 && r.bottom > y1) { + const path = row.getAttribute("data-file-path") || row.getAttribute("data-folder-path"); + if (path) newSelected.add(path); + } + }); + + // if marquee selected nothing, deselect all + useFileStore.setState({ selectedFiles: newSelected }); + }, []); + + // all entries in display order for shift-select across folders and files + const allDisplayEntries = useMemo(() => [...sortedFolders, ...files], [sortedFolders, files]); + + function handleRowClick(e: React.MouseEvent, filePath: string, index: number) { + // don't handle if it was a drag + if (didDragRef.current) return; + + const target = e.target as HTMLElement; + if (target.closest("[role='checkbox']") || target.closest("button") || target.closest("label")) return; + + if (e.shiftKey && lastClickedIdx.current >= 0) { + const from = Math.min(lastClickedIdx.current, index); + const to = Math.max(lastClickedIdx.current, index); + const rangeSelected = new Set(e.ctrlKey ? selectedFiles : new Set()); + for (let i = from; i <= to; i++) { + rangeSelected.add(allDisplayEntries[i].path); + } + useFileStore.setState({ selectedFiles: rangeSelected }); + } else if (e.ctrlKey) { + toggleFileSelection(filePath); + } else { + if (selectedFiles.size === 1 && selectedFiles.has(filePath)) { + useFileStore.setState({ selectedFiles: new Set() }); + } else { + useFileStore.setState({ selectedFiles: new Set([filePath]) }); + } + } + lastClickedIdx.current = index; + } + + function handleFolderDoubleClick(path: string) { + navigateToFolder(path); + } + + function handleBackgroundClick(e: React.MouseEvent) { + if (didDragRef.current) return; + // only deselect when clicking actual empty space (the scroll container itself, not a row) + const target = e.target as HTMLElement; + if (target.closest("[data-file-path]") || target.closest("[data-folder-path]") || target.closest("[role='checkbox']")) return; + useFileStore.setState({ selectedFiles: new Set() }); + } + + function handleSelectAll() { + useFileStore.getState().selectAll(); + } + + function handleDeselectAll() { + useFileStore.getState().deselectAll(); + } + + function handleSelectChanged() { + const changed = new Set(); + previewResults.forEach((r) => { + if (r.original_name !== r.new_name && !r.has_error && !r.has_conflict) { + changed.add(r.original_path); + } + }); + useFileStore.setState({ selectedFiles: changed }); + } + + function handleInvertSelection() { + const inverted = new Set(); + allDisplayEntries.forEach((f) => { + if (!selectedFiles.has(f.path)) inverted.add(f.path); + }); + useFileStore.setState({ selectedFiles: inverted }); + } + + function handleCopyNames() { + const names = files + .filter((f) => selectedFiles.has(f.path)) + .map((f) => f.name) + .join("\n"); + navigator.clipboard.writeText(names); + } + + function handleCopyNewNames() { + const names = files + .filter((f) => selectedFiles.has(f.path)) + .map((f) => { + const preview = previewMap.get(f.path); + return preview?.new_name || f.name; + }) + .join("\n"); + navigator.clipboard.writeText(names); + } + + if (allEntries.length === 0 && !currentPath) { return ( -
- Navigate to a folder to see files -
+ + + + + Navigate to a folder to see files + + + Use the sidebar to browse directories + + ); } return ( -
- {/* header */} -
-
-
Original Name
-
New Name
-
Size
-
Status
-
- - {/* virtual rows */} -
-
- {rowVirtualizer.getVirtualItems().map((virtualRow) => { - const file = files[virtualRow.index]; - const preview = previewMap.get(file.path); - const isSelected = selectedFiles.has(file.path); - const changed = preview && preview.new_name !== preview.original_name; - const hasError = preview?.has_error; - const hasConflict = preview?.has_conflict; - - let rowBg = virtualRow.index % 2 === 0 ? "var(--row-even)" : "var(--row-odd)"; - if (hasConflict) rowBg = "rgba(220, 38, 38, 0.1)"; - else if (hasError) rowBg = "rgba(217, 119, 6, 0.1)"; - else if (changed) rowBg = "rgba(22, 163, 74, 0.06)"; - - return ( -
+ +
+ {/* header */} + +
+ 0 && selectedFiles.size === allDisplayEntries.length} + onCheckedChange={(checked) => { + if (checked) handleSelectAll(); + else handleDeselectAll(); }} - > -
- toggleFileSelection(file.path)} - className="accent-[var(--accent)]" - /> -
-
- {file.name} -
-
- {preview?.new_name || file.name} -
-
- {file.is_dir ? "-" : formatSize(file.size)} -
-
- {hasConflict && Conflict} - {hasError && !hasConflict && ( - Error - )} - {changed && !hasError && OK} -
-
- ); - })} + aria-label="Select all files" + /> +
+ + + + onResizePointerDown(e, "size")} onPointerMove={onResizePointerMove} onPointerUp={onResizePointerUp} colWidths={colWidths} setColWidths={setColWidths} /> + + onResizePointerDown(e, "date")} onPointerMove={onResizePointerMove} onPointerUp={onResizePointerUp} colWidths={colWidths} setColWidths={setColWidths} /> + + onResizePointerDown(e, "status")} onPointerMove={onResizePointerMove} onPointerUp={onResizePointerUp} colWidths={colWidths} setColWidths={setColWidths} /> + + + + {/* virtual rows */} +
+
+ {rowVirtualizer.getVirtualItems().map((virtualRow) => { + // parent directory row + if (virtualRow.index < parentRow) { + return ( +
{ if (e.key === "Enter" || e.key === " ") { e.preventDefault(); navigateUp(); } }} + className="flex items-center text-[13px] px-1 absolute w-full border-b border-border/50 hover:bg-muted/50 cursor-pointer" + style={{ + height: `${virtualRow.size}px`, + transform: `translateY(${virtualRow.start}px)`, + }} + onClick={navigateUp} + > +
+ +
+
..
+
+ ); + } + + const entry = getEntry(virtualRow.index)!; + const isDir = entry.is_dir; + const preview = previewMap.get(entry.path); + const isSelected = selectedFiles.has(entry.path); + const changed = preview && preview.new_name !== preview.original_name; + const hasError = preview?.has_error; + const hasConflict = preview?.has_conflict; + + // entry index relative to allDisplayEntries for shift-select + const entryIndex = isDir + ? virtualRow.index - parentRow + : virtualRow.index - parentRow; + + const isOdd = virtualRow.index % 2 === 1; + + // folder row - single click selects, double click navigates + if (isDir) { + return ( +
setContextFile(entry.path)} + className={cn( + "flex items-center text-[13px] px-1 absolute w-full border-b border-border/50 transition-colors", + isSelected && "bg-primary/[0.06]", + hasConflict && "bg-destructive/5", + hasError && !hasConflict && "bg-warning/5", + changed && !hasError && !hasConflict && !isSelected && "bg-primary/[0.03]", + !changed && !hasError && !hasConflict && !isSelected && "hover:bg-muted/50", + zebraStriping && isOdd && !isSelected && !hasConflict && !hasError && !changed && "bg-muted/30", + )} + style={{ + height: `${virtualRow.size}px`, + transform: `translateY(${virtualRow.start}px)`, + }} + onClick={(e) => handleRowClick(e, entry.path, entryIndex)} + onDoubleClick={() => handleFolderDoubleClick(entry.path)} + > +
+ toggleFileSelection(entry.path)} + aria-label={`Select folder ${entry.name}`} + /> +
+ +
+ {entry.name} +
+
+ + {changed && ( + + + + )} + +
+
+ {preview?.new_name || entry.name} +
+
-
+
+ {formatDate(entry.modified)} +
+
+ + {hasConflict && ( + + Conflict + + )} + {changed && !hasError && !hasConflict && ( + + OK + + )} + +
+
+ ); + } + + // file row + return ( +
setContextFile(entry.path)} + className={cn( + "flex items-center text-[13px] px-1 absolute w-full border-b border-border/50 transition-colors", + isSelected && "bg-primary/[0.06]", + hasConflict && "bg-destructive/5", + hasError && !hasConflict && "bg-warning/5", + changed && !hasError && !hasConflict && !isSelected && "bg-primary/[0.03]", + !changed && !hasError && !hasConflict && !isSelected && "hover:bg-muted/50", + zebraStriping && isOdd && !isSelected && !hasConflict && !hasError && !changed && "bg-muted/30", + )} + style={{ + height: `${virtualRow.size}px`, + transform: `translateY(${virtualRow.start}px)`, + }} + onClick={(e) => handleRowClick(e, entry.path, entryIndex)} + onDoubleClick={() => handleFileDoubleClick(entry.path)} + > +
+ toggleFileSelection(entry.path)} + aria-label={`Select file ${entry.name}`} + /> +
+ +
+ {showFullPath ? entry.path : entry.name} +
+
+ + {changed && ( + + + + )} + +
+
+ {preview?.new_name || entry.name} +
+
+ {formatSize(entry.size)} +
+
+ {formatDate(entry.modified)} +
+
+ + {hasConflict && ( + + + Conflict + + )} + {hasError && !hasConflict && ( + + + Error + + )} + {changed && !hasError && !hasConflict && ( + + + OK + + )} + +
+
+ ); + })} +
+
+ + {/* marquee selection overlay */} + {marqueeBox && ( +
+ )}
-
-
+ + + + + + Select all + + + + Deselect all + + + + Invert selection + + + + Select changed only + + + + + Copy original names + + + + Copy new names + + + { + if (contextFile) { + const isSelected = selectedFiles.has(contextFile); + if (isSelected) { + const s = new Set(selectedFiles); + s.delete(contextFile); + useFileStore.setState({ selectedFiles: s }); + } else { + const s = new Set(selectedFiles); + s.add(contextFile); + useFileStore.setState({ selectedFiles: s }); + } + } + }} + className="gap-2 text-[13px]" + > + {contextFile && selectedFiles.has(contextFile) ? ( + <> + + Exclude from rename + + ) : ( + <> + + Include in rename + + )} + + {contextFile && ( + <> + + invoke("reveal_in_explorer", { path: contextFile }).catch(() => {})} + className="gap-2 text-[13px]" + > + + Open in file explorer + + navigator.clipboard.writeText(contextFile)} + className="gap-2 text-[13px]" + > + + Copy path + + + )} + + + ); +} + +function ColResizeHandle({ + col, + onPointerDown, + onPointerMove, + onPointerUp, + colWidths, + setColWidths, +}: { + col: string; + onPointerDown: (e: React.PointerEvent) => void; + onPointerMove: (e: React.PointerEvent) => void; + onPointerUp: (e: React.PointerEvent) => void; + colWidths: typeof DEFAULT_COL_WIDTHS; + setColWidths: React.Dispatch>; +}) { + const handleKeyDown = useCallback((e: React.KeyboardEvent) => { + const step = e.shiftKey ? 40 : 10; + if (e.key === "ArrowRight") { + e.preventDefault(); + setColWidths((prev) => ({ ...prev, [col]: Math.max(40, ((prev as any)[col] || 80) + step) })); + } else if (e.key === "ArrowLeft") { + e.preventDefault(); + setColWidths((prev) => ({ ...prev, [col]: Math.max(40, ((prev as any)[col] || 80) - step) })); + } + }, [col, setColWidths]); + + return ( +
); } diff --git a/ui/src/components/layout/AppShell.tsx b/ui/src/components/layout/AppShell.tsx index 343a564..5f7a11a 100644 --- a/ui/src/components/layout/AppShell.tsx +++ b/ui/src/components/layout/AppShell.tsx @@ -1,26 +1,153 @@ -import { useState } from "react"; +import { useState, useCallback, useEffect, useRef } from "react"; +import { invoke } from "@tauri-apps/api/core"; import { Sidebar } from "./Sidebar"; import { StatusBar } from "./StatusBar"; import { Toolbar } from "./Toolbar"; -import { FileList } from "../browser/FileList"; -import { RulePanel } from "../rules/RulePanel"; +import { FileList } from "@/components/browser/FileList"; +import { PipelineStrip } from "@/components/pipeline/PipelineStrip"; +import { Toaster } from "@/components/ui/sonner"; +import { useSettingsStore } from "@/stores/settingsStore"; +import { motion, AnimatePresence, MotionConfig } from "framer-motion"; +import { PortalContainerProvider } from "@/lib/portal"; +import { ResizeEdges } from "./ResizeEdges"; +import { toast } from "sonner"; +import { useAnnounceStore } from "@/hooks/useAnnounce"; export function AppShell() { - const [sidebarWidth, setSidebarWidth] = useState(240); + const [sidebarOpen, setSidebarOpen] = useState(true); + const [sidebarWidth, setSidebarWidth] = useState(220); + const [portalContainer, setPortalContainer] = useState(null); + const zoom = useSettingsStore((s) => s.zoom); + const animationsEnabled = useSettingsStore((s) => s.animationsEnabled); + const announceMessage = useAnnounceStore((s) => s.message); + const [maximized, setMaximized] = useState(() => document.documentElement.classList.contains("maximized")); + const dragging = useRef(false); + + useEffect(() => { + const observer = new MutationObserver(() => { + setMaximized(document.documentElement.classList.contains("maximized")); + }); + observer.observe(document.documentElement, { attributes: true, attributeFilter: ["class"] }); + return () => observer.disconnect(); + }, []); + + // suppress default browser context menu everywhere + useEffect(() => { + const handler = (e: MouseEvent) => { + const target = e.target as HTMLElement; + if (target.closest("[data-slot='context-menu-trigger']")) return; + e.preventDefault(); + }; + document.addEventListener("contextmenu", handler); + return () => document.removeEventListener("contextmenu", handler); + }, []); + + // prevent scroll events on overlay menus from leaking to the app + useEffect(() => { + const handler = (e: WheelEvent) => { + const target = e.target as HTMLElement; + const overlay = target.closest( + "[data-slot='context-menu-content'], [data-slot='dropdown-menu-content'], [data-slot='popover-content']" + ); + if (!overlay) return; + const el = overlay as HTMLElement; + const atTop = el.scrollTop <= 0 && e.deltaY < 0; + const atBottom = el.scrollTop + el.clientHeight >= el.scrollHeight && e.deltaY > 0; + const notScrollable = el.scrollHeight <= el.clientHeight; + if (atTop || atBottom || notScrollable) { + e.preventDefault(); + } + }; + document.addEventListener("wheel", handler, { passive: false }); + return () => document.removeEventListener("wheel", handler); + }, []); + + // startup update check + useEffect(() => { + const { checkForUpdates } = useSettingsStore.getState(); + if (!checkForUpdates) return; + invoke<{ available: boolean; latest_version: string; url: string }>("check_for_updates") + .then((info) => { + if (info.available) { + toast.info(`Update available: v${info.latest_version}`, { + description: "A new version of Nomina is available.", + duration: 8000, + }); + } + }) + .catch(() => {}); + }, []); + + const onDividerPointerDown = useCallback((e: React.PointerEvent) => { + e.preventDefault(); + dragging.current = true; + (e.target as HTMLElement).setPointerCapture(e.pointerId); + }, []); + + const onDividerPointerMove = useCallback((e: React.PointerEvent) => { + if (!dragging.current) return; + const z = useSettingsStore.getState().zoom; + const x = e.clientX / z; + setSidebarWidth(Math.max(140, Math.min(450, x))); + }, []); + + const onDividerPointerUp = useCallback(() => { + dragging.current = false; + }, []); return ( -
- -
- -
-
- + + +
+ Skip to file list + {/* live region for screen reader announcements */} +
{announceMessage}
+ + setSidebarOpen(!sidebarOpen)} /> +
+ + {sidebarOpen && ( + + + + )} + + {sidebarOpen && ( +
+ )} +
+
+ +
+
-
+
- -
+
+ +
); } diff --git a/ui/src/components/layout/ResizeEdges.tsx b/ui/src/components/layout/ResizeEdges.tsx new file mode 100644 index 0000000..19ae268 --- /dev/null +++ b/ui/src/components/layout/ResizeEdges.tsx @@ -0,0 +1,95 @@ +import { useRef } from "react"; +import { getCurrentWindow } from "@tauri-apps/api/window"; +import { PhysicalPosition, PhysicalSize } from "@tauri-apps/api/dpi"; + +type Dir = "n" | "s" | "e" | "w" | "ne" | "nw" | "se" | "sw"; + +const edges: { dir: Dir; className: string }[] = [ + { dir: "n", className: "absolute -top-[3px] left-3 right-3 h-[6px] cursor-n-resize z-50" }, + { dir: "s", className: "absolute -bottom-[3px] left-3 right-3 h-[6px] cursor-s-resize z-50" }, + { dir: "w", className: "absolute top-3 -left-[3px] bottom-3 w-[6px] cursor-w-resize z-50" }, + { dir: "e", className: "absolute top-3 -right-[3px] bottom-3 w-[6px] cursor-e-resize z-50" }, + { dir: "nw", className: "absolute -top-[3px] -left-[3px] w-4 h-4 cursor-nw-resize z-50" }, + { dir: "ne", className: "absolute -top-[3px] -right-[3px] w-4 h-4 cursor-ne-resize z-50" }, + { dir: "sw", className: "absolute -bottom-[3px] -left-[3px] w-4 h-4 cursor-sw-resize z-50" }, + { dir: "se", className: "absolute -bottom-[3px] -right-[3px] w-4 h-4 cursor-se-resize z-50" }, +]; + +interface DragState { + dir: Dir; + startX: number; + startY: number; + startPos: { x: number; y: number }; + startSize: { width: number; height: number }; +} + +const MIN_W = 900; +const MIN_H = 600; + +export function ResizeEdges() { + const drag = useRef(null); + + const onPointerDown = (dir: Dir) => async (e: React.PointerEvent) => { + e.preventDefault(); + (e.target as HTMLElement).setPointerCapture(e.pointerId); + + const win = getCurrentWindow(); + const pos = await win.outerPosition(); + const size = await win.outerSize(); + + drag.current = { + dir, + startX: e.screenX, + startY: e.screenY, + startPos: { x: pos.x, y: pos.y }, + startSize: { width: size.width, height: size.height }, + }; + }; + + const onPointerMove = async (e: React.PointerEvent) => { + if (!drag.current) return; + const d = drag.current; + const dx = e.screenX - d.startX; + const dy = e.screenY - d.startY; + + let x = d.startPos.x; + let y = d.startPos.y; + let w = d.startSize.width; + let h = d.startSize.height; + + if (d.dir.includes("e")) w = Math.max(MIN_W, d.startSize.width + dx); + if (d.dir.includes("s")) h = Math.max(MIN_H, d.startSize.height + dy); + if (d.dir.includes("w")) { + const newW = Math.max(MIN_W, d.startSize.width - dx); + x = d.startPos.x + (d.startSize.width - newW); + w = newW; + } + if (d.dir.includes("n")) { + const newH = Math.max(MIN_H, d.startSize.height - dy); + y = d.startPos.y + (d.startSize.height - newH); + h = newH; + } + + const win = getCurrentWindow(); + await win.setPosition(new PhysicalPosition(x, y)); + await win.setSize(new PhysicalSize(w, h)); + }; + + const onPointerUp = () => { + drag.current = null; + }; + + return ( + <> + {edges.map(({ dir, className }) => ( +
+ ))} + + ); +} diff --git a/ui/src/components/layout/Sidebar.tsx b/ui/src/components/layout/Sidebar.tsx index f776a72..3b3ec5a 100644 --- a/ui/src/components/layout/Sidebar.tsx +++ b/ui/src/components/layout/Sidebar.tsx @@ -1,59 +1,266 @@ -import { useState, useEffect } from "react"; +import { useState, useEffect, useRef, useCallback } from "react"; import { invoke } from "@tauri-apps/api/core"; -import { useFileStore } from "../../stores/fileStore"; +import { useFileStore } from "@/stores/fileStore"; +import { useSettingsStore } from "@/stores/settingsStore"; +import { Button } from "@/components/ui/button"; +import { Tooltip, TooltipContent, TooltipTrigger } from "@/components/ui/tooltip"; +import { Input } from "@/components/ui/input"; +import { ScrollArea } from "@/components/ui/scroll-area"; +import { + ContextMenu, + ContextMenuContent, + ContextMenuItem, + ContextMenuSeparator, + ContextMenuTrigger, +} from "@/components/ui/context-menu"; +import { + IconFolder, + IconFolderOpen, + IconChevronRight, + IconArrowUp, + IconFolderPlus, + IconExternalLink, + IconCopy, + IconFolderSymlink, +} from "@tabler/icons-react"; +import { cn } from "@/lib/utils"; +import { motion, AnimatePresence } from "framer-motion"; -interface SidebarProps { - width: number; - onResize: (width: number) => void; +interface FolderNode { + path: string; + name: string; + children: FolderNode[]; + loaded: boolean; + expanded: boolean; } -export function Sidebar({ width }: SidebarProps) { +export function Sidebar() { const [pathInput, setPathInput] = useState(""); - const [drives, setDrives] = useState([]); - const [folders, setFolders] = useState([]); + const [tree, setTree] = useState([]); const scanDirectory = useFileStore((s) => s.scanDirectory); const currentPath = useFileStore((s) => s.currentPath); + const restoredRef = useRef(false); + const treeRef = useRef([]); + const scrollRef = useRef(null); + const expandGenRef = useRef(0); + + // keep treeRef in sync so async functions see latest tree + treeRef.current = tree; useEffect(() => { - // detect windows drives - const detected: string[] = []; - for (const letter of "CDEFGHIJKLMNOPQRSTUVWXYZ") { - detected.push(`${letter}:\\`); - } - setDrives(detected); + loadDrives().then(async () => { + if (restoredRef.current) return; + restoredRef.current = true; + + // check if launched from context menu with paths + try { + const args = await invoke("get_launch_args"); + if (args.length > 0) { + const result = await invoke<[string, string[]] | null>("resolve_launch_paths", { args }); + if (result) { + const [folder, selected] = result; + await scanDirectory(folder); + if (selected.length > 0) { + useFileStore.setState({ selectedFiles: new Set(selected) }); + } else { + useFileStore.setState({ selectedFiles: new Set() }); + } + expandToPath(folder); + return; + } + } + } catch { + // no launch args support or failed - fall through + } + + const { lastFolder, openLastFolder } = useSettingsStore.getState(); + if (openLastFolder && lastFolder) { + scanDirectory(lastFolder); + expandToPath(lastFolder); + } + }); }, []); useEffect(() => { if (currentPath) { setPathInput(currentPath); - loadFolders(currentPath); + useSettingsStore.getState().setLastFolder(currentPath); + expandToPath(currentPath); } }, [currentPath]); - async function loadFolders(path: string) { + async function loadDrives() { + const drives: FolderNode[] = []; + for (const letter of "CDEFGHIJKLMNOPQRSTUVWXYZ") { + const path = `${letter}:\\`; + try { + const entries = await invoke>( + "scan_directory", + { + path, + filters: { + mask: "*", regex_filter: null, min_size: null, max_size: null, + include_files: false, include_folders: true, include_hidden: false, subfolder_depth: 0, + }, + }, + ); + if (entries.length >= 0) { + drives.push({ path, name: path, children: [], loaded: false, expanded: false }); + } + } catch { + // drive doesn't exist + } + } + setTree(drives); + treeRef.current = drives; + } + + async function loadChildren(node: FolderNode): Promise { try { const entries = await invoke>( "scan_directory", { - path, + path: node.path, filters: { - mask: "*", - regex_filter: null, - min_size: null, - max_size: null, - include_files: false, - include_folders: true, - include_hidden: false, - subfolder_depth: 0, + mask: "*", regex_filter: null, min_size: null, max_size: null, + include_files: false, include_folders: true, include_hidden: false, subfolder_depth: 0, }, }, ); - setFolders(entries.filter((e) => e.is_dir).map((e) => e.path)); + return entries + .filter((e) => e.is_dir) + .map((e) => ({ + path: e.path, + name: e.name, + children: [], + loaded: false, + expanded: false, + })); } catch { - setFolders([]); + return []; } } + // parse a Windows path into ancestor segments: "D:\foo\bar" -> ["D:\", "D:\foo", "D:\foo\bar"] + function pathSegments(p: string): string[] { + const normalized = p.replace(/\//g, "\\").replace(/\\$/, ""); + const parts = normalized.split("\\"); + const segments: string[] = []; + for (let i = 0; i < parts.length; i++) { + if (i === 0) { + segments.push(parts[0] + "\\"); + } else { + segments.push(segments[i - 1] + (segments[i - 1].endsWith("\\") ? "" : "\\") + parts[i]); + } + } + return segments; + } + + async function expandToPath(targetPath: string) { + const gen = ++expandGenRef.current; + const segments = pathSegments(targetPath); + if (segments.length === 0) return; + + // build set of ancestor paths that should stay expanded + const ancestorSet = new Set(segments.map((s) => s.replace(/\\$/, "").toLowerCase())); + + // collapse everything that isn't an ancestor of the target + setTree((prev) => { + function collapse(nodes: FolderNode[]): FolderNode[] { + return nodes.map((n) => { + const nNorm = n.path.replace(/\\$/, "").toLowerCase(); + const isAncestor = ancestorSet.has(nNorm); + return { + ...n, + expanded: isAncestor, + children: collapse(n.children), + }; + }); + } + const updated = collapse(prev); + treeRef.current = updated; + return updated; + }); + + for (const seg of segments) { + if (expandGenRef.current !== gen) return; + const segNorm = seg.replace(/\\$/, "").toLowerCase(); + + // find the node in the current tree + function findInTree(nodes: FolderNode[]): FolderNode | undefined { + for (const n of nodes) { + if (n.path.replace(/\\$/, "").toLowerCase() === segNorm) return n; + const found = findInTree(n.children); + if (found) return found; + } + return undefined; + } + + const node = findInTree(treeRef.current); + if (!node) return; + + // load children if needed (async) + let newChildren: FolderNode[] | null = null; + if (!node.loaded) { + newChildren = await loadChildren(node); + if (expandGenRef.current !== gen) return; + } + + // update tree immutably, always from latest state + setTree((prev) => { + function update(nodes: FolderNode[]): FolderNode[] { + return nodes.map((n) => { + if (n.path.replace(/\\$/, "").toLowerCase() === segNorm) { + return { + ...n, + expanded: true, + loaded: true, + children: newChildren || n.children, + }; + } + return { ...n, children: update(n.children) }; + }); + } + const updated = update(prev); + treeRef.current = updated; + return updated; + }); + } + + // scroll active node into view after render + requestAnimationFrame(() => { + const el = scrollRef.current?.querySelector("[data-tree-active='true']"); + if (el) el.scrollIntoView({ block: "center", behavior: "smooth" }); + }); + } + + async function toggleExpand(path: string) { + async function updateNode(nodes: FolderNode[]): Promise { + const result: FolderNode[] = []; + for (const node of nodes) { + if (node.path === path) { + if (!node.loaded) { + const children = await loadChildren(node); + result.push({ ...node, expanded: true, loaded: true, children }); + } else { + result.push({ ...node, expanded: !node.expanded }); + } + } else { + const updatedChildren = await updateNode(node.children); + result.push({ ...node, children: updatedChildren }); + } + } + return result; + } + const updated = await updateNode(treeRef.current); + setTree(updated); + treeRef.current = updated; + } + + function handleFolderClick(path: string) { + scanDirectory(path); + } + function handlePathSubmit(e: React.FormEvent) { e.preventDefault(); if (pathInput.trim()) { @@ -61,91 +268,255 @@ export function Sidebar({ width }: SidebarProps) { } } - function handleDriveClick(drive: string) { - scanDirectory(drive); + function handleGoUp() { + if (!currentPath) return; + const normalized = currentPath.replace(/\\/g, "/").replace(/\/$/, ""); + const parent = normalized.replace(/\/[^/]+$/, ""); + if (!parent) return; + let result = parent.replace(/\//g, "\\"); + if (/^[A-Za-z]:$/.test(result)) result += "\\"; + scanDirectory(result); } - function handleFolderClick(path: string) { - scanDirectory(path); - } + // flatten visible tree for keyboard navigation + const [focusedPath, setFocusedPath] = useState(null); - function folderName(path: string): string { - const parts = path.replace(/\\/g, "/").split("/").filter(Boolean); - return parts[parts.length - 1] || path; + const flattenVisible = useCallback((nodes: FolderNode[]): FolderNode[] => { + const result: FolderNode[] = []; + for (const n of nodes) { + result.push(n); + if (n.expanded) result.push(...flattenVisible(n.children)); + } + return result; + }, []); + + const findParent = useCallback((nodes: FolderNode[], targetPath: string, parent: FolderNode | null = null): FolderNode | null => { + for (const n of nodes) { + if (n.path === targetPath) return parent; + const found = findParent(n.children, targetPath, n); + if (found) return found; + } + return null; + }, []); + + const handleTreeKeyDown = useCallback((e: React.KeyboardEvent) => { + const visible = flattenVisible(tree); + if (visible.length === 0) return; + + const idx = focusedPath ? visible.findIndex((n) => n.path === focusedPath) : -1; + const current = idx >= 0 ? visible[idx] : null; + + switch (e.key) { + case "ArrowDown": { + e.preventDefault(); + const next = Math.min(idx + 1, visible.length - 1); + setFocusedPath(visible[Math.max(0, next)].path); + break; + } + case "ArrowUp": { + e.preventDefault(); + const prev = Math.max(idx - 1, 0); + setFocusedPath(visible[prev].path); + break; + } + case "ArrowRight": { + e.preventDefault(); + if (current && !current.expanded) { + toggleExpand(current.path); + } else if (current && current.expanded && current.children.length > 0) { + setFocusedPath(current.children[0].path); + } + break; + } + case "ArrowLeft": { + e.preventDefault(); + if (current && current.expanded) { + toggleExpand(current.path); + } else if (current) { + const parent = findParent(tree, current.path); + if (parent) setFocusedPath(parent.path); + } + break; + } + case "Enter": + case " ": { + e.preventDefault(); + if (current) handleFolderClick(current.path); + break; + } + case "Home": { + e.preventDefault(); + if (visible.length > 0) setFocusedPath(visible[0].path); + break; + } + case "End": { + e.preventDefault(); + if (visible.length > 0) setFocusedPath(visible[visible.length - 1].path); + break; + } + } + }, [tree, focusedPath, flattenVisible, findParent]); + + // scroll focused node into view + useEffect(() => { + if (!focusedPath || !scrollRef.current) return; + const el = scrollRef.current.querySelector(`[data-tree-path="${CSS.escape(focusedPath)}"]`); + if (el) { + (el as HTMLElement).focus(); + el.scrollIntoView({ block: "nearest", behavior: "smooth" }); + } + }, [focusedPath]); + + function renderNode(node: FolderNode, depth: number) { + const isActive = node.path === currentPath; + const isFocused = node.path === focusedPath; + return ( +
+ + + setFocusedPath(node.path)} + > + + + + + + handleFolderClick(node.path)} className="gap-2 text-[13px]"> + Open in Nomina + + invoke("reveal_in_explorer", { path: node.path }).catch(() => {})} className="gap-2 text-[13px]"> + Open in file explorer + + + navigator.clipboard.writeText(node.path)} className="gap-2 text-[13px]"> + Copy path + + + toggleExpand(node.path)} className="gap-2 text-[13px]"> + {node.expanded ? "Collapse" : "Expand"} + + + + + {node.expanded && ( + + {node.children.map((child) => renderNode(child, depth + 1))} + + )} + +
+ ); } return ( -
-
- + setPathInput(e.target.value)} - placeholder="Enter path..." - className="w-full px-2 py-1 text-xs rounded border" - style={{ - background: "var(--bg-primary)", - borderColor: "var(--border)", - color: "var(--text-primary)", - }} + placeholder="Path..." + aria-label="Folder path" + className="h-7 text-xs font-mono" /> -
- -
- {!currentPath && ( -
-
- Drives -
- {drives.map((d) => ( - - ))} -
- )} - {currentPath && ( -
- {currentPath.includes("\\") || currentPath.includes("/") ? ( - - ) : null} - {folders.map((f) => ( - - ))} -
+ + + + + + Go up + + )} -
-
+ + + +
+ {tree.length === 0 && ( + + + + + Loading drives... + + )} + {tree.map((node) => renderNode(node, 0))} +
+
+ ); } diff --git a/ui/src/components/layout/StatusBar.tsx b/ui/src/components/layout/StatusBar.tsx index 82ae916..30bc25d 100644 --- a/ui/src/components/layout/StatusBar.tsx +++ b/ui/src/components/layout/StatusBar.tsx @@ -1,35 +1,99 @@ -import { useFileStore } from "../../stores/fileStore"; +import { useFileStore } from "@/stores/fileStore"; +import { motion, AnimatePresence } from "framer-motion"; export function StatusBar() { const files = useFileStore((s) => s.files); const selectedFiles = useFileStore((s) => s.selectedFiles); const previewResults = useFileStore((s) => s.previewResults); const loading = useFileStore((s) => s.loading); + const currentPath = useFileStore((s) => s.currentPath); - const conflicts = previewResults.filter((r) => r.has_conflict).length; - const changes = previewResults.filter( + const selectedPreviews = previewResults.filter((r) => selectedFiles.has(r.original_path)); + const conflicts = selectedPreviews.filter((r) => r.has_conflict).length; + const changes = selectedPreviews.filter( (r) => r.original_name !== r.new_name && !r.has_error, ).length; - const status = loading ? "Scanning..." : changes > 0 ? "Preview ready" : "Ready"; - return ( -
- {files.length} files + {/* visually hidden summary for screen readers */} + + {currentPath ? `${currentPath}. ` : ""} + {files.length} files, {selectedFiles.size} selected + {changes > 0 ? `, ${changes} to rename` : ""} + {conflicts > 0 ? `, ${conflicts} conflicts` : ""}. + {loading ? " Scanning." : changes > 0 ? " Preview ready." : " Ready."} + + + {currentPath && ( + + {currentPath} + + )} + + + {files.length} files + {selectedFiles.size} selected - {changes > 0 && {changes} to rename} - {conflicts > 0 && ( - {conflicts} conflicts - )} + + {changes > 0 && ( + + {changes} to rename + + )} + + + {conflicts > 0 && ( + + {conflicts} conflicts + + )} +
- {status} -
+ + 0 ? "preview" : "ready"} + initial={{ opacity: 0, y: 5 }} + animate={{ opacity: 1, y: 0 }} + exit={{ opacity: 0, y: -5 }} + transition={{ duration: 0.15 }} + > + {loading ? "Scanning..." : changes > 0 ? "Preview ready" : "Ready"} + + + ); } diff --git a/ui/src/components/layout/Toolbar.tsx b/ui/src/components/layout/Toolbar.tsx index 1742710..b4cf4a4 100644 --- a/ui/src/components/layout/Toolbar.tsx +++ b/ui/src/components/layout/Toolbar.tsx @@ -1,34 +1,107 @@ +import { useState } from "react"; import { invoke } from "@tauri-apps/api/core"; -import { useFileStore } from "../../stores/fileStore"; -import { useRuleStore } from "../../stores/ruleStore"; +import { getCurrentWindow } from "@tauri-apps/api/window"; +import { useFileStore } from "@/stores/fileStore"; +import { useSettingsStore } from "@/stores/settingsStore"; +import { toast } from "sonner"; +import { Button } from "@/components/ui/button"; +import { Tooltip, TooltipContent, TooltipTrigger } from "@/components/ui/tooltip"; +import { + IconArrowBackUp, + IconLayoutSidebar, + IconSettings, + IconTemplate, + IconMinus, + IconSquare, + IconX, +} from "@tabler/icons-react"; +import { SettingsDialog } from "@/components/settings/SettingsDialog"; +import { PresetsDialog } from "@/components/presets/PresetsDialog"; +import { motion } from "framer-motion"; -export function Toolbar() { +interface ToolbarProps { + sidebarOpen: boolean; + onToggleSidebar: () => void; +} + +export function Toolbar({ sidebarOpen, onToggleSidebar }: ToolbarProps) { + const [settingsOpen, setSettingsOpen] = useState(false); + const [presetsOpen, setPresetsOpen] = useState(false); const previewResults = useFileStore((s) => s.previewResults); + const selectedFiles = useFileStore((s) => s.selectedFiles); const currentPath = useFileStore((s) => s.currentPath); const scanDirectory = useFileStore((s) => s.scanDirectory); - const requestPreview = useRuleStore((s) => s.requestPreview); - const resetAllRules = useRuleStore((s) => s.resetAllRules); + + const selectedPreviews = previewResults.filter( + (r) => selectedFiles.has(r.original_path) && !r.has_conflict && !r.has_error && r.original_name !== r.new_name, + ); + const renameCount = selectedPreviews.length; + + const appWindow = getCurrentWindow(); async function handleRename() { - const ops = previewResults.filter( - (r) => !r.has_conflict && !r.has_error && r.original_name !== r.new_name, - ); + const ops = selectedPreviews; if (ops.length === 0) return; + const settings = useSettingsStore.getState(); + if (settings.confirmBeforeRename) { + const ok = window.confirm(`Rename ${ops.length} file${ops.length !== 1 ? "s" : ""}?`); + if (!ok) return; + } + try { const report = await invoke<{ succeeded: number; failed: string[] }>( "execute_rename", - { operations: ops }, + { + operations: ops, + createBackup: settings.createBackups, + undoLimit: settings.undoHistoryLimit, + skipReadOnly: settings.skipReadOnly, + conflictStrategy: settings.conflictStrategy, + backupPath: settings.backupLocation === "custom" ? settings.customBackupPath : null, + }, ); - if (report.failed.length > 0) { - console.error("Some renames failed:", report.failed); - } - // refresh + if (currentPath) { await scanDirectory(currentPath); } + + // toast notification + if (settings.showToastOnComplete) { + if (report.failed.length > 0) { + toast.warning(`Renamed ${report.succeeded} file${report.succeeded !== 1 ? "s" : ""}`, { + description: `${report.failed.length} failed`, + }); + } else { + toast.success(`Renamed ${report.succeeded} file${report.succeeded !== 1 ? "s" : ""}`, { + description: "All operations completed", + }); + } + } + + // play sound + if (settings.playSoundOnComplete) { + try { + const ctx = new AudioContext(); + const osc = ctx.createOscillator(); + const gain = ctx.createGain(); + osc.connect(gain); + gain.connect(ctx.destination); + osc.frequency.value = 800; + osc.type = "sine"; + gain.gain.setValueAtTime(0.15, ctx.currentTime); + gain.gain.exponentialRampToValueAtTime(0.001, ctx.currentTime + 0.2); + osc.start(ctx.currentTime); + osc.stop(ctx.currentTime + 0.2); + } catch {} + } + + // flash taskbar + if (settings.flashTaskbarOnComplete && document.hidden) { + getCurrentWindow().requestUserAttention(2).catch(() => {}); + } } catch (e) { - console.error("Rename failed:", e); + toast.error("Rename failed", { description: String(e) }); } } @@ -43,56 +116,120 @@ export function Toolbar() { } } - const hasChanges = previewResults.some( - (r) => r.original_name !== r.new_name && !r.has_conflict && !r.has_error, - ); - return ( -
- - Nomina - - - + + + + + {sidebarOpen ? "Hide sidebar" : "Show sidebar"} + - +
- + + + -
+ + + + + Undo last rename + - -
+
+ + Nomina + +
+ + + + + + Presets + + + + + + + Settings + + +
+ + {/* window controls */} + + + + + + + + ); } diff --git a/ui/src/components/pipeline/PipelineStrip.tsx b/ui/src/components/pipeline/PipelineStrip.tsx new file mode 100644 index 0000000..b77a6d3 --- /dev/null +++ b/ui/src/components/pipeline/PipelineStrip.tsx @@ -0,0 +1,694 @@ +import { useCallback, useEffect, useRef, useState } from "react"; +import { OverlayScrollbarsComponent, type OverlayScrollbarsComponentRef } from "overlayscrollbars-react"; +import { useRuleStore, type PipelineRule } from "@/stores/ruleStore"; +import { useFileStore } from "@/stores/fileStore"; +import { useSettingsStore } from "@/stores/settingsStore"; +import { Button } from "@/components/ui/button"; +import { Popover, PopoverAnchor, PopoverContent, PopoverTrigger } from "@/components/ui/popover"; +import { Tooltip, TooltipContent, TooltipTrigger } from "@/components/ui/tooltip"; +import { + DropdownMenu, + DropdownMenuContent, + DropdownMenuItem, + DropdownMenuTrigger, + DropdownMenuSeparator, + DropdownMenuLabel, +} from "@/components/ui/dropdown-menu"; +import { + ContextMenu, + ContextMenuContent, + ContextMenuItem, + ContextMenuSeparator, + ContextMenuTrigger, +} from "@/components/ui/context-menu"; +import { Switch } from "@/components/ui/switch"; +import { + IconPlus, + IconReplace, + IconRegex, + IconEraser, + IconTextPlus, + IconLetterCase, + IconNumbers, + IconCalendar, + IconArrowsExchange, + IconFileTypography, + IconTrash, + IconX, + IconEdit, + IconHash, + IconFolder, + IconLanguage, + IconSortAscendingNumbers, + IconCut, + IconDice, + IconArrowsRightLeft, + IconShieldCheck, + IconCopy, + IconChevronLeft, + IconChevronRight, + IconPlayerPlay, + IconPlayerPause, +} from "@tabler/icons-react"; +import { cn } from "@/lib/utils"; +import { motion, AnimatePresence } from "framer-motion"; +import { + DndContext, + closestCenter, + PointerSensor, + KeyboardSensor, + useSensor, + useSensors, + type DragEndEvent, +} from "@dnd-kit/core"; +import { + SortableContext, + horizontalListSortingStrategy, + useSortable, + sortableKeyboardCoordinates, +} from "@dnd-kit/sortable"; +import { CSS } from "@dnd-kit/utilities"; +import { ReplaceConfig } from "./configs/ReplaceConfig"; +import { RegexConfig } from "./configs/RegexConfig"; +import { RemoveConfig } from "./configs/RemoveConfig"; +import { AddConfig } from "./configs/AddConfig"; +import { CaseConfig } from "./configs/CaseConfig"; +import { NumberingConfig } from "./configs/NumberingConfig"; +import { ExtensionConfig } from "./configs/ExtensionConfig"; +import { DateConfig } from "./configs/DateConfig"; +import { MovePartsConfig } from "./configs/MovePartsConfig"; +import { TextEditorConfig } from "./configs/TextEditorConfig"; +import { HashConfig } from "./configs/HashConfig"; +import { FolderNameConfig } from "./configs/FolderNameConfig"; +import { TransliterateConfig } from "./configs/TransliterateConfig"; +import { PaddingConfig } from "./configs/PaddingConfig"; +import { TruncateConfig } from "./configs/TruncateConfig"; +import { RandomizeConfig } from "./configs/RandomizeConfig"; +import { SwapConfig } from "./configs/SwapConfig"; +import { SanitizeConfig } from "./configs/SanitizeConfig"; + +const ruleTypes = [ + { id: "add", label: "Add", icon: IconTextPlus, desc: "Insert text" }, + { id: "case", label: "Case", icon: IconLetterCase, desc: "Change letter case" }, + { id: "date", label: "Date", icon: IconCalendar, desc: "Insert date/time info" }, + { id: "text_editor", label: "Editor", icon: IconEdit, desc: "Edit names as text" }, + { id: "extension", label: "Extension", icon: IconFileTypography, desc: "Change file extension" }, + { id: "folder_name", label: "Folder", icon: IconFolder, desc: "Insert parent folder name" }, + { id: "hash", label: "Hash", icon: IconHash, desc: "Add file content hash" }, + { id: "move_parts", label: "Move", icon: IconArrowsExchange, desc: "Move or swap parts" }, + { id: "numbering", label: "Number", icon: IconNumbers, desc: "Add sequence numbers" }, + { id: "padding", label: "Padding", icon: IconSortAscendingNumbers, desc: "Pad numbers in names" }, + { id: "randomize", label: "Random", icon: IconDice, desc: "Add random characters" }, + { id: "regex", label: "Regex", icon: IconRegex, desc: "Pattern matching" }, + { id: "remove", label: "Remove", icon: IconEraser, desc: "Strip characters" }, + { id: "replace", label: "Replace", icon: IconReplace, desc: "Find and replace text" }, + { id: "sanitize", label: "Sanitize", icon: IconShieldCheck, desc: "Clean up filenames" }, + { id: "swap", label: "Swap", icon: IconArrowsRightLeft, desc: "Swap parts around delimiter" }, + { id: "transliterate", label: "Translit", icon: IconLanguage, desc: "Non-ASCII to ASCII" }, + { id: "truncate", label: "Truncate", icon: IconCut, desc: "Limit filename length" }, +] as const; + +const ruleIconMap: Record = { + replace: IconReplace, + regex: IconRegex, + remove: IconEraser, + add: IconTextPlus, + case: IconLetterCase, + numbering: IconNumbers, + date: IconCalendar, + move_parts: IconArrowsExchange, + extension: IconFileTypography, + text_editor: IconEdit, + hash: IconHash, + folder_name: IconFolder, + transliterate: IconLanguage, + padding: IconSortAscendingNumbers, + truncate: IconCut, + randomize: IconDice, + swap: IconArrowsRightLeft, + sanitize: IconShieldCheck, +}; + +function getRuleSummary(rule: Record): string { + const type = rule.type as string; + switch (type) { + case "replace": { + const s = rule.search as string; + const r = rule.replace_with as string; + if (!s) return "Not configured"; + return `"${s}" -> "${r}"`; + } + case "regex": { + const p = rule.pattern as string; + if (!p) return "Not configured"; + return `/${p}/`; + } + case "remove": { + const parts: string[] = []; + if ((rule.first_n as number) > 0) parts.push(`First ${rule.first_n}`); + if ((rule.last_n as number) > 0) parts.push(`Last ${rule.last_n}`); + if (rule.crop_before) parts.push("Crop"); + if (rule.remove_pattern) parts.push("Pattern"); + const trim = rule.trim as Record | undefined; + if (trim && Object.values(trim).some(Boolean)) parts.push("Trim"); + return parts.length > 0 ? parts.join(", ") : "Not configured"; + } + case "add": { + const parts: string[] = []; + if (rule.prefix) parts.push(`"${rule.prefix}"+`); + if (rule.suffix) parts.push(`+"${rule.suffix}"`); + return parts.length > 0 ? parts.join(" ") : "Not configured"; + } + case "case": + return (rule.mode as string) === "Same" ? "Not configured" : (rule.mode as string); + case "numbering": + return (rule.mode as string) === "None" ? "Not configured" : `${rule.mode} from ${rule.start}`; + case "extension": + return (rule.mode as string) === "Same" ? "Not configured" : (rule.mode as string); + case "date": + return (rule.mode as string) === "None" ? "Not configured" : `${rule.source} ${rule.mode}`; + case "move_parts": { + const len = rule.source_length as number; + if (!len) return "Not configured"; + return `${rule.copy_mode ? "Copy" : "Move"} ${len} chars`; + } + case "text_editor": { + const names = rule.names as string[]; + if (!names?.length) return "Not configured"; + return `${names.length} name${names.length !== 1 ? "s" : ""}`; + } + case "hash": + return (rule.mode as string) === "None" ? "Not configured" : `${rule.algorithm} ${rule.mode}`; + case "folder_name": + return (rule.mode as string) === "None" ? "Not configured" : `Level ${rule.level} ${rule.mode}`; + case "transliterate": + return "ASCII conversion"; + case "padding": + return `Width ${rule.width}, pad '${rule.pad_char}'`; + case "truncate": + return `Max ${rule.max_length} chars`; + case "randomize": + return `${rule.format} ${rule.mode}`; + case "swap": + return (rule.delimiter as string) ? `Split on "${rule.delimiter}"` : "Not configured"; + case "sanitize": { + const parts: string[] = []; + if (rule.illegal_chars) parts.push("Illegal"); + if ((rule.spaces_to as string) !== "None") parts.push("Spaces"); + if (rule.strip_diacritics) parts.push("Accents"); + if (rule.normalize_unicode) parts.push("Unicode"); + if (rule.strip_zero_width) parts.push("ZW"); + if (rule.collapse_whitespace) parts.push("Collapse"); + if (rule.trim_dots_spaces) parts.push("Trim"); + return parts.length > 0 ? parts.join(", ") : "Not configured"; + } + default: + return "..."; + } +} + +function isRuleActive(rule: Record): boolean { + const type = rule.type as string; + if (!rule.enabled) return false; + switch (type) { + case "replace": return !!(rule.search as string); + case "regex": return !!(rule.pattern as string); + case "remove": { + const trim = rule.trim as Record | undefined; + const hasTrim = trim && Object.values(trim).some(Boolean); + return (rule.first_n as number) > 0 || (rule.last_n as number) > 0 || (rule.from as number) !== (rule.to as number) || !!(rule.collapse_chars) || !!(rule.remove_pattern) || !!(rule.crop_before) || !!(rule.crop_after) || !!hasTrim; + } + case "add": return !!(rule.prefix as string) || !!(rule.suffix as string) || !!(rule.insert as string) || !!(rule.inserts as unknown[])?.length; + case "case": return (rule.mode as string) !== "Same"; + case "numbering": return (rule.mode as string) !== "None"; + case "extension": return (rule.mode as string) !== "Same" || !!(rule.mapping as unknown[])?.length; + case "date": return (rule.mode as string) !== "None"; + case "move_parts": return (rule.source_length as number) > 0; + case "text_editor": return !!(rule.names as string[])?.length; + case "hash": return (rule.mode as string) !== "None"; + case "folder_name": return (rule.mode as string) !== "None"; + case "transliterate": return true; + case "padding": return true; + case "truncate": return true; + case "randomize": return true; + case "swap": return !!(rule.delimiter as string); + case "sanitize": return !!(rule.illegal_chars) || (rule.spaces_to as string) !== "None" || !!(rule.strip_diacritics) || !!(rule.normalize_unicode) || !!(rule.strip_zero_width) || !!(rule.collapse_whitespace) || !!(rule.trim_dots_spaces); + default: return false; + } +} + +function ConfigContent({ ruleId }: { ruleId: string }) { + const rule = useRuleStore((s) => s.pipeline.find((r) => r.id === ruleId)); + if (!rule) return null; + switch (rule.config.type) { + case "replace": return ; + case "regex": return ; + case "remove": return ; + case "add": return ; + case "case": return ; + case "numbering": return ; + case "extension": return ; + case "date": return ; + case "move_parts": return ; + case "text_editor": return ; + case "hash": return ; + case "folder_name": return ; + case "transliterate": return ; + case "padding": return ; + case "truncate": return ; + case "randomize": return ; + case "swap": return ; + case "sanitize": return ; + default: return null; + } +} + +function PopoverArrowWithBorder() { + return ( +
+ + + + + + +
+ ); +} + +function PipelineCard({ rule, index, onScrollToCenter }: { rule: PipelineRule; index: number; onScrollToCenter: (el: HTMLElement) => void }) { + const [open, setOpen] = useState(false); + const [tooltipsReady, setTooltipsReady] = useState(false); + const updateRule = useRuleStore((s) => s.updateRule); + const removeRule = useRuleStore((s) => s.removeRule); + const resetRule = useRuleStore((s) => s.resetRule); + const duplicateRule = useRuleStore((s) => s.duplicateRule); + const reorderPipeline = useRuleStore((s) => s.reorderPipeline); + const pipeline = useRuleStore((s) => s.pipeline); + const cardRef = useRef(null); + + const { + attributes, + listeners, + setNodeRef: setSortableRef, + transform, + transition, + isDragging, + } = useSortable({ id: rule.id }); + + const setNodeRef = useCallback((node: HTMLElement | null) => { + setSortableRef(node); + (cardRef as React.MutableRefObject).current = node as HTMLDivElement | null; + }, [setSortableRef]); + + const style = { + transform: CSS.Transform.toString(transform), + transition, + zIndex: isDragging ? 10 : undefined, + opacity: isDragging ? 0.5 : undefined, + }; + + useEffect(() => { + if (open) { + setTooltipsReady(false); + const timer = setTimeout(() => setTooltipsReady(true), 700); + if (cardRef.current) onScrollToCenter(cardRef.current); + return () => clearTimeout(timer); + } else { + setTooltipsReady(false); + } + }, [open, onScrollToCenter]); + + const Icon = ruleIconMap[rule.config.type] || IconReplace; + const active = isRuleActive(rule.config as unknown as Record); + const summary = getRuleSummary(rule.config as unknown as Record); + const typeInfo = ruleTypes.find((r) => r.id === rule.config.type); + const pipelineIndex = pipeline.findIndex((r) => r.id === rule.id); + const canMoveLeft = pipelineIndex > 0; + const canMoveRight = pipelineIndex < pipeline.length - 1; + const stateLabel = !rule.config.enabled ? "disabled" : active ? "active" : "inactive"; + return ( +
+ + +
+ + +
+ + + {/* step number - bottom right */} +
+ {index + 1} +
+ +
+ + {/* non-color active indicator - small filled dot */} + {active && rule.config.enabled && ( +
+ +
+ + {typeInfo?.label || rule.config.type} + + + {summary} + +
+ + updateRule(rule.id, { enabled: checked })} + className="mt-1 scale-75 origin-top-right" + onClick={(e) => e.stopPropagation()} + aria-label={`${rule.config.enabled ? "Disable" : "Enable"} ${typeInfo?.label || rule.config.type} rule`} + /> +
+
+
+
+ { + // focus first interactive element inside the config panel + e.preventDefault(); + requestAnimationFrame(() => { + const el = cardRef.current?.closest("[data-slot='popover']")?.querySelector( + ".p-4 input, .p-4 select, .p-4 button, .p-4 [tabindex='0']" + ); + el?.focus(); + }); + }} + className={cn( + rule.config.type === "text_editor" ? "w-[560px]" : "w-[440px]", + "p-0 shadow-xl shadow-black/10 dark:shadow-black/30 !overflow-visible relative", + )} + > +
+
+
+ +
+ + {typeInfo?.label || rule.config.type} + + + Step {index + 1} + +
+
+ {tooltipsReady ? ( + + + + + Reset to defaults + + ) : ( + + )} + {tooltipsReady ? ( + + + + + Remove step + + ) : ( + + )} + {tooltipsReady ? ( + + + + + Close + + ) : ( + + )} +
+
+ + + + +
+
+
+
+ + updateRule(rule.id, { enabled: !rule.config.enabled })} + className="gap-2 text-[13px]" + > + {rule.config.enabled + ? <> Disable + : <> Enable + } + + + resetRule(rule.id)} className="gap-2 text-[13px]"> + Reset to defaults + + duplicateRule(rule.id)} className="gap-2 text-[13px]"> + Duplicate + + + canMoveLeft && reorderPipeline(pipelineIndex, pipelineIndex - 1)} + disabled={!canMoveLeft} + className="gap-2 text-[13px]" + > + Move left + + canMoveRight && reorderPipeline(pipelineIndex, pipelineIndex + 1)} + disabled={!canMoveRight} + className="gap-2 text-[13px]" + > + Move right + + <> + + removeRule(rule.id)} + className="gap-2 text-[13px] text-destructive focus:text-destructive" + > + Remove + + + +
+
+ ); +} + +export function PipelineStrip() { + const pipeline = useRuleStore((s) => s.pipeline); + const addRule = useRuleStore((s) => s.addRule); + const reorderPipeline = useRuleStore((s) => s.reorderPipeline); + const requestPreview = useRuleStore((s) => s.requestPreview); + const currentPath = useFileStore((s) => s.currentPath); + const sortedFilePaths = useFileStore((s) => s.sortedFilePaths); + const showDisabledRules = useSettingsStore((s) => s.showDisabledRules); + + const visiblePipeline = showDisabledRules === "hidden" + ? pipeline.filter((r) => r.config.enabled) + : pipeline; + + const osRef = useRef(null); + + const scrollToCenter = useCallback((el: HTMLElement) => { + const instance = osRef.current?.osInstance(); + if (!instance) return; + const viewport = instance.elements().viewport; + const viewportRect = viewport.getBoundingClientRect(); + const elRect = el.getBoundingClientRect(); + const elCenter = elRect.left + elRect.width / 2; + const viewportCenter = viewportRect.left + viewportRect.width / 2; + const scrollLeft = viewport.scrollLeft + (elCenter - viewportCenter); + viewport.scrollTo({ left: scrollLeft, behavior: "smooth" }); + }, []); + + const sensors = useSensors( + useSensor(PointerSensor, { + activationConstraint: { distance: 8 }, + }), + useSensor(KeyboardSensor, { + coordinateGetter: sortableKeyboardCoordinates, + }), + ); + + function handleDragEnd(event: DragEndEvent) { + const { active, over } = event; + if (over && active.id !== over.id) { + const oldIndex = pipeline.findIndex((r) => r.id === active.id); + const newIndex = pipeline.findIndex((r) => r.id === over.id); + if (oldIndex !== -1 && newIndex !== -1) { + reorderPipeline(oldIndex, newIndex); + } + } + } + + useEffect(() => { + if (currentPath) { + requestPreview(currentPath); + } + }, [pipeline, currentPath, sortedFilePaths, requestPreview]); + + const isEmpty = visiblePipeline.length === 0; + + return ( +
+ +
+ + r.id)} + strategy={horizontalListSortingStrategy} + > + + {visiblePipeline.map((rule, index) => ( + + ))} + + + + +
+ + + + + + Add a rule to the pipeline + +
+ {ruleTypes.map((rt) => ( + addRule(rt.id)} + > +
+ +
+
+ {rt.label} + {rt.desc} +
+
+ ))} +
+
+
+
+
+
+
+ ); +} diff --git a/ui/src/components/pipeline/configs/AddConfig.tsx b/ui/src/components/pipeline/configs/AddConfig.tsx new file mode 100644 index 0000000..86c20d0 --- /dev/null +++ b/ui/src/components/pipeline/configs/AddConfig.tsx @@ -0,0 +1,95 @@ +import { useRuleStore } from "@/stores/ruleStore"; +import { Input } from "@/components/ui/input"; +import { NumberInput } from "@/components/ui/number-input"; +import { Checkbox } from "@/components/ui/checkbox"; +import { IconPlus, IconTrash } from "@tabler/icons-react"; +import type { AddConfig as AddConfigType } from "@/types/rules"; + +export function AddConfig({ ruleId }: { ruleId: string }) { + const rule = useRuleStore((s) => s.pipeline.find((r) => r.id === ruleId))?.config as AddConfigType | undefined; + const updateRule = useRuleStore((s) => s.updateRule); + if (!rule) return null; + const update = (changes: Partial) => updateRule(ruleId, changes); + + const addInsert = () => { + update({ inserts: [...rule.inserts, { position: 0, text: "" }] }); + }; + + const removeInsert = (idx: number) => { + update({ inserts: rule.inserts.filter((_, i) => i !== idx) }); + }; + + const updateInsert = (idx: number, changes: Partial<{ position: number; text: string }>) => { + update({ + inserts: rule.inserts.map((ins, i) => (i === idx ? { ...ins, ...changes } : ins)), + }); + }; + + return ( +
+
+ + +
+
+ + +
+ {rule.inserts.length > 0 && ( +
+ Extra inserts + {rule.inserts.map((ins, idx) => ( +
+ updateInsert(idx, { text: e.target.value })} + placeholder="Text..." + className="h-7 text-xs font-mono flex-1" + /> + updateInsert(idx, { position: v })} min={0} /> + +
+ ))} +
+ )} +
+ + +
+
+ ); +} diff --git a/ui/src/components/pipeline/configs/CaseConfig.tsx b/ui/src/components/pipeline/configs/CaseConfig.tsx new file mode 100644 index 0000000..a648a6d --- /dev/null +++ b/ui/src/components/pipeline/configs/CaseConfig.tsx @@ -0,0 +1,65 @@ +import { useRuleStore } from "@/stores/ruleStore"; +import { Input } from "@/components/ui/input"; +import { SegmentedControl } from "@/components/ui/segmented-control"; +import type { CaseConfig as CaseConfigType } from "@/types/rules"; + +const basicModes = [ + { value: "Same", label: "Same" }, + { value: "Upper", label: "UPPER" }, + { value: "Lower", label: "lower" }, + { value: "Title", label: "Title" }, + { value: "Sentence", label: "Sent." }, + { value: "SmartTitle", label: "Smart" }, + { value: "Invert", label: "iNVERT" }, + { value: "Random", label: "rAnD" }, +] as const; + +const devModes = [ + { value: "CamelCase", label: "camelCase" }, + { value: "PascalCase", label: "Pascal" }, + { value: "SnakeCase", label: "snake_case" }, + { value: "KebabCase", label: "kebab-case" }, + { value: "DotCase", label: "dot.case" }, +] as const; + +export function CaseConfig({ ruleId }: { ruleId: string }) { + const rule = useRuleStore((s) => s.pipeline.find((r) => r.id === ruleId))?.config as CaseConfigType | undefined; + const updateRule = useRuleStore((s) => s.updateRule); + if (!rule) return null; + const update = (changes: Partial) => updateRule(ruleId, changes); + + const devValues = ["CamelCase", "PascalCase", "SnakeCase", "KebabCase", "DotCase"]; + const isDevMode = devValues.includes(rule.mode); + + return ( +
+
+ Text case + update({ mode: m })} + options={basicModes} + size="sm" + /> +
+
+ Developer case + update({ mode: m })} + options={devModes} + size="sm" + /> +
+ +
+ ); +} diff --git a/ui/src/components/pipeline/configs/DateConfig.tsx b/ui/src/components/pipeline/configs/DateConfig.tsx new file mode 100644 index 0000000..ffb77cf --- /dev/null +++ b/ui/src/components/pipeline/configs/DateConfig.tsx @@ -0,0 +1,86 @@ +import { useRuleStore } from "@/stores/ruleStore"; +import { Input } from "@/components/ui/input"; +import { Checkbox } from "@/components/ui/checkbox"; +import { SegmentedControl } from "@/components/ui/segmented-control"; +import type { DateConfig as DateConfigType } from "@/types/rules"; + +const dateModes = [ + { value: "None", label: "None" }, + { value: "Prefix", label: "Prefix" }, + { value: "Suffix", label: "Suffix" }, + { value: "Insert", label: "Insert" }, +] as const; + +const dateSources = [ + { value: "Created", label: "Created" }, + { value: "Modified", label: "Modified" }, + { value: "Accessed", label: "Accessed" }, + { value: "ExifTaken", label: "EXIF" }, + { value: "Current", label: "Now" }, +] as const; + +const dateFormats = [ + { value: "YMD", label: "Y-M-D" }, + { value: "DMY", label: "D-M-Y" }, + { value: "MDY", label: "M-D-Y" }, + { value: "YM", label: "Y-M" }, + { value: "MY", label: "M-Y" }, + { value: "Y", label: "Year" }, +] as const; + +export function DateConfig({ ruleId }: { ruleId: string }) { + const rule = useRuleStore((s) => s.pipeline.find((r) => r.id === ruleId))?.config as DateConfigType | undefined; + const updateRule = useRuleStore((s) => s.updateRule); + if (!rule) return null; + const update = (changes: Partial) => updateRule(ruleId, changes); + + return ( +
+
+ Position + update({ mode: m })} options={dateModes} /> +
+
+ Source + update({ source: s })} options={dateSources} size="sm" /> +
+
+ Format + update({ format: f })} options={dateFormats} size="sm" /> +
+
+ + +
+ + +
+ ); +} diff --git a/ui/src/components/pipeline/configs/ExtensionConfig.tsx b/ui/src/components/pipeline/configs/ExtensionConfig.tsx new file mode 100644 index 0000000..192056f --- /dev/null +++ b/ui/src/components/pipeline/configs/ExtensionConfig.tsx @@ -0,0 +1,98 @@ +import { useRuleStore } from "@/stores/ruleStore"; +import { Input } from "@/components/ui/input"; +import { SegmentedControl } from "@/components/ui/segmented-control"; +import { Checkbox } from "@/components/ui/checkbox"; +import { IconPlus, IconTrash } from "@tabler/icons-react"; +import type { ExtensionConfig as ExtensionConfigType } from "@/types/rules"; + +const extModes = [ + { value: "Same", label: "Same" }, + { value: "Lower", label: "lower" }, + { value: "Upper", label: "UPPER" }, + { value: "Title", label: "Title" }, + { value: "Extra", label: "Extra" }, + { value: "Remove", label: "Remove" }, + { value: "Fixed", label: "Fixed" }, +] as const; + +export function ExtensionConfig({ ruleId }: { ruleId: string }) { + const rule = useRuleStore((s) => s.pipeline.find((r) => r.id === ruleId))?.config as ExtensionConfigType | undefined; + const updateRule = useRuleStore((s) => s.updateRule); + if (!rule) return null; + const update = (changes: Partial) => updateRule(ruleId, changes); + + const mappings = rule.mapping || []; + + const addMapping = () => { + update({ mapping: [...mappings, ["", ""]] }); + }; + + const removeMapping = (idx: number) => { + const next = mappings.filter((_, i) => i !== idx); + update({ mapping: next.length > 0 ? next : null }); + }; + + const updateMapping = (idx: number, pos: 0 | 1, val: string) => { + const next = mappings.map((m, i) => { + if (i !== idx) return m; + const copy: [string, string] = [...m]; + copy[pos] = val; + return copy; + }); + update({ mapping: next }); + }; + + return ( +
+
+ Mode + update({ mode: m })} options={extModes} size="sm" /> +
+ {rule.mode === "Fixed" && ( + + )} + {mappings.length > 0 && ( +
+ Extension mappings + {mappings.map((m, idx) => ( +
+ updateMapping(idx, 0, e.target.value)} + placeholder="From..." + className="h-7 text-xs font-mono flex-1" + /> + {"->"} + updateMapping(idx, 1, e.target.value)} + placeholder="To..." + className="h-7 text-xs font-mono flex-1" + /> + +
+ ))} +
+ )} +
+ + +
+
+ ); +} diff --git a/ui/src/components/pipeline/configs/FolderNameConfig.tsx b/ui/src/components/pipeline/configs/FolderNameConfig.tsx new file mode 100644 index 0000000..38913d5 --- /dev/null +++ b/ui/src/components/pipeline/configs/FolderNameConfig.tsx @@ -0,0 +1,38 @@ +import { useRuleStore } from "@/stores/ruleStore"; +import { Input } from "@/components/ui/input"; +import { NumberInput } from "@/components/ui/number-input"; +import { SegmentedControl } from "@/components/ui/segmented-control"; +import type { FolderNameConfig as FolderNameConfigType } from "@/types/rules"; + +const folderModes = [ + { value: "None", label: "None" }, + { value: "Prefix", label: "Prefix" }, + { value: "Suffix", label: "Suffix" }, + { value: "Replace", label: "Replace" }, +] as const; + +export function FolderNameConfig({ ruleId }: { ruleId: string }) { + const rule = useRuleStore((s) => s.pipeline.find((r) => r.id === ruleId))?.config as FolderNameConfigType | undefined; + const updateRule = useRuleStore((s) => s.updateRule); + if (!rule) return null; + const update = (changes: Partial) => updateRule(ruleId, changes); + + return ( +
+
+ Position + update({ mode: m })} options={folderModes} /> +
+
+ + +
+
+ ); +} diff --git a/ui/src/components/pipeline/configs/HashConfig.tsx b/ui/src/components/pipeline/configs/HashConfig.tsx new file mode 100644 index 0000000..354c8c2 --- /dev/null +++ b/ui/src/components/pipeline/configs/HashConfig.tsx @@ -0,0 +1,53 @@ +import { useRuleStore } from "@/stores/ruleStore"; +import { Input } from "@/components/ui/input"; +import { NumberInput } from "@/components/ui/number-input"; +import { Checkbox } from "@/components/ui/checkbox"; +import { SegmentedControl } from "@/components/ui/segmented-control"; +import type { HashConfig as HashConfigType } from "@/types/rules"; + +const hashModes = [ + { value: "None", label: "None" }, + { value: "Prefix", label: "Prefix" }, + { value: "Suffix", label: "Suffix" }, + { value: "Replace", label: "Replace" }, +] as const; + +const algorithms = [ + { value: "MD5", label: "MD5" }, + { value: "SHA1", label: "SHA1" }, + { value: "SHA256", label: "SHA256" }, +] as const; + +export function HashConfig({ ruleId }: { ruleId: string }) { + const rule = useRuleStore((s) => s.pipeline.find((r) => r.id === ruleId))?.config as HashConfigType | undefined; + const updateRule = useRuleStore((s) => s.updateRule); + if (!rule) return null; + const update = (changes: Partial) => updateRule(ruleId, changes); + + return ( +
+
+ Position + update({ mode: m })} options={hashModes} /> +
+
+ Algorithm + update({ algorithm: a })} options={algorithms} /> +
+
+ + +
+ +
+ ); +} diff --git a/ui/src/components/pipeline/configs/MovePartsConfig.tsx b/ui/src/components/pipeline/configs/MovePartsConfig.tsx new file mode 100644 index 0000000..8595fcd --- /dev/null +++ b/ui/src/components/pipeline/configs/MovePartsConfig.tsx @@ -0,0 +1,83 @@ +import { useRuleStore } from "@/stores/ruleStore"; +import { Input } from "@/components/ui/input"; +import { NumberInput } from "@/components/ui/number-input"; +import { Checkbox } from "@/components/ui/checkbox"; +import { SegmentedControl } from "@/components/ui/segmented-control"; +import type { MovePartsConfig as MovePartsConfigType } from "@/types/rules"; + +const selectionModes = [ + { value: "Chars", label: "Chars" }, + { value: "Words", label: "Words" }, + { value: "Regex", label: "Regex" }, +] as const; + +const targetModes = [ + { value: "None", label: "None" }, + { value: "Start", label: "Start" }, + { value: "End", label: "End" }, +] as const; + +export function MovePartsConfig({ ruleId }: { ruleId: string }) { + const rule = useRuleStore((s) => s.pipeline.find((r) => r.id === ruleId))?.config as MovePartsConfigType | undefined; + const updateRule = useRuleStore((s) => s.updateRule); + if (!rule) return null; + const update = (changes: Partial) => updateRule(ruleId, changes); + + const targetValue = typeof rule.target === "string" ? rule.target : "Position"; + + return ( +
+
+ Selection mode + update({ selection_mode: m })} options={selectionModes} /> +
+ {rule.selection_mode === "Regex" ? ( +
+ + +
+ ) : ( +
+ + +
+ )} +
+ Target + update({ target: t as "None" | "Start" | "End" })} options={targetModes} /> +
+ +
+ +
+
+ ); +} diff --git a/ui/src/components/pipeline/configs/NumberingConfig.tsx b/ui/src/components/pipeline/configs/NumberingConfig.tsx new file mode 100644 index 0000000..9aaf892 --- /dev/null +++ b/ui/src/components/pipeline/configs/NumberingConfig.tsx @@ -0,0 +1,80 @@ +import { useRuleStore } from "@/stores/ruleStore"; +import { Input } from "@/components/ui/input"; +import { NumberInput } from "@/components/ui/number-input"; +import { SegmentedControl } from "@/components/ui/segmented-control"; +import { Checkbox } from "@/components/ui/checkbox"; +import type { NumberingConfig as NumberingConfigType } from "@/types/rules"; + +const numberModes = [ + { value: "None", label: "None" }, + { value: "Prefix", label: "Prefix" }, + { value: "Suffix", label: "Suffix" }, + { value: "Both", label: "Both" }, + { value: "Insert", label: "Insert" }, +] as const; + +const bases = [ + { value: "Decimal", label: "Dec" }, + { value: "Hex", label: "Hex" }, + { value: "Octal", label: "Oct" }, + { value: "Binary", label: "Bin" }, + { value: "Alpha", label: "Alpha" }, + { value: "Roman", label: "Roman" }, +] as const; + +export function NumberingConfig({ ruleId }: { ruleId: string }) { + const rule = useRuleStore((s) => s.pipeline.find((r) => r.id === ruleId))?.config as NumberingConfigType | undefined; + const updateRule = useRuleStore((s) => s.updateRule); + if (!rule) return null; + const update = (changes: Partial) => updateRule(ruleId, changes); + + return ( +
+
+ Position + update({ mode: m })} options={numberModes} /> +
+
+ + + + +
+
+ Base + update({ base: b })} options={bases} size="sm" /> +
+ +
+ + +
+
+ ); +} diff --git a/ui/src/components/pipeline/configs/PaddingConfig.tsx b/ui/src/components/pipeline/configs/PaddingConfig.tsx new file mode 100644 index 0000000..62f53de --- /dev/null +++ b/ui/src/components/pipeline/configs/PaddingConfig.tsx @@ -0,0 +1,36 @@ +import { useRuleStore } from "@/stores/ruleStore"; +import { Input } from "@/components/ui/input"; +import { NumberInput } from "@/components/ui/number-input"; +import { Checkbox } from "@/components/ui/checkbox"; +import type { PaddingConfig as PaddingConfigType } from "@/types/rules"; + +export function PaddingConfig({ ruleId }: { ruleId: string }) { + const rule = useRuleStore((s) => s.pipeline.find((r) => r.id === ruleId))?.config as PaddingConfigType | undefined; + const updateRule = useRuleStore((s) => s.updateRule); + if (!rule) return null; + const update = (changes: Partial) => updateRule(ruleId, changes); + + return ( +
+
+ + +
+ +
+ ); +} diff --git a/ui/src/components/pipeline/configs/RandomizeConfig.tsx b/ui/src/components/pipeline/configs/RandomizeConfig.tsx new file mode 100644 index 0000000..79137b1 --- /dev/null +++ b/ui/src/components/pipeline/configs/RandomizeConfig.tsx @@ -0,0 +1,52 @@ +import { useRuleStore } from "@/stores/ruleStore"; +import { Input } from "@/components/ui/input"; +import { NumberInput } from "@/components/ui/number-input"; +import { SegmentedControl } from "@/components/ui/segmented-control"; +import type { RandomizeConfig as RandomizeConfigType } from "@/types/rules"; + +const randomModes = [ + { value: "Replace", label: "Replace" }, + { value: "Prefix", label: "Prefix" }, + { value: "Suffix", label: "Suffix" }, +] as const; + +const randomFormats = [ + { value: "Hex", label: "Hex" }, + { value: "Alpha", label: "Alpha" }, + { value: "AlphaNum", label: "AlphaNum" }, + { value: "UUID", label: "UUID" }, +] as const; + +export function RandomizeConfig({ ruleId }: { ruleId: string }) { + const rule = useRuleStore((s) => s.pipeline.find((r) => r.id === ruleId))?.config as RandomizeConfigType | undefined; + const updateRule = useRuleStore((s) => s.updateRule); + if (!rule) return null; + const update = (changes: Partial) => updateRule(ruleId, changes); + + return ( +
+
+ Position + update({ mode: m })} options={randomModes} /> +
+
+ Format + update({ format: f })} options={randomFormats} /> +
+
+ {rule.format !== "UUID" && ( + + )} + {rule.mode !== "Replace" && ( + + )} +
+
+ ); +} diff --git a/ui/src/components/pipeline/configs/RegexConfig.tsx b/ui/src/components/pipeline/configs/RegexConfig.tsx new file mode 100644 index 0000000..446194f --- /dev/null +++ b/ui/src/components/pipeline/configs/RegexConfig.tsx @@ -0,0 +1,47 @@ +import { useRuleStore } from "@/stores/ruleStore"; +import { Input } from "@/components/ui/input"; +import { NumberInput } from "@/components/ui/number-input"; +import { Checkbox } from "@/components/ui/checkbox"; +import type { RegexConfig as RegexConfigType } from "@/types/rules"; + +export function RegexConfig({ ruleId }: { ruleId: string }) { + const rule = useRuleStore((s) => s.pipeline.find((r) => r.id === ruleId))?.config as RegexConfigType | undefined; + const updateRule = useRuleStore((s) => s.updateRule); + if (!rule) return null; + const update = (changes: Partial) => updateRule(ruleId, changes); + + return ( +
+
+ + +
+
+ + +
+
+ ); +} diff --git a/ui/src/components/pipeline/configs/RemoveConfig.tsx b/ui/src/components/pipeline/configs/RemoveConfig.tsx new file mode 100644 index 0000000..b5393ea --- /dev/null +++ b/ui/src/components/pipeline/configs/RemoveConfig.tsx @@ -0,0 +1,113 @@ +import { useRuleStore } from "@/stores/ruleStore"; +import { Input } from "@/components/ui/input"; +import { NumberInput } from "@/components/ui/number-input"; +import { Checkbox } from "@/components/ui/checkbox"; +import { SegmentedControl } from "@/components/ui/segmented-control"; +import type { RemoveConfig as RemoveConfigType } from "@/types/rules"; + +const removeModes = [ + { value: "Chars", label: "Chars" }, + { value: "Words", label: "Words" }, +] as const; + +export function RemoveConfig({ ruleId }: { ruleId: string }) { + const rule = useRuleStore((s) => s.pipeline.find((r) => r.id === ruleId))?.config as RemoveConfigType | undefined; + const updateRule = useRuleStore((s) => s.updateRule); + if (!rule) return null; + const update = (changes: Partial) => updateRule(ruleId, changes); + + const label = rule.mode === "Words" ? "words" : "chars"; + + return ( +
+
+ Mode + update({ mode: m })} options={removeModes} /> +
+
+ + + + +
+
+ + +
+
+ + + + + +
+
+ + +
+ +
+ ); +} diff --git a/ui/src/components/pipeline/configs/ReplaceConfig.tsx b/ui/src/components/pipeline/configs/ReplaceConfig.tsx new file mode 100644 index 0000000..431ebc9 --- /dev/null +++ b/ui/src/components/pipeline/configs/ReplaceConfig.tsx @@ -0,0 +1,65 @@ +import { useRuleStore } from "@/stores/ruleStore"; +import { Input } from "@/components/ui/input"; +import { NumberInput } from "@/components/ui/number-input"; +import { Checkbox } from "@/components/ui/checkbox"; +import type { ReplaceConfig as ReplaceConfigType } from "@/types/rules"; + +export function ReplaceConfig({ ruleId }: { ruleId: string }) { + const rule = useRuleStore((s) => s.pipeline.find((r) => r.id === ruleId))?.config as ReplaceConfigType | undefined; + const updateRule = useRuleStore((s) => s.updateRule); + if (!rule) return null; + const update = (changes: Partial) => updateRule(ruleId, changes); + + return ( +
+
+ + +
+
+ + + +
+
+ + + +
+
+ ); +} diff --git a/ui/src/components/pipeline/configs/SanitizeConfig.tsx b/ui/src/components/pipeline/configs/SanitizeConfig.tsx new file mode 100644 index 0000000..51d23ad --- /dev/null +++ b/ui/src/components/pipeline/configs/SanitizeConfig.tsx @@ -0,0 +1,53 @@ +import { useRuleStore } from "@/stores/ruleStore"; +import { Checkbox } from "@/components/ui/checkbox"; +import { SegmentedControl } from "@/components/ui/segmented-control"; +import type { SanitizeConfig as SanitizeConfigType } from "@/types/rules"; + +const spaceModes = [ + { value: "None", label: "Keep" }, + { value: "Underscores", label: "_" }, + { value: "Dashes", label: "-" }, + { value: "Dots", label: "." }, +] as const; + +export function SanitizeConfig({ ruleId }: { ruleId: string }) { + const rule = useRuleStore((s) => s.pipeline.find((r) => r.id === ruleId))?.config as SanitizeConfigType | undefined; + const updateRule = useRuleStore((s) => s.updateRule); + if (!rule) return null; + const update = (changes: Partial) => updateRule(ruleId, changes); + + return ( +
+
+ Spaces + update({ spaces_to: s })} options={spaceModes} /> +
+
+ + + + + + +
+
+ ); +} diff --git a/ui/src/components/pipeline/configs/SwapConfig.tsx b/ui/src/components/pipeline/configs/SwapConfig.tsx new file mode 100644 index 0000000..7b8a3f3 --- /dev/null +++ b/ui/src/components/pipeline/configs/SwapConfig.tsx @@ -0,0 +1,45 @@ +import { useRuleStore } from "@/stores/ruleStore"; +import { Input } from "@/components/ui/input"; +import { SegmentedControl } from "@/components/ui/segmented-control"; +import type { SwapConfig as SwapConfigType } from "@/types/rules"; + +const swapOccurrences = [ + { value: "First", label: "First" }, + { value: "Last", label: "Last" }, +] as const; + +export function SwapConfig({ ruleId }: { ruleId: string }) { + const rule = useRuleStore((s) => s.pipeline.find((r) => r.id === ruleId))?.config as SwapConfigType | undefined; + const updateRule = useRuleStore((s) => s.updateRule); + if (!rule) return null; + const update = (changes: Partial) => updateRule(ruleId, changes); + + return ( +
+
+ + +
+
+ Split at occurrence + update({ occurrence: o })} options={swapOccurrences} /> +
+
+ ); +} diff --git a/ui/src/components/pipeline/configs/TextEditorConfig.tsx b/ui/src/components/pipeline/configs/TextEditorConfig.tsx new file mode 100644 index 0000000..41d39c3 --- /dev/null +++ b/ui/src/components/pipeline/configs/TextEditorConfig.tsx @@ -0,0 +1,85 @@ +import { useEffect, useState } from "react"; +import { useRuleStore } from "@/stores/ruleStore"; +import { useFileStore } from "@/stores/fileStore"; +import { Button } from "@/components/ui/button"; +import type { TextEditorConfig as TextEditorConfigType } from "@/types/rules"; + +export function TextEditorConfig({ ruleId }: { ruleId: string }) { + const rule = useRuleStore((s) => s.pipeline.find((r) => r.id === ruleId))?.config as TextEditorConfigType | undefined; + const updateRule = useRuleStore((s) => s.updateRule); + const sortedFilePaths = useFileStore((s) => s.sortedFilePaths); + const selectedFiles = useFileStore((s) => s.selectedFiles); + const files = useFileStore((s) => s.files); + + const [text, setText] = useState(""); + const [initialized, setInitialized] = useState(false); + + // get selected file stems in display order + const fileMap = new Map(files.map((f) => [f.path, f])); + const selectedStems = sortedFilePaths + .filter((p) => selectedFiles.has(p)) + .map((p) => fileMap.get(p)?.stem ?? ""); + + const lineCount = text ? text.split("\n").length : 0; + const fileCount = selectedStems.length; + const mismatch = lineCount !== fileCount && text.length > 0; + + // populate textarea with current stems when first opened or when selection changes and user hasn't edited + useEffect(() => { + if (!rule) return; + if (rule.names.length === 0 && !initialized) { + const joined = selectedStems.join("\n"); + setText(joined); + setInitialized(true); + } + }, [rule, selectedStems.length, initialized]); + + // sync rule names when first loaded with existing data + useEffect(() => { + if (!rule) return; + if (rule.names.length > 0 && !initialized) { + setText(rule.names.join("\n")); + setInitialized(true); + } + }, [rule, initialized]); + + if (!rule) return null; + + const handleChange = (val: string) => { + setText(val); + const names = val.split("\n"); + updateRule(ruleId, { names }); + }; + + const handleLoad = () => { + const joined = selectedStems.join("\n"); + setText(joined); + updateRule(ruleId, { names: selectedStems }); + }; + + return ( +
+
+ + {lineCount} line{lineCount !== 1 ? "s" : ""} / {fileCount} file{fileCount !== 1 ? "s" : ""} + + +
+ {mismatch && ( +
+ Line count ({lineCount}) doesn't match selected files ({fileCount}). + Extra lines will be ignored, missing lines will keep original names. +
+ )} +