Skip to content

Conversation

@jdreetz
Copy link

@jdreetz jdreetz commented Nov 24, 2025

What does it change?

This PR addresses #695. It migrates the Watch and Publish UIs to a separate hang-ui package, that provides two Web Components (hang-publish-ui and hang-watch-ui). Internally, the Web Components use SolidJS to render and interact with the elements.

Web Components

The Web Components expect to wrap their respective hang Web Components (hang-publish for hang-publish-ui and hang-watch for hang-watch-ui). In order to use the existing Signal reactivity, the UI components will wait until they have a reference to the respective Hang...Instance (published as a CustomEvent: watch-instance-available, publish-instance-available), that provides information about the state of the watch/publish. To interact with the Signals on the Instances, we had to make some of their properties public: video, audio, signals, file.

Context Provider

Once the Instance properties are ready, we use a SolidJS Context to expose the Instance properties to the UI components:

  • WatchUIContextProvider.tsx
  • PublishUIContextProvider.tsx

I'm open to using the Instance Signals directly in the components, but we still need something that waits for the Instance to become in the wrapped component (for some reason it's not available immediately) before the UI components can use them. Personally, I think it's helpful to keep the binding layers (Context) separate from the UI component implementation, as it reduces the noise in the components and allows the developer to focus on the markup and interactions, but I'm open to other design ideas.

How to test

  • Run the dev command (nix develop -c just dev) to start your local dev
  • Open the "publish" demo page
  • Verify the controls are displayed under the preview element
  • Verify the controls look and function the same as on main
  • Open the index demo page
  • Verify the controls are displayed under the preview element
  • Verify the controls look and function the same as on main

@coderabbitai
Copy link

coderabbitai bot commented Nov 24, 2025

Walkthrough

Adds a new TypeScript SolidJS package @kixelated/hang-ui (package.json, rollup config, tsconfig, CSS typings, README) implementing multiple Solid components, context providers, and two distributable element bundles. Registers web components hang-watch-ui and hang-publish-ui and integrates them into the demo (imports, HTML changes, Vite config, workspace and dependency updates). Changes core hang code to expose former private signals/effects as public fields (signals, video, audio, file), adds videoDevice/audioDevice setters on HangPublish, and re-exports Time from the library index.

Pre-merge checks and finishing touches

❌ Failed checks (1 warning)
Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 0.00% 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 clearly describes the main change: extracting and packaging the Hang UI as a separate package, directly addressing issue 695.
Description check ✅ Passed The description comprehensively explains the PR's purpose, implementation approach, design decisions, and testing instructions, all directly related to the changeset.
✨ 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: CodeRabbit UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 49ce3cf and 15c3705.

📒 Files selected for processing (1)
  • js/hang-ui/src/Components/publish/PublishUIContextProvider.tsx (1 hunks)
🚧 Files skipped from review as they are similar to previous changes (1)
  • js/hang-ui/src/Components/publish/PublishUIContextProvider.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

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

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

<!-- It's optional to provide a video element to preview the outgoing media. -->
<video style="width: 100%; height: auto; border-radius: 4px; margin: 0 auto;" muted autoplay></video>
</hang-publish>
<hang-publish-ui url="%VITE_RELAY_URL%" path="me"></hang-publish-ui>
Copy link
Owner

@kixelated kixelated Nov 27, 2025

Choose a reason for hiding this comment

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

I think we should have:

<hang-publish-ui>
  <hang-publish><hang-publish>
<hang-publish-ui>

That way we don't need to duplicate all of the attributes, and can make attributes that only apply to the UI (ex. the location of various controls).

Of course it's more annoying to implement but that's web components baybee.

Copy link
Author

Choose a reason for hiding this comment

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

I was rendering <hang-publish> within the web component for hang-publish-ui because it's easier to get a ref to the hang-publish el, but I see what you're saying about having to duplicate all the attributes. I don't think it'll be an issue to get the ref using query selectors though.

@@ -0,0 +1,12 @@
{
Copy link
Owner

Choose a reason for hiding this comment

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

Give biome a try; it's sooo much better than prettier IMO.

"rollup": "^4.53.3",
"rollup-plugin-string": "^3.0.0",
"solid-element": "^1.9.1",
"solid-js": "^1.9.10",
Copy link
Owner

Choose a reason for hiding this comment

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

your choice if you use solids. I'd be down with trying out other frameworks

"exports": {
".": "./src/index.ts",
"./zod": "./src/zod.ts"
".": "./dist/index.js",
Copy link
Owner

Choose a reason for hiding this comment

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

Ideally we can avoid this change because it makes local testing (via link) more difficult.

Copy link
Author

Choose a reason for hiding this comment

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

I was having difficulties getting the types for HangPublish in the UI without this change. Is there another way to do that? Maybe I can avoid using the types, but it'd make things easier to have access to them.

Copy link
Author

Choose a reason for hiding this comment

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

I got around this requirement by using esbuild to do the typescript handling in Rollup.

@jdreetz
Copy link
Author

jdreetz commented Nov 27, 2025

@kixelated do we want to do the "Meet" UI as well?

…by using esbuild instead of typescript rollup plugin.
Copy link

@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: 0

♻️ Duplicate comments (2)
js/hang-ui/src/Components/publish/styles.css (2)

14-21: Add focus-visible styles for keyboard accessibility.

The .publishButton resets default browser styles including focus outlines. This breaks keyboard navigation accessibility. Add explicit focus styles.

 .publishButton {
 	background: transparent;
 	border: none;
 	cursor: pointer;
 	display: inline-block;
 	font: inherit;
 	color: inherit;
 }
+
+.publishButton:focus-visible {
+	outline: 2px solid currentColor;
+	outline-offset: 2px;
+}

43-48: Missing left: 50% for horizontal centering.

The transform: translateX(-50%) requires left: 50% to center the element. Without it, the element starts at the parent's left edge and shifts left by half its width, causing misalignment.

 .mediaSourceSelector {
 	position: absolute;
 	top: 100%;
+	left: 50%;
 	transform: translateX(-50%);
 	display: block;
 }
🧹 Nitpick comments (2)
js/hang-ui/src/Components/publish/PublishUIContextProvider.tsx (2)

70-88: Potential issue if hangPublish signal changes before event fires.

While { once: true } handles cleanup after the event fires, if props.hangPublish() reactively changes to a different element before the publish-instance-available event fires on the original element, the old listener remains attached (though it will auto-remove after firing once on the old element).

Consider using onCleanup to explicitly remove the listener when the effect re-runs:

+import { createContext, createEffect, createSignal, onCleanup } from "solid-js";
 
 // in createEffect:
 	createEffect(() => {
 		const hangPublishEl = props.hangPublish();
 		if (!hangPublishEl) return;
 
+		const handleInstanceAvailable = (event: CustomEvent) => {
+			const publishInstance = event.detail.instance.peek?.() as HangPublishInstance;
+			onPublishInstanceAvailable(hangPublishEl, publishInstance);
+		};
+
 		if (!hangPublishEl.active) {
 			// @ts-expect-error ignore custom event - todo add event map
-			hangPublishEl.addEventListener(
-				"publish-instance-available",
-				(event: CustomEvent) => {
-					const publishInstance = event.detail.instance.peek?.() as HangPublishInstance;
-					onPublishInstanceAvailable(hangPublishEl, publishInstance);
-				},
-				{ once: true },
-			);
+			hangPublishEl.addEventListener("publish-instance-available", handleInstanceAvailable, { once: true });
+			onCleanup(() => {
+				// @ts-expect-error ignore custom event
+				hangPublishEl.removeEventListener("publish-instance-available", handleInstanceAvailable);
+			});
 		} else {

44-53: Dual setFileActive calls may cause UI flicker.

setFileActive(true) is called in setFile (line 52), and trackFileActive effect (lines 186-189) also updates fileActive based on el.signals.source. When setFile is called, line 52 sets it to true, then immediately after, if the source signal updates, trackFileActive will also fire and potentially set the same value.

This is likely harmless (SolidJS dedupes identical signal values), but the explicit setFileActive(true) in setFile may be redundant since trackFileActive will set it correctly when source becomes "file".

Consider removing the explicit call in setFile to rely solely on the reactive effect:

 const setFile = (file: File) => {
 	const hangPublishEl = props.hangPublish();
 	if (!hangPublishEl) return;
 
 	hangPublishEl.file = file;
 	hangPublishEl.source = "file";
 	hangPublishEl.video = true;
 	hangPublishEl.audio = true;
-	setFileActive(true);
 };

Also applies to: 186-189

📜 Review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 93f474f and 814326b.

⛔ Files ignored due to path filters (1)
  • js/deno.lock is excluded by !**/*.lock
📒 Files selected for processing (10)
  • README.md (1 hunks)
  • js/hang-ui/src/Components/publish/FileSourceButton.tsx (1 hunks)
  • js/hang-ui/src/Components/publish/PublishUIContextProvider.tsx (1 hunks)
  • js/hang-ui/src/Components/publish/element.tsx (1 hunks)
  • js/hang-ui/src/Components/publish/styles.css (1 hunks)
  • js/hang-ui/src/Components/watch/VolumeSlider.tsx (1 hunks)
  • js/hang-ui/src/Components/watch/WatchUIContextProvider.tsx (1 hunks)
  • js/hang-ui/src/Components/watch/element.tsx (1 hunks)
  • js/hang-ui/src/Components/watch/styles.css (1 hunks)
  • js/hang-ui/tsconfig.json (1 hunks)
🚧 Files skipped from review as they are similar to previous changes (6)
  • js/hang-ui/tsconfig.json
  • js/hang-ui/src/Components/watch/WatchUIContextProvider.tsx
  • js/hang-ui/src/Components/watch/styles.css
  • README.md
  • js/hang-ui/src/Components/watch/element.tsx
  • js/hang-ui/src/Components/watch/VolumeSlider.tsx
🧰 Additional context used
📓 Path-based instructions (1)
js/**/*.{js,ts,tsx,json}

📄 CodeRabbit inference engine (CLAUDE.md)

Use bun workspaces configured in js/package.json for JavaScript/TypeScript development

Files:

  • js/hang-ui/src/Components/publish/FileSourceButton.tsx
  • js/hang-ui/src/Components/publish/element.tsx
  • js/hang-ui/src/Components/publish/PublishUIContextProvider.tsx
🧠 Learnings (1)
📚 Learning: 2025-11-22T15:03:15.091Z
Learnt from: pzanella
Repo: kixelated/moq PR: 714
File: js/hang/src/watch/element.ts:469-469
Timestamp: 2025-11-22T15:03:15.091Z
Learning: In js/hang/src/watch/element.ts, the buffering overlay intentionally uses `pointerEvents: "auto"` to block clicks and prevent accidental interactions with underlying controls during buffering.

Applied to files:

  • js/hang-ui/src/Components/publish/FileSourceButton.tsx
🧬 Code graph analysis (1)
js/hang-ui/src/Components/publish/FileSourceButton.tsx (1)
js/hang-ui/src/Components/publish/PublishUIContextProvider.tsx (1)
  • PublishUIContext (28-28)
🔇 Additional comments (3)
js/hang-ui/src/Components/publish/element.tsx (1)

11-26: LGTM!

The race condition handling is well implemented. The code first attempts to find an existing hang-publish element, and if not found, sets up a one-time event listener with { once: true } which automatically removes itself after firing. This correctly handles both scenarios where the child element is already present or connects later.

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

4-16: LGTM!

The file input pattern is correctly implemented. The component properly:

  • Uses a ref signal to access the hidden input
  • Triggers the file dialog via the visible button
  • Handles file selection with appropriate validation
  • Resets the input value to allow re-selecting the same file
  • Uses optional chaining on context for safety
js/hang-ui/src/Components/publish/PublishUIContextProvider.tsx (1)

92-189: The suggested solution uses the wrong library and will not work in this codebase.

The review correctly identifies that publishInstance.signals.effect() calls return disposal functions that must be captured and called to prevent memory leaks. However, the suggested diff incorrectly imports from Solid.js (createEffect, createContext, onCleanup, createSignal), when the code uses the @kixelated/signals library.

The correct approach is to:

  1. Capture the dispose functions returned by signals.effect(): const dispose1 = publishInstance?.signals.effect(...)
  2. Store them in an array
  3. Call each dispose function in a cleanup callback (the mechanism depends on whether the component itself uses Solid.js or another framework)

The concern about memory leaks from undisposed effects is valid, but the specific implementation provided will not resolve it for this codebase.

Likely an incorrect or invalid review comment.

Copy link

@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: 0

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

12-18: Simplify toggle handler with functional updater

You can simplify toggleSourcesVisible to avoid the explicit if/else and rely on the previous value:

- const toggleSourcesVisible = () => {
-   if (sourcesVisible()) {
-     setSourcesVisible(false);
-   } else {
-     setSourcesVisible(true);
-   }
- };
+ const toggleSourcesVisible = () => {
+   setSourcesVisible((visible) => !visible);
+ };

22-39: Minor UX/a11y nits on toggle button and select

Two small polish points:

  • The constant title "Show Sources" and the ▼/▲ indicators are slightly inverted from common patterns (▼ usually means “expand”; ▲ “collapse”). Consider flipping the symbols or updating the title dynamically ("Show Sources" / "Hide Sources").
  • The <select> lacks an accessible name here. If it isn’t wrapped by a <label> elsewhere, add aria-label or an explicit label element so screen readers can identify what this selector controls.

These are non-blocking but would improve clarity and accessibility.

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

27-29: Consider tracking listener registration for targeted cleanup.

The cleanup unconditionally removes the event listener even when it was only added in the else branch (line 23). While removeEventListener on a never-added listener is a safe no-op, you could optimize this by tracking whether the listener was registered.

Apply this diff to optimize cleanup:

 customElement("hang-watch-ui", {}, function WatchUIWebComponent(_, { element }) {
 	const [hangWatchEl, setHangWatchEl] = createSignal<HangWatch>();
+	let listenerAdded = false;
 
 	const onInstanceAvailable = (event: CustomEvent) => {
 		const hangWatchEl = event.target as HangWatch;
 		setHangWatchEl(hangWatchEl);
 	};
 
 	onMount(() => {
 		const watchEl = element.querySelector("hang-watch");
 
 		if (watchEl) {
 			setHangWatchEl(watchEl);
 		} else {
 			element.addEventListener("watch-instance-available", onInstanceAvailable);
+			listenerAdded = true;
 		}
 	});
 
 	onCleanup(() => {
-		element.removeEventListener("watch-instance-available", onInstanceAvailable);
+		if (listenerAdded) {
+			element.removeEventListener("watch-instance-available", onInstanceAvailable);
+		}
 	});
📜 Review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 814326b and 61e2660.

📒 Files selected for processing (6)
  • js/hang-ui/src/Components/publish/MediaSourceSelector.tsx (1 hunks)
  • js/hang-ui/src/Components/publish/PublishUIContextProvider.tsx (1 hunks)
  • js/hang-ui/src/Components/publish/element.tsx (1 hunks)
  • js/hang-ui/src/Components/publish/styles.css (1 hunks)
  • js/hang-ui/src/Components/watch/WatchUIContextProvider.tsx (1 hunks)
  • js/hang-ui/src/Components/watch/element.tsx (1 hunks)
🚧 Files skipped from review as they are similar to previous changes (4)
  • js/hang-ui/src/Components/publish/styles.css
  • js/hang-ui/src/Components/watch/WatchUIContextProvider.tsx
  • js/hang-ui/src/Components/publish/PublishUIContextProvider.tsx
  • js/hang-ui/src/Components/publish/element.tsx
🧰 Additional context used
📓 Path-based instructions (1)
js/**/*.{js,ts,tsx,json}

📄 CodeRabbit inference engine (CLAUDE.md)

Use bun workspaces configured in js/package.json for JavaScript/TypeScript development

Files:

  • js/hang-ui/src/Components/watch/element.tsx
  • js/hang-ui/src/Components/publish/MediaSourceSelector.tsx
🧠 Learnings (1)
📚 Learning: 2025-11-22T15:03:15.091Z
Learnt from: pzanella
Repo: kixelated/moq PR: 714
File: js/hang/src/watch/element.ts:469-469
Timestamp: 2025-11-22T15:03:15.091Z
Learning: In js/hang/src/watch/element.ts, the buffering overlay intentionally uses `pointerEvents: "auto"` to block clicks and prevent accidental interactions with underlying controls during buffering.

Applied to files:

  • js/hang-ui/src/Components/watch/element.tsx
🧬 Code graph analysis (1)
js/hang-ui/src/Components/watch/element.tsx (5)
js/hang/src/watch/element.ts (1)
  • HangWatch (23-181)
js/signals/src/index.ts (1)
  • event (489-504)
js/hang-ui/src/Components/watch/WatchUIContextProvider.tsx (1)
  • WatchUIContextProvider (27-137)
js/hang-ui/src/Components/watch/BufferingIndicator.tsx (1)
  • BufferingIndicator (4-14)
js/hang-ui/src/Components/watch/WatchControls.tsx (1)
  • WatchControls (6-15)
⏰ 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/publish/MediaSourceSelector.tsx (1)

1-43: Component structure and typings look solid

Typing of props, SolidJS usage (createSignal, Show, For), and the onSelected wiring are all correct and straightforward. Behavior for undefined sources is safe, and the optional callback is handled properly.

js/hang-ui/src/Components/watch/element.tsx (3)

12-15: LGTM!

The event handler correctly captures the HangWatch element from event.target and updates the signal. The WatchUIContextProvider will later access the instance via the element's active property.


31-40: LGTM!

The render structure is well-organized: the WatchUIContextProvider wraps all UI components to expose watch state and controls, the slot allows the child <hang-watch> element to be inserted, and the BufferingIndicator and WatchControls are properly positioned.


17-25: The referenced file js/hang/src/watch/element.ts does not exist in the repository, and the watch-instance-available event dispatch is not found anywhere in the codebase. The verification cannot be completed as described because the complementary code being referenced is not present. Consider verifying that the HangWatch component implementation exists and implements the expected event dispatch behavior before addressing this concern.

Likely an incorrect or invalid review comment.

Copy link

@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: 3

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

27-27: Apply the latencyValueDisplay class and avoid redundant calls.

The display span doesn't use the .latencyValueDisplay class defined in styles.css, and context?.latency() is called twice (inefficient).

Apply this diff:

-<span>{typeof context?.latency() !== "undefined" ? `${Math.round(context?.latency())}ms` : ""}</span>
+<span class="latencyValueDisplay">{typeof context?.latency() !== "undefined" ? `${Math.round(context.latency())}ms` : ""}</span>
📜 Review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 3796fe2 and 092a163.

📒 Files selected for processing (7)
  • js/hang-demo/src/index.html (1 hunks)
  • js/hang-ui/src/Components/publish/MediaSourceSelector.tsx (1 hunks)
  • js/hang-ui/src/Components/watch/LatencySlider.tsx (1 hunks)
  • js/hang-ui/src/Components/watch/WatchControls.tsx (1 hunks)
  • js/hang-ui/src/Components/watch/WatchUIContextProvider.tsx (1 hunks)
  • js/hang-ui/src/Components/watch/styles.css (1 hunks)
  • js/hang/src/index.ts (1 hunks)
🚧 Files skipped from review as they are similar to previous changes (3)
  • js/hang-demo/src/index.html
  • js/hang-ui/src/Components/publish/MediaSourceSelector.tsx
  • js/hang-ui/src/Components/watch/WatchControls.tsx
🧰 Additional context used
📓 Path-based instructions (1)
js/**/*.{js,ts,tsx,json}

📄 CodeRabbit inference engine (CLAUDE.md)

Use bun workspaces configured in js/package.json for JavaScript/TypeScript development

Files:

  • js/hang/src/index.ts
  • js/hang-ui/src/Components/watch/LatencySlider.tsx
  • js/hang-ui/src/Components/watch/WatchUIContextProvider.tsx
🧠 Learnings (1)
📚 Learning: 2025-11-22T15:03:15.091Z
Learnt from: pzanella
Repo: kixelated/moq PR: 714
File: js/hang/src/watch/element.ts:469-469
Timestamp: 2025-11-22T15:03:15.091Z
Learning: In js/hang/src/watch/element.ts, the buffering overlay intentionally uses `pointerEvents: "auto"` to block clicks and prevent accidental interactions with underlying controls during buffering.

Applied to files:

  • js/hang-ui/src/Components/watch/styles.css
🧬 Code graph analysis (2)
js/hang-ui/src/Components/watch/LatencySlider.tsx (1)
js/hang-ui/src/Components/watch/WatchUIContextProvider.tsx (1)
  • WatchUIContext (28-28)
js/hang-ui/src/Components/watch/WatchUIContextProvider.tsx (1)
js/hang/src/watch/element.ts (10)
  • HangWatch (23-181)
  • latency (174-176)
  • latency (178-180)
  • volume (142-144)
  • volume (146-148)
  • HangWatchInstance (185-300)
  • url (109-111)
  • url (113-115)
  • paused (134-136)
  • paused (138-140)
⏰ 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 (2)
js/hang/src/index.ts (1)

7-7: Time namespace re-export is consistent with existing barrel pattern

Adding export * as Time from "./time"; matches the existing namespace-style exports (Moq, Catalog, Frame, Publish, Support, Watch) and cleanly exposes the time utilities as part of the public hang surface. As long as ./time is intended to be stable/public API, this looks good to ship.

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

86-106: LGTM! Event listener cleanup implemented.

The event listener is now properly removed using onCleanup, which addresses the memory leak concern from the previous review.

Copy link

@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)

32-36: Avoid desync between isMuted signal and the underlying Hang audio state

Right now isMuted is managed as local Solid state and only updated in toggleMuted, while currentVolume is driven from watchInstance.audio.volume. This can cause drift:

  • toggleMuted (Lines 54-62) flips isMuted() even when props.hangWatch() is undefined, so the UI may display “muted” with no underlying element.
  • External changes to hangWatchEl.muted or the underlying audio emitter won’t be reflected back into isMuted.

To align with how volume is handled, consider:

  1. Drive isMuted from the instance, similar to trackVolume:
@@ function onWatchInstanceAvailable(watchEl: HangWatch, watchInstance: HangWatchInstance) {
 		watchInstance?.signals.effect(function trackVolume(effect) {
 			const volume = effect.get(watchInstance?.audio.volume);
 			setCurrentVolume(volume * 100);
 		});
+
+		watchInstance?.signals.effect(function trackMuted(effect) {
+			const muted = effect.get(watchInstance?.audio.muted);
+			setIsMuted(muted);
+		});
  1. Make toggleMuted only toggle the element and rely on the effect above to update the context:
-	const toggleMuted = () => {
-		setIsMuted(!isMuted());
-
-		const hangWatchEl = props.hangWatch();
-
-		if (hangWatchEl) {
-			hangWatchEl.muted = isMuted();
-		}
-	};
+	const toggleMuted = () => {
+		const hangWatchEl = props.hangWatch();
+		if (!hangWatchEl) return;
+
+		hangWatchEl.muted = !hangWatchEl.muted;
+	};

This keeps the UI state in sync with the actual audio pipeline and avoids misleading toggles when the watch element isn’t available yet.

📜 Review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 48b18a9 and cc111d5.

📒 Files selected for processing (2)
  • js/hang-ui/src/Components/watch/WatchUIContextProvider.tsx (1 hunks)
  • js/hang-ui/src/Components/watch/styles.css (1 hunks)
🚧 Files skipped from review as they are similar to previous changes (1)
  • js/hang-ui/src/Components/watch/styles.css
🧰 Additional context used
📓 Path-based instructions (1)
js/**/*.{js,ts,tsx,json}

📄 CodeRabbit inference engine (CLAUDE.md)

Use bun workspaces configured in js/package.json for JavaScript/TypeScript development

Files:

  • js/hang-ui/src/Components/watch/WatchUIContextProvider.tsx
🧬 Code graph analysis (1)
js/hang-ui/src/Components/watch/WatchUIContextProvider.tsx (1)
js/hang/src/watch/element.ts (10)
  • HangWatch (23-181)
  • latency (174-176)
  • latency (178-180)
  • volume (142-144)
  • volume (146-148)
  • HangWatchInstance (185-300)
  • url (109-111)
  • url (113-115)
  • paused (134-136)
  • paused (138-140)
⏰ 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)

110-155: Signal wiring from HangWatchInstance into context looks solid

The effects for status, playing, volume, buffering, and latency correctly subscribe to the corresponding HangWatchInstance / HangWatch signals and project them into Solid signals, matching the shapes exposed by WatchUIContextValues. No issues stand out here.

@jdreetz
Copy link
Author

jdreetz commented Dec 1, 2025

@christriants updated this branch to include the Latency Slider as a component.

Copy link

@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/LatencySlider.tsx (1)

10-14: Consider using onInput for real-time updates.

The onChange event fires only when the user releases the slider. For a latency control, using onInput instead would provide continuous updates as the user drags, offering better real-time feedback.

Apply this diff to enable real-time updates:

 const onInputChange = (event: Event) => {
 	const target = event.currentTarget as HTMLInputElement;
 	const latency = parseFloat(target.value);
 	context?.setLatencyValue(latency);
 };

 return (
 	<div class="latencySliderContainer">
 		<span class="latencyLabel">Latency: </span>
 		<input
-			onChange={onInputChange}
+			onInput={onInputChange}
 			class="latencySlider"
 			type="range"
 			value={context?.latency()}
 			min={MIN_RANGE}
 			max={MAX_RANGE}
 			step={RANGE_STEP}
 		/>
📜 Review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between cc111d5 and e16c241.

📒 Files selected for processing (1)
  • js/hang-ui/src/Components/watch/LatencySlider.tsx (1 hunks)
🧰 Additional context used
📓 Path-based instructions (1)
js/**/*.{js,ts,tsx,json}

📄 CodeRabbit inference engine (CLAUDE.md)

Use bun workspaces configured in js/package.json for JavaScript/TypeScript development

Files:

  • js/hang-ui/src/Components/watch/LatencySlider.tsx
🧬 Code graph analysis (1)
js/hang-ui/src/Components/watch/LatencySlider.tsx (1)
js/hang-ui/src/Components/watch/WatchUIContextProvider.tsx (1)
  • WatchUIContext (28-28)
⏰ 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

Copy link

@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

♻️ Duplicate comments (1)
js/hang-ui/src/Components/watch/WatchUIContextProvider.tsx (1)

86-106: Instance binding logic and event listener cleanup look correct now

The createEffect correctly:

  • Reads the current HangWatch element from props.hangWatch().
  • Uses active.peek() to decide between immediate binding vs. waiting for "watch-instance-available".
  • Registers the event listener only when needed and removes it in onCleanup.

This addresses the earlier concerns about dead code (if (!hangWatchEl.active)) and potential listener leaks.

🧹 Nitpick comments (4)
js/hang-ui/src/Components/publish/PublishUIContextProvider.tsx (2)

44-52: Consider validating file type and handling errors.

The setFile function assumes the provided file is valid and supported. Adding file type validation and error handling would improve robustness, especially if the underlying API rejects certain file types.


164-185: Consider explicitly handling all connection status values.

The status derivation logic implicitly assumes that if status is neither "disconnected" nor "connecting", the connection is established. Making this explicit would improve code clarity and prevent subtle bugs if new status values are added.

Consider adding an explicit check:

 		} else if (status === "connecting") {
 			setPublishStatus("connecting");
+		} else if (status !== "connected") {
+			// Handle unexpected status values
+			setPublishStatus("disconnected");
 		} else if (!audio && !video) {
js/hang-ui/src/Components/watch/WatchUIContextProvider.tsx (2)

32-62: Derive isMuted from the HangWatch instance instead of maintaining local-only mute state

Right now isMuted is only updated when toggleMuted runs; if something else toggles hangWatch.muted (e.g., another UI or attribute change), the context gets out of sync. Since other fields (isPlaying, currentVolume) are driven from watchInstance signals, it would be more consistent to do the same for mute and let toggleMuted just flip the element property.

Suggested refactor:

-	const [isMuted, setIsMuted] = createSignal<boolean>(false);
+	const [isMuted, setIsMuted] = createSignal<boolean>(false);
@@
-	const toggleMuted = () => {
-		setIsMuted(!isMuted());
-
-		const hangWatchEl = props.hangWatch();
-
-		if (hangWatchEl) {
-			hangWatchEl.muted = isMuted();
-		}
-	};
+	const toggleMuted = () => {
+		const hangWatchEl = props.hangWatch();
+
+		if (hangWatchEl) {
+			hangWatchEl.muted = !hangWatchEl.muted;
+		}
+	};
@@
-		watchInstance?.signals.effect(function trackVolume(effect) {
+		watchInstance?.signals.effect(function trackVolume(effect) {
 			const volume = effect.get(watchInstance?.audio.volume);
 			setCurrentVolume(volume * 100);
 		});
+
+		watchInstance?.signals.effect(function trackMuted(effect) {
+			const muted = effect.get(watchInstance?.audio.muted);
+			setIsMuted(muted);
+		});

This keeps the context as a pure view of the underlying instance state and avoids divergence.

Also applies to: 138-141, 151-154


46-52: Optionally clamp volume to [0, 1] when mapping from 0–100 UI range

setVolume divides by 100 and trackVolume multiplies by 100, which is symmetric, but if a caller passes values outside 0–100 you can end up feeding >1 or <0 into hangWatchEl.volume.

A small defensive tweak would make this more robust:

-	const setVolume = (volume: number) => {
+	const setVolume = (volume: number) => {
 		const hangWatchEl = props.hangWatch();
 
 		if (hangWatchEl) {
-			hangWatchEl.volume = volume / 100;
+			const normalized = Math.min(1, Math.max(0, volume / 100));
+			hangWatchEl.volume = normalized;
 		}
 	};

Not critical, but it hardens the API against bad inputs.

Also applies to: 138-141

📜 Review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between e16c241 and 0a2f34e.

📒 Files selected for processing (3)
  • js/hang-ui/src/Components/publish/PublishUIContextProvider.tsx (1 hunks)
  • js/hang-ui/src/Components/watch/LatencySlider.tsx (1 hunks)
  • js/hang-ui/src/Components/watch/WatchUIContextProvider.tsx (1 hunks)
🚧 Files skipped from review as they are similar to previous changes (1)
  • js/hang-ui/src/Components/watch/LatencySlider.tsx
🧰 Additional context used
📓 Path-based instructions (1)
js/**/*.{js,ts,tsx,json}

📄 CodeRabbit inference engine (CLAUDE.md)

Use bun workspaces configured in js/package.json for JavaScript/TypeScript development

Files:

  • js/hang-ui/src/Components/publish/PublishUIContextProvider.tsx
  • js/hang-ui/src/Components/watch/WatchUIContextProvider.tsx
🧠 Learnings (1)
📚 Learning: 2025-11-22T15:03:15.091Z
Learnt from: pzanella
Repo: kixelated/moq PR: 714
File: js/hang/src/watch/element.ts:469-469
Timestamp: 2025-11-22T15:03:15.091Z
Learning: In js/hang/src/watch/element.ts, the buffering overlay intentionally uses `pointerEvents: "auto"` to block clicks and prevent accidental interactions with underlying controls during buffering.

Applied to files:

  • js/hang-ui/src/Components/watch/WatchUIContextProvider.tsx
🧬 Code graph analysis (1)
js/hang-ui/src/Components/watch/WatchUIContextProvider.tsx (1)
js/hang/src/watch/element.ts (10)
  • HangWatch (23-181)
  • latency (174-176)
  • latency (178-180)
  • volume (142-144)
  • volume (146-148)
  • HangWatchInstance (185-300)
  • url (109-111)
  • url (113-115)
  • paused (134-136)
  • paused (138-140)
⏰ 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 (3)
js/hang-ui/src/Components/publish/PublishUIContextProvider.tsx (1)

93-191: Effect subscriptions require verification of cleanup implementation.

The nine publishInstance.signals.effect() calls (trackCameraDevices, trackMicrophoneDevices, trackNothingSourceActive, trackMicrophoneSourceActive, trackVideoSourcesActive, trackSelectedCameraSource, trackSelectedMicrophoneSource, trackPublishStatus, trackFileActive) may not be properly disposed. Verify whether:

  1. effect() returns a dispose/cleanup function
  2. These cleanup functions are collected and invoked in an onCleanup callback
  3. This cleanup is already implemented elsewhere in the component

If effects are not being disposed, this will cause memory leaks and stale state updates when the component unmounts or hangPublishEl changes.

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

7-28: Context types and value shape look consistent and sufficient

WatchUIContextProviderProps, WatchStatus, and WatchUIContextValues line up well with the HangWatch/HangWatchInstance API and the controls you expose; nothing blocking here.


110-155: Signal effects for status/playback/volume/buffering/latency are wired coherently

The onWatchInstanceAvailable effects correctly derive:

  • watchStatus from connection URL/status and broadcast status.
  • isPlaying from video.paused.
  • currentVolume from audio.volume with the expected 0–100 mapping.
  • buffering from syncStatus/bufferStatus (matching the existing buffering overlay behavior).
  • latency from watchEl.signals.latency.

This is a solid integration of the HangWatch signals into the UI context; aside from the separate mute handling already noted, there are no functional issues here.

Copy link

@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

♻️ Duplicate comments (1)
js/hang-ui/src/Components/publish/PublishUIContextProvider.tsx (1)

86-86: Inconsistent optional chaining on active property.

Line 78 uses hangPublishEl?.active?.peek() with optional chaining on active, but line 86 uses hangPublishEl.active.peek?.() without optional chaining on active. While technically safe due to the control flow (the else branch only executes when active.peek() is truthy), this inconsistency could cause confusion or become error-prone during refactoring.

Apply this diff for consistency:

 	} else {
-		const publishInstance = hangPublishEl.active.peek?.() as HangPublishInstance;
+		const publishInstance = hangPublishEl.active?.peek?.() as HangPublishInstance;
 		onPublishInstanceAvailable(hangPublishEl, publishInstance);
 	}
🧹 Nitpick comments (1)
js/hang-ui/src/Components/publish/PublishUIContextProvider.tsx (1)

44-52: Consider potential intermediate states when setting multiple properties.

The function sets four properties sequentially on hangPublishEl. While this likely works correctly given synchronous setters, intermediate states could theoretically cause issues if any internal reactions observe partial updates.

If the hang library supports batching or atomic updates, consider using that pattern:

const setFile = (file: File) => {
	const hangPublishEl = props.hangPublish();
	if (!hangPublishEl) return;

	// If hang supports batching, wrap in a batch operation:
	// batch(() => {
		hangPublishEl.file = file;
		hangPublishEl.source = "file";
		hangPublishEl.video = true;
		hangPublishEl.audio = true;
	// });
};
📜 Review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 0a2f34e and d1af166.

📒 Files selected for processing (2)
  • js/hang-ui/src/Components/publish/PublishUIContextProvider.tsx (1 hunks)
  • js/hang-ui/src/Components/watch/WatchUIContextProvider.tsx (1 hunks)
🧰 Additional context used
📓 Path-based instructions (1)
js/**/*.{js,ts,tsx,json}

📄 CodeRabbit inference engine (CLAUDE.md)

Use bun workspaces configured in js/package.json for JavaScript/TypeScript development

Files:

  • js/hang-ui/src/Components/watch/WatchUIContextProvider.tsx
  • js/hang-ui/src/Components/publish/PublishUIContextProvider.tsx
🧠 Learnings (1)
📚 Learning: 2025-11-22T15:03:15.091Z
Learnt from: pzanella
Repo: kixelated/moq PR: 714
File: js/hang/src/watch/element.ts:469-469
Timestamp: 2025-11-22T15:03:15.091Z
Learning: In js/hang/src/watch/element.ts, the buffering overlay intentionally uses `pointerEvents: "auto"` to block clicks and prevent accidental interactions with underlying controls during buffering.

Applied to files:

  • js/hang-ui/src/Components/watch/WatchUIContextProvider.tsx
🧬 Code graph analysis (2)
js/hang-ui/src/Components/watch/WatchUIContextProvider.tsx (1)
js/hang/src/watch/element.ts (10)
  • HangWatch (23-181)
  • latency (174-176)
  • latency (178-180)
  • volume (142-144)
  • volume (146-148)
  • HangWatchInstance (185-300)
  • url (109-111)
  • url (113-115)
  • paused (134-136)
  • paused (138-140)
js/hang-ui/src/Components/publish/PublishUIContextProvider.tsx (2)
js/hang/src/publish/source/file.ts (2)
  • File (9-126)
  • setFile (35-37)
js/hang/src/publish/element.ts (1)
  • HangPublishInstance (178-339)
⏰ 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/publish/PublishUIContextProvider.tsx (1)

93-191: The nested effects registered within onPublishInstanceAvailable are automatically cleaned up by Solid.js when the outer createEffect reruns. In Solid.js, effects created within a reactive scope are part of that scope's lifecycle—when the scope reruns, previous inner effects are disposed before new ones are created. No manual cleanup is required for this nested effect pattern.

Likely an incorrect or invalid review comment.

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

86-106: LGTM: Instance binding logic correctly handles both immediate and event-driven binding.

The conditional check on Line 95 properly examines active.peek() (the signal's value) rather than the signal itself, ensuring the event listener is registered only when no instance exists yet. The cleanup logic correctly removes the listener when the effect re-runs or the component unmounts.


151-154: LGTM: Latency tracking is correctly implemented.

The trackLatency effect properly subscribes to the latency signal and updates the UI state. The previously reported typo has been fixed.


143-149: Optional chaining is unnecessary here.

The syncStatus and bufferStatus values returned by effect.get() are guaranteed to be defined. These values come from Signal instances that are always initialized in the Source class and maintain their .state property. Adding optional chaining would be defensive programming without practical benefit.

Likely an incorrect or invalid review comment.

Copy link

@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: 0

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

38-60: Optionally clamp volume and latency inputs before writing to HangWatch.

Right now setVolume and setLatencyValue trust callers to pass sane ranges; a stray value (e.g., >100 or negative) would propagate directly into the Hang signals. Consider clamping to defensive ranges (0–100 for volume slider percentage, and >= 0 for latency) to make the context safer to use from any UI component.

-	const setVolume = (volume: number) => {
-		const hangWatchEl = props.hangWatch();
-
-		if (hangWatchEl) {
-			hangWatchEl.volume = volume / 100;
-		}
-	};
+	const setVolume = (volume: number) => {
+		const hangWatchEl = props.hangWatch();
+		if (!hangWatchEl) return;
+
+		const clamped = Math.min(100, Math.max(0, volume));
+		hangWatchEl.volume = clamped / 100;
+	};
@@
-	const setLatencyValue = (latency: number) => {
-		const hangWatchEl = props.hangWatch();
-
-		if (hangWatchEl) {
-			hangWatchEl.signals.latency.set(latency as Time.Milli);
-		}
-	};
+	const setLatencyValue = (latency: number) => {
+		const hangWatchEl = props.hangWatch();
+		if (!hangWatchEl) return;
+
+		const clamped = Math.max(0, latency);
+		hangWatchEl.signals.latency.set(clamped as Time.Milli);
+	};

Please double‑check expected ranges for volume and latency against @kixelated/hang’s current API before adopting this.

Also applies to: 62-68

📜 Review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between d1af166 and fa6f555.

📒 Files selected for processing (1)
  • js/hang-ui/src/Components/watch/WatchUIContextProvider.tsx (1 hunks)
🧰 Additional context used
📓 Path-based instructions (1)
js/**/*.{js,ts,tsx,json}

📄 CodeRabbit inference engine (CLAUDE.md)

Use bun workspaces configured in js/package.json for JavaScript/TypeScript development

Files:

  • js/hang-ui/src/Components/watch/WatchUIContextProvider.tsx
🧠 Learnings (1)
📚 Learning: 2025-11-22T15:03:15.091Z
Learnt from: pzanella
Repo: kixelated/moq PR: 714
File: js/hang/src/watch/element.ts:469-469
Timestamp: 2025-11-22T15:03:15.091Z
Learning: In js/hang/src/watch/element.ts, the buffering overlay intentionally uses `pointerEvents: "auto"` to block clicks and prevent accidental interactions with underlying controls during buffering.

Applied to files:

  • js/hang-ui/src/Components/watch/WatchUIContextProvider.tsx
🧬 Code graph analysis (1)
js/hang-ui/src/Components/watch/WatchUIContextProvider.tsx (1)
js/hang/src/watch/element.ts (12)
  • HangWatch (23-181)
  • latency (174-176)
  • latency (178-180)
  • volume (142-144)
  • volume (146-148)
  • HangWatchInstance (185-300)
  • url (109-111)
  • url (113-115)
  • paused (134-136)
  • paused (138-140)
  • muted (150-152)
  • muted (154-156)
⏰ 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 (3)
js/hang-ui/src/Components/watch/WatchUIContextProvider.tsx (3)

7-37: Context shape and signal wiring look coherent.

Props, WatchStatus, WatchUIContextValues, and the provider’s internal signals line up cleanly with the exposed context API; the provider value matches the declared types and keeps UI state localized while delegating actual media control to the element/instance. No changes needed here.

Also applies to: 70-82, 106-106


84-104: Instance binding and listener cleanup correctly handle eager and deferred availability.

Using hangWatchEl.active.peek() to decide between immediate binding and subscribing to "watch-instance-available", plus onCleanup to remove the listener, addresses the earlier leak/race concerns and should work both when an instance already exists and when it appears later. This looks good; just ensure the demos still behave as expected when swapping/re‑creating <hang-watch> elements.


108-158: Effects for status, playback, volume, mute, buffering, and latency are well-aligned with HangWatchInstance signals.

The onWatchInstanceAvailable helpers consistently derive UI state from connection, broadcast, video, audio, and latency signals, and push into the Solid signals backing the context. The watch-status mapping covers the documented connection/broadcast states, and buffering/latency tracking aligns with the underlying element signals. No changes needed here.

Copy link

@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: 0

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

62-68: Use element property for consistency with other setters.

The other action functions (togglePlayback, setVolume, toggleMuted) consistently use element properties (paused, volume, muted), but setLatencyValue directly accesses signals.latency.set(). Since HangWatch has a latency property setter, using it would maintain consistency.

Apply this diff:

 const setLatencyValue = (latency: number) => {
 	const hangWatchEl = props.hangWatch();
 
 	if (hangWatchEl) {
-		hangWatchEl.signals.latency.set(latency as Time.Milli);
+		hangWatchEl.latency = latency;
 	}
 };

93-93: Type assertion could be more precise.

The type assertion as HangWatchInstance is misleading because peek() returns HangWatchInstance | undefined. While the code is safe due to the subsequent if (hangWatchInstance) check, removing the type assertion or using as HangWatchInstance | undefined would be more accurate.

Apply this diff:

-	const hangWatchInstance = hangWatchEl?.active?.peek?.() as HangWatchInstance;
+	const hangWatchInstance = hangWatchEl?.active?.peek?.();
📜 Review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between fa6f555 and 49ce3cf.

📒 Files selected for processing (2)
  • js/hang-ui/src/Components/publish/PublishUIContextProvider.tsx (1 hunks)
  • js/hang-ui/src/Components/watch/WatchUIContextProvider.tsx (1 hunks)
🚧 Files skipped from review as they are similar to previous changes (1)
  • js/hang-ui/src/Components/publish/PublishUIContextProvider.tsx
🧰 Additional context used
📓 Path-based instructions (1)
js/**/*.{js,ts,tsx,json}

📄 CodeRabbit inference engine (CLAUDE.md)

Use bun workspaces configured in js/package.json for JavaScript/TypeScript development

Files:

  • js/hang-ui/src/Components/watch/WatchUIContextProvider.tsx
🧠 Learnings (1)
📚 Learning: 2025-11-22T15:03:15.091Z
Learnt from: pzanella
Repo: kixelated/moq PR: 714
File: js/hang/src/watch/element.ts:469-469
Timestamp: 2025-11-22T15:03:15.091Z
Learning: In js/hang/src/watch/element.ts, the buffering overlay intentionally uses `pointerEvents: "auto"` to block clicks and prevent accidental interactions with underlying controls during buffering.

Applied to files:

  • js/hang-ui/src/Components/watch/WatchUIContextProvider.tsx
🧬 Code graph analysis (1)
js/hang-ui/src/Components/watch/WatchUIContextProvider.tsx (2)
js/hang/src/watch/element.ts (12)
  • HangWatch (23-181)
  • latency (174-176)
  • latency (178-180)
  • volume (142-144)
  • volume (146-148)
  • HangWatchInstance (185-300)
  • url (109-111)
  • url (113-115)
  • paused (134-136)
  • paused (138-140)
  • muted (150-152)
  • muted (154-156)
js/signals/src/index.ts (1)
  • event (489-504)
⏰ 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)

109-159: Well-structured effect tracking with automatic cleanup.

The onWatchInstanceAvailable function correctly sets up reactive effects to track all relevant watch state (status, playback, volume, muting, buffering, latency). The effects are automatically cleaned up when the HangWatchInstance is closed (via signals.close()), which happens when the element disconnects from the DOM.

Copy link
Owner

@kixelated kixelated left a comment

Choose a reason for hiding this comment

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

Oh and we could roll this back into @kixelated/hang if we migrate that package over to rollup as well.

},
resolve: {
alias: {
"@kixelated/hang-ui/publish/element": path.resolve(
Copy link
Owner

Choose a reason for hiding this comment

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

kinda gross, but harmless I guess. You should leave a comment that this isn't needed when using the package from NPM.

This wasn't needed when package.json pointed to src instead of dist.

},
"sideEffects": true,
"scripts": {
"build": "npm run clean && rollup -c",
Copy link
Owner

Choose a reason for hiding this comment

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

And yeah, the other packages use a script to rewrite package.json to change src to dist for publishing. Your alias approach works too.


function inlineCss() {
return {
name: 'inline-css',
Copy link
Owner

Choose a reason for hiding this comment

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

We might want to do this anyway for @kixelated/hang because of worklets. I left some thoughts in #677

},
"devDependencies": {
"@biomejs/biome": "^2.2.2",
"@kixelated/hang": "workspace:^0.7.0",
Copy link
Owner

Choose a reason for hiding this comment

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

Should this be a peerDependency?

return <PublishUIContext.Provider value={value}>{props.children}</PublishUIContext.Provider>;

function onPublishInstanceAvailable(el: HangPublish, publishInstance: HangPublishInstance) {
publishInstance?.signals.effect(function trackCameraDevices(effect) {
Copy link
Owner

Choose a reason for hiding this comment

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

you should remove the ? from all of these, it can't be null.

const [hangPublishEl, setHangPublishEl] = createSignal<HangPublish>();

const onInstanceAvailable = (event: CustomEvent) => {
const hangPublishEl = event.target as HangPublish;
Copy link
Owner

Choose a reason for hiding this comment

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

Not a fan of casting, the event should be typed.

if (publishEl) {
setHangPublishEl(publishEl);
} else {
element.addEventListener("publish-instance-available", onInstanceAvailable);
Copy link
Owner

Choose a reason for hiding this comment

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

You can use the signal instead if you want.

const dispose = element.active.watch((instance?: PublishInstance) => {

});

onCleanup(dispose);

Copy link
Owner

Choose a reason for hiding this comment

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

Also we might be able to avoid the instance being undefined. The issue is the web component lifetime; we need to start work when inserted into the DOM and stop work when removed. Toggling enabled instead might do the trick.

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