mirror of
https://github.com/supabase/supabase.git
synced 2026-10-05 09:25:06 +03:00
1 parent
9d06a27e91
commit
6b0ea89f5b
9 files changed
+278
-188
No files matched your search
Generated
+2
-1
@@ -9794,7 +9794,8 @@
|
||||
},
|
||||
"node_modules/@radix-ui/react-dialog": {
|
||||
"version": "1.0.5",
|
||||
"license": "MIT",
|
||||
"resolved": "https://registry.npmjs.org/@radix-ui/react-dialog/-/react-dialog-1.0.5.tgz",
|
||||
"integrity": "sha512-GjWJX/AUpB703eEBanuBnIWdIXg6NvJFCXcNlSZk4xdszCdhrJgBoUd1cGk67vFO+WdA2pfI/plOpqz/5GUP6Q==",
|
||||
"dependencies": {
|
||||
"@babel/runtime": "^7.13.10",
|
||||
"@radix-ui/primitive": "1.0.1",
|
||||
|
||||
@@ -3,6 +3,7 @@ export * from './useConstant'
|
||||
export * from './useCopy'
|
||||
export * from './useDebounce'
|
||||
export * from './useDocsSearch'
|
||||
export * from './useDragToClose'
|
||||
export * from './useParams'
|
||||
export * from './useSearchParamsShallow'
|
||||
export * from './useTelemetryProps'
|
||||
|
||||
@@ -0,0 +1,42 @@
|
||||
import React from 'react'
|
||||
|
||||
/**
|
||||
* Custom hook to enable closing bottom drawers by dragging down.
|
||||
*/
|
||||
const useDragToClose = ({
|
||||
onClose,
|
||||
threshold = 100,
|
||||
}: {
|
||||
onClose: Function
|
||||
threshold?: number
|
||||
}) => {
|
||||
const [startY, setStartY] = React.useState(0)
|
||||
const [currentY, setCurrentY] = React.useState(0)
|
||||
const ref = React.useRef<HTMLDivElement>(null)
|
||||
|
||||
const handleTouchStart = (e: React.TouchEvent<HTMLDivElement>) => {
|
||||
setStartY(e.touches[0].clientY)
|
||||
}
|
||||
|
||||
const handleTouchMove = (e: React.TouchEvent<HTMLDivElement>) => {
|
||||
setCurrentY(e.touches[0].clientY)
|
||||
const translateY = currentY - startY
|
||||
if (translateY > 0 && ref.current) {
|
||||
ref.current.style.transform = `translateY(${translateY}px)`
|
||||
}
|
||||
}
|
||||
|
||||
const handleTouchEnd = () => {
|
||||
if (currentY - startY > threshold) {
|
||||
onClose()
|
||||
} else if (ref.current) {
|
||||
ref.current.style.transform = 'translateY(0)'
|
||||
}
|
||||
setStartY(0)
|
||||
setCurrentY(0)
|
||||
}
|
||||
|
||||
return { ref, handleTouchStart, handleTouchMove, handleTouchEnd }
|
||||
}
|
||||
|
||||
export default useDragToClose
|
||||
@@ -1,6 +1,5 @@
|
||||
const deepMerge = require('deepmerge')
|
||||
const forms = require('@tailwindcss/forms')
|
||||
const plugin = require('tailwindcss/plugin')
|
||||
const radixUiColors = require('@radix-ui/colors')
|
||||
const brandColors = require('./default-colors')
|
||||
const svgToDataUri = require('mini-svg-data-uri')
|
||||
|
||||
@@ -31,6 +31,7 @@ import ReactMarkdown from 'react-markdown'
|
||||
import remarkGfm from 'remark-gfm'
|
||||
import { cn } from 'ui/src/lib/utils'
|
||||
import { StatusIcon } from '../Icons/StatusIcons'
|
||||
import { isBrowser } from 'common'
|
||||
|
||||
const questions = [
|
||||
'How do I get started with Supabase?',
|
||||
@@ -320,12 +321,17 @@ export function queryAi(messages: Message[], timeout = 0) {
|
||||
|
||||
const AiCommand = () => {
|
||||
const { isLoading, setIsLoading, search, setSearch } = useCommandMenu()
|
||||
const isMobile = isBrowser && window.screen.width <= 640
|
||||
|
||||
const { submit, reset, messages, isResponding, hasError } = useAiChat({
|
||||
setIsLoading,
|
||||
})
|
||||
|
||||
const inputRef = useAutoInputFocus()
|
||||
/**
|
||||
* Disable autofocusing on the input on mobile
|
||||
* since it messes up with the dialog height and positioning
|
||||
* */
|
||||
const inputRef = useAutoInputFocus(!isMobile)
|
||||
|
||||
useHistoryKeys({
|
||||
enable: !isResponding,
|
||||
@@ -353,139 +359,43 @@ const AiCommand = () => {
|
||||
handleSubmit(search)
|
||||
}
|
||||
}, [])
|
||||
|
||||
// Detect an IME composition (so that we can ignore Enter keypress)
|
||||
const [isImeComposing, setIsImeComposing] = useState(false)
|
||||
const showSubmitButton = !isLoading && !isResponding && search
|
||||
|
||||
return (
|
||||
<div onClick={(e) => e.stopPropagation()}>
|
||||
<div className={cn('relative mb-[145px] py-4')}>
|
||||
{!hasError &&
|
||||
messages.map((message, index) => {
|
||||
switch (message.role) {
|
||||
case MessageRole.User:
|
||||
return (
|
||||
<div key={index} className="flex gap-6 mx-4 [overflow-anchor:none] mb-6">
|
||||
<div
|
||||
className="
|
||||
w-7 h-7 bg-background rounded-full border border-muted flex items-center justify-center text-foreground-lighter first-letter:
|
||||
ring-background
|
||||
ring-1
|
||||
shadow-sm
|
||||
"
|
||||
>
|
||||
<IconUser strokeWidth={1.5} size={16} />
|
||||
</div>
|
||||
<div className="prose text-foreground-lighter">{message.content}</div>
|
||||
</div>
|
||||
)
|
||||
case MessageRole.Assistant:
|
||||
return (
|
||||
<div key={index} className="px-4 [overflow-anchor:none] mb-[25px]">
|
||||
<div className="flex gap-6 [overflow-anchor:none] mb-6">
|
||||
<AiIconChat
|
||||
loading={
|
||||
message.status === MessageStatus.Pending ||
|
||||
message.status === MessageStatus.InProgress
|
||||
}
|
||||
/>
|
||||
<>
|
||||
{message.status === MessageStatus.Pending ? (
|
||||
<div className="bg-border-strong h-[21px] w-[13px] mt-1 animate-pulse animate-bounce"></div>
|
||||
) : (
|
||||
<ReactMarkdown
|
||||
remarkPlugins={[remarkGfm]}
|
||||
components={{
|
||||
...markdownComponents,
|
||||
a: (props) => (
|
||||
<a {...props} target="_blank" rel="noopener noreferrer" />
|
||||
),
|
||||
}}
|
||||
className="prose dark:prose-dark"
|
||||
urlTransform={(href: string) => {
|
||||
const supabaseUrl = new URL('https://supabase.com')
|
||||
const linkUrl = new URL(href, 'https://supabase.com')
|
||||
|
||||
if (linkUrl.origin === supabaseUrl.origin) {
|
||||
return linkUrl.toString()
|
||||
}
|
||||
|
||||
return href
|
||||
}}
|
||||
>
|
||||
{message.content}
|
||||
</ReactMarkdown>
|
||||
)}
|
||||
</>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
})}
|
||||
|
||||
{messages.length === 0 && !hasError && (
|
||||
<CommandGroup heading="Examples">
|
||||
{questions.map((question) => {
|
||||
const key = question.replace(/\s+/g, '_')
|
||||
return (
|
||||
<CommandItem
|
||||
type="command"
|
||||
onSelect={() => {
|
||||
if (!search) {
|
||||
handleSubmit(question)
|
||||
}
|
||||
}}
|
||||
key={key}
|
||||
>
|
||||
<AiIconAnimation />
|
||||
{question}
|
||||
</CommandItem>
|
||||
)
|
||||
})}
|
||||
</CommandGroup>
|
||||
)}
|
||||
{hasError && (
|
||||
<div className="p-6 flex flex-col items-center gap-2 mt-4">
|
||||
<StatusIcon variant="warning" />
|
||||
<div>
|
||||
<p className="text-sm text-foreground text-center">
|
||||
Sorry, looks like Supabase AI is having a hard time!
|
||||
</p>
|
||||
<p className="text-sm text-foreground-lighter text-center">
|
||||
Please try again in a bit.
|
||||
</p>
|
||||
</div>
|
||||
<Button size="tiny" type="default" onClick={handleReset}>
|
||||
Try again?
|
||||
</Button>
|
||||
</div>
|
||||
)}
|
||||
|
||||
<div className="[overflow-anchor:auto] h-px w-full"></div>
|
||||
</div>
|
||||
<div className="absolute bottom-0 w-full bg-background py-3">
|
||||
{messages.length > 0 && !hasError && <AiWarning className="mb-3 mx-3" />}
|
||||
<div className="absolute z-10 top-[45px] md:top-auto md:bottom-0 w-full bg-background pt-2 md:pt-0 md:pb-2">
|
||||
<Input
|
||||
className="bg-alternative rounded mx-3 [&_input]:pr-32 md:[&_input]:pr-40"
|
||||
className={cn(
|
||||
'bg-alternative rounded mx-3 [&_input]:text-base [&_input]:placeholder:text-sm',
|
||||
showSubmitButton && '[&_input]:pr-32 md:[&_input]:pr-40'
|
||||
)}
|
||||
inputRef={inputRef}
|
||||
autoFocus
|
||||
autoFocus={!isMobile}
|
||||
placeholder={
|
||||
isLoading || isResponding ? 'Waiting on an answer...' : 'Ask Supabase AI a question...'
|
||||
}
|
||||
value={search}
|
||||
actions={
|
||||
<>
|
||||
{!isLoading && !isResponding ? (
|
||||
<div
|
||||
className={`flex items-center gap-3 mr-3 transition-opacity duration-700 ${
|
||||
search ? 'opacity-100' : 'opacity-0'
|
||||
}`}
|
||||
{showSubmitButton ? (
|
||||
<Button
|
||||
onClick={() => {
|
||||
if (!search || isLoading || isResponding || isImeComposing) {
|
||||
return
|
||||
}
|
||||
return handleSubmit(search)
|
||||
}}
|
||||
className={`flex items-center mr-3 ${search ? 'opacity-100' : 'opacity-0'}`}
|
||||
iconRight={
|
||||
<div className="hidden md:flex">
|
||||
<IconCornerDownLeft size={12} strokeWidth={1.5} />
|
||||
</div>
|
||||
}
|
||||
>
|
||||
<span className="text-foreground-light">Submit message</span>
|
||||
<div className="hidden text-foreground-light md:flex items-center justify-center h-6 w-6 rounded bg-overlay-hover">
|
||||
<IconCornerDownLeft size={12} strokeWidth={1.5} />
|
||||
</div>
|
||||
</div>
|
||||
Submit message
|
||||
</Button>
|
||||
) : null}
|
||||
</>
|
||||
}
|
||||
@@ -509,6 +419,113 @@ const AiCommand = () => {
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
<div className="pt-14 md:pt-0">
|
||||
{messages.length > 0 && !hasError && <AiWarning className="mt-2 mx-3 px-3" />}
|
||||
<div className={cn('relative py-3')}>
|
||||
{!hasError &&
|
||||
messages.map((message, index) => {
|
||||
switch (message.role) {
|
||||
case MessageRole.User:
|
||||
return (
|
||||
<div key={index} className="flex gap-3 mx-4 [overflow-anchor:none] mb-6">
|
||||
<div
|
||||
className="
|
||||
w-7 h-7 bg-background rounded-full border border-muted flex items-center justify-center text-foreground-lighter first-letter:
|
||||
ring-background
|
||||
ring-1
|
||||
shadow-sm
|
||||
"
|
||||
>
|
||||
<IconUser strokeWidth={1.5} size={16} />
|
||||
</div>
|
||||
<div className="prose text-foreground-lighter">{message.content}</div>
|
||||
</div>
|
||||
)
|
||||
case MessageRole.Assistant:
|
||||
return (
|
||||
<div key={index} className="px-4 pb-8 [overflow-anchor:none] mb-[25px]">
|
||||
<div className="flex gap-4 [overflow-anchor:none] mb-6">
|
||||
<AiIconChat
|
||||
loading={
|
||||
message.status === MessageStatus.Pending ||
|
||||
message.status === MessageStatus.InProgress
|
||||
}
|
||||
/>
|
||||
<>
|
||||
{message.status === MessageStatus.Pending ? (
|
||||
<div className="bg-border-strong h-[21px] w-[13px] mt-1 animate-pulse"></div>
|
||||
) : (
|
||||
<ReactMarkdown
|
||||
remarkPlugins={[remarkGfm]}
|
||||
components={{
|
||||
...markdownComponents,
|
||||
a: (props) => (
|
||||
<a {...props} target="_blank" rel="noopener noreferrer" />
|
||||
),
|
||||
}}
|
||||
className="prose dark:prose-dark"
|
||||
urlTransform={(href: string) => {
|
||||
const supabaseUrl = new URL('https://supabase.com')
|
||||
const linkUrl = new URL(href, 'https://supabase.com')
|
||||
|
||||
if (linkUrl.origin === supabaseUrl.origin) {
|
||||
return linkUrl.toString()
|
||||
}
|
||||
|
||||
return href
|
||||
}}
|
||||
>
|
||||
{message.content}
|
||||
</ReactMarkdown>
|
||||
)}
|
||||
</>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
})}
|
||||
|
||||
{messages.length === 0 && !hasError && (
|
||||
<CommandGroup heading="Examples">
|
||||
{questions.map((question) => {
|
||||
const key = question.replace(/\s+/g, '_')
|
||||
return (
|
||||
<CommandItem
|
||||
type="command"
|
||||
onSelect={() => {
|
||||
if (!search) {
|
||||
handleSubmit(question)
|
||||
}
|
||||
}}
|
||||
key={key}
|
||||
>
|
||||
<AiIconAnimation />
|
||||
{question}
|
||||
</CommandItem>
|
||||
)
|
||||
})}
|
||||
</CommandGroup>
|
||||
)}
|
||||
{hasError && (
|
||||
<div className="p-6 flex flex-col items-center gap-2 mt-4">
|
||||
<StatusIcon variant="warning" />
|
||||
<div>
|
||||
<p className="text-sm text-foreground text-center">
|
||||
Sorry, looks like Supabase AI is having a hard time!
|
||||
</p>
|
||||
<p className="text-sm text-foreground-lighter text-center">
|
||||
Please try again in a bit.
|
||||
</p>
|
||||
</div>
|
||||
<Button size="tiny" type="default" onClick={handleReset}>
|
||||
Try again?
|
||||
</Button>
|
||||
</div>
|
||||
)}
|
||||
|
||||
<div className="[overflow-anchor:auto] h-px w-full"></div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { Command as CommandPrimitive } from 'cmdk'
|
||||
import * as React from 'react'
|
||||
import { Command as CommandPrimitive } from 'cmdk'
|
||||
import { ErrorBoundary } from 'react-error-boundary'
|
||||
|
||||
import { cn } from 'ui/src/lib/utils'
|
||||
@@ -10,6 +10,7 @@ import { Dialog, DialogContent } from 'ui'
|
||||
import { Button } from 'ui/src/components/Button'
|
||||
import { LoadingLine } from 'ui/src/components/LoadingLine/LoadingLine'
|
||||
import { useCommandMenu } from './CommandMenuContext'
|
||||
import { useBreakpoint } from 'common'
|
||||
|
||||
type CommandPrimitiveElement = React.ElementRef<typeof CommandPrimitive>
|
||||
type CommandPrimitiveProps = React.ComponentPropsWithoutRef<typeof CommandPrimitive>
|
||||
@@ -59,52 +60,62 @@ interface CommandDialogProps extends React.ComponentProps<typeof Dialog> {
|
||||
setIsOpen: (open: boolean) => void
|
||||
}
|
||||
|
||||
export const CommandDialog = ({
|
||||
children,
|
||||
onKeyDown,
|
||||
page,
|
||||
setIsOpen,
|
||||
...props
|
||||
}: CommandDialogProps) => {
|
||||
const [animateBounce, setAnimateBounce] = React.useState(false)
|
||||
type CommandPrimitiveDialogElement = React.ElementRef<typeof CommandPrimitive.Dialog>
|
||||
|
||||
React.useEffect(() => {
|
||||
setAnimateBounce(true)
|
||||
setTimeout(() => setAnimateBounce(false), 126)
|
||||
}, [page])
|
||||
export const CommandDialog = React.forwardRef<CommandPrimitiveDialogElement, CommandDialogProps>(
|
||||
({ children, onKeyDown, page, setIsOpen, ...props }: CommandDialogProps, ref) => {
|
||||
const isOpen = props.visible || props.open
|
||||
const isMobile = useBreakpoint()
|
||||
|
||||
return (
|
||||
<Dialog {...props} open={props.visible || props.open}>
|
||||
<DialogContent
|
||||
onInteractOutside={(e) => {
|
||||
// Only hide menu when clicking outside, not focusing outside
|
||||
// Prevents Firefox dropdown issue that immediately closes menu after opening
|
||||
if (e.type === 'dismissableLayer.pointerDownOutside') {
|
||||
setIsOpen(!open)
|
||||
}
|
||||
}}
|
||||
hideClose
|
||||
size={'xlarge'}
|
||||
className={cn('place-self-start mx-auto top-20')}
|
||||
>
|
||||
<ErrorBoundary FallbackComponent={CommandError}>
|
||||
<Command
|
||||
className={[
|
||||
'[&_[cmdk-group]]:px-2 [&_[cmdk-group]]:!bg-transparent [&_[cmdk-group-heading]]:!bg-transparent [&_[cmdk-group-heading]]:px-2 [&_[cmdk-group-heading]]:font-medium [&_[cmdk-group-heading]]:text-border-stronger [&_[cmdk-input]]:h-12',
|
||||
'[&_[cmdk-item]_svg]:h-5',
|
||||
'[&_[cmdk-item]_svg]:w-5',
|
||||
'[&_[cmdk-input-wrapper]_svg]:h-5',
|
||||
'[&_[cmdk-input-wrapper]_svg]:w-5',
|
||||
'[&_[cmdk-group]:not([hidden])_~[cmdk-group]]:pt-0',
|
||||
].join(' ')}
|
||||
>
|
||||
{children}
|
||||
</Command>
|
||||
</ErrorBoundary>
|
||||
</DialogContent>
|
||||
</Dialog>
|
||||
)
|
||||
}
|
||||
return (
|
||||
<Dialog {...props} open={isOpen} onOpenChange={setIsOpen}>
|
||||
<DialogContent
|
||||
ref={ref}
|
||||
forceMount
|
||||
onOpenAutoFocus={(e) => isMobile && e.preventDefault()}
|
||||
onInteractOutside={(e) => {
|
||||
// Only hide menu when clicking outside, not focusing outside
|
||||
// Prevents Firefox dropdown issue that immediately closes menu after opening
|
||||
if (e.type === 'dismissableLayer.pointerDownOutside') {
|
||||
setIsOpen(!open)
|
||||
}
|
||||
}}
|
||||
hideClose
|
||||
size="xlarge"
|
||||
dialogOverlayProps={{
|
||||
className: cn('overflow-hidden flex data-closed:delay-100'),
|
||||
}}
|
||||
className={cn(
|
||||
'relative my-0 mx-auto rounded-t-lg overflow-y-scroll',
|
||||
'h-[85dvh] mt-[15vh] md:max-h-[500px] md:mt-0 left-0 bottom-0 md:bottom-auto',
|
||||
'place-self-start md:place-self-auto',
|
||||
isOpen && '!animate-in !slide-in-from-bottom !duration-300',
|
||||
'data-[state=closed]:!animate-out data-[state=closed]:!slide-out-to-bottom',
|
||||
'md:data-[state=open]:!animate-in md:data-[state=closed]:!animate-out',
|
||||
'md:data-[state=closed]:!zoom-out-95 md:data-[state=open]:!zoom-in-95',
|
||||
'md:data-[state=closed]:!slide-out-to-left-[0%] md:data-[state=closed]:!slide-out-to-top-[0%]',
|
||||
'md:data-[state=open]:!slide-in-from-left-[0%] md:data-[state=open]:!slide-in-from-top-[0%]'
|
||||
)}
|
||||
>
|
||||
<ErrorBoundary FallbackComponent={CommandError}>
|
||||
<Command
|
||||
className={cn(
|
||||
'[&_[cmdk-group]]:px-2 [&_[cmdk-group]]:!bg-transparent [&_[cmdk-group-heading]]:!bg-transparent [&_[cmdk-group-heading]]:px-2 [&_[cmdk-group-heading]]:font-medium [&_[cmdk-group-heading]]:text-border-stronger [&_[cmdk-input]]:h-12',
|
||||
'[&_[cmdk-item]_svg]:h-5',
|
||||
'[&_[cmdk-item]_svg]:w-5',
|
||||
'[&_[cmdk-input-wrapper]_svg]:h-5',
|
||||
'[&_[cmdk-input-wrapper]_svg]:w-5',
|
||||
'[&_[cmdk-group]:not([hidden])_~[cmdk-group]]:pt-0'
|
||||
)}
|
||||
>
|
||||
{children}
|
||||
</Command>
|
||||
</ErrorBoundary>
|
||||
</DialogContent>
|
||||
</Dialog>
|
||||
)
|
||||
}
|
||||
)
|
||||
|
||||
CommandDialog.displayName = 'CommandDialog'
|
||||
|
||||
@@ -121,11 +132,10 @@ export const CommandInput = React.forwardRef<
|
||||
<div className="flex flex-col items-center" cmdk-input-wrapper="">
|
||||
<CommandPrimitive.Input
|
||||
value={value}
|
||||
autoFocus
|
||||
onValueChange={onValueChange}
|
||||
ref={ref}
|
||||
className={cn(
|
||||
'flex h-11 w-full rounded-md bg-transparent px-4 py-7 text-sm outline-none',
|
||||
'flex h-11 w-full rounded-md bg-transparent px-4 py-7 outline-none',
|
||||
'focus:shadow-none focus:ring-transparent',
|
||||
'text-foreground-light placeholder:text-foreground-muted disabled:cursor-not-allowed disabled:opacity-50 border-0',
|
||||
className
|
||||
@@ -418,12 +428,12 @@ export function useHistoryKeys({ enable, messages, setPrompt }: UseHistoryKeysOp
|
||||
*
|
||||
* @returns An input ref for the input to focus
|
||||
*/
|
||||
export function useAutoInputFocus() {
|
||||
export function useAutoInputFocus(isEnabled: boolean = true) {
|
||||
const [input, setInput] = React.useState<HTMLInputElement>()
|
||||
|
||||
// Use a callback-style ref to access the element when it mounts
|
||||
const inputRef = React.useCallback((inputElement: HTMLInputElement) => {
|
||||
if (inputElement) {
|
||||
if (isEnabled && inputElement) {
|
||||
setInput(inputElement)
|
||||
|
||||
// We need to delay the focus until the end of the call stack
|
||||
@@ -442,7 +452,7 @@ export function useAutoInputFocus() {
|
||||
}
|
||||
}
|
||||
|
||||
window.addEventListener('keydown', onKeyDown)
|
||||
isEnabled && window.addEventListener('keydown', onKeyDown)
|
||||
|
||||
return () => {
|
||||
window.removeEventListener('keydown', onKeyDown)
|
||||
|
||||
@@ -38,6 +38,8 @@ import GenerateSQL from './GenerateSQL'
|
||||
import SearchableStudioItems from './SearchableStudioItems'
|
||||
import ThemeOptions from './ThemeOptions'
|
||||
import sharedItems from './utils/shared-nav-items.json'
|
||||
import useDragToClose from 'common/hooks/useDragToClose'
|
||||
import { DialogHeader } from 'ui'
|
||||
|
||||
export const CHAT_ROUTES = [
|
||||
COMMAND_ROUTES.AI, // this one is temporary
|
||||
@@ -86,12 +88,22 @@ const CommandMenu = () => {
|
||||
currentPage === COMMAND_ROUTES.DOCS_SEARCH ||
|
||||
currentPage === COMMAND_ROUTES.AI ||
|
||||
currentPage === COMMAND_ROUTES.GENERATE_SQL
|
||||
? 'min(600px, 60vh)'
|
||||
: '425px'
|
||||
? 'min(600px, 70vh)'
|
||||
: 'auto'
|
||||
|
||||
const {
|
||||
ref: dialogRef,
|
||||
handleTouchStart,
|
||||
handleTouchMove,
|
||||
handleTouchEnd,
|
||||
} = useDragToClose({
|
||||
onClose: () => setIsOpen(!open),
|
||||
})
|
||||
|
||||
return (
|
||||
<>
|
||||
<CommandDialog
|
||||
ref={dialogRef}
|
||||
setIsOpen={setIsOpen}
|
||||
page={currentPage}
|
||||
visible={isOpen}
|
||||
@@ -99,15 +111,23 @@ const CommandMenu = () => {
|
||||
setIsOpen(!isOpen)
|
||||
}}
|
||||
>
|
||||
{pages.length > 0 && <CommandMenuShortcuts />}
|
||||
{showCommandInput && (
|
||||
<CommandInput
|
||||
ref={commandInputRef}
|
||||
placeholder="Type a command or search..."
|
||||
value={search}
|
||||
onValueChange={handleInputChange}
|
||||
/>
|
||||
)}
|
||||
<DialogHeader
|
||||
className="p-0 gap-0"
|
||||
// Close dialog by dragging the header down
|
||||
onTouchStart={handleTouchStart}
|
||||
onTouchMove={handleTouchMove}
|
||||
onTouchEnd={handleTouchEnd}
|
||||
>
|
||||
{pages.length > 0 && <CommandMenuShortcuts />}
|
||||
{showCommandInput && (
|
||||
<CommandInput
|
||||
ref={commandInputRef}
|
||||
placeholder="Type a command or search..."
|
||||
value={search}
|
||||
onValueChange={handleInputChange}
|
||||
/>
|
||||
)}
|
||||
</DialogHeader>
|
||||
<CommandList
|
||||
style={{
|
||||
maxHeight: commandListMaxHeight,
|
||||
@@ -118,7 +138,7 @@ const CommandMenu = () => {
|
||||
? commandListMaxHeight
|
||||
: 'auto',
|
||||
}}
|
||||
className="my-2"
|
||||
className="pb-20 md:pb-0"
|
||||
>
|
||||
{!currentPage && (
|
||||
<>
|
||||
|
||||
@@ -9,7 +9,7 @@ export default function CommandMenuShortcuts() {
|
||||
const { setPages, pages, currentPage } = useCommandMenu()
|
||||
|
||||
return (
|
||||
<div className="flex w-full gap-2 px-4 pt-4 justify-items-start flex-row items-center justify-between">
|
||||
<div className="flex w-full gap-2 px-4 pt-4 pb-2 justify-items-start flex-row items-center justify-between">
|
||||
<div className="flex gap-2 flex-row items-center">
|
||||
<CommandShortcut onClick={() => setPages([])}>
|
||||
<div className="flex items-center gap-2">
|
||||
|
||||
@@ -42,7 +42,7 @@ const DialogOverlay = React.forwardRef<
|
||||
ref={ref}
|
||||
className={cn(
|
||||
'bg-black/40 backdrop-blur-sm',
|
||||
'z-50 fixed inset-0 grid place-items-center overflow-y-auto data-open:animate-overlay-show data-closed:animate-overlay-hide',
|
||||
'z-50 fixed inset-0 grid place-items-center overflow-y-auto data-closed:animate-overlay-hide',
|
||||
className
|
||||
)}
|
||||
{...props}
|
||||
@@ -54,9 +54,9 @@ const DialogContentVariants = cva(
|
||||
cn(
|
||||
'my-8',
|
||||
'relative z-50 grid w-full border shadow-md dark:shadow-sm 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=open]:animate-in data-[state=closed]:animate-out',
|
||||
'data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95',
|
||||
'data-[state=closed]:slide-out-to-left-[0%] data-[state=closed]:slide-out-to-top-[0%',
|
||||
'data-[state=closed]:slide-out-to-left-[0%] data-[state=closed]:slide-out-to-top-[0%]',
|
||||
'data-[state=open]:slide-in-from-left-[0%] data-[state=open]:slide-in-from-top-[0%]',
|
||||
'sm:rounded-lg md:w-full',
|
||||
'bg-200'
|
||||
|
||||
Reference in new issue
Block a user