diff --git a/packages/react/package.json b/packages/react/package.json
index 78f118107f..3ee570cce9 100644
--- a/packages/react/package.json
+++ b/packages/react/package.json
@@ -31,6 +31,7 @@
"dependencies": {
"@mui/base": "5.0.0-beta.60",
"@tanstack/react-table": "^8.10.7",
+ "clsx": "^2.1.1",
"date-fns": "^4.0.0",
"feather-icons": "^4.29.0",
"prop-types": "^15.8.1",
diff --git a/packages/react/src/components/attached-file/attached-file-classes.ts b/packages/react/src/components/attached-file/attached-file-classes.ts
new file mode 100644
index 0000000000..ca4532d3de
--- /dev/null
+++ b/packages/react/src/components/attached-file/attached-file-classes.ts
@@ -0,0 +1,20 @@
+import { generateComponentClasses } from '../../utils/generate-component-classes';
+
+const COMPONENT_NAME = 'AttachedFile';
+
+export interface AttachedFileClasses {
+ root: string;
+ icon: string;
+ status: string;
+}
+
+export type AttachedFileClassKeys = keyof AttachedFileClasses;
+
+export const attachedFileClasses: AttachedFileClasses = generateComponentClasses(
+ COMPONENT_NAME,
+ [
+ 'root',
+ 'icon',
+ 'status',
+ ],
+);
diff --git a/packages/react/src/components/attached-file/attached-file.test.tsx b/packages/react/src/components/attached-file/attached-file.test.tsx
new file mode 100644
index 0000000000..3c77e63b7f
--- /dev/null
+++ b/packages/react/src/components/attached-file/attached-file.test.tsx
@@ -0,0 +1,54 @@
+import { renderWithProviders } from '../../test-utils/renderer';
+import { AttachedFile } from './attached-file';
+
+describe('AttachedFile', () => {
+ test('Matches Default Snapshot', () => {
+ const tree = renderWithProviders();
+
+ expect(tree).toMatchSnapshot();
+ });
+
+ test('Matches Uploading Snapshot', () => {
+ const tree = renderWithProviders();
+
+ expect(tree).toMatchSnapshot();
+ });
+
+ test('Matches Success Snapshot', () => {
+ const tree = renderWithProviders();
+
+ expect(tree).toMatchSnapshot();
+ });
+
+ test('Matches Cancelled Snapshot', () => {
+ const tree = renderWithProviders();
+
+ expect(tree).toMatchSnapshot();
+ });
+
+ test('Matches Error Snapshot', () => {
+ const tree = renderWithProviders();
+
+ expect(tree).toMatchSnapshot();
+ });
+});
diff --git a/packages/react/src/components/attached-file/attached-file.test.tsx.snap b/packages/react/src/components/attached-file/attached-file.test.tsx.snap
new file mode 100644
index 0000000000..ea70b4c9e5
--- /dev/null
+++ b/packages/react/src/components/attached-file/attached-file.test.tsx.snap
@@ -0,0 +1,1035 @@
+// Jest Snapshot v1, https://goo.gl/fbAQLP
+
+exports[`AttachedFile Matches Cancelled Snapshot 1`] = `
+.c4 {
+ -webkit-align-items: center;
+ -webkit-box-align: center;
+ -ms-flex-align: center;
+ align-items: center;
+ -webkit-appearance: none;
+ -moz-appearance: none;
+ appearance: none;
+ background: inherit;
+ border: 1px solid;
+ border-radius: 1.5rem;
+ box-sizing: border-box;
+ color: inherit;
+ display: -webkit-inline-box;
+ display: -webkit-inline-flex;
+ display: -ms-inline-flexbox;
+ display: inline-flex;
+ font-family: inherit;
+ font-size: 0.75rem;
+ font-weight: var(--font-bold);
+ -webkit-box-pack: center;
+ -webkit-justify-content: center;
+ -ms-flex-pack: center;
+ justify-content: center;
+ -webkit-letter-spacing: 0.025rem;
+ -moz-letter-spacing: 0.025rem;
+ -ms-letter-spacing: 0.025rem;
+ letter-spacing: 0.025rem;
+ line-height: 1rem;
+ min-height: var(--size-1halfx);
+ min-width: 2rem;
+ outline: none;
+ padding: 0 var(--spacing-1halfx);
+ text-transform: uppercase;
+ -webkit-user-select: none;
+ -moz-user-select: none;
+ -ms-user-select: none;
+ user-select: none;
+}
+
+.c4 {
+ outline: 2px solid transparent;
+ outline-offset: -2px;
+}
+
+.c4:focus {
+ box-shadow: 0 0 0 2px #006296;
+ outline: 2px solid #84C6EA;
+ outline-offset: -2px;
+}
+
+.c4 > svg {
+ height: var(--size-1x);
+ width: var(--size-1x);
+}
+
+.c5 {
+ background-color: transparent;
+ border-color: transparent;
+ color: #60666E;
+}
+
+.c5 {
+ outline: 2px solid transparent;
+ outline-offset: -2px;
+}
+
+.c5:focus {
+ box-shadow: 0 0 0 2px #006296;
+ outline: 2px solid #84C6EA;
+ outline-offset: -2px;
+}
+
+.c5:hover,
+.c5[aria-expanded='true'] {
+ background-color: rgb(0 0 0 / 0.15);
+ border-color: transparent;
+ color: #000000;
+}
+
+.c5[aria-disabled='true'] {
+ background-color: transparent;
+ border-color: transparent;
+ color: #B7BBC2;
+ cursor: not-allowed;
+}
+
+.c6 {
+ background-color: transparent;
+ border-color: transparent;
+ color: #60666E;
+ min-width: var(--size-1halfx);
+ padding: 0;
+ width: var(--size-1halfx);
+}
+
+.c6 {
+ outline: 2px solid transparent;
+ outline-offset: -2px;
+}
+
+.c6:focus {
+ box-shadow: 0 0 0 2px #006296;
+ outline: 2px solid #84C6EA;
+ outline-offset: -2px;
+}
+
+.c6:hover,
+.c6[aria-expanded='true'] {
+ background-color: rgb(0 0 0 / 0.15);
+ border-color: transparent;
+ color: #000000;
+}
+
+.c6[aria-disabled='true'] {
+ background-color: transparent;
+ border-color: transparent;
+ color: #B7BBC2;
+ cursor: not-allowed;
+}
+
+.c6 > svg {
+ height: var(--size-1x);
+ width: var(--size-1x);
+}
+
+.c0 {
+ -webkit-align-items: flex-start;
+ -webkit-box-align: flex-start;
+ -ms-flex-align: flex-start;
+ align-items: flex-start;
+ background-color: #FFFFFF;
+ border: 1px solid #DBDEE1;
+ border-radius: var(--border-radius);
+ display: -webkit-box;
+ display: -webkit-flex;
+ display: -ms-flexbox;
+ display: flex;
+ gap: var(--spacing-1x);
+ padding: var(--spacing-1x) var(--spacing-2x) var(--spacing-1x) var(--spacing-1halfx);
+}
+
+.c0 .eds-AttachedFile-icon {
+ height: var(--size-1x);
+ margin-top: var(--spacing-half);
+ width: var(--size-1x);
+}
+
+.c0 .eds-AttachedFile-status {
+ color: #60666E;
+}
+
+.c1 {
+ color: #1B1C1E;
+ display: -webkit-box;
+ display: -webkit-flex;
+ display: -ms-flexbox;
+ display: flex;
+ -webkit-flex-direction: column;
+ -ms-flex-direction: column;
+ flex-direction: column;
+ font-size: 0.75rem;
+ gap: var(--spacing-half);
+ line-height: 1rem;
+ margin-top: var(--spacing-half);
+}
+
+.c2 {
+ display: block;
+}
+
+.c3 {
+ -webkit-align-items: center;
+ -webkit-box-align: center;
+ -ms-flex-align: center;
+ align-items: center;
+ display: -webkit-box;
+ display: -webkit-flex;
+ display: -ms-flexbox;
+ display: flex;
+ -webkit-flex-shrink: 0;
+ -ms-flex-negative: 0;
+ flex-shrink: 0;
+ gap: var(--spacing-1x);
+ margin-left: auto;
+}
+
+
+
+
+
+ File.txt (97.7 KB)
+
+
+ Uploading cancelled
+
+
+
+
+
+
+
+`;
+
+exports[`AttachedFile Matches Default Snapshot 1`] = `
+.c4 {
+ -webkit-align-items: center;
+ -webkit-box-align: center;
+ -ms-flex-align: center;
+ align-items: center;
+ -webkit-appearance: none;
+ -moz-appearance: none;
+ appearance: none;
+ background: inherit;
+ border: 1px solid;
+ border-radius: 1.5rem;
+ box-sizing: border-box;
+ color: inherit;
+ display: -webkit-inline-box;
+ display: -webkit-inline-flex;
+ display: -ms-inline-flexbox;
+ display: inline-flex;
+ font-family: inherit;
+ font-size: 0.75rem;
+ font-weight: var(--font-bold);
+ -webkit-box-pack: center;
+ -webkit-justify-content: center;
+ -ms-flex-pack: center;
+ justify-content: center;
+ -webkit-letter-spacing: 0.025rem;
+ -moz-letter-spacing: 0.025rem;
+ -ms-letter-spacing: 0.025rem;
+ letter-spacing: 0.025rem;
+ line-height: 1rem;
+ min-height: var(--size-1halfx);
+ min-width: 2rem;
+ outline: none;
+ padding: 0 var(--spacing-1halfx);
+ text-transform: uppercase;
+ -webkit-user-select: none;
+ -moz-user-select: none;
+ -ms-user-select: none;
+ user-select: none;
+}
+
+.c4 {
+ outline: 2px solid transparent;
+ outline-offset: -2px;
+}
+
+.c4:focus {
+ box-shadow: 0 0 0 2px #006296;
+ outline: 2px solid #84C6EA;
+ outline-offset: -2px;
+}
+
+.c4 > svg {
+ height: var(--size-1x);
+ width: var(--size-1x);
+}
+
+.c5 {
+ background-color: transparent;
+ border-color: transparent;
+ color: #60666E;
+ min-width: var(--size-1halfx);
+ padding: 0;
+ width: var(--size-1halfx);
+}
+
+.c5 {
+ outline: 2px solid transparent;
+ outline-offset: -2px;
+}
+
+.c5:focus {
+ box-shadow: 0 0 0 2px #006296;
+ outline: 2px solid #84C6EA;
+ outline-offset: -2px;
+}
+
+.c5:hover,
+.c5[aria-expanded='true'] {
+ background-color: rgb(0 0 0 / 0.15);
+ border-color: transparent;
+ color: #000000;
+}
+
+.c5[aria-disabled='true'] {
+ background-color: transparent;
+ border-color: transparent;
+ color: #B7BBC2;
+ cursor: not-allowed;
+}
+
+.c5 > svg {
+ height: var(--size-1x);
+ width: var(--size-1x);
+}
+
+.c0 {
+ -webkit-align-items: flex-start;
+ -webkit-box-align: flex-start;
+ -ms-flex-align: flex-start;
+ align-items: flex-start;
+ background-color: #FFFFFF;
+ border: 1px solid #DBDEE1;
+ border-radius: var(--border-radius);
+ display: -webkit-box;
+ display: -webkit-flex;
+ display: -ms-flexbox;
+ display: flex;
+ gap: var(--spacing-1x);
+ padding: var(--spacing-1x) var(--spacing-2x) var(--spacing-1x) var(--spacing-1halfx);
+}
+
+.c0 .eds-AttachedFile-icon {
+ height: var(--size-1x);
+ margin-top: var(--spacing-half);
+ width: var(--size-1x);
+}
+
+.c0 .eds-AttachedFile-status {
+ color: #60666E;
+}
+
+.c1 {
+ color: #1B1C1E;
+ display: -webkit-box;
+ display: -webkit-flex;
+ display: -ms-flexbox;
+ display: flex;
+ -webkit-flex-direction: column;
+ -ms-flex-direction: column;
+ flex-direction: column;
+ font-size: 0.75rem;
+ gap: var(--spacing-half);
+ line-height: 1rem;
+ margin-top: var(--spacing-half);
+}
+
+.c2 {
+ display: block;
+}
+
+.c3 {
+ -webkit-align-items: center;
+ -webkit-box-align: center;
+ -ms-flex-align: center;
+ align-items: center;
+ display: -webkit-box;
+ display: -webkit-flex;
+ display: -ms-flexbox;
+ display: flex;
+ -webkit-flex-shrink: 0;
+ -ms-flex-negative: 0;
+ flex-shrink: 0;
+ gap: var(--spacing-1x);
+ margin-left: auto;
+}
+
+
+
+
+
+ File.txt (97.7 KB)
+
+
+
+
+
+
+`;
+
+exports[`AttachedFile Matches Error Snapshot 1`] = `
+.c4 {
+ -webkit-align-items: center;
+ -webkit-box-align: center;
+ -ms-flex-align: center;
+ align-items: center;
+ -webkit-appearance: none;
+ -moz-appearance: none;
+ appearance: none;
+ background: inherit;
+ border: 1px solid;
+ border-radius: 1.5rem;
+ box-sizing: border-box;
+ color: inherit;
+ display: -webkit-inline-box;
+ display: -webkit-inline-flex;
+ display: -ms-inline-flexbox;
+ display: inline-flex;
+ font-family: inherit;
+ font-size: 0.75rem;
+ font-weight: var(--font-bold);
+ -webkit-box-pack: center;
+ -webkit-justify-content: center;
+ -ms-flex-pack: center;
+ justify-content: center;
+ -webkit-letter-spacing: 0.025rem;
+ -moz-letter-spacing: 0.025rem;
+ -ms-letter-spacing: 0.025rem;
+ letter-spacing: 0.025rem;
+ line-height: 1rem;
+ min-height: var(--size-1halfx);
+ min-width: 2rem;
+ outline: none;
+ padding: 0 var(--spacing-1halfx);
+ text-transform: uppercase;
+ -webkit-user-select: none;
+ -moz-user-select: none;
+ -ms-user-select: none;
+ user-select: none;
+}
+
+.c4 {
+ outline: 2px solid transparent;
+ outline-offset: -2px;
+}
+
+.c4:focus {
+ box-shadow: 0 0 0 2px #006296;
+ outline: 2px solid #84C6EA;
+ outline-offset: -2px;
+}
+
+.c4 > svg {
+ height: var(--size-1x);
+ width: var(--size-1x);
+}
+
+.c5 {
+ background-color: transparent;
+ border-color: transparent;
+ color: #60666E;
+ min-width: var(--size-1halfx);
+ padding: 0;
+ width: var(--size-1halfx);
+}
+
+.c5 {
+ outline: 2px solid transparent;
+ outline-offset: -2px;
+}
+
+.c5:focus {
+ box-shadow: 0 0 0 2px #006296;
+ outline: 2px solid #84C6EA;
+ outline-offset: -2px;
+}
+
+.c5:hover,
+.c5[aria-expanded='true'] {
+ background-color: rgb(0 0 0 / 0.15);
+ border-color: transparent;
+ color: #000000;
+}
+
+.c5[aria-disabled='true'] {
+ background-color: transparent;
+ border-color: transparent;
+ color: #B7BBC2;
+ cursor: not-allowed;
+}
+
+.c5 > svg {
+ height: var(--size-1x);
+ width: var(--size-1x);
+}
+
+.c0 {
+ -webkit-align-items: flex-start;
+ -webkit-box-align: flex-start;
+ -ms-flex-align: flex-start;
+ align-items: flex-start;
+ background-color: #FFFFFF;
+ border: 1px solid #DBDEE1;
+ border-radius: var(--border-radius);
+ display: -webkit-box;
+ display: -webkit-flex;
+ display: -ms-flexbox;
+ display: flex;
+ gap: var(--spacing-1x);
+ padding: var(--spacing-1x) var(--spacing-2x) var(--spacing-1x) var(--spacing-1halfx);
+}
+
+.c0 .eds-AttachedFile-icon {
+ height: var(--size-1x);
+ margin-top: var(--spacing-half);
+ width: var(--size-1x);
+}
+
+.c0 .eds-AttachedFile-status {
+ color: #CD2C23;
+}
+
+.c1 {
+ color: #1B1C1E;
+ display: -webkit-box;
+ display: -webkit-flex;
+ display: -ms-flexbox;
+ display: flex;
+ -webkit-flex-direction: column;
+ -ms-flex-direction: column;
+ flex-direction: column;
+ font-size: 0.75rem;
+ gap: var(--spacing-half);
+ line-height: 1rem;
+ margin-top: var(--spacing-half);
+}
+
+.c2 {
+ display: block;
+}
+
+.c3 {
+ -webkit-align-items: center;
+ -webkit-box-align: center;
+ -ms-flex-align: center;
+ align-items: center;
+ display: -webkit-box;
+ display: -webkit-flex;
+ display: -ms-flexbox;
+ display: flex;
+ -webkit-flex-shrink: 0;
+ -ms-flex-negative: 0;
+ flex-shrink: 0;
+ gap: var(--spacing-1x);
+ margin-left: auto;
+}
+
+
+
+
+
+ File.txt (97.7 KB)
+
+
+
+
+
+
+`;
+
+exports[`AttachedFile Matches Success Snapshot 1`] = `
+.c4 {
+ -webkit-align-items: center;
+ -webkit-box-align: center;
+ -ms-flex-align: center;
+ align-items: center;
+ -webkit-appearance: none;
+ -moz-appearance: none;
+ appearance: none;
+ background: inherit;
+ border: 1px solid;
+ border-radius: 1.5rem;
+ box-sizing: border-box;
+ color: inherit;
+ display: -webkit-inline-box;
+ display: -webkit-inline-flex;
+ display: -ms-inline-flexbox;
+ display: inline-flex;
+ font-family: inherit;
+ font-size: 0.75rem;
+ font-weight: var(--font-bold);
+ -webkit-box-pack: center;
+ -webkit-justify-content: center;
+ -ms-flex-pack: center;
+ justify-content: center;
+ -webkit-letter-spacing: 0.025rem;
+ -moz-letter-spacing: 0.025rem;
+ -ms-letter-spacing: 0.025rem;
+ letter-spacing: 0.025rem;
+ line-height: 1rem;
+ min-height: var(--size-1halfx);
+ min-width: 2rem;
+ outline: none;
+ padding: 0 var(--spacing-1halfx);
+ text-transform: uppercase;
+ -webkit-user-select: none;
+ -moz-user-select: none;
+ -ms-user-select: none;
+ user-select: none;
+}
+
+.c4 {
+ outline: 2px solid transparent;
+ outline-offset: -2px;
+}
+
+.c4:focus {
+ box-shadow: 0 0 0 2px #006296;
+ outline: 2px solid #84C6EA;
+ outline-offset: -2px;
+}
+
+.c4 > svg {
+ height: var(--size-1x);
+ width: var(--size-1x);
+}
+
+.c5 {
+ background-color: transparent;
+ border-color: transparent;
+ color: #60666E;
+ min-width: var(--size-1halfx);
+ padding: 0;
+ width: var(--size-1halfx);
+}
+
+.c5 {
+ outline: 2px solid transparent;
+ outline-offset: -2px;
+}
+
+.c5:focus {
+ box-shadow: 0 0 0 2px #006296;
+ outline: 2px solid #84C6EA;
+ outline-offset: -2px;
+}
+
+.c5:hover,
+.c5[aria-expanded='true'] {
+ background-color: rgb(0 0 0 / 0.15);
+ border-color: transparent;
+ color: #000000;
+}
+
+.c5[aria-disabled='true'] {
+ background-color: transparent;
+ border-color: transparent;
+ color: #B7BBC2;
+ cursor: not-allowed;
+}
+
+.c5 > svg {
+ height: var(--size-1x);
+ width: var(--size-1x);
+}
+
+.c0 {
+ -webkit-align-items: flex-start;
+ -webkit-box-align: flex-start;
+ -ms-flex-align: flex-start;
+ align-items: flex-start;
+ background-color: #FFFFFF;
+ border: 1px solid #DBDEE1;
+ border-radius: var(--border-radius);
+ display: -webkit-box;
+ display: -webkit-flex;
+ display: -ms-flexbox;
+ display: flex;
+ gap: var(--spacing-1x);
+ padding: var(--spacing-1x) var(--spacing-2x) var(--spacing-1x) var(--spacing-1halfx);
+}
+
+.c0 .eds-AttachedFile-icon {
+ height: var(--size-1x);
+ margin-top: var(--spacing-half);
+ width: var(--size-1x);
+}
+
+.c0 .eds-AttachedFile-status {
+ color: #008533;
+}
+
+.c1 {
+ color: #1B1C1E;
+ display: -webkit-box;
+ display: -webkit-flex;
+ display: -ms-flexbox;
+ display: flex;
+ -webkit-flex-direction: column;
+ -ms-flex-direction: column;
+ flex-direction: column;
+ font-size: 0.75rem;
+ gap: var(--spacing-half);
+ line-height: 1rem;
+ margin-top: var(--spacing-half);
+}
+
+.c2 {
+ display: block;
+}
+
+.c3 {
+ -webkit-align-items: center;
+ -webkit-box-align: center;
+ -ms-flex-align: center;
+ align-items: center;
+ display: -webkit-box;
+ display: -webkit-flex;
+ display: -ms-flexbox;
+ display: flex;
+ -webkit-flex-shrink: 0;
+ -ms-flex-negative: 0;
+ flex-shrink: 0;
+ gap: var(--spacing-1x);
+ margin-left: auto;
+}
+
+
+
+
+
+ File.txt (97.7 KB)
+
+
+ Uploading completed
+
+
+
+
+
+
+`;
+
+exports[`AttachedFile Matches Uploading Snapshot 1`] = `
+.c5 {
+ -webkit-align-items: center;
+ -webkit-box-align: center;
+ -ms-flex-align: center;
+ align-items: center;
+ -webkit-appearance: none;
+ -moz-appearance: none;
+ appearance: none;
+ background: inherit;
+ border: 1px solid;
+ border-radius: 1.5rem;
+ box-sizing: border-box;
+ color: inherit;
+ display: -webkit-inline-box;
+ display: -webkit-inline-flex;
+ display: -ms-inline-flexbox;
+ display: inline-flex;
+ font-family: inherit;
+ font-size: 0.75rem;
+ font-weight: var(--font-bold);
+ -webkit-box-pack: center;
+ -webkit-justify-content: center;
+ -ms-flex-pack: center;
+ justify-content: center;
+ -webkit-letter-spacing: 0.025rem;
+ -moz-letter-spacing: 0.025rem;
+ -ms-letter-spacing: 0.025rem;
+ letter-spacing: 0.025rem;
+ line-height: 1rem;
+ min-height: var(--size-1halfx);
+ min-width: 2rem;
+ outline: none;
+ padding: 0 var(--spacing-1halfx);
+ text-transform: uppercase;
+ -webkit-user-select: none;
+ -moz-user-select: none;
+ -ms-user-select: none;
+ user-select: none;
+}
+
+.c5 {
+ outline: 2px solid transparent;
+ outline-offset: -2px;
+}
+
+.c5:focus {
+ box-shadow: 0 0 0 2px #006296;
+ outline: 2px solid #84C6EA;
+ outline-offset: -2px;
+}
+
+.c5 > svg {
+ height: var(--size-1x);
+ width: var(--size-1x);
+}
+
+.c1 {
+ color: #006296;
+ height: 64px;
+ width: 64px;
+}
+
+.c6 {
+ background-color: transparent;
+ border-color: transparent;
+ color: #60666E;
+}
+
+.c6 {
+ outline: 2px solid transparent;
+ outline-offset: -2px;
+}
+
+.c6:focus {
+ box-shadow: 0 0 0 2px #006296;
+ outline: 2px solid #84C6EA;
+ outline-offset: -2px;
+}
+
+.c6:hover,
+.c6[aria-expanded='true'] {
+ background-color: rgb(0 0 0 / 0.15);
+ border-color: transparent;
+ color: #000000;
+}
+
+.c6[aria-disabled='true'] {
+ background-color: transparent;
+ border-color: transparent;
+ color: #B7BBC2;
+ cursor: not-allowed;
+}
+
+.c0 {
+ -webkit-align-items: flex-start;
+ -webkit-box-align: flex-start;
+ -ms-flex-align: flex-start;
+ align-items: flex-start;
+ background-color: #FFFFFF;
+ border: 1px solid #DBDEE1;
+ border-radius: var(--border-radius);
+ display: -webkit-box;
+ display: -webkit-flex;
+ display: -ms-flexbox;
+ display: flex;
+ gap: var(--spacing-1x);
+ padding: var(--spacing-1x) var(--spacing-2x) var(--spacing-1x) var(--spacing-1halfx);
+}
+
+.c0 .eds-AttachedFile-icon {
+ height: var(--size-1x);
+ margin-top: var(--spacing-half);
+ width: var(--size-1x);
+}
+
+.c0 .eds-AttachedFile-status {
+ color: #60666E;
+}
+
+.c2 {
+ color: #1B1C1E;
+ display: -webkit-box;
+ display: -webkit-flex;
+ display: -ms-flexbox;
+ display: flex;
+ -webkit-flex-direction: column;
+ -ms-flex-direction: column;
+ flex-direction: column;
+ font-size: 0.75rem;
+ gap: var(--spacing-half);
+ line-height: 1rem;
+ margin-top: var(--spacing-half);
+}
+
+.c3 {
+ display: block;
+}
+
+.c4 {
+ -webkit-align-items: center;
+ -webkit-box-align: center;
+ -ms-flex-align: center;
+ align-items: center;
+ display: -webkit-box;
+ display: -webkit-flex;
+ display: -ms-flexbox;
+ display: flex;
+ -webkit-flex-shrink: 0;
+ -ms-flex-negative: 0;
+ flex-shrink: 0;
+ gap: var(--spacing-1x);
+ margin-left: auto;
+}
+
+
+
+
+
+ File.txt (97.7 KB)
+
+
+ Uploading...null
+
+
+
+
+
+
+`;
diff --git a/packages/react/src/components/attached-file/attached-file.tsx b/packages/react/src/components/attached-file/attached-file.tsx
new file mode 100644
index 0000000000..995485d184
--- /dev/null
+++ b/packages/react/src/components/attached-file/attached-file.tsx
@@ -0,0 +1,262 @@
+import { FunctionComponent, PropsWithChildren, useEffect, useMemo, useState } from 'react';
+import styled, { DefaultTheme } from 'styled-components';
+import { clsx } from 'clsx';
+import { useTranslation } from '../../i18n/use-translation';
+import { Spinner } from '../spinner/spinner';
+import { useTheme } from '../../hooks/use-theme';
+import { Lozenge } from '../lozenge/lozenge';
+import { useId } from '../../hooks/use-id';
+import { Button, IconButton } from '../buttons';
+import { Icon } from '../icon/icon';
+import { attachedFileClasses } from './attached-file-classes';
+
+type AttachedFileStatus = 'default' | 'uploading' | 'cancelled' | 'error' | 'success';
+
+function formatFilesize(units: string[], size: number): string {
+ let i = 0;
+ let scaledSize = size;
+ while (scaledSize >= 1024) {
+ scaledSize /= 1024;
+ i += 1;
+ }
+ const roundedValue = Math.round(scaledSize * 10) / 10;
+
+ return `${roundedValue} ${units[i]}`;
+}
+
+const getStatustextColor = (status: AttachedFileStatus, theme: DefaultTheme): string => {
+ switch (status) {
+ case 'error':
+ return theme.component['attached-file-error-text-color'];
+ case 'success':
+ return theme.component['attached-file-success-text-color'];
+ default:
+ return theme.component['attached-file-auxiliary-text-color'];
+ }
+};
+
+const AttachedFileRoot = styled.div<{ $status: AttachedFileStatus }>`
+ align-items: flex-start;
+ background-color: ${({ theme }) => theme.component['attached-file-background-color']};
+ border: 1px solid ${({ theme }) => theme.component['attached-file-border-color']};
+ border-radius: var(--border-radius);
+ display: flex;
+ gap: var(--spacing-1x);
+ padding: var(--spacing-1x) var(--spacing-2x) var(--spacing-1x) var(--spacing-1halfx);
+
+ .${attachedFileClasses.icon} {
+ height: var(--size-1x);
+ margin-top: var(--spacing-half);
+ width: var(--size-1x);
+ }
+
+ .${attachedFileClasses.status} {
+ color: ${({ theme, $status }) => getStatustextColor($status, theme)};
+ }
+`;
+
+const AttachedFileContent = styled.div`
+ color: ${({ theme }) => theme.component['attached-file-text-color']};
+ display: flex;
+ flex-direction: column;
+ font-size: 0.75rem;
+ gap: var(--spacing-half);
+ line-height: 1rem;
+ margin-top: var(--spacing-half);
+`;
+
+const AttachedFileName = styled.span`
+ display: block;
+`;
+
+const AttachedFileActions = styled.div`
+ align-items: center;
+ display: flex;
+ flex-shrink: 0;
+ gap: var(--spacing-1x);
+ margin-left: auto;
+`;
+
+export interface AttachedFileProps {
+ id?: string;
+ filename: string;
+ className?: string;
+ /**
+ * Size in bytes
+ */
+ filesize?: number;
+ percent?: number;
+ errorText?: string;
+ onCancel?(): void;
+ onDelete?(): void;
+ onRetry?(): void;
+ onClose?(): void;
+ status?: AttachedFileStatus;
+ lozengeText?: string;
+}
+
+export const AttachedFile: FunctionComponent> = ({
+ id: providedId,
+ className,
+ filename,
+ filesize,
+ percent,
+ errorText,
+ onCancel,
+ onDelete,
+ onRetry,
+ onClose,
+ status = 'default',
+ lozengeText,
+ children,
+}) => {
+ const { t } = useTranslation('attached-file');
+ const { t: tCommon } = useTranslation('common');
+ const id = useId(providedId);
+ const theme = useTheme();
+ const [currentStatus, setCurrentStatus] = useState(status);
+
+ useEffect(() => {
+ if (status === 'success') {
+ const timer = setTimeout(() => setCurrentStatus('default'), 2000);
+ return () => clearTimeout(timer);
+ }
+ return undefined;
+ }, [status]);
+
+ const renderIcon = (): JSX.Element => {
+ switch (currentStatus) {
+ case 'uploading':
+ return (
+
+ );
+ case 'cancelled':
+ return (
+
+ );
+ case 'error':
+ return (
+
+ );
+ case 'success':
+ return (
+
+ );
+ default:
+ return (
+
+ );
+ }
+ };
+
+ const renderActions = (): JSX.Element => {
+ switch (currentStatus) {
+ case 'uploading':
+ return ;
+ case 'cancelled':
+ return (
+ <>
+
+
+ >
+ );
+ case 'error':
+ return (
+ <>
+ {onRetry && }
+
+ >
+ );
+ default:
+ return (
+
+ );
+ }
+ };
+
+ const statusText = useMemo(() => {
+ switch (currentStatus) {
+ case 'uploading':
+ return t('uploading') + (percent !== undefined ? ` ${percent}%` : null);
+ case 'cancelled':
+ return t('uploadCancelled');
+ case 'error':
+ return errorText;
+ case 'success':
+ return t('uploadCompleted');
+ default:
+ return undefined;
+ }
+ }, [t, currentStatus, errorText, percent]);
+
+ const filesizeText = filesize !== undefined ? ` (${formatFilesize(tCommon('unitSymbolBytes', { returnObjects: true }) as string[], filesize)})` : null;
+
+ return (
+
+ {renderIcon()}
+
+
+ {filename}
+ {filesizeText}
+
+ {statusText && (
+
+ {statusText}
+
+ )}
+
+
+ {lozengeText ? (
+
+ {lozengeText}
+
+ ) : children}
+ {renderActions()}
+
+
+ );
+};
diff --git a/packages/react/src/components/attached-file/index.ts b/packages/react/src/components/attached-file/index.ts
new file mode 100644
index 0000000000..bc3ba94a23
--- /dev/null
+++ b/packages/react/src/components/attached-file/index.ts
@@ -0,0 +1,2 @@
+export { attachedFileClasses, AttachedFileClasses } from './attached-file-classes';
+export { AttachedFile, AttachedFileProps } from './attached-file';
diff --git a/packages/react/src/components/icon/icon.tsx b/packages/react/src/components/icon/icon.tsx
index d94aa0d2e5..84fe734cf0 100644
--- a/packages/react/src/components/icon/icon.tsx
+++ b/packages/react/src/components/icon/icon.tsx
@@ -20,6 +20,8 @@ import Edit from 'feather-icons/dist/icons/edit-2.svg';
import ExternalLink from 'feather-icons/dist/icons/external-link.svg';
import Eye from 'feather-icons/dist/icons/eye.svg';
import EyeOff from 'feather-icons/dist/icons/eye-off.svg';
+import File from 'feather-icons/dist/icons/file.svg';
+import FileText from 'feather-icons/dist/icons/file-text.svg';
import Graph from 'feather-icons/dist/icons/bar-chart-2.svg';
import HelpCircle from 'feather-icons/dist/icons/help-circle.svg';
import Home from 'feather-icons/dist/icons/home.svg';
@@ -39,9 +41,11 @@ import Send from 'feather-icons/dist/icons/send.svg';
import Settings from 'feather-icons/dist/icons/settings.svg';
import Star from 'feather-icons/dist/icons/star.svg';
import Trash from 'feather-icons/dist/icons/trash-2.svg';
+import Upload from 'feather-icons/dist/icons/upload.svg';
import User from 'feather-icons/dist/icons/user.svg';
import Users from 'feather-icons/dist/icons/users.svg';
import X from 'feather-icons/dist/icons/x.svg';
+import XOctagon from 'feather-icons/dist/icons/x-octagon.svg';
import { VoidFunctionComponent } from 'react';
import AlertFilled from '../../icons/alert-filled.svg';
import ArrowDownCircle from '../../icons/arrow-down-circle.svg';
@@ -96,7 +100,9 @@ const iconMapping = {
export: Export,
eye: Eye,
eyeOff: EyeOff,
+ file: File,
files: Files,
+ fileText: FileText,
graph: Graph,
helpCircle: HelpCircle,
history: History,
@@ -125,10 +131,12 @@ const iconMapping = {
transfer: Transfer,
trash: Trash,
unlink: Unlink,
+ upload: Upload,
user: User,
users: Users,
warningFilled: WarningFilled,
x: X,
+ xOctagon: XOctagon,
} as const;
export type IconName = keyof typeof iconMapping;
diff --git a/packages/react/src/i18n/translations.ts b/packages/react/src/i18n/translations.ts
index b4c9ba6e1b..cc822ff9bf 100644
--- a/packages/react/src/i18n/translations.ts
+++ b/packages/react/src/i18n/translations.ts
@@ -3,6 +3,16 @@ export const Translations = {
avatar: {
ariaLabel: '{{username}} avatar',
},
+ 'attached-file': {
+ close: 'Close',
+ retry: 'Retry',
+ resume: 'Resume',
+ cancel: 'Cancel',
+ deleteFile: 'Delete file',
+ uploading: 'Uploading...',
+ uploadCompleted: 'Uploading completed',
+ uploadCancelled: 'Uploading cancelled',
+ },
bento: {
productsLabel: 'Products',
externalsLabel: 'Resources',
@@ -22,6 +32,7 @@ export const Translations = {
opensInNewTab: 'opens in a new tab',
opensInNewTabScreenReader: '(opens in a new tab)',
error: 'Error',
+ unitSymbolBytes: ['B', 'KB', 'MB', 'GB', 'TB', 'PB'],
},
datepicker: {
calendarButtonLabel: 'Choose date',
@@ -144,6 +155,16 @@ export const Translations = {
avatar: {
ariaLabel: 'Avatar de {{username}}',
},
+ 'attached-file': {
+ close: 'Fermer',
+ retry: 'Réessayer',
+ resume: 'Reprendre',
+ cancel: 'Annuler',
+ deleteFile: 'Supprimer le fichier',
+ uploading: 'Téléversement...',
+ uploadCompleted: 'Téléversement terminé',
+ uploadCancelled: 'Téléversement annulé',
+ },
bento: {
productsLabel: 'Produits',
externalsLabel: 'Ressources',
@@ -163,6 +184,7 @@ export const Translations = {
opensInNewTab: 'ouvre dans un nouvel onglet',
opensInNewTabScreenReader: '(ouvre dans un nouvel onglet)',
error: 'Erreur',
+ unitSymbolBytes: ['o', 'Ko', 'Mo', 'Go', 'To', 'Po'],
},
datepicker: {
calendarButtonLabel: 'Choisissez une date',
diff --git a/packages/react/src/index.ts b/packages/react/src/index.ts
index dde5511eae..9a164cb02e 100644
--- a/packages/react/src/index.ts
+++ b/packages/react/src/index.ts
@@ -51,6 +51,7 @@ export { Listbox, ListboxOption } from './components/listbox/listbox';
// Miscellaneous
export { AccordionItem, Accordion, ItemsProps } from './components/accordion/index';
+export * from './components/attached-file';
export { Badge } from './components/badge/badge';
export { Card } from './components/card/card';
export { CardLink } from './components/card-link/card-link';
diff --git a/packages/react/src/themes/tokens/component-tokens.ts b/packages/react/src/themes/tokens/component-tokens.ts
index c18a00b6ac..f1e2dc75c7 100644
--- a/packages/react/src/themes/tokens/component-tokens.ts
+++ b/packages/react/src/themes/tokens/component-tokens.ts
@@ -1,5 +1,6 @@
import { AccordionToken, defaultAccordionTokens } from './component/accordion-tokens';
import { AvatarToken, defaultAvatarTokens } from './component/avatar-tokens';
+import { AttachedFileToken, defaultAttachedFileTokens } from './component/attached-file-tokens';
import { BadgeToken, defaultBadgeTokens } from './component/badge-tokens';
import { BentoMenuButtonToken, defaultBentoMenuButtonTokens } from './component/bento-menu-button-tokens';
import { BreadcrumbToken, defaultBreadcrumbTokens } from './component/breadcrumb-tokens';
@@ -56,6 +57,7 @@ import { defaultTooltipTokens, TooltipToken } from './component/tooltip-tokens';
export type ComponentToken =
| AccordionToken
| AvatarToken
+ | AttachedFileToken
| BadgeToken
| BentoMenuButtonToken
| BreadcrumbToken
@@ -112,6 +114,7 @@ export type ComponentToken =
export const defaultComponentTokens = {
...defaultAccordionTokens,
...defaultAvatarTokens,
+ ...defaultAttachedFileTokens,
...defaultBadgeTokens,
...defaultBentoMenuButtonTokens,
...defaultBreadcrumbTokens,
diff --git a/packages/react/src/themes/tokens/component/attached-file-tokens.ts b/packages/react/src/themes/tokens/component/attached-file-tokens.ts
new file mode 100644
index 0000000000..dbfb3b3def
--- /dev/null
+++ b/packages/react/src/themes/tokens/component/attached-file-tokens.ts
@@ -0,0 +1,24 @@
+import type { ComponentTokenMap } from '../tokens';
+
+export type AttachedFileToken =
+ | 'attached-file-background-color'
+ | 'attached-file-border-color'
+ | 'attached-file-icon-color'
+ | 'attached-file-text-color'
+ | 'attached-file-auxiliary-text-color'
+ | 'attached-file-success-text-color'
+ | 'attached-file-success-icon-color'
+ | 'attached-file-error-text-color'
+ | 'attached-file-error-icon-color'
+
+export const defaultAttachedFileTokens: ComponentTokenMap = {
+ 'attached-file-background-color': 'color-background',
+ 'attached-file-border-color': 'color-border',
+ 'attached-file-icon-color': 'color-content-subtle',
+ 'attached-file-text-color': 'color-content',
+ 'attached-file-auxiliary-text-color': 'color-content-subtle',
+ 'attached-file-success-text-color': 'color-control-auxiliary-success',
+ 'attached-file-success-icon-color': 'color-control-auxiliary-success',
+ 'attached-file-error-text-color': 'color-control-auxiliary-error',
+ 'attached-file-error-icon-color': 'color-control-auxiliary-error',
+};
diff --git a/packages/storybook/stories/attached-file.mdx b/packages/storybook/stories/attached-file.mdx
new file mode 100644
index 0000000000..78471f5182
--- /dev/null
+++ b/packages/storybook/stories/attached-file.mdx
@@ -0,0 +1,59 @@
+import { AttachedFile } from "@equisoft/design-elements-react";
+import { ArgTypes, Canvas, Meta } from "@storybook/blocks";
+import * as AttachedFileStories from "./attached-file.stories";
+
+
+
+# Attached File
+
+1. [Definition](#definition)
+2. [Usage](#usage)
+3. [States](#states)
+4. [Properties](#properties)
+
+## Definition
+
+Drop zone allows users to upload files by dragging and dropping the files (or folders) into an area on a page, or activating a button [2].
+
+
+
+## Usage
+
+### When to use
+
+- Use when you need to display files that users have uploaded, such as documents, images, or other attachments.
+- Use alongside the Dropzone component to visually represent files that users have dragged and dropped into an upload area.
+- Use when you want to provide users with real-time feedback on the status of their file uploads, including progress indicators and success/error states.
+- Use when you want to allow users to perform actions on uploaded files, such as deleting or retrying a failed upload.
+- Use in forms where users are required to upload and manage documents, ensuring they can easily add, view, and manage their files as part of the submission process.
+- Use when you want to enforce file type, size restrictions, or other validation rules.
+
+### When not to use
+
+- Do not use if you need to display content directly on the page rather than as an attachment.
+- Do not use for files requiring complex interactions such as in-line editing, annotation, or version control.
+
+## States
+
+### Default
+
+The Default state is the final state of the attached file, following the Success state.
+
+### Uploading
+
+The Uploading state is the initial state of the attached file, indicating the progress of the file as it is being imported into the application.
+
+### Error
+
+The Error state occurs when there is an issue with the upload. Possible issues include:
+
+- Unsupported file type.
+- Upload failure.
+
+### Success
+
+The Success state confirms that the attached file has been successfully uploaded. This state is displayed briefly (2 seconds) before transitioning to the Default state.
+
+## Properties
+
+
diff --git a/packages/storybook/stories/attached-file.stories.tsx b/packages/storybook/stories/attached-file.stories.tsx
new file mode 100644
index 0000000000..baa0f4d1fe
--- /dev/null
+++ b/packages/storybook/stories/attached-file.stories.tsx
@@ -0,0 +1,93 @@
+import { AttachedFile } from '@equisoft/design-elements-react';
+import { Meta, StoryObj } from '@storybook/react';
+
+const AttachedFileMeta: Meta = {
+ title: 'Components/Attached File',
+ component: AttachedFile,
+ argTypes: {
+ children: {
+ control: {
+ type: 'text',
+ },
+ },
+ },
+ args: {
+ onCancel() {
+ // eslint-disable-next-line no-console
+ console.info('onCancel');
+ },
+ onDelete() {
+ // eslint-disable-next-line no-console
+ console.info('onDelete');
+ },
+ onRetry() {
+ // eslint-disable-next-line no-console
+ console.info('onRetry');
+ },
+ onClose() {
+ // eslint-disable-next-line no-console
+ console.info('onClose');
+ },
+ },
+};
+
+export default AttachedFileMeta;
+
+type Story = StoryObj;
+
+export const Default: Story = {
+ args: {
+ ...AttachedFileMeta.args,
+ filename: 'File.txt',
+ status: 'default',
+ filesize: 100000,
+ },
+};
+
+export const Uploading: Story = {
+ args: {
+ ...AttachedFileMeta.args,
+ status: 'uploading',
+ filename: 'File.txt',
+ filesize: 100000,
+ percent: 40,
+ },
+};
+
+export const Error: Story = {
+ args: {
+ ...AttachedFileMeta.args,
+ status: 'error',
+ filename: 'File.txt',
+ filesize: 100000,
+ errorText: 'Error message',
+ },
+};
+
+export const Cancelled: Story = {
+ args: {
+ ...AttachedFileMeta.args,
+ status: 'cancelled',
+ filename: 'File.txt',
+ filesize: 100000,
+ },
+};
+
+export const Success: Story = {
+ args: {
+ ...AttachedFileMeta.args,
+ filename: 'File.txt',
+ filesize: 100000,
+ status: 'success',
+ },
+};
+
+export const WithLozenge: Story = {
+ args: {
+ ...AttachedFileMeta.args,
+ filename: 'File.txt',
+ status: 'default',
+ filesize: 100000,
+ lozengeText: 'Label',
+ },
+};
diff --git a/packages/storybook/stories/icon-library.mdx b/packages/storybook/stories/icon-library.mdx
index bf6df52236..fc6b17dffb 100644
--- a/packages/storybook/stories/icon-library.mdx
+++ b/packages/storybook/stories/icon-library.mdx
@@ -186,6 +186,9 @@ import { ArgTypes, Canvas, IconGallery, IconItem, Meta, Title } from '@storybook
+
+
+
diff --git a/yarn.lock b/yarn.lock
index 273532478e..d7030379c3 100644
--- a/yarn.lock
+++ b/yarn.lock
@@ -1703,6 +1703,7 @@ __metadata:
"@typescript-eslint/eslint-plugin": "npm:7.18.0"
"@typescript-eslint/parser": "npm:7.18.0"
"@wojtekmaj/enzyme-adapter-react-17": "npm:^0.8.0"
+ clsx: "npm:^2.1.1"
cross-env: "npm:^7.0.3"
css-loader: "npm:^7.0.0"
date-fns: "npm:^4.0.0"