@pushwoosh/smart-components-kit 1.2.3 → 1.2.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.
@@ -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;
@@ -174,44 +193,63 @@ export const CreateCropDrawer = ({
174
193
  children: lang({
175
194
  message: 'SMART.MEDIA_STORAGE.CREATE_CROP.RATIO_LABEL'
176
195
  })
177
- }), _jsx(Chips, {
178
- children: RATIOS.map(item => _jsx(Chip, {
179
- type: "button",
180
- "$active": item.value === ratio,
196
+ }), _jsxs(Chips, {
197
+ children: [_jsx(Chip, {
198
+ "$selected": isOriginalView,
199
+ onClick: () => setRatio('original'),
200
+ children: lang({
201
+ message: 'SMART.MEDIA_STORAGE.CREATE_CROP.ORIGINAL'
202
+ })
203
+ }), RATIOS.map(item => _jsx(Chip, {
204
+ "$selected": item.value === ratio,
181
205
  onClick: () => setRatio(item.value),
182
206
  children: item.label
183
- }, item.value))
207
+ }, item.value))]
184
208
  })]
185
- }), _jsxs("div", {
209
+ }), !isOriginalView && _jsxs("div", {
186
210
  children: [_jsx(FieldLabel, {
187
211
  children: lang({
188
212
  message: 'SMART.MEDIA_STORAGE.CREATE_CROP.SIZE_LABEL'
189
213
  })
190
214
  }), _jsxs(Columns, {
191
- "$sizes": "auto 1fr",
215
+ "$sizes": "auto 1fr 1fr",
192
216
  "$gap": 8,
193
217
  "$alignItems": "center",
194
218
  children: [_jsxs(Segment, {
195
219
  children: [_jsx(SegmentItem, {
196
220
  type: "button",
197
221
  "$active": dimension === 'width',
198
- onClick: () => setDimension('width'),
222
+ onClick: () => handleDimensionChange('width'),
199
223
  children: lang({
200
224
  message: 'SMART.MEDIA_STORAGE.CREATE_CROP.WIDTH'
201
225
  })
202
226
  }), _jsx(SegmentItem, {
203
227
  type: "button",
204
228
  "$active": dimension === 'height',
205
- onClick: () => setDimension('height'),
229
+ onClick: () => handleDimensionChange('height'),
206
230
  children: lang({
207
231
  message: 'SMART.MEDIA_STORAGE.CREATE_CROP.HEIGHT'
208
232
  })
209
233
  })]
210
- }), _jsx(NativeInput, {
234
+ }), _jsx(SizeInput, {
211
235
  type: "number",
212
236
  min: 1,
213
- value: size,
214
- "$isErrored": !isSizeValid,
237
+ value: widthText,
238
+ readOnly: dimension !== 'width',
239
+ "$isErrored": dimension === 'width' && !isSizeValid,
240
+ "aria-label": lang({
241
+ message: 'SMART.MEDIA_STORAGE.CREATE_CROP.WIDTH'
242
+ }),
243
+ onChange: event => setSize(event.target.value)
244
+ }), _jsx(SizeInput, {
245
+ type: "number",
246
+ min: 1,
247
+ value: heightText,
248
+ readOnly: dimension !== 'height',
249
+ "$isErrored": dimension === 'height' && !isSizeValid,
250
+ "aria-label": lang({
251
+ message: 'SMART.MEDIA_STORAGE.CREATE_CROP.HEIGHT'
252
+ }),
215
253
  onChange: event => setSize(event.target.value)
216
254
  })]
217
255
  })]
@@ -219,16 +257,18 @@ export const CreateCropDrawer = ({
219
257
  "$gap": 6,
220
258
  children: [_jsx(PreviewStage, {
221
259
  children: _jsx(PreviewFrame, {
222
- "$ratio": selectedRatio.ar,
260
+ "$ratio": isOriginalView ? originalRatio : selectedRatio.ar,
223
261
  children: _jsx(PreviewImage, {
224
262
  src: original.url,
225
263
  alt: original.filename
226
264
  })
227
265
  })
228
- }), isSizeValid && _jsxs(Text, {
266
+ }), isOriginalView && originalMeta && _jsxs(Text, {
267
+ children: [originalMeta.width, ' × ', originalMeta.height]
268
+ }), !isOriginalView && isSizeValid && _jsxs(Text, {
229
269
  children: [selectedRatio.label, ' · ', outputWidth, ' × ', outputHeight]
230
270
  })]
231
- }), isJpeg && _jsxs("div", {
271
+ }), !isOriginalView && isJpeg && _jsxs("div", {
232
272
  children: [_jsx(FieldLabel, {
233
273
  children: lang({
234
274
  message: 'SMART.MEDIA_STORAGE.CREATE_CROP.QUALITY_LABEL'
@@ -241,14 +281,14 @@ export const CreateCropDrawer = ({
241
281
  "$isErrored": !isQualityValid,
242
282
  onChange: event => setQuality(event.target.value)
243
283
  })]
244
- }), ratioExists && _jsx(Text, {
284
+ }), duplicateExists && _jsx(Text, {
245
285
  children: lang({
246
- message: 'SMART.MEDIA_STORAGE.CREATE_CROP.RATIO_EXISTS'
286
+ message: 'SMART.MEDIA_STORAGE.CREATE_CROP.DUPLICATE_EXISTS'
247
287
  })
248
- }), _jsx(Button, {
288
+ }), !isOriginalView && _jsx(Button, {
249
289
  color: "primary",
250
290
  size: "field",
251
- isDisabled: !isSizeValid || !isQualityValid || ratioExists,
291
+ isDisabled: !isSizeValid || !isQualityValid || duplicateExists,
252
292
  isLoading: isCreating,
253
293
  onClick: () => void handleCreate(),
254
294
  children: lang({
@@ -282,7 +322,7 @@ export const CreateCropDrawer = ({
282
322
  onClick: () => setPendingDeleteCrop(crop)
283
323
  })
284
324
  }), _jsx(CropThumbCaption, {
285
- children: getRatioLabel(crop) || crop.filename
325
+ children: getCropCaption(crop)
286
326
  })]
287
327
  }, crop.uuid))
288
328
  })]
@@ -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.4"
52
52
  }