From e5141002788e7b8a97cbf4358e40f158d019d568 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Oddbj=C3=B8rn=20=C3=98vernes?= Date: Tue, 24 Feb 2026 12:13:46 +0100 Subject: [PATCH 01/14] wip FileUpload --- packages/css/src/file-upload.css | 34 ++++++++++ packages/css/src/index.css | 1 + .../file-upload/file-upload-button.tsx | 19 ++++++ .../file-upload/file-upload-description.tsx | 19 ++++++ .../file-upload/file-upload-label.tsx | 19 ++++++ .../file-upload/file-upload.stories.tsx | 45 +++++++++++++ .../components/file-upload/file-upload.tsx | 37 +++++++++++ .../react/src/components/file-upload/index.ts | 65 +++++++++++++++++++ 8 files changed, 239 insertions(+) create mode 100644 packages/css/src/file-upload.css create mode 100644 packages/react/src/components/file-upload/file-upload-button.tsx create mode 100644 packages/react/src/components/file-upload/file-upload-description.tsx create mode 100644 packages/react/src/components/file-upload/file-upload-label.tsx create mode 100644 packages/react/src/components/file-upload/file-upload.stories.tsx create mode 100644 packages/react/src/components/file-upload/file-upload.tsx create mode 100644 packages/react/src/components/file-upload/index.ts diff --git a/packages/css/src/file-upload.css b/packages/css/src/file-upload.css new file mode 100644 index 0000000000..f431334e8f --- /dev/null +++ b/packages/css/src/file-upload.css @@ -0,0 +1,34 @@ +.ds-file-upload { + border: 2px dashed var(--ds-color-border-default); + width: auto; + border-radius: var(--ds-border-radius-md); + padding-block: var(--ds-size-8); + padding-inline: var(--ds-size-6); + display: flex; + flex-direction: column; + align-items: center; + background-color: var(--ds-color-surface-default); + &:hover { + border-style: solid; + background-color: var(--ds-color-surface-tinted); + } + > * + * { + margin-top: var(--ds-size-4); + } + input { + display: none; + } + svg { + color: var(--ds-color-text-subtle); + height: var(--ds-size-8); + width: auto; + } + .ds-file-upload__label { + margin-top: var(--ds-size-2); + color: var(--ds-color-text-default); + } + .ds-file-upload__description { + color: var(--ds-color-neutral-text-subtle); + margin-top: 0; + } +} diff --git a/packages/css/src/index.css b/packages/css/src/index.css index 23fb4ab15d..2ae7846935 100644 --- a/packages/css/src/index.css +++ b/packages/css/src/index.css @@ -14,6 +14,7 @@ @import url('./input.css') layer(ds.components); @import url('./field.css') layer(ds.components); @import url('./fieldset.css') layer(ds.components); +@import url('./file-upload.css') layer(ds.components); @import url('./alert.css') layer(ds.components); @import url('./popover.css') layer(ds.components); @import url('./skip-link.css') layer(ds.components); diff --git a/packages/react/src/components/file-upload/file-upload-button.tsx b/packages/react/src/components/file-upload/file-upload-button.tsx new file mode 100644 index 0000000000..f088a694ef --- /dev/null +++ b/packages/react/src/components/file-upload/file-upload-button.tsx @@ -0,0 +1,19 @@ +import { forwardRef, type HTMLAttributes } from 'react'; +import type { ButtonProps } from '../button/button'; +import { Button } from '../button/button'; + +export type FileUploadButtonProps = Pick & + HTMLAttributes; + +export const FileUploadButton = forwardRef< + HTMLSpanElement, + FileUploadButtonProps +>(function FileUploadButton({ className, children, ...rest }, ref) { + return ( + + ); +}); diff --git a/packages/react/src/components/file-upload/file-upload-description.tsx b/packages/react/src/components/file-upload/file-upload-description.tsx new file mode 100644 index 0000000000..17698c3e09 --- /dev/null +++ b/packages/react/src/components/file-upload/file-upload-description.tsx @@ -0,0 +1,19 @@ +import cl from 'clsx/lite'; +import { forwardRef, type HTMLAttributes } from 'react'; +import { Paragraph, type ParagraphProps } from '../paragraph/paragraph'; + +export type FileUploadDescriptionProps = ParagraphProps & + HTMLAttributes; + +export const FileUploadDescription = forwardRef< + HTMLParagraphElement, + FileUploadDescriptionProps +>(function FileUploadDescription({ className, ...rest }, ref) { + return ( + + ); +}); diff --git a/packages/react/src/components/file-upload/file-upload-label.tsx b/packages/react/src/components/file-upload/file-upload-label.tsx new file mode 100644 index 0000000000..45ad5983a6 --- /dev/null +++ b/packages/react/src/components/file-upload/file-upload-label.tsx @@ -0,0 +1,19 @@ +import { forwardRef, type HTMLAttributes } from 'react'; +import { Paragraph, type ParagraphProps } from '../paragraph/paragraph'; +import cl from 'clsx/lite'; + +export type FileUploadLabelProps = ParagraphProps & + HTMLAttributes; + +export const FileUploadLabel = forwardRef< + HTMLParagraphElement, + FileUploadLabelProps +>(function FileUploadLabel({ className, ...rest }, ref) { + return ( + + ); +}); diff --git a/packages/react/src/components/file-upload/file-upload.stories.tsx b/packages/react/src/components/file-upload/file-upload.stories.tsx new file mode 100644 index 0000000000..522f979ff6 --- /dev/null +++ b/packages/react/src/components/file-upload/file-upload.stories.tsx @@ -0,0 +1,45 @@ +import { CloudUpIcon } from '@navikt/aksel-icons'; +import type { Meta, StoryFn, StoryObj } from '@storybook/react-vite'; + +import { FileUpload } from './'; + +type Story = StoryObj; + +const meta: Meta = { + title: 'Komponenter/FileUpload', + component: FileUpload, + parameters: { + customStyles: { + display: 'flex', + flexDirection: 'row', + justifyContent: 'center', + alignItems: 'center', + flexWrap: 'wrap', + gap: 'var(--ds-size-4)', + }, + }, +}; + +export default meta; + +export const Preview: Story = { + render: ({ ...args }) => { + return ( + + + ); + }, +}; + +export const Variants: StoryFn = () => ( + + +); diff --git a/packages/react/src/components/file-upload/file-upload.tsx b/packages/react/src/components/file-upload/file-upload.tsx new file mode 100644 index 0000000000..afaf7027e0 --- /dev/null +++ b/packages/react/src/components/file-upload/file-upload.tsx @@ -0,0 +1,37 @@ +import cl from 'clsx/lite'; +import type { HTMLAttributes, ReactNode } from 'react'; +import { forwardRef } from 'react'; +import type { DefaultProps } from '../../types'; +import type { MergeRight } from '../../utilities'; + +export type FileUploadProps = MergeRight< + DefaultProps & HTMLAttributes, + { + /** Instances of `FileUpload.Button`, `FileUpload.label` or other React nodes */ + children: ReactNode; + } +>; + +/** + * FileUpload component to present a file upload area. + * + * @example + * + * + */ +export const FileUpload = forwardRef( + function FileUpload({ className, children, ...rest }, ref) { + return ( + + ); + }, +); diff --git a/packages/react/src/components/file-upload/index.ts b/packages/react/src/components/file-upload/index.ts new file mode 100644 index 0000000000..8f64e0d8eb --- /dev/null +++ b/packages/react/src/components/file-upload/index.ts @@ -0,0 +1,65 @@ +import { FileUpload as FileUploadParent } from './file-upload'; +import { FileUploadButton } from './file-upload-button'; +import { FileUploadDescription } from './file-upload-description'; +import { FileUploadLabel } from './file-upload-label'; + +type FileUpload = typeof FileUploadParent & { + /** + * Use `FileUpload.Button` to add a fake button for click affordance + * + * Place as a descendant of `FileUpload` + * + * @example + * + * Upload file + * + */ + Button: typeof FileUploadButton; + /** + * Use `FileUpload.Label` to add primary text inside FileUpload + * + * Place as a descendant of `FileUpload` + * + * @example + * + * Drop file here + * + */ + Label: typeof FileUploadLabel; + /** + * Use `FileUpload.Description` to add secondary text inside FileUpload + * + * Place as a descendant of `FileUpload` + * + * @example + * + * File must be in csv format and less than 2MB + * + */ + Description: typeof FileUploadDescription; +}; +/** + * FileUpload component to present a file upload area. + * + * @example + * + * Drop file here + * File must be in csv format + * Upload file + * + */ +const FileUploadComponent: FileUpload = Object.assign(FileUploadParent, { + Button: FileUploadButton, + Label: FileUploadLabel, + Description: FileUploadDescription, +}); + +FileUploadComponent.Button.displayName = 'FileUpload.Button'; +FileUploadComponent.Label.displayName = 'FileUpload.Label'; +FileUploadComponent.Description.displayName = 'FileUpload.Description'; + +export type { FileUploadProps } from './file-upload'; +export type { FileUploadButtonProps } from './file-upload-button'; +export type { FileUploadDescriptionProps } from './file-upload-description'; +export type { FileUploadLabelProps } from './file-upload-label'; +export { FileUploadComponent as FileUpload }; From e6c2009efe6b753ce582218e2dfc362fb6b9dffa Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Oddbj=C3=B8rn=20=C3=98vernes?= Date: Tue, 24 Feb 2026 12:25:03 +0100 Subject: [PATCH 02/14] format --- packages/react/src/components/file-upload/file-upload-label.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/react/src/components/file-upload/file-upload-label.tsx b/packages/react/src/components/file-upload/file-upload-label.tsx index 45ad5983a6..f8ca345115 100644 --- a/packages/react/src/components/file-upload/file-upload-label.tsx +++ b/packages/react/src/components/file-upload/file-upload-label.tsx @@ -1,6 +1,6 @@ +import cl from 'clsx/lite'; import { forwardRef, type HTMLAttributes } from 'react'; import { Paragraph, type ParagraphProps } from '../paragraph/paragraph'; -import cl from 'clsx/lite'; export type FileUploadLabelProps = ParagraphProps & HTMLAttributes; From fa60cdc1801d2ac98465e11e491a96d3e2392220 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Oddbj=C3=B8rn=20=C3=98vernes?= Date: Tue, 24 Feb 2026 12:42:29 +0100 Subject: [PATCH 03/14] hover & variant --- packages/css/src/file-upload.css | 6 ++++++ .../src/components/file-upload/file-upload-button.tsx | 7 +++++-- 2 files changed, 11 insertions(+), 2 deletions(-) diff --git a/packages/css/src/file-upload.css b/packages/css/src/file-upload.css index f431334e8f..f8b176906f 100644 --- a/packages/css/src/file-upload.css +++ b/packages/css/src/file-upload.css @@ -11,6 +11,12 @@ &:hover { border-style: solid; background-color: var(--ds-color-surface-tinted); + cursor: pointer; + .ds-button { + /*link or not link hover to button?*/ + --dsc-button-background: var(--dsc-button-background--hover); + --dsc-button-color: var(--dsc-button-color--hover); + } } > * + * { margin-top: var(--ds-size-4); diff --git a/packages/react/src/components/file-upload/file-upload-button.tsx b/packages/react/src/components/file-upload/file-upload-button.tsx index f088a694ef..c987a8f8ce 100644 --- a/packages/react/src/components/file-upload/file-upload-button.tsx +++ b/packages/react/src/components/file-upload/file-upload-button.tsx @@ -8,9 +8,12 @@ export type FileUploadButtonProps = Pick & export const FileUploadButton = forwardRef< HTMLSpanElement, FileUploadButtonProps ->(function FileUploadButton({ className, children, ...rest }, ref) { +>(function FileUploadButton( + { className, variant = 'secondary', children, ...rest }, + ref, +) { return ( - + + )} ); }; From 9be26245ee8f065b910dc05678913347c6706e93 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Oddbj=C3=B8rn=20=C3=98vernes?= Date: Fri, 27 Feb 2026 09:16:54 +0100 Subject: [PATCH 14/14] wider --- .../react/src/components/file-upload/file-upload.stories.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/react/src/components/file-upload/file-upload.stories.tsx b/packages/react/src/components/file-upload/file-upload.stories.tsx index ff8ea525d4..cf53b9f6dd 100644 --- a/packages/react/src/components/file-upload/file-upload.stories.tsx +++ b/packages/react/src/components/file-upload/file-upload.stories.tsx @@ -179,7 +179,7 @@ export const WorkingExample: StoryFn = () => { }; return ( -
+