feat(table): support card layout

This commit is contained in:
CaIon
2026-06-19 17:43:13 +08:00
parent 68585568d6
commit 29c3dcb927
18 changed files with 320 additions and 111 deletions
@@ -131,6 +131,17 @@ export type DataTablePageProps<TData> = {
*/
hideMobile?: boolean
/**
* Render the card view on mobile instead of the default {@link MobileCardList}.
* When enabled, the mobile layout reuses the same {@link DataTableCardGrid}
* (and therefore `renderCard` / `cardGridClassName`) as the desktop card view,
* stacked in a single column. Falls back to the generic card content when no
* `renderCard` is provided. Ignored when a custom `mobile` slot is supplied.
*
* Defaults to `false`, so existing pages keep the list-style mobile layout.
*/
mobileCardView?: boolean
/**
* Row className resolver — applied to both desktop `TableRow` and mobile card.
* Composes with the default `data-state="selected"` styling on desktop.
@@ -247,7 +258,9 @@ export type DataTablePageProps<TData> = {
viewModeStorageKey?: string
/**
* Initial (uncontrolled) view mode. Defaults to `'table'`.
* Initial (uncontrolled) view mode. When unset, defaults to `'card'` if
* `enableCardView` is `true`, otherwise `'table'`. A persisted selection
* (via `viewModeStorageKey`) always takes precedence over this default.
*/
defaultViewMode?: DataTableViewMode
@@ -292,7 +305,12 @@ export function DataTablePage<TData>(props: DataTablePageProps<TData>) {
const [internalViewMode, setInternalViewMode] = useDataTableViewMode({
storageKey: props.viewModeStorageKey,
defaultMode: props.defaultViewMode,
// When card view is enabled, prefer it as the default unless the consumer
// explicitly opts into a different initial mode. A persisted choice (via
// `viewModeStorageKey`) still takes precedence over this default.
defaultMode:
props.defaultViewMode ??
(props.enableCardView ? DATA_TABLE_VIEW_MODES.CARD : undefined),
})
const viewMode = props.viewMode ?? internalViewMode
const setViewMode = props.onViewModeChange ?? setInternalViewMode
@@ -381,16 +399,33 @@ function renderMobile<TData>(
(ownGetRowClassName
? (row: Row<TData>) => ownGetRowClassName(row, { isMobile: true })
: undefined)
const mobileContent = props.mobile ?? (
<MobileCardList
table={props.table}
isLoading={props.isLoading}
emptyTitle={props.emptyTitle}
emptyDescription={props.emptyDescription}
getRowKey={props.mobileProps?.getRowKey}
getRowClassName={mobileGetRowClassName}
/>
)
let mobileContent = props.mobile
if (mobileContent === undefined) {
mobileContent = props.mobileCardView ? (
<DataTableCardGrid
table={props.table}
isLoading={props.isLoading}
emptyTitle={props.emptyTitle}
emptyDescription={props.emptyDescription}
emptyIcon={props.emptyIcon}
renderCard={props.renderCard}
gridClassName={props.cardGridClassName ?? 'grid grid-cols-1 gap-3'}
skeletonKeyPrefix={props.skeletonKeyPrefix}
getRowKey={props.mobileProps?.getRowKey}
getRowClassName={mobileGetRowClassName}
/>
) : (
<MobileCardList
table={props.table}
isLoading={props.isLoading}
emptyTitle={props.emptyTitle}
emptyDescription={props.emptyDescription}
getRowKey={props.mobileProps?.getRowKey}
getRowClassName={mobileGetRowClassName}
/>
)
}
return <div className='min-h-0 flex-1 overflow-y-auto'>{mobileContent}</div>
}
@@ -50,16 +50,16 @@ export function DataTableViewModeToggle(props: DataTableViewModeToggleProps) {
const { t } = useTranslation()
const segments: Segment[] = [
{
value: DATA_TABLE_VIEW_MODES.TABLE,
icon: Table2,
tooltip: t('Table view'),
},
{
value: DATA_TABLE_VIEW_MODES.CARD,
icon: Grid2X2,
tooltip: t('Card view'),
},
{
value: DATA_TABLE_VIEW_MODES.TABLE,
icon: Table2,
tooltip: t('Table view'),
},
]
return (
+5 -1
View File
@@ -24,6 +24,8 @@ type ProviderBadgeProps = Omit<StatusBadgeProps, 'children' | 'label'> & {
iconKey?: string | null
iconSize?: number
label: string
/** Color the label text by provider name. Set false for a neutral label. */
colorText?: boolean
}
export function ProviderBadge({
@@ -31,6 +33,7 @@ export function ProviderBadge({
iconKey,
iconSize = 14,
label,
colorText = true,
...badgeProps
}: ProviderBadgeProps) {
const icon = iconKey ? getLobeIcon(iconKey, iconSize) : null
@@ -43,7 +46,8 @@ export function ProviderBadge({
{icon && <span className='flex shrink-0 items-center'>{icon}</span>}
<StatusBadge
label={label}
autoColor={label}
autoColor={colorText ? label : undefined}
variant={colorText ? undefined : 'neutral'}
size='sm'
className={cn('min-w-0 shrink overflow-hidden', !icon && 'pl-0')}
{...badgeProps}
+6 -6
View File
@@ -86,15 +86,15 @@ export const StatusBadgeTypeContext =
React.createContext<StatusBadgeType>('badge')
const sizeMap = {
sm: 'h-5 gap-1 px-1.5 text-xs leading-none',
md: 'h-5 gap-1 px-1.5 text-xs leading-none',
lg: 'h-6 gap-1.5 px-2 text-xs leading-none',
sm: 'h-5 gap-1 px-1.5 text-sm leading-none',
md: 'h-5 gap-1 px-1.5 text-sm leading-none',
lg: 'h-6 gap-1.5 px-2 text-sm leading-none',
} as const
const textSizeMap = {
sm: 'gap-1 text-xs leading-none',
md: 'gap-1 text-xs leading-none',
lg: 'gap-1.5 text-xs leading-none',
sm: 'gap-1 text-sm leading-none',
md: 'gap-1 text-sm leading-none',
lg: 'gap-1.5 text-sm leading-none',
} as const
export interface StatusBadgeProps extends Omit<