@djangocfg/layouts 2.1.591 → 2.1.592

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (71) hide show
  1. package/package.json +12 -12
  2. package/src/layouts/PrivateLayout/PrivateLayout.tsx +37 -26
  3. package/src/layouts/PrivateLayout/README.md +20 -12
  4. package/src/layouts/PrivateLayout/components/PrivateContent.tsx +29 -42
  5. package/src/layouts/PrivateLayout/components/PrivateSidebar.tsx +23 -92
  6. package/src/layouts/PrivateLayout/components/PrivateSidebarAccount.tsx +32 -62
  7. package/src/layouts/PrivateLayout/components/SidebarBody.tsx +45 -0
  8. package/src/layouts/PrivateLayout/components/SidebarBrand.tsx +64 -152
  9. package/src/layouts/PrivateLayout/components/SidebarBrandSwitcher.tsx +2 -30
  10. package/src/layouts/PrivateLayout/components/SidebarNavGroup.tsx +55 -115
  11. package/src/layouts/PrivateLayout/components/SidebarNavItem.tsx +37 -100
  12. package/src/layouts/PrivateLayout/components/SidebarPeek.tsx +34 -0
  13. package/src/layouts/PrivateLayout/components/SidebarResizeHandle.tsx +76 -0
  14. package/src/layouts/PrivateLayout/components/SidebarSlots.tsx +14 -53
  15. package/src/layouts/PrivateLayout/components/index.ts +0 -4
  16. package/src/layouts/PrivateLayout/context.tsx +10 -115
  17. package/src/layouts/PrivateLayout/geometry.ts +17 -0
  18. package/src/layouts/PrivateLayout/hooks/index.ts +2 -1
  19. package/src/layouts/PrivateLayout/hooks/useLayoutVisual.ts +4 -4
  20. package/src/layouts/PrivateLayout/hooks/useSidebarKeyboard.ts +33 -93
  21. package/src/layouts/PrivateLayout/hooks/useSidebarPeek.tsx +151 -0
  22. package/src/layouts/PrivateLayout/hooks/useSidebarWidth.ts +83 -0
  23. package/src/layouts/PrivateLayout/types.ts +13 -20
  24. package/src/layouts/SettingsLayout/README.md +23 -10
  25. package/src/layouts/SettingsLayout/SettingsDialog.tsx +14 -8
  26. package/src/layouts/SettingsLayout/SettingsForm.tsx +13 -9
  27. package/src/layouts/SettingsLayout/components/ApiKeySection/ApiKeySection.tsx +9 -7
  28. package/src/layouts/SettingsLayout/components/ApiKeySection/context.tsx +6 -41
  29. package/src/layouts/SettingsLayout/components/AvatarSection.tsx +11 -3
  30. package/src/layouts/SettingsLayout/components/SettingsNav.tsx +5 -9
  31. package/src/layouts/SettingsLayout/components/SettingsNavItem.tsx +5 -7
  32. package/src/layouts/SettingsLayout/components/SettingsPanel.tsx +5 -10
  33. package/src/layouts/SettingsLayout/components/SettingsSearch.tsx +5 -3
  34. package/src/layouts/SettingsLayout/components/SettingsShell.tsx +14 -21
  35. package/src/layouts/SettingsLayout/components/TwoFactorSection/TwoFactorSection.tsx +52 -105
  36. package/src/layouts/SettingsLayout/components/index.ts +0 -1
  37. package/src/layouts/SettingsLayout/context/SettingsContext.tsx +5 -2
  38. package/src/layouts/SettingsLayout/i18n/index.ts +39 -0
  39. package/src/layouts/SettingsLayout/i18n/locales/ar.ts +66 -0
  40. package/src/layouts/SettingsLayout/i18n/locales/da.ts +66 -0
  41. package/src/layouts/SettingsLayout/i18n/locales/de.ts +66 -0
  42. package/src/layouts/SettingsLayout/i18n/locales/en.ts +66 -0
  43. package/src/layouts/SettingsLayout/i18n/locales/es.ts +66 -0
  44. package/src/layouts/SettingsLayout/i18n/locales/fr.ts +66 -0
  45. package/src/layouts/SettingsLayout/i18n/locales/it.ts +66 -0
  46. package/src/layouts/SettingsLayout/i18n/locales/ja.ts +66 -0
  47. package/src/layouts/SettingsLayout/i18n/locales/ko.ts +66 -0
  48. package/src/layouts/SettingsLayout/i18n/locales/nl.ts +66 -0
  49. package/src/layouts/SettingsLayout/i18n/locales/no.ts +66 -0
  50. package/src/layouts/SettingsLayout/i18n/locales/pl.ts +66 -0
  51. package/src/layouts/SettingsLayout/i18n/locales/ptBR.ts +66 -0
  52. package/src/layouts/SettingsLayout/i18n/locales/ru.ts +66 -0
  53. package/src/layouts/SettingsLayout/i18n/locales/sv.ts +66 -0
  54. package/src/layouts/SettingsLayout/i18n/locales/tr.ts +66 -0
  55. package/src/layouts/SettingsLayout/i18n/locales/zh.ts +66 -0
  56. package/src/layouts/SettingsLayout/i18n/types.ts +65 -0
  57. package/src/layouts/SettingsLayout/index.ts +0 -1
  58. package/src/layouts/SettingsLayout/sections/AccountSection.tsx +9 -4
  59. package/src/layouts/SettingsLayout/sections/DeleteAccountRow.tsx +4 -1
  60. package/src/layouts/SettingsLayout/sections/GeneralSection.tsx +25 -0
  61. package/src/layouts/SettingsLayout/sections/PreferencesRows.tsx +12 -4
  62. package/src/layouts/SettingsLayout/sections/builtins.tsx +34 -29
  63. package/src/layouts/SettingsLayout/sections/index.ts +2 -2
  64. package/src/layouts/SettingsLayout/types.ts +12 -4
  65. package/src/layouts/types/layout.types.ts +5 -6
  66. package/src/styles/dashboard.css +10 -0
  67. package/src/layouts/PrivateLayout/density.ts +0 -48
  68. package/src/layouts/PrivateLayout/hooks/useHoverExpand.ts +0 -110
  69. package/src/layouts/PrivateLayout/hooks/useShellVisualState.ts +0 -130
  70. package/src/layouts/SettingsLayout/components/SettingsRailFooter.tsx +0 -74
  71. package/src/layouts/SettingsLayout/sections/SecuritySection.tsx +0 -15
@@ -13,6 +13,7 @@ import {
13
13
  } from '@djangocfg/ui-core/components';
14
14
  import { cn } from '@djangocfg/ui-core/lib';
15
15
 
16
+ import { useSettingsLocale } from '../../i18n';
16
17
  import { ApiKeyProvider, useApiKeyContext } from './context';
17
18
 
18
19
  // ─────────────────────────────────────────────────────────────────────────────
@@ -26,12 +27,12 @@ import { ApiKeyProvider, useApiKeyContext } from './context';
26
27
  //
27
28
  // A bare `new Date(iso)` would read a timestamp without a zone designator as
28
29
  // LOCAL time; the API sends UTC, so the `Z` is appended when it is missing.
29
- function formatDate(iso: string | null, fallback: string): string {
30
+ function formatDate(iso: string | null, fallback: string, locale: string): string {
30
31
  if (!iso) return fallback;
31
32
  const utc = /(?:Z|[+-]\d{2}:?\d{2})$/.test(iso) ? iso : `${iso}Z`;
32
33
  const date = new Date(utc);
33
34
  if (Number.isNaN(date.getTime())) return fallback;
34
- return new Intl.DateTimeFormat('en-US', {
35
+ return new Intl.DateTimeFormat(locale, {
35
36
  year: 'numeric',
36
37
  month: 'short',
37
38
  day: 'numeric',
@@ -48,6 +49,7 @@ function isMasked(key: string): boolean {
48
49
  // ─────────────────────────────────────────────────────────────────────────────
49
50
 
50
51
  function ApiKeyCard() {
52
+ const locale = useSettingsLocale();
51
53
  const {
52
54
  labels,
53
55
  apiKey,
@@ -79,7 +81,7 @@ function ApiKeyCard() {
79
81
  // SettingRow renders `description` inside a block, so this is valid.
80
82
  const titleDescription = (
81
83
  <div className="flex flex-wrap items-center gap-2">
82
- {apiKey && <Badge variant="secondary" className="text-xs">Active</Badge>}
84
+ {apiKey && <Badge variant="secondary" className="text-xs">{labels.active}</Badge>}
83
85
  <span>{labels.description}</span>
84
86
  </div>
85
87
  );
@@ -127,7 +129,7 @@ function ApiKeyCard() {
127
129
  {isFresh && (
128
130
  <div className="flex items-center gap-2 rounded-control bg-accent px-3 py-2 text-sm text-foreground">
129
131
  <KeyRound className="w-4 h-4 flex-shrink-0 text-primary" />
130
- <span className="flex-1">This is your new API key — copy it now. It will be masked when you leave this page.</span>
132
+ <span className="flex-1">{labels.freshKey}</span>
131
133
  <Button variant="ghost" size="sm" className="h-7 px-2" onClick={dismissFresh}>
132
134
  <Check className="w-4 h-4 mr-1" />
133
135
  {labels.done}
@@ -150,7 +152,7 @@ function ApiKeyCard() {
150
152
 
151
153
  {/* API key value row */}
152
154
  {apiKey && (
153
- <SettingRow label="API key" stacked>
155
+ <SettingRow label={labels.keyRow} stacked>
154
156
  <div className="flex items-center gap-2">
155
157
  <div className="flex-1 min-w-0 rounded-control bg-muted px-3 py-2.5 font-mono text-sm select-all">
156
158
  <span className={cn(masked && 'tracking-widest')}>
@@ -181,10 +183,10 @@ function ApiKeyCard() {
181
183
 
182
184
  {/* Dates */}
183
185
  {apiKey && createdAt && (
184
- <SettingRow label={labels.created} value={formatDate(createdAt, '—')} />
186
+ <SettingRow label={labels.created} value={formatDate(createdAt, '—', locale)} />
185
187
  )}
186
188
  {apiKey && (
187
- <SettingRow label={labels.reissued} value={formatDate(reissuedAt, labels.neverReissued)} />
189
+ <SettingRow label={labels.reissued} value={formatDate(reissuedAt, labels.neverReissued, locale)} />
188
190
  )}
189
191
 
190
192
  {error && (
@@ -5,45 +5,9 @@ import React, { createContext, useCallback, useContext, useMemo, useState } from
5
5
  import { useApiKey } from '@djangocfg/api/hooks';
6
6
  import { toast } from '@djangocfg/ui-core/hooks';
7
7
 
8
- // ─────────────────────────────────────────────────────────────────────────────
9
- // Labels (hardcoded English — same pattern as TwoFactorSection)
10
- // ─────────────────────────────────────────────────────────────────────────────
8
+ import { useSettingsCopy, type SettingsCopy } from '../../i18n';
11
9
 
12
- export interface ApiKeyLabels {
13
- title: string;
14
- description: string;
15
- created: string;
16
- reissued: string;
17
- neverReissued: string;
18
- regenerate: string;
19
- confirmRegenerate: string;
20
- regenerating: string;
21
- regenerated: string;
22
- failedToRegenerate: string;
23
- done: string;
24
- test: string;
25
- testing: string;
26
- testSuccess: string;
27
- testFailed: string;
28
- }
29
-
30
- const LABELS: ApiKeyLabels = {
31
- title: 'API Key',
32
- description: 'Your personal API key for programmatic access',
33
- created: 'Created',
34
- reissued: 'Last reissued',
35
- neverReissued: 'Never reissued',
36
- regenerate: 'Regenerate API Key',
37
- confirmRegenerate: 'Click to confirm',
38
- regenerating: 'Regenerating…',
39
- regenerated: 'API key regenerated',
40
- failedToRegenerate: 'Failed to regenerate API key',
41
- done: 'Done',
42
- test: 'Test API Key',
43
- testing: 'Testing…',
44
- testSuccess: 'API key is valid',
45
- testFailed: 'API key test failed',
46
- };
10
+ export type ApiKeyLabels = SettingsCopy['apiKey'];
47
11
 
48
12
  // ─────────────────────────────────────────────────────────────────────────────
49
13
  // Context
@@ -83,6 +47,7 @@ export const useApiKeyContext = (): ApiKeyContextValue => {
83
47
  export const ApiKeyProvider: React.FC<{ children: React.ReactNode }> = ({ children }) => {
84
48
  const { apiKey, reissuedAt, createdAt, isLoading, isRegenerating, isTesting, regenerate, testKey, refresh } = useApiKey();
85
49
 
50
+ const LABELS = useSettingsCopy().apiKey;
86
51
  const [isArmed, setIsArmed] = useState(false);
87
52
  const [error, setError] = useState<string | null>(null);
88
53
  const [testResult, setTestResult] = useState<boolean | null>(null);
@@ -109,7 +74,7 @@ export const ApiKeyProvider: React.FC<{ children: React.ReactNode }> = ({ childr
109
74
  setError(LABELS.failedToRegenerate);
110
75
  toast.error(LABELS.failedToRegenerate);
111
76
  }
112
- }, [regenerate]);
77
+ }, [regenerate, LABELS]);
113
78
 
114
79
  const handleTest = useCallback(async () => {
115
80
  if (!apiKey) return;
@@ -126,7 +91,7 @@ export const ApiKeyProvider: React.FC<{ children: React.ReactNode }> = ({ childr
126
91
  setTestResult(false);
127
92
  toast.error(LABELS.testFailed);
128
93
  }
129
- }, [apiKey, testKey]);
94
+ }, [apiKey, testKey, LABELS]);
130
95
 
131
96
  const value = useMemo<ApiKeyContextValue>(() => ({
132
97
  labels: LABELS,
@@ -146,7 +111,7 @@ export const ApiKeyProvider: React.FC<{ children: React.ReactNode }> = ({ childr
146
111
  isTesting,
147
112
  testResult,
148
113
  }), [
149
- apiKey, reissuedAt, createdAt, isLoading, error,
114
+ LABELS, apiKey, reissuedAt, createdAt, isLoading, error,
150
115
  isArmed, arm, disarm, handleRegenerate, isRegenerating, isFresh, dismissFresh,
151
116
  handleTest, isTesting, testResult,
152
117
  ]);
@@ -7,6 +7,7 @@ import { toast } from '@djangocfg/ui-core/hooks';
7
7
  import { useAuth } from '@djangocfg/api/auth';
8
8
  import { Avatar, AvatarFallback, Button } from '@djangocfg/ui-core/components';
9
9
 
10
+ import { useSettingsCopy } from '../i18n';
10
11
  import { profileLogger } from '../../../utils/logger';
11
12
 
12
13
  const MAX_AVATAR_BYTES = 5 * 1024 * 1024;
@@ -16,7 +17,7 @@ const MAX_AVATAR_BYTES = 5 * 1024 * 1024;
16
17
  * settings rows, not inside one. Avatar isn't a form field like "First name";
17
18
  * it's the user's identity, so it gets the canonical Apple-ID / Slack / Linear
18
19
  * treatment: large centered avatar → name/email → a single "Change avatar"
19
- * action.
20
+ * action, on a soft primary-tinted hero card.
20
21
  *
21
22
  * UX: the avatar is a preview only; "Change avatar" opens the picker and
22
23
  * uploads immediately (no Save/Cancel step). The preview is optimistic — shown
@@ -24,6 +25,7 @@ const MAX_AVATAR_BYTES = 5 * 1024 * 1024;
24
25
  * The whole avatar is also clickable for a faster path.
25
26
  */
26
27
  export const AvatarSection = () => {
28
+ const copy = useSettingsCopy();
27
29
  const { user, uploadAvatar } = useAuth();
28
30
  const inputRef = useRef<HTMLInputElement>(null);
29
31
  const [isUploading, setIsUploading] = useState(false);
@@ -70,13 +72,19 @@ export const AvatarSection = () => {
70
72
  const initials = user?.initials || 'UN';
71
73
 
72
74
  return (
73
- <div className="flex flex-col items-center gap-3 py-2">
75
+ <div
76
+ className={
77
+ 'flex flex-col items-center gap-3 rounded-2xl border border-foreground/5 px-6 pb-6 pt-8 ' +
78
+ // Soft hero wash tinted by the theme primary — works in light and dark.
79
+ 'bg-[radial-gradient(ellipse_at_50%_0%,color-mix(in_oklab,var(--primary)_22%,transparent),transparent_70%),linear-gradient(to_bottom,color-mix(in_oklab,var(--primary)_6%,transparent),transparent)]'
80
+ }
81
+ >
74
82
  <button
75
83
  type="button"
76
84
  onClick={handlePick}
77
85
  disabled={isUploading}
78
86
  className="group relative rounded-full focus:outline-none focus-visible:ring-2 focus-visible:ring-primary focus-visible:ring-offset-2 focus-visible:ring-offset-background"
79
- aria-label="Change avatar"
87
+ aria-label={copy.account.changeAvatar}
80
88
  >
81
89
  <Avatar className="h-16 w-16 overflow-hidden rounded-full ring-1 ring-foreground/10 transition-transform group-hover:scale-105 md:h-20 md:w-20">
82
90
  {displayUrl ? (
@@ -10,11 +10,11 @@ import { SettingsSearch } from './SettingsSearch';
10
10
 
11
11
  /**
12
12
  * Left rail: optional title, optional search, then grouped section items.
13
- * Groups with a label render a small uppercase header; the default (unlabelled)
13
+ * Groups with a label render a small muted header; the default (unlabelled)
14
14
  * group renders bare. Empty search results show a quiet "no matches" line.
15
15
  */
16
16
  export const SettingsNav: React.FC = () => {
17
- const { title, groups, searchable, activeId, setActive, query } = useSettingsContext();
17
+ const { groups, searchable, activeId, setActive, query } = useSettingsContext();
18
18
 
19
19
  const hasResults = groups.some((g) => g.sections.length > 0);
20
20
 
@@ -22,12 +22,8 @@ export const SettingsNav: React.FC = () => {
22
22
  <nav
23
23
  role="tablist"
24
24
  aria-orientation="vertical"
25
- className="flex h-full flex-col gap-3"
25
+ className="flex h-full flex-col gap-2"
26
26
  >
27
- <div className="px-1">
28
- <h2 className="px-2.5 text-sm font-semibold text-foreground">{title}</h2>
29
- </div>
30
-
31
27
  {searchable && (
32
28
  <div className="px-1">
33
29
  <SettingsSearch />
@@ -47,8 +43,8 @@ export const SettingsNav: React.FC = () => {
47
43
  {group.label && (
48
44
  <div
49
45
  className={cn(
50
- 'px-2.5 pb-1 text-[11px] font-medium uppercase tracking-wider',
51
- 'text-muted-foreground/70',
46
+ 'px-2.5 pb-1 pt-1 text-xs',
47
+ 'text-muted-foreground',
52
48
  )}
53
49
  >
54
50
  {group.label}
@@ -31,22 +31,20 @@ export const SettingsNavItem: React.FC<SettingsNavItemProps> = ({
31
31
  aria-selected={active}
32
32
  onClick={() => onSelect(section.id)}
33
33
  className={cn(
34
- 'group flex w-full items-center gap-2.5 rounded-control px-2.5 py-2 text-left text-sm',
34
+ 'group flex h-8 w-full items-center gap-2.5 rounded-lg px-2.5 text-left text-base',
35
35
  'outline-none transition-colors',
36
36
  'focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-1 focus-visible:ring-offset-background',
37
37
  active
38
- // Solid raised surface (popover-level) — pops off the muted rail in
39
- // BOTH themes (on light the rail-vs-accent gap is only ~2% and
40
- // invisible; an elevated surface fixes that). No shadow — flat fill.
41
- ? 'bg-popover text-foreground font-medium'
42
- : 'text-muted-foreground hover:bg-accent/60 hover:text-foreground',
38
+ // Neutral foreground overlay, like the app sidebar — theme-agnostic.
39
+ ? 'bg-foreground/10 text-foreground'
40
+ : 'text-foreground/90 hover:bg-foreground/[0.07]',
43
41
  )}
44
42
  >
45
43
  {Icon && (
46
44
  <Icon
47
45
  className={cn(
48
46
  'size-4 shrink-0 transition-colors',
49
- active ? 'text-foreground' : 'text-muted-foreground group-hover:text-foreground',
47
+ 'text-foreground/80 group-hover:text-foreground',
50
48
  )}
51
49
  />
52
50
  )}
@@ -7,9 +7,9 @@ import { ScrollArea } from '@djangocfg/ui-core/components';
7
7
  import { useSettingsContext } from '../context';
8
8
 
9
9
  /**
10
- * Right side: the active section's detail. Renders a header (title + optional
11
- * description) pinned above an independently scrolling body. The header falls
12
- * back to the section's nav label when no explicit `title` is given.
10
+ * Right side: the active section's detail in an independently scrolling body.
11
+ * The heading is screen-reader only (the nav already names the section); the
12
+ * visible structure comes from each section's own blocks, like Claude.
13
13
  *
14
14
  * Only the active section's `content` is in the tree, so heavy panels don't
15
15
  * render until selected (and unmount when the dialog closes — Radix default).
@@ -29,17 +29,12 @@ export const SettingsPanel: React.FC = () => {
29
29
 
30
30
  return (
31
31
  <div className="flex h-full min-h-0 flex-col">
32
- <header className="shrink-0 border-b border-border px-6 py-4 md:px-8 md:py-5">
33
- <h1 className="text-lg font-semibold tracking-tight text-foreground">{heading}</h1>
34
- {active.description && (
35
- <p className="mt-1 text-sm text-muted-foreground">{active.description}</p>
36
- )}
37
- </header>
32
+ <h1 className="sr-only">{heading}</h1>
38
33
 
39
34
  <ScrollArea className="min-h-0 flex-1">
40
35
  {/* key forces a fresh subtree per section: resets scroll + transient
41
36
  form state when switching, which is the expected settings UX. */}
42
- <div key={active.id} className="px-6 py-6 md:px-8">
37
+ <div key={active.id} className="px-6 py-5 md:px-8 md:py-6">
43
38
  {active.content}
44
39
  </div>
45
40
  </ScrollArea>
@@ -6,6 +6,7 @@ import { useAppT } from '@djangocfg/i18n';
6
6
  import { cn } from '@djangocfg/ui-core/lib';
7
7
  import { Search, X } from 'lucide-react';
8
8
 
9
+ import { useSettingsCopy } from '../i18n';
9
10
  import { useSettingsContext } from '../context';
10
11
 
11
12
  /**
@@ -16,6 +17,7 @@ import { useSettingsContext } from '../context';
16
17
  export const SettingsSearch: React.FC = () => {
17
18
  const { query, setQuery } = useSettingsContext();
18
19
  const t = useAppT();
20
+ const copy = useSettingsCopy();
19
21
  const placeholder = t('ui.select.search') || 'Search';
20
22
 
21
23
  return (
@@ -28,18 +30,18 @@ export const SettingsSearch: React.FC = () => {
28
30
  placeholder={placeholder}
29
31
  aria-label={placeholder}
30
32
  className={cn(
31
- 'rounded-control h-9 w-full border border-border bg-muted/40 pl-8 pr-8 text-sm',
33
+ 'h-8 w-full rounded-lg border border-transparent bg-foreground/[0.05] pl-8 pr-8 text-base',
32
34
  'text-foreground placeholder:text-muted-foreground',
33
35
  'outline-none transition-[border-color,background-color,box-shadow]',
34
36
  // Crisp thin accent outline (matches ui-core Input), no blurry halo.
35
- 'focus-visible:border-ring focus-visible:bg-background focus-visible:ring-1 focus-visible:ring-ring',
37
+ 'focus-visible:border-ring focus-visible:ring-1 focus-visible:ring-ring',
36
38
  )}
37
39
  />
38
40
  {query && (
39
41
  <button
40
42
  type="button"
41
43
  onClick={() => setQuery('')}
42
- aria-label="Clear search"
44
+ aria-label={copy.search.clear}
43
45
  className="absolute right-2 top-1/2 -translate-y-1/2 rounded-sm p-0.5 text-muted-foreground transition-colors hover:text-foreground"
44
46
  >
45
47
  <X className="size-3.5" />
@@ -8,7 +8,6 @@ import { cn } from '@djangocfg/ui-core/lib';
8
8
  import { useSettingsContext } from '../context';
9
9
  import { SettingsNav } from './SettingsNav';
10
10
  import { SettingsPanel } from './SettingsPanel';
11
- import { SettingsRailFooter } from './SettingsRailFooter';
12
11
  import { SettingsTabs } from './SettingsTabs';
13
12
 
14
13
  interface SettingsShellProps {
@@ -44,18 +43,15 @@ export const SettingsShell: React.FC<SettingsShellProps> = ({ bare = false, clas
44
43
  if (isMobile) {
45
44
  return (
46
45
  <div className={frame}>
47
- <div className="shrink-0 bg-muted">
46
+ <div className="shrink-0 bg-background">
48
47
  {/* Title row — `pr-12` leaves room for the dialog's corner close button. */}
49
48
  <div className="flex items-center gap-3 px-4 pb-2 pt-3.5 pr-12">
50
49
  <h2 className="flex-1 truncate text-base font-semibold tracking-tight text-foreground">
51
50
  {title}
52
51
  </h2>
53
- {/* Global prefs (theme + language) mirror the desktop rail footer —
54
- on phones they ride the header instead of a bottom rail. */}
55
- <SettingsRailFooter horizontal className="border-t-0" />
56
52
  </div>
57
53
  {/* Tabs on their own row (below the header), so nothing overlaps the X. */}
58
- <div className="border-b border-border px-1.5 pb-2">
54
+ <div className="border-b border-foreground/10 px-1.5 pb-2">
59
55
  <SettingsTabs />
60
56
  </div>
61
57
  </div>
@@ -66,23 +62,20 @@ export const SettingsShell: React.FC<SettingsShellProps> = ({ bare = false, clas
66
62
  );
67
63
  }
68
64
 
69
- // ── Desktop: rail + panel ──
65
+ // ── Desktop: header bar, then rail + panel on one surface ──
70
66
  return (
71
- <div className={cn(frame, 'flex-row')}>
72
- {/* Nav rail — recessed surface, one notch below the content panel.
73
- Column layout so the section list scrolls in the middle and the
74
- global-preferences footer (theme + language) pins to the bottom. */}
75
- <aside className="flex shrink-0 flex-col border-r border-border bg-muted p-3 md:w-52">
76
- <div className="min-h-0 flex-1">
67
+ <div className={frame}>
68
+ <header className="flex h-11 shrink-0 items-center border-b border-foreground/[0.12] px-5 pr-14">
69
+ <h2 className="truncate text-lg font-medium text-foreground">{title}</h2>
70
+ </header>
71
+ <div className="flex min-h-0 flex-1">
72
+ <aside className="flex w-[13.25rem] shrink-0 flex-col border-e border-foreground/10 bg-background p-3">
77
73
  <SettingsNav />
78
- </div>
79
- <SettingsRailFooter className="mt-3" />
80
- </aside>
81
-
82
- {/* Detail panel — the lighter, primary surface. */}
83
- <section className="min-h-0 flex-1 bg-card">
84
- <SettingsPanel />
85
- </section>
74
+ </aside>
75
+ <section className="min-h-0 flex-1">
76
+ <SettingsPanel />
77
+ </section>
78
+ </div>
86
79
  </div>
87
80
  );
88
81
  };