Skip to content

Conversation

@pzanella
Copy link
Contributor

Introduce shared UI components folder

This pull request creates the folder directory to organize common UI components. Centralizing shared elements improves code reuse, maintainability, and consistency across the user interface.

@pzanella pzanella marked this pull request as ready for review December 20, 2025 08:39
@coderabbitai
Copy link
Contributor

coderabbitai bot commented Dec 20, 2025

Walkthrough

Adds a shared Button component (with exported ButtonProps) and tests, a lightweight icon type system (Icon, IconSet, SVGIcons), and a useConstructableStyle utility. Introduces shared CSS: variables, flex utilities, and Button stylesheet. Replaces many native elements with the new Button and swaps emoji labels for inline SVG icons (SVGs marked aria-hidden where applied). Watch UI context now exposes isFullscreen and toggleFullscreen. Two devDependencies were added to package.json. Removes legacy .publishButton and .watchControlButton CSS rules.

Pre-merge checks and finishing touches

❌ Failed checks (1 warning)
Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 21.43% which is insufficient. The required threshold is 80.00%. You can run @coderabbitai generate docstrings to improve docstring coverage.
✅ Passed checks (2 passed)
Check name Status Explanation
Title check ✅ Passed The title accurately describes the main change: adding a shared Button component as part of a broader shared UI components refactoring.
Description check ✅ Passed The description is related to the changeset, describing the creation of a shared components folder to organize common UI elements and improve code reuse and maintainability.
✨ Finishing touches
  • 📝 Generate docstrings
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Post copyable unit tests in a comment

📜 Recent review details

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between d2ed9c4 and 15d5fd4.

📒 Files selected for processing (2)
  • js/hang-ui/src/Components/shared/components/button/index.tsx
  • js/hang-ui/src/Components/shared/styles/flex.css
🚧 Files skipped from review as they are similar to previous changes (2)
  • js/hang-ui/src/Components/shared/components/button/index.tsx
  • js/hang-ui/src/Components/shared/styles/flex.css
⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (1)
  • GitHub Check: Check

Comment @coderabbitai help to get the list of available commands and usage tips.

Copy link
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 4

🧹 Nitpick comments (4)
js/hang-ui/src/Components/shared/styles/utils.ts (1)

10-16: Consider safer type handling for the root node.

The type assertion on line 12 assumes getRootNode() returns either ShadowRoot or Document, but getRootNode() can also return the element itself in some edge cases. While the "adoptedStyleSheets" in root check provides a safety net, consider using a type guard for more explicit handling:

export function useConstructableStyle(ref: HTMLElement | undefined, sheet: CSSStyleSheet): void {
	if (!ref) return;
	const root = ref.getRootNode();
	if ("adoptedStyleSheets" in root && !root.adoptedStyleSheets.includes(sheet)) {
		root.adoptedStyleSheets = [...root.adoptedStyleSheets, sheet];
	}
}

This avoids the type assertion entirely while maintaining the same behavior.

js/hang-ui/src/Components/shared/styles/flex.css (1)

1-5: Consider clarifying class semantics.

The .flex class applies both display: flex and centering properties. This might be unexpected for developers who associate .flex with just display: flex (without automatic centering). Consider either:

  1. Renaming .flex to .flex-center for clarity
  2. Separating concerns: .flex { display: flex; } and .center { justify-content: center; align-items: center; }

The current implementation works but may lead to confusion if other flex layouts (flex-start, space-between, etc.) are needed later.

js/hang-ui/src/Components/shared/types/icons.ts (1)

3-3: Verify usage of the SVGIcons type.

The SVGIcons type is defined but doesn't appear to be used in the reviewed components. Components use either Icon (for single icons) or IconSet (for icon collections). Consider removing this type if it's unused or document its intended use case.

Run the following script to check if SVGIcons is used anywhere:

#!/bin/bash
# Description: Search for usage of SVGIcons type in the codebase

# Search for SVGIcons type usage (excluding the definition itself)
rg -n --type=ts --type=tsx 'SVGIcons' | rg -v 'export type SVGIcons'
js/hang-ui/src/Components/watch/FullscreenButton.tsx (1)

54-56: Consider making the button title dynamic to match the icon state.

The button title is currently static ("Fullscreen"), but the icon changes based on fullscreen state. For better accessibility and UX, the title should reflect the action: "Enter Fullscreen" or "Exit Fullscreen".

🔎 Proposed improvement for dynamic title
-		<Button title="Fullscreen" onClick={onClick}>
+		<Button title={context.isFullscreen() ? "Exit Fullscreen" : "Enter Fullscreen"} onClick={onClick}>
 			{context.isFullscreen() ? fullscreenIcons.exit() : fullscreenIcons.enter()}
 		</Button>
📜 Review details

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between dc0b358 and c5be3c3.

⛔ Files ignored due to path filters (2)
  • bun.lock is excluded by !**/*.lock
  • deno.lock is excluded by !**/*.lock
📒 Files selected for processing (23)
  • js/hang-ui/package.json (1 hunks)
  • js/hang-ui/src/Components/publish/CameraSourceButton.tsx (2 hunks)
  • js/hang-ui/src/Components/publish/FileSourceButton.tsx (2 hunks)
  • js/hang-ui/src/Components/publish/MediaSourceSelector.tsx (2 hunks)
  • js/hang-ui/src/Components/publish/MicrophoneSourceButton.tsx (2 hunks)
  • js/hang-ui/src/Components/publish/NothingSourceButton.tsx (2 hunks)
  • js/hang-ui/src/Components/publish/ScreenSourceButton.tsx (2 hunks)
  • js/hang-ui/src/Components/publish/styles.css (0 hunks)
  • js/hang-ui/src/Components/shared/__tests__/components/index.test.tsx (1 hunks)
  • js/hang-ui/src/Components/shared/components/button/index.tsx (1 hunks)
  • js/hang-ui/src/Components/shared/components/button/style.css (1 hunks)
  • js/hang-ui/src/Components/shared/styles/flex.css (1 hunks)
  • js/hang-ui/src/Components/shared/styles/utils.ts (1 hunks)
  • js/hang-ui/src/Components/shared/styles/variables.css (1 hunks)
  • js/hang-ui/src/Components/shared/types/icons.ts (1 hunks)
  • js/hang-ui/src/Components/stats/__tests__/components/StatsItem.test.tsx (10 hunks)
  • js/hang-ui/src/Components/stats/components/StatsPanel.tsx (4 hunks)
  • js/hang-ui/src/Components/watch/FullscreenButton.tsx (1 hunks)
  • js/hang-ui/src/Components/watch/PlayPauseButton.tsx (1 hunks)
  • js/hang-ui/src/Components/watch/StatsButton.tsx (2 hunks)
  • js/hang-ui/src/Components/watch/VolumeSlider.tsx (2 hunks)
  • js/hang-ui/src/Components/watch/WatchUIContextProvider.tsx (3 hunks)
  • js/hang-ui/src/Components/watch/styles.css (0 hunks)
💤 Files with no reviewable changes (2)
  • js/hang-ui/src/Components/watch/styles.css
  • js/hang-ui/src/Components/publish/styles.css
🧰 Additional context used
🧬 Code graph analysis (12)
js/hang-ui/src/Components/watch/VolumeSlider.tsx (2)
js/hang-ui/src/Components/shared/types/icons.ts (1)
  • IconSet (6-6)
js/hang-ui/src/Components/shared/components/button/index.tsx (1)
  • Button (44-68)
js/hang-ui/src/Components/watch/FullscreenButton.tsx (3)
js/hang-ui/src/Components/shared/types/icons.ts (1)
  • IconSet (6-6)
js/hang-ui/src/Components/watch/useWatchUIContext.ts (1)
  • useWatchUIContext (4-12)
js/hang-ui/src/Components/shared/components/button/index.tsx (1)
  • Button (44-68)
js/hang-ui/src/Components/publish/MediaSourceSelector.tsx (1)
js/hang-ui/src/Components/shared/components/button/index.tsx (1)
  • Button (44-68)
js/hang-ui/src/Components/watch/PlayPauseButton.tsx (3)
js/hang-ui/src/Components/shared/types/icons.ts (1)
  • IconSet (6-6)
js/hang-ui/src/Components/watch/useWatchUIContext.ts (1)
  • useWatchUIContext (4-12)
js/hang-ui/src/Components/shared/components/button/index.tsx (1)
  • Button (44-68)
js/hang-ui/src/Components/shared/components/button/index.tsx (2)
js/signals/src/dom.ts (1)
  • Element (65-65)
js/hang-ui/src/Components/shared/styles/utils.ts (1)
  • useConstructableStyle (10-16)
js/hang-ui/src/Components/watch/StatsButton.tsx (2)
js/hang-ui/src/Components/shared/types/icons.ts (1)
  • Icon (7-7)
js/hang-ui/src/Components/shared/components/button/index.tsx (1)
  • Button (44-68)
js/hang-ui/src/Components/publish/NothingSourceButton.tsx (2)
js/hang-ui/src/Components/shared/types/icons.ts (1)
  • Icon (7-7)
js/hang-ui/src/Components/shared/components/button/index.tsx (1)
  • Button (44-68)
js/hang-ui/src/Components/publish/FileSourceButton.tsx (2)
js/hang-ui/src/Components/shared/types/icons.ts (1)
  • Icon (7-7)
js/hang-ui/src/Components/shared/components/button/index.tsx (1)
  • Button (44-68)
js/hang-ui/src/Components/publish/MicrophoneSourceButton.tsx (2)
js/hang-ui/src/Components/shared/types/icons.ts (1)
  • Icon (7-7)
js/hang-ui/src/Components/shared/components/button/index.tsx (1)
  • Button (44-68)
js/hang-ui/src/Components/publish/ScreenSourceButton.tsx (2)
js/hang-ui/src/Components/shared/types/icons.ts (1)
  • Icon (7-7)
js/hang-ui/src/Components/shared/components/button/index.tsx (1)
  • Button (44-68)
js/hang-ui/src/Components/publish/CameraSourceButton.tsx (2)
js/hang-ui/src/Components/shared/types/icons.ts (1)
  • Icon (7-7)
js/hang-ui/src/Components/shared/components/button/index.tsx (1)
  • Button (44-68)
js/hang-ui/src/Components/shared/types/icons.ts (1)
js/signals/src/dom.ts (1)
  • Element (65-65)
🔇 Additional comments (20)
js/hang-ui/src/Components/stats/__tests__/components/StatsItem.test.tsx (1)

41-41: LGTM! Consistent accessibility improvements across test fixtures.

The addition of aria-hidden="true" to all test SVGs correctly marks them as decorative elements, aligning with the accessibility improvements made throughout the PR. The removal of <title> elements is appropriate when using aria-hidden.

Also applies to: 61-61, 94-94, 114-114, 142-142, 169-169, 204-204, 234-234, 291-291, 315-315

js/hang-ui/src/Components/stats/components/StatsPanel.tsx (1)

25-25: LGTM! Consistent accessibility improvements for stats icons.

Adding aria-hidden="true" to all SVG icons correctly marks them as decorative elements, which is appropriate since the icon purpose is conveyed through the adjacent text labels (Network, Video, Audio, Buffer).

Also applies to: 51-51, 73-73, 95-95

js/hang-ui/src/Components/shared/styles/variables.css (1)

9-16: Verify the spacing scale is intentional.

The spacing variables use very large values (1rem to 32rem). Assuming a 16px base font size, --spacing-32 would be 512px, which is unusually large for UI spacing. Please confirm this scale is intentional and not meant to be in pixels (e.g., --spacing-1: 1px instead of 1rem).

js/hang-ui/src/Components/watch/PlayPauseButton.tsx (2)

5-39: Well-structured icon set implementation.

The playbackIcons IconSet is properly typed and the SVGs are correctly marked with aria-hidden="true", allowing the Button component's title prop to provide accessibility context. The icons use consistent sizing and styling.

Note: These icons reference var(--color-white) on lines 12 and 28, which depends on the CSS variables being correctly defined. Ensure the typo in variables.css is fixed (see comment on that file).


41-52: LGTM! Clean refactor to shared Button component.

The component correctly uses the shared Button with dynamic title and icon based on playback state. The logic properly syncs the UI (icon/title) with the isPlaying() state from context.

js/hang-ui/package.json (1)

32-33: Package versions are current and secure.

@solidjs/testing-library is at version 0.8.10, the latest version, and @testing-library/jest-dom is at version 6.9.1, the latest version. @testing-library/jest-dom 6.9.1 is the latest non-vulnerable version with healthy maintenance status. No updates required.

js/hang-ui/src/Components/publish/MediaSourceSelector.tsx (1)

2-2: LGTM! Clean migration to the shared Button component.

The refactoring correctly replaces the native button with the shared Button component. The class update removes the now-unnecessary publishButton class while retaining the specific mediaSourceVisibilityToggle class for styling.

Also applies to: 17-23

js/hang-ui/src/Components/publish/ScreenSourceButton.tsx (1)

1-22: LGTM! Excellent SVG icon implementation and accessibility.

The screenIcon SVG is properly marked with aria-hidden="true" (line 16), which correctly indicates it's decorative since the Button's title provides the accessible label. The refactoring maintains all functionality while improving code consistency.

Also applies to: 34-40

js/hang-ui/src/Components/shared/__tests__/components/index.test.tsx (1)

1-60: Excellent test coverage for the Button component!

The test suite comprehensively covers all Button functionality including:

  • Default and custom props
  • Event handling
  • ARIA attributes (aria-label, aria-disabled)
  • Disabled state and tabIndex
  • Both string and JSX children

The tests follow best practices with proper use of getByRole and clear assertions.

js/hang-ui/src/Components/watch/FullscreenButton.tsx (1)

5-44: LGTM! Well-structured icon set with proper accessibility.

The fullscreenIcons IconSet correctly defines both enter and exit states, with proper aria-hidden="true" attributes on both SVGs (lines 17, 36). This follows the established pattern of decorative icons.

js/hang-ui/src/Components/watch/StatsButton.tsx (1)

1-40: LGTM! Excellent refactoring with improved consistency and accessibility.

The changes improve the component in multiple ways:

  • SVG size increased to 24x24 for consistency with other icons
  • aria-hidden="true" properly marks the decorative icon (line 16)
  • Dynamic title provides clear feedback to users ("Show stats" vs "Hide stats")
  • Explicit Icon typing improves type safety
js/hang-ui/src/Components/publish/MicrophoneSourceButton.tsx (1)

1-63: LGTM! Consistent implementation with proper device selection handling.

The refactoring correctly:

  • Replaces emoji with SVG icon marked aria-hidden="true" (line 18)
  • Uses the shared Button component
  • Maintains existing toggle behavior for mute/unmute
  • Adds controlled source selection via MediaSourceSourceSelector with selectedSource and onSelected props
  • Properly updates audio device preferences in onSourceSelected using peek() to avoid unnecessary subscriptions
js/hang-ui/src/Components/publish/NothingSourceButton.tsx (1)

5-21: No action needed. The CSS variable --color-red is defined in js/hang-ui/src/Components/shared/styles/variables.css and is properly accessible to the component through the Button component's injected stylesheet.

js/hang-ui/src/Components/watch/VolumeSlider.tsx (2)

6-90: LGTM! Well-structured icon system with proper accessibility.

The volumeIcons definition and getVolumeIcon helper provide a clean, maintainable way to handle volume state visualization. All SVG icons correctly include aria-hidden="true" for accessibility since the button itself has a descriptive title.


109-111: LGTM! Button integration is correct.

The Button component is properly integrated with:

  • Dynamic title for accessibility
  • Correct icon rendering pattern (function call)
  • Proper click handler wiring
js/hang-ui/src/Components/publish/FileSourceButton.tsx (2)

6-21: LGTM! Clean icon definition with proper accessibility.

The fileIcon SVG is well-defined with aria-hidden="true", following the established pattern for decorative icons.


46-52: LGTM! Button properly integrated with active state styling.

The Button component correctly:

  • Applies conditional active class
  • Renders the fileIcon
  • Wires the onClick handler
js/hang-ui/src/Components/shared/components/button/index.tsx (1)

1-68: LGTM! Well-designed accessible Button component.

The implementation is solid with:

  • Proper use of constructable stylesheets for style encapsulation
  • Comprehensive accessibility support (aria-label, aria-disabled, disabled, tabIndex)
  • Intelligent aria-label fallback chain
  • Clean prop handling with sensible defaults
js/hang-ui/src/Components/publish/CameraSourceButton.tsx (2)

7-23: LGTM! Camera icon properly defined.

The SVG icon follows the established pattern with proper aria-hidden="true" for accessibility.


46-52: LGTM! Button integration is correct.

The Button component is properly integrated with:

  • Conditional active class based on camera state
  • Correct icon rendering
  • Proper onClick handler

Copy link
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 1

🧹 Nitpick comments (1)
js/hang-ui/src/Components/watch/WatchUIContextProvider.tsx (1)

59-65: Excellent! Previous race condition is fixed.

The critical issue from the previous review has been properly addressed by removing the synchronous setIsFullscreen call. State updates now correctly happen via the fullscreenchange event listener.

Optional: Consider error handling for edge cases.

Both requestFullscreen() and exitFullscreen() return promises that can reject (e.g., browser restrictions, lack of user gesture). Adding .catch() handlers could improve UX by providing user feedback on failures, though in practice these failures are rare with modern browsers.

🔎 Optional enhancement for error handling
 const toggleFullscreen = () => {
 	if (document.fullscreenElement) {
-		document.exitFullscreen();
+		document.exitFullscreen().catch((err) => {
+			console.error("Failed to exit fullscreen:", err);
+		});
 	} else {
-		props.hangWatch.requestFullscreen();
+		props.hangWatch.requestFullscreen().catch((err) => {
+			console.error("Failed to enter fullscreen:", err);
+		});
 	}
 };
📜 Review details

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between c5be3c3 and 4766d8f.

📒 Files selected for processing (3)
  • js/hang-ui/src/Components/shared/components/button/index.tsx (1 hunks)
  • js/hang-ui/src/Components/shared/styles/variables.css (1 hunks)
  • js/hang-ui/src/Components/watch/WatchUIContextProvider.tsx (4 hunks)
🚧 Files skipped from review as they are similar to previous changes (1)
  • js/hang-ui/src/Components/shared/components/button/index.tsx
🔇 Additional comments (5)
js/hang-ui/src/Components/watch/WatchUIContextProvider.tsx (4)

37-38: LGTM! Type definitions are correct.

The additions to WatchUIContextValues properly declare the new fullscreen API with appropriate types.


53-53: LGTM! Proper state initialization.

Initializing from document.fullscreenElement ensures the signal reflects the actual fullscreen state on mount.


103-104: LGTM! Context value properly configured.

The fullscreen API is correctly exposed through the context provider.


179-187: Perfect! Event listener correctly implements the recommended fix.

The fullscreenchange event handler ensures isFullscreen stays synchronized with the actual document fullscreen state, completely resolving the race condition from the previous review. The cleanup function properly prevents memory leaks.

js/hang-ui/src/Components/shared/styles/variables.css (1)

1-8: LGTM! Color variables and host scoping are correctly defined.

The :host selector is appropriate for Shadow DOM/Web Components, and the color variables are well-defined. The typo fix from the previous review (--color-transparent) is confirmed.

Copy link
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 2

🧹 Nitpick comments (2)
js/hang-ui/src/Components/shared/styles/flex.css (1)

1-14: Optional: Simplify :is() usage and reduce redundancy.

Two optional refactoring suggestions:

  1. Unnecessary :is() pseudo-class: Using :is() with a single selector adds no value. It's typically used to group multiple selectors. Consider using plain class selectors:

    .flex { /* ... */ }
    .flex--center { /* ... */ }
    .flex--align-center { /* ... */ }
  2. Redundant properties: Both .flex and .flex--center set identical justify-content: center; align-items: center;. When both are applied (as in the Button component), these properties are duplicated. Consider restructuring to avoid redundancy if both classes are frequently used together.

js/hang-ui/src/Components/shared/components/button/index.tsx (1)

56-56: Optional: Redundant flex utility classes.

The class string applies both flex and flex--center, which results in duplicate justify-content: center; align-items: center; declarations (both utilities set these properties). This redundancy stems from the flex utility design noted in flex.css.

While not breaking functionality, consider simplifying once the flex utilities are restructured per the recommendations in js/hang-ui/src/Components/shared/styles/flex.css.

📜 Review details

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between c657d09 and 5b70771.

📒 Files selected for processing (9)
  • js/hang-ui/src/Components/publish/MediaSourceSelector.tsx
  • js/hang-ui/src/Components/publish/styles.css
  • js/hang-ui/src/Components/shared/components/button/index.tsx
  • js/hang-ui/src/Components/shared/components/button/style.css
  • js/hang-ui/src/Components/shared/styles/flex.css
  • js/hang-ui/src/Components/shared/styles/variables.css
  • js/hang-ui/src/Components/watch/PlayPauseButton.tsx
  • js/hang-ui/src/Components/watch/WatchControls.tsx
  • js/hang-ui/src/Components/watch/styles.css
✅ Files skipped from review due to trivial changes (1)
  • js/hang-ui/src/Components/watch/WatchControls.tsx
🚧 Files skipped from review as they are similar to previous changes (3)
  • js/hang-ui/src/Components/publish/MediaSourceSelector.tsx
  • js/hang-ui/src/Components/watch/styles.css
  • js/hang-ui/src/Components/shared/styles/variables.css
🧰 Additional context used
🧬 Code graph analysis (2)
js/hang-ui/src/Components/watch/PlayPauseButton.tsx (3)
js/hang-ui/src/Components/shared/types/icons.ts (1)
  • IconSet (6-6)
js/hang-ui/src/Components/watch/useWatchUIContext.ts (1)
  • useWatchUIContext (4-12)
js/hang-ui/src/Components/shared/components/button/index.tsx (1)
  • Button (44-68)
js/hang-ui/src/Components/shared/components/button/index.tsx (1)
js/hang-ui/src/Components/shared/styles/utils.ts (1)
  • useConstructableStyle (10-16)
🔇 Additional comments (5)
js/hang-ui/src/Components/publish/styles.css (1)

34-39: LGTM: Style overrides for media source selector buttons.

The CSS correctly uses specificity to override the base Button styles, removing background, shadow, and border for media source selector buttons in both default and hover states.

js/hang-ui/src/Components/watch/PlayPauseButton.tsx (2)

5-39: LGTM: Well-structured icon definitions.

The playbackIcons IconSet is cleanly defined with proper accessibility attributes (aria-hidden="true") on the decorative SVGs. The icons use CSS variables for theming, which maintains consistency with the design system.


48-50: LGTM: Accessible button implementation.

The Button correctly uses dynamic title attributes that change based on playback state, providing proper accessibility. The conditional icon rendering based on context.isPlaying() is clean and straightforward.

js/hang-ui/src/Components/shared/components/button/index.tsx (2)

6-7: LGTM: Efficient constructable stylesheet setup.

Creating the CSSStyleSheet at module scope ensures it's shared across all Button instances, which is an efficient pattern for style encapsulation.


58-60: LGTM: Robust aria-label fallback logic.

The fallback chain (ariaLabeltitle → string children) ensures proper accessibility labeling, with appropriate handling of non-string children that can't be used as labels.

Copy link
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 1

🧹 Nitpick comments (3)
js/hang-ui/src/Components/watch/VolumeSlider.tsx (2)

6-78: Consider extracting common SVG attributes to reduce duplication.

The four SVG icons share identical attributes (xmlns, dimensions, viewBox, fill, stroke properties, aria-hidden). While the current implementation is clear and functional, extracting these common attributes into a wrapper component or utility function would improve maintainability.

💡 Example refactor using a wrapper function
+const createVolumeIcon = (paths: string[]) => () => (
+	<svg
+		xmlns="http://www.w3.org/2000/svg"
+		width="24"
+		height="24"
+		viewBox="0 0 24 24"
+		fill="var(--color-white)"
+		stroke="currentColor"
+		stroke-width="2"
+		stroke-linecap="round"
+		stroke-linejoin="round"
+		aria-hidden="true"
+	>
+		{paths.map(d => <path d={d} />)}
+	</svg>
+);
+
 const volumeIcons: IconSet = {
-	mute: () => (
-		<svg
-			xmlns="http://www.w3.org/2000/svg"
-			width="24"
-			height="24"
-			viewBox="0 0 24 24"
-			fill="var(--color-white)"
-			stroke="currentColor"
-			stroke-width="2"
-			stroke-linecap="round"
-			stroke-linejoin="round"
-			aria-hidden="true"
-		>
-			<path d="M16 9a5 5 0 0 1 .95 2.293" />
-			<path d="M19.364 5.636a9 9 0 0 1 1.889 9.96" />
-			<path d="m2 2 20 20" />
-			<path d="m7 7-.587.587A1.4 1.4 0 0 1 5.416 8H3a1 1 0 0 0-1 1v6a1 1 0 0 0 1 1h2.416a1.4 1.4 0 0 1 .997.413l3.383 3.384A.705.705 0 0 0 11 19.298V11" />
-			<path d="M9.828 4.172A.686.686 0 0 1 11 4.657v.686" />
-		</svg>
-	),
+	mute: createVolumeIcon([
+		"M16 9a5 5 0 0 1 .95 2.293",
+		"M19.364 5.636a9 9 0 0 1 1.889 9.96",
+		"m2 2 20 20",
+		"m7 7-.587.587A1.4 1.4 0 0 1 5.416 8H3a1 1 0 0 0-1 1v6a1 1 0 0 0 1 1h2.416a1.4 1.4 0 0 1 .997.413l3.383 3.384A.705.705 0 0 0 11 19.298V11",
+		"M9.828 4.172A.686.686 0 0 1 11 4.657v.686"
+	]),
	// ... similar for other icons
 };

80-90: Logic is correct; consider extracting magic numbers.

The icon selection logic correctly divides the volume range into appropriate tiers. However, the threshold values (33, 66) are hard-coded, which reduces maintainability if the ranges need adjustment.

💡 Suggested refactor with constants
+const VOLUME_THRESHOLDS = {
+	LOW: 33,
+	MEDIUM: 66,
+} as const;
+
 const getVolumeIcon = (volume: number, isMuted: boolean) => {
 	if (isMuted || volume === 0) {
 		return volumeIcons.mute;
-	} else if (volume > 0 && volume <= 33) {
+	} else if (volume > 0 && volume <= VOLUME_THRESHOLDS.LOW) {
 		return volumeIcons.volumeLow;
-	} else if (volume > 33 && volume <= 66) {
+	} else if (volume > VOLUME_THRESHOLDS.LOW && volume <= VOLUME_THRESHOLDS.MEDIUM) {
 		return volumeIcons.volumeMedium;
 	} else {
 		return volumeIcons.volumeHigh;
 	}
 };
js/hang-ui/src/Components/watch/FullscreenButton.tsx (1)

54-56: Consider a dynamic title for improved accessibility.

The button's title (and derived aria-label) is always "Fullscreen", but the action toggles between entering and exiting. Screen reader users would benefit from knowing the current action.

🔎 Suggested improvement
 	return (
-		<Button title="Fullscreen" onClick={onClick}>
+		<Button title={context.isFullscreen() ? "Exit fullscreen" : "Enter fullscreen"} onClick={onClick}>
 			{context.isFullscreen() ? fullscreenIcons.exit() : fullscreenIcons.enter()}
 		</Button>
 	);
📜 Review details

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 5b70771 and 4f21ed1.

⛔ Files ignored due to path filters (2)
  • bun.lock is excluded by !**/*.lock
  • deno.lock is excluded by !**/*.lock
📒 Files selected for processing (24)
  • js/hang-ui/package.json
  • js/hang-ui/src/Components/publish/CameraSourceButton.tsx
  • js/hang-ui/src/Components/publish/FileSourceButton.tsx
  • js/hang-ui/src/Components/publish/MediaSourceSelector.tsx
  • js/hang-ui/src/Components/publish/MicrophoneSourceButton.tsx
  • js/hang-ui/src/Components/publish/NothingSourceButton.tsx
  • js/hang-ui/src/Components/publish/ScreenSourceButton.tsx
  • js/hang-ui/src/Components/publish/styles.css
  • js/hang-ui/src/Components/shared/__tests__/components/index.test.tsx
  • js/hang-ui/src/Components/shared/components/button/index.tsx
  • js/hang-ui/src/Components/shared/components/button/style.css
  • js/hang-ui/src/Components/shared/styles/flex.css
  • js/hang-ui/src/Components/shared/styles/utils.ts
  • js/hang-ui/src/Components/shared/styles/variables.css
  • js/hang-ui/src/Components/shared/types/icons.ts
  • js/hang-ui/src/Components/stats/__tests__/components/StatsItem.test.tsx
  • js/hang-ui/src/Components/stats/components/StatsPanel.tsx
  • js/hang-ui/src/Components/watch/FullscreenButton.tsx
  • js/hang-ui/src/Components/watch/PlayPauseButton.tsx
  • js/hang-ui/src/Components/watch/StatsButton.tsx
  • js/hang-ui/src/Components/watch/VolumeSlider.tsx
  • js/hang-ui/src/Components/watch/WatchControls.tsx
  • js/hang-ui/src/Components/watch/WatchUIContextProvider.tsx
  • js/hang-ui/src/Components/watch/styles.css
🚧 Files skipped from review as they are similar to previous changes (20)
  • js/hang-ui/package.json
  • js/hang-ui/src/Components/stats/tests/components/StatsItem.test.tsx
  • js/hang-ui/src/Components/shared/styles/variables.css
  • js/hang-ui/src/Components/publish/MicrophoneSourceButton.tsx
  • js/hang-ui/src/Components/watch/styles.css
  • js/hang-ui/src/Components/stats/components/StatsPanel.tsx
  • js/hang-ui/src/Components/publish/MediaSourceSelector.tsx
  • js/hang-ui/src/Components/publish/ScreenSourceButton.tsx
  • js/hang-ui/src/Components/publish/FileSourceButton.tsx
  • js/hang-ui/src/Components/shared/components/button/index.tsx
  • js/hang-ui/src/Components/publish/CameraSourceButton.tsx
  • js/hang-ui/src/Components/shared/types/icons.ts
  • js/hang-ui/src/Components/shared/styles/flex.css
  • js/hang-ui/src/Components/shared/components/button/style.css
  • js/hang-ui/src/Components/shared/tests/components/index.test.tsx
  • js/hang-ui/src/Components/publish/styles.css
  • js/hang-ui/src/Components/shared/styles/utils.ts
  • js/hang-ui/src/Components/publish/NothingSourceButton.tsx
  • js/hang-ui/src/Components/watch/StatsButton.tsx
  • js/hang-ui/src/Components/watch/WatchControls.tsx
🧰 Additional context used
🧬 Code graph analysis (3)
js/hang-ui/src/Components/watch/VolumeSlider.tsx (2)
js/hang-ui/src/Components/shared/types/icons.ts (1)
  • IconSet (6-6)
js/hang-ui/src/Components/shared/components/button/index.tsx (1)
  • Button (44-68)
js/hang-ui/src/Components/watch/PlayPauseButton.tsx (3)
js/hang-ui/src/Components/shared/types/icons.ts (1)
  • IconSet (6-6)
js/hang-ui/src/Components/watch/useWatchUIContext.ts (1)
  • useWatchUIContext (4-12)
js/hang-ui/src/Components/shared/components/button/index.tsx (1)
  • Button (44-68)
js/hang-ui/src/Components/watch/FullscreenButton.tsx (3)
js/hang-ui/src/Components/shared/types/icons.ts (1)
  • IconSet (6-6)
js/hang-ui/src/Components/watch/useWatchUIContext.ts (1)
  • useWatchUIContext (4-12)
js/hang-ui/src/Components/shared/components/button/index.tsx (1)
  • Button (44-68)
⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (1)
  • GitHub Check: Check
🔇 Additional comments (4)
js/hang-ui/src/Components/watch/VolumeSlider.tsx (2)

2-3: LGTM!

The imports are correctly structured and the shared Button component and IconSet type are appropriately utilized throughout the file.


109-111: Correct Button integration with proper accessibility.

The Button component is properly integrated with dynamic title/aria-label based on mute state. The getVolumeIcon(...)() pattern correctly follows the IconSet architecture where icon functions are returned and then invoked to produce JSX.

js/hang-ui/src/Components/watch/PlayPauseButton.tsx (1)

1-51: LGTM!

The migration to the shared Button component is clean. SVGs correctly use aria-hidden="true" since the Button component derives aria-label from the title prop. The dynamic icon selection based on isPlaying() state is straightforward.

js/hang-ui/src/Components/watch/WatchUIContextProvider.tsx (1)

59-65: Fullscreen toggle logic is correctly implemented.

The race condition from the previous review has been addressed. The toggleFullscreen function now only triggers the DOM API, and the fullscreenchange event handler (lines 179-181) properly synchronizes the isFullscreen signal with the actual fullscreen state.

Also applies to: 179-181

Copy link
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 1

🧹 Nitpick comments (2)
js/hang-ui/src/Components/shared/components/button/style.css (1)

16-21: Remove redundant color property in hover state.

Line 18 sets color: var(--color-white);, but this is already defined in the base .button rule (line 5) and doesn't change on hover. The redundant declaration adds noise without serving a purpose.

🔎 Proposed fix
 .button:hover:not(:disabled) {
 	transform: scale(1.02);
-	color: var(--color-white);
 	background-color: var(--color-white-alpha-20);
 	box-shadow: 0 var(--spacing-2) var(--spacing-8) var(--color-black-alpha-10);
 }
js/hang-ui/src/Components/watch/WatchUIContextProvider.tsx (1)

59-65: Consider adding error handling for fullscreen API calls.

The fullscreen implementation correctly uses event-driven state updates to avoid race conditions, and the cleanup is properly handled with onCleanup. The past critical issues have been addressed.

However, exitFullscreen() and requestFullscreen() can fail (e.g., user denial, browser restrictions, permission issues). While the state will remain accurate because fullscreenchange only fires on successful transitions, unhandled promise rejections could still surface in the console or cause unexpected behavior.

🔎 Optional: Add error handling
 const toggleFullscreen = () => {
 	if (document.fullscreenElement) {
-		document.exitFullscreen();
+		document.exitFullscreen().catch((err) => {
+			console.warn("Failed to exit fullscreen:", err);
+		});
 	} else {
-		props.hangWatch.requestFullscreen();
+		props.hangWatch.requestFullscreen().catch((err) => {
+			console.warn("Failed to enter fullscreen:", err);
+		});
 	}
 };
📜 Review details

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 4f21ed1 and d2ed9c4.

⛔ Files ignored due to path filters (2)
  • bun.lock is excluded by !**/*.lock
  • deno.lock is excluded by !**/*.lock
📒 Files selected for processing (3)
  • js/hang-ui/src/Components/shared/components/button/style.css
  • js/hang-ui/src/Components/shared/styles/flex.css
  • js/hang-ui/src/Components/watch/WatchUIContextProvider.tsx
⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (1)
  • GitHub Check: Check
🔇 Additional comments (1)
js/hang-ui/src/Components/watch/WatchUIContextProvider.tsx (1)

179-187: LGTM: Fullscreen event listener and cleanup correctly implemented.

The fullscreenchange listener is properly registered in createEffect and cleaned up using SolidJS's onCleanup. This ensures the state stays in sync with the actual fullscreen status and prevents memory leaks. The past critical issue regarding SolidJS cleanup patterns has been correctly addressed.

Comment on lines 1 to 11
:is(.flex) {
display: flex;
justify-content: center;
align-items: center;
}

:is(.flex--center) {
display: flex;
justify-content: center;
align-items: center;
}
Copy link
Contributor

Choose a reason for hiding this comment

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

🛠️ Refactor suggestion | 🟠 Major

Remove duplicate utility classes.

.flex and .flex--center are completely identical. This duplication is unnecessary and will confuse maintainers about which class to use.

Consider one of these approaches:

  1. Remove .flex--center entirely and use only .flex
  2. Make .flex a base class with just display: flex; and .flex--center a modifier that adds centering (requiring both classes: class="flex flex--center")
  3. If both are needed as standalone utilities, rename them to clarify their distinct purposes (though they currently have none)

Note: If you choose approach 2, .flex--align-center (lines 13-15) should also be updated to follow the same modifier pattern.

🔎 Proposed fix (approach 1 - remove duplication)
 :is(.flex) {
 	display: flex;
 	justify-content: center;
 	align-items: center;
 }
 
-:is(.flex--center) {
-	display: flex;
-	justify-content: center;
-	align-items: center;
-}
-
 :is(.flex--align-center) {
+	display: flex;
 	align-items: center;
 }

Committable suggestion skipped: line range outside the PR's diff.

🤖 Prompt for AI Agents
In js/hang-ui/src/Components/shared/styles/flex.css around lines 1-11, the .flex
and .flex--center rules are identical; convert to a base/modifier pattern: make
.flex contain only display: flex; make .flex--center contain justify-content:
center and align-items: center (so consumers use class="flex flex--center");
also update the related .flex--align-center rule (lines ~13-15) to be a modifier
matching this pattern (remove duplicate full definitions), and remove any
redundant identical blocks so only the base + modifiers remain.

import type { IconSet } from "../shared/types/icons";
import useWatchUIContext from "./useWatchUIContext";

const fullscreenIcons: IconSet = {
Copy link
Collaborator

Choose a reason for hiding this comment

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

Could we make these files on disk instead of inlined Javascript?

Either the bundler could inline them (meh) or they could be packaged into a public folder that would need to be served by the application (cachable).

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.

2 participants