Skip to content

Conversation

@bpowers
Copy link
Owner

@bpowers bpowers commented Jan 28, 2026

Summary

  • Replace all MUI components in @system-dynamics/diagram with custom implementations using CSS modules, Radix Tabs, and Downshift
  • Add explicit MUI dependencies to @system-dynamics/app (previously inherited via hoisting)
  • Remove MUI/Emotion/ThemeProvider from the website package

Details

New custom components in src/diagram/components/:

  • SvgIcon + icons: SVG icon base component and 15 named icon exports
  • Button / IconButton: Class components matching MUI's variant/color/size API
  • TextField: Outlined and standard variants with floating label
  • Snackbar / SnackbarContent: Portal-based with auto-hide timer
  • Tabs / Tab: Wraps @radix-ui/react-tabs with animated indicator
  • SpeedDial / SpeedDialAction / SpeedDialIcon: Custom FAB with action menu
  • Drawer: Portal-based with backdrop, escape key, CSS transitions
  • Autocomplete: Downshift useCombobox with portal dropdown

MUI's Card, Paper, CardContent, CardActions, and Typography replaced with plain <div> elements styled via CSS module classes.

Test plan

  • yarn tsc passes
  • yarn lint passes
  • yarn build succeeds (app, web component, and website)
  • Zero @mui or @emotion imports remain in src/diagram/
  • Zero MUI CSS selectors remain
  • Visual inspection of all affected components
  • Add unit tests for behavioral components (Snackbar timer, Drawer escape, SpeedDial close reasons, Tabs onChange, TextField label shrink, Autocomplete filtering)

Replace all MUI components in the diagram package with custom
implementations: SvgIcon, Button, IconButton, TextField, Snackbar,
Tabs (Radix), SpeedDial, Drawer, and Autocomplete (Downshift).

MUI's Card, Paper, CardContent, CardActions, and Typography are
replaced with plain divs with CSS module classes that replicate
MUI's elevation shadows, padding, and layout. The global
MuiAutocomplete CSS selector is removed in favor of a scoped
CSS module class in the new Autocomplete component.

The app package gains explicit MUI dependencies (previously
inherited via hoisting from diagram), and the website drops
its MUI/Emotion/ThemeProvider usage entirely.
@claude
Copy link

claude bot commented Jan 28, 2026

Code Review: Remove MUI and Emotion Dependencies from Diagram Package

This is an excellent refactoring that successfully eliminates MUI dependencies from the @system-dynamics/diagram package. The implementation is clean, well-structured, and follows React best practices. Here's my detailed feedback:


Strengths

Architecture & Design

  • Clean separation of concerns: Moving MUI to only @system-dynamics/app while the diagram package uses custom components is the right architectural choice
  • Consistent component API: The custom components maintain MUI-compatible props (variant, color, size, etc.), making migration smooth
  • Good use of modern libraries: Radix UI for Tabs and Downshift for Autocomplete are solid, accessible choices
  • CSS Modules: Proper scoping prevents style conflicts and follows modern best practices

Code Quality

  • Class components: Correctly uses class components consistently (avoiding hooks as per CLAUDE.md)
  • Proper TypeScript typing: All components have well-defined interfaces
  • Memory management: Proper cleanup in lifecycle methods (Snackbar clears timers, Drawer removes event listeners)
  • Portal usage: Correct use of ReactDOM.createPortal for Drawer, Snackbar, and Autocomplete dropdowns

Accessibility

  • SVG icons properly marked with aria-hidden="true"
  • Buttons include aria-label support
  • Radix Tabs provides built-in accessibility
  • Proper semantic HTML (buttons, not divs with onClick)

⚠️ Issues & Concerns

1. Bug: Autocomplete Component Uses Hooks (Should Use Class)

Location: src/diagram/components/Autocomplete.tsx:27

Per CLAUDE.md guidelines: "Prefer class components — AVOID hooks like useState"

The Autocomplete component uses function components with hooks (useState, useRef, useEffect, useMemo). This violates the codebase conventions.

Recommendation: Refactor to a class component to maintain consistency.

2. Type Safety: Unsafe Type Assertions

Location: src/diagram/components/SpeedDial.tsx:56 and SpeedDial.tsx:86

Using onClick={onClick as any} defeats TypeScript's type safety.

Recommendation: Fix the type definitions to match the actual event types without casting.

3. Type Safety: Tabs Uses Internal Props Hack

Location: src/diagram/components/Tabs.tsx:31

const value = (rest as any)._tabValue as string;

Injecting private props (_tabValue) and accessing them with as any is fragile.

Recommendation: Use React Context or properly extend the TabProps interface.

4. TextField Border Width Jumps on Focus/Error

Location: src/diagram/components/TextField.module.css:31-37

Border width changes from 1px to 2px on focus/error, causing layout shift.

Recommendation: Use a pseudo-element for the thicker border to avoid layout shift.

5. Missing Accessibility: Drawer Lacks ARIA Attributes

Location: src/diagram/components/Drawer.tsx:41-50

The Drawer is missing:

  • role="dialog" on the panel
  • aria-modal="true"
  • Focus trap (focus should stay within drawer when open)

Recommendation: Add proper ARIA attributes and focus management.

6. TextField: Missing id and htmlFor Connection

Location: src/diagram/components/TextField.tsx:95-106

Labels aren't properly associated with inputs for accessibility.

Recommendation: Add id/htmlFor connection between label and input.


🧪 Testing Concerns

No Unit Tests for New Components

The PR description notes "Add unit tests for behavioral components" as incomplete. Per CLAUDE.md's TDD approach and pre-commit hooks that check for test quality, these tests should be added before merging.


📦 Dependency Management

Added Dependencies (Good Choices)

  • @radix-ui/react-tabs: Battle-tested, accessible ✅
  • downshift: Industry standard for autocomplete ✅
  • clsx: Minimal utility for classnames ✅

Removed Dependencies

  • Successfully removed @mui/material, @emotion/react, @emotion/styled from diagram package ✅
  • Correctly moved MUI dependencies to @system-dynamics/app

🎯 Recommendations Summary

Must Fix Before Merge:

  1. Refactor Autocomplete to class component (violates codebase conventions)
  2. Remove as any type assertions in SpeedDial and Tabs
  3. Add unit tests for behavioral components (per pre-commit requirements)

Should Fix:

  1. Fix TextField border width jump causing layout shift
  2. Add proper ARIA attributes to Drawer
  3. Connect TextField labels with htmlFor/id

Overall Assessment

This is high-quality work that successfully achieves the goal of removing MUI dependencies. The custom components are well-implemented with proper styling that replicates MUI's appearance. The main issues are:

  1. Convention violation (Autocomplete using hooks)
  2. Missing tests
  3. Some type safety gaps

Once these are addressed, this PR will be production-ready. The architecture change is sound and will reduce bundle size and increase flexibility.

Recommended Action: Request changes for the must-fix items, particularly the Autocomplete refactor and test coverage.

@codecov
Copy link

codecov bot commented Jan 28, 2026

Codecov Report

✅ All modified and coverable lines are covered by tests.
✅ Project coverage is 76.81%. Comparing base (4781bcb) to head (0ed43be).
⚠️ Report is 8 commits behind head on main.

Additional details and impacted files
@@            Coverage Diff             @@
##             main     #217      +/-   ##
==========================================
+ Coverage   76.79%   76.81%   +0.01%     
==========================================
  Files          69       69              
  Lines       20523    20523              
==========================================
+ Hits        15761    15765       +4     
+ Misses       4762     4758       -4     

☔ View full report in Codecov by Sentry.
📢 Have feedback on the report? Share it here.

🚀 New features to boost your workflow:
  • ❄️ Test Analytics: Detect flaky tests, report on failures, and find test suite problems.
  • 📦 JS Bundle Analysis: Save yourself from yourself by tracking and limiting bundle sizes in JS merges.

Copy link

@chatgpt-codex-connector chatgpt-codex-connector bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: ddec24f8dc

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment on lines 84 to 87
<button
className={clsx(styles.actionButton, styles.actionButtonOpen, className)}
onClick={onClick as any}
aria-label={title}

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Close speed dial on action click

The new SpeedDialAction only forwards the action’s onClick handler and never triggers the parent onClose, so selecting Stock/Flow/etc. does not clear dialOpen anymore. In Editor, dialOpen is only toggled by the FAB click or handleDialClose (which ignores mouseLeave/blur), so tool selection now leaves the speed dial open until the user clicks the FAB again or presses Escape. This is a behavior regression from MUI’s SpeedDialAction, which closes after action clicks, and it keeps the overlay covering the canvas unexpectedly.

Useful? React with 👍 / 👎.

- SpeedDial: Close dial on action click by injecting _onActionClick
  callback from parent to children via cloneElement. Add 'actionClick'
  to CloseReason type. Editor.tsx now handles this to close the dial
  while keeping the selected tool.

- SpeedDial: Fix type safety by changing onClick to accept
  HTMLButtonElement instead of HTMLDivElement, removing 'as any' casts.

- Tabs: Replace _tabValue cloneElement hack with React Context for
  passing tab index from parent to child.

- TextField: Add id/htmlFor connection between labels and inputs for
  accessibility. Generate unique IDs when not provided.

- TextField: Fix border width layout shift by using box-shadow for the
  thicker focus/error indicator instead of changing border-width.

- Drawer: Add role="dialog", aria-modal="true", and tabIndex for
  accessibility. Implement focus trap (Tab cycles within panel when
  open) and focus management (save/restore focus on open/close).
@bpowers
Copy link
Owner Author

bpowers commented Jan 28, 2026

@codex review

@claude
Copy link

claude bot commented Jan 28, 2026

PR Review: Remove MUI and Emotion dependencies from @system-dynamics/diagram

Overview

This PR successfully removes Material-UI and Emotion dependencies from the @system-dynamics/diagram package by implementing custom components. The implementation is well-structured with CSS modules and demonstrates a clean separation of concerns. Here's my detailed feedback:

✅ Strengths

Architecture & Design

  • Clean CSS Module approach: All components use CSS modules with proper scoping, avoiding global CSS pollution
  • Consistent component API: Custom components maintain MUI-compatible prop interfaces, making migration seamless
  • Portal usage: Appropriate use of React portals for Snackbar, Drawer, and Autocomplete dropdown positioning
  • Class components: Correctly follows the codebase standard of using class components over hooks (except where necessary)
  • Proper lifecycle management: Good cleanup in componentWillUnmount for Drawer and Snackbar to prevent memory leaks

Implementation Quality

  • Drawer component: Excellent accessibility implementation with focus management, escape key handling, and focus trap
  • TextField component: Well-implemented label shrink animation and proper state management for focus/blur
  • Snackbar component: Clean timer implementation with proper cleanup
  • SpeedDial: Nice use of cloneElement to inject handlers into children
  • Tabs: Good use of Radix UI primitives with custom styling via animated indicator

🔍 Issues & Concerns

Critical Issues

1. Autocomplete: Inconsistent React paradigm (src/diagram/components/Autocomplete.tsx:27)

The Autocomplete component is a function component with hooks, while the codebase standard (per CLAUDE.md) is to use class components and avoid hooks. This is inconsistent with all other components in this PR.

Recommendation: Refactor to a class component, or provide justification for why hooks are required here (likely due to Downshift's useCombobox hook). If hooks are necessary, add a comment explaining this exception.

2. Missing unit tests (Test plan checklist item incomplete)

The PR description includes a test plan with an incomplete item:

  • Add unit tests for behavioral components (Snackbar timer, Drawer escape, SpeedDial close reasons, Tabs onChange, TextField label shrink, Autocomplete filtering)

Recommendation: Add comprehensive unit tests before merging, especially for:

  • Snackbar auto-hide timer behavior
  • Drawer keyboard navigation and focus trap
  • TextField label shrink transitions
  • Autocomplete filtering logic
  • SpeedDial close reasons

Per CLAUDE.md's TDD principle, tests should have been written first to guide implementation.

High-Priority Issues

3. TextField: Missing accessibility labels (src/diagram/components/TextField.tsx:148-152)

The outlined variant's label is not properly associated with the input when shrunk. The label uses htmlFor but should also have proper ARIA attributes for screen readers when the label is in the "shrunk" state.

Recommendation: Add aria-label or ensure the label's association is maintained in all states.

4. Drawer: Global event listener not scoped (src/diagram/components/Drawer.tsx:24)

The keydown listener is added to the document in componentDidMount, but it will fire even when the drawer is closed. The handler does check this.props.open, but this is inefficient.

Recommendation: Add/remove the listener in componentDidUpdate based on the open state:

componentDidUpdate(prevProps: DrawerProps) {
  if (this.props.open && !prevProps.open) {
    document.addEventListener('keydown', this.handleKeyDown);
    // existing focus management...
  } else if (!this.props.open && prevProps.open) {
    document.removeEventListener('keydown', this.handleKeyDown);
    // existing focus restoration...
  }
}

5. Autocomplete: Potential stale closure in dropdown positioning (src/diagram/components/Autocomplete.tsx:74-83)

The useEffect for dropdown positioning only runs when isOpen changes. If the input wrapper is scrolled or the window is resized while the dropdown is open, the position becomes stale.

Recommendation: Add window resize and scroll event listeners, or use a library like @floating-ui/react-dom for robust positioning.

Medium-Priority Issues

6. Hardcoded color values instead of CSS variables (Multiple CSS files)

Many CSS files use hardcoded color values:

  • Button.module.css: rgba(25, 118, 210, 0.04), #1565c0, #9a0036
  • Autocomplete.module.css: #ADD8E6 (light blue for highlighted option)
  • TextField.module.css: rgba(0, 0, 0, 0.23), rgba(0, 0, 0, 0.87)

Some CSS variables are used (--color-primary, --color-secondary, --color-error), but not consistently.

Recommendation: Extract all color values to CSS variables for maintainability and potential theming support.

7. Button: Missing ripple effect (src/diagram/components/Button.tsx)

MUI buttons have a ripple effect on click. The custom Button lacks this, which may be a noticeable visual regression.

Recommendation: Either add a ripple effect or acknowledge this as an intentional simplification in the PR description.

8. SnackbarContent: Fragile prop filtering (src/diagram/components/Snackbar.tsx:76-82)

The component manually filters out onClose and variant props. This is brittle and could miss other non-DOM props.

Recommendation: Use a whitelist approach or TypeScript's Omit to be more explicit about allowed props.

9. TextField: Global ID counter potential collision (src/diagram/components/TextField.tsx:11-12)

The textFieldIdCounter is a module-level variable that increments indefinitely. In long-running SPAs, this could theoretically overflow (though unlikely).

Recommendation: Use a more robust ID generation strategy, like useId (if React 18+) or a UUID library. However, this is a very low priority issue.

Low-Priority / Style Issues

10. Missing CSS variable definitions

The code references --color-primary, --color-secondary, and --color-error but these variables aren't defined in the new CSS files.

Recommendation: Ensure these are defined in a global stylesheet or document where they should be set.

11. SpeedDial: Unused prop type (src/diagram/components/SpeedDial.tsx:14)

The onOpen prop is defined but never used in the component.

Recommendation: Remove if not needed, or implement if this was an oversight.

📊 Security & Performance

  • No security concerns: No XSS vulnerabilities or unsafe DOM manipulation detected
  • Performance: Portal usage is appropriate and shouldn't cause performance issues. The only concern is the Drawer's global keydown listener (see issue show simulation results #4).
  • Memory leaks: Properly handled with componentWillUnmount cleanup in Drawer and Snackbar

🧪 Test Coverage

Status: ❌ Incomplete

The PR test plan explicitly notes that unit tests are not yet added. Per CLAUDE.md:

Test Driven Development (TDD): Follow TDD best practices, ensure tests actually assert the behavior we're expecting AND have high code coverage.

Required before merge:

  1. Unit tests for all behavioral components
  2. Integration tests showing components work with existing Editor components
  3. Visual regression tests (if available) or manual verification checklist

📦 Dependencies

  • Additions:

    • @radix-ui/react-tabs (good choice for accessible tabs)
    • downshift (industry-standard for autocomplete)
    • clsx (lightweight classname utility)
  • Removals: MUI and Emotion correctly removed from diagram package

  • App package: Correctly adds explicit MUI dependencies (previously inherited via hoisting)

🎯 Recommendations Summary

Must fix before merge:

  1. Add comprehensive unit tests (critical per test plan)
  2. Fix Drawer global event listener inefficiency (show simulation results #4)
  3. Fix Autocomplete dropdown positioning (view + edit equation #5)
  4. Document/justify Autocomplete's use of hooks (edit variable names #1)

Should fix before merge:
5. Extract hardcoded colors to CSS variables (#6)
6. Improve TextField accessibility (#3)
7. Fix SnackbarContent prop filtering (#8)

Nice to have:
8. Add button ripple effect or document its omission (#7)
9. Remove unused SpeedDial.onOpen prop (#11)

✨ Conclusion

This is a well-structured PR that successfully removes MUI dependencies with clean, maintainable custom components. The CSS module approach is excellent, and the component APIs are thoughtfully designed. However, the PR should not be merged until unit tests are added per the project's TDD requirements and CLAUDE.md guidelines.

The implementation quality is high, but the missing tests and a few architectural issues (particularly around Autocomplete's hook usage and Drawer's event listener efficiency) should be addressed.

Overall assessment: Approve with required changes ✅ (pending test coverage)


Review generated following CLAUDE.md conventions and development standards

@claude
Copy link

claude bot commented Jan 28, 2026

Code Review: PR #217 - Remove MUI and Emotion Dependencies

Summary

This PR successfully removes Material-UI and Emotion dependencies from @system-dynamics/diagram by implementing custom components. This is a substantial undertaking (1,763 additions, 220 deletions) that replaces a mature component library with custom implementations.

Strengths

Architecture & Design

Clean separation of concerns: Custom components are properly organized in src/diagram/components/
CSS Modules approach: Avoids runtime CSS-in-JS overhead from Emotion
Portal-based overlays: Proper use of ReactDOM.createPortal for Drawer, Snackbar, and Autocomplete dropdowns
Class components: Follows the project's established pattern (per CLAUDE.md preference)
Zero MUI imports remaining: Successfully eliminated all @mui imports from the diagram package

Component Quality

Accessibility: Good ARIA attributes (aria-label, aria-modal, aria-describedby, aria-expanded)
Keyboard navigation: Drawer implements Escape key handling and focus trap
Focus management: Drawer properly saves and restores focus
Lifecycle management: Proper cleanup of timers and event listeners in componentWillUnmount

Critical Issues

1. Missing Test Coverage ⚠️ BLOCKER

The PR description acknowledges this: "Add unit tests for behavioral components". Currently there are zero tests for the new components. According to CLAUDE.md's TDD requirement: "Test Driven Development (TDD): Follow TDD best practices, ensure tests actually assert the behavior we're expecting AND have high code coverage."

Required tests for:

  • Snackbar: Auto-hide timer behavior, proper cleanup
  • Drawer: Escape key handling, focus trap, backdrop clicks
  • SpeedDial: Close reasons, action click propagation
  • TextField: Label shrink behavior, focus states
  • Autocomplete: Filtering logic, keyboard navigation
  • Tabs: onChange events, indicator positioning

2. TextField Component - ID Counter Memory Leak

src/diagram/components/TextField.tsx:11

let textFieldIdCounter = 0;

This module-scoped counter increments on every TextField construction but never resets. In a long-running SPA, this could theoretically overflow (after ~4 billion components). Consider using a WeakMap or UUID approach instead.

3. Autocomplete - Function Component in Class Component Codebase

src/diagram/components/Autocomplete.tsx:27

export default function Autocomplete(props: AutocompleteProps) {

This is the only function component using hooks in a codebase that explicitly prefers class components (CLAUDE.md: "Prefer class components — AVOID hooks like useState"). While Downshift's useCombobox requires hooks, this inconsistency is jarring. Consider:

  • Documenting why this exception is necessary
  • Or exploring if Downshift has a render-prop API that could work with class components

4. SpeedDial - Unsafe Type Assertions

src/diagram/components/SpeedDial.tsx:47,53

React.cloneElement(icon as React.ReactElement<SpeedDialIconProps>, { open })
React.cloneElement(child as React.ReactElement<SpeedDialActionProps>, {

Type assertions bypass TypeScript's safety. If icon or child aren't valid elements, this will fail at runtime. Use type guards instead:

if (!React.isValidElement<SpeedDialIconProps>(icon)) return icon;
return React.cloneElement(icon, { open });

5. Drawer - Focus Trap Selector Could Miss Elements

src/diagram/components/Drawer.tsx:53-54

const focusableElements = panel.querySelectorAll<HTMLElement>(
  'button, [href], input, select, textarea, [tabindex]:not([tabindex="-1"])',
);

This selector is incomplete. Missing:

  • a tags without href (common in React)
  • Elements with contenteditable
  • Custom elements with tabindex="0"

Consider using a library like tabbable or at minimum: 'a, button, [href], input, select, textarea, [tabindex]:not([tabindex="-1"]), [contenteditable]'

6. Snackbar Timer - Race Condition on Fast Open/Close

src/diagram/components/Snackbar.tsx:23-29

componentDidUpdate(prevProps: SnackbarProps) {
  if (this.props.open && !prevProps.open && this.props.autoHideDuration && this.props.onClose) {
    this.startTimer();
  }
  if (!this.props.open && prevProps.open) {
    this.clearTimer();
  }
}

If open toggles rapidly (false → true → false within the same render cycle), the timer might not be cleared properly. Consider consolidating the logic or using a guard.

7. Missing Error Boundaries

None of the portal-based components (Drawer, Snackbar, Autocomplete) are wrapped in error boundaries. If a portal throws during render, it could crash the entire app. Consider wrapping portals or documenting this limitation.

Moderate Issues

8. TextField - No Input Validation or Sanitization

The TextField forwards all props to the underlying <input> via rest spread. While flexible, this could inadvertently pass invalid HTML attributes. Consider a whitelist approach or document that consumers must pass valid input props.

9. Button - Missing Ripple Effect

MUI buttons have a visual ripple effect on click. Your custom Button lacks this. While not critical, it's a UX regression users might notice. Consider adding a simple CSS animation or document this as a known difference.

10. CSS Color Variables Assume Light Mode

src/diagram/components/TextField.module.css:68

background: #fff;

Hard-coded white backgrounds will break in dark mode. If dark mode support is planned, use CSS custom properties: background: var(--background-color, #fff);

11. Autocomplete Portal Positioning - No Viewport Overflow Handling

src/diagram/components/Autocomplete.tsx:76-82
The dropdown is positioned directly below the input, but there's no check for viewport overflow. If the input is near the bottom of the screen, the dropdown could be cut off or overflow the viewport. Consider flipping the dropdown above the input when needed.

12. Package Dependencies - Downshift Not in package.json

Looking at the diff, I don't see downshift added to src/diagram/package.json. It should be an explicit dependency since it's imported in Autocomplete. Verify it's not being relied upon via hoisting from another package.

Minor Issues

13. Inconsistent Naming: _onActionClick

src/diagram/components/SpeedDial.tsx:54,92-93
The underscore prefix suggests private/internal, but TypeScript has actual private modifiers. Use JSDoc @internal tag instead for better type safety.

14. Missing Display Names

Class components should have displayName set for better debugging. Example:

export default class Button extends React.PureComponent<ButtonProps> {
  static displayName = 'Button';

15. No PropTypes Runtime Validation

While TypeScript provides compile-time safety, runtime PropTypes would catch errors when components are used from JavaScript or with incorrect types. Not critical for a TypeScript-only project.

Performance Considerations

PureComponent usage: Proper use of React.PureComponent for most components
CSS Modules: Better performance than CSS-in-JS at runtime
⚠️ Portal Reflow: Every portal render triggers a reflow. Document components should call these sparingly
⚠️ Autocomplete Filtering: Filtered on every render with useMemo. For large lists (>1000 items), consider virtualizing

Security Concerns

No innerHTML or dangerouslySetInnerHTML: Safe from XSS
No eval or Function constructor: No code injection vectors
⚠️ Rest Spread Props: Components spread ...rest to DOM elements. Could accidentally pass non-DOM props causing React warnings, but not a security issue

Test Plan Status

From the PR description:

  • yarn tsc passes ✅
  • yarn lint passes ✅
  • yarn build succeeds ✅
  • Zero @mui or @emotion imports remain ✅
  • Zero MUI CSS selectors remain ✅
  • Visual inspection of all affected components ❌
  • Add unit tests for behavioral components ❌ BLOCKER

Recommendations

Before Merge:

  1. Add comprehensive unit tests (BLOCKER per CLAUDE.md TDD requirements)
  2. Fix TextField ID counter memory leak
  3. Add type guards for SpeedDial cloneElement calls
  4. Improve Drawer focus trap selector
  5. Verify Downshift is in package.json dependencies
  6. Add error handling for portal components

Post-Merge:

  1. Visual regression testing of all components
  2. Add ripple effect to buttons or document the difference
  3. Plan for dark mode support (CSS variable architecture)
  4. Consider virtualizing Autocomplete for large lists
  5. Add display names to all class components

Conclusion

This is solid foundational work that successfully removes MUI dependencies. The component implementations are generally well-structured with good attention to accessibility and lifecycle management. However, the complete lack of unit tests is a blocker per the project's TDD requirements in CLAUDE.md.

Once tests are added and the critical issues are addressed, this will be a strong improvement to the codebase by eliminating heavy external dependencies.

Recommendation: Request Changes - Add unit tests before merging.

Copy link

@chatgpt-codex-connector chatgpt-codex-connector bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: d3be20b51e

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment on lines +53 to +56
inputValue,
selectedItem: value || null,
onInputValueChange: ({ inputValue: newInputValue }) => {
setInputValue(newInputValue || '');

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Sync input value when value changes

In src/diagram/components/Autocomplete.tsx, inputValue is only updated from onInputValueChange, while selectedItem is controlled by the value prop. If a parent updates value programmatically (for example, selection changes outside the dropdown), the text field can keep the old inputValue and keep filtering against stale text because nothing re-syncs inputValue to value. Consider syncing inputValue in an effect when value changes or exposing a controlled inputValue prop so the displayed input stays consistent with the selected item.

Useful? React with 👍 / 👎.

Comment on lines 23 to 26
componentDidUpdate(prevProps: SnackbarProps) {
if (this.props.open && !prevProps.open && this.props.autoHideDuration && this.props.onClose) {
this.startTimer();
}

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Start auto-hide timer on initial open

In src/diagram/components/Snackbar.tsx, the auto-hide timer only starts in componentDidUpdate when open transitions from false to true. If the Snackbar mounts with open={true} (e.g., the screen loads with existing errors), the timer never starts and onClose will not fire, so autoHideDuration is effectively ignored. Starting the timer in componentDidMount when open is already true would align with expected Snackbar behavior.

Useful? React with 👍 / 👎.

Fix several issues identified in the PR review:

- Autocomplete: Add useEffect to sync inputValue when value prop changes
  externally. Previously, if a parent updated the value programmatically,
  the text field would show stale text.

- Snackbar: Start auto-hide timer in componentDidMount when open={true},
  not just on transitions. Also simplify update logic to always clear
  timer first, preventing race conditions on rapid open/close cycles.

- SpeedDial: Replace unsafe type assertions with proper type guards using
  React.isValidElement<T>() generic parameter instead of casting after.

- Drawer: Expand focus trap selector to include 'a' and '[contenteditable]'
  elements that were previously missed.

Add comprehensive tests for these components covering:
- Autocomplete: External value sync, filtering, clearOnEscape
- Snackbar: Timer behavior on mount, transitions, rapid toggles
- Drawer: Focus management, focus trap with various element types
- SpeedDial: Rendering, interactions, icon transitions
@bpowers
Copy link
Owner Author

bpowers commented Jan 29, 2026

@codex review

Copy link

@chatgpt-codex-connector chatgpt-codex-connector bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: a6509d4f07

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment on lines 30 to 36
componentDidUpdate(_prevProps: SnackbarProps) {
// Always clear first to prevent race conditions on rapid open/close
this.clearTimer();

// Start new timer if now open
if (this.props.open) {
this.startTimer();

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Only restart auto-hide when open/duration changes

Right now componentDidUpdate clears and restarts the timer on every update, even when open stays true and only unrelated parent state changes. In the editor, frequent re-renders (selection updates, mouse moves, etc.) will keep resetting the timeout so the snackbar never auto-dismisses despite autoHideDuration being set. MUI’s Snackbar only restarts on open transitions or duration changes; this version should similarly gate the timer reset on open/autoHideDuration changes to avoid an effectively infinite timeout.

Useful? React with 👍 / 👎.

The previous implementation reset the auto-hide timer on every
componentDidUpdate call, which meant unrelated parent re-renders would
continuously reset the countdown, effectively preventing the snackbar
from ever auto-dismissing in practice.

Now the timer only restarts when `open` or `autoHideDuration` actually
change. Added a test that verifies unrelated re-renders don't reset the
timer, ensuring the snackbar dismisses after the configured duration
regardless of how often the parent re-renders.
@bpowers
Copy link
Owner Author

bpowers commented Jan 29, 2026

@codex review

Copy link

@chatgpt-codex-connector chatgpt-codex-connector bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: c4f51159c7

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment on lines 112 to 122
<input
id={inputId}
className={styles.standardInput}
value={value}
onChange={onChange}
type={type}
placeholder={placeholder}
onFocus={this.handleFocus}
onBlur={this.handleBlur}
{...inputProps}
{...rest}

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P3 Badge Preserve TextField focus tracking when inputProps adds handlers

In TextField, inputProps is spread after the component’s own onFocus/onBlur handlers. When inputProps contains these handlers (e.g., Downshift’s getInputProps used by Autocomplete), it overwrites the internal handlers so isFocused never updates. This prevents the floating label and focus/error styles from activating while the field is focused, which is visible any time TextField is used via Autocomplete with a label or outlined variant. Consider chaining inputProps.onFocus/onBlur with handleFocus/handleBlur instead of letting them override.

Useful? React with 👍 / 👎.

When TextField is used with inputProps that include onFocus/onBlur
handlers (e.g., from Downshift's getInputProps in Autocomplete), those
handlers were overwriting the internal handlers that track isFocused
state. This meant the floating label and focus styles wouldn't work
properly when used via Autocomplete.

Now the internal handlers call any external handlers from inputProps
after updating internal state, ensuring both the component's focus
tracking and any external handlers work correctly together.

Added comprehensive tests for TextField including handler chaining.
@bpowers
Copy link
Owner Author

bpowers commented Jan 29, 2026

@codex review

@chatgpt-codex-connector
Copy link

Codex Review: Didn't find any major issues. Chef's kiss.

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

@bobby-stripe bobby-stripe merged commit d88a1bb into main Jan 29, 2026
8 checks passed
@bobby-stripe bobby-stripe deleted the remove-mui-from-diagram branch January 29, 2026 16:59
@claude
Copy link

claude bot commented Jan 29, 2026

PR Review: Remove MUI and Emotion dependencies

Summary

Excellent refactoring with comprehensive test coverage and strong accessibility. Code quality is high with only minor issues identified.

Strengths

1. Excellent Test Coverage

  • Snackbar (314 lines): Thorough timer behavior testing including edge cases
  • Drawer (299 lines): Complete focus management and focus trap testing
  • SpeedDial (340 lines): Comprehensive rendering and interaction tests
  • TextField (125 lines): Handler chaining verification
  • Autocomplete (159 lines): External value sync testing

2. Strong Accessibility

  • TextField: Proper id/htmlFor with auto-generated IDs
  • Drawer: role="dialog", aria-modal, focus trap, focus restoration
  • SpeedDial: Proper ARIA labels and aria-expanded

3. Good Event Handler Design

  • TextField chains external handlers correctly
  • SpeedDial uses cloneElement appropriately
  • Snackbar manages timer lifecycle efficiently

Issues Found

1. TextField Handler Extraction (Priority: High)

File: src/diagram/components/TextField.tsx:82

Line 82 extracts handlers into unused variables (_onFocus, _onBlur), but lines 126-127 and 167-168 reference this.props.inputProps directly.

Fix: Use the extracted values consistently:

const { onFocus: externalOnFocus, onBlur: externalOnBlur, ...restInputProps } = inputProps || {};

handleFocus = (event) => {
  this.setState({ isFocused: true });
  externalOnFocus?.(event);
};

2. Drawer Event Listener (Priority: Medium)

File: src/diagram/components/Drawer.tsx:24

Attaches global keydown listener in componentDidMount regardless of open state. Consider only adding when open=true and managing in componentDidUpdate for better performance.

3. Duplicated CSS Shadows (Priority: Low)

File: src/diagram/Editor.module.css:22, 68

MUI elevation shadows duplicated. Consider CSS custom properties for maintainability.

Performance & Security

  • ✅ Appropriate ReactDOM.createPortal usage
  • ✅ Good PureComponent usage
  • ✅ Proper memoization in Autocomplete
  • ✅ No security issues
  • 🎉 Bundle size savings: ~100-200KB

Verdict

APPROVE with minor changes requested

High-quality work demonstrating excellent engineering practices. Issues identified are minor and don't block merging, though fixing the TextField handler extraction improves code clarity.

Great migration work! 🚀

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants