2025-12-02 09:54:38 +01:00
|
|
|
<script lang="ts">
|
2026-04-12 08:58:41 +01:00
|
|
|
import { resolve } from '$app/paths';
|
2026-07-06 20:51:17 +01:00
|
|
|
import { page } from '$app/state';
|
2026-04-12 08:58:41 +01:00
|
|
|
import { goto } from '$app/navigation';
|
2026-05-18 03:47:22 +01:00
|
|
|
import ArrowRightIcon from '@lucide/svelte/icons/arrow-right';
|
2026-05-23 12:24:57 +01:00
|
|
|
import EllipsisVerticalIcon from '@lucide/svelte/icons/ellipsis-vertical';
|
|
|
|
|
import EyeIcon from '@lucide/svelte/icons/eye';
|
2026-05-18 03:47:22 +01:00
|
|
|
import LayoutTemplateIcon from '@lucide/svelte/icons/layout-template';
|
2026-05-23 12:24:57 +01:00
|
|
|
import PaintbrushIcon from '@lucide/svelte/icons/paintbrush';
|
2026-05-18 03:47:22 +01:00
|
|
|
import ZapIcon from '@lucide/svelte/icons/zap';
|
|
|
|
|
|
2026-04-12 08:58:41 +01:00
|
|
|
import { currentOrg, currentOrgPath, isFreePlan } from '$lib/utils/store/org';
|
2026-08-18 13:17:06 +01:00
|
|
|
import { openUpgradeModal } from '$lib/utils/store/upgrade-modal';
|
2025-12-02 09:54:38 +01:00
|
|
|
import { t } from '$lib/utils/functions/translations';
|
2026-05-18 03:47:22 +01:00
|
|
|
import { user } from '$lib/utils/store/user';
|
|
|
|
|
import { basePath } from '$lib/utils/store/app';
|
2026-05-23 12:24:57 +01:00
|
|
|
import type { AccountOrg } from '$features/app/types';
|
|
|
|
|
import { orgApi } from '$features/org/api/org.svelte';
|
|
|
|
|
import { snackbar } from '$features/ui/snackbar/store';
|
2026-05-18 03:47:22 +01:00
|
|
|
|
2025-12-02 09:54:38 +01:00
|
|
|
import { Button } from '@cio/ui/base/button';
|
2026-05-18 03:47:22 +01:00
|
|
|
import { Badge } from '@cio/ui/base/badge';
|
|
|
|
|
import * as Card from '@cio/ui/base/card';
|
2026-05-23 12:24:57 +01:00
|
|
|
import * as DropdownMenu from '@cio/ui/base/dropdown-menu';
|
|
|
|
|
import { IconButton } from '@cio/ui/custom/icon-button';
|
2026-05-18 03:47:22 +01:00
|
|
|
import { PremiumIcon } from '@cio/ui/custom/moving-icons';
|
|
|
|
|
|
2026-05-23 12:24:57 +01:00
|
|
|
import ThemePreviewDialog from '$features/settings/components/theme-preview-dialog.svelte';
|
|
|
|
|
|
2026-04-12 08:58:41 +01:00
|
|
|
import {
|
|
|
|
|
createDefaultLandingPageSettings,
|
|
|
|
|
landingPageThemes,
|
2026-05-18 03:47:22 +01:00
|
|
|
normalizeLandingPageSettings,
|
|
|
|
|
buildOrgLandingPageProps
|
2026-04-12 08:58:41 +01:00
|
|
|
} from '$features/org/utils/landing-page';
|
2026-05-18 03:47:22 +01:00
|
|
|
import { landingPageThemeComponents } from '$features/org/utils/landing-page-components';
|
2025-12-02 09:54:38 +01:00
|
|
|
|
2026-05-18 03:47:22 +01:00
|
|
|
type LandingPageTheme = (typeof landingPageThemes)[number];
|
|
|
|
|
|
|
|
|
|
let normalized = $state(createDefaultLandingPageSettings());
|
2026-04-12 08:58:41 +01:00
|
|
|
let hasInitialized = $state(false);
|
2026-05-18 03:47:22 +01:00
|
|
|
|
|
|
|
|
$effect(() => {
|
|
|
|
|
if (hasInitialized || !$currentOrg.id) return;
|
|
|
|
|
|
|
|
|
|
normalized = normalizeLandingPageSettings($currentOrg.landingpage);
|
|
|
|
|
hasInitialized = true;
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
const currentTheme = $derived(normalized.theme as LandingPageTheme);
|
2025-12-02 09:54:38 +01:00
|
|
|
|
2026-06-07 18:43:05 +01:00
|
|
|
const CDN_BASE = 'https://assets.cdn.clsrio.com/templates';
|
|
|
|
|
|
2026-04-12 08:58:41 +01:00
|
|
|
const themeCards = [
|
|
|
|
|
{
|
|
|
|
|
value: 'minimal',
|
2026-06-07 18:43:05 +01:00
|
|
|
preview: `${CDN_BASE}/minimal.png`,
|
2026-04-12 08:58:41 +01:00
|
|
|
titleKey: 'settings.landing_page.theme.cards.minimal.title',
|
|
|
|
|
descriptionKey: 'settings.landing_page.theme.cards.minimal.description'
|
|
|
|
|
},
|
|
|
|
|
{
|
|
|
|
|
value: 'bold',
|
2026-06-07 18:43:05 +01:00
|
|
|
preview: `${CDN_BASE}/bold.png`,
|
2026-04-12 08:58:41 +01:00
|
|
|
titleKey: 'settings.landing_page.theme.cards.bold.title',
|
|
|
|
|
descriptionKey: 'settings.landing_page.theme.cards.bold.description'
|
|
|
|
|
},
|
|
|
|
|
{
|
|
|
|
|
value: 'classic',
|
2026-06-07 18:43:05 +01:00
|
|
|
preview: `${CDN_BASE}/classic.png`,
|
2026-04-12 08:58:41 +01:00
|
|
|
titleKey: 'settings.landing_page.theme.cards.classic.title',
|
|
|
|
|
descriptionKey: 'settings.landing_page.theme.cards.classic.description'
|
2026-05-18 03:47:22 +01:00
|
|
|
},
|
|
|
|
|
{
|
|
|
|
|
value: 'saas',
|
2026-06-07 18:43:05 +01:00
|
|
|
preview: `${CDN_BASE}/saas.png`,
|
2026-05-18 03:47:22 +01:00
|
|
|
titleKey: 'settings.landing_page.theme.cards.saas.title',
|
|
|
|
|
descriptionKey: 'settings.landing_page.theme.cards.saas.description'
|
|
|
|
|
},
|
|
|
|
|
{
|
|
|
|
|
value: 'tech',
|
2026-06-07 18:43:05 +01:00
|
|
|
preview: `${CDN_BASE}/tech.png`,
|
2026-05-18 03:47:22 +01:00
|
|
|
titleKey: 'settings.landing_page.theme.cards.tech.title',
|
|
|
|
|
descriptionKey: 'settings.landing_page.theme.cards.tech.description'
|
|
|
|
|
},
|
|
|
|
|
{
|
|
|
|
|
value: 'studio',
|
2026-06-07 18:43:05 +01:00
|
|
|
preview: `${CDN_BASE}/studio.png`,
|
2026-05-18 03:47:22 +01:00
|
|
|
titleKey: 'settings.landing_page.theme.cards.studio.title',
|
|
|
|
|
descriptionKey: 'settings.landing_page.theme.cards.studio.description'
|
|
|
|
|
},
|
|
|
|
|
{
|
|
|
|
|
value: 'corporate',
|
2026-06-07 18:43:05 +01:00
|
|
|
preview: `${CDN_BASE}/corporate.png`,
|
2026-05-18 03:47:22 +01:00
|
|
|
titleKey: 'settings.landing_page.theme.cards.corporate.title',
|
|
|
|
|
descriptionKey: 'settings.landing_page.theme.cards.corporate.description'
|
|
|
|
|
},
|
|
|
|
|
{
|
|
|
|
|
value: 'terminal',
|
2026-06-07 18:43:05 +01:00
|
|
|
preview: `${CDN_BASE}/terminal.png`,
|
2026-05-18 03:47:22 +01:00
|
|
|
titleKey: 'settings.landing_page.theme.cards.terminal.title',
|
|
|
|
|
descriptionKey: 'settings.landing_page.theme.cards.terminal.description'
|
2026-05-23 12:24:57 +01:00
|
|
|
},
|
|
|
|
|
{
|
|
|
|
|
value: 'editorial',
|
2026-06-07 18:43:05 +01:00
|
|
|
preview: `${CDN_BASE}/editorial.png`,
|
2026-05-23 12:24:57 +01:00
|
|
|
titleKey: 'settings.landing_page.theme.cards.editorial.title',
|
|
|
|
|
descriptionKey: 'settings.landing_page.theme.cards.editorial.description'
|
|
|
|
|
},
|
|
|
|
|
{
|
|
|
|
|
value: 'vibrant',
|
2026-06-07 18:43:05 +01:00
|
|
|
preview: `${CDN_BASE}/vibrant.png`,
|
2026-05-23 12:24:57 +01:00
|
|
|
titleKey: 'settings.landing_page.theme.cards.vibrant.title',
|
|
|
|
|
descriptionKey: 'settings.landing_page.theme.cards.vibrant.description'
|
2026-04-12 08:58:41 +01:00
|
|
|
}
|
2026-05-18 03:47:22 +01:00
|
|
|
] as const satisfies ReadonlyArray<{
|
|
|
|
|
value: LandingPageTheme;
|
|
|
|
|
preview: string;
|
|
|
|
|
titleKey: string;
|
|
|
|
|
descriptionKey: string;
|
|
|
|
|
}>;
|
2025-12-02 09:54:38 +01:00
|
|
|
|
2026-05-18 03:47:22 +01:00
|
|
|
const currentThemeCard = $derived(themeCards.find((card) => card.value === currentTheme) ?? themeCards[0]);
|
|
|
|
|
const otherThemeCards = $derived(themeCards.filter((card) => card.value !== currentTheme));
|
|
|
|
|
|
|
|
|
|
function isPaidTheme(theme: LandingPageTheme): boolean {
|
2026-04-12 08:58:41 +01:00
|
|
|
return theme !== 'minimal';
|
2025-12-02 09:54:38 +01:00
|
|
|
}
|
|
|
|
|
|
2026-05-18 03:47:22 +01:00
|
|
|
const authAction = $derived(
|
|
|
|
|
$user.isLoggedIn
|
|
|
|
|
? {
|
|
|
|
|
label: t.get($basePath === '/lms' || $basePath === '#' ? 'navigation.goto_lms' : 'navigation.goto_dashboard'),
|
|
|
|
|
href: resolve(`${$basePath !== '#' ? $basePath : '/lms'}`, {})
|
|
|
|
|
}
|
|
|
|
|
: { label: t.get('navigation.login'), href: '/login' }
|
|
|
|
|
);
|
|
|
|
|
|
2026-07-06 20:51:17 +01:00
|
|
|
const previewSiteName = $derived($currentOrg.siteName || page.params.slug || '');
|
|
|
|
|
|
|
|
|
|
const coursesLoaded = $derived(!previewSiteName || orgApi.publicCoursesLoadedSiteName === previewSiteName);
|
|
|
|
|
|
|
|
|
|
const previewProps = $derived.by(() =>
|
|
|
|
|
buildOrgLandingPageProps($currentOrg, normalized, orgApi.publicCourses, orgApi.hasMorePublicCourses, authAction, {
|
|
|
|
|
coursesLoaded
|
|
|
|
|
})
|
|
|
|
|
);
|
|
|
|
|
|
|
|
|
|
$effect(() => {
|
|
|
|
|
if (!previewSiteName) return;
|
|
|
|
|
|
|
|
|
|
void orgApi.refreshPublicCourses(previewSiteName);
|
|
|
|
|
});
|
2026-05-18 03:47:22 +01:00
|
|
|
|
|
|
|
|
const ThemeComponent = $derived(landingPageThemeComponents[currentTheme] ?? landingPageThemeComponents.minimal);
|
|
|
|
|
|
|
|
|
|
const customizeHref = $derived(resolve(`${$currentOrgPath}/settings/landingpage/edit`, {}));
|
|
|
|
|
|
|
|
|
|
function handleAddTheme(theme: LandingPageTheme) {
|
2026-04-12 08:58:41 +01:00
|
|
|
if ($isFreePlan && isPaidTheme(theme)) {
|
|
|
|
|
openUpgradeModal();
|
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
|
2026-05-18 03:47:22 +01:00
|
|
|
goto(resolve(`${$currentOrgPath}/settings/landingpage/edit?theme=${theme}`, {}));
|
2026-04-12 08:58:41 +01:00
|
|
|
}
|
|
|
|
|
|
2026-05-23 12:24:57 +01:00
|
|
|
async function handleApplyTheme(theme: LandingPageTheme) {
|
|
|
|
|
if (!$currentOrg.id) return;
|
|
|
|
|
|
|
|
|
|
if ($isFreePlan && isPaidTheme(theme)) {
|
|
|
|
|
openUpgradeModal();
|
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
const updatedLandingPage = normalizeLandingPageSettings({
|
|
|
|
|
...normalized,
|
|
|
|
|
theme
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
await orgApi.update($currentOrg.id, {
|
|
|
|
|
landingpage: updatedLandingPage as AccountOrg['landingpage']
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
normalized = updatedLandingPage;
|
|
|
|
|
snackbar.success('snackbar.success_update');
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
function handlePreviewTheme(theme: LandingPageTheme) {
|
|
|
|
|
const nextUrl = new URL(window.location.href);
|
|
|
|
|
nextUrl.searchParams.set('preview', theme);
|
|
|
|
|
goto(`${nextUrl.pathname}${nextUrl.search}${nextUrl.hash}`, {
|
|
|
|
|
replaceState: false,
|
|
|
|
|
keepFocus: true,
|
|
|
|
|
noScroll: true
|
|
|
|
|
});
|
|
|
|
|
}
|
|
|
|
|
|
2026-05-18 03:47:22 +01:00
|
|
|
const navCount = $derived(normalized.navItems?.length ?? 0);
|
|
|
|
|
const heroHeading = $derived(normalized.hero?.heading?.trim() || t.get('settings.landing_page.current.hero_empty'));
|
|
|
|
|
const navCountLabel = $derived(
|
|
|
|
|
t.get(
|
|
|
|
|
navCount === 1
|
|
|
|
|
? 'settings.landing_page.current.field_nav_count_one'
|
|
|
|
|
: 'settings.landing_page.current.field_nav_count_other',
|
|
|
|
|
{ count: navCount }
|
|
|
|
|
)
|
|
|
|
|
);
|
2026-04-12 08:58:41 +01:00
|
|
|
|
2026-05-18 03:47:22 +01:00
|
|
|
// Disabled if the current theme is paid and the org is on a free plan (legacy state).
|
|
|
|
|
const customizeLocked = $derived($isFreePlan && isPaidTheme(currentTheme));
|
2026-04-12 08:58:41 +01:00
|
|
|
|
2026-05-18 03:47:22 +01:00
|
|
|
const PREVIEW_RENDER_WIDTH = 1440;
|
|
|
|
|
let previewWidth = $state(0);
|
|
|
|
|
const previewScale = $derived(previewWidth > 0 ? previewWidth / PREVIEW_RENDER_WIDTH : 0.5);
|
2025-12-02 09:54:38 +01:00
|
|
|
</script>
|
|
|
|
|
|
2026-05-18 03:47:22 +01:00
|
|
|
<div class="w-full space-y-10 px-2 pb-12">
|
|
|
|
|
<section>
|
|
|
|
|
<div class="grid gap-6 lg:grid-cols-[minmax(0,1fr)_320px]">
|
|
|
|
|
<div
|
|
|
|
|
class="ui:border-border bg-background relative aspect-[16/10] w-full overflow-hidden rounded-xl border select-none"
|
|
|
|
|
bind:clientWidth={previewWidth}
|
|
|
|
|
>
|
|
|
|
|
<div
|
|
|
|
|
class="pointer-events-none absolute top-0 left-0 origin-top-left"
|
|
|
|
|
style="width: {PREVIEW_RENDER_WIDTH}px; height: {PREVIEW_RENDER_WIDTH *
|
|
|
|
|
0.625}px; transform: scale({previewScale});"
|
|
|
|
|
>
|
|
|
|
|
{#if $currentOrg.id}
|
|
|
|
|
<ThemeComponent {...previewProps} disableCourseLinks={true} />
|
|
|
|
|
{/if}
|
|
|
|
|
</div>
|
|
|
|
|
<div
|
|
|
|
|
class="from-background pointer-events-none absolute inset-x-0 bottom-0 h-24 bg-gradient-to-t to-transparent"
|
|
|
|
|
></div>
|
2026-04-12 08:58:41 +01:00
|
|
|
</div>
|
2025-12-02 09:54:38 +01:00
|
|
|
|
2026-05-18 03:47:22 +01:00
|
|
|
<Card.Root class="flex flex-col">
|
|
|
|
|
<Card.Header>
|
|
|
|
|
<Card.Title class="text-base">{$t('settings.landing_page.current.title')}</Card.Title>
|
|
|
|
|
<Card.Description>{$t('settings.landing_page.current.description')}</Card.Description>
|
|
|
|
|
</Card.Header>
|
|
|
|
|
<Card.Content class="space-y-3 pb-4">
|
|
|
|
|
<div class="flex items-center justify-between gap-3">
|
|
|
|
|
<span class="ui:text-muted-foreground text-xs tracking-wider uppercase">
|
|
|
|
|
{$t('settings.landing_page.current.field_theme')}
|
|
|
|
|
</span>
|
|
|
|
|
<Badge variant="secondary" class="text-[10px] uppercase">
|
|
|
|
|
{$t(currentThemeCard.titleKey)}
|
|
|
|
|
</Badge>
|
|
|
|
|
</div>
|
|
|
|
|
<div class="flex items-start justify-between gap-3">
|
|
|
|
|
<span class="ui:text-muted-foreground shrink-0 text-xs tracking-wider uppercase">
|
|
|
|
|
{$t('settings.landing_page.current.field_hero')}
|
|
|
|
|
</span>
|
|
|
|
|
<span class="line-clamp-1 text-right text-xs font-medium">{heroHeading}</span>
|
|
|
|
|
</div>
|
|
|
|
|
<div class="flex items-center justify-between gap-3">
|
|
|
|
|
<span class="ui:text-muted-foreground text-xs tracking-wider uppercase">
|
|
|
|
|
{$t('settings.landing_page.current.field_nav')}
|
|
|
|
|
</span>
|
|
|
|
|
<span class="text-xs font-medium">{navCountLabel}</span>
|
|
|
|
|
</div>
|
|
|
|
|
</Card.Content>
|
|
|
|
|
<Card.Footer class="mt-auto">
|
|
|
|
|
<Button
|
|
|
|
|
class="w-full justify-center"
|
|
|
|
|
href={customizeLocked ? undefined : customizeHref}
|
|
|
|
|
disabled={!$currentOrg.id}
|
|
|
|
|
onclick={customizeLocked ? () => openUpgradeModal() : undefined}
|
|
|
|
|
>
|
|
|
|
|
{#if customizeLocked}
|
|
|
|
|
<ZapIcon class="size-4" />
|
|
|
|
|
{/if}
|
|
|
|
|
{$t('settings.landing_page.current.customize')}
|
|
|
|
|
<ArrowRightIcon class="size-4" />
|
|
|
|
|
</Button>
|
|
|
|
|
</Card.Footer>
|
|
|
|
|
</Card.Root>
|
|
|
|
|
</div>
|
|
|
|
|
</section>
|
2025-12-02 09:54:38 +01:00
|
|
|
|
2026-05-18 03:47:22 +01:00
|
|
|
<section>
|
|
|
|
|
<div class="mb-6 flex items-start gap-4">
|
|
|
|
|
<div class="ui:bg-primary/10 ui:text-primary flex size-12 shrink-0 items-center justify-center rounded-lg">
|
|
|
|
|
<LayoutTemplateIcon class="size-6" />
|
|
|
|
|
</div>
|
|
|
|
|
<div>
|
|
|
|
|
<h2 class="text-lg font-semibold">{$t('settings.landing_page.gallery.heading')}</h2>
|
|
|
|
|
<p class="ui:text-muted-foreground text-sm">{$t('settings.landing_page.gallery.description')}</p>
|
|
|
|
|
</div>
|
2026-04-12 08:58:41 +01:00
|
|
|
</div>
|
2025-12-02 09:54:38 +01:00
|
|
|
|
2026-05-18 03:47:22 +01:00
|
|
|
<div class="grid gap-6 sm:grid-cols-2 lg:grid-cols-3">
|
|
|
|
|
{#each otherThemeCards as themeCard (themeCard.value)}
|
|
|
|
|
{@const isLocked = $isFreePlan && isPaidTheme(themeCard.value)}
|
|
|
|
|
<div class="ui:border-border bg-background relative overflow-hidden rounded-xl border">
|
|
|
|
|
{#if isLocked}
|
|
|
|
|
<div
|
2026-05-23 12:24:57 +01:00
|
|
|
class="absolute top-2 left-2 z-10 flex items-center gap-1 rounded-full bg-blue-600 px-2 py-0.5 text-xs font-medium text-white shadow-sm"
|
2026-05-18 03:47:22 +01:00
|
|
|
>
|
|
|
|
|
<PremiumIcon size={14} color="white" />
|
|
|
|
|
{$t('settings.landing_page.gallery.upgrade')}
|
2025-12-02 09:54:38 +01:00
|
|
|
</div>
|
2026-05-18 03:47:22 +01:00
|
|
|
{/if}
|
2026-05-23 12:24:57 +01:00
|
|
|
<DropdownMenu.Root>
|
|
|
|
|
<DropdownMenu.Trigger class="absolute top-2 right-2 z-10">
|
|
|
|
|
<IconButton variant="secondary" aria-label={$t('settings.landing_page.gallery.card_menu_label')}>
|
|
|
|
|
<EllipsisVerticalIcon size={16} />
|
|
|
|
|
</IconButton>
|
|
|
|
|
</DropdownMenu.Trigger>
|
|
|
|
|
<DropdownMenu.Content align="end">
|
|
|
|
|
<DropdownMenu.Item onclick={() => handlePreviewTheme(themeCard.value)}>
|
|
|
|
|
<EyeIcon size={16} class="mr-2" />
|
|
|
|
|
{$t('settings.landing_page.gallery.preview')}
|
|
|
|
|
</DropdownMenu.Item>
|
|
|
|
|
{#if !isLocked}
|
|
|
|
|
<DropdownMenu.Item onclick={() => handleApplyTheme(themeCard.value)}>
|
|
|
|
|
<PaintbrushIcon size={16} class="mr-2" />
|
|
|
|
|
{$t('settings.landing_page.gallery.apply_theme')}
|
|
|
|
|
</DropdownMenu.Item>
|
|
|
|
|
{/if}
|
|
|
|
|
</DropdownMenu.Content>
|
|
|
|
|
</DropdownMenu.Root>
|
2026-05-18 03:47:22 +01:00
|
|
|
<div class="bg-background aspect-[4/3] w-full overflow-hidden">
|
|
|
|
|
<img
|
|
|
|
|
src={themeCard.preview}
|
|
|
|
|
alt={$t(themeCard.titleKey)}
|
|
|
|
|
class="h-full w-full object-cover object-top"
|
|
|
|
|
loading="lazy"
|
|
|
|
|
/>
|
|
|
|
|
</div>
|
|
|
|
|
<div class="flex items-center justify-between gap-3 px-4 py-3">
|
|
|
|
|
<div class="min-w-0">
|
|
|
|
|
<p class="ui:text-primary truncate text-sm font-semibold">{$t(themeCard.titleKey)}</p>
|
|
|
|
|
<p class="ui:text-muted-foreground truncate text-xs">
|
|
|
|
|
{$t('settings.landing_page.gallery.by_classroomio')}
|
|
|
|
|
</p>
|
2025-12-02 09:54:38 +01:00
|
|
|
</div>
|
2026-05-18 03:47:22 +01:00
|
|
|
<Button variant="outline" size="sm" onclick={() => handleAddTheme(themeCard.value)}>
|
|
|
|
|
{$t('settings.landing_page.gallery.add')}
|
|
|
|
|
</Button>
|
|
|
|
|
</div>
|
|
|
|
|
</div>
|
|
|
|
|
{/each}
|
|
|
|
|
</div>
|
|
|
|
|
</section>
|
|
|
|
|
</div>
|
2026-05-23 12:24:57 +01:00
|
|
|
|
|
|
|
|
<ThemePreviewDialog />
|