|
|
@@ -1,98 +1,128 @@
|
|
|
import { CopyableText } from '@/vdb/components/shared/copyable-text.js';
|
|
|
import { Button } from '@/vdb/components/ui/button.js';
|
|
|
import { Popover, PopoverContent, PopoverTrigger } from '@/vdb/components/ui/popover.js';
|
|
|
+import { usePageBlock } from '@/vdb/hooks/use-page-block.js';
|
|
|
import { usePage } from '@/vdb/hooks/use-page.js';
|
|
|
import { useUserSettings } from '@/vdb/hooks/use-user-settings.js';
|
|
|
-import { Trans } from '@/vdb/lib/trans.js';
|
|
|
import { cn } from '@/vdb/lib/utils.js';
|
|
|
-import { CodeXmlIcon, InfoIcon } from 'lucide-react';
|
|
|
-import { createContext, useContext, useState } from 'react';
|
|
|
+import { CodeXmlIcon } from 'lucide-react';
|
|
|
+import React, { useEffect, useState } from 'react';
|
|
|
|
|
|
-const LocationWrapperContext = createContext<{
|
|
|
- parentId: string | null;
|
|
|
- hoveredId: string | null;
|
|
|
- setHoveredId: ((id: string | null) => void) | null;
|
|
|
-}>({
|
|
|
- parentId: null,
|
|
|
- hoveredId: null,
|
|
|
- setHoveredId: null,
|
|
|
-});
|
|
|
+// Singleton state for hover tracking
|
|
|
+let globalHoveredId: string | null = null;
|
|
|
+const hoverListeners: Set<(id: string | null) => void> = new Set();
|
|
|
|
|
|
-export function LocationWrapper({
|
|
|
- children,
|
|
|
- blockId,
|
|
|
-}: Readonly<{ children: React.ReactNode; blockId?: string }>) {
|
|
|
+const setGlobalHoveredId = (id: string | null) => {
|
|
|
+ globalHoveredId = id;
|
|
|
+ hoverListeners.forEach(listener => listener(id));
|
|
|
+};
|
|
|
+
|
|
|
+export interface LocationWrapperProps {
|
|
|
+ children: React.ReactNode;
|
|
|
+ identifier?: string;
|
|
|
+}
|
|
|
+
|
|
|
+export function LocationWrapper({ children, identifier }: Readonly<LocationWrapperProps>) {
|
|
|
const page = usePage();
|
|
|
+ const pageBlock = usePageBlock({ optional: true });
|
|
|
const { settings } = useUserSettings();
|
|
|
const [isPopoverOpen, setIsPopoverOpen] = useState(false);
|
|
|
+ const blockId = pageBlock?.blockId ?? null;
|
|
|
const isPageWrapper = !blockId;
|
|
|
|
|
|
- const [hoveredIdTopLevel, setHoveredIdTopLevel] = useState<string | null>(null);
|
|
|
- const { hoveredId, setHoveredId, parentId } = useContext(LocationWrapperContext);
|
|
|
- const id = `${page.pageId}-${blockId ?? 'page'}`;
|
|
|
- const isHovered = hoveredId === id || hoveredIdTopLevel === id;
|
|
|
+ const [hoveredId, setHoveredId] = useState<string | null>(globalHoveredId);
|
|
|
+ const id = `${page.pageId}-${blockId ?? 'page'}-${identifier ?? ''}`;
|
|
|
+ const isHovered = hoveredId === id;
|
|
|
+
|
|
|
+ // Subscribe to global hover changes
|
|
|
+ useEffect(() => {
|
|
|
+ const listener = (newHoveredId: string | null) => {
|
|
|
+ setHoveredId(newHoveredId);
|
|
|
+ };
|
|
|
+ hoverListeners.add(listener);
|
|
|
+ return () => {
|
|
|
+ hoverListeners.delete(listener);
|
|
|
+ };
|
|
|
+ }, []);
|
|
|
|
|
|
const setHoverId = (id: string | null) => {
|
|
|
- if (setHoveredId) {
|
|
|
- setHoveredId(id);
|
|
|
- } else {
|
|
|
- setHoveredIdTopLevel(id);
|
|
|
+ setGlobalHoveredId(id);
|
|
|
+ };
|
|
|
+
|
|
|
+ const handleMouseEnter = () => {
|
|
|
+ // Set this element as hovered
|
|
|
+ setHoverId(id);
|
|
|
+ };
|
|
|
+
|
|
|
+ const handleMouseLeave = () => {
|
|
|
+ // If we're at the top level (page wrapper), go to null
|
|
|
+ // If we're at block level, go to page level
|
|
|
+ // If we're at identifier level, go to block level
|
|
|
+ if (isPageWrapper) {
|
|
|
+ setHoverId(null);
|
|
|
+ } else if (blockId && !identifier) {
|
|
|
+ // Block level - go to page level
|
|
|
+ setHoverId(`${page.pageId}-page-`);
|
|
|
+ } else if (identifier) {
|
|
|
+ // Identifier level - go to block level
|
|
|
+ setHoverId(`${page.pageId}-${blockId}-`);
|
|
|
}
|
|
|
};
|
|
|
|
|
|
if (settings.devMode) {
|
|
|
const pageId = page.pageId;
|
|
|
return (
|
|
|
- <LocationWrapperContext.Provider
|
|
|
- value={{ hoveredId: hoveredIdTopLevel, setHoveredId: setHoveredIdTopLevel, parentId: id }}
|
|
|
+ <div
|
|
|
+ className={cn(
|
|
|
+ `ring-2 transition-all delay-50 relative`,
|
|
|
+ isHovered || isPopoverOpen ? 'ring-dev-mode' : 'ring-transparent',
|
|
|
+ isPageWrapper ? 'ring-inset' : '',
|
|
|
+ identifier ? 'rounded-md' : 'rounded-xl',
|
|
|
+ )}
|
|
|
+ onMouseEnter={handleMouseEnter}
|
|
|
+ onMouseLeave={handleMouseLeave}
|
|
|
>
|
|
|
<div
|
|
|
- className={cn(
|
|
|
- `ring-2 rounded-xl transition-all delay-50 relative`,
|
|
|
- isHovered || isPopoverOpen ? 'ring-dev-mode' : 'ring-transparent',
|
|
|
- isPageWrapper ? 'ring-inset' : '',
|
|
|
- )}
|
|
|
- onMouseEnter={() => setHoverId(id)}
|
|
|
- onMouseLeave={() => setHoverId(parentId)}
|
|
|
+ className={`absolute top-1 right-1 transition-all delay-50 z-10 ${isHovered || isPopoverOpen ? 'visible' : 'invisible'}`}
|
|
|
>
|
|
|
- <div
|
|
|
- className={`absolute top-0.5 right-0.5 transition-all delay-50 z-10 ${isHovered || isPopoverOpen ? 'visible' : 'invisible'}`}
|
|
|
- >
|
|
|
- <Popover open={isPopoverOpen} onOpenChange={setIsPopoverOpen}>
|
|
|
- <PopoverTrigger asChild>
|
|
|
- <Button variant="ghost" size="icon" className="rounded-lg">
|
|
|
- <CodeXmlIcon className="text-dev-mode w-5 h-5" />
|
|
|
- </Button>
|
|
|
- </PopoverTrigger>
|
|
|
- <PopoverContent className="w-60">
|
|
|
- <div className="space-y-2">
|
|
|
- <div className="flex items-center gap-2">
|
|
|
- <InfoIcon className="h-4 w-4 text-dev-mode" />
|
|
|
- <span className="font-medium">
|
|
|
- <Trans>Location Details</Trans>
|
|
|
- </span>
|
|
|
- </div>
|
|
|
- <div className="space-y-1.5">
|
|
|
- {pageId && (
|
|
|
- <div>
|
|
|
- <div className="text-xs text-muted-foreground">pageId</div>
|
|
|
- <CopyableText text={pageId} />
|
|
|
- </div>
|
|
|
- )}
|
|
|
- {blockId && (
|
|
|
- <div>
|
|
|
- <div className="text-xs text-muted-foreground">blockId</div>
|
|
|
- <CopyableText text={blockId} />
|
|
|
- </div>
|
|
|
- )}
|
|
|
- </div>
|
|
|
+ <Popover open={isPopoverOpen} onOpenChange={setIsPopoverOpen}>
|
|
|
+ <PopoverTrigger asChild>
|
|
|
+ <Button
|
|
|
+ variant="secondary"
|
|
|
+ size="icon"
|
|
|
+ className="h-8 w-8 rounded-full bg-dev-mode/10 hover:bg-dev-mode/20 border border-dev-mode/20 shadow-sm"
|
|
|
+ >
|
|
|
+ <CodeXmlIcon className="text-dev-mode w-4 h-4" />
|
|
|
+ </Button>
|
|
|
+ </PopoverTrigger>
|
|
|
+ <PopoverContent className="w-48 p-3">
|
|
|
+ <div className="space-y-2">
|
|
|
+ <div className="space-y-1">
|
|
|
+ {pageId && (
|
|
|
+ <div className="text-xs">
|
|
|
+ <div className="text-muted-foreground mb-0.5">pageId</div>
|
|
|
+ <CopyableText text={pageId} />
|
|
|
+ </div>
|
|
|
+ )}
|
|
|
+ {blockId && (
|
|
|
+ <div className="text-xs">
|
|
|
+ <div className="text-muted-foreground mb-0.5">blockId</div>
|
|
|
+ <CopyableText text={blockId} />
|
|
|
+ </div>
|
|
|
+ )}
|
|
|
+ {identifier && (
|
|
|
+ <div className="text-xs">
|
|
|
+ <div className="text-muted-foreground mb-0.5">identifier</div>
|
|
|
+ <CopyableText text={identifier} />
|
|
|
+ </div>
|
|
|
+ )}
|
|
|
</div>
|
|
|
- </PopoverContent>
|
|
|
- </Popover>
|
|
|
- </div>
|
|
|
- {children}
|
|
|
+ </div>
|
|
|
+ </PopoverContent>
|
|
|
+ </Popover>
|
|
|
</div>
|
|
|
- </LocationWrapperContext.Provider>
|
|
|
+ {children}
|
|
|
+ </div>
|
|
|
);
|
|
|
}
|
|
|
return children;
|