@djangocfg/layouts 2.1.591 → 2.1.593
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/components/errors/ErrorLayout.tsx +1 -164
- package/src/components/errors/ErrorsTracker/providers/ErrorTrackingProvider.tsx +1 -100
- package/src/components/errors/ErrorsTracker/providers/errorDedupe.ts +101 -0
- package/src/components/errors/errorGlyphs.tsx +167 -0
- package/src/layouts/AppLayout/BaseApp.tsx +1 -1
- package/src/layouts/AppLayout/index.ts +2 -2
- package/src/layouts/AuthLayout/AuthLayout.tsx +3 -23
- package/src/layouts/AuthLayout/components/shared/AuthTopBar.tsx +1 -1
- package/src/layouts/AuthLayout/components/steps/IdentifierStep.tsx +1 -1
- package/src/layouts/AuthLayout/layoutContext.ts +25 -0
- package/src/layouts/AuthLayout/styles/split-shell.css +0 -1
- 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 +41 -112
- 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/PublicLayout/footers/DefaultFooter/DefaultFooter.tsx +3 -89
- package/src/layouts/PublicLayout/footers/DefaultFooter/ThemeModeControl.tsx +90 -0
- package/src/layouts/PublicLayout/primitives/DesktopDropdownPortal.tsx +47 -0
- package/src/layouts/PublicLayout/primitives/NavControls.tsx +1 -1
- package/src/layouts/PublicLayout/primitives/NavDesktopItems.tsx +2 -83
- package/src/layouts/PublicLayout/primitives/navDesktopStyles.ts +42 -0
- package/src/layouts/PublicLayout/shared/NavbarShell.tsx +4 -171
- package/src/layouts/PublicLayout/shared/index.ts +1 -1
- package/src/layouts/PublicLayout/shared/navbarShell.types.ts +109 -0
- package/src/layouts/PublicLayout/shared/navbarShellStyles.ts +69 -0
- package/src/layouts/SettingsLayout/README.md +23 -10
- package/src/layouts/SettingsLayout/SettingsDialog.tsx +27 -11
- package/src/layouts/SettingsLayout/SettingsForm.tsx +13 -9
- package/src/layouts/SettingsLayout/components/AccountMenuButton.tsx +83 -0
- 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 +63 -131
- package/src/layouts/SettingsLayout/components/TwoFactorSection/TwoFactorSetupDialog.tsx +35 -0
- package/src/layouts/SettingsLayout/components/index.ts +0 -1
- package/src/layouts/SettingsLayout/context/SettingsContext.tsx +5 -2
- package/src/layouts/SettingsLayout/hooks/index.ts +3 -0
- package/src/layouts/SettingsLayout/hooks/useAccountMenuItems.ts +69 -0
- package/src/layouts/SettingsLayout/hooks/useSettingsDialog.ts +6 -3
- 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 +3 -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 +13 -5
- package/src/layouts/SettingsLayout/sections/builtins.tsx +34 -29
- package/src/layouts/SettingsLayout/sections/index.ts +2 -2
- package/src/layouts/SettingsLayout/store.ts +12 -0
- package/src/layouts/SettingsLayout/types.ts +12 -4
- package/src/layouts/_components/locale-switcher/LocaleSwitcher.tsx +1 -1
- package/src/layouts/types/layout.types.ts +5 -6
- package/src/{layouts/AppLayout → shared}/LayoutI18nProvider.tsx +1 -1
- 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
|
@@ -0,0 +1,109 @@
|
|
|
1
|
+
import type { ReactNode } from 'react';
|
|
2
|
+
|
|
3
|
+
import type {
|
|
4
|
+
PublicDesktopDropdownRenderer,
|
|
5
|
+
PublicNavbarHeight,
|
|
6
|
+
PublicNavbarInsetX,
|
|
7
|
+
PublicNavbarPosition,
|
|
8
|
+
PublicNavbarVariant,
|
|
9
|
+
PublicNavLayout,
|
|
10
|
+
} from '../navbarTypes';
|
|
11
|
+
import type { NavigationItem, UserMenuConfig } from '../../types';
|
|
12
|
+
import type { NavAction } from '../primitives/NavActionItem';
|
|
13
|
+
|
|
14
|
+
export interface ShapeInputs {
|
|
15
|
+
/** true at scrollY >= threshold */
|
|
16
|
+
scrolled: boolean;
|
|
17
|
+
/** Matches `transparent` flag at the source. */
|
|
18
|
+
transparent: boolean;
|
|
19
|
+
/**
|
|
20
|
+
* true while the mobile drawer is open. A `transparent` bar MUST go opaque
|
|
21
|
+
* here regardless of scroll position: an open drawer means the brand + close
|
|
22
|
+
* button sit over page content, and a see-through bar leaves them floating on
|
|
23
|
+
* whatever is scrolled behind — the "navbar lost its background" bug.
|
|
24
|
+
*/
|
|
25
|
+
menuOpen: boolean;
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
export interface NavbarShellProps {
|
|
29
|
+
// ── Surface identity (reported to context) ────────────────────────────────
|
|
30
|
+
variant: PublicNavbarVariant;
|
|
31
|
+
position: PublicNavbarPosition;
|
|
32
|
+
|
|
33
|
+
// ── Content ───────────────────────────────────────────────────────────────
|
|
34
|
+
brand?: ReactNode;
|
|
35
|
+
brandHref?: string;
|
|
36
|
+
navigation?: NavigationItem[];
|
|
37
|
+
userMenu?: UserMenuConfig;
|
|
38
|
+
renderDesktopDropdown?: PublicDesktopDropdownRenderer;
|
|
39
|
+
/**
|
|
40
|
+
* Hard cap for how many items to try to fit on desktop. Useful when a
|
|
41
|
+
* consumer wants to force a compact primary row even if more would fit.
|
|
42
|
+
* Defaults to the full list length (let the responsive measurer decide).
|
|
43
|
+
*/
|
|
44
|
+
desktopMaxPrimaryItems?: number;
|
|
45
|
+
|
|
46
|
+
// ── Layout ────────────────────────────────────────────────────────────────
|
|
47
|
+
navLayout?: PublicNavLayout;
|
|
48
|
+
navbarHeight?: PublicNavbarHeight;
|
|
49
|
+
/** Horizontal inset between outer edge and the inner row.
|
|
50
|
+
* @default 'sm' */
|
|
51
|
+
insetX?: PublicNavbarInsetX;
|
|
52
|
+
/** Full override for the row container (escape hatch).
|
|
53
|
+
* When set, wins over `insetX`. */
|
|
54
|
+
innerPadding?: string;
|
|
55
|
+
|
|
56
|
+
// ── Scroll behavior ───────────────────────────────────────────────────────
|
|
57
|
+
hideNavOnScroll?: boolean;
|
|
58
|
+
transparent?: boolean;
|
|
59
|
+
transparentThreshold?: number;
|
|
60
|
+
|
|
61
|
+
// ── Chrome (per-variant) ──────────────────────────────────────────────────
|
|
62
|
+
/** Classes on the outer wrapper (fixed/sticky, top offset, outer x-padding). */
|
|
63
|
+
outerClassName?: string;
|
|
64
|
+
/** Classes for the `<nav>` element defining the shape (rounding, border, shadow). */
|
|
65
|
+
shapeClassName?: string;
|
|
66
|
+
/** Surface classes (bg, backdrop-blur). Driven by `shapeForState`. */
|
|
67
|
+
shapeForState?: (inputs: ShapeInputs) => string;
|
|
68
|
+
|
|
69
|
+
/**
|
|
70
|
+
* Replaces the default actions column (UserMenu + mobile-toggle).
|
|
71
|
+
* Variant owns the mobile toggle too. Receives helpers so it can build the
|
|
72
|
+
* right-hand side however it wants.
|
|
73
|
+
*/
|
|
74
|
+
renderActions?: (ctx: NavbarActionsContext) => ReactNode;
|
|
75
|
+
|
|
76
|
+
// ── Default-actions slots (only used when `renderActions` is NOT provided) ─
|
|
77
|
+
/** Typed CTA pills (Book a demo / Get started / …) before UserMenu. */
|
|
78
|
+
actions?: NavAction[];
|
|
79
|
+
/** Arbitrary ReactNode between actions and UserMenu. */
|
|
80
|
+
actionsLeadingSlot?: ReactNode;
|
|
81
|
+
/** Arbitrary ReactNode after the mobile toggle. */
|
|
82
|
+
actionsTrailingSlot?: ReactNode;
|
|
83
|
+
|
|
84
|
+
// ── Theme + locale controls (rendered next to UserMenu on desktop) ────────
|
|
85
|
+
/** Toggle individual controls. Locale switcher also requires `i18n`. */
|
|
86
|
+
controls?: {
|
|
87
|
+
/** Light / system / dark pill. @default false */
|
|
88
|
+
showThemeSwitcher?: boolean;
|
|
89
|
+
/** Locale dropdown. Requires `i18n`. @default false */
|
|
90
|
+
showLocaleSwitcher?: boolean;
|
|
91
|
+
};
|
|
92
|
+
|
|
93
|
+
// ── Props override (used by variants that proxy ctx differently) ──────────
|
|
94
|
+
mobileMenuOpen?: boolean;
|
|
95
|
+
onMobileMenuToggle?: () => void;
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
export interface NavbarActionsContext {
|
|
99
|
+
userMenu?: UserMenuConfig;
|
|
100
|
+
mobileMenuOpen: boolean;
|
|
101
|
+
toggleMobileMenu: () => void;
|
|
102
|
+
toggleMobileLabel: string;
|
|
103
|
+
navLayout: PublicNavLayout;
|
|
104
|
+
/**
|
|
105
|
+
* Pre-built theme/locale controls node. `null` when both toggles are off.
|
|
106
|
+
* Variants with a custom `renderActions` can choose where to place it.
|
|
107
|
+
*/
|
|
108
|
+
controls: ReactNode | null;
|
|
109
|
+
}
|
|
@@ -0,0 +1,69 @@
|
|
|
1
|
+
'use client';
|
|
2
|
+
|
|
3
|
+
import { cn } from '@djangocfg/ui-core/lib';
|
|
4
|
+
|
|
5
|
+
import type { PublicNavbarHeight, PublicNavbarInsetX } from '../navbarTypes';
|
|
6
|
+
|
|
7
|
+
/**
|
|
8
|
+
* Vertical padding, per breakpoint. Two independent scales rather than one with
|
|
9
|
+
* `sm:` steps inside it: the mobile row's tallest element is the 44px burger,
|
|
10
|
+
* so padding there is pure added height, while the desktop row needs the extra
|
|
11
|
+
* air to keep the link list off the panel edge.
|
|
12
|
+
*/
|
|
13
|
+
export const heightClsDesktop: Record<PublicNavbarHeight, string> = {
|
|
14
|
+
sm: 'py-2',
|
|
15
|
+
md: 'py-3.5',
|
|
16
|
+
lg: 'py-5',
|
|
17
|
+
};
|
|
18
|
+
|
|
19
|
+
export const heightClsMobile: Record<PublicNavbarHeight, string> = {
|
|
20
|
+
sm: 'py-1.5',
|
|
21
|
+
md: 'py-2',
|
|
22
|
+
lg: 'py-3',
|
|
23
|
+
};
|
|
24
|
+
|
|
25
|
+
/**
|
|
26
|
+
* Token → Tailwind class for the inner-row horizontal padding.
|
|
27
|
+
*
|
|
28
|
+
* Deliberately ASYMMETRIC at every breakpoint (`pl` > `pr`) to correct an
|
|
29
|
+
* optical imbalance, not a measured one. On the left the gutter meets the
|
|
30
|
+
* brand's glyphs directly; on the right it meets a button — an icon square on
|
|
31
|
+
* phones, the Login pill on desktop — whose box carries its own internal
|
|
32
|
+
* padding around the glyph. Equal `px` therefore *looks* lopsided, with a
|
|
33
|
+
* conspicuous moat to the right of the last control.
|
|
34
|
+
*
|
|
35
|
+
* It also stacks with the navbar shell's own outer gutter (`px-3 sm:px-4
|
|
36
|
+
* lg:px-6`), which is why the right side stays small in absolute terms: at
|
|
37
|
+
* `lg` the symmetric version put ~48px between the Login button and the
|
|
38
|
+
* viewport edge.
|
|
39
|
+
*/
|
|
40
|
+
/** Desktop (`lg`+) horizontal inset. Right stays trimmed — see the note above. */
|
|
41
|
+
export const insetXClsDesktop: Record<'none' | 'xs' | 'sm' | 'md' | 'lg', string> = {
|
|
42
|
+
none: '',
|
|
43
|
+
xs: 'lg:pl-4 lg:pr-1.5',
|
|
44
|
+
sm: 'lg:pl-6 lg:pr-2',
|
|
45
|
+
md: 'lg:pl-8 lg:pr-3',
|
|
46
|
+
lg: 'lg:pl-12 lg:pr-5',
|
|
47
|
+
};
|
|
48
|
+
|
|
49
|
+
/** Mobile horizontal inset — tighter, and asymmetric for the same reason. */
|
|
50
|
+
export const insetXClsMobile: Record<'none' | 'xs' | 'sm' | 'md' | 'lg', string> = {
|
|
51
|
+
none: '',
|
|
52
|
+
xs: 'pl-3 pr-1',
|
|
53
|
+
sm: 'pl-4 pr-1.5',
|
|
54
|
+
md: 'pl-5 pr-2',
|
|
55
|
+
lg: 'pl-6 pr-3',
|
|
56
|
+
};
|
|
57
|
+
|
|
58
|
+
/**
|
|
59
|
+
* Resolves the inset token into one class string covering both breakpoints.
|
|
60
|
+
*
|
|
61
|
+
* A caller may also pass raw Tailwind instead of a token; that escape hatch is
|
|
62
|
+
* applied verbatim to both rows, since we cannot know how to split it.
|
|
63
|
+
*/
|
|
64
|
+
export function resolveInsetX(value: PublicNavbarInsetX | undefined): string {
|
|
65
|
+
const token = (value ?? 'sm') as keyof typeof insetXClsMobile;
|
|
66
|
+
if (!(token in insetXClsMobile)) return value as string;
|
|
67
|
+
return cn(insetXClsMobile[token], insetXClsDesktop[token]);
|
|
68
|
+
}
|
|
69
|
+
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
# SettingsLayout
|
|
2
2
|
|
|
3
|
-
A Claude.ai-style **settings surface**: a
|
|
3
|
+
A Claude.ai-style **settings surface**: a grouped section rail (search appears from 10 sections, `searchable`) on the
|
|
4
4
|
left and a scrollable detail panel on the right. Available as a **global dialog**
|
|
5
5
|
(URL-hash driven, zustand-backed, openable from anywhere) or an **inline form**.
|
|
6
6
|
|
|
@@ -127,7 +127,7 @@ ordering/labelling, or just name an ad-hoc `group` on a section:
|
|
|
127
127
|
|
|
128
128
|
```ts
|
|
129
129
|
const groups = [
|
|
130
|
-
{ id: 'general',
|
|
130
|
+
{ id: 'general', order: 0 },
|
|
131
131
|
{ id: 'workspace', label: 'Workspace', order: 1 },
|
|
132
132
|
];
|
|
133
133
|
```
|
|
@@ -139,16 +139,19 @@ toggled via `builtins`:
|
|
|
139
139
|
|
|
140
140
|
| id | flag | default | content |
|
|
141
141
|
| ----------- | -------------------- | ------- | ------------------------------------ |
|
|
142
|
-
| `account` | `builtins.account` | `true` | avatar
|
|
143
|
-
| `
|
|
142
|
+
| `account` | `builtins.account` | `true` | avatar, inline-editable profile/work fields, **Security** block (two-factor auth; hide with `builtins.security: false`) and a **collapsible Danger zone** (delete account) |
|
|
143
|
+
| `general` | `builtins.general` | `true` | **General** block: **Theme** (`ThemeSegmented`), **Time zone** (`TimezoneSelect` → `useTimezone`, sent as `X-Timezone`) and **Language** (shown when a layout i18n context exists); host rows from `generalRows` follow in a second block ("Other") |
|
|
144
144
|
| `api-keys` | `builtins.apiKeys` | `true` | API key display / regenerate / test (row-style) |
|
|
145
145
|
|
|
146
|
-
|
|
146
|
+
`generalRows` (on `SettingsDialog` / `SettingsForm` / `PrivateLayout settings`) takes
|
|
147
|
+
your own `SettingRow`s for preferences only the host knows (e.g. a product-specific toggle); the layout adds none itself.
|
|
148
|
+
|
|
149
|
+
Rail order: **Account** (opens first), **Preferences** (id `general`), then Platform → API keys. Built-ins are merged **ahead of** app sections. They reuse the localized labels
|
|
147
150
|
and save/validation logic from `ProfileProvider`, so behaviour stays in one place.
|
|
148
151
|
The built-in bodies are built from the ui-core `SettingRow`/`SettingsBlock`
|
|
149
152
|
primitives (see below) — flat rows with hairline dividers, no card frames.
|
|
150
153
|
|
|
151
|
-
> The
|
|
154
|
+
> The Account security block and `api-keys` body hit the real backend (2FA status, API key
|
|
152
155
|
> retrieval). With no backend (e.g. Storybook) they show an error/empty state —
|
|
153
156
|
> that's expected; gate them with `builtins` where there's no API.
|
|
154
157
|
|
|
@@ -215,17 +218,18 @@ SettingsLayout/
|
|
|
215
218
|
│ └── useSettingsSections.ts pure: merge/order/group/filter sections
|
|
216
219
|
├── components/
|
|
217
220
|
│ ├── SettingsShell.tsx desktop rail+panel vs mobile tabs+panel (useIsMobile)
|
|
218
|
-
│ ├──
|
|
221
|
+
│ ├── SettingsShell.tsx header bar + rail + panel (desktop), tabs + panel (mobile)
|
|
222
|
+
│ ├── SettingsNav.tsx desktop rail: search + grouped items
|
|
219
223
|
│ ├── SettingsNavItem.tsx one rail row
|
|
220
224
|
│ ├── SettingsTabs.tsx mobile horizontal tab strip (text chips, auto-scroll active)
|
|
221
225
|
│ ├── SettingsSearch.tsx filter box
|
|
222
226
|
│ └── SettingsPanel.tsx header + independently-scrolling body
|
|
223
227
|
└── sections/
|
|
224
|
-
├── builtins.tsx account/
|
|
225
|
-
├──
|
|
228
|
+
├── builtins.tsx general/account/api-keys factory
|
|
229
|
+
├── GeneralSection.tsx Appearance + Language + host `generalRows`
|
|
230
|
+
├── AccountSection.tsx avatar + fields + 2FA + danger zone (row-style)
|
|
226
231
|
├── PreferencesRows.tsx Language dropdown + ThemeSegmented as SettingRows
|
|
227
232
|
├── DeleteAccountRow.tsx compact destructive row (inside collapsible Danger zone)
|
|
228
|
-
├── SecuritySection.tsx 2FA wrapper
|
|
229
233
|
└── ApiKeysSection.tsx API-keys wrapper
|
|
230
234
|
```
|
|
231
235
|
|
|
@@ -256,3 +260,12 @@ exported too; the row primitives come from `@djangocfg/ui-core/components`.
|
|
|
256
260
|
2. `useProfileDialogStore.open({ initialTab })` → `useSettingsDialog().open(id)`.
|
|
257
261
|
3. Mount `<SettingsDialog/>` instead of `<ProfileDialog/>` (or pass `settings` to
|
|
258
262
|
PrivateLayout). The account button can be repointed to `useSettingsDialog()`.
|
|
263
|
+
|
|
264
|
+
## Localization
|
|
265
|
+
|
|
266
|
+
Built-in text (rail labels, Preferences, Security, API key) comes from the
|
|
267
|
+
self-contained dictionaries in `i18n/` (17 locales, `useSettingsCopy`), following the
|
|
268
|
+
`@djangocfg/i18n` provider the host mounts, with English as the fallback. Widgets a host adds as sections (for example `mcpSettingsSection()` from `@djangocfg/widget-mcp`)
|
|
269
|
+
follow the same provider. Text the host passes in (`sections`, `groups`, `generalRows`, titles) is the
|
|
270
|
+
host's to translate; profile field labels still come from the central `layouts.profilePage.*`
|
|
271
|
+
catalogue. A new built-in string goes into every `i18n/locales/*.ts` (`tsc` names a missing key).
|
|
@@ -1,7 +1,8 @@
|
|
|
1
1
|
'use client';
|
|
2
2
|
|
|
3
|
-
import React, { useCallback, useMemo } from 'react';
|
|
3
|
+
import React, { useCallback, useEffect, useMemo } from 'react';
|
|
4
4
|
|
|
5
|
+
import { useAuth } from '@djangocfg/api/auth';
|
|
5
6
|
import {
|
|
6
7
|
Dialog,
|
|
7
8
|
DialogContent,
|
|
@@ -13,6 +14,7 @@ import { cn } from '@djangocfg/ui-core/lib';
|
|
|
13
14
|
|
|
14
15
|
import { SettingsForm } from './SettingsForm';
|
|
15
16
|
import { useSettingsUrl } from './hooks/useSettingsUrl';
|
|
17
|
+
import { useSettingsCopy } from './i18n';
|
|
16
18
|
import { buildBuiltinSections } from './sections';
|
|
17
19
|
import { useSettingsDialogStore } from './store';
|
|
18
20
|
import type { SettingsDialogProps } from './types';
|
|
@@ -34,8 +36,9 @@ export const SettingsDialog: React.FC<SettingsDialogProps> = ({
|
|
|
34
36
|
sections: appSections = [],
|
|
35
37
|
groups,
|
|
36
38
|
builtins = {},
|
|
39
|
+
generalRows,
|
|
37
40
|
initialSection,
|
|
38
|
-
searchable =
|
|
41
|
+
searchable = 'auto',
|
|
39
42
|
enableDeleteAccount = true,
|
|
40
43
|
}) => {
|
|
41
44
|
const isOpen = useSettingsDialogStore((s) => s.isOpen);
|
|
@@ -43,39 +46,51 @@ export const SettingsDialog: React.FC<SettingsDialogProps> = ({
|
|
|
43
46
|
const close = useSettingsDialogStore((s) => s.close);
|
|
44
47
|
const setSection = useSettingsDialogStore((s) => s.setSection);
|
|
45
48
|
const isMobile = useIsMobile();
|
|
49
|
+
const copy = useSettingsCopy();
|
|
50
|
+
// Settings are about the signed-in user: for a guest the panels are empty, so `#settings` opens nothing until sign-in.
|
|
51
|
+
const { isAuthenticated } = useAuth();
|
|
46
52
|
|
|
47
53
|
// Set of valid ids, so a bad URL hash falls back gracefully instead of
|
|
48
54
|
// selecting a non-existent section. Cheap to recompute; built-ins are pure.
|
|
49
55
|
const knownIds = useMemo(() => {
|
|
50
|
-
const builtin = buildBuiltinSections({ builtins, enableDeleteAccount });
|
|
56
|
+
const builtin = buildBuiltinSections({ builtins, enableDeleteAccount, copy });
|
|
51
57
|
const ids = new Set<string>();
|
|
52
58
|
[...builtin, ...appSections].forEach((s) => ids.add(s.id));
|
|
53
59
|
return ids;
|
|
54
|
-
}, [builtins, enableDeleteAccount, appSections]);
|
|
60
|
+
}, [builtins, enableDeleteAccount, appSections, copy]);
|
|
61
|
+
|
|
62
|
+
// Menus (the account menu) link to these; only this component knows the host's sections.
|
|
63
|
+
const setMenuSections = useSettingsDialogStore((s) => s.setSections);
|
|
64
|
+
useEffect(() => {
|
|
65
|
+
const all = [...buildBuiltinSections({ builtins, enableDeleteAccount, copy }), ...appSections];
|
|
66
|
+
setMenuSections(all.filter((s) => !s.hidden).map(({ id, label, icon }) => ({ id, label, icon })));
|
|
67
|
+
}, [builtins, enableDeleteAccount, appSections, copy, setMenuSections]);
|
|
55
68
|
|
|
56
69
|
const resolveSection = useCallback(
|
|
57
70
|
(id: string | null) => (id && knownIds.has(id) ? id : null),
|
|
58
71
|
[knownIds],
|
|
59
72
|
);
|
|
60
73
|
|
|
61
|
-
useSettingsUrl({ urlKey, enabled: syncUrl, resolveSection });
|
|
74
|
+
useSettingsUrl({ urlKey, enabled: syncUrl && isAuthenticated, resolveSection });
|
|
62
75
|
|
|
63
|
-
const resolvedTitle = title ??
|
|
76
|
+
const resolvedTitle = title ?? copy.nav.title;
|
|
64
77
|
|
|
65
78
|
return (
|
|
66
|
-
<Dialog open={isOpen} onOpenChange={(open) => !open && close()}>
|
|
79
|
+
<Dialog open={isOpen && isAuthenticated} onOpenChange={(open) => !open && close()}>
|
|
67
80
|
<DialogContent
|
|
68
|
-
// Desktop:
|
|
81
|
+
// Desktop: large centred card, proportional to the viewport.
|
|
69
82
|
// Mobile: near-fullscreen sheet (small inset) so the phone settings get
|
|
70
83
|
// the whole screen, like Claude. Elevated `bg-card` lifts it off the page.
|
|
71
84
|
className={cn(
|
|
72
|
-
'flex max-w-none flex-col gap-0 overflow-hidden border border-
|
|
85
|
+
'flex max-w-none flex-col gap-0 overflow-hidden border border-foreground/5 bg-card p-0',
|
|
73
86
|
isMobile
|
|
74
87
|
? 'h-[calc(100dvh-2rem)] w-[calc(100vw-1.5rem)]'
|
|
75
|
-
:
|
|
88
|
+
// Claude proportions: 1024×800 at most, 1.28:1 at every size (ui-core
|
|
89
|
+
// DialogContent derives the width from the viewport height).
|
|
90
|
+
: '[--aspect-dialog:1.28] [--width-dialog:1024px]',
|
|
76
91
|
)}
|
|
77
92
|
>
|
|
78
|
-
{/* Accessible label — the visible heading
|
|
93
|
+
{/* Accessible label — the visible heading is the shell header bar. */}
|
|
79
94
|
<DialogTitle className="sr-only">
|
|
80
95
|
{typeof resolvedTitle === 'string' ? resolvedTitle : 'Settings'}
|
|
81
96
|
</DialogTitle>
|
|
@@ -89,6 +104,7 @@ export const SettingsDialog: React.FC<SettingsDialogProps> = ({
|
|
|
89
104
|
sections={appSections}
|
|
90
105
|
groups={groups}
|
|
91
106
|
builtins={builtins}
|
|
107
|
+
generalRows={generalRows}
|
|
92
108
|
initialSection={initialSection}
|
|
93
109
|
searchable={searchable}
|
|
94
110
|
enableDeleteAccount={enableDeleteAccount}
|
|
@@ -8,7 +8,8 @@ import { Preloader } from '@djangocfg/ui-core/components';
|
|
|
8
8
|
|
|
9
9
|
import { SettingsShell } from './components';
|
|
10
10
|
import { SettingsProvider } from './context';
|
|
11
|
-
import {
|
|
11
|
+
import { useSettingsCopy } from './i18n';
|
|
12
|
+
import { buildBuiltinSections, BUILTIN_GROUP, PLATFORM_GROUP } from './sections';
|
|
12
13
|
import type { SettingsFormProps, SettingsSection } from './types';
|
|
13
14
|
|
|
14
15
|
/**
|
|
@@ -30,8 +31,9 @@ export const SettingsForm: React.FC<SettingsFormProps> = ({
|
|
|
30
31
|
sections: appSections = [],
|
|
31
32
|
groups,
|
|
32
33
|
builtins = {},
|
|
34
|
+
generalRows,
|
|
33
35
|
initialSection,
|
|
34
|
-
searchable =
|
|
36
|
+
searchable = 'auto',
|
|
35
37
|
enableDeleteAccount = true,
|
|
36
38
|
activeSection,
|
|
37
39
|
onSectionChange,
|
|
@@ -40,21 +42,23 @@ export const SettingsForm: React.FC<SettingsFormProps> = ({
|
|
|
40
42
|
bare = false,
|
|
41
43
|
}: SettingsFormProps) => {
|
|
42
44
|
const t = useAppT();
|
|
45
|
+
const copy = useSettingsCopy();
|
|
43
46
|
const { user, isLoading } = useAuth();
|
|
44
47
|
|
|
45
48
|
// Built-ins first, then app sections. Memoized so JSX identity is stable.
|
|
46
49
|
const allSections = useMemo<SettingsSection[]>(() => {
|
|
47
|
-
const builtin = buildBuiltinSections({ builtins, enableDeleteAccount });
|
|
50
|
+
const builtin = buildBuiltinSections({ builtins, enableDeleteAccount, generalRows, copy });
|
|
48
51
|
return [...builtin, ...appSections];
|
|
49
|
-
}, [builtins, enableDeleteAccount, appSections]);
|
|
52
|
+
}, [builtins, enableDeleteAccount, generalRows, appSections, copy]);
|
|
50
53
|
|
|
51
54
|
// Ensure the built-in group exists/ordered even if the app passed its own groups.
|
|
52
55
|
const allGroups = useMemo(() => {
|
|
53
56
|
const provided = groups ?? [];
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
57
|
+
const missing = [BUILTIN_GROUP, { ...PLATFORM_GROUP, label: copy.nav.platform }].filter(
|
|
58
|
+
(b) => !provided.some((g) => g.id === b.id),
|
|
59
|
+
);
|
|
60
|
+
return [...missing, ...provided];
|
|
61
|
+
}, [groups, copy]);
|
|
58
62
|
|
|
59
63
|
// Uncontrolled selection fallback.
|
|
60
64
|
const [localActive, setLocalActive] = useState<string | null>(initialSection ?? null);
|
|
@@ -82,7 +86,7 @@ export const SettingsForm: React.FC<SettingsFormProps> = ({
|
|
|
82
86
|
}
|
|
83
87
|
|
|
84
88
|
// Default to "Settings" — this surface is broader than the old Profile page.
|
|
85
|
-
const resolvedTitle = title ??
|
|
89
|
+
const resolvedTitle = title ?? copy.nav.title;
|
|
86
90
|
|
|
87
91
|
return (
|
|
88
92
|
<SettingsProvider
|
|
@@ -0,0 +1,83 @@
|
|
|
1
|
+
'use client';
|
|
2
|
+
|
|
3
|
+
import { ChevronDown } from 'lucide-react';
|
|
4
|
+
import React from 'react';
|
|
5
|
+
|
|
6
|
+
import { useAuth } from '@djangocfg/api/auth';
|
|
7
|
+
import { Avatar, AvatarFallback, AvatarImage, Button, MenuBuilder } from '@djangocfg/ui-core/components';
|
|
8
|
+
import type { MenuItem } from '@djangocfg/ui-core/components';
|
|
9
|
+
import { cn } from '@djangocfg/ui-core/lib';
|
|
10
|
+
|
|
11
|
+
import { useAccountMenuItems } from '../hooks/useAccountMenuItems';
|
|
12
|
+
|
|
13
|
+
export interface AccountMenuButtonProps {
|
|
14
|
+
/** `row`: avatar, name and chevron (a sidebar footer; the menu opens up). `avatar`: the avatar alone (a header; the menu opens down). */
|
|
15
|
+
variant?: 'row' | 'avatar';
|
|
16
|
+
/** Items between the Settings shortcuts and Sign out (language, app links). */
|
|
17
|
+
extra?: MenuItem[];
|
|
18
|
+
/** Small muted text after the name (a plan). */
|
|
19
|
+
plan?: string | null;
|
|
20
|
+
className?: string;
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
/**
|
|
24
|
+
* The ready user menu: trigger button plus the shared menu (email, Settings, a shortcut per
|
|
25
|
+
* Settings section, Sign out). Drop it into any chrome; renders nothing for a signed-out visitor.
|
|
26
|
+
* `PrivateLayout`'s sidebar footer builds on the same `useAccountMenuItems`.
|
|
27
|
+
*/
|
|
28
|
+
export const AccountMenuButton: React.FC<AccountMenuButtonProps> = ({
|
|
29
|
+
variant = 'row',
|
|
30
|
+
extra,
|
|
31
|
+
plan,
|
|
32
|
+
className,
|
|
33
|
+
}) => {
|
|
34
|
+
const { user } = useAuth();
|
|
35
|
+
const items = useAccountMenuItems({ extra });
|
|
36
|
+
const [open, setOpen] = React.useState(false);
|
|
37
|
+
|
|
38
|
+
if (!user) return null;
|
|
39
|
+
|
|
40
|
+
const name = user.display_username || user.email || 'User';
|
|
41
|
+
const initial = (name.charAt(0) || '?').toUpperCase();
|
|
42
|
+
const isRow = variant === 'row';
|
|
43
|
+
|
|
44
|
+
return (
|
|
45
|
+
<MenuBuilder
|
|
46
|
+
items={items}
|
|
47
|
+
open={open}
|
|
48
|
+
onOpenChange={setOpen}
|
|
49
|
+
side={isRow ? 'top' : 'bottom'}
|
|
50
|
+
align={isRow ? 'start' : 'end'}
|
|
51
|
+
sideOffset={8}
|
|
52
|
+
contentClassName="w-60 p-1.5"
|
|
53
|
+
>
|
|
54
|
+
<Button
|
|
55
|
+
type="button"
|
|
56
|
+
variant="ghost"
|
|
57
|
+
aria-label={name}
|
|
58
|
+
className={cn(
|
|
59
|
+
'group min-w-0 select-none gap-2 rounded-lg font-normal focus-visible:ring-0',
|
|
60
|
+
isRow ? 'h-9 w-full justify-start px-2 text-left' : 'size-9 p-0',
|
|
61
|
+
className,
|
|
62
|
+
)}
|
|
63
|
+
>
|
|
64
|
+
<Avatar className="size-6 shrink-0">
|
|
65
|
+
<AvatarImage src={user.avatar ?? ''} alt={name} />
|
|
66
|
+
<AvatarFallback className="text-xs font-medium">{initial}</AvatarFallback>
|
|
67
|
+
</Avatar>
|
|
68
|
+
{isRow && (
|
|
69
|
+
<>
|
|
70
|
+
<span className="flex min-w-0 items-baseline gap-1">
|
|
71
|
+
<span className="truncate text-sm">{name}</span>
|
|
72
|
+
{plan ? <span className="shrink-0 text-xs text-muted-foreground">· {plan}</span> : null}
|
|
73
|
+
</span>
|
|
74
|
+
<ChevronDown
|
|
75
|
+
className="ml-auto size-4 shrink-0 opacity-50 transition-transform duration-150 group-data-[state=open]:rotate-180"
|
|
76
|
+
aria-hidden
|
|
77
|
+
/>
|
|
78
|
+
</>
|
|
79
|
+
)}
|
|
80
|
+
</Button>
|
|
81
|
+
</MenuBuilder>
|
|
82
|
+
);
|
|
83
|
+
};
|
|
@@ -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
|
]);
|