@pushwoosh/smart-components-kit 1.1.1 → 1.1.3

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.
@@ -0,0 +1,13 @@
1
+ import { type FC } from 'react';
2
+ import type { MediaFile } from '@pushwoosh/rpc-v2-http-api-data/media_files';
3
+ type CreateCropDrawerProps = {
4
+ application: string;
5
+ original: MediaFile | null;
6
+ isOpen: boolean;
7
+ existingCrops: MediaFile[];
8
+ onClose: () => void;
9
+ onCreated: (parentUuid: string, crop: MediaFile) => void;
10
+ onDeleted: (parentUuid: string, cropUuid: string) => void;
11
+ };
12
+ export declare const CreateCropDrawer: FC<CreateCropDrawerProps>;
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, FieldLabel, PreviewFrame, PreviewImage, PreviewStage, Segment, SegmentItem, TileImage, VariantStrip, VariantThumb, VariantThumbCaption } 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',
@@ -48,13 +49,14 @@ const RATIOS = [{
48
49
  const DEFAULT_RATIO = 'ASPECT_RATIO_16_9';
49
50
  const DEFAULT_SIZE = '1080';
50
51
  const DEFAULT_QUALITY = '90';
51
- export const CreateVariantDrawer = ({
52
+ export const CreateCropDrawer = ({
52
53
  application,
53
54
  original,
54
55
  isOpen,
55
- existingVariants,
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 CreateVariantDrawer = ({
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.
@@ -87,14 +91,14 @@ export const CreateVariantDrawer = ({
87
91
  const selectedRatio = RATIOS.find(item => item.value === ratio) ?? RATIOS[0];
88
92
  const outputWidth = dimension === 'width' ? sizeValue : Math.round(sizeValue * selectedRatio.ar);
89
93
  const outputHeight = dimension === 'height' ? sizeValue : Math.round(sizeValue / selectedRatio.ar);
90
- const ratioExists = existingVariants.some(variant => {
91
- var _variant$meta;
92
- return ((_variant$meta = variant.meta) === null || _variant$meta === void 0 ? void 0 : _variant$meta.type) === 'imageMeta' && variant.meta.data.ratio === selectedRatio.label;
94
+ const ratioExists = existingCrops.some(crop => {
95
+ var _crop$meta;
96
+ return ((_crop$meta = crop.meta) === null || _crop$meta === void 0 ? void 0 : _crop$meta.type) === 'imageMeta' && crop.meta.data.ratio === selectedRatio.label;
93
97
  });
94
98
  const handleCreate = async () => {
95
99
  if (!original || !isSizeValid || !isQualityValid || ratioExists) return;
96
100
  // Capture the parent up front so a concurrent reopen for another image
97
- // can't misroute the created variant.
101
+ // can't misroute the created crop.
98
102
  const parent = original;
99
103
  setIsCreating(true);
100
104
  try {
@@ -112,7 +116,7 @@ export const CreateVariantDrawer = ({
112
116
  });
113
117
  openToast({
114
118
  title: lang({
115
- message: 'SMART.MEDIA_STORE.CREATE_VARIANT.SUCCESS'
119
+ message: 'SMART.MEDIA_STORE.CREATE_CROP.SUCCESS'
116
120
  }),
117
121
  type: 'success'
118
122
  });
@@ -120,7 +124,7 @@ export const CreateVariantDrawer = ({
120
124
  } catch (error) {
121
125
  openToast({
122
126
  title: error.message || lang({
123
- message: 'SMART.MEDIA_STORE.CREATE_VARIANT.ERROR'
127
+ message: 'SMART.MEDIA_STORE.CREATE_CROP.ERROR'
124
128
  }),
125
129
  type: 'error'
126
130
  });
@@ -128,6 +132,17 @@ export const CreateVariantDrawer = ({
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,
@@ -136,7 +151,7 @@ export const CreateVariantDrawer = ({
136
151
  children: _jsxs(DrawerTitle, {
137
152
  children: [_jsx("span", {
138
153
  children: lang({
139
- message: 'SMART.MEDIA_STORE.CREATE_VARIANT.TITLE'
154
+ message: 'SMART.MEDIA_STORE.CREATE_CROP.TITLE'
140
155
  })
141
156
  }), _jsx(Button, {
142
157
  view: "ghost",
@@ -146,7 +161,7 @@ export const CreateVariantDrawer = ({
146
161
  size: "small"
147
162
  }),
148
163
  "aria-label": lang({
149
- message: 'SMART.MEDIA_STORE.CREATE_VARIANT.CLOSE'
164
+ message: 'SMART.MEDIA_STORE.CREATE_CROP.CLOSE'
150
165
  }),
151
166
  onClick: onClose
152
167
  })]
@@ -157,7 +172,7 @@ export const CreateVariantDrawer = ({
157
172
  children: [_jsxs("div", {
158
173
  children: [_jsx(FieldLabel, {
159
174
  children: lang({
160
- message: 'SMART.MEDIA_STORE.CREATE_VARIANT.RATIO_LABEL'
175
+ message: 'SMART.MEDIA_STORE.CREATE_CROP.RATIO_LABEL'
161
176
  })
162
177
  }), _jsx(Chips, {
163
178
  children: RATIOS.map(item => _jsx(Chip, {
@@ -170,7 +185,7 @@ export const CreateVariantDrawer = ({
170
185
  }), _jsxs("div", {
171
186
  children: [_jsx(FieldLabel, {
172
187
  children: lang({
173
- message: 'SMART.MEDIA_STORE.CREATE_VARIANT.SIZE_LABEL'
188
+ message: 'SMART.MEDIA_STORE.CREATE_CROP.SIZE_LABEL'
174
189
  })
175
190
  }), _jsxs(Columns, {
176
191
  "$sizes": "auto 1fr",
@@ -182,14 +197,14 @@ export const CreateVariantDrawer = ({
182
197
  "$active": dimension === 'width',
183
198
  onClick: () => setDimension('width'),
184
199
  children: lang({
185
- message: 'SMART.MEDIA_STORE.CREATE_VARIANT.WIDTH'
200
+ message: 'SMART.MEDIA_STORE.CREATE_CROP.WIDTH'
186
201
  })
187
202
  }), _jsx(SegmentItem, {
188
203
  type: "button",
189
204
  "$active": dimension === 'height',
190
205
  onClick: () => setDimension('height'),
191
206
  children: lang({
192
- message: 'SMART.MEDIA_STORE.CREATE_VARIANT.HEIGHT'
207
+ message: 'SMART.MEDIA_STORE.CREATE_CROP.HEIGHT'
193
208
  })
194
209
  })]
195
210
  }), _jsx(NativeInput, {
@@ -216,7 +231,7 @@ export const CreateVariantDrawer = ({
216
231
  }), isJpeg && _jsxs("div", {
217
232
  children: [_jsx(FieldLabel, {
218
233
  children: lang({
219
- message: 'SMART.MEDIA_STORE.CREATE_VARIANT.QUALITY_LABEL'
234
+ message: 'SMART.MEDIA_STORE.CREATE_CROP.QUALITY_LABEL'
220
235
  })
221
236
  }), _jsx(NativeInput, {
222
237
  type: "number",
@@ -228,7 +243,7 @@ export const CreateVariantDrawer = ({
228
243
  })]
229
244
  }), ratioExists && _jsx(Text, {
230
245
  children: lang({
231
- message: 'SMART.MEDIA_STORE.CREATE_VARIANT.RATIO_EXISTS'
246
+ message: 'SMART.MEDIA_STORE.CREATE_CROP.RATIO_EXISTS'
232
247
  })
233
248
  }), _jsx(Button, {
234
249
  color: "primary",
@@ -237,29 +252,53 @@ export const CreateVariantDrawer = ({
237
252
  isLoading: isCreating,
238
253
  onClick: () => void handleCreate(),
239
254
  children: lang({
240
- message: 'SMART.MEDIA_STORE.CREATE_VARIANT.CREATE_BUTTON'
255
+ message: 'SMART.MEDIA_STORE.CREATE_CROP.CREATE_BUTTON'
241
256
  })
242
257
  }), _jsxs("div", {
243
258
  children: [_jsx(FieldLabel, {
244
259
  children: lang({
245
- message: 'SMART.MEDIA_STORE.CREATE_VARIANT.EXISTING_LABEL'
260
+ message: 'SMART.MEDIA_STORE.CREATE_CROP.EXISTING_LABEL'
246
261
  })
247
- }), existingVariants.length === 0 ? _jsx(Text, {
262
+ }), existingCrops.length === 0 ? _jsx(Text, {
248
263
  children: lang({
249
- message: 'SMART.MEDIA_STORE.CREATE_VARIANT.NO_VARIANTS'
264
+ message: 'SMART.MEDIA_STORE.CREATE_CROP.NO_CROPS'
250
265
  })
251
- }) : _jsx(VariantStrip, {
252
- children: existingVariants.map(variant => _jsxs(VariantThumb, {
266
+ }) : _jsx(CropStrip, {
267
+ children: existingCrops.map(crop => _jsxs(CropThumb, {
253
268
  children: [_jsx(TileImage, {
254
- src: getThumbUrl(variant),
255
- alt: variant.filename
256
- }), _jsx(VariantThumbCaption, {
257
- children: getRatioLabel(variant) || variant.filename
269
+ src: getThumbUrl(crop),
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
+ })
284
+ }), _jsx(CropThumbCaption, {
285
+ children: getRatioLabel(crop) || crop.filename
258
286
  })]
259
- }, variant.uuid))
287
+ }, crop.uuid))
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
  };
@@ -3,6 +3,6 @@ import type { MediaStoreProps } from './types';
3
3
  /**
4
4
  * Controlled media gallery: the parent owns the current page (and page size)
5
5
  * and is notified of navigation via `onPageChange`. For a self-contained
6
- * variant that keeps the page in internal state, use `MediaStorePaginated`.
6
+ * version that keeps the page in internal state, use `MediaStorePaginated`.
7
7
  */
8
8
  export declare const MediaStore: FC<MediaStoreProps>;
@@ -1,19 +1,20 @@
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';
3
+ import { Button, DropdownMenuButton, DropdownMenuItem, DropdownMenuSection, ModalConfirm, useToast } from '@pushwoosh/dumb-components';
4
4
  import { Horizontal, useAsync } from '@pushwoosh/kit-helpers';
5
- import { CheckIcon, EditIcon, UploadIcon } from '@pushwoosh/kit-icons';
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
- import { CreateVariantDrawer } from './CreateVariantDrawer';
9
- import { getRatioLabel, getThumbUrl } from './mediaFile.utils';
8
+ import { CreateCropDrawer } from './CreateCropDrawer';
9
+ import { useDeleteMediaFile } from './hooks';
10
+ import { formatBytes, getRatioLabel, getThumbUrl } from './mediaFile.utils';
10
11
  import { useLang } from './polyglot';
11
- import { Dropzone, EmptyState, Grid, HiddenInput, Tile, TileActions, TileImage, VariantBadge } 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)
15
16
  * and is notified of navigation via `onPageChange`. For a self-contained
16
- * variant that keeps the page in internal state, use `MediaStorePaginated`.
17
+ * version that keeps the page in internal state, use `MediaStorePaginated`.
17
18
  */
18
19
  export const MediaStore = ({
19
20
  application,
@@ -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
- const [variantsByUuid, setVariantsByUuid] = 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.
44
+ const [cropsByUuid, setCropsByUuid] = useState({});
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
- const [createVariantFor, setCreateVariantFor] = useState(null);
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({
@@ -73,15 +85,15 @@ export const MediaStore = ({
73
85
  });
74
86
  }
75
87
  }, [listAsync.error, openToast, lang]);
76
- // The List of originals carries no variant count, so once a page of originals
77
- // resolves, fetch each one's variants in the background and let the
78
- // "N variants" badge appear per file as soon as its request resolves. The
88
+ // The List of originals carries no crop count, so once a page of originals
89
+ // resolves, fetch each one's crops in the background and let the
90
+ // "N crops" badge appear per file as soon as its request resolves. The
79
91
  // cleanup flag drops results of a page that has since been replaced.
80
92
  useEffect(() => {
81
93
  var _listAsync$value3;
82
94
  const originals = (_listAsync$value3 = listAsync.value) === null || _listAsync$value3 === void 0 ? void 0 : _listAsync$value3.files;
83
95
  if (!originals) return undefined;
84
- setVariantsByUuid({});
96
+ setCropsByUuid({});
85
97
  let cancelled = false;
86
98
  originals.forEach(file => {
87
99
  void (async () => {
@@ -91,7 +103,7 @@ export const MediaStore = ({
91
103
  parentUuid: file.uuid
92
104
  });
93
105
  if (cancelled) return;
94
- setVariantsByUuid(prev => ({
106
+ setCropsByUuid(prev => ({
95
107
  ...prev,
96
108
  [file.uuid]: response.files ?? []
97
109
  }));
@@ -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({
@@ -152,15 +164,42 @@ export const MediaStore = ({
152
164
  setIsUploading(false);
153
165
  }
154
166
  };
155
- const handleVariantCreated = (parentUuid, variant) => {
156
- setVariantsByUuid(prev => ({
167
+ const handleCropCreated = (parentUuid, crop) => {
168
+ setCropsByUuid(prev => ({
157
169
  ...prev,
158
- [parentUuid]: [...(prev[parentUuid] ?? []), variant]
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);
162
195
  return _jsxs(_Fragment, {
163
- children: [_jsxs(Dropzone, {
196
+ children: [usageAsync.value && _jsx(UsageLine, {
197
+ children: lang({
198
+ message: 'SMART.MEDIA_STORE.USAGE',
199
+ count: usageAsync.value.totalCount,
200
+ size: formatBytes(usageAsync.value.totalSize)
201
+ })
202
+ }), _jsxs(Dropzone, {
164
203
  type: "button",
165
204
  "$dragOver": isDragOver,
166
205
  disabled: isUploading,
@@ -203,18 +242,18 @@ export const MediaStore = ({
203
242
  }), !loading && files.length > 0 && _jsxs(_Fragment, {
204
243
  children: [_jsx(Grid, {
205
244
  children: files.map(file => {
206
- const variants = variantsByUuid[file.uuid] ?? [];
245
+ const crops = cropsByUuid[file.uuid] ?? [];
207
246
  return _jsxs(Tile, {
208
247
  children: [_jsx(TileImage, {
209
248
  src: getThumbUrl(file),
210
249
  alt: file.filename
211
- }), variants.length > 0 && _jsx(VariantBadge, {
250
+ }), crops.length > 0 && _jsx(CropBadge, {
212
251
  children: lang({
213
- message: 'SMART.MEDIA_STORE.VARIANTS_BADGE',
214
- count: variants.length
252
+ message: 'SMART.MEDIA_STORE.CROPS_BADGE',
253
+ count: crops.length
215
254
  })
216
255
  }), _jsxs(TileActions, {
217
- children: [variants.length > 0 ? _jsx(DropdownMenuButton, {
256
+ children: [crops.length > 0 ? _jsx(DropdownMenuButton, {
218
257
  color: "secondary",
219
258
  size: "compact",
220
259
  icon: _jsx(CheckIcon, {
@@ -227,10 +266,10 @@ export const MediaStore = ({
227
266
  children: lang({
228
267
  message: 'SMART.MEDIA_STORE.SELECT_ORIGINAL'
229
268
  })
230
- }), variants.map(variant => _jsx(DropdownMenuItem, {
231
- onClick: () => onSelect(variant.url),
232
- children: getRatioLabel(variant) || variant.filename
233
- }, variant.uuid))]
269
+ }), crops.map(crop => _jsx(DropdownMenuItem, {
270
+ onClick: () => onSelect(crop.url),
271
+ children: getRatioLabel(crop) || crop.filename
272
+ }, crop.uuid))]
234
273
  })
235
274
  }) : _jsx(Button, {
236
275
  color: "secondary",
@@ -250,9 +289,20 @@ export const MediaStore = ({
250
289
  view: "lined"
251
290
  }),
252
291
  "aria-label": lang({
253
- message: 'SMART.MEDIA_STORE.CREATE_VARIANT_HINT'
292
+ message: 'SMART.MEDIA_STORE.CREATE_CROP_HINT'
293
+ }),
294
+ onClick: () => setCreateCropFor(file)
295
+ }), _jsx(Button, {
296
+ color: "secondary",
297
+ size: "compact",
298
+ icon: _jsx(TrashIcon, {
299
+ size: "small",
300
+ view: "lined"
301
+ }),
302
+ "aria-label": lang({
303
+ message: 'SMART.MEDIA_STORE.DELETE_HINT'
254
304
  }),
255
- onClick: () => setCreateVariantFor(file)
305
+ onClick: () => setPendingDelete(file)
256
306
  })]
257
307
  })]
258
308
  }, file.uuid);
@@ -286,13 +336,28 @@ export const MediaStore = ({
286
336
  })
287
337
  })]
288
338
  })]
289
- }), _jsx(CreateVariantDrawer, {
339
+ }), _jsx(CreateCropDrawer, {
290
340
  application: application,
291
- original: createVariantFor,
292
- isOpen: createVariantFor !== null,
293
- existingVariants: createVariantFor ? variantsByUuid[createVariantFor.uuid] ?? [] : [],
294
- onClose: () => setCreateVariantFor(null),
295
- onCreated: handleVariantCreated
341
+ original: createCropFor,
342
+ isOpen: createCropFor !== null,
343
+ existingCrops: createCropFor ? cropsByUuid[createCropFor.uuid] ?? [] : [],
344
+ onClose: () => setCreateCropFor(null),
345
+ onCreated: handleCropCreated,
346
+ onDeleted: handleCropDeleted
347
+ }), _jsx(ModalConfirm, {
348
+ isOpen: pendingDelete !== null,
349
+ title: lang({
350
+ message: 'SMART.MEDIA_STORE.DELETE_CONFIRM.TITLE'
351
+ }),
352
+ description: lang({
353
+ message: 'SMART.MEDIA_STORE.DELETE_CONFIRM.DESCRIPTION'
354
+ }),
355
+ confirmButtonText: lang({
356
+ message: 'SMART.MEDIA_STORE.DELETE_CONFIRM.BUTTON'
357
+ }),
358
+ confirmButtonColor: "danger",
359
+ onAccept: () => void handleDeleteConfirmed(),
360
+ onCancel: () => setPendingDelete(null)
296
361
  })]
297
362
  });
298
363
  };
@@ -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
+ }
@@ -1,5 +1,7 @@
1
1
  import type { MediaFile } from '@pushwoosh/rpc-v2-http-api-data/media_files';
2
2
  /** Thumbnail URL from image meta, falling back to the full-size URL. */
3
3
  export declare const getThumbUrl: (file: MediaFile) => string;
4
- /** Aspect-ratio label (e.g. "16:9") for a variant; empty for originals. */
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;
@@ -3,8 +3,19 @@ export const getThumbUrl = file => {
3
3
  var _file$meta;
4
4
  return ((_file$meta = file.meta) === null || _file$meta === void 0 ? void 0 : _file$meta.type) === 'imageMeta' ? file.meta.data.thumbnailUrl : file.url;
5
5
  };
6
- /** Aspect-ratio label (e.g. "16:9") for a variant; empty for originals. */
6
+ /** Aspect-ratio label (e.g. "16:9") for a crop; empty for originals. */
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,20 +9,30 @@ 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.VARIANTS_BADGE': '{{count}} var.',
12
+ 'SMART.MEDIA_STORE.USAGE': '{{count}} files · {{size}}',
13
+ 'SMART.MEDIA_STORE.CROPS_BADGE': '{{count}} cr.',
13
14
  'SMART.MEDIA_STORE.SELECT_ORIGINAL': 'Original',
14
- 'SMART.MEDIA_STORE.CREATE_VARIANT_HINT': 'Create variants',
15
- 'SMART.MEDIA_STORE.CREATE_VARIANT.TITLE': 'Create variant',
16
- 'SMART.MEDIA_STORE.CREATE_VARIANT.CLOSE': 'Close',
17
- 'SMART.MEDIA_STORE.CREATE_VARIANT.RATIO_LABEL': 'Aspect ratio',
18
- 'SMART.MEDIA_STORE.CREATE_VARIANT.SIZE_LABEL': 'Size',
19
- 'SMART.MEDIA_STORE.CREATE_VARIANT.WIDTH': 'Width',
20
- 'SMART.MEDIA_STORE.CREATE_VARIANT.HEIGHT': 'Height',
21
- 'SMART.MEDIA_STORE.CREATE_VARIANT.QUALITY_LABEL': 'JPEG quality (1–100)',
22
- 'SMART.MEDIA_STORE.CREATE_VARIANT.CREATE_BUTTON': 'Create variant',
23
- 'SMART.MEDIA_STORE.CREATE_VARIANT.RATIO_EXISTS': 'A variant with this aspect ratio already exists',
24
- 'SMART.MEDIA_STORE.CREATE_VARIANT.EXISTING_LABEL': 'Existing variants',
25
- 'SMART.MEDIA_STORE.CREATE_VARIANT.NO_VARIANTS': 'No variants yet',
26
- 'SMART.MEDIA_STORE.CREATE_VARIANT.SUCCESS': 'Variant created',
27
- 'SMART.MEDIA_STORE.CREATE_VARIANT.ERROR': 'Failed to create variant'
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',
22
+ 'SMART.MEDIA_STORE.CREATE_CROP.TITLE': 'Create crop',
23
+ 'SMART.MEDIA_STORE.CREATE_CROP.CLOSE': 'Close',
24
+ 'SMART.MEDIA_STORE.CREATE_CROP.RATIO_LABEL': 'Aspect ratio',
25
+ 'SMART.MEDIA_STORE.CREATE_CROP.SIZE_LABEL': 'Size',
26
+ 'SMART.MEDIA_STORE.CREATE_CROP.WIDTH': 'Width',
27
+ 'SMART.MEDIA_STORE.CREATE_CROP.HEIGHT': 'Height',
28
+ 'SMART.MEDIA_STORE.CREATE_CROP.QUALITY_LABEL': 'JPEG quality (1–100)',
29
+ 'SMART.MEDIA_STORE.CREATE_CROP.CREATE_BUTTON': 'Create crop',
30
+ 'SMART.MEDIA_STORE.CREATE_CROP.RATIO_EXISTS': 'A crop with this aspect ratio already exists',
31
+ 'SMART.MEDIA_STORE.CREATE_CROP.EXISTING_LABEL': 'Existing crops',
32
+ 'SMART.MEDIA_STORE.CREATE_CROP.NO_CROPS': 'No crops yet',
33
+ 'SMART.MEDIA_STORE.CREATE_CROP.SUCCESS': 'Crop created',
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,20 +9,30 @@ 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.VARIANTS_BADGE': '{{count}} вар.',
12
+ 'SMART.MEDIA_STORE.USAGE': '{{count}} файлов · {{size}}',
13
+ 'SMART.MEDIA_STORE.CROPS_BADGE': '{{count}} кр.',
13
14
  'SMART.MEDIA_STORE.SELECT_ORIGINAL': 'Оригинал',
14
- 'SMART.MEDIA_STORE.CREATE_VARIANT_HINT': 'Создать варианты',
15
- 'SMART.MEDIA_STORE.CREATE_VARIANT.TITLE': 'Создание варианта',
16
- 'SMART.MEDIA_STORE.CREATE_VARIANT.CLOSE': 'Закрыть',
17
- 'SMART.MEDIA_STORE.CREATE_VARIANT.RATIO_LABEL': 'Соотношение сторон',
18
- 'SMART.MEDIA_STORE.CREATE_VARIANT.SIZE_LABEL': 'Размер',
19
- 'SMART.MEDIA_STORE.CREATE_VARIANT.WIDTH': 'Ширина',
20
- 'SMART.MEDIA_STORE.CREATE_VARIANT.HEIGHT': 'Высота',
21
- 'SMART.MEDIA_STORE.CREATE_VARIANT.QUALITY_LABEL': 'Качество JPEG (1–100)',
22
- 'SMART.MEDIA_STORE.CREATE_VARIANT.CREATE_BUTTON': 'Создать вариант',
23
- 'SMART.MEDIA_STORE.CREATE_VARIANT.RATIO_EXISTS': 'Вариант с таким соотношением сторон уже существует',
24
- 'SMART.MEDIA_STORE.CREATE_VARIANT.EXISTING_LABEL': 'Созданные варианты',
25
- 'SMART.MEDIA_STORE.CREATE_VARIANT.NO_VARIANTS': 'Вариантов пока нет',
26
- 'SMART.MEDIA_STORE.CREATE_VARIANT.SUCCESS': 'Вариант создан',
27
- 'SMART.MEDIA_STORE.CREATE_VARIANT.ERROR': 'Не удалось создать вариант'
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': 'Удалить',
22
+ 'SMART.MEDIA_STORE.CREATE_CROP.TITLE': 'Создание кропа',
23
+ 'SMART.MEDIA_STORE.CREATE_CROP.CLOSE': 'Закрыть',
24
+ 'SMART.MEDIA_STORE.CREATE_CROP.RATIO_LABEL': 'Соотношение сторон',
25
+ 'SMART.MEDIA_STORE.CREATE_CROP.SIZE_LABEL': 'Размер',
26
+ 'SMART.MEDIA_STORE.CREATE_CROP.WIDTH': 'Ширина',
27
+ 'SMART.MEDIA_STORE.CREATE_CROP.HEIGHT': 'Высота',
28
+ 'SMART.MEDIA_STORE.CREATE_CROP.QUALITY_LABEL': 'Качество JPEG (1–100)',
29
+ 'SMART.MEDIA_STORE.CREATE_CROP.CREATE_BUTTON': 'Создать кроп',
30
+ 'SMART.MEDIA_STORE.CREATE_CROP.RATIO_EXISTS': 'Кроп с таким соотношением сторон уже существует',
31
+ 'SMART.MEDIA_STORE.CREATE_CROP.EXISTING_LABEL': 'Созданные кропы',
32
+ 'SMART.MEDIA_STORE.CREATE_CROP.NO_CROPS': 'Кропов пока нет',
33
+ 'SMART.MEDIA_STORE.CREATE_CROP.SUCCESS': 'Кроп создан',
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,22 +10,35 @@ 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.VARIANTS_BADGE': {
13
+ 'SMART.MEDIA_STORE.USAGE': {
14
+ count: number;
15
+ size: string;
16
+ };
17
+ 'SMART.MEDIA_STORE.CROPS_BADGE': {
14
18
  count: number;
15
19
  };
16
20
  'SMART.MEDIA_STORE.SELECT_ORIGINAL': NoParams;
17
- 'SMART.MEDIA_STORE.CREATE_VARIANT_HINT': NoParams;
18
- 'SMART.MEDIA_STORE.CREATE_VARIANT.TITLE': NoParams;
19
- 'SMART.MEDIA_STORE.CREATE_VARIANT.CLOSE': NoParams;
20
- 'SMART.MEDIA_STORE.CREATE_VARIANT.RATIO_LABEL': NoParams;
21
- 'SMART.MEDIA_STORE.CREATE_VARIANT.SIZE_LABEL': NoParams;
22
- 'SMART.MEDIA_STORE.CREATE_VARIANT.WIDTH': NoParams;
23
- 'SMART.MEDIA_STORE.CREATE_VARIANT.HEIGHT': NoParams;
24
- 'SMART.MEDIA_STORE.CREATE_VARIANT.QUALITY_LABEL': NoParams;
25
- 'SMART.MEDIA_STORE.CREATE_VARIANT.CREATE_BUTTON': NoParams;
26
- 'SMART.MEDIA_STORE.CREATE_VARIANT.RATIO_EXISTS': NoParams;
27
- 'SMART.MEDIA_STORE.CREATE_VARIANT.EXISTING_LABEL': NoParams;
28
- 'SMART.MEDIA_STORE.CREATE_VARIANT.NO_VARIANTS': NoParams;
29
- 'SMART.MEDIA_STORE.CREATE_VARIANT.SUCCESS': NoParams;
30
- 'SMART.MEDIA_STORE.CREATE_VARIANT.ERROR': NoParams;
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;
28
+ 'SMART.MEDIA_STORE.CREATE_CROP.TITLE': NoParams;
29
+ 'SMART.MEDIA_STORE.CREATE_CROP.CLOSE': NoParams;
30
+ 'SMART.MEDIA_STORE.CREATE_CROP.RATIO_LABEL': NoParams;
31
+ 'SMART.MEDIA_STORE.CREATE_CROP.SIZE_LABEL': NoParams;
32
+ 'SMART.MEDIA_STORE.CREATE_CROP.WIDTH': NoParams;
33
+ 'SMART.MEDIA_STORE.CREATE_CROP.HEIGHT': NoParams;
34
+ 'SMART.MEDIA_STORE.CREATE_CROP.QUALITY_LABEL': NoParams;
35
+ 'SMART.MEDIA_STORE.CREATE_CROP.CREATE_BUTTON': NoParams;
36
+ 'SMART.MEDIA_STORE.CREATE_CROP.RATIO_EXISTS': NoParams;
37
+ 'SMART.MEDIA_STORE.CREATE_CROP.EXISTING_LABEL': NoParams;
38
+ 'SMART.MEDIA_STORE.CREATE_CROP.NO_CROPS': NoParams;
39
+ 'SMART.MEDIA_STORE.CREATE_CROP.SUCCESS': NoParams;
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
  };
@@ -1,13 +1,14 @@
1
1
  export declare const Grid: import("styled-components").StyledComponent<"div", any, {}, never>;
2
2
  export declare const Tile: import("styled-components").StyledComponent<"div", any, {}, never>;
3
3
  export declare const TileImage: import("styled-components").StyledComponent<"img", any, {}, never>;
4
- export declare const VariantBadge: import("styled-components").StyledComponent<"span", any, {}, never>;
4
+ export declare const CropBadge: import("styled-components").StyledComponent<"span", any, {}, never>;
5
5
  export declare const TileActions: import("styled-components").StyledComponent<"div", any, {}, never>;
6
6
  export declare const EmptyState: import("styled-components").StyledComponent<"div", any, {}, never>;
7
7
  export declare const Dropzone: import("styled-components").StyledComponent<"button", any, {
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;
@@ -22,6 +23,7 @@ export declare const Segment: import("styled-components").StyledComponent<"div",
22
23
  export declare const SegmentItem: import("styled-components").StyledComponent<"button", any, {
23
24
  $active?: boolean;
24
25
  }, never>;
25
- export declare const VariantStrip: import("styled-components").StyledComponent<"div", any, {}, never>;
26
- export declare const VariantThumb: import("styled-components").StyledComponent<"div", any, {}, never>;
27
- export declare const VariantThumbCaption: import("styled-components").StyledComponent<"div", any, {}, never>;
26
+ export declare const CropStrip: import("styled-components").StyledComponent<"div", any, {}, never>;
27
+ export declare const CropThumb: import("styled-components").StyledComponent<"div", any, {}, never>;
28
+ export declare const CropThumbDelete: import("styled-components").StyledComponent<"div", any, {}, never>;
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"
@@ -12,8 +12,8 @@ export const TileImage = styled.img.withConfig({
12
12
  displayName: "TileImage",
13
13
  componentId: "sc-1d9kmgw-2"
14
14
  })(["width:100%;aspect-ratio:1;object-fit:cover;display:block;"]);
15
- export const VariantBadge = styled.span.withConfig({
16
- displayName: "VariantBadge",
15
+ export const CropBadge = styled.span.withConfig({
16
+ displayName: "CropBadge",
17
17
  componentId: "sc-1d9kmgw-3"
18
18
  })(["position:absolute;top:6px;right:6px;font-size:10px;font-weight:", ";line-height:1;color:", ";background:", ";border-radius:20px;padding:3px 7px;pointer-events:none;"], FontWeight.BOLD, Color.CLEAR, Color.PRIMARY);
19
19
  export const TileActions = styled.div.withConfig({
@@ -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
- /* ---------- CreateVariantDrawer ---------- */
39
+ export const UsageLine = styled.div.withConfig({
40
+ displayName: "UsageLine",
41
+ componentId: "sc-1d9kmgw-8"
42
+ })(["margin-bottom:8px;text-align:right;font-size:", ";color:", ";"], FontSize.SMALL, Color.LOCKED);
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,25 +75,29 @@ 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, ({
82
86
  $active
83
87
  }) => $active ? Color.CLEAR : Color.MAIN, Color.DIVIDER);
84
- export const VariantStrip = styled.div.withConfig({
85
- displayName: "VariantStrip",
86
- componentId: "sc-1d9kmgw-16"
87
- })(["display:flex;gap:10px;flex-wrap:wrap;align-items:flex-start;"]);
88
- export const VariantThumb = styled.div.withConfig({
89
- displayName: "VariantThumb",
88
+ export const CropStrip = styled.div.withConfig({
89
+ displayName: "CropStrip",
90
90
  componentId: "sc-1d9kmgw-17"
91
- })(["width:88px;border:1px solid ", ";border-radius:6px;overflow:hidden;background:", ";"], Color.DIVIDER, Color.SOFT_LIGHT);
92
- export const VariantThumbCaption = styled.div.withConfig({
93
- displayName: "VariantThumbCaption",
91
+ })(["display:flex;gap:10px;flex-wrap:wrap;align-items:flex-start;"]);
92
+ export const CropThumb = styled.div.withConfig({
93
+ displayName: "CropThumb",
94
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);
100
+ export const CropThumbCaption = styled.div.withConfig({
101
+ displayName: "CropThumbCaption",
102
+ componentId: "sc-1d9kmgw-20"
95
103
  })(["font-size:10px;text-align:center;padding:3px 2px;color:", ";"], Color.LOCKED);
@@ -7,7 +7,7 @@ export type MediaStoreProps = {
7
7
  perPage: number;
8
8
  /** Called when the user requests a different page. */
9
9
  onPageChange: (page: number) => void;
10
- /** Called with the chosen file URL (original or variant). */
10
+ /** Called with the chosen file URL (original or crop). */
11
11
  onSelect: (url: string) => void;
12
12
  };
13
13
  export type MediaStorePaginatedProps = {
@@ -17,6 +17,6 @@ export type MediaStorePaginatedProps = {
17
17
  initialPage?: number;
18
18
  /** Page size. Defaults to 30. */
19
19
  perPage?: number;
20
- /** Called with the chosen file URL (original or variant). */
20
+ /** Called with the chosen file URL (original or crop). */
21
21
  onSelect: (url: string) => void;
22
22
  };
package/package.json CHANGED
@@ -26,7 +26,7 @@
26
26
  "@pushwoosh/kit-helpers": "^4.9.1",
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.1"
52
+ "version": "1.1.3"
53
53
  }
@@ -1,12 +0,0 @@
1
- import { type FC } from 'react';
2
- import type { MediaFile } from '@pushwoosh/rpc-v2-http-api-data/media_files';
3
- type CreateVariantDrawerProps = {
4
- application: string;
5
- original: MediaFile | null;
6
- isOpen: boolean;
7
- existingVariants: MediaFile[];
8
- onClose: () => void;
9
- onCreated: (parentUuid: string, variant: MediaFile) => void;
10
- };
11
- export declare const CreateVariantDrawer: FC<CreateVariantDrawerProps>;
12
- export {};