Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -5,4 +5,5 @@ node_modules/
temp/
build/bin/
frontend/dist/
frontend/frontend/
build/darwin/
2 changes: 1 addition & 1 deletion frontend/package.json.md5
Original file line number Diff line number Diff line change
@@ -1 +1 @@
8e148ec9052b758c27dcf34bc3a3d905
358f50431c733436a625f0a56bcdbbb0
30 changes: 27 additions & 3 deletions frontend/src/lib/components/editor/AppColorOverrides.svelte
Original file line number Diff line number Diff line change
Expand Up @@ -6,8 +6,9 @@
getAppOverrides,
clearAppOverridesForApp,
removeAppOverride,
setAppOverride,
} from '$lib/stores/theme.svelte';
import {openOverrideColorPicker} from '$lib/stores/ui.svelte';
import {openOverrideColorPicker, getColorDrag, setColorDrag} from '$lib/stores/ui.svelte';
import {isLightColor, copyColor} from '$lib/utils/color';
import ContextMenu from '$lib/components/shared/ContextMenu.svelte';
import ExpandableSection from '$lib/components/shared/ExpandableSection.svelte';
Expand Down Expand Up @@ -113,6 +114,24 @@
return SHORT_LABELS[role] || role.replace(/_/g, ' ');
}

let dragOverRole = $state('');

$effect(() => {
if (!getColorDrag()) dragOverRole = '';
});

function onButtonMouseEnter(role: string) {
if (getColorDrag()) dragOverRole = role;
}

function onButtonMouseUp(role: string) {
const drag = getColorDrag();
if (!drag) return;
setColorDrag(null);
setAppOverride(selectedApp, role, drag.color);
dragOverRole = '';
}

let menu = $state({open: false, x: 0, y: 0, role: ''});

function openMenu(e: MouseEvent, role: string) {
Expand Down Expand Up @@ -207,14 +226,19 @@
class="group relative flex h-9 cursor-pointer items-end justify-center overflow-hidden border px-1 transition-all duration-100
{isOverridden
? 'border-accent border-2'
: 'border-border hover:border-border-focus'}"
: dragOverRole === role
? 'border-accent border-2 scale-[1.06] shadow-md'
: 'border-border hover:border-border-focus'}"
style:background-color={display}
onclick={() =>
openOverrideColorPicker(selectedApp, role)}
oncontextmenu={e => openMenu(e, role)}
onmouseenter={() => onButtonMouseEnter(role)}
onmouseleave={() => (dragOverRole = '')}
onmouseup={() => onButtonMouseUp(role)}
title="{role}{isOverridden
? ` · override ${appOverrides[role]}`
: ` · computed ${display}`}\nClick edit · Right-click for menu"
: ` · computed ${display}`}\nClick edit · Right-click for menu · Drag palette color to override"
>
<span
class="block w-full select-none truncate pb-0.5 text-center text-[8px] leading-none opacity-80 transition-opacity group-hover:opacity-100
Expand Down
18 changes: 18 additions & 0 deletions frontend/src/lib/components/editor/ColorDragGhost.svelte
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
<script lang="ts">
import {getColorDrag} from '$lib/stores/ui.svelte';

let drag = $derived(getColorDrag());

$effect(() => {
document.body.style.cursor = drag ? 'copy' : '';
});
</script>

{#if drag}
<div
class="pointer-events-none fixed z-[9999] h-7 w-7 border-2 border-white shadow-lg"
style:background-color={drag.color}
style:left="{drag.x + 12}px"
style:top="{drag.y + 12}px"
></div>
{/if}
51 changes: 49 additions & 2 deletions frontend/src/lib/components/editor/ColorSwatch.svelte
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@
contrastRatio,
contrastLevel,
} from '$lib/utils/color';
import {setEyedropperActive} from '$lib/stores/ui.svelte';
import {setEyedropperActive, getColorDrag, setColorDrag} from '$lib/stores/ui.svelte';
import {onActivate} from '$lib/utils/keyboard';
import LockIcon from '$lib/components/shared/LockIcon.svelte';
import ContextMenu from '$lib/components/shared/ContextMenu.svelte';
Expand Down Expand Up @@ -99,6 +99,10 @@
}

function handleClick(event: MouseEvent) {
if (didDrag) {
didDrag = false;
return;
}
if (event.ctrlKey || event.metaKey) {
event.preventDefault();
copyColor(color);
Expand All @@ -113,6 +117,47 @@
}

let light = $derived(isLightColor(color));

let isDragging = $state(false);
let didDrag = $state(false);
let pendingDrag: {startX: number; startY: number} | null = null;

function onMouseDown(e: MouseEvent) {
if (e.button !== 0) return;
pendingDrag = {startX: e.clientX, startY: e.clientY};
window.addEventListener('mousemove', onDragMove);
window.addEventListener('mouseup', onDragUp);
}

function onDragMove(e: MouseEvent) {
if (isDragging) {
setColorDrag({color, x: e.clientX, y: e.clientY});
return;
}
if (!pendingDrag) return;
const dx = e.clientX - pendingDrag.startX;
const dy = e.clientY - pendingDrag.startY;
if (Math.hypot(dx, dy) >= 4) {
didDrag = true;
isDragging = true;
setColorDrag({color, x: e.clientX, y: e.clientY});
}
}

function onDragUp() {
window.removeEventListener('mousemove', onDragMove);
window.removeEventListener('mouseup', onDragUp);
pendingDrag = null;
isDragging = false;
setColorDrag(null);
}

$effect(() => {
return () => {
window.removeEventListener('mousemove', onDragMove);
window.removeEventListener('mouseup', onDragUp);
};
});
</script>

<!-- Keyboard handling lives on the parent ColorPaletteGrid (roving tabindex). -->
Expand All @@ -123,12 +168,14 @@
? 'border-border cursor-default'
: selected
? 'border-accent cursor-pointer border-2'
: 'hover:border-accent border-border cursor-pointer hover:z-10 hover:scale-[1.04] hover:shadow-lg'}"
: 'hover:border-accent border-border cursor-pointer hover:z-10 hover:scale-[1.04] hover:shadow-lg'}
{isDragging ? 'opacity-60' : ''}"
style:background-color={color}
role="button"
tabindex={focused ? 0 : -1}
data-swatch-idx={index}
onclick={handleClick}
onmousedown={onMouseDown}
oncontextmenu={handleContextMenu}
title={`${label}${role ? ` · ${role}` : ''}\n${color}${
showBadge ? `\nContrast vs BG: ${ratio.toFixed(2)}:1 (${level})` : ''
Expand Down
2 changes: 2 additions & 0 deletions frontend/src/lib/components/editor/ThemeEditor.svelte
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@
import SettingsSidebar from '../sidebar/SettingsSidebar.svelte';
import ColorPickerDialog from '../color-picker/ColorPickerDialog.svelte';
import WallpaperEditor from '../wallpaper-editor/WallpaperEditor.svelte';
import ColorDragGhost from './ColorDragGhost.svelte';
import {getWallpaperPath, getPalette} from '$lib/stores/theme.svelte';
import {
getSidebarVisible,
Expand Down Expand Up @@ -74,4 +75,5 @@
open={getImageEditorOpen()}
onclose={() => setImageEditorOpen(false)}
/>
<ColorDragGhost />
</div>
10 changes: 10 additions & 0 deletions frontend/src/lib/stores/ui.svelte.ts
Original file line number Diff line number Diff line change
Expand Up @@ -45,6 +45,7 @@ function writeBoolPref(key: string, value: boolean): void {
} catch {}
}
let colorPickerOpen = $state<boolean>(false);
let colorDrag = $state<{color: string; x: number; y: number} | null>(null);
let colorPickerIndex = $state<number>(-1);
let colorPickerExtKey = $state<string>(''); // non-empty = editing an extended color
let colorPickerOverrideApp = $state<string>(''); // non-empty = editing an app override
Expand Down Expand Up @@ -276,3 +277,12 @@ export function getApplySaveDialogOpen(): boolean {
export function setApplySaveDialogOpen(v: boolean): void {
applySaveDialogOpen = v;
}

export function getColorDrag(): {color: string; x: number; y: number} | null {
return colorDrag;
}
export function setColorDrag(
v: {color: string; x: number; y: number} | null
): void {
colorDrag = v;
}
Loading