-
10.babelrc
-
40add-data-locator.cjs
-
67app/api/open-in-ide/route.ts
-
44app/globals.css
-
2app/layout.tsx
-
785components/dev-click-to-component.tsx
-
16components/sections/AiCopilotSection.tsx
-
10components/sections/CallToActionSection.tsx
-
24components/sections/DashboardSection.tsx
-
12components/sections/FaqSection.tsx
-
2components/sections/HeroSection.tsx
-
10components/sections/PricingSection.tsx
-
10components/sections/StepsSection.tsx
-
33components/sections/TaskAnalysisSection.tsx
-
35components/sections/TeambyAssistSection.tsx
-
32components/sections/VoiceAgentSection.tsx
-
51public/assets/images/call_to_action_particles.svg
-
BINpublic/assets/images/copilot_content_frame.png
-
BINpublic/assets/images/feature_grid_card_glow.png
-
BINpublic/assets/images/feature_grid_card_orbit1.png
-
BINpublic/assets/images/feature_grid_card_orbit2.png
-
BINpublic/assets/images/footer_object_left.png
-
BINpublic/assets/images/footer_object_right.png
-
BINpublic/assets/images/object_left.png
-
21public/assets/images/object_left.svg
-
BINpublic/assets/images/object_right.png
@ -0,0 +1,10 @@ |
|||||
|
{ |
||||
|
"presets": ["next/babel"], |
||||
|
"env": { |
||||
|
"development": { |
||||
|
"plugins": [ |
||||
|
"./add-data-locator.cjs" |
||||
|
] |
||||
|
} |
||||
|
} |
||||
|
} |
||||
@ -0,0 +1,40 @@ |
|||||
|
module.exports = function addDataLocator({ types: t }) { |
||||
|
return { |
||||
|
name: "add-data-locator", |
||||
|
visitor: { |
||||
|
JSXOpeningElement(path, state) { |
||||
|
if (process.env.NODE_ENV !== "development") { |
||||
|
return; |
||||
|
} |
||||
|
|
||||
|
const filePath = state.file.opts.filename; |
||||
|
|
||||
|
if (!filePath || filePath.includes("node_modules")) { |
||||
|
return; |
||||
|
} |
||||
|
|
||||
|
const attributeExists = path.node.attributes.some( |
||||
|
(attribute) => |
||||
|
t.isJSXAttribute(attribute) && |
||||
|
t.isJSXIdentifier(attribute.name) && |
||||
|
attribute.name.name === "data-locator", |
||||
|
); |
||||
|
|
||||
|
if (attributeExists) { |
||||
|
return; |
||||
|
} |
||||
|
|
||||
|
const lineNumber = path.node.loc?.start.line ?? "unknown"; |
||||
|
const columnNumber = path.node.loc?.start.column ?? "unknown"; |
||||
|
const locatorValue = `${filePath}:${lineNumber}:${columnNumber}`; |
||||
|
|
||||
|
path.node.attributes.push( |
||||
|
t.jsxAttribute( |
||||
|
t.jsxIdentifier("data-locator"), |
||||
|
t.stringLiteral(locatorValue), |
||||
|
), |
||||
|
); |
||||
|
}, |
||||
|
}, |
||||
|
}; |
||||
|
}; |
||||
@ -0,0 +1,67 @@ |
|||||
|
import { NextResponse } from "next/server"; |
||||
|
import { exec } from "child_process"; |
||||
|
import os from "os"; |
||||
|
|
||||
|
export async function POST(req: Request) { |
||||
|
// Never run in production
|
||||
|
if (process.env.NODE_ENV !== "development") { |
||||
|
return NextResponse.json( |
||||
|
{ message: "Not available in production" }, |
||||
|
{ status: 404 } |
||||
|
); |
||||
|
} |
||||
|
|
||||
|
try { |
||||
|
const { locator } = await req.json(); |
||||
|
|
||||
|
if (!locator) { |
||||
|
return NextResponse.json( |
||||
|
{ message: "Locator is required" }, |
||||
|
{ status: 400 } |
||||
|
); |
||||
|
} |
||||
|
|
||||
|
const isWindows = os.platform() === "win32"; |
||||
|
const bin = isWindows ? "antigravity-ide.cmd" : "antigravity-ide"; |
||||
|
const cmd = `${bin} -r -g "${locator}"`; |
||||
|
|
||||
|
return new Promise<Response>((resolve) => { |
||||
|
exec(cmd, (error) => { |
||||
|
if (error) { |
||||
|
// If primary IDE launcher isn't available, try fallback to code/cursor
|
||||
|
const fallbackBin = isWindows ? "code.cmd" : "code"; |
||||
|
const fallbackCmd = `${fallbackBin} -r -g "${locator}"`; |
||||
|
|
||||
|
exec(fallbackCmd, (fallbackError) => { |
||||
|
if (fallbackError) { |
||||
|
console.error( |
||||
|
`[open-in-ide] Error opening file: ${error.message} / Fallback error: ${fallbackError.message}` |
||||
|
); |
||||
|
resolve( |
||||
|
NextResponse.json( |
||||
|
{ |
||||
|
message: "Failed to open file in IDE", |
||||
|
error: error.message, |
||||
|
}, |
||||
|
{ status: 500 } |
||||
|
) |
||||
|
); |
||||
|
} else { |
||||
|
resolve(NextResponse.json({ success: true, via: "code" })); |
||||
|
} |
||||
|
}); |
||||
|
return; |
||||
|
} |
||||
|
|
||||
|
resolve(NextResponse.json({ success: true, via: "antigravity" })); |
||||
|
}); |
||||
|
}); |
||||
|
} catch (err: unknown) { |
||||
|
const message = err instanceof Error ? err.message : String(err); |
||||
|
console.error("[open-in-ide] Handler error:", err); |
||||
|
return NextResponse.json( |
||||
|
{ message: "Internal server error", error: message }, |
||||
|
{ status: 500 } |
||||
|
); |
||||
|
} |
||||
|
} |
||||
@ -0,0 +1,785 @@ |
|||||
|
"use client"; |
||||
|
|
||||
|
import { useEffect, useState, useRef, useCallback } from "react"; |
||||
|
|
||||
|
const IDE_SCHEMES = [ |
||||
|
{ |
||||
|
matches: ["antigravity"], |
||||
|
createUrl: (locator: string) => `antigravity://file/${locator}`, |
||||
|
}, |
||||
|
{ |
||||
|
matches: ["cursor"], |
||||
|
createUrl: (locator: string) => `cursor://file/${locator}`, |
||||
|
}, |
||||
|
{ |
||||
|
matches: ["vscode", "code"], |
||||
|
createUrl: (locator: string) => `vscode://file/${locator}`, |
||||
|
}, |
||||
|
{ |
||||
|
matches: ["webstorm", "intellij"], |
||||
|
createUrl: (locator: string) => `webstorm://open?file=${locator}`, |
||||
|
}, |
||||
|
{ |
||||
|
matches: ["sublime"], |
||||
|
createUrl: (locator: string) => `subl://open?url=file://${locator}`, |
||||
|
}, |
||||
|
{ |
||||
|
matches: ["atom", "nova"], |
||||
|
createUrl: (locator: string) => `atom://open?url=file://${locator}`, |
||||
|
}, |
||||
|
] as const; |
||||
|
|
||||
|
function parseLocator(locator: string) { |
||||
|
const match = locator.match(/^(.*):(\d+|unknown):(\d+|unknown)$/); |
||||
|
|
||||
|
if (!match) { |
||||
|
return { filePath: locator, line: null, column: null }; |
||||
|
} |
||||
|
|
||||
|
const [, filePath, line, column] = match; |
||||
|
|
||||
|
return { |
||||
|
filePath, |
||||
|
line: line === "unknown" ? null : Number(line), |
||||
|
column: column === "unknown" ? null : Number(column), |
||||
|
}; |
||||
|
} |
||||
|
|
||||
|
function formatLocatorLabel(locator: string) { |
||||
|
const { filePath, line } = parseLocator(locator); |
||||
|
const parts = filePath.split(/[/\\]/); |
||||
|
const shortPath = parts.slice(-2).join("/"); |
||||
|
const lineInfo = line !== null ? `:${line}` : ""; |
||||
|
return `${shortPath}${lineInfo}`; |
||||
|
} |
||||
|
|
||||
|
function getHierarchy(startEl: Element | null): Element[] { |
||||
|
const list: Element[] = []; |
||||
|
let curr: Element | null = startEl; |
||||
|
while (curr && curr !== document.body && curr !== document.documentElement) { |
||||
|
if (curr.hasAttribute("data-locator")) { |
||||
|
list.push(curr); |
||||
|
} |
||||
|
curr = curr.parentElement; |
||||
|
} |
||||
|
return list; // [0: innermost leaf, ..., N-1: outermost container]
|
||||
|
} |
||||
|
|
||||
|
export type HierarchyMode = "OUTER" | "PARENT" | "INNER" | "LEVEL"; |
||||
|
|
||||
|
interface HierarchyTarget { |
||||
|
element: Element; |
||||
|
index: number; |
||||
|
total: number; |
||||
|
mode: HierarchyMode; |
||||
|
} |
||||
|
|
||||
|
function selectTargetFromHierarchy( |
||||
|
hierarchy: Element[], |
||||
|
baseMode: "OUTER" | "PARENT" | "INNER", |
||||
|
customIndex: number | null |
||||
|
): HierarchyTarget | null { |
||||
|
if (hierarchy.length === 0) return null; |
||||
|
const total = hierarchy.length; |
||||
|
|
||||
|
if (customIndex !== null && customIndex >= 0 && customIndex < total) { |
||||
|
let mode: HierarchyMode = "LEVEL"; |
||||
|
if (customIndex === 0) mode = "INNER"; |
||||
|
else if (customIndex === total - 1) mode = "OUTER"; |
||||
|
else if (customIndex === 1) mode = "PARENT"; |
||||
|
|
||||
|
return { |
||||
|
element: hierarchy[customIndex], |
||||
|
index: customIndex, |
||||
|
total, |
||||
|
mode, |
||||
|
}; |
||||
|
} |
||||
|
|
||||
|
if (baseMode === "INNER" || total === 1) { |
||||
|
return { |
||||
|
element: hierarchy[0], |
||||
|
index: 0, |
||||
|
total, |
||||
|
mode: "INNER", |
||||
|
}; |
||||
|
} |
||||
|
|
||||
|
if (baseMode === "PARENT") { |
||||
|
const parentIdx = Math.min(1, total - 1); |
||||
|
return { |
||||
|
element: hierarchy[parentIdx], |
||||
|
index: parentIdx, |
||||
|
total, |
||||
|
mode: "PARENT", |
||||
|
}; |
||||
|
} |
||||
|
|
||||
|
// Outer Mode: find outermost component container, filtering out root page/layout if deeper elements exist
|
||||
|
const nonRootIndices: number[] = []; |
||||
|
hierarchy.forEach((el, idx) => { |
||||
|
const loc = el.getAttribute("data-locator") || ""; |
||||
|
if (!loc.includes("app/layout.") && !loc.includes("app/page.")) { |
||||
|
nonRootIndices.push(idx); |
||||
|
} |
||||
|
}); |
||||
|
|
||||
|
const outerIdx = |
||||
|
nonRootIndices.length > 0 |
||||
|
? nonRootIndices[nonRootIndices.length - 1] |
||||
|
: total - 1; |
||||
|
|
||||
|
return { |
||||
|
element: hierarchy[outerIdx], |
||||
|
index: outerIdx, |
||||
|
total, |
||||
|
mode: "OUTER", |
||||
|
}; |
||||
|
} |
||||
|
|
||||
|
interface HoveredTargetState { |
||||
|
rect: DOMRect; |
||||
|
locator: string; |
||||
|
borderRadius: string; |
||||
|
tagName: string; |
||||
|
mode: HierarchyMode; |
||||
|
levelIndex: number; |
||||
|
totalLevels: number; |
||||
|
hierarchyTags: string[]; |
||||
|
} |
||||
|
|
||||
|
export function DevClickToComponent() { |
||||
|
const [isInspecting, setIsInspecting] = useState(false); |
||||
|
const [isMobile, setIsMobile] = useState(false); |
||||
|
const [position, setPosition] = useState<{ x: number; y: number } | null>(null); |
||||
|
const [isDragging, setIsDragging] = useState(false); |
||||
|
const [hoveredTarget, setHoveredTarget] = useState<HoveredTargetState | null>(null); |
||||
|
const [isOpening, setIsOpening] = useState(false); |
||||
|
const [customDepthIndex, setCustomDepthIndex] = useState<number | null>(null); |
||||
|
|
||||
|
const currentElementRef = useRef<Element | null>(null); |
||||
|
const currentHierarchyRef = useRef<Element[]>([]); |
||||
|
const currentDepthIndexRef = useRef<number | null>(null); |
||||
|
const lastPointerPos = useRef<{ x: number; y: number } | null>(null); |
||||
|
const lastHoveredLeafRef = useRef<Element | null>(null); |
||||
|
const dragStart = useRef({ x: 0, y: 0, buttonX: 0, buttonY: 0, hasMoved: false }); |
||||
|
|
||||
|
// Keep ref synced
|
||||
|
useEffect(() => { |
||||
|
currentDepthIndexRef.current = customDepthIndex; |
||||
|
}, [customDepthIndex]); |
||||
|
|
||||
|
// Detect mobile / small screen size
|
||||
|
useEffect(() => { |
||||
|
const checkSize = () => { |
||||
|
setIsMobile(window.innerWidth <= 768); |
||||
|
}; |
||||
|
checkSize(); |
||||
|
window.addEventListener("resize", checkSize); |
||||
|
return () => window.removeEventListener("resize", checkSize); |
||||
|
}, []); |
||||
|
|
||||
|
// Keep button in bounds on resize
|
||||
|
useEffect(() => { |
||||
|
const handleResize = () => { |
||||
|
if (!isMobile) return; |
||||
|
setPosition((prev) => { |
||||
|
if (!prev) return null; |
||||
|
return { |
||||
|
x: Math.max(16, Math.min(window.innerWidth - 66, prev.x)), |
||||
|
y: Math.max(16, Math.min(window.innerHeight - 66, prev.y)), |
||||
|
}; |
||||
|
}); |
||||
|
}; |
||||
|
window.addEventListener("resize", handleResize); |
||||
|
return () => window.removeEventListener("resize", handleResize); |
||||
|
}, [isMobile]); |
||||
|
|
||||
|
const openInIde = useCallback(async (locator: string) => { |
||||
|
const userAgent = navigator.userAgent.toLowerCase(); |
||||
|
const { filePath, line, column } = parseLocator(locator); |
||||
|
const positionSuffix = |
||||
|
line === null ? "" : `:${line}${column === null ? "" : `:${column}`}`; |
||||
|
const locatorString = `${filePath}${positionSuffix}`; |
||||
|
|
||||
|
setIsOpening(true); |
||||
|
setTimeout(() => setIsOpening(false), 600); |
||||
|
|
||||
|
try { |
||||
|
await fetch("/api/open-in-ide", { |
||||
|
method: "POST", |
||||
|
headers: { "Content-Type": "application/json" }, |
||||
|
body: JSON.stringify({ locator: locatorString, userAgent }), |
||||
|
}); |
||||
|
} catch (err) { |
||||
|
console.error("[DevClickToComponent] Error sending API request:", err); |
||||
|
|
||||
|
// Fallback to URL scheme
|
||||
|
const ideUrl = |
||||
|
IDE_SCHEMES.find(({ matches }) => |
||||
|
matches.some((match) => userAgent.includes(match)) |
||||
|
)?.createUrl(locatorString) ?? `antigravity://file/${locatorString}`; |
||||
|
|
||||
|
try { |
||||
|
window.location.href = ideUrl; |
||||
|
} catch { |
||||
|
window.open(`file://${filePath}`, "_blank", "noopener,noreferrer"); |
||||
|
} |
||||
|
} |
||||
|
}, []); |
||||
|
|
||||
|
// Desktop: Alt (Outer) vs Alt+Shift (Parent) vs Alt+Ctrl (Inner) vs Scroll Wheel / Arrow depth stepping
|
||||
|
useEffect(() => { |
||||
|
if (process.env.NODE_ENV !== "development") { |
||||
|
return; |
||||
|
} |
||||
|
|
||||
|
const updateHoverTarget = ( |
||||
|
el: Element | null, |
||||
|
baseMode: "OUTER" | "PARENT" | "INNER", |
||||
|
overrideDepthIndex?: number | null |
||||
|
) => { |
||||
|
const hierarchy = getHierarchy(el); |
||||
|
currentHierarchyRef.current = hierarchy; |
||||
|
|
||||
|
const leafEl = hierarchy[0] || null; |
||||
|
let depthIdx = overrideDepthIndex !== undefined ? overrideDepthIndex : currentDepthIndexRef.current; |
||||
|
|
||||
|
// If moved to a completely different leaf element, reset custom depth index
|
||||
|
if (leafEl !== lastHoveredLeafRef.current) { |
||||
|
lastHoveredLeafRef.current = leafEl; |
||||
|
if (overrideDepthIndex === undefined) { |
||||
|
depthIdx = null; |
||||
|
setCustomDepthIndex(null); |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
const target = selectTargetFromHierarchy(hierarchy, baseMode, depthIdx); |
||||
|
|
||||
|
if (target) { |
||||
|
currentElementRef.current = target.element; |
||||
|
const rect = target.element.getBoundingClientRect(); |
||||
|
const computed = window.getComputedStyle(target.element); |
||||
|
setHoveredTarget({ |
||||
|
rect, |
||||
|
locator: target.element.getAttribute("data-locator") || "", |
||||
|
borderRadius: computed.borderRadius, |
||||
|
tagName: target.element.tagName.toLowerCase(), |
||||
|
mode: target.mode, |
||||
|
levelIndex: target.index, |
||||
|
totalLevels: target.total, |
||||
|
hierarchyTags: hierarchy.map((h) => h.tagName.toLowerCase()), |
||||
|
}); |
||||
|
} else { |
||||
|
currentElementRef.current = null; |
||||
|
setHoveredTarget(null); |
||||
|
} |
||||
|
}; |
||||
|
|
||||
|
const determineBaseMode = (e: { ctrlKey?: boolean; metaKey?: boolean; shiftKey?: boolean }): "OUTER" | "PARENT" | "INNER" => { |
||||
|
if (e.ctrlKey || e.metaKey) return "INNER"; |
||||
|
if (e.shiftKey) return "PARENT"; |
||||
|
return "OUTER"; |
||||
|
}; |
||||
|
|
||||
|
const handlePointerMove = (e: PointerEvent) => { |
||||
|
lastPointerPos.current = { x: e.clientX, y: e.clientY }; |
||||
|
|
||||
|
if (e.altKey) { |
||||
|
const target = e.target instanceof Element ? e.target : null; |
||||
|
const mode = determineBaseMode(e); |
||||
|
updateHoverTarget(target, mode); |
||||
|
} else if (currentElementRef.current) { |
||||
|
currentElementRef.current = null; |
||||
|
setHoveredTarget(null); |
||||
|
setCustomDepthIndex(null); |
||||
|
} |
||||
|
}; |
||||
|
|
||||
|
const handleKeyDown = (e: KeyboardEvent) => { |
||||
|
const isAlt = e.altKey || e.key === "Alt"; |
||||
|
if (!isAlt || !lastPointerPos.current) return; |
||||
|
|
||||
|
const hierarchy = currentHierarchyRef.current; |
||||
|
const total = hierarchy.length; |
||||
|
|
||||
|
// Level Stepping: ArrowDown or '[' steps deeper (towards inner leaf 0)
|
||||
|
if (e.key === "ArrowDown" || e.key === "[") { |
||||
|
e.preventDefault(); |
||||
|
if (total > 0) { |
||||
|
const current = currentDepthIndexRef.current ?? (e.shiftKey ? 1 : total - 1); |
||||
|
const next = Math.max(0, current - 1); |
||||
|
setCustomDepthIndex(next); |
||||
|
const el = document.elementFromPoint(lastPointerPos.current.x, lastPointerPos.current.y); |
||||
|
updateHoverTarget(el, "LEVEL" as any, next); |
||||
|
} |
||||
|
return; |
||||
|
} |
||||
|
|
||||
|
// Level Stepping: ArrowUp or ']' steps outwards (towards outer container N-1)
|
||||
|
if (e.key === "ArrowUp" || e.key === "]") { |
||||
|
e.preventDefault(); |
||||
|
if (total > 0) { |
||||
|
const current = currentDepthIndexRef.current ?? (e.shiftKey ? 1 : 0); |
||||
|
const next = Math.min(total - 1, current + 1); |
||||
|
setCustomDepthIndex(next); |
||||
|
const el = document.elementFromPoint(lastPointerPos.current.x, lastPointerPos.current.y); |
||||
|
updateHoverTarget(el, "LEVEL" as any, next); |
||||
|
} |
||||
|
return; |
||||
|
} |
||||
|
|
||||
|
const mode = determineBaseMode({ |
||||
|
ctrlKey: e.ctrlKey || e.key === "Control", |
||||
|
metaKey: e.metaKey || e.key === "Meta", |
||||
|
shiftKey: e.shiftKey || e.key === "Shift", |
||||
|
}); |
||||
|
|
||||
|
const el = document.elementFromPoint( |
||||
|
lastPointerPos.current.x, |
||||
|
lastPointerPos.current.y |
||||
|
); |
||||
|
updateHoverTarget(el, mode, null); |
||||
|
}; |
||||
|
|
||||
|
const handleKeyUp = (e: KeyboardEvent) => { |
||||
|
if (e.key === "Alt" || !e.altKey) { |
||||
|
currentElementRef.current = null; |
||||
|
setHoveredTarget(null); |
||||
|
setCustomDepthIndex(null); |
||||
|
} else if (e.altKey && lastPointerPos.current) { |
||||
|
// Modifier key released while keeping Alt pressed -> update target accordingly
|
||||
|
const mode = determineBaseMode(e); |
||||
|
const el = document.elementFromPoint( |
||||
|
lastPointerPos.current.x, |
||||
|
lastPointerPos.current.y |
||||
|
); |
||||
|
updateHoverTarget(el, mode, null); |
||||
|
} |
||||
|
}; |
||||
|
|
||||
|
const handleWheel = (e: WheelEvent) => { |
||||
|
if (!e.altKey || !currentHierarchyRef.current.length) return; |
||||
|
|
||||
|
e.preventDefault(); |
||||
|
e.stopPropagation(); |
||||
|
|
||||
|
const total = currentHierarchyRef.current.length; |
||||
|
if (total <= 1) return; |
||||
|
|
||||
|
// Scroll Down (deltaY > 0) -> Step Deeper / Inner (towards index 0)
|
||||
|
// Scroll Up (deltaY < 0) -> Step Outer (towards index total - 1)
|
||||
|
const current = |
||||
|
currentDepthIndexRef.current ?? |
||||
|
(hoveredTarget?.levelIndex ?? (e.ctrlKey || e.metaKey ? 0 : total - 1)); |
||||
|
|
||||
|
const delta = e.deltaY > 0 ? -1 : 1; |
||||
|
const next = Math.max(0, Math.min(total - 1, current + delta)); |
||||
|
|
||||
|
setCustomDepthIndex(next); |
||||
|
|
||||
|
if (lastPointerPos.current) { |
||||
|
const el = document.elementFromPoint( |
||||
|
lastPointerPos.current.x, |
||||
|
lastPointerPos.current.y |
||||
|
); |
||||
|
updateHoverTarget(el, "LEVEL" as any, next); |
||||
|
} |
||||
|
}; |
||||
|
|
||||
|
const handleWindowBlur = () => { |
||||
|
currentElementRef.current = null; |
||||
|
setHoveredTarget(null); |
||||
|
setCustomDepthIndex(null); |
||||
|
}; |
||||
|
|
||||
|
const handleScrollOrResize = () => { |
||||
|
if (currentElementRef.current) { |
||||
|
const rect = currentElementRef.current.getBoundingClientRect(); |
||||
|
const computed = window.getComputedStyle(currentElementRef.current); |
||||
|
setHoveredTarget((prev) => |
||||
|
prev |
||||
|
? { |
||||
|
...prev, |
||||
|
rect, |
||||
|
borderRadius: computed.borderRadius, |
||||
|
} |
||||
|
: null |
||||
|
); |
||||
|
} |
||||
|
}; |
||||
|
|
||||
|
const handleDesktopClick = (event: MouseEvent) => { |
||||
|
if (!event.altKey) { |
||||
|
return; |
||||
|
} |
||||
|
|
||||
|
const target = event.target instanceof Element ? event.target : null; |
||||
|
const hierarchy = getHierarchy(target); |
||||
|
const mode = determineBaseMode(event); |
||||
|
const selected = selectTargetFromHierarchy( |
||||
|
hierarchy, |
||||
|
mode, |
||||
|
currentDepthIndexRef.current |
||||
|
); |
||||
|
|
||||
|
const locator = selected?.element?.getAttribute("data-locator"); |
||||
|
|
||||
|
if (!locator) { |
||||
|
return; |
||||
|
} |
||||
|
|
||||
|
event.preventDefault(); |
||||
|
event.stopPropagation(); |
||||
|
|
||||
|
openInIde(locator); |
||||
|
}; |
||||
|
|
||||
|
window.addEventListener("pointermove", handlePointerMove, true); |
||||
|
window.addEventListener("keydown", handleKeyDown, true); |
||||
|
window.addEventListener("keyup", handleKeyUp, true); |
||||
|
window.addEventListener("wheel", handleWheel, { capture: true, passive: false }); |
||||
|
window.addEventListener("blur", handleWindowBlur); |
||||
|
document.addEventListener("mouseleave", handleWindowBlur); |
||||
|
window.addEventListener("scroll", handleScrollOrResize, true); |
||||
|
window.addEventListener("resize", handleScrollOrResize, true); |
||||
|
document.addEventListener("click", handleDesktopClick, true); |
||||
|
|
||||
|
return () => { |
||||
|
window.removeEventListener("pointermove", handlePointerMove, true); |
||||
|
window.removeEventListener("keydown", handleKeyDown, true); |
||||
|
window.removeEventListener("keyup", handleKeyUp, true); |
||||
|
window.removeEventListener("wheel", handleWheel, true); |
||||
|
window.removeEventListener("blur", handleWindowBlur); |
||||
|
document.removeEventListener("mouseleave", handleWindowBlur); |
||||
|
window.removeEventListener("scroll", handleScrollOrResize, true); |
||||
|
window.removeEventListener("resize", handleScrollOrResize, true); |
||||
|
document.removeEventListener("click", handleDesktopClick, true); |
||||
|
}; |
||||
|
}, [openInIde, hoveredTarget?.levelIndex]); |
||||
|
|
||||
|
// Mobile: Inspect Mode click interception
|
||||
|
useEffect(() => { |
||||
|
if (process.env.NODE_ENV !== "development" || !isMobile || !isInspecting) return; |
||||
|
|
||||
|
const handleInspectClick = async (event: MouseEvent) => { |
||||
|
const target = event.target; |
||||
|
if (!(target instanceof Element)) return; |
||||
|
|
||||
|
// Ignore clicks on the inspect button itself
|
||||
|
if (target.closest(".dev-inspect-btn")) { |
||||
|
return; |
||||
|
} |
||||
|
|
||||
|
event.preventDefault(); |
||||
|
event.stopPropagation(); |
||||
|
|
||||
|
const hierarchy = getHierarchy(target); |
||||
|
// On mobile tap, choose innermost leaf by default or first available element
|
||||
|
const selectedEl = hierarchy[0] || target.closest<HTMLElement>("[data-locator]"); |
||||
|
const locator = selectedEl?.getAttribute("data-locator"); |
||||
|
|
||||
|
if (locator && selectedEl) { |
||||
|
console.log(`[DevClickToComponent] Mobile inspect matched element. Opening in IDE:`, locator); |
||||
|
|
||||
|
if (selectedEl instanceof HTMLElement) { |
||||
|
const originalOutline = selectedEl.style.outline; |
||||
|
selectedEl.style.outline = "3px solid #38bdf8"; |
||||
|
setTimeout(() => { |
||||
|
selectedEl.style.outline = originalOutline; |
||||
|
}, 400); |
||||
|
} |
||||
|
|
||||
|
openInIde(locator); |
||||
|
} |
||||
|
|
||||
|
setIsInspecting(false); |
||||
|
}; |
||||
|
|
||||
|
document.addEventListener("click", handleInspectClick, true); |
||||
|
|
||||
|
return () => { |
||||
|
document.removeEventListener("click", handleInspectClick, true); |
||||
|
}; |
||||
|
}, [isMobile, isInspecting, openInIde]); |
||||
|
|
||||
|
// Dragging event handlers for mobile floating button
|
||||
|
const handlePointerDown = (e: React.PointerEvent<HTMLButtonElement>) => { |
||||
|
e.currentTarget.setPointerCapture(e.pointerId); |
||||
|
setIsDragging(true); |
||||
|
const startX = position?.x ?? window.innerWidth - 66; |
||||
|
const startY = position?.y ?? window.innerHeight - 66; |
||||
|
dragStart.current = { |
||||
|
x: e.clientX, |
||||
|
y: e.clientY, |
||||
|
buttonX: startX, |
||||
|
buttonY: startY, |
||||
|
hasMoved: false, |
||||
|
}; |
||||
|
}; |
||||
|
|
||||
|
const handlePointerMoveButton = (e: React.PointerEvent<HTMLButtonElement>) => { |
||||
|
if (!isDragging) return; |
||||
|
const dx = e.clientX - dragStart.current.x; |
||||
|
const dy = e.clientY - dragStart.current.y; |
||||
|
|
||||
|
if (Math.abs(dx) > 5 || Math.abs(dy) > 5) { |
||||
|
dragStart.current.hasMoved = true; |
||||
|
} |
||||
|
|
||||
|
const newX = Math.max(16, Math.min(window.innerWidth - 66, dragStart.current.buttonX + dx)); |
||||
|
const newY = Math.max(16, Math.min(window.innerHeight - 66, dragStart.current.buttonY + dy)); |
||||
|
|
||||
|
setPosition({ x: newX, y: newY }); |
||||
|
}; |
||||
|
|
||||
|
const handlePointerUpButton = (e: React.PointerEvent<HTMLButtonElement>) => { |
||||
|
if (!isDragging) return; |
||||
|
e.currentTarget.releasePointerCapture(e.pointerId); |
||||
|
setIsDragging(false); |
||||
|
}; |
||||
|
|
||||
|
const handleToggleInspect = () => { |
||||
|
if (!dragStart.current.hasMoved) { |
||||
|
setIsInspecting(!isInspecting); |
||||
|
} |
||||
|
}; |
||||
|
|
||||
|
// Theme colors for different modes
|
||||
|
// INNER: Cyan (#38bdf8), PARENT / LEVEL: Amber (#fbbf24) / Purple (#a78bfa), OUTER: Emerald (#2dd9a4)
|
||||
|
let accentColor = "#2dd9a4"; |
||||
|
let accentBg = "rgba(45, 217, 164, 0.08)"; |
||||
|
let accentShadow = "0 0 14px rgba(45, 217, 164, 0.45), inset 0 0 6px rgba(45, 217, 164, 0.15)"; |
||||
|
let modeBadgeLabel = "OUTER"; |
||||
|
|
||||
|
if (hoveredTarget?.mode === "INNER") { |
||||
|
accentColor = "#38bdf8"; |
||||
|
accentBg = "rgba(56, 189, 248, 0.08)"; |
||||
|
accentShadow = "0 0 14px rgba(56, 189, 248, 0.5), inset 0 0 6px rgba(56, 189, 248, 0.2)"; |
||||
|
modeBadgeLabel = "INNER LEAF"; |
||||
|
} else if (hoveredTarget?.mode === "PARENT") { |
||||
|
accentColor = "#a78bfa"; |
||||
|
accentBg = "rgba(167, 139, 250, 0.08)"; |
||||
|
accentShadow = "0 0 14px rgba(167, 139, 250, 0.45), inset 0 0 6px rgba(167, 139, 250, 0.15)"; |
||||
|
modeBadgeLabel = "PARENT"; |
||||
|
} else if (hoveredTarget?.mode === "LEVEL") { |
||||
|
accentColor = "#fbbf24"; |
||||
|
accentBg = "rgba(251, 191, 36, 0.08)"; |
||||
|
accentShadow = "0 0 14px rgba(251, 191, 36, 0.45), inset 0 0 6px rgba(251, 191, 36, 0.15)"; |
||||
|
modeBadgeLabel = `DEPTH ${hoveredTarget.levelIndex + 1}/${hoveredTarget.totalLevels}`; |
||||
|
} |
||||
|
|
||||
|
if (process.env.NODE_ENV !== "development") { |
||||
|
return null; |
||||
|
} |
||||
|
|
||||
|
return ( |
||||
|
<> |
||||
|
{/* Component Bounding Box Highlight Overlay */} |
||||
|
{hoveredTarget && ( |
||||
|
<div |
||||
|
style={{ |
||||
|
position: "fixed", |
||||
|
top: `${hoveredTarget.rect.top}px`, |
||||
|
left: `${hoveredTarget.rect.left}px`, |
||||
|
width: `${hoveredTarget.rect.width}px`, |
||||
|
height: `${hoveredTarget.rect.height}px`, |
||||
|
borderRadius: hoveredTarget.borderRadius || "4px", |
||||
|
border: isOpening ? "2px solid #ffffff" : `2px solid ${accentColor}`, |
||||
|
backgroundColor: isOpening ? "rgba(255, 255, 255, 0.2)" : accentBg, |
||||
|
boxShadow: isOpening |
||||
|
? `0 0 22px ${accentColor}, inset 0 0 12px ${accentColor}` |
||||
|
: accentShadow, |
||||
|
pointerEvents: "none", |
||||
|
zIndex: 999999, |
||||
|
transition: |
||||
|
"top 0.05s ease-out, left 0.05s ease-out, width 0.05s ease-out, height 0.05s ease-out, border 0.12s, background-color 0.12s, box-shadow 0.12s", |
||||
|
}} |
||||
|
> |
||||
|
{/* Smart Component Info Tag */} |
||||
|
<div |
||||
|
style={{ |
||||
|
position: "absolute", |
||||
|
top: hoveredTarget.rect.top < 34 ? "6px" : "-32px", |
||||
|
left: Math.max(0, -hoveredTarget.rect.left), |
||||
|
display: "inline-flex", |
||||
|
flexDirection: "column", |
||||
|
gap: "3px", |
||||
|
backgroundColor: "#020d09", |
||||
|
color: accentColor, |
||||
|
fontSize: "11px", |
||||
|
fontWeight: 600, |
||||
|
fontFamily: |
||||
|
"ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, monospace", |
||||
|
padding: "4px 8px", |
||||
|
borderRadius: "6px", |
||||
|
whiteSpace: "nowrap", |
||||
|
boxShadow: "0 6px 18px rgba(0,0,0,0.75)", |
||||
|
border: `1px solid ${accentColor}88`, |
||||
|
pointerEvents: "none", |
||||
|
}} |
||||
|
> |
||||
|
{/* Top row: Mode + Tag Name + File Location */} |
||||
|
<div style={{ display: "flex", alignItems: "center", gap: "6px" }}> |
||||
|
<span |
||||
|
style={{ |
||||
|
backgroundColor: `${accentColor}33`, |
||||
|
color: accentColor, |
||||
|
padding: "1px 6px", |
||||
|
borderRadius: "3px", |
||||
|
fontSize: "10px", |
||||
|
fontWeight: 700, |
||||
|
letterSpacing: "0.04em", |
||||
|
}} |
||||
|
> |
||||
|
{modeBadgeLabel} |
||||
|
</span> |
||||
|
|
||||
|
<span style={{ color: "#ffffff", opacity: 0.85, fontWeight: 700 }}> |
||||
|
<{hoveredTarget.tagName}> |
||||
|
</span> |
||||
|
<span style={{ color: accentColor }}> |
||||
|
{formatLocatorLabel(hoveredTarget.locator)} |
||||
|
</span> |
||||
|
|
||||
|
{isOpening && ( |
||||
|
<span style={{ color: "#ffffff", fontWeight: 700 }}> |
||||
|
⚡ Opening IDE... |
||||
|
</span> |
||||
|
)} |
||||
|
</div> |
||||
|
|
||||
|
{/* Bottom row: Breadcrumb Hierarchy Trail & Interactive Shortcut Hints */} |
||||
|
{!isOpening && hoveredTarget.totalLevels > 1 && ( |
||||
|
<div |
||||
|
style={{ |
||||
|
display: "flex", |
||||
|
alignItems: "center", |
||||
|
gap: "4px", |
||||
|
fontSize: "9.5px", |
||||
|
color: "#94a3b8", |
||||
|
paddingTop: "2px", |
||||
|
borderTop: "1px solid rgba(255,255,255,0.08)", |
||||
|
}} |
||||
|
> |
||||
|
{/* Visual breadcrumbs */} |
||||
|
<span style={{ opacity: 0.7 }}>Hierarchy:</span> |
||||
|
{hoveredTarget.hierarchyTags.map((tag, idx) => ( |
||||
|
<span |
||||
|
key={idx} |
||||
|
style={{ |
||||
|
color: |
||||
|
idx === hoveredTarget.levelIndex |
||||
|
? accentColor |
||||
|
: "rgba(255,255,255,0.45)", |
||||
|
fontWeight: idx === hoveredTarget.levelIndex ? 700 : 400, |
||||
|
textDecoration: |
||||
|
idx === hoveredTarget.levelIndex ? "underline" : "none", |
||||
|
}} |
||||
|
> |
||||
|
{tag} |
||||
|
{idx < hoveredTarget.hierarchyTags.length - 1 && " › "} |
||||
|
</span> |
||||
|
))} |
||||
|
|
||||
|
{/* Shortcut Guide */} |
||||
|
<span |
||||
|
style={{ |
||||
|
marginLeft: "8px", |
||||
|
color: "rgba(255,255,255,0.5)", |
||||
|
fontSize: "9px", |
||||
|
}} |
||||
|
> |
||||
|
(Scroll / ↑↓ / [ ]: Level | Ctrl: Inner | Shift: Parent) |
||||
|
</span> |
||||
|
</div> |
||||
|
)} |
||||
|
</div> |
||||
|
</div> |
||||
|
)} |
||||
|
|
||||
|
{/* Floating Action Draggable Button (Inspect Mode for Mobile/Tablet) */} |
||||
|
{isMobile && ( |
||||
|
<div |
||||
|
style={{ |
||||
|
position: "fixed", |
||||
|
left: position ? `${position.x}px` : "auto", |
||||
|
top: position ? `${position.y}px` : "auto", |
||||
|
bottom: position ? "auto" : "16px", |
||||
|
right: position ? "auto" : "16px", |
||||
|
display: "flex", |
||||
|
flexDirection: "column", |
||||
|
gap: "10px", |
||||
|
zIndex: 99998, |
||||
|
touchAction: "none", |
||||
|
}} |
||||
|
> |
||||
|
<button |
||||
|
className="dev-inspect-btn" |
||||
|
onPointerDown={handlePointerDown} |
||||
|
onPointerMove={handlePointerMoveButton} |
||||
|
onPointerUp={handlePointerUpButton} |
||||
|
onClick={handleToggleInspect} |
||||
|
style={{ |
||||
|
width: "50px", |
||||
|
height: "50px", |
||||
|
borderRadius: "25px", |
||||
|
backgroundColor: isInspecting ? "#38bdf8" : "#020d09", |
||||
|
color: isInspecting ? "#020d09" : "#ffffff", |
||||
|
border: isInspecting |
||||
|
? "2px solid #38bdf8" |
||||
|
: "1px solid rgba(255,255,255,0.15)", |
||||
|
boxShadow: isInspecting |
||||
|
? "0 0 16px rgba(56, 189, 248, 0.7)" |
||||
|
: "0 4px 14px rgba(0,0,0,0.4)", |
||||
|
display: "flex", |
||||
|
alignItems: "center", |
||||
|
justifyContent: "center", |
||||
|
cursor: isDragging ? "grabbing" : "pointer", |
||||
|
transition: isDragging |
||||
|
? "none" |
||||
|
: "background-color 0.25s, transform 0.25s, box-shadow 0.25s", |
||||
|
outline: "none", |
||||
|
transform: isInspecting ? "scale(1.1)" : "scale(1)", |
||||
|
}} |
||||
|
title="Toggle Dev Inspect Mode (Drag to move, tap to activate)" |
||||
|
> |
||||
|
{isInspecting ? ( |
||||
|
<svg |
||||
|
xmlns="http://www.w3.org/2000/svg" |
||||
|
fill="none" |
||||
|
viewBox="0 0 24 24" |
||||
|
strokeWidth={2.5} |
||||
|
stroke="currentColor" |
||||
|
style={{ width: "22px", height: "22px", pointerEvents: "none" }} |
||||
|
> |
||||
|
<path |
||||
|
strokeLinecap="round" |
||||
|
strokeLinejoin="round" |
||||
|
d="M6 18 18 6M6 6l12 12" |
||||
|
/> |
||||
|
</svg> |
||||
|
) : ( |
||||
|
<svg |
||||
|
xmlns="http://www.w3.org/2000/svg" |
||||
|
fill="none" |
||||
|
viewBox="0 0 24 24" |
||||
|
strokeWidth={2} |
||||
|
stroke="currentColor" |
||||
|
style={{ width: "22px", height: "22px", pointerEvents: "none" }} |
||||
|
> |
||||
|
<path |
||||
|
strokeLinecap="round" |
||||
|
strokeLinejoin="round" |
||||
|
d="m15.75 15.75-2.489-2.489m0 0a3.375 3.375 0 1 0-4.773-4.773 3.375 3.375 0 0 0 4.774 4.774ZM21 12a9 9 0 1 1-18 0 9 9 0 0 1 18 0Z" |
||||
|
/> |
||||
|
</svg> |
||||
|
)} |
||||
|
</button> |
||||
|
</div> |
||||
|
)} |
||||
|
</> |
||||
|
); |
||||
|
} |
||||
|
|
||||
|
export default DevClickToComponent; |
||||
@ -0,0 +1,51 @@ |
|||||
|
<svg width="915" height="294" viewBox="0 0 915 294" fill="none" xmlns="http://www.w3.org/2000/svg"> |
||||
|
<path d="M790.677 100.648C790.146 100.697 789.754 101.166 789.802 101.698C789.851 102.229 790.32 102.621 790.852 102.572C791.383 102.524 791.775 102.054 791.726 101.523C791.678 100.992 791.208 100.6 790.677 100.648Z" fill="#9DB2D1"/> |
||||
|
<path d="M760.122 181.011C759.591 181.059 759.199 181.529 759.248 182.06C759.296 182.591 759.766 182.983 760.297 182.935C760.828 182.887 761.22 182.417 761.172 181.885C761.124 181.354 760.654 180.963 760.122 181.011Z" fill="#9DB2D1"/> |
||||
|
<path d="M742.927 87.5185C742.396 87.5667 742.004 88.0364 742.052 88.5677C742.101 89.099 742.57 89.4907 743.102 89.4425C743.633 89.3944 744.025 88.9246 743.976 88.3933C743.928 87.862 743.458 87.4703 742.927 87.5185Z" fill="#9DB2D1"/> |
||||
|
<path d="M735.654 146.368C735.122 146.416 734.731 146.886 734.779 147.417C734.827 147.949 735.297 148.34 735.828 148.292C736.359 148.244 736.751 147.774 736.703 147.243C736.655 146.712 736.185 146.32 735.654 146.368Z" fill="#9DB2D1"/> |
||||
|
<path d="M725.076 125.992C724.544 126.04 724.153 126.51 724.201 127.041C724.249 127.573 724.719 127.964 725.25 127.916C725.781 127.868 726.173 127.398 726.125 126.867C726.077 126.336 725.607 125.944 725.076 125.992Z" fill="#9DB2D1"/> |
||||
|
<path d="M691.037 167.875C690.505 167.923 690.114 168.393 690.162 168.924C690.21 169.455 690.68 169.847 691.211 169.799C691.742 169.751 692.134 169.281 692.086 168.75C692.038 168.218 691.568 167.827 691.037 167.875Z" fill="#9DB2D1"/> |
||||
|
<path opacity="0.3" d="M664.029 137.343C663.497 137.391 663.106 137.861 663.154 138.392C663.202 138.923 663.672 139.315 664.203 139.267C664.734 139.219 665.126 138.749 665.078 138.218C665.03 137.686 664.56 137.295 664.029 137.343Z" fill="#9DB2D1"/> |
||||
|
<path opacity="0.3" d="M651.349 125.888C650.818 125.936 650.426 126.406 650.474 126.937C650.522 127.468 650.992 127.86 651.523 127.812C652.055 127.764 652.446 127.294 652.398 126.762C652.35 126.231 651.88 125.839 651.349 125.888Z" fill="#9DB2D1"/> |
||||
|
<path d="M639.365 143.462C638.833 143.51 638.442 143.98 638.49 144.511C638.538 145.042 639.008 145.434 639.539 145.386C640.07 145.338 640.462 144.868 640.414 144.337C640.366 143.805 639.896 143.414 639.365 143.462Z" fill="#9DB2D1"/> |
||||
|
<path d="M626.24 148.53C625.708 148.578 625.317 149.048 625.365 149.579C625.413 150.111 625.883 150.502 626.414 150.454C626.945 150.406 627.337 149.936 627.289 149.405C627.241 148.874 626.771 148.482 626.24 148.53Z" fill="#9DB2D1"/> |
||||
|
<path d="M603.052 224.345C602.521 224.393 602.129 224.863 602.177 225.394C602.226 225.925 602.695 226.317 603.227 226.269C603.758 226.221 604.15 225.751 604.101 225.219C604.053 224.688 603.583 224.297 603.052 224.345Z" fill="#9DB2D1"/> |
||||
|
<path opacity="0.5" d="M593.544 130.16C593.013 130.208 592.621 130.678 592.67 131.209C592.718 131.741 593.187 132.132 593.719 132.084C594.25 132.036 594.642 131.566 594.594 131.035C594.545 130.504 594.076 130.112 593.544 130.16Z" fill="#9DB2D1"/> |
||||
|
<path opacity="0.5" d="M597.029 168.635C596.497 168.683 596.106 169.153 596.154 169.684C596.202 170.215 596.672 170.607 597.203 170.559C597.734 170.511 598.126 170.041 598.078 169.51C598.03 168.978 597.56 168.587 597.029 168.635Z" fill="#9DB2D1"/> |
||||
|
<path d="M551.372 199.935C550.841 199.984 550.449 200.453 550.498 200.985C550.546 201.516 551.016 201.908 551.547 201.86C552.078 201.811 552.47 201.342 552.422 200.81C552.374 200.279 551.904 199.887 551.372 199.935Z" fill="#9DB2D1"/> |
||||
|
<path opacity="0.3" d="M861.349 185.432C860.818 185.48 860.426 185.949 860.474 186.481C860.522 187.012 860.992 187.404 861.523 187.356C862.055 187.307 862.446 186.838 862.398 186.306C862.35 185.775 861.88 185.383 861.349 185.432Z" fill="#9DB2D1"/> |
||||
|
<path d="M522.966 132.676C522.435 132.724 522.043 133.194 522.091 133.725C522.14 134.256 522.609 134.648 523.141 134.6C523.672 134.552 524.064 134.082 524.015 133.551C523.967 133.019 523.498 132.628 522.966 132.676Z" fill="#9DB2D1"/> |
||||
|
<path d="M526.458 171.157C525.927 171.205 525.535 171.675 525.584 172.206C525.632 172.738 526.102 173.129 526.633 173.081C527.164 173.033 527.556 172.563 527.508 172.032C527.459 171.501 526.99 171.109 526.458 171.157Z" fill="#9DB2D1"/> |
||||
|
<path d="M508.896 95.1533C508.365 95.2014 507.973 95.6712 508.021 96.2025C508.069 96.7338 508.539 97.1255 509.07 97.0773C509.602 97.0291 509.993 96.5594 509.945 96.0281C509.897 95.4968 509.427 95.1051 508.896 95.1533Z" fill="#9DB2D1"/> |
||||
|
<path d="M910.083 210.378C909.552 210.426 909.16 210.896 909.209 211.427C909.257 211.958 909.727 212.35 910.258 212.302C910.789 212.254 911.181 211.784 911.133 211.253C911.084 210.721 910.615 210.33 910.083 210.378Z" fill="#9DB2D1"/> |
||||
|
<path d="M909.31 166.806C908.779 166.854 908.387 167.324 908.435 167.855C908.483 168.386 908.953 168.778 909.484 168.73C910.016 168.681 910.407 168.212 910.359 167.68C910.311 167.149 909.841 166.757 909.31 166.806Z" fill="#9DB2D1"/> |
||||
|
<path opacity="0.3" d="M506.771 221.438C506.24 221.487 505.848 221.956 505.896 222.488C505.944 223.019 506.414 223.411 506.945 223.362C507.477 223.314 507.868 222.845 507.82 222.313C507.772 221.782 507.302 221.39 506.771 221.438Z" fill="#9DB2D1"/> |
||||
|
<path d="M494.107 156.631C493.576 156.679 493.184 157.149 493.232 157.68C493.28 158.211 493.75 158.603 494.281 158.555C494.813 158.507 495.204 158.037 495.156 157.506C495.108 156.974 494.638 156.583 494.107 156.631Z" fill="#9DB2D1"/> |
||||
|
<path opacity="0.5" d="M469.826 38.5576C469.294 38.6057 468.903 39.0755 468.951 39.6068C468.999 40.1381 469.469 40.5298 470 40.4816C470.531 40.4334 470.923 39.9637 470.875 39.4324C470.827 38.9011 470.357 38.5094 469.826 38.5576Z" fill="#9DB2D1"/> |
||||
|
<path d="M482.294 186.8C481.763 186.848 481.371 187.318 481.42 187.849C481.468 188.38 481.937 188.772 482.469 188.724C483 188.676 483.392 188.206 483.344 187.675C483.295 187.143 482.826 186.752 482.294 186.8Z" fill="#9DB2D1"/> |
||||
|
<path d="M465.333 138.873C464.802 138.921 464.41 139.391 464.459 139.922C464.507 140.454 464.977 140.845 465.508 140.797C466.039 140.749 466.431 140.279 466.383 139.748C466.334 139.216 465.865 138.825 465.333 138.873Z" fill="#9DB2D1"/> |
||||
|
<path d="M423.38 71.872C422.849 71.9202 422.457 72.3899 422.505 72.9212C422.554 73.4525 423.023 73.8442 423.555 73.796C424.086 73.7479 424.478 73.2781 424.429 72.7468C424.381 72.2155 423.912 71.8238 423.38 71.872Z" fill="#9DB2D1"/> |
||||
|
<path d="M420.193 186.612C419.661 186.66 419.27 187.13 419.318 187.661C419.366 188.193 419.836 188.584 420.367 188.536C420.898 188.488 421.29 188.018 421.242 187.487C421.194 186.956 420.724 186.564 420.193 186.612Z" fill="#9DB2D1"/> |
||||
|
<path d="M855.24 95.5331C854.708 95.5813 854.317 96.0511 854.365 96.5824C854.413 97.1137 854.883 97.5053 855.414 97.4572C855.945 97.409 856.337 96.9393 856.289 96.4079C856.241 95.8766 855.771 95.485 855.24 95.5331Z" fill="#9DB2D1"/> |
||||
|
<path opacity="0.3" d="M392.81 152.255C392.279 152.303 391.887 152.773 391.935 153.304C391.983 153.835 392.453 154.227 392.984 154.179C393.516 154.131 393.907 153.661 393.859 153.13C393.811 152.598 393.341 152.207 392.81 152.255Z" fill="#9DB2D1"/> |
||||
|
<path d="M368.318 117.617C367.786 117.665 367.395 118.135 367.443 118.666C367.491 119.198 367.961 119.589 368.492 119.541C369.023 119.493 369.415 119.023 369.367 118.492C369.319 117.961 368.849 117.569 368.318 117.617Z" fill="#9DB2D1"/> |
||||
|
<path d="M348.638 167.9C348.107 167.948 347.715 168.418 347.763 168.95C347.811 169.481 348.281 169.873 348.813 169.824C349.344 169.776 349.735 169.306 349.687 168.775C349.639 168.244 349.169 167.852 348.638 167.9Z" fill="#9DB2D1"/> |
||||
|
<path d="M323.716 139.123C323.185 139.171 322.793 139.641 322.841 140.172C322.89 140.704 323.359 141.095 323.891 141.047C324.422 140.999 324.814 140.529 324.765 139.998C324.717 139.466 324.248 139.075 323.716 139.123Z" fill="#9DB2D1"/> |
||||
|
<path d="M296.708 108.591C296.177 108.639 295.785 109.109 295.834 109.64C295.882 110.171 296.352 110.563 296.883 110.515C297.414 110.467 297.806 109.997 297.758 109.466C297.709 108.934 297.24 108.543 296.708 108.591Z" fill="#9DB2D1"/> |
||||
|
<path opacity="0.5" d="M272.037 114.707C271.505 114.755 271.114 115.225 271.162 115.756C271.21 116.288 271.68 116.679 272.211 116.631C272.742 116.583 273.134 116.113 273.086 115.582C273.038 115.05 272.568 114.659 272.037 114.707Z" fill="#9DB2D1"/> |
||||
|
<path d="M832.591 167.674C832.06 167.722 831.668 168.192 831.716 168.723C831.765 169.254 832.234 169.646 832.766 169.598C833.297 169.55 833.689 169.08 833.64 168.549C833.592 168.017 833.123 167.626 832.591 167.674Z" fill="#9DB2D1"/> |
||||
|
<path d="M258.919 119.778C258.388 119.826 257.996 120.296 258.045 120.827C258.093 121.359 258.562 121.75 259.094 121.702C259.625 121.654 260.017 121.184 259.969 120.653C259.92 120.122 259.451 119.73 258.919 119.778Z" fill="#9DB2D1"/> |
||||
|
<path d="M235.747 195.589C235.216 195.637 234.824 196.107 234.873 196.638C234.921 197.169 235.391 197.561 235.922 197.513C236.453 197.465 236.845 196.995 236.797 196.464C236.749 195.932 236.279 195.541 235.747 195.589Z" fill="#9DB2D1"/> |
||||
|
<path opacity="0.5" d="M229.724 139.88C229.193 139.928 228.801 140.398 228.849 140.929C228.897 141.46 229.367 141.852 229.898 141.804C230.43 141.756 230.821 141.286 230.773 140.755C230.725 140.223 230.255 139.832 229.724 139.88Z" fill="#9DB2D1"/> |
||||
|
<path d="M184.068 171.179C183.536 171.227 183.145 171.697 183.193 172.228C183.241 172.759 183.711 173.151 184.242 173.103C184.773 173.055 185.165 172.585 185.117 172.053C185.069 171.522 184.599 171.13 184.068 171.179Z" fill="#9DB2D1"/> |
||||
|
<path d="M822.177 181.225C821.646 181.273 821.254 181.742 821.302 182.274C821.351 182.805 821.82 183.197 822.352 183.149C822.883 183.1 823.275 182.631 823.226 182.099C823.178 181.568 822.708 181.176 822.177 181.225Z" fill="#9DB2D1"/> |
||||
|
<path d="M139.458 192.683C138.927 192.731 138.535 193.2 138.584 193.732C138.632 194.263 139.102 194.655 139.633 194.607C140.164 194.558 140.556 194.089 140.508 193.557C140.459 193.026 139.99 192.634 139.458 192.683Z" fill="#9DB2D1"/> |
||||
|
<path opacity="0.3" d="M128.982 119.915C128.451 119.963 128.059 120.433 128.107 120.964C128.155 121.496 128.625 121.887 129.156 121.839C129.688 121.791 130.079 121.321 130.031 120.79C129.983 120.258 129.513 119.867 128.982 119.915Z" fill="#9DB2D1"/> |
||||
|
<path opacity="0.3" d="M18.2162 271.39C17.6849 271.438 17.2932 271.908 17.3414 272.439C17.3896 272.97 17.8593 273.362 18.3906 273.314C18.9219 273.265 19.3136 272.796 19.2654 272.264C19.2173 271.733 18.7475 271.341 18.2162 271.39Z" fill="#9DB2D1"/> |
||||
|
<path opacity="0.5" d="M114.974 158.047C114.443 158.095 114.051 158.565 114.099 159.096C114.147 159.627 114.617 160.019 115.148 159.971C115.68 159.923 116.071 159.453 116.023 158.922C115.975 158.39 115.505 157.999 114.974 158.047Z" fill="#9DB2D1"/> |
||||
|
<path d="M91.9037 117.459C91.3724 117.507 90.9807 117.977 91.0289 118.508C91.0771 119.039 91.5468 119.431 92.0781 119.383C92.6094 119.335 93.0011 118.865 92.9529 118.334C92.9048 117.802 92.435 117.411 91.9037 117.459Z" fill="#9DB2D1"/> |
||||
|
<path d="M84.8178 135.558C84.2865 135.606 83.8948 136.075 83.943 136.607C83.9911 137.138 84.4609 137.53 84.9922 137.482C85.5235 137.433 85.9152 136.964 85.867 136.432C85.8188 135.901 85.3491 135.509 84.8178 135.558Z" fill="#9DB2D1"/> |
||||
|
<path d="M70.5521 192.139C70.0208 192.187 69.6292 192.657 69.6773 193.188C69.7255 193.719 70.1953 194.111 70.7266 194.063C71.2579 194.014 71.6495 193.545 71.6014 193.013C71.5532 192.482 71.0835 192.09 70.5521 192.139Z" fill="#9DB2D1"/> |
||||
|
<path d="M29.0287 233.118C28.4974 233.166 28.1057 233.636 28.1539 234.167C28.2021 234.699 28.6718 235.09 29.2031 235.042C29.7344 234.994 30.1261 234.524 30.0779 233.993C30.0298 233.462 29.56 233.07 29.0287 233.118Z" fill="#9DB2D1"/> |
||||
|
<path d="M15.8803 184.32C15.349 184.368 14.9573 184.838 15.0055 185.369C15.0536 185.901 15.5234 186.292 16.0547 186.244C16.586 186.196 16.9777 185.726 16.9295 185.195C16.8813 184.664 16.4116 184.272 15.8803 184.32Z" fill="#9DB2D1"/> |
||||
|
</svg> |
||||
|
Before Width: 1612 | Height: 865 | Size: 1.7 MiB After Width: 1400 | Height: 578 | Size: 1.2 MiB |
|
After Width: 389 | Height: 481 | Size: 247 KiB |
|
After Width: 389 | Height: 406 | Size: 48 KiB |
|
After Width: 389 | Height: 481 | Size: 54 KiB |
|
Before Width: 115 | Height: 116 | Size: 8.2 KiB After Width: 56 | Height: 56 | Size: 3.6 KiB |
|
Before Width: 108 | Height: 108 | Size: 5.2 KiB After Width: 48 | Height: 48 | Size: 1.8 KiB |
|
Before Width: 115 | Height: 116 | Size: 8.2 KiB After Width: 56 | Height: 56 | Size: 3.6 KiB |
21
public/assets/images/object_left.svg
File diff suppressed because it is too large
View File
|
Before Width: 108 | Height: 108 | Size: 5.2 KiB After Width: 48 | Height: 48 | Size: 1.8 KiB |