mirror of
https://github.com/infinilabs/coco-app.git
synced 2025-12-16 11:37:47 +01:00
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:
@@ -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>
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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 {
|
||||||
|
|||||||
@@ -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>
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -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>
|
||||||
|
|||||||
@@ -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 {
|
||||||
|
|||||||
@@ -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>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -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)
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|||||||
134
src/main.css
134
src/main.css
@@ -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;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -42,5 +42,5 @@ export default {
|
|||||||
},
|
},
|
||||||
plugins: [],
|
plugins: [],
|
||||||
mode: "jit",
|
mode: "jit",
|
||||||
darkMode: "class",
|
darkMode: ["class", '[data-theme="dark"]'],
|
||||||
};
|
};
|
||||||
|
|||||||
Reference in New Issue
Block a user