feat: redesign secure access flows

This commit is contained in:
k1nq
2026-09-22 08:29:49 +05:00
parent b2b1e14cab
commit c70dd11d18
11 changed files with 228 additions and 165 deletions
+2
View File
@@ -14,6 +14,8 @@
"api:generate": "node scripts/generate-api.mjs"
},
"dependencies": {
"@fontsource/funnel-sans": "^5.3.0",
"@fontsource/geist-mono": "^5.3.0",
"@hookform/resolvers": "^5.1.0",
"@tanstack/react-query": "^5.80.0",
"i18next": "^25.2.0",
+85 -69
View File
@@ -8,6 +8,12 @@ importers:
.:
dependencies:
'@fontsource/funnel-sans':
specifier: ^5.3.0
version: 5.3.0
'@fontsource/geist-mono':
specifier: ^5.3.0
version: 5.3.0
'@hookform/resolvers':
specifier: ^5.1.0
version: 5.9.1(react-hook-form@7.87.0(react@19.3.0))(zod@3.25.76)
@@ -59,22 +65,22 @@ importers:
version: 19.3.0(@types/react@19.3.0)
'@vitejs/plugin-react':
specifier: ^4.5.0
version: 4.7.0(vite@6.4.3)
version: 4.7.0(supports-color@10.2.2)(vite@6.4.3)
eslint:
specifier: ^9.28.0
version: 9.39.5
version: 9.39.5(supports-color@10.2.2)
eslint-plugin-jsx-a11y:
specifier: ^6.10.2
version: 6.10.2(eslint@9.39.5)
version: 6.10.2(eslint@9.39.5(supports-color@10.2.2))
eslint-plugin-react-hooks:
specifier: ^5.2.0
version: 5.2.0(eslint@9.39.5)
version: 5.2.0(eslint@9.39.5(supports-color@10.2.2))
globals:
specifier: ^16.2.0
version: 16.5.0
jsdom:
specifier: ^26.1.0
version: 26.1.0
version: 26.1.0(supports-color@10.2.2)
openapi-typescript:
specifier: ^7.8.0
version: 7.13.0(typescript@5.8.3)
@@ -83,13 +89,13 @@ importers:
version: 5.8.3
typescript-eslint:
specifier: ^8.33.0
version: 8.70.0(eslint@9.39.5)(typescript@5.8.3)
version: 8.70.0(eslint@9.39.5(supports-color@10.2.2))(supports-color@10.2.2)(typescript@5.8.3)
vite:
specifier: ^6.3.5
version: 6.4.3
vitest:
specifier: ^3.2.0
version: 3.2.7(jsdom@26.1.0)
version: 3.2.7(jsdom@26.1.0(supports-color@10.2.2))(supports-color@10.2.2)
packages:
@@ -408,6 +414,12 @@ packages:
resolution: {integrity: sha512-43/qtrDUokr7LJqoF2c3+RInu/t4zfrpYdoSDfYyhg52rwLV6TnOvdG4fXm7IkSB3wErkcmJS9iEhjVtOSEjjA==}
engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0}
'@fontsource/funnel-sans@5.3.0':
resolution: {integrity: sha512-LmINq/6qfoYW1LOkOwaiZw7YVEpi3QACa36V7jxgBlO8MPGPbeSxFrPjk4lF6uP1ZbCp2VsBWmneo+1b59GbbA==}
'@fontsource/geist-mono@5.3.0':
resolution: {integrity: sha512-UtJ1BBBCVpMYdIcW7nEB45UAoAw5M53ZXs2t0ciPW+IokuAAIc56M8+kW5tXbRJCTpDw4XTtU7proT6NdQAHTg==}
'@hookform/resolvers@5.9.1':
resolution: {integrity: sha512-7b7vsbraJxKgjVSA1Nur9tLwj539WGJUBLA7QNvXnFoT2pM5Z7G+6rlukk4B2/QrTZy6huRtH6wKeESPKuIr6w==}
peerDependencies:
@@ -2195,16 +2207,16 @@ snapshots:
'@babel/compat-data@7.29.7': {}
'@babel/core@7.29.7':
'@babel/core@7.29.7(supports-color@10.2.2)':
dependencies:
'@babel/code-frame': 7.29.7
'@babel/generator': 7.29.8
'@babel/helper-compilation-targets': 7.29.7
'@babel/helper-module-transforms': 7.29.7(@babel/core@7.29.7)
'@babel/helper-module-transforms': 7.29.7(@babel/core@7.29.7(supports-color@10.2.2))(supports-color@10.2.2)
'@babel/helpers': 7.29.7
'@babel/parser': 7.29.8
'@babel/template': 7.29.7
'@babel/traverse': 7.29.8
'@babel/traverse': 7.29.8(supports-color@10.2.2)
'@babel/types': 7.29.8
'@jridgewell/remapping': 2.3.5
convert-source-map: 2.0.0
@@ -2233,19 +2245,19 @@ snapshots:
'@babel/helper-globals@7.29.7': {}
'@babel/helper-module-imports@7.29.7':
'@babel/helper-module-imports@7.29.7(supports-color@10.2.2)':
dependencies:
'@babel/traverse': 7.29.8
'@babel/traverse': 7.29.8(supports-color@10.2.2)
'@babel/types': 7.29.8
transitivePeerDependencies:
- supports-color
'@babel/helper-module-transforms@7.29.7(@babel/core@7.29.7)':
'@babel/helper-module-transforms@7.29.7(@babel/core@7.29.7(supports-color@10.2.2))(supports-color@10.2.2)':
dependencies:
'@babel/core': 7.29.7
'@babel/helper-module-imports': 7.29.7
'@babel/core': 7.29.7(supports-color@10.2.2)
'@babel/helper-module-imports': 7.29.7(supports-color@10.2.2)
'@babel/helper-validator-identifier': 7.29.7
'@babel/traverse': 7.29.8
'@babel/traverse': 7.29.8(supports-color@10.2.2)
transitivePeerDependencies:
- supports-color
@@ -2266,14 +2278,14 @@ snapshots:
dependencies:
'@babel/types': 7.29.8
'@babel/plugin-transform-react-jsx-self@7.29.7(@babel/core@7.29.7)':
'@babel/plugin-transform-react-jsx-self@7.29.7(@babel/core@7.29.7(supports-color@10.2.2))':
dependencies:
'@babel/core': 7.29.7
'@babel/core': 7.29.7(supports-color@10.2.2)
'@babel/helper-plugin-utils': 7.29.7
'@babel/plugin-transform-react-jsx-source@7.29.7(@babel/core@7.29.7)':
'@babel/plugin-transform-react-jsx-source@7.29.7(@babel/core@7.29.7(supports-color@10.2.2))':
dependencies:
'@babel/core': 7.29.7
'@babel/core': 7.29.7(supports-color@10.2.2)
'@babel/helper-plugin-utils': 7.29.7
'@babel/runtime@7.29.7': {}
@@ -2284,7 +2296,7 @@ snapshots:
'@babel/parser': 7.29.8
'@babel/types': 7.29.8
'@babel/traverse@7.29.8':
'@babel/traverse@7.29.8(supports-color@10.2.2)':
dependencies:
'@babel/code-frame': 7.29.7
'@babel/generator': 7.29.8
@@ -2399,14 +2411,14 @@ snapshots:
'@esbuild/win32-x64@0.25.12':
optional: true
'@eslint-community/eslint-utils@4.10.1(eslint@9.39.5)':
'@eslint-community/eslint-utils@4.10.1(eslint@9.39.5(supports-color@10.2.2))':
dependencies:
eslint: 9.39.5
eslint: 9.39.5(supports-color@10.2.2)
eslint-visitor-keys: 3.4.3
'@eslint-community/regexpp@4.12.2': {}
'@eslint/config-array@0.21.2':
'@eslint/config-array@0.21.2(supports-color@10.2.2)':
dependencies:
'@eslint/object-schema': 2.1.7
debug: 4.4.3(supports-color@10.2.2)
@@ -2422,7 +2434,7 @@ snapshots:
dependencies:
'@types/json-schema': 7.0.15
'@eslint/eslintrc@3.3.7':
'@eslint/eslintrc@3.3.7(supports-color@10.2.2)':
dependencies:
ajv: 6.15.0
debug: 4.4.3(supports-color@10.2.2)
@@ -2445,6 +2457,10 @@ snapshots:
'@eslint/core': 0.17.0
levn: 0.4.1
'@fontsource/funnel-sans@5.3.0': {}
'@fontsource/geist-mono@5.3.0': {}
'@hookform/resolvers@5.9.1(react-hook-form@7.87.0(react@19.3.0))(zod@3.25.76)':
dependencies:
'@standard-schema/utils': 0.3.0
@@ -2676,15 +2692,15 @@ snapshots:
dependencies:
csstype: 3.2.3
'@typescript-eslint/eslint-plugin@8.70.0(@typescript-eslint/parser@8.70.0(eslint@9.39.5)(typescript@5.8.3))(eslint@9.39.5)(typescript@5.8.3)':
'@typescript-eslint/eslint-plugin@8.70.0(@typescript-eslint/parser@8.70.0(eslint@9.39.5(supports-color@10.2.2))(supports-color@10.2.2)(typescript@5.8.3))(eslint@9.39.5(supports-color@10.2.2))(supports-color@10.2.2)(typescript@5.8.3)':
dependencies:
'@eslint-community/regexpp': 4.12.2
'@typescript-eslint/parser': 8.70.0(eslint@9.39.5)(typescript@5.8.3)
'@typescript-eslint/parser': 8.70.0(eslint@9.39.5(supports-color@10.2.2))(supports-color@10.2.2)(typescript@5.8.3)
'@typescript-eslint/scope-manager': 8.70.0
'@typescript-eslint/type-utils': 8.70.0(eslint@9.39.5)(typescript@5.8.3)
'@typescript-eslint/utils': 8.70.0(eslint@9.39.5)(typescript@5.8.3)
'@typescript-eslint/type-utils': 8.70.0(eslint@9.39.5(supports-color@10.2.2))(supports-color@10.2.2)(typescript@5.8.3)
'@typescript-eslint/utils': 8.70.0(eslint@9.39.5(supports-color@10.2.2))(supports-color@10.2.2)(typescript@5.8.3)
'@typescript-eslint/visitor-keys': 8.70.0
eslint: 9.39.5
eslint: 9.39.5(supports-color@10.2.2)
ignore: 7.0.9
natural-compare: 1.4.0
ts-api-utils: 2.5.0(typescript@5.8.3)
@@ -2692,19 +2708,19 @@ snapshots:
transitivePeerDependencies:
- supports-color
'@typescript-eslint/parser@8.70.0(eslint@9.39.5)(typescript@5.8.3)':
'@typescript-eslint/parser@8.70.0(eslint@9.39.5(supports-color@10.2.2))(supports-color@10.2.2)(typescript@5.8.3)':
dependencies:
'@typescript-eslint/scope-manager': 8.70.0
'@typescript-eslint/types': 8.70.0
'@typescript-eslint/typescript-estree': 8.70.0(typescript@5.8.3)
'@typescript-eslint/typescript-estree': 8.70.0(supports-color@10.2.2)(typescript@5.8.3)
'@typescript-eslint/visitor-keys': 8.70.0
debug: 4.4.3(supports-color@10.2.2)
eslint: 9.39.5
eslint: 9.39.5(supports-color@10.2.2)
typescript: 5.8.3
transitivePeerDependencies:
- supports-color
'@typescript-eslint/project-service@8.70.0(typescript@5.8.3)':
'@typescript-eslint/project-service@8.70.0(supports-color@10.2.2)(typescript@5.8.3)':
dependencies:
'@typescript-eslint/tsconfig-utils': 8.70.0(typescript@5.8.3)
'@typescript-eslint/types': 8.70.0
@@ -2722,13 +2738,13 @@ snapshots:
dependencies:
typescript: 5.8.3
'@typescript-eslint/type-utils@8.70.0(eslint@9.39.5)(typescript@5.8.3)':
'@typescript-eslint/type-utils@8.70.0(eslint@9.39.5(supports-color@10.2.2))(supports-color@10.2.2)(typescript@5.8.3)':
dependencies:
'@typescript-eslint/types': 8.70.0
'@typescript-eslint/typescript-estree': 8.70.0(typescript@5.8.3)
'@typescript-eslint/utils': 8.70.0(eslint@9.39.5)(typescript@5.8.3)
'@typescript-eslint/typescript-estree': 8.70.0(supports-color@10.2.2)(typescript@5.8.3)
'@typescript-eslint/utils': 8.70.0(eslint@9.39.5(supports-color@10.2.2))(supports-color@10.2.2)(typescript@5.8.3)
debug: 4.4.3(supports-color@10.2.2)
eslint: 9.39.5
eslint: 9.39.5(supports-color@10.2.2)
ts-api-utils: 2.5.0(typescript@5.8.3)
typescript: 5.8.3
transitivePeerDependencies:
@@ -2736,9 +2752,9 @@ snapshots:
'@typescript-eslint/types@8.70.0': {}
'@typescript-eslint/typescript-estree@8.70.0(typescript@5.8.3)':
'@typescript-eslint/typescript-estree@8.70.0(supports-color@10.2.2)(typescript@5.8.3)':
dependencies:
'@typescript-eslint/project-service': 8.70.0(typescript@5.8.3)
'@typescript-eslint/project-service': 8.70.0(supports-color@10.2.2)(typescript@5.8.3)
'@typescript-eslint/tsconfig-utils': 8.70.0(typescript@5.8.3)
'@typescript-eslint/types': 8.70.0
'@typescript-eslint/visitor-keys': 8.70.0
@@ -2751,13 +2767,13 @@ snapshots:
transitivePeerDependencies:
- supports-color
'@typescript-eslint/utils@8.70.0(eslint@9.39.5)(typescript@5.8.3)':
'@typescript-eslint/utils@8.70.0(eslint@9.39.5(supports-color@10.2.2))(supports-color@10.2.2)(typescript@5.8.3)':
dependencies:
'@eslint-community/eslint-utils': 4.10.1(eslint@9.39.5)
'@eslint-community/eslint-utils': 4.10.1(eslint@9.39.5(supports-color@10.2.2))
'@typescript-eslint/scope-manager': 8.70.0
'@typescript-eslint/types': 8.70.0
'@typescript-eslint/typescript-estree': 8.70.0(typescript@5.8.3)
eslint: 9.39.5
'@typescript-eslint/typescript-estree': 8.70.0(supports-color@10.2.2)(typescript@5.8.3)
eslint: 9.39.5(supports-color@10.2.2)
typescript: 5.8.3
transitivePeerDependencies:
- supports-color
@@ -2767,11 +2783,11 @@ snapshots:
'@typescript-eslint/types': 8.70.0
eslint-visitor-keys: 5.0.1
'@vitejs/plugin-react@4.7.0(vite@6.4.3)':
'@vitejs/plugin-react@4.7.0(supports-color@10.2.2)(vite@6.4.3)':
dependencies:
'@babel/core': 7.29.7
'@babel/plugin-transform-react-jsx-self': 7.29.7(@babel/core@7.29.7)
'@babel/plugin-transform-react-jsx-source': 7.29.7(@babel/core@7.29.7)
'@babel/core': 7.29.7(supports-color@10.2.2)
'@babel/plugin-transform-react-jsx-self': 7.29.7(@babel/core@7.29.7(supports-color@10.2.2))
'@babel/plugin-transform-react-jsx-source': 7.29.7(@babel/core@7.29.7(supports-color@10.2.2))
'@rolldown/pluginutils': 1.0.0-beta.27
'@types/babel__core': 7.20.5
react-refresh: 0.17.0
@@ -3198,7 +3214,7 @@ snapshots:
escape-string-regexp@4.0.0: {}
eslint-plugin-jsx-a11y@6.10.2(eslint@9.39.5):
eslint-plugin-jsx-a11y@6.10.2(eslint@9.39.5(supports-color@10.2.2)):
dependencies:
aria-query: 5.3.2
array-includes: 3.2.0
@@ -3208,7 +3224,7 @@ snapshots:
axobject-query: 4.1.0
damerau-levenshtein: 1.0.8
emoji-regex: 9.2.2
eslint: 9.39.5
eslint: 9.39.5(supports-color@10.2.2)
hasown: 2.0.4
jsx-ast-utils: 3.3.5
language-tags: 1.0.9
@@ -3217,9 +3233,9 @@ snapshots:
safe-regex-test: 1.1.0
string.prototype.includes: 2.0.1
eslint-plugin-react-hooks@5.2.0(eslint@9.39.5):
eslint-plugin-react-hooks@5.2.0(eslint@9.39.5(supports-color@10.2.2)):
dependencies:
eslint: 9.39.5
eslint: 9.39.5(supports-color@10.2.2)
eslint-scope@8.4.0:
dependencies:
@@ -3232,14 +3248,14 @@ snapshots:
eslint-visitor-keys@5.0.1: {}
eslint@9.39.5:
eslint@9.39.5(supports-color@10.2.2):
dependencies:
'@eslint-community/eslint-utils': 4.10.1(eslint@9.39.5)
'@eslint-community/eslint-utils': 4.10.1(eslint@9.39.5(supports-color@10.2.2))
'@eslint-community/regexpp': 4.12.2
'@eslint/config-array': 0.21.2
'@eslint/config-array': 0.21.2(supports-color@10.2.2)
'@eslint/config-helpers': 0.4.2
'@eslint/core': 0.17.0
'@eslint/eslintrc': 3.3.7
'@eslint/eslintrc': 3.3.7(supports-color@10.2.2)
'@eslint/js': 9.39.5
'@eslint/plugin-kit': 0.4.1
'@humanfs/node': 0.16.8
@@ -3417,7 +3433,7 @@ snapshots:
dependencies:
void-elements: 3.1.0
http-proxy-agent@7.0.2:
http-proxy-agent@7.0.2(supports-color@10.2.2):
dependencies:
agent-base: 7.1.4
debug: 4.4.3(supports-color@10.2.2)
@@ -3584,13 +3600,13 @@ snapshots:
dependencies:
argparse: 2.0.1
jsdom@26.1.0:
jsdom@26.1.0(supports-color@10.2.2):
dependencies:
cssstyle: 4.6.0
data-urls: 5.0.0
decimal.js: 10.6.0
html-encoding-sniffer: 4.0.0
http-proxy-agent: 7.0.2
http-proxy-agent: 7.0.2(supports-color@10.2.2)
https-proxy-agent: 7.0.6(supports-color@10.2.2)
is-potential-custom-element-name: 1.0.1
nwsapi: 2.2.27
@@ -4122,13 +4138,13 @@ snapshots:
possible-typed-array-names: 1.1.0
reflect.getprototypeof: 1.0.10
typescript-eslint@8.70.0(eslint@9.39.5)(typescript@5.8.3):
typescript-eslint@8.70.0(eslint@9.39.5(supports-color@10.2.2))(supports-color@10.2.2)(typescript@5.8.3):
dependencies:
'@typescript-eslint/eslint-plugin': 8.70.0(@typescript-eslint/parser@8.70.0(eslint@9.39.5)(typescript@5.8.3))(eslint@9.39.5)(typescript@5.8.3)
'@typescript-eslint/parser': 8.70.0(eslint@9.39.5)(typescript@5.8.3)
'@typescript-eslint/typescript-estree': 8.70.0(typescript@5.8.3)
'@typescript-eslint/utils': 8.70.0(eslint@9.39.5)(typescript@5.8.3)
eslint: 9.39.5
'@typescript-eslint/eslint-plugin': 8.70.0(@typescript-eslint/parser@8.70.0(eslint@9.39.5(supports-color@10.2.2))(supports-color@10.2.2)(typescript@5.8.3))(eslint@9.39.5(supports-color@10.2.2))(supports-color@10.2.2)(typescript@5.8.3)
'@typescript-eslint/parser': 8.70.0(eslint@9.39.5(supports-color@10.2.2))(supports-color@10.2.2)(typescript@5.8.3)
'@typescript-eslint/typescript-estree': 8.70.0(supports-color@10.2.2)(typescript@5.8.3)
'@typescript-eslint/utils': 8.70.0(eslint@9.39.5(supports-color@10.2.2))(supports-color@10.2.2)(typescript@5.8.3)
eslint: 9.39.5(supports-color@10.2.2)
typescript: 5.8.3
transitivePeerDependencies:
- supports-color
@@ -4154,7 +4170,7 @@ snapshots:
dependencies:
punycode: 2.3.1
vite-node@3.2.4:
vite-node@3.2.4(supports-color@10.2.2):
dependencies:
cac: 6.7.14
debug: 4.4.3(supports-color@10.2.2)
@@ -4186,7 +4202,7 @@ snapshots:
optionalDependencies:
fsevents: 2.3.3
vitest@3.2.7(jsdom@26.1.0):
vitest@3.2.7(jsdom@26.1.0(supports-color@10.2.2))(supports-color@10.2.2):
dependencies:
'@types/chai': 5.2.3
'@vitest/expect': 3.2.7
@@ -4209,10 +4225,10 @@ snapshots:
tinypool: 1.1.1
tinyrainbow: 2.0.0
vite: 6.4.3
vite-node: 3.2.4
vite-node: 3.2.4(supports-color@10.2.2)
why-is-node-running: 2.3.0
optionalDependencies:
jsdom: 26.1.0
jsdom: 26.1.0(supports-color@10.2.2)
transitivePeerDependencies:
- jiti
- less
@@ -0,0 +1,51 @@
import type { ReactNode } from 'react'
import { Link } from 'react-router-dom'
import { useTranslation } from 'react-i18next'
import { LangSwitcher } from '@/components/layout/LangSwitcher'
interface AuthLayoutProps {
title: string
subtitle: string
context: string
children: ReactNode
}
export function AuthLayout({ title, subtitle, context, children }: AuthLayoutProps) {
const { t } = useTranslation()
return (
<div className="auth-page">
<aside className="auth-identity">
<Link to="/login" className="brand">
<span className="brand-mark" aria-hidden="true">FS</span>
<span>FINSPACE</span>
</Link>
<div className="auth-manifest">
<h2>{t('authShell.statement')}</h2>
<p>{t('authShell.explanation')}</p>
</div>
<ol className="security-facts">
<li>{t('authShell.session')}</li>
<li>{t('authShell.encryption')}</li>
<li>{t('authShell.isolation')}</li>
</ol>
</aside>
<main className="auth-workspace">
<header className="auth-top">
<span>{context}</span>
<LangSwitcher />
</header>
<section className="auth-card">
<h1 className="page-title">{title}</h1>
<p className="page-subtitle">{subtitle}</p>
{children}
</section>
<footer className="auth-contract">
<span>{t('authShell.private')}</span>
<span>{t('authShell.singleUse')}</span>
<span>{t('authShell.rub')}</span>
</footer>
</main>
</div>
)
}
+1 -1
View File
@@ -42,7 +42,7 @@ export function Modal({ open, onClose, title, children }: ModalProps) {
onClick={onClose}
aria-label={t('common.close')}
>
✕
<span className="close-glyph" aria-hidden="true" />
</button>
</div>
<div className="modal-body">{children}</div>
+13
View File
@@ -39,6 +39,19 @@
"reviewBlockers": "Review blockers",
"categoryPulse": "Category pulse"
},
"authShell": {
"statement": "Your household ledger, sealed by design.",
"explanation": "Invite-only access. Every account, operation and statement stays inside one encrypted financial space.",
"session": "14-day secure session",
"encryption": "AES-GCM statement encryption",
"isolation": "Space-isolated financial data",
"private": "Private workspace",
"singleUse": "Single-use invitations",
"rub": "RUB accounting",
"signIn": "Access / sign in",
"invitation": "Access / invitation",
"reset": "Access / reset password"
},
"lang": {
"label": "Language"
},
+13
View File
@@ -39,6 +39,19 @@
"reviewBlockers": "Разобрать блокировки",
"categoryPulse": "Состояние категорий"
},
"authShell": {
"statement": "Семейный гроссбух, закрытый по замыслу.",
"explanation": "Доступ только по приглашению. Счета, операции и выписки остаются внутри одного зашифрованного пространства.",
"session": "Защищённая сессия на 14 дней",
"encryption": "Шифрование выписок AES-GCM",
"isolation": "Изоляция данных пространства",
"private": "Приватное пространство",
"singleUse": "Одноразовые приглашения",
"rub": "Учёт в рублях",
"signIn": "Доступ / вход",
"invitation": "Доступ / приглашение",
"reset": "Доступ / сброс пароля"
},
"lang": {
"label": "Язык"
},
+8
View File
@@ -1,5 +1,13 @@
import { StrictMode } from 'react'
import { createRoot } from 'react-dom/client'
import '@fontsource/funnel-sans/400.css'
import '@fontsource/funnel-sans/500.css'
import '@fontsource/funnel-sans/600.css'
import '@fontsource/funnel-sans/700.css'
import '@fontsource/geist-mono/400.css'
import '@fontsource/geist-mono/500.css'
import '@fontsource/geist-mono/600.css'
import '@fontsource/geist-mono/700.css'
import App from './App'
import './i18n'
import './styles/global.css'
+7 -14
View File
@@ -6,7 +6,7 @@ import { Link, Navigate, useNavigate, useSearchParams } from 'react-router-dom'
import { z } from 'zod'
import { Button } from '@/components/ui/Button'
import { TextField } from '@/components/ui/fields'
import { LangSwitcher } from '@/components/layout/LangSwitcher'
import { AuthLayout } from '@/components/layout/AuthLayout'
import { useAcceptInvite, useMe } from '@/hooks/useAuth'
import { describeApiError } from '@/lib/apiErrors'
@@ -63,17 +63,11 @@ export function AcceptInvitePage() {
})
return (
<div className="auth-page">
<div className="auth-card card">
<div className="auth-top">
<span className="brand">
<span className="brand-mark" aria-hidden="true" />
{t('app.brand')}
</span>
<LangSwitcher />
</div>
<h1 className="page-title">{t('auth.inviteTitle')}</h1>
<p className="page-subtitle">{t('auth.inviteSubtitle')}</p>
<AuthLayout
title={t('auth.inviteTitle')}
subtitle={t('auth.inviteSubtitle')}
context={t('authShell.invitation')}
>
{apiError ? (
<div className="banner banner-danger" role="alert">
{apiError}
@@ -114,7 +108,6 @@ export function AcceptInvitePage() {
<p className="auth-alt">
<Link to="/login">{t('auth.backToLogin')}</Link>
</p>
</div>
</div>
</AuthLayout>
)
}
+7 -14
View File
@@ -6,7 +6,7 @@ import { Link, Navigate, useLocation, useNavigate } from 'react-router-dom'
import { z } from 'zod'
import { Button } from '@/components/ui/Button'
import { TextField } from '@/components/ui/fields'
import { LangSwitcher } from '@/components/layout/LangSwitcher'
import { AuthLayout } from '@/components/layout/AuthLayout'
import { useLogin, useMe } from '@/hooks/useAuth'
import { describeApiError } from '@/lib/apiErrors'
@@ -54,17 +54,11 @@ export function LoginPage() {
})
return (
<div className="auth-page">
<div className="auth-card card">
<div className="auth-top">
<span className="brand">
<span className="brand-mark" aria-hidden="true" />
{t('app.brand')}
</span>
<LangSwitcher />
</div>
<h1 className="page-title">{t('auth.loginTitle')}</h1>
<p className="page-subtitle">{t('auth.loginSubtitle')}</p>
<AuthLayout
title={t('auth.loginTitle')}
subtitle={t('auth.loginSubtitle')}
context={t('authShell.signIn')}
>
{apiError ? (
<div className="banner banner-danger" role="alert">
{apiError}
@@ -95,7 +89,6 @@ export function LoginPage() {
<p className="auth-alt">
<Link to="/invite/accept">{t('auth.haveInvite')}</Link>
</p>
</div>
</div>
</AuthLayout>
)
}
+7 -14
View File
@@ -4,7 +4,7 @@ import { useForm } from 'react-hook-form'
import { useTranslation } from 'react-i18next'
import { Link, useNavigate, useSearchParams } from 'react-router-dom'
import { z } from 'zod'
import { LangSwitcher } from '@/components/layout/LangSwitcher'
import { AuthLayout } from '@/components/layout/AuthLayout'
import { Button } from '@/components/ui/Button'
import { TextField } from '@/components/ui/fields'
import { useConfirmPasswordReset, useRequestPasswordReset } from '@/hooks/useAuth'
@@ -73,17 +73,11 @@ export function ResetPasswordPage() {
})
return (
<div className="auth-page">
<div className="auth-card card">
<div className="auth-top">
<span className="brand">
<span className="brand-mark" aria-hidden="true" />
{t('app.brand')}
</span>
<LangSwitcher />
</div>
<h1 className="page-title">{t('auth.resetTitle')}</h1>
<p className="page-subtitle">{t('auth.resetSubtitle')}</p>
<AuthLayout
title={t('auth.resetTitle')}
subtitle={t('auth.resetSubtitle')}
context={t('authShell.reset')}
>
{apiError ? (
<div className="banner banner-danger" role="alert">
{apiError}
@@ -130,7 +124,6 @@ export function ResetPasswordPage() {
<p className="auth-alt">
<Link to="/login">{t('auth.backToLogin')}</Link>
</p>
</div>
</div>
</AuthLayout>
)
}
+34 -53
View File
@@ -1,5 +1,3 @@
@import url('https://fonts.googleapis.com/css2?family=Funnel+Sans:wght@400;500;600;700&family=Geist+Mono:wght@400;500;600;700&display=swap');
:root {
color-scheme: light;
--paper: #f7f5ef;
@@ -384,6 +382,11 @@ a {
.btn-danger:hover:not(:disabled) { background: #8e241d; }
.btn-small { min-height: 28px; padding: 5px 8px; font-family: var(--font-data); font-size: 9px; }
.btn-icon { width: 36px; min-height: 36px; padding: 0; }
.close-glyph { position: relative; width: 14px; height: 14px; display: block; }
.close-glyph::before,
.close-glyph::after { content: ''; position: absolute; left: 6px; top: 0; width: 1px; height: 14px; background: currentColor; }
.close-glyph::before { transform: rotate(45deg); }
.close-glyph::after { transform: rotate(-45deg); }
.lang-switch { display: inline-flex; }
.lang-switch .btn { color: #81817d; }
@@ -672,55 +675,26 @@ input[type='file']::file-selector-button { min-height: 30px; margin-right: 10px;
.pulse-track span { display: block; height: 100%; background: var(--blue); }
.mobile-primary-action { display: none; }
.auth-page {
min-height: 100vh;
display: grid;
place-items: center;
padding: 34px;
background: linear-gradient(90deg, var(--ink) 0 39%, var(--paper) 39% 100%);
}
.auth-card {
width: min(560px, calc(61vw - 96px));
margin-left: 39vw;
padding: 34px 0;
}
.auth-card::before {
content: 'FS';
position: fixed;
left: 56px;
top: 54px;
display: grid;
place-items: center;
width: 40px;
height: 40px;
color: var(--white);
background: var(--blue);
font: 700 11px var(--font-data);
}
.auth-card::after {
content: 'Your household ledger, sealed by design.';
position: fixed;
left: 56px;
top: 150px;
width: min(31vw, 410px);
color: var(--white);
font-size: clamp(34px, 4vw, 52px);
font-weight: 600;
line-height: 0.98;
letter-spacing: -0.035em;
}
.auth-top { display: flex; align-items: center; justify-content: space-between; gap: 16px; margin-bottom: 44px; }
.auth-top .brand { color: var(--blue); }
.auth-top .brand-mark { display: none; }
.auth-page { min-height: 100vh; display: grid; grid-template-columns: minmax(360px, 39%) minmax(0, 1fr); background: var(--paper); }
.auth-identity { min-height: 100vh; display: grid; grid-template-rows: auto 1fr auto; gap: 64px; padding: 54px 56px; color: var(--white); background: var(--ink); }
.auth-manifest { align-self: start; max-width: 460px; padding-top: 56px; }
.auth-manifest h2 { max-width: 9ch; margin: 0; font-size: clamp(40px, 4.2vw, 58px); font-weight: 600; line-height: 0.96; letter-spacing: -0.035em; }
.auth-manifest p { max-width: 48ch; margin: 24px 0 0; color: #aaa9a5; font-size: 15px; line-height: 1.55; }
.security-facts { margin: 0; padding: 0; list-style: none; counter-reset: facts; border-top: 1px solid #ffffff33; }
.security-facts li { counter-increment: facts; display: grid; grid-template-columns: 28px 1fr; gap: 14px; padding: 14px 0; color: #d5d5d1; border-bottom: 1px solid #ffffff22; font-size: 13px; }
.security-facts li::before { content: counter(facts, decimal-leading-zero); color: #6f8bc6; font-family: var(--font-data); font-size: 9px; }
.auth-workspace { min-width: 0; min-height: 100vh; display: grid; grid-template-rows: auto 1fr auto; gap: 28px; padding: 48px clamp(42px, 6vw, 90px) 34px; }
.auth-top { display: flex; align-items: center; justify-content: space-between; gap: 16px; color: var(--blue); font-family: var(--font-data); font-size: 9px; font-weight: 700; letter-spacing: 0.08em; text-transform: uppercase; }
.auth-top .lang-switch .btn { color: var(--muted); }
.auth-top .lang-switch .is-active { color: var(--white); }
.auth-card { align-self: center; width: min(550px, 100%); padding: 28px 0 42px; }
.auth-card .page-title { font-size: 38px; }
.auth-card .page-subtitle { margin-bottom: 28px; }
.auth-card .form-grid { margin-top: 22px; }
.auth-card .btn-primary { width: 100%; min-height: 48px; }
.auth-alt { margin: 16px 0 0; color: var(--muted); font-size: 13px; }
.auth-contract { display: grid; grid-template-columns: repeat(3, 1fr); gap: 14px; padding-top: 14px; border-top: 1px solid var(--line); }
.auth-contract span { color: var(--muted); font-family: var(--font-data); font-size: 8px; line-height: 1.4; text-transform: uppercase; }
@media (max-width: 1120px) {
:root { --sidebar: 214px; }
@@ -781,6 +755,7 @@ input[type='file']::file-selector-button { min-height: 30px; margin-right: 10px;
.mobile-pool small { color: #a9a9a4; font-family: var(--font-data); font-size: 8px; text-transform: uppercase; }
.mobile-pool strong { margin: 4px 0; font-size: 28px; }
.overview-stats .stat:first-child { display: none; }
.overview-stats .stat:nth-child(4) { display: block; }
.overview-workbench { display: block; margin-top: 20px; }
.month-story { display: none; }
.decision-panel { margin-bottom: 24px; padding: 14px; color: var(--ink); background: var(--amber-soft); }
@@ -793,8 +768,8 @@ input[type='file']::file-selector-button { min-height: 30px; margin-right: 10px;
.pulse-grid { display: block; border: 0; }
.pulse-item { padding: 13px 0; border-width: 0 0 1px; background: transparent; }
.mobile-primary-action { position: fixed; right: 12px; bottom: 72px; z-index: 45; display: block; }
.mobile-primary-action .btn { width: 48px; height: 48px; padding: 0; overflow: hidden; color: transparent; }
.mobile-primary-action .btn::after { content: '+'; color: var(--white); font: 500 25px var(--font-ui); }
.mobile-primary-action .btn { position: relative; width: 48px; height: 48px; padding: 0; overflow: hidden; color: transparent; }
.mobile-primary-action .btn::after { content: '+'; position: absolute; inset: 0; display: grid; place-items: center; color: var(--white); font: 500 25px var(--font-ui); }
.filters { grid-template-columns: 1fr 1fr; }
.filters .field:first-child { grid-column: 1 / -1; }
.filters .checkbox-field { grid-column: 1 / -1; }
@@ -825,12 +800,18 @@ input[type='file']::file-selector-button { min-height: 30px; margin-right: 10px;
.modal { width: 100vw; }
.modal-head { padding: 18px 16px; }
.modal-body { padding: 18px 16px 90px; }
.auth-page { display: block; padding: 0; background: var(--paper); }
.auth-card { width: 100%; min-height: 100vh; margin: 0; padding: 96px 20px 40px; }
.auth-card::before { position: absolute; left: 20px; top: 24px; }
.auth-card::after { display: none; }
.auth-top { margin-bottom: 34px; }
.auth-page { display: block; background: var(--paper); }
.auth-identity { min-height: 180px; display: block; padding: 20px; }
.auth-identity .brand { margin-bottom: 34px; }
.auth-manifest { padding: 0; }
.auth-manifest h2 { max-width: 12ch; font-size: 31px; }
.auth-manifest p,
.security-facts { display: none; }
.auth-workspace { min-height: calc(100vh - 180px); display: block; padding: 20px 20px 36px; }
.auth-top { margin-bottom: 42px; }
.auth-card { width: 100%; padding: 0; }
.auth-card .page-title { font-size: 32px; }
.auth-contract { margin-top: 42px; }
}
@media (prefers-reduced-motion: reduce) {