Compare commits

..

5 Commits

10 changed files with 1125 additions and 1196 deletions

View File

@ -3,11 +3,22 @@ import logoPng from "~/images/lume-logo.png?url"
import lumeColorLogoPng from "~/images/lume-color-logo.png?url"
import discordLogoPng from "~/images/discord-logo.png?url"
import { Link } from "@remix-run/react"
import {
Dialog,
DialogContent,
DialogHeader,
DialogTitle,
DialogTrigger
} from "./ui/dialog"
import { useUppy } from "./lib/uppy"
import { UppyFile } from "@uppy/core"
import { Progress } from "./ui/progress"
import { DialogClose } from "@radix-ui/react-dialog"
export const GeneralLayout = ({ children }: React.PropsWithChildren<{}>) => {
return (
<div className="p-10 h-full flex flex-row">
<header className="w-full flex flex-col max-w-[240px] h-full">
<div className="h-full flex flex-row">
<header className="p-10 pr-0 flex flex-col w-[240px] h-full scroll-m-0 overflow-hidden">
<img src={logoPng} alt="Lume logo" className="h-10 w-32" />
<nav className="my-10 flex-1">
@ -39,20 +50,27 @@ export const GeneralLayout = ({ children }: React.PropsWithChildren<{}>) => {
</ul>
</nav>
<span className="text-primary-2 mb-3 -space-y-1 opacity-40">
<p>Freedom</p>
<p>Privacy</p>
<p>Ownership</p>
<p>Freedom</p>
<p>Privacy</p>
<p>Ownership</p>
</span>
<Button className="w-[calc(100%-3rem)] font-semibold h-16">
<CloudUploadIcon className="w-6 h-6 -ml-3 mr-4" />
Upload Files
</Button>
<Dialog>
<DialogTrigger asChild>
<Button size={"lg"} className="w-[calc(100%-3rem)] font-semibold">
<CloudUploadIcon className="w-6 h-6 -ml-3 mr-4" />
Upload Files
</Button>
</DialogTrigger>
<DialogContent>
<UploadFileForm />
</DialogContent>
</Dialog>
</header>
<div className="flex-1">
<div className="flex-1 overflow-y-auto p-10">
{children}
<footer className="my-5">
<footer className="mt-5">
<ul className="flex flex-row">
<li>
<Link to="https://discord.lumeweb.com">
@ -83,9 +101,147 @@ export const GeneralLayout = ({ children }: React.PropsWithChildren<{}>) => {
)
}
const UploadFileForm = () => {
const {
getRootProps,
getInputProps,
getFiles,
upload,
state,
removeFile,
cancelAll
} = useUppy({
uploader: "tus",
endpoint: import.meta.env.VITE_PUBLIC_TUS_ENDPOINT
})
console.log({ state, files: getFiles() })
const isUploading = state === "uploading"
const isCompleted = state === "completed"
const hasStarted = state !== "idle" && state !== "initializing"
return (
<>
<DialogHeader className="mb-6">
<DialogTitle>Upload Files</DialogTitle>
</DialogHeader>
{!hasStarted ? (
<div
{...getRootProps()}
className="border border-border rounded text-primary-2 bg-primary-dark h-48 flex flex-col items-center justify-center"
>
<input
hidden
aria-hidden
name="uppyFiles[]"
key={new Date().toISOString()}
multiple
{...getInputProps()}
/>
<CloudUploadIcon className="w-24 h-24 stroke stroke-primary-dark" />
<p>Drag & Drop Files or Browse</p>
</div>
) : null}
<div className="w-full space-y-3 max-h-48 overflow-y-auto">
{getFiles().map((file) => (
<UploadFileItem
key={file.id}
file={file}
onRemove={(id) => {
removeFile(id)
}}
/>
))}
</div>
{hasStarted ? (
<div className="flex flex-col items-center gap-y-2 w-full text-primary-1">
<CloudCheckIcon className="w-32 h-32" />
{isCompleted
? "Upload completed"
: `${getFiles().length} files being uploaded`}
</div>
) : null}
{isUploading ? (
<DialogClose asChild onClick={cancelAll}>
<Button size={"lg"} className="mt-6">
Cancel
</Button>
</DialogClose>
) : null}
{isCompleted ? (
<DialogClose asChild>
<Button size={"lg"} className="mt-6">
Close
</Button>
</DialogClose>
) : null}
{!hasStarted && !isCompleted && !isUploading ? (
<Button size={"lg"} className="mt-6" onClick={upload}>
Upload
</Button>
) : null}
</>
)
}
function bytestoMegabytes(bytes: number) {
return bytes / 1024 / 1024
}
const UploadFileItem = ({
file,
onRemove
}: {
file: UppyFile
onRemove: (id: string) => void
}) => {
return (
<div className="flex flex-col w-full py-4 px-2 bg-primary-dark">
<div className="flex text-primary-1 items-center justify-between">
<div className="flex items-center">
<div className="p-2">
{file.progress?.uploadComplete ? (
<BoxCheckedIcon className="w-4 h-4" />
) : (
<PageIcon className="w-4 h-4" />
)}
</div>
<p className="w-full flex justify-between items-center">
<span className="truncate text-ellipsis max-w-[30ch]">
{file.name}
</span>{" "}
<span>({bytestoMegabytes(file.size).toFixed(2)} MB)</span>
</p>
</div>
<Button
size={"icon"}
variant={"ghost"}
className="!text-inherit"
onClick={() => onRemove(file.id)}
>
<TrashIcon className="w-4 h-4" />
</Button>
</div>
{file.progress?.uploadStarted && !file.progress.uploadComplete ? (
<Progress max={100} value={file.progress.percentage} className="mt-2" />
) : null}
</div>
)
}
const NavigationButton = ({ children }: React.PropsWithChildren) => {
return (
<Button variant="ghost" className="justify-start h-14 w-[calc(100%-3rem)] font-semibold">
<Button
variant="ghost"
className="justify-start h-14 w-full font-semibold"
>
{children}
</Button>
)
@ -166,3 +322,143 @@ const CloudUploadIcon = ({ className }: { className?: string }) => {
</svg>
)
}
const PageIcon = ({ className }: { className?: string }) => {
return (
<svg
aria-hidden="true"
width="21"
height="21"
viewBox="0 0 16 16"
fill="none"
xmlns="http://www.w3.org/2000/svg"
className={className}
>
<path
d="M10.3276 4.21337V15.0287H0.59375V0.96875H5.46067"
stroke="currentColor"
strokeLinecap="round"
strokeLinejoin="round"
/>
<path
d="M5.46191 0.96875L10.3288 4.21337"
stroke="currentColor"
strokeLinecap="round"
strokeLinejoin="round"
/>
<path
d="M5.46191 0.96875V4.21337"
stroke="currentColor"
strokeLinecap="round"
strokeLinejoin="round"
/>
<path
d="M10.3288 4.21289H5.46191"
stroke="currentColor"
strokeLinecap="round"
strokeLinejoin="round"
/>
<path
d="M2.75684 8.53906H5.46068"
stroke="currentColor"
strokeLinecap="round"
strokeLinejoin="round"
/>
<path
d="M2.75684 10.7021H7.62376"
stroke="currentColor"
strokeLinecap="round"
strokeLinejoin="round"
/>
</svg>
)
}
const TrashIcon = ({ className }: { className?: string }) => {
return (
<svg
aria-hidden="true"
width="21"
height="21"
viewBox="0 0 16 16"
fill="none"
xmlns="http://www.w3.org/2000/svg"
className={className}
>
<path
d="M10.0769 14.3093H2.69194C2.3247 14.3093 1.9725 14.1634 1.71282 13.9037C1.45314 13.6441 1.30725 13.2919 1.30725 12.9246V4.15492C1.30725 4.03251 1.35588 3.91511 1.44244 3.82855C1.529 3.74199 1.6464 3.69336 1.76881 3.69336C1.89123 3.69336 2.00863 3.74199 2.09519 3.82855C2.18175 3.91511 2.23038 4.03251 2.23038 4.15492V12.9246C2.23038 13.047 2.279 13.1644 2.36556 13.251C2.45212 13.3375 2.56952 13.3862 2.69194 13.3862H10.0769C10.1994 13.3862 10.3168 13.3375 10.4033 13.251C10.4899 13.1644 10.5385 13.047 10.5385 12.9246V4.15492C10.5385 4.03251 10.5871 3.91511 10.6737 3.82855C10.7602 3.74199 10.8776 3.69336 11.0001 3.69336C11.1225 3.69336 11.2399 3.74199 11.3264 3.82855C11.413 3.91511 11.4616 4.03251 11.4616 4.15492V12.9246C11.4616 13.2919 11.3157 13.6441 11.0561 13.9037C10.7964 14.1634 10.4442 14.3093 10.0769 14.3093Z"
fill="currentColor"
/>
<path
d="M11.9237 3.23172H0.846206C0.723792 3.23172 0.606392 3.18309 0.519832 3.09653C0.433272 3.00997 0.384644 2.89257 0.384644 2.77016C0.384644 2.64774 0.433272 2.53034 0.519832 2.44378C0.606392 2.35722 0.723792 2.30859 0.846206 2.30859H11.9237C12.0461 2.30859 12.1635 2.35722 12.2501 2.44378C12.3366 2.53034 12.3853 2.64774 12.3853 2.77016C12.3853 2.89257 12.3366 3.00997 12.2501 3.09653C12.1635 3.18309 12.0461 3.23172 11.9237 3.23172Z"
fill="currentColor"
/>
<path
d="M8.23121 3.23129C8.1088 3.23129 7.9914 3.18266 7.90484 3.0961C7.81828 3.00954 7.76965 2.89214 7.76965 2.76973V1.38504H5.00027V2.76973C5.00027 2.89214 4.95164 3.00954 4.86508 3.0961C4.77853 3.18266 4.66112 3.23129 4.53871 3.23129C4.4163 3.23129 4.2989 3.18266 4.21234 3.0961C4.12578 3.00954 4.07715 2.89214 4.07715 2.76973V0.923477C4.07715 0.801063 4.12578 0.683662 4.21234 0.597103C4.2989 0.510543 4.4163 0.461914 4.53871 0.461914H8.23121C8.35362 0.461914 8.47103 0.510543 8.55759 0.597103C8.64415 0.683662 8.69277 0.801063 8.69277 0.923477V2.76973C8.69277 2.89214 8.64415 3.00954 8.55759 3.0961C8.47103 3.18266 8.35362 3.23129 8.23121 3.23129Z"
fill="currentColor"
/>
<path
d="M6.3849 12.0012C6.26249 12.0012 6.14509 11.9526 6.05853 11.866C5.97197 11.7795 5.92334 11.6621 5.92334 11.5396V5.07777C5.92334 4.95536 5.97197 4.83796 6.05853 4.7514C6.14509 4.66484 6.26249 4.61621 6.3849 4.61621C6.50732 4.61621 6.62472 4.66484 6.71128 4.7514C6.79784 4.83796 6.84646 4.95536 6.84646 5.07777V11.5396C6.84646 11.6621 6.79784 11.7795 6.71128 11.866C6.62472 11.9526 6.50732 12.0012 6.3849 12.0012Z"
fill="currentColor"
/>
<path
d="M8.69313 11.0778C8.57072 11.0778 8.45332 11.0292 8.36676 10.9426C8.2802 10.8561 8.23157 10.7387 8.23157 10.6163V6.00062C8.23157 5.87821 8.2802 5.76081 8.36676 5.67425C8.45332 5.58769 8.57072 5.53906 8.69313 5.53906C8.81554 5.53906 8.93294 5.58769 9.0195 5.67425C9.10606 5.76081 9.15469 5.87821 9.15469 6.00062V10.6163C9.15469 10.7387 9.10606 10.8561 9.0195 10.9426C8.93294 11.0292 8.81554 11.0778 8.69313 11.0778Z"
fill="currentColor"
/>
<path
d="M4.07716 11.0778C3.95475 11.0778 3.83735 11.0292 3.75079 10.9426C3.66423 10.8561 3.6156 10.7387 3.6156 10.6163V6.00062C3.6156 5.87821 3.66423 5.76081 3.75079 5.67425C3.83735 5.58769 3.95475 5.53906 4.07716 5.53906C4.19958 5.53906 4.31698 5.58769 4.40354 5.67425C4.4901 5.76081 4.53873 5.87821 4.53873 6.00062V10.6163C4.53873 10.7387 4.4901 10.8561 4.40354 10.9426C4.31698 11.0292 4.19958 11.0778 4.07716 11.0778Z"
fill="currentColor"
/>
</svg>
)
}
const CloudCheckIcon = ({ className }: { className?: string }) => {
return (
<svg
aria-hidden="true"
width="21"
height="21"
viewBox="0 0 72 48"
fill="none"
xmlns="http://www.w3.org/2000/svg"
className={className}
>
<path
fillRule="evenodd"
clipRule="evenodd"
d="M58.2 18C56.1 7.8 47.1 0 36 0C27.3 0 19.8 4.8 16.2 12C6.9 13.2 0 20.7 0 30C0 39.9 8.1 48 18 48H57C65.4 48 72 41.4 72 33C72 25.2 65.7 18.6 58.2 18ZM30 39L19.5 28.5L23.7 24.3L30 30.6L45.6 15L49.8 19.2L30 39Z"
fill="currentColor"
/>
</svg>
)
}
const BoxCheckedIcon = ({ className }: { className?: string }) => {
return (
<svg
aria-hidden="true"
width="21"
height="21"
viewBox="0 0 21 21"
fill="none"
xmlns="http://www.w3.org/2000/svg"
className={className}
>
<path
fillRule="evenodd"
clipRule="evenodd"
d="M5.5 4.5H15.5C16.6046 4.5 17.5 5.39543 17.5 6.5V16.5C17.5 17.6046 16.6046 18.5 15.5 18.5H5.5C4.39543 18.5 3.5 17.6046 3.5 16.5V6.5C3.5 5.39543 4.39543 4.5 5.5 4.5Z"
stroke="currentColor"
strokeLinecap="round"
strokeLinejoin="round"
/>
<path
d="M7.5 11.5L9.5 13.5L13.5 9.5"
stroke="currentColor"
strokeLinecap="round"
strokeLinejoin="round"
/>
</svg>
)
}

View File

@ -0,0 +1,206 @@
// Copied from https://github.com/transloadit/uppy/blob/main/packages/%40uppy/drop-target/src/index.ts
// Is a less invasive implementation that allows for better unstyled integration
import {
type Uppy,
type PluginOptions,
type BasePlugin as TUppyBasePlugin
} from "@uppy/core"
// @ts-expect-error -- Uppy types are all over the place it really is weird
import UppyBasePlugin from "@uppy/core/lib/BasePlugin"
import type { IndexedObject } from "@uppy/utils"
import getDroppedFiles from "@uppy/utils/lib/getDroppedFiles"
import toArray from "@uppy/utils/lib/toArray"
export type DropTargetOptions = PluginOptions & {
target?: HTMLElement | string | null
onDrop?: (event: DragEvent) => void
onDragOver?: (event: DragEvent) => void
onDragLeave?: (event: DragEvent) => void
}
const BasePlugin = UppyBasePlugin as typeof TUppyBasePlugin<DropTargetOptions>
type Meta = {
relativePath?: string | null
}
type Body = IndexedObject<unknown>
// Default options
const defaultOpts = {
target: null,
} satisfies DropTargetOptions
interface DragEventWithFileTransfer extends DragEvent {
dataTransfer: NonNullable<DragEvent["dataTransfer"]>
}
function isFileTransfer(event: DragEvent): event is DragEventWithFileTransfer {
return event.dataTransfer?.types?.some((type) => type === "Files") ?? false
}
/**
* Drop Target plugin
*
*/
class DropTarget<M extends Meta, B extends Body> extends BasePlugin {
static VERSION = "lume-internal"
private removeDragOverDataAttr: ReturnType<typeof setTimeout> | undefined
private nodes?: Array<HTMLElement>
public opts: DropTargetOptions
constructor(uppy: Uppy<M, B>, opts?: DropTargetOptions) {
super(uppy, { ...defaultOpts, ...opts })
this.opts = opts || defaultOpts
this.type = "acquirer"
this.id = this.opts.id || "DropTarget"
// @ts-expect-error TODO: remove in major
this.title = "Drop Target"
}
addFiles = (files: Array<File>): void => {
const descriptors = files.map((file) => ({
source: this.id,
name: file.name,
type: file.type,
data: file,
meta: {
// path of the file relative to the ancestor directory the user selected.
// e.g. 'docs/Old Prague/airbnb.pdf'
relativePath: (file as { relativePath?: string }).relativePath || null
} as Meta
}))
try {
this.uppy.addFiles(descriptors)
} catch (err) {
this.uppy.log(err as string)
}
}
handleDrop = async (event: DragEvent): Promise<void> => {
if (!isFileTransfer(event)) {
return
}
event.preventDefault()
event.stopPropagation()
clearTimeout(this.removeDragOverDataAttr)
// Remove dragover class
if (event.currentTarget) {
(event.currentTarget as HTMLElement).dataset.uppyIsDragOver = "false"
this.setPluginState({ isDraggingOver: false })
}
// Let any acquirer plugin (Url/Webcam/etc.) handle drops to the root
this.uppy.iteratePlugins((plugin) => {
if (plugin.type === "acquirer") {
// @ts-expect-error Every Plugin with .type acquirer can define handleRootDrop(event)
plugin.handleRootDrop?.(event)
}
})
// Add all dropped files, handle errors
let executedDropErrorOnce = false
const logDropError = (error: Error): void => {
this.uppy.log(error.message, "error")
// In practice all drop errors are most likely the same,
// so let's just show one to avoid overwhelming the user
if (!executedDropErrorOnce) {
this.uppy.info(error.message, "error")
executedDropErrorOnce = true
}
}
const files = await getDroppedFiles(event.dataTransfer, { logDropError })
if (files.length > 0) {
this.uppy.log("[DropTarget] Files were dropped")
this.addFiles(files)
}
this.opts.onDrop?.(event)
}
handleDragOver = (event: DragEvent): void => {
if (!isFileTransfer(event)) {
return
}
event.preventDefault()
event.stopPropagation()
// Add a small (+) icon on drop
// (and prevent browsers from interpreting this as files being _moved_ into the browser,
// https://github.com/transloadit/uppy/issues/1978)
event.dataTransfer.dropEffect = "copy" // eslint-disable-line no-param-reassign
clearTimeout(this.removeDragOverDataAttr)
;(event.currentTarget as HTMLElement).dataset.uppyIsDragOver = "true"
this.setPluginState({ isDraggingOver: true })
this.opts.onDragOver?.(event)
}
handleDragLeave = (event: DragEvent): void => {
if (!isFileTransfer(event)) {
return
}
event.preventDefault()
event.stopPropagation()
const { currentTarget } = event
clearTimeout(this.removeDragOverDataAttr)
// Timeout against flickering, this solution is taken from drag-drop library.
// Solution with 'pointer-events: none' didn't work across browsers.
this.removeDragOverDataAttr = setTimeout(() => {
(currentTarget as HTMLElement).dataset.uppyIsDragOver = "false"
this.setPluginState({ isDraggingOver: false })
}, 50)
this.opts.onDragLeave?.(event)
}
addListeners = (): void => {
const { target } = this.opts
if (target instanceof Element) {
this.nodes = [target]
} else if (typeof target === "string") {
this.nodes = toArray(document.querySelectorAll(target))
}
if (!this.nodes || this.nodes.length === 0) {
throw new Error(`"${target}" does not match any HTML elements`)
}
for (const node of this.nodes) {
node.addEventListener("dragover", this.handleDragOver, false)
node.addEventListener("dragleave", this.handleDragLeave, false)
node.addEventListener("drop", this.handleDrop, false)
}
}
removeListeners = (): void => {
if (this.nodes) {
for (const node of this.nodes) {
node.removeEventListener("dragover", this.handleDragOver, false)
node.removeEventListener("dragleave", this.handleDragLeave, false)
node.removeEventListener("drop", this.handleDrop, false)
}
}
}
install(): void {
this.setPluginState({ isDraggingOver: false })
this.addListeners()
}
uninstall(): void {
this.removeListeners()
}
}
export default DropTarget

173
app/components/lib/uppy.ts Normal file
View File

@ -0,0 +1,173 @@
import Uppy, { State, debugLogger } from "@uppy/core"
import Tus from "@uppy/tus"
import toArray from "@uppy/utils/lib/toArray"
import {
ChangeEvent,
useCallback,
useEffect,
useMemo,
useRef,
useState
} from "react"
import DropTarget, { DropTargetOptions } from "./uppy-dropzone"
const LISTENING_EVENTS = [
"upload",
"upload-success",
"upload-error",
"file-added",
"file-removed",
"files-added"
] as const
export function useUppy({
uploader,
endpoint
}: {
uploader: "tus"
endpoint: string
}) {
const inputRef = useRef<HTMLInputElement>(null)
const [targetRef, _setTargetRef] = useState<HTMLElement | null>(null)
const uppyInstance = useRef<Uppy>()
const setRef = useCallback(
(element: HTMLElement | null) => _setTargetRef(element),
[]
)
const [uppyState, setUppyState] = useState<State>()
const [state, setState] = useState<
"completed" | "idle" | "initializing" | "error" | "uploading"
>("initializing")
const [inputProps, setInputProps] = useState<
| {
ref: typeof inputRef
type: "file"
onChange: (event: ChangeEvent<HTMLInputElement>) => void
}
| object
>({})
const getRootProps = useMemo(
() => () => {
return {
ref: setRef,
onClick: () => {
if (inputRef.current) {
//@ts-expect-error -- dumb html
inputRef.current.value = null
inputRef.current.click()
console.log("clicked", { input: inputRef.current })
}
},
role: "presentation"
}
},
[setRef]
)
const removeFile = useCallback(
(id: string) => {
uppyInstance.current?.removeFile(id)
},
// eslint-disable-next-line react-hooks/exhaustive-deps
[targetRef, uppyInstance]
)
const cancelAll = useCallback(
() => uppyInstance.current?.cancelAll({ reason: "user" }),
// eslint-disable-next-line react-hooks/exhaustive-deps
[targetRef, uppyInstance]
)
useEffect(() => {
if (!targetRef) return
const uppy = new Uppy({ logger: debugLogger }).use(DropTarget, {
target: targetRef
} as DropTargetOptions)
uppyInstance.current = uppy
setInputProps({
ref: inputRef,
type: "file",
onChange: (event) => {
const files = toArray(event.target.files)
if (files.length > 0) {
uppyInstance.current?.log("[DragDrop] Files selected through input")
uppyInstance.current?.addFiles(files)
}
// We clear the input after a file is selected, because otherwise
// change event is not fired in Chrome and Safari when a file
// with the same name is selected.
// ___Why not use value="" on <input/> instead?
// Because if we use that method of clearing the input,
// Chrome will not trigger change if we drop the same file twice (Issue #768).
// @ts-expect-error TS freaks out, but this is fine
// eslint-disable-next-line no-param-reassign
event.target.value = null
}
})
switch (uploader) {
case "tus":
uppy.use(Tus, { endpoint: endpoint, limit: 6 })
break
default:
}
uppy.on("complete", (result) => {
if (result.failed.length === 0) {
console.log("Upload successful üòÄ")
setState("completed")
} else {
console.warn("Upload failed üòû")
setState("error")
}
console.log("successful files:", result.successful)
console.log("failed files:", result.failed)
})
const setStateCb = (event: (typeof LISTENING_EVENTS)[number]) => {
switch (event) {
case "upload":
setState("uploading")
break
case "upload-error":
setState("error")
break
default:
break
}
setUppyState(uppy.getState())
}
for (const event of LISTENING_EVENTS) {
uppy.on(event, function cb() {
setStateCb(event)
})
}
setState("idle")
}, [targetRef, endpoint, uploader])
useEffect(() => {
return () => {
uppyInstance.current?.cancelAll({ reason: "unmount" })
uppyInstance.current?.logout()
uppyInstance.current?.close()
uppyInstance.current = undefined
}
}, [])
return {
getFiles: () => uppyInstance.current?.getFiles() ?? [],
error: uppyInstance.current?.getState,
state,
upload: () =>
uppyInstance.current?.upload() ??
new Error("[useUppy] Uppy has not initialized yet."),
getInputProps: () => inputProps,
getRootProps,
removeFile,
cancelAll
}
}

View File

@ -25,7 +25,7 @@ const buttonVariants = cva(
size: {
default: "h-9 px-4 py-2",
sm: "h-8 rounded-md px-3 text-xs",
lg: "h-10 rounded-md px-8",
lg: "h-16 rounded-md px-8",
icon: "h-9 w-9",
},
},

View File

@ -0,0 +1,120 @@
import * as React from "react"
import * as DialogPrimitive from "@radix-ui/react-dialog"
import { Cross2Icon } from "@radix-ui/react-icons"
import { cn } from "~/utils"
const Dialog = DialogPrimitive.Root
const DialogTrigger = DialogPrimitive.Trigger
const DialogPortal = DialogPrimitive.Portal
const DialogClose = DialogPrimitive.Close
const DialogOverlay = React.forwardRef<
React.ElementRef<typeof DialogPrimitive.Overlay>,
React.ComponentPropsWithoutRef<typeof DialogPrimitive.Overlay>
>(({ className, ...props }, ref) => (
<DialogPrimitive.Overlay
ref={ref}
className={cn(
"fixed inset-0 z-50 bg-black/80 data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0",
className
)}
{...props}
/>
))
DialogOverlay.displayName = DialogPrimitive.Overlay.displayName
const DialogContent = React.forwardRef<
React.ElementRef<typeof DialogPrimitive.Content>,
React.ComponentPropsWithoutRef<typeof DialogPrimitive.Content>
>(({ className, children, ...props }, ref) => (
<DialogPortal>
<DialogOverlay />
<DialogPrimitive.Content
ref={ref}
className={cn(
"fixed left-[50%] top-[50%] z-50 grid w-full max-w-lg translate-x-[-50%] translate-y-[-50%] gap-4 border bg-background p-6 shadow-lg duration-200 data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 data-[state=closed]:slide-out-to-left-1/2 data-[state=closed]:slide-out-to-top-[48%] data-[state=open]:slide-in-from-left-1/2 data-[state=open]:slide-in-from-top-[48%] sm:rounded-lg",
className
)}
{...props}
>
{children}
<DialogPrimitive.Close className="absolute right-4 top-4 rounded-sm opacity-70 ring-offset-background transition-opacity hover:opacity-100 focus:outline-none focus:ring-2 focus:ring-ring focus:ring-offset-2 disabled:pointer-events-none data-[state=open]:bg-accent data-[state=open]:text-muted-foreground">
<Cross2Icon className="h-4 w-4" />
<span className="sr-only">Close</span>
</DialogPrimitive.Close>
</DialogPrimitive.Content>
</DialogPortal>
))
DialogContent.displayName = DialogPrimitive.Content.displayName
const DialogHeader = ({
className,
...props
}: React.HTMLAttributes<HTMLDivElement>) => (
<div
className={cn(
"flex flex-col space-y-1.5 text-center sm:text-left",
className
)}
{...props}
/>
)
DialogHeader.displayName = "DialogHeader"
const DialogFooter = ({
className,
...props
}: React.HTMLAttributes<HTMLDivElement>) => (
<div
className={cn(
"flex flex-col-reverse sm:flex-row sm:justify-end sm:space-x-2",
className
)}
{...props}
/>
)
DialogFooter.displayName = "DialogFooter"
const DialogTitle = React.forwardRef<
React.ElementRef<typeof DialogPrimitive.Title>,
React.ComponentPropsWithoutRef<typeof DialogPrimitive.Title>
>(({ className, ...props }, ref) => (
<DialogPrimitive.Title
ref={ref}
className={cn(
"text-lg font-semibold leading-none tracking-tight",
className
)}
{...props}
/>
))
DialogTitle.displayName = DialogPrimitive.Title.displayName
const DialogDescription = React.forwardRef<
React.ElementRef<typeof DialogPrimitive.Description>,
React.ComponentPropsWithoutRef<typeof DialogPrimitive.Description>
>(({ className, ...props }, ref) => (
<DialogPrimitive.Description
ref={ref}
className={cn("text-sm text-muted-foreground", className)}
{...props}
/>
))
DialogDescription.displayName = DialogPrimitive.Description.displayName
export {
Dialog,
DialogPortal,
DialogOverlay,
DialogTrigger,
DialogClose,
DialogContent,
DialogHeader,
DialogFooter,
DialogTitle,
DialogDescription,
}

View File

@ -16,6 +16,7 @@
--primary: 242 51% 14%;
--primary-1: 241 90% 82%;
--primary-2: 241 21% 42%;
--primary-dark: 240 33% 4%;
--primary-foreground: 0 0% 88%;
--primary-1-foreground: 240 50% 9%;

1467
package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

@ -17,6 +17,7 @@
"@lumeweb/portal-sdk": "^0.0.0-20240306231947",
"@radix-ui/react-avatar": "^1.0.4",
"@radix-ui/react-checkbox": "^1.0.4",
"@radix-ui/react-dialog": "^1.0.5",
"@radix-ui/react-icons": "^1.3.0",
"@radix-ui/react-label": "^2.0.2",
"@radix-ui/react-progress": "^1.0.3",
@ -29,6 +30,9 @@
"@remix-run/react": "^2.8.0",
"@tanstack/react-table": "^8.13.2",
"@visx/visx": "^3.10.2",
"@uppy/core": "^3.9.3",
"@uppy/tus": "^3.5.3",
"@uppy/utils": "^5.7.4",
"class-variance-authority": "^0.7.0",
"clsx": "^2.1.0",
"react": "^18.2.0",

View File

@ -20,7 +20,7 @@ const config = {
border: "hsl(var(--border))",
input: {
DEFAULT: "hsl(var(--input))",
placeholder: "hsl(var(--input-placeholder))",
placeholder: "hsl(var(--input-placeholder))"
},
ring: "hsl(var(--ring))",
background: "hsl(var(--background))",
@ -34,7 +34,10 @@ const config = {
foreground: "hsl(var(--primary-1-foreground))"
},
"primary-2": {
DEFAULT: "hsl(var(--primary-2))",
DEFAULT: "hsl(var(--primary-2))"
},
"primary-dark": {
DEFAULT: "hsl(var(--primary-dark))"
},
secondary: {
DEFAULT: "hsl(var(--secondary))",

View File

@ -1,14 +1,14 @@
import { vitePlugin as remix } from "@remix-run/dev";
import { defineConfig } from "vite";
import tsconfigPaths from "vite-tsconfig-paths";
import { vitePlugin as remix } from "@remix-run/dev"
import { defineConfig } from "vite"
import tsconfigPaths from "vite-tsconfig-paths"
export default defineConfig({
plugins: [
remix({
ssr: false,
ignoredRouteFiles: ["**/*.css"],
ignoredRouteFiles: ["**/*.css"]
}),
tsconfigPaths(),
tsconfigPaths()
],
server: {
fs: {
@ -18,7 +18,10 @@ export default defineConfig({
// If you're comfortable with Vite's dev server making any file within the
// project root available, you can remove this option. See more:
// https://vitejs.dev/config/server-options.html#server-fs-allow
allow: ["app", "node_modules/@fontsource-variable/manrope"],
},
},
});
allow: [
"app",
"node_modules/@fontsource-variable/manrope",
]
}
}
})