mirror of
https://github.com/BetterSEQTA/BetterSEQTA-Plus.git
synced 2026-06-06 11:44:40 +00:00
feat(themeCreator): add svelte theme creator
This commit is contained in:
@@ -0,0 +1,4 @@
|
||||
<script lang="ts">
|
||||
</script>
|
||||
|
||||
<div>Code Editor Here</div>
|
||||
@@ -1,8 +1,8 @@
|
||||
.dark {
|
||||
div:has(> #rbgcp-wrapper) {
|
||||
background: transparent !important;
|
||||
}
|
||||
div:has(> #rbgcp-wrapper) {
|
||||
background: transparent !important;
|
||||
}
|
||||
|
||||
.dark {
|
||||
#rbgcp-wrapper {
|
||||
div[style="padding-top: 11px; position: relative;"] div {
|
||||
color: white !important;
|
||||
@@ -31,11 +31,14 @@
|
||||
#rbgcp-radial-btn,
|
||||
#rbgcp-linear-btn {
|
||||
&[style*="background: white;"] {
|
||||
background-color: #28282B !important;
|
||||
background-color: #28282b !important;
|
||||
}
|
||||
|
||||
svg {
|
||||
path, g, polyline, circle {
|
||||
path,
|
||||
g,
|
||||
polyline,
|
||||
circle {
|
||||
stroke: white !important;
|
||||
fill: transparent !important;
|
||||
}
|
||||
@@ -80,4 +83,4 @@
|
||||
background-color: #37373b !important;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -5,14 +5,17 @@
|
||||
import { animate, spring } from 'motion';
|
||||
import { delay } from '@/seqta/utils/delay.ts'
|
||||
|
||||
const { hidePicker } = $props<{
|
||||
hidePicker: () => void
|
||||
const { hidePicker, standalone = false } = $props<{
|
||||
hidePicker?: () => void,
|
||||
standalone?: boolean
|
||||
}>();
|
||||
|
||||
let background: HTMLDivElement;
|
||||
let content: HTMLDivElement;
|
||||
|
||||
const closePicker = async () => {
|
||||
if (standalone) return;
|
||||
|
||||
animate(
|
||||
content,
|
||||
{ scale: [1, 0.4], opacity: [1, 0] },
|
||||
@@ -25,29 +28,36 @@
|
||||
{ easing: [0.4, 0, 0.2, 1] }
|
||||
);
|
||||
|
||||
|
||||
await delay(400);
|
||||
hidePicker();
|
||||
}
|
||||
|
||||
|
||||
onMount(() => {
|
||||
if (standalone) return;
|
||||
|
||||
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) => {
|
||||
const handleEscapeKey = (e: KeyboardEvent) => {
|
||||
if (e.key === 'Escape') {
|
||||
closePicker();
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
document.addEventListener('keydown', handleEscapeKey);
|
||||
|
||||
return () => {
|
||||
document.removeEventListener('keydown', handleEscapeKey);
|
||||
};
|
||||
});
|
||||
|
||||
function handleBackgroundClick(event: MouseEvent) {
|
||||
@@ -57,17 +67,23 @@
|
||||
}
|
||||
</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 cursor-pointer 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 cursor-auto rounded-xl dark:bg-zinc-800 border-zinc-100 dark:border-zinc-700"
|
||||
>
|
||||
{#if standalone}
|
||||
<div class="h-auto rounded-xl overflow-clip">
|
||||
<ReactAdapter el={ColourPicker} />
|
||||
</div>
|
||||
</div>
|
||||
{:else}
|
||||
<!-- 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 cursor-pointer 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 cursor-auto rounded-xl dark:bg-zinc-800 border-zinc-100 dark:border-zinc-700"
|
||||
>
|
||||
<ReactAdapter el={ColourPicker} />
|
||||
</div>
|
||||
</div>
|
||||
{/if}
|
||||
|
||||
@@ -0,0 +1,3 @@
|
||||
<script lang="ts"></script>
|
||||
|
||||
<div class='w-full h-0.5 my-4 bg-zinc-200 dark:bg-zinc-700'></div>
|
||||
@@ -1,23 +1,24 @@
|
||||
import styles from './index.css?inline';
|
||||
import { mount } from 'svelte';
|
||||
import type { ComponentType } from 'svelte';
|
||||
import styles from "./index.css?inline"
|
||||
import { mount } from "svelte"
|
||||
import type { ComponentType } from "svelte"
|
||||
|
||||
export default function renderSvelte(
|
||||
Component: ComponentType | any,
|
||||
mountPoint: ShadowRoot | HTMLElement,
|
||||
props: Record<string, any> = {}
|
||||
) {
|
||||
props: Record<string, any> = {},
|
||||
) {
|
||||
const app = mount(Component, {
|
||||
target: mountPoint,
|
||||
props: {
|
||||
standalone: false,
|
||||
...props
|
||||
}
|
||||
});
|
||||
...props,
|
||||
},
|
||||
})
|
||||
|
||||
const style = document.createElement("style");
|
||||
style.setAttribute("type", "text/css");
|
||||
style.innerHTML = styles;
|
||||
mountPoint.appendChild(style);
|
||||
return app;
|
||||
}
|
||||
const style = document.createElement("style")
|
||||
style.setAttribute("type", "text/css")
|
||||
style.innerHTML = styles
|
||||
mountPoint.appendChild(style)
|
||||
|
||||
return app
|
||||
}
|
||||
|
||||
@@ -0,0 +1,160 @@
|
||||
<script lang="ts">
|
||||
import { v4 as uuidv4 } from 'uuid';
|
||||
import { onMount } from 'svelte';
|
||||
|
||||
import type { CustomTheme } from '@/types/CustomThemes'
|
||||
|
||||
import { settingsState } from '@/seqta/utils/listeners/SettingsState'
|
||||
import { getTheme } from '@/seqta/ui/themes/getTheme'
|
||||
|
||||
import Divider from '@/svelte-interface/components/themeCreator/divider.svelte'
|
||||
import Switch from '@/svelte-interface/components/Switch.svelte'
|
||||
import Button from '@/svelte-interface/components/Button.svelte'
|
||||
import Slider from '@/svelte-interface/components/Slider.svelte'
|
||||
import ColourPicker from '../components/ColourPicker.svelte'
|
||||
import CodeEditor from '../components/CodeEditor.svelte'
|
||||
|
||||
|
||||
const { themeID } = $props<{ themeID: string }>()
|
||||
let theme = $state<CustomTheme>({
|
||||
id: uuidv4(),
|
||||
name: '',
|
||||
description: '',
|
||||
defaultColour: 'blue',
|
||||
CanChangeColour: true,
|
||||
allowBackgrounds: true,
|
||||
CustomCSS: '',
|
||||
CustomImages: [],
|
||||
coverImage: null,
|
||||
isEditable: true,
|
||||
hideThemeName: false,
|
||||
forceDark: false
|
||||
})
|
||||
|
||||
onMount(async () => {
|
||||
console.log(themeID)
|
||||
|
||||
if (themeID) {
|
||||
const tempTheme = await getTheme(themeID)
|
||||
if (tempTheme) theme = tempTheme
|
||||
}
|
||||
});
|
||||
|
||||
type SettingType = 'switch' | 'button' | 'slider' | 'colourPicker' | 'select' | 'codeEditor';
|
||||
|
||||
type SwitchProps = { state: boolean; onChange: (value: boolean) => void };
|
||||
type ButtonProps = { onClick: () => void; text: string };
|
||||
type SliderProps = { state: number; onChange: (value: number) => void; min?: number; max?: number };
|
||||
type ColourPickerProps = { color: string; onChange: (color: string) => void };
|
||||
type SelectProps = { options: Array<{ value: string; label: string }>; value: string; onChange: (value: string) => void };
|
||||
type CodeEditorProps = { value: string; onChange: (value: string) => void };
|
||||
|
||||
type ComponentProps = SwitchProps | ButtonProps | SliderProps | ColourPickerProps | SelectProps | CodeEditorProps;
|
||||
|
||||
type SettingItem = {
|
||||
type: SettingType;
|
||||
title: string;
|
||||
description: string;
|
||||
direction?: 'horizontal' | 'vertical';
|
||||
props: ComponentProps;
|
||||
};
|
||||
</script>
|
||||
|
||||
{#snippet settingItem(item: SettingItem)}
|
||||
<div class="flex justify-between {item.direction === 'vertical' ? 'flex-col items-start gap-2' : 'items-center'} py-3">
|
||||
<div class="pr-4">
|
||||
<h2 class="text-sm font-bold">{item.title}</h2>
|
||||
<p class="text-xs">{item.description}</p>
|
||||
</div>
|
||||
<div>
|
||||
{#if item.type === 'switch'}
|
||||
<Switch {...(item.props as SwitchProps)} />
|
||||
{:else if item.type === 'button'}
|
||||
<Button {...(item.props as ButtonProps)} />
|
||||
{:else if item.type === 'slider'}
|
||||
<Slider {...(item.props as SliderProps)} />
|
||||
{:else if item.type === 'colourPicker'}
|
||||
<ColourPicker standalone={true} {...(item.props)} />
|
||||
{:else if item.type === 'codeEditor'}
|
||||
<CodeEditor {...(item.props as CodeEditorProps)} />
|
||||
{/if}
|
||||
</div>
|
||||
</div>
|
||||
{/snippet}
|
||||
|
||||
<div class='h-screen overflow-y-scroll {$settingsState.DarkMode && "dark"} '>
|
||||
<div class='w-full min-h-screen bg-zinc-100 dark:bg-zinc-800 flex flex-col p-2 dark:text-white'>
|
||||
<h1 class='text-xl font-semibold'>Theme Creator</h1>
|
||||
<a href='https://betterseqta.gitbook.io/betterseqta-docs' target='_blank' class='text-sm font-light text-zinc-500 dark:text-zinc-400'>
|
||||
<span class='no-underline font-IconFamily pr-0.5'>{'\ueb44'}</span>
|
||||
<span class='underline'>
|
||||
Need help? Check out the docs!
|
||||
</span>
|
||||
</a>
|
||||
|
||||
<Divider />
|
||||
|
||||
<div>
|
||||
<div class='pb-2 text-sm'>Theme Name</div>
|
||||
<input
|
||||
id='themeName'
|
||||
type='text'
|
||||
placeholder='What is your theme called?'
|
||||
bind:value={theme.name}
|
||||
class='w-full p-2 mb-4 transition-all duration-300 rounded-lg focus:outline-none ring-0 focus:ring-1 ring-zinc-100 dark:ring-zinc-700 dark:bg-zinc-900 dark:text-white' />
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<div class='pb-2 text-sm'>Description <span class='italic font-light opacity-80'>(optional)</span></div>
|
||||
<textarea
|
||||
id='themeDescription'
|
||||
placeholder="Don't worry, this one's optional!"
|
||||
bind:value={theme.description}
|
||||
class='w-full p-2 rounded-lg focus:outline-none ring-0 focus:ring-1 ring-zinc-100 dark:ring-zinc-700 dark:bg-zinc-900 dark:text-white'></textarea>
|
||||
</div>
|
||||
|
||||
<Divider />
|
||||
|
||||
{#each [
|
||||
{
|
||||
type: 'switch',
|
||||
title: 'Hide Theme Name',
|
||||
description: 'Useful when your cover image contains text',
|
||||
props: {
|
||||
state: theme.hideThemeName,
|
||||
onChange: (value) => theme.hideThemeName = value
|
||||
}
|
||||
},
|
||||
{
|
||||
type: 'switch',
|
||||
title: 'Force Theme',
|
||||
description: 'Force users to use either dark or light mode',
|
||||
props: {
|
||||
state: theme.forceDark !== undefined,
|
||||
onChange: (value) => theme.forceDark = value ? false : undefined
|
||||
}
|
||||
},
|
||||
{
|
||||
type: 'colourPicker',
|
||||
title: 'Default Theme Colour',
|
||||
description: 'Set the default color for your theme',
|
||||
direction: 'vertical',
|
||||
props: {
|
||||
color: theme.defaultColour,
|
||||
onChange: (color) => theme.defaultColour = color
|
||||
}
|
||||
},
|
||||
{
|
||||
type: 'codeEditor',
|
||||
title: 'Custom CSS',
|
||||
description: 'Add custom CSS to your theme',
|
||||
props: {
|
||||
value: theme.CustomCSS,
|
||||
onChange: (value) => theme.CustomCSS = value
|
||||
}
|
||||
}
|
||||
] as SettingItem[] as setting}
|
||||
{@render settingItem(setting)}
|
||||
{/each}
|
||||
</div>
|
||||
</div>
|
||||
@@ -1,14 +1,7 @@
|
||||
import type { SettingsState } from './AppProps';
|
||||
export interface SettingsList {
|
||||
title: string;
|
||||
id: number;
|
||||
description: string;
|
||||
Component: any; /* TODO: Give this a type */
|
||||
props?: any;
|
||||
}
|
||||
|
||||
|
||||
export interface SettingsProps {
|
||||
settingsState: SettingsState;
|
||||
setSettingsState: React.Dispatch<React.SetStateAction<SettingsState>>;
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user