bitsnap-react 1.15.0 → 1.16.0

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 (140) hide show
  1. package/AGENTS.md +3 -0
  2. package/README.md +76 -2
  3. package/dist/CartProvider-VGZMDVFJ.js +2 -0
  4. package/dist/CartProvider-VGZMDVFJ.js.map +1 -0
  5. package/dist/CartProvider-ZY5OLTAF.mjs +2 -0
  6. package/dist/PanelModelFiles-NYCOFPLY.js +2 -0
  7. package/dist/PanelModelFiles-NYCOFPLY.js.map +1 -0
  8. package/dist/PanelModelFiles-RV7OHUUZ.mjs +2 -0
  9. package/dist/PanelModelFiles-RV7OHUUZ.mjs.map +1 -0
  10. package/dist/Viewer3D-2IDEH6W3.mjs +2 -0
  11. package/dist/Viewer3D-2IDEH6W3.mjs.map +1 -0
  12. package/dist/analytics.js +1 -1
  13. package/dist/analytics.js.map +1 -1
  14. package/dist/analytics.mjs +1 -1
  15. package/dist/catalog.d.mts +5 -1
  16. package/dist/catalog.d.ts +5 -1
  17. package/dist/catalog.js +1 -1
  18. package/dist/catalog.js.map +1 -1
  19. package/dist/catalog.mjs +1 -1
  20. package/dist/chunk-5AYH23UJ.mjs +4 -0
  21. package/dist/chunk-5AYH23UJ.mjs.map +1 -0
  22. package/dist/chunk-5ED7FWNY.mjs +2 -0
  23. package/dist/chunk-5ED7FWNY.mjs.map +1 -0
  24. package/dist/chunk-5R6AGO3Z.mjs +2 -0
  25. package/dist/chunk-5R6AGO3Z.mjs.map +1 -0
  26. package/dist/chunk-6Q3VGPSF.js +2 -0
  27. package/dist/chunk-6Q3VGPSF.js.map +1 -0
  28. package/dist/chunk-6YIRRDCL.mjs +2 -0
  29. package/dist/chunk-6YIRRDCL.mjs.map +1 -0
  30. package/dist/chunk-7RXW6XFY.js +2 -0
  31. package/dist/chunk-7RXW6XFY.js.map +1 -0
  32. package/dist/chunk-AGYS7Z6G.mjs +2 -0
  33. package/dist/{chunk-2TLWMYXW.mjs.map → chunk-AGYS7Z6G.mjs.map} +1 -1
  34. package/dist/chunk-CUPG4B5W.mjs +2 -0
  35. package/dist/chunk-CUPG4B5W.mjs.map +1 -0
  36. package/dist/chunk-DPKWYLN7.mjs +2 -0
  37. package/dist/chunk-DPKWYLN7.mjs.map +1 -0
  38. package/dist/{chunk-MG6VV5J2.js → chunk-GQJDFLSL.js} +2 -2
  39. package/dist/chunk-GQJDFLSL.js.map +1 -0
  40. package/dist/chunk-GXUNRJ5S.js +2 -0
  41. package/dist/chunk-GXUNRJ5S.js.map +1 -0
  42. package/dist/chunk-I3QXJJ7U.mjs +2 -0
  43. package/dist/{chunk-7TLJKYZD.mjs.map → chunk-I3QXJJ7U.mjs.map} +1 -1
  44. package/dist/chunk-JSDGZ4NY.js.map +1 -1
  45. package/dist/chunk-KOYBZORL.js.map +1 -1
  46. package/dist/{chunk-XZYCQ655.js → chunk-LL7ASBGY.js} +2 -2
  47. package/dist/chunk-LL7ASBGY.js.map +1 -0
  48. package/dist/chunk-OZHXRKMX.js +2 -0
  49. package/dist/chunk-OZHXRKMX.js.map +1 -0
  50. package/dist/chunk-QERYM7RC.js +2 -0
  51. package/dist/chunk-QERYM7RC.js.map +1 -0
  52. package/dist/{chunk-PEVB3VL4.mjs → chunk-QQQ2JSPQ.mjs} +2 -2
  53. package/dist/{chunk-PEVB3VL4.mjs.map → chunk-QQQ2JSPQ.mjs.map} +1 -1
  54. package/dist/chunk-QXGK337V.mjs +2 -0
  55. package/dist/{chunk-Q3DSK66F.mjs.map → chunk-QXGK337V.mjs.map} +1 -1
  56. package/dist/{chunk-7DG4UPWE.js → chunk-SRQOFCMO.js} +2 -2
  57. package/dist/chunk-SRQOFCMO.js.map +1 -0
  58. package/dist/chunk-SULHGFYN.mjs +2 -0
  59. package/dist/chunk-SULHGFYN.mjs.map +1 -0
  60. package/dist/chunk-SXKJ26OA.mjs +2 -0
  61. package/dist/chunk-SXKJ26OA.mjs.map +1 -0
  62. package/dist/chunk-VNYHJT6V.mjs +2 -0
  63. package/dist/chunk-VNYHJT6V.mjs.map +1 -0
  64. package/dist/chunk-W3RGQYPD.js.map +1 -1
  65. package/dist/chunk-WKE6ECID.mjs +2 -0
  66. package/dist/chunk-WKE6ECID.mjs.map +1 -0
  67. package/dist/{chunk-FGB5FJCI.js → chunk-WPOXLITY.js} +2 -2
  68. package/dist/chunk-WPOXLITY.js.map +1 -0
  69. package/dist/chunk-Y5KWVTXT.js +4 -0
  70. package/dist/chunk-Y5KWVTXT.js.map +1 -0
  71. package/dist/consent.js.map +1 -1
  72. package/dist/course.js +1 -1
  73. package/dist/course.js.map +1 -1
  74. package/dist/course.mjs +1 -1
  75. package/dist/course.mjs.map +1 -1
  76. package/dist/embed/analytics.js +1 -1
  77. package/dist/errors.js.map +1 -1
  78. package/dist/index.css +1 -1
  79. package/dist/index.css.map +1 -1
  80. package/dist/index.js +1 -1
  81. package/dist/index.js.map +1 -1
  82. package/dist/index.mjs +1 -1
  83. package/dist/index.mjs.map +1 -1
  84. package/dist/internal.js.map +1 -1
  85. package/dist/methods-NJEP64AG.mjs +2 -0
  86. package/dist/methods-WRONKK3D.js +2 -0
  87. package/dist/methods-WRONKK3D.js.map +1 -0
  88. package/dist/model-files-client-HHDXJDTS.mjs +2 -0
  89. package/dist/model-files-client-HHDXJDTS.mjs.map +1 -0
  90. package/dist/model-files-client-P4PORZJ6.js +2 -0
  91. package/dist/model-files-client-P4PORZJ6.js.map +1 -0
  92. package/dist/models.d.mts +2 -0
  93. package/dist/models.d.ts +2 -0
  94. package/dist/models.js +1 -1
  95. package/dist/models.js.map +1 -1
  96. package/dist/models.mjs +1 -1
  97. package/dist/models.mjs.map +1 -1
  98. package/dist/returns.js +1 -1
  99. package/dist/returns.js.map +1 -1
  100. package/dist/returns.mjs +1 -1
  101. package/dist/viewer3d.css +2 -0
  102. package/dist/viewer3d.css.map +1 -0
  103. package/dist/viewer3d.d.mts +318 -0
  104. package/dist/viewer3d.d.ts +318 -0
  105. package/dist/viewer3d.js +2 -0
  106. package/dist/viewer3d.js.map +1 -0
  107. package/dist/viewer3d.mjs +63 -0
  108. package/dist/viewer3d.mjs.map +1 -0
  109. package/package.json +43 -16
  110. package/dist/CartProvider-64QEJ7A7.js +0 -2
  111. package/dist/CartProvider-64QEJ7A7.js.map +0 -1
  112. package/dist/CartProvider-XKK7A3C2.mjs +0 -2
  113. package/dist/chunk-2TLWMYXW.mjs +0 -2
  114. package/dist/chunk-7DG4UPWE.js.map +0 -1
  115. package/dist/chunk-7TLJKYZD.mjs +0 -2
  116. package/dist/chunk-AZQOW73L.mjs +0 -4
  117. package/dist/chunk-AZQOW73L.mjs.map +0 -1
  118. package/dist/chunk-DY2S42Q6.js +0 -2
  119. package/dist/chunk-DY2S42Q6.js.map +0 -1
  120. package/dist/chunk-FGB5FJCI.js.map +0 -1
  121. package/dist/chunk-GLAJPUMA.js +0 -2
  122. package/dist/chunk-GLAJPUMA.js.map +0 -1
  123. package/dist/chunk-H4WAQQVO.js +0 -2
  124. package/dist/chunk-H4WAQQVO.js.map +0 -1
  125. package/dist/chunk-MG6VV5J2.js.map +0 -1
  126. package/dist/chunk-Q3DSK66F.mjs +0 -2
  127. package/dist/chunk-RWEP6IUC.mjs +0 -2
  128. package/dist/chunk-RWEP6IUC.mjs.map +0 -1
  129. package/dist/chunk-S4Z4O46O.mjs +0 -2
  130. package/dist/chunk-S4Z4O46O.mjs.map +0 -1
  131. package/dist/chunk-VY6CZVVA.js +0 -4
  132. package/dist/chunk-VY6CZVVA.js.map +0 -1
  133. package/dist/chunk-XZYCQ655.js.map +0 -1
  134. package/dist/chunk-ZIWQ57GY.mjs +0 -2
  135. package/dist/chunk-ZIWQ57GY.mjs.map +0 -1
  136. package/dist/methods-NUMVR53L.js +0 -2
  137. package/dist/methods-NUMVR53L.js.map +0 -1
  138. package/dist/methods-YKZZ6OVT.mjs +0 -2
  139. /package/dist/{CartProvider-XKK7A3C2.mjs.map → CartProvider-ZY5OLTAF.mjs.map} +0 -0
  140. /package/dist/{methods-YKZZ6OVT.mjs.map → methods-NJEP64AG.mjs.map} +0 -0
package/AGENTS.md CHANGED
@@ -70,6 +70,9 @@ For Astro, omit `"use client"`, and mount with `client:load` or `client:visible`
70
70
  client before mounting. Use an effect and a ready flag for standalone buttons.
71
71
  Configure Apple Pay and test on an eligible device over HTTPS.
72
72
  - `BitsnapReturns` takes `projectID` and optional `locale`, `appearance`, `texts`.
73
+ - `Product3DViewer` (`bitsnap-react/viewer3d`, ESM only) takes `projectID`,
74
+ `productId`, optional `variantId`; it needs `three`, `@react-three/fiber` and
75
+ `@react-three/drei` installed. Never import it from the main entry.
73
76
  - For the panel, configure `setProjectID` and `setLoginURL("/panel/login")` on
74
77
  the client before mounting `<PanelProvider><Panel /></PanelProvider>`.
75
78
  Mount `<PanelProvider><PanelLogin /></PanelProvider>` at that login URL.
package/README.md CHANGED
@@ -645,7 +645,9 @@ import { ProductList, useProducts } from "bitsnap-react/catalog";
645
645
  Prices are in minor units (grosze, cents), as everywhere else in the library.
646
646
  Cards show "od {price}" when the variants differ in price, a struck-through
647
647
  regular price for discounts and an "unavailable" state for products out of
648
- stock.
648
+ stock. Products with a ready 3D model (`has3d`, sent by Bitsnap servers with
649
+ 3D support) get a small "3D" badge in the top right corner (slot
650
+ `model3dBadge`, texts `model3d` / `model3dLabel`).
649
651
 
650
652
  ### Add to cart
651
653
 
@@ -698,7 +700,7 @@ Tokens: `fontFamily`, `fontSize`, `background`, `surface`, `foreground`,
698
700
  `badgeForeground`, `radius`. Slots (`PRODUCT_LIST_SLOT_NAMES`, also exposed as
699
701
  `data-bitsnap-products-slot`): `root`, `grid`, `card`, `cardLink`, `image`,
700
702
  `imagePlaceholder`, `body`, `name`, `description`, `price`, `comparePrice`,
701
- `badge`, `actions`, `addToCartButton`, `selectButton`, `message`, `pagination`,
703
+ `badge`, `model3dBadge`, `actions`, `addToCartButton`, `selectButton`, `message`, `pagination`,
702
704
  `paginationButton`, `pageIndicator`, `emptyState`, `error`, `retryButton`,
703
705
  `skeleton`. All rules are scoped to `[data-bitsnap-products]`.
704
706
 
@@ -726,6 +728,73 @@ add it to the project's site, success or cancel URLs, or to the allowed domains
726
728
  in the Bitsnap dashboard. Otherwise the browser blocks the response. On a Bitsnap
727
729
  custom domain nothing has to be configured.
728
730
 
731
+ ## 3D product viewer (`bitsnap-react/viewer3d`)
732
+
733
+ An interactive 3D model of a product: slow auto-rotation (about one turn per
734
+ 34 s, paused while the buyer drags and when off screen or in a background tab,
735
+ off with `prefers-reduced-motion`), models over 10 MB load only after a click
736
+ ("Pokaż model 3D (24 MB)"), full screen with a buy bar, a gesture hint shown once
737
+ per browser, phones keep scrolling the page until the buyer taps the model.
738
+
739
+ The subpath is **ESM only** and needs three optional peer dependencies:
740
+
741
+ ```bash
742
+ npm install three @react-three/fiber @react-three/drei
743
+ ```
744
+
745
+ ```tsx
746
+ import { Product3DViewer } from "bitsnap-react/viewer3d";
747
+
748
+ <Product3DViewer
749
+ projectID="your-project-id"
750
+ productId="prodv2_..."
751
+ variantId={selectedVariantId} // optional; switching swaps the model in place
752
+ />;
753
+ ```
754
+
755
+ The viewer reads `GET {host}/api/integrations/{projectID}/public-commerce/products?ids={productId}&include=model3d`
756
+ (the endpoint the cart already uses) and renders `fallback` (nothing by
757
+ default) while loading, when the product or the chosen variant has no ready
758
+ model, and on servers without 3D support. three and React Three Fiber load
759
+ with `import()` only once the model may be shown, never during the server
760
+ render; the main `bitsnap-react` and `bitsnap-react/catalog` entries never
761
+ reach them, so pages without the viewer do not grow.
762
+
763
+ | Prop | Description |
764
+ | --------------- | ------------------------------------------------------------------------------------------------------------ |
765
+ | `productId` | Bitsnap product ID (or a variant ID). |
766
+ | `variantId` | The variant the page shows. A variant without its own model shows the product's. |
767
+ | `projectID` | Defaults to `setProjectID`. |
768
+ | `host` | Backend origin. Defaults to `https://bitsnap.pl` / `setCustomHost`. On a custom domain pass the domain. |
769
+ | `locale` | `pl-PL` (default) or `en*`. |
770
+ | `decoderPath` | Draco decoder folder. Default: three's decoder on jsDelivr for the installed `three` revision (see below). |
771
+ | `renderBuyBar` | `({ product, variant, close }) => ReactNode`: the bar of the full screen view. Default: name, price, "Dodaj do koszyka" (Bitsnap cart, loaded on click). |
772
+ | `productUrl` | Product page stored with the cart item by the default bar. Defaults to the current page. |
773
+ | `fallback` | Rendered when there is nothing to show. |
774
+ | `reportHref` | "Zgłoś ten model" link (DSA notice form); the viewer adds `url` with the page address. |
775
+ | `startSignal` | Bump the number to start loading now, a big model included (for your own "View in 3D" button). |
776
+ | `hideNotes` | Hide the disclaimer, report link and model facts under the viewer. |
777
+ | `texts` | Overrides of any string (`Viewer3DCopy`). |
778
+ | `appearance` | `{ background, foreground, muted, border, accent, radius }`, or set `--bitsnap-viewer3d-*` in your CSS. |
779
+
780
+ `useProduct3DModel(productId, variantId?, { projectID, host, locale })` returns
781
+ `{ status: "loading" | "ready" | "none" | "error", model, product, variant, retry }`
782
+ for your own layout, and `Model3DViewer` renders a model you already have
783
+ (`model`, `productName`, plus the props above).
784
+
785
+ **Draco decoder.** Compressed models need the Draco decoder. By default the
786
+ viewer loads three's own decoder files from
787
+ `https://cdn.jsdelivr.net/npm/three@0.<REVISION>.0/examples/jsm/libs/draco/gltf/`,
788
+ matching the `three` you installed (jsDelivr allows any origin). To self-host,
789
+ copy `node_modules/three/examples/jsm/libs/draco/gltf/` to your site and pass
790
+ `decoderPath="/draco/"`. A Content-Security-Policy needs `connect-src` for the
791
+ file host (`files.bitsnap.pl`) and the decoder host, `worker-src blob:` and
792
+ `'wasm-unsafe-eval'` (Meshopt).
793
+
794
+ **CORS.** Model files are served from `files.bitsnap.pl` with
795
+ `Access-Control-Allow-Origin: *` for model paths; the product request follows
796
+ the same rules as the product list (see below).
797
+
729
798
  # Examples
730
799
 
731
800
  1. `BitsnapCheckout` Component:
@@ -1208,6 +1277,11 @@ The panel provides the following screens:
1208
1277
  - Shows product cards with image, name, and description
1209
1278
  - Click to view product details
1210
1279
  - Supports audio, file, and ticket products
1280
+ - "Pliki 3D": purchased 3D model files (download on click with a short-lived
1281
+ link, licence, "the seller removed this file"); hidden on servers without 3D
1282
+ delivery
1283
+ - An empty list shows an empty state; only an expired session signs the buyer
1284
+ out
1211
1285
 
1212
1286
  ### Product Details
1213
1287
 
@@ -0,0 +1,2 @@
1
+ "use strict";Object.defineProperty(exports, "__esModule", {value: true});var _chunkY5KWVTXTjs = require('./chunk-Y5KWVTXT.js');require('./chunk-QERYM7RC.js');require('./chunk-KOYBZORL.js');require('./chunk-SRQOFCMO.js');require('./chunk-WPOXLITY.js');require('./chunk-GXUNRJ5S.js');require('./chunk-GQJDFLSL.js');var _chunk7RXW6XFYjs = require('./chunk-7RXW6XFY.js');require('./chunk-W3RGQYPD.js');require('./chunk-JSDGZ4NY.js');exports.MARKETING_AGREEMENT_ID = _chunkY5KWVTXTjs.L; exports.bitsnapProjectID = _chunkY5KWVTXTjs.M; exports.default = _chunkY5KWVTXTjs.T; exports.getCartLines = _chunkY5KWVTXTjs.Q; exports.getCheckoutMethods = _chunkY5KWVTXTjs.U; exports.getCheckoutSelection = _chunkY5KWVTXTjs.P; exports.getLocale = _chunk7RXW6XFYjs.f; exports.getProjectID = _chunkY5KWVTXTjs.O; exports.reconcileOrderOfLastPayment = _chunkY5KWVTXTjs.R; exports.setLocale = _chunk7RXW6XFYjs.e; exports.setProjectID = _chunkY5KWVTXTjs.N; exports.useCartProvider = _chunkY5KWVTXTjs.S;
2
+ //# sourceMappingURL=CartProvider-VGZMDVFJ.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["/home/emems/web/bitsnap/bitsnap/bitsnap-react/dist/CartProvider-VGZMDVFJ.js"],"names":[],"mappings":"AAAA,+HAAsG,+BAA4B,+BAA4B,+BAA4B,+BAA4B,+BAA4B,+BAA4B,sDAA+C,+BAA4B,+BAA4B,siBAAmQ","file":"/home/emems/web/bitsnap/bitsnap/bitsnap-react/dist/CartProvider-VGZMDVFJ.js"}
@@ -0,0 +1,2 @@
1
+ import{L as c,M as d,N as e,O as f,P as g,Q as h,R as i,S as j,T as k,U as l}from"./chunk-5AYH23UJ.mjs";import"./chunk-QXGK337V.mjs";import"./chunk-KOVKUJAW.mjs";import"./chunk-SULHGFYN.mjs";import"./chunk-CUPG4B5W.mjs";import"./chunk-AGYS7Z6G.mjs";import"./chunk-5R6AGO3Z.mjs";import"./chunk-SXKJ26OA.mjs";import"./chunk-QQQ2JSPQ.mjs";import{e as a,f as b}from"./chunk-VNYHJT6V.mjs";import"./chunk-MDCP5JNP.mjs";import"./chunk-U6FLIRIV.mjs";export{c as MARKETING_AGREEMENT_ID,d as bitsnapProjectID,k as default,h as getCartLines,l as getCheckoutMethods,g as getCheckoutSelection,b as getLocale,f as getProjectID,i as reconcileOrderOfLastPayment,a as setLocale,e as setProjectID,j as useCartProvider};
2
+ //# sourceMappingURL=CartProvider-ZY5OLTAF.mjs.map
@@ -0,0 +1,2 @@
1
+ "use strict";Object.defineProperty(exports, "__esModule", {value: true}); function _nullishCoalesce(lhs, rhsFn) { if (lhs != null) { return lhs; } else { return rhsFn(); } } function _optionalChain(ops) { let lastAccessLHS = undefined; let value = ops[0]; let i = 1; while (i < ops.length) { const op = ops[i]; const fn = ops[i + 1]; i += 2; if ((op === 'optionalAccess' || op === 'optionalCall') && value == null) { return undefined; } if (op === 'access' || op === 'optionalAccess') { lastAccessLHS = value; value = fn(value); } else if (op === 'call' || op === 'optionalCall') { value = fn((...args) => value.call(lastAccessLHS, ...args)); lastAccessLHS = undefined; } } return value; }var _chunk6Q3VGPSFjs = require('./chunk-6Q3VGPSF.js');var _chunk7RXW6XFYjs = require('./chunk-7RXW6XFY.js');require('./chunk-JSDGZ4NY.js');var _connect = require('@connectrpc/connect');var _connectquery = require('@connectrpc/connect-query');var _react = require('react');var _jsxruntime = require('react/jsx-runtime');var k={pl:{title:"Pliki 3D",subtitle:"Pobierasz zawsze aktualn\u0105 wersj\u0119 pliku.",download:"Pobierz",preparing:"Przygotowuj\u0119\u2026",licence:"Licencja",purchased:"Kupiono",accessUntil:"Dost\u0119p do",removed:"Sprzedawca usun\u0105\u0142 ten plik.",contact:"Napisz do sprzedawcy",expired:"Dost\u0119p do pliku wygas\u0142",failure:{1:"Sesja wygas\u0142a. Zaloguj si\u0119 ponownie.",2:"Nie znale\u017Ali\u015Bmy tego pliku.",3:"Zam\xF3wienie zosta\u0142o zwr\xF3cone lub anulowane.",4:"Dost\u0119p do pliku wygas\u0142.",5:"Sprzedawca usun\u0105\u0142 ten plik.",6:"Zam\xF3wienie nie jest op\u0142acone."},error:"Nie uda\u0142o si\u0119 pobra\u0107 pliku. Spr\xF3buj ponownie."},en:{title:"3D files",subtitle:"You always download the current version of the file.",download:"Download",preparing:"Preparing\u2026",licence:"Licence",purchased:"Purchased",accessUntil:"Access until",removed:"The seller removed this file.",contact:"Contact the seller",expired:"Access to the file has ended",failure:{1:"Your session has expired. Sign in again.",2:"We could not find this file.",3:"The order was refunded or canceled.",4:"Access to the file has ended.",5:"The seller removed this file.",6:"The order is not paid."},error:"The file could not be downloaded. Try again."}};function A(i,o){let c=Number(i);if(!Number.isFinite(c)||c<=0)return"";let l=c/(1024*1024),t=new Intl.NumberFormat(o?"en-GB":"pl-PL",{maximumFractionDigits:1});return l>=1?`${t.format(l)} MB`:`${t.format(c/1024)} KB`}function m(i,o){return i==null||i<=0n?"":new Intl.DateTimeFormat(o?"en-GB":"pl-PL",{dateStyle:"medium"}).format(new Date(Number(i)*1e3))}function U({className:i}){return _jsxruntime.jsxs.call(void 0, "svg",{viewBox:"0 0 24 24",fill:"none",stroke:"currentColor",strokeWidth:1.5,className:i,"aria-hidden":!0,children:[_jsxruntime.jsx.call(void 0, "path",{d:"M12 2.5 3.5 7v10L12 21.5 20.5 17V7L12 2.5Z",strokeLinejoin:"round"}),_jsxruntime.jsx.call(void 0, "path",{d:"M3.5 7 12 11.5 20.5 7M12 11.5v10",strokeLinejoin:"round"})]})}function M({accessToken:i,models:o,buttonStyle:c}){let l=_chunk7RXW6XFYjs.f.call(void 0, ).toLowerCase().startsWith("en"),t=l?k.en:k.pl,f=_connectquery.useTransport.call(void 0, ),g=_react.useMemo.call(void 0, ()=>_connect.createClient.call(void 0, _chunk6Q3VGPSFjs.b,f),[f]),[x,b]=_react.useState.call(void 0, null),[h,p]=_react.useState.call(void 0, {}),D=_react.useCallback.call(void 0, async(e,d)=>{let s=`${e.orderId}:${e.lineId}:${d}`;b(s),p(a=>({...a,[s]:""}));try{let a=await g.getModelFileDownloadUrl({accessToken:i,orderId:e.orderId,lineId:e.lineId,fileIndex:d});if(a.result.case==="success")window.location.assign(a.result.value.url);else{let u=a.result.case==="failure"?a.result.value:void 0;p(E=>({...E,[s]:_nullishCoalesce((u!=null?t.failure[u]:void 0), () => (t.error))}))}}catch (e2){p(a=>({...a,[s]:t.error}))}finally{b(null)}},[g,i,t]);return o.length===0?null:_jsxruntime.jsx.call(void 0, "section",{"aria-labelledby":"bitsnap-panel-model-files",className:"mt-10 w-full max-w-5xl","data-testid":"model-files",children:_jsxruntime.jsxs.call(void 0, "div",{className:"rounded-2xl border border-neutral-200 bg-white dark:border-neutral-800 dark:bg-neutral-900",children:[_jsxruntime.jsxs.call(void 0, "header",{className:"border-b border-neutral-200 px-5 py-4 dark:border-neutral-800",children:[_jsxruntime.jsx.call(void 0, "h2",{id:"bitsnap-panel-model-files",className:"text-lg font-semibold text-neutral-900 dark:text-neutral-50",children:t.title}),_jsxruntime.jsx.call(void 0, "p",{className:"mt-0.5 text-sm text-neutral-500 dark:text-neutral-400",children:t.subtitle})]}),_jsxruntime.jsx.call(void 0, "ul",{className:"divide-y divide-neutral-200 dark:divide-neutral-800",children:o.map(e=>{let d=e.variantName?`${e.productName} \u2013 ${e.variantName}`:e.productName;return _jsxruntime.jsxs.call(void 0, "li",{className:"flex flex-col gap-3 px-5 py-4 sm:flex-row sm:items-center","data-state":_chunk6Q3VGPSFjs.a[e.state],children:[_jsxruntime.jsxs.call(void 0, "div",{className:"flex min-w-0 flex-1 items-center gap-4",children:[_jsxruntime.jsx.call(void 0, "div",{className:"flex size-14 shrink-0 items-center justify-center overflow-hidden rounded-xl bg-neutral-100 text-neutral-400 dark:bg-neutral-800",children:e.thumbnailUrl?_jsxruntime.jsx.call(void 0, "img",{src:e.thumbnailUrl,alt:"",className:"size-full object-contain",loading:"lazy"}):_jsxruntime.jsx.call(void 0, U,{className:"size-7"})}),_jsxruntime.jsxs.call(void 0, "div",{className:"min-w-0",children:[_jsxruntime.jsx.call(void 0, "p",{className:"truncate font-medium text-neutral-900 dark:text-neutral-50",children:d}),_jsxruntime.jsx.call(void 0, "p",{className:"mt-0.5 text-sm text-neutral-500 dark:text-neutral-400",children:[e.files[0]?`${e.files[0].format.toUpperCase()} \xB7 ${A(e.files[0].fileSize,l)}`:"",`${t.purchased} ${m(e.purchasedAt,l)}`,e.accessExpiresAt!=null&&e.state===1?`${t.accessUntil} ${m(e.accessExpiresAt,l)}`:""].filter(Boolean).join(" \xB7 ")}),e.licence&&_jsxruntime.jsxs.call(void 0, "p",{className:"mt-0.5 text-sm text-neutral-600 dark:text-neutral-300",children:[t.licence,":"," ",e.licence.url?_jsxruntime.jsx.call(void 0, "a",{href:e.licence.url,target:"_blank",rel:"noopener noreferrer",className:"underline underline-offset-2 hover:text-neutral-900 dark:hover:text-neutral-50",children:e.licence.name}):e.licence.name]}),e.state===2&&_jsxruntime.jsxs.call(void 0, "p",{className:"mt-1 text-sm text-neutral-700 dark:text-neutral-200","data-model-removed":"",children:[t.removed,_optionalChain([e, 'access', _ => _.seller, 'optionalAccess', _2 => _2.email])&&_jsxruntime.jsxs.call(void 0, _jsxruntime.Fragment,{children:[" ",t.contact,e.seller.name?` (${e.seller.name})`:"",":"," ",_jsxruntime.jsx.call(void 0, "a",{className:"font-medium underline underline-offset-2",href:`mailto:${e.seller.email}`,children:e.seller.email})]})]}),e.state===3&&_jsxruntime.jsxs.call(void 0, "p",{className:"mt-1 text-sm text-neutral-700 dark:text-neutral-200",children:[t.expired,e.accessExpiresAt!=null?` ${m(e.accessExpiresAt,l)}`:"","."]})]})]}),e.state===1&&e.files.map(s=>{let a=`${e.orderId}:${e.lineId}:${s.index}`,u=x===a;return _jsxruntime.jsxs.call(void 0, "div",{className:"flex flex-col items-start gap-1 sm:items-end",children:[_jsxruntime.jsx.call(void 0, "button",{type:"button",onClick:()=>void D(e,s.index),disabled:x!=null,"aria-busy":u||void 0,"aria-label":`${t.download}: ${s.fileName}`,"data-model-download":"",style:c,className:"inline-flex h-10 items-center justify-center rounded-lg bg-neutral-900 px-4 text-sm font-medium text-white transition-opacity hover:opacity-90 disabled:opacity-60 dark:bg-neutral-50 dark:text-neutral-900",children:u?t.preparing:t.download}),h[a]&&_jsxruntime.jsx.call(void 0, "p",{className:"text-sm text-red-700 dark:text-red-400",role:"alert",children:h[a]})]},a)})]},`${e.orderId}:${e.lineId}`)})})]})})}exports.default = M;
2
+ //# sourceMappingURL=PanelModelFiles-NYCOFPLY.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["/home/emems/web/bitsnap/bitsnap/bitsnap-react/dist/PanelModelFiles-NYCOFPLY.js","../src/panel/components/PanelModelFiles.tsx"],"names":["COPY","formatSize","bytes","en","value","mb","fmt"],"mappings":"AAAA,uuBAA8C,sDAAwC,+BAA4B,8CCArF,yDACA,8BACkB,+CAsG3C,IAhEEA,CAAAA,CAA+B,CACnC,EAAA,CAAI,CACF,KAAA,CAAO,UAAA,CACP,QAAA,CAAU,mDAAA,CACV,QAAA,CAAU,SAAA,CACV,SAAA,CAAW,yBAAA,CACX,OAAA,CAAS,UAAA,CACT,SAAA,CAAW,SAAA,CACX,WAAA,CAAa,gBAAA,CACb,OAAA,CAAS,uCAAA,CACT,OAAA,CAAS,sBAAA,CACT,OAAA,CAAS,kCAAA,CACT,OAAA,CAAS,CACN,CAAA,CAAoC,gDAAA,CACpC,CAAA,CAAiC,uCAAA,CACjC,CAAA,CAAgC,uDAAA,CAChC,CAAA,CAA+B,mCAAA,CAC/B,CAAA,CAA+B,uCAAA,CAC/B,CAAA,CAA8B,uCACjC,CAAA,CACA,KAAA,CAAO,iEACT,CAAA,CACA,EAAA,CAAI,CACF,KAAA,CAAO,UAAA,CACP,QAAA,CAAU,sDAAA,CACV,QAAA,CAAU,UAAA,CACV,SAAA,CAAW,iBAAA,CACX,OAAA,CAAS,SAAA,CACT,SAAA,CAAW,WAAA,CACX,WAAA,CAAa,cAAA,CACb,OAAA,CAAS,+BAAA,CACT,OAAA,CAAS,oBAAA,CACT,OAAA,CAAS,8BAAA,CACT,OAAA,CAAS,CACN,CAAA,CAAoC,0CAAA,CACpC,CAAA,CAAiC,8BAAA,CACjC,CAAA,CAAgC,qCAAA,CAChC,CAAA,CAA+B,+BAAA,CAC/B,CAAA,CAA+B,+BAAA,CAC/B,CAAA,CAA8B,wBACjC,CAAA,CACA,KAAA,CAAO,8CACT,CACF,CAAA,CAEA,SAASC,CAAAA,CAAWC,CAAAA,CAAeC,CAAAA,CAAqB,CACtD,IAAMC,CAAAA,CAAQ,MAAA,CAAOF,CAAK,CAAA,CAC1B,EAAA,CAAI,CAAC,MAAA,CAAO,QAAA,CAASE,CAAK,CAAA,EAAKA,CAAAA,EAAS,CAAA,CAAG,MAAO,EAAA,CAClD,IAAMC,CAAAA,CAAKD,CAAAA,CAAAA,CAAS,IAAA,CAAO,IAAA,CAAA,CACrBE,CAAAA,CAAM,IAAI,IAAA,CAAK,YAAA,CAAaH,CAAAA,CAAK,OAAA,CAAU,OAAA,CAAS,CACxD,qBAAA,CAAuB,CACzB,CAAC,CAAA,CACD,OAAOE,CAAAA,EAAM,CAAA,CAAI,CAAA,EAAA","file":"/home/emems/web/bitsnap/bitsnap/bitsnap-react/dist/PanelModelFiles-NYCOFPLY.js","sourcesContent":[null,"import { createClient } from \"@connectrpc/connect\";\nimport { useTransport } from \"@connectrpc/connect-query\";\nimport { useCallback, useMemo, useState } from \"react\";\n\nimport { getLocale } from \"../../components/checkout/locale\";\nimport {\n ModelDeliveryFailure,\n ModelDeliveryService,\n ModelFileState,\n type PurchasedModel,\n} from \"../../gen/proto/public/v1/model_delivery_pb\";\n\n/**\n * \"3D files\" of the customer panel: the 3D files of the buyer's paid orders in\n * this shop (`public.v1.ModelDeliveryService`), with a download button, the\n * licence and the \"seller removed the file\" state. The download URL is signed\n * on click and expires quickly, so it is never rendered into the page.\n *\n * Older Bitsnap servers have no such service: any transport error (404,\n * unimplemented, network) hides the section, so the panel looks as before.\n *\n * Loaded on demand (with `model-files-client.ts`) only when the buyer has 3D\n * files, so the main bundle carries just `model-files-load.ts`.\n */\n\ntype Copy = {\n title: string;\n subtitle: string;\n download: string;\n preparing: string;\n licence: string;\n purchased: string;\n accessUntil: string;\n removed: string;\n contact: string;\n expired: string;\n failure: Partial<Record<ModelDeliveryFailure, string>>;\n error: string;\n};\n\nconst COPY: { pl: Copy; en: Copy } = {\n pl: {\n title: \"Pliki 3D\",\n subtitle: \"Pobierasz zawsze aktualną wersję pliku.\",\n download: \"Pobierz\",\n preparing: \"Przygotowuję…\",\n licence: \"Licencja\",\n purchased: \"Kupiono\",\n accessUntil: \"Dostęp do\",\n removed: \"Sprzedawca usunął ten plik.\",\n contact: \"Napisz do sprzedawcy\",\n expired: \"Dostęp do pliku wygasł\",\n failure: {\n [ModelDeliveryFailure.UNAUTHORIZED]: \"Sesja wygasła. Zaloguj się ponownie.\",\n [ModelDeliveryFailure.NOT_FOUND]: \"Nie znaleźliśmy tego pliku.\",\n [ModelDeliveryFailure.REFUNDED]: \"Zamówienie zostało zwrócone lub anulowane.\",\n [ModelDeliveryFailure.EXPIRED]: \"Dostęp do pliku wygasł.\",\n [ModelDeliveryFailure.REMOVED]: \"Sprzedawca usunął ten plik.\",\n [ModelDeliveryFailure.UNPAID]: \"Zamówienie nie jest opłacone.\",\n },\n error: \"Nie udało się pobrać pliku. Spróbuj ponownie.\",\n },\n en: {\n title: \"3D files\",\n subtitle: \"You always download the current version of the file.\",\n download: \"Download\",\n preparing: \"Preparing…\",\n licence: \"Licence\",\n purchased: \"Purchased\",\n accessUntil: \"Access until\",\n removed: \"The seller removed this file.\",\n contact: \"Contact the seller\",\n expired: \"Access to the file has ended\",\n failure: {\n [ModelDeliveryFailure.UNAUTHORIZED]: \"Your session has expired. Sign in again.\",\n [ModelDeliveryFailure.NOT_FOUND]: \"We could not find this file.\",\n [ModelDeliveryFailure.REFUNDED]: \"The order was refunded or canceled.\",\n [ModelDeliveryFailure.EXPIRED]: \"Access to the file has ended.\",\n [ModelDeliveryFailure.REMOVED]: \"The seller removed this file.\",\n [ModelDeliveryFailure.UNPAID]: \"The order is not paid.\",\n },\n error: \"The file could not be downloaded. Try again.\",\n },\n};\n\nfunction formatSize(bytes: bigint, en: boolean): string {\n const value = Number(bytes);\n if (!Number.isFinite(value) || value <= 0) return \"\";\n const mb = value / (1024 * 1024);\n const fmt = new Intl.NumberFormat(en ? \"en-GB\" : \"pl-PL\", {\n maximumFractionDigits: 1,\n });\n return mb >= 1 ? `${fmt.format(mb)} MB` : `${fmt.format(value / 1024)} KB`;\n}\n\nfunction formatDate(seconds: bigint | undefined, en: boolean): string {\n if (seconds == null || seconds <= 0n) return \"\";\n return new Intl.DateTimeFormat(en ? \"en-GB\" : \"pl-PL\", {\n dateStyle: \"medium\",\n }).format(new Date(Number(seconds) * 1000));\n}\n\nfunction CubeIcon({ className }: { className?: string }) {\n return (\n <svg\n viewBox=\"0 0 24 24\"\n fill=\"none\"\n stroke=\"currentColor\"\n strokeWidth={1.5}\n className={className}\n aria-hidden\n >\n <path d=\"M12 2.5 3.5 7v10L12 21.5 20.5 17V7L12 2.5Z\" strokeLinejoin=\"round\" />\n <path d=\"M3.5 7 12 11.5 20.5 7M12 11.5v10\" strokeLinejoin=\"round\" />\n </svg>\n );\n}\n\nexport default function PanelModelFiles({\n accessToken,\n models,\n buttonStyle,\n}: {\n accessToken: string;\n models: PurchasedModel[];\n buttonStyle?: React.CSSProperties;\n}) {\n const en = getLocale().toLowerCase().startsWith(\"en\");\n const t = en ? COPY.en : COPY.pl;\n const transport = useTransport();\n const client = useMemo(\n () => createClient(ModelDeliveryService, transport),\n [transport],\n );\n const [pending, setPending] = useState<string | null>(null);\n const [errors, setErrors] = useState<Record<string, string>>({});\n\n const download = useCallback(\n async (model: PurchasedModel, index: number) => {\n const key = `${model.orderId}:${model.lineId}:${index}`;\n setPending(key);\n setErrors((current) => ({ ...current, [key]: \"\" }));\n try {\n const response = await client.getModelFileDownloadUrl({\n accessToken,\n orderId: model.orderId,\n lineId: model.lineId,\n fileIndex: index,\n });\n if (response.result.case === \"success\") {\n // An attachment: the browser downloads it and stays on the panel.\n window.location.assign(response.result.value.url);\n } else {\n const failure =\n response.result.case === \"failure\" ? response.result.value : undefined;\n setErrors((current) => ({\n ...current,\n [key]: (failure != null ? t.failure[failure] : undefined) ?? t.error,\n }));\n }\n } catch {\n setErrors((current) => ({ ...current, [key]: t.error }));\n } finally {\n setPending(null);\n }\n },\n [client, accessToken, t],\n );\n\n if (models.length === 0) return null;\n\n return (\n <section\n aria-labelledby=\"bitsnap-panel-model-files\"\n className=\"mt-10 w-full max-w-5xl\"\n data-testid=\"model-files\"\n >\n <div className=\"rounded-2xl border border-neutral-200 bg-white dark:border-neutral-800 dark:bg-neutral-900\">\n <header className=\"border-b border-neutral-200 px-5 py-4 dark:border-neutral-800\">\n <h2\n id=\"bitsnap-panel-model-files\"\n className=\"text-lg font-semibold text-neutral-900 dark:text-neutral-50\"\n >\n {t.title}\n </h2>\n <p className=\"mt-0.5 text-sm text-neutral-500 dark:text-neutral-400\">\n {t.subtitle}\n </p>\n </header>\n <ul className=\"divide-y divide-neutral-200 dark:divide-neutral-800\">\n {models.map((model) => {\n const title = model.variantName\n ? `${model.productName} – ${model.variantName}`\n : model.productName;\n return (\n <li\n key={`${model.orderId}:${model.lineId}`}\n className=\"flex flex-col gap-3 px-5 py-4 sm:flex-row sm:items-center\"\n data-state={ModelFileState[model.state]}\n >\n <div className=\"flex min-w-0 flex-1 items-center gap-4\">\n <div className=\"flex size-14 shrink-0 items-center justify-center overflow-hidden rounded-xl bg-neutral-100 text-neutral-400 dark:bg-neutral-800\">\n {model.thumbnailUrl ? (\n <img\n src={model.thumbnailUrl}\n alt=\"\"\n className=\"size-full object-contain\"\n loading=\"lazy\"\n />\n ) : (\n <CubeIcon className=\"size-7\" />\n )}\n </div>\n <div className=\"min-w-0\">\n <p className=\"truncate font-medium text-neutral-900 dark:text-neutral-50\">\n {title}\n </p>\n <p className=\"mt-0.5 text-sm text-neutral-500 dark:text-neutral-400\">\n {[\n model.files[0]\n ? `${model.files[0].format.toUpperCase()} · ${formatSize(model.files[0].fileSize, en)}`\n : \"\",\n `${t.purchased} ${formatDate(model.purchasedAt, en)}`,\n model.accessExpiresAt != null &&\n model.state === ModelFileState.AVAILABLE\n ? `${t.accessUntil} ${formatDate(model.accessExpiresAt, en)}`\n : \"\",\n ]\n .filter(Boolean)\n .join(\" · \")}\n </p>\n {model.licence && (\n <p className=\"mt-0.5 text-sm text-neutral-600 dark:text-neutral-300\">\n {t.licence}:{\" \"}\n {model.licence.url ? (\n <a\n href={model.licence.url}\n target=\"_blank\"\n rel=\"noopener noreferrer\"\n className=\"underline underline-offset-2 hover:text-neutral-900 dark:hover:text-neutral-50\"\n >\n {model.licence.name}\n </a>\n ) : (\n model.licence.name\n )}\n </p>\n )}\n {model.state === ModelFileState.REMOVED && (\n <p\n className=\"mt-1 text-sm text-neutral-700 dark:text-neutral-200\"\n data-model-removed=\"\"\n >\n {t.removed}\n {model.seller?.email && (\n <>\n {\" \"}\n {t.contact}\n {model.seller.name ? ` (${model.seller.name})` : \"\"}:{\" \"}\n <a\n className=\"font-medium underline underline-offset-2\"\n href={`mailto:${model.seller.email}`}\n >\n {model.seller.email}\n </a>\n </>\n )}\n </p>\n )}\n {model.state === ModelFileState.EXPIRED && (\n <p className=\"mt-1 text-sm text-neutral-700 dark:text-neutral-200\">\n {t.expired}\n {model.accessExpiresAt != null\n ? ` ${formatDate(model.accessExpiresAt, en)}`\n : \"\"}\n .\n </p>\n )}\n </div>\n </div>\n {model.state === ModelFileState.AVAILABLE &&\n model.files.map((file) => {\n const key = `${model.orderId}:${model.lineId}:${file.index}`;\n const busy = pending === key;\n return (\n <div\n key={key}\n className=\"flex flex-col items-start gap-1 sm:items-end\"\n >\n <button\n type=\"button\"\n onClick={() => void download(model, file.index)}\n disabled={pending != null}\n aria-busy={busy || undefined}\n aria-label={`${t.download}: ${file.fileName}`}\n data-model-download=\"\"\n style={buttonStyle}\n className=\"inline-flex h-10 items-center justify-center rounded-lg bg-neutral-900 px-4 text-sm font-medium text-white transition-opacity hover:opacity-90 disabled:opacity-60 dark:bg-neutral-50 dark:text-neutral-900\"\n >\n {busy ? t.preparing : t.download}\n </button>\n {errors[key] && (\n <p\n className=\"text-sm text-red-700 dark:text-red-400\"\n role=\"alert\"\n >\n {errors[key]}\n </p>\n )}\n </div>\n );\n })}\n </li>\n );\n })}\n </ul>\n </div>\n </section>\n );\n}\n"]}
@@ -0,0 +1,2 @@
1
+ import{a as w,b as v}from"./chunk-6YIRRDCL.mjs";import{f as N}from"./chunk-VNYHJT6V.mjs";import"./chunk-U6FLIRIV.mjs";import{createClient as I}from"@connectrpc/connect";import{useTransport as $}from"@connectrpc/connect-query";import{useCallback as P,useMemo as z,useState as y}from"react";import{Fragment as L,jsx as r,jsxs as n}from"react/jsx-runtime";var k={pl:{title:"Pliki 3D",subtitle:"Pobierasz zawsze aktualn\u0105 wersj\u0119 pliku.",download:"Pobierz",preparing:"Przygotowuj\u0119\u2026",licence:"Licencja",purchased:"Kupiono",accessUntil:"Dost\u0119p do",removed:"Sprzedawca usun\u0105\u0142 ten plik.",contact:"Napisz do sprzedawcy",expired:"Dost\u0119p do pliku wygas\u0142",failure:{1:"Sesja wygas\u0142a. Zaloguj si\u0119 ponownie.",2:"Nie znale\u017Ali\u015Bmy tego pliku.",3:"Zam\xF3wienie zosta\u0142o zwr\xF3cone lub anulowane.",4:"Dost\u0119p do pliku wygas\u0142.",5:"Sprzedawca usun\u0105\u0142 ten plik.",6:"Zam\xF3wienie nie jest op\u0142acone."},error:"Nie uda\u0142o si\u0119 pobra\u0107 pliku. Spr\xF3buj ponownie."},en:{title:"3D files",subtitle:"You always download the current version of the file.",download:"Download",preparing:"Preparing\u2026",licence:"Licence",purchased:"Purchased",accessUntil:"Access until",removed:"The seller removed this file.",contact:"Contact the seller",expired:"Access to the file has ended",failure:{1:"Your session has expired. Sign in again.",2:"We could not find this file.",3:"The order was refunded or canceled.",4:"Access to the file has ended.",5:"The seller removed this file.",6:"The order is not paid."},error:"The file could not be downloaded. Try again."}};function A(i,o){let c=Number(i);if(!Number.isFinite(c)||c<=0)return"";let l=c/(1024*1024),t=new Intl.NumberFormat(o?"en-GB":"pl-PL",{maximumFractionDigits:1});return l>=1?`${t.format(l)} MB`:`${t.format(c/1024)} KB`}function m(i,o){return i==null||i<=0n?"":new Intl.DateTimeFormat(o?"en-GB":"pl-PL",{dateStyle:"medium"}).format(new Date(Number(i)*1e3))}function U({className:i}){return n("svg",{viewBox:"0 0 24 24",fill:"none",stroke:"currentColor",strokeWidth:1.5,className:i,"aria-hidden":!0,children:[r("path",{d:"M12 2.5 3.5 7v10L12 21.5 20.5 17V7L12 2.5Z",strokeLinejoin:"round"}),r("path",{d:"M3.5 7 12 11.5 20.5 7M12 11.5v10",strokeLinejoin:"round"})]})}function M({accessToken:i,models:o,buttonStyle:c}){let l=N().toLowerCase().startsWith("en"),t=l?k.en:k.pl,f=$(),g=z(()=>I(v,f),[f]),[x,b]=y(null),[h,p]=y({}),D=P(async(e,d)=>{let s=`${e.orderId}:${e.lineId}:${d}`;b(s),p(a=>({...a,[s]:""}));try{let a=await g.getModelFileDownloadUrl({accessToken:i,orderId:e.orderId,lineId:e.lineId,fileIndex:d});if(a.result.case==="success")window.location.assign(a.result.value.url);else{let u=a.result.case==="failure"?a.result.value:void 0;p(E=>({...E,[s]:(u!=null?t.failure[u]:void 0)??t.error}))}}catch{p(a=>({...a,[s]:t.error}))}finally{b(null)}},[g,i,t]);return o.length===0?null:r("section",{"aria-labelledby":"bitsnap-panel-model-files",className:"mt-10 w-full max-w-5xl","data-testid":"model-files",children:n("div",{className:"rounded-2xl border border-neutral-200 bg-white dark:border-neutral-800 dark:bg-neutral-900",children:[n("header",{className:"border-b border-neutral-200 px-5 py-4 dark:border-neutral-800",children:[r("h2",{id:"bitsnap-panel-model-files",className:"text-lg font-semibold text-neutral-900 dark:text-neutral-50",children:t.title}),r("p",{className:"mt-0.5 text-sm text-neutral-500 dark:text-neutral-400",children:t.subtitle})]}),r("ul",{className:"divide-y divide-neutral-200 dark:divide-neutral-800",children:o.map(e=>{let d=e.variantName?`${e.productName} \u2013 ${e.variantName}`:e.productName;return n("li",{className:"flex flex-col gap-3 px-5 py-4 sm:flex-row sm:items-center","data-state":w[e.state],children:[n("div",{className:"flex min-w-0 flex-1 items-center gap-4",children:[r("div",{className:"flex size-14 shrink-0 items-center justify-center overflow-hidden rounded-xl bg-neutral-100 text-neutral-400 dark:bg-neutral-800",children:e.thumbnailUrl?r("img",{src:e.thumbnailUrl,alt:"",className:"size-full object-contain",loading:"lazy"}):r(U,{className:"size-7"})}),n("div",{className:"min-w-0",children:[r("p",{className:"truncate font-medium text-neutral-900 dark:text-neutral-50",children:d}),r("p",{className:"mt-0.5 text-sm text-neutral-500 dark:text-neutral-400",children:[e.files[0]?`${e.files[0].format.toUpperCase()} \xB7 ${A(e.files[0].fileSize,l)}`:"",`${t.purchased} ${m(e.purchasedAt,l)}`,e.accessExpiresAt!=null&&e.state===1?`${t.accessUntil} ${m(e.accessExpiresAt,l)}`:""].filter(Boolean).join(" \xB7 ")}),e.licence&&n("p",{className:"mt-0.5 text-sm text-neutral-600 dark:text-neutral-300",children:[t.licence,":"," ",e.licence.url?r("a",{href:e.licence.url,target:"_blank",rel:"noopener noreferrer",className:"underline underline-offset-2 hover:text-neutral-900 dark:hover:text-neutral-50",children:e.licence.name}):e.licence.name]}),e.state===2&&n("p",{className:"mt-1 text-sm text-neutral-700 dark:text-neutral-200","data-model-removed":"",children:[t.removed,e.seller?.email&&n(L,{children:[" ",t.contact,e.seller.name?` (${e.seller.name})`:"",":"," ",r("a",{className:"font-medium underline underline-offset-2",href:`mailto:${e.seller.email}`,children:e.seller.email})]})]}),e.state===3&&n("p",{className:"mt-1 text-sm text-neutral-700 dark:text-neutral-200",children:[t.expired,e.accessExpiresAt!=null?` ${m(e.accessExpiresAt,l)}`:"","."]})]})]}),e.state===1&&e.files.map(s=>{let a=`${e.orderId}:${e.lineId}:${s.index}`,u=x===a;return n("div",{className:"flex flex-col items-start gap-1 sm:items-end",children:[r("button",{type:"button",onClick:()=>void D(e,s.index),disabled:x!=null,"aria-busy":u||void 0,"aria-label":`${t.download}: ${s.fileName}`,"data-model-download":"",style:c,className:"inline-flex h-10 items-center justify-center rounded-lg bg-neutral-900 px-4 text-sm font-medium text-white transition-opacity hover:opacity-90 disabled:opacity-60 dark:bg-neutral-50 dark:text-neutral-900",children:u?t.preparing:t.download}),h[a]&&r("p",{className:"text-sm text-red-700 dark:text-red-400",role:"alert",children:h[a]})]},a)})]},`${e.orderId}:${e.lineId}`)})})]})})}export{M as default};
2
+ //# sourceMappingURL=PanelModelFiles-RV7OHUUZ.mjs.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../src/panel/components/PanelModelFiles.tsx"],"sourcesContent":["import { createClient } from \"@connectrpc/connect\";\nimport { useTransport } from \"@connectrpc/connect-query\";\nimport { useCallback, useMemo, useState } from \"react\";\n\nimport { getLocale } from \"../../components/checkout/locale\";\nimport {\n ModelDeliveryFailure,\n ModelDeliveryService,\n ModelFileState,\n type PurchasedModel,\n} from \"../../gen/proto/public/v1/model_delivery_pb\";\n\n/**\n * \"3D files\" of the customer panel: the 3D files of the buyer's paid orders in\n * this shop (`public.v1.ModelDeliveryService`), with a download button, the\n * licence and the \"seller removed the file\" state. The download URL is signed\n * on click and expires quickly, so it is never rendered into the page.\n *\n * Older Bitsnap servers have no such service: any transport error (404,\n * unimplemented, network) hides the section, so the panel looks as before.\n *\n * Loaded on demand (with `model-files-client.ts`) only when the buyer has 3D\n * files, so the main bundle carries just `model-files-load.ts`.\n */\n\ntype Copy = {\n title: string;\n subtitle: string;\n download: string;\n preparing: string;\n licence: string;\n purchased: string;\n accessUntil: string;\n removed: string;\n contact: string;\n expired: string;\n failure: Partial<Record<ModelDeliveryFailure, string>>;\n error: string;\n};\n\nconst COPY: { pl: Copy; en: Copy } = {\n pl: {\n title: \"Pliki 3D\",\n subtitle: \"Pobierasz zawsze aktualną wersję pliku.\",\n download: \"Pobierz\",\n preparing: \"Przygotowuję…\",\n licence: \"Licencja\",\n purchased: \"Kupiono\",\n accessUntil: \"Dostęp do\",\n removed: \"Sprzedawca usunął ten plik.\",\n contact: \"Napisz do sprzedawcy\",\n expired: \"Dostęp do pliku wygasł\",\n failure: {\n [ModelDeliveryFailure.UNAUTHORIZED]: \"Sesja wygasła. Zaloguj się ponownie.\",\n [ModelDeliveryFailure.NOT_FOUND]: \"Nie znaleźliśmy tego pliku.\",\n [ModelDeliveryFailure.REFUNDED]: \"Zamówienie zostało zwrócone lub anulowane.\",\n [ModelDeliveryFailure.EXPIRED]: \"Dostęp do pliku wygasł.\",\n [ModelDeliveryFailure.REMOVED]: \"Sprzedawca usunął ten plik.\",\n [ModelDeliveryFailure.UNPAID]: \"Zamówienie nie jest opłacone.\",\n },\n error: \"Nie udało się pobrać pliku. Spróbuj ponownie.\",\n },\n en: {\n title: \"3D files\",\n subtitle: \"You always download the current version of the file.\",\n download: \"Download\",\n preparing: \"Preparing…\",\n licence: \"Licence\",\n purchased: \"Purchased\",\n accessUntil: \"Access until\",\n removed: \"The seller removed this file.\",\n contact: \"Contact the seller\",\n expired: \"Access to the file has ended\",\n failure: {\n [ModelDeliveryFailure.UNAUTHORIZED]: \"Your session has expired. Sign in again.\",\n [ModelDeliveryFailure.NOT_FOUND]: \"We could not find this file.\",\n [ModelDeliveryFailure.REFUNDED]: \"The order was refunded or canceled.\",\n [ModelDeliveryFailure.EXPIRED]: \"Access to the file has ended.\",\n [ModelDeliveryFailure.REMOVED]: \"The seller removed this file.\",\n [ModelDeliveryFailure.UNPAID]: \"The order is not paid.\",\n },\n error: \"The file could not be downloaded. Try again.\",\n },\n};\n\nfunction formatSize(bytes: bigint, en: boolean): string {\n const value = Number(bytes);\n if (!Number.isFinite(value) || value <= 0) return \"\";\n const mb = value / (1024 * 1024);\n const fmt = new Intl.NumberFormat(en ? \"en-GB\" : \"pl-PL\", {\n maximumFractionDigits: 1,\n });\n return mb >= 1 ? `${fmt.format(mb)} MB` : `${fmt.format(value / 1024)} KB`;\n}\n\nfunction formatDate(seconds: bigint | undefined, en: boolean): string {\n if (seconds == null || seconds <= 0n) return \"\";\n return new Intl.DateTimeFormat(en ? \"en-GB\" : \"pl-PL\", {\n dateStyle: \"medium\",\n }).format(new Date(Number(seconds) * 1000));\n}\n\nfunction CubeIcon({ className }: { className?: string }) {\n return (\n <svg\n viewBox=\"0 0 24 24\"\n fill=\"none\"\n stroke=\"currentColor\"\n strokeWidth={1.5}\n className={className}\n aria-hidden\n >\n <path d=\"M12 2.5 3.5 7v10L12 21.5 20.5 17V7L12 2.5Z\" strokeLinejoin=\"round\" />\n <path d=\"M3.5 7 12 11.5 20.5 7M12 11.5v10\" strokeLinejoin=\"round\" />\n </svg>\n );\n}\n\nexport default function PanelModelFiles({\n accessToken,\n models,\n buttonStyle,\n}: {\n accessToken: string;\n models: PurchasedModel[];\n buttonStyle?: React.CSSProperties;\n}) {\n const en = getLocale().toLowerCase().startsWith(\"en\");\n const t = en ? COPY.en : COPY.pl;\n const transport = useTransport();\n const client = useMemo(\n () => createClient(ModelDeliveryService, transport),\n [transport],\n );\n const [pending, setPending] = useState<string | null>(null);\n const [errors, setErrors] = useState<Record<string, string>>({});\n\n const download = useCallback(\n async (model: PurchasedModel, index: number) => {\n const key = `${model.orderId}:${model.lineId}:${index}`;\n setPending(key);\n setErrors((current) => ({ ...current, [key]: \"\" }));\n try {\n const response = await client.getModelFileDownloadUrl({\n accessToken,\n orderId: model.orderId,\n lineId: model.lineId,\n fileIndex: index,\n });\n if (response.result.case === \"success\") {\n // An attachment: the browser downloads it and stays on the panel.\n window.location.assign(response.result.value.url);\n } else {\n const failure =\n response.result.case === \"failure\" ? response.result.value : undefined;\n setErrors((current) => ({\n ...current,\n [key]: (failure != null ? t.failure[failure] : undefined) ?? t.error,\n }));\n }\n } catch {\n setErrors((current) => ({ ...current, [key]: t.error }));\n } finally {\n setPending(null);\n }\n },\n [client, accessToken, t],\n );\n\n if (models.length === 0) return null;\n\n return (\n <section\n aria-labelledby=\"bitsnap-panel-model-files\"\n className=\"mt-10 w-full max-w-5xl\"\n data-testid=\"model-files\"\n >\n <div className=\"rounded-2xl border border-neutral-200 bg-white dark:border-neutral-800 dark:bg-neutral-900\">\n <header className=\"border-b border-neutral-200 px-5 py-4 dark:border-neutral-800\">\n <h2\n id=\"bitsnap-panel-model-files\"\n className=\"text-lg font-semibold text-neutral-900 dark:text-neutral-50\"\n >\n {t.title}\n </h2>\n <p className=\"mt-0.5 text-sm text-neutral-500 dark:text-neutral-400\">\n {t.subtitle}\n </p>\n </header>\n <ul className=\"divide-y divide-neutral-200 dark:divide-neutral-800\">\n {models.map((model) => {\n const title = model.variantName\n ? `${model.productName} – ${model.variantName}`\n : model.productName;\n return (\n <li\n key={`${model.orderId}:${model.lineId}`}\n className=\"flex flex-col gap-3 px-5 py-4 sm:flex-row sm:items-center\"\n data-state={ModelFileState[model.state]}\n >\n <div className=\"flex min-w-0 flex-1 items-center gap-4\">\n <div className=\"flex size-14 shrink-0 items-center justify-center overflow-hidden rounded-xl bg-neutral-100 text-neutral-400 dark:bg-neutral-800\">\n {model.thumbnailUrl ? (\n <img\n src={model.thumbnailUrl}\n alt=\"\"\n className=\"size-full object-contain\"\n loading=\"lazy\"\n />\n ) : (\n <CubeIcon className=\"size-7\" />\n )}\n </div>\n <div className=\"min-w-0\">\n <p className=\"truncate font-medium text-neutral-900 dark:text-neutral-50\">\n {title}\n </p>\n <p className=\"mt-0.5 text-sm text-neutral-500 dark:text-neutral-400\">\n {[\n model.files[0]\n ? `${model.files[0].format.toUpperCase()} · ${formatSize(model.files[0].fileSize, en)}`\n : \"\",\n `${t.purchased} ${formatDate(model.purchasedAt, en)}`,\n model.accessExpiresAt != null &&\n model.state === ModelFileState.AVAILABLE\n ? `${t.accessUntil} ${formatDate(model.accessExpiresAt, en)}`\n : \"\",\n ]\n .filter(Boolean)\n .join(\" · \")}\n </p>\n {model.licence && (\n <p className=\"mt-0.5 text-sm text-neutral-600 dark:text-neutral-300\">\n {t.licence}:{\" \"}\n {model.licence.url ? (\n <a\n href={model.licence.url}\n target=\"_blank\"\n rel=\"noopener noreferrer\"\n className=\"underline underline-offset-2 hover:text-neutral-900 dark:hover:text-neutral-50\"\n >\n {model.licence.name}\n </a>\n ) : (\n model.licence.name\n )}\n </p>\n )}\n {model.state === ModelFileState.REMOVED && (\n <p\n className=\"mt-1 text-sm text-neutral-700 dark:text-neutral-200\"\n data-model-removed=\"\"\n >\n {t.removed}\n {model.seller?.email && (\n <>\n {\" \"}\n {t.contact}\n {model.seller.name ? ` (${model.seller.name})` : \"\"}:{\" \"}\n <a\n className=\"font-medium underline underline-offset-2\"\n href={`mailto:${model.seller.email}`}\n >\n {model.seller.email}\n </a>\n </>\n )}\n </p>\n )}\n {model.state === ModelFileState.EXPIRED && (\n <p className=\"mt-1 text-sm text-neutral-700 dark:text-neutral-200\">\n {t.expired}\n {model.accessExpiresAt != null\n ? ` ${formatDate(model.accessExpiresAt, en)}`\n : \"\"}\n .\n </p>\n )}\n </div>\n </div>\n {model.state === ModelFileState.AVAILABLE &&\n model.files.map((file) => {\n const key = `${model.orderId}:${model.lineId}:${file.index}`;\n const busy = pending === key;\n return (\n <div\n key={key}\n className=\"flex flex-col items-start gap-1 sm:items-end\"\n >\n <button\n type=\"button\"\n onClick={() => void download(model, file.index)}\n disabled={pending != null}\n aria-busy={busy || undefined}\n aria-label={`${t.download}: ${file.fileName}`}\n data-model-download=\"\"\n style={buttonStyle}\n className=\"inline-flex h-10 items-center justify-center rounded-lg bg-neutral-900 px-4 text-sm font-medium text-white transition-opacity hover:opacity-90 disabled:opacity-60 dark:bg-neutral-50 dark:text-neutral-900\"\n >\n {busy ? t.preparing : t.download}\n </button>\n {errors[key] && (\n <p\n className=\"text-sm text-red-700 dark:text-red-400\"\n role=\"alert\"\n >\n {errors[key]}\n </p>\n )}\n </div>\n );\n })}\n </li>\n );\n })}\n </ul>\n </div>\n </section>\n );\n}\n"],"mappings":"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","names":["createClient","useTransport","useCallback","useMemo","useState","Fragment","jsx","jsxs","COPY","formatSize","bytes","en","value","mb","fmt","formatDate","seconds","CubeIcon","className","PanelModelFiles","accessToken","models","buttonStyle","getLocale","transport","useTransport","client","useMemo","createClient","ModelDeliveryService","pending","setPending","useState","errors","setErrors","download","useCallback","model","index","key","current","response","failure","title","ModelFileState","file","busy"]}
@@ -0,0 +1,2 @@
1
+ import{c as V,d as G,f as R,g as _,i as P}from"./chunk-5ED7FWNY.mjs";import"./chunk-U6FLIRIV.mjs";import{ContactShadows as ie}from"@react-three/drei";import{Canvas as ce,useFrame as de,useThree as w}from"@react-three/fiber";import{Component as ue,useEffect as v,useLayoutEffect as z,useMemo as le,useRef as x,useState as H}from"react";import{Vector3 as me}from"three";import{OrbitControls as pe}from"three/examples/jsm/controls/OrbitControls.js";async function I(e,t={}){let o=await fetch(e,{signal:t.signal,credentials:"omit"});if(!o.ok)throw new Error(`model download failed: ${o.status}`);let i=Number(o.headers.get("content-length")),a=Number.isFinite(i)&&i>0?i:t.expectedSize??0;if(o.body==null){let s=await o.arrayBuffer();return t.onProgress?.(s.byteLength,s.byteLength),s}let m=o.body.getReader(),u=[],c=0;for(t.onProgress?.(0,a);;){let{done:s,value:g}=await m.read();if(s)break;u.push(g),c+=g.byteLength,t.onProgress?.(c,Math.max(a,c))}let d=new Uint8Array(c),p=0;for(let s of u)d.set(s,p),p+=s.byteLength;return d.buffer}import{Box3 as N,MathUtils as A,PMREMGenerator as Q,REVISION as X,Sphere as Y,Vector3 as M}from"three";import{RoomEnvironment as ee}from"three/examples/jsm/environments/RoomEnvironment.js";import{MeshoptDecoder as te}from"three/examples/jsm/libs/meshopt_decoder.module.js";import{DRACOLoader as re}from"three/examples/jsm/loaders/DRACOLoader.js";import{GLTFLoader as ne}from"three/examples/jsm/loaders/GLTFLoader.js";var B=new Map;function oe(e){let t=e?.trim()||_(X);return t.endsWith("/")?t:`${t}/`}function ae(e){let t=oe(e),o=B.get(t);return o==null&&(o=new re().setDecoderPath(t),B.set(t,o)),new ne().setDRACOLoader(o).setMeshoptDecoder(te)}function U(e,t){return ae(t).parseAsync(e,"")}function W(e){e.position.set(0,0,0),e.updateMatrixWorld(!0);let t=new N().setFromObject(e);if(t.isEmpty())return{object:e,radius:1,size:new M(1,1,1)};let o=t.getCenter(new M);e.position.set(-o.x,-t.min.y,-o.z),e.updateMatrixWorld(!0);let i=t.getSize(new M),a=new N().setFromObject(e).getBoundingSphere(new Y);return{object:e,radius:Math.max(a.radius,.001),size:i}}var se={azimuthDeg:35,elevationDeg:18};function j(e,t,o,i=se,a=1.18){let m=new M(0,e.size.y/2,0),u=A.degToRad(t)/2,c=Math.atan(Math.tan(u)*Math.max(o,.1)),d=Math.min(u,c),p=e.radius/Math.sin(d)*a,s=A.degToRad(i.azimuthDeg),g=A.degToRad(i.elevationDeg);return{position:new M(Math.sin(s)*Math.cos(g),Math.sin(g),Math.cos(s)*Math.cos(g)).multiplyScalar(p).add(m),target:m,distance:p}}function S(e,t){e.position.copy(t.position),e.near=Math.max(t.distance/200,1e-4),e.far=t.distance*50,e.lookAt(t.target),e.updateProjectionMatrix()}function q(e){let t=new Q(e),o=new ee,i=t.fromScene(o,.04);return o.dispose(),t.dispose(),{texture:i.texture,dispose:()=>i.dispose()}}import{Fragment as ve,jsx as b,jsxs as ye}from"react/jsx-runtime";var $=.08,L=new Map;function fe(e,t,o,i){let a=L.get(e);if(a==null){let m=new AbortController,u={controller:m,users:0,progress:new Set,promise:void 0};u.promise=I(e,{signal:m.signal,expectedSize:t,onProgress:(c,d)=>{u.last=[c,d];for(let p of u.progress)p(c,d)}}).then(c=>U(c,o)).then(c=>W(c.scene)),u.promise.catch(()=>{L.get(e)===u&&L.delete(e)}),L.set(e,u),a=u}return a.abortTimer!=null&&(clearTimeout(a.abortTimer),a.abortTimer=void 0),a.users+=1,a.progress.add(i),a.last!=null&&i(...a.last),a}function ge(e,t){e.users-=1,e.progress.delete(t),!(e.users>0)&&(e.abortTimer=setTimeout(()=>{e.abortTimer=void 0,e.users===0&&e.controller.abort()},0))}function he(e){let[t,o]=H(),i=x(e);return i.current=e,v(()=>{let a=!1;o(void 0);let m=(c,d)=>{a||i.current.onProgress(c,d)},u=fe(e.url,e.expectedSize,e.decoderPath,m);return u.promise.then(c=>{a||o(c)},c=>{a||i.current.onError(c)}),()=>{a=!0,ge(u,m)}},[e.url,e.attempt]),t==null?null:b(F,{onError:a=>i.current.onError(a),children:b(ce,{className:e.className,dpr:[1,2],frameloop:e.motionAllowed?"always":"demand",camera:{fov:R,position:[0,0,5]},gl:{alpha:!0,antialias:!0,preserveDrawingBuffer:!1},style:{background:"transparent",touchAction:e.interactive?"none":"pan-y"},"aria-hidden":"true",children:b(be,{model:t,...e})},e.url)})}function be({model:e,motionAllowed:t,interactive:o,allowZoom:i,resetSignal:a,onLoaded:m,onInteract:u,onError:c}){let d=w(r=>r.gl),p=w(r=>r.scene),s=w(r=>r.camera),g=w(r=>r.size),l=w(r=>r.invalidate),[Z,D]=H(!1),C=x(!1),y=x(void 0),E=x({onLoaded:m,onInteract:u,onError:c});E.current={onLoaded:m,onInteract:u,onError:c};let n=le(()=>new pe(s,d.domElement),[s,d]);v(()=>{let r=q(d);return p.environment=r.texture,p.environmentIntensity=P.environmentIntensity,l(),()=>{p.environment=null,r.dispose()}},[d,p,l]),v(()=>{let r=d.domElement,f=h=>{h.preventDefault(),E.current.onError(new Error("webgl context lost"))};return r.addEventListener("webglcontextlost",f),()=>r.removeEventListener("webglcontextlost",f)},[d]),z(()=>{n.connect(d.domElement)},[n,d]),v(()=>{n.enablePan=!1,n.enableDamping=!0,n.dampingFactor=$,n.rotateSpeed=.8,n.minPolarAngle=.05,n.maxPolarAngle=Math.PI*.62,n.autoRotateSpeed=V;let r,f=()=>{clearTimeout(r),D(!0),E.current.onInteract()},h=()=>{clearTimeout(r),r=setTimeout(()=>D(!1),G)},T=()=>l();return n.addEventListener("start",f),n.addEventListener("end",h),n.addEventListener("change",T),()=>{clearTimeout(r),n.removeEventListener("start",f),n.removeEventListener("end",h),n.removeEventListener("change",T),n.dispose()}},[n,l]),z(()=>{n.enabled=o,n.enableZoom=i,d.domElement.style.touchAction=o?"none":"pan-y",l()},[n,d,o,i,l]);let k=t&&!Z;v(()=>{n.autoRotate=k,l()},[n,k,l]);let O=g.width/Math.max(g.height,1);z(()=>{let r=j(e,R,O),f=y.current;if(y.current=r,n.minDistance=r.distance*.35,n.maxDistance=r.distance*2.5,f==null)S(s,r),n.target.copy(r.target);else{let h=new me().subVectors(s.position,n.target).normalize(),T=s.position.distanceTo(n.target)/Math.max(f.distance,1e-6);s.position.copy(r.target).addScaledVector(h,r.distance*T),s.near=r.distance/200,s.far=r.distance*50,s.updateProjectionMatrix()}n.update(),l()},[e,O,s,n,l]);let J=x(a);v(()=>{a===J.current||y.current==null||(S(s,y.current),n.target.copy(y.current.target),n.update(),D(!1),l())},[a,s,n,l]),de((r,f)=>{let h=Math.min(f,.1);n.dampingFactor=1-(1-$)**(h*60),n.update(h)&&l(),C.current||(C.current=!0,requestAnimationFrame(()=>E.current.onLoaded()))});let K=Math.max(e.size.x,e.size.z,e.radius);return ye(ve,{children:[b("ambientLight",{intensity:P.ambient}),P.directional.map(r=>b("directionalLight",{position:r.position,intensity:r.intensity},r.position.join(","))),b("primitive",{object:e.object}),b(ie,{position:[0,5e-4*e.radius,0],scale:K*2.4,far:Math.max(e.size.y,e.radius)*1.2,blur:2.6,opacity:.42,resolution:512,frames:1})]})}var F=class extends ue{state={failed:!1};static getDerivedStateFromError(){return{failed:!0}}componentDidCatch(t){this.props.onError(t)}render(){return this.state.failed?null:this.props.children}};export{he as default};
2
+ //# sourceMappingURL=Viewer3D-2IDEH6W3.mjs.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../src/viewer3d/Viewer3D.tsx","../src/viewer3d/fetch-model.ts","../src/viewer3d/scene.three.ts"],"sourcesContent":["/**\n * The interactive 3D viewer. With `scene.three.ts`, the only module that\n * imports `three`, `@react-three/fiber` and `@react-three/drei` (optional peer\n * dependencies). `Model3DShell` loads it with `import()` inside an effect,\n * never during the server render; `test:package` checks that the main and\n * catalog entries never reach it.\n */\nimport { ContactShadows } from \"@react-three/drei\";\nimport { Canvas, useFrame, useThree } from \"@react-three/fiber\";\nimport {\n Component,\n useEffect,\n useLayoutEffect,\n useMemo,\n useRef,\n useState,\n type ReactNode,\n} from \"react\";\nimport { Vector3 } from \"three\";\nimport { OrbitControls } from \"three/examples/jsm/controls/OrbitControls.js\";\n\nimport {\n AUTO_ROTATE_RESUME_MS,\n AUTO_ROTATE_SPEED,\n CAMERA_FOV,\n STUDIO_LIGHTS,\n} from \"./constants\";\nimport { fetchModel } from \"./fetch-model\";\nimport {\n applyFraming,\n createStudioEnvironment,\n frameModel,\n parseGLB,\n prepareModel,\n type Framing,\n type PreparedModel,\n} from \"./scene.three\";\n\n/** Glide after a drag: the share of speed lost per frame at 60 Hz. */\nconst DAMPING_PER_60HZ_FRAME = 0.08;\n\nexport type Viewer3DProps = {\n url: string;\n /** Draco decoder folder; default: three's decoder on jsDelivr for the installed revision. */\n decoderPath?: string;\n /** Bytes the server reported; the progress total when the response has none. */\n expectedSize: number;\n /** Bumped by the shell on retry; a failed download is not reused. */\n attempt: number;\n /** Rotation may run: on screen, tab visible, no reduced motion. */\n motionAllowed: boolean;\n /** The buyer can drag (on a phone only after a tap, so the page scrolls). */\n interactive: boolean;\n /** Wheel and pinch zoom: full screen and active touch only, so the page keeps its scroll. */\n allowZoom: boolean;\n /** Bumped by \"Reset widoku\". */\n resetSignal: number;\n onProgress: (loaded: number, total: number) => void;\n /** The first frame with the model is on screen. */\n onLoaded: () => void;\n onError: (error: unknown) => void;\n /** The buyer dragged or zoomed (hides the gesture hint). */\n onInteract: () => void;\n className?: string;\n};\n\n/**\n * Downloads and parsed models by URL, shared by every viewer of the page:\n * switching back to a variant does not download again. A download is aborted\n * only when its last viewer is gone, one task later, so a remount (React\n * StrictMode, a parent re-keying the viewer) picks the running download up\n * instead of failing with an AbortError.\n */\ntype ModelEntry = {\n promise: Promise<PreparedModel>;\n controller: AbortController;\n users: number;\n progress: Set<(loaded: number, total: number) => void>;\n last?: [number, number];\n abortTimer?: ReturnType<typeof setTimeout>;\n};\n\nconst models = new Map<string, ModelEntry>();\n\nfunction acquireModel(\n url: string,\n expectedSize: number,\n decoderPath: string | undefined,\n onProgress: (loaded: number, total: number) => void,\n): ModelEntry {\n let entry = models.get(url);\n if (entry == null) {\n const controller = new AbortController();\n const created: ModelEntry = {\n controller,\n users: 0,\n progress: new Set(),\n promise: undefined as unknown as Promise<PreparedModel>,\n };\n created.promise = fetchModel(url, {\n signal: controller.signal,\n expectedSize,\n onProgress: (loaded, total) => {\n created.last = [loaded, total];\n for (const listener of created.progress) listener(loaded, total);\n },\n })\n .then((buffer) => parseGLB(buffer, decoderPath))\n .then((gltf) => prepareModel(gltf.scene));\n // A failed or aborted download is forgotten, so a retry downloads again.\n created.promise.catch(() => {\n if (models.get(url) === created) models.delete(url);\n });\n models.set(url, created);\n entry = created;\n }\n if (entry.abortTimer != null) {\n clearTimeout(entry.abortTimer);\n entry.abortTimer = undefined;\n }\n entry.users += 1;\n entry.progress.add(onProgress);\n if (entry.last != null) onProgress(...entry.last);\n return entry;\n}\n\nfunction releaseModel(\n entry: ModelEntry,\n onProgress: (loaded: number, total: number) => void,\n): void {\n entry.users -= 1;\n entry.progress.delete(onProgress);\n if (entry.users > 0) return;\n entry.abortTimer = setTimeout(() => {\n entry.abortTimer = undefined;\n // Done downloads ignore it; the parsed model stays cached.\n if (entry.users === 0) entry.controller.abort();\n }, 0);\n}\n\nexport default function Viewer3D(props: Viewer3DProps) {\n const [model, setModel] = useState<PreparedModel>();\n const callbacks = useRef(props);\n callbacks.current = props;\n\n useEffect(() => {\n let cancelled = false;\n setModel(undefined);\n const onProgress = (loaded: number, total: number) => {\n if (!cancelled) callbacks.current.onProgress(loaded, total);\n };\n const entry = acquireModel(\n props.url,\n props.expectedSize,\n props.decoderPath,\n onProgress,\n );\n entry.promise.then(\n (prepared) => {\n if (!cancelled) setModel(prepared);\n },\n (error) => {\n if (!cancelled) callbacks.current.onError(error);\n },\n );\n return () => {\n cancelled = true;\n releaseModel(entry, onProgress);\n };\n // eslint-disable-next-line react-hooks/exhaustive-deps\n }, [props.url, props.attempt]);\n\n if (model == null) return null;\n\n return (\n <CanvasErrorBoundary onError={(error) => callbacks.current.onError(error)}>\n <Canvas\n key={props.url}\n className={props.className}\n dpr={[1, 2]}\n frameloop={props.motionAllowed ? \"always\" : \"demand\"}\n camera={{ fov: CAMERA_FOV, position: [0, 0, 5] }}\n gl={{ alpha: true, antialias: true, preserveDrawingBuffer: false }}\n // The page background shows through: the theme's card colour, never a grey box.\n style={{\n background: \"transparent\",\n touchAction: props.interactive ? \"none\" : \"pan-y\",\n }}\n aria-hidden=\"true\"\n >\n <Scene model={model} {...props} />\n </Canvas>\n </CanvasErrorBoundary>\n );\n}\n\nfunction Scene({\n model,\n motionAllowed,\n interactive,\n allowZoom,\n resetSignal,\n onLoaded,\n onInteract,\n onError,\n}: Viewer3DProps & { model: PreparedModel }) {\n const gl = useThree((state) => state.gl);\n const scene = useThree((state) => state.scene);\n const camera = useThree(\n (state) => state.camera,\n ) as import(\"three\").PerspectiveCamera;\n const size = useThree((state) => state.size);\n const invalidate = useThree((state) => state.invalidate);\n const [paused, setPaused] = useState(false);\n const loadedRef = useRef(false);\n const startRef = useRef<Framing | undefined>(undefined);\n const callbacks = useRef({ onLoaded, onInteract, onError });\n callbacks.current = { onLoaded, onInteract, onError };\n\n const controls = useMemo(\n () => new OrbitControls(camera, gl.domElement),\n [camera, gl],\n );\n\n // Light: a studio environment generated on the device.\n useEffect(() => {\n const environment = createStudioEnvironment(gl);\n scene.environment = environment.texture;\n scene.environmentIntensity = STUDIO_LIGHTS.environmentIntensity;\n invalidate();\n return () => {\n scene.environment = null;\n environment.dispose();\n };\n }, [gl, scene, invalidate]);\n\n // A lost GPU context is an error the buyer can retry.\n useEffect(() => {\n const canvas = gl.domElement;\n const onLost = (event: Event) => {\n event.preventDefault();\n callbacks.current.onError(new Error(\"webgl context lost\"));\n };\n canvas.addEventListener(\"webglcontextlost\", onLost);\n return () => canvas.removeEventListener(\"webglcontextlost\", onLost);\n }, [gl]);\n\n // (Re)attach the controls to the canvas: a cleanup's `dispose` detaches\n // them and React StrictMode runs effects twice. A layout effect, before the\n // one below that sets `touch-action` (connect sets it to \"none\").\n useLayoutEffect(() => {\n controls.connect(gl.domElement);\n }, [controls, gl]);\n\n // Controls: rotate and zoom around the model, no panning.\n useEffect(() => {\n controls.enablePan = false;\n controls.enableDamping = true;\n controls.dampingFactor = DAMPING_PER_60HZ_FRAME;\n controls.rotateSpeed = 0.8;\n controls.minPolarAngle = 0.05;\n controls.maxPolarAngle = Math.PI * 0.62;\n controls.autoRotateSpeed = AUTO_ROTATE_SPEED;\n let resume: ReturnType<typeof setTimeout> | undefined;\n const onStart = () => {\n clearTimeout(resume);\n setPaused(true);\n callbacks.current.onInteract();\n };\n const onEnd = () => {\n clearTimeout(resume);\n resume = setTimeout(() => setPaused(false), AUTO_ROTATE_RESUME_MS);\n };\n const onChange = () => invalidate();\n controls.addEventListener(\"start\", onStart);\n controls.addEventListener(\"end\", onEnd);\n controls.addEventListener(\"change\", onChange);\n return () => {\n clearTimeout(resume);\n controls.removeEventListener(\"start\", onStart);\n controls.removeEventListener(\"end\", onEnd);\n controls.removeEventListener(\"change\", onChange);\n controls.dispose();\n };\n }, [controls, invalidate]);\n\n // OrbitControls sets `touch-action: none` on connect; the page decides.\n useLayoutEffect(() => {\n controls.enabled = interactive;\n controls.enableZoom = allowZoom;\n gl.domElement.style.touchAction = interactive ? \"none\" : \"pan-y\";\n invalidate();\n }, [controls, gl, interactive, allowZoom, invalidate]);\n\n const rotating = motionAllowed && !paused;\n useEffect(() => {\n controls.autoRotate = rotating;\n invalidate();\n }, [controls, rotating, invalidate]);\n\n // Framing: the start view, again on reset; a new aspect keeps the direction.\n const aspect = size.width / Math.max(size.height, 1);\n useLayoutEffect(() => {\n const framing = frameModel(model, CAMERA_FOV, aspect);\n const previous = startRef.current;\n startRef.current = framing;\n controls.minDistance = framing.distance * 0.35;\n controls.maxDistance = framing.distance * 2.5;\n if (previous == null) {\n applyFraming(camera, framing);\n controls.target.copy(framing.target);\n } else {\n const direction = new Vector3()\n .subVectors(camera.position, controls.target)\n .normalize();\n const scale =\n camera.position.distanceTo(controls.target) /\n Math.max(previous.distance, 1e-6);\n camera.position\n .copy(framing.target)\n .addScaledVector(direction, framing.distance * scale);\n camera.near = framing.distance / 200;\n camera.far = framing.distance * 50;\n camera.updateProjectionMatrix();\n }\n controls.update();\n invalidate();\n }, [model, aspect, camera, controls, invalidate]);\n\n const firstReset = useRef(resetSignal);\n useEffect(() => {\n if (resetSignal === firstReset.current || startRef.current == null) return;\n applyFraming(camera, startRef.current);\n controls.target.copy(startRef.current.target);\n controls.update();\n setPaused(false);\n invalidate();\n }, [resetSignal, camera, controls, invalidate]);\n\n useFrame((_, delta) => {\n // Time-based: the same rotation speed and glide at 60 Hz, 120 Hz or a\n // slow device (OrbitControls damps per call); damping keeps drawing.\n const step = Math.min(delta, 0.1);\n controls.dampingFactor = 1 - (1 - DAMPING_PER_60HZ_FRAME) ** (step * 60);\n if (controls.update(step)) invalidate();\n if (!loadedRef.current) {\n loadedRef.current = true;\n requestAnimationFrame(() => callbacks.current.onLoaded());\n }\n });\n\n const footprint = Math.max(model.size.x, model.size.z, model.radius);\n return (\n <>\n <ambientLight intensity={STUDIO_LIGHTS.ambient} />\n {STUDIO_LIGHTS.directional.map((light) => (\n <directionalLight key={light.position.join(\",\")} position={light.position} intensity={light.intensity} />\n ))}\n <primitive object={model.object} />\n <ContactShadows\n position={[0, 0.0005 * model.radius, 0]}\n scale={footprint * 2.4}\n far={Math.max(model.size.y, model.radius) * 1.2}\n blur={2.6}\n opacity={0.42}\n resolution={512}\n frames={1}\n />\n </>\n );\n}\n\nclass CanvasErrorBoundary extends Component<\n { onError: (error: unknown) => void; children: ReactNode },\n { failed: boolean }\n> {\n state = { failed: false };\n\n static getDerivedStateFromError() {\n return { failed: true };\n }\n\n componentDidCatch(error: unknown) {\n this.props.onError(error);\n }\n\n render() {\n return this.state.failed ? null : this.props.children;\n }\n}\n","import { needsClickToLoad } from \"./constants\";\n\n/**\n * Downloads a display copy with byte progress (no three here; `Viewer3D`\n * parses the buffer). The total comes from Content-Length, else from the size\n * the server reported (`previewSize`).\n */\nexport async function fetchModel(\n url: string,\n options: {\n signal?: AbortSignal;\n expectedSize?: number;\n onProgress?: (loaded: number, total: number) => void;\n } = {},\n): Promise<ArrayBuffer> {\n const response = await fetch(url, {\n signal: options.signal,\n credentials: \"omit\",\n });\n if (!response.ok)\n throw new Error(`model download failed: ${response.status}`);\n const header = Number(response.headers.get(\"content-length\"));\n const total =\n Number.isFinite(header) && header > 0\n ? header\n : (options.expectedSize ?? 0);\n if (response.body == null) {\n const buffer = await response.arrayBuffer();\n options.onProgress?.(buffer.byteLength, buffer.byteLength);\n return buffer;\n }\n\n const reader = response.body.getReader();\n const chunks: Uint8Array[] = [];\n let loaded = 0;\n options.onProgress?.(0, total);\n for (;;) {\n const { done, value } = await reader.read();\n if (done) break;\n chunks.push(value);\n loaded += value.byteLength;\n options.onProgress?.(loaded, Math.max(total, loaded));\n }\n const buffer = new Uint8Array(loaded);\n let offset = 0;\n for (const chunk of chunks) {\n buffer.set(chunk, offset);\n offset += chunk.byteLength;\n }\n return buffer.buffer;\n}\n\nconst prefetched = new Set<string>();\n\n/**\n * Warms the HTTP cache with another variant's model when the buyer points at\n * its chip (plan N5: on hover/focus only, never on idle). Models over the\n * click-to-load limit are never prefetched.\n */\nexport function prefetchModel(url: string | undefined, size: number): void {\n if (url == null || prefetched.has(url) || needsClickToLoad(size)) return;\n if (typeof window === \"undefined\") return;\n // Save-Data and slow connections skip it.\n const connection = (\n navigator as Navigator & {\n connection?: { saveData?: boolean; effectiveType?: string };\n }\n ).connection;\n if (connection?.saveData || /(^|-)2g$/.test(connection?.effectiveType ?? \"\"))\n return;\n prefetched.add(url);\n fetch(url, { credentials: \"omit\", priority: \"low\" } as RequestInit).catch(\n () => {\n prefetched.delete(url);\n },\n );\n}\n","/**\n * The 3D scene of a product model: loading, framing and light. Imports\n * `three`, so it is only reached through the dynamic `import()` of\n * `Viewer3D` (the `bitsnap-react` main and catalog entries never reach it;\n * `test:package` checks that).\n */\nimport {\n Box3,\n MathUtils,\n PMREMGenerator,\n REVISION,\n Sphere,\n Vector3,\n type Object3D,\n type PerspectiveCamera,\n type Texture,\n type WebGLRenderer,\n} from \"three\";\nimport { RoomEnvironment } from \"three/examples/jsm/environments/RoomEnvironment.js\";\nimport { MeshoptDecoder } from \"three/examples/jsm/libs/meshopt_decoder.module.js\";\nimport { DRACOLoader } from \"three/examples/jsm/loaders/DRACOLoader.js\";\nimport {\n GLTFLoader,\n type GLTF,\n} from \"three/examples/jsm/loaders/GLTFLoader.js\";\n\nimport { defaultDecoderPath } from \"./constants\";\n\nconst dracoLoaders = new Map<string, DRACOLoader>();\n\n/** The Draco decoder path in effect: the caller's, else three's files on jsDelivr. */\nexport function resolveDecoderPath(decoderPath?: string): string {\n const path = decoderPath?.trim() || defaultDecoderPath(REVISION);\n return path.endsWith(\"/\") ? path : `${path}/`;\n}\n\n/** A GLTF loader that decodes Draco and Meshopt. One Draco loader per decoder path. */\nexport function createGLTFLoader(decoderPath?: string): GLTFLoader {\n const path = resolveDecoderPath(decoderPath);\n let draco = dracoLoaders.get(path);\n if (draco == null) {\n draco = new DRACOLoader().setDecoderPath(path);\n dracoLoaders.set(path, draco);\n }\n return new GLTFLoader().setDRACOLoader(draco).setMeshoptDecoder(MeshoptDecoder);\n}\n\n/** Parses a downloaded GLB. */\nexport function parseGLB(buffer: ArrayBuffer, decoderPath?: string): Promise<GLTF> {\n return createGLTFLoader(decoderPath).parseAsync(buffer, \"\");\n}\n\nexport type PreparedModel = {\n object: Object3D;\n /** Radius of the bounding sphere, after centring (model units). */\n radius: number;\n /** Size of the bounding box (x, y, z). */\n size: Vector3;\n};\n\n/**\n * Centres the model on the origin in x and z and stands it on y = 0, so the\n * contact shadow sits under it and the camera orbits its middle.\n */\nexport function prepareModel(object: Object3D): PreparedModel {\n object.position.set(0, 0, 0);\n object.updateMatrixWorld(true);\n const box = new Box3().setFromObject(object);\n if (box.isEmpty()) {\n return { object, radius: 1, size: new Vector3(1, 1, 1) };\n }\n const center = box.getCenter(new Vector3());\n object.position.set(-center.x, -box.min.y, -center.z);\n object.updateMatrixWorld(true);\n const size = box.getSize(new Vector3());\n const sphere = new Box3()\n .setFromObject(object)\n .getBoundingSphere(new Sphere());\n return { object, radius: Math.max(sphere.radius, 1e-3), size };\n}\n\nexport type Framing = {\n position: Vector3;\n target: Vector3;\n distance: number;\n};\n\n/** Camera angles of the start view (and of the main \"3q\" thumbnail). */\nexport const START_VIEW = { azimuthDeg: 35, elevationDeg: 18 } as const;\n\n/**\n * Where the camera stands so the whole model fits with a margin, seen from\n * `view` (azimuth around y, elevation above the horizon).\n */\nexport function frameModel(\n model: PreparedModel,\n fovDeg: number,\n aspect: number,\n view: { azimuthDeg: number; elevationDeg: number } = START_VIEW,\n margin = 1.18,\n): Framing {\n const target = new Vector3(0, model.size.y / 2, 0);\n const vertical = MathUtils.degToRad(fovDeg) / 2;\n const horizontal = Math.atan(Math.tan(vertical) * Math.max(aspect, 0.1));\n const halfAngle = Math.min(vertical, horizontal);\n const distance = (model.radius / Math.sin(halfAngle)) * margin;\n const azimuth = MathUtils.degToRad(view.azimuthDeg);\n const elevation = MathUtils.degToRad(view.elevationDeg);\n const position = new Vector3(\n Math.sin(azimuth) * Math.cos(elevation),\n Math.sin(elevation),\n Math.cos(azimuth) * Math.cos(elevation),\n )\n .multiplyScalar(distance)\n .add(target);\n return { position, target, distance };\n}\n\n/** Points the camera along a framing and sets near/far planes for its size. */\nexport function applyFraming(\n camera: PerspectiveCamera,\n framing: Framing,\n): void {\n camera.position.copy(framing.position);\n camera.near = Math.max(framing.distance / 200, 1e-4);\n camera.far = framing.distance * 50;\n camera.lookAt(framing.target);\n camera.updateProjectionMatrix();\n}\n\n/**\n * A neutral studio light for PBR materials, generated on the device (three's\n * `RoomEnvironment`); no HDR download (plan S13).\n */\nexport function createStudioEnvironment(renderer: WebGLRenderer): {\n texture: Texture;\n dispose: () => void;\n} {\n const pmrem = new PMREMGenerator(renderer);\n const room = new RoomEnvironment();\n const target = pmrem.fromScene(room, 0.04);\n room.dispose();\n pmrem.dispose();\n return { texture: target.texture, dispose: () => target.dispose() };\n}\n"],"mappings":"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","names":["ContactShadows","Canvas","useFrame","useThree","Component","useEffect","useLayoutEffect","useMemo","useRef","useState","Vector3","OrbitControls","fetchModel","url","options","response","header","total","buffer","reader","chunks","loaded","done","value","offset","chunk","Box3","MathUtils","PMREMGenerator","REVISION","Sphere","Vector3","RoomEnvironment","MeshoptDecoder","DRACOLoader","GLTFLoader","dracoLoaders","resolveDecoderPath","decoderPath","path","defaultDecoderPath","REVISION","createGLTFLoader","draco","DRACOLoader","GLTFLoader","MeshoptDecoder","parseGLB","buffer","prepareModel","object","box","Box3","Vector3","center","size","sphere","Sphere","START_VIEW","frameModel","model","fovDeg","aspect","view","margin","target","vertical","MathUtils","horizontal","halfAngle","distance","azimuth","elevation","applyFraming","camera","framing","createStudioEnvironment","renderer","pmrem","PMREMGenerator","room","RoomEnvironment","Fragment","jsx","jsxs","DAMPING_PER_60HZ_FRAME","models","acquireModel","url","expectedSize","decoderPath","onProgress","entry","controller","created","fetchModel","loaded","total","listener","buffer","parseGLB","gltf","prepareModel","releaseModel","Viewer3D","props","model","setModel","useState","callbacks","useRef","useEffect","cancelled","prepared","error","CanvasErrorBoundary","Canvas","CAMERA_FOV","Scene","motionAllowed","interactive","allowZoom","resetSignal","onLoaded","onInteract","onError","gl","useThree","state","scene","camera","size","invalidate","paused","setPaused","loadedRef","startRef","controls","useMemo","OrbitControls","environment","createStudioEnvironment","STUDIO_LIGHTS","canvas","onLost","event","useLayoutEffect","AUTO_ROTATE_SPEED","resume","onStart","onEnd","AUTO_ROTATE_RESUME_MS","onChange","rotating","aspect","framing","frameModel","previous","applyFraming","direction","Vector3","scale","firstReset","useFrame","_","delta","step","footprint","light","ContactShadows","Component"]}
package/dist/analytics.js CHANGED
@@ -1,2 +1,2 @@
1
- "use strict";Object.defineProperty(exports, "__esModule", {value: true});var _chunkMG6VV5J2js = require('./chunk-MG6VV5J2.js');require('./chunk-W3RGQYPD.js');require('./chunk-JSDGZ4NY.js');exports.BitsnapAnalyticsProvider = _chunkMG6VV5J2js.i; exports.DEFAULT_ENDPOINT = _chunkMG6VV5J2js.c; exports.InstrumentProvider = _chunkMG6VV5J2js.l; exports.createTracker = _chunkMG6VV5J2js.d; exports.getTracker = _chunkMG6VV5J2js.f; exports.initTracker = _chunkMG6VV5J2js.e; exports.noopInstrument = _chunkMG6VV5J2js.k; exports.resetTracker = _chunkMG6VV5J2js.g; exports.scoreBucket = _chunkMG6VV5J2js.r; exports.startAutoPageViews = _chunkMG6VV5J2js.b; exports.useAnalytics = _chunkMG6VV5J2js.j; exports.useInstrument = _chunkMG6VV5J2js.n; exports.useInstrumentContext = _chunkMG6VV5J2js.m;
1
+ "use strict";Object.defineProperty(exports, "__esModule", {value: true});var _chunkGQJDFLSLjs = require('./chunk-GQJDFLSL.js');require('./chunk-W3RGQYPD.js');require('./chunk-JSDGZ4NY.js');exports.BitsnapAnalyticsProvider = _chunkGQJDFLSLjs.i; exports.DEFAULT_ENDPOINT = _chunkGQJDFLSLjs.c; exports.InstrumentProvider = _chunkGQJDFLSLjs.l; exports.createTracker = _chunkGQJDFLSLjs.d; exports.getTracker = _chunkGQJDFLSLjs.f; exports.initTracker = _chunkGQJDFLSLjs.e; exports.noopInstrument = _chunkGQJDFLSLjs.k; exports.resetTracker = _chunkGQJDFLSLjs.g; exports.scoreBucket = _chunkGQJDFLSLjs.r; exports.startAutoPageViews = _chunkGQJDFLSLjs.b; exports.useAnalytics = _chunkGQJDFLSLjs.j; exports.useInstrument = _chunkGQJDFLSLjs.n; exports.useInstrumentContext = _chunkGQJDFLSLjs.m;
2
2
  //# sourceMappingURL=analytics.js.map
@@ -1 +1 @@
1
- {"version":3,"sources":["/private/tmp/bsr-src/dist/analytics.js"],"names":[],"mappings":"AAAA,+HAAsH,+BAA4B,+BAA4B,klBAAsR","file":"/private/tmp/bsr-src/dist/analytics.js"}
1
+ {"version":3,"sources":["/home/emems/web/bitsnap/bitsnap/bitsnap-react/dist/analytics.js"],"names":[],"mappings":"AAAA,+HAAsH,+BAA4B,+BAA4B,klBAAsR","file":"/home/emems/web/bitsnap/bitsnap/bitsnap-react/dist/analytics.js"}
@@ -1,2 +1,2 @@
1
- import{b as e,c as t,d as r,e as n,f as o,g as a,i as s,j as p,k as c,l as i,m,n as u,r as v}from"./chunk-PEVB3VL4.mjs";import"./chunk-MDCP5JNP.mjs";import"./chunk-U6FLIRIV.mjs";export{s as BitsnapAnalyticsProvider,t as DEFAULT_ENDPOINT,i as InstrumentProvider,r as createTracker,o as getTracker,n as initTracker,c as noopInstrument,a as resetTracker,v as scoreBucket,e as startAutoPageViews,p as useAnalytics,u as useInstrument,m as useInstrumentContext};
1
+ import{b as e,c as t,d as r,e as n,f as o,g as a,i as s,j as p,k as c,l as i,m,n as u,r as v}from"./chunk-QQQ2JSPQ.mjs";import"./chunk-MDCP5JNP.mjs";import"./chunk-U6FLIRIV.mjs";export{s as BitsnapAnalyticsProvider,t as DEFAULT_ENDPOINT,i as InstrumentProvider,r as createTracker,o as getTracker,n as initTracker,c as noopInstrument,a as resetTracker,v as scoreBucket,e as startAutoPageViews,p as useAnalytics,u as useInstrument,m as useInstrumentContext};
2
2
  //# sourceMappingURL=analytics.mjs.map
@@ -52,7 +52,7 @@ type ProductListAppearanceTokens = {
52
52
  * Stable slot names. They are exposed as `data-bitsnap-products-slot`
53
53
  * attributes so they can also be targeted from plain CSS.
54
54
  */
55
- declare const PRODUCT_LIST_SLOT_NAMES: readonly ["root", "grid", "card", "cardLink", "image", "imagePlaceholder", "body", "name", "description", "price", "comparePrice", "badge", "actions", "addToCartButton", "selectButton", "message", "pagination", "paginationButton", "pageIndicator", "emptyState", "error", "retryButton", "skeleton"];
55
+ declare const PRODUCT_LIST_SLOT_NAMES: readonly ["root", "grid", "card", "cardLink", "image", "imagePlaceholder", "body", "name", "description", "price", "comparePrice", "badge", "model3dBadge", "actions", "addToCartButton", "selectButton", "message", "pagination", "paginationButton", "pageIndicator", "emptyState", "error", "retryButton", "skeleton"];
56
56
  type ProductListSlotName = (typeof PRODUCT_LIST_SLOT_NAMES)[number];
57
57
  type ProductListSlotAppearance = SlotAppearance;
58
58
  type ProductListAppearanceElements = AppearanceElements<ProductListSlotName>;
@@ -90,6 +90,10 @@ type ProductListTexts = {
90
90
  unavailable: string;
91
91
  /** Badge of subscription products. */
92
92
  subscription: string;
93
+ /** Badge of products with a 3D model. */
94
+ model3d: string;
95
+ /** Screen reader label of the 3D badge. */
96
+ model3dLabel: string;
93
97
  /** Price of products whose variants differ in price. `{price}` is the lowest one. */
94
98
  priceFrom: string;
95
99
  /** Screen reader label of the struck-through regular price. */
package/dist/catalog.d.ts CHANGED
@@ -52,7 +52,7 @@ type ProductListAppearanceTokens = {
52
52
  * Stable slot names. They are exposed as `data-bitsnap-products-slot`
53
53
  * attributes so they can also be targeted from plain CSS.
54
54
  */
55
- declare const PRODUCT_LIST_SLOT_NAMES: readonly ["root", "grid", "card", "cardLink", "image", "imagePlaceholder", "body", "name", "description", "price", "comparePrice", "badge", "actions", "addToCartButton", "selectButton", "message", "pagination", "paginationButton", "pageIndicator", "emptyState", "error", "retryButton", "skeleton"];
55
+ declare const PRODUCT_LIST_SLOT_NAMES: readonly ["root", "grid", "card", "cardLink", "image", "imagePlaceholder", "body", "name", "description", "price", "comparePrice", "badge", "model3dBadge", "actions", "addToCartButton", "selectButton", "message", "pagination", "paginationButton", "pageIndicator", "emptyState", "error", "retryButton", "skeleton"];
56
56
  type ProductListSlotName = (typeof PRODUCT_LIST_SLOT_NAMES)[number];
57
57
  type ProductListSlotAppearance = SlotAppearance;
58
58
  type ProductListAppearanceElements = AppearanceElements<ProductListSlotName>;
@@ -90,6 +90,10 @@ type ProductListTexts = {
90
90
  unavailable: string;
91
91
  /** Badge of subscription products. */
92
92
  subscription: string;
93
+ /** Badge of products with a 3D model. */
94
+ model3d: string;
95
+ /** Screen reader label of the 3D badge. */
96
+ model3dLabel: string;
93
97
  /** Price of products whose variants differ in price. `{price}` is the lowest one. */
94
98
  priceFrom: string;
95
99
  /** Screen reader label of the struck-through regular price. */
package/dist/catalog.js CHANGED
@@ -1,2 +1,2 @@
1
- "use strict";Object.defineProperty(exports, "__esModule", {value: true});var _chunkGLAJPUMAjs = require('./chunk-GLAJPUMA.js');require('./chunk-KOYBZORL.js');require('./chunk-DY2S42Q6.js');require('./chunk-MG6VV5J2.js');require('./chunk-W3RGQYPD.js');require('./chunk-JSDGZ4NY.js');exports.PRODUCT_LIST_SLOT_NAMES = _chunkGLAJPUMAjs.a; exports.ProductList = _chunkGLAJPUMAjs.d; exports.formatProductPrice = _chunkGLAJPUMAjs.b; exports.useProducts = _chunkGLAJPUMAjs.c;
1
+ "use strict";Object.defineProperty(exports, "__esModule", {value: true});var _chunkOZHXRKMXjs = require('./chunk-OZHXRKMX.js');require('./chunk-KOYBZORL.js');require('./chunk-GXUNRJ5S.js');require('./chunk-GQJDFLSL.js');require('./chunk-7RXW6XFY.js');require('./chunk-W3RGQYPD.js');require('./chunk-JSDGZ4NY.js');exports.PRODUCT_LIST_SLOT_NAMES = _chunkOZHXRKMXjs.a; exports.ProductList = _chunkOZHXRKMXjs.d; exports.formatProductPrice = _chunkOZHXRKMXjs.b; exports.useProducts = _chunkOZHXRKMXjs.c;
2
2
  //# sourceMappingURL=catalog.js.map
@@ -1 +1 @@
1
- {"version":3,"sources":["/private/tmp/bsr-src/dist/catalog.js"],"names":[],"mappings":"AAAA,+HAA4D,+BAA4B,+BAA4B,+BAA4B,+BAA4B,+BAA4B,0LAA+F","file":"/private/tmp/bsr-src/dist/catalog.js"}
1
+ {"version":3,"sources":["/home/emems/web/bitsnap/bitsnap/bitsnap-react/dist/catalog.js"],"names":[],"mappings":"AAAA,+HAA4D,+BAA4B,+BAA4B,+BAA4B,+BAA4B,+BAA4B,+BAA4B,0LAA+F","file":"/home/emems/web/bitsnap/bitsnap/bitsnap-react/dist/catalog.js"}
package/dist/catalog.mjs CHANGED
@@ -1,2 +1,2 @@
1
- import{a as o,b as r,c as e,d as f}from"./chunk-ZIWQ57GY.mjs";import"./chunk-KOVKUJAW.mjs";import"./chunk-S4Z4O46O.mjs";import"./chunk-PEVB3VL4.mjs";import"./chunk-MDCP5JNP.mjs";import"./chunk-U6FLIRIV.mjs";export{o as PRODUCT_LIST_SLOT_NAMES,f as ProductList,r as formatProductPrice,e as useProducts};
1
+ import{a as o,b as e,c as f}from"./chunk-DPKWYLN7.mjs";import{f as r}from"./chunk-WKE6ECID.mjs";import"./chunk-KOVKUJAW.mjs";import"./chunk-5R6AGO3Z.mjs";import"./chunk-SXKJ26OA.mjs";import"./chunk-QQQ2JSPQ.mjs";import"./chunk-VNYHJT6V.mjs";import"./chunk-MDCP5JNP.mjs";import"./chunk-U6FLIRIV.mjs";export{o as PRODUCT_LIST_SLOT_NAMES,f as ProductList,r as formatProductPrice,e as useProducts};
2
2
  //# sourceMappingURL=catalog.mjs.map