@pushwoosh/smart-components-kit 1.3.36 → 1.4.0

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,9 +7,9 @@ import { Text } from '@pushwoosh/kit-typography';
7
7
  import { useSPAContext } from '@pushwoosh/spa-context';
8
8
  import { CreateCropDrawer } from './CreateCropDrawer';
9
9
  import { useDeleteMediaFile, useDeleteMediaFiles } from './hooks';
10
- import { formatBytes, getCropCaption, getThumbUrl } from './mediaFile.utils';
10
+ import { formatBytes, getCropCaption, getSourceLabel, getThumbUrl, hasVersions } from './mediaFile.utils';
11
11
  import { Lang, useLang } from './polyglot';
12
- import { CropBadge, Dropzone, EmptyState, Grid, HiddenInput, SelectionBar, SelectionCount, Tile, TileActions, TileCheckbox, TileImage, UsageLine, WarningBadge } from './styles';
12
+ import { CountBadge, Dropzone, EmptyState, Grid, HiddenInput, SelectionBar, SelectionCount, SourceBadge, Tile, TileActions, TileBadges, TileCheckbox, TileImage, UsageLine, WarningBadge } from './styles';
13
13
  const UPLOAD_PATH = '/api/media/upload';
14
14
  const ALLOWED_IMAGE_EXTENSIONS = ['.jpg', '.jpeg', '.png', '.gif', '.webp'];
15
15
  const isAllowedImage = file => {
@@ -314,6 +314,7 @@ export const MediaStorage = ({
314
314
  children: files.map(file => {
315
315
  const crops = cropsByUuid[file.uuid] ?? [];
316
316
  const warning = fileWarning?.(file);
317
+ const sourceLabel = getSourceLabel(file);
317
318
  return _jsxs(Tile, {
318
319
  "$selected": selectedUuids.includes(file.uuid),
319
320
  children: [_jsx(TileImage, {
@@ -328,11 +329,20 @@ export const MediaStorage = ({
328
329
  isDisabled: isBulkDeleting,
329
330
  onChange: () => toggleSelected(file.uuid)
330
331
  })
331
- }), crops.length > 0 && _jsx(CropBadge, {
332
- children: lang({
333
- message: 'SMART.MEDIA_STORAGE.CROPS_BADGE',
334
- count: crops.length
335
- })
332
+ }), _jsxs(TileBadges, {
333
+ children: [sourceLabel && _jsx(SourceBadge, {
334
+ children: sourceLabel
335
+ }), hasVersions(file) && _jsx(CountBadge, {
336
+ children: lang({
337
+ message: 'SMART.MEDIA_STORAGE.VERSIONS_BADGE',
338
+ count: file.versionsCount
339
+ })
340
+ }), crops.length > 0 && _jsx(CountBadge, {
341
+ children: lang({
342
+ message: 'SMART.MEDIA_STORAGE.CROPS_BADGE',
343
+ count: crops.length
344
+ })
345
+ })]
336
346
  }), warning && _jsx(WarningBadge, {
337
347
  title: warning,
338
348
  children: warning
@@ -442,7 +452,7 @@ export const MediaStorage = ({
442
452
  message: 'SMART.MEDIA_STORAGE.DELETE_CONFIRM.TITLE'
443
453
  }),
444
454
  description: lang({
445
- message: 'SMART.MEDIA_STORAGE.DELETE_CONFIRM.DESCRIPTION'
455
+ message: pendingDelete && hasVersions(pendingDelete) ? 'SMART.MEDIA_STORAGE.DELETE_CONFIRM.DESCRIPTION_VERSION' : 'SMART.MEDIA_STORAGE.DELETE_CONFIRM.DESCRIPTION'
446
456
  }),
447
457
  confirmButtonText: lang({
448
458
  message: 'SMART.MEDIA_STORAGE.DELETE_CONFIRM.BUTTON'
@@ -3,5 +3,9 @@ import type { MediaFile } from '@pushwoosh/rpc-v2-http-api-data/media_files';
3
3
  export declare const getThumbUrl: (file: MediaFile) => string;
4
4
  /** Caption for a crop: ratio + pixel size (e.g. "16:9 · 1080×608"); filename fallback. */
5
5
  export declare const getCropCaption: (file: MediaFile) => string;
6
+ /** Brand name of the service an image was imported from; undefined for a file uploaded from disk. */
7
+ export declare const getSourceLabel: (file: MediaFile) => string | undefined;
8
+ /** True for an image that has been edited at least once, so older versions exist behind it. */
9
+ export declare const hasVersions: (file: MediaFile) => boolean;
6
10
  /** Human-readable byte size, e.g. 4404019 -> "4.2 MB". */
7
11
  export declare const formatBytes: (bytes: number) => string;
@@ -11,6 +11,14 @@ export const getCropCaption = file => {
11
11
  const size = `${width}×${height}`;
12
12
  return ratio ? `${ratio} · ${size}` : size;
13
13
  };
14
+ const SOURCE_LABELS = {
15
+ canva: 'Canva',
16
+ figma: 'Figma'
17
+ };
18
+ /** Brand name of the service an image was imported from; undefined for a file uploaded from disk. */
19
+ export const getSourceLabel = file => file.source ? SOURCE_LABELS[file.source] : undefined;
20
+ /** True for an image that has been edited at least once, so older versions exist behind it. */
21
+ export const hasVersions = file => file.versionsCount > 1;
14
22
  const BYTE_UNITS = ['B', 'KB', 'MB', 'GB', 'TB'];
15
23
  /** Human-readable byte size, e.g. 4404019 -> "4.2 MB". */
16
24
  export const formatBytes = bytes => {
@@ -12,6 +12,8 @@ export const messages = {
12
12
  'SMART.MEDIA_STORAGE.UPLOAD_SUCCESS': 'تم رفع الصورة',
13
13
  'SMART.MEDIA_STORAGE.USAGE': 'الملفات: {{count}} · {{size}}',
14
14
  'SMART.MEDIA_STORAGE.CROPS_BADGE': 'اقتصاصات: {{count}}',
15
+ 'SMART.MEDIA_STORAGE.VERSIONS_BADGE': 'إصدارات: {{count}}',
16
+ 'SMART.MEDIA_STORAGE.DELETE_CONFIRM.DESCRIPTION_VERSION': 'سيتم حذف هذا الإصدار واقتصاصاته فقط، وسيظهر الإصدار السابق بدلًا منه. لا يمكن التراجع عن هذا الإجراء.',
15
17
  'SMART.MEDIA_STORAGE.SELECT_ORIGINAL': 'الأصل',
16
18
  'SMART.MEDIA_STORAGE.CREATE_CROP_HINT': 'إنشاء اقتصاصات',
17
19
  'SMART.MEDIA_STORAGE.DELETE_HINT': 'حذف',
@@ -12,6 +12,8 @@ export const messages = {
12
12
  'SMART.MEDIA_STORAGE.UPLOAD_SUCCESS': 'ছবি আপলোড হয়েছে',
13
13
  'SMART.MEDIA_STORAGE.USAGE': '{{count}}টি ফাইল · {{size}}',
14
14
  'SMART.MEDIA_STORAGE.CROPS_BADGE': '{{count}}টি ক্রপ',
15
+ 'SMART.MEDIA_STORAGE.VERSIONS_BADGE': '{{count}}টি সংস্করণ',
16
+ 'SMART.MEDIA_STORAGE.DELETE_CONFIRM.DESCRIPTION_VERSION': 'শুধু এই সংস্করণ এবং এর ক্রপ মুছে যাবে; এর জায়গায় আগের সংস্করণটি দেখানো হবে। এটি পূর্বাবস্থায় ফেরানো যাবে না।',
15
17
  'SMART.MEDIA_STORAGE.SELECT_ORIGINAL': 'মূল',
16
18
  'SMART.MEDIA_STORAGE.CREATE_CROP_HINT': 'ক্রপ তৈরি করুন',
17
19
  'SMART.MEDIA_STORAGE.DELETE_HINT': 'মুছুন',
@@ -12,6 +12,8 @@ export const messages = {
12
12
  'SMART.MEDIA_STORAGE.UPLOAD_SUCCESS': 'Bild hochgeladen',
13
13
  'SMART.MEDIA_STORAGE.USAGE': '{{count}} Dateien · {{size}}',
14
14
  'SMART.MEDIA_STORAGE.CROPS_BADGE': '{{count}} Zuschn.',
15
+ 'SMART.MEDIA_STORAGE.VERSIONS_BADGE': '{{count}} Vers.',
16
+ 'SMART.MEDIA_STORAGE.DELETE_CONFIRM.DESCRIPTION_VERSION': 'Nur diese Version und ihre Zuschnitte werden gelöscht; stattdessen wird die vorherige Version angezeigt. Dies kann nicht rückgängig gemacht werden.',
15
17
  'SMART.MEDIA_STORAGE.SELECT_ORIGINAL': 'Original',
16
18
  'SMART.MEDIA_STORAGE.CREATE_CROP_HINT': 'Zuschnitte erstellen',
17
19
  'SMART.MEDIA_STORAGE.DELETE_HINT': 'Löschen',
@@ -12,6 +12,8 @@ export const messages = {
12
12
  'SMART.MEDIA_STORAGE.UPLOAD_SUCCESS': 'Image uploaded',
13
13
  'SMART.MEDIA_STORAGE.USAGE': '{{count}} files · {{size}}',
14
14
  'SMART.MEDIA_STORAGE.CROPS_BADGE': '{{count}} cr.',
15
+ 'SMART.MEDIA_STORAGE.VERSIONS_BADGE': '{{count}} ver.',
16
+ 'SMART.MEDIA_STORAGE.DELETE_CONFIRM.DESCRIPTION_VERSION': 'Only this version and its crops are deleted; the previous version is shown instead. This can’t be undone.',
15
17
  'SMART.MEDIA_STORAGE.SELECT_ORIGINAL': 'Original',
16
18
  'SMART.MEDIA_STORAGE.CREATE_CROP_HINT': 'Create crops',
17
19
  'SMART.MEDIA_STORAGE.DELETE_HINT': 'Delete',
@@ -12,6 +12,8 @@ export const messages = {
12
12
  'SMART.MEDIA_STORAGE.UPLOAD_SUCCESS': 'Imagen subida',
13
13
  'SMART.MEDIA_STORAGE.USAGE': '{{count}} archivos · {{size}}',
14
14
  'SMART.MEDIA_STORAGE.CROPS_BADGE': '{{count}} rec.',
15
+ 'SMART.MEDIA_STORAGE.VERSIONS_BADGE': '{{count}} vers.',
16
+ 'SMART.MEDIA_STORAGE.DELETE_CONFIRM.DESCRIPTION_VERSION': 'Solo se eliminan esta versión y sus recortes; en su lugar se muestra la versión anterior. Esta acción no se puede deshacer.',
15
17
  'SMART.MEDIA_STORAGE.SELECT_ORIGINAL': 'Original',
16
18
  'SMART.MEDIA_STORAGE.CREATE_CROP_HINT': 'Crear recortes',
17
19
  'SMART.MEDIA_STORAGE.DELETE_HINT': 'Eliminar',
@@ -12,6 +12,8 @@ export const messages = {
12
12
  'SMART.MEDIA_STORAGE.UPLOAD_SUCCESS': 'Image importée',
13
13
  'SMART.MEDIA_STORAGE.USAGE': '{{count}} fichiers · {{size}}',
14
14
  'SMART.MEDIA_STORAGE.CROPS_BADGE': '{{count}} recadr.',
15
+ 'SMART.MEDIA_STORAGE.VERSIONS_BADGE': '{{count}} vers.',
16
+ 'SMART.MEDIA_STORAGE.DELETE_CONFIRM.DESCRIPTION_VERSION': 'Seuls cette version et ses recadrages sont supprimés ; la version précédente s’affiche à la place. Cette action est irréversible.',
15
17
  'SMART.MEDIA_STORAGE.SELECT_ORIGINAL': 'Original',
16
18
  'SMART.MEDIA_STORAGE.CREATE_CROP_HINT': 'Créer des recadrages',
17
19
  'SMART.MEDIA_STORAGE.DELETE_HINT': 'Supprimer',
@@ -12,6 +12,8 @@ export const messages = {
12
12
  'SMART.MEDIA_STORAGE.UPLOAD_SUCCESS': 'התמונה הועלתה',
13
13
  'SMART.MEDIA_STORAGE.USAGE': '{{count}} קבצים · {{size}}',
14
14
  'SMART.MEDIA_STORAGE.CROPS_BADGE': '{{count}} חית.',
15
+ 'SMART.MEDIA_STORAGE.VERSIONS_BADGE': '{{count}} גרס.',
16
+ 'SMART.MEDIA_STORAGE.DELETE_CONFIRM.DESCRIPTION_VERSION': 'רק הגרסה הזו והחיתוכים שלה יימחקו; במקומה תוצג הגרסה הקודמת. לא ניתן לבטל פעולה זו.',
15
17
  'SMART.MEDIA_STORAGE.SELECT_ORIGINAL': 'מקורי',
16
18
  'SMART.MEDIA_STORAGE.CREATE_CROP_HINT': 'יצירת חיתוכים',
17
19
  'SMART.MEDIA_STORAGE.DELETE_HINT': 'מחיקה',
@@ -12,6 +12,8 @@ export const messages = {
12
12
  'SMART.MEDIA_STORAGE.UPLOAD_SUCCESS': 'छवि अपलोड हो गई',
13
13
  'SMART.MEDIA_STORAGE.USAGE': '{{count}} फ़ाइलें · {{size}}',
14
14
  'SMART.MEDIA_STORAGE.CROPS_BADGE': '{{count}} क्रॉप',
15
+ 'SMART.MEDIA_STORAGE.VERSIONS_BADGE': '{{count}} संस्करण',
16
+ 'SMART.MEDIA_STORAGE.DELETE_CONFIRM.DESCRIPTION_VERSION': 'केवल यह संस्करण और इसके क्रॉप हटेंगे; इसकी जगह पिछला संस्करण दिखाया जाएगा। इसे पूर्ववत नहीं किया जा सकता।',
15
17
  'SMART.MEDIA_STORAGE.SELECT_ORIGINAL': 'मूल',
16
18
  'SMART.MEDIA_STORAGE.CREATE_CROP_HINT': 'क्रॉप बनाएँ',
17
19
  'SMART.MEDIA_STORAGE.DELETE_HINT': 'हटाएँ',
@@ -12,6 +12,8 @@ export const messages = {
12
12
  'SMART.MEDIA_STORAGE.UPLOAD_SUCCESS': '画像をアップロードしました',
13
13
  'SMART.MEDIA_STORAGE.USAGE': '{{count}}ファイル · {{size}}',
14
14
  'SMART.MEDIA_STORAGE.CROPS_BADGE': 'トリミング{{count}}',
15
+ 'SMART.MEDIA_STORAGE.VERSIONS_BADGE': 'バージョン{{count}}',
16
+ 'SMART.MEDIA_STORAGE.DELETE_CONFIRM.DESCRIPTION_VERSION': 'このバージョンとそのトリミングのみが削除され、代わりに以前のバージョンが表示されます。この操作は元に戻せません。',
15
17
  'SMART.MEDIA_STORAGE.SELECT_ORIGINAL': 'オリジナル',
16
18
  'SMART.MEDIA_STORAGE.CREATE_CROP_HINT': 'トリミングを作成',
17
19
  'SMART.MEDIA_STORAGE.DELETE_HINT': '削除',
@@ -12,6 +12,8 @@ export const messages = {
12
12
  'SMART.MEDIA_STORAGE.UPLOAD_SUCCESS': '이미지가 업로드되었습니다',
13
13
  'SMART.MEDIA_STORAGE.USAGE': '파일 {{count}}개 · {{size}}',
14
14
  'SMART.MEDIA_STORAGE.CROPS_BADGE': '크롭 {{count}}개',
15
+ 'SMART.MEDIA_STORAGE.VERSIONS_BADGE': '버전 {{count}}개',
16
+ 'SMART.MEDIA_STORAGE.DELETE_CONFIRM.DESCRIPTION_VERSION': '이 버전과 해당 크롭만 삭제되며 대신 이전 버전이 표시됩니다. 이 작업은 되돌릴 수 없습니다.',
15
17
  'SMART.MEDIA_STORAGE.SELECT_ORIGINAL': '원본',
16
18
  'SMART.MEDIA_STORAGE.CREATE_CROP_HINT': '크롭 만들기',
17
19
  'SMART.MEDIA_STORAGE.DELETE_HINT': '삭제',
@@ -12,6 +12,8 @@ export const messages = {
12
12
  'SMART.MEDIA_STORAGE.UPLOAD_SUCCESS': 'Imagem enviada',
13
13
  'SMART.MEDIA_STORAGE.USAGE': '{{count}} arquivos · {{size}}',
14
14
  'SMART.MEDIA_STORAGE.CROPS_BADGE': '{{count}} rec.',
15
+ 'SMART.MEDIA_STORAGE.VERSIONS_BADGE': '{{count}} vers.',
16
+ 'SMART.MEDIA_STORAGE.DELETE_CONFIRM.DESCRIPTION_VERSION': 'Apenas esta versão e seus recortes são excluídos; a versão anterior é exibida no lugar. Esta ação não pode ser desfeita.',
15
17
  'SMART.MEDIA_STORAGE.SELECT_ORIGINAL': 'Original',
16
18
  'SMART.MEDIA_STORAGE.CREATE_CROP_HINT': 'Criar recortes',
17
19
  'SMART.MEDIA_STORAGE.DELETE_HINT': 'Excluir',
@@ -12,6 +12,8 @@ export const messages = {
12
12
  'SMART.MEDIA_STORAGE.UPLOAD_SUCCESS': 'Изображение загружено',
13
13
  'SMART.MEDIA_STORAGE.USAGE': '{{count}} файлов · {{size}}',
14
14
  'SMART.MEDIA_STORAGE.CROPS_BADGE': '{{count}} кр.',
15
+ 'SMART.MEDIA_STORAGE.VERSIONS_BADGE': '{{count}} вер.',
16
+ 'SMART.MEDIA_STORAGE.DELETE_CONFIRM.DESCRIPTION_VERSION': 'Удалится только эта версия и её кропы, вместо неё снова появится предыдущая. Действие необратимо.',
15
17
  'SMART.MEDIA_STORAGE.SELECT_ORIGINAL': 'Оригинал',
16
18
  'SMART.MEDIA_STORAGE.CREATE_CROP_HINT': 'Создать кропы',
17
19
  'SMART.MEDIA_STORAGE.DELETE_HINT': 'Удалить',
@@ -12,6 +12,8 @@ export const messages = {
12
12
  'SMART.MEDIA_STORAGE.UPLOAD_SUCCESS': 'อัปโหลดรูปภาพแล้ว',
13
13
  'SMART.MEDIA_STORAGE.USAGE': '{{count}} ไฟล์ · {{size}}',
14
14
  'SMART.MEDIA_STORAGE.CROPS_BADGE': '{{count}} ครอป',
15
+ 'SMART.MEDIA_STORAGE.VERSIONS_BADGE': '{{count}} เวอร์ชัน',
16
+ 'SMART.MEDIA_STORAGE.DELETE_CONFIRM.DESCRIPTION_VERSION': 'จะลบเฉพาะเวอร์ชันนี้และครอปของเวอร์ชันนี้ และจะแสดงเวอร์ชันก่อนหน้าแทน การดำเนินการนี้ไม่สามารถย้อนกลับได้',
15
17
  'SMART.MEDIA_STORAGE.SELECT_ORIGINAL': 'ต้นฉบับ',
16
18
  'SMART.MEDIA_STORAGE.CREATE_CROP_HINT': 'สร้างครอป',
17
19
  'SMART.MEDIA_STORAGE.DELETE_HINT': 'ลบ',
@@ -12,6 +12,8 @@ export const messages = {
12
12
  'SMART.MEDIA_STORAGE.UPLOAD_SUCCESS': '图片已上传',
13
13
  'SMART.MEDIA_STORAGE.USAGE': '{{count}} 个文件 · {{size}}',
14
14
  'SMART.MEDIA_STORAGE.CROPS_BADGE': '{{count}} 个裁剪图',
15
+ 'SMART.MEDIA_STORAGE.VERSIONS_BADGE': '{{count}} 个版本',
16
+ 'SMART.MEDIA_STORAGE.DELETE_CONFIRM.DESCRIPTION_VERSION': '仅删除此版本及其裁剪图,并改为显示上一个版本。此操作无法撤销。',
15
17
  'SMART.MEDIA_STORAGE.SELECT_ORIGINAL': '原图',
16
18
  'SMART.MEDIA_STORAGE.CREATE_CROP_HINT': '创建裁剪图',
17
19
  'SMART.MEDIA_STORAGE.DELETE_HINT': '删除',
@@ -18,6 +18,10 @@ export type MediaStorageMessages = {
18
18
  'SMART.MEDIA_STORAGE.CROPS_BADGE': {
19
19
  count: number;
20
20
  };
21
+ 'SMART.MEDIA_STORAGE.VERSIONS_BADGE': {
22
+ count: number;
23
+ };
24
+ 'SMART.MEDIA_STORAGE.DELETE_CONFIRM.DESCRIPTION_VERSION': NoParams;
21
25
  'SMART.MEDIA_STORAGE.SELECT_ORIGINAL': NoParams;
22
26
  'SMART.MEDIA_STORAGE.CREATE_CROP_HINT': NoParams;
23
27
  'SMART.MEDIA_STORAGE.DELETE_HINT': NoParams;
@@ -3,7 +3,9 @@ export declare const Tile: import("styled-components").StyledComponent<"div", an
3
3
  $selected?: boolean;
4
4
  }, never>;
5
5
  export declare const TileImage: import("styled-components").StyledComponent<"img", any, {}, never>;
6
- export declare const CropBadge: import("styled-components").StyledComponent<"span", any, {}, never>;
6
+ export declare const TileBadges: import("styled-components").StyledComponent<"div", any, {}, never>;
7
+ export declare const SourceBadge: import("styled-components").StyledComponent<"span", any, {}, never>;
8
+ export declare const CountBadge: import("styled-components").StyledComponent<"span", any, {}, never>;
7
9
  export declare const WarningBadge: import("styled-components").StyledComponent<"span", any, {}, never>;
8
10
  export declare const TileCheckbox: import("styled-components").StyledComponent<"div", any, {}, never>;
9
11
  export declare const SelectionBar: import("styled-components").StyledComponent<"div", any, {}, never>;
@@ -17,37 +17,45 @@ export const TileImage = styled.img.withConfig({
17
17
  displayName: "TileImage",
18
18
  componentId: "sc-uu3cbo-2"
19
19
  })(["width:100%;aspect-ratio:1;object-fit:cover;display:block;"]);
20
- export const CropBadge = styled.span.withConfig({
21
- displayName: "CropBadge",
20
+ export const TileBadges = styled.div.withConfig({
21
+ displayName: "TileBadges",
22
22
  componentId: "sc-uu3cbo-3"
23
- })(["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);
23
+ })(["position:absolute;top:6px;right:6px;display:flex;flex-direction:column;align-items:flex-end;gap:4px;z-index:2;pointer-events:none;"]);
24
+ export const SourceBadge = styled.span.withConfig({
25
+ displayName: "SourceBadge",
26
+ componentId: "sc-uu3cbo-4"
27
+ })(["font-size:10px;font-weight:", ";line-height:1;color:", ";background:", ";border-radius:20px;padding:3px 7px;"], FontWeight.BOLD, Color.MAIN, Color.CLEAR);
28
+ export const CountBadge = styled.span.withConfig({
29
+ displayName: "CountBadge",
30
+ componentId: "sc-uu3cbo-5"
31
+ })(["font-size:10px;font-weight:", ";line-height:1;color:", ";background:", ";border-radius:20px;padding:3px 7px;"], FontWeight.BOLD, Color.CLEAR, Color.PRIMARY);
24
32
  export const WarningBadge = styled.span.withConfig({
25
33
  displayName: "WarningBadge",
26
- componentId: "sc-uu3cbo-4"
34
+ componentId: "sc-uu3cbo-6"
27
35
  })(["position:absolute;bottom:0;left:0;right:0;font-size:10px;line-height:1.2;text-align:center;color:", ";background:", ";padding:3px 6px;white-space:nowrap;overflow:hidden;text-overflow:ellipsis;pointer-events:none;"], Color.CLEAR, Color.WARNING);
28
36
  export const TileCheckbox = styled.div.withConfig({
29
37
  displayName: "TileCheckbox",
30
- componentId: "sc-uu3cbo-5"
38
+ componentId: "sc-uu3cbo-7"
31
39
  })(["position:absolute;top:6px;left:6px;z-index:2;padding:2px;border-radius:4px;background:", ";"], Color.CLEAR);
32
40
  export const SelectionBar = styled.div.withConfig({
33
41
  displayName: "SelectionBar",
34
- componentId: "sc-uu3cbo-6"
42
+ componentId: "sc-uu3cbo-8"
35
43
  })(["display:flex;align-items:center;gap:12px;min-height:32px;margin-bottom:12px;"]);
36
44
  export const SelectionCount = styled.span.withConfig({
37
45
  displayName: "SelectionCount",
38
- componentId: "sc-uu3cbo-7"
46
+ componentId: "sc-uu3cbo-9"
39
47
  })(["font-size:", ";color:", ";"], FontSize.SMALL, Color.LOCKED);
40
48
  export const TileActions = styled.div.withConfig({
41
49
  displayName: "TileActions",
42
- componentId: "sc-uu3cbo-8"
50
+ componentId: "sc-uu3cbo-10"
43
51
  })(["position:absolute;inset:0;z-index:1;display:flex;align-items:center;justify-content:center;gap:6px;background:rgba(0,0,0,0.45);backdrop-filter:blur(2px);opacity:0;transition:opacity 0.15s;", ":hover &{opacity:1;}@media (hover:none){opacity:1;}"], Tile);
44
52
  export const EmptyState = styled.div.withConfig({
45
53
  displayName: "EmptyState",
46
- componentId: "sc-uu3cbo-9"
54
+ componentId: "sc-uu3cbo-11"
47
55
  })(["text-align:center;padding:48px 16px;color:", ";"], Color.LOCKED);
48
56
  export const Dropzone = styled.button.withConfig({
49
57
  displayName: "Dropzone",
50
- componentId: "sc-uu3cbo-10"
58
+ componentId: "sc-uu3cbo-12"
51
59
  })(["width:100%;min-height:96px;margin-bottom:16px;display:flex;flex-direction:column;align-items:center;justify-content:center;gap:8px;padding:16px;font:inherit;font-size:13px;color:", ";cursor:pointer;border:2px dashed ", ";border-radius:8px;background:", ";transition:border-color 0.15s,background 0.15s;&:hover{border-color:", ";}&:disabled{cursor:default;opacity:0.7;}"], Color.LOCKED, ({
52
60
  $dragOver
53
61
  }) => $dragOver ? Color.PRIMARY : Color.DIVIDER, ({
@@ -55,46 +63,46 @@ export const Dropzone = styled.button.withConfig({
55
63
  }) => $dragOver ? Color.SOFT_LIGHT : Color.CLEAR, Color.PRIMARY);
56
64
  export const HiddenInput = styled.input.withConfig({
57
65
  displayName: "HiddenInput",
58
- componentId: "sc-uu3cbo-11"
66
+ componentId: "sc-uu3cbo-13"
59
67
  })(["display:none;"]);
60
68
  export const UsageLine = styled.div.withConfig({
61
69
  displayName: "UsageLine",
62
- componentId: "sc-uu3cbo-12"
70
+ componentId: "sc-uu3cbo-14"
63
71
  })(["margin-left:auto;font-size:", ";color:", ";"], FontSize.SMALL, Color.LOCKED);
64
72
  /* ---------- CreateCropDrawer ---------- */
65
73
  export const PreviewStage = styled.div.withConfig({
66
74
  displayName: "PreviewStage",
67
- componentId: "sc-uu3cbo-13"
75
+ componentId: "sc-uu3cbo-15"
68
76
  })(["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);
69
77
  export const PreviewFrame = styled.div.withConfig({
70
78
  displayName: "PreviewFrame",
71
- componentId: "sc-uu3cbo-14"
79
+ componentId: "sc-uu3cbo-16"
72
80
  })(["aspect-ratio:", ";max-width:100%;max-height:336px;overflow:hidden;border-radius:4px;"], ({
73
81
  $ratio
74
82
  }) => $ratio);
75
83
  export const PreviewImage = styled.img.withConfig({
76
84
  displayName: "PreviewImage",
77
- componentId: "sc-uu3cbo-15"
85
+ componentId: "sc-uu3cbo-17"
78
86
  })(["width:100%;height:100%;object-fit:cover;display:block;"]);
79
87
  export const FieldLabel = styled.div.withConfig({
80
88
  displayName: "FieldLabel",
81
- componentId: "sc-uu3cbo-16"
89
+ componentId: "sc-uu3cbo-18"
82
90
  })(["font-size:11px;font-weight:600;text-transform:uppercase;letter-spacing:0.4px;color:", ";margin-bottom:6px;"], Color.LOCKED);
83
91
  export const SizeInput = styled(NativeInput).withConfig({
84
92
  displayName: "SizeInput",
85
- componentId: "sc-uu3cbo-17"
93
+ componentId: "sc-uu3cbo-19"
86
94
  })(["&[readonly]{background:", ";color:", ";}"], Color.SOFT_LIGHT, Color.LOCKED);
87
95
  export const Chips = styled.div.withConfig({
88
96
  displayName: "Chips",
89
- componentId: "sc-uu3cbo-18"
97
+ componentId: "sc-uu3cbo-20"
90
98
  })(["display:flex;flex-wrap:wrap;gap:7px;"]);
91
99
  export const Segment = styled.div.withConfig({
92
100
  displayName: "Segment",
93
- componentId: "sc-uu3cbo-19"
101
+ componentId: "sc-uu3cbo-21"
94
102
  })(["display:inline-flex;border:1px solid ", ";border-radius:6px;overflow:hidden;"], Color.DIVIDER);
95
103
  export const SegmentItem = styled.button.withConfig({
96
104
  displayName: "SegmentItem",
97
- componentId: "sc-uu3cbo-20"
105
+ componentId: "sc-uu3cbo-22"
98
106
  })(["font:inherit;font-size:13px;cursor:pointer;padding:7px 14px;border:none;background:", ";color:", ";& + &{border-left:1px solid ", ";}"], ({
99
107
  $active
100
108
  }) => $active ? Color.PRIMARY : Color.CLEAR, ({
@@ -102,17 +110,17 @@ export const SegmentItem = styled.button.withConfig({
102
110
  }) => $active ? Color.CLEAR : Color.MAIN, Color.DIVIDER);
103
111
  export const CropStrip = styled.div.withConfig({
104
112
  displayName: "CropStrip",
105
- componentId: "sc-uu3cbo-21"
113
+ componentId: "sc-uu3cbo-23"
106
114
  })(["display:flex;gap:10px;flex-wrap:wrap;align-items:flex-start;"]);
107
115
  export const CropThumb = styled.div.withConfig({
108
116
  displayName: "CropThumb",
109
- componentId: "sc-uu3cbo-22"
117
+ componentId: "sc-uu3cbo-24"
110
118
  })(["position:relative;width:88px;border:1px solid ", ";border-radius:6px;overflow:hidden;background:", ";"], Color.DIVIDER, Color.SOFT_LIGHT);
111
119
  export const CropThumbDelete = styled.div.withConfig({
112
120
  displayName: "CropThumbDelete",
113
- componentId: "sc-uu3cbo-23"
121
+ componentId: "sc-uu3cbo-25"
114
122
  })(["position:absolute;top:4px;right:4px;opacity:0;transition:opacity 0.15s;", ":hover &{opacity:1;}@media (hover:none){opacity:1;}"], CropThumb);
115
123
  export const CropThumbCaption = styled.div.withConfig({
116
124
  displayName: "CropThumbCaption",
117
- componentId: "sc-uu3cbo-24"
125
+ componentId: "sc-uu3cbo-26"
118
126
  })(["font-size:10px;text-align:center;padding:3px 2px;color:", ";"], Color.LOCKED);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@pushwoosh/smart-components-kit",
3
- "version": "1.3.36",
3
+ "version": "1.4.0",
4
4
  "description": "Stateful, data-connected React components shared by Pushwoosh micro-frontends",
5
5
  "repository": {
6
6
  "type": "git",
@@ -17,12 +17,12 @@
17
17
  },
18
18
  "dependencies": {
19
19
  "@floating-ui/react-dom": "^2.1.2",
20
- "@pushwoosh/dumb-components": "^2.9.12",
20
+ "@pushwoosh/dumb-components": "^2.9.13",
21
21
  "@pushwoosh/kit-constants": "^2.1.0",
22
22
  "@pushwoosh/kit-helpers": "^4.12.7",
23
23
  "@pushwoosh/kit-icons": "^2.3.3",
24
24
  "@pushwoosh/kit-typography": "^1.8.7",
25
- "@pushwoosh/rpc-v2-http-api-data": "^0.2.151",
25
+ "@pushwoosh/rpc-v2-http-api-data": "^0.2.376",
26
26
  "@pushwoosh/spa-context": "^1.8.1",
27
27
  "idb-keyval": "^6.2.2",
28
28
  "jwt-decode": "^4.0.0",
@@ -30,8 +30,9 @@
30
30
  "react-router-dom": "^5.3.4"
31
31
  },
32
32
  "devDependencies": {
33
- "@pushwoosh/frontend-builder-engine": "^3.1.3",
33
+ "@pushwoosh/frontend-builder-engine": "^3.1.4",
34
34
  "@svgr/webpack": "^8.1.0",
35
+ "@types/node": "^24.19.1",
35
36
  "@types/react": "^18.3.31",
36
37
  "@types/react-dom": "^18.3.7",
37
38
  "@types/react-router-dom": "^5.3.3",
@@ -40,7 +41,7 @@
40
41
  "react": "^18.3.1",
41
42
  "react-dom": "^18.3.1",
42
43
  "styled-components": "^5.3.11",
43
- "vitest": "^3.2.4"
44
+ "vitest": "^4.1.10"
44
45
  },
45
46
  "peerDependencies": {
46
47
  "react": "^18.0.0",
@@ -1 +0,0 @@
1
- export {};
@@ -1,59 +0,0 @@
1
- import { describe, expect, it } from 'vitest';
2
- import { parseContent } from './parseContent';
3
- // Compact projection: `**bold**` becomes [bold] so a case reads as one line.
4
- function sketch(blocks) {
5
- return blocks.map(block => {
6
- const lines = block.lines.map(line => line.nodes.map(node => node.type === 'bold' ? `[${node.value}]` : node.value).join(''));
7
- return block.type === 'list' ? `ul: ${lines.join(' | ')}` : `p: ${lines.join(' / ')}`;
8
- });
9
- }
10
- describe('parseContent', () => {
11
- it('splits paragraphs on a blank line', () => {
12
- expect(sketch(parseContent('first\n\nsecond'))).toEqual(['p: first', 'p: second']);
13
- });
14
- it('keeps a single newline inside one paragraph', () => {
15
- expect(sketch(parseContent('first\nsecond'))).toEqual(['p: first / second']);
16
- });
17
- it('collects consecutive bullets into one list', () => {
18
- expect(sketch(parseContent('- one\n- two\n- three'))).toEqual(['ul: one | two | three']);
19
- });
20
- it('reads bold inside a list item', () => {
21
- expect(sketch(parseContent('- plain **loud**'))).toEqual(['ul: plain [loud]']);
22
- });
23
- it('separates a lead-in line from the bullets that follow it', () => {
24
- expect(sketch(parseContent('what changed:\n- one\n- two\nand that is all'))).toEqual(['p: what changed:', 'ul: one | two', 'p: and that is all']);
25
- });
26
- it('treats three or more newlines as a single paragraph break', () => {
27
- expect(sketch(parseContent('first\n\n\n\nsecond'))).toEqual(['p: first', 'p: second']);
28
- });
29
- it('ignores blank lines around the content', () => {
30
- expect(sketch(parseContent('\n\n text \n\n'))).toEqual(['p: text']);
31
- });
32
- it('leaves a lone asterisk as text', () => {
33
- expect(sketch(parseContent('2 * 3 = 6'))).toEqual(['p: 2 * 3 = 6']);
34
- });
35
- it('does not read `* ` as a bullet marker', () => {
36
- expect(sketch(parseContent('* one\n* two'))).toEqual(['p: * one / * two']);
37
- });
38
- it('leaves an unclosed bold marker as text', () => {
39
- expect(sketch(parseContent('**loud and then not'))).toEqual(['p: **loud and then not']);
40
- });
41
- it('leaves an empty bold marker as text', () => {
42
- expect(sketch(parseContent('nothing **** here'))).toEqual(['p: nothing **** here']);
43
- });
44
- it('reads several bold runs in one line', () => {
45
- expect(sketch(parseContent('**a** and **b**'))).toEqual(['p: [a] and [b]']);
46
- });
47
- it('leaves HTML as text', () => {
48
- expect(sketch(parseContent('<b>test</b>'))).toEqual(['p: <b>test</b>']);
49
- });
50
- it('returns nothing for empty or blank content', () => {
51
- expect(parseContent('')).toEqual([]);
52
- expect(parseContent(' \n \n')).toEqual([]);
53
- });
54
- it('gives every block, line and node a unique id', () => {
55
- const blocks = parseContent('lead **in**\n- one **two**\n- three\n\ntail');
56
- const ids = blocks.flatMap(block => [block.id, ...block.lines.flatMap(line => [line.id, ...line.nodes.map(node => node.id)])]);
57
- expect(new Set(ids).size).toBe(ids.length);
58
- });
59
- });
@@ -1 +0,0 @@
1
- export {};
@@ -1,48 +0,0 @@
1
- import { createElement } from 'react';
2
- import { renderToStaticMarkup } from 'react-dom/server';
3
- import { ServerStyleSheet } from 'styled-components';
4
- import { describe, expect, it } from 'vitest';
5
- import { Table } from './styles';
6
- const render = () => {
7
- const sheet = new ServerStyleSheet();
8
- try {
9
- const html = renderToStaticMarkup(sheet.collectStyles(createElement(Table, {
10
- columns: '1fr 340px',
11
- width: '100%',
12
- isBordered: true
13
- })));
14
- return {
15
- html,
16
- css: sheet.getStyleTags()
17
- };
18
- } finally {
19
- sheet.seal();
20
- }
21
- };
22
- describe('Table', () => {
23
- it('denies columns and isBordered in shouldForwardProp, which SC 6 uses instead of isPropValid', () => {
24
- const {
25
- shouldForwardProp
26
- } = Table;
27
- expect(shouldForwardProp?.('columns')).toBe(false);
28
- expect(shouldForwardProp?.('isBordered')).toBe(false);
29
- expect(shouldForwardProp?.('width')).toBe(true);
30
- });
31
- it('does not forward columns and isBordered to the DOM', () => {
32
- const {
33
- html
34
- } = render();
35
- expect(html).not.toMatch(/columns=/i);
36
- expect(html).not.toMatch(/isbordered=/i);
37
- });
38
- it('keeps width as a DOM attribute', () => {
39
- expect(render().html).toContain('width="100%"');
40
- });
41
- it('still styles from columns and isBordered', () => {
42
- const {
43
- css
44
- } = render();
45
- expect(css).toContain('--table-columns:1fr 340px');
46
- expect(css).toMatch(/border:1px solid/);
47
- });
48
- });
@@ -1 +0,0 @@
1
- export {};
@@ -1,49 +0,0 @@
1
- import { describe, expect, it } from 'vitest';
2
- import { isStaffImpersonation } from './isStaffImpersonation';
3
- function tokenWith(claims) {
4
- const payload = Buffer.from(JSON.stringify(claims)).toString('base64url');
5
- return `eyJhbGciOiJub25lIn0.${payload}.signature`;
6
- }
7
- function clientWith(token) {
8
- return {
9
- getAccessToken: async () => {
10
- if (token instanceof Error) {
11
- throw token;
12
- }
13
- return token;
14
- }
15
- };
16
- }
17
- describe('isStaffImpersonation', () => {
18
- it('is false for a plain client login', async () => {
19
- await expect(isStaffImpersonation(clientWith(tokenWith({
20
- account_id: 42
21
- })))).resolves.toBe(false);
22
- });
23
- it('is true for staff impersonation without parent_user_id', async () => {
24
- await expect(isStaffImpersonation(clientWith(tokenWith({
25
- is_su: true
26
- })))).resolves.toBe(true);
27
- });
28
- it('is true for staff impersonation with parent_user_id "0"', async () => {
29
- await expect(isStaffImpersonation(clientWith(tokenWith({
30
- is_su: true,
31
- parent_user_id: '0'
32
- })))).resolves.toBe(true);
33
- });
34
- it('is false for a client switched into their own child account', async () => {
35
- await expect(isStaffImpersonation(clientWith(tokenWith({
36
- is_su: true,
37
- parent_user_id: '77123'
38
- })))).resolves.toBe(false);
39
- });
40
- it('is false when the token carries no claims at all', async () => {
41
- await expect(isStaffImpersonation(clientWith(tokenWith({})))).resolves.toBe(false);
42
- });
43
- it('is false for a garbage token', async () => {
44
- await expect(isStaffImpersonation(clientWith('not-a-jwt'))).resolves.toBe(false);
45
- });
46
- it('is false when there is no token to read', async () => {
47
- await expect(isStaffImpersonation(clientWith(new Error('Unauthorized!')))).resolves.toBe(false);
48
- });
49
- });