Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
121 changes: 121 additions & 0 deletions CLAUDE.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,121 @@
# CLAUDE.md

This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository.

## Development Commands

### Build and Testing
```bash
# Install dependencies
npm install

# Build all packages
npm run build --workspaces

# Test all packages (requires build first)
npm run test --workspaces

# Test single package
npm run test --workspace @atomicjolt/atomic-elements

# Watch tests for a specific package
npm run test:watch --workspace @atomicjolt/atomic-elements

# Update test snapshots
npm run test:update --workspace @atomicjolt/atomic-elements
```

### Development Environment
```bash
# Run the interactive playground
npm run playground

# Start Storybook development server
npm run storybook:dev

# Start documentation development server
npm run docs:start
```

### Version Management (Changesets)
```bash
# Create a new changeset for package changes
npx changeset

# Check changeset status
npx changeset status

# Version packages based on changesets
npx changeset version

# Publish new package versions
npx changeset publish
```

### Component and Package Scaffolding
```bash
# Create new component in atomic-elements
npm run scaffold:component

# Create new package
npm run scaffold:package
```

## Repository Architecture

This is a monorepo containing multiple NPM packages maintained by Atomic Jolt, built with:
- **Workspaces**: npm workspaces for monorepo management
- **Build tools**: Rollup (packages), Vite (dev/playground), TypeScript
- **Testing**: Vitest with jsdom environment
- **Documentation**: Storybook + Docusaurus
- **Components**: React with styled-components, React Aria, React Stately

### Package Structure

**Main packages** (all in `packages/` directory):
- `atomic-elements` - Core React component library with design system
- `atomic-fuel` - Redux utilities and React components for LTI applications
- `hooks` - Reusable React hooks
- `forms` - Form components and utilities
- `lti-client` - LTI client-side utilities and components
- `lti-server` - LTI server-side utilities and validation
- `lti-components` - LTI-specific React components
- `lti-types` - TypeScript types for LTI
- `canvas-client` - Canvas API client utilities

### Key Directories

- `packages/atomic-elements/src/components/` - All UI components organized by category (Buttons, Fields, Layout, etc.)
- `packages/atomic-elements/src/styles/` - Design system tokens, themes, and styling utilities
- `packages/atomic-elements/src/hooks/` - Component-specific hooks
- `.storybook/` - Storybook configuration and utilities
- `playground/` - Interactive development environment
- `templates/` - Scaffolding templates for new components and packages
- `docs/` - Documentation source files (Docusaurus)

### Component Architecture

Components follow a consistent structure:
- `ComponentName.component.tsx` - Main component implementation
- `ComponentName.styles.ts` - Styled-components styling
- `ComponentName.context.ts` - React context (if needed)
- `ComponentName.stories.tsx` - Storybook stories
- `ComponentName.spec.tsx` - Vitest tests
- `ComponentName.types.ts` - TypeScript types (if complex)
- `index.tsx` - Public exports

### Build System

- Each package has its own `tsconfig.build.json` and build configuration
- `atomic-elements` uses Rollup to generate both ESM and CJS outputs
- Path aliases configured in `vite.config.ts` for easier imports (@components, @hooks, @styles, @utils)
- TypeScript with strict mode enabled
- Components built with React 18/19 compatibility

### Testing Strategy

- Vitest for all testing with jsdom environment
- React Testing Library for component testing
- Each package has individual test configuration
- Shared test setup in `vitest.setup.ts` files
- Build packages before running tests to ensure proper imports
6 changes: 6 additions & 0 deletions packages/atomic-elements/CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,5 +1,11 @@
# @atomicjolt/atomic-elements

## 3.6.0

### Minor Changes

- Add ErrorBoundary helper component

## 3.5.1

### Patch Changes
Expand Down
159 changes: 159 additions & 0 deletions packages/atomic-elements/docs/Guides/ErrorBoundary.mdx
Original file line number Diff line number Diff line change
@@ -0,0 +1,159 @@
---
title: ErrorBoundary
---

import { ErrorBoundary } from "@atomicjolt/atomic-elements";

# ErrorBoundary

The `ErrorBoundary` component provides a way to catch JavaScript errors in your component tree and display fallback UI instead of crashing the entire application.

## Basic Usage

### Static Fallback

The simplest usage is with a static fallback UI:

```tsx
import { ErrorBoundary } from "@atomicjolt/atomic-elements";

function App() {
return (
<ErrorBoundary fallback={<div>Something went wrong!</div>}>
<MyComponent />
</ErrorBoundary>
);
}
```

### Function Fallback

For more dynamic error handling, use a function fallback:

```tsx
function App() {
return (
<ErrorBoundary
fallback={(error, reset) => (
<div>
<h2>Oops! Something went wrong</h2>
<p>Error: {error.message}</p>
<button onClick={reset}>Try again</button>
</div>
)}
>
<MyComponent />
</ErrorBoundary>
);
}
```

## Reset Keys

The `resetKeys` prop allows the ErrorBoundary to automatically reset when certain values change. This is useful for resetting the error state when the conditions that caused the error are no longer present.

When any value in the `resetKeys` array changes (using strict equality `!==` comparison), the ErrorBoundary will:

1. Clear the error state
2. Call the `onReset` callback (if provided)
3. Re-render the children components

```tsx
function UserDashboard() {
const { currentUser } = useAuth();
const [retryCount, setRetryCount] = useState(0);

return (
<ErrorBoundary
// Reset when user changes or when retryCount increments
resetKeys={[currentUser?.id, retryCount]}
fallback={(error, reset) => (
<ErrorBanner>
<p>Failed to load dashboard: {error.message}</p>
<Button onPress={() => setRetryCount((prev) => prev + 1)}>
Try Again
</Button>
</ErrorBanner>
)}
onReset={() => {
console.log("Dashboard error boundary reset");
}}
>
<Dashboard user={currentUser} />
</ErrorBoundary>
);
}
```

## Manual Reset

You can also trigger a reset manually using the `reset` function provided to function fallbacks:

```tsx
function AppWithManualReset() {
const [shouldThrow, setShouldThrow] = useState(true);

return (
<ErrorBoundary
fallback={(error, reset) => (
<div>
<h2>Component Error</h2>
<p>{error.message}</p>
<button
onClick={() =>
// Manually reset the error boundary state
reset();
}}
>
Fix and Retry
</button>
</div>
)}
>
<BuggyComponent />
</ErrorBoundary>
);
}
```

## Error Monitoring

Use the `onError` callback to report errors to monitoring services:

```tsx
function MonitoredApp() {
return (
<ErrorBoundary
onError={(error, errorInfo) => {
// Report to your error monitoring service
errorReporting.captureException(error, {
extra: errorInfo,
tags: { component: "App" },
});
}}
fallback={(error, reset) => (
<ErrorBanner>
<h3>Something went wrong</h3>
<p>We've been notified and are working on a fix.</p>
<Button onPress={reset}>Try Again</Button>
</ErrorBanner>
)}
>
<App />
</ErrorBoundary>
);
}
```

## Best Practices

- **Use resetKeys** - Include any state that could affect whether the error occurs
- **Provide meaningful fallbacks** - Show what went wrong and how to recover
- **Report errors** - Use the `onError` callback to send errors to monitoring services
- **Test your error boundaries** - Throw errors in development to verify your fallbacks work

## Limitations

- Error boundaries only catch errors in child components, not in the error boundary itself
- Error boundaries don't catch errors in event handlers - use try/catch for those
- Error boundaries don't catch errors in async code - handle Promise rejections separately
2 changes: 1 addition & 1 deletion packages/atomic-elements/package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "@atomicjolt/atomic-elements",
"version": "3.5.1",
"version": "3.6.0",
"sideEffects": false,
"module": "dist/esm/index.js",
"main": "dist/cjs/index.js",
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,83 @@
import { Component, ErrorInfo, ReactNode } from "react";

export interface ErrorBoundaryProps {
children: ReactNode;
/** Fallback UI to display when an error is caught. Can be a ReactNode or a function that receives the error and a reset function. */
fallback: ReactNode | ((error: Error, reset: () => void) => ReactNode);
/** Callback invoked when an error is caught. */
onError?: (error: Error, errorInfo: ErrorInfo) => void;
/** Callback invoked when the error boundary is reset. */
onReset?: () => void;
/** An array of values that, when changed, will reset the error boundary. */
resetKeys?: Array<any>;
}

interface ErrorBoundaryState {
hasError: boolean;
error: Error | null;
}

export class ErrorBoundary extends Component<
ErrorBoundaryProps,
ErrorBoundaryState
> {
constructor(props: ErrorBoundaryProps) {
super(props);
this.state = {
hasError: false,
error: null,
};
}

static getDerivedStateFromError(error: Error): Partial<ErrorBoundaryState> {
return {
hasError: true,
error,
};
}

componentDidCatch(error: Error, errorInfo: ErrorInfo) {
this.props.onError?.(error, errorInfo);
}

componentDidUpdate(prevProps: ErrorBoundaryProps) {
const { resetKeys } = this.props;
const prevResetKeys = prevProps.resetKeys;

if (this.state.hasError && prevResetKeys !== resetKeys) {
if (resetKeys && prevResetKeys) {
const hasResetKeyChanged = resetKeys.some(
(key, idx) => prevResetKeys[idx] !== key
);
if (hasResetKeyChanged) {
this.reset();
}
} else if (resetKeys !== prevResetKeys) {
this.reset();
}
}
}

reset = () => {
this.props.onReset?.();

this.setState({
hasError: false,
error: null,
});
};

render() {
if (this.state.hasError && this.state.error) {
const { fallback } = this.props;

if (typeof fallback === "function") {
return fallback(this.state.error, this.reset);
}

return fallback;
}

return this.props.children;
}
}
Loading