bb551d1d59
- Sidebar: the hairline above the user block is inset to the content edges instead of running edge-to-edge (concept language). - User menu: the Discord community link renders the actual Discord mark (inlined simple-icons path, CC0) instead of lucide MessagesSquare. - Dialogs: open/close animation toned down, zoom 98 instead of 95 and 150ms instead of 200ms. - Settings modal: switching tabs no longer remounts the intercepted route (and replayed the whole open animation on every click). The rail now swaps sections via history.replaceState, which Next syncs into usePathname(), so the dialog stays mounted and tab switches are instant. Verified via Playwright: dialog DOM node survives three tab switches, URL tracks the section, Esc still closes back. - /chat loading: the shared dashboard skeleton stretched edge-to-edge in chat's full-bleed wrapper (chat's own layout is what suspends, so a chat/loading.tsx cannot catch it). The shared fallback is now route- aware and renders a two-pane chat silhouette for /chat. Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
74 lines
3.1 KiB
TypeScript
74 lines
3.1 KiB
TypeScript
'use client'
|
|
|
|
import { usePathname, useRouter } from 'next/navigation'
|
|
import { useTranslations } from 'next-intl'
|
|
import { useCompany } from '@/contexts/CompanyContext'
|
|
import {
|
|
Dialog,
|
|
DialogContent,
|
|
DialogDescription,
|
|
DialogTitle,
|
|
} from '@/components/ui/dialog'
|
|
import { SETTINGS_SECTIONS } from './sections'
|
|
import { SettingsShell } from './SettingsShell'
|
|
import { ActiveCompanyBadge } from './ActiveCompanyBadge'
|
|
|
|
/**
|
|
* The settings popup. Rendered only by the intercepting route
|
|
* (`@settings/(.)settings/[[...section]]`) on in-app soft navigation, so its
|
|
* mere presence means "open". Closing pops the history entry that opened it,
|
|
* returning the user to the page they came from (which stayed mounted in the
|
|
* `children` slot behind the scrim). On hard load / refresh / deep-link the
|
|
* interceptor doesn't fire and the real full-page settings render instead.
|
|
*/
|
|
export function SettingsModal({ sectionId }: { sectionId?: string }) {
|
|
const router = useRouter()
|
|
const pathname = usePathname()
|
|
const { company } = useCompany()
|
|
const t = useTranslations('settings_modal')
|
|
|
|
// Tab clicks inside the modal update the URL shallowly (see SettingsRail),
|
|
// so the pathname is the live source of truth for the active section; the
|
|
// sectionId route param only covers the very first intercepted render.
|
|
// Bare /settings (or an unknown section) defaults to company, or to account
|
|
// when there is no active company (the no-company escape hatch).
|
|
const urlSection = pathname.split('/')[2] ?? sectionId
|
|
const resolved =
|
|
urlSection && SETTINGS_SECTIONS[urlSection]
|
|
? urlSection
|
|
: company
|
|
? 'company'
|
|
: 'account'
|
|
|
|
function onOpenChange(open: boolean) {
|
|
if (!open) router.back()
|
|
}
|
|
|
|
// Parallel-route safety net. This modal lives in the @settingsModal slot and
|
|
// should only ever show on /settings/* routes. On a soft navigation to a
|
|
// non-settings route (e.g. a cross-link inside the modal like "Kontoplan"),
|
|
// Next.js can keep this intercepted slot mounted over the new page. Once the
|
|
// URL is no longer a settings route, render nothing so those links actually
|
|
// leave the modal instead of appearing to do nothing.
|
|
if (!pathname.startsWith('/settings')) return null
|
|
|
|
return (
|
|
<Dialog open onOpenChange={onOpenChange}>
|
|
<DialogContent
|
|
className="flex h-[100dvh] max-h-[100dvh] max-w-none flex-col gap-0 overflow-hidden rounded-none p-0 md:h-auto md:max-h-[85dvh] md:max-w-4xl md:rounded-lg"
|
|
>
|
|
<div className="flex shrink-0 items-center gap-3 border-b border-border px-6 py-4">
|
|
<DialogTitle className="font-display text-lg tracking-tight">
|
|
{t('title')}
|
|
</DialogTitle>
|
|
{/* The modal covers the sidebar's CompanySwitcher, so the active
|
|
company must stay visible here (mr-6 clears the close button). */}
|
|
<ActiveCompanyBadge className="ml-auto mr-6" />
|
|
</div>
|
|
<DialogDescription className="sr-only">{t('description')}</DialogDescription>
|
|
<SettingsShell variant="modal" activeSection={resolved} />
|
|
</DialogContent>
|
|
</Dialog>
|
|
)
|
|
}
|