Skip to content
Merged
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
34 changes: 31 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,13 @@
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 +118,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(e: MouseEvent, role: string) {
const drag = getColorDrag();
if (!drag || e.button !== 0) return;
setColorDrag(null);
setAppOverride(selectedApp, role, drag.color, true);
dragOverRole = '';
}

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

function openMenu(e: MouseEvent, role: string) {
Expand Down Expand Up @@ -207,14 +230,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 scale-[1.06] border-2 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={e => onButtonMouseUp(e, 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
25 changes: 25 additions & 0 deletions frontend/src/lib/components/editor/ColorDragGhost.svelte
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
<script lang="ts">
import {getColorDrag} from '$lib/stores/ui.svelte';

let drag = $derived(getColorDrag());
let active = $derived(!!drag);

$effect(() => {
if (!active) return;
const previous = document.body.style.cursor;
document.body.style.cursor = 'copy';
return () => {
if (document.body.style.cursor === 'copy')
document.body.style.cursor = previous;
};
});
</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}
88 changes: 85 additions & 3 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, 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,82 @@
}

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

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

function onMouseDown(e: MouseEvent) {
if (e.button !== 0) return;
clearDrag();
didDrag = false;
if (e.ctrlKey || e.metaKey || e.shiftKey) return;
if (
(e.target as Element).closest('[role="button"]') !== e.currentTarget
)
return;
pendingDrag = {startX: e.clientX, startY: e.clientY, color};
window.addEventListener('mousemove', onDragMove);
window.addEventListener('mouseup', onDragUp);
window.addEventListener('blur', clearDrag);
window.addEventListener('keydown', onDragKeydown);
}

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

function onDragKeydown(e: KeyboardEvent) {
if (e.key === 'Escape') {
e.preventDefault();
clearDrag();
}
}

function onDragUp(e: MouseEvent) {
if (e.button === 0) clearDrag();
}

function clearDrag() {
window.removeEventListener('mousemove', onDragMove);
window.removeEventListener('mouseup', onDragUp);
window.removeEventListener('blur', clearDrag);
window.removeEventListener('keydown', onDragKeydown);
if (pendingDrag || isDragging) setColorDrag(null);
pendingDrag = null;
isDragging = false;
}

$effect(() => {
const _ = color;
return clearDrag;
});
</script>

<!-- Keyboard handling lives on the parent ColorPaletteGrid (roving tabindex). -->
Expand All @@ -123,16 +203,18 @@
? '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})` : ''
}\nClick edit · Ctrl+click copy · Shift+click select · Right-click for menu\n← → ↑ ↓ navigate · Enter open · L lock · C copy`}
}\nClick edit · Ctrl+click copy · Shift+click select · Right-click for menu\nDrag to copy into a template override\n← → ↑ ↓ navigate · Enter open · L lock · C copy`}
>
{#if selected}
<span
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>
7 changes: 7 additions & 0 deletions frontend/src/lib/stores/history.svelte.ts
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@ export interface Snapshot {
basePalette: string[];
extendedColors: Record<string, string>;
baseExtendedColors: Record<string, string>;
appOverrides: Record<string, Record<string, string>>;
adjustments: Adjustments;
paletteCurvePoints: [number, number][];
extractionMode: string;
Expand Down Expand Up @@ -50,6 +51,12 @@ export function copySnapshot(snapshot: Snapshot): Snapshot {
basePalette: [...snapshot.basePalette],
extendedColors: {...snapshot.extendedColors},
baseExtendedColors: {...snapshot.baseExtendedColors},
appOverrides: Object.fromEntries(
Object.entries(snapshot.appOverrides).map(([app, colors]) => [
app,
{...colors},
])
),
adjustments: {...snapshot.adjustments},
paletteCurvePoints: snapshot.paletteCurvePoints.map(([x, y]) => [x, y]),
extractionMode: snapshot.extractionMode,
Expand Down
14 changes: 13 additions & 1 deletion frontend/src/lib/stores/theme.svelte.ts
Original file line number Diff line number Diff line change
Expand Up @@ -180,6 +180,7 @@ export function getHistorySnapshot(): Snapshot {
basePalette,
extendedColors,
baseExtendedColors,
appOverrides,
adjustments,
paletteCurvePoints,
extractionMode,
Expand All @@ -195,6 +196,7 @@ export function restoreHistorySnapshot(snapshot: Snapshot): void {
basePalette = restored.basePalette;
extendedColors = restored.extendedColors;
baseExtendedColors = restored.baseExtendedColors;
appOverrides = restored.appOverrides;
adjustments = restored.adjustments;
paletteCurvePoints = restored.paletteCurvePoints;
extractionMode = restored.extractionMode;
Expand Down Expand Up @@ -379,8 +381,18 @@ export function setPaletteCurvePoints(pts: [number, number][]): void {
invalidateThemeRequests();
paletteCurvePoints = pts.map(([x, y]) => [x, y]);
}
export function setAppOverride(app: string, role: string, hex: string): void {
export function setAppOverride(
app: string,
role: string,
hex: string,
recordHistory = false
): void {
const current = appOverrides[app] || {};
if (current[role] === hex) return;
if (recordHistory) {
endColorEditSessions();
pushState(getHistorySnapshot());
}
appOverrides = {...appOverrides, [app]: {...current, [role]: hex}};
}
export function removeAppOverride(app: string, role: string): void {
Expand Down
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
Loading