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:
Amer Agovic
2026-04-18 10:43:52 -05:00
commit 94a9f32969
87 changed files with 19750 additions and 0 deletions
+313
View File
@@ -0,0 +1,313 @@
import React from 'react';
import { Button, Checkbox, Input, Paragraph, ScrollView, Text, XStack, YStack } from 'tamagui';
import { getIcon } from '../IconMapper.jsx';
import { useGridView } from './context.js';
import { formatValueByColumn } from './utils.js';
function renderToolbarItem(item) {
if (!item) {
return null;
}
if (React.isValidElement(item)) {
return item;
}
if (item.kind === 'button') {
const IconComponent = item.icon ? getIcon(item.icon) : null;
return (
<Button
key={item.key || item.label}
size="$3"
theme={item.theme}
chromeless={item.chromeless}
disabled={item.disabled}
icon={IconComponent ? <IconComponent size={16} /> : undefined}
onPress={item.onClick || item.onPress}
>
{item.label}
</Button>
);
}
if (item.kind === 'text') {
return (
<Text key={item.key || item.text} color="$color" opacity={0.7}>
{item.text}
</Text>
);
}
if (item.kind === 'search') {
return (
<Input
key={item.key || item.placeholder || 'search'}
width={item.width || 240}
value={item.value}
placeholder={item.placeholder || 'Search'}
onChangeText={(value) => item.onChange?.(value)}
/>
);
}
if (item.kind === 'node') {
return <React.Fragment key={item.key || 'node'}>{item.node}</React.Fragment>;
}
return <React.Fragment key={item.key || 'item'}>{item}</React.Fragment>;
}
function DefaultPanelRecordRenderer({ row }) {
const grid = useGridView();
const titleColumn =
grid.resolvedColumns.find((column) =>
['customer', 'name', 'title', 'label'].includes(column.field)
) ||
grid.resolvedColumns.find((column) => column.type === 'text') ||
grid.resolvedColumns[0];
const subtitleColumn = grid.resolvedColumns.find((column) =>
['description', 'region', 'owner', 'status'].includes(column.field)
);
const summaryColumns = grid.resolvedColumns.filter(
(column) => ![titleColumn?.field, subtitleColumn?.field, 'description'].includes(column.field)
);
return (
<YStack gap="$3">
<YStack gap="$1">
<Text fontSize="$3" letterSpacing={1} textTransform="uppercase" color="$accentColor">
Record Summary
</Text>
<Text fontSize="$6" fontWeight="700">
{titleColumn ? row?.[titleColumn.field] : row?.id}
</Text>
{subtitleColumn ? (
<Paragraph color="$color" opacity={0.7}>
{row?.[subtitleColumn.field] || ''}
</Paragraph>
) : null}
</YStack>
<XStack gap="$2" flexWrap="wrap">
{summaryColumns.slice(0, 3).map((column) => (
<YStack
key={`${row.id}-${column.field}-chip`}
paddingHorizontal="$3"
paddingVertical="$2"
borderRadius="$6"
backgroundColor="$accentSurface"
borderWidth={1}
borderColor="$accentBorder"
>
<Text fontSize="$2" color="$color" opacity={0.65}>
{column.label}
</Text>
<Text fontSize="$4" fontWeight="600">
{formatValueByColumn(row?.[column.field], column)}
</Text>
</YStack>
))}
</XStack>
<XStack gap="$3" flexWrap="wrap">
{summaryColumns.map((column) => (
<YStack
key={`${row.id}-${column.field}`}
minWidth={160}
flex={1}
padding="$3"
borderRadius="$4"
borderWidth={1}
borderColor="$borderColor"
backgroundColor="$background"
gap="$1"
>
<Text fontSize="$3" color="$color" opacity={0.65}>
{column.label}
</Text>
<Text>{formatValueByColumn(row?.[column.field], column)}</Text>
</YStack>
))}
</XStack>
</YStack>
);
}
export function PanelToolBar({ items = [], visible = true }) {
if (visible === false) {
return null;
}
return (
<XStack gap="$2" alignItems="center" justifyContent="flex-end" flexWrap="wrap">
{items.map((item, index) => (
<React.Fragment key={item?.key || item?.label || item?.text || index}>
{renderToolbarItem(item)}
</React.Fragment>
))}
</XStack>
);
}
export function PanelFooterStatusBar({ text, visible = true }) {
const grid = useGridView();
if (visible === false) {
return null;
}
return (
<Text color="$color" opacity={0.7}>
{text || grid.statusText}
</Text>
);
}
export function PanelHeader({ title, toolbarItems = [], visible = true, showDivider = true }) {
const grid = useGridView();
const RefreshIcon = getIcon('refresh');
const CloseIcon = getIcon('close');
if (visible === false) {
return null;
}
return (
<XStack
alignItems="center"
justifyContent="space-between"
gap="$3"
padding="$3"
minHeight={64}
borderBottomWidth={showDivider ? 1 : 0}
borderBottomColor="$borderColor"
backgroundColor="$accentSurface"
>
<Text fontSize="$6" fontWeight="700" color="$accentColor">
{title}
</Text>
<XStack gap="$2" alignItems="center" flexWrap="wrap" justifyContent="flex-end">
<PanelToolBar items={toolbarItems} />
<Button
size="$3"
chromeless
circular
icon={RefreshIcon ? <RefreshIcon size={16} /> : undefined}
onPress={grid.reload}
/>
<Button
size="$3"
chromeless
circular
disabled={!grid.close}
icon={CloseIcon ? <CloseIcon size={16} /> : undefined}
onPress={grid.close}
/>
</XStack>
</XStack>
);
}
export function PanelFooter({ toolbarItems = [], visible = true }) {
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"
>
<PanelFooterStatusBar />
<PanelToolBar items={toolbarItems} />
</XStack>
);
}
export function PanelBodyView({
visible = true,
recordRenderer: RecordRenderer = DefaultPanelRecordRenderer,
columns = 2
}) {
const grid = useGridView();
if (visible === false) {
return null;
}
if (grid.error) {
return (
<YStack flex={1} alignItems="center" justifyContent="center" padding="$5">
<Text color="#b91c1c">{grid.error}</Text>
</YStack>
);
}
if (grid.isLoading && !grid.rows.length) {
return (
<YStack flex={1} alignItems="center" justifyContent="center" padding="$5">
<Text color="$color" opacity={0.7}>Loading cards...</Text>
</YStack>
);
}
if (!grid.rows.length) {
return (
<YStack flex={1} alignItems="center" justifyContent="center" padding="$5">
<Text color="$color" opacity={0.7}>No records available.</Text>
</YStack>
);
}
const responsiveColumns = typeof columns === 'number' ? columns : 2;
return (
<ScrollView flex={1}>
<YStack padding="$4" gap="$3">
<XStack gap="$3" flexWrap="wrap">
{grid.rows.map((row) => (
<YStack
key={row.id}
minWidth={responsiveColumns > 1 ? 320 : 240}
flex={1}
flexBasis={responsiveColumns > 1 ? '48%' : '100%'}
padding="$4"
borderWidth={1}
borderColor={grid.selectedIds.has(row.id) ? '$accentBorder' : '$borderColor'}
backgroundColor={grid.selectedIds.has(row.id) ? '$accentSurface' : '$background'}
borderRadius="$5"
gap="$3"
>
{grid.selectable ? (
<XStack justifyContent="flex-start">
<Checkbox
checked={grid.selectedIds.has(row.id)}
onCheckedChange={() => grid.toggleSelectRow(row.id)}
>
<Checkbox.Indicator />
</Checkbox>
</XStack>
) : null}
<RecordRenderer row={row} />
</YStack>
))}
</XStack>
{grid.isLoading ? (
<Text color="$color" opacity={0.7}>
Refreshing records...
</Text>
) : null}
</YStack>
</ScrollView>
);
}