@pushwoosh/smart-components-kit 1.2.3 → 1.2.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.
@@ -1,14 +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, ModalConfirm, NativeInput, useToast } from '@pushwoosh/dumb-components';
3
+ import { Button, Chip, Drawer, DrawerBody, DrawerHeader, DrawerTitle, ModalConfirm, NativeInput, useToast } from '@pushwoosh/dumb-components';
4
4
  import { Columns, Vertical } from '@pushwoosh/kit-helpers';
5
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
8
  import { useDeleteMediaFile } from './hooks';
9
- import { getRatioLabel, getThumbUrl } from './mediaFile.utils';
9
+ import { getCropCaption, getThumbUrl } from './mediaFile.utils';
10
10
  import { useLang } from './polyglot';
11
- import { Chip, Chips, CropStrip, CropThumb, CropThumbCaption, CropThumbDelete, FieldLabel, PreviewFrame, PreviewImage, PreviewStage, Segment, SegmentItem, TileImage } from './styled';
11
+ import { Chips, CropStrip, CropThumb, CropThumbCaption, CropThumbDelete, FieldLabel, PreviewFrame, PreviewImage, PreviewStage, Segment, SegmentItem, SizeInput, TileImage } from './styled';
12
12
  const RATIOS = [{
13
13
  value: 'ASPECT_RATIO_1_1',
14
14
  label: '1:1',
@@ -46,7 +46,6 @@ const RATIOS = [{
46
46
  label: '9:16',
47
47
  ar: 9 / 16
48
48
  }];
49
- const DEFAULT_RATIO = 'ASPECT_RATIO_16_9';
50
49
  const DEFAULT_SIZE = '1080';
51
50
  const DEFAULT_QUALITY = '90';
52
51
  export const CreateCropDrawer = ({
@@ -58,6 +57,7 @@ export const CreateCropDrawer = ({
58
57
  onCreated,
59
58
  onDeleted
60
59
  }) => {
60
+ var _original$meta;
61
61
  const lang = useLang();
62
62
  const {
63
63
  rpcGateway
@@ -66,7 +66,7 @@ export const CreateCropDrawer = ({
66
66
  openToast
67
67
  } = useToast();
68
68
  const deleteMediaFile = useDeleteMediaFile(application);
69
- const [ratio, setRatio] = useState(DEFAULT_RATIO);
69
+ const [ratio, setRatio] = useState('original');
70
70
  const [dimension, setDimension] = useState('width');
71
71
  const [size, setSize] = useState(DEFAULT_SIZE);
72
72
  const [quality, setQuality] = useState(DEFAULT_QUALITY);
@@ -77,7 +77,7 @@ export const CreateCropDrawer = ({
77
77
  // `ratioExists` guard) leak from the previously edited image.
78
78
  useEffect(() => {
79
79
  if (original) {
80
- setRatio(DEFAULT_RATIO);
80
+ setRatio('original');
81
81
  setDimension('width');
82
82
  setSize(DEFAULT_SIZE);
83
83
  setQuality(DEFAULT_QUALITY);
@@ -88,15 +88,34 @@ export const CreateCropDrawer = ({
88
88
  const isSizeValid = Number.isFinite(sizeValue) && sizeValue > 0;
89
89
  const qualityValue = Number(quality);
90
90
  const isQualityValid = !isJpeg || Number.isFinite(qualityValue) && qualityValue >= 1 && qualityValue <= 100;
91
+ const isOriginalView = ratio === 'original';
92
+ const originalMeta = (original === null || original === void 0 || (_original$meta = original.meta) === null || _original$meta === void 0 ? void 0 : _original$meta.type) === 'imageMeta' ? original.meta.data : null;
93
+ const originalRatio = originalMeta && originalMeta.height > 0 ? originalMeta.width / originalMeta.height : 1;
91
94
  const selectedRatio = RATIOS.find(item => item.value === ratio) ?? RATIOS[0];
92
95
  const outputWidth = dimension === 'width' ? sizeValue : Math.round(sizeValue * selectedRatio.ar);
93
96
  const outputHeight = dimension === 'height' ? sizeValue : Math.round(sizeValue / selectedRatio.ar);
94
- const ratioExists = existingCrops.some(crop => {
97
+ // Both inputs are always visible: the driving one is editable, the other is
98
+ // read-only and shows the value computed from the aspect ratio.
99
+ const computedText = value => isSizeValid ? String(value) : '';
100
+ const widthText = dimension === 'width' ? size : computedText(outputWidth);
101
+ const heightText = dimension === 'height' ? size : computedText(outputHeight);
102
+ const handleDimensionChange = next => {
103
+ if (next === dimension) return;
104
+ // Seed the newly editable field with its computed value so the visible
105
+ // numbers stay put and only editability swaps.
106
+ if (isSizeValid) {
107
+ setSize(String(next === 'width' ? outputWidth : outputHeight));
108
+ }
109
+ setDimension(next);
110
+ };
111
+ // Same ratio with a different size is allowed; only an exact ratio + output
112
+ // size match counts as a duplicate.
113
+ const duplicateExists = !isOriginalView && isSizeValid && existingCrops.some(crop => {
95
114
  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;
115
+ return ((_crop$meta = crop.meta) === null || _crop$meta === void 0 ? void 0 : _crop$meta.type) === 'imageMeta' && crop.meta.data.ratio === selectedRatio.label && crop.meta.data.width === outputWidth && crop.meta.data.height === outputHeight;
97
116
  });
98
117
  const handleCreate = async () => {
99
- if (!original || !isSizeValid || !isQualityValid || ratioExists) return;
118
+ if (!original || ratio === 'original' || !isSizeValid || !isQualityValid || duplicateExists) return;
100
119
  // Capture the parent up front so a concurrent reopen for another image
101
120
  // can't misroute the created crop.
102
121
  const parent = original;
@@ -121,6 +140,10 @@ export const CreateCropDrawer = ({
121
140
  type: 'success'
122
141
  });
123
142
  onCreated(parent.uuid, response.file);
143
+ // Back to the view-only default: the form's ratio+size now match the
144
+ // crop that was just created, so leaving it would flash the
145
+ // "duplicate exists" note right after a successful create.
146
+ setRatio('original');
124
147
  } catch (error) {
125
148
  openToast({
126
149
  title: error.message || lang({
@@ -174,44 +197,63 @@ export const CreateCropDrawer = ({
174
197
  children: lang({
175
198
  message: 'SMART.MEDIA_STORAGE.CREATE_CROP.RATIO_LABEL'
176
199
  })
177
- }), _jsx(Chips, {
178
- children: RATIOS.map(item => _jsx(Chip, {
179
- type: "button",
180
- "$active": item.value === ratio,
200
+ }), _jsxs(Chips, {
201
+ children: [_jsx(Chip, {
202
+ "$selected": isOriginalView,
203
+ onClick: () => setRatio('original'),
204
+ children: lang({
205
+ message: 'SMART.MEDIA_STORAGE.CREATE_CROP.ORIGINAL'
206
+ })
207
+ }), RATIOS.map(item => _jsx(Chip, {
208
+ "$selected": item.value === ratio,
181
209
  onClick: () => setRatio(item.value),
182
210
  children: item.label
183
- }, item.value))
211
+ }, item.value))]
184
212
  })]
185
- }), _jsxs("div", {
213
+ }), !isOriginalView && _jsxs("div", {
186
214
  children: [_jsx(FieldLabel, {
187
215
  children: lang({
188
216
  message: 'SMART.MEDIA_STORAGE.CREATE_CROP.SIZE_LABEL'
189
217
  })
190
218
  }), _jsxs(Columns, {
191
- "$sizes": "auto 1fr",
219
+ "$sizes": "auto 1fr 1fr",
192
220
  "$gap": 8,
193
221
  "$alignItems": "center",
194
222
  children: [_jsxs(Segment, {
195
223
  children: [_jsx(SegmentItem, {
196
224
  type: "button",
197
225
  "$active": dimension === 'width',
198
- onClick: () => setDimension('width'),
226
+ onClick: () => handleDimensionChange('width'),
199
227
  children: lang({
200
228
  message: 'SMART.MEDIA_STORAGE.CREATE_CROP.WIDTH'
201
229
  })
202
230
  }), _jsx(SegmentItem, {
203
231
  type: "button",
204
232
  "$active": dimension === 'height',
205
- onClick: () => setDimension('height'),
233
+ onClick: () => handleDimensionChange('height'),
206
234
  children: lang({
207
235
  message: 'SMART.MEDIA_STORAGE.CREATE_CROP.HEIGHT'
208
236
  })
209
237
  })]
210
- }), _jsx(NativeInput, {
238
+ }), _jsx(SizeInput, {
211
239
  type: "number",
212
240
  min: 1,
213
- value: size,
214
- "$isErrored": !isSizeValid,
241
+ value: widthText,
242
+ readOnly: dimension !== 'width',
243
+ "$isErrored": dimension === 'width' && !isSizeValid,
244
+ "aria-label": lang({
245
+ message: 'SMART.MEDIA_STORAGE.CREATE_CROP.WIDTH'
246
+ }),
247
+ onChange: event => setSize(event.target.value)
248
+ }), _jsx(SizeInput, {
249
+ type: "number",
250
+ min: 1,
251
+ value: heightText,
252
+ readOnly: dimension !== 'height',
253
+ "$isErrored": dimension === 'height' && !isSizeValid,
254
+ "aria-label": lang({
255
+ message: 'SMART.MEDIA_STORAGE.CREATE_CROP.HEIGHT'
256
+ }),
215
257
  onChange: event => setSize(event.target.value)
216
258
  })]
217
259
  })]
@@ -219,16 +261,18 @@ export const CreateCropDrawer = ({
219
261
  "$gap": 6,
220
262
  children: [_jsx(PreviewStage, {
221
263
  children: _jsx(PreviewFrame, {
222
- "$ratio": selectedRatio.ar,
264
+ "$ratio": isOriginalView ? originalRatio : selectedRatio.ar,
223
265
  children: _jsx(PreviewImage, {
224
266
  src: original.url,
225
267
  alt: original.filename
226
268
  })
227
269
  })
228
- }), isSizeValid && _jsxs(Text, {
270
+ }), isOriginalView && originalMeta && _jsxs(Text, {
271
+ children: [originalMeta.width, ' × ', originalMeta.height]
272
+ }), !isOriginalView && isSizeValid && _jsxs(Text, {
229
273
  children: [selectedRatio.label, ' · ', outputWidth, ' × ', outputHeight]
230
274
  })]
231
- }), isJpeg && _jsxs("div", {
275
+ }), !isOriginalView && isJpeg && _jsxs("div", {
232
276
  children: [_jsx(FieldLabel, {
233
277
  children: lang({
234
278
  message: 'SMART.MEDIA_STORAGE.CREATE_CROP.QUALITY_LABEL'
@@ -241,14 +285,14 @@ export const CreateCropDrawer = ({
241
285
  "$isErrored": !isQualityValid,
242
286
  onChange: event => setQuality(event.target.value)
243
287
  })]
244
- }), ratioExists && _jsx(Text, {
288
+ }), duplicateExists && _jsx(Text, {
245
289
  children: lang({
246
- message: 'SMART.MEDIA_STORAGE.CREATE_CROP.RATIO_EXISTS'
290
+ message: 'SMART.MEDIA_STORAGE.CREATE_CROP.DUPLICATE_EXISTS'
247
291
  })
248
- }), _jsx(Button, {
292
+ }), !isOriginalView && _jsx(Button, {
249
293
  color: "primary",
250
294
  size: "field",
251
- isDisabled: !isSizeValid || !isQualityValid || ratioExists,
295
+ isDisabled: !isSizeValid || !isQualityValid || duplicateExists,
252
296
  isLoading: isCreating,
253
297
  onClick: () => void handleCreate(),
254
298
  children: lang({
@@ -282,7 +326,7 @@ export const CreateCropDrawer = ({
282
326
  onClick: () => setPendingDeleteCrop(crop)
283
327
  })
284
328
  }), _jsx(CropThumbCaption, {
285
- children: getRatioLabel(crop) || crop.filename
329
+ children: getCropCaption(crop)
286
330
  })]
287
331
  }, crop.uuid))
288
332
  })]
@@ -7,7 +7,7 @@ import { Text } from '@pushwoosh/kit-typography';
7
7
  import { useSPAContext } from '@pushwoosh/spa-context';
8
8
  import { CreateCropDrawer } from './CreateCropDrawer';
9
9
  import { useDeleteMediaFile } from './hooks';
10
- import { formatBytes, getRatioLabel, getThumbUrl } from './mediaFile.utils';
10
+ import { formatBytes, getCropCaption, getThumbUrl } from './mediaFile.utils';
11
11
  import { useLang } from './polyglot';
12
12
  import { CropBadge, Dropzone, EmptyState, Grid, HiddenInput, Tile, TileActions, TileImage, UsageLine } from './styled';
13
13
  const UPLOAD_PATH = '/api/media/upload';
@@ -276,7 +276,7 @@ export const MediaStorage = ({
276
276
  })
277
277
  }), crops.map(crop => _jsx(DropdownMenuItem, {
278
278
  onClick: () => onSelect(crop.url),
279
- children: getRatioLabel(crop) || crop.filename
279
+ children: getCropCaption(crop)
280
280
  }, crop.uuid))]
281
281
  })
282
282
  }) : _jsx(Button, {
@@ -1,7 +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 crop; empty for originals. */
5
- export declare const getRatioLabel: (file: MediaFile) => string;
4
+ /** Caption for a crop: ratio + pixel size (e.g. "16:9 · 1080×608"); filename fallback. */
5
+ export declare const getCropCaption: (file: MediaFile) => string;
6
6
  /** Human-readable byte size, e.g. 4404019 -> "4.2 MB". */
7
7
  export declare const formatBytes: (bytes: number) => string;
@@ -3,10 +3,17 @@ 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 crop; empty for originals. */
7
- export const getRatioLabel = file => {
6
+ /** Caption for a crop: ratio + pixel size (e.g. "16:9 · 1080×608"); filename fallback. */
7
+ export const getCropCaption = file => {
8
8
  var _file$meta2;
9
- return ((_file$meta2 = file.meta) === null || _file$meta2 === void 0 ? void 0 : _file$meta2.type) === 'imageMeta' ? file.meta.data.ratio : '';
9
+ if (((_file$meta2 = file.meta) === null || _file$meta2 === void 0 ? void 0 : _file$meta2.type) !== 'imageMeta') return file.filename;
10
+ const {
11
+ ratio,
12
+ width,
13
+ height
14
+ } = file.meta.data;
15
+ const size = `${width}×${height}`;
16
+ return ratio ? `${ratio} · ${size}` : size;
10
17
  };
11
18
  const BYTE_UNITS = ['B', 'KB', 'MB', 'GB', 'TB'];
12
19
  /** Human-readable byte size, e.g. 4404019 -> "4.2 MB". */
@@ -23,12 +23,13 @@ export const messages = {
23
23
  'SMART.MEDIA_STORAGE.CREATE_CROP.TITLE': 'Create crop',
24
24
  'SMART.MEDIA_STORAGE.CREATE_CROP.CLOSE': 'Close',
25
25
  'SMART.MEDIA_STORAGE.CREATE_CROP.RATIO_LABEL': 'Aspect ratio',
26
+ 'SMART.MEDIA_STORAGE.CREATE_CROP.ORIGINAL': 'Original',
26
27
  'SMART.MEDIA_STORAGE.CREATE_CROP.SIZE_LABEL': 'Size',
27
28
  'SMART.MEDIA_STORAGE.CREATE_CROP.WIDTH': 'Width',
28
29
  'SMART.MEDIA_STORAGE.CREATE_CROP.HEIGHT': 'Height',
29
30
  'SMART.MEDIA_STORAGE.CREATE_CROP.QUALITY_LABEL': 'JPEG quality (1–100)',
30
31
  'SMART.MEDIA_STORAGE.CREATE_CROP.CREATE_BUTTON': 'Create crop',
31
- 'SMART.MEDIA_STORAGE.CREATE_CROP.RATIO_EXISTS': 'A crop with this aspect ratio already exists',
32
+ 'SMART.MEDIA_STORAGE.CREATE_CROP.DUPLICATE_EXISTS': 'A crop with this aspect ratio and size already exists',
32
33
  'SMART.MEDIA_STORAGE.CREATE_CROP.EXISTING_LABEL': 'Existing crops',
33
34
  'SMART.MEDIA_STORAGE.CREATE_CROP.NO_CROPS': 'No crops yet',
34
35
  'SMART.MEDIA_STORAGE.CREATE_CROP.SUCCESS': 'Crop created',
@@ -23,12 +23,13 @@ export const messages = {
23
23
  'SMART.MEDIA_STORAGE.CREATE_CROP.TITLE': 'Создание кропа',
24
24
  'SMART.MEDIA_STORAGE.CREATE_CROP.CLOSE': 'Закрыть',
25
25
  'SMART.MEDIA_STORAGE.CREATE_CROP.RATIO_LABEL': 'Соотношение сторон',
26
+ 'SMART.MEDIA_STORAGE.CREATE_CROP.ORIGINAL': 'Оригинал',
26
27
  'SMART.MEDIA_STORAGE.CREATE_CROP.SIZE_LABEL': 'Размер',
27
28
  'SMART.MEDIA_STORAGE.CREATE_CROP.WIDTH': 'Ширина',
28
29
  'SMART.MEDIA_STORAGE.CREATE_CROP.HEIGHT': 'Высота',
29
30
  'SMART.MEDIA_STORAGE.CREATE_CROP.QUALITY_LABEL': 'Качество JPEG (1–100)',
30
31
  'SMART.MEDIA_STORAGE.CREATE_CROP.CREATE_BUTTON': 'Создать кроп',
31
- 'SMART.MEDIA_STORAGE.CREATE_CROP.RATIO_EXISTS': 'Кроп с таким соотношением сторон уже существует',
32
+ 'SMART.MEDIA_STORAGE.CREATE_CROP.DUPLICATE_EXISTS': 'Кроп с таким соотношением сторон и размером уже существует',
32
33
  'SMART.MEDIA_STORAGE.CREATE_CROP.EXISTING_LABEL': 'Созданные кропы',
33
34
  'SMART.MEDIA_STORAGE.CREATE_CROP.NO_CROPS': 'Кропов пока нет',
34
35
  'SMART.MEDIA_STORAGE.CREATE_CROP.SUCCESS': 'Кроп создан',
@@ -29,12 +29,13 @@ export type MediaStorageMessages = {
29
29
  'SMART.MEDIA_STORAGE.CREATE_CROP.TITLE': NoParams;
30
30
  'SMART.MEDIA_STORAGE.CREATE_CROP.CLOSE': NoParams;
31
31
  'SMART.MEDIA_STORAGE.CREATE_CROP.RATIO_LABEL': NoParams;
32
+ 'SMART.MEDIA_STORAGE.CREATE_CROP.ORIGINAL': NoParams;
32
33
  'SMART.MEDIA_STORAGE.CREATE_CROP.SIZE_LABEL': NoParams;
33
34
  'SMART.MEDIA_STORAGE.CREATE_CROP.WIDTH': NoParams;
34
35
  'SMART.MEDIA_STORAGE.CREATE_CROP.HEIGHT': NoParams;
35
36
  'SMART.MEDIA_STORAGE.CREATE_CROP.QUALITY_LABEL': NoParams;
36
37
  'SMART.MEDIA_STORAGE.CREATE_CROP.CREATE_BUTTON': NoParams;
37
- 'SMART.MEDIA_STORAGE.CREATE_CROP.RATIO_EXISTS': NoParams;
38
+ 'SMART.MEDIA_STORAGE.CREATE_CROP.DUPLICATE_EXISTS': NoParams;
38
39
  'SMART.MEDIA_STORAGE.CREATE_CROP.EXISTING_LABEL': NoParams;
39
40
  'SMART.MEDIA_STORAGE.CREATE_CROP.NO_CROPS': NoParams;
40
41
  'SMART.MEDIA_STORAGE.CREATE_CROP.SUCCESS': NoParams;
@@ -15,10 +15,8 @@ export declare const PreviewFrame: import("styled-components").StyledComponent<"
15
15
  }, never>;
16
16
  export declare const PreviewImage: import("styled-components").StyledComponent<"img", any, {}, never>;
17
17
  export declare const FieldLabel: import("styled-components").StyledComponent<"div", any, {}, never>;
18
+ export declare const SizeInput: import("styled-components").StyledComponent<"input", any, import("@pushwoosh/dumb-components/native/Input").NativeInputProps, never>;
18
19
  export declare const Chips: import("styled-components").StyledComponent<"div", any, {}, never>;
19
- export declare const Chip: import("styled-components").StyledComponent<"button", any, {
20
- $active?: boolean;
21
- }, never>;
22
20
  export declare const Segment: import("styled-components").StyledComponent<"div", any, {}, never>;
23
21
  export declare const SegmentItem: import("styled-components").StyledComponent<"button", any, {
24
22
  $active?: boolean;
@@ -1,4 +1,5 @@
1
1
  import styled from 'styled-components';
2
+ import { NativeInput } from '@pushwoosh/dumb-components';
2
3
  import { Color, FontSize, FontWeight } from '@pushwoosh/kit-constants';
3
4
  export const Grid = styled.div.withConfig({
4
5
  displayName: "Grid",
@@ -59,20 +60,14 @@ export const FieldLabel = styled.div.withConfig({
59
60
  displayName: "FieldLabel",
60
61
  componentId: "sc-14317rj-12"
61
62
  })(["font-size:11px;font-weight:600;text-transform:uppercase;letter-spacing:0.4px;color:", ";margin-bottom:6px;"], Color.LOCKED);
63
+ export const SizeInput = styled(NativeInput).withConfig({
64
+ displayName: "SizeInput",
65
+ componentId: "sc-14317rj-13"
66
+ })(["&[readonly]{background:", ";color:", ";}"], Color.SOFT_LIGHT, Color.LOCKED);
62
67
  export const Chips = styled.div.withConfig({
63
68
  displayName: "Chips",
64
- componentId: "sc-14317rj-13"
65
- })(["display:flex;flex-wrap:wrap;gap:7px;"]);
66
- export const Chip = styled.button.withConfig({
67
- displayName: "Chip",
68
69
  componentId: "sc-14317rj-14"
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;"], ({
70
- $active
71
- }) => $active ? Color.PRIMARY : Color.DIVIDER, ({
72
- $active
73
- }) => $active ? Color.PRIMARY : Color.CLEAR, ({
74
- $active
75
- }) => $active ? Color.CLEAR : Color.MAIN);
70
+ })(["display:flex;flex-wrap:wrap;gap:7px;"]);
76
71
  export const Segment = styled.div.withConfig({
77
72
  displayName: "Segment",
78
73
  componentId: "sc-14317rj-15"
package/package.json CHANGED
@@ -48,5 +48,5 @@
48
48
  "react": "^16.14.0 || ^18.0.0",
49
49
  "react-dom": "^16.14.0 || ^18.0.0"
50
50
  },
51
- "version": "1.2.3"
51
+ "version": "1.2.5"
52
52
  }