git-subtree-dir: ai-context/workbench-ui git-subtree-split: 32e36a5c2131e429a7081cfaf67dabad3193cda3
24 KiB
UI Toolkits and Framework Notes
Change Management and API Stability
CRITICAL: Components in src/components/common/ are foundational to the entire application. DO NOT modify these components without explicit approval from the application design authority. Changes to common components have extensive downstream impact and can break multiple features across the application.
Change Impact Assessment
Before modifying any common component:
- Document all consumers - Search the entire codebase for usage
- Assess breaking changes - Any interface changes affect all consumers
- Test extensively - Changes can break seemingly unrelated features
- Get approval - Design authority must approve all common component changes
Lessons from SelectField Issues
Recent issues with SelectField demonstrate why common component changes are dangerous:
- September 2025: Changes to SelectField to support one feature (Ontology editor) broke document submission
- Root cause: Interface contract violations between array/string APIs
- Impact: Multiple components across different domains affected
- Resolution required: Systematic updates to 15+ components across the application
Key takeaway: Changing common components to fix one feature often breaks others. Always prefer adapter patterns or feature-specific solutions over modifying shared infrastructure.
Directory Structure and Organization Rationale
Core Principles
We follow a domain-driven, flat structure that avoids unnecessary nesting and keeps related code together:
- Avoid generic aggregation directories - No
src/hooks/,src/constants/,src/utils/that become dumping grounds - Colocate by domain - Keep related code together in feature-specific directories
- Flat when possible - Single files don't need their own subdirectories
- Clear separation of concerns - Different types of logic go in appropriate places
Directory Layout
src/
├── components/ # UI components organized by domain
│ ├── schemas/ # All schema-related UI components
│ │ ├── EditSchemaDialog.tsx # Main orchestrator component
│ │ ├── SchemaFieldEditor.tsx # Individual field editing
│ │ ├── SchemaFieldsList.tsx # Fields list management
│ │ ├── SchemaTableStates.tsx # Reusable table states
│ │ ├── useSchemaForm.ts # Form state logic (colocated)
│ │ └── ...
│ ├── taxonomies/ # All taxonomy-related UI components
│ └── common/ # Truly shared/generic components
├── model/ # Data models, types, and domain constants
│ ├── schemas-table.tsx # Schema data models
│ ├── schemaTypes.ts # Schema type constants
│ └── ...
├── state/ # Application state management
│ ├── schemas.ts # Schema API calls and state
│ └── ...
├── api/ # Direct API communication
└── utils/ # Pure utility functions (no React/UI)
Rationale by Directory
src/components/[domain]/
- Contains ALL UI components for a specific domain (schemas, taxonomies, etc.)
- Includes domain-specific hooks like
useSchemaForm.ts - Why: Keeps everything needed to work on a feature in one place
- Avoid: Generic
src/hooks/that becomes a dumping ground
src/model/
- Data types, interfaces, constants, and domain models
- Why: Centralized data definitions that can be imported anywhere
- Example:
schemaTypes.tscontainsSCHEMA_TYPE_OPTIONSandDEFAULT_FIELD
src/state/
- High-level application state management
- React Query hooks for API calls and caching
- Why: Separates data fetching/caching from UI logic
src/api/
- Direct API communication layer
- WebSocket management
- Why: Abstracts network concerns from business logic
Benefits of This Approach
- Discoverability: All schema-related code is in
src/components/schemas/ - Maintainability: Changes to schema features are localized
- Reusability: Shared types in
src/model/can be imported anywhere - Scalability: New domains get their own component directories
- Avoids Anti-patterns: No generic directories that accumulate unrelated files
Example: Schema Feature Organization
When working on schema-related features, everything you need is in one place:
- UI components:
src/components/schemas/ - Data models:
src/model/schemas-table.tsx,src/model/schemaTypes.ts - API/state:
src/state/schemas.ts
This eliminates the need to hunt through multiple generic directories to understand or modify a feature.
Icon Library
CRITICAL: Always use lucide-react for icons throughout the application. Do NOT use react-icons or any other icon library.
// ✅ Correct - Use lucide-react
import { Plus, Save, Trash2, Edit, Settings } from "lucide-react";
// ❌ Wrong - Don't use react-icons
import { FiPlus, FiSave } from "react-icons/fi";
Common icon mappings from react-icons to lucide-react:
FiPlus→PlusFiX→XFiSave→SaveFiTrash2→Trash2FiEdit/FiEdit3→EditFiSettings→SettingsFiDownload→DownloadFiUpload→UploadFiMove→MoveFiMoreVertical→MoreVerticalFiList→List
Chakra UI Version
CRITICAL: This project uses Chakra UI v3, NOT v2. Always check component APIs against v3 documentation.
Key Chakra v3 Migration Points
Modal → Dialog
// ❌ Chakra v2
<Modal isOpen={open} onClose={onClose}>
<ModalOverlay />
<ModalContent>
<ModalHeader>Title</ModalHeader>
<ModalBody>Content</ModalBody>
</ModalContent>
</Modal>
// ✅ Chakra v3
<Dialog.Root open={open} onOpenChange={(x) => onOpenChange(x.open)}>
<Portal>
<Dialog.Backdrop />
<Dialog.Positioner>
<Dialog.Content>
<Dialog.Header>
<Dialog.Title>Title</Dialog.Title>
</Dialog.Header>
<Dialog.Body>Content</Dialog.Body>
</Dialog.Content>
</Dialog.Positioner>
</Portal>
</Dialog.Root>
Toast System
// ❌ Chakra v2
const toast = useToast();
toast({ title: "Success", status: "success" });
// ✅ Chakra v3
import { toaster } from "../ui/toaster";
toaster.create({ title: "Success", status: "success" });
Form Components
// ❌ Chakra v2
<FormControl>
<FormLabel>Label</FormLabel>
<Input />
</FormControl>
// ✅ Chakra v3
<Field.Root>
<Field.Label>Label</Field.Label>
<Input />
</Field.Root>
Tabs Structure
// ❌ Chakra v2
<Tabs>
<TabList>
<Tab>Tab 1</Tab>
</TabList>
<TabPanels>
<TabPanel>Content</TabPanel>
</TabPanels>
</Tabs>
// ✅ Chakra v3
<Tabs.Root>
<Tabs.List>
<Tabs.Trigger value="tab1">Tab 1</Tabs.Trigger>
</Tabs.List>
<Tabs.Content value="tab1">Content</Tabs.Content>
</Tabs.Root>
Menu Components
// ❌ Chakra v2
<Menu>
<MenuButton>Button</MenuButton>
<MenuList>
<MenuItem>Item</MenuItem>
</MenuList>
</Menu>
// ✅ Chakra v3
<Menu.Root>
<Menu.Trigger>Button</Menu.Trigger>
<Menu.Content>
<Menu.Item>Item</Menu.Item>
</Menu.Content>
</Menu.Root>
Props Changes
// ❌ Chakra v2 props
colorScheme="blue"
isDisabled={true}
// ✅ Chakra v3 props
colorPalette="blue"
disabled={true}
Layout Components
// ❌ Chakra v2
<Divider />
// ✅ Chakra v3
<Separator />
Spacing Props
// ❌ Old pattern
<VStack spacing={4}>
<HStack spacing={2}>
// ✅ Chakra v3
<VStack gap={4}>
<HStack gap={2}>
Button Icons
// ❌ Old pattern
<Button leftIcon={<Plus />}>Add</Button>
<IconButton icon={<Upload />} aria-label="Upload" />
// ✅ Chakra v3
<Button><Plus /> Add</Button>
<IconButton aria-label="Upload"><Upload /></IconButton>
Input Groups (Simplified)
// ❌ Chakra v2
<InputGroup>
<InputLeftElement>🔍</InputLeftElement>
<Input placeholder="Search..." />
</InputGroup>
// ✅ Chakra v3 (simplified approach)
<Input placeholder="🔍 Search..." />
Avatar Structure
// ❌ Chakra v2
<Avatar name="John Doe" />
// ✅ Chakra v3
<Avatar.Root>
<Avatar.Fallback name="John Doe" />
</Avatar.Root>
Alert Component
// ❌ Chakra v2
<Alert status="error">
<AlertIcon />
<Text>Error message</Text>
</Alert>
// ✅ Chakra v3
<Alert.Root status="error">
<Alert.Indicator />
<Alert.Content>
<Alert.Description>Error message</Alert.Description>
</Alert.Content>
</Alert.Root>
Alert Status Options:
status="error"- Red error alertsstatus="warning"- Orange warning alertsstatus="success"- Green success alertsstatus="info"- Blue info alerts
Alert with Title:
<Alert.Root status="warning">
<Alert.Indicator />
<Alert.Content>
<Alert.Title>Warning Title</Alert.Title>
<Alert.Description>Warning description text</Alert.Description>
</Alert.Content>
</Alert.Root>
Progress Component
// ❌ Chakra v2
<Progress value={60} colorScheme="blue" />
// ✅ Chakra v3
<Progress.Root value={60}>
<Progress.Track>
<Progress.Range />
</Progress.Track>
<Progress.Label />
<Progress.ValueText />
</Progress.Root>
Progress with custom styling:
<Progress.Root value={75} colorPalette="green" size="sm">
<Progress.Track>
<Progress.Range />
</Progress.Track>
</Progress.Root>
Layout Components Still Work
Important: VStack, HStack, Box, Grid, GridItem, Text, Button, Input, etc. still work the same way in v3. The confusion around VStack/HStack causing "invalid component type" errors is usually due to circular import dependencies, not Chakra version issues.
Common Debugging Steps
- Check imports: Ensure all Chakra components are imported from
@chakra-ui/react - Verify component structure: Use the v3 nested component patterns (Component.Root, Component.Trigger, etc.)
- Check props: Use
colorPaletteinstead ofcolorScheme,disabledinstead ofisDisabled - Circular imports: If getting "invalid component type" errors with basic components like VStack, check for circular import dependencies
Migration Verification Checklist
When migrating components to Chakra v3:
- Replace
<Alert>with<Alert.Root> - Replace
<AlertIcon />with<Alert.Indicator /> - Wrap text in
<Alert.Content><Alert.Description>...</Alert.Description></Alert.Content> - Replace
<Progress>with<Progress.Root><Progress.Track><Progress.Range /></Progress.Track></Progress.Root> - Use
<Card.Root><Card.Header /><Card.Body /></Card.Root>structure for cards - Replace
<Modal>with<Dialog.Root> - Replace
spacingprops withgapprops - Replace
colorSchemewithcolorPalette - Replace
isDisabledwithdisabled - Test build after changes
- Verify visual styling is preserved
- Be systematic: search for old patterns, document the fix, then apply consistently
Project-Specific Patterns
Notifications
CRITICAL: NEVER use the toaster directly. The toaster from @chakra-ui/react or ../ui/toaster must NOT be imported or used directly. Always use the useNotification hook:
// ❌ NEVER do this - toaster is forbidden
import { toaster } from "../ui/toaster";
import { toaster } from "@chakra-ui/react";
toaster.create({ title: "Success", status: "success" });
// ✅ ALWAYS do this instead
import { useNotification } from "../../state/notify";
const notify = useNotification();
notify.success("Operation completed successfully");
notify.error("Something went wrong");
notify.info("FYI: This is informational");
Why toaster is forbidden:
- Direct toaster usage bypasses the project's notification standards
- The
useNotificationhook provides consistent error prefixing and styling - It maintains a unified notification interface across the application
- Direct toaster usage can cause inconsistent user experience
Common Components
ALWAYS prefer using pre-built components from src/components/common/ instead of raw Chakra components. These components handle Chakra v3 APIs correctly and reduce boilerplate:
// ❌ Don't use raw Chakra components
<Field.Root required>
<Field.Label>Name</Field.Label>
<Input value={value} onChange={(e) => setValue(e.target.value)} />
</Field.Root>
// ✅ Use common components instead
<TextField
label="Name"
value={value}
onValueChange={setValue}
required
/>
Available Common Components:
TextField- Text input with label and validationTextAreaField- Multi-line text inputSelectField- Dropdown select with rich optionsBasicTable- Pre-configured Tanstack TableCard- Consistent card layout with title/descriptionProgressSubmitButton- Submit button with loading statePageHeader- Standard page header layoutStatusBadge- Consistent status indicatorsCenterSpinner- Loading spinnerChipInputField- Tag/chip input fieldNumberField- Numeric input with validationSlider- Range slider component
SelectField Usage
CRITICAL: SelectField expects array values for selection and MUST include description fields for dropdown display:
// ✅ Correct usage
import SelectField from "../common/SelectField";
import SelectOptionText from "../common/SelectOptionText";
<SelectField
label="Select Option"
items={[
{
value: 'option1',
label: 'Option 1',
description: (
<SelectOptionText>
Option 1
</SelectOptionText>
)
},
{
value: 'option2',
label: 'Option 2',
description: (
<SelectOptionText>
Option 2
</SelectOptionText>
)
}
]}
value={selectedValues} // array - current selection (empty array for no selection)
onValueChange={(values) => setSelectedValues(values)} // receives array
/>
Important Notes:
- Pass an empty array
[]for no selection, not an empty string - The
valueprop should always be an array - The
onValueChangecallback receives an array - For single selection, extract the first element:
values.length > 0 ? values[0] : null - REQUIRED: The
descriptionfield MUST be provided usingSelectOptionTextorSelectOptioncomponents - Missing descriptions will result in empty dropdown options
Example with single selection extraction:
const [selectedValues, setSelectedValues] = useState([]);
// Get the selected value (for single select behavior)
const selectedValue = selectedValues.length > 0 ? selectedValues[0] : null;
// Handle submission
const handleSubmit = () => {
if (selectedValue) {
onSubmit(selectedValue);
}
};
Common Mistake - Missing Descriptions:
// ❌ WRONG - Will show empty dropdown options
items={[
{value: 'option1', label: 'Option 1'}, // Missing description!
{value: 'option2', label: 'Option 2'} // Missing description!
]}
// ✅ CORRECT - Includes required descriptions
items={[
{
value: 'option1',
label: 'Option 1',
description: <SelectOptionText>Option 1</SelectOptionText>
},
{
value: 'option2',
label: 'Option 2',
description: <SelectOptionText>Option 2</SelectOptionText>
}
]}
Theming and Colors
ALWAYS use semantic color tokens instead of direct color palettes. The theme provides semantic tokens that automatically handle light/dark mode:
// ❌ Don't use direct color palettes
colorPalette="blue"
bg="gray.100"
color="deepPlum.700"
// ✅ Use semantic tokens instead
colorPalette="primary"
bg="bg.muted"
color="primary.fg"
Available Semantic Color Palettes:
primary- Main brand color (airForceBlue)accent- Secondary brand color (deepPlum)observing- For observation callouts (warmNeutral)thinking- For thinking callouts (deepPlum variants)insightful- For answer callouts (neutralGreen)
Semantic Token Structure: Each palette has these variants:
.solid- Strong, high contrast (buttons, badges).contrast- Text on solid backgrounds.fg- Foreground text color.muted- Subtle backgrounds.subtle- Light backgrounds.emphasized- Medium emphasis backgrounds.focusRing- Focus indicators
Background/Text Tokens:
background- Main page backgroundtext- Main text colorbg.muted- Subtle background areasfg.muted- Muted text
Page Structure
ALWAYS use consistent page structure with PageHeader:
// ❌ Don't embed headings in components
export const MyComponent = () => {
return (
<VStack>
<Heading>My Page Title</Heading>
<Content />
</VStack>
);
};
// ✅ Use PageHeader at the page level
// In pages/MyPage.tsx:
import PageHeader from "../components/common/PageHeader";
import MyComponent from "../components/MyComponent";
const MyPage = () => {
return (
<>
<PageHeader
icon={<IconName />}
title="Page Title"
description="Brief description of what this page does"
/>
<MyComponent />
</>
);
};
// In components/MyComponent.tsx (no heading):
export const MyComponent = () => {
return (
<VStack>
<Content />
</VStack>
);
};
Page Structure Rules:
- Page components go in
src/pages/directory - Always use
PageHeadercomponent for consistent headers - Page title and description should be at page level, not component level
- Components should not contain their own page-level headings
- Use appropriate lucide-react icons for the page icon
Progress Management and Loading States
CRITICAL: Always use the useActivity hook for loading states instead of managing spinners manually. This provides consistent loading indicators across the application.
// ❌ Don't manage loading states manually
const [isLoading, setIsLoading] = useState(false);
const handleSubmit = async () => {
setIsLoading(true);
try {
await submitData();
} finally {
setIsLoading(false);
}
};
// ✅ Use useActivity hook instead
import { useActivity } from "../../state/activity";
const submitMutation = useMutation({
mutationFn: submitData,
});
// Automatically shows/hides loading indicator
useActivity(submitMutation.isPending, "Submitting data");
Progress System Components:
-
useProgressStateStore- Zustand store that manages global activity trackingactivity: Set<string>- Active operations being trackederror: string- Current error stateaddActivity(name)- Add a loading operationremoveActivity(name)- Remove a loading operationsetError(message)- Set/clear error state
-
useActivity(isActive, description)- React hook for automatic activity managementisActive: boolean- Whether the activity is currently runningdescription: string- User-friendly description of the activity- Automatically adds/removes activities based on the boolean condition
- Handles cleanup when component unmounts or dependencies change
Usage Patterns:
// ✅ With React Query mutations
const updateMutation = useMutation({ mutationFn: updateData });
useActivity(updateMutation.isPending, "Updating settings");
// ✅ With React Query queries
const dataQuery = useQuery({ queryKey: ['data'], queryFn: fetchData });
useActivity(dataQuery.isLoading, "Loading data");
// ✅ Multiple activities for complex operations
useActivity(settingsQuery.isLoading, "Loading settings");
useActivity(updateSettingsMutation.isPending, "Saving settings");
useActivity(resetSettingsMutation.isPending, "Resetting settings");
// ✅ Manual activity management (when useActivity isn't sufficient)
const addActivity = useProgressStateStore((state) => state.addActivity);
const removeActivity = useProgressStateStore((state) => state.removeActivity);
const handleComplexOperation = async () => {
const activityId = "Processing complex operation";
addActivity(activityId);
try {
await step1();
await step2();
await step3();
} finally {
removeActivity(activityId);
}
};
Benefits of the Progress System:
- Consistent UX: All loading states are managed centrally
- Automatic cleanup: Activities are removed when operations complete or components unmount
- Deduplication: Multiple identical activity names are automatically deduplicated
- Global visibility: The UI can show a global loading indicator when any activities are active
- Error handling: Centralized error state management
- Zero boilerplate: Just call
useActivity()with a boolean and description
Integration with TanStack Query: The progress system integrates perfectly with TanStack Query's loading states:
// All these patterns work seamlessly together
export const useSettings = () => {
const settingsQuery = useQuery({
queryKey: ["settings"],
queryFn: fetchSettings,
});
const updateMutation = useMutation({
mutationFn: updateSettings,
});
// Automatic activity tracking
useActivity(settingsQuery.isLoading, "Loading settings");
useActivity(updateMutation.isPending, "Saving settings");
return {
settings: settingsQuery.data,
isLoading: settingsQuery.isLoading,
updateSettings: updateMutation.mutate,
isSaving: updateMutation.isPending,
};
};
Table Components
CRITICAL: Always use TanStack Table with our standardized table components instead of manually implementing Chakra Table structures.
Standard Table Pattern:
- Create Model File (
src/model/[feature]-table.tsx):
import { createColumnHelper } from "@tanstack/react-table";
export type MyData = {
id: string;
name: string;
description: string;
};
export const columnHelper = createColumnHelper<MyData>();
export const columns = [
columnHelper.accessor("id", {
header: "ID",
cell: (info) => info.getValue(),
}),
columnHelper.accessor("name", {
header: "Name",
cell: (info) => info.getValue(),
}),
columnHelper.accessor("description", {
header: "Description",
cell: (info) => info.getValue(),
}),
];
- Use Common Table Components:
// ❌ Don't manually implement table structure
<Table.Root>
<Table.Header>
<Table.Row>
<Table.ColumnHeader>Name</Table.ColumnHeader>
</Table.Row>
</Table.Header>
<Table.Body>
{data.map(row => (
<Table.Row key={row.id}>
<Table.Cell>{row.name}</Table.Cell>
</Table.Row>
))}
</Table.Body>
</Table.Root>
// ✅ Use standardized components and models
import { BasicTable } from "../common/BasicTable";
import { columns, MyData } from "../../model/my-data-table";
const table = useReactTable({
data: myData as MyData[],
columns,
getCoreRowModel: getCoreRowModel(),
getSortedRowModel: getSortedRowModel(),
});
return <BasicTable table={table} />;
Available Table Components:
BasicTable- Standard table displayClickableTable- Table with row click handlersSelectableTable- Table with row selection checkboxes
Standard Column Patterns:
// Selection column (for SelectableTable)
columnHelper.display({
id: "select",
header: ({ table }) => (
<Checkbox.Root
checked={selectionState(table)}
onChange={table.getToggleAllRowsSelectedHandler()}
>
<Checkbox.HiddenInput />
<Checkbox.Control />
</Checkbox.Root>
),
cell: ({ row }) => (
<Checkbox.Root
checked={row.getIsSelected()}
onChange={row.getToggleSelectedHandler()}
>
<Checkbox.HiddenInput />
<Checkbox.Control />
</Checkbox.Root>
),
});
// Data columns with custom formatting
columnHelper.accessor("timestamp", {
header: "Created",
cell: (info) => timeString(info.getValue()),
});
// Tags/badges column
columnHelper.accessor("tags", {
header: "Tags",
cell: (info) =>
info.getValue()?.map((tag) => (
<Tag.Root key={tag} mr={2}>
<Tag.Label>{tag}</Tag.Label>
</Tag.Root>
)),
});
Benefits of Standardized Tables:
- ✅ Consistent behavior and styling across the application
- ✅ Built-in sorting, selection, and interaction patterns
- ✅ Type safety with column definitions
- ✅ Easier testing and maintenance
- ✅ Better performance with TanStack optimizations
- ✅ Automatic loading state integration with progress system
Other Patterns
- Use Tanstack Query for state management with existing socket-based config API
- Follow kebab-case naming conventions for IDs and URLs
- Always prefer TanStack Table models over manual Chakra Table implementation