Squashed 'ai-context/workbench-ui/' content from commit 32e36a5c

git-subtree-dir: ai-context/workbench-ui
git-subtree-split: 32e36a5c2131e429a7081cfaf67dabad3193cda3
This commit is contained in:
elpresidank 2026-04-05 21:08:02 -05:00
commit a8390532f7
310 changed files with 56430 additions and 0 deletions

View file

@ -0,0 +1,39 @@
import React from "react";
import { Box, Flex, Heading, Text } from "@chakra-ui/react";
interface AltCardProps {
title: string;
description?: string;
icon?: React.ReactNode;
children?: React.ReactNode;
}
const AltCard: React.FC<AltCardProps> = ({
title,
description,
icon,
children,
}) => {
return (
<Box borderRadius="lg" boxShadow="sm" p={5} border="1px" height="100%">
<Flex alignItems="center" mb={description ? 2 : 4}>
{icon && (
<Box mr={3} color="accent.solid">
{icon}
</Box>
)}
<Heading as="h3" size="md" fontWeight="semibold">
{title}
</Heading>
</Flex>
{description && (
<Text mb={4} fontSize="sm">
{description}
</Text>
)}
{children}
</Box>
);
};
export default AltCard;

View file

@ -0,0 +1,40 @@
import { Table } from "@chakra-ui/react";
import { flexRender } from "@tanstack/react-table";
const BasicTable = ({ table }) => {
return (
<>
<Table.Root>
<Table.Header>
{table.getHeaderGroups().map((headerGroup) => (
<Table.Row key={headerGroup.id}>
{headerGroup.headers.map((header) => (
<Table.ColumnHeader key={header.id}>
{header.isPlaceholder
? null
: flexRender(
header.column.columnDef.header,
header.getContext(),
)}
</Table.ColumnHeader>
))}
</Table.Row>
))}
</Table.Header>
<Table.Body>
{table.getRowModel().rows.map((row) => (
<Table.Row key={row.id}>
{row.getVisibleCells().map((cell) => (
<Table.Cell key={cell.id}>
{flexRender(cell.column.columnDef.cell, cell.getContext())}
</Table.Cell>
))}
</Table.Row>
))}
</Table.Body>
</Table.Root>
</>
);
};
export default BasicTable;

View file

@ -0,0 +1,34 @@
import React from "react";
import { Box, Flex, Heading, Text } from "@chakra-ui/react";
interface CardProps {
title: string;
description?: string;
icon?: React.ReactNode;
children?: React.ReactNode;
}
const Card: React.FC<CardProps> = ({ title, description, icon, children }) => {
return (
<Box borderRadius="lg" boxShadow="sm" p={5} border="1px" height="100%">
<Flex alignItems="center" mb={description ? 2 : 4}>
{icon && (
<Box mr={3} color="primary.solid">
{icon}
</Box>
)}
<Heading as="h3" size="md" fontWeight="semibold">
{title}
</Heading>
</Flex>
{description && (
<Text mb={4} fontSize="sm">
{description}
</Text>
)}
{children}
</Box>
);
};
export default Card;

View file

@ -0,0 +1,28 @@
import React from "react";
import { Box, Spinner } from "@chakra-ui/react";
import { useProgressStateStore } from "@trustgraph/react-state";
const CenterSpinner: React.FC = () => {
const activity = useProgressStateStore((state) => state.activity);
if (activity.size < 1) {
return null;
}
return (
<Box
position="absolute"
top="calc(50% - 3rem)"
left="calc(50% - 3rem)"
zIndex="999"
margin="0"
padding="0"
>
<Spinner size="xl" />
</Box>
);
};
export default CenterSpinner;

View file

@ -0,0 +1,118 @@
import React, { useState } from "react";
import { Input, Tag, Wrap, Field } from "@chakra-ui/react";
// Represents a label added to the list. Highlighted with a close button for
// removal.
const Chip = ({ label, onCloseClick }) => (
<Tag.Root
key={label}
borderRadius="full"
variant="solid"
colorScheme="green"
>
<Tag.Label>{label}</Tag.Label>
<Tag.EndElement>
<Tag.CloseTrigger
onClick={() => {
onCloseClick(label);
}}
/>
</Tag.EndElement>
</Tag.Root>
);
// A horizontal stack of chips. Like a Pringles can on its side.
const ChipList = ({ items = [], onCloseClick }) => (
<Wrap spacing={1} mb={3}>
{items.map((item) => (
<Chip label={item} key={item} onCloseClick={onCloseClick} />
))}
</Wrap>
);
// Form field wrapper.
const ChipInput = ({ ...rest }) => <Input {...rest} />;
// Field wrapping chip list and input
const ChipInputField: React.FC<{
values: string[];
onValuesChange: (v: string[]) => void;
label: string;
}> = ({ values, onValuesChange, label }) => {
const [inputValue, setInputValue] = useState("");
// Checks whether we've added this item already.
const itemChipExists = (item) => values.includes(item);
// Add an item to the list, if it's valid and isn't already there.
const addItems = (itemsToAdd) => {
const validatedItems = itemsToAdd
.map((e) => e.trim())
.filter((item) => !itemChipExists(item));
const newItems = [...values, ...validatedItems];
onValuesChange(newItems);
setInputValue("");
};
// Remove an item from the list.
const removeItem = (item) => {
const index = values.findIndex((e) => e === item);
if (index !== -1) {
const newItems = [...values];
newItems.splice(index, 1);
onValuesChange(newItems);
}
};
// Save input field contents in state when changed.
const handleChange = (e) => {
setInputValue(e.target.value);
};
// Validate and add the item if we press tab, enter or comma.
const handleKeyDown = (e) => {
if (["Enter", "Tab", ","].includes(e.key)) {
e.preventDefault();
addItems([inputValue]);
}
};
// Split and add items when pasting.
const handlePaste = (e) => {
e.preventDefault();
const pastedData = e.clipboardData.getData("text");
const pastedItems = pastedData.split(",");
addItems(pastedItems);
};
const handleCloseClick = (item) => {
removeItem(item);
};
const required = false;
return (
<Field.Root mb={4} required={required}>
<Field.Label>
{label} {required && <Field.RequiredIndicator />}
</Field.Label>
<ChipList items={values} onCloseClick={handleCloseClick} />
<ChipInput
placeholder="enter items"
onPaste={handlePaste}
onKeyDown={handleKeyDown}
onChange={handleChange}
value={inputValue}
variant="subtle"
/>
</Field.Root>
);
};
export default ChipInputField;

View file

@ -0,0 +1,40 @@
import { Table } from "@chakra-ui/react";
import { flexRender } from "@tanstack/react-table";
const ClickableTable = ({ table, onClick, ...tableProps }) => {
return (
<>
<Table.Root interactive {...tableProps}>
<Table.Header>
{table.getHeaderGroups().map((headerGroup) => (
<Table.Row key={headerGroup.id}>
{headerGroup.headers.map((header) => (
<Table.ColumnHeader key={header.id}>
{header.isPlaceholder
? null
: flexRender(
header.column.columnDef.header,
header.getContext(),
)}
</Table.ColumnHeader>
))}
</Table.Row>
))}
</Table.Header>
<Table.Body>
{table.getRowModel().rows.map((row) => (
<Table.Row key={row.id} onClick={() => onClick(row)}>
{row.getVisibleCells().map((cell) => (
<Table.Cell key={cell.id}>
{flexRender(cell.column.columnDef.cell, cell.getContext())}
</Table.Cell>
))}
</Table.Row>
))}
</Table.Body>
</Table.Root>
</>
);
};
export default ClickableTable;

View file

@ -0,0 +1,139 @@
import React from "react";
import { Box, VStack, HStack, Text, Button } from "@chakra-ui/react";
import { AlertTriangle, X } from "lucide-react";
interface ConfirmDialogProps {
isOpen: boolean;
onClose: () => void;
onConfirm: () => void;
title: string;
message: string;
confirmText?: string;
cancelText?: string;
variant?: "danger" | "warning" | "info";
}
export const ConfirmDialog: React.FC<ConfirmDialogProps> = ({
isOpen,
onClose,
onConfirm,
title,
message,
confirmText = "Confirm",
cancelText = "Cancel",
variant = "warning",
}) => {
if (!isOpen) return null;
const handleConfirm = () => {
onConfirm();
onClose();
};
const getVariantColors = () => {
switch (variant) {
case "danger":
return {
icon: "red.500",
confirmButton: "red",
bg: "red.50",
border: "red.200",
};
case "warning":
return {
icon: "orange.500",
confirmButton: "orange",
bg: "orange.50",
border: "orange.200",
};
case "info":
return {
icon: "blue.500",
confirmButton: "blue",
bg: "blue.50",
border: "blue.200",
};
default:
return {
icon: "orange.500",
confirmButton: "orange",
bg: "orange.50",
border: "orange.200",
};
}
};
const colors = getVariantColors();
return (
<Box
position="fixed"
top="0"
left="0"
right="0"
bottom="0"
bg="blackAlpha.600"
display="flex"
alignItems="center"
justifyContent="center"
zIndex="modal"
>
<Box
bg="white"
borderRadius="lg"
boxShadow="xl"
w="500px"
maxW="90vw"
maxH="90vh"
overflow="auto"
>
{/* Header */}
<Box p={6} borderBottomWidth="1px">
<HStack justify="space-between" align="center">
<HStack>
<AlertTriangle size={20} color={colors.icon} />
<Text fontSize="lg" fontWeight="semibold">
{title}
</Text>
</HStack>
<Button variant="ghost" size="sm" onClick={onClose}>
<X size={16} />
</Button>
</HStack>
</Box>
{/* Content */}
<Box p={6}>
<VStack align="stretch" spacing={4}>
<Box
p={4}
bg={colors.bg}
borderRadius="md"
borderWidth="1px"
borderColor={colors.border}
>
<Text fontSize="sm" color="gray.700" whiteSpace="pre-line">
{message}
</Text>
</Box>
</VStack>
</Box>
{/* Footer */}
<Box p={6} borderTopWidth="1px" bg="gray.50">
<HStack justify="flex-end" spacing={3}>
<Button variant="ghost" onClick={onClose}>
{cancelText}
</Button>
<Button
colorPalette={colors.confirmButton}
onClick={handleConfirm}
>
{confirmText}
</Button>
</HStack>
</Box>
</Box>
</Box>
);
};

View file

@ -0,0 +1,117 @@
import React from "react";
import { Box, HStack, Text, Tooltip } from "@chakra-ui/react";
import { Info, Clock, Wifi, WifiOff, Shield, ShieldOff } from "lucide-react";
import { useConnectionState } from "@trustgraph/react-provider";
import type { ConnectionState } from "@trustgraph/client";
interface ConnectionStatusProps {
showDetails?: boolean;
size?: "sm" | "md" | "lg";
}
const getStatusDisplay = (state: ConnectionState) => {
switch (state.status) {
case "connecting":
return {
icon: Clock,
color: "yellow.500",
text: "Connecting...",
tooltip: "Establishing connection to server",
};
case "connected":
return {
icon: Wifi,
color: "green.500",
text: "Connected",
tooltip: "Connected to server",
};
case "authenticated":
return {
icon: Shield,
color: "green.500",
text: "Authenticated",
tooltip: "Connected with API key authentication",
};
case "unauthenticated":
return {
icon: ShieldOff,
color: "blue.500",
text: "Unauthenticated",
tooltip: "Connected but no API key provided (limited functionality)",
};
case "reconnecting":
return {
icon: Clock,
color: "orange.500",
text: `Reconnecting... (${state.reconnectAttempt}/${state.maxAttempts})`,
tooltip: `Attempting to reconnect. Try ${state.reconnectAttempt} of ${state.maxAttempts}`,
};
case "failed":
return {
icon: WifiOff,
color: "red.500",
text: "Connection Failed",
tooltip:
state.lastError || "Connection failed after maximum retry attempts",
};
default:
return {
icon: Info,
color: "gray.500",
text: "Unknown",
tooltip: "Unknown connection state",
};
}
};
export const ConnectionStatus: React.FC<ConnectionStatusProps> = ({
showDetails = false,
size = "md",
}) => {
const connectionState = useConnectionState();
if (!connectionState) {
return null;
}
const {
icon: StatusIcon,
color,
text,
tooltip,
} = getStatusDisplay(connectionState);
const iconSize = size === "sm" ? 16 : size === "lg" ? 24 : 20;
const fontSize = size === "sm" ? "xs" : size === "lg" ? "md" : "sm";
return (
<Tooltip.Root>
<Tooltip.Trigger asChild>
<HStack spacing={2}>
<Box color={color}>
<StatusIcon size={iconSize} />
</Box>
<Text fontSize={fontSize} color="fg.default">
{showDetails ? text : connectionState.status}
</Text>
{showDetails && connectionState.hasApiKey && (
<Text fontSize="xs" color="fg.muted">
(API Key)
</Text>
)}
</HStack>
</Tooltip.Trigger>
<Tooltip.Positioner>
<Tooltip.Content>{tooltip}</Tooltip.Content>
</Tooltip.Positioner>
</Tooltip.Root>
);
};
export default ConnectionStatus;

View file

@ -0,0 +1,39 @@
import { useNavigate } from "react-router";
import { HStack, Tag } from "@chakra-ui/react";
import { Entity } from "@trustgraph/react-state";
import { useWorkbenchStateStore } from "@trustgraph/react-state";
const EntityList = () => {
const entities = useWorkbenchStateStore((state) => state.entities);
const setSelected = useWorkbenchStateStore((state) => state.setSelected);
const navigate = useNavigate();
const onSelect = (x: Entity) => {
setSelected(x);
navigate("/entity");
};
return (
<HStack mt={8}>
{entities.slice(0, 8).map((entity, ix) => (
<Tag.Root
asChild
size="sm"
key={ix}
color="primary.solid"
bgColor="bg"
variant="surface"
>
<button onClick={() => onSelect(entity)}>
<Tag.Label>{entity.label}</Tag.Label>
</button>
</Tag.Root>
))}
</HStack>
);
};
export default EntityList;

View file

@ -0,0 +1,17 @@
import React, { PropsWithChildren } from "react";
import { Link } from "@chakra-ui/react";
const ExternalDocs: React.FC<
PropsWithChildren<{
href: string;
}>
> = ({ href, children }) => {
return (
<Link href={href} target="_blank" colorPalette="accent">
{children}
</Link>
);
};
export default ExternalDocs;

View file

@ -0,0 +1,255 @@
import { useState } from "react";
import { Text, Box, Stack, HStack, Popover, Portal } from "@chakra-ui/react";
import { Database, Workflow } from "lucide-react";
import { useSessionStore } from "@trustgraph/react-state";
import { useFlows } from "@trustgraph/react-state";
import { useSettings } from "@trustgraph/react-state";
import { useCollections } from "@trustgraph/react-state";
const FlowSelector = () => {
const flowState = useFlows();
const flows = flowState.flows ? flowState.flows : [];
const collectionsState = useCollections();
const collections = collectionsState.collections || [];
const flowId = useSessionStore((state) => state.flowId);
const setFlowId = useSessionStore((state) => state.setFlowId);
const setFlow = useSessionStore((state) => state.setFlow);
const { settings, updateSetting } = useSettings();
const [open, setOpen] = useState(false);
return (
<Popover.Root
open={open}
onOpenChange={(e) => setOpen(e.open)}
size="xl"
positioning={{ placement: "bottom-end" }}
>
<Popover.Trigger asChild>
<Stack
p={3}
gap={2}
borderWidth="1px"
borderRadius="8px"
borderColor="border.inverted/20"
color="fg.muted"
backgroundColor="primary.bg"
_hover={{
backgroundColor: "bg.emphasized",
borderColor: "border.inverted",
color: "fg",
}}
onClick={() => setOpen(true)}
cursor="pointer"
>
<HStack gap={2} align="center">
<Database size={14} />
<Text fontSize="xs" fontWeight="medium">
{settings.collection}
</Text>
</HStack>
<HStack gap={2} align="center">
<Workflow size={14} />
<Text fontSize="xs" fontWeight="medium">
{flowId || "<none>"}
</Text>
</HStack>
</Stack>
</Popover.Trigger>
<Portal>
<Popover.Positioner>
<Popover.Content>
<Popover.Arrow />
<Popover.Body>
<Stack gap={4} p={4}>
{/* Collection Selection */}
<Stack gap={3}>
<Text
fontSize="sm"
fontWeight="semibold"
color="fg.muted"
mb={2}
>
Select Collection
</Text>
<Stack gap="1">
{collections.map((collection) => {
const isSelected =
settings.collection === collection.collection;
return (
<Box
key={collection.collection}
p={3}
borderRadius="md"
borderWidth="1px"
borderColor={
isSelected ? "primary.500" : "border.subtle"
}
backgroundColor={
isSelected ? "primary.50" : "transparent"
}
_hover={{
borderColor: "primary.300",
backgroundColor: isSelected
? "primary.100"
: "bg.subtle",
}}
cursor="pointer"
onClick={() => {
updateSetting("collection", collection.collection);
}}
>
<HStack gap={3} align="start">
<Box
w={4}
h={4}
borderRadius="full"
borderWidth="2px"
borderColor={
isSelected
? "colorPalette.500"
: "border.emphasized"
}
backgroundColor={
isSelected ? "colorPalette.500" : "transparent"
}
mt={0.5}
flexShrink={0}
position="relative"
>
{isSelected && (
<Box
w="6px"
h="6px"
borderRadius="full"
backgroundColor="bg"
position="absolute"
top="50%"
left="50%"
transform="translate(-50%, -50%)"
/>
)}
</Box>
<Box flex="1">
<Text fontWeight="semibold" fontSize="sm" mb={1}>
{collection.name}
</Text>
<Text
fontSize="xs"
color="fg.muted"
lineHeight="1.4"
>
{collection.description}
</Text>
</Box>
</HStack>
</Box>
);
})}
</Stack>
</Stack>
{/* Flow Selection */}
<Box borderTopWidth="1px" borderColor="border.subtle" pt={4}>
<Text
fontSize="sm"
fontWeight="semibold"
color="fg.muted"
mb={3}
>
Select Flow
</Text>
<Stack gap="1">
{flows.map((flow) => {
const isSelected = flowId === flow.id;
return (
<Box
key={flow.id}
p={3}
borderRadius="md"
borderWidth="1px"
borderColor={
isSelected ? "primary.500" : "border.subtle"
}
backgroundColor={
isSelected ? "primary.50" : "transparent"
}
_hover={{
borderColor: "primary.300",
backgroundColor: isSelected
? "primary.100"
: "bg.subtle",
}}
cursor="pointer"
onClick={() => {
setFlowId(flow.id);
setFlow(flow);
}}
>
<HStack gap={3} align="start">
<Box
w={4}
h={4}
borderRadius="full"
borderWidth="2px"
borderColor={
isSelected
? "colorPalette.500"
: "border.emphasized"
}
backgroundColor={
isSelected ? "colorPalette.500" : "transparent"
}
mt={0.5}
flexShrink={0}
position="relative"
>
{isSelected && (
<Box
w="6px"
h="6px"
borderRadius="full"
backgroundColor="bg"
position="absolute"
top="50%"
left="50%"
transform="translate(-50%, -50%)"
/>
)}
</Box>
<Box flex="1">
<Text fontWeight="semibold" fontSize="sm" mb={1}>
{flow.id}
</Text>
<Text
fontSize="xs"
color="fg.muted"
lineHeight="1.4"
>
{flow.description}
</Text>
</Box>
</HStack>
</Box>
);
})}
</Stack>
</Box>
</Stack>
</Popover.Body>
</Popover.Content>
</Popover.Positioner>
</Portal>
</Popover.Root>
);
};
export default FlowSelector;

View file

@ -0,0 +1,45 @@
import React from "react";
import { Field, NumberInput } from "@chakra-ui/react";
interface NumberFieldProps {
label: string;
minValue: number;
maxValue: number;
value: number;
onValueChange: (x: number) => void;
}
const NumberField: React.FC<NumberFieldProps> = ({
label,
minValue,
maxValue,
value,
onValueChange,
}) => {
return (
<Field.Root mb={4}>
<Field.Label fontWeight="medium">{label}</Field.Label>
<NumberInput.Root
min={minValue}
max={maxValue}
value={value.toString()}
onValueChange={(e) => {
const numValue =
e.value === "" || e.value == null ? 0 : Number(e.value);
if (!isNaN(numValue)) {
onValueChange(numValue);
}
}}
>
<NumberInput.Input />
<NumberInput.Control>
<NumberInput.IncrementTrigger />
<NumberInput.DecrementTrigger />
</NumberInput.Control>
</NumberInput.Root>
</Field.Root>
);
};
export default NumberField;

View file

@ -0,0 +1,42 @@
import React from "react";
import {
Box,
Stack,
Image,
Flex,
Heading,
Text,
Center,
} from "@chakra-ui/react";
const OptionWithImage: React.FC<{
image: string;
title: string;
description?: string | React.ReactNode;
badge?: React.ReactNode;
}> = ({ description, title, image, badge }) => {
return (
<Stack>
<Flex alignItems="center">
<Box mr={4} minWidth="5rem" width="5rem">
<Center>
<Image rounded="md" src={image} alt={title} />
</Center>
</Box>
<Box>
<Flex alignItems="center">
<Heading as="h1" size="md" color="fg" fontWeight="bold" mr={2}>
{title}
</Heading>
{badge && badge}
</Flex>
<Text mt={1} textStyle="xs" color="fg.muted">
{description}
</Text>
</Box>
</Flex>
</Stack>
);
};
export default OptionWithImage;

View file

@ -0,0 +1,64 @@
import React from "react";
import { Flex, Text, Box, HStack, VStack, Heading } from "@chakra-ui/react";
import ColorModeToggle from "../color-mode-toggle";
import FlowSelector from "./FlowSelector";
import ConnectionStatus from "./ConnectionStatus";
import UserDisplay from "./UserDisplay";
interface PageHeaderProps {
title: string;
description: string;
icon?: React.ReactNode;
}
const PageHeader: React.FC<PageHeaderProps> = ({
title,
description,
icon,
}) => {
return (
<Flex
mb={8}
alignItems="center"
justifyContent="space-between"
width="100%"
px={1}
py={1}
>
<Flex alignItems="center">
{icon && (
<Box mr={4} color="{colors.primary.fg}" fontSize="xl">
{icon}
</Box>
)}
<Box>
<Heading
as="h1"
size="xl"
color="{colors.primary.fg}"
fontWeight="bold"
>
{title}
</Heading>
<Text mt={1} fontSize="md" color="{colors.primary.emphasized}">
{description}
</Text>
</Box>
</Flex>
<Box>
<HStack gap={6} align="center">
<VStack gap={1} align="end">
<ConnectionStatus showDetails={true} size="sm" />
<UserDisplay />
</VStack>
<FlowSelector />
<ColorModeToggle />
</HStack>
</Box>
</Flex>
);
};
export default PageHeader;

View file

@ -0,0 +1,41 @@
import React from "react";
import { Box, Text } from "@chakra-ui/react";
import { useProgressStateStore } from "@trustgraph/react-state";
const Progress: React.FC = () => {
const activity = useProgressStateStore((state) => state.activity);
return (
<>
{activity.size > 0 && (
<Box
position="absolute"
bottom="1rem"
left="1rem"
zIndex="999"
pt="0.8rem"
pb="0.8rem"
pl="1.2rem"
pr="1.2rem"
backgroundColor="bg.emphasized/40"
borderWidth="1px"
borderColor="border.inverted/40"
borderRadius="5px"
width="25rem"
>
{Array.from(activity)
.slice(0, 4)
.map((a, ix) => (
<Box key={ix} color="fg/40" truncate>
<Text textStyle="sm">{a}...</Text>
</Box>
))}
</Box>
)}
</>
);
};
export default Progress;

View file

@ -0,0 +1,33 @@
import React from "react";
import { SendHorizontal } from "lucide-react";
import { Box, Button } from "@chakra-ui/react";
interface ProgressSubmitButtonProps {
disabled: boolean;
working: boolean;
onClick: () => void;
}
const ProgressSubmitButton: React.FC<ProgressSubmitButtonProps> = ({
disabled,
working,
onClick,
}) => {
return (
<Box>
<Button
variant="subtle"
disabled={disabled}
loading={working}
color="primary"
onClick={() => onClick()}
>
Send <SendHorizontal />
</Button>
</Box>
);
};
export default ProgressSubmitButton;

View file

@ -0,0 +1,11 @@
import { Badge } from "@chakra-ui/react";
const RecommendedBadge = () => {
return (
<Badge colorPalette="green" size="sm">
recommended
</Badge>
);
};
export default RecommendedBadge;

View file

@ -0,0 +1,92 @@
/*
* CRITICAL: DO NOT MODIFY THIS COMPONENT WITHOUT DESIGN AUTHORITY APPROVAL
*
* This SelectField component is used throughout the application by 15+ components
* across multiple domains (ontologies, flows, documents, etc.). Any changes to
* this component's interface or behavior will have extensive downstream impact.
*
* Changes to this component in September 2025 broke multiple features and required
* systematic updates across the entire application. Always prefer adapter patterns
* or feature-specific solutions over modifying this shared infrastructure.
*
* Required API contract:
* - value: string[] (arrays only)
* - onValueChange: (values: string[]) => void
* - items must include description fields with SelectOptionText/SelectOption
*/
import React, { useMemo } from "react";
import {
Field,
Select,
Portal,
Stack,
createListCollection,
} from "@chakra-ui/react";
export interface SelectFieldValue {
value: string;
label: string;
description?: string | React.ReactElement;
}
interface SelectFieldProps {
label: string;
items: SelectFieldValue[];
value: string[];
onValueChange: (x: string[]) => void;
contentRef?;
}
const SelectField: React.FC<SelectFieldProps> = ({
label,
items,
value,
onValueChange,
contentRef,
}) => {
// Only create new collection when items actually change
const collection = useMemo(
() => createListCollection({ items: items }),
[items],
);
return (
<Field.Root mb={4}>
<Field.Label fontWeight="medium">{label}</Field.Label>
<Select.Root
collection={collection}
value={value}
onValueChange={(e) => onValueChange(e.value)}
>
<Select.HiddenSelect />
<Select.Control>
<Select.Trigger>
<Select.ValueText placeholder={label} />
</Select.Trigger>
<Select.IndicatorGroup>
<Select.Indicator />
</Select.IndicatorGroup>
</Select.Control>
<Portal container={contentRef}>
<Select.Positioner>
<Select.Content>
{items.map((v) => (
<Select.Item item={v.value} key={v.value}>
<Stack>{v.description && v.description}</Stack>
<Select.ItemIndicator />
</Select.Item>
))}
</Select.Content>
</Select.Positioner>
</Portal>
</Select.Root>
</Field.Root>
);
};
export default SelectField;

View file

@ -0,0 +1,34 @@
/*
* CRITICAL: DO NOT MODIFY THIS COMPONENT WITHOUT DESIGN AUTHORITY APPROVAL
*
* This SelectOption component is used by SelectField throughout the application.
* Changes to this component's interface or styling will affect all dropdown
* options across multiple domains. Any modifications require extensive testing
* and approval from the application design authority.
*/
import React, { PropsWithChildren } from "react";
import { Box, Flex, Heading, Text } from "@chakra-ui/react";
const SelectOption: React.FC<
PropsWithChildren<{
title: string;
badge?: React.ReactNode;
}>
> = ({ title, badge, children }) => {
return (
<Box>
<Flex alignItems="center">
<Heading as="h1" size="sm" color="fg" fontWeight="bold">
{title}
</Heading>
{badge && badge}
</Flex>
<Text mt={1} textStyle="xs" color="fg.muted">
{children}
</Text>
</Box>
);
};
export default SelectOption;

View file

@ -0,0 +1,23 @@
/*
* CRITICAL: DO NOT MODIFY THIS COMPONENT WITHOUT DESIGN AUTHORITY APPROVAL
*
* This SelectOptionText component is used by SelectField throughout the application.
* Changes to this component's interface or styling will affect all dropdown
* options across multiple domains. Any modifications require extensive testing
* and approval from the application design authority.
*/
import React from "react";
import { Text } from "@chakra-ui/react";
const SelectOptionText: React.FC<{
children: React.ReactNode;
}> = ({ children }) => {
return (
<Text mt={1} textStyle="xs" color="fg.muted">
{children}
</Text>
);
};
export default SelectOptionText;

View file

@ -0,0 +1,40 @@
import { Table } from "@chakra-ui/react";
import { flexRender } from "@tanstack/react-table";
const SelectableTable = ({ table }) => {
return (
<>
<Table.Root interactive>
<Table.Header>
{table.getHeaderGroups().map((headerGroup) => (
<Table.Row key={headerGroup.id}>
{headerGroup.headers.map((header) => (
<Table.ColumnHeader key={header.id}>
{header.isPlaceholder
? null
: flexRender(
header.column.columnDef.header,
header.getContext(),
)}
</Table.ColumnHeader>
))}
</Table.Row>
))}
</Table.Header>
<Table.Body>
{table.getRowModel().rows.map((row) => (
<Table.Row key={row.id} onClick={row.getToggleSelectedHandler()}>
{row.getVisibleCells().map((cell) => (
<Table.Cell key={cell.id}>
{flexRender(cell.column.columnDef.cell, cell.getContext())}
</Table.Cell>
))}
</Table.Row>
))}
</Table.Body>
</Table.Root>
</>
);
};
export default SelectableTable;

View file

@ -0,0 +1,45 @@
import React, { PropsWithChildren } from "react";
import { Box, Container, Flex, Heading, Stack } from "@chakra-ui/react";
import UnauthedHeader from "./UnauthedHeader";
const SimplePage: React.FC<
PropsWithChildren<{
title: string;
}>
> = ({ title, children }) => {
return (
<>
<UnauthedHeader />
<Flex minH="100vh" align="center" justify="center" bg="primary.900">
<Container maxW="md" py={12}>
<Box
bg="primary.800"
p={8}
borderRadius="md"
boxShadow="lg"
borderWidth="1px"
borderColor="primary.muted"
>
<Stack spacing={6}>
<Heading
as="h1"
fontSize="2xl"
textAlign="center"
color="primary.400"
mb={2}
>
{title}
</Heading>
{children}
</Stack>
</Box>
</Container>
</Flex>
</>
);
};
export default SimplePage;

View file

@ -0,0 +1,45 @@
import React from "react";
import { Field, Slider as ChakraSlider } from "@chakra-ui/react";
interface SliderProps {
label: string;
minValue: number;
maxValue: number;
value: number;
step: number;
onValueChange: (x: number) => void;
}
const Slider: React.FC<SliderProps> = ({
label,
minValue,
maxValue,
value,
onValueChange,
step,
}) => {
return (
<Field.Root mb={4}>
<Field.Label fontWeight="medium">{label}</Field.Label>
<ChakraSlider.Root
min={minValue}
max={maxValue}
step={step}
value={[value]}
onValueChange={(e) => onValueChange(e.value[0])}
width="100%"
>
<ChakraSlider.ValueText />
<ChakraSlider.Control>
<ChakraSlider.Track bg="{colors.primary.muted}">
<ChakraSlider.Range bg="{colors.primary.solid}" />
</ChakraSlider.Track>
<ChakraSlider.Thumbs rounded={11} />
</ChakraSlider.Control>
</ChakraSlider.Root>
</Field.Root>
);
};
export default Slider;

View file

@ -0,0 +1,35 @@
import React from "react";
import { Badge, BadgeProps } from "@chakra-ui/react";
type StatusType = "success" | "warning" | "error" | "info" | "default";
interface StatusBadgeProps extends Omit<BadgeProps, "colorScheme"> {
status: StatusType;
label: string;
}
const StatusBadge: React.FC<StatusBadgeProps> = ({
status,
label,
...rest
}) => {
const colorSchemes: Record<StatusType, string> = {
success: "#65c97a",
warning: "orange",
error: "red",
info: "#5285ed",
default: "gray",
};
return (
<Badge
px={2}
py={1}
borderRadius="md"
bg={colorSchemes[status]}
color={status === "default" ? "gray.800" : "white"}
fontWeight="medium"
fontSize="xs"
{...rest}
>
{label}
</Badge>
);
};
export default StatusBadge;

View file

@ -0,0 +1,36 @@
import React from "react";
import { Box, Text, Center } from "@chakra-ui/react";
interface ErrorStateProps {
error: Error | unknown;
title?: string;
}
export const ErrorState: React.FC<ErrorStateProps> = ({
error,
title = "Error loading data",
}) => (
<Box
p={4}
borderWidth="1px"
borderColor="red.500"
borderRadius="md"
bg="red.50"
>
<Text color="red.700">
{title}: {error?.toString()}
</Text>
</Box>
);
interface EmptyStateProps {
message?: string;
}
export const EmptyState: React.FC<EmptyStateProps> = ({
message = "No data found.",
}) => (
<Center h="200px">
<Text color="fg.muted">{message}</Text>
</Center>
);

View file

@ -0,0 +1,55 @@
import React from "react";
import { Box } from "@chakra-ui/react";
import { Table } from "@tanstack/react-table";
import { ErrorState, EmptyState } from "./TableStates";
import BasicTable from "./BasicTable";
import ClickableTable from "./ClickableTable";
interface TableWithStatesProps<T> {
table: Table<T>;
data: T[];
error?: Error | unknown;
onClick?: (row: T) => void;
emptyMessage?: string;
errorTitle?: string;
bordered?: boolean;
}
const TableWithStates = <T,>({
table,
data,
error,
onClick,
emptyMessage = "No data found.",
errorTitle = "Error loading data",
bordered = true,
}: TableWithStatesProps<T>) => {
// Handle error state
if (error) {
return <ErrorState error={error} title={errorTitle} />;
}
// Handle empty state
if (data.length === 0) {
return <EmptyState message={emptyMessage} />;
}
// Render table with optional border wrapper
const TableComponent = onClick ? (
<ClickableTable table={table} onClick={(row) => onClick(row.original)} />
) : (
<BasicTable table={table} />
);
if (bordered) {
return (
<Box overflowX="auto" borderWidth="1px" borderRadius="lg">
{TableComponent}
</Box>
);
}
return TableComponent;
};
export default TableWithStates;

View file

@ -0,0 +1,40 @@
import React from "react";
import { Field, Textarea } from "@chakra-ui/react";
interface TextFieldProps {
label: string;
placeholder?: string;
value: string;
onValueChange: (x: string) => void;
required?: boolean;
disabled?: boolean;
}
const TextAreaField: React.FC<TextFieldProps> = ({
label,
placeholder,
value,
onValueChange,
required,
disabled,
}) => {
return (
<Field.Root mb={4} required={required}>
<Field.Label>
{label} {required && <Field.RequiredIndicator />}
</Field.Label>
<Textarea
placeholder={placeholder}
variant="subtle"
value={value}
onChange={(e) => onValueChange(e.target.value)}
maxH="30lh"
h="10lh"
disabled={disabled}
/>
</Field.Root>
);
};
export default TextAreaField;

View file

@ -0,0 +1,44 @@
import React from "react";
import { Field, Input } from "@chakra-ui/react";
interface TextFieldProps {
label: string;
placeholder?: string;
value: string;
onValueChange: (x: string) => void;
required?: boolean;
helperText?: string;
disabled?: boolean;
type?: string;
}
const TextField: React.FC<TextFieldProps> = ({
label,
placeholder,
value,
onValueChange,
required,
helperText,
disabled,
type = "text",
}) => {
return (
<Field.Root mb={4} required={required}>
<Field.Label>
{label} {required && <Field.RequiredIndicator />}
</Field.Label>
<Input
type={type}
value={value}
onChange={(e) => onValueChange(e.target.value)}
placeholder={placeholder}
variant="subtle"
disabled={disabled}
/>
{helperText && <Field.HelperText>{helperText}</Field.HelperText>}
</Field.Root>
);
};
export default TextField;

View file

@ -0,0 +1,25 @@
import { Box, Flex } from "@chakra-ui/react";
import ColorModeToggle from "../color-mode-toggle";
const UnauthedHeader = () => {
return (
<>
<Flex
mb={2}
alignItems="center"
justifyContent="space-between"
width="100%"
px={4}
py={2}
>
<Flex mb={2} alignItems="center"></Flex>
<Box>
<ColorModeToggle />
</Box>
</Flex>
</>
);
};
export default UnauthedHeader;

View file

@ -0,0 +1,19 @@
import React from "react";
import { HStack, Text } from "@chakra-ui/react";
import { User } from "lucide-react";
import { useSettings } from "@trustgraph/react-state";
const UserDisplay: React.FC = () => {
const { settings } = useSettings();
return (
<HStack gap={2} align="center">
<User size={14} color="currentColor" />
<Text fontSize="xs" fontWeight="medium" color="fg.muted">
{settings.user}
</Text>
</HStack>
);
};
export default UserDisplay;

View file

@ -0,0 +1,408 @@
import { describe, it, expect, vi } from "vitest";
import { render, screen } from "@testing-library/react";
import BasicTable from "../BasicTable";
// Mock Chakra UI components
vi.mock("@chakra-ui/react", () => ({
Table: {
Root: ({ children }: React.PropsWithChildren) => (
<table data-testid="table-root">{children}</table>
),
Header: ({ children }: React.PropsWithChildren) => (
<thead data-testid="table-header">{children}</thead>
),
Body: ({ children }: React.PropsWithChildren) => (
<tbody data-testid="table-body">{children}</tbody>
),
Row: ({ children }: React.PropsWithChildren) => (
<tr data-testid="table-row">{children}</tr>
),
ColumnHeader: ({ children }: React.PropsWithChildren) => (
<th data-testid="table-column-header">{children}</th>
),
Cell: ({ children }: React.PropsWithChildren) => (
<td data-testid="table-cell">{children}</td>
),
},
}));
// Mock TanStack React Table
vi.mock("@tanstack/react-table", () => ({
flexRender: vi.fn((content) => content),
}));
describe("BasicTable", () => {
const mockTable = {
getHeaderGroups: vi.fn(),
getRowModel: vi.fn(),
};
beforeEach(() => {
vi.clearAllMocks();
});
it("should render table structure", () => {
mockTable.getHeaderGroups.mockReturnValue([]);
mockTable.getRowModel.mockReturnValue({ rows: [] });
render(<BasicTable table={mockTable} />);
expect(screen.getByTestId("table-root")).toBeInTheDocument();
expect(screen.getByTestId("table-header")).toBeInTheDocument();
expect(screen.getByTestId("table-body")).toBeInTheDocument();
});
it("should render header groups", () => {
const mockHeaderGroups = [
{
id: "header-group-1",
headers: [
{
id: "header-1",
isPlaceholder: false,
column: {
columnDef: {
header: "Column 1",
},
},
getContext: () => ({}),
},
{
id: "header-2",
isPlaceholder: false,
column: {
columnDef: {
header: "Column 2",
},
},
getContext: () => ({}),
},
],
},
];
mockTable.getHeaderGroups.mockReturnValue(mockHeaderGroups);
mockTable.getRowModel.mockReturnValue({ rows: [] });
render(<BasicTable table={mockTable} />);
expect(screen.getByText("Column 1")).toBeInTheDocument();
expect(screen.getByText("Column 2")).toBeInTheDocument();
});
it("should not render placeholder headers", () => {
const mockHeaderGroups = [
{
id: "header-group-1",
headers: [
{
id: "header-1",
isPlaceholder: true,
column: {
columnDef: {
header: "Placeholder Column",
},
},
getContext: () => ({}),
},
{
id: "header-2",
isPlaceholder: false,
column: {
columnDef: {
header: "Visible Column",
},
},
getContext: () => ({}),
},
],
},
];
mockTable.getHeaderGroups.mockReturnValue(mockHeaderGroups);
mockTable.getRowModel.mockReturnValue({ rows: [] });
render(<BasicTable table={mockTable} />);
expect(screen.queryByText("Placeholder Column")).not.toBeInTheDocument();
expect(screen.getByText("Visible Column")).toBeInTheDocument();
});
it("should render table rows", () => {
const mockRows = [
{
id: "row-1",
getVisibleCells: () => [
{
id: "cell-1",
column: {
columnDef: {
cell: "Cell 1",
},
},
getContext: () => ({}),
},
{
id: "cell-2",
column: {
columnDef: {
cell: "Cell 2",
},
},
getContext: () => ({}),
},
],
},
];
mockTable.getHeaderGroups.mockReturnValue([]);
mockTable.getRowModel.mockReturnValue({ rows: mockRows });
render(<BasicTable table={mockTable} />);
expect(screen.getByText("Cell 1")).toBeInTheDocument();
expect(screen.getByText("Cell 2")).toBeInTheDocument();
});
it("should render multiple rows", () => {
const mockRows = [
{
id: "row-1",
getVisibleCells: () => [
{
id: "cell-1-1",
column: {
columnDef: {
cell: "Row 1 Cell 1",
},
},
getContext: () => ({}),
},
],
},
{
id: "row-2",
getVisibleCells: () => [
{
id: "cell-2-1",
column: {
columnDef: {
cell: "Row 2 Cell 1",
},
},
getContext: () => ({}),
},
],
},
];
mockTable.getHeaderGroups.mockReturnValue([]);
mockTable.getRowModel.mockReturnValue({ rows: mockRows });
render(<BasicTable table={mockTable} />);
expect(screen.getByText("Row 1 Cell 1")).toBeInTheDocument();
expect(screen.getByText("Row 2 Cell 1")).toBeInTheDocument();
});
it("should render complete table with headers and rows", () => {
const mockHeaderGroups = [
{
id: "header-group-1",
headers: [
{
id: "header-1",
isPlaceholder: false,
column: {
columnDef: {
header: "Name",
},
},
getContext: () => ({}),
},
{
id: "header-2",
isPlaceholder: false,
column: {
columnDef: {
header: "Age",
},
},
getContext: () => ({}),
},
],
},
];
const mockRows = [
{
id: "row-1",
getVisibleCells: () => [
{
id: "cell-1-1",
column: {
columnDef: {
cell: "John",
},
},
getContext: () => ({}),
},
{
id: "cell-1-2",
column: {
columnDef: {
cell: "25",
},
},
getContext: () => ({}),
},
],
},
{
id: "row-2",
getVisibleCells: () => [
{
id: "cell-2-1",
column: {
columnDef: {
cell: "Jane",
},
},
getContext: () => ({}),
},
{
id: "cell-2-2",
column: {
columnDef: {
cell: "30",
},
},
getContext: () => ({}),
},
],
},
];
mockTable.getHeaderGroups.mockReturnValue(mockHeaderGroups);
mockTable.getRowModel.mockReturnValue({ rows: mockRows });
render(<BasicTable table={mockTable} />);
// Check headers
expect(screen.getByText("Name")).toBeInTheDocument();
expect(screen.getByText("Age")).toBeInTheDocument();
// Check rows
expect(screen.getByText("John")).toBeInTheDocument();
expect(screen.getByText("25")).toBeInTheDocument();
expect(screen.getByText("Jane")).toBeInTheDocument();
expect(screen.getByText("30")).toBeInTheDocument();
});
it("should handle empty table", () => {
mockTable.getHeaderGroups.mockReturnValue([]);
mockTable.getRowModel.mockReturnValue({ rows: [] });
render(<BasicTable table={mockTable} />);
expect(screen.getByTestId("table-root")).toBeInTheDocument();
expect(screen.getByTestId("table-header")).toBeInTheDocument();
expect(screen.getByTestId("table-body")).toBeInTheDocument();
});
it("should handle table with headers but no rows", () => {
const mockHeaderGroups = [
{
id: "header-group-1",
headers: [
{
id: "header-1",
isPlaceholder: false,
column: {
columnDef: {
header: "Empty Table Header",
},
},
getContext: () => ({}),
},
],
},
];
mockTable.getHeaderGroups.mockReturnValue(mockHeaderGroups);
mockTable.getRowModel.mockReturnValue({ rows: [] });
render(<BasicTable table={mockTable} />);
expect(screen.getByText("Empty Table Header")).toBeInTheDocument();
});
it("should handle table with rows but no headers", () => {
const mockRows = [
{
id: "row-1",
getVisibleCells: () => [
{
id: "cell-1",
column: {
columnDef: {
cell: "Orphan Cell",
},
},
getContext: () => ({}),
},
],
},
];
mockTable.getHeaderGroups.mockReturnValue([]);
mockTable.getRowModel.mockReturnValue({ rows: mockRows });
render(<BasicTable table={mockTable} />);
expect(screen.getByText("Orphan Cell")).toBeInTheDocument();
});
it("should handle multiple header groups", () => {
const mockHeaderGroups = [
{
id: "header-group-1",
headers: [
{
id: "header-1",
isPlaceholder: false,
column: {
columnDef: {
header: "Group 1 Header",
},
},
getContext: () => ({}),
},
],
},
{
id: "header-group-2",
headers: [
{
id: "header-2",
isPlaceholder: false,
column: {
columnDef: {
header: "Group 2 Header",
},
},
getContext: () => ({}),
},
],
},
];
mockTable.getHeaderGroups.mockReturnValue(mockHeaderGroups);
mockTable.getRowModel.mockReturnValue({ rows: [] });
render(<BasicTable table={mockTable} />);
expect(screen.getByText("Group 1 Header")).toBeInTheDocument();
expect(screen.getByText("Group 2 Header")).toBeInTheDocument();
});
});

View file

@ -0,0 +1,327 @@
import { describe, it, expect, vi } from "vitest";
import { render, screen } from "@testing-library/react";
import Card from "../Card";
// Helper function to filter out Chakra UI props
const filterChakraProps = (props: Record<string, unknown>) => {
const chakraProps = [
"alignItems",
"justifyContent",
"direction",
"gap",
"p",
"px",
"py",
"pt",
"pb",
"pl",
"pr",
"m",
"mx",
"my",
"mt",
"mb",
"ml",
"mr",
"w",
"h",
"maxW",
"maxH",
"minW",
"minH",
"bg",
"color",
"borderRadius",
"borderWidth",
"borderColor",
"borderStyle",
"boxShadow",
"display",
"position",
"top",
"right",
"bottom",
"left",
"zIndex",
"overflow",
"textAlign",
"fontSize",
"fontWeight",
"lineHeight",
"letterSpacing",
"textTransform",
"textDecoration",
"opacity",
"visibility",
"cursor",
"pointerEvents",
"userSelect",
"resize",
"outline",
"transform",
"transformOrigin",
"transition",
"animation",
"colorPalette",
"variant",
"size",
"loading",
"disabled",
"checked",
"selected",
"active",
"focus",
"hover",
"flexDirection",
"flexWrap",
"flex",
"flexGrow",
"flexShrink",
"flexBasis",
"alignSelf",
"justifySelf",
"order",
"gridColumn",
"gridRow",
"gridArea",
"gridTemplateColumns",
"gridTemplateRows",
"gridGap",
"rowGap",
"columnGap",
"placeItems",
"placeContent",
"placeSelf",
"area",
"colSpan",
"rowSpan",
"start",
"end",
];
const filtered = { ...props };
chakraProps.forEach((prop) => delete filtered[prop]);
return filtered;
};
// Mock Chakra UI components
vi.mock("@chakra-ui/react", () => ({
Box: ({
children,
...props
}: React.PropsWithChildren<Record<string, unknown>>) => (
<div data-testid="box" {...filterChakraProps(props)}>
{children}
</div>
),
Flex: ({
children,
...props
}: React.PropsWithChildren<Record<string, unknown>>) => (
<div data-testid="flex" {...filterChakraProps(props)}>
{children}
</div>
),
Heading: ({
children,
...props
}: React.PropsWithChildren<Record<string, unknown>>) => (
<h3 data-testid="heading" {...filterChakraProps(props)}>
{children}
</h3>
),
Text: ({
children,
...props
}: React.PropsWithChildren<Record<string, unknown>>) => (
<p data-testid="text" {...filterChakraProps(props)}>
{children}
</p>
),
}));
describe("Card", () => {
it("should render with title", () => {
render(<Card title="Test Title" />);
expect(screen.getByTestId("heading")).toBeInTheDocument();
expect(screen.getByText("Test Title")).toBeInTheDocument();
});
it("should render with description when provided", () => {
render(<Card title="Test Title" description="Test description" />);
expect(screen.getByTestId("text")).toBeInTheDocument();
expect(screen.getByText("Test description")).toBeInTheDocument();
});
it("should not render description when not provided", () => {
render(<Card title="Test Title" />);
expect(screen.queryByTestId("text")).not.toBeInTheDocument();
});
it("should render icon when provided", () => {
const TestIcon = () => <span data-testid="test-icon">🎯</span>;
render(<Card title="Test Title" icon={<TestIcon />} />);
expect(screen.getByTestId("test-icon")).toBeInTheDocument();
expect(screen.getByText("🎯")).toBeInTheDocument();
});
it("should not render icon when not provided", () => {
render(<Card title="Test Title" />);
expect(screen.queryByTestId("test-icon")).not.toBeInTheDocument();
});
it("should render children when provided", () => {
render(
<Card title="Test Title">
<div data-testid="child-content">Child content</div>
</Card>,
);
expect(screen.getByTestId("child-content")).toBeInTheDocument();
expect(screen.getByText("Child content")).toBeInTheDocument();
});
it("should not render children when not provided", () => {
render(<Card title="Test Title" />);
expect(screen.queryByTestId("child-content")).not.toBeInTheDocument();
});
it("should render with all props together", () => {
const TestIcon = () => <span data-testid="test-icon"></span>;
render(
<Card
title="Complete Card"
description="This is a complete card with all props"
icon={<TestIcon />}
>
<div data-testid="child-content">Children content</div>
</Card>,
);
expect(screen.getByText("Complete Card")).toBeInTheDocument();
expect(
screen.getByText("This is a complete card with all props"),
).toBeInTheDocument();
expect(screen.getByTestId("test-icon")).toBeInTheDocument();
expect(screen.getByTestId("child-content")).toBeInTheDocument();
});
it("should handle empty title", () => {
render(<Card title="" />);
expect(screen.getByTestId("heading")).toBeInTheDocument();
expect(screen.getByTestId("heading")).toHaveTextContent("");
});
it("should handle empty description", () => {
render(<Card title="Test Title" description="" />);
// Empty description should not render the text element
expect(screen.queryByTestId("text")).not.toBeInTheDocument();
});
it("should handle long title", () => {
const longTitle = "a".repeat(100);
render(<Card title={longTitle} />);
expect(screen.getByText(longTitle)).toBeInTheDocument();
});
it("should handle long description", () => {
const longDescription = "b".repeat(500);
render(<Card title="Test Title" description={longDescription} />);
expect(screen.getByText(longDescription)).toBeInTheDocument();
});
it("should handle special characters in title", () => {
const specialTitle = "!@#$%^&*()_+{}[]|\\:;\"'<>,.?/~`";
render(<Card title={specialTitle} />);
expect(screen.getByText(specialTitle)).toBeInTheDocument();
});
it("should handle special characters in description", () => {
const specialDescription = "!@#$%^&*()_+{}[]|\\:;\"'<>,.?/~`";
render(<Card title="Test Title" description={specialDescription} />);
expect(screen.getByText(specialDescription)).toBeInTheDocument();
});
it("should handle multiline title", () => {
const multilineTitle = "Line 1\nLine 2\nLine 3";
render(<Card title={multilineTitle} />);
// Check that content is rendered (newlines may be normalized)
expect(screen.getByTestId("heading")).toBeInTheDocument();
expect(screen.getByTestId("heading").textContent).toContain("Line 1");
expect(screen.getByTestId("heading").textContent).toContain("Line 2");
expect(screen.getByTestId("heading").textContent).toContain("Line 3");
});
it("should handle multiline description", () => {
const multilineDescription = "Line 1\nLine 2\nLine 3";
render(<Card title="Test Title" description={multilineDescription} />);
// Check that content is rendered (newlines may be normalized)
expect(screen.getByTestId("text")).toBeInTheDocument();
expect(screen.getByTestId("text").textContent).toContain("Line 1");
expect(screen.getByTestId("text").textContent).toContain("Line 2");
expect(screen.getByTestId("text").textContent).toContain("Line 3");
});
it("should handle complex icon component", () => {
const ComplexIcon = () => (
<div data-testid="complex-icon">
<span>🎯</span>
<span>Complex</span>
</div>
);
render(<Card title="Test Title" icon={<ComplexIcon />} />);
expect(screen.getByTestId("complex-icon")).toBeInTheDocument();
expect(screen.getByText("🎯")).toBeInTheDocument();
expect(screen.getByText("Complex")).toBeInTheDocument();
});
it("should handle complex children", () => {
render(
<Card title="Test Title">
<div data-testid="complex-child">
<button>Click me</button>
<input placeholder="Enter text" />
<ul>
<li>Item 1</li>
<li>Item 2</li>
</ul>
</div>
</Card>,
);
expect(screen.getByTestId("complex-child")).toBeInTheDocument();
expect(screen.getByText("Click me")).toBeInTheDocument();
expect(screen.getByPlaceholderText("Enter text")).toBeInTheDocument();
expect(screen.getByText("Item 1")).toBeInTheDocument();
expect(screen.getByText("Item 2")).toBeInTheDocument();
});
it("should handle null icon", () => {
render(<Card title="Test Title" icon={null} />);
expect(screen.getByText("Test Title")).toBeInTheDocument();
expect(screen.queryByTestId("test-icon")).not.toBeInTheDocument();
});
it("should handle undefined icon", () => {
render(<Card title="Test Title" icon={undefined} />);
expect(screen.getByText("Test Title")).toBeInTheDocument();
expect(screen.queryByTestId("test-icon")).not.toBeInTheDocument();
});
});

View file

@ -0,0 +1,461 @@
import React from "react";
import { describe, it, expect, vi, beforeEach } from "vitest";
import { render, screen, fireEvent } from "@testing-library/react";
import ProgressSubmitButton from "../ProgressSubmitButton";
// Helper function to filter out Chakra UI props
const filterChakraProps = (props: Record<string, unknown>) => {
const chakraProps = [
"alignItems",
"justifyContent",
"direction",
"gap",
"p",
"px",
"py",
"pt",
"pb",
"pl",
"pr",
"m",
"mx",
"my",
"mt",
"mb",
"ml",
"mr",
"w",
"h",
"maxW",
"maxH",
"minW",
"minH",
"bg",
"color",
"borderRadius",
"borderWidth",
"borderColor",
"borderStyle",
"boxShadow",
"display",
"position",
"top",
"right",
"bottom",
"left",
"zIndex",
"overflow",
"textAlign",
"fontSize",
"fontWeight",
"lineHeight",
"letterSpacing",
"textTransform",
"textDecoration",
"opacity",
"visibility",
"cursor",
"pointerEvents",
"userSelect",
"resize",
"outline",
"transform",
"transformOrigin",
"transition",
"animation",
"colorPalette",
"variant",
"size",
"loading",
"disabled",
"checked",
"selected",
"active",
"focus",
"hover",
"flexDirection",
"flexWrap",
"flex",
"flexGrow",
"flexShrink",
"flexBasis",
"alignSelf",
"justifySelf",
"order",
"gridColumn",
"gridRow",
"gridArea",
"gridTemplateColumns",
"gridTemplateRows",
"gridGap",
"rowGap",
"columnGap",
"placeItems",
"placeContent",
"placeSelf",
"area",
"colSpan",
"rowSpan",
"start",
"end",
];
const filtered = { ...props };
chakraProps.forEach((prop) => delete filtered[prop]);
return filtered;
};
// Mock Chakra UI components
vi.mock("@chakra-ui/react", () => ({
Box: ({
children,
...props
}: React.PropsWithChildren<Record<string, unknown>>) => (
<div data-testid="box" {...filterChakraProps(props)}>
{children}
</div>
),
Button: ({
children,
onClick,
disabled,
loading,
...props
}: React.PropsWithChildren<
{
onClick?: React.MouseEventHandler<HTMLButtonElement>;
disabled?: boolean;
loading?: boolean;
} & Record<string, unknown>
>) => (
<button
data-testid="progress-button"
onClick={onClick}
disabled={disabled}
data-loading={loading}
{...filterChakraProps(props)}
>
{children}
</button>
),
}));
// Mock lucide-react icon
vi.mock("lucide-react", () => ({
SendHorizontal: () => <div data-testid="send-icon">Send</div>,
}));
describe("ProgressSubmitButton", () => {
const mockOnClick = vi.fn();
beforeEach(() => {
vi.clearAllMocks();
});
it("should render button with correct content", () => {
render(
<ProgressSubmitButton
disabled={false}
working={false}
onClick={mockOnClick}
/>,
);
expect(screen.getByTestId("progress-button")).toBeInTheDocument();
expect(screen.getByTestId("progress-button")).toHaveTextContent("Send");
expect(screen.getByTestId("send-icon")).toBeInTheDocument();
});
it("should render within Box wrapper", () => {
render(
<ProgressSubmitButton
disabled={false}
working={false}
onClick={mockOnClick}
/>,
);
expect(screen.getByTestId("box")).toBeInTheDocument();
expect(screen.getByTestId("box")).toContainElement(
screen.getByTestId("progress-button"),
);
});
it("should call onClick when button is clicked", () => {
render(
<ProgressSubmitButton
disabled={false}
working={false}
onClick={mockOnClick}
/>,
);
fireEvent.click(screen.getByTestId("progress-button"));
expect(mockOnClick).toHaveBeenCalledTimes(1);
});
it("should be disabled when disabled prop is true", () => {
render(
<ProgressSubmitButton
disabled={true}
working={false}
onClick={mockOnClick}
/>,
);
expect(screen.getByTestId("progress-button")).toBeDisabled();
});
it("should not be disabled when disabled prop is false", () => {
render(
<ProgressSubmitButton
disabled={false}
working={false}
onClick={mockOnClick}
/>,
);
expect(screen.getByTestId("progress-button")).not.toBeDisabled();
});
it("should have loading state when working prop is true", () => {
render(
<ProgressSubmitButton
disabled={false}
working={true}
onClick={mockOnClick}
/>,
);
expect(screen.getByTestId("progress-button")).toHaveAttribute(
"data-loading",
"true",
);
});
it("should not have loading state when working prop is false", () => {
render(
<ProgressSubmitButton
disabled={false}
working={false}
onClick={mockOnClick}
/>,
);
expect(screen.getByTestId("progress-button")).toHaveAttribute(
"data-loading",
"false",
);
});
it("should render button with correct structure", () => {
render(
<ProgressSubmitButton
disabled={false}
working={false}
onClick={mockOnClick}
/>,
);
const button = screen.getByTestId("progress-button");
expect(button).toBeInTheDocument();
expect(button.tagName).toBe("BUTTON");
});
it("should handle both disabled and working states", () => {
render(
<ProgressSubmitButton
disabled={true}
working={true}
onClick={mockOnClick}
/>,
);
const button = screen.getByTestId("progress-button");
expect(button).toBeDisabled();
expect(button).toHaveAttribute("data-loading", "true");
});
it("should not call onClick when disabled", () => {
render(
<ProgressSubmitButton
disabled={true}
working={false}
onClick={mockOnClick}
/>,
);
fireEvent.click(screen.getByTestId("progress-button"));
expect(mockOnClick).not.toHaveBeenCalled();
});
it("should handle multiple clicks when enabled", () => {
render(
<ProgressSubmitButton
disabled={false}
working={false}
onClick={mockOnClick}
/>,
);
const button = screen.getByTestId("progress-button");
fireEvent.click(button);
fireEvent.click(button);
fireEvent.click(button);
expect(mockOnClick).toHaveBeenCalledTimes(3);
});
it("should maintain consistent state when props change", () => {
const { rerender } = render(
<ProgressSubmitButton
disabled={false}
working={false}
onClick={mockOnClick}
/>,
);
let button = screen.getByTestId("progress-button");
expect(button).not.toBeDisabled();
expect(button).toHaveAttribute("data-loading", "false");
// Change to working state
rerender(
<ProgressSubmitButton
disabled={false}
working={true}
onClick={mockOnClick}
/>,
);
button = screen.getByTestId("progress-button");
expect(button).not.toBeDisabled();
expect(button).toHaveAttribute("data-loading", "true");
// Change to disabled state
rerender(
<ProgressSubmitButton
disabled={true}
working={false}
onClick={mockOnClick}
/>,
);
button = screen.getByTestId("progress-button");
expect(button).toBeDisabled();
expect(button).toHaveAttribute("data-loading", "false");
});
it("should handle rapid state changes", () => {
const { rerender } = render(
<ProgressSubmitButton
disabled={false}
working={false}
onClick={mockOnClick}
/>,
);
// Rapidly change states
rerender(
<ProgressSubmitButton
disabled={true}
working={true}
onClick={mockOnClick}
/>,
);
rerender(
<ProgressSubmitButton
disabled={false}
working={false}
onClick={mockOnClick}
/>,
);
const button = screen.getByTestId("progress-button");
expect(button).not.toBeDisabled();
expect(button).toHaveAttribute("data-loading", "false");
});
it("should handle onClick function changes", () => {
const newOnClick = vi.fn();
const { rerender } = render(
<ProgressSubmitButton
disabled={false}
working={false}
onClick={mockOnClick}
/>,
);
fireEvent.click(screen.getByTestId("progress-button"));
expect(mockOnClick).toHaveBeenCalledTimes(1);
// Change onClick function
rerender(
<ProgressSubmitButton
disabled={false}
working={false}
onClick={newOnClick}
/>,
);
fireEvent.click(screen.getByTestId("progress-button"));
expect(newOnClick).toHaveBeenCalledTimes(1);
expect(mockOnClick).toHaveBeenCalledTimes(1); // Should not be called again
});
it("should render icon alongside text", () => {
render(
<ProgressSubmitButton
disabled={false}
working={false}
onClick={mockOnClick}
/>,
);
const button = screen.getByTestId("progress-button");
expect(button).toHaveTextContent("Send");
expect(screen.getByTestId("send-icon")).toBeInTheDocument();
});
it("should handle edge case combinations", () => {
// Test disabled=true, working=false
const { rerender } = render(
<ProgressSubmitButton
disabled={true}
working={false}
onClick={mockOnClick}
/>,
);
let button = screen.getByTestId("progress-button");
expect(button).toBeDisabled();
expect(button).toHaveAttribute("data-loading", "false");
// Test disabled=false, working=true
rerender(
<ProgressSubmitButton
disabled={false}
working={true}
onClick={mockOnClick}
/>,
);
button = screen.getByTestId("progress-button");
expect(button).not.toBeDisabled();
expect(button).toHaveAttribute("data-loading", "true");
// Test disabled=true, working=true
rerender(
<ProgressSubmitButton
disabled={true}
working={true}
onClick={mockOnClick}
/>,
);
button = screen.getByTestId("progress-button");
expect(button).toBeDisabled();
expect(button).toHaveAttribute("data-loading", "true");
});
});

View file

@ -0,0 +1,329 @@
import { describe, it, expect, vi } from "vitest";
import { render, screen, fireEvent } from "@testing-library/react";
import userEvent from "@testing-library/user-event";
import TextField from "../TextField";
// Helper function to filter out Chakra UI props
const filterChakraProps = (props: Record<string, unknown>) => {
const chakraProps = [
"alignItems",
"justifyContent",
"direction",
"gap",
"p",
"px",
"py",
"pt",
"pb",
"pl",
"pr",
"m",
"mx",
"my",
"mt",
"mb",
"ml",
"mr",
"w",
"h",
"maxW",
"maxH",
"minW",
"minH",
"bg",
"color",
"borderRadius",
"borderWidth",
"borderColor",
"borderStyle",
"boxShadow",
"display",
"position",
"top",
"right",
"bottom",
"left",
"zIndex",
"overflow",
"textAlign",
"fontSize",
"fontWeight",
"lineHeight",
"letterSpacing",
"textTransform",
"textDecoration",
"opacity",
"visibility",
"cursor",
"pointerEvents",
"userSelect",
"resize",
"outline",
"transform",
"transformOrigin",
"transition",
"animation",
"colorPalette",
"variant",
"size",
"loading",
"disabled",
"checked",
"selected",
"active",
"focus",
"hover",
"flexDirection",
"flexWrap",
"flex",
"flexGrow",
"flexShrink",
"flexBasis",
"alignSelf",
"justifySelf",
"order",
"gridColumn",
"gridRow",
"gridArea",
"gridTemplateColumns",
"gridTemplateRows",
"gridGap",
"rowGap",
"columnGap",
"placeItems",
"placeContent",
"placeSelf",
"area",
"colSpan",
"rowSpan",
"start",
"end",
"required",
];
const filtered = { ...props };
chakraProps.forEach((prop) => delete filtered[prop]);
return filtered;
};
// Mock Chakra UI components
vi.mock("@chakra-ui/react", () => ({
Field: {
Root: ({
children,
...props
}: React.PropsWithChildren<Record<string, unknown>>) => (
<div data-testid="field-root" {...filterChakraProps(props)}>
{children}
</div>
),
Label: ({
children,
...props
}: React.PropsWithChildren<Record<string, unknown>>) => (
<label data-testid="field-label" {...filterChakraProps(props)}>
{children}
</label>
),
RequiredIndicator: ({ ...props }: Record<string, unknown>) => (
<span data-testid="required-indicator" {...filterChakraProps(props)}>
*
</span>
),
HelperText: ({
children,
...props
}: React.PropsWithChildren<Record<string, unknown>>) => (
<div data-testid="helper-text" {...filterChakraProps(props)}>
{children}
</div>
),
},
Input: ({
value,
onChange,
placeholder,
...props
}: {
value?: string;
onChange?: React.ChangeEventHandler<HTMLInputElement>;
placeholder?: string;
} & Record<string, unknown>) => (
<input
data-testid="text-input"
value={value}
onChange={onChange}
placeholder={placeholder}
{...filterChakraProps(props)}
/>
),
}));
describe("TextField", () => {
const defaultProps = {
label: "Test Label",
value: "",
onValueChange: vi.fn(),
};
beforeEach(() => {
vi.clearAllMocks();
});
it("should render with label and input", () => {
render(<TextField {...defaultProps} />);
expect(screen.getByTestId("field-label")).toBeInTheDocument();
expect(screen.getByText("Test Label")).toBeInTheDocument();
expect(screen.getByTestId("text-input")).toBeInTheDocument();
});
it("should display the current value", () => {
render(<TextField {...defaultProps} value="test value" />);
const input = screen.getByTestId("text-input");
expect(input).toHaveValue("test value");
});
it("should call onValueChange when input value changes", async () => {
const user = userEvent.setup();
const mockOnValueChange = vi.fn();
render(<TextField {...defaultProps} onValueChange={mockOnValueChange} />);
const input = screen.getByTestId("text-input");
await user.type(input, "new value");
expect(mockOnValueChange).toHaveBeenCalledWith("n");
expect(mockOnValueChange).toHaveBeenCalledWith("e");
expect(mockOnValueChange).toHaveBeenCalledWith("w");
expect(mockOnValueChange).toHaveBeenCalledWith(" ");
expect(mockOnValueChange).toHaveBeenCalledWith("v");
expect(mockOnValueChange).toHaveBeenCalledWith("a");
expect(mockOnValueChange).toHaveBeenCalledWith("l");
expect(mockOnValueChange).toHaveBeenCalledWith("u");
expect(mockOnValueChange).toHaveBeenCalledWith("e");
});
it("should handle onChange event correctly", () => {
const mockOnValueChange = vi.fn();
render(<TextField {...defaultProps} onValueChange={mockOnValueChange} />);
const input = screen.getByTestId("text-input");
fireEvent.change(input, { target: { value: "test input" } });
expect(mockOnValueChange).toHaveBeenCalledWith("test input");
});
it("should display placeholder when provided", () => {
render(<TextField {...defaultProps} placeholder="Enter text here" />);
const input = screen.getByTestId("text-input");
expect(input).toHaveAttribute("placeholder", "Enter text here");
});
it("should show required indicator when required is true", () => {
render(<TextField {...defaultProps} required />);
expect(screen.getByTestId("required-indicator")).toBeInTheDocument();
expect(screen.getByText("*")).toBeInTheDocument();
});
it("should not show required indicator when required is false", () => {
render(<TextField {...defaultProps} required={false} />);
expect(screen.queryByTestId("required-indicator")).not.toBeInTheDocument();
});
it("should not show required indicator when required is undefined", () => {
render(<TextField {...defaultProps} />);
expect(screen.queryByTestId("required-indicator")).not.toBeInTheDocument();
});
it("should display helper text when provided", () => {
render(<TextField {...defaultProps} helperText="This is helper text" />);
expect(screen.getByTestId("helper-text")).toBeInTheDocument();
expect(screen.getByText("This is helper text")).toBeInTheDocument();
});
it("should not display helper text when not provided", () => {
render(<TextField {...defaultProps} />);
expect(screen.queryByTestId("helper-text")).not.toBeInTheDocument();
});
it("should handle empty string value", () => {
render(<TextField {...defaultProps} value="" />);
const input = screen.getByTestId("text-input");
expect(input).toHaveValue("");
});
it("should handle special characters in value", () => {
const specialValue = "!@#$%^&*()_+{}[]|\\:;\"'<>,.?/~`";
render(<TextField {...defaultProps} value={specialValue} />);
const input = screen.getByTestId("text-input");
expect(input).toHaveValue(specialValue);
});
it("should handle multiline text in value", () => {
const multilineValue = "Line 1\nLine 2\nLine 3";
render(<TextField {...defaultProps} value={multilineValue} />);
const input = screen.getByTestId("text-input");
// Check that the value contains the expected content
expect(input).toBeInTheDocument();
expect(input.getAttribute("value")).toContain("Line 1");
expect(input.getAttribute("value")).toContain("Line 2");
expect(input.getAttribute("value")).toContain("Line 3");
});
it("should render field root structure", () => {
render(<TextField {...defaultProps} required />);
const fieldRoot = screen.getByTestId("field-root");
expect(fieldRoot).toBeInTheDocument();
expect(fieldRoot).toContainElement(screen.getByTestId("field-label"));
expect(fieldRoot).toContainElement(screen.getByTestId("text-input"));
});
it("should handle long text values", () => {
const longValue = "a".repeat(1000);
render(<TextField {...defaultProps} value={longValue} />);
const input = screen.getByTestId("text-input");
expect(input).toHaveValue(longValue);
});
it("should handle rapid input changes", async () => {
const user = userEvent.setup();
const mockOnValueChange = vi.fn();
render(<TextField {...defaultProps} onValueChange={mockOnValueChange} />);
const input = screen.getByTestId("text-input");
await user.type(input, "abc", { delay: 1 });
expect(mockOnValueChange).toHaveBeenCalledTimes(3);
expect(mockOnValueChange).toHaveBeenNthCalledWith(1, "a");
expect(mockOnValueChange).toHaveBeenNthCalledWith(2, "b");
expect(mockOnValueChange).toHaveBeenNthCalledWith(3, "c");
});
it("should clear input when value changes to empty string", () => {
const { rerender } = render(
<TextField {...defaultProps} value="initial value" />,
);
let input = screen.getByTestId("text-input");
expect(input).toHaveValue("initial value");
rerender(<TextField {...defaultProps} value="" />);
input = screen.getByTestId("text-input");
expect(input).toHaveValue("");
});
});