@sqrzro/ui 4.0.0-alpha.61 → 4.0.0-alpha.63

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.
@@ -1,6 +1,8 @@
1
1
  import type { ClassNameProps, StylableClassName } from '../../../styles/classnames/interfaces';
2
2
  import type { StyleVariant } from '../../../utility/interfaces';
3
3
  export interface InfoPanelClassNames {
4
+ content: string;
5
+ icon: StylableClassName;
4
6
  root: StylableClassName;
5
7
  }
6
8
  export interface InfoPanelProps extends ClassNameProps<InfoPanelClassNames> {
@@ -1,9 +1,20 @@
1
1
  'use client';
2
- import { jsx as _jsx } from "react/jsx-runtime";
3
- import { useClassNames } from '../../../styles/context';
2
+ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
3
+ import { useClassNames, useIcon } from '../../../styles/context';
4
4
  import applyVariants from '../../../styles/classnames/utility/apply-variants';
5
5
  function InfoPanel({ classNameProps, classNames, children, variant, }) {
6
- const componentClassNames = useClassNames('infoPanel', { props: { ...classNameProps, ...applyVariants(variant) } }, classNames);
7
- return _jsx("aside", { className: componentClassNames?.root, children: children });
6
+ const ErrorIcon = useIcon('infoPanel.error');
7
+ const InfoIcon = useIcon('infoPanel.info');
8
+ const SuccessIcon = useIcon('infoPanel.success');
9
+ const WarningIcon = useIcon('infoPanel.warning');
10
+ const iconMap = {
11
+ error: ErrorIcon,
12
+ info: InfoIcon,
13
+ success: SuccessIcon,
14
+ warning: WarningIcon,
15
+ };
16
+ const Icon = variant ? iconMap[Array.isArray(variant) ? variant[0] : variant] : undefined;
17
+ const componentClassNames = useClassNames('infoPanel', { props: { ...classNameProps, ...applyVariants(variant), hasIcon: Boolean(Icon) } }, classNames);
18
+ return (_jsxs("aside", { className: componentClassNames?.root, children: [Icon && _jsx(Icon, { className: componentClassNames?.icon }), _jsx("div", { className: componentClassNames?.content, children: children })] }));
8
19
  }
9
20
  export default InfoPanel;
@@ -5,5 +5,5 @@ interface MenuItemProps<Data, Response> extends ClassNameProps<MenuClassNames>,
5
5
  isDownload?: boolean;
6
6
  isNewWindow?: boolean;
7
7
  }
8
- declare function MenuItem<Data, Response>({ children, classNameProps, classNames, href, isDownload, isLoading, isNewWindow, onClick, }: MenuItemProps<Data, Response>): React.ReactElement;
8
+ declare function MenuItem<Data, Response>({ children, classNameProps, classNames, href, isDisabled, isDownload, isLoading, isNewWindow, onClick, }: MenuItemProps<Data, Response>): React.ReactElement;
9
9
  export default MenuItem;
@@ -1,12 +1,14 @@
1
1
  'use client';
2
2
  import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
3
3
  import { useClassNames } from '../../../styles/context';
4
+ import tw from '../../../styles/classnames/utility/tw';
4
5
  import Link from '../Link';
5
- function MenuItem({ children, classNameProps, classNames, href, isDownload, isLoading, isNewWindow, onClick, }) {
6
+ function MenuItem({ children, classNameProps, classNames, href, isDisabled, isDownload, isLoading, isNewWindow, onClick, }) {
6
7
  const componentClassNames = useClassNames('menu', { props: classNameProps }, classNames);
8
+ const linkClass = tw(componentClassNames?.link, isDisabled ? 'pointer-events-none opacity-30' : null);
7
9
  if (href) {
8
- return (_jsx(Link, { className: componentClassNames?.link, href: href, isDownload: isDownload, isNewWindow: isNewWindow, onClick: onClick, children: children }));
10
+ return (_jsx(Link, { className: linkClass, href: href, isDownload: isDownload, isNewWindow: isNewWindow, onClick: onClick, children: children }));
9
11
  }
10
- return (_jsxs("button", { className: componentClassNames?.link, onClick: onClick, type: "button", children: [children, " ", isLoading && '...'] }));
12
+ return (_jsxs("button", { className: linkClass, onClick: onClick, type: "button", children: [children, " ", isLoading && '...'] }));
11
13
  }
12
14
  export default MenuItem;
@@ -1,12 +1,15 @@
1
1
  import type { EditableFormFieldComponentProps } from '../../interfaces';
2
2
  import type { ColorInputComponentProps } from '../ColorInput';
3
3
  import type { DropdownComponentProps } from '../Dropdown';
4
+ import type { ImageInputComponentProps } from '../ImageInput';
4
5
  import type { TextAreaComponentProps } from '../TextArea';
5
6
  import type { TextInputComponentProps } from '../TextInput';
6
7
  export type EditableColorFormFieldProps = EditableFormFieldComponentProps<string | undefined> & ColorInputComponentProps;
7
8
  export declare function EditableColorFormField(props: Readonly<EditableColorFormFieldProps>): React.ReactElement;
8
9
  export type EditableDropdownFormFieldProps<T> = EditableFormFieldComponentProps<T | null> & DropdownComponentProps<T>;
9
10
  export declare function EditableDropdownFormField<T>(props: Readonly<EditableDropdownFormFieldProps<T>>): React.ReactElement;
11
+ export type EditableImageFormFieldProps = EditableFormFieldComponentProps<string | null> & ImageInputComponentProps;
12
+ export declare function EditableImageFormField(props: Readonly<EditableImageFormFieldProps>): React.ReactElement;
10
13
  export type EditableTextAreaFormFieldProps = EditableFormFieldComponentProps<string> & TextAreaComponentProps;
11
14
  export declare function EditableTextAreaFormField(props: Readonly<EditableTextAreaFormFieldProps>): React.ReactElement;
12
15
  export type EditableTextFormFieldProps = EditableFormFieldComponentProps<string> & TextInputComponentProps;
@@ -5,6 +5,7 @@ import tw from '../../../styles/classnames/utility/tw';
5
5
  import extractEditableInputProps from '../../utility/extract-editable-input-props';
6
6
  import ColorInput from '../ColorInput';
7
7
  import Dropdown from '../Dropdown';
8
+ import ImageInput from '../ImageInput';
8
9
  import EditableFormField from '../EditableFormField';
9
10
  import TextArea from '../TextArea';
10
11
  import TextInput from '../TextInput';
@@ -29,6 +30,18 @@ export function EditableDropdownFormField(props) {
29
30
  }, [inputProps]);
30
31
  return _jsx(EditableFormField, { ...fieldProps, render: renderInput, renderValue: renderValue });
31
32
  }
33
+ export function EditableImageFormField(props) {
34
+ const classNames = useClassNames('imageInput');
35
+ const { fieldProps, inputProps } = extractEditableInputProps(props);
36
+ const renderInput = useCallback((renderProps) => (_jsx(ImageInput, { ...renderProps, ...inputProps })), [inputProps]);
37
+ const renderValue = useCallback((value) => {
38
+ if (!value) {
39
+ return '-';
40
+ }
41
+ return (_jsx("div", { className: tw('relative flex items-center justify-center overflow-hidden text-center', classNames?.root), children: _jsx("img", { src: value, className: classNames?.image }) }));
42
+ }, [fieldProps.value]);
43
+ return _jsx(EditableFormField, { ...fieldProps, render: renderInput, renderValue: renderValue });
44
+ }
32
45
  export function EditableTextAreaFormField(props) {
33
46
  const { fieldProps, inputProps } = extractEditableInputProps(props);
34
47
  const renderInput = useCallback((renderProps) => (_jsx(TextArea, { ...renderProps, ...inputProps })), [inputProps]);
@@ -3,6 +3,7 @@ import type { AutocompleteComponentProps } from '../Autocomplete';
3
3
  import type { ColorInputComponentProps } from '../ColorInput';
4
4
  import type { CalendarInputComponentProps } from '../CalendarInput';
5
5
  import type { DropdownComponentProps } from '../Dropdown';
6
+ import type { ImageInputComponentProps } from '../ImageInput';
6
7
  import type { PointsInputComponentProps } from '../PointsInput';
7
8
  import type { NumberInputComponentProps } from '../NumberInput';
8
9
  import type { PasswordInputComponentProps } from '../PasswordInput';
@@ -20,6 +21,8 @@ export type CSVFormFieldProps = FormFieldComponentProps<string>;
20
21
  export declare function CSVFormField(props: Readonly<CSVFormFieldProps>): React.ReactElement;
21
22
  export type DropdownFormFieldProps<T> = FormFieldComponentProps<T | null> & DropdownComponentProps<T>;
22
23
  export declare function DropdownFormField<T>(props: Readonly<DropdownFormFieldProps<T>>): React.ReactElement;
24
+ export type ImageFormFieldProps = FormFieldComponentProps<string | null> & ImageInputComponentProps;
25
+ export declare function ImageFormField(props: Readonly<ImageFormFieldProps>): React.ReactElement;
23
26
  export type MoneyFormFieldProps = FormFieldComponentProps<number> & PointsInputComponentProps;
24
27
  export declare function MoneyFormField(props: Readonly<MoneyFormFieldProps>): React.ReactElement;
25
28
  export type NumberFormFieldProps = FormFieldComponentProps<number> & NumberInputComponentProps;
@@ -7,6 +7,7 @@ import CSVInput from '../CSVInput';
7
7
  import CalendarInput from '../CalendarInput';
8
8
  import Dropdown from '../Dropdown';
9
9
  import FormField from '../FormField';
10
+ import ImageInput from '../ImageInput';
10
11
  import PointsInput from '../PointsInput';
11
12
  import NumberInput from '../NumberInput';
12
13
  import PasswordInput from '../PasswordInput';
@@ -39,6 +40,11 @@ export function DropdownFormField(props) {
39
40
  const renderInput = useCallback((renderProps) => (_jsx(Dropdown, { ...renderProps, ...inputProps })), [inputProps]);
40
41
  return _jsx(FormField, { ...fieldProps, render: renderInput });
41
42
  }
43
+ export function ImageFormField(props) {
44
+ const { fieldProps, inputProps } = extractInputProps(props);
45
+ const renderInput = useCallback((renderProps) => (_jsx(ImageInput, { ...renderProps, ...inputProps })), [inputProps]);
46
+ return _jsx(FormField, { ...fieldProps, render: renderInput });
47
+ }
42
48
  export function MoneyFormField(props) {
43
49
  const { fieldProps, inputProps } = extractInputProps(props);
44
50
  const renderInput = useCallback((renderProps) => (_jsx(PointsInput, { ...renderProps, ...inputProps })), [inputProps]);
@@ -0,0 +1,15 @@
1
+ import type { FormResponse, InputProps } from '../../../forms/interfaces';
2
+ import type { ClassNameProps, HighlightableClassName } from '../../../styles/classnames/interfaces';
3
+ export interface ImageInputClassNames {
4
+ description: string;
5
+ icon: HighlightableClassName;
6
+ image: string;
7
+ root: string;
8
+ title: string;
9
+ }
10
+ export interface ImageInputComponentProps {
11
+ onUpload?: (file: File) => FormResponse<string>;
12
+ }
13
+ export type ImageInputProps = ClassNameProps<ImageInputClassNames> & InputProps<string | null> & ImageInputComponentProps;
14
+ declare function ImageInput({ classNames, classNameProps, id, isDisabled, name, onChange, onUpload, value, }: Readonly<ImageInputProps>): React.ReactElement;
15
+ export default ImageInput;
@@ -0,0 +1,46 @@
1
+ 'use client';
2
+ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
3
+ import { useState } from 'react';
4
+ import preprocessImage from '../../../forms/utility/preprocess-image';
5
+ import { useClassNames } from '../../../styles/context';
6
+ import tw from '../../../styles/classnames/utility/tw';
7
+ import ImageInputPanel from '../ImageInputPanel';
8
+ function ImageInput({ classNames, classNameProps, id, isDisabled, name, onChange, onUpload, value, }) {
9
+ const [error, setError] = useState(null);
10
+ const [isLoading, setIsLoading] = useState(false);
11
+ const componentClassNames = useClassNames('imageInput', {
12
+ props: classNameProps,
13
+ }, classNames);
14
+ async function handleChange(event) {
15
+ if (!onUpload) {
16
+ return;
17
+ }
18
+ setError(null);
19
+ setIsLoading(true);
20
+ const imageFiles = event.target.files;
21
+ if (!imageFiles?.length) {
22
+ setIsLoading(false);
23
+ return;
24
+ }
25
+ try {
26
+ const preprocessed = await preprocessImage(imageFiles[0]);
27
+ const fnReturn = await onUpload(preprocessed);
28
+ setIsLoading(false);
29
+ if (fnReturn.error) {
30
+ setError(fnReturn.error);
31
+ }
32
+ else if (fnReturn.validation) {
33
+ setError(Object.values(fnReturn.validation)[0]);
34
+ }
35
+ else if (fnReturn.data !== null) {
36
+ onChange?.({ target: { name, value: fnReturn.data } });
37
+ }
38
+ }
39
+ catch (err) {
40
+ setIsLoading(false);
41
+ setError(err instanceof Error ? err.message : 'An unexpected error occurred');
42
+ }
43
+ }
44
+ return (_jsxs("div", { className: tw('relative flex items-center justify-center overflow-hidden text-center', componentClassNames?.root, isDisabled || isLoading ? 'pointer-events-none opacity-20' : null), children: [_jsx("input", { name: name, type: "hidden", value: value || '' }), _jsx("input", { accept: "image/*", className: "absolute left-0 top-0 h-full w-full cursor-pointer opacity-0", disabled: isDisabled || isLoading, id: id || name, name: name, onChange: handleChange, type: "file" }), _jsx(ImageInputPanel, { classNames: classNames, classNameProps: { ...classNameProps, isError: Boolean(error) }, error: error, isLoading: isLoading, value: value })] }));
45
+ }
46
+ export default ImageInput;
@@ -0,0 +1,9 @@
1
+ import type { ClassNameProps } from '../../../styles/classnames/interfaces';
2
+ import type { ImageInputClassNames } from '../ImageInput';
3
+ interface ImageInputPanelProps extends ClassNameProps<ImageInputClassNames> {
4
+ error?: string | null;
5
+ isLoading?: boolean;
6
+ value?: string | null;
7
+ }
8
+ declare function ImageInputPanel({ classNameProps, classNames, error, isLoading, value, }: ImageInputPanelProps): React.ReactElement;
9
+ export default ImageInputPanel;
@@ -0,0 +1,17 @@
1
+ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
+ import { useClassNames, useIcon } from '../../../styles/context';
3
+ function ImageInputPanel({ classNameProps, classNames, error, isLoading, value, }) {
4
+ const ErrorIcon = useIcon('imageInput.error');
5
+ const UploadIcon = useIcon('imageInput.upload');
6
+ const componentClassNames = useClassNames('imageInput', {
7
+ props: classNameProps,
8
+ }, classNames);
9
+ if (value) {
10
+ return _jsx("img", { src: value, className: componentClassNames?.image });
11
+ }
12
+ if (error) {
13
+ return (_jsxs("div", { children: [_jsx("i", { className: componentClassNames?.icon, children: ErrorIcon ? _jsx(ErrorIcon, {}) : null }), _jsx("p", { className: componentClassNames?.title, children: error }), _jsx("p", { className: componentClassNames?.description, children: "Please try again" })] }));
14
+ }
15
+ return (_jsxs("div", { children: [_jsx("i", { className: componentClassNames?.icon, children: UploadIcon ? _jsx(UploadIcon, {}) : null }), _jsx("p", { className: componentClassNames?.title, children: "Click to Upload" }), _jsx("p", { className: componentClassNames?.description, children: "(or drag an image over this box)" })] }));
16
+ }
17
+ export default ImageInputPanel;
@@ -14,6 +14,6 @@ function Switch({ classNameProps, classNames, details, id, isDisabled, label, na
14
14
  onChange(inputEvent);
15
15
  }
16
16
  }
17
- return (_jsxs(Fragment, { children: [_jsx("input", { name: name, type: "hidden", value: "false" }), _jsxs("div", { className: tw('block', componentClassNames?.root), children: [_jsxs("div", { className: tw('relative', componentClassNames?.control), children: [_jsx("input", { "aria-checked": Boolean(value), checked: Boolean(value), className: tw('appearance-none', componentClassNames?.input), disabled: isDisabled, id: id || name, name: name, onChange: handleChange, type: "checkbox", value: "true" }), _jsx("i", { className: componentClassNames?.icon })] }), label ? (_jsxs("label", { htmlFor: id || name, children: [_jsx("span", { className: tw('', componentClassNames?.label), children: label }), details ? (_jsx("small", { className: tw('', componentClassNames?.details), children: details })) : null] })) : (_jsx(Assistive, { children: "Yes?" }))] })] }));
17
+ return (_jsxs(Fragment, { children: [_jsx("input", { name: name, type: "hidden", value: "false" }), _jsxs("div", { className: tw('block', componentClassNames?.root), children: [_jsxs("div", { className: tw('relative', componentClassNames?.control, isDisabled ? 'pointer-events-none opacity-30' : ''), children: [_jsx("input", { "aria-checked": Boolean(value), checked: Boolean(value), className: tw('appearance-none', componentClassNames?.input), disabled: isDisabled, id: id || name, name: name, onChange: handleChange, type: "checkbox", value: "true" }), _jsx("i", { className: componentClassNames?.icon })] }), label ? (_jsxs("label", { htmlFor: id || name, children: [_jsx("span", { className: tw('', componentClassNames?.label), children: label }), details ? (_jsx("small", { className: tw('', componentClassNames?.details), children: details })) : null] })) : (_jsx(Assistive, { children: "Yes?" }))] })] }));
18
18
  }
19
19
  export default Switch;
@@ -1,12 +1,6 @@
1
- import type { Default, InputEvent } from '../../forms/interfaces';
1
+ import type { Default, FormResponse, InputEvent } from '../../forms/interfaces';
2
2
  import type { UseSuccessArgs } from '../../hooks/useSuccess';
3
3
  import type { FormProps } from '../components/Form';
4
- interface AwaitedFormResponse<M> {
5
- data: M | null;
6
- error: string | null;
7
- validation: Record<string, string> | null;
8
- }
9
- export type FormResponse<M> = Promise<AwaitedFormResponse<M>>;
10
4
  export interface FieldPropsReturn<Request, K extends keyof Request = keyof Request> {
11
5
  error: Record<string, string> | null;
12
6
  isDisabled?: boolean;
@@ -1,5 +1,11 @@
1
1
  import type { EditableFormFieldProps } from './components/EditableFormField';
2
2
  import type { FormFieldProps } from './components/FormField';
3
+ export interface AwaitedFormResponse<M> {
4
+ data: M | null;
5
+ error: string | null;
6
+ validation: Record<string, string> | null;
7
+ }
8
+ export type FormResponse<M> = Promise<AwaitedFormResponse<M>>;
3
9
  export type Default<T> = {
4
10
  [K in keyof T]?: T[K] | null;
5
11
  };
@@ -28,6 +34,7 @@ export interface InputProps<V, T extends V = V> {
28
34
  name: string;
29
35
  ref?: React.Ref<HTMLInputElement>;
30
36
  onChange?: (event: InputEvent<T>) => void;
37
+ onError?: (error: string) => void;
31
38
  onKeyDown?: React.KeyboardEventHandler;
32
39
  value?: V;
33
40
  }
@@ -0,0 +1,2 @@
1
+ export declare function preprocessImage(file: File, maxWidth?: number): Promise<File>;
2
+ export default preprocessImage;
@@ -0,0 +1,50 @@
1
+ const ALLOWED_TYPES = [
2
+ 'image/jpeg',
3
+ 'image/png',
4
+ 'image/gif',
5
+ 'image/svg+xml',
6
+ 'image/webp',
7
+ ];
8
+ function isSupportedImage(file) {
9
+ return ALLOWED_TYPES.includes(file.type);
10
+ }
11
+ export async function preprocessImage(file, maxWidth = 1200) {
12
+ if (!isSupportedImage(file)) {
13
+ throw new Error('Unsupported file type');
14
+ }
15
+ if (file.type === 'image/svg+xml') {
16
+ return file;
17
+ }
18
+ const bitmap = await createImageBitmap(file);
19
+ let { width, height } = bitmap;
20
+ if (width > maxWidth) {
21
+ const scale = maxWidth / width;
22
+ width = maxWidth;
23
+ height = Math.round(height * scale);
24
+ }
25
+ const canvas = document.createElement('canvas');
26
+ canvas.width = width;
27
+ canvas.height = height;
28
+ const ctx = canvas.getContext('2d');
29
+ if (!ctx) {
30
+ throw new Error('Could not get canvas context');
31
+ }
32
+ ctx.drawImage(bitmap, 0, 0, width, height);
33
+ const blob = await new Promise((resolve, reject) => {
34
+ canvas.toBlob((data) => {
35
+ if (data) {
36
+ resolve(data);
37
+ }
38
+ else {
39
+ reject(new Error('Failed to create WebP blob'));
40
+ }
41
+ }, 'image/webp', 1);
42
+ });
43
+ const baseName = file.name.replace(/\.\w+$/, '');
44
+ const newName = `${baseName}.webp`;
45
+ return new File([blob], newName, {
46
+ type: 'image/webp',
47
+ lastModified: Date.now(),
48
+ });
49
+ }
50
+ export default preprocessImage;
@@ -34,6 +34,7 @@ import type { EditableFormClassNames } from '../../forms/components/EditableForm
34
34
  import type { EditableFormFieldClassNames } from '../../forms/components/EditableFormField';
35
35
  import type { FormClassNames } from '../../forms/components/Form';
36
36
  import type { FormFieldClassNames } from '../../forms/components/FormField';
37
+ import type { ImageInputClassNames } from '../../forms/components/ImageInput';
37
38
  import type { PasswordComplexityClassNames } from '../../forms/components/PasswordComplexity';
38
39
  import type { PasswordInputClassNames } from '../../forms/components/PasswordInput';
39
40
  import type { StaticTextInputClassNames } from '../../forms/components/StaticTextInput';
@@ -66,6 +67,7 @@ export interface UIClassNames {
66
67
  filterSearch?: ComponentClassNames<SearchFilterClassNames>;
67
68
  form?: ComponentClassNames<FormClassNames>;
68
69
  formField?: ComponentClassNames<FormFieldClassNames>;
70
+ imageInput?: ComponentClassNames<ImageInputClassNames>;
69
71
  infoPanel?: ComponentClassNames<InfoPanelClassNames>;
70
72
  link?: ComponentClassNames<LinkClassNames>;
71
73
  list?: ComponentClassNames<ListClassNames>;
@@ -12,6 +12,12 @@ export interface UIIcons {
12
12
  'filter.panel'?: IconComponent;
13
13
  'filter.search'?: IconComponent;
14
14
  'formField.error'?: IconComponent;
15
+ 'imageInput.error'?: IconComponent;
16
+ 'imageInput.upload'?: IconComponent;
17
+ 'infoPanel.error'?: IconComponent;
18
+ 'infoPanel.info'?: IconComponent;
19
+ 'infoPanel.success'?: IconComponent;
20
+ 'infoPanel.warning'?: IconComponent;
15
21
  'passwordComplexity.invalid'?: IconComponent;
16
22
  'passwordComplexity.valid'?: IconComponent;
17
23
  'passwordInput.hidden'?: IconComponent;
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@sqrzro/ui",
3
3
  "type": "module",
4
- "version": "4.0.0-alpha.61",
4
+ "version": "4.0.0-alpha.63",
5
5
  "main": "dist/index.js",
6
6
  "types": "dist/index.d.ts",
7
7
  "license": "ISC",