mirror of
https://github.com/BetterSEQTA/BetterSEQTA-Plus.git
synced 2026-06-06 11:44:40 +00:00
chore(ui): add react gradient colour picker
This commit is contained in:
@@ -0,0 +1,73 @@
|
||||
<script lang="ts">
|
||||
import { onMount } from 'svelte'
|
||||
import ColourPicker from './ColourPicker.tsx';
|
||||
import ReactAdapter from './utils/ReactAdapter.svelte';
|
||||
import { animate, spring } from 'motion';
|
||||
import { delay } from '@/seqta/utils/delay.ts'
|
||||
|
||||
const { hidePicker } = $props<{
|
||||
hidePicker: () => void
|
||||
}>();
|
||||
|
||||
let background: HTMLDivElement;
|
||||
let content: HTMLDivElement;
|
||||
|
||||
const closePicker = async () => {
|
||||
// reverse animation
|
||||
animate(
|
||||
content,
|
||||
{ scale: [1, 0.4], opacity: [1, 0] },
|
||||
{ easing: spring({ stiffness: 400, damping: 30 }) }
|
||||
);
|
||||
|
||||
animate(
|
||||
background,
|
||||
{ opacity: [1, 0] },
|
||||
{ easing: [0.4, 0, 0.2, 1] }
|
||||
);
|
||||
|
||||
await delay(400);
|
||||
hidePicker();
|
||||
}
|
||||
|
||||
onMount(() => {
|
||||
animate(
|
||||
background,
|
||||
{ opacity: [0, 1] },
|
||||
{ duration: 0.3, easing: [0.4, 0, 0.2, 1] }
|
||||
);
|
||||
|
||||
animate(
|
||||
content,
|
||||
{ scale: [0.4, 1], opacity: [0, 1] },
|
||||
{ easing: spring({ stiffness: 400, damping: 30 }) }
|
||||
);
|
||||
|
||||
document.addEventListener('keydown', (e) => {
|
||||
if (e.key === 'Escape') {
|
||||
closePicker();
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
function handleBackgroundClick(event: MouseEvent) {
|
||||
if (event.target === background) {
|
||||
closePicker();
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<!-- svelte-ignore a11y_no_static_element_interactions -->
|
||||
<div
|
||||
bind:this={background}
|
||||
class="absolute top-0 left-0 z-50 flex items-center justify-center w-full h-full bg-black/20"
|
||||
onclick={handleBackgroundClick}
|
||||
onkeydown={(e) => { e.key === 'Enter' && handleBackgroundClick }}
|
||||
>
|
||||
<div
|
||||
bind:this={content}
|
||||
class="h-auto p-4 bg-white border shadow-lg rounded-xl dark:bg-zinc-800 border-zinc-100 dark:border-zinc-700"
|
||||
>
|
||||
<ReactAdapter el={ColourPicker} />
|
||||
</div>
|
||||
</div>
|
||||
@@ -0,0 +1,70 @@
|
||||
import ColorPicker from 'react-best-gradient-color-picker';
|
||||
import { useEffect, useState } from 'react';
|
||||
import { settingsState } from '@/seqta/utils/listeners/SettingsState';
|
||||
|
||||
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)'
|
||||
];
|
||||
|
||||
function Picker() {
|
||||
const [customThemeColor, setCustomThemeColor] = useState(() => {
|
||||
return settingsState.selectedColor
|
||||
});
|
||||
const [presets, setPresets] = useState(() => {
|
||||
const savedPresets = localStorage.getItem('colorPickerPresets');
|
||||
return savedPresets ? JSON.parse(savedPresets) : defaultPresets;
|
||||
});
|
||||
|
||||
|
||||
useEffect(() => {
|
||||
// on component dismount, save the presets to local storage
|
||||
return () => {
|
||||
// Check if the selected color is already in the presets
|
||||
const existingIndex = presets.indexOf(customThemeColor);
|
||||
|
||||
let updatedPresets;
|
||||
if (existingIndex > -1) {
|
||||
// If the color exists, move it to the front
|
||||
updatedPresets = [
|
||||
customThemeColor,
|
||||
...presets.slice(0, existingIndex),
|
||||
...presets.slice(existingIndex + 1)
|
||||
];
|
||||
} else {
|
||||
// If the color is new, add it to the front and slice the array
|
||||
updatedPresets = [customThemeColor, ...presets].slice(0, 18);
|
||||
}
|
||||
|
||||
setPresets(updatedPresets);
|
||||
localStorage.setItem('colorPickerPresets', JSON.stringify(updatedPresets));
|
||||
}
|
||||
}, []);
|
||||
|
||||
useEffect(() => {
|
||||
settingsState.selectedColor = customThemeColor;
|
||||
}, [customThemeColor]);
|
||||
|
||||
return (
|
||||
<ColorPicker disableDarkMode={true} presets={presets} hideInputs={true} value={customThemeColor} onChange={setCustomThemeColor} />
|
||||
);
|
||||
}
|
||||
|
||||
export default Picker;
|
||||
@@ -1,129 +1,11 @@
|
||||
<!-- <script lang="ts">
|
||||
import { onMount } from 'svelte';
|
||||
import iro from '@jaames/iro';
|
||||
<script lang="ts">
|
||||
import { settingsState } from '@/seqta/utils/listeners/SettingsState'
|
||||
|
||||
type GradientStop = { color: string; position: number };
|
||||
|
||||
let ColorPicker: iro.ColorPicker;
|
||||
let gradientStops: GradientStop[] = [
|
||||
{ color: '#ff0000', position: 0 },
|
||||
{ color: '#00ff00', position: 0.5 },
|
||||
{ color: '#0000ff', position: 1 },
|
||||
];
|
||||
let currentStop = 0;
|
||||
let draggingStop = -1;
|
||||
let initialDragPosition = 0;
|
||||
|
||||
onMount(() => {
|
||||
ColorPicker = new (iro.ColorPicker as any)('#picker', {
|
||||
width: 320,
|
||||
color: gradientStops[0].color,
|
||||
layout: [
|
||||
{
|
||||
component: iro.ui.Box,
|
||||
},
|
||||
{
|
||||
component: iro.ui.Slider,
|
||||
options: {
|
||||
id: 'hue-slider',
|
||||
sliderType: 'hue',
|
||||
},
|
||||
},
|
||||
{
|
||||
component: iro.ui.Slider,
|
||||
options: {
|
||||
id: 'alpha-slider',
|
||||
sliderType: 'alpha',
|
||||
},
|
||||
},
|
||||
],
|
||||
});
|
||||
|
||||
ColorPicker.on('color:change', () => {
|
||||
gradientStops[currentStop].color = ColorPicker.color.rgbaString;
|
||||
});
|
||||
|
||||
console.log(ColorPicker.color.rgba);
|
||||
});
|
||||
|
||||
function handleDragStart(event: PointerEvent, index: number) {
|
||||
if (draggingStop !== -1) {
|
||||
// Prevent starting a new drag if one is already in progress.
|
||||
event.preventDefault(); // This stops the pointerdown event from taking any effect.
|
||||
return;
|
||||
}
|
||||
|
||||
draggingStop = index; // Mark this stop as being dragged.
|
||||
initialDragPosition = event.clientX;
|
||||
}
|
||||
|
||||
function handleDragMove(event: PointerEvent) {
|
||||
if (draggingStop === -1) return;
|
||||
|
||||
const container = event.currentTarget as HTMLDivElement;
|
||||
const stopWidth = container.offsetWidth;
|
||||
const containerOffset = container.getBoundingClientRect().left;
|
||||
const relativePosition = (event.clientX - containerOffset) / stopWidth;
|
||||
|
||||
const sortedStops = [...gradientStops];
|
||||
sortedStops.sort((a, b) => a.position - b.position);
|
||||
|
||||
const prevStopIndex = sortedStops.findIndex(
|
||||
(stop, index) => index < draggingStop && stop.position < relativePosition
|
||||
);
|
||||
const nextStopIndex = sortedStops.findIndex(
|
||||
(stop, index) => index > draggingStop && stop.position > relativePosition
|
||||
);
|
||||
|
||||
const prevStop = prevStopIndex >= 0 ? sortedStops[prevStopIndex] : { position: 0 };
|
||||
const nextStop = nextStopIndex >= 0 ? sortedStops[nextStopIndex] : { position: 1 };
|
||||
|
||||
const clampedPosition = Math.max(prevStop.position, Math.min(nextStop.position, relativePosition));
|
||||
|
||||
const newGradientStops = gradientStops.slice();
|
||||
newGradientStops.sort((a, b) => a.position - b.position);
|
||||
|
||||
const draggedStop = newGradientStops[draggingStop];
|
||||
newGradientStops.splice(draggingStop, 1);
|
||||
|
||||
const insertIndex = newGradientStops.findIndex(stop => stop.position >= clampedPosition);
|
||||
newGradientStops.splice(insertIndex, 0, { ...draggedStop, position: clampedPosition });
|
||||
|
||||
gradientStops = newGradientStops;
|
||||
}
|
||||
|
||||
function handleDragEnd() {
|
||||
draggingStop = -1;
|
||||
}
|
||||
let { onClick } = $props<{ onClick: () => void }>();
|
||||
</script>
|
||||
|
||||
<div
|
||||
class="w-16 h-8 rounded-md swatch"
|
||||
style="background: linear-gradient(to right, {gradientStops
|
||||
.map(({ color, position }) => `${color} ${position * 100}%`)
|
||||
.join(', ')});"
|
||||
></div>
|
||||
<div class="fixed top-0 left-0 z-20 flex flex-col w-48 h-32 gap-8">
|
||||
<div id="picker"></div>
|
||||
|
||||
<div
|
||||
class="w-[320px] h-4 relative"
|
||||
style={`background: linear-gradient(to right, ${gradientStops
|
||||
.map(({ color, position }) => `${color} ${position * 100}%`)
|
||||
.join(', ')});`}
|
||||
on:pointermove={handleDragMove}
|
||||
on:pointerup={handleDragEnd}
|
||||
>
|
||||
<span class="opacity-0">This makes the gradient show up</span>
|
||||
{#each gradientStops as { position }, index}
|
||||
<button
|
||||
class="absolute w-4 h-4 bg-white rounded-md top-1/2"
|
||||
style={`left: ${position * 100}%; transform: translate(-50%, -50%);`}
|
||||
on:click={() => (currentStop = index)}
|
||||
on:pointerdown={(event) => handleDragStart(event, index)}
|
||||
></button>
|
||||
{/each}
|
||||
</div>
|
||||
</div> -->
|
||||
|
||||
<script></script>
|
||||
<button
|
||||
onclick={onClick}
|
||||
style="background: {$settingsState.selectedColor}"
|
||||
class="w-16 h-8 rounded-md"
|
||||
></button>
|
||||
@@ -3,7 +3,7 @@
|
||||
import './TabbedContainer.css';
|
||||
import { onMount } from 'svelte';
|
||||
|
||||
let { tabs } = $props<{ tabs: { title: string, Content: any }[] }>();
|
||||
let { tabs } = $props<{ tabs: { title: string, Content: any, props?: any }[] }>();
|
||||
let activeTab = $state(0);
|
||||
let hoveredTab = $state<number | null>(null);
|
||||
let containerRef: HTMLElement | null = null;
|
||||
@@ -71,10 +71,10 @@
|
||||
transition={springTransition}
|
||||
>
|
||||
<div class="flex">
|
||||
{#each tabs as { Content }, index}
|
||||
{#each tabs as { Content, props }, index}
|
||||
<div class="absolute w-full h-full transition-opacity duration-300 overflow-y-scroll tab {activeTab === index ? 'opacity-100 active' : 'opacity-0'}"
|
||||
style="left: {index * 100}%;">
|
||||
{@render Content()}
|
||||
<Content {...props} />
|
||||
</div>
|
||||
{/each}
|
||||
</div>
|
||||
|
||||
@@ -0,0 +1,27 @@
|
||||
<script lang="ts">
|
||||
import React from "react";
|
||||
import ReactDOM from "react-dom";
|
||||
import { onDestroy, onMount } from "svelte";
|
||||
|
||||
const e = React.createElement;
|
||||
let container: HTMLDivElement;
|
||||
|
||||
onMount(() => {
|
||||
const { el, children, class: _, ...props } = $$props;
|
||||
try {
|
||||
ReactDOM.render(e(el, props, children), container);
|
||||
} catch (err) {
|
||||
console.warn(`react-adapter failed to mount.`, { err });
|
||||
}
|
||||
});
|
||||
|
||||
onDestroy(() => {
|
||||
try {
|
||||
ReactDOM.unmountComponentAtNode(container);
|
||||
} catch (err) {
|
||||
console.warn(`react-adapter failed to unmount.`, { err });
|
||||
}
|
||||
});
|
||||
</script>
|
||||
|
||||
<div bind:this={container} class={$$props.class}></div>
|
||||
@@ -10,6 +10,12 @@
|
||||
import { settingsState } from '@/seqta/utils/listeners/SettingsState'
|
||||
|
||||
import { closeSettings, OpenAboutPage, OpenWhatsNewPopup } from "@/SEQTA"
|
||||
import ColourPicker from '../components/ColourPicker.svelte'
|
||||
|
||||
|
||||
const openColourPicker = () => {
|
||||
showColourPicker = true;
|
||||
}
|
||||
|
||||
const openChangelog = () => {
|
||||
OpenWhatsNewPopup();
|
||||
@@ -22,6 +28,7 @@
|
||||
};
|
||||
|
||||
let { standalone = false } = $props<{ standalone?: boolean }>();
|
||||
let showColourPicker = $state<boolean>(false);
|
||||
|
||||
onMount(() => {
|
||||
if (!standalone) return;
|
||||
@@ -38,12 +45,17 @@
|
||||
<img src={browser.runtime.getURL('resources/icons/betterseqta-light-full.png')} class="hidden w-4/5 dark:block" alt="Dark logo" />
|
||||
<button onclick={openChangelog} class="absolute right-0 w-8 h-8 text-lg rounded-xl font-IconFamily top-1 bg-zinc-100 dark:bg-zinc-700"></button>
|
||||
<button onclick={openAbout} class="absolute w-8 h-8 text-lg rounded-xl font-IconFamily top-1 right-10 bg-zinc-100 dark:bg-zinc-700">ⓘ</button>
|
||||
<!-- <button onclick={() => showColourPicker = true} class="absolute w-8 h-8 text-lg rounded-xl font-IconFamily top-1 right-10 bg-zinc-100 dark:bg-zinc-700">ⓘ</button> -->
|
||||
</div>
|
||||
|
||||
<TabbedContainer tabs={[
|
||||
{ title: 'Settings', Content: Settings },
|
||||
{ title: 'Settings', Content: Settings, props: { showColourPicker: openColourPicker } },
|
||||
{ title: 'Shortcuts', Content: Shortcuts },
|
||||
{ title: 'Themes', Content: Theme },
|
||||
]} />
|
||||
</div>
|
||||
|
||||
{#if showColourPicker}
|
||||
<ColourPicker hidePicker={() => { showColourPicker = false }} />
|
||||
{/if}
|
||||
</div>
|
||||
@@ -1,13 +1,15 @@
|
||||
<script lang="ts">
|
||||
import Switch from "../../components/Switch.svelte"
|
||||
import Button from "../../components/Button.svelte"
|
||||
import PickerSwatch from "../../components/PickerSwatch.svelte"
|
||||
import Slider from "../../components/Slider.svelte"
|
||||
|
||||
import browser from "webextension-polyfill"
|
||||
|
||||
import type { SettingsList } from "@/svelte-interface/types/SettingsProps"
|
||||
import { settingsState } from "@/seqta/utils/listeners/SettingsState.ts"
|
||||
import PickerSwatch from "@/svelte-interface/components/PickerSwatch.svelte"
|
||||
|
||||
const { showColourPicker } = $props<{ showColourPicker: () => void }>();
|
||||
</script>
|
||||
|
||||
{#snippet Setting({ title, description, Component, props }: SettingsList) }
|
||||
@@ -54,12 +56,15 @@
|
||||
onChange: (value: number) => settingsState.bksliderinput = `${value}`
|
||||
}
|
||||
},
|
||||
/* {
|
||||
{
|
||||
title: "Custom Theme Colour",
|
||||
description: "Customise the overall theme colour of SEQTA Learn.",
|
||||
id: 4,
|
||||
Component: PickerSwatch
|
||||
}, */
|
||||
Component: PickerSwatch,
|
||||
props: {
|
||||
onClick: showColourPicker
|
||||
}
|
||||
},
|
||||
{
|
||||
title: "Edit Sidebar Layout",
|
||||
description: "Customise the sidebar layout.",
|
||||
|
||||
Reference in New Issue
Block a user