@djangocfg/ui-tools 2.1.201 → 2.1.202

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/ui-tools",
3
- "version": "2.1.201",
3
+ "version": "2.1.202",
4
4
  "description": "Heavy React tools with lazy loading - for Electron, Vite, CRA, Next.js apps",
5
5
  "keywords": [
6
6
  "ui-tools",
@@ -78,8 +78,8 @@
78
78
  "check": "tsc --noEmit"
79
79
  },
80
80
  "peerDependencies": {
81
- "@djangocfg/i18n": "^2.1.201",
82
- "@djangocfg/ui-core": "^2.1.201",
81
+ "@djangocfg/i18n": "^2.1.202",
82
+ "@djangocfg/ui-core": "^2.1.202",
83
83
  "lucide-react": "^0.545.0",
84
84
  "react": "^19.0.0",
85
85
  "react-dom": "^19.0.0",
@@ -112,10 +112,10 @@
112
112
  "@maplibre/maplibre-gl-geocoder": "^1.7.0"
113
113
  },
114
114
  "devDependencies": {
115
- "@djangocfg/i18n": "^2.1.201",
115
+ "@djangocfg/i18n": "^2.1.202",
116
116
  "@djangocfg/playground": "workspace:*",
117
- "@djangocfg/typescript-config": "^2.1.201",
118
- "@djangocfg/ui-core": "^2.1.201",
117
+ "@djangocfg/typescript-config": "^2.1.202",
118
+ "@djangocfg/ui-core": "^2.1.202",
119
119
  "@types/mapbox__mapbox-gl-draw": "^1.4.8",
120
120
  "@types/node": "^24.7.2",
121
121
  "@types/react": "^19.1.0",
@@ -49,7 +49,32 @@ All-in-one component with dropzone and preview list.
49
49
  />
50
50
  ```
51
51
 
52
- ### Custom Composition
52
+ ### Standalone — custom upload handler (no UploadProvider)
53
+
54
+ If you handle uploads yourself (custom API hooks, multipart POST, etc.), pass `uploadFn` instead of wrapping with `UploadProvider`. Supports drag/drop, click, and Ctrl+V paste.
55
+
56
+ ```tsx
57
+ import { UploadDropzone } from '@djangocfg/ui-tools/upload';
58
+
59
+ function MyUploader() {
60
+ const { uploadAsset } = useAssets(); // your own API hook
61
+
62
+ return (
63
+ <UploadDropzone
64
+ accept={['image', 'video']}
65
+ maxSizeMB={50}
66
+ pasteEnabled
67
+ uploadFn={async (files) => {
68
+ for (const file of files) {
69
+ await uploadAsset({ file, name: file.name });
70
+ }
71
+ }}
72
+ />
73
+ );
74
+ }
75
+ ```
76
+
77
+ ### Custom Composition (with rpldy)
53
78
 
54
79
  ```tsx
55
80
  import {
@@ -116,11 +141,11 @@ Custom overlay:
116
141
 
117
142
  ### Components
118
143
 
119
- | Component | Description |
120
- |-----------|-------------|
121
- | `Uploader` | All-in-one (Provider + Dropzone + Preview) |
122
- | `UploadProvider` | Context provider wrapping @rpldy/uploady |
123
- | `UploadDropzone` | Drag-drop zone with file input |
144
+ | Component | Needs UploadProvider | Description |
145
+ |-----------|---------------------|-------------|
146
+ | `Uploader` | Yes | All-in-one (Provider + Dropzone + Preview) |
147
+ | `UploadDropzone` | **Optional** | Drag-drop zone — use `uploadFn` for standalone mode |
148
+ | `UploadProvider` | | Context provider wrapping @rpldy/uploady |
124
149
  | `UploadPreviewList` | List of upload items with progress |
125
150
  | `UploadPreviewItem` | Single item (thumbnail, status, actions) |
126
151
  | `UploadAddButton` | Button to add files |
@@ -369,3 +369,47 @@ export const PageDropCustomOverlay = () => (
369
369
  </UploadProvider>
370
370
  </div>
371
371
  );
372
+
373
+ // Standalone — uploadFn instead of UploadProvider (custom API hooks, no rpldy)
374
+ export const StandaloneWithUploadFn = () => {
375
+ const [files, setFiles] = useState<string[]>([]);
376
+
377
+ return (
378
+ <div className="max-w-2xl space-y-4">
379
+ <Card>
380
+ <CardContent className="pt-4">
381
+ <p className="text-sm text-muted-foreground flex items-center gap-2">
382
+ <ClipboardPaste className="h-4 w-4" />
383
+ No <code className="text-xs bg-muted px-1 rounded">UploadProvider</code> needed.
384
+ Pass <code className="text-xs bg-muted px-1 rounded">uploadFn</code> to handle files yourself.
385
+ Drag, click, or paste (Ctrl+V).
386
+ </p>
387
+ </CardContent>
388
+ </Card>
389
+ <UploadDropzone
390
+ accept={['image', 'document']}
391
+ maxSizeMB={10}
392
+ pasteEnabled
393
+ uploadFn={(selected) => {
394
+ setFiles((prev) => [...prev, ...selected.map((f) => f.name)]);
395
+ logger.info('Custom uploadFn received: ' + selected.map((f) => f.name).join(', '));
396
+ }}
397
+ onPasteNoMatch={() => logger.info('Paste: no uploadable content found')}
398
+ />
399
+ {files.length > 0 && (
400
+ <Card>
401
+ <CardHeader>
402
+ <CardTitle className="text-sm">Received by uploadFn</CardTitle>
403
+ </CardHeader>
404
+ <CardContent>
405
+ <ul className="text-sm space-y-1">
406
+ {files.map((name, i) => (
407
+ <li key={i} className="text-muted-foreground">{name}</li>
408
+ ))}
409
+ </ul>
410
+ </CardContent>
411
+ </Card>
412
+ )}
413
+ </div>
414
+ );
415
+ };
@@ -9,6 +9,17 @@ import { buildAcceptString, logger } from '../utils';
9
9
  import { useClipboardPaste } from '../hooks/useClipboardPaste';
10
10
  import type { UploadDropzoneProps } from '../types';
11
11
 
12
+ function useOptionalUploady(uploadFn?: (files: File[]) => void) {
13
+ try {
14
+ // eslint-disable-next-line react-hooks/rules-of-hooks
15
+ const { upload } = useUploady();
16
+ return uploadFn ?? upload;
17
+ } catch {
18
+ // Not inside UploadProvider — use uploadFn if provided, otherwise noop
19
+ return uploadFn ?? (() => {});
20
+ }
21
+ }
22
+
12
23
  export function UploadDropzone({
13
24
  accept = ['image', 'audio', 'video', 'document'],
14
25
  multiple = true,
@@ -18,11 +29,12 @@ export function UploadDropzone({
18
29
  className,
19
30
  children,
20
31
  onFilesSelected,
32
+ uploadFn,
21
33
  pasteEnabled = true,
22
34
  onPasteNoMatch,
23
35
  }: UploadDropzoneProps) {
24
36
  const t = useT();
25
- const { upload } = useUploady();
37
+ const upload = useOptionalUploady(uploadFn);
26
38
  const inputRef = useRef<HTMLInputElement>(null);
27
39
  const [isDragging, setIsDragging] = useState(false);
28
40
  const dragCounter = useRef(0);
@@ -13,7 +13,7 @@ import {
13
13
  Skeleton,
14
14
  } from '@djangocfg/ui-core/components';
15
15
  import { useT } from '@djangocfg/i18n';
16
- import { formatFileSize } from '../utils';
16
+ import { formatFileSize, truncateFilename } from '../utils';
17
17
  import type { UploadPreviewItemProps, AssetType, UploadStatus } from '../types';
18
18
 
19
19
  const ASSET_ICONS: Record<AssetType, typeof FileIcon> = {
@@ -53,6 +53,7 @@ export function UploadPreviewItem({
53
53
  }, [file.type]);
54
54
 
55
55
  const fileSize = useMemo(() => formatFileSize(file.size), [file.size]);
56
+ const shortName = useMemo(() => truncateFilename(file.name), [file.name]);
56
57
  const progressPercent = Math.round(progress);
57
58
 
58
59
  // Prepare visibility flags
@@ -144,11 +145,15 @@ export function UploadPreviewItem({
144
145
  <div className="flex-1 min-w-0">
145
146
  <Tooltip>
146
147
  <TooltipTrigger asChild>
147
- <p className="text-sm font-medium truncate cursor-default">{file.name}</p>
148
+ <p className="text-sm font-medium cursor-default leading-tight">
149
+ {shortName}
150
+ </p>
148
151
  </TooltipTrigger>
149
- <TooltipContent side="top" className="max-w-xs">
150
- <p className="break-all">{file.name}</p>
151
- </TooltipContent>
152
+ {shortName !== file.name && (
153
+ <TooltipContent side="top" className="max-w-xs">
154
+ <p className="break-all">{file.name}</p>
155
+ </TooltipContent>
156
+ )}
152
157
  </Tooltip>
153
158
 
154
159
  <p className="text-xs text-muted-foreground tabular-nums">{fileSize}</p>
@@ -21,6 +21,7 @@ export {
21
21
  buildAcceptString,
22
22
  formatFileSize,
23
23
  formatDuration,
24
+ truncateFilename,
24
25
  } from './utils';
25
26
 
26
27
  // Types
@@ -85,7 +85,14 @@ export interface UploadDropzoneProps {
85
85
  disabled?: boolean;
86
86
  className?: string;
87
87
  children?: React.ReactNode;
88
+ /** Called with validated files on drop/click/paste */
88
89
  onFilesSelected?: (files: File[]) => void;
90
+ /**
91
+ * Custom upload function. If provided, rpldy upload is skipped — files go only here.
92
+ * Use this when you handle uploads yourself (e.g. custom API hooks).
93
+ * Requires no UploadProvider ancestor.
94
+ */
95
+ uploadFn?: (files: File[]) => void;
89
96
  /** Enable Ctrl+V / Cmd+V paste-to-upload (default: true) */
90
97
  pasteEnabled?: boolean;
91
98
  /** Called when paste fires but no uploadable content was found */
@@ -1,3 +1,31 @@
1
+ /**
2
+ * Middle-truncates a filename preserving the extension and a meaningful suffix.
3
+ *
4
+ * "telegram-cloud-photo-size-5-6152015652357607055-y.jpg"
5
+ * → "telegram-cloud-photo-…7055-y.jpg" (maxLength=32)
6
+ *
7
+ * Short names are returned as-is.
8
+ */
9
+ export function truncateFilename(filename: string, maxLength = 32): string {
10
+ if (filename.length <= maxLength) return filename;
11
+
12
+ const dotIdx = filename.lastIndexOf('.');
13
+ const ext = dotIdx > 0 ? filename.slice(dotIdx) : ''; // ".jpg"
14
+ const base = dotIdx > 0 ? filename.slice(0, dotIdx) : filename; // without ext
15
+
16
+ // How many chars we can show from the start vs. end of the base
17
+ const available = maxLength - ext.length - 1; // 1 for '…'
18
+ if (available <= 2) return `…${ext}`; // extreme edge case
19
+
20
+ const headLen = Math.ceil(available * 0.55);
21
+ const tailLen = available - headLen;
22
+
23
+ const head = base.slice(0, headLen);
24
+ const tail = tailLen > 0 ? base.slice(-tailLen) : '';
25
+
26
+ return `${head}…${tail}${ext}`;
27
+ }
28
+
1
29
  export function formatFileSize(bytes: number): string {
2
30
  if (bytes === 0) return '0 B';
3
31
 
@@ -5,7 +5,7 @@ export {
5
5
  buildAcceptString,
6
6
  } from './assetTypes';
7
7
 
8
- export { formatFileSize, formatDuration } from './formatters';
8
+ export { formatFileSize, formatDuration, truncateFilename } from './formatters';
9
9
 
10
10
  export { logger } from './logger';
11
11