@pushwoosh/smart-components-kit 1.1.2 → 1.1.4

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.
@@ -7,6 +7,7 @@ type CreateCropDrawerProps = {
7
7
  existingCrops: MediaFile[];
8
8
  onClose: () => void;
9
9
  onCreated: (parentUuid: string, crop: MediaFile) => void;
10
+ onDeleted: (parentUuid: string, cropUuid: string) => void;
10
11
  };
11
12
  export declare const CreateCropDrawer: FC<CreateCropDrawerProps>;
12
13
  export {};
@@ -1,13 +1,14 @@
1
1
  import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
2
  import { useEffect, useState } from 'react';
3
- import { Button, Drawer, DrawerBody, DrawerHeader, DrawerTitle, NativeInput, useToast } from '@pushwoosh/dumb-components';
3
+ import { Button, Drawer, DrawerBody, DrawerHeader, DrawerTitle, ModalConfirm, NativeInput, useToast } from '@pushwoosh/dumb-components';
4
4
  import { Columns, Vertical } from '@pushwoosh/kit-helpers';
5
- import { CloseIcon } from '@pushwoosh/kit-icons';
5
+ import { CloseIcon, TrashIcon } from '@pushwoosh/kit-icons';
6
6
  import { Text } from '@pushwoosh/kit-typography';
7
7
  import { useSPAContext } from '@pushwoosh/spa-context';
8
+ import { useDeleteMediaFile } from './hooks';
8
9
  import { getRatioLabel, getThumbUrl } from './mediaFile.utils';
9
10
  import { useLang } from './polyglot';
10
- import { Chip, Chips, CropStrip, CropThumb, CropThumbCaption, FieldLabel, PreviewFrame, PreviewImage, PreviewStage, Segment, SegmentItem, TileImage } from './styled';
11
+ import { Chip, Chips, CropStrip, CropThumb, CropThumbCaption, CropThumbDelete, FieldLabel, PreviewFrame, PreviewImage, PreviewStage, Segment, SegmentItem, TileImage } from './styled';
11
12
  const RATIOS = [{
12
13
  value: 'ASPECT_RATIO_1_1',
13
14
  label: '1:1',
@@ -54,7 +55,8 @@ export const CreateCropDrawer = ({
54
55
  isOpen,
55
56
  existingCrops,
56
57
  onClose,
57
- onCreated
58
+ onCreated,
59
+ onDeleted
58
60
  }) => {
59
61
  const lang = useLang();
60
62
  const {
@@ -63,11 +65,13 @@ export const CreateCropDrawer = ({
63
65
  const {
64
66
  openToast
65
67
  } = useToast();
68
+ const deleteMediaFile = useDeleteMediaFile(application);
66
69
  const [ratio, setRatio] = useState(DEFAULT_RATIO);
67
70
  const [dimension, setDimension] = useState('width');
68
71
  const [size, setSize] = useState(DEFAULT_SIZE);
69
72
  const [quality, setQuality] = useState(DEFAULT_QUALITY);
70
73
  const [isCreating, setIsCreating] = useState(false);
74
+ const [pendingDeleteCrop, setPendingDeleteCrop] = useState(null);
71
75
  // The drawer mounts once and is toggled via `isOpen`, so reset every field
72
76
  // whenever it opens for a different original — otherwise values (and the
73
77
  // `ratioExists` guard) leak from the previously edited image.
@@ -128,6 +132,17 @@ export const CreateCropDrawer = ({
128
132
  setIsCreating(false);
129
133
  }
130
134
  };
135
+ const handleDeleteCrop = async () => {
136
+ const crop = pendingDeleteCrop;
137
+ setPendingDeleteCrop(null);
138
+ if (!crop) return;
139
+ // `parent_uuid` is set on crops; fall back to the open original's uuid.
140
+ const parentUuid = crop.parentUuid ?? (original === null || original === void 0 ? void 0 : original.uuid);
141
+ if (!parentUuid) return;
142
+ if (await deleteMediaFile(crop.uuid)) {
143
+ onDeleted(parentUuid, crop.uuid);
144
+ }
145
+ };
131
146
  return _jsxs(Drawer, {
132
147
  size: "medium",
133
148
  isOpen: isOpen,
@@ -253,6 +268,19 @@ export const CreateCropDrawer = ({
253
268
  children: [_jsx(TileImage, {
254
269
  src: getThumbUrl(crop),
255
270
  alt: crop.filename
271
+ }), _jsx(CropThumbDelete, {
272
+ children: _jsx(Button, {
273
+ color: "secondary",
274
+ size: "compact",
275
+ icon: _jsx(TrashIcon, {
276
+ size: "small",
277
+ view: "lined"
278
+ }),
279
+ "aria-label": lang({
280
+ message: 'SMART.MEDIA_STORE.CREATE_CROP.DELETE_HINT'
281
+ }),
282
+ onClick: () => setPendingDeleteCrop(crop)
283
+ })
256
284
  }), _jsx(CropThumbCaption, {
257
285
  children: getRatioLabel(crop) || crop.filename
258
286
  })]
@@ -260,6 +288,17 @@ export const CreateCropDrawer = ({
260
288
  })]
261
289
  })]
262
290
  })
291
+ }), _jsx(ModalConfirm, {
292
+ isOpen: pendingDeleteCrop !== null,
293
+ title: lang({
294
+ message: 'SMART.MEDIA_STORE.CREATE_CROP.DELETE_CONFIRM.TITLE'
295
+ }),
296
+ confirmButtonText: lang({
297
+ message: 'SMART.MEDIA_STORE.CREATE_CROP.DELETE_CONFIRM.BUTTON'
298
+ }),
299
+ confirmButtonColor: "danger",
300
+ onAccept: () => void handleDeleteCrop(),
301
+ onCancel: () => setPendingDeleteCrop(null)
263
302
  })]
264
303
  });
265
304
  };
@@ -1,14 +1,15 @@
1
1
  import { jsx as _jsx, jsxs as _jsxs, Fragment as _Fragment } from "react/jsx-runtime";
2
2
  import { useEffect, useRef, useState } from 'react';
3
- import { Button, DropdownMenuButton, DropdownMenuItem, DropdownMenuSection, useToast } from '@pushwoosh/dumb-components';
4
- import { Horizontal, useAsync } from '@pushwoosh/kit-helpers';
5
- import { CheckIcon, EditIcon, UploadIcon } from '@pushwoosh/kit-icons';
3
+ import { Button, DropdownMenuButton, DropdownMenuItem, DropdownMenuSection, ModalConfirm, useToast } from '@pushwoosh/dumb-components';
4
+ import { FlexContainer, useAsync } from '@pushwoosh/kit-helpers';
5
+ import { CheckIcon, EditIcon, TrashIcon, UploadIcon } from '@pushwoosh/kit-icons';
6
6
  import { Text } from '@pushwoosh/kit-typography';
7
7
  import { useSPAContext } from '@pushwoosh/spa-context';
8
8
  import { CreateCropDrawer } from './CreateCropDrawer';
9
- import { getRatioLabel, getThumbUrl } from './mediaFile.utils';
9
+ import { useDeleteMediaFile } from './hooks';
10
+ import { formatBytes, getRatioLabel, getThumbUrl } from './mediaFile.utils';
10
11
  import { useLang } from './polyglot';
11
- import { CropBadge, Dropzone, EmptyState, Grid, HiddenInput, Tile, TileActions, TileImage } from './styled';
12
+ import { CropBadge, Dropzone, EmptyState, Grid, HiddenInput, Tile, TileActions, TileImage, UsageLine } from './styled';
12
13
  const UPLOAD_PATH = '/api/media/upload';
13
14
  /**
14
15
  * Controlled media gallery: the parent owns the current page (and page size)
@@ -34,18 +35,23 @@ export const MediaStore = ({
34
35
  const {
35
36
  openToast
36
37
  } = useToast();
38
+ const deleteMediaFile = useDeleteMediaFile(application);
37
39
  const uploadInputRef = useRef(null);
38
40
  // `uploadFiles` resolves asynchronously; read the latest page from the ref so
39
41
  // the post-upload decision isn't made on a value captured at click time.
40
42
  const pageRef = useRef(page);
41
43
  pageRef.current = page;
42
44
  const [cropsByUuid, setCropsByUuid] = useState({});
43
- // Bumped to force a reload of the current page (e.g. after an upload) when
44
- // the page prop itself does not change.
45
+ // Bumped to force a reload of the current page when the originals list changes
46
+ // without the page prop changing (upload, delete of an original).
45
47
  const [reloadToken, setReloadToken] = useState(0);
48
+ // Bumped on changes that affect storage usage but not the originals list
49
+ // (crop create/delete), so the usage line refreshes without refetching files.
50
+ const [usageToken, setUsageToken] = useState(0);
46
51
  const [isUploading, setIsUploading] = useState(false);
47
52
  const [isDragOver, setIsDragOver] = useState(false);
48
53
  const [createCropFor, setCreateCropFor] = useState(null);
54
+ const [pendingDelete, setPendingDelete] = useState(null);
49
55
  const listAsync = useAsync(async () => {
50
56
  const response = await rpcGateway.mediaFiles.List({
51
57
  application,
@@ -58,11 +64,17 @@ export const MediaStore = ({
58
64
  total: response.total
59
65
  };
60
66
  }, [rpcGateway, application, page, perPage, reloadToken]);
67
+ const usageAsync = useAsync(() => rpcGateway.mediaFiles.GetUsage({
68
+ application
69
+ }), [rpcGateway, application, reloadToken, usageToken]);
61
70
  const {
62
71
  loading
63
72
  } = listAsync;
64
73
  const files = ((_listAsync$value = listAsync.value) === null || _listAsync$value === void 0 ? void 0 : _listAsync$value.files) ?? [];
65
74
  const total = ((_listAsync$value2 = listAsync.value) === null || _listAsync$value2 === void 0 ? void 0 : _listAsync$value2.total) ?? 0;
75
+ // Read inside the async delete handler to avoid a stale-closure decision.
76
+ const filesCountRef = useRef(files.length);
77
+ filesCountRef.current = files.length;
66
78
  useEffect(() => {
67
79
  if (listAsync.error) {
68
80
  openToast({
@@ -129,13 +141,13 @@ export const MediaStore = ({
129
141
  }),
130
142
  type: 'success'
131
143
  });
132
- // Jump back to the first page to surface the freshly uploaded files. If
133
- // already there, the prop won't change, so bump the reload token too.
134
- if (pageRef.current === 0) {
135
- setReloadToken(token => token + 1);
136
- } else {
144
+ // Surface the freshly uploaded files on the first page. Bump the reload
145
+ // token unconditionally (refetches page 0 / refreshes usage); also
146
+ // navigate when we're not already there.
147
+ if (pageRef.current !== 0) {
137
148
  onPageChange(0);
138
149
  }
150
+ setReloadToken(token => token + 1);
139
151
  }
140
152
  if (failed > 0) {
141
153
  openToast({
@@ -157,8 +169,30 @@ export const MediaStore = ({
157
169
  ...prev,
158
170
  [parentUuid]: [...(prev[parentUuid] ?? []), crop]
159
171
  }));
172
+ setUsageToken(token => token + 1);
173
+ };
174
+ const handleCropDeleted = (parentUuid, cropUuid) => {
175
+ setCropsByUuid(prev => ({
176
+ ...prev,
177
+ [parentUuid]: (prev[parentUuid] ?? []).filter(crop => crop.uuid !== cropUuid)
178
+ }));
179
+ setUsageToken(token => token + 1);
180
+ };
181
+ const handleDeleteConfirmed = async () => {
182
+ const file = pendingDelete;
183
+ setPendingDelete(null);
184
+ if (!file) return;
185
+ if (!(await deleteMediaFile(file.uuid))) return;
186
+ // Deleting an original drops its crops too. If it was the last item on a
187
+ // non-first page, step back so we don't land on an empty page; the page
188
+ // change refetches the list. Otherwise refetch the current page.
189
+ if (filesCountRef.current === 1 && pageRef.current > 0) {
190
+ onPageChange(pageRef.current - 1);
191
+ }
192
+ setReloadToken(token => token + 1);
160
193
  };
161
194
  const totalPages = Math.ceil(total / perPage);
195
+ const showPagination = !loading && files.length > 0 && totalPages > 1;
162
196
  return _jsxs(_Fragment, {
163
197
  children: [_jsxs(Dropzone, {
164
198
  type: "button",
@@ -200,70 +234,81 @@ export const MediaStore = ({
200
234
  children: lang({
201
235
  message: 'SMART.MEDIA_STORE.EMPTY'
202
236
  })
203
- }), !loading && files.length > 0 && _jsxs(_Fragment, {
204
- children: [_jsx(Grid, {
205
- children: files.map(file => {
206
- const crops = cropsByUuid[file.uuid] ?? [];
207
- return _jsxs(Tile, {
208
- children: [_jsx(TileImage, {
209
- src: getThumbUrl(file),
210
- alt: file.filename
211
- }), crops.length > 0 && _jsx(CropBadge, {
212
- children: lang({
213
- message: 'SMART.MEDIA_STORE.CROPS_BADGE',
214
- count: crops.length
237
+ }), !loading && files.length > 0 && _jsx(Grid, {
238
+ children: files.map(file => {
239
+ const crops = cropsByUuid[file.uuid] ?? [];
240
+ return _jsxs(Tile, {
241
+ children: [_jsx(TileImage, {
242
+ src: getThumbUrl(file),
243
+ alt: file.filename
244
+ }), crops.length > 0 && _jsx(CropBadge, {
245
+ children: lang({
246
+ message: 'SMART.MEDIA_STORE.CROPS_BADGE',
247
+ count: crops.length
248
+ })
249
+ }), _jsxs(TileActions, {
250
+ children: [crops.length > 0 ? _jsx(DropdownMenuButton, {
251
+ color: "secondary",
252
+ size: "compact",
253
+ icon: _jsx(CheckIcon, {
254
+ size: "small"
255
+ }),
256
+ menuPlacement: "bottom-start",
257
+ children: _jsxs(DropdownMenuSection, {
258
+ children: [_jsx(DropdownMenuItem, {
259
+ onClick: () => onSelect(file.url),
260
+ children: lang({
261
+ message: 'SMART.MEDIA_STORE.SELECT_ORIGINAL'
262
+ })
263
+ }), crops.map(crop => _jsx(DropdownMenuItem, {
264
+ onClick: () => onSelect(crop.url),
265
+ children: getRatioLabel(crop) || crop.filename
266
+ }, crop.uuid))]
215
267
  })
216
- }), _jsxs(TileActions, {
217
- children: [crops.length > 0 ? _jsx(DropdownMenuButton, {
218
- color: "secondary",
219
- size: "compact",
220
- icon: _jsx(CheckIcon, {
221
- size: "small"
222
- }),
223
- menuPlacement: "bottom-start",
224
- children: _jsxs(DropdownMenuSection, {
225
- children: [_jsx(DropdownMenuItem, {
226
- onClick: () => onSelect(file.url),
227
- children: lang({
228
- message: 'SMART.MEDIA_STORE.SELECT_ORIGINAL'
229
- })
230
- }), crops.map(crop => _jsx(DropdownMenuItem, {
231
- onClick: () => onSelect(crop.url),
232
- children: getRatioLabel(crop) || crop.filename
233
- }, crop.uuid))]
234
- })
235
- }) : _jsx(Button, {
236
- color: "secondary",
237
- size: "compact",
238
- icon: _jsx(CheckIcon, {
239
- size: "small"
240
- }),
241
- "aria-label": lang({
242
- message: 'SMART.MEDIA_STORE.SELECT_HINT'
243
- }),
244
- onClick: () => onSelect(file.url)
245
- }), _jsx(Button, {
246
- color: "secondary",
247
- size: "compact",
248
- icon: _jsx(EditIcon, {
249
- size: "small",
250
- view: "lined"
251
- }),
252
- "aria-label": lang({
253
- message: 'SMART.MEDIA_STORE.CREATE_CROP_HINT'
254
- }),
255
- onClick: () => setCreateCropFor(file)
256
- })]
268
+ }) : _jsx(Button, {
269
+ color: "secondary",
270
+ size: "compact",
271
+ icon: _jsx(CheckIcon, {
272
+ size: "small"
273
+ }),
274
+ "aria-label": lang({
275
+ message: 'SMART.MEDIA_STORE.SELECT_HINT'
276
+ }),
277
+ onClick: () => onSelect(file.url)
278
+ }), _jsx(Button, {
279
+ color: "secondary",
280
+ size: "compact",
281
+ icon: _jsx(EditIcon, {
282
+ size: "small",
283
+ view: "lined"
284
+ }),
285
+ "aria-label": lang({
286
+ message: 'SMART.MEDIA_STORE.CREATE_CROP_HINT'
287
+ }),
288
+ onClick: () => setCreateCropFor(file)
289
+ }), _jsx(Button, {
290
+ color: "secondary",
291
+ size: "compact",
292
+ icon: _jsx(TrashIcon, {
293
+ size: "small",
294
+ view: "lined"
295
+ }),
296
+ "aria-label": lang({
297
+ message: 'SMART.MEDIA_STORE.DELETE_HINT'
298
+ }),
299
+ onClick: () => setPendingDelete(file)
257
300
  })]
258
- }, file.uuid);
259
- })
260
- }), totalPages > 1 && _jsxs(Horizontal, {
261
- "$justifyContent": "center",
301
+ })]
302
+ }, file.uuid);
303
+ })
304
+ }), (showPagination || usageAsync.value) && _jsxs(FlexContainer, {
305
+ "$alignItems": "center",
306
+ "$margin": {
307
+ top: 16
308
+ },
309
+ children: [showPagination && _jsxs(FlexContainer, {
262
310
  "$alignItems": "center",
263
311
  "$gap": 8,
264
- "$margin": {
265
- top: 16
266
- },
267
312
  children: [_jsx(Button, {
268
313
  view: "ghost",
269
314
  color: "secondary",
@@ -285,6 +330,12 @@ export const MediaStore = ({
285
330
  message: 'SMART.MEDIA_STORE.NEXT_BUTTON'
286
331
  })
287
332
  })]
333
+ }), usageAsync.value && _jsx(UsageLine, {
334
+ children: lang({
335
+ message: 'SMART.MEDIA_STORE.USAGE',
336
+ count: usageAsync.value.totalCount,
337
+ size: formatBytes(usageAsync.value.totalSize)
338
+ })
288
339
  })]
289
340
  }), _jsx(CreateCropDrawer, {
290
341
  application: application,
@@ -292,7 +343,22 @@ export const MediaStore = ({
292
343
  isOpen: createCropFor !== null,
293
344
  existingCrops: createCropFor ? cropsByUuid[createCropFor.uuid] ?? [] : [],
294
345
  onClose: () => setCreateCropFor(null),
295
- onCreated: handleCropCreated
346
+ onCreated: handleCropCreated,
347
+ onDeleted: handleCropDeleted
348
+ }), _jsx(ModalConfirm, {
349
+ isOpen: pendingDelete !== null,
350
+ title: lang({
351
+ message: 'SMART.MEDIA_STORE.DELETE_CONFIRM.TITLE'
352
+ }),
353
+ description: lang({
354
+ message: 'SMART.MEDIA_STORE.DELETE_CONFIRM.DESCRIPTION'
355
+ }),
356
+ confirmButtonText: lang({
357
+ message: 'SMART.MEDIA_STORE.DELETE_CONFIRM.BUTTON'
358
+ }),
359
+ confirmButtonColor: "danger",
360
+ onAccept: () => void handleDeleteConfirmed(),
361
+ onCancel: () => setPendingDelete(null)
296
362
  })]
297
363
  });
298
364
  };
@@ -0,0 +1,7 @@
1
+ /**
2
+ * Returns a function that deletes a media file (original or crop) and shows the
3
+ * success/error toast. Resolves to `true` on success so the caller can run its
4
+ * own post-delete routing (refetch, state update, pagination), `false` on
5
+ * failure. Both `MediaStore` and `CreateCropDrawer` share this.
6
+ */
7
+ export declare function useDeleteMediaFile(application: string): (uuid: string) => Promise<boolean>;
@@ -0,0 +1,42 @@
1
+ import { useToast } from '@pushwoosh/dumb-components';
2
+ import { usePersistentFunction } from '@pushwoosh/kit-helpers';
3
+ import { useSPAContext } from '@pushwoosh/spa-context';
4
+ import { useLang } from './polyglot';
5
+ /**
6
+ * Returns a function that deletes a media file (original or crop) and shows the
7
+ * success/error toast. Resolves to `true` on success so the caller can run its
8
+ * own post-delete routing (refetch, state update, pagination), `false` on
9
+ * failure. Both `MediaStore` and `CreateCropDrawer` share this.
10
+ */
11
+ export function useDeleteMediaFile(application) {
12
+ const {
13
+ rpcGateway
14
+ } = useSPAContext();
15
+ const {
16
+ openToast
17
+ } = useToast();
18
+ const lang = useLang();
19
+ return usePersistentFunction(async uuid => {
20
+ try {
21
+ await rpcGateway.mediaFiles.Delete({
22
+ application,
23
+ uuid
24
+ });
25
+ openToast({
26
+ title: lang({
27
+ message: 'SMART.MEDIA_STORE.DELETE_SUCCESS'
28
+ }),
29
+ type: 'success'
30
+ });
31
+ return true;
32
+ } catch (error) {
33
+ openToast({
34
+ title: error.message || lang({
35
+ message: 'SMART.MEDIA_STORE.DELETE_ERROR'
36
+ }),
37
+ type: 'error'
38
+ });
39
+ return false;
40
+ }
41
+ });
42
+ }
@@ -3,3 +3,5 @@ import type { MediaFile } from '@pushwoosh/rpc-v2-http-api-data/media_files';
3
3
  export declare const getThumbUrl: (file: MediaFile) => string;
4
4
  /** Aspect-ratio label (e.g. "16:9") for a crop; empty for originals. */
5
5
  export declare const getRatioLabel: (file: MediaFile) => string;
6
+ /** Human-readable byte size, e.g. 4404019 -> "4.2 MB". */
7
+ export declare const formatBytes: (bytes: number) => string;
@@ -7,4 +7,15 @@ export const getThumbUrl = file => {
7
7
  export const getRatioLabel = file => {
8
8
  var _file$meta2;
9
9
  return ((_file$meta2 = file.meta) === null || _file$meta2 === void 0 ? void 0 : _file$meta2.type) === 'imageMeta' ? file.meta.data.ratio : '';
10
+ };
11
+ const BYTE_UNITS = ['B', 'KB', 'MB', 'GB', 'TB'];
12
+ /** Human-readable byte size, e.g. 4404019 -> "4.2 MB". */
13
+ export const formatBytes = bytes => {
14
+ if (bytes === 0) return '0 B';
15
+ if (!Number.isFinite(bytes) || bytes < 0) return '—';
16
+ const exp = Math.min(Math.floor(Math.log(bytes) / Math.log(1024)), BYTE_UNITS.length - 1);
17
+ const value = bytes / 1024 ** exp;
18
+ // Whole numbers for bytes and for values >= 100; one decimal otherwise.
19
+ const rounded = exp === 0 || value >= 100 ? Math.round(value) : Math.round(value * 10) / 10;
20
+ return `${rounded} ${BYTE_UNITS[exp]}`;
10
21
  };
@@ -9,9 +9,16 @@ export const messages = {
9
9
  'SMART.MEDIA_STORE.LOAD_ERROR': 'Failed to load media files',
10
10
  'SMART.MEDIA_STORE.UPLOAD_ERROR': 'Failed to upload image',
11
11
  'SMART.MEDIA_STORE.UPLOAD_SUCCESS': 'Image uploaded',
12
+ 'SMART.MEDIA_STORE.USAGE': '{{count}} files · {{size}}',
12
13
  'SMART.MEDIA_STORE.CROPS_BADGE': '{{count}} cr.',
13
14
  'SMART.MEDIA_STORE.SELECT_ORIGINAL': 'Original',
14
15
  'SMART.MEDIA_STORE.CREATE_CROP_HINT': 'Create crops',
16
+ 'SMART.MEDIA_STORE.DELETE_HINT': 'Delete',
17
+ 'SMART.MEDIA_STORE.DELETE_SUCCESS': 'Deleted',
18
+ 'SMART.MEDIA_STORE.DELETE_ERROR': 'Failed to delete',
19
+ 'SMART.MEDIA_STORE.DELETE_CONFIRM.TITLE': 'Delete this file?',
20
+ 'SMART.MEDIA_STORE.DELETE_CONFIRM.DESCRIPTION': 'Deleting the original also removes all of its crops. This can’t be undone.',
21
+ 'SMART.MEDIA_STORE.DELETE_CONFIRM.BUTTON': 'Delete',
15
22
  'SMART.MEDIA_STORE.CREATE_CROP.TITLE': 'Create crop',
16
23
  'SMART.MEDIA_STORE.CREATE_CROP.CLOSE': 'Close',
17
24
  'SMART.MEDIA_STORE.CREATE_CROP.RATIO_LABEL': 'Aspect ratio',
@@ -24,5 +31,8 @@ export const messages = {
24
31
  'SMART.MEDIA_STORE.CREATE_CROP.EXISTING_LABEL': 'Existing crops',
25
32
  'SMART.MEDIA_STORE.CREATE_CROP.NO_CROPS': 'No crops yet',
26
33
  'SMART.MEDIA_STORE.CREATE_CROP.SUCCESS': 'Crop created',
27
- 'SMART.MEDIA_STORE.CREATE_CROP.ERROR': 'Failed to create crop'
34
+ 'SMART.MEDIA_STORE.CREATE_CROP.ERROR': 'Failed to create crop',
35
+ 'SMART.MEDIA_STORE.CREATE_CROP.DELETE_HINT': 'Delete crop',
36
+ 'SMART.MEDIA_STORE.CREATE_CROP.DELETE_CONFIRM.TITLE': 'Delete this crop?',
37
+ 'SMART.MEDIA_STORE.CREATE_CROP.DELETE_CONFIRM.BUTTON': 'Delete'
28
38
  };
@@ -9,9 +9,16 @@ export const messages = {
9
9
  'SMART.MEDIA_STORE.LOAD_ERROR': 'Не удалось загрузить медиафайлы',
10
10
  'SMART.MEDIA_STORE.UPLOAD_ERROR': 'Не удалось загрузить изображение',
11
11
  'SMART.MEDIA_STORE.UPLOAD_SUCCESS': 'Изображение загружено',
12
+ 'SMART.MEDIA_STORE.USAGE': '{{count}} файлов · {{size}}',
12
13
  'SMART.MEDIA_STORE.CROPS_BADGE': '{{count}} кр.',
13
14
  'SMART.MEDIA_STORE.SELECT_ORIGINAL': 'Оригинал',
14
15
  'SMART.MEDIA_STORE.CREATE_CROP_HINT': 'Создать кропы',
16
+ 'SMART.MEDIA_STORE.DELETE_HINT': 'Удалить',
17
+ 'SMART.MEDIA_STORE.DELETE_SUCCESS': 'Удалено',
18
+ 'SMART.MEDIA_STORE.DELETE_ERROR': 'Не удалось удалить',
19
+ 'SMART.MEDIA_STORE.DELETE_CONFIRM.TITLE': 'Удалить файл?',
20
+ 'SMART.MEDIA_STORE.DELETE_CONFIRM.DESCRIPTION': 'Удаление оригинала также удалит все его кропы. Действие необратимо.',
21
+ 'SMART.MEDIA_STORE.DELETE_CONFIRM.BUTTON': 'Удалить',
15
22
  'SMART.MEDIA_STORE.CREATE_CROP.TITLE': 'Создание кропа',
16
23
  'SMART.MEDIA_STORE.CREATE_CROP.CLOSE': 'Закрыть',
17
24
  'SMART.MEDIA_STORE.CREATE_CROP.RATIO_LABEL': 'Соотношение сторон',
@@ -24,5 +31,8 @@ export const messages = {
24
31
  'SMART.MEDIA_STORE.CREATE_CROP.EXISTING_LABEL': 'Созданные кропы',
25
32
  'SMART.MEDIA_STORE.CREATE_CROP.NO_CROPS': 'Кропов пока нет',
26
33
  'SMART.MEDIA_STORE.CREATE_CROP.SUCCESS': 'Кроп создан',
27
- 'SMART.MEDIA_STORE.CREATE_CROP.ERROR': 'Не удалось создать кроп'
34
+ 'SMART.MEDIA_STORE.CREATE_CROP.ERROR': 'Не удалось создать кроп',
35
+ 'SMART.MEDIA_STORE.CREATE_CROP.DELETE_HINT': 'Удалить кроп',
36
+ 'SMART.MEDIA_STORE.CREATE_CROP.DELETE_CONFIRM.TITLE': 'Удалить кроп?',
37
+ 'SMART.MEDIA_STORE.CREATE_CROP.DELETE_CONFIRM.BUTTON': 'Удалить'
28
38
  };
@@ -10,11 +10,21 @@ export type MediaStoreMessages = {
10
10
  'SMART.MEDIA_STORE.LOAD_ERROR': NoParams;
11
11
  'SMART.MEDIA_STORE.UPLOAD_ERROR': NoParams;
12
12
  'SMART.MEDIA_STORE.UPLOAD_SUCCESS': NoParams;
13
+ 'SMART.MEDIA_STORE.USAGE': {
14
+ count: number;
15
+ size: string;
16
+ };
13
17
  'SMART.MEDIA_STORE.CROPS_BADGE': {
14
18
  count: number;
15
19
  };
16
20
  'SMART.MEDIA_STORE.SELECT_ORIGINAL': NoParams;
17
21
  'SMART.MEDIA_STORE.CREATE_CROP_HINT': NoParams;
22
+ 'SMART.MEDIA_STORE.DELETE_HINT': NoParams;
23
+ 'SMART.MEDIA_STORE.DELETE_SUCCESS': NoParams;
24
+ 'SMART.MEDIA_STORE.DELETE_ERROR': NoParams;
25
+ 'SMART.MEDIA_STORE.DELETE_CONFIRM.TITLE': NoParams;
26
+ 'SMART.MEDIA_STORE.DELETE_CONFIRM.DESCRIPTION': NoParams;
27
+ 'SMART.MEDIA_STORE.DELETE_CONFIRM.BUTTON': NoParams;
18
28
  'SMART.MEDIA_STORE.CREATE_CROP.TITLE': NoParams;
19
29
  'SMART.MEDIA_STORE.CREATE_CROP.CLOSE': NoParams;
20
30
  'SMART.MEDIA_STORE.CREATE_CROP.RATIO_LABEL': NoParams;
@@ -28,4 +38,7 @@ export type MediaStoreMessages = {
28
38
  'SMART.MEDIA_STORE.CREATE_CROP.NO_CROPS': NoParams;
29
39
  'SMART.MEDIA_STORE.CREATE_CROP.SUCCESS': NoParams;
30
40
  'SMART.MEDIA_STORE.CREATE_CROP.ERROR': NoParams;
41
+ 'SMART.MEDIA_STORE.CREATE_CROP.DELETE_HINT': NoParams;
42
+ 'SMART.MEDIA_STORE.CREATE_CROP.DELETE_CONFIRM.TITLE': NoParams;
43
+ 'SMART.MEDIA_STORE.CREATE_CROP.DELETE_CONFIRM.BUTTON': NoParams;
31
44
  };
@@ -8,6 +8,7 @@ export declare const Dropzone: import("styled-components").StyledComponent<"butt
8
8
  $dragOver?: boolean;
9
9
  }, never>;
10
10
  export declare const HiddenInput: import("styled-components").StyledComponent<"input", any, {}, never>;
11
+ export declare const UsageLine: import("styled-components").StyledComponent<"div", any, {}, never>;
11
12
  export declare const PreviewStage: import("styled-components").StyledComponent<"div", any, {}, never>;
12
13
  export declare const PreviewFrame: import("styled-components").StyledComponent<"div", any, {
13
14
  $ratio: number;
@@ -24,4 +25,5 @@ export declare const SegmentItem: import("styled-components").StyledComponent<"b
24
25
  }, never>;
25
26
  export declare const CropStrip: import("styled-components").StyledComponent<"div", any, {}, never>;
26
27
  export declare const CropThumb: import("styled-components").StyledComponent<"div", any, {}, never>;
28
+ export declare const CropThumbDelete: import("styled-components").StyledComponent<"div", any, {}, never>;
27
29
  export declare const CropThumbCaption: import("styled-components").StyledComponent<"div", any, {}, never>;
@@ -1,5 +1,5 @@
1
1
  import styled from 'styled-components';
2
- import { Color, FontWeight } from '@pushwoosh/kit-constants';
2
+ import { Color, FontSize, FontWeight } from '@pushwoosh/kit-constants';
3
3
  export const Grid = styled.div.withConfig({
4
4
  displayName: "Grid",
5
5
  componentId: "sc-1d9kmgw-0"
@@ -36,32 +36,36 @@ export const HiddenInput = styled.input.withConfig({
36
36
  displayName: "HiddenInput",
37
37
  componentId: "sc-1d9kmgw-7"
38
38
  })(["display:none;"]);
39
+ export const UsageLine = styled.div.withConfig({
40
+ displayName: "UsageLine",
41
+ componentId: "sc-1d9kmgw-8"
42
+ })(["margin-left:auto;font-size:", ";color:", ";"], FontSize.SMALL, Color.LOCKED);
39
43
  /* ---------- CreateCropDrawer ---------- */
40
44
  export const PreviewStage = styled.div.withConfig({
41
45
  displayName: "PreviewStage",
42
- componentId: "sc-1d9kmgw-8"
46
+ componentId: "sc-1d9kmgw-9"
43
47
  })(["display:flex;align-items:center;justify-content:center;min-height:200px;max-height:360px;padding:12px;border:1px solid ", ";border-radius:6px;overflow:hidden;background:", ";"], Color.DIVIDER, Color.SOFT_LIGHT);
44
48
  export const PreviewFrame = styled.div.withConfig({
45
49
  displayName: "PreviewFrame",
46
- componentId: "sc-1d9kmgw-9"
50
+ componentId: "sc-1d9kmgw-10"
47
51
  })(["aspect-ratio:", ";max-width:100%;max-height:336px;overflow:hidden;border-radius:4px;"], ({
48
52
  $ratio
49
53
  }) => $ratio);
50
54
  export const PreviewImage = styled.img.withConfig({
51
55
  displayName: "PreviewImage",
52
- componentId: "sc-1d9kmgw-10"
56
+ componentId: "sc-1d9kmgw-11"
53
57
  })(["width:100%;height:100%;object-fit:cover;display:block;"]);
54
58
  export const FieldLabel = styled.div.withConfig({
55
59
  displayName: "FieldLabel",
56
- componentId: "sc-1d9kmgw-11"
60
+ componentId: "sc-1d9kmgw-12"
57
61
  })(["font-size:11px;font-weight:600;text-transform:uppercase;letter-spacing:0.4px;color:", ";margin-bottom:6px;"], Color.LOCKED);
58
62
  export const Chips = styled.div.withConfig({
59
63
  displayName: "Chips",
60
- componentId: "sc-1d9kmgw-12"
64
+ componentId: "sc-1d9kmgw-13"
61
65
  })(["display:flex;flex-wrap:wrap;gap:7px;"]);
62
66
  export const Chip = styled.button.withConfig({
63
67
  displayName: "Chip",
64
- componentId: "sc-1d9kmgw-13"
68
+ componentId: "sc-1d9kmgw-14"
65
69
  })(["font:inherit;font-size:13px;cursor:pointer;padding:6px 12px;border-radius:20px;border:1px solid ", ";background:", ";color:", ";transition:background 0.12s,border-color 0.12s;"], ({
66
70
  $active
67
71
  }) => $active ? Color.PRIMARY : Color.DIVIDER, ({
@@ -71,11 +75,11 @@ export const Chip = styled.button.withConfig({
71
75
  }) => $active ? Color.CLEAR : Color.MAIN);
72
76
  export const Segment = styled.div.withConfig({
73
77
  displayName: "Segment",
74
- componentId: "sc-1d9kmgw-14"
78
+ componentId: "sc-1d9kmgw-15"
75
79
  })(["display:inline-flex;border:1px solid ", ";border-radius:6px;overflow:hidden;"], Color.DIVIDER);
76
80
  export const SegmentItem = styled.button.withConfig({
77
81
  displayName: "SegmentItem",
78
- componentId: "sc-1d9kmgw-15"
82
+ componentId: "sc-1d9kmgw-16"
79
83
  })(["font:inherit;font-size:13px;cursor:pointer;padding:7px 14px;border:none;background:", ";color:", ";& + &{border-left:1px solid ", ";}"], ({
80
84
  $active
81
85
  }) => $active ? Color.PRIMARY : Color.CLEAR, ({
@@ -83,13 +87,17 @@ export const SegmentItem = styled.button.withConfig({
83
87
  }) => $active ? Color.CLEAR : Color.MAIN, Color.DIVIDER);
84
88
  export const CropStrip = styled.div.withConfig({
85
89
  displayName: "CropStrip",
86
- componentId: "sc-1d9kmgw-16"
90
+ componentId: "sc-1d9kmgw-17"
87
91
  })(["display:flex;gap:10px;flex-wrap:wrap;align-items:flex-start;"]);
88
92
  export const CropThumb = styled.div.withConfig({
89
93
  displayName: "CropThumb",
90
- componentId: "sc-1d9kmgw-17"
91
- })(["width:88px;border:1px solid ", ";border-radius:6px;overflow:hidden;background:", ";"], Color.DIVIDER, Color.SOFT_LIGHT);
94
+ componentId: "sc-1d9kmgw-18"
95
+ })(["position:relative;width:88px;border:1px solid ", ";border-radius:6px;overflow:hidden;background:", ";"], Color.DIVIDER, Color.SOFT_LIGHT);
96
+ export const CropThumbDelete = styled.div.withConfig({
97
+ displayName: "CropThumbDelete",
98
+ componentId: "sc-1d9kmgw-19"
99
+ })(["position:absolute;top:4px;right:4px;opacity:0;transition:opacity 0.15s;", ":hover &{opacity:1;}@media (hover:none){opacity:1;}"], CropThumb);
92
100
  export const CropThumbCaption = styled.div.withConfig({
93
101
  displayName: "CropThumbCaption",
94
- componentId: "sc-1d9kmgw-18"
102
+ componentId: "sc-1d9kmgw-20"
95
103
  })(["font-size:10px;text-align:center;padding:3px 2px;color:", ";"], Color.LOCKED);
package/package.json CHANGED
@@ -23,10 +23,10 @@
23
23
  "@floating-ui/react-dom": "^2.1.2",
24
24
  "@pushwoosh/dumb-components": "^1.1.134",
25
25
  "@pushwoosh/kit-constants": "^1.6.9",
26
- "@pushwoosh/kit-helpers": "^4.9.1",
26
+ "@pushwoosh/kit-helpers": "^4.11.0",
27
27
  "@pushwoosh/kit-icons": "^2.1.6",
28
28
  "@pushwoosh/kit-typography": "^1.7.3",
29
- "@pushwoosh/rpc-v2-http-api-data": "^0.1.974",
29
+ "@pushwoosh/rpc-v2-http-api-data": "^0.1.980",
30
30
  "@pushwoosh/spa-context": "^1.1.2",
31
31
  "idb-keyval": "^6.2.2",
32
32
  "react-hook-form": "^7.68.0",
@@ -49,5 +49,5 @@
49
49
  "react": "^16.14.0 || ^18.0.0",
50
50
  "react-dom": "^16.14.0 || ^18.0.0"
51
51
  },
52
- "version": "1.1.2"
52
+ "version": "1.1.4"
53
53
  }