mirror of
https://github.com/supabase/supabase.git
synced 2026-10-05 09:25:06 +03:00
fix(studio): align storage row icon weight (#48480)
## What kind of change does this PR introduce? Bug fix stacked on #48478. ## What is the current behavior? Storage Explorer renders the custom closed-folder icon at `1.5` but leaves the Lucide open-folder icon at its default `2`. The duplicated file-picker implementation also uses a different set of stroke-width overrides. ## What is the new behavior? A shared `StorageRowIcon` renders loading, open and closed folder, image, audio, video and generic file icons at `1.5` across Storage Explorer, row editing and the bucket file picker. Test by comparing open and closed folders and file-type rows in Storage Explorer and the bucket file picker. | Before | After | | --- | --- | | <img width="524" height="336" alt="CleanShot 2026-08-03 at 18 38 06@2x" src="https://github.com/user-attachments/assets/15eb8b9f-69fc-4eee-8428-d7ec26dce8dc" /> | <img width="522" height="328" alt="CleanShot 2026-08-03 at 18 39 20@2x" src="https://github.com/user-attachments/assets/17b7dddd-8d36-421c-8356-c9c1bd7456e8" /> | | _Thicker image and file icon compared to folder icon_ | _Every icon has the same stroke thickness_ | <!-- This is an auto-generated comment: release notes by coderabbit.ai --> ## Summary by CodeRabbit * **Improvements** * Standardized file, folder, media, and loading icons across storage views. * Improved visual consistency with unified icon sizing, styling, and stroke width. * **Tests** * Added coverage for loading, folder, media, and generic file icon states. <!-- end of auto-generated comment: release notes by coderabbit.ai -->
This commit is contained in:
1 parent
fc69c45985
commit
76257f164b
5 files changed
+135
-94
No files matched your search
+3
-45
@@ -1,54 +1,12 @@
|
||||
import { FilesBucket as FilesBucketIcon } from 'icons'
|
||||
import { AlertCircle, File, Film, FolderOpen, Image, LoaderCircle, Music } from 'lucide-react'
|
||||
import { AlertCircle, LoaderCircle } from 'lucide-react'
|
||||
import type { CSSProperties, MouseEvent } from 'react'
|
||||
import { Checkbox, cn, Tooltip, TooltipContent, TooltipTrigger } from 'ui'
|
||||
|
||||
import { STORAGE_ROW_STATUS, STORAGE_ROW_TYPES, STORAGE_VIEWS } from '../Storage.constants'
|
||||
import { type StorageItem } from '../Storage.types'
|
||||
import { StorageRowIcon } from '../StorageRowIcon'
|
||||
import { formatBytes } from '@/lib/helpers'
|
||||
|
||||
const RowIcon = ({
|
||||
view,
|
||||
status,
|
||||
fileType,
|
||||
isOpened = false,
|
||||
mimeType,
|
||||
}: {
|
||||
view: STORAGE_VIEWS
|
||||
status: STORAGE_ROW_STATUS
|
||||
fileType: string
|
||||
isOpened?: boolean
|
||||
mimeType: string | undefined
|
||||
}) => {
|
||||
if (view === STORAGE_VIEWS.LIST && status === STORAGE_ROW_STATUS.LOADING) {
|
||||
return (
|
||||
<LoaderCircle size={14} strokeWidth={2} className="animate-spin text-foreground-lighter" />
|
||||
)
|
||||
}
|
||||
|
||||
if (fileType === STORAGE_ROW_TYPES.FOLDER) {
|
||||
return isOpened ? (
|
||||
<FolderOpen size={16} strokeWidth={2} className="text-foreground-lighter" />
|
||||
) : (
|
||||
<FilesBucketIcon size={16} strokeWidth={2} className="text-foreground-lighter" />
|
||||
)
|
||||
}
|
||||
|
||||
if (mimeType?.includes('image')) {
|
||||
return <Image size={16} className="text-foreground-lighter" />
|
||||
}
|
||||
|
||||
if (mimeType?.includes('audio')) {
|
||||
return <Music size={16} strokeWidth={2} className="text-foreground-lighter" />
|
||||
}
|
||||
|
||||
if (mimeType?.includes('video')) {
|
||||
return <Film size={16} strokeWidth={2} className="text-foreground-lighter" />
|
||||
}
|
||||
|
||||
return <File size={16} strokeWidth={2} className="text-foreground-lighter" />
|
||||
}
|
||||
|
||||
interface BucketFilePickerRowProps {
|
||||
item: StorageItem
|
||||
view: STORAGE_VIEWS
|
||||
@@ -114,7 +72,7 @@ export const BucketFilePickerRow = ({
|
||||
hidden: isSelected,
|
||||
})}
|
||||
>
|
||||
<RowIcon
|
||||
<StorageRowIcon
|
||||
view={view}
|
||||
status={item.status}
|
||||
fileType={item.type}
|
||||
|
||||
@@ -1,19 +1,13 @@
|
||||
import { PermissionAction } from '@supabase/shared-types/out/constants'
|
||||
import { FilesBucket as FilesBucketIcon } from 'icons'
|
||||
import { find, isEmpty, isEqual } from 'lodash'
|
||||
import {
|
||||
AlertCircle,
|
||||
Copy,
|
||||
Download,
|
||||
Edit,
|
||||
File,
|
||||
Film,
|
||||
FolderOpen,
|
||||
Image,
|
||||
LoaderCircle,
|
||||
MoreVertical,
|
||||
Move,
|
||||
Music,
|
||||
Trash2,
|
||||
} from 'lucide-react'
|
||||
import type { CSSProperties } from 'react'
|
||||
@@ -41,6 +35,7 @@ import {
|
||||
URL_EXPIRY_DURATION,
|
||||
} from '../Storage.constants'
|
||||
import { StorageItemWithColumn, type StorageItem } from '../Storage.types'
|
||||
import { StorageRowIcon } from '../StorageRowIcon'
|
||||
import { useFileExplorerContextMenu } from './FileExplorerRowContextMenu'
|
||||
import { FileExplorerRowEditing } from './FileExplorerRowEditing'
|
||||
import { copyPathToFolder } from './StorageExplorer.utils'
|
||||
@@ -49,46 +44,6 @@ import { useAsyncCheckPermissions } from '@/hooks/misc/useCheckPermissions'
|
||||
import { formatBytes } from '@/lib/helpers'
|
||||
import { useStorageExplorerStateSnapshot } from '@/state/storage-explorer'
|
||||
|
||||
export const RowIcon = ({
|
||||
view,
|
||||
status,
|
||||
fileType,
|
||||
isOpened = false,
|
||||
mimeType,
|
||||
}: {
|
||||
view: STORAGE_VIEWS
|
||||
status: STORAGE_ROW_STATUS
|
||||
fileType: string
|
||||
isOpened?: boolean
|
||||
mimeType: string | undefined
|
||||
}) => {
|
||||
if (view === STORAGE_VIEWS.LIST && status === STORAGE_ROW_STATUS.LOADING) {
|
||||
return <LoaderCircle size={14} className="animate-spin text-foreground-lighter" />
|
||||
}
|
||||
|
||||
if (fileType === STORAGE_ROW_TYPES.FOLDER) {
|
||||
return isOpened ? (
|
||||
<FolderOpen size={16} className="text-foreground-lighter" />
|
||||
) : (
|
||||
<FilesBucketIcon size={16} className="text-foreground-lighter" />
|
||||
)
|
||||
}
|
||||
|
||||
if (mimeType?.includes('image')) {
|
||||
return <Image size={16} className="text-foreground-lighter" />
|
||||
}
|
||||
|
||||
if (mimeType?.includes('audio')) {
|
||||
return <Music size={16} className="text-foreground-lighter" />
|
||||
}
|
||||
|
||||
if (mimeType?.includes('video')) {
|
||||
return <Film size={16} className="text-foreground-lighter" />
|
||||
}
|
||||
|
||||
return <File size={16} className="text-foreground-lighter" />
|
||||
}
|
||||
|
||||
interface FileExplorerRowProps {
|
||||
index: number
|
||||
item: StorageItem
|
||||
@@ -329,7 +284,7 @@ export const FileExplorerRow = ({
|
||||
)}
|
||||
style={{ top: '2px' }}
|
||||
>
|
||||
<RowIcon
|
||||
<StorageRowIcon
|
||||
view={view}
|
||||
status={item.status}
|
||||
fileType={item.type}
|
||||
|
||||
+2
-2
@@ -3,7 +3,7 @@ import { useEffect, useRef, useState, type CSSProperties } from 'react'
|
||||
|
||||
import { STORAGE_ROW_STATUS, STORAGE_ROW_TYPES, STORAGE_VIEWS } from '../Storage.constants'
|
||||
import { StorageItem } from '../Storage.types'
|
||||
import { RowIcon } from './FileExplorerRow'
|
||||
import { StorageRowIcon } from '../StorageRowIcon'
|
||||
import { useStorageExplorerStateSnapshot } from '@/state/storage-explorer'
|
||||
|
||||
export interface FileExplorerRowEditingProps {
|
||||
@@ -94,7 +94,7 @@ export const FileExplorerRowEditing = ({
|
||||
>
|
||||
<div className="flex h-full grow items-center px-2.5">
|
||||
<div>
|
||||
<RowIcon
|
||||
<StorageRowIcon
|
||||
view={view}
|
||||
status={item.status}
|
||||
fileType={item.type}
|
||||
|
||||
@@ -0,0 +1,68 @@
|
||||
import { describe, expect, test } from 'vitest'
|
||||
|
||||
import { STORAGE_ROW_STATUS, STORAGE_ROW_TYPES, STORAGE_VIEWS } from './Storage.constants'
|
||||
import { StorageRowIcon } from './StorageRowIcon'
|
||||
import { customRender } from '@/tests/lib/custom-render'
|
||||
|
||||
const CASES: {
|
||||
name: string
|
||||
view?: STORAGE_VIEWS
|
||||
status?: STORAGE_ROW_STATUS
|
||||
fileType?: STORAGE_ROW_TYPES
|
||||
isOpened?: boolean
|
||||
mimeType?: string
|
||||
expectedIcon: string
|
||||
}[] = [
|
||||
{
|
||||
name: 'loading list row',
|
||||
view: STORAGE_VIEWS.LIST,
|
||||
status: STORAGE_ROW_STATUS.LOADING,
|
||||
expectedIcon: 'lucide-loader-circle',
|
||||
},
|
||||
{
|
||||
name: 'loading column row',
|
||||
view: STORAGE_VIEWS.COLUMNS,
|
||||
status: STORAGE_ROW_STATUS.LOADING,
|
||||
expectedIcon: 'lucide-file',
|
||||
},
|
||||
{
|
||||
name: 'ready list row',
|
||||
view: STORAGE_VIEWS.LIST,
|
||||
status: STORAGE_ROW_STATUS.READY,
|
||||
expectedIcon: 'lucide-file',
|
||||
},
|
||||
{
|
||||
name: 'closed folder',
|
||||
fileType: STORAGE_ROW_TYPES.FOLDER,
|
||||
expectedIcon: 'lucide-files-bucket',
|
||||
},
|
||||
{
|
||||
name: 'open folder',
|
||||
fileType: STORAGE_ROW_TYPES.FOLDER,
|
||||
isOpened: true,
|
||||
expectedIcon: 'lucide-folder-open',
|
||||
},
|
||||
{ name: 'image', mimeType: 'image/png', expectedIcon: 'lucide-image' },
|
||||
{ name: 'audio', mimeType: 'audio/mpeg', expectedIcon: 'lucide-music' },
|
||||
{ name: 'video', mimeType: 'video/mp4', expectedIcon: 'lucide-film' },
|
||||
{ name: 'generic file', expectedIcon: 'lucide-file' },
|
||||
]
|
||||
|
||||
describe('StorageRowIcon', () => {
|
||||
test.each(CASES)('renders $name at the canonical stroke width', (props) => {
|
||||
const { container } = customRender(
|
||||
<StorageRowIcon
|
||||
view={props.view ?? STORAGE_VIEWS.COLUMNS}
|
||||
status={props.status ?? STORAGE_ROW_STATUS.READY}
|
||||
fileType={props.fileType ?? STORAGE_ROW_TYPES.FILE}
|
||||
isOpened={props.isOpened}
|
||||
mimeType={props.mimeType}
|
||||
/>
|
||||
)
|
||||
|
||||
const icon = container.querySelector('svg')
|
||||
|
||||
expect(icon).toHaveClass(props.expectedIcon)
|
||||
expect(icon).toHaveAttribute('stroke-width', '1.5')
|
||||
})
|
||||
})
|
||||
@@ -0,0 +1,60 @@
|
||||
import { FilesBucket as FilesBucketIcon } from 'icons'
|
||||
import { File, Film, FolderOpen, Image as ImageIcon, LoaderCircle, Music } from 'lucide-react'
|
||||
|
||||
import { STORAGE_ROW_STATUS, STORAGE_ROW_TYPES, STORAGE_VIEWS } from './Storage.constants'
|
||||
|
||||
const ICON_STROKE_WIDTH = 1.5
|
||||
|
||||
interface StorageRowIconProps {
|
||||
view: STORAGE_VIEWS
|
||||
status: STORAGE_ROW_STATUS
|
||||
fileType: string
|
||||
isOpened?: boolean
|
||||
mimeType: string | undefined
|
||||
}
|
||||
|
||||
export const StorageRowIcon = ({
|
||||
view,
|
||||
status,
|
||||
fileType,
|
||||
isOpened = false,
|
||||
mimeType,
|
||||
}: StorageRowIconProps) => {
|
||||
if (view === STORAGE_VIEWS.LIST && status === STORAGE_ROW_STATUS.LOADING) {
|
||||
return (
|
||||
<LoaderCircle
|
||||
size={14}
|
||||
strokeWidth={ICON_STROKE_WIDTH}
|
||||
className="animate-spin text-foreground-lighter"
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
if (fileType === STORAGE_ROW_TYPES.FOLDER) {
|
||||
return isOpened ? (
|
||||
<FolderOpen size={16} strokeWidth={ICON_STROKE_WIDTH} className="text-foreground-lighter" />
|
||||
) : (
|
||||
<FilesBucketIcon
|
||||
size={16}
|
||||
strokeWidth={ICON_STROKE_WIDTH}
|
||||
className="text-foreground-lighter"
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
if (mimeType?.includes('image')) {
|
||||
return (
|
||||
<ImageIcon size={16} strokeWidth={ICON_STROKE_WIDTH} className="text-foreground-lighter" />
|
||||
)
|
||||
}
|
||||
|
||||
if (mimeType?.includes('audio')) {
|
||||
return <Music size={16} strokeWidth={ICON_STROKE_WIDTH} className="text-foreground-lighter" />
|
||||
}
|
||||
|
||||
if (mimeType?.includes('video')) {
|
||||
return <Film size={16} strokeWidth={ICON_STROKE_WIDTH} className="text-foreground-lighter" />
|
||||
}
|
||||
|
||||
return <File size={16} strokeWidth={ICON_STROKE_WIDTH} className="text-foreground-lighter" />
|
||||
}
|
||||
Reference in new issue
Block a user