mirror of
https://github.com/windmill-labs/windmill.git
synced 2026-09-10 08:07:03 +00:00
214 lines
5.5 KiB
Svelte
214 lines
5.5 KiB
Svelte
<script lang="ts">
|
|
import { NodeResizer } from '@xyflow/svelte'
|
|
import { X } from 'lucide-svelte'
|
|
import { twMerge } from 'tailwind-merge'
|
|
import GfmMarkdown from '$lib/components/GfmMarkdown.svelte'
|
|
import { fade } from 'svelte/transition'
|
|
import NoteColorPicker from '../../NoteColorPicker.svelte'
|
|
import { NoteColor, NOTE_COLORS, DEFAULT_NOTE_COLOR } from '../../noteColors'
|
|
import { Button } from '$lib/components/common'
|
|
|
|
interface Props {
|
|
data: {
|
|
text: string
|
|
color: NoteColor
|
|
onUpdate?: (text: string) => void
|
|
onDelete?: () => void
|
|
onColorChange?: (color: NoteColor) => void
|
|
onSizeChange?: (size: { width: number; height: number }) => void
|
|
}
|
|
selected?: boolean
|
|
dragging?: boolean
|
|
}
|
|
|
|
let { data, selected = false, dragging = false }: Props = $props()
|
|
|
|
let textareaElement: HTMLTextAreaElement | undefined = $state(undefined)
|
|
let editMode = $state(false)
|
|
let hovering = $state(false)
|
|
let textContent = $state(data.text ?? '')
|
|
|
|
function handleTextSave() {
|
|
// Only update parent when done editing
|
|
data.onUpdate?.(textContent)
|
|
}
|
|
|
|
function handleDelete(event?: Event) {
|
|
event?.preventDefault?.()
|
|
event?.stopPropagation?.()
|
|
// Call the delete callback
|
|
data.onDelete?.()
|
|
}
|
|
|
|
function handleColorChange(color: NoteColor) {
|
|
data.onColorChange?.(color)
|
|
}
|
|
|
|
// Get color configuration for current color
|
|
const colorConfig = $derived(NOTE_COLORS[data.color] || NOTE_COLORS[DEFAULT_NOTE_COLOR])
|
|
|
|
function handleDoubleClick(event: Event) {
|
|
console.log('Double click detected', { editMode, selected, dragging })
|
|
event.preventDefault()
|
|
event.stopPropagation()
|
|
textContent = data.text ?? '' // Initialize local state with current data
|
|
editMode = true
|
|
// Focus the textarea after a short delay to ensure it's rendered
|
|
setTimeout(() => {
|
|
textareaElement?.focus()
|
|
}, 0)
|
|
}
|
|
|
|
function handleMouseEnter() {
|
|
hovering = true
|
|
}
|
|
|
|
function handleMouseLeave() {
|
|
hovering = false
|
|
}
|
|
|
|
// Exit edit mode when note is deselected
|
|
$effect(() => {
|
|
if (!selected && editMode) {
|
|
handleTextSave() // Save changes before exiting
|
|
editMode = false
|
|
}
|
|
})
|
|
|
|
let colorPickerIsOpen = $state(false)
|
|
</script>
|
|
|
|
<div
|
|
class={twMerge(
|
|
'relative w-full h-full rounded-md group hover:outline outline-1',
|
|
colorConfig.background,
|
|
colorConfig.text,
|
|
colorConfig.outlineHover,
|
|
selected ? 'outline' : '',
|
|
selected ? colorConfig.outline : '',
|
|
editMode ? 'outline-0' : ''
|
|
)}
|
|
onpointerup={() => {
|
|
dragging = false
|
|
}}
|
|
ondragstart={() => {
|
|
dragging = true
|
|
}}
|
|
ondragend={() => {
|
|
dragging = false
|
|
}}
|
|
onmouseenter={handleMouseEnter}
|
|
onmouseleave={handleMouseLeave}
|
|
role="note"
|
|
>
|
|
<!-- Color picker button -->
|
|
<div class="absolute -top-10 -right-2.5 p-2 w-20 h-12 group flex justify-end">
|
|
<div
|
|
class={twMerge(
|
|
'hidden group-hover:flex flex-row gap-2 h-fit',
|
|
hovering || editMode || colorPickerIsOpen || selected ? 'flex' : ''
|
|
)}
|
|
>
|
|
<NoteColorPicker
|
|
selectedColor={data.color}
|
|
onColorChange={handleColorChange}
|
|
bind:isOpen={colorPickerIsOpen}
|
|
/>
|
|
<!-- Delete button -->
|
|
<Button
|
|
variant="subtle"
|
|
unifiedSize="sm"
|
|
title="Delete note"
|
|
aria-label="Delete note"
|
|
startIcon={{ icon: X }}
|
|
onClick={handleDelete}
|
|
iconOnly
|
|
destructive
|
|
/>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Hover help text -->
|
|
{#if hovering || selected}
|
|
{#if !editMode && data.text}
|
|
<div
|
|
in:fade={{ duration: 200 }}
|
|
class="absolute -top-5 h-5 left-0 text-2xs text-secondary rounded-md z-10 transition-opacity duration-300"
|
|
>
|
|
Double click to edit
|
|
</div>
|
|
{:else}
|
|
<div
|
|
in:fade={{ duration: 200 }}
|
|
class="absolute -top-5 h-5 left-0 text-2xs text-secondary rounded-md z-10 transition-opacity duration-300"
|
|
>GH Markdown</div
|
|
>
|
|
{/if}
|
|
{/if}
|
|
|
|
<!-- Note content -->
|
|
<div class="h-full rounded-md">
|
|
{#if editMode}
|
|
<!-- Edit mode: show textarea -->
|
|
<textarea
|
|
bind:this={textareaElement}
|
|
bind:value={textContent}
|
|
class={twMerge(
|
|
'windmillapp w-full h-full min-h-0 shadow-none resize-none text-xs overflow-y-auto border-none rounded-md bg-transparent transition-colors p-4',
|
|
colorConfig.text
|
|
)}
|
|
placeholder="Add your note here... (Markdown supported)"
|
|
onblur={handleTextSave}
|
|
spellcheck="false"
|
|
></textarea>
|
|
{:else}
|
|
<!-- Render mode: show markdown or empty state -->
|
|
<!-- svelte-ignore a11y_no_static_element_interactions -->
|
|
<div
|
|
class={twMerge(
|
|
'w-full h-full overflow-auto cursor-pointer flex items-start justify-center rounded-md p-4'
|
|
)}
|
|
ondblclick={handleDoubleClick}
|
|
>
|
|
{#if data.text}
|
|
<div class={twMerge('w-full h-full text-xs rounded-md', colorConfig.text)}>
|
|
<GfmMarkdown md={data.text} noPadding />
|
|
</div>
|
|
{:else}
|
|
<div class={twMerge('text-xs italic opacity-60', colorConfig.text)}>
|
|
Double-click to add a note
|
|
</div>
|
|
{/if}
|
|
</div>
|
|
{/if}
|
|
</div>
|
|
|
|
<!-- Node resizer - only visible when selected -->
|
|
<NodeResizer
|
|
isVisible={selected && !dragging}
|
|
minWidth={200}
|
|
minHeight={100}
|
|
lineClass="!border-4 !border-transparent !rounded-md"
|
|
handleClass="!bg-transparent !w-4 !h-4 !border-none !rounded-md"
|
|
onResizeEnd={(_, params) => {
|
|
// Update note size when resizing ends
|
|
if (data.onSizeChange && params.width !== undefined && params.height !== undefined) {
|
|
data.onSizeChange({ width: params.width, height: params.height })
|
|
}
|
|
}}
|
|
/>
|
|
</div>
|
|
|
|
<style>
|
|
textarea::placeholder {
|
|
color: #6b7280;
|
|
opacity: 0.7;
|
|
}
|
|
|
|
/* Remove default textarea styling */
|
|
textarea {
|
|
font-family: inherit;
|
|
line-height: 1.4;
|
|
}
|
|
</style>
|