diff --git a/Cargo.lock b/Cargo.lock index 3f49ee9..72a5ed9 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -67,48 +67,6 @@ version = "1.0.104" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "330a5ed07fa54e4702c9d6c4174f74427fc0ef6e214bbd677ae50a5099946470" -[[package]] -name = "askama" -version = "0.14.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f75363874b771be265f4ffe307ca705ef6f3baa19011c149da8674a87f1b75c4" -dependencies = [ - "askama_derive", - "itoa", - "percent-encoding", - "serde", - "serde_json", -] - -[[package]] -name = "askama_derive" -version = "0.14.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "129397200fe83088e8a68407a8e2b1f826cf0086b21ccdb866a722c8bcd3a94f" -dependencies = [ - "askama_parser", - "basic-toml", - "memchr", - "proc-macro2", - "quote", - "rustc-hash", - "serde", - "serde_derive", - "syn 2.0.119", -] - -[[package]] -name = "askama_parser" -version = "0.14.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d6ab5630b3d5eaf232620167977f95eb51f3432fc76852328774afbd242d4358" -dependencies = [ - "memchr", - "serde", - "serde_derive", - "winnow", -] - [[package]] name = "async-stream" version = "0.3.6" @@ -189,15 +147,6 @@ dependencies = [ "tracing", ] -[[package]] -name = "basic-toml" -version = "0.1.10" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ba62675e8242a4c4e806d12f11d136e626e6c8361d6b829310732241652a178a" -dependencies = [ - "serde", -] - [[package]] name = "bitflags" version = "2.13.2" @@ -420,6 +369,25 @@ dependencies = [ "tower-service", ] +[[package]] +name = "include_dir" +version = "0.7.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "923d117408f1e49d914f1a379a309cffe4f18c05cf4e3d12e613a15fc81bd0dd" +dependencies = [ + "include_dir_macros", +] + +[[package]] +name = "include_dir_macros" +version = "0.7.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7cab85a7ed0bd5f0e76d93846e0147172bed2e2d3f859bcc33a8d9699cad1a75" +dependencies = [ + "proc-macro2", + "quote", +] + [[package]] name = "inotify" version = "0.11.5" @@ -480,9 +448,9 @@ checksum = "20870f649af7073d53e38067b2a84312175d56ea15217e1b15bc83506ec50afb" [[package]] name = "libc" -version = "0.2.190" +version = "0.2.191" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ce5d3ddc6d3fa000eb1536d85e147bfe31aacaba692ed6a876f95cb7c855be78" +checksum = "65cd87c9a6d60536580b316ad985726104730a5151c99845f2490428ef91240a" [[package]] name = "lock_api" @@ -695,12 +663,6 @@ version = "0.8.11" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d6f6ff9a378485b298a5286656da665ba74413d36db0979633275d2e708145d4" -[[package]] -name = "rustc-hash" -version = "2.1.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6b1e7f9a428571be2dc5bc0505c13fb6bf936822b894ec87abf8a08a4e51742d" - [[package]] name = "ryu" version = "1.0.23" @@ -830,26 +792,15 @@ dependencies = [ ] [[package]] -name = "stash-cli" +name = "stash" version = "0.1.0" dependencies = [ "anyhow", - "clap", - "colored", - "stash-core", - "tokio", - "tracing", - "tracing-subscriber", -] - -[[package]] -name = "stash-core" -version = "0.1.0" -dependencies = [ - "anyhow", - "askama", "async-stream", "axum", + "clap", + "colored", + "include_dir", "mime_guess", "notify", "serde", @@ -859,6 +810,7 @@ dependencies = [ "tower 0.4.13", "tower-http", "tracing", + "tracing-subscriber", "walkdir", ] @@ -898,22 +850,22 @@ checksum = "0bf256ce5efdfa370213c1dabab5935a12e49f2c58d15e9eac2870d3b4f27263" [[package]] name = "thiserror" -version = "1.0.69" +version = "2.0.21" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b6aaf5339b578ea85b50e080feb250a3e8ae8cfcdff9a461c9ec2904bc923f52" +checksum = "09e52cb86a36cede5cb101bf8908837b3e4c6e5e59fe7fd85c23fb56200d189e" dependencies = [ "thiserror-impl", ] [[package]] name = "thiserror-impl" -version = "1.0.69" +version = "2.0.21" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4fee6c4efc90059e10f81e6d42c60a18f76588c3d74cb83a0b242a2b6c7504c1" +checksum = "fe5197923287db20a58125f0bc85c062f7f2c892de97b18c356f9efb14b28524" dependencies = [ "proc-macro2", "quote", - "syn 2.0.119", + "syn 3.0.9", ] [[package]] @@ -927,9 +879,9 @@ dependencies = [ [[package]] name = "tokio" -version = "1.53.3" +version = "1.54.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ce3335fa71841cda333a58d7615b03901380ecf09d59b3296d21f8bbac0dde4e" +checksum = "a0c168a474b95bfecfe908a65d4bdffc523144b649a74c09c286afb35d53e0bc" dependencies = [ "bytes", "libc", @@ -1235,15 +1187,6 @@ version = "0.53.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d6bbff5f0aada427a1e5a6da5f1f98158182f26556f345ac9e04d36d0ebed650" -[[package]] -name = "winnow" -version = "0.7.15" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "df79d97927682d2fd8adb29682d1140b343be4ac0f08fd68b7765d9c059d3945" -dependencies = [ - "memchr", -] - [[package]] name = "zmij" version = "1.0.23" diff --git a/Cargo.toml b/Cargo.toml index cf837a0..eeeadcc 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,18 +1,30 @@ -[workspace] -resolver = "2" -members = [ - "stash-core", - "stash-cli", -] - -[workspace.package] +[package] +name = "stash" version = "0.1.0" +description = "" +authors = [""] edition = "2024" -license = "MIT OR Apache-2.0" -[workspace.dependencies] +[dependencies] tokio = { version = "1.38", features = ["full"] } tracing = "0.1" tracing-subscriber = { version = "0.3", features = ["env-filter"] } -thiserror = "1.0" -anyhow = "1.0" \ No newline at end of file +thiserror = "2.0.21" +anyhow = "1.0" +clap = { version = "4.6.7", features = ["derive"] } +colored = "3.1.1" + +# Web 核心与静态托管 +axum = "0.8.9" +tower-http = { version = "0.7.1", features = ["fs", "cors", "trace"] } +tower = { version = "0.4", features = ["util"] } +serde = { version = "1.0", features = ["derive"] } +serde_json = "1.0" +mime_guess = "2.0" +include_dir = "0.7" +async-stream = "0.3" +notify = "8" + +[build-dependencies] +# build.rs 用它递归复制前端产物到 $OUT_DIR/web +walkdir = "2.4" diff --git a/stash-core/assets/dashboard.css b/assets/dashboard.css similarity index 100% rename from stash-core/assets/dashboard.css rename to assets/dashboard.css diff --git a/stash-core/assets/dashboard.js b/assets/dashboard.js similarity index 100% rename from stash-core/assets/dashboard.js rename to assets/dashboard.js diff --git a/build.rs b/build.rs new file mode 100644 index 0000000..dfd848b --- /dev/null +++ b/build.rs @@ -0,0 +1,88 @@ +// build.rs +// +// 把前端产物 stash-web/dist 同步到 $OUT_DIR/web, +// 供 include_dir!("$OUT_DIR/web") 在编译期内嵌进二进制。 +// +// 这里只做「校验 + 搬运」,不在这里跑 npm: +// 那样会把 Node 和网络依赖带进 cargo build。 +// 编译 Rust 之前请先执行:cd stash-web && npm install && npm run build + +use std::{ + env, fs, + path::{Path, PathBuf}, +}; + +use walkdir::WalkDir; + +/// 前端工程目录,相对 crate 根目录。 +const WEB_PROJECT_DIR: &str = "stash-web"; +/// 前端构建产物目录,相对前端工程目录。 +const WEB_DIST_DIR: &str = "dist"; +/// 同步到 $OUT_DIR 下的子目录名,需与 include_dir! 中的路径保持一致。 +const EMBED_DIR: &str = "web"; + +fn main() { + // 前端产物变了要重新嵌入(Cargo 对目录会递归扫描) + println!("cargo:rerun-if-changed={}/{}", WEB_PROJECT_DIR, WEB_DIST_DIR); + println!("cargo:rerun-if-changed={}/package.json", WEB_PROJECT_DIR); + + let dist = Path::new(env!("CARGO_MANIFEST_DIR")) + .join(WEB_PROJECT_DIR) + .join(WEB_DIST_DIR); + let out = PathBuf::from(env::var("OUT_DIR").expect("构建环境缺少 OUT_DIR")).join(EMBED_DIR); + + // 缺产物就给出人能看懂的报错,而不是 proc-macro 的 "No such file or directory" + assert!( + dist.join("index.html").is_file(), + "未找到前端产物:{}\n请先执行: cd {} && npm install && npm run build", + dist.display(), + WEB_PROJECT_DIR + ); + + // 必须整体重建,否则 Vite 换 hash 后旧 chunk 会残留在二进制里 + let _ = fs::remove_dir_all(&out); + fs::create_dir_all(&out).expect("创建内嵌目录失败"); + + copy_dir(&dist, &out).expect("复制前端产物失败"); +} + +/// 用 walkdir 递归复制 `from` 到 `to`,并保留目录结构。 +fn copy_dir(from: &Path, to: &Path) -> Result<(), Box> { + let mut copied = 0usize; + + for entry in WalkDir::new(from) { + let entry = entry?; + + let relative = entry.path().strip_prefix(from)?; + + // 跳过 dist 目录自身 + if relative.as_os_str().is_empty() { + continue; + } + + let destination = to.join(relative); + + if entry.file_type().is_dir() { + fs::create_dir_all(&destination)?; + continue; + } + + // 符号链接等非普通文件不参与内嵌 + if !entry.file_type().is_file() { + continue; + } + + fs::copy(entry.path(), &destination)?; + copied += 1; + } + + if copied == 0 { + return Err(std::io::Error::other(format!( + "{} 中没有任何文件可内嵌", + from.display() + )) + .into()); + } + + Ok(()) +} diff --git a/public/git_helper/git-flow.png b/public/git_helper/git-flow.png new file mode 100644 index 0000000..2cf153c Binary files /dev/null and b/public/git_helper/git-flow.png differ diff --git a/public/git_helper/index.html b/public/git_helper/index.html new file mode 100644 index 0000000..5122fc6 --- /dev/null +++ b/public/git_helper/index.html @@ -0,0 +1,745 @@ + + + + + + Git 规范标准化与工作流生成器 + + + + +
+
+

Git 规范标准化生成器

+

企业级 Git 分支管理与 Commit 标准化生成工具

+
+ + + + + +
+
+
初始化项目 Commit 模版
+ +
+ + +
+ +
+ + +
+ +
+ + +
+ +
+ + +
+ +
+ + +
+
+ +
+
+ 生成的 Init Commit Message + +
+

+    
+
+ + +
+
+
1. 选择提交类型 (Type)
+
+ +
2. 填写提交内容
+
+
+ + +
+
+ + +
+
+ +
+ + +
+ +
3. 脚注选项 (Footer)
+
+
+ + +
+
+
+
+ + +
+
+ + +
+
+ +
+ +
+
+ +
+
+ 生成的 Commit Message + +
+

+    
+
+ + +
+
+
企业敏捷 Git 演进架构图 (原生高清预览)
+ + +
+ 企业敏捷 Git 拓扑流转图 +
+ +
选择分支类型生成 Git 命令
+
+ + + + + +
+ +
填写项目与人员配置
+
+
+ + +
+
+ + +
+
+ + +
+
+ + + +
+ 当前分支说明与规则: +
    +
  • 从 main 拉取并创建 develop 分支作为团队日常开发基线。
  • +
  • 执行命令:git checkout -b develop main
  • +
+
+
+ +
+
+ 生成的 Git 创建/切换分支命令 + +
+

+    
+
+
+ + + + + \ No newline at end of file diff --git a/stash-core/src/config.rs b/src/config.rs similarity index 93% rename from stash-core/src/config.rs rename to src/config.rs index 51a4019..8b311a6 100644 --- a/stash-core/src/config.rs +++ b/src/config.rs @@ -17,7 +17,7 @@ pub struct Config { impl Default for Config { fn default() -> Self { Self { - root: PathBuf::from("./public"), + root: PathBuf::from("../public"), port: 8080, host: "127.0.0.1".to_string(), directory_listing: true, diff --git a/stash-core/src/error.rs b/src/error.rs similarity index 93% rename from stash-core/src/error.rs rename to src/error.rs index f66ea14..33ada44 100644 --- a/stash-core/src/error.rs +++ b/src/error.rs @@ -11,9 +11,6 @@ pub enum StashError { #[error("IO 错误: {0}")] Io(#[from] std::io::Error), - #[error("模板渲染失败: {0}")] - Template(#[from] askama::Error), - #[error("文件监听失败: {0}")] Watch(#[from] notify::Error), diff --git a/stash-core/src/lib.rs b/src/lib.rs similarity index 100% rename from stash-core/src/lib.rs rename to src/lib.rs diff --git a/stash-core/src/listing.rs b/src/listing.rs similarity index 66% rename from stash-core/src/listing.rs rename to src/listing.rs index 17aaa90..fdf8676 100644 --- a/stash-core/src/listing.rs +++ b/src/listing.rs @@ -1,9 +1,6 @@ -use askama::Template; use serde::Serialize; use std::{fmt::Write as _, path::Path}; -use crate::error::StashError; - #[derive(Debug, Serialize, Clone, PartialEq, Eq)] pub struct AppProject { pub name: String, @@ -21,18 +18,6 @@ impl AppProject { } } -#[derive(Template)] -#[template(path = "index.html")] -struct DashboardTemplate<'a> { - projects: &'a [AppProject], -} - -#[derive(Template)] -#[template(path = "404.html")] -struct NotFoundTemplate<'a> { - path: &'a str, -} - pub fn scan_projects(root: &Path) -> Vec { let mut projects = Vec::new(); @@ -66,14 +51,6 @@ pub fn scan_projects(root: &Path) -> Vec { projects } -pub fn render_dashboard(projects: &[AppProject]) -> Result { - Ok(DashboardTemplate { projects }.render()?) -} - -pub fn render_not_found(path: &str) -> Result { - Ok(NotFoundTemplate { path }.render()?) -} - fn encode_path_segment(segment: &str) -> String { let mut encoded = String::with_capacity(segment.len()); @@ -129,18 +106,6 @@ mod tests { } } - fn contains_escaped_less_than(html: &str) -> bool { - ["<", "<", "<", "<"] - .iter() - .any(|entity| html.contains(entity)) - } - - fn contains_escaped_ampersand(html: &str) -> bool { - ["&", "&", "&", "&"] - .iter() - .any(|entity| html.contains(entity)) - } - #[test] fn scan_projects_detects_only_immediate_visible_projects() { let root = TestDir::new(); @@ -192,46 +157,4 @@ mod tests { fn path_segment_encoding_preserves_unreserved_characters() { assert_eq!(encode_path_segment("aZ09-_.~/?#%"), "aZ09-_.~%2F%3F%23%25"); } - - #[test] - fn dashboard_renders_empty_state() { - let html = render_dashboard(&[]).expect("空项目列表应能正常渲染"); - - assert!(html.contains("这里还没有项目")); - assert!(html.contains("index.html")); - assert!(!html.contains("class=\"site-item\"")); - } - - #[test] - fn dashboard_escapes_project_content() { - let projects = vec![AppProject { - name: "<项目 & \"测试\">".to_string(), - route_path: "/safe%20path/".to_string(), - }]; - - let html = render_dashboard(&projects).expect("项目卡片应能正常渲染"); - - assert!( - contains_escaped_less_than(&html), - "项目名称中的 < 应被 HTML 转义,实际输出: {html}" - ); - assert!( - contains_escaped_ampersand(&html), - "项目名称中的 & 应被 HTML 转义,实际输出: {html}" - ); - assert!(!html.contains("<项目 & \"测试\">")); - } - - #[test] - fn not_found_page_renders_and_escapes_request_path() { - let html = - render_not_found("/missing/").expect("404 页面应能正常渲染"); - - assert!(html.contains("资源或页面不存在")); - assert!( - contains_escaped_less_than(&html), - "请求路径中的 < 应被 HTML 转义,实际输出: {html}" - ); - assert!(!html.contains("")); - } } diff --git a/stash-cli/src/main.rs b/src/main.rs similarity index 96% rename from stash-cli/src/main.rs rename to src/main.rs index 3fe2541..b9534d9 100644 --- a/stash-cli/src/main.rs +++ b/src/main.rs @@ -1,7 +1,7 @@ use std::path::PathBuf; use clap::Parser; use colored::*; -use stash_core::{Config, start}; +use stash::{Config, start}; use tracing_subscriber::{layer::SubscriberExt, util::SubscriberInitExt}; #[derive(Parser, Debug)] @@ -93,8 +93,8 @@ async fn main() -> anyhow::Result<()> { println!("{}", "==================================================".bright_blue()); println!(" ➜ 本地访问: {}", format!("http://{}", handle.local_addr).cyan().bold().underline()); println!(" ➜ 托管路径: {}", abs_root.display().to_string().white()); - println!(" ➜ 目录导航页: {}", if !cli.no_listing { "开启".green() } else { "关闭".red() }); - println!(" ➜ 接口列表: {}", format!("http://{}/api/pages", handle.local_addr).dimmed()); + println!(" ➜ 前端资源: {}", "已内嵌 (stash-web/dist)".white()); + println!(" ➜ 接口列表: {}", format!("http://{}/api/projects", handle.local_addr).dimmed()); println!("{}", "--------------------------------------------------".bright_blue()); println!(" 按 {} 停止服务", "Ctrl+C".yellow().bold()); println!(); diff --git a/stash-core/src/server.rs b/src/server.rs similarity index 55% rename from stash-core/src/server.rs rename to src/server.rs index 672405e..197a89f 100644 --- a/stash-core/src/server.rs +++ b/src/server.rs @@ -1,11 +1,12 @@ use axum::{ Json, Router, - extract::{OriginalUri, State}, - http::{StatusCode, header}, + extract::{OriginalUri, Path, State}, + http::{HeaderValue, StatusCode, header}, response::sse::{Event, KeepAlive, Sse}, response::{Html, IntoResponse, Response}, routing::get, }; +use include_dir::{Dir, include_dir}; use std::{convert::Infallible, net::SocketAddr, sync::Arc, time::Duration}; use tokio::sync::{broadcast, oneshot}; use tower_http::{ @@ -17,11 +18,16 @@ use tracing::{info, warn}; use crate::config::Config; use crate::error::StashError; -use crate::listing::{AppProject, render_dashboard, render_not_found, scan_projects}; +use crate::listing::{AppProject, scan_projects}; use crate::watcher; -const DASHBOARD_CSS: &str = include_str!("../assets/dashboard.css"); -const DASHBOARD_JS: &str = include_str!("../assets/dashboard.js"); +/// build.rs 会把 `stash-web/dist` 同步到 `$OUT_DIR/web`, +/// 这里把整棵目录树在编译期内嵌,最终产出带前端的单文件二进制。 +static WEB: Dir<'static> = include_dir!("$OUT_DIR/web"); + +/// Vite 产物文件名带内容 hash,可以长期强缓存;HTML 每次都要回源校验。 +const HASHED_ASSET_CACHE: &str = "public, max-age=31536000, immutable"; +const HTML_CACHE: &str = "no-cache"; #[derive(Clone)] pub struct AppState { @@ -40,46 +46,80 @@ impl ServerHandle { } } -/// 根路径处理器 -async fn root_handler(State(state): State) -> Result { - let index_file = state.config.root.join("index.html"); +/// 根路径:返回内嵌前端的入口页。 +async fn web_index() -> Response { + embedded_file("index.html").unwrap_or_else(|| not_found_page("/")) +} - // 1. 如果托管目录本身就是一个完整的前端项目(含 index.html),优先直接展示 - if index_file.exists() { - let content = tokio::fs::read_to_string(&index_file).await?; - return Ok(( - [(header::CONTENT_TYPE, "text/html; charset=utf-8")], - Html(content), +/// `/assets/**`:Vite 产出的 CSS / JS / 图片。 +async fn web_asset(OriginalUri(uri): OriginalUri, Path(path): Path) -> Response { + let file = format!("assets/{path}"); + embedded_file(&file).unwrap_or_else(|| not_found_page(uri.path())) +} + +/// 前端放在 dist 根目录的其他文件(favicon 等)。 +async fn web_root_file(OriginalUri(uri): OriginalUri) -> Response { + let path = uri.path().trim_start_matches('/'); + embedded_file(path).unwrap_or_else(|| not_found_page(uri.path())) +} + +/// 从内嵌目录取文件并组装响应;找不到返回 `None`。 +fn embedded_file(path: &str) -> Option { + let file = WEB.get_file(path)?; + + let mime = mime_guess::from_path(path).first_or_octet_stream(); + let content_type = HeaderValue::from_str(mime.as_ref()) + .unwrap_or_else(|_| HeaderValue::from_static("application/octet-stream")); + let cache = if path.starts_with("assets/") { + HeaderValue::from_static(HASHED_ASSET_CACHE) + } else { + HeaderValue::from_static(HTML_CACHE) + }; + + Some( + ( + StatusCode::OK, + [ + (header::CONTENT_TYPE, content_type), + (header::CACHE_CONTROL, cache), + ], + file.contents(), ) - .into_response()); - } - - // 2. 否则展示筛选后的可用前端应用大厅 - let projects = scan_projects(&state.config.root); - let html = render_dashboard(&projects)?; - Ok(Html(html).into_response()) -} - -/// 全局 404 自定义页面处理器 -async fn not_found_handler( - OriginalUri(uri): OriginalUri, -) -> Result<(StatusCode, Html), StashError> { - let html = render_not_found(uri.path())?; - Ok((StatusCode::NOT_FOUND, Html(html))) -} - -async fn dashboard_css() -> impl IntoResponse { - ( - [(header::CONTENT_TYPE, "text/css; charset=utf-8")], - DASHBOARD_CSS, + .into_response(), ) } -async fn dashboard_js() -> impl IntoResponse { - ( - [(header::CONTENT_TYPE, "text/javascript; charset=utf-8")], - DASHBOARD_JS, - ) +/// 统一 404:内嵌前端没有这个文件,托管目录里也没有这个路径。 +fn not_found_page(path: &str) -> Response { + let body = format!( + r#" + +404 · 页面不存在 + +
+

404

+

资源或页面不存在

+

{}

+返回本地应用 +
+ +"#, + escape_html(path) + ); + + (StatusCode::NOT_FOUND, Html(body)).into_response() +} + +async fn not_found_handler(OriginalUri(uri): OriginalUri) -> Response { + not_found_page(uri.path()) +} + +fn escape_html(value: &str) -> String { + value + .replace('&', "&") + .replace('<', "<") + .replace('>', ">") + .replace('"', """) } /// API: 健康检查 @@ -153,7 +193,8 @@ pub async fn start(config: Config) -> Result { project_updates, }; - // 静态文件服务:找不到静态资源时 fallback 到自定义 not_found_handler + // 托管目录里找不到的路径,交给 404 页面而不是 SPA 回落: + // 前端目前没有客户端路由,把 404 冒充 200 只会让排错更难。 let serve_dir = ServeDir::new(&config.root) .append_index_html_on_directories(true) .fallback(axum::handler::HandlerWithoutStateExt::into_service( @@ -164,9 +205,10 @@ pub async fn start(config: Config) -> Result { .route("/api/health", get(health_check)) .route("/api/projects", get(list_projects_api)) .route("/api/projects/events", get(project_events)) - .route("/__stash__/dashboard.css", get(dashboard_css)) - .route("/__stash__/dashboard.js", get(dashboard_js)) - .route("/", get(root_handler)) + .route("/", get(web_index)) + .route("/index.html", get(web_index)) + .route("/favicon.svg", get(web_root_file)) + .route("/assets/{*path}", get(web_asset)) .fallback_service(serve_dir) .with_state(shared_state) .layer(TraceLayer::new_for_http()); diff --git a/stash-core/src/watcher.rs b/src/watcher.rs similarity index 100% rename from stash-core/src/watcher.rs rename to src/watcher.rs diff --git a/stash-cli/Cargo.toml b/stash-cli/Cargo.toml deleted file mode 100644 index a68f627..0000000 --- a/stash-cli/Cargo.toml +++ /dev/null @@ -1,18 +0,0 @@ -[package] -name = "stash-cli" -version = { workspace = true } -edition = { workspace = true } -license = { workspace = true } - -[[bin]] -name = "stash" -path = "src/main.rs" - -[dependencies] -stash-core = { path = "../stash-core",version = "0.1.0" } -tokio = { workspace = true } -tracing = { workspace = true } -tracing-subscriber = { workspace = true } -clap = { version = "4.6.7", features = ["derive"] } -anyhow = { workspace = true } -colored = "3.1.1" \ No newline at end of file diff --git a/stash-core/Cargo.toml b/stash-core/Cargo.toml deleted file mode 100644 index 022b009..0000000 --- a/stash-core/Cargo.toml +++ /dev/null @@ -1,23 +0,0 @@ -[package] -name = "stash-core" -version = { workspace = true } -edition = { workspace = true } -license = { workspace = true } - -[dependencies] -tokio = { workspace = true } -tracing = { workspace = true } -thiserror = { workspace = true } -anyhow = { workspace = true } - -# Web 核心与静态托管 -axum = "0.8.9" -tower-http = { version = "0.7.1", features = ["fs", "cors", "trace"] } -tower = { version = "0.4", features = ["util"] } -serde = { version = "1.0", features = ["derive"] } -serde_json = "1.0" -walkdir = "2.4" -mime_guess = "2.0" -askama = "0.14" -async-stream = "0.3" -notify = "8" diff --git a/stash-web/.gitignore b/stash-web/.gitignore new file mode 100644 index 0000000..a547bf3 --- /dev/null +++ b/stash-web/.gitignore @@ -0,0 +1,24 @@ +# Logs +logs +*.log +npm-debug.log* +yarn-debug.log* +yarn-error.log* +pnpm-debug.log* +lerna-debug.log* + +node_modules +dist +dist-ssr +*.local + +# Editor directories and files +.vscode/* +!.vscode/extensions.json +.idea +.DS_Store +*.suo +*.ntvs* +*.njsproj +*.sln +*.sw? diff --git a/stash-web/.oxlintrc.json b/stash-web/.oxlintrc.json new file mode 100644 index 0000000..6fa991d --- /dev/null +++ b/stash-web/.oxlintrc.json @@ -0,0 +1,8 @@ +{ + "$schema": "./node_modules/oxlint/configuration_schema.json", + "plugins": ["react", "typescript", "oxc"], + "rules": { + "react/rules-of-hooks": "error", + "react/only-export-components": ["warn", { "allowConstantExport": true }] + } +} diff --git a/stash-web/README.md b/stash-web/README.md new file mode 100644 index 0000000..d6af7e3 --- /dev/null +++ b/stash-web/README.md @@ -0,0 +1,32 @@ +# React + TypeScript + Vite + +This template provides a minimal setup to get React working in Vite with HMR and some Oxlint rules. + +Currently, two official plugins are available: + +- [@vitejs/plugin-react](https://github.com/vitejs/vite-plugin-react/blob/main/packages/plugin-react) uses [Oxc](https://oxc.rs) +- [@vitejs/plugin-react-swc](https://github.com/vitejs/vite-plugin-react/blob/main/packages/plugin-react-swc) uses [SWC](https://swc.rs/) + +## React Compiler + +The React Compiler is not enabled on this template because of its impact on dev & build performances. To add it, see [this documentation](https://react.dev/learn/react-compiler/installation). + +## Expanding the Oxlint configuration + +If you are developing a production application, we recommend enabling type-aware lint rules by installing `oxlint-tsgolint` and editing `.oxlintrc.json`: + +```json +{ + "$schema": "./node_modules/oxlint/configuration_schema.json", + "plugins": ["react", "typescript", "oxc"], + "options": { + "typeAware": true + }, + "rules": { + "react/rules-of-hooks": "error", + "react/only-export-components": ["warn", { "allowConstantExport": true }] + } +} +``` + +See the [Oxlint rules documentation](https://oxc.rs/docs/guide/usage/linter/rules) for the full list of rules and categories. diff --git a/stash-web/index.html b/stash-web/index.html new file mode 100644 index 0000000..25e12d7 --- /dev/null +++ b/stash-web/index.html @@ -0,0 +1,14 @@ + + + + + + + + Stash · 本地应用工作区 + + +
+ + + diff --git a/stash-web/package-lock.json b/stash-web/package-lock.json new file mode 100644 index 0000000..7a7d8d7 --- /dev/null +++ b/stash-web/package-lock.json @@ -0,0 +1,1300 @@ +{ + "name": "stash-web", + "version": "0.0.0", + "lockfileVersion": 3, + "requires": true, + "packages": { + "": { + "name": "stash-web", + "version": "0.0.0", + "dependencies": { + "react": "^19.3.1", + "react-dom": "^19.3.1" + }, + "devDependencies": { + "@types/node": "^24.19.2", + "@types/react": "^19.3.0", + "@types/react-dom": "^19.3.0", + "@vitejs/plugin-react": "^6.1.2", + "oxlint": "^1.88.0", + "typescript": "~6.0.3", + "vite": "^8.3.4" + } + }, + "node_modules/@oxc-project/types": { + "version": "0.154.0", + "resolved": "https://registry.npmmirror.com/@oxc-project/types/-/types-0.154.0.tgz", + "integrity": "sha512-KYOUGQFlLWR+s4QMV2Q9ko38TZm7EdO87Qm7rvZLqk/webse/cDjtyxKhg/47l0byCbs/k46LT8qc4PKjAikzA==", + "dev": true, + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/oxc-project" + } + }, + "node_modules/@oxlint/binding-android-arm-eabi": { + "version": "1.89.0", + "resolved": "https://registry.npmmirror.com/@oxlint/binding-android-arm-eabi/-/binding-android-arm-eabi-1.89.0.tgz", + "integrity": "sha512-Pflpba5F3gRAQ12sWglRxvWTHEdAusNDvmi4Y5N1T5SY1AGkM7AixOnET8qkrnLJ/F32qAlNokO6eCknT8tmnw==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@oxlint/binding-android-arm64": { + "version": "1.89.0", + "resolved": "https://registry.npmmirror.com/@oxlint/binding-android-arm64/-/binding-android-arm64-1.89.0.tgz", + "integrity": "sha512-dYmyD1x7LaPlb79vZWUcpTShk8dI9z1I+IubtqrBxWNJaDhQdPHwcsvg7q5StTU4xWyd9AwPfLaTYFZY8NZgtQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@oxlint/binding-darwin-arm64": { + "version": "1.89.0", + "resolved": "https://registry.npmmirror.com/@oxlint/binding-darwin-arm64/-/binding-darwin-arm64-1.89.0.tgz", + "integrity": "sha512-Z/oxOPiIqiS7fItRdxaaOEC+6GD3OpGi0IxoR0dMefzQHolsKLzScw2DNcu/5gUPok3tv+G9mslvYB5bo3i0qg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@oxlint/binding-darwin-x64": { + "version": "1.89.0", + "resolved": "https://registry.npmmirror.com/@oxlint/binding-darwin-x64/-/binding-darwin-x64-1.89.0.tgz", + "integrity": "sha512-eAjTkHSjAxrdCPJxO6y5sPQL5pYLKb2kvxZz9UU7tXnOZk9I/HLtkvx9S7GHY883k+AmL/eC7mqFt+w7WCwk0w==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@oxlint/binding-freebsd-x64": { + "version": "1.89.0", + "resolved": "https://registry.npmmirror.com/@oxlint/binding-freebsd-x64/-/binding-freebsd-x64-1.89.0.tgz", + "integrity": "sha512-0lNCA9Tl1n1FKpqy2/9ckp0kIuSrtJ/l9bMRgaEhOtM7TsOHshFuwOcvW8oA9teXLMOJT61LjEKWiaHDFGxF/w==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@oxlint/binding-linux-arm-gnueabihf": { + "version": "1.89.0", + "resolved": "https://registry.npmmirror.com/@oxlint/binding-linux-arm-gnueabihf/-/binding-linux-arm-gnueabihf-1.89.0.tgz", + "integrity": "sha512-OTFEvi9W+JczaZD1Caj17SGCoW8H7TE+fvx0D7ATEWa1Lohn91UMxFQ6r145SiGy/yCxeIwptMys/RAM7gSVSQ==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@oxlint/binding-linux-arm-musleabihf": { + "version": "1.89.0", + "resolved": "https://registry.npmmirror.com/@oxlint/binding-linux-arm-musleabihf/-/binding-linux-arm-musleabihf-1.89.0.tgz", + "integrity": "sha512-fRxgNHvBLswj6MrhW2uBq5UDhPRQlCbHeq/Nf8yuG9zOxhDsXqX/mi2PX1axgsQxMm88el7HLiJAzglAnflYxw==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@oxlint/binding-linux-arm64-gnu": { + "version": "1.89.0", + "resolved": "https://registry.npmmirror.com/@oxlint/binding-linux-arm64-gnu/-/binding-linux-arm64-gnu-1.89.0.tgz", + "integrity": "sha512-v9ENzHvwrQoYSWTtviH1pOYc7R9UovDxVofnck50cNmPihRRTcrg5X0UlRbLOZ7IVtxPOlh2hb7kDO+FpE8SfQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@oxlint/binding-linux-arm64-musl": { + "version": "1.89.0", + "resolved": "https://registry.npmmirror.com/@oxlint/binding-linux-arm64-musl/-/binding-linux-arm64-musl-1.89.0.tgz", + "integrity": "sha512-vfffe8mQ484AzVadlUOFpgL+JDwhvs9hrzxTgQP1FVfi4ZEUUYeffXlzC9dFV7Dgnu5s/KrNKvfmG6HxBd/ISQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@oxlint/binding-linux-ppc64-gnu": { + "version": "1.89.0", + "resolved": "https://registry.npmmirror.com/@oxlint/binding-linux-ppc64-gnu/-/binding-linux-ppc64-gnu-1.89.0.tgz", + "integrity": "sha512-Buonlp4hVS7RGNP5M7bPpGNQrenyzA2C64JcO/oz9C1c6dGOXJ7VEAnaiAMY7voou0JYyCJdvDVlFQc5DZX3DQ==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@oxlint/binding-linux-riscv64-gnu": { + "version": "1.89.0", + "resolved": "https://registry.npmmirror.com/@oxlint/binding-linux-riscv64-gnu/-/binding-linux-riscv64-gnu-1.89.0.tgz", + "integrity": "sha512-1eVwJJrJ3lYIPT+cm7BcBBZZXY6IQZP/oyonaruTor/MrVyWOpvnwt1oD5CLdsHg5aVBIxXodQSvt+1gK6ck5w==", + "cpu": [ + "riscv64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@oxlint/binding-linux-riscv64-musl": { + "version": "1.89.0", + "resolved": "https://registry.npmmirror.com/@oxlint/binding-linux-riscv64-musl/-/binding-linux-riscv64-musl-1.89.0.tgz", + "integrity": "sha512-fRQQsKUGShdEL5Fl9o9uhj3grkZbeXKLv0icQ5tspUCNYSDy8wzzEcP1WC2bKdTQ0rSszbKlO3iiHORxV22DLg==", + "cpu": [ + "riscv64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@oxlint/binding-linux-s390x-gnu": { + "version": "1.89.0", + "resolved": "https://registry.npmmirror.com/@oxlint/binding-linux-s390x-gnu/-/binding-linux-s390x-gnu-1.89.0.tgz", + "integrity": "sha512-dEtYjIy6+fHcjKklsmtmMkZRhwHr2UbzjmpvuPtQYSeN9awQ2hx5JLms5Wvri+8P/NZtD8v5hfgHALUql+/Ycg==", + "cpu": [ + "s390x" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@oxlint/binding-linux-x64-gnu": { + "version": "1.89.0", + "resolved": "https://registry.npmmirror.com/@oxlint/binding-linux-x64-gnu/-/binding-linux-x64-gnu-1.89.0.tgz", + "integrity": "sha512-hqjwIj5KOYM8Wjef/p7IruNi1iByLhSzbMBk62y22P/E8q7Jj0Pp+DeUTKTRYyZYCDtU1wEGlnWOnttHbyNIww==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@oxlint/binding-linux-x64-musl": { + "version": "1.89.0", + "resolved": "https://registry.npmmirror.com/@oxlint/binding-linux-x64-musl/-/binding-linux-x64-musl-1.89.0.tgz", + "integrity": "sha512-iAQN3e6o9UgUCcdGwqRx/jYcKbG3HK7ur3rH6lTqVcQeNGZ85AeWIg6cgn/edwFQ0ofEZZItMwkzyCVTahnvEA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@oxlint/binding-openharmony-arm64": { + "version": "1.89.0", + "resolved": "https://registry.npmmirror.com/@oxlint/binding-openharmony-arm64/-/binding-openharmony-arm64-1.89.0.tgz", + "integrity": "sha512-MNxjEByz3N/7Q4qFQuPWvc14TpN9LoD3pECEDsJTZbP/wyB7U+hduUXEl7QAbbgVezFwsb9sWtNUO6XVBndOhg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openharmony" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@oxlint/binding-win32-arm64-msvc": { + "version": "1.89.0", + "resolved": "https://registry.npmmirror.com/@oxlint/binding-win32-arm64-msvc/-/binding-win32-arm64-msvc-1.89.0.tgz", + "integrity": "sha512-xxe+i9wfuFso/VnoL4mfj0UlzE+cfCFiUB06vkXCMyQdgVVmItF5FfAF4tATAktSU6uHruWR+FjqvAT9cHZZzg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@oxlint/binding-win32-ia32-msvc": { + "version": "1.89.0", + "resolved": "https://registry.npmmirror.com/@oxlint/binding-win32-ia32-msvc/-/binding-win32-ia32-msvc-1.89.0.tgz", + "integrity": "sha512-saFy07cO94E664glwgLmKLI40HgTL1PY86y4T6+ud4niX4H0Kdqjcmqmjps7mQNdBytV5guGbXDdyQtRfeLbLA==", + "cpu": [ + "ia32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@oxlint/binding-win32-x64-msvc": { + "version": "1.89.0", + "resolved": "https://registry.npmmirror.com/@oxlint/binding-win32-x64-msvc/-/binding-win32-x64-msvc-1.89.0.tgz", + "integrity": "sha512-hLFJ/o0f/dTJh/EW0vmcbREYSQO9tYgfiAAlsOkiLFiJuRRWvYtFELUXN5o2ijpSo/tlO1n+YlWN58tZwPG18A==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-android-arm-eabi": { + "version": "1.2.14", + "resolved": "https://registry.npmmirror.com/@rolldown/binding-android-arm-eabi/-/binding-android-arm-eabi-1.2.14.tgz", + "integrity": "sha512-aWR+XKH9+rW7m5fWq/5360J3mHycrx/o6xZM2DQFdJnTMdyP7/WzxPjhtLbVWdKo2y612/4U1bWqb3HQhiF7vg==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-android-arm64": { + "version": "1.2.14", + "resolved": "https://registry.npmmirror.com/@rolldown/binding-android-arm64/-/binding-android-arm64-1.2.14.tgz", + "integrity": "sha512-QiVxHofl6A8lQU7WAtOUY4QMVZXohxwz7WJJoeh8NBEUyKfIkMRKhYyq8az/1RypNiIXzNTtiS6KwHz0erxYRQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-darwin-arm64": { + "version": "1.2.14", + "resolved": "https://registry.npmmirror.com/@rolldown/binding-darwin-arm64/-/binding-darwin-arm64-1.2.14.tgz", + "integrity": "sha512-lTQOzHuBIooh3jIr5XINQkjPGEuGy+Dsy0meXDr6Sjty0b99kYgGh4uBC/8mJX1T0kC8d6S0aPP1Y+bHkh6aBw==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-darwin-x64": { + "version": "1.2.14", + "resolved": "https://registry.npmmirror.com/@rolldown/binding-darwin-x64/-/binding-darwin-x64-1.2.14.tgz", + "integrity": "sha512-hyMKEGatjmBYCs/Q4sbIcL/GqpFEqsX3wc+HYVQ/85A4rxc1o7rPm4DUI4GV4k/h0ITddwFSGpqQunI14GXjTA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-freebsd-x64": { + "version": "1.2.14", + "resolved": "https://registry.npmmirror.com/@rolldown/binding-freebsd-x64/-/binding-freebsd-x64-1.2.14.tgz", + "integrity": "sha512-u371GbXoVXrEgP20PwzoGNOdAOqAMhzo19JzzY+V4YmmnC+DTOoqdbMu+O+54emM59Mik/UdrtWgcuEDKWRPKA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-linux-arm-gnueabihf": { + "version": "1.2.14", + "resolved": "https://registry.npmmirror.com/@rolldown/binding-linux-arm-gnueabihf/-/binding-linux-arm-gnueabihf-1.2.14.tgz", + "integrity": "sha512-BqwKsdZQf6EqVWr8fv2MrDYzAvOZ7+NYFSJESRGs87fkseEhs0TU9TXgRJTkZoRRkxTizyEg1LKUXfU3/78uqg==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-linux-arm64-gnu": { + "version": "1.2.14", + "resolved": "https://registry.npmmirror.com/@rolldown/binding-linux-arm64-gnu/-/binding-linux-arm64-gnu-1.2.14.tgz", + "integrity": "sha512-SD9kY8M009x/7s57Z1dgUVlP5+uYBgKQK0Tkr1WIWngrtUJmRCW1RJFMY7QDla2khXhmg2i5Io7MUjcmZNZ/KQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-linux-arm64-musl": { + "version": "1.2.14", + "resolved": "https://registry.npmmirror.com/@rolldown/binding-linux-arm64-musl/-/binding-linux-arm64-musl-1.2.14.tgz", + "integrity": "sha512-BJ2CL19M6uuWesMgnqJzgxoI+N+eV7b0A9O9vLV42y+gitC41fNa8KElau1tHVzuLWX5GzEuxMPhAO0tQXQMkg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-linux-ppc64-gnu": { + "version": "1.2.14", + "resolved": "https://registry.npmmirror.com/@rolldown/binding-linux-ppc64-gnu/-/binding-linux-ppc64-gnu-1.2.14.tgz", + "integrity": "sha512-4+Kl8daPV8NDHaD7mfCcc1pRM+Qg7ab8DyMm5FG5WoZgv4h2i3KFN1CybYBNbQqmuYm7ftJLvmMI1eHYJ22S/Q==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-linux-s390x-gnu": { + "version": "1.2.14", + "resolved": "https://registry.npmmirror.com/@rolldown/binding-linux-s390x-gnu/-/binding-linux-s390x-gnu-1.2.14.tgz", + "integrity": "sha512-V/h8ePq1fKDrmtFMz6xcCN3DKvuvMRrwwD1q0PkBeeo4DpK64biaKvqWGVxqCeYKqeRk7gE7/iKPxLCn08O97A==", + "cpu": [ + "s390x" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-linux-x64-gnu": { + "version": "1.2.14", + "resolved": "https://registry.npmmirror.com/@rolldown/binding-linux-x64-gnu/-/binding-linux-x64-gnu-1.2.14.tgz", + "integrity": "sha512-WxalXzgrLVpKC93xfpaJ3gCXUWzeCr+FRCbDVD2mUAy4L6Vo3lNVz0epUEUpxPWJkLNYUwM4OSIikv36OJ9mLg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-linux-x64-musl": { + "version": "1.2.14", + "resolved": "https://registry.npmmirror.com/@rolldown/binding-linux-x64-musl/-/binding-linux-x64-musl-1.2.14.tgz", + "integrity": "sha512-3G3tnAmTpdk9jO3GQtyHXqeFD2SlecfTws/Y3M2mmj9nMBwwuLT4kur8sZKeu7ENUHSpVO3cIsm5A52LEWWwlg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-openharmony-arm64": { + "version": "1.2.14", + "resolved": "https://registry.npmmirror.com/@rolldown/binding-openharmony-arm64/-/binding-openharmony-arm64-1.2.14.tgz", + "integrity": "sha512-qyr5XiNGxm2ytx17vvtcJ9nXn6v1gENETi503bJx3OGwZ1tSonj7BosxbMa/xeAfSl5iS9X6KjEWYXLu6v1L7Q==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openharmony" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-win32-arm64-msvc": { + "version": "1.2.14", + "resolved": "https://registry.npmmirror.com/@rolldown/binding-win32-arm64-msvc/-/binding-win32-arm64-msvc-1.2.14.tgz", + "integrity": "sha512-oA4o5o0S2yB199YyYXDzKUv1Z56Rc+mgOjwm7Z/vJ9Z8igiDPYvoOuDyo6g7AK/ZaF09nPhDKVnGBcYHO11GXA==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-win32-x64-msvc": { + "version": "1.2.14", + "resolved": "https://registry.npmmirror.com/@rolldown/binding-win32-x64-msvc/-/binding-win32-x64-msvc-1.2.14.tgz", + "integrity": "sha512-9mE94bf2GZUaR/fUSXCMpBoWkctu421Mi0ORpm4zqfbkjKep7RxzPFa5etcGs5h+2KApoWBOA/hAJa/7saGgEQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/pluginutils": { + "version": "1.0.1", + "resolved": "https://registry.npmmirror.com/@rolldown/pluginutils/-/pluginutils-1.0.1.tgz", + "integrity": "sha512-2j9bGt5Jh8hj+vPtgzPtl72j0yRxHAyumoo6TNfAjsLB04UtpSvPbPcDcBMxz7n+9CYB0c1GxQFxYRg2jimqGw==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/node": { + "version": "24.19.2", + "resolved": "https://registry.npmmirror.com/@types/node/-/node-24.19.2.tgz", + "integrity": "sha512-93d49wCWJkCyN87MxLpNN6KXPCwFw7bVUg79mGUV1OS40JGWwVCr8n61DXCSqz7CV6/VfPkb7xec7uP2M+QHWQ==", + "dev": true, + "license": "MIT", + "peer": true, + "dependencies": { + "undici-types": ">=7.24.0 <7.24.7" + } + }, + "node_modules/@types/react": { + "version": "19.3.0", + "resolved": "https://registry.npmmirror.com/@types/react/-/react-19.3.0.tgz", + "integrity": "sha512-N0rFCuH9YoxG9/m61l9MfpJKfmLOVU0em7ipIz6TRgSSkvReLB9vL85GB+yr8Bs5leqpvg96JSwF4ZS1s4viQg==", + "dev": true, + "license": "MIT", + "peer": true, + "dependencies": { + "csstype": "^3.2.2" + } + }, + "node_modules/@types/react-dom": { + "version": "19.3.0", + "resolved": "https://registry.npmmirror.com/@types/react-dom/-/react-dom-19.3.0.tgz", + "integrity": "sha512-ZI7bU42mZXXKHn/qNLEw2IrbiINU7X5+vfgdixBHkCNpYWXjKgfQ/P+uyGb5CjOLB9UcnTeg3rylQtV2hym44Q==", + "dev": true, + "license": "MIT", + "peerDependencies": { + "@types/react": "^19.3.0" + } + }, + "node_modules/@vitejs/plugin-react": { + "version": "6.1.2", + "resolved": "https://registry.npmmirror.com/@vitejs/plugin-react/-/plugin-react-6.1.2.tgz", + "integrity": "sha512-fhJNGUdscQa4HkI0xFxwonWyhnVReCPDbXI7FH7jmzpnVCrFQI7PRrhWecO263610vOrs9tr2Y4xcf8uAdFxbw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@rolldown/pluginutils": "^1.0.1" + }, + "engines": { + "node": "^20.19.0 || >=22.12.0" + }, + "peerDependencies": { + "@rolldown/plugin-babel": "^0.1.7 || ^0.2.0", + "babel-plugin-react-compiler": "^1.0.0", + "oxc-transform-react": "^0.152.0", + "vite": "^8.0.0" + }, + "peerDependenciesMeta": { + "@rolldown/plugin-babel": { + "optional": true + }, + "babel-plugin-react-compiler": { + "optional": true + }, + "oxc-transform-react": { + "optional": true + } + } + }, + "node_modules/csstype": { + "version": "3.2.3", + "resolved": "https://registry.npmmirror.com/csstype/-/csstype-3.2.3.tgz", + "integrity": "sha512-z1HGKcYy2xA8AGQfwrn0PAy+PB7X/GSj3UVJW9qKyn43xWa+gl5nXmU4qqLMRzWVLFC8KusUX8T/0kCiOYpAIQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/detect-libc": { + "version": "2.1.2", + "resolved": "https://registry.npmmirror.com/detect-libc/-/detect-libc-2.1.2.tgz", + "integrity": "sha512-Btj2BOOO83o3WyH59e8MgXsxEQVcarkUOpEYrubB0urwnN10yQ364rsiByU11nZlqWYZm05i/of7io4mzihBtQ==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": ">=8" + } + }, + "node_modules/fdir": { + "version": "6.5.0", + "resolved": "https://registry.npmmirror.com/fdir/-/fdir-6.5.0.tgz", + "integrity": "sha512-tIbYtZbucOs0BRGqPJkshJUYdL+SDH7dVM8gjy+ERp3WAUjLEFJE+02kanyHtwjWOnwrKYBiwAmM0p4kLJAnXg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12.0.0" + }, + "peerDependencies": { + "picomatch": "^3 || ^4" + }, + "peerDependenciesMeta": { + "picomatch": { + "optional": true + } + } + }, + "node_modules/fsevents": { + "version": "2.3.3", + "resolved": "https://registry.npmmirror.com/fsevents/-/fsevents-2.3.3.tgz", + "integrity": "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==", + "dev": true, + "hasInstallScript": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": "^8.16.0 || ^10.6.0 || >=11.0.0" + } + }, + "node_modules/lightningcss": { + "version": "1.33.0", + "resolved": "https://registry.npmmirror.com/lightningcss/-/lightningcss-1.33.0.tgz", + "integrity": "sha512-WkUDrojuJs0xkgGf2udWxa3yGBRxPtxUkB79i6aCZLRgc7PM8fZe9TosfPDcvEpQZbuFASnHYmRLBLUbmLOIIA==", + "dev": true, + "license": "MPL-2.0", + "dependencies": { + "detect-libc": "^2.0.3" + }, + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + }, + "optionalDependencies": { + "lightningcss-android-arm64": "1.33.0", + "lightningcss-darwin-arm64": "1.33.0", + "lightningcss-darwin-x64": "1.33.0", + "lightningcss-freebsd-x64": "1.33.0", + "lightningcss-linux-arm-gnueabihf": "1.33.0", + "lightningcss-linux-arm64-gnu": "1.33.0", + "lightningcss-linux-arm64-musl": "1.33.0", + "lightningcss-linux-x64-gnu": "1.33.0", + "lightningcss-linux-x64-musl": "1.33.0", + "lightningcss-win32-arm64-msvc": "1.33.0", + "lightningcss-win32-x64-msvc": "1.33.0" + } + }, + "node_modules/lightningcss-android-arm64": { + "version": "1.33.0", + "resolved": "https://registry.npmmirror.com/lightningcss-android-arm64/-/lightningcss-android-arm64-1.33.0.tgz", + "integrity": "sha512-gEpRTalKdosp4Bb8qWtc2iOgE5SeIHlpS1up9bFq2wAyYhl1UdTObYiHe98zEM9SQvSoqQZ1IQD0JNpg3Ml5pg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MPL-2.0", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-darwin-arm64": { + "version": "1.33.0", + "resolved": "https://registry.npmmirror.com/lightningcss-darwin-arm64/-/lightningcss-darwin-arm64-1.33.0.tgz", + "integrity": "sha512-Sciaz8eenNTKn9b3t7+xr0ipTp9YxKQY4npwQ3mrRuL0BAVHBLyZxofhaKBAVtzmtRZ/zTyo0/to4B1uWG/Djg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MPL-2.0", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-darwin-x64": { + "version": "1.33.0", + "resolved": "https://registry.npmmirror.com/lightningcss-darwin-x64/-/lightningcss-darwin-x64-1.33.0.tgz", + "integrity": "sha512-Z5UPAxzrjlWNNyGy6i65cJzzvgJ5D3T6wMvs+gWpY9d7qRhANrxqAp6LhxIgZhWEw18RfJTGcRxjuLIBr+m8XQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MPL-2.0", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-freebsd-x64": { + "version": "1.33.0", + "resolved": "https://registry.npmmirror.com/lightningcss-freebsd-x64/-/lightningcss-freebsd-x64-1.33.0.tgz", + "integrity": "sha512-QQM/Ti/hQajJwCY+RiWuCZ9sdtI/XQk7nDK5vC8kkdwixezOlDgvDx7+RT+QjK6FcFT4MpsuoBnHIo/O3StRRg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MPL-2.0", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-linux-arm-gnueabihf": { + "version": "1.33.0", + "resolved": "https://registry.npmmirror.com/lightningcss-linux-arm-gnueabihf/-/lightningcss-linux-arm-gnueabihf-1.33.0.tgz", + "integrity": "sha512-N7FVBe6iS24MlM6R/4RBTxGhQheZGs7tiQ9U32UtF75NzP5Q7xWPRqLBCKxlRQRk3rY1jCIPLzx7WzOhuUIRLQ==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MPL-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-linux-arm64-gnu": { + "version": "1.33.0", + "resolved": "https://registry.npmmirror.com/lightningcss-linux-arm64-gnu/-/lightningcss-linux-arm64-gnu-1.33.0.tgz", + "integrity": "sha512-j2v/itmy4HlNxlc6voKXYgBqNi0Ng2LShg4z7GufpEgs05P+2suBVyi9I6YHq5uoVFx9ETin3eCEhLVyXGQnKg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MPL-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-linux-arm64-musl": { + "version": "1.33.0", + "resolved": "https://registry.npmmirror.com/lightningcss-linux-arm64-musl/-/lightningcss-linux-arm64-musl-1.33.0.tgz", + "integrity": "sha512-yiO5ROMuYQgXbC60yjZU5CYSFZGKXL0HFATXt9mHJn1+zW55oCtMI9NfcVhYLMFDL7gV7oBPon/EmMMGg2OvtQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MPL-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-linux-x64-gnu": { + "version": "1.33.0", + "resolved": "https://registry.npmmirror.com/lightningcss-linux-x64-gnu/-/lightningcss-linux-x64-gnu-1.33.0.tgz", + "integrity": "sha512-ar+Ju7LmcN0Jo4FpL4hpFybwNG9/3A/Br5KW2n2jyODg3MEZXaDYADdemoNS+BDNfMgKvylJLj4S5tyRActuAg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MPL-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-linux-x64-musl": { + "version": "1.33.0", + "resolved": "https://registry.npmmirror.com/lightningcss-linux-x64-musl/-/lightningcss-linux-x64-musl-1.33.0.tgz", + "integrity": "sha512-RYiYbkokw0trfKqqzfF55lginwEPrD3OJDfTuJzFs1MK6iFnDenaz1fqLLtX4ITG3OktJQXOeTaw1awrBAlZPw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MPL-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-win32-arm64-msvc": { + "version": "1.33.0", + "resolved": "https://registry.npmmirror.com/lightningcss-win32-arm64-msvc/-/lightningcss-win32-arm64-msvc-1.33.0.tgz", + "integrity": "sha512-1K+MPfLSFVpphzpdbfkhlWk6wBrTObBzS2T6db10PNOZgR9GoVsAWzwNyuhUYYbTp23j+4RrncfujZ4uAzXvwA==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MPL-2.0", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-win32-x64-msvc": { + "version": "1.33.0", + "resolved": "https://registry.npmmirror.com/lightningcss-win32-x64-msvc/-/lightningcss-win32-x64-msvc-1.33.0.tgz", + "integrity": "sha512-OlEICDx/Xl0FqSp4bry8zFnCvGpig3Gl4gCquvYwHuqJKEC1+n9NgDniFvqHGmMv1ZkqDJrDqKKSykTDX+ehuA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MPL-2.0", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/nanoid": { + "version": "3.3.20", + "resolved": "https://registry.npmmirror.com/nanoid/-/nanoid-3.3.20.tgz", + "integrity": "sha512-uKdg2G3GNCKQn9byYOpxbGqrT2fGO5KRt5J/8b3pok8rT6qxGWF6hxMyJiEYtAf+FVyYuD9hRaDqX5uPFYJ4ZQ==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "bin": { + "nanoid": "bin/nanoid.cjs" + }, + "engines": { + "node": "^10 || ^12 || ^13.7 || ^14 || >=15.0.1" + } + }, + "node_modules/oxlint": { + "version": "1.89.0", + "resolved": "https://registry.npmmirror.com/oxlint/-/oxlint-1.89.0.tgz", + "integrity": "sha512-a25hkfo61okGPvJz432fRIPoxlKtNnOVy9w4rzrmGgNm4iVTPwpSQlmdqNWnp6p9QyyPVfAZRgnf5XvSBZFk/A==", + "dev": true, + "license": "MIT", + "bin": { + "oxlint": "bin/oxlint" + }, + "engines": { + "node": "^20.19.0 || >=22.12.0" + }, + "funding": { + "url": "https://github.com/sponsors/oxc-project" + }, + "optionalDependencies": { + "@oxlint/binding-android-arm-eabi": "1.89.0", + "@oxlint/binding-android-arm64": "1.89.0", + "@oxlint/binding-darwin-arm64": "1.89.0", + "@oxlint/binding-darwin-x64": "1.89.0", + "@oxlint/binding-freebsd-x64": "1.89.0", + "@oxlint/binding-linux-arm-gnueabihf": "1.89.0", + "@oxlint/binding-linux-arm-musleabihf": "1.89.0", + "@oxlint/binding-linux-arm64-gnu": "1.89.0", + "@oxlint/binding-linux-arm64-musl": "1.89.0", + "@oxlint/binding-linux-ppc64-gnu": "1.89.0", + "@oxlint/binding-linux-riscv64-gnu": "1.89.0", + "@oxlint/binding-linux-riscv64-musl": "1.89.0", + "@oxlint/binding-linux-s390x-gnu": "1.89.0", + "@oxlint/binding-linux-x64-gnu": "1.89.0", + "@oxlint/binding-linux-x64-musl": "1.89.0", + "@oxlint/binding-openharmony-arm64": "1.89.0", + "@oxlint/binding-win32-arm64-msvc": "1.89.0", + "@oxlint/binding-win32-ia32-msvc": "1.89.0", + "@oxlint/binding-win32-x64-msvc": "1.89.0" + }, + "peerDependencies": { + "oxlint-tsgolint": ">=7.0.2003", + "vite-plus": "*" + }, + "peerDependenciesMeta": { + "oxlint-tsgolint": { + "optional": true + }, + "vite-plus": { + "optional": true + } + } + }, + "node_modules/picocolors": { + "version": "1.1.1", + "resolved": "https://registry.npmmirror.com/picocolors/-/picocolors-1.1.1.tgz", + "integrity": "sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==", + "dev": true, + "license": "ISC" + }, + "node_modules/picomatch": { + "version": "4.0.7", + "resolved": "https://registry.npmmirror.com/picomatch/-/picomatch-4.0.7.tgz", + "integrity": "sha512-qcJu88Q2IWqJsDD529JKMdwGm/dvInW4HvQnRwiH9JtihJvzGOscDtHE3x1pBKeUOTysQ8kVmLnJ2kJu7yhcGA==", + "dev": true, + "license": "MIT", + "peer": true, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/jonschlinkert" + } + }, + "node_modules/postcss": { + "version": "8.5.31", + "resolved": "https://registry.npmmirror.com/postcss/-/postcss-8.5.31.tgz", + "integrity": "sha512-YUbWYCgErwnMAVkjUnYa+KlMBzGpHwiIt/DE7f0G33AuX2LH9UcTk6KgWxd3zRU+pN3u/C+Xl3EnMLM4Hea7Sg==", + "dev": true, + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/postcss/" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/postcss" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "dependencies": { + "nanoid": "^3.3.20", + "picocolors": "^1.1.1", + "source-map-js": "^1.2.2" + }, + "engines": { + "node": "^10 || ^12 || >=14" + } + }, + "node_modules/react": { + "version": "19.3.1", + "resolved": "https://registry.npmmirror.com/react/-/react-19.3.1.tgz", + "integrity": "sha512-QNhWdt7nrgVWWQG58QuGsAR3jv3Hbe4mb4vz7w2csnHavciZJgda1CPNaldF6eVxv90ud3jUXpKlHL69gO38yw==", + "license": "MIT", + "peer": true, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/react-dom": { + "version": "19.3.1", + "resolved": "https://registry.npmmirror.com/react-dom/-/react-dom-19.3.1.tgz", + "integrity": "sha512-Y0bARz53lhEC1NMCASs0/j+faB/zqt3R8hpvg1e8+61ZVJ4UL0UdC4cBbXfZMcIuhrfgeILTLe+ZeBCavQPtKQ==", + "license": "MIT", + "dependencies": { + "scheduler": "^0.28.0" + }, + "peerDependencies": { + "react": "^19.3.1" + } + }, + "node_modules/rolldown": { + "version": "1.2.14", + "resolved": "https://registry.npmmirror.com/rolldown/-/rolldown-1.2.14.tgz", + "integrity": "sha512-eTBTTqMR6uHKLgcUqpJP8ntzDI6V4LYA8negAMBEI7gkgVvOdrx6VsvS2Ym+83KhOPdLCHWJu0w7UQFJZhHKNw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@oxc-project/types": "=0.154.0", + "@rolldown/pluginutils": "^1.0.0" + }, + "bin": { + "rolldown": "bin/cli.mjs" + }, + "engines": { + "node": "^20.19.0 || >=22.12.0" + }, + "optionalDependencies": { + "@rolldown/binding-android-arm-eabi": "1.2.14", + "@rolldown/binding-android-arm64": "1.2.14", + "@rolldown/binding-darwin-arm64": "1.2.14", + "@rolldown/binding-darwin-x64": "1.2.14", + "@rolldown/binding-freebsd-x64": "1.2.14", + "@rolldown/binding-linux-arm-gnueabihf": "1.2.14", + "@rolldown/binding-linux-arm64-gnu": "1.2.14", + "@rolldown/binding-linux-arm64-musl": "1.2.14", + "@rolldown/binding-linux-ppc64-gnu": "1.2.14", + "@rolldown/binding-linux-s390x-gnu": "1.2.14", + "@rolldown/binding-linux-x64-gnu": "1.2.14", + "@rolldown/binding-linux-x64-musl": "1.2.14", + "@rolldown/binding-openharmony-arm64": "1.2.14", + "@rolldown/binding-win32-arm64-msvc": "1.2.14", + "@rolldown/binding-win32-x64-msvc": "1.2.14" + } + }, + "node_modules/scheduler": { + "version": "0.28.0", + "resolved": "https://registry.npmmirror.com/scheduler/-/scheduler-0.28.0.tgz", + "integrity": "sha512-juorfCmIkIw8tT+p5BXSm6PJjQF/ycEYmKyzURCIt/RaZIhL+PulbQ9Yu2z1HdOJDdqDTlxA1+xKBmHXJsczAw==", + "license": "MIT" + }, + "node_modules/source-map-js": { + "version": "1.2.2", + "resolved": "https://registry.npmmirror.com/source-map-js/-/source-map-js-1.2.2.tgz", + "integrity": "sha512-KGj/8Y43x35aZVDtt+J4mK1hoLGHULMYfSkODJNQjNDC3oW1PqPoxMwo0pLUsWM/UEGzON/NxeHywEfNXNP3Vw==", + "dev": true, + "license": "BSD-3-Clause", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/tinyglobby": { + "version": "0.2.17", + "resolved": "https://registry.npmmirror.com/tinyglobby/-/tinyglobby-0.2.17.tgz", + "integrity": "sha512-wXR/dYpcqKmfWpEdZjiKJOwCNFndD0DMnrW/cYjVGttEkBfVgcLFHoNrlj47mjOVic9yyNu65alsgF4NQyTa2g==", + "dev": true, + "license": "MIT", + "dependencies": { + "fdir": "^6.5.0", + "picomatch": "^4.0.4" + }, + "engines": { + "node": ">=12.0.0" + }, + "funding": { + "url": "https://github.com/sponsors/SuperchupuDev" + } + }, + "node_modules/typescript": { + "version": "6.0.3", + "resolved": "https://registry.npmmirror.com/typescript/-/typescript-6.0.3.tgz", + "integrity": "sha512-y2TvuxSZPDyQakkFRPZHKFm+KKVqIisdg9/CZwm9ftvKXLP8NRWj38/ODjNbr43SsoXqNuAisEf1GdCxqWcdBw==", + "dev": true, + "license": "Apache-2.0", + "bin": { + "tsc": "bin/tsc", + "tsserver": "bin/tsserver" + }, + "engines": { + "node": ">=14.17" + } + }, + "node_modules/undici-types": { + "version": "7.24.6", + "resolved": "https://registry.npmmirror.com/undici-types/-/undici-types-7.24.6.tgz", + "integrity": "sha512-WRNW+sJgj5OBN4/0JpHFqtqzhpbnV0GuB+OozA9gCL7a993SmU+1JBZCzLNxYsbMfIeDL+lTsphD5jN5N+n0zg==", + "dev": true, + "license": "MIT" + }, + "node_modules/vite": { + "version": "8.3.4", + "resolved": "https://registry.npmmirror.com/vite/-/vite-8.3.4.tgz", + "integrity": "sha512-IftLE8phJhdNRIGNEV4JCCvfNScoZQVfcXaN8TQropWGJtOHcu+iAB9pHcuGvmEb/kddw0LBe/jS8ztdC6xdiQ==", + "dev": true, + "license": "MIT", + "peer": true, + "dependencies": { + "lightningcss": "^1.33.0", + "picomatch": "^4.0.7", + "postcss": "^8.5.29", + "rolldown": "~1.2.12", + "tinyglobby": "^0.2.17" + }, + "bin": { + "vite": "bin/vite.js" + }, + "engines": { + "node": "^20.19.0 || >=22.12.0" + }, + "funding": { + "url": "https://github.com/vitejs/vite?sponsor=1" + }, + "optionalDependencies": { + "fsevents": "~2.3.3" + }, + "peerDependencies": { + "@types/node": "^20.19.0 || >=22.12.0", + "@vitejs/devtools": "^0.7.1", + "esbuild": "^0.27.0 || ^0.28.0", + "jiti": ">=1.21.0", + "less": "^4.0.0", + "sass": "^1.70.0", + "sass-embedded": "^1.70.0", + "stylus": ">=0.54.8", + "sugarss": "^5.0.0", + "terser": "^5.16.0", + "tsx": "^4.8.1", + "yaml": "^2.4.2" + }, + "peerDependenciesMeta": { + "@types/node": { + "optional": true + }, + "@vitejs/devtools": { + "optional": true + }, + "esbuild": { + "optional": true + }, + "jiti": { + "optional": true + }, + "less": { + "optional": true + }, + "sass": { + "optional": true + }, + "sass-embedded": { + "optional": true + }, + "stylus": { + "optional": true + }, + "sugarss": { + "optional": true + }, + "terser": { + "optional": true + }, + "tsx": { + "optional": true + }, + "yaml": { + "optional": true + } + } + } + } +} diff --git a/stash-web/package.json b/stash-web/package.json new file mode 100644 index 0000000..37c7667 --- /dev/null +++ b/stash-web/package.json @@ -0,0 +1,29 @@ +{ + "name": "stash-web", + "private": true, + "version": "0.0.0", + "type": "module", + "scripts": { + "dev": "vite", + "build": "tsc -b && vite build", + "lint": "oxlint", + "preview": "vite preview" + }, + "dependencies": { + "react": "^19.3.1", + "react-dom": "^19.3.1" + }, + "devDependencies": { + "@types/node": "^24.19.2", + "@types/react": "^19.3.0", + "@types/react-dom": "^19.3.0", + "@vitejs/plugin-react": "^6.1.2", + "oxlint": "^1.88.0", + "typescript": "~6.0.3", + "vite": "^8.3.4" + }, + "volta": { + "node": "24.12.0", + "npm": "11.6.2" + } +} diff --git a/stash-web/public/favicon.svg b/stash-web/public/favicon.svg new file mode 100644 index 0000000..6893eb1 --- /dev/null +++ b/stash-web/public/favicon.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/stash-web/public/git_helper/git-flow.png b/stash-web/public/git_helper/git-flow.png new file mode 100644 index 0000000..2cf153c Binary files /dev/null and b/stash-web/public/git_helper/git-flow.png differ diff --git a/stash-web/public/git_helper/index.html b/stash-web/public/git_helper/index.html new file mode 100644 index 0000000..5122fc6 --- /dev/null +++ b/stash-web/public/git_helper/index.html @@ -0,0 +1,745 @@ + + + + + + Git 规范标准化与工作流生成器 + + + + +
+
+

Git 规范标准化生成器

+

企业级 Git 分支管理与 Commit 标准化生成工具

+
+ + + + + +
+
+
初始化项目 Commit 模版
+ +
+ + +
+ +
+ + +
+ +
+ + +
+ +
+ + +
+ +
+ + +
+
+ +
+
+ 生成的 Init Commit Message + +
+

+    
+
+ + +
+
+
1. 选择提交类型 (Type)
+
+ +
2. 填写提交内容
+
+
+ + +
+
+ + +
+
+ +
+ + +
+ +
3. 脚注选项 (Footer)
+
+
+ + +
+
+
+
+ + +
+
+ + +
+
+ +
+ +
+
+ +
+
+ 生成的 Commit Message + +
+

+    
+
+ + +
+
+
企业敏捷 Git 演进架构图 (原生高清预览)
+ + +
+ 企业敏捷 Git 拓扑流转图 +
+ +
选择分支类型生成 Git 命令
+
+ + + + + +
+ +
填写项目与人员配置
+
+
+ + +
+
+ + +
+
+ + +
+
+ + + +
+ 当前分支说明与规则: +
    +
  • 从 main 拉取并创建 develop 分支作为团队日常开发基线。
  • +
  • 执行命令:git checkout -b develop main
  • +
+
+
+ +
+
+ 生成的 Git 创建/切换分支命令 + +
+

+    
+
+
+ + + + + \ No newline at end of file diff --git a/stash-web/src/App.css b/stash-web/src/App.css new file mode 100644 index 0000000..e5b31e2 --- /dev/null +++ b/stash-web/src/App.css @@ -0,0 +1,939 @@ +/* ============ 基础 ============ */ + +.app { + height: 100%; + display: flex; + flex-direction: column; +} + +.card-box { + background-color: var(--card-bg); + border: 1px solid var(--border-color); + border-radius: var(--radius-main); + box-shadow: var(--shadow-card); +} + +/* 仅屏幕阅读器可见 */ +.sr-only { + position: absolute; + width: 1px; + height: 1px; + padding: 0; + margin: -1px; + overflow: hidden; + clip: rect(0, 0, 0, 0); + white-space: nowrap; + border: 0; +} + +/* ============ 顶栏 ============ */ + +.topbar { + position: fixed; + top: 0; + left: 0; + width: 100%; + height: var(--top-nav-height); + background-color: var(--card-bg); + border-bottom: 1px solid var(--border-color); + z-index: 200; +} + +/* 顶栏内容与下方 .layout 使用同一套宽度与左右内边距, + 这样 logo、右侧图标才能和侧栏、内容区严格对齐 */ +.topbar-inner { + width: 100%; + max-width: var(--layout-max-width); + height: 100%; + margin: 0 auto; + padding: 0 24px 0 20px; + display: flex; + align-items: center; + justify-content: space-between; + gap: 16px; +} + +.brand { + display: flex; + align-items: center; + gap: 10px; + width: 180px; + flex-shrink: 0; + font-size: 18px; + font-weight: 800; + color: #0f172a; +} + +.brand-mark { + width: 32px; + height: 32px; + border-radius: 50%; + background-color: #0f172a; + color: #fff; + display: flex; + align-items: center; + justify-content: center; + font-size: 16px; + font-weight: 700; +} + +.brand-name { + letter-spacing: 0.04em; +} + +/* 跑马灯 */ + +.ticker { + flex: 1; + max-width: 560px; + height: 36px; + margin: 0 20px; + overflow: hidden; + position: relative; + display: flex; + align-items: center; + -webkit-mask-image: linear-gradient( + to right, + transparent, + #000 10%, + #000 90%, + transparent + ); + mask-image: linear-gradient( + to right, + transparent, + #000 10%, + #000 90%, + transparent + ); +} + +.ticker-track { + display: flex; + align-items: center; + gap: 40px; + white-space: nowrap; + animation: ticker 26s linear infinite; +} + +.ticker:hover .ticker-track { + animation-play-state: paused; +} + +.ticker-item { + display: flex; + align-items: center; + gap: 6px; + font-size: 13.5px; + color: var(--text-sub); +} + +.ticker-tag { + padding: 2px 6px; + border-radius: 4px; + background-color: var(--primary-soft); + color: var(--primary-color); + font-size: 11px; + font-weight: 600; +} + +@keyframes ticker { + from { + transform: translateX(0); + } + to { + transform: translateX(-50%); + } +} + +/* 顶栏操作 */ + +.top-actions { + display: flex; + align-items: center; + gap: 12px; + flex-shrink: 0; +} + +.icon-button { + width: 34px; + height: 34px; + display: flex; + align-items: center; + justify-content: center; + padding: 6px; + border-radius: 50%; + color: #475569; + cursor: pointer; + transition: + background-color 0.2s, + color 0.2s; +} + +.icon-button:hover { + background-color: #f1f5f9; + color: var(--primary-color); +} + +.icon-button svg { + width: 19px; + height: 19px; + stroke: currentColor; + fill: none; + stroke-width: 2; + stroke-linecap: round; + stroke-linejoin: round; +} + +.icon-button[data-spinning='true'] svg, +.service-action[data-spinning='true'] svg { + animation: spin 0.9s linear infinite; +} + +@keyframes spin { + to { + transform: rotate(360deg); + } +} + +/* ============ 搜索框 ============ */ + +.search-row { + position: fixed; + top: 76px; + left: 50%; + transform: translateX(-50%); + width: min(480px, calc(100vw - 48px)); + height: 44px; + z-index: 100; +} + +.search-bar { + display: flex; + align-items: center; + gap: 10px; + height: 100%; + padding: 0 12px; +} + +.search-icon { + flex-shrink: 0; + width: 16px; + height: 16px; + color: var(--text-sub); + stroke: currentColor; + fill: none; + stroke-width: 2; + stroke-linecap: round; +} + +.search-input { + flex: 1; + min-width: 0; + height: 36px; + border: none; + outline: none; + background: transparent; + font-size: 13.5px; + color: var(--text-main); +} + +.search-input::placeholder { + color: var(--text-muted); +} + +.search-clear { + flex-shrink: 0; + width: 24px; + height: 24px; + display: flex; + align-items: center; + justify-content: center; + border-radius: 50%; + color: var(--text-muted); + cursor: pointer; + transition: + background-color 0.2s, + color 0.2s; +} + +.search-clear:hover { + background-color: #f1f5f9; + color: var(--text-main); +} + +.search-clear svg { + width: 13px; + height: 13px; + stroke: currentColor; + fill: none; + stroke-width: 2.4; + stroke-linecap: round; +} + +/* ============ 双列布局 ============ */ + +.layout { + width: 100%; + max-width: var(--layout-max-width); + /* 宽屏时整体居中,避免内容全部贴左 */ + margin: var(--top-nav-height) auto 0; + height: calc(100vh - var(--top-nav-height)); + padding: 36px 24px 0 20px; + display: grid; + grid-template-columns: var(--sidebar-width) minmax(0, 1fr); + gap: 20px; + align-items: stretch; +} + +/* 左侧栏 */ + +.sidebar { + display: flex; + flex-direction: column; + gap: 16px; + min-height: 0; + margin-top: 68px; +} + +/* 用户资料模块(数据占位,见 profile.ts) */ + +.profile-card { + display: flex; + flex-direction: column; + align-items: center; + gap: 6px; + padding: 18px 14px 14px; + text-align: center; + flex-shrink: 0; +} + +.profile-avatar { + position: relative; + width: 64px; + height: 64px; + margin-bottom: 4px; + border-radius: 50%; + display: grid; + place-items: center; + background: linear-gradient(135deg, #dbeafe, #bfdbfe); + color: #1d4ed8; +} + +.profile-avatar svg { + width: 34px; + height: 34px; + stroke: currentColor; + fill: none; + stroke-width: 1.8; + stroke-linecap: round; +} + +.profile-badge { + position: absolute; + right: -2px; + bottom: -2px; + width: 20px; + height: 20px; + border-radius: 50%; + border: 2px solid var(--card-bg); + background-color: #f59e0b; + color: #fff; + display: grid; + place-items: center; + font-size: 10px; + font-weight: 700; +} + +.profile-name { + font-size: 14px; + font-weight: 600; + color: var(--text-main); + line-height: 1.2; +} + +.profile-role { + padding: 1px 8px; + border: 1px solid var(--border-color); + border-radius: 999px; + font-size: 11px; + color: var(--text-sub); +} + +.profile-bio { + margin: 2px 0 0; + font-size: 11.5px; + line-height: 1.55; + color: var(--text-sub); +} + +.profile-location { + margin-top: 2px; + font-size: 11px; + color: var(--text-muted); +} + +.nav-card { + padding: 8px; + flex-shrink: 0; +} + +.nav-group { + list-style: none; +} + +.nav-item { + margin-bottom: 4px; +} + +.nav-item:last-child { + margin-bottom: 0; +} + +.nav-link { + width: 100%; + display: flex; + align-items: center; + justify-content: space-between; + gap: 8px; + padding: 9px 12px; + border-radius: 8px; + font-size: 13.5px; + font-weight: 500; + color: var(--text-sub); + cursor: pointer; + text-align: left; + transition: + background-color 0.2s, + color 0.2s; +} + +.nav-link:hover, +.nav-link.active { + background-color: var(--primary-soft); + color: var(--primary-color); +} + +.nav-count { + padding: 1px 6px; + border-radius: 10px; + background-color: #f1f5f9; + color: var(--text-muted); + font-size: 11px; + font-weight: 600; +} + +.nav-trailing { + display: flex; + align-items: center; + gap: 8px; + flex-shrink: 0; +} + +.nav-link.active .nav-count, +.nav-link:hover .nav-count { + background-color: #fff; + color: var(--primary-color); +} + +.nav-arrow { + color: var(--text-muted); + font-weight: 700; + line-height: 1; + transition: transform 0.3s; +} + +.nav-item.open > .nav-link .nav-arrow { + transform: rotate(90deg); +} + +.submenu { + list-style: none; + padding-left: 12px; + max-height: 0; + overflow: hidden; + transition: max-height 0.3s cubic-bezier(0, 1, 0, 1); +} + +.nav-item.open > .submenu { + max-height: 320px; + overflow-y: auto; + transition: max-height 0.3s ease-in-out; + scrollbar-width: thin; +} + +.submenu .nav-link { + padding: 7px 12px; + font-size: 12.5px; +} + +.submenu .nav-link > span:first-child { + overflow: hidden; + text-overflow: ellipsis; + white-space: nowrap; +} + +.nav-empty { + color: var(--text-muted); + cursor: default; + font-size: 12.5px; + padding: 7px 12px; + display: block; +} + +/* 本地服务面板:独立模块,不与导航项混排 */ + +.service-panel { + padding: 12px 14px; + flex-shrink: 0; +} + +.service-head { + display: flex; + align-items: center; + justify-content: space-between; + gap: 8px; +} + +.service-title { + font-size: 13px; + font-weight: 600; + color: var(--text-main); +} + +.service-pill { + display: inline-flex; + align-items: center; + gap: 6px; + padding: 2px 8px; + border-radius: 999px; + background-color: #f1f5f9; + font-size: 11px; + color: var(--text-sub); +} + +.service-pill.is-on { + background-color: rgba(34, 197, 94, 0.12); + color: #15803d; +} + +.service-stats { + display: grid; + gap: 6px; + margin-top: 10px; +} + +.service-stats > div { + display: flex; + align-items: center; + justify-content: space-between; + gap: 8px; + font-size: 12px; +} + +.service-stats dt { + flex-shrink: 0; + color: var(--text-sub); +} + +.service-stats dd { + margin: 0; + overflow: hidden; + font-family: var(--font-mono); + font-size: 11.5px; + font-weight: 600; + color: var(--text-main); + text-overflow: ellipsis; + white-space: nowrap; +} + +/* 面板底部操作:重新扫描 / 查看接口 */ +.service-actions { + display: flex; + gap: 8px; + margin-top: 12px; +} + +.service-action { + flex: 1; + display: inline-flex; + align-items: center; + justify-content: center; + gap: 5px; + padding: 6px 8px; + border: 1px solid var(--border-color); + border-radius: 8px; + background-color: var(--card-bg); + color: var(--text-sub); + font-size: 12px; + white-space: nowrap; + cursor: pointer; + transition: + border-color 0.2s, + color 0.2s, + background-color 0.2s; +} + +.service-action:not(:disabled):hover { + border-color: var(--primary-color); + background-color: var(--primary-soft); + color: var(--primary-color); +} + +.service-action:disabled { + opacity: 0.6; + cursor: default; +} + +.service-action svg { + width: 13px; + height: 13px; + flex-shrink: 0; + stroke: currentColor; + fill: none; + stroke-width: 2; + stroke-linecap: round; + stroke-linejoin: round; +} + +.status-dot { + width: 8px; + height: 8px; + border-radius: 50%; + background-color: #cbd5e1; + flex-shrink: 0; + transition: + background-color 0.2s, + box-shadow 0.2s; +} + +.status-dot.is-on { + background-color: #22c55e; + box-shadow: 0 0 0 3px rgba(34, 197, 94, 0.16); +} + +/* 右侧内容 */ + +.content { + display: flex; + flex-direction: column; + min-height: 0; + padding-top: 52px; +} + +.content-scroll { + flex: 1; + min-height: 0; + overflow-y: auto; + /* 顶部留出渐隐区,避免第一个分区标题被 mask 吃掉 */ + padding-top: 24px; + padding-right: 4px; + padding-bottom: 40px; + scroll-behavior: smooth; + -webkit-mask-image: linear-gradient( + to bottom, + transparent 0, + #000 22px, + #000 100% + ); + mask-image: linear-gradient(to bottom, transparent 0, #000 22px, #000 100%); + -ms-overflow-style: none; + scrollbar-width: none; +} + +.content-scroll::-webkit-scrollbar { + display: none; + width: 0; +} + +.section-heading { + display: flex; + align-items: baseline; + justify-content: space-between; + gap: 12px; + margin-bottom: 16px; +} + +.section-title { + display: flex; + align-items: center; + font-size: 15px; + font-weight: 600; + color: var(--text-main); + line-height: 1; +} + +.section-title::before { + content: ''; + width: 4px; + height: 15px; + margin-right: 8px; + border-radius: 2px; + background-color: var(--primary-color); +} + +/* 右上角数量角标:粉色渐变胶囊 */ +.section-badge { + flex-shrink: 0; + display: inline-flex; + align-items: center; + justify-content: center; + min-width: 26px; + height: 20px; + padding: 0 9px; + border-radius: 999px; + background-image: linear-gradient(90deg, #ff8a8a 0%, #f42a63 100%); + box-shadow: 0 2px 6px rgba(244, 42, 99, 0.28); + color: #fff; + font-size: 12px; + font-weight: 700; + line-height: 1; + letter-spacing: 0.02em; +} + +.section-group + .section-group { + margin-top: 28px; +} + +.grid { + display: grid; + grid-template-columns: repeat(auto-fill, minmax(220px, 1fr)); + gap: 16px; +} + +.card { + display: flex; + align-items: center; + padding: 12px 14px; + border: 1px solid var(--border-color); + border-radius: 10px; + background-color: var(--card-bg); + box-shadow: var(--shadow-card); + transition: + transform 0.2s, + box-shadow 0.2s, + border-color 0.2s; + animation: card-in 0.28s ease backwards; +} + +.card:hover { + transform: translateY(-2px); + border-color: rgba(59, 130, 246, 0.35); + box-shadow: var(--shadow-hover); +} + +.card-icon { + width: 36px; + height: 36px; + margin-right: 12px; + border-radius: 8px; + background-color: #f1f5f9; + color: var(--primary-color); + display: flex; + align-items: center; + justify-content: center; + font-weight: 600; + flex-shrink: 0; +} + +.card-body { + display: flex; + flex-direction: column; + min-width: 0; +} + +.card-title { + font-size: 14px; + font-weight: 600; + color: var(--text-main); + white-space: nowrap; + overflow: hidden; + text-overflow: ellipsis; +} + +.card-desc { + margin-top: 2px; + font-size: 11.5px; + color: var(--text-sub); + white-space: nowrap; + overflow: hidden; + text-overflow: ellipsis; +} + +/* 路径类描述用等宽字体,普通文案保持无衬线 */ +.card-desc.is-mono { + font-family: var(--font-mono); +} + +@keyframes card-in { + from { + opacity: 0; + transform: translateY(6px); + } + to { + opacity: 1; + transform: translateY(0); + } +} + +/* 空状态 / 错误 */ + +.notice { + display: flex; + flex-direction: column; + gap: 6px; + padding: 36px 24px; + border: 1px dashed var(--border-color); + border-radius: var(--radius-main); + background-color: rgba(255, 255, 255, 0.6); + color: var(--text-sub); + font-size: 13px; +} + +.notice strong { + font-size: 14px; + font-weight: 600; + color: var(--text-main); +} + +.notice code { + padding: 1px 5px; + border-radius: 5px; + background-color: #eef2f7; + font-family: var(--font-mono); + font-size: 12px; + color: var(--text-main); +} + +.notice-error { + border-style: solid; + border-color: #f3c6c6; + background-color: #fff7f7; +} + +.notice-error strong { + color: #b42318; +} + +.notice-action { + align-self: flex-start; + margin-top: 8px; + padding: 6px 14px; + border: 1px solid var(--border-color); + border-radius: 8px; + background-color: var(--card-bg); + font-size: 13px; + cursor: pointer; + transition: + border-color 0.2s, + color 0.2s; +} + +.notice-action:hover { + border-color: var(--primary-color); + color: var(--primary-color); +} + +/* 回到顶部 */ + +.back-to-top { + position: fixed; + right: 30px; + bottom: 30px; + width: 40px; + height: 40px; + display: flex; + align-items: center; + justify-content: center; + border-radius: 50%; + background-color: #e2e8f0; + color: #1e293b; + box-shadow: var(--shadow-card); + cursor: pointer; + z-index: 90; + transition: + background-color 0.2s, + transform 0.2s; +} + +.back-to-top:hover { + background-color: #cbd5e1; + transform: translateY(-2px); +} + +.back-to-top svg { + width: 18px; + height: 18px; + stroke: currentColor; + fill: none; + stroke-width: 2.5; + stroke-linecap: round; + stroke-linejoin: round; +} + +/* ============ 响应式 ============ */ + +@media (max-width: 1100px) { + :root { + --sidebar-width: 200px; + } +} + +@media (max-width: 900px) { + .app { + height: auto; + min-height: 100vh; + } + + .topbar { + position: sticky; + } + + .topbar-inner { + padding: 0 16px; + } + + .brand { + width: auto; + } + + .ticker { + display: none; + } + + .search-row { + position: static; + top: auto; + left: auto; + transform: none; + width: auto; + height: 44px; + margin: 16px 16px 0; + } + + .layout { + margin-top: 0; + height: auto; + padding: 16px; + grid-template-columns: minmax(0, 1fr); + } + + .sidebar { + display: none; + } + + .content { + padding-top: 0; + } + + .content-scroll { + overflow: visible; + -webkit-mask-image: none; + mask-image: none; + } + + .back-to-top { + right: 20px; + bottom: 20px; + } +} diff --git a/stash-web/src/App.tsx b/stash-web/src/App.tsx new file mode 100644 index 0000000..377589f --- /dev/null +++ b/stash-web/src/App.tsx @@ -0,0 +1,152 @@ +import { useMemo, useRef, useState } from 'react' +import { BackToTop } from './components/BackToTop' +import { SearchBar } from './components/SearchBar' +import { Sidebar } from './components/Sidebar' +import { SiteCard } from './components/SiteCard' +import { TopBar } from './components/TopBar' +import { MOCK_SECTIONS, type CatalogCard, type CatalogSection } from './catalog' +import { useProjects } from './useProjects' +import './App.css' + +function initialOf(name: string): string { + const [first] = Array.from(name.trim()) + return first ? first.toUpperCase() : '#' +} + +export default function App() { + const { status, projects, error, live, refreshing, refresh } = useProjects() + const [keyword, setKeyword] = useState('') + const searchRef = useRef(null) + + // 真实项目放最前面,占位分类跟在后面 + const sections = useMemo(() => { + const localCards: CatalogCard[] = projects.map((project) => ({ + id: project.route_path, + title: project.name, + desc: project.route_path, + href: project.route_path, + initial: initialOf(project.name), + mono: true, + })) + + return [{ id: 'local', title: '本地服务', cards: localCards }, ...MOCK_SECTIONS] + }, [projects]) + + const query = keyword.trim().toLowerCase() + const searching = query !== '' + + // 搜索时过滤卡片,并丢掉没有命中的分区 + const sectionsToRender = useMemo(() => { + if (!searching) return sections + + return sections + .map((section) => ({ + ...section, + cards: section.cards.filter( + (card) => + card.title.toLowerCase().includes(query) || + card.desc.toLowerCase().includes(query), + ), + })) + .filter((section) => section.cards.length > 0) + }, [sections, searching, query]) + + const noMatch = searching && sectionsToRender.length === 0 + + function renderLocalPlaceholder() { + if (status === 'error') { + return ( +
+ 连不上 Stash 服务 +

{error}

+

+ 请确认后端已启动(默认 127.0.0.1:8080),开发时 Vite 会把{' '} + /api 代理到后端。 +

+ +
+ ) + } + + if (status === 'loading') { + return ( +
+ 正在读取项目列表… +

首次加载会扫描一次托管目录。

+
+ ) + } + + return ( +
+ 这里还没有项目 +

+ 在托管根目录下创建子目录并放入 index.html,刷新后即可在这里看到它。 +

+
+ ) + } + + function renderSection(section: CatalogSection) { + return ( +
+
+

{section.title}

+ {section.cards.length} +
+ + {section.cards.length > 0 ? ( +
+ {section.cards.map((card, index) => ( + + ))} +
+ ) : ( + renderLocalPlaceholder() + )} +
+ ) + } + + return ( +
+ searchRef.current?.focus()} + /> + +
+ +
+ +
+ + +
+
+

Stash 本地应用工作区

+ + {sectionsToRender.map(renderSection)} + + {noMatch && ( +
+ 没有匹配「{keyword.trim()}」的内容 +

换个关键字试试,或清空搜索查看全部。

+
+ )} +
+
+
+ + +
+ ) +} diff --git a/stash-web/src/api.ts b/stash-web/src/api.ts new file mode 100644 index 0000000..d90ff29 --- /dev/null +++ b/stash-web/src/api.ts @@ -0,0 +1,69 @@ +export type Project = { + name: string + route_path: string +} + +type ProjectsPayload = { + projects?: Project[] +} + +const PROJECTS_ENDPOINT = '/api/projects' +const EVENTS_ENDPOINT = '/api/projects/events' + +/** 读取服务端扫描出的项目列表。 */ +export async function fetchProjects(signal?: AbortSignal): Promise { + const response = await fetch(PROJECTS_ENDPOINT, { + signal, + headers: { Accept: 'application/json' }, + }) + + if (!response.ok) { + throw new Error(`请求 ${PROJECTS_ENDPOINT} 失败:HTTP ${response.status}`) + } + + const payload = (await response.json()) as ProjectsPayload + return Array.isArray(payload.projects) ? payload.projects : [] +} + +export type ProjectEventsHandlers = { + onProjects: (projects: Project[]) => void + /** SSE 连接建立 / 断开时回调,浏览器的 EventSource 会自行重连。 */ + onConnectionChange?: (connected: boolean) => void +} + +/** + * 订阅服务端的项目快照推送。 + * 浏览器不支持 SSE 时静默降级为空清理函数,页面仍可用手动刷新。 + */ +export function subscribeProjects(handlers: ProjectEventsHandlers): () => void { + if (typeof EventSource === 'undefined') { + return () => {} + } + + const source = new EventSource(EVENTS_ENDPOINT) + + const handleProjects = (event: Event) => { + try { + const payload = JSON.parse((event as MessageEvent).data) as ProjectsPayload + if (Array.isArray(payload.projects)) { + handlers.onProjects(payload.projects) + } + } catch { + // 单条事件格式异常时保留旧数据,等待下一次推送 + } + } + + const handleOpen = () => handlers.onConnectionChange?.(true) + const handleError = () => handlers.onConnectionChange?.(false) + + source.addEventListener('projects', handleProjects) + source.addEventListener('open', handleOpen) + source.addEventListener('error', handleError) + + return () => { + source.removeEventListener('projects', handleProjects) + source.removeEventListener('open', handleOpen) + source.removeEventListener('error', handleError) + source.close() + } +} diff --git a/stash-web/src/catalog.ts b/stash-web/src/catalog.ts new file mode 100644 index 0000000..599966f --- /dev/null +++ b/stash-web/src/catalog.ts @@ -0,0 +1,130 @@ +/** + * 占位数据:沿用 index9 设计稿里的分类与卡片。 + * + * 后端目前只提供扁平的项目列表,没有分类概念, + * 所以这里的内容仅用于撑起界面结构,等接口支持分类后再替换。 + */ + +export type CatalogCard = { + id: string + title: string + desc: string + href: string + /** 卡片左侧的字符标记 */ + initial: string + /** desc 是路径时用等宽字体 */ + mono?: boolean +} + +export type CatalogSection = { + id: string + title: string + cards: CatalogCard[] +} + +export type NavLink = { + id: string + label: string +} + +export type NavGroup = { + id: string + label: string + links: NavLink[] +} + +/** 侧栏顶层链接 */ +export const MOCK_NAV_PRIMARY: NavLink[] = [{ id: 'common', label: '常用推荐' }] + +/** 侧栏可折叠分组 */ +export const MOCK_NAV_GROUPS: NavGroup[] = [ + { + id: 'assets', + label: '素材资源', + links: [ + { id: 'design', label: 'UI 设计资源' }, + { id: 'icon', label: '矢量图标' }, + { id: 'photo', label: '免费壁纸' }, + ], + }, + { + id: 'tools', + label: '开发工具', + links: [ + { id: 'dev', label: '前端调试工具' }, + { id: 'code', label: '在线编辑器' }, + ], + }, +] + +/** 内容区占位分区,顺序与设计稿一致 */ +export const MOCK_SECTIONS: CatalogSection[] = [ + { + id: 'common', + title: '常用推荐', + cards: [ + { + id: 'github', + title: 'GitHub', + desc: '代码托管与开源社区', + href: '#', + initial: 'G', + }, + { id: 'zhihu', title: '知乎', desc: '深度内容问答社区', href: '#', initial: 'Z' }, + { + id: 'bilibili', + title: '哔哩哔哩', + desc: '弹幕视频分享网站', + href: '#', + initial: 'B', + }, + { + id: 'v2ex', + title: 'V2EX', + desc: '创意工作者讨论社区', + href: '#', + initial: 'V', + }, + ], + }, + { + id: 'dev', + title: '前端调试工具', + cards: [ + { + id: 'stackoverflow', + title: 'Stack Overflow', + desc: '程序员技术问答', + href: '#', + initial: 'S', + }, + { id: 'juejin', title: '掘金', desc: '开发者技术社区', href: '#', initial: 'J' }, + ], + }, + { + id: 'code', + title: '在线编辑器', + cards: [ + { + id: 'codepen', + title: 'CodePen', + desc: '前端在线代码演练场', + href: '#', + initial: 'C', + }, + ], + }, + { + id: 'design', + title: 'UI 设计资源', + cards: [ + { + id: 'dribbble', + title: 'Dribbble', + desc: '全球设计作品分享社区', + href: '#', + initial: 'D', + }, + ], + }, +] diff --git a/stash-web/src/components/BackToTop.tsx b/stash-web/src/components/BackToTop.tsx new file mode 100644 index 0000000..e7b4d8f --- /dev/null +++ b/stash-web/src/components/BackToTop.tsx @@ -0,0 +1,22 @@ +export function BackToTop() { + const handleClick = () => { + // 桌面端滚动的是内容容器,窄屏滚动的是窗口 + document.getElementById('content-scroll')?.scrollTo({ top: 0, behavior: 'smooth' }) + window.scrollTo({ top: 0, behavior: 'smooth' }) + } + + return ( + + ) +} diff --git a/stash-web/src/components/ProfileCard.tsx b/stash-web/src/components/ProfileCard.tsx new file mode 100644 index 0000000..593401f --- /dev/null +++ b/stash-web/src/components/ProfileCard.tsx @@ -0,0 +1,23 @@ +import { PLACEHOLDER_PROFILE } from '../profile' + +/** 登录后展示的用户资料卡(数据为占位,见 profile.ts)。 */ +export function ProfileCard() { + const { name, role, bio, location } = PLACEHOLDER_PROFILE + + return ( +
+ + +
{name}
+ {role} +

{bio}

+
IP属地:{location}
+
+ ) +} diff --git a/stash-web/src/components/SearchBar.tsx b/stash-web/src/components/SearchBar.tsx new file mode 100644 index 0000000..7a5fa8b --- /dev/null +++ b/stash-web/src/components/SearchBar.tsx @@ -0,0 +1,58 @@ +import { useEffect, type RefObject } from 'react' + +type SearchBarProps = { + value: string + onChange: (value: string) => void + inputRef: RefObject +} + +export function SearchBar({ value, onChange, inputRef }: SearchBarProps) { + useEffect(() => { + const handleKeyDown = (event: KeyboardEvent) => { + if ((event.ctrlKey || event.metaKey) && event.key.toLowerCase() === 'k') { + event.preventDefault() + inputRef.current?.focus() + inputRef.current?.select() + } + } + + window.addEventListener('keydown', handleKeyDown) + return () => window.removeEventListener('keydown', handleKeyDown) + }, [inputRef]) + + return ( +
+ + + onChange(event.target.value)} + onKeyDown={(event) => { + if (event.key === 'Escape') onChange('') + }} + /> + + {value !== '' && ( + + )} +
+ ) +} diff --git a/stash-web/src/components/ServicePanel.tsx b/stash-web/src/components/ServicePanel.tsx new file mode 100644 index 0000000..824f399 --- /dev/null +++ b/stash-web/src/components/ServicePanel.tsx @@ -0,0 +1,65 @@ +import type { Project } from '../api' +import { LinkIcon, RefreshIcon } from './icons' + +type ServicePanelProps = { + projects: Project[] + live: boolean + refreshing: boolean + onRefresh: () => void +} + +/** + * 本地服务面板:状态概览 + 操作入口。 + * 项目列表本身在右侧内容区以卡片呈现,这里不再展开。 + */ +export function ServicePanel({ projects, live, refreshing, onRefresh }: ServicePanelProps) { + const host = typeof window === 'undefined' ? '-' : window.location.host + + return ( +
+
+

本地服务

+ + +
+ +
+
+
项目数
+
{projects.length}
+
+
+
访问地址
+
{host}
+
+
+ +
+ + + + + 查看接口 + +
+
+ ) +} diff --git a/stash-web/src/components/Sidebar.tsx b/stash-web/src/components/Sidebar.tsx new file mode 100644 index 0000000..09dead0 --- /dev/null +++ b/stash-web/src/components/Sidebar.tsx @@ -0,0 +1,84 @@ +import { useState } from 'react' +import type { Project } from '../api' +import { MOCK_NAV_GROUPS, MOCK_NAV_PRIMARY } from '../catalog' +import { ProfileCard } from './ProfileCard' +import { ServicePanel } from './ServicePanel' + +type SidebarProps = { + projects: Project[] + live: boolean + refreshing: boolean + onRefresh: () => void +} + +export function Sidebar({ projects, live, refreshing, onRefresh }: SidebarProps) { + const [openGroups, setOpenGroups] = useState([]) + const [activeId, setActiveId] = useState('common') + + const toggle = (id: string) => { + setOpenGroups((previous) => + previous.includes(id) ? previous.filter((item) => item !== id) : [...previous, id], + ) + } + + const isOpen = (id: string) => openGroups.includes(id) + + return ( + + ) +} diff --git a/stash-web/src/components/SiteCard.tsx b/stash-web/src/components/SiteCard.tsx new file mode 100644 index 0000000..f7db7c0 --- /dev/null +++ b/stash-web/src/components/SiteCard.tsx @@ -0,0 +1,26 @@ +import type { CatalogCard } from '../catalog' + +type SiteCardProps = { + card: CatalogCard + index: number +} + +export function SiteCard({ card, index }: SiteCardProps) { + return ( + + + + {card.title} + {card.desc} + + + ) +} diff --git a/stash-web/src/components/TopBar.tsx b/stash-web/src/components/TopBar.tsx new file mode 100644 index 0000000..d430298 --- /dev/null +++ b/stash-web/src/components/TopBar.tsx @@ -0,0 +1,89 @@ +import { useMemo } from 'react' +import type { Project } from '../api' +import { LoginIcon, SearchIcon } from './icons' + +type TopBarProps = { + projects: Project[] + live: boolean + onFocusSearch: () => void +} + +type TickerItem = { + tag: string + text: string +} + +export function TopBar({ projects, live, onFocusSearch }: TopBarProps) { + return ( +
+
+ + + STASH + + + + +
+ + {/* TODO(auth): 后端登录接口就绪后接上,目前仅作入口占位 */} + +
+
+
+ ) +} + +/** 顶栏跑马灯:展示连接状态、项目数量与前几个项目名。 */ +function Ticker({ projects, live }: { projects: Project[]; live: boolean }) { + const items = useMemo(() => buildTickerItems(projects, live), [projects, live]) + + // 复制一份实现无缝循环,动画位移为 -50% + const loop = [...items, ...items] + + return ( + + ) +} + +function buildTickerItems(projects: Project[], live: boolean): TickerItem[] { + const items: TickerItem[] = [ + { tag: '状态', text: live ? '实时同步已连接' : '等待服务连接' }, + { tag: '项目', text: `共 ${projects.length} 个本地应用` }, + ] + + if (projects.length === 0) { + items.push({ + tag: '提示', + text: '把静态站点放进托管目录,并保留 index.html,就会出现在这里', + }) + return items + } + + for (const project of projects.slice(0, 6)) { + items.push({ tag: '本地', text: project.name }) + } + + return items +} diff --git a/stash-web/src/components/icons.tsx b/stash-web/src/components/icons.tsx new file mode 100644 index 0000000..b2c4aea --- /dev/null +++ b/stash-web/src/components/icons.tsx @@ -0,0 +1,37 @@ +/** 统一的小图标集合:只负责路径,尺寸与颜色由使用处的 CSS 决定。 */ + +export function RefreshIcon() { + return ( + + ) +} + +export function SearchIcon() { + return ( + + ) +} + +export function LinkIcon() { + return ( + + ) +} + +export function LoginIcon() { + return ( + + ) +} diff --git a/stash-web/src/index.css b/stash-web/src/index.css new file mode 100644 index 0000000..a7de93e --- /dev/null +++ b/stash-web/src/index.css @@ -0,0 +1,82 @@ +:root { + --bg-color: #f7f9fb; + --card-bg: #ffffff; + --primary-color: #3b82f6; + --primary-soft: #eff6ff; + --text-main: #1e293b; + --text-sub: #64748b; + --text-muted: #94a3b8; + --border-color: #e2e8f0; + --radius-main: 12px; + --shadow-card: 0 4px 12px rgba(0, 0, 0, 0.03), 0 1px 3px rgba(0, 0, 0, 0.02); + --shadow-hover: 0 10px 20px rgba(0, 0, 0, 0.06), 0 4px 6px rgba(0, 0, 0, 0.03); + + --sidebar-width: 240px; + --top-nav-height: 60px; + /* 内容区最大宽度,宽屏时整体居中 */ + --layout-max-width: 1280px; + + --font-sans: + -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, 'Helvetica Neue', + 'PingFang SC', 'Microsoft YaHei', Arial, sans-serif; + --font-mono: ui-monospace, SFMono-Regular, Consolas, 'Liberation Mono', monospace; + + font-family: var(--font-sans); + font-size: 14px; + line-height: 1.5; + color: var(--text-main); + background-color: var(--bg-color); + color-scheme: light; + -webkit-font-smoothing: antialiased; + -moz-osx-font-smoothing: grayscale; +} + +* { + box-sizing: border-box; + margin: 0; + padding: 0; +} + +html, +body, +#root { + height: 100%; +} + +/* 桌面端锁死视口,滚动交给右侧内容区 */ +body { + height: 100vh; + overflow: hidden; + background-color: var(--bg-color); + color: var(--text-main); +} + +a { + color: inherit; + text-decoration: none; +} + +button { + font: inherit; + color: inherit; + border: none; + background: none; +} + +:focus-visible { + outline: 2px solid var(--primary-color); + outline-offset: 2px; +} + +@media (max-width: 900px) { + html, + body, + #root { + height: auto; + } + + body { + height: auto; + overflow: auto; + } +} diff --git a/stash-web/src/main.tsx b/stash-web/src/main.tsx new file mode 100644 index 0000000..bef5202 --- /dev/null +++ b/stash-web/src/main.tsx @@ -0,0 +1,10 @@ +import { StrictMode } from 'react' +import { createRoot } from 'react-dom/client' +import './index.css' +import App from './App.tsx' + +createRoot(document.getElementById('root')!).render( + + + , +) diff --git a/stash-web/src/profile.ts b/stash-web/src/profile.ts new file mode 100644 index 0000000..9e36bd2 --- /dev/null +++ b/stash-web/src/profile.ts @@ -0,0 +1,19 @@ +/** + * 登录后展示的用户模块数据。 + * + * 后端目前没有鉴权 / 用户信息接口,这里先用占位数据, + * 等类似 `/api/me` 的接口就绪后替换为真实数据即可。 + */ +export type UserProfile = { + name: string + role: string + bio: string + location: string +} + +export const PLACEHOLDER_PROFILE: UserProfile = { + name: '本地管理员', + role: '管理员', + bio: '本地静态站点托管与预览,所有项目都运行在这台机器上。', + location: '局域网', +} diff --git a/stash-web/src/useProjects.ts b/stash-web/src/useProjects.ts new file mode 100644 index 0000000..d7dfac2 --- /dev/null +++ b/stash-web/src/useProjects.ts @@ -0,0 +1,92 @@ +import { useCallback, useEffect, useRef, useState } from 'react' +import { fetchProjects, subscribeProjects, type Project } from './api' + +export type ProjectsStatus = 'loading' | 'ready' | 'error' + +export type ProjectsState = { + status: ProjectsStatus + projects: Project[] + error: string | null +} + +export type ProjectsController = ProjectsState & { + /** SSE 是否已连接 */ + live: boolean + /** 是否正在手动刷新 */ + refreshing: boolean + refresh: () => void +} + +function toErrorMessage(error: unknown): string { + return error instanceof Error ? error.message : String(error) +} + +/** 刷新失败时保留上一份列表,避免页面突然清空。 */ +function toErrorState(previous: ProjectsState, error: unknown): ProjectsState { + return { status: 'error', projects: previous.projects, error: toErrorMessage(error) } +} + +/** 项目列表:首屏拉取 + SSE 推送 + 手动刷新。 */ +export function useProjects(): ProjectsController { + const [state, setState] = useState({ + status: 'loading', + projects: [], + error: null, + }) + const [live, setLive] = useState(false) + const [refreshing, setRefreshing] = useState(false) + const controllerRef = useRef(null) + + // 首屏加载:setState 只发生在 Promise 回调里。 + // 不在 effect 内同步 setState,否则会触发 react-hooks/set-state-in-effect。 + useEffect(() => { + const controller = new AbortController() + controllerRef.current = controller + + fetchProjects(controller.signal) + .then((projects) => { + if (!controller.signal.aborted) { + setState({ status: 'ready', projects, error: null }) + } + }) + .catch((error: unknown) => { + if (!controller.signal.aborted) { + setState((previous) => toErrorState(previous, error)) + } + }) + + return () => controller.abort() + }, []) + + // 服务端已做防抖与去重,这里直接整体替换快照。 + useEffect(() => { + return subscribeProjects({ + onProjects: (projects) => setState({ status: 'ready', projects, error: null }), + onConnectionChange: setLive, + }) + }, []) + + const refresh = useCallback(() => { + controllerRef.current?.abort() + const controller = new AbortController() + controllerRef.current = controller + + setRefreshing(true) + fetchProjects(controller.signal) + .then((projects) => { + if (!controller.signal.aborted) { + setState({ status: 'ready', projects, error: null }) + } + }) + .catch((error: unknown) => { + if (!controller.signal.aborted) { + setState((previous) => toErrorState(previous, error)) + } + }) + .finally(() => { + if (!controller.signal.aborted) setRefreshing(false) + }) + }, []) + + return { ...state, live, refreshing, refresh } +} diff --git a/stash-web/tsconfig.app.json b/stash-web/tsconfig.app.json new file mode 100644 index 0000000..6830b6f --- /dev/null +++ b/stash-web/tsconfig.app.json @@ -0,0 +1,26 @@ +{ + "compilerOptions": { + "tsBuildInfoFile": "./node_modules/.tmp/tsconfig.app.tsbuildinfo", + "target": "es2023", + "lib": ["ES2023", "DOM"], + "module": "esnext", + "types": ["vite/client"], + "allowArbitraryExtensions": true, + "skipLibCheck": true, + + /* Bundler mode */ + "moduleResolution": "bundler", + "allowImportingTsExtensions": true, + "verbatimModuleSyntax": true, + "moduleDetection": "force", + "noEmit": true, + "jsx": "react-jsx", + + /* Linting */ + "noUnusedLocals": true, + "noUnusedParameters": true, + "erasableSyntaxOnly": true, + "noFallthroughCasesInSwitch": true + }, + "include": ["src"] +} diff --git a/stash-web/tsconfig.json b/stash-web/tsconfig.json new file mode 100644 index 0000000..1ffef60 --- /dev/null +++ b/stash-web/tsconfig.json @@ -0,0 +1,7 @@ +{ + "files": [], + "references": [ + { "path": "./tsconfig.app.json" }, + { "path": "./tsconfig.node.json" } + ] +} diff --git a/stash-web/tsconfig.node.json b/stash-web/tsconfig.node.json new file mode 100644 index 0000000..8455dcb --- /dev/null +++ b/stash-web/tsconfig.node.json @@ -0,0 +1,23 @@ +{ + "compilerOptions": { + "tsBuildInfoFile": "./node_modules/.tmp/tsconfig.node.tsbuildinfo", + "target": "es2023", + "lib": ["ES2023"], + "types": ["node"], + "skipLibCheck": true, + + /* Bundler mode */ + "module": "nodenext", + "allowImportingTsExtensions": true, + "verbatimModuleSyntax": true, + "moduleDetection": "force", + "noEmit": true, + + /* Linting */ + "noUnusedLocals": true, + "noUnusedParameters": true, + "erasableSyntaxOnly": true, + "noFallthroughCasesInSwitch": true + }, + "include": ["vite.config.ts"] +} diff --git a/stash-web/vite.config.ts b/stash-web/vite.config.ts new file mode 100644 index 0000000..957eb68 --- /dev/null +++ b/stash-web/vite.config.ts @@ -0,0 +1,25 @@ +import react from '@vitejs/plugin-react' +import { defineConfig } from 'vite' + +const BACKEND = 'http://127.0.0.1:8080' + +// 前后端分离开发:dev / preview 都把 /api 透传给本地 Rust 服务 +export default defineConfig({ + plugins: [react()], + server: { + port: 5173, + proxy: { + '/api': { target: BACKEND, changeOrigin: true }, + }, + }, + preview: { + port: 4173, + proxy: { + '/api': { target: BACKEND, changeOrigin: true }, + }, + }, + build: { + outDir: 'dist', + emptyOutDir: true, + }, +}) diff --git a/stash-core/templates/404.html b/templates/404.html similarity index 89% rename from stash-core/templates/404.html rename to templates/404.html index c2441a5..462d4d2 100644 --- a/stash-core/templates/404.html +++ b/templates/404.html @@ -11,7 +11,7 @@
404

资源或页面不存在

请求的路径 {{ path }} 不存在或缺少 index.html

- 返回应用大厅 + 返回应用大厅 diff --git a/stash-core/templates/index.html b/templates/index.html similarity index 98% rename from stash-core/templates/index.html rename to templates/index.html index edbe1c6..380452e 100644 --- a/stash-core/templates/index.html +++ b/templates/index.html @@ -16,7 +16,7 @@