@djangocfg/layouts 1.2.32 → 1.2.34
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 +5 -5
- package/src/auth/context/AuthContext.tsx +15 -1
- package/src/index.ts +4 -1
- package/src/layouts/AppLayout/AppLayout.tsx +9 -2
- package/src/layouts/AppLayout/components/PackageVersions/packageVersions.config.ts +8 -8
- package/src/layouts/AppLayout/layouts/AdminLayout/AdminLayout.tsx +39 -13
- package/src/layouts/AppLayout/layouts/AdminLayout/hooks/useApp.ts +37 -8
- package/src/layouts/AppLayout/providers/CoreProviders.tsx +12 -2
- package/src/layouts/UILayout/components/layout/Header/Header.tsx +11 -4
- package/src/layouts/UILayout/components/layout/Header/HeaderDesktop.tsx +14 -7
- package/src/layouts/UILayout/components/layout/Header/TestValidationButton.tsx +265 -0
- package/src/layouts/UILayout/components/layout/Header/index.ts +2 -0
- package/src/validation/README.md +593 -0
- package/src/validation/REFACTORING.md +162 -0
- package/src/validation/ValidationErrorButtons.tsx +80 -0
- package/src/validation/ValidationErrorContext.tsx +333 -0
- package/src/validation/ValidationErrorToast.tsx +181 -0
- package/src/validation/curl-generator.ts +118 -0
- package/src/validation/index.ts +36 -0
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@djangocfg/layouts",
|
|
3
|
-
"version": "1.2.
|
|
3
|
+
"version": "1.2.34",
|
|
4
4
|
"description": "Layout system and components for Unrealon applications",
|
|
5
5
|
"author": {
|
|
6
6
|
"name": "DjangoCFG",
|
|
@@ -63,9 +63,9 @@
|
|
|
63
63
|
"check": "tsc --noEmit"
|
|
64
64
|
},
|
|
65
65
|
"peerDependencies": {
|
|
66
|
-
"@djangocfg/api": "^1.2.
|
|
67
|
-
"@djangocfg/og-image": "^1.2.
|
|
68
|
-
"@djangocfg/ui": "^1.2.
|
|
66
|
+
"@djangocfg/api": "^1.2.34",
|
|
67
|
+
"@djangocfg/og-image": "^1.2.34",
|
|
68
|
+
"@djangocfg/ui": "^1.2.34",
|
|
69
69
|
"@hookform/resolvers": "^5.2.0",
|
|
70
70
|
"consola": "^3.4.2",
|
|
71
71
|
"lucide-react": "^0.468.0",
|
|
@@ -86,7 +86,7 @@
|
|
|
86
86
|
"vidstack": "0.6.15"
|
|
87
87
|
},
|
|
88
88
|
"devDependencies": {
|
|
89
|
-
"@djangocfg/typescript-config": "^1.2.
|
|
89
|
+
"@djangocfg/typescript-config": "^1.2.34",
|
|
90
90
|
"@types/node": "^24.7.2",
|
|
91
91
|
"@types/react": "19.2.2",
|
|
92
92
|
"@types/react-dom": "19.2.1",
|
|
@@ -156,6 +156,10 @@ const AuthProviderInternal: React.FC<AuthProviderProps> = ({ children, config })
|
|
|
156
156
|
const initializeAuth = async () => {
|
|
157
157
|
authLogger.info('Initializing auth...');
|
|
158
158
|
|
|
159
|
+
// Check if running in iframe (AdminLayout will handle auth via postMessage)
|
|
160
|
+
const isInIframe = typeof window !== 'undefined' && window.self !== window.top;
|
|
161
|
+
authLogger.info('Is in iframe:', isInIframe);
|
|
162
|
+
|
|
159
163
|
// Debug token state
|
|
160
164
|
const token = api.getToken();
|
|
161
165
|
const refreshToken = api.getRefreshToken();
|
|
@@ -183,11 +187,21 @@ const AuthProviderInternal: React.FC<AuthProviderProps> = ({ children, config })
|
|
|
183
187
|
return;
|
|
184
188
|
}
|
|
185
189
|
|
|
190
|
+
// In iframe mode WITHOUT tokens yet - wait for AdminLayout to receive them via postMessage
|
|
191
|
+
// Don't initialize yet - AdminLayout.handleAuthToken will call loadCurrentProfile
|
|
192
|
+
if (isInIframe && !hasTokens) {
|
|
193
|
+
authLogger.info('Running in iframe without tokens - waiting for parent to send via postMessage');
|
|
194
|
+
authLogger.info('AdminLayout will handle auth initialization, skipping AuthContext init');
|
|
195
|
+
setInitialized(true); // Mark as initialized to prevent re-initialization
|
|
196
|
+
setIsLoading(false);
|
|
197
|
+
return;
|
|
198
|
+
}
|
|
199
|
+
|
|
186
200
|
if (hasTokens) {
|
|
187
201
|
setIsLoading(true);
|
|
188
202
|
try {
|
|
189
203
|
authLogger.info('No cached profile found, loading from API...');
|
|
190
|
-
await loadCurrentProfile();
|
|
204
|
+
await loadCurrentProfile('AuthContext.initializeAuth');
|
|
191
205
|
} catch (error) {
|
|
192
206
|
authLogger.error('Failed to load profile during initialization:', error);
|
|
193
207
|
// If profile loading fails, clear auth state
|
package/src/index.ts
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* @djangocfg/layouts
|
|
3
|
-
*
|
|
3
|
+
*
|
|
4
4
|
* Reusable layout components and authentication system
|
|
5
5
|
*/
|
|
6
6
|
|
|
@@ -13,3 +13,6 @@ export * from './layouts';
|
|
|
13
13
|
// Snippets - Reusable UI components
|
|
14
14
|
export * from './snippets';
|
|
15
15
|
|
|
16
|
+
// Validation error tracking
|
|
17
|
+
export * from './validation';
|
|
18
|
+
|
|
@@ -36,6 +36,7 @@ import { AdminLayout } from './layouts/AdminLayout';
|
|
|
36
36
|
import { determineLayoutMode, getRedirectUrl } from './utils';
|
|
37
37
|
import { useAuth } from '../../auth';
|
|
38
38
|
import type { AppLayoutConfig } from './types';
|
|
39
|
+
import type { ValidationErrorConfig } from '../../validation';
|
|
39
40
|
|
|
40
41
|
export interface AppLayoutProps {
|
|
41
42
|
children: ReactNode;
|
|
@@ -69,6 +70,12 @@ export interface AppLayoutProps {
|
|
|
69
70
|
* @example showPackageVersions={true}
|
|
70
71
|
*/
|
|
71
72
|
showPackageVersions?: boolean;
|
|
73
|
+
/**
|
|
74
|
+
* Configuration for validation error tracking
|
|
75
|
+
* @default { enableToast: true, maxErrors: 50 }
|
|
76
|
+
* @example validationConfig={{ enableToast: false }}
|
|
77
|
+
*/
|
|
78
|
+
validationConfig?: Partial<ValidationErrorConfig>;
|
|
72
79
|
}
|
|
73
80
|
|
|
74
81
|
/**
|
|
@@ -249,7 +256,7 @@ function LayoutRouter({
|
|
|
249
256
|
* </AppLayout>
|
|
250
257
|
* ```
|
|
251
258
|
*/
|
|
252
|
-
export function AppLayout({ children, config, disableLayout = false, forceLayout, fontFamily, showPackageVersions }: AppLayoutProps) {
|
|
259
|
+
export function AppLayout({ children, config, disableLayout = false, forceLayout, fontFamily, showPackageVersions, validationConfig }: AppLayoutProps) {
|
|
253
260
|
const router = useRouter();
|
|
254
261
|
|
|
255
262
|
// Check if ErrorBoundary is enabled (default: true)
|
|
@@ -292,7 +299,7 @@ export function AppLayout({ children, config, disableLayout = false, forceLayout
|
|
|
292
299
|
}} />
|
|
293
300
|
)}
|
|
294
301
|
|
|
295
|
-
<CoreProviders config={config}>
|
|
302
|
+
<CoreProviders config={config} validationConfig={validationConfig}>
|
|
296
303
|
{appContent}
|
|
297
304
|
</CoreProviders>
|
|
298
305
|
</>
|
|
@@ -16,36 +16,36 @@ export interface PackageInfo {
|
|
|
16
16
|
/**
|
|
17
17
|
* Package versions registry
|
|
18
18
|
* Auto-synced from package.json files
|
|
19
|
-
* Last updated: 2025-11-
|
|
19
|
+
* Last updated: 2025-11-11T06:22:05.284Z
|
|
20
20
|
*/
|
|
21
21
|
const PACKAGE_VERSIONS: PackageInfo[] = [
|
|
22
22
|
{
|
|
23
23
|
"name": "@djangocfg/ui",
|
|
24
|
-
"version": "1.2.
|
|
24
|
+
"version": "1.2.34"
|
|
25
25
|
},
|
|
26
26
|
{
|
|
27
27
|
"name": "@djangocfg/api",
|
|
28
|
-
"version": "1.2.
|
|
28
|
+
"version": "1.2.34"
|
|
29
29
|
},
|
|
30
30
|
{
|
|
31
31
|
"name": "@djangocfg/layouts",
|
|
32
|
-
"version": "1.2.
|
|
32
|
+
"version": "1.2.34"
|
|
33
33
|
},
|
|
34
34
|
{
|
|
35
35
|
"name": "@djangocfg/markdown",
|
|
36
|
-
"version": "1.2.
|
|
36
|
+
"version": "1.2.34"
|
|
37
37
|
},
|
|
38
38
|
{
|
|
39
39
|
"name": "@djangocfg/og-image",
|
|
40
|
-
"version": "1.2.
|
|
40
|
+
"version": "1.2.34"
|
|
41
41
|
},
|
|
42
42
|
{
|
|
43
43
|
"name": "@djangocfg/eslint-config",
|
|
44
|
-
"version": "1.2.
|
|
44
|
+
"version": "1.2.34"
|
|
45
45
|
},
|
|
46
46
|
{
|
|
47
47
|
"name": "@djangocfg/typescript-config",
|
|
48
|
-
"version": "1.2.
|
|
48
|
+
"version": "1.2.34"
|
|
49
49
|
}
|
|
50
50
|
];
|
|
51
51
|
|
|
@@ -118,39 +118,65 @@ function AdminLayoutClient({
|
|
|
118
118
|
const { isAdminUser, user, isLoading, isAuthenticated, loadCurrentProfile } = useAuth();
|
|
119
119
|
// console.log('[AdminLayout] Rendering with user:', user, 'isLoading:', isLoading, 'isAuthenticated:', isAuthenticated);
|
|
120
120
|
|
|
121
|
-
// Use
|
|
121
|
+
// Use refs to prevent re-renders and maintain state across renders
|
|
122
122
|
const profileLoadedRef = React.useRef(false);
|
|
123
|
+
const tokensReceivedRef = React.useRef(false);
|
|
124
|
+
const loadCurrentProfileRef = React.useRef(loadCurrentProfile);
|
|
123
125
|
|
|
124
|
-
//
|
|
126
|
+
// Update ref when loadCurrentProfile changes (but don't trigger re-render)
|
|
127
|
+
React.useEffect(() => {
|
|
128
|
+
loadCurrentProfileRef.current = loadCurrentProfile;
|
|
129
|
+
}, [loadCurrentProfile]);
|
|
130
|
+
|
|
131
|
+
// Create a STABLE callback that NEVER changes (no dependencies)
|
|
132
|
+
// Use refs to access latest values without recreating the callback
|
|
125
133
|
const handleAuthToken = React.useCallback(async (authToken: string, refreshToken?: string) => {
|
|
126
|
-
|
|
127
|
-
|
|
134
|
+
console.log('[AdminLayout] handleAuthToken called, tokensReceived:', tokensReceivedRef.current, 'profileLoaded:', profileLoadedRef.current);
|
|
135
|
+
|
|
136
|
+
// Prevent duplicate token processing
|
|
137
|
+
if (tokensReceivedRef.current) {
|
|
138
|
+
console.log('[AdminLayout] Tokens already received and processed, ignoring duplicate');
|
|
139
|
+
return;
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
// Mark tokens as received IMMEDIATELY to prevent race conditions
|
|
143
|
+
tokensReceivedRef.current = true;
|
|
144
|
+
|
|
145
|
+
console.log('[AdminLayout] First time receiving tokens, processing...');
|
|
146
|
+
console.log('[AdminLayout] authToken:', authToken.substring(0, 20) + '...', 'refreshToken:', refreshToken ? refreshToken.substring(0, 20) + '...' : 'null');
|
|
128
147
|
|
|
129
148
|
// Always set tokens in API client
|
|
130
149
|
api.setToken(authToken, refreshToken);
|
|
131
|
-
|
|
150
|
+
console.log('[AdminLayout] Tokens set in API client');
|
|
132
151
|
|
|
133
152
|
// Load user profile after setting tokens - ONLY ONCE per session
|
|
134
153
|
if (!profileLoadedRef.current) {
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
154
|
+
console.log('[AdminLayout] Loading user profile (first time)...');
|
|
155
|
+
try {
|
|
156
|
+
await loadCurrentProfileRef.current('AdminLayout.onAuthTokenReceived');
|
|
157
|
+
profileLoadedRef.current = true;
|
|
158
|
+
console.log('[AdminLayout] User profile loaded successfully');
|
|
159
|
+
} catch (error) {
|
|
160
|
+
console.error('[AdminLayout] Failed to load profile:', error);
|
|
161
|
+
// Reset flags on error so user can retry
|
|
162
|
+
tokensReceivedRef.current = false;
|
|
163
|
+
profileLoadedRef.current = false;
|
|
164
|
+
}
|
|
139
165
|
} else {
|
|
140
|
-
|
|
166
|
+
console.log('[AdminLayout] Profile already loaded, skipping duplicate call');
|
|
141
167
|
}
|
|
142
168
|
|
|
143
169
|
// Call custom handler if provided
|
|
144
170
|
if (config?.onAuthTokenReceived) {
|
|
145
|
-
|
|
171
|
+
console.log('[AdminLayout] Calling custom onAuthTokenReceived handler');
|
|
146
172
|
config.onAuthTokenReceived(authToken, refreshToken);
|
|
147
173
|
}
|
|
148
|
-
}, [
|
|
174
|
+
}, []); // ← EMPTY DEPS - callback NEVER changes
|
|
149
175
|
|
|
150
176
|
// useCfgApp hook is called here to initialize iframe communication
|
|
151
177
|
// Automatically sets tokens in API client when received from parent
|
|
152
178
|
const { isEmbedded } = useCfgApp({
|
|
153
|
-
onAuthTokenReceived: handleAuthToken //
|
|
179
|
+
onAuthTokenReceived: handleAuthToken // Now truly stable - never recreated
|
|
154
180
|
});
|
|
155
181
|
|
|
156
182
|
// console.log('[AdminLayout] isEmbedded:', isEmbedded);
|
|
@@ -4,6 +4,7 @@
|
|
|
4
4
|
|
|
5
5
|
'use client';
|
|
6
6
|
|
|
7
|
+
import React from 'react';
|
|
7
8
|
import { useState, useEffect } from 'react';
|
|
8
9
|
import { useRouter } from 'next/router';
|
|
9
10
|
import { authLogger } from '../../../../../utils/logger';
|
|
@@ -94,6 +95,14 @@ export function useCfgApp(options?: UseCfgAppOptions): UseCfgAppReturn {
|
|
|
94
95
|
const [parentTheme, setParentTheme] = useState<'light' | 'dark' | null>(null);
|
|
95
96
|
const [parentThemeMode, setParentThemeMode] = useState<string | null>(null);
|
|
96
97
|
|
|
98
|
+
// Store callback in ref OUTSIDE useEffect so it's always available
|
|
99
|
+
const callbackRef = React.useRef(options?.onAuthTokenReceived);
|
|
100
|
+
|
|
101
|
+
// Update callback ref when options change (but don't re-register listener)
|
|
102
|
+
React.useEffect(() => {
|
|
103
|
+
callbackRef.current = options?.onAuthTokenReceived;
|
|
104
|
+
}, [options?.onAuthTokenReceived]);
|
|
105
|
+
|
|
97
106
|
useEffect(() => {
|
|
98
107
|
setIsMounted(true);
|
|
99
108
|
|
|
@@ -112,6 +121,8 @@ export function useCfgApp(options?: UseCfgAppOptions): UseCfgAppReturn {
|
|
|
112
121
|
|
|
113
122
|
// Debounce timeout for parent-auth messages
|
|
114
123
|
let authTokenTimeout: NodeJS.Timeout | null = null;
|
|
124
|
+
// Track if we've already processed auth tokens
|
|
125
|
+
let authTokenProcessed = false;
|
|
115
126
|
|
|
116
127
|
// Listen for messages from parent window
|
|
117
128
|
const handleMessage = (event: MessageEvent) => {
|
|
@@ -127,7 +138,13 @@ export function useCfgApp(options?: UseCfgAppOptions): UseCfgAppReturn {
|
|
|
127
138
|
|
|
128
139
|
switch (type) {
|
|
129
140
|
case 'parent-auth':
|
|
130
|
-
|
|
141
|
+
console.log('[useCfgApp] parent-auth message received, authTokenProcessed:', authTokenProcessed);
|
|
142
|
+
|
|
143
|
+
// Prevent processing if already handled
|
|
144
|
+
if (authTokenProcessed) {
|
|
145
|
+
console.log('[useCfgApp] Auth tokens already processed, ignoring duplicate message');
|
|
146
|
+
return;
|
|
147
|
+
}
|
|
131
148
|
|
|
132
149
|
// Cancel previous timeout to debounce rapid auth messages
|
|
133
150
|
if (authTokenTimeout) {
|
|
@@ -136,18 +153,30 @@ export function useCfgApp(options?: UseCfgAppOptions): UseCfgAppReturn {
|
|
|
136
153
|
|
|
137
154
|
// Debounce auth token processing to prevent rapid calls (300ms)
|
|
138
155
|
authTokenTimeout = setTimeout(() => {
|
|
156
|
+
// Double-check still not processed (race condition protection)
|
|
157
|
+
if (authTokenProcessed) {
|
|
158
|
+
console.log('[useCfgApp] Auth tokens already processed during debounce, skipping');
|
|
159
|
+
return;
|
|
160
|
+
}
|
|
161
|
+
|
|
139
162
|
// Receive authentication tokens from parent
|
|
140
|
-
if (data?.authToken &&
|
|
141
|
-
|
|
142
|
-
|
|
163
|
+
if (data?.authToken && callbackRef.current) {
|
|
164
|
+
console.log('[useCfgApp] Auth tokens found, calling onAuthTokenReceived callback');
|
|
165
|
+
console.log('[useCfgApp] authToken:', data.authToken.substring(0, 20) + '...', 'refreshToken:', data.refreshToken ? data.refreshToken.substring(0, 20) + '...' : 'null');
|
|
166
|
+
|
|
167
|
+
// Mark as processed BEFORE calling callback
|
|
168
|
+
authTokenProcessed = true;
|
|
169
|
+
|
|
143
170
|
try {
|
|
144
|
-
|
|
145
|
-
|
|
171
|
+
callbackRef.current(data.authToken, data.refreshToken);
|
|
172
|
+
console.log('[useCfgApp] onAuthTokenReceived callback completed successfully');
|
|
146
173
|
} catch (e) {
|
|
147
174
|
authLogger.error('Failed to process auth tokens:', e);
|
|
175
|
+
// Reset on error to allow retry
|
|
176
|
+
authTokenProcessed = false;
|
|
148
177
|
}
|
|
149
178
|
} else {
|
|
150
|
-
authLogger.warn('parent-auth message received but authToken or callback missing:', { hasToken: !!data?.authToken, hasCallback: !!
|
|
179
|
+
authLogger.warn('parent-auth message received but authToken or callback missing:', { hasToken: !!data?.authToken, hasCallback: !!callbackRef.current });
|
|
151
180
|
}
|
|
152
181
|
}, 300); // 300ms debounce
|
|
153
182
|
break;
|
|
@@ -202,7 +231,7 @@ export function useCfgApp(options?: UseCfgAppOptions): UseCfgAppReturn {
|
|
|
202
231
|
clearTimeout(authTokenTimeout);
|
|
203
232
|
}
|
|
204
233
|
};
|
|
205
|
-
}, [
|
|
234
|
+
}, []); // ← EMPTY DEPS - register listener ONCE
|
|
206
235
|
|
|
207
236
|
// Notify parent about route changes
|
|
208
237
|
useEffect(() => {
|
|
@@ -9,11 +9,18 @@
|
|
|
9
9
|
import React, { ReactNode } from 'react';
|
|
10
10
|
import { ThemeProvider, Toaster } from '@djangocfg/ui';
|
|
11
11
|
import { AuthProvider } from '../../../auth';
|
|
12
|
+
import { ValidationErrorProvider } from '../../../validation';
|
|
12
13
|
import type { AppLayoutConfig } from '../types';
|
|
14
|
+
import type { ValidationErrorConfig } from '../../../validation';
|
|
13
15
|
|
|
14
16
|
export interface CoreProvidersProps {
|
|
15
17
|
children: ReactNode;
|
|
16
18
|
config: AppLayoutConfig;
|
|
19
|
+
/**
|
|
20
|
+
* Configuration for validation error tracking
|
|
21
|
+
* @default { enableToast: true, maxErrors: 50 }
|
|
22
|
+
*/
|
|
23
|
+
validationConfig?: Partial<ValidationErrorConfig>;
|
|
17
24
|
}
|
|
18
25
|
|
|
19
26
|
/**
|
|
@@ -22,9 +29,10 @@ export interface CoreProvidersProps {
|
|
|
22
29
|
* Provides:
|
|
23
30
|
* - ThemeProvider (dark/light mode)
|
|
24
31
|
* - AuthProvider (authentication)
|
|
32
|
+
* - ValidationErrorProvider (Zod validation error tracking)
|
|
25
33
|
* - Toaster (notifications)
|
|
26
34
|
*/
|
|
27
|
-
export function CoreProviders({ children, config }: CoreProvidersProps) {
|
|
35
|
+
export function CoreProviders({ children, config, validationConfig }: CoreProvidersProps) {
|
|
28
36
|
return (
|
|
29
37
|
<ThemeProvider>
|
|
30
38
|
<AuthProvider
|
|
@@ -37,7 +45,9 @@ export function CoreProviders({ children, config }: CoreProvidersProps) {
|
|
|
37
45
|
},
|
|
38
46
|
}}
|
|
39
47
|
>
|
|
40
|
-
{
|
|
48
|
+
<ValidationErrorProvider config={validationConfig}>
|
|
49
|
+
{children}
|
|
50
|
+
</ValidationErrorProvider>
|
|
41
51
|
</AuthProvider>
|
|
42
52
|
|
|
43
53
|
{/* Global toast notifications */}
|
|
@@ -7,6 +7,7 @@
|
|
|
7
7
|
|
|
8
8
|
import React from 'react';
|
|
9
9
|
import { CopyAIButton } from './CopyAIButton';
|
|
10
|
+
import { TestValidationButton } from './TestValidationButton';
|
|
10
11
|
|
|
11
12
|
export interface HeaderProps {
|
|
12
13
|
/** Page title */
|
|
@@ -42,10 +43,16 @@ export function Header({
|
|
|
42
43
|
</div>
|
|
43
44
|
</div>
|
|
44
45
|
|
|
45
|
-
{/* Right:
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
46
|
+
{/* Right: Developer Tools */}
|
|
47
|
+
<div className="flex items-center gap-2">
|
|
48
|
+
{/* Test Validation Error Button (dev only) */}
|
|
49
|
+
<TestValidationButton size="sm" />
|
|
50
|
+
|
|
51
|
+
{/* Copy for AI Button */}
|
|
52
|
+
{onCopyForAI && (
|
|
53
|
+
<CopyAIButton onCopyForAI={onCopyForAI} />
|
|
54
|
+
)}
|
|
55
|
+
</div>
|
|
49
56
|
</div>
|
|
50
57
|
</div>
|
|
51
58
|
</header>
|
|
@@ -7,6 +7,7 @@
|
|
|
7
7
|
|
|
8
8
|
import React from 'react';
|
|
9
9
|
import { CopyAIButton } from './CopyAIButton';
|
|
10
|
+
import { TestValidationButton } from './TestValidationButton';
|
|
10
11
|
|
|
11
12
|
interface HeaderDesktopProps {
|
|
12
13
|
/** Page title */
|
|
@@ -31,13 +32,19 @@ export function HeaderDesktop({
|
|
|
31
32
|
<h1 className="text-lg font-semibold">{title}</h1>
|
|
32
33
|
</div>
|
|
33
34
|
|
|
34
|
-
{/*
|
|
35
|
-
<
|
|
36
|
-
|
|
37
|
-
size="sm"
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
35
|
+
{/* Developer Tools */}
|
|
36
|
+
<div className="flex items-center gap-2">
|
|
37
|
+
{/* Test Validation Error Button (dev only) */}
|
|
38
|
+
<TestValidationButton size="sm" />
|
|
39
|
+
|
|
40
|
+
{/* Copy for AI Button */}
|
|
41
|
+
<CopyAIButton
|
|
42
|
+
onCopyForAI={onCopyForAI}
|
|
43
|
+
size="sm"
|
|
44
|
+
showLabel={true}
|
|
45
|
+
className="gap-2"
|
|
46
|
+
/>
|
|
47
|
+
</div>
|
|
41
48
|
</div>
|
|
42
49
|
</div>
|
|
43
50
|
);
|