@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.
- package/package.json +12 -12
- package/src/layouts/PrivateLayout/PrivateLayout.tsx +37 -26
- package/src/layouts/PrivateLayout/README.md +20 -12
- package/src/layouts/PrivateLayout/components/PrivateContent.tsx +29 -42
- package/src/layouts/PrivateLayout/components/PrivateSidebar.tsx +23 -92
- package/src/layouts/PrivateLayout/components/PrivateSidebarAccount.tsx +32 -62
- package/src/layouts/PrivateLayout/components/SidebarBody.tsx +45 -0
- package/src/layouts/PrivateLayout/components/SidebarBrand.tsx +64 -152
- package/src/layouts/PrivateLayout/components/SidebarBrandSwitcher.tsx +2 -30
- package/src/layouts/PrivateLayout/components/SidebarNavGroup.tsx +55 -115
- package/src/layouts/PrivateLayout/components/SidebarNavItem.tsx +37 -100
- package/src/layouts/PrivateLayout/components/SidebarPeek.tsx +34 -0
- package/src/layouts/PrivateLayout/components/SidebarResizeHandle.tsx +76 -0
- package/src/layouts/PrivateLayout/components/SidebarSlots.tsx +14 -53
- package/src/layouts/PrivateLayout/components/index.ts +0 -4
- package/src/layouts/PrivateLayout/context.tsx +10 -115
- package/src/layouts/PrivateLayout/geometry.ts +17 -0
- package/src/layouts/PrivateLayout/hooks/index.ts +2 -1
- package/src/layouts/PrivateLayout/hooks/useLayoutVisual.ts +4 -4
- package/src/layouts/PrivateLayout/hooks/useSidebarKeyboard.ts +33 -93
- package/src/layouts/PrivateLayout/hooks/useSidebarPeek.tsx +151 -0
- package/src/layouts/PrivateLayout/hooks/useSidebarWidth.ts +83 -0
- package/src/layouts/PrivateLayout/types.ts +13 -20
- package/src/layouts/SettingsLayout/README.md +23 -10
- package/src/layouts/SettingsLayout/SettingsDialog.tsx +14 -8
- package/src/layouts/SettingsLayout/SettingsForm.tsx +13 -9
- package/src/layouts/SettingsLayout/components/ApiKeySection/ApiKeySection.tsx +9 -7
- package/src/layouts/SettingsLayout/components/ApiKeySection/context.tsx +6 -41
- package/src/layouts/SettingsLayout/components/AvatarSection.tsx +11 -3
- package/src/layouts/SettingsLayout/components/SettingsNav.tsx +5 -9
- package/src/layouts/SettingsLayout/components/SettingsNavItem.tsx +5 -7
- package/src/layouts/SettingsLayout/components/SettingsPanel.tsx +5 -10
- package/src/layouts/SettingsLayout/components/SettingsSearch.tsx +5 -3
- package/src/layouts/SettingsLayout/components/SettingsShell.tsx +14 -21
- package/src/layouts/SettingsLayout/components/TwoFactorSection/TwoFactorSection.tsx +52 -105
- package/src/layouts/SettingsLayout/components/index.ts +0 -1
- package/src/layouts/SettingsLayout/context/SettingsContext.tsx +5 -2
- package/src/layouts/SettingsLayout/i18n/index.ts +39 -0
- package/src/layouts/SettingsLayout/i18n/locales/ar.ts +66 -0
- package/src/layouts/SettingsLayout/i18n/locales/da.ts +66 -0
- package/src/layouts/SettingsLayout/i18n/locales/de.ts +66 -0
- package/src/layouts/SettingsLayout/i18n/locales/en.ts +66 -0
- package/src/layouts/SettingsLayout/i18n/locales/es.ts +66 -0
- package/src/layouts/SettingsLayout/i18n/locales/fr.ts +66 -0
- package/src/layouts/SettingsLayout/i18n/locales/it.ts +66 -0
- package/src/layouts/SettingsLayout/i18n/locales/ja.ts +66 -0
- package/src/layouts/SettingsLayout/i18n/locales/ko.ts +66 -0
- package/src/layouts/SettingsLayout/i18n/locales/nl.ts +66 -0
- package/src/layouts/SettingsLayout/i18n/locales/no.ts +66 -0
- package/src/layouts/SettingsLayout/i18n/locales/pl.ts +66 -0
- package/src/layouts/SettingsLayout/i18n/locales/ptBR.ts +66 -0
- package/src/layouts/SettingsLayout/i18n/locales/ru.ts +66 -0
- package/src/layouts/SettingsLayout/i18n/locales/sv.ts +66 -0
- package/src/layouts/SettingsLayout/i18n/locales/tr.ts +66 -0
- package/src/layouts/SettingsLayout/i18n/locales/zh.ts +66 -0
- package/src/layouts/SettingsLayout/i18n/types.ts +65 -0
- package/src/layouts/SettingsLayout/index.ts +0 -1
- package/src/layouts/SettingsLayout/sections/AccountSection.tsx +9 -4
- package/src/layouts/SettingsLayout/sections/DeleteAccountRow.tsx +4 -1
- package/src/layouts/SettingsLayout/sections/GeneralSection.tsx +25 -0
- package/src/layouts/SettingsLayout/sections/PreferencesRows.tsx +12 -4
- package/src/layouts/SettingsLayout/sections/builtins.tsx +34 -29
- package/src/layouts/SettingsLayout/sections/index.ts +2 -2
- package/src/layouts/SettingsLayout/types.ts +12 -4
- package/src/layouts/types/layout.types.ts +5 -6
- package/src/styles/dashboard.css +10 -0
- package/src/layouts/PrivateLayout/density.ts +0 -48
- package/src/layouts/PrivateLayout/hooks/useHoverExpand.ts +0 -110
- package/src/layouts/PrivateLayout/hooks/useShellVisualState.ts +0 -130
- package/src/layouts/SettingsLayout/components/SettingsRailFooter.tsx +0 -74
- 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(
|
|
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">
|
|
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">
|
|
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=
|
|
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
|
|
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
|
|
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=
|
|
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
|
|
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 {
|
|
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-
|
|
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
|
|
51
|
-
'text-muted-foreground
|
|
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-
|
|
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
|
-
//
|
|
39
|
-
|
|
40
|
-
|
|
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
|
-
|
|
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
|
|
11
|
-
*
|
|
12
|
-
*
|
|
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
|
-
<
|
|
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-
|
|
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
|
-
'
|
|
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:
|
|
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=
|
|
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-
|
|
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-
|
|
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={
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
<
|
|
76
|
-
<
|
|
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
|
-
</
|
|
79
|
-
<
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
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
|
};
|