laif-ds 1.1.4 → 1.1.5

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/dist/CHANGELOG.md CHANGED
@@ -5,6 +5,13 @@ All notable technical changes to the laif-ds core will be documented in this fil
5
5
  The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
6
6
  and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
7
7
 
8
+ ## [laif-ds@1.1.5]
9
+
10
+ ### 🚀 Added
11
+
12
+ - **FilePreview**: Added a `loading` prop that replaces the thumbnail with a same-size spinner, sets `aria-busy`, and disables container activation and the action menu, while keeping the inline remove button active so an in-progress upload can still be cancelled
13
+ - **FilePreview**: Image thumbnails now show a spinner until the image has loaded, then fade in
14
+
8
15
  ## [laif-ds@1.1.4]
9
16
 
10
17
  ### 🚀 Added
@@ -1,9 +1,9 @@
1
1
  {
2
2
  "schemaVersion": "1.0.0",
3
- "generatedAt": "2026-10-20T20:53:04.357Z",
3
+ "generatedAt": "2026-10-21T07:11:32.035Z",
4
4
  "package": {
5
5
  "name": "laif-ds",
6
- "version": "1.1.4"
6
+ "version": "1.1.5"
7
7
  },
8
8
  "summary": {
9
9
  "scannedFiles": 504,
@@ -647,6 +647,16 @@
647
647
  "src/components/stories/tabs.stories.tsx"
648
648
  ]
649
649
  },
650
+ {
651
+ "className": "flex flex-wrap gap-2",
652
+ "occurrences": 8,
653
+ "files": [
654
+ "src/components/stories/app-card.stories.tsx",
655
+ "src/components/stories/app-data-grid-filtri-custom.stories.tsx",
656
+ "src/components/stories/file-preview.stories.tsx",
657
+ "src/components/stories/toaster.stories.tsx"
658
+ ]
659
+ },
650
660
  {
651
661
  "className": "text-muted-foreground text-xs",
652
662
  "occurrences": 8,
@@ -711,16 +721,6 @@
711
721
  "src/components/ui/weekly-calendar/appointment-card.tsx"
712
722
  ]
713
723
  },
714
- {
715
- "className": "flex flex-wrap gap-2",
716
- "occurrences": 6,
717
- "files": [
718
- "src/components/stories/app-card.stories.tsx",
719
- "src/components/stories/app-data-grid-filtri-custom.stories.tsx",
720
- "src/components/stories/file-preview.stories.tsx",
721
- "src/components/stories/toaster.stories.tsx"
722
- ]
723
- },
724
724
  {
725
725
  "className": "text-lg font-semibold",
726
726
  "occurrences": 6,
@@ -18,6 +18,7 @@ Compact file chip with name, format/size info, optional remove button, and optio
18
18
  | `onDownload` | `(url: string) => void \| Promise<void>` | `undefined` | Called when user clicks Download from the action menu. |
19
19
  | `showActionMenu` | `boolean` | `false` | Show overflow menu (Preview/Download/Delete if provided). |
20
20
  | `showThumbnail` | `boolean` | `true` | Controls whether a thumbnail/text preview is rendered when available. |
21
+ | `loading` | `boolean` | `false` | Shows a spinner in place of the thumbnail; disables container click and the action menu (e.g. uploads). |
21
22
  | `variant` | `"default" \| "outline" \| "ghost"` | `"default"` | Visual style of the chip container. |
22
23
  | `size` | `"default" \| "sm"` | `"default"` | Visual size of the chip container and its internal elements. |
23
24
  | `className` | `string` | `""` | Additional classes for the wrapper. |
@@ -42,6 +43,11 @@ Compact file chip with name, format/size info, optional remove button, and optio
42
43
  - **Thumbnails**:
43
44
  - Images: if `showThumbnail` is `false`, an icon is shown instead of the image.
44
45
  - Text: if `showThumbnail` is `false` (or a preview can't be extracted), an icon is shown instead of the text snippet.
46
+ - **Loading**:
47
+ - `loading` replaces the thumbnail with a spinner of the same size (the chip does not change size) and sets `aria-busy` on the container.
48
+ - While loading, container click/keyboard activation and the action menu trigger are disabled; the inline remove button stays active, so removing the file can cancel an upload.
49
+ - Image thumbnails show the same spinner automatically until the image has loaded, then fade the image in; if it fails to load, the format icon is shown.
50
+ - When `onClick`, `onPreview` or the inline `onRemove` return a Promise, the chip shows the same loading state until it settles and ignores repeated activations; the remove button is disabled meanwhile. Action-menu items keep their own per-item spinner.
45
51
 
46
52
  ---
47
53
 
@@ -110,6 +116,24 @@ export function WithMenu() {
110
116
  }
111
117
  ```
112
118
 
119
+ ### Upload in Progress
120
+
121
+ ```tsx
122
+ import { FilePreview } from "laif-ds";
123
+
124
+ export function UploadingPreview({
125
+ file,
126
+ isUploading,
127
+ onCancel,
128
+ }: {
129
+ file: File;
130
+ isUploading: boolean;
131
+ onCancel: () => void;
132
+ }) {
133
+ return <FilePreview file={file} loading={isUploading} onRemove={onCancel} />;
134
+ }
135
+ ```
136
+
113
137
  ### With Custom Click Handler
114
138
 
115
139
  ```tsx
@@ -1,9 +1,9 @@
1
1
  {
2
2
  "schemaVersion": "1.1.0",
3
- "generatedAt": "2026-10-20T20:53:03.846Z",
3
+ "generatedAt": "2026-10-21T07:11:31.501Z",
4
4
  "package": {
5
5
  "name": "laif-ds",
6
- "version": "1.1.4"
6
+ "version": "1.1.5"
7
7
  },
8
8
  "stats": {
9
9
  "documentedComponentCount": 97,
@@ -3280,6 +3280,7 @@
3280
3280
  "Image",
3281
3281
  "With Remove Button",
3282
3282
  "With Action Menu",
3283
+ "Upload in Progress",
3283
3284
  "With Custom Click Handler"
3284
3285
  ],
3285
3286
  "controlledPattern": "not-specified",
@@ -3287,17 +3288,20 @@
3287
3288
  },
3288
3289
  "metadata": {
3289
3290
  "props": {
3290
- "totalProps": 11,
3291
+ "totalProps": 12,
3291
3292
  "requiredPropsCount": 0,
3292
- "typedPropsCount": 11,
3293
- "describedPropsCount": 11
3293
+ "typedPropsCount": 12,
3294
+ "describedPropsCount": 12
3294
3295
  },
3295
3296
  "accessibility": {
3296
3297
  "hasCoverage": true
3297
3298
  },
3298
3299
  "states": {
3299
- "coveredStates": [],
3300
- "coveredStateCount": 0
3300
+ "coveredStates": [
3301
+ "disabled",
3302
+ "loading"
3303
+ ],
3304
+ "coveredStateCount": 2
3301
3305
  }
3302
3306
  },
3303
3307
  "aiReadiness": {
@@ -6552,5 +6556,5 @@
6552
6556
  ]
6553
6557
  }
6554
6558
  ],
6555
- "checksum": "34fc8ab2b8cfb6421c8b1c4cfbe8a792ee0a1bc846c62fc266a58d4b3a158fc2"
6559
+ "checksum": "acf523f850e6553b7e6ee468f3246be9dd5b9c624ff99c56260dc3c475afa1c3"
6556
6560
  }
@@ -1,20 +1,20 @@
1
1
  "use client";
2
- import { jsx as r, jsxs as m, Fragment as T } from "react/jsx-runtime";
3
- import { cva as v } from "../../../node_modules/class-variance-authority/dist/index.js";
4
- import { motion as K } from "framer-motion";
5
- import f, { useEffect as $, Fragment as B } from "react";
6
- import { getNameOrUrl as z, getExt as F, getMimeFromFile as U, imageExts as W, isFile as D, getFileMetadata as q, getFormatIcon as G, formatMetadata as X } from "../../../lib/file-preview.js";
7
- import { Button as H } from "../button.js";
8
- import { DropdownMenu as J, DropdownMenuTrigger as Q, DropdownMenuContent as Y, DropdownMenuSeparator as Z, DropdownMenuItem as ee } from "../dropdown-menu.js";
9
- import { Icon as P } from "../icon.js";
10
- import { Spinner as te } from "../spinner.js";
11
- import { cn as x } from "../../../lib/utils.js";
12
- const re = {
2
+ import { jsx as t, jsxs as f, Fragment as O } from "react/jsx-runtime";
3
+ import { cva as y } from "../../../node_modules/class-variance-authority/dist/index.js";
4
+ import { motion as Q } from "framer-motion";
5
+ import d, { useEffect as K, Fragment as Y } from "react";
6
+ import { getNameOrUrl as N, getExt as C, getMimeFromFile as B, imageExts as Z, isFile as V, getFileMetadata as ee, getFormatIcon as te, formatMetadata as ae } from "../../../lib/file-preview.js";
7
+ import { Button as ne } from "../button.js";
8
+ import { DropdownMenu as re, DropdownMenuTrigger as ie, DropdownMenuContent as oe, DropdownMenuSeparator as se, DropdownMenuItem as le } from "../dropdown-menu.js";
9
+ import { Icon as S } from "../icon.js";
10
+ import { Spinner as W } from "../spinner.js";
11
+ import { cn as g } from "../../../lib/utils.js";
12
+ const ce = {
13
13
  layout: !0,
14
14
  initial: { opacity: 0, y: "100%" },
15
15
  animate: { opacity: 1, y: 0 },
16
16
  exit: { opacity: 0, y: "100%" }
17
- }, ae = v(
17
+ }, de = y(
18
18
  "group/file ring-d-ring focus-visible:ring-d-accent relative flex items-center gap-1.5 overflow-hidden rounded-md transition-colors focus-within:ring focus-visible:ring focus-visible:outline-none",
19
19
  {
20
20
  variants: {
@@ -33,7 +33,7 @@ const re = {
33
33
  size: "default"
34
34
  }
35
35
  }
36
- ), V = v(
36
+ ), T = y(
37
37
  "aspect-square h-full shrink-0 rounded-sm",
38
38
  {
39
39
  variants: {
@@ -46,7 +46,7 @@ const re = {
46
46
  size: "default"
47
47
  }
48
48
  }
49
- ), ne = v("", {
49
+ ), fe = y("", {
50
50
  variants: {
51
51
  size: {
52
52
  default: "h-5 w-5",
@@ -56,7 +56,7 @@ const re = {
56
56
  defaultVariants: {
57
57
  size: "default"
58
58
  }
59
- }), ie = v(
59
+ }), ue = y(
60
60
  "flex max-w-full overflow-hidden whitespace-nowrap",
61
61
  {
62
62
  variants: {
@@ -69,7 +69,7 @@ const re = {
69
69
  size: "default"
70
70
  }
71
71
  }
72
- ), oe = v("text-d-muted-foreground", {
72
+ ), me = y("text-d-muted-foreground", {
73
73
  variants: {
74
74
  size: {
75
75
  default: "text-sm",
@@ -79,7 +79,7 @@ const re = {
79
79
  defaultVariants: {
80
80
  size: "default"
81
81
  }
82
- }), se = v(
82
+ }), he = y(
83
83
  "hover:bg-d-accent hover:text-d-accent-foreground data-[state=open]:bg-d-accent data-[state=open]:text-d-accent-foreground cursor-pointer focus-visible:bg-d-accent rounded-r-sm focus-visible:outline-none",
84
84
  {
85
85
  variants: {
@@ -92,7 +92,7 @@ const re = {
92
92
  size: "default"
93
93
  }
94
94
  }
95
- ), le = v(
95
+ ), ge = y(
96
96
  "absolute flex self-start overflow-visible rounded-sm p-0.5 text-xs opacity-0 group-hover/file:opacity-100 before:absolute before:top-0 before:right-1/2 before:-z-10 before:h-[400%] before:w-[300%] before:translate-x-1/2 before:-translate-y-1/2 before:rounded-full before:bg-radial before:from-50% before:to-transparent before:to-80% before:blur-xs before:content-[''] focus-visible:opacity-100",
97
97
  {
98
98
  variants: {
@@ -111,84 +111,122 @@ const re = {
111
111
  size: "default"
112
112
  }
113
113
  }
114
- ), j = f.createContext({ variant: "default", size: "default" }), N = () => f.useContext(j), I = ({
114
+ ), q = d.createContext({ variant: "default", size: "default", loading: !1 }), b = () => d.useContext(q), R = ({
115
115
  sizeLabel: e,
116
- formatLabel: a
116
+ formatLabel: n
117
117
  }) => {
118
- const { size: o } = N(), t = X(e, a);
119
- return t ? /* @__PURE__ */ r(
118
+ const { size: o } = b(), a = ae(e, n);
119
+ return a ? /* @__PURE__ */ t(
120
120
  "div",
121
121
  {
122
- className: oe({ size: o }),
123
- "aria-label": `${e ? `Size: ${e}` : ""}${e && a ? ", " : ""}${a ? `Format: ${a}` : ""}`,
124
- children: t
122
+ className: me({ size: o }),
123
+ "aria-label": `${e ? `Size: ${e}` : ""}${e && n ? ", " : ""}${n ? `Format: ${n}` : ""}`,
124
+ children: a
125
125
  }
126
126
  ) : null;
127
- }, ce = ({
127
+ }, G = ({ className: e }) => {
128
+ const { size: n } = b();
129
+ return /* @__PURE__ */ t(
130
+ W,
131
+ {
132
+ size: n === "sm" ? "sm" : "md",
133
+ className: g("text-d-muted-foreground", e),
134
+ "aria-hidden": "true"
135
+ }
136
+ );
137
+ }, be = () => {
138
+ const { size: e } = b();
139
+ return /* @__PURE__ */ t(
140
+ "div",
141
+ {
142
+ className: g(
143
+ T({ size: e }),
144
+ "flex items-center justify-center"
145
+ ),
146
+ children: /* @__PURE__ */ t(G, {})
147
+ }
148
+ );
149
+ }, M = ({ children: e }) => {
150
+ const { loading: n } = b();
151
+ return n ? /* @__PURE__ */ t(be, {}) : e;
152
+ }, pe = ({
128
153
  fileUrl: e,
129
- fileName: a
154
+ fileName: n
130
155
  }) => {
131
- const { size: o } = N(), [t, n] = f.useState(e), s = F(a);
132
- f.useEffect(() => {
133
- n(e);
134
- }, [e]);
135
- const c = () => {
136
- n(void 0);
137
- };
138
- return t ? /* @__PURE__ */ r(
139
- "img",
156
+ const { size: o } = b(), [a, r] = d.useState("loading"), s = d.useCallback((c) => {
157
+ c?.complete && c.naturalWidth > 0 && r("loaded");
158
+ }, []);
159
+ return !e || a === "error" ? /* @__PURE__ */ t(D, { ext: C(n), mimeType: "image/*" }) : /* @__PURE__ */ f(
160
+ "div",
140
161
  {
141
- alt: a,
142
- className: x(V({ size: o }), "object-cover"),
143
- width: 40,
144
- height: 40,
145
- loading: "lazy",
146
- onError: c,
147
- decoding: "async",
148
- src: t
162
+ className: g(
163
+ T({ size: o }),
164
+ "relative overflow-hidden"
165
+ ),
166
+ children: [
167
+ a === "loading" && /* @__PURE__ */ t(G, { className: "absolute inset-0 m-auto" }),
168
+ /* @__PURE__ */ t(
169
+ "img",
170
+ {
171
+ ref: s,
172
+ alt: n,
173
+ className: g(
174
+ "size-full object-cover transition-opacity",
175
+ a === "loaded" ? "opacity-100" : "opacity-0"
176
+ ),
177
+ width: 40,
178
+ height: 40,
179
+ loading: "lazy",
180
+ onLoad: () => r("loaded"),
181
+ onError: () => r("error"),
182
+ decoding: "async",
183
+ src: e
184
+ }
185
+ )
186
+ ]
149
187
  }
150
- ) : /* @__PURE__ */ r(k, { ext: s, mimeType: "image/*" });
151
- }, k = ({
188
+ );
189
+ }, D = ({
152
190
  className: e,
153
- ext: a,
191
+ ext: n,
154
192
  mimeType: o
155
193
  }) => {
156
- const { size: t } = N(), { iconName: n, colorClass: s } = G(a ?? "", o);
157
- return /* @__PURE__ */ r(
194
+ const { size: a } = b(), { iconName: r, colorClass: s } = te(n ?? "", o);
195
+ return /* @__PURE__ */ t(
158
196
  "div",
159
197
  {
160
- className: x(
161
- V({ size: t }),
198
+ className: g(
199
+ T({ size: a }),
162
200
  "flex items-center justify-center",
163
201
  e
164
202
  ),
165
203
  "aria-hidden": "true",
166
- children: /* @__PURE__ */ r(
167
- P,
204
+ children: /* @__PURE__ */ t(
205
+ S,
168
206
  {
169
- name: n,
170
- className: x(ne({ size: t }), s),
207
+ name: r,
208
+ className: g(fe({ size: a }), s),
171
209
  "aria-hidden": "true"
172
210
  }
173
211
  )
174
212
  }
175
213
  );
176
- }, de = ({
214
+ }, xe = ({
177
215
  url: e,
178
- onPreview: a,
216
+ onPreview: n,
179
217
  onDownload: o,
180
- onRemove: t,
181
- disabled: n = !1
218
+ onRemove: a,
219
+ disabled: r = !1
182
220
  }) => {
183
- const { size: s } = N(), [c, h] = f.useState(!1), [b, g] = f.useState(
221
+ const { size: s } = b(), [c, p] = d.useState(!1), [x, m] = d.useState(
184
222
  null
185
223
  ), u = [
186
224
  {
187
225
  key: "preview",
188
- cond: !!a,
226
+ cond: !!n,
189
227
  icon: "Presentation",
190
228
  label: "Anteprima",
191
- onSelect: a
229
+ onSelect: n
192
230
  },
193
231
  {
194
232
  key: "download",
@@ -199,61 +237,61 @@ const re = {
199
237
  },
200
238
  {
201
239
  key: "remove",
202
- cond: !!t,
240
+ cond: !!a,
203
241
  icon: "Trash",
204
242
  label: "Elimina",
205
- onSelect: t,
243
+ onSelect: a,
206
244
  danger: !0
207
245
  }
208
- ], w = async (i) => {
209
- if (i.onSelect && !b)
246
+ ], z = async (i) => {
247
+ if (i.onSelect && !x)
210
248
  try {
211
- const d = i.onSelect(e);
212
- typeof d?.then == "function" && (g(i.key), await d);
213
- } catch (d) {
214
- console.error(d);
249
+ const v = i.onSelect(e);
250
+ typeof v?.then == "function" && (m(i.key), await v);
251
+ } catch (v) {
252
+ console.error(v);
215
253
  } finally {
216
- g(null), h(!1);
254
+ m(null), p(!1);
217
255
  }
218
256
  };
219
- return /* @__PURE__ */ m(J, { onOpenChange: (i) => h(i), open: c, children: [
220
- /* @__PURE__ */ r(
221
- Q,
257
+ return /* @__PURE__ */ f(re, { onOpenChange: (i) => p(i), open: c, children: [
258
+ /* @__PURE__ */ t(
259
+ ie,
222
260
  {
223
- disabled: n || !e,
224
- className: se({ size: s }),
261
+ disabled: r || !e,
262
+ className: he({ size: s }),
225
263
  "aria-label": "File actions",
226
264
  onClick: (i) => i.stopPropagation(),
227
265
  onKeyDown: (i) => i.stopPropagation(),
228
- children: /* @__PURE__ */ r(
229
- P,
266
+ children: /* @__PURE__ */ t(
267
+ S,
230
268
  {
231
269
  name: "ChevronDown",
232
- className: x(s === "sm" ? "size-3" : "size-4"),
270
+ className: g(s === "sm" ? "size-3" : "size-4"),
233
271
  "aria-hidden": "true"
234
272
  }
235
273
  )
236
274
  }
237
275
  ),
238
- /* @__PURE__ */ r(
239
- Y,
276
+ /* @__PURE__ */ t(
277
+ oe,
240
278
  {
241
279
  align: "end",
242
280
  onClick: (i) => i.stopPropagation(),
243
281
  onPointerDown: (i) => i.stopPropagation(),
244
- children: u.filter((i) => i.cond).map((i, d, y) => /* @__PURE__ */ m(B, { children: [
245
- i.danger && y.length > 1 && /* @__PURE__ */ r(Z, {}),
246
- /* @__PURE__ */ m(
247
- ee,
282
+ children: u.filter((i) => i.cond).map((i, v, h) => /* @__PURE__ */ f(Y, { children: [
283
+ i.danger && h.length > 1 && /* @__PURE__ */ t(se, {}),
284
+ /* @__PURE__ */ f(
285
+ le,
248
286
  {
249
- onSelect: (S) => {
250
- S.preventDefault(), w(i);
287
+ onSelect: (P) => {
288
+ P.preventDefault(), z(i);
251
289
  },
252
290
  variant: i.danger ? "destructive" : "default",
253
- disabled: !!b,
291
+ disabled: !!x,
254
292
  children: [
255
- b === i.key ? /* @__PURE__ */ r(te, { size: "sm" }) : /* @__PURE__ */ r(P, { name: i.icon, size: "sm" }),
256
- /* @__PURE__ */ r("span", { children: i.label })
293
+ x === i.key ? /* @__PURE__ */ t(W, { size: "sm" }) : /* @__PURE__ */ t(S, { name: i.icon, size: "sm" }),
294
+ /* @__PURE__ */ t("span", { children: i.label })
257
295
  ]
258
296
  }
259
297
  )
@@ -261,132 +299,138 @@ const re = {
261
299
  }
262
300
  )
263
301
  ] });
264
- }, O = ({
302
+ }, $ = ({
265
303
  name: e,
266
- className: a
304
+ className: n
267
305
  }) => {
268
- const { size: o } = N(), t = e.lastIndexOf("."), n = t > 0 && t < e.length - 1, s = n ? e.slice(0, t) : e, c = n ? e.slice(t + 1) : "";
269
- return /* @__PURE__ */ m(
306
+ const { size: o } = b(), a = e.lastIndexOf("."), r = a > 0 && a < e.length - 1, s = r ? e.slice(0, a) : e, c = r ? e.slice(a + 1) : "";
307
+ return /* @__PURE__ */ f(
270
308
  "span",
271
309
  {
272
- className: x(ie({ size: o }), a),
310
+ className: g(ue({ size: o }), n),
273
311
  title: e,
274
312
  children: [
275
- /* @__PURE__ */ r("span", { className: "shrink-1 truncate", children: s }),
276
- c && /* @__PURE__ */ r("span", { className: "w-fit", children: `.${c}` })
313
+ /* @__PURE__ */ t("span", { className: "shrink-1 truncate", children: s }),
314
+ c && /* @__PURE__ */ t("span", { className: "w-fit", children: `.${c}` })
277
315
  ]
278
316
  }
279
317
  );
280
- }, Pe = f.forwardRef(
281
- (e, a) => {
282
- const o = z(e.file), t = F(o), n = U(e.file), s = t === "csv" || n === "text/csv";
283
- return n && n.startsWith("image/") || t && W.has(t) ? /* @__PURE__ */ r(fe, { ...e, ref: a }) : !s && (t === "txt" || t === "md" || n && n.startsWith("text/")) ? /* @__PURE__ */ r(ue, { ...e, ref: a }) : /* @__PURE__ */ r(me, { ...e, ref: a });
318
+ }, Le = d.forwardRef(
319
+ (e, n) => {
320
+ const o = N(e.file), a = C(o), r = B(e.file), s = a === "csv" || r === "text/csv";
321
+ return r && r.startsWith("image/") || a && Z.has(a) ? /* @__PURE__ */ t(ve, { ...e, ref: n }) : !s && (a === "txt" || a === "md" || r && r.startsWith("text/")) ? /* @__PURE__ */ t(we, { ...e, ref: n }) : /* @__PURE__ */ t(ye, { ...e, ref: n });
284
322
  }
285
- ), L = f.forwardRef(
323
+ ), j = d.forwardRef(
286
324
  ({
287
325
  file: e,
288
- clickable: a = !0,
326
+ clickable: n = !0,
289
327
  onRemove: o,
290
- onPreview: t,
291
- onDownload: n,
328
+ onPreview: a,
329
+ onDownload: r,
292
330
  showActionMenu: s = !1,
293
331
  className: c,
294
- defaultClassName: h,
295
- children: b,
296
- onClick: g,
332
+ defaultClassName: p,
333
+ children: x,
334
+ onClick: m,
297
335
  variant: u = "default",
298
- size: w = "default"
299
- }, i) => {
300
- const [d, y] = f.useState(
301
- () => D(e) ? "" : e.url
302
- );
303
- $(() => {
304
- if (D(e)) {
336
+ size: z = "default",
337
+ loading: i = !1
338
+ }, v) => {
339
+ const [h, P] = d.useState(
340
+ () => V(e) ? "" : e.url
341
+ ), [I, U] = d.useState(!1), F = i || I;
342
+ K(() => {
343
+ if (V(e)) {
305
344
  const l = URL.createObjectURL(e);
306
- return y(l), () => {
345
+ return P(l), () => {
307
346
  URL.revokeObjectURL(l);
308
347
  };
309
348
  } else
310
- y(e.url);
349
+ P(e.url);
311
350
  }, [e]);
312
- const S = q(e), R = z(e), M = !s && !!g, p = a && (!!t || M), E = async (l) => {
313
- if (M) {
314
- try {
315
- await g(l);
316
- } catch (C) {
317
- console.error(C);
318
- }
319
- return;
320
- }
321
- if (t) {
351
+ const X = ee(e), E = N(e), A = !s && !!m, w = n && !F && (!!a || A), L = async (l) => {
352
+ if (!I)
322
353
  try {
323
- await t(d);
324
- } catch (C) {
325
- console.error(C);
354
+ const k = l();
355
+ typeof k?.then == "function" && (U(!0), await k);
356
+ } catch (k) {
357
+ console.error(k);
358
+ } finally {
359
+ U(!1);
326
360
  }
327
- return;
328
- }
329
- }, A = (l) => {
330
- p && (l.stopPropagation(), E(l));
331
361
  }, _ = (l) => {
332
- p && (l.stopPropagation(), (l.key === "Enter" || l.key === " ") && (l.preventDefault(), E(l)));
362
+ if (A)
363
+ return L(() => m(l));
364
+ if (a)
365
+ return L(() => a(h));
366
+ }, H = (l) => {
367
+ w && (l.stopPropagation(), _(l));
368
+ }, J = (l) => {
369
+ w && (l.stopPropagation(), (l.key === "Enter" || l.key === " ") && (l.preventDefault(), _(l)));
333
370
  };
334
- return /* @__PURE__ */ r(
335
- K.div,
371
+ return /* @__PURE__ */ t(
372
+ Q.div,
336
373
  {
337
- ref: i,
338
- role: p ? "button" : "group",
339
- tabIndex: p ? 0 : -1,
340
- "aria-label": `File: ${R}`,
341
- className: x(
342
- ae({ variant: u, size: w }),
343
- p && "cursor-pointer",
344
- h,
374
+ ref: v,
375
+ role: w ? "button" : "group",
376
+ tabIndex: w ? 0 : -1,
377
+ "aria-label": `File: ${E}`,
378
+ "aria-busy": F || void 0,
379
+ className: g(
380
+ de({ variant: u, size: z }),
381
+ w && "cursor-pointer",
382
+ p,
345
383
  c
346
384
  ),
347
- onClick: p ? A : void 0,
348
- onKeyDown: p ? _ : void 0,
349
- ...re,
350
- children: /* @__PURE__ */ m(j.Provider, { value: { variant: u, size: w }, children: [
351
- /* @__PURE__ */ m("div", { className: "flex h-full w-full min-w-0 items-center gap-1", children: [
352
- b(d, S),
353
- s && /* @__PURE__ */ r(
354
- de,
355
- {
356
- url: d,
357
- onPreview: t,
358
- onDownload: n,
359
- onRemove: o,
360
- disabled: !d
361
- }
362
- )
363
- ] }),
364
- !s && o ? /* @__PURE__ */ r(
365
- H,
366
- {
367
- className: le({ variant: u, size: w }),
368
- variant: "ghost",
369
- size: "icon",
370
- "aria-label": `Remove ${R}`,
371
- disabled: !d,
372
- onClick: (l) => {
373
- l.stopPropagation(), d && o(d);
374
- },
375
- children: /* @__PURE__ */ r(P, { name: "X", className: "size-full", "aria-hidden": "true" })
376
- }
377
- ) : null
378
- ] })
385
+ onClick: w ? H : void 0,
386
+ onKeyDown: w ? J : void 0,
387
+ ...ce,
388
+ children: /* @__PURE__ */ f(
389
+ q.Provider,
390
+ {
391
+ value: { variant: u, size: z, loading: F },
392
+ children: [
393
+ /* @__PURE__ */ f("div", { className: "flex h-full w-full min-w-0 items-center gap-1", children: [
394
+ x(h, X),
395
+ s && /* @__PURE__ */ t(
396
+ xe,
397
+ {
398
+ url: h,
399
+ onPreview: a,
400
+ onDownload: r,
401
+ onRemove: o,
402
+ disabled: !h || F
403
+ }
404
+ )
405
+ ] }),
406
+ !s && o ? /* @__PURE__ */ t(
407
+ ne,
408
+ {
409
+ className: ge({ variant: u, size: z }),
410
+ variant: "ghost",
411
+ size: "icon",
412
+ "aria-label": `Remove ${E}`,
413
+ disabled: !h || I,
414
+ onClick: (l) => {
415
+ l.stopPropagation(), h && L(() => o(h));
416
+ },
417
+ children: /* @__PURE__ */ t(S, { name: "X", className: "size-full", "aria-hidden": "true" })
418
+ }
419
+ ) : null
420
+ ]
421
+ }
422
+ )
379
423
  }
380
424
  );
381
425
  }
382
- ), fe = f.forwardRef(({ showThumbnail: e = !0, ...a }, o) => {
383
- const t = z(a.file);
384
- return /* @__PURE__ */ r(L, { ...a, ref: o, children: (n, s) => /* @__PURE__ */ m(T, { children: [
385
- e ? /* @__PURE__ */ r(ce, { fileUrl: n, fileName: t }) : /* @__PURE__ */ r(k, { ext: F(t), mimeType: "image/*" }),
386
- /* @__PURE__ */ m("div", { className: "mr-1 flex min-w-0 flex-1 flex-col gap-0.5", children: [
387
- /* @__PURE__ */ r(O, { name: t, className: "inline-flex min-w-0 flex-1" }),
388
- /* @__PURE__ */ r(
389
- I,
426
+ ), ve = d.forwardRef(({ showThumbnail: e = !0, ...n }, o) => {
427
+ const a = N(n.file);
428
+ return /* @__PURE__ */ t(j, { ...n, ref: o, children: (r, s) => /* @__PURE__ */ f(O, { children: [
429
+ /* @__PURE__ */ t(M, { children: e ? /* @__PURE__ */ t(pe, { fileUrl: r, fileName: a }, r) : /* @__PURE__ */ t(D, { ext: C(a), mimeType: "image/*" }) }),
430
+ /* @__PURE__ */ f("div", { className: "mr-1 flex min-w-0 flex-1 flex-col gap-0.5", children: [
431
+ /* @__PURE__ */ t($, { name: a, className: "inline-flex min-w-0 flex-1" }),
432
+ /* @__PURE__ */ t(
433
+ R,
390
434
  {
391
435
  sizeLabel: s.sizeLabel,
392
436
  formatLabel: s.formatLabel
@@ -394,41 +438,41 @@ const re = {
394
438
  )
395
439
  ] })
396
440
  ] }) });
397
- }), ue = f.forwardRef(({ file: e, showThumbnail: a = !0, ...o }, t) => {
398
- const [n, s] = f.useState(null), c = z(e), h = o.size ?? "default";
399
- $(() => {
400
- if (D(e)) {
401
- const g = new FileReader();
402
- return g.onload = (u) => {
441
+ }), we = d.forwardRef(({ file: e, showThumbnail: n = !0, ...o }, a) => {
442
+ const [r, s] = d.useState(null), c = N(e), p = o.size ?? "default";
443
+ K(() => {
444
+ if (V(e)) {
445
+ const m = new FileReader();
446
+ return m.onload = (u) => {
403
447
  u.target?.result && s(
404
448
  typeof u.target.result == "string" ? u.target.result.slice(0, 100) : ""
405
449
  );
406
- }, g.readAsText(e), () => {
407
- g.abort();
450
+ }, m.readAsText(e), () => {
451
+ m.abort();
408
452
  };
409
453
  } else
410
454
  s(null);
411
455
  }, [e]);
412
- const b = "text/plain";
413
- return /* @__PURE__ */ r(L, { ...o, ref: t, file: e, children: (g, u) => /* @__PURE__ */ m(T, { children: [
414
- n && a ? /* @__PURE__ */ r(
456
+ const x = "text/plain";
457
+ return /* @__PURE__ */ t(j, { ...o, ref: a, file: e, children: (m, u) => /* @__PURE__ */ f(O, { children: [
458
+ /* @__PURE__ */ t(M, { children: r && n ? /* @__PURE__ */ t(
415
459
  "div",
416
460
  {
417
- className: x(
418
- V({ size: h }),
461
+ className: g(
462
+ T({ size: p }),
419
463
  "flex items-start justify-start overflow-hidden p-0.5 leading-tight",
420
- h === "sm" ? "text-[7px]" : "text-[8px]"
464
+ p === "sm" ? "text-[7px]" : "text-[8px]"
421
465
  ),
422
- title: n,
466
+ title: r,
423
467
  "aria-label": "Text preview",
424
468
  role: "img",
425
- children: n
469
+ children: r
426
470
  }
427
- ) : /* @__PURE__ */ r(k, { mimeType: b }),
428
- /* @__PURE__ */ m("div", { className: "mr-1 min-w-0 flex-1", children: [
429
- /* @__PURE__ */ r(O, { name: c, className: "inline-flex min-w-0 flex-1" }),
430
- /* @__PURE__ */ r(
431
- I,
471
+ ) : /* @__PURE__ */ t(D, { mimeType: x }) }),
472
+ /* @__PURE__ */ f("div", { className: "mr-1 min-w-0 flex-1", children: [
473
+ /* @__PURE__ */ t($, { name: c, className: "inline-flex min-w-0 flex-1" }),
474
+ /* @__PURE__ */ t(
475
+ R,
432
476
  {
433
477
  sizeLabel: u.sizeLabel,
434
478
  formatLabel: u.formatLabel
@@ -436,14 +480,14 @@ const re = {
436
480
  )
437
481
  ] })
438
482
  ] }) });
439
- }), me = f.forwardRef((e, a) => {
440
- const o = z(e.file), t = F(o), n = U(e.file);
441
- return /* @__PURE__ */ r(L, { ...e, ref: a, children: (s, c) => /* @__PURE__ */ m(T, { children: [
442
- /* @__PURE__ */ r(k, { ext: t, mimeType: n }),
443
- /* @__PURE__ */ m("div", { className: "mr-1 flex min-w-0 flex-1 flex-col gap-0.5", children: [
444
- /* @__PURE__ */ r(O, { name: o, className: "inline-flex min-w-0 flex-1" }),
445
- /* @__PURE__ */ r(
446
- I,
483
+ }), ye = d.forwardRef((e, n) => {
484
+ const o = N(e.file), a = C(o), r = B(e.file);
485
+ return /* @__PURE__ */ t(j, { ...e, ref: n, children: (s, c) => /* @__PURE__ */ f(O, { children: [
486
+ /* @__PURE__ */ t(M, { children: /* @__PURE__ */ t(D, { ext: a, mimeType: r }) }),
487
+ /* @__PURE__ */ f("div", { className: "mr-1 flex min-w-0 flex-1 flex-col gap-0.5", children: [
488
+ /* @__PURE__ */ t($, { name: o, className: "inline-flex min-w-0 flex-1" }),
489
+ /* @__PURE__ */ t(
490
+ R,
447
491
  {
448
492
  sizeLabel: c.sizeLabel,
449
493
  formatLabel: c.formatLabel
@@ -453,8 +497,8 @@ const re = {
453
497
  ] }) });
454
498
  });
455
499
  export {
456
- Pe as FilePreview,
457
- me as GenericFilePreview,
458
- fe as ImageFilePreview,
459
- ue as TextFilePreview
500
+ Le as FilePreview,
501
+ ye as GenericFilePreview,
502
+ ve as ImageFilePreview,
503
+ we as TextFilePreview
460
504
  };
package/dist/index.d.ts CHANGED
@@ -2462,6 +2462,18 @@ declare type FilePreviewBaseProps = {
2462
2462
  * @default true
2463
2463
  */
2464
2464
  showThumbnail?: boolean;
2465
+ /**
2466
+ * Shows a spinner in place of the thumbnail (e.g. while the file is uploading).
2467
+ *
2468
+ * While loading, container click/keyboard activation and the action menu are disabled.
2469
+ * The inline remove button stays active, so the upload can be cancelled.
2470
+ *
2471
+ * The same spinner is also shown automatically while an async `onClick`, `onPreview`
2472
+ * or `onRemove` is pending, and while an image thumbnail is still loading.
2473
+ *
2474
+ * @default false
2475
+ */
2476
+ loading?: boolean;
2465
2477
  /** Optional additional className applied to the root container. */
2466
2478
  className?: string;
2467
2479
  variant?: FilePreviewVariant;
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "laif-ds",
3
3
  "private": false,
4
- "version": "1.1.4",
4
+ "version": "1.1.5",
5
5
  "type": "module",
6
6
  "main": "dist/index.js",
7
7
  "module": "dist/index.js",