@skinhub/viewer 0.1.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (48) hide show
  1. package/EMBED.md +442 -0
  2. package/README.md +153 -0
  3. package/dist/SkinViewer.d.ts +18 -0
  4. package/dist/SkinViewer.d.ts.map +1 -0
  5. package/dist/SkinViewer.js +404 -0
  6. package/dist/SkinViewer.js.map +1 -0
  7. package/dist/index.d.ts +53 -0
  8. package/dist/index.d.ts.map +1 -0
  9. package/dist/index.js +51 -0
  10. package/dist/index.js.map +1 -0
  11. package/dist/item.d.ts +118 -0
  12. package/dist/item.d.ts.map +1 -0
  13. package/dist/item.js +319 -0
  14. package/dist/item.js.map +1 -0
  15. package/dist/link.d.ts +30 -0
  16. package/dist/link.d.ts.map +1 -0
  17. package/dist/link.js +18 -0
  18. package/dist/link.js.map +1 -0
  19. package/dist/protocol.d.ts +231 -0
  20. package/dist/protocol.d.ts.map +1 -0
  21. package/dist/protocol.js +128 -0
  22. package/dist/protocol.js.map +1 -0
  23. package/dist/state.d.ts +107 -0
  24. package/dist/state.d.ts.map +1 -0
  25. package/dist/state.js +351 -0
  26. package/dist/state.js.map +1 -0
  27. package/dist/types.d.ts +573 -0
  28. package/dist/types.d.ts.map +1 -0
  29. package/dist/types.js +106 -0
  30. package/dist/types.js.map +1 -0
  31. package/dist/useSkinViewer.d.ts +3 -0
  32. package/dist/useSkinViewer.d.ts.map +1 -0
  33. package/dist/useSkinViewer.js +66 -0
  34. package/dist/useSkinViewer.js.map +1 -0
  35. package/dist/weapons.d.ts +109 -0
  36. package/dist/weapons.d.ts.map +1 -0
  37. package/dist/weapons.js +260 -0
  38. package/dist/weapons.js.map +1 -0
  39. package/package.json +65 -0
  40. package/src/SkinViewer.tsx +465 -0
  41. package/src/index.ts +88 -0
  42. package/src/item.ts +373 -0
  43. package/src/link.ts +33 -0
  44. package/src/protocol.ts +241 -0
  45. package/src/state.ts +389 -0
  46. package/src/types.ts +672 -0
  47. package/src/useSkinViewer.ts +80 -0
  48. package/src/weapons.ts +284 -0
package/dist/item.d.ts ADDED
@@ -0,0 +1,118 @@
1
+ /**
2
+ * *** THE BRIDGE - an integrator's item into the frame's item, and back out again. ***
3
+ *
4
+ * This is the package's actual work. Everything else is an `<iframe>` and a message listener.
5
+ *
6
+ * ═════════════════════════════════════════════════════════════════════════════════════════════
7
+ * *** THE TWO VOCABULARIES, AND WHY WE DO NOT JUST PICK ONE. ***
8
+ *
9
+ * The frame speaks the renderer's names (`weaponType`) and the GAME's names (`sticker_id`,
10
+ * `offset_x`, `pattern`), because those are what the shader parameters, the WeaponPaints columns and
11
+ * the inspect codec all call the same six slots. That is right for the wire and wrong for a prop: an
12
+ * integrator writing `offset_x` in a React tree is writing protobuf into their view layer.
13
+ *
14
+ * So there are two, and exactly one file that knows both. A field renamed on either side breaks HERE,
15
+ * at compile time, rather than becoming a picture that is quietly missing a sticker.
16
+ *
17
+ * ═════════════════════════════════════════════════════════════════════════════════════════════
18
+ * *** AND THE PIECE THE FRAME CANNOT DO FOR ITSELF: AN INSPECT LINK'S IDENTITY. ***
19
+ *
20
+ * `?i=` carries an item's CONFIGURATION - float, seed, StatTrak, the plate, five stickers and the
21
+ * charm - and the frame decodes all of it. What it does not carry, for the frame, is WHICH WEAPON: a
22
+ * link says `defindex: 7` and the renderer wants `weapon_ak47`, and the app's own `SkinViewer`
23
+ * says so in a comment naming this package as the place the table lives:
24
+ *
25
+ * "the 63-row table between them lives in `@skinhub/viewer`'s `src/weapons.ts` - an in-flight file
26
+ * outside this app's dependency graph."
27
+ *
28
+ * Which makes `inspectLink` a first-class prop rather than a documented gap. This file resolves the
29
+ * identity here, on the host, and hands the frame both halves. An integrator never decodes a link to
30
+ * use us - That is stated as a requirement and it is the path the product exists for.
31
+ */
32
+ import { type SkinPlacement } from '@skinhub/cdn/placement';
33
+ import type { FrameItem, PlacementSlots } from './protocol.js';
34
+ import type { SkinViewerCharm, SkinViewerError, SkinViewerItem, SkinViewerSticker, ViewerSubject } from './types.js';
35
+ /**
36
+ * The public sticker list and charm as the six-slot tuple the frame validates.
37
+ *
38
+ * *** ARRAY POSITION IS THE SLOT UNLESS A STICKER NAMES ITS OWN, *** which is what makes
39
+ * `stickers={[a, null, b]}` mean slots 0 and 2 - the shape a marketplace's own UI produces when a user
40
+ * has filled two of five holders. A sticker naming a slot outside `0..4` is dropped rather than
41
+ * wrapped, because a sixth sticker slot is the charm's and putting a sticker there would be a picture
42
+ * nobody asked for.
43
+ *
44
+ * RETURNS `undefined` WHEN THERE IS NOTHING PLACED, so an item with no stickers sends no `stickers`
45
+ * key at all and cannot disturb the frame's sticker draft. See `patch.ts`.
46
+ */
47
+ export declare const toSlots: (stickers: readonly (SkinViewerSticker | null)[] | undefined, charm: SkinViewerCharm | null | undefined) => PlacementSlots | undefined;
48
+ /** The six-slot tuple back into the public shape - what `onChange` hands an integrator. */
49
+ export declare const fromSlots: (slots: PlacementSlots | undefined) => {
50
+ stickers: SkinViewerSticker[];
51
+ charm: SkinViewerCharm | null;
52
+ };
53
+ export type ResolvedSubject = {
54
+ item: FrameItem;
55
+ inspectPayload: string | null;
56
+ error: null;
57
+ }
58
+ /**
59
+ * *** NO ITEM AND NO GUESS. *** Every failure here produces a null item, and `SkinViewer` then asks
60
+ * the frame for its instruction card rather than letting it fall back to our default AK - which
61
+ * would look exactly like a successful render of the wrong gun, and is the one outcome worth more
62
+ * than all the rest of this file to avoid.
63
+ */
64
+ | {
65
+ item: null;
66
+ inspectPayload: null;
67
+ error: SkinViewerError;
68
+ };
69
+ /**
70
+ * *** THE ONE FUNCTION THE WHOLE PROP SURFACE FUNNELS THROUGH. *** Either arm of
71
+ * {@link ViewerSubject}, plus the runtime cases the types were supposed to prevent, into one item.
72
+ *
73
+ * IT IS PURE AND SYNCHRONOUS - no fetch, no React, no catalogue - which is what lets the component
74
+ * build the frame's URL in a `useState` initialiser and have the FIRST PAINT be the integrator's item
75
+ * rather than ours swapped a tick later.
76
+ */
77
+ export declare const resolveSubject: (subject: Partial<ViewerSubject>) => ResolvedSubject;
78
+ /**
79
+ * The frame's item as the public one - what {@link SkinViewerProps.onChange} hands back.
80
+ *
81
+ * *** ALWAYS THE `{ weapon }` FORM, NEVER `{ defindex }`. *** The frame reports a `weaponType`, and
82
+ * turning it back into a number is a lookup that can fail for a weapon shipped after this package
83
+ * was built. An integrator who needs the number has `defindexForWeaponId` exported for it, where the
84
+ * failure is theirs to see rather than ours to hide.
85
+ */
86
+ export declare const toPublicItem: (item: FrameItem) => SkinViewerItem;
87
+ /**
88
+ * A viewer item as a `SkinPlacement` — `@skinhub/cdn`'s decoded-inspect-link shape.
89
+ *
90
+ * Reach for this when you want the placement itself: to write WeaponPaints rows, to diff against a
91
+ * link you already hold, or to hand to another `@skinhub/cdn` helper. If you just want the link, use
92
+ * {@link toInspectLink}.
93
+ *
94
+ * *** THROWS ON A WEAPON WITH NO DEFINDEX, and that is the only honest answer. *** An inspect link
95
+ * identifies its item by number, so an id this build has no row for cannot be encoded at all. Returning
96
+ * a link with `defindex: 0` in it would produce a string that looks like a link, copies like a link and
97
+ * opens an empty CS2 inspect screen.
98
+ */
99
+ export declare const toPlacement: (item: SkinViewerItem) => SkinPlacement;
100
+ /**
101
+ * A viewer item as a masked Steam inspect link — the string a user pastes into the game.
102
+ *
103
+ * <button onClick={() => navigator.clipboard.writeText(toInspectLink(item))}>Copy inspect link</button>
104
+ *
105
+ * The inverse of passing `inspectLink` to `<SkinViewer>`, and it round-trips: a link built here decodes
106
+ * back to the same item through {@link fromInspectLink}.
107
+ */
108
+ export declare const toInspectLink: (item: SkinViewerItem) => string;
109
+ /**
110
+ * A masked inspect link as a viewer item — what `<SkinViewer inspectLink={…} />` does internally,
111
+ * exposed for a host that wants the fields rather than the picture (to seed an editor from a link, or
112
+ * to read a float out of one).
113
+ *
114
+ * *** RETURNS `null` RATHER THAN THROWING ON A LINK IT CANNOT READ, *** because the input is usually
115
+ * something a user pasted, and a paste being wrong is an ordinary event rather than an exception.
116
+ */
117
+ export declare const fromInspectLink: (link: string) => SkinViewerItem | null;
118
+ //# sourceMappingURL=item.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"item.d.ts","sourceRoot":"","sources":["../src/item.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GA8BG;AAGH,OAAO,EAAkD,KAAK,aAAa,EAAE,MAAM,wBAAwB,CAAA;AAE3G,OAAO,KAAK,EAAE,SAAS,EAAE,cAAc,EAAE,MAAM,eAAe,CAAA;AAC9D,OAAO,KAAK,EAAE,eAAe,EAAE,eAAe,EAAE,cAAc,EAAE,iBAAiB,EAAE,aAAa,EAAE,MAAM,YAAY,CAAA;AAgBpH;;;;;;;;;;;GAWG;AACH,eAAO,MAAM,OAAO,GACnB,UAAU,SAAS,CAAC,iBAAiB,GAAG,IAAI,CAAC,EAAE,GAAG,SAAS,EAC3D,OAAO,eAAe,GAAG,IAAI,GAAG,SAAS,KACvC,cAAc,GAAG,SAkCnB,CAAA;AAED,2FAA2F;AAC3F,eAAO,MAAM,SAAS,GACrB,OAAO,cAAc,GAAG,SAAS,KAC/B;IAAE,QAAQ,EAAE,iBAAiB,EAAE,CAAC;IAAC,KAAK,EAAE,eAAe,GAAG,IAAI,CAAA;CAyBhE,CAAA;AAMD,MAAM,MAAM,eAAe,GACxB;IAAE,IAAI,EAAE,SAAS,CAAC;IAAC,cAAc,EAAE,MAAM,GAAG,IAAI,CAAC;IAAC,KAAK,EAAE,IAAI,CAAA;CAAE;AACjE;;;;;GAKG;GACD;IAAE,IAAI,EAAE,IAAI,CAAC;IAAC,cAAc,EAAE,IAAI,CAAC;IAAC,KAAK,EAAE,eAAe,CAAA;CAAE,CAAA;AA+B/D;;;;;;;GAOG;AACH,eAAO,MAAM,cAAc,GAAI,SAAS,OAAO,CAAC,aAAa,CAAC,KAAG,eAwFhE,CAAA;AAED;;;;;;;GAOG;AACH,eAAO,MAAM,YAAY,GAAI,MAAM,SAAS,KAAG,cAa9C,CAAA;AAqBD;;;;;;;;;;;GAWG;AACH,eAAO,MAAM,WAAW,GAAI,MAAM,cAAc,KAAG,aAqBlD,CAAA;AAED;;;;;;;GAOG;AACH,eAAO,MAAM,aAAa,GAAI,MAAM,cAAc,KAAG,MAA4C,CAAA;AAEjG;;;;;;;GAOG;AACH,eAAO,MAAM,eAAe,GAAI,MAAM,MAAM,KAAG,cAAc,GAAG,IAG/D,CAAA"}
package/dist/item.js ADDED
@@ -0,0 +1,319 @@
1
+ /**
2
+ * *** THE BRIDGE - an integrator's item into the frame's item, and back out again. ***
3
+ *
4
+ * This is the package's actual work. Everything else is an `<iframe>` and a message listener.
5
+ *
6
+ * ═════════════════════════════════════════════════════════════════════════════════════════════
7
+ * *** THE TWO VOCABULARIES, AND WHY WE DO NOT JUST PICK ONE. ***
8
+ *
9
+ * The frame speaks the renderer's names (`weaponType`) and the GAME's names (`sticker_id`,
10
+ * `offset_x`, `pattern`), because those are what the shader parameters, the WeaponPaints columns and
11
+ * the inspect codec all call the same six slots. That is right for the wire and wrong for a prop: an
12
+ * integrator writing `offset_x` in a React tree is writing protobuf into their view layer.
13
+ *
14
+ * So there are two, and exactly one file that knows both. A field renamed on either side breaks HERE,
15
+ * at compile time, rather than becoming a picture that is quietly missing a sticker.
16
+ *
17
+ * ═════════════════════════════════════════════════════════════════════════════════════════════
18
+ * *** AND THE PIECE THE FRAME CANNOT DO FOR ITSELF: AN INSPECT LINK'S IDENTITY. ***
19
+ *
20
+ * `?i=` carries an item's CONFIGURATION - float, seed, StatTrak, the plate, five stickers and the
21
+ * charm - and the frame decodes all of it. What it does not carry, for the frame, is WHICH WEAPON: a
22
+ * link says `defindex: 7` and the renderer wants `weapon_ak47`, and the app's own `SkinViewer`
23
+ * says so in a comment naming this package as the place the table lives:
24
+ *
25
+ * "the 63-row table between them lives in `@skinhub/viewer`'s `src/weapons.ts` - an in-flight file
26
+ * outside this app's dependency graph."
27
+ *
28
+ * Which makes `inspectLink` a first-class prop rather than a documented gap. This file resolves the
29
+ * identity here, on the host, and hands the frame both halves. An integrator never decodes a link to
30
+ * use us - That is stated as a requirement and it is the path the product exists for.
31
+ */
32
+ import { buildInspectUrl, readInspectUrl } from '@skinhub/cdn/inspect';
33
+ import { emptyKeychain, emptySticker, makeSkinPlacement } from '@skinhub/cdn/placement';
34
+ import { defindexForWeaponId, normalizeWeaponId, weaponIdForDefindex } from './weapons.js';
35
+ /** `sticker_id === 0` is how the wire says "this slot is empty". */
36
+ const isPlaced = (placement) => placement.sticker_id > 0;
37
+ /** Six empty slots. Rebuilt per call because the caller mutates the copy it is handed. */
38
+ const emptySlots = () => [
39
+ emptySticker(0),
40
+ emptySticker(1),
41
+ emptySticker(2),
42
+ emptySticker(3),
43
+ emptySticker(4),
44
+ emptyKeychain(),
45
+ ];
46
+ /**
47
+ * The public sticker list and charm as the six-slot tuple the frame validates.
48
+ *
49
+ * *** ARRAY POSITION IS THE SLOT UNLESS A STICKER NAMES ITS OWN, *** which is what makes
50
+ * `stickers={[a, null, b]}` mean slots 0 and 2 - the shape a marketplace's own UI produces when a user
51
+ * has filled two of five holders. A sticker naming a slot outside `0..4` is dropped rather than
52
+ * wrapped, because a sixth sticker slot is the charm's and putting a sticker there would be a picture
53
+ * nobody asked for.
54
+ *
55
+ * RETURNS `undefined` WHEN THERE IS NOTHING PLACED, so an item with no stickers sends no `stickers`
56
+ * key at all and cannot disturb the frame's sticker draft. See `patch.ts`.
57
+ */
58
+ export const toSlots = (stickers, charm) => {
59
+ if ((!stickers || stickers.length === 0) && !charm)
60
+ return undefined;
61
+ const slots = emptySlots();
62
+ stickers?.forEach((sticker, index) => {
63
+ if (!sticker)
64
+ return;
65
+ const slot = sticker.slot ?? index;
66
+ if (slot < 0 || slot > 4)
67
+ return;
68
+ slots[slot] = {
69
+ slot,
70
+ sticker_id: sticker.id,
71
+ wear: sticker.wear ?? 0,
72
+ // The game treats an unset scale as 1 and an inspect link cannot carry `scale <= 0`. It is not
73
+ // on the public surface because nothing in CS2 sets it per sticker.
74
+ scale: 1,
75
+ rotation: sticker.rotation ?? 0,
76
+ offset_x: sticker.offsetX ?? 0,
77
+ offset_y: sticker.offsetY ?? 0,
78
+ };
79
+ });
80
+ if (charm)
81
+ slots[5] = {
82
+ slot: 0,
83
+ sticker_id: charm.id,
84
+ // `pattern` IS the charm's seed - the keychain message is the sticker message reused, so the
85
+ // field it lands in is the one a sticker calls its pattern index.
86
+ pattern: charm.seed ?? 0,
87
+ offset_x: charm.offset?.[0] ?? 0,
88
+ offset_y: charm.offset?.[1] ?? 0,
89
+ offset_z: charm.offset?.[2] ?? 0,
90
+ };
91
+ return slots;
92
+ };
93
+ /** The six-slot tuple back into the public shape - what `onChange` hands an integrator. */
94
+ export const fromSlots = (slots) => {
95
+ if (!slots)
96
+ return { stickers: [], charm: null };
97
+ const stickers = slots.slice(0, 5).flatMap((placement) => {
98
+ const sticker = placement;
99
+ if (!isPlaced(sticker))
100
+ return [];
101
+ return [
102
+ {
103
+ id: sticker.sticker_id,
104
+ slot: sticker.slot,
105
+ wear: sticker.wear,
106
+ rotation: sticker.rotation,
107
+ offsetX: sticker.offset_x,
108
+ offsetY: sticker.offset_y,
109
+ },
110
+ ];
111
+ });
112
+ const keychain = slots[5];
113
+ const charm = isPlaced(keychain)
114
+ ? {
115
+ id: keychain.sticker_id,
116
+ seed: keychain.pattern,
117
+ offset: [keychain.offset_x, keychain.offset_y, keychain.offset_z],
118
+ }
119
+ : null;
120
+ return { stickers, charm };
121
+ };
122
+ /**
123
+ * A `SkinPlacement` (whatever produced it) as the frame's item.
124
+ *
125
+ * Every field maps 1:1 with no rescaling: `g_vStickerNOffset` is `Range2(-0.5,-0.5, 0.5,0.5)` and the
126
+ * protobuf carries that range verbatim, so the offsets really are the same numbers.
127
+ */
128
+ const fromPlacement = (placement, weaponType) => {
129
+ const slots = emptySlots();
130
+ for (const sticker of placement.stickers ?? []) {
131
+ if (!isPlaced(sticker))
132
+ continue;
133
+ const slot = sticker.slot;
134
+ if (slot < 0 || slot > 4)
135
+ continue;
136
+ slots[slot] = sticker;
137
+ }
138
+ if (placement.keychain && isPlaced(placement.keychain))
139
+ slots[5] = placement.keychain;
140
+ return {
141
+ weaponType,
142
+ paintIndex: placement.paintindex,
143
+ float: placement.paintwear,
144
+ seed: placement.paintseed,
145
+ // `stattrak` present is what makes an item StatTrak and `0` is a real count - see the prop's own
146
+ // doc for why this collapses to one field rather than two.
147
+ statTrak: placement.stattrak ? (placement.stattrak_count ?? 0) : false,
148
+ nameTag: placement.nametag ?? null,
149
+ stickers: slots,
150
+ };
151
+ };
152
+ /**
153
+ * *** THE ONE FUNCTION THE WHOLE PROP SURFACE FUNNELS THROUGH. *** Either arm of
154
+ * {@link ViewerSubject}, plus the runtime cases the types were supposed to prevent, into one item.
155
+ *
156
+ * IT IS PURE AND SYNCHRONOUS - no fetch, no React, no catalogue - which is what lets the component
157
+ * build the frame's URL in a `useState` initialiser and have the FIRST PAINT be the integrator's item
158
+ * rather than ours swapped a tick later.
159
+ */
160
+ export const resolveSubject = (subject) => {
161
+ const fail = (code, message) => ({
162
+ item: null,
163
+ inspectPayload: null,
164
+ error: { code, message },
165
+ });
166
+ if (typeof subject.inspectLink === 'string' && subject.inspectLink.length > 0) {
167
+ let placement;
168
+ try {
169
+ placement = readInspectUrl(subject.inspectLink);
170
+ }
171
+ catch (cause) {
172
+ /*
173
+ * MASKED LINKS ONLY, and this is where an integrator finds that out. The `S…A…D…` / `M…`
174
+ * inventory and market forms carry no item data at all - they needed a Game Coordinator round
175
+ * trip Valve has shut down - so there is nothing in them to render and no partial answer to
176
+ * fall back to.
177
+ */
178
+ return fail('bad-inspect-link', `The inspect link did not decode: ${cause instanceof Error ? cause.message : String(cause)}. Only MASKED links (the long hex payload) carry item data; the S…A…D… and M… inventory forms needed a Game Coordinator round trip Valve has shut down and cannot be rendered by anyone.`);
179
+ }
180
+ const weaponType = weaponIdForDefindex(placement.defindex);
181
+ if (!weaponType)
182
+ return fail('unknown-weapon', `The inspect link decoded, but defindex ${placement.defindex} is not a weapon this build of @skinhub/viewer has an id for. Pass \`item={{ weapon: '…', paintIndex: ${placement.paintindex} }}\` instead, or update the package.`);
183
+ return {
184
+ item: fromPlacement(placement, weaponType),
185
+ /*
186
+ * THE ORIGINAL STRING IS KEPT AND FORWARDED AS `?i=` RATHER THAN RE-ENCODED. A round trip
187
+ * through our own encoder would be a second implementation of the codec in the URL path, free
188
+ * to disagree with the one the frame decodes with; forwarding the customer's own bytes means
189
+ * the frame reads exactly what Valve wrote.
190
+ */
191
+ inspectPayload: subject.inspectLink,
192
+ error: null,
193
+ };
194
+ }
195
+ const item = subject.item;
196
+ if (!item || typeof item !== 'object')
197
+ return fail('no-item', 'No item. <SkinViewer> needs exactly one of `inspectLink` or `item` - in TypeScript that is a compile error, so this is JavaScript, an `any`, or data that had not arrived yet.');
198
+ const weapon = item.weapon ?? (typeof item.defindex === 'number' ? weaponIdForDefindex(item.defindex) : undefined);
199
+ if (!weapon)
200
+ return typeof item.defindex === 'number'
201
+ ? fail('unknown-weapon', `defindex ${item.defindex} is not a weapon this build of @skinhub/viewer has an id for. Pass \`weapon\` instead, or update the package.`)
202
+ : fail('no-item', '`item` named no weapon. Pass either `weapon: "weapon_ak47"` (from `skin.weapon.id` on a @skinhub/cdn row) or `defindex: 7`.');
203
+ if (typeof item.paintIndex !== 'number')
204
+ return fail('no-item', '`item.paintIndex` is required. `0` is the correct value for a vanilla item and renders the bare model.');
205
+ return {
206
+ item: {
207
+ // HUD aliases folded here rather than at the frame: `skins.json` gives the twenty VANILLA knife
208
+ // rows an `sfui_wpnhud_*` id, so `row.weapon.id` off a vanilla Bayonet is a HUD string. The
209
+ // renderer resolves both to the same GLB; folding here also makes the value we echo back in
210
+ // `onChange` the item id rather than the alias.
211
+ weaponType: normalizeWeaponId(weapon),
212
+ paintIndex: item.paintIndex,
213
+ ...(item.legacyModel !== undefined && { legacyModel: item.legacyModel }),
214
+ ...(item.float !== undefined && { float: item.float }),
215
+ ...(item.seed !== undefined && { seed: item.seed }),
216
+ ...(item.statTrak !== undefined && { statTrak: item.statTrak }),
217
+ ...(item.nameTag !== undefined && { nameTag: item.nameTag }),
218
+ ...(() => {
219
+ const slots = toSlots(item.stickers, item.charm);
220
+ return slots ? { stickers: slots } : {};
221
+ })(),
222
+ },
223
+ inspectPayload: null,
224
+ error: null,
225
+ };
226
+ };
227
+ /**
228
+ * The frame's item as the public one - what {@link SkinViewerProps.onChange} hands back.
229
+ *
230
+ * *** ALWAYS THE `{ weapon }` FORM, NEVER `{ defindex }`. *** The frame reports a `weaponType`, and
231
+ * turning it back into a number is a lookup that can fail for a weapon shipped after this package
232
+ * was built. An integrator who needs the number has `defindexForWeaponId` exported for it, where the
233
+ * failure is theirs to see rather than ours to hide.
234
+ */
235
+ export const toPublicItem = (item) => {
236
+ const { stickers, charm } = fromSlots(item.stickers);
237
+ return {
238
+ weapon: item.weaponType,
239
+ paintIndex: item.paintIndex,
240
+ ...(item.legacyModel !== undefined && { legacyModel: item.legacyModel }),
241
+ ...(item.float !== undefined && { float: item.float }),
242
+ ...(item.seed !== undefined && { seed: item.seed }),
243
+ ...(item.statTrak !== undefined && { statTrak: item.statTrak }),
244
+ ...(item.nameTag !== undefined && { nameTag: item.nameTag }),
245
+ stickers,
246
+ charm,
247
+ };
248
+ };
249
+ /* ═════════════════════════════════════════════════════════════════════════════════════════════
250
+ * BACK OUT AGAIN — AN ITEM AS AN INSPECT LINK
251
+ *
252
+ * *** THE RETURN JOURNEY, AND IT IS THE HALF THAT WAS MISSING. *** Everything above turns an
253
+ * integrator's item INTO a picture. A skin picker is not finished when it can show you the item; it is
254
+ * finished when it can hand you the link. Without these two functions every integrator writes the same
255
+ * forty lines against `@skinhub/cdn`'s placement API - and writes them from the same three facts that
256
+ * are easy to get wrong and silent when you do:
257
+ *
258
+ * - `stattrak: 0` is a REAL, freshly-minted counter and `false` is no module. One boolean and one
259
+ * count, not one nullable number.
260
+ * - an unset sticker `scale` is `1`, not `0`. The WeaponPaints row default is `0` meaning "default",
261
+ * and an encoder rejects `scale <= 0`, so passing it through produces a link that will not build.
262
+ * - a charm's seed rides in `pattern`, because the keychain message is the sticker message reused.
263
+ *
264
+ * That list is the argument for these living here rather than in a docs snippet: they are the same
265
+ * three facts `toSlots` already encodes, and having them written twice is how the two copies disagree.
266
+ * ═══════════════════════════════════════════════════════════════════════════════════════════ */
267
+ /**
268
+ * A viewer item as a `SkinPlacement` — `@skinhub/cdn`'s decoded-inspect-link shape.
269
+ *
270
+ * Reach for this when you want the placement itself: to write WeaponPaints rows, to diff against a
271
+ * link you already hold, or to hand to another `@skinhub/cdn` helper. If you just want the link, use
272
+ * {@link toInspectLink}.
273
+ *
274
+ * *** THROWS ON A WEAPON WITH NO DEFINDEX, and that is the only honest answer. *** An inspect link
275
+ * identifies its item by number, so an id this build has no row for cannot be encoded at all. Returning
276
+ * a link with `defindex: 0` in it would produce a string that looks like a link, copies like a link and
277
+ * opens an empty CS2 inspect screen.
278
+ */
279
+ export const toPlacement = (item) => {
280
+ const weapon = 'weapon' in item && item.weapon ? item.weapon : undefined;
281
+ const defindex = weapon ? defindexForWeaponId(weapon) : item.defindex;
282
+ if (typeof defindex !== 'number')
283
+ throw new Error(`@skinhub/viewer: cannot build an inspect link for ${JSON.stringify(weapon)} — this build has no defindex for it. Pass \`defindex\` on the item, or update the package.`);
284
+ const slots = toSlots(item.stickers, item.charm) ?? emptySlots();
285
+ return makeSkinPlacement({
286
+ defindex,
287
+ paintindex: item.paintIndex,
288
+ paintseed: item.seed ?? 0,
289
+ paintwear: item.float ?? 0,
290
+ ...(item.nameTag ? { nametag: item.nameTag } : {}),
291
+ // `statTrak: 0` is a counter that has not counted yet. `false` is no module at all.
292
+ stattrak: item.statTrak !== undefined && item.statTrak !== false,
293
+ stattrak_count: typeof item.statTrak === 'number' ? item.statTrak : 0,
294
+ stickers: slots.slice(0, 5),
295
+ keychain: slots[5],
296
+ });
297
+ };
298
+ /**
299
+ * A viewer item as a masked Steam inspect link — the string a user pastes into the game.
300
+ *
301
+ * <button onClick={() => navigator.clipboard.writeText(toInspectLink(item))}>Copy inspect link</button>
302
+ *
303
+ * The inverse of passing `inspectLink` to `<SkinViewer>`, and it round-trips: a link built here decodes
304
+ * back to the same item through {@link fromInspectLink}.
305
+ */
306
+ export const toInspectLink = (item) => buildInspectUrl(toPlacement(item));
307
+ /**
308
+ * A masked inspect link as a viewer item — what `<SkinViewer inspectLink={…} />` does internally,
309
+ * exposed for a host that wants the fields rather than the picture (to seed an editor from a link, or
310
+ * to read a float out of one).
311
+ *
312
+ * *** RETURNS `null` RATHER THAN THROWING ON A LINK IT CANNOT READ, *** because the input is usually
313
+ * something a user pasted, and a paste being wrong is an ordinary event rather than an exception.
314
+ */
315
+ export const fromInspectLink = (link) => {
316
+ const resolved = resolveSubject({ inspectLink: link });
317
+ return resolved.item ? toPublicItem(resolved.item) : null;
318
+ };
319
+ //# sourceMappingURL=item.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"item.js","sourceRoot":"","sources":["../src/item.ts"],"names":[],"mappings":"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"}
package/dist/link.d.ts ADDED
@@ -0,0 +1,30 @@
1
+ /**
2
+ * *** THE PRIVATE CHANNEL BETWEEN THE HOOK AND THE COMPONENT. ***
3
+ *
4
+ * `useSkinViewer()` returns a handle; `<SkinViewer handle={…} />` fills it in. Something has to carry
5
+ * `reload` from the component that can do it to the object that exposes it, and carry `status` back
6
+ * the other way.
7
+ *
8
+ * *** A SYMBOL KEY RATHER THAN `__internal`, *** for one practical reason: a symbol does not appear in
9
+ * an editor's autocomplete on the handle, so the only members an integrator ever sees are the four
10
+ * documented ones. It is not hiding - anyone can reach it - it is keeping the public surface exactly
11
+ * as large as the documentation says it is.
12
+ *
13
+ * *** AND IT IS A MUTABLE OBJECT RATHER THAN A CALLBACK PROP, *** because the identity of the thing
14
+ * passed as `handle` must never change: it is a plain prop on a component that re-renders on every
15
+ * frame of a float drag, and a fresh object there would be one more thing to compare per frame.
16
+ */
17
+ import type { SkinViewerError, ViewerStatus } from './types.js';
18
+ export declare const LINK: unique symbol;
19
+ export type ViewerSnapshot = {
20
+ status: ViewerStatus;
21
+ error: SkinViewerError | null;
22
+ problems: readonly string[];
23
+ };
24
+ export type ViewerLink = {
25
+ /** Replaced by the mounted component; a no-op before mount and after unmount. */
26
+ reload: () => void;
27
+ /** The component pushing its state up. Bails when nothing moved - see `useSkinViewer`. */
28
+ publish: (snapshot: ViewerSnapshot) => void;
29
+ };
30
+ //# sourceMappingURL=link.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"link.d.ts","sourceRoot":"","sources":["../src/link.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;GAeG;AAEH,OAAO,KAAK,EAAE,eAAe,EAAE,YAAY,EAAE,MAAM,YAAY,CAAA;AAE/D,eAAO,MAAM,IAAI,EAAE,OAAO,MAAuC,CAAA;AAEjE,MAAM,MAAM,cAAc,GAAG;IAC5B,MAAM,EAAE,YAAY,CAAA;IACpB,KAAK,EAAE,eAAe,GAAG,IAAI,CAAA;IAC7B,QAAQ,EAAE,SAAS,MAAM,EAAE,CAAA;CAC3B,CAAA;AAED,MAAM,MAAM,UAAU,GAAG;IACxB,iFAAiF;IACjF,MAAM,EAAE,MAAM,IAAI,CAAA;IAClB,0FAA0F;IAC1F,OAAO,EAAE,CAAC,QAAQ,EAAE,cAAc,KAAK,IAAI,CAAA;CAC3C,CAAA"}
package/dist/link.js ADDED
@@ -0,0 +1,18 @@
1
+ /**
2
+ * *** THE PRIVATE CHANNEL BETWEEN THE HOOK AND THE COMPONENT. ***
3
+ *
4
+ * `useSkinViewer()` returns a handle; `<SkinViewer handle={…} />` fills it in. Something has to carry
5
+ * `reload` from the component that can do it to the object that exposes it, and carry `status` back
6
+ * the other way.
7
+ *
8
+ * *** A SYMBOL KEY RATHER THAN `__internal`, *** for one practical reason: a symbol does not appear in
9
+ * an editor's autocomplete on the handle, so the only members an integrator ever sees are the four
10
+ * documented ones. It is not hiding - anyone can reach it - it is keeping the public surface exactly
11
+ * as large as the documentation says it is.
12
+ *
13
+ * *** AND IT IS A MUTABLE OBJECT RATHER THAN A CALLBACK PROP, *** because the identity of the thing
14
+ * passed as `handle` must never change: it is a plain prop on a component that re-renders on every
15
+ * frame of a float drag, and a fresh object there would be one more thing to compare per frame.
16
+ */
17
+ export const LINK = Symbol('@skinhub/viewer/link');
18
+ //# sourceMappingURL=link.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"link.js","sourceRoot":"","sources":["../src/link.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;GAeG;AAIH,MAAM,CAAC,MAAM,IAAI,GAAkB,MAAM,CAAC,sBAAsB,CAAC,CAAA"}