feat(inventory): SkinCraft 3D viewer embed - #410
Merged
Conversation
…SFloat First step toward embedding the SkinCraft 3D viewer in the extension (mirroring the phoenix partner-iframe integration). Adds a "View in 3D" button to the right of "List on CSFloat" in the inventory selected-item pane, laid out side-by-side via a new .market-btn-row. Styled after SkinCraft's classic-theme primary button (the viewer's top-island "Inspect" action): brand indigo fill with the white logo mark so it stands out beside the muted List button. Shares the List gating via a new canListOnCSFloat getter; the click handler is a no-op stub until the persistent-iframe embed is wired up.
Applies the review conventions from phoenix#1669 to the extension embed. - drop the export-for-test inventory-click helper and its spec - collapse the duplicate frame-reveal methods into showFrame() - share one target builder, one Steam image helper, and named inspect/hex-colour patterns instead of repeated literals - move the shared target types beside the protocol so the service no longer imports a type from a component - split the modal CSS into skincraft_viewer_modal_styles.ts - fold isSellableOnCSFloat into canListOnCSFloat and derive the brand icon from environment.skincraft_embed_origin - point the dev embed origin at localhost
Replaces the createElement tree and the manual classList toggling with a lit-html template, keeping the imperative shell the modal needs. A FloatElement is not an option here: the content script builds this modal and Chrome leaves `customElements` null in isolated worlds, so no custom element can be defined in that context. `render()` only needs the DOM. - classMap/styleMap replace hand-built class strings and style.setProperty - createRef/ref replace the stored dialog and iframe handles - guard() around the inventory grid so progress ticks don't re-diff every card - both status blocks stay mounted and toggle `hidden`, so the item icon survives an error -> retry cycle (its fade-in needs the <img> to already exist) The iframe stays unconditional with a static src, so it is created once and only diffed after that, and the embed still boots exactly once. The service's view of the modal is unchanged.
Matches phoenix's WebGpuAvailabilityService: probe with requestAdapter() rather than trusting `navigator.gpu` to exist, since a present API still fails on outdated drivers or without hardware acceleration. The status is three-state and pessimistic while checking, so the button never appears and then fails. Clients with no `navigator.gpu` at all resolve to unavailable synchronously — that covers Firefox before 141, and the manifest supports 127+. Those users simply see no 3D button, rather than sitting through the 20s embed load timeout. Adapted to this codebase: a module singleton instead of a signals service, with the probe memoised behind settled() and started on first use so no GPU work happens at import time in the content-script world, where nothing renders.
ykns3
marked this pull request as ready for review
July 28, 2026 21:55
GODrums
requested changes
Jul 31, 2026
GODrums
left a comment
Collaborator
There was a problem hiding this comment.
Looking forward to using this myself 👀
GODrums
approved these changes
Aug 3, 2026
There was a problem hiding this comment.
Cursor Bugbot has reviewed your changes using default effort and found 1 potential issue.
❌ Bugbot Autofix is OFF. To automatically fix reported issues with cloud agents, enable autofix in the Cursor dashboard.
Reviewed by Cursor Bugbot for commit f8fcbc5. Configure here.
Step7750
reviewed
Aug 3, 2026
Step7750
left a comment
Collaborator
There was a problem hiding this comment.
Apologies for the late review! Few UI/UX comments here, perhaps suitable for a follow up.
| } | ||
|
|
||
| dialog { | ||
| width: min(1120px, calc(100vw - 48px)); |
Collaborator
| ); | ||
| } | ||
|
|
||
| if (this.canListOnCSFloat || this.show3dButton) { |
Collaborator
|
|
||
| if (isSellableOnCSFloat(this.asset.description)) { | ||
| containerChildren.push(html`${this.renderListOnCSFloat()} ${this.renderFloatMarketListing()}`); | ||
| containerChildren.push(this.renderFloatMarketListing()); |
Collaborator
|
|
||
| if (isSellableOnCSFloat(this.asset.description)) { | ||
| containerChildren.push(html`${this.renderListOnCSFloat()} ${this.renderFloatMarketListing()}`); | ||
| containerChildren.push(this.renderFloatMarketListing()); |
Collaborator
There was a problem hiding this comment.
We should keep the CSFloat Market action button in a similar spot regardless of whether the item is listed or not.
Right now, the button is on the left if not listed and on the right otherwise.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.




Summary
Adds a View in 3D action to Steam inventory items, opening the SkinCraft viewer in a modal over the page. The viewer is a persistent, sandboxed cross-origin iframe driven entirely over
postMessage— the extension never re-srcs it, so switching items inside the modal is instant once warm.How it works
gSkinCraftEmbedis the single source of truth for the boot/load lifecycle: a versioned protocol (origin-pinned in both directions, never'*'), load correlation ids so staleprogress/loaded/errorevents can't clobber a newer load, a 20s load timeout, andpause/resumeon tab visibility.g_ActiveInventoryand posts the clicked item plus a bounded inventory snapshot to the content-script instance, which owns the iframe. The iframe deliberately stays out of the page world, so Steam's own scripts can't reach it, its message channel, or its closed shadow root.asset_properties[propertyid=6]— SkinCraft resolves it client-side, so there's no Steam GC round-trip and no dependency on a float fetch.SkinCraftViewerModalowns one<dialog>created once and never unmounted, since unmounting would reload the embed. It renders withlit-html'srender()rather than extendingFloatElement: the content script builds it, and Chrome leavescustomElementsnull in isolated worlds, so no custom element can be defined there.guard()so load-progress ticks don't re-diff every card.webgpu_availability.ts): adapter-only probe, so the button renders only where the viewer can actually run. Three-state and pessimistic while probing, so it never appears and then fails.Sandbox / security
sandbox="allow-scripts allow-same-origin allow-downloads",allow="fullscreen",referrerpolicy="no-referrer". Inbound messages are gated onevent.originand shape-validated at both boundaries (skincraft_embed_protocol.ts,skincraft_viewer_protocol.ts); outbound is pinned to the embed origin. The inventory snapshot is length-bounded and every field is checked before it crosses.Behaviour notes
navigator.gpuisn't Baseline — absent before Firefox 141, platform-dependent after — against astrict_min_versionof 127.Dev notes
skincraft_embed_originat a local SkinCraft dev server. The prod embed allowlist has no localhost entries, andsteamcommunity.comstill needs adding as an allowed ancestor before this works outside dev — that SkinCraft deploy ships alongside this. Without either, the modal shows its error state; nothing else is affected.Rollout & testing
npm run build,npm run checkformat,npm test(30 tests / 5 files), plustsc --noEmitandmadge --circular(345 files, no cycles). Node 24, matching CI.master@f882f20, unchanged).Note
Medium Risk
New cross-origin iframe and dual postMessage boundaries are security-sensitive but origin-validated and sandboxed; inventory UI changes are localized with protocol tests covering validators.
Overview
Adds a View in 3D flow on Steam inventory item panels that opens SkinCraft in a modal with a single persistent sandboxed iframe (no re-
srcon item switches).gSkinCraftEmbedowns the lifecycle: page world posts a validated open message + bounded inventory snapshot to the content script, which drives the embed via a versioned, origin-pinnedpostMessageprotocol (load ids, timeouts, pause/resume on tab hide).SkinCraftViewerModalis lit-html (not a custom element) with a closed shadow root, loading/error UI, and an optional inventory strip (seed/float from float cache, rarity styling). Items are keyed off Steamasset_propertiesinspect hex via newskincraft_inventory_targetshelpers;float_fetcher.getCachedexposes cache for strip badges.WebGPU is probed once (
webgpu_availability+ browser-specificwebgpu_guidance); the button stays hidden while checking, shows disabled + tooltip when unavailable, and opens the viewer when an adapter is available.skincraft_embed_originis added to all environment files. List on CSFloat is refactored into a flex row beside the new button, with the list modal rendered outside the row to avoid layout shift.Reviewed by Cursor Bugbot for commit f8fcbc5. Bugbot is set up for automated code reviews on this repo. Configure here.