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 ( -
+
diff --git a/src/components/orders/OrdersTable.jsx b/src/components/orders/OrdersTable.jsx index fbadd6d..f6774f0 100644 --- a/src/components/orders/OrdersTable.jsx +++ b/src/components/orders/OrdersTable.jsx @@ -94,7 +94,7 @@ export const OrdersTable = ({ } return ( - +
diff --git a/src/context/FontSettingsContext.jsx b/src/context/FontSettingsContext.jsx new file mode 100644 index 0000000..9511cf1 --- /dev/null +++ b/src/context/FontSettingsContext.jsx @@ -0,0 +1,73 @@ +import React, { createContext, useContext, useEffect, useState } from "react"; + +const FontSettingsContext = createContext(null); + +const STORAGE_KEY = "supersam-font-settings"; + +// Categories: key → { label, defaultScale, min, max, step, description } +export const FONT_CATEGORIES = [ + { key: "table", label: "Таблицы", description: "Текст в таблицах и списках", defaultScale: 1.0, min: 0.8, max: 1.5, step: 0.05 }, + { key: "card", label: "Карточки", description: "Текст в карточках и панелях", defaultScale: 1.0, min: 0.8, max: 1.5, step: 0.05 }, + { key: "nav", label: "Меню и навигация", description: "Пункты меню, вкладки, кнопки", defaultScale: 1.0, min: 0.8, max: 1.5, step: 0.05 }, + { key: "heading", label: "Заголовки", description: "Названия секций и страниц", defaultScale: 1.0, min: 0.8, max: 1.6, step: 0.05 }, + { key: "body", label: "Основной текст", description: "Обычный текст в интерфейсе", defaultScale: 1.0, min: 0.8, max: 1.5, step: 0.05 }, + { key: "small", label: "Мелкий текст", description: "Подписи, метки, временные отметки", defaultScale: 1.0, min: 0.8, max: 1.5, step: 0.05 }, +]; + +const DEFAULT_SETTINGS = FONT_CATEGORIES.reduce((acc, cat) => { + acc[cat.key] = cat.defaultScale; + return acc; +}, {}); + +function loadSettings() { + try { + const raw = localStorage.getItem(STORAGE_KEY); + if (!raw) return { ...DEFAULT_SETTINGS }; + const parsed = JSON.parse(raw); + // Merge with defaults to handle new categories + return { ...DEFAULT_SETTINGS, ...parsed }; + } catch { + return { ...DEFAULT_SETTINGS }; + } +} + +function applySettings(settings) { + const root = document.documentElement; + for (const cat of FONT_CATEGORIES) { + root.style.setProperty(`--fs-scale-${cat.key}`, String(settings[cat.key] ?? 1.0)); + } +} + +export const FontSettingsProvider = ({ children }) => { + const [settings, setSettings] = useState(loadSettings); + + useEffect(() => { + applySettings(settings); + localStorage.setItem(STORAGE_KEY, JSON.stringify(settings)); + }, [settings]); + + const updateCategory = (key, scale) => { + setSettings((prev) => ({ ...prev, [key]: scale })); + }; + + const resetAll = () => { + setSettings({ ...DEFAULT_SETTINGS }); + }; + + const value = { + settings, + updateCategory, + resetAll, + categories: FONT_CATEGORIES, + }; + + return {children}; +}; + +export const useFontSettings = () => { + const context = useContext(FontSettingsContext); + if (!context) { + throw new Error("useFontSettings must be used within FontSettingsProvider"); + } + return context; +}; \ No newline at end of file diff --git a/src/layouts/AppShell.jsx b/src/layouts/AppShell.jsx index 85626c8..53a162d 100644 --- a/src/layouts/AppShell.jsx +++ b/src/layouts/AppShell.jsx @@ -1,4 +1,5 @@ import React from "react"; +import { useNavigate } from "react-router-dom"; import { ROLE_LABELS } from "../constants/roles"; import { Badge } from "../components/UI/Badge"; import { Button } from "../components/UI/Button"; @@ -47,7 +48,7 @@ export const AppShell = ({
{/* Desktop sidebar */} - +

Панель @@ -81,6 +82,9 @@ export const AppShell = ({ {isGuideOpen ? "К рабочей области" : "Справка"} ) : null} + @@ -88,9 +92,9 @@ export const AppShell = ({ {/* Main content area */} -

+
{/* Mobile header */} - +

@@ -117,6 +121,9 @@ export const AppShell = ({ ) : null} + ) : null} +

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 */} +
+
+

Настройки

+

+ Настройки интерфейса +

+
+ +
+ + {/* Presets */} + +

+ Быстрые пресеты +

+
+ {PRESETS.map((preset, i) => ( + + ))} +
+
+ + {/* 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