<script module>
	function useResizeObserver(node, callback) {
		const observer = new ResizeObserver(() => {
			callback()
		})

		observer.observe(node)

		return {
			destroy() {
				observer.disconnect()
			}
		}
	}
</script>

<script lang="ts">
	import Button from '$lib/components/common/button/Button.svelte'
	import { Pin, History, Pen, Check, X, Loader2, Pencil, ScrollText } from 'lucide-svelte'
	import ObjectViewer from '$lib/components/propertyPicker/ObjectViewer.svelte'
	import StepHistory from './StepHistory.svelte'
	import { Popover } from '$lib/components/meltComponents'
	import { getContext, untrack } from 'svelte'
	import { Tooltip } from '$lib/components/meltComponents'
	import type { Job } from '$lib/gen'
	import DisplayResult from '$lib/components/DisplayResult.svelte'
	import OutputBadge from './OutputBadge.svelte'
	import { twMerge } from 'tailwind-merge'
	import DisplayResultControlBar from '$lib/components/DisplayResultControlBar.svelte'
	import LogViewer from '$lib/components/LogViewer.svelte'
	import { base } from '$lib/base'
	import { fade } from 'svelte/transition'
	import type { FlowEditorContext, OutputViewerJob } from '../types'

	interface Props {
		prefix?: string
		allowCopy?: boolean
		connectingData?: any | undefined
		mock?:
			| {
					enabled?: boolean
					return_value?: unknown
			  }
			| undefined
		moduleId?: string
		fullResult?: boolean
		closeOnOutsideClick?: boolean
		getLogs?: boolean
		isLoading?: boolean
		hideHeaderBar?: boolean
		simpleViewer?: any | undefined
		path?: string
		loopStatus?: { type: 'inside' | 'self'; flow: 'forloopflow' | 'whileloopflow' } | undefined
		customHeight?: number | undefined
		rightMargin?: boolean
		disableMock?: boolean
		disableHistory?: boolean
		testJob?: Job & { result_stream?: string }
		derivedHistoryOpen?: boolean // derived from historyOpen
		historyOffset?: any
		clazz?: string
		copilot_fix?: import('svelte').Snippet
		onSelect?: (key: string) => void
		onUpdateMock?: (mock: { enabled: boolean; return_value?: unknown }) => void
		onEditInput?: (moduleId: string, key: string) => void
		selectionId?: string
		initial?: boolean
		onResetInitial?: () => void
		customEmptyJobMessage?: string
	}

	let {
		testJob,
		prefix = '',
		allowCopy = false,
		connectingData = undefined,
		mock = $bindable({ enabled: false }),
		moduleId = '',
		fullResult = false,
		closeOnOutsideClick = false,
		getLogs = false,
		isLoading = $bindable(false),
		hideHeaderBar = false,
		simpleViewer = undefined,
		path = '',
		loopStatus = undefined,
		customHeight = undefined,
		rightMargin = false,
		disableMock = false,
		disableHistory = false,
		historyOffset = { mainAxis: 8, crossAxis: -4.5 },
		clazz,
		copilot_fix,
		onSelect,
		onUpdateMock,
		onEditInput,
		selectionId,
		initial,
		customEmptyJobMessage
	}: Props = $props()

	let jsonView = $state(false)
	let clientHeight: number = $state(0)
	let tmpMock: { enabled: boolean; return_value?: unknown } | undefined = $state(undefined)
	let error = $state('')
	let stepHistoryPopover: Popover | undefined = $state(undefined)
	let historyOpen = $state(false)
	let contentEl: HTMLDivElement | undefined = $state(undefined)
	let hasOverflow = $state(false)
	let preview: 'mock' | 'job' | undefined = $state(undefined)
	let selectedJob: OutputViewerJob = $state(undefined)
	let showLogs = $state(false)

	function checkOverflow() {
		if (contentEl) {
			hasOverflow = contentEl.scrollHeight > contentEl.clientHeight
		}
	}

	function selectJob(nJob: OutputViewerJob | undefined) {
		if (nJob && (nJob.result_stream || nJob.type == 'CompletedJob')) {
			selectedJob = nJob
		} else if (lastJob && (lastJob.result_stream || lastJob.type == 'CompletedJob')) {
			selectedJob = lastJob
		} else {
			selectedJob = undefined
		}
	}

	$effect(() => {
		if (!lastJob || !(lastJob.result_stream || lastJob.type == 'CompletedJob')) {
			return
		}
		selectJob(lastJob)
	})

	function togglePreview(nPrev: 'mock' | 'job' | undefined) {
		if (!nPrev && preview === 'job') {
			// Reset the job
			selectJob(undefined)
		}
		preview = nPrev
	}

	let dblClickDisabled = $state(false)
	let hoveringResult = $state(false)
	let debouncedCanEditWithDblClick = $state(false)
	let debounceTimeout: ReturnType<typeof setTimeout> | null = null
	let canEditWithDblClick = $state(false)
	let displayResultJob: DisplayResult | undefined = $state(undefined)
	let displayResultMock: DisplayResult | undefined = $state(undefined)
	let toolbarLocationJob: 'self' | 'external' | undefined = $state(undefined)
	let toolbarLocationMock: 'self' | 'external' | undefined = $state(undefined)

	const flowEditorContext = getContext<FlowEditorContext | undefined>('FlowEditorContext')
	const flowStateStore = flowEditorContext?.flowStateStore

	function updateCanEditWithDblClick(newValue: boolean) {
		canEditWithDblClick = newValue
		if (debounceTimeout) {
			clearTimeout(debounceTimeout)
		}
		debounceTimeout = setTimeout(() => {
			debouncedCanEditWithDblClick = newValue
		}, 200)
	}

	function togglePin() {
		if (mock?.enabled && !preview) {
			// Unpin
			onUpdateMock?.({
				...mock,
				enabled: false
			})
		} else if (preview === 'mock') {
			// Restore the pin
			onUpdateMock?.({
				...mock,
				enabled: true
			})
		} else if (selectedJob && 'result' in selectedJob) {
			// Pin the job
			let mockValue: any = structuredClone($state.snapshot(selectedJob.result))
			if (selectedJob.result === 'never tested this far') {
				mockValue = { example: 'value' }
			}
			const newMock = {
				enabled: true,
				return_value: mockValue
			}
			onUpdateMock?.(newMock)
		} else {
			// Fallback to mock
			onUpdateMock?.({
				enabled: true,
				return_value: { example: 'value' }
			})
		}
		if (preview) {
			// Reset the preview
			if (historyOpen) {
				stepHistoryPopover?.close()
			}
			togglePreview(undefined)
		}
	}

	function updateLastJob() {
		// Prefer testJob when actively running/streaming (individual step test in progress)
		// or when there's no moduleId (result node — no flowStateStore entry exists)
		if (testJob && (testJob.result_stream || testJob.type === 'QueuedJob' || !moduleId)) {
			return testJob
		}
		if (
			!flowStateStore ||
			!moduleId ||
			flowStateStore.val[moduleId]?.previewResult === 'never tested this far'
		) {
			return
		}
		// Use flowStateStore as source of truth — it's updated by both individual step tests
		// (ModuleTest.jobDone) and flow tests (FlowStatusViewerInner.onJobsLoadedInner)
		return {
			id: flowStateStore.val[moduleId]?.previewJobId ?? '',
			result: flowStateStore.val[moduleId]?.previewResult,
			type: 'CompletedJob' as const,
			success: flowStateStore.val[moduleId]?.previewSuccess ?? undefined,
			logs: flowStateStore.val[moduleId]?.previewLogs
		} as Job & { result_stream?: string } & { preview?: boolean }
	}

	const lastJob = $derived.by(updateLastJob)

	export function setJobPreview() {
		if (mock?.enabled) {
			preview = 'job'
		}
	}

	const mockUpdateStatus = $derived(
		preview === 'mock' && !mock?.enabled
			? 'restore'
			: preview === 'job' && mock?.enabled && selectedJob?.['type'] === 'CompletedJob'
				? 'override'
				: undefined
	)

	$effect(() => {
		if (displayResultJob && typeof displayResultJob.getToolbarLocation === 'function') {
			toolbarLocationJob = displayResultJob.getToolbarLocation()
		}
	})
	$effect(() => {
		if (displayResultMock && typeof displayResultMock.getToolbarLocation === 'function') {
			toolbarLocationMock = displayResultMock.getToolbarLocation()
		}
	})
	$effect(() => {
		const newValue =
			!!mock?.enabled &&
			!connectingData &&
			!dblClickDisabled &&
			hoveringResult &&
			!jsonView &&
			!preview
		if (newValue != canEditWithDblClick) {
			canEditWithDblClick = newValue
			untrack(() => updateCanEditWithDblClick(newValue))
		}
	})

	let popoverHeight = $derived(customHeight ?? (clientHeight > 0 ? clientHeight : 0))

	// If a test job is running, we override the preview to show the test job
	const executingTestJob = $derived(testJob && testJob.type === 'QueuedJob')

	const isLoadingAndNotMock = $derived(
		isLoading && lastJob?.result_stream === undefined && (!mock?.enabled || executingTestJob)
	)

	const copilot_fix_render = $derived(copilot_fix)

	export function getPreview() {
		return preview
	}

	async function selectJobFromHistory({
		detail
	}: {
		detail: 'mock' | (Job & { getFullJob: () => Promise<Job> })
	}) {
		if (!detail) {
			togglePreview(undefined)
			return
		}
		if (detail === 'mock') {
			if (mock?.enabled) {
				togglePreview(undefined)
				return
			}
			togglePreview('mock')
			return
		}
		if (detail.id === lastJob?.id && !mock?.enabled) {
			togglePreview(undefined)
			return
		}

		isLoading = true

		try {
			const fullJob = await detail.getFullJob()
			if (fullJob) {
				selectJob(fullJob)
				togglePreview('job')
			}
		} finally {
			isLoading = false
		}
	}

	export function getHistoryOpen() {
		return historyOpen
	}

	export function getSelectedJob() {
		return selectedJob
	}

	const noHistory = $derived(
		loopStatus ? (loopStatus.type === 'self' ? 'isLoop' : 'isInsideLoop') : undefined
	)
</script>

<div
	class={twMerge('w-full h-full flex flex-col', clazz)}
	bind:clientHeight
	style={canEditWithDblClick ? 'cursor: text;' : ''}
>
	<!-- Top toolbar -->
	<div
		class={twMerge(
			'text-xs px-1',
			'border-none',
			hideHeaderBar || connectingData ? 'hidden' : 'block',
			hasOverflow ? 'shadow-sm' : ''
		)}
	>
		<div
			class={twMerge(
				'flex flex-row items-center gap-2 justify-between min-h-[33.5px]',
				rightMargin ? `mr-[30px]` : ''
			)}
		>
			<div class="flex flex-row items-center gap-0.5">
				<!-- History picker -->
				{#if !disableHistory}
					{@render historyPicker()}
				{/if}
				<!-- Badge-->
				{#if executingTestJob}
					<OutputBadge
						job={testJob}
						class={twMerge(
							'min-w-16 min-h-[23px] text-secondary',
							preview ? 'bg-surface shadow-sm' : ''
						)}
					/>
				{:else if isLoadingAndNotMock && !mock?.enabled}
					<div
						class="flex flex-row w-fit items-center justify-between gap-2 rounded-md bg-surface-secondary p-1 px-2 min-w-16 min-h-[23px]"
					>
						<Loader2 size={12} class="animate-spin text-secondary shrink-0" />
						<span class="text-xs text-secondary w-[56px]">&nbsp;</span>
					</div>
				{:else if !isLoadingAndNotMock || mock?.enabled}
					<div
						class={twMerge(
							'w-grow min-w-0 flex gap-1 items-center h-[27px] rounded-md  group',
							preview || selectedJob?.id !== lastJob?.id ? 'p-[2px] bg-surface-secondary' : ''
						)}
					>
						{#if loopStatus?.type === 'self'}
							<div
								class={twMerge(
									'min-w-16 rounded-md py-1 px-2 justify-center',
									mockUpdateStatus ? 'bg-surface h-[23px]' : 'bg-surface-secondary h-[27px]'
								)}
							>
								<span
									class="text-xs text-secondary truncate"
									title={loopStatus.flow === 'forloopflow'
										? 'For loop result'
										: 'While loop result'}
								>
									{loopStatus.flow === 'forloopflow' ? 'For loop result' : 'While loop result'}
								</span>
							</div>
						{:else if preview === 'mock' || (mock?.enabled && !preview)}
							<div
								class={twMerge(
									'min-w-16 text-secondary flex flex-row w-fit items-center gap-2 rounded-md bg-surface-secondary p-1 px-2',
									mockUpdateStatus ? 'bg-surface h-[23px]' : 'bg-surface-secondary h-[27px]'
								)}
							>
								<Pin size={12} />
								<span class="text-xs text-secondary truncate"
									>{mock?.enabled ? 'Pin' : 'Last pin'}</span
								>
							</div>
						{:else if preview === 'job' || (selectedJob && !preview)}
							<OutputBadge
								job={selectedJob}
								class={twMerge(
									'min-w-16 text-secondary',
									preview || selectedJob?.id !== lastJob?.id ? 'bg-surface shadow-sm h-[23px]' : ''
								)}
							/>
						{/if}

						{#if preview}
							<button
								class="px-1 shrink-0 text-secondary text-xs text-thin"
								onclick={() => {
									if (historyOpen) {
										// closing history popover exits preview
										stepHistoryPopover?.close()
									}
									togglePreview(undefined)
								}}>Exit preview</button
							>
						{/if}
					</div>
				{/if}

				<!-- Pin button -->
				{#if !disableMock}
					<Tooltip disablePopup={mock?.enabled}>
						<Button
							color="light"
							size="xs2"
							variant="contained"
							btnClasses={twMerge(
								'bg-transparent h-[27px]',
								mock?.enabled || mockUpdateStatus === 'override'
									? 'text-white bg-blue-500 hover:text-primary hover:bg-blue-700 hover:text-gray-100'
									: '',
								jsonView ? 'pointer-events-none' : ''
							)}
							startIcon={{ icon: Pin }}
							iconOnly={mockUpdateStatus !== 'override'}
							on:click={() => togglePin()}
						>
							{#if mockUpdateStatus === 'override'}
								Override pin
							{/if}
						</Button>
						{#snippet text()}
							Pin data
						{/snippet}
					</Tooltip>
				{/if}

				{#if jsonView}
					<!-- Save button -->
					<Button
						size="xs2"
						color="green"
						variant="contained"
						startIcon={{ icon: Check }}
						btnClasses="h-[27px]"
						on:click={() => {
							if (!tmpMock) {
								return
							}
							jsonView = false
							mock = tmpMock
							onUpdateMock?.({
								enabled: tmpMock?.enabled ?? false,
								return_value: tmpMock?.return_value
							})
							tmpMock = undefined
						}}
						disabled={!!error || !tmpMock}
					/>
					<!-- Cancel button -->
					<Button
						size="xs2"
						color="red"
						variant="contained"
						startIcon={{ icon: X }}
						btnClasses="h-[27px]"
						on:click={() => {
							jsonView = false
							tmpMock = undefined
						}}
					/>
				{:else if mock?.enabled && !preview && !executingTestJob}
					<!-- Edit pin button -->
					<Tooltip disablePopup={mock?.enabled}>
						<Button
							size="xs2"
							color="light"
							variant="contained"
							startIcon={{ icon: Pen }}
							on:click={() => {
								stepHistoryPopover?.close()
								jsonView = true
								tmpMock = undefined
							}}
							disabled={!mock?.enabled || !!connectingData}
							btnClasses={twMerge(
								'transition-all duration-100 h-[27px]',
								debouncedCanEditWithDblClick
									? 'bg-blue-500/10 text-blue-800 dark:text-blue-200'
									: ''
							)}
						/>
						{#snippet text()}
							{'Pin the output to allow editing'}
						{/snippet}
					</Tooltip>
				{/if}

				<!-- Logs button -->
				{#if selectedJob?.type === 'CompletedJob' && selectedJob?.['logs']}
					<Tooltip>
						<Button
							size="xs2"
							color="light"
							variant="contained"
							btnClasses={twMerge(
								'h-[27px]',
								showLogs ? 'bg-blue-500/10 text-blue-800 dark:text-blue-200' : 'bg-transparent'
							)}
							startIcon={{ icon: ScrollText }}
							on:click={() => {
								showLogs = !showLogs
							}}
						/>
						{#snippet text()}
							{showLogs ? 'Show result' : 'Show logs'}
						{/snippet}
					</Tooltip>
				{/if}
			</div>

			<div class="px-2">
				{#if selectedJob && displayResultJob && toolbarLocationJob === 'external'}
					<DisplayResultControlBar
						workspaceId={selectedJob.workspace_id}
						jobId={selectedJob.id}
						{base}
						result={selectedJob.result}
						disableTooltips={false}
						on:open-drawer={() => {
							if (displayResultJob && typeof displayResultJob.openDrawer === 'function') {
								displayResultJob.openDrawer()
							}
						}}
					/>
				{:else if mock?.enabled && displayResultMock && toolbarLocationMock === 'external'}
					<DisplayResultControlBar
						customUi={{ disableDownload: true }}
						{base}
						result={mock.return_value}
						disableTooltips={false}
						on:open-drawer={() => {
							if (displayResultMock && typeof displayResultMock.openDrawer === 'function') {
								displayResultMock.openDrawer()
							}
						}}
					/>
				{/if}
			</div>
		</div>
	</div>

	<!-- svelte-ignore a11y_no_static_element_interactions -->
	<!-- svelte-ignore a11y_mouse_events_have_key_events -->
	<div
		class={twMerge(
			'grow min-h-0 rounded-md w-full pl-2 py-1 pb-2 overflow-auto',
			'transition-all duration-100 outline outline-2 outline-blue-500/0 outline-offset-[-2px]',
			debouncedCanEditWithDblClick ? 'outline outline-2 outline-blue-500/30' : ''
		)}
		style="scrollbar-gutter: stable;"
	>
		<div
			class={twMerge('h-full w-full rounded-md')}
			bind:this={contentEl}
			onscroll={checkOverflow}
			use:useResizeObserver={checkOverflow}
			onmouseover={(event) => {
				if (
					!event.target ||
					!(event.target instanceof HTMLElement) ||
					event.target.closest('[data-interactive]') ||
					event.target.closest('button') ||
					event.target.closest('input') ||
					event.target.closest('textarea') ||
					event.target.closest('select') ||
					event.target.closest('option') ||
					event.target.closest('label') ||
					event.target.closest('a') ||
					event.target.closest('svg')
				) {
					dblClickDisabled = true
				} else {
					dblClickDisabled = false
				}
			}}
			ondblclick={() => {
				if (canEditWithDblClick) {
					stepHistoryPopover?.close()
					jsonView = true
					tmpMock = undefined
				}
			}}
			onmouseenter={() => {
				hoveringResult = true
			}}
			onmouseleave={() => {
				hoveringResult = false
			}}
		>
			{#if showLogs && selectedJob?.type === 'CompletedJob' && selectedJob?.['logs']}
				<LogViewer
					small
					jobId={selectedJob.id}
					duration={selectedJob['duration_ms']}
					mem={selectedJob['mem_peak']}
					content={selectedJob['logs']}
					isLoading={false}
					tag={selectedJob['tag']}
					onLogsResolved={(logs) => {
						if (
							moduleId &&
							flowStateStore?.val[moduleId] &&
							flowStateStore.val[moduleId].previewJobId === selectedJob?.id
						) {
							flowStateStore.val[moduleId] = {
								...flowStateStore.val[moduleId],
								previewLogs: logs
							}
						}
					}}
				/>
			{:else if isLoadingAndNotMock}
				<div class="flex flex-col items-center justify-center">
					<Loader2 class="animate-spin" />
				</div>
			{:else if connectingData !== undefined || simpleViewer}
				<!-- Display data for input connections -->
				<ObjectViewer
					json={moduleId
						? {
								[moduleId]: connectingData ?? simpleViewer
							}
						: (connectingData ?? simpleViewer)}
					topBrackets={false}
					pureViewer={false}
					{prefix}
					on:select={(e) => {
						onSelect?.(e.detail)
					}}
					{allowCopy}
					{editKey}
				/>
			{:else if jsonView}
				<!-- Json editor for editing mock data -->
				{#await import('$lib/components/JsonEditor.svelte')}
					<Loader2 class="animate-spin" />
				{:then Module}
					<Module.default
						bind:error
						small
						on:changeValue={({ detail }) => {
							if (mock?.enabled) {
								const newMock = {
									enabled: true,
									return_value: structuredClone($state.snapshot(detail))
								}
								tmpMock = newMock
							}
						}}
						code={JSON.stringify(
							mock?.enabled && mock.return_value ? mock.return_value : '',
							null,
							2
						)}
						class="h-full"
					/>
				{/await}
			{:else if (mock?.enabled || preview == 'mock') && preview != 'job' && !executingTestJob}
				<!-- Display data for mock data -->
				{#if fullResult}
					<div class="break-words relative h-full">
						<DisplayResult
							bind:this={displayResultMock}
							workspaceId={undefined}
							jobId={undefined}
							result_stream={undefined}
							result={mock?.return_value}
							externalToolbarAvailable
							fixTableSizingToParent
							on:toolbar-location-changed={({ detail }) => {
								toolbarLocationMock = detail
							}}
						/>
					</div>
				{:else}
					<ObjectViewer
						json={moduleId
							? {
									[moduleId]: mock?.return_value
								}
							: mock?.return_value}
						topBrackets={false}
						pureViewer={false}
					/>
				{/if}
			{:else if selectedJob != undefined && (selectedJob.result_stream || selectedJob.type == 'CompletedJob')}
				<!-- Display data for job data -->
				{#if fullResult}
					<div class="break-words relative h-full">
						{#key selectedJob}
							<DisplayResult
								bind:this={displayResultJob}
								workspaceId={selectedJob?.workspace_id}
								jobId={selectedJob?.id}
								result={selectedJob?.result}
								result_stream={selectedJob?.result_stream}
								externalToolbarAvailable
								fixTableSizingToParent
								on:toolbar-location-changed={({ detail }) => {
									toolbarLocationJob = detail
								}}
							>
								{#snippet copilot_fix()}
									{@render copilot_fix_render?.()}
								{/snippet}
							</DisplayResult>
						{/key}
					</div>
				{:else}
					<ObjectViewer
						json={moduleId
							? {
									[moduleId]: selectedJob.result
								}
							: selectedJob.result}
						topBrackets={false}
						pureViewer={false}
					/>
				{/if}
			{:else if !lastJob}
				<div class="flex flex-col items-center justify-center h-full">
					<p class="text-xs text-secondary">
						{customEmptyJobMessage ?? 'Test this step to see results'}{#if !disableMock}
							{' or'}
							<button
								class="text-blue-500 hover:text-blue-700 underline"
								onclick={() => {
									const newMock = {
										enabled: true,
										return_value: mock?.return_value ?? { example: 'value' }
									}
									onUpdateMock?.(newMock)
								}}>pin data<Pin size={16} class="inline" /></button
							>{:else}.{/if}
					</p>
				</div>
			{/if}
		</div>
	</div>
	{#if initial && !mock?.enabled}
		<span
			in:fade
			class="-mb-1 -mt-0.5 w-full text-right pr-4 dark:text-gray-500 text-gray-400 font-normal text-2xs py-0"
			>Run loaded from history</span
		>
	{/if}
</div>

{#snippet editKey(key: string)}
	<button
		onclick={() => onEditInput?.(selectionId ?? '', key)}
		class="h-4 w-fit items-center text-gray-300 dark:text-gray-500 hover:text-primary dark:hover:text-primary px-1 rounded-[0.275rem] align-baseline"
	>
		<Pencil size={12} class="-my-1 inline-flex items-center" />
	</button>
{/snippet}

{#snippet historyPicker()}
	<Popover
		bind:this={stepHistoryPopover}
		floatingConfig={{
			strategy: 'fixed',
			placement: 'left-start',
			offset: historyOffset,
			gutter: 0 // hack to make offset effective, see https://github.com/melt-ui/melt-ui/issues/528
		}}
		contentClasses="w-[225px] overflow-hidden"
		{closeOnOutsideClick}
		usePointerDownOutside={closeOnOutsideClick}
		disablePopup={!!connectingData || jsonView}
		bind:isOpen={historyOpen}
	>
		{#snippet trigger()}
			<Button
				color="light"
				size="xs2"
				variant="contained"
				btnClasses="bg-surface h-[27px]"
				startIcon={{ icon: History }}
				nonCaptureEvent
			/>
		{/snippet}
		{#snippet content()}
			<div class="rounded-[inherit]" style={`height: ${popoverHeight}px`}>
				<StepHistory
					{moduleId}
					{getLogs}
					on:select={selectJobFromHistory}
					mockValue={mock?.return_value}
					mockEnabled={mock?.enabled}
					{path}
					{noHistory}
				/>
			</div>
		{/snippet}
	</Popover>
{/snippet}

<style>
	.dbl-click-editable {
		cursor: text;
		transition: all 0.2s ease;
	}

	.dbl-click-editable:hover {
		box-shadow: 0 0 0 2px rgba(59, 130, 246, 0.5) inset;
	}
</style>
