Expand theme system and refresh UI components
This commit is contained in:
@@ -2,6 +2,7 @@ import React, { useEffect, useMemo, useState } from 'react';
|
||||
import { Button, Input, Paragraph, ScrollView, Separator, Spinner, Text, XStack, YStack } from 'tamagui';
|
||||
import { getIcon } from './IconMapper.jsx';
|
||||
import { normalizeColumnsArray } from './grid/utils.js';
|
||||
import { getTypographyRoleProps } from '../styles/index.js';
|
||||
|
||||
const EMPTY_COLUMNS = [];
|
||||
const EMPTY_ACTIONS = [];
|
||||
@@ -43,15 +44,15 @@ function SummaryCards({ summary }) {
|
||||
minWidth={140}
|
||||
padding="$3"
|
||||
borderWidth={1}
|
||||
borderColor="$borderColor"
|
||||
borderRadius="$4"
|
||||
backgroundColor="$accentSurface"
|
||||
borderColor="$lineSubtle"
|
||||
borderRadius="$radiusMd"
|
||||
backgroundColor="$bgPanel"
|
||||
gap="$1"
|
||||
>
|
||||
<Text fontSize="$3" color="$color" opacity={0.7}>
|
||||
<Text fontSize="$3" color="$textMuted">
|
||||
{item.label}
|
||||
</Text>
|
||||
<Text fontSize="$7" fontWeight="700" color="$accentColor">
|
||||
<Text fontSize="$7" fontWeight="700" color="$accent">
|
||||
{normalizeSummaryValue(item.value)}
|
||||
</Text>
|
||||
</YStack>
|
||||
@@ -63,7 +64,8 @@ function SummaryCards({ summary }) {
|
||||
function HeaderCell({ column, orderBy, order, onSort }) {
|
||||
const sortable = column.sortable !== false;
|
||||
const isActive = orderBy === column.id;
|
||||
const arrow = isActive ? (order === 'asc' ? '↑' : '↓') : '';
|
||||
const CaretUp = getIcon('caret-up');
|
||||
const CaretDown = getIcon('caret-down');
|
||||
const justifyContent = getColumnJustify(column.align);
|
||||
|
||||
return (
|
||||
@@ -81,10 +83,23 @@ function HeaderCell({ column, orderBy, order, onSort }) {
|
||||
padding={0}
|
||||
justifyContent={justifyContent}
|
||||
width="100%"
|
||||
hoverStyle={sortable ? { backgroundColor: '$bgPage' } : undefined}
|
||||
pressStyle={sortable ? { backgroundColor: '$bgPanelElev' } : undefined}
|
||||
>
|
||||
<Text fontSize="$4" fontWeight="700" color="$color" textAlign={column.align || 'left'} width="100%">
|
||||
{column.label}{arrow ? ` ${arrow}` : ''}
|
||||
</Text>
|
||||
<XStack width="100%" alignItems="center" justifyContent={justifyContent} gap="$2">
|
||||
<Text {...getTypographyRoleProps('tableHeader')} textAlign={column.align || 'left'} numberOfLines={1}>
|
||||
{column.label}
|
||||
</Text>
|
||||
{sortable ? (
|
||||
isActive ? (
|
||||
order === 'asc'
|
||||
? (CaretUp ? <CaretUp size="xs" color="$textSecondary" /> : null)
|
||||
: (CaretDown ? <CaretDown size="xs" color="$textSecondary" /> : null)
|
||||
) : (
|
||||
CaretDown ? <CaretDown size="xs" color="$textMuted" style={{ opacity: 0.6 }} /> : null
|
||||
)
|
||||
) : null}
|
||||
</XStack>
|
||||
</Button>
|
||||
</XStack>
|
||||
);
|
||||
@@ -133,7 +148,11 @@ export function DirView({
|
||||
bodyMaxHeight = 480,
|
||||
onRowClick = null,
|
||||
onRowPress = null,
|
||||
onRefresh = null
|
||||
onRefresh = null,
|
||||
showHeader = true,
|
||||
showSummary = true,
|
||||
density = 'comfortable',
|
||||
striped = false
|
||||
}) {
|
||||
const [dataVersion, setDataVersion] = useState(0);
|
||||
const [records, setRecords] = useState([]);
|
||||
@@ -155,6 +174,7 @@ export function DirView({
|
||||
const PreviousPageIcon = getIcon('chevron-left');
|
||||
const NextPageIcon = getIcon('chevron-right');
|
||||
const LastPageIcon = getIcon('last-page');
|
||||
const paddingRow = density === 'compact' ? '$2' : density === 'spacious' ? '$4' : '$3';
|
||||
|
||||
useEffect(() => {
|
||||
if (!dataModel?.subscribe) {
|
||||
@@ -275,49 +295,54 @@ export function DirView({
|
||||
|
||||
return (
|
||||
<YStack gap="$4" width="100%">
|
||||
<XStack justifyContent="space-between" alignItems="center" gap="$4" flexWrap="wrap">
|
||||
<XStack alignItems="center" gap="$3" flex={1} minWidth={240} flexWrap="wrap">
|
||||
<Text fontSize="$8" fontWeight="800" color="$accentColor">
|
||||
{title}
|
||||
</Text>
|
||||
{topLeftContent}
|
||||
</XStack>
|
||||
{showHeader ? (
|
||||
<XStack justifyContent="space-between" alignItems="center" gap="$4" flexWrap="wrap">
|
||||
<XStack alignItems="center" gap="$3" flex={1} minWidth={240} flexWrap="wrap">
|
||||
<Text {...getTypographyRoleProps('sectionTitle')}>
|
||||
{title}
|
||||
</Text>
|
||||
{topLeftContent}
|
||||
</XStack>
|
||||
|
||||
<XStack alignItems="center" justifyContent="flex-end" gap="$2" flexWrap="wrap">
|
||||
{searchConfig?.enabled ? (
|
||||
<Input
|
||||
width={260}
|
||||
placeholder={searchConfig.placeholder || 'Search records...'}
|
||||
value={effectiveSearchTerm}
|
||||
onChangeText={updateSearchTerm}
|
||||
<XStack alignItems="center" justifyContent="flex-end" gap="$2" flexWrap="wrap">
|
||||
{searchConfig?.enabled ? (
|
||||
<Input
|
||||
width={260}
|
||||
placeholder={searchConfig.placeholder || 'Search records...'}
|
||||
value={effectiveSearchTerm}
|
||||
onChangeText={updateSearchTerm}
|
||||
backgroundColor="$bgPanel"
|
||||
borderColor="$lineSubtle"
|
||||
focusStyle={{ borderColor: '$accent' }}
|
||||
/>
|
||||
) : null}
|
||||
{effectiveToolbarItems.map(renderToolbarButton)}
|
||||
<Button
|
||||
size="$3"
|
||||
chromeless
|
||||
circular
|
||||
aria-label="Refresh directory"
|
||||
icon={RefreshIcon ? <RefreshIcon size="sm" color="$textSecondary" /> : undefined}
|
||||
onPress={handleRefresh}
|
||||
disabled={loading}
|
||||
/>
|
||||
) : null}
|
||||
{effectiveToolbarItems.map(renderToolbarButton)}
|
||||
<Button
|
||||
size="$3"
|
||||
chromeless
|
||||
circular
|
||||
aria-label="Refresh directory"
|
||||
icon={RefreshIcon ? <RefreshIcon size={16} /> : undefined}
|
||||
onPress={handleRefresh}
|
||||
disabled={loading}
|
||||
/>
|
||||
{topRightContent}
|
||||
{topRightContent}
|
||||
</XStack>
|
||||
</XStack>
|
||||
</XStack>
|
||||
) : null}
|
||||
|
||||
{error ? (
|
||||
<YStack padding="$3" borderRadius="$4" backgroundColor="#fef2f2" borderWidth={1} borderColor="#fecaca">
|
||||
<Text color="#b91c1c">{error}</Text>
|
||||
<YStack padding="$3" borderRadius="$radiusMd" backgroundColor="$dangerBg" borderWidth={1} borderColor="$danger">
|
||||
<Text color="$danger" fontWeight="600">{error}</Text>
|
||||
</YStack>
|
||||
) : null}
|
||||
|
||||
<SummaryCards summary={summary} />
|
||||
{showSummary ? <SummaryCards summary={summary} /> : null}
|
||||
|
||||
{bodyHeaderContent}
|
||||
|
||||
<YStack borderWidth={1} borderColor="$borderColor" borderRadius="$5" overflow="hidden" backgroundColor="$background">
|
||||
<XStack padding="$3" gap="$3" backgroundColor="$accentSurface" borderBottomWidth={1} borderBottomColor="$borderColor">
|
||||
<YStack borderWidth={1} borderColor="$lineSubtle" borderRadius="$radiusLg" overflow="hidden" backgroundColor="$bgPanel">
|
||||
<XStack padding={paddingRow} gap="$3" backgroundColor="transparent" borderBottomWidth={1} borderBottomColor="$lineSubtle">
|
||||
{resolvedColumns.map((column) => (
|
||||
<HeaderCell
|
||||
key={column.id}
|
||||
@@ -334,27 +359,29 @@ export function DirView({
|
||||
))}
|
||||
</XStack>
|
||||
|
||||
<ScrollView maxHeight={bodyMaxHeight}>
|
||||
<ScrollView {...(bodyMaxHeight != null ? { maxHeight: bodyMaxHeight } : {})}>
|
||||
<YStack>
|
||||
{loading ? (
|
||||
<XStack justifyContent="center" padding="$6">
|
||||
<Spinner size="large" color="$accentColor" />
|
||||
<Spinner size="large" color="$accent" />
|
||||
</XStack>
|
||||
) : records.length === 0 ? (
|
||||
<YStack padding="$6" alignItems="center">
|
||||
<Paragraph color="$color" opacity={0.7}>
|
||||
No records found.
|
||||
<YStack padding="$6" alignItems="center" gap="$2">
|
||||
<Paragraph color="$textSecondary">
|
||||
No records found
|
||||
</Paragraph>
|
||||
<Text color="$textMuted" fontSize="$3">Try adjusting your search or filters.</Text>
|
||||
</YStack>
|
||||
) : (
|
||||
records.map((record, index) => (
|
||||
<YStack key={record?.[dataModel?.getIdField?.() || 'id'] || index}>
|
||||
<XStack
|
||||
padding="$3"
|
||||
padding={paddingRow}
|
||||
gap="$3"
|
||||
alignItems="center"
|
||||
hoverStyle={{ backgroundColor: '$accentSurface' }}
|
||||
pressStyle={{ backgroundColor: '$accentSurface' }}
|
||||
backgroundColor={striped && index % 2 === 1 ? '$bgPage' : 'transparent'}
|
||||
hoverStyle={{ backgroundColor: '$bgPage' }}
|
||||
pressStyle={{ backgroundColor: '$bgPanelElev' }}
|
||||
cursor={effectiveRowPress ? 'pointer' : undefined}
|
||||
onPress={() => effectiveRowPress?.(record)}
|
||||
>
|
||||
@@ -362,7 +389,7 @@ export function DirView({
|
||||
<RowCell key={column.id} column={column} record={record} />
|
||||
))}
|
||||
</XStack>
|
||||
{index < records.length - 1 ? <Separator /> : null}
|
||||
{index < records.length - 1 ? <Separator borderColor="$lineSubtle" /> : null}
|
||||
</YStack>
|
||||
))
|
||||
)}
|
||||
@@ -373,15 +400,15 @@ export function DirView({
|
||||
{bodyFooterContent}
|
||||
|
||||
<XStack justifyContent="space-between" alignItems="center" gap="$3" flexWrap="wrap">
|
||||
<Text color="$color" opacity={0.7}>
|
||||
<Text color="$textMuted">
|
||||
Rows: {totalRecords}
|
||||
</Text>
|
||||
<XStack alignItems="center" gap="$2" flexWrap="wrap">
|
||||
<XStack alignItems="center" gap="$2" flexWrap="wrap" padding="$1" borderWidth={1} borderColor="$lineSubtle" borderRadius="$radiusMd" backgroundColor="$bgPanel">
|
||||
<Button
|
||||
size="$3"
|
||||
chromeless
|
||||
aria-label="First page"
|
||||
icon={FirstPageIcon ? <FirstPageIcon size={16} /> : undefined}
|
||||
icon={FirstPageIcon ? <FirstPageIcon size="sm" color="$textSecondary" /> : undefined}
|
||||
onPress={() => setCurrentPage(1)}
|
||||
disabled={currentPage === 1 || loading}
|
||||
/>
|
||||
@@ -389,18 +416,18 @@ export function DirView({
|
||||
size="$3"
|
||||
chromeless
|
||||
aria-label="Previous page"
|
||||
icon={PreviousPageIcon ? <PreviousPageIcon size={16} /> : undefined}
|
||||
icon={PreviousPageIcon ? <PreviousPageIcon size="sm" color="$textSecondary" /> : undefined}
|
||||
onPress={() => setCurrentPage((value) => Math.max(1, value - 1))}
|
||||
disabled={currentPage === 1 || loading}
|
||||
/>
|
||||
<Text color="$color" opacity={0.75}>
|
||||
<Text color="$textSecondary">
|
||||
Page {currentPage} of {totalPages}
|
||||
</Text>
|
||||
<Button
|
||||
size="$3"
|
||||
chromeless
|
||||
aria-label="Next page"
|
||||
icon={NextPageIcon ? <NextPageIcon size={16} /> : undefined}
|
||||
icon={NextPageIcon ? <NextPageIcon size="sm" color="$textSecondary" /> : undefined}
|
||||
onPress={() => setCurrentPage((value) => Math.min(totalPages, value + 1))}
|
||||
disabled={currentPage >= totalPages || loading}
|
||||
/>
|
||||
@@ -408,7 +435,7 @@ export function DirView({
|
||||
size="$3"
|
||||
chromeless
|
||||
aria-label="Last page"
|
||||
icon={LastPageIcon ? <LastPageIcon size={16} /> : undefined}
|
||||
icon={LastPageIcon ? <LastPageIcon size="sm" color="$textSecondary" /> : undefined}
|
||||
onPress={() => setCurrentPage(totalPages)}
|
||||
disabled={currentPage >= totalPages || loading}
|
||||
/>
|
||||
|
||||
Reference in New Issue
Block a user