chore: ran linting

This commit is contained in:
Anish Sarkar 2026-02-06 05:35:15 +05:30
parent 00a617ef17
commit aa66928154
44 changed files with 2025 additions and 1658 deletions

View file

@ -38,7 +38,9 @@ export function DocumentTypeChip({ type, className }: { type: string; className?
className={`inline-flex items-center gap-1.5 rounded bg-muted/40 px-2 py-1 text-xs text-muted-foreground max-w-full overflow-hidden ${className ?? ""}`}
>
<span className="opacity-80 flex-shrink-0">{icon}</span>
<span ref={textRef} className="truncate min-w-0">{fullLabel}</span>
<span ref={textRef} className="truncate min-w-0">
{fullLabel}
</span>
</span>
);

View file

@ -68,9 +68,7 @@ export function DocumentsFilters({
const filteredTypes = useMemo(() => {
if (!typeSearchQuery.trim()) return uniqueTypes;
const query = typeSearchQuery.toLowerCase();
return uniqueTypes.filter((type) =>
getDocumentTypeLabel(type).toLowerCase().includes(query)
);
return uniqueTypes.filter((type) => getDocumentTypeLabel(type).toLowerCase().includes(query));
}, [uniqueTypes, typeSearchQuery]);
const typeCounts = useMemo(() => {
@ -156,94 +154,95 @@ export function DocumentsFilters({
{/* Filter Buttons Group */}
<div className="flex items-center gap-2 flex-wrap">
{/* Type Filter */}
<Popover>
<PopoverTrigger asChild>
<Button
variant="outline"
size="sm"
className="h-9 gap-2 border-dashed border-border/60 text-muted-foreground hover:text-foreground hover:border-border"
>
<FileType size={14} className="text-muted-foreground" />
<span className="hidden sm:inline">Type</span>
{activeTypes.length > 0 && (
<span className="flex h-5 w-5 items-center justify-center rounded-full bg-primary text-[10px] font-medium text-primary-foreground">
{activeTypes.length}
</span>
)}
</Button>
</PopoverTrigger>
<PopoverContent className="w-64 !p-0 overflow-hidden" align="end">
<div>
{/* Search input */}
<div className="p-2 border-b border-border/50">
<div className="relative">
<Search className="absolute left-0.5 top-1/2 -translate-y-1/2 h-4 w-4 text-muted-foreground" />
<Input
placeholder="Search types..."
value={typeSearchQuery}
onChange={(e) => setTypeSearchQuery(e.target.value)}
className="h-6 pl-6 text-sm bg-transparent border-0 focus-visible:ring-0"
/>
</div>
</div>
<div className="max-h-[300px] overflow-y-auto overflow-x-hidden py-1.5 px-1.5">
{filteredTypes.length === 0 ? (
<div className="py-6 text-center text-sm text-muted-foreground">
No types found
{/* Type Filter */}
<Popover>
<PopoverTrigger asChild>
<Button
variant="outline"
size="sm"
className="h-9 gap-2 border-dashed border-border/60 text-muted-foreground hover:text-foreground hover:border-border"
>
<FileType size={14} className="text-muted-foreground" />
<span className="hidden sm:inline">Type</span>
{activeTypes.length > 0 && (
<span className="flex h-5 w-5 items-center justify-center rounded-full bg-primary text-[10px] font-medium text-primary-foreground">
{activeTypes.length}
</span>
)}
</Button>
</PopoverTrigger>
<PopoverContent className="w-64 !p-0 overflow-hidden" align="end">
<div>
{/* Search input */}
<div className="p-2 border-b border-border/50">
<div className="relative">
<Search className="absolute left-0.5 top-1/2 -translate-y-1/2 h-4 w-4 text-muted-foreground" />
<Input
placeholder="Search types..."
value={typeSearchQuery}
onChange={(e) => setTypeSearchQuery(e.target.value)}
className="h-6 pl-6 text-sm bg-transparent border-0 focus-visible:ring-0"
/>
</div>
) : (
filteredTypes.map((value: DocumentTypeEnum, i) => (
<button
key={value}
type="button"
className="flex w-full items-center gap-2.5 py-2 px-3 rounded-md hover:bg-muted/50 transition-colors cursor-pointer text-left"
onClick={() => onToggleType(value, !activeTypes.includes(value))}
</div>
<div className="max-h-[300px] overflow-y-auto overflow-x-hidden py-1.5 px-1.5">
{filteredTypes.length === 0 ? (
<div className="py-6 text-center text-sm text-muted-foreground">
No types found
</div>
) : (
filteredTypes.map((value: DocumentTypeEnum, i) => (
<button
key={value}
type="button"
className="flex w-full items-center gap-2.5 py-2 px-3 rounded-md hover:bg-muted/50 transition-colors cursor-pointer text-left"
onClick={() => onToggleType(value, !activeTypes.includes(value))}
>
{/* Icon */}
<div className="flex h-7 w-7 shrink-0 items-center justify-center rounded-md bg-muted/50 text-foreground/80">
{getDocumentTypeIcon(value, "h-4 w-4")}
</div>
{/* Text content */}
<div className="flex flex-col min-w-0 flex-1 gap-0.5">
<span className="text-[13px] font-medium text-foreground truncate leading-tight">
{getDocumentTypeLabel(value)}
</span>
<span className="text-[11px] text-muted-foreground leading-tight">
{typeCounts.get(value)} document
{(typeCounts.get(value) ?? 0) !== 1 ? "s" : ""}
</span>
</div>
{/* Checkbox */}
<Checkbox
id={`${id}-${i}`}
checked={activeTypes.includes(value)}
onCheckedChange={(checked: boolean) => onToggleType(value, !!checked)}
className="h-4 w-4 shrink-0 rounded border-muted-foreground/30 data-[state=checked]:bg-primary data-[state=checked]:border-primary"
/>
</button>
))
)}
</div>
{activeTypes.length > 0 && (
<div className="px-3 pt-1.5 pb-1.5 border-t border-border/50">
<Button
variant="ghost"
size="sm"
className="w-full h-7 text-[11px] text-muted-foreground hover:text-foreground"
onClick={() => {
activeTypes.forEach((t) => {
onToggleType(t, false);
});
}}
>
{/* Icon */}
<div className="flex h-7 w-7 shrink-0 items-center justify-center rounded-md bg-muted/50 text-foreground/80">
{getDocumentTypeIcon(value, "h-4 w-4")}
</div>
{/* Text content */}
<div className="flex flex-col min-w-0 flex-1 gap-0.5">
<span className="text-[13px] font-medium text-foreground truncate leading-tight">
{getDocumentTypeLabel(value)}
</span>
<span className="text-[11px] text-muted-foreground leading-tight">
{typeCounts.get(value)} document{(typeCounts.get(value) ?? 0) !== 1 ? "s" : ""}
</span>
</div>
{/* Checkbox */}
<Checkbox
id={`${id}-${i}`}
checked={activeTypes.includes(value)}
onCheckedChange={(checked: boolean) => onToggleType(value, !!checked)}
className="h-4 w-4 shrink-0 rounded border-muted-foreground/30 data-[state=checked]:bg-primary data-[state=checked]:border-primary"
/>
</button>
))
Clear filters
</Button>
</div>
)}
</div>
{activeTypes.length > 0 && (
<div className="px-3 pt-1.5 pb-1.5 border-t border-border/50">
<Button
variant="ghost"
size="sm"
className="w-full h-7 text-[11px] text-muted-foreground hover:text-foreground"
onClick={() => {
activeTypes.forEach((t) => {
onToggleType(t, false);
});
}}
>
Clear filters
</Button>
</div>
)}
</div>
</PopoverContent>
</Popover>
</PopoverContent>
</Popover>
{/* Bulk Delete Button */}
{selectedIds.size > 0 && (
@ -255,22 +254,14 @@ export function DocumentsFilters({
exit={{ opacity: 0, scale: 0.9 }}
>
{/* Mobile: icon with count */}
<Button
variant="destructive"
size="sm"
className="h-9 gap-1.5 px-2.5 md:hidden"
>
<Button variant="destructive" size="sm" className="h-9 gap-1.5 px-2.5 md:hidden">
<Trash size={14} />
<span className="flex h-5 w-5 items-center justify-center rounded-full bg-destructive-foreground/20 text-[10px] font-medium">
{selectedIds.size}
</span>
</Button>
{/* Desktop: full button */}
<Button
variant="destructive"
size="sm"
className="h-9 gap-2 hidden md:flex"
>
<Button variant="destructive" size="sm" className="h-9 gap-2 hidden md:flex">
<Trash size={14} />
Delete
<span className="flex h-5 w-5 items-center justify-center rounded-full bg-destructive-foreground/20 text-[10px] font-medium">
@ -288,9 +279,12 @@ export function DocumentsFilters({
<CircleAlert size={18} strokeWidth={2} />
</div>
<AlertDialogHeader className="flex-1">
<AlertDialogTitle>Delete {selectedIds.size} document{selectedIds.size !== 1 ? "s" : ""}?</AlertDialogTitle>
<AlertDialogTitle>
Delete {selectedIds.size} document{selectedIds.size !== 1 ? "s" : ""}?
</AlertDialogTitle>
<AlertDialogDescription>
This action cannot be undone. This will permanently delete the selected {selectedIds.size === 1 ? "document" : "documents"} from your search space.
This action cannot be undone. This will permanently delete the selected{" "}
{selectedIds.size === 1 ? "document" : "documents"} from your search space.
</AlertDialogDescription>
</AlertDialogHeader>
</div>

View file

@ -1,7 +1,20 @@
"use client";
import { formatDistanceToNow } from "date-fns";
import { AlertCircle, Calendar, CheckCircle2, ChevronDown, ChevronUp, Clock, FileText, FileX, Loader2, Network, Plus, User } from "lucide-react";
import {
AlertCircle,
Calendar,
CheckCircle2,
ChevronDown,
ChevronUp,
Clock,
FileText,
FileX,
Loader2,
Network,
Plus,
User,
} from "lucide-react";
import { motion } from "motion/react";
import { useTranslations } from "next-intl";
import React, { useRef, useState, useEffect, useCallback } from "react";
@ -10,12 +23,7 @@ import { JsonMetadataViewer } from "@/components/json-metadata-viewer";
import { MarkdownViewer } from "@/components/markdown-viewer";
import { Button } from "@/components/ui/button";
import { Checkbox } from "@/components/ui/checkbox";
import {
Dialog,
DialogContent,
DialogHeader,
DialogTitle,
} from "@/components/ui/dialog";
import { Dialog, DialogContent, DialogHeader, DialogTitle } from "@/components/ui/dialog";
import { Skeleton } from "@/components/ui/skeleton";
import { Spinner } from "@/components/ui/spinner";
import {
@ -35,7 +43,7 @@ import type { ColumnVisibility, Document, DocumentStatus } from "./types";
// Status indicator component for document processing status
function StatusIndicator({ status }: { status?: DocumentStatus }) {
const state = status?.state ?? "ready";
switch (state) {
case "pending":
return (
@ -176,12 +184,10 @@ function SortableHeader({
>
{icon && <span className="opacity-60">{icon}</span>}
{children}
<span className={`transition-opacity ${isActive ? "opacity-100" : "opacity-0 group-hover:opacity-50"}`}>
{isActive && sortDesc ? (
<ChevronDown size={14} />
) : (
<ChevronUp size={14} />
)}
<span
className={`transition-opacity ${isActive ? "opacity-100" : "opacity-0 group-hover:opacity-50"}`}
>
{isActive && sortDesc ? <ChevronDown size={14} /> : <ChevronUp size={14} />}
</span>
</button>
);
@ -300,8 +306,10 @@ export function DocumentsTableShell({
// Only consider selectable documents for "select all" logic
const selectableDocs = sorted.filter(isSelectable);
const allSelectedOnPage = selectableDocs.length > 0 && selectableDocs.every((d) => selectedIds.has(d.id));
const someSelectedOnPage = selectableDocs.some((d) => selectedIds.has(d.id)) && !allSelectedOnPage;
const allSelectedOnPage =
selectableDocs.length > 0 && selectableDocs.every((d) => selectedIds.has(d.id));
const someSelectedOnPage =
selectableDocs.some((d) => selectedIds.has(d.id)) && !allSelectedOnPage;
const toggleAll = (checked: boolean) => {
const next = new Set(selectedIds);
@ -388,10 +396,7 @@ export function DocumentsTableShell({
</div>
</TableCell>
<TableCell className="w-[35%] py-2.5 max-w-0 border-r border-border/40">
<Skeleton
className="h-4"
style={{ width: `${widthPercent}%` }}
/>
<Skeleton className="h-4" style={{ width: `${widthPercent}%` }} />
</TableCell>
{columnVisibility.document_type && (
<TableCell className="w-[20%] min-w-[120px] max-w-[200px] py-2.5 border-r border-border/40 overflow-hidden">
@ -429,24 +434,15 @@ export function DocumentsTableShell({
<div className="flex items-start gap-3">
<Skeleton className="h-4 w-4 mt-0.5 rounded" />
<div className="flex-1 min-w-0 space-y-2">
<Skeleton
className="h-4"
style={{ width: `${widthPercent}%` }}
/>
<Skeleton className="h-4" style={{ width: `${widthPercent}%` }} />
<div className="flex flex-wrap items-center gap-2">
<Skeleton className="h-5 w-20 rounded" />
{columnVisibility.created_by && (
<Skeleton className="h-3 w-14" />
)}
{columnVisibility.created_at && (
<Skeleton className="h-3 w-20" />
)}
{columnVisibility.created_by && <Skeleton className="h-3 w-14" />}
{columnVisibility.created_at && <Skeleton className="h-3 w-20" />}
</div>
</div>
<div className="flex items-center gap-2">
{columnVisibility.status && (
<Skeleton className="h-5 w-5 rounded-full" />
)}
{columnVisibility.status && <Skeleton className="h-5 w-5 rounded-full" />}
<Skeleton className="h-7 w-7 rounded" />
</div>
</div>
@ -549,9 +545,7 @@ export function DocumentsTableShell({
)}
{columnVisibility.status && (
<TableHead className="w-20 text-center">
<span className="text-sm font-medium text-muted-foreground/70">
Status
</span>
<span className="text-sm font-medium text-muted-foreground/70">Status</span>
</TableHead>
)}
<TableHead className="w-10">
@ -580,9 +574,7 @@ export function DocumentsTableShell({
},
}}
className={`border-b border-border/40 transition-colors ${
isSelected
? "bg-primary/5 hover:bg-primary/8"
: "hover:bg-muted/30"
isSelected ? "bg-primary/5 hover:bg-primary/8" : "hover:bg-muted/30"
}`}
>
<TableCell className="w-8 px-0 py-2.5 text-center">
@ -591,7 +583,9 @@ export function DocumentsTableShell({
checked={isSelected}
onCheckedChange={(v) => canSelect && toggleOne(doc.id, !!v)}
disabled={!canSelect}
aria-label={canSelect ? "Select row" : "Cannot select while processing"}
aria-label={
canSelect ? "Select row" : "Cannot select while processing"
}
className={`border-foreground data-[state=checked]:bg-primary data-[state=checked]:border-primary ${!canSelect ? "opacity-40 cursor-not-allowed" : ""}`}
/>
</div>
@ -639,7 +633,9 @@ export function DocumentsTableShell({
<TableCell className="w-32 py-2.5 text-sm text-foreground border-r border-border/40">
<Tooltip>
<TooltipTrigger asChild>
<span className="cursor-default">{formatRelativeDate(doc.created_at)}</span>
<span className="cursor-default">
{formatRelativeDate(doc.created_at)}
</span>
</TooltipTrigger>
<TooltipContent side="top">
{formatAbsoluteDate(doc.created_at)}
@ -720,9 +716,7 @@ export function DocumentsTableShell({
<div className="flex flex-wrap items-center gap-2">
<DocumentTypeChip type={doc.document_type} />
{columnVisibility.created_by && doc.created_by_name && (
<span className="text-xs text-foreground">
{doc.created_by_name}
</span>
<span className="text-xs text-foreground">{doc.created_by_name}</span>
)}
{columnVisibility.created_at && (
<Tooltip>

View file

@ -46,7 +46,8 @@ export function RowActions({
);
// Documents in "pending" or "processing" state should show disabled delete
const isBeingProcessed = document.status?.state === "pending" || document.status?.state === "processing";
const isBeingProcessed =
document.status?.state === "pending" || document.status?.state === "processing";
// SURFSENSE_DOCS are system-managed and should not show delete at all
const shouldShowDelete = !NON_DELETABLE_DOCUMENT_TYPES.includes(
@ -67,8 +68,9 @@ export function RowActions({
} catch (error: unknown) {
console.error("Error deleting document:", error);
// Check for 409 Conflict (document started processing after UI loaded)
const status = (error as { response?: { status?: number } })?.response?.status
?? (error as { status?: number })?.status;
const status =
(error as { response?: { status?: number } })?.response?.status ??
(error as { status?: number })?.status;
if (status === 409) {
toast.error("Document is now being processed. Please try again later.");
} else {
@ -92,7 +94,11 @@ export function RowActions({
// Editable documents: show 3-dot dropdown with edit + delete
<DropdownMenu>
<DropdownMenuTrigger asChild>
<Button variant="ghost" size="icon" className="h-8 w-8 text-muted-foreground hover:text-foreground hover:bg-muted/80">
<Button
variant="ghost"
size="icon"
className="h-8 w-8 text-muted-foreground hover:text-foreground hover:bg-muted/80"
>
<MoreHorizontal className="h-4 w-4" />
<span className="sr-only">Open menu</span>
</Button>
@ -101,7 +107,9 @@ export function RowActions({
<DropdownMenuItem
onClick={() => !isEditDisabled && handleEdit()}
disabled={isEditDisabled}
className={isEditDisabled ? "text-muted-foreground cursor-not-allowed opacity-50" : ""}
className={
isEditDisabled ? "text-muted-foreground cursor-not-allowed opacity-50" : ""
}
>
<Pencil className="mr-2 h-4 w-4" />
<span>Edit</span>
@ -110,7 +118,11 @@ export function RowActions({
<DropdownMenuItem
onClick={() => !isDeleteDisabled && setIsDeleteOpen(true)}
disabled={isDeleteDisabled}
className={isDeleteDisabled ? "text-muted-foreground cursor-not-allowed opacity-50" : "text-destructive focus:text-destructive"}
className={
isDeleteDisabled
? "text-muted-foreground cursor-not-allowed opacity-50"
: "text-destructive focus:text-destructive"
}
>
<Trash2 className="mr-2 h-4 w-4" />
<span>Delete</span>
@ -150,7 +162,9 @@ export function RowActions({
<DropdownMenuItem
onClick={() => !isEditDisabled && handleEdit()}
disabled={isEditDisabled}
className={isEditDisabled ? "text-muted-foreground cursor-not-allowed opacity-50" : ""}
className={
isEditDisabled ? "text-muted-foreground cursor-not-allowed opacity-50" : ""
}
>
<Pencil className="mr-2 h-4 w-4" />
<span>Edit</span>
@ -159,7 +173,11 @@ export function RowActions({
<DropdownMenuItem
onClick={() => !isDeleteDisabled && setIsDeleteOpen(true)}
disabled={isDeleteDisabled}
className={isDeleteDisabled ? "text-muted-foreground cursor-not-allowed opacity-50" : "text-destructive focus:text-destructive"}
className={
isDeleteDisabled
? "text-muted-foreground cursor-not-allowed opacity-50"
: "text-destructive focus:text-destructive"
}
>
<Trash2 className="mr-2 h-4 w-4" />
<span>Delete</span>

View file

@ -116,13 +116,15 @@ export default function DocumentsTable() {
created_by_id: item.created_by_id ?? null,
created_by_name: item.created_by_name ?? null,
created_at: item.created_at,
status: (item as { status?: { state: "ready" | "pending" | "processing" | "failed"; reason?: string } }).status ?? { state: "ready" as const },
status: (
item as {
status?: { state: "ready" | "pending" | "processing" | "failed"; reason?: string };
}
).status ?? { state: "ready" as const },
}))
: paginatedRealtimeDocuments;
const displayTotal = isSearchMode
? searchResponse?.total || 0
: sortedRealtimeDocuments.length;
const displayTotal = isSearchMode ? searchResponse?.total || 0 : sortedRealtimeDocuments.length;
const loading = isSearchMode ? isSearchLoading : realtimeLoading;
const error = isSearchMode ? searchError : realtimeError;
@ -149,13 +151,13 @@ export default function DocumentsTable() {
// Filter out pending/processing documents - they cannot be deleted
// For real-time mode, use sortedRealtimeDocuments (which has status)
// For search mode, use searchResponse items (need to safely access status)
const allDocs = isSearchMode
? (searchResponse?.items || []).map(item => ({
id: item.id,
status: (item as { status?: { state: string } }).status,
}))
: sortedRealtimeDocuments.map(doc => ({ id: doc.id, status: doc.status }));
const allDocs = isSearchMode
? (searchResponse?.items || []).map((item) => ({
id: item.id,
status: (item as { status?: { state: string } }).status,
}))
: sortedRealtimeDocuments.map((doc) => ({ id: doc.id, status: doc.status }));
const selectedDocs = allDocs.filter((doc) => selectedIds.has(doc.id));
const deletableIds = selectedDocs
.filter((doc) => doc.status?.state !== "pending" && doc.status?.state !== "processing")
@ -163,7 +165,9 @@ export default function DocumentsTable() {
const inProgressCount = selectedIds.size - deletableIds.length;
if (inProgressCount > 0) {
toast.warning(`${inProgressCount} document(s) are pending or processing and cannot be deleted.`);
toast.warning(
`${inProgressCount} document(s) are pending or processing and cannot be deleted.`
);
}
if (deletableIds.length === 0) {
@ -180,8 +184,9 @@ export default function DocumentsTable() {
await deleteDocumentMutation({ id });
return true;
} catch (error: unknown) {
const status = (error as { response?: { status?: number } })?.response?.status
?? (error as { status?: number })?.status;
const status =
(error as { response?: { status?: number } })?.response?.status ??
(error as { status?: number })?.status;
if (status === 409) conflictCount++;
return false;
}
@ -195,13 +200,13 @@ export default function DocumentsTable() {
} else {
toast.error(t("delete_partial_failed"));
}
// If in search mode, refetch search results to reflect deletion
if (isSearchMode) {
await refetchSearch();
}
// Real-time mode: Electric will sync the deletion automatically
setSelectedIds(new Set());
} catch (e) {
console.error(e);
@ -210,21 +215,24 @@ export default function DocumentsTable() {
};
// Single document delete handler for RowActions
const handleDeleteDocument = useCallback(async (id: number): Promise<boolean> => {
try {
await deleteDocumentMutation({ id });
toast.success(t("delete_success") || "Document deleted");
// If in search mode, refetch search results to reflect deletion
if (isSearchMode) {
await refetchSearch();
const handleDeleteDocument = useCallback(
async (id: number): Promise<boolean> => {
try {
await deleteDocumentMutation({ id });
toast.success(t("delete_success") || "Document deleted");
// If in search mode, refetch search results to reflect deletion
if (isSearchMode) {
await refetchSearch();
}
// Real-time mode: Electric will sync the deletion automatically
return true;
} catch (e) {
console.error("Error deleting document:", e);
return false;
}
// Real-time mode: Electric will sync the deletion automatically
return true;
} catch (e) {
console.error("Error deleting document:", e);
return false;
}
}, [deleteDocumentMutation, isSearchMode, refetchSearch, t]);
},
[deleteDocumentMutation, isSearchMode, refetchSearch, t]
);
const handleSortChange = useCallback((key: SortKey) => {
setSortKey((currentKey) => {

View file

@ -2,4 +2,3 @@ import { atom } from "jotai";
// Atom to control the connector dialog open state from anywhere in the app
export const connectorDialogOpenAtom = atom(false);

View file

@ -191,7 +191,9 @@ export const ConnectorIndicator: FC<{ hideTrigger?: boolean }> = ({ hideTrigger
{!hideTrigger && (
<TooltipIconButton
data-joyride="connector-icon"
tooltip={hasConnectors ? `Manage ${activeConnectorsCount} connectors` : "Connect your data"}
tooltip={
hasConnectors ? `Manage ${activeConnectorsCount} connectors` : "Connect your data"
}
side="bottom"
className={cn(
"size-[34px] rounded-full p-1 flex items-center justify-center transition-colors relative",

View file

@ -346,13 +346,13 @@ export const useConnectorDialog = () => {
const connectorId = parseInt(params.connectorId, 10);
newConnector = result.data.find((c: SearchSourceConnector) => c.id === connectorId);
// If we found the connector, find the matching OAuth/Composio connector by type
if (newConnector) {
const connectorType = newConnector.connector_type;
oauthConnector =
OAUTH_CONNECTORS.find((c) => c.connectorType === connectorType) ||
COMPOSIO_CONNECTORS.find((c) => c.connectorType === connectorType);
}
// If we found the connector, find the matching OAuth/Composio connector by type
if (newConnector) {
const connectorType = newConnector.connector_type;
oauthConnector =
OAUTH_CONNECTORS.find((c) => c.connectorType === connectorType) ||
COMPOSIO_CONNECTORS.find((c) => c.connectorType === connectorType);
}
}
// If we don't have a connector yet, try to find by connector param
@ -361,12 +361,12 @@ export const useConnectorDialog = () => {
OAUTH_CONNECTORS.find((c) => c.id === params.connector) ||
COMPOSIO_CONNECTORS.find((c) => c.id === params.connector);
if (oauthConnector) {
const oauthConnectorType = oauthConnector.connectorType;
newConnector = result.data.find(
(c: SearchSourceConnector) => c.connector_type === oauthConnectorType
);
}
if (oauthConnector) {
const oauthConnectorType = oauthConnector.connectorType;
newConnector = result.data.find(
(c: SearchSourceConnector) => c.connector_type === oauthConnectorType
);
}
}
if (newConnector && oauthConnector) {
@ -679,11 +679,11 @@ export const useConnectorDialog = () => {
},
});
const successMessage =
currentConnectorType === "MCP_CONNECTOR"
? `${connector.name} added successfully`
: `${connectorTitle} connected and syncing started!`;
toast.success(successMessage);
const successMessage =
currentConnectorType === "MCP_CONNECTOR"
? `${connector.name} added successfully`
: `${connectorTitle} connected and syncing started!`;
toast.success(successMessage);
const url = new URL(window.location.href);
url.searchParams.delete("modal");

View file

@ -8,172 +8,167 @@ import { cn } from "@/lib/utils";
// ///////////////////////////////////////////////////////////////////////////
// Types
export type AnimationVariant =
| "circle"
| "rectangle"
| "gif"
| "polygon"
| "circle-blur";
export type AnimationVariant = "circle" | "rectangle" | "gif" | "polygon" | "circle-blur";
export type AnimationStart =
| "top-left"
| "top-right"
| "bottom-left"
| "bottom-right"
| "center"
| "top-center"
| "bottom-center"
| "bottom-up"
| "top-down"
| "left-right"
| "right-left";
| "top-left"
| "top-right"
| "bottom-left"
| "bottom-right"
| "center"
| "top-center"
| "bottom-center"
| "bottom-up"
| "top-down"
| "left-right"
| "right-left";
interface Animation {
name: string;
css: string;
name: string;
css: string;
}
// ///////////////////////////////////////////////////////////////////////////
// Helper functions
const getPositionCoords = (position: AnimationStart) => {
switch (position) {
case "top-left":
return { cx: "0", cy: "0" };
case "top-right":
return { cx: "40", cy: "0" };
case "bottom-left":
return { cx: "0", cy: "40" };
case "bottom-right":
return { cx: "40", cy: "40" };
case "top-center":
return { cx: "20", cy: "0" };
case "bottom-center":
return { cx: "20", cy: "40" };
case "bottom-up":
case "top-down":
case "left-right":
case "right-left":
return { cx: "20", cy: "20" };
}
switch (position) {
case "top-left":
return { cx: "0", cy: "0" };
case "top-right":
return { cx: "40", cy: "0" };
case "bottom-left":
return { cx: "0", cy: "40" };
case "bottom-right":
return { cx: "40", cy: "40" };
case "top-center":
return { cx: "20", cy: "0" };
case "bottom-center":
return { cx: "20", cy: "40" };
case "bottom-up":
case "top-down":
case "left-right":
case "right-left":
return { cx: "20", cy: "20" };
}
};
const generateSVG = (variant: AnimationVariant, start: AnimationStart) => {
if (variant === "circle-blur") {
if (start === "center") {
return `data:image/svg+xml,<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 40 40"><defs><filter id="blur"><feGaussianBlur stdDeviation="2"/></filter></defs><circle cx="20" cy="20" r="18" fill="white" filter="url(%23blur)"/></svg>`;
}
const positionCoords = getPositionCoords(start);
if (!positionCoords) {
throw new Error(`Invalid start position: ${start}`);
}
const { cx, cy } = positionCoords;
return `data:image/svg+xml,<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 40 40"><defs><filter id="blur"><feGaussianBlur stdDeviation="2"/></filter></defs><circle cx="${cx}" cy="${cy}" r="18" fill="white" filter="url(%23blur)"/></svg>`;
}
if (variant === "circle-blur") {
if (start === "center") {
return `data:image/svg+xml,<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 40 40"><defs><filter id="blur"><feGaussianBlur stdDeviation="2"/></filter></defs><circle cx="20" cy="20" r="18" fill="white" filter="url(%23blur)"/></svg>`;
}
const positionCoords = getPositionCoords(start);
if (!positionCoords) {
throw new Error(`Invalid start position: ${start}`);
}
const { cx, cy } = positionCoords;
return `data:image/svg+xml,<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 40 40"><defs><filter id="blur"><feGaussianBlur stdDeviation="2"/></filter></defs><circle cx="${cx}" cy="${cy}" r="18" fill="white" filter="url(%23blur)"/></svg>`;
}
if (start === "center") return;
if (start === "center") return;
if (variant === "rectangle") return "";
if (variant === "rectangle") return "";
const positionCoords = getPositionCoords(start);
if (!positionCoords) {
throw new Error(`Invalid start position: ${start}`);
}
const { cx, cy } = positionCoords;
const positionCoords = getPositionCoords(start);
if (!positionCoords) {
throw new Error(`Invalid start position: ${start}`);
}
const { cx, cy } = positionCoords;
if (variant === "circle") {
return `data:image/svg+xml,<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 40 40"><circle cx="${cx}" cy="${cy}" r="20" fill="white"/></svg>`;
}
if (variant === "circle") {
return `data:image/svg+xml,<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 40 40"><circle cx="${cx}" cy="${cy}" r="20" fill="white"/></svg>`;
}
return "";
return "";
};
const getTransformOrigin = (start: AnimationStart) => {
switch (start) {
case "top-left":
return "top left";
case "top-right":
return "top right";
case "bottom-left":
return "bottom left";
case "bottom-right":
return "bottom right";
case "top-center":
return "top center";
case "bottom-center":
return "bottom center";
case "bottom-up":
case "top-down":
case "left-right":
case "right-left":
return "center";
}
switch (start) {
case "top-left":
return "top left";
case "top-right":
return "top right";
case "bottom-left":
return "bottom left";
case "bottom-right":
return "bottom right";
case "top-center":
return "top center";
case "bottom-center":
return "bottom center";
case "bottom-up":
case "top-down":
case "left-right":
case "right-left":
return "center";
}
};
export const createAnimation = (
variant: AnimationVariant,
start: AnimationStart = "center",
blur = false,
url?: string,
variant: AnimationVariant,
start: AnimationStart = "center",
blur = false,
url?: string
): Animation => {
const svg = generateSVG(variant, start);
const transformOrigin = getTransformOrigin(start);
const svg = generateSVG(variant, start);
const transformOrigin = getTransformOrigin(start);
if (variant === "rectangle") {
const getClipPath = (direction: AnimationStart) => {
switch (direction) {
case "bottom-up":
return {
from: "polygon(0% 100%, 100% 100%, 100% 100%, 0% 100%)",
to: "polygon(0% 0%, 100% 0%, 100% 100%, 0% 100%)",
};
case "top-down":
return {
from: "polygon(0% 0%, 100% 0%, 100% 0%, 0% 0%)",
to: "polygon(0% 0%, 100% 0%, 100% 100%, 0% 100%)",
};
case "left-right":
return {
from: "polygon(0% 0%, 0% 0%, 0% 100%, 0% 100%)",
to: "polygon(0% 0%, 100% 0%, 100% 100%, 0% 100%)",
};
case "right-left":
return {
from: "polygon(100% 0%, 100% 0%, 100% 100%, 100% 100%)",
to: "polygon(0% 0%, 100% 0%, 100% 100%, 0% 100%)",
};
case "top-left":
return {
from: "polygon(0% 0%, 0% 0%, 0% 0%, 0% 0%)",
to: "polygon(0% 0%, 100% 0%, 100% 100%, 0% 100%)",
};
case "top-right":
return {
from: "polygon(100% 0%, 100% 0%, 100% 0%, 100% 0%)",
to: "polygon(0% 0%, 100% 0%, 100% 100%, 0% 100%)",
};
case "bottom-left":
return {
from: "polygon(0% 100%, 0% 100%, 0% 100%, 0% 100%)",
to: "polygon(0% 0%, 100% 0%, 100% 100%, 0% 100%)",
};
case "bottom-right":
return {
from: "polygon(100% 100%, 100% 100%, 100% 100%, 100% 100%)",
to: "polygon(0% 0%, 100% 0%, 100% 100%, 0% 100%)",
};
default:
return {
from: "polygon(0% 100%, 100% 100%, 100% 100%, 0% 100%)",
to: "polygon(0% 0%, 100% 0%, 100% 100%, 0% 100%)",
};
}
};
if (variant === "rectangle") {
const getClipPath = (direction: AnimationStart) => {
switch (direction) {
case "bottom-up":
return {
from: "polygon(0% 100%, 100% 100%, 100% 100%, 0% 100%)",
to: "polygon(0% 0%, 100% 0%, 100% 100%, 0% 100%)",
};
case "top-down":
return {
from: "polygon(0% 0%, 100% 0%, 100% 0%, 0% 0%)",
to: "polygon(0% 0%, 100% 0%, 100% 100%, 0% 100%)",
};
case "left-right":
return {
from: "polygon(0% 0%, 0% 0%, 0% 100%, 0% 100%)",
to: "polygon(0% 0%, 100% 0%, 100% 100%, 0% 100%)",
};
case "right-left":
return {
from: "polygon(100% 0%, 100% 0%, 100% 100%, 100% 100%)",
to: "polygon(0% 0%, 100% 0%, 100% 100%, 0% 100%)",
};
case "top-left":
return {
from: "polygon(0% 0%, 0% 0%, 0% 0%, 0% 0%)",
to: "polygon(0% 0%, 100% 0%, 100% 100%, 0% 100%)",
};
case "top-right":
return {
from: "polygon(100% 0%, 100% 0%, 100% 0%, 100% 0%)",
to: "polygon(0% 0%, 100% 0%, 100% 100%, 0% 100%)",
};
case "bottom-left":
return {
from: "polygon(0% 100%, 0% 100%, 0% 100%, 0% 100%)",
to: "polygon(0% 0%, 100% 0%, 100% 100%, 0% 100%)",
};
case "bottom-right":
return {
from: "polygon(100% 100%, 100% 100%, 100% 100%, 100% 100%)",
to: "polygon(0% 0%, 100% 0%, 100% 100%, 0% 100%)",
};
default:
return {
from: "polygon(0% 100%, 100% 100%, 100% 100%, 0% 100%)",
to: "polygon(0% 0%, 100% 0%, 100% 100%, 0% 100%)",
};
}
};
const clipPath = getClipPath(start);
const clipPath = getClipPath(start);
return {
name: `${variant}-${start}${blur ? "-blur" : ""}`,
css: `
return {
name: `${variant}-${start}${blur ? "-blur" : ""}`,
css: `
::view-transition-group(root) {
animation-duration: 0.7s;
animation-timing-function: var(--expo-out);
@ -218,12 +213,12 @@ export const createAnimation = (
}
}
`,
};
}
if (variant === "circle" && start == "center") {
return {
name: `${variant}-${start}${blur ? "-blur" : ""}`,
css: `
};
}
if (variant === "circle" && start == "center") {
return {
name: `${variant}-${start}${blur ? "-blur" : ""}`,
css: `
::view-transition-group(root) {
animation-duration: 0.7s;
animation-timing-function: var(--expo-out);
@ -268,12 +263,12 @@ export const createAnimation = (
}
}
`,
};
}
if (variant === "gif") {
return {
name: `${variant}-${start}`,
css: `
};
}
if (variant === "gif") {
return {
name: `${variant}-${start}`,
css: `
::view-transition-group(root) {
animation-timing-function: var(--expo-in);
}
@ -302,14 +297,14 @@ export const createAnimation = (
mask-size: 2000vmax;
}
}`,
};
}
};
}
if (variant === "circle-blur") {
if (start === "center") {
return {
name: `${variant}-${start}`,
css: `
if (variant === "circle-blur") {
if (start === "center") {
return {
name: `${variant}-${start}`,
css: `
::view-transition-group(root) {
animation-timing-function: var(--expo-out);
}
@ -334,12 +329,12 @@ export const createAnimation = (
}
}
`,
};
}
};
}
return {
name: `${variant}-${start}`,
css: `
return {
name: `${variant}-${start}`,
css: `
::view-transition-group(root) {
animation-timing-function: var(--expo-out);
}
@ -364,41 +359,41 @@ export const createAnimation = (
}
}
`,
};
}
};
}
if (variant === "polygon") {
const getPolygonClipPaths = (position: AnimationStart) => {
switch (position) {
case "top-left":
return {
darkFrom: "polygon(50% -71%, -50% 71%, -50% 71%, 50% -71%)",
darkTo: "polygon(50% -71%, -50% 71%, 50% 171%, 171% 50%)",
lightFrom: "polygon(171% 50%, 50% 171%, 50% 171%, 171% 50%)",
lightTo: "polygon(171% 50%, 50% 171%, -50% 71%, 50% -71%)",
};
case "top-right":
return {
darkFrom: "polygon(150% -71%, 250% 71%, 250% 71%, 150% -71%)",
darkTo: "polygon(150% -71%, 250% 71%, 50% 171%, -71% 50%)",
lightFrom: "polygon(-71% 50%, 50% 171%, 50% 171%, -71% 50%)",
lightTo: "polygon(-71% 50%, 50% 171%, 250% 71%, 150% -71%)",
};
default:
return {
darkFrom: "polygon(50% -71%, -50% 71%, -50% 71%, 50% -71%)",
darkTo: "polygon(50% -71%, -50% 71%, 50% 171%, 171% 50%)",
lightFrom: "polygon(171% 50%, 50% 171%, 50% 171%, 171% 50%)",
lightTo: "polygon(171% 50%, 50% 171%, -50% 71%, 50% -71%)",
};
}
};
if (variant === "polygon") {
const getPolygonClipPaths = (position: AnimationStart) => {
switch (position) {
case "top-left":
return {
darkFrom: "polygon(50% -71%, -50% 71%, -50% 71%, 50% -71%)",
darkTo: "polygon(50% -71%, -50% 71%, 50% 171%, 171% 50%)",
lightFrom: "polygon(171% 50%, 50% 171%, 50% 171%, 171% 50%)",
lightTo: "polygon(171% 50%, 50% 171%, -50% 71%, 50% -71%)",
};
case "top-right":
return {
darkFrom: "polygon(150% -71%, 250% 71%, 250% 71%, 150% -71%)",
darkTo: "polygon(150% -71%, 250% 71%, 50% 171%, -71% 50%)",
lightFrom: "polygon(-71% 50%, 50% 171%, 50% 171%, -71% 50%)",
lightTo: "polygon(-71% 50%, 50% 171%, 250% 71%, 150% -71%)",
};
default:
return {
darkFrom: "polygon(50% -71%, -50% 71%, -50% 71%, 50% -71%)",
darkTo: "polygon(50% -71%, -50% 71%, 50% 171%, 171% 50%)",
lightFrom: "polygon(171% 50%, 50% 171%, 50% 171%, 171% 50%)",
lightTo: "polygon(171% 50%, 50% 171%, -50% 71%, 50% -71%)",
};
}
};
const clipPaths = getPolygonClipPaths(start);
const clipPaths = getPolygonClipPaths(start);
return {
name: `${variant}-${start}${blur ? "-blur" : ""}`,
css: `
return {
name: `${variant}-${start}${blur ? "-blur" : ""}`,
css: `
::view-transition-group(root) {
animation-duration: 0.7s;
animation-timing-function: var(--expo-out);
@ -443,35 +438,35 @@ export const createAnimation = (
}
}
`,
};
}
};
}
// Handle circle variants with start positions using clip-path
if (variant === "circle" && start !== "center") {
const getClipPathPosition = (position: AnimationStart) => {
switch (position) {
case "top-left":
return "0% 0%";
case "top-right":
return "100% 0%";
case "bottom-left":
return "0% 100%";
case "bottom-right":
return "100% 100%";
case "top-center":
return "50% 0%";
case "bottom-center":
return "50% 100%";
default:
return "50% 50%";
}
};
// Handle circle variants with start positions using clip-path
if (variant === "circle" && start !== "center") {
const getClipPathPosition = (position: AnimationStart) => {
switch (position) {
case "top-left":
return "0% 0%";
case "top-right":
return "100% 0%";
case "bottom-left":
return "0% 100%";
case "bottom-right":
return "100% 100%";
case "top-center":
return "50% 0%";
case "bottom-center":
return "50% 100%";
default:
return "50% 50%";
}
};
const clipPosition = getClipPathPosition(start);
const clipPosition = getClipPathPosition(start);
return {
name: `${variant}-${start}${blur ? "-blur" : ""}`,
css: `
return {
name: `${variant}-${start}${blur ? "-blur" : ""}`,
css: `
::view-transition-group(root) {
animation-duration: 1s;
animation-timing-function: var(--expo-out);
@ -516,12 +511,12 @@ export const createAnimation = (
}
}
`,
};
}
};
}
return {
name: `${variant}-${start}${blur ? "-blur" : ""}`,
css: `
return {
name: `${variant}-${start}${blur ? "-blur" : ""}`,
css: `
::view-transition-group(root) {
animation-timing-function: var(--expo-in);
}
@ -549,237 +544,229 @@ export const createAnimation = (
}
}
`,
};
};
};
// ///////////////////////////////////////////////////////////////////////////
// Custom hook for theme toggle functionality
export const useThemeToggle = ({
variant = "circle",
start = "center",
blur = false,
gifUrl = "",
variant = "circle",
start = "center",
blur = false,
gifUrl = "",
}: {
variant?: AnimationVariant;
start?: AnimationStart;
blur?: boolean;
gifUrl?: string;
variant?: AnimationVariant;
start?: AnimationStart;
blur?: boolean;
gifUrl?: string;
} = {}) => {
const { theme, setTheme, resolvedTheme } = useTheme();
const { theme, setTheme, resolvedTheme } = useTheme();
const [isDark, setIsDark] = useState(false);
const [isDark, setIsDark] = useState(false);
// Sync isDark state with resolved theme after hydration
useEffect(() => {
setIsDark(resolvedTheme === "dark");
}, [resolvedTheme]);
// Sync isDark state with resolved theme after hydration
useEffect(() => {
setIsDark(resolvedTheme === "dark");
}, [resolvedTheme]);
const styleId = "theme-transition-styles";
const styleId = "theme-transition-styles";
const updateStyles = useCallback((css: string) => {
if (typeof window === "undefined") return;
const updateStyles = useCallback((css: string) => {
if (typeof window === "undefined") return;
let styleElement = document.getElementById(styleId) as HTMLStyleElement;
let styleElement = document.getElementById(styleId) as HTMLStyleElement;
if (!styleElement) {
styleElement = document.createElement("style");
styleElement.id = styleId;
document.head.appendChild(styleElement);
}
if (!styleElement) {
styleElement = document.createElement("style");
styleElement.id = styleId;
document.head.appendChild(styleElement);
}
styleElement.textContent = css;
}, []);
styleElement.textContent = css;
}, []);
const toggleTheme = useCallback(() => {
setIsDark(!isDark);
const toggleTheme = useCallback(() => {
setIsDark(!isDark);
const animation = createAnimation(variant, start, blur, gifUrl);
const animation = createAnimation(variant, start, blur, gifUrl);
updateStyles(animation.css);
updateStyles(animation.css);
if (typeof window === "undefined") return;
if (typeof window === "undefined") return;
const switchTheme = () => {
setTheme(theme === "light" ? "dark" : "light");
};
const switchTheme = () => {
setTheme(theme === "light" ? "dark" : "light");
};
if (!document.startViewTransition) {
switchTheme();
return;
}
if (!document.startViewTransition) {
switchTheme();
return;
}
document.startViewTransition(switchTheme);
}, [theme, setTheme, variant, start, blur, gifUrl, updateStyles, isDark]);
document.startViewTransition(switchTheme);
}, [theme, setTheme, variant, start, blur, gifUrl, updateStyles, isDark]);
const setCrazyLightTheme = useCallback(() => {
setIsDark(false);
const setCrazyLightTheme = useCallback(() => {
setIsDark(false);
const animation = createAnimation(variant, start, blur, gifUrl);
const animation = createAnimation(variant, start, blur, gifUrl);
updateStyles(animation.css);
updateStyles(animation.css);
if (typeof window === "undefined") return;
if (typeof window === "undefined") return;
const switchTheme = () => {
setTheme("light");
};
const switchTheme = () => {
setTheme("light");
};
if (!document.startViewTransition) {
switchTheme();
return;
}
if (!document.startViewTransition) {
switchTheme();
return;
}
document.startViewTransition(switchTheme);
}, [setTheme, variant, start, blur, gifUrl, updateStyles]);
document.startViewTransition(switchTheme);
}, [setTheme, variant, start, blur, gifUrl, updateStyles]);
const setCrazyDarkTheme = useCallback(() => {
setIsDark(true);
const setCrazyDarkTheme = useCallback(() => {
setIsDark(true);
const animation = createAnimation(variant, start, blur, gifUrl);
const animation = createAnimation(variant, start, blur, gifUrl);
updateStyles(animation.css);
updateStyles(animation.css);
if (typeof window === "undefined") return;
if (typeof window === "undefined") return;
const switchTheme = () => {
setTheme("dark");
};
const switchTheme = () => {
setTheme("dark");
};
if (!document.startViewTransition) {
switchTheme();
return;
}
if (!document.startViewTransition) {
switchTheme();
return;
}
document.startViewTransition(switchTheme);
}, [setTheme, variant, start, blur, gifUrl, updateStyles]);
document.startViewTransition(switchTheme);
}, [setTheme, variant, start, blur, gifUrl, updateStyles]);
const setCrazySystemTheme = useCallback(() => {
if (typeof window === "undefined") return;
const setCrazySystemTheme = useCallback(() => {
if (typeof window === "undefined") return;
const prefersDark = window.matchMedia(
"(prefers-color-scheme: dark)",
).matches;
setIsDark(prefersDark);
const prefersDark = window.matchMedia("(prefers-color-scheme: dark)").matches;
setIsDark(prefersDark);
const animation = createAnimation(variant, start, blur, gifUrl);
const animation = createAnimation(variant, start, blur, gifUrl);
updateStyles(animation.css);
updateStyles(animation.css);
const switchTheme = () => {
setTheme("system");
};
const switchTheme = () => {
setTheme("system");
};
if (!document.startViewTransition) {
switchTheme();
return;
}
if (!document.startViewTransition) {
switchTheme();
return;
}
document.startViewTransition(switchTheme);
}, [setTheme, variant, start, blur, gifUrl, updateStyles]);
document.startViewTransition(switchTheme);
}, [setTheme, variant, start, blur, gifUrl, updateStyles]);
return {
isDark,
setIsDark,
toggleTheme,
setCrazyLightTheme,
setCrazyDarkTheme,
setCrazySystemTheme,
};
return {
isDark,
setIsDark,
toggleTheme,
setCrazyLightTheme,
setCrazyDarkTheme,
setCrazySystemTheme,
};
};
// ///////////////////////////////////////////////////////////////////////////
// Theme Toggle Button Component (Sun/Moon Style)
export const ThemeToggleButton = ({
className = "",
variant = "circle",
start = "center",
blur = false,
gifUrl = "",
className = "",
variant = "circle",
start = "center",
blur = false,
gifUrl = "",
}: {
className?: string;
variant?: AnimationVariant;
start?: AnimationStart;
blur?: boolean;
gifUrl?: string;
className?: string;
variant?: AnimationVariant;
start?: AnimationStart;
blur?: boolean;
gifUrl?: string;
}) => {
const { isDark, toggleTheme } = useThemeToggle({
variant,
start,
blur,
gifUrl,
});
const clipId = useId();
const clipPathId = `theme-toggle-clip-${clipId}`;
const { isDark, toggleTheme } = useThemeToggle({
variant,
start,
blur,
gifUrl,
});
const clipId = useId();
const clipPathId = `theme-toggle-clip-${clipId}`;
return (
<button
type="button"
className={cn(
"size-10 cursor-pointer rounded-full p-2 transition-all duration-300 active:scale-95 bg-transparent",
isDark ? "text-white" : "text-black",
className,
)}
onClick={toggleTheme}
aria-label="Toggle theme"
>
<span className="sr-only">Toggle theme</span>
<svg
xmlns="http://www.w3.org/2000/svg"
aria-hidden="true"
fill="currentColor"
strokeLinecap="round"
viewBox="0 0 32 32"
>
<clipPath id={clipPathId}>
<motion.path
animate={{ y: isDark ? 10 : 0, x: isDark ? -12 : 0 }}
transition={{ ease: "easeInOut", duration: 0.35 }}
d="M0-5h30a1 1 0 0 0 9 13v24H0Z"
/>
</clipPath>
<g clipPath={`url(#${clipPathId})`}>
<motion.circle
animate={{ r: isDark ? 10 : 8 }}
transition={{ ease: "easeInOut", duration: 0.35 }}
cx="16"
cy="16"
/>
<motion.g
animate={{
rotate: isDark ? -100 : 0,
scale: isDark ? 0.5 : 1,
opacity: isDark ? 0 : 1,
}}
transition={{ ease: "easeInOut", duration: 0.35 }}
stroke="currentColor"
strokeWidth="1.5"
>
<path d="M16 5.5v-4" />
<path d="M16 30.5v-4" />
<path d="M1.5 16h4" />
<path d="M26.5 16h4" />
<path d="m23.4 8.6 2.8-2.8" />
<path d="m5.7 26.3 2.9-2.9" />
<path d="m5.8 5.8 2.8 2.8" />
<path d="m23.4 23.4 2.9 2.9" />
</motion.g>
</g>
</svg>
</button>
);
return (
<button
type="button"
className={cn(
"size-10 cursor-pointer rounded-full p-2 transition-all duration-300 active:scale-95 bg-transparent",
isDark ? "text-white" : "text-black",
className
)}
onClick={toggleTheme}
aria-label="Toggle theme"
>
<span className="sr-only">Toggle theme</span>
<svg
xmlns="http://www.w3.org/2000/svg"
aria-hidden="true"
fill="currentColor"
strokeLinecap="round"
viewBox="0 0 32 32"
>
<clipPath id={clipPathId}>
<motion.path
animate={{ y: isDark ? 10 : 0, x: isDark ? -12 : 0 }}
transition={{ ease: "easeInOut", duration: 0.35 }}
d="M0-5h30a1 1 0 0 0 9 13v24H0Z"
/>
</clipPath>
<g clipPath={`url(#${clipPathId})`}>
<motion.circle
animate={{ r: isDark ? 10 : 8 }}
transition={{ ease: "easeInOut", duration: 0.35 }}
cx="16"
cy="16"
/>
<motion.g
animate={{
rotate: isDark ? -100 : 0,
scale: isDark ? 0.5 : 1,
opacity: isDark ? 0 : 1,
}}
transition={{ ease: "easeInOut", duration: 0.35 }}
stroke="currentColor"
strokeWidth="1.5"
>
<path d="M16 5.5v-4" />
<path d="M16 30.5v-4" />
<path d="M1.5 16h4" />
<path d="M26.5 16h4" />
<path d="m23.4 8.6 2.8-2.8" />
<path d="m5.7 26.3 2.9-2.9" />
<path d="m5.8 5.8 2.8 2.8" />
<path d="m23.4 23.4 2.9 2.9" />
</motion.g>
</g>
</svg>
</button>
);
};
// ///////////////////////////////////////////////////////////////////////////
// Backwards compatible export (alias for ThemeToggleButton with default settings)
export function ThemeTogglerComponent() {
return (
<ThemeToggleButton
variant="circle"
start="top-right"
className="size-8"
/>
);
return <ThemeToggleButton variant="circle" start="top-right" className="size-8" />;
}
/**

View file

@ -144,7 +144,7 @@ export function useDocuments(
(doc: DocumentElectric): DocumentDisplay => ({
...doc,
created_by_name: doc.created_by_id
? userCacheRef.current.get(doc.created_by_id) ?? null
? (userCacheRef.current.get(doc.created_by_id) ?? null)
: null,
status: doc.status ?? { state: "ready" },
}),
@ -232,7 +232,15 @@ export function useDocuments(
const handle = await client.syncShape({
table: "documents",
where: `search_space_id = ${spaceId}`,
columns: ["id", "document_type", "search_space_id", "title", "created_by_id", "created_at", "status"],
columns: [
"id",
"document_type",
"search_space_id",
"title",
"created_by_id",
"created_at",
"status",
],
primaryKey: ["id"],
});
@ -258,7 +266,10 @@ export function useDocuments(
// Set up live query
const db = client.db as {
live?: {
query: <T>(sql: string, params?: (number | string)[]) => Promise<{
query: <T>(
sql: string,
params?: (number | string)[]
) => Promise<{
subscribe: (cb: (result: { rows: T[] }) => void) => void;
unsubscribe?: () => void;
}>;
@ -297,8 +308,7 @@ export function useDocuments(
if (!mounted || !result.rows) return;
// DEBUG: Log first few raw documents to see what's coming from Electric
console.log("[useDocuments] Raw data sample:", result.rows.slice(0, 3));
console.log("[useDocuments] Raw data sample:", result.rows.slice(0, 3));
const validItems = result.rows.filter(isValidDocument);
const isFullySynced = syncHandleRef.current?.isUpToDate ?? false;
@ -309,8 +319,9 @@ export function useDocuments(
// Fetch user names for new users (non-blocking)
const unknownUserIds = validItems
.filter((doc): doc is DocumentElectric & { created_by_id: string } =>
doc.created_by_id !== null && !userCacheRef.current.has(doc.created_by_id)
.filter(
(doc): doc is DocumentElectric & { created_by_id: string } =>
doc.created_by_id !== null && !userCacheRef.current.has(doc.created_by_id)
)
.map((doc) => doc.created_by_id);
@ -326,7 +337,7 @@ export function useDocuments(
prev.map((doc) => ({
...doc,
created_by_name: doc.created_by_id
? userCacheRef.current.get(doc.created_by_id) ?? null
? (userCacheRef.current.get(doc.created_by_id) ?? null)
: null,
}))
);
@ -358,7 +369,9 @@ export function useDocuments(
// Case 2: Electric is fully synced - TRUST IT COMPLETELY (handles bulk deletes)
if (isFullySynced) {
const liveDocs = deduplicateAndSort(validItems.map(electricToDisplayDoc));
console.log(`[useDocuments] Synced update: ${liveDocs.length} docs (was ${prev.length})`);
console.log(
`[useDocuments] Synced update: ${liveDocs.length} docs (was ${prev.length})`
);
return liveDocs;
}

View file

@ -444,9 +444,9 @@ export async function initElectric(userId: string): Promise<ElectricClient> {
// in use-inbox.ts generating different sync keys on each render.
// That's now fixed (rounded to midnight UTC in getSyncCutoffDate).
// We can safely use shapeKey for fast incremental sync.
const shapeKey = `${userId}_v${SYNC_VERSION}_${table}_${where?.replace(/[^a-zA-Z0-9]/g, "_") || "all"}`;
// Type assertion to PGlite with electric extension
const pgWithElectric = db as unknown as {
electric: {
@ -495,9 +495,7 @@ export async function initElectric(userId: string): Promise<ElectricClient> {
// Parse the WHERE clause to build a DELETE statement
// The WHERE clause is already validated and formatted
await tx.exec(`DELETE FROM ${table} WHERE ${validatedWhere}`);
debugLog(
`[Electric] 🗑️ Cleared ${table} rows matching: ${validatedWhere}`
);
debugLog(`[Electric] 🗑️ Cleared ${table} rows matching: ${validatedWhere}`);
} else {
// No WHERE clause means we're syncing the entire table
await tx.exec(`DELETE FROM ${table}`);
@ -514,10 +512,7 @@ export async function initElectric(userId: string): Promise<ElectricClient> {
},
};
debugLog(
"[Electric] syncShapeToTable config:",
JSON.stringify(shapeConfig, null, 2)
);
debugLog("[Electric] syncShapeToTable config:", JSON.stringify(shapeConfig, null, 2));
let shape: { unsubscribe: () => void; isUpToDate: boolean; stream: unknown };
try {
@ -550,9 +545,7 @@ export async function initElectric(userId: string): Promise<ElectricClient> {
retryError instanceof Error ? retryError.message : String(retryError);
if (retryMessage.includes("Already syncing")) {
// Still syncing - create a placeholder handle that indicates the table is being synced
debugWarn(
`[Electric] ${table} still syncing, creating placeholder handle`
);
debugWarn(`[Electric] ${table} still syncing, creating placeholder handle`);
const placeholderHandle: SyncHandle = {
unsubscribe: () => {
debugLog(`[Electric] Placeholder unsubscribe for: ${cacheKey}`);
@ -656,9 +649,7 @@ export async function initElectric(userId: string): Promise<ElectricClient> {
// Also check stream's isUpToDate property immediately
if (stream?.isUpToDate) {
debugLog(
`[Electric] ✅ Stream isUpToDate is true immediately for ${table}`
);
debugLog(`[Electric] ✅ Stream isUpToDate is true immediately for ${table}`);
resolveInitialSync();
}
}
@ -671,9 +662,7 @@ export async function initElectric(userId: string): Promise<ElectricClient> {
}
if (shape.isUpToDate || stream?.isUpToDate) {
debugLog(
`[Electric] ✅ Sync completed (detected via polling) for ${table}`
);
debugLog(`[Electric] ✅ Sync completed (detected via polling) for ${table}`);
clearInterval(pollInterval);
resolveInitialSync();
}