feat: risks and sticky notes

This commit is contained in:
2025-07-08 12:56:52 +02:00
parent 4d8cc643a3
commit 8c1508f3ae
45 changed files with 613 additions and 34 deletions

View File

@ -7,6 +7,7 @@
x?: number;
y?: number;
zIndex?: number;
canvasOffset?: { x: number; y: number };
onMove?: (id: number, x: number, y: number) => void;
onClick?: (id: number) => void;
onDrop?: (id: number, x: number, y: number) => void;
@ -20,6 +21,7 @@
x = $bindable(0),
y = $bindable(0),
zIndex = $bindable(0),
canvasOffset = { x: 0, y: 0 },
onMove,
onClick,
onDrop
@ -59,8 +61,8 @@
hasMoved = true;
}
const newX = event.clientX - dragOffset.x;
const newY = event.clientY - dragOffset.y;
const newX = event.clientX - dragOffset.x - canvasOffset.x;
const newY = event.clientY - dragOffset.y - canvasOffset.y;
x = newX;
y = newY;

223
src/lib/StickyNote.svelte Normal file
View File

@ -0,0 +1,223 @@
<script lang="ts">
interface Props {
id: number;
text: string;
x: number;
y: number;
zIndex: number;
color: string;
canvasOffset?: { x: number; y: number };
onMove?: (id: number, x: number, y: number) => void;
onUpdate?: (id: number, text: string) => void;
onDelete?: (id: number) => void;
}
let {
id,
text = $bindable(''),
x = $bindable(0),
y = $bindable(0),
zIndex = $bindable(0),
color,
canvasOffset = { x: 0, y: 0 },
onMove,
onUpdate,
onDelete
}: Props = $props();
let isDragging = $state(false);
let dragOffset = $state({ x: 0, y: 0 });
let dragStartPos = $state({ x: 0, y: 0 });
let hasMoved = $state(false);
let noteElement: HTMLDivElement;
let textareaElement: HTMLTextAreaElement;
let isEditing = $state(false);
function handleMouseDown(event: MouseEvent) {
if (event.button !== 0) return;
isDragging = true;
hasMoved = false;
const rect = noteElement.getBoundingClientRect();
dragOffset = {
x: event.clientX - rect.left,
y: event.clientY - rect.top
};
dragStartPos = {
x: event.clientX,
y: event.clientY
};
event.preventDefault();
}
function handleMouseMove(event: MouseEvent) {
if (!isDragging) return;
const deltaX = Math.abs(event.clientX - dragStartPos.x);
const deltaY = Math.abs(event.clientY - dragStartPos.y);
if (deltaX > 5 || deltaY > 5) {
hasMoved = true;
}
const newX = event.clientX - dragOffset.x - canvasOffset.x;
const newY = event.clientY - dragOffset.y - canvasOffset.y;
x = newX;
y = newY;
onMove?.(id, newX, newY);
}
function handleMouseUp() {
isDragging = false;
}
function handleClick() {
if (!hasMoved) {
isEditing = true;
setTimeout(() => textareaElement?.focus(), 0);
}
}
function handleTextBlur() {
isEditing = false;
onUpdate?.(id, text);
}
function handleKeyDown(event: KeyboardEvent) {
if (event.key === 'Enter' && !event.shiftKey) {
event.preventDefault();
isEditing = false;
onUpdate?.(id, text);
}
if (event.key === 'Escape') {
isEditing = false;
}
}
function handleDelete(event: MouseEvent) {
event.stopPropagation();
onDelete?.(id);
}
$effect(() => {
if (isDragging) {
document.addEventListener('mousemove', handleMouseMove);
document.addEventListener('mouseup', handleMouseUp);
return () => {
document.removeEventListener('mousemove', handleMouseMove);
document.removeEventListener('mouseup', handleMouseUp);
};
}
});
</script>
<div
bind:this={noteElement}
class="sticky-note"
class:dragging={isDragging}
class:editing={isEditing}
style="left: {x}px; top: {y}px; z-index: {zIndex}; background-color: {color};"
onmousedown={handleMouseDown}
onclick={handleClick}
role="button"
tabindex="0"
>
<button class="delete-btn" onclick={handleDelete}>×</button>
{#if isEditing}
<textarea
bind:this={textareaElement}
bind:value={text}
onblur={handleTextBlur}
onkeydown={handleKeyDown}
class="note-textarea"
placeholder="Enter your note..."
></textarea>
{:else}
<div class="note-text">{text || 'Click to edit'}</div>
{/if}
</div>
<style>
.sticky-note {
position: absolute;
width: 200px;
min-height: 120px;
padding: 15px;
border-radius: 8px;
cursor: pointer;
user-select: none;
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.15);
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
font-size: 14px;
line-height: 1.4;
transition: transform 0.1s ease;
}
.sticky-note:hover {
transform: translateY(-2px);
}
.sticky-note.dragging {
cursor: grabbing;
z-index: 1000 !important;
transform: rotate(2deg);
}
.sticky-note.dragging:hover {
transform: rotate(2deg);
}
.delete-btn {
position: absolute;
top: 5px;
right: 5px;
width: 20px;
height: 20px;
border: none;
background: rgba(0, 0, 0, 0.1);
border-radius: 50%;
cursor: pointer;
font-size: 14px;
font-weight: bold;
display: flex;
align-items: center;
justify-content: center;
color: rgba(0, 0, 0, 0.6);
transition: all 0.2s ease;
}
.delete-btn:hover {
background: rgba(255, 0, 0, 0.2);
color: #ff0000;
}
.note-textarea {
width: 100%;
min-height: 80px;
border: none;
background: transparent;
resize: none;
font-family: inherit;
font-size: inherit;
line-height: inherit;
outline: none;
padding: 0;
}
.note-text {
min-height: 80px;
word-wrap: break-word;
white-space: pre-wrap;
color: rgba(0, 0, 0, 0.8);
}
.note-text:empty::before {
content: 'Click to edit';
color: rgba(0, 0, 0, 0.4);
font-style: italic;
}
</style>