chore(studio): remove @headlessui/react dependency (#44845)

Remove `@headlessui/react` as a direct dependency from both
`apps/studio` and `packages/ui`. It's incompatible with React 19 (at the
pinned v1 version) and overlaps with our existing Radix/shadcn
primitives.

The only usage was the `<Transition>` component in 3 files + a dead
`Overlay` component in `packages/ui`.

**Removed:**
- `@headlessui/react` from `apps/studio/package.json` and
`packages/ui/package.json`
- Dead `packages/ui/src/lib/Overlay/` directory (not exported or
imported anywhere)

**Changed:**
- `ChooseFunctionForm.tsx` — replaced `Transition` with a shadcn
`Accordion` for the "View definition" toggle
- `FileExplorerColumn.tsx` — replaced `Transition` with `framer-motion`
`AnimatePresence` for drag-over overlay
- `PreviewPane.tsx` — removed `Transition` wrapper entirely (wasn't
visually animating on prod), replaced with simple conditional render

Note: `@headlessui/react` will remain in `pnpm-lock.yaml` as a
transitive dependency of `@graphiql/react` and
`@graphiql/plugin-doc-explorer` — that's expected and not something we
control.

## To test

- **Triggers page** (`/dashboard/project/_/database/triggers`): Create
or edit a trigger, click "Choose a function" to open the side panel.
Click "View definition" on a function row — the SQL definition should
expand/collapse with a smooth height animation. Clicking the row itself
should still select the function.
- **Storage explorer**
(`/dashboard/project/_/storage/buckets/<bucket>`): Navigate into a
folder, drag a file over the column — the drag overlay should fade
in/out smoothly.
- **Storage file preview**
(`/dashboard/project/_/storage/buckets/<bucket>`): Click on a file — the
preview pane should appear on the right (no animation, same as current
prod behaviour).

<!-- This is an auto-generated comment: release notes by coderabbit.ai
-->
## Summary by CodeRabbit

* **Refactor**
* Replaced several transition wrappers with new animation/mounting
behavior for overlays, preview panes, and drag-over UI to improve
consistency and responsiveness.
* Swapped the function-definition toggle for an Accordion and updated
click handling to prevent accidental row selection.
* Removed the legacy overlay component, its context, and associated
overlay styling.

* **Chores**
  * Removed HeadlessUI dependency from project packages.
<!-- end of auto-generated comment: release notes by coderabbit.ai -->

---------

Co-authored-by: Alaister Young <10985857+alaister@users.noreply.github.com>
This commit is contained in:
Alaister Young
2026-04-14 17:51:29 +09:00
committed by GitHub
parent 623589c915
commit c31230a90e
9 changed files with 187 additions and 388 deletions
@@ -1,11 +1,16 @@
import { Transition } from '@headlessui/react'
import { useParams } from 'common'
import { map as lodashMap, uniqBy } from 'lodash'
import { ChevronDown, HelpCircle, Terminal } from 'lucide-react'
import { HelpCircle, Terminal } from 'lucide-react'
import Link from 'next/link'
import { useRouter } from 'next/router'
import { useState } from 'react'
import { Button, SidePanel } from 'ui'
import {
Accordion_Shadcn_,
AccordionContent_Shadcn_,
AccordionItem_Shadcn_,
AccordionTrigger_Shadcn_,
Button,
SidePanel,
} from 'ui'
import ProductEmptyState from '@/components/to-be-cleaned/ProductEmptyState'
import InformationBox from '@/components/ui/InformationBox'
@@ -145,40 +150,31 @@ export interface FunctionProps {
}
const Function = ({ id, completeStatement, name, onClick }: FunctionProps) => {
const [visible, setVisible] = useState(false)
return (
<div className="cursor-pointer rounded p-3 px-6 hover:bg-studio" onClick={() => onClick(id)}>
<div className="flex items-center justify-between space-x-3">
<div className="flex items-center space-x-3">
<div className="flex items-center justify-center rounded bg-foreground p-1 text-background">
<Terminal strokeWidth={2} size={14} />
<Accordion_Shadcn_ type="single" collapsible>
<AccordionItem_Shadcn_ value="definition" className="border-none">
<div className="flex items-center justify-between space-x-3">
<div className="flex items-center space-x-3">
<div className="flex items-center justify-center rounded bg-foreground p-1 text-background">
<Terminal strokeWidth={2} size={14} />
</div>
<p className="mb-0 text-sm">{name}</p>
</div>
<AccordionTrigger_Shadcn_
onClick={(e) => e.stopPropagation()}
className="py-0 text-xs font-normal text-foreground-light hover:no-underline"
>
View definition
</AccordionTrigger_Shadcn_>
</div>
<p className="mb-0 text-sm">{name}</p>
</div>
<Button
type="text"
onClick={(e) => {
e.stopPropagation()
setVisible(!visible)
}}
icon={<ChevronDown className={visible ? 'rotate-180 transform' : 'rotate-0 transform'} />}
>
{visible ? 'Hide definition' : 'View definition'}
</Button>
</div>
<Transition
show={visible}
enter="transition ease-out duration-300"
enterFrom="transform opacity-0"
enterTo="transform opacity-100"
leave="transition ease-in duration-75"
leaveFrom="transform opacity-100"
leaveTo="transform opacity-0"
>
<div className="mt-4 h-64 border border-default">
<SqlEditor defaultValue={completeStatement} readOnly={true} contextmenu={false} />
</div>
</Transition>
<AccordionContent_Shadcn_ className="[&>div]:pb-0" onClick={(e) => e.stopPropagation()}>
<div className="mt-4 h-64 border border-default">
<SqlEditor defaultValue={completeStatement} readOnly={true} contextmenu={false} />
</div>
</AccordionContent_Shadcn_>
</AccordionItem_Shadcn_>
</Accordion_Shadcn_>
</div>
)
}
@@ -1,5 +1,5 @@
import { Transition } from '@headlessui/react'
import { PermissionAction } from '@supabase/shared-types/out/constants'
import { AnimatePresence, motion } from 'framer-motion'
import { get, noop, sum, uniqBy } from 'lodash'
import { ChevronsDown, ChevronsUp, Copy, Eye, FolderPlus, Upload } from 'lucide-react'
import { useCallback, useEffect, useMemo, useRef, useState } from 'react'
@@ -37,35 +37,36 @@ import { useStorageExplorerStateSnapshot } from '@/state/storage-explorer'
const DragOverOverlay = ({ isOpen, onDragLeave, onDrop, folderIsEmpty }: any) => {
return (
<Transition
show={isOpen}
enter="transition ease-out duration-100"
enterFrom="transform opacity-0"
enterTo="transform opacity-100"
leave="transition ease-in duration-75"
leaveFrom="transform opacity-100"
leaveTo="transform opacity-0"
className="h-full w-full absolute top-0"
>
<div
onDragLeave={onDragLeave}
onDrop={onDrop}
className="absolute top-0 flex h-full w-full items-center justify-center"
style={{ backgroundColor: folderIsEmpty ? 'rgba(0,0,0,0.1)' : 'rgba(0,0,0,0.2)' }}
>
{!folderIsEmpty && (
<AnimatePresence>
{isOpen && (
<motion.div
initial={{ opacity: 0 }}
animate={{ opacity: 1 }}
exit={{ opacity: 0 }}
transition={{ duration: 0.1, ease: 'easeOut' }}
className="h-full w-full absolute top-0"
>
<div
className="w-3/4 h-32 border-2 border-dashed border-muted rounded-md flex flex-col items-center justify-center p-6 pointer-events-none"
style={{ backgroundColor: 'rgba(0,0,0,0.4)' }}
onDragLeave={onDragLeave}
onDrop={onDrop}
className="absolute top-0 flex h-full w-full items-center justify-center"
style={{ backgroundColor: folderIsEmpty ? 'rgba(0,0,0,0.1)' : 'rgba(0,0,0,0.2)' }}
>
<Upload className="text-white pointer-events-none" size={20} strokeWidth={2} />
<p className="text-center text-sm text-white mt-2 pointer-events-none">
Drop your files to upload to this folder
</p>
{!folderIsEmpty && (
<div
className="w-3/4 h-32 border-2 border-dashed border-muted rounded-md flex flex-col items-center justify-center p-6 pointer-events-none"
style={{ backgroundColor: 'rgba(0,0,0,0.4)' }}
>
<Upload className="text-white pointer-events-none" size={20} strokeWidth={2} />
<p className="text-center text-sm text-white mt-2 pointer-events-none">
Drop your files to upload to this folder
</p>
</div>
)}
</div>
)}
</div>
</Transition>
</motion.div>
)}
</AnimatePresence>
)
}
@@ -1,6 +1,4 @@
import { Transition } from '@headlessui/react'
import { PermissionAction } from '@supabase/shared-types/out/constants'
import { isEmpty } from 'lodash'
import { AlertCircle, ChevronDown, Copy, Download, LoaderCircle, Trash2, X } from 'lucide-react'
import SVG from 'react-inlinesvg'
import {
@@ -129,154 +127,143 @@ export const PreviewPane = () => {
if (!file) return null
const width = 450
const isOpen = !isEmpty(file)
const size = file.metadata ? formatBytes(file.metadata.size) : null
const mimeType = file.metadata ? file.metadata.mimetype : undefined
const createdAt = file.created_at ? new Date(file.created_at).toLocaleString() : 'Unknown'
const updatedAt = file.updated_at ? new Date(file.updated_at).toLocaleString() : 'Unknown'
return (
<Transition
show={isOpen}
enter="transition ease-out duration-150"
enterFrom="transform opacity-0"
enterTo="transform opacity-100"
leave="transition ease-in duration-100"
leaveFrom="transform opacity-100"
leaveTo="transform opacity-0"
<div
className="h-full border-l border-overlay bg-surface-100 p-4 overflow-y-auto"
style={{ width }}
>
<div
className="h-full border-l border-overlay bg-surface-100 p-4 overflow-y-auto"
style={{ width }}
>
{/* Preview Header */}
<div className="flex w-full justify-end text-foreground-lighter transition-colors hover:text-foreground">
<X
className="cursor-pointer"
size={14}
strokeWidth={2}
onClick={() => setSelectedFilePreview(undefined)}
/>
</div>
{/* Preview Header */}
<div className="flex w-full justify-end text-foreground-lighter transition-colors hover:text-foreground">
<X
className="cursor-pointer"
size={14}
strokeWidth={2}
onClick={() => setSelectedFilePreview(undefined)}
/>
</div>
{/* Preview Thumbnail*/}
<div className="my-4 border border-overlay">
<div className="flex h-56 w-full items-center 2xl:h-72">
<PreviewFile item={file} />
</div>
</div>
<div className="w-full space-y-6">
{/* Preview Information */}
<div className="space-y-1">
<h5 className="break-words text-base text-foreground">{file.name}</h5>
{file.isCorrupted && (
<div className="flex items-center space-x-2">
<AlertCircle size={14} strokeWidth={2} className="text-foreground-light" />
<p className="text-sm text-foreground-light">
File is corrupted, please delete and reupload this file again
</p>
</div>
)}
{mimeType && (
<p className="text-sm text-foreground-light">
{mimeType}
{size && <span> - {size}</span>}
</p>
)}
</div>
{/* Preview Metadata */}
<div className="space-y-2">
<div>
<label className="mb-1 text-xs text-foreground-lighter">Added on</label>
<p className="text-sm text-foreground-light">{createdAt}</p>
</div>
<div>
<label className="mb-1 text-xs text-foreground-lighter">Last modified</label>
<p className="text-sm text-foreground-light">{updatedAt}</p>
</div>
</div>
{/* Actions */}
<div className="flex space-x-2 border-b border-overlay pb-4">
<Button
type="default"
icon={<Download />}
disabled={file.isCorrupted}
onClick={() => downloadFile(file)}
>
Download
</Button>
{selectedBucket.public ? (
<Button
type="outline"
icon={<Copy />}
onClick={() => onCopyUrl(file.path!)}
disabled={file.isCorrupted}
>
Get URL
</Button>
) : (
<DropdownMenu>
<DropdownMenuTrigger asChild>
<Button
type="outline"
icon={<Copy />}
iconRight={<ChevronDown />}
disabled={file.isCorrupted}
>
Get URL
</Button>
</DropdownMenuTrigger>
<DropdownMenuContent side="bottom" align="center">
<DropdownMenuItem
key="expires-one-week"
onClick={() => onCopyUrl(file.path!, URL_EXPIRY_DURATION.WEEK)}
>
Expire in 1 week
</DropdownMenuItem>
<DropdownMenuItem
key="expires-one-month"
onClick={() => onCopyUrl(file.path!, URL_EXPIRY_DURATION.MONTH)}
>
Expire in 1 month
</DropdownMenuItem>
<DropdownMenuItem
key="expires-one-year"
onClick={() => onCopyUrl(file.path!, URL_EXPIRY_DURATION.YEAR)}
>
Expire in 1 year
</DropdownMenuItem>
<DropdownMenuItem
key="custom-expiry"
onClick={() => setSelectedFileCustomExpiry(file)}
>
Custom expiry
</DropdownMenuItem>
</DropdownMenuContent>
</DropdownMenu>
)}
</div>
<ButtonTooltip
type="outline"
disabled={!canUpdateFiles}
size="tiny"
icon={<Trash2 strokeWidth={2} />}
onClick={() => setSelectedItemsToDelete([file])}
tooltip={{
content: {
side: 'bottom',
text: !canUpdateFiles
? 'You need additional permissions to delete this file'
: undefined,
},
}}
>
Delete file
</ButtonTooltip>
{/* Preview Thumbnail*/}
<div className="my-4 border border-overlay">
<div className="flex h-56 w-full items-center 2xl:h-72">
<PreviewFile item={file} />
</div>
</div>
</Transition>
<div className="w-full space-y-6">
{/* Preview Information */}
<div className="space-y-1">
<h5 className="break-words text-base text-foreground">{file.name}</h5>
{file.isCorrupted && (
<div className="flex items-center space-x-2">
<AlertCircle size={14} strokeWidth={2} className="text-foreground-light" />
<p className="text-sm text-foreground-light">
File is corrupted, please delete and reupload this file again
</p>
</div>
)}
{mimeType && (
<p className="text-sm text-foreground-light">
{mimeType}
{size && <span> - {size}</span>}
</p>
)}
</div>
{/* Preview Metadata */}
<div className="space-y-2">
<div>
<label className="mb-1 text-xs text-foreground-lighter">Added on</label>
<p className="text-sm text-foreground-light">{createdAt}</p>
</div>
<div>
<label className="mb-1 text-xs text-foreground-lighter">Last modified</label>
<p className="text-sm text-foreground-light">{updatedAt}</p>
</div>
</div>
{/* Actions */}
<div className="flex space-x-2 border-b border-overlay pb-4">
<Button
type="default"
icon={<Download />}
disabled={file.isCorrupted}
onClick={() => downloadFile(file)}
>
Download
</Button>
{selectedBucket.public ? (
<Button
type="outline"
icon={<Copy />}
onClick={() => onCopyUrl(file.path!)}
disabled={file.isCorrupted}
>
Get URL
</Button>
) : (
<DropdownMenu>
<DropdownMenuTrigger asChild>
<Button
type="outline"
icon={<Copy />}
iconRight={<ChevronDown />}
disabled={file.isCorrupted}
>
Get URL
</Button>
</DropdownMenuTrigger>
<DropdownMenuContent side="bottom" align="center">
<DropdownMenuItem
key="expires-one-week"
onClick={() => onCopyUrl(file.path!, URL_EXPIRY_DURATION.WEEK)}
>
Expire in 1 week
</DropdownMenuItem>
<DropdownMenuItem
key="expires-one-month"
onClick={() => onCopyUrl(file.path!, URL_EXPIRY_DURATION.MONTH)}
>
Expire in 1 month
</DropdownMenuItem>
<DropdownMenuItem
key="expires-one-year"
onClick={() => onCopyUrl(file.path!, URL_EXPIRY_DURATION.YEAR)}
>
Expire in 1 year
</DropdownMenuItem>
<DropdownMenuItem
key="custom-expiry"
onClick={() => setSelectedFileCustomExpiry(file)}
>
Custom expiry
</DropdownMenuItem>
</DropdownMenuContent>
</DropdownMenu>
)}
</div>
<ButtonTooltip
type="outline"
disabled={!canUpdateFiles}
size="tiny"
icon={<Trash2 strokeWidth={2} />}
onClick={() => setSelectedItemsToDelete([file])}
tooltip={{
content: {
side: 'bottom',
text: !canUpdateFiles
? 'You need additional permissions to delete this file'
: undefined,
},
}}
>
Delete file
</ButtonTooltip>
</div>
</div>
)
}
-1
View File
@@ -44,7 +44,6 @@
"@graphiql/react": "^0.19.4",
"@graphiql/toolkit": "^0.9.1",
"@hcaptcha/react-hcaptcha": "^1.12.0",
"@headlessui/react": "^1.7.17",
"@heroicons/react": "^2.1.3",
"@hookform/resolvers": "^3.1.1",
"@mjackson/multipart-parser": "^0.10.1",
-1
View File
@@ -19,7 +19,6 @@
"test:report": "open coverage/lcov-report/index.html"
},
"dependencies": {
"@headlessui/react": "^1.7.17",
"@radix-ui/react-accordion": "^1.2.12",
"@radix-ui/react-alert-dialog": "^1.1.15",
"@radix-ui/react-aspect-ratio": "^1.0.3",
@@ -1,75 +0,0 @@
/*
Overlay animations imported first as so placement styles are not affected.
*/
.sbui-overlay--enter {
@apply transition ease-out duration-100;
}
.sbui-overlay--enterFrom {
@apply transform opacity-0 scale-95;
}
.sbui-overlay--enterTo {
@apply transform opacity-100 scale-100;
}
.sbui-overlay--leave {
@apply transition ease-in duration-75;
}
.sbui-overlay--leaveFrom {
@apply transform opacity-100 scale-100;
}
.sbui-overlay--leaveTo {
@apply transform opacity-0 scale-95;
}
.sbui-overlay {
@apply relative inline-block;
}
.sbui-overlay-container {
@apply w-56;
}
.sbui-overlay-container--bottomRight {
@apply absolute origin-top-right right-0 mt-2;
}
.sbui-overlay-container--bottomLeft {
@apply absolute origin-top-left left-0 mt-2;
}
.sbui-overlay-container--bottomCenter {
margin-top: 0.5rem;
position: absolute;
transform-origin: center;
transform-origin: bottom center;
left: 50%;
-webkit-transform: translateX(-50%);
transform: translateX(-50%);
}
.sbui-overlay-container--topRight {
@apply origin-bottom-right;
margin-top: -0.5rem;
position: absolute;
left: 0;
transform-origin: top left;
transform: translatey(-100%);
}
.sbui-overlay-container--topLeft {
@apply origin-bottom-left;
margin-top: -0.5rem;
position: absolute;
right: 0;
transform-origin: top left;
transform: translatey(-100%);
}
.sbui-overlay-container--topCenter {
@apply origin-bottom-left;
margin-top: -0.5rem;
position: absolute;
left: 50%;
transform-origin: top left;
transform: translatey(-100%) translateX(-50%);
}
-95
View File
@@ -1,95 +0,0 @@
'use client'
import { Transition } from '@headlessui/react'
import React, { useEffect, useRef, useState } from 'react'
import { AnimationTailwindClasses } from '../../types'
//@ts-ignore
import { useOnClickOutside } from './../../lib/Hooks'
// @ts-ignore
import OverlayStyles from './Overlay.module.css'
import { DropdownContext } from './OverlayContext'
interface Props {
visible?: boolean
overlay?: React.ReactNode
children?: React.ReactNode
placement?: 'bottomLeft' | 'bottomRight' | 'bottomCenter' | 'topLeft' | 'topRight' | 'topCenter'
onVisibleChange?: any
disabled?: boolean
triggerElement?: any
overlayStyle?: React.CSSProperties
overlayClassName?: string
transition?: AnimationTailwindClasses
}
function Overlay({
visible,
overlay,
children,
placement = 'topCenter',
onVisibleChange,
disabled,
triggerElement,
overlayStyle,
overlayClassName,
transition,
}: Props) {
const ref = useRef(null)
const [visibleState, setVisibleState] = useState(!!visible)
let classes = [
OverlayStyles['sbui-overlay-container'],
OverlayStyles[`sbui-overlay-container--${placement}`],
]
if (overlayClassName) classes.push(overlayClassName)
function onToggle() {
setVisibleState(!visibleState)
if (onVisibleChange) onVisibleChange(visibleState)
}
// allow ovveride of Dropdown
useEffect(() => {
setVisibleState(!!visible)
}, [visible])
// detect clicks from outside
useOnClickOutside(ref, () => {
if (visibleState) {
setVisibleState(!visibleState)
}
})
const TriggerElement = () => {
return <div onClick={onToggle}>{triggerElement}</div>
}
return (
<div ref={ref} className={OverlayStyles['sbui-overlay']}>
{placement === 'bottomRight' || placement === 'bottomLeft' || placement === 'bottomCenter' ? (
<TriggerElement />
) : null}
<Transition
show={visibleState}
enter={OverlayStyles[`sbui-overlay--enter`]}
enterFrom={OverlayStyles[`sbui-overlay--enterFrom`]}
enterTo={OverlayStyles[`sbui-overlay--enterTo`]}
leave={OverlayStyles[`sbui-overlay--leave`]}
leaveFrom={OverlayStyles[`sbui-overlay--leaveFrom`]}
leaveTo={OverlayStyles[`sbui-overlay--leaveTo`]}
>
<div className={classes.join(' ')} style={overlayStyle}>
<DropdownContext.Provider value={{ onClick: onToggle }}>
{children}
</DropdownContext.Provider>
</div>
</Transition>
{placement === 'topRight' || placement === 'topLeft' || placement === 'topCenter' ? (
<TriggerElement />
) : null}
</div>
)
}
export default Overlay
@@ -1,7 +0,0 @@
import { createContext } from 'react'
// Make sure the shape of the default value passed to
// createContext matches the shape that the consumers expect!
export const DropdownContext = createContext({
onClick: (e: any) => {},
})
-6
View File
@@ -886,9 +886,6 @@ importers:
'@hcaptcha/react-hcaptcha':
specifier: ^1.12.0
version: 1.12.0(react-dom@18.3.1(react@18.3.1))(react@18.3.1)
'@headlessui/react':
specifier: ^1.7.17
version: 1.7.17(react-dom@18.3.1(react@18.3.1))(react@18.3.1)
'@heroicons/react':
specifier: ^2.1.3
version: 2.1.3(react@18.3.1)
@@ -2437,9 +2434,6 @@ importers:
packages/ui:
dependencies:
'@headlessui/react':
specifier: ^1.7.17
version: 1.7.17(react-dom@18.3.1(react@18.3.1))(react@18.3.1)
'@radix-ui/react-accordion':
specifier: ^1.2.12
version: 1.2.12(@types/react-dom@18.3.0)(@types/react@18.3.3)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)