mirror of
https://github.com/BetterSEQTA/BetterSEQTA-Plus.git
synced 2026-06-05 19:24:39 +00:00
feat: add back react colour picker
This commit is contained in:
+3
-1
@@ -60,7 +60,6 @@
|
|||||||
"@codemirror/search": "^6.5.10",
|
"@codemirror/search": "^6.5.10",
|
||||||
"@codemirror/state": "^6.5.2",
|
"@codemirror/state": "^6.5.2",
|
||||||
"@codemirror/view": "^6.36.4",
|
"@codemirror/view": "^6.36.4",
|
||||||
"@jaames/iro": "^5.5.2",
|
|
||||||
"@sveltejs/vite-plugin-svelte": "^5.0.3",
|
"@sveltejs/vite-plugin-svelte": "^5.0.3",
|
||||||
"@tailwindcss/forms": "^0.5.9",
|
"@tailwindcss/forms": "^0.5.9",
|
||||||
"@tailwindcss/vite": "^4.0.12",
|
"@tailwindcss/vite": "^4.0.12",
|
||||||
@@ -86,6 +85,9 @@
|
|||||||
"lodash": "^4.17.21",
|
"lodash": "^4.17.21",
|
||||||
"million": "^3.1.11",
|
"million": "^3.1.11",
|
||||||
"motion": "^12.4.12",
|
"motion": "^12.4.12",
|
||||||
|
"react": "17",
|
||||||
|
"react-best-gradient-color-picker": "3.0.11",
|
||||||
|
"react-dom": "17",
|
||||||
"rss-parser": "^3.13.0",
|
"rss-parser": "^3.13.0",
|
||||||
"sortablejs": "^1.15.6",
|
"sortablejs": "^1.15.6",
|
||||||
"svelte": "^5.22.6",
|
"svelte": "^5.22.6",
|
||||||
|
|||||||
@@ -1,93 +1,20 @@
|
|||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
import { onMount } from 'svelte'
|
import { onMount } from 'svelte'
|
||||||
|
import ColourPicker from './ColourPicker.tsx';
|
||||||
|
import ReactAdapter from './utils/ReactAdapter.svelte';
|
||||||
import { animate } from 'motion';
|
import { animate } from 'motion';
|
||||||
import { delay } from '@/seqta/utils/delay.ts'
|
import { delay } from '@/seqta/utils/delay.ts'
|
||||||
import { settingsState } from '@/seqta/utils/listeners/SettingsState.ts'
|
|
||||||
import iro from '@jaames/iro'
|
|
||||||
|
|
||||||
interface GradientStop {
|
const { hidePicker, standalone = false, savePresets = true, customOnChange = null, customState = null } = $props<{
|
||||||
color: string;
|
|
||||||
offset: number;
|
|
||||||
alpha: number;
|
|
||||||
}
|
|
||||||
|
|
||||||
const { hidePicker, standalone = false, customOnChange = null, customState = null } = $props<{
|
|
||||||
hidePicker?: () => void,
|
hidePicker?: () => void,
|
||||||
standalone?: boolean,
|
standalone?: boolean,
|
||||||
|
savePresets?: boolean,
|
||||||
customOnChange?: (color: string) => void,
|
customOnChange?: (color: string) => void,
|
||||||
customState?: string
|
customState?: string
|
||||||
}>();
|
}>();
|
||||||
|
|
||||||
let background = $state<HTMLDivElement | null>(null);
|
let background = $state<HTMLDivElement | null>(null);
|
||||||
let content = $state<HTMLDivElement | null>(null);
|
let content = $state<HTMLDivElement | null>(null);
|
||||||
let pickerContainer = $state<HTMLDivElement | null>(null);
|
|
||||||
let gradientBarElement = $state<HTMLDivElement | null>(null);
|
|
||||||
let colorPicker: iro.ColorPicker | null = null;
|
|
||||||
|
|
||||||
let currentMode = $state('solid');
|
|
||||||
let currentColor = $state(getInitialColor());
|
|
||||||
let gradientAngle = $state(90);
|
|
||||||
let gradientStops = $state<GradientStop[]>(getInitialGradientStops());
|
|
||||||
let selectedStopIndex = $state(0);
|
|
||||||
let isDragging = $state(false);
|
|
||||||
let dragStartX = $state(0);
|
|
||||||
let dragStartOffset = $state(0);
|
|
||||||
|
|
||||||
function getInitialColor() {
|
|
||||||
const color = customState || settingsState.selectedColor || '#007bff';
|
|
||||||
if (color.includes('gradient')) {
|
|
||||||
const match = color.match(/#[0-9a-fA-F]{6}|rgb\([^)]+\)/);
|
|
||||||
return match ? match[0] : '#007bff';
|
|
||||||
}
|
|
||||||
return color;
|
|
||||||
}
|
|
||||||
|
|
||||||
function getInitialGradientStops(): GradientStop[] {
|
|
||||||
const color = customState || settingsState.selectedColor || '#007bff';
|
|
||||||
if (color.includes('gradient')) {
|
|
||||||
const matches = color.match(/(#[0-9a-fA-F]{6}|rgba?\([^)]+\))\s+(\d+)%/g);
|
|
||||||
if (matches) {
|
|
||||||
return matches.map((match: string) => {
|
|
||||||
const [_, color, offset] = match.match(/(#[0-9a-fA-F]{6}|rgba?\([^)]+\))\s+(\d+)/)!;
|
|
||||||
const alpha = color.startsWith('rgba') ?
|
|
||||||
parseFloat(color.match(/rgba?\([^)]+,\s*([^)]+)\)/)?.[1] || '1') :
|
|
||||||
1;
|
|
||||||
return {
|
|
||||||
color: color.startsWith('#') ? color : rgbaToHex(color),
|
|
||||||
offset: parseInt(offset),
|
|
||||||
alpha
|
|
||||||
};
|
|
||||||
});
|
|
||||||
}
|
|
||||||
const angleMatch = color.match(/gradient\((\d+)deg/);
|
|
||||||
if (angleMatch) {
|
|
||||||
gradientAngle = parseInt(angleMatch[1]);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return [
|
|
||||||
{ color: '#007bff', offset: 0, alpha: 1 },
|
|
||||||
{ color: '#00ff88', offset: 100, alpha: 1 }
|
|
||||||
];
|
|
||||||
}
|
|
||||||
|
|
||||||
const updateColor = () => {
|
|
||||||
if (!colorPicker) return;
|
|
||||||
|
|
||||||
const newColor = currentMode === 'solid'
|
|
||||||
? colorPicker.color.hexString
|
|
||||||
: `linear-gradient(${gradientAngle}deg, ${gradientStops.map(stop => {
|
|
||||||
const color = stop.alpha < 1
|
|
||||||
? hexToRgba(stop.color, stop.alpha)
|
|
||||||
: stop.color;
|
|
||||||
return `${color} ${stop.offset}%`;
|
|
||||||
}).join(', ')})`;
|
|
||||||
|
|
||||||
if (customOnChange) {
|
|
||||||
customOnChange(newColor);
|
|
||||||
} else {
|
|
||||||
settingsState.selectedColor = newColor;
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
const closePicker = async () => {
|
const closePicker = async () => {
|
||||||
if (standalone) return;
|
if (standalone) return;
|
||||||
@@ -110,79 +37,40 @@
|
|||||||
);
|
);
|
||||||
|
|
||||||
await delay(400);
|
await delay(400);
|
||||||
hidePicker?.();
|
hidePicker();
|
||||||
}
|
}
|
||||||
|
|
||||||
onMount(() => {
|
onMount(() => {
|
||||||
if (!pickerContainer) return;
|
if (standalone) return;
|
||||||
|
if (!background || !content) return;
|
||||||
|
|
||||||
const picker = new (iro as any).ColorPicker(pickerContainer, {
|
animate(
|
||||||
width: 250,
|
background,
|
||||||
color: currentColor,
|
{ opacity: [0, 1] },
|
||||||
layout: [
|
{ duration: 0.3, ease: [0.4, 0, 0.2, 1] }
|
||||||
{
|
);
|
||||||
component: iro.ui.Box,
|
|
||||||
options: {}
|
|
||||||
},
|
|
||||||
{
|
|
||||||
component: iro.ui.Slider,
|
|
||||||
options: {
|
|
||||||
sliderType: 'hue'
|
|
||||||
}
|
|
||||||
},
|
|
||||||
{
|
|
||||||
component: iro.ui.Slider,
|
|
||||||
options: {
|
|
||||||
sliderType: 'alpha'
|
|
||||||
}
|
|
||||||
}
|
|
||||||
]
|
|
||||||
});
|
|
||||||
|
|
||||||
colorPicker = picker;
|
animate(
|
||||||
picker.on('color:change', (color: any) => {
|
content,
|
||||||
if (currentMode === 'solid') {
|
{ scale: [0.4, 1], opacity: [0, 1] },
|
||||||
currentColor = color.hexString;
|
{
|
||||||
updateColor();
|
type: 'spring',
|
||||||
} else {
|
stiffness: 400,
|
||||||
gradientStops[selectedStopIndex].color = color.hexString;
|
damping: 30
|
||||||
gradientStops[selectedStopIndex].alpha = color.alpha;
|
|
||||||
gradientStops = [...gradientStops];
|
|
||||||
updateColor();
|
|
||||||
}
|
}
|
||||||
});
|
);
|
||||||
|
|
||||||
if (!standalone) {
|
const handleEscapeKey = (e: KeyboardEvent) => {
|
||||||
if (!background || !content) return;
|
if (e.key === 'Escape') {
|
||||||
|
closePicker();
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
animate(
|
document.addEventListener('keydown', handleEscapeKey);
|
||||||
background,
|
|
||||||
{ opacity: [0, 1] },
|
|
||||||
{ duration: 0.3, ease: [0.4, 0, 0.2, 1] }
|
|
||||||
);
|
|
||||||
|
|
||||||
animate(
|
return () => {
|
||||||
content,
|
document.removeEventListener('keydown', handleEscapeKey);
|
||||||
{ scale: [0.4, 1], opacity: [0, 1] },
|
};
|
||||||
{
|
|
||||||
type: 'spring',
|
|
||||||
stiffness: 400,
|
|
||||||
damping: 30
|
|
||||||
}
|
|
||||||
);
|
|
||||||
|
|
||||||
const handleEscapeKey = (e: KeyboardEvent) => {
|
|
||||||
if (e.key === 'Escape') {
|
|
||||||
closePicker();
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
document.addEventListener('keydown', handleEscapeKey);
|
|
||||||
|
|
||||||
return () => {
|
|
||||||
document.removeEventListener('keydown', handleEscapeKey);
|
|
||||||
};
|
|
||||||
}
|
|
||||||
});
|
});
|
||||||
|
|
||||||
function handleBackgroundClick(event: MouseEvent) {
|
function handleBackgroundClick(event: MouseEvent) {
|
||||||
@@ -190,357 +78,25 @@
|
|||||||
closePicker();
|
closePicker();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
function switchMode(mode: 'solid' | 'gradient') {
|
|
||||||
if (!colorPicker) return;
|
|
||||||
|
|
||||||
currentMode = mode;
|
|
||||||
if (mode === 'solid') {
|
|
||||||
colorPicker.color.hexString = gradientStops[0].color;
|
|
||||||
} else {
|
|
||||||
selectedStopIndex = 0;
|
|
||||||
colorPicker.color.hexString = gradientStops[0].color;
|
|
||||||
}
|
|
||||||
updateColor();
|
|
||||||
}
|
|
||||||
|
|
||||||
function selectGradientStop(index: number) {
|
|
||||||
if (!colorPicker) return;
|
|
||||||
selectedStopIndex = index;
|
|
||||||
colorPicker.color.hexString = gradientStops[index].color;
|
|
||||||
}
|
|
||||||
|
|
||||||
function updateGradientAngle(event: Event) {
|
|
||||||
gradientAngle = parseInt((event.target as HTMLInputElement).value);
|
|
||||||
updateColor();
|
|
||||||
}
|
|
||||||
|
|
||||||
function handleMouseDown(event: MouseEvent, index: number) {
|
|
||||||
isDragging = true;
|
|
||||||
dragStartX = event.clientX;
|
|
||||||
dragStartOffset = gradientStops[index].offset;
|
|
||||||
selectGradientStop(index);
|
|
||||||
|
|
||||||
// Store the gradient bar element reference
|
|
||||||
const gradientBar = gradientBarElement;
|
|
||||||
if (!gradientBar) return;
|
|
||||||
|
|
||||||
const handleMouseMove = (e: MouseEvent) => {
|
|
||||||
if (!isDragging || !gradientBar) return;
|
|
||||||
|
|
||||||
const previewRect = gradientBar.getBoundingClientRect();
|
|
||||||
const delta = e.clientX - dragStartX;
|
|
||||||
const percentDelta = (delta / previewRect.width) * 100;
|
|
||||||
const newOffset = Math.max(0, Math.min(100, dragStartOffset + percentDelta));
|
|
||||||
|
|
||||||
// Update the current stop's position
|
|
||||||
gradientStops[selectedStopIndex].offset = Math.round(newOffset);
|
|
||||||
|
|
||||||
// Check if we need to reorder stops
|
|
||||||
const stops = [...gradientStops];
|
|
||||||
stops.sort((a, b) => a.offset - b.offset);
|
|
||||||
|
|
||||||
// Find the new index of our stop
|
|
||||||
const newIndex = stops.findIndex(stop => stop === gradientStops[selectedStopIndex]);
|
|
||||||
|
|
||||||
if (newIndex !== selectedStopIndex) {
|
|
||||||
// Update the selected index to match the new position
|
|
||||||
selectedStopIndex = newIndex;
|
|
||||||
}
|
|
||||||
|
|
||||||
gradientStops = stops;
|
|
||||||
updateColor();
|
|
||||||
};
|
|
||||||
|
|
||||||
const handleMouseUp = () => {
|
|
||||||
isDragging = false;
|
|
||||||
document.removeEventListener('mousemove', handleMouseMove);
|
|
||||||
document.removeEventListener('mouseup', handleMouseUp);
|
|
||||||
};
|
|
||||||
|
|
||||||
document.addEventListener('mousemove', handleMouseMove);
|
|
||||||
document.addEventListener('mouseup', handleMouseUp);
|
|
||||||
}
|
|
||||||
|
|
||||||
function handlePreviewClick(event: MouseEvent) {
|
|
||||||
if (isDragging) return;
|
|
||||||
|
|
||||||
const previewRect = (event.currentTarget as HTMLElement).getBoundingClientRect();
|
|
||||||
const clickOffset = ((event.clientX - previewRect.left) / previewRect.width) * 100;
|
|
||||||
|
|
||||||
// Find insertion point
|
|
||||||
let insertIndex = gradientStops.findIndex(stop => stop.offset > clickOffset);
|
|
||||||
if (insertIndex === -1) insertIndex = gradientStops.length;
|
|
||||||
|
|
||||||
// Get color at click position
|
|
||||||
const prevStop = gradientStops.reduce((prev, curr) =>
|
|
||||||
curr.offset <= clickOffset ? curr : prev
|
|
||||||
, gradientStops[0]);
|
|
||||||
const nextStop = gradientStops.find(stop => stop.offset > clickOffset) || gradientStops[gradientStops.length - 1];
|
|
||||||
|
|
||||||
// Interpolate color and alpha
|
|
||||||
const t = (clickOffset - prevStop.offset) / (nextStop.offset - prevStop.offset);
|
|
||||||
const color = interpolateColor(prevStop.color, nextStop.color, t);
|
|
||||||
const alpha = prevStop.alpha + (nextStop.alpha - prevStop.alpha) * t;
|
|
||||||
|
|
||||||
const newStop = {
|
|
||||||
color,
|
|
||||||
offset: Math.round(clickOffset),
|
|
||||||
alpha
|
|
||||||
};
|
|
||||||
|
|
||||||
if (gradientStops.length < 5) {
|
|
||||||
gradientStops = [
|
|
||||||
...gradientStops.slice(0, insertIndex),
|
|
||||||
newStop,
|
|
||||||
...gradientStops.slice(insertIndex)
|
|
||||||
];
|
|
||||||
selectGradientStop(insertIndex);
|
|
||||||
updateColor();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
function removeGradientStop(index: number) {
|
|
||||||
if (gradientStops.length > 2) {
|
|
||||||
gradientStops.splice(index, 1);
|
|
||||||
selectedStopIndex = index === 0 ? 0 : index - 1;
|
|
||||||
updateColor();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
function interpolateColor(color1: string, color2: string, t: number): string {
|
|
||||||
// Convert hex to rgb
|
|
||||||
const c1 = hexToRgb(color1);
|
|
||||||
const c2 = hexToRgb(color2);
|
|
||||||
|
|
||||||
// Interpolate
|
|
||||||
const r = Math.round(c1.r + (c2.r - c1.r) * t);
|
|
||||||
const g = Math.round(c1.g + (c2.g - c1.g) * t);
|
|
||||||
const b = Math.round(c1.b + (c2.b - c1.b) * t);
|
|
||||||
|
|
||||||
// Convert back to hex
|
|
||||||
return rgbToHex(r, g, b);
|
|
||||||
}
|
|
||||||
|
|
||||||
function hexToRgb(hex: string) {
|
|
||||||
const result = /^#?([a-f\d]{2})([a-f\d]{2})([a-f\d]{2})$/i.exec(hex);
|
|
||||||
return result ? {
|
|
||||||
r: parseInt(result[1], 16),
|
|
||||||
g: parseInt(result[2], 16),
|
|
||||||
b: parseInt(result[3], 16)
|
|
||||||
} : { r: 0, g: 0, b: 0 };
|
|
||||||
}
|
|
||||||
|
|
||||||
function rgbToHex(r: number, g: number, b: number) {
|
|
||||||
return `#${((1 << 24) + (r << 16) + (g << 8) + b).toString(16).slice(1)}`;
|
|
||||||
}
|
|
||||||
|
|
||||||
function hexToRgba(hex: string, alpha: number): string {
|
|
||||||
const result = /^#?([a-f\d]{2})([a-f\d]{2})([a-f\d]{2})$/i.exec(hex);
|
|
||||||
if (!result) return `rgba(0, 0, 0, ${alpha})`;
|
|
||||||
const r = parseInt(result[1], 16);
|
|
||||||
const g = parseInt(result[2], 16);
|
|
||||||
const b = parseInt(result[3], 16);
|
|
||||||
return `rgba(${r}, ${g}, ${b}, ${alpha})`;
|
|
||||||
}
|
|
||||||
|
|
||||||
function rgbaToHex(rgba: string): string {
|
|
||||||
const match = rgba.match(/rgba?\((\d+),\s*(\d+),\s*(\d+)/);
|
|
||||||
if (!match) return '#000000';
|
|
||||||
const [_, r, g, b] = match;
|
|
||||||
return rgbToHex(parseInt(r), parseInt(g), parseInt(b));
|
|
||||||
}
|
|
||||||
|
|
||||||
const gradientPreviewStyle = $derived(`
|
|
||||||
background: linear-gradient(${gradientAngle}deg, ${gradientStops.map(stop =>
|
|
||||||
`${stop.alpha < 1 ? hexToRgba(stop.color, stop.alpha) : stop.color} ${stop.offset}%`
|
|
||||||
).join(', ')})
|
|
||||||
`);
|
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
{#if standalone}
|
{#if standalone}
|
||||||
<div class="h-auto rounded-xl overflow-clip bg-white dark:bg-zinc-800 p-4">
|
<div class="h-auto rounded-xl overflow-clip">
|
||||||
<div class="flex gap-2 mb-4">
|
<ReactAdapter customOnChange={customOnChange} customState={customState} savePresets={savePresets} el={ColourPicker} />
|
||||||
<button
|
|
||||||
class="px-3 py-1 rounded-md text-sm {currentMode === 'solid' ? 'bg-blue-500 text-white' : 'bg-zinc-100 dark:bg-zinc-700'}"
|
|
||||||
onclick={() => switchMode('solid')}
|
|
||||||
>
|
|
||||||
Solid
|
|
||||||
</button>
|
|
||||||
<button
|
|
||||||
class="px-3 py-1 rounded-md text-sm {currentMode === 'gradient' ? 'bg-blue-500 text-white' : 'bg-zinc-100 dark:bg-zinc-700'}"
|
|
||||||
onclick={() => switchMode('gradient')}
|
|
||||||
>
|
|
||||||
Gradient
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div bind:this={pickerContainer} class="mb-4"></div>
|
|
||||||
|
|
||||||
{#if currentMode === 'gradient'}
|
|
||||||
<div class="space-y-4">
|
|
||||||
<!-- Gradient Preview Bar -->
|
|
||||||
<div
|
|
||||||
bind:this={gradientBarElement}
|
|
||||||
class="h-8 rounded-full relative cursor-pointer overflow-hidden shadow-lg mx-4"
|
|
||||||
style="background-image: linear-gradient(45deg, #808080 25%, transparent 25%), linear-gradient(-45deg, #808080 25%, transparent 25%), linear-gradient(45deg, transparent 75%, #808080 75%), linear-gradient(-45deg, transparent 75%, #808080 75%);
|
|
||||||
background-size: 16px 16px;
|
|
||||||
background-position: 0 0, 0 8px, 8px -8px, -8px 0px;"
|
|
||||||
onmousedown={handlePreviewClick}
|
|
||||||
role="button"
|
|
||||||
aria-label="Gradient preview bar"
|
|
||||||
tabindex="0"
|
|
||||||
>
|
|
||||||
<!-- Gradient Overlay -->
|
|
||||||
<div
|
|
||||||
class="absolute inset-0 w-full h-full"
|
|
||||||
style={gradientPreviewStyle}
|
|
||||||
></div>
|
|
||||||
|
|
||||||
<!-- Gradient Stops -->
|
|
||||||
{#each gradientStops as stop, i}
|
|
||||||
<button
|
|
||||||
class="absolute w-6 h-6 -ml-3 rounded-full"
|
|
||||||
style="left: {stop.offset}%; top: 50%; transform: translate(0, -50%); background: transparent; box-shadow: 0 0 0 2px white, 0 0 0 4px rgba(0,0,0,0.1);"
|
|
||||||
onmousedown={(e) => handleMouseDown(e, i)}
|
|
||||||
onclick={(e) => e.stopPropagation()}
|
|
||||||
>
|
|
||||||
{#if selectedStopIndex === i}
|
|
||||||
<div class="absolute inset-0 m-auto w-1 h-1 rounded-full bg-white"></div>
|
|
||||||
{/if}
|
|
||||||
</button>
|
|
||||||
{/each}
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<!-- Controls -->
|
|
||||||
<div class="flex items-center justify-between gap-4">
|
|
||||||
<div class="flex items-center gap-2 flex-1">
|
|
||||||
<label for="gradient-angle" class="text-sm font-medium">Angle:</label>
|
|
||||||
<input
|
|
||||||
id="gradient-angle"
|
|
||||||
type="range"
|
|
||||||
min="0"
|
|
||||||
max="360"
|
|
||||||
value={gradientAngle}
|
|
||||||
onchange={updateGradientAngle}
|
|
||||||
class="flex-1"
|
|
||||||
/>
|
|
||||||
<span class="text-sm font-medium w-12 text-right">{gradientAngle}°</span>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{#if gradientStops.length > 2}
|
|
||||||
<button
|
|
||||||
onclick={() => removeGradientStop(selectedStopIndex)}
|
|
||||||
class="p-1.5 rounded-full hover:bg-zinc-100 dark:hover:bg-zinc-700 transition-colors"
|
|
||||||
title="Remove selected stop"
|
|
||||||
aria-label="Remove selected stop"
|
|
||||||
>
|
|
||||||
<svg xmlns="http://www.w3.org/2000/svg" class="w-4 h-4" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
|
|
||||||
<path d="M3 6h18M19 6v14a2 2 0 01-2 2H7a2 2 0 01-2-2V6m3 0V4a2 2 0 012-2h4a2 2 0 012 2v2"></path>
|
|
||||||
</svg>
|
|
||||||
</button>
|
|
||||||
{/if}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
{/if}
|
|
||||||
</div>
|
</div>
|
||||||
{:else}
|
{:else}
|
||||||
<!-- Modal version -->
|
<!-- svelte-ignore a11y_no_static_element_interactions -->
|
||||||
<div
|
<div
|
||||||
bind:this={background}
|
bind:this={background}
|
||||||
class="absolute top-0 left-0 z-50 flex items-center justify-center w-full h-full cursor-pointer bg-black/20"
|
class="absolute top-0 left-0 z-50 flex items-center justify-center w-full h-full cursor-pointer bg-black/20"
|
||||||
onclick={handleBackgroundClick}
|
onclick={handleBackgroundClick}
|
||||||
onkeydown={(e) => { e.key === 'Enter' && handleBackgroundClick }}
|
onkeydown={(e) => { e.key === 'Enter' && handleBackgroundClick }}
|
||||||
role="button"
|
|
||||||
aria-label="Close color picker"
|
|
||||||
tabindex="0"
|
|
||||||
>
|
>
|
||||||
<div
|
<div
|
||||||
bind:this={content}
|
bind:this={content}
|
||||||
class="h-auto p-4 bg-white border shadow-lg cursor-auto rounded-xl dark:bg-zinc-800 border-zinc-100 dark:border-zinc-700"
|
class="h-auto p-4 bg-white border shadow-lg cursor-auto rounded-xl dark:bg-zinc-800 border-zinc-100 dark:border-zinc-700"
|
||||||
>
|
>
|
||||||
<!-- Same content as standalone version -->
|
<ReactAdapter customOnChange={customOnChange} customState={customState} savePresets={savePresets} el={ColourPicker} />
|
||||||
<div class="flex gap-2 mb-4">
|
|
||||||
<button
|
|
||||||
class="px-3 py-1 rounded-md text-sm {currentMode === 'solid' ? 'bg-blue-500 text-white' : 'bg-zinc-100 dark:bg-zinc-700'}"
|
|
||||||
onclick={() => switchMode('solid')}
|
|
||||||
>
|
|
||||||
Solid
|
|
||||||
</button>
|
|
||||||
<button
|
|
||||||
class="px-3 py-1 rounded-md text-sm {currentMode === 'gradient' ? 'bg-blue-500 text-white' : 'bg-zinc-100 dark:bg-zinc-700'}"
|
|
||||||
onclick={() => switchMode('gradient')}
|
|
||||||
>
|
|
||||||
Gradient
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div bind:this={pickerContainer} class="mb-4"></div>
|
|
||||||
|
|
||||||
{#if currentMode === 'gradient'}
|
|
||||||
<div class="space-y-4">
|
|
||||||
<!-- Gradient Preview Bar -->
|
|
||||||
<div
|
|
||||||
bind:this={gradientBarElement}
|
|
||||||
class="h-8 rounded-full relative cursor-pointer overflow-hidden shadow-lg mx-4"
|
|
||||||
style="background-image: linear-gradient(45deg, #808080 25%, transparent 25%), linear-gradient(-45deg, #808080 25%, transparent 25%), linear-gradient(45deg, transparent 75%, #808080 75%), linear-gradient(-45deg, transparent 75%, #808080 75%);
|
|
||||||
background-size: 16px 16px;
|
|
||||||
background-position: 0 0, 0 8px, 8px -8px, -8px 0px;"
|
|
||||||
onclick={handlePreviewClick}
|
|
||||||
role="button"
|
|
||||||
aria-label="Gradient preview bar"
|
|
||||||
tabindex="0"
|
|
||||||
>
|
|
||||||
<!-- Gradient Overlay -->
|
|
||||||
<div
|
|
||||||
class="absolute inset-0 w-full h-full"
|
|
||||||
style={gradientPreviewStyle}
|
|
||||||
></div>
|
|
||||||
|
|
||||||
<!-- Gradient Stops -->
|
|
||||||
{#each gradientStops as stop, i}
|
|
||||||
<button
|
|
||||||
class="absolute w-6 h-6 -ml-3 rounded-full transition-all"
|
|
||||||
style="left: {stop.offset}%; top: 50%; transform: translate(0, -50%); background: transparent; box-shadow: 0 0 0 2px white, 0 0 0 4px rgba(0,0,0,0.1);"
|
|
||||||
onmousedown={(e) => handleMouseDown(e, i)}
|
|
||||||
onclick={(e) => e.stopPropagation()}
|
|
||||||
>
|
|
||||||
{#if selectedStopIndex === i}
|
|
||||||
<div class="absolute inset-0 m-auto w-1 h-1 rounded-full bg-white"></div>
|
|
||||||
{/if}
|
|
||||||
</button>
|
|
||||||
{/each}
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<!-- Controls -->
|
|
||||||
<div class="flex items-center justify-between gap-4">
|
|
||||||
<div class="flex items-center gap-2 flex-1">
|
|
||||||
<label class="text-sm font-medium">Angle:</label>
|
|
||||||
<input
|
|
||||||
type="range"
|
|
||||||
min="0"
|
|
||||||
max="360"
|
|
||||||
value={gradientAngle}
|
|
||||||
onchange={updateGradientAngle}
|
|
||||||
class="flex-1"
|
|
||||||
/>
|
|
||||||
<span class="text-sm font-medium w-12 text-right">{gradientAngle}°</span>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{#if gradientStops.length > 2}
|
|
||||||
<button
|
|
||||||
onclick={() => removeGradientStop(selectedStopIndex)}
|
|
||||||
class="p-1.5 rounded-full hover:bg-zinc-100 dark:hover:bg-zinc-700 transition-colors"
|
|
||||||
title="Remove selected stop"
|
|
||||||
>
|
|
||||||
<svg xmlns="http://www.w3.org/2000/svg" class="w-4 h-4" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
|
|
||||||
<path d="M3 6h18M19 6v14a2 2 0 01-2 2H7a2 2 0 01-2-2V6m3 0V4a2 2 0 012-2h4a2 2 0 012 2v2"></path>
|
|
||||||
</svg>
|
|
||||||
</button>
|
|
||||||
{/if}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
{/if}
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
{/if}
|
{/if}
|
||||||
|
|||||||
@@ -0,0 +1,108 @@
|
|||||||
|
import ColorPicker from "react-best-gradient-color-picker"
|
||||||
|
import { useEffect, useRef, useState } from "react"
|
||||||
|
import { settingsState } from "@/seqta/utils/listeners/SettingsState.ts"
|
||||||
|
|
||||||
|
const defaultPresets = [
|
||||||
|
"linear-gradient(30deg, rgba(229,209,218,1) 0%, RGBA(235,169,202,1) 46%, rgba(214,155,162,1) 100%)",
|
||||||
|
"linear-gradient(40deg, rgba(201,61,0,1) 0%, RGBA(170, 5, 58, 1) 100%)",
|
||||||
|
"linear-gradient(40deg, rgba(0, 141, 201, 0.76) 0%, rgba(8, 5, 170, 0.66) 100%)",
|
||||||
|
"linear-gradient(40deg, rgba(0, 201, 20, 0.76) 0%, rgba(4, 160, 105, 0.66) 100%)",
|
||||||
|
"linear-gradient(40deg, rgba(199, 20, 55, 0.76) 0%, rgba(95, 11, 160, 0.66) 100%)",
|
||||||
|
"linear-gradient(40deg, rgba(24, 20, 199, 0.76) 0%, rgba(23, 173, 65, 0.66) 100%)",
|
||||||
|
"radial-gradient(circle, rgba(20, 199, 178, 0.76) 32%, rgba(3, 120, 57, 0.66) 100%)",
|
||||||
|
"radial-gradient(circle, rgba(13, 15, 145, 0.76) 12%, rgba(103, 3, 120, 0.66) 100%)",
|
||||||
|
"linear-gradient(20deg, rgb(230, 21, 21) 0%, rgb(230, 109, 21) 12%, rgb(230, 34, 21) 26%, rgb(230, 21, 21) 39%, rgb(230, 84, 21) 48%, rgb(230, 34, 21) 58%, rgb(230, 96, 21) 69%, rgb(230, 34, 21) 80%, rgb(230, 71, 21) 89%, rgb(230, 21, 21) 100%)",
|
||||||
|
"rgba(114, 1, 170, 0.89)",
|
||||||
|
"rgba(93, 135, 63, 0.89)",
|
||||||
|
"rgba(4, 4, 138, 0.77)",
|
||||||
|
"rgba(21, 20, 20, 0.89)",
|
||||||
|
"linear-gradient(340deg, rgb(205, 74, 82) 18%, rgba(132, 8, 8, 0.89) 46%, rgb(204, 78, 85) 72%)",
|
||||||
|
"radial-gradient(circle, rgb(74, 205, 158) 0%, rgba(8, 72, 132, 0.89) 99%)",
|
||||||
|
"rgba(17, 94, 89, 1)",
|
||||||
|
"rgba(30, 64, 175, 0.89)",
|
||||||
|
"rgba(134, 25, 143, 1)",
|
||||||
|
"rgba(14, 165, 233, 0.9)",
|
||||||
|
]
|
||||||
|
|
||||||
|
interface PickerProps {
|
||||||
|
customOnChange?: (color: string) => void
|
||||||
|
customState?: string
|
||||||
|
savePresets?: boolean
|
||||||
|
}
|
||||||
|
|
||||||
|
export default function Picker({
|
||||||
|
customOnChange,
|
||||||
|
customState,
|
||||||
|
savePresets = true,
|
||||||
|
}: PickerProps) {
|
||||||
|
const [customThemeColor, setCustomThemeColor] = useState<string | null>()
|
||||||
|
const [presets, setPresets] = useState<string[]>()
|
||||||
|
|
||||||
|
const latestValuesRef = useRef({ customThemeColor, customOnChange, savePresets, presets });
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (customState !== undefined && customState !== null) {
|
||||||
|
setCustomThemeColor(customState)
|
||||||
|
} else {
|
||||||
|
setCustomThemeColor(settingsState.selectedColor ?? null)
|
||||||
|
}
|
||||||
|
|
||||||
|
if (presets === undefined) {
|
||||||
|
const savedPresets = localStorage.getItem("colorPickerPresets")
|
||||||
|
setPresets(savedPresets ? JSON.parse(savedPresets) : defaultPresets)
|
||||||
|
}
|
||||||
|
}, [])
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
latestValuesRef.current = { customThemeColor, customOnChange, savePresets, presets };
|
||||||
|
}, [customThemeColor, customOnChange, savePresets, presets]);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
return () => {
|
||||||
|
const { customThemeColor, customOnChange, savePresets, presets } = latestValuesRef.current;
|
||||||
|
if (!(customThemeColor && !customOnChange && savePresets && presets)) return;
|
||||||
|
|
||||||
|
// Only proceed if presets are different (avoid unnecessary updates)
|
||||||
|
const existingIndex = presets.indexOf(customThemeColor);
|
||||||
|
let updatedPresets;
|
||||||
|
|
||||||
|
if (existingIndex === 0) {
|
||||||
|
// No need to update if the selected color is already the first element
|
||||||
|
return;
|
||||||
|
} else if (existingIndex > -1) {
|
||||||
|
updatedPresets = [
|
||||||
|
customThemeColor,
|
||||||
|
...presets.slice(0, existingIndex),
|
||||||
|
...presets.slice(existingIndex + 1),
|
||||||
|
];
|
||||||
|
} else {
|
||||||
|
updatedPresets = [customThemeColor, ...presets].slice(0, 18);
|
||||||
|
}
|
||||||
|
|
||||||
|
localStorage.setItem("colorPickerPresets", JSON.stringify(updatedPresets));
|
||||||
|
}
|
||||||
|
}, [])
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (customThemeColor && !customOnChange) {
|
||||||
|
settingsState.selectedColor = customThemeColor
|
||||||
|
}
|
||||||
|
}, [customThemeColor, customOnChange])
|
||||||
|
|
||||||
|
return (
|
||||||
|
<ColorPicker
|
||||||
|
disableDarkMode={true}
|
||||||
|
presets={presets}
|
||||||
|
hideInputs={customOnChange ? false : true}
|
||||||
|
value={customThemeColor ?? ""}
|
||||||
|
onChange={(color: string) => {
|
||||||
|
if (customOnChange) {
|
||||||
|
customOnChange(color)
|
||||||
|
setCustomThemeColor(color)
|
||||||
|
} else {
|
||||||
|
setCustomThemeColor(color)
|
||||||
|
}
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
)
|
||||||
|
}
|
||||||
+2
-2
@@ -25,7 +25,7 @@ const targets: BuildTarget[] = [
|
|||||||
]
|
]
|
||||||
|
|
||||||
const mode = process.env.MODE || 'chrome'; // Check the environment variable to determine which build type to use.
|
const mode = process.env.MODE || 'chrome'; // Check the environment variable to determine which build type to use.
|
||||||
const sourcemap = (process.env.SOURCEMAP === "true") || false; // Check whether we want sourcemaps.
|
//const sourcemap = (process.env.SOURCEMAP === "true") || false; // Check whether we want sourcemaps.
|
||||||
|
|
||||||
export default defineConfig(({ command }) => ({
|
export default defineConfig(({ command }) => ({
|
||||||
plugins: [
|
plugins: [
|
||||||
@@ -74,7 +74,7 @@ export default defineConfig(({ command }) => ({
|
|||||||
outDir: resolve(__dirname, 'dist', mode),
|
outDir: resolve(__dirname, 'dist', mode),
|
||||||
emptyOutDir: false,
|
emptyOutDir: false,
|
||||||
minify: false,
|
minify: false,
|
||||||
sourcemap: sourcemap,
|
//sourcemap: sourcemap,
|
||||||
rollupOptions: {
|
rollupOptions: {
|
||||||
input: {
|
input: {
|
||||||
settings: join(__dirname, 'src', 'interface', 'index.html'),
|
settings: join(__dirname, 'src', 'interface', 'index.html'),
|
||||||
|
|||||||
Reference in New Issue
Block a user