diff --git a/src/components/logistics/LogisticsReadinessBoard.jsx b/src/components/logistics/LogisticsReadinessBoard.jsx index 65ee545..8bda277 100644 --- a/src/components/logistics/LogisticsReadinessBoard.jsx +++ b/src/components/logistics/LogisticsReadinessBoard.jsx @@ -319,7 +319,7 @@ export const LogisticsReadinessBoard = ({ orderGroups = [], onSelectSet, statusO } return ( -
Панель @@ -81,6 +82,9 @@ export const AppShell = ({ {isGuideOpen ? "К рабочей области" : "Справка"} ) : null} + navigate("/settings")}> + Настройки + Выйти @@ -88,9 +92,9 @@ export const AppShell = ({ {/* Main content area */} -
@@ -117,6 +121,9 @@ export const AppShell = ({ ) : null} + navigate("/settings")} aria-label="Настройки"> + ⚙ + Выйти @@ -127,7 +134,7 @@ export const AppShell = ({ {/* Mobile tab navigation — STICKY TOP */} {shouldShowMobileNav && ( - + {navItems.map((item) => ( + @@ -184,6 +191,9 @@ export const AppShell = ({ ) : null} + navigate("/settings")} aria-label="Настройки"> + ⚙ + diff --git a/src/main.jsx b/src/main.jsx index dd5a49f..391bef8 100644 --- a/src/main.jsx +++ b/src/main.jsx @@ -4,10 +4,12 @@ import { RouterProvider } from "react-router-dom"; import { router } from "./router"; import { ThemeProvider } from "./context/ThemeContext"; import { AuthProvider } from "./context/AuthContext"; +import { FontSettingsProvider } from "./context/FontSettingsContext"; import ErrorBoundary from "./components/ErrorBoundary"; import { initErrorLogging } from "./utils/errorLogger"; import { registerPwaServiceWorker } from "./hooks/usePwaStatus"; import "./index.css"; +import "./styles/fontSettings.css"; registerPwaServiceWorker(); initErrorLogging(); @@ -15,9 +17,11 @@ initErrorLogging(); ReactDOM.createRoot(document.getElementById("root")).render( - - - + + + + + , ); \ No newline at end of file diff --git a/src/pages/SettingsPage.jsx b/src/pages/SettingsPage.jsx new file mode 100644 index 0000000..22a2237 --- /dev/null +++ b/src/pages/SettingsPage.jsx @@ -0,0 +1,186 @@ +/** + * @file SettingsPage.jsx + * @description Page for user settings: font size controls for all UI categories. + * Settings persist to localStorage via FontSettingsContext. + */ +import React from "react"; +import { useNavigate } from "react-router-dom"; +import { useFontSettings, FONT_CATEGORIES } from "../context/FontSettingsContext"; +import { Panel } from "../components/UI/Panel"; +import { Button } from "../components/UI/Button"; + +const PRESETS = [ + { label: "Мелкий", scales: { table: 0.85, card: 0.85, nav: 0.85, heading: 0.85, body: 0.85, small: 0.85 } }, + { label: "Стандарт", scales: { table: 1.0, card: 1.0, nav: 1.0, heading: 1.0, body: 1.0, small: 1.0 } }, + { label: "Крупный", scales: { table: 1.15, card: 1.15, nav: 1.15, heading: 1.2, body: 1.15, small: 1.1 } }, + { label: "Очень крупный", scales: { table: 1.3, card: 1.3, nav: 1.25, heading: 1.4, body: 1.3, small: 1.2 } }, +]; + +const Slider = ({ category, value, onChange }) => { + const pct = ((value - category.min) / (category.max - category.min)) * 100; + + return ( + + + + {category.label} + {category.description} + + + {Math.round(value * 100)}% + + + + onChange(parseFloat(e.target.value))} + className="fs-slider flex-1" + style={{ + background: `linear-gradient(to right, var(--color-accent) 0%, var(--color-accent) ${pct}%, var(--color-border) ${pct}%, var(--color-border) 100%)`, + }} + aria-label={category.label} + /> + + {value.toFixed(2)}× + + + + ); +}; + +export const SettingsPage = () => { + const { settings, updateCategory, resetAll, categories } = useFontSettings(); + const navigate = useNavigate(); + + const applyPreset = (scales) => { + for (const [key, scale] of Object.entries(scales)) { + updateCategory(key, scale); + } + }; + + // Check if current settings match a preset + const activePreset = PRESETS.findIndex((p) => + Object.entries(p.scales).every(([k, v]) => Math.abs((settings[k] ?? 1.0) - v) < 0.001) + ); + + return ( + + {/* Header */} + + + Настройки + + Настройки интерфейса + + + navigate(-1)}> + ← Назад + + + + {/* Presets */} + + + Быстрые пресеты + + + {PRESETS.map((preset, i) => ( + applyPreset(preset.scales)} + className={[ + "rounded-[18px] border-2 px-4 py-3 text-center transition", + activePreset === i + ? "border-[var(--color-accent)] bg-[var(--color-accent-soft)] text-[var(--color-text)] font-semibold" + : "border-[var(--color-border)] bg-[var(--color-surface-strong)] text-[var(--color-text-muted)] hover:border-[var(--color-accent)] hover:text-[var(--color-text)]", + ].join(" ")} + > + {preset.label} + + ))} + + + + {/* Font size sliders */} + + + + Размеры шрифтов + + + Сбросить + + + + + {categories.map((cat) => ( + updateCategory(cat.key, v)} + /> + ))} + + + {/* Preview */} + + Предпросмотр + + + Заголовок секции + + + + + Пункт меню + + + Вкладка + + + + + + + Дата + Статус + + + + + 02.07.2026 + В работе + + + + + + + Текст в карточке — описание доставки или заказа. + + + + Основной текст интерфейса. + + + мелкая подпись · временная отметка + + + + + + Настройки сохраняются на этом устройстве + + + ); +}; + +export default SettingsPage; \ No newline at end of file diff --git a/src/router.jsx b/src/router.jsx index 20f16de..1ad44e4 100644 --- a/src/router.jsx +++ b/src/router.jsx @@ -7,6 +7,7 @@ import { GroupDetailPage } from "./pages/GroupDetailPage"; import { LoginPage } from "./pages/LoginPage"; import { NotFoundPage } from "./pages/NotFoundPage"; import { ForbiddenPage } from "./pages/ForbiddenPage"; +import { SettingsPage } from "./pages/SettingsPage"; import { useAuth } from "./context/AuthContext"; /** @@ -66,6 +67,14 @@ export const router = createBrowserRouter([ ), }, + { + path: "settings", + element: ( + + + + ), + }, { path: "*", element: , diff --git a/src/styles/fontSettings.css b/src/styles/fontSettings.css new file mode 100644 index 0000000..6f71344 --- /dev/null +++ b/src/styles/fontSettings.css @@ -0,0 +1,101 @@ +/* Font size scale variables — applied by FontSettingsContext */ +:root { + --fs-scale-table: 1.0; + --fs-scale-card: 1.0; + --fs-scale-nav: 1.0; + --fs-scale-heading: 1.0; + --fs-scale-body: 1.0; + --fs-scale-small: 1.0; +} + +/* Slider styling */ +.fs-slider { + -webkit-appearance: none; + appearance: none; + height: 6px; + border-radius: 3px; + outline: none; + cursor: pointer; +} + +.fs-slider::-webkit-slider-thumb { + -webkit-appearance: none; + appearance: none; + width: 20px; + height: 20px; + border-radius: 50%; + background: var(--color-accent); + border: 3px solid var(--color-surface-strong); + cursor: pointer; + transition: transform 120ms ease; +} + +.fs-slider::-webkit-slider-thumb:hover { + transform: scale(1.2); +} + +.fs-slider::-moz-range-thumb { + width: 20px; + height: 20px; + border-radius: 50%; + background: var(--color-accent); + border: 3px solid var(--color-surface-strong); + cursor: pointer; + border: none; +} + +/* ── Zone overrides ────────────────────────────────────────────────────── + .fs-zone-* wrappers override Tailwind text-* classes inside them. + Specificity: .fs-zone-X .text-Y (0,2,0) > .text-Y (0,1,0). + This lets us scale fonts without patching every component. +────────────────────────────────────────────────────────────────────────── */ + +/* Table zone */ +.fs-zone-table { font-size: calc(0.875rem * var(--fs-scale-table, 1)); } +.fs-zone-table .text-xs { font-size: calc(0.75rem * var(--fs-scale-table, 1)); } +.fs-zone-table .text-sm { font-size: calc(0.875rem * var(--fs-scale-table, 1)); } +.fs-zone-table .text-base { font-size: calc(1rem * var(--fs-scale-table, 1)); } +.fs-zone-table .text-lg { font-size: calc(1.125rem * var(--fs-scale-table, 1)); } +.fs-zone-table .text-xl { font-size: calc(1.25rem * var(--fs-scale-table, 1)); } +/* Arbitrary pixel sizes used in tables */ +.fs-zone-table .text-\[10px\] { font-size: calc(10px * var(--fs-scale-table, 1)); } +.fs-zone-table .text-\[11px\] { font-size: calc(11px * var(--fs-scale-table, 1)); } +.fs-zone-table .text-\[12px\] { font-size: calc(12px * var(--fs-scale-table, 1)); } +.fs-zone-table .text-\[13px\] { font-size: calc(13px * var(--fs-scale-table, 1)); } +.fs-zone-table .text-\[14px\] { font-size: calc(14px * var(--fs-scale-table, 1)); } + +/* Card zone */ +.fs-zone-card { font-size: calc(0.875rem * var(--fs-scale-card, 1)); } +.fs-zone-card .text-xs { font-size: calc(0.75rem * var(--fs-scale-card, 1)); } +.fs-zone-card .text-sm { font-size: calc(0.875rem * var(--fs-scale-card, 1)); } +.fs-zone-card .text-base { font-size: calc(1rem * var(--fs-scale-card, 1)); } +.fs-zone-card .text-lg { font-size: calc(1.125rem * var(--fs-scale-card, 1)); } +.fs-zone-card .text-\[10px\] { font-size: calc(10px * var(--fs-scale-card, 1)); } +.fs-zone-card .text-\[11px\] { font-size: calc(11px * var(--fs-scale-card, 1)); } + +/* Nav zone */ +.fs-zone-nav { font-size: calc(0.875rem * var(--fs-scale-nav, 1)); } +.fs-zone-nav .text-xs { font-size: calc(0.75rem * var(--fs-scale-nav, 1)); } +.fs-zone-nav .text-sm { font-size: calc(0.875rem * var(--fs-scale-nav, 1)); } +.fs-zone-nav .text-base { font-size: calc(1rem * var(--fs-scale-nav, 1)); } + +/* Heading zone */ +.fs-zone-heading { font-size: calc(1rem * var(--fs-scale-heading, 1)); } +.fs-zone-heading .text-sm { font-size: calc(0.875rem * var(--fs-scale-heading, 1)); } +.fs-zone-heading .text-base { font-size: calc(1rem * var(--fs-scale-heading, 1)); } +.fs-zone-heading .text-lg { font-size: calc(1.125rem * var(--fs-scale-heading, 1)); } +.fs-zone-heading .text-xl { font-size: calc(1.25rem * var(--fs-scale-heading, 1)); } +.fs-zone-heading .text-2xl { font-size: calc(1.5rem * var(--fs-scale-heading, 1)); } +.fs-zone-heading .text-3xl { font-size: calc(1.875rem * var(--fs-scale-heading, 1)); } + +/* Small text zone */ +.fs-zone-small { font-size: calc(0.75rem * var(--fs-scale-small, 1)); } +.fs-zone-small .text-xs { font-size: calc(0.75rem * var(--fs-scale-small, 1)); } +.fs-zone-small .text-sm { font-size: calc(0.875rem * var(--fs-scale-small, 1)); } + +/* Body zone — LAST so table/card/nav/heading zones inside body win at equal specificity */ +.fs-zone-body { font-size: calc(1rem * var(--fs-scale-body, 1)); } +.fs-zone-body .text-xs { font-size: calc(0.75rem * var(--fs-scale-body, 1)); } +.fs-zone-body .text-sm { font-size: calc(0.875rem * var(--fs-scale-body, 1)); } +.fs-zone-body .text-base { font-size: calc(1rem * var(--fs-scale-body, 1)); } +.fs-zone-body .text-lg { font-size: calc(1.125rem * var(--fs-scale-body, 1)); } \ No newline at end of file
@@ -184,6 +191,9 @@ export const AppShell = ({ ) : null} + navigate("/settings")} aria-label="Настройки"> + ⚙ +
{category.description}
Настройки
Предпросмотр
+ Текст в карточке — описание доставки или заказа. +
+ Основной текст интерфейса. +
+ мелкая подпись · временная отметка +
+ Настройки сохраняются на этом устройстве +