diff --git a/src/lib/components/common/skincraft_viewer_modal.ts b/src/lib/components/common/skincraft_viewer_modal.ts index 0756cf99..ff4084e1 100644 --- a/src/lib/components/common/skincraft_viewer_modal.ts +++ b/src/lib/components/common/skincraft_viewer_modal.ts @@ -4,7 +4,8 @@ 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 {FLOAT_CONDITION_BANDS} from '../../utils/skin'; import {MODAL_TRANSITION_MS, skinCraftViewerModalStyles} from './skincraft_viewer_modal_styles'; type LoadPhase = 'loading' | 'revealed' | 'error'; @@ -14,11 +15,50 @@ 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 user nears the end of the loaded items — strip scroll in `grid`, selection proximity in `details`. */ + onItemsNearEnd?: () => void; + /** Enables the details panel's Buy button; the host hands off to the surface's purchase flow. */ + onBuy?: (listingId: string) => 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 = FLOAT_CONDITION_BANDS.map((band) => ({width: band.max - band.min, color: band.color})); + +function renderChevron(direction: 'left' | 'right'): TemplateResult { + return html` + + `; +} + +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,12 +97,14 @@ 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[] = []; private phase: LoadPhase = 'loading'; private progress: number | null = null; private errorMessage = ''; + private buyNotice = ''; private entering = false; private closing = false; private iconReady = false; @@ -102,6 +144,7 @@ export class SkinCraftViewerModal { show(target: SkinCraftViewerTarget): void { this.target = target; this.iconReady = false; + this.buyNotice = ''; const request = ++this.iconRequest; this.cancelClose(); @@ -111,6 +154,7 @@ export class SkinCraftViewerModal { void this.revealIconWhenDecoded(target.iconUrl, request); this.scrollSelectedIntoView(); + this.maybeRequestMore(this.selectedIndex); if (opening) this.openDialog(); } @@ -142,66 +186,45 @@ export class SkinCraftViewerModal { this.update(); } + /** Surfaces a failed buy hand-off next to the Buy button; cleared on the next selection. */ + setBuyNotice(message: string): void { + this.buyNotice = message; + this.update(); + } + // `host` binds `this` inside the template's @event handlers, the way LitElement does it. private update(): void { 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` - + ${details ? nothing : this.renderHeader()}