+ Vzhled
+
+ Styl
+ appSettings?.setStyleTheme(event.target.value as StyleTheme)}
+ >
+ {Object.values(StyleTheme).map(value => (
+
+ ))}
+
+
+
Cena za 100 g
U podniků, které účtují podle váhy, se z ceny jídla a této sazby dopočítá
diff --git a/client/src/context/settings.tsx b/client/src/context/settings.tsx
index 93c41db..a412c27 100644
--- a/client/src/context/settings.tsx
+++ b/client/src/context/settings.tsx
@@ -1,15 +1,19 @@
import React, { ReactNode, useCallback, useContext, useEffect, useMemo, useState } from "react";
+import { StyleTheme } from "../enums";
/** Volba motivu — buď pevná, nebo řízená nastavením operačního systému. */
export type ThemePreference = 'light' | 'dark' | 'system';
const THEME_KEY = 'themePreference';
+const STYLE_THEME_KEY = 'styleTheme';
export type SettingsContextProps = {
themePreference: ThemePreference,
/** Skutečně použitý motiv po vyhodnocení volby 'system'. */
effectiveDark: boolean,
setThemePreference: (preference: ThemePreference) => void,
+ styleTheme: StyleTheme,
+ setStyleTheme: (styleTheme: StyleTheme) => void,
};
const settingsContext = React.createContext(null);
@@ -22,8 +26,15 @@ function getStoredPreference(): ThemePreference {
return stored === 'light' || stored === 'dark' ? stored : 'system';
}
+/** Vrátí uložený vizuální styl; neznámou nebo chybějící hodnotu nahradí výchozím. */
+const getStoredStyleTheme = (): StyleTheme => {
+ const stored = localStorage.getItem(STYLE_THEME_KEY);
+ return Object.values(StyleTheme).find(value => value === stored) ?? StyleTheme.JEDNODUCHY;
+};
+
export function ProvideSettings(props: Readonly<{ children: ReactNode }>) {
const [themePreference, setStoredPreference] = useState(getStoredPreference);
+ const [styleTheme, setStyleThemeState] = useState(getStoredStyleTheme);
const [systemDark, setSystemDark] = useState(() =>
globalThis.matchMedia?.('(prefers-color-scheme: dark)').matches ?? false);
@@ -43,6 +54,19 @@ export function ProvideSettings(props: Readonly<{ children: ReactNode }>) {
document.documentElement.setAttribute('data-bs-theme', effectiveDark ? 'dark' : 'light');
}, [effectiveDark]);
+ useEffect(() => {
+ document.documentElement.setAttribute('data-ft-style', styleTheme);
+ }, [styleTheme]);
+
+ const setStyleTheme = useCallback((value: StyleTheme) => {
+ setStyleThemeState(value);
+ if (value === StyleTheme.JEDNODUCHY) {
+ localStorage.removeItem(STYLE_THEME_KEY);
+ } else {
+ localStorage.setItem(STYLE_THEME_KEY, value);
+ }
+ }, []);
+
const setThemePreference = useCallback((preference: ThemePreference) => {
setStoredPreference(preference);
if (preference === 'system') {
@@ -53,8 +77,8 @@ export function ProvideSettings(props: Readonly<{ children: ReactNode }>) {
}, []);
const value = useMemo(
- () => ({ themePreference, effectiveDark, setThemePreference }),
- [themePreference, effectiveDark, setThemePreference]);
+ () => ({ themePreference, effectiveDark, setThemePreference, styleTheme, setStyleTheme }),
+ [themePreference, effectiveDark, setThemePreference, styleTheme, setStyleTheme]);
return {props.children};
}
diff --git a/client/src/enums.ts b/client/src/enums.ts
index 078e66a..8c885f3 100644
--- a/client/src/enums.ts
+++ b/client/src/enums.ts
@@ -192,3 +192,17 @@ export enum SourceFilter {
VSE = 'VSE',
BEZ_ZDROJE = 'BEZ_ZDROJE',
}
+
+/**
+ * Vizuální styl aplikace, nezávislý na světlém/tmavém režimu. Převzaté z Luncheru,
+ * hodnota se zapisuje do atributu `data-ft-style` na .
+ */
+export enum StyleTheme {
+ JEDNODUCHY = 'JEDNODUCHY',
+ HELL_GIRL = 'HELL_GIRL',
+}
+
+export const STYLE_THEME_NAMES: Record = {
+ JEDNODUCHY: 'Jednoduchý',
+ HELL_GIRL: 'Hell Girl',
+};
diff --git a/client/src/index.tsx b/client/src/index.tsx
index e0d9622..081c04d 100644
--- a/client/src/index.tsx
+++ b/client/src/index.tsx
@@ -11,6 +11,7 @@ import 'bootstrap/dist/css/bootstrap.min.css';
import 'react-toastify/dist/ReactToastify.css';
import './index.css';
import './App.scss';
+import './themes/hellgirl.scss';
client.setConfig({
auth: () => getToken(),
diff --git a/client/src/themes/hellgirl-flame.gif b/client/src/themes/hellgirl-flame.gif
new file mode 100644
index 0000000..eabe6d3
Binary files /dev/null and b/client/src/themes/hellgirl-flame.gif differ
diff --git a/client/src/themes/hellgirl-symbol.png b/client/src/themes/hellgirl-symbol.png
new file mode 100644
index 0000000..d15083e
Binary files /dev/null and b/client/src/themes/hellgirl-symbol.png differ
diff --git a/client/src/themes/hellgirl.scss b/client/src/themes/hellgirl.scss
new file mode 100644
index 0000000..0821dc5
--- /dev/null
+++ b/client/src/themes/hellgirl.scss
@@ -0,0 +1,290 @@
+// ============================================
+// STYL "HELL GIRL" (volitelný, výchozí je jednoduchý styl)
+// Převzato z Luncheru (plamen při otevření dialogu je v HellGirlFlames). Inspirováno anime Jigoku Shōjo (地獄少女):
+// purpurovo-rudé nebe soumraku, papír washi, karmínová rudá nit, květy higanbany
+// a ostré tradiční tvary s písmem mincho. Tmavý režim = půlnoc Pekelné korespondence.
+// Aktivní, pokud má atribut data-ft-style="HELL_GIRL".
+// ============================================
+
+$hg: 'html[data-ft-style="HELL_GIRL"]';
+
+// Nebe věčného soumraku (fialová → karmínová → oranžová)
+$hg-twilight: linear-gradient(90deg, #1e0a1c, #4a0f2a 35%, #8e0f24 70%, #c8452a);
+// Rudá nit — tenká linka s jemnou září
+$hg-thread: linear-gradient(90deg, transparent, #c8102e 12%, #e8344f 50%, #c8102e 88%, transparent);
+
+#{$hg} {
+ --hg-font: 'Shippori Mincho', 'Noto Serif JP', 'Georgia', serif;
+ --hg-crimson: #a3122a;
+ --hg-crimson-deep: #7a0c1f;
+ --hg-crimson-shadow: #4a0612;
+ --hg-higanbana: #d4203c;
+ --hg-dusk: #c8452a;
+ --hg-ink: #1c1012;
+ --hg-washi: #f4ece0;
+ --hg-card-border: #dccbbb;
+ --hg-petal: rgb(163 18 42 / 0.10);
+ --hg-petal-2: rgb(200 69 42 / 0.08);
+
+ --ft-primary: var(--hg-crimson);
+ --ft-primary-hover: var(--hg-crimson-deep);
+ --ft-primary-light: #f7e3e2;
+ --ft-bg: var(--hg-washi);
+ --ft-bg-card: #fffbf5;
+ --ft-bg-hover: #f8ebe6;
+ --ft-text: var(--hg-ink);
+ --ft-text-secondary: #4f3a3c;
+ --ft-text-muted: #8c7672;
+ --ft-border: var(--hg-card-border);
+ --ft-border-light: #eadfd2;
+ --ft-navbar-bg: var(--hg-crimson-deep);
+ --ft-navbar-text: #fff4ee;
+ --ft-success: #3d7a4a;
+ --ft-warning: #c07a1a;
+ --ft-danger: #c8102e;
+ // Ostré, tradiční tvary — téměř bez zaoblení
+ --ft-radius-sm: 2px;
+ --ft-radius: 3px;
+ --ft-radius-lg: 4px;
+ --ft-radius-xl: 6px;
+ --ft-shadow-sm: 0 1px 4px -2px rgb(74 6 18 / 0.2);
+ --ft-shadow: 0 3px 12px -4px rgb(74 6 18 / 0.25);
+ --ft-shadow-lg: 0 10px 26px -8px rgb(74 6 18 / 0.38);
+
+ &[data-bs-theme="dark"] {
+ --hg-crimson: #e0334f;
+ --hg-crimson-deep: #b81c38;
+ --hg-crimson-shadow: #3a0510;
+ --hg-higanbana: #ff4d63;
+ --hg-dusk: #ff7a4a;
+ --hg-card-border: #3d1c22;
+ --hg-petal: rgb(224 51 79 / 0.22);
+ --hg-petal-2: rgb(255 122 74 / 0.12);
+
+ --ft-primary: #ec4a63;
+ --ft-primary-hover: #ff6b80;
+ --ft-primary-light: #2e1015;
+ --ft-bg: #0c0608;
+ --ft-bg-card: #170c0f;
+ --ft-bg-hover: #221216;
+ --ft-text: #f3e6e2;
+ --ft-text-secondary: #c9aeaa;
+ --ft-text-muted: #8a6c6a;
+ --ft-border: #3d1c22;
+ --ft-border-light: #2a1418;
+ --ft-navbar-bg: #12070a;
+ --ft-success: #6fbf7e;
+ --ft-warning: #f0b45a;
+ --ft-danger: #ff4d63;
+ --ft-shadow-sm: 0 1px 4px -1px rgb(0 0 0 / 0.6);
+ --ft-shadow: 0 4px 16px -4px rgb(0 0 0 / 0.7);
+ --ft-shadow-lg: 0 12px 30px -8px rgb(0 0 0 / 0.8), 0 0 18px -6px rgb(224 51 79 / 0.25);
+ }
+
+ body {
+ font-family: var(--hg-font);
+ background-color: var(--ft-bg);
+ // Rozfoukané okvětní lístky higanbany nad papírem, nahoře záře soumraku
+ background-image:
+ radial-gradient(ellipse 3px 1.5px at 20% 30%, var(--hg-petal) 0 100%, transparent 101%),
+ radial-gradient(ellipse 2px 4px at 70% 75%, var(--hg-petal-2) 0 100%, transparent 101%),
+ radial-gradient(ellipse 2.5px 1.2px at 85% 15%, var(--hg-petal) 0 100%, transparent 101%),
+ linear-gradient(180deg, #f3d9cc 0, var(--ft-bg) 380px);
+ background-size: 150px 150px, 210px 210px, 260px 260px, 100% 100%;
+ background-attachment: fixed;
+ }
+
+ &[data-bs-theme="dark"] body {
+ // Půlnoční tma s rudou září pekelného ohně odspodu
+ background-image:
+ radial-gradient(ellipse 3px 1.5px at 20% 30%, var(--hg-petal) 0 100%, transparent 101%),
+ radial-gradient(ellipse 2px 4px at 70% 75%, var(--hg-petal-2) 0 100%, transparent 101%),
+ radial-gradient(ellipse 2.5px 1.2px at 85% 15%, var(--hg-petal) 0 100%, transparent 101%),
+ radial-gradient(ellipse 120% 45% at 50% 115%, rgb(140 12 36 / 0.35), transparent 70%),
+ linear-gradient(180deg, #1c0a12 0, var(--ft-bg) 420px);
+ background-size: 150px 150px, 210px 210px, 260px 260px, 100% 100%, 100% 100%;
+ }
+
+ .app-container {
+ background: transparent;
+ }
+
+ h1, h2, h3, h4, .navbar-brand, .btn, .modal-title {
+ font-family: var(--hg-font);
+ }
+
+ // ---------- Navbar: nebe soumraku s rudou nití dole ----------
+ .navbar {
+ background:
+ #{$hg-thread} bottom / 100% 2px no-repeat,
+ #{$hg-twilight} !important;
+ box-shadow: 0 3px 14px -2px rgb(74 6 18 / 0.5);
+
+ .navbar-brand {
+ font-weight: 800;
+ letter-spacing: 1.5px;
+ text-shadow: 0 0 12px rgb(232 52 79 / 0.6);
+
+ // Pekelná korespondence
+ &::after {
+ content: '地獄通信';
+ display: inline-block;
+ margin-left: 8px;
+ font-size: 0.55em;
+ font-weight: 600;
+ letter-spacing: 2px;
+ vertical-align: middle;
+ color: #ffb3a8;
+ opacity: 0.85;
+ animation: hg-flicker 5s ease-in-out infinite;
+ }
+ }
+
+ .nav-pill.active {
+ background: rgb(0 0 0 / 0.35);
+ box-shadow: inset 0 -2px 0 #e8344f;
+ }
+
+ .dropdown-menu {
+ border: 1px solid var(--hg-crimson);
+ }
+ }
+
+ &[data-bs-theme="dark"] .navbar {
+ background:
+ #{$hg-thread} bottom / 100% 2px no-repeat,
+ linear-gradient(90deg, #0a0406, #1f0810 55%, #3d0a18) !important;
+ }
+
+ // ---------- Výběr dne: černá lakovaná deska s rudým lemem a symbolem ohně ----------
+ .day-picker {
+ background: linear-gradient(180deg, #2a1216, #12070a);
+ border: 1px solid var(--hg-crimson);
+ outline: 1px solid rgb(163 18 42 / 0.35);
+ outline-offset: 3px;
+
+ .day-picker-date {
+ color: #fff4ee;
+ font-family: var(--hg-font);
+ font-weight: 800;
+ letter-spacing: 0.5px;
+ text-shadow: 0 0 10px rgb(232 52 79 / 0.55);
+
+ &::before {
+ content: '';
+ display: inline-block;
+ width: 0.9em;
+ height: 0.9em;
+ margin-right: 10px;
+ vertical-align: -0.1em;
+ background: linear-gradient(180deg, var(--hg-dusk), var(--hg-higanbana) 45%, var(--hg-crimson-deep));
+ -webkit-mask: url('./hellgirl-symbol.png') center / contain no-repeat;
+ mask: url('./hellgirl-symbol.png') center / contain no-repeat;
+ }
+ }
+
+ .day-picker-weekday {
+ color: #c9aeaa;
+ }
+
+ .btn-outline-secondary, .btn-icon {
+ color: #fff4ee;
+ border-color: rgb(255 244 238 / 0.3);
+ }
+ }
+
+ // ---------- Panely a dlaždice ----------
+ .card-panel,
+ .summary-tile {
+ border: 1px solid var(--hg-card-border);
+ border-top: 3px solid var(--hg-crimson);
+ box-shadow: var(--ft-shadow);
+ }
+
+ .day-tab.active {
+ border-bottom-color: var(--hg-crimson);
+ }
+
+ .form-select, .form-control {
+ &:focus {
+ border-color: var(--hg-crimson);
+ box-shadow: 0 0 0 3px rgb(200 16 46 / 0.2);
+ }
+ }
+
+ // ---------- Tlačítka: ostrá, karmínová s dvojitou linkou jako pás obi ----------
+ .btn {
+ font-weight: 700;
+ letter-spacing: 0.4px;
+ }
+
+ .btn-primary {
+ background: linear-gradient(180deg, #c01733, var(--hg-crimson-deep));
+ border: 1px solid var(--hg-crimson-shadow);
+ color: #fff4ee;
+ box-shadow: inset 0 0 0 2px rgb(255 255 255 / 0.12);
+
+ &:hover, &:focus, &:active {
+ background: linear-gradient(180deg, #d41d3b, #8e0f24) !important;
+ border-color: var(--hg-crimson-shadow) !important;
+ box-shadow: inset 0 0 0 2px rgb(255 255 255 / 0.18), 0 0 14px rgb(212 32 60 / 0.5);
+ }
+
+ &:disabled {
+ background: #b9a3a0;
+ border-color: #9c8582;
+ box-shadow: none;
+ }
+ }
+
+ .btn-danger {
+ background: linear-gradient(180deg, #e0243f, #8e0f24);
+ border: 1px solid var(--hg-crimson-shadow);
+ }
+
+ // ---------- Modaly ----------
+ .modal-content {
+ border: 1px solid var(--hg-crimson);
+ overflow: hidden;
+ }
+
+ .modal-header {
+ background:
+ #{$hg-thread} bottom / 100% 2px no-repeat,
+ #{$hg-twilight};
+ border-bottom: none;
+
+ .modal-title {
+ color: #fff4ee;
+ font-weight: 800;
+ letter-spacing: 0.5px;
+ text-shadow: 0 0 10px rgb(232 52 79 / 0.5);
+ }
+
+ .btn-close {
+ filter: invert(1) grayscale(1) brightness(2);
+ }
+ }
+}
+
+// Mihotání nápisu jako plamen svíčky
+@keyframes hg-flicker {
+ 0%, 100% { opacity: 0.85; }
+ 45% { opacity: 0.85; }
+ 48% { opacity: 0.35; }
+ 50% { opacity: 0.9; }
+ 53% { opacity: 0.5; }
+ 56% { opacity: 0.85; }
+}
+
+@media (prefers-reduced-motion: reduce) {
+ #{$hg} .navbar .navbar-brand::after {
+ animation: none;
+ }
+}
+
+@media (max-width: 575.98px) {
+ #{$hg} .navbar .navbar-brand::after {
+ display: none;
+ }
+}