feat(web/default): unified UI overhaul — Base UI migration, theme presets, rankings dashboard, and table toolbar refactor (#4633)
* 🎨 feat(web/default): add shadcn-style theme presets, radius prefs, and fix selection badges Integrate the qn-platform–style OKLCH color system into the default frontend while keeping the existing blue-tinted dark tokens for the default theme. Add [data-theme-preset] palettes for seven named presets plus the default zinc-like scale, define [data-theme-radius] overrides so user radius beats preset --radius, and align the Tailwind @custom-variant dark helper with .dark usage. Introduce ThemeCustomizationProvider to own preset and radius state, persist choices in cookies (theme-preset, theme-radius), and sync data-theme-preset / data-theme-radius on <html>. Wrap the tree in main.tsx. Extend ConfigDrawer with theme preset swatches (scoped data-theme-preset) and radius previews wired to context; refactor swatch/card markup so selected CircleCheck badges sit outside clipped rows (remove outer overflow-hidden that hid the centered checkmark). Add i18n keys for preset names, radius, and accessibility labels across en, zh, fr, ja, ru, vi. * 🎨 fix(web): align segmented controls with theme radius tokens - Replace hard-coded inner pill radii (rounded-[5px]) on dashboard chart toolbars with radius-md so the active state follows --radius when users change Radius in Theme Settings. - Use nested radii consistent with TabsList/TabsTrigger: outer rounded-lg (var(--radius)) and inner rounded-md (calc(var(--radius) - 2px)) so the track and active thumb stay concentric at small scales (e.g. 0.3rem) instead of a squared “focus” block inside a rounded shell. - Apply the same pattern to pricing SegmentedControl and the segmented groups in consumption-distribution-chart, model-charts, and user-charts. Verified: bun run typecheck (web/default) * ✨ feat(pricing): enrich model details with uptime sparkline and API documentation Add a compact 30-day uptime sparkline (OpenRouter-style bars + aggregate %) with per-day tooltips, surface it in a status row under quick stats and in the per-group performance table, and extend mock data so uptime series are stable and optionally scoped by group. Introduce an API tab with Shiki-highlighted code samples (cURL, Python, TypeScript, JavaScript), endpoint-type switching, authentication guidance, a supported-parameters table, and mock per-group RPM/TPM/RPD limits. Infer vendor, tokenizer, license, and data-retention hints for a provider & data privacy card on the Overview tab (capabilities/modalities stay with model identity; rate limits stay with the API tab). Update i18n for all new user-facing strings across en, zh, fr, ja, ru, and vi. * 🏆 feat(rankings): add comprehensive rankings dashboard Add a mock-data powered rankings experience with period tabs, model, app, and vendor leaderboards, market share and history charts, movers, new releases, and per-category sections while backend analytics are pending. Link ranked models to pricing details and ranked vendors to filtered pricing results, and include localized copy for all supported frontend locales. * fix(theme): correct theme preset selection state - update Base UI Radio selectors to use data-checked/data-unchecked states. - fix unchecked theme options still showing selected indicators. - isolate the default theme preview tokens to prevent preset changes from leaking into it. * fix(setup): correct usage mode radio state - use Base UI data-checked/data-unchecked states for RadioGroup styling. - hide radio indicators when options are unchecked to avoid setup page display issues. - drive usage mode card and icon selection styles from Base UI state. * fix(auth): submit sign-in and sign-up forms * 🎨 refactor: Align default theme with shadcn Base Nova and prune legacy customization Migrate shadcn UI to Base UI primitives via CLI (`base-nova` / `components.json`) and reinstall full component registry with `--overwrite`, including Hugeicons-backed widgets and newly added registry components. - Remove custom multi-preset/theme-radius system (`ThemeCustomizationProvider`, cookies, preset UI from config drawer); rely on official semantic CSS tokens + light/dark only. - Replace `theme.css` with shadcn’s documented neutral `:root`/`.dark` palette and `@theme inline` mappings (plus skeleton token vars for existing shimmer usage). - Update global styles for Base UI: collapsible animation uses `--collapsible-panel-height`; clarify scroll-lock override comment. Application compatibility: - Keep minimal shims where app code diverged from official APIs (popover collision props, combobox legacy `options` callers, Spinner prop typing). - Switch interactive styling from Radix-era `data-state` / `--radix-*` selectors to Base UI semantics (`data-open`, `data-popup-open`, `data-panel-open`, `--anchor-width`, etc.) Tooling / docs / build: - Rename Rsbuild vendor chunk grouping to `@base-ui` + transitive `@radix-ui`. - Refresh AGENTS.md / CLAUDE.md / classic→default sync skill for Base UI stack. - Bump `package.json` / lockfile for shadcn-postinstall deps (Hugeicons, chart stack, themes, etc.) Verified: `bun run typecheck` passes. Note: `bun run lint` still reports pre-existing hooks rule violations elsewhere; not addressed in this change. * 🎨 chore(web/default): unify table toolbar, relocate usage stats, refine filters - Refactor DataTableToolbar to a single wrapping flex row with a right-aligned action cluster (Reset / Search / View / Expand) for a cleaner Ant Design Pro–style filter bar; remove the dedicated stats row and the toolbar `stats` prop. - Move Common Logs summary badges (Usage / RPM / TPM) and the sensitive- data visibility toggle into the page header via CommonLogsHeaderActions and SectionPageLayout.Actions so the toolbar stays focused on filters. - Slim CommonLogsFilterBar props (no stats / preActions eye control). - Improve CompactDateTimeRangePicker: show minute-precision labels on the trigger (seconds omitted; aligns with datetime-local inputs); widen the trigger on sm+ breakpoints so the full range is visible without truncation; apply the same width in task logs filters. - Simplify DataTableViewOptions: text-only “View” trigger, no sliders icon. - Earlier layout tweak: extra top padding on SectionPageLayout scroll content so control focus rings are not clipped by overflow. * feat(web/default): Base UI migration and component foundation Migrate from Radix UI to Base UI, rewrite core UI primitives, update dependencies (recharts, date-fns, next-themes), add shadcn agent skill documentation, and refresh AI element components. This is the foundational work from the v2/localmain lineage that was not covered by the individual feature commits above. --------- Co-authored-by: t0ng7u <dev@aiass.cc> Co-authored-by: QuentinHsu <xuquentinyang@gmail.com>
This commit is contained in:
co-authored by
t0ng7u
QuentinHsu
parent
dac55f0fde
commit
8b2b03d276
+3
-1
@@ -44,7 +44,9 @@ export async function updateUserSettings(
|
||||
/**
|
||||
* Update interface language preference
|
||||
*/
|
||||
export async function updateUserLanguage(language: string): Promise<ApiResponse> {
|
||||
export async function updateUserLanguage(
|
||||
language: string
|
||||
): Promise<ApiResponse> {
|
||||
const res = await api.put('/api/user/self', { language })
|
||||
return res.data
|
||||
}
|
||||
|
||||
@@ -226,7 +226,7 @@ export function CheckinCalendarCard({
|
||||
}
|
||||
|
||||
return (
|
||||
<TooltipProvider delayDuration={100}>
|
||||
<TooltipProvider delay={100}>
|
||||
<Dialog
|
||||
open={turnstileModalVisible}
|
||||
onOpenChange={(open) => {
|
||||
@@ -425,7 +425,7 @@ export function CheckinCalendarCard({
|
||||
if (isCheckedIn && dayObj.isCurrentMonth) {
|
||||
return (
|
||||
<Tooltip key={idx}>
|
||||
<TooltipTrigger asChild>{dayButton}</TooltipTrigger>
|
||||
<TooltipTrigger render={dayButton}></TooltipTrigger>
|
||||
<TooltipContent>
|
||||
<div className='text-xs'>
|
||||
<div className='font-medium'>
|
||||
|
||||
+31
-30
@@ -54,7 +54,8 @@ export function LanguagePreferencesCard(props: LanguagePreferencesCardProps) {
|
||||
setCurrentLanguage(savedLanguage)
|
||||
}, [savedLanguage])
|
||||
|
||||
const handleLanguageChange = async (language: string) => {
|
||||
const handleLanguageChange = async (language: string | null) => {
|
||||
if (!language) return
|
||||
const nextLanguage = normalizeLanguage(language)
|
||||
if (nextLanguage === currentLanguage) return
|
||||
|
||||
@@ -100,37 +101,37 @@ export function LanguagePreferencesCard(props: LanguagePreferencesCardProps) {
|
||||
description={t('Set the language used across the interface')}
|
||||
icon={<Languages className='h-4 w-4' />}
|
||||
>
|
||||
<div className='flex flex-col gap-3 sm:flex-row sm:items-center sm:justify-between sm:gap-4'>
|
||||
<div className='space-y-1'>
|
||||
<div className='text-sm font-medium'>{t('Interface Language')}</div>
|
||||
<p className='text-muted-foreground line-clamp-2 text-xs sm:text-sm'>
|
||||
{t(
|
||||
'Language preferences sync across your signed-in devices and affect API error messages.'
|
||||
)}
|
||||
</p>
|
||||
</div>
|
||||
<div className='flex items-center gap-2 sm:min-w-48'>
|
||||
<Select
|
||||
value={currentLanguage}
|
||||
onValueChange={handleLanguageChange}
|
||||
disabled={saving}
|
||||
>
|
||||
<SelectTrigger className='w-full sm:w-48'>
|
||||
<SelectValue placeholder={t('Select language')} />
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
{LANGUAGE_OPTIONS.map((language) => (
|
||||
<SelectItem key={language.value} value={language.value}>
|
||||
{language.label}
|
||||
</SelectItem>
|
||||
))}
|
||||
</SelectContent>
|
||||
</Select>
|
||||
{saving && (
|
||||
<Loader2 className='text-muted-foreground size-4 animate-spin' />
|
||||
<div className='flex flex-col gap-3 sm:flex-row sm:items-center sm:justify-between sm:gap-4'>
|
||||
<div className='space-y-1'>
|
||||
<div className='text-sm font-medium'>{t('Interface Language')}</div>
|
||||
<p className='text-muted-foreground line-clamp-2 text-xs sm:text-sm'>
|
||||
{t(
|
||||
'Language preferences sync across your signed-in devices and affect API error messages.'
|
||||
)}
|
||||
</div>
|
||||
</p>
|
||||
</div>
|
||||
<div className='flex items-center gap-2 sm:min-w-48'>
|
||||
<Select
|
||||
value={currentLanguage}
|
||||
onValueChange={handleLanguageChange}
|
||||
disabled={saving}
|
||||
>
|
||||
<SelectTrigger className='w-full sm:w-48'>
|
||||
<SelectValue placeholder={t('Select language')} />
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
{LANGUAGE_OPTIONS.map((language) => (
|
||||
<SelectItem key={language.value} value={language.value}>
|
||||
{language.label}
|
||||
</SelectItem>
|
||||
))}
|
||||
</SelectContent>
|
||||
</Select>
|
||||
{saving && (
|
||||
<Loader2 className='text-muted-foreground size-4 animate-spin' />
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</TitledCard>
|
||||
)
|
||||
}
|
||||
|
||||
+47
-45
@@ -204,43 +204,43 @@ export function PasskeyCard({ loading: pageLoading }: PasskeyCardProps) {
|
||||
<div className='space-y-6'>
|
||||
<div className='flex flex-col gap-4 sm:flex-row sm:items-start sm:justify-between xl:flex-col 2xl:flex-row'>
|
||||
<div className='flex items-start gap-4'>
|
||||
<div className='bg-muted rounded-md p-2'>
|
||||
<KeyRound className='h-5 w-5' />
|
||||
</div>
|
||||
<div className='space-y-1'>
|
||||
<div className='flex flex-wrap items-center gap-2'>
|
||||
<p className='font-medium'>{t('Passkey Authentication')}</p>
|
||||
<StatusBadge
|
||||
label={enabled ? t('Enabled') : t('Disabled')}
|
||||
variant={enabled ? 'success' : 'neutral'}
|
||||
showDot
|
||||
copyable={false}
|
||||
/>
|
||||
{status?.backup_eligible !== undefined && (
|
||||
<div className='bg-muted rounded-md p-2'>
|
||||
<KeyRound className='h-5 w-5' />
|
||||
</div>
|
||||
<div className='space-y-1'>
|
||||
<div className='flex flex-wrap items-center gap-2'>
|
||||
<p className='font-medium'>{t('Passkey Authentication')}</p>
|
||||
<StatusBadge
|
||||
label={
|
||||
status.backup_eligible
|
||||
? status.backup_state
|
||||
? t('Backed up')
|
||||
: t('Not backed up')
|
||||
: t('No backup')
|
||||
}
|
||||
variant={
|
||||
status.backup_eligible
|
||||
? status.backup_state
|
||||
? 'success'
|
||||
: 'warning'
|
||||
: 'neutral'
|
||||
}
|
||||
label={enabled ? t('Enabled') : t('Disabled')}
|
||||
variant={enabled ? 'success' : 'neutral'}
|
||||
showDot
|
||||
copyable={false}
|
||||
/>
|
||||
)}
|
||||
{status?.backup_eligible !== undefined && (
|
||||
<StatusBadge
|
||||
label={
|
||||
status.backup_eligible
|
||||
? status.backup_state
|
||||
? t('Backed up')
|
||||
: t('Not backed up')
|
||||
: t('No backup')
|
||||
}
|
||||
variant={
|
||||
status.backup_eligible
|
||||
? status.backup_state
|
||||
? 'success'
|
||||
: 'warning'
|
||||
: 'neutral'
|
||||
}
|
||||
showDot
|
||||
copyable={false}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
<p className='text-muted-foreground text-sm'>
|
||||
{t('Last used:')} {formattedLastUsed}
|
||||
</p>
|
||||
</div>
|
||||
<p className='text-muted-foreground text-sm'>
|
||||
{t('Last used:')} {formattedLastUsed}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{!enabled && (
|
||||
@@ -260,19 +260,21 @@ export function PasskeyCard({ loading: pageLoading }: PasskeyCardProps) {
|
||||
{enabled && (
|
||||
<div className='flex flex-col gap-3 border-t pt-6 sm:flex-row xl:flex-col 2xl:flex-row'>
|
||||
<AlertDialog open={confirmOpen} onOpenChange={setConfirmOpen}>
|
||||
<AlertDialogTrigger asChild>
|
||||
<Button
|
||||
variant='destructive'
|
||||
className='flex-1'
|
||||
disabled={removing}
|
||||
>
|
||||
{removing ? (
|
||||
<Loader2 className='mr-2 h-4 w-4 animate-spin' />
|
||||
) : (
|
||||
<AlertTriangle className='mr-2 h-4 w-4' />
|
||||
)}
|
||||
{t('Remove Passkey')}
|
||||
</Button>
|
||||
<AlertDialogTrigger
|
||||
render={
|
||||
<Button
|
||||
variant='destructive'
|
||||
className='flex-1'
|
||||
disabled={removing}
|
||||
/>
|
||||
}
|
||||
>
|
||||
{removing ? (
|
||||
<Loader2 className='mr-2 h-4 w-4 animate-spin' />
|
||||
) : (
|
||||
<AlertTriangle className='mr-2 h-4 w-4' />
|
||||
)}
|
||||
{t('Remove Passkey')}
|
||||
</AlertDialogTrigger>
|
||||
<AlertDialogContent>
|
||||
<AlertDialogHeader>
|
||||
|
||||
@@ -1,11 +1,7 @@
|
||||
import { Shield, Key, Trash2 } from 'lucide-react'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import { useDialogs } from '@/hooks/use-dialog'
|
||||
import {
|
||||
Card,
|
||||
CardContent,
|
||||
CardHeader,
|
||||
} from '@/components/ui/card'
|
||||
import { Card, CardContent, CardHeader } from '@/components/ui/card'
|
||||
import { Skeleton } from '@/components/ui/skeleton'
|
||||
import { TitledCard } from '@/components/ui/titled-card'
|
||||
import type { UserProfile } from '../types'
|
||||
@@ -80,36 +76,36 @@ export function ProfileSecurityCard({
|
||||
description={t('Manage your security settings and account access')}
|
||||
icon={<Shield className='h-4 w-4' />}
|
||||
>
|
||||
<div className='grid grid-cols-1 gap-2.5 sm:gap-3 md:grid-cols-3'>
|
||||
{securityActions.map((item) => (
|
||||
<button
|
||||
key={item.title}
|
||||
type='button'
|
||||
onClick={item.action}
|
||||
className={`hover:bg-muted/50 flex items-center gap-3 rounded-lg border p-3 text-left transition-colors md:flex-col md:gap-2 md:p-4 md:text-center ${
|
||||
<div className='grid grid-cols-1 gap-2.5 sm:gap-3 md:grid-cols-3'>
|
||||
{securityActions.map((item) => (
|
||||
<button
|
||||
key={item.title}
|
||||
type='button'
|
||||
onClick={item.action}
|
||||
className={`hover:bg-muted/50 flex items-center gap-3 rounded-lg border p-3 text-left transition-colors md:flex-col md:gap-2 md:p-4 md:text-center ${
|
||||
item.variant === 'destructive'
|
||||
? 'border-destructive/30 hover:border-destructive/50 hover:bg-destructive/5'
|
||||
: ''
|
||||
}`}
|
||||
>
|
||||
<div
|
||||
className={`rounded-md p-2 ${
|
||||
item.variant === 'destructive'
|
||||
? 'border-destructive/30 hover:border-destructive/50 hover:bg-destructive/5'
|
||||
: ''
|
||||
? 'bg-destructive/10 text-destructive'
|
||||
: 'bg-muted'
|
||||
}`}
|
||||
>
|
||||
<div
|
||||
className={`rounded-md p-2 ${
|
||||
item.variant === 'destructive'
|
||||
? 'bg-destructive/10 text-destructive'
|
||||
: 'bg-muted'
|
||||
}`}
|
||||
>
|
||||
<item.icon className='h-5 w-5' />
|
||||
</div>
|
||||
<div className='min-w-0 md:contents'>
|
||||
<p className='text-sm font-medium'>{item.title}</p>
|
||||
<p className='text-muted-foreground line-clamp-1 text-xs md:line-clamp-none'>
|
||||
{item.description}
|
||||
</p>
|
||||
</div>
|
||||
</button>
|
||||
))}
|
||||
</div>
|
||||
<item.icon className='h-5 w-5' />
|
||||
</div>
|
||||
<div className='min-w-0 md:contents'>
|
||||
<p className='text-sm font-medium'>{item.title}</p>
|
||||
<p className='text-muted-foreground line-clamp-1 text-xs md:line-clamp-none'>
|
||||
{item.description}
|
||||
</p>
|
||||
</div>
|
||||
</button>
|
||||
))}
|
||||
</div>
|
||||
</TitledCard>
|
||||
|
||||
{/* Dialogs */}
|
||||
|
||||
@@ -1,11 +1,7 @@
|
||||
import { useState } from 'react'
|
||||
import { Link2, Settings } from 'lucide-react'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import {
|
||||
Card,
|
||||
CardContent,
|
||||
CardHeader,
|
||||
} from '@/components/ui/card'
|
||||
import { Card, CardContent, CardHeader } from '@/components/ui/card'
|
||||
import { Skeleton } from '@/components/ui/skeleton'
|
||||
import { Tabs, TabsContent, TabsList, TabsTrigger } from '@/components/ui/tabs'
|
||||
import { TitledCard } from '@/components/ui/titled-card'
|
||||
@@ -54,36 +50,36 @@ export function ProfileSettingsCard({
|
||||
description={t('Configure your account preferences and integrations')}
|
||||
icon={<Settings className='h-4 w-4' />}
|
||||
>
|
||||
<Tabs value={activeTab} onValueChange={setActiveTab}>
|
||||
<TabsList className='grid h-auto w-full grid-cols-2 gap-1 rounded-xl p-1'>
|
||||
<TabsTrigger
|
||||
value='bindings'
|
||||
className='h-auto gap-2 rounded-lg px-3 py-2'
|
||||
>
|
||||
<Link2 className='h-4 w-4' />
|
||||
<span className='hidden sm:inline'>{t('Account Bindings')}</span>
|
||||
<span className='sm:hidden'>{t('Bindings')}</span>
|
||||
</TabsTrigger>
|
||||
<TabsTrigger
|
||||
value='settings'
|
||||
className='h-auto gap-2 rounded-lg px-3 py-2'
|
||||
>
|
||||
<Settings className='h-4 w-4' />
|
||||
<span className='hidden sm:inline'>
|
||||
{t('Settings & Preferences')}
|
||||
</span>
|
||||
<span className='sm:hidden'>{t('Settings')}</span>
|
||||
</TabsTrigger>
|
||||
</TabsList>
|
||||
<Tabs value={activeTab} onValueChange={setActiveTab}>
|
||||
<TabsList className='grid h-auto w-full grid-cols-2 gap-1 rounded-xl p-1'>
|
||||
<TabsTrigger
|
||||
value='bindings'
|
||||
className='h-auto gap-2 rounded-lg px-3 py-2'
|
||||
>
|
||||
<Link2 className='h-4 w-4' />
|
||||
<span className='hidden sm:inline'>{t('Account Bindings')}</span>
|
||||
<span className='sm:hidden'>{t('Bindings')}</span>
|
||||
</TabsTrigger>
|
||||
<TabsTrigger
|
||||
value='settings'
|
||||
className='h-auto gap-2 rounded-lg px-3 py-2'
|
||||
>
|
||||
<Settings className='h-4 w-4' />
|
||||
<span className='hidden sm:inline'>
|
||||
{t('Settings & Preferences')}
|
||||
</span>
|
||||
<span className='sm:hidden'>{t('Settings')}</span>
|
||||
</TabsTrigger>
|
||||
</TabsList>
|
||||
|
||||
<TabsContent value='bindings' className='mt-4 sm:mt-6'>
|
||||
<AccountBindingsTab profile={profile} onUpdate={onProfileUpdate} />
|
||||
</TabsContent>
|
||||
<TabsContent value='bindings' className='mt-4 sm:mt-6'>
|
||||
<AccountBindingsTab profile={profile} onUpdate={onProfileUpdate} />
|
||||
</TabsContent>
|
||||
|
||||
<TabsContent value='settings' className='mt-4 sm:mt-6'>
|
||||
<NotificationTab profile={profile} onUpdate={onProfileUpdate} />
|
||||
</TabsContent>
|
||||
</Tabs>
|
||||
<TabsContent value='settings' className='mt-4 sm:mt-6'>
|
||||
<NotificationTab profile={profile} onUpdate={onProfileUpdate} />
|
||||
</TabsContent>
|
||||
</Tabs>
|
||||
</TitledCard>
|
||||
)
|
||||
}
|
||||
|
||||
+38
-41
@@ -1,6 +1,6 @@
|
||||
import { useAuthStore } from '@/stores/auth-store'
|
||||
import { useStatus } from '@/hooks/use-status'
|
||||
import { AppHeader, Main } from '@/components/layout'
|
||||
import { Main } from '@/components/layout'
|
||||
import {
|
||||
CardStaggerContainer,
|
||||
CardStaggerItem,
|
||||
@@ -28,47 +28,44 @@ export function Profile() {
|
||||
const canConfigureSidebar = permissions?.sidebar_settings !== false
|
||||
|
||||
return (
|
||||
<>
|
||||
<AppHeader />
|
||||
<Main>
|
||||
<div className='min-h-0 flex-1 overflow-auto px-3 py-3 sm:px-4 sm:py-6'>
|
||||
<CardStaggerContainer className='mx-auto flex w-full max-w-7xl flex-col gap-4 sm:gap-6'>
|
||||
<CardStaggerItem>
|
||||
<ProfileHeader profile={profile} loading={loading} />
|
||||
</CardStaggerItem>
|
||||
<Main>
|
||||
<div className='min-h-0 flex-1 overflow-auto px-3 py-3 sm:px-4 sm:py-6'>
|
||||
<CardStaggerContainer className='mx-auto flex w-full max-w-7xl flex-col gap-4 sm:gap-6'>
|
||||
<CardStaggerItem>
|
||||
<ProfileHeader profile={profile} loading={loading} />
|
||||
</CardStaggerItem>
|
||||
|
||||
<CardStaggerItem>
|
||||
<div className='grid gap-4 sm:gap-5 xl:grid-cols-[minmax(0,1fr)_minmax(360px,0.46fr)] xl:items-start'>
|
||||
<div className='space-y-4 sm:space-y-6'>
|
||||
<ProfileSettingsCard
|
||||
profile={profile}
|
||||
loading={loading}
|
||||
onProfileUpdate={refreshProfile}
|
||||
/>
|
||||
<LanguagePreferencesCard
|
||||
profile={profile}
|
||||
onProfileUpdate={refreshProfile}
|
||||
/>
|
||||
<ProfileSecurityCard profile={profile} loading={loading} />
|
||||
</div>
|
||||
|
||||
<div className='space-y-4 sm:space-y-6 xl:sticky xl:top-6'>
|
||||
{checkinEnabled && (
|
||||
<CheckinCalendarCard
|
||||
checkinEnabled={checkinEnabled}
|
||||
turnstileEnabled={turnstileEnabled}
|
||||
turnstileSiteKey={turnstileSiteKey}
|
||||
/>
|
||||
)}
|
||||
{canConfigureSidebar && <SidebarModulesCard />}
|
||||
<PasskeyCard loading={loading} />
|
||||
<TwoFACard loading={loading} />
|
||||
</div>
|
||||
<CardStaggerItem>
|
||||
<div className='grid gap-4 sm:gap-5 xl:grid-cols-[minmax(0,1fr)_minmax(360px,0.46fr)] xl:items-start'>
|
||||
<div className='space-y-4 sm:space-y-6'>
|
||||
<ProfileSettingsCard
|
||||
profile={profile}
|
||||
loading={loading}
|
||||
onProfileUpdate={refreshProfile}
|
||||
/>
|
||||
<LanguagePreferencesCard
|
||||
profile={profile}
|
||||
onProfileUpdate={refreshProfile}
|
||||
/>
|
||||
<ProfileSecurityCard profile={profile} loading={loading} />
|
||||
</div>
|
||||
</CardStaggerItem>
|
||||
</CardStaggerContainer>
|
||||
</div>
|
||||
</Main>
|
||||
</>
|
||||
|
||||
<div className='space-y-4 sm:space-y-6 xl:sticky xl:top-6'>
|
||||
{checkinEnabled && (
|
||||
<CheckinCalendarCard
|
||||
checkinEnabled={checkinEnabled}
|
||||
turnstileEnabled={turnstileEnabled}
|
||||
turnstileSiteKey={turnstileSiteKey}
|
||||
/>
|
||||
)}
|
||||
{canConfigureSidebar && <SidebarModulesCard />}
|
||||
<PasskeyCard loading={loading} />
|
||||
<TwoFACard loading={loading} />
|
||||
</div>
|
||||
</div>
|
||||
</CardStaggerItem>
|
||||
</CardStaggerContainer>
|
||||
</div>
|
||||
</Main>
|
||||
)
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user