Skip to content

feat(inventory): SkinCraft 3D viewer embed - #410

Open
ykns3 wants to merge 17 commits into
masterfrom
feat/skincraft-3d-embed
Open

feat(inventory): SkinCraft 3D viewer embed#410
ykns3 wants to merge 17 commits into
masterfrom
feat/skincraft-3d-embed

Conversation

@ykns3

@ykns3 ykns3 commented Jul 28, 2026

Copy link
Copy Markdown
Collaborator

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

  • gSkinCraftEmbed is the single source of truth for the boot/load lifecycle: a versioned protocol (origin-pinned in both directions, never '*'), load correlation ids so stale progress/loaded/error events can't clobber a newer load, a 20s load timeout, and pause/resume on tab visibility.
  • Two worlds. The page-world instance reads g_ActiveInventory and 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.
  • Item identity is the masked inspect hex from asset_properties[propertyid=6] — SkinCraft resolves it client-side, so there's no Steam GC round-trip and no dependency on a float fetch.
  • SkinCraftViewerModal owns one <dialog> created once and never unmounted, since unmounting would reload the embed. It renders with lit-html's render() rather than extending FloatElement: the content script builds it, and Chrome leaves customElements null in isolated worlds, so no custom element can be defined there.
  • Inventory strip inside the modal for moving between loaded skins without closing it, with seed/float badges from the existing float cache and card colours from Steam's own rarity/background tags. Wrapped in guard() so load-progress ticks don't re-diff every card.
  • WebGPU gating (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.
  • Keyboard focus is handed to the iframe on open and on every inventory-strip selection, since the embed's hotkeys listen inside its own document.

Sandbox / security

sandbox="allow-scripts allow-same-origin allow-downloads", allow="fullscreen", referrerpolicy="no-referrer". Inbound messages are gated on event.origin and 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

  • The 3D button also appears on other users' inventories — viewing is deliberately independent of listing eligibility, unlike List on CSFloat.
  • List on CSFloat now sits in a flex row alongside the new button. That's the only part of this PR that changes existing UI rather than adding to it.
  • Clients without a WebGPU adapter see no button at all. navigator.gpu isn't Baseline — absent before Firefox 141, platform-dependent after — against a strict_min_version of 127.

Dev notes

  • Local dev points skincraft_embed_origin at a local SkinCraft dev server. The prod embed allowlist has no localhost entries, and steamcommunity.com still 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.
  • If the dev origin serves a self-signed cert, visit it top-level once and accept it first: Chrome refuses an untrusted-cert iframe silently, with no interstitial.

Rollout & testing

  • Local runs of the CI gates are green: npm run build, npm run checkformat, npm test (30 tests / 5 files), plus tsc --noEmit and madge --circular (345 files, no cycles). Node 24, matching CI.
  • Tests cover the two protocol validators, progress clamping/monotonicity, and inventory extraction. The modal is verified by hand.
  • Merges cleanly into current master (branch base is master@f882f20, unchanged).

Note

Medium Risk
Cross-origin iframe messaging and sandbox boundaries are security-sensitive, but origins and payloads are validated; main rollout risk is dependency on SkinCraft embed allowlisting and WebGPU availability on user browsers.

Overview
Adds a View in 3D flow on Steam inventory item details that opens SkinCraft in an in-page modal, gated on WebGPU and valid masked inspect hex from Steam asset properties (no float fetch required to open).

skincraft_embed_origin is added to all environment configs. gSkinCraftEmbed owns a long-lived sandboxed iframe and a versioned, origin-pinned postMessage protocol (load ids, 20s timeout, pause/resume on tab hide). The page world posts an open message plus a bounded inventory snapshot; the content script hosts the modal and iframe so Steam page scripts cannot reach the embed.

SkinCraftViewerModal is lit-html (not a custom element) with a closed shadow root, persistent iframe src, loading/error UI, and an optional inventory strip (seed/float from float cache, rarity styling). selected_item_info shows the new button beside refactored List on CSFloat in a flex row; listing rules are unchanged but extracted into canListOnCSFloat. float_fetcher.getCached supports inventory card metadata.

Tests cover embed and viewer message validation, progress handling, and inventory target extraction.

Reviewed by Cursor Bugbot for commit 0afeb89. Bugbot is set up for automated code reviews on this repo. Configure here.

ykns3 added 9 commits July 20, 2026 15:08
…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
ykns3 requested review from GODrums and Step7750 July 28, 2026 21:36
@ykns3
ykns3 marked this pull request as ready for review July 28, 2026 21:55
Comment thread src/lib/components/inventory/skincraft_viewer_modal.ts
Comment thread src/lib/services/skincraft_embed.ts
Comment thread src/lib/services/skincraft_embed.ts
Comment thread src/lib/services/skincraft_inventory_targets.ts Outdated
Comment thread src/lib/services/skincraft_embed.ts

@cursor cursor Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Cursor Bugbot has reviewed your changes using default effort and found 1 potential issue.

Fix All in Cursor

❌ Bugbot Autofix is OFF. To automatically fix reported issues with cloud agents, enable autofix in the Cursor dashboard.

Reviewed by Cursor Bugbot for commit 587fd98. Configure here.

Comment thread src/lib/services/skincraft_embed.ts
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.

1 participant