studio(chore): improve storage bucket presentation (#40230)
* badge and admonition updates * whole file row tappable * file bucket improvements * match tables across all storage types * keyboard focussable rows * share function * other bucket types * fix: a11y of table rows * clean up focus state on rows * accessibility * address review comments * move accessibility --------- Co-authored-by: Charis Lam <26616127+charislam@users.noreply.github.com>
This commit is contained in:
@@ -42,6 +42,7 @@ We use Tailwind for styling.
|
||||
- 'text-warning' for calling out information that needs action
|
||||
- 'text-destructive' for calling out when something went wrong
|
||||
- When needing to apply typography styles, read @apps/studio/styles/typography.scss and use one of the available classes instead of hard coding classes e.g. use "heading-default" instead of "text-sm font-medium"
|
||||
- When applying focus styles for keyboard navigation, read @apps/studio/styles/focus.scss for any appropriate classes for consistency with other focus styles
|
||||
|
||||
## Page structure
|
||||
|
||||
|
||||
@@ -35,6 +35,11 @@ export const docsConfig: DocsConfig = {
|
||||
href: '/docs/icons',
|
||||
items: [],
|
||||
},
|
||||
{
|
||||
items: [],
|
||||
href: '/docs/ui-patterns/accessibility',
|
||||
title: 'Accessibility',
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
|
||||
@@ -0,0 +1,50 @@
|
||||
---
|
||||
title: Accessibility
|
||||
description: Make Supabase work for everyone.
|
||||
---
|
||||
|
||||
Accessibility is about making an interface work for as many people as possible across as many circumstances as possible. All of us lean on affordances that accessible experiences provide:
|
||||
|
||||
- Keyboard navigation
|
||||
- Legible and resizable elements
|
||||
- Large tap targets
|
||||
- Clear and simple language
|
||||
|
||||
## Checklist
|
||||
|
||||
About to push some code? At a minimum, check your work against this list:
|
||||
|
||||
- Are interactive page elements [keyboard-focusable](#focus-management)?
|
||||
- Are all elements announcable by a [screen reader](#screen-reader-support)?
|
||||
- Are textual elements legible and scalable?
|
||||
- Can I use this on a smaller and/or older device?
|
||||
|
||||
## Focus management
|
||||
|
||||
All interactive page elements should be reachable by keyboard. They should also provide visual feedback upon selection via a `focus-visible` state. We use consistent focus styles such as `inset-focus` so users recognize this state instantly.
|
||||
|
||||
```jsx
|
||||
<TableCell>
|
||||
<p>{name}</p>
|
||||
<button
|
||||
className={cn('absolute inset-0', 'inset-focus')}
|
||||
onClick={(event) => handleBucketNavigation(name, event)}
|
||||
>
|
||||
<span className="sr-only">Go to bucket details</span>
|
||||
</button>
|
||||
</TableCell>
|
||||
```
|
||||
|
||||
Consider also affordances like `ctrl` and `meta` key support for opening in a new tab. Anything that you can do with a mouse input should be replicable by keyboard.
|
||||
|
||||
## Screen reader support
|
||||
|
||||
Textual elements are supported out-of-the-box by screen readers. Imagery of course should be described by `alt` tags.
|
||||
|
||||
Less obvious however are scaffolding elements that only makes sense visually, when paired with other content. For example: a table column for actions may not have a visual _Actions_ label because its purpose is obvious to a sighted person. For everyone else’s sake, this column should be titled with `sr-only` text:
|
||||
|
||||
```jsx
|
||||
<TableHead>
|
||||
<span className="sr-only">Actions</span>
|
||||
</TableHead>
|
||||
```
|
||||
@@ -64,7 +64,7 @@ export const DeleteAnalyticsBucketModal = ({
|
||||
visible={visible}
|
||||
size="medium"
|
||||
variant="destructive"
|
||||
title={`Confirm deletion of ${bucketId}`}
|
||||
title={`Delete bucket “${bucketId}”`}
|
||||
loading={isDeleting}
|
||||
confirmPlaceholder="Type bucket name"
|
||||
confirmString={bucketId ?? ''}
|
||||
|
||||
@@ -1,37 +1,23 @@
|
||||
import { ExternalLink, MoreVertical, Search, Trash2 } from 'lucide-react'
|
||||
import Link from 'next/link'
|
||||
import { ChevronRight, ExternalLink, Search } from 'lucide-react'
|
||||
import { useRouter } from 'next/navigation'
|
||||
import { useState } from 'react'
|
||||
|
||||
import { useParams } from 'common'
|
||||
import { ScaffoldHeader, ScaffoldSection, ScaffoldSectionTitle } from 'components/layouts/Scaffold'
|
||||
import { GenericSkeletonLoader } from 'components/ui/ShimmeringLoader'
|
||||
import { AnalyticsBucket, useAnalyticsBucketsQuery } from 'data/storage/analytics-buckets-query'
|
||||
import {
|
||||
Button,
|
||||
Card,
|
||||
DropdownMenu,
|
||||
DropdownMenuContent,
|
||||
DropdownMenuItem,
|
||||
DropdownMenuTrigger,
|
||||
Table,
|
||||
TableBody,
|
||||
TableCell,
|
||||
TableHead,
|
||||
TableHeader,
|
||||
TableRow,
|
||||
} from 'ui'
|
||||
import { useAnalyticsBucketsQuery } from 'data/storage/analytics-buckets-query'
|
||||
import { Bucket as BucketIcon } from 'icons'
|
||||
import { Button, Card, cn, Table, TableBody, TableCell, TableHead, TableHeader, TableRow } from 'ui'
|
||||
import { Admonition, TimestampInfo } from 'ui-patterns'
|
||||
import { Input } from 'ui-patterns/DataInputs/Input'
|
||||
import { EmptyBucketState } from '../EmptyBucketState'
|
||||
import { CreateAnalyticsBucketModal } from './CreateAnalyticsBucketModal'
|
||||
import { DeleteAnalyticsBucketModal } from './DeleteAnalyticsBucketModal'
|
||||
|
||||
export const AnalyticsBuckets = () => {
|
||||
const { ref } = useParams()
|
||||
const router = useRouter()
|
||||
|
||||
const [filterString, setFilterString] = useState('')
|
||||
const [selectedBucket, setSelectedBucket] = useState<AnalyticsBucket>()
|
||||
const [modal, setModal] = useState<'edit' | 'empty' | 'delete' | null>(null)
|
||||
|
||||
const { data: buckets = [], isLoading: isLoadingBuckets } = useAnalyticsBucketsQuery({
|
||||
projectRef: ref,
|
||||
@@ -41,13 +27,25 @@ export const AnalyticsBuckets = () => {
|
||||
filterString.length === 0 ? true : bucket.id.toLowerCase().includes(filterString.toLowerCase())
|
||||
)
|
||||
|
||||
const handleBucketNavigation = (
|
||||
bucketId: string,
|
||||
event: React.MouseEvent | React.KeyboardEvent
|
||||
) => {
|
||||
const url = `/project/${ref}/storage/analytics/buckets/${encodeURIComponent(bucketId)}`
|
||||
if (event.metaKey || event.ctrlKey) {
|
||||
window.open(url, '_blank')
|
||||
} else {
|
||||
router.push(url)
|
||||
}
|
||||
}
|
||||
|
||||
return (
|
||||
<ScaffoldSection isFullWidth>
|
||||
<Admonition
|
||||
type="warning"
|
||||
type="note"
|
||||
layout="horizontal"
|
||||
className="mb-12 [&>div]:!translate-y-0 [&>svg]:!translate-y-1"
|
||||
title="Analytics buckets are in alpha"
|
||||
className="-mt-4 mb-8 [&>div]:!translate-y-0 [&>svg]:!translate-y-1"
|
||||
title="Private alpha"
|
||||
actions={
|
||||
<Button asChild type="default" icon={<ExternalLink />}>
|
||||
<a
|
||||
@@ -55,15 +53,16 @@ export const AnalyticsBuckets = () => {
|
||||
rel="noopener noreferrer"
|
||||
href="https://github.com/orgs/supabase/discussions/40116"
|
||||
>
|
||||
Leave feedback
|
||||
Share feedback
|
||||
</a>
|
||||
</Button>
|
||||
}
|
||||
>
|
||||
<p className="!leading-normal !mb-0">
|
||||
Expect rapid changes, limited features, and possible breaking updates as we expand access.
|
||||
<p className="!leading-normal !mb-0 text-balance">
|
||||
Analytics buckets are now in private alpha. Expect rapid changes, limited features, and
|
||||
possible breaking updates. Please share feedback as we refine the experience and expand
|
||||
access.
|
||||
</p>
|
||||
<p className="!leading-normal !mb-0">Please share feedback as we refine the experience!</p>
|
||||
</Admonition>
|
||||
|
||||
{!isLoadingBuckets &&
|
||||
@@ -94,9 +93,16 @@ export const AnalyticsBuckets = () => {
|
||||
<Table>
|
||||
<TableHeader>
|
||||
<TableRow>
|
||||
{analyticsBuckets.length > 0 && (
|
||||
<TableHead className="w-2 pr-1">
|
||||
<span className="sr-only">Icon</span>
|
||||
</TableHead>
|
||||
)}
|
||||
<TableHead>Name</TableHead>
|
||||
<TableHead>Created at</TableHead>
|
||||
<TableHead />
|
||||
<TableHead>
|
||||
<span className="sr-only">Actions</span>
|
||||
</TableHead>
|
||||
</TableRow>
|
||||
</TableHeader>
|
||||
<TableBody>
|
||||
@@ -111,15 +117,18 @@ export const AnalyticsBuckets = () => {
|
||||
</TableRow>
|
||||
)}
|
||||
{analyticsBuckets.map((bucket) => (
|
||||
<TableRow key={bucket.id}>
|
||||
<TableRow key={bucket.id} className="relative cursor-pointer h-16">
|
||||
<TableCell className="w-2 pr-1">
|
||||
<BucketIcon size={16} className="text-foreground-muted" />
|
||||
</TableCell>
|
||||
<TableCell>
|
||||
<Link
|
||||
href={`/project/${ref}/storage/analytics/buckets/${encodeURIComponent(bucket.id)}`}
|
||||
title={bucket.id}
|
||||
className="text-link-table-cell"
|
||||
<p className="whitespace-nowrap max-w-[512px] truncate">{bucket.id}</p>
|
||||
<button
|
||||
className={cn('absolute inset-0', 'inset-focus')}
|
||||
onClick={(event) => handleBucketNavigation(bucket.id, event)}
|
||||
>
|
||||
{bucket.id}
|
||||
</Link>
|
||||
<span className="sr-only">Go to table details</span>
|
||||
</button>
|
||||
</TableCell>
|
||||
|
||||
<TableCell>
|
||||
@@ -132,31 +141,8 @@ export const AnalyticsBuckets = () => {
|
||||
</TableCell>
|
||||
|
||||
<TableCell>
|
||||
<div className="flex justify-end gap-2">
|
||||
<Button asChild type="default">
|
||||
<Link
|
||||
href={`/project/${ref}/storage/analytics/buckets/${encodeURIComponent(bucket.id)}`}
|
||||
>
|
||||
View contents
|
||||
</Link>
|
||||
</Button>
|
||||
<DropdownMenu>
|
||||
<DropdownMenuTrigger asChild>
|
||||
<Button type="default" className="px-1" icon={<MoreVertical />} />
|
||||
</DropdownMenuTrigger>
|
||||
<DropdownMenuContent side="bottom" align="end" className="w-40">
|
||||
<DropdownMenuItem
|
||||
className="flex items-center space-x-2"
|
||||
onClick={(e) => {
|
||||
setModal('delete')
|
||||
setSelectedBucket(bucket)
|
||||
}}
|
||||
>
|
||||
<Trash2 size={12} />
|
||||
<p>Delete bucket</p>
|
||||
</DropdownMenuItem>
|
||||
</DropdownMenuContent>
|
||||
</DropdownMenu>
|
||||
<div className="flex justify-end items-center h-full">
|
||||
<ChevronRight size={14} className="text-foreground-muted/60" />
|
||||
</div>
|
||||
</TableCell>
|
||||
</TableRow>
|
||||
@@ -167,14 +153,6 @@ export const AnalyticsBuckets = () => {
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
|
||||
{selectedBucket && (
|
||||
<DeleteAnalyticsBucketModal
|
||||
visible={modal === 'delete'}
|
||||
bucketId={selectedBucket.id}
|
||||
onClose={() => setModal(null)}
|
||||
/>
|
||||
)}
|
||||
</ScaffoldSection>
|
||||
)
|
||||
}
|
||||
|
||||
@@ -229,7 +229,7 @@ export const CreateBucketModal = ({
|
||||
|
||||
<DialogContent aria-describedby={undefined}>
|
||||
<DialogHeader>
|
||||
<DialogTitle>Create a storage bucket</DialogTitle>
|
||||
<DialogTitle>Create file bucket</DialogTitle>
|
||||
</DialogHeader>
|
||||
|
||||
<DialogSectionSeparator />
|
||||
|
||||
@@ -83,7 +83,7 @@ export const DeleteBucketModal = ({ visible, bucket, onClose }: DeleteBucketModa
|
||||
visible={visible}
|
||||
size="medium"
|
||||
variant="destructive"
|
||||
title={`Confirm deletion of ${bucket.id}`}
|
||||
title={`Delete bucket “${bucket.id}”`}
|
||||
loading={isDeletingBucket || isDeletingPolicies}
|
||||
confirmPlaceholder="Type bucket name"
|
||||
confirmString={bucket.id}
|
||||
|
||||
@@ -197,7 +197,7 @@ export const EditBucketModal = ({ visible, bucket, onClose }: EditBucketModalPro
|
||||
>
|
||||
<DialogContent>
|
||||
<DialogHeader>
|
||||
<DialogTitle>{`Edit bucket "${bucket?.name}"`}</DialogTitle>
|
||||
<DialogTitle>{`Edit bucket “${bucket?.name}”`}</DialogTitle>
|
||||
</DialogHeader>
|
||||
|
||||
<DialogSectionSeparator />
|
||||
|
||||
@@ -55,7 +55,7 @@ export const EmptyBucketModal = ({ visible, bucket, onClose }: EmptyBucketModalP
|
||||
>
|
||||
<DialogContent>
|
||||
<DialogHeader>
|
||||
<DialogTitle>{`Confirm to delete all contents from ${bucket?.name}`}</DialogTitle>
|
||||
<DialogTitle>{`Empty bucket “${bucket?.name}”`}</DialogTitle>
|
||||
</DialogHeader>
|
||||
<DialogSectionSeparator />
|
||||
<Admonition
|
||||
@@ -65,7 +65,9 @@ export const EmptyBucketModal = ({ visible, bucket, onClose }: EmptyBucketModalP
|
||||
description="The contents of your bucket cannot be recovered once deleted."
|
||||
/>
|
||||
<DialogSection>
|
||||
<p className="text-sm">Are you sure you want to empty the bucket "{bucket?.name}"?</p>
|
||||
<p className="text-sm">
|
||||
Are you sure you want to remove all contents from the bucket “{bucket?.name}”?
|
||||
</p>
|
||||
</DialogSection>
|
||||
<DialogFooter>
|
||||
<Button type="default" disabled={isLoading} onClick={onClose}>
|
||||
|
||||
@@ -1,6 +1,3 @@
|
||||
import { Edit, FolderOpen, MoreVertical, Trash2 } from 'lucide-react'
|
||||
import Link from 'next/link'
|
||||
|
||||
import {
|
||||
VirtualizedTableCell,
|
||||
VirtualizedTableHead,
|
||||
@@ -8,29 +5,21 @@ import {
|
||||
VirtualizedTableRow,
|
||||
} from 'components/ui/VirtualizedTable'
|
||||
import { Bucket } from 'data/storage/buckets-query'
|
||||
import { Bucket as BucketIcon } from 'icons'
|
||||
import { formatBytes } from 'lib/helpers'
|
||||
import {
|
||||
Badge,
|
||||
Button,
|
||||
cn,
|
||||
DropdownMenu,
|
||||
DropdownMenuContent,
|
||||
DropdownMenuItem,
|
||||
DropdownMenuSeparator,
|
||||
DropdownMenuTrigger,
|
||||
TableCell,
|
||||
TableHead,
|
||||
TableHeader,
|
||||
TableRow,
|
||||
} from 'ui'
|
||||
import { ChevronRight } from 'lucide-react'
|
||||
import { useRouter } from 'next/navigation'
|
||||
import type React from 'react'
|
||||
import { Badge, cn, TableCell, TableHead, TableHeader, TableRow } from 'ui'
|
||||
|
||||
type BucketTableMode = 'standard' | 'virtualized'
|
||||
|
||||
type BucketTableHeaderProps = {
|
||||
mode: BucketTableMode
|
||||
hasBuckets?: boolean
|
||||
}
|
||||
|
||||
export const BucketTableHeader = ({ mode }: BucketTableHeaderProps) => {
|
||||
export const BucketTableHeader = ({ mode, hasBuckets = true }: BucketTableHeaderProps) => {
|
||||
const BucketTableHeader = mode === 'standard' ? TableHeader : VirtualizedTableHeader
|
||||
const BucketTableRow = mode === 'standard' ? TableRow : VirtualizedTableRow
|
||||
const BucketTableHead = mode === 'standard' ? TableHead : VirtualizedTableHead
|
||||
@@ -40,7 +29,12 @@ export const BucketTableHeader = ({ mode }: BucketTableHeaderProps) => {
|
||||
return (
|
||||
<BucketTableHeader>
|
||||
<BucketTableRow>
|
||||
<BucketTableHead className={cn('w-[280px]', stickyClasses)}>Name</BucketTableHead>
|
||||
{hasBuckets && (
|
||||
<BucketTableHead className={`${stickyClasses} w-2 pr-1`}>
|
||||
<span className="sr-only">Icon</span>
|
||||
</BucketTableHead>
|
||||
)}
|
||||
<BucketTableHead className={stickyClasses}>Name</BucketTableHead>
|
||||
<BucketTableHead className={stickyClasses}>Policies</BucketTableHead>
|
||||
<BucketTableHead className={stickyClasses}>File size limit</BucketTableHead>
|
||||
<BucketTableHead className={stickyClasses}>Allowed MIME types</BucketTableHead>
|
||||
@@ -65,8 +59,8 @@ export const BucketTableEmptyState = ({ mode, filterString }: BucketTableEmptySt
|
||||
<BucketTableRow className="[&>td]:hover:bg-inherit">
|
||||
<BucketTableCell colSpan={5}>
|
||||
<p className="text-sm text-foreground">No results found</p>
|
||||
<p className="text-sm text-foreground-light">
|
||||
Your search for "{filterString}" did not return any results
|
||||
<p className="text-sm text-foreground-lighter">
|
||||
Your search for “{filterString}” did not return any results
|
||||
</p>
|
||||
</BucketTableCell>
|
||||
</BucketTableRow>
|
||||
@@ -79,8 +73,6 @@ type BucketTableRowProps = {
|
||||
projectRef: string
|
||||
formattedGlobalUploadLimit: string
|
||||
getPolicyCount: (bucketName: string) => number
|
||||
setSelectedBucket: (bucket: Bucket) => void
|
||||
setModal: (modal: 'edit' | 'empty' | 'delete' | null) => void
|
||||
}
|
||||
|
||||
export const BucketTableRow = ({
|
||||
@@ -89,25 +81,40 @@ export const BucketTableRow = ({
|
||||
projectRef,
|
||||
formattedGlobalUploadLimit,
|
||||
getPolicyCount,
|
||||
setSelectedBucket,
|
||||
setModal,
|
||||
}: BucketTableRowProps) => {
|
||||
const BucketTableRow = mode === 'standard' ? TableRow : VirtualizedTableRow
|
||||
const BucketTableCell = mode === 'standard' ? TableCell : VirtualizedTableCell
|
||||
|
||||
const router = useRouter()
|
||||
|
||||
const handleBucketNavigation = (
|
||||
bucketId: string,
|
||||
event: React.MouseEvent | React.KeyboardEvent
|
||||
) => {
|
||||
const url = `/project/${projectRef}/storage/files/buckets/${encodeURIComponent(bucketId)}`
|
||||
if (event.metaKey || event.ctrlKey) {
|
||||
window.open(url, '_blank')
|
||||
} else {
|
||||
router.push(url)
|
||||
}
|
||||
}
|
||||
|
||||
return (
|
||||
<BucketTableRow key={bucket.id}>
|
||||
<BucketTableCell>
|
||||
<div className="flex items-center gap-2">
|
||||
<Link
|
||||
href={`/project/${projectRef}/storage/files/buckets/${encodeURIComponent(bucket.id)}`}
|
||||
title={bucket.id}
|
||||
className="text-link-table-cell"
|
||||
>
|
||||
{bucket.id}
|
||||
</Link>
|
||||
<BucketTableRow key={bucket.id} className="relative cursor-pointer h-16">
|
||||
<BucketTableCell className="w-2 pr-1">
|
||||
<BucketIcon size={16} className="text-foreground-muted" />
|
||||
</BucketTableCell>
|
||||
<BucketTableCell className="flex-1">
|
||||
<div className="flex items-center gap-2.5">
|
||||
<p className="whitespace-nowrap max-w-[512px] truncate">{bucket.id}</p>
|
||||
{bucket.public && <Badge variant="warning">Public</Badge>}
|
||||
</div>
|
||||
<button
|
||||
className={cn('absolute inset-0', 'inset-focus')}
|
||||
onClick={(event) => handleBucketNavigation(bucket.id, event)}
|
||||
>
|
||||
<span className="sr-only">Go to bucket details</span>
|
||||
</button>
|
||||
</BucketTableCell>
|
||||
|
||||
<BucketTableCell>
|
||||
@@ -115,7 +122,9 @@ export const BucketTableRow = ({
|
||||
</BucketTableCell>
|
||||
|
||||
<BucketTableCell>
|
||||
<p className={bucket.file_size_limit ? 'text-foreground-light' : 'text-foreground-muted'}>
|
||||
<p
|
||||
className={`whitespace-nowrap ${bucket.file_size_limit ? 'text-foreground-light' : 'text-foreground-muted'}`}
|
||||
>
|
||||
{bucket.file_size_limit
|
||||
? formatBytes(bucket.file_size_limit)
|
||||
: `Unset (${formattedGlobalUploadLimit})`}
|
||||
@@ -131,71 +140,10 @@ export const BucketTableRow = ({
|
||||
</BucketTableCell>
|
||||
|
||||
<BucketTableCell>
|
||||
<div className="flex justify-end gap-2">
|
||||
<Button asChild type="default">
|
||||
<Link
|
||||
href={`/project/${projectRef}/storage/files/buckets/${encodeURIComponent(bucket.id)}`}
|
||||
>
|
||||
View files
|
||||
</Link>
|
||||
</Button>
|
||||
<BucketDropdownMenu
|
||||
bucket={bucket}
|
||||
setSelectedBucket={setSelectedBucket}
|
||||
setModal={setModal}
|
||||
/>
|
||||
<div className="flex justify-end items-center h-full">
|
||||
<ChevronRight size={14} className="text-foreground-muted/60" />
|
||||
</div>
|
||||
</BucketTableCell>
|
||||
</BucketTableRow>
|
||||
)
|
||||
}
|
||||
|
||||
type BucketDropdownMenuProps = {
|
||||
bucket: Bucket
|
||||
setSelectedBucket: (bucket: Bucket) => void
|
||||
setModal: (modal: 'edit' | 'empty' | 'delete' | null) => void
|
||||
}
|
||||
|
||||
const BucketDropdownMenu = ({ bucket, setSelectedBucket, setModal }: BucketDropdownMenuProps) => {
|
||||
return (
|
||||
<DropdownMenu>
|
||||
<DropdownMenuTrigger asChild>
|
||||
<Button type="default" className="px-1" icon={<MoreVertical />} />
|
||||
</DropdownMenuTrigger>
|
||||
<DropdownMenuContent side="bottom" align="end" className="w-40">
|
||||
<DropdownMenuItem
|
||||
className="flex items-center space-x-2"
|
||||
onClick={() => {
|
||||
setModal('edit')
|
||||
setSelectedBucket(bucket)
|
||||
}}
|
||||
>
|
||||
<Edit size={12} />
|
||||
<p>Edit bucket</p>
|
||||
</DropdownMenuItem>
|
||||
<DropdownMenuSeparator />
|
||||
<DropdownMenuItem
|
||||
className="flex items-center space-x-2"
|
||||
onClick={() => {
|
||||
setModal('empty')
|
||||
setSelectedBucket(bucket)
|
||||
}}
|
||||
>
|
||||
<FolderOpen size={12} />
|
||||
<p>Empty bucket</p>
|
||||
</DropdownMenuItem>
|
||||
|
||||
<DropdownMenuItem
|
||||
className="flex items-center space-x-2"
|
||||
onClick={() => {
|
||||
setModal('delete')
|
||||
setSelectedBucket(bucket)
|
||||
}}
|
||||
>
|
||||
<Trash2 size={12} />
|
||||
<p>Delete bucket</p>
|
||||
</DropdownMenuItem>
|
||||
</DropdownMenuContent>
|
||||
</DropdownMenu>
|
||||
)
|
||||
}
|
||||
|
||||
@@ -8,8 +8,6 @@ type BucketsTableProps = {
|
||||
projectRef: string
|
||||
filterString: string
|
||||
formattedGlobalUploadLimit: string
|
||||
setSelectedBucket: (bucket: Bucket) => void
|
||||
setModal: (modal: 'edit' | 'empty' | 'delete' | null) => void
|
||||
getPolicyCount: (bucketName: string) => number
|
||||
}
|
||||
|
||||
@@ -27,8 +25,6 @@ const BucketsTableUnvirtualized = ({
|
||||
projectRef,
|
||||
filterString,
|
||||
formattedGlobalUploadLimit,
|
||||
setSelectedBucket,
|
||||
setModal,
|
||||
getPolicyCount,
|
||||
}: BucketsTableProps) => {
|
||||
const showSearchEmptyState = buckets.length === 0 && filterString.length > 0
|
||||
@@ -37,7 +33,7 @@ const BucketsTableUnvirtualized = ({
|
||||
<Table
|
||||
containerProps={{ containerClassName: 'h-full overflow-auto', className: 'overflow-visible' }}
|
||||
>
|
||||
<BucketTableHeader mode="standard" />
|
||||
<BucketTableHeader mode="standard" hasBuckets={buckets.length > 0} />
|
||||
<TableBody>
|
||||
{showSearchEmptyState ? (
|
||||
<BucketTableEmptyState mode="standard" filterString={filterString} />
|
||||
@@ -50,8 +46,6 @@ const BucketsTableUnvirtualized = ({
|
||||
projectRef={projectRef}
|
||||
formattedGlobalUploadLimit={formattedGlobalUploadLimit}
|
||||
getPolicyCount={getPolicyCount}
|
||||
setSelectedBucket={setSelectedBucket}
|
||||
setModal={setModal}
|
||||
/>
|
||||
))
|
||||
)}
|
||||
@@ -65,15 +59,13 @@ const BucketsTableVirtualized = ({
|
||||
projectRef,
|
||||
filterString,
|
||||
formattedGlobalUploadLimit,
|
||||
setSelectedBucket,
|
||||
setModal,
|
||||
getPolicyCount,
|
||||
}: BucketsTableProps) => {
|
||||
const showSearchEmptyState = buckets.length === 0 && filterString.length > 0
|
||||
|
||||
return (
|
||||
<VirtualizedTable data={buckets} estimateSize={() => 59} getItemKey={(bucket) => bucket.id}>
|
||||
<BucketTableHeader mode="virtualized" />
|
||||
<BucketTableHeader mode="virtualized" hasBuckets={buckets.length > 0} />
|
||||
<VirtualizedTableBody<Bucket>
|
||||
paddingColSpan={5}
|
||||
emptyContent={
|
||||
@@ -90,8 +82,6 @@ const BucketsTableVirtualized = ({
|
||||
projectRef={projectRef}
|
||||
formattedGlobalUploadLimit={formattedGlobalUploadLimit}
|
||||
getPolicyCount={getPolicyCount}
|
||||
setSelectedBucket={setSelectedBucket}
|
||||
setModal={setModal}
|
||||
/>
|
||||
)}
|
||||
</VirtualizedTableBody>
|
||||
|
||||
@@ -109,8 +109,6 @@ export const FilesBuckets = () => {
|
||||
projectRef={ref ?? '_'}
|
||||
filterString={filterString}
|
||||
formattedGlobalUploadLimit={formattedGlobalUploadLimit}
|
||||
setSelectedBucket={setSelectedBucket}
|
||||
setModal={setModal}
|
||||
getPolicyCount={getPolicyCount}
|
||||
/>
|
||||
</Card>
|
||||
|
||||
@@ -34,7 +34,11 @@ export const StorageMenuV2 = () => {
|
||||
<Menu.Item rounded active={isSelected}>
|
||||
<div className="flex items-center justify-between">
|
||||
<p className="truncate">{config.displayName}</p>
|
||||
{isAlphaEnabled && <Badge variant="warning">ALPHA</Badge>}
|
||||
{isAlphaEnabled && (
|
||||
<Badge variant="default" size="small">
|
||||
New
|
||||
</Badge>
|
||||
)}
|
||||
</div>
|
||||
</Menu.Item>
|
||||
</Link>
|
||||
|
||||
@@ -61,7 +61,7 @@ export const DeleteVectorBucketModal = ({
|
||||
visible={visible}
|
||||
size="medium"
|
||||
variant="destructive"
|
||||
title={`Confirm deletion of ${bucketName}`}
|
||||
title={`Delete bucket “${bucketName}”`}
|
||||
loading={isDeletingBucket || isDeletingIndexes}
|
||||
confirmPlaceholder="Type bucket name"
|
||||
confirmString={bucketName ?? ''}
|
||||
|
||||
@@ -1,31 +1,19 @@
|
||||
import { ExternalLink, MoreVertical, Search, Trash2 } from 'lucide-react'
|
||||
import Link from 'next/link'
|
||||
import { ChevronRight, ExternalLink, Search } from 'lucide-react'
|
||||
import { useRouter } from 'next/navigation'
|
||||
import type React from 'react'
|
||||
import { useState } from 'react'
|
||||
|
||||
import { useParams } from 'common'
|
||||
import { ScaffoldHeader, ScaffoldSection, ScaffoldSectionTitle } from 'components/layouts/Scaffold'
|
||||
import { GenericSkeletonLoader } from 'components/ui/ShimmeringLoader'
|
||||
import { useVectorBucketsQuery } from 'data/storage/vector-buckets-query'
|
||||
import {
|
||||
Button,
|
||||
Card,
|
||||
DropdownMenu,
|
||||
DropdownMenuContent,
|
||||
DropdownMenuItem,
|
||||
DropdownMenuTrigger,
|
||||
Table,
|
||||
TableBody,
|
||||
TableCell,
|
||||
TableHead,
|
||||
TableHeader,
|
||||
TableRow,
|
||||
} from 'ui'
|
||||
import { Bucket as BucketIcon } from 'icons'
|
||||
import { Button, Card, cn, Table, TableBody, TableCell, TableHead, TableHeader, TableRow } from 'ui'
|
||||
import { Admonition } from 'ui-patterns'
|
||||
import { Input } from 'ui-patterns/DataInputs/Input'
|
||||
import { TimestampInfo } from 'ui-patterns/TimestampInfo'
|
||||
import { EmptyBucketState } from '../EmptyBucketState'
|
||||
import { CreateVectorBucketDialog } from './CreateVectorBucketDialog'
|
||||
import { DeleteVectorBucketModal } from './DeleteVectorBucketModal'
|
||||
|
||||
/**
|
||||
* [Joshen] Low-priority refactor: We should use a virtualized table here as per how we do it
|
||||
@@ -34,12 +22,9 @@ import { DeleteVectorBucketModal } from './DeleteVectorBucketModal'
|
||||
|
||||
export const VectorsBuckets = () => {
|
||||
const { ref: projectRef } = useParams()
|
||||
const router = useRouter()
|
||||
|
||||
const [filterString, setFilterString] = useState('')
|
||||
const [bucketForDeletion, setBucketForDeletion] = useState<{
|
||||
vectorBucketName: string
|
||||
creationTime: string
|
||||
} | null>(null)
|
||||
|
||||
const { data, isLoading: isLoadingBuckets } = useVectorBucketsQuery({ projectRef })
|
||||
const bucketsList = data?.vectorBuckets ?? []
|
||||
@@ -51,13 +36,25 @@ export const VectorsBuckets = () => {
|
||||
bucket.vectorBucketName.toLowerCase().includes(filterString.toLowerCase())
|
||||
)
|
||||
|
||||
const handleBucketNavigation = (
|
||||
bucketName: string,
|
||||
event: React.MouseEvent | React.KeyboardEvent
|
||||
) => {
|
||||
const url = `/project/${projectRef}/storage/vectors/buckets/${encodeURIComponent(bucketName)}`
|
||||
if (event.metaKey || event.ctrlKey) {
|
||||
window.open(url, '_blank')
|
||||
} else {
|
||||
router.push(url)
|
||||
}
|
||||
}
|
||||
|
||||
return (
|
||||
<ScaffoldSection isFullWidth>
|
||||
<Admonition
|
||||
type="warning"
|
||||
type="note"
|
||||
layout="horizontal"
|
||||
className="mb-12 [&>div]:!translate-y-0 [&>svg]:!translate-y-1"
|
||||
title="Vector buckets are in alpha"
|
||||
className="-mt-4 mb-8 [&>div]:!translate-y-0 [&>svg]:!translate-y-1"
|
||||
title="Private alpha"
|
||||
actions={
|
||||
<Button asChild type="default" icon={<ExternalLink />}>
|
||||
<a
|
||||
@@ -66,15 +63,16 @@ export const VectorsBuckets = () => {
|
||||
// [Joshen] To update with Vector specific GH discussion
|
||||
href="https://github.com/orgs/supabase/discussions/40116"
|
||||
>
|
||||
Leave feedback
|
||||
Share feedback
|
||||
</a>
|
||||
</Button>
|
||||
}
|
||||
>
|
||||
<p className="!leading-normal !mb-0">
|
||||
Expect rapid changes, limited features, and possible breaking updates as we expand access.
|
||||
<p className="!leading-normal !mb-0 text-balance">
|
||||
Vector buckets are now in private alpha. Expect rapid changes, limited features, and
|
||||
possible breaking updates. Please share feedback as we refine the experience and expand
|
||||
access.
|
||||
</p>
|
||||
<p className="!leading-normal !mb-0">Please share feedback as we refine the experience!</p>
|
||||
</Admonition>
|
||||
|
||||
{!isLoadingBuckets && bucketsList.length === 0 ? (
|
||||
@@ -103,14 +101,21 @@ export const VectorsBuckets = () => {
|
||||
<Table>
|
||||
<TableHeader>
|
||||
<TableRow>
|
||||
{filteredBuckets.length > 0 && (
|
||||
<TableHead className="w-2 pr-1">
|
||||
<span className="sr-only">Icon</span>
|
||||
</TableHead>
|
||||
)}
|
||||
<TableHead>Name</TableHead>
|
||||
<TableHead>Created at</TableHead>
|
||||
<TableHead />
|
||||
<TableHead>
|
||||
<span className="sr-only">Actions</span>
|
||||
</TableHead>
|
||||
</TableRow>
|
||||
</TableHeader>
|
||||
<TableBody>
|
||||
{filteredBuckets.length === 0 && filterString.length > 0 && (
|
||||
<TableRow>
|
||||
<TableRow className="[&>td]:hover:bg-inherit">
|
||||
<TableCell colSpan={3}>
|
||||
<p className="text-sm text-foreground">No results found</p>
|
||||
<p className="text-sm text-foreground-lighter">
|
||||
@@ -126,15 +131,18 @@ export const VectorsBuckets = () => {
|
||||
const created = +bucket.creationTime * 1000
|
||||
|
||||
return (
|
||||
<TableRow key={id}>
|
||||
<TableRow key={id} className="relative cursor-pointer h-16">
|
||||
<TableCell className="w-2 pr-1">
|
||||
<BucketIcon size={16} className="text-foreground-muted" />
|
||||
</TableCell>
|
||||
<TableCell>
|
||||
<Link
|
||||
href={`/project/${projectRef}/storage/vectors/buckets/${encodeURIComponent(name)}`}
|
||||
title={name}
|
||||
className="text-link-table-cell"
|
||||
<p className="whitespace-nowrap max-w-[512px] truncate">{name}</p>
|
||||
<button
|
||||
className={cn('absolute inset-0', 'inset-focus')}
|
||||
onClick={(event) => handleBucketNavigation(name, event)}
|
||||
>
|
||||
{name}
|
||||
</Link>
|
||||
<span className="sr-only">Go to bucket details</span>
|
||||
</button>
|
||||
</TableCell>
|
||||
<TableCell>
|
||||
<p className="text-foreground-light">
|
||||
@@ -145,30 +153,8 @@ export const VectorsBuckets = () => {
|
||||
</p>
|
||||
</TableCell>
|
||||
<TableCell>
|
||||
<div className="flex justify-end gap-2">
|
||||
<Button asChild type="default">
|
||||
<Link
|
||||
href={`/project/${projectRef}/storage/vectors/buckets/${encodeURIComponent(name)}`}
|
||||
>
|
||||
View contents
|
||||
</Link>
|
||||
</Button>
|
||||
<DropdownMenu>
|
||||
<DropdownMenuTrigger asChild>
|
||||
<Button type="default" className="px-1" icon={<MoreVertical />} />
|
||||
</DropdownMenuTrigger>
|
||||
<DropdownMenuContent side="bottom" align="end" className="w-40">
|
||||
<DropdownMenuItem
|
||||
className="flex items-center space-x-2"
|
||||
onClick={() => {
|
||||
setBucketForDeletion(bucket)
|
||||
}}
|
||||
>
|
||||
<Trash2 size={12} />
|
||||
<p>Delete bucket</p>
|
||||
</DropdownMenuItem>
|
||||
</DropdownMenuContent>
|
||||
</DropdownMenu>
|
||||
<div className="flex justify-end items-center h-full">
|
||||
<ChevronRight size={14} className="text-foreground-muted/60" />
|
||||
</div>
|
||||
</TableCell>
|
||||
</TableRow>
|
||||
@@ -180,13 +166,6 @@ export const VectorsBuckets = () => {
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
|
||||
<DeleteVectorBucketModal
|
||||
visible={!!bucketForDeletion}
|
||||
bucketName={bucketForDeletion?.vectorBucketName!}
|
||||
onCancel={() => setBucketForDeletion(null)}
|
||||
onSuccess={() => setBucketForDeletion(null)}
|
||||
/>
|
||||
</ScaffoldSection>
|
||||
)
|
||||
}
|
||||
|
||||
@@ -48,10 +48,12 @@ export const PageHeader = ({
|
||||
{(displayBreadcrumbs.length > 0 ||
|
||||
(isCompact && (title || primaryActions || secondaryActions))) && (
|
||||
<div className={cn('flex items-center gap-4', isCompact ? 'justify-between' : 'mb-4')}>
|
||||
<div className="flex items-center gap-4">
|
||||
<div className="flex items-center gap-4 flex-1 min-w-0">
|
||||
{breadcrumbs.length > 0 ? (
|
||||
<Breadcrumb className={cn('text-foreground-muted', isCompact && 'text-base')}>
|
||||
<BreadcrumbList className={isCompact ? 'text-base' : 'text-xs'}>
|
||||
<Breadcrumb
|
||||
className={cn('text-foreground-muted', isCompact && 'text-base', 'min-w-0 flex-1')}
|
||||
>
|
||||
<BreadcrumbList className={cn(isCompact ? 'text-base' : 'text-xs', 'min-w-0')}>
|
||||
{breadcrumbs.map((item, index) => (
|
||||
<Fragment key={item.label || `breadcrumb-${index}`}>
|
||||
<BreadcrumbItem>
|
||||
@@ -81,19 +83,19 @@ export const PageHeader = ({
|
||||
{isCompact && title && (
|
||||
<>
|
||||
<BreadcrumbSeparator />
|
||||
<BreadcrumbItem>
|
||||
<BreadcrumbPageItem>{title}</BreadcrumbPageItem>
|
||||
<BreadcrumbItem className="min-w-0 flex-1">
|
||||
<BreadcrumbPageItem className="min-w-0">{title}</BreadcrumbPageItem>
|
||||
</BreadcrumbItem>
|
||||
</>
|
||||
)}
|
||||
</BreadcrumbList>
|
||||
</Breadcrumb>
|
||||
) : isCompact ? (
|
||||
title
|
||||
<div className="min-w-0 flex-1">{title}</div>
|
||||
) : null}
|
||||
</div>
|
||||
{isCompact && (
|
||||
<div className="flex items-center gap-2">
|
||||
<div className="flex items-center gap-2 flex-shrink-0">
|
||||
{secondaryActions && (
|
||||
<div className="flex items-center gap-2">{secondaryActions}</div>
|
||||
)}
|
||||
|
||||
@@ -2,6 +2,7 @@ import 'react-data-grid/lib/styles.css'
|
||||
import 'styles/code.scss'
|
||||
import 'styles/contextMenu.scss'
|
||||
import 'styles/editor.scss'
|
||||
import 'styles/focus.scss'
|
||||
import 'styles/graphiql-base.scss'
|
||||
import 'styles/grid.scss'
|
||||
import 'styles/main.scss'
|
||||
|
||||
@@ -1,9 +1,11 @@
|
||||
import { Edit, Shield } from 'lucide-react'
|
||||
import { ChevronDown, FolderOpen, Settings, Shield, Trash2 } from 'lucide-react'
|
||||
import Link from 'next/link'
|
||||
import { useState } from 'react'
|
||||
|
||||
import { useParams } from 'common'
|
||||
import { DeleteBucketModal } from 'components/interfaces/Storage/DeleteBucketModal'
|
||||
import { EditBucketModal } from 'components/interfaces/Storage/EditBucketModal'
|
||||
import { EmptyBucketModal } from 'components/interfaces/Storage/EmptyBucketModal'
|
||||
import StorageBucketsError from 'components/interfaces/Storage/StorageBucketsError'
|
||||
import { StorageExplorer } from 'components/interfaces/Storage/StorageExplorer/StorageExplorer'
|
||||
import { useSelectedBucket } from 'components/interfaces/Storage/StorageExplorer/useSelectedBucket'
|
||||
@@ -15,14 +17,22 @@ import { useSelectedProjectQuery } from 'hooks/misc/useSelectedProject'
|
||||
import { useStoragePolicyCounts } from 'hooks/storage/useStoragePolicyCounts'
|
||||
import { useStorageExplorerStateSnapshot } from 'state/storage-explorer'
|
||||
import type { NextPageWithLayout } from 'types'
|
||||
import { Badge, Button } from 'ui'
|
||||
import {
|
||||
Badge,
|
||||
Button,
|
||||
DropdownMenu,
|
||||
DropdownMenuContent,
|
||||
DropdownMenuItem,
|
||||
DropdownMenuSeparator,
|
||||
DropdownMenuTrigger,
|
||||
} from 'ui'
|
||||
|
||||
const BucketPage: NextPageWithLayout = () => {
|
||||
const { bucketId, ref } = useParams()
|
||||
const { data: project } = useSelectedProjectQuery()
|
||||
const { projectRef } = useStorageExplorerStateSnapshot()
|
||||
const { bucket, error, isSuccess, isError } = useSelectedBucket()
|
||||
const [showEditModal, setShowEditModal] = useState(false)
|
||||
const [modal, setModal] = useState<'edit' | 'empty' | 'delete' | null>(null)
|
||||
|
||||
const { getPolicyCount } = useStoragePolicyCounts(bucket ? [bucket as Bucket] : [])
|
||||
const policyCount = bucket ? getPolicyCount(bucket.id) : 0
|
||||
@@ -50,9 +60,13 @@ const BucketPage: NextPageWithLayout = () => {
|
||||
isCompact
|
||||
className="[&>div:first-child]:!border-b-0" // Override the border-b from ScaffoldContainer
|
||||
title={
|
||||
<div className="flex items-center gap-2">
|
||||
<span>{bucket.name}</span>
|
||||
{bucket.public && <Badge variant="warning">Public</Badge>}
|
||||
<div className="flex items-center gap-2 min-w-0 flex-1">
|
||||
<span className="truncate">{bucket.name}</span>
|
||||
{bucket.public && (
|
||||
<Badge variant="warning" size="small" className="flex-shrink-0">
|
||||
Public
|
||||
</Badge>
|
||||
)}
|
||||
</div>
|
||||
}
|
||||
breadcrumbs={[
|
||||
@@ -77,9 +91,37 @@ const BucketPage: NextPageWithLayout = () => {
|
||||
>
|
||||
<Link href={`/project/${ref}/storage/files/policies`}>Policies</Link>
|
||||
</Button>
|
||||
<Button type="default" icon={<Edit size={14} />} onClick={() => setShowEditModal(true)}>
|
||||
Edit bucket
|
||||
</Button>
|
||||
<DropdownMenu>
|
||||
<DropdownMenuTrigger asChild>
|
||||
<Button type="default" iconRight={<ChevronDown size={14} />}>
|
||||
Edit bucket
|
||||
</Button>
|
||||
</DropdownMenuTrigger>
|
||||
<DropdownMenuContent align="end" className="w-40">
|
||||
<DropdownMenuItem
|
||||
className="flex items-center space-x-2"
|
||||
onClick={() => setModal('edit')}
|
||||
>
|
||||
<Settings size={12} />
|
||||
<p>Bucket settings</p>
|
||||
</DropdownMenuItem>
|
||||
<DropdownMenuSeparator />
|
||||
<DropdownMenuItem
|
||||
className="flex items-center space-x-2"
|
||||
onClick={() => setModal('empty')}
|
||||
>
|
||||
<FolderOpen size={12} />
|
||||
<p>Empty bucket</p>
|
||||
</DropdownMenuItem>
|
||||
<DropdownMenuItem
|
||||
className="flex items-center space-x-2"
|
||||
onClick={() => setModal('delete')}
|
||||
>
|
||||
<Trash2 size={12} />
|
||||
<p>Delete bucket</p>
|
||||
</DropdownMenuItem>
|
||||
</DropdownMenuContent>
|
||||
</DropdownMenu>
|
||||
</>
|
||||
}
|
||||
>
|
||||
@@ -88,11 +130,25 @@ const BucketPage: NextPageWithLayout = () => {
|
||||
</div>
|
||||
</PageLayout>
|
||||
|
||||
<EditBucketModal
|
||||
visible={showEditModal}
|
||||
bucket={bucket}
|
||||
onClose={() => setShowEditModal(false)}
|
||||
/>
|
||||
{bucket && (
|
||||
<>
|
||||
<EditBucketModal
|
||||
visible={modal === 'edit'}
|
||||
bucket={bucket}
|
||||
onClose={() => setModal(null)}
|
||||
/>
|
||||
<EmptyBucketModal
|
||||
visible={modal === 'empty'}
|
||||
bucket={bucket}
|
||||
onClose={() => setModal(null)}
|
||||
/>
|
||||
<DeleteBucketModal
|
||||
visible={modal === 'delete'}
|
||||
bucket={bucket}
|
||||
onClose={() => setModal(null)}
|
||||
/>
|
||||
</>
|
||||
)}
|
||||
</>
|
||||
)
|
||||
}
|
||||
|
||||
4
apps/studio/styles/focus.scss
Normal file
4
apps/studio/styles/focus.scss
Normal file
@@ -0,0 +1,4 @@
|
||||
/* Focus styles for keyboard navigation on interactive table rows */
|
||||
.inset-focus {
|
||||
@apply ease-out duration-100 focus-visible:outline focus-visible:outline-2 focus-visible:-outline-offset-2 focus-visible:outline-brand-600 focus-visible:rounded-md transition-all;
|
||||
}
|
||||
@@ -3,34 +3,32 @@ import * as React from 'react'
|
||||
|
||||
import { cn } from '../../../lib/utils/cn'
|
||||
|
||||
const badgeVariants = cva(
|
||||
'inline-flex items-center px-2.5 py-0.5 rounded-full text-xs bg-opacity-10 whitespace-nowrap',
|
||||
{
|
||||
variants: {
|
||||
variant: {
|
||||
default: 'bg-surface-200 text-foreground-light border border-strong',
|
||||
warning: 'bg-warning text-warning border border-warning-500',
|
||||
success: 'bg-brand text-brand-600 border border-brand-500',
|
||||
destructive: 'bg-destructive text-destructive-600 border border-destructive-500',
|
||||
brand: 'bg-brand text-brand-600 border border-brand-500',
|
||||
secondary:
|
||||
'bg-secondary hover:bg-secondary/80 border-transparent text-secondary-foreground',
|
||||
outline: 'bg-transparent text border border-foreground-muted',
|
||||
},
|
||||
size: {
|
||||
small: 'px-2.5 py-0.5 text-xs',
|
||||
large: 'px-3 py-0.5 rounded-full text-sm',
|
||||
},
|
||||
dot: {
|
||||
true: '-ml-0.5 mr-1.5 h-2 w-2 rounded-full',
|
||||
},
|
||||
const badgeVariants = cva('inline-flex items-center rounded-full font-normal whitespace-nowrap', {
|
||||
variants: {
|
||||
variant: {
|
||||
default: 'bg-surface-75 text-foreground-light border border-strong',
|
||||
warning: 'bg-warning bg-opacity-10 text-warning border border-warning-500',
|
||||
success: 'bg-brand bg-opacity-10 text-brand-600 border border-brand-500',
|
||||
destructive:
|
||||
'bg-destructive bg-opacity-10 text-destructive-600 border border-destructive-500',
|
||||
brand: 'bg-brand bg-opacity-10 text-brand-600 border border-brand-500',
|
||||
secondary:
|
||||
'bg-secondary bg-opacity-10 hover:bg-secondary/80 border-transparent text-secondary-foreground',
|
||||
outline: 'bg-transparent text border border-foreground-muted',
|
||||
},
|
||||
defaultVariants: {
|
||||
variant: 'default',
|
||||
size: 'small',
|
||||
size: {
|
||||
small: 'px-2 py-0.5 text-xs',
|
||||
large: 'px-3 py-0.5 text-sm',
|
||||
},
|
||||
}
|
||||
)
|
||||
dot: {
|
||||
true: '-ml-0.5 mr-1.5 h-2 w-2',
|
||||
},
|
||||
},
|
||||
defaultVariants: {
|
||||
variant: 'default',
|
||||
size: 'small',
|
||||
},
|
||||
})
|
||||
|
||||
export interface BadgeProps
|
||||
extends React.HTMLAttributes<HTMLDivElement>,
|
||||
@@ -39,7 +37,7 @@ export interface BadgeProps
|
||||
function Badge({
|
||||
className,
|
||||
variant = 'default',
|
||||
size,
|
||||
size = 'small',
|
||||
dot = false,
|
||||
children,
|
||||
...props
|
||||
|
||||
@@ -161,7 +161,8 @@ const DialogTitle = React.forwardRef<
|
||||
>(({ className, ...props }, ref) => (
|
||||
<DialogPrimitive.Title
|
||||
ref={ref}
|
||||
className={cn('text-base leading-none font-normal', className)}
|
||||
// [Danny] max-w to make space for the close button
|
||||
className={cn('text-base leading-none font-normal max-w-[calc(100%-1rem)]', className)}
|
||||
{...props}
|
||||
/>
|
||||
))
|
||||
|
||||
Reference in New Issue
Block a user