@appcorp/stellar-solutions-modules 0.1.51 → 0.1.53

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.
@@ -11,13 +11,11 @@ export type EnhancedDropzoneProps = {
11
11
  maxSize?: number;
12
12
  minSize?: number;
13
13
  disabled?: boolean;
14
- /** initial list of files or urls to show as previews */
15
- initial?: Array<File | string>;
16
- /** Called when selected File[] changes */
14
+ /** Remote image URLs to display as previews */
15
+ value?: string[];
16
+ /** Called when files are selected or removed */
17
17
  onChange?: (files: File[]) => void;
18
- /** Called when the rendered previews (string[]) change */
19
- onPreviewsChange?: (previews: string[]) => void;
20
- /** Called when a provided remote URL preview is removed (if provided) */
21
- onRemoveUrl?: (url: string) => void;
18
+ /** Called when a remote URL is removed */
19
+ onRemoveRemote?: (url: string) => void;
22
20
  };
23
21
  export declare const EnhancedDropzone: React.FC<EnhancedDropzoneProps>;
@@ -61,210 +61,127 @@ var utils_1 = require("../../lib/utils");
61
61
  var carousel_1 = require("./carousel");
62
62
  var button_1 = require("./button");
63
63
  var lucide_react_1 = require("lucide-react");
64
- var dropzone_1 = require("./shadcn-io/dropzone");
65
64
  var EnhancedDropzone = function (_a) {
66
- var id = _a.id, label = _a.label, info = _a.info, error = _a.error, accept = _a.accept, _b = _a.maxFiles, maxFiles = _b === void 0 ? 10 : _b, maxSize = _a.maxSize, minSize = _a.minSize, disabled = _a.disabled, _c = _a.initial, initial = _c === void 0 ? [] : _c, onChange = _a.onChange, onPreviewsChange = _a.onPreviewsChange, onRemoveUrl = _a.onRemoveUrl, className = _a.className;
67
- // Files selected locally (File objects)
68
- var _d = (0, react_1.useState)([]), files = _d[0], setFiles = _d[1];
69
- // Previews array contains strings (object URLs or provided URLs) in display order
70
- var _e = (0, react_1.useState)([]), previews = _e[0], setPreviews = _e[1];
71
- // Track which previews are remote (came from initial string URLs)
72
- var _f = (0, react_1.useState)({}), remotePreviews = _f[0], setRemotePreviews = _f[1];
73
- var createdUrlsRef = (0, react_1.useRef)([]);
74
- // Track remote URLs removed by the user so they don't reappear when
75
- // previews are rebuilt from the unchanged `initial` prop.
76
- var removedRemoteRef = (0, react_1.useRef)(new Set());
77
- // Track previous previews and remote map to avoid emitting unchanged values
78
- var previewsRef = (0, react_1.useRef)([]);
79
- var remotePreviewsRef = (0, react_1.useRef)({});
80
- var dropzoneOptions = {
81
- // eslint-disable-next-line @typescript-eslint/no-explicit-any
82
- accept: (Array.isArray(accept) ? undefined : accept) || undefined,
83
- maxFiles: maxFiles,
84
- maxSize: maxSize,
85
- minSize: minSize,
86
- disabled: disabled,
87
- onDrop: function (acceptedFiles) {
88
- // append files, respecting maxFiles
89
- var nextFiles = __spreadArray(__spreadArray([], files, true), acceptedFiles, true);
90
- if (maxFiles && nextFiles.length > maxFiles)
91
- nextFiles = nextFiles.slice(0, maxFiles);
92
- setFiles(nextFiles);
93
- onChange === null || onChange === void 0 ? void 0 : onChange(nextFiles);
94
- },
95
- };
96
- var _g = (0, react_dropzone_1.useDropzone)(dropzoneOptions), getRootProps = _g.getRootProps, getInputProps = _g.getInputProps, isDragActive = _g.isDragActive;
97
- // Build previews from initial and files
98
- (0, react_1.useEffect)(function () {
99
- // cleanup previous object URLs
100
- (createdUrlsRef.current || []).forEach(function (u) {
101
- try {
102
- URL.revokeObjectURL(u);
103
- }
104
- catch (_a) { }
105
- });
106
- createdUrlsRef.current = [];
107
- var next = [];
108
- var remoteMap = {};
109
- // start with initial items (may be File or string)
110
- initial.forEach(function (item) {
111
- if (typeof item === "string") {
112
- // skip any remote URL the user removed earlier
113
- if (removedRemoteRef.current.has(item))
114
- return;
115
- next.push(item);
116
- remoteMap[item] = true;
117
- }
118
- else if (item instanceof File) {
65
+ var id = _a.id, label = _a.label, info = _a.info, error = _a.error, accept = _a.accept, _b = _a.maxFiles, maxFiles = _b === void 0 ? 10 : _b, maxSize = _a.maxSize, minSize = _a.minSize, disabled = _a.disabled, _c = _a.value, value = _c === void 0 ? [] : _c, onChange = _a.onChange, onRemoveRemote = _a.onRemoveRemote, className = _a.className;
66
+ // Local files selected by user
67
+ var _d = (0, react_1.useState)([]), localFiles = _d[0], setLocalFiles = _d[1];
68
+ // Track object URLs created for local files
69
+ var localPreviewsRef = (0, react_1.useRef)(new Map());
70
+ // Create object URLs for local files synchronously to avoid loading state
71
+ var createObjectURLs = (0, react_1.useCallback)(function () {
72
+ var map = localPreviewsRef.current;
73
+ // Create URLs for new files
74
+ localFiles.forEach(function (file) {
75
+ if (!map.has(file)) {
119
76
  try {
120
- var url = URL.createObjectURL(item);
121
- next.push(url);
122
- createdUrlsRef.current.push(url);
77
+ var url = URL.createObjectURL(file);
78
+ map.set(file, url);
123
79
  }
124
- catch (_a) {
125
- next.push("");
80
+ catch (error) {
81
+ console.error("Failed to create object URL:", error);
126
82
  }
127
83
  }
128
- else {
129
- next.push("");
130
- }
131
84
  });
132
- // then local files
133
- files.forEach(function (f) {
134
- try {
135
- var url = URL.createObjectURL(f);
136
- next.push(url);
137
- createdUrlsRef.current.push(url);
138
- }
139
- catch (_a) {
140
- next.push("");
85
+ // Clean up URLs for removed files
86
+ var currentFiles = new Set(localFiles);
87
+ Array.from(map.entries()).forEach(function (_a) {
88
+ var file = _a[0], url = _a[1];
89
+ if (!currentFiles.has(file)) {
90
+ URL.revokeObjectURL(url);
91
+ map.delete(file);
141
92
  }
142
93
  });
143
- // Avoid unnecessary updates: only set state / notify parent when previews
144
- // or remoteMap actually changed (shallow equality check). This prevents
145
- // a cycle when parent mirrors the previews back into `initial`.
146
- var prev = previewsRef.current || [];
147
- var arraysEqual = function (a, b) {
148
- if (a === b)
149
- return true;
150
- if (!a || !b)
151
- return false;
152
- if (a.length !== b.length)
153
- return false;
154
- for (var i = 0; i < a.length; i++)
155
- if (a[i] !== b[i])
156
- return false;
157
- return true;
158
- };
159
- var remoteKeysEqual = function (m1, m2) {
160
- var k1 = Object.keys(m1 || {});
161
- var k2 = Object.keys(m2 || {});
162
- if (k1.length !== k2.length)
163
- return false;
164
- k1.sort();
165
- k2.sort();
166
- for (var i = 0; i < k1.length; i++)
167
- if (k1[i] !== k2[i])
168
- return false;
169
- return true;
170
- };
171
- var previewsChanged = !arraysEqual(prev, next);
172
- var remoteChanged = !remoteKeysEqual(remotePreviewsRef.current || {}, remoteMap);
173
- if (previewsChanged) {
174
- setPreviews(next);
175
- previewsRef.current = next.slice();
176
- }
177
- if (remoteChanged) {
178
- setRemotePreviews(remoteMap);
179
- remotePreviewsRef.current = __assign({}, remoteMap);
180
- }
181
- if (previewsChanged) {
182
- onPreviewsChange === null || onPreviewsChange === void 0 ? void 0 : onPreviewsChange(next);
183
- }
184
- // notify parent about the current rendered previews (object URLs and remote URLs)
185
- onPreviewsChange === null || onPreviewsChange === void 0 ? void 0 : onPreviewsChange(next);
94
+ }, [localFiles]);
95
+ // Run synchronously on every render to avoid "loading" flash
96
+ createObjectURLs();
97
+ // Cleanup on unmount
98
+ (0, react_1.useEffect)(function () {
99
+ var map = localPreviewsRef.current;
186
100
  return function () {
187
- (createdUrlsRef.current || []).forEach(function (u) {
188
- try {
189
- URL.revokeObjectURL(u);
190
- }
191
- catch (_a) { }
101
+ Array.from(map.values()).forEach(function (url) {
102
+ URL.revokeObjectURL(url);
192
103
  });
193
- createdUrlsRef.current = [];
104
+ map.clear();
194
105
  };
195
- // files and initial are the dependencies
196
- }, [files, initial, onPreviewsChange]);
197
- // Remove a local file by index (index relative to files slice after initial length)
198
- var handleRemoveLocal = function (index) {
199
- var next = files.filter(function (_, i) { return i !== index; });
200
- setFiles(next);
201
- onChange === null || onChange === void 0 ? void 0 : onChange(next);
202
- };
203
- // Remove a preview by its absolute preview index (index within `previews`)
204
- // If the preview is remote (string from `initial`), call onRemoveUrl and
205
- // remove only that single occurrence. If it's a local file, delegate to
206
- // handleRemoveLocal with the computed files index.
207
- var handleRemoveByPreviewIndex = function (previewIndex) {
208
- var entry = previews[previewIndex];
209
- if (entry === undefined)
210
- return;
211
- var isRemote = Boolean(remotePreviews[entry]);
212
- if (isRemote) {
213
- // notify parent and mark as removed so it won't reappear from `initial`
214
- onRemoveUrl === null || onRemoveUrl === void 0 ? void 0 : onRemoveUrl(entry);
215
- removedRemoteRef.current.add(entry);
216
- // remove only the specific preview at previewIndex
217
- setPreviews(function (p) {
218
- var next = __spreadArray([], p, true);
219
- next.splice(previewIndex, 1);
220
- return next;
221
- });
222
- setRemotePreviews(function (m) {
223
- var next = __assign({}, m);
224
- delete next[entry];
225
- return next;
106
+ }, []);
107
+ var dropzoneOptions = {
108
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
109
+ accept: (Array.isArray(accept) ? undefined : accept) || undefined,
110
+ maxFiles: maxFiles,
111
+ maxSize: maxSize,
112
+ minSize: minSize,
113
+ disabled: disabled,
114
+ onDrop: (0, react_1.useCallback)(function (acceptedFiles) {
115
+ setLocalFiles(function (prev) {
116
+ var combined = __spreadArray(__spreadArray([], prev, true), acceptedFiles, true);
117
+ var limited = maxFiles ? combined.slice(0, maxFiles) : combined;
118
+ onChange === null || onChange === void 0 ? void 0 : onChange(limited);
119
+ return limited;
226
120
  });
227
- // inform parent of the immediate change
228
- var updatedPreviews = previews.filter(function (_, i) { return i !== previewIndex; });
229
- onPreviewsChange === null || onPreviewsChange === void 0 ? void 0 : onPreviewsChange(updatedPreviews);
230
- return;
231
- }
232
- // local file - compute index relative to files array and remove
233
- var fileIndex = previewIndex - initial.length;
234
- if (fileIndex >= 0 && fileIndex < files.length) {
235
- handleRemoveLocal(fileIndex);
236
- }
121
+ }, [maxFiles, onChange]),
237
122
  };
238
- // (handleRemoveRemote removed) - use handleRemoveByPreviewIndex instead so we
239
- // can remove single occurrences by index.
240
- // carousel (embla) handles scrolling via CarouselPrevious/Next
241
- return (react_1.default.createElement("div", { className: (0, utils_1.cn)("w-full border-red-300", className) },
123
+ var _e = (0, react_dropzone_1.useDropzone)(dropzoneOptions), getRootProps = _e.getRootProps, getInputProps = _e.getInputProps, isDragActive = _e.isDragActive;
124
+ // Remove remote URL
125
+ var handleRemoveRemote = (0, react_1.useCallback)(function (url) {
126
+ onRemoveRemote === null || onRemoveRemote === void 0 ? void 0 : onRemoveRemote(url);
127
+ }, [onRemoveRemote]);
128
+ // Remove local file
129
+ var handleRemoveLocal = (0, react_1.useCallback)(function (index) {
130
+ setLocalFiles(function (prev) {
131
+ var updated = prev.filter(function (_, i) { return i !== index; });
132
+ onChange === null || onChange === void 0 ? void 0 : onChange(updated);
133
+ return updated;
134
+ });
135
+ }, [onChange]);
136
+ // Get all preview URLs (remote + local)
137
+ var allPreviews = __spreadArray(__spreadArray([], value.map(function (url) { return ({ type: "remote", url: url, index: 0 }); }), true), localFiles.map(function (file, index) { return ({
138
+ type: "local",
139
+ url: localPreviewsRef.current.get(file) || "",
140
+ index: index,
141
+ }); }), true);
142
+ return (react_1.default.createElement("div", { className: (0, utils_1.cn)("w-full", className) },
242
143
  label && (react_1.default.createElement("label", { className: "mb-2 block text-sm font-medium" }, label)),
243
- react_1.default.createElement("div", __assign({}, getRootProps(), { className: (0, utils_1.cn)("relative w-full rounded-md border border-dashed p-6 text-center mx-auto", isDragActive && "ring-1 ring-ring", disabled && "opacity-60 pointer-events-none") }),
144
+ react_1.default.createElement("div", __assign({}, getRootProps(), { className: (0, utils_1.cn)("relative w-full rounded-md border border-dashed p-6 text-center min-h-[280px] flex items-center justify-center", isDragActive && "ring-2 ring-ring ring-offset-2", disabled && "opacity-60 pointer-events-none") }),
244
145
  react_1.default.createElement("input", __assign({}, getInputProps(), { id: id })),
245
- previews && previews.length > 0 ? (react_1.default.createElement("div", { className: "flex flex-col items-center" },
246
- react_1.default.createElement("div", { className: "relative w-60" },
247
- react_1.default.createElement("div", { className: "relative w-60" },
248
- react_1.default.createElement(carousel_1.Carousel, { className: "w-60" },
249
- react_1.default.createElement(carousel_1.CarouselPrevious, { onPointerDown: function (e) { return e.stopPropagation(); }, onMouseDown: function (e) { return e.stopPropagation(); } }),
250
- react_1.default.createElement(carousel_1.CarouselNext, { onPointerDown: function (e) { return e.stopPropagation(); }, onMouseDown: function (e) { return e.stopPropagation(); } }),
251
- react_1.default.createElement(carousel_1.CarouselContent, { className: "no-scrollbar flex gap-2 px-4 py-2", onPointerDown: function (e) { return e.stopPropagation(); }, onClick: function (e) { return e.stopPropagation(); } }, previews.map(function (p, idx) {
252
- return (react_1.default.createElement(carousel_1.CarouselItem, { key: "".concat(String(p), "-").concat(idx), className: "max-w-30 max-auto" }, p ? (react_1.default.createElement("div", { className: "relative" },
253
- react_1.default.createElement("img", { src: p, alt: "preview-".concat(idx), className: "h-28 w-28 rounded-md object-cover" }),
254
- react_1.default.createElement(button_1.Button, { size: "icon", variant: "ghost", onClick: function (e) {
255
- e.stopPropagation();
256
- handleRemoveByPreviewIndex(idx);
257
- }, className: "absolute right-1 top-1 rounded bg-white/80 p-0", "aria-label": "Remove image" },
258
- react_1.default.createElement(lucide_react_1.XIcon, { size: 7 })))) : (react_1.default.createElement("div", { className: "flex h-28 w-40 items-center justify-center rounded-md bg-muted text-muted-foreground" },
259
- react_1.default.createElement("span", { className: "text-xs" }, "File")))));
260
- }))))),
261
- react_1.default.createElement("p", { className: "mt-2 w-full truncate text-sm font-medium" },
262
- previews.length,
263
- " selected"))) : (
264
- // <div className="text-sm">
265
- // Drag & drop files here, or click to select
266
- // </div>
267
- react_1.default.createElement(dropzone_1.DropzoneEmptyState, null))),
146
+ allPreviews.length > 0 ? (react_1.default.createElement("div", { className: "flex flex-col items-center w-full" },
147
+ react_1.default.createElement("div", { className: "relative w-full max-w-md" },
148
+ react_1.default.createElement(carousel_1.Carousel, { className: "w-full" },
149
+ react_1.default.createElement(carousel_1.CarouselPrevious, { type: "button", onClick: function (e) { return e.stopPropagation(); }, onPointerDown: function (e) { return e.stopPropagation(); }, onMouseDown: function (e) { return e.stopPropagation(); } }),
150
+ react_1.default.createElement(carousel_1.CarouselNext, { type: "button", onClick: function (e) { return e.stopPropagation(); }, onPointerDown: function (e) { return e.stopPropagation(); }, onMouseDown: function (e) { return e.stopPropagation(); } }),
151
+ react_1.default.createElement(carousel_1.CarouselContent, { className: "ml-0" }, allPreviews.map(function (preview, idx) { return (react_1.default.createElement(carousel_1.CarouselItem, { key: "".concat(preview.type, "-").concat(preview.url, "-").concat(idx), className: "pl-4" },
152
+ react_1.default.createElement("div", { className: "relative aspect-square w-full max-w-xs mx-auto" }, preview.url ? (react_1.default.createElement(react_1.default.Fragment, null,
153
+ react_1.default.createElement("img", { src: preview.url, alt: "Preview ".concat(idx + 1), className: "h-full w-full rounded-lg object-cover", onClick: function (e) { return e.stopPropagation(); } }),
154
+ react_1.default.createElement(button_1.Button, { type: "button", size: "icon", variant: "destructive", onClick: function (e) {
155
+ e.stopPropagation();
156
+ if (preview.type === "remote") {
157
+ handleRemoveRemote(preview.url);
158
+ }
159
+ else {
160
+ handleRemoveLocal(preview.index);
161
+ }
162
+ }, className: "absolute right-2 top-2 h-8 w-8 rounded-full", "aria-label": "Remove image" },
163
+ react_1.default.createElement(lucide_react_1.XIcon, { className: "h-4 w-4" })))) : (react_1.default.createElement("div", { className: "flex h-full w-full items-center justify-center rounded-lg bg-muted text-muted-foreground" },
164
+ react_1.default.createElement("span", { className: "text-sm" }, "Loading...")))))); })))),
165
+ react_1.default.createElement("p", { className: "mt-4 text-sm font-medium text-muted-foreground" },
166
+ allPreviews.length,
167
+ " image",
168
+ allPreviews.length !== 1 ? "s" : "",
169
+ " ",
170
+ "selected"))) : (react_1.default.createElement("div", { className: "flex flex-col items-center justify-center gap-2" },
171
+ react_1.default.createElement("div", { className: "flex h-12 w-12 items-center justify-center rounded-lg bg-muted text-muted-foreground" },
172
+ react_1.default.createElement("svg", { xmlns: "http://www.w3.org/2000/svg", width: "24", height: "24", viewBox: "0 0 24 24", fill: "none", stroke: "currentColor", strokeWidth: "2", strokeLinecap: "round", strokeLinejoin: "round", className: "lucide lucide-image" },
173
+ react_1.default.createElement("rect", { width: "18", height: "18", x: "3", y: "3", rx: "2", ry: "2" }),
174
+ react_1.default.createElement("circle", { cx: "9", cy: "9", r: "2" }),
175
+ react_1.default.createElement("path", { d: "m21 15-3.086-3.086a2 2 0 0 0-2.828 0L6 21" }))),
176
+ react_1.default.createElement("div", { className: "space-y-1 text-center" },
177
+ react_1.default.createElement("p", { className: "font-medium text-sm" },
178
+ "Upload ",
179
+ maxFiles === 1 ? "an image" : "images"),
180
+ react_1.default.createElement("p", { className: "text-muted-foreground text-xs" }, "Drag and drop or click to browse"),
181
+ maxFiles > 1 && (react_1.default.createElement("p", { className: "text-muted-foreground text-xs" },
182
+ "Up to ",
183
+ maxFiles,
184
+ " files")))))),
268
185
  (error || info) && (react_1.default.createElement("div", { className: "mt-2" }, error ? (react_1.default.createElement("p", { className: "text-xs text-destructive" }, error)) : info ? (react_1.default.createElement("p", { className: "text-xs text-blue-600 dark:text-blue-400" }, info)) : null))));
269
186
  };
270
187
  exports.EnhancedDropzone = EnhancedDropzone;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@appcorp/stellar-solutions-modules",
3
- "version": "0.1.51",
3
+ "version": "0.1.53",
4
4
  "scripts": {
5
5
  "automate": "./automate.sh",
6
6
  "build": "yarn clean && yarn build:ts && cp package.json lib && cp README.md lib && cp yarn.lock lib",