@pushwoosh/smart-components-kit 1.11.1 → 1.12.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.
@@ -0,0 +1,12 @@
1
+ import { type FC } from 'react';
2
+ import type { MediaFile } from '@pushwoosh/rpc-v2-http-api-data/media_files';
3
+ type GenerateImageTabProps = {
4
+ application: string;
5
+ aspectRatios?: string[];
6
+ defaultAspectRatio?: string;
7
+ onSelect: (file: MediaFile, alt: string) => void;
8
+ onGenerated: () => void;
9
+ };
10
+ /** Prompt → one image that onyx stores in the library; the preview is already the stored file. */
11
+ export declare const GenerateImageTab: FC<GenerateImageTabProps>;
12
+ export {};
@@ -0,0 +1,104 @@
1
+ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
+ import { useState } from 'react';
3
+ import { Button, NativeSelect } from '@pushwoosh/dumb-components';
4
+ import { useSPAContext } from '@pushwoosh/spa-context';
5
+ import { GENERATE_ASPECT_RATIOS, toMediaFile } from './aiGallery';
6
+ import { useErrorToast } from './hooks';
7
+ import { useLang } from './polyglot';
8
+ import { GenerateControls, GeneratedPreview, GenerateForm, PromptField } from './styles';
9
+ const ASPECT_LABELS = {
10
+ '1:1': 'SMART.MEDIA_STORAGE.GENERATE.ASPECT_SQUARE',
11
+ '4:3': 'SMART.MEDIA_STORAGE.GENERATE.ASPECT_LANDSCAPE',
12
+ '16:9': 'SMART.MEDIA_STORAGE.GENERATE.ASPECT_WIDE',
13
+ '21:9': 'SMART.MEDIA_STORAGE.GENERATE.ASPECT_BANNER',
14
+ '3:4': 'SMART.MEDIA_STORAGE.GENERATE.ASPECT_PORTRAIT',
15
+ '9:16': 'SMART.MEDIA_STORAGE.GENERATE.ASPECT_TALL'
16
+ };
17
+ const isLabelledAspect = ratio => ratio in ASPECT_LABELS;
18
+ const generateButtonMessage = (busy, hasResult) => {
19
+ if (busy) return 'SMART.MEDIA_STORAGE.GENERATE.GENERATING';
20
+ return hasResult ? 'SMART.MEDIA_STORAGE.GENERATE.AGAIN' : 'SMART.MEDIA_STORAGE.GENERATE.BUTTON';
21
+ };
22
+ /** Prompt → one image that onyx stores in the library; the preview is already the stored file. */
23
+ export const GenerateImageTab = ({
24
+ application,
25
+ aspectRatios,
26
+ defaultAspectRatio,
27
+ onSelect,
28
+ onGenerated
29
+ }) => {
30
+ const {
31
+ rpcGatewayAIAssistant
32
+ } = useSPAContext();
33
+ const errorToast = useErrorToast();
34
+ const lang = useLang();
35
+ const ratios = aspectRatios?.length ? aspectRatios : GENERATE_ASPECT_RATIOS;
36
+ const [prompt, setPrompt] = useState('');
37
+ const [aspect, setAspect] = useState(defaultAspectRatio && ratios.includes(defaultAspectRatio) ? defaultAspectRatio : ratios[0]);
38
+ const [busy, setBusy] = useState(false);
39
+ const [generated, setGenerated] = useState(null);
40
+ const aspectLabel = ratio => {
41
+ return isLabelledAspect(ratio) ? lang({
42
+ message: ASPECT_LABELS[ratio]
43
+ }) : ratio;
44
+ };
45
+ const generate = async () => {
46
+ const text = prompt.trim();
47
+ if (!text || busy) return;
48
+ setGenerated(null);
49
+ setBusy(true);
50
+ try {
51
+ const res = await rpcGatewayAIAssistant.aiGen.GenerateMediaImage({
52
+ application,
53
+ prompt: text,
54
+ aspectRatio: aspect
55
+ });
56
+ setGenerated({
57
+ file: toMediaFile(res, 'ai', `${res.mediaUuid}.png`),
58
+ alt: text
59
+ });
60
+ onGenerated();
61
+ } catch (error) {
62
+ errorToast('SMART.MEDIA_STORAGE.GENERATE.ERROR', error);
63
+ } finally {
64
+ setBusy(false);
65
+ }
66
+ };
67
+ return _jsxs(GenerateForm, {
68
+ children: [_jsx(PromptField, {
69
+ value: prompt,
70
+ rows: 3,
71
+ placeholder: lang({
72
+ message: 'SMART.MEDIA_STORAGE.GENERATE.PROMPT_PLACEHOLDER'
73
+ }),
74
+ onChange: event => setPrompt(event.target.value)
75
+ }), _jsxs(GenerateControls, {
76
+ children: [ratios.length > 1 && _jsx(NativeSelect, {
77
+ value: aspect,
78
+ onChange: event => setAspect(event.target.value),
79
+ children: ratios.map(ratio => _jsx("option", {
80
+ value: ratio,
81
+ children: aspectLabel(ratio)
82
+ }, ratio))
83
+ }), _jsx(Button, {
84
+ color: "primary",
85
+ size: "field",
86
+ isDisabled: busy || !prompt.trim(),
87
+ onClick: () => void generate(),
88
+ children: lang({
89
+ message: generateButtonMessage(busy, generated !== null)
90
+ })
91
+ }), generated && _jsx(Button, {
92
+ color: "secondary",
93
+ size: "field",
94
+ onClick: () => onSelect(generated.file, generated.alt),
95
+ children: lang({
96
+ message: 'SMART.MEDIA_STORAGE.GENERATE.SELECT'
97
+ })
98
+ })]
99
+ }), generated && _jsx(GeneratedPreview, {
100
+ src: generated.file.url,
101
+ alt: generated.alt
102
+ })]
103
+ });
104
+ };
@@ -1,19 +1,27 @@
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, Checkbox, DropdownMenuButton, DropdownMenuItem, DropdownMenuSection, ModalConfirm, useToast } from '@pushwoosh/dumb-components';
3
+ import { Button, Checkbox, DropdownMenuButton, DropdownMenuItem, DropdownMenuSection, ModalConfirm, Tab, TabBar, TabPanel, Tabs, useToast } from '@pushwoosh/dumb-components';
4
4
  import { FlexContainer, useAsync } from '@pushwoosh/kit-helpers';
5
5
  import { CheckIcon, DropdownIcon, EditIcon, OpenIcon, TrashIcon, UploadIcon } from '@pushwoosh/kit-icons';
6
6
  import { Text } from '@pushwoosh/kit-typography';
7
7
  import { useSPAContext } from '@pushwoosh/spa-context';
8
+ import { visibleSources } from './aiGallery';
8
9
  import { CANVA_DESIGN_PRESETS } from './canvaEdit';
9
10
  import { CanvaImportDrawer } from './CanvaImportDrawer';
10
11
  import { CreateCropDrawer } from './CreateCropDrawer';
12
+ import { GenerateImageTab } from './GenerateImageTab';
11
13
  import { useCanvaEdit, useDeleteMediaFile, useDeleteMediaFiles } from './hooks';
12
14
  import { formatBytes, getCropCaption, getSourceLabel, getThumbUrl, hasVersions } from './mediaFile.utils';
13
15
  import { Lang, useLang } from './polyglot';
14
- import { CanvaActions, CountBadge, Dropzone, DropzoneRow, EmptyState, Grid, HiddenInput, SelectionBar, SelectionCount, SourceBadge, Tile, TileActions, TileBadges, TileCheckbox, TileImage, UsageLine, WarningBadge } from './styles';
16
+ import { StockPhotosTab } from './StockPhotosTab';
17
+ import { CanvaActions, CountBadge, Dropzone, DropzoneRow, EmptyState, Grid, HiddenInput, SelectionBar, SelectionCount, SourceBadge, TabBody, Tile, TileActions, TileBadges, TileCheckbox, TileImage, UsageLine, WarningBadge } from './styles';
15
18
  const UPLOAD_PATH = '/api/media/upload';
16
19
  const ALLOWED_IMAGE_EXTENSIONS = ['.jpg', '.jpeg', '.png', '.gif', '.webp'];
20
+ const TAB_MESSAGES = {
21
+ library: 'SMART.MEDIA_STORAGE.TAB.LIBRARY',
22
+ stock: 'SMART.MEDIA_STORAGE.TAB.STOCK',
23
+ generate: 'SMART.MEDIA_STORAGE.TAB.GENERATE'
24
+ };
17
25
  const isAllowedImage = file => {
18
26
  const name = file.name.toLowerCase();
19
27
  return ALLOWED_IMAGE_EXTENSIONS.some(ext => name.endsWith(ext));
@@ -30,7 +38,11 @@ export const MediaStorage = ({
30
38
  onPageChange,
31
39
  onSelect,
32
40
  extensions,
33
- fileWarning
41
+ fileWarning,
42
+ sources,
43
+ aspectRatios,
44
+ defaultAspectRatio,
45
+ onSuggestAlt
34
46
  }) => {
35
47
  const lang = useLang();
36
48
  const {
@@ -94,6 +106,24 @@ export const MediaStorage = ({
94
106
  setReloadToken(token => token + 1);
95
107
  };
96
108
  const canvaEdit = useCanvaEdit(application, Boolean(canvaStatus?.available), showImportedFirst);
109
+ const tabs = visibleSources(sources, extensions);
110
+ const [selectedTab, setTab] = useState(tabs[0]);
111
+ const tab = tabs.includes(selectedTab) ? selectedTab : tabs[0];
112
+ const pickStored = (file, alt) => {
113
+ const warning = fileWarning?.(file);
114
+ if (warning) {
115
+ openToast({
116
+ title: warning,
117
+ type: 'warning'
118
+ });
119
+ setTab('library');
120
+ return;
121
+ }
122
+ if (alt) {
123
+ onSuggestAlt?.(file, alt);
124
+ }
125
+ onSelect(file);
126
+ };
97
127
  const {
98
128
  loading
99
129
  } = listAsync;
@@ -255,7 +285,7 @@ export const MediaStorage = ({
255
285
  const isAllSelected = files.length > 0 && selectedUuids.length === files.length;
256
286
  const totalPages = Math.ceil(total / perPage);
257
287
  const showPagination = !loading && files.length > 0 && totalPages > 1;
258
- return _jsxs(_Fragment, {
288
+ const library = _jsxs(_Fragment, {
259
289
  children: [_jsxs(DropzoneRow, {
260
290
  children: [_jsxs(Dropzone, {
261
291
  type: "button",
@@ -497,7 +527,48 @@ export const MediaStorage = ({
497
527
  size: formatBytes(usageAsync.value.totalSize)
498
528
  })
499
529
  })]
500
- }), canvaStatus?.available && _jsx(CanvaImportDrawer, {
530
+ })]
531
+ });
532
+ return _jsxs(_Fragment, {
533
+ children: [tabs.length > 1 ? _jsxs(Tabs, {
534
+ activeCode: tab,
535
+ onChange: code => setTab(code),
536
+ children: [_jsx(TabBar, {
537
+ children: tabs.map(source => _jsx(Tab, {
538
+ code: source,
539
+ children: _jsx(Lang, {
540
+ message: TAB_MESSAGES[source]
541
+ })
542
+ }, source))
543
+ }), _jsx(TabPanel, {
544
+ code: "library",
545
+ children: _jsx(TabBody, {
546
+ children: library
547
+ })
548
+ }), tabs.includes('stock') && _jsx(TabPanel, {
549
+ code: "stock",
550
+ children: _jsx(TabBody, {
551
+ children: _jsx(StockPhotosTab, {
552
+ application: application,
553
+ onImported: (file, alt) => {
554
+ showImportedFirst();
555
+ pickStored(file, alt);
556
+ }
557
+ })
558
+ })
559
+ }), tabs.includes('generate') && _jsx(TabPanel, {
560
+ code: "generate",
561
+ children: _jsx(TabBody, {
562
+ children: _jsx(GenerateImageTab, {
563
+ application: application,
564
+ aspectRatios: aspectRatios,
565
+ defaultAspectRatio: defaultAspectRatio,
566
+ onGenerated: showImportedFirst,
567
+ onSelect: pickStored
568
+ })
569
+ })
570
+ })]
571
+ }) : library, canvaStatus?.available && _jsx(CanvaImportDrawer, {
501
572
  application: application,
502
573
  isOpen: isCanvaOpen,
503
574
  status: canvaStatus,
@@ -8,21 +8,15 @@ const DEFAULT_PER_PAGE = 30;
8
8
  * `MediaStorage` directly to drive the page from the URL or another store.
9
9
  */
10
10
  export const MediaStoragePaginated = ({
11
- application,
12
11
  initialPage = 0,
13
12
  perPage = DEFAULT_PER_PAGE,
14
- onSelect,
15
- extensions,
16
- fileWarning
13
+ ...props
17
14
  }) => {
18
15
  const [page, setPage] = useState(initialPage);
19
16
  return _jsx(MediaStorage, {
20
- application: application,
17
+ ...props,
21
18
  page: page,
22
19
  perPage: perPage,
23
- onPageChange: setPage,
24
- onSelect: onSelect,
25
- extensions: extensions,
26
- fileWarning: fileWarning
20
+ onPageChange: setPage
27
21
  });
28
22
  };
@@ -0,0 +1,9 @@
1
+ import { type FC } from 'react';
2
+ import type { MediaFile } from '@pushwoosh/rpc-v2-http-api-data/media_files';
3
+ type StockPhotosTabProps = {
4
+ application: string;
5
+ onImported: (file: MediaFile, alt: string) => void;
6
+ };
7
+ /** Pexels search through onyx; picking a photo copies it into the library and hands back the stored file. */
8
+ export declare const StockPhotosTab: FC<StockPhotosTabProps>;
9
+ export {};
@@ -0,0 +1,135 @@
1
+ import { jsx as _jsx, jsxs as _jsxs, Fragment as _Fragment } from "react/jsx-runtime";
2
+ import { useState } from 'react';
3
+ import { Button } from '@pushwoosh/dumb-components';
4
+ import { useSPAContext } from '@pushwoosh/spa-context';
5
+ import { isStockUnconfigured, STOCK_QUERY_MAX_CHARS, toMediaFile } from './aiGallery';
6
+ import { useErrorToast } from './hooks';
7
+ import { Lang, useLang } from './polyglot';
8
+ import { EmptyState, Grid, ProviderNote, SearchField, SearchRow, StockCredit, StockTile, TileImage } from './styles';
9
+ const PEXELS_HOME = 'https://www.pexels.com';
10
+ /** Pexels search through onyx; picking a photo copies it into the library and hands back the stored file. */
11
+ export const StockPhotosTab = ({
12
+ application,
13
+ onImported
14
+ }) => {
15
+ const {
16
+ rpcGatewayAIAssistant
17
+ } = useSPAContext();
18
+ const errorToast = useErrorToast();
19
+ const lang = useLang();
20
+ const [query, setQuery] = useState('');
21
+ const [photos, setPhotos] = useState([]);
22
+ const [searched, setSearched] = useState(false);
23
+ const [loading, setLoading] = useState(false);
24
+ const [unconfigured, setUnconfigured] = useState(false);
25
+ const [importingId, setImportingId] = useState(null);
26
+ const search = async () => {
27
+ const q = query.trim();
28
+ if (!q || loading) return;
29
+ setLoading(true);
30
+ try {
31
+ const res = await rpcGatewayAIAssistant.aiGen.SearchStockPhotos({
32
+ application,
33
+ query: q,
34
+ page: 1,
35
+ provider: 'STOCK_PROVIDER_PEXELS'
36
+ });
37
+ setPhotos(res.photos ?? []);
38
+ setSearched(true);
39
+ } catch (error) {
40
+ if (isStockUnconfigured(error)) {
41
+ setUnconfigured(true);
42
+ } else {
43
+ errorToast('SMART.MEDIA_STORAGE.STOCK.SEARCH_ERROR', error);
44
+ }
45
+ } finally {
46
+ setLoading(false);
47
+ }
48
+ };
49
+ const importPhoto = async photo => {
50
+ if (importingId) return;
51
+ setImportingId(photo.id);
52
+ try {
53
+ const res = await rpcGatewayAIAssistant.aiGen.ImportStockPhoto({
54
+ application,
55
+ provider: 'STOCK_PROVIDER_PEXELS',
56
+ photoId: photo.id
57
+ });
58
+ onImported(toMediaFile(res, 'stock', `pexels-${photo.id}.jpg`), res.alt || photo.alt || '');
59
+ } catch (error) {
60
+ errorToast('SMART.MEDIA_STORAGE.STOCK.IMPORT_ERROR', error);
61
+ } finally {
62
+ setImportingId(null);
63
+ }
64
+ };
65
+ if (unconfigured) {
66
+ return _jsx(EmptyState, {
67
+ children: _jsx(Lang, {
68
+ message: "SMART.MEDIA_STORAGE.STOCK.UNCONFIGURED"
69
+ })
70
+ });
71
+ }
72
+ return _jsxs(_Fragment, {
73
+ children: [_jsxs(SearchRow, {
74
+ children: [_jsx(SearchField, {
75
+ type: "text",
76
+ value: query,
77
+ maxLength: STOCK_QUERY_MAX_CHARS,
78
+ placeholder: lang({
79
+ message: 'SMART.MEDIA_STORAGE.STOCK.SEARCH_PLACEHOLDER'
80
+ }),
81
+ onChange: event => setQuery(event.target.value),
82
+ onKeyDown: event => {
83
+ if (event.key === 'Enter') void search();
84
+ }
85
+ }), _jsx(Button, {
86
+ color: "secondary",
87
+ size: "field",
88
+ isDisabled: loading || !query.trim(),
89
+ onClick: () => void search(),
90
+ children: lang({
91
+ message: loading ? 'SMART.MEDIA_STORAGE.STOCK.SEARCHING' : 'SMART.MEDIA_STORAGE.STOCK.SEARCH_BUTTON'
92
+ })
93
+ })]
94
+ }), !searched && !loading && _jsx(EmptyState, {
95
+ children: _jsx(Lang, {
96
+ message: "SMART.MEDIA_STORAGE.STOCK.HINT"
97
+ })
98
+ }), searched && !loading && photos.length === 0 && _jsx(EmptyState, {
99
+ children: _jsx(Lang, {
100
+ message: "SMART.MEDIA_STORAGE.STOCK.NO_RESULTS"
101
+ })
102
+ }), photos.length > 0 && _jsx(Grid, {
103
+ children: photos.map(photo => _jsxs("div", {
104
+ children: [_jsx(StockTile, {
105
+ type: "button",
106
+ disabled: importingId !== null,
107
+ title: lang({
108
+ message: importingId === photo.id ? 'SMART.MEDIA_STORAGE.STOCK.IMPORTING' : 'SMART.MEDIA_STORAGE.STOCK.CLICK_TO_SELECT'
109
+ }),
110
+ "$avgColor": photo.avgColor,
111
+ "$dimmed": importingId !== null && importingId !== photo.id,
112
+ onClick: () => void importPhoto(photo),
113
+ children: _jsx(TileImage, {
114
+ src: photo.thumbUrl,
115
+ alt: photo.alt
116
+ })
117
+ }), _jsx(StockCredit, {
118
+ href: photo.photographerUrl,
119
+ target: "_blank",
120
+ rel: "noreferrer",
121
+ children: photo.photographer
122
+ })]
123
+ }, photo.id))
124
+ }), _jsxs(ProviderNote, {
125
+ children: [_jsx(Lang, {
126
+ message: "SMART.MEDIA_STORAGE.STOCK.ATTRIBUTION"
127
+ }), ' ', _jsx("a", {
128
+ href: PEXELS_HOME,
129
+ target: "_blank",
130
+ rel: "noreferrer",
131
+ children: "Pexels"
132
+ })]
133
+ })]
134
+ });
135
+ };
@@ -0,0 +1,49 @@
1
+ import type { MediaFile } from '@pushwoosh/rpc-v2-http-api-data/media_files';
2
+ import type { MediaSource } from './types';
3
+ /** Stored file as onyx returns it with a generated or imported image (`AiGenMediaFile`). */
4
+ export type AiGenMediaFile = {
5
+ uuid: string;
6
+ filename: string;
7
+ url: string;
8
+ fileSize: number;
9
+ mimeType: string;
10
+ createdAt: string;
11
+ description?: string;
12
+ width: number;
13
+ height: number;
14
+ thumbnailUrl: string;
15
+ thumbnailWidth: number;
16
+ thumbnailHeight: number;
17
+ };
18
+ /** The part of a Generate / Stock import response the gallery reads; `file` is absent on older onyx builds. */
19
+ export type AiGenStoredImage = {
20
+ url: string;
21
+ mediaUuid: string;
22
+ width: number;
23
+ height: number;
24
+ file?: AiGenMediaFile;
25
+ };
26
+ /** Aspect ratios onyx accepts for a generated image, in picker order. */
27
+ export declare const GENERATE_ASPECT_RATIOS: string[];
28
+ /** One Pexels hit from `aiGen.SearchStockPhotos`. */
29
+ export type StockPhoto = {
30
+ id: string;
31
+ thumbUrl: string;
32
+ width: number;
33
+ height: number;
34
+ photographer: string;
35
+ photographerUrl: string;
36
+ avgColor: string;
37
+ alt: string;
38
+ };
39
+ /** Longest stock query onyx accepts (`stockphotos.MaxQueryChars`); capping the input keeps 400 meaning "not configured". */
40
+ export declare const STOCK_QUERY_MAX_CHARS = 200;
41
+ /** onyx answers an unconfigured Pexels key with FailedPrecondition, which the gateway turns into HTTP 400. */
42
+ export declare const isStockUnconfigured: (error: unknown) => boolean;
43
+ /**
44
+ * Sources to show: the requested ones minus those whose output the channel's `extensions` filter rejects.
45
+ * Stock is always JPEG; Generate is PNG or JPEG depending on the model, so it needs both accepted.
46
+ */
47
+ export declare const visibleSources: (sources: MediaSource[] | undefined, extensions: string[] | undefined) => MediaSource[];
48
+ /** Media-library file for an image onyx just stored, so `onSelect` gets the same shape as a library pick. */
49
+ export declare const toMediaFile: (stored: AiGenStoredImage, source: "ai" | "stock", fallbackName: string) => MediaFile;
@@ -0,0 +1,55 @@
1
+ /** Aspect ratios onyx accepts for a generated image, in picker order. */
2
+ export const GENERATE_ASPECT_RATIOS = ['1:1', '4:3', '3:2', '16:9', '21:9', '5:4', '4:5', '3:4', '2:3', '9:16'];
3
+ /** Longest stock query onyx accepts (`stockphotos.MaxQueryChars`); capping the input keeps 400 meaning "not configured". */
4
+ export const STOCK_QUERY_MAX_CHARS = 200;
5
+ /** onyx answers an unconfigured Pexels key with FailedPrecondition, which the gateway turns into HTTP 400. */
6
+ export const isStockUnconfigured = error => error.code === 400;
7
+ const SOURCE_EXTENSIONS = {
8
+ stock: [['jpg', 'jpeg']],
9
+ generate: [['png'], ['jpg', 'jpeg']]
10
+ };
11
+ const normalizeExtension = ext => ext.replace(/^\./, '').toLowerCase();
12
+ /**
13
+ * Sources to show: the requested ones minus those whose output the channel's `extensions` filter rejects.
14
+ * Stock is always JPEG; Generate is PNG or JPEG depending on the model, so it needs both accepted.
15
+ */
16
+ export const visibleSources = (sources, extensions) => {
17
+ const requested = sources?.length ? sources : ['library'];
18
+ const allowed = extensions?.map(normalizeExtension);
19
+ return requested.filter(source => {
20
+ if (source === 'library' || !allowed) return true;
21
+ return SOURCE_EXTENSIONS[source].every(formats => formats.some(ext => allowed.includes(ext)));
22
+ });
23
+ };
24
+ /** Media-library file for an image onyx just stored, so `onSelect` gets the same shape as a library pick. */
25
+ export const toMediaFile = (stored, source, fallbackName) => {
26
+ const {
27
+ file
28
+ } = stored;
29
+ const width = file?.width ?? stored.width;
30
+ const height = file?.height ?? stored.height;
31
+ return {
32
+ uuid: file?.uuid || stored.mediaUuid,
33
+ filename: file?.filename || fallbackName,
34
+ url: file?.url || stored.url,
35
+ fileSize: file?.fileSize ?? 0,
36
+ mimeType: file?.mimeType ?? '',
37
+ createdAt: file?.createdAt ? new Date(file.createdAt) : new Date(),
38
+ description: file?.description,
39
+ source,
40
+ version: 0,
41
+ versionRootUuid: file?.uuid || stored.mediaUuid,
42
+ versionsCount: 1,
43
+ meta: {
44
+ type: 'imageMeta',
45
+ data: {
46
+ width,
47
+ height,
48
+ thumbnailUrl: file?.thumbnailUrl || file?.url || stored.url,
49
+ thumbnailWidth: file?.thumbnailWidth ?? width,
50
+ thumbnailHeight: file?.thumbnailHeight ?? height,
51
+ ratio: ''
52
+ }
53
+ }
54
+ };
55
+ };
@@ -13,4 +13,7 @@ type CanvaEdit = {
13
13
  };
14
14
  /** Sends images to Canva and, on the page Canva returns to, stores what the user brought back. */
15
15
  export declare function useCanvaEdit(application: string, isAvailable: boolean, onSaved: () => void): CanvaEdit;
16
+ type ErrorToastTitle = 'SMART.MEDIA_STORAGE.STOCK.SEARCH_ERROR' | 'SMART.MEDIA_STORAGE.STOCK.IMPORT_ERROR' | 'SMART.MEDIA_STORAGE.GENERATE.ERROR';
17
+ /** Error toast with a fixed title and the server's message as the description. */
18
+ export declare function useErrorToast(): (title: ErrorToastTitle, error: unknown) => void;
16
19
  export {};
@@ -204,4 +204,20 @@ export function useCanvaEdit(application, isAvailable, onSaved) {
204
204
  create,
205
205
  isSaving
206
206
  };
207
+ }
208
+ /** Error toast with a fixed title and the server's message as the description. */
209
+ export function useErrorToast() {
210
+ const {
211
+ openToast
212
+ } = useToast();
213
+ const lang = useLang();
214
+ return usePersistentFunction((title, error) => {
215
+ openToast({
216
+ title: lang({
217
+ message: title
218
+ }),
219
+ description: error.message,
220
+ type: 'error'
221
+ });
222
+ });
207
223
  }
@@ -1,4 +1,4 @@
1
1
  export { MediaStorage } from './MediaStorage';
2
2
  export { MediaStoragePaginated } from './MediaStoragePaginated';
3
- export type { MediaStorageProps, MediaStoragePaginatedProps } from './types';
3
+ export type { MediaSource, MediaStorageProps, MediaStoragePaginatedProps } from './types';
4
4
  export { connectCanva, ConsentClosedError, disconnectCanva, PopupBlockedError, UnsafeReturnPageError, } from './canva';
@@ -13,7 +13,9 @@ export const getCropCaption = file => {
13
13
  };
14
14
  const SOURCE_LABELS = {
15
15
  canva: 'Canva',
16
- figma: 'Figma'
16
+ figma: 'Figma',
17
+ ai: 'AI',
18
+ stock: 'Pexels'
17
19
  };
18
20
  /** Brand name of the service an image was imported from; undefined for a file uploaded from disk. */
19
21
  export const getSourceLabel = file => file.source ? SOURCE_LABELS[file.source] : undefined;
@@ -79,5 +79,31 @@ export const messages = {
79
79
  'SMART.MEDIA_STORAGE.CANVA.SAVED': 'تم الحفظ من Canva',
80
80
  'SMART.MEDIA_STORAGE.CANVA.EDIT_COPY': 'تصميم Canva لهذه الصورة يخص مستخدمًا آخر — أنت تحرر نسخة',
81
81
  'SMART.MEDIA_STORAGE.CANVA.EDIT_ERROR': 'تعذّر فتح Canva',
82
- 'SMART.MEDIA_STORAGE.CANVA.SAVE_ERROR': 'تعذّر حفظ التصميم من Canva'
82
+ 'SMART.MEDIA_STORAGE.CANVA.SAVE_ERROR': 'تعذّر حفظ التصميم من Canva',
83
+ 'SMART.MEDIA_STORAGE.TAB.LIBRARY': 'المكتبة',
84
+ 'SMART.MEDIA_STORAGE.TAB.STOCK': 'صور المخزون',
85
+ 'SMART.MEDIA_STORAGE.TAB.GENERATE': 'إنشاء بالذكاء الاصطناعي',
86
+ 'SMART.MEDIA_STORAGE.STOCK.SEARCH_PLACEHOLDER': 'ابحث عن صور مجانية… (مثل: تخفيضات الصيف، قهوة، مدينة)',
87
+ 'SMART.MEDIA_STORAGE.STOCK.SEARCH_BUTTON': 'بحث',
88
+ 'SMART.MEDIA_STORAGE.STOCK.SEARCHING': 'جارٍ البحث…',
89
+ 'SMART.MEDIA_STORAGE.STOCK.HINT': 'ابحث في ملايين الصور المجانية',
90
+ 'SMART.MEDIA_STORAGE.STOCK.NO_RESULTS': 'لم يتم العثور على شيء — جرب بحثًا آخر',
91
+ 'SMART.MEDIA_STORAGE.STOCK.UNCONFIGURED': 'صور المخزون غير مهيأة في هذا التثبيت.',
92
+ 'SMART.MEDIA_STORAGE.STOCK.SEARCH_ERROR': 'فشل البحث في الصور',
93
+ 'SMART.MEDIA_STORAGE.STOCK.IMPORT_ERROR': 'تعذر استيراد الصورة',
94
+ 'SMART.MEDIA_STORAGE.STOCK.IMPORTING': 'جارٍ الاستيراد…',
95
+ 'SMART.MEDIA_STORAGE.STOCK.CLICK_TO_SELECT': 'انقر للتحديد',
96
+ 'SMART.MEDIA_STORAGE.STOCK.ATTRIBUTION': 'الصور مقدمة من',
97
+ 'SMART.MEDIA_STORAGE.GENERATE.PROMPT_PLACEHOLDER': 'صف الصورة… (مثال: صورة مسطحة لمستلزمات نزهة صيفية على خلفية بألوان الباستيل)',
98
+ 'SMART.MEDIA_STORAGE.GENERATE.BUTTON': 'إنشاء',
99
+ 'SMART.MEDIA_STORAGE.GENERATE.AGAIN': 'إنشاء مرة أخرى',
100
+ 'SMART.MEDIA_STORAGE.GENERATE.GENERATING': 'جارٍ الإنشاء…',
101
+ 'SMART.MEDIA_STORAGE.GENERATE.ERROR': 'فشل إنشاء الصورة',
102
+ 'SMART.MEDIA_STORAGE.GENERATE.SELECT': 'إدراج صورة',
103
+ 'SMART.MEDIA_STORAGE.GENERATE.ASPECT_SQUARE': 'مربع (1:1)',
104
+ 'SMART.MEDIA_STORAGE.GENERATE.ASPECT_LANDSCAPE': 'منظر طبيعي (4:3)',
105
+ 'SMART.MEDIA_STORAGE.GENERATE.ASPECT_WIDE': 'عريض (16:9)',
106
+ 'SMART.MEDIA_STORAGE.GENERATE.ASPECT_BANNER': 'بانر (21:9)',
107
+ 'SMART.MEDIA_STORAGE.GENERATE.ASPECT_PORTRAIT': 'بورتريه (3:4)',
108
+ 'SMART.MEDIA_STORAGE.GENERATE.ASPECT_TALL': 'طويل (9:16)'
83
109
  };
@@ -79,5 +79,31 @@ export const messages = {
79
79
  'SMART.MEDIA_STORAGE.CANVA.SAVED': 'Canva থেকে সংরক্ষিত হয়েছে',
80
80
  'SMART.MEDIA_STORAGE.CANVA.EDIT_COPY': 'এই ছবির Canva ডিজাইনটি অন্য কারও — আপনি একটি কপি এডিট করছেন',
81
81
  'SMART.MEDIA_STORAGE.CANVA.EDIT_ERROR': 'Canva খোলা যায়নি',
82
- 'SMART.MEDIA_STORAGE.CANVA.SAVE_ERROR': 'Canva থেকে ডিজাইন সংরক্ষণ করা যায়নি'
82
+ 'SMART.MEDIA_STORAGE.CANVA.SAVE_ERROR': 'Canva থেকে ডিজাইন সংরক্ষণ করা যায়নি',
83
+ 'SMART.MEDIA_STORAGE.TAB.LIBRARY': 'লাইব্রেরি',
84
+ 'SMART.MEDIA_STORAGE.TAB.STOCK': 'স্টক ফটো',
85
+ 'SMART.MEDIA_STORAGE.TAB.GENERATE': 'AI দিয়ে তৈরি করুন',
86
+ 'SMART.MEDIA_STORAGE.STOCK.SEARCH_PLACEHOLDER': 'বিনামূল্যে ছবি খুঁজুন… (যেমন গ্রীষ্মকালীন সেল, কফি, শহর)',
87
+ 'SMART.MEDIA_STORAGE.STOCK.SEARCH_BUTTON': 'অনুসন্ধান',
88
+ 'SMART.MEDIA_STORAGE.STOCK.SEARCHING': 'অনুসন্ধান করা হচ্ছে…',
89
+ 'SMART.MEDIA_STORAGE.STOCK.HINT': 'লক্ষ লক্ষ বিনামূল্যে স্টক ফটো অনুসন্ধান করুন',
90
+ 'SMART.MEDIA_STORAGE.STOCK.NO_RESULTS': 'কিছু পাওয়া যায়নি — অন্য কোনো কোয়েরি দিয়ে চেষ্টা করুন',
91
+ 'SMART.MEDIA_STORAGE.STOCK.UNCONFIGURED': 'এই ইনস্টলেশনে স্টক ফটোগুলো কনফিগার করা নেই।',
92
+ 'SMART.MEDIA_STORAGE.STOCK.SEARCH_ERROR': 'স্টক অনুসন্ধানে ব্যর্থ হয়েছে',
93
+ 'SMART.MEDIA_STORAGE.STOCK.IMPORT_ERROR': 'ছবিটি ইম্পোর্ট করা যায়নি',
94
+ 'SMART.MEDIA_STORAGE.STOCK.IMPORTING': 'ইম্পোর্ট করা হচ্ছে…',
95
+ 'SMART.MEDIA_STORAGE.STOCK.CLICK_TO_SELECT': 'নির্বাচন করতে ক্লিক করুন',
96
+ 'SMART.MEDIA_STORAGE.STOCK.ATTRIBUTION': 'ছবি সরবরাহ করেছে',
97
+ 'SMART.MEDIA_STORAGE.GENERATE.PROMPT_PLACEHOLDER': 'ছবিটি বর্ণনা করুন… (যেমন, প্যাস্টেল ব্যাকগ্রাউন্ডে গ্রীষ্মের পিকনিকের প্রয়োজনীয় জিনিসপত্রের ফ্ল্যাট-লে)',
98
+ 'SMART.MEDIA_STORAGE.GENERATE.BUTTON': 'জেনারেট করুন',
99
+ 'SMART.MEDIA_STORAGE.GENERATE.AGAIN': 'আবার জেনারেট করুন',
100
+ 'SMART.MEDIA_STORAGE.GENERATE.GENERATING': 'জেনারেট করা হচ্ছে…',
101
+ 'SMART.MEDIA_STORAGE.GENERATE.ERROR': 'ছবি জেনারেট করা যায়নি',
102
+ 'SMART.MEDIA_STORAGE.GENERATE.SELECT': 'ছবি যোগ করুন',
103
+ 'SMART.MEDIA_STORAGE.GENERATE.ASPECT_SQUARE': 'বর্গক্ষেত্র (1:1)',
104
+ 'SMART.MEDIA_STORAGE.GENERATE.ASPECT_LANDSCAPE': 'ল্যান্ডস্কেপ (4:3)',
105
+ 'SMART.MEDIA_STORAGE.GENERATE.ASPECT_WIDE': 'চওড়া (16:9)',
106
+ 'SMART.MEDIA_STORAGE.GENERATE.ASPECT_BANNER': 'ব্যানার (21:9)',
107
+ 'SMART.MEDIA_STORAGE.GENERATE.ASPECT_PORTRAIT': 'পোর্ট্রেট (3:4)',
108
+ 'SMART.MEDIA_STORAGE.GENERATE.ASPECT_TALL': 'লম্বা (9:16)'
83
109
  };