Initial commit: bface library, build fixes, and refreshed docs
- Externalize all @tamagui/* and tamagui subpaths so dist no longer vendors Tamagui. - Emit TypeScript declarations with vite-plugin-dts; fix package exports types for ui/*. - Align initEnv with profiles: displayName, brandLogo, api.baseURL, themeColor, uiShell. - Stabilize tests with Node localStorage file; env tests pass. - Update README and component docs for services, menus, API client, and development.
This commit is contained in:
@@ -0,0 +1,256 @@
|
||||
import React, { useEffect, useRef } from 'react';
|
||||
import { Button, Checkbox, ScrollView, Separator, Text, XStack, YStack } from 'tamagui';
|
||||
import { getIcon } from '../IconMapper.jsx';
|
||||
import { useGridView } from './context.js';
|
||||
import {
|
||||
formatValueByColumn,
|
||||
getColumnJustify,
|
||||
getColumnLayoutStyle,
|
||||
resolveCellAlignment,
|
||||
resolveCellValue
|
||||
} from './utils.js';
|
||||
|
||||
function DefaultGridCellRenderer({ value, column }) {
|
||||
return (
|
||||
<Text width="100%" textAlign={column.align || 'left'} opacity={value == null || value === '' ? 0.6 : 1}>
|
||||
{formatValueByColumn(value, column)}
|
||||
</Text>
|
||||
);
|
||||
}
|
||||
|
||||
function UtilityCell({ rowId }) {
|
||||
const grid = useGridView();
|
||||
const ChevronRightIcon = getIcon('chevron-right');
|
||||
|
||||
if (!grid.selectable && !grid.nested) {
|
||||
return null;
|
||||
}
|
||||
|
||||
if (grid.nested) {
|
||||
return (
|
||||
<XStack width={36} alignItems="center" justifyContent="center">
|
||||
{ChevronRightIcon ? <ChevronRightIcon size={16} /> : <Text>{'>'}</Text>}
|
||||
</XStack>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<XStack width={36} alignItems="center" justifyContent="center">
|
||||
<Checkbox checked={grid.selectedIds.has(rowId)} onCheckedChange={() => grid.toggleSelectRow(rowId)}>
|
||||
<Checkbox.Indicator />
|
||||
</Checkbox>
|
||||
</XStack>
|
||||
);
|
||||
}
|
||||
|
||||
function useViewportTracking(enabled = true) {
|
||||
const grid = useGridView();
|
||||
const bodyRef = useRef(null);
|
||||
|
||||
useEffect(() => {
|
||||
if (!enabled || typeof window === 'undefined') {
|
||||
return undefined;
|
||||
}
|
||||
|
||||
const updateViewportWidth = () => {
|
||||
const element = bodyRef.current;
|
||||
if (!element) {
|
||||
return;
|
||||
}
|
||||
const nextWidth = Math.max(0, Math.round(element.getBoundingClientRect().width));
|
||||
grid.setTableViewportWidth((current) => (current === nextWidth ? current : nextWidth));
|
||||
};
|
||||
|
||||
updateViewportWidth();
|
||||
window.addEventListener('resize', updateViewportWidth);
|
||||
|
||||
return () => {
|
||||
window.removeEventListener('resize', updateViewportWidth);
|
||||
};
|
||||
}, [enabled, grid]);
|
||||
|
||||
return bodyRef;
|
||||
}
|
||||
|
||||
export function TableHeader({ visible = true, showTopBorder = true }) {
|
||||
const grid = useGridView();
|
||||
|
||||
if (visible === false) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const activeColumns = grid.visibleColumns?.length ? grid.visibleColumns : grid.resolvedColumns;
|
||||
|
||||
return (
|
||||
<XStack
|
||||
alignItems="stretch"
|
||||
borderTopWidth={showTopBorder ? 1 : 0}
|
||||
borderBottomWidth={1}
|
||||
borderColor="$accentBorder"
|
||||
backgroundColor="$accentSurface"
|
||||
paddingHorizontal="$2"
|
||||
>
|
||||
{grid.selectable || grid.nested ? <XStack width={36} /> : null}
|
||||
{activeColumns.map((column) => {
|
||||
const activeSort = grid.sortBy.find((entry) => entry.field === column.field);
|
||||
const sortLabel =
|
||||
activeSort?.direction === 'asc' ? '↑' : activeSort?.direction === 'desc' ? '↓' : '';
|
||||
|
||||
return (
|
||||
<Button
|
||||
key={column.field}
|
||||
chromeless
|
||||
disabled={!column.sortable}
|
||||
onPress={() => column.sortable && grid.toggleSort(column.field)}
|
||||
justifyContent={getColumnJustify(column.align)}
|
||||
alignItems="center"
|
||||
paddingVertical="$3"
|
||||
paddingHorizontal="$2"
|
||||
{...getColumnLayoutStyle(column)}
|
||||
>
|
||||
<Text width="100%" textAlign={column.align || 'left'} fontWeight="700">
|
||||
{column.label}{sortLabel ? ` ${sortLabel}` : ''}
|
||||
</Text>
|
||||
</Button>
|
||||
);
|
||||
})}
|
||||
</XStack>
|
||||
);
|
||||
}
|
||||
|
||||
export function TableBodyView({ visible = true }) {
|
||||
const grid = useGridView();
|
||||
const bodyRef = useViewportTracking(visible);
|
||||
|
||||
if (visible === false) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const activeColumns = grid.visibleColumns?.length ? grid.visibleColumns : grid.resolvedColumns;
|
||||
|
||||
if (grid.error) {
|
||||
return (
|
||||
<YStack flex={1} alignItems="center" justifyContent="center" padding="$5">
|
||||
<Text color="#b91c1c">{grid.error}</Text>
|
||||
</YStack>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<ScrollView flex={1}>
|
||||
<YStack ref={bodyRef}>
|
||||
{grid.rows.map((row, index) => (
|
||||
<YStack key={row.id ?? index}>
|
||||
<XStack
|
||||
alignItems="stretch"
|
||||
paddingHorizontal="$2"
|
||||
backgroundColor={grid.selectedIds.has(row.id) ? '$accentSurface' : '$background'}
|
||||
>
|
||||
{grid.selectable || grid.nested ? <UtilityCell rowId={row.id} /> : null}
|
||||
{activeColumns.map((column) => {
|
||||
const Renderer = column.renderer || DefaultGridCellRenderer;
|
||||
const cellValue = resolveCellValue(row, column);
|
||||
|
||||
return (
|
||||
<XStack
|
||||
key={`${row.id}-${column.field}`}
|
||||
alignItems="center"
|
||||
justifyContent={getColumnJustify(resolveCellAlignment(column))}
|
||||
paddingVertical="$3"
|
||||
paddingHorizontal="$2"
|
||||
minHeight={46}
|
||||
{...getColumnLayoutStyle(column)}
|
||||
>
|
||||
<Renderer value={cellValue} row={row} column={column} grid={grid} />
|
||||
</XStack>
|
||||
);
|
||||
})}
|
||||
</XStack>
|
||||
<Separator />
|
||||
</YStack>
|
||||
))}
|
||||
|
||||
{!grid.rows.length && !grid.isLoading ? (
|
||||
<YStack minHeight={120} alignItems="center" justifyContent="center" padding="$5">
|
||||
<Text color="$color" opacity={0.7}>No records available.</Text>
|
||||
</YStack>
|
||||
) : null}
|
||||
|
||||
{grid.isLoading ? (
|
||||
<YStack minHeight={64} alignItems="center" justifyContent="center" padding="$4">
|
||||
<Text color="$color" opacity={0.7}>Loading...</Text>
|
||||
</YStack>
|
||||
) : null}
|
||||
</YStack>
|
||||
</ScrollView>
|
||||
);
|
||||
}
|
||||
|
||||
export function TableFooter({ visible = true }) {
|
||||
const grid = useGridView();
|
||||
const FirstPageIcon = getIcon('first-page');
|
||||
const PreviousPageIcon = getIcon('chevron-left');
|
||||
const NextPageIcon = getIcon('chevron-right');
|
||||
const LastPageIcon = getIcon('last-page');
|
||||
|
||||
if (visible === false) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return (
|
||||
<XStack
|
||||
alignItems="center"
|
||||
justifyContent="space-between"
|
||||
gap="$3"
|
||||
padding="$3"
|
||||
minHeight={56}
|
||||
borderTopWidth={1}
|
||||
borderTopColor="$borderColor"
|
||||
backgroundColor="$background"
|
||||
flexWrap="wrap"
|
||||
>
|
||||
<Text color="$color" opacity={0.7}>
|
||||
{grid.total} records
|
||||
</Text>
|
||||
|
||||
<XStack gap="$1" alignItems="center" flexWrap="wrap">
|
||||
<Button
|
||||
size="$3"
|
||||
chromeless
|
||||
circular
|
||||
disabled={grid.currentPage <= 1}
|
||||
icon={FirstPageIcon ? <FirstPageIcon size={16} /> : undefined}
|
||||
onPress={() => grid.setPage(1)}
|
||||
/>
|
||||
<Button
|
||||
size="$3"
|
||||
chromeless
|
||||
circular
|
||||
disabled={grid.currentPage <= 1}
|
||||
icon={PreviousPageIcon ? <PreviousPageIcon size={16} /> : undefined}
|
||||
onPress={() => grid.setPage(grid.currentPage - 1)}
|
||||
/>
|
||||
<Text color="$color" opacity={0.75}>
|
||||
Page {grid.currentPage} of {grid.pageCount}
|
||||
</Text>
|
||||
<Button
|
||||
size="$3"
|
||||
chromeless
|
||||
circular
|
||||
disabled={grid.currentPage >= grid.pageCount}
|
||||
icon={NextPageIcon ? <NextPageIcon size={16} /> : undefined}
|
||||
onPress={() => grid.setPage(grid.currentPage + 1)}
|
||||
/>
|
||||
<Button
|
||||
size="$3"
|
||||
chromeless
|
||||
circular
|
||||
disabled={grid.currentPage >= grid.pageCount}
|
||||
icon={LastPageIcon ? <LastPageIcon size={16} /> : undefined}
|
||||
onPress={() => grid.setPage(grid.pageCount)}
|
||||
/>
|
||||
</XStack>
|
||||
</XStack>
|
||||
);
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user