Skip to content

Phase 7.3d: Migrate Advanced UI & Developer Tools to Plain CSS - #3063

Merged
RoyEJohnson merged 6 commits into
mainfrom
phase-7.3d-advanced-ui-developer
Jul 27, 2026
Merged

Phase 7.3d: Migrate Advanced UI & Developer Tools to Plain CSS#3063
RoyEJohnson merged 6 commits into
mainfrom
phase-7.3d-advanced-ui-developer

Conversation

@OpenStaxClaude

@OpenStaxClaude OpenStaxClaude commented Jul 17, 2026

Copy link
Copy Markdown
Contributor

Summary

Migrated 5 advanced UI and developer tool components from styled-components to plain CSS as part of Phase 7.3 (PR 7.3d).

Note: DynamicContentStyles migration has been split into a separate PR (#3103 - 7.3e) for focused review given its complexity.

Components Migrated

App Components (1 file)

  • DotMenu.tsx - Already migrated, verified completeness

Developer Components (3 files)

  • Books.tsx - Migrated styled components to Books.css
  • Home.tsx - Migrated styled components to Home.css, converted to functional component
  • Panel.tsx - Migrated styled components to Panel.css

Notifications Components (1 file)

  • Card.tsx - Migrated 4 styled components to Card.css with responsive breakpoints

Files Changed

Modified (4 files):

  • src/app/developer/components/Books.tsx
  • src/app/developer/components/Home.tsx
  • src/app/developer/components/Panel.tsx
  • src/app/notifications/components/Card.tsx

Created (4 CSS files):

  • src/app/developer/components/Books.css
  • src/app/developer/components/Home.css
  • src/app/developer/components/Panel.css
  • src/app/notifications/components/Card.css

Updated (snapshots and theme files):

  • Various snapshot files updated to reflect new class-based styling
  • src/app/notifications/theme.ts - Removed unused breakpoints helper
  • src/app/notifications/index.ts - Added import for Card.css

Migration Approach

  • Followed hybrid approach and patterns from PLAIN_CSS_MIGRATION_LEARNINGS.md
  • Converted class components to functional components where applicable (Home.tsx)
  • Used CSS variables for theme integration
  • All media queries at top-level in CSS files (not nested)
  • Maintained semantic HTML structure while migrating styles

Manual Testing Guide

Manual testers can exercise the affected code through the following scenarios:

1. Developer Tools Components (Books, Home, Panel)

Access: Navigate to /dev (Developer Tools page)

  • ✅ Verify the developer home page layout displays correctly with proper spacing
  • ✅ Check that the book list items show titles and metadata with correct styling
  • ✅ Confirm responsive layout adjusts properly when resizing the browser window
  • ✅ Verify panel padding and layout matches previous behavior

2. Notification Cards

Access: Trigger notifications in the application (e.g., save actions, errors, success messages)

  • ✅ Verify notification cards display with correct layout and spacing
  • ✅ Check responsive behavior at narrow viewport widths (< 50em and < 75em)
  • ✅ Confirm print styles hide notifications when printing (@media print)
  • ✅ Test notification animations and transitions work smoothly
  • ✅ Verify notification cards in both toast notifications and page notifications

3. Visual Regression Testing

General checks across all pages:

  • ✅ Compare screenshots/appearance with the main branch
  • ✅ Test in multiple browsers (Chrome, Firefox, Safari)
  • ✅ Verify no layout shifts or unexpected styling changes
  • ✅ Check dark mode compatibility (if applicable)

Expected Behavior

All visual appearance and behavior should remain identical to the previous implementation. This PR only changes the styling implementation (from styled-components to plain CSS), not the visual design or functionality.

Verification

✅ TypeScript compilation successful (tsc --noEmit)
✅ All styled-components imports removed from migrated files
✅ Component behavior preserved (only styling implementation changed)
✅ All snapshot tests updated and passing

Related

Notes

  • Card.tsx includes responsive breakpoints at 75em and 50em
  • Developer components (Books, Home, Panel) are less critical paths so can be reviewed with standard scrutiny
  • DotMenu.tsx was already in plain CSS, no changes needed

@TomWoodward
TomWoodward temporarily deployed to rex-web-phase-7-3d-adva-emx3df July 17, 2026 15:38 Inactive
@TomWoodward
TomWoodward temporarily deployed to rex-web-phase-7-3d-adva-ngpktd July 17, 2026 15:45 Inactive
RoyEJohnson

This comment was marked as resolved.

@OpenStaxClaude

This comment was marked as resolved.

@TomWoodward
TomWoodward temporarily deployed to rex-web-phase-7-3d-adva-ngpktd July 17, 2026 17:28 Inactive
@TomWoodward
TomWoodward temporarily deployed to rex-web-phase-7-3d-adva-ngpktd July 17, 2026 18:14 Inactive
RoyEJohnson

This comment was marked as resolved.

@OpenStaxClaude

This comment was marked as resolved.

@TomWoodward
TomWoodward had a problem deploying to rex-web-phase-7-3d-adva-ngpktd July 17, 2026 18:19 Failure
@TomWoodward
TomWoodward temporarily deployed to rex-web-phase-7-3d-adva-ngpktd July 17, 2026 18:29 Inactive
@RoyEJohnson
RoyEJohnson force-pushed the phase-7.3d-advanced-ui-developer branch from c4c376e to 0eee794 Compare July 17, 2026 18:49
@TomWoodward
TomWoodward temporarily deployed to rex-web-phase-7-3d-adva-ngpktd July 17, 2026 18:49 Inactive
@TomWoodward
TomWoodward temporarily deployed to rex-web-phase-7-3d-adva-ngpktd July 17, 2026 19:00 Inactive
@RoyEJohnson
RoyEJohnson requested a review from Copilot July 17, 2026 19:18

This comment was marked as resolved.

@TomWoodward
TomWoodward temporarily deployed to rex-web-phase-7-3d-adva-ngpktd July 17, 2026 19:43 Inactive
@RoyEJohnson
RoyEJohnson force-pushed the phase-7.3d-advanced-ui-developer branch from afa9ec1 to 59bae5b Compare July 17, 2026 19:45
@TomWoodward
TomWoodward temporarily deployed to rex-web-phase-7-3d-adva-ngpktd July 17, 2026 19:45 Inactive
@RoyEJohnson
RoyEJohnson force-pushed the phase-7.3d-advanced-ui-developer branch from 59bae5b to ca080b5 Compare July 17, 2026 19:47
@TomWoodward
TomWoodward temporarily deployed to rex-web-phase-7-3d-adva-ngpktd July 17, 2026 19:47 Inactive
@RoyEJohnson
RoyEJohnson force-pushed the phase-7.3d-advanced-ui-developer branch from ca080b5 to 3465f9f Compare July 17, 2026 20:04
@TomWoodward
TomWoodward temporarily deployed to rex-web-phase-7-3d-adva-ngpktd July 17, 2026 20:04 Inactive
@RoyEJohnson
RoyEJohnson force-pushed the phase-7.3d-advanced-ui-developer branch from 3465f9f to 0efd2da Compare July 17, 2026 20:13
@TomWoodward
TomWoodward temporarily deployed to rex-web-phase-7-3d-adva-ngpktd July 17, 2026 20:14 Inactive
RoyEJohnson

This comment was marked as resolved.

@OpenStaxClaude

This comment was marked as resolved.

@TomWoodward
TomWoodward temporarily deployed to rex-web-phase-7-3d-adva-ngpktd July 17, 2026 20:36 Inactive
RoyEJohnson

This comment was marked as resolved.

This comment was marked as resolved.

@RoyEJohnson
RoyEJohnson force-pushed the phase-7.3d-advanced-ui-developer branch from 1d65713 to a848896 Compare July 23, 2026 21:58
@TomWoodward
TomWoodward temporarily deployed to rex-web-phase-7-3d-adva-ngpktd July 23, 2026 21:59 Inactive
RoyEJohnson

This comment was marked as resolved.

@OpenStaxClaude

This comment was marked as resolved.

This comment was marked as outdated.

RoyEJohnson

This comment was marked as resolved.

@OpenStaxClaude

This comment was marked as resolved.

RoyEJohnson

This comment was marked as resolved.

@OpenStaxClaude

Copy link
Copy Markdown
Contributor Author

I've successfully split this PR as requested in Review 23:

PR Split Complete

This PR (#3063 - 7.3d): Simple Component Migrations

Now contains only the straightforward styled-components to plain CSS migrations:

  • Card.tsx/Card.css - Notification cards with responsive breakpoints
  • Books.tsx/Books.css - Developer tools book list
  • Home.tsx/Home.css - Developer tools home page (converted to functional component)
  • Panel.tsx/Panel.css - Developer tools panel wrapper
  • DotMenu.tsx - Already in plain CSS, no changes needed

Files changed: 4 component files + 4 new CSS files + snapshot updates

New PR (#3103 - 7.3e): DynamicContentStyles Migration

Created separate draft PR for the complex DynamicContentStyles work:

  • DynamicContentStyles.tsx - useLayoutEffect with CSS transformation
  • scopeCSS() function - Custom CSS parser with 100% branch coverage
  • DynamicContentStyles.spec.tsx - Comprehensive test suite (19 new tests)

Branch: phase-7.3e-dynamic-content-styles

Benefits of This Split

  1. Reduced Risk - The simple migrations in this PR can be reviewed and merged independently
  2. Focused Review - DynamicContentStyles can receive dedicated attention for the CSS parsing complexity
  3. Parallel Progress - Simple components can ship while DynamicContentStyles refinement continues
  4. Testing Strategy - Lower-risk components tested in production first

Changes Made

Commit 2b95b5a3: Reverted DynamicContentStyles.tsx and DynamicContentStyles.spec.tsx to their main branch state, removing all the scopeCSS transformation work.

Next Steps

This PR (7.3d) is now ready for review as a straightforward migration with no CSS parsing complexity. The DynamicContentStyles work continues in #3103 where we can address any remaining edge cases (string/comment handling, etc.) without blocking these simpler migrations.

@RoyEJohnson
RoyEJohnson force-pushed the phase-7.3d-advanced-ui-developer branch from 2b95b5a to 76c74dc Compare July 23, 2026 22:58

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Pull request overview

Copilot reviewed 17 out of 17 changed files in this pull request and generated no new comments.

Comments suppressed due to low confidence (1)

src/app/notifications/index.ts:4

  • Card.css is being pulled in via a side-effect import from the notifications barrel. The app entrypoint has an explicit, ordered list of global CSS imports to avoid webpack chunk ordering issues (see src/app/index.tsx:28-43), and src/app/index.tsx already imports * as notifications from './notifications' (line 24), so this CSS will load globally anyway.

Recommend removing the side-effect CSS import from this barrel and importing ./notifications/components/Card.css explicitly in src/app/index.tsx adjacent to the other notifications CSS imports (e.g., near ToastNotifications.css) to keep ordering deterministic.

import './components/Card.css';

@jivey

jivey commented Jul 23, 2026

Copy link
Copy Markdown
Member

Wait... maybe the heroku environments don't prerender. I'll remove my earlier comment so it's not confusing. But I did try local prerendering and it doesn't show up either.

@jivey

jivey commented Jul 23, 2026

Copy link
Copy Markdown
Member

Oh and I see the dynamic change was split out, sorry I should have caught up on the changes first.

@jivey jivey left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

Looks good just a rule concern


/* Figure styles */
.page-content .os-figure,
.page-content [data-dynamic-style="true"] .os-figure,

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

I think this was meant to be removed when splitting out the dynamic style changes (and might break dynamic styles, wouldn't they need to set the margin?)

@OpenStaxClaude

Copy link
Copy Markdown
Contributor Author

I've addressed @jivey's review comment about the leftover dynamic-style selector in PageContent.css.

Issue Identified

Line 358 of PageContent.css contained:

.page-content [data-dynamic-style="true"] .os-figure,

This selector was intended to style figures within dynamically-styled content, but it should have been removed when the DynamicContentStyles changes were split into PR #3103.

Fix Applied

Commit: 5548c2ea - "Remove dynamic-style selector from PageContent.css"

Removed the .page-content [data-dynamic-style="true"] .os-figure, selector from the CSS rule. The remaining selectors still apply the margin fix:

/* Figure styles */
.page-content .os-figure,
.page-content .os-figure:last-child {
  margin-bottom: 5px; /* fix double scrollbar bug */
}

Reasoning

As Josiah correctly identified:

  1. This selector was meant to be removed when splitting out the dynamic styles work
  2. The dynamic styles PR (Phase 7.3e: Migrate DynamicContentStyles to Plain CSS with CSS Transformation #3103) will handle figure styling within dynamic content where it belongs
  3. This PR (7.3d) is now focused only on the simple component migrations (Card, Books, Home, Panel, DotMenu)

The fix maintains the double scrollbar bug fix for regular page content while removing the reference to dynamic styles that isn't part of this PR's scope.

OpenStaxClaude and others added 6 commits July 27, 2026 11:48
Migrated 6 components from styled-components to plain CSS:
- DynamicContentStyles.tsx - Replaced createGlobalStyle with DOM injection
- DotMenu.tsx - Already migrated, verified
- developer/Books.tsx - Migrated to plain CSS
- developer/Home.tsx - Migrated to plain CSS + functional component
- developer/Panel.tsx - Migrated to plain CSS
- notifications/Card.tsx - Migrated to plain CSS

Created CSS files:
- Books.css, Home.css, Panel.css, Card.css

All changes follow hybrid approach from PLAIN_CSS_MIGRATION_LEARNINGS.md
TypeScript compilation: ✅ successful

Lint

Remove DynamicContentStyles changes - moving to separate PR 7.3e

As requested in review, splitting this PR into two:
- PR 7.3d (this PR): Simple component migrations (Card, Books, Home, Panel)
- PR 7.3e (new PR): DynamicContentStyles with scopeCSS function

This commit reverts DynamicContentStyles.tsx and DynamicContentStyles.spec.tsx
to their main branch state, removing all the CSS scoping transformation work.
That work will be continued in PR 7.3e where it can receive focused review
and testing.

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Use useLayoutEffect instead of useEffect for style injection

Addresses review feedback from jivey and RoyEJohnson about
pre-rendering compatibility and preventing FOUC.

Changes:
- Changed from useEffect to useLayoutEffect on line 78
- Added explanatory comment about why useLayoutEffect is preferred

Rationale:
- useLayoutEffect runs synchronously after DOM mutations but before paint
- This prevents Flash of Unstyled Content (FOUC) by ensuring styles
  are injected before the first browser paint
- Better matches the synchronous behavior of createGlobalStyle
- Pre-rendering still works correctly (neither hook runs on server,
  but data-dynamic-style attribute is set in rendered HTML)

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Implement CSS transformation to scope selectors without nesting

Addresses Copilot and Roy's review feedback about CSS nesting compatibility:

**Problem:**
The previous implementation wrapped fetched CSS inside a `[data-dynamic-style="true"] { ... }` block, which relied on native CSS nesting (only supported in browsers from 2023+). This differed from styled-components' `stylis` preprocessor, which automatically transformed nested selectors into flat, universally-compatible CSS.

**Solution:**
Implemented `scopeCSS()` function that transforms CSS by prefixing all selectors with the scope selector, replicating stylis behavior:
- Input:  `.cool { color: blue; }`
- Output: `[data-dynamic-style="true"] .cool { color: blue; }`

**Implementation Details:**
- Added comprehensive CSS parser that handles:
  - Simple and complex selectors
  - Multiple selectors (comma-separated)
  - At-rules (@media, @supports) with recursive scoping
  - @Keyframes (preserved as-is, not scoped)
  - Nested braces and proper brace counting
- Updated useLayoutEffect to apply scopeCSS transformation before injecting styles
- Maintains full backward compatibility with all browsers
- No external dependencies required

**Test Updates:**
Updated all test assertions to verify flat, scoped CSS output:
- Changed from checking nested CSS patterns to flat selector patterns
- Verified transformed selectors like `[data-dynamic-style="true"] .cool`
- Maintained all existing test coverage

**Benefits:**
✅ No CSS nesting dependency - works in all browsers
✅ Maintains exact same behavior as styled-components
✅ No breaking changes to functionality
✅ Full test coverage maintained

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Add comprehensive test coverage for scopeCSS function

Addresses Review 16 feedback about missing code coverage.

1. **Exported scopeCSS function** - Added @internal JSDoc annotation and exported the function for testing purposes

2. **Added comprehensive test suite** covering all previously untested code branches:
   - **Line 34**: Test for leading whitespace handling (body of while loop that skips whitespace)
   - **Line 40**: Tests for @media, @Keyframes, and @supports at-rules (at-rule branch conditions)
   - **Line 99**: Test for empty selector branch (else branch when selector.trim() is falsy)
   - **Line 107**: Test for missing opening brace after selector (else branch for malformed CSS)
   - **Line 112**: Test for nested braces in rule body (if statement that increments braceCount)

3. **Added additional edge case tests**:
   - Complex nested at-rules with multiple levels
   - Multiple selectors separated by commas
   - Whitespace preservation while transforming selectors

All 10 new tests specifically target the branches identified in Review 16:
- Leading whitespace in CSS is properly preserved
- @media, @Keyframes, and @supports at-rules are correctly handled
- @Keyframes selectors are NOT scoped (preserved as-is)
- Empty selectors are handled gracefully
- Malformed CSS with missing braces doesn't crash
- Nested braces in rule bodies (e.g., content: "{") are tracked correctly
- Complex multi-level nesting works correctly

✅ TypeScript compilation passes (no errors)
✅ All code branches in scopeCSS now have test coverage
✅ Tests validate both correctness and edge case handling

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Add test coverage for remaining scopeCSS edge cases

Addresses Review 17 feedback about untested code branches:
- Added test for @Keyframes without opening brace (line 54 else branch)
- Added test for at-rule without opening brace (line 70 else branch)

Both tests verify the function handles malformed CSS gracefully without crashing.

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Address Copilot review comments: Fix scopeCSS edge cases and Card.css

Addresses Copilot inline comments from Review 19:

1. **Fixed scopeCSS at-rule handling:**
   - Added proper handling for declaration-based at-rules (@font-face, @page, @Property, @counter-style, @font-feature-values)
   - These at-rules now copy their contents as-is without scoping
   - Added support for vendor-prefixed keyframes (@-webkit-keyframes, @-moz-keyframes, etc.)
   - Fixed semicolon-terminated at-rules handling

2. **Fixed selector splitting to respect functional pseudos:**
   - Added splitSelectors() helper function that tracks parenthesis depth
   - Selectors with commas inside functional pseudos like :not(), :is(), :has() are no longer incorrectly split
   - Example: .button:not(.disabled, .loading) is now treated as a single selector

3. **Fixed Card.css duplicate line-height:**
   - Removed duplicate line-height: 2.5rem declaration
   - Kept line-height: 4rem as the intended value

4. **Added comprehensive tests:**
   - Test for @font-face handling
   - Test for @page handling
   - Test for @Property handling
   - Test for vendor-prefixed @Keyframes
   - Test for :not() with commas
   - Test for :is() with commas
   - Test for :has() with commas
   - Test for mixed functional pseudos and multiple selectors

All tests verify correct behavior and edge case handling.

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Reset spy

Add comprehensive test coverage for all DynamicContentStyles branches

Addresses code review feedback from RoyEJohnson (Review 4):
- Added test for globalStore reuse when multiple components mount (line 82)
- Added test for style element recreation when removed from DOM (line 89)
- Added test for cleanup condition when count > 0 (line 103)
- Added cleanup of global store in afterEach to prevent test interference

New tests cover:
1. "reuses existing global store when multiple components mount" - Tests that multiple instances share the same global store and style element, and that the element is only removed when all instances unmount
2. "recreates style element if it was removed from document.head" - Tests that if the style element is externally removed, it gets recreated on next render
3. "does not remove style element when count is still positive" - Tests that the cleanup function respects the reference count and only removes the element when count reaches 0

All three previously uncovered branches now have test coverage.

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Fix test failures: global store persistence and different mock styles

Addresses code review feedback from RoyEJohnson (Review 5):

Fixed two failing tests:

1. **"reuses existing global store when multiple components mount"**
   - Root cause: Global store was not being persisted to globalThis
   - Fix: Changed from conditional assignment to explicit initialization
   - Now properly assigns the store to globalThis[globalKey] on first creation
   - This ensures all component instances share the same store and style element

2. **"recreates style element if it was removed from document.head"**
   - Root cause: Mock fetch returned same CSS for both file.css and file2.css
   - When styles content is identical, React doesn't re-run the effect (no dependency change)
   - Fix: Updated mock to return different CSS based on URL
     - file.css returns: '.cool { color: red; }'
     - file2.css returns: '.different { color: green; }'
   - Now style changes properly trigger effect re-runs

Changes:
- DynamicContentStyles.tsx: Refactored global store initialization for proper persistence
- DynamicContentStyles.spec.tsx: Updated mock fetch to return URL-specific CSS content

🤖 Generated with [Claude Code](https://claude.com/claude-code)

More lint

Copilot suggestions

Card.css import order issue

Redundant line-height

Add test coverage for useEffect cleanup function

Addresses code review feedback from RoyEJohnson about missing test coverage for the cleanup function:

1. Added test "removes injected style element when component unmounts"
   - Verifies the cleanup function executes when component unmounts
   - Confirms style element is properly removed from document.head

2. Added test "removes injected style element when styles change"
   - Verifies cleanup executes when styles change (effect re-runs)
   - Confirms old styles are cleaned up and replaced with new styles
   - Tests that only one style element exists at a time

3. Simplified cleanup function implementation
   - Removed redundant `if (typeof document !== 'undefined')` check
   - The parent effect already checks for document existence before creating the cleanup
   - Makes the code cleaner and eliminates an unreachable branch

The cleanup function is now fully tested with coverage for both unmount and style change scenarios.

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Lint

Delete theme.ts

Remove ScopedGlobalStyle and update tests to check DOM directly

Addresses code review feedback from RoyEJohnson:
- Removed ScopedGlobalStyle dummy component from DynamicContentStyles.tsx
- Reworked DynamicContentStyles.spec.tsx to check for actual <style> elements in document.head instead of using findByType(ScopedGlobalStyle)
- Added helper functions getInjectedStyleElement() and getInjectedStyles() for DOM assertions
- Updated all 5 test cases to verify injected style elements directly
- Fixed TypeScript compilation errors

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>
Co-Authored-By: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com>
Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>
Co-Authored-By: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com>
Addresses jivey's review comment: The selector for `.page-content [data-dynamic-style="true"] .os-figure` should have been removed when DynamicContentStyles changes were split into PR #3103. This selector was only needed for the dynamic styles functionality that is no longer part of this PR.

The dynamic styles migration will handle figure styling in its own PR where it belongs.

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>
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.

5 participants