diff --git a/package.json b/package.json index 035beb48..abcfdadb 100644 --- a/package.json +++ b/package.json @@ -38,7 +38,6 @@ "@bedframe/cli": "^0.0.91", "@crxjs/vite-plugin": "2.0.0-beta.25", "@types/mime-types": "^2.1.4", - "@vitejs/plugin-react-swc": "^3.8.0", "cross-env": "^7.0.3", "dependency-cruiser": "^16.10.0", "eslint": "9.22.0", @@ -61,6 +60,7 @@ "@codemirror/search": "^6.5.10", "@codemirror/state": "^6.5.2", "@codemirror/view": "^6.36.4", + "@jaames/iro": "^5.5.2", "@sveltejs/vite-plugin-svelte": "^5.0.3", "@tailwindcss/forms": "^0.5.9", "@tailwindcss/vite": "^4.0.12", @@ -74,7 +74,6 @@ "@types/webextension-polyfill": "^0.12.3", "@uiw/codemirror-extensions-color": "^4.23.10", "@uiw/codemirror-theme-github": "^4.23.10", - "@vitejs/plugin-react": "^4.3.4", "autoprefixer": "^10.4.21", "codemirror": "^6.0.1", "color": "^5.0.0", diff --git a/src/interface/components/ColourPicker.svelte b/src/interface/components/ColourPicker.svelte index eb77d008..4095fc8e 100644 --- a/src/interface/components/ColourPicker.svelte +++ b/src/interface/components/ColourPicker.svelte @@ -3,19 +3,91 @@ import { animate } from 'motion'; import { delay } from '@/seqta/utils/delay.ts' import { settingsState } from '@/seqta/utils/listeners/SettingsState.ts' + import iro from '@jaames/iro' - const { hidePicker, standalone = false, savePresets = true, customOnChange = null, customState = null } = $props<{ + interface GradientStop { + color: string; + offset: number; + alpha: number; + } + + const { hidePicker, standalone = false, customOnChange = null, customState = null } = $props<{ hidePicker?: () => void, standalone?: boolean, - savePresets?: boolean, customOnChange?: (color: string) => void, customState?: string }>(); let background = $state(null); let content = $state(null); + let pickerContainer = $state(null); + let gradientBarElement = $state(null); + let colorPicker: iro.ColorPicker | null = null; - let colour = $state(null); + let currentMode = $state('solid'); + let currentColor = $state(getInitialColor()); + let gradientAngle = $state(90); + let gradientStops = $state(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 () => { if (standalone) return; @@ -38,40 +110,79 @@ ); await delay(400); - hidePicker(); + hidePicker?.(); } onMount(() => { - if (standalone) return; - if (!background || !content) return; + if (!pickerContainer) return; - animate( - background, - { opacity: [0, 1] }, - { duration: 0.3, ease: [0.4, 0, 0.2, 1] } - ); + const picker = new (iro as any).ColorPicker(pickerContainer, { + width: 250, + color: currentColor, + layout: [ + { + component: iro.ui.Box, + options: {} + }, + { + component: iro.ui.Slider, + options: { + sliderType: 'hue' + } + }, + { + component: iro.ui.Slider, + options: { + sliderType: 'alpha' + } + } + ] + }); - animate( - content, - { scale: [0.4, 1], opacity: [0, 1] }, - { - type: 'spring', - stiffness: 400, - damping: 30 + colorPicker = picker; + picker.on('color:change', (color: any) => { + if (currentMode === 'solid') { + currentColor = color.hexString; + updateColor(); + } else { + gradientStops[selectedStopIndex].color = color.hexString; + gradientStops[selectedStopIndex].alpha = color.alpha; + gradientStops = [...gradientStops]; + updateColor(); } - ); + }); - const handleEscapeKey = (e: KeyboardEvent) => { - if (e.key === 'Escape') { - closePicker(); - } - }; + if (!standalone) { + if (!background || !content) return; - document.addEventListener('keydown', handleEscapeKey); + animate( + background, + { opacity: [0, 1] }, + { duration: 0.3, ease: [0.4, 0, 0.2, 1] } + ); - return () => { - document.removeEventListener('keydown', handleEscapeKey); - }; + animate( + content, + { 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) { @@ -80,28 +191,356 @@ } } - const changeColour = async () => { - settingsState.selectedColor = colour.value; + 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(', ')}) + `); {#if standalone} -
- changeColour()} /> +
+
+ + +
+ +
+ + {#if currentMode === 'gradient'} +
+ +
+ +
+ + + {#each gradientStops as stop, i} + + {/each} +
+ + +
+
+ + + {gradientAngle}° +
+ + {#if gradientStops.length > 2} + + {/if} +
+
+ {/if}
{:else} - +
{ e.key === 'Enter' && handleBackgroundClick }} + role="button" + aria-label="Close color picker" + tabindex="0" >
- changeColour()} /> + +
+ + +
+ +
+ + {#if currentMode === 'gradient'} +
+ +
+ +
+ + + {#each gradientStops as stop, i} + + {/each} +
+ + +
+
+ + + {gradientAngle}° +
+ + {#if gradientStops.length > 2} + + {/if} +
+
+ {/if}
{/if} diff --git a/src/plugins/monofile.ts b/src/plugins/monofile.ts index a1a59716..d4cc659c 100644 --- a/src/plugins/monofile.ts +++ b/src/plugins/monofile.ts @@ -97,7 +97,7 @@ export async function init() { // TEMP FIX for bug! -> this is a hack to get the injected.css file to have HMR in development mode as this import system is currently broken with crxjs if (import.meta.env.MODE === "development") { - import("./css/injected.scss") + import("../css/injected.scss") } else { const injectedStyle = document.createElement("style") injectedStyle.textContent = injectedCSS diff --git a/vite.config.ts b/vite.config.ts index 4f80b022..0e408fcf 100644 --- a/vite.config.ts +++ b/vite.config.ts @@ -6,7 +6,6 @@ import { base64Loader } from './lib/base64loader'; import type { BuildTarget } from './lib/types'; import ClosePlugin from './lib/closePlugin'; -import react from '@vitejs/plugin-react'; import million from "million/compiler"; //import MillionLint from '@million/lint'; @@ -31,7 +30,6 @@ const sourcemap = (process.env.SOURCEMAP === "true") || false; // Check whether export default defineConfig(({ command }) => ({ plugins: [ base64Loader, - react(), tailwindcss(), svelte({ emitCss: false