@aglyn/shared-ui-jsx 1.0.0-beta.282 → 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 -6
- 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/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/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@aglyn/shared-ui-jsx",
|
|
3
|
-
"version": "1.0.0-beta.
|
|
3
|
+
"version": "1.0.0-beta.283",
|
|
4
4
|
"license": "Apache-2.0",
|
|
5
5
|
"homepage": "https://aglyn.com",
|
|
6
6
|
"repository": {
|
|
@@ -25,11 +25,11 @@
|
|
|
25
25
|
"./package.json": "./package.json"
|
|
26
26
|
},
|
|
27
27
|
"dependencies": {
|
|
28
|
-
"@aglyn/shared-data-mdi": "1.0.0-beta.
|
|
29
|
-
"@aglyn/shared-ui-snackstack": "1.0.0-beta.
|
|
30
|
-
"@aglyn/shared-ui-theme": "1.0.0-beta.
|
|
31
|
-
"@aglyn/shared-util-tools": "1.0.0-beta.
|
|
32
|
-
"@aglyn/shared-util-vendor": "1.0.0-beta.
|
|
28
|
+
"@aglyn/shared-data-mdi": "1.0.0-beta.283",
|
|
29
|
+
"@aglyn/shared-ui-snackstack": "1.0.0-beta.283",
|
|
30
|
+
"@aglyn/shared-ui-theme": "1.0.0-beta.283",
|
|
31
|
+
"@aglyn/shared-util-tools": "1.0.0-beta.283",
|
|
32
|
+
"@aglyn/shared-util-vendor": "1.0.0-beta.283",
|
|
33
33
|
"@mdi/js": "^7.4.47",
|
|
34
34
|
"@swc/helpers": "0.5.23",
|
|
35
35
|
"change-case": "^5.4.4",
|
|
@@ -38,7 +38,12 @@ export function ConfirmationProviderComponent(props) {
|
|
|
38
38
|
const handleClose = useCallback(()=>{
|
|
39
39
|
setResolveReject([]);
|
|
40
40
|
}, []);
|
|
41
|
+
// The first answer settles the promise and clears the pair, but the dialog
|
|
42
|
+
// stays on screen through its exit transition. A second click in that
|
|
43
|
+
// window (a double click, a repeated Enter) has no question left to answer,
|
|
44
|
+
// so it must not call the cleared resolve or reject.
|
|
41
45
|
const handleCancel = useCallback(()=>{
|
|
46
|
+
if (!reject) return;
|
|
42
47
|
reject();
|
|
43
48
|
handleClose();
|
|
44
49
|
}, [
|
|
@@ -46,6 +51,7 @@ export function ConfirmationProviderComponent(props) {
|
|
|
46
51
|
handleClose
|
|
47
52
|
]);
|
|
48
53
|
const handleConfirm = useCallback(()=>{
|
|
54
|
+
if (!resolve) return;
|
|
49
55
|
resolve();
|
|
50
56
|
handleClose();
|
|
51
57
|
}, [
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../../../../../../../../libs/shared/ui/jsx/src/lib/components/confirmation-provider.component.tsx"],"sourcesContent":["/**\n * @license\n * Copyright 2024 Aglyn LLC\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n'use client'\n\nimport {\n type ElementType,\n Fragment,\n type MouseEventHandler,\n useCallback,\n useMemo,\n useState,\n} from 'react'\nimport ConfirmationContext, {\n buildConfirmationContextConfig,\n type ConfirmationContextConfig,\n type ConfirmFunction,\n DEFAULT_CONTEXT_CONFIG,\n} from '../contexts/confirmation.context'\nimport DialogConfirm from './dialog-confirm'\n\nexport interface ConfirmationProviderComponentProps {\n defaultOptions?: ConfirmationContextConfig\n children?: JSX.Children\n component?: ElementType<{\n open: boolean\n options: ConfirmationContextConfig\n onClose: MouseEventHandler<unknown>\n onCancel: MouseEventHandler<unknown>\n onConfirm: MouseEventHandler<unknown>\n }>\n}\n\nexport function ConfirmationProviderComponent(\n props: ConfirmationProviderComponentProps,\n) {\n const {\n children,\n defaultOptions = {},\n component: Component = DialogConfirm,\n } = props\n const [options, setOptions] = useState({\n ...DEFAULT_CONTEXT_CONFIG,\n ...defaultOptions,\n })\n const [resolveReject, setResolveReject] = useState([])\n const [resolve, reject] = resolveReject\n\n const confirm = useCallback<ConfirmFunction>(\n (options: ConfirmationContextConfig = {}) => {\n return new Promise((resolve, reject) => {\n setOptions(buildConfirmationContextConfig(defaultOptions, options))\n setResolveReject([resolve, reject])\n })\n },\n [defaultOptions],\n )\n\n const handleClose = useCallback(() => {\n setResolveReject([])\n }, [])\n\n const handleCancel = useCallback(() => {\n reject()\n handleClose()\n }, [reject, handleClose])\n\n const handleConfirm = useCallback(() => {\n resolve()\n handleClose()\n }, [resolve, handleClose])\n\n const child = useMemo(() => {\n return (\n <ConfirmationContext.Provider value={{ confirm }}>\n {children}\n </ConfirmationContext.Provider>\n )\n }, [children, confirm])\n\n return (\n <Fragment>\n {child}\n <Component\n open={resolveReject.length === 2}\n options={options}\n onClose={handleClose}\n onCancel={handleCancel}\n onConfirm={handleConfirm}\n />\n </Fragment>\n )\n}\nConfirmationProviderComponent.displayName = 'ConfirmationProviderComponent'\nConfirmationProviderComponent.aglyn = true\n\nexport default ConfirmationProviderComponent\n"],"names":["Fragment","useCallback","useMemo","useState","ConfirmationContext","buildConfirmationContextConfig","DEFAULT_CONTEXT_CONFIG","DialogConfirm","ConfirmationProviderComponent","props","children","defaultOptions","component","Component","options","setOptions","resolveReject","setResolveReject","resolve","reject","confirm","Promise","handleClose","handleCancel","handleConfirm","child","Provider","value","open","length","onClose","onCancel","onConfirm","displayName","aglyn"],"mappings":"AAAA;;;;;;;;;;;;;;;CAeC,GACD;;;AAEA,SAEEA,QAAQ,EAERC,WAAW,EACXC,OAAO,EACPC,QAAQ,QACH,QAAO;AACd,OAAOC,uBACLC,8BAA8B,EAG9BC,sBAAsB,QACjB,sCAAkC;AACzC,OAAOC,mBAAmB,sBAAkB;AAc5C,OAAO,SAASC,8BACdC,KAAyC;IAEzC,MAAM,EACJC,QAAQ,EACRC,iBAAiB,CAAC,CAAC,EACnBC,WAAWC,YAAYN,aAAa,EACrC,GAAGE;IACJ,MAAM,CAACK,SAASC,WAAW,GAAGZ,SAAS,aAClCG,wBACAK;IAEL,MAAM,CAACK,eAAeC,iBAAiB,GAAGd,SAAS,EAAE;IACrD,MAAM,CAACe,SAASC,OAAO,GAAGH;IAE1B,MAAMI,UAAUnB,YACd,CAACa,UAAqC,CAAC,CAAC;QACtC,OAAO,IAAIO,QAAQ,CAACH,SAASC;YAC3BJ,WAAWV,+BAA+BM,gBAAgBG;YAC1DG,iBAAiB;gBAACC;gBAASC;aAAO;QACpC;IACF,GACA;QAACR;KAAe;IAGlB,MAAMW,cAAcrB,YAAY;QAC9BgB,iBAAiB,EAAE;IACrB,GAAG,EAAE;IAEL,MAAMM,eAAetB,YAAY;QAC/
|
|
1
|
+
{"version":3,"sources":["../../../../../../../../libs/shared/ui/jsx/src/lib/components/confirmation-provider.component.tsx"],"sourcesContent":["/**\n * @license\n * Copyright 2024 Aglyn LLC\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n'use client'\n\nimport {\n type ElementType,\n Fragment,\n type MouseEventHandler,\n useCallback,\n useMemo,\n useState,\n} from 'react'\nimport ConfirmationContext, {\n buildConfirmationContextConfig,\n type ConfirmationContextConfig,\n type ConfirmFunction,\n DEFAULT_CONTEXT_CONFIG,\n} from '../contexts/confirmation.context'\nimport DialogConfirm from './dialog-confirm'\n\nexport interface ConfirmationProviderComponentProps {\n defaultOptions?: ConfirmationContextConfig\n children?: JSX.Children\n component?: ElementType<{\n open: boolean\n options: ConfirmationContextConfig\n onClose: MouseEventHandler<unknown>\n onCancel: MouseEventHandler<unknown>\n onConfirm: MouseEventHandler<unknown>\n }>\n}\n\nexport function ConfirmationProviderComponent(\n props: ConfirmationProviderComponentProps,\n) {\n const {\n children,\n defaultOptions = {},\n component: Component = DialogConfirm,\n } = props\n const [options, setOptions] = useState({\n ...DEFAULT_CONTEXT_CONFIG,\n ...defaultOptions,\n })\n const [resolveReject, setResolveReject] = useState([])\n const [resolve, reject] = resolveReject\n\n const confirm = useCallback<ConfirmFunction>(\n (options: ConfirmationContextConfig = {}) => {\n return new Promise((resolve, reject) => {\n setOptions(buildConfirmationContextConfig(defaultOptions, options))\n setResolveReject([resolve, reject])\n })\n },\n [defaultOptions],\n )\n\n const handleClose = useCallback(() => {\n setResolveReject([])\n }, [])\n\n // The first answer settles the promise and clears the pair, but the dialog\n // stays on screen through its exit transition. A second click in that\n // window (a double click, a repeated Enter) has no question left to answer,\n // so it must not call the cleared resolve or reject.\n const handleCancel = useCallback(() => {\n if (!reject) return\n reject()\n handleClose()\n }, [reject, handleClose])\n\n const handleConfirm = useCallback(() => {\n if (!resolve) return\n resolve()\n handleClose()\n }, [resolve, handleClose])\n\n const child = useMemo(() => {\n return (\n <ConfirmationContext.Provider value={{ confirm }}>\n {children}\n </ConfirmationContext.Provider>\n )\n }, [children, confirm])\n\n return (\n <Fragment>\n {child}\n <Component\n open={resolveReject.length === 2}\n options={options}\n onClose={handleClose}\n onCancel={handleCancel}\n onConfirm={handleConfirm}\n />\n </Fragment>\n )\n}\nConfirmationProviderComponent.displayName = 'ConfirmationProviderComponent'\nConfirmationProviderComponent.aglyn = true\n\nexport default ConfirmationProviderComponent\n"],"names":["Fragment","useCallback","useMemo","useState","ConfirmationContext","buildConfirmationContextConfig","DEFAULT_CONTEXT_CONFIG","DialogConfirm","ConfirmationProviderComponent","props","children","defaultOptions","component","Component","options","setOptions","resolveReject","setResolveReject","resolve","reject","confirm","Promise","handleClose","handleCancel","handleConfirm","child","Provider","value","open","length","onClose","onCancel","onConfirm","displayName","aglyn"],"mappings":"AAAA;;;;;;;;;;;;;;;CAeC,GACD;;;AAEA,SAEEA,QAAQ,EAERC,WAAW,EACXC,OAAO,EACPC,QAAQ,QACH,QAAO;AACd,OAAOC,uBACLC,8BAA8B,EAG9BC,sBAAsB,QACjB,sCAAkC;AACzC,OAAOC,mBAAmB,sBAAkB;AAc5C,OAAO,SAASC,8BACdC,KAAyC;IAEzC,MAAM,EACJC,QAAQ,EACRC,iBAAiB,CAAC,CAAC,EACnBC,WAAWC,YAAYN,aAAa,EACrC,GAAGE;IACJ,MAAM,CAACK,SAASC,WAAW,GAAGZ,SAAS,aAClCG,wBACAK;IAEL,MAAM,CAACK,eAAeC,iBAAiB,GAAGd,SAAS,EAAE;IACrD,MAAM,CAACe,SAASC,OAAO,GAAGH;IAE1B,MAAMI,UAAUnB,YACd,CAACa,UAAqC,CAAC,CAAC;QACtC,OAAO,IAAIO,QAAQ,CAACH,SAASC;YAC3BJ,WAAWV,+BAA+BM,gBAAgBG;YAC1DG,iBAAiB;gBAACC;gBAASC;aAAO;QACpC;IACF,GACA;QAACR;KAAe;IAGlB,MAAMW,cAAcrB,YAAY;QAC9BgB,iBAAiB,EAAE;IACrB,GAAG,EAAE;IAEL,2EAA2E;IAC3E,sEAAsE;IACtE,4EAA4E;IAC5E,qDAAqD;IACrD,MAAMM,eAAetB,YAAY;QAC/B,IAAI,CAACkB,QAAQ;QACbA;QACAG;IACF,GAAG;QAACH;QAAQG;KAAY;IAExB,MAAME,gBAAgBvB,YAAY;QAChC,IAAI,CAACiB,SAAS;QACdA;QACAI;IACF,GAAG;QAACJ;QAASI;KAAY;IAEzB,MAAMG,QAAQvB,QAAQ;QACpB,qBACE,KAACE,oBAAoBsB,QAAQ;YAACC,OAAO;gBAAEP;YAAQ;sBAC5CV;;IAGP,GAAG;QAACA;QAAUU;KAAQ;IAEtB,qBACE,MAACpB;;YACEyB;0BACD,KAACZ;gBACCe,MAAMZ,cAAca,MAAM,KAAK;gBAC/Bf,SAASA;gBACTgB,SAASR;gBACTS,UAAUR;gBACVS,WAAWR;;;;AAInB;AACAhB,8BAA8ByB,WAAW,GAAG;AAC5CzB,8BAA8B0B,KAAK,GAAG;AAEtC,eAAe1B,8BAA6B"}
|
|
@@ -53,12 +53,14 @@ export const DialogConfirm = /*#__PURE__*/ forwardRef((props, ref)=>{
|
|
|
53
53
|
/*#__PURE__*/ _jsxs(DialogActions, {
|
|
54
54
|
children: [
|
|
55
55
|
/*#__PURE__*/ _jsx(Button, _extends({}, cancellationButtonProps, {
|
|
56
|
+
disabled: !open || (cancellationButtonProps == null ? void 0 : cancellationButtonProps.disabled),
|
|
56
57
|
onClick: onCancel,
|
|
57
58
|
children: cancellationText
|
|
58
59
|
})),
|
|
59
60
|
/*#__PURE__*/ _jsx(Button, _extends({
|
|
60
61
|
color: "primary"
|
|
61
62
|
}, confirmationButtonProps, {
|
|
63
|
+
disabled: !open || (confirmationButtonProps == null ? void 0 : confirmationButtonProps.disabled),
|
|
62
64
|
onClick: onConfirm,
|
|
63
65
|
children: confirmationText
|
|
64
66
|
}))
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../../../../../../../../libs/shared/ui/jsx/src/lib/components/dialog-confirm.tsx"],"sourcesContent":["/**\n * @license\n * Copyright 2023 Aglyn LLC\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\nimport Button, { ButtonProps } from '@mui/material/Button'\nimport Dialog, { DialogProps } from '@mui/material/Dialog'\nimport DialogActions from '@mui/material/DialogActions'\nimport DialogContent from '@mui/material/DialogContent'\nimport DialogContentText, {\n DialogContentTextProps,\n} from '@mui/material/DialogContentText'\nimport DialogTitle, { DialogTitleProps } from '@mui/material/DialogTitle'\nimport { forwardRef } from 'react'\n\nexport interface DialogConfirmOptions {\n cancellationText?: ButtonProps['children']\n confirmationText?: ButtonProps['children']\n cancellationButtonProps?: Partial<ButtonProps>\n confirmationButtonProps?: Partial<ButtonProps>\n dialogProps?: Partial<DialogProps>\n title?: DialogTitleProps['children']\n description?: DialogContentTextProps['children']\n}\n\nexport interface DialogConfirmProps extends DialogProps {\n options?: DialogConfirmOptions\n onConfirm?: ButtonProps['onClick']\n onCancel?: ButtonProps['onClick']\n}\n\nexport const DialogConfirm = forwardRef<any, DialogConfirmProps>(\n (props, ref) => {\n const {\n open,\n options = {\n cancellationText: 'Cancel',\n confirmationText: 'OK',\n },\n onCancel,\n onConfirm,\n onClose,\n ...rest\n } = props\n const {\n title,\n description,\n confirmationText,\n cancellationText,\n dialogProps,\n confirmationButtonProps,\n cancellationButtonProps,\n } = options\n return (\n <Dialog\n ref={ref}\n fullWidth\n {...rest}\n {...dialogProps}\n open={open}\n onClose={onClose}\n >\n {title && <DialogTitle>{title}</DialogTitle>}\n {description && (\n <DialogContent>\n <DialogContentText>{description}</DialogContentText>\n </DialogContent>\n )}\n <DialogActions>\n <Button
|
|
1
|
+
{"version":3,"sources":["../../../../../../../../libs/shared/ui/jsx/src/lib/components/dialog-confirm.tsx"],"sourcesContent":["/**\n * @license\n * Copyright 2023 Aglyn LLC\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\nimport Button, { ButtonProps } from '@mui/material/Button'\nimport Dialog, { DialogProps } from '@mui/material/Dialog'\nimport DialogActions from '@mui/material/DialogActions'\nimport DialogContent from '@mui/material/DialogContent'\nimport DialogContentText, {\n DialogContentTextProps,\n} from '@mui/material/DialogContentText'\nimport DialogTitle, { DialogTitleProps } from '@mui/material/DialogTitle'\nimport { forwardRef } from 'react'\n\nexport interface DialogConfirmOptions {\n cancellationText?: ButtonProps['children']\n confirmationText?: ButtonProps['children']\n cancellationButtonProps?: Partial<ButtonProps>\n confirmationButtonProps?: Partial<ButtonProps>\n dialogProps?: Partial<DialogProps>\n title?: DialogTitleProps['children']\n description?: DialogContentTextProps['children']\n}\n\nexport interface DialogConfirmProps extends DialogProps {\n options?: DialogConfirmOptions\n onConfirm?: ButtonProps['onClick']\n onCancel?: ButtonProps['onClick']\n}\n\nexport const DialogConfirm = forwardRef<any, DialogConfirmProps>(\n (props, ref) => {\n const {\n open,\n options = {\n cancellationText: 'Cancel',\n confirmationText: 'OK',\n },\n onCancel,\n onConfirm,\n onClose,\n ...rest\n } = props\n const {\n title,\n description,\n confirmationText,\n cancellationText,\n dialogProps,\n confirmationButtonProps,\n cancellationButtonProps,\n } = options\n return (\n <Dialog\n ref={ref}\n fullWidth\n {...rest}\n {...dialogProps}\n open={open}\n onClose={onClose}\n >\n {title && <DialogTitle>{title}</DialogTitle>}\n {description && (\n <DialogContent>\n <DialogContentText>{description}</DialogContentText>\n </DialogContent>\n )}\n {/* Once answered the dialog fades out with its buttons still drawn;\n they are disabled for that moment so they don't look live. */}\n <DialogActions>\n <Button\n {...cancellationButtonProps}\n disabled={!open || cancellationButtonProps?.disabled}\n onClick={onCancel}\n >\n {cancellationText}\n </Button>\n <Button\n color=\"primary\"\n {...confirmationButtonProps}\n disabled={!open || confirmationButtonProps?.disabled}\n onClick={onConfirm}\n >\n {confirmationText}\n </Button>\n </DialogActions>\n </Dialog>\n )\n },\n)\n\nDialogConfirm.displayName = 'DialogConfirm'\nDialogConfirm.aglyn = true\n\nexport default DialogConfirm\n"],"names":["Button","Dialog","DialogActions","DialogContent","DialogContentText","DialogTitle","forwardRef","DialogConfirm","props","ref","open","options","cancellationText","confirmationText","onCancel","onConfirm","onClose","rest","title","description","dialogProps","confirmationButtonProps","cancellationButtonProps","fullWidth","disabled","onClick","color","displayName","aglyn"],"mappings":";;;AAAA;;;;;;;;;;;;;;;CAeC,GAED,OAAOA,YAA6B,uBAAsB;AAC1D,OAAOC,YAA6B,uBAAsB;AAC1D,OAAOC,mBAAmB,8BAA6B;AACvD,OAAOC,mBAAmB,8BAA6B;AACvD,OAAOC,uBAEA,kCAAiC;AACxC,OAAOC,iBAAuC,4BAA2B;AACzE,SAASC,UAAU,QAAQ,QAAO;AAkBlC,OAAO,MAAMC,8BAAgBD,WAC3B,CAACE,OAAOC;IACN,MAAM,EACJC,IAAI,EACJC,UAAU;QACRC,kBAAkB;QAClBC,kBAAkB;IACpB,CAAC,EACDC,QAAQ,EACRC,SAAS,EACTC,OAAO,EAER,GAAGR,OADCS,wCACDT;;;;;;;IACJ,MAAM,EACJU,KAAK,EACLC,WAAW,EACXN,gBAAgB,EAChBD,gBAAgB,EAChBQ,WAAW,EACXC,uBAAuB,EACvBC,uBAAuB,EACxB,GAAGX;IACJ,qBACE,MAACV;QACCQ,KAAKA;QACLc,SAAS;OACLN,MACAG;QACJV,MAAMA;QACNM,SAASA;;YAERE,uBAAS,KAACb;0BAAaa;;YACvBC,6BACC,KAAChB;0BACC,cAAA,KAACC;8BAAmBe;;;0BAKxB,MAACjB;;kCACC,KAACF,qBACKsB;wBACJE,UAAU,CAACd,SAAQY,2CAAAA,wBAAyBE,QAAQ;wBACpDC,SAASX;kCAERF;;kCAEH,KAACZ;wBACC0B,OAAM;uBACFL;wBACJG,UAAU,CAACd,SAAQW,2CAAAA,wBAAyBG,QAAQ;wBACpDC,SAASV;kCAERF;;;;;;AAKX,GACD;AAEDN,cAAcoB,WAAW,GAAG;AAC5BpB,cAAcqB,KAAK,GAAG;AAEtB,eAAerB,cAAa"}
|
|
@@ -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
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../../../../../../../../libs/shared/ui/jsx/src/lib/components/stock-photo-search.component.tsx"],"sourcesContent":["/**\n * @license\n * Copyright 2026 Aglyn LLC\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\n'use client'\n\nimport { mdiArrowLeft, mdiImageOutline, mdiMagnify } from '@mdi/js'\nimport {\n Alert,\n Box,\n Button,\n ButtonBase,\n InputAdornment,\n LinearProgress,\n Link,\n Skeleton,\n Stack,\n TextField,\n ToggleButton,\n ToggleButtonGroup,\n Typography,\n type Theme,\n} from '@mui/material'\nimport { type FormEvent, useCallback, useEffect, useRef, useState } from 'react'\nimport EmptyStateComponent from './empty-state.component'\nimport { MdiIcon } from './mdi-icon'\n\n/**\n * SEARCH A STOCK PHOTO LIBRARY AND CHOOSE ONE (AGL-4066): the media picker's\n * Stock photos tab, and the details of the photo chosen.\n *\n * Presentation only. What a search asks and where a photo goes are the\n * caller's: it hands in a {@link StockPhotoSearchClient} (the console binds\n * its route and the member's credentials) and receives the photo clicked,\n * which it places the way it places a library file. Every result shows its\n * photographer and the library it comes from, since a library's terms ask\n * that its results say where they are from.\n *\n * Thumbnails arrive as image bytes from the caller's server and are shown\n * through object URLs this component owns, so no address at the library is\n * ever loaded by the browser; it fetches them a few at a time, only for the\n * results on screen.\n */\n\n/** The shapes the search offers. */\nexport type StockPhotoSearchOrientation = 'any' | 'horizontal' | 'vertical'\n\n/** One result, as much of it as this shows. */\nexport interface StockPhotoSearchHit {\n /** What the caller names the photo by when it asks for its preview. */\n ref: unknown\n /** Unique among the results. */\n sourceKey: string\n width: number\n height: number\n photographer: string\n tags: string[]\n alt?: string\n}\n\n/** The details panel's further facts about a result. */\nexport interface StockPhotoDetailsHit extends StockPhotoSearchHit {\n photographerUrl?: string\n pageUrl: string\n /** \"Photo by … on …\". */\n credit: string\n license: string\n licenseUrl: string\n}\n\ntype Refusal = { status: 'busy' | 'unconfigured' | 'refused' | 'gone'; message: string }\n\n/** What the caller's client answers. */\nexport interface StockPhotoSearchClient<Hit extends StockPhotoSearchHit> {\n status(): Promise<{ status: 'ready'; library: { label: string } } | Refusal>\n search(\n query: string,\n orientation: StockPhotoSearchOrientation,\n ): Promise<{ status: 'ok'; library: { label: string }; hits: Hit[] } | Refusal>\n preview(ref: Hit['ref']): Promise<Blob | null>\n}\n\nexport interface StockPhotoSearchPanelProps<Hit extends StockPhotoSearchHit> {\n client: StockPhotoSearchClient<Hit>\n /** Where a used photo is saved, as a sentence ends: \"this site's library\". */\n libraryName: string\n /** The longest search the library takes. */\n maxQueryLength: number\n /** The result currently chosen. */\n selectedKey?: string | null\n /** A result was clicked; `previewUrl` is its thumbnail, when it has loaded. */\n onSelect: (hit: Hit, context: { previewUrl: string | null; libraryLabel: string }) => void\n}\n\n/** Results shown at first, and added by each Show more. */\nexport const STOCK_PHOTO_SEARCH_PAGE = 20\n/** Previews fetched at once. */\nconst PREVIEWS_AT_ONCE = 4\n/** Skeleton tiles while a search runs. */\nconst SKELETON_TILES = 12\n\nconst ORIENTATION_LABELS: ReadonlyArray<[StockPhotoSearchOrientation, string]> = [\n ['any', 'Any'],\n ['horizontal', 'Landscape'],\n ['vertical', 'Portrait'],\n]\n\n/** The grid every state of the results lays out in. */\nconst gridSx = (theme: Theme) => ({\n display: 'grid',\n gap: theme.spacing(2),\n gridTemplateColumns: `repeat(auto-fill, minmax(${theme.spacing(20)}, 1fr))`,\n})\n\n/** A result's tile: the thumbnail, the photographer and the library. */\nfunction StockPhotoTile(props: {\n hit: StockPhotoSearchHit\n previewUrl: string | null | undefined\n libraryLabel: string\n selected: boolean\n onClick: () => void\n}) {\n const { hit, previewUrl, libraryLabel, selected, onClick } = props\n const described = hit.alt || hit.tags.slice(0, 3).join(', ')\n return (\n <ButtonBase\n onClick={onClick}\n aria-pressed={selected}\n aria-label={`Photo by ${hit.photographer} on ${libraryLabel}${described ? `: ${described}` : ''}`}\n data-testid=\"stock-photo-tile\"\n sx={(theme) => ({\n display: 'flex',\n flexDirection: 'column',\n alignItems: 'stretch',\n textAlign: 'left',\n borderRadius: 1,\n overflow: 'hidden',\n border: 1,\n borderColor: selected ? 'primary.main' : 'divider',\n boxShadow: selected ? `0 0 0 1px ${theme.palette.primary.main}` : 'none',\n bgcolor: 'background.paper',\n transition: theme.transitions.create(['border-color', 'box-shadow']),\n '&:hover': { borderColor: selected ? 'primary.main' : 'text.primary' },\n '&.Mui-focusVisible': { outline: `${theme.spacing(0.25)} solid ${theme.palette.primary.main}`, outlineOffset: theme.spacing(0.25) },\n })}\n >\n <Box sx={{ aspectRatio: '4 / 3', bgcolor: 'action.hover', display: 'flex', alignItems: 'center', justifyContent: 'center', color: 'text.disabled' }}>\n {previewUrl ? (\n <Box component=\"img\" src={previewUrl} alt=\"\" sx={{ width: '100%', height: '100%', objectFit: 'cover', display: 'block' }} />\n ) : previewUrl === null ? (\n <MdiIcon path={mdiImageOutline} fontSize=\"large\" />\n ) : (\n <Skeleton variant=\"rectangular\" sx={{ width: '100%', height: '100%' }} />\n )}\n </Box>\n <Box sx={{ px: 1, py: 0.75, minWidth: 0 }}>\n <Typography variant=\"body2\" noWrap>\n {hit.photographer}\n </Typography>\n <Typography variant=\"caption\" color=\"text.secondary\" noWrap component=\"div\">\n {libraryLabel}\n </Typography>\n </Box>\n </ButtonBase>\n )\n}\n\ntype SearchState<Hit> =\n | { kind: 'idle' }\n | { kind: 'searching'; query: string }\n | { kind: 'results'; query: string; hits: Hit[] }\n | { kind: 'refused'; refusal: Refusal; query: string }\n\n/**\n * The Stock photos tab: a search box, the shape, and the results. Asks the\n * client for its status once, on mount, so a deployment with no library, or\n * a member who cannot add files here, is told so before typing anything.\n */\nexport function StockPhotoSearchPanel<Hit extends StockPhotoSearchHit>(props: StockPhotoSearchPanelProps<Hit>) {\n const { client, libraryName, maxQueryLength, selectedKey, onSelect } = props\n const [status, setStatus] = useState<{ status: 'loading' } | Awaited<ReturnType<StockPhotoSearchClient<Hit>['status']>>>({ status: 'loading' })\n const [query, setQuery] = useState('')\n const [orientation, setOrientation] = useState<StockPhotoSearchOrientation>('any')\n const [search, setSearch] = useState<SearchState<Hit>>({ kind: 'idle' })\n const [shown, setShown] = useState(STOCK_PHOTO_SEARCH_PAGE)\n const [previews, setPreviews] = useState<Record<string, string | null>>({})\n const owned = useRef<Map<string, string | null>>(new Map())\n const asked = useRef<Set<string>>(new Set())\n const latest = useRef(0)\n\n useEffect(() => {\n let live = true\n setStatus({ status: 'loading' })\n client.status().then((answer) => {\n if (live) setStatus(answer)\n })\n return () => {\n live = false\n }\n }, [client])\n\n // The object URLs are this component's: let them go with it.\n useEffect(() => {\n const urls = owned.current\n return () => {\n for (const url of urls.values()) if (url) URL.revokeObjectURL(url)\n urls.clear()\n }\n }, [])\n\n const libraryLabel = status.status === 'ready' ? status.library.label : ''\n\n const run = useCallback(\n async (words: string, shape: StockPhotoSearchOrientation) => {\n const trimmed = words.replace(/\\s+/g, ' ').trim()\n if (!trimmed) return\n const ticket = ++latest.current\n setSearch({ kind: 'searching', query: trimmed })\n setShown(STOCK_PHOTO_SEARCH_PAGE)\n const answer = await client.search(trimmed, shape)\n if (ticket !== latest.current) return\n setSearch(answer.status === 'ok' ? { kind: 'results', query: trimmed, hits: answer.hits } : { kind: 'refused', refusal: answer, query: trimmed })\n },\n [client],\n )\n\n const visible = search.kind === 'results' ? search.hits.slice(0, shown) : []\n\n // Previews for the results on screen, a few at a time, each asked once.\n useEffect(() => {\n let live = true\n const queue = visible.filter((hit) => !asked.current.has(hit.sourceKey))\n const next = async (): Promise<void> => {\n const hit = queue.shift()\n if (!hit || !live) return\n asked.current.add(hit.sourceKey)\n const blob = await client.preview(hit.ref)\n if (!live) {\n // Superseded: the next run asks for it again, for the results then on screen.\n asked.current.delete(hit.sourceKey)\n return\n }\n const url = blob ? URL.createObjectURL(blob) : null\n owned.current.set(hit.sourceKey, url)\n setPreviews((held) => ({ ...held, [hit.sourceKey]: url }))\n return next()\n }\n void Promise.all(Array.from({ length: PREVIEWS_AT_ONCE }, next))\n return () => {\n live = false\n }\n // `visible` is derived from these.\n // eslint-disable-next-line react-hooks/exhaustive-deps\n }, [client, search, shown])\n\n const submit = (event: FormEvent) => {\n event.preventDefault()\n void run(query, orientation)\n }\n\n if (status.status === 'loading') {\n return <LinearProgress aria-label=\"Loading stock photos\" />\n }\n if (status.status !== 'ready') {\n return (\n <Alert severity={status.status === 'refused' ? 'warning' : 'info'} data-testid=\"stock-photos-unavailable\">\n {status.message}\n </Alert>\n )\n }\n\n return (\n <Stack spacing={2} data-testid=\"stock-photo-search\">\n <Box component=\"form\" role=\"search\" onSubmit={submit} sx={{ display: 'flex', flexWrap: 'wrap', gap: 1, alignItems: 'center' }}>\n <TextField\n size=\"small\"\n value={query}\n onChange={(event) => setQuery(event.target.value.slice(0, maxQueryLength))}\n placeholder={`Search ${libraryLabel} photos`}\n slotProps={{\n htmlInput: { maxLength: maxQueryLength, 'aria-label': `Search ${libraryLabel} photos` },\n input: {\n startAdornment: (\n <InputAdornment position=\"start\">\n <MdiIcon path={mdiMagnify} />\n </InputAdornment>\n ),\n },\n }}\n sx={{ flex: 1, minWidth: (theme) => theme.spacing(25) }}\n />\n <ToggleButtonGroup\n size=\"small\"\n exclusive\n value={orientation}\n aria-label=\"Photo shape\"\n onChange={(_event, value: StockPhotoSearchOrientation | null) => {\n if (!value) return\n setOrientation(value)\n if (search.kind !== 'idle') void run(search.query, value)\n }}\n >\n {ORIENTATION_LABELS.map(([value, label]) => (\n <ToggleButton key={value} value={value}>\n {label}\n </ToggleButton>\n ))}\n </ToggleButtonGroup>\n <Button type=\"submit\" variant=\"contained\" disabled={!query.trim() || search.kind === 'searching'}>\n {'Search'}\n </Button>\n </Box>\n <Typography variant=\"body2\" color=\"text.secondary\">\n {`Photos come from ${libraryLabel}. The one you use is saved to ${libraryName} with the photographer's credit.`}\n </Typography>\n {search.kind === 'idle' ? (\n <EmptyStateComponent\n label=\"Find a photo for your page\"\n description=\"Describe what the photo should show, such as a coffee shop counter or a mountain lake, then choose Search.\"\n />\n ) : search.kind === 'searching' ? (\n <Box sx={gridSx} aria-busy=\"true\" aria-label=\"Searching\">\n {Array.from({ length: SKELETON_TILES }, (_, index) => (\n <Skeleton key={index} variant=\"rectangular\" sx={{ aspectRatio: '4 / 3', height: 'auto', borderRadius: 1 }} />\n ))}\n </Box>\n ) : search.kind === 'refused' ? (\n <Alert\n severity={search.refusal.status === 'refused' ? 'warning' : 'info'}\n action={\n search.refusal.status === 'busy' ? (\n <Button color=\"inherit\" size=\"small\" onClick={() => void run(search.query, orientation)}>\n {'Try again'}\n </Button>\n ) : undefined\n }\n >\n {search.refusal.message}\n </Alert>\n ) : search.hits.length === 0 ? (\n <EmptyStateComponent label={`No photos found for “${search.query}”`} description=\"Try fewer words, or describe it differently.\" />\n ) : (\n <>\n <Box sx={gridSx}>\n {visible.map((hit) => (\n <StockPhotoTile\n key={hit.sourceKey}\n hit={hit}\n previewUrl={previews[hit.sourceKey]}\n libraryLabel={libraryLabel}\n selected={hit.sourceKey === selectedKey}\n onClick={() => onSelect(hit, { previewUrl: previews[hit.sourceKey] ?? null, libraryLabel })}\n />\n ))}\n </Box>\n {search.hits.length > shown ? (\n <Button onClick={() => setShown((count) => count + STOCK_PHOTO_SEARCH_PAGE)} sx={{ alignSelf: 'center' }}>\n {'Show more photos'}\n </Button>\n ) : null}\n </>\n )}\n </Stack>\n )\n}\n\nexport interface StockPhotoDetailsProps {\n hit: StockPhotoDetailsHit\n previewUrl: string | null\n /** The library's name: \"Pixabay\". */\n libraryLabel: string\n /** Where using the photo saves it, as a sentence ends: \"this site's library\". */\n libraryName: string\n /** Why using it failed, said above the facts. */\n error?: string | null\n /** Small screens, where the details replace the results: back to them. */\n onBack?: () => void\n}\n\n/** A caption over one fact. */\nconst Label = ({ children }: { children: string }) => (\n <Typography variant=\"caption\" color=\"text.secondary\" component=\"div\" sx={{ mb: 0.5 }}>\n {children}\n </Typography>\n)\n\n/** An outside page, opened in a new tab. */\nconst OutLink = ({ href, children }: { href: string; children: string }) => (\n <Link href={href} target=\"_blank\" rel=\"noopener noreferrer\">\n {children}\n </Link>\n)\n\n/**\n * The stock photo clicked, before it is used: a larger look, who took it,\n * the license and where it will be saved. Nothing is copied until the\n * caller's confirm button is pressed.\n */\nexport function StockPhotoDetails(props: StockPhotoDetailsProps) {\n const { hit, previewUrl, libraryLabel, libraryName, error, onBack } = props\n return (\n <Stack spacing={2} data-testid=\"stock-photo-details\">\n {onBack ? (\n <Button size=\"small\" startIcon={<MdiIcon path={mdiArrowLeft} />} onClick={onBack} sx={{ alignSelf: 'flex-start' }}>\n {'Back to photos'}\n </Button>\n ) : null}\n <Box\n sx={{\n aspectRatio: `${hit.width} / ${hit.height}`,\n maxHeight: (theme) => theme.spacing(30),\n borderRadius: 1,\n overflow: 'hidden',\n bgcolor: 'action.hover',\n display: 'flex',\n alignItems: 'center',\n justifyContent: 'center',\n color: 'text.disabled',\n }}\n >\n {previewUrl ? (\n <Box component=\"img\" src={previewUrl} alt={hit.alt || `Photo by ${hit.photographer}`} sx={{ width: '100%', height: '100%', objectFit: 'contain', display: 'block' }} />\n ) : (\n <MdiIcon path={mdiImageOutline} fontSize=\"large\" />\n )}\n </Box>\n <Typography variant=\"subtitle1\" component=\"h3\">\n {hit.photographerUrl ? (\n <>\n {'Photo by '}\n <OutLink href={hit.photographerUrl}>{hit.photographer}</OutLink>\n </>\n ) : (\n `Photo by ${hit.photographer}`\n )}\n </Typography>\n {error ? <Alert severity=\"warning\">{error}</Alert> : null}\n <Box>\n <Label>{'Credit saved with the photo'}</Label>\n <Typography variant=\"body2\">{hit.credit}</Typography>\n </Box>\n <Box>\n <Label>{'License'}</Label>\n <Typography variant=\"body2\">\n {hit.licenseUrl ? <OutLink href={hit.licenseUrl}>{hit.license}</OutLink> : hit.license}\n </Typography>\n </Box>\n <Box>\n <Label>{'Size'}</Label>\n <Typography variant=\"body2\">{`${hit.width} × ${hit.height} pixels`}</Typography>\n </Box>\n <Typography variant=\"body2\">\n <OutLink href={hit.pageUrl}>{`See it on ${libraryLabel}`}</OutLink>\n </Typography>\n <Typography variant=\"body2\" color=\"text.secondary\">\n {`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.`}\n </Typography>\n </Stack>\n )\n}\n"],"names":["mdiArrowLeft","mdiImageOutline","mdiMagnify","Alert","Box","Button","ButtonBase","InputAdornment","LinearProgress","Link","Skeleton","Stack","TextField","ToggleButton","ToggleButtonGroup","Typography","useCallback","useEffect","useRef","useState","EmptyStateComponent","MdiIcon","STOCK_PHOTO_SEARCH_PAGE","PREVIEWS_AT_ONCE","SKELETON_TILES","ORIENTATION_LABELS","gridSx","theme","display","gap","spacing","gridTemplateColumns","StockPhotoTile","props","hit","previewUrl","libraryLabel","selected","onClick","described","alt","tags","slice","join","aria-pressed","aria-label","photographer","data-testid","sx","flexDirection","alignItems","textAlign","borderRadius","overflow","border","borderColor","boxShadow","palette","primary","main","bgcolor","transition","transitions","create","outline","outlineOffset","aspectRatio","justifyContent","color","component","src","width","height","objectFit","path","fontSize","variant","px","py","minWidth","noWrap","StockPhotoSearchPanel","client","libraryName","maxQueryLength","selectedKey","onSelect","status","setStatus","query","setQuery","orientation","setOrientation","search","setSearch","kind","shown","setShown","previews","setPreviews","owned","Map","asked","Set","latest","live","then","answer","urls","current","url","values","URL","revokeObjectURL","clear","library","label","run","words","shape","trimmed","replace","trim","ticket","hits","refusal","visible","queue","filter","has","sourceKey","next","shift","add","blob","preview","ref","delete","createObjectURL","set","held","Promise","all","Array","from","length","submit","event","preventDefault","severity","message","role","onSubmit","flexWrap","size","value","onChange","target","placeholder","slotProps","htmlInput","maxLength","input","startAdornment","position","flex","exclusive","_event","map","type","disabled","description","aria-busy","_","index","action","undefined","count","alignSelf","Label","children","mb","OutLink","href","rel","StockPhotoDetails","error","onBack","startIcon","maxHeight","photographerUrl","credit","licenseUrl","license","pageUrl"],"mappings":"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"}
|