feat(themes): polish theme editor, updates, and card motion

- Lazy-load the theme editor, workshop, diagnostics, logs, and presence editor.
- Check installed themes for newer workshop versions and support updates with backup/restore.
- Preserve accordion state for the current editor session and add section icons.
- Animate installed and workshop cards on insertion, and fix the duplicate presence idle action.
This commit is contained in:
HorizonCode committed 2026-11-04 09:54:58 +01:00
1 parent 51f5fa3e46
commit d56ed9d786
16 files changed
+720 -434

No files matched your search

+3 -2
View File
@@ -951,11 +951,12 @@ export async function reviewCommunityTheme(
*/ */
export async function downloadAndInstallCommunityTheme( export async function downloadAndInstallCommunityTheme(
theme: CommunityTheme, theme: CommunityTheme,
onProgress?: (status: string, percentage: number) => void onProgress?: (status: string, percentage: number) => void,
options?: { folderName?: string }
): Promise<{ success: boolean; folderName: string }> { ): Promise<{ success: boolean; folderName: string }> {
const { onProgress: reportProgress = () => {} } = { onProgress }; const { onProgress: reportProgress = () => {} } = { onProgress };
const baseThemesDir = await themesDirectory(); const baseThemesDir = await themesDirectory();
const folderName = theme.id; const folderName = options?.folderName?.trim() || theme.id;
const targetFolder = await path.join(baseThemesDir, folderName); const targetFolder = await path.join(baseThemesDir, folderName);
reportProgress('Downloading theme package...', 10); reportProgress('Downloading theme package...', 10);
@@ -51,6 +51,7 @@
import { normalizeThemeTags } from '$lib/themeV2'; import { normalizeThemeTags } from '$lib/themeV2';
import { reloadThemes, updateInstalledThemeTags, type Theme } from '$lib/themes'; import { reloadThemes, updateInstalledThemeTags, type Theme } from '$lib/themes';
import { currentUser } from '$lib/userAuthentication'; import { currentUser } from '$lib/userAuthentication';
import { reduceAnimations } from '$lib/userSettings';
import WorkshopTooltip from './WorkshopTooltip.svelte'; import WorkshopTooltip from './WorkshopTooltip.svelte';
import { import {
Archive, Archive,
@@ -87,6 +88,7 @@
import { sileo } from 'sileo'; import { sileo } from 'sileo';
import { onDestroy, onMount } from 'svelte'; import { onDestroy, onMount } from 'svelte';
import { SvelteMap, SvelteSet } from 'svelte/reactivity'; import { SvelteMap, SvelteSet } from 'svelte/reactivity';
import { fade } from 'svelte/transition';
let { let {
onApplyTheme, onApplyTheme,
@@ -2051,7 +2053,7 @@
</div> </div>
{:else} {:else}
<div class="grid grid-cols-3 gap-3 pb-4"> <div class="grid grid-cols-3 gap-3 pb-4">
{#each themes as theme (theme.id)} {#each themes as theme, index (theme.id)}
{@const localTheme = {@const localTheme =
localThemeMap.get(theme.id.toLowerCase()) || localThemeMap.get(theme.id.toLowerCase()) ||
localThemeMap.get(theme.name.toLowerCase())} localThemeMap.get(theme.name.toLowerCase())}
@@ -2077,6 +2079,9 @@
class="group flex flex-col overflow-hidden rounded-xl border bg-theme-900/30 {isActive class="group flex flex-col overflow-hidden rounded-xl border bg-theme-900/30 {isActive
? 'border-primary/60 ring-1 ring-primary/20' ? 'border-primary/60 ring-1 ring-primary/20'
: 'border-theme-800/60'} transition-all duration-300 hover:-translate-y-0.5 hover:border-white/20 hover:bg-theme-900/50 hover:shadow-xl hover:shadow-black/30" : 'border-theme-800/60'} transition-all duration-300 hover:-translate-y-0.5 hover:border-white/20 hover:bg-theme-900/50 hover:shadow-xl hover:shadow-black/30"
class:workshop-card-enter={!$reduceAnimations}
style="animation-delay: {Math.min(index * 24, 144)}ms"
out:fade={{ duration: $reduceAnimations ? 0 : 140 }}
> >
<!-- 16:9 Thumbnail --> <!-- 16:9 Thumbnail -->
<div <div
@@ -4481,3 +4486,20 @@
</AlertDialog.Content> </AlertDialog.Content>
</AlertDialog.Root> </AlertDialog.Root>
</Tooltip.Provider> </Tooltip.Provider>
<style>
.workshop-card-enter {
animation: workshop-card-in 240ms cubic-bezier(0.22, 1, 0.36, 1) backwards;
}
@keyframes workshop-card-in {
from {
opacity: 0;
transform: translateY(8px);
}
to {
opacity: 1;
transform: translateY(0);
}
}
</style>
@@ -110,6 +110,8 @@
import { formatBytes, openURL } from '$lib/utils'; import { formatBytes, openURL } from '$lib/utils';
import { import {
AlertTriangle, AlertTriangle,
AlignCenter,
AlignHorizontalSpaceAround,
Boxes, Boxes,
Check, Check,
CheckCircle2, CheckCircle2,
@@ -128,12 +130,14 @@
Download, Download,
Eye, Eye,
EyeOff, EyeOff,
FileText,
Film, Film,
Gauge, Gauge,
Globe, Globe,
GripVertical, GripVertical,
Hand, Hand,
Image as ImageIcon, Image as ImageIcon,
Info,
Keyboard, Keyboard,
Layers, Layers,
LayoutGrid, LayoutGrid,
@@ -5075,7 +5079,12 @@
</div> </div>
{:else} {:else}
<div class="min-h-0 flex-1 space-y-3 overflow-y-auto p-3"> <div class="min-h-0 flex-1 space-y-3 overflow-y-auto p-3">
<ThemeSection title="Theme details" bordered={false} contentClass="space-y-3 pt-2"> <ThemeSection
title="Theme details"
icon={Info}
bordered={false}
contentClass="space-y-3 pt-2"
>
<div class="space-y-1.5"> <div class="space-y-1.5">
<Label for="theme-name" class={labelClass}>Name</Label> <Label for="theme-name" class={labelClass}>Name</Label>
<Input <Input
@@ -5234,7 +5243,11 @@
onchange={setVariableDefault} onchange={setVariableDefault}
/> />
<ThemeSection title="Launcher Layout & Chrome" contentClass="space-y-3 pt-2.5"> <ThemeSection
title="Launcher Layout & Chrome"
icon={LayoutGrid}
contentClass="space-y-3 pt-2.5"
>
<div class="space-y-1.5"> <div class="space-y-1.5">
<Label class={labelClass}>Layout Preset</Label> <Label class={labelClass}>Layout Preset</Label>
<div class="grid grid-cols-3 gap-1 rounded-md bg-black/30 p-1"> <div class="grid grid-cols-3 gap-1 rounded-md bg-black/30 p-1">
@@ -5419,7 +5432,7 @@
</div> </div>
</ThemeSection> </ThemeSection>
<ThemeSection title="Background" contentClass="space-y-3 pt-2.5"> <ThemeSection title="Background" icon={ImageIcon} contentClass="space-y-3 pt-2.5">
<div class="grid grid-cols-4 gap-1 rounded-md bg-black/30 p-1"> <div class="grid grid-cols-4 gap-1 rounded-md bg-black/30 p-1">
{#each backgroundTypes as option (option.value)} {#each backgroundTypes as option (option.value)}
<Button <Button
@@ -6230,6 +6243,7 @@
<div class="min-h-0 flex-1 space-y-3 overflow-y-auto p-3"> <div class="min-h-0 flex-1 space-y-3 overflow-y-auto p-3">
<ThemeSection <ThemeSection
title="Align" title="Align"
icon={AlignCenter}
bordered={false} bordered={false}
headingClass="text-[10px] font-semibold uppercase tracking-wide text-muted-foreground" headingClass="text-[10px] font-semibold uppercase tracking-wide text-muted-foreground"
contentClass="space-y-2 pt-2" contentClass="space-y-2 pt-2"
@@ -6275,6 +6289,7 @@
</ThemeSection> </ThemeSection>
<ThemeSection <ThemeSection
title="Distribute" title="Distribute"
icon={AlignHorizontalSpaceAround}
headingClass="text-[10px] font-semibold uppercase tracking-wide text-muted-foreground" headingClass="text-[10px] font-semibold uppercase tracking-wide text-muted-foreground"
contentClass="space-y-2 pt-2.5" contentClass="space-y-2 pt-2.5"
> >
@@ -6298,6 +6313,7 @@
</ThemeSection> </ThemeSection>
<ThemeSection <ThemeSection
title="Layers" title="Layers"
icon={Layers}
headingClass="text-[10px] font-semibold uppercase tracking-wide text-muted-foreground" headingClass="text-[10px] font-semibold uppercase tracking-wide text-muted-foreground"
contentClass="space-y-2 pt-2.5" contentClass="space-y-2 pt-2.5"
> >
@@ -6621,6 +6637,7 @@
<ThemeSection <ThemeSection
title="Theme summary" title="Theme summary"
icon={FileText}
class="mt-4" class="mt-4"
bordered={false} bordered={false}
contentClass="space-y-2 pt-2" contentClass="space-y-2 pt-2"
@@ -6649,6 +6666,7 @@
<ThemeSection <ThemeSection
title="Shortcuts" title="Shortcuts"
icon={Keyboard}
class="mt-4" class="mt-4"
bordered={false} bordered={false}
open={false} open={false}
@@ -38,12 +38,15 @@
Film, Film,
Gauge, Gauge,
Image as ImageIcon, Image as ImageIcon,
Layers,
LayoutGrid, LayoutGrid,
Megaphone, Megaphone,
MousePointerClick, MousePointerClick,
Move,
Play, Play,
RefreshCw, RefreshCw,
Repeat, Repeat,
Scan,
Shapes, Shapes,
Sparkles, Sparkles,
Spline, Spline,
@@ -221,7 +224,7 @@
</div> </div>
</div> </div>
<ThemeSection title="Position & size" bordered={false} contentClass="space-y-3 pt-2"> <ThemeSection title="Position & size" icon={Move} bordered={false} contentClass="space-y-3 pt-2">
<div class="space-y-1"> <div class="space-y-1">
<Label for="element-name" class={label}>Layer name</Label> <Label for="element-name" class={label}>Layer name</Label>
<Input <Input
@@ -290,6 +293,7 @@
<ThemeSection <ThemeSection
title="Layer state" title="Layer state"
icon={Layers}
contentClass="flex flex-wrap items-center gap-x-4 gap-y-2 pt-2.5" contentClass="flex flex-wrap items-center gap-x-4 gap-y-2 pt-2.5"
> >
<div class="flex items-center gap-1.5"> <div class="flex items-center gap-1.5">
@@ -402,7 +406,11 @@
/> />
{#if element.type === 'text' || element.type === 'data'} {#if element.type === 'text' || element.type === 'data'}
<ThemeSection title={element.type === 'data' ? 'Data' : 'Text'} contentClass="space-y-3 pt-2.5"> <ThemeSection
title={element.type === 'data' ? 'Data' : 'Text'}
icon={Type}
contentClass="space-y-3 pt-2.5"
>
{#if element.type === 'data'} {#if element.type === 'data'}
<div class="space-y-1.5"> <div class="space-y-1.5">
<Label for="element-data-field" class={label}>Value</Label> <Label for="element-data-field" class={label}>Value</Label>
@@ -695,7 +703,7 @@
<ThemeTextStyleControls {element} onchange={(patch) => Object.assign(element, patch)} /> <ThemeTextStyleControls {element} onchange={(patch) => Object.assign(element, patch)} />
</ThemeSection> </ThemeSection>
{:else if element.type === 'svelte'} {:else if element.type === 'svelte'}
<ThemeSection title="Svelte component" contentClass="space-y-3 pt-2.5"> <ThemeSection title="Svelte component" icon={Component} contentClass="space-y-3 pt-2.5">
<div <div
class="rounded-md border border-amber-500/25 bg-amber-500/10 p-2 text-[9px] leading-relaxed text-amber-100" class="rounded-md border border-amber-500/25 bg-amber-500/10 p-2 text-[9px] leading-relaxed text-amber-100"
> >
@@ -721,7 +729,7 @@
</div> </div>
</ThemeSection> </ThemeSection>
{:else if element.type === 'image' || element.type === 'video'} {:else if element.type === 'image' || element.type === 'video'}
<ThemeSection title="Media" contentClass="space-y-3 pt-2.5"> <ThemeSection title="Media" icon={ImageIcon} contentClass="space-y-3 pt-2.5">
<div class="rounded-md border border-theme-800 bg-black/20 p-2"> <div class="rounded-md border border-theme-800 bg-black/20 p-2">
<p class="text-[10px] uppercase tracking-wide text-muted-foreground">Asset</p> <p class="text-[10px] uppercase tracking-wide text-muted-foreground">Asset</p>
<p class="mt-1 truncate font-mono text-[10px] text-theme-100">{element.src}</p> <p class="mt-1 truncate font-mono text-[10px] text-theme-100">{element.src}</p>
@@ -829,7 +837,7 @@
{/if} {/if}
</ThemeSection> </ThemeSection>
{:else if element.type === 'lottie'} {:else if element.type === 'lottie'}
<ThemeSection title="Lottie animation" contentClass="space-y-3 pt-2.5"> <ThemeSection title="Lottie animation" icon={Film} contentClass="space-y-3 pt-2.5">
<div class="rounded-md border border-theme-800 bg-black/20 p-2"> <div class="rounded-md border border-theme-800 bg-black/20 p-2">
<p class="text-[10px] uppercase tracking-wide text-muted-foreground">Animation</p> <p class="text-[10px] uppercase tracking-wide text-muted-foreground">Animation</p>
<p class="mt-1 truncate font-mono text-[10px] text-theme-100">{element.src}</p> <p class="mt-1 truncate font-mono text-[10px] text-theme-100">{element.src}</p>
@@ -885,7 +893,7 @@
</p> </p>
</ThemeSection> </ThemeSection>
{:else if element.type === 'shape'} {:else if element.type === 'shape'}
<ThemeSection title="Shape" contentClass="space-y-3 pt-2.5"> <ThemeSection title="Shape" icon={Shapes} contentClass="space-y-3 pt-2.5">
<div class="space-y-1"> <div class="space-y-1">
<Label for="element-shape" class={label}>Shape</Label> <Label for="element-shape" class={label}>Shape</Label>
<Select.Root <Select.Root
@@ -1153,7 +1161,7 @@
</div> </div>
</ThemeSection> </ThemeSection>
{:else if element.type === 'icon'} {:else if element.type === 'icon'}
<ThemeSection title="Icon" contentClass="space-y-3 pt-2.5"> <ThemeSection title="Icon" icon={Sparkles} contentClass="space-y-3 pt-2.5">
<div class="space-y-1"> <div class="space-y-1">
<Label for="element-icon" class={label}>Icon</Label> <Label for="element-icon" class={label}>Icon</Label>
<ThemeIconPicker bind:value={element.icon} {iconSet} /> <ThemeIconPicker bind:value={element.icon} {iconSet} />
@@ -1185,7 +1193,7 @@
</div> </div>
</ThemeSection> </ThemeSection>
{:else if element.type === 'repeater'} {:else if element.type === 'repeater'}
<ThemeSection title="Repeater" contentClass="space-y-3 pt-2.5"> <ThemeSection title="Repeater" icon={Repeat} contentClass="space-y-3 pt-2.5">
<div class="space-y-1"> <div class="space-y-1">
<Label for="element-repeater-source" class={label}>Tile</Label> <Label for="element-repeater-source" class={label}>Tile</Label>
<Select.Root <Select.Root
@@ -1307,7 +1315,7 @@
</p> </p>
</ThemeSection> </ThemeSection>
{:else if element.type === 'gauge'} {:else if element.type === 'gauge'}
<ThemeSection title="Gauge" contentClass="space-y-3 pt-2.5"> <ThemeSection title="Gauge" icon={Gauge} contentClass="space-y-3 pt-2.5">
<div class="grid grid-cols-2 gap-2"> <div class="grid grid-cols-2 gap-2">
<div class="space-y-1"> <div class="space-y-1">
<Label for="element-gauge-mode" class={label}>Style</Label> <Label for="element-gauge-mode" class={label}>Style</Label>
@@ -1423,7 +1431,7 @@
</div> </div>
</ThemeSection> </ThemeSection>
{:else if element.type === 'ticker'} {:else if element.type === 'ticker'}
<ThemeSection title="Ticker" contentClass="space-y-3 pt-2.5"> <ThemeSection title="Ticker" icon={Type} contentClass="space-y-3 pt-2.5">
<div class="space-y-1"> <div class="space-y-1">
<Label for="element-ticker-text" class={label}>Text</Label> <Label for="element-ticker-text" class={label}>Text</Label>
<Textarea <Textarea
@@ -1520,7 +1528,7 @@
{/if} {/if}
</ThemeSection> </ThemeSection>
{:else if element.type === 'button'} {:else if element.type === 'button'}
<ThemeSection title="Button" contentClass="space-y-3 pt-2.5"> <ThemeSection title="Button" icon={MousePointerClick} contentClass="space-y-3 pt-2.5">
<div class="space-y-1"> <div class="space-y-1">
<Label for="element-button-label" class={label}>Label</Label> <Label for="element-button-label" class={label}>Label</Label>
<Input id="element-button-label" class={field} bind:value={element.label} /> <Input id="element-button-label" class={field} bind:value={element.label} />
@@ -1704,7 +1712,7 @@
</div> </div>
</ThemeSection> </ThemeSection>
{:else if element.type === 'path'} {:else if element.type === 'path'}
<ThemeSection title="Vector path" contentClass="space-y-3 pt-2.5"> <ThemeSection title="Vector path" icon={Spline} contentClass="space-y-3 pt-2.5">
<div class="space-y-1"> <div class="space-y-1">
<Label for="element-path-data" class={label}>SVG path data</Label> <Label for="element-path-data" class={label}>SVG path data</Label>
<Textarea <Textarea
@@ -1850,7 +1858,7 @@
</div> </div>
</ThemeSection> </ThemeSection>
{:else if element.type === 'particles'} {:else if element.type === 'particles'}
<ThemeSection title="Particles" contentClass="space-y-3 pt-2.5"> <ThemeSection title="Particles" icon={Sparkles} contentClass="space-y-3 pt-2.5">
<div class="space-y-1"> <div class="space-y-1">
<Label for="element-particles-preset" class={label}>Preset</Label> <Label for="element-particles-preset" class={label}>Preset</Label>
<Select.Root <Select.Root
@@ -1954,7 +1962,7 @@
</p> </p>
</ThemeSection> </ThemeSection>
{:else if element.type === 'clock'} {:else if element.type === 'clock'}
<ThemeSection title="Clock" contentClass="space-y-3 pt-2.5"> <ThemeSection title="Clock" icon={Clock} contentClass="space-y-3 pt-2.5">
<div class="space-y-1"> <div class="space-y-1">
<Label for="element-clock-mode" class={label}>Style</Label> <Label for="element-clock-mode" class={label}>Style</Label>
<Select.Root <Select.Root
@@ -2121,7 +2129,7 @@
</p> </p>
</ThemeSection> </ThemeSection>
{:else if element.type === 'countdown'} {:else if element.type === 'countdown'}
<ThemeSection title="Countdown" contentClass="space-y-3 pt-2.5"> <ThemeSection title="Countdown" icon={Timer} contentClass="space-y-3 pt-2.5">
<div class="space-y-1.5"> <div class="space-y-1.5">
<Label for="element-countdown-target" class={label}>Target date & time</Label> <Label for="element-countdown-target" class={label}>Target date & time</Label>
<DatePicker <DatePicker
@@ -2220,7 +2228,7 @@
</div> </div>
</ThemeSection> </ThemeSection>
{:else if element.type === 'widget'} {:else if element.type === 'widget'}
<ThemeSection title="Widget" contentClass="space-y-3 pt-2.5"> <ThemeSection title="Widget" icon={LayoutGrid} contentClass="space-y-3 pt-2.5">
<div class="space-y-1.5"> <div class="space-y-1.5">
<Label for="element-widget-type" class={label}>Widget Type</Label> <Label for="element-widget-type" class={label}>Widget Type</Label>
<Select.Root <Select.Root
@@ -2558,7 +2566,7 @@
</ThemeSection> </ThemeSection>
{/if} {/if}
<ThemeSection title="Mask" contentClass="space-y-3 pt-2.5"> <ThemeSection title="Mask" icon={Scan} contentClass="space-y-3 pt-2.5">
<div class="space-y-1"> <div class="space-y-1">
<Label for="element-mask-shape" class={label}>Clip shape</Label> <Label for="element-mask-shape" class={label}>Clip shape</Label>
<Select.Root <Select.Root
@@ -3,6 +3,7 @@
import Button from '$lib/components/ui/button/button.svelte'; import Button from '$lib/components/ui/button/button.svelte';
import Slider from '$lib/components/ui/slider/slider.svelte'; import Slider from '$lib/components/ui/slider/slider.svelte';
import type { ThemeV2Filters } from '$lib/themeV2'; import type { ThemeV2Filters } from '$lib/themeV2';
import { SlidersHorizontal } from '@lucide/svelte';
let { let {
filters, filters,
@@ -67,6 +68,7 @@
<ThemeSection <ThemeSection
title="Filters" title="Filters"
icon={SlidersHorizontal}
open={false} open={false}
actions={filterActions} actions={filterActions}
contentClass="space-y-2.5 pt-2.5" contentClass="space-y-2.5 pt-2.5"
@@ -17,7 +17,7 @@
type ThemeV2Group, type ThemeV2Group,
type ThemeV2MaskShape, type ThemeV2MaskShape,
} from '$lib/themeV2'; } from '$lib/themeV2';
import { Boxes, RefreshCw, Ungroup } from '@lucide/svelte'; import { Boxes, Layers, RefreshCw, Scan, Ungroup } from '@lucide/svelte';
let { let {
group = $bindable(), group = $bindable(),
@@ -58,7 +58,7 @@
</div> </div>
</div> </div>
<ThemeSection title="Group" bordered={false} contentClass="space-y-3 pt-2"> <ThemeSection title="Group" icon={Boxes} bordered={false} contentClass="space-y-3 pt-2">
<div class="space-y-1"> <div class="space-y-1">
<Label for="group-name" class={label}>Group name</Label> <Label for="group-name" class={label}>Group name</Label>
<Input <Input
@@ -96,6 +96,7 @@
<ThemeSection <ThemeSection
title="Group state" title="Group state"
icon={Layers}
contentClass="flex flex-wrap items-center gap-x-4 gap-y-2 pt-2.5" contentClass="flex flex-wrap items-center gap-x-4 gap-y-2 pt-2.5"
> >
<div class="flex items-center gap-1.5"> <div class="flex items-center gap-1.5">
@@ -167,7 +168,7 @@
ondelete={ondeleterule} ondelete={ondeleterule}
/> />
<ThemeSection title="Mask" contentClass="space-y-3 pt-2.5"> <ThemeSection title="Mask" icon={Scan} contentClass="space-y-3 pt-2.5">
<div class="space-y-1"> <div class="space-y-1">
<Label for="group-mask-shape" class={label}>Clip shape</Label> <Label for="group-mask-shape" class={label}>Clip shape</Label>
<Select.Root <Select.Root
@@ -1,6 +1,7 @@
<script lang="ts"> <script lang="ts">
import ThemeBehaviorInspector from '$lib/components/theme/ThemeBehaviorInspector.svelte'; import ThemeBehaviorInspector from '$lib/components/theme/ThemeBehaviorInspector.svelte';
import Button from '$lib/components/ui/button/button.svelte'; import Button from '$lib/components/ui/button/button.svelte';
import { themeEditorSectionOpen } from '$lib/themeEditorUiState';
import { themeBehaviorSummary, themeGroupInteractiveCount } from '$lib/themeLogic'; import { themeBehaviorSummary, themeGroupInteractiveCount } from '$lib/themeLogic';
import type { import type {
ThemeV2Behavior, ThemeV2Behavior,
@@ -30,8 +31,13 @@
ondelete: (id: string) => void; ondelete: (id: string) => void;
} = $props(); } = $props();
let open = $state(false); const sectionKey = 'Logic';
let open = $state(themeEditorSectionOpen.get(sectionKey) ?? false);
let selectedId = $state<string | undefined>(undefined); let selectedId = $state<string | undefined>(undefined);
$effect(() => {
themeEditorSectionOpen.set(sectionKey, open);
});
let section = $state<HTMLElement>(); let section = $state<HTMLElement>();
let lastFocusToken = 0; let lastFocusToken = 0;
+13 -5
View File
@@ -1,10 +1,12 @@
<script lang="ts"> <script lang="ts">
import { themeEditorSectionOpen } from '$lib/themeEditorUiState';
import { ChevronDown } from '@lucide/svelte'; import { ChevronDown } from '@lucide/svelte';
import { untrack } from 'svelte';
import type { Component, Snippet } from 'svelte'; import type { Component, Snippet } from 'svelte';
let { let {
title, title,
open = $bindable(true), open: initialOpen = true,
icon: Icon, icon: Icon,
badge, badge,
description, description,
@@ -27,6 +29,12 @@
headingClass?: string; headingClass?: string;
bordered?: boolean; bordered?: boolean;
} = $props(); } = $props();
let isOpen = $state(untrack(() => themeEditorSectionOpen.get(title) ?? initialOpen));
$effect(() => {
themeEditorSectionOpen.set(title, isOpen);
});
</script> </script>
<section class="{bordered ? 'border-t border-theme-800 pt-3 ' : ''}{className}"> <section class="{bordered ? 'border-t border-theme-800 pt-3 ' : ''}{className}">
@@ -34,8 +42,8 @@
<button <button
type="button" type="button"
class="group flex min-w-0 flex-1 items-center gap-1.5 rounded-md py-0.5 text-left transition-colors hover:text-primary focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-primary/60" class="group flex min-w-0 flex-1 items-center gap-1.5 rounded-md py-0.5 text-left transition-colors hover:text-primary focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-primary/60"
aria-expanded={open} aria-expanded={isOpen}
onclick={() => (open = !open)} onclick={() => (isOpen = !isOpen)}
> >
{#if Icon} {#if Icon}
<Icon class="size-3.5 shrink-0 text-primary" /> <Icon class="size-3.5 shrink-0 text-primary" />
@@ -49,7 +57,7 @@
</span> </span>
{/if} {/if}
<ChevronDown <ChevronDown
class="ml-auto size-3.5 shrink-0 text-muted-foreground transition-transform duration-200 {open class="ml-auto size-3.5 shrink-0 text-muted-foreground transition-transform duration-200 {isOpen
? 'rotate-180' ? 'rotate-180'
: ''}" : ''}"
/> />
@@ -61,7 +69,7 @@
{/if} {/if}
</div> </div>
{#if open} {#if isOpen}
<div class={contentClass}> <div class={contentClass}>
{#if description} {#if description}
<p class="text-[10px] leading-relaxed text-muted-foreground">{description}</p> <p class="text-[10px] leading-relaxed text-muted-foreground">{description}</p>
@@ -5,7 +5,7 @@
import Label from '$lib/components/ui/label/label.svelte'; import Label from '$lib/components/ui/label/label.svelte';
import * as Select from '$lib/components/ui/select'; import * as Select from '$lib/components/ui/select';
import type { ThemeV2TextDecoration, ThemeV2TextStyle, ThemeV2TextTransform } from '$lib/themeV2'; import type { ThemeV2TextDecoration, ThemeV2TextStyle, ThemeV2TextTransform } from '$lib/themeV2';
import { Bold, Italic, RotateCcw, Strikethrough, Underline } from '@lucide/svelte'; import { Bold, Italic, RotateCcw, Strikethrough, Type, Underline } from '@lucide/svelte';
let { let {
element, element,
@@ -89,6 +89,7 @@
<ThemeSection <ThemeSection
title="Text style" title="Text style"
icon={Type}
open={false} open={false}
actions={textStyleActions} actions={textStyleActions}
contentClass="space-y-2 pt-2.5" contentClass="space-y-2 pt-2.5"
@@ -112,6 +112,7 @@
<ThemeSection <ThemeSection
title="Transform" title="Transform"
icon={RotateCw}
open={false} open={false}
actions={transformActions} actions={transformActions}
contentClass="space-y-2 pt-2.5" contentClass="space-y-2 pt-2.5"
@@ -3,7 +3,7 @@
import Button from '$lib/components/ui/button/button.svelte'; import Button from '$lib/components/ui/button/button.svelte';
import Input from '$lib/components/ui/input/input.svelte'; import Input from '$lib/components/ui/input/input.svelte';
import type { ThemeV2ScriptValue } from '$lib/themeV2'; import type { ThemeV2ScriptValue } from '$lib/themeV2';
import { RotateCcw } from '@lucide/svelte'; import { Braces, RotateCcw } from '@lucide/svelte';
let { let {
variables, variables,
@@ -38,7 +38,12 @@
}; };
</script> </script>
<ThemeSection title="Variables" badge={variables.length} contentClass="space-y-2 pt-2"> <ThemeSection
title="Variables"
icon={Braces}
badge={variables.length}
contentClass="space-y-2 pt-2"
>
<p class="text-[10px] leading-relaxed text-muted-foreground"> <p class="text-[10px] leading-relaxed text-muted-foreground">
Starting values for Starting values for
<code class="rounded bg-theme-900 px-1 py-0.5 text-theme-100">{'{name}'}</code> <code class="rounded bg-theme-900 px-1 py-0.5 text-theme-100">{'{name}'}</code>
+2 -2
View File
@@ -125,8 +125,8 @@ export const DISCORD_PRESENCE_PRESETS: DiscordPresencePreset[] = [
}, },
button2: { button2: {
enabled: true, enabled: true,
label: 'View Profile', label: 'Join EZPPFarm',
url: '{profileUrl}', url: 'https://ez-pp.farm/discord',
}, },
}, },
}, },
+8
View File
@@ -0,0 +1,8 @@
import { SvelteMap } from 'svelte/reactivity';
/**
* Open state for accordion sections in the current theme-editor WebView session.
* This module is re-evaluated when the editor window closes, so the state is not
* persisted across app launches.
*/
export const themeEditorSectionOpen = new SvelteMap<string, boolean>();
+3 -2
View File
@@ -522,6 +522,7 @@ export const reloadThemes = async () => {
if (ACTIVE_STATUSES.has(storeTheme.status)) return storeTheme; if (ACTIVE_STATUSES.has(storeTheme.status)) return storeTheme;
const theme = installedMap.get(storeTheme.name); const theme = installedMap.get(storeTheme.name);
if (!theme) return storeTheme; if (!theme) return storeTheme;
const versionChanged = storeTheme.version !== theme.version;
// Replace the object rather than mutating it so the theme cards see the // Replace the object rather than mutating it so the theme cards see the
// new values when the list is rendered from a filtered derived array. // new values when the list is rendered from a filtered derived array.
@@ -538,8 +539,8 @@ export const reloadThemes = async () => {
nsfw: theme.nsfw, nsfw: theme.nsfw,
workshopId: theme.workshopId, workshopId: theme.workshopId,
definition: theme.definition, definition: theme.definition,
updateAvailable: theme.updateAvailable, updateAvailable: versionChanged ? false : storeTheme.updateAvailable,
availableVersion: theme.availableVersion, availableVersion: versionChanged ? undefined : storeTheme.availableVersion,
cssStyle: theme.cssStyle, cssStyle: theme.cssStyle,
backups: theme.backups ?? storeTheme.backups, backups: theme.backups ?? storeTheme.backups,
}; };
+12 -4
View File
@@ -1,8 +1,6 @@
<script lang="ts"> <script lang="ts">
import { browser } from '$app/environment'; import { browser } from '$app/environment';
import Logo from '$assets/logo.png'; import Logo from '$assets/logo.png';
import DiscordPresenceEditor from '$lib/components/presence/DiscordPresenceEditor.svelte';
import ThemeEditorWindow from '$lib/components/theme/ThemeEditorWindow.svelte';
import ThemeV2Background from '$lib/components/theme/ThemeV2Background.svelte'; import ThemeV2Background from '$lib/components/theme/ThemeV2Background.svelte';
import * as AlertDialog from '$lib/components/ui/alert-dialog'; import * as AlertDialog from '$lib/components/ui/alert-dialog';
import AnimatedBg from '$lib/components/ui/animated-bg/AnimatedBg.svelte'; import AnimatedBg from '$lib/components/ui/animated-bg/AnimatedBg.svelte';
@@ -449,9 +447,19 @@
/> />
{#if editorWindow} {#if editorWindow}
<ThemeEditorWindow /> {#await import('$lib/components/theme/ThemeEditorWindow.svelte')}
<div class="h-screen w-screen bg-theme-950"></div>
{:then editorModule}
{@const ThemeEditorWindow = editorModule.default}
<ThemeEditorWindow />
{/await}
{:else if presenceEditorWindow} {:else if presenceEditorWindow}
<DiscordPresenceEditor standalone /> {#await import('$lib/components/presence/DiscordPresenceEditor.svelte')}
<div class="h-screen w-screen bg-theme-950"></div>
{:then presenceModule}
{@const DiscordPresenceEditor = presenceModule.default}
<DiscordPresenceEditor standalone />
{/await}
{:else} {:else}
{#if editorOpen} {#if editorOpen}
<div <div
+587 -391
View File
File diff suppressed because it is too large. Load diff