From 17de5f44a5ca6be67590234b6e60bcb6a7165876 Mon Sep 17 00:00:00 2001 From: Syed Zaidi Date: Fri, 21 Aug 2026 00:10:13 -0400 Subject: [PATCH 1/8] feat(market): open listings in the skincraft 3d viewer on the market beta Adds a View in 3D launcher to every listing card in the market beta grid and gives the viewer a listing-details panel (name, wear bar, pattern, name tag, accessories, price with a native buy handoff, restrictions, and description) with prev/next navigation that pages through listings via steam's own infinite-scroll loader. --- .../common/skincraft_viewer_modal.ts | 288 +++++++++++++++++- .../common/skincraft_viewer_modal_styles.ts | 258 ++++++++++++++++ src/lib/components/market/react/listing.ts | 27 ++ src/lib/components/market/react/placement.ts | 10 +- src/lib/components/market/react/view_3d.ts | 117 +++++++ src/lib/page_scripts/market_listing.ts | 8 +- src/lib/services/skincraft_embed.ts | 142 ++++++++- .../services/skincraft_inventory_targets.ts | 2 +- .../services/skincraft_market_targets.test.ts | 184 +++++++++++ src/lib/services/skincraft_market_targets.ts | 194 ++++++++++++ .../skincraft_viewer_protocol.test.ts | 112 +++++++ src/lib/services/skincraft_viewer_protocol.ts | 160 +++++++++- 12 files changed, 1472 insertions(+), 30 deletions(-) create mode 100644 src/lib/components/market/react/view_3d.ts create mode 100644 src/lib/services/skincraft_market_targets.test.ts create mode 100644 src/lib/services/skincraft_market_targets.ts diff --git a/src/lib/components/common/skincraft_viewer_modal.ts b/src/lib/components/common/skincraft_viewer_modal.ts index 0756cf99..6c95a8d4 100644 --- a/src/lib/components/common/skincraft_viewer_modal.ts +++ b/src/lib/components/common/skincraft_viewer_modal.ts @@ -4,7 +4,7 @@ import {classMap} from 'lit/directives/class-map.js'; import {guard} from 'lit/directives/guard.js'; import {createRef, ref} from 'lit/directives/ref.js'; import {styleMap} from 'lit/directives/style-map.js'; -import type {SkinCraftViewerTarget} from '../../services/skincraft_viewer_protocol'; +import type {SkinCraftListingDetails, SkinCraftViewerTarget} from '../../services/skincraft_viewer_protocol'; import {MODAL_TRANSITION_MS, skinCraftViewerModalStyles} from './skincraft_viewer_modal_styles'; type LoadPhase = 'loading' | 'revealed' | 'error'; @@ -14,11 +14,38 @@ export type SkinCraftViewerModalOptions = { embedSrc: string; /** Heading for the item strip, e.g. "Inventory" — the modal itself is surface-agnostic. */ itemsTitle: string; + /** `grid` shows the item strip; `details` swaps it for a listing-details panel with prev/next. */ + layout?: 'grid' | 'details'; onClose: () => void; onRetry: () => void; onSelect: (target: SkinCraftViewerTarget) => void; + /** Fired when the item strip scrolls near its end, for surfaces with paginated items. */ + onItemsNearEnd?: () => void; + /** Enables the details panel's Buy button; the host hands off to the surface's purchase flow. */ + onBuy?: (target: SkinCraftViewerTarget) => void; }; +/** About two card rows — asking for more this early keeps strip scrolling seamless. */ +const ITEMS_NEAR_END_PX = 240; + +/** In details mode, prefetch the next page once selection comes within this many items of the end. */ +const ITEMS_NEAR_END_COUNT = 5; + +/** Full 0–1 wear range in the same bands the float bar uses. */ +const WEAR_SEGMENTS = [ + {width: 7, color: 'green'}, + {width: 8, color: '#18a518'}, + {width: 23, color: '#9acd32'}, + {width: 7, color: '#cd5c5c'}, + {width: 55, color: '#f92424'}, +]; + +function formatRestrictionDays(days: number): string { + if (days === 7) return 'one week'; + if (days === 1) return 'one day'; + return `${days} days`; +} + function mixHexColors(base: string, tint: string, tintAmount: number): string { const mixChannel = (offset: number): number => { const baseChannel = Number.parseInt(base.slice(offset, offset + 2), 16); @@ -57,6 +84,7 @@ export class SkinCraftViewerModal { private readonly root: ShadowRoot; private readonly dialogRef = createRef(); private readonly frameRef = createRef(); + private readonly itemGridRef = createRef(); private target?: SkinCraftViewerTarget; private items: SkinCraftViewerTarget[] = []; @@ -111,6 +139,7 @@ export class SkinCraftViewerModal { void this.revealIconWhenDecoded(target.iconUrl, request); this.scrollSelectedIntoView(); + this.maybeRequestMore(this.selectedIndex); if (opening) this.openDialog(); } @@ -147,14 +176,20 @@ export class SkinCraftViewerModal { render(this.template(), this.root, {host: this}); } + private get layout(): 'grid' | 'details' { + return this.options.layout ?? 'grid'; + } + private template(): TemplateResult { const revealed = this.phase === 'revealed'; + const details = this.layout === 'details'; return html`