feat: nový vzhled Hatsune Miku
CI / Generate TypeScript types (push) Successful in 10s
CI / Server unit tests (push) Successful in 26s
CI / Build server (push) Successful in 28s
CI / Build client (push) Successful in 42s
CI / Playwright E2E tests (push) Successful in 1m54s
CI / Build and push Docker image (push) Successful in 45s
CI / Notify (push) Successful in 2s

This commit is contained in:
mates committed 2026-10-26 08:45:35 +01:00
1 parent 7da5b99f0c
commit f042a0b831
10 files changed
+931 -6

No files matched your search

+2 -2
View File
@@ -11,7 +11,7 @@
<link rel="manifest" href="/manifest.json" /> <link rel="manifest" href="/manifest.json" />
<link rel="preconnect" href="https://fonts.googleapis.com" /> <link rel="preconnect" href="https://fonts.googleapis.com" />
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin /> <link rel="preconnect" href="https://fonts.gstatic.com" crossorigin />
<link href="https://fonts.googleapis.com/css2?family=M+PLUS+Rounded+1c:wght@500;800&family=Nunito:wght@600;800;900&family=Fredoka:wght@500;600;700&family=Shippori+Mincho:wght@600;700;800&family=Zen+Maru+Gothic:wght@500;700;900&family=Yusei+Magic&family=Dela+Gothic+One&family=Barlow+Condensed:wght@500;600;700&family=Kiwi+Maru:wght@400;500&family=Playfair+Display:ital,wght@0,800;1,800&family=Courier+Prime:wght@700&family=Cormorant+Garamond:wght@600;700&family=Baloo+2:wght@600;800&family=Metal+Mania&family=Klee+One:wght@600&display=swap" rel="stylesheet" /> <link href="https://fonts.googleapis.com/css2?family=M+PLUS+Rounded+1c:wght@500;800&family=Nunito:wght@600;800;900&family=Fredoka:wght@500;600;700&family=Shippori+Mincho:wght@600;700;800&family=Zen+Maru+Gothic:wght@500;700;900&family=Yusei+Magic&family=Dela+Gothic+One&family=Barlow+Condensed:wght@500;600;700&family=Kiwi+Maru:wght@400;500&family=Playfair+Display:ital,wght@0,800;1,800&family=Courier+Prime:wght@700&family=Cormorant+Garamond:wght@600;700&family=Baloo+2:wght@600;800&family=Metal+Mania&family=Klee+One:wght@600&family=Exo+2:wght@700;800&display=swap" rel="stylesheet" />
<title>Luncher</title> <title>Luncher</title>
<script> <script>
(function() { (function() {
@@ -28,7 +28,7 @@
} }
document.documentElement.setAttribute('data-bs-theme', theme); document.documentElement.setAttribute('data-bs-theme', theme);
var style = localStorage.getItem('style_theme'); var style = localStorage.getItem('style_theme');
var styles = ['umamusume', 'mlp', 'hellgirl', 'kon', 'bocchi', 'evangelion', 'yurucamp', 'spyfamily', 'apothecary', 'frieren', 'mlpg5', 'aggretsuko', 'sumikko', 'gudetama']; var styles = ['umamusume', 'mlp', 'hellgirl', 'kon', 'bocchi', 'evangelion', 'yurucamp', 'spyfamily', 'apothecary', 'frieren', 'mlpg5', 'aggretsuko', 'sumikko', 'gudetama', 'miku'];
document.documentElement.setAttribute('data-luncher-theme', styles.indexOf(style) >= 0 ? style : 'simple'); document.documentElement.setAttribute('data-luncher-theme', styles.indexOf(style) >= 0 ? style : 'simple');
} catch (e) { } catch (e) {
// Fallback pokud localStorage není dostupný // Fallback pokud localStorage není dostupný
+1
View File
@@ -24,6 +24,7 @@ import './themes/mlpg5.scss';
import './themes/aggretsuko.scss'; import './themes/aggretsuko.scss';
import './themes/sumikko.scss'; import './themes/sumikko.scss';
import './themes/gudetama.scss'; import './themes/gudetama.scss';
import './themes/miku.scss';
import { faCircleCheck, faNoteSticky, faTrashCan, faComment } from '@fortawesome/free-regular-svg-icons'; import { faCircleCheck, faNoteSticky, faTrashCan, faComment } from '@fortawesome/free-regular-svg-icons';
import { useSettings } from './context/settings'; import { useSettings } from './context/settings';
import Footer from './components/Footer'; import Footer from './components/Footer';
+11
View File
@@ -175,6 +175,17 @@
box-shadow: 0 0 0 2px #f5b301; box-shadow: 0 0 0 2px #f5b301;
} }
// Styl Hatsune Miku: emoji v bílém kroužku s tyrkysovou září
.eater-badge-miku {
display: inline-grid;
place-items: center;
font-size: 15px;
line-height: 1;
border-color: #fff;
background: radial-gradient(circle at 40% 35%, #ffffff, #dff5f3);
box-shadow: 0 0 0 2px #39c5bb, 0 0 8px rgb(57 197 187 / 0.7);
}
// Odznak a jméno drží na jednom řádku (úzký sloupec „Člen" na stránce objednávek) // Odznak a jméno drží na jednom řádku (úzký sloupec „Člen" na stránce objednávek)
.user-info:has(.eater-badge) { .user-info:has(.eater-badge) {
display: inline-flex; display: inline-flex;
+8 -1
View File
@@ -91,6 +91,12 @@ const GT_LEVELS = [
{ minItems: 2, emoji: '🥓', title: 'Úroveň slaniny — k obědu ještě plátek slaniny… ale jen jako deka' }, { minItems: 2, emoji: '🥓', title: 'Úroveň slaniny — k obědu ještě plátek slaniny… ale jen jako deka' },
]; ];
/** Úrovně ve stylu Hatsune Miku — emoji v kroužku s tyrkysovou září. Seřazeno od nejvyšší úrovně. */
const MIKU_LEVELS = [
{ minItems: 3, emoji: '🍨', title: 'Úroveň Kaito — po obědě ještě zmrzlina, klidně i v zimě' },
{ minItems: 2, emoji: '🥬', title: 'Úroveň Miku — k obědu ještě pórek, ať se dá točit nad hlavou' },
];
/** Úrovně podle stylu pro odznaky s emoji. */ /** Úrovně podle stylu pro odznaky s emoji. */
const EMOJI_LEVELS: Partial<Record<StyleTheme, typeof MLP_LEVELS>> = { const EMOJI_LEVELS: Partial<Record<StyleTheme, typeof MLP_LEVELS>> = {
mlp: MLP_LEVELS, mlp: MLP_LEVELS,
@@ -106,6 +112,7 @@ const EMOJI_LEVELS: Partial<Record<StyleTheme, typeof MLP_LEVELS>> = {
aggretsuko: AGG_LEVELS, aggretsuko: AGG_LEVELS,
sumikko: SK_LEVELS, sumikko: SK_LEVELS,
gudetama: GT_LEVELS, gudetama: GT_LEVELS,
miku: MIKU_LEVELS,
}; };
type Props = { type Props = {
@@ -113,7 +120,7 @@ type Props = {
items: number, items: number,
}; };
/** Odznak s postavou u strávníků s více jídly — jen ve stylech Umamusume, My Little Pony, Hell Girl, K-ON!, Bocchi the Rock!, Evangelion, Yuru Camp△, Spy × Family, Apothecary Diaries, Frieren, My Little Pony G5, Aggretsuko, Sumikko Gurashi a Gudetama. */ /** Odznak s postavou u strávníků s více jídly — jen ve stylech Umamusume, My Little Pony, Hell Girl, K-ON!, Bocchi the Rock!, Evangelion, Yuru Camp△, Spy × Family, Apothecary Diaries, Frieren, My Little Pony G5, Aggretsuko, Sumikko Gurashi, Gudetama a Hatsune Miku. */
export default function EaterBadge({ items }: Readonly<Props>) { export default function EaterBadge({ items }: Readonly<Props>) {
const styleTheme = useSettings()?.styleTheme; const styleTheme = useSettings()?.styleTheme;
const isUma = styleTheme === 'umamusume'; const isUma = styleTheme === 'umamusume';
+84
View File
@@ -1191,3 +1191,87 @@
} }
} }
} }
// ============================================
// Konfety ve stylu Hatsune Miku
// ============================================
.confetti-miku {
filter: drop-shadow(0 0 6px rgb(57 197 187 / 0.6)) drop-shadow(0 3px 3px rgb(0 0 0 / 0.2));
}
// Štítky jako noty z piano rollu — zaoblený obdélník se světlejším horním okrajem
.confetti-miku-badge {
padding: 3px 14px 4px;
border-radius: 6px;
border: 1px solid rgb(255 255 255 / 0.5);
box-shadow: inset 0 2px 0 rgb(255 255 255 / 0.3);
color: #fff;
font-family: 'Exo 2', 'Nunito', sans-serif;
font-weight: 800;
font-size: 1.15rem;
white-space: nowrap;
}
// Nápis „♪ 初音ミク ♪" — velkoplošná obrazovka na koncertě, světla pulzují do rytmu
.confetti-miku-banner {
position: absolute;
top: 38%;
left: 50%;
transform: translate(-50%, -50%);
padding: 16px 48px 22px;
font-family: 'Exo 2', 'Nunito', sans-serif;
font-weight: 800;
font-size: clamp(2rem, 7vw, 4.2rem);
letter-spacing: 0.04em;
white-space: nowrap;
color: #e9fffd;
text-shadow: 0 0 10px #39c5bb, 0 0 26px #39c5bb;
background:
url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='98' height='10'%3E%3Crect width='98' height='10' fill='%23ffffff'/%3E%3Cpath d='M14 0V10M28 0V10M42 0V10M56 0V10M70 0V10M84 0V10M98 0V10' stroke='%239aa3a8'/%3E%3Cpath d='M10 0h8v6h-8zM24 0h8v6h-8zM52 0h8v6h-8zM66 0h8v6h-8zM80 0h8v6h-8z' fill='%232b2f33'/%3E%3C/svg%3E") left bottom / 98px 10px repeat-x,
#0b0d10;
border: 2px solid #39c5bb;
border-radius: 10px;
box-shadow: 0 0 0 4px rgb(225 40 133 / 0.5), 0 0 40px rgb(57 197 187 / 0.6);
animation: miku-banner 2.6s ease both;
}
@keyframes miku-banner {
0% { transform: translate(-50%, -50%) scale(0.4); opacity: 0; }
12% { transform: translate(-50%, -50%) scale(1.05); opacity: 1; }
18%, 80% { transform: translate(-50%, -50%) scale(1); opacity: 1; }
// Pulz do rytmu
30%, 54% { box-shadow: 0 0 0 4px rgb(225 40 133 / 0.8), 0 0 60px rgb(57 197 187 / 0.9); }
42%, 66% { box-shadow: 0 0 0 4px rgb(225 40 133 / 0.4), 0 0 30px rgb(57 197 187 / 0.5); }
100% { transform: translate(-50%, -50%) scale(0.92); opacity: 0; }
}
// Vocaloid s hláškou — kruh s tyrkysovou září a bublina jako okno editoru
.confetti-miku-voice {
.confetti-mlp-avatar {
border: 3px solid #fff;
background: radial-gradient(circle at 40% 35%, #ffffff, #dff5f3);
box-shadow: 0 0 0 3px var(--mlp-speaker, #39c5bb), 0 0 18px rgb(57 197 187 / 0.6);
}
.confetti-mlp-bubble {
background: #ffffff;
color: #263036;
border: 1px solid #d8e3e6;
border-top: 4px solid var(--mlp-speaker, #39c5bb);
border-radius: 10px;
box-shadow: 0 8px 20px rgb(0 0 0 / 0.25);
font-family: 'Nunito', sans-serif;
p {
font-weight: 700;
}
small {
font-family: 'Exo 2', 'Nunito', sans-serif;
}
&::before {
border-right-color: #d8e3e6;
}
}
}
+17 -1
View File
@@ -211,6 +211,21 @@ const GT_SPEAKERS = [
{ name: 'Gudetama v ramenu', emoji: '🍜', color: '#d9a066', quote: 'Plavu si tu v ramenu… vývar je teplý, nechce se mi ven.' }, { name: 'Gudetama v ramenu', emoji: '🍜', color: '#d9a066', quote: 'Plavu si tu v ramenu… vývar je teplý, nechce se mi ven.' },
{ name: 'Gudetama na rýži', emoji: '🍚', color: '#a3927c', quote: 'Na rýži se leží nejlíp… itadakimasu… zítra.' }, { name: 'Gudetama na rýži', emoji: '🍚', color: '#a3927c', quote: 'Na rýži se leží nejlíp… itadakimasu… zítra.' },
]; ];
const MIKU_EMOJI = ['🎵', '🎶', '🎤', '🥬', '💙', '🎹', '🎧', '✨', '🍊', '🍌', '🐟', '🍨', '🌟', '🎼'];
// Hlášky z fandomu Vocaloidů (Ievan Polkka, World is Mine, Senbonzakura, 39 = „děkuji"...)
const MIKU_BADGES = ['初音ミク', '01', 'Ievan Polkka', 'World is Mine', 'Senbonzakura', 'Magical Mirai', 'Pórek!', '39!', 'Miku Miku Beam!', 'Přídavek!'];
const MIKU_BADGE_COLORS = ['#39c5bb', '#138f87', '#e12885', '#3a3f44', '#e0245e', '#3a6fd8'];
/** Vocaloidi (a Teto) s „hláškou" po finálním výsledku. */
const MIKU_SPEAKERS = [
{ name: 'Hatsune Miku', emoji: '🥬', color: '#39c5bb', quote: 'To zní skvěle! Po obědě vám zazpívám — a přinesu pórek!' },
{ name: 'Kagamine Rin', emoji: '🍊', color: '#f2a900', quote: 'Já chci tohle! A pak pomeranč. Hned!' },
{ name: 'Kagamine Len', emoji: '🍌', color: '#d9b31a', quote: 'Dobře, souhlasím… ale banán si dám jako dezert.' },
{ name: 'Megurine Luka', emoji: '🐟', color: '#e88aa8', quote: 'Elegantní volba. Nemají tam náhodou tuňáka?' },
{ name: 'Kaito', emoji: '🍨', color: '#3a6fd8', quote: 'Super! A po obědě zmrzlina. Jo, i když je říjen.' },
{ name: 'Meiko', emoji: '🍶', color: '#c8302c', quote: 'Výborně, a k tomu jedno saké… je přece po práci, ne? …Není?' },
{ name: 'GUMI', emoji: '🥕', color: '#6fbf3a', quote: 'Mrkvičku si k tomu dám taky! Ať mám hlas jako zvonek.' },
{ name: 'Kasane Teto', emoji: '🥖', color: '#d9405e', quote: 'Já si dám bagetu. Jako vždycky. Jsem přece Teto!' },
];
/** Styly, jejichž konfety tvoří emoji a samolepky s hláškami (a po finále promluví postava s emoji). */ /** Styly, jejichž konfety tvoří emoji a samolepky s hláškami (a po finále promluví postava s emoji). */
const EMOJI_CONFETTI: Partial<Record<WheelTheme, { const EMOJI_CONFETTI: Partial<Record<WheelTheme, {
prefix: string, badges: string[], colors: string[], emoji: string[], prefix: string, badges: string[], colors: string[], emoji: string[],
@@ -230,6 +245,7 @@ const EMOJI_CONFETTI: Partial<Record<WheelTheme, {
aggretsuko: { prefix: 'confetti-agg', badges: AGG_BADGES, colors: AGG_BADGE_COLORS, emoji: AGG_EMOJI, speakers: AGG_SPEAKERS, banner: '🤘 RAAAAAH!!! 🎤' }, aggretsuko: { prefix: 'confetti-agg', badges: AGG_BADGES, colors: AGG_BADGE_COLORS, emoji: AGG_EMOJI, speakers: AGG_SPEAKERS, banner: '🤘 RAAAAAH!!! 🎤' },
sumikko: { prefix: 'confetti-sk', badges: SK_BADGES, colors: SK_BADGE_COLORS, emoji: SK_EMOJI, speakers: SK_SPEAKERS, banner: '🍤 すみっコぐらし 🐧' }, sumikko: { prefix: 'confetti-sk', badges: SK_BADGES, colors: SK_BADGE_COLORS, emoji: SK_EMOJI, speakers: SK_SPEAKERS, banner: '🍤 すみっコぐらし 🐧' },
gudetama: { prefix: 'confetti-gt', badges: GT_BADGES, colors: GT_BADGE_COLORS, emoji: GT_EMOJI, speakers: GT_SPEAKERS, banner: '🍳 ぐでたま… 🥓' }, gudetama: { prefix: 'confetti-gt', badges: GT_BADGES, colors: GT_BADGE_COLORS, emoji: GT_EMOJI, speakers: GT_SPEAKERS, banner: '🍳 ぐでたま… 🥓' },
miku: { prefix: 'confetti-miku', badges: MIKU_BADGES, colors: MIKU_BADGE_COLORS, emoji: MIKU_EMOJI, speakers: MIKU_SPEAKERS, banner: '♪ 初音ミク ♪' },
}; };
const DURATION_MS = 6400; const DURATION_MS = 6400;
@@ -383,7 +399,7 @@ export default function WheelConfetti({ theme, burstKey, big = false }: Readonly
// eslint-disable-next-line react-hooks/exhaustive-deps // eslint-disable-next-line react-hooks/exhaustive-deps
}, [burstKey]); }, [burstKey]);
// Ostatní tematické styly: hlášku pronese náhodná postava (Mane 6, Ai Enma, Houkago Tea Time, Kessoku Band, piloti NERV, Nokuru, Forgerovi, vnitřní palác, družina Frieren, poníci z Maretime Bay, kancelář Retsuko, postavičky z koutků, Gudetama...) // Ostatní tematické styly: hlášku pronese náhodná postava (Mane 6, Ai Enma, Houkago Tea Time, Kessoku Band, piloti NERV, Nokuru, Forgerovi, vnitřní palác, družina Frieren, poníci z Maretime Bay, kancelář Retsuko, postavičky z koutků, Gudetama, Vocaloidi...)
const emojiConfetti = EMOJI_CONFETTI[theme]; const emojiConfetti = EMOJI_CONFETTI[theme];
const emojiSpeaker = useMemo( const emojiSpeaker = useMemo(
() => big && emojiConfetti ? pick(emojiConfetti.speakers) : undefined, () => big && emojiConfetti ? pick(emojiConfetti.speakers) : undefined,
@@ -3914,6 +3914,262 @@
} }
} }
// ============================================
// STYL HATSUNE MIKU kola štěstí (dle stylu zvoleného v nastavení).
// Výseče v barvách Vocaloidů (Miku, Luka, Rin, Kaito, Meiko, šedá rukávů),
// bílé linky a střed jako reproduktor s tyrkysovým lemem.
// ============================================
.spin-wheel-modal.spin-theme-miku {
--miku-font: 'Nunito', sans-serif;
--miku-display: 'Exo 2', 'Nunito', sans-serif;
--miku-teal: #39c5bb;
--miku-teal-deep: #138f87;
--miku-teal-ink: #137a7f;
--miku-pink: #e12885;
--miku-grey: #3a3f44;
--miku-line: #d8e3e6;
--miku-panel: #ffffff;
--miku-text: #263036;
--wheel-c0: #2fb3a9;
--wheel-c1: #e12885;
--wheel-c2: #5a6268;
--wheel-c3: #e0a000;
--wheel-c4: #3a6fd8;
--wheel-c5: #c8302c;
--wheel-rim: #3a3f44;
--wheel-hub: #2b2f33;
font-family: var(--miku-font);
.modal-content {
border: 1px solid var(--miku-line);
border-radius: 16px;
overflow: hidden;
background:
linear-gradient(90deg, rgb(57 197 187 / 0.12) 1px, transparent 1px) 0 0 / 88px 100%,
repeating-linear-gradient(180deg, #f6fafb 0 22px, #edf3f5 22px 44px);
color: var(--miku-text);
}
.modal-header {
background:
url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='98' height='10'%3E%3Crect width='98' height='10' fill='%23ffffff'/%3E%3Cpath d='M14 0V10M28 0V10M42 0V10M56 0V10M70 0V10M84 0V10M98 0V10' stroke='%239aa3a8'/%3E%3Cpath d='M10 0h8v6h-8zM24 0h8v6h-8zM52 0h8v6h-8zM66 0h8v6h-8zM80 0h8v6h-8z' fill='%232b2f33'/%3E%3C/svg%3E") left bottom / 98px 10px repeat-x,
linear-gradient(var(--miku-teal), var(--miku-teal)) left bottom 10px / 100% 3px no-repeat,
linear-gradient(180deg, #454b51, var(--miku-grey));
border-bottom: none;
padding-bottom: calc(var(--bs-modal-header-padding-y, 1rem) + 13px);
h2 {
color: #fff;
font-family: var(--miku-display);
font-weight: 700;
}
.btn-close {
filter: invert(1) grayscale(1) brightness(2);
opacity: 0.9;
}
}
.modal-footer {
border-top: 1px solid var(--miku-line);
}
.spin-steps span {
font-family: var(--miku-display);
font-weight: 700;
background: #e3eaed;
color: #8a979e;
border-radius: 6px;
padding: 3px 16px;
&.active {
background: var(--miku-teal-deep);
color: #fff;
box-shadow: 0 0 10px rgb(57 197 187 / 0.5);
}
&.done {
background: var(--miku-pink);
color: #fff;
}
}
.btn {
font-family: var(--miku-font);
font-weight: 800;
border-radius: 6px;
}
.btn-primary {
background: linear-gradient(180deg, #23a99f, var(--miku-teal-deep));
border: 1px solid #0e7a73;
color: #fff;
&:hover:not(:disabled) {
background: linear-gradient(180deg, #2fb3a9, #179a91);
color: #fff;
box-shadow: 0 0 0 3px rgb(57 197 187 / 0.3), 0 0 16px rgb(57 197 187 / 0.55);
}
&:disabled {
background: #cfd8dc;
border-color: #cfd8dc;
color: #8a979e;
}
}
// Tlačítko roztočení — „play"
.spin-button {
font-family: var(--miku-display);
font-weight: 800;
font-size: 1.25rem;
letter-spacing: 0.04em;
padding: 10px 38px;
&::before {
content: '▶ ';
font-size: 0.8em;
}
}
.btn-outline-primary {
background: var(--miku-panel);
border: 1px solid var(--miku-teal);
color: var(--miku-teal-ink);
&:hover {
background: #e0f6f4;
color: var(--miku-teal-ink);
}
}
.btn-secondary {
background: var(--miku-pink);
border: 1px solid #b81e6c;
color: #fff;
}
.spin-source {
background: #e3eaed;
border-radius: 8px;
> .btn {
border-radius: 6px !important;
}
> .btn-outline-primary {
background: transparent;
color: var(--miku-text);
&:hover {
background: var(--miku-panel);
}
}
}
.spin-result {
font-weight: 800;
strong {
color: var(--miku-teal-ink);
}
}
.spin-options {
background: var(--miku-panel);
border: 1px solid var(--miku-line);
border-radius: 12px;
li:hover {
background: #eef8f7;
}
}
.spin-options-header {
border-bottom: 2px solid var(--miku-teal);
color: var(--miku-teal-ink);
font-family: var(--miku-display);
}
.spin-empty {
border-color: var(--miku-line);
}
.spin-wheel-pointer {
border-top-color: var(--miku-pink);
filter: drop-shadow(0 0 4px rgb(225 40 133 / 0.6));
}
.spin-wheel-hub {
stroke: var(--miku-teal);
}
.spin-seg {
stroke: #fff;
stroke-width: 2;
}
.spin-seg-label {
font-family: var(--miku-display);
font-weight: 700;
}
}
// Tmavý režim — koncert
[data-bs-theme="dark"] .spin-wheel-modal.spin-theme-miku {
--miku-teal-ink: #5fd6cd;
--miku-line: #23303a;
--miku-panel: #14191d;
--miku-text: #eef4f6;
--wheel-c2: #3a4248;
--wheel-rim: #050607;
--wheel-hub: #050607;
.modal-content {
border-color: #23303a;
background:
linear-gradient(115deg, transparent 38%, rgb(57 197 187 / 0.10) 46%, transparent 54%),
linear-gradient(-115deg, transparent 38%, rgb(225 40 133 / 0.09) 46%, transparent 54%),
#0b0d10;
box-shadow: 0 0 30px rgb(57 197 187 / 0.2);
}
.modal-header {
background:
url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='98' height='10'%3E%3Crect width='98' height='10' fill='%23ffffff'/%3E%3Cpath d='M14 0V10M28 0V10M42 0V10M56 0V10M70 0V10M84 0V10M98 0V10' stroke='%239aa3a8'/%3E%3Cpath d='M10 0h8v6h-8zM24 0h8v6h-8zM52 0h8v6h-8zM66 0h8v6h-8zM80 0h8v6h-8z' fill='%232b2f33'/%3E%3C/svg%3E") left bottom / 98px 10px repeat-x,
linear-gradient(var(--miku-teal), var(--miku-teal)) left bottom 10px / 100% 2px no-repeat,
#101417;
}
.spin-steps span:not(.active):not(.done) {
background: #1b2328;
color: #82919a;
}
.btn-primary:disabled {
background: #23303a;
border-color: #23303a;
color: #82919a;
}
.btn-outline-primary:hover {
background: #12302e;
}
.spin-source {
background: #1b2328;
}
.spin-options li:hover {
background: #1b2328;
}
.spin-seg {
stroke: #0b0d10;
}
}
// Ikona kola se při najetí pootočí // Ikona kola se při najetí pootočí
.spin-wheel-icon { .spin-wheel-icon {
transition: transform 0.4s ease !important; transition: transform 0.4s ease !important;
+3 -2
View File
@@ -13,7 +13,7 @@ const AUTO_ORDER_CHOICE_KEY = 'auto_order_choice';
export type ThemePreference = 'system' | 'light' | 'dark'; export type ThemePreference = 'system' | 'light' | 'dark';
/** Vizuální styl aplikace (nezávislý na světlém/tmavém režimu). */ /** Vizuální styl aplikace (nezávislý na světlém/tmavém režimu). */
export type StyleTheme = 'simple' | 'umamusume' | 'mlp' | 'hellgirl' | 'kon' | 'bocchi' | 'evangelion' | 'yurucamp' | 'spyfamily' | 'apothecary' | 'frieren' | 'mlpg5' | 'aggretsuko' | 'sumikko' | 'gudetama'; export type StyleTheme = 'simple' | 'umamusume' | 'mlp' | 'hellgirl' | 'kon' | 'bocchi' | 'evangelion' | 'yurucamp' | 'spyfamily' | 'apothecary' | 'frieren' | 'mlpg5' | 'aggretsuko' | 'sumikko' | 'gudetama' | 'miku';
export const STYLE_THEMES: { value: StyleTheme, label: string }[] = [ export const STYLE_THEMES: { value: StyleTheme, label: string }[] = [
{ value: 'simple', label: 'Jednoduchý' }, { value: 'simple', label: 'Jednoduchý' },
@@ -31,6 +31,7 @@ export const STYLE_THEMES: { value: StyleTheme, label: string }[] = [
{ value: 'aggretsuko', label: 'Aggretsuko' }, { value: 'aggretsuko', label: 'Aggretsuko' },
{ value: 'sumikko', label: 'Sumikko Gurashi' }, { value: 'sumikko', label: 'Sumikko Gurashi' },
{ value: 'gudetama', label: 'Gudetama' }, { value: 'gudetama', label: 'Gudetama' },
{ value: 'miku', label: 'Hatsune Miku' },
]; ];
export type SettingsContextProps = { export type SettingsContextProps = {
@@ -262,7 +263,7 @@ function useProvideSettings(): SettingsContextProps {
}, [autoOrderChoice]); }, [autoOrderChoice]);
// Aplikuje accent barvy při změně hue nebo přepnutí světlý/tmavý. // Aplikuje accent barvy při změně hue nebo přepnutí světlý/tmavý.
// Tematické styly (Umamusume, My Little Pony, Hell Girl, K-ON!, Bocchi the Rock!, Evangelion, Yuru Camp△, Spy × Family, Apothecary Diaries, Frieren, My Little Pony G5, Aggretsuko, Sumikko Gurashi, Gudetama) mají vlastní pevnou paletu, accent barvu proto uplatňuje jen jednoduchý styl. // Tematické styly (Umamusume, My Little Pony, Hell Girl, K-ON!, Bocchi the Rock!, Evangelion, Yuru Camp△, Spy × Family, Apothecary Diaries, Frieren, My Little Pony G5, Aggretsuko, Sumikko Gurashi, Gudetama, Hatsune Miku) mají vlastní pevnou paletu, accent barvu proto uplatňuje jen jednoduchý styl.
useEffect(() => { useEffect(() => {
localStorage.setItem(ACCENT_HUE_KEY, String(accentHue)); localStorage.setItem(ACCENT_HUE_KEY, String(accentHue));
if (styleTheme === 'simple') { if (styleTheme === 'simple') {
+2
View File
@@ -33,6 +33,8 @@ const ICONS: Record<StyleTheme, Record<GameId, string>> = {
sumikko: { drawing: '✏️' }, sumikko: { drawing: '✏️' },
// Vajíčko — kreslit se mu ale stejně nechce // Vajíčko — kreslit se mu ale stejně nechce
gudetama: { drawing: '🥚' }, gudetama: { drawing: '🥚' },
// Klávesy zpěvového editoru
miku: { drawing: '🎹' },
}; };
/** Styl z <html data-luncher-theme> — pro místa mimo React kontext nastavení. */ /** Styl z <html data-luncher-theme> — pro místa mimo React kontext nastavení. */
+547
View File
@@ -0,0 +1,547 @@
// ============================================
// STYL "HATSUNE MIKU" (volitelný, výchozí je jednoduchý styl)
// Inspirováno virtuální zpěvačkou Hatsune Miku (初音ミク).
// Světlý režim = zpěvový editor Vocaloid: pozadí jako piano roll, Mikina
// tyrkysová, šedá z rukávů, růžová a červené „01" z jejího ramene.
// Hlavičky lemuje pruh klávesnice, u loga poskakuje ekvalizér.
// Tmavý režim = koncert Magical Mirai: černé pódium, reflektory a dole
// moře tyrkysových a růžových světelných tyčinek.
// Aktivní, pokud má <html> atribut data-luncher-theme="miku".
// ============================================
$miku: 'html[data-luncher-theme="miku"]';
// Pruh klávesnice — jedna oktáva (7 bílých a 5 černých kláves), opakuje se vodorovně
$miku-keys: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='98' height='10'%3E%3Crect width='98' height='10' fill='%23ffffff'/%3E%3Cpath d='M14 0V10M28 0V10M42 0V10M56 0V10M70 0V10M84 0V10M98 0V10' stroke='%239aa3a8'/%3E%3Cpath d='M10 0h8v6h-8zM24 0h8v6h-8zM52 0h8v6h-8zM66 0h8v6h-8zM80 0h8v6h-8z' fill='%232b2f33'/%3E%3C/svg%3E");
// Světelné tyčinky publika (tyrkysové a růžová) se září, opakují se vodorovně
$miku-glowsticks: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='72' height='56'%3E%3Cg stroke-linecap='round'%3E%3Cpath d='M12 56L18 24' stroke='%2339c5bb' stroke-opacity='.25' stroke-width='9'/%3E%3Cpath d='M12 56L18 24' stroke='%237ff0e6' stroke-width='3'/%3E%3Cpath d='M38 56L34 18' stroke='%23e12885' stroke-opacity='.25' stroke-width='9'/%3E%3Cpath d='M38 56L34 18' stroke='%23ff7ab8' stroke-width='3'/%3E%3Cpath d='M60 56L64 28' stroke='%2339c5bb' stroke-opacity='.25' stroke-width='9'/%3E%3Cpath d='M60 56L64 28' stroke='%237ff0e6' stroke-width='3'/%3E%3C/g%3E%3C/svg%3E");
#{$miku} {
--miku-font: 'Nunito', -apple-system, sans-serif;
--miku-display: 'Exo 2', 'Nunito', sans-serif;
--miku-teal: #39c5bb; // Mikina tyrkysová
--miku-teal-deep: #138f87;
--miku-teal-ink: #137a7f;
--miku-pink: #e12885;
--miku-grey: #3a3f44; // rukávy a sukně
--miku-red: #e0245e; // „01" na rameni
--miku-line: #d8e3e6;
--luncher-primary: var(--miku-teal-ink);
--luncher-primary-hover: #0e5f63;
--luncher-primary-light: #e0f6f4;
--luncher-bg: #f3f7f8;
--luncher-bg-card: #ffffff;
--luncher-bg-hover: #eef8f7;
--luncher-text: #263036;
--luncher-text-secondary: #4f5d66;
--luncher-text-muted: #8a979e;
--luncher-border: var(--miku-line);
--luncher-border-light: #e8eff1;
--luncher-navbar-bg: var(--miku-grey);
--luncher-navbar-text: #ffffff;
--luncher-action-icon: var(--miku-teal-ink);
--luncher-buyer-icon: var(--miku-pink);
--luncher-success: #2a9d6a;
--luncher-warning: #e0a000;
--luncher-danger: var(--miku-red);
--luncher-radius-sm: 6px;
--luncher-radius: 10px;
--luncher-radius-lg: 12px;
--luncher-radius-xl: 16px;
--luncher-shadow: 0 3px 12px rgb(38 48 54 / 0.08);
--luncher-shadow-lg: 0 0 0 1px var(--miku-teal), 0 10px 26px rgb(57 197 187 / 0.25);
&[data-bs-theme="dark"] {
// Koncert Magical Mirai
--miku-teal-ink: #5fd6cd;
--miku-line: #23303a;
--luncher-primary: #5fd6cd;
--luncher-primary-hover: #8ae6df;
--luncher-primary-light: #12302e;
--luncher-bg: #0b0d10;
--luncher-bg-card: #14191d;
--luncher-bg-hover: #1b2328;
--luncher-text: #eef4f6;
--luncher-text-secondary: #c2ced4;
--luncher-text-muted: #82919a;
--luncher-border-light: #1b2328;
--luncher-navbar-bg: #050607;
--luncher-action-icon: #5fd6cd;
--luncher-buyer-icon: #ff7ab8;
--luncher-success: #5fd69a;
--luncher-warning: #ffc04d;
--luncher-danger: #ff5c8a;
--luncher-shadow: 0 4px 16px rgb(0 0 0 / 0.5);
--luncher-shadow-lg: 0 0 0 1px var(--miku-teal), 0 0 28px rgb(57 197 187 / 0.35);
}
body {
font-family: var(--miku-font);
background-color: var(--luncher-bg);
// Piano roll — střídavé pruhy tónů a dobové linky
background-image:
linear-gradient(90deg, rgb(57 197 187 / 0.14) 1px, transparent 1px),
repeating-linear-gradient(180deg, #f6fafb 0 22px, #edf3f5 22px 44px);
background-size: 88px 100%, 100% 44px;
background-attachment: fixed;
}
&[data-bs-theme="dark"] body {
// Reflektory nad pódiem a moře světelných tyčinek dole
background-image:
#{$miku-glowsticks},
linear-gradient(115deg, transparent 38%, rgb(57 197 187 / 0.10) 46%, transparent 54%),
linear-gradient(-115deg, transparent 38%, rgb(225 40 133 / 0.09) 46%, transparent 54%),
radial-gradient(ellipse 60% 40% at 50% -10%, rgb(57 197 187 / 0.22), transparent 70%),
linear-gradient(0deg, rgb(57 197 187 / 0.12), transparent 140px);
background-size: 72px 56px, 100% 100%, 100% 100%, 100% 100%, 100% 100%;
background-position: left bottom, 0 0, 0 0, 0 0, 0 0;
background-repeat: repeat-x, no-repeat, no-repeat, no-repeat, no-repeat;
}
.app-container {
background: transparent;
}
h1, h2, h3, h4, .navbar-brand {
font-family: var(--miku-display);
font-weight: 700;
}
.btn {
font-family: var(--miku-font);
}
// ---------- Navbar: šedá jako rukávy, tyrkysový lem a ekvalizér ----------
.navbar {
background:
linear-gradient(var(--miku-teal), var(--miku-teal)) left bottom / 100% 3px no-repeat,
linear-gradient(180deg, #454b51, var(--luncher-navbar-bg)) !important;
padding-bottom: calc(var(--bs-navbar-padding-y, 0.5rem) + 3px);
border-bottom: none;
box-shadow: 0 2px 14px rgb(57 197 187 / 0.35);
.navbar-brand {
letter-spacing: 0.02em;
// Ekvalizér — právě hraje
&::after {
content: '';
display: inline-block;
width: 15px;
height: 16px;
margin-left: 8px;
margin-right: 0.6em;
vertical-align: -1px;
background-image:
linear-gradient(var(--miku-teal), var(--miku-teal)),
linear-gradient(var(--miku-pink), var(--miku-pink)),
linear-gradient(var(--miku-teal), var(--miku-teal)),
linear-gradient(var(--miku-teal), var(--miku-teal));
background-repeat: no-repeat;
background-position: 0 100%, 4px 100%, 8px 100%, 12px 100%;
background-size: 3px 40%, 3px 90%, 3px 60%, 3px 30%;
animation: miku-eq 0.9s ease-in-out infinite alternate;
}
}
.dropdown-menu {
border: 1px solid var(--miku-line);
border-radius: var(--luncher-radius);
box-shadow: var(--luncher-shadow-lg);
}
}
&[data-bs-theme="dark"] .navbar {
background:
linear-gradient(var(--miku-teal), var(--miku-teal)) left bottom / 100% 2px no-repeat,
var(--luncher-navbar-bg) !important;
}
// ---------- Titulek dne: tyrkysová nota z piano rollu se značkou „01" ----------
.day-navigator .title {
position: relative;
display: inline-block;
padding: 6px 28px 8px;
font-family: var(--miku-display);
font-weight: 700;
font-size: 1.65rem;
letter-spacing: 0.01em;
color: #ffffff !important;
text-shadow: 0 1px 0 rgb(0 0 0 / 0.2);
background: linear-gradient(180deg, #5fd6cd, var(--miku-teal) 60%, #2fb3a9);
border: 1px solid #2a9d95;
border-radius: 8px;
box-shadow: 0 0 0 4px rgb(57 197 187 / 0.2), 0 8px 18px rgb(57 197 187 / 0.3);
// Červené „01" z Mikina ramene
&::before {
content: '01';
position: absolute;
left: -12px;
top: -12px;
padding: 1px 6px;
font-size: 0.45em;
font-weight: 800;
line-height: 1.4;
color: #fff;
background: var(--miku-red);
border-radius: 4px;
text-shadow: none;
transform: rotate(-8deg);
}
// Nota, která z titulku vylétá
&::after {
content: '♪';
position: absolute;
right: -20px;
top: -6px;
font-size: 0.8em;
color: var(--miku-pink);
text-shadow: none;
animation: miku-note 2.6s ease-in-out infinite;
}
&.text-muted {
color: #f4f7f8 !important;
background: linear-gradient(180deg, #c3ccd0, #aab5ba);
border-color: #98a4aa;
box-shadow: none;
&::before {
background: #8a979e;
}
&::after {
display: none;
}
}
}
&[data-bs-theme="dark"] .day-navigator .title {
box-shadow: 0 0 0 4px rgb(57 197 187 / 0.15), 0 0 26px rgb(57 197 187 / 0.55);
&.text-muted {
color: #c2ced4 !important;
background: #1b2328;
border-color: #2c3a42;
box-shadow: none;
}
}
// ---------- Karty podniků: šedá hlavička s tyrkysovým lemem a klávesnicí ----------
.restaurant-card {
border: 1px solid var(--miku-line);
border-radius: var(--luncher-radius-lg);
box-shadow: var(--luncher-shadow);
overflow: hidden;
&:hover {
transform: translateY(-2px);
border-color: var(--miku-teal);
box-shadow: var(--luncher-shadow-lg);
}
.restaurant-header {
background:
#{$miku-keys} left bottom / 98px 10px repeat-x,
linear-gradient(var(--miku-teal), var(--miku-teal)) left bottom 10px / 100% 3px no-repeat,
linear-gradient(180deg, #454b51, var(--miku-grey));
padding-bottom: 28px;
// Při najetí se rozsvítí tyrkysově
&:hover {
background:
#{$miku-keys} left bottom / 98px 10px repeat-x,
linear-gradient(var(--miku-pink), var(--miku-pink)) left bottom 10px / 100% 3px no-repeat,
linear-gradient(135deg, #2fb3a9, var(--miku-teal-deep));
}
h3 {
letter-spacing: 0.02em;
}
small {
color: rgb(255 255 255 / 0.78);
}
}
}
&[data-bs-theme="dark"] .restaurant-card .restaurant-header:not(:hover) {
background:
#{$miku-keys} left bottom / 98px 10px repeat-x,
linear-gradient(var(--miku-teal), var(--miku-teal)) left bottom 10px / 100% 2px no-repeat,
#101417;
}
.food-table tbody tr:hover > td {
background: #eef8f7;
}
&[data-bs-theme="dark"] .food-table tbody tr:hover > td {
background: #1b2328;
}
.food-price {
color: var(--miku-teal-ink);
font-family: var(--miku-display);
font-weight: 700;
}
// ---------- Panely a tabulka voleb ----------
.choice-section,
.choices-table,
.pizza-section,
.no-votes {
border: 1px solid var(--miku-line);
border-radius: var(--luncher-radius-lg);
box-shadow: var(--luncher-shadow);
}
.choices-table {
border-collapse: separate;
border-spacing: 0;
}
// Sloupec s podnikem: tyrkysová stopa
.choices-table > tbody > tr > td:first-child {
background: #e0f6f4;
color: var(--miku-teal-ink);
font-family: var(--miku-display);
font-weight: 700;
border-right: 3px solid var(--miku-teal);
}
&[data-bs-theme="dark"] .choices-table > tbody > tr > td:first-child {
background: #12302e;
}
// Bootstrap karty (skupinové objednávky apod.) a seznamy
.card {
border: 1px solid var(--miku-line);
border-radius: var(--luncher-radius-lg);
box-shadow: var(--luncher-shadow);
overflow: hidden;
background: var(--luncher-bg-card);
// Rozbalovací výběr z nabídky nesmí karta oříznout
&:has(.select-search-container) {
overflow: visible;
}
> .card-header {
background: var(--luncher-primary-light);
border-bottom: 2px solid var(--miku-teal);
font-weight: 700;
&:first-child {
border-radius: calc(var(--luncher-radius-lg) - 1px) calc(var(--luncher-radius-lg) - 1px) 0 0;
}
}
.table {
--bs-table-bg: transparent;
margin-bottom: 0;
> :last-child > tr:last-child > * {
border-bottom-width: 0;
}
}
}
.list-group {
border-radius: var(--luncher-radius);
overflow: hidden;
border: 1px solid var(--miku-line);
> .list-group-item {
border-left: none;
border-right: none;
&:first-child {
border-top: none;
}
&:last-child {
border-bottom: none;
}
}
}
.form-select, .form-control,
input[type="text"], input[type="email"], input[type="password"] {
border-radius: var(--luncher-radius-sm);
border: 1px solid var(--miku-line);
&:focus {
border-color: var(--miku-teal);
box-shadow: 0 0 0 3px rgb(57 197 187 / 0.3);
}
}
// ---------- Tlačítka: tyrkysová, při najetí zazáří jako tyčinka ----------
.btn {
border-radius: var(--luncher-radius-sm);
font-weight: 800;
letter-spacing: 0.02em;
transition: transform 0.15s ease, box-shadow 0.2s ease, background 0.2s ease;
}
.btn-primary {
background: linear-gradient(180deg, #23a99f, var(--miku-teal-deep));
border: 1px solid #0e7a73;
color: #fff;
&:hover {
background: linear-gradient(180deg, #2fb3a9, #179a91);
border-color: #0e7a73;
color: #fff;
box-shadow: 0 0 0 3px rgb(57 197 187 / 0.3), 0 0 16px rgb(57 197 187 / 0.55);
}
&:active {
transform: translateY(1px) !important;
}
&:disabled {
background: #cfd8dc;
border-color: #cfd8dc;
color: #8a979e;
box-shadow: none;
}
}
&[data-bs-theme="dark"] .btn-primary:disabled {
background: #23303a;
border-color: #23303a;
color: #82919a;
}
.btn-outline-primary {
background: var(--luncher-bg-card);
border: 1px solid var(--miku-teal);
color: var(--luncher-primary);
&:hover {
background: var(--luncher-primary-light);
border-color: var(--miku-teal);
color: var(--luncher-primary);
}
}
// Sekundární — růžová ze stužek ve vlasech
.btn-secondary {
background: var(--miku-pink);
border: 1px solid #b81e6c;
color: #fff;
&:hover {
background: #c81f75;
color: #fff;
box-shadow: 0 0 14px rgb(225 40 133 / 0.45);
}
}
.btn-danger {
background: var(--miku-red);
border: 1px solid #b01a48;
}
.action-icon:hover {
background: var(--luncher-primary-light);
color: var(--luncher-primary);
}
// ---------- Modaly: šedé okno editoru s klávesnicí ----------
.modal-content {
border: 1px solid var(--miku-line);
border-radius: var(--luncher-radius-xl);
box-shadow: 0 16px 40px rgb(38 48 54 / 0.3);
overflow: hidden;
}
.modal-header {
background:
#{$miku-keys} left bottom / 98px 10px repeat-x,
linear-gradient(var(--miku-teal), var(--miku-teal)) left bottom 10px / 100% 3px no-repeat,
linear-gradient(180deg, #454b51, var(--miku-grey));
border-bottom: none;
padding-bottom: calc(var(--bs-modal-header-padding-y, 1rem) + 13px);
.modal-title, .modal-title h2 {
color: #fff;
font-family: var(--miku-display);
font-weight: 700;
}
.btn-close {
filter: invert(1) grayscale(1) brightness(2);
}
}
&[data-bs-theme="dark"] {
.modal-content {
border-color: #23303a;
box-shadow: 0 0 30px rgb(57 197 187 / 0.2);
}
.modal-header {
background:
#{$miku-keys} left bottom / 98px 10px repeat-x,
linear-gradient(var(--miku-teal), var(--miku-teal)) left bottom 10px / 100% 2px no-repeat,
#101417;
}
}
.footer {
border-top: none;
background-image: $miku-keys;
background-size: 98px 10px;
background-repeat: repeat-x;
background-position: top;
padding-top: 26px;
a {
color: var(--miku-teal);
}
}
}
// Ekvalizér — sloupce střídavě poskakují
@keyframes miku-eq {
0% { background-size: 3px 40%, 3px 90%, 3px 60%, 3px 30%; }
25% { background-size: 3px 80%, 3px 50%, 3px 30%, 3px 70%; }
50% { background-size: 3px 30%, 3px 70%, 3px 90%, 3px 50%; }
75% { background-size: 3px 70%, 3px 30%, 3px 50%, 3px 90%; }
100% { background-size: 3px 50%, 3px 80%, 3px 40%, 3px 60%; }
}
// Nota vylétá a mizí
@keyframes miku-note {
0% { transform: translate(0, 4px) rotate(-10deg); opacity: 0; }
20% { opacity: 1; }
70% { opacity: 1; }
100% { transform: translate(8px, -16px) rotate(12deg); opacity: 0; }
}
@media (max-width: 575.98px) {
#{$miku} .day-navigator .title {
font-size: 1.3rem;
padding: 5px 20px 6px;
&::after {
right: -14px;
}
}
}
@media (prefers-reduced-motion: reduce) {
#{$miku} .navbar .navbar-brand::after,
#{$miku} .day-navigator .title::after {
animation: none;
}
}