@aglyn/shared-ui-jsx 1.0.0-beta.277 → 1.0.0-beta.283
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.
- package/package.json +6 -5
- package/src/lib/components/beta-chip.component.d.ts +27 -0
- package/src/lib/components/beta-chip.component.js +65 -0
- package/src/lib/components/beta-chip.component.js.map +1 -0
- package/src/lib/components/confirmation-provider.component.js +6 -0
- package/src/lib/components/confirmation-provider.component.js.map +1 -1
- package/src/lib/components/dialog-confirm.js +2 -0
- package/src/lib/components/dialog-confirm.js.map +1 -1
- package/src/lib/components/permission-choice-list.component.d.ts +72 -0
- package/src/lib/components/permission-choice-list.component.js +225 -0
- package/src/lib/components/permission-choice-list.component.js.map +1 -0
- package/src/lib/components/status-chip.component.d.ts +5 -0
- package/src/lib/components/status-chip.component.js +4 -2
- package/src/lib/components/status-chip.component.js.map +1 -1
- package/src/lib/components/stock-photo-search.component.d.ts +117 -0
- package/src/lib/components/stock-photo-search.component.js +506 -0
- package/src/lib/components/stock-photo-search.component.js.map +1 -0
- package/src/lib/hooks/use-discard-changes-menu-item.d.ts +55 -0
- package/src/lib/hooks/use-discard-changes-menu-item.js +83 -0
- package/src/lib/hooks/use-discard-changes-menu-item.js.map +1 -0
- package/src/lib/hooks/use-online-status.d.ts +16 -0
- package/src/lib/hooks/use-online-status.js +86 -0
- package/src/lib/hooks/use-online-status.js.map +1 -0
|
@@ -0,0 +1,117 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @license
|
|
3
|
+
* Copyright 2026 Aglyn LLC
|
|
4
|
+
*
|
|
5
|
+
* Licensed under the Apache License, Version 2.0 (the "License");
|
|
6
|
+
* you may not use this file except in compliance with the License.
|
|
7
|
+
* You may obtain a copy of the License at
|
|
8
|
+
*
|
|
9
|
+
* http://www.apache.org/licenses/LICENSE-2.0
|
|
10
|
+
*
|
|
11
|
+
* Unless required by applicable law or agreed to in writing, software
|
|
12
|
+
* distributed under the License is distributed on an "AS IS" BASIS,
|
|
13
|
+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
14
|
+
* See the License for the specific language governing permissions and
|
|
15
|
+
* limitations under the License.
|
|
16
|
+
*/
|
|
17
|
+
/**
|
|
18
|
+
* SEARCH A STOCK PHOTO LIBRARY AND CHOOSE ONE (AGL-4066): the media picker's
|
|
19
|
+
* Stock photos tab, and the details of the photo chosen.
|
|
20
|
+
*
|
|
21
|
+
* Presentation only. What a search asks and where a photo goes are the
|
|
22
|
+
* caller's: it hands in a {@link StockPhotoSearchClient} (the console binds
|
|
23
|
+
* its route and the member's credentials) and receives the photo clicked,
|
|
24
|
+
* which it places the way it places a library file. Every result shows its
|
|
25
|
+
* photographer and the library it comes from, since a library's terms ask
|
|
26
|
+
* that its results say where they are from.
|
|
27
|
+
*
|
|
28
|
+
* Thumbnails arrive as image bytes from the caller's server and are shown
|
|
29
|
+
* through object URLs this component owns, so no address at the library is
|
|
30
|
+
* ever loaded by the browser; it fetches them a few at a time, only for the
|
|
31
|
+
* results on screen.
|
|
32
|
+
*/
|
|
33
|
+
/** The shapes the search offers. */
|
|
34
|
+
export type StockPhotoSearchOrientation = 'any' | 'horizontal' | 'vertical';
|
|
35
|
+
/** One result, as much of it as this shows. */
|
|
36
|
+
export interface StockPhotoSearchHit {
|
|
37
|
+
/** What the caller names the photo by when it asks for its preview. */
|
|
38
|
+
ref: unknown;
|
|
39
|
+
/** Unique among the results. */
|
|
40
|
+
sourceKey: string;
|
|
41
|
+
width: number;
|
|
42
|
+
height: number;
|
|
43
|
+
photographer: string;
|
|
44
|
+
tags: string[];
|
|
45
|
+
alt?: string;
|
|
46
|
+
}
|
|
47
|
+
/** The details panel's further facts about a result. */
|
|
48
|
+
export interface StockPhotoDetailsHit extends StockPhotoSearchHit {
|
|
49
|
+
photographerUrl?: string;
|
|
50
|
+
pageUrl: string;
|
|
51
|
+
/** "Photo by … on …". */
|
|
52
|
+
credit: string;
|
|
53
|
+
license: string;
|
|
54
|
+
licenseUrl: string;
|
|
55
|
+
}
|
|
56
|
+
type Refusal = {
|
|
57
|
+
status: 'busy' | 'unconfigured' | 'refused' | 'gone';
|
|
58
|
+
message: string;
|
|
59
|
+
};
|
|
60
|
+
/** What the caller's client answers. */
|
|
61
|
+
export interface StockPhotoSearchClient<Hit extends StockPhotoSearchHit> {
|
|
62
|
+
status(): Promise<{
|
|
63
|
+
status: 'ready';
|
|
64
|
+
library: {
|
|
65
|
+
label: string;
|
|
66
|
+
};
|
|
67
|
+
} | Refusal>;
|
|
68
|
+
search(query: string, orientation: StockPhotoSearchOrientation): Promise<{
|
|
69
|
+
status: 'ok';
|
|
70
|
+
library: {
|
|
71
|
+
label: string;
|
|
72
|
+
};
|
|
73
|
+
hits: Hit[];
|
|
74
|
+
} | Refusal>;
|
|
75
|
+
preview(ref: Hit['ref']): Promise<Blob | null>;
|
|
76
|
+
}
|
|
77
|
+
export interface StockPhotoSearchPanelProps<Hit extends StockPhotoSearchHit> {
|
|
78
|
+
client: StockPhotoSearchClient<Hit>;
|
|
79
|
+
/** Where a used photo is saved, as a sentence ends: "this site's library". */
|
|
80
|
+
libraryName: string;
|
|
81
|
+
/** The longest search the library takes. */
|
|
82
|
+
maxQueryLength: number;
|
|
83
|
+
/** The result currently chosen. */
|
|
84
|
+
selectedKey?: string | null;
|
|
85
|
+
/** A result was clicked; `previewUrl` is its thumbnail, when it has loaded. */
|
|
86
|
+
onSelect: (hit: Hit, context: {
|
|
87
|
+
previewUrl: string | null;
|
|
88
|
+
libraryLabel: string;
|
|
89
|
+
}) => void;
|
|
90
|
+
}
|
|
91
|
+
/** Results shown at first, and added by each Show more. */
|
|
92
|
+
export declare const STOCK_PHOTO_SEARCH_PAGE = 20;
|
|
93
|
+
/**
|
|
94
|
+
* The Stock photos tab: a search box, the shape, and the results. Asks the
|
|
95
|
+
* client for its status once, on mount, so a deployment with no library, or
|
|
96
|
+
* a member who cannot add files here, is told so before typing anything.
|
|
97
|
+
*/
|
|
98
|
+
export declare function StockPhotoSearchPanel<Hit extends StockPhotoSearchHit>(props: StockPhotoSearchPanelProps<Hit>): import("react").JSX.Element;
|
|
99
|
+
export interface StockPhotoDetailsProps {
|
|
100
|
+
hit: StockPhotoDetailsHit;
|
|
101
|
+
previewUrl: string | null;
|
|
102
|
+
/** The library's name: "Pixabay". */
|
|
103
|
+
libraryLabel: string;
|
|
104
|
+
/** Where using the photo saves it, as a sentence ends: "this site's library". */
|
|
105
|
+
libraryName: string;
|
|
106
|
+
/** Why using it failed, said above the facts. */
|
|
107
|
+
error?: string | null;
|
|
108
|
+
/** Small screens, where the details replace the results: back to them. */
|
|
109
|
+
onBack?: () => void;
|
|
110
|
+
}
|
|
111
|
+
/**
|
|
112
|
+
* The stock photo clicked, before it is used: a larger look, who took it,
|
|
113
|
+
* the license and where it will be saved. Nothing is copied until the
|
|
114
|
+
* caller's confirm button is pressed.
|
|
115
|
+
*/
|
|
116
|
+
export declare function StockPhotoDetails(props: StockPhotoDetailsProps): import("react").JSX.Element;
|
|
117
|
+
export {};
|
|
@@ -0,0 +1,506 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @license
|
|
3
|
+
* Copyright 2026 Aglyn LLC
|
|
4
|
+
*
|
|
5
|
+
* Licensed under the Apache License, Version 2.0 (the "License");
|
|
6
|
+
* you may not use this file except in compliance with the License.
|
|
7
|
+
* You may obtain a copy of the License at
|
|
8
|
+
*
|
|
9
|
+
* http://www.apache.org/licenses/LICENSE-2.0
|
|
10
|
+
*
|
|
11
|
+
* Unless required by applicable law or agreed to in writing, software
|
|
12
|
+
* distributed under the License is distributed on an "AS IS" BASIS,
|
|
13
|
+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
14
|
+
* See the License for the specific language governing permissions and
|
|
15
|
+
* limitations under the License.
|
|
16
|
+
*/ 'use client';
|
|
17
|
+
import { _ as _extends } from "@swc/helpers/_/_extends";
|
|
18
|
+
import { jsx as _jsx, jsxs as _jsxs, Fragment as _Fragment } from "react/jsx-runtime";
|
|
19
|
+
import { mdiArrowLeft, mdiImageOutline, mdiMagnify } from "@mdi/js";
|
|
20
|
+
import { Alert, Box, Button, ButtonBase, InputAdornment, LinearProgress, Link, Skeleton, Stack, TextField, ToggleButton, ToggleButtonGroup, Typography } from "@mui/material";
|
|
21
|
+
import { useCallback, useEffect, useRef, useState } from "react";
|
|
22
|
+
import EmptyStateComponent from "./empty-state.component.js";
|
|
23
|
+
import { MdiIcon } from "./mdi-icon/index.js";
|
|
24
|
+
/** Results shown at first, and added by each Show more. */ export const STOCK_PHOTO_SEARCH_PAGE = 20;
|
|
25
|
+
/** Previews fetched at once. */ const PREVIEWS_AT_ONCE = 4;
|
|
26
|
+
/** Skeleton tiles while a search runs. */ const SKELETON_TILES = 12;
|
|
27
|
+
const ORIENTATION_LABELS = [
|
|
28
|
+
[
|
|
29
|
+
'any',
|
|
30
|
+
'Any'
|
|
31
|
+
],
|
|
32
|
+
[
|
|
33
|
+
'horizontal',
|
|
34
|
+
'Landscape'
|
|
35
|
+
],
|
|
36
|
+
[
|
|
37
|
+
'vertical',
|
|
38
|
+
'Portrait'
|
|
39
|
+
]
|
|
40
|
+
];
|
|
41
|
+
/** The grid every state of the results lays out in. */ const gridSx = (theme)=>({
|
|
42
|
+
display: 'grid',
|
|
43
|
+
gap: theme.spacing(2),
|
|
44
|
+
gridTemplateColumns: `repeat(auto-fill, minmax(${theme.spacing(20)}, 1fr))`
|
|
45
|
+
});
|
|
46
|
+
/** A result's tile: the thumbnail, the photographer and the library. */ function StockPhotoTile(props) {
|
|
47
|
+
const { hit, previewUrl, libraryLabel, selected, onClick } = props;
|
|
48
|
+
const described = hit.alt || hit.tags.slice(0, 3).join(', ');
|
|
49
|
+
return /*#__PURE__*/ _jsxs(ButtonBase, {
|
|
50
|
+
onClick: onClick,
|
|
51
|
+
"aria-pressed": selected,
|
|
52
|
+
"aria-label": `Photo by ${hit.photographer} on ${libraryLabel}${described ? `: ${described}` : ''}`,
|
|
53
|
+
"data-testid": "stock-photo-tile",
|
|
54
|
+
sx: (theme)=>({
|
|
55
|
+
display: 'flex',
|
|
56
|
+
flexDirection: 'column',
|
|
57
|
+
alignItems: 'stretch',
|
|
58
|
+
textAlign: 'left',
|
|
59
|
+
borderRadius: 1,
|
|
60
|
+
overflow: 'hidden',
|
|
61
|
+
border: 1,
|
|
62
|
+
borderColor: selected ? 'primary.main' : 'divider',
|
|
63
|
+
boxShadow: selected ? `0 0 0 1px ${theme.palette.primary.main}` : 'none',
|
|
64
|
+
bgcolor: 'background.paper',
|
|
65
|
+
transition: theme.transitions.create([
|
|
66
|
+
'border-color',
|
|
67
|
+
'box-shadow'
|
|
68
|
+
]),
|
|
69
|
+
'&:hover': {
|
|
70
|
+
borderColor: selected ? 'primary.main' : 'text.primary'
|
|
71
|
+
},
|
|
72
|
+
'&.Mui-focusVisible': {
|
|
73
|
+
outline: `${theme.spacing(0.25)} solid ${theme.palette.primary.main}`,
|
|
74
|
+
outlineOffset: theme.spacing(0.25)
|
|
75
|
+
}
|
|
76
|
+
}),
|
|
77
|
+
children: [
|
|
78
|
+
/*#__PURE__*/ _jsx(Box, {
|
|
79
|
+
sx: {
|
|
80
|
+
aspectRatio: '4 / 3',
|
|
81
|
+
bgcolor: 'action.hover',
|
|
82
|
+
display: 'flex',
|
|
83
|
+
alignItems: 'center',
|
|
84
|
+
justifyContent: 'center',
|
|
85
|
+
color: 'text.disabled'
|
|
86
|
+
},
|
|
87
|
+
children: previewUrl ? /*#__PURE__*/ _jsx(Box, {
|
|
88
|
+
component: "img",
|
|
89
|
+
src: previewUrl,
|
|
90
|
+
alt: "",
|
|
91
|
+
sx: {
|
|
92
|
+
width: '100%',
|
|
93
|
+
height: '100%',
|
|
94
|
+
objectFit: 'cover',
|
|
95
|
+
display: 'block'
|
|
96
|
+
}
|
|
97
|
+
}) : previewUrl === null ? /*#__PURE__*/ _jsx(MdiIcon, {
|
|
98
|
+
path: mdiImageOutline,
|
|
99
|
+
fontSize: "large"
|
|
100
|
+
}) : /*#__PURE__*/ _jsx(Skeleton, {
|
|
101
|
+
variant: "rectangular",
|
|
102
|
+
sx: {
|
|
103
|
+
width: '100%',
|
|
104
|
+
height: '100%'
|
|
105
|
+
}
|
|
106
|
+
})
|
|
107
|
+
}),
|
|
108
|
+
/*#__PURE__*/ _jsxs(Box, {
|
|
109
|
+
sx: {
|
|
110
|
+
px: 1,
|
|
111
|
+
py: 0.75,
|
|
112
|
+
minWidth: 0
|
|
113
|
+
},
|
|
114
|
+
children: [
|
|
115
|
+
/*#__PURE__*/ _jsx(Typography, {
|
|
116
|
+
variant: "body2",
|
|
117
|
+
noWrap: true,
|
|
118
|
+
children: hit.photographer
|
|
119
|
+
}),
|
|
120
|
+
/*#__PURE__*/ _jsx(Typography, {
|
|
121
|
+
variant: "caption",
|
|
122
|
+
color: "text.secondary",
|
|
123
|
+
noWrap: true,
|
|
124
|
+
component: "div",
|
|
125
|
+
children: libraryLabel
|
|
126
|
+
})
|
|
127
|
+
]
|
|
128
|
+
})
|
|
129
|
+
]
|
|
130
|
+
});
|
|
131
|
+
}
|
|
132
|
+
/**
|
|
133
|
+
* The Stock photos tab: a search box, the shape, and the results. Asks the
|
|
134
|
+
* client for its status once, on mount, so a deployment with no library, or
|
|
135
|
+
* a member who cannot add files here, is told so before typing anything.
|
|
136
|
+
*/ export function StockPhotoSearchPanel(props) {
|
|
137
|
+
const { client, libraryName, maxQueryLength, selectedKey, onSelect } = props;
|
|
138
|
+
const [status, setStatus] = useState({
|
|
139
|
+
status: 'loading'
|
|
140
|
+
});
|
|
141
|
+
const [query, setQuery] = useState('');
|
|
142
|
+
const [orientation, setOrientation] = useState('any');
|
|
143
|
+
const [search, setSearch] = useState({
|
|
144
|
+
kind: 'idle'
|
|
145
|
+
});
|
|
146
|
+
const [shown, setShown] = useState(STOCK_PHOTO_SEARCH_PAGE);
|
|
147
|
+
const [previews, setPreviews] = useState({});
|
|
148
|
+
const owned = useRef(new Map());
|
|
149
|
+
const asked = useRef(new Set());
|
|
150
|
+
const latest = useRef(0);
|
|
151
|
+
useEffect(()=>{
|
|
152
|
+
let live = true;
|
|
153
|
+
setStatus({
|
|
154
|
+
status: 'loading'
|
|
155
|
+
});
|
|
156
|
+
client.status().then((answer)=>{
|
|
157
|
+
if (live) setStatus(answer);
|
|
158
|
+
});
|
|
159
|
+
return ()=>{
|
|
160
|
+
live = false;
|
|
161
|
+
};
|
|
162
|
+
}, [
|
|
163
|
+
client
|
|
164
|
+
]);
|
|
165
|
+
// The object URLs are this component's: let them go with it.
|
|
166
|
+
useEffect(()=>{
|
|
167
|
+
const urls = owned.current;
|
|
168
|
+
return ()=>{
|
|
169
|
+
for (const url of urls.values())if (url) URL.revokeObjectURL(url);
|
|
170
|
+
urls.clear();
|
|
171
|
+
};
|
|
172
|
+
}, []);
|
|
173
|
+
const libraryLabel = status.status === 'ready' ? status.library.label : '';
|
|
174
|
+
const run = useCallback(async (words, shape)=>{
|
|
175
|
+
const trimmed = words.replace(/\s+/g, ' ').trim();
|
|
176
|
+
if (!trimmed) return;
|
|
177
|
+
const ticket = ++latest.current;
|
|
178
|
+
setSearch({
|
|
179
|
+
kind: 'searching',
|
|
180
|
+
query: trimmed
|
|
181
|
+
});
|
|
182
|
+
setShown(STOCK_PHOTO_SEARCH_PAGE);
|
|
183
|
+
const answer = await client.search(trimmed, shape);
|
|
184
|
+
if (ticket !== latest.current) return;
|
|
185
|
+
setSearch(answer.status === 'ok' ? {
|
|
186
|
+
kind: 'results',
|
|
187
|
+
query: trimmed,
|
|
188
|
+
hits: answer.hits
|
|
189
|
+
} : {
|
|
190
|
+
kind: 'refused',
|
|
191
|
+
refusal: answer,
|
|
192
|
+
query: trimmed
|
|
193
|
+
});
|
|
194
|
+
}, [
|
|
195
|
+
client
|
|
196
|
+
]);
|
|
197
|
+
const visible = search.kind === 'results' ? search.hits.slice(0, shown) : [];
|
|
198
|
+
// Previews for the results on screen, a few at a time, each asked once.
|
|
199
|
+
useEffect(()=>{
|
|
200
|
+
let live = true;
|
|
201
|
+
const queue = visible.filter((hit)=>!asked.current.has(hit.sourceKey));
|
|
202
|
+
const next = async ()=>{
|
|
203
|
+
const hit = queue.shift();
|
|
204
|
+
if (!hit || !live) return;
|
|
205
|
+
asked.current.add(hit.sourceKey);
|
|
206
|
+
const blob = await client.preview(hit.ref);
|
|
207
|
+
if (!live) {
|
|
208
|
+
// Superseded: the next run asks for it again, for the results then on screen.
|
|
209
|
+
asked.current.delete(hit.sourceKey);
|
|
210
|
+
return;
|
|
211
|
+
}
|
|
212
|
+
const url = blob ? URL.createObjectURL(blob) : null;
|
|
213
|
+
owned.current.set(hit.sourceKey, url);
|
|
214
|
+
setPreviews((held)=>_extends({}, held, {
|
|
215
|
+
[hit.sourceKey]: url
|
|
216
|
+
}));
|
|
217
|
+
return next();
|
|
218
|
+
};
|
|
219
|
+
void Promise.all(Array.from({
|
|
220
|
+
length: PREVIEWS_AT_ONCE
|
|
221
|
+
}, next));
|
|
222
|
+
return ()=>{
|
|
223
|
+
live = false;
|
|
224
|
+
};
|
|
225
|
+
// `visible` is derived from these.
|
|
226
|
+
// eslint-disable-next-line react-hooks/exhaustive-deps
|
|
227
|
+
}, [
|
|
228
|
+
client,
|
|
229
|
+
search,
|
|
230
|
+
shown
|
|
231
|
+
]);
|
|
232
|
+
const submit = (event)=>{
|
|
233
|
+
event.preventDefault();
|
|
234
|
+
void run(query, orientation);
|
|
235
|
+
};
|
|
236
|
+
if (status.status === 'loading') {
|
|
237
|
+
return /*#__PURE__*/ _jsx(LinearProgress, {
|
|
238
|
+
"aria-label": "Loading stock photos"
|
|
239
|
+
});
|
|
240
|
+
}
|
|
241
|
+
if (status.status !== 'ready') {
|
|
242
|
+
return /*#__PURE__*/ _jsx(Alert, {
|
|
243
|
+
severity: status.status === 'refused' ? 'warning' : 'info',
|
|
244
|
+
"data-testid": "stock-photos-unavailable",
|
|
245
|
+
children: status.message
|
|
246
|
+
});
|
|
247
|
+
}
|
|
248
|
+
return /*#__PURE__*/ _jsxs(Stack, {
|
|
249
|
+
spacing: 2,
|
|
250
|
+
"data-testid": "stock-photo-search",
|
|
251
|
+
children: [
|
|
252
|
+
/*#__PURE__*/ _jsxs(Box, {
|
|
253
|
+
component: "form",
|
|
254
|
+
role: "search",
|
|
255
|
+
onSubmit: submit,
|
|
256
|
+
sx: {
|
|
257
|
+
display: 'flex',
|
|
258
|
+
flexWrap: 'wrap',
|
|
259
|
+
gap: 1,
|
|
260
|
+
alignItems: 'center'
|
|
261
|
+
},
|
|
262
|
+
children: [
|
|
263
|
+
/*#__PURE__*/ _jsx(TextField, {
|
|
264
|
+
size: "small",
|
|
265
|
+
value: query,
|
|
266
|
+
onChange: (event)=>setQuery(event.target.value.slice(0, maxQueryLength)),
|
|
267
|
+
placeholder: `Search ${libraryLabel} photos`,
|
|
268
|
+
slotProps: {
|
|
269
|
+
htmlInput: {
|
|
270
|
+
maxLength: maxQueryLength,
|
|
271
|
+
'aria-label': `Search ${libraryLabel} photos`
|
|
272
|
+
},
|
|
273
|
+
input: {
|
|
274
|
+
startAdornment: /*#__PURE__*/ _jsx(InputAdornment, {
|
|
275
|
+
position: "start",
|
|
276
|
+
children: /*#__PURE__*/ _jsx(MdiIcon, {
|
|
277
|
+
path: mdiMagnify
|
|
278
|
+
})
|
|
279
|
+
})
|
|
280
|
+
}
|
|
281
|
+
},
|
|
282
|
+
sx: {
|
|
283
|
+
flex: 1,
|
|
284
|
+
minWidth: (theme)=>theme.spacing(25)
|
|
285
|
+
}
|
|
286
|
+
}),
|
|
287
|
+
/*#__PURE__*/ _jsx(ToggleButtonGroup, {
|
|
288
|
+
size: "small",
|
|
289
|
+
exclusive: true,
|
|
290
|
+
value: orientation,
|
|
291
|
+
"aria-label": "Photo shape",
|
|
292
|
+
onChange: (_event, value)=>{
|
|
293
|
+
if (!value) return;
|
|
294
|
+
setOrientation(value);
|
|
295
|
+
if (search.kind !== 'idle') void run(search.query, value);
|
|
296
|
+
},
|
|
297
|
+
children: ORIENTATION_LABELS.map(([value, label])=>/*#__PURE__*/ _jsx(ToggleButton, {
|
|
298
|
+
value: value,
|
|
299
|
+
children: label
|
|
300
|
+
}, value))
|
|
301
|
+
}),
|
|
302
|
+
/*#__PURE__*/ _jsx(Button, {
|
|
303
|
+
type: "submit",
|
|
304
|
+
variant: "contained",
|
|
305
|
+
disabled: !query.trim() || search.kind === 'searching',
|
|
306
|
+
children: 'Search'
|
|
307
|
+
})
|
|
308
|
+
]
|
|
309
|
+
}),
|
|
310
|
+
/*#__PURE__*/ _jsx(Typography, {
|
|
311
|
+
variant: "body2",
|
|
312
|
+
color: "text.secondary",
|
|
313
|
+
children: `Photos come from ${libraryLabel}. The one you use is saved to ${libraryName} with the photographer's credit.`
|
|
314
|
+
}),
|
|
315
|
+
search.kind === 'idle' ? /*#__PURE__*/ _jsx(EmptyStateComponent, {
|
|
316
|
+
label: "Find a photo for your page",
|
|
317
|
+
description: "Describe what the photo should show, such as a coffee shop counter or a mountain lake, then choose Search."
|
|
318
|
+
}) : search.kind === 'searching' ? /*#__PURE__*/ _jsx(Box, {
|
|
319
|
+
sx: gridSx,
|
|
320
|
+
"aria-busy": "true",
|
|
321
|
+
"aria-label": "Searching",
|
|
322
|
+
children: Array.from({
|
|
323
|
+
length: SKELETON_TILES
|
|
324
|
+
}, (_, index)=>/*#__PURE__*/ _jsx(Skeleton, {
|
|
325
|
+
variant: "rectangular",
|
|
326
|
+
sx: {
|
|
327
|
+
aspectRatio: '4 / 3',
|
|
328
|
+
height: 'auto',
|
|
329
|
+
borderRadius: 1
|
|
330
|
+
}
|
|
331
|
+
}, index))
|
|
332
|
+
}) : search.kind === 'refused' ? /*#__PURE__*/ _jsx(Alert, {
|
|
333
|
+
severity: search.refusal.status === 'refused' ? 'warning' : 'info',
|
|
334
|
+
action: search.refusal.status === 'busy' ? /*#__PURE__*/ _jsx(Button, {
|
|
335
|
+
color: "inherit",
|
|
336
|
+
size: "small",
|
|
337
|
+
onClick: ()=>void run(search.query, orientation),
|
|
338
|
+
children: 'Try again'
|
|
339
|
+
}) : undefined,
|
|
340
|
+
children: search.refusal.message
|
|
341
|
+
}) : search.hits.length === 0 ? /*#__PURE__*/ _jsx(EmptyStateComponent, {
|
|
342
|
+
label: `No photos found for “${search.query}”`,
|
|
343
|
+
description: "Try fewer words, or describe it differently."
|
|
344
|
+
}) : /*#__PURE__*/ _jsxs(_Fragment, {
|
|
345
|
+
children: [
|
|
346
|
+
/*#__PURE__*/ _jsx(Box, {
|
|
347
|
+
sx: gridSx,
|
|
348
|
+
children: visible.map((hit)=>/*#__PURE__*/ _jsx(StockPhotoTile, {
|
|
349
|
+
hit: hit,
|
|
350
|
+
previewUrl: previews[hit.sourceKey],
|
|
351
|
+
libraryLabel: libraryLabel,
|
|
352
|
+
selected: hit.sourceKey === selectedKey,
|
|
353
|
+
onClick: ()=>{
|
|
354
|
+
var _previews_hit_sourceKey;
|
|
355
|
+
return onSelect(hit, {
|
|
356
|
+
previewUrl: (_previews_hit_sourceKey = previews[hit.sourceKey]) != null ? _previews_hit_sourceKey : null,
|
|
357
|
+
libraryLabel
|
|
358
|
+
});
|
|
359
|
+
}
|
|
360
|
+
}, hit.sourceKey))
|
|
361
|
+
}),
|
|
362
|
+
search.hits.length > shown ? /*#__PURE__*/ _jsx(Button, {
|
|
363
|
+
onClick: ()=>setShown((count)=>count + STOCK_PHOTO_SEARCH_PAGE),
|
|
364
|
+
sx: {
|
|
365
|
+
alignSelf: 'center'
|
|
366
|
+
},
|
|
367
|
+
children: 'Show more photos'
|
|
368
|
+
}) : null
|
|
369
|
+
]
|
|
370
|
+
})
|
|
371
|
+
]
|
|
372
|
+
});
|
|
373
|
+
}
|
|
374
|
+
/** A caption over one fact. */ const Label = ({ children })=>/*#__PURE__*/ _jsx(Typography, {
|
|
375
|
+
variant: "caption",
|
|
376
|
+
color: "text.secondary",
|
|
377
|
+
component: "div",
|
|
378
|
+
sx: {
|
|
379
|
+
mb: 0.5
|
|
380
|
+
},
|
|
381
|
+
children: children
|
|
382
|
+
});
|
|
383
|
+
/** An outside page, opened in a new tab. */ const OutLink = ({ href, children })=>/*#__PURE__*/ _jsx(Link, {
|
|
384
|
+
href: href,
|
|
385
|
+
target: "_blank",
|
|
386
|
+
rel: "noopener noreferrer",
|
|
387
|
+
children: children
|
|
388
|
+
});
|
|
389
|
+
/**
|
|
390
|
+
* The stock photo clicked, before it is used: a larger look, who took it,
|
|
391
|
+
* the license and where it will be saved. Nothing is copied until the
|
|
392
|
+
* caller's confirm button is pressed.
|
|
393
|
+
*/ export function StockPhotoDetails(props) {
|
|
394
|
+
const { hit, previewUrl, libraryLabel, libraryName, error, onBack } = props;
|
|
395
|
+
return /*#__PURE__*/ _jsxs(Stack, {
|
|
396
|
+
spacing: 2,
|
|
397
|
+
"data-testid": "stock-photo-details",
|
|
398
|
+
children: [
|
|
399
|
+
onBack ? /*#__PURE__*/ _jsx(Button, {
|
|
400
|
+
size: "small",
|
|
401
|
+
startIcon: /*#__PURE__*/ _jsx(MdiIcon, {
|
|
402
|
+
path: mdiArrowLeft
|
|
403
|
+
}),
|
|
404
|
+
onClick: onBack,
|
|
405
|
+
sx: {
|
|
406
|
+
alignSelf: 'flex-start'
|
|
407
|
+
},
|
|
408
|
+
children: 'Back to photos'
|
|
409
|
+
}) : null,
|
|
410
|
+
/*#__PURE__*/ _jsx(Box, {
|
|
411
|
+
sx: {
|
|
412
|
+
aspectRatio: `${hit.width} / ${hit.height}`,
|
|
413
|
+
maxHeight: (theme)=>theme.spacing(30),
|
|
414
|
+
borderRadius: 1,
|
|
415
|
+
overflow: 'hidden',
|
|
416
|
+
bgcolor: 'action.hover',
|
|
417
|
+
display: 'flex',
|
|
418
|
+
alignItems: 'center',
|
|
419
|
+
justifyContent: 'center',
|
|
420
|
+
color: 'text.disabled'
|
|
421
|
+
},
|
|
422
|
+
children: previewUrl ? /*#__PURE__*/ _jsx(Box, {
|
|
423
|
+
component: "img",
|
|
424
|
+
src: previewUrl,
|
|
425
|
+
alt: hit.alt || `Photo by ${hit.photographer}`,
|
|
426
|
+
sx: {
|
|
427
|
+
width: '100%',
|
|
428
|
+
height: '100%',
|
|
429
|
+
objectFit: 'contain',
|
|
430
|
+
display: 'block'
|
|
431
|
+
}
|
|
432
|
+
}) : /*#__PURE__*/ _jsx(MdiIcon, {
|
|
433
|
+
path: mdiImageOutline,
|
|
434
|
+
fontSize: "large"
|
|
435
|
+
})
|
|
436
|
+
}),
|
|
437
|
+
/*#__PURE__*/ _jsx(Typography, {
|
|
438
|
+
variant: "subtitle1",
|
|
439
|
+
component: "h3",
|
|
440
|
+
children: hit.photographerUrl ? /*#__PURE__*/ _jsxs(_Fragment, {
|
|
441
|
+
children: [
|
|
442
|
+
'Photo by ',
|
|
443
|
+
/*#__PURE__*/ _jsx(OutLink, {
|
|
444
|
+
href: hit.photographerUrl,
|
|
445
|
+
children: hit.photographer
|
|
446
|
+
})
|
|
447
|
+
]
|
|
448
|
+
}) : `Photo by ${hit.photographer}`
|
|
449
|
+
}),
|
|
450
|
+
error ? /*#__PURE__*/ _jsx(Alert, {
|
|
451
|
+
severity: "warning",
|
|
452
|
+
children: error
|
|
453
|
+
}) : null,
|
|
454
|
+
/*#__PURE__*/ _jsxs(Box, {
|
|
455
|
+
children: [
|
|
456
|
+
/*#__PURE__*/ _jsx(Label, {
|
|
457
|
+
children: 'Credit saved with the photo'
|
|
458
|
+
}),
|
|
459
|
+
/*#__PURE__*/ _jsx(Typography, {
|
|
460
|
+
variant: "body2",
|
|
461
|
+
children: hit.credit
|
|
462
|
+
})
|
|
463
|
+
]
|
|
464
|
+
}),
|
|
465
|
+
/*#__PURE__*/ _jsxs(Box, {
|
|
466
|
+
children: [
|
|
467
|
+
/*#__PURE__*/ _jsx(Label, {
|
|
468
|
+
children: 'License'
|
|
469
|
+
}),
|
|
470
|
+
/*#__PURE__*/ _jsx(Typography, {
|
|
471
|
+
variant: "body2",
|
|
472
|
+
children: hit.licenseUrl ? /*#__PURE__*/ _jsx(OutLink, {
|
|
473
|
+
href: hit.licenseUrl,
|
|
474
|
+
children: hit.license
|
|
475
|
+
}) : hit.license
|
|
476
|
+
})
|
|
477
|
+
]
|
|
478
|
+
}),
|
|
479
|
+
/*#__PURE__*/ _jsxs(Box, {
|
|
480
|
+
children: [
|
|
481
|
+
/*#__PURE__*/ _jsx(Label, {
|
|
482
|
+
children: 'Size'
|
|
483
|
+
}),
|
|
484
|
+
/*#__PURE__*/ _jsx(Typography, {
|
|
485
|
+
variant: "body2",
|
|
486
|
+
children: `${hit.width} × ${hit.height} pixels`
|
|
487
|
+
})
|
|
488
|
+
]
|
|
489
|
+
}),
|
|
490
|
+
/*#__PURE__*/ _jsx(Typography, {
|
|
491
|
+
variant: "body2",
|
|
492
|
+
children: /*#__PURE__*/ _jsx(OutLink, {
|
|
493
|
+
href: hit.pageUrl,
|
|
494
|
+
children: `See it on ${libraryLabel}`
|
|
495
|
+
})
|
|
496
|
+
}),
|
|
497
|
+
/*#__PURE__*/ _jsx(Typography, {
|
|
498
|
+
variant: "body2",
|
|
499
|
+
color: "text.secondary",
|
|
500
|
+
children: `When you use it, a copy is saved to ${libraryName} and placed from there. If the library already has this photo, that copy is used.`
|
|
501
|
+
})
|
|
502
|
+
]
|
|
503
|
+
});
|
|
504
|
+
}
|
|
505
|
+
|
|
506
|
+
//# sourceMappingURL=stock-photo-search.component.js.map
|