@superdispatch/ui-lab 0.50.5 → 0.50.7

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.
Files changed (124) hide show
  1. package/LICENSE +21 -0
  2. package/{pkg/dist-node → dist-node}/index.js +31 -23
  3. package/dist-node/index.js.map +1 -0
  4. package/{pkg/dist-src → dist-src}/navbar/Navbar.js +18 -3
  5. package/{pkg/dist-src → dist-src}/navbar/NavbarAccordion.js +9 -19
  6. package/{pkg/dist-src → dist-src}/navbar/NavbarContext.js +3 -1
  7. package/{pkg/dist-src → dist-src}/navbar/NavbarList.js +2 -1
  8. package/{pkg/dist-types → dist-types}/index.d.ts +8 -6
  9. package/{pkg/dist-web → dist-web}/index.js +31 -23
  10. package/dist-web/index.js.map +1 -0
  11. package/package.json +14 -39
  12. package/.babelrc.js +0 -5
  13. package/pkg/README.md +0 -10
  14. package/pkg/dist-node/index.js.map +0 -1
  15. package/pkg/dist-web/index.js.map +0 -1
  16. package/pkg/package.json +0 -41
  17. package/playroom.ts +0 -24
  18. package/src/alert/Alert.stories.tsx +0 -162
  19. package/src/alert/Alert.tsx +0 -135
  20. package/src/banner/Banner.stories.tsx +0 -64
  21. package/src/banner/Banner.tsx +0 -120
  22. package/src/box/Box.stories.tsx +0 -20
  23. package/src/box/Box.tsx +0 -257
  24. package/src/button/Button.stories.tsx +0 -739
  25. package/src/button/Button.tsx +0 -498
  26. package/src/button-area/ButtonArea.stories.tsx +0 -65
  27. package/src/button-area/ButtonArea.tsx +0 -90
  28. package/src/chat/Chat.stories.tsx +0 -130
  29. package/src/chat/Chat.tsx +0 -57
  30. package/src/chat/ChatMessage.tsx +0 -45
  31. package/src/chat/README.MD +0 -7
  32. package/src/chat/__tests__/Chat.spec.tsx +0 -29
  33. package/src/chat/__tests__/ChatMessage.spec.tsx +0 -39
  34. package/src/container/Container.tsx +0 -48
  35. package/src/description-item/DescriptionItem.stories.tsx +0 -163
  36. package/src/description-item/DescriptionItem.tsx +0 -104
  37. package/src/description-line-item/DescriptionLineItem.stories.tsx +0 -23
  38. package/src/description-line-item/DescriptionLineItem.tsx +0 -29
  39. package/src/email-autocomplate/CloseIcon.tsx +0 -20
  40. package/src/email-autocomplate/EmailAutocomplete.stories.tsx +0 -47
  41. package/src/email-autocomplate/EmailAutocomplete.tsx +0 -138
  42. package/src/file-drop-zone/FileDropZone.stories.tsx +0 -44
  43. package/src/file-drop-zone/FileDropZone.tsx +0 -170
  44. package/src/file-list-item/FileListItem.stories.tsx +0 -37
  45. package/src/file-list-item/FileListItem.tsx +0 -148
  46. package/src/file-list-item/__tests__/FileListItem.spec.tsx +0 -339
  47. package/src/flag-list/FlagList.stories.tsx +0 -72
  48. package/src/flag-list/FlagList.tsx +0 -54
  49. package/src/flag-list/FlagListItem.tsx +0 -126
  50. package/src/index.spec.ts +0 -53
  51. package/src/index.ts +0 -36
  52. package/src/linked-text/LinkeText.stories.tsx +0 -42
  53. package/src/linked-text/LinkedText.tsx +0 -47
  54. package/src/multiline-text/MultilineText.stories.tsx +0 -30
  55. package/src/multiline-text/MultilineText.ts +0 -16
  56. package/src/navbar/Navbar.stories.tsx +0 -137
  57. package/src/navbar/Navbar.tsx +0 -132
  58. package/src/navbar/NavbarAccordion.tsx +0 -195
  59. package/src/navbar/NavbarAvatar.tsx +0 -51
  60. package/src/navbar/NavbarBottomBar.tsx +0 -135
  61. package/src/navbar/NavbarContext.tsx +0 -22
  62. package/src/navbar/NavbarItem.tsx +0 -125
  63. package/src/navbar/NavbarList.tsx +0 -247
  64. package/src/navbar/NavbarMenu.tsx +0 -102
  65. package/src/passwordStepper/PasswordStrength.stories.tsx +0 -95
  66. package/src/passwordStepper/PasswordStrength.tsx +0 -107
  67. package/src/passwordStepper/PasswordUtils.tsx +0 -42
  68. package/src/passwordStepper/PasswordValidationComponents.tsx +0 -95
  69. package/src/sidebar/Sidebar.stories.tsx +0 -376
  70. package/src/sidebar/Sidebar.tsx +0 -75
  71. package/src/sidebar/SidebarBackButton.tsx +0 -33
  72. package/src/sidebar/SidebarContainer.tsx +0 -114
  73. package/src/sidebar/SidebarContent.tsx +0 -119
  74. package/src/sidebar/SidebarDivider.tsx +0 -15
  75. package/src/sidebar/SidebarMenuItem.tsx +0 -196
  76. package/src/sidebar/SidebarMenuItemAction.tsx +0 -27
  77. package/src/sidebar/SidebarMenuItemAvatar.tsx +0 -59
  78. package/src/sidebar/SidebarMenuItemContext.tsx +0 -33
  79. package/src/sidebar/SidebarSubheader.tsx +0 -38
  80. package/src/styled.d.ts +0 -12
  81. package/src/text-box/TextBox.stories.tsx +0 -114
  82. package/src/text-box/TextBox.tsx +0 -238
  83. package/src/utils/RuleNormalizer.ts +0 -24
  84. package/src/utils/mergeStyles.ts +0 -28
  85. package/tsconfig.json +0 -19
  86. /package/{pkg/dist-src → dist-src}/alert/Alert.js +0 -0
  87. /package/{pkg/dist-src → dist-src}/banner/Banner.js +0 -0
  88. /package/{pkg/dist-src → dist-src}/box/Box.js +0 -0
  89. /package/{pkg/dist-src → dist-src}/button/Button.js +0 -0
  90. /package/{pkg/dist-src → dist-src}/button-area/ButtonArea.js +0 -0
  91. /package/{pkg/dist-src → dist-src}/chat/Chat.js +0 -0
  92. /package/{pkg/dist-src → dist-src}/chat/ChatMessage.js +0 -0
  93. /package/{pkg/dist-src → dist-src}/container/Container.js +0 -0
  94. /package/{pkg/dist-src → dist-src}/description-item/DescriptionItem.js +0 -0
  95. /package/{pkg/dist-src → dist-src}/description-line-item/DescriptionLineItem.js +0 -0
  96. /package/{pkg/dist-src → dist-src}/email-autocomplate/CloseIcon.js +0 -0
  97. /package/{pkg/dist-src → dist-src}/email-autocomplate/EmailAutocomplete.js +0 -0
  98. /package/{pkg/dist-src → dist-src}/file-drop-zone/FileDropZone.js +0 -0
  99. /package/{pkg/dist-src → dist-src}/file-list-item/FileListItem.js +0 -0
  100. /package/{pkg/dist-src → dist-src}/flag-list/FlagList.js +0 -0
  101. /package/{pkg/dist-src → dist-src}/flag-list/FlagListItem.js +0 -0
  102. /package/{pkg/dist-src → dist-src}/index.js +0 -0
  103. /package/{pkg/dist-src → dist-src}/linked-text/LinkedText.js +0 -0
  104. /package/{pkg/dist-src → dist-src}/multiline-text/MultilineText.js +0 -0
  105. /package/{pkg/dist-src → dist-src}/navbar/NavbarAvatar.js +0 -0
  106. /package/{pkg/dist-src → dist-src}/navbar/NavbarBottomBar.js +0 -0
  107. /package/{pkg/dist-src → dist-src}/navbar/NavbarItem.js +0 -0
  108. /package/{pkg/dist-src → dist-src}/navbar/NavbarMenu.js +0 -0
  109. /package/{pkg/dist-src → dist-src}/passwordStepper/PasswordStrength.js +0 -0
  110. /package/{pkg/dist-src → dist-src}/passwordStepper/PasswordUtils.js +0 -0
  111. /package/{pkg/dist-src → dist-src}/passwordStepper/PasswordValidationComponents.js +0 -0
  112. /package/{pkg/dist-src → dist-src}/sidebar/Sidebar.js +0 -0
  113. /package/{pkg/dist-src → dist-src}/sidebar/SidebarBackButton.js +0 -0
  114. /package/{pkg/dist-src → dist-src}/sidebar/SidebarContainer.js +0 -0
  115. /package/{pkg/dist-src → dist-src}/sidebar/SidebarContent.js +0 -0
  116. /package/{pkg/dist-src → dist-src}/sidebar/SidebarDivider.js +0 -0
  117. /package/{pkg/dist-src → dist-src}/sidebar/SidebarMenuItem.js +0 -0
  118. /package/{pkg/dist-src → dist-src}/sidebar/SidebarMenuItemAction.js +0 -0
  119. /package/{pkg/dist-src → dist-src}/sidebar/SidebarMenuItemAvatar.js +0 -0
  120. /package/{pkg/dist-src → dist-src}/sidebar/SidebarMenuItemContext.js +0 -0
  121. /package/{pkg/dist-src → dist-src}/sidebar/SidebarSubheader.js +0 -0
  122. /package/{pkg/dist-src → dist-src}/text-box/TextBox.js +0 -0
  123. /package/{pkg/dist-src → dist-src}/utils/RuleNormalizer.js +0 -0
  124. /package/{pkg/dist-src → dist-src}/utils/mergeStyles.js +0 -0
@@ -1,20 +0,0 @@
1
- import { SvgIcon, SvgIconProps } from '@material-ui/core';
2
- import { Color } from '@superdispatch/ui';
3
- import { ReactElement } from 'react';
4
-
5
- export function CloseIcon(props: SvgIconProps): ReactElement {
6
- return (
7
- <SvgIcon
8
- {...props}
9
- viewBox="0 0 6 6"
10
- style={{ width: '6px', height: '6px' }}
11
- >
12
- <path
13
- fillRule="evenodd"
14
- clipRule="evenodd"
15
- d="M3.72278 2.99999L5.91671 0.806074L5.19396 0.0833282L3.00004 2.27725L0.80612 0.0833282L0.083374 0.806076L2.27729 2.99999L0.083374 5.19391L0.80612 5.91666L3.00004 3.72274L5.19396 5.91666L5.91671 5.19392L3.72278 2.99999Z"
16
- fill={Color.Dark100}
17
- />
18
- </SvgIcon>
19
- );
20
- }
@@ -1,47 +0,0 @@
1
- import { Meta } from '@storybook/react';
2
- import { UseState } from '@superdispatch/ui-docs';
3
- import { Box } from '@superdispatch/ui-lab';
4
- import { EmailAutocomplete } from './EmailAutocomplete';
5
-
6
- export default {
7
- title: 'Inputs/EmailAutocomplete',
8
- component: EmailAutocomplete,
9
- decorators: [
10
- (Story) => (
11
- <Box maxWidth="400px">
12
- <Story />
13
- </Box>
14
- ),
15
- ],
16
- } as Meta;
17
-
18
- const options = [
19
- 'test@example.com',
20
- 'user@example.com',
21
- 'admin@example.com',
22
- 'test@mail.com,test2@mail.com',
23
- ];
24
-
25
- export const basic = () => (
26
- <UseState initialState={[]}>
27
- {(state, setState) => (
28
- <EmailAutocomplete options={options} value={state} onChange={setState} />
29
- )}
30
- </UseState>
31
- );
32
-
33
- export const textarea = () => (
34
- <UseState initialState={[]}>
35
- {(state, setState) => (
36
- <EmailAutocomplete
37
- options={options}
38
- value={state}
39
- onChange={setState}
40
- TextFieldProps={{
41
- multiline: true,
42
- placeholder: 'Enter email addresses',
43
- }}
44
- />
45
- )}
46
- </UseState>
47
- );
@@ -1,138 +0,0 @@
1
- import {
2
- IconButton,
3
- TextField,
4
- TextFieldProps as TextFieldPropsType,
5
- } from '@material-ui/core';
6
- import {
7
- Autocomplete,
8
- AutocompleteChangeReason,
9
- AutocompleteProps,
10
- } from '@material-ui/lab';
11
- import { Inline, Tag } from '@superdispatch/ui';
12
- import { forwardRef } from 'react';
13
- import styled from 'styled-components';
14
- import { TextBox } from '../text-box/TextBox';
15
- import { CloseIcon } from './CloseIcon';
16
-
17
- const MultipleFieldText = styled(TextField)`
18
- /* Inline input next to tags rather than next line on focus */
19
- & {
20
- width: 100%;
21
- margin-top: 8px;
22
- overflow: hidden;
23
- }
24
-
25
- && .MuiAutocomplete-inputRoot {
26
- flex-direction: column;
27
- align-items: flex-start;
28
- width: 100%;
29
- gap: 8px;
30
-
31
- & .MuiAutocomplete-input {
32
- width: 100%;
33
- }
34
- }
35
- `;
36
-
37
- export interface EmailAutocompleteProps
38
- extends Omit<
39
- AutocompleteProps<string, true, true, true>,
40
- 'onChange' | 'renderInput' | 'renderTags'
41
- > {
42
- options: string[];
43
- TextFieldProps?: Omit<TextFieldPropsType, 'onChange' | 'onBlur'>;
44
-
45
- onAdd?: (value: string) => void;
46
- onRemove?: (value: string) => void;
47
- onChange?: (
48
- value: string[] | undefined,
49
- reason: AutocompleteChangeReason,
50
- ) => void;
51
- }
52
-
53
- export const EmailAutocomplete = forwardRef<
54
- HTMLDivElement,
55
- Omit<EmailAutocompleteProps, 'ref'>
56
- >(({ value, onChange, TextFieldProps, ...props }, ref) => {
57
- function handleDelete(index: number): void {
58
- const filteredOrders = value?.filter(
59
- (_item, fieldIndex) => fieldIndex !== index,
60
- );
61
- void onChange?.(filteredOrders, 'remove-option');
62
- }
63
-
64
- return (
65
- <Autocomplete
66
- {...props}
67
- ref={ref}
68
- multiple={true}
69
- freeSolo={true}
70
- value={value}
71
- disableClearable={true}
72
- filterSelectedOptions={true}
73
- filterOptions={(filterOptions) => {
74
- return filterOptions.filter((option) => option !== '');
75
- }}
76
- onChange={(_event, selectedValue, reason) => {
77
- const emails = selectedValue
78
- .flatMap((item) => item.split(','))
79
- .map((item) => item.trim());
80
-
81
- onChange?.(emails, reason);
82
- }}
83
- renderTags={(items) => {
84
- return (
85
- <Inline space="xxsmall">
86
- {items.map((option, index) => {
87
- return (
88
- <Tag key={index} color="grey" variant="subtle">
89
- <Inline space="xxsmall">
90
- <TextBox wrapOverflow={true} color="primary">
91
- {option}
92
- </TextBox>
93
-
94
- <IconButton
95
- size="small"
96
- onClick={() => {
97
- handleDelete(index);
98
- }}
99
- >
100
- <CloseIcon />
101
- </IconButton>
102
- </Inline>
103
- </Tag>
104
- );
105
- })}
106
- </Inline>
107
- );
108
- }}
109
- renderInput={(params) => (
110
- <MultipleFieldText
111
- {...params}
112
- {...TextFieldProps}
113
- InputProps={{
114
- ...TextFieldProps?.InputProps,
115
- ...params.InputProps,
116
- startAdornment: params.InputProps.startAdornment,
117
- }}
118
- onChange={(event) => {
119
- const text = event.target.value.replace(/,/g, '');
120
- const hasCommaOrSpace = /,|\s/.test(event.target.value);
121
-
122
- if (hasCommaOrSpace && text.trim() !== '') {
123
- onChange?.([...(value || []), text.trim()], 'select-option');
124
- }
125
- }}
126
- onBlur={(event) => {
127
- const text = event.target.value;
128
- if (text.trim() !== '') {
129
- onChange?.([...(value || []), text.trim()], 'select-option');
130
- }
131
- }}
132
- />
133
- )}
134
- />
135
- );
136
- });
137
-
138
- EmailAutocomplete.displayName = 'EmailAutocomplete';
@@ -1,44 +0,0 @@
1
- import { Meta } from '@storybook/react';
2
- import { FileDropZone, toBytes } from './FileDropZone';
3
-
4
- export default { title: 'Lab/FileDropZone', component: FileDropZone } as Meta;
5
-
6
- export const basic = () => (
7
- <FileDropZone
8
- onDropAccepted={(files) => {
9
- alert(`Accepted files: ${files.map((file) => file.name).join(', ')}`);
10
- }}
11
- />
12
- );
13
-
14
- export const accept = () => (
15
- <FileDropZone
16
- accept={['.jpeg', '.jpg', '.png', '.gif']}
17
- hintText="or Drag & Drop .jpeg .jpg .png .gif files"
18
- onDropAccepted={(files) => {
19
- alert(`Accepted files: ${files.map((file) => file.name).join(', ')}`);
20
- }}
21
- />
22
- );
23
-
24
- export const maxSize = () => (
25
- <FileDropZone
26
- maxSize={toBytes(20, 'mb')}
27
- hintText="or Drag & Drop files less than 20 MB"
28
- onDropAccepted={(files) => {
29
- alert(`Accepted files: ${files.map((file) => file.name).join(', ')}`);
30
- }}
31
- />
32
- );
33
-
34
- export const maxFiles = () => (
35
- <FileDropZone
36
- maxFiles={1}
37
- hintText="or Drag & Drop file"
38
- onDropAccepted={(files) => {
39
- alert(`Accepted files: ${files.map((file) => file.name).join(', ')}`);
40
- }}
41
- >
42
- Upload attachment
43
- </FileDropZone>
44
- );
@@ -1,170 +0,0 @@
1
- import { CircularProgress, SvgIcon } from '@material-ui/core';
2
- import { Error } from '@material-ui/icons';
3
- import { mdiUpload } from '@mdi/js';
4
- import { CardButton, ColorDynamic, Column, Columns } from '@superdispatch/ui';
5
- import { forwardRef, ReactElement, ReactNode, Suspense } from 'react';
6
- import Dropzone, { FileRejection } from 'react-dropzone';
7
- import styled from 'styled-components';
8
-
9
- export function toBytes(input: number, unit: 'kb' | 'mb' | 'gb'): number {
10
- if (unit === 'gb') return input * (1 << 30);
11
- if (unit === 'mb') return input * (1 << 20);
12
- return input * (1 << 10);
13
- }
14
-
15
- const KILOBYTE = 1024;
16
- const BYTE_UNITS = [
17
- 'B',
18
- 'KB',
19
- 'MB',
20
- 'GB',
21
- 'TB',
22
- 'PB',
23
- 'EB',
24
- 'ZB',
25
- 'YB',
26
- ] as const;
27
- export function formatBytes(bytes: number): string {
28
- if (bytes === 0) return '0 Bytes';
29
- const unitIndex = Math.floor(Math.log(bytes) / Math.log(KILOBYTE));
30
- const unit = BYTE_UNITS[unitIndex] as string;
31
- return `${(bytes / Math.pow(KILOBYTE, unitIndex)).toFixed(2)} ${unit}`;
32
- }
33
-
34
- const StyledCardButton = styled(CardButton)<{
35
- status?: 'idle' | 'active' | 'error';
36
- }>(({ status }) => ({
37
- backgroundColor: status === 'active' ? ColorDynamic.Blue50 : undefined,
38
- }));
39
-
40
- interface UploadRejectionProps {
41
- maxSize?: number;
42
- maxFiles?: number;
43
- accept?: string | string[];
44
- rejection: FileRejection;
45
- }
46
-
47
- function UploadRejection({
48
- maxSize,
49
- rejection,
50
- }: UploadRejectionProps): null | ReactElement {
51
- const [error] = rejection.errors;
52
- if (!error) return null;
53
-
54
- return (
55
- <Columns align="center">
56
- <Column width="content">
57
- <Error />
58
- </Column>
59
-
60
- <Column>
61
- {error.code === 'file-too-large'
62
- ? maxSize == null
63
- ? 'Attachment size is too large'
64
- : `Attachment size should be less than ${formatBytes(maxSize)}`
65
- : error.message}
66
- </Column>
67
- </Columns>
68
- );
69
- }
70
-
71
- export interface FileDropZoneProps {
72
- children?: ReactNode;
73
- startIcon?: ReactNode;
74
- hintText?: ReactNode;
75
- fallback?: ReactNode;
76
- disabled?: boolean;
77
-
78
- maxSize?: number;
79
- maxFiles?: number;
80
- accept?: string | string[];
81
- onDropAccepted?: (files: File[]) => void;
82
- onDropRejected?: (fileRejections: FileRejection[]) => void;
83
- }
84
-
85
- export const FileDropZone = forwardRef<HTMLButtonElement, FileDropZoneProps>(
86
- (props, ref) => {
87
- const {
88
- // CardButton
89
- disabled = false,
90
- children = 'Upload Attachments',
91
- hintText = 'or Drag & Drop files',
92
- startIcon = (
93
- <SvgIcon>
94
- <path d={mdiUpload} />
95
- </SvgIcon>
96
- ),
97
- fallback = (
98
- <CardButton
99
- ref={ref}
100
- disabled={true}
101
- startIcon={<CircularProgress size="1em" color="inherit" />}
102
- >
103
- Loading dependencies…
104
- </CardButton>
105
- ),
106
-
107
- // Dropzone
108
- accept,
109
- maxSize = Infinity,
110
- maxFiles = Infinity,
111
- onDropRejected,
112
- onDropAccepted,
113
- ...dropzoneProps
114
- } = props;
115
-
116
- return (
117
- <Suspense fallback={fallback}>
118
- <Dropzone
119
- {...dropzoneProps}
120
- accept={accept}
121
- maxSize={maxSize}
122
- maxFiles={maxFiles}
123
- disabled={disabled}
124
- onDropAccepted={(files) => {
125
- onDropAccepted?.(files);
126
- }}
127
- onDropRejected={(fileRejections) => {
128
- onDropRejected?.(fileRejections);
129
- }}
130
- >
131
- {({
132
- isDragActive,
133
- isDragReject,
134
- getRootProps,
135
- getInputProps,
136
- fileRejections: [fileRejection],
137
- }) => {
138
- return (
139
- <>
140
- <input {...getInputProps()} />
141
-
142
- <StyledCardButton
143
- {...getRootProps()}
144
- ref={ref}
145
- hint={hintText}
146
- disabled={disabled}
147
- startIcon={startIcon}
148
- status={
149
- isDragActive ? 'active' : isDragReject ? 'error' : 'idle'
150
- }
151
- error={
152
- !!fileRejection && (
153
- <UploadRejection
154
- accept={accept}
155
- maxSize={maxSize}
156
- rejection={fileRejection}
157
- />
158
- )
159
- }
160
- >
161
- {children}
162
- </StyledCardButton>
163
- </>
164
- );
165
- }}
166
- </Dropzone>
167
- </Suspense>
168
- );
169
- },
170
- );
@@ -1,37 +0,0 @@
1
- import { Meta } from '@storybook/react';
2
- import { Stack } from '@superdispatch/ui';
3
- import { Box } from '../box/Box';
4
- import { FileListItem } from './FileListItem';
5
-
6
- export default {
7
- title: 'Lab/FileListItem',
8
- component: FileListItem,
9
- decorators: [
10
- (Story) => (
11
- <Box maxWidth="200px">
12
- <Story />
13
- </Box>
14
- ),
15
- ],
16
- } as Meta;
17
-
18
- export const basic = () => (
19
- <Stack>
20
- <FileListItem name="Read this document.txt" />
21
- <FileListItem name="TST1208 Dispatcher Info.pdf" />
22
- <FileListItem name="TST1208 Dispatcher Info.pdf" canDelete={false} />
23
- <FileListItem
24
- name="attachment.jpg"
25
- url="https://picsum.photos/seed/picsum/1024/768"
26
- />
27
- </Stack>
28
- );
29
-
30
- export const status = () => (
31
- <Stack>
32
- <FileListItem name="TST1208 Dispatcher Info.pdf" status="idle" />
33
- <FileListItem name="TST1208 Dispatcher Info.pdf" status="loading" />
34
- <FileListItem name="TST1208 Dispatcher Info.pdf" status="success" />
35
- <FileListItem name="TST1208 Dispatcher Info.pdf" status="error" />
36
- </Stack>
37
- );
@@ -1,148 +0,0 @@
1
- import {
2
- CircularProgress,
3
- IconButton,
4
- Link,
5
- SvgIcon,
6
- SvgIconProps,
7
- Tooltip,
8
- } from '@material-ui/core';
9
- import { CheckCircle, Delete, Error, Image, Refresh } from '@material-ui/icons';
10
- import { mdiFilePdfBox, mdiTextBox } from '@mdi/js';
11
- import {
12
- ColorDynamic,
13
- Column,
14
- Columns,
15
- useResponsiveValue,
16
- useUID,
17
- } from '@superdispatch/ui';
18
- import { forwardRef, memo, ReactNode, useState } from 'react';
19
- import styled from 'styled-components';
20
-
21
- const FileListItemName = styled.div`
22
- overflow: hidden;
23
- line-height: 22px;
24
- white-space: nowrap;
25
- text-overflow: ellipsis;
26
- `;
27
-
28
- const FileListItemProgress = styled(CircularProgress)`
29
- font-size: 24px;
30
-
31
- ${({ theme }) => theme.breakpoints.up('sm')} {
32
- font-size: 14px;
33
- }
34
- `;
35
-
36
- const PdfIcon = memo((props: SvgIconProps) => (
37
- <SvgIcon {...props}>
38
- <path d={mdiFilePdfBox} />
39
- </SvgIcon>
40
- ));
41
- const TextBoxIcon = memo((props: SvgIconProps) => (
42
- <SvgIcon {...props}>
43
- <path d={mdiTextBox} />
44
- </SvgIcon>
45
- ));
46
-
47
- type FileType = 'pdf' | 'image' | 'unknown';
48
- const PDF_FILE_PATTERN = /\.pdf$/;
49
- const IMAGE_FILE_PATTERN = /\.(gif|png|jpg|jpeg)$/;
50
-
51
- function getFileType(name: string): FileType {
52
- if (PDF_FILE_PATTERN.exec(name)) return 'pdf';
53
- if (IMAGE_FILE_PATTERN.exec(name)) return 'image';
54
- return 'unknown';
55
- }
56
-
57
- export interface FileListItemProps {
58
- url?: string;
59
- name: string;
60
- onRetry?: () => void;
61
- onDelete?: () => void;
62
- canDelete?: boolean;
63
- helperText?: ReactNode;
64
- status?: 'idle' | 'loading' | 'error' | 'success';
65
- }
66
- export const FileListItem = forwardRef<HTMLDivElement, FileListItemProps>(
67
- ({ url, name, status, canDelete = true, onRetry, onDelete }, ref) => {
68
- const uid = useUID();
69
- const fileType = getFileType(name);
70
- const [isHoveredState, setIsHovered] = useState(false);
71
- const isHovered = useResponsiveValue(true, isHoveredState);
72
-
73
- return (
74
- <div
75
- ref={ref}
76
- onMouseOver={() => {
77
- setIsHovered(true);
78
- }}
79
- onMouseLeave={() => {
80
- setIsHovered(false);
81
- }}
82
- aria-label="file-list-item"
83
- >
84
- <Columns align="center" space="xsmall">
85
- <Column width="content">
86
- {status === 'error' ? (
87
- <Error color="error" fontSize="small" />
88
- ) : fileType === 'pdf' ? (
89
- <PdfIcon color="action" fontSize="small" />
90
- ) : fileType === 'image' ? (
91
- <Image color="action" fontSize="small" />
92
- ) : (
93
- <TextBoxIcon color="action" fontSize="small" />
94
- )}
95
- </Column>
96
-
97
- <Column width="fluid">
98
- <FileListItemName id={uid}>
99
- {!url ? (
100
- name
101
- ) : (
102
- <Link
103
- href={url}
104
- noWrap={true}
105
- target="_blank"
106
- rel="noopener noreferrer"
107
- >
108
- {name}
109
- </Link>
110
- )}
111
- </FileListItemName>
112
- </Column>
113
-
114
- {status === 'error' && (
115
- <Column width="content">
116
- <Tooltip title="Retry">
117
- <IconButton size="small" onClick={onRetry}>
118
- <Refresh fontSize="small" />
119
- </IconButton>
120
- </Tooltip>
121
- </Column>
122
- )}
123
-
124
- <Column width="content">
125
- {status === 'loading' ? (
126
- <IconButton size="small" disabled={true}>
127
- <FileListItemProgress size="1em" />
128
- </IconButton>
129
- ) : !isHovered && status === 'success' ? (
130
- <IconButton size="small">
131
- <CheckCircle
132
- fontSize="small"
133
- htmlColor={ColorDynamic.Green300}
134
- />
135
- </IconButton>
136
- ) : canDelete ? (
137
- <Tooltip title="Delete">
138
- <IconButton size="small" onClick={onDelete}>
139
- <Delete fontSize="small" />
140
- </IconButton>
141
- </Tooltip>
142
- ) : null}
143
- </Column>
144
- </Columns>
145
- </div>
146
- );
147
- },
148
- );