diff --git a/FEATURES.md b/FEATURES.md index 2ae105f0a..7e4f84bb9 100644 --- a/FEATURES.md +++ b/FEATURES.md @@ -99,6 +99,7 @@ - Drag and drop to organize mail and assign tags - A guided tour for first-time users - Right-click menus, and toasts that offer an undo +- Every screen has a permalink. `/mail/message/`, `/mail/thread/`, `/mail/folder/inbox`, `/calendar/day/2026-08-06`, `/calendar/event/`, `/contacts/`, `/files/`, and `/settings/` all open directly, the address bar follows what you are looking at, and "Copy link" is in the message, conversation, and event menus. Dashboards and launchers can link straight to a message; browser back and forward walk the mail UI. - Toolbar position, favicon, and login branding are configurable - Sidebar apps pin and reorder by drag - Settings sync between devices, encrypted diff --git a/app/(main)/[locale]/auth/callback/page.tsx b/app/(main)/[locale]/auth/callback/page.tsx index d994fda80..a52a17ead 100644 --- a/app/(main)/[locale]/auth/callback/page.tsx +++ b/app/(main)/[locale]/auth/callback/page.tsx @@ -5,6 +5,7 @@ import { useRouter, useSearchParams } from "next/navigation"; import { useTranslations } from "next-intl"; import { useAuthStore } from "@/stores/auth-store"; import { apiFetch, getPathPrefix, toRouterPath } from "@/lib/browser-navigation"; +import { buildSettingsPath } from "@/lib/deep-links"; import { Loader2, AlertCircle } from "lucide-react"; import { Button } from "@/components/ui/button"; import { useParams } from "next/navigation"; @@ -56,11 +57,13 @@ function OAuthCallbackInner() { } try { sessionStorage.setItem("pair_reauth_done", "1"); - // Land back on the Security tab (readPersistedTab reads this key). - sessionStorage.setItem("settings-deep-link-tab", "security"); } catch { /* ignore */ } + // Land back on the Security tab. The tab is part of the URL now + // (#733), so the redirect target says where it is going. const prefix = getPathPrefix(params.locale as string); - router.push(toRouterPath(`${prefix}/${params.locale}/settings`)); + router.push(toRouterPath( + `${prefix}/${params.locale}${buildSettingsPath("security")}`, + )); } catch { setError("token_exchange_failed"); } diff --git a/app/(main)/[locale]/calendar/[[...segments]]/page.tsx b/app/(main)/[locale]/calendar/[[...segments]]/page.tsx new file mode 100644 index 000000000..2a8cb8a5c --- /dev/null +++ b/app/(main)/[locale]/calendar/[[...segments]]/page.tsx @@ -0,0 +1,15 @@ +import { CalendarApp } from "@/components/calendar/calendar-app"; + +// Calendar permalinks (#733): +// /calendar the stored view, today +// /calendar// a view anchored on a date +// /calendar/event/ one event, with ?calendar= to +// disambiguate across accounts +export default async function CalendarRoute({ + params, +}: { + params: Promise<{ segments?: string[] }>; +}) { + const { segments } = await params; + return ; +} diff --git a/app/(main)/[locale]/contacts/[[...segments]]/page.tsx b/app/(main)/[locale]/contacts/[[...segments]]/page.tsx new file mode 100644 index 000000000..4adf80a48 --- /dev/null +++ b/app/(main)/[locale]/contacts/[[...segments]]/page.tsx @@ -0,0 +1,13 @@ +import { ContactsApp } from "@/components/contacts/contacts-app"; + +// Contact permalinks (#733): /contacts/[/edit], plus /contacts/new +// for the "add this sender" flow. The older query form (?contactId=, ?addEmail=) +// still works - see parseContactsPath. +export default async function ContactsRoute({ + params, +}: { + params: Promise<{ segments?: string[] }>; +}) { + const { segments } = await params; + return ; +} diff --git a/app/(main)/[locale]/files/[[...segments]]/page.tsx b/app/(main)/[locale]/files/[[...segments]]/page.tsx new file mode 100644 index 000000000..810b38d15 --- /dev/null +++ b/app/(main)/[locale]/files/[[...segments]]/page.tsx @@ -0,0 +1,12 @@ +import { FilesApp } from "@/components/files/files-app"; + +// Files permalinks (#733): /files//, one path segment per +// level, with ?preview= to open a file straight into the preview modal. +export default async function FilesRoute({ + params, +}: { + params: Promise<{ segments?: string[] }>; +}) { + const { segments } = await params; + return ; +} diff --git a/app/(main)/[locale]/login/page.tsx b/app/(main)/[locale]/login/page.tsx index 296b7c07b..01fbed66f 100644 --- a/app/(main)/[locale]/login/page.tsx +++ b/app/(main)/[locale]/login/page.tsx @@ -11,6 +11,7 @@ import { useAccountStore } from "@/stores/account-store"; import { useThemeStore } from "@/stores/theme-store"; import { useShallow } from "zustand/react/shallow"; import { useConfig } from "@/hooks/use-config"; +import { useMenuNavigation } from "@/hooks/use-menu-navigation"; import { apiFetch, getPathPrefix, toRouterPath, withBasePath } from "@/lib/browser-navigation"; import { cn } from "@/lib/utils"; import { AlertCircle, Loader2, X, Info, Eye, EyeOff, LogIn, Sun, Moon, Monitor, Check, Shield, Play, Copy } from "lucide-react"; @@ -183,6 +184,13 @@ export default function LoginPage() { const totpInputRef = useRef(null); const prevError = useRef(null); const themeMenuRef = useRef(null); + const themeButtonRef = useRef(null); + const closeThemeMenu = useCallback(() => setShowThemeMenu(false), []); + const { menuRef: themeListRef, onKeyDown: onThemeMenuKeyDown } = useMenuNavigation({ + open: showThemeMenu, + onClose: closeThemeMenu, + triggerRef: themeButtonRef, + }); // Captured by handleSubmit when in mobile handoff mode; consumed by the // isAuthenticated effect to build the deep-link fragment. const mobileHandoffPayloadRef = useRef<{ server_url: string; username: string; password: string } | null>(null); @@ -689,6 +697,7 @@ export default function LoginPage() {