Skip to content

feat: composable primitive components #947

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Open
wants to merge 48 commits into
base: future-uploadFiles
Choose a base branch
from

Conversation

veloii
Copy link

@veloii veloii commented Sep 14, 2024

This PR adds unstyled composable components through generateUploadPrimitives which returns:

  • <Root>
  • <Dropzone>
  • <Button>
  • <AllowedContent>

I've also added examples for minimal-appdir, minimal-pagedir, with-clerk-appdir, with-clerk-pagedir. I can add it to all the rest of the react ones if needed.

All these components accept parameters as children like ({ isUploading }) => <></>.
There is no asChild or as={YourComponent} prop at the moment as I'm not sure which one to use.
I haven't implemented the docs either as I'm not sure if the current styling page should be split up.

Summary by CodeRabbit

  • New Features

    • Introduced unstyled "primitive" upload components, including Root, Button, Dropzone, AllowedContent, and ClearButton, enabling full customization of upload interfaces.
    • Added new upload experiences to example apps and playground, showcasing drag-and-drop, manual and auto upload modes, and allowed file type displays.
    • Exposed a function to generate these primitives for use in custom applications.
  • Documentation

    • Expanded theming documentation to explain usage and customization of the new primitive components, with detailed examples.
  • Refactor

    • Refactored existing upload button and dropzone components to delegate logic to the new primitive components, simplifying their structure and improving flexibility.
  • Chores

    • Updated exports and internal utilities to support the new primitive upload system.

Copy link

changeset-bot bot commented Sep 14, 2024

⚠️ No Changeset found

Latest commit: 2552d05

Merging this PR will not cause a version bump for any packages. If these changes should not result in a new version, you're good to go. If these changes should result in a version bump, you need to add a changeset.

This PR includes no changesets

When changesets are added to this PR, you'll see the packages that this PR includes changesets for and the associated semver types

Click here to learn what changesets are, and how to add one.

Click here if you're a maintainer who wants to add a changeset to this PR

Copy link

vercel bot commented Sep 14, 2024

The latest updates on your projects. Learn more about Vercel for Git ↗︎

Name Status Preview Comments Updated (UTC)
docs-uploadthing ✅ Ready (Inspect) Visit Preview 💬 Add feedback Apr 25, 2025 7:19pm
1 Skipped Deployment
Name Status Preview Comments Updated (UTC)
legacy-docs-uploadthing ⬜️ Ignored (Inspect) Visit Preview Apr 25, 2025 7:19pm

@markflorkowski
Copy link
Collaborator

I like how this is looking! I would probably add some basic styling to the examples, to at least identify the component, since right now, it kind of just looks like the page is broken, rather than there is another dropzone:
image

I've also added examples for minimal-appdir, minimal-pagedir, with-clerk-appdir, with-clerk-pagedir. I can add it to all the rest of the react ones if needed.

I think this is plenty in the examples, i'd potentially want to add an example with-composable-components that shows off an example of what one could do with them, but I wouldn't block on this.

I haven't implemented the docs either as I'm not sure if the current styling page should be split up.

Tentatively I would say it doesn't need to be split for now, but cc @juliusmarminge -- thoughts?

@juliusmarminge
Copy link
Member

Tentatively I would say it doesn't need to be split for now, but cc @juliusmarminge -- thoughts?

Kinda feels like it should be split?


Should our default component use these primitives? A lot of code duplication if not?

@markflorkowski
Copy link
Collaborator

Kinda feels like it should be split?

How would you want it split? I felt like the sub sections in theming wasn't bad
image

Should our default component use these primitives? A lot of code duplication if not?

This is a good point, and also makes sure that they stay in sync. I don't see why we couldn't.

@juliusmarminge
Copy link
Member

How would you want it split? I felt like the sub sections in theming wasn't bad

I guess not, perhaps a leading section with the level of theming possible with links to respective section could be sufficient

Copy link
Contributor

coderabbitai bot commented Sep 20, 2024

Walkthrough

This update introduces a new set of unstyled "primitive" upload components for React, including Root, Button, Dropzone, AllowedContent, and ClearButton. These primitives are exposed through a new generateUploadPrimitives function, allowing developers to build fully custom upload flows with granular control over UI and behavior. Existing styled components (UploadButton, UploadDropzone) are refactored to delegate their internal logic to these primitives, simplifying their implementation. The documentation is expanded to explain the new primitives and provide usage examples. Example apps and playground code are updated to demonstrate and utilize the new primitives.

Changes

File(s) Change Summary
packages/react/src/components/primitive/root.tsx
button.tsx
dropzone.tsx
clear-button.tsx
allowed-content.tsx
Introduce primitive upload components: Root, Button, Dropzone, AllowedContent, ClearButton, and supporting context/state logic.
packages/react/src/components/button.tsx
dropzone.tsx
Refactor UploadButton and UploadDropzone to delegate upload logic and state to primitive components via render prop pattern.
packages/react/src/components/index.tsx
packages/react/src/index.ts
Export generateUploadPrimitives, update exports to include new primitives, and adjust dropzone export path.
packages/react/src/utils/useControllableState.ts Add a new hook for controlled/uncontrolled state management.
docs/src/app/(docs)/concepts/theming/page.mdx Expand documentation to cover the new primitive components, with usage examples and explanations.
examples/minimal-appdir/src/app/page.tsx
examples/minimal-pagedir/src/pages/index.tsx
examples/with-clerk-appdir/src/app/page.tsx
examples/with-clerk-pagesdir/src/pages/index.tsx
Update example apps to use and demonstrate the new upload primitives in their upload flows.
playground/app/layout.tsx Refactor navigation markup for clarity; not directly related to upload primitives.
playground/app/primitives/page.tsx Add a new page demonstrating a custom uploader built with the new primitives.
playground/components/text.tsx Add reusable text/heading components for playground UI.
playground/components/uploader.tsx Remove old upload helper usage; switch to using primitives via a local module.
playground/lib/uploadthing.ts Add a local bridge module exporting UT primitives, UTButton, and useUploadThing for playground use.

Sequence Diagram(s)

sequenceDiagram
    participant User
    participant Root as UT.Root
    participant Dropzone as UT.Dropzone
    participant Button as UT.Button
    participant AllowedContent as UT.AllowedContent

    User->>Dropzone: Drag and drop or select file
    Dropzone->>Root: Notify files selected
    Root->>Button: Enable upload action
    User->>Button: Click to start upload
    Button->>Root: Trigger upload process
    Root->>Dropzone: Update progress/state
    Root->>AllowedContent: Show allowed file types
    Root->>User: Notify on upload completion
Loading

Possibly related PRs

  • pingdotgg/uploadthing#980: Fixes internal dropzone event handling and multiple file upload bugs, closely related to the dropzone logic refactored in this PR.
  • pingdotgg/uploadthing#1151: Refines upload progress granularity and styling in upload components, which are now built on the primitives introduced here.

Suggested reviewers

  • markflorkowski

Warning

There were issues while running some tools. Please review the errors and either fix the tool's configuration or disable the tool if it's a critical failure.

🔧 ESLint

If the error stems from missing dependencies, add them to the package.json file. For unrecoverable errors (e.g., due to private dependencies), disable the tool in the CodeRabbit configuration.

Scope: all 3 workspace projects
Progress: resolved 0, reused 0, downloaded 1, added 0
/tmp/eslint/packages/uploadthing:
 ERR_PNPM_WORKSPACE_PKG_NOT_FOUND  In packages/uploadthing: "@uploadthing/mime-types@workspace:*" is in the dependencies but no package named "@uploadthing/mime-types" is present in the workspace

This error happened while installing a direct dependency of /tmp/eslint/packages/uploadthing

Packages found in the workspace:

✨ Finishing Touches
  • 📝 Generate Docstrings

🪧 Tips

Chat

There are 3 ways to chat with CodeRabbit:

  • Review comments: Directly reply to a review comment made by CodeRabbit. Example:
    • I pushed a fix in commit <commit_id>, please review it.
    • Generate unit testing code for this file.
    • Open a follow-up GitHub issue for this discussion.
  • Files and specific lines of code (under the "Files changed" tab): Tag @coderabbitai in a new review comment at the desired location with your query. Examples:
    • @coderabbitai generate unit testing code for this file.
    • @coderabbitai modularize this function.
  • PR comments: Tag @coderabbitai in a new PR comment to ask questions about the PR branch. For the best results, please provide a very specific query, as very limited context is provided in this mode. Examples:
    • @coderabbitai gather interesting stats about this repository and render them as a table. Additionally, render a pie chart showing the language distribution in the codebase.
    • @coderabbitai read src/utils.ts and generate unit testing code.
    • @coderabbitai read the files in the src/scheduler package and generate a class diagram using mermaid and a README in the markdown format.
    • @coderabbitai help me debug CodeRabbit configuration file.

Note: Be mindful of the bot's finite context window. It's strongly recommended to break down tasks such as reading entire modules into smaller chunks. For a focused discussion, use review comments to chat about specific files and their changes, instead of using the PR comments.

CodeRabbit Commands (Invoked using PR comments)

  • @coderabbitai pause to pause the reviews on a PR.
  • @coderabbitai resume to resume the paused reviews.
  • @coderabbitai review to trigger an incremental review. This is useful when automatic reviews are disabled for the repository.
  • @coderabbitai full review to do a full review from scratch and review all the files again.
  • @coderabbitai summary to regenerate the summary of the PR.
  • @coderabbitai generate docstrings to generate docstrings for this PR.
  • @coderabbitai generate sequence diagram to generate a sequence diagram of the changes in this PR.
  • @coderabbitai resolve resolve all the CodeRabbit review comments.
  • @coderabbitai configuration to show the current CodeRabbit configuration for the repository.
  • @coderabbitai help to get help.

Other keywords and placeholders

  • Add @coderabbitai ignore anywhere in the PR description to prevent this PR from being reviewed.
  • Add @coderabbitai summary to generate the high-level summary at a specific location in the PR description.
  • Add @coderabbitai anywhere in the PR title to generate the title automatically.

CodeRabbit Configuration File (.coderabbit.yaml)

  • You can programmatically configure CodeRabbit by adding a .coderabbit.yaml file to the root of your repository.
  • Please see the configuration documentation for more information.
  • If your editor has YAML language server enabled, you can add the path at the top of this file to enable auto-completion and validation: # yaml-language-server: $schema=https://coderabbit.ai/integrations/schema.v2.json

Documentation and Community

  • Visit our Documentation for detailed information on how to use CodeRabbit.
  • Join our Discord Community to get help, request features, and share feedback.
  • Follow us on X/Twitter for updates and announcements.

@veloii
Copy link
Author

veloii commented Sep 20, 2024

I'm currently refactoring the prebuilt components to use the primitives. For the dropzone, you can get the isDragActive property through the children as a parameter or if you want to change a className based on it you use the data-drag attribute (ex: data-[drag]:bg-blue-500/30). In the current prebuilt button style API the isDragActive property is passed a parameter for the class but we can't access it.

Exmaple:

<Primitive.Dropzone
  className={cn(
    "data-[drag]:bg-blue-600/10",
    className,
    // isDragActive isn't defined here
    styleFieldToClassName(appearance?.container, { isDragActive }),
  )}
  // or here
  style={styleFieldToCssObject(appearance?.container, { isDragActive )}
>
  {({ dropzone: { isDragActive } }) => /* we have it here */}
</Primitive.Dropzone>

Should the className and style property accept a parameter like?

<Primitive.Dropzone className={({ dropzone: { isDragActive }) => "your class"} />

This feels a bit hacky imo as it still means you can't use isDragActive to compute any other property (unless we did an Object.entries on the props and made a ts helper to convert all the normal attributes to accept functions too). Another way is with a controlled parameter? Imo this feels better.

<Primitive.Dropzone
  dropzone={dropzone}
  onDropzoneChange={setDropzone}
  className={`${dropzone.isDragActive ? "bg-blue-500/10" : ""`}
/>

@veloii
Copy link
Author

veloii commented Sep 21, 2024

cc @juliusmarminge

@juliusmarminge
Copy link
Member

juliusmarminge commented Sep 25, 2024

We hope to be able to get this in a minor release, meaning no breaking changes so
styleFieldToClassName($props.appearance?.container, styleFieldArg) should if possible work as before.

Can we not just wrap in another div or something?

<Primitive.Dropzone>
  {({ dropzone: { isDragActive } }) => (
    <div
    className={cn(
      "data-[drag]:bg-blue-600/10",
      className,
      // isDragActive isn't defined here
      styleFieldToClassName(appearance?.container, { isDragActive }),
    )}
    // or here
    style={styleFieldToCssObject(appearance?.container, { isDragActive )}
   >
     ...
   </div>
</Primitive.Dropzone>

Copy link
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 0

🧹 Outside diff range and nitpick comments (3)
packages/react/src/components/index.tsx (1)

63-81: Add JSDoc documentation for the new function.

Consider adding JSDoc documentation to explain the purpose, usage, and return value of generateUploadPrimitives. This would help developers understand how to use the composable primitive components effectively.

Example documentation:

/**
 * Generates unstyled composable upload components that can be customized.
 * @param opts - Optional configuration options for the upload components
 * @returns An object containing primitive components: Root, Dropzone, Button, and AllowedContent
 * @example
 * const UT = generateUploadPrimitives();
 * 
 * function Upload() {
 *   return (
 *     <UT.Root endpoint="imageUploader">
 *       <UT.Dropzone>
 *         {({ isUploading }) => (
 *           <UT.Button>Upload a file</UT.Button>
 *         )}
 *       </UT.Dropzone>
 *     </UT.Root>
 *   );
 * }
 */
docs/src/app/(docs)/concepts/theming/page.mdx (2)

22-23: Fix grammatical issues in the introduction.

There are a couple of grammatical issues that should be addressed:

  1. "These comes" should be "These come" (subject-verb agreement)
  2. Add a comma before "but" as it connects two independent clauses
-These comes with behavior built-in but you have full control over what's rendered when and where.
+These come with behavior built-in, but you have full control over what's rendered when and where.
🧰 Tools
🪛 LanguageTool

[grammar] ~22-~22: The verb ‘comes’ is singular. Did you mean: “this comes” or “These come”?
Context: ...tives](#unstyled-primitive-components). These comes with behavior built-in but you have ful...

(SINGULAR_VERB_AFTER_THESE_OR_THOSE)


[uncategorized] ~23-~23: Use a comma before ‘but’ if it connects two independent clauses (unless they are closely connected and short).
Context: ...nts). These comes with behavior built-in but you have full control over what's rende...

(COMMA_COMPOUND_SENTENCE)


527-612: Consider enhancing the Unstyled Primitive Components documentation.

While the current documentation provides a good foundation, consider adding:

  1. A complete list of available state properties that can be accessed in children functions
  2. Examples of manual mode usage for both Button and Dropzone
  3. Examples of using the ClearButton component
  4. TypeScript examples showing proper type inference

Would you like me to help generate these additional documentation sections?

📜 Review details

Configuration used: CodeRabbit UI
Review profile: CHILL

📥 Commits

Files that changed from the base of the PR and between aac8916 and 11f77eb.

📒 Files selected for processing (2)
  • docs/src/app/(docs)/concepts/theming/page.mdx (2 hunks)
  • packages/react/src/components/index.tsx (2 hunks)
🧰 Additional context used
🪛 LanguageTool
docs/src/app/(docs)/concepts/theming/page.mdx

[grammar] ~22-~22: The verb ‘comes’ is singular. Did you mean: “this comes” or “These come”?
Context: ...tives](#unstyled-primitive-components). These comes with behavior built-in but you have ful...

(SINGULAR_VERB_AFTER_THESE_OR_THOSE)


[uncategorized] ~23-~23: Use a comma before ‘but’ if it connects two independent clauses (unless they are closely connected and short).
Context: ...nts). These comes with behavior built-in but you have full control over what's rende...

(COMMA_COMPOUND_SENTENCE)


[style] ~27-~27: Opting for a less wordy alternative here can improve the clarity of your writing.
Context: ...e-upload-thing) hook that allows you to not only customize the look but also have full control of the behavior. ## Uploa...

(NOT_ONLY_ALSO)

🔇 Additional comments (3)
packages/react/src/components/index.tsx (2)

17-18: LGTM! Clean import statements for new primitive components.

The imports are well-structured and follow TypeScript best practices.


63-81: Verify the primitive components implementation.

Let's ensure all the primitive components mentioned in the PR objectives are properly implemented.

✅ Verification successful

All primitive components are properly implemented

The verification confirms that all required primitive components are properly implemented in the codebase:

  1. Root component (root.tsx): Implements the core upload functionality, context provider, and state management
  2. Dropzone component (dropzone.tsx): Implements drag-and-drop functionality with proper file validation
  3. Button component (button.tsx): Implements file selection and upload trigger functionality
  4. AllowedContent component (allowed-content.tsx): Implements content display for allowed file types

Each component follows best practices with proper TypeScript typing, client-side directive, and accessibility considerations. The implementation matches the PR objectives and maintains consistency across the component suite.

🏁 Scripts executed

The following scripts were executed for the analysis:

Script:

#!/bin/bash
# Description: Verify the existence and implementation of primitive components

# Test 1: Check if all primitive components exist
echo "Checking for primitive component files..."
fd --type f --glob "*.tsx" --glob "*.ts" . packages/react/src/components/primitive

# Test 2: Verify the exports from primitive/index
echo -e "\nChecking primitive exports..."
rg --type ts "export.*from" packages/react/src/components/primitive/index.ts

# Test 3: Look for component implementations
echo -e "\nChecking component implementations..."
for component in Root Dropzone Button AllowedContent; do
  echo -e "\nLooking for $component implementation..."
  ast-grep --pattern "export function $component" --pattern "export const $component"
done

Length of output: 8324


Script:

#!/bin/bash
# Let's verify each primitive component individually

# Check Root component implementation
echo "Checking Root component..."
ast-grep --pattern 'export const Root = '

echo -e "\nChecking Dropzone component..."
ast-grep --pattern 'export const Dropzone = '

echo -e "\nChecking Button component..."
ast-grep --pattern 'export const Button = '

echo -e "\nChecking AllowedContent component..."
ast-grep --pattern 'export const AllowedContent = '

# Check exports from primitive/index.ts
echo -e "\nChecking primitive/index.ts contents..."
cat packages/react/src/components/primitive/index.ts

# Check individual component files
echo -e "\nChecking individual component implementations..."
for file in root.tsx dropzone.tsx button.tsx allowed-content.tsx; do
    echo -e "\nContents of packages/react/src/components/primitive/$file:"
    cat "packages/react/src/components/primitive/$file"
done

Length of output: 24707

docs/src/app/(docs)/concepts/theming/page.mdx (1)

585-602: Update Dropzone example for accuracy and completeness.

The example needs several improvements:

  1. Remove as prop usage since it's not implemented yet (as mentioned in PR objectives)
  2. Add error handling example
  3. Demonstrate more state properties
<UT.Root endpoint="mockRoute">
  <UT.Dropzone>
-    {({ state }) => (
+    {({ state, files, error }) => (
      <div>
        <p>Drag and drop</p>
+        {error && <p style={{ color: 'red' }}>{error.message}</p>}
+        {files.length > 0 && (
+          <p>Selected files: {files.map(f => f.name).join(', ')}</p>
+        )}
-        <UT.Button as="button">
+        <UT.Button>
          {state === "uploading" ? "Uploading" : "Upload file"}
        </UT.Button>
        <UT.AllowedContent
-          as="p"
          style={{ fontSize: 12, width: "fit-content" }}
        />
      </div>
    )}
  </UT.Dropzone>
</UT.Root>

@juliusmarminge
Copy link
Member

Just played with this a bit and I love this - found some issues with pasting files. I'll try and have a look at fixing that when i got some time

Copy link
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 2

🧹 Outside diff range and nitpick comments (14)
playground/lib/uploadthing.ts (2)

9-11: Add JSDoc comments to document the exported entities

The exports look well-structured and type-safe, but they would benefit from JSDoc comments explaining their purpose and usage patterns, especially since these are foundational primitives.

Here's a suggested improvement:

+/**
+ * Primitive components for building custom upload UI
+ * @example
+ * const MyUploader = () => (
+ *   <UT.Root>
+ *     <UT.Dropzone>
+ *       {({ isUploading }) => <div>{isUploading ? 'Uploading...' : 'Drop files'}</div>}
+ *     </UT.Dropzone>
+ *   </UT.Root>
+ * );
+ */
 export const UT = generateUploadPrimitives<UploadRouter>();
+
+/** Pre-built button component for quick implementation */
 export const UTButton = generateUploadButton<UploadRouter>();
+
+/** Hook for custom upload implementations */
 export const { useUploadThing } = generateReactHelpers<UploadRouter>();

9-9: Consider a more explicit name for primitive components

While UT is concise, a more explicit name like UploadPrimitives might better convey its purpose, especially for new developers encountering the codebase.

playground/app/primitives/page.tsx (1)

26-38: Consider extracting the dropzone UI into a separate component

The inline JSX within the dropzone render prop is becoming complex. Consider extracting it into a separate component for better maintainability and reusability.

type DropzoneContentProps = {
  dropzone: { isDragActive: boolean };
  state: string;
  uploadProgress: number;
};

function DropzoneContent({ dropzone, state, uploadProgress }: DropzoneContentProps) {
  return (
    <div
      data-dragactive={dropzone?.isDragActive}
      className="flex flex-col items-center rounded-md border-2 border-dashed border-zinc-200 p-4 data-[dragactive=true]:border-zinc-400"
    >
      <Subheading>Drag and drop</Subheading>
      <UT.Button as={Button} color="red" className="mt-2 px-4 py-2">
        {state === "uploading" ? "Uploading" : "Upload file"}
        {!!uploadProgress && ` ${uploadProgress}%`}
      </UT.Button>
      <UT.AllowedContent as={Code} className="mt-2" />
    </div>
  );
}
playground/components/text.tsx (5)

1-6: LGTM! Consider adding readonly modifier for better type safety.

The imports and type definitions are well-structured. For additional type safety, consider making the level union type readonly:

type HeadingProps = {
-  level?: 1 | 2 | 3 | 4 | 5 | 6;
+  level?: readonly [1, 2, 3, 4, 5, 6][number];
}

8-20: LGTM! Consider adding aria-level for accessibility.

The component implementation is solid with proper type safety and responsive design.

<Element
  {...props}
+ aria-level={level}
  className={cx(
    "text-2xl/8 font-semibold text-zinc-950 sm:text-xl/8",
    className,
  )}
/>

36-44: LGTM! Document the purpose of data-slot attribute.

The implementation is clean and follows best practices. Consider adding JSDoc to explain the purpose of the data-slot attribute.

+/**
+ * Text component that renders a paragraph with consistent styling.
+ * @param data-slot - Used for component targeting in the design system
+ */
export function Text({ className, ...props }: React.ComponentProps<"p">) {

46-59: Consider using :hover pseudo-class for better accessibility.

The component correctly wraps Next.js Link, but the data-hover approach might not work with all input devices.

<Link
  {...props}
  className={cx(
-   "text-zinc-950 underline decoration-zinc-950/50 data-[hover]:decoration-zinc-950",
+   "text-zinc-950 underline decoration-zinc-950/50 hover:decoration-zinc-950 focus-visible:decoration-zinc-950",
    className,
  )}
/>

73-83: Consider adding semantic improvements for code elements.

The styling provides good visual distinction, but could benefit from additional semantic attributes.

<code
  {...props}
+ translate="no"
+ role="code"
  className={cx(
    "rounded border border-zinc-950/10 bg-zinc-950/[2.5%] px-0.5 text-sm font-medium text-zinc-950 sm:text-[0.8125rem]",
    className,
  )}
/>

The translate="no" attribute prevents translation tools from modifying code content, and role="code" improves semantic meaning for screen readers.

playground/components/uploader.tsx (4)

60-61: Consider adding endpoint validation

The endpoint callback is using a generic anything type without validation. Consider adding type safety and validation for the endpoint configuration.

-endpoint={(rr) => rr.anything}
-input={{}}
+endpoint={(rr) => {
+  if (!rr.anything) {
+    throw new Error("Endpoint configuration is invalid");
+  }
+  return rr.anything;
+}}
+input={{ /* Add input validation schema here */ }}

73-76: Review CSS specificity overrides

The appearance props use ! to override styles. This might make it harder to maintain and customize styles in the future.

Consider using a more maintainable approach:

-appearance={{
-  button: "!text-sm/6",
-  allowedContent: "!h-0",
-}}
+appearance={{
+  button: "text-sm leading-6",
+  allowedContent: "h-0",
+}}

62-67: Enhance error handling and success feedback

The current implementation uses basic window.alert for errors and only refreshes the page on success. Consider implementing a more user-friendly feedback mechanism.

 onUploadError={(error) => {
-  window.alert(error.message);
+  // Consider using a toast notification or error boundary
+  console.error('Upload failed:', error);
+  // Add user-friendly error display
 }}
 onClientUploadComplete={() => {
+  // Consider adding success feedback before refresh
   router.refresh();
 }}

55-80: Consider extracting upload configuration

The component mixes presentation with upload configuration. Consider extracting the upload configuration into a separate configuration object or custom hook for better maintainability.

Example approach:

// useUploadConfig.ts
export const useUploadConfig = () => {
  const router = useRouter();
  return {
    endpoint: (rr) => rr.anything,
    onError: (error) => { /* error handling */ },
    onSuccess: () => router.refresh(),
    // ... other config
  };
};

// In component:
const uploadConfig = useUploadConfig();
<UTButton {...uploadConfig} />
playground/lib/actions.ts (1)

84-86: Consider extracting the expiration time constant and documenting the duration.

While adding URL expiration is a good security practice, the magic number calculation could be more readable and maintainable.

Consider applying this change:

-  const { url } = await utapi.getSignedURL(key, {
-    expiresIn: 60 * 60 * 24 * 7,
-  });
+  // URL expires in 7 days
+  const SIGNED_URL_EXPIRATION = 60 * 60 * 24 * 7; // seconds
+  const { url } = await utapi.getSignedURL(key, {
+    expiresIn: SIGNED_URL_EXPIRATION,
+  });
playground/app/layout.tsx (1)

70-73: Enhance navigation accessibility and user experience.

Consider the following improvements:

  1. Add aria-label to the nav element for better screen reader support
  2. Add visual indication for the current active route
  3. Include hover states for links
-      <nav className="flex gap-4 bg-zinc-100 p-4">
+      <nav 
+        aria-label="Main navigation" 
+        className="flex gap-4 bg-zinc-100 p-4"
+      >
-        <Link href="/">Home</Link>
-        <Link href="/primitives">Primitives</Link>
+        <Link 
+          href="/" 
+          className="hover:underline"
+        >
+          Home
+        </Link>
+        <Link 
+          href="/primitives"
+          className="hover:underline"
+        >
+          Primitives
+        </Link>
       </nav>
📜 Review details

Configuration used: CodeRabbit UI
Review profile: CHILL

📥 Commits

Reviewing files that changed from the base of the PR and between 11f77eb and d1291ff.

📒 Files selected for processing (7)
  • package.json (1 hunks)
  • playground/app/layout.tsx (2 hunks)
  • playground/app/primitives/page.tsx (1 hunks)
  • playground/components/text.tsx (1 hunks)
  • playground/components/uploader.tsx (2 hunks)
  • playground/lib/actions.ts (1 hunks)
  • playground/lib/uploadthing.ts (1 hunks)
🔇 Additional comments (13)
playground/lib/uploadthing.ts (2)

9-11: LGTM! Type-safe implementation

The consistent use of the UploadRouter type parameter ensures type safety across all exported entities. This will provide excellent TypeScript support for consumers of these primitives.


1-7: Consider making the import path more resilient

The relative import path might be brittle if the file structure changes. Consider using path aliases (e.g., @/app/api/uploadthing/route) to make imports more maintainable.

Let's check for potential circular dependencies and verify the route file location:

playground/app/primitives/page.tsx (3)

1-7: LGTM: Appropriate setup for client-side component

The imports and client-side directive are correctly configured for an interactive file upload component.


44-50: LGTM: Clean and focused page component

The Page component follows the single responsibility principle and provides appropriate layout spacing.


11-12: Consider restricting the upload endpoint and configuring input parameters

The current endpoint configuration allows "anything" which might be too permissive. Consider restricting to specific file types or sizes based on your requirements.

playground/components/text.tsx (1)

61-71: Verify text-foreground class in Tailwind configuration.

The implementation is clean, but let's ensure the text-foreground utility class is properly configured.

✅ Verification successful

The output shows that foreground is properly defined in the Tailwind configuration as "hsl(var(--foreground))", which means the text-foreground utility class is correctly configured and available for use in the Strong component.

The text-foreground utility class is properly configured in the Tailwind theme.

The class is defined using CSS variables and HSL color format, following a consistent pattern used across other color utilities in the theme configuration.

🏁 Scripts executed

The following scripts were executed for the analysis:

Script:

#!/bin/bash
# Check if text-foreground is defined in Tailwind config
fd -e js -e ts -e json -g "*tailwind*config*" | xargs cat | grep -A 5 "foreground"

Length of output: 1125

package.json (1)

29-29: LGTM! Script addition follows established patterns.

The new dev:playground script follows the existing pattern of development scripts and will help in testing the new primitive components.

Let's verify the playground setup:

✅ Verification successful

The playground setup is properly configured and integrated

The verification confirms:

  • The playground directory exists and contains the expected Next.js files (layout.tsx and page.tsx)
  • The new primitive components are properly imported and used in the playground, specifically in playground/app/primitives/page.tsx
  • The setup aligns with other examples in the repository and correctly implements the new UT components
🏁 Scripts executed

The following scripts were executed for the analysis:

Script:

#!/bin/bash
# Description: Verify the next-playground directory exists and contains necessary files

# Check if next-playground directory exists in examples
fd -t d "next-playground" examples/

# Check for typical Next.js files in the playground
fd -t f "^(page|layout)\.(tsx|js|jsx)$" -E "*.test.*" -E "*.spec.*"

# Check if the new primitive components are imported
rg -l "generateUploadPrimitives|UT\.(Root|Dropzone|Button|AllowedContent)"

Length of output: 1881

playground/components/uploader.tsx (2)

68-72: LGTM: Composable content implementation

The content prop implementation using render props pattern aligns well with the PR objectives for composable components. This allows for flexible customization while maintaining type safety.


7-7: Verify the local import path for UTButton

The import has been moved from @uploadthing/react to a local path. Let's ensure this path is correct and the component exists.

✅ Verification successful

Import path for UTButton is correct and verified

The UTButton component is correctly exported from playground/lib/uploadthing.ts, and the relative import path "../lib/uploadthing" in playground/components/uploader.tsx is accurate.

🏁 Scripts executed

The following scripts were executed for the analysis:

Script:

#!/bin/bash
# Check if the UTButton export exists in the local path
rg -l "export.*UTButton" "lib/uploadthing"

Length of output: 98


Script:

#!/bin/bash
# Let's first find the correct path by searching for files containing "UTButton"
rg -l "UTButton"

# Also search for any file named "uploadthing" to verify the correct path
fd uploadthing

Length of output: 3341


Script:

#!/bin/bash
# Let's check the content of playground/lib/uploadthing.ts to verify UTButton export
rg "UTButton" "playground/lib/uploadthing.ts" -C 2

Length of output: 243

playground/lib/actions.ts (2)

84-86: Verify the URL expiration time requirements.

The 7-day expiration time for signed URLs should be validated against:

  1. Application requirements for URL validity
  2. Security policies regarding temporary access
  3. User experience expectations

Consider making this duration configurable through environment variables if different environments need different expiration times.

Let's check if this duration is documented or configurable elsewhere:

✅ Verification successful

7-day expiration time is within the documented limits and follows library constraints

The expiration time of 7 days (60 * 60 * 24 * 7 seconds) is:

  • Explicitly enforced by the library which throws an error if expiration exceeds 7 days
  • Well documented in the API reference
  • Configurable through the expiresIn parameter which accepts both numeric seconds or human-readable time strings
  • Matches the maximum allowed duration (604800 seconds) as per the library's validation

The implementation is using the maximum allowed duration, which is a reasonable default for temporary access URLs. If different durations are needed, they can be configured per request as documented.

🏁 Scripts executed

The following scripts were executed for the analysis:

Script:

#!/bin/bash
# Search for any documentation or configuration related to URL expiration
rg -i "url.*expir|signed.*url" --type md
rg "process\.env.*EXPIR|config.*EXPIR" 

Length of output: 290


Script:

#!/bin/bash
# Let's check for any mentions of URL expiration or signed URLs in the codebase
rg -A 5 "getSignedURL|signedUrl|expiresIn" 

# Also check for any configuration or constant definitions
ast-grep --pattern 'const $_ = $$$* 60 * 60 * 24'

Length of output: 19204


84-86: Verify error handling for expired URLs.

Ensure that components consuming these signed URLs handle expiration gracefully. The application should:

  1. Detect expired URLs
  2. Provide appropriate user feedback
  3. Allow easy URL refresh when needed

Let's check how URL expiration errors are handled:

✅ Verification successful

URL expiration is properly handled in the application

The codebase already has appropriate error handling mechanisms in place:

  • The uploader component has error handling that displays alerts to users when issues occur (onUploadError)
  • The actions have proper error states and return structured error responses
  • The URL expiration time is set to a reasonable 7 days, which is within the allowed limits as verified by the test cases
  • Components using these URLs have error states and user feedback mechanisms

The implementation follows best practices for handling URL-related errors, including expired URLs, through the existing error handling infrastructure.

🏁 Scripts executed

The following scripts were executed for the analysis:

Script:

#!/bin/bash
# Search for error handling related to URL expiration
ast-grep --pattern 'catch ($_) {
  $$$
}'

# Search for error messages related to URL expiration
rg -i "url.*expired|link.*expired"

Length of output: 80


Script:

#!/bin/bash
# Let's search for any error handling around the getSignedURL usage
rg -A 5 "getSignedURL" 

# Also check for any error boundaries or error handling in components that might use these URLs
rg -A 5 "catch|throw" --type typescript

# Search for any error states or loading states in components
ast-grep --pattern 'const [$_] = useState<$_>($$$)'

Length of output: 6974


Script:

#!/bin/bash
# Let's check for error handling in the specific file and its imports
rg -A 5 "catch|throw" playground/lib/actions.ts

# Check for any error states or loading states in components that might use this action
rg -A 5 "actions.ts" playground/

# Look for any error handling in components
rg -A 5 "error|Error" playground/

Length of output: 4241

playground/app/layout.tsx (2)

51-61: Well-implemented loading state!

The Suspense fallback implementation follows best practices by:

  • Maintaining layout stability
  • Providing clear visual feedback
  • Matching the structure of the loaded content

66-68: Review security implications of displaying token information.

Exposing the appId and region information in the UI might not be necessary in a production environment. Consider:

  1. Moving this information to environment variables
  2. Only showing it in development mode
  3. Or removing it entirely if not needed for debugging

@veloii
Copy link
Author

veloii commented Nov 27, 2024

Thanks! In the meantime, should I fix the linting issues?

Copy link
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 1

🧹 Nitpick comments (7)
packages/react/src/components/primitive/clear-button.tsx (1)

30-35: Enhance error handling and user experience.

Consider adding error handling and user feedback when clearing files:

 onClick={(e) => {
   if (state === "disabled") return;
   // eslint-disable-next-line @typescript-eslint/no-unsafe-call
   onClick?.(e);
-  setFiles([]);
+  try {
+    setFiles([]);
+  } catch (error) {
+    console.error("Failed to clear files:", error);
+    // Consider adding an onError prop to handle errors
+  }
 }}
packages/react/src/index.ts (1)

7-7: Add usage documentation for generateUploadPrimitives.

This new export looks good, aligning well with the composable approach outlined in the PR. However, it's unclear how consumers should utilize this function. Consider adding (or referencing) documentation to explain how it's intended to be used, along with any patterns or best practices you recommend.

Would you like help drafting minimal usage docs or code examples to guide users?

packages/react/src/components/primitive/allowed-content.tsx (1)

20-22: Consider using a generic ref type to align with the as prop

Currently, the ref type is fixed to HTMLDivElement, but the component allows rendering different element types via the as prop. To ensure accurate type inference, switch to a more generic type:

-  ref: Ref<HTMLDivElement>,
+  ref: Ref<React.ElementRef<TTag>>,
packages/react/src/components/primitive/root.tsx (2)

230-237: Simplify state determination logic

The inline function returning the state can be simplified while retaining clarity. Consider early returns or consolidated checks for improved readability:


239-258: Refactor paste handling for better maintainability

The inline paste handling is beneficial, but extracting it into a dedicated custom hook (e.g., usePasteHandler) can enhance clarity and potential reuse.

packages/react/src/components/primitive/dropzone.tsx (2)

206-207: Remove or reconsider event.persist() usage

From React v17 onwards, the synthetic event pool has been removed, making event.persist() largely unnecessary. Verify if you still need it, or remove for cleaner code.


371-375: Evaluate continued support for IE/Edge check

IE11 and legacy Edge are deprecated. If your project no longer supports them, remove the isIeOrEdge() check and simplify the file dialog opening logic.

📜 Review details

Configuration used: CodeRabbit UI
Review profile: CHILL
Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between d1291ff and d88e8a3.

📒 Files selected for processing (7)
  • packages/react/src/components/index.tsx (2 hunks)
  • packages/react/src/components/primitive/allowed-content.tsx (1 hunks)
  • packages/react/src/components/primitive/button.tsx (1 hunks)
  • packages/react/src/components/primitive/clear-button.tsx (1 hunks)
  • packages/react/src/components/primitive/dropzone.tsx (1 hunks)
  • packages/react/src/components/primitive/root.tsx (1 hunks)
  • packages/react/src/index.ts (1 hunks)
🚧 Files skipped from review as they are similar to previous changes (2)
  • packages/react/src/components/index.tsx
  • packages/react/src/components/primitive/button.tsx
🧰 Additional context used
📓 Learnings (1)
packages/react/src/components/primitive/allowed-content.tsx (1)
Learnt from: veloii
PR: pingdotgg/uploadthing#947
File: packages/react/src/components/primitive/allowed-content.tsx:29-32
Timestamp: 2024-11-12T10:36:58.532Z
Learning: For slot components, sometimes it's preferred to manually specify `children` via props to avoid the appearance of nested elements when they aren't actually nested.
🪛 Biome (1.9.4)
packages/react/src/components/primitive/clear-button.tsx

[error] 38-38: Avoid passing children using a prop

The canonical way to pass children in React is to use JSX elements

(lint/correctness/noChildrenProp)

packages/react/src/components/primitive/allowed-content.tsx

[error] 31-31: Avoid passing children using a prop

The canonical way to pass children in React is to use JSX elements

(lint/correctness/noChildrenProp)

🔇 Additional comments (6)
packages/react/src/components/primitive/clear-button.tsx (3)

43-50: LGTM! Well-structured type definitions.

The type definitions and component export are well-implemented, providing good type safety and proper display name support.


38-38: 🛠️ Refactor suggestion

Use JSX children pattern instead of children prop.

The static analysis tool correctly flags that children should be passed using JSX syntax rather than as a prop.

-<PrimitiveSlot default="Clear" children={children} />
+<PrimitiveSlot default="Clear">{children}</PrimitiveSlot>

Likely invalid or redundant comment.

🧰 Tools
🪛 Biome (1.9.4)

[error] 38-38: Avoid passing children using a prop

The canonical way to pass children in React is to use JSX elements

(lint/correctness/noChildrenProp)


9-9: 🛠️ Refactor suggestion

Consider using 'button' as the default tag instead of 'label'.

The current implementation uses 'label' as the default tag for the clear button. While this works, it might be more semantically correct and accessible to use a 'button' element by default, as labels are typically used to describe form controls rather than trigger actions.

-const DEFAULT_CLEAR_BUTTON_TAG = "label";
+const DEFAULT_CLEAR_BUTTON_TAG = "button" as const;

Likely invalid or redundant comment.

packages/react/src/index.ts (1)

15-15: Verify consistency of path changes for useDropzone.

You've updated the import path to ./components/primitive/dropzone. Ensure that any references to the old path ("./components/dropzone") are removed or updated throughout the codebase.

Below is a shell script you can run to find any outdated references:

✅ Verification successful

Let me verify if there are any other potential references to the dropzone component or its path.


Let me try a more comprehensive search without file type restrictions.


No outdated references to old dropzone path found

Based on the comprehensive search results, there are no references to the old path "./components/dropzone" in the codebase. The only import of useDropzone found is in the documentation example which uses the package import @uploadthing/react. The path change in packages/react/src/index.ts to "./components/primitive/dropzone" appears to be consistent with the codebase structure.

🏁 Scripts executed

The following scripts were executed for the analysis:

Script:

#!/bin/bash
# Description: Search for all imports of useDropzone from old or new paths
rg 'import\s*\{?\s*useDropzone\s*\}?\s*from\s*"[^"]+"'

Length of output: 155


Script:

#!/bin/bash
# Search for any mentions of dropzone path or component
rg -i "dropzone" --type ts --type tsx

Length of output: 65


Script:

#!/bin/bash
# Search for any mentions of dropzone in the codebase
rg -i "dropzone"

Length of output: 45781

packages/react/src/components/primitive/allowed-content.tsx (1)

29-32: Avoid passing children via prop ()

Passing children via the children prop can be seen as an anti-pattern in React. While you have a valid reason (avoiding nested DOM nodes), consider the standard pattern of passing children as JSX elements for consistency.

🧰 Tools
🪛 Biome (1.9.4)

[error] 31-31: Avoid passing children using a prop

The canonical way to pass children in React is to use JSX elements

(lint/correctness/noChildrenProp)

packages/react/src/components/primitive/root.tsx (1)

189-189: Ensure compatibility of globalThis.fetch

You are using globalThis.fetch to make upload requests. If the library is meant to be used in older browsers or non-browser environments, confirm the availability of globalThis.fetch or provide a fallback.

Comment on lines +392 to +420
onDrop: onDropCb as any,
}
: {}),
}),
[
disabled,
onBlur,
onClick,
onDragEnter,
onDragLeave,
onDragOver,
onDropCb,
onFocus,
onKeyDown,
],
);

const getInputProps = useMemo(
() => (): HTMLProps<HTMLInputElement> => ({
ref: inputRef,
type: "file",
style: { display: "none" },
accept: acceptAttr,
multiple,
tabIndex: -1,
...(!disabled
? {
// eslint-disable-next-line @typescript-eslint/no-unsafe-assignment
onChange: onDropCb as any,
Copy link
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue

Eliminate as any casts in event handlers

Casting to any bypasses type safety. Instead, match the event signatures by adjusting the function’s argument type definition or refining TypeScript types.

Copy link
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 1

♻️ Duplicate comments (4)
packages/react/src/components/button.tsx (2)

168-170: Remove redundant case "disabled"/"ready" branches

default already covers those states, so these clauses are dead code and
Biome correctly flags them.

-            case "disabled":
-            case "ready":

Removing them shortens the switch and eliminates linter noise.

🧰 Tools
🪛 Biome (1.9.4)

[error] 168-168: Useless case clause.

because the default clause is present:

Unsafe fix: Remove the useless case.

(lint/complexity/noUselessSwitchCase)


[error] 169-169: Useless case clause.

because the default clause is present:

Unsafe fix: Remove the useless case.

(lint/complexity/noUselessSwitchCase)


100-103: 🛠️ Refactor suggestion

Avoid as any – preserve type safety

rootProps is spread into <Primitive.Root> with ...(rootProps as any).
Casting to any bypasses the benefit of the generics you already defined.

-<Primitive.Root<TRouter, TEndpoint>
-  {...(rootProps as any)}
+<Primitive.Root<TRouter, TEndpoint>
+  {...rootProps}

If TypeScript complains, create a dedicated type that merges
UploadthingComponentProps<TRouter, TEndpoint> with the internal flags instead
of silencing the checker.

packages/react/src/components/dropzone.tsx (2)

150-151: Delete useless switch cases

case "disabled" and case "ready" are redundant – the default branch
handles them. Please drop them to satisfy the linter and keep the code tidy.

🧰 Tools
🪛 Biome (1.9.4)

[error] 150-150: Useless case clause.

because the default clause is present:

Unsafe fix: Remove the useless case.

(lint/complexity/noUselessSwitchCase)


[error] 151-151: Useless case clause.

because the default clause is present:

Unsafe fix: Remove the useless case.

(lint/complexity/noUselessSwitchCase)


110-110: 🛠️ Refactor suggestion

Retain type checking – remove as any when spreading rootProps

Same issue as in button.tsx: spreading ...(rootProps as any) drops static
safety.

-<Primitive.Root<TRouter, TEndpoint> {...(rootProps as any)}>
+<Primitive.Root<TRouter, TEndpoint> {...rootProps}>

Define/extend a proper type instead of falling back to any.

🧹 Nitpick comments (3)
docs/src/app/(docs)/concepts/theming/page.mdx (1)

14-28: Fix grammar and flow in introduction

Minor language issues hurt the professional tone:

-UploadThing ships with a default styled button and dropzone component that you
-can mount in your app if you don't have special needs on design.
+UploadThing ships with default-styled Button and Dropzone components that you
+can mount in your app if you don't have special design requirements.

-These default components are customizable, both in styling and content.
+These default components are fully customizable in both styling and content.

-These comes with behavior built-in but you have full control over what's rendered when and where.
+These come with behaviour built-in, but you retain full control over what is rendered, when and where.

-You can also build a fully custom flow you can opt for the
-[`useUploadThing`](/api-reference/react#use-upload-thing) hook that allows you to not only customize the look but also have full control of the behavior.
+If you need a completely custom flow, use the
+[`useUploadThing`](/api-reference/react#use-upload-thing) hook, which gives you full control over both look and behaviour.

This tightens wording, fixes “on design” → “in design”, corrects subject–verb agreement, and removes a “not only … but also” verbosity.

🧰 Tools
🪛 LanguageTool

[uncategorized] ~15-~15: The preposition “for” seems more likely in this position.
Context: ...our app if you don't have special needs on design. These default components are cu...

(AI_EN_LECTOR_REPLACEMENT_PREPOSITION)


[grammar] ~22-~22: The verb ‘comes’ is singular. Did you mean: “this comes” or “These come”?
Context: ...tives](#unstyled-primitive-components). These comes with behavior built-in but you have ful...

(SINGULAR_VERB_AFTER_THESE_OR_THOSE)


[uncategorized] ~23-~23: Use a comma before ‘but’ if it connects two independent clauses (unless they are closely connected and short).
Context: ...nts). These comes with behavior built-in but you have full control over what's rende...

(COMMA_COMPOUND_SENTENCE)


[style] ~27-~27: Opting for a less wordy alternative here can improve the clarity of your writing.
Context: ...e-upload-thing) hook that allows you to not only customize the look but also have full control of the behavior. ## Uploa...

(NOT_ONLY_ALSO)

packages/react/src/components/button.tsx (1)

146-176: Duplication with UploadDropzone – extract shared “get button label” helper

The whole switch for deciding button content is copy-pasted in
dropzone.tsx. Consider extracting to a pure helper:

function getUploadLabel(state: State, opts: { files: File[]; ...}) {  }

Benefits:
• single place to tweak wording
• smaller components → easier to test

Happy to draft the refactor if useful.

🧰 Tools
🪛 Biome (1.9.4)

[error] 168-168: Useless case clause.

because the default clause is present:

Unsafe fix: Remove the useless case.

(lint/complexity/noUselessSwitchCase)


[error] 169-169: Useless case clause.

because the default clause is present:

Unsafe fix: Remove the useless case.

(lint/complexity/noUselessSwitchCase)

packages/react/src/components/dropzone.tsx (1)

125-158: Factor out duplicated upload-button content logic

The logic in getUploadButtonContents() is 1-for-1 with the version in
button.tsx. Extracting it to a shared utility (e.g.
shared-upload-label.ts) avoids divergence and reduces bundle size.

🧰 Tools
🪛 Biome (1.9.4)

[error] 150-150: Useless case clause.

because the default clause is present:

Unsafe fix: Remove the useless case.

(lint/complexity/noUselessSwitchCase)


[error] 151-151: Useless case clause.

because the default clause is present:

Unsafe fix: Remove the useless case.

(lint/complexity/noUselessSwitchCase)

📜 Review details

Configuration used: CodeRabbit UI
Review profile: CHILL
Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between d88e8a3 and 2552d05.

📒 Files selected for processing (5)
  • docs/src/app/(docs)/concepts/theming/page.mdx (2 hunks)
  • packages/react/src/components/button.tsx (2 hunks)
  • packages/react/src/components/dropzone.tsx (2 hunks)
  • playground/app/layout.tsx (2 hunks)
  • playground/components/uploader.tsx (1 hunks)
🚧 Files skipped from review as they are similar to previous changes (2)
  • playground/components/uploader.tsx
  • playground/app/layout.tsx
🧰 Additional context used
🧠 Learnings (1)
packages/react/src/components/dropzone.tsx (1)
Learnt from: juliusmarminge
PR: pingdotgg/uploadthing#977
File: packages/solid/src/components/dropzone.tsx:539-544
Timestamp: 2024-09-24T20:46:58.415Z
Learning: In `packages/solid/src/components/dropzone.tsx`, issues related to the `onClick` handler definition will be fixed in PR #980.
🧬 Code Graph Analysis (1)
packages/react/src/components/button.tsx (2)
packages/shared/src/component-utils.ts (3)
  • styleFieldToClassName (168-180)
  • styleFieldToCssObject (182-194)
  • contentFieldToContent (196-207)
packages/react/src/components/shared.tsx (2)
  • Spinner (3-17)
  • Cancel (19-37)
🪛 LanguageTool
docs/src/app/(docs)/concepts/theming/page.mdx

[uncategorized] ~15-~15: The preposition “for” seems more likely in this position.
Context: ...our app if you don't have special needs on design. These default components are cu...

(AI_EN_LECTOR_REPLACEMENT_PREPOSITION)


[grammar] ~22-~22: The verb ‘comes’ is singular. Did you mean: “this comes” or “These come”?
Context: ...tives](#unstyled-primitive-components). These comes with behavior built-in but you have ful...

(SINGULAR_VERB_AFTER_THESE_OR_THOSE)


[uncategorized] ~23-~23: Use a comma before ‘but’ if it connects two independent clauses (unless they are closely connected and short).
Context: ...nts). These comes with behavior built-in but you have full control over what's rende...

(COMMA_COMPOUND_SENTENCE)


[style] ~27-~27: Opting for a less wordy alternative here can improve the clarity of your writing.
Context: ...e-upload-thing) hook that allows you to not only customize the look but also have full control of the behavior. ## Uploa...

(NOT_ONLY_ALSO)

🪛 Biome (1.9.4)
packages/react/src/components/button.tsx

[error] 168-168: Useless case clause.

because the default clause is present:

Unsafe fix: Remove the useless case.

(lint/complexity/noUselessSwitchCase)


[error] 169-169: Useless case clause.

because the default clause is present:

Unsafe fix: Remove the useless case.

(lint/complexity/noUselessSwitchCase)

packages/react/src/components/dropzone.tsx

[error] 150-150: Useless case clause.

because the default clause is present:

Unsafe fix: Remove the useless case.

(lint/complexity/noUselessSwitchCase)


[error] 151-151: Useless case clause.

because the default clause is present:

Unsafe fix: Remove the useless case.

(lint/complexity/noUselessSwitchCase)

Comment on lines +546 to +552
# Unstyled Primitive Components

These components allow you to bring your own styling solution while not having
to implement any of the internals. They accept any normal HTML props and can be
assigned specific HTML tags through the `as` prop.

<Note>This is currently only implemented by `@uploadthing/react`.</Note>
Copy link
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue

Docs contradict current implementation of the as prop

The paragraph and later examples advertise an as prop on the primitives, but the PR description clearly states “there is no support for asChild or as={YourComponent} props, as the author is undecided”. Exposing the prop in the docs before it exists is misleading.

Either:

  1. Remove references & examples that use as, or
  2. Land the actual as implementation in this PR.

Until the feature exists, visitors will copy-paste examples that fail at runtime.

Copy link
Member

juliusmarminge commented Apr 25, 2025

Warning

This pull request is not mergeable via GitHub because a downstack PR is open. Once all requirements are satisfied, merge this PR as a stack on Graphite.
Learn more

This stack of pull requests is managed by Graphite. Learn more about stacking.

@juliusmarminge juliusmarminge changed the base branch from main to graphite-base/947 April 25, 2025 19:34
@juliusmarminge juliusmarminge changed the base branch from graphite-base/947 to future-uploadFiles April 25, 2025 19:34
@juliusmarminge juliusmarminge changed the base branch from future-uploadFiles to graphite-base/947 May 1, 2025 08:37
@juliusmarminge juliusmarminge changed the base branch from graphite-base/947 to future-uploadFiles May 1, 2025 09:00
@juliusmarminge juliusmarminge force-pushed the future-uploadFiles branch 6 times, most recently from bc9db78 to f8c7cd2 Compare May 1, 2025 09:29
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Projects
None yet
Development

Successfully merging this pull request may close these issues.

3 participants