@propeller-commerce/propeller-v2-react-ui 0.21.0 → 0.22.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.
package/CHANGELOG.md CHANGED
@@ -8,6 +8,47 @@ once it reaches 1.0. Until then (the `0.x` line) the public API may change
8
8
  between minor versions; breaking changes are called out below and in
9
9
  [MIGRATION.md](./MIGRATION.md).
10
10
 
11
+ ## [0.22.0] - 2026-09-24
12
+
13
+ ### Fixed
14
+
15
+ - **Media language is matched case-insensitively.** The API lowercases
16
+ `documents[].language` while leaving `alt[].language` as uploaded, so
17
+ `ProductDownloads` matched the label but never the file and served
18
+ `documents[0]` — the Dutch PDF under an English label. `ProductVideos` had
19
+ the identical comparison and worked only by data luck. Also fixed in
20
+ `useProductSearch.filterByLanguage`, where a case mismatch dropped **every**
21
+ product from the grid, and in `DefaultProductImage`. (PWP-984)
22
+ - **`checkoutAllowed` is usable, and fails closed.** It read the raw `companyId`
23
+ option rather than the resolved one, so a contact acting for their own company
24
+ matched no authorization config and every cart reported as within limit; and it
25
+ evaluated its own cart state, which stays null when the consumer only seeds a
26
+ `cartId`. The hook now hydrates a seeded cart, and reports `false` while that
27
+ is in flight rather than `true`. (PWP-988)
28
+ - **The place-order button is gated on the authorization limit.** An over-limit
29
+ purchaser opening checkout directly got an ungated button and a backend
30
+ rejection; `CartOverview` now shows an `authorizationRequired` message
31
+ instead, using the same predicate as the cart summary. (PWP-988)
32
+ - **Semi-closed surfaces no longer flash their logged-out state** for a signed-in
33
+ visitor while the profile loads. Every `isContentHidden` call site passes the
34
+ provider’s `isAuthenticated` (core-ui 0.9.0+). (PWP-989)
35
+ - **The cart sidebar slides again.** The overflow fix named only `transform` in
36
+ its transition, but Tailwind v4 emits `translate-x-*` as the `translate`
37
+ property, so the panel jumped. (PWP-1002)
38
+ - **Header triggers inherit their colour.** `AccountIconAndMenu` and
39
+ `CompanySwitcher` hardcoded `text-white`, which broke light headers;
40
+ `text-foreground` then broke dark ones. Both now use `text-inherit`, with the
41
+ hover tint derived from `currentColor`. (PWP-1005)
42
+
43
+ ### Added
44
+
45
+ - `CompanySwitcher` accepts `triggerClassName`; it had no class prop at all.
46
+ - `ProductInfo` accepts `cartId`, `createCart` and `onCartCreated`, and forwards
47
+ them to both the injected and the default add-to-cart. It declared none of
48
+ them, so the PDP’s add-to-cart always failed for a visitor without a cart.
49
+ `ProductCard` forwards the same to an injected component. (PWP-986)
50
+ - `CartOverview` resolves `user` and `companyId` from `PropellerProvider`.
51
+
11
52
  ## [0.21.0] - 2026-09-23
12
53
 
13
54
  `MachineGrid` stops losing rows, and becomes usable in a translated or
package/dist/index.cjs CHANGED
@@ -767,10 +767,29 @@ function useCart(options) {
767
767
  const [loading, setLoading] = React6.useState(false);
768
768
  const [error, setError] = React6.useState(null);
769
769
  const notesTimers = React6.useRef({});
770
- const checkoutAllowed = React6.useMemo(
771
- () => propellerV2CoreUi.isCheckoutAllowed(user, companyId, cart),
772
- [user, companyId, cart]
773
- );
770
+ React6.useEffect(() => {
771
+ if (!graphqlClient || !cartId || cart) return;
772
+ let cancelled = false;
773
+ (async () => {
774
+ try {
775
+ const fetched = await propellerV2CoreUi.createServices(graphqlClient).cart.getCart({
776
+ cartId,
777
+ imageSearchFilters: imageSearchFilters(),
778
+ imageVariantFilters: imageVariantFilters(),
779
+ language
780
+ });
781
+ if (!cancelled && fetched) setCart(fetched);
782
+ } catch {
783
+ }
784
+ })();
785
+ return () => {
786
+ cancelled = true;
787
+ };
788
+ }, [graphqlClient, cartId, cart, language, imageSearchFilters, imageVariantFilters]);
789
+ const checkoutAllowed = React6.useMemo(() => {
790
+ if (cartId && !cart) return false;
791
+ return propellerV2CoreUi.isCheckoutAllowed(user, resolvedCompanyId, cart);
792
+ }, [user, resolvedCompanyId, cart, cartId]);
774
793
  function getMinQuantity(product) {
775
794
  const min = product?.minimumQuantity;
776
795
  return min && min > 0 ? min : 1;
@@ -1591,7 +1610,8 @@ var INFRA_KEYS = [
1591
1610
  "includeTax",
1592
1611
  "currency",
1593
1612
  "configuration",
1594
- "portalMode"
1613
+ "portalMode",
1614
+ "isAuthenticated"
1595
1615
  ];
1596
1616
  function useInfraProps(props) {
1597
1617
  const ctx = usePropellerContext();
@@ -2268,10 +2288,11 @@ function useProductSearch(options) {
2268
2288
  const isLoading = !isControlled && internalLoading;
2269
2289
  function filterByLanguage(products, lang) {
2270
2290
  if (!lang) return products;
2291
+ const target = lang.toUpperCase();
2271
2292
  return products.filter((p) => {
2272
2293
  const names = p.names || p.names || [];
2273
2294
  if (!names || names.length === 0) return true;
2274
- return names.some((n) => n.language === lang);
2295
+ return names.some((n) => (n.language || "").toUpperCase() === target);
2275
2296
  });
2276
2297
  }
2277
2298
  const fetchProducts = React6.useCallback(async () => {
@@ -4228,7 +4249,7 @@ function AccountIconAndMenu(rawProps) {
4228
4249
  onClick: (event) => handleIconClick(),
4229
4250
  "aria-label": propellerV2CoreUi.getLabel(props.labels, "accountLabel", "Account"),
4230
4251
  "data-open": menuOpen ? "true" : "false",
4231
- className: cn(`propeller-account-menu__trigger inline-flex items-center gap-2 px-3 py-2 rounded-control text-sm font-medium transition-colors text-foreground hover:bg-surface-hover${props.iconClassName ? " " + props.iconClassName : ""}`),
4252
+ className: cn(`propeller-account-menu__trigger inline-flex items-center gap-2 px-3 py-2 rounded-control text-sm font-medium transition-colors text-inherit${props.iconClassName ? " " + props.iconClassName : ""}`),
4232
4253
  children: [
4233
4254
  /* @__PURE__ */ jsxRuntime.jsx(
4234
4255
  "svg",
@@ -7640,7 +7661,8 @@ function CartIconAndSidebar(rawProps) {
7640
7661
  ] });
7641
7662
  }
7642
7663
  var CartIconAndSidebar_default = CartIconAndSidebar;
7643
- function CartOverview(props) {
7664
+ function CartOverview(rawProps) {
7665
+ const props = useInfraProps(rawProps);
7644
7666
  const [reference, setReference] = React6.useState("");
7645
7667
  const [notes, setNotes] = React6.useState("");
7646
7668
  const [termsAccepted, setTermsAccepted] = React6.useState(false);
@@ -7650,6 +7672,7 @@ function CartOverview(props) {
7650
7672
  const showReference = props.showReference !== false;
7651
7673
  const showTermsAndConditions = props.showTermsAndConditions !== false;
7652
7674
  const showPurchaseButton = props.showPurchaseButton !== false;
7675
+ const overLimit = propellerV2CoreUi.isOverAuthorizationLimit(props.user, props.companyId, props.cart);
7653
7676
  const invoiceAddress = props.cart?.invoiceAddress;
7654
7677
  const deliveryAddress = props.cart?.deliveryAddress;
7655
7678
  const getCountryName = (code) => propellerV2CoreUi.getCountryName(code, props.countries);
@@ -7789,7 +7812,12 @@ function CartOverview(props) {
7789
7812
  ] });
7790
7813
  })() })
7791
7814
  ] }) : null,
7792
- showPurchaseButton ? /* @__PURE__ */ jsxRuntime.jsxs(
7815
+ showPurchaseButton && overLimit ? /* @__PURE__ */ jsxRuntime.jsx("p", { className: "propeller-cart-overview__authorization-required text-sm text-muted-foreground mt-2", children: propellerV2CoreUi.getLabel(
7816
+ props.labels,
7817
+ "authorizationRequired",
7818
+ "This order exceeds your authorization limit. Request authorization from the cart to continue."
7819
+ ) }) : null,
7820
+ showPurchaseButton && !overLimit ? /* @__PURE__ */ jsxRuntime.jsxs(
7793
7821
  "button",
7794
7822
  {
7795
7823
  type: "button",
@@ -8164,6 +8192,7 @@ var RESOLVE_SPEC = {
8164
8192
  currency: { infra: "currency", default: "\u20AC" },
8165
8193
  user: { infra: "user", default: null },
8166
8194
  portalMode: { infra: "portalMode" },
8195
+ isAuthenticated: { infra: "isAuthenticated" },
8167
8196
  columns: { grid: "columns", default: 3 },
8168
8197
  showPrice: { grid: "showPrice" },
8169
8198
  showStock: { grid: "showStock" },
@@ -8207,7 +8236,7 @@ function ClusterCard(rawProps) {
8207
8236
  const textLabelValues = resolveAttributeValues(cluster, props.textLabels).map((value) => ({
8208
8237
  value
8209
8238
  }));
8210
- const contentHidden = propellerV2CoreUi.isContentHidden(props.portalMode, props.user);
8239
+ const contentHidden = propellerV2CoreUi.isContentHidden(props.portalMode, props.user, props.isAuthenticated);
8211
8240
  const defaultProductInventory = contentHidden ? void 0 : cluster?.defaultProduct?.inventory;
8212
8241
  const useTax = props.includeTax !== void 0 ? !!props.includeTax : false;
8213
8242
  const priceObj = contentHidden ? void 0 : cluster?.defaultProduct?.price;
@@ -8634,7 +8663,7 @@ function LoginToOrderButton(props) {
8634
8663
  var LoginToOrderButton_default = LoginToOrderButton;
8635
8664
  function ProductPriceDisplay(props) {
8636
8665
  function isHidden2() {
8637
- return propellerV2CoreUi.isContentHidden(props.portalMode, props.user);
8666
+ return propellerV2CoreUi.isContentHidden(props.portalMode, props.user, props.isAuthenticated);
8638
8667
  }
8639
8668
  function formatPrice17(value) {
8640
8669
  if (value === null || value === void 0) return "";
@@ -8726,6 +8755,7 @@ var RESOLVE_SPEC2 = {
8726
8755
  language: { infra: "language", default: "NL" },
8727
8756
  includeTax: { infra: "includeTax" },
8728
8757
  portalMode: { infra: "portalMode" },
8758
+ isAuthenticated: { infra: "isAuthenticated" },
8729
8759
  configuration: { infra: "configuration" },
8730
8760
  // Same entry ClusterCard has always had. Without it a shop priced in
8731
8761
  // anything but euros had no route to the cards at all: the grid resolved the
@@ -8784,7 +8814,7 @@ function ProductCardRoot(rawProps) {
8784
8814
  const language = props.language || "NL";
8785
8815
  const product = props.product;
8786
8816
  const isRow = props.columns === 1;
8787
- const contentHidden = propellerV2CoreUi.isContentHidden(props.portalMode, props.user);
8817
+ const contentHidden = propellerV2CoreUi.isContentHidden(props.portalMode, props.user, props.isAuthenticated);
8788
8818
  const showStock = !!props.showStock && !contentHidden;
8789
8819
  const showCta = props.allowAddToCart !== false || contentHidden;
8790
8820
  const name = getProductName(product, language);
@@ -9081,6 +9111,7 @@ function ProductCardPrice(props) {
9081
9111
  currency: resolved.currency,
9082
9112
  language: resolved.language,
9083
9113
  portalMode: resolved.portalMode,
9114
+ isAuthenticated: resolved.isAuthenticated,
9084
9115
  user: resolved.user,
9085
9116
  showLoginPrompt: false
9086
9117
  }
@@ -9088,7 +9119,7 @@ function ProductCardPrice(props) {
9088
9119
  }
9089
9120
  function ProductCardAddToCart(props) {
9090
9121
  const { product, resolved } = useProductCard();
9091
- if (propellerV2CoreUi.isContentHidden(resolved.portalMode, resolved.user)) {
9122
+ if (propellerV2CoreUi.isContentHidden(resolved.portalMode, resolved.user, resolved.isAuthenticated)) {
9092
9123
  return /* @__PURE__ */ jsxRuntime.jsx(
9093
9124
  LoginToOrderButton_default,
9094
9125
  {
@@ -9109,9 +9140,13 @@ function ProductCardAddToCart(props) {
9109
9140
  allowIncrDecr: resolved.allowIncrDecr,
9110
9141
  showModal: resolved.showModal,
9111
9142
  enableStockValidation: resolved.enableStockValidation,
9143
+ onAddToCart: resolved.onAddToCart,
9112
9144
  afterAddToCart: resolved.afterAddToCart,
9113
9145
  onProceedToCheckout: resolved.onProceedToCheckout,
9114
9146
  onRequestQuoteClick: resolved.onRequestQuoteClick,
9147
+ createCart: resolved.createCart,
9148
+ onCartCreated: resolved.onCartCreated,
9149
+ includeTax: resolved.includeTax,
9115
9150
  labels: resolved.addToCartLabels
9116
9151
  }
9117
9152
  );
@@ -9234,7 +9269,7 @@ function ProductGrid(rawProps) {
9234
9269
  }
9235
9270
  function showAddToCart() {
9236
9271
  const allow = props.allowAddToCart !== false;
9237
- return !propellerV2CoreUi.isContentHidden(props.portalMode, props.user) && allow;
9272
+ return !propellerV2CoreUi.isContentHidden(props.portalMode, props.user, props.isAuthenticated) && allow;
9238
9273
  }
9239
9274
  function getSkeletonItems() {
9240
9275
  const cols = props.columns || 3;
@@ -9512,7 +9547,7 @@ function GridFilters(rawProps) {
9512
9547
  }
9513
9548
  function showAvailability() {
9514
9549
  if (!props.showAvailabilityFilter) return false;
9515
- return !propellerV2CoreUi.isContentHidden(props.portalMode, props.user);
9550
+ return !propellerV2CoreUi.isContentHidden(props.portalMode, props.user, props.isAuthenticated);
9516
9551
  }
9517
9552
  function getFilterName(filter) {
9518
9553
  return filter?.attributeDescription?.name || "";
@@ -10067,7 +10102,7 @@ function GridToolbar(rawProps) {
10067
10102
  return badges;
10068
10103
  }
10069
10104
  function isPriceSortDisabled() {
10070
- return propellerV2CoreUi.isContentHidden(props.portalMode, props.user);
10105
+ return propellerV2CoreUi.isContentHidden(props.portalMode, props.user, props.isAuthenticated);
10071
10106
  }
10072
10107
  function getAvailabilityLabel() {
10073
10108
  const qty = props.minStock;
@@ -10920,7 +10955,7 @@ function ClusterOptions(rawProps) {
10920
10955
  const language = props.language;
10921
10956
  const options = props.options || [];
10922
10957
  const sel = selectedProductIds;
10923
- const hidePrices = propellerV2CoreUi.isContentHidden(props.portalMode, props.user);
10958
+ const hidePrices = propellerV2CoreUi.isContentHidden(props.portalMode, props.user, props.isAuthenticated);
10924
10959
  return options.filter((option) => option.hidden !== propellerSdkV2.YesNo.Y).map((option) => {
10925
10960
  const idStr = option.id.toString();
10926
10961
  const selectedProductId = sel[idStr] || "";
@@ -11104,7 +11139,11 @@ function CompanySwitcher(props) {
11104
11139
  type: "button",
11105
11140
  "aria-haspopup": "listbox",
11106
11141
  "aria-label": propellerV2CoreUi.getLabel(props.labels, "switchCompanyAriaLabel", "Switch company"),
11107
- className: "propeller-company-switcher__trigger flex items-center gap-2 rounded-md px-3 py-1.5 text-sm font-medium transition-colors hover:bg-white/10",
11142
+ className: cn(
11143
+ // `text-inherit`: this sits in the host's header, which may be dark or
11144
+ // light. See the note on AccountIconAndMenu's trigger.
11145
+ `propeller-company-switcher__trigger flex items-center gap-2 rounded-md px-3 py-1.5 text-sm font-medium transition-colors text-inherit${props.triggerClassName ? " " + props.triggerClassName : ""}`
11146
+ ),
11108
11147
  onClick: (event) => toggleDropdown(),
11109
11148
  "aria-expanded": isOpen,
11110
11149
  children: [
@@ -15079,7 +15118,7 @@ function PriceToggle(props) {
15079
15118
  var PriceToggle_default = PriceToggle;
15080
15119
  function ProductBulkPrices(props) {
15081
15120
  const includeTax = !!props.includeTax;
15082
- const isHidden2 = propellerV2CoreUi.isContentHidden(props.portalMode, props.user);
15121
+ const isHidden2 = propellerV2CoreUi.isContentHidden(props.portalMode, props.user, props.isAuthenticated);
15083
15122
  function getTierQuantity(tier) {
15084
15123
  const discount = tier.discount;
15085
15124
  return discount?.quantityFrom ?? tier.quantity ?? null;
@@ -15626,14 +15665,10 @@ function ProductDownloads(props) {
15626
15665
  const hasItems = items.length > 0;
15627
15666
  const lang = props.language || "NL";
15628
15667
  function getDocumentUrl(doc) {
15629
- const docs = doc.documents || [];
15630
- const match = docs.find((d) => d.language === lang);
15631
- return match?.originalUrl || docs?.[0]?.originalUrl || "";
15668
+ return propellerV2CoreUi.resolveLanguageEntry(doc.documents, lang)?.originalUrl || "";
15632
15669
  }
15633
15670
  function getDocumentName(doc) {
15634
- const alts = doc.alt || [];
15635
- const match = alts.find((a) => a.language === lang);
15636
- return match?.value || alts?.[0]?.value || "Download";
15671
+ return propellerV2CoreUi.getLanguageString(doc.alt, lang, "Download");
15637
15672
  }
15638
15673
  return /* @__PURE__ */ jsxRuntime.jsxs("div", { className: cn(`propeller-product-downloads ${props.className || ""}`), children: [
15639
15674
  hasItems ? /* @__PURE__ */ jsxRuntime.jsx("h3", { className: "propeller-product-downloads__title text-base font-semibold text-foreground mb-3", children: propellerV2CoreUi.getLabel(props.labels, "title", "Downloads") }) : null,
@@ -15839,12 +15874,14 @@ function ProductGallery(props) {
15839
15874
  var ProductGallery_default = ProductGallery;
15840
15875
  function pickImageUrl(mediaItems, target) {
15841
15876
  if (!mediaItems || mediaItems.length === 0) return null;
15877
+ const targetUpper = (target || "").toUpperCase();
15878
+ const langMatches = (value) => (value || "").toUpperCase() === targetUpper;
15842
15879
  for (const m of mediaItems) {
15843
- const match = m.imageVariants?.find((v) => v.language === target && v.url);
15880
+ const match = m.imageVariants?.find((v) => langMatches(v.language) && v.url);
15844
15881
  if (match?.url) return match.url;
15845
15882
  }
15846
15883
  for (const m of mediaItems) {
15847
- const match = m.images?.find((i) => i.language === target && i.originalUrl);
15884
+ const match = m.images?.find((i) => langMatches(i.language) && i.originalUrl);
15848
15885
  if (match?.originalUrl) return match.originalUrl;
15849
15886
  }
15850
15887
  for (const m of mediaItems) {
@@ -16172,6 +16209,7 @@ function ProductInfoPrice(props = {}) {
16172
16209
  currency: derived.currency,
16173
16210
  taxZone: derived.taxZone,
16174
16211
  portalMode: derived.portalMode,
16212
+ isAuthenticated: resolved.isAuthenticated,
16175
16213
  user: derived.user,
16176
16214
  labels: derived.labels,
16177
16215
  className: props.className
@@ -16181,7 +16219,7 @@ function ProductInfoPrice(props = {}) {
16181
16219
  function ProductInfoStock(props = {}) {
16182
16220
  const { product, resolved, derived } = useProductInfoContext();
16183
16221
  if (!product.inventory) return null;
16184
- if (propellerV2CoreUi.isContentHidden(derived.portalMode, resolved.user)) return null;
16222
+ if (propellerV2CoreUi.isContentHidden(derived.portalMode, resolved.user, resolved.isAuthenticated)) return null;
16185
16223
  const Injected = resolved.stockComponent;
16186
16224
  if (Injected) {
16187
16225
  return /* @__PURE__ */ jsxRuntime.jsx(
@@ -16208,7 +16246,7 @@ function ProductInfoStock(props = {}) {
16208
16246
  }
16209
16247
  function ProductInfoAddToCart(props = {}) {
16210
16248
  const { product, resolved, derived } = useProductInfoContext();
16211
- if (propellerV2CoreUi.isContentHidden(derived.portalMode, resolved.user)) {
16249
+ if (propellerV2CoreUi.isContentHidden(derived.portalMode, resolved.user, resolved.isAuthenticated)) {
16212
16250
  return /* @__PURE__ */ jsxRuntime.jsx(
16213
16251
  LoginToOrderButton_default,
16214
16252
  {
@@ -16220,7 +16258,18 @@ function ProductInfoAddToCart(props = {}) {
16220
16258
  }
16221
16259
  const Injected = resolved.addToCartComponent;
16222
16260
  if (Injected) {
16223
- return /* @__PURE__ */ jsxRuntime.jsx(Injected, { product, labels: derived.labels, className: props.className });
16261
+ return /* @__PURE__ */ jsxRuntime.jsx(
16262
+ Injected,
16263
+ {
16264
+ product,
16265
+ cartId: resolved.cartId,
16266
+ createCart: resolved.createCart,
16267
+ onCartCreated: resolved.onCartCreated,
16268
+ includeTax: derived.useTax,
16269
+ labels: derived.labels,
16270
+ className: props.className
16271
+ }
16272
+ );
16224
16273
  }
16225
16274
  return /* @__PURE__ */ jsxRuntime.jsx(
16226
16275
  AddToCart_default,
@@ -16229,6 +16278,9 @@ function ProductInfoAddToCart(props = {}) {
16229
16278
  user: derived.user,
16230
16279
  currency: derived.currency,
16231
16280
  product,
16281
+ cartId: resolved.cartId,
16282
+ createCart: resolved.createCart,
16283
+ onCartCreated: resolved.onCartCreated,
16232
16284
  configuration: resolved.configuration,
16233
16285
  companyId: resolved.companyId,
16234
16286
  includeTax: derived.useTax,
@@ -16282,6 +16334,7 @@ function ProductInfoBulkPrices(props = {}) {
16282
16334
  bulkPrices: product.bulkPrices,
16283
16335
  includeTax: derived.useTax,
16284
16336
  portalMode: derived.portalMode,
16337
+ isAuthenticated: resolved.isAuthenticated,
16285
16338
  user: derived.user,
16286
16339
  taxZone: derived.taxZone,
16287
16340
  currency: derived.currency,
@@ -16444,7 +16497,7 @@ function ProductSlider(rawProps) {
16444
16497
  }
16445
16498
  function showAddToCart() {
16446
16499
  const allow = props.allowAddToCart !== false;
16447
- return !propellerV2CoreUi.isContentHidden(props.portalMode, props.user) && allow;
16500
+ return !propellerV2CoreUi.isContentHidden(props.portalMode, props.user, props.isAuthenticated) && allow;
16448
16501
  }
16449
16502
  function isCluster(item) {
16450
16503
  return "clusterId" in item && !("productId" in item);
@@ -16748,14 +16801,10 @@ function ProductVideos(props) {
16748
16801
  const hasItems = items.length > 0;
16749
16802
  const lang = props.language || "NL";
16750
16803
  function getVideoUri(video) {
16751
- const vids = video.videos || [];
16752
- const match = vids.find((v) => v.language === lang);
16753
- return match?.uri || vids?.[0]?.uri || "";
16804
+ return propellerV2CoreUi.getLanguageUri(video.videos, lang, "");
16754
16805
  }
16755
16806
  function getVideoTitle(video) {
16756
- const alts = video.alt || [];
16757
- const match = alts.find((a) => a.language === lang);
16758
- return match?.value || alts?.[0]?.value || "Video";
16807
+ return propellerV2CoreUi.getLanguageString(video.alt, lang, "Video");
16759
16808
  }
16760
16809
  function isEmbeddable(uri) {
16761
16810
  return uri.includes("youtube.com") || uri.includes("youtu.be") || uri.includes("vimeo.com");