From 89895a7286c08f4a2bd1fe552396d3c7d0bf77cb Mon Sep 17 00:00:00 2001
From: Danny White <3104761+dnywh@users.noreply.github.com>
Date: Tue, 17 Mar 2026 18:47:25 +1100
Subject: [PATCH] chore(studio): file explorer rows (#43845)
## What kind of change does this PR introduce?
UI touch-up
## What is the current behavior?
Explorer rows in file storage is a bit rough around the edges.
## What is the new behavior?
- Consistent loaders
- Consistent icons
Also includes:
- Fix to `stroke-width` on files-bucket.svg as documented in icons.mdx
| Before | After |
| --- | --- |
| | |
---
apps/design-system/content/docs/icons.mdx | 6 +-
.../StorageExplorer/FileExplorerHeader.tsx | 6 +-
.../StorageExplorer/FileExplorerRow.tsx | 76 +++++++++----------
.../Storage/StorageExplorer/PreviewPane.tsx | 10 +--
apps/studio/public/img/bucket-filled.svg | 15 ----
apps/studio/public/img/folder-filled.svg | 12 ---
packages/icons/__registry__/index.tsx | 4 +-
packages/icons/src/icons/files-bucket.ts | 10 +--
packages/icons/src/raw-icons/files-bucket.svg | 6 +-
9 files changed, 56 insertions(+), 89 deletions(-)
delete mode 100644 apps/studio/public/img/bucket-filled.svg
delete mode 100644 apps/studio/public/img/folder-filled.svg
diff --git a/apps/design-system/content/docs/icons.mdx b/apps/design-system/content/docs/icons.mdx
index f50082c5255..eefdbb97749 100644
--- a/apps/design-system/content/docs/icons.mdx
+++ b/apps/design-system/content/docs/icons.mdx
@@ -45,7 +45,7 @@ function app() {
}
```
-**Default props**: All icons default to `size={24}`. Stroke and fill defaults come from the source SVG's root attributes (e.g. `stroke-width="1"` in the source becomes the component's default `strokeWidth`). Override these props as needed for your use case.
+**Default props**: All icons default to `size={24}`. Stroke and fill defaults come from the source SVG's root attributes (e.g. `stroke-width="1"` on the root `` becomes the component's default `strokeWidth`). You can override those defaults at the call site, but any `stroke`, `fill`, or `stroke-width` set on individual child paths will still win.
### Adding new custom icons
@@ -63,7 +63,7 @@ Follow these steps to add a new custom icon to the Supabase icon library.
For **fill-only icons** (e.g. logos that use shapes instead of strokes), add `stroke="none"` to the root `` element. The build will propagate this so the component never renders an unwanted stroke.
-Leave attributes like `stroke-width` as they are. The root SVG's `fill`, `stroke`, `stroke-width`, `stroke-linecap`, and `stroke-linejoin` attributes are automatically propagated as the component's defaults by the build process. The conversion to camel-case for React compatibility (e.g. `strokeWidth`) is also handled automatically.
+Prefer putting shared styling like `fill`, `stroke`, `stroke-width`, `stroke-linecap`, and `stroke-linejoin` on the root ``. The build propagates those root attributes as the component's defaults, and also converts them to camel-case for React compatibility (e.g. `strokeWidth`). If you put those attributes on individual child paths instead, they become fixed path-level styling and will override props passed to the component.
2. **Build the component**: Run `npm run build:icons` from inside the `packages/icons` directory
@@ -133,4 +133,4 @@ Note `stroke="none"` on the root to prevent unwanted strokes, and `fill="current
### Troubleshooting
-If your SVG specifies `stroke-width` attributes, they will override the component's `strokeWidth` prop. Remove stroke attributes from individual paths to let the component control them.
+If your SVG specifies `stroke-width` attributes on individual child paths, they will override the component's `strokeWidth` prop. Keep shared stroke attributes on the root `` and remove them from individual paths if you want consumers to control stroke weight.
diff --git a/apps/studio/components/interfaces/Storage/StorageExplorer/FileExplorerHeader.tsx b/apps/studio/components/interfaces/Storage/StorageExplorer/FileExplorerHeader.tsx
index cca26003006..f8d5cce061b 100644
--- a/apps/studio/components/interfaces/Storage/StorageExplorer/FileExplorerHeader.tsx
+++ b/apps/studio/components/interfaces/Storage/StorageExplorer/FileExplorerHeader.tsx
@@ -12,7 +12,7 @@ import {
Edit2,
FolderPlus,
List,
- Loader,
+ LoaderCircle,
RefreshCw,
Search,
Upload,
@@ -61,7 +61,7 @@ const HeaderPathEdit = ({ loading, isSearching, breadcrumbs, togglePathEdit }: a
>
{loading.isLoading ? (
) : (
@@ -113,7 +113,7 @@ const HeaderBreadcrumbs = ({
return loading.isLoading ? (
) : (
diff --git a/apps/studio/components/interfaces/Storage/StorageExplorer/FileExplorerRow.tsx b/apps/studio/components/interfaces/Storage/StorageExplorer/FileExplorerRow.tsx
index a65afc9403b..79e9362c414 100644
--- a/apps/studio/components/interfaces/Storage/StorageExplorer/FileExplorerRow.tsx
+++ b/apps/studio/components/interfaces/Storage/StorageExplorer/FileExplorerRow.tsx
@@ -1,4 +1,7 @@
import { PermissionAction } from '@supabase/shared-types/out/constants'
+import { useAsyncCheckPermissions } from 'hooks/misc/useCheckPermissions'
+import { FilesBucket as FilesBucketIcon } from 'icons'
+import { formatBytes } from 'lib/helpers'
import { find, isEmpty, isEqual } from 'lodash'
import {
AlertCircle,
@@ -7,20 +10,16 @@ import {
Edit,
File,
Film,
+ FolderOpen,
Image,
- Loader,
+ LoaderCircle,
MoreVertical,
Move,
Music,
Trash2,
} from 'lucide-react'
-import { useContextMenu } from 'react-contexify'
-import SVG from 'react-inlinesvg'
-
-import { useAsyncCheckPermissions } from 'hooks/misc/useCheckPermissions'
-import { BASE_PATH } from 'lib/constants'
-import { formatBytes } from 'lib/helpers'
import type { CSSProperties } from 'react'
+import { useContextMenu } from 'react-contexify'
import { useStorageExplorerStateSnapshot } from 'state/storage-explorer'
import {
Checkbox,
@@ -38,6 +37,7 @@ import {
TooltipContent,
TooltipTrigger,
} from 'ui'
+
import {
CONTEXT_MENU_KEYS,
STORAGE_ROW_STATUS,
@@ -54,47 +54,42 @@ 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
+ return (
+
+ )
}
- if (fileType === STORAGE_ROW_TYPES.BUCKET || fileType === STORAGE_ROW_TYPES.FOLDER) {
- const iconSrc =
- fileType === STORAGE_ROW_TYPES.BUCKET
- ? `${BASE_PATH}/img/bucket-filled.svg`
- : fileType === STORAGE_ROW_TYPES.FOLDER
- ? `${BASE_PATH}/img/folder-filled.svg`
- : `${BASE_PATH}/img/file-filled.svg`
- return (
-
- code.replace(/svg/, 'svg class="w-4 h-4 text-color-inherit opacity-75"')
- }
- />
+ if (fileType === STORAGE_ROW_TYPES.FOLDER) {
+ return isOpened ? (
+
+ ) : (
+
)
}
if (mimeType?.includes('image')) {
- return
+ return
}
if (mimeType?.includes('audio')) {
- return
+ return
}
if (mimeType?.includes('video')) {
- return
+ return
}
- return
+ return
}
interface FileExplorerRowProps {
@@ -173,19 +168,19 @@ export const FileExplorerRow = ({
? [
{
name: 'Rename',
- icon: ,
+ icon: ,
onClick: () => setSelectedItemToRename(itemWithColumnIndex),
},
]
: []),
{
name: 'Download',
- icon: ,
+ icon: ,
onClick: () => downloadFolder(itemWithColumnIndex),
},
{
name: 'Copy path to folder',
- icon: ,
+ icon: ,
onClick: () => copyPathToFolder(openedFolders, itemWithColumnIndex),
},
...(canUpdateFiles
@@ -193,7 +188,7 @@ export const FileExplorerRow = ({
{ name: 'Separator', icon: undefined, onClick: undefined },
{
name: 'Delete',
- icon: ,
+ icon: ,
onClick: () => setSelectedItemsToDelete([itemWithColumnIndex]),
},
]
@@ -206,14 +201,14 @@ export const FileExplorerRow = ({
? [
{
name: 'Get URL',
- icon: ,
+ icon: ,
onClick: () => onCopyUrl(itemWithColumnIndex.name),
},
]
: [
{
name: 'Get URL',
- icon: ,
+ icon: ,
children: [
{
name: 'Expire in 1 week',
@@ -239,19 +234,19 @@ export const FileExplorerRow = ({
]),
{
name: 'Download',
- icon: ,
+ icon: ,
onClick: () => downloadFile(itemWithColumnIndex),
},
...(canUpdateFiles
? [
{
name: 'Rename',
- icon: ,
+ icon: ,
onClick: () => setSelectedItemToRename(itemWithColumnIndex),
},
{
name: 'Move',
- icon: ,
+ icon: ,
onClick: () => setSelectedItemsToMove([itemWithColumnIndex]),
},
{ name: 'Separator', icon: undefined, onClick: undefined },
@@ -263,7 +258,7 @@ export const FileExplorerRow = ({
? [
{
name: 'Delete',
- icon: ,
+ icon: ,
onClick: () => setSelectedItemsToDelete([itemWithColumnIndex]),
},
]
@@ -324,7 +319,7 @@ export const FileExplorerRow = ({
event.stopPropagation()
event.preventDefault()
if (item.status !== STORAGE_ROW_STATUS.LOADING && !isOpened && !isPreviewed) {
- item.type === STORAGE_ROW_TYPES.FOLDER || item.type === STORAGE_ROW_TYPES.BUCKET
+ item.type === STORAGE_ROW_TYPES.FOLDER
? openFolder(columnIndex, item)
: onSelectFile(columnIndex)
}
@@ -348,6 +343,7 @@ export const FileExplorerRow = ({
view={view}
status={item.status}
fileType={item.type}
+ isOpened={isOpened}
mimeType={item.metadata?.mimetype}
/>
@@ -398,9 +394,9 @@ export const FileExplorerRow = ({
}
>
{item.status === STORAGE_ROW_STATUS.LOADING ? (
-
) : (
diff --git a/apps/studio/components/interfaces/Storage/StorageExplorer/PreviewPane.tsx b/apps/studio/components/interfaces/Storage/StorageExplorer/PreviewPane.tsx
index a57c4562384..68176cc020b 100644
--- a/apps/studio/components/interfaces/Storage/StorageExplorer/PreviewPane.tsx
+++ b/apps/studio/components/interfaces/Storage/StorageExplorer/PreviewPane.tsx
@@ -1,13 +1,12 @@
import { Transition } from '@headlessui/react'
import { PermissionAction } from '@supabase/shared-types/out/constants'
-import { isEmpty } from 'lodash'
-import { AlertCircle, ChevronDown, Copy, Download, Loader, Trash2, X } from 'lucide-react'
-import SVG from 'react-inlinesvg'
-
import { ButtonTooltip } from 'components/ui/ButtonTooltip'
import { useAsyncCheckPermissions } from 'hooks/misc/useCheckPermissions'
import { BASE_PATH } from 'lib/constants'
import { formatBytes } from 'lib/helpers'
+import { isEmpty } from 'lodash'
+import { AlertCircle, ChevronDown, Copy, Download, LoaderCircle, Trash2, X } from 'lucide-react'
+import SVG from 'react-inlinesvg'
import { useStorageExplorerStateSnapshot } from 'state/storage-explorer'
import {
Button,
@@ -16,6 +15,7 @@ import {
DropdownMenuItem,
DropdownMenuTrigger,
} from 'ui'
+
import { URL_EXPIRY_DURATION } from '../Storage.constants'
import { StorageItem } from '../Storage.types'
import { useCopyUrl } from './useCopyUrl'
@@ -41,7 +41,7 @@ const PreviewFile = ({ item }: { item: StorageItem }) => {
if (isLoading) {
return (
-
+
)
}
diff --git a/apps/studio/public/img/bucket-filled.svg b/apps/studio/public/img/bucket-filled.svg
deleted file mode 100644
index abf6ea5e043..00000000000
--- a/apps/studio/public/img/bucket-filled.svg
+++ /dev/null
@@ -1,15 +0,0 @@
-
-
-
- Group 6
- Created with Sketch.
-
-
-
-
-
-
-
-
-
-
\ No newline at end of file
diff --git a/apps/studio/public/img/folder-filled.svg b/apps/studio/public/img/folder-filled.svg
deleted file mode 100644
index e179aebbd01..00000000000
--- a/apps/studio/public/img/folder-filled.svg
+++ /dev/null
@@ -1,12 +0,0 @@
-
-
-
- Group 5
- Created with Sketch.
-
-
-
-
-
-
-
\ No newline at end of file
diff --git a/packages/icons/__registry__/index.tsx b/packages/icons/__registry__/index.tsx
index a64698d95c3..6a1b1713733 100644
--- a/packages/icons/__registry__/index.tsx
+++ b/packages/icons/__registry__/index.tsx
@@ -148,10 +148,10 @@ export const Index: Record = [
name: "files-bucket",
componentName: "FilesBucket",
deprecated: false,
- raw: "import createSupabaseIcon from '../createSupabaseIcon';\n\n/**\n * @component @name FilesBucket\n * @description Supabase SVG icon component, renders SVG Element with children.\n *\n * @preview \n *\n * @param {Object} props - Supabase icons props and any valid SVG attribute\n * @returns {JSX.Element} JSX Element\n *\n */\nconst FilesBucket = createSupabaseIcon(\n 'FilesBucket',\n [\n [\n 'path',\n {\n d: 'M3.99988 20H19.9999C20.5303 20 21.039 19.7893 21.4141 19.4142C21.7892 19.0391 21.9999 18.5304 21.9999 18V8C21.9999 7.46957 21.7892 6.96086 21.4141 6.58579C21.039 6.21071 20.5303 6 19.9999 6H12.0699C11.7405 5.9983 11.4166 5.91525 11.127 5.75824C10.8374 5.60123 10.5911 5.37512 10.4099 5.1L9.58988 3.9C9.4087 3.62488 9.16237 3.39877 8.87278 3.24176C8.58319 3.08475 8.25929 3.0017 7.92988 3H3.99988C3.46944 3 2.96074 3.21071 2.58566 3.58579C2.21059 3.96086 1.99988 4.46957 1.99988 5V18C1.99988 19.1 2.89988 20 3.99988 20Z',\n stroke: 'currentColor',\n 'stroke-width': '1.5',\n 'stroke-linecap': 'round',\n 'stroke-linejoin': 'round',\n key: 'o4u44y',\n },\n ],\n [\n 'path',\n {\n d: 'M5 10.5H19',\n stroke: 'currentColor',\n 'stroke-width': '1.5',\n 'stroke-linecap': 'round',\n 'stroke-linejoin': 'round',\n key: 'cf9ou5',\n },\n ],\n ],\n { fill: 'none' },\n);\n\nexport default FilesBucket;\n",
+ raw: "import createSupabaseIcon from '../createSupabaseIcon';\n\n/**\n * @component @name FilesBucket\n * @description Supabase SVG icon component, renders SVG Element with children.\n *\n * @preview \n *\n * @param {Object} props - Supabase icons props and any valid SVG attribute\n * @returns {JSX.Element} JSX Element\n *\n */\nconst FilesBucket = createSupabaseIcon(\n 'FilesBucket',\n [\n [\n 'path',\n {\n d: 'M3.99988 20H19.9999C20.5303 20 21.039 19.7893 21.4141 19.4142C21.7892 19.0391 21.9999 18.5304 21.9999 18V8C21.9999 7.46957 21.7892 6.96086 21.4141 6.58579C21.039 6.21071 20.5303 6 19.9999 6H12.0699C11.7405 5.9983 11.4166 5.91525 11.127 5.75824C10.8374 5.60123 10.5911 5.37512 10.4099 5.1L9.58988 3.9C9.4087 3.62488 9.16237 3.39877 8.87278 3.24176C8.58319 3.08475 8.25929 3.0017 7.92988 3H3.99988C3.46944 3 2.96074 3.21071 2.58566 3.58579C2.21059 3.96086 1.99988 4.46957 1.99988 5V18C1.99988 19.1 2.89988 20 3.99988 20Z',\n stroke: 'currentColor',\n 'stroke-linecap': 'round',\n 'stroke-linejoin': 'round',\n key: '1i7att',\n },\n ],\n [\n 'path',\n {\n d: 'M5 10.5H19',\n stroke: 'currentColor',\n 'stroke-linecap': 'round',\n 'stroke-linejoin': 'round',\n key: 'ys6uda',\n },\n ],\n ],\n { fill: 'none', strokeWidth: '1.5' },\n);\n\nexport default FilesBucket;\n",
component: React.lazy(() => import('icons/src/icons/files-bucket')),
import: "import { FilesBucket } from 'icons'",
- svg: "\n\n\n \n",
+ svg: "\n\n\n \n",
jsx: "import { FilesBucket } from \"icons\"\n \n "
},
{
diff --git a/packages/icons/src/icons/files-bucket.ts b/packages/icons/src/icons/files-bucket.ts
index dcfc647247f..2d352e4a450 100644
--- a/packages/icons/src/icons/files-bucket.ts
+++ b/packages/icons/src/icons/files-bucket.ts
@@ -4,7 +4,7 @@ import createSupabaseIcon from '../createSupabaseIcon';
* @component @name FilesBucket
* @description Supabase SVG icon component, renders SVG Element with children.
*
- * @preview 
+ * @preview 
*
* @param {Object} props - Supabase icons props and any valid SVG attribute
* @returns {JSX.Element} JSX Element
@@ -18,10 +18,9 @@ const FilesBucket = createSupabaseIcon(
{
d: 'M3.99988 20H19.9999C20.5303 20 21.039 19.7893 21.4141 19.4142C21.7892 19.0391 21.9999 18.5304 21.9999 18V8C21.9999 7.46957 21.7892 6.96086 21.4141 6.58579C21.039 6.21071 20.5303 6 19.9999 6H12.0699C11.7405 5.9983 11.4166 5.91525 11.127 5.75824C10.8374 5.60123 10.5911 5.37512 10.4099 5.1L9.58988 3.9C9.4087 3.62488 9.16237 3.39877 8.87278 3.24176C8.58319 3.08475 8.25929 3.0017 7.92988 3H3.99988C3.46944 3 2.96074 3.21071 2.58566 3.58579C2.21059 3.96086 1.99988 4.46957 1.99988 5V18C1.99988 19.1 2.89988 20 3.99988 20Z',
stroke: 'currentColor',
- 'stroke-width': '1.5',
'stroke-linecap': 'round',
'stroke-linejoin': 'round',
- key: 'o4u44y',
+ key: '1i7att',
},
],
[
@@ -29,14 +28,13 @@ const FilesBucket = createSupabaseIcon(
{
d: 'M5 10.5H19',
stroke: 'currentColor',
- 'stroke-width': '1.5',
'stroke-linecap': 'round',
'stroke-linejoin': 'round',
- key: 'cf9ou5',
+ key: 'ys6uda',
},
],
],
- { fill: 'none' },
+ { fill: 'none', strokeWidth: '1.5' },
);
export default FilesBucket;
diff --git a/packages/icons/src/raw-icons/files-bucket.svg b/packages/icons/src/raw-icons/files-bucket.svg
index 2014992f8eb..909f237781f 100644
--- a/packages/icons/src/raw-icons/files-bucket.svg
+++ b/packages/icons/src/raw-icons/files-bucket.svg
@@ -1,4 +1,4 @@
-
-
-
+
+
+