元旦节皮肤
|
After Width: | Height: | Size: 10 KiB |
|
After Width: | Height: | Size: 47 KiB |
|
After Width: | Height: | Size: 45 KiB |
@@ -0,0 +1,10 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 64 64" fill="none">
|
||||
<defs><linearGradient id="paper" x2=".7" y2="1"><stop stop-color="#fff7db"/><stop offset="1" stop-color="#e6c886"/></linearGradient></defs>
|
||||
<rect x="9" y="11" width="47" height="47" rx="7" fill="#9b6833"/>
|
||||
<rect x="7" y="8" width="47" height="47" rx="6" fill="url(#paper)" stroke="#f6d98f" stroke-width="2"/>
|
||||
<path d="M7 14a6 6 0 0 1 6-6h35a6 6 0 0 1 6 6v9H7Z" fill="#b84846"/>
|
||||
<path d="M19 4v9m23-9v9" stroke="#fff1c2" stroke-width="4" stroke-linecap="round"/>
|
||||
<text x="30.5" y="20" text-anchor="middle" fill="#fff8dc" font-family="Arial,sans-serif" font-size="9" font-weight="700" letter-spacing="2">JAN</text>
|
||||
<path d="m26 33 7-5v20m-7 0h14" stroke="#a83835" stroke-width="5" stroke-linecap="round" stroke-linejoin="round"/>
|
||||
<path d="m53 39 2 5 5 2-5 2-2 5-2-5-5-2 5-2Z" fill="#fff1b5" stroke="#d4a74f"/>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 903 B |
@@ -0,0 +1,9 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 64 64" fill="none">
|
||||
<defs><linearGradient id="gold" x2="1" y2="1"><stop stop-color="#fff4bb"/><stop offset=".5" stop-color="#d5a346"/><stop offset="1" stop-color="#ffe6a0"/></linearGradient></defs>
|
||||
<circle cx="32" cy="33" r="26" fill="url(#gold)"/>
|
||||
<circle cx="32" cy="33" r="21.5" fill="#142139" stroke="#fff0bd"/>
|
||||
<g stroke="#eac778" stroke-width="2.3" stroke-linecap="round"><path d="M32 15v3m0 30v3M14 33h3m30 0h3M19 20l2 2m22 22 2 2m0-26-2 2M21 44l-2 2"/></g>
|
||||
<path d="M32 19v14l-5-10" stroke="#fff2bc" stroke-width="3" stroke-linecap="round" stroke-linejoin="round"/>
|
||||
<circle cx="32" cy="33" r="3" fill="#ef9b73"/>
|
||||
<path d="m52 2 2 5 5 2-5 2-2 5-2-5-5-2 5-2Z" fill="#fff0b7"/>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 756 B |
@@ -0,0 +1,10 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 64 64" fill="none">
|
||||
<defs><linearGradient id="cone" x2="1" y2="1"><stop stop-color="#ffecaa"/><stop offset="1" stop-color="#ba752d"/></linearGradient></defs>
|
||||
<path d="m8 57 10-33 23 23Z" fill="url(#cone)" stroke="#ffe9ae" stroke-width="1.5" stroke-linejoin="round"/>
|
||||
<path d="m13 42 10 9m-7-18 17 15" stroke="#c65351" stroke-width="5"/>
|
||||
<ellipse cx="29.5" cy="35.5" rx="6.5" ry="16" transform="rotate(-45 29.5 35.5)" fill="#30233a" stroke="#f5d88b" stroke-width="2"/>
|
||||
<path d="M32 28q-1-13 8-12t8-10M37 33q5-10 16-8M27 23q-9-7-4-16" stroke="#f9d880" stroke-width="2.5" stroke-linecap="round"/>
|
||||
<path d="m47 35 5 4m-6 9 5-1M9 19l4-4m25-9 3-3" stroke="#e68686" stroke-width="3.5" stroke-linecap="round"/>
|
||||
<path d="m55 10 1.5 4 4 1.5-4 1.5-1.5 4-1.5-4-4-1.5 4-1.5Z" fill="#fff0b5"/>
|
||||
<circle cx="17" cy="6" r="2" fill="#a9cfe3"/><circle cx="59" cy="32" r="2" fill="#a9cfe3"/>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 947 B |
@@ -0,0 +1,11 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 64 64" fill="none">
|
||||
<defs><radialGradient id="gold"><stop stop-color="#fff6ce"/><stop offset="1" stop-color="#e6a848"/></radialGradient></defs>
|
||||
<g stroke="#f8d780" stroke-width="2.8" stroke-linecap="round">
|
||||
<path d="M31 7v9m0 23v7M8 27h10m24 0h11M14 10l8 9m18 16 7 8M48 10l-9 9M21 36l-9 8"/>
|
||||
<path d="m22 5 3 8m14-8-3 8M8 17l9 4m27 12 8 4M7 37l10-4m28-13 9-4" stroke-width="1.6"/>
|
||||
</g>
|
||||
<path d="m31 17 3 7 8 3-8 3-3 8-3-8-8-3 8-3Z" fill="url(#gold)"/>
|
||||
<path d="M31 43q-7 10-2 17m5-11q3 5 1 10" stroke="#e6a848" stroke-width="1.6" stroke-linecap="round"/>
|
||||
<g fill="#ef8c82"><circle cx="5" cy="8" r="2"/><circle cx="57" cy="42" r="2.5"/><circle cx="53" cy="4" r="1.8"/></g>
|
||||
<g fill="#fff4c5"><circle cx="5" cy="49" r="1.5"/><circle cx="59" cy="26" r="1.5"/><circle cx="22" cy="50" r="1.5"/></g>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 874 B |
@@ -0,0 +1,18 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 64 64" fill="none">
|
||||
<defs>
|
||||
<linearGradient id="hat" x1="10" y1="22" x2="45" y2="51" gradientUnits="userSpaceOnUse"><stop stop-color="#435574"/><stop offset=".45" stop-color="#162139"/><stop offset="1" stop-color="#090f1c"/></linearGradient>
|
||||
<linearGradient id="gold" x1="8" y1="24" x2="46" y2="58" gradientUnits="userSpaceOnUse"><stop stop-color="#fff1ba"/><stop offset=".45" stop-color="#ecc065"/><stop offset="1" stop-color="#b77826"/></linearGradient>
|
||||
</defs>
|
||||
<path d="m42 49 11-33" stroke="#efc66e" stroke-width="2.5" stroke-linecap="round"/>
|
||||
<path d="m52 5 1 7m0 9 1 5m-13-11 6 1m10 1 5 1M44 7l5 5m8 9 4 4m-2-18-3 5m-12 12 5-5" stroke="#ffe8a5" stroke-width="1.8" stroke-linecap="round"/>
|
||||
<path d="m53 10 2 5 5 2-5 2-2 5-2-5-5-2 5-2Z" fill="#fff4ca"/>
|
||||
<g transform="rotate(-12 28 40)">
|
||||
<ellipse cx="27" cy="51" rx="24" ry="7" fill="url(#hat)" stroke="url(#gold)" stroke-width="1.8"/>
|
||||
<path d="m12 24 3 26q12 7 25 0l3-26Z" fill="url(#hat)" stroke="url(#gold)" stroke-width="1.5"/>
|
||||
<path d="m14 40 1 8q12 7 25 0l1-8q-14 6-27 0Z" fill="url(#gold)"/>
|
||||
<ellipse cx="27.5" cy="24" rx="15.5" ry="5" fill="#263650" stroke="url(#gold)" stroke-width="1.5"/>
|
||||
<path d="m29 38 1.5 3 3.5.5-2.5 2.4.6 3.5L29 46l-3.1 1.4.6-3.5-2.5-2.4 3.5-.5Z" fill="#fff5d1"/>
|
||||
</g>
|
||||
<path d="m6 13 2-4m10 2 3 3m-15 23-3 2" stroke="#ef8d77" stroke-width="2.5" stroke-linecap="round"/>
|
||||
<circle cx="31" cy="9" r="1.5" fill="#f8dc93"/>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 1.5 KiB |
@@ -0,0 +1,6 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 64 64" fill="none">
|
||||
<path d="m22 59 10-30" stroke="#d8bb7c" stroke-width="3" stroke-linecap="round"/>
|
||||
<path d="m35 19 2 6 6 2-6 2-2 6-2-6-6-2 6-2Z" fill="#fff1c1"/>
|
||||
<path d="M35 9v5m0 26v4M17 27h5m25 0h5M22 14l4 4m18 18 4 4m0-27-4 5m-18 18-4 4" stroke="#e9c36d" stroke-width="2" stroke-linecap="round"/>
|
||||
<circle cx="16" cy="15" r="1.5" fill="#ef8c82"/><circle cx="53" cy="37" r="1.5" fill="#ef8c82"/>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 468 B |
@@ -1,7 +1,7 @@
|
||||
/* ============================================================
|
||||
BOTTOM NAV — Luxsin X9 Controller (Mobile)
|
||||
Design: iOS/Android App-style bottom tab bar
|
||||
3 个入口(社区/主页/我的 AI),文案来自 locales/bottomNav,语言跟随 deviceState.language
|
||||
5 tabs — labels from locales/bottomNav, language = deviceState.language (同系统页)
|
||||
- 社区:移动端进入 /community 引导复制链接;桌面(lg+)点击仍 window.open 论坛
|
||||
- Active tab: cyan icon + top indicator bar
|
||||
- Inactive: muted icon + label
|
||||
@@ -13,7 +13,13 @@ import { useIsLgUp } from "@/hooks/useIsLgUp";
|
||||
import localeZh from "@/locales/data-zh.json";
|
||||
import localeEn from "@/locales/data-en.json";
|
||||
import { resolveLocalePack } from "@/locales/resolveLocale";
|
||||
import { Bot, Home, MessageSquare } from "lucide-react";
|
||||
import {
|
||||
Activity,
|
||||
Bot,
|
||||
Home,
|
||||
MessageSquare,
|
||||
Sliders,
|
||||
} from "lucide-react";
|
||||
import { Link, useLocation } from "wouter";
|
||||
import { useMemo } from "react";
|
||||
import { bottomNavInnerClass, bottomNavOuterClass } from "@/lib/appShell";
|
||||
@@ -33,7 +39,7 @@ interface NavItem {
|
||||
const FORUM_URL = "https://forum.zidoo.tv/index.php";
|
||||
|
||||
const festivalIcon = (festival: FestivalId, name: string) =>
|
||||
`${import.meta.env.BASE_URL}images/festival/${festival}/${name}.png`;
|
||||
`${import.meta.env.BASE_URL}images/festival/${festival}/${name}.${festival === "newyear" ? "svg" : "png"}`;
|
||||
|
||||
const NAV_ITEMS: NavItem[] = [
|
||||
{
|
||||
@@ -43,6 +49,17 @@ const NAV_ITEMS: NavItem[] = [
|
||||
festivalIcons: {
|
||||
christmas: festivalIcon("christmas", "bell"),
|
||||
halloween: festivalIcon("halloween", "ghost"),
|
||||
newyear: festivalIcon("newyear", "confetti"),
|
||||
},
|
||||
},
|
||||
{
|
||||
path: "/eq",
|
||||
icon: Sliders,
|
||||
labelKey: "hpEq",
|
||||
festivalIcons: {
|
||||
christmas: festivalIcon("christmas", "reindeer"),
|
||||
halloween: festivalIcon("halloween", "skeleton"),
|
||||
newyear: festivalIcon("newyear", "clock"),
|
||||
},
|
||||
},
|
||||
{
|
||||
@@ -52,6 +69,17 @@ const NAV_ITEMS: NavItem[] = [
|
||||
festivalIcons: {
|
||||
christmas: festivalIcon("christmas", "santa"),
|
||||
halloween: festivalIcon("halloween", "pumpkin"),
|
||||
newyear: festivalIcon("newyear", "calendar"),
|
||||
},
|
||||
},
|
||||
{
|
||||
path: "/effects",
|
||||
icon: Activity,
|
||||
labelKey: "effects",
|
||||
festivalIcons: {
|
||||
christmas: festivalIcon("christmas", "tree"),
|
||||
halloween: festivalIcon("halloween", "witch"),
|
||||
newyear: festivalIcon("newyear", "fireworks"),
|
||||
},
|
||||
},
|
||||
{
|
||||
@@ -61,6 +89,7 @@ const NAV_ITEMS: NavItem[] = [
|
||||
festivalIcons: {
|
||||
christmas: festivalIcon("christmas", "gift"),
|
||||
halloween: festivalIcon("halloween", "vampire"),
|
||||
newyear: festivalIcon("newyear", "party-hat"),
|
||||
},
|
||||
},
|
||||
];
|
||||
@@ -78,7 +107,9 @@ export default function BottomNav() {
|
||||
}, [deviceState?.language]);
|
||||
|
||||
return (
|
||||
<nav className={bottomNavOuterClass}>
|
||||
<nav
|
||||
className={bottomNavOuterClass}
|
||||
>
|
||||
<div
|
||||
className={bottomNavInnerClass}
|
||||
style={{
|
||||
@@ -90,7 +121,7 @@ export default function BottomNav() {
|
||||
paddingBottom: "env(safe-area-inset-bottom, 8px)",
|
||||
}}
|
||||
>
|
||||
<div className="grid grid-cols-3 items-stretch">
|
||||
<div className="flex items-stretch justify-around">
|
||||
{NAV_ITEMS.map((item) => {
|
||||
const isActive =
|
||||
item.path === "/ai"
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import { useLayoutEffect, useRef, useState } from "react";
|
||||
import { FESTIVAL_GREETINGS, type FestivalId } from "@/lib/festival";
|
||||
import { FESTIVAL_GREETINGS, getNewYearDisplayYear, type FestivalId } from "@/lib/festival";
|
||||
import "./festival.css";
|
||||
import "./newyear.css";
|
||||
|
||||
/**
|
||||
* VU 区的静态插画横幅:两端角色保持比例,中央背景平铺,文字独立缩放。
|
||||
@@ -15,6 +16,8 @@ export default function FestivalGreeting({
|
||||
const textBoxRef = useRef<HTMLDivElement>(null);
|
||||
const innerRef = useRef<HTMLDivElement>(null);
|
||||
const [scale, setScale] = useState(1);
|
||||
const [newYear] = useState(() => getNewYearDisplayYear());
|
||||
const isNewYear = festival === "newyear";
|
||||
const artworkRoot = `${import.meta.env.BASE_URL}images/festival/${festival}/`;
|
||||
|
||||
useLayoutEffect(() => {
|
||||
@@ -60,7 +63,7 @@ export default function FestivalGreeting({
|
||||
ref={boxRef}
|
||||
className={`festival-greeting festival-greeting--${festival}`}
|
||||
role="img"
|
||||
aria-label={FESTIVAL_GREETINGS[festival]}
|
||||
aria-label={isNewYear ? `${FESTIVAL_GREETINGS[festival]} ${newYear}` : FESTIVAL_GREETINGS[festival]}
|
||||
>
|
||||
<div
|
||||
className="festival-greeting-backdrop"
|
||||
@@ -78,6 +81,15 @@ export default function FestivalGreeting({
|
||||
/>
|
||||
))}
|
||||
<div className="festival-greeting-shade" aria-hidden="true" />
|
||||
{isNewYear && (
|
||||
<img
|
||||
src={`${artworkRoot}calendar.svg`}
|
||||
className="newyear-greeting-calendar"
|
||||
alt=""
|
||||
aria-hidden="true"
|
||||
draggable={false}
|
||||
/>
|
||||
)}
|
||||
<div
|
||||
ref={textBoxRef}
|
||||
className="festival-greeting-title"
|
||||
@@ -88,7 +100,20 @@ export default function FestivalGreeting({
|
||||
className={`${festival}-greeting-text festival-greeting-inner`}
|
||||
style={{ transform: `scale(${scale})` }}
|
||||
>
|
||||
{FESTIVAL_GREETINGS[festival].split(" ").map(word => (
|
||||
{isNewYear ? (
|
||||
<>
|
||||
<div className="newyear-greeting-heading">
|
||||
<div className="newyear-greeting-words">
|
||||
<span>Happy</span>
|
||||
<span>New Year</span>
|
||||
</div>
|
||||
<strong className="newyear-greeting-year">{newYear}</strong>
|
||||
</div>
|
||||
<span className="newyear-greeting-tagline">
|
||||
NEW YEAR · NEW CHAPTER · MORE POSSIBILITIES
|
||||
</span>
|
||||
</>
|
||||
) : FESTIVAL_GREETINGS[festival].split(" ").map(word => (
|
||||
<span key={word} className="festival-greeting-line">
|
||||
{word}
|
||||
</span>
|
||||
|
||||
@@ -0,0 +1,58 @@
|
||||
import { memo } from "react";
|
||||
import { cn } from "@/lib/utils";
|
||||
import "./newyear.css";
|
||||
|
||||
const ROOT = `${import.meta.env.BASE_URL}images/festival/newyear/`;
|
||||
|
||||
/** 新年礼帽与仙女棒只替换视觉层,拖拽命中和音量提交仍由滑块管理。 */
|
||||
export const NewYearVolumeThumb = memo(function NewYearVolumeThumb() {
|
||||
return (
|
||||
<img
|
||||
src={`${ROOT}party-hat.svg`}
|
||||
alt=""
|
||||
draggable={false}
|
||||
className="newyear-volume-thumb"
|
||||
/>
|
||||
);
|
||||
});
|
||||
|
||||
/** 两端占位仍为 24px:最小音量是小仙女棒,最大音量是绽放的烟花。 */
|
||||
export function NewYearVolumeIcon({ dim = false }: { dim?: boolean }) {
|
||||
return (
|
||||
<span className="newyear-volume-icon-slot" aria-hidden="true">
|
||||
<img
|
||||
src={`${ROOT}${dim ? "sparkler" : "fireworks"}.svg`}
|
||||
alt=""
|
||||
draggable={false}
|
||||
className={cn("newyear-volume-icon", dim && "newyear-volume-icon--dim")}
|
||||
/>
|
||||
</span>
|
||||
);
|
||||
}
|
||||
|
||||
/** 卡片顶部的金色彩带与星屑,不覆盖端口、不拦截指针事件。 */
|
||||
export const NewYearCardDecoration = memo(function NewYearCardDecoration() {
|
||||
return (
|
||||
<div className="newyear-card-decoration" aria-hidden="true">
|
||||
<svg viewBox="0 0 600 20" preserveAspectRatio="none" focusable="false">
|
||||
<path
|
||||
d="M0 2Q150 19 300 5T600 2"
|
||||
fill="none"
|
||||
stroke="#e7bd6b"
|
||||
strokeOpacity="0.35"
|
||||
strokeWidth="0.7"
|
||||
vectorEffect="non-scaling-stroke"
|
||||
/>
|
||||
</svg>
|
||||
{[7, 20, 35, 51, 67, 82, 95].map((left, index) => (
|
||||
<span
|
||||
key={left}
|
||||
className="newyear-card-star"
|
||||
style={{ left: `${left}%`, top: index % 2 ? 7 : 2 }}
|
||||
>
|
||||
{index % 2 ? "·" : "✦"}
|
||||
</span>
|
||||
))}
|
||||
</div>
|
||||
);
|
||||
});
|
||||
@@ -0,0 +1,152 @@
|
||||
/* 元旦视觉层:保留滑轨尺寸、青色音量反馈与所有交互热区。 */
|
||||
.newyear-volume-thumb {
|
||||
display: block;
|
||||
width: 44px;
|
||||
height: 44px;
|
||||
max-width: none;
|
||||
object-fit: contain;
|
||||
pointer-events: none;
|
||||
user-select: none;
|
||||
filter: drop-shadow(0 2px 4px #000) drop-shadow(0 0 3px rgba(242, 196, 98, 0.35));
|
||||
}
|
||||
|
||||
.newyear-volume-icon-slot {
|
||||
display: block;
|
||||
position: relative;
|
||||
width: 24px;
|
||||
height: 24px;
|
||||
pointer-events: none;
|
||||
}
|
||||
|
||||
.newyear-volume-icon {
|
||||
position: absolute;
|
||||
left: 50%;
|
||||
top: 50%;
|
||||
width: 32px;
|
||||
height: 32px;
|
||||
max-width: none;
|
||||
transform: translate(-50%, -50%);
|
||||
filter: drop-shadow(0 0 3px rgba(242, 196, 98, 0.3));
|
||||
pointer-events: none;
|
||||
user-select: none;
|
||||
}
|
||||
|
||||
.newyear-volume-icon--dim {
|
||||
width: 27px;
|
||||
height: 27px;
|
||||
opacity: 0.75;
|
||||
}
|
||||
|
||||
.newyear-card-decoration {
|
||||
position: absolute;
|
||||
inset: 0 0 auto;
|
||||
height: 20px;
|
||||
z-index: 1;
|
||||
overflow: hidden;
|
||||
pointer-events: none;
|
||||
user-select: none;
|
||||
}
|
||||
|
||||
.newyear-card-decoration * {
|
||||
pointer-events: none;
|
||||
}
|
||||
|
||||
.newyear-card-decoration > svg {
|
||||
width: 100%;
|
||||
height: 20px;
|
||||
}
|
||||
|
||||
.newyear-card-star {
|
||||
position: absolute;
|
||||
color: #e7bd6b;
|
||||
opacity: 0.75;
|
||||
font-size: 9px;
|
||||
line-height: 1;
|
||||
}
|
||||
|
||||
.festival-greeting--newyear {
|
||||
background: #071221;
|
||||
}
|
||||
|
||||
.festival-greeting--newyear .festival-greeting-art {
|
||||
opacity: 0.75;
|
||||
}
|
||||
|
||||
.festival-greeting--newyear .festival-greeting-shade {
|
||||
background: linear-gradient(90deg, transparent, rgba(3, 9, 20, 0.3) 40%, rgba(3, 9, 20, 0.3) 60%, transparent);
|
||||
}
|
||||
|
||||
/* 标题整体仍由共享组件测量缩放,窄屏按三行排列,宽屏增加副标题。 */
|
||||
.festival-greeting.festival-greeting--newyear[data-layout] .newyear-greeting-text {
|
||||
flex-direction: column;
|
||||
color: #ffe8ae;
|
||||
text-shadow: 0 1px 1px #71390d, 0 0 8px rgba(236, 145, 52, 0.6);
|
||||
}
|
||||
|
||||
.newyear-greeting-heading,
|
||||
.newyear-greeting-words {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.newyear-greeting-words {
|
||||
font-family: "Snell Roundhand", "Apple Chancery", "URW Chancery L", Georgia, cursive;
|
||||
font-size: 36px;
|
||||
font-style: italic;
|
||||
line-height: 1.05;
|
||||
}
|
||||
|
||||
.newyear-greeting-year {
|
||||
font-family: Georgia, "Times New Roman", serif;
|
||||
font-size: 43px;
|
||||
font-style: italic;
|
||||
font-weight: 700;
|
||||
letter-spacing: 0.03em;
|
||||
line-height: 1;
|
||||
color: #ffd57a;
|
||||
}
|
||||
|
||||
.newyear-greeting-tagline {
|
||||
display: none;
|
||||
font-family: Georgia, "Times New Roman", serif;
|
||||
font-size: 8px;
|
||||
line-height: 1.6;
|
||||
letter-spacing: 2px;
|
||||
color: #f5da9b;
|
||||
text-shadow: 0 1px 3px #000;
|
||||
}
|
||||
|
||||
.newyear-greeting-calendar {
|
||||
display: none;
|
||||
position: absolute;
|
||||
width: 48px;
|
||||
height: 48px;
|
||||
left: 22px;
|
||||
bottom: 4px;
|
||||
transform: rotate(-9deg);
|
||||
filter: drop-shadow(0 2px 4px #000);
|
||||
}
|
||||
|
||||
.festival-greeting--newyear[data-layout="wide"] .newyear-greeting-calendar {
|
||||
display: block;
|
||||
}
|
||||
|
||||
.festival-greeting--newyear[data-layout="wide"] .newyear-greeting-heading,
|
||||
.festival-greeting--newyear[data-layout="wide"] .newyear-greeting-words {
|
||||
flex-direction: row;
|
||||
}
|
||||
|
||||
.festival-greeting--newyear[data-layout="wide"] .newyear-greeting-words > span + span {
|
||||
margin-left: 0.22em;
|
||||
}
|
||||
|
||||
.festival-greeting--newyear[data-layout="wide"] .newyear-greeting-year {
|
||||
margin-left: 18px;
|
||||
font-size: 54px;
|
||||
}
|
||||
|
||||
.festival-greeting--newyear[data-layout="wide"] .newyear-greeting-tagline {
|
||||
display: block;
|
||||
margin-top: 4px;
|
||||
}
|
||||
@@ -6,7 +6,7 @@
|
||||
============================================================ */
|
||||
import { useMemo } from "react";
|
||||
|
||||
export type FestivalId = "christmas" | "halloween";
|
||||
export type FestivalId = "christmas" | "halloween" | "newyear";
|
||||
|
||||
/** Inclusive [month, day] windows. Christmas: Dec 20 – Dec 26; Halloween: Oct 26 – Nov 1. */
|
||||
const FESTIVAL_WINDOWS: Record<
|
||||
@@ -15,6 +15,8 @@ const FESTIVAL_WINDOWS: Record<
|
||||
> = {
|
||||
christmas: { from: [12, 20], to: [12, 26] },
|
||||
halloween: { from: [10, 26], to: [11, 1] },
|
||||
// 元旦接续圣诞节,跨年窗口包含首尾日期。
|
||||
newyear: { from: [12, 27], to: [1, 3] },
|
||||
};
|
||||
|
||||
function inWindow(
|
||||
@@ -55,8 +57,14 @@ export function useFestival(): FestivalId | null {
|
||||
return useMemo(() => getFestival(), []);
|
||||
}
|
||||
|
||||
/** 跨年夜展示即将到来的年份,其余月份预览展示当年。 */
|
||||
export function getNewYearDisplayYear(now: Date = new Date()): number {
|
||||
return now.getFullYear() + (now.getMonth() === 11 ? 1 : 0);
|
||||
}
|
||||
|
||||
/** 首页 VU 区的节日问候语,键与 FestivalId 对应。 */
|
||||
export const FESTIVAL_GREETINGS: Record<FestivalId, string> = {
|
||||
christmas: "Merry Christmas",
|
||||
halloween: "Happy Halloween",
|
||||
newyear: "Happy New Year",
|
||||
};
|
||||
|
||||
@@ -20,6 +20,11 @@ import HalloweenVolumeThumb from "@/components/festival/HalloweenVolumeThumb";
|
||||
import HalloweenPumpkinIcon from "@/components/festival/HalloweenPumpkinIcon";
|
||||
import HalloweenCardDecoration from "@/components/festival/HalloweenCardDecoration";
|
||||
import FestivalGreeting from "@/components/festival/FestivalGreeting";
|
||||
import {
|
||||
NewYearCardDecoration,
|
||||
NewYearVolumeIcon,
|
||||
NewYearVolumeThumb,
|
||||
} from "@/components/festival/NewYearDecoration";
|
||||
import "@/components/festival/christmas.css";
|
||||
import "@/components/festival/halloween.css";
|
||||
import "@/components/festival/festival.css";
|
||||
@@ -200,6 +205,7 @@ export default function Home() {
|
||||
const festival = useFestival();
|
||||
const isChristmas = festival === "christmas";
|
||||
const isHalloween = festival === "halloween";
|
||||
const isNewYear = festival === "newyear";
|
||||
const powerActionRef = useRef(false);
|
||||
const [powerConfirmOpen, setPowerConfirmOpen] = useState(false);
|
||||
const [volumeConfirm, setVolumeConfirm] = useState<null | "min" | "max">(null);
|
||||
@@ -466,6 +472,7 @@ export default function Home() {
|
||||
style={{ background: "rgba(18,18,20,0.8)", minHeight: 140 }}>
|
||||
{isChristmas && <ChristmasCardDecoration />}
|
||||
{isHalloween && <HalloweenCardDecoration />}
|
||||
{isNewYear && <NewYearCardDecoration />}
|
||||
{/* Stylized device illustration */}
|
||||
<div className="w-full relative" style={{ height: 140 }}>
|
||||
{/* Device body */}
|
||||
@@ -613,6 +620,8 @@ export default function Home() {
|
||||
<ChristmasBellIcon dim />
|
||||
) : isHalloween ? (
|
||||
<HalloweenPumpkinIcon dim />
|
||||
) : isNewYear ? (
|
||||
<NewYearVolumeIcon dim />
|
||||
) : (
|
||||
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="1.8" className="w-6 h-6">
|
||||
<polygon points="11 5 6 9 2 9 2 15 6 15 11 19 11 5"/>
|
||||
@@ -655,7 +664,9 @@ export default function Home() {
|
||||
? () => <ChristmasVolumeThumb />
|
||||
: isHalloween
|
||||
? () => <HalloweenVolumeThumb />
|
||||
: undefined
|
||||
: isNewYear
|
||||
? () => <NewYearVolumeThumb />
|
||||
: undefined
|
||||
}
|
||||
/>
|
||||
</div>
|
||||
@@ -673,6 +684,8 @@ export default function Home() {
|
||||
<ChristmasBellIcon />
|
||||
) : isHalloween ? (
|
||||
<HalloweenPumpkinIcon />
|
||||
) : isNewYear ? (
|
||||
<NewYearVolumeIcon />
|
||||
) : (
|
||||
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="1.8" className="w-6 h-6">
|
||||
<polygon points="11 5 6 9 2 9 2 15 6 15 11 19 11 5"/>
|
||||
|
||||