mirror of
https://github.com/infinilabs/coco-app.git
synced 2025-12-14 18:47:42 +01:00
feat: add search-chat
This commit is contained in:
12
.editorconfig
Normal file
12
.editorconfig
Normal file
@@ -0,0 +1,12 @@
|
||||
# EditorConfig is awesome: https://EditorConfig.org
|
||||
|
||||
# top-most EditorConfig file
|
||||
root = true
|
||||
|
||||
[*]
|
||||
indent_style = space
|
||||
indent_size = 2
|
||||
end_of_line = lf
|
||||
charset = utf-8
|
||||
trim_trailing_whitespace = false
|
||||
insert_final_newline = false
|
||||
@@ -4,7 +4,7 @@
|
||||
<meta charset="UTF-8" />
|
||||
<link rel="icon" type="image/svg+xml" href="/vite.svg" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||
<title>Tauri + React + Typescript</title>
|
||||
<title>Coco</title>
|
||||
</head>
|
||||
|
||||
<body>
|
||||
|
||||
23
package.json
23
package.json
@@ -10,17 +10,32 @@
|
||||
"tauri": "tauri"
|
||||
},
|
||||
"dependencies": {
|
||||
"@headlessui/react": "^2.1.10",
|
||||
"@tauri-apps/api": ">=2.0.0",
|
||||
"@tauri-apps/plugin-http": "~2",
|
||||
"@tauri-apps/plugin-shell": ">=2.0.0",
|
||||
"clsx": "^2.1.1",
|
||||
"i18next": "^23.16.2",
|
||||
"lodash": "^4.17.21",
|
||||
"lucide-react": "^0.453.0",
|
||||
"react": "^18.2.0",
|
||||
"react-dom": "^18.2.0",
|
||||
"@tauri-apps/api": ">=2.0.0",
|
||||
"@tauri-apps/plugin-shell": ">=2.0.0"
|
||||
"react-hotkeys-hook": "^4.5.1",
|
||||
"react-i18next": "^15.1.0",
|
||||
"react-router-dom": "^6.27.0",
|
||||
"zustand": "^5.0.0"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@tauri-apps/cli": ">=2.0.0",
|
||||
"@types/lodash": "^4.17.12",
|
||||
"@types/react": "^18.2.15",
|
||||
"@types/react-dom": "^18.2.7",
|
||||
"@types/react-i18next": "^8.1.0",
|
||||
"@vitejs/plugin-react": "^4.2.1",
|
||||
"autoprefixer": "^10.4.20",
|
||||
"postcss": "^8.4.47",
|
||||
"tailwindcss": "^3.4.14",
|
||||
"typescript": "^5.2.2",
|
||||
"vite": "^5.3.1",
|
||||
"@tauri-apps/cli": ">=2.0.0"
|
||||
"vite": "^5.3.1"
|
||||
}
|
||||
}
|
||||
|
||||
1172
pnpm-lock.yaml
generated
1172
pnpm-lock.yaml
generated
File diff suppressed because it is too large
Load Diff
6
postcss.config.js
Normal file
6
postcss.config.js
Normal file
@@ -0,0 +1,6 @@
|
||||
export default {
|
||||
plugins: {
|
||||
tailwindcss: {},
|
||||
autoprefixer: {},
|
||||
},
|
||||
}
|
||||
380
src-tauri/Cargo.lock
generated
380
src-tauri/Cargo.lock
generated
@@ -85,6 +85,12 @@ dependencies = [
|
||||
"system-deps",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "atomic-waker"
|
||||
version = "1.1.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "1505bd5d3d116872e7271a6d4e16d81d0c8570876c8de68093a09ac269d8aac0"
|
||||
|
||||
[[package]]
|
||||
name = "autocfg"
|
||||
version = "1.4.0"
|
||||
@@ -341,6 +347,7 @@ dependencies = [
|
||||
"serde_json",
|
||||
"tauri",
|
||||
"tauri-build",
|
||||
"tauri-plugin-http",
|
||||
"tauri-plugin-shell",
|
||||
]
|
||||
|
||||
@@ -353,7 +360,7 @@ dependencies = [
|
||||
"bitflags 2.6.0",
|
||||
"block",
|
||||
"cocoa-foundation",
|
||||
"core-foundation",
|
||||
"core-foundation 0.10.0",
|
||||
"core-graphics",
|
||||
"foreign-types",
|
||||
"libc",
|
||||
@@ -368,7 +375,7 @@ checksum = "e14045fb83be07b5acf1c0884b2180461635b433455fa35d1cd6f17f1450679d"
|
||||
dependencies = [
|
||||
"bitflags 2.6.0",
|
||||
"block",
|
||||
"core-foundation",
|
||||
"core-foundation 0.10.0",
|
||||
"core-graphics-types",
|
||||
"libc",
|
||||
"objc",
|
||||
@@ -390,6 +397,44 @@ version = "0.4.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "6245d59a3e82a7fc217c5828a6692dbc6dfb63a0c8c90495621f7b9d79704a0e"
|
||||
|
||||
[[package]]
|
||||
name = "cookie"
|
||||
version = "0.18.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "4ddef33a339a91ea89fb53151bd0a4689cfce27055c291dfa69945475d22c747"
|
||||
dependencies = [
|
||||
"percent-encoding",
|
||||
"time",
|
||||
"version_check",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "cookie_store"
|
||||
version = "0.21.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "4934e6b7e8419148b6ef56950d277af8561060b56afd59e2aadf98b59fce6baa"
|
||||
dependencies = [
|
||||
"cookie",
|
||||
"idna 0.5.0",
|
||||
"log",
|
||||
"publicsuffix",
|
||||
"serde",
|
||||
"serde_derive",
|
||||
"serde_json",
|
||||
"time",
|
||||
"url",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "core-foundation"
|
||||
version = "0.9.4"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "91e195e091a93c46f7102ec7818a2aa394e1e1771c3ab4825963fa03e45afb8f"
|
||||
dependencies = [
|
||||
"core-foundation-sys",
|
||||
"libc",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "core-foundation"
|
||||
version = "0.10.0"
|
||||
@@ -413,7 +458,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "fa95a34622365fa5bbf40b20b75dba8dfa8c94c734aea8ac9a5ca38af14316f1"
|
||||
dependencies = [
|
||||
"bitflags 2.6.0",
|
||||
"core-foundation",
|
||||
"core-foundation 0.10.0",
|
||||
"core-graphics-types",
|
||||
"foreign-types",
|
||||
"libc",
|
||||
@@ -426,7 +471,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "3d44a101f213f6c4cdc1853d4b78aef6db6bdfa3468798cc1d9912f4735013eb"
|
||||
dependencies = [
|
||||
"bitflags 2.6.0",
|
||||
"core-foundation",
|
||||
"core-foundation 0.10.0",
|
||||
"libc",
|
||||
]
|
||||
|
||||
@@ -545,6 +590,12 @@ dependencies = [
|
||||
"syn 2.0.79",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "data-url"
|
||||
version = "0.3.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "5c297a1c74b71ae29df00c3e22dd9534821d60eb9af5a0192823fa2acea70c2a"
|
||||
|
||||
[[package]]
|
||||
name = "deranged"
|
||||
version = "0.3.11"
|
||||
@@ -1165,6 +1216,25 @@ dependencies = [
|
||||
"syn 2.0.79",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "h2"
|
||||
version = "0.4.6"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "524e8ac6999421f49a846c2d4411f337e53497d8ec55d67753beffa43c5d9205"
|
||||
dependencies = [
|
||||
"atomic-waker",
|
||||
"bytes",
|
||||
"fnv",
|
||||
"futures-core",
|
||||
"futures-sink",
|
||||
"http",
|
||||
"indexmap 2.6.0",
|
||||
"slab",
|
||||
"tokio",
|
||||
"tokio-util",
|
||||
"tracing",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "hashbrown"
|
||||
version = "0.12.3"
|
||||
@@ -1264,6 +1334,7 @@ dependencies = [
|
||||
"bytes",
|
||||
"futures-channel",
|
||||
"futures-util",
|
||||
"h2",
|
||||
"http",
|
||||
"http-body",
|
||||
"httparse",
|
||||
@@ -1274,6 +1345,24 @@ dependencies = [
|
||||
"want",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "hyper-rustls"
|
||||
version = "0.27.3"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "08afdbb5c31130e3034af566421053ab03787c640246a446327f550d11bcb333"
|
||||
dependencies = [
|
||||
"futures-util",
|
||||
"http",
|
||||
"hyper",
|
||||
"hyper-util",
|
||||
"rustls",
|
||||
"rustls-pki-types",
|
||||
"tokio",
|
||||
"tokio-rustls",
|
||||
"tower-service",
|
||||
"webpki-roots",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "hyper-util"
|
||||
version = "0.1.9"
|
||||
@@ -1332,6 +1421,16 @@ version = "1.0.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "b9e0384b61958566e926dc50660321d12159025e767c18e043daf26b70104c39"
|
||||
|
||||
[[package]]
|
||||
name = "idna"
|
||||
version = "0.3.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "e14ddfc70884202db2244c223200c204c2bda1bc6e0998d11b5e024d657209e6"
|
||||
dependencies = [
|
||||
"unicode-bidi",
|
||||
"unicode-normalization",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "idna"
|
||||
version = "0.5.0"
|
||||
@@ -2355,6 +2454,22 @@ dependencies = [
|
||||
"unicode-ident",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "psl-types"
|
||||
version = "2.0.11"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "33cb294fe86a74cbcf50d4445b37da762029549ebeea341421c7c70370f86cac"
|
||||
|
||||
[[package]]
|
||||
name = "publicsuffix"
|
||||
version = "2.2.3"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "96a8c1bda5ae1af7f99a2962e49df150414a43d62404644d98dd5c3a93d07457"
|
||||
dependencies = [
|
||||
"idna 0.3.0",
|
||||
"psl-types",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "quick-xml"
|
||||
version = "0.32.0"
|
||||
@@ -2364,6 +2479,54 @@ dependencies = [
|
||||
"memchr",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "quinn"
|
||||
version = "0.11.5"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "8c7c5fdde3cdae7203427dc4f0a68fe0ed09833edc525a03456b153b79828684"
|
||||
dependencies = [
|
||||
"bytes",
|
||||
"pin-project-lite",
|
||||
"quinn-proto",
|
||||
"quinn-udp",
|
||||
"rustc-hash",
|
||||
"rustls",
|
||||
"socket2",
|
||||
"thiserror",
|
||||
"tokio",
|
||||
"tracing",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "quinn-proto"
|
||||
version = "0.11.8"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "fadfaed2cd7f389d0161bb73eeb07b7b78f8691047a6f3e73caaeae55310a4a6"
|
||||
dependencies = [
|
||||
"bytes",
|
||||
"rand 0.8.5",
|
||||
"ring",
|
||||
"rustc-hash",
|
||||
"rustls",
|
||||
"slab",
|
||||
"thiserror",
|
||||
"tinyvec",
|
||||
"tracing",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "quinn-udp"
|
||||
version = "0.5.5"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "4fe68c2e9e1a1234e218683dbdf9f9dfcb094113c5ac2b938dfcb9bab4c4140b"
|
||||
dependencies = [
|
||||
"libc",
|
||||
"once_cell",
|
||||
"socket2",
|
||||
"tracing",
|
||||
"windows-sys 0.59.0",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "quote"
|
||||
version = "1.0.37"
|
||||
@@ -2517,12 +2680,17 @@ checksum = "f713147fbe92361e52392c73b8c9e48c04c6625bce969ef54dc901e58e042a7b"
|
||||
dependencies = [
|
||||
"base64 0.22.1",
|
||||
"bytes",
|
||||
"cookie",
|
||||
"cookie_store",
|
||||
"encoding_rs",
|
||||
"futures-core",
|
||||
"futures-util",
|
||||
"h2",
|
||||
"http",
|
||||
"http-body",
|
||||
"http-body-util",
|
||||
"hyper",
|
||||
"hyper-rustls",
|
||||
"hyper-util",
|
||||
"ipnet",
|
||||
"js-sys",
|
||||
@@ -2531,11 +2699,17 @@ dependencies = [
|
||||
"once_cell",
|
||||
"percent-encoding",
|
||||
"pin-project-lite",
|
||||
"quinn",
|
||||
"rustls",
|
||||
"rustls-pemfile",
|
||||
"rustls-pki-types",
|
||||
"serde",
|
||||
"serde_json",
|
||||
"serde_urlencoded",
|
||||
"sync_wrapper",
|
||||
"system-configuration",
|
||||
"tokio",
|
||||
"tokio-rustls",
|
||||
"tokio-util",
|
||||
"tower-service",
|
||||
"url",
|
||||
@@ -2543,15 +2717,37 @@ dependencies = [
|
||||
"wasm-bindgen-futures",
|
||||
"wasm-streams",
|
||||
"web-sys",
|
||||
"webpki-roots",
|
||||
"windows-registry",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "ring"
|
||||
version = "0.17.8"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "c17fa4cb658e3583423e915b9f3acc01cceaee1860e33d59ebae66adc3a2dc0d"
|
||||
dependencies = [
|
||||
"cc",
|
||||
"cfg-if",
|
||||
"getrandom 0.2.15",
|
||||
"libc",
|
||||
"spin",
|
||||
"untrusted",
|
||||
"windows-sys 0.52.0",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "rustc-demangle"
|
||||
version = "0.1.24"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "719b953e2095829ee67db738b3bfa9fa368c94900df327b3f07fe6e794d2fe1f"
|
||||
|
||||
[[package]]
|
||||
name = "rustc-hash"
|
||||
version = "2.0.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "583034fd73374156e66797ed8e5b0d5690409c9226b22d87cb7f19821c05d152"
|
||||
|
||||
[[package]]
|
||||
name = "rustc_version"
|
||||
version = "0.4.1"
|
||||
@@ -2561,6 +2757,46 @@ dependencies = [
|
||||
"semver",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "rustls"
|
||||
version = "0.23.15"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "5fbb44d7acc4e873d613422379f69f237a1b141928c02f6bc6ccfddddc2d7993"
|
||||
dependencies = [
|
||||
"once_cell",
|
||||
"ring",
|
||||
"rustls-pki-types",
|
||||
"rustls-webpki",
|
||||
"subtle",
|
||||
"zeroize",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "rustls-pemfile"
|
||||
version = "2.2.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "dce314e5fee3f39953d46bb63bb8a46d40c2f8fb7cc5a3b6cab2bde9721d6e50"
|
||||
dependencies = [
|
||||
"rustls-pki-types",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "rustls-pki-types"
|
||||
version = "1.10.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "16f1201b3c9a7ee8039bcadc17b7e605e2945b27eee7631788c1bd2b0643674b"
|
||||
|
||||
[[package]]
|
||||
name = "rustls-webpki"
|
||||
version = "0.102.8"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "64ca1bc8749bd4cf37b5ce386cc146580777b4e8572c7b97baf22c83f444bee9"
|
||||
dependencies = [
|
||||
"ring",
|
||||
"rustls-pki-types",
|
||||
"untrusted",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "ryu"
|
||||
version = "1.0.18"
|
||||
@@ -2898,6 +3134,12 @@ dependencies = [
|
||||
"system-deps",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "spin"
|
||||
version = "0.9.8"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "6980e8d7511241f8acf4aebddbb1ff938df5eebe98691418c4468d0b72a96a67"
|
||||
|
||||
[[package]]
|
||||
name = "stable_deref_trait"
|
||||
version = "1.2.0"
|
||||
@@ -2936,6 +3178,12 @@ version = "0.11.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "7da8b5736845d9f2fcb837ea5d9e2628564b3b043a70948a3f0b778838c5fb4f"
|
||||
|
||||
[[package]]
|
||||
name = "subtle"
|
||||
version = "2.6.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "13c2bddecc57b384dee18652358fb23172facb8a2c51ccc10d74c157bdea3292"
|
||||
|
||||
[[package]]
|
||||
name = "swift-rs"
|
||||
version = "1.0.7"
|
||||
@@ -2978,6 +3226,27 @@ dependencies = [
|
||||
"futures-core",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "system-configuration"
|
||||
version = "0.6.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "3c879d448e9d986b661742763247d3693ed13609438cf3d006f51f5368a5ba6b"
|
||||
dependencies = [
|
||||
"bitflags 2.6.0",
|
||||
"core-foundation 0.9.4",
|
||||
"system-configuration-sys",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "system-configuration-sys"
|
||||
version = "0.6.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "8e1d1b10ced5ca923a1fcb8d03e96b8d3268065d724548c0211415ff6ac6bac4"
|
||||
dependencies = [
|
||||
"core-foundation-sys",
|
||||
"libc",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "system-deps"
|
||||
version = "6.2.2"
|
||||
@@ -2999,7 +3268,7 @@ checksum = "a0dbbebe82d02044dfa481adca1550d6dd7bd16e086bc34fa0fbecceb5a63751"
|
||||
dependencies = [
|
||||
"bitflags 2.6.0",
|
||||
"cocoa",
|
||||
"core-foundation",
|
||||
"core-foundation 0.10.0",
|
||||
"core-graphics",
|
||||
"crossbeam-channel",
|
||||
"dispatch",
|
||||
@@ -3049,9 +3318,9 @@ checksum = "61c41af27dd6d1e27b1b16b489db798443478cef1f06a660c96db617ba5de3b1"
|
||||
|
||||
[[package]]
|
||||
name = "tauri"
|
||||
version = "2.0.3"
|
||||
version = "2.0.5"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "fd96d46534b10765ce0c6208f9451d98ea38636364a41b272d3610c70dd0e4c3"
|
||||
checksum = "5ce2818e803ce3097987296623ed8c0d9f65ed93b4137ff9a83e168bdbf62932"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"bytes",
|
||||
@@ -3177,6 +3446,49 @@ dependencies = [
|
||||
"walkdir",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "tauri-plugin-fs"
|
||||
version = "2.0.3"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "96ba7d46e86db8c830d143ef90ab5a453328365b0cc834c24edea4267b16aba0"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"dunce",
|
||||
"glob",
|
||||
"percent-encoding",
|
||||
"schemars",
|
||||
"serde",
|
||||
"serde_json",
|
||||
"serde_repr",
|
||||
"tauri",
|
||||
"tauri-plugin",
|
||||
"thiserror",
|
||||
"url",
|
||||
"uuid",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "tauri-plugin-http"
|
||||
version = "2.0.3"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "c752aee1b00ec3c4d4f440095995d9bd2c640b478f2067d1fba388900b82eb96"
|
||||
dependencies = [
|
||||
"data-url",
|
||||
"http",
|
||||
"regex",
|
||||
"reqwest",
|
||||
"schemars",
|
||||
"serde",
|
||||
"serde_json",
|
||||
"tauri",
|
||||
"tauri-plugin",
|
||||
"tauri-plugin-fs",
|
||||
"thiserror",
|
||||
"tokio",
|
||||
"url",
|
||||
"urlpattern",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "tauri-plugin-shell"
|
||||
version = "2.0.1"
|
||||
@@ -3219,9 +3531,9 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "tauri-runtime-wry"
|
||||
version = "2.1.0"
|
||||
version = "2.1.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "aaac63b65df8e85570993eaf93ae1dd73a6fb66d8bd99674ce65f41dc3c63e7d"
|
||||
checksum = "1431602bcc71f2f840ad623915c9842ecc32999b867c4a787d975a17a9625cc6"
|
||||
dependencies = [
|
||||
"gtk",
|
||||
"http",
|
||||
@@ -3384,9 +3696,32 @@ dependencies = [
|
||||
"mio",
|
||||
"pin-project-lite",
|
||||
"socket2",
|
||||
"tokio-macros",
|
||||
"windows-sys 0.52.0",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "tokio-macros"
|
||||
version = "2.4.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "693d596312e88961bc67d7f1f97af8a70227d9f90c31bba5806eec004978d752"
|
||||
dependencies = [
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
"syn 2.0.79",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "tokio-rustls"
|
||||
version = "0.26.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "0c7bc40d0e5a97695bb96e27995cd3a08538541b0a846f65bba7a359f36700d4"
|
||||
dependencies = [
|
||||
"rustls",
|
||||
"rustls-pki-types",
|
||||
"tokio",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "tokio-util"
|
||||
version = "0.7.12"
|
||||
@@ -3591,6 +3926,12 @@ version = "1.12.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "f6ccf251212114b54433ec949fd6a7841275f9ada20dddd2f29e9ceea4501493"
|
||||
|
||||
[[package]]
|
||||
name = "untrusted"
|
||||
version = "0.9.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "8ecb6da28b8a351d773b68d5825ac39017e680750f980f3a1a85cd8dd28a47c1"
|
||||
|
||||
[[package]]
|
||||
name = "url"
|
||||
version = "2.5.2"
|
||||
@@ -3598,7 +3939,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "22784dbdf76fdde8af1aeda5622b546b422b6fc585325248a2bf9f5e41e94d6c"
|
||||
dependencies = [
|
||||
"form_urlencoded",
|
||||
"idna",
|
||||
"idna 0.5.0",
|
||||
"percent-encoding",
|
||||
"serde",
|
||||
]
|
||||
@@ -3828,6 +4169,15 @@ dependencies = [
|
||||
"system-deps",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "webpki-roots"
|
||||
version = "0.26.6"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "841c67bff177718f1d4dfefde8d8f0e78f9b6589319ba88312f567fc5841a958"
|
||||
dependencies = [
|
||||
"rustls-pki-types",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "webview2-com"
|
||||
version = "0.33.0"
|
||||
@@ -4237,9 +4587,9 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "wry"
|
||||
version = "0.46.0"
|
||||
version = "0.46.3"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "469a3765ecc3e8aa9ccdf3c5a52c82697ec03037cd60494488763880d31a1b3a"
|
||||
checksum = "cd5cdf57c66813d97601181349c63b96994b3074fc3d7a31a8cce96e968e3bbd"
|
||||
dependencies = [
|
||||
"base64 0.22.1",
|
||||
"block2",
|
||||
@@ -4317,3 +4667,9 @@ dependencies = [
|
||||
"quote",
|
||||
"syn 2.0.79",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "zeroize"
|
||||
version = "1.8.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "ced3678a2879b30306d323f4542626697a464a97c0a07c9aebf7ebca65cd4dde"
|
||||
|
||||
@@ -22,6 +22,7 @@ tauri = { version = "2.0.0", features = [] }
|
||||
tauri-plugin-shell = "2.0.0"
|
||||
serde = { version = "1", features = ["derive"] }
|
||||
serde_json = "1"
|
||||
tauri-plugin-http = "2"
|
||||
|
||||
[profile.dev]
|
||||
incremental = true # Compile your binary in smaller steps.
|
||||
@@ -31,4 +32,4 @@ codegen-units = 1 # Allows LLVM to perform better optimization.
|
||||
lto = true # Enables link-time-optimizations.
|
||||
opt-level = "s" # Prioritizes small binary size. Use `3` if you prefer speed.
|
||||
panic = "abort" # Higher performance by disabling panic handlers.
|
||||
strip = true # Ensures debug symbols are removed.
|
||||
strip = true # Ensures debug symbols are removed.
|
||||
|
||||
@@ -5,6 +5,16 @@
|
||||
"windows": ["main"],
|
||||
"permissions": [
|
||||
"core:default",
|
||||
"shell:allow-open"
|
||||
"shell:allow-open",
|
||||
"http:default",
|
||||
"http:allow-fetch",
|
||||
"http:allow-fetch-cancel",
|
||||
"http:allow-fetch-read-body",
|
||||
"http:allow-fetch-send",
|
||||
{
|
||||
"identifier": "http:default",
|
||||
"allow": [{ "url": "https://*.tauri.app" }],
|
||||
"deny": [{ "url": "https://private.tauri.app" }]
|
||||
}
|
||||
]
|
||||
}
|
||||
|
||||
@@ -7,6 +7,7 @@ fn greet(name: &str) -> String {
|
||||
#[cfg_attr(mobile, tauri::mobile_entry_point)]
|
||||
pub fn run() {
|
||||
tauri::Builder::default()
|
||||
.plugin(tauri_plugin_http::init())
|
||||
.plugin(tauri_plugin_shell::init())
|
||||
.invoke_handler(tauri::generate_handler![greet])
|
||||
.run(tauri::generate_context!())
|
||||
|
||||
@@ -15,7 +15,6 @@
|
||||
"title": "coco",
|
||||
"width": 800,
|
||||
"height": 600
|
||||
|
||||
}
|
||||
],
|
||||
"security": {
|
||||
@@ -32,5 +31,8 @@
|
||||
"icons/icon.icns",
|
||||
"icons/icon.ico"
|
||||
]
|
||||
},
|
||||
"plugins": {
|
||||
"window": {}
|
||||
}
|
||||
}
|
||||
|
||||
116
src/App.css
116
src/App.css
@@ -1,116 +0,0 @@
|
||||
.logo.vite:hover {
|
||||
filter: drop-shadow(0 0 2em #747bff);
|
||||
}
|
||||
|
||||
.logo.react:hover {
|
||||
filter: drop-shadow(0 0 2em #61dafb);
|
||||
}
|
||||
:root {
|
||||
font-family: Inter, Avenir, Helvetica, Arial, sans-serif;
|
||||
font-size: 16px;
|
||||
line-height: 24px;
|
||||
font-weight: 400;
|
||||
|
||||
color: #0f0f0f;
|
||||
background-color: #f6f6f6;
|
||||
|
||||
font-synthesis: none;
|
||||
text-rendering: optimizeLegibility;
|
||||
-webkit-font-smoothing: antialiased;
|
||||
-moz-osx-font-smoothing: grayscale;
|
||||
-webkit-text-size-adjust: 100%;
|
||||
}
|
||||
|
||||
.container {
|
||||
margin: 0;
|
||||
padding-top: 10vh;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
justify-content: center;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.logo {
|
||||
height: 6em;
|
||||
padding: 1.5em;
|
||||
will-change: filter;
|
||||
transition: 0.75s;
|
||||
}
|
||||
|
||||
.logo.tauri:hover {
|
||||
filter: drop-shadow(0 0 2em #24c8db);
|
||||
}
|
||||
|
||||
.row {
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
a {
|
||||
font-weight: 500;
|
||||
color: #646cff;
|
||||
text-decoration: inherit;
|
||||
}
|
||||
|
||||
a:hover {
|
||||
color: #535bf2;
|
||||
}
|
||||
|
||||
h1 {
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
input,
|
||||
button {
|
||||
border-radius: 8px;
|
||||
border: 1px solid transparent;
|
||||
padding: 0.6em 1.2em;
|
||||
font-size: 1em;
|
||||
font-weight: 500;
|
||||
font-family: inherit;
|
||||
color: #0f0f0f;
|
||||
background-color: #ffffff;
|
||||
transition: border-color 0.25s;
|
||||
box-shadow: 0 2px 2px rgba(0, 0, 0, 0.2);
|
||||
}
|
||||
|
||||
button {
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
button:hover {
|
||||
border-color: #396cd8;
|
||||
}
|
||||
button:active {
|
||||
border-color: #396cd8;
|
||||
background-color: #e8e8e8;
|
||||
}
|
||||
|
||||
input,
|
||||
button {
|
||||
outline: none;
|
||||
}
|
||||
|
||||
#greet-input {
|
||||
margin-right: 5px;
|
||||
}
|
||||
|
||||
@media (prefers-color-scheme: dark) {
|
||||
:root {
|
||||
color: #f6f6f6;
|
||||
background-color: #2f2f2f;
|
||||
}
|
||||
|
||||
a:hover {
|
||||
color: #24c8db;
|
||||
}
|
||||
|
||||
input,
|
||||
button {
|
||||
color: #ffffff;
|
||||
background-color: #0f0f0f98;
|
||||
}
|
||||
button:active {
|
||||
background-color: #0f0f0f69;
|
||||
}
|
||||
}
|
||||
66
src/App.tsx
66
src/App.tsx
@@ -1,51 +1,33 @@
|
||||
import { useState } from "react";
|
||||
import reactLogo from "./assets/react.svg";
|
||||
import { invoke } from "@tauri-apps/api/core";
|
||||
import "./App.css";
|
||||
import { useTranslation } from "react-i18next";
|
||||
|
||||
import useEscape from "./hooks/useEscape";
|
||||
import "./i18n";
|
||||
import Header from "./components/Header";
|
||||
import Chat from "./components/Chat";
|
||||
import Raycast from "./components/Raycast";
|
||||
import Footer from "./components/Footer";
|
||||
|
||||
function App() {
|
||||
const [greetMsg, setGreetMsg] = useState("");
|
||||
const [name, setName] = useState("");
|
||||
const { t } = useTranslation();
|
||||
|
||||
async function greet() {
|
||||
// Learn more about Tauri commands at https://tauri.app/v1/guides/features/command
|
||||
setGreetMsg(await invoke("greet", { name }));
|
||||
}
|
||||
useEscape();
|
||||
|
||||
return (
|
||||
<div className="container">
|
||||
<h1>Welcome to Tauri!</h1>
|
||||
<div
|
||||
className={`
|
||||
bg-background
|
||||
w-screen h-screen
|
||||
`}
|
||||
>
|
||||
{/* <Header /> */}
|
||||
<main className="w-[100%] h-[100%] flex flex-col items-center justify-center">
|
||||
<div className="text-xl text-primary">{t("welcome")}</div>
|
||||
|
||||
<div className="row">
|
||||
<a href="https://vitejs.dev" target="_blank">
|
||||
<img src="/vite.svg" className="logo vite" alt="Vite logo" />
|
||||
</a>
|
||||
<a href="https://tauri.app" target="_blank">
|
||||
<img src="/tauri.svg" className="logo tauri" alt="Tauri logo" />
|
||||
</a>
|
||||
<a href="https://reactjs.org" target="_blank">
|
||||
<img src={reactLogo} className="logo react" alt="React logo" />
|
||||
</a>
|
||||
</div>
|
||||
|
||||
<p>Click on the Tauri, Vite, and React logos to learn more.</p>
|
||||
|
||||
<form
|
||||
className="row"
|
||||
onSubmit={(e) => {
|
||||
e.preventDefault();
|
||||
greet();
|
||||
}}
|
||||
>
|
||||
<input
|
||||
id="greet-input"
|
||||
onChange={(e) => setName(e.currentTarget.value)}
|
||||
placeholder="Enter a name..."
|
||||
/>
|
||||
<button type="submit">Greet</button>
|
||||
</form>
|
||||
|
||||
<p>{greetMsg}</p>
|
||||
<div className="mx-0 mt-5 w-[100%]">
|
||||
<Raycast />
|
||||
</div>
|
||||
</main>
|
||||
<Footer />
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
105
src/components/Chat.tsx
Normal file
105
src/components/Chat.tsx
Normal file
@@ -0,0 +1,105 @@
|
||||
import { useState, useEffect, useRef } from "react";
|
||||
// import { invoke } from "@tauri-apps/api/core";
|
||||
import { useHotkeys } from "react-hotkeys-hook";
|
||||
import debounce from "lodash/debounce";
|
||||
import { Textarea } from "@headlessui/react";
|
||||
import clsx from "clsx";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import { fetch } from "@tauri-apps/plugin-http";
|
||||
|
||||
import SendIcon from "../icons/Send";
|
||||
|
||||
export default function ChatInput() {
|
||||
const { t } = useTranslation();
|
||||
|
||||
const inputRef = useRef<HTMLTextAreaElement>(null);
|
||||
const [message, setMessage] = useState("");
|
||||
const [info, setInfo] = useState("");
|
||||
const isMac = true;
|
||||
|
||||
useEffect(() => {
|
||||
const syncMessage = debounce(async () => {
|
||||
try {
|
||||
// await invoke("ask_sync", { message: JSON.stringify(message) });
|
||||
} catch (error) {
|
||||
console.error("Error syncing message:", error);
|
||||
}
|
||||
}, 300); // Debounce by 300ms
|
||||
|
||||
syncMessage();
|
||||
return () => syncMessage.cancel(); // Cleanup debounce on unmount
|
||||
}, [message]);
|
||||
|
||||
useHotkeys(
|
||||
isMac ? "meta+enter" : "ctrl+enter",
|
||||
async (event: KeyboardEvent) => {
|
||||
event.preventDefault();
|
||||
await handleSend();
|
||||
},
|
||||
{
|
||||
enableOnFormTags: true,
|
||||
},
|
||||
[message]
|
||||
);
|
||||
|
||||
const handleInput = (e: React.ChangeEvent<HTMLTextAreaElement>) => {
|
||||
setInfo("");
|
||||
setMessage(e.target.value);
|
||||
};
|
||||
|
||||
const handleSend = async () => {
|
||||
if (!message) return;
|
||||
try {
|
||||
// await invoke("ask_send", { message: JSON.stringify(message) });
|
||||
|
||||
// Send a GET request
|
||||
const response = await fetch("https://test.tauri.app/data.json", {
|
||||
method: "GET",
|
||||
});
|
||||
setInfo(JSON.stringify(response));
|
||||
console.log(response.status); // e.g. 200
|
||||
console.log(response.statusText); // e.g. "OK"
|
||||
} catch (error) {
|
||||
console.error("Error sending message:", error);
|
||||
setInfo(JSON.stringify(error));
|
||||
}
|
||||
setMessage("");
|
||||
if (inputRef.current) {
|
||||
inputRef.current.value = "";
|
||||
inputRef.current.focus();
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="w-[100%] h-[100%]">
|
||||
<div>{info}</div>
|
||||
<div className="relative flex dark:bg-app-gray-2/[0.98] dark:text-slate-200 items-center gap-1">
|
||||
<Textarea
|
||||
ref={inputRef}
|
||||
onChange={handleInput}
|
||||
spellCheck="false"
|
||||
autoFocus
|
||||
className={clsx(
|
||||
"mt-3 block w-full resize-none rounded-xl border border-transparent bg-white/10",
|
||||
"py-3 px-4 text-sm text-black placeholder-gray-500 shadow-md",
|
||||
// Transition for smoother appearance changes
|
||||
"transition-colors duration-300 ease-in-out",
|
||||
// Dark mode styles
|
||||
"dark:bg-gray-800 dark:text-white dark:placeholder-gray-400",
|
||||
"focus:border-blue-400 focus:ring-2 focus:ring-blue-400 focus:ring-opacity-50",
|
||||
"dark:focus:border-white dark:focus:ring-white/25",
|
||||
"focus:outline-none"
|
||||
)}
|
||||
placeholder={t("InputMessage")}
|
||||
/>
|
||||
<SendIcon
|
||||
size={30}
|
||||
className="absolute right-2 text-gray-400/80 dark:text-gray-600 cursor-pointer"
|
||||
onClick={handleSend}
|
||||
title={`Send message (${isMac ? "⌘⏎" : "⌃⏎"})`}
|
||||
aria-label="Send message"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
189
src/components/CommandPalette.tsx
Normal file
189
src/components/CommandPalette.tsx
Normal file
@@ -0,0 +1,189 @@
|
||||
import React, { useState, useEffect, useCallback } from "react";
|
||||
import {
|
||||
Search,
|
||||
Settings,
|
||||
Calculator,
|
||||
Calendar,
|
||||
Mail,
|
||||
Music,
|
||||
User,
|
||||
} from "lucide-react";
|
||||
|
||||
interface CommandItem {
|
||||
id: string;
|
||||
icon: React.ReactNode;
|
||||
title: string;
|
||||
description: string;
|
||||
action: () => void;
|
||||
}
|
||||
|
||||
export function CommandPalette() {
|
||||
const [isOpen, setIsOpen] = useState(false);
|
||||
const [search, setSearch] = useState("");
|
||||
const [selectedIndex, setSelectedIndex] = useState(0);
|
||||
|
||||
const commands: CommandItem[] = [
|
||||
{
|
||||
id: "settings",
|
||||
icon: <Settings className="w-5 h-5" />,
|
||||
title: "Settings",
|
||||
description: "Adjust your preferences",
|
||||
action: () => console.log("Settings clicked"),
|
||||
},
|
||||
{
|
||||
id: "calculator",
|
||||
icon: <Calculator className="w-5 h-5" />,
|
||||
title: "Calculator",
|
||||
description: "Perform quick calculations",
|
||||
action: () => console.log("Calculator clicked"),
|
||||
},
|
||||
{
|
||||
id: "calendar",
|
||||
icon: <Calendar className="w-5 h-5" />,
|
||||
title: "Calendar",
|
||||
description: "View your schedule",
|
||||
action: () => console.log("Calendar clicked"),
|
||||
},
|
||||
{
|
||||
id: "mail",
|
||||
icon: <Mail className="w-5 h-5" />,
|
||||
title: "Mail",
|
||||
description: "Check your inbox",
|
||||
action: () => console.log("Mail clicked"),
|
||||
},
|
||||
{
|
||||
id: "music",
|
||||
icon: <Music className="w-5 h-5" />,
|
||||
title: "Music",
|
||||
description: "Control playback",
|
||||
action: () => console.log("Music clicked"),
|
||||
},
|
||||
{
|
||||
id: "profile",
|
||||
icon: <User className="w-5 h-5" />,
|
||||
title: "Profile",
|
||||
description: "View your profile",
|
||||
action: () => console.log("Profile clicked"),
|
||||
},
|
||||
];
|
||||
|
||||
const filteredCommands = commands.filter(
|
||||
(command) =>
|
||||
command.title.toLowerCase().includes(search.toLowerCase()) ||
|
||||
command.description.toLowerCase().includes(search.toLowerCase())
|
||||
);
|
||||
|
||||
const handleKeyDown = useCallback(
|
||||
(e: KeyboardEvent) => {
|
||||
if (e.key === "k" && (e.metaKey || e.ctrlKey)) {
|
||||
e.preventDefault();
|
||||
setIsOpen((prev) => !prev);
|
||||
} else if (e.key === "Escape") {
|
||||
setIsOpen(false);
|
||||
} else if (e.key === "ArrowDown") {
|
||||
e.preventDefault();
|
||||
setSelectedIndex((prev) =>
|
||||
prev < filteredCommands.length - 1 ? prev + 1 : prev
|
||||
);
|
||||
} else if (e.key === "ArrowUp") {
|
||||
e.preventDefault();
|
||||
setSelectedIndex((prev) => (prev > 0 ? prev - 1 : prev));
|
||||
} else if (e.key === "Enter" && filteredCommands[selectedIndex]) {
|
||||
filteredCommands[selectedIndex].action();
|
||||
setIsOpen(false);
|
||||
}
|
||||
},
|
||||
[filteredCommands, selectedIndex]
|
||||
);
|
||||
|
||||
useEffect(() => {
|
||||
document.addEventListener("keydown", handleKeyDown);
|
||||
return () => document.removeEventListener("keydown", handleKeyDown);
|
||||
}, [handleKeyDown]);
|
||||
|
||||
useEffect(() => {
|
||||
setSelectedIndex(0);
|
||||
}, [search]);
|
||||
|
||||
if (!isOpen) return null;
|
||||
|
||||
return (
|
||||
<div className="fixed inset-0 z-50 overflow-y-auto">
|
||||
<div className="min-h-screen px-4 text-center">
|
||||
<div
|
||||
className="fixed inset-0 bg-black/50 transition-opacity"
|
||||
onClick={() => setIsOpen(false)}
|
||||
/>
|
||||
|
||||
<div className="inline-block w-full max-w-2xl my-16 text-left align-middle transition-all transform">
|
||||
<div className="relative bg-white rounded-xl shadow-2xl overflow-hidden">
|
||||
<div className="flex items-center px-4 border-b border-gray-200">
|
||||
<Search className="w-5 h-5 text-gray-400" />
|
||||
<input
|
||||
autoFocus
|
||||
type="text"
|
||||
className="w-full px-4 py-4 text-gray-700 bg-transparent border-none focus:outline-none focus:ring-0"
|
||||
placeholder="Search commands..."
|
||||
value={search}
|
||||
onChange={(e) => setSearch(e.target.value)}
|
||||
/>
|
||||
<div className="w-32 flex items-center gap-1">
|
||||
<kbd className="px-2 py-1 text-xs font-semibold text-gray-500 bg-gray-100 border border-gray-200 rounded-md">
|
||||
Esc
|
||||
</kbd>
|
||||
<span className="text-gray-400">to close</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="max-h-[60vh] overflow-y-auto">
|
||||
{filteredCommands.length === 0 ? (
|
||||
<div className="px-4 py-14 text-center text-gray-500">
|
||||
No results found.
|
||||
</div>
|
||||
) : (
|
||||
<div className="py-2">
|
||||
{filteredCommands.map((command, index) => (
|
||||
<div
|
||||
key={command.id}
|
||||
className={`px-4 py-3 flex items-center gap-3 cursor-pointer transition-colors ${
|
||||
selectedIndex === index
|
||||
? "bg-blue-50 text-blue-700"
|
||||
: "text-gray-700 hover:bg-gray-50"
|
||||
}`}
|
||||
onClick={() => {
|
||||
command.action();
|
||||
setIsOpen(false);
|
||||
}}
|
||||
>
|
||||
<div
|
||||
className={`${
|
||||
selectedIndex === index
|
||||
? "text-blue-600"
|
||||
: "text-gray-400"
|
||||
}`}
|
||||
>
|
||||
{command.icon}
|
||||
</div>
|
||||
<div>
|
||||
<div className="font-medium">{command.title}</div>
|
||||
<div
|
||||
className={`text-sm ${
|
||||
selectedIndex === index
|
||||
? "text-blue-500"
|
||||
: "text-gray-500"
|
||||
}`}
|
||||
>
|
||||
{command.description}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
95
src/components/Footer.tsx
Normal file
95
src/components/Footer.tsx
Normal file
@@ -0,0 +1,95 @@
|
||||
import { Menu, MenuButton, MenuItems, MenuItem } from "@headlessui/react";
|
||||
import { Command, Settings, LogOut, User, ChevronUp, Home } from "lucide-react";
|
||||
import { Link } from "react-router-dom";
|
||||
|
||||
const Footer = () => {
|
||||
return (
|
||||
<div className="fixed bottom-0 left-0 right-0 bg-white dark:bg-gray-800 border-t dark:border-gray-700">
|
||||
<div className="max-w-6xl mx-auto px-4 h-14 flex items-center justify-between">
|
||||
<Menu as="div" className="relative">
|
||||
<MenuButton className="flex items-center space-x-2 px-3 py-1.5 rounded-lg hover:bg-gray-100 dark:hover:bg-gray-700 transition-colors">
|
||||
<Command className="w-5 h-5 text-gray-600 dark:text-gray-400" />
|
||||
<span className="text-sm font-medium text-gray-700 dark:text-gray-300">
|
||||
Coco
|
||||
</span>
|
||||
<ChevronUp className="w-4 h-4 text-gray-500 dark:text-gray-400" />
|
||||
</MenuButton>
|
||||
|
||||
<MenuItems className="absolute bottom-full mb-2 left-0 w-64 rounded-lg bg-white dark:bg-gray-800 shadow-lg ring-1 ring-black ring-opacity-5 focus:outline-none">
|
||||
<div className="p-1">
|
||||
<MenuItem>
|
||||
{({ active }) => (
|
||||
<button
|
||||
className={`${
|
||||
active
|
||||
? "bg-gray-100 dark:bg-gray-700"
|
||||
: "text-gray-900 dark:text-gray-100"
|
||||
} group flex w-full items-center rounded-md px-3 py-2 text-sm`}
|
||||
>
|
||||
<Home className="w-4 h-4 mr-2" />
|
||||
<Link to={`/`}>Home</Link>
|
||||
</button>
|
||||
)}
|
||||
</MenuItem>
|
||||
<MenuItem>
|
||||
{({ active }) => (
|
||||
<button
|
||||
className={`${
|
||||
active
|
||||
? "bg-gray-100 dark:bg-gray-700"
|
||||
: "text-gray-900 dark:text-gray-100"
|
||||
} group flex w-full items-center rounded-md px-3 py-2 text-sm`}
|
||||
>
|
||||
<User className="w-4 h-4 mr-2" />
|
||||
Profile
|
||||
</button>
|
||||
)}
|
||||
</MenuItem>
|
||||
<MenuItem>
|
||||
{({ active }) => (
|
||||
<button
|
||||
className={`${
|
||||
active
|
||||
? "bg-gray-100 dark:bg-gray-700"
|
||||
: "text-gray-900 dark:text-gray-100"
|
||||
} group flex w-full items-center rounded-md px-3 py-2 text-sm`}
|
||||
>
|
||||
<Settings className="w-4 h-4 mr-2" />
|
||||
<Link to={`settings`}>Settings</Link>
|
||||
</button>
|
||||
)}
|
||||
</MenuItem>
|
||||
<div className="h-px bg-gray-200 dark:bg-gray-700 my-1" />
|
||||
<MenuItem>
|
||||
{({ active }) => (
|
||||
<button
|
||||
className={`${
|
||||
active
|
||||
? "bg-gray-100 dark:bg-gray-700"
|
||||
: "text-gray-900 dark:text-gray-100"
|
||||
} group flex w-full items-center rounded-md px-3 py-2 text-sm`}
|
||||
>
|
||||
<LogOut className="w-4 h-4 mr-2" />
|
||||
Sign Out
|
||||
</button>
|
||||
)}
|
||||
</MenuItem>
|
||||
</div>
|
||||
</MenuItems>
|
||||
</Menu>
|
||||
|
||||
<div className="flex items-center space-x-4">
|
||||
<span className="text-xs text-gray-500 dark:text-gray-400">
|
||||
Version 1.0.0
|
||||
</span>
|
||||
<div className="h-4 w-px bg-gray-200 dark:bg-gray-700" />
|
||||
<button className="text-xs text-gray-500 dark:text-gray-400 hover:text-gray-900 dark:hover:text-white transition-colors">
|
||||
Check for Updates
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default Footer;
|
||||
19
src/components/Header.tsx
Normal file
19
src/components/Header.tsx
Normal file
@@ -0,0 +1,19 @@
|
||||
import ThemeToggle from "./ThemeToggle";
|
||||
import LangToggle from "./LangToggle";
|
||||
|
||||
export default function Header() {
|
||||
return (
|
||||
<div className="fixed w-[100%] h-10 px-4 flex justify-between items-center">
|
||||
<div>Coco</div>
|
||||
<div className="flex items-center gap-2">
|
||||
<LangToggle />
|
||||
<ThemeToggle />
|
||||
<img
|
||||
className="h-5 w-5 rounded-full ring-2 ring-white"
|
||||
src="https://images.unsplash.com/photo-1491528323818-fdd1faba62cc?ixlib=rb-1.2.1&ixid=eyJhcHBfaWQiOjEyMDd9&auto=format&fit=facearea&facepad=2&w=256&h=256&q=80"
|
||||
alt=""
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
55
src/components/LangToggle.tsx
Normal file
55
src/components/LangToggle.tsx
Normal file
@@ -0,0 +1,55 @@
|
||||
import { Menu, MenuButton, MenuItem, MenuItems } from "@headlessui/react";
|
||||
import { ChevronDown, Globe2 } from "lucide-react";
|
||||
import { useState } from "react";
|
||||
import { useTranslation } from "react-i18next";
|
||||
|
||||
interface Language {
|
||||
code: string;
|
||||
name: string;
|
||||
flag: string;
|
||||
keyboard: string;
|
||||
}
|
||||
|
||||
const languages: Language[] = [
|
||||
{ code: "en", name: "English", flag: "🇺🇸", keyboard: "E" },
|
||||
{ code: "zh", name: "中文", flag: "🇨🇳", keyboard: "Z" },
|
||||
];
|
||||
|
||||
export default function LangToggle() {
|
||||
const { i18n } = useTranslation();
|
||||
const [currentLng, setCurrentLng] = useState(languages[0]);
|
||||
const changeLanguage = (lng: Language) => {
|
||||
setCurrentLng(lng);
|
||||
i18n.changeLanguage(lng.code);
|
||||
};
|
||||
|
||||
return (
|
||||
<Menu>
|
||||
<MenuButton className="inline-flex items-center gap-2 rounded-md py-1.5 px-3 text-sm/6 font-semibold dark:text-white shadow-inner dark:shadow-white/10 focus:outline-none dark:data-[hover]:bg-gray-700 dark:data-[open]:bg-gray-700 data-[focus]:outline-1 dark:data-[focus]:outline-white">
|
||||
<Globe2 className="h-4 w-4 text-gray-600" />
|
||||
<span className="text-base">{currentLng.flag}</span>
|
||||
<span>{currentLng.name}</span>
|
||||
<ChevronDown className="size-4 dark:fill-white/60" />
|
||||
</MenuButton>
|
||||
<MenuItems
|
||||
anchor="bottom end"
|
||||
className="w-[160px] origin-top-right rounded-xl border dark:border-white/5 dark:bg-white/5 p-1 text-sm/6 dark:text-white transition duration-100 ease-out [--anchor-gap:var(--spacing-1)] focus:outline-none data-[closed]:scale-95 data-[closed]:opacity-0"
|
||||
>
|
||||
{languages.map((language) => (
|
||||
<MenuItem key={language.code}>
|
||||
<button
|
||||
className="group flex w-full items-center gap-2 rounded-lg py-1.5 px-3 dark:data-[focus]:bg-white/10"
|
||||
onClick={() => changeLanguage(language)}
|
||||
>
|
||||
<span className="mr-1 text-base">{language.flag}</span>
|
||||
<span>{language.name}</span>
|
||||
<kbd className="ml-auto hidden font-sans text-xs dark:text-white/50 group-data-[focus]:inline">
|
||||
⌘{language.keyboard}
|
||||
</kbd>
|
||||
</button>
|
||||
</MenuItem>
|
||||
))}
|
||||
</MenuItems>
|
||||
</Menu>
|
||||
);
|
||||
}
|
||||
28
src/components/Raycast.tsx
Normal file
28
src/components/Raycast.tsx
Normal file
@@ -0,0 +1,28 @@
|
||||
import { Command } from "lucide-react";
|
||||
import { CommandPalette } from "./CommandPalette";
|
||||
|
||||
function Raycast() {
|
||||
return (
|
||||
<div className="h-[100%] w-[100%]">
|
||||
<div className="mx-auto px-4 py-16">
|
||||
<div className="text-center">
|
||||
<div className="inline-flex items-center gap-2 bg-white px-4 py-2 rounded-lg shadow-sm border border-gray-200">
|
||||
<Command className="w-5 h-5 text-gray-500" />
|
||||
<span className="text-gray-600">Press</span>
|
||||
<kbd className="px-2 py-1 text-sm font-semibold text-gray-700 bg-gray-100 border border-gray-200 rounded-md">
|
||||
⌘
|
||||
</kbd>
|
||||
<kbd className="px-2 py-1 text-sm font-semibold text-gray-700 bg-gray-100 border border-gray-200 rounded-md">
|
||||
K
|
||||
</kbd>
|
||||
<span className="text-gray-600">to open command palette</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<CommandPalette />
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
export default Raycast;
|
||||
71
src/components/SearchChat/DocumentDetail.tsx
Normal file
71
src/components/SearchChat/DocumentDetail.tsx
Normal file
@@ -0,0 +1,71 @@
|
||||
import React from "react";
|
||||
import { Calendar, User, Clock } from "lucide-react";
|
||||
|
||||
interface DocumentDetailProps {
|
||||
documentId?: string;
|
||||
}
|
||||
|
||||
export const DocumentDetail: React.FC<DocumentDetailProps> = ({
|
||||
documentId,
|
||||
}) => {
|
||||
if (!documentId) {
|
||||
return (
|
||||
<div className="h-full flex items-center justify-center text-gray-400">
|
||||
请选择一个文档查看详情
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="p-8 space-y-8">
|
||||
<div className="space-y-6">
|
||||
<h2 className="text-2xl font-semibold text-gray-900">
|
||||
产品需求规划文档
|
||||
</h2>
|
||||
|
||||
<div className="flex items-center gap-6 text-sm text-gray-500">
|
||||
<div className="flex items-center gap-2">
|
||||
<Calendar className="w-4 h-4" />
|
||||
<span>2024-02-20</span>
|
||||
</div>
|
||||
<div className="flex items-center gap-2">
|
||||
<User className="w-4 h-4" />
|
||||
<span>张小明</span>
|
||||
</div>
|
||||
<div className="flex items-center gap-2">
|
||||
<Clock className="w-4 h-4" />
|
||||
<span>最近更新于 2小时前</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<img
|
||||
src="https://images.unsplash.com/photo-1664575602276-acd073f104c1"
|
||||
alt="Document preview"
|
||||
className="w-full aspect-video object-cover rounded-xl shadow-md"
|
||||
/>
|
||||
|
||||
<div className="prose prose-gray max-w-none">
|
||||
<h3 className="text-lg font-medium text-gray-900">文档概述</h3>
|
||||
<p className="text-gray-600 leading-relaxed">
|
||||
本文档详细说明了2024年Q1的产品规划方向和具体功能需求。包含了用户研究结果、
|
||||
竞品分析、功能优先级排序等重要内容。产品团队可以基于此文档进行后续的设计和开发工作。
|
||||
</p>
|
||||
|
||||
<h3 className="text-lg font-medium text-gray-900 mt-6">主要内容</h3>
|
||||
<ul className="list-disc pl-4 text-gray-600 space-y-2">
|
||||
<li>用户痛点分析与解决方案</li>
|
||||
<li>核心功能详细说明</li>
|
||||
<li>交互流程设计</li>
|
||||
<li>技术可行性评估</li>
|
||||
<li>项目时间节点规划</li>
|
||||
</ul>
|
||||
|
||||
<p className="text-gray-600 leading-relaxed mt-6">
|
||||
通过实施本文档中规划的功能,我们期望能够提升用户体验,增强产品竞争力,
|
||||
实现Q1的业务增长目标。
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
100
src/components/SearchChat/DocumentList.tsx
Normal file
100
src/components/SearchChat/DocumentList.tsx
Normal file
@@ -0,0 +1,100 @@
|
||||
import React from "react";
|
||||
import { FileText, Image, FileCode, Users, User, Globe } from "lucide-react";
|
||||
|
||||
interface Document {
|
||||
id: string;
|
||||
title: string;
|
||||
type: "text" | "image" | "code";
|
||||
owner: "personal" | "team" | "public";
|
||||
description: string;
|
||||
date: string;
|
||||
}
|
||||
|
||||
const documents: Document[] = [
|
||||
{
|
||||
id: "1",
|
||||
title: "产品需求规划文档.doc",
|
||||
type: "text",
|
||||
owner: "team",
|
||||
description:
|
||||
"2024年Q1产品规划及功能需求文档,包含详细的功能描述和交互设计说明。",
|
||||
date: "2024-02-20",
|
||||
},
|
||||
{
|
||||
id: "2",
|
||||
title: "UI设计规范.fig",
|
||||
type: "image",
|
||||
owner: "public",
|
||||
description: "最新的设计系统规范文档,包含组件库使用说明和设计标准。",
|
||||
date: "2024-02-19",
|
||||
},
|
||||
{
|
||||
id: "3",
|
||||
title: "API接口文档.ts",
|
||||
type: "code",
|
||||
owner: "personal",
|
||||
description:
|
||||
"TypeScript版本的API接口定义文档,包含所有接口的请求和响应类型。",
|
||||
date: "2024-02-18",
|
||||
},
|
||||
];
|
||||
|
||||
const getIcon = (type: Document["type"]) => {
|
||||
switch (type) {
|
||||
case "image":
|
||||
return <Image className="w-5 h-5 text-blue-500" />;
|
||||
case "code":
|
||||
return <FileCode className="w-5 h-5 text-green-500" />;
|
||||
default:
|
||||
return <FileText className="w-5 h-5 text-purple-500" />;
|
||||
}
|
||||
};
|
||||
|
||||
const getOwnerIcon = (owner: Document["owner"]) => {
|
||||
switch (owner) {
|
||||
case "team":
|
||||
return <Users className="w-4 h-4 text-blue-500" />;
|
||||
case "public":
|
||||
return <Globe className="w-4 h-4 text-green-500" />;
|
||||
default:
|
||||
return <User className="w-4 h-4 text-gray-500" />;
|
||||
}
|
||||
};
|
||||
|
||||
interface DocumentListProps {
|
||||
onSelectDocument: (id: string) => void;
|
||||
selectedId?: string;
|
||||
}
|
||||
|
||||
export const DocumentList: React.FC<DocumentListProps> = ({
|
||||
onSelectDocument,
|
||||
selectedId,
|
||||
}) => {
|
||||
return (
|
||||
<div className="space-y-1 py-2">
|
||||
{documents.map((doc) => (
|
||||
<button
|
||||
key={doc.id}
|
||||
onClick={() => onSelectDocument(doc.id)}
|
||||
className={`w-full flex items-start px-4 py-3 rounded-lg hover:bg-gray-50 transition-colors ${
|
||||
selectedId === doc.id ? "bg-blue-50" : ""
|
||||
}`}
|
||||
>
|
||||
<span className="mr-3 mt-0.5">{getIcon(doc.type)}</span>
|
||||
<div className="flex-1 text-left">
|
||||
<div className="flex items-center gap-2">
|
||||
<span className="text-sm font-medium text-gray-900">
|
||||
{doc.title}
|
||||
</span>
|
||||
<span className="mt-0.5">{getOwnerIcon(doc.owner)}</span>
|
||||
</div>
|
||||
<p className="text-xs text-gray-500 mt-1 line-clamp-2">
|
||||
{doc.description}
|
||||
</p>
|
||||
<span className="text-xs text-gray-400 mt-1 block">{doc.date}</span>
|
||||
</div>
|
||||
</button>
|
||||
))}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
106
src/components/SearchChat/SearchHeader.tsx
Normal file
106
src/components/SearchChat/SearchHeader.tsx
Normal file
@@ -0,0 +1,106 @@
|
||||
import React, { useState, Fragment } from "react";
|
||||
import { Menu, MenuButton, MenuItems, MenuItem } from "@headlessui/react";
|
||||
import { ChevronDown } from "lucide-react";
|
||||
|
||||
interface FilterOption {
|
||||
id: string;
|
||||
label: string;
|
||||
}
|
||||
|
||||
interface FilterDropdownProps {
|
||||
label: string;
|
||||
options: FilterOption[];
|
||||
value?: string;
|
||||
onChange: (value: string) => void;
|
||||
}
|
||||
|
||||
const FilterDropdown: React.FC<FilterDropdownProps> = ({
|
||||
label,
|
||||
options,
|
||||
value,
|
||||
onChange,
|
||||
}) => {
|
||||
return (
|
||||
<Menu as="div" className="relative text-xs">
|
||||
<MenuButton className="inline-flex items-center px-3 py-1.5 text-sm bg-white text-gray-700 hover:bg-gray-50 rounded-lg border border-gray-200 font-medium">
|
||||
{label}
|
||||
<ChevronDown className="w-4 h-4 ml-1.5 text-gray-500" />
|
||||
</MenuButton>
|
||||
|
||||
<MenuItems className="absolute right-0 mt-1 w-48 bg-white rounded-lg shadow-lg border border-gray-200 py-1 z-10 focus:outline-none">
|
||||
{options.map((option) => (
|
||||
<MenuItem key={option.id}>
|
||||
{({ active }) => (
|
||||
<button
|
||||
onClick={() => onChange(option.id)}
|
||||
className={`w-full text-left px-4 py-2 text-sm ${
|
||||
active ? "bg-gray-50" : ""
|
||||
} ${
|
||||
value === option.id
|
||||
? "text-blue-600 bg-blue-50"
|
||||
: "text-gray-700"
|
||||
}`}
|
||||
>
|
||||
{option.label}
|
||||
</button>
|
||||
)}
|
||||
</MenuItem>
|
||||
))}
|
||||
</MenuItems>
|
||||
</Menu>
|
||||
);
|
||||
};
|
||||
|
||||
const typeOptions: FilterOption[] = [
|
||||
{ id: "all", label: "全部类型" },
|
||||
{ id: "doc", label: "文档" },
|
||||
{ id: "image", label: "图片" },
|
||||
{ id: "code", label: "代码" },
|
||||
];
|
||||
|
||||
const ownerOptions: FilterOption[] = [
|
||||
{ id: "all", label: "全部归属" },
|
||||
{ id: "personal", label: "个人" },
|
||||
{ id: "team", label: "团队" },
|
||||
{ id: "public", label: "公开" },
|
||||
];
|
||||
|
||||
const creatorOptions: FilterOption[] = [
|
||||
{ id: "all", label: "全部创建者" },
|
||||
{ id: "me", label: "我创建的" },
|
||||
{ id: "shared", label: "共享给我的" },
|
||||
];
|
||||
|
||||
export const SearchHeader: React.FC = () => {
|
||||
const [typeFilter, setTypeFilter] = useState("all");
|
||||
const [ownerFilter, setOwnerFilter] = useState("all");
|
||||
const [creatorFilter, setCreatorFilter] = useState("all");
|
||||
|
||||
return (
|
||||
<div className="flex items-center justify-between py-4 border-b border-gray-200 text-xs">
|
||||
<div className="text-gray-600">
|
||||
搜索到 <span className="font-medium text-gray-900">200</span> 条数据
|
||||
</div>
|
||||
<div className="flex gap-3">
|
||||
<FilterDropdown
|
||||
label="类型"
|
||||
options={typeOptions}
|
||||
value={typeFilter}
|
||||
onChange={setTypeFilter}
|
||||
/>
|
||||
<FilterDropdown
|
||||
label="归属"
|
||||
options={ownerOptions}
|
||||
value={ownerFilter}
|
||||
onChange={setOwnerFilter}
|
||||
/>
|
||||
<FilterDropdown
|
||||
label="创建者"
|
||||
options={creatorOptions}
|
||||
value={creatorFilter}
|
||||
onChange={setCreatorFilter}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
33
src/components/SearchChat/SearchResults.tsx
Normal file
33
src/components/SearchChat/SearchResults.tsx
Normal file
@@ -0,0 +1,33 @@
|
||||
import React, { useState } from "react";
|
||||
|
||||
import { SearchHeader } from "./SearchHeader";
|
||||
import { DocumentList } from "./DocumentList";
|
||||
import { DocumentDetail } from "./DocumentDetail";
|
||||
|
||||
export const SearchResults: React.FC = () => {
|
||||
const [selectedDocumentId, setSelectedDocumentId] = useState("1"); // Default to first document
|
||||
|
||||
return (
|
||||
<div className="bg-white rounded-xl shadow-sm border border-gray-200 mt-4 overflow-hidden">
|
||||
<div className="flex h-[calc(100vh-220px)]">
|
||||
{/* Left Panel */}
|
||||
<div className="w-[420px] border-r border-gray-200 flex flex-col overflow-hidden">
|
||||
<div className="px-4 flex-shrink-0">
|
||||
<SearchHeader />
|
||||
</div>
|
||||
<div className="overflow-y-auto flex-1">
|
||||
<DocumentList
|
||||
onSelectDocument={setSelectedDocumentId}
|
||||
selectedId={selectedDocumentId}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Right Panel */}
|
||||
<div className="flex-1 overflow-y-auto">
|
||||
<DocumentDetail documentId={selectedDocumentId} />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
110
src/components/SearchChat/index.tsx
Normal file
110
src/components/SearchChat/index.tsx
Normal file
@@ -0,0 +1,110 @@
|
||||
import React, { useState } from "react";
|
||||
import { Mic, Filter, Upload, MessageSquare } from "lucide-react";
|
||||
import { Switch } from "@headlessui/react";
|
||||
|
||||
import { SearchResults } from "./SearchResults";
|
||||
|
||||
interface Tag {
|
||||
id: string;
|
||||
text: string;
|
||||
}
|
||||
|
||||
function Search() {
|
||||
const [tags, setTags] = useState<Tag[]>([]);
|
||||
const [input, setInput] = useState("");
|
||||
const [isChatMode, setIsChatMode] = useState(false);
|
||||
|
||||
const handleKeyDown = (e: React.KeyboardEvent) => {
|
||||
if (e.key === "Enter" && input.trim()) {
|
||||
setTags([...tags, { id: Date.now().toString(), text: input.trim() }]);
|
||||
setInput("");
|
||||
}
|
||||
};
|
||||
|
||||
const removeTag = (tagId: string) => {
|
||||
setTags(tags.filter((tag) => tag.id !== tagId));
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="min-h-screen bg-gray-50 flex items-start justify-center pt-10 px-4">
|
||||
<div className="w-full max-w-3xl space-y-4">
|
||||
<div className="border b-t-none border-gray-200 rounded-xl">
|
||||
{/* Search Bar */}
|
||||
<div className="relative">
|
||||
<div className="flex items-center bg-white rounded-xl shadow-sm border border-gray-200 p-2 focus-within:ring-2 focus-within:ring-blue-100 focus-within:border-blue-400 transition-all">
|
||||
<div className="flex flex-wrap gap-2 flex-1 min-h-12 items-center">
|
||||
{tags.map((tag) => (
|
||||
<span
|
||||
key={tag.id}
|
||||
className="inline-flex items-center bg-blue-50 text-blue-600 px-2.5 py-1 rounded-lg text-sm"
|
||||
>
|
||||
{tag.text}
|
||||
<button
|
||||
onClick={() => removeTag(tag.id)}
|
||||
className="ml-1.5 text-blue-400 hover:text-blue-600"
|
||||
>
|
||||
×
|
||||
</button>
|
||||
</span>
|
||||
))}
|
||||
<input
|
||||
type="text"
|
||||
className="flex-1 outline-none min-w-[200px] text-gray-800 placeholder-gray-400"
|
||||
placeholder="有问题尽管问 Coco"
|
||||
value={input}
|
||||
onChange={(e) => setInput(e.target.value)}
|
||||
onKeyDown={handleKeyDown}
|
||||
/>
|
||||
</div>
|
||||
<button className="p-2 hover:bg-gray-50 rounded-lg transition-colors">
|
||||
<Mic className="w-5 h-5 text-gray-400" />
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Controls */}
|
||||
<div className="flex justify-between items-center p-2">
|
||||
<div className="flex gap-3">
|
||||
<button className="inline-flex items-center px-2 py-1 bg-blue-600 text-white rounded-lg hover:bg-blue-700 transition-colors">
|
||||
<MessageSquare className="w-4 h-4 mr-2" />问 Coco
|
||||
</button>
|
||||
<button className="inline-flex items-center px-2 py-1 bg-white border border-gray-200 rounded-lg hover:bg-gray-50 transition-colors text-gray-700">
|
||||
<Filter className="w-4 h-4 mr-2" />
|
||||
筛选
|
||||
</button>
|
||||
<button className="inline-flex items-center px-2 py-1 bg-white border border-gray-200 rounded-lg hover:bg-gray-50 transition-colors text-gray-700">
|
||||
<Upload className="w-4 h-4 mr-2" />
|
||||
上传
|
||||
</button>
|
||||
</div>
|
||||
|
||||
{/* Switch */}
|
||||
<div className="flex items-center">
|
||||
<span className="mr-3 text-sm font-medium text-gray-700">
|
||||
Chat 模式
|
||||
</span>
|
||||
<Switch
|
||||
checked={isChatMode}
|
||||
onChange={setIsChatMode}
|
||||
className={`${
|
||||
isChatMode ? "bg-blue-600" : "bg-gray-200"
|
||||
} relative inline-flex h-6 w-11 items-center rounded-full transition-colors focus:outline-none focus:ring-2 focus:ring-blue-500 focus:ring-offset-2`}
|
||||
>
|
||||
<span
|
||||
className={`${
|
||||
isChatMode ? "translate-x-6" : "translate-x-1"
|
||||
} inline-block h-4 w-4 transform rounded-full bg-white transition-transform`}
|
||||
/>
|
||||
</Switch>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Search Results Panel */}
|
||||
<SearchResults />
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
export default Search;
|
||||
107
src/components/Settings/GeneralSettings.tsx
Normal file
107
src/components/Settings/GeneralSettings.tsx
Normal file
@@ -0,0 +1,107 @@
|
||||
import { useState } from "react";
|
||||
import {
|
||||
Command,
|
||||
Monitor,
|
||||
Palette,
|
||||
Layout,
|
||||
Star,
|
||||
Moon,
|
||||
Sun
|
||||
} from "lucide-react";
|
||||
|
||||
import SettingsItem from "./SettingsItem";
|
||||
import SettingsSelect from "./SettingsSelect";
|
||||
import SettingsToggle from "./SettingsToggle";
|
||||
import { ThemeOption } from "./index2";
|
||||
|
||||
interface GeneralSettingsProps {
|
||||
theme: "light" | "dark" | "system";
|
||||
setTheme: (theme: "light" | "dark" | "system") => void;
|
||||
}
|
||||
|
||||
export default function GeneralSettings({
|
||||
theme,
|
||||
setTheme,
|
||||
}: GeneralSettingsProps) {
|
||||
const [launchAtLogin, setLaunchAtLogin] = useState(true);
|
||||
|
||||
return (
|
||||
<div className="space-y-8">
|
||||
<div>
|
||||
<h2 className="text-lg font-semibold text-gray-900 dark:text-white mb-4">
|
||||
General Settings
|
||||
</h2>
|
||||
<div className="space-y-6">
|
||||
<SettingsItem
|
||||
icon={Command}
|
||||
title="Launch at Login"
|
||||
description="Automatically start Coco when you login"
|
||||
>
|
||||
<SettingsToggle
|
||||
checked={launchAtLogin}
|
||||
onChange={setLaunchAtLogin}
|
||||
label="Launch at login"
|
||||
/>
|
||||
</SettingsItem>
|
||||
|
||||
<SettingsItem
|
||||
icon={Command}
|
||||
title="Coco Hotkey"
|
||||
description="Global shortcut to open Coco"
|
||||
>
|
||||
<button className="px-3 py-1.5 text-sm bg-gray-100 dark:bg-gray-700 rounded-md border border-gray-200 dark:border-gray-600 hover:bg-gray-200 dark:hover:bg-gray-600 transition-colors duration-200">
|
||||
⌘ Space
|
||||
</button>
|
||||
</SettingsItem>
|
||||
|
||||
<SettingsItem
|
||||
icon={Monitor}
|
||||
title="Window Mode"
|
||||
description="Choose how Coco appears on your screen"
|
||||
>
|
||||
<SettingsSelect
|
||||
options={["Standard Window", "Compact Mode", "Full Screen"]}
|
||||
/>
|
||||
</SettingsItem>
|
||||
|
||||
<SettingsItem
|
||||
icon={Palette}
|
||||
title="Appearance"
|
||||
description="Choose your preferred theme"
|
||||
>
|
||||
<SettingsSelect
|
||||
options={["Light", "Dark", "system"]}
|
||||
value={theme}
|
||||
onChange={(value) =>
|
||||
setTheme(value as "light" | "dark" | "system")
|
||||
}
|
||||
/>
|
||||
</SettingsItem>
|
||||
<div className="grid grid-cols-3 gap-4">
|
||||
<ThemeOption icon={Sun} title="Light" theme="light" />
|
||||
<ThemeOption icon={Moon} title="Dark" theme="dark" />
|
||||
<ThemeOption icon={Monitor} title="System" theme="system" />
|
||||
</div>
|
||||
|
||||
<SettingsItem
|
||||
icon={Layout}
|
||||
title="Text Size"
|
||||
description="Adjust the application text size"
|
||||
>
|
||||
<SettingsSelect options={["Small", "Medium", "Large"]} />
|
||||
</SettingsItem>
|
||||
|
||||
<SettingsItem
|
||||
icon={Star}
|
||||
title="Favorites"
|
||||
description="Manage your favorite commands"
|
||||
>
|
||||
<button className="text-sm text-blue-600 dark:text-blue-400 hover:text-blue-700 dark:hover:text-blue-300 font-medium transition-colors duration-200">
|
||||
Manage Favorites
|
||||
</button>
|
||||
</SettingsItem>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
32
src/components/Settings/SettingsItem.tsx
Normal file
32
src/components/Settings/SettingsItem.tsx
Normal file
@@ -0,0 +1,32 @@
|
||||
import { LucideIcon } from "lucide-react";
|
||||
|
||||
interface SettingsItemProps {
|
||||
icon: LucideIcon;
|
||||
title: string;
|
||||
description: string;
|
||||
children: React.ReactNode;
|
||||
}
|
||||
|
||||
export default function SettingsItem({
|
||||
icon: Icon,
|
||||
title,
|
||||
description,
|
||||
children,
|
||||
}: SettingsItemProps) {
|
||||
return (
|
||||
<div className="flex items-center justify-between">
|
||||
<div className="flex items-center space-x-3">
|
||||
<Icon className="h-5 w-5 text-gray-400 dark:text-gray-500" />
|
||||
<div>
|
||||
<h3 className="text-sm font-medium text-gray-900 dark:text-gray-100">
|
||||
{title}
|
||||
</h3>
|
||||
<p className="text-sm text-gray-500 dark:text-gray-400">
|
||||
{description}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
{children}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
17
src/components/Settings/SettingsPanel.tsx
Normal file
17
src/components/Settings/SettingsPanel.tsx
Normal file
@@ -0,0 +1,17 @@
|
||||
import React from "react";
|
||||
|
||||
interface SettingsPanelProps {
|
||||
title: string;
|
||||
children: React.ReactNode;
|
||||
}
|
||||
|
||||
const SettingsPanel: React.FC<SettingsPanelProps> = ({ title, children }) => {
|
||||
return (
|
||||
<div className="bg-white dark:bg-gray-800 rounded-xl p-6 shadow-sm">
|
||||
{/* <h2 className="text-xl font-semibold mb-6">{title}</h2> */}
|
||||
{children}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default SettingsPanel;
|
||||
27
src/components/Settings/SettingsSelect.tsx
Normal file
27
src/components/Settings/SettingsSelect.tsx
Normal file
@@ -0,0 +1,27 @@
|
||||
import { Select } from '@headlessui/react'
|
||||
|
||||
interface SettingsSelectProps {
|
||||
options: string[];
|
||||
value?: string;
|
||||
onChange?: (value: string) => void;
|
||||
}
|
||||
|
||||
export default function SettingsSelect({
|
||||
options,
|
||||
value,
|
||||
onChange,
|
||||
}: SettingsSelectProps) {
|
||||
return (
|
||||
<Select
|
||||
value={value}
|
||||
onChange={(e) => onChange?.(e.target.value)}
|
||||
className="rounded-md border-gray-300 dark:border-gray-600 bg-white dark:bg-gray-700 text-gray-900 dark:text-gray-100 shadow-sm focus:border-blue-500 focus:ring-blue-500"
|
||||
>
|
||||
{options.map((option) => (
|
||||
<option key={option} value={option.toLowerCase()}>
|
||||
{option}
|
||||
</option>
|
||||
))}
|
||||
</Select>
|
||||
);
|
||||
}
|
||||
30
src/components/Settings/SettingsToggle.tsx
Normal file
30
src/components/Settings/SettingsToggle.tsx
Normal file
@@ -0,0 +1,30 @@
|
||||
import { Switch } from "@headlessui/react";
|
||||
|
||||
interface SettingsToggleProps {
|
||||
checked: boolean;
|
||||
onChange: (checked: boolean) => void;
|
||||
label: string;
|
||||
}
|
||||
|
||||
export default function SettingsToggle({
|
||||
checked,
|
||||
onChange,
|
||||
label,
|
||||
}: SettingsToggleProps) {
|
||||
return (
|
||||
<Switch
|
||||
checked={checked}
|
||||
onChange={onChange}
|
||||
className={`${checked ? "bg-blue-600" : "bg-gray-200 dark:bg-gray-700"}
|
||||
relative inline-flex h-6 w-11 flex-shrink-0 cursor-pointer rounded-full border-2 border-transparent
|
||||
transition-colors duration-200 ease-in-out focus:outline-none focus:ring-2 focus:ring-blue-500 focus:ring-offset-2`}
|
||||
>
|
||||
<span className="sr-only">{label}</span>
|
||||
<span
|
||||
className={`${checked ? "translate-x-5" : "translate-x-0"}
|
||||
pointer-events-none relative inline-block h-5 w-5 transform rounded-full bg-white shadow
|
||||
ring-0 transition duration-200 ease-in-out`}
|
||||
/>
|
||||
</Switch>
|
||||
);
|
||||
}
|
||||
48
src/components/Settings/ThemeSelector.tsx
Normal file
48
src/components/Settings/ThemeSelector.tsx
Normal file
@@ -0,0 +1,48 @@
|
||||
import React, { useContext } from 'react';
|
||||
import { Menu } from '@headlessui/react';
|
||||
import { Monitor, Moon, Sun } from 'lucide-react';
|
||||
import { Theme, ThemeContext } from './index2';
|
||||
|
||||
const ThemeSelector = () => {
|
||||
const { theme, setTheme } = useContext(ThemeContext);
|
||||
|
||||
const themes: { value: Theme; label: string; icon: any }[] = [
|
||||
{ value: 'light', label: 'Light', icon: Sun },
|
||||
{ value: 'dark', label: 'Dark', icon: Moon },
|
||||
{ value: 'system', label: 'System', icon: Monitor },
|
||||
];
|
||||
|
||||
const currentTheme = themes.find((t) => t.value === theme);
|
||||
|
||||
return (
|
||||
<Menu as="div" className="relative">
|
||||
<Menu.Button className="flex items-center space-x-2 px-4 py-2 rounded-lg bg-gray-100 dark:bg-gray-800 hover:bg-gray-200 dark:hover:bg-gray-700 transition-colors">
|
||||
{currentTheme && <currentTheme.icon className="w-4 h-4" />}
|
||||
<span>{currentTheme?.label}</span>
|
||||
</Menu.Button>
|
||||
<Menu.Items className="absolute right-0 mt-2 w-48 rounded-lg bg-white dark:bg-gray-800 shadow-lg ring-1 ring-black ring-opacity-5 focus:outline-none">
|
||||
<div className="p-1">
|
||||
{themes.map((item) => (
|
||||
<Menu.Item key={item.value}>
|
||||
{({ active }) => (
|
||||
<button
|
||||
onClick={() => setTheme(item.value)}
|
||||
className={`${
|
||||
active
|
||||
? 'bg-gray-100 dark:bg-gray-700'
|
||||
: 'text-gray-900 dark:text-gray-100'
|
||||
} group flex w-full items-center rounded-md px-2 py-2 text-sm`}
|
||||
>
|
||||
<item.icon className="w-4 h-4 mr-2" />
|
||||
{item.label}
|
||||
</button>
|
||||
)}
|
||||
</Menu.Item>
|
||||
))}
|
||||
</div>
|
||||
</Menu.Items>
|
||||
</Menu>
|
||||
);
|
||||
};
|
||||
|
||||
export default ThemeSelector;
|
||||
145
src/components/Settings/index.tsx
Normal file
145
src/components/Settings/index.tsx
Normal file
@@ -0,0 +1,145 @@
|
||||
import { useState } from "react";
|
||||
import {
|
||||
Settings,
|
||||
Search,
|
||||
Command,
|
||||
Keyboard,
|
||||
Globe,
|
||||
Zap,
|
||||
ChevronRight,
|
||||
Moon,
|
||||
Sun,
|
||||
} from "lucide-react";
|
||||
|
||||
function NavItem({ icon: Icon, label, active, onClick }: any) {
|
||||
return (
|
||||
<button
|
||||
onClick={onClick}
|
||||
className={`w-full flex items-center px-3 py-2 rounded-lg text-sm ${
|
||||
active
|
||||
? "bg-indigo-50 text-indigo-600"
|
||||
: "text-gray-700 hover:bg-gray-100"
|
||||
}`}
|
||||
>
|
||||
<Icon className="w-4 h-4 mr-3" />
|
||||
<span className="font-medium">{label}</span>
|
||||
</button>
|
||||
);
|
||||
}
|
||||
|
||||
function SettingItem({ icon: Icon, title, description, action }: any) {
|
||||
return (
|
||||
<div className="flex items-center justify-between py-4 border-b border-gray-100">
|
||||
<div className="flex items-start space-x-3">
|
||||
<div className="p-2 bg-gray-50 rounded-lg">
|
||||
<Icon className="w-5 h-5 text-gray-600" />
|
||||
</div>
|
||||
<div>
|
||||
<h3 className="text-sm font-medium text-gray-900">{title}</h3>
|
||||
<p className="text-sm text-gray-500">{description}</p>
|
||||
</div>
|
||||
</div>
|
||||
{action}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
function AppSettings() {
|
||||
const [activeSection, setActiveSection] = useState("general");
|
||||
const [darkMode, setDarkMode] = useState(false);
|
||||
|
||||
const sections = [
|
||||
{ id: "general", label: "General", icon: Settings },
|
||||
{ id: "appearance", label: "Appearance", icon: Search },
|
||||
{ id: "extensions", label: "Extensions", icon: Command },
|
||||
{ id: "keyboard", label: "Keyboard", icon: Keyboard },
|
||||
{ id: "advanced", label: "Advanced", icon: Zap },
|
||||
];
|
||||
|
||||
return (
|
||||
<div className="min-h-screen bg-gray-50">
|
||||
<div className="max-w-7xl mx-auto px-4 py-8">
|
||||
<div className="bg-white rounded-2xl shadow-sm border border-gray-200 flex min-h-[500px]">
|
||||
<div className="w-64 p-4 border-r border-gray-100">
|
||||
<div className="space-y-1">
|
||||
{sections.map((section) => (
|
||||
<NavItem
|
||||
key={section.id}
|
||||
icon={section.icon}
|
||||
label={section.label}
|
||||
active={activeSection === section.id}
|
||||
onClick={() => setActiveSection(section.id)}
|
||||
/>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Main Content */}
|
||||
<div className="flex-1 p-6">
|
||||
<div className="max-w-3xl">
|
||||
<h1 className="text-2xl font-semibold text-gray-900 mb-6">
|
||||
Settings
|
||||
</h1>
|
||||
|
||||
<div className="space-y-1">
|
||||
<SettingItem
|
||||
icon={Globe}
|
||||
title="Language"
|
||||
description="Choose your preferred language for the interface"
|
||||
action={
|
||||
<button className="flex items-center text-sm text-gray-600 hover:text-gray-900">
|
||||
English
|
||||
<ChevronRight className="w-4 h-4 ml-1" />
|
||||
</button>
|
||||
}
|
||||
/>
|
||||
|
||||
<SettingItem
|
||||
icon={darkMode ? Moon : Sun}
|
||||
title="Appearance"
|
||||
description="Switch between light and dark mode"
|
||||
action={
|
||||
<button
|
||||
onClick={() => setDarkMode(!darkMode)}
|
||||
className="relative inline-flex h-6 w-11 items-center rounded-full bg-gray-200 transition-colors focus:outline-none focus:ring-2 focus:ring-indigo-500 focus:ring-offset-2"
|
||||
>
|
||||
<span
|
||||
className={`${
|
||||
darkMode ? "translate-x-6" : "translate-x-1"
|
||||
} inline-block h-4 w-4 transform rounded-full bg-white transition-transform`}
|
||||
/>
|
||||
</button>
|
||||
}
|
||||
/>
|
||||
|
||||
<SettingItem
|
||||
icon={Command}
|
||||
title="Keyboard Shortcuts"
|
||||
description="Customize your keyboard shortcuts"
|
||||
action={
|
||||
<button className="px-3 py-1 text-sm text-gray-600 hover:text-gray-900 border border-gray-200 rounded-md hover:border-gray-300">
|
||||
Configure
|
||||
</button>
|
||||
}
|
||||
/>
|
||||
|
||||
<SettingItem
|
||||
icon={Zap}
|
||||
title="Performance Mode"
|
||||
description="Optimize for better performance"
|
||||
action={
|
||||
<button className="relative inline-flex h-6 w-11 items-center rounded-full bg-indigo-600 transition-colors focus:outline-none focus:ring-2 focus:ring-indigo-500 focus:ring-offset-2">
|
||||
<span className="translate-x-6 inline-block h-4 w-4 transform rounded-full bg-white transition-transform" />
|
||||
</button>
|
||||
}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
export default AppSettings;
|
||||
157
src/components/Settings/index2.tsx
Normal file
157
src/components/Settings/index2.tsx
Normal file
@@ -0,0 +1,157 @@
|
||||
import { createContext, useContext, useState } from "react";
|
||||
import { Tab, TabGroup, TabList, TabPanel, TabPanels } from "@headlessui/react";
|
||||
import {
|
||||
Monitor,
|
||||
Moon,
|
||||
Sun,
|
||||
Keyboard,
|
||||
Bell,
|
||||
Palette,
|
||||
Shield,
|
||||
Workflow,
|
||||
Settings,
|
||||
Puzzle,
|
||||
User,
|
||||
Users,
|
||||
Settings2,
|
||||
Info,
|
||||
} from "lucide-react";
|
||||
import SettingsPanel from "./SettingsPanel";
|
||||
import GeneralSettings from "./GeneralSettings";
|
||||
import Footer from "../Footer";
|
||||
|
||||
export type Theme = "light" | "dark" | "system";
|
||||
|
||||
export const ThemeContext = createContext<{
|
||||
theme: Theme;
|
||||
setTheme: (theme: Theme) => void;
|
||||
}>({
|
||||
theme: "system",
|
||||
setTheme: () => {},
|
||||
});
|
||||
|
||||
function SettingsPage() {
|
||||
const [theme, setTheme] = useState<Theme>("system");
|
||||
|
||||
const tabs = [
|
||||
{ name: "General", icon: Settings },
|
||||
{ name: "Extensions", icon: Puzzle },
|
||||
{ name: "Account", icon: User },
|
||||
{ name: "Organizations", icon: Users },
|
||||
{ name: "Advanced", icon: Settings2 },
|
||||
{ name: "About", icon: Info },
|
||||
];
|
||||
|
||||
return (
|
||||
<ThemeContext.Provider value={{ theme, setTheme }}>
|
||||
<div className={theme}>
|
||||
<div className="min-h-screen bg-white dark:bg-gray-900 text-gray-900 dark:text-white">
|
||||
<div className="max-w-6xl mx-auto p-4">
|
||||
<div className="flex items-center justify-center mb-2">
|
||||
<h1 className="text-xl font-bold">Coco Settings</h1>
|
||||
</div>
|
||||
|
||||
<TabGroup>
|
||||
<TabList className="flex space-x-1 rounded-xl bg-gray-100 dark:bg-gray-800 p-1">
|
||||
{tabs.map((tab) => (
|
||||
<Tab
|
||||
key={tab.name}
|
||||
className={({ selected }) =>
|
||||
`w-full rounded-lg py-2.5 text-sm font-medium leading-5
|
||||
${
|
||||
selected
|
||||
? "bg-white dark:bg-gray-700 shadow text-gray-900 dark:text-white"
|
||||
: "text-gray-700 dark:text-gray-400 hover:bg-white/[0.12] hover:text-gray-900 dark:hover:text-white"
|
||||
}
|
||||
flex items-center justify-center space-x-2 focus:outline-none`
|
||||
}
|
||||
>
|
||||
<tab.icon className="w-4 h-4" />
|
||||
<span>{tab.name}</span>
|
||||
</Tab>
|
||||
))}
|
||||
</TabList>
|
||||
|
||||
<TabPanels className="mt-6">
|
||||
<TabPanel>
|
||||
<SettingsPanel title="">
|
||||
<GeneralSettings theme={theme} setTheme={setTheme} />
|
||||
</SettingsPanel>
|
||||
</TabPanel>
|
||||
<TabPanel>
|
||||
<SettingsPanel title="">
|
||||
<div className="text-gray-600 dark:text-gray-400">
|
||||
Extensions settings content
|
||||
</div>
|
||||
</SettingsPanel>
|
||||
</TabPanel>
|
||||
<TabPanel>
|
||||
<SettingsPanel title="">
|
||||
<div className="text-gray-600 dark:text-gray-400">
|
||||
Account settings content
|
||||
</div>
|
||||
</SettingsPanel>
|
||||
</TabPanel>
|
||||
<TabPanel>
|
||||
<SettingsPanel title="">
|
||||
<div className="text-gray-600 dark:text-gray-400">
|
||||
Organizations settings content
|
||||
</div>
|
||||
</SettingsPanel>
|
||||
</TabPanel>
|
||||
<TabPanel>
|
||||
<SettingsPanel title="">
|
||||
<div className="text-gray-600 dark:text-gray-400">
|
||||
Advanced settings content
|
||||
</div>
|
||||
</SettingsPanel>
|
||||
</TabPanel>
|
||||
<TabPanel>
|
||||
<SettingsPanel title="">
|
||||
<div className="text-gray-600 dark:text-gray-400">
|
||||
About settings content
|
||||
</div>
|
||||
</SettingsPanel>
|
||||
</TabPanel>
|
||||
</TabPanels>
|
||||
</TabGroup>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<Footer />
|
||||
</ThemeContext.Provider>
|
||||
);
|
||||
}
|
||||
|
||||
export function ThemeOption({
|
||||
icon: Icon,
|
||||
title,
|
||||
theme,
|
||||
}: {
|
||||
icon: any;
|
||||
title: string;
|
||||
theme: Theme;
|
||||
}) {
|
||||
const { theme: currentTheme, setTheme } = useContext(ThemeContext);
|
||||
const isSelected = currentTheme === theme;
|
||||
|
||||
return (
|
||||
<button
|
||||
onClick={() => setTheme(theme)}
|
||||
className={`p-4 rounded-lg border-2 ${
|
||||
isSelected
|
||||
? "border-blue-500 bg-blue-50 dark:bg-blue-900/20"
|
||||
: "border-gray-200 dark:border-gray-700 hover:border-gray-300 dark:hover:border-gray-600"
|
||||
} flex flex-col items-center justify-center space-y-2 transition-all`}
|
||||
>
|
||||
<Icon className={`w-6 h-6 ${isSelected ? "text-blue-500" : ""}`} />
|
||||
<span
|
||||
className={`text-sm font-medium ${isSelected ? "text-blue-500" : ""}`}
|
||||
>
|
||||
{title}
|
||||
</span>
|
||||
</button>
|
||||
);
|
||||
}
|
||||
|
||||
export default SettingsPage;
|
||||
73
src/components/ThemeProvider.tsx
Normal file
73
src/components/ThemeProvider.tsx
Normal file
@@ -0,0 +1,73 @@
|
||||
import { createContext, useContext, useEffect, useState } from "react";
|
||||
|
||||
type Theme = "dark" | "light" | "system";
|
||||
|
||||
type ThemeProviderProps = {
|
||||
children: React.ReactNode;
|
||||
defaultTheme?: Theme;
|
||||
storageKey?: string;
|
||||
};
|
||||
|
||||
type ThemeProviderState = {
|
||||
theme: Theme;
|
||||
setTheme: (theme: Theme) => void;
|
||||
};
|
||||
|
||||
const initialState: ThemeProviderState = {
|
||||
theme: "system",
|
||||
setTheme: () => null,
|
||||
};
|
||||
|
||||
const ThemeProviderContext = createContext<ThemeProviderState>(initialState);
|
||||
|
||||
export function ThemeProvider({
|
||||
children,
|
||||
defaultTheme = "system",
|
||||
storageKey = "theme",
|
||||
...props
|
||||
}: ThemeProviderProps) {
|
||||
const [theme, setTheme] = useState<Theme>(
|
||||
() => (localStorage.getItem(storageKey) as Theme) || defaultTheme
|
||||
);
|
||||
|
||||
useEffect(() => {
|
||||
const root = window.document.documentElement;
|
||||
|
||||
root.classList.remove("light", "dark");
|
||||
|
||||
if (theme === "system") {
|
||||
const systemTheme = window.matchMedia("(prefers-color-scheme: dark)")
|
||||
.matches
|
||||
? "dark"
|
||||
: "light";
|
||||
|
||||
root.classList.add(systemTheme);
|
||||
return;
|
||||
}
|
||||
|
||||
root.classList.add(theme);
|
||||
}, [theme]);
|
||||
|
||||
const value = {
|
||||
theme,
|
||||
setTheme: (theme: Theme) => {
|
||||
localStorage.setItem(storageKey, theme);
|
||||
setTheme(theme);
|
||||
},
|
||||
};
|
||||
|
||||
return (
|
||||
<ThemeProviderContext.Provider {...props} value={value}>
|
||||
{children}
|
||||
</ThemeProviderContext.Provider>
|
||||
);
|
||||
}
|
||||
|
||||
export const useTheme = () => {
|
||||
const context = useContext(ThemeProviderContext);
|
||||
|
||||
if (context === undefined)
|
||||
throw new Error("useTheme 必须在 ThemeProvider 中使用");
|
||||
|
||||
return context;
|
||||
};
|
||||
24
src/components/ThemeToggle.tsx
Normal file
24
src/components/ThemeToggle.tsx
Normal file
@@ -0,0 +1,24 @@
|
||||
import { Sun, Moon } from "lucide-react";
|
||||
|
||||
import { useTheme } from "./ThemeProvider";
|
||||
|
||||
const ThemeToggle = () => {
|
||||
const { theme, setTheme } = useTheme();
|
||||
return (
|
||||
<button
|
||||
onClick={() => setTheme(theme === "light" ? "dark" : "light")}
|
||||
className="inline-flex rounded-md p-2 hover:bg-zinc-100 dark:hover:bg-zinc-900"
|
||||
>
|
||||
<Sun
|
||||
size={20}
|
||||
className="rotate-0 scale-100 transition-all dark:-rotate-90 dark:scale-0"
|
||||
/>
|
||||
<Moon
|
||||
size={20}
|
||||
className="absolute rotate-90 scale-0 transition-all dark:rotate-0 dark:scale-100"
|
||||
/>
|
||||
</button>
|
||||
);
|
||||
};
|
||||
|
||||
export default ThemeToggle;
|
||||
16
src/error-page.tsx
Normal file
16
src/error-page.tsx
Normal file
@@ -0,0 +1,16 @@
|
||||
import { useRouteError } from "react-router-dom";
|
||||
|
||||
export default function ErrorPage() {
|
||||
const error: any = useRouteError();
|
||||
console.error(error);
|
||||
|
||||
return (
|
||||
<div id="error-page">
|
||||
<h1>Oops!</h1>
|
||||
<p>Sorry, an unexpected error has occurred.</p>
|
||||
<p>
|
||||
<i>{error.statusText || error.message}</i>
|
||||
</p>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
20
src/hooks/useEscape.ts
Normal file
20
src/hooks/useEscape.ts
Normal file
@@ -0,0 +1,20 @@
|
||||
import { invoke } from "@tauri-apps/api/core";
|
||||
import { useEffect } from "react";
|
||||
|
||||
const useEscape = () => {
|
||||
const handleEscape = (event: KeyboardEvent) => {
|
||||
if (event.key === "Escape") {
|
||||
event.preventDefault();
|
||||
|
||||
invoke("hide");
|
||||
}
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
window.addEventListener("keydown", handleEscape);
|
||||
|
||||
return () => window.removeEventListener("keydown", handleEscape);
|
||||
}, []);
|
||||
};
|
||||
|
||||
export default useEscape;
|
||||
23
src/i18n.ts
Normal file
23
src/i18n.ts
Normal file
@@ -0,0 +1,23 @@
|
||||
import i18n from "i18next";
|
||||
import { initReactI18next } from "react-i18next";
|
||||
|
||||
import enTranslation from "./locales/en/translation.json";
|
||||
import zhTranslation from "./locales/zh/translation.json";
|
||||
|
||||
i18n.use(initReactI18next).init({
|
||||
resources: {
|
||||
en: {
|
||||
translation: enTranslation,
|
||||
},
|
||||
zh: {
|
||||
translation: zhTranslation,
|
||||
},
|
||||
},
|
||||
lng: "en",
|
||||
fallbackLng: "en",
|
||||
interpolation: {
|
||||
escapeValue: false,
|
||||
},
|
||||
});
|
||||
|
||||
export default i18n;
|
||||
12
src/icons/ArrowLeft.tsx
Normal file
12
src/icons/ArrowLeft.tsx
Normal file
@@ -0,0 +1,12 @@
|
||||
import SVGWrap from "./SVGWrap";
|
||||
|
||||
export default function ArrowLeft(props: I.SVG) {
|
||||
return (
|
||||
<SVGWrap {...props} viewBox="0 0 24 24">
|
||||
<path
|
||||
fill="currentColor"
|
||||
d="m7.85 13l2.85 2.85q.3.3.288.7t-.288.7q-.3.3-.712.313t-.713-.288L4.7 12.7q-.3-.3-.3-.7t.3-.7l4.575-4.575q.3-.3.713-.287t.712.312q.275.3.288.7t-.288.7L7.85 11H19q.425 0 .713.288T20 12t-.288.713T19 13z"
|
||||
/>
|
||||
</SVGWrap>
|
||||
);
|
||||
}
|
||||
16
src/icons/Ask.tsx
Normal file
16
src/icons/Ask.tsx
Normal file
@@ -0,0 +1,16 @@
|
||||
import SVGWrap from "./SVGWrap";
|
||||
|
||||
export default function Ask(props: I.SVG) {
|
||||
return (
|
||||
<SVGWrap {...props} viewBox="0 0 24 24">
|
||||
<path
|
||||
fill="none"
|
||||
stroke="currentColor"
|
||||
strokeLinecap="round"
|
||||
strokeLinejoin="round"
|
||||
strokeWidth="2"
|
||||
d="M13.038 19.927A9.93 9.93 0 0 1 7.7 19L3 20l1.3-3.9C1.976 12.663 2.874 8.228 6.4 5.726c3.526-2.501 8.59-2.296 11.845.48c1.993 1.7 2.93 4.043 2.746 6.346M19 16l-2 3h4l-2 3"
|
||||
/>
|
||||
</SVGWrap>
|
||||
);
|
||||
}
|
||||
12
src/icons/Link.tsx
Normal file
12
src/icons/Link.tsx
Normal file
@@ -0,0 +1,12 @@
|
||||
import SVGWrap from "./SVGWrap";
|
||||
|
||||
export default function Link(props: I.SVG) {
|
||||
return (
|
||||
<SVGWrap {...props} viewBox="0 0 256 256">
|
||||
<path
|
||||
fill="currentColor"
|
||||
d="M117.18 188.74a12 12 0 0 1 0 17l-5.12 5.12A58.26 58.26 0 0 1 70.6 228a58.62 58.62 0 0 1-41.46-100.08l34.75-34.75a58.64 58.64 0 0 1 98.56 28.11a12 12 0 1 1-23.37 5.44a34.65 34.65 0 0 0-58.22-16.58l-34.75 34.75A34.62 34.62 0 0 0 70.57 204a34.41 34.41 0 0 0 24.49-10.14l5.11-5.12a12 12 0 0 1 17.01 0M226.83 45.17a58.65 58.65 0 0 0-82.93 0l-5.11 5.11a12 12 0 0 0 17 17l5.12-5.12a34.63 34.63 0 1 1 49 49l-34.81 34.7A34.39 34.39 0 0 1 150.61 156a34.63 34.63 0 0 1-33.69-26.72a12 12 0 0 0-23.38 5.44A58.64 58.64 0 0 0 150.56 180h.05a58.28 58.28 0 0 0 41.47-17.17l34.75-34.75a58.62 58.62 0 0 0 0-82.91"
|
||||
/>
|
||||
</SVGWrap>
|
||||
);
|
||||
}
|
||||
13
src/icons/Pin.tsx
Normal file
13
src/icons/Pin.tsx
Normal file
@@ -0,0 +1,13 @@
|
||||
import SVGWrap from "./SVGWrap";
|
||||
|
||||
export default function Pin(props: I.SVG) {
|
||||
return (
|
||||
<SVGWrap {...props} viewBox="0 0 24 24">
|
||||
<path
|
||||
fill="currentColor"
|
||||
fillRule="evenodd"
|
||||
d="M16 9V4h1c.55 0 1-.45 1-1s-.45-1-1-1H7c-.55 0-1 .45-1 1s.45 1 1 1h1v5c0 1.66-1.34 3-3 3v2h5.97v7l1 1l1-1v-7H19v-2c-1.66 0-3-1.34-3-3"
|
||||
/>
|
||||
</SVGWrap>
|
||||
);
|
||||
}
|
||||
12
src/icons/Reload.tsx
Normal file
12
src/icons/Reload.tsx
Normal file
@@ -0,0 +1,12 @@
|
||||
import SVGWrap from "./SVGWrap";
|
||||
|
||||
export default function Reload(props: I.SVG) {
|
||||
return (
|
||||
<SVGWrap {...props} viewBox="0 0 24 24">
|
||||
<path
|
||||
fill="currentColor"
|
||||
d="M12 20q-3.35 0-5.675-2.325T4 12t2.325-5.675T12 4q1.725 0 3.3.712T18 6.75V5q0-.425.288-.712T19 4t.713.288T20 5v5q0 .425-.288.713T19 11h-5q-.425 0-.712-.288T13 10t.288-.712T14 9h3.2q-.8-1.4-2.187-2.2T12 6Q9.5 6 7.75 7.75T6 12t1.75 4.25T12 18q1.7 0 3.113-.862t2.187-2.313q.2-.35.563-.487t.737-.013q.4.125.575.525t-.025.75q-1.025 2-2.925 3.2T12 20"
|
||||
/>
|
||||
</SVGWrap>
|
||||
);
|
||||
}
|
||||
43
src/icons/SVGWrap.tsx
Normal file
43
src/icons/SVGWrap.tsx
Normal file
@@ -0,0 +1,43 @@
|
||||
import React from "react";
|
||||
import clsx from "clsx";
|
||||
|
||||
export default function SVGWrap({
|
||||
size = 18,
|
||||
children,
|
||||
type,
|
||||
className,
|
||||
title,
|
||||
onClick,
|
||||
action = false,
|
||||
...props
|
||||
}: I.SVG) {
|
||||
const handleClick = (e: React.MouseEvent) => {
|
||||
onClick && onClick(e);
|
||||
};
|
||||
|
||||
return (
|
||||
<i
|
||||
style={{
|
||||
width: size,
|
||||
height: size,
|
||||
}}
|
||||
title={title}
|
||||
onClick={handleClick}
|
||||
className={clsx(
|
||||
"inline-flex items-center justify-center rounded-sm p-[2px] text-slate-500 dark:text-slate-500 transition-all",
|
||||
{
|
||||
"cursor-pointer hover:bg-slate-300/50 hover:dark:bg-white/10": action,
|
||||
},
|
||||
className
|
||||
)}
|
||||
>
|
||||
<svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
style={{ widows: "100%", height: "100%" }}
|
||||
{...props}
|
||||
>
|
||||
{children}
|
||||
</svg>
|
||||
</i>
|
||||
);
|
||||
}
|
||||
15
src/icons/Send.tsx
Normal file
15
src/icons/Send.tsx
Normal file
@@ -0,0 +1,15 @@
|
||||
import SVGWrap from "./SVGWrap";
|
||||
|
||||
export default function Send(props: I.SVG) {
|
||||
return (
|
||||
<SVGWrap {...props} viewBox="0 0 24 24">
|
||||
<g fill="none">
|
||||
<path d="M24 0v24H0V0zM12.593 23.258l-.011.002l-.071.035l-.02.004l-.014-.004l-.071-.035c-.01-.004-.019-.001-.024.005l-.004.01l-.017.428l.005.02l.01.013l.104.074l.015.004l.012-.004l.104-.074l.012-.016l.004-.017l-.017-.427c-.002-.01-.009-.017-.017-.018m.265-.113l-.013.002l-.185.093l-.01.01l-.003.011l.018.43l.005.012l.008.007l.201.093c.012.004.023 0 .029-.008l.004-.014l-.034-.614c-.003-.012-.01-.02-.02-.022m-.715.002a.023.023 0 0 0-.027.006l-.006.014l-.034.614c0 .012.007.02.017.024l.015-.002l.201-.093l.01-.008l.004-.011l.017-.43l-.003-.012l-.01-.01z" />
|
||||
<path
|
||||
fill="currentColor"
|
||||
d="m21.433 4.861l-6 15.5a1 1 0 0 1-1.624.362l-3.382-3.235l-2.074 2.073a.5.5 0 0 1-.853-.354v-4.519L2.309 9.723a1 1 0 0 1 .442-1.691l17.5-4.5a1 1 0 0 1 1.181 1.329ZM19 6.001L8.032 13.152l1.735 1.66L19 6Z"
|
||||
/>
|
||||
</g>
|
||||
</SVGWrap>
|
||||
);
|
||||
}
|
||||
12
src/icons/Setting.tsx
Normal file
12
src/icons/Setting.tsx
Normal file
@@ -0,0 +1,12 @@
|
||||
import SVGWrap from "./SVGWrap";
|
||||
|
||||
export default function Setting(props: I.SVG) {
|
||||
return (
|
||||
<SVGWrap {...props} viewBox="0 0 24 24">
|
||||
<g fill="none" stroke="currentColor" strokeWidth="2">
|
||||
<path d="M3.082 13.945c-.529-.95-.793-1.426-.793-1.945c0-.519.264-.994.793-1.944L4.43 7.63l1.426-2.381c.559-.933.838-1.4 1.287-1.66c.45-.259.993-.267 2.08-.285L12 3.26l2.775.044c1.088.018 1.631.026 2.08.286c.45.26.73.726 1.288 1.659L19.57 7.63l1.35 2.426c.528.95.792 1.425.792 1.944c0 .519-.264.994-.793 1.944L19.57 16.37l-1.426 2.381c-.559.933-.838 1.4-1.287 1.66c-.45.259-.993.267-2.08.285L12 20.74l-2.775-.044c-1.088-.018-1.631-.026-2.08-.286c-.45-.26-.73-.726-1.288-1.659L4.43 16.37z" />
|
||||
<circle cx="12" cy="12" r="3" />
|
||||
</g>
|
||||
</SVGWrap>
|
||||
);
|
||||
}
|
||||
12
src/icons/ThemeDark.tsx
Normal file
12
src/icons/ThemeDark.tsx
Normal file
@@ -0,0 +1,12 @@
|
||||
import SVGWrap from "./SVGWrap";
|
||||
|
||||
export default function ThemeDark(props: I.SVG) {
|
||||
return (
|
||||
<SVGWrap {...props} viewBox="0 0 24 24">
|
||||
<path
|
||||
fill="currentColor"
|
||||
d="M12 21q-3.775 0-6.387-2.613T3 12q0-3.45 2.25-5.988T11 3.05q.325-.05.575.088t.4.362t.163.525t-.188.575q-.425.65-.638 1.375T11.1 7.5q0 2.25 1.575 3.825T16.5 12.9q.775 0 1.538-.225t1.362-.625q.275-.175.563-.162t.512.137q.25.125.388.375t.087.6q-.35 3.45-2.937 5.725T12 21m0-2q2.2 0 3.95-1.213t2.55-3.162q-.5.125-1 .2t-1 .075q-3.075 0-5.238-2.163T9.1 7.5q0-.5.075-1t.2-1q-1.95.8-3.163 2.55T5 12q0 2.9 2.05 4.95T12 19m-.25-6.75"
|
||||
/>
|
||||
</SVGWrap>
|
||||
);
|
||||
}
|
||||
12
src/icons/ThemeLight.tsx
Normal file
12
src/icons/ThemeLight.tsx
Normal file
@@ -0,0 +1,12 @@
|
||||
import SVGWrap from "./SVGWrap";
|
||||
|
||||
export default function ThemeLight(props: I.SVG) {
|
||||
return (
|
||||
<SVGWrap {...props} viewBox="0 0 24 24">
|
||||
<path
|
||||
fill="currentColor"
|
||||
d="M12 15q1.25 0 2.125-.875T15 12t-.875-2.125T12 9t-2.125.875T9 12t.875 2.125T12 15m0 2q-2.075 0-3.537-1.463T7 12t1.463-3.537T12 7t3.538 1.463T17 12t-1.463 3.538T12 17M2 13q-.425 0-.712-.288T1 12t.288-.712T2 11h2q.425 0 .713.288T5 12t-.288.713T4 13zm18 0q-.425 0-.712-.288T19 12t.288-.712T20 11h2q.425 0 .713.288T23 12t-.288.713T22 13zm-8-8q-.425 0-.712-.288T11 4V2q0-.425.288-.712T12 1t.713.288T13 2v2q0 .425-.288.713T12 5m0 18q-.425 0-.712-.288T11 22v-2q0-.425.288-.712T12 19t.713.288T13 20v2q0 .425-.288.713T12 23M5.65 7.05L4.575 6q-.3-.275-.288-.7t.288-.725q.3-.3.725-.3t.7.3L7.05 5.65q.275.3.275.7t-.275.7t-.687.288t-.713-.288M18 19.425l-1.05-1.075q-.275-.3-.275-.712t.275-.688q.275-.3.688-.287t.712.287L19.425 18q.3.275.288.7t-.288.725q-.3.3-.725.3t-.7-.3M16.95 7.05q-.3-.275-.288-.687t.288-.713L18 4.575q.275-.3.7-.288t.725.288q.3.3.3.725t-.3.7L18.35 7.05q-.3.275-.7.275t-.7-.275M4.575 19.425q-.3-.3-.3-.725t.3-.7l1.075-1.05q.3-.275.712-.275t.688.275q.3.275.288.688t-.288.712L6 19.425q-.275.3-.7.288t-.725-.288M12 12"
|
||||
/>
|
||||
</SVGWrap>
|
||||
);
|
||||
}
|
||||
12
src/icons/ThemeSystem.tsx
Normal file
12
src/icons/ThemeSystem.tsx
Normal file
@@ -0,0 +1,12 @@
|
||||
import SVGWrap from "./SVGWrap";
|
||||
|
||||
export default function ThemeSystem(props: I.SVG) {
|
||||
return (
|
||||
<SVGWrap {...props} viewBox="0 0 24 24">
|
||||
<path
|
||||
fill="currentColor"
|
||||
d="M12 22c5.523 0 10-4.477 10-10S17.523 2 12 2S2 6.477 2 12s4.477 10 10 10m0-2V4a8 8 0 1 1 0 16"
|
||||
/>
|
||||
</SVGWrap>
|
||||
);
|
||||
}
|
||||
12
src/icons/UnPin.tsx
Normal file
12
src/icons/UnPin.tsx
Normal file
@@ -0,0 +1,12 @@
|
||||
import SVGWrap from "./SVGWrap";
|
||||
|
||||
export default function UnPin(props: I.SVG) {
|
||||
return (
|
||||
<SVGWrap {...props} viewBox="0 0 24 24">
|
||||
<path
|
||||
fill="currentColor"
|
||||
d="M14 4v5c0 1.12.37 2.16 1 3H9c.65-.86 1-1.9 1-3V4zm3-2H7c-.55 0-1 .45-1 1s.45 1 1 1h1v5c0 1.66-1.34 3-3 3v2h5.97v7l1 1l1-1v-7H19v-2c-1.66 0-3-1.34-3-3V4h1c.55 0 1-.45 1-1s-.45-1-1-1"
|
||||
/>
|
||||
</SVGWrap>
|
||||
);
|
||||
}
|
||||
22
src/index.d.ts
vendored
Normal file
22
src/index.d.ts
vendored
Normal file
@@ -0,0 +1,22 @@
|
||||
declare namespace I {
|
||||
export type AppConf = {
|
||||
theme: "light" | "dark" | "system";
|
||||
stay_on_top: boolean;
|
||||
ask_mode: boolean;
|
||||
mac_header_hidden: boolean;
|
||||
};
|
||||
|
||||
export interface SVG extends React.SVGProps<SVGSVGElement> {
|
||||
children?: React.ReactNode;
|
||||
size?: number;
|
||||
title?: string;
|
||||
action?: boolean;
|
||||
onClick?: (e: React.MouseEvent) => void;
|
||||
}
|
||||
}
|
||||
|
||||
declare global {
|
||||
interface Window {
|
||||
__TAURI__: Record<string, unknown>;
|
||||
}
|
||||
}
|
||||
7
src/locales/en/translation.json
Normal file
7
src/locales/en/translation.json
Normal file
@@ -0,0 +1,7 @@
|
||||
{
|
||||
"welcome": "Welcome to Coco App",
|
||||
"home": "Home",
|
||||
"settings": "Settings",
|
||||
"activeTheme": "Current theme:",
|
||||
"InputMessage": "Input your message here..."
|
||||
}
|
||||
7
src/locales/zh/translation.json
Normal file
7
src/locales/zh/translation.json
Normal file
@@ -0,0 +1,7 @@
|
||||
{
|
||||
"welcome": "欢迎使用 Coco App",
|
||||
"home": "主页",
|
||||
"settings": "设置",
|
||||
"activeTheme": "当前主题:",
|
||||
"InputMessage": "在此输入您的消息..."
|
||||
}
|
||||
49
src/main.css
Normal file
49
src/main.css
Normal file
@@ -0,0 +1,49 @@
|
||||
@tailwind base;
|
||||
@tailwind components;
|
||||
@tailwind utilities;
|
||||
|
||||
@layer {
|
||||
:root {
|
||||
--background: #ffffff;
|
||||
--foreground: #09090b;
|
||||
--border: #e3e3e7;
|
||||
}
|
||||
|
||||
.dark {
|
||||
--background: #09090b;
|
||||
--foreground: #f9f9f9;
|
||||
--border: #27272a;
|
||||
}
|
||||
}
|
||||
|
||||
@layer base {
|
||||
* {
|
||||
@apply box-border border-[--border];
|
||||
}
|
||||
|
||||
body {
|
||||
@apply bg-gray-50 text-gray-900 antialiased;
|
||||
}
|
||||
|
||||
.dark body {
|
||||
@apply bg-gray-900 text-gray-100;
|
||||
}
|
||||
}
|
||||
|
||||
@layer components {
|
||||
.settings-input {
|
||||
@apply block w-full rounded-md border-gray-300 dark:border-gray-600
|
||||
bg-white dark:bg-gray-700
|
||||
text-gray-900 dark:text-gray-100
|
||||
shadow-sm focus:border-blue-500 focus:ring-blue-500
|
||||
transition-colors duration-200;
|
||||
}
|
||||
|
||||
.settings-select {
|
||||
@apply text-sm rounded-md border-gray-300 dark:border-gray-600
|
||||
bg-white dark:bg-gray-700
|
||||
text-gray-900 dark:text-gray-100
|
||||
shadow-sm focus:border-blue-500 focus:ring-blue-500
|
||||
transition-colors duration-200;
|
||||
}
|
||||
}
|
||||
13
src/main.tsx
13
src/main.tsx
@@ -1,9 +1,16 @@
|
||||
import React from "react";
|
||||
import ReactDOM from "react-dom/client";
|
||||
import App from "./App";
|
||||
import { RouterProvider } from "react-router-dom";
|
||||
|
||||
import { ThemeProvider } from "./components/ThemeProvider";
|
||||
import { router } from "./routes/index";
|
||||
|
||||
import './main.css';
|
||||
|
||||
ReactDOM.createRoot(document.getElementById("root") as HTMLElement).render(
|
||||
<React.StrictMode>
|
||||
<App />
|
||||
</React.StrictMode>,
|
||||
<ThemeProvider defaultTheme="dark" storageKey="theme">
|
||||
<RouterProvider router={router} />
|
||||
</ThemeProvider>
|
||||
</React.StrictMode>
|
||||
);
|
||||
|
||||
20
src/routes/index.tsx
Normal file
20
src/routes/index.tsx
Normal file
@@ -0,0 +1,20 @@
|
||||
import { createBrowserRouter } from "react-router-dom";
|
||||
|
||||
import App from "../App";
|
||||
import ErrorPage from "../error-page";
|
||||
import Settings from "../components/Settings";
|
||||
import Settings2 from "../components/Settings/index2";
|
||||
import SearchChat from "../components/SearchChat";
|
||||
|
||||
export const router = createBrowserRouter([
|
||||
{
|
||||
path: "/",
|
||||
element: <SearchChat />,
|
||||
errorElement: <ErrorPage />,
|
||||
},
|
||||
{
|
||||
path: "/settings",
|
||||
element: <Settings2 />,
|
||||
errorElement: <ErrorPage />,
|
||||
},
|
||||
]);
|
||||
43
src/routes/root.tsx
Normal file
43
src/routes/root.tsx
Normal file
@@ -0,0 +1,43 @@
|
||||
export default function Root() {
|
||||
return (
|
||||
<>
|
||||
<div id="sidebar">
|
||||
<h1>React Router Contacts</h1>
|
||||
<div>
|
||||
<form id="search-form" role="search">
|
||||
<input
|
||||
id="q"
|
||||
aria-label="Search contacts"
|
||||
placeholder="Search"
|
||||
type="search"
|
||||
name="q"
|
||||
/>
|
||||
<div
|
||||
id="search-spinner"
|
||||
aria-hidden
|
||||
hidden={true}
|
||||
/>
|
||||
<div
|
||||
className="sr-only"
|
||||
aria-live="polite"
|
||||
></div>
|
||||
</form>
|
||||
<form method="post">
|
||||
<button type="submit">New</button>
|
||||
</form>
|
||||
</div>
|
||||
<nav>
|
||||
<ul>
|
||||
<li>
|
||||
<a href={`/contacts/1`}>Your Name</a>
|
||||
</li>
|
||||
<li>
|
||||
<a href={`/contacts/2`}>Your Friend</a>
|
||||
</li>
|
||||
</ul>
|
||||
</nav>
|
||||
</div>
|
||||
<div id="detail"></div>
|
||||
</>
|
||||
);
|
||||
}
|
||||
30
src/stores/themeStore.ts
Normal file
30
src/stores/themeStore.ts
Normal file
@@ -0,0 +1,30 @@
|
||||
import { create } from "zustand";
|
||||
import { persist, createJSONStorage } from "zustand/middleware";
|
||||
|
||||
export type ITheme = "dark" | "light" | "system";
|
||||
|
||||
export type IThemeStore = {
|
||||
themes: ITheme[];
|
||||
activeTheme: ITheme;
|
||||
setTheme: (theme: ITheme) => void;
|
||||
};
|
||||
|
||||
export const useThemeStore = create<IThemeStore>()(
|
||||
// 持久化中间件
|
||||
persist(
|
||||
(set) => ({
|
||||
themes: ["dark", "light", "system"],
|
||||
activeTheme: "system",
|
||||
setTheme: (activeTheme: ITheme) => set(() => ({ activeTheme })),
|
||||
}),
|
||||
{
|
||||
name: "active-theme", // 存储在 storage 中的 key 名
|
||||
// storage: createJSONStorage(() => sessionStorage), // 存储数据库配置,默认使用 localstorage
|
||||
// 过滤函数
|
||||
partialize: (state) =>
|
||||
Object.fromEntries(
|
||||
Object.entries(state).filter(([key]) => key === "activeTheme")
|
||||
),
|
||||
}
|
||||
)
|
||||
);
|
||||
30
tailwind.config.js
Normal file
30
tailwind.config.js
Normal file
@@ -0,0 +1,30 @@
|
||||
/** @type {import('tailwindcss').Config} */
|
||||
export default {
|
||||
content: ["./index.html", "./src/**/*.{html,js,jsx,ts,tsx}"],
|
||||
theme: {
|
||||
extend: {
|
||||
backgroundColor: {
|
||||
primary: "rgb(var(--color-primary) / <alpha-value>)",
|
||||
secondary: "rgb(var(--color-secondary) / <alpha-value>)",
|
||||
background: "rgb(var(--color-background) / <alpha-value>)",
|
||||
foreground: "rgb(var(--color-foreground) / <alpha-value>)",
|
||||
separator: "rgb(var(--color-separator) / <alpha-value>)",
|
||||
},
|
||||
textColor: {
|
||||
primary: "rgb(var(--color-foreground) / <alpha-value>)",
|
||||
},
|
||||
animation: {
|
||||
"fade-in": "fade-in 0.2s ease-in-out",
|
||||
},
|
||||
keyframes: {
|
||||
"fade-in": {
|
||||
"0%": { opacity: "0" },
|
||||
"100%": { opacity: "1" },
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
plugins: [],
|
||||
mode: "jit",
|
||||
darkMode: "class",
|
||||
};
|
||||
Reference in New Issue
Block a user