From bfd6bf9619eee4d4fba47befc70d288e2ab262a0 Mon Sep 17 00:00:00 2001 From: VitekHub Date: Thu, 28 May 2026 15:12:14 +0200 Subject: [PATCH 1/3] feat: add landing page with hero, features, and how-it-works sections --- src/app/layouts/PublicLayout.tsx | 6 +- src/app/router.test.tsx | 5 +- src/app/routes/__root.tsx | 2 + src/app/routes/index.tsx | 5 +- src/app/styles/globals.css | 64 +++++++++++---------- src/features/landing/ui/CtaButtons.tsx | 22 ++++++++ src/features/landing/ui/FeaturesGrid.tsx | 52 +++++++++++++++++ src/features/landing/ui/HeroSection.tsx | 31 ++++++++++ src/features/landing/ui/HowItWorks.tsx | 66 ++++++++++++++++++++++ src/features/landing/ui/LandingPage.tsx | 33 +++++++++++ src/features/landing/ui/SecurityBanner.tsx | 21 +++++++ src/shared/i18n/config.ts | 2 +- src/shared/i18n/locales/cs/common.json | 4 ++ src/shared/i18n/locales/cs/landing.json | 48 ++++++++++++++++ src/shared/i18n/locales/en/common.json | 4 ++ src/shared/i18n/locales/en/landing.json | 48 ++++++++++++++++ src/shared/ui/PreAlphaBanner.tsx | 21 +++++++ src/shared/ui/nav/PublicHeader.tsx | 24 ++++++++ src/shared/ui/nav/ThemeToggle.tsx | 37 ++++++++++++ 19 files changed, 456 insertions(+), 39 deletions(-) create mode 100644 src/features/landing/ui/CtaButtons.tsx create mode 100644 src/features/landing/ui/FeaturesGrid.tsx create mode 100644 src/features/landing/ui/HeroSection.tsx create mode 100644 src/features/landing/ui/HowItWorks.tsx create mode 100644 src/features/landing/ui/LandingPage.tsx create mode 100644 src/features/landing/ui/SecurityBanner.tsx create mode 100644 src/shared/i18n/locales/cs/landing.json create mode 100644 src/shared/i18n/locales/en/landing.json create mode 100644 src/shared/ui/PreAlphaBanner.tsx create mode 100644 src/shared/ui/nav/PublicHeader.tsx create mode 100644 src/shared/ui/nav/ThemeToggle.tsx diff --git a/src/app/layouts/PublicLayout.tsx b/src/app/layouts/PublicLayout.tsx index ee8a056..157d6ca 100644 --- a/src/app/layouts/PublicLayout.tsx +++ b/src/app/layouts/PublicLayout.tsx @@ -1,14 +1,12 @@ import { Outlet } from '@tanstack/react-router' -import { LanguageSwitcher } from '@/shared/ui/nav/LanguageSwitcher' +import { PublicHeader } from '@/shared/ui/nav/PublicHeader' function PublicLayout() { return (
-
- -
+
diff --git a/src/app/router.test.tsx b/src/app/router.test.tsx index e9c965b..f75c627 100644 --- a/src/app/router.test.tsx +++ b/src/app/router.test.tsx @@ -34,10 +34,11 @@ function renderWithRouter(authOverrides: Partial = {}, initialPath } describe('Router redirects', () => { - it('redirects / to /login when not authenticated', async () => { + it('does not redirect / to /login when not authenticated', async () => { const { router } = renderWithRouter({}, '/') await waitFor(() => { - expect(router.state.location.pathname).toBe('/login') + expect(router.state.location.pathname).not.toBe('/login') + expect(router.state.location.pathname).toBe('/') }) }) diff --git a/src/app/routes/__root.tsx b/src/app/routes/__root.tsx index 02c330d..e10d6ac 100644 --- a/src/app/routes/__root.tsx +++ b/src/app/routes/__root.tsx @@ -3,6 +3,7 @@ import { ThemeProvider } from '@/shared/lib/theme-provider' import { Toaster } from '@/shared/ui/sonner' import { PageSkeleton } from '@/app/Pending' import { RootErrorBoundary } from '@/app/ErrorBoundary' +import { PreAlphaBanner } from '@/shared/ui/PreAlphaBanner' import type { AuthContext } from '@/shared/auth/auth-context' interface RouterContext { @@ -18,6 +19,7 @@ export const Route = createRootRouteWithContext()({ function RootLayout() { return ( + diff --git a/src/app/routes/index.tsx b/src/app/routes/index.tsx index c4b718e..5301eb1 100644 --- a/src/app/routes/index.tsx +++ b/src/app/routes/index.tsx @@ -1,12 +1,15 @@ import { createFileRoute, redirect } from '@tanstack/react-router' +import { lazy } from 'react' + +const LandingPage = lazy(() => import('@/features/landing/ui/LandingPage')) const Route = createFileRoute('/')({ beforeLoad: ({ context }) => { if (context.auth.isAuthenticated) { throw redirect({ to: '/dashboard' }) } - throw redirect({ to: '/login' }) }, + component: LandingPage, }) export { Route } diff --git a/src/app/styles/globals.css b/src/app/styles/globals.css index 08cc101..86baeed 100644 --- a/src/app/styles/globals.css +++ b/src/app/styles/globals.css @@ -51,43 +51,45 @@ } :root { - --background: oklch(1 0 0); - --foreground: oklch(0.145 0 0); - --card: oklch(0.995 0.005 285); - --card-foreground: oklch(0.145 0 0); - --popover: oklch(0.995 0.005 285); - --popover-foreground: oklch(0.145 0 0); - --primary: oklch(0.4 0.17 285); + color-scheme: light; + --background: oklch(0.98 0.004 215); + --foreground: oklch(0.18 0.015 215); + --card: oklch(1 0 0); + --card-foreground: oklch(0.18 0.015 215); + --popover: oklch(1 0 0); + --popover-foreground: oklch(0.18 0.015 215); + --primary: oklch(0.42 0.14 215); --primary-foreground: oklch(0.985 0 0); - --secondary: oklch(0.97 0.006 285); - --secondary-foreground: oklch(0.3 0.12 285); - --muted: oklch(0.96 0.008 285); - --muted-foreground: oklch(0.5 0.08 285); - --accent: oklch(0.96 0.012 300); - --accent-foreground: oklch(0.3 0.12 285); + --secondary: oklch(0.94 0.012 215); + --secondary-foreground: oklch(0.28 0.1 215); + --muted: oklch(0.93 0.008 215); + --muted-foreground: oklch(0.46 0.05 215); + --accent: oklch(0.93 0.014 200); + --accent-foreground: oklch(0.28 0.1 215); --destructive: oklch(0.577 0.245 20); - --success: oklch(0.55 0.17 155); - --warning: oklch(0.82 0.16 80); - --border: oklch(0.922 0 0); - --input: oklch(0.922 0 0); - --ring: oklch(0.55 0.15 285); - --chart-1: oklch(0.55 0.17 285); - --chart-2: oklch(0.55 0.14 300); - --chart-3: oklch(0.6 0.14 190); - --chart-4: oklch(0.6 0.14 150); - --chart-5: oklch(0.65 0.15 70); + --success: oklch(0.52 0.17 155); + --warning: oklch(0.72 0.16 80); + --border: oklch(0.88 0.012 215); + --input: oklch(0.84 0.016 215); + --ring: oklch(0.5 0.12 215); + --chart-1: oklch(0.5 0.15 215); + --chart-2: oklch(0.52 0.13 190); + --chart-3: oklch(0.55 0.14 160); + --chart-4: oklch(0.55 0.14 130); + --chart-5: oklch(0.6 0.15 70); --radius: 0.625rem; - --sidebar: oklch(0.995 0.006 285); - --sidebar-foreground: oklch(0.145 0 0); - --sidebar-primary: oklch(0.4 0.17 285); + --sidebar: oklch(0.955 0.01 215); + --sidebar-foreground: oklch(0.18 0.015 215); + --sidebar-primary: oklch(0.42 0.14 215); --sidebar-primary-foreground: oklch(0.985 0 0); - --sidebar-accent: oklch(0.96 0.012 300); - --sidebar-accent-foreground: oklch(0.3 0.12 285); - --sidebar-border: oklch(0.922 0 0); - --sidebar-ring: oklch(0.55 0.15 285); + --sidebar-accent: oklch(0.93 0.014 200); + --sidebar-accent-foreground: oklch(0.28 0.1 215); + --sidebar-border: oklch(0.88 0.012 215); + --sidebar-ring: oklch(0.5 0.12 215); } .dark { + color-scheme: dark; --background: oklch(0.18 0.01 285); --foreground: oklch(0.985 0 0); --card: oklch(0.205 0.02 285); @@ -132,7 +134,7 @@ } html { @apply font-sans; - color-scheme: dark; + scrollbar-gutter: stable; } button, a, diff --git a/src/features/landing/ui/CtaButtons.tsx b/src/features/landing/ui/CtaButtons.tsx new file mode 100644 index 0000000..8a2ec52 --- /dev/null +++ b/src/features/landing/ui/CtaButtons.tsx @@ -0,0 +1,22 @@ +import { Link } from '@tanstack/react-router' +import { useTranslation } from 'react-i18next' + +import { buttonVariants } from '@/shared/ui/button' +import { cn } from '@/shared/lib/utils' + +function CtaButtons() { + const { t } = useTranslation('landing') + + return ( +
+ + {t('hero.cta')} + + + {t('hero.login')} + +
+ ) +} + +export { CtaButtons } diff --git a/src/features/landing/ui/FeaturesGrid.tsx b/src/features/landing/ui/FeaturesGrid.tsx new file mode 100644 index 0000000..5cd696f --- /dev/null +++ b/src/features/landing/ui/FeaturesGrid.tsx @@ -0,0 +1,52 @@ +import { ShieldCheck, KeyRound, Fingerprint, Lock } from 'lucide-react' +import { useTranslation } from 'react-i18next' +import type { LucideIcon } from 'lucide-react' + +interface FeatureItem { + icon: LucideIcon + titleKey: string + descriptionKey: string +} + +const FEATURES: FeatureItem[] = [ + { icon: ShieldCheck, titleKey: 'features.zeroKnowledge.title', descriptionKey: 'features.zeroKnowledge.description' }, + { icon: KeyRound, titleKey: 'features.splitKey.title', descriptionKey: 'features.splitKey.description' }, + { icon: Fingerprint, titleKey: 'features.recovery.title', descriptionKey: 'features.recovery.description' }, + { icon: Lock, titleKey: 'features.openDesign.title', descriptionKey: 'features.openDesign.description' }, +] + +function FeaturesGrid() { + const { t } = useTranslation('landing') + + return ( +
+
+

+ {t('features.heading')} +

+ +
+ {FEATURES.map((feature) => ( + + ))} +
+
+
+ ) +} + +function FeatureCard({ feature, t }: { feature: FeatureItem; t: (key: string) => string }) { + const Icon = feature.icon + + return ( +
+
+ +
+

{t(feature.titleKey)}

+

{t(feature.descriptionKey)}

+
+ ) +} + +export { FeaturesGrid } diff --git a/src/features/landing/ui/HeroSection.tsx b/src/features/landing/ui/HeroSection.tsx new file mode 100644 index 0000000..cf2a28d --- /dev/null +++ b/src/features/landing/ui/HeroSection.tsx @@ -0,0 +1,31 @@ +import { useTranslation } from 'react-i18next' + +import { CipherNoteIcon } from '@/shared/ui/brand/CipherNoteIcon' +import { CtaButtons } from '@/features/landing/ui/CtaButtons' + +function HeroSection() { + const { t } = useTranslation('landing') + + return ( +
+
+
+ + Cipher Note +
+ +

+ {t('hero.title')} +

+ +

+ {t('hero.subtitle')} +

+ + +
+
+ ) +} + +export { HeroSection } diff --git a/src/features/landing/ui/HowItWorks.tsx b/src/features/landing/ui/HowItWorks.tsx new file mode 100644 index 0000000..ccacc50 --- /dev/null +++ b/src/features/landing/ui/HowItWorks.tsx @@ -0,0 +1,66 @@ +import { UserPlus, PenLine, ShieldCheck } from 'lucide-react' +import { useTranslation } from 'react-i18next' +import type { LucideIcon } from 'lucide-react' + +interface Step { + icon: LucideIcon + titleKey: string + descriptionKey: string +} + +const STEPS: Step[] = [ + { icon: UserPlus, titleKey: 'howItWorks.step1.title', descriptionKey: 'howItWorks.step1.description' }, + { icon: PenLine, titleKey: 'howItWorks.step2.title', descriptionKey: 'howItWorks.step2.description' }, + { icon: ShieldCheck, titleKey: 'howItWorks.step3.title', descriptionKey: 'howItWorks.step3.description' }, +] + +function HowItWorks() { + const { t } = useTranslation('landing') + + return ( +
+
+

+ {t('howItWorks.heading')} +

+ +
+ {STEPS.map((step, index) => ( + + ))} +
+
+
+ ) +} + +interface StepCardProps { + step: Step + index: number + last: boolean + t: (key: string) => string +} + +function StepCard({ step, index, last, t }: StepCardProps) { + const Icon = step.icon + + return ( +
+ + {index + 1} + +
+ {!last && ( +
+ )} +
+ +
+
+

{t(step.titleKey)}

+

{t(step.descriptionKey)}

+
+ ) +} + +export { HowItWorks } diff --git a/src/features/landing/ui/LandingPage.tsx b/src/features/landing/ui/LandingPage.tsx new file mode 100644 index 0000000..50d8c1d --- /dev/null +++ b/src/features/landing/ui/LandingPage.tsx @@ -0,0 +1,33 @@ +import { useTranslation } from 'react-i18next' + +import { HeroSection } from '@/features/landing/ui/HeroSection' +import { FeaturesGrid } from '@/features/landing/ui/FeaturesGrid' +import { HowItWorks } from '@/features/landing/ui/HowItWorks' +import { SecurityBanner } from '@/features/landing/ui/SecurityBanner' +import { PublicHeader } from '@/shared/ui/nav/PublicHeader' + +function LandingPage() { + const { t } = useTranslation('landing') + + return ( +
+
+
+ + + +
+ + + + +
+ +
+

{t('footer.tagline')}

+
+
+ ) +} + +export default LandingPage diff --git a/src/features/landing/ui/SecurityBanner.tsx b/src/features/landing/ui/SecurityBanner.tsx new file mode 100644 index 0000000..7807c42 --- /dev/null +++ b/src/features/landing/ui/SecurityBanner.tsx @@ -0,0 +1,21 @@ +import { useTranslation } from 'react-i18next' + +import { CtaButtons } from '@/features/landing/ui/CtaButtons' + +function SecurityBanner() { + const { t } = useTranslation('landing') + + return ( +
+
+

+ {t('security.statement')} +

+ + +
+
+ ) +} + +export { SecurityBanner } diff --git a/src/shared/i18n/config.ts b/src/shared/i18n/config.ts index 0ea1fa0..3529c1f 100644 --- a/src/shared/i18n/config.ts +++ b/src/shared/i18n/config.ts @@ -10,7 +10,7 @@ void i18n .init({ fallbackLng: 'en', supportedLngs: ['en', 'cs'], - ns: ['common', 'auth', 'fields', 'settings', 'crypto'], + ns: ['common', 'auth', 'fields', 'settings', 'crypto', 'landing'], defaultNS: 'common', interpolation: { escapeValue: false, diff --git a/src/shared/i18n/locales/cs/common.json b/src/shared/i18n/locales/cs/common.json index 311e8cb..7f74f13 100644 --- a/src/shared/i18n/locales/cs/common.json +++ b/src/shared/i18n/locales/cs/common.json @@ -16,6 +16,10 @@ "loading": "Načítání...", "error": "Něco se pokazilo" }, + "preAlpha": { + "ariaLabel": "Upozornění na předběžnou verzi", + "message": "Cipher Note je předběžná alfa verze. Očekávejte zásadní změny a občasné resety dat." + }, "nav": { "dashboard": "Nástěnka", "settings": "Nastavení", diff --git a/src/shared/i18n/locales/cs/landing.json b/src/shared/i18n/locales/cs/landing.json new file mode 100644 index 0000000..f998aba --- /dev/null +++ b/src/shared/i18n/locales/cs/landing.json @@ -0,0 +1,48 @@ +{ + "hero": { + "title": "Vaše poznámky. Vaše klíče.", + "subtitle": "End-to-end šifrované poznámky, které ani naše servery nedokážou přečíst. Postaveno na zero-knowledge kryptografii.", + "cta": "Vytvořit účet zdarma", + "login": "Přihlásit se" + }, + "features": { + "heading": "Bezpečnost bez kompromisu", + "zeroKnowledge": { + "title": "Zero-Knowledge šifrování", + "description": "Vaše heslo nikdy neopustí zařízení. Šifrovací klíče odvozujeme lokálně - server vidí pouze šifrovaný text." + }, + "splitKey": { + "title": "Rozdělená derivace klíčů", + "description": "Jedno heslo, dva nezávislé klíče: jeden pro autentizaci, druhý pro šifrování. Ani narušení serveru nic neodhalí." + }, + "recovery": { + "title": "Záloha obnovovací frází", + "description": "BIP-39 mnemonická fráze vám umožní obnovit data, pokud zapomenete heslo. Zapište si ji a uložte na bezpečné místo." + }, + "openDesign": { + "title": "Transparentní architektura", + "description": "Postaveno na AES-256-GCM, Argon2id a HKDF. Žádné proprietární algoritmy. Každé kryptografické rozhodnutí je ověřitelné." + } + }, + "howItWorks": { + "heading": "Jak to funguje", + "step1": { + "title": "Vytvořte si účet", + "description": "Zvolte uživatelské jméno a heslo. Vaše heslo se nikdy neodesílá na server - k autentizaci se používá pouze odvozený hash." + }, + "step2": { + "title": "Zapište své poznámky", + "description": "Přidejte poznámky, webové stránky a e-maily do šifrovaného trezoru. Rozhraní je jednoduché a přehledné." + }, + "step3": { + "title": "Okamžitě zašifrováno", + "description": "Každé pole je zašifrováno jedinečným klíčem před opuštěním prohlížeče. Server ukládá pouze šifrovaný text." + } + }, + "security": { + "statement": "Postaveno na AES-256-GCM, Argon2id a HKDF. Žádné sledování. Žádné reklamy. Žádné kompromisy." + }, + "footer": { + "tagline": "Soukromí není vymoženost - je to základ." + } +} diff --git a/src/shared/i18n/locales/en/common.json b/src/shared/i18n/locales/en/common.json index 5af01ca..61ecf3c 100644 --- a/src/shared/i18n/locales/en/common.json +++ b/src/shared/i18n/locales/en/common.json @@ -16,6 +16,10 @@ "loading": "Loading...", "error": "Something went wrong" }, + "preAlpha": { + "ariaLabel": "Pre-alpha software notice", + "message": "Cipher Note is in pre-alpha version. Expect breaking changes and periodic data resets." + }, "nav": { "dashboard": "Dashboard", "settings": "Settings", diff --git a/src/shared/i18n/locales/en/landing.json b/src/shared/i18n/locales/en/landing.json new file mode 100644 index 0000000..97c5eb6 --- /dev/null +++ b/src/shared/i18n/locales/en/landing.json @@ -0,0 +1,48 @@ +{ + "hero": { + "title": "Your notes. Your keys.", + "subtitle": "End-to-end encrypted notes that not even our servers can read. Built on zero-knowledge cryptography.", + "cta": "Create a free account", + "login": "Sign in" + }, + "features": { + "heading": "Security without compromise", + "zeroKnowledge": { + "title": "Zero-Knowledge Encryption", + "description": "Your password never leaves your device. We derive encryption keys locally - the server only sees ciphertext." + }, + "splitKey": { + "title": "Split Key Derivation", + "description": "One password, two independent keys: one for authentication, one for encryption. Even a server breach reveals nothing." + }, + "recovery": { + "title": "Recovery Phrase Backup", + "description": "A BIP-39 mnemonic phrase lets you recover your data if you forget your password. Write it down, keep it safe." + }, + "openDesign": { + "title": "Transparent Architecture", + "description": "Built on AES-256-GCM, Argon2id, and HKDF. No proprietary algorithms. Every cryptographic choice is auditable." + } + }, + "howItWorks": { + "heading": "How it works", + "step1": { + "title": "Create your account", + "description": "Pick a username and password. Your password is never sent to the server - only a derived hash is used for authentication." + }, + "step2": { + "title": "Write your notes", + "description": "Add notes, websites, and emails to your encrypted vault. The interface is simple and focused." + }, + "step3": { + "title": "Encrypted instantly", + "description": "Every field is encrypted with a unique key before leaving your browser. The server stores only ciphertext." + } + }, + "security": { + "statement": "Built on AES-256-GCM, Argon2id, and HKDF. No tracking. No ads. No compromise." + }, + "footer": { + "tagline": "Privacy is not a feature - it's the foundation." + } +} diff --git a/src/shared/ui/PreAlphaBanner.tsx b/src/shared/ui/PreAlphaBanner.tsx new file mode 100644 index 0000000..87d1968 --- /dev/null +++ b/src/shared/ui/PreAlphaBanner.tsx @@ -0,0 +1,21 @@ +import { useTranslation } from 'react-i18next' +import { TriangleAlert } from 'lucide-react' + +function PreAlphaBanner() { + const { t } = useTranslation('common') + + return ( +
+
+
+
+ ) +} + +export { PreAlphaBanner } diff --git a/src/shared/ui/nav/PublicHeader.tsx b/src/shared/ui/nav/PublicHeader.tsx new file mode 100644 index 0000000..41f8180 --- /dev/null +++ b/src/shared/ui/nav/PublicHeader.tsx @@ -0,0 +1,24 @@ +import { Link } from '@tanstack/react-router' + +import { CipherNoteIcon } from '@/shared/ui/brand/CipherNoteIcon' +import { LanguageSwitcher } from '@/shared/ui/nav/LanguageSwitcher' +import { ThemeToggle } from '@/shared/ui/nav/ThemeToggle' + +function PublicHeader() { + return ( +
+
+ + + Cipher Note + +
+ + +
+
+
+ ) +} + +export { PublicHeader } diff --git a/src/shared/ui/nav/ThemeToggle.tsx b/src/shared/ui/nav/ThemeToggle.tsx new file mode 100644 index 0000000..b28571c --- /dev/null +++ b/src/shared/ui/nav/ThemeToggle.tsx @@ -0,0 +1,37 @@ +import { Moon, Sun, Monitor } from 'lucide-react' + +import { Button } from '@/shared/ui/button' +import { useTheme } from '@/shared/lib/theme-provider' + +const THEME_CYCLE = ['dark', 'light', 'system'] as const +type Theme = (typeof THEME_CYCLE)[number] + +const THEME_ICONS: Record = { + dark: , + light: , + system: , +} + +const THEME_LABELS: Record = { + dark: 'Switch to light theme', + light: 'Switch to system theme', + system: 'Switch to dark theme', +} + +function ThemeToggle() { + const { theme, setTheme } = useTheme() + + const cycleTheme = () => { + const currentIndex = THEME_CYCLE.indexOf(theme as Theme) + const nextIndex = (currentIndex + 1) % THEME_CYCLE.length + setTheme(THEME_CYCLE[nextIndex]) + } + + return ( + + ) +} + +export { ThemeToggle } From 43cfe97c1a714a308fceb17e8ff0156bd18bd07b Mon Sep 17 00:00:00 2001 From: VitekHub Date: Thu, 28 May 2026 16:26:17 +0200 Subject: [PATCH 2/3] feat: replace theme toggle with segmented control and add theme preferences --- src/app/layouts/ProtectedLayout.tsx | 2 +- src/app/styles/globals.css | 1 + .../settings/ui/PreferencesSection.test.tsx | 2 +- .../settings/ui/PreferencesSection.tsx | 7 ++- src/shared/i18n/locales/cs/settings.json | 1 + src/shared/i18n/locales/en/settings.json | 1 + src/shared/ui/PreAlphaBanner.tsx | 4 +- src/shared/ui/SegmentedControl.tsx | 40 ++++++++++++ src/shared/ui/nav/LanguageSwitcher.test.tsx | 12 ++-- src/shared/ui/nav/LanguageSwitcher.tsx | 26 ++++---- src/shared/ui/nav/PublicHeader.tsx | 6 +- src/shared/ui/nav/ThemeSwitcher.tsx | 62 +++++++++++++++++++ src/shared/ui/nav/ThemeToggle.tsx | 37 ----------- 13 files changed, 136 insertions(+), 65 deletions(-) create mode 100644 src/shared/ui/SegmentedControl.tsx create mode 100644 src/shared/ui/nav/ThemeSwitcher.tsx delete mode 100644 src/shared/ui/nav/ThemeToggle.tsx diff --git a/src/app/layouts/ProtectedLayout.tsx b/src/app/layouts/ProtectedLayout.tsx index aeb4f42..14a1cbf 100644 --- a/src/app/layouts/ProtectedLayout.tsx +++ b/src/app/layouts/ProtectedLayout.tsx @@ -32,7 +32,7 @@ function ProtectedLayout() { useVaultTimeout() return ( -
+
{/* Desktop sidebar */}