mirror of
https://github.com/infinilabs/coco-app.git
synced 2025-12-23 14:59:24 +01:00
refactor: replace legacy components with shadcn/ui components (#1002)
* chore: shadcn config * feat: add shadcn ui config * style: adjust styles * style: adjust styles * refactor: update style * style: adjust styles * style: adjust styles * style: adjust styles * style: adjust styles * refactor: update * refactor: update * refactor: update * refactor: update * style: adjust styles * style: adjust styles * refactor: update * refactor: update * refactor: update * refactor: update * refactor: update * refactor: update * style: web styles * refactor: update * style: web styles * style: web styles * refactor: update * refactor: update * refactor: update * chhore: add * chore: add * refactor: update * refactor: update * refactor: update * refactor: update * chore: update * refactor: update * refactor: update * refactor: update * refactor: update * refactor: update * refactor: update * chore: rename * refactor: update * refactor: update * chore: add * refactor: update * chore: update * chroe: up * refactor: update * refactor: update * chore: up * refactor: update * chore: up * feat: support for extracting css variables * chore: update * fix: fixed dark mode * refactor: update * refactor: update * refactor: update * refactor: update * docs: update release notes * style: adjust styles * style: adjust styles * refactor: update * refactor: update * refactor: update * refactor: update * refactor: update * refactor: update --------- Co-authored-by: ayang <473033518@qq.com>
This commit is contained in:
4
.gitignore
vendored
4
.gitignore
vendored
@@ -13,6 +13,8 @@ dist-ssr
|
||||
*.local
|
||||
out
|
||||
src/components/web
|
||||
SearchChatDemo/
|
||||
web.md
|
||||
|
||||
# Editor directories and files
|
||||
# .vscode/*
|
||||
@@ -26,3 +28,5 @@ src/components/web
|
||||
*.sln
|
||||
*.sw?
|
||||
.env
|
||||
|
||||
.trae
|
||||
5
.vscode/settings.json
vendored
5
.vscode/settings.json
vendored
@@ -37,15 +37,18 @@
|
||||
"meval",
|
||||
"Minimizable",
|
||||
"msvc",
|
||||
"njsproj",
|
||||
"nord",
|
||||
"nowrap",
|
||||
"nspanel",
|
||||
"nsstring",
|
||||
"ntvs",
|
||||
"objc",
|
||||
"overscan",
|
||||
"partialize",
|
||||
"patchelf",
|
||||
"Quicklink",
|
||||
"Quicklinks",
|
||||
"Raycast",
|
||||
"rehype",
|
||||
"reqwest",
|
||||
@@ -54,6 +57,7 @@
|
||||
"rustup",
|
||||
"screenshotable",
|
||||
"serde",
|
||||
"Shadcn",
|
||||
"swatinem",
|
||||
"tailwindcss",
|
||||
"tauri",
|
||||
@@ -61,6 +65,7 @@
|
||||
"timedout",
|
||||
"titlebar",
|
||||
"tpddns",
|
||||
"trae",
|
||||
"traptitech",
|
||||
"unlisten",
|
||||
"unlistener",
|
||||
|
||||
@@ -20,6 +20,7 @@ Information about release notes of Coco App is provided here.
|
||||
|
||||
### ✈️ Improvements
|
||||
|
||||
- refactor: replace legacy components with shadcn/ui components #1002
|
||||
- chore: show error msg (not err code) when installing exts via deeplink fails #1007
|
||||
- refactor: treat Applications and File Search as normal extensions #1012
|
||||
|
||||
|
||||
21
package.json
21
package.json
@@ -8,9 +8,10 @@
|
||||
"build": "tsc && vite build",
|
||||
"build:web": "cross-env BUILD_TARGET=web tsc && cross-env BUILD_TARGET=web tsup --format esm",
|
||||
"publish:web": "cd out/search-chat && npm publish",
|
||||
"publish:web:beta": "cd dist/search-chat && npm publish --tag beta",
|
||||
"publish:web:alpha": "cd dist/search-chat && npm publish --tag alpha",
|
||||
"publish:web:rc": "cd dist/search-chat && npm publish --tag rc",
|
||||
"publish:web:beta": "cd out/search-chat && npm publish --tag beta",
|
||||
"publish:web:alpha": "cd out/search-chat && npm publish --tag alpha",
|
||||
"publish:web:rc": "cd out/search-chat && npm publish --tag rc",
|
||||
"publish:web:otp": "cd out/search-chat && npm publish --access public --otp $NPM_OTP",
|
||||
"preview": "vite preview",
|
||||
"tauri": "tauri",
|
||||
"release": "release-it",
|
||||
@@ -18,10 +19,18 @@
|
||||
"release-beta": "release-it --preRelease=beta --preReleaseBase=1"
|
||||
},
|
||||
"dependencies": {
|
||||
"@headlessui/react": "^2.2.2",
|
||||
"@infinilabs/custom-icons": "0.0.4",
|
||||
"@radix-ui/react-checkbox": "^1.1.5",
|
||||
"@radix-ui/react-dialog": "^1.1.4",
|
||||
"@radix-ui/react-dropdown-menu": "^2.1.16",
|
||||
"@radix-ui/react-label": "^2.1.1",
|
||||
"@radix-ui/react-popover": "^1.1.6",
|
||||
"@radix-ui/react-select": "^2.1.4",
|
||||
"@radix-ui/react-separator": "^1.1.8",
|
||||
"@radix-ui/react-slider": "^1.2.1",
|
||||
"@radix-ui/react-slot": "^1.2.3",
|
||||
"@radix-ui/react-switch": "^1.1.3",
|
||||
"@radix-ui/react-tabs": "^1.1.13",
|
||||
"@tauri-apps/api": "^2.5.0",
|
||||
"@tauri-apps/plugin-autostart": "~2.2.0",
|
||||
"@tauri-apps/plugin-clipboard-manager": "~2.3.2",
|
||||
@@ -77,6 +86,8 @@
|
||||
"zustand": "^5.0.4"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@tailwindcss/postcss": "^4.1.17",
|
||||
"@tailwindcss/vite": "^4.0.0",
|
||||
"@tauri-apps/cli": "^2.5.0",
|
||||
"@types/dom-speech-recognition": "^0.0.4",
|
||||
"@types/lodash-es": "^4.17.12",
|
||||
@@ -93,7 +104,7 @@
|
||||
"postcss": "^8.5.3",
|
||||
"release-it": "^18.1.2",
|
||||
"sass": "^1.87.0",
|
||||
"tailwindcss": "^3.4.17",
|
||||
"tailwindcss": "^4.0.0",
|
||||
"tsup": "^8.4.0",
|
||||
"tsx": "^4.19.4",
|
||||
"typescript": "^5.8.3",
|
||||
|
||||
1550
pnpm-lock.yaml
generated
1550
pnpm-lock.yaml
generated
File diff suppressed because it is too large
Load Diff
@@ -1,6 +1,7 @@
|
||||
export default {
|
||||
plugins: {
|
||||
tailwindcss: {},
|
||||
// Tailwind v4 PostCSS plugin has moved to @tailwindcss/postcss
|
||||
'@tailwindcss/postcss': {},
|
||||
autoprefixer: {},
|
||||
},
|
||||
}
|
||||
|
||||
39
scripts/buildWebAfter.ts
Normal file
39
scripts/buildWebAfter.ts
Normal file
@@ -0,0 +1,39 @@
|
||||
import { readFileSync, writeFileSync } from "fs";
|
||||
import { join, dirname } from "path";
|
||||
import { fileURLToPath } from "url";
|
||||
|
||||
const __filename = fileURLToPath(import.meta.url);
|
||||
const __dirname = dirname(__filename);
|
||||
|
||||
const extractCssVars = () => {
|
||||
const filePath = join(__dirname, "../out/search-chat/index.css");
|
||||
|
||||
const cssContent = readFileSync(filePath, "utf-8");
|
||||
|
||||
const vars: Record<string, string> = {};
|
||||
|
||||
const propertyBlockRegex = /@property\s+(--[\w-]+)\s*\{([\s\S]*?)\}/g;
|
||||
|
||||
let match: RegExpExecArray | null;
|
||||
|
||||
while ((match = propertyBlockRegex.exec(cssContent))) {
|
||||
const [, varName, body] = match;
|
||||
|
||||
const initialValueMatch = /initial-value\s*:\s*([^;]+);/.exec(body);
|
||||
|
||||
if (initialValueMatch) {
|
||||
vars[varName] = initialValueMatch[1].trim();
|
||||
}
|
||||
}
|
||||
|
||||
const cssVarsBlock =
|
||||
`.coco-container {\n` +
|
||||
Object.entries(vars)
|
||||
.map(([k, v]) => ` ${k}: ${v};`)
|
||||
.join("\n") +
|
||||
`\n}\n`;
|
||||
|
||||
writeFileSync(filePath, `${cssContent}\n${cssVarsBlock}`, "utf-8");
|
||||
};
|
||||
|
||||
extractCssVars();
|
||||
@@ -12,7 +12,7 @@ import {
|
||||
handleNetworkError,
|
||||
} from "./tools";
|
||||
|
||||
type Fn = (data: FcResponse<any>) => unknown;
|
||||
type Fn = (data: FcResponse<unknown>) => unknown;
|
||||
|
||||
interface IAnyObj {
|
||||
[index: string]: unknown;
|
||||
@@ -85,8 +85,26 @@ export const Get = <T>(
|
||||
new Promise((resolve) => {
|
||||
const appStore = JSON.parse(localStorage.getItem("app-store") || "{}");
|
||||
|
||||
let baseURL = appStore.state?.endpoint_http;
|
||||
if (!baseURL || baseURL === "undefined") {
|
||||
// In Vite dev, prefer using the proxy by keeping requests relative
|
||||
const isDev = (import.meta as any).env?.DEV === true;
|
||||
const PROXY_PREFIXES: readonly string[] = [
|
||||
"account",
|
||||
"chat",
|
||||
"query",
|
||||
"connector",
|
||||
"integration",
|
||||
"assistant",
|
||||
"datasource",
|
||||
"settings",
|
||||
"mcp_server",
|
||||
];
|
||||
const shouldProxy =
|
||||
isDev &&
|
||||
url.startsWith("/") &&
|
||||
PROXY_PREFIXES.some((p) => url.startsWith(`/${p}`));
|
||||
|
||||
let baseURL: string = appStore.state?.endpoint_http as string;
|
||||
if (!baseURL || baseURL === "undefined" || shouldProxy) {
|
||||
baseURL = "";
|
||||
}
|
||||
|
||||
@@ -117,8 +135,25 @@ export const Post = <T>(
|
||||
return new Promise((resolve) => {
|
||||
const appStore = JSON.parse(localStorage.getItem("app-store") || "{}");
|
||||
|
||||
let baseURL = appStore.state?.endpoint_http;
|
||||
if (!baseURL || baseURL === "undefined") {
|
||||
const isDev = (import.meta as any).env?.DEV === true;
|
||||
const PROXY_PREFIXES: readonly string[] = [
|
||||
"account",
|
||||
"chat",
|
||||
"query",
|
||||
"connector",
|
||||
"integration",
|
||||
"assistant",
|
||||
"datasource",
|
||||
"settings",
|
||||
"mcp_server",
|
||||
];
|
||||
const shouldProxy =
|
||||
isDev &&
|
||||
url.startsWith("/") &&
|
||||
PROXY_PREFIXES.some((p) => url.startsWith(`/${p}`));
|
||||
|
||||
let baseURL: string = appStore.state?.endpoint_http as string;
|
||||
if (!baseURL || baseURL === "undefined" || shouldProxy) {
|
||||
baseURL = "";
|
||||
}
|
||||
|
||||
|
||||
@@ -41,15 +41,20 @@ const AssistantItem = memo(
|
||||
)}
|
||||
onClick={onClick}
|
||||
>
|
||||
<div className="flex items-center justify-center size-6 bg-white border border-[#E6E6E6] rounded-full overflow-hidden">
|
||||
{_source?.icon?.startsWith("font_") ? (
|
||||
<FontIcon name={_source?.icon} className="size-4" />
|
||||
{_source?.icon?.startsWith("font_") ? (
|
||||
<FontIcon
|
||||
name={_source?.icon}
|
||||
className="w-4 h-4 rounded-full dark:drop-shadow-[0_0_6px_rgb(255,255,255)]"
|
||||
/>
|
||||
) : (
|
||||
<img src={logoImg} className="size-4" alt={name} />
|
||||
<img
|
||||
src={logoImg}
|
||||
className="w-4 h-4 rounded-full dark:drop-shadow-[0_0_6px_rgb(255,255,255)]"
|
||||
alt={name}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
<div className="text-left flex-1 min-w-0">
|
||||
<div className="font-medium text-gray-900 dark:text-white truncate">
|
||||
<div className="text-sm font-medium text-gray-900 dark:text-white truncate">
|
||||
{_source?.name || "-"}
|
||||
</div>
|
||||
<div className="text-xs text-gray-500 dark:text-gray-400 truncate">
|
||||
|
||||
@@ -1,8 +1,11 @@
|
||||
import { useState, useRef, useCallback, useEffect } from "react";
|
||||
import { ChevronDownIcon, RefreshCw } from "lucide-react";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import { isNil } from "lodash-es";
|
||||
import { Popover, PopoverButton, PopoverPanel } from "@headlessui/react";
|
||||
import {
|
||||
Popover,
|
||||
PopoverTrigger,
|
||||
PopoverContent,
|
||||
} from "@/components/ui/popover";
|
||||
import { useDebounce, useKeyPress, usePagination } from "ahooks";
|
||||
import clsx from "clsx";
|
||||
|
||||
@@ -17,6 +20,7 @@ import { AssistantFetcher } from "./AssistantFetcher";
|
||||
import AssistantItem from "./AssistantItem";
|
||||
import Pagination from "@/components/Common/Pagination";
|
||||
import { useSearchStore } from "@/stores/searchStore";
|
||||
import { Button } from "../ui/button";
|
||||
|
||||
interface AssistantListProps {
|
||||
assistantIDs?: string[];
|
||||
@@ -83,6 +87,7 @@ export function AssistantList({ assistantIDs = [] }: AssistantListProps) {
|
||||
|
||||
const [highlightIndex, setHighlightIndex] = useState<number>(-1);
|
||||
const [isKeyboardActive, setIsKeyboardActive] = useState(false);
|
||||
const [open, setOpen] = useState(false);
|
||||
|
||||
useEffect(() => {
|
||||
const targetId = askAiAssistantId ?? targetAssistantId;
|
||||
@@ -105,7 +110,7 @@ export function AssistantList({ assistantIDs = [] }: AssistantListProps) {
|
||||
useKeyPress(
|
||||
["uparrow", "downarrow", "enter"],
|
||||
(event, key) => {
|
||||
const isClose = isNil(popoverButtonRef.current?.dataset["open"]);
|
||||
const isClose = !open;
|
||||
|
||||
if (isClose) return;
|
||||
|
||||
@@ -161,26 +166,29 @@ export function AssistantList({ assistantIDs = [] }: AssistantListProps) {
|
||||
}, []);
|
||||
|
||||
return (
|
||||
<div className="relative">
|
||||
<Popover ref={popoverRef}>
|
||||
<PopoverButton
|
||||
<div ref={popoverRef} className="relative">
|
||||
<Popover
|
||||
open={open}
|
||||
onOpenChange={(v) => {
|
||||
setOpen(v);
|
||||
}}
|
||||
>
|
||||
<PopoverTrigger
|
||||
ref={popoverButtonRef}
|
||||
className="h-6 p-1 px-1.5 flex items-center gap-1 rounded-full bg-white dark:bg-[#202126] text-sm/6 font-semibold text-gray-800 dark:text-[#d8d8d8] border border-gray-200 dark:border-gray-700 hover:bg-gray-50 dark:hover:bg-gray-700 focus:outline-none"
|
||||
className="h-6 p-1 px-1.5 flex items-center gap-1 rounded-full border border-input bg-background text-sm/6 font-semibold text-foreground hover:bg-accent hover:text-accent-foreground focus:outline-none"
|
||||
>
|
||||
<div className="w-4 h-4 flex justify-center items-center rounded-full bg-white border border-[#E6E6E6]">
|
||||
{currentAssistant?._source?.icon?.startsWith("font_") ? (
|
||||
<FontIcon
|
||||
name={currentAssistant._source.icon}
|
||||
className="w-3 h-3"
|
||||
/>
|
||||
) : (
|
||||
<img
|
||||
src={logoImg}
|
||||
className="w-3 h-3"
|
||||
alt={t("assistant.message.logo")}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
{currentAssistant?._source?.icon?.startsWith("font_") ? (
|
||||
<FontIcon
|
||||
name={currentAssistant._source.icon}
|
||||
className="w-4 h-4"
|
||||
/>
|
||||
) : (
|
||||
<img
|
||||
src={logoImg}
|
||||
className="w-4 h-4"
|
||||
alt={t("assistant.message.logo")}
|
||||
/>
|
||||
)}
|
||||
<div className="max-w-[100px] truncate">
|
||||
{currentAssistant?._source?.name || "Coco AI"}
|
||||
</div>
|
||||
@@ -190,12 +198,14 @@ export function AssistantList({ assistantIDs = [] }: AssistantListProps) {
|
||||
popoverButtonRef.current?.click();
|
||||
}}
|
||||
>
|
||||
<ChevronDownIcon className="size-4 text-gray-500 dark:text-gray-400 transition-transform" />
|
||||
<ChevronDownIcon className="size-4 text-muted-foreground transition-transform" />
|
||||
</VisibleKey>
|
||||
</PopoverButton>
|
||||
</PopoverTrigger>
|
||||
|
||||
<PopoverPanel
|
||||
className="absolute z-50 top-full mt-1 left-0 w-60 rounded-xl bg-white dark:bg-[#202126] p-3 text-sm/6 text-[#333] dark:text-[#D8D8D8] shadow-lg border dark:border-white/10 focus:outline-none max-h-[calc(100vh-150px)] overflow-y-auto"
|
||||
<PopoverContent
|
||||
side="bottom"
|
||||
align="start"
|
||||
className="z-50 w-60 rounded-xl p-3 shadow-lg focus:outline-none max-h-[calc(100vh-150px)] overflow-y-auto"
|
||||
onMouseMove={handleMouseMove}
|
||||
>
|
||||
<div className="flex items-center justify-between text-sm font-bold">
|
||||
@@ -203,9 +213,11 @@ export function AssistantList({ assistantIDs = [] }: AssistantListProps) {
|
||||
{t("assistant.popover.title")}({pagination.total})
|
||||
</div>
|
||||
|
||||
<button
|
||||
<Button
|
||||
variant="outline"
|
||||
size="icon"
|
||||
onClick={handleRefresh}
|
||||
className="flex items-center justify-center size-6 bg-white dark:bg-[#202126] rounded-lg border dark:border-white/10"
|
||||
className="size-6"
|
||||
disabled={isRefreshing}
|
||||
>
|
||||
<VisibleKey shortcut="R" onKeyPress={handleRefresh}>
|
||||
@@ -218,7 +230,7 @@ export function AssistantList({ assistantIDs = [] }: AssistantListProps) {
|
||||
)}
|
||||
/>
|
||||
</VisibleKey>
|
||||
</button>
|
||||
</Button>
|
||||
</div>
|
||||
|
||||
<VisibleKey
|
||||
@@ -234,8 +246,8 @@ export function AssistantList({ assistantIDs = [] }: AssistantListProps) {
|
||||
autoFocus
|
||||
value={keyword}
|
||||
placeholder={t("assistant.popover.search")}
|
||||
className="w-full h-8 px-2 bg-transparent border rounded-[6px] dark:border-white/10"
|
||||
onChange={(event) => {
|
||||
className="w-full h-8"
|
||||
onChange={(event: React.ChangeEvent<HTMLInputElement>) => {
|
||||
setKeyword(event.target.value);
|
||||
}}
|
||||
/>
|
||||
@@ -272,7 +284,7 @@ export function AssistantList({ assistantIDs = [] }: AssistantListProps) {
|
||||
<NoDataImage />
|
||||
</div>
|
||||
)}
|
||||
</PopoverPanel>
|
||||
</PopoverContent>
|
||||
</Popover>
|
||||
</div>
|
||||
);
|
||||
|
||||
@@ -388,7 +388,7 @@ const ChatAI = memo(
|
||||
<div
|
||||
data-tauri-drag-region
|
||||
data-chat-instance={instanceId}
|
||||
className={`flex flex-col rounded-[6px] h-full overflow-hidden relative`}
|
||||
className={`flex flex-col rounded-md h-full overflow-hidden relative`}
|
||||
>
|
||||
<ChatHeader
|
||||
clearChat={clearChat}
|
||||
|
||||
@@ -95,13 +95,13 @@ export function ChatHeader({
|
||||
{isChatPage ? null : (
|
||||
<button className="inline-flex" onClick={onOpenChatAI}>
|
||||
<VisibleKey shortcut={external} onKeyPress={onOpenChatAI}>
|
||||
<WindowsFullIcon className="rotate-30 scale-x-[-1]" />
|
||||
<WindowsFullIcon className="scale-x-[-1]" />
|
||||
</VisibleKey>
|
||||
</button>
|
||||
)}
|
||||
</div>
|
||||
) : (
|
||||
<WebLogin panelClassName="top-8 right-0" />
|
||||
<WebLogin side="bottom" align="end" />
|
||||
)}
|
||||
</header>
|
||||
);
|
||||
|
||||
@@ -28,7 +28,7 @@ const ConnectPrompt = () => {
|
||||
<p className="mb-4 w-[388px]">{t("assistant.chat.connect_tip")}</p>
|
||||
|
||||
<button
|
||||
className="flex items-center gap-2 px-6 py-2 rounded-[6px] text-[#0072ff] transition-colors"
|
||||
className="flex items-center gap-2 px-6 py-2 rounded-md text-[#0072ff] transition-colors"
|
||||
onClick={handleConnect}
|
||||
>
|
||||
<span>{t("assistant.chat.connect")}</span>
|
||||
|
||||
@@ -1,9 +1,13 @@
|
||||
import { useState, useCallback, useEffect, useRef } from "react";
|
||||
import { Settings, RefreshCw, Check, Server } from "lucide-react";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import { Popover, PopoverButton, PopoverPanel } from "@headlessui/react";
|
||||
import {
|
||||
Popover,
|
||||
PopoverTrigger,
|
||||
PopoverContent,
|
||||
} from "@/components/ui/popover";
|
||||
import { Button } from "@/components/ui/button";
|
||||
import { useKeyPress } from "ahooks";
|
||||
import { isNil } from "lodash-es";
|
||||
|
||||
import logoImg from "@/assets/icon.svg";
|
||||
import ServerIcon from "@/icons/Server";
|
||||
@@ -61,6 +65,7 @@ export function ServerList({ clearChat }: ServerListProps) {
|
||||
|
||||
const popoverRef = useRef<HTMLDivElement>(null);
|
||||
const serverListButtonRef = useRef<HTMLButtonElement>(null);
|
||||
const [open, setOpen] = useState(false);
|
||||
|
||||
const { refreshServerList } = useServers();
|
||||
|
||||
@@ -143,7 +148,7 @@ export function ServerList({ clearChat }: ServerListProps) {
|
||||
["uparrow", "downarrow", "enter"],
|
||||
async (event, key) => {
|
||||
const service = await getCurrentWindowService();
|
||||
const isClose = isNil(serverListButtonRef.current?.dataset["open"]);
|
||||
const isClose = !open;
|
||||
const length = serverList.length;
|
||||
|
||||
if (isClose || length <= 1) return;
|
||||
@@ -182,122 +187,130 @@ export function ServerList({ clearChat }: ServerListProps) {
|
||||
}, []);
|
||||
|
||||
return (
|
||||
<Popover ref={popoverRef} className="relative">
|
||||
<PopoverButton ref={serverListButtonRef} className="flex items-center">
|
||||
<VisibleKey
|
||||
shortcut={serviceListShortcut}
|
||||
onKeyPress={() => {
|
||||
serverListButtonRef.current?.click();
|
||||
}}
|
||||
>
|
||||
<ServerIcon />
|
||||
</VisibleKey>
|
||||
</PopoverButton>
|
||||
<div ref={popoverRef} className="relative">
|
||||
<Popover open={open} onOpenChange={setOpen}>
|
||||
<PopoverTrigger ref={serverListButtonRef} className="flex items-center">
|
||||
<VisibleKey
|
||||
shortcut={serviceListShortcut}
|
||||
onKeyPress={() => {
|
||||
serverListButtonRef.current?.click();
|
||||
}}
|
||||
>
|
||||
<ServerIcon />
|
||||
</VisibleKey>
|
||||
</PopoverTrigger>
|
||||
|
||||
<PopoverPanel
|
||||
onMouseMove={handleMouseMove}
|
||||
className="absolute right-0 z-10 mt-2 min-w-[240px] bg-white dark:bg-[#202126] rounded-lg shadow-lg border border-gray-200 dark:border-gray-700"
|
||||
>
|
||||
<div className="p-3">
|
||||
<div className="flex items-center justify-between mb-3 whitespace-nowrap">
|
||||
<h3 className="text-sm font-medium text-gray-900 dark:text-gray-100">
|
||||
{t("assistant.chat.servers")}
|
||||
</h3>
|
||||
<div className="flex items-center gap-2">
|
||||
<button
|
||||
onClick={openSettings}
|
||||
className="p-1 rounded-[6px] hover:bg-gray-100 dark:hover:bg-gray-800 text-gray-500 dark:text-gray-400"
|
||||
>
|
||||
<VisibleKey shortcut=",">
|
||||
<Settings className="h-4 w-4 text-[#0287FF]" />
|
||||
</VisibleKey>
|
||||
</button>
|
||||
<button
|
||||
onClick={handleRefresh}
|
||||
className="p-1 rounded-[6px] hover:bg-gray-100 dark:hover:bg-gray-800 text-gray-500 dark:text-gray-400"
|
||||
disabled={isRefreshing}
|
||||
>
|
||||
<VisibleKey shortcut="R" onKeyPress={handleRefresh}>
|
||||
<RefreshCw
|
||||
className={`h-4 w-4 text-[#0287FF] transition-transform duration-1000 ${
|
||||
isRefreshing ? "animate-spin" : ""
|
||||
}`}
|
||||
/>
|
||||
</VisibleKey>
|
||||
</button>
|
||||
<PopoverContent
|
||||
side="bottom"
|
||||
align="end"
|
||||
onMouseMove={handleMouseMove}
|
||||
className="z-10 min-w-60 rounded-lg shadow-lg"
|
||||
>
|
||||
<div className="p-3">
|
||||
<div className="flex items-center justify-between mb-3 whitespace-nowrap">
|
||||
<h3 className="text-sm font-medium text-foreground">
|
||||
{t("assistant.chat.servers")}
|
||||
</h3>
|
||||
<div className="flex items-center gap-2">
|
||||
<Button
|
||||
onClick={openSettings}
|
||||
variant="ghost"
|
||||
size="icon"
|
||||
className="rounded-md focus-visible:ring-0 focus-visible:ring-offset-0"
|
||||
>
|
||||
<VisibleKey shortcut=",">
|
||||
<Settings className="h-4 w-4 text-primary" />
|
||||
</VisibleKey>
|
||||
</Button>
|
||||
<Button
|
||||
onClick={handleRefresh}
|
||||
variant="ghost"
|
||||
size="icon"
|
||||
className="rounded-md focus-visible:ring-0 focus-visible:ring-offset-0"
|
||||
disabled={isRefreshing}
|
||||
>
|
||||
<VisibleKey shortcut="R" onKeyPress={handleRefresh}>
|
||||
<RefreshCw
|
||||
className={`h-4 w-4 text-primary transition-transform duration-1000 ${
|
||||
isRefreshing ? "animate-spin" : ""
|
||||
}`}
|
||||
/>
|
||||
</VisibleKey>
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div className="space-y-1">
|
||||
{list.length > 0 ? (
|
||||
list.map((server) => (
|
||||
<div
|
||||
key={server.id}
|
||||
onClick={() => switchServer(server)}
|
||||
className={`w-full flex items-center justify-between gap-1 p-2 rounded-lg transition-colors whitespace-nowrap
|
||||
<div className="space-y-1">
|
||||
{list.length > 0 ? (
|
||||
list.map((server) => (
|
||||
<div
|
||||
key={server.id}
|
||||
onClick={() => switchServer(server)}
|
||||
className={`w-full flex items-center justify-between gap-1 p-2 rounded-lg transition-colors whitespace-nowrap
|
||||
${
|
||||
currentService?.id === server.id ||
|
||||
highlightId === server.id
|
||||
? "bg-gray-100 dark:bg-gray-800"
|
||||
: "hover:bg-gray-50 dark:hover:bg-gray-800/50"
|
||||
? "bg-muted"
|
||||
: "hover:bg-muted"
|
||||
}`}
|
||||
>
|
||||
<div className="flex items-center gap-2 min-w-0">
|
||||
<img
|
||||
src={server?.provider?.icon || logoImg}
|
||||
alt={server.name}
|
||||
className="w-6 h-6 rounded-full bg-gray-100 dark:bg-gray-800 dark:drop-shadow-[0_0_6px_rgb(255,255,255)]"
|
||||
onError={(e) => {
|
||||
const target = e.target as HTMLImageElement;
|
||||
target.src = logoImg;
|
||||
}}
|
||||
/>
|
||||
<div className="text-left flex-1 min-w-0">
|
||||
<div className="text-sm font-medium text-gray-900 dark:text-gray-100 truncate max-w-[200px]">
|
||||
{server.name}
|
||||
>
|
||||
<div className="flex items-center gap-2 min-w-0">
|
||||
<img
|
||||
src={server?.provider?.icon || logoImg}
|
||||
alt={server.name}
|
||||
className="w-6 h-6 rounded-full dark:drop-shadow-[0_0_6px_rgb(255,255,255)]"
|
||||
onError={(e) => {
|
||||
const target = e.target as HTMLImageElement;
|
||||
target.src = logoImg;
|
||||
}}
|
||||
/>
|
||||
<div className="text-left flex-1 min-w-0">
|
||||
<div className="text-sm font-medium text-foreground truncate max-w-[200px]">
|
||||
{server.name}
|
||||
</div>
|
||||
<div className="text-xs text-muted-foreground truncate max-w-[200px]">
|
||||
{t("assistant.chat.aiAssistant")}:{" "}
|
||||
{server.stats?.assistant_count || 1}
|
||||
</div>
|
||||
</div>
|
||||
<div className="text-xs text-gray-500 dark:text-gray-400 truncate max-w-[200px]">
|
||||
{t("assistant.chat.aiAssistant")}:{" "}
|
||||
{server.stats?.assistant_count || 1}
|
||||
</div>
|
||||
<div className="flex flex-col items-center gap-2">
|
||||
<StatusIndicator
|
||||
enabled={server.enabled}
|
||||
public={server.public}
|
||||
hasProfile={!!server?.profile}
|
||||
status={server.health?.status}
|
||||
/>
|
||||
<div className="size-4 flex justify-end">
|
||||
{currentService?.id === server.id && (
|
||||
<VisibleKey
|
||||
shortcut="↓↑"
|
||||
shortcutClassName="w-6 -translate-x-4"
|
||||
>
|
||||
<Check className="w-full h-full text-muted-foreground" />
|
||||
</VisibleKey>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div className="flex flex-col items-center gap-2">
|
||||
<StatusIndicator
|
||||
enabled={server.enabled}
|
||||
public={server.public}
|
||||
hasProfile={!!server?.profile}
|
||||
status={server.health?.status}
|
||||
/>
|
||||
<div className="size-4 flex justify-end">
|
||||
{currentService?.id === server.id && (
|
||||
<VisibleKey
|
||||
shortcut="↓↑"
|
||||
shortcutClassName="w-6 -translate-x-4"
|
||||
>
|
||||
<Check className="w-full h-full text-gray-500 dark:text-gray-400" />
|
||||
</VisibleKey>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
))
|
||||
) : (
|
||||
<div className="flex flex-col items-center justify-center py-6 text-center">
|
||||
<Server className="w-8 h-8 text-muted-foreground mb-2" />
|
||||
<p className="text-sm text-muted-foreground">
|
||||
{t("assistant.chat.noServers")}
|
||||
</p>
|
||||
<button
|
||||
onClick={openSettings}
|
||||
className="mt-2 text-xs text-[#0287FF] hover:underline"
|
||||
>
|
||||
{t("assistant.chat.addServer")}
|
||||
</button>
|
||||
</div>
|
||||
))
|
||||
) : (
|
||||
<div className="flex flex-col items-center justify-center py-6 text-center">
|
||||
<Server className="w-8 h-8 text-gray-400 dark:text-gray-600 mb-2" />
|
||||
<p className="text-sm text-gray-500 dark:text-gray-400">
|
||||
{t("assistant.chat.noServers")}
|
||||
</p>
|
||||
<button
|
||||
onClick={openSettings}
|
||||
className="mt-2 text-xs text-[#0287FF] hover:underline"
|
||||
>
|
||||
{t("assistant.chat.addServer")}
|
||||
</button>
|
||||
</div>
|
||||
)}
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</PopoverPanel>
|
||||
</Popover>
|
||||
</PopoverContent>
|
||||
</Popover>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -122,7 +122,7 @@ const Splash = ({ assistantIDs = [], startPage }: SplashProps) => {
|
||||
return (
|
||||
<li key={id} className="mobile:w-full w-1/2 p-1">
|
||||
<div
|
||||
className="group h-[74px] px-3 py-2 text-sm rounded-xl border dark:border-[#262626] bg-white dark:bg-black cursor-pointer transition hover:!border-[#0087FF]"
|
||||
className="group h-[74px] px-3 py-2 text-sm rounded-xl border border-input bg-white dark:bg-black cursor-pointer transition hover:border-[#0087FF]!"
|
||||
onClick={() => {
|
||||
setCurrentAssistant(item);
|
||||
|
||||
|
||||
@@ -34,7 +34,7 @@ const AudioRecording: FC<AudioRecordingProps> = (props) => {
|
||||
const state = useReactive({ ...INITIAL_STATE });
|
||||
const containerRef = useRef<HTMLDivElement>(null);
|
||||
const recordRef = useRef<RecordPlugin>();
|
||||
const { withVisibility, addError } = useAppStore();
|
||||
const { addError } = useAppStore();
|
||||
const { currentService } = useConnectStore();
|
||||
|
||||
const { wavesurfer } = useWavesurfer({
|
||||
@@ -146,7 +146,7 @@ const AudioRecording: FC<AudioRecordingProps> = (props) => {
|
||||
};
|
||||
|
||||
const startRecording = async () => {
|
||||
await withVisibility(checkPermission);
|
||||
await checkPermission();
|
||||
state.isRecording = true;
|
||||
recordRef.current?.startRecording();
|
||||
};
|
||||
@@ -173,9 +173,9 @@ const AudioRecording: FC<AudioRecordingProps> = (props) => {
|
||||
|
||||
<div
|
||||
className={clsx(
|
||||
"absolute -inset-2 flex items-center gap-1 px-1 rounded translate-x-full transition-all bg-[#ededed] dark:bg-[#202126]",
|
||||
"absolute inset-0 flex items-center gap-1 px-1 rounded translate-x-full transition-all bg-[#ededed] dark:bg-[#202126]",
|
||||
{
|
||||
"!translate-x-0": state.isRecording || state.converting,
|
||||
"translate-x-0!": state.isRecording || state.converting,
|
||||
}
|
||||
)}
|
||||
>
|
||||
@@ -184,7 +184,7 @@ const AudioRecording: FC<AudioRecordingProps> = (props) => {
|
||||
className={clsx(
|
||||
"flex items-center justify-center size-6 bg-white dark:bg-black rounded-full transition cursor-pointer",
|
||||
{
|
||||
"!cursor-not-allowed opacity-50": state.converting,
|
||||
"cursor-not-allowed! opacity-50": state.converting,
|
||||
}
|
||||
)}
|
||||
onClick={() => resetState()}
|
||||
|
||||
@@ -107,7 +107,7 @@ export const MessageActions = ({
|
||||
<button
|
||||
id={copyButtonId}
|
||||
onClick={handleCopy}
|
||||
className="p-1 hover:bg-black/5 dark:hover:bg-white/5 rounded-lg transition-colors"
|
||||
className="p-1 rounded-lg hover:bg-muted transition-colors"
|
||||
>
|
||||
{copied ? (
|
||||
<Check
|
||||
@@ -131,7 +131,7 @@ export const MessageActions = ({
|
||||
{!isRefreshOnly && (
|
||||
<button
|
||||
onClick={handleLike}
|
||||
className={`p-1 hover:bg-black/5 dark:hover:bg-white/5 rounded-lg transition-colors ${
|
||||
className={`p-1 rounded-lg hover:bg-muted transition-colors ${
|
||||
liked ? "animate-shake" : ""
|
||||
}`}
|
||||
>
|
||||
@@ -151,7 +151,7 @@ export const MessageActions = ({
|
||||
{!isRefreshOnly && (
|
||||
<button
|
||||
onClick={handleDislike}
|
||||
className={`p-1 hover:bg-black/5 dark:hover:bg-white/5 rounded-lg transition-colors ${
|
||||
className={`p-1 rounded-lg hover:bg-muted transition-colors ${
|
||||
disliked ? "animate-shake" : ""
|
||||
}`}
|
||||
>
|
||||
@@ -172,7 +172,7 @@ export const MessageActions = ({
|
||||
<>
|
||||
<button
|
||||
onClick={handleSpeak}
|
||||
className="p-1 hover:bg-black/5 dark:hover:bg-white/5 rounded-lg transition-colors"
|
||||
className="p-1 rounded-lg hover:bg-muted transition-colors"
|
||||
>
|
||||
<Volume2
|
||||
className={`w-4 h-4 ${
|
||||
@@ -191,7 +191,7 @@ export const MessageActions = ({
|
||||
{question && (
|
||||
<button
|
||||
onClick={handleResend}
|
||||
className={`p-1 hover:bg-black/5 dark:hover:bg-white/5 rounded-lg transition-colors ${
|
||||
className={`p-1 rounded-lg hover:bg-muted transition-colors ${
|
||||
isResending ? "animate-spin" : ""
|
||||
}`}
|
||||
>
|
||||
|
||||
@@ -59,7 +59,7 @@ export function DataSourceItem({ name, icon, connector }: DataSourceItemProps) {
|
||||
{icon?.startsWith("font_") ? (
|
||||
<FontIcon name={icon} className="size-6" />
|
||||
) : (
|
||||
<img src={getTypeIcon()} alt={name} className="size-6 dark:drop-shadow-[0_0_6px_rgb(255,255,255)]" />
|
||||
<img src={getTypeIcon()} alt={name} className="size-6 rounded-full dark:drop-shadow-[0_0_6px_rgb(255,255,255)]" />
|
||||
)}
|
||||
|
||||
<span className="font-medium text-gray-900 dark:text-white">
|
||||
|
||||
@@ -44,7 +44,7 @@ export function DataSourcesList({ server }: { server: string }) {
|
||||
<h2 className="flex justify-between text-xl font-medium text-gray-900 dark:text-white">
|
||||
{t("cloud.dataSource.title")}
|
||||
<button
|
||||
className="p-2 text-gray-500 hover:text-gray-700 dark:text-gray-400 dark:hover:text-gray-300 rounded-[6px] bg-white dark:bg-gray-800 border border-[rgba(228,229,239,1)] dark:border-gray-700"
|
||||
className="p-2 text-gray-500 hover:text-gray-700 dark:text-gray-400 dark:hover:text-gray-300 rounded-md bg-white dark:bg-gray-800 border border-[rgba(228,229,239,1)] dark:border-gray-700"
|
||||
onClick={() => initServerAppData()}
|
||||
>
|
||||
<RefreshCcw
|
||||
|
||||
@@ -165,7 +165,7 @@ const LoginButton: FC<LoginButtonProps> = memo((props) => {
|
||||
|
||||
return (
|
||||
<button
|
||||
className="px-6 py-2 bg-blue-500 text-white rounded-[6px] hover:bg-blue-600 transition-colors mb-3"
|
||||
className="px-6 py-2 bg-blue-500 text-white rounded-md hover:bg-blue-600 transition-colors mb-3"
|
||||
onClick={LoginClick}
|
||||
aria-label={t("cloud.login")}
|
||||
>
|
||||
@@ -186,7 +186,7 @@ const LoadingState: FC<LoadingStateProps> = memo((props) => {
|
||||
return (
|
||||
<div className="flex items-center space-x-2 mb-3">
|
||||
<button
|
||||
className="px-6 py-2 text-white bg-red-500 rounded-[6px] hover:bg-red-600 transition-colors"
|
||||
className="px-6 py-2 text-white bg-red-500 rounded-md hover:bg-red-600 transition-colors"
|
||||
onClick={onCancel}
|
||||
>
|
||||
{t("cloud.cancel")}
|
||||
|
||||
@@ -18,7 +18,9 @@ const ServiceHeader = memo(
|
||||
({ refreshLoading, refreshClick }: ServiceHeaderProps) => {
|
||||
const { t } = useTranslation();
|
||||
|
||||
const cloudSelectService = useConnectStore((state) => state.cloudSelectService);
|
||||
const cloudSelectService = useConnectStore(
|
||||
(state) => state.cloudSelectService
|
||||
);
|
||||
|
||||
const { enableServer, removeServer } = useServers();
|
||||
|
||||
@@ -46,7 +48,7 @@ const ServiceHeader = memo(
|
||||
/>
|
||||
|
||||
<button
|
||||
className="p-2 text-gray-500 hover:text-gray-700 dark:text-gray-400 dark:hover:text-gray-300 rounded-[6px] bg-white dark:bg-gray-800 border border-[rgba(228,229,239,1)] dark:border-gray-700"
|
||||
className="p-2 text-gray-500 hover:text-gray-700 dark:text-gray-400 dark:hover:text-gray-300 rounded-md bg-white dark:bg-gray-800 border border-[rgba(228,229,239,1)] dark:border-gray-700"
|
||||
onClick={() =>
|
||||
OpenURLWithBrowser(cloudSelectService?.provider?.website)
|
||||
}
|
||||
@@ -54,7 +56,7 @@ const ServiceHeader = memo(
|
||||
<Globe className="w-3.5 h-3.5" />
|
||||
</button>
|
||||
<button
|
||||
className="p-2 text-gray-500 hover:text-gray-700 dark:text-gray-400 dark:hover:text-gray-300 rounded-[6px] bg-white dark:bg-gray-800 border border-[rgba(228,229,239,1)] dark:border-gray-700"
|
||||
className="p-2 text-gray-500 hover:text-gray-700 dark:text-gray-400 dark:hover:text-gray-300 rounded-md bg-white dark:bg-gray-800 border border-[rgba(228,229,239,1)] dark:border-gray-700"
|
||||
onClick={() => refreshClick(cloudSelectService?.id)}
|
||||
>
|
||||
<RefreshCcw
|
||||
@@ -63,7 +65,7 @@ const ServiceHeader = memo(
|
||||
</button>
|
||||
{!cloudSelectService?.builtin && (
|
||||
<button
|
||||
className="p-2 text-gray-500 hover:text-gray-700 dark:text-gray-400 dark:hover:text-gray-300 rounded-[6px] bg-white dark:bg-gray-800 border border-[rgba(228,229,239,1)] dark:border-gray-700"
|
||||
className="p-2 text-gray-500 hover:text-gray-700 dark:text-gray-400 dark:hover:text-gray-300 rounded-md bg-white dark:bg-gray-800 border border-[rgba(228,229,239,1)] dark:border-gray-700"
|
||||
onClick={() => removeServer(cloudSelectService?.id)}
|
||||
>
|
||||
<Trash2 className="w-3.5 h-3.5 text-[#ff4747]" />
|
||||
|
||||
@@ -53,7 +53,7 @@ export const Sidebar = forwardRef<{ refreshData: () => void }, SidebarProps>(
|
||||
<img
|
||||
src={item?.provider?.icon || cocoLogoImg}
|
||||
alt={`${item.name} logo`}
|
||||
className="w-5 h-5 flex-shrink-0 dark:drop-shadow-[0_0_6px_rgb(255,255,255)]"
|
||||
className="w-5 h-5 shrink-0 rounded-full dark:drop-shadow-[0_0_6px_rgb(255,255,255)]"
|
||||
onError={(e) => {
|
||||
const target = e.target as HTMLImageElement;
|
||||
target.src = cocoLogoImg;
|
||||
|
||||
@@ -62,13 +62,13 @@ const ApiDetails: React.FC = () => {
|
||||
{logs.map((log, index) => (
|
||||
<div
|
||||
key={index}
|
||||
className="p-4 border rounded-[6px] shadow-sm bg-gray-50"
|
||||
className="p-4 border rounded-md shadow-sm bg-gray-50"
|
||||
>
|
||||
<h4 className="font-semibold text-gray-800">
|
||||
Latest Request {index + 1}:
|
||||
</h4>
|
||||
<div className="text-sm text-gray-700 mt-1">
|
||||
<pre className="bg-gray-100 p-2 rounded-[6px] whitespace-pre-wrap">
|
||||
<pre className="bg-gray-100 p-2 rounded-md whitespace-pre-wrap">
|
||||
{JSON.stringify(log.request, null, 2)}
|
||||
</pre>
|
||||
</div>
|
||||
@@ -87,7 +87,7 @@ const ApiDetails: React.FC = () => {
|
||||
</h4>
|
||||
{showIndex === index ? (
|
||||
<div className="text-sm text-gray-700 mt-1">
|
||||
<pre className="bg-green-100 p-2 rounded-[6px] text-green-700 whitespace-pre-wrap">
|
||||
<pre className="bg-green-100 p-2 rounded-md text-green-700 whitespace-pre-wrap">
|
||||
{JSON.stringify(log.response, null, 2)}
|
||||
</pre>
|
||||
</div>
|
||||
@@ -98,7 +98,7 @@ const ApiDetails: React.FC = () => {
|
||||
<>
|
||||
<h4 className="font-semibold text-red-800 mt-4">Error:</h4>
|
||||
<div className="text-sm text-gray-700 mt-1">
|
||||
<pre className="bg-red-100 p-2 rounded-[6px] text-red-700 whitespace-pre-wrap">
|
||||
<pre className="bg-red-100 p-2 rounded-md text-red-700 whitespace-pre-wrap">
|
||||
{JSON.stringify(log.error, null, 2)}
|
||||
</pre>
|
||||
</div>
|
||||
|
||||
@@ -2,6 +2,7 @@ import React, { useEffect, useCallback } from "react";
|
||||
import { Bot, Search } from "lucide-react";
|
||||
|
||||
import platformAdapter from "@/utils/platformAdapter";
|
||||
import clsx from "clsx";
|
||||
|
||||
interface ChatSwitchProps {
|
||||
isChatMode: boolean;
|
||||
@@ -29,19 +30,31 @@ const ChatSwitch: React.FC<ChatSwitchProps> = ({ isChatMode, onChange }) => {
|
||||
<div
|
||||
role="switch"
|
||||
aria-checked={isChatMode}
|
||||
className={`relative flex items-center justify-between w-10 h-[20px] rounded-full cursor-pointer transition-colors duration-300 ${
|
||||
isChatMode ? "bg-[#0072ff]" : "bg-[var(--coco-primary-color)]"
|
||||
className={`relative flex items-center justify-between w-10 h-5 rounded-full cursor-pointer transition-colors duration-300 ${
|
||||
isChatMode ? "bg-[#0072ff]" : "bg-(--coco-primary-color)"
|
||||
}`}
|
||||
onClick={handleToggle}
|
||||
>
|
||||
<div className="absolute top-0 left-0 w-full h-full pointer-events-none flex items-center justify-between px-1">
|
||||
{isChatMode ? <Bot className="w-4 h-4 text-white" /> : <div></div>}
|
||||
{!isChatMode ? <Search className="w-4 h-4 text-white" /> : <div></div>}
|
||||
</div>
|
||||
<div
|
||||
className={`absolute top-[1px] left-[1px] h-[18px] w-[18px] bg-white rounded-full shadow-md transform transition-transform duration-300 ${
|
||||
isChatMode ? "translate-x-5" : "translate-x-0"
|
||||
}`}
|
||||
className={clsx(
|
||||
"absolute inset-0 pointer-events-none flex items-center px-1 text-white",
|
||||
{
|
||||
"justify-end": !isChatMode,
|
||||
}
|
||||
)}
|
||||
>
|
||||
{isChatMode ? (
|
||||
<Bot className="size-4" />
|
||||
) : (
|
||||
<Search className="size-4" />
|
||||
)}
|
||||
</div>
|
||||
|
||||
<div
|
||||
className={clsx(
|
||||
"absolute top-px h-4.5 w-4.5 bg-white rounded-full shadow-md",
|
||||
[isChatMode ? "right-px" : "left-px"]
|
||||
)}
|
||||
></div>
|
||||
</div>
|
||||
);
|
||||
|
||||
@@ -1,33 +1,35 @@
|
||||
import {
|
||||
CheckboxProps as HeadlessCheckboxProps,
|
||||
Checkbox as HeadlessCheckbox,
|
||||
} from "@headlessui/react";
|
||||
import * as CheckboxPrimitive from "@radix-ui/react-checkbox";
|
||||
import type { ComponentProps } from "react";
|
||||
import clsx from "clsx";
|
||||
import { CheckIcon } from "lucide-react";
|
||||
|
||||
interface CheckboxProps extends HeadlessCheckboxProps {
|
||||
interface CheckboxProps
|
||||
extends Omit<ComponentProps<typeof CheckboxPrimitive.Root>, "onCheckedChange" | "onChange"> {
|
||||
indeterminate?: boolean;
|
||||
onChange?: (checked: boolean) => void;
|
||||
}
|
||||
|
||||
const Checkbox = (props: CheckboxProps) => {
|
||||
const { indeterminate, className, ...rest } = props;
|
||||
const { indeterminate, className, onChange, checked, ...rest } = props;
|
||||
|
||||
return (
|
||||
<HeadlessCheckbox
|
||||
<CheckboxPrimitive.Root
|
||||
{...rest}
|
||||
checked={checked}
|
||||
onCheckedChange={(v) => onChange?.(v === true)}
|
||||
className={clsx(
|
||||
"group size-4 rounded-sm border border-black/30 dark:border-white/30 data-[checked]:bg-[#2F54EB] data-[checked]:!border-[#2F54EB] transition cursor-pointer",
|
||||
"group h-4 w-4 rounded-sm border border-black/30 dark:border-white/30 data-[state=checked]:bg-[#2F54EB] data-[state=checked]:border-[#2F54EB] transition cursor-pointer inline-flex items-center justify-center",
|
||||
className
|
||||
)}
|
||||
>
|
||||
{indeterminate && (
|
||||
<div className="size-full flex items-center justify-center group-data-[checked]:hidden">
|
||||
<div className="size-2 bg-[#2F54EB]"></div>
|
||||
<div className="h-full w-full flex items-center justify-center group-data-[state=checked]:hidden">
|
||||
<div className="h-2 w-2 bg-[#2F54EB]"></div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
<CheckIcon className="hidden size-[14px] text-white group-data-[checked]:block" />
|
||||
</HeadlessCheckbox>
|
||||
<CheckIcon className="hidden h-[14px] w-[14px] text-white group-data-[state=checked]:block" />
|
||||
</CheckboxPrimitive.Root>
|
||||
);
|
||||
};
|
||||
|
||||
|
||||
@@ -8,7 +8,7 @@ const Copyright = () => {
|
||||
const renderLogo = () => {
|
||||
return (
|
||||
<a href="https://coco.rs/" target="_blank">
|
||||
<img src={isDark ? logoDark : logoLight} alt="Logo" className="h-4" />
|
||||
<img src={isDark ? logoDark : logoLight} alt="Logo" className="h-4!" />
|
||||
</a>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -1,24 +1,20 @@
|
||||
import {
|
||||
Button,
|
||||
ButtonProps,
|
||||
Description,
|
||||
Dialog,
|
||||
DialogPanel,
|
||||
DialogTitle,
|
||||
} from "@headlessui/react";
|
||||
import { FC, KeyboardEvent } from "react";
|
||||
import { Dialog, DialogContent, DialogHeader, DialogTitle, DialogDescription } from "@/components/ui/dialog";
|
||||
import { Button } from "@/components/ui/button";
|
||||
import { FC, KeyboardEvent, ComponentProps } from "react";
|
||||
import clsx from "clsx";
|
||||
import { twMerge } from "tailwind-merge";
|
||||
|
||||
import VisibleKey from "./VisibleKey";
|
||||
import { useTranslation } from "react-i18next";
|
||||
|
||||
type ShadButtonProps = ComponentProps<typeof Button>;
|
||||
|
||||
interface DeleteDialogProps {
|
||||
isOpen: boolean;
|
||||
title: string;
|
||||
description: string;
|
||||
deleteButtonProps?: ButtonProps;
|
||||
cancelButtonProps?: ButtonProps;
|
||||
deleteButtonProps?: ShadButtonProps;
|
||||
cancelButtonProps?: ShadButtonProps;
|
||||
reverseButtonPosition?: boolean;
|
||||
setIsOpen: (isOpen: boolean) => void;
|
||||
onCancel: () => void;
|
||||
@@ -49,69 +45,60 @@ const DeleteDialog: FC<DeleteDialogProps> = (props) => {
|
||||
};
|
||||
|
||||
return (
|
||||
<Dialog
|
||||
open={isOpen}
|
||||
onClose={() => setIsOpen(false)}
|
||||
className="relative z-1000"
|
||||
>
|
||||
<div
|
||||
id="headlessui-popover-panel:delete-history"
|
||||
className="fixed inset-0 flex items-center justify-center w-screen"
|
||||
>
|
||||
<DialogPanel className="flex flex-col justify-between w-[360px] h-[160px] p-3 text-[#333] dark:text-white/90 border border-[#e6e6e6] bg-white dark:bg-[#202126] dark:border-white/10 shadow-[0_4px_10px_rgba(0,0,0,0.2)] rounded-lg dark:shadow-[0_8px_20px_rgba(0,0,0,0.4)]">
|
||||
<div className="flex flex-col gap-3">
|
||||
<DialogTitle className="text-base font-bold">{title}</DialogTitle>
|
||||
<Description className="text-sm">{description}</Description>
|
||||
</div>
|
||||
<Dialog open={isOpen} onOpenChange={setIsOpen}>
|
||||
<DialogContent className="flex flex-col justify-between w-[360px] h-[160px] p-3 text-[#333] dark:text-white/90 border border-[#e6e6e6] bg-white dark:bg-[#202126] dark:border-white/10 shadow-[0_4px_10px_rgba(0,0,0,0.2)] rounded-lg dark:shadow-[0_8px_20px_rgba(0,0,0,0.4)]">
|
||||
<DialogHeader className="mb-2">
|
||||
<DialogTitle className="text-base font-bold">{title}</DialogTitle>
|
||||
<DialogDescription className="text-sm">{description}</DialogDescription>
|
||||
</DialogHeader>
|
||||
|
||||
<div
|
||||
className={clsx("flex gap-4 self-end", {
|
||||
"flex-row-reverse": reverseButtonPosition,
|
||||
})}
|
||||
<div
|
||||
className={clsx("flex gap-4 self-end", {
|
||||
"flex-row-reverse": reverseButtonPosition,
|
||||
})}
|
||||
>
|
||||
<VisibleKey
|
||||
shortcut="N"
|
||||
shortcutClassName="left-[unset] right-0"
|
||||
onKeyPress={onCancel}
|
||||
>
|
||||
<VisibleKey
|
||||
shortcut="N"
|
||||
shortcutClassName="left-[unset] right-0"
|
||||
onKeyPress={onCancel}
|
||||
<Button
|
||||
{...cancelButtonProps}
|
||||
autoFocus
|
||||
className={twMerge(
|
||||
"h-8 px-4 text-sm text-[#666666] bg-[#F8F9FA] dark:text-white dark:bg-[#202126] border border-[#E6E6E6] dark:border-white/10 rounded-lg focus:border-black/30 dark:focus:border-white/50 transition",
|
||||
cancelButtonProps?.className as string
|
||||
)}
|
||||
onClick={onCancel}
|
||||
onKeyDown={(event) => {
|
||||
handleEnter(event, onCancel);
|
||||
}}
|
||||
>
|
||||
<Button
|
||||
{...cancelButtonProps}
|
||||
autoFocus
|
||||
className={twMerge(
|
||||
"h-8 px-4 text-sm text-[#666666] bg-[#F8F9FA] dark:text-white dark:bg-[#202126] border border-[#E6E6E6] dark:border-white/10 rounded-lg focus:border-black/30 dark:focus:border-white/50 transition",
|
||||
cancelButtonProps?.className as string
|
||||
)}
|
||||
onClick={onCancel}
|
||||
onKeyDown={(event) => {
|
||||
handleEnter(event, onCancel);
|
||||
}}
|
||||
>
|
||||
{t("deleteDialog.button.cancel")}
|
||||
</Button>
|
||||
</VisibleKey>
|
||||
{t("deleteDialog.button.cancel")}
|
||||
</Button>
|
||||
</VisibleKey>
|
||||
|
||||
<VisibleKey
|
||||
shortcut="Y"
|
||||
shortcutClassName="left-[unset] right-0"
|
||||
onKeyPress={onDelete}
|
||||
<VisibleKey
|
||||
shortcut="Y"
|
||||
shortcutClassName="left-[unset] right-0"
|
||||
onKeyPress={onDelete}
|
||||
>
|
||||
<Button
|
||||
{...deleteButtonProps}
|
||||
className={twMerge(
|
||||
"h-8 px-4 text-sm text-white bg-[#EF4444] rounded-lg border border-[#EF4444] focus:border-black/30 dark:focus:border-white/50 transition",
|
||||
deleteButtonProps?.className as string
|
||||
)}
|
||||
onClick={onDelete}
|
||||
onKeyDown={(event) => {
|
||||
handleEnter(event, onDelete);
|
||||
}}
|
||||
>
|
||||
<Button
|
||||
{...deleteButtonProps}
|
||||
className={twMerge(
|
||||
"h-8 px-4 text-sm text-white bg-[#EF4444] rounded-lg border border-[#EF4444] focus:border-black/30 dark:focus:border-white/50 transition",
|
||||
deleteButtonProps?.className as string
|
||||
)}
|
||||
onClick={onDelete}
|
||||
onKeyDown={(event) => {
|
||||
handleEnter(event, onDelete);
|
||||
}}
|
||||
>
|
||||
{t("deleteDialog.button.delete")}
|
||||
</Button>
|
||||
</VisibleKey>
|
||||
</div>
|
||||
</DialogPanel>
|
||||
</div>
|
||||
{t("deleteDialog.button.delete")}
|
||||
</Button>
|
||||
</VisibleKey>
|
||||
</div>
|
||||
</DialogContent>
|
||||
</Dialog>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -1,10 +1,11 @@
|
||||
import {
|
||||
Button,
|
||||
Description,
|
||||
Dialog,
|
||||
DialogPanel,
|
||||
DialogContent,
|
||||
DialogHeader,
|
||||
DialogTitle,
|
||||
} from "@headlessui/react";
|
||||
DialogDescription,
|
||||
} from "@/components/ui/dialog";
|
||||
import { Button } from "@/components/ui/button";
|
||||
import { useTranslation } from "react-i18next";
|
||||
|
||||
import VisibleKey from "@/components/Common/VisibleKey";
|
||||
@@ -36,69 +37,63 @@ const DeleteDialog = ({
|
||||
};
|
||||
|
||||
return (
|
||||
<Dialog
|
||||
open={isOpen}
|
||||
onClose={() => setIsOpen(false)}
|
||||
className="relative z-1000"
|
||||
>
|
||||
<div
|
||||
id="headlessui-popover-panel:delete-history"
|
||||
className="fixed inset-0 flex items-center justify-center w-screen"
|
||||
>
|
||||
<DialogPanel className="flex flex-col justify-between w-[360px] h-[160px] p-3 text-[#333] dark:text-white/90 border border-[#e6e6e6] bg-white dark:bg-[#202126] dark:border-white/10 shadow-xl rounded-lg">
|
||||
<div className="flex flex-col gap-3">
|
||||
<DialogTitle className="text-base font-bold">
|
||||
{t("history_list.delete_modal.title")}
|
||||
</DialogTitle>
|
||||
<Description className="text-sm">
|
||||
{t("history_list.delete_modal.description", {
|
||||
replace: [
|
||||
active?._source?.title ||
|
||||
active?._source?.message ||
|
||||
active?._id,
|
||||
],
|
||||
})}
|
||||
</Description>
|
||||
</div>
|
||||
<Dialog open={isOpen} onOpenChange={setIsOpen}>
|
||||
<DialogContent className="flex flex-col justify-between w-[360px] h-40 p-3 text-[#333] dark:text-white/90 border border-[#e6e6e6] bg-white dark:bg-[#202126] dark:border-white/10 shadow-xl rounded-lg">
|
||||
<DialogHeader className="mb-2">
|
||||
<DialogTitle className="text-base font-bold">
|
||||
{t("history_list.delete_modal.title")}
|
||||
</DialogTitle>
|
||||
<DialogDescription className="text-sm">
|
||||
{t("history_list.delete_modal.description", {
|
||||
replace: [
|
||||
active?._source?.title ||
|
||||
active?._source?.message ||
|
||||
active?._id,
|
||||
],
|
||||
})}
|
||||
</DialogDescription>
|
||||
</DialogHeader>
|
||||
|
||||
<div className="flex gap-4 self-end">
|
||||
<VisibleKey
|
||||
shortcut="N"
|
||||
shortcutClassName="left-[unset] right-0"
|
||||
onKeyPress={() => setIsOpen(false)}
|
||||
<div className="flex gap-4 self-end">
|
||||
<VisibleKey
|
||||
shortcut="N"
|
||||
shortcutClassName="left-[unset] right-0"
|
||||
onKeyPress={() => {
|
||||
setIsOpen(false);
|
||||
}}
|
||||
>
|
||||
<Button
|
||||
variant="outline"
|
||||
autoFocus
|
||||
onClick={() => setIsOpen(false)}
|
||||
onKeyDown={(event) => {
|
||||
handleEnter(event, () => {
|
||||
setIsOpen(false);
|
||||
});
|
||||
}}
|
||||
>
|
||||
<Button
|
||||
autoFocus
|
||||
className="h-8 px-4 text-sm text-[#666666] bg-[#F8F9FA] dark:text-white dark:bg-[#202126] border border-[#E6E6E6] dark:border-white/10 rounded-lg focus:border-black/30 dark:focus:border-white/50 transition"
|
||||
onClick={() => setIsOpen(false)}
|
||||
onKeyDown={(event) => {
|
||||
handleEnter(event, () => {
|
||||
setIsOpen(false);
|
||||
});
|
||||
}}
|
||||
>
|
||||
{t("history_list.delete_modal.button.cancel")}
|
||||
</Button>
|
||||
</VisibleKey>
|
||||
{t("history_list.delete_modal.button.cancel")}
|
||||
</Button>
|
||||
</VisibleKey>
|
||||
|
||||
<VisibleKey
|
||||
shortcut="Y"
|
||||
shortcutClassName="left-[unset] right-0"
|
||||
onKeyPress={handleRemove}
|
||||
<VisibleKey
|
||||
shortcut="Y"
|
||||
shortcutClassName="left-[unset] right-0"
|
||||
onKeyPress={handleRemove}
|
||||
>
|
||||
<Button
|
||||
variant="destructive"
|
||||
className="text-white"
|
||||
onClick={handleRemove}
|
||||
onKeyDown={(event) => {
|
||||
handleEnter(event, handleRemove);
|
||||
}}
|
||||
>
|
||||
<Button
|
||||
className="h-8 px-4 text-sm text-white bg-[#EF4444] rounded-lg border border-[#EF4444] focus:border-black/30 dark:focus:border-white/50 transition"
|
||||
onClick={handleRemove}
|
||||
onKeyDown={(event) => {
|
||||
handleEnter(event, handleRemove);
|
||||
}}
|
||||
>
|
||||
{t("history_list.delete_modal.button.delete")}
|
||||
</Button>
|
||||
</VisibleKey>
|
||||
</div>
|
||||
</DialogPanel>
|
||||
</div>
|
||||
{t("history_list.delete_modal.button.delete")}
|
||||
</Button>
|
||||
</VisibleKey>
|
||||
</div>
|
||||
</DialogContent>
|
||||
</Dialog>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -113,7 +113,8 @@ const HistoryListContent: FC<HistoryListContentProps> = ({
|
||||
const scrollToElement = useCallback(
|
||||
(elementId: string, isKeyboardNav: boolean) => {
|
||||
if (!listRef.current) return;
|
||||
if (typeof window === 'undefined' || typeof document === 'undefined') return;
|
||||
if (typeof window === "undefined" || typeof document === "undefined")
|
||||
return;
|
||||
|
||||
const element = listRef.current.querySelector(`#${elementId}`);
|
||||
if (!element) return;
|
||||
@@ -123,7 +124,7 @@ const HistoryListContent: FC<HistoryListContentProps> = ({
|
||||
const isVisible =
|
||||
rect.top >= 0 &&
|
||||
rect.bottom <=
|
||||
(window.innerHeight || document.documentElement.clientHeight);
|
||||
(window.innerHeight || document.documentElement.clientHeight);
|
||||
|
||||
// Only scroll if element is not visible
|
||||
if (!isVisible) {
|
||||
|
||||
@@ -1,10 +1,16 @@
|
||||
import { FC, useRef, useCallback, useState } from "react";
|
||||
import { Input, Popover, PopoverButton, PopoverPanel } from "@headlessui/react";
|
||||
import { FC, useRef, useCallback, useState, useEffect } from "react";
|
||||
import { Ellipsis } from "lucide-react";
|
||||
import clsx from "clsx";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import { Pencil, Trash2 } from "lucide-react";
|
||||
|
||||
import {
|
||||
Popover,
|
||||
PopoverTrigger,
|
||||
PopoverContent,
|
||||
PopoverPortal,
|
||||
} from "@/components/ui/popover";
|
||||
import { Input } from "@/components/ui/input";
|
||||
import type { Chat } from "@/types/chat";
|
||||
import VisibleKey from "../VisibleKey";
|
||||
|
||||
@@ -31,9 +37,11 @@ const HistoryListItem: FC<HistoryListItemProps> = ({
|
||||
const moreButtonRef = useRef<HTMLButtonElement>(null);
|
||||
const { _id, _source } = item;
|
||||
const title = _source?.title ?? _id;
|
||||
const isActive = item._id === active?._id || item._id === highlightId;
|
||||
const isSelected = item._id === active?._id;
|
||||
const isHovered = item._id === highlightId;
|
||||
|
||||
const [isEdit, setIsEdit] = useState(false);
|
||||
const [open, setOpen] = useState(false);
|
||||
|
||||
const onContextMenu = useCallback(
|
||||
(e: React.MouseEvent) => {
|
||||
@@ -72,24 +80,34 @@ const HistoryListItem: FC<HistoryListItemProps> = ({
|
||||
},
|
||||
];
|
||||
|
||||
useEffect(() => {
|
||||
if (!(isSelected || isHovered) || isEdit) {
|
||||
setOpen(false);
|
||||
}
|
||||
}, [isSelected, isHovered, isEdit]);
|
||||
|
||||
return (
|
||||
<li
|
||||
key={_id}
|
||||
id={_id}
|
||||
className={clsx(
|
||||
"flex items-center mt-1 h-10 rounded-lg cursor-pointer hover:bg-[#EDEDED] dark:hover:bg-[#353F4D] transition",
|
||||
"group flex w-full items-center mt-1 h-10 rounded-lg cursor-pointer hover:bg-[#EDEDED] dark:hover:bg-[#353F4D] transition-colors",
|
||||
{
|
||||
"!bg-[#E5E7EB] dark:!bg-[#2B3444]": isActive,
|
||||
"bg-[#E5E7EB] dark:bg-[#2B3444]": isSelected,
|
||||
"bg-[#EDEDED] dark:bg-[#353F4D]": isHovered && !isSelected,
|
||||
}
|
||||
)}
|
||||
onClick={() => {
|
||||
if (!isActive) {
|
||||
if (!isSelected) {
|
||||
setIsEdit(false);
|
||||
}
|
||||
|
||||
onSelect(item);
|
||||
}}
|
||||
onMouseEnter={onMouseEnter}
|
||||
onMouseLeave={() => {
|
||||
setOpen(false);
|
||||
}}
|
||||
onContextMenu={onContextMenu}
|
||||
>
|
||||
<div
|
||||
@@ -99,11 +117,11 @@ const HistoryListItem: FC<HistoryListItemProps> = ({
|
||||
/>
|
||||
|
||||
<div className="flex-1 flex items-center justify-between gap-2 px-2 overflow-hidden">
|
||||
{isEdit && isActive ? (
|
||||
{isEdit && isSelected ? (
|
||||
<Input
|
||||
autoFocus
|
||||
defaultValue={title}
|
||||
className="flex-1 -mx-px outline-none bg-transparent border border-[#0061FF] rounded-[4px]"
|
||||
className="flex-1 -mx-px outline-none bg-transparent border border-[#0061FF] rounded-sm"
|
||||
onKeyDown={(event) => {
|
||||
if (event.key !== "Enter") return;
|
||||
|
||||
@@ -128,7 +146,7 @@ const HistoryListItem: FC<HistoryListItemProps> = ({
|
||||
)}
|
||||
|
||||
<div className="flex items-center gap-2">
|
||||
{isActive && !isEdit && (
|
||||
{!isEdit && isSelected && (
|
||||
<VisibleKey
|
||||
shortcut="↑↓"
|
||||
rootClassName="w-6"
|
||||
@@ -136,56 +154,73 @@ const HistoryListItem: FC<HistoryListItemProps> = ({
|
||||
/>
|
||||
)}
|
||||
|
||||
<Popover>
|
||||
{isActive && !isEdit && (
|
||||
<PopoverButton ref={moreButtonRef} className="flex gap-2">
|
||||
<VisibleKey
|
||||
shortcut="O"
|
||||
onKeyPress={() => {
|
||||
moreButtonRef.current?.click();
|
||||
}}
|
||||
>
|
||||
<Ellipsis className="size-4 text-[#979797]" />
|
||||
</VisibleKey>
|
||||
</PopoverButton>
|
||||
)}
|
||||
|
||||
<PopoverPanel
|
||||
anchor="bottom"
|
||||
className="flex flex-col rounded-lg shadow-md z-100 bg-white dark:bg-[#202126] p-1 border border-black/2 dark:border-white/10"
|
||||
onClick={(event) => {
|
||||
event.stopPropagation();
|
||||
<Popover open={open} onOpenChange={setOpen}>
|
||||
<PopoverTrigger
|
||||
ref={moreButtonRef}
|
||||
className={clsx("flex gap-2", {
|
||||
"opacity-100 pointer-events-auto":
|
||||
!isEdit && (isSelected || isHovered),
|
||||
"opacity-0 pointer-events-none": !(
|
||||
!isEdit &&
|
||||
(isSelected || isHovered)
|
||||
),
|
||||
})}
|
||||
onClick={(e) => {
|
||||
e.stopPropagation();
|
||||
setOpen((prev) => !prev);
|
||||
}}
|
||||
>
|
||||
{menuItems.map((menuItem) => {
|
||||
const {
|
||||
label,
|
||||
icon: Icon,
|
||||
shortcut,
|
||||
iconColor,
|
||||
onClick,
|
||||
} = menuItem;
|
||||
<VisibleKey
|
||||
shortcut="O"
|
||||
onKeyPress={() => {
|
||||
moreButtonRef.current?.click();
|
||||
}}
|
||||
>
|
||||
<Ellipsis className="size-4 text-[#979797]" />
|
||||
</VisibleKey>
|
||||
</PopoverTrigger>
|
||||
|
||||
return (
|
||||
<button
|
||||
key={label}
|
||||
className="flex items-center gap-2 px-3 py-2 text-sm rounded-[6px] hover:bg-[#EDEDED] dark:hover:bg-[#2B2C31] transition"
|
||||
onClick={onClick}
|
||||
>
|
||||
<VisibleKey shortcut={shortcut} onKeyPress={onClick}>
|
||||
<Icon
|
||||
className="size-4"
|
||||
style={{
|
||||
color: iconColor,
|
||||
}}
|
||||
/>
|
||||
</VisibleKey>
|
||||
<PopoverPortal>
|
||||
<PopoverContent
|
||||
side="bottom"
|
||||
className="flex flex-col rounded-lg shadow-md z-100 bg-white dark:bg-[#202126] p-1 border border-black/2 dark:border-white/10"
|
||||
onClick={(event) => {
|
||||
event.stopPropagation();
|
||||
}}
|
||||
onMouseLeave={() => {
|
||||
setOpen(false);
|
||||
}}
|
||||
>
|
||||
{menuItems.map((menuItem) => {
|
||||
const {
|
||||
label,
|
||||
icon: Icon,
|
||||
shortcut,
|
||||
iconColor,
|
||||
onClick,
|
||||
} = menuItem;
|
||||
|
||||
<span>{t(label)}</span>
|
||||
</button>
|
||||
);
|
||||
})}
|
||||
</PopoverPanel>
|
||||
return (
|
||||
<button
|
||||
key={label}
|
||||
className="flex items-center gap-2 px-3 py-2 text-sm rounded-md hover:bg-[#EDEDED] dark:hover:bg-[#2B2C31] transition"
|
||||
onClick={onClick}
|
||||
>
|
||||
<VisibleKey shortcut={shortcut} onKeyPress={onClick}>
|
||||
<Icon
|
||||
className="size-4"
|
||||
style={{
|
||||
color: iconColor,
|
||||
}}
|
||||
/>
|
||||
</VisibleKey>
|
||||
|
||||
<span>{t(label)}</span>
|
||||
</button>
|
||||
);
|
||||
})}
|
||||
</PopoverContent>
|
||||
</PopoverPortal>
|
||||
</Popover>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { Input } from "@headlessui/react";
|
||||
import { Input } from "@/components/ui/input";
|
||||
import { debounce } from "lodash-es";
|
||||
import { FC, useMemo, useRef, useState } from "react";
|
||||
import clsx from "clsx";
|
||||
@@ -9,6 +9,7 @@ import VisibleKey from "../VisibleKey";
|
||||
import { Chat } from "@/types/chat";
|
||||
import { closeHistoryPanel } from "@/utils";
|
||||
import HistoryListContent from "./HistoryListContent";
|
||||
import { Button } from "@/components/ui/button";
|
||||
|
||||
interface HistoryListProps {
|
||||
historyPanelId?: string;
|
||||
@@ -57,21 +58,21 @@ const HistoryList: FC<HistoryListProps> = (props) => {
|
||||
"flex flex-col h-screen text-sm bg-[#F3F4F6] dark:bg-[#1F2937]"
|
||||
)}
|
||||
>
|
||||
<div className="flex gap-1 p-2 border-b dark:border-[#343D4D]">
|
||||
<div className="flex-1 flex items-center gap-2 px-2 rounded-lg border transition border-[#E6E6E6] bg-[#F8F9FA] dark:bg-[#2B3444] dark:border-[#343D4D] focus-within:border-[#0061FF]">
|
||||
<div className="flex gap-1 p-2 border-b border-input">
|
||||
<div className="flex-1 h-8 flex items-center px-2 rounded-lg border border-input bg-background transition focus-within:ring-2 focus-within:ring-ring focus-within:ring-offset-2 focus-within:ring-offset-background">
|
||||
<VisibleKey
|
||||
shortcut="F"
|
||||
onKeyPress={() => {
|
||||
searchInputRef.current?.focus();
|
||||
}}
|
||||
>
|
||||
<Search className="size-4 text-[#6B7280]" />
|
||||
<Search className="size-4 text-muted-foreground" />
|
||||
</VisibleKey>
|
||||
|
||||
<Input
|
||||
autoFocus
|
||||
ref={searchInputRef}
|
||||
className="w-full bg-transparent outline-none"
|
||||
className="w-full h-8 bg-transparent border-0 shadow-none focus-visible:ring-0 focus-visible:ring-offset-0"
|
||||
placeholder={t("history_list.search.placeholder")}
|
||||
onChange={(event) => {
|
||||
debouncedSearch(event.target.value);
|
||||
@@ -79,18 +80,20 @@ const HistoryList: FC<HistoryListProps> = (props) => {
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div
|
||||
className="size-8 flex items-center justify-center rounded-lg border text-[#0072FF] border-[#E6E6E6] bg-[#F3F4F6] dark:border-[#343D4D] dark:bg-[#1F2937] hover:bg-[#F8F9FA] dark:hover:bg-[#353F4D] cursor-pointer transition"
|
||||
<Button
|
||||
variant="outline"
|
||||
size="icon"
|
||||
className="size-8"
|
||||
onClick={handleRefresh}
|
||||
>
|
||||
<VisibleKey shortcut="R" onKeyPress={handleRefresh}>
|
||||
<RefreshCcw
|
||||
className={clsx("size-4", {
|
||||
className={clsx("size-4 text-[#0287FF]", {
|
||||
"animate-spin": isRefresh,
|
||||
})}
|
||||
/>
|
||||
</VisibleKey>
|
||||
</div>
|
||||
</Button>
|
||||
</div>
|
||||
|
||||
<div className="flex-1 px-2 overflow-auto custom-scrollbar">
|
||||
@@ -104,10 +107,10 @@ const HistoryList: FC<HistoryListProps> = (props) => {
|
||||
</div>
|
||||
|
||||
{historyPanelId && (
|
||||
<div className="flex justify-end p-2 border-t dark:border-[#343D4D]">
|
||||
<div className="flex justify-end p-2 border-t border-input">
|
||||
<VisibleKey shortcut="Esc" shortcutClassName="w-7">
|
||||
<PanelLeftClose
|
||||
className="size-4 text-black/80 dark:text-white/80 cursor-pointer"
|
||||
className="size-4 text-muted-foreground cursor-pointer"
|
||||
onClick={closeHistoryPanel}
|
||||
/>
|
||||
</VisibleKey>
|
||||
|
||||
@@ -41,7 +41,7 @@ function UniversalIcon({
|
||||
icon,
|
||||
defaultIcon = File,
|
||||
appIcon = false,
|
||||
className = "w-5 h-5 flex-shrink-0",
|
||||
className = "w-5 h-5 shrink-0",
|
||||
onClick = () => {},
|
||||
wrapWithIconWrapper = true,
|
||||
}: UniversalIconProps) {
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import { ChevronLeft, ChevronRight } from "lucide-react";
|
||||
|
||||
import VisibleKey from "./VisibleKey";
|
||||
import { cn } from "@/lib/utils";
|
||||
|
||||
interface PaginationProps {
|
||||
current: number;
|
||||
@@ -19,10 +20,15 @@ function Pagination({
|
||||
}: PaginationProps) {
|
||||
return (
|
||||
<div
|
||||
className={`flex items-center justify-between h-8 px-3 text-[#999] border-t dark:border-t-white/10 ${className}`}
|
||||
className={`flex items-center justify-between h-8 px-2 text-muted-foreground border-t border-input ${className}`}
|
||||
>
|
||||
<VisibleKey shortcut="leftarrow" onKeyPress={onPrev}>
|
||||
<ChevronLeft className="size-4 cursor-pointer" onClick={onPrev} />
|
||||
<ChevronLeft
|
||||
className={cn("size-4 cursor-pointer", {
|
||||
"cursor-not-allowed opacity-50": current === 1,
|
||||
})}
|
||||
onClick={onPrev}
|
||||
/>
|
||||
</VisibleKey>
|
||||
|
||||
<div className="text-xs">
|
||||
@@ -30,7 +36,12 @@ function Pagination({
|
||||
</div>
|
||||
|
||||
<VisibleKey shortcut="rightarrow" onKeyPress={onNext}>
|
||||
<ChevronRight className="size-4 cursor-pointer" onClick={onNext} />
|
||||
<ChevronRight
|
||||
className={cn("size-4 cursor-pointer", {
|
||||
"cursor-not-allowed opacity-50": current === totalPage,
|
||||
})}
|
||||
onClick={onNext}
|
||||
/>
|
||||
</VisibleKey>
|
||||
</div>
|
||||
);
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import { Input, InputProps } from "@headlessui/react";
|
||||
import type { InputProps } from "@/components/ui/input";
|
||||
import { Input } from "@/components/ui/input";
|
||||
import { useKeyPress } from "ahooks";
|
||||
import { forwardRef, useImperativeHandle, useRef } from "react";
|
||||
|
||||
@@ -29,7 +30,7 @@ const PopoverInput = forwardRef<HTMLInputElement, InputProps>((props, ref) => {
|
||||
}
|
||||
);
|
||||
|
||||
return <Input autoCorrect="off" ref={inputRef} {...props} />;
|
||||
return <Input autoCorrect="off" ref={inputRef} {...(props as any)} />;
|
||||
});
|
||||
|
||||
export default PopoverInput;
|
||||
|
||||
@@ -1,20 +1,20 @@
|
||||
import { RefObject } from "react";
|
||||
import clsx from "clsx";
|
||||
import { ArrowDown } from "lucide-react";
|
||||
import { Button } from "../ui/button";
|
||||
|
||||
interface ScrollToBottomProps {
|
||||
scrollRef: RefObject<HTMLDivElement>;
|
||||
isAtBottom: boolean;
|
||||
}
|
||||
|
||||
const ScrollToBottom = ({
|
||||
scrollRef,
|
||||
isAtBottom,
|
||||
}: ScrollToBottomProps) => {
|
||||
const ScrollToBottom = ({ scrollRef, isAtBottom }: ScrollToBottomProps) => {
|
||||
return (
|
||||
<button
|
||||
<Button
|
||||
size="icon"
|
||||
variant="outline"
|
||||
className={clsx(
|
||||
"absolute right-4 bottom-4 flex items-center justify-center size-8 border bg-white rounded-full shadow dark:border-[#272828] dark:bg-black dark:shadow-white/15",
|
||||
"absolute right-4 bottom-4 border border-border rounded-full shadow dark:shadow-white/15",
|
||||
{
|
||||
hidden: isAtBottom,
|
||||
}
|
||||
@@ -27,7 +27,7 @@ const ScrollToBottom = ({
|
||||
}}
|
||||
>
|
||||
<ArrowDown className="size-5" />
|
||||
</button>
|
||||
</Button>
|
||||
);
|
||||
};
|
||||
|
||||
|
||||
@@ -1,40 +1,38 @@
|
||||
import {
|
||||
Popover,
|
||||
PopoverButton,
|
||||
PopoverPanel,
|
||||
PopoverPanelProps,
|
||||
} from "@headlessui/react";
|
||||
import { FC, ReactNode } from "react";
|
||||
import { useBoolean } from "ahooks";
|
||||
import clsx from "clsx";
|
||||
import { FC, ReactNode } from "react";
|
||||
|
||||
interface Tooltip2Props extends PopoverPanelProps {
|
||||
import {
|
||||
Popover,
|
||||
PopoverTrigger,
|
||||
PopoverContent,
|
||||
} from "@/components/ui/popover";
|
||||
interface Tooltip2Props {
|
||||
content: string;
|
||||
children: ReactNode;
|
||||
className?: string;
|
||||
}
|
||||
|
||||
const Tooltip2: FC<Tooltip2Props> = (props) => {
|
||||
const { content, children, anchor = "top", ...rest } = props;
|
||||
const { content, children, className } = props;
|
||||
const [visible, { setTrue, setFalse }] = useBoolean(false);
|
||||
|
||||
return (
|
||||
<Popover>
|
||||
<PopoverButton onMouseOver={setTrue} onMouseOut={setFalse}>
|
||||
<PopoverTrigger onMouseOver={setTrue} onMouseOut={setFalse}>
|
||||
{children}
|
||||
</PopoverButton>
|
||||
<PopoverPanel
|
||||
{...rest}
|
||||
static
|
||||
anchor={anchor}
|
||||
</PopoverTrigger>
|
||||
<PopoverContent
|
||||
side="top"
|
||||
className={clsx(
|
||||
"fixed z-1000 p-2 rounded-[6px] text-xs text-white bg-black/75 hidden",
|
||||
"z-1000 p-2 rounded-md text-xs text-white bg-black/75 hidden",
|
||||
{
|
||||
"!block": visible,
|
||||
}
|
||||
block: visible,
|
||||
},
|
||||
className
|
||||
)}
|
||||
>
|
||||
{content}
|
||||
</PopoverPanel>
|
||||
</PopoverContent>
|
||||
</Popover>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -71,7 +71,7 @@ export default function Footer({ setIsPinnedWeb }: FooterProps) {
|
||||
<div className="flex items-center gap-2">
|
||||
<img
|
||||
src={selectedExtension.icon}
|
||||
className="size-5 dark:drop-shadow-[0_0_6px_rgb(255,255,255)]"
|
||||
className="h-5 w-5 rounded-full dark:drop-shadow-[0_0_6px_rgb(255,255,255)]"
|
||||
/>
|
||||
<span className="text-sm">{selectedExtension.name}</span>
|
||||
</div>
|
||||
@@ -81,7 +81,7 @@ export default function Footer({ setIsPinnedWeb }: FooterProps) {
|
||||
if (visibleExtensionStore) {
|
||||
return (
|
||||
<div className="flex items-center gap-2">
|
||||
<FontIcon name="font_Store" className="size-5" />
|
||||
<FontIcon name="font_Store" className="h-5 w-5" />
|
||||
<span className="text-sm">Extension Store</span>
|
||||
</div>
|
||||
);
|
||||
@@ -100,7 +100,7 @@ export default function Footer({ setIsPinnedWeb }: FooterProps) {
|
||||
{hasUpdate ? (
|
||||
<div className="cursor-pointer" onClick={() => setVisible(true)}>
|
||||
<span>{t("search.footer.updateAvailable")}</span>
|
||||
<span className="absolute top-0 -right-2 size-1.5 bg-[#FF3434] rounded-full"></span>
|
||||
<span className="absolute top-0 -right-2 h-1.5 w-1.5 bg-[#FF3434] rounded-full"></span>
|
||||
</div>
|
||||
) : (
|
||||
sourceData?.source?.name ||
|
||||
@@ -117,7 +117,7 @@ export default function Footer({ setIsPinnedWeb }: FooterProps) {
|
||||
<div
|
||||
data-tauri-drag-region={isTauri}
|
||||
className={clsx(
|
||||
"px-4 z-999 mx-[1px] h-8 absolute bottom-0 left-0 right-0 border-t border-gray-200 dark:border-gray-700 flex items-center justify-between rounded-[6px] rounded-t-none",
|
||||
"px-4 z-999 mx-px h-8 absolute bottom-0 left-0 right-0 border-t! border-gray-200 dark:border-gray-700 flex items-center justify-between rounded-md rounded-t-none",
|
||||
{
|
||||
"overflow-hidden": isTauri,
|
||||
}
|
||||
@@ -137,7 +137,7 @@ export default function Footer({ setIsPinnedWeb }: FooterProps) {
|
||||
</div>
|
||||
</div>
|
||||
) : (
|
||||
<WebLogin panelClassName="bottom-5 left-0" />
|
||||
<WebLogin side="top" align="start" />
|
||||
)}
|
||||
|
||||
<div className={`flex mobile:hidden items-center gap-3`}>
|
||||
|
||||
@@ -37,7 +37,7 @@ export const NoResults = () => {
|
||||
<div className="flex gap-2">
|
||||
<WebLoginButton />
|
||||
|
||||
<WebRefreshButton className="size-8" />
|
||||
<WebRefreshButton />
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
@@ -54,7 +54,7 @@ export const NoResults = () => {
|
||||
|
||||
<span
|
||||
className={clsx(
|
||||
"ml-3 h-5 min-w-5 rounded-[6px] border border-[#D8D8D8] flex justify-center items-center",
|
||||
"ml-3 h-5 min-w-5 rounded-md border border-[#D8D8D8] flex justify-center items-center",
|
||||
{
|
||||
"px-1": !isMac,
|
||||
}
|
||||
@@ -63,7 +63,7 @@ export const NoResults = () => {
|
||||
{formatKey(modifierKey)}
|
||||
</span>
|
||||
|
||||
<span className="ml-1 w-5 h-5 rounded-[6px] border border-[#D8D8D8] flex justify-center items-center">
|
||||
<span className="ml-1 w-5 h-5 rounded-md border border-[#D8D8D8] flex justify-center items-center">
|
||||
{modeSwitch}
|
||||
</span>
|
||||
</div>
|
||||
|
||||
@@ -1,13 +1,16 @@
|
||||
import { Menu, MenuButton } from "@headlessui/react";
|
||||
import {
|
||||
DropdownMenu,
|
||||
DropdownMenuTrigger,
|
||||
} from "@/components/ui/dropdown-menu";
|
||||
|
||||
import logoImg from "@/assets/icon.svg";
|
||||
|
||||
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="fixed bottom-0 left-0 right-0 bg-white dark:bg-gray-800 border-t border-gray-200 dark:border-white/10">
|
||||
<div className="max-w-6xl mx-auto px-4 h-8 flex items-center justify-between">
|
||||
<Menu as="div" className="relative">
|
||||
<MenuButton className="h-7 flex items-center space-x-2 px-1 py-1.5 rounded-lg hover:bg-gray-100 dark:hover:bg-gray-700 transition-colors">
|
||||
<DropdownMenu>
|
||||
<DropdownMenuTrigger className="h-7 flex items-center space-x-2 px-1 py-1.5 rounded-lg hover:bg-gray-100 dark:hover:bg-gray-700 transition-colors">
|
||||
<img
|
||||
src={logoImg}
|
||||
className="w-5 h-5 text-gray-600 dark:text-gray-400"
|
||||
@@ -16,7 +19,7 @@ const Footer = () => {
|
||||
Coco
|
||||
</span>
|
||||
{/* <ChevronUp className="w-4 h-4 text-gray-500 dark:text-gray-400" /> */}
|
||||
</MenuButton>
|
||||
</DropdownMenuTrigger>
|
||||
|
||||
{/* <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">
|
||||
@@ -27,7 +30,7 @@ const Footer = () => {
|
||||
active
|
||||
? "bg-gray-100 dark:bg-gray-700"
|
||||
: "text-gray-900 dark:text-gray-100"
|
||||
} group flex w-full items-center rounded-[6px] px-3 py-2 text-sm`}
|
||||
} 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>
|
||||
@@ -41,7 +44,7 @@ const Footer = () => {
|
||||
active
|
||||
? "bg-gray-100 dark:bg-gray-700"
|
||||
: "text-gray-900 dark:text-gray-100"
|
||||
} group flex w-full items-center rounded-[6px] px-3 py-2 text-sm`}
|
||||
} group flex w-full items-center rounded-md px-3 py-2 text-sm`}
|
||||
>
|
||||
<User className="w-4 h-4 mr-2" />
|
||||
Profile
|
||||
@@ -55,7 +58,7 @@ const Footer = () => {
|
||||
active
|
||||
? "bg-gray-100 dark:bg-gray-700"
|
||||
: "text-gray-900 dark:text-gray-100"
|
||||
} group flex w-full items-center rounded-[6px] px-3 py-2 text-sm`}
|
||||
} 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>
|
||||
@@ -70,7 +73,7 @@ const Footer = () => {
|
||||
active
|
||||
? "bg-gray-100 dark:bg-gray-700"
|
||||
: "text-gray-900 dark:text-gray-100"
|
||||
} group flex w-full items-center rounded-[6px] px-3 py-2 text-sm`}
|
||||
} group flex w-full items-center rounded-md px-3 py-2 text-sm`}
|
||||
>
|
||||
<LogOut className="w-4 h-4 mr-2" />
|
||||
Sign Out
|
||||
@@ -79,7 +82,7 @@ const Footer = () => {
|
||||
</MenuItem>
|
||||
</div>
|
||||
</MenuItems> */}
|
||||
</Menu>
|
||||
</DropdownMenu>
|
||||
|
||||
<div className="flex items-center space-x-4">
|
||||
<span className="text-xs text-gray-500 dark:text-gray-400">
|
||||
|
||||
@@ -111,7 +111,7 @@ const VisibleKey: FC<VisibleKeyProps> = (props) => {
|
||||
{showTooltip && visibleShortcut ? (
|
||||
<div
|
||||
className={clsx(
|
||||
"size-4 flex items-center justify-center font-normal text-xs text-[#333] leading-[14px] bg-[#ccc] dark:bg-[#6B6B6B] rounded-[6px] shadow-[-6px_0px_6px_2px_#fff] dark:shadow-[-6px_0px_6px_2px_#000] absolute top-1/2 left-1/2 -translate-x-1/2 -translate-y-1/2",
|
||||
"size-4 flex items-center justify-center font-normal text-xs text-[#333] leading-[14px] bg-[#ccc] dark:bg-[#6B6B6B] rounded-md shadow-[-6px_0px_6px_2px_#fff] dark:shadow-[-6px_0px_6px_2px_#000] absolute top-1/2 left-1/2 -translate-x-1/2 -translate-y-1/2",
|
||||
shortcutClassName
|
||||
)}
|
||||
>
|
||||
|
||||
@@ -40,7 +40,7 @@ const AiOverview: FC<AiSummaryProps> = (props) => {
|
||||
)}
|
||||
>
|
||||
<div
|
||||
className="absolute top-2 right-2 flex items-center justify-center size-[20px] border rounded-[6px] cursor-pointer dark:border-[#282828]"
|
||||
className="absolute top-2 right-2 flex items-center justify-center size-[20px] border rounded-md cursor-pointer dark:border-[#282828]"
|
||||
onClick={() => {
|
||||
setVisible(false);
|
||||
}}
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
import { cn } from "@/lib/utils";
|
||||
import { useAppStore } from "@/stores/appStore";
|
||||
import { useWebConfigStore } from "@/stores/webConfigStore";
|
||||
import { useBoolean } from "ahooks";
|
||||
@@ -37,6 +38,7 @@ const AutoResizeTextarea = forwardRef<
|
||||
setInput,
|
||||
handleKeyDown,
|
||||
chatPlaceholder,
|
||||
lineCount,
|
||||
onLineCountChange,
|
||||
firstLineMaxWidth,
|
||||
},
|
||||
@@ -115,7 +117,12 @@ const AutoResizeTextarea = forwardRef<
|
||||
autoComplete="off"
|
||||
autoCapitalize="none"
|
||||
spellCheck="false"
|
||||
className="auto-resize-textarea text-base flex-1 outline-none w-full min-w-[200px] text-[#333] dark:text-[#d8d8d8] placeholder-text-xs placeholder-[#999] dark:placeholder-gray-500 bg-transparent custom-scrollbar resize-none overflow-y-auto"
|
||||
className={cn(
|
||||
"auto-resize-textarea text-base flex-1 outline-none w-full min-w-[200px] text-[#333] dark:text-[#d8d8d8] placeholder-text-xs placeholder-[#999] dark:placeholder-gray-500 bg-transparent custom-scrollbar resize-none overflow-y-auto",
|
||||
{
|
||||
"overflow-y-hidden": lineCount === 1,
|
||||
}
|
||||
)}
|
||||
placeholder={chatPlaceholder || t("search.textarea.placeholder")}
|
||||
aria-label={t("search.textarea.ariaLabel")}
|
||||
value={input}
|
||||
|
||||
@@ -12,7 +12,6 @@ import {
|
||||
} from "lucide-react";
|
||||
import { cloneElement, useEffect, useRef, useState } from "react";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import { Input } from "@headlessui/react";
|
||||
|
||||
import { useOSKeyPress } from "@/hooks/useOSKeyPress";
|
||||
import { useSearchStore } from "@/stores/searchStore";
|
||||
@@ -292,9 +291,9 @@ const ContextMenu = ({ formatUrl }: ContextMenuProps) => {
|
||||
ref={containerRef}
|
||||
id={visibleContextMenu ? CONTEXT_MENU_PANEL_ID : ""}
|
||||
className={clsx(
|
||||
"absolute bottom-[50px] right-[18px] w-[300px] flex flex-col gap-2 scale-0 transition origin-bottom-right text-sm p-3 pb-0 bg-white dark:bg-black rounded-lg shadow-xs border border-[#EDEDED] dark:border-[#272828] shadow-lg dark:shadow-white/15",
|
||||
"absolute bottom-[50px] right-[18px] w-[300px] flex flex-col gap-2 scale-0 transition origin-bottom-right text-sm p-3 pb-0 bg-white dark:bg-black rounded-lg border border-[#EDEDED] dark:border-[#272828] shadow-lg dark:shadow-white/15",
|
||||
{
|
||||
"!scale-100": visibleContextMenu,
|
||||
"scale-100": visibleContextMenu,
|
||||
}
|
||||
)}
|
||||
>
|
||||
@@ -329,12 +328,12 @@ const ContextMenu = ({ formatUrl }: ContextMenuProps) => {
|
||||
<span style={{ color }}>{name}</span>
|
||||
</div>
|
||||
|
||||
<div className="flex gap-[4px] text-black/60 dark:text-white/60">
|
||||
<div className="flex gap-1 text-black/60 dark:text-white/60">
|
||||
{keys.map((key) => (
|
||||
<kbd
|
||||
key={key}
|
||||
className={clsx(
|
||||
"flex justify-center items-center font-sans h-[20px] min-w-[20px] text-[10px] rounded-[6px] border border-[#EDEDED] dark:border-white/10 bg-white dark:bg-[#202126]",
|
||||
"flex justify-center items-center font-sans h-5 min-w-5 text-[10px] rounded-md border border-[#EDEDED] dark:border-white/10 bg-white dark:bg-[#202126]",
|
||||
{
|
||||
"px-1": key.length > 1,
|
||||
}
|
||||
@@ -363,7 +362,7 @@ const ContextMenu = ({ formatUrl }: ContextMenuProps) => {
|
||||
searchInputRef.current?.focus();
|
||||
}}
|
||||
>
|
||||
<Input
|
||||
<input
|
||||
ref={searchInputRef}
|
||||
autoFocus
|
||||
autoCorrect="off"
|
||||
|
||||
@@ -46,8 +46,8 @@ const DropdownListItem = memo(
|
||||
aria-selected={isSelected}
|
||||
id={`search-item-${currentIndex}`}
|
||||
className={clsx("p-2 transition rounded-lg", {
|
||||
"bg-[#EDEDED] dark:bg-[#202126]": isSelected,
|
||||
"!p-0": isAiOverview,
|
||||
"bg-muted": isSelected,
|
||||
"p-0!": isAiOverview,
|
||||
})}
|
||||
>
|
||||
{isCalculator && <Calculator item={item} isSelected={isSelected} />}
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { Button } from "@headlessui/react";
|
||||
import { Button } from "@/components/ui/button";
|
||||
import dayjs from "dayjs";
|
||||
import {
|
||||
CircleCheck,
|
||||
|
||||
@@ -252,7 +252,7 @@ export default function ChatInput({
|
||||
replace: [akiAiTooltipPrefix, askAI.name],
|
||||
})}
|
||||
</span>
|
||||
<div className="flex items-center justify-center px-1 h-[20px] text-xs rounded-[6px] border border-black/10 dark:border-[#545454]">
|
||||
<div className="flex items-center justify-center px-1 h-5 text-xs rounded-md border border-black/10 dark:border-[#545454]">
|
||||
{formatKey(modifierKey)} + {formatKey("Enter")}
|
||||
</div>
|
||||
</div>
|
||||
@@ -276,8 +276,8 @@ export default function ChatInput({
|
||||
return (
|
||||
<VisibleKey
|
||||
shortcut={returnToInput}
|
||||
rootClassName="flex-1 flex items-center justify-center"
|
||||
shortcutClassName="!left-0 !translate-x-0"
|
||||
rootClassName="flex-1 flex items-center justify-center w-full"
|
||||
shortcutClassName="!left-auto !right-2 !translate-x-0"
|
||||
>
|
||||
<AutoResizeTextarea
|
||||
ref={textareaRef}
|
||||
@@ -308,14 +308,14 @@ export default function ChatInput({
|
||||
<div className={`w-full relative`}>
|
||||
<div
|
||||
ref={containerRef}
|
||||
className={`flex items-center dark:text-[#D8D8D8] rounded-[6px] transition-all relative overflow-hidden`}
|
||||
className={`flex items-center dark:text-[#D8D8D8] rounded-md transition-all relative overflow-hidden`}
|
||||
>
|
||||
{lineCount === 1 && renderSearchIcon()}
|
||||
|
||||
{visibleSearchBar() && (
|
||||
<div
|
||||
className={clsx(
|
||||
"relative w-full p-2 bg-[#ededed] dark:bg-[#202126]",
|
||||
"min-h-10 w-full p-[7px] bg-[#ededed] dark:bg-[#202126]",
|
||||
{
|
||||
"flex items-center gap-2": lineCount === 1,
|
||||
}
|
||||
|
||||
@@ -187,9 +187,9 @@ const InputControls = ({
|
||||
{source?.type === "deep_think" && source?.config?.visible && (
|
||||
<button
|
||||
className={clsx(
|
||||
"flex items-center justify-center gap-1 h-[20px] px-1 rounded-[6px] transition hover:bg-[#EDEDED] dark:hover:bg-[#202126]",
|
||||
"flex items-center justify-center gap-1 h-5 px-1 rounded-md transition hover:bg-[#EDEDED] dark:hover:bg-[#202126] cursor-pointer",
|
||||
{
|
||||
"!bg-[rgba(0,114,255,0.3)]": isDeepThinkActive,
|
||||
"bg-[rgba(0,114,255,0.3)]!": isDeepThinkActive,
|
||||
}
|
||||
)}
|
||||
onClick={setIsDeepThinkActive}
|
||||
@@ -250,7 +250,7 @@ const InputControls = ({
|
||||
!visibleExtensionStore && (
|
||||
<div
|
||||
className={clsx(
|
||||
"inline-flex items-center gap-1 h-[20px] px-1 rounded-full hover:!text-[#881c94] cursor-pointer transition",
|
||||
"inline-flex items-center gap-1 h-5 px-1 rounded-full hover:text-[#881c94]! cursor-pointer transition",
|
||||
[
|
||||
enabledAiOverview
|
||||
? "text-[#881c94]"
|
||||
|
||||
@@ -2,14 +2,16 @@ import { FC, Fragment, MouseEvent, useEffect, useRef } from "react";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import { ChevronRight, Plus } from "lucide-react";
|
||||
import {
|
||||
Menu,
|
||||
MenuButton,
|
||||
MenuItem,
|
||||
MenuItems,
|
||||
DropdownMenu,
|
||||
DropdownMenuTrigger,
|
||||
DropdownMenuContent,
|
||||
DropdownMenuItem,
|
||||
} from "@/components/ui/dropdown-menu";
|
||||
import {
|
||||
Popover,
|
||||
PopoverButton,
|
||||
PopoverPanel,
|
||||
} from "@headlessui/react";
|
||||
PopoverTrigger,
|
||||
PopoverContent,
|
||||
} from "@/components/ui/popover";
|
||||
import { castArray, find, isNil } from "lodash-es";
|
||||
import { nanoid } from "nanoid";
|
||||
import { useCreation, useMount, useReactive } from "ahooks";
|
||||
@@ -198,8 +200,8 @@ const InputUpload: FC<InputUploadProps> = (props) => {
|
||||
]);
|
||||
|
||||
return (
|
||||
<Menu>
|
||||
<MenuButton className="flex items-center justify-center h-[20px] px-1 rounded-[6px] transition hover:bg-[#EDEDED] dark:hover:bg-[#202126]">
|
||||
<DropdownMenu>
|
||||
<DropdownMenuTrigger className="flex items-center justify-center h-[20px] px-1 rounded-md transition hover:bg-[#EDEDED] dark:hover:bg-[#202126]">
|
||||
<Tooltip
|
||||
content={t("search.input.uploadFileHints.tooltip", {
|
||||
replace: [
|
||||
@@ -212,32 +214,41 @@ const InputUpload: FC<InputUploadProps> = (props) => {
|
||||
<Plus className="size-3 scale-[1.3]" />
|
||||
</VisibleKey>
|
||||
</Tooltip>
|
||||
</MenuButton>
|
||||
</DropdownMenuTrigger>
|
||||
|
||||
<MenuItems
|
||||
anchor="bottom start"
|
||||
className="p-1 text-sm bg-white dark:bg-[#202126] rounded-lg shadow-xs border border-gray-200 dark:border-gray-700"
|
||||
<DropdownMenuContent
|
||||
side="bottom"
|
||||
align="start"
|
||||
className="p-1 text-sm rounded-lg"
|
||||
>
|
||||
{menuItems.map((item) => {
|
||||
const { label, children, clickEvent } = item;
|
||||
|
||||
return (
|
||||
<MenuItem key={label}>
|
||||
<DropdownMenuItem
|
||||
key={label}
|
||||
onSelect={(e: Event) => {
|
||||
if (children) e.preventDefault();
|
||||
}}
|
||||
className="px-0 py-0"
|
||||
>
|
||||
{children ? (
|
||||
<Popover>
|
||||
<PopoverButton
|
||||
className="flex items-center justify-between gap-2 px-3 py-2 hover:bg-black/5 hover:dark:bg-white/5 rounded-lg cursor-pointer"
|
||||
onClick={clickEvent}
|
||||
>
|
||||
<span>{label}</span>
|
||||
<PopoverTrigger asChild>
|
||||
<div
|
||||
className="flex items-center justify-between gap-2 px-3 py-2 rounded-lg hover:bg-muted cursor-pointer"
|
||||
onClick={clickEvent}
|
||||
>
|
||||
<span>{label}</span>
|
||||
|
||||
<ChevronRight className="size-4" />
|
||||
</PopoverButton>
|
||||
<ChevronRight className="size-4" />
|
||||
</div>
|
||||
</PopoverTrigger>
|
||||
|
||||
<PopoverPanel
|
||||
transition
|
||||
anchor="right"
|
||||
className="p-1 text-sm bg-white dark:bg-[#202126] rounded-lg shadow-xs border border-gray-200 dark:border-gray-700"
|
||||
<PopoverContent
|
||||
side="right"
|
||||
align="start"
|
||||
className="p-1 text-sm rounded-lg"
|
||||
>
|
||||
{children.map((childItem) => {
|
||||
const { groupName, groupItems } = childItem;
|
||||
@@ -259,7 +270,7 @@ const InputUpload: FC<InputUploadProps> = (props) => {
|
||||
return (
|
||||
<div
|
||||
key={id}
|
||||
className="px-3 py-2 hover:bg-black/5 hover:dark:bg-white/5 rounded-lg cursor-pointer"
|
||||
className="px-3 py-2 rounded-lg hover:bg-muted cursor-pointer"
|
||||
onClick={clickEvent}
|
||||
>
|
||||
{label}
|
||||
@@ -269,21 +280,21 @@ const InputUpload: FC<InputUploadProps> = (props) => {
|
||||
</Fragment>
|
||||
);
|
||||
})}
|
||||
</PopoverPanel>
|
||||
</PopoverContent>
|
||||
</Popover>
|
||||
) : (
|
||||
<div
|
||||
className="px-3 py-2 hover:bg-black/5 hover:dark:bg-white/5 rounded-lg cursor-pointer"
|
||||
className="px-3 py-2 rounded-lg hover:bg-muted cursor-pointer"
|
||||
onClick={clickEvent}
|
||||
>
|
||||
{label}
|
||||
</div>
|
||||
)}
|
||||
</MenuItem>
|
||||
</DropdownMenuItem>
|
||||
);
|
||||
})}
|
||||
</MenuItems>
|
||||
</Menu>
|
||||
</DropdownMenuContent>
|
||||
</DropdownMenu>
|
||||
);
|
||||
};
|
||||
|
||||
|
||||
@@ -1,10 +1,14 @@
|
||||
import { useState, useEffect, useCallback, useRef } from "react";
|
||||
import { Popover, PopoverButton, PopoverPanel } from "@headlessui/react";
|
||||
import { ChevronDownIcon, RefreshCw, Layers, Hammer } from "lucide-react";
|
||||
import clsx from "clsx";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import { useDebounce } from "ahooks";
|
||||
|
||||
import {
|
||||
Popover,
|
||||
PopoverTrigger,
|
||||
PopoverContent,
|
||||
} from "@/components/ui/popover";
|
||||
import CommonIcon from "@/components/Common/Icons/CommonIcon";
|
||||
import { useConnectStore } from "@/stores/connectStore";
|
||||
import { useSearchStore } from "@/stores/searchStore";
|
||||
@@ -16,6 +20,7 @@ import NoDataImage from "@/components/Common/NoDataImage";
|
||||
import PopoverInput from "@/components/Common/PopoverInput";
|
||||
import Pagination from "@/components/Common/Pagination";
|
||||
import { SearchQuery } from "@/utils";
|
||||
import { Button } from "../ui/button";
|
||||
|
||||
interface MCPPopoverProps {
|
||||
mcp_servers: any;
|
||||
@@ -79,6 +84,7 @@ export default function MCPPopover({
|
||||
}, [currentService?.id, debouncedKeyword, getMCPByServer]);
|
||||
|
||||
const popoverButtonRef = useRef<HTMLButtonElement>(null);
|
||||
const [open, setOpen] = useState(false);
|
||||
const mcpSearch = useShortcutsStore((state) => state.mcpSearch);
|
||||
const mcpSearchScope = useShortcutsStore((state) => {
|
||||
return state.mcpSearchScope;
|
||||
@@ -166,9 +172,9 @@ export default function MCPPopover({
|
||||
return (
|
||||
<div
|
||||
className={clsx(
|
||||
"flex justify-center items-center gap-1 h-[20px] px-1 rounded-[6px] transition hover:bg-[#EDEDED] dark:hover:bg-[#202126] cursor-pointer",
|
||||
"flex justify-center items-center gap-1 h-5 px-1 rounded-md transition cursor-pointer hover:bg-[#EDEDED] dark:hover:bg-[#202126]",
|
||||
{
|
||||
"!bg-[rgba(0,114,255,0.3)]": isMCPActive,
|
||||
"bg-[rgba(0,114,255,0.3)]!": isMCPActive,
|
||||
}
|
||||
)}
|
||||
onClick={setIsMCPActive}
|
||||
@@ -191,8 +197,14 @@ export default function MCPPopover({
|
||||
{t("search.input.MCP")}
|
||||
</span>
|
||||
|
||||
<Popover className="relative">
|
||||
<PopoverButton ref={popoverButtonRef} className="flex items-center">
|
||||
<Popover open={open} onOpenChange={setOpen}>
|
||||
<PopoverTrigger
|
||||
ref={popoverButtonRef}
|
||||
className="flex items-center"
|
||||
onClick={(e) => {
|
||||
e.stopPropagation();
|
||||
}}
|
||||
>
|
||||
<VisibleKey
|
||||
shortcut={mcpSearchScope}
|
||||
onKeyPress={() => {
|
||||
@@ -200,29 +212,35 @@ export default function MCPPopover({
|
||||
}}
|
||||
>
|
||||
<ChevronDownIcon
|
||||
className={clsx("size-3", [
|
||||
className={clsx("size-3 cursor-pointer", [
|
||||
isMCPActive
|
||||
? "text-[#0072FF] dark:text-[#0072FF]"
|
||||
: "text-[#333] dark:text-white",
|
||||
])}
|
||||
/>
|
||||
</VisibleKey>
|
||||
</PopoverButton>
|
||||
</PopoverTrigger>
|
||||
|
||||
<PopoverPanel className="absolute z-50 left-0 bottom-6 w-[240px] overflow-y-auto bg-white dark:bg-[#202126] rounded-lg shadow-lg border border-gray-200 dark:border-gray-700">
|
||||
<PopoverContent
|
||||
side="top"
|
||||
align="start"
|
||||
className="z-50 w-60 overflow-y-auto rounded-lg shadow-lg p-0"
|
||||
>
|
||||
<div
|
||||
className="text-sm"
|
||||
onClick={(e) => {
|
||||
onClick={(e: React.MouseEvent<HTMLDivElement>) => {
|
||||
e.stopPropagation();
|
||||
}}
|
||||
>
|
||||
<div className="p-3">
|
||||
<div className="p-2">
|
||||
<div className="flex justify-between">
|
||||
<span>{t("search.input.searchPopover.title")}</span>
|
||||
|
||||
<div
|
||||
<Button
|
||||
variant="outline"
|
||||
size="icon"
|
||||
className="size-6"
|
||||
onClick={handleRefresh}
|
||||
className="size-[24px] flex justify-center items-center rounded-lg border border-black/10 dark:border-white/10 cursor-pointer"
|
||||
>
|
||||
<VisibleKey shortcut="R" onKeyPress={handleRefresh}>
|
||||
<RefreshCw
|
||||
@@ -231,7 +249,7 @@ export default function MCPPopover({
|
||||
}`}
|
||||
/>
|
||||
</VisibleKey>
|
||||
</div>
|
||||
</Button>
|
||||
</div>
|
||||
|
||||
<div className="relative h-8 my-2">
|
||||
@@ -250,7 +268,7 @@ export default function MCPPopover({
|
||||
value={keyword}
|
||||
ref={searchInputRef}
|
||||
className="size-full px-2 rounded-lg border dark:border-white/10 bg-transparent"
|
||||
onChange={(e) => {
|
||||
onChange={(e: React.ChangeEvent<HTMLInputElement>) => {
|
||||
setKeyword(e.target.value);
|
||||
}}
|
||||
/>
|
||||
@@ -280,7 +298,7 @@ export default function MCPPopover({
|
||||
>
|
||||
<div className="flex items-center gap-2 overflow-hidden">
|
||||
{isAll ? (
|
||||
<Layers className="size-[16px] text-[#0287FF]" />
|
||||
<Layers className="min-w-4 min-h-4 size-4 text-[#0287FF]" />
|
||||
) : (
|
||||
<CommonIcon
|
||||
item={item}
|
||||
@@ -290,7 +308,7 @@ export default function MCPPopover({
|
||||
"default_icon",
|
||||
]}
|
||||
itemIcon={item.icon}
|
||||
className="size-4"
|
||||
className="min-w-4 min-h-4 size-4"
|
||||
/>
|
||||
)}
|
||||
|
||||
@@ -308,7 +326,7 @@ export default function MCPPopover({
|
||||
}}
|
||||
/>
|
||||
|
||||
<div className="flex justify-center items-center size-[24px]">
|
||||
<div className="flex justify-center items-center size-6">
|
||||
<Checkbox
|
||||
checked={isChecked()}
|
||||
indeterminate={isAll}
|
||||
@@ -339,7 +357,7 @@ export default function MCPPopover({
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
</PopoverPanel>
|
||||
</PopoverContent>
|
||||
</Popover>
|
||||
</>
|
||||
)}
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import { useEffect, memo, useRef, useCallback, useState } from "react";
|
||||
import clsx from "clsx";
|
||||
|
||||
import DropdownList from "./DropdownList";
|
||||
import { SearchResults } from "@/components/Search/SearchResults";
|
||||
@@ -12,7 +13,6 @@ import ExtensionStore from "./ExtensionStore";
|
||||
import platformAdapter from "@/utils/platformAdapter";
|
||||
import ViewExtension from "./ViewExtension";
|
||||
import { visibleFooterBar } from "@/utils";
|
||||
import clsx from "clsx";
|
||||
|
||||
const SearchResultsPanel = memo<{
|
||||
input: string;
|
||||
|
||||
@@ -1,15 +1,16 @@
|
||||
import { useSearchStore } from "@/stores/searchStore";
|
||||
import { ChevronLeft, Search } from "lucide-react";
|
||||
import { FC } from "react";
|
||||
import clsx from "clsx";
|
||||
|
||||
import FontIcon from "@/components/Common/Icons/FontIcon";
|
||||
import { FC } from "react";
|
||||
import lightDefaultIcon from "@/assets/images/source_default.png";
|
||||
import darkDefaultIcon from "@/assets/images/source_default_dark.png";
|
||||
import { useThemeStore } from "@/stores/themeStore";
|
||||
import platformAdapter from "@/utils/platformAdapter";
|
||||
import { navigateBack, visibleSearchBar } from "@/utils";
|
||||
import VisibleKey from "../Common/VisibleKey";
|
||||
import clsx from "clsx";
|
||||
import { cn } from "@/lib/utils";
|
||||
|
||||
interface MultilevelWrapperProps {
|
||||
title?: string;
|
||||
@@ -36,7 +37,7 @@ const MultilevelWrapper: FC<MultilevelWrapperProps> = (props) => {
|
||||
<div
|
||||
data-tauri-drag-region
|
||||
className={clsx(
|
||||
"flex items-center h-10 gap-1 px-2 border border-[#EDEDED] dark:border-[#202126] rounded-l-lg",
|
||||
"flex items-center h-10 gap-1 px-2 border border-(--border) rounded-l-lg",
|
||||
{
|
||||
"justify-center": visibleSearchBar(),
|
||||
"w-[calc(100vw-16px)] rounded-r-lg": !visibleSearchBar(),
|
||||
@@ -50,7 +51,7 @@ const MultilevelWrapper: FC<MultilevelWrapperProps> = (props) => {
|
||||
/>
|
||||
</VisibleKey>
|
||||
|
||||
<div className="size-5 [&>*]:size-full">{renderIcon()}</div>
|
||||
<div className="size-5 *:size-full">{renderIcon()}</div>
|
||||
|
||||
<span className="text-sm whitespace-nowrap">{title}</span>
|
||||
</div>
|
||||
@@ -115,7 +116,14 @@ export default function SearchIcons({
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="flex items-center justify-center pl-2 h-10 bg-[#ededed] dark:bg-[#202126]">
|
||||
<div
|
||||
className={cn(
|
||||
"flex items-center justify-center bg-[#ededed] dark:bg-[#202126]",
|
||||
{
|
||||
"pl-2 h-10": lineCount === 1,
|
||||
}
|
||||
)}
|
||||
>
|
||||
<Search className="w-4 h-4 text-[#ccc] dark:text-[#d8d8d8]" />
|
||||
</div>
|
||||
);
|
||||
|
||||
@@ -1,10 +1,14 @@
|
||||
import { useState, useEffect, useCallback, useRef } from "react";
|
||||
import { Popover, PopoverButton, PopoverPanel } from "@headlessui/react";
|
||||
import { ChevronDownIcon, RefreshCw, Layers, Globe } from "lucide-react";
|
||||
import clsx from "clsx";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import { useDebounce } from "ahooks";
|
||||
|
||||
import {
|
||||
Popover,
|
||||
PopoverTrigger,
|
||||
PopoverContent,
|
||||
} from "@/components/ui/popover";
|
||||
import CommonIcon from "@/components/Common/Icons/CommonIcon";
|
||||
import { useConnectStore } from "@/stores/connectStore";
|
||||
import { useSearchStore } from "@/stores/searchStore";
|
||||
@@ -15,6 +19,7 @@ import VisibleKey from "@/components/Common/VisibleKey";
|
||||
import NoDataImage from "@/components/Common/NoDataImage";
|
||||
import PopoverInput from "@/components/Common/PopoverInput";
|
||||
import Pagination from "@/components/Common/Pagination";
|
||||
import { Button } from "../ui/button";
|
||||
|
||||
interface SearchPopoverProps {
|
||||
datasource: any;
|
||||
@@ -85,6 +90,7 @@ export default function SearchPopover({
|
||||
}, [currentService?.id, debouncedKeyword, getDataSourcesByServer]);
|
||||
|
||||
const popoverButtonRef = useRef<HTMLButtonElement>(null);
|
||||
const [open, setOpen] = useState(false);
|
||||
const internetSearch = useShortcutsStore((state) => state.internetSearch);
|
||||
const internetSearchScope = useShortcutsStore((state) => {
|
||||
return state.internetSearchScope;
|
||||
@@ -172,9 +178,9 @@ export default function SearchPopover({
|
||||
return (
|
||||
<div
|
||||
className={clsx(
|
||||
"flex justify-center items-center gap-1 h-[20px] px-1 rounded-[6px] transition hover:bg-[#EDEDED] dark:hover:bg-[#202126] cursor-pointer",
|
||||
"flex justify-center items-center gap-1 h-5 px-1 rounded-md transition cursor-pointer hover:bg-[#EDEDED] dark:hover:bg-[#202126]",
|
||||
{
|
||||
"!bg-[rgba(0,114,255,0.3)]": isSearchActive,
|
||||
"bg-[rgba(0,114,255,0.3)]!": isSearchActive,
|
||||
}
|
||||
)}
|
||||
onClick={setIsSearchActive}
|
||||
@@ -199,8 +205,14 @@ export default function SearchPopover({
|
||||
{t("search.input.search")}
|
||||
</span>
|
||||
|
||||
<Popover className="relative">
|
||||
<PopoverButton ref={popoverButtonRef} className="flex items-center">
|
||||
<Popover open={open} onOpenChange={setOpen}>
|
||||
<PopoverTrigger
|
||||
ref={popoverButtonRef}
|
||||
className="flex items-center"
|
||||
onClick={(e) => {
|
||||
e.stopPropagation();
|
||||
}}
|
||||
>
|
||||
<VisibleKey
|
||||
shortcut={internetSearchScope}
|
||||
onKeyPress={() => {
|
||||
@@ -208,29 +220,35 @@ export default function SearchPopover({
|
||||
}}
|
||||
>
|
||||
<ChevronDownIcon
|
||||
className={clsx("size-3", [
|
||||
className={clsx("size-3 cursor-pointer", [
|
||||
isSearchActive
|
||||
? "text-[#0072FF] dark:text-[#0072FF]"
|
||||
: "text-[#333] dark:text-white",
|
||||
])}
|
||||
/>
|
||||
</VisibleKey>
|
||||
</PopoverButton>
|
||||
</PopoverTrigger>
|
||||
|
||||
<PopoverPanel className="absolute z-50 left-0 bottom-6 w-[240px] overflow-y-auto bg-white dark:bg-[#202126] rounded-lg shadow-lg border border-gray-200 dark:border-gray-700">
|
||||
<PopoverContent
|
||||
side="top"
|
||||
align="start"
|
||||
className="z-50 w-60 overflow-y-auto rounded-lg shadow-lg p-0"
|
||||
>
|
||||
<div
|
||||
className="text-sm"
|
||||
onClick={(e) => {
|
||||
onClick={(e: React.MouseEvent<HTMLDivElement>) => {
|
||||
e.stopPropagation();
|
||||
}}
|
||||
>
|
||||
<div className="p-3">
|
||||
<div className="p-2">
|
||||
<div className="flex justify-between">
|
||||
<span>{t("search.input.searchPopover.title")}</span>
|
||||
|
||||
<div
|
||||
<Button
|
||||
variant="outline"
|
||||
size="icon"
|
||||
className="size-6"
|
||||
onClick={handleRefresh}
|
||||
className="size-[24px] flex justify-center items-center rounded-lg border border-black/10 dark:border-white/10 cursor-pointer"
|
||||
>
|
||||
<VisibleKey shortcut="R" onKeyPress={handleRefresh}>
|
||||
<RefreshCw
|
||||
@@ -239,7 +257,7 @@ export default function SearchPopover({
|
||||
}`}
|
||||
/>
|
||||
</VisibleKey>
|
||||
</div>
|
||||
</Button>
|
||||
</div>
|
||||
|
||||
<div className="relative h-8 my-2">
|
||||
@@ -258,7 +276,7 @@ export default function SearchPopover({
|
||||
value={keyword}
|
||||
ref={searchInputRef}
|
||||
className="size-full px-2 rounded-lg border dark:border-white/10 bg-transparent"
|
||||
onChange={(e) => {
|
||||
onChange={(e: React.ChangeEvent<HTMLInputElement>) => {
|
||||
setKeyword(e.target.value);
|
||||
}}
|
||||
/>
|
||||
@@ -288,7 +306,7 @@ export default function SearchPopover({
|
||||
>
|
||||
<div className="flex items-center gap-2 overflow-hidden">
|
||||
{isAll ? (
|
||||
<Layers className="size-[16px] text-[#0287FF]" />
|
||||
<Layers className="size-4 text-[#0287FF]" />
|
||||
) : (
|
||||
<CommonIcon
|
||||
item={item}
|
||||
@@ -316,7 +334,7 @@ export default function SearchPopover({
|
||||
}}
|
||||
/>
|
||||
|
||||
<div className="flex justify-center items-center size-[24px]">
|
||||
<div className="flex justify-center items-center size-6">
|
||||
<Checkbox
|
||||
checked={isChecked()}
|
||||
indeterminate={isAll}
|
||||
@@ -347,7 +365,7 @@ export default function SearchPopover({
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
</PopoverPanel>
|
||||
</PopoverContent>
|
||||
</Popover>
|
||||
</>
|
||||
)}
|
||||
|
||||
@@ -35,7 +35,10 @@ import {
|
||||
visibleSearchBar,
|
||||
} from "@/utils";
|
||||
import { useTauriFocus } from "@/hooks/useTauriFocus";
|
||||
import { POPOVER_PANEL_SELECTOR, WINDOW_CENTER_BASELINE_HEIGHT } from "@/constants";
|
||||
import {
|
||||
POPOVER_PANEL_SELECTOR,
|
||||
WINDOW_CENTER_BASELINE_HEIGHT,
|
||||
} from "@/constants";
|
||||
import { useChatStore } from "@/stores/chatStore";
|
||||
import { useSearchStore } from "@/stores/searchStore";
|
||||
|
||||
@@ -383,11 +386,11 @@ function SearchChat({
|
||||
<div
|
||||
data-tauri-drag-region={isTauri}
|
||||
className={clsx(
|
||||
"m-auto overflow-hidden relative bg-no-repeat bg-white dark:bg-black flex flex-col",
|
||||
"m-auto overflow-hidden relative bg-no-repeat flex flex-col",
|
||||
[
|
||||
isTransitioned
|
||||
? "bg-bottom bg-chat_bg_light dark:bg-chat_bg_dark"
|
||||
: "bg-top bg-search_bg_light dark:bg-search_bg_dark",
|
||||
? "bg-bottom bg-[url('/assets/chat_bg_light.png')] dark:bg-[url('/assets/chat_bg_dark.png')]"
|
||||
: "bg-top bg-[url('/assets/search_bg_light.png')] dark:bg-[url('/assets/search_bg_dark.png')]",
|
||||
],
|
||||
{
|
||||
"size-full": !isTauri,
|
||||
@@ -438,7 +441,7 @@ function SearchChat({
|
||||
{!hideMiddleBorder && (
|
||||
<div
|
||||
className={clsx(
|
||||
"pointer-events-none absolute left-0 right-0 h-[1px] bg-[#E6E6E6] dark:bg-[#272626]",
|
||||
"pointer-events-none absolute left-0 right-0 h-px bg-[#E6E6E6] dark:bg-[#272626]",
|
||||
isTransitioned ? "top-0" : "bottom-0"
|
||||
)}
|
||||
/>
|
||||
|
||||
@@ -6,6 +6,13 @@ import { nanoid } from "nanoid";
|
||||
|
||||
import { AssistantFetcher } from "@/components/Assistant/AssistantFetcher";
|
||||
import { Button } from "@/components/ui/button";
|
||||
import {
|
||||
Select,
|
||||
SelectTrigger,
|
||||
SelectContent,
|
||||
SelectItem,
|
||||
SelectValue,
|
||||
} from "@/components/ui/select";
|
||||
import { ButtonConfig } from "./config";
|
||||
import { useThemeStore } from "@/stores/themeStore";
|
||||
import { useAppStore } from "@/stores/appStore";
|
||||
@@ -169,43 +176,58 @@ export default function AddChatDialog({
|
||||
<label className="text-sm font-medium text-muted-foreground">
|
||||
{t("selection.bind.service")}
|
||||
</label>
|
||||
<select
|
||||
className="h-8 rounded-md px-2 py-1 text-sm bg-white dark:bg-[#0B1220] w-full"
|
||||
<Select
|
||||
value={serverId}
|
||||
onChange={(e) => setServerId(e.target.value)}
|
||||
onValueChange={(v) => setServerId(v === "__default__" ? "" : v)}
|
||||
>
|
||||
<option value="" disabled>
|
||||
{t("selection.bind.defaultService")}
|
||||
</option>
|
||||
{serverList.map((s: any) => (
|
||||
<option key={s.id} value={s.id}>
|
||||
{s.name || s.endpoint || s.id}
|
||||
</option>
|
||||
))}
|
||||
</select>
|
||||
<SelectTrigger className="h-8 w-full">
|
||||
<SelectValue className="truncate" placeholder={t("selection.bind.defaultService") as string} />
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
<SelectItem value="__default__" disabled>
|
||||
{t("selection.bind.defaultService")}
|
||||
</SelectItem>
|
||||
{serverList.map((s: any) => (
|
||||
<SelectItem key={s.id} value={s.id}>
|
||||
{s.name || s.endpoint || s.id}
|
||||
</SelectItem>
|
||||
))}
|
||||
</SelectContent>
|
||||
</Select>
|
||||
</div>
|
||||
<div className="space-y-1.5">
|
||||
<label className="text-sm font-medium text-muted-foreground">
|
||||
{t("selection.bind.assistant")}
|
||||
</label>
|
||||
<select
|
||||
className="h-8 rounded-md px-2 py-1 text-sm bg-white dark:bg-[#0B1220] w-full"
|
||||
<Select
|
||||
value={assistantId}
|
||||
onChange={(e) => setAssistantId(e.target.value)}
|
||||
onValueChange={(v) => setAssistantId(v === "__default__" ? "" : v)}
|
||||
disabled={loading || !serverId}
|
||||
>
|
||||
<option value="" disabled>
|
||||
{loading
|
||||
? t("common.loading")
|
||||
: t("selection.bind.defaultAssistant")}
|
||||
</option>
|
||||
{!loading &&
|
||||
assistantList.map((a: any) => (
|
||||
<option key={a._id} value={a._id}>
|
||||
{a._source?.name || a._id}
|
||||
</option>
|
||||
))}
|
||||
</select>
|
||||
<SelectTrigger className="h-8 w-full">
|
||||
<SelectValue
|
||||
className="truncate"
|
||||
placeholder={
|
||||
(loading
|
||||
? t("common.loading")
|
||||
: t("selection.bind.defaultAssistant")) as string
|
||||
}
|
||||
/>
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
{!loading && (
|
||||
<SelectItem value="__default__">
|
||||
{t("selection.bind.defaultAssistant")}
|
||||
</SelectItem>
|
||||
)}
|
||||
{!loading &&
|
||||
assistantList.map((a: any) => (
|
||||
<SelectItem key={a._id} value={a._id}>
|
||||
{a._source?.name || a._id}
|
||||
</SelectItem>
|
||||
))}
|
||||
</SelectContent>
|
||||
</Select>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -7,6 +7,13 @@ import { AssistantFetcher } from "@/components/Assistant/AssistantFetcher";
|
||||
import { setCurrentWindowService } from "@/commands/windowService";
|
||||
import { AddChatButton } from "./AddChatButton";
|
||||
import { ButtonConfig, resolveLucideIcon } from "./config";
|
||||
import {
|
||||
Select,
|
||||
SelectTrigger,
|
||||
SelectContent,
|
||||
SelectItem,
|
||||
SelectValue,
|
||||
} from "@/components/ui/select";
|
||||
|
||||
const ASSISTANT_CACHE_KEY = "assistant_list_cache";
|
||||
|
||||
@@ -250,50 +257,58 @@ const ButtonsList = ({ buttons, setButtons, serverList }: ButtonsListProps) => {
|
||||
<div className="ml-auto flex items-center gap-2">
|
||||
{isChat && (
|
||||
<>
|
||||
<select
|
||||
className="rounded-md px-2 py-1 text-sm bg-white dark:bg-[#0B1220] w-44"
|
||||
<Select
|
||||
value={btn.action.assistantServerId || ""}
|
||||
onChange={(e) => handleServerSelect(btn, e.target.value)}
|
||||
title={t("selection.bind.service")}
|
||||
onValueChange={(v) =>
|
||||
handleServerSelect(btn, v === "__default__" ? "" : v)
|
||||
}
|
||||
>
|
||||
<option value="">
|
||||
{t("selection.bind.defaultService")}
|
||||
</option>
|
||||
{serverList.map((s: any) => (
|
||||
<option key={s.id} value={s.id}>
|
||||
{s.name || s.endpoint || s.id}
|
||||
</option>
|
||||
))}
|
||||
</select>
|
||||
<SelectTrigger className="h-8 w-60">
|
||||
<SelectValue className="truncate" placeholder={t("selection.bind.defaultService") as string} />
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
<SelectItem value="__default__">
|
||||
{t("selection.bind.defaultService")}
|
||||
</SelectItem>
|
||||
{serverList.map((s: any) => (
|
||||
<SelectItem key={s.id} value={s.id}>
|
||||
{s.name || s.endpoint || s.id}
|
||||
</SelectItem>
|
||||
))}
|
||||
</SelectContent>
|
||||
</Select>
|
||||
|
||||
{(() => {
|
||||
const sid = btn.action.assistantServerId;
|
||||
const list = (sid && assistantByServer[sid]) || [];
|
||||
const loading = !!(sid && assistantLoadingByServer[sid]);
|
||||
return (
|
||||
<select
|
||||
className="rounded-md px-2 py-1 text-sm bg-white dark:bg-[#0B1220] w-44"
|
||||
<Select
|
||||
value={btn.action.assistantId || ""}
|
||||
onChange={(e) =>
|
||||
handleAssistantSelect(btn, e.target.value)
|
||||
onValueChange={(v) =>
|
||||
handleAssistantSelect(
|
||||
btn,
|
||||
v === "__default__" ? "" : v
|
||||
)
|
||||
}
|
||||
title={t("selection.bind.assistant")}
|
||||
disabled={loading}
|
||||
>
|
||||
<option value="">
|
||||
{t("selection.bind.defaultAssistant")}
|
||||
</option>
|
||||
{loading && (
|
||||
<option value="" disabled>
|
||||
{t("common.loading")}
|
||||
</option>
|
||||
)}
|
||||
{list.map((a: any) => (
|
||||
<option key={a._id} value={a._id}>
|
||||
{a._source?.name || a._id}
|
||||
</option>
|
||||
))}
|
||||
</select>
|
||||
<SelectTrigger className="h-8 w-60">
|
||||
<SelectValue className="truncate" placeholder={t("selection.bind.defaultAssistant") as string} />
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
{!loading && (
|
||||
<SelectItem value="__default__">
|
||||
{t("selection.bind.defaultAssistant")}
|
||||
</SelectItem>
|
||||
)}
|
||||
{list.map((a: any) => (
|
||||
<SelectItem key={a._id} value={a._id}>
|
||||
{a._source?.name || a._id}
|
||||
</SelectItem>
|
||||
))}
|
||||
</SelectContent>
|
||||
</Select>
|
||||
);
|
||||
})()}
|
||||
</>
|
||||
|
||||
@@ -117,7 +117,7 @@ const SelectionSettings = () => {
|
||||
<h2 className="text-lg font-semibold">{t("selection.title")}</h2>
|
||||
</div>
|
||||
|
||||
<div className="relative rounded-xl p-4 bg-gradient-to-r from-[#E6F0FA] to-[#FFF1F1]">
|
||||
<div className="relative rounded-xl p-4 bg-linear-to-r from-[#E6F0FA] to-[#FFF1F1] dark:from-[#0B1220] dark:to-[#1A2234] dark:border dark:border-gray-800 dark:shadow-sm transition-colors">
|
||||
<div className="flex items-center flex-col" aria-hidden="true">
|
||||
<div className="rounded-xl border border-gray-200 bg-white/70 shadow-sm dark:border-gray-700 dark:bg-gray-900/40">
|
||||
<HeaderToolbar
|
||||
@@ -148,7 +148,7 @@ const SelectionSettings = () => {
|
||||
</SettingsItem>
|
||||
|
||||
{selectionEnabled && (
|
||||
<div className="grid grid-cols-1 lg:grid-cols-2 gap-4">
|
||||
<div className="grid grid-cols-1 gap-4">
|
||||
<SettingsItem
|
||||
icon={Sparkles}
|
||||
title={t("selection.display.title")}
|
||||
|
||||
@@ -1,7 +1,14 @@
|
||||
import { useTranslation } from "react-i18next";
|
||||
import { Command, RotateCcw } from "lucide-react";
|
||||
import {
|
||||
Select,
|
||||
SelectTrigger,
|
||||
SelectContent,
|
||||
SelectItem,
|
||||
SelectValue,
|
||||
} from "@/components/ui/select";
|
||||
import { useEffect } from "react";
|
||||
import { Button } from "@headlessui/react";
|
||||
import { Button } from "@/components/ui/button";
|
||||
import clsx from "clsx";
|
||||
|
||||
import { formatKey } from "@/utils/keyboardUtils";
|
||||
@@ -246,21 +253,21 @@ const Shortcuts = () => {
|
||||
title={t("settings.advanced.shortcuts.modifierKey.title")}
|
||||
description={t("settings.advanced.shortcuts.modifierKey.description")}
|
||||
>
|
||||
<select
|
||||
<Select
|
||||
value={modifierKey}
|
||||
className="px-3 py-1.5 bg-white dark:bg-gray-800 border border-gray-200 dark:border-gray-700 rounded-lg text-sm focus:outline-none focus:ring-2 focus:ring-blue-500"
|
||||
onChange={(event) => {
|
||||
setModifierKey(event.target.value as ModifierKey);
|
||||
}}
|
||||
onValueChange={(v) => setModifierKey(v as ModifierKey)}
|
||||
>
|
||||
{modifierKeys.map((item) => {
|
||||
return (
|
||||
<option key={item} value={item}>
|
||||
<SelectTrigger className="h-8 w-40">
|
||||
<SelectValue />
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
{modifierKeys.map((item) => (
|
||||
<SelectItem key={item} value={item}>
|
||||
{formatKey(item)}
|
||||
</option>
|
||||
);
|
||||
})}
|
||||
</select>
|
||||
</SelectItem>
|
||||
))}
|
||||
</SelectContent>
|
||||
</Select>
|
||||
</SettingsItem>
|
||||
|
||||
{list.map((item) => {
|
||||
@@ -279,6 +286,7 @@ const Shortcuts = () => {
|
||||
<span>{formatKey(modifierKey)}</span>
|
||||
<span>+</span>
|
||||
<SettingsInput
|
||||
className="w-20"
|
||||
value={value}
|
||||
max={1}
|
||||
onChange={(value) => {
|
||||
@@ -287,23 +295,14 @@ const Shortcuts = () => {
|
||||
/>
|
||||
|
||||
<Button
|
||||
variant="outline"
|
||||
disabled={disabled}
|
||||
className={clsx(
|
||||
"flex items-center justify-center size-8 rounded-[6px] border border-black/5 dark:border-white/10 transition",
|
||||
{
|
||||
"hover:border-[#0072FF]": !disabled,
|
||||
"opacity-70 cursor-not-allowed": disabled,
|
||||
}
|
||||
)}
|
||||
size="icon"
|
||||
onClick={() => {
|
||||
handleChange(initialValue, setValue);
|
||||
}}
|
||||
>
|
||||
<RotateCcw
|
||||
className={clsx("size-4 text-[#999]", {
|
||||
"!text-[#0072FF]": !disabled,
|
||||
})}
|
||||
/>
|
||||
<RotateCcw className={clsx("size-4 opacity-80")} />
|
||||
</Button>
|
||||
</div>
|
||||
</SettingsItem>
|
||||
|
||||
@@ -21,8 +21,15 @@ import SettingsInput from "@/components//Settings/SettingsInput";
|
||||
import platformAdapter from "@/utils/platformAdapter";
|
||||
import UpdateSettings from "./components/UpdateSettings";
|
||||
import SettingsToggle from "../SettingsToggle";
|
||||
// import SelectionSettings from "./components/Selection";
|
||||
// import { isMac } from "@/utils/platform";
|
||||
import SelectionSettings from "./components/Selection";
|
||||
import { isMac } from "@/utils/platform";
|
||||
import {
|
||||
Select,
|
||||
SelectTrigger,
|
||||
SelectContent,
|
||||
SelectItem,
|
||||
SelectValue,
|
||||
} from "@/components/ui/select";
|
||||
|
||||
const Advanced = () => {
|
||||
const { t } = useTranslation();
|
||||
@@ -169,29 +176,27 @@ const Advanced = () => {
|
||||
title={t(title)}
|
||||
description={t(description)}
|
||||
>
|
||||
<select
|
||||
value={value}
|
||||
onChange={(event) => {
|
||||
onChange(event.target.value as never);
|
||||
}}
|
||||
className="px-3 py-1.5 bg-white dark:bg-gray-800 border border-gray-200 dark:border-gray-700 rounded-lg text-sm focus:outline-none focus:ring-2 focus:ring-blue-500"
|
||||
>
|
||||
{items.map((item) => {
|
||||
const { label, value } = item;
|
||||
|
||||
return (
|
||||
<option key={value} value={value}>
|
||||
{t(label)}
|
||||
</option>
|
||||
);
|
||||
})}
|
||||
</select>
|
||||
<Select value={value as string} onValueChange={(v) => onChange(v as never)}>
|
||||
<SelectTrigger className="h-8 w-44">
|
||||
<SelectValue className="truncate" />
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
{items.map((item) => {
|
||||
const { label, value } = item;
|
||||
return (
|
||||
<SelectItem key={value} value={value as string}>
|
||||
{t(label)}
|
||||
</SelectItem>
|
||||
);
|
||||
})}
|
||||
</SelectContent>
|
||||
</Select>
|
||||
</SettingsItem>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
|
||||
{/* {isMac && <SelectionSettings />} */}
|
||||
{isMac && <SelectionSettings />}
|
||||
|
||||
<Shortcuts />
|
||||
|
||||
@@ -278,33 +283,35 @@ const Advanced = () => {
|
||||
"settings.advanced.other.localSearchResultWeight.description"
|
||||
)}
|
||||
>
|
||||
<select
|
||||
value={localSearchResultWeight}
|
||||
onChange={(event) => {
|
||||
const weight = Number(event.target.value);
|
||||
|
||||
<Select
|
||||
value={String(localSearchResultWeight)}
|
||||
onValueChange={(v) => {
|
||||
const weight = Number(v);
|
||||
setLocalSearchResultWeight(weight);
|
||||
|
||||
platformAdapter.invokeBackend("set_local_query_source_weight", {
|
||||
value: weight,
|
||||
});
|
||||
}}
|
||||
className="px-3 py-1.5 bg-white dark:bg-gray-800 border border-gray-200 dark:border-gray-700 rounded-lg text-sm focus:outline-none focus:ring-2 focus:ring-blue-500"
|
||||
>
|
||||
<option value="0.5">
|
||||
{t("settings.advanced.other.localSearchResultWeight.options.low")}
|
||||
</option>
|
||||
<option value="1">
|
||||
{t(
|
||||
"settings.advanced.other.localSearchResultWeight.options.medium"
|
||||
)}
|
||||
</option>
|
||||
<option value="2">
|
||||
{t(
|
||||
"settings.advanced.other.localSearchResultWeight.options.high"
|
||||
)}
|
||||
</option>
|
||||
</select>
|
||||
<SelectTrigger className="h-8 w-44">
|
||||
<SelectValue className="truncate" />
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
<SelectItem value="0.5">
|
||||
{t("settings.advanced.other.localSearchResultWeight.options.low")}
|
||||
</SelectItem>
|
||||
<SelectItem value="1">
|
||||
{t(
|
||||
"settings.advanced.other.localSearchResultWeight.options.medium"
|
||||
)}
|
||||
</SelectItem>
|
||||
<SelectItem value="2">
|
||||
{t(
|
||||
"settings.advanced.other.localSearchResultWeight.options.high"
|
||||
)}
|
||||
</SelectItem>
|
||||
</SelectContent>
|
||||
</Select>
|
||||
</SettingsItem>
|
||||
|
||||
<SettingsItem
|
||||
|
||||
@@ -13,6 +13,7 @@ import Shortcut from "../Shortcut";
|
||||
import SettingsToggle from "@/components/Settings/SettingsToggle";
|
||||
import { platform } from "@/utils/platform";
|
||||
import { useExtensionsStore } from "@/stores/extensionsStore";
|
||||
import { cn } from "@/lib/utils";
|
||||
|
||||
const Content = () => {
|
||||
const { rootState } = useContext(ExtensionsContext);
|
||||
@@ -165,7 +166,9 @@ const Item: FC<ItemProps> = (props) => {
|
||||
<SettingsInput
|
||||
defaultValue={alias}
|
||||
placeholder={t("settings.extensions.hints.addAlias")}
|
||||
className="!w-[90%] !h-6 !border-transparent rounded-[4px]"
|
||||
className={cn(
|
||||
"w-[90%] h-6 px-1 py-0 border-none rounded-sm shadow-none bg-transparent placeholder:text-[#999]"
|
||||
)}
|
||||
onChange={(value) => {
|
||||
handleChange(String(value));
|
||||
}}
|
||||
@@ -292,7 +295,7 @@ const Item: FC<ItemProps> = (props) => {
|
||||
return (
|
||||
<>
|
||||
<div
|
||||
className={clsx("-mx-2 px-2 text-sm rounded-[6px]", {
|
||||
className={clsx("-mx-2 px-2 text-sm rounded-md", {
|
||||
"bg-[#f0f6fe] dark:bg-gray-700":
|
||||
id === rootState.activeExtension?.id,
|
||||
})}
|
||||
|
||||
@@ -72,7 +72,7 @@ const AiOverview = () => {
|
||||
/>
|
||||
|
||||
<>
|
||||
<div className="mt-6 text-[#333] dark:text-white/90">
|
||||
<div className="mt-6">
|
||||
{t("settings.extensions.aiOverview.details.aiOverviewTrigger.title")}
|
||||
</div>
|
||||
|
||||
@@ -88,9 +88,7 @@ const AiOverview = () => {
|
||||
|
||||
return (
|
||||
<div>
|
||||
<div className="mb-2 text-[#666] dark:text-white/70">
|
||||
{label}
|
||||
</div>
|
||||
<div className="mb-2">{label}</div>
|
||||
|
||||
<SettingsInput
|
||||
type="number"
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { Button } from "@headlessui/react";
|
||||
import { Button } from "@/components/ui/button";
|
||||
import { useMount } from "ahooks";
|
||||
import { useState } from "react";
|
||||
import { useTranslation } from "react-i18next";
|
||||
@@ -35,15 +35,13 @@ const Applications = () => {
|
||||
|
||||
return (
|
||||
<>
|
||||
<div className="text-[#999]">
|
||||
<p className="font-bold mb-2">
|
||||
{t("settings.extensions.application.details.searchScope")}
|
||||
</p>
|
||||
<p className="mb-2">
|
||||
{t("settings.extensions.application.details.searchScope")}
|
||||
</p>
|
||||
|
||||
<p>
|
||||
{t("settings.extensions.application.details.searchScopeDescription")}
|
||||
</p>
|
||||
</div>
|
||||
<p className="text-[#999]">
|
||||
{t("settings.extensions.application.details.searchScopeDescription")}
|
||||
</p>
|
||||
|
||||
<DirectoryScope
|
||||
paths={paths}
|
||||
@@ -72,18 +70,18 @@ const Applications = () => {
|
||||
}}
|
||||
/>
|
||||
|
||||
<div className="text-[#999] mt-4">
|
||||
<p className="font-bold mb-2">
|
||||
{t("settings.extensions.application.details.rebuildIndex")}
|
||||
</p>
|
||||
<p className="mt-4 mb-2">
|
||||
{t("settings.extensions.application.details.rebuildIndex")}
|
||||
</p>
|
||||
|
||||
<p>
|
||||
{t("settings.extensions.application.details.rebuildIndexDescription")}
|
||||
</p>
|
||||
</div>
|
||||
<p className="text-[#999]">
|
||||
{t("settings.extensions.application.details.rebuildIndexDescription")}
|
||||
</p>
|
||||
|
||||
<Button
|
||||
className="w-full h-8 my-4 text-[#0087FF] border border-[#EEF0F3] hover:!border-[#0087FF] dark:border-gray-700 rounded-[6px] transition"
|
||||
variant="outline"
|
||||
className="w-full my-4"
|
||||
// className="w-full h-8 my-4 text-[#0087FF] border border-[#EEF0F3] hover:border-[#0087FF] dark:border-gray-700 rounded-md transition"
|
||||
onClick={handleReindex}
|
||||
>
|
||||
{t("settings.extensions.application.details.reindex")}
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import { useAppStore } from "@/stores/appStore";
|
||||
import platformAdapter from "@/utils/platformAdapter";
|
||||
import { Button } from "@headlessui/react";
|
||||
import { Button } from "@/components/ui/button";
|
||||
import clsx from "clsx";
|
||||
import { castArray } from "lodash-es";
|
||||
import { Folder, SquareArrowOutUpRight, X } from "lucide-react";
|
||||
@@ -82,7 +82,7 @@ const DirectoryScope: FC<DirectoryScopeProps> = (props) => {
|
||||
return (
|
||||
<div
|
||||
key={item}
|
||||
className="flex items-center justify-between gap-2"
|
||||
className="flex items-center justify-between gap-2 text-[#666] dark:text-white/70"
|
||||
>
|
||||
<div className="flex items-center gap-1 flex-1 overflow-hidden">
|
||||
<Folder className="size-4" />
|
||||
@@ -112,7 +112,9 @@ const DirectoryScope: FC<DirectoryScopeProps> = (props) => {
|
||||
)}
|
||||
|
||||
<Button
|
||||
className="w-full h-8 text-[#0087FF] border border-[#EEF0F3] hover:!border-[#0087FF] dark:border-gray-700 rounded-[6px] transition"
|
||||
variant="outline"
|
||||
className="w-full"
|
||||
size="sm"
|
||||
onClick={handleAdd}
|
||||
>
|
||||
{t("settings.extensions.directoryScope.button.addDirectories")}
|
||||
|
||||
@@ -82,7 +82,7 @@ const FileSearch = () => {
|
||||
{t("settings.extensions.fileSearch.description")}
|
||||
</div>
|
||||
|
||||
<div className="mt-4 mb-2 text-[#666] dark:text-white/70">
|
||||
<div className="mt-4 mb-2">
|
||||
{t("settings.extensions.fileSearch.label.searchBy")}
|
||||
</div>
|
||||
|
||||
@@ -99,10 +99,7 @@ const FileSearch = () => {
|
||||
|
||||
return (
|
||||
<>
|
||||
<div
|
||||
key={label}
|
||||
className="mt-4 mb-2 text-[#666] dark:text-white/70"
|
||||
>
|
||||
<div key={label} className="mt-4 mb-2">
|
||||
{label}
|
||||
</div>
|
||||
|
||||
@@ -111,16 +108,16 @@ const FileSearch = () => {
|
||||
);
|
||||
})}
|
||||
|
||||
<div className="mt-4 mb-2 text-[#666] dark:text-white/70">
|
||||
<div className="mt-4 mb-2">
|
||||
{t("settings.extensions.fileSearch.label.searchFileTypes")}
|
||||
</div>
|
||||
|
||||
<div className="flex flex-wrap gap-2 p-2 border rounded-[6px] dark:border-gray-700">
|
||||
<div className="flex flex-wrap items-center gap-2 p-2 rounded-lg border border-input bg-background hover:border-[#0072FF] focus-within:border-[#0072FF] transition">
|
||||
{config.file_types.map((item) => {
|
||||
return (
|
||||
<div
|
||||
key={item}
|
||||
className="flex items-center gap-1 h-6 px-1 rounded bg-[#f0f0f0] dark:bg-[#444]"
|
||||
className="flex items-center gap-1 h-6 px-2 rounded-full text-xs border border-black/5 dark:border-white/10 bg-black/5 dark:bg-white/10"
|
||||
>
|
||||
<span>{item}</span>
|
||||
|
||||
@@ -140,7 +137,7 @@ const FileSearch = () => {
|
||||
|
||||
<SettingsInput
|
||||
placeholder=".*"
|
||||
className="h-6 border-0 -ml-2"
|
||||
className="h-6 w-24 px-2 border-0 outline-none focus-visible:ring-0 focus-visible:ring-offset-0"
|
||||
onKeyDown={(event) => {
|
||||
if (event.code !== "Enter") return;
|
||||
|
||||
|
||||
@@ -4,7 +4,13 @@ import { isArray } from "lodash-es";
|
||||
import { useAsyncEffect, useMount } from "ahooks";
|
||||
|
||||
import { AssistantFetcher } from "@/components/Assistant/AssistantFetcher";
|
||||
import SettingsSelectPro from "@/components/Settings/SettingsSelectPro";
|
||||
import {
|
||||
Select,
|
||||
SelectTrigger,
|
||||
SelectContent,
|
||||
SelectItem,
|
||||
SelectValue,
|
||||
} from "@/components/ui/select";
|
||||
import { useAppStore } from "@/stores/appStore";
|
||||
import { ExtensionId } from "@/components/Settings/Extensions/index";
|
||||
import { useConnectStore } from "@/stores/connectStore";
|
||||
@@ -175,27 +181,40 @@ const SharedAi: FC<SharedAiProps> = (props) => {
|
||||
<>
|
||||
<div className="text-[#999]">{renderDescription()}</div>
|
||||
|
||||
<div className="mt-6 text-[#333] dark:text-white/90">
|
||||
<div className="mt-6">
|
||||
{t("settings.extensions.shardAi.details.linkedAssistant.title")}
|
||||
</div>
|
||||
|
||||
{selectList.map((item) => {
|
||||
const { label, value, data, searchable, onChange, onSearch } = item;
|
||||
const { label, value, data, searchable, onChange } = item;
|
||||
|
||||
return (
|
||||
<div key={label} className="mt-4">
|
||||
<div className="mb-2 text-[#666] dark:text-white/70">{label}</div>
|
||||
|
||||
<SettingsSelectPro
|
||||
<Select
|
||||
value={value}
|
||||
options={data}
|
||||
searchable={searchable}
|
||||
onChange={onChange}
|
||||
onSearch={onSearch}
|
||||
placeholder={
|
||||
isLoadingAssistants && searchable ? "Loading..." : undefined
|
||||
}
|
||||
/>
|
||||
onValueChange={(v) => onChange?.(v)}
|
||||
disabled={searchable && isLoadingAssistants}
|
||||
>
|
||||
<SelectTrigger className="ml-1 h-9 w-full max-w-[480px]">
|
||||
<SelectValue
|
||||
className="truncate"
|
||||
placeholder={
|
||||
(searchable && isLoadingAssistants
|
||||
? (t("common.loading") as string)
|
||||
: undefined) as string | undefined
|
||||
}
|
||||
/>
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
{data?.map((opt: any) => (
|
||||
<SelectItem key={opt.id} value={opt.id}>
|
||||
{opt.name || opt.id}
|
||||
</SelectItem>
|
||||
))}
|
||||
</SelectContent>
|
||||
</Select>
|
||||
</div>
|
||||
);
|
||||
})}
|
||||
|
||||
@@ -9,7 +9,13 @@ import AiOverview from "./AiOverview";
|
||||
import Calculator from "./Calculator";
|
||||
import FileSearch from "./FileSearch";
|
||||
import { Ellipsis, Info } from "lucide-react";
|
||||
import { Menu, MenuButton, MenuItem, MenuItems } from "@headlessui/react";
|
||||
import {
|
||||
DropdownMenu,
|
||||
DropdownMenuTrigger,
|
||||
DropdownMenuItem,
|
||||
DropdownMenuContent,
|
||||
} from "@/components/ui/dropdown-menu";
|
||||
import { Button } from "@/components/ui/button";
|
||||
import platformAdapter from "@/utils/platformAdapter";
|
||||
import { useAppStore } from "@/stores/appStore";
|
||||
import { useTranslation } from "react-i18next";
|
||||
@@ -93,58 +99,60 @@ const Details = () => {
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="flex-1 h-full pr-4 pb-4 overflow-auto">
|
||||
<div className="flex items-start justify-between gap-4 mb-4">
|
||||
<div className="flex-1 h-full p-4 overflow-auto">
|
||||
<div className="flex items-start justify-between gap-4 mb-2">
|
||||
<h2 className="m-0 text-lg font-semibold text-gray-900 dark:text-white">
|
||||
{rootState.activeExtension?.name}
|
||||
</h2>
|
||||
|
||||
{rootState.activeExtension?.developer && (
|
||||
<Menu>
|
||||
<MenuButton className="h-7">
|
||||
<Ellipsis className="size-5 text-[#999]" />
|
||||
</MenuButton>
|
||||
|
||||
<MenuItems
|
||||
anchor="bottom end"
|
||||
className="p-1 text-sm bg-white dark:bg-[#202126] rounded-lg shadow-xs border border-gray-200 dark:border-gray-700"
|
||||
<DropdownMenu>
|
||||
<DropdownMenuTrigger asChild>
|
||||
<Button
|
||||
variant="ghost"
|
||||
size="icon"
|
||||
className="h-7 w-7 p-0 focus-visible:ring-0 focus-visible:ring-offset-0"
|
||||
>
|
||||
<Ellipsis className="h-4 w-4 text-muted-foreground" />
|
||||
</Button>
|
||||
</DropdownMenuTrigger>
|
||||
<DropdownMenuContent
|
||||
side="bottom"
|
||||
align="end"
|
||||
className="p-1 text-sm rounded-lg"
|
||||
>
|
||||
<MenuItem>
|
||||
<div
|
||||
className="px-3 py-2 text-nowrap text-red-500 hover:bg-black/5 hover:dark:bg-white/5 rounded-lg cursor-pointer"
|
||||
onClick={async () => {
|
||||
try {
|
||||
const { id, developer } = rootState.activeExtension!;
|
||||
<DropdownMenuItem
|
||||
className="px-3 py-2 text-nowrap text-red-500 rounded-lg hover:bg-muted"
|
||||
onSelect={async (e: Event) => {
|
||||
e.preventDefault();
|
||||
try {
|
||||
const { id, developer } = rootState.activeExtension!;
|
||||
|
||||
await platformAdapter.invokeBackend(
|
||||
"uninstall_extension",
|
||||
{
|
||||
extensionId: id,
|
||||
developer: developer,
|
||||
}
|
||||
);
|
||||
await platformAdapter.invokeBackend("uninstall_extension", {
|
||||
extensionId: id,
|
||||
developer: developer,
|
||||
});
|
||||
|
||||
Object.assign(rootState, {
|
||||
activeExtension: void 0,
|
||||
extensions: rootState.extensions.filter((item) => {
|
||||
return item.id !== id;
|
||||
}),
|
||||
});
|
||||
Object.assign(rootState, {
|
||||
activeExtension: void 0,
|
||||
extensions: rootState.extensions.filter((item) => {
|
||||
return item.id !== id;
|
||||
}),
|
||||
});
|
||||
|
||||
addError(
|
||||
t("settings.extensions.hints.uninstallSuccess"),
|
||||
"info"
|
||||
);
|
||||
} catch (error) {
|
||||
addError(String(error));
|
||||
}
|
||||
}}
|
||||
>
|
||||
{t("settings.extensions.hints.uninstall")}
|
||||
</div>
|
||||
</MenuItem>
|
||||
</MenuItems>
|
||||
</Menu>
|
||||
addError(
|
||||
t("settings.extensions.hints.uninstallSuccess"),
|
||||
"info"
|
||||
);
|
||||
} catch (error) {
|
||||
addError(String(error));
|
||||
}
|
||||
}}
|
||||
>
|
||||
{t("settings.extensions.hints.uninstall")}
|
||||
</DropdownMenuItem>
|
||||
</DropdownMenuContent>
|
||||
</DropdownMenu>
|
||||
)}
|
||||
</div>
|
||||
|
||||
|
||||
@@ -3,15 +3,21 @@ import { useReactive } from "ahooks";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import type { LiteralUnion } from "type-fest";
|
||||
import { cloneDeep, sortBy } from "lodash-es";
|
||||
import clsx from "clsx";
|
||||
import { Plus } from "lucide-react";
|
||||
import { Menu, MenuButton, MenuItem, MenuItems } from "@headlessui/react";
|
||||
import {
|
||||
DropdownMenu,
|
||||
DropdownMenuTrigger,
|
||||
DropdownMenuContent,
|
||||
DropdownMenuItem,
|
||||
} from "@/components/ui/dropdown-menu";
|
||||
import { Button } from "@/components/ui/button";
|
||||
import { Input } from "@/components/ui/input";
|
||||
import { Tabs, TabsList, TabsTrigger } from "@/components/ui/tabs";
|
||||
|
||||
import platformAdapter from "@/utils/platformAdapter";
|
||||
import Content from "./components/Content";
|
||||
import Details from "./components/Details";
|
||||
import { useExtensionsStore } from "@/stores/extensionsStore";
|
||||
import SettingsInput from "../SettingsInput";
|
||||
import { useAppStore } from "@/stores/appStore";
|
||||
import { installExtensionError } from "@/utils";
|
||||
|
||||
@@ -184,95 +190,88 @@ export const Extensions = () => {
|
||||
rootState: state,
|
||||
}}
|
||||
>
|
||||
<div className="flex h-[calc(100vh-128px)] -mx-6 gap-4 text-sm">
|
||||
<div className="w-2/3 h-full px-4 border-r dark:border-gray-700 overflow-auto">
|
||||
<div className="flex h-[calc(100vh-128px)] -mx-6 text-sm">
|
||||
<div className="w-2/3 h-full px-4 border-r border-border overflow-auto">
|
||||
<div className="flex items-center justify-between">
|
||||
<h2 className="text-lg font-semibold text-gray-900 dark:text-white">
|
||||
{t("settings.extensions.title")}
|
||||
</h2>
|
||||
|
||||
<Menu>
|
||||
<MenuButton className="flex items-center justify-center size-6 border rounded-[6px] dark:border-gray-700 hover:!border-[#0096FB] transition">
|
||||
<Plus className="size-4 text-[#0096FB]" />
|
||||
</MenuButton>
|
||||
<DropdownMenu>
|
||||
<DropdownMenuTrigger asChild>
|
||||
<Button variant="outline" size="icon" className="size-6">
|
||||
<Plus className="h-4 w-4 text-primary" />
|
||||
</Button>
|
||||
</DropdownMenuTrigger>
|
||||
|
||||
<MenuItems
|
||||
anchor={{ gap: 4 }}
|
||||
className="p-1 text-sm bg-white dark:bg-[#202126] rounded-lg shadow-xs border border-gray-200 dark:border-gray-700"
|
||||
<DropdownMenuContent
|
||||
sideOffset={4}
|
||||
className="p-1 text-sm rounded-lg"
|
||||
>
|
||||
<MenuItem>
|
||||
<div
|
||||
className="px-3 py-2 hover:bg-black/5 hover:dark:bg-white/5 rounded-lg cursor-pointer"
|
||||
onClick={() => {
|
||||
platformAdapter.emitEvent("open-extension-store");
|
||||
}}
|
||||
>
|
||||
{t("settings.extensions.menuItem.extensionStore")}
|
||||
</div>
|
||||
</MenuItem>
|
||||
<MenuItem>
|
||||
<div
|
||||
className="px-3 py-2 hover:bg-black/5 hover:dark:bg-white/5 rounded-lg cursor-pointer"
|
||||
onClick={async () => {
|
||||
try {
|
||||
const path = await platformAdapter.openFileDialog({
|
||||
directory: true,
|
||||
});
|
||||
<DropdownMenuItem
|
||||
className="px-3 py-2 rounded-lg hover:bg-muted"
|
||||
onSelect={(e: Event) => {
|
||||
e.preventDefault();
|
||||
platformAdapter.emitEvent("open-extension-store");
|
||||
}}
|
||||
>
|
||||
{t("settings.extensions.menuItem.extensionStore")}
|
||||
</DropdownMenuItem>
|
||||
<DropdownMenuItem
|
||||
className="px-3 py-2 rounded-lg hover:bg-muted"
|
||||
onSelect={async (e: Event) => {
|
||||
e.preventDefault();
|
||||
try {
|
||||
const path = await platformAdapter.openFileDialog({
|
||||
directory: true,
|
||||
});
|
||||
|
||||
if (!path) return;
|
||||
if (!path) return;
|
||||
|
||||
await platformAdapter.invokeBackend(
|
||||
"install_local_extension",
|
||||
{ path }
|
||||
);
|
||||
await platformAdapter.invokeBackend(
|
||||
"install_local_extension",
|
||||
{ path }
|
||||
);
|
||||
|
||||
await getExtensions();
|
||||
await getExtensions();
|
||||
|
||||
addError(
|
||||
t("settings.extensions.hints.importSuccess"),
|
||||
"info"
|
||||
);
|
||||
} catch (error) {
|
||||
installExtensionError(error);
|
||||
}
|
||||
}}
|
||||
>
|
||||
{t("settings.extensions.menuItem.localExtensionImport")}
|
||||
</div>
|
||||
</MenuItem>
|
||||
</MenuItems>
|
||||
</Menu>
|
||||
addError(
|
||||
t("settings.extensions.hints.importSuccess"),
|
||||
"info"
|
||||
);
|
||||
} catch (error) {
|
||||
installExtensionError(error);
|
||||
}
|
||||
}}
|
||||
>
|
||||
{t("settings.extensions.menuItem.localExtensionImport")}
|
||||
</DropdownMenuItem>
|
||||
</DropdownMenuContent>
|
||||
</DropdownMenu>
|
||||
</div>
|
||||
|
||||
<div className="flex justify-between gap-6 my-4">
|
||||
<div className="flex h-8 border dark:border-gray-700 rounded-[6px] overflow-hidden">
|
||||
{state.categories.map((item) => {
|
||||
return (
|
||||
<div
|
||||
key={item}
|
||||
className={clsx(
|
||||
"flex items-center h-full px-4 cursor-pointer",
|
||||
{
|
||||
"bg-[#F0F6FE] dark:bg-gray-700":
|
||||
item === state.currentCategory,
|
||||
}
|
||||
)}
|
||||
onClick={() => {
|
||||
state.currentCategory = item;
|
||||
}}
|
||||
>
|
||||
<div className="flex items-center justify-between gap-6 my-4">
|
||||
<Tabs
|
||||
value={state.currentCategory}
|
||||
onValueChange={(v) => {
|
||||
state.currentCategory = v as Category;
|
||||
}}
|
||||
>
|
||||
<TabsList>
|
||||
{state.categories.map((item) => (
|
||||
<TabsTrigger key={item} value={item}>
|
||||
{item}
|
||||
</div>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
</TabsTrigger>
|
||||
))}
|
||||
</TabsList>
|
||||
</Tabs>
|
||||
|
||||
<SettingsInput
|
||||
className="flex-1"
|
||||
<Input
|
||||
className="flex-1 h-8"
|
||||
placeholder="Search"
|
||||
value={state.searchValue}
|
||||
onChange={(value) => {
|
||||
state.searchValue = String(value);
|
||||
value={state.searchValue ?? ""}
|
||||
onChange={(e) => {
|
||||
state.searchValue = e.target.value;
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
|
||||
@@ -36,6 +36,13 @@ import {
|
||||
} from "@/commands";
|
||||
import platformAdapter from "@/utils/platformAdapter";
|
||||
import { useAppearanceStore, WindowMode } from "@/stores/appearanceStore";
|
||||
import {
|
||||
Select,
|
||||
SelectTrigger,
|
||||
SelectContent,
|
||||
SelectItem,
|
||||
SelectValue,
|
||||
} from "@/components/ui/select";
|
||||
|
||||
export function ThemeOption({
|
||||
icon: Icon,
|
||||
@@ -83,8 +90,6 @@ export default function GeneralSettings() {
|
||||
const { showTooltip, setShowTooltip, language, setLanguage } = useAppStore();
|
||||
const { windowMode, setWindowMode } = useAppearanceStore();
|
||||
|
||||
|
||||
|
||||
const fetchAutoStartStatus = async () => {
|
||||
if (isTauri()) {
|
||||
try {
|
||||
@@ -283,7 +288,7 @@ export default function GeneralSettings() {
|
||||
className={clsx(
|
||||
"p-4 rounded-lg border-2 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",
|
||||
{
|
||||
"!border-blue-500 bg-blue-50 dark:bg-blue-900/20":
|
||||
"border-blue-500! bg-blue-50! dark:bg-blue-900/20!":
|
||||
isSelected,
|
||||
}
|
||||
)}
|
||||
@@ -307,28 +312,31 @@ export default function GeneralSettings() {
|
||||
})}
|
||||
</div>
|
||||
|
||||
|
||||
|
||||
<SettingsItem
|
||||
icon={Globe}
|
||||
title={t("settings.language.title")}
|
||||
description={t("settings.language.description")}
|
||||
>
|
||||
<div className="flex items-center gap-2">
|
||||
<select
|
||||
<Select
|
||||
value={currentLanguage}
|
||||
onChange={(event) => {
|
||||
const lang = event.currentTarget.value;
|
||||
|
||||
onValueChange={(lang) => {
|
||||
setLanguage(lang);
|
||||
|
||||
platformAdapter.invokeBackend("update_app_lang", { lang });
|
||||
}}
|
||||
className="px-3 py-1.5 bg-white dark:bg-gray-800 border border-gray-200 dark:border-gray-700 rounded-lg text-sm focus:outline-none focus:ring-2 focus:ring-blue-500"
|
||||
>
|
||||
<option value="en">{t("settings.language.english")}</option>
|
||||
<option value="zh">{t("settings.language.chinese")}</option>
|
||||
</select>
|
||||
<SelectTrigger className="h-8 w-44">
|
||||
<SelectValue className="truncate" />
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
<SelectItem value="en">
|
||||
{t("settings.language.english")}
|
||||
</SelectItem>
|
||||
<SelectItem value="zh">
|
||||
{t("settings.language.chinese")}
|
||||
</SelectItem>
|
||||
</SelectContent>
|
||||
</Select>
|
||||
</div>
|
||||
</SettingsItem>
|
||||
|
||||
|
||||
@@ -1,10 +1,13 @@
|
||||
import { Input, InputProps } from "@headlessui/react";
|
||||
import { Input } from "@/components/ui/input";
|
||||
import { isNumber } from "lodash-es";
|
||||
import { FC, FocusEvent } from "react";
|
||||
import { FC, FocusEvent, InputHTMLAttributes } from "react";
|
||||
import { twMerge } from "tailwind-merge";
|
||||
|
||||
interface SettingsInputProps
|
||||
extends Omit<InputProps, "onChange" | "className"> {
|
||||
extends Omit<
|
||||
InputHTMLAttributes<HTMLInputElement>,
|
||||
"onChange" | "className"
|
||||
> {
|
||||
className?: string;
|
||||
onChange?: (value?: string | number) => void;
|
||||
}
|
||||
@@ -35,10 +38,7 @@ const SettingsInput: FC<SettingsInputProps> = (props) => {
|
||||
<Input
|
||||
{...rest}
|
||||
autoCorrect="off"
|
||||
className={twMerge(
|
||||
"w-20 h-8 px-2 rounded-[6px] border bg-transparent border-black/5 dark:border-white/10 hover:!border-[#0072FF] focus:!border-[#0072FF] transition",
|
||||
className
|
||||
)}
|
||||
className={twMerge("w-44 h-8", className)}
|
||||
onBlur={handleBlur}
|
||||
onChange={(event) => {
|
||||
onChange?.(event.target.value);
|
||||
|
||||
@@ -7,7 +7,7 @@ interface SettingsPanelProps {
|
||||
|
||||
const SettingsPanel: React.FC<SettingsPanelProps> = ({ children }) => {
|
||||
return (
|
||||
<div className="bg-white dark:bg-gray-800 rounded-xl p-6">
|
||||
<div className="bg-background text-foreground rounded-xl p-6">
|
||||
{/* <h2 className="text-xl font-semibold mb-6">{title}</h2> */}
|
||||
{children}
|
||||
</div>
|
||||
|
||||
@@ -1,7 +1,8 @@
|
||||
import { useBoolean, useClickAway, useDebounce } from "ahooks";
|
||||
import clsx from "clsx";
|
||||
import { FC, useEffect, useMemo, useRef, useState } from "react";
|
||||
import SettingsInput from "./SettingsInput";
|
||||
|
||||
import { Input } from "@/components/ui/input";
|
||||
import NoDataImage from "../Common/NoDataImage";
|
||||
|
||||
interface SettingsSelectProProps {
|
||||
@@ -47,7 +48,7 @@ const SettingsSelectPro: FC<SettingsSelectProProps> = (props) => {
|
||||
return (
|
||||
<div ref={containerRef} className="relative">
|
||||
<div
|
||||
className="flex items-center h-8 px-3 truncate rounded-[6px] border dark:bg-[#1F2937] bg-white dark:border-[#374151]"
|
||||
className="flex items-center h-9 px-3 truncate rounded-md border border-input bg-background text-foreground shadow-sm"
|
||||
onClick={toggle}
|
||||
>
|
||||
{option?.[labelField] ?? (
|
||||
@@ -57,7 +58,7 @@ const SettingsSelectPro: FC<SettingsSelectProProps> = (props) => {
|
||||
|
||||
<div
|
||||
className={clsx(
|
||||
"absolute z-100 top-10 left-0 right-0 rounded-[6px] py-2 border dark:border-[#374151] bg-white dark:bg-[#1F2937] shadow-[0_5px_15px_rgba(0,0,0,0.2)] dark:shadow-[0_5px_10px_rgba(0,0,0,0.3)]",
|
||||
"absolute z-50 top-11 left-0 right-0 rounded-md p-2 border border-input bg-popover text-popover-foreground shadow-md",
|
||||
{
|
||||
hidden: !open,
|
||||
}
|
||||
@@ -65,12 +66,12 @@ const SettingsSelectPro: FC<SettingsSelectProProps> = (props) => {
|
||||
>
|
||||
{searchable && (
|
||||
<div className="px-2 mb-2">
|
||||
<SettingsInput
|
||||
<Input
|
||||
autoFocus
|
||||
value={searchValue}
|
||||
className="w-full"
|
||||
onChange={(value) => {
|
||||
setSearchValue(String(value));
|
||||
className="w-full h-8 border-0 shadow-none focus-visible:ring-0 focus-visible:ring-offset-0"
|
||||
onChange={(e) => {
|
||||
setSearchValue(String(e.target.value));
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
@@ -83,9 +84,9 @@ const SettingsSelectPro: FC<SettingsSelectProProps> = (props) => {
|
||||
<div
|
||||
key={item?.[valueField] ?? index}
|
||||
className={clsx(
|
||||
"h-8 leading-8 px-2 rounded-[6px] hover:bg-[#EDEDED] hover:dark:bg-[#374151] transition cursor-pointer",
|
||||
"h-8 leading-8 px-2 rounded-md hover:bg-accent hover:text-accent-foreground transition cursor-pointer",
|
||||
{
|
||||
"bg-[#EDEDED] dark:bg-[#374151]":
|
||||
"bg-accent text-accent-foreground":
|
||||
value === item?.[valueField],
|
||||
}
|
||||
)}
|
||||
|
||||
@@ -1,28 +1,26 @@
|
||||
import { Switch, SwitchProps } from "@headlessui/react";
|
||||
import { Switch } from "@/components/ui/switch";
|
||||
import clsx from "clsx";
|
||||
|
||||
interface SettingsToggleProps extends SwitchProps {
|
||||
type BaseSwitchProps = React.ComponentProps<typeof Switch>;
|
||||
interface SettingsToggleProps
|
||||
extends Omit<BaseSwitchProps, "onChange" | "onCheckedChange"> {
|
||||
label: string;
|
||||
className?: string;
|
||||
onChange?: (checked: boolean) => void;
|
||||
}
|
||||
|
||||
export default function SettingsToggle(props: SettingsToggleProps) {
|
||||
const { label, className, ...rest } = props;
|
||||
const { label, className, onChange, ...rest } = props;
|
||||
|
||||
return (
|
||||
<Switch
|
||||
{...rest}
|
||||
aria-label={label}
|
||||
onCheckedChange={(v) => onChange?.(v)}
|
||||
className={clsx(
|
||||
`group 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 bg-gray-200 data-[checked]:bg-blue-600`,
|
||||
"h-5 w-9",
|
||||
className
|
||||
)}
|
||||
>
|
||||
<span className="sr-only">{label}</span>
|
||||
<span
|
||||
className="pointer-events-none relative inline-block h-5 w-5 transform rounded-full bg-white shadow
|
||||
ring-0 transition duration-200 ease-in-out translate-x-0 group-data-[checked]:translate-x-5"
|
||||
/>
|
||||
</Switch>
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import { useCallback, useMemo, useEffect } from "react";
|
||||
import { Button, Dialog, DialogPanel } from "@headlessui/react";
|
||||
import { Dialog, DialogContent } from "@/components/ui/dialog";
|
||||
import { Button } from "@/components/ui/button";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import { noop } from "lodash-es";
|
||||
import { LoaderCircle, X } from "lucide-react";
|
||||
import { useInterval, useReactive } from "ahooks";
|
||||
import clsx from "clsx";
|
||||
@@ -141,117 +141,107 @@ const UpdateApp = ({ isCheckPage }: UpdateAppProps) => {
|
||||
return (
|
||||
<Dialog
|
||||
open={isCheckPage ? true : visible}
|
||||
as="div"
|
||||
id="update-app-dialog"
|
||||
className="relative z-10 focus:outline-none"
|
||||
onClose={noop}
|
||||
onOpenChange={(v) => {
|
||||
if (!isCheckPage) setVisible(v);
|
||||
}}
|
||||
>
|
||||
<div
|
||||
className={`fixed inset-0 z-10 w-screen overflow-y-auto ${
|
||||
<DialogContent
|
||||
id="update-app-dialog"
|
||||
overlayClassName={clsx("bg-transparent backdrop-blur-0 rounded-xl")}
|
||||
className={clsx(
|
||||
isCheckPage
|
||||
? "rounded-lg bg-white dark:bg-[#333] border border-[#EDEDED] dark:border-black/20 shadow-md"
|
||||
: ""
|
||||
}`}
|
||||
? "inset-0 left-0 top-0 translate-x-0 translate-y-0 w-full h-screen max-w-none rounded-lg border-none bg-background text-foreground p-0"
|
||||
: "w-[340px] py-8 flex flex-col items-center rounded-lg border border-input bg-background text-foreground shadow-md"
|
||||
)}
|
||||
>
|
||||
<div
|
||||
data-tauri-drag-region
|
||||
className={clsx(
|
||||
"flex min-h-full items-center justify-center",
|
||||
!isCheckPage && "p-4"
|
||||
"w-full flex flex-col items-center justify-center px-6",
|
||||
isCheckPage && "h-full"
|
||||
)}
|
||||
>
|
||||
<DialogPanel
|
||||
transition
|
||||
className={clsx(
|
||||
"relative w-[340px] py-8 flex flex-col items-center",
|
||||
{
|
||||
"rounded-lg bg-white dark:bg-[#333] border border-[#EDEDED] dark:border-black/20 shadow-md":
|
||||
!isCheckPage,
|
||||
}
|
||||
)}
|
||||
>
|
||||
{!isCheckPage && isOptional && (
|
||||
<X
|
||||
className={clsx(
|
||||
"absolute size-5 top-3 right-3 text-[#999] dark:text-[#D8D8D8]",
|
||||
cursorClassName
|
||||
)}
|
||||
onClick={handleCancel}
|
||||
role="button"
|
||||
aria-label="Close dialog"
|
||||
tabIndex={0}
|
||||
/>
|
||||
)}
|
||||
|
||||
<img src={isDark ? darkIcon : lightIcon} className="h-6" />
|
||||
|
||||
<div className="text-[#333] text-sm leading-5 py-2 dark:text-[#D8D8D8] text-center">
|
||||
{updateInfo ? (
|
||||
isOptional ? (
|
||||
t("update.optional_description")
|
||||
) : (
|
||||
<>
|
||||
<p>{t("update.force_description1")}</p>
|
||||
<p>{t("update.force_description2")}</p>
|
||||
</>
|
||||
)
|
||||
) : (
|
||||
t("update.date")
|
||||
)}
|
||||
</div>
|
||||
|
||||
{updateInfo ? (
|
||||
<div
|
||||
className="text-xs text-[#0072FF] cursor-pointer"
|
||||
onClick={() =>
|
||||
OpenURLWithBrowser(
|
||||
"https://docs.infinilabs.com/coco-app/main/docs/release-notes"
|
||||
)
|
||||
}
|
||||
>
|
||||
v{updateInfo.version} {t("update.releaseNotes")}
|
||||
</div>
|
||||
) : (
|
||||
<div className={clsx("text-xs text-[#999]", cursorClassName)}>
|
||||
{t("update.latest", {
|
||||
replace: [
|
||||
updateInfo?.version || process.env.VERSION || "N/A",
|
||||
],
|
||||
})}
|
||||
</div>
|
||||
)}
|
||||
|
||||
<Button
|
||||
{!isCheckPage && isOptional && (
|
||||
<X
|
||||
className={clsx(
|
||||
"mb-3 mt-6 bg-[#0072FF] text-white text-sm px-[14px] py-[8px] rounded-lg",
|
||||
cursorClassName,
|
||||
state.loading && "opacity-50"
|
||||
"absolute h-5 w-5 top-3 right-3 text-muted-foreground",
|
||||
cursorClassName
|
||||
)}
|
||||
onClick={updateInfo ? handleDownload : handleSkip}
|
||||
>
|
||||
{state.loading ? (
|
||||
<div className="flex justify-center items-center gap-2">
|
||||
<LoaderCircle className="animate-spin size-5" />
|
||||
{percent}%
|
||||
</div>
|
||||
) : updateInfo ? (
|
||||
t("update.button.install")
|
||||
) : (
|
||||
t("update.button.ok")
|
||||
)}
|
||||
</Button>
|
||||
onClick={handleCancel}
|
||||
role="button"
|
||||
aria-label="Close dialog"
|
||||
tabIndex={0}
|
||||
/>
|
||||
)}
|
||||
|
||||
{updateInfo && isOptional && (
|
||||
<div
|
||||
className={clsx("text-xs text-[#999]", cursorClassName)}
|
||||
onClick={handleSkip}
|
||||
>
|
||||
{t("update.skip_version")}
|
||||
</div>
|
||||
<img src={isDark ? darkIcon : lightIcon} className="h-6" />
|
||||
|
||||
<div className="text-sm leading-5 py-2 text-foreground text-center">
|
||||
{updateInfo ? (
|
||||
isOptional ? (
|
||||
t("update.optional_description")
|
||||
) : (
|
||||
<>
|
||||
<p>{t("update.force_description1")}</p>
|
||||
<p>{t("update.force_description2")}</p>
|
||||
</>
|
||||
)
|
||||
) : (
|
||||
t("update.date")
|
||||
)}
|
||||
</DialogPanel>
|
||||
</div>
|
||||
|
||||
{updateInfo ? (
|
||||
<div
|
||||
className="text-xs text-primary cursor-pointer"
|
||||
onClick={() =>
|
||||
OpenURLWithBrowser(
|
||||
"https://docs.infinilabs.com/coco-app/main/docs/release-notes"
|
||||
)
|
||||
}
|
||||
>
|
||||
v{updateInfo.version} {t("update.releaseNotes")}
|
||||
</div>
|
||||
) : (
|
||||
<div
|
||||
className={clsx("text-xs text-muted-foreground", cursorClassName)}
|
||||
>
|
||||
{t("update.latest", {
|
||||
replace: [updateInfo?.version || process.env.VERSION || "N/A"],
|
||||
})}
|
||||
</div>
|
||||
)}
|
||||
|
||||
<Button
|
||||
className={clsx(
|
||||
"mb-3 mt-6 bg-primary text-primary-foreground text-sm px-[14px] py-[8px] rounded-lg",
|
||||
cursorClassName,
|
||||
state.loading && "opacity-50"
|
||||
)}
|
||||
onClick={updateInfo ? handleDownload : handleSkip}
|
||||
>
|
||||
{state.loading ? (
|
||||
<div className="flex justify-center items-center gap-2">
|
||||
<LoaderCircle className="animate-spin h-5 w-5" />
|
||||
{percent}%
|
||||
</div>
|
||||
) : updateInfo ? (
|
||||
t("update.button.install")
|
||||
) : (
|
||||
t("update.button.ok")
|
||||
)}
|
||||
</Button>
|
||||
|
||||
{updateInfo && isOptional && (
|
||||
<div
|
||||
className={clsx("text-xs text-muted-foreground", cursorClassName)}
|
||||
onClick={handleSkip}
|
||||
>
|
||||
{t("update.skip_version")}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</DialogContent>
|
||||
</Dialog>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { useAppStore } from "@/stores/appStore";
|
||||
import { Button } from "@headlessui/react";
|
||||
import { Button } from "@/components/ui/button";
|
||||
import { SquareArrowOutUpRight } from "lucide-react";
|
||||
import { useTranslation } from "react-i18next";
|
||||
|
||||
@@ -12,10 +12,7 @@ const LoginButton = () => {
|
||||
};
|
||||
|
||||
return (
|
||||
<Button
|
||||
className="px-6 h-8 text-white bg-[#0287FF] flex rounded-[8px] items-center justify-center gap-1"
|
||||
onClick={handleClick}
|
||||
>
|
||||
<Button className="h-8" onClick={handleClick}>
|
||||
<span>{t("webLogin.buttons.login")}</span>
|
||||
|
||||
<SquareArrowOutUpRight className="size-4" />
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import { RefreshCw } from "lucide-react";
|
||||
import { FC, useState } from "react";
|
||||
import { Button, ButtonProps } from "@headlessui/react";
|
||||
import { Button, ButtonProps } from "@/components/ui/button";
|
||||
import clsx from "clsx";
|
||||
|
||||
import { useWebConfigStore } from "@/stores/webConfigStore";
|
||||
@@ -25,10 +25,9 @@ const RefreshButton: FC<ButtonProps> = (props) => {
|
||||
<Button
|
||||
{...rest}
|
||||
onClick={handleRefresh}
|
||||
className={clsx(
|
||||
"flex items-center justify-center size-6 bg-white dark:bg-[#202126] rounded-[8px] border dark:border-white/10",
|
||||
className
|
||||
)}
|
||||
variant="outline"
|
||||
size="icon"
|
||||
className={clsx("size-8", className)}
|
||||
disabled={isRefreshing}
|
||||
>
|
||||
<VisibleKey shortcut="R" onKeyPress={handleRefresh}>
|
||||
|
||||
@@ -12,7 +12,7 @@ const UserAvatar: FC<UserAvatarProps> = (props) => {
|
||||
return (
|
||||
<div
|
||||
className={clsx(
|
||||
"flex items-center justify-center size-5 rounded-full border dark:border-white/10 overflow-hidden",
|
||||
"flex items-center justify-center size-5 rounded-full border border-border overflow-hidden",
|
||||
className
|
||||
)}
|
||||
>
|
||||
|
||||
@@ -1,4 +1,8 @@
|
||||
import { Popover, PopoverButton, PopoverPanel } from "@headlessui/react";
|
||||
import {
|
||||
Popover,
|
||||
PopoverTrigger,
|
||||
PopoverContent,
|
||||
} from "@/components/ui/popover";
|
||||
import { useWebConfigStore } from "@/stores/webConfigStore";
|
||||
import { LogOut } from "lucide-react";
|
||||
import clsx from "clsx";
|
||||
@@ -10,21 +14,18 @@ import RefreshButton from "./RefreshButton";
|
||||
import LoginButton from "./LoginButton";
|
||||
import { FC } from "react";
|
||||
import Copyright from "../Common/Copyright";
|
||||
import { PopoverContentProps } from "@radix-ui/react-popover";
|
||||
import { Button } from "../ui/button";
|
||||
|
||||
interface WebLoginProps {
|
||||
panelClassName: string;
|
||||
}
|
||||
|
||||
const WebLogin: FC<WebLoginProps> = (props) => {
|
||||
const { panelClassName } = props;
|
||||
const WebLogin: FC<PopoverContentProps> = (props) => {
|
||||
const { integration, loginInfo, setIntegration, setLoginInfo } =
|
||||
useWebConfigStore();
|
||||
const { t } = useTranslation();
|
||||
|
||||
return (
|
||||
<div className="relative">
|
||||
<div className="flex items-center relative text-sm">
|
||||
<Popover>
|
||||
<PopoverButton>
|
||||
<PopoverTrigger className="cursor-pointer">
|
||||
{loginInfo ? (
|
||||
<UserAvatar />
|
||||
) : (
|
||||
@@ -33,38 +34,35 @@ const WebLogin: FC<WebLoginProps> = (props) => {
|
||||
className="size-5 text-[#999]"
|
||||
/>
|
||||
)}
|
||||
</PopoverButton>
|
||||
</PopoverTrigger>
|
||||
|
||||
<PopoverPanel
|
||||
className={clsx(
|
||||
"absolute z-50 w-[300px] rounded-xl bg-white dark:bg-[#202126] text-sm/6 text-[#333] dark:text-[#D8D8D8] shadow-lg border dark:border-white/10 -translate-y-2",
|
||||
panelClassName
|
||||
)}
|
||||
>
|
||||
<PopoverContent {...props} className="p-0">
|
||||
<div className="p-3">
|
||||
<div className="flex items-center justify-between mb-2">
|
||||
<span>{t("webLogin.title")}</span>
|
||||
|
||||
<RefreshButton />
|
||||
<RefreshButton className="size-6" />
|
||||
</div>
|
||||
|
||||
<div className="py-2">
|
||||
{loginInfo ? (
|
||||
<div className="flex justify-between items-center">
|
||||
<div className="flex justify-between items-center gap-3">
|
||||
<div className="flex items-center gap-3">
|
||||
<UserAvatar
|
||||
className="!size-12"
|
||||
icon={{ className: "!size-6" }}
|
||||
className="h-12 w-12"
|
||||
icon={{ className: "h-6 w-6" }}
|
||||
/>
|
||||
|
||||
<div className="flex flex-col">
|
||||
<span>{loginInfo.name}</span>
|
||||
<span className="text-[#999]">{loginInfo.email}</span>
|
||||
<span>{loginInfo?.name}</span>
|
||||
<span className="text-[#999]">{loginInfo?.email}</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<button
|
||||
className="flex items-center justify-center size-6 bg-white dark:bg-[#202126] rounded-[8px] border dark:border-white/10"
|
||||
<Button
|
||||
variant="outline"
|
||||
size="icon"
|
||||
className="size-6"
|
||||
onClick={async () => {
|
||||
await Post("/account/logout", void 0);
|
||||
|
||||
@@ -77,7 +75,7 @@ const WebLogin: FC<WebLoginProps> = (props) => {
|
||||
"size-3 text-[#0287FF] transition-transform duration-1000"
|
||||
)}
|
||||
/>
|
||||
</button>
|
||||
</Button>
|
||||
</div>
|
||||
) : (
|
||||
<div className="flex flex-col items-center gap-3">
|
||||
@@ -93,10 +91,10 @@ const WebLogin: FC<WebLoginProps> = (props) => {
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="p-3 border-t dark:border-t-white/10">
|
||||
<div className="p-3 border-t border-border">
|
||||
<Copyright />
|
||||
</div>
|
||||
</PopoverPanel>
|
||||
</PopoverContent>
|
||||
</Popover>
|
||||
</div>
|
||||
);
|
||||
|
||||
@@ -1,52 +1,46 @@
|
||||
import * as React from "react";
|
||||
import { Slot } from "@radix-ui/react-slot";
|
||||
import { cva, type VariantProps } from "class-variance-authority";
|
||||
|
||||
import { cn } from "@/lib/utils";
|
||||
|
||||
const buttonVariants = cva(
|
||||
"inline-flex items-center justify-center gap-2 whitespace-nowrap rounded-[6px] text-sm font-medium transition-colors focus-visible:outline-none focus-visible:ring-1 focus-visible:ring-ring disabled:pointer-events-none disabled:opacity-50 [&_svg]:pointer-events-none [&_svg]:size-4 [&_svg]:shrink-0",
|
||||
"inline-flex items-center justify-center gap-2 whitespace-nowrap rounded-md text-sm font-medium transition-colors focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 ring-offset-background cursor-pointer disabled:cursor-not-allowed disabled:pointer-events-none disabled:opacity-50",
|
||||
{
|
||||
variants: {
|
||||
variant: {
|
||||
default:
|
||||
"bg-primary text-primary-foreground shadow hover:bg-primary/90",
|
||||
destructive:
|
||||
"bg-destructive text-destructive-foreground shadow-sm hover:bg-destructive/90",
|
||||
outline:
|
||||
"border border-input bg-background shadow-sm hover:bg-accent hover:text-accent-foreground",
|
||||
"bg-primary text-primary-foreground hover:bg-primary/90",
|
||||
secondary:
|
||||
"bg-secondary text-secondary-foreground shadow-sm hover:bg-secondary/80",
|
||||
"bg-secondary text-secondary-foreground hover:bg-secondary/80",
|
||||
outline:
|
||||
"border border-input bg-background hover:bg-accent hover:text-accent-foreground",
|
||||
ghost: "hover:bg-accent hover:text-accent-foreground",
|
||||
link: "text-primary underline-offset-4 hover:underline",
|
||||
destructive:
|
||||
"bg-destructive text-destructive-foreground hover:bg-destructive/90",
|
||||
},
|
||||
size: {
|
||||
default: "h-9 px-4 py-2",
|
||||
sm: "h-8 rounded-[6px] px-3 text-xs",
|
||||
lg: "h-10 rounded-[6px] px-8",
|
||||
sm: "h-8 px-3",
|
||||
md: "h-9 px-4",
|
||||
lg: "h-10 px-6",
|
||||
icon: "h-9 w-9",
|
||||
},
|
||||
},
|
||||
defaultVariants: {
|
||||
variant: "default",
|
||||
size: "default",
|
||||
size: "md",
|
||||
},
|
||||
}
|
||||
);
|
||||
|
||||
export interface ButtonProps
|
||||
extends React.ButtonHTMLAttributes<HTMLButtonElement>,
|
||||
VariantProps<typeof buttonVariants> {
|
||||
asChild?: boolean;
|
||||
}
|
||||
VariantProps<typeof buttonVariants> {}
|
||||
|
||||
const Button = React.forwardRef<HTMLButtonElement, ButtonProps>(
|
||||
({ className, variant, size, asChild = false, ...props }, ref) => {
|
||||
const Comp = asChild ? Slot : "button";
|
||||
({ className, variant, size, ...props }, ref) => {
|
||||
return (
|
||||
<Comp
|
||||
className={cn(buttonVariants({ variant, size, className }))}
|
||||
<button
|
||||
ref={ref}
|
||||
className={cn(buttonVariants({ variant, size }), className)}
|
||||
{...props}
|
||||
/>
|
||||
);
|
||||
|
||||
25
src/components/ui/checkbox.tsx
Normal file
25
src/components/ui/checkbox.tsx
Normal file
@@ -0,0 +1,25 @@
|
||||
import * as React from "react";
|
||||
import * as CheckboxPrimitive from "@radix-ui/react-checkbox";
|
||||
import { Check } from "lucide-react";
|
||||
import { cn } from "@/lib/utils";
|
||||
|
||||
const Checkbox = React.forwardRef<
|
||||
React.ElementRef<typeof CheckboxPrimitive.Root>,
|
||||
React.ComponentPropsWithoutRef<typeof CheckboxPrimitive.Root>
|
||||
>(({ className, ...props }, ref) => (
|
||||
<CheckboxPrimitive.Root
|
||||
ref={ref}
|
||||
className={cn(
|
||||
"peer h-4 w-4 shrink-0 rounded-sm border border-input bg-background shadow ring-offset-background focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 disabled:cursor-not-allowed disabled:opacity-50 data-[state=checked]:bg-primary data-[state=checked]:text-primary-foreground",
|
||||
className
|
||||
)}
|
||||
{...props}
|
||||
>
|
||||
<CheckboxPrimitive.Indicator className="flex items-center justify-center text-primary-foreground">
|
||||
<Check className="h-3.5 w-3.5" />
|
||||
</CheckboxPrimitive.Indicator>
|
||||
</CheckboxPrimitive.Root>
|
||||
));
|
||||
Checkbox.displayName = CheckboxPrimitive.Root.displayName;
|
||||
|
||||
export { Checkbox };
|
||||
108
src/components/ui/dialog.tsx
Normal file
108
src/components/ui/dialog.tsx
Normal file
@@ -0,0 +1,108 @@
|
||||
import * as React from "react";
|
||||
import * as DialogPrimitive from "@radix-ui/react-dialog";
|
||||
import { cn } from "@/lib/utils";
|
||||
|
||||
const Dialog = DialogPrimitive.Root;
|
||||
const DialogTrigger = DialogPrimitive.Trigger;
|
||||
const DialogPortal = DialogPrimitive.Portal;
|
||||
const DialogClose = DialogPrimitive.Close;
|
||||
|
||||
const DialogOverlay = React.forwardRef<
|
||||
React.ElementRef<typeof DialogPrimitive.Overlay>,
|
||||
React.ComponentPropsWithoutRef<typeof DialogPrimitive.Overlay>
|
||||
>(({ className, ...props }, ref) => (
|
||||
<DialogPrimitive.Overlay
|
||||
ref={ref}
|
||||
className={cn(
|
||||
"fixed inset-0 z-3000 bg-black/60 backdrop-blur-sm",
|
||||
className
|
||||
)}
|
||||
{...props}
|
||||
/>
|
||||
));
|
||||
DialogOverlay.displayName = DialogPrimitive.Overlay.displayName;
|
||||
|
||||
type DialogContentProps = React.ComponentPropsWithoutRef<typeof DialogPrimitive.Content> & {
|
||||
overlayClassName?: string;
|
||||
};
|
||||
|
||||
const DialogContent = React.forwardRef<
|
||||
React.ElementRef<typeof DialogPrimitive.Content>,
|
||||
DialogContentProps
|
||||
>(({ className, overlayClassName, ...props }, ref) => (
|
||||
<DialogPortal>
|
||||
<DialogOverlay className={overlayClassName} />
|
||||
<DialogPrimitive.Content
|
||||
ref={ref}
|
||||
className={cn(
|
||||
"fixed left-1/2 top-1/2 z-3001 w-full max-w-lg -translate-x-1/2 -translate-y-1/2 rounded-lg border border-input bg-background p-6 text-foreground shadow-lg focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 ring-offset-background",
|
||||
className
|
||||
)}
|
||||
{...props}
|
||||
/>
|
||||
</DialogPortal>
|
||||
));
|
||||
DialogContent.displayName = DialogPrimitive.Content.displayName;
|
||||
|
||||
const DialogHeader = ({
|
||||
className,
|
||||
...props
|
||||
}: React.HTMLAttributes<HTMLDivElement>) => (
|
||||
<div
|
||||
className={cn(
|
||||
"flex flex-col space-y-2 text-center sm:text-left",
|
||||
className
|
||||
)}
|
||||
{...props}
|
||||
/>
|
||||
);
|
||||
|
||||
const DialogFooter = ({
|
||||
className,
|
||||
...props
|
||||
}: React.HTMLAttributes<HTMLDivElement>) => (
|
||||
<div
|
||||
className={cn(
|
||||
"flex flex-col-reverse sm:flex-row sm:justify-end sm:space-x-2",
|
||||
className
|
||||
)}
|
||||
{...props}
|
||||
/>
|
||||
);
|
||||
|
||||
const DialogTitle = React.forwardRef<
|
||||
React.ElementRef<typeof DialogPrimitive.Title>,
|
||||
React.ComponentPropsWithoutRef<typeof DialogPrimitive.Title>
|
||||
>(({ className, ...props }, ref) => (
|
||||
<DialogPrimitive.Title
|
||||
ref={ref}
|
||||
className={cn("text-lg font-semibold", className)}
|
||||
{...props}
|
||||
/>
|
||||
));
|
||||
DialogTitle.displayName = DialogPrimitive.Title.displayName;
|
||||
|
||||
const DialogDescription = React.forwardRef<
|
||||
React.ElementRef<typeof DialogPrimitive.Description>,
|
||||
React.ComponentPropsWithoutRef<typeof DialogPrimitive.Description>
|
||||
>(({ className, ...props }, ref) => (
|
||||
<DialogPrimitive.Description
|
||||
ref={ref}
|
||||
className={cn("text-sm text-muted-foreground", className)}
|
||||
{...props}
|
||||
/>
|
||||
));
|
||||
DialogDescription.displayName = DialogPrimitive.Description.displayName;
|
||||
|
||||
export {
|
||||
Dialog,
|
||||
DialogTrigger,
|
||||
DialogPortal,
|
||||
DialogOverlay,
|
||||
DialogContent,
|
||||
DialogHeader,
|
||||
DialogFooter,
|
||||
DialogTitle,
|
||||
DialogDescription,
|
||||
DialogClose,
|
||||
};
|
||||
78
src/components/ui/dropdown-menu.tsx
Normal file
78
src/components/ui/dropdown-menu.tsx
Normal file
@@ -0,0 +1,78 @@
|
||||
import * as React from "react";
|
||||
import * as DropdownMenuPrimitive from "@radix-ui/react-dropdown-menu";
|
||||
import { cn } from "@/lib/utils";
|
||||
|
||||
const DropdownMenu = DropdownMenuPrimitive.Root;
|
||||
const DropdownMenuTrigger = DropdownMenuPrimitive.Trigger;
|
||||
|
||||
const DropdownMenuContent = React.forwardRef<
|
||||
React.ElementRef<typeof DropdownMenuPrimitive.Content>,
|
||||
React.ComponentPropsWithoutRef<typeof DropdownMenuPrimitive.Content>
|
||||
>(({ className, side = "bottom", align = "start", sideOffset = 8, ...props }, ref) => (
|
||||
<DropdownMenuPrimitive.Portal>
|
||||
<DropdownMenuPrimitive.Content
|
||||
ref={ref}
|
||||
side={side}
|
||||
align={align}
|
||||
sideOffset={sideOffset}
|
||||
className={cn(
|
||||
"z-50 min-w-40 rounded-lg border border-input bg-background p-1 text-foreground shadow-lg",
|
||||
className
|
||||
)}
|
||||
{...props}
|
||||
/>
|
||||
</DropdownMenuPrimitive.Portal>
|
||||
));
|
||||
DropdownMenuContent.displayName = DropdownMenuPrimitive.Content.displayName;
|
||||
|
||||
const DropdownMenuItem = React.forwardRef<
|
||||
React.ElementRef<typeof DropdownMenuPrimitive.Item>,
|
||||
React.ComponentPropsWithoutRef<typeof DropdownMenuPrimitive.Item>
|
||||
>(({ className, ...props }, ref) => (
|
||||
<DropdownMenuPrimitive.Item
|
||||
ref={ref}
|
||||
className={cn(
|
||||
"flex cursor-pointer select-none items-center rounded-md px-3 py-2 text-sm outline-none focus:bg-muted",
|
||||
className
|
||||
)}
|
||||
{...props}
|
||||
/>
|
||||
));
|
||||
DropdownMenuItem.displayName = DropdownMenuPrimitive.Item.displayName;
|
||||
|
||||
const DropdownMenuLabel = React.forwardRef<
|
||||
React.ElementRef<typeof DropdownMenuPrimitive.Label>,
|
||||
React.ComponentPropsWithoutRef<typeof DropdownMenuPrimitive.Label>
|
||||
>(({ className, ...props }, ref) => (
|
||||
<DropdownMenuPrimitive.Label
|
||||
ref={ref}
|
||||
className={cn("px-3 py-1 text-xs text-muted-foreground", className)}
|
||||
{...props}
|
||||
/>
|
||||
));
|
||||
DropdownMenuLabel.displayName = DropdownMenuPrimitive.Label.displayName;
|
||||
|
||||
const DropdownMenuSeparator = React.forwardRef<
|
||||
React.ElementRef<typeof DropdownMenuPrimitive.Separator>,
|
||||
React.ComponentPropsWithoutRef<typeof DropdownMenuPrimitive.Separator>
|
||||
>(({ className, ...props }, ref) => (
|
||||
<DropdownMenuPrimitive.Separator
|
||||
ref={ref}
|
||||
className={cn("my-1 h-px bg-muted", className)}
|
||||
{...props}
|
||||
/>
|
||||
));
|
||||
DropdownMenuSeparator.displayName = DropdownMenuPrimitive.Separator.displayName;
|
||||
|
||||
const DropdownMenuGroup = DropdownMenuPrimitive.Group;
|
||||
|
||||
export {
|
||||
DropdownMenu,
|
||||
DropdownMenuTrigger,
|
||||
DropdownMenuContent,
|
||||
DropdownMenuItem,
|
||||
DropdownMenuLabel,
|
||||
DropdownMenuSeparator,
|
||||
DropdownMenuGroup,
|
||||
};
|
||||
|
||||
24
src/components/ui/input.tsx
Normal file
24
src/components/ui/input.tsx
Normal file
@@ -0,0 +1,24 @@
|
||||
import * as React from "react";
|
||||
import { cn } from "@/lib/utils";
|
||||
|
||||
export interface InputProps
|
||||
extends React.InputHTMLAttributes<HTMLInputElement> {}
|
||||
|
||||
const Input = React.forwardRef<HTMLInputElement, InputProps>(
|
||||
({ className, type = "text", ...props }, ref) => {
|
||||
return (
|
||||
<input
|
||||
type={type}
|
||||
ref={ref}
|
||||
className={cn(
|
||||
"flex h-10 w-full rounded-md border border-input bg-background px-3 py-2 text-sm text-foreground shadow-sm transition-colors file:border-0 file:bg-transparent file:text-sm file:font-medium placeholder:text-muted-foreground focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 ring-offset-background disabled:cursor-not-allowed disabled:opacity-50",
|
||||
className
|
||||
)}
|
||||
{...props}
|
||||
/>
|
||||
);
|
||||
}
|
||||
);
|
||||
Input.displayName = "Input";
|
||||
|
||||
export { Input };
|
||||
21
src/components/ui/label.tsx
Normal file
21
src/components/ui/label.tsx
Normal file
@@ -0,0 +1,21 @@
|
||||
import * as React from "react";
|
||||
import * as LabelPrimitive from "@radix-ui/react-label";
|
||||
import { cn } from "@/lib/utils";
|
||||
|
||||
const Label = React.forwardRef<
|
||||
React.ElementRef<typeof LabelPrimitive.Root>,
|
||||
React.ComponentPropsWithoutRef<typeof LabelPrimitive.Root>
|
||||
>(({ className, ...props }, ref) => (
|
||||
<LabelPrimitive.Root
|
||||
ref={ref}
|
||||
className={cn(
|
||||
"text-sm font-medium leading-none peer-disabled:cursor-not-allowed peer-disabled:opacity-70",
|
||||
className
|
||||
)}
|
||||
{...props}
|
||||
/>
|
||||
));
|
||||
Label.displayName = LabelPrimitive.Root.displayName;
|
||||
|
||||
export { Label };
|
||||
|
||||
97
src/components/ui/multi-select.tsx
Normal file
97
src/components/ui/multi-select.tsx
Normal file
@@ -0,0 +1,97 @@
|
||||
import * as React from "react";
|
||||
import * as PopoverPrimitive from "@radix-ui/react-popover";
|
||||
import { cn } from "@/lib/utils";
|
||||
import { Checkbox } from "@/components/ui/checkbox";
|
||||
|
||||
type Option = { value: string; label: string };
|
||||
|
||||
export interface MultiSelectProps {
|
||||
options: Option[];
|
||||
value: string[];
|
||||
onChange?: (next: string[]) => void;
|
||||
placeholder?: string;
|
||||
className?: string;
|
||||
disabled?: boolean;
|
||||
}
|
||||
|
||||
export const MultiSelect: React.FC<MultiSelectProps> = ({
|
||||
options,
|
||||
value,
|
||||
onChange,
|
||||
placeholder = "",
|
||||
className,
|
||||
disabled,
|
||||
}) => {
|
||||
const [open, setOpen] = React.useState(false);
|
||||
const values = React.useMemo(() => new Set(value), [value]);
|
||||
|
||||
const toggle = (v: string) => {
|
||||
const next = new Set(values);
|
||||
if (next.has(v)) next.delete(v);
|
||||
else next.add(v);
|
||||
onChange?.(Array.from(next));
|
||||
};
|
||||
|
||||
const display = React.useMemo(() => {
|
||||
if (values.size === 0) return placeholder;
|
||||
const labels = options
|
||||
.filter((o) => values.has(o.value))
|
||||
.map((o) => o.label);
|
||||
return labels.join(", ");
|
||||
}, [options, values, placeholder]);
|
||||
|
||||
return (
|
||||
<PopoverPrimitive.Root open={open} onOpenChange={setOpen}>
|
||||
<PopoverPrimitive.Trigger asChild>
|
||||
<button
|
||||
type="button"
|
||||
disabled={disabled}
|
||||
className={cn(
|
||||
"flex h-10 w-full items-center justify-between rounded-md border border-input bg-background px-3 py-2 text-sm ring-offset-background placeholder:text-muted-foreground focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 disabled:cursor-not-allowed disabled:opacity-50",
|
||||
className
|
||||
)}
|
||||
>
|
||||
<span className={cn(values.size === 0 && "text-muted-foreground")}>{display}</span>
|
||||
<svg
|
||||
className="h-4 w-4 opacity-70"
|
||||
viewBox="0 0 24 24"
|
||||
fill="none"
|
||||
stroke="currentColor"
|
||||
strokeWidth="2"
|
||||
strokeLinecap="round"
|
||||
strokeLinejoin="round"
|
||||
>
|
||||
<path d="M6 9l6 6 6-6" />
|
||||
</svg>
|
||||
</button>
|
||||
</PopoverPrimitive.Trigger>
|
||||
<PopoverPrimitive.Content
|
||||
sideOffset={4}
|
||||
className={cn(
|
||||
"z-50 w-(--radix-popover-trigger-width) min-w-[220px] rounded-md border border-input bg-popover p-2 text-popover-foreground shadow-md outline-none",
|
||||
"ring-offset-background focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2"
|
||||
)}
|
||||
>
|
||||
<div className="max-h-48 overflow-y-auto space-y-1">
|
||||
{options.map((opt) => {
|
||||
const checked = values.has(opt.value) ? "checked" : "unchecked";
|
||||
return (
|
||||
<label
|
||||
key={opt.value}
|
||||
className="flex cursor-pointer items-center gap-2 rounded-sm px-2 py-1 hover:bg-accent hover:text-accent-foreground"
|
||||
onClick={(e) => {
|
||||
e.preventDefault();
|
||||
toggle(opt.value);
|
||||
}}
|
||||
>
|
||||
<Checkbox checked={checked === "checked"} className="h-4 w-4" />
|
||||
<span className="text-sm">{opt.label}</span>
|
||||
</label>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
</PopoverPrimitive.Content>
|
||||
</PopoverPrimitive.Root>
|
||||
);
|
||||
};
|
||||
|
||||
43
src/components/ui/popover.tsx
Normal file
43
src/components/ui/popover.tsx
Normal file
@@ -0,0 +1,43 @@
|
||||
import * as React from "react";
|
||||
import * as PopoverPrimitive from "@radix-ui/react-popover";
|
||||
import { cn } from "@/lib/utils";
|
||||
|
||||
const Popover = PopoverPrimitive.Root;
|
||||
const PopoverTrigger = PopoverPrimitive.Trigger;
|
||||
const PopoverPortal = PopoverPrimitive.Portal;
|
||||
|
||||
const PopoverContent = React.forwardRef<
|
||||
React.ElementRef<typeof PopoverPrimitive.Content>,
|
||||
React.ComponentPropsWithoutRef<typeof PopoverPrimitive.Content> & {
|
||||
panelId?: string;
|
||||
}
|
||||
>(
|
||||
(
|
||||
{
|
||||
className,
|
||||
panelId,
|
||||
side = "bottom",
|
||||
align = "start",
|
||||
sideOffset = 8,
|
||||
...props
|
||||
},
|
||||
ref
|
||||
) => (
|
||||
<PopoverPrimitive.Content
|
||||
ref={ref}
|
||||
side={side}
|
||||
align={align}
|
||||
sideOffset={sideOffset}
|
||||
className={cn(
|
||||
"z-50 rounded-lg border border-input bg-background p-1 text-foreground shadow-lg outline-none",
|
||||
className
|
||||
)}
|
||||
data-popover-panel
|
||||
id={panelId}
|
||||
{...props}
|
||||
/>
|
||||
)
|
||||
);
|
||||
PopoverContent.displayName = "PopoverContent";
|
||||
|
||||
export { Popover, PopoverTrigger, PopoverContent, PopoverPortal };
|
||||
160
src/components/ui/select.tsx
Normal file
160
src/components/ui/select.tsx
Normal file
@@ -0,0 +1,160 @@
|
||||
import * as React from "react"
|
||||
import * as SelectPrimitive from "@radix-ui/react-select"
|
||||
import { Check, ChevronDown, ChevronUp } from "lucide-react"
|
||||
|
||||
import { cn } from "@/lib/utils"
|
||||
|
||||
const Select = SelectPrimitive.Root
|
||||
|
||||
const SelectGroup = SelectPrimitive.Group
|
||||
|
||||
const SelectValue = SelectPrimitive.Value
|
||||
|
||||
const SelectTrigger = React.forwardRef<
|
||||
React.ElementRef<typeof SelectPrimitive.Trigger>,
|
||||
React.ComponentPropsWithoutRef<typeof SelectPrimitive.Trigger>
|
||||
>(({ className, children, ...props }, ref) => (
|
||||
<SelectPrimitive.Trigger
|
||||
ref={ref}
|
||||
className={cn(
|
||||
"flex h-9 w-full items-center justify-between whitespace-nowrap rounded-md border border-input bg-transparent px-3 py-2 text-sm shadow-sm ring-offset-background data-placeholder:text-muted-foreground focus:outline-none focus:ring-1 focus:ring-ring disabled:cursor-not-allowed disabled:opacity-50 [&>span]:line-clamp-1",
|
||||
className
|
||||
)}
|
||||
{...props}
|
||||
>
|
||||
{children}
|
||||
<SelectPrimitive.Icon asChild>
|
||||
<ChevronDown className="h-4 w-4 opacity-50" />
|
||||
</SelectPrimitive.Icon>
|
||||
</SelectPrimitive.Trigger>
|
||||
))
|
||||
SelectTrigger.displayName = SelectPrimitive.Trigger.displayName
|
||||
|
||||
const SelectScrollUpButton = React.forwardRef<
|
||||
React.ElementRef<typeof SelectPrimitive.ScrollUpButton>,
|
||||
React.ComponentPropsWithoutRef<typeof SelectPrimitive.ScrollUpButton>
|
||||
>(({ className, ...props }, ref) => (
|
||||
<SelectPrimitive.ScrollUpButton
|
||||
ref={ref}
|
||||
className={cn(
|
||||
"flex cursor-default items-center justify-center py-1",
|
||||
className
|
||||
)}
|
||||
{...props}
|
||||
>
|
||||
<ChevronUp className="h-4 w-4" />
|
||||
</SelectPrimitive.ScrollUpButton>
|
||||
))
|
||||
SelectScrollUpButton.displayName = SelectPrimitive.ScrollUpButton.displayName
|
||||
|
||||
const SelectScrollDownButton = React.forwardRef<
|
||||
React.ElementRef<typeof SelectPrimitive.ScrollDownButton>,
|
||||
React.ComponentPropsWithoutRef<typeof SelectPrimitive.ScrollDownButton>
|
||||
>(({ className, ...props }, ref) => (
|
||||
<SelectPrimitive.ScrollDownButton
|
||||
ref={ref}
|
||||
className={cn(
|
||||
"flex cursor-default items-center justify-center py-1",
|
||||
className
|
||||
)}
|
||||
{...props}
|
||||
>
|
||||
<ChevronDown className="h-4 w-4" />
|
||||
</SelectPrimitive.ScrollDownButton>
|
||||
))
|
||||
SelectScrollDownButton.displayName =
|
||||
SelectPrimitive.ScrollDownButton.displayName
|
||||
|
||||
const SelectContent = React.forwardRef<
|
||||
React.ElementRef<typeof SelectPrimitive.Content>,
|
||||
React.ComponentPropsWithoutRef<typeof SelectPrimitive.Content>
|
||||
>(({ className, children, position = "popper", side = "bottom", avoidCollisions = false, sideOffset = 4, ...props }, ref) => (
|
||||
<SelectPrimitive.Portal>
|
||||
<SelectPrimitive.Content
|
||||
ref={ref}
|
||||
className={cn(
|
||||
"relative z-50 max-h-[var(--radix-select-content-available-height)] min-w-[var(--radix-select-trigger-width)] w-[var(--radix-select-trigger-width)] overflow-hidden rounded-md border border-input bg-popover text-popover-foreground shadow-md data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2 origin-[var(--radix-select-content-transform-origin)]",
|
||||
position === "popper" &&
|
||||
"data-[side=bottom]:translate-y-1 data-[side=left]:-translate-x-1 data-[side=right]:translate-x-1 data-[side=top]:-translate-y-1",
|
||||
className
|
||||
)}
|
||||
position={position}
|
||||
side={side}
|
||||
avoidCollisions={avoidCollisions}
|
||||
sideOffset={sideOffset}
|
||||
{...props}
|
||||
>
|
||||
<SelectScrollUpButton />
|
||||
<SelectPrimitive.Viewport
|
||||
className={cn(
|
||||
"p-1 max-h-[50vh] overflow-y-auto",
|
||||
position === "popper" &&
|
||||
"w-full min-w-[var(--radix-select-trigger-width)]"
|
||||
)}
|
||||
>
|
||||
{children}
|
||||
</SelectPrimitive.Viewport>
|
||||
<SelectScrollDownButton />
|
||||
</SelectPrimitive.Content>
|
||||
</SelectPrimitive.Portal>
|
||||
))
|
||||
SelectContent.displayName = SelectPrimitive.Content.displayName
|
||||
|
||||
const SelectLabel = React.forwardRef<
|
||||
React.ElementRef<typeof SelectPrimitive.Label>,
|
||||
React.ComponentPropsWithoutRef<typeof SelectPrimitive.Label>
|
||||
>(({ className, ...props }, ref) => (
|
||||
<SelectPrimitive.Label
|
||||
ref={ref}
|
||||
className={cn("px-2 py-1.5 text-sm font-semibold", className)}
|
||||
{...props}
|
||||
/>
|
||||
))
|
||||
SelectLabel.displayName = SelectPrimitive.Label.displayName
|
||||
|
||||
const SelectItem = React.forwardRef<
|
||||
React.ElementRef<typeof SelectPrimitive.Item>,
|
||||
React.ComponentPropsWithoutRef<typeof SelectPrimitive.Item>
|
||||
>(({ className, children, ...props }, ref) => (
|
||||
<SelectPrimitive.Item
|
||||
ref={ref}
|
||||
className={cn(
|
||||
"relative flex w-full cursor-default select-none items-center rounded-sm py-1.5 pl-2 pr-8 text-sm outline-none focus:bg-accent focus:text-accent-foreground data-disabled:pointer-events-none data-disabled:opacity-50",
|
||||
className
|
||||
)}
|
||||
{...props}
|
||||
>
|
||||
<span className="absolute right-2 flex h-3.5 w-3.5 items-center justify-center">
|
||||
<SelectPrimitive.ItemIndicator>
|
||||
<Check className="h-4 w-4" />
|
||||
</SelectPrimitive.ItemIndicator>
|
||||
</span>
|
||||
<SelectPrimitive.ItemText>{children}</SelectPrimitive.ItemText>
|
||||
</SelectPrimitive.Item>
|
||||
))
|
||||
SelectItem.displayName = SelectPrimitive.Item.displayName
|
||||
|
||||
const SelectSeparator = React.forwardRef<
|
||||
React.ElementRef<typeof SelectPrimitive.Separator>,
|
||||
React.ComponentPropsWithoutRef<typeof SelectPrimitive.Separator>
|
||||
>(({ className, ...props }, ref) => (
|
||||
<SelectPrimitive.Separator
|
||||
ref={ref}
|
||||
className={cn("-mx-1 my-1 h-px bg-muted", className)}
|
||||
{...props}
|
||||
/>
|
||||
))
|
||||
SelectSeparator.displayName = SelectPrimitive.Separator.displayName
|
||||
|
||||
export {
|
||||
Select,
|
||||
SelectGroup,
|
||||
SelectValue,
|
||||
SelectTrigger,
|
||||
SelectContent,
|
||||
SelectLabel,
|
||||
SelectItem,
|
||||
SelectSeparator,
|
||||
SelectScrollUpButton,
|
||||
SelectScrollDownButton,
|
||||
}
|
||||
@@ -1,29 +1,24 @@
|
||||
import * as React from "react"
|
||||
import * as SeparatorPrimitive from "@radix-ui/react-separator"
|
||||
|
||||
import { cn } from "@/lib/utils"
|
||||
import * as React from "react";
|
||||
import * as SeparatorPrimitive from "@radix-ui/react-separator";
|
||||
import { cn } from "@/lib/utils";
|
||||
|
||||
const Separator = React.forwardRef<
|
||||
React.ElementRef<typeof SeparatorPrimitive.Root>,
|
||||
React.ComponentPropsWithoutRef<typeof SeparatorPrimitive.Root>
|
||||
>(
|
||||
(
|
||||
{ className, orientation = "horizontal", decorative = true, ...props },
|
||||
ref
|
||||
) => (
|
||||
<SeparatorPrimitive.Root
|
||||
ref={ref}
|
||||
decorative={decorative}
|
||||
orientation={orientation}
|
||||
className={cn(
|
||||
"shrink-0 bg-border",
|
||||
orientation === "horizontal" ? "h-[1px] w-full" : "h-full w-[1px]",
|
||||
className
|
||||
)}
|
||||
{...props}
|
||||
/>
|
||||
)
|
||||
)
|
||||
Separator.displayName = SeparatorPrimitive.Root.displayName
|
||||
>(({ className, orientation = "horizontal", decorative, ...props }, ref) => (
|
||||
<SeparatorPrimitive.Root
|
||||
ref={ref}
|
||||
decorative={decorative}
|
||||
orientation={orientation}
|
||||
className={cn(
|
||||
"shrink-0 bg-border",
|
||||
orientation === "horizontal" ? "h-px w-full" : "h-full w-px",
|
||||
className
|
||||
)}
|
||||
{...props}
|
||||
/>
|
||||
));
|
||||
Separator.displayName = SeparatorPrimitive.Root.displayName;
|
||||
|
||||
export { Separator };
|
||||
|
||||
export { Separator }
|
||||
|
||||
27
src/components/ui/slider.tsx
Normal file
27
src/components/ui/slider.tsx
Normal file
@@ -0,0 +1,27 @@
|
||||
import * as React from "react";
|
||||
import * as SliderPrimitive from "@radix-ui/react-slider";
|
||||
import { cn } from "@/lib/utils";
|
||||
|
||||
const Slider = React.forwardRef<
|
||||
React.ElementRef<typeof SliderPrimitive.Root>,
|
||||
React.ComponentPropsWithoutRef<typeof SliderPrimitive.Root>
|
||||
>(({ className, ...props }, ref) => (
|
||||
<SliderPrimitive.Root
|
||||
ref={ref}
|
||||
className={cn(
|
||||
"relative flex w-full touch-none select-none items-center",
|
||||
className
|
||||
)}
|
||||
{...props}
|
||||
>
|
||||
<SliderPrimitive.Track className="relative h-1.5 w-full grow overflow-hidden rounded-full bg-secondary">
|
||||
<SliderPrimitive.Range className="absolute h-full bg-primary" />
|
||||
</SliderPrimitive.Track>
|
||||
<SliderPrimitive.Thumb className="block h-4 w-4 rounded-full border-2 border-primary bg-background ring-offset-background transition-colors focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 disabled:pointer-events-none disabled:opacity-50" />
|
||||
</SliderPrimitive.Root>
|
||||
));
|
||||
|
||||
Slider.displayName = SliderPrimitive.Root.displayName;
|
||||
|
||||
export { Slider };
|
||||
|
||||
27
src/components/ui/switch.tsx
Normal file
27
src/components/ui/switch.tsx
Normal file
@@ -0,0 +1,27 @@
|
||||
import * as React from "react"
|
||||
import * as SwitchPrimitives from "@radix-ui/react-switch"
|
||||
|
||||
import { cn } from "@/lib/utils"
|
||||
|
||||
const Switch = React.forwardRef<
|
||||
React.ElementRef<typeof SwitchPrimitives.Root>,
|
||||
React.ComponentPropsWithoutRef<typeof SwitchPrimitives.Root>
|
||||
>(({ className, ...props }, ref) => (
|
||||
<SwitchPrimitives.Root
|
||||
className={cn(
|
||||
"peer inline-flex h-5 w-9 shrink-0 cursor-pointer items-center rounded-full border-2 border-transparent transition-colors focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 focus-visible:ring-offset-background disabled:cursor-not-allowed disabled:opacity-50 data-[state=checked]:bg-primary data-[state=unchecked]:bg-input",
|
||||
className
|
||||
)}
|
||||
{...props}
|
||||
ref={ref}
|
||||
>
|
||||
<SwitchPrimitives.Thumb
|
||||
className={cn(
|
||||
"pointer-events-none block h-4 w-4 rounded-full bg-white shadow ring-0 transition-transform data-[state=checked]:translate-x-4 data-[state=unchecked]:translate-x-0"
|
||||
)}
|
||||
/>
|
||||
</SwitchPrimitives.Root>
|
||||
))
|
||||
Switch.displayName = SwitchPrimitives.Root.displayName
|
||||
|
||||
export { Switch }
|
||||
53
src/components/ui/tabs.tsx
Normal file
53
src/components/ui/tabs.tsx
Normal file
@@ -0,0 +1,53 @@
|
||||
import * as React from "react";
|
||||
import * as TabsPrimitive from "@radix-ui/react-tabs";
|
||||
|
||||
import { cn } from "@/lib/utils";
|
||||
|
||||
const Tabs = TabsPrimitive.Root;
|
||||
|
||||
const TabsList = React.forwardRef<
|
||||
React.ElementRef<typeof TabsPrimitive.List>,
|
||||
React.ComponentPropsWithoutRef<typeof TabsPrimitive.List>
|
||||
>(({ className, ...props }, ref) => (
|
||||
<TabsPrimitive.List
|
||||
ref={ref}
|
||||
className={cn(
|
||||
"inline-flex h-9 items-center justify-center rounded-lg bg-muted p-1 text-muted-foreground",
|
||||
className
|
||||
)}
|
||||
{...props}
|
||||
/>
|
||||
));
|
||||
TabsList.displayName = TabsPrimitive.List.displayName;
|
||||
|
||||
const TabsTrigger = React.forwardRef<
|
||||
React.ElementRef<typeof TabsPrimitive.Trigger>,
|
||||
React.ComponentPropsWithoutRef<typeof TabsPrimitive.Trigger>
|
||||
>(({ className, ...props }, ref) => (
|
||||
<TabsPrimitive.Trigger
|
||||
ref={ref}
|
||||
className={cn(
|
||||
"inline-flex items-center justify-center whitespace-nowrap rounded-md px-3 py-1 text-sm font-medium ring-offset-background transition-all focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 disabled:pointer-events-none disabled:opacity-50 data-[state=active]:bg-background data-[state=active]:text-foreground data-[state=active]:shadow",
|
||||
className
|
||||
)}
|
||||
{...props}
|
||||
/>
|
||||
));
|
||||
TabsTrigger.displayName = TabsPrimitive.Trigger.displayName;
|
||||
|
||||
const TabsContent = React.forwardRef<
|
||||
React.ElementRef<typeof TabsPrimitive.Content>,
|
||||
React.ComponentPropsWithoutRef<typeof TabsPrimitive.Content>
|
||||
>(({ className, ...props }, ref) => (
|
||||
<TabsPrimitive.Content
|
||||
ref={ref}
|
||||
className={cn(
|
||||
"mt-2 ring-offset-background focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2",
|
||||
className
|
||||
)}
|
||||
{...props}
|
||||
/>
|
||||
));
|
||||
TabsContent.displayName = TabsPrimitive.Content.displayName;
|
||||
|
||||
export { Tabs, TabsList, TabsTrigger, TabsContent };
|
||||
@@ -1,4 +1,4 @@
|
||||
export const POPOVER_PANEL_SELECTOR = '[id^="headlessui-popover-panel"]';
|
||||
export const POPOVER_PANEL_SELECTOR = '[data-popover-panel]';
|
||||
|
||||
export const HISTORY_PANEL_ID = "headlessui-popover-panel:history-panel";
|
||||
|
||||
|
||||
301
src/main.css
301
src/main.css
@@ -1,8 +1,14 @@
|
||||
@tailwind base;
|
||||
@tailwind components;
|
||||
@tailwind utilities;
|
||||
@import "tailwindcss";
|
||||
/* Tailwind v4: ensure class extraction scans our source files */
|
||||
@source "../index.html";
|
||||
@source "./**/*.{ts,tsx}";
|
||||
|
||||
/* Tailwind v4 custom variant for dark mode */
|
||||
@custom-variant dark (&:where(.dark, .dark *));
|
||||
@custom-variant dark (&:where([data-theme="dark"], [data-theme="dark"] *));
|
||||
|
||||
/* Base variables */
|
||||
.coco-container,
|
||||
:root {
|
||||
--spacing-base: 12px;
|
||||
--modal-width: 560px;
|
||||
@@ -11,13 +17,126 @@
|
||||
--hit-height: 56px;
|
||||
--footer-height: 44px;
|
||||
--icon-stroke-width: 1.4;
|
||||
--background: #ffffff;
|
||||
--foreground: #09090b;
|
||||
--border: #e3e3e7;
|
||||
--coco-primary-color: rgb(149, 5, 153);
|
||||
/* Default coco light extras to ensure availability when data-theme="auto" */
|
||||
--coco-text-color: rgb(28, 30, 33);
|
||||
--coco-muted-color: rgb(150, 159, 175);
|
||||
--coco-modal-container-background: rgba(101, 108, 133, 0.8);
|
||||
--coco-modal-background: rgb(245, 246, 247);
|
||||
--coco-modal-shadow: inset 1px 1px 0 0 rgba(255, 255, 255, 0.5),
|
||||
0 3px 8px 0 rgba(85, 90, 100, 1);
|
||||
--coco-searchbox-background: rgb(235, 237, 240);
|
||||
--coco-searchbox-focus-background: #fff;
|
||||
--coco-hit-color: rgb(68, 73, 80);
|
||||
--coco-hit-active-color: #fff;
|
||||
--coco-hit-background: #fff;
|
||||
--coco-hit-shadow: 0 1px 3px 0 rgb(212, 217, 225);
|
||||
--coco-key-gradient: linear-gradient(
|
||||
-225deg,
|
||||
rgb(213, 219, 228) 0%,
|
||||
rgb(248, 248, 248) 100%
|
||||
);
|
||||
--coco-key-shadow: inset 0 -2px 0 0 rgb(205, 205, 230), inset 0 0 1px 1px #fff,
|
||||
0 1px 2px 1px rgba(30, 35, 90, 0.4);
|
||||
--coco-footer-background: #fff;
|
||||
--coco-footer-shadow: 0 -1px 0 0 rgb(224, 227, 232),
|
||||
0 -3px 6px 0 rgba(69, 98, 155, 0.12);
|
||||
--coco-icon-color: rgb(21, 21, 21);
|
||||
|
||||
/* Theme tokens using oklch color space */
|
||||
--radius: 0.65rem;
|
||||
/* shadcn blue theme (light) */
|
||||
--background: hsl(0 0% 100%);
|
||||
--foreground: hsl(222.2 84% 4.9%);
|
||||
--card: hsl(0 0% 100%);
|
||||
--card-foreground: hsl(222.2 84% 4.9%);
|
||||
--popover: hsl(0 0% 100%);
|
||||
--popover-foreground: hsl(222.2 84% 4.9%);
|
||||
--primary: hsl(221.2 83.2% 53.3%);
|
||||
--primary-foreground: hsl(210 40% 98%);
|
||||
--secondary: hsl(210 40% 96.1%);
|
||||
--secondary-foreground: hsl(222.2 47.4% 11.2%);
|
||||
--muted: hsl(210 40% 96.1%);
|
||||
--muted-foreground: hsl(215.4 16.3% 46.9%);
|
||||
--accent: hsl(210 40% 96.1%);
|
||||
--accent-foreground: hsl(222.2 47.4% 11.2%);
|
||||
--destructive: hsl(0 84.2% 60.2%);
|
||||
--border: hsl(214.3 31.8% 91.4%);
|
||||
--input: hsl(214.3 31.8% 91.4%);
|
||||
--ring: hsl(221.2 83.2% 53.3%);
|
||||
--chart-1: oklch(0.809 0.105 251.813);
|
||||
--chart-2: oklch(0.623 0.214 259.815);
|
||||
--chart-3: oklch(0.546 0.245 262.881);
|
||||
--chart-4: oklch(0.488 0.243 264.376);
|
||||
--chart-5: oklch(0.424 0.199 265.638);
|
||||
--sidebar: oklch(0.985 0 0);
|
||||
--sidebar-foreground: oklch(0.141 0.005 285.823);
|
||||
--sidebar-primary: oklch(0.546 0.245 262.881);
|
||||
--sidebar-primary-foreground: oklch(0.97 0.014 254.604);
|
||||
--sidebar-accent: oklch(0.967 0.001 286.375);
|
||||
--sidebar-accent-foreground: oklch(0.21 0.006 285.885);
|
||||
--sidebar-border: oklch(0.92 0.004 286.32);
|
||||
--sidebar-ring: oklch(0.708 0 0);
|
||||
}
|
||||
|
||||
/* Light theme */
|
||||
@theme {
|
||||
/* Map tokens directly; they are oklch(...) or other full values */
|
||||
--color-background: var(--background);
|
||||
--color-foreground: var(--foreground);
|
||||
--color-card: var(--card);
|
||||
--color-card-foreground: var(--card-foreground);
|
||||
--color-popover: var(--popover);
|
||||
--color-popover-foreground: var(--popover-foreground);
|
||||
--color-primary: var(--primary);
|
||||
--color-primary-foreground: var(--primary-foreground);
|
||||
--color-secondary: var(--secondary);
|
||||
--color-secondary-foreground: var(--secondary-foreground);
|
||||
--color-muted: var(--muted);
|
||||
--color-muted-foreground: var(--muted-foreground);
|
||||
--color-accent: var(--accent);
|
||||
--color-accent-foreground: var(--accent-foreground);
|
||||
--color-destructive: var(--destructive);
|
||||
--color-destructive-foreground: var(--destructive-foreground);
|
||||
--color-border: var(--border);
|
||||
--color-input: var(--input);
|
||||
--color-ring: var(--ring);
|
||||
--color-chart-1: var(--chart-1);
|
||||
--color-chart-2: var(--chart-2);
|
||||
--color-chart-3: var(--chart-3);
|
||||
--color-chart-4: var(--chart-4);
|
||||
--color-chart-5: var(--chart-5);
|
||||
}
|
||||
|
||||
#searchChat-container {
|
||||
/* Map tokens directly; they are oklch(...) or other full values */
|
||||
--color-background: var(--background);
|
||||
--color-foreground: var(--foreground);
|
||||
--color-card: var(--card);
|
||||
--color-card-foreground: var(--card-foreground);
|
||||
--color-popover: var(--popover);
|
||||
--color-popover-foreground: var(--popover-foreground);
|
||||
--color-primary: var(--primary);
|
||||
--color-primary-foreground: var(--primary-foreground);
|
||||
--color-secondary: var(--secondary);
|
||||
--color-secondary-foreground: var(--secondary-foreground);
|
||||
--color-muted: var(--muted);
|
||||
--color-muted-foreground: var(--muted-foreground);
|
||||
--color-accent: var(--accent);
|
||||
--color-accent-foreground: var(--accent-foreground);
|
||||
--color-destructive: var(--destructive);
|
||||
--color-destructive-foreground: var(--destructive-foreground);
|
||||
--color-border: var(--border);
|
||||
--color-input: var(--input);
|
||||
--color-ring: var(--ring);
|
||||
--color-chart-1: var(--chart-1);
|
||||
--color-chart-2: var(--chart-2);
|
||||
--color-chart-3: var(--chart-3);
|
||||
--color-chart-4: var(--chart-4);
|
||||
--color-chart-5: var(--chart-5);
|
||||
}
|
||||
|
||||
/* Light theme extras for coco-specific tokens */
|
||||
.light.coco-container,
|
||||
[data-theme="light"] {
|
||||
--coco-primary-color: rgb(149, 5, 153);
|
||||
--coco-text-color: rgb(28, 30, 33);
|
||||
@@ -45,12 +164,45 @@
|
||||
--coco-icon-color: rgb(21, 21, 21);
|
||||
}
|
||||
|
||||
/* Dark theme */
|
||||
/* Dark theme tokens mapped for both `.dark` class and `[data-theme="dark"]` */
|
||||
|
||||
.dark.coco-container,
|
||||
[data-theme="dark"] {
|
||||
/* shadcn blue theme (dark) */
|
||||
--background: hsl(222.2 84% 4.9%);
|
||||
--foreground: hsl(210 40% 98%);
|
||||
--card: hsl(222.2 84% 4.9%);
|
||||
--card-foreground: hsl(210 40% 98%);
|
||||
--popover: hsl(222.2 84% 4.9%);
|
||||
--popover-foreground: hsl(210 40% 98%);
|
||||
--primary: hsl(221.2 83.2% 53.3%);
|
||||
--primary-foreground: hsl(210 40% 98%);
|
||||
--secondary: hsl(217.2 32.6% 17.5%);
|
||||
--secondary-foreground: hsl(210 40% 98%);
|
||||
--muted: hsl(217.2 32.6% 17.5%);
|
||||
--muted-foreground: hsl(215 20.2% 65.1%);
|
||||
--accent: hsl(217.2 32.6% 17.5%);
|
||||
--accent-foreground: hsl(210 40% 98%);
|
||||
--destructive: hsl(0 62.8% 30.6%);
|
||||
--border: hsl(217.2 32.6% 17.5%);
|
||||
--input: hsl(217.2 32.6% 17.5%);
|
||||
--ring: hsl(221.2 83.2% 53.3%);
|
||||
--chart-1: oklch(0.809 0.105 251.813);
|
||||
--chart-2: oklch(0.623 0.214 259.815);
|
||||
--chart-3: oklch(0.546 0.245 262.881);
|
||||
--chart-4: oklch(0.488 0.243 264.376);
|
||||
--chart-5: oklch(0.424 0.199 265.638);
|
||||
--sidebar: oklch(0.21 0.006 285.885);
|
||||
--sidebar-foreground: oklch(0.985 0 0);
|
||||
--sidebar-primary: oklch(0.623 0.214 259.815);
|
||||
--sidebar-primary-foreground: oklch(0.97 0.014 254.604);
|
||||
--sidebar-accent: oklch(0.274 0.006 286.033);
|
||||
--sidebar-accent-foreground: oklch(0.985 0 0);
|
||||
--sidebar-border: oklch(1 0 0 / 10%);
|
||||
--sidebar-ring: oklch(0.439 0 0);
|
||||
|
||||
/* coco dark extras */
|
||||
--coco-primary-color: rgb(149, 5, 153);
|
||||
--background: #09090b;
|
||||
--foreground: #f9f9f9;
|
||||
--border: #27272a;
|
||||
--coco-text-color: rgb(245, 246, 247);
|
||||
--coco-modal-container-background: rgba(9, 10, 17, 0.8);
|
||||
--coco-modal-background: rgb(21, 23, 42);
|
||||
@@ -75,137 +227,76 @@
|
||||
--coco-icon-color: rgb(255, 255, 255);
|
||||
}
|
||||
|
||||
/* Base styles */
|
||||
/* Base styles (scoped to coco container to avoid global overrides) */
|
||||
@layer base {
|
||||
* {
|
||||
@apply box-border border-[--border] outline-none;
|
||||
}
|
||||
|
||||
html {
|
||||
@apply h-full overscroll-none select-none;
|
||||
@apply overscroll-none;
|
||||
}
|
||||
|
||||
body,
|
||||
#root {
|
||||
@apply h-full text-gray-900 antialiased;
|
||||
.coco-container * {
|
||||
@apply box-border outline-none;
|
||||
}
|
||||
|
||||
.dark body,
|
||||
.dark #root {
|
||||
@apply text-gray-100;
|
||||
.coco-container {
|
||||
@apply antialiased rounded-xl text-foreground;
|
||||
}
|
||||
|
||||
.input-body {
|
||||
@apply rounded-[6px] overflow-hidden;
|
||||
.coco-container .input-body {
|
||||
@apply rounded-xl overflow-hidden;
|
||||
}
|
||||
|
||||
.icon {
|
||||
.coco-container .icon {
|
||||
width: 1em;
|
||||
height: 1em;
|
||||
vertical-align: -0.15em;
|
||||
fill: currentColor;
|
||||
overflow: hidden;
|
||||
}
|
||||
:root {
|
||||
--background: 0 0% 100%;
|
||||
--foreground: 0 0% 3.9%;
|
||||
--card: 0 0% 100%;
|
||||
--card-foreground: 0 0% 3.9%;
|
||||
--popover: 0 0% 100%;
|
||||
--popover-foreground: 0 0% 3.9%;
|
||||
--primary: 0 0% 9%;
|
||||
--primary-foreground: 0 0% 98%;
|
||||
--secondary: 0 0% 96.1%;
|
||||
--secondary-foreground: 0 0% 9%;
|
||||
--muted: 0 0% 96.1%;
|
||||
--muted-foreground: 0 0% 45.1%;
|
||||
--accent: 0 0% 96.1%;
|
||||
--accent-foreground: 0 0% 9%;
|
||||
--destructive: 0 84.2% 60.2%;
|
||||
--destructive-foreground: 0 0% 98%;
|
||||
--border: 0 0% 89.8%;
|
||||
--input: 0 0% 89.8%;
|
||||
--ring: 0 0% 3.9%;
|
||||
--chart-1: 12 76% 61%;
|
||||
--chart-2: 173 58% 39%;
|
||||
--chart-3: 197 37% 24%;
|
||||
--chart-4: 43 74% 66%;
|
||||
--chart-5: 27 87% 67%;
|
||||
--radius: 0.5rem;
|
||||
}
|
||||
.dark {
|
||||
--background: 0 0% 3.9%;
|
||||
--foreground: 0 0% 98%;
|
||||
--card: 0 0% 3.9%;
|
||||
--card-foreground: 0 0% 98%;
|
||||
--popover: 0 0% 3.9%;
|
||||
--popover-foreground: 0 0% 98%;
|
||||
--primary: 0 0% 98%;
|
||||
--primary-foreground: 0 0% 9%;
|
||||
--secondary: 0 0% 14.9%;
|
||||
--secondary-foreground: 0 0% 98%;
|
||||
--muted: 0 0% 14.9%;
|
||||
--muted-foreground: 0 0% 63.9%;
|
||||
--accent: 0 0% 14.9%;
|
||||
--accent-foreground: 0 0% 98%;
|
||||
--destructive: 0 62.8% 30.6%;
|
||||
--destructive-foreground: 0 0% 98%;
|
||||
--border: 0 0% 14.9%;
|
||||
--input: 0 0% 14.9%;
|
||||
--ring: 0 0% 83.1%;
|
||||
--chart-1: 220 70% 50%;
|
||||
--chart-2: 160 60% 45%;
|
||||
--chart-3: 30 80% 55%;
|
||||
--chart-4: 280 65% 60%;
|
||||
--chart-5: 340 75% 55%;
|
||||
}
|
||||
}
|
||||
|
||||
/* Component styles */
|
||||
@layer components {
|
||||
.settings-input {
|
||||
@apply block w-full rounded-[6px] 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
|
||||
@apply block w-full rounded-md border border-border
|
||||
bg-background text-foreground
|
||||
shadow-sm focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring
|
||||
transition-colors duration-200;
|
||||
}
|
||||
|
||||
.settings-select {
|
||||
@apply text-sm rounded-[6px] 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
|
||||
@apply text-sm rounded-md border border-border
|
||||
bg-background text-foreground
|
||||
shadow-sm focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring
|
||||
transition-colors duration-200;
|
||||
}
|
||||
}
|
||||
|
||||
/* Utility styles */
|
||||
/* Utility styles (scoped to coco container where reasonable) */
|
||||
@layer utilities {
|
||||
/* Fallback for Tailwind v4 class extraction edge-cases: ensure rounded-xl exists */
|
||||
.rounded-xl {
|
||||
border-radius: 0.75rem;
|
||||
}
|
||||
/* Scrollbar styles */
|
||||
.custom-scrollbar {
|
||||
.coco-container .custom-scrollbar {
|
||||
scrollbar-width: thin;
|
||||
scrollbar-color: #cbd5e1 transparent;
|
||||
}
|
||||
|
||||
.custom-scrollbar::-webkit-scrollbar {
|
||||
.coco-container .custom-scrollbar::-webkit-scrollbar {
|
||||
width: 6px;
|
||||
}
|
||||
|
||||
.custom-scrollbar::-webkit-scrollbar-track {
|
||||
.coco-container .custom-scrollbar::-webkit-scrollbar-track {
|
||||
background: transparent;
|
||||
}
|
||||
|
||||
.custom-scrollbar::-webkit-scrollbar-thumb {
|
||||
.coco-container .custom-scrollbar::-webkit-scrollbar-thumb {
|
||||
background-color: #cbd5e1;
|
||||
border-radius: 3px;
|
||||
}
|
||||
|
||||
.dark .custom-scrollbar {
|
||||
.dark.coco-container .custom-scrollbar {
|
||||
scrollbar-color: #475569 transparent;
|
||||
}
|
||||
|
||||
.dark .custom-scrollbar::-webkit-scrollbar-thumb {
|
||||
.dark.coco-container .custom-scrollbar::-webkit-scrollbar-thumb {
|
||||
background-color: #475569;
|
||||
}
|
||||
|
||||
@@ -315,11 +406,11 @@
|
||||
}
|
||||
}
|
||||
|
||||
@layer base {
|
||||
* {
|
||||
@apply border-border;
|
||||
}
|
||||
body {
|
||||
@apply bg-background text-foreground;
|
||||
}
|
||||
/* Hide the waveform visualization for speech-to-text (only appears in production with two waveforms) */
|
||||
::part(progress) {
|
||||
display: none;
|
||||
}
|
||||
|
||||
::part(scroll) {
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
@@ -1,12 +1,16 @@
|
||||
import ReactDOM from "react-dom/client";
|
||||
import { RouterProvider } from "react-router-dom";
|
||||
|
||||
import platformAdapter from "@/utils/platformAdapter";
|
||||
import { router } from "./routes/index";
|
||||
import { routerWeb } from "./routes/web";
|
||||
import "./i18n";
|
||||
import '@/utils/global-logger';
|
||||
|
||||
import "./main.css";
|
||||
|
||||
const isTauri = platformAdapter.isTauri();
|
||||
|
||||
ReactDOM.createRoot(document.getElementById("root") as HTMLElement).render(
|
||||
<RouterProvider router={router} />
|
||||
<RouterProvider router={isTauri ? router : routerWeb} />
|
||||
);
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { useState, useEffect } from "react";
|
||||
import { Tab, TabGroup, TabList, TabPanel, TabPanels } from "@headlessui/react";
|
||||
import { Tabs, TabsList, TabsTrigger, TabsContent } from "@/components/ui/tabs";
|
||||
import { Settings, Puzzle, Settings2, Info, Server } from "lucide-react";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import { listen } from "@tauri-apps/api/event";
|
||||
@@ -18,13 +18,14 @@ import { useAppStore } from "@/stores/appStore";
|
||||
import { useExtensionsStore } from "@/stores/extensionsStore";
|
||||
import { useAppearanceStore } from "@/stores/appearanceStore";
|
||||
|
||||
const tabIndexMap: { [key: string]: number } = {
|
||||
general: 0,
|
||||
extensions: 1,
|
||||
connect: 2,
|
||||
advanced: 3,
|
||||
about: 4,
|
||||
};
|
||||
const tabValues = [
|
||||
"general",
|
||||
"extensions",
|
||||
"connect",
|
||||
"advanced",
|
||||
"about",
|
||||
] as const;
|
||||
type TabValue = (typeof tabValues)[number];
|
||||
|
||||
function SettingsPage() {
|
||||
const { t } = useTranslation();
|
||||
@@ -32,22 +33,21 @@ function SettingsPage() {
|
||||
|
||||
useTray();
|
||||
|
||||
const tabs = [
|
||||
{ name: t("settings.tabs.general"), icon: Settings },
|
||||
{ name: t("settings.tabs.extensions"), icon: Puzzle },
|
||||
{ name: t("settings.tabs.connect"), icon: Server },
|
||||
{ name: t("settings.tabs.advanced"), icon: Settings2 },
|
||||
{ name: t("settings.tabs.about"), icon: Info },
|
||||
const tabs: { name: string; icon: any; value: TabValue }[] = [
|
||||
{ name: t("settings.tabs.general"), icon: Settings, value: "general" },
|
||||
{ name: t("settings.tabs.extensions"), icon: Puzzle, value: "extensions" },
|
||||
{ name: t("settings.tabs.connect"), icon: Server, value: "connect" },
|
||||
{ name: t("settings.tabs.advanced"), icon: Settings2, value: "advanced" },
|
||||
{ name: t("settings.tabs.about"), icon: Info, value: "about" },
|
||||
];
|
||||
|
||||
const [defaultIndex, setDefaultIndex] = useState<number>(0);
|
||||
const [selectedTab, setSelectedTab] = useState<TabValue>("general");
|
||||
|
||||
useEffect(() => {
|
||||
const unlisten = listen("tab_index", (event) => {
|
||||
const tabName = event.payload as string;
|
||||
const index = tabIndexMap[tabName];
|
||||
if (index !== -1) {
|
||||
setDefaultIndex(index);
|
||||
const tabName = event.payload as TabValue;
|
||||
if (tabValues.includes(tabName)) {
|
||||
setSelectedTab(tabName);
|
||||
}
|
||||
});
|
||||
|
||||
@@ -67,7 +67,7 @@ function SettingsPage() {
|
||||
"config-extension",
|
||||
({ payload }) => {
|
||||
platformAdapter.showWindow();
|
||||
setDefaultIndex(1);
|
||||
setSelectedTab("extensions");
|
||||
setConfigId(payload);
|
||||
}
|
||||
);
|
||||
@@ -82,69 +82,60 @@ function SettingsPage() {
|
||||
}, []);
|
||||
|
||||
useEffect(() => {
|
||||
document.body.style.overflow = defaultIndex === 1 ? "hidden" : "auto";
|
||||
}, [defaultIndex]);
|
||||
document.body.style.overflow =
|
||||
selectedTab === "extensions" ? "hidden" : "auto";
|
||||
}, [selectedTab]);
|
||||
|
||||
return (
|
||||
<div>
|
||||
<div className="min-h-screen pb-8 bg-white dark:bg-gray-900 text-gray-900 dark:text-white">
|
||||
<>
|
||||
<div className="min-h-screen pb-8 bg-background text-foreground">
|
||||
<div className="max-w-6xl mx-auto p-4">
|
||||
<TabGroup
|
||||
selectedIndex={defaultIndex}
|
||||
onChange={(index) => {
|
||||
setDefaultIndex(index);
|
||||
}}
|
||||
<Tabs
|
||||
value={selectedTab}
|
||||
onValueChange={(v) => setSelectedTab(v as TabValue)}
|
||||
>
|
||||
<TabList className="flex space-x-1 rounded-xl bg-gray-100 dark:bg-gray-800 p-1">
|
||||
<TabsList className="flex h-10 rounded-xl">
|
||||
{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`
|
||||
}
|
||||
<TabsTrigger
|
||||
key={tab.value}
|
||||
value={tab.value}
|
||||
className="flex-1 gap-2 h-full"
|
||||
>
|
||||
<tab.icon className="w-4 h-4" />
|
||||
<span>{tab.name}</span>
|
||||
</Tab>
|
||||
))}
|
||||
</TabList>
|
||||
<tab.icon className="size-4" />
|
||||
|
||||
<TabPanels className="mt-2">
|
||||
<TabPanel>
|
||||
<SettingsPanel title="">
|
||||
<GeneralSettings />
|
||||
</SettingsPanel>
|
||||
</TabPanel>
|
||||
<TabPanel>
|
||||
<SettingsPanel title="">
|
||||
<Extensions />
|
||||
</SettingsPanel>
|
||||
</TabPanel>
|
||||
<TabPanel>
|
||||
<Cloud />
|
||||
</TabPanel>
|
||||
<TabPanel>
|
||||
<SettingsPanel title="">
|
||||
<Advanced />
|
||||
</SettingsPanel>
|
||||
</TabPanel>
|
||||
<TabPanel>
|
||||
<SettingsPanel title="">
|
||||
<AboutView />
|
||||
</SettingsPanel>
|
||||
</TabPanel>
|
||||
</TabPanels>
|
||||
</TabGroup>
|
||||
<span>{tab.name}</span>
|
||||
</TabsTrigger>
|
||||
))}
|
||||
</TabsList>
|
||||
|
||||
<TabsContent value="general">
|
||||
<SettingsPanel title="">
|
||||
<GeneralSettings />
|
||||
</SettingsPanel>
|
||||
</TabsContent>
|
||||
<TabsContent value="extensions">
|
||||
<SettingsPanel title="">
|
||||
<Extensions />
|
||||
</SettingsPanel>
|
||||
</TabsContent>
|
||||
<TabsContent value="connect">
|
||||
<Cloud />
|
||||
</TabsContent>
|
||||
<TabsContent value="advanced">
|
||||
<SettingsPanel title="">
|
||||
<Advanced />
|
||||
</SettingsPanel>
|
||||
</TabsContent>
|
||||
<TabsContent value="about">
|
||||
<SettingsPanel title="">
|
||||
<AboutView />
|
||||
</SettingsPanel>
|
||||
</TabsContent>
|
||||
</Tabs>
|
||||
</div>
|
||||
</div>
|
||||
<Footer />
|
||||
</div>
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
114
src/pages/settings/shadcn-demo.tsx
Normal file
114
src/pages/settings/shadcn-demo.tsx
Normal file
@@ -0,0 +1,114 @@
|
||||
import { useState } from "react";
|
||||
import { Button } from "@/components/ui/button";
|
||||
import { Input } from "@/components/ui/input";
|
||||
import { Label } from "@/components/ui/label";
|
||||
import { Checkbox } from "@/components/ui/checkbox";
|
||||
import { Switch } from "@/components/ui/switch";
|
||||
import { Separator } from "@/components/ui/separator";
|
||||
import { Slider } from "@/components/ui/slider";
|
||||
import { MultiSelect } from "@/components/ui/multi-select";
|
||||
import {
|
||||
Dialog,
|
||||
DialogTrigger,
|
||||
DialogContent,
|
||||
DialogHeader,
|
||||
DialogTitle,
|
||||
DialogDescription,
|
||||
DialogFooter,
|
||||
} from "@/components/ui/dialog";
|
||||
import {
|
||||
Select,
|
||||
SelectTrigger,
|
||||
SelectContent,
|
||||
SelectItem,
|
||||
SelectValue,
|
||||
} from "@/components/ui/select";
|
||||
|
||||
const ShadcnDemo = () => {
|
||||
const [checked, setChecked] = useState(false);
|
||||
const [enabled, setEnabled] = useState(false);
|
||||
const [sliderValue, setSliderValue] = useState<number[]>([50]);
|
||||
const [selected, setSelected] = useState<string[]>([]);
|
||||
const options = [
|
||||
{ value: "a", label: "选项 A" },
|
||||
{ value: "b", label: "选项 B" },
|
||||
{ value: "c", label: "选项 C" },
|
||||
{ value: "d", label: "选项 D" },
|
||||
];
|
||||
|
||||
return (
|
||||
<div className="p-6 space-y-6">
|
||||
<h2 className="text-xl font-semibold">Shadcn 组件演示</h2>
|
||||
|
||||
<div className="grid grid-cols-1 gap-4 sm:grid-cols-2">
|
||||
<div className="space-y-2">
|
||||
<Label htmlFor="name">名称</Label>
|
||||
<Input id="name" placeholder="输入名称" />
|
||||
</div>
|
||||
|
||||
<div className="space-y-2">
|
||||
<Label>选择</Label>
|
||||
<Select>
|
||||
<SelectTrigger>
|
||||
<SelectValue placeholder="请选择" />
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
<SelectItem value="a">选项 A</SelectItem>
|
||||
<SelectItem value="b">选项 B</SelectItem>
|
||||
<SelectItem value="c">选项 C</SelectItem>
|
||||
</SelectContent>
|
||||
</Select>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<Separator />
|
||||
|
||||
<div className="grid grid-cols-1 gap-4 sm:grid-cols-2">
|
||||
<div className="space-y-2">
|
||||
<Label>多选框</Label>
|
||||
<MultiSelect options={options} value={selected} onChange={setSelected} />
|
||||
<div className="text-sm text-muted-foreground">当前选择:{selected.length ? selected.join(", ") : "无"}</div>
|
||||
</div>
|
||||
|
||||
<div className="space-y-2">
|
||||
<Label>滑块(值:{sliderValue[0]})</Label>
|
||||
<Slider value={sliderValue} onValueChange={setSliderValue} max={100} step={1} />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="flex items-center gap-6">
|
||||
<div className="flex items-center gap-2">
|
||||
<Checkbox checked={checked} onCheckedChange={(v) => setChecked(Boolean(v))} />
|
||||
<span>复选框:{checked ? "已选" : "未选"}</span>
|
||||
</div>
|
||||
<div className="flex items-center gap-2">
|
||||
<Switch checked={enabled} onCheckedChange={setEnabled} />
|
||||
<span>开关:{enabled ? "开启" : "关闭"}</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="flex gap-3">
|
||||
<Button>默认按钮</Button>
|
||||
<Button variant="secondary">次级按钮</Button>
|
||||
<Button variant="outline">描边按钮</Button>
|
||||
<Dialog>
|
||||
<DialogTrigger asChild>
|
||||
<Button variant="ghost">打开对话框</Button>
|
||||
</DialogTrigger>
|
||||
<DialogContent>
|
||||
<DialogHeader>
|
||||
<DialogTitle>示例对话框</DialogTitle>
|
||||
<DialogDescription>这是一个 Shadcn Dialog 示例。</DialogDescription>
|
||||
</DialogHeader>
|
||||
<DialogFooter>
|
||||
<Button variant="secondary">取消</Button>
|
||||
<Button>确认</Button>
|
||||
</DialogFooter>
|
||||
</DialogContent>
|
||||
</Dialog>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default ShadcnDemo;
|
||||
@@ -16,7 +16,8 @@ import { Get } from "@/api/axiosRequest";
|
||||
import { useWebConfigStore } from "@/stores/webConfigStore";
|
||||
|
||||
import "@/i18n";
|
||||
import "@/web.css";
|
||||
import { useIconfontScript } from "@/hooks/useScript";
|
||||
// Styles are distributed separately in the library build (out/search-chat/index.css)
|
||||
|
||||
interface WebAppProps {
|
||||
headers?: Record<string, unknown>;
|
||||
@@ -117,6 +118,7 @@ function WebApp({
|
||||
useEscape();
|
||||
useModifierKeyPress();
|
||||
useViewportHeight();
|
||||
useIconfontScript();
|
||||
|
||||
useEffect(() => {
|
||||
setDisabled(!loginInfo && !integration?.guest?.enabled);
|
||||
@@ -125,7 +127,7 @@ function WebApp({
|
||||
return (
|
||||
<div
|
||||
id="searchChat-container"
|
||||
className={`coco-container relative ${theme}`}
|
||||
className={`coco-container relative ${theme} border! border-(--border) rounded-xl`}
|
||||
data-theme={theme}
|
||||
style={{
|
||||
maxWidth: `${width}px`,
|
||||
|
||||
@@ -7,9 +7,8 @@ import ErrorPage from "@/pages/error/index";
|
||||
const DesktopApp = lazy(() => import("@/pages/main/index"));
|
||||
const SettingsPage = lazy(() => import("@/pages/settings/index"));
|
||||
const StandaloneChat = lazy(() => import("@/pages/chat/index"));
|
||||
const WebPage = lazy(() => import("@/pages/web/index"));
|
||||
const CheckPage = lazy(() => import("@/pages/check/index"));
|
||||
// const SelectionWindow = lazy(() => import("@/pages/selection/index"));
|
||||
const SelectionWindow = lazy(() => import("@/pages/selection/index"));
|
||||
|
||||
const routerOptions = {
|
||||
basename: "/",
|
||||
@@ -30,8 +29,7 @@ export const router = createBrowserRouter(
|
||||
{ path: "/ui/settings", element: (<Suspense fallback={<></>}><SettingsPage /></Suspense>) },
|
||||
{ path: "/ui/chat", element: (<Suspense fallback={<></>}><StandaloneChat /></Suspense>) },
|
||||
{ path: "/ui/check", element: (<Suspense fallback={<></>}><CheckPage /></Suspense>) },
|
||||
// { path: "/ui/selection", element: (<Suspense fallback={<></>}><SelectionWindow /></Suspense>) },
|
||||
{ path: "/web", element: (<Suspense fallback={<></>}><WebPage /></Suspense>) },
|
||||
{ path: "/ui/selection", element: (<Suspense fallback={<></>}><SelectionWindow /></Suspense>) },
|
||||
],
|
||||
},
|
||||
],
|
||||
|
||||
@@ -15,7 +15,7 @@ import { useModifierKeyPress } from "@/hooks/useModifierKeyPress";
|
||||
import { useIconfontScript } from "@/hooks/useScript";
|
||||
import { Extension } from "@/components/Settings/Extensions";
|
||||
import { useExtensionsStore } from "@/stores/extensionsStore";
|
||||
import { useSelectionStore } from "@/stores/selectionStore";
|
||||
import { useSelectionStore, startSelectionStorePersistence } from "@/stores/selectionStore";
|
||||
import { useServers } from "@/hooks/useServers";
|
||||
import { useDeepLinkManager } from "@/hooks/useDeepLinkManager";
|
||||
// import { useSelectionWindow } from "@/hooks/useSelectionWindow";
|
||||
@@ -30,6 +30,11 @@ export default function LayoutOutlet() {
|
||||
// Initialize selection store synchronization
|
||||
useSelectionStore();
|
||||
|
||||
// Initialize Tauri-backed persistence for selection store only in desktop mode.
|
||||
useMount(() => {
|
||||
startSelectionStorePersistence();
|
||||
});
|
||||
|
||||
// init servers isTauri
|
||||
useServers();
|
||||
// init deep link manager
|
||||
@@ -39,13 +44,11 @@ export default function LayoutOutlet() {
|
||||
i18n.changeLanguage(language);
|
||||
}, [language]);
|
||||
|
||||
function updateBodyClass(path: string) {
|
||||
function updateBodyClass(_path: string) {
|
||||
const body = document.body;
|
||||
body.classList.remove("input-body");
|
||||
|
||||
if (path === "/ui") {
|
||||
body.classList.add("input-body");
|
||||
}
|
||||
// Ensure rounded corners and clipping are applied to the whole window
|
||||
// Tailwind v4 + Tauri: relying on container radius may not show at window edges
|
||||
body.classList.add("input-body");
|
||||
}
|
||||
|
||||
useMount(async () => {
|
||||
|
||||
30
src/routes/web.tsx
Normal file
30
src/routes/web.tsx
Normal file
@@ -0,0 +1,30 @@
|
||||
import { createBrowserRouter } from "react-router-dom";
|
||||
import { Suspense, lazy } from "react";
|
||||
|
||||
import ErrorPage from "@/pages/error/index";
|
||||
|
||||
const WebPage = lazy(() => import("@/pages/web/index"));
|
||||
|
||||
const routerOptions = {
|
||||
basename: "/",
|
||||
future: {
|
||||
v7_startTransition: true,
|
||||
v7_relativeSplatPath: true,
|
||||
},
|
||||
} as const;
|
||||
|
||||
export const routerWeb = createBrowserRouter(
|
||||
[
|
||||
{
|
||||
path: "/",
|
||||
errorElement: <ErrorPage />,
|
||||
element: (
|
||||
<Suspense fallback={<></>}>
|
||||
<WebPage />
|
||||
</Suspense>
|
||||
),
|
||||
},
|
||||
],
|
||||
routerOptions
|
||||
);
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { create } from 'zustand';
|
||||
import { createTauriStore } from '@tauri-store/zustand';
|
||||
import { create } from "zustand";
|
||||
import platformAdapter from "@/utils/platformAdapter";
|
||||
|
||||
type IconConfig =
|
||||
| { type: "lucide"; name: string; color?: string }
|
||||
@@ -37,9 +37,18 @@ export const useSelectionStore = create<SelectionStore>((set) => ({
|
||||
setSelectionEnabled: (selectionEnabled) => set({ selectionEnabled }),
|
||||
}));
|
||||
|
||||
// A handle to the Tauri plugin.
|
||||
// We will need this to start the store.
|
||||
export const tauriHandler = createTauriStore('selection-store', useSelectionStore, {
|
||||
saveOnChange: true,
|
||||
autoStart: true,
|
||||
});
|
||||
/**
|
||||
* Initialize Selection store persistence on Tauri only.
|
||||
* In Web mode, this is a no-op to avoid loading Tauri-specific plugins.
|
||||
*
|
||||
* Returns a promise that resolves when persistence has been started on Tauri.
|
||||
*/
|
||||
export async function startSelectionStorePersistence(): Promise<void> {
|
||||
if (!platformAdapter.isTauri()) return;
|
||||
|
||||
const { createTauriStore } = await import("@tauri-store/zustand");
|
||||
createTauriStore("selection-store", useSelectionStore, {
|
||||
saveOnChange: true,
|
||||
autoStart: true,
|
||||
});
|
||||
}
|
||||
|
||||
741
src/web.css
741
src/web.css
@@ -1,741 +0,0 @@
|
||||
/* @tailwind base; */
|
||||
@tailwind components;
|
||||
@tailwind utilities;
|
||||
|
||||
/* Base variables */
|
||||
:host,
|
||||
:root,
|
||||
.searchbox-container,
|
||||
.coco-container {
|
||||
--spacing-base: 12px;
|
||||
--modal-width: 560px;
|
||||
--modal-height: 600px;
|
||||
--searchbox-height: 56px;
|
||||
--hit-height: 56px;
|
||||
--footer-height: 44px;
|
||||
--icon-stroke-width: 1.4;
|
||||
--background: #ffffff;
|
||||
--foreground: #09090b;
|
||||
--border: #e3e3e7;
|
||||
--coco-primary-color: rgb(149, 5, 153);
|
||||
}
|
||||
|
||||
/* Light theme */
|
||||
.light.coco-container {
|
||||
--coco-primary-color: rgb(149, 5, 153);
|
||||
--coco-text-color: rgb(28, 30, 33);
|
||||
--coco-muted-color: rgb(150, 159, 175);
|
||||
--coco-modal-container-background: rgba(101, 108, 133, 0.8);
|
||||
--coco-modal-background: rgb(245, 246, 247);
|
||||
--coco-modal-shadow: inset 1px 1px 0 0 rgba(255, 255, 255, 0.5),
|
||||
0 3px 8px 0 rgba(85, 90, 100, 1);
|
||||
--coco-searchbox-background: rgb(235, 237, 240);
|
||||
--coco-searchbox-focus-background: #fff;
|
||||
--coco-hit-color: rgb(68, 73, 80);
|
||||
--coco-hit-active-color: #fff;
|
||||
--coco-hit-background: #fff;
|
||||
--coco-hit-shadow: 0 1px 3px 0 rgb(212, 217, 225);
|
||||
--coco-key-gradient: linear-gradient(
|
||||
-225deg,
|
||||
rgb(213, 219, 228) 0%,
|
||||
rgb(248, 248, 248) 100%
|
||||
);
|
||||
--coco-key-shadow: inset 0 -2px 0 0 rgb(205, 205, 230), inset 0 0 1px 1px #fff,
|
||||
0 1px 2px 1px rgba(30, 35, 90, 0.4);
|
||||
--coco-footer-background: #fff;
|
||||
--coco-footer-shadow: 0 -1px 0 0 rgb(224, 227, 232),
|
||||
0 -3px 6px 0 rgba(69, 98, 155, 0.12);
|
||||
--coco-icon-color: rgb(21, 21, 21);
|
||||
}
|
||||
|
||||
/* Dark theme */
|
||||
.dark.coco-container {
|
||||
--coco-primary-color: rgb(149, 5, 153);
|
||||
--background: #09090b;
|
||||
--foreground: #f9f9f9;
|
||||
--border: #27272a;
|
||||
--coco-text-color: rgb(245, 246, 247);
|
||||
--coco-modal-container-background: rgba(9, 10, 17, 0.8);
|
||||
--coco-modal-background: rgb(21, 23, 42);
|
||||
--coco-modal-shadow: inset 1px 1px 0 0 rgb(44, 46, 64),
|
||||
0 3px 8px 0 rgb(0, 3, 9);
|
||||
--coco-searchbox-background: rgb(9, 10, 17);
|
||||
--coco-searchbox-focus-background: #000;
|
||||
--coco-hit-color: rgb(190, 195, 201);
|
||||
--coco-hit-shadow: none;
|
||||
--coco-hit-background: rgb(9, 10, 17);
|
||||
--coco-key-gradient: linear-gradient(
|
||||
-26.5deg,
|
||||
rgb(86, 88, 114) 0%,
|
||||
rgb(49, 53, 91) 100%
|
||||
);
|
||||
--coco-key-shadow: inset 0 -2px 0 0 rgb(40, 45, 85),
|
||||
inset 0 0 1px 1px rgb(81, 87, 125), 0 2px 2px 0 rgba(3, 4, 9, 0.3);
|
||||
--coco-footer-background: rgb(30, 33, 54);
|
||||
--coco-footer-shadow: inset 0 1px 0 0 rgba(73, 76, 106, 0.5),
|
||||
0 -4px 8px 0 rgba(0, 0, 0, 0.2);
|
||||
--coco-muted-color: rgb(127, 132, 151);
|
||||
--coco-icon-color: rgb(255, 255, 255);
|
||||
}
|
||||
|
||||
@media (prefers-color-scheme: dark) {
|
||||
.dark.coco-container {
|
||||
--coco-primary-color: rgb(149, 5, 153);
|
||||
--background: #09090b;
|
||||
--foreground: #f9f9f9;
|
||||
--border: #27272a;
|
||||
--coco-text-color: rgb(245, 246, 247);
|
||||
--coco-modal-container-background: rgba(9, 10, 17, 0.8);
|
||||
--coco-modal-background: rgb(21, 23, 42);
|
||||
--coco-modal-shadow: inset 1px 1px 0 0 rgb(44, 46, 64),
|
||||
0 3px 8px 0 rgb(0, 3, 9);
|
||||
--coco-searchbox-background: rgb(9, 10, 17);
|
||||
--coco-searchbox-focus-background: #000;
|
||||
--coco-hit-color: rgb(190, 195, 201);
|
||||
--coco-hit-shadow: none;
|
||||
--coco-hit-background: rgb(9, 10, 17);
|
||||
--coco-key-gradient: linear-gradient(
|
||||
-26.5deg,
|
||||
rgb(86, 88, 114) 0%,
|
||||
rgb(49, 53, 91) 100%
|
||||
);
|
||||
--coco-key-shadow: inset 0 -2px 0 0 rgb(40, 45, 85),
|
||||
inset 0 0 1px 1px rgb(81, 87, 125), 0 2px 2px 0 rgba(3, 4, 9, 0.3);
|
||||
--coco-footer-background: rgb(30, 33, 54);
|
||||
--coco-footer-shadow: inset 0 1px 0 0 rgba(73, 76, 106, 0.5),
|
||||
0 -4px 8px 0 rgba(0, 0, 0, 0.2);
|
||||
--coco-muted-color: rgb(127, 132, 151);
|
||||
--coco-icon-color: rgb(255, 255, 255);
|
||||
}
|
||||
}
|
||||
|
||||
/* html,
|
||||
:host {
|
||||
line-height: 1.5;
|
||||
-webkit-text-size-adjust: 100%;
|
||||
-moz-tab-size: 4;
|
||||
-o-tab-size: 4;
|
||||
tab-size: 4;
|
||||
font-family: ui-sans-serif, system-ui, sans-serif, "Apple Color Emoji",
|
||||
"Segoe UI Emoji", Segoe UI Symbol, "Noto Color Emoji";
|
||||
font-feature-settings: normal;
|
||||
font-variation-settings: normal;
|
||||
-webkit-tap-highlight-color: transparent;
|
||||
} */
|
||||
|
||||
/* html {
|
||||
height: 100%;
|
||||
-webkit-user-select: none;
|
||||
-moz-user-select: none;
|
||||
user-select: none;
|
||||
overscroll-behavior: none;
|
||||
}
|
||||
|
||||
body,
|
||||
#root {
|
||||
height: 100%;
|
||||
--tw-text-opacity: 1;
|
||||
color: rgb(17 24 39 / var(--tw-text-opacity, 1));
|
||||
-webkit-font-smoothing: antialiased;
|
||||
-moz-osx-font-smoothing: grayscale;
|
||||
} */
|
||||
|
||||
/* body {
|
||||
margin: 0;
|
||||
line-height: inherit;
|
||||
} */
|
||||
|
||||
.dark body,
|
||||
.dark #root,
|
||||
.dark.coco-container,
|
||||
.dark.coco-container {
|
||||
--tw-text-opacity: 1;
|
||||
color: rgb(243 244 246 / var(--tw-text-opacity, 1));
|
||||
}
|
||||
|
||||
.input-body {
|
||||
overflow: hidden;
|
||||
border-radius: 0.375rem;
|
||||
}
|
||||
|
||||
.icon {
|
||||
width: 1em;
|
||||
height: 1em;
|
||||
vertical-align: -0.15em;
|
||||
fill: currentColor;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.\!container {
|
||||
width: 100% !important;
|
||||
}
|
||||
.container {
|
||||
width: 100%;
|
||||
}
|
||||
@media (min-width: 640px) {
|
||||
.\!container {
|
||||
max-width: 640px !important;
|
||||
}
|
||||
.container {
|
||||
max-width: 640px;
|
||||
}
|
||||
}
|
||||
@media (min-width: 768px) {
|
||||
.\!container {
|
||||
max-width: 768px !important;
|
||||
}
|
||||
.container {
|
||||
max-width: 768px;
|
||||
}
|
||||
}
|
||||
@media (min-width: 1024px) {
|
||||
.\!container {
|
||||
max-width: 1024px !important;
|
||||
}
|
||||
.container {
|
||||
max-width: 1024px;
|
||||
}
|
||||
}
|
||||
@media (min-width: 1280px) {
|
||||
.\!container {
|
||||
max-width: 1280px !important;
|
||||
}
|
||||
.container {
|
||||
max-width: 1280px;
|
||||
}
|
||||
}
|
||||
@media (min-width: 1536px) {
|
||||
.\!container {
|
||||
max-width: 1536px !important;
|
||||
}
|
||||
.container {
|
||||
max-width: 1536px;
|
||||
}
|
||||
}
|
||||
|
||||
.coco-container {
|
||||
line-height: 1.5;
|
||||
-webkit-text-size-adjust: 100%;
|
||||
-moz-tab-size: 4;
|
||||
-o-tab-size: 4;
|
||||
tab-size: 4;
|
||||
font-family: ui-sans-serif, system-ui, sans-serif, "Apple Color Emoji",
|
||||
"Segoe UI Emoji", Segoe UI Symbol, "Noto Color Emoji";
|
||||
font-feature-settings: normal;
|
||||
font-variation-settings: normal;
|
||||
-webkit-tap-highlight-color: transparent;
|
||||
|
||||
*,
|
||||
:before,
|
||||
:after {
|
||||
--tw-border-spacing-x: 0;
|
||||
--tw-border-spacing-y: 0;
|
||||
--tw-translate-x: 0;
|
||||
--tw-translate-y: 0;
|
||||
--tw-rotate: 0;
|
||||
--tw-skew-x: 0;
|
||||
--tw-skew-y: 0;
|
||||
--tw-scale-x: 1;
|
||||
--tw-scale-y: 1;
|
||||
--tw-pan-x: ;
|
||||
--tw-pan-y: ;
|
||||
--tw-pinch-zoom: ;
|
||||
--tw-scroll-snap-strictness: proximity;
|
||||
--tw-gradient-from-position: ;
|
||||
--tw-gradient-via-position: ;
|
||||
--tw-gradient-to-position: ;
|
||||
--tw-ordinal: ;
|
||||
--tw-slashed-zero: ;
|
||||
--tw-numeric-figure: ;
|
||||
--tw-numeric-spacing: ;
|
||||
--tw-numeric-fraction: ;
|
||||
--tw-ring-inset: ;
|
||||
--tw-ring-offset-width: 0px;
|
||||
--tw-ring-offset-color: #fff;
|
||||
--tw-ring-color: rgb(59 130 246 / 0.5);
|
||||
--tw-ring-offset-shadow: 0 0 #0000;
|
||||
--tw-ring-shadow: 0 0 #0000;
|
||||
--tw-shadow: 0 0 #0000;
|
||||
--tw-shadow-colored: 0 0 #0000;
|
||||
--tw-blur: ;
|
||||
--tw-brightness: ;
|
||||
--tw-contrast: ;
|
||||
--tw-grayscale: ;
|
||||
--tw-hue-rotate: ;
|
||||
--tw-invert: ;
|
||||
--tw-saturate: ;
|
||||
--tw-sepia: ;
|
||||
--tw-drop-shadow: ;
|
||||
--tw-backdrop-blur: ;
|
||||
--tw-backdrop-brightness: ;
|
||||
--tw-backdrop-contrast: ;
|
||||
--tw-backdrop-grayscale: ;
|
||||
--tw-backdrop-hue-rotate: ;
|
||||
--tw-backdrop-invert: ;
|
||||
--tw-backdrop-opacity: ;
|
||||
--tw-backdrop-saturate: ;
|
||||
--tw-backdrop-sepia: ;
|
||||
--tw-contain-size: ;
|
||||
--tw-contain-layout: ;
|
||||
--tw-contain-paint: ;
|
||||
--tw-contain-style: ;
|
||||
}
|
||||
::backdrop {
|
||||
--tw-border-spacing-x: 0;
|
||||
--tw-border-spacing-y: 0;
|
||||
--tw-translate-x: 0;
|
||||
--tw-translate-y: 0;
|
||||
--tw-rotate: 0;
|
||||
--tw-skew-x: 0;
|
||||
--tw-skew-y: 0;
|
||||
--tw-scale-x: 1;
|
||||
--tw-scale-y: 1;
|
||||
--tw-pan-x: ;
|
||||
--tw-pan-y: ;
|
||||
--tw-pinch-zoom: ;
|
||||
--tw-scroll-snap-strictness: proximity;
|
||||
--tw-gradient-from-position: ;
|
||||
--tw-gradient-via-position: ;
|
||||
--tw-gradient-to-position: ;
|
||||
--tw-ordinal: ;
|
||||
--tw-slashed-zero: ;
|
||||
--tw-numeric-figure: ;
|
||||
--tw-numeric-spacing: ;
|
||||
--tw-numeric-fraction: ;
|
||||
--tw-ring-inset: ;
|
||||
--tw-ring-offset-width: 0px;
|
||||
--tw-ring-offset-color: #fff;
|
||||
--tw-ring-color: rgb(59 130 246 / 0.5);
|
||||
--tw-ring-offset-shadow: 0 0 #0000;
|
||||
--tw-ring-shadow: 0 0 #0000;
|
||||
--tw-shadow: 0 0 #0000;
|
||||
--tw-shadow-colored: 0 0 #0000;
|
||||
--tw-blur: ;
|
||||
--tw-brightness: ;
|
||||
--tw-contrast: ;
|
||||
--tw-grayscale: ;
|
||||
--tw-hue-rotate: ;
|
||||
--tw-invert: ;
|
||||
--tw-saturate: ;
|
||||
--tw-sepia: ;
|
||||
--tw-drop-shadow: ;
|
||||
--tw-backdrop-blur: ;
|
||||
--tw-backdrop-brightness: ;
|
||||
--tw-backdrop-contrast: ;
|
||||
--tw-backdrop-grayscale: ;
|
||||
--tw-backdrop-hue-rotate: ;
|
||||
--tw-backdrop-invert: ;
|
||||
--tw-backdrop-opacity: ;
|
||||
--tw-backdrop-saturate: ;
|
||||
--tw-backdrop-sepia: ;
|
||||
--tw-contain-size: ;
|
||||
--tw-contain-layout: ;
|
||||
--tw-contain-paint: ;
|
||||
--tw-contain-style: ;
|
||||
}
|
||||
*,
|
||||
:before,
|
||||
:after {
|
||||
box-sizing: border-box;
|
||||
border-width: 0;
|
||||
border-style: solid;
|
||||
border-color: #e5e7eb;
|
||||
}
|
||||
:before,
|
||||
:after {
|
||||
--tw-content: "";
|
||||
}
|
||||
|
||||
hr {
|
||||
height: 0;
|
||||
color: inherit;
|
||||
border-top-width: 1px;
|
||||
}
|
||||
abbr:where([title]) {
|
||||
-webkit-text-decoration: underline dotted;
|
||||
text-decoration: underline dotted;
|
||||
}
|
||||
h1,
|
||||
h2,
|
||||
h3,
|
||||
h4,
|
||||
h5,
|
||||
h6 {
|
||||
font-size: inherit;
|
||||
font-weight: inherit;
|
||||
}
|
||||
a {
|
||||
color: inherit;
|
||||
text-decoration: inherit;
|
||||
}
|
||||
b,
|
||||
strong {
|
||||
font-weight: bolder;
|
||||
}
|
||||
code,
|
||||
kbd,
|
||||
samp,
|
||||
pre {
|
||||
font-family: ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas,
|
||||
Liberation Mono, Courier New, monospace;
|
||||
font-feature-settings: normal;
|
||||
font-variation-settings: normal;
|
||||
font-size: 1em;
|
||||
}
|
||||
small {
|
||||
font-size: 80%;
|
||||
}
|
||||
sub,
|
||||
sup {
|
||||
font-size: 75%;
|
||||
line-height: 0;
|
||||
position: relative;
|
||||
vertical-align: baseline;
|
||||
}
|
||||
sub {
|
||||
bottom: -0.25em;
|
||||
}
|
||||
sup {
|
||||
top: -0.5em;
|
||||
}
|
||||
button,
|
||||
input,
|
||||
optgroup,
|
||||
select,
|
||||
textarea {
|
||||
font-family: inherit;
|
||||
font-feature-settings: inherit;
|
||||
font-variation-settings: inherit;
|
||||
font-size: 100%;
|
||||
font-weight: inherit;
|
||||
line-height: inherit;
|
||||
letter-spacing: inherit;
|
||||
color: inherit;
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
}
|
||||
button,
|
||||
select {
|
||||
text-transform: none;
|
||||
}
|
||||
button,
|
||||
input:where([type="button"]),
|
||||
input:where([type="reset"]),
|
||||
input:where([type="submit"]) {
|
||||
-webkit-appearance: button;
|
||||
background-color: transparent;
|
||||
background-image: none;
|
||||
}
|
||||
:-moz-focusring {
|
||||
outline: auto;
|
||||
}
|
||||
:-moz-ui-invalid {
|
||||
box-shadow: none;
|
||||
}
|
||||
progress {
|
||||
vertical-align: baseline;
|
||||
}
|
||||
::-webkit-inner-spin-button,
|
||||
::-webkit-outer-spin-button {
|
||||
height: auto;
|
||||
}
|
||||
[type="search"] {
|
||||
-webkit-appearance: textfield;
|
||||
outline-offset: -2px;
|
||||
}
|
||||
::-webkit-search-decoration {
|
||||
-webkit-appearance: none;
|
||||
}
|
||||
::-webkit-file-upload-button {
|
||||
-webkit-appearance: button;
|
||||
font: inherit;
|
||||
}
|
||||
summary {
|
||||
display: list-item;
|
||||
}
|
||||
blockquote,
|
||||
dl,
|
||||
dd,
|
||||
h1,
|
||||
h2,
|
||||
h3,
|
||||
h4,
|
||||
h5,
|
||||
h6,
|
||||
hr,
|
||||
figure,
|
||||
p,
|
||||
pre {
|
||||
margin: inherit;
|
||||
}
|
||||
fieldset {
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
}
|
||||
legend {
|
||||
padding: 0;
|
||||
}
|
||||
ol,
|
||||
ul,
|
||||
menu {
|
||||
list-style: none;
|
||||
margin: inherit;
|
||||
padding: inherit;
|
||||
}
|
||||
dialog {
|
||||
padding: 0;
|
||||
}
|
||||
textarea {
|
||||
resize: vertical;
|
||||
}
|
||||
input::-moz-placeholder,
|
||||
textarea::-moz-placeholder {
|
||||
opacity: 1;
|
||||
color: #9ca3af;
|
||||
}
|
||||
input::placeholder,
|
||||
textarea::placeholder {
|
||||
opacity: 1;
|
||||
color: #9ca3af;
|
||||
}
|
||||
button,
|
||||
[role="button"] {
|
||||
cursor: pointer;
|
||||
}
|
||||
:disabled {
|
||||
cursor: default;
|
||||
}
|
||||
img,
|
||||
svg,
|
||||
video,
|
||||
canvas,
|
||||
audio,
|
||||
iframe,
|
||||
embed,
|
||||
object {
|
||||
display: block;
|
||||
vertical-align: middle;
|
||||
}
|
||||
img,
|
||||
video {
|
||||
max-width: 100%;
|
||||
height: auto;
|
||||
}
|
||||
[hidden]:where(:not([hidden="until-found"])) {
|
||||
display: none;
|
||||
}
|
||||
* {
|
||||
box-sizing: border-box;
|
||||
border-color: var(--border);
|
||||
}
|
||||
.settings-input {
|
||||
display: block;
|
||||
width: 100%;
|
||||
border-radius: 0.375rem;
|
||||
--tw-border-opacity: 1;
|
||||
border-color: rgb(209 213 219 / var(--tw-border-opacity, 1));
|
||||
--tw-bg-opacity: 1;
|
||||
background-color: rgb(255 255 255 / var(--tw-bg-opacity, 1));
|
||||
--tw-text-opacity: 1;
|
||||
color: rgb(17 24 39 / var(--tw-text-opacity, 1));
|
||||
--tw-shadow: 0 1px 2px 0 rgb(0 0 0 / 0.05);
|
||||
--tw-shadow-colored: 0 1px 2px 0 var(--tw-shadow-color);
|
||||
box-shadow: var(--tw-ring-offset-shadow, 0 0 #0000),
|
||||
var(--tw-ring-shadow, 0 0 #0000), var(--tw-shadow);
|
||||
transition-property: color, background-color, border-color,
|
||||
text-decoration-color, fill, stroke;
|
||||
transition-timing-function: cubic-bezier(0.4, 0, 0.2, 1);
|
||||
transition-duration: 0.2s;
|
||||
}
|
||||
.settings-input:focus {
|
||||
--tw-border-opacity: 1;
|
||||
border-color: rgb(59 130 246 / var(--tw-border-opacity, 1));
|
||||
--tw-ring-opacity: 1;
|
||||
--tw-ring-color: rgb(59 130 246 / var(--tw-ring-opacity, 1));
|
||||
}
|
||||
.settings-input:is([data-theme="dark"] *) {
|
||||
--tw-border-opacity: 1;
|
||||
border-color: rgb(75 85 99 / var(--tw-border-opacity, 1));
|
||||
--tw-bg-opacity: 1;
|
||||
background-color: rgb(55 65 81 / var(--tw-bg-opacity, 1));
|
||||
--tw-text-opacity: 1;
|
||||
color: rgb(243 244 246 / var(--tw-text-opacity, 1));
|
||||
}
|
||||
.settings-select {
|
||||
border-radius: 0.375rem;
|
||||
--tw-border-opacity: 1;
|
||||
border-color: rgb(209 213 219 / var(--tw-border-opacity, 1));
|
||||
--tw-bg-opacity: 1;
|
||||
background-color: rgb(255 255 255 / var(--tw-bg-opacity, 1));
|
||||
font-size: 0.875rem;
|
||||
line-height: 1.25rem;
|
||||
--tw-text-opacity: 1;
|
||||
color: rgb(17 24 39 / var(--tw-text-opacity, 1));
|
||||
--tw-shadow: 0 1px 2px 0 rgb(0 0 0 / 0.05);
|
||||
--tw-shadow-colored: 0 1px 2px 0 var(--tw-shadow-color);
|
||||
box-shadow: var(--tw-ring-offset-shadow, 0 0 #0000),
|
||||
var(--tw-ring-shadow, 0 0 #0000), var(--tw-shadow);
|
||||
transition-property: color, background-color, border-color,
|
||||
text-decoration-color, fill, stroke;
|
||||
transition-timing-function: cubic-bezier(0.4, 0, 0.2, 1);
|
||||
transition-duration: 0.2s;
|
||||
}
|
||||
.settings-select:focus {
|
||||
--tw-border-opacity: 1;
|
||||
border-color: rgb(59 130 246 / var(--tw-border-opacity, 1));
|
||||
--tw-ring-opacity: 1;
|
||||
--tw-ring-color: rgb(59 130 246 / var(--tw-ring-opacity, 1));
|
||||
}
|
||||
.settings-select:is([data-theme="dark"] *) {
|
||||
--tw-border-opacity: 1;
|
||||
border-color: rgb(75 85 99 / var(--tw-border-opacity, 1));
|
||||
--tw-bg-opacity: 1;
|
||||
background-color: rgb(55 65 81 / var(--tw-bg-opacity, 1));
|
||||
--tw-text-opacity: 1;
|
||||
color: rgb(243 244 246 / var(--tw-text-opacity, 1));
|
||||
}
|
||||
}
|
||||
|
||||
/* Component styles */
|
||||
@layer components {
|
||||
.settings-input {
|
||||
@apply block w-full rounded-[6px] 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-[6px] 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;
|
||||
}
|
||||
}
|
||||
|
||||
/* Utility styles */
|
||||
@layer utilities {
|
||||
/* Scrollbar styles */
|
||||
.custom-scrollbar {
|
||||
scrollbar-width: thin;
|
||||
scrollbar-color: #cbd5e1 transparent;
|
||||
}
|
||||
|
||||
.custom-scrollbar::-webkit-scrollbar {
|
||||
width: 6px;
|
||||
}
|
||||
|
||||
.custom-scrollbar::-webkit-scrollbar-track {
|
||||
background: transparent;
|
||||
}
|
||||
|
||||
.custom-scrollbar::-webkit-scrollbar-thumb {
|
||||
background-color: #cbd5e1;
|
||||
border-radius: 3px;
|
||||
}
|
||||
|
||||
.dark .custom-scrollbar {
|
||||
scrollbar-color: #475569 transparent;
|
||||
}
|
||||
|
||||
.dark .custom-scrollbar::-webkit-scrollbar-thumb {
|
||||
background-color: #475569;
|
||||
}
|
||||
|
||||
/* Background styles */
|
||||
.bg-100 {
|
||||
background-size: 100% 100%;
|
||||
}
|
||||
|
||||
/* Error page styles */
|
||||
#error-page {
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
height: 100%;
|
||||
background: linear-gradient(to right, #f79c42, #f2d600);
|
||||
font-family: "Arial", sans-serif;
|
||||
color: #fff;
|
||||
text-align: center;
|
||||
padding: 0 20px;
|
||||
}
|
||||
|
||||
.error-content {
|
||||
background-color: rgba(0, 0, 0, 0.6);
|
||||
padding: 40px;
|
||||
border-radius: 8px;
|
||||
box-shadow: 0px 10px 20px rgba(0, 0, 0, 0.2);
|
||||
max-width: 500px;
|
||||
width: 100%;
|
||||
margin: 0 20px;
|
||||
}
|
||||
|
||||
.error-title {
|
||||
font-size: 60px;
|
||||
font-weight: bold;
|
||||
margin-bottom: 20px;
|
||||
color: #f2d600;
|
||||
}
|
||||
|
||||
.error-message {
|
||||
font-size: 18px;
|
||||
margin-bottom: 20px;
|
||||
font-weight: 300;
|
||||
color: #fff;
|
||||
}
|
||||
|
||||
.error-details {
|
||||
font-size: 16px;
|
||||
color: #ffcc00;
|
||||
font-style: italic;
|
||||
}
|
||||
|
||||
.error-content button {
|
||||
background-color: #f2d600;
|
||||
border: none;
|
||||
padding: 10px 20px;
|
||||
font-size: 16px;
|
||||
color: #333;
|
||||
border-radius: 5px;
|
||||
cursor: pointer;
|
||||
transition: background-color 0.3s ease;
|
||||
}
|
||||
|
||||
.error-content button:hover {
|
||||
background-color: #f79c42;
|
||||
}
|
||||
|
||||
/* coco styles */
|
||||
.coco-modal-footer-commands-key {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
border-radius: 4px;
|
||||
border: 0;
|
||||
padding: 2px;
|
||||
background: var(--coco-key-gradient);
|
||||
box-shadow: var(--coco-key-shadow);
|
||||
color: var(--coco-muted-color);
|
||||
}
|
||||
|
||||
/* User selection styles */
|
||||
.user-select {
|
||||
-webkit-touch-callout: none;
|
||||
-webkit-user-select: none;
|
||||
-khtml-user-select: none;
|
||||
-moz-user-select: none;
|
||||
-ms-user-select: none;
|
||||
user-select: none;
|
||||
}
|
||||
|
||||
.user-select-text {
|
||||
-webkit-touch-callout: text;
|
||||
-webkit-user-select: text;
|
||||
-khtml-user-select: text;
|
||||
-moz-user-select: text;
|
||||
-ms-user-select: text;
|
||||
user-select: text;
|
||||
}
|
||||
}
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user