diff --git a/apps/web/package.json b/apps/web/package.json index 85623f6..3408846 100644 --- a/apps/web/package.json +++ b/apps/web/package.json @@ -16,6 +16,8 @@ "@authportal/shared": "workspace:*", "@authportal/ui": "workspace:*", "@base-ui/react": "^1.5.0", + "@fontsource-variable/geist": "^5.3.0", + "@fontsource-variable/geist-mono": "^5.3.0", "@hookform/resolvers": "^5.4.0", "@simplewebauthn/browser": "^13.3.0", "@tailwindcss/vite": "^4.3.1", diff --git a/apps/web/src/design-preview/access-dialog.tsx b/apps/web/src/design-preview/access-dialog.tsx index e699ec1..c22a5ee 100644 --- a/apps/web/src/design-preview/access-dialog.tsx +++ b/apps/web/src/design-preview/access-dialog.tsx @@ -181,7 +181,6 @@ export function AccessDialog({ Отмена + + + + ) } diff --git a/apps/web/src/design-preview/design-preview.css b/apps/web/src/design-preview/design-preview.css index c298762..e554b47 100644 --- a/apps/web/src/design-preview/design-preview.css +++ b/apps/web/src/design-preview/design-preview.css @@ -1,3 +1,6 @@ +@import '@fontsource-variable/geist'; +@import '@fontsource-variable/geist-mono'; + /* Плотность и контраст макета. Атрибуты ставит PreviewProvider и снимает при выходе. */ html[data-preview-density='compact'] [data-slot='data-grid-table'] :is(th, td) { padding-block: 0.25rem; @@ -12,3 +15,14 @@ html[data-preview-contrast='high'] { --border: color-mix(in oklch, var(--foreground) 32%, var(--background)); --input: color-mix(in oklch, var(--foreground) 40%, var(--background)); } + +/* Шрифты дизайн-системы (Geist / Geist Mono) — только пока смонтирован макет. */ +html[data-preview-density] { + --font-sans: 'Geist Variable', ui-sans-serif, system-ui, sans-serif; + --font-mono: 'Geist Mono Variable', ui-monospace, monospace; +} + +html[data-preview-density], +html[data-preview-density] body { + font-family: var(--font-sans); +} diff --git a/apps/web/src/design-preview/pages/app-links-page.tsx b/apps/web/src/design-preview/pages/app-links-page.tsx index d6a7f1f..df11b99 100644 --- a/apps/web/src/design-preview/pages/app-links-page.tsx +++ b/apps/web/src/design-preview/pages/app-links-page.tsx @@ -79,7 +79,7 @@ export function AppLinksPage() { const columns = useMemo[]>( () => [ - { id: 'name', title: 'Приложение', size: 260, cell: (r) => }, + { id: 'name', title: 'Приложение', size: 260, cell: (r) => }, { id: 'menu', title: 'В меню', size: 150, cell: (r) => {r.enabled ? 'В меню' : 'Скрыто'} }, { id: 'icon', title: 'Иконка', size: 110, cell: (r) => {r.icon} }, { id: 'url', title: 'URL', size: 260, cell: (r) => {r.url} }, @@ -193,7 +193,6 @@ export function AppLinksPage() { <> diff --git a/apps/web/src/design-preview/preview-provider.tsx b/apps/web/src/design-preview/preview-provider.tsx index aa50ef0..b86407e 100644 --- a/apps/web/src/design-preview/preview-provider.tsx +++ b/apps/web/src/design-preview/preview-provider.tsx @@ -17,6 +17,25 @@ export function PreviewProvider({ children }: { children: ReactNode }) { const [paletteOpen, setPaletteOpen] = useState(false) const [density, setDensity] = useState('comfortable') const [contrast, setContrast] = useState('standard') + const [pins, setPins] = useState(() => { + try { + const raw = window.localStorage.getItem('design-preview:pins') + return raw ? (JSON.parse(raw) as string[]) : [] + } catch { + return [] + } + }) + const togglePin = useCallback((id: string) => { + setPins((prev) => { + const next = prev.includes(id) ? prev.filter((x) => x !== id) : [...prev, id] + try { + window.localStorage.setItem('design-preview:pins', JSON.stringify(next)) + } catch { + /* без сохранения закреплённых */ + } + return next + }) + }, []) const seq = useRef(0) const pendingRef = useRef(pending) useEffect(() => { @@ -60,7 +79,7 @@ export function PreviewProvider({ children }: { children: ReactNode }) { const count = pendingRef.current.length setPending([]) toast.success(`Сохранено изменений: ${count}`, { - description: 'Макет: записи в журнал не отправляются.', + description: 'Записано в портал и журнал «Изменения» (макет: ничего не отправляется).', }) }, []) @@ -72,12 +91,14 @@ export function PreviewProvider({ children }: { children: ReactNode }) { applyAll, paletteOpen, setPaletteOpen, + pins, + togglePin, density, setDensity, contrast, setContrast, }), - [pending, addChange, discardAll, applyAll, paletteOpen, density, contrast], + [pending, addChange, discardAll, applyAll, paletteOpen, pins, togglePin, density, contrast], ) return ( diff --git a/apps/web/src/design-preview/preview-shell.tsx b/apps/web/src/design-preview/preview-shell.tsx index 41e3b36..fa231c5 100644 --- a/apps/web/src/design-preview/preview-shell.tsx +++ b/apps/web/src/design-preview/preview-shell.tsx @@ -9,7 +9,16 @@ import './design-preview.css' /** Оболочка макета: сайдбар, шапка, палитра и панель «Не сохранено». Монтируется после гидрации. */ export function PreviewShell({ children }: { children: ReactNode }) { const [mounted, setMounted] = useState(false) - useEffect(() => setMounted(true), []) + const [open, setOpen] = useState(true) + useEffect(() => { + // До 1100px меню сворачивается в колонку иконок (на мобильных его заменяет Sheet). + const mq = window.matchMedia('(min-width: 1100px)') + const sync = () => setOpen(mq.matches) + sync() + mq.addEventListener('change', sync) + setMounted(true) + return () => mq.removeEventListener('change', sync) + }, []) if (!mounted) { return ( @@ -22,7 +31,7 @@ export function PreviewShell({ children }: { children: ReactNode }) { return ( - + void paletteOpen: boolean setPaletteOpen: (open: boolean) => void + pins: string[] + togglePin: (id: string) => void density: Density setDensity: (v: Density) => void contrast: Contrast diff --git a/apps/web/src/design-preview/section-grid.tsx b/apps/web/src/design-preview/section-grid.tsx index 4759481..b485cbb 100644 --- a/apps/web/src/design-preview/section-grid.tsx +++ b/apps/web/src/design-preview/section-grid.tsx @@ -31,6 +31,8 @@ import { Separator } from '@authportal/ui/components/separator' import { InputGroup, InputGroupAddon, InputGroupInput } from '@authportal/ui/components/input-group' import { cn } from '@authportal/ui/lib/utils' import { Segmented } from './bits' +import type { Tone } from './mock' +import { TONE_DOT } from './tone' export type GridColumn = { id: string @@ -84,7 +86,23 @@ export function PreviewSectionGrid({ emptyMessage?: string className?: string }) { - const [view, setView] = useState(0) + const storageKey = `design-preview:view:${title}` + const [view, setViewState] = useState(() => { + try { + const v = Number(window.localStorage.getItem(storageKey)) + return Number.isInteger(v) && v >= 0 && v < (views?.length ?? 0) ? v : 0 + } catch { + return 0 + } + }) + const setView = (i: number) => { + setViewState(i) + try { + window.localStorage.setItem(storageKey, String(i)) + } catch { + /* без сохранения вида */ + } + } const [query, setQuery] = useState('') const [sorting, setSorting] = useState([]) const [rowSelection, setRowSelection] = useState({}) @@ -170,7 +188,7 @@ export function PreviewSectionGrid({ {views ? ( v.label)} + options={views.map((v) => `${v.label} · ${rows.filter(v.match).length}`)} value={view} onChange={(i) => { setView(i) @@ -233,11 +251,14 @@ export function PreviewSectionGrid({ } /** Двухстрочная ячейка: заголовок + подпись. */ -export function TwoLineCell({ title, sub }: { title: ReactNode; sub?: ReactNode }) { +export function TwoLineCell({ title, sub, dot }: { title: ReactNode; sub?: ReactNode; dot?: Tone }) { return ( -
- {title} - {sub ? {sub} : null} +
+ {dot ? : null} +
+ {title} + {sub ? {sub} : null} +
) } diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index a9f882a..e3e1f43 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -129,7 +129,7 @@ importers: version: 8.64.0(eslint@10.7.0(jiti@2.7.0))(typescript@6.0.3) vitest: specifier: 'catalog:' - version: 4.1.10(@types/node@22.20.1)(vite@8.1.5(@types/node@22.20.1)(esbuild@0.27.7)(jiti@2.7.0)(tsx@4.23.1)) + version: 4.1.10(@types/node@22.20.1)(vite@8.1.5(@types/node@22.20.1)(esbuild@0.28.1)(jiti@2.7.0)(tsx@4.23.1)) apps/web: dependencies: @@ -142,6 +142,12 @@ importers: '@base-ui/react': specifier: ^1.5.0 version: 1.6.0(@date-fns/tz@1.5.0)(@types/react@19.2.17)(date-fns@4.4.0)(react-dom@19.2.7(react@19.2.7))(react@19.2.7) + '@fontsource-variable/geist': + specifier: ^5.3.0 + version: 5.3.0 + '@fontsource-variable/geist-mono': + specifier: ^5.3.0 + version: 5.3.0 '@hookform/resolvers': specifier: ^5.4.0 version: 5.4.0(react-hook-form@7.82.0(react@19.2.7)) @@ -303,7 +309,7 @@ importers: version: 8.64.0(eslint@10.7.0(jiti@2.7.0))(typescript@6.0.3) vitest: specifier: 'catalog:' - version: 4.1.10(@types/node@24.13.3)(vite@8.1.5(@types/node@24.13.3)(esbuild@0.28.1)(jiti@2.7.0)(tsx@4.23.1)) + version: 4.1.10(@types/node@24.13.3)(vite@8.1.5(@types/node@24.13.3)(esbuild@0.27.7)(jiti@2.7.0)(tsx@4.23.1)) packages/ui: dependencies: @@ -1261,6 +1267,12 @@ packages: '@floating-ui/utils@0.2.12': resolution: {integrity: sha512-HpCo8tmWzLVad5s2d19EhAz5zqrrQ6s69qd6moPMQvkOuSwDT1YgRfWSVuc4ennqrgv3OHppiOGMQ7oC13yIww==} + '@fontsource-variable/geist-mono@5.3.0': + resolution: {integrity: sha512-vBbuwDEo9AkrqADMXOrlAR3DFcJi4/JxeuU43FoiQERnNwsfXNnvxvReZG02cQKmyk4DZkZdBZX3oTDvy2zBAw==} + + '@fontsource-variable/geist@5.3.0': + resolution: {integrity: sha512-j0m+vLQuG5XAYoHtGCVu0spvlGreR3EzpECUVzkFmI1mTVnAO38l/NEPDCFgZ177JxzYJCLSmTQibIiYPilGrA==} + '@hexagon/base64@1.1.28': resolution: {integrity: sha512-lhqDEAvWixy3bZ+UOYbPwUbBkwBq5C1LAJ/xPC8Oi+lL54oyakv/npbA0aU2hgCsx/1NUd4IBvV03+aUBWxerw==} @@ -5705,6 +5717,10 @@ snapshots: '@floating-ui/utils@0.2.12': {} + '@fontsource-variable/geist-mono@5.3.0': {} + + '@fontsource-variable/geist@5.3.0': {} + '@hexagon/base64@1.1.28': {} '@hookform/resolvers@5.4.0(react-hook-form@7.82.0(react@19.2.7))': @@ -6666,13 +6682,21 @@ snapshots: chai: 6.2.2 tinyrainbow: 3.1.0 - '@vitest/mocker@4.1.10(vite@8.1.5(@types/node@22.20.1)(esbuild@0.27.7)(jiti@2.7.0)(tsx@4.23.1))': + '@vitest/mocker@4.1.10(vite@8.1.5(@types/node@22.20.1)(esbuild@0.28.1)(jiti@2.7.0)(tsx@4.23.1))': dependencies: '@vitest/spy': 4.1.10 estree-walker: 3.0.3 magic-string: 0.30.21 optionalDependencies: - vite: 8.1.5(@types/node@22.20.1)(esbuild@0.27.7)(jiti@2.7.0)(tsx@4.23.1) + vite: 8.1.5(@types/node@22.20.1)(esbuild@0.28.1)(jiti@2.7.0)(tsx@4.23.1) + + '@vitest/mocker@4.1.10(vite@8.1.5(@types/node@24.13.3)(esbuild@0.27.7)(jiti@2.7.0)(tsx@4.23.1))': + dependencies: + '@vitest/spy': 4.1.10 + estree-walker: 3.0.3 + magic-string: 0.30.21 + optionalDependencies: + vite: 8.1.5(@types/node@24.13.3)(esbuild@0.27.7)(jiti@2.7.0)(tsx@4.23.1) '@vitest/mocker@4.1.10(vite@8.1.5(@types/node@24.13.3)(esbuild@0.28.1)(jiti@2.7.0)(tsx@4.23.1))': dependencies: @@ -9121,7 +9145,7 @@ snapshots: vary@1.1.2: {} - vite@8.1.5(@types/node@22.20.1)(esbuild@0.27.7)(jiti@2.7.0)(tsx@4.23.1): + vite@8.1.5(@types/node@22.20.1)(esbuild@0.28.1)(jiti@2.7.0)(tsx@4.23.1): dependencies: lightningcss: 1.32.0 picomatch: 4.0.5 @@ -9130,6 +9154,20 @@ snapshots: tinyglobby: 0.2.17 optionalDependencies: '@types/node': 22.20.1 + esbuild: 0.28.1 + fsevents: 2.3.3 + jiti: 2.7.0 + tsx: 4.23.1 + + vite@8.1.5(@types/node@24.13.3)(esbuild@0.27.7)(jiti@2.7.0)(tsx@4.23.1): + dependencies: + lightningcss: 1.32.0 + picomatch: 4.0.5 + postcss: 8.5.19 + rolldown: 1.1.5 + tinyglobby: 0.2.17 + optionalDependencies: + '@types/node': 24.13.3 esbuild: 0.27.7 fsevents: 2.3.3 jiti: 2.7.0 @@ -9149,10 +9187,10 @@ snapshots: jiti: 2.7.0 tsx: 4.23.1 - vitest@4.1.10(@types/node@22.20.1)(vite@8.1.5(@types/node@22.20.1)(esbuild@0.27.7)(jiti@2.7.0)(tsx@4.23.1)): + vitest@4.1.10(@types/node@22.20.1)(vite@8.1.5(@types/node@22.20.1)(esbuild@0.28.1)(jiti@2.7.0)(tsx@4.23.1)): dependencies: '@vitest/expect': 4.1.10 - '@vitest/mocker': 4.1.10(vite@8.1.5(@types/node@22.20.1)(esbuild@0.27.7)(jiti@2.7.0)(tsx@4.23.1)) + '@vitest/mocker': 4.1.10(vite@8.1.5(@types/node@22.20.1)(esbuild@0.28.1)(jiti@2.7.0)(tsx@4.23.1)) '@vitest/pretty-format': 4.1.10 '@vitest/runner': 4.1.10 '@vitest/snapshot': 4.1.10 @@ -9169,13 +9207,40 @@ snapshots: tinyexec: 1.2.4 tinyglobby: 0.2.17 tinyrainbow: 3.1.0 - vite: 8.1.5(@types/node@22.20.1)(esbuild@0.27.7)(jiti@2.7.0)(tsx@4.23.1) + vite: 8.1.5(@types/node@22.20.1)(esbuild@0.28.1)(jiti@2.7.0)(tsx@4.23.1) why-is-node-running: 2.3.0 optionalDependencies: '@types/node': 22.20.1 transitivePeerDependencies: - msw + vitest@4.1.10(@types/node@24.13.3)(vite@8.1.5(@types/node@24.13.3)(esbuild@0.27.7)(jiti@2.7.0)(tsx@4.23.1)): + dependencies: + '@vitest/expect': 4.1.10 + '@vitest/mocker': 4.1.10(vite@8.1.5(@types/node@24.13.3)(esbuild@0.27.7)(jiti@2.7.0)(tsx@4.23.1)) + '@vitest/pretty-format': 4.1.10 + '@vitest/runner': 4.1.10 + '@vitest/snapshot': 4.1.10 + '@vitest/spy': 4.1.10 + '@vitest/utils': 4.1.10 + es-module-lexer: 2.3.1 + expect-type: 1.4.0 + magic-string: 0.30.21 + obug: 2.1.4 + pathe: 2.0.3 + picomatch: 4.0.5 + std-env: 4.2.0 + tinybench: 2.9.0 + tinyexec: 1.2.4 + tinyglobby: 0.2.17 + tinyrainbow: 3.1.0 + vite: 8.1.5(@types/node@24.13.3)(esbuild@0.27.7)(jiti@2.7.0)(tsx@4.23.1) + why-is-node-running: 2.3.0 + optionalDependencies: + '@types/node': 24.13.3 + transitivePeerDependencies: + - msw + vitest@4.1.10(@types/node@24.13.3)(vite@8.1.5(@types/node@24.13.3)(esbuild@0.28.1)(jiti@2.7.0)(tsx@4.23.1)): dependencies: '@vitest/expect': 4.1.10