Files
Danny White 1131e3e2ce fix(ui): default Button variant to default instead of primary (#50160)
## What kind of change does this PR introduce?

Bug fix / design-system alignment for the legacy `Button` from `ui`.

## What is the current behavior?

Omitting `variant` on the legacy `Button` falls back to brand-green
`primary`. That makes accidental greens easy, and it is hard to spot the
real main action on busy pages.

## What is the new behavior?

- Legacy `Button` now defaults to neutral `default`
- Intentional primary CTAs (create, save, submit, marketing CTAs, and
matching `ButtonTooltip` usages) now set `variant="primary"` so their
appearance is unchanged
- Neutral actions that previously relied on the old fallback (cancel,
close, back, dashboard nav, and similar) become grey/white
- Design-system docs updated; regression tests cover the new default

`Button_Shadcn_` is unchanged. It already uses its own CVA default.

This is PR 1 of 2 in a stack. PR 2 drops now-redundant
`variant="default"` props.

## To test

Studio (http://localhost:8082):

- `/sign-in`: Sign in stays green
- Open a project → Database → Tables: New table stays green
- Auth → Users → Invite: Invite user stays green; Cancel / dismiss
controls stay neutral
- Project Settings → General: edit a field so Cancel and Save appear.
Cancel is neutral, Save is green

Design system (http://localhost:3003):

- Components → Button: default demo is neutral; primary demo is green;
featured preview is the default variant

Marketing (optional):

- www header: Start your project stays green; logged-in Dashboard is
neutral

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

- **Style**
- Buttons now default to a neutral style, while primary actions across
Studio, documentation, marketing pages, forms, dialogs, and error states
use prominent primary styling.
- Updated button examples and previews clarify the distinction between
default and primary variants.
  - Event registration now includes a directional arrow icon.

- **Tests**
- Added coverage confirming default button styling and explicit primary
styling behave as expected.
- Updated related test fixtures to use primary styling where
appropriate.
<!-- end of auto-generated comment: release notes by coderabbit.ai -->
2026-09-10 11:23:17 +10:00

193 lines
6.4 KiB
TypeScript

import { useParams } from 'common'
import { AlertCircle, LoaderCircle, X } from 'lucide-react'
import SVG from 'react-inlinesvg'
import { Button } from 'ui'
import { StorageItemWithColumn } from '../Storage.types'
import { useFetchFileUrlQuery } from '../StorageExplorer/useFetchFileUrlQuery'
import { useBucketFilePickerStateSnapshot } from './BucketFilePickerState'
import { BASE_PATH } from '@/lib/constants'
import { formatBytes } from '@/lib/helpers'
const PREVIEW_SIZE_LIMIT = 10 * 1024 * 1024 // 10MB
const PreviewFile = ({ item }: { item: StorageItemWithColumn }) => {
const { ref: projectRef } = useParams()
const { bucket, columns } = useBucketFilePickerStateSnapshot()
const path = columns.slice(0, item.columnIndex).concat(item.name).join('/')
const { data: previewUrl, isPending: isLoading } = useFetchFileUrlQuery({
path,
projectRef: projectRef!,
bucket,
})
// if the size is not available, we set it to be greater than the max size
const size = +(item.metadata?.size ?? PREVIEW_SIZE_LIMIT + 1)
const mimeType = item.metadata?.mimetype
const isSkipped = !!mimeType && !!size && size > PREVIEW_SIZE_LIMIT
if (isLoading) {
return (
<div className="flex h-full w-full items-center justify-center text-foreground-lighter">
<LoaderCircle size={14} strokeWidth={2} className="animate-spin text-foreground-lighter" />
</div>
)
}
if (isSkipped) {
return (
<div className="flex h-full w-full flex-col items-center justify-center">
<SVG
src={`${BASE_PATH}/img/file-filled.svg`}
preProcessor={(code) =>
code.replace(/svg/, 'svg class="mx-auto w-32 h-32 text-color-inherit opacity-75"')
}
/>
<p className="mt-2 w-2/5 text-center text-sm">
File size is too large to preview in the explorer
</p>
</div>
)
}
if (!mimeType || !previewUrl) {
return (
<SVG
src={`${BASE_PATH}/img/file-filled.svg`}
preProcessor={(code) =>
code.replace(/svg/, 'svg class="mx-auto w-32 h-32 text-color-inherit opacity-75"')
}
/>
)
}
if (mimeType.includes('image')) {
return (
<div
className="flex h-full w-full items-center justify-center bg-contain bg-center bg-no-repeat"
style={{ backgroundImage: `url('${previewUrl}')` }}
/>
)
}
if (mimeType.includes('audio')) {
return (
<div className="flex h-full w-full items-center justify-center px-10">
<audio key={previewUrl} controls style={{ width: 'inherit' }}>
<source src={previewUrl} type="audio/mpeg" />
<p className="text-sm text-foreground-light">
Your browser does not support the audio element.
</p>
</audio>
</div>
)
}
if (mimeType.includes('video')) {
return (
<div className="flex h-full w-full items-center justify-center">
<video key={previewUrl} controls style={{ maxHeight: '100%' }}>
<source src={previewUrl} type="video/mp4" />
<p className="text-sm text-foreground-light">
Your browser does not support the video tag.
</p>
</video>
</div>
)
}
return (
<SVG
src={`${BASE_PATH}/img/file-filled.svg`}
preProcessor={(code) =>
code.replace(/svg/, 'svg class="mx-auto w-32 h-32 text-color-inherit opacity-75"')
}
/>
)
}
export const PreviewPane = ({ onSelect }: { onSelect: (url: string) => void }) => {
const { ref: projectRef } = useParams()
const {
selectedFilePreview: file,
setSelectedFilePreview,
bucket,
columns,
} = useBucketFilePickerStateSnapshot()
const path = file ? columns.slice(0, file.columnIndex).concat(file.name).join('/') : ''
const { data: previewUrl, isLoading } = useFetchFileUrlQuery(
{ path, projectRef: projectRef!, bucket },
{ enabled: !!file }
)
if (!file) return null
const size = file.metadata ? formatBytes(file.metadata.size) : null
const mimeType = file.metadata ? file.metadata.mimetype : undefined
const createdAt = file.created_at ? new Date(file.created_at).toLocaleString() : 'Unknown'
const updatedAt = file.updated_at ? new Date(file.updated_at).toLocaleString() : 'Unknown'
return (
<div className="h-full border-l border-overlay bg-surface-100 overflow-y-auto w-[450px] pb-4">
{/* Preview Header */}
<div className="flex w-full justify-end items-center gap-2 sticky top-0 bg-surface-100 p-4 border-b">
<Button
variant="primary"
size="tiny"
onClick={() => onSelect(previewUrl!)}
disabled={!previewUrl}
loading={isLoading}
>
Select
</Button>
<div className="text-foreground-lighter transition-colors hover:text-foreground">
<X
className="cursor-pointer"
size={14}
strokeWidth={2}
onClick={() => setSelectedFilePreview(undefined)}
/>
</div>
</div>
{/* Preview Thumbnail*/}
<div className="my-4 border border-overlay mx-4">
<div className="flex h-56 w-full items-center 2xl:h-72">
<PreviewFile item={file} />
</div>
</div>
<div className="w-full space-y-6 px-4">
{/* Preview Information */}
<div className="space-y-1">
<h5 className="wrap-break-word text-base text-foreground">{file.name}</h5>
{file.isCorrupted && (
<div className="flex items-center space-x-2">
<AlertCircle size={14} strokeWidth={2} className="text-foreground-light" />
<p className="text-sm text-foreground-light">
File is corrupted, please delete and reupload this file again
</p>
</div>
)}
{mimeType && (
<p className="text-sm text-foreground-light">
{mimeType}
{size && <span> - {size}</span>}
</p>
)}
</div>
{/* Preview Metadata */}
<div className="space-y-2">
<div>
<label className="mb-1 text-xs text-foreground-lighter">Added on</label>
<p className="text-sm text-foreground-light">{createdAt}</p>
</div>
<div>
<label className="mb-1 text-xs text-foreground-lighter">Last modified</label>
<p className="text-sm text-foreground-light">{updatedAt}</p>
</div>
</div>
</div>
</div>
)
}