mirror of
https://github.com/windmill-labs/windmill.git
synced 2026-09-09 16:05:42 +00:00
add note color picker
This commit is contained in:
@@ -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>
|
||||
|
||||
@@ -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}
|
||||
|
||||
Reference in New Issue
Block a user