Skip to content

feat(FileUpload): improve document upload experience#3918

Open
desiree-np wants to merge 3 commits intomainfrom
feat/f0-file-upload-redesign
Open

feat(FileUpload): improve document upload experience#3918
desiree-np wants to merge 3 commits intomainfrom
feat/f0-file-upload-redesign

Conversation

@desiree-np
Copy link
Copy Markdown
Collaborator

Summary

Rework of the file upload UI (FileFieldRenderer + FileAttachment) to be pixel-accurate to the Figma spec. FileUploadItem is removed and replaced by the new FileAttachment component.

Changes

FileAttachment.tsx (new)

Replaces FileUploadItem. Key additions:

  • position prop (single | top | middle | bottom) drives per-card border-radius so cards in a list share a rounded group
  • -mt-px on non-first cards to collapse double borders into a single 1px line
  • Border color: border-f1-border-secondary (normal) / border-f1-border-critical (error)
  • F0AvatarFile size="lg" (40px), filename text-sm font-medium, subtitle text-sm text-f1-foreground-secondary
  • Remove button: F0Button variant="outline" size="sm" hideLabel icon={Cross}

FileFieldRenderer.tsx (rewritten)

  • rounded-xl (16px) dropzone
  • Drag state: border-f1-border-selected-bold bg-f1-background-selected
  • Error state: border-f1-border-critical-bold bg-f1-background-critical/10
  • border-[1px] (not border-2)
  • py-10 (40px) vertical padding, gap-3 between icon and text, gap-4 between dropzone and card list
  • Error hint: AlertCircle icon + text-sm font-medium text-f1-foreground-critical (matches InputMessages pattern)
  • maxFiles support — hides dropzone when limit is reached
  • Dropzone text: text-base (14px)

FileUploadItem.tsx (deleted)

Superseded by FileAttachment.

F0AvatarIcon.tsx

  • md size corrected: size-9 (36px) → size-8 (32px) per Figma

i18n-provider-defaults.ts

  • Added fileWeight: "File weight: {{size}}"
  • Added maxFilesReached: "Maximum {{maxFiles}} files"

F0FormField.stories.tsx

  • Added FileMultiple story

Design reference

Figma: https://www.figma.com/design/pZzg1KTe9lpKTSGPUZa8OJ/Components

  • FileAttachment card: nodes 30003:118744 / 30003:118869
  • DropArea default: node 29978:13631
  • Error state: node 30003:119126

Token decisions

Decision Token used Rationale
Card border (normal) border-f1-border-secondary (6% opacity) Closest available — Figma specifies 14%, no exact token exists
Dropzone border border-f1-border (20% opacity) Matches default dashed outline
Border radius rounded-xl (16px) Applied consistently to dropzone and all cards

Notes

  • F0AvatarIcon md size change (32px) also affects CommunityPost, ActivityItem, CardSelectable — all consume md and will render correctly at the intended Figma size
  • TypeScript: pnpm tsc --noEmit passes with zero errors

@desiree-np desiree-np requested a review from a team as a code owner April 10, 2026 18:50
Copilot AI review requested due to automatic review settings April 10, 2026 18:50
@github-actions github-actions bot added feat react Changes affect packages/react labels Apr 10, 2026
@desiree-np desiree-np changed the title feat(F0Form): redesign file upload components to match Figma feat(FileUpload): match Figma design for drop area and file cards Apr 10, 2026
@desiree-np desiree-np changed the title feat(FileUpload): match Figma design for drop area and file cards feat(FileUpload): improve document upload experience Apr 10, 2026
Copy link
Copy Markdown
Contributor

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

This PR updates the F0Form file upload field UI to match the latest Figma spec by replacing the legacy FileUploadItem row with a new FileAttachment card component and revising the dropzone styles/behavior (including grouped border radii and maxFiles support).

Changes:

  • Introduces FileAttachment (replacing FileUploadItem) with grouped-list positioning and updated visuals.
  • Rewrites FileFieldRenderer dropzone and list rendering to match the new design and adds maxFiles handling.
  • Updates i18n defaults and adjusts F0AvatarIcon md sizing; refreshes Storybook story coverage.

Reviewed changes

Copilot reviewed 7 out of 8 changed files in this pull request and generated 8 comments.

Show a summary per file
File Description
pnpm-lock.yaml Dependency lockfile update (includes Storybook patch bump).
packages/react/src/patterns/F0FormField/stories/F0FormField.stories.tsx Adds a multiple-file story and reformats file.
packages/react/src/patterns/F0Form/fields/file/types.ts Adds maxFiles + updates types; renames upload item props to attachment props.
packages/react/src/patterns/F0Form/fields/file/FileUploadItem.tsx Removes old upload row component.
packages/react/src/patterns/F0Form/fields/file/FileFieldRenderer.tsx Updates dropzone UI/states; renders FileAttachment; adds maxFiles limit logic.
packages/react/src/patterns/F0Form/fields/file/FileAttachment.tsx New attachment card component with grouped border radius support.
packages/react/src/lib/providers/i18n/i18n-provider-defaults.ts Adds file upload strings (fileWeight, maxFilesReached).
packages/react/src/components/avatars/F0AvatarIcon/F0AvatarIcon.tsx Adjusts md avatar icon size to match Figma.
Files not reviewed (1)
  • pnpm-lock.yaml: Language not supported

Comment on lines 235 to 259
const addFiles = useCallback(
(files: File[]) => {
setValidationError(null)
setValidationError(null);

const filesToProcess = isMultiple ? files : [files[0]]
const filesToProcess = isMultiple ? files : [files[0]];

for (const file of filesToProcess) {
const validationMsg = validateFile(file)
const validationMsg = validateFile(file);
if (validationMsg) {
setValidationError(validationMsg)
continue
setValidationError(validationMsg);
continue;
}

if (!resolvedUseUpload) {
console.warn(
"[F0Form] No useUpload hook provided. Pass useUpload to <F0Form> or to the file field config."
)
"[F0Form] No useUpload hook provided. Pass useUpload to <F0Form> or to the file field config.",
);
}

const key = `${file.name}-${file.size}-${Date.now()}-${Math.random()}`
const key = `${file.name}-${file.size}-${Date.now()}-${Math.random()}`;
setEntries((prev) => {
if (!isMultiple) return [{ key, file }]
return [...prev, { key, file }]
})
if (!isMultiple) return [{ key, file }];
return [...prev, { key, file }];
});
}
Copy link

Copilot AI Apr 10, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

maxFiles is only used to hide the dropzone once the limit is reached, but addFiles() can still append more than maxFiles if the user selects/drops multiple files at once (e.g. 2 existing + select 5 with maxFiles=3). This makes maxFiles unenforced and can exceed the UI/UX contract.

Consider capping filesToProcess to the remaining slots when field.maxFiles is set, and surface an i18n error (e.g. translations.maxFilesReached with {{maxFiles}}) when the selection exceeds the limit.

Copilot uses AI. Check for mistakes.
Comment on lines +190 to +194
// In single mode, hide dropzone once a file entry exists.
// In multiple mode, hide dropzone if maxFiles limit is reached.
const isAtLimit =
isMultiple && field.maxFiles != null && entries.length >= field.maxFiles;
const showDropzone = !isAtLimit && (isMultiple || !hasFiles);
Copy link

Copilot AI Apr 10, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

maxFiles introduces new behavior (hiding the dropzone at the limit and limiting additions), but the existing test suite for FileFieldRenderer doesn’t cover it (no maxFiles assertions). Adding tests for: (1) dropzone hidden when entries reach the limit, (2) selecting/dropping more than the remaining slots is prevented and shows maxFilesReached would help prevent regressions.

Copilot generated this review using guidance from repository custom instructions.
Comment on lines 1 to 3
import { useCallback, useId, useMemo, useRef, useState } from "react";
import { ControllerRenderProps, FieldValues } from "react-hook-form";

Copy link

Copilot AI Apr 10, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

packages/react is formatted with oxfmt configured with semi:false (see packages/react/.oxfmtrc.jsonc:4), but this file now includes semicolons throughout. This will likely be rewritten by the formatter and may cause pnpm -C packages/react format:check to fail.

Please re-run the package formatter (e.g. pnpm -C packages/react format) so the diff matches repo formatting conventions.

Copilot uses AI. Check for mistakes.
Comment on lines +1 to +3
import { useCallback, useEffect, useRef, useState } from "react";

import { F0AvatarFile } from "@/components/avatars/F0AvatarFile";
Copy link

Copilot AI Apr 10, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This file uses semicolons, but packages/react formatting is handled by oxfmt with semi:false (packages/react/.oxfmtrc.jsonc:4). Please re-run pnpm -C packages/react format so the diff matches the repo’s formatting conventions (no semicolons).

Copilot uses AI. Check for mistakes.
Comment on lines 1 to 4
import type { Meta, StoryObj } from "@storybook/react-vite";

import { useCallback, useRef, useState } from "react"
import { useCallback, useRef, useState } from "react";

Copy link

Copilot AI Apr 10, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This story file has been reformatted with semicolons, but packages/react uses oxfmt with semi:false (packages/react/.oxfmtrc.jsonc:4). Please re-run pnpm -C packages/react format so formatting stays consistent and format:check doesn’t fail.

Copilot uses AI. Check for mistakes.
Comment on lines 1 to 6
import type {
F0BaseField,
F0BaseFieldRenderIfFunction,
CommonRenderIfCondition,
} from "../types"
} from "../types";

Copy link

Copilot AI Apr 10, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This file now includes semicolons, but packages/react is formatted with oxfmt configured with semi:false (packages/react/.oxfmtrc.jsonc:4). Re-run pnpm -C packages/react format to align with repo formatting and avoid format:check failures.

Copilot uses AI. Check for mistakes.
Comment on lines 1 to 4
import { i18nCountries } from "./partials/countries";

export const defaultTranslations = {
countries: i18nCountries,
Copy link

Copilot AI Apr 10, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This file was reformatted with semicolons, but packages/react uses oxfmt with semi:false (packages/react/.oxfmtrc.jsonc:4). Please re-run pnpm -C packages/react format so the diff matches project formatting conventions.

Copilot uses AI. Check for mistakes.
Comment on lines 1 to 5
import { F0Icon, F0IconProps, IconType } from "@/components/F0Icon";
import { cn } from "@/lib/utils";

import { BaseAvatarProps } from "../internal/BaseAvatar"
import { BaseAvatarProps } from "../internal/BaseAvatar";

Copy link

Copilot AI Apr 10, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This file now uses semicolons, but packages/react is formatted with oxfmt configured with semi:false (packages/react/.oxfmtrc.jsonc:4). Re-run pnpm -C packages/react format to keep formatting consistent.

Copilot uses AI. Check for mistakes.
- Add FileAttachment component with position prop (single/top/middle/bottom)
  for grouped card lists with overlapping borders via -mt-px
- Rewrite FileFieldRenderer: rounded-xl dropzone, drag/error border states,
  AlertCircle error hint, maxFiles support, position-aware card rendering
- Remove FileUploadItem (replaced by FileAttachment)
- Update F0AvatarIcon md size from size-9 to size-8 (36px → 32px) per Figma
- Add fileWeight and maxFilesReached i18n keys
- Add FileMultiple story to F0FormField.stories
@desiree-np desiree-np force-pushed the feat/f0-file-upload-redesign branch from 03061f0 to 9b68e5e Compare April 10, 2026 19:07
@github-actions
Copy link
Copy Markdown
Contributor

github-actions bot commented Apr 10, 2026

✅ No New Circular Dependencies

No new circular dependencies detected. Current count: 0

@github-actions
Copy link
Copy Markdown
Contributor

github-actions bot commented Apr 10, 2026

📦 Alpha Package Version Published

Use pnpm i github:factorialco/f0#npm/alpha-pr-3918 to install the package

Use pnpm i github:factorialco/f0#e477b8c68d7e1aefb67168a319a7e0fc67864cd7 to install this specific commit

@github-actions
Copy link
Copy Markdown
Contributor

github-actions bot commented Apr 10, 2026

🔍 Visual review for your branch is published 🔍

Here are the links to:

@github-actions
Copy link
Copy Markdown
Contributor

github-actions bot commented Apr 10, 2026

Coverage Report for packages/react

Status Category Percentage Covered / Total
🔵 Lines 46.34% 11511 / 24837
🔵 Statements 45.57% 11867 / 26037
🔵 Functions 37.99% 2587 / 6809
🔵 Branches 38.23% 7588 / 19844
File Coverage
File Stmts Branches Functions Lines Uncovered Lines
Changed Files
packages/react/src/components/avatars/F0AvatarIcon/F0AvatarIcon.tsx 100% 100% 100% 100%
packages/react/src/lib/providers/i18n/i18n-provider-defaults.ts 100% 100% 100% 100%
packages/react/src/patterns/F0Form/fields/file/FileFieldRenderer.tsx 88.07% 77.14% 91.66% 89.78% 31, 90-91, 119, 152, 212, 274-276, 285, 310-311, 317-319, 332-335
packages/react/src/patterns/F0Form/fields/file/types.ts 100% 100% 100% 100%
Generated in workflow #12764 for commit 5d0f190 by the Vitest Coverage Report Action

Copilot AI review requested due to automatic review settings April 10, 2026 19:33
Copy link
Copy Markdown
Contributor

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Copilot reviewed 7 out of 7 changed files in this pull request and generated 3 comments.

Comment on lines +190 to +194
// In single mode, hide dropzone once a file entry exists.
// In multiple mode, hide dropzone if maxFiles limit is reached.
const isAtLimit =
isMultiple && field.maxFiles != null && entries.length >= field.maxFiles
const showDropzone = !isAtLimit && (isMultiple || !hasFiles)
Copy link

Copilot AI Apr 10, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

New maxFiles behavior (hiding the dropzone / preventing extra uploads) isn’t covered by tests. Add a unit test that sets multiple: true + maxFiles, uploads/drops more than the limit, and asserts the limit is enforced (and that the dropzone/message state is correct).

Copilot generated this review using guidance from repository custom instructions.
Comment on lines +1 to +4
import userEvent from "@testing-library/user-event";
import React from "react";
import { describe, expect, it, vi } from "vitest";
import { z } from "zod";
Copy link

Copilot AI Apr 10, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This test file has been reformatted with semicolons, but packages/react/.oxfmtrc.jsonc sets semi: false. Please run the package formatter (or revert to the no-semicolon style) to keep formatting consistent and avoid format-check CI failures.

Copilot uses AI. Check for mistakes.
fileTooLarge: "File exceeds {{maxSize}} MB limit",
invalidFileType: "File type not accepted. Accepted formats: {{types}}",
fileWeight: "File weight: {{size}}",
maxFilesReached: "Maximum {{maxFiles}} files",
Copy link

Copilot AI Apr 10, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

maxFilesReached was added to the default i18n translations but is not referenced anywhere in packages/react/src (dead key). Either wire it into the file field UX (e.g., when reaching/exceeding maxFiles) or remove it to avoid unused translation drift.

Suggested change
maxFilesReached: "Maximum {{maxFiles}} files",

Copilot uses AI. Check for mistakes.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

feat react Changes affect packages/react

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants