@aglyn/shared-ui-jsx 1.0.0-beta.296 → 1.1.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.
- package/package.json +6 -6
- package/src/lib/components/file-drop-zone.component.d.ts +4 -2
- package/src/lib/components/file-drop-zone.component.js +4 -3
- package/src/lib/components/file-drop-zone.component.js.map +1 -1
- package/src/lib/components/image-file-field.component.d.ts +32 -0
- package/src/lib/components/image-file-field.component.js +144 -0
- package/src/lib/components/image-file-field.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.
|
|
3
|
+
"version": "1.1.0",
|
|
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.
|
|
29
|
-
"@aglyn/shared-ui-snackstack": "1.
|
|
30
|
-
"@aglyn/shared-ui-theme": "1.
|
|
31
|
-
"@aglyn/shared-util-tools": "1.
|
|
32
|
-
"@aglyn/shared-util-vendor": "1.
|
|
28
|
+
"@aglyn/shared-data-mdi": "1.1.0",
|
|
29
|
+
"@aglyn/shared-ui-snackstack": "1.1.0",
|
|
30
|
+
"@aglyn/shared-ui-theme": "1.1.0",
|
|
31
|
+
"@aglyn/shared-util-tools": "1.1.0",
|
|
32
|
+
"@aglyn/shared-util-vendor": "1.1.0",
|
|
33
33
|
"@mdi/js": "^7.4.47",
|
|
34
34
|
"@swc/helpers": "0.5.23",
|
|
35
35
|
"change-case": "^5.4.4",
|
|
@@ -36,8 +36,10 @@ export interface FileDropZoneProps {
|
|
|
36
36
|
label: ReactNode;
|
|
37
37
|
/** One line under it, e.g. the formats and the size limit. */
|
|
38
38
|
hint?: ReactNode;
|
|
39
|
-
/** An mdi path for the icon above the label. */
|
|
40
|
-
icon?: string;
|
|
39
|
+
/** An mdi path for the icon above the label; `null` draws none. */
|
|
40
|
+
icon?: string | null;
|
|
41
|
+
/** The button's name, when the label is a picture rather than words. */
|
|
42
|
+
ariaLabel?: string;
|
|
41
43
|
/** Called with the files `accept` refused, so the caller can say why. */
|
|
42
44
|
onRejected?: (files: File[]) => void;
|
|
43
45
|
sx?: SxProps<Theme>;
|
|
@@ -30,7 +30,7 @@ import { MdiIcon } from "./mdi-icon/index.js";
|
|
|
30
30
|
});
|
|
31
31
|
}
|
|
32
32
|
export function FileDropZone(props) {
|
|
33
|
-
const { onFiles, accept, multiple = true, disabled, label, hint, icon = mdiTrayArrowUp, onRejected, sx } = props;
|
|
33
|
+
const { onFiles, accept, multiple = true, disabled, label, hint, icon = mdiTrayArrowUp, ariaLabel, onRejected, sx } = props;
|
|
34
34
|
const input = useRef(null);
|
|
35
35
|
const hintId = useId();
|
|
36
36
|
const [dragging, setDragging] = useState(false);
|
|
@@ -75,6 +75,7 @@ export function FileDropZone(props) {
|
|
|
75
75
|
return /*#__PURE__*/ _jsxs(ButtonBase, {
|
|
76
76
|
component: "div",
|
|
77
77
|
role: "button",
|
|
78
|
+
"aria-label": ariaLabel,
|
|
78
79
|
"aria-describedby": hint ? hintId : undefined,
|
|
79
80
|
"aria-disabled": disabled || undefined,
|
|
80
81
|
disabled: disabled,
|
|
@@ -132,11 +133,11 @@ export function FileDropZone(props) {
|
|
|
132
133
|
]
|
|
133
134
|
],
|
|
134
135
|
children: [
|
|
135
|
-
/*#__PURE__*/ _jsx(MdiIcon, {
|
|
136
|
+
icon ? /*#__PURE__*/ _jsx(MdiIcon, {
|
|
136
137
|
path: icon,
|
|
137
138
|
fontSize: "medium",
|
|
138
139
|
color: "inherit"
|
|
139
|
-
}),
|
|
140
|
+
}) : null,
|
|
140
141
|
/*#__PURE__*/ _jsx(Typography, {
|
|
141
142
|
variant: "body2",
|
|
142
143
|
sx: {
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../../../../../../../../libs/shared/ui/jsx/src/lib/components/file-drop-zone.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 { mdiTrayArrowUp } from '@mdi/js'\nimport { Box, ButtonBase, Typography, type SxProps, type Theme } from '@mui/material'\nimport { useCallback, useId, useRef, useState, type DragEvent, type ReactNode } from 'react'\nimport { MdiIcon } from './mdi-icon'\n\n/**\n * A place to drop files, which is also the button that opens the file\n * chooser (AGL-3656). Keyboard and screen reader reachable as one button;\n * the drag states are drawn from theme tokens, so it follows light and dark.\n *\n * It hands back the files and nothing else: what is accepted, how each one\n * is checked and where it goes are the caller's, which is what lets a font\n * installer, an importer and a media library share it.\n */\nexport interface FileDropZoneProps {\n /** Called with the files dropped or chosen, in order. */\n onFiles: (files: File[]) => void\n /** The file input's `accept`; dropped files are filtered by it too. */\n accept?: string\n multiple?: boolean\n disabled?: boolean\n /** The headline, e.g. \"Drop font files here\". */\n label: ReactNode\n /** One line under it, e.g. the formats and the size limit. */\n hint?: ReactNode\n /** An mdi path for the icon above the label. */\n icon?: string\n /** Called with the files `accept` refused, so the caller can say why. */\n onRejected?: (files: File[]) => void\n sx?: SxProps<Theme>\n}\n\n/** Whether a file matches an `accept` list of types, wildcards and extensions. */\nexport function fileMatchesAccept(file: Pick<File, 'name' | 'type'>, accept: string | undefined): boolean {\n if (!accept) return true\n const name = file.name.toLowerCase()\n const type = (file.type || '').toLowerCase()\n return accept\n .split(',')\n .map((entry) => entry.trim().toLowerCase())\n .filter(Boolean)\n .some((entry) => {\n if (entry.startsWith('.')) return name.endsWith(entry)\n if (entry.endsWith('/*')) return type.startsWith(entry.slice(0, -1))\n return type === entry\n })\n}\n\nexport function FileDropZone(props: FileDropZoneProps) {\n const { onFiles, accept, multiple = true, disabled, label, hint, icon = mdiTrayArrowUp, onRejected, sx } = props\n const input = useRef<HTMLInputElement>(null)\n const hintId = useId()\n const [dragging, setDragging] = useState(false)\n // Child elements fire their own enter/leave; counting keeps the highlight\n // steady while the pointer crosses them.\n const depth = useRef(0)\n\n const deliver = useCallback(\n (list: FileList | null) => {\n const files = Array.from(list ?? [])\n if (!files.length) return\n const accepted = files.filter((file) => fileMatchesAccept(file, accept))\n const rejected = files.filter((file) => !accepted.includes(file))\n if (rejected.length) onRejected?.(rejected)\n const chosen = multiple ? accepted : accepted.slice(0, 1)\n if (chosen.length) onFiles(chosen)\n },\n [accept, multiple, onFiles, onRejected],\n )\n\n const onDragEnter = (event: DragEvent) => {\n event.preventDefault()\n if (disabled) return\n depth.current += 1\n setDragging(true)\n }\n const onDragLeave = (event: DragEvent) => {\n event.preventDefault()\n depth.current = Math.max(0, depth.current - 1)\n if (!depth.current) setDragging(false)\n }\n const onDragOver = (event: DragEvent) => {\n event.preventDefault()\n if (!disabled) event.dataTransfer.dropEffect = 'copy'\n }\n const onDrop = (event: DragEvent) => {\n event.preventDefault()\n depth.current = 0\n setDragging(false)\n if (!disabled) deliver(event.dataTransfer.files)\n }\n\n return (\n <ButtonBase\n component=\"div\"\n role=\"button\"\n aria-describedby={hint ? hintId : undefined}\n aria-disabled={disabled || undefined}\n disabled={disabled}\n onClick={() => input.current?.click()}\n onDragEnter={onDragEnter}\n onDragLeave={onDragLeave}\n onDragOver={onDragOver}\n onDrop={onDrop}\n data-dragging={dragging || undefined}\n sx={[\n (theme) => {\n const tokens = theme.vars ?? theme\n return {\n width: '100%',\n display: 'flex',\n flexDirection: 'column',\n alignItems: 'center',\n justifyContent: 'center',\n gap: 0.5,\n px: 2,\n py: 3,\n textAlign: 'center',\n borderRadius: `${tokens.shape.borderRadius}px`,\n border: `1px dashed ${tokens.palette.divider}`,\n backgroundColor: 'transparent',\n color: tokens.palette.text.secondary,\n transition: theme.transitions.create(['border-color', 'background-color', 'color'], {\n duration: theme.transitions.duration.shortest,\n }),\n '&:hover, &.Mui-focusVisible': {\n borderColor: tokens.palette.primary.main,\n color: tokens.palette.text.primary,\n },\n '&[data-dragging]': {\n borderStyle: 'solid',\n borderColor: tokens.palette.primary.main,\n backgroundColor: theme.alpha(tokens.palette.primary.main, tokens.palette.action.hoverOpacity),\n color: tokens.palette.primary.main,\n },\n '&.Mui-disabled': { opacity: tokens.palette.action.disabledOpacity },\n }\n },\n ...(Array.isArray(sx) ? sx : [sx]),\n ]}\n >\n <MdiIcon path={icon} fontSize=\"medium\" color=\"inherit\" />\n <Typography variant=\"body2\" sx={{ fontWeight: 'fontWeightMedium', color: 'inherit' }}>\n {label}\n </Typography>\n {hint ? (\n <Typography id={hintId} variant=\"caption\" color=\"text.secondary\">\n {hint}\n </Typography>\n ) : null}\n <Box\n component=\"input\"\n ref={input}\n type=\"file\"\n accept={accept}\n multiple={multiple}\n hidden\n tabIndex={-1}\n onClick={(event) => event.stopPropagation()}\n onChange={(event) => {\n deliver((event.target as HTMLInputElement).files)\n ;(event.target as HTMLInputElement).value = ''\n }}\n />\n </ButtonBase>\n )\n}\n\nexport default FileDropZone\n"],"names":["mdiTrayArrowUp","Box","ButtonBase","Typography","useCallback","useId","useRef","useState","MdiIcon","fileMatchesAccept","file","accept","name","toLowerCase","type","split","map","entry","trim","filter","Boolean","some","startsWith","endsWith","slice","FileDropZone","props","onFiles","multiple","disabled","label","hint","icon","onRejected","sx","input","hintId","dragging","setDragging","depth","deliver","list","files","Array","from","length","accepted","rejected","includes","chosen","onDragEnter","event","preventDefault","current","onDragLeave","Math","max","onDragOver","dataTransfer","dropEffect","onDrop","component","role","aria-describedby","undefined","aria-disabled","onClick","click","data-dragging","theme","tokens","vars","width","display","flexDirection","alignItems","justifyContent","gap","px","py","textAlign","borderRadius","shape","border","palette","divider","backgroundColor","color","text","secondary","transition","transitions","create","duration","shortest","borderColor","primary","main","borderStyle","alpha","action","hoverOpacity","opacity","disabledOpacity","isArray","path","fontSize","variant","fontWeight","id","ref","hidden","tabIndex","stopPropagation","onChange","target","value"],"mappings":"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"}
|
|
1
|
+
{"version":3,"sources":["../../../../../../../../libs/shared/ui/jsx/src/lib/components/file-drop-zone.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 { mdiTrayArrowUp } from '@mdi/js'\nimport { Box, ButtonBase, Typography, type SxProps, type Theme } from '@mui/material'\nimport { useCallback, useId, useRef, useState, type DragEvent, type ReactNode } from 'react'\nimport { MdiIcon } from './mdi-icon'\n\n/**\n * A place to drop files, which is also the button that opens the file\n * chooser (AGL-3656). Keyboard and screen reader reachable as one button;\n * the drag states are drawn from theme tokens, so it follows light and dark.\n *\n * It hands back the files and nothing else: what is accepted, how each one\n * is checked and where it goes are the caller's, which is what lets a font\n * installer, an importer and a media library share it.\n */\nexport interface FileDropZoneProps {\n /** Called with the files dropped or chosen, in order. */\n onFiles: (files: File[]) => void\n /** The file input's `accept`; dropped files are filtered by it too. */\n accept?: string\n multiple?: boolean\n disabled?: boolean\n /** The headline, e.g. \"Drop font files here\". */\n label: ReactNode\n /** One line under it, e.g. the formats and the size limit. */\n hint?: ReactNode\n /** An mdi path for the icon above the label; `null` draws none. */\n icon?: string | null\n /** The button's name, when the label is a picture rather than words. */\n ariaLabel?: string\n /** Called with the files `accept` refused, so the caller can say why. */\n onRejected?: (files: File[]) => void\n sx?: SxProps<Theme>\n}\n\n/** Whether a file matches an `accept` list of types, wildcards and extensions. */\nexport function fileMatchesAccept(file: Pick<File, 'name' | 'type'>, accept: string | undefined): boolean {\n if (!accept) return true\n const name = file.name.toLowerCase()\n const type = (file.type || '').toLowerCase()\n return accept\n .split(',')\n .map((entry) => entry.trim().toLowerCase())\n .filter(Boolean)\n .some((entry) => {\n if (entry.startsWith('.')) return name.endsWith(entry)\n if (entry.endsWith('/*')) return type.startsWith(entry.slice(0, -1))\n return type === entry\n })\n}\n\nexport function FileDropZone(props: FileDropZoneProps) {\n const {\n onFiles,\n accept,\n multiple = true,\n disabled,\n label,\n hint,\n icon = mdiTrayArrowUp,\n ariaLabel,\n onRejected,\n sx,\n } = props\n const input = useRef<HTMLInputElement>(null)\n const hintId = useId()\n const [dragging, setDragging] = useState(false)\n // Child elements fire their own enter/leave; counting keeps the highlight\n // steady while the pointer crosses them.\n const depth = useRef(0)\n\n const deliver = useCallback(\n (list: FileList | null) => {\n const files = Array.from(list ?? [])\n if (!files.length) return\n const accepted = files.filter((file) => fileMatchesAccept(file, accept))\n const rejected = files.filter((file) => !accepted.includes(file))\n if (rejected.length) onRejected?.(rejected)\n const chosen = multiple ? accepted : accepted.slice(0, 1)\n if (chosen.length) onFiles(chosen)\n },\n [accept, multiple, onFiles, onRejected],\n )\n\n const onDragEnter = (event: DragEvent) => {\n event.preventDefault()\n if (disabled) return\n depth.current += 1\n setDragging(true)\n }\n const onDragLeave = (event: DragEvent) => {\n event.preventDefault()\n depth.current = Math.max(0, depth.current - 1)\n if (!depth.current) setDragging(false)\n }\n const onDragOver = (event: DragEvent) => {\n event.preventDefault()\n if (!disabled) event.dataTransfer.dropEffect = 'copy'\n }\n const onDrop = (event: DragEvent) => {\n event.preventDefault()\n depth.current = 0\n setDragging(false)\n if (!disabled) deliver(event.dataTransfer.files)\n }\n\n return (\n <ButtonBase\n component=\"div\"\n role=\"button\"\n aria-label={ariaLabel}\n aria-describedby={hint ? hintId : undefined}\n aria-disabled={disabled || undefined}\n disabled={disabled}\n onClick={() => input.current?.click()}\n onDragEnter={onDragEnter}\n onDragLeave={onDragLeave}\n onDragOver={onDragOver}\n onDrop={onDrop}\n data-dragging={dragging || undefined}\n sx={[\n (theme) => {\n const tokens = theme.vars ?? theme\n return {\n width: '100%',\n display: 'flex',\n flexDirection: 'column',\n alignItems: 'center',\n justifyContent: 'center',\n gap: 0.5,\n px: 2,\n py: 3,\n textAlign: 'center',\n borderRadius: `${tokens.shape.borderRadius}px`,\n border: `1px dashed ${tokens.palette.divider}`,\n backgroundColor: 'transparent',\n color: tokens.palette.text.secondary,\n transition: theme.transitions.create(['border-color', 'background-color', 'color'], {\n duration: theme.transitions.duration.shortest,\n }),\n '&:hover, &.Mui-focusVisible': {\n borderColor: tokens.palette.primary.main,\n color: tokens.palette.text.primary,\n },\n '&[data-dragging]': {\n borderStyle: 'solid',\n borderColor: tokens.palette.primary.main,\n backgroundColor: theme.alpha(tokens.palette.primary.main, tokens.palette.action.hoverOpacity),\n color: tokens.palette.primary.main,\n },\n '&.Mui-disabled': { opacity: tokens.palette.action.disabledOpacity },\n }\n },\n ...(Array.isArray(sx) ? sx : [sx]),\n ]}\n >\n {icon ? <MdiIcon path={icon} fontSize=\"medium\" color=\"inherit\" /> : null}\n <Typography variant=\"body2\" sx={{ fontWeight: 'fontWeightMedium', color: 'inherit' }}>\n {label}\n </Typography>\n {hint ? (\n <Typography id={hintId} variant=\"caption\" color=\"text.secondary\">\n {hint}\n </Typography>\n ) : null}\n <Box\n component=\"input\"\n ref={input}\n type=\"file\"\n accept={accept}\n multiple={multiple}\n hidden\n tabIndex={-1}\n onClick={(event) => event.stopPropagation()}\n onChange={(event) => {\n deliver((event.target as HTMLInputElement).files)\n ;(event.target as HTMLInputElement).value = ''\n }}\n />\n </ButtonBase>\n )\n}\n\nexport default FileDropZone\n"],"names":["mdiTrayArrowUp","Box","ButtonBase","Typography","useCallback","useId","useRef","useState","MdiIcon","fileMatchesAccept","file","accept","name","toLowerCase","type","split","map","entry","trim","filter","Boolean","some","startsWith","endsWith","slice","FileDropZone","props","onFiles","multiple","disabled","label","hint","icon","ariaLabel","onRejected","sx","input","hintId","dragging","setDragging","depth","deliver","list","files","Array","from","length","accepted","rejected","includes","chosen","onDragEnter","event","preventDefault","current","onDragLeave","Math","max","onDragOver","dataTransfer","dropEffect","onDrop","component","role","aria-label","aria-describedby","undefined","aria-disabled","onClick","click","data-dragging","theme","tokens","vars","width","display","flexDirection","alignItems","justifyContent","gap","px","py","textAlign","borderRadius","shape","border","palette","divider","backgroundColor","color","text","secondary","transition","transitions","create","duration","shortest","borderColor","primary","main","borderStyle","alpha","action","hoverOpacity","opacity","disabledOpacity","isArray","path","fontSize","variant","fontWeight","id","ref","hidden","tabIndex","stopPropagation","onChange","target","value"],"mappings":"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"}
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
import { type SxProps, type Theme } from '@mui/material';
|
|
2
|
+
import { type ReactNode } from 'react';
|
|
3
|
+
/** What a picture is previewed on: the ground it will be seen on. */
|
|
4
|
+
export type ImageFileFieldGround = 'light' | 'dark';
|
|
5
|
+
export interface ImageFileFieldProps {
|
|
6
|
+
label: string;
|
|
7
|
+
/** The file held, or `null`. Nothing is uploaded here. */
|
|
8
|
+
file: File | null;
|
|
9
|
+
/** Called with the file chosen, or `null` when it is removed. */
|
|
10
|
+
onChange: (file: File | null) => void;
|
|
11
|
+
/** The file chooser's `accept`; a dropped file is held to it too. */
|
|
12
|
+
accept?: string;
|
|
13
|
+
/** Called with a file `accept` refused, so the caller can say why. */
|
|
14
|
+
onRejected?: (file: File) => void;
|
|
15
|
+
/** Draws the field on this ground. Without one it sits on the surface it is on. */
|
|
16
|
+
ground?: ImageFileFieldGround;
|
|
17
|
+
/** One line under the field, while no file is held. */
|
|
18
|
+
helperText?: ReactNode;
|
|
19
|
+
/** Why the last file was refused. Shown over the helper text and the file's name. */
|
|
20
|
+
error?: ReactNode;
|
|
21
|
+
disabled?: boolean;
|
|
22
|
+
sx?: SxProps<Theme>;
|
|
23
|
+
}
|
|
24
|
+
/**
|
|
25
|
+
* Pick one image, see it, and take it away again (AGL-4119). A compact tile on
|
|
26
|
+
* {@link FileDropZone}, for a form that holds a file until there is somewhere
|
|
27
|
+
* to put it. It hands back the file and nothing else: what a form accepts,
|
|
28
|
+
* what it refuses and where the file goes are the form's. Figma: "Create site /
|
|
29
|
+
* Image file field" in Aglyn Console (web), page Sites · Create site.
|
|
30
|
+
*/
|
|
31
|
+
export declare function ImageFileField(props: ImageFileFieldProps): import("react").JSX.Element;
|
|
32
|
+
export default ImageFileField;
|
|
@@ -0,0 +1,144 @@
|
|
|
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 { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
18
|
+
import { mdiTrayArrowUp } from "@mdi/js";
|
|
19
|
+
import { Box, Link, Stack, Typography } from "@mui/material";
|
|
20
|
+
import { useEffect, useState } from "react";
|
|
21
|
+
import { FileDropZone } from "./file-drop-zone.component.js";
|
|
22
|
+
/** The tile's colors on a ground, read from the theme's own greys. */ const GROUND_SX = {
|
|
23
|
+
light: {
|
|
24
|
+
bgcolor: 'common.white',
|
|
25
|
+
color: 'grey.600',
|
|
26
|
+
'&:hover, &.Mui-focusVisible': {
|
|
27
|
+
color: 'grey.900'
|
|
28
|
+
}
|
|
29
|
+
},
|
|
30
|
+
dark: {
|
|
31
|
+
bgcolor: 'grey.900',
|
|
32
|
+
color: 'grey.400',
|
|
33
|
+
'&:hover, &.Mui-focusVisible': {
|
|
34
|
+
color: 'common.white'
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
};
|
|
38
|
+
/**
|
|
39
|
+
* A URL for a file held in memory, for as long as it is held. Empty outside a
|
|
40
|
+
* browser, and released when the file changes or the field goes away.
|
|
41
|
+
*/ function useFilePreview(file) {
|
|
42
|
+
const [preview, setPreview] = useState(null);
|
|
43
|
+
useEffect(()=>{
|
|
44
|
+
if (!file || typeof URL.createObjectURL !== 'function') return void setPreview(null);
|
|
45
|
+
const url = URL.createObjectURL(file);
|
|
46
|
+
setPreview(url);
|
|
47
|
+
return ()=>URL.revokeObjectURL(url);
|
|
48
|
+
}, [
|
|
49
|
+
file
|
|
50
|
+
]);
|
|
51
|
+
return preview;
|
|
52
|
+
}
|
|
53
|
+
/**
|
|
54
|
+
* Pick one image, see it, and take it away again (AGL-4119). A compact tile on
|
|
55
|
+
* {@link FileDropZone}, for a form that holds a file until there is somewhere
|
|
56
|
+
* to put it. It hands back the file and nothing else: what a form accepts,
|
|
57
|
+
* what it refuses and where the file goes are the form's. Figma: "Create site /
|
|
58
|
+
* Image file field" in Aglyn Console (web), page Sites · Create site.
|
|
59
|
+
*/ export function ImageFileField(props) {
|
|
60
|
+
const { label, file, onChange, accept, onRejected, ground, helperText, error, disabled, sx } = props;
|
|
61
|
+
const preview = useFilePreview(file);
|
|
62
|
+
const below = error != null ? error : file ? file.name : helperText;
|
|
63
|
+
return /*#__PURE__*/ _jsxs(Stack, {
|
|
64
|
+
spacing: 0.5,
|
|
65
|
+
sx: [
|
|
66
|
+
{
|
|
67
|
+
minWidth: 0
|
|
68
|
+
},
|
|
69
|
+
...Array.isArray(sx) ? sx : [
|
|
70
|
+
sx
|
|
71
|
+
]
|
|
72
|
+
],
|
|
73
|
+
children: [
|
|
74
|
+
/*#__PURE__*/ _jsxs(Stack, {
|
|
75
|
+
direction: "row",
|
|
76
|
+
sx: {
|
|
77
|
+
alignItems: 'baseline',
|
|
78
|
+
justifyContent: 'space-between',
|
|
79
|
+
columnGap: 1
|
|
80
|
+
},
|
|
81
|
+
children: [
|
|
82
|
+
/*#__PURE__*/ _jsx(Typography, {
|
|
83
|
+
variant: "body2",
|
|
84
|
+
noWrap: true,
|
|
85
|
+
title: label,
|
|
86
|
+
children: label
|
|
87
|
+
}),
|
|
88
|
+
file ? /*#__PURE__*/ _jsx(Link, {
|
|
89
|
+
component: "button",
|
|
90
|
+
type: "button",
|
|
91
|
+
variant: "caption",
|
|
92
|
+
color: "text.secondary",
|
|
93
|
+
disabled: disabled,
|
|
94
|
+
"aria-label": `Remove ${label}`,
|
|
95
|
+
onClick: ()=>onChange(null),
|
|
96
|
+
children: 'Remove'
|
|
97
|
+
}) : null
|
|
98
|
+
]
|
|
99
|
+
}),
|
|
100
|
+
/*#__PURE__*/ _jsx(FileDropZone, {
|
|
101
|
+
accept: accept,
|
|
102
|
+
multiple: false,
|
|
103
|
+
disabled: disabled,
|
|
104
|
+
ariaLabel: file ? `Replace ${label}` : `Choose ${label}`,
|
|
105
|
+
icon: file ? null : mdiTrayArrowUp,
|
|
106
|
+
label: file && preview ? /*#__PURE__*/ _jsx(Box, {
|
|
107
|
+
component: "img",
|
|
108
|
+
src: preview,
|
|
109
|
+
alt: "",
|
|
110
|
+
sx: {
|
|
111
|
+
display: 'block',
|
|
112
|
+
maxWidth: '100%',
|
|
113
|
+
maxHeight: (theme)=>theme.spacing(8),
|
|
114
|
+
objectFit: 'contain'
|
|
115
|
+
}
|
|
116
|
+
}) : file ? file.name : 'Choose file',
|
|
117
|
+
onFiles: ([chosen])=>onChange(chosen != null ? chosen : null),
|
|
118
|
+
onRejected: ([refused])=>refused ? onRejected == null ? void 0 : onRejected(refused) : undefined,
|
|
119
|
+
sx: [
|
|
120
|
+
{
|
|
121
|
+
height: (theme)=>theme.spacing(9),
|
|
122
|
+
flexDirection: file ? 'row' : 'column',
|
|
123
|
+
gap: 0,
|
|
124
|
+
px: 1,
|
|
125
|
+
py: 0.5,
|
|
126
|
+
overflow: 'hidden'
|
|
127
|
+
},
|
|
128
|
+
ground ? GROUND_SX[ground] : {}
|
|
129
|
+
]
|
|
130
|
+
}),
|
|
131
|
+
below ? /*#__PURE__*/ _jsx(Typography, {
|
|
132
|
+
variant: "caption",
|
|
133
|
+
color: error ? 'error' : 'text.secondary',
|
|
134
|
+
noWrap: !error && Boolean(file),
|
|
135
|
+
role: error ? 'alert' : undefined,
|
|
136
|
+
title: !error && file ? file.name : undefined,
|
|
137
|
+
children: below
|
|
138
|
+
}) : null
|
|
139
|
+
]
|
|
140
|
+
});
|
|
141
|
+
}
|
|
142
|
+
export default ImageFileField;
|
|
143
|
+
|
|
144
|
+
//# sourceMappingURL=image-file-field.component.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../../../../../../../../libs/shared/ui/jsx/src/lib/components/image-file-field.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'use client'\n\nimport { mdiTrayArrowUp } from '@mdi/js'\nimport { Box, Link, Stack, Typography, type SxProps, type Theme } from '@mui/material'\nimport { useEffect, useState, type ReactNode } from 'react'\nimport { FileDropZone } from './file-drop-zone.component'\n\n/** What a picture is previewed on: the ground it will be seen on. */\nexport type ImageFileFieldGround = 'light' | 'dark'\n\nexport interface ImageFileFieldProps {\n label: string\n /** The file held, or `null`. Nothing is uploaded here. */\n file: File | null\n /** Called with the file chosen, or `null` when it is removed. */\n onChange: (file: File | null) => void\n /** The file chooser's `accept`; a dropped file is held to it too. */\n accept?: string\n /** Called with a file `accept` refused, so the caller can say why. */\n onRejected?: (file: File) => void\n /** Draws the field on this ground. Without one it sits on the surface it is on. */\n ground?: ImageFileFieldGround\n /** One line under the field, while no file is held. */\n helperText?: ReactNode\n /** Why the last file was refused. Shown over the helper text and the file's name. */\n error?: ReactNode\n disabled?: boolean\n sx?: SxProps<Theme>\n}\n\n/** The tile's colors on a ground, read from the theme's own greys. */\nconst GROUND_SX: Readonly<Record<ImageFileFieldGround, Record<string, unknown>>> = {\n light: {\n bgcolor: 'common.white',\n color: 'grey.600',\n '&:hover, &.Mui-focusVisible': { color: 'grey.900' },\n },\n dark: {\n bgcolor: 'grey.900',\n color: 'grey.400',\n '&:hover, &.Mui-focusVisible': { color: 'common.white' },\n },\n}\n\n/**\n * A URL for a file held in memory, for as long as it is held. Empty outside a\n * browser, and released when the file changes or the field goes away.\n */\nfunction useFilePreview(file: File | null): string | null {\n const [preview, setPreview] = useState<string | null>(null)\n useEffect(() => {\n if (!file || typeof URL.createObjectURL !== 'function') return void setPreview(null)\n const url = URL.createObjectURL(file)\n setPreview(url)\n return () => URL.revokeObjectURL(url)\n }, [file])\n return preview\n}\n\n/**\n * Pick one image, see it, and take it away again (AGL-4119). A compact tile on\n * {@link FileDropZone}, for a form that holds a file until there is somewhere\n * to put it. It hands back the file and nothing else: what a form accepts,\n * what it refuses and where the file goes are the form's. Figma: \"Create site /\n * Image file field\" in Aglyn Console (web), page Sites · Create site.\n */\nexport function ImageFileField(props: ImageFileFieldProps) {\n const { label, file, onChange, accept, onRejected, ground, helperText, error, disabled, sx } = props\n const preview = useFilePreview(file)\n const below = error ?? (file ? file.name : helperText)\n return (\n <Stack spacing={0.5} sx={[{ minWidth: 0 }, ...(Array.isArray(sx) ? sx : [sx])]}>\n <Stack direction=\"row\" sx={{ alignItems: 'baseline', justifyContent: 'space-between', columnGap: 1 }}>\n <Typography variant=\"body2\" noWrap title={label}>\n {label}\n </Typography>\n {file ? (\n <Link\n component=\"button\"\n type=\"button\"\n variant=\"caption\"\n color=\"text.secondary\"\n disabled={disabled}\n aria-label={`Remove ${label}`}\n onClick={() => onChange(null)}\n >\n {'Remove'}\n </Link>\n ) : null}\n </Stack>\n <FileDropZone\n accept={accept}\n multiple={false}\n disabled={disabled}\n ariaLabel={file ? `Replace ${label}` : `Choose ${label}`}\n icon={file ? null : mdiTrayArrowUp}\n label={\n file && preview ? (\n <Box\n component=\"img\"\n src={preview}\n alt=\"\"\n sx={{\n display: 'block',\n maxWidth: '100%',\n maxHeight: (theme) => theme.spacing(8),\n objectFit: 'contain',\n }}\n />\n ) : file ? (\n file.name\n ) : (\n 'Choose file'\n )\n }\n onFiles={([chosen]) => onChange(chosen ?? null)}\n onRejected={([refused]) => (refused ? onRejected?.(refused) : undefined)}\n sx={[\n {\n height: (theme) => theme.spacing(9),\n flexDirection: file ? 'row' : 'column',\n gap: 0,\n px: 1,\n py: 0.5,\n overflow: 'hidden',\n },\n ground ? GROUND_SX[ground] : {},\n ]}\n />\n {below ? (\n <Typography\n variant=\"caption\"\n color={error ? 'error' : 'text.secondary'}\n noWrap={!error && Boolean(file)}\n role={error ? 'alert' : undefined}\n title={!error && file ? file.name : undefined}\n >\n {below}\n </Typography>\n ) : null}\n </Stack>\n )\n}\n\nexport default ImageFileField\n"],"names":["mdiTrayArrowUp","Box","Link","Stack","Typography","useEffect","useState","FileDropZone","GROUND_SX","light","bgcolor","color","dark","useFilePreview","file","preview","setPreview","URL","createObjectURL","url","revokeObjectURL","ImageFileField","props","label","onChange","accept","onRejected","ground","helperText","error","disabled","sx","below","name","spacing","minWidth","Array","isArray","direction","alignItems","justifyContent","columnGap","variant","noWrap","title","component","type","aria-label","onClick","multiple","ariaLabel","icon","src","alt","display","maxWidth","maxHeight","theme","objectFit","onFiles","chosen","refused","undefined","height","flexDirection","gap","px","py","overflow","Boolean","role"],"mappings":"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"}
|