add note color picker

This commit is contained in:
Guilhem
2025-11-04 16:49:14 +01:00
parent f2a0107dcd
commit 4b451f561e
7 changed files with 242 additions and 28 deletions
@@ -194,7 +194,7 @@ export namespace ButtonType {
accent:
'bg-red-500 dark:bg-red-600 hover:bg-red-600 dark:hover:bg-red-700 focus-visible:bg-red-700 text-white focus-visible:ring-red-300',
default:
'border border-border-light bg-transparent hover:bg-red-500 dark:hover:bg-red-600 hover:text-white dark:hover:bg-red-900/20 text-primary focus-visible:bg-red-100 dark:focus-visible:bg-red-900/30 focus-visible:ring-red-300',
'border border-border-light bg-transparent hover:bg-red-500 dark:hover:bg-red-600 hover:text-white dark:hover:bg-red-600 text-primary focus-visible:bg-red-100 dark:focus-visible:bg-red-900/30 focus-visible:ring-red-300',
subtle:
'bg-transparent hover:bg-red-500 hover:text-white dark:hover:bg-red-600 text-primary focus-visible:bg-red-100 dark:focus-visible:bg-red-900/30 focus-visible:ring-red-300'
}
@@ -92,7 +92,7 @@
onclick={() => toggleNoteMode?.()}
iconOnly
variant="default"
size="sm"
unifiedSize="sm"
startIcon={{ icon: StickyNote }}
selected={noteMode}
></Button>
+3 -1
View File
@@ -15,13 +15,15 @@ import type ResourceEditorDrawer from '../ResourceEditorDrawer.svelte'
import type { ModulesTestStates } from '../modulesTest.svelte'
import type { ButtonProp } from '$lib/components/DiffEditor.svelte'
import type { NoteColor } from '../graph/noteColors'
// Type for flow notes stored in the UI field
export type Note = {
id: string
text: string
position: { x: number; y: number }
size: { width: number; height: number }
color: string
color: NoteColor
}
export type FlowInput = Record<
@@ -2,6 +2,7 @@
import { FlowService, type FlowModule, type Job } from '../../gen'
import { NODE, type GraphModuleState } from '.'
import type { Note } from '../flows/types'
import { DEFAULT_NOTE_COLOR, type NoteColor } from './noteColors'
import { getContext, onDestroy, setContext, tick, untrack, type Snippet } from 'svelte'
import { get, writable, type Writable } from 'svelte/store'
@@ -420,7 +421,7 @@
text: '',
position: newNoteFromTool.position,
size: newNoteFromTool.size || { width: 200, height: 100 },
color: 'oklch(96.7% 0.067 122.328)'
color: DEFAULT_NOTE_COLOR
}
onNotesChange([...notes, newNote])
nextNoteId += 1
@@ -455,6 +456,13 @@
}
}
function updateNoteColor(noteId: string, color: NoteColor) {
if (onNotesChange) {
onNotesChange(notes.map((note) => (note.id === noteId ? { ...note, color } : note)))
}
updateStores()
}
function convertNotesToNodes(): Node[] {
return notes.map((note) => ({
id: note.id,
@@ -465,6 +473,7 @@
color: note.color,
onUpdate: (text: string) => updateNoteText(note.id, text),
onDelete: () => deleteNote(note.id),
onColorChange: (color: NoteColor) => updateNoteColor(note.id, color),
onSizeChange: (size: { width: number; height: number }) => updateNoteSize(note.id, size)
},
style: `width: ${note.size.width}px; height: ${note.size.height}px;`,
@@ -0,0 +1,50 @@
<script lang="ts">
import { Palette } from 'lucide-svelte'
import Popover from '../meltComponents/Popover.svelte'
import { NoteColor, NOTE_COLOR_SWATCHES } from './noteColors'
import Button from '../common/button/Button.svelte'
interface Props {
selectedColor: NoteColor
onColorChange: (color: NoteColor) => void
isOpen?: boolean
}
let { selectedColor, onColorChange, isOpen = $bindable(false) }: Props = $props()
</script>
<Popover
placement="bottom"
contentClasses="p-2"
floatingConfig={{ strategy: 'absolute' }}
usePointerDownOutside
bind:isOpen
>
{#snippet trigger()}
<Button
variant="subtle"
unifiedSize="sm"
selected={isOpen}
nonCaptureEvent
title={'Select color'}
startIcon={{ icon: Palette }}
iconOnly
/>
{/snippet}
{#snippet content()}
<div class="grid grid-cols-5 gap-1" style="min-width: 140px">
{#each Object.values(NoteColor) as color (color)}
<button
class="w-6 h-6 rounded-full hover:scale-110 transition-transform duration-100 {NOTE_COLOR_SWATCHES[
color
]} {selectedColor === color ? 'ring-2 ring-accent' : ' dark:border-gray-600'}"
onclick={() => {
onColorChange(color)
}}
title={color.charAt(0).toUpperCase() + color.slice(1)}
aria-label={`Select ${color} color`}
></button>
{/each}
</div>
{/snippet}
</Popover>
@@ -0,0 +1,112 @@
// Note color definitions with Tailwind classes for light and dark mode
export enum NoteColor {
YELLOW = 'yellow',
BLUE = 'blue',
GREEN = 'green',
PURPLE = 'purple',
PINK = 'pink',
ORANGE = 'orange',
RED = 'red',
CYAN = 'cyan',
LIME = 'lime',
GRAY = 'gray'
}
export interface NoteColorConfig {
background: string
outline: string
outlineHover: string
text: string
hover: string
}
// Color configurations for each note color with dark mode support
export const NOTE_COLORS: Record<NoteColor, NoteColorConfig> = {
[NoteColor.YELLOW]: {
background: 'bg-yellow-100 dark:bg-yellow-900',
outline: 'outline-yellow-300 dark:outline-yellow-600',
outlineHover: 'outline-yellow-300/60 dark:outline-yellow-600/60',
text: 'text-yellow-900 dark:text-yellow-100',
hover: 'hover:bg-yellow-200 dark:hover:bg-yellow-800'
},
[NoteColor.BLUE]: {
background: 'bg-blue-100 dark:bg-blue-900',
outline: 'outline-blue-300 dark:outline-blue-600',
outlineHover: 'outline-blue-300/60 dark:outline-blue-600/60',
text: 'text-blue-900 dark:text-blue-100',
hover: 'hover:bg-blue-200 dark:hover:bg-blue-800'
},
[NoteColor.GREEN]: {
background: 'bg-green-100 dark:bg-green-900',
outline: 'outline-green-300 dark:outline-green-600',
outlineHover: 'outline-green-300/60 dark:outline-green-600/60',
text: 'text-green-900 dark:text-green-100',
hover: 'hover:bg-green-200 dark:hover:bg-green-800'
},
[NoteColor.PURPLE]: {
background: 'bg-purple-100 dark:bg-purple-900/30',
outline: 'outline-purple-300 dark:outline-purple-600',
outlineHover: 'outline-purple-300/60 dark:outline-purple-600/60',
text: 'text-purple-900 dark:text-purple-100',
hover: 'hover:bg-purple-200 dark:hover:bg-purple-800'
},
[NoteColor.PINK]: {
background: 'bg-pink-100 dark:bg-pink-900',
outline: 'outline-pink-300 dark:outline-pink-600',
outlineHover: 'outline-pink-300/60 dark:outline-pink-600/60',
text: 'text-pink-900 dark:text-pink-100',
hover: 'hover:bg-pink-200 dark:hover:bg-pink-800'
},
[NoteColor.ORANGE]: {
background: 'bg-orange-100 dark:bg-orange-900',
outline: 'outline-orange-300 dark:outline-orange-600',
outlineHover: 'outline-orange-300/60 dark:outline-orange-600/60',
text: 'text-orange-900 dark:text-orange-100',
hover: 'hover:bg-orange-200 dark:hover:bg-orange-800'
},
[NoteColor.RED]: {
background: 'bg-red-100 dark:bg-red-900',
outline: 'outline-red-300 dark:outline-red-600',
outlineHover: 'outline-red-300/60 dark:outline-red-600/60',
text: 'text-red-900 dark:text-red-100',
hover: 'hover:bg-red-200 dark:hover:bg-red-800'
},
[NoteColor.CYAN]: {
background: 'bg-cyan-100 dark:bg-cyan-900',
outline: 'outline-cyan-300 dark:outline-cyan-600',
outlineHover: 'outline-cyan-300/60 dark:outline-cyan-600/60',
text: 'text-cyan-900 dark:text-cyan-100',
hover: 'hover:bg-cyan-200 dark:hover:bg-cyan-800'
},
[NoteColor.LIME]: {
background: 'bg-lime-100 dark:bg-lime-900',
outline: 'outline-lime-300 dark:outline-lime-600',
outlineHover: 'outline-lime-300/60 dark:outline-lime-600/60',
text: 'text-lime-900 dark:text-lime-100',
hover: 'hover:bg-lime-200 dark:hover:bg-lime-800'
},
[NoteColor.GRAY]: {
background: 'bg-gray-100 dark:bg-gray-800',
outline: 'outline-gray-300 dark:outline-gray-600',
outlineHover: 'outline-gray-300/60 dark:outline-gray-600/60',
text: 'text-gray-900 dark:text-gray-100',
hover: 'hover:bg-gray-200 dark:hover:bg-gray-700'
}
}
// Color swatch colors for the picker (solid colors for the palette dots)
export const NOTE_COLOR_SWATCHES: Record<NoteColor, string> = {
[NoteColor.YELLOW]: 'bg-yellow-400',
[NoteColor.BLUE]: 'bg-blue-400',
[NoteColor.GREEN]: 'bg-green-400',
[NoteColor.PURPLE]: 'bg-purple-400',
[NoteColor.PINK]: 'bg-pink-400',
[NoteColor.ORANGE]: 'bg-orange-400',
[NoteColor.RED]: 'bg-red-400',
[NoteColor.CYAN]: 'bg-cyan-400',
[NoteColor.LIME]: 'bg-lime-400',
[NoteColor.GRAY]: 'bg-gray-400'
}
// Default note color
export const DEFAULT_NOTE_COLOR = NoteColor.YELLOW
@@ -4,13 +4,17 @@
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: string
color: NoteColor
onUpdate?: (text: string) => void
onDelete?: () => void
onColorChange?: (color: NoteColor) => void
onSizeChange?: (size: { width: number; height: number }) => void
}
selected?: boolean
@@ -29,13 +33,20 @@
data.onUpdate?.(textContent)
}
function handleDelete(event: Event) {
event.preventDefault()
event.stopPropagation()
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()
@@ -63,14 +74,20 @@
editMode = false
}
})
let colorPickerIsOpen = $state(false)
</script>
<div
class={twMerge(
'relative w-full h-full rounded-md border group',
selected ? `outline outline-1 outline-lime-300` : ''
'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' : ''
)}
style:background-color={data.color}
onpointerup={() => {
dragging = false
}}
@@ -84,42 +101,62 @@
onmouseleave={handleMouseLeave}
role="note"
>
<!-- Delete button -->
<button
class="opacity-0 group-hover:opacity-100 hover:opacity-100 absolute -top-6 right-0 w-5 h-5 bg-transparent text-secondary hover:bg-red-500 hover:border-red-500 hover:text-white border rounded-md flex items-center justify-center transition-all duration-100 z-10"
onclick={handleDelete}
title="Delete note"
aria-label="Delete note"
>
<X size="12" />
</button>
<!-- 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}
{#if hovering || selected}
{#if !editMode && data.text}
<div
in:fade={{ duration: 200 }}
class="absolute -top-5 h-5 left-0 text-xs text-gray-400 rounded-md z-10 transition-opacity duration-300"
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-xs text-gray-400 rounded-md z-10 transition-opacity duration-300"
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="p-2 h-full rounded-md">
<div class="h-full rounded-md">
{#if editMode}
<!-- Edit mode: show textarea -->
<textarea
bind:this={textareaElement}
bind:value={textContent}
class="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-2"
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"
@@ -128,15 +165,19 @@
<!-- Render mode: show markdown or empty state -->
<!-- svelte-ignore a11y_no_static_element_interactions -->
<div
class="w-full h-full overflow-auto cursor-pointer flex items-start justify-center hover:bg-gray-400/10 rounded-md p-2"
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="w-full h-full text-xs rounded-md">
<div class={twMerge('w-full h-full text-xs rounded-md', colorConfig.text)}>
<GfmMarkdown md={data.text} noPadding />
</div>
{:else}
<div class="text-secondary text-xs italic"> Double-click to add a note </div>
<div class={twMerge('text-xs italic opacity-60', colorConfig.text)}>
Double-click to add a note
</div>
{/if}
</div>
{/if}