@djangocfg/layouts 1.2.33 → 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 CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@djangocfg/layouts",
3
- "version": "1.2.33",
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.33",
67
- "@djangocfg/og-image": "^1.2.33",
68
- "@djangocfg/ui": "^1.2.33",
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.33",
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",
@@ -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-10T16:46:04.571Z
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.33"
24
+ "version": "1.2.34"
25
25
  },
26
26
  {
27
27
  "name": "@djangocfg/api",
28
- "version": "1.2.33"
28
+ "version": "1.2.34"
29
29
  },
30
30
  {
31
31
  "name": "@djangocfg/layouts",
32
- "version": "1.2.33"
32
+ "version": "1.2.34"
33
33
  },
34
34
  {
35
35
  "name": "@djangocfg/markdown",
36
- "version": "1.2.33"
36
+ "version": "1.2.34"
37
37
  },
38
38
  {
39
39
  "name": "@djangocfg/og-image",
40
- "version": "1.2.33"
40
+ "version": "1.2.34"
41
41
  },
42
42
  {
43
43
  "name": "@djangocfg/eslint-config",
44
- "version": "1.2.33"
44
+ "version": "1.2.34"
45
45
  },
46
46
  {
47
47
  "name": "@djangocfg/typescript-config",
48
- "version": "1.2.33"
48
+ "version": "1.2.34"
49
49
  }
50
50
  ];
51
51
 
@@ -8,7 +8,8 @@ Automatic Zod validation error tracking with toast notifications and copy-to-cli
8
8
 
9
9
  ✅ **Auto-capture validation errors** from API client via CustomEvent
10
10
  ✅ **Toast notifications** with destructive styling
11
- ✅ **Copy button** - One-click copy full error details to clipboard
11
+ ✅ **Copy Error button** - One-click copy full error details JSON to clipboard
12
+ ✅ **Copy cURL button** - Generate and copy cURL command with auth token
12
13
  ✅ **Error history** - Store and manage validation errors
13
14
  ✅ **Configurable** - Customize display and behavior
14
15
  ✅ **Type-safe** - Full TypeScript support
@@ -69,7 +70,9 @@ window.dispatchEvent(new CustomEvent('zod-validation-error', {
69
70
  You'll see a toast with:
70
71
  - **Title**: "❌ Validation Error in operation_name"
71
72
  - **Description**: Endpoint, error count, first 3 errors
72
- - **Copy Button**: "📋 Copy" - Copies full error JSON to clipboard
73
+ - **Two Buttons** (displayed at bottom):
74
+ - **📋 Copy Error** - Copies full error JSON to clipboard
75
+ - **🔄 Copy cURL** - Generates and copies cURL command with auth token
73
76
 
74
77
  ---
75
78
 
@@ -490,8 +493,91 @@ validation/
490
493
 
491
494
  ---
492
495
 
496
+ ## Copy as cURL Feature
497
+
498
+ ### What Gets Copied
499
+
500
+ When you click **🔄 Copy cURL**, a ready-to-use cURL command is generated:
501
+
502
+ ```bash
503
+ curl 'http://localhost:8000/api/proxies/proxies/?page=1&page_size=100' \
504
+ -H 'Accept: */*' \
505
+ -H 'Content-Type: application/json' \
506
+ -H 'Authorization: Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9...'
507
+ ```
508
+
509
+ The generator automatically:
510
+ - ✅ Adds your auth token from localStorage
511
+ - ✅ Formats headers properly
512
+ - ✅ Uses correct HTTP method
513
+ - ✅ Escapes special characters
514
+
515
+ ### Token Auto-Detection
516
+
517
+ The cURL generator looks for your token in localStorage:
518
+ - `access_token` (default)
519
+ - `token`
520
+ - `auth_token`
521
+
522
+ ### Manual cURL Generation
523
+
524
+ You can also generate cURL commands programmatically:
525
+
526
+ ```typescript
527
+ import { generateCurl, copyCurlToClipboard } from '@djangocfg/layouts/validation';
528
+
529
+ // Generate cURL
530
+ const curl = generateCurl({
531
+ method: 'POST',
532
+ path: '/api/users/',
533
+ token: 'your-token',
534
+ body: { name: 'John' },
535
+ headers: { 'X-Custom': 'value' },
536
+ });
537
+
538
+ // Copy to clipboard
539
+ await copyCurlToClipboard(curl);
540
+ ```
541
+
542
+ ### API Reference - cURL
543
+
544
+ #### `generateCurl(options: CurlOptions): string`
545
+
546
+ **Options:**
547
+ ```typescript
548
+ interface CurlOptions {
549
+ method: string; // HTTP method
550
+ path: string; // API path
551
+ token?: string; // Auth token (auto-fetched if omitted)
552
+ body?: any; // Request body
553
+ headers?: Record<string, string>; // Custom headers
554
+ baseUrl?: string; // API base URL (from env by default)
555
+ }
556
+ ```
557
+
558
+ #### `generateCurlFromError(detail): string`
559
+
560
+ Generate cURL from validation error details. Auto-fetches token from localStorage.
561
+
562
+ #### `getAuthToken(): string | null`
563
+
564
+ Get authentication token from localStorage.
565
+
566
+ #### `copyCurlToClipboard(curl: string): Promise<boolean>`
567
+
568
+ Copy cURL command to clipboard. Returns `true` on success.
569
+
570
+ ---
571
+
493
572
  ## Changelog
494
573
 
574
+ ### 2025-11-11 - v2.1.0
575
+ - ✨ Added **Copy cURL button** with automatic token injection
576
+ - ✨ Added `curl-generator.ts` utilities
577
+ - ✨ Added two buttons layout at bottom of toast
578
+ - ✨ Auto-detect auth token from localStorage
579
+ - 📝 Updated documentation with cURL examples
580
+
495
581
  ### 2025-11-11 - v2.0.0
496
582
  - ✨ Added copy-to-clipboard button in toast
497
583
  - ✨ Added `ValidationErrorToast` utilities
@@ -0,0 +1,162 @@
1
+ # Validation Error System - Refactoring Summary
2
+
3
+ ## Changes Made
4
+
5
+ ### ✅ Added: Copy as cURL Feature
6
+
7
+ **New Files:**
8
+ - `curl-generator.ts` - cURL command generation
9
+ - `ValidationErrorButtons.tsx` - Button component using `useCopy` hook
10
+
11
+ **Key Improvements:**
12
+ 1. **Auto token injection** - Token automatically fetched from localStorage
13
+ 2. **useCopy integration** - Uses existing `useCopy` hook instead of custom clipboard logic
14
+ 3. **Two buttons layout** - Both buttons at bottom of toast (not on the side)
15
+ 4. **Clean separation** - Buttons in separate component, generator in separate file
16
+
17
+ ### 🗑️ Removed: Legacy Code
18
+
19
+ **Removed Functions:**
20
+ - `copyToClipboard()` from ValidationErrorToast.tsx (replaced by `useCopy`)
21
+ - `copyCurlToClipboard()` from curl-generator.ts (replaced by `useCopy`)
22
+ - `createCopyAction()` (replaced by ValidationErrorButtons)
23
+ - `createCopyErrorAction()` (replaced by ValidationErrorButtons)
24
+ - `createCopyCurlAction()` (replaced by ValidationErrorButtons)
25
+
26
+ ### ♻️ Refactored: Cleaner API
27
+
28
+ **Before:**
29
+ ```typescript
30
+ // Multiple separate functions
31
+ createCopyAction(detail, onSuccess, onError);
32
+ createCopyErrorAction(detail, onSuccess, onError);
33
+ createCopyCurlAction(detail, onSuccess, onError);
34
+ ```
35
+
36
+ **After:**
37
+ ```typescript
38
+ // Single component with useCopy
39
+ <ValidationErrorButtons detail={detail} />
40
+ ```
41
+
42
+ ### 📦 File Structure
43
+
44
+ ```
45
+ validation/
46
+ ├── curl-generator.ts # cURL generation logic only
47
+ ├── ValidationErrorButtons.tsx # Button component (uses useCopy)
48
+ ├── ValidationErrorToast.tsx # Toast utilities (no copy logic)
49
+ ├── ValidationErrorContext.tsx # Provider & hook
50
+ ├── index.ts # Public exports
51
+ └── README.md # Documentation
52
+ ```
53
+
54
+ ## Benefits
55
+
56
+ 1. **No code duplication** - Uses existing `useCopy` instead of custom clipboard code
57
+ 2. **Consistent UX** - All copy operations show the same toast feedback
58
+ 3. **Cleaner separation** - Each file has single responsibility
59
+ 4. **Easier to maintain** - Less code, clearer structure
60
+ 5. **Better DX** - Simple `<ValidationErrorButtons />` instead of 3 separate functions
61
+
62
+ ## API Changes
63
+
64
+ ### Removed Exports
65
+ ```typescript
66
+ // ❌ No longer exported
67
+ createCopyAction
68
+ createCopyErrorAction
69
+ createCopyCurlAction
70
+ copyCurlToClipboard
71
+ ```
72
+
73
+ ### New Exports
74
+ ```typescript
75
+ // ✅ New exports
76
+ ValidationErrorButtons // Component
77
+ generateCurl // Generate cURL string
78
+ generateCurlFromError // Generate from error detail
79
+ getAuthToken // Get token from localStorage
80
+ ```
81
+
82
+ ### Unchanged Exports
83
+ ```typescript
84
+ // ✅ Still available
85
+ ValidationErrorProvider
86
+ useValidationErrors
87
+ createValidationErrorToast
88
+ formatErrorForClipboard
89
+ buildToastTitle
90
+ buildToastDescription
91
+ ```
92
+
93
+ ## Migration Guide
94
+
95
+ If you were using the old `createCopyAction`:
96
+
97
+ **Before:**
98
+ ```typescript
99
+ const toastOptions = createValidationErrorToast(errorDetail, {
100
+ onCopySuccess: () => console.log('Copied'),
101
+ onCopyError: (e) => console.error(e),
102
+ });
103
+ ```
104
+
105
+ **After:**
106
+ ```typescript
107
+ // Just use it - ValidationErrorButtons handles everything
108
+ const toastOptions = createValidationErrorToast(errorDetail);
109
+ // That's it! Toast feedback is automatic via useCopy
110
+ ```
111
+
112
+ ## Implementation Details
113
+
114
+ ### ValidationErrorButtons Component
115
+
116
+ Uses `useCopy` hook from `@djangocfg/ui`:
117
+
118
+ ```typescript
119
+ const { copyToClipboard } = useCopy();
120
+
121
+ const handleCopyError = async (e) => {
122
+ const json = JSON.stringify(errorData, null, 2);
123
+ await copyToClipboard(json, '✅ Error details copied');
124
+ };
125
+
126
+ const handleCopyCurl = async (e) => {
127
+ const curl = generateCurlFromError(detail);
128
+ await copyToClipboard(curl, '✅ cURL command copied');
129
+ };
130
+ ```
131
+
132
+ ### Auto Token Injection
133
+
134
+ ```typescript
135
+ export function generateCurl(options: CurlOptions): string {
136
+ const { token = getAuthToken() || undefined } = options;
137
+ // ...
138
+ }
139
+ ```
140
+
141
+ Token is auto-fetched if not provided.
142
+
143
+ ## Testing
144
+
145
+ 1. Trigger validation error (e.g., visit `/api/proxies/proxies/`)
146
+ 2. Toast appears with two buttons at bottom
147
+ 3. Click **📋 Copy Error** → Success toast appears → JSON in clipboard
148
+ 4. Click **🔄 Copy cURL** → Success toast appears → cURL in clipboard
149
+ 5. Paste cURL in terminal → Works with your auth token!
150
+
151
+ ## What's Next
152
+
153
+ Possible future enhancements:
154
+ - [ ] Support POST/PUT body in cURL
155
+ - [ ] Option to copy without token
156
+ - [ ] Different formats (fetch, axios)
157
+ - [ ] Copy request+response together
158
+
159
+ ---
160
+
161
+ **Date:** 2025-11-11
162
+ **Impact:** Breaking changes for anyone using internal copy functions (unlikely)
@@ -0,0 +1,80 @@
1
+ /**
2
+ * ValidationErrorButtons Component
3
+ *
4
+ * Copy buttons for validation errors using useCopy hook
5
+ */
6
+
7
+ 'use client';
8
+
9
+ import React from 'react';
10
+ import { Button, useCopy } from '@djangocfg/ui';
11
+ import { generateCurlFromError } from './curl-generator';
12
+ import type { ValidationErrorDetail } from './ValidationErrorToast';
13
+
14
+ export interface ValidationErrorButtonsProps {
15
+ detail: ValidationErrorDetail;
16
+ }
17
+
18
+ export function ValidationErrorButtons({ detail }: ValidationErrorButtonsProps) {
19
+ const { copyToClipboard } = useCopy();
20
+
21
+ const handleCopyError = async (e: React.MouseEvent) => {
22
+ e.preventDefault();
23
+ e.stopPropagation();
24
+
25
+ const errorData = {
26
+ timestamp: detail.timestamp.toISOString(),
27
+ operation: detail.operation,
28
+ endpoint: {
29
+ method: detail.method,
30
+ path: detail.path,
31
+ },
32
+ validation_errors: detail.error.issues.map((issue) => ({
33
+ path: issue.path.join('.') || 'root',
34
+ message: issue.message,
35
+ code: issue.code,
36
+ ...(('expected' in issue) && { expected: issue.expected }),
37
+ ...(('received' in issue) && { received: issue.received }),
38
+ })),
39
+ response: detail.response,
40
+ total_errors: detail.error.issues.length,
41
+ };
42
+
43
+ const formattedError = JSON.stringify(errorData, null, 2);
44
+ await copyToClipboard(formattedError, '✅ Error details copied');
45
+ };
46
+
47
+ const handleCopyCurl = async (e: React.MouseEvent) => {
48
+ e.preventDefault();
49
+ e.stopPropagation();
50
+
51
+ const curl = generateCurlFromError({
52
+ method: detail.method,
53
+ path: detail.path,
54
+ response: detail.response,
55
+ });
56
+
57
+ await copyToClipboard(curl, '✅ cURL command copied');
58
+ };
59
+
60
+ return (
61
+ <div className="flex gap-2 mt-2">
62
+ <Button
63
+ size="sm"
64
+ variant="outline"
65
+ onClick={handleCopyError}
66
+ className="h-8 text-xs"
67
+ >
68
+ 📋 Copy Error
69
+ </Button>
70
+ <Button
71
+ size="sm"
72
+ variant="outline"
73
+ onClick={handleCopyCurl}
74
+ className="h-8 text-xs"
75
+ >
76
+ 🔄 Copy cURL
77
+ </Button>
78
+ </div>
79
+ );
80
+ }
@@ -10,6 +10,7 @@
10
10
  import React from 'react';
11
11
  import { ToastAction } from '@djangocfg/ui';
12
12
  import type { ZodError } from 'zod';
13
+ import { ValidationErrorButtons } from './ValidationErrorButtons';
13
14
 
14
15
  export interface ValidationErrorDetail {
15
16
  operation: string;
@@ -89,34 +90,6 @@ export function formatErrorForClipboard(detail: ValidationErrorDetail): string {
89
90
  return JSON.stringify(errorData, null, 2);
90
91
  }
91
92
 
92
- /**
93
- * Copy text to clipboard
94
- */
95
- async function copyToClipboard(text: string): Promise<boolean> {
96
- if (typeof window === 'undefined') return false;
97
-
98
- try {
99
- if (navigator.clipboard && navigator.clipboard.writeText) {
100
- await navigator.clipboard.writeText(text);
101
- return true;
102
- } else {
103
- // Fallback for older browsers
104
- const textarea = document.createElement('textarea');
105
- textarea.value = text;
106
- textarea.style.position = 'fixed';
107
- textarea.style.opacity = '0';
108
- document.body.appendChild(textarea);
109
- textarea.select();
110
- const success = document.execCommand('copy');
111
- document.body.removeChild(textarea);
112
- return success;
113
- }
114
- } catch (error) {
115
- console.error('Failed to copy to clipboard:', error);
116
- return false;
117
- }
118
- }
119
-
120
93
  /**
121
94
  * Build toast title from validation error
122
95
  */
@@ -165,49 +138,6 @@ export function buildToastDescription(
165
138
  return descriptionParts.join(' • ');
166
139
  }
167
140
 
168
- /**
169
- * Create copy action button for toast
170
- *
171
- * @param detail - Validation error details
172
- * @param onCopySuccess - Optional callback when copy succeeds
173
- * @param onCopyError - Optional callback when copy fails
174
- */
175
- export function createCopyAction(
176
- detail: ValidationErrorDetail,
177
- onCopySuccess?: () => void,
178
- onCopyError?: (error: Error) => void
179
- ): React.ReactElement<typeof ToastAction> {
180
- const handleCopy = async (e: React.MouseEvent) => {
181
- e.preventDefault();
182
- e.stopPropagation();
183
-
184
- try {
185
- const formattedError = formatErrorForClipboard(detail);
186
- const success = await copyToClipboard(formattedError);
187
-
188
- if (success) {
189
- console.log('✅ Validation error copied to clipboard');
190
- onCopySuccess?.();
191
- } else {
192
- throw new Error('Clipboard API failed');
193
- }
194
- } catch (error) {
195
- console.error('❌ Failed to copy validation error:', error);
196
- onCopyError?.(error as Error);
197
- }
198
- };
199
-
200
- return (
201
- <ToastAction
202
- altText="Copy error details"
203
- onClick={handleCopy}
204
- className="shrink-0"
205
- >
206
- 📋 Copy
207
- </ToastAction>
208
- );
209
- }
210
-
211
141
  /**
212
142
  * Create complete toast options for validation error
213
143
  *
@@ -239,13 +169,13 @@ export function createValidationErrorToast(
239
169
 
240
170
  return {
241
171
  title: buildToastTitle(detail, config),
242
- description: buildToastDescription(detail, config),
172
+ description: (
173
+ <div className="flex flex-col gap-2">
174
+ <div>{buildToastDescription(detail, config)}</div>
175
+ <ValidationErrorButtons detail={detail} />
176
+ </div>
177
+ ),
243
178
  variant: 'destructive' as const,
244
179
  duration: options?.duration,
245
- action: createCopyAction(
246
- detail,
247
- options?.onCopySuccess,
248
- options?.onCopyError
249
- ),
250
180
  };
251
181
  }
@@ -0,0 +1,118 @@
1
+ /**
2
+ * cURL Generator
3
+ *
4
+ * Generates cURL commands from API request details with authentication token
5
+ */
6
+
7
+ export interface CurlOptions {
8
+ method: string;
9
+ path: string;
10
+ token?: string;
11
+ body?: any;
12
+ headers?: Record<string, string>;
13
+ baseUrl?: string;
14
+ queryParams?: Record<string, string>;
15
+ }
16
+
17
+ /**
18
+ * Get authentication token from localStorage
19
+ */
20
+ export function getAuthToken(): string | null {
21
+ if (typeof window === 'undefined') return null;
22
+
23
+ try {
24
+ // Priority order: access_token > token > auth_token
25
+ const token = localStorage.getItem('access_token') ||
26
+ localStorage.getItem('token') ||
27
+ localStorage.getItem('auth_token');
28
+
29
+ return token;
30
+ } catch (error) {
31
+ console.error('Failed to get auth token:', error);
32
+ return null;
33
+ }
34
+ }
35
+
36
+ /**
37
+ * Format headers for cURL command
38
+ */
39
+ function formatHeaders(headers: Record<string, string>): string[] {
40
+ return Object.entries(headers).map(
41
+ ([key, value]) => `-H '${key}: ${value}'`
42
+ );
43
+ }
44
+
45
+ /**
46
+ * Escape single quotes in string for shell
47
+ */
48
+ function escapeShell(str: string): string {
49
+ return str.replace(/'/g, "'\\''");
50
+ }
51
+
52
+ /**
53
+ * Generate cURL command from request details
54
+ */
55
+ export function generateCurl(options: CurlOptions): string {
56
+ const {
57
+ method,
58
+ path,
59
+ token = getAuthToken() || undefined, // Auto-fetch if not provided
60
+ body,
61
+ headers = {},
62
+ baseUrl = process.env.NEXT_PUBLIC_API_URL || 'http://localhost:8000',
63
+ } = options;
64
+
65
+ const curlParts: string[] = ['curl'];
66
+
67
+ // Build URL
68
+ const url = `${baseUrl}${path}`;
69
+ curlParts.push(`'${url}'`);
70
+
71
+ // Add method if not GET
72
+ if (method.toUpperCase() !== 'GET') {
73
+ curlParts.push(`-X ${method.toUpperCase()}`);
74
+ }
75
+
76
+ // Default headers
77
+ const allHeaders: Record<string, string> = {
78
+ 'Accept': '*/*',
79
+ 'Content-Type': 'application/json',
80
+ ...headers,
81
+ };
82
+
83
+ // Add Authorization header if token exists
84
+ if (token) {
85
+ allHeaders['Authorization'] = `Bearer ${token}`;
86
+ }
87
+
88
+ // Add all headers
89
+ const headerStrings = formatHeaders(allHeaders);
90
+ curlParts.push(...headerStrings);
91
+
92
+ // Add body for non-GET requests
93
+ if (body && method.toUpperCase() !== 'GET') {
94
+ const bodyJson = typeof body === 'string'
95
+ ? body
96
+ : JSON.stringify(body, null, 2);
97
+ curlParts.push(`-d '${escapeShell(bodyJson)}'`);
98
+ }
99
+
100
+ // Join with line continuation
101
+ return curlParts.join(' \\\n ');
102
+ }
103
+
104
+ /**
105
+ * Generate cURL from validation error details
106
+ * Auto-fetches token from localStorage
107
+ */
108
+ export function generateCurlFromError(detail: {
109
+ method: string;
110
+ path: string;
111
+ response?: any;
112
+ }): string {
113
+ return generateCurl({
114
+ method: detail.method,
115
+ path: detail.path,
116
+ // token is auto-fetched in generateCurl
117
+ });
118
+ }
@@ -16,10 +16,21 @@ export {
16
16
 
17
17
  export {
18
18
  createValidationErrorToast,
19
- createCopyAction,
20
19
  buildToastTitle,
21
20
  buildToastDescription,
22
21
  formatZodIssuesForToast,
23
22
  formatErrorForClipboard,
24
23
  type ValidationErrorToastConfig,
25
24
  } from './ValidationErrorToast';
25
+
26
+ export {
27
+ ValidationErrorButtons,
28
+ type ValidationErrorButtonsProps,
29
+ } from './ValidationErrorButtons';
30
+
31
+ export {
32
+ generateCurl,
33
+ generateCurlFromError,
34
+ getAuthToken,
35
+ type CurlOptions,
36
+ } from './curl-generator';