fix: mouse & keyDown (#122)

* feat: impl Coco server related APIs

* chore: remove unused method

* fix: invoke Rust interfaces in tauri::run()

* chore: add invoke

* feat: add add_coco_server

* fix: trim the tailing forward slash

* feat: interface get_user_profiles

* chore: add

* fix: store the servers in add interface

* chore: ass

* fix: skip non-publich servers with no token

* feat: add

* feat: get datasources and connectors

* fix: invoke interfaces in tauri::run()

* chore: add SidebarRef

* refactor: refactoring coco-app

* refactor: refactoring coco app

* refactor: refactoring project layout

* refactor: refactoring server management

* chore: cleanup code

* chore: display error when connect failed

* refactor: refactoring refresh server's info

* refactor: refactoring how to connect the coco serverg

* chore: rename to cloud

* refactor: refactoring remove coco server

* fix: refresh current selected server

* fix: reset server selection

* chore: update login status

* feat: add error message tips

* fix: fix login and logout

* refactor: refactoring http client

* fix: fix the datasources

* chore: minor fix

* refactor: refactoring code

* fix: fix search api

* chore: optimize part of icons

* chore: fix build

* refactor: search list icon

* refactor: search list icon

* chore: lib

* feat: add plugin-os

* feat: add data-dark

* fix: mouse & keyDown

* fix: mouse & keyDown

* fix: mouse & keyDown

---------

Co-authored-by: Steve Lau <stevelauc@outlook.com>
Co-authored-by: medcl <m@medcl.net>
This commit is contained in:
BiggerRain
2025-02-07 20:26:45 +08:00
committed by GitHub
parent 1b1d9bfc40
commit 81a02890d6
9 changed files with 582 additions and 585 deletions

View File

@@ -1,476 +1,443 @@
import { useState, useEffect, useCallback, useRef } from "react"; import { useState, useEffect, useCallback, useRef } from "react";
import { import {
RefreshCcw, RefreshCcw,
Globe, Globe,
PackageOpen, PackageOpen,
GitFork, GitFork,
CalendarSync, CalendarSync,
Trash2, Trash2,
Copy, Copy,
} from "lucide-react"; } from "lucide-react";
import { v4 as uuidv4 } from "uuid"; import { v4 as uuidv4 } from "uuid";
import { getCurrentWindow } from "@tauri-apps/api/window"; import { getCurrentWindow } from "@tauri-apps/api/window";
import { import {
onOpenUrl, onOpenUrl,
getCurrent as getCurrentDeepLinkUrls, getCurrent as getCurrentDeepLinkUrls,
} from "@tauri-apps/plugin-deep-link"; } from "@tauri-apps/plugin-deep-link";
import { invoke } from "@tauri-apps/api/core"; import { invoke } from "@tauri-apps/api/core";
import { UserProfile } from "./UserProfile"; import { UserProfile } from "./UserProfile";
import { DataSourcesList } from "./DataSourcesList"; import { DataSourcesList } from "./DataSourcesList";
import { Sidebar } from "./Sidebar"; import { Sidebar } from "./Sidebar";
import { Connect } from "./Connect.tsx"; import { Connect } from "./Connect";
import { OpenURLWithBrowser } from "@/utils"; import { OpenURLWithBrowser } from "@/utils";
import { useAppStore } from "@/stores/appStore"; import { useAppStore } from "@/stores/appStore";
import { useConnectStore } from "@/stores/connectStore"; import { useConnectStore } from "@/stores/connectStore";
import bannerImg from "@/assets/images/coco-cloud-banner.jpeg"; import bannerImg from "@/assets/images/coco-cloud-banner.jpeg";
export default function Cloud() { export default function Cloud() {
const SidebarRef = useRef<{ refreshData: () => void; }>(null); const SidebarRef = useRef<{ refreshData: () => void }>(null);
// const [error, setError] = useState<string | null>(null); const error = useAppStore((state) => state.error);
const error = useAppStore((state) => state.error); const setError = useAppStore((state) => state.setError);
const setError = useAppStore((state) => state.setError);
const [isConnect, setIsConnect] = useState(true); const [isConnect, setIsConnect] = useState(true);
// const [ssoRequestID, setSSORequestID] = useState("");
const ssoRequestID = useAppStore((state) => state.ssoRequestID);
const setSSORequestID = useAppStore((state) => state.setSSORequestID);
// const ssoServerID = useAppStore((state) => state.ssoServerID); const ssoRequestID = useAppStore((state) => state.ssoRequestID);
// const setSSOServerID = useAppStore((state) => state.setSSOServerID); const setSSORequestID = useAppStore((state) => state.setSSORequestID);
const endpoint = useAppStore((state) => state.endpoint); const endpoint = useAppStore((state) => state.endpoint);
const currentService = useConnectStore((state) => state.currentService); const currentService = useConnectStore((state) => state.currentService);
const setCurrentService = useConnectStore((state) => state.setCurrentService); const setCurrentService = useConnectStore((state) => state.setCurrentService);
const serverList = useConnectStore((state) => state.serverList); const serverList = useConnectStore((state) => state.serverList);
const setServerList = useConnectStore((state) => state.setServerList); const setServerList = useConnectStore((state) => state.setServerList);
const [loading, setLoading] = useState(false); const [loading, setLoading] = useState(false);
const [refreshLoading, setRefreshLoading] = useState(false); const [refreshLoading, setRefreshLoading] = useState(false);
// const [profiles, setProfiles] = useState<any>({});
// const [userInfo, setUserInfo] = useState<any>({});
// fetch the servers
useEffect(() => {
fetchServers(true);
}, []);
useEffect(() => {
console.log("currentService", currentService);
setLoading(false);
setRefreshLoading(false);
setError("");
setIsConnect(true);
}, [JSON.stringify(currentService)]);
//fetch the servers const fetchServers = async (resetSelection: boolean) => {
useEffect(() => { invoke("list_coco_servers")
fetchServers(true); .then((res: any) => {
}, []); console.log("list_coco_servers", res);
setServerList(res);
if (resetSelection && res.length > 0) {
console.log("setCurrentService", res[res.length - 1]);
setCurrentService(res[res.length - 1]);
} else {
console.warn("Service list is empty or last item has no id");
}
})
.catch((err: any) => {
setError(err);
console.error(err);
});
};
useEffect(() => { const add_coco_server = (endpointLink: string) => {
console.log("currentService", currentService); if (!endpointLink) {
setLoading(false); throw new Error("Endpoint is required");
}
if (
!endpointLink.startsWith("http://") &&
!endpointLink.startsWith("https://")
) {
throw new Error("Invalid Endpoint");
}
setRefreshLoading(true);
return invoke("add_coco_server", { endpoint: endpointLink })
.then((res: any) => {
console.log("add_coco_server", res);
fetchServers(false)
.then((r) => {
console.log("fetchServers", r);
setCurrentService(res);
})
.catch((err: any) => {
console.error("fetchServers failed:", err);
setError(err);
throw err; // Propagate error back up to outer promise chain
});
})
.catch((err: any) => {
// Handle the invoke error
console.error("add coco server failed:", err);
setError(err);
throw err; // Propagate error back up
})
.finally(() => {
setRefreshLoading(false); setRefreshLoading(false);
setError(""); });
// setEndpoint(currentService.endpoint); };
setIsConnect(true);
// setUserInfo(profiles[endpoint] || {})
}, [JSON.stringify(currentService)]);
// const get_user_profiles = useCallback(() => { const handleOAuthCallback = useCallback(
// invoke("get_user_profiles") async (code: string | null, serverId: string | null) => {
// .then((res: any) => { if (!code) {
// console.log("get_user_profiles", res); setError("No authorization code received");
// setProfiles(res); return;
// console.log("setUserInfo", res[endpoint]); }
// setUserInfo(res[endpoint] || {})
// })
// .catch((err: any) => {
// console.error(err);
// });
// }, [endpoint]);
useEffect(() => { try {
// get_user_profiles() console.log("Handling OAuth callback:", { code, serverId });
}, []) await invoke("handle_sso_callback", {
serverId: serverId, // Make sure 'server_id' is the correct argument
requestId: ssoRequestID, // Make sure 'request_id' is the correct argument
code: code,
});
const fetchServers = async (resetSelection: boolean) => { if (serverId != null) {
invoke("list_coco_servers") refreshClick(serverId);
.then((res: any) => {
console.log("list_coco_servers", res);
setServerList(res);
if (resetSelection && res.length > 0) {
console.log("setCurrentService", res[res.length - 1]);
setCurrentService(res[res.length - 1]);
} else {
console.warn("Service list is empty or last item has no id");
}
})
.catch((err: any) => {
setError(err);
console.error(err);
});
};
const add_coco_server = (endpointLink: string) => {
if (!endpointLink) {
throw new Error('Endpoint is required');
}
if (!endpointLink.startsWith("http://") && !endpointLink.startsWith("https://")) {
throw new Error('Invalid Endpoint');
} }
setRefreshLoading(true); getCurrentWindow()
.setFocus()
.catch((err) => {
setError(err);
});
} catch (e) {
console.error("Sign in failed:", e);
setError("SSO login failed: " + e);
throw error;
} finally {
setLoading(false);
}
},
[ssoRequestID, endpoint]
);
return invoke("add_coco_server", { endpoint: endpointLink }) const handleUrl = (url: string) => {
.then((res: any) => { try {
console.log("add_coco_server", res); const urlObject = new URL(url);
fetchServers(false) console.log("handle urlObject:", urlObject);
.then((r) => {
console.log("fetchServers", r);
setCurrentService(res);
})
.catch((err: any) => {
console.error("fetchServers failed:", err);
setError(err);
throw err; // Propagate error back up to outer promise chain
});
})
.catch((err: any) => {
// Handle the invoke error
console.error("add coco server failed:", err);
setError(err);
throw err; // Propagate error back up
})
.finally(() => {
setRefreshLoading(false);
});
};
const handleOAuthCallback = useCallback( // TODO, pass request_id and check with local, if the request_id are same, then continue
async (code: string | null, serverId: string | null) => { const reqId = urlObject.searchParams.get("request_id");
const code = urlObject.searchParams.get("code");
if (!code) { if (reqId != ssoRequestID) {
setError("No authorization code received"); console.log("Request ID not matched, skip");
return; setError("Request ID not matched, skip");
} return;
}
try { const serverId = currentService?.id;
console.log("Handling OAuth callback:", { code, serverId }); handleOAuthCallback(code, serverId);
await invoke("handle_sso_callback", { } catch (err) {
serverId: serverId, // Make sure 'server_id' is the correct argument console.error("Failed to parse URL:", err);
requestId: ssoRequestID, // Make sure 'request_id' is the correct argument setError("Invalid URL format: " + err);
code: code
});
if (serverId != null) {
refreshClick(serverId);
}
getCurrentWindow()
.setFocus()
.catch((err) => {
setError(err);
});
} catch (e) {
console.error("Sign in failed:", e);
setError("SSO login failed: " + e);
// setAuth(undefined, endpoint);
throw error;
} finally {
setLoading(false);
}
},
[ssoRequestID, endpoint]
);
const handleUrl = (url: string) => {
try {
// url = "coco://oauth_callback?code=cuhhi8o2sdbbbcoe0g10ktmht6aky3jmd4xkwsgvzf748i4zdgr898bfeu3kze7ffdusdtbgtnpke8ng3fe6&provider=coco-cloud/"
const urlObject = new URL(url);
console.log("handle urlObject:", urlObject);
//TODO, pass request_id and check with local, if the request_id are same, then continue
const reqId = urlObject.searchParams.get("request_id");
const code = urlObject.searchParams.get("code");
if (reqId != ssoRequestID) {
console.log("Request ID not matched, skip");
setError("Request ID not matched, skip");
return;
}
const serverId = currentService?.id;
handleOAuthCallback(code, serverId);
// switch (urlObject.hostname) {
// case "/oauth_callback":
// break;
// default:
// console.log("Unhandled deep link path:", urlObject.pathname);
// }
} catch (err) {
console.error("Failed to parse URL:", err);
setError("Invalid URL format: " + err);
}
};
// Fetch the initial deep link intent
useEffect(() => {
// Function to handle pasted URL
const handlePaste = (event: any) => {
const pastedText = event.clipboardData.getData('text');
console.log("handle paste text:", pastedText);
if (isValidCallbackUrl(pastedText)) {
// Handle the URL as if it's a deep link
console.log("handle callback on paste:", pastedText);
handleUrl(pastedText);
}
};
// Function to check if the pasted URL is valid for our deep link scheme
const isValidCallbackUrl = (url: string) => {
return url && url.startsWith('coco://oauth_callback');
};
// Adding event listener for paste events
document.addEventListener('paste', handlePaste);
getCurrentDeepLinkUrls()
.then((urls) => {
console.log("URLs:", urls);
if (urls && urls.length > 0) {
if (isValidCallbackUrl(urls[0])) {
handleUrl(urls[0]);
}
}
})
.catch((err) => {
console.error("Failed to get initial URLs:", err);
setError("Failed to get initial URLs: " + err);
});
const unlisten = onOpenUrl((urls) => handleUrl(urls[0]));
return () => {
unlisten.then((fn) => fn());
document.removeEventListener('paste', handlePaste);
};
}, [ssoRequestID]);
// const generateLogin = () => {
// const requestID = uuidv4();
// setSSORequestID(requestID);
// setSSOServerID(currentService?.id); // Set server ID
//
// // The URL is now updated when ssoRequestID and ssoServerID are both set
// };
const LoginClick = useCallback(() => {
if (loading) return; // Prevent multiple clicks if already loading
// If the appUid doesn't exist, generate one
// if (!ssoRequestID) {
let requestID = uuidv4();
setSSORequestID(requestID);
// setSSOServerID(currentService?.id);
// }
// Generate the login URL with the current appUid
const url = `${currentService?.auth_provider?.sso?.url}/?provider=${currentService?.id}&product=coco&request_id=${requestID}`;
console.log("Open SSO link, requestID:", ssoRequestID, url);
// Open the URL in a browser
OpenURLWithBrowser(url);
// Start loading state
setLoading(true);
}, [ssoRequestID, loading, currentService]);
const refreshClick = (id: string) => {
setRefreshLoading(true);
invoke("refresh_coco_server_info", { id })
.then((res: any) => {
console.log("refresh_coco_server_info", id, JSON.stringify(res));
fetchServers(false).then(r => {
console.log("fetchServers", r);
});
//update currentService
setCurrentService(res);
})
.catch((err: any) => {
setError(err);
console.error(err);
})
.finally(() => {
setRefreshLoading(false);
});
};
function onAddServer() {
setIsConnect(false);
}
function onLogout(id: string) {
console.log("onLogout", id);
setRefreshLoading(true);
invoke("logout_coco_server", { id })
.then((res: any) => {
console.log("logout_coco_server", id, JSON.stringify(res));
refreshClick(id);
})
.catch((err: any) => {
setError(err);
console.error(err);
}).finally(() => {
setRefreshLoading(false);
});
} }
};
const remove_coco_server = (id: string) => { // Fetch the initial deep link intent
invoke("remove_coco_server", { id }) useEffect(() => {
.then((res: any) => { // Test the handleUrl function
console.log("remove_coco_server", id, JSON.stringify(res)); // handleUrl("coco://oauth_callback?code=cui88lg2sdb4dnu97jpgypcugrskkt1i3venntth7gk52exnq8hxufxvqn8hhegoaw369s394bcyb6ehtnhz&request_id=642a985c-6baa-4ec8-be41-d8c6ddbc0e60&provider=coco-cloud/");
fetchServers(true).then(r => { // Function to handle pasted URL
console.log("fetchServers", r); const handlePaste = (event: any) => {
}) const pastedText = event.clipboardData.getData("text");
}) console.log("handle paste text:", pastedText);
.catch((err: any) => { if (isValidCallbackUrl(pastedText)) {
//TODO display the error message // Handle the URL as if it's a deep link
setError(err); console.log("handle callback on paste:", pastedText);
console.error(err); handleUrl(pastedText);
}); }
}; };
return ( // Function to check if the pasted URL is valid for our deep link scheme
<div className="flex bg-gray-50 dark:bg-gray-900"> const isValidCallbackUrl = (url: string) => {
<Sidebar ref={SidebarRef} onAddServer={onAddServer} serverList={serverList} /> return url && url.startsWith("coco://oauth_callback");
};
<main className="flex-1 p-4 py-8"> // Adding event listener for paste events
document.addEventListener("paste", handlePaste);
{isConnect ? ( getCurrentDeepLinkUrls()
<div className="max-w-4xl mx-auto"> .then((urls) => {
<div className="w-full rounded-[4px] bg-[rgba(229,229,229,1)] dark:bg-gray-800 mb-6"> console.log("URLs:", urls);
<img if (urls && urls.length > 0) {
width="100%" if (isValidCallbackUrl(urls[0])) {
src={currentService?.provider?.banner || bannerImg} handleUrl(urls[0]);
alt="banner" }
/> }
</div> })
<div className="flex items-center justify-between mb-4"> .catch((err) => {
<div className="flex items-center space-x-3"> console.error("Failed to get initial URLs:", err);
<div className="flex items-center text-gray-900 dark:text-white font-medium"> setError("Failed to get initial URLs: " + err);
{currentService?.name} });
</div>
</div>
<div className="flex gap-2">
<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"
onClick={() => OpenURLWithBrowser(currentService?.provider?.website)}
>
<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"
onClick={() => refreshClick(currentService?.id)}
>
<RefreshCcw
className={`w-3.5 h-3.5 ${refreshLoading ? "animate-spin" : ""
}`}
/>
</button>
{!currentService?.builtin && ( const unlisten = onOpenUrl((urls) => handleUrl(urls[0]));
<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"
onClick={() => remove_coco_server(currentService?.id)}
>
<Trash2 className="w-3.5 h-3.5 text-[#ff4747]" />
</button>
)}
</div>
</div>
<div className="mb-8"> return () => {
<div className="text-sm text-gray-500 dark:text-gray-400 mb-2 flex"> unlisten.then((fn) => fn());
<span className="flex items-center gap-1"> document.removeEventListener("paste", handlePaste);
<PackageOpen className="w-4 h-4" />{" "} };
{currentService?.provider?.name} }, [ssoRequestID]);
</span>
<span className="mx-4">|</span>
<span className="flex items-center gap-1">
<GitFork className="w-4 h-4" />{" "}
{currentService?.version?.number}
</span>
<span className="mx-4">|</span>
<span className="flex items-center gap-1">
<CalendarSync className="w-4 h-4" /> {currentService?.updated}
</span>
</div>
<p className="text-gray-600 dark:text-gray-300 leading-relaxed">
{currentService?.provider?.description}
</p>
</div>
{currentService?.auth_provider?.sso?.url ? ( const LoginClick = useCallback(() => {
<div className="mb-8"> if (loading) return; // Prevent multiple clicks if already loading
<h2 className="text-lg font-medium text-gray-900 dark:text-white mb-4">
Account Information
</h2>
{currentService?.profile ? (
<UserProfile server={currentService?.id} userInfo={currentService?.profile} onLogout={onLogout} />
) : (
<div>
{/* Login Button (conditionally rendered when not loading) */}
{!loading && (
<button
className="px-6 py-2 bg-blue-500 text-white rounded-md hover:bg-blue-600 transition-colors mb-3"
onClick={LoginClick}
>
Login
</button>
)}
{/* Cancel Button and Copy URL button while loading */} let requestID = uuidv4();
{loading && ( setSSORequestID(requestID);
<div className="flex items-center space-x-2">
<button
className="px-6 py-2 text-white bg-red-500 rounded-md hover:bg-red-600 transition-colors mb-3"
onClick={() => setLoading(false)} // Reset loading state
>
Cancel
</button>
<button
onClick={() => {
navigator.clipboard.writeText(
`${currentService?.auth_provider?.sso?.url}/?provider=${currentService?.id}&product=coco&request_id=${ssoRequestID}`
);
}}
className="text-xl text-blue-500 hover:text-blue-600"
>
<Copy className="inline mr-2" /> {/* Lucide Copy Icon */}
</button>
</div>
)}
{/* Privacy Policy Link */} // Generate the login URL with the current appUid
<button const url = `${currentService?.auth_provider?.sso?.url}/?provider=${currentService?.id}&product=coco&request_id=${requestID}`;
className="text-xs text-[#0096FB] dark:text-blue-400 block"
onClick={() =>
OpenURLWithBrowser(currentService?.provider?.privacy_policy)
}
>
EULA | Privacy Policy
</button>
</div>
)}
</div>
) : null}
{currentService?.profile ? <DataSourcesList server={currentService?.id} /> : null} console.log("Open SSO link, requestID:", ssoRequestID, url);
</div>
) : ( // Open the URL in a browser
<Connect setIsConnect={setIsConnect} onAddServer={add_coco_server} /> OpenURLWithBrowser(url);
// Start loading state
setLoading(true);
}, [ssoRequestID, loading, currentService]);
const refreshClick = (id: string) => {
setRefreshLoading(true);
invoke("refresh_coco_server_info", { id })
.then((res: any) => {
console.log("refresh_coco_server_info", id, JSON.stringify(res));
fetchServers(false).then((r) => {
console.log("fetchServers", r);
});
// update currentService
setCurrentService(res);
})
.catch((err: any) => {
setError(err);
console.error(err);
})
.finally(() => {
setRefreshLoading(false);
});
};
function onAddServer() {
setIsConnect(false);
}
function onLogout(id: string) {
console.log("onLogout", id);
setRefreshLoading(true);
invoke("logout_coco_server", { id })
.then((res: any) => {
console.log("logout_coco_server", id, JSON.stringify(res));
refreshClick(id);
})
.catch((err: any) => {
setError(err);
console.error(err);
})
.finally(() => {
setRefreshLoading(false);
});
}
const remove_coco_server = (id: string) => {
invoke("remove_coco_server", { id })
.then((res: any) => {
console.log("remove_coco_server", id, JSON.stringify(res));
fetchServers(true).then((r) => {
console.log("fetchServers", r);
});
})
.catch((err: any) => {
// TODO display the error message
setError(err);
console.error(err);
});
};
return (
<div className="flex bg-gray-50 dark:bg-gray-900">
<Sidebar
ref={SidebarRef}
onAddServer={onAddServer}
serverList={serverList}
/>
<main className="flex-1 p-4 py-8">
{isConnect ? (
<div className="max-w-4xl mx-auto">
<div className="w-full rounded-[4px] bg-[rgba(229,229,229,1)] dark:bg-gray-800 mb-6">
<img
width="100%"
src={currentService?.provider?.banner || bannerImg}
alt="banner"
/>
</div>
<div className="flex items-center justify-between mb-4">
<div className="flex items-center space-x-3">
<div className="flex items-center text-gray-900 dark:text-white font-medium">
{currentService?.name}
</div>
</div>
<div className="flex gap-2">
<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"
onClick={() =>
OpenURLWithBrowser(currentService?.provider?.website)
}
>
<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"
onClick={() => refreshClick(currentService?.id)}
>
<RefreshCcw
className={`w-3.5 h-3.5 ${
refreshLoading ? "animate-spin" : ""
}`}
/>
</button>
{!currentService?.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"
onClick={() => remove_coco_server(currentService?.id)}
>
<Trash2 className="w-3.5 h-3.5 text-[#ff4747]" />
</button>
)} )}
</main> </div>
</div> </div>
);
<div className="mb-8">
<div className="text-sm text-gray-500 dark:text-gray-400 mb-2 flex">
<span className="flex items-center gap-1">
<PackageOpen className="w-4 h-4" />{" "}
{currentService?.provider?.name}
</span>
<span className="mx-4">|</span>
<span className="flex items-center gap-1">
<GitFork className="w-4 h-4" />{" "}
{currentService?.version?.number}
</span>
<span className="mx-4">|</span>
<span className="flex items-center gap-1">
<CalendarSync className="w-4 h-4" /> {currentService?.updated}
</span>
</div>
<p className="text-gray-600 dark:text-gray-300 leading-relaxed">
{currentService?.provider?.description}
</p>
</div>
{currentService?.auth_provider?.sso?.url ? (
<div className="mb-8">
<h2 className="text-lg font-medium text-gray-900 dark:text-white mb-4">
Account Information
</h2>
{currentService?.profile ? (
<UserProfile
server={currentService?.id}
userInfo={currentService?.profile}
onLogout={onLogout}
/>
) : (
<div>
{/* Login Button (conditionally rendered when not loading) */}
{!loading && (
<button
className="px-6 py-2 bg-blue-500 text-white rounded-md hover:bg-blue-600 transition-colors mb-3"
onClick={LoginClick}
>
Login
</button>
)}
{/* Cancel Button and Copy URL button while loading */}
{loading && (
<div className="flex items-center space-x-2">
<button
className="px-6 py-2 text-white bg-red-500 rounded-md hover:bg-red-600 transition-colors mb-3"
onClick={() => setLoading(false)} // Reset loading state
>
Cancel
</button>
<button
onClick={() => {
navigator.clipboard.writeText(
`${currentService?.auth_provider?.sso?.url}/?provider=${currentService?.id}&product=coco&request_id=${ssoRequestID}`
);
}}
className="text-xl text-blue-500 hover:text-blue-600"
>
<Copy className="inline mr-2" />{" "}
{/* Lucide Copy Icon */}
</button>
</div>
)}
{/* Privacy Policy Link */}
<button
className="text-xs text-[#0096FB] dark:text-blue-400 block"
onClick={() =>
OpenURLWithBrowser(
currentService?.provider?.privacy_policy
)
}
>
EULA | Privacy Policy
</button>
</div>
)}
</div>
) : null}
{currentService?.profile ? (
<DataSourcesList server={currentService?.id} />
) : null}
</div>
) : (
<Connect setIsConnect={setIsConnect} onAddServer={add_coco_server} />
)}
</main>
</div>
);
} }

View File

@@ -107,7 +107,7 @@ export default function ChatInput({
useEffect(() => { useEffect(() => {
const setupListener = async () => { const setupListener = async () => {
const unlisten = await listen("tauri://focus", () => { const unlisten = await listen("tauri://focus", () => {
console.log("Window focused!"); // console.log("Window focused!");
if (isChatMode) { if (isChatMode) {
textareaRef.current?.focus(); textareaRef.current?.focus();
} else { } else {

View File

@@ -1,4 +1,4 @@
import React, { useState, useRef, useEffect } from "react"; import React, { useState, useRef, useEffect, useCallback } from "react";
import { useInfiniteScroll } from "ahooks"; import { useInfiniteScroll } from "ahooks";
import { isTauri, invoke } from "@tauri-apps/api/core"; import { isTauri, invoke } from "@tauri-apps/api/core";
import { open } from "@tauri-apps/plugin-shell"; import { open } from "@tauri-apps/plugin-shell";
@@ -29,10 +29,11 @@ export const DocumentList: React.FC<DocumentListProps> = ({
const [total, setTotal] = useState(0); const [total, setTotal] = useState(0);
const containerRef = useRef<HTMLDivElement>(null); const containerRef = useRef<HTMLDivElement>(null);
const itemRefs = useRef<(HTMLDivElement | null)[]>([]); const itemRefs = useRef<(HTMLDivElement | null)[]>([]);
const [isKeyboardMode, setIsKeyboardMode] = useState(false);
const { data, loading } = useInfiniteScroll( const { data, loading } = useInfiniteScroll(
async (d) => { async (d) => {
const from = d?.list.length || 0; const from = d?.list?.length || 0;
let queryStrings: any = { let queryStrings: any = {
query: input, query: input,
@@ -55,40 +56,26 @@ export const DocumentList: React.FC<DocumentListProps> = ({
const list = response?.hits || []; const list = response?.hits || [];
const total = response?.total_hits || 0; const total = response?.total_hits || 0;
// console.log("docs:", list, total);
setTotal(total); setTotal(total);
getDocDetail(list[0] || {});
return { return {
list, list: list,
hasMore: from + list.length < total, hasMore: list.length === PAGE_SIZE,
}; };
} catch (error) { } catch (error) {
console.error("Failed to fetch documents:", error); console.error("Failed to fetch documents:", error);
return { return {
list: [], list: d?.list || [],
hasMore: false, hasMore: false,
}; };
} }
}, },
{ {
target: containerRef, target: containerRef,
isNoMore: (d) => (d?.list.length || 0) >= total, isNoMore: (d) => !d?.hasMore,
reloadDeps: [input, JSON.stringify(sourceData)], reloadDeps: [input, JSON.stringify(sourceData)],
onBefore: () => {
setTimeout(() => {
const parentRef = containerRef.current;
if (parentRef && parentRef.childElementCount > 10) {
const itemHeight =
(parentRef.firstChild as HTMLElement)?.offsetHeight || 80;
parentRef.scrollTo({
top: (parentRef.lastChild as HTMLElement)?.offsetTop - itemHeight,
behavior: "instant",
});
}
});
},
onFinally: (data) => onFinally(data, containerRef), onFinally: (data) => onFinally(data, containerRef),
} }
); );
@@ -96,22 +83,31 @@ export const DocumentList: React.FC<DocumentListProps> = ({
const onFinally = (data: any, ref: any) => { const onFinally = (data: any, ref: any) => {
if (data?.page === 1) return; if (data?.page === 1) return;
const parentRef = ref.current; const parentRef = ref.current;
if (!parentRef) return; if (!parentRef || selectedItem === null) return;
const itemHeight = parentRef.firstChild?.offsetHeight || 80;
parentRef.scrollTo({ const targetElement = itemRefs.current[selectedItem];
top: if (!targetElement) return;
parentRef.lastChild?.offsetTop - (data?.list?.length + 1) * itemHeight,
behavior: "instant", requestAnimationFrame(() => {
targetElement.scrollIntoView({
behavior: "instant",
block: "nearest",
});
}); });
}; };
function onMouseEnter(index: number, item: any) { const onMouseEnter = useCallback(
getDocDetail(item); (index: number, item: any) => {
setSelectedItem(index); if (isKeyboardMode) return;
} getDocDetail(item);
setSelectedItem(index);
},
[isKeyboardMode, getDocDetail]
);
useEffect(() => { useEffect(() => {
setSelectedItem(null); setSelectedItem(null);
setIsKeyboardMode(false);
}, [isChatMode, input]); }, [isChatMode, input]);
const handleOpenURL = async (url: string) => { const handleOpenURL = async (url: string) => {
@@ -126,28 +122,58 @@ export const DocumentList: React.FC<DocumentListProps> = ({
} }
}; };
const handleKeyDown = (e: KeyboardEvent) => { const handleKeyDown = useCallback(
if (!data?.list?.length) return; (e: KeyboardEvent) => {
if (!data?.list?.length) return;
if (e.key === "ArrowUp") { if (e.key === "ArrowUp" || e.key === "ArrowDown") {
e.preventDefault(); e.preventDefault();
setSelectedItem((prev) => (prev === null || prev === 0 ? 0 : prev - 1)); setIsKeyboardMode(true);
} else if (e.key === "ArrowDown") {
e.preventDefault();
setSelectedItem((prev) =>
prev === null ? 0 : prev === data?.list?.length - 1 ? prev : prev + 1
);
} else if (e.key === "Meta") {
e.preventDefault();
}
if (e.key === "Enter" && selectedItem !== null) { if (e.key === "ArrowUp") {
const item = data?.list?.[selectedItem]; setSelectedItem((prev) => {
if (item?.url) { const newIndex = prev === null || prev === 0 ? 0 : prev - 1;
handleOpenURL(item?.url); getDocDetail(data.list[newIndex]?.document);
return newIndex;
});
} else {
setSelectedItem((prev) => {
const newIndex =
prev === null
? 0
: prev === data.list.length - 1
? prev
: prev + 1;
getDocDetail(data.list[newIndex]?.document);
return newIndex;
});
}
} else if (e.key === "Meta") {
e.preventDefault();
} }
}
}; if (e.key === "Enter" && selectedItem !== null) {
const item = data?.list?.[selectedItem];
if (item?.url) {
handleOpenURL(item?.url);
}
}
},
[data, selectedItem, getDocDetail]
);
useEffect(() => {
const handleMouseMove = (e: MouseEvent) => {
if (e.movementX !== 0 || e.movementY !== 0) {
setIsKeyboardMode(false);
}
};
window.addEventListener("mousemove", handleMouseMove);
return () => {
window.removeEventListener("mousemove", handleMouseMove);
};
}, []);
useEffect(() => { useEffect(() => {
window.addEventListener("keydown", handleKeyDown); window.addEventListener("keydown", handleKeyDown);
@@ -155,13 +181,15 @@ export const DocumentList: React.FC<DocumentListProps> = ({
return () => { return () => {
window.removeEventListener("keydown", handleKeyDown); window.removeEventListener("keydown", handleKeyDown);
}; };
}, [selectedItem]); }, [handleKeyDown]);
useEffect(() => { useEffect(() => {
if (selectedItem !== null && itemRefs.current[selectedItem]) { if (selectedItem !== null && itemRefs.current[selectedItem]) {
itemRefs.current[selectedItem]?.scrollIntoView({ requestAnimationFrame(() => {
behavior: "smooth", itemRefs.current[selectedItem]?.scrollIntoView({
block: "nearest", behavior: "instant",
block: "nearest",
});
}); });
} }
}, [selectedItem]); }, [selectedItem]);
@@ -196,13 +224,8 @@ export const DocumentList: React.FC<DocumentListProps> = ({
}`} }`}
> >
<div className="flex gap-2 items-center flex-1 min-w-0"> <div className="flex gap-2 items-center flex-1 min-w-0">
<ItemIcon item={item} /> <ItemIcon item={item} />
<span <span className={`text-sm truncate`}>{item?.title}</span>
className={`text-sm truncate`}
>
{item?.title}
</span>
</div> </div>
</div> </div>
); );

View File

@@ -91,16 +91,16 @@ export default function Footer({ }: FooterProps) {
<div className="flex items-center gap-3"> <div className="flex items-center gap-3">
<div className="gap-1 flex items-center text-[#666] dark:text-[#666] text-sm"> <div className="gap-1 flex items-center text-[#666] dark:text-[#666] text-sm">
<span className="mr-1.5 ">Quick open</span> <span className="mr-1.5 ">Quick open</span>
<kbd className="docsearch-modal-footer-commands-key pr-1"> <kbd className="coco-modal-footer-commands-key pr-1">
<Command className="w-3 h-3" /> <Command className="w-3 h-3" />
</kbd> </kbd>
<kbd className="docsearch-modal-footer-commands-key pr-1"> <kbd className="coco-modal-footer-commands-key pr-1">
<ArrowDown01 className="w-3 h-3" /> <ArrowDown01 className="w-3 h-3" />
</kbd> </kbd>
</div> </div>
<div className="flex items-center text-[#666] dark:text-[#666] text-sm"> <div className="flex items-center text-[#666] dark:text-[#666] text-sm">
<span className="mr-1.5 ">Open</span> <span className="mr-1.5 ">Open</span>
<kbd className="docsearch-modal-footer-commands-key pr-1"> <kbd className="coco-modal-footer-commands-key pr-1">
<CornerDownLeft className="w-3 h-3" /> <CornerDownLeft className="w-3 h-3" />
</kbd> </kbd>
</div> </div>

View File

@@ -152,7 +152,7 @@ export default function ChatInput({
if (!isTauri()) return; if (!isTauri()) return;
const setupListener = async () => { const setupListener = async () => {
const unlisten = await listen("tauri://focus", () => { const unlisten = await listen("tauri://focus", () => {
console.log("Window focused!"); // console.log("Window focused!");
if (isChatMode) { if (isChatMode) {
textareaRef.current?.focus(); textareaRef.current?.focus();
} else { } else {

View File

@@ -21,17 +21,14 @@ import { ShortcutItem } from "./ShortcutItem";
import { Shortcut } from "./shortcut"; import { Shortcut } from "./shortcut";
import { useShortcutEditor } from "@/hooks/useShortcutEditor"; import { useShortcutEditor } from "@/hooks/useShortcutEditor";
import { useAppStore } from "@/stores/appStore"; import { useAppStore } from "@/stores/appStore";
import {AppTheme} from "@/utils/tauri.ts"; import { AppTheme } from "@/utils/tauri";
import {useTheme} from "@/contexts/ThemeContext.tsx"; import { useTheme } from "@/contexts/ThemeContext";
// import { useAuthStore } from "@/stores/authStore";
// import { useConnectStore } from "@/stores/connectStore";
export function ThemeOption({ export function ThemeOption({
icon: Icon, icon: Icon,
title, title,
theme, theme,
}: { }: {
icon: any; icon: any;
title: string; title: string;
theme: AppTheme; theme: AppTheme;
@@ -41,21 +38,21 @@ export function ThemeOption({
const isSelected = currentTheme === theme; const isSelected = currentTheme === theme;
return ( return (
<button <button
onClick={() => changeTheme(theme)} onClick={() => changeTheme(theme)}
className={`p-4 rounded-lg border-2 ${ className={`p-4 rounded-lg border-2 ${
isSelected isSelected
? "border-blue-500 bg-blue-50 dark:bg-blue-900/20" ? "border-blue-500 bg-blue-50 dark:bg-blue-900/20"
: "border-gray-200 dark:border-gray-700 hover:border-gray-300 dark:hover:border-gray-600" : "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`} } flex flex-col items-center justify-center space-y-2 transition-all`}
>
<Icon className={`w-6 h-6 ${isSelected ? "text-blue-500" : ""}`} />
<span
className={`text-sm font-medium ${isSelected ? "text-blue-500" : ""}`}
> >
<Icon className={`w-6 h-6 ${isSelected ? "text-blue-500" : ""}`} />
<span
className={`text-sm font-medium ${isSelected ? "text-blue-500" : ""}`}
>
{title} {title}
</span> </span>
</button> </button>
); );
} }

View File

@@ -7,7 +7,7 @@ import React, {
} from "react"; } from "react";
import { isTauri, invoke } from "@tauri-apps/api/core"; import { isTauri, invoke } from "@tauri-apps/api/core";
import { getCurrentWindow } from "@tauri-apps/api/window"; import { getCurrentWindow } from "@tauri-apps/api/window";
import { listen } from "@tauri-apps/api/event"; import { listen, emit } from "@tauri-apps/api/event";
import { AppTheme, WindowTheme } from "../utils/tauri"; import { AppTheme, WindowTheme } from "../utils/tauri";
import { useThemeStore } from "../stores/themeStore"; import { useThemeStore } from "../stores/themeStore";
@@ -45,6 +45,8 @@ export function ThemeProvider({ children }: { children: React.ReactNode }) {
unlisten = await currentWindow.onThemeChanged(({ payload: w_theme }) => { unlisten = await currentWindow.onThemeChanged(({ payload: w_theme }) => {
console.log("window New theme:", w_theme); console.log("window New theme:", w_theme);
setWindowTheme(w_theme); setWindowTheme(w_theme);
// Update tray icon
switchTrayIcon(w_theme);
if (theme === "auto") applyTheme(w_theme); if (theme === "auto") applyTheme(w_theme);
}); });
}; };
@@ -75,6 +77,8 @@ export function ThemeProvider({ children }: { children: React.ReactNode }) {
const root = window.document.documentElement; const root = window.document.documentElement;
root.classList.remove("light", "dark"); root.classList.remove("light", "dark");
root.classList.add(displayTheme); root.classList.add(displayTheme);
//
root.setAttribute("data-theme", displayTheme);
} }
// Apply theme to UI and sync with Tauri // Apply theme to UI and sync with Tauri
@@ -91,16 +95,13 @@ export function ThemeProvider({ children }: { children: React.ReactNode }) {
console.error("Failed to update window theme:", err); console.error("Failed to update window theme:", err);
} }
// Update tray icon
await switchTrayIcon(displayTheme);
// Notify other windows to update the theme // Notify other windows to update the theme
// try { try {
// console.log("theme-changed", displayTheme); // console.log("theme-changed", displayTheme);
// await emit("theme-changed", { theme: displayTheme }); await emit("theme-changed", { theme: displayTheme });
// } catch (err) { } catch (err) {
// console.error("Failed to emit theme-changed event:", err); console.error("Failed to emit theme-changed event:", err);
// } }
} }
}; };
@@ -126,19 +127,18 @@ export function ThemeProvider({ children }: { children: React.ReactNode }) {
// Handle theme changes from user interaction // Handle theme changes from user interaction
const changeTheme = async (newTheme: AppTheme) => { const changeTheme = async (newTheme: AppTheme) => {
console.log("Theme changed to:", newTheme);
setTheme(newTheme); setTheme(newTheme);
const displayTheme = getDisplayTheme(newTheme); const displayTheme = getDisplayTheme(newTheme);
await applyTheme(displayTheme); await applyTheme(displayTheme);
}; };
useEffect(() => { useEffect(() => {
if (!isTauri()) return;
let unlisten: () => void; let unlisten: () => void;
const setupListener = async () => { const setupListener = async () => {
unlisten = await listen("theme-changed", (event: any) => { unlisten = await listen("theme-changed", (event: any) => {
console.log("Theme updated to:", event.payload); // console.log("Theme updated to:", event.payload);
changeClassTheme(event.payload.theme) changeClassTheme(event.payload.theme)
}); });
}; };

View File

@@ -2,67 +2,70 @@
@tailwind components; @tailwind components;
@tailwind utilities; @tailwind utilities;
@layer { /* Base variables */
:root { :root {
--background: #ffffff; --spacing-base: 12px;
--foreground: #09090b; --modal-width: 560px;
--border: #e3e3e7; --modal-height: 600px;
--docsearch-primary-color: rgb(149, 5, 153); --searchbox-height: 56px;
--docsearch-text-color: rgb(28, 30, 33); --hit-height: 56px;
--docsearch-spacing: 12px; --footer-height: 44px;
--docsearch-icon-stroke-width: 1.4; --icon-stroke-width: 1.4;
--docsearch-highlight-color: var(--docsearch-primary-color); --background: #ffffff;
--docsearch-muted-color: rgb(150, 159, 175); --foreground: #09090b;
--docsearch-modal-container-background: rgba(101, 108, 133, .8); --border: #e3e3e7;
--docsearch-modal-width: 560px;
--docsearch-modal-height: 600px;
--docsearch-modal-background: rgb(245, 246, 247);
--docsearch-modal-shadow: inset 1px 1px 0 0 rgba(255, 255, 255, .5), 0 3px 8px 0 rgba(85, 90, 100, 1);
--docsearch-searchbox-height: 56px;
--docsearch-searchbox-background: rgb(235, 237, 240);
--docsearch-searchbox-focus-background: #fff;
--docsearch-searchbox-shadow: inset 0 0 0 2px var(--docsearch-primary-color);
--docsearch-hit-height: 56px;
--docsearch-hit-color: rgb(68, 73, 80);
--docsearch-hit-active-color: #fff;
--docsearch-hit-background: #fff;
--docsearch-hit-shadow: 0 1px 3px 0 rgb(212, 217, 225);
--docsearch-key-gradient: linear-gradient(-225deg, rgb(213, 219, 228) 0%, rgb(248, 248, 248) 100%);
--docsearch-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, .4);
--docsearch-footer-height: 44px;
--docsearch-footer-background: #fff;
--docsearch-footer-shadow: 0 -1px 0 0 rgb(224, 227, 232), 0 -3px 6px 0 rgba(69, 98, 155, .12);
--docsearch-icon-color: rgb(21, 21, 21);
}
.dark {
--background: #09090b;
--foreground: #f9f9f9;
--border: #27272a;
--docsearch-text-color: rgb(245, 246, 247);
--docsearch-modal-container-background: rgba(9, 10, 17, .8);
--docsearch-modal-background: rgb(21, 23, 42);
--docsearch-modal-shadow: inset 1px 1px 0 0 rgb(44, 46, 64), 0 3px 8px 0 rgb(0, 3, 9);
--docsearch-searchbox-background: rgb(9, 10, 17);
--docsearch-searchbox-focus-background: #000;
--docsearch-hit-color: rgb(190, 195, 201);
--docsearch-hit-shadow: none;
--docsearch-hit-background: rgb(9, 10, 17);
--docsearch-key-gradient: linear-gradient(-26.5deg, rgb(86, 88, 114) 0%, rgb(49, 53, 91) 100%);
--docsearch-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, .3);
--docsearch-footer-background: rgb(30, 33, 54);
--docsearch-footer-shadow: inset 0 1px 0 0 rgba(73, 76, 106, .5), 0 -4px 8px 0 rgba(0, 0, 0, .2);
--docsearch-muted-color: rgb(127, 132, 151);
--docsearch-icon-color: rgb(255, 255, 255);
}
} }
/* Light theme */
[data-theme="light"] {
--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, .8);
--coco-modal-background: rgb(245, 246, 247);
--coco-modal-shadow: inset 1px 1px 0 0 rgba(255, 255, 255, .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, .4);
--coco-footer-background: #fff;
--coco-footer-shadow: 0 -1px 0 0 rgb(224, 227, 232), 0 -3px 6px 0 rgba(69, 98, 155, .12);
--coco-icon-color: rgb(21, 21, 21);
}
/* Dark theme */
[data-theme="dark"] {
--background: #09090b;
--foreground: #f9f9f9;
--border: #27272a;
--coco-text-color: rgb(245, 246, 247);
--coco-modal-container-background: rgba(9, 10, 17, .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, .3);
--coco-footer-background: rgb(30, 33, 54);
--coco-footer-shadow: inset 0 1px 0 0 rgba(73, 76, 106, .5), 0 -4px 8px 0 rgba(0, 0, 0, .2);
--coco-muted-color: rgb(127, 132, 151);
--coco-icon-color: rgb(255, 255, 255);
}
/* Base styles */
@layer base { @layer base {
* { * {
@apply box-border border-[--border]; @apply box-border border-[--border];
} }
html{ html {
@apply h-full; @apply h-full;
} }
@@ -81,6 +84,7 @@
} }
} }
/* Component styles */
@layer components { @layer components {
.settings-input { .settings-input {
@apply block w-full rounded-md border-gray-300 dark:border-gray-600 @apply block w-full rounded-md border-gray-300 dark:border-gray-600
@@ -99,7 +103,9 @@
} }
} }
/* Utility styles */
@layer utilities { @layer utilities {
/* Scrollbar styles */
.custom-scrollbar { .custom-scrollbar {
scrollbar-width: thin; scrollbar-width: thin;
scrollbar-color: #cbd5e1 transparent; scrollbar-color: #cbd5e1 transparent;
@@ -126,10 +132,12 @@
background-color: #475569; background-color: #475569;
} }
/* Background styles */
.bg-100 { .bg-100 {
background-size: 100% 100%; background-size: 100% 100%;
} }
/* Error page styles */
#error-page { #error-page {
display: flex; display: flex;
justify-content: center; justify-content: center;
@@ -187,19 +195,21 @@
background-color: #f79c42; background-color: #f79c42;
} }
.docsearch-modal-footer-commands-key { /* coco styles */
.coco-modal-footer-commands-key {
display: flex; display: flex;
align-items: center; align-items: center;
justify-content: center; justify-content: center;
border-radius: 4px; border-radius: 4px;
border: 0px; border: 0;
padding: 2px; padding: 2px;
background: var(--docsearch-key-gradient); background: var(--coco-key-gradient);
box-shadow: var(--docsearch-key-shadow); box-shadow: var(--coco-key-shadow);
color: var(--docsearch-muted-color); color: var(--coco-muted-color);
} }
.user-select{ /* User selection styles */
.user-select {
-webkit-touch-callout: none; -webkit-touch-callout: none;
-webkit-user-select: none; -webkit-user-select: none;
-khtml-user-select: none; -khtml-user-select: none;
@@ -207,4 +217,4 @@
-ms-user-select: none; -ms-user-select: none;
user-select: none; user-select: none;
} }
} }

View File

@@ -42,5 +42,5 @@ export default {
}, },
plugins: [], plugins: [],
mode: "jit", mode: "jit",
darkMode: "class", darkMode: ["class", '[data-theme="dark"]'],
}; };