feat(theme): animate public background with aurora and particles, polish landing pages

Add background_effect (aurora/particles), background_overlay tint and
opacity to the theme manager, rendered site-wide by ThemeVars on every
public page. Polish the home and register pages (hero mascot, live stat
pulse, date pills, photo strip, CTA band, theme-aware register intro,
i18n for home/register section).
This commit is contained in:
openhands committed 2026-09-18 12:44:39 +02:00
1 parent 5923b736fa
commit d131124515
9 files changed
+573 -25

No files matched your search

@@ -15,9 +15,12 @@ import {
} from "@/actions/theme-manager";
import { Button } from "@/components/ui/button";
import {
BACKGROUND_EFFECTS,
BACKGROUND_POSITIONS,
BACKGROUND_REPEATS,
BACKGROUND_SIZES,
OVERLAY_OPACITY_MAX,
OVERLAY_PRESETS,
} from "@/lib/theme-bg";
import type { CustomTheme } from "@/lib/theme-custom-store";
import { serializeThemeExport, swatchColors } from "@/lib/theme-manager-format";
@@ -28,6 +31,9 @@ export type ThemeManagerBackground = {
position: string;
size: string;
repeat: string;
effect: string;
overlay: string;
overlayOpacity: string;
};
interface ThemeManagerPanelProps {
@@ -133,6 +139,45 @@ function RenameInline({ theme }: { theme: CustomTheme }) {
);
}
function OverlayOpacityField({
defaultValue = "0",
}: {
defaultValue?: string;
}) {
const t = useTranslations(
"pages.housekeeping.domains.hotel.routes.themeManager",
);
const parsed = /^\d{1,2}$/.test(defaultValue)
? Math.min(Number(defaultValue), OVERLAY_OPACITY_MAX)
: 0;
const [value, setValue] = useState(parsed);
return (
<div className="flex flex-col gap-1 text-xs text-[var(--admin-text-muted)] sm:col-span-2 lg:col-span-2">
<label
htmlFor="background_overlay_opacity"
className="flex items-center justify-between"
>
<span>{t("backgroundOverlayOpacityLabel")}</span>
<span className="font-semibold text-[var(--admin-text)] tabular-nums">
{value}%
</span>
</label>
<input
id="background_overlay_opacity"
type="range"
name="background_overlay_opacity"
min={0}
max={OVERLAY_OPACITY_MAX}
step={5}
value={value}
onChange={(e) => setValue(Number(e.target.value))}
className="accent-[var(--admin-accent)] py-2"
style={{ border: "none", background: "transparent" }}
/>
</div>
);
}
export function ThemeManagerPanel({
themes,
current,
@@ -397,8 +442,20 @@ export function ThemeManagerPanel({
backgroundRepeat: background.repeat,
}
: {}),
...(background.overlay
? {
backgroundColor: `color-mix(in srgb, ${background.overlay} ${Number(background.overlayOpacity) || 0}%, transparent)`,
}
: {}),
}}
/>
{background.effect !== "none" ? (
<p className="mt-1.5 text-xs text-[var(--admin-accent)]">
{t(`backgroundEffectPreview`, {
effect: t(`backgroundEffect_${background.effect}`),
})}
</p>
) : null}
</div>
<form
@@ -461,6 +518,68 @@ export function ThemeManagerPanel({
))}
</select>
</label>
<label className="flex flex-col gap-1 text-xs text-[var(--admin-text-muted)] lg:col-span-2">
<span>{t("backgroundEffectLabel")}</span>
<select
name="background_effect"
defaultValue={background.effect}
className="px-3 py-2 rounded-lg border bg-[var(--admin-canvas)] text-sm text-[var(--admin-text)]"
style={{ borderColor: "var(--admin-border)" }}
>
{BACKGROUND_EFFECTS.map((value) => (
<option key={value} value={value}>
{t(`backgroundEffect_${value}`)}
</option>
))}
</select>
</label>
<div className="flex flex-col gap-1 text-xs text-[var(--admin-text-muted)] lg:col-span-2">
<label
htmlFor="background_overlay"
className="flex items-center justify-between"
>
<span>{t("backgroundOverlayLabel")}</span>
<input
id="background_overlay"
name="background_overlay"
type="color"
value={
/^#[0-9a-fA-F]{6}$/.test(background.overlay)
? background.overlay
: "#000000"
}
className="h-8 w-12 cursor-pointer rounded-md border bg-[var(--admin-canvas)] px-0.5"
style={{ borderColor: "var(--admin-border)" }}
/>
</label>
<div className="flex flex-wrap items-center gap-1.5">
{OVERLAY_PRESETS.map((preset) => (
<button
key={preset.label}
type="button"
title={preset.label}
onClick={() => {
const input = document.getElementById(
"background_overlay",
) as HTMLInputElement | null;
if (input) input.value = preset.value || "#000000";
}}
className="h-7 w-7 rounded-md border transition-transform hover:scale-110"
style={{
borderColor: "var(--admin-border)",
background:
preset.value ||
"linear-gradient(135deg, #64748b, #94a3b8)",
}}
/>
))}
</div>
</div>
<OverlayOpacityField defaultValue={background.overlayOpacity} />
<div className="sm:col-span-2 lg:col-span-4 flex flex-wrap gap-2">
<Button type="submit" size="sm">
{t("applyBackground")}