Skip to content
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

feat: composable primitive components #947

Open
wants to merge 43 commits into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
43 commits
Select commit Hold shift + click to select a range
ad67fd0
feat: primitive components
veloii Sep 14, 2024
bd7475f
feat: controllable files in primitive components
veloii Sep 14, 2024
e67ae68
Merge branch 'pingdotgg:main' into main
veloii Sep 20, 2024
a184147
refactor: remove dead code
veloii Sep 20, 2024
b1c57d9
feat: primitive clear button
veloii Sep 20, 2024
90ab08f
refactor: button component to use primitives
veloii Sep 20, 2024
1eb1b31
feat: wrap primitive allowed content in a div
veloii Sep 26, 2024
f2d191d
feat: `as` prop & ref support in primitive components
veloii Sep 26, 2024
7a26a1a
style: prefix generics with `T` for consistency
veloii Sep 26, 2024
f546640
fix: add the 'use client' directive to primitive components
veloii Sep 26, 2024
5f12570
refactor: dropzone component to use primitives
veloii Sep 26, 2024
1dc0a96
Merge branch 'main' into without-dropzone-package
veloii Sep 26, 2024
4a9abce
todo: remove redundant import
veloii Sep 26, 2024
3739b2d
todo: re-add use dropzone hook into primitive dropzone
veloii Sep 26, 2024
314da61
style: consistency with import type statements
veloii Sep 26, 2024
6a395b1
chore: add basic styling to the examples
veloii Sep 26, 2024
1d4d5c2
chore: `onCompleteUploadComplete` and `onUploadBegin` props added for…
veloii Sep 26, 2024
12be484
fix: disabled accessibility for primitive button components
veloii Sep 26, 2024
29469b3
fix: useUncontrolledState potential issue with falsy value
veloii Sep 26, 2024
d1dd53c
fix: dropzone and button components not using custom cn
veloii Sep 26, 2024
1330c72
refactor: button component to use `rootProps` variable name for consi…
veloii Sep 26, 2024
753c95d
refactor: use more precise function type for `RefProp`
veloii Sep 26, 2024
fc72978
perf: optimise useEffect deps in the root primitive component
veloii Sep 26, 2024
3c1d7dc
remove redundant type guards in the root primitive component
veloii Oct 2, 2024
f9bb2a8
fix: root primitive syntax
veloii Oct 2, 2024
3b51eaa
feat: disabled prop on dropzone primitive
veloii Oct 2, 2024
46678ec
fix: allow undefined on disabled prop
veloii Oct 2, 2024
a7b146a
fix: internal component props
veloii Oct 2, 2024
1fea662
fix: add disabled check on primitive button on click
veloii Oct 2, 2024
07424f3
fix: usePaste hook in primtive root not auto uploading the pasted files
veloii Oct 2, 2024
3b8a366
chore: remove redundant import in root primitve
veloii Oct 2, 2024
8faf68f
fix: backwards compat with onDrop prop
veloii Oct 2, 2024
444237b
Merge branch 'main' into without-dropzone-package
veloii Oct 23, 2024
f49d5c6
fix: merge conflicts
veloii Oct 23, 2024
24082c7
fix: examples to use state instead of isUploading
veloii Oct 23, 2024
08dbf99
docs: unstyled primitive components
veloii Oct 23, 2024
b601196
fix: add disabled prop to clear button
veloii Oct 23, 2024
b140b50
fix: add disabled prop to button
veloii Oct 23, 2024
7d38176
Update packages/react/src/components/primitive/root.tsx
veloii Oct 23, 2024
c18d02c
fix: import useMemo
veloii Oct 23, 2024
aac8916
docs: unstyled primitive components usage
veloii Oct 23, 2024
6d86f6e
Merge branch 'main' into without-dropzone-package
juliusmarminge Oct 24, 2024
11f77eb
more docs
juliusmarminge Oct 24, 2024
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
105 changes: 103 additions & 2 deletions docs/src/app/(docs)/concepts/theming/page.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -11,8 +11,20 @@ import * as d from "./demos";

# Theming

Our prebuilt components are customizable so you can make them fit with the theme
of your application.
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. These default
components are customizable, both in styling and content. The first parts of
this doc will cover how to customize the default components and how you can make
them fit with the theme of your application.

Due to their nature, there are certain customizations you cannot make on the
default components, which is why we also expose the unstyled,
[headless primitives](#unstyled-primitive-components). These comes with behavior
built-in but you have full control over what's 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.

## UploadButton Anatomy

Expand Down Expand Up @@ -509,3 +521,92 @@ type UploadDropzoneProps = {
<d.CustomContent1 />
<d.CustomContent2 />
<d.CustomContent3 />

<div className="mt-16"></div>

# 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>

## Creating the unstyled components

```ts {{ title: 'src/utils/uploadthing.ts' }}
import { generateUploadPrimitives } from "@uploadthing/react";

import type { OurFileRouter } from "~/server/uploadthing";

export const UT = generateUploadPrimitives<OurFileRouter>();
```

The returned `UT` object includes the following components:

<Properties>

<Property name="Root" type="component" since="7.2">
This is the main provider that accept most of the same props as the default `
<UploadButton />` and `<UploadDropzone />` accept.
</Property>

<Property name="Button" type="component" since="7.2">
The button element can be used to open the file selector. If you have auto mode
enabled, files are automatically uploaded once they are selected. For manual mode,
a second press on the button will upload the selected files.
</Property>
<Property name="Dropzone" type="component" since="7.2">
A dropzone area which accepts files to be dropped. As for the button, you may have both
auto and manual mode.
</Property>
<Property name="AllowedContent" type="component" since="7.2">
A text field where you can display what types of files are allowed to be uploaded.
</Property>
<Property name="ClearButton" type="component" since="7.2">
A button that clears the selected files.
</Property>
</Properties>

## Using Unstyled Components

All components accept a children function which allows you to grab any piece of
internal state. This includes `files`, `state`, `dropzone` state, and many more.

<Note>A children function can also be passed as a prop if you prefer.</Note>

### Example of Dropzone

<Note>
The `dropzone` parameter will only be defined from within children of the
`Dropzone` component.
</Note>

```jsx
<UT.Root endpoint="mockRoute">
<UT.Dropzone>
{({ state }) => (
<div>
<p>Drag and drop</p>
<UT.Button as="button">
{state === "uploading" ? "Uploading" : "Upload file"}
</UT.Button>
<UT.AllowedContent
as="p"
style={{ fontSize: 12, width: "fit-content" }}
/>
</div>
)}
</UT.Dropzone>
</UT.Root>
```

### Example of Button

```jsx
<UT.Root endpoint="mockRoute">
<UT.Button>
{({ state }) => (state === "uploading" ? "Uploading" : "Upload file")}
</UT.Button>
</UT.Root>
```
39 changes: 39 additions & 0 deletions examples/minimal-appdir/src/app/page.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ import {
UploadButton,
UploadDropzone,
useUploadThing,
UT,
} from "~/utils/uploadthing";

export default function Home() {
Expand Down Expand Up @@ -70,6 +71,44 @@ export default function Home() {
await startUpload(files);
}}
/>
<UT.Root
endpoint="videoAndImage"
onClientUploadComplete={(res) => {
console.log(`onClientUploadComplete`, res);
alert("Upload Completed");
}}
onUploadBegin={() => {
console.log("upload begin");
}}
>
<UT.Dropzone style={{ marginTop: 24 }}>
{({ dropzone, state }) => (
<div
style={{
borderWidth: 2,
borderStyle: "dashed",
borderColor: dropzone?.isDragActive ? "#2563f5" : "#11182725",
padding: 16,
}}
>
<p
style={{
width: "fit-content",
}}
>
Drag and drop
</p>
<UT.Button as="button">
{state === "uploading" ? "Uploading" : "Upload file"}
</UT.Button>
Comment on lines +101 to +103
Copy link
Contributor

Choose a reason for hiding this comment

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

🛠️ Refactor suggestion

Enhance button loading state feedback.

The button's state indication could be more informative. Consider adding a loading spinner and disabled state during upload.

 <UT.Button as="button">
-  {state === "uploading" ? "Uploading" : "Upload file"}
+  {state === "uploading" ? (
+    <>
+      <LoadingSpinner className="mr-2" />
+      <span>Uploading...</span>
+    </>
+  ) : (
+    "Upload file"
+  )}
 </UT.Button>

Committable suggestion was skipped due to low confidence.

<UT.AllowedContent
as="p"
style={{ fontSize: 12, width: "fit-content" }}
/>
juliusmarminge marked this conversation as resolved.
Show resolved Hide resolved
veloii marked this conversation as resolved.
Show resolved Hide resolved
</div>
)}
</UT.Dropzone>
</UT.Root>
</main>
);
}
2 changes: 2 additions & 0 deletions examples/minimal-appdir/src/utils/uploadthing.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,11 +2,13 @@ import {
generateReactHelpers,
generateUploadButton,
generateUploadDropzone,
generateUploadPrimitives,
} from "@uploadthing/react";

import type { OurFileRouter } from "~/server/uploadthing";

export const UploadButton = generateUploadButton<OurFileRouter>();
export const UploadDropzone = generateUploadDropzone<OurFileRouter>();
export const UT = generateUploadPrimitives<OurFileRouter>();

export const { useUploadThing } = generateReactHelpers<OurFileRouter>();
39 changes: 39 additions & 0 deletions examples/minimal-pagedir/src/pages/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ import {
UploadButton,
UploadDropzone,
useUploadThing,
UT,
} from "~/utils/uploadthing";

export default function Home() {
Expand Down Expand Up @@ -54,6 +55,44 @@ export default function Home() {
await startUpload([file]);
}}
/>
<UT.Root
endpoint="videoAndImage"
onClientUploadComplete={(res) => {
console.log(`onClientUploadComplete`, res);
alert("Upload Completed");
}}
onUploadBegin={() => {
console.log("upload begin");
}}
>
<UT.Dropzone style={{ marginTop: 24 }}>
{({ dropzone, state }) => (
<div
style={{
borderWidth: 2,
borderStyle: "dashed",
borderColor: dropzone?.isDragActive ? "#2563f5" : "#11182725",
padding: 16,
}}
>
<p
style={{
width: "fit-content",
}}
>
Drag and drop
</p>
<UT.Button as="button">
{state === "uploading" ? "Uploading" : "Upload file"}
</UT.Button>
<UT.AllowedContent
as="p"
style={{ fontSize: 12, width: "fit-content" }}
/>
</div>
)}
</UT.Dropzone>
</UT.Root>
</main>
);
}
2 changes: 2 additions & 0 deletions examples/minimal-pagedir/src/utils/uploadthing.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,11 +2,13 @@ import {
generateReactHelpers,
generateUploadButton,
generateUploadDropzone,
generateUploadPrimitives,
} from "@uploadthing/react";

import type { OurFileRouter } from "~/server/uploadthing";

export const UploadButton = generateUploadButton<OurFileRouter>();
export const UploadDropzone = generateUploadDropzone<OurFileRouter>();
export const UT = generateUploadPrimitives<OurFileRouter>();

export const { useUploadThing } = generateReactHelpers<OurFileRouter>();
40 changes: 39 additions & 1 deletion examples/with-clerk-appdir/src/app/page.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@

import { SignIn, useAuth } from "@clerk/nextjs";

import { UploadButton, UploadDropzone } from "~/utils/uploadthing";
import { UploadButton, UploadDropzone, UT } from "~/utils/uploadthing";

export default function Home() {
const { isSignedIn } = useAuth();
Expand Down Expand Up @@ -35,6 +35,44 @@ export default function Home() {
console.log("upload begin");
}}
/>
<UT.Root
endpoint="videoAndImage"
onClientUploadComplete={(res) => {
console.log(`onClientUploadComplete`, res);
alert("Upload Completed");
}}
onUploadBegin={() => {
console.log("upload begin");
}}
>
<UT.Dropzone style={{ marginTop: 24 }}>
{({ dropzone, state }) => (
<div
style={{
borderWidth: 2,
borderStyle: "dashed",
borderColor: dropzone?.isDragActive ? "#2563f5" : "#11182725",
padding: 16,
}}
>
<p
style={{
width: "fit-content",
}}
>
Drag and drop
</p>
<UT.Button as="button">
{state === "uploading" ? "Uploading" : "Upload file"}
</UT.Button>
<UT.AllowedContent
as="p"
style={{ fontSize: 12, width: "fit-content" }}
/>
</div>
)}
</UT.Dropzone>
</UT.Root>
{!isSignedIn ? (
<div
style={{
Expand Down
2 changes: 2 additions & 0 deletions examples/with-clerk-appdir/src/utils/uploadthing.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,11 +2,13 @@ import {
generateReactHelpers,
generateUploadButton,
generateUploadDropzone,
generateUploadPrimitives,
} from "@uploadthing/react";

import type { OurFileRouter } from "~/server/uploadthing";

export const UploadButton = generateUploadButton<OurFileRouter>();
export const UploadDropzone = generateUploadDropzone<OurFileRouter>();
export const UT = generateUploadPrimitives<OurFileRouter>();

export const { useUploadThing } = generateReactHelpers<OurFileRouter>();
40 changes: 39 additions & 1 deletion examples/with-clerk-pagesdir/src/pages/index.tsx
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import { Inter } from "next/font/google";
import { SignIn, useAuth } from "@clerk/nextjs";

import { UploadButton, UploadDropzone } from "~/utils/uploadthing";
import { UploadButton, UploadDropzone, UT } from "~/utils/uploadthing";

const inter = Inter({ subsets: ["latin"] });

Expand Down Expand Up @@ -36,6 +36,44 @@ export default function Home() {
console.log("upload begin");
}}
/>
<UT.Root
endpoint="videoAndImage"
onClientUploadComplete={(res) => {
console.log(`onClientUploadComplete`, res);
alert("Upload Completed");
}}
onUploadBegin={() => {
console.log("upload begin");
}}
juliusmarminge marked this conversation as resolved.
Show resolved Hide resolved
>
<UT.Dropzone style={{ marginTop: 24 }}>
{({ dropzone, state }) => (
<div
style={{
borderWidth: 2,
borderStyle: "dashed",
borderColor: dropzone?.isDragActive ? "#2563f5" : "#11182725",
padding: 16,
}}
>
<p
style={{
width: "fit-content",
}}
>
Drag and drop
</p>
<UT.Button as="button">
{state === "uploading" ? "Uploading" : "Upload file"}
</UT.Button>
<UT.AllowedContent
as="p"
style={{ fontSize: 12, width: "fit-content" }}
/>
</div>
)}
</UT.Dropzone>
juliusmarminge marked this conversation as resolved.
Show resolved Hide resolved
</UT.Root>
{!isSignedIn ? (
<div
style={{
Expand Down
2 changes: 2 additions & 0 deletions examples/with-clerk-pagesdir/src/utils/uploadthing.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,11 +2,13 @@ import {
generateReactHelpers,
generateUploadButton,
generateUploadDropzone,
generateUploadPrimitives,
} from "@uploadthing/react";

import type { OurFileRouter } from "~/server/uploadthing";

export const UploadButton = generateUploadButton<OurFileRouter>();
export const UploadDropzone = generateUploadDropzone<OurFileRouter>();
export const UT = generateUploadPrimitives<OurFileRouter>();

export const { useUploadThing } = generateReactHelpers<OurFileRouter>();
Loading
Loading