@shopify/hydrogen 2026.10.0-preview.4 → 2026.10.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/dist/cli/index.mjs +23 -12
- package/dist/client/client.d.mts +18 -15
- package/dist/client/client.mjs +8 -3
- package/dist/client/client.mjs.map +1 -1
- package/dist/client/errors.d.mts +35 -13
- package/dist/client/errors.mjs +23 -13
- package/dist/client/errors.mjs.map +1 -1
- package/dist/client/types.d.mts +140 -90
- package/dist/core/analytics/cart-tracker.d.mts +19 -10
- package/dist/core/analytics/cart-tracker.mjs +18 -10
- package/dist/core/analytics/cart-tracker.mjs.map +1 -1
- package/dist/core/analytics/events.d.mts +17 -11
- package/dist/core/analytics/events.mjs +13 -4
- package/dist/core/analytics/events.mjs.map +1 -1
- package/dist/core/analytics/types.d.mts +207 -118
- package/dist/core/analytics/utils/flatten-connection.d.mts +11 -9
- package/dist/core/analytics/utils/flatten-connection.mjs +7 -6
- package/dist/core/analytics/utils/flatten-connection.mjs.map +1 -1
- package/dist/core/cache/fetch-with-cache.d.mts +24 -4
- package/dist/core/cache/fetch-with-cache.mjs +17 -2
- package/dist/core/cache/fetch-with-cache.mjs.map +1 -1
- package/dist/core/cache/key.d.mts +2 -0
- package/dist/core/cache/key.mjs.map +1 -1
- package/dist/core/cache/run-with-cache.d.mts +56 -8
- package/dist/core/cache/run-with-cache.mjs +17 -6
- package/dist/core/cache/run-with-cache.mjs.map +1 -1
- package/dist/core/cache/store.d.mts +8 -6
- package/dist/core/cache/store.mjs.map +1 -1
- package/dist/core/cache/strategies.d.mts +27 -2
- package/dist/core/cache/strategies.mjs +6 -0
- package/dist/core/cache/strategies.mjs.map +1 -1
- package/dist/core/cart/actions.d.mts +37 -30
- package/dist/core/cart/actions.mjs +21 -9
- package/dist/core/cart/actions.mjs.map +1 -1
- package/dist/core/cart/attach-quantity-input.d.mts +8 -7
- package/dist/core/cart/attach-quantity-input.mjs +8 -7
- package/dist/core/cart/attach-quantity-input.mjs.map +1 -1
- package/dist/core/cart/buyer-identity-sync.d.mts +7 -6
- package/dist/core/cart/buyer-identity-sync.mjs.map +1 -1
- package/dist/core/cart/cart.d.mts +59 -46
- package/dist/core/cart/cart.mjs +46 -29
- package/dist/core/cart/cart.mjs.map +1 -1
- package/dist/core/cart/cookie.d.mts +8 -4
- package/dist/core/cart/cookie.mjs +8 -4
- package/dist/core/cart/cookie.mjs.map +1 -1
- package/dist/core/cart/form.d.mts +34 -20
- package/dist/core/cart/form.mjs +6 -4
- package/dist/core/cart/form.mjs.map +1 -1
- package/dist/core/cart/get-cart.d.mts +27 -15
- package/dist/core/cart/get-cart.mjs +13 -8
- package/dist/core/cart/get-cart.mjs.map +1 -1
- package/dist/core/cart/quantity.d.mts +16 -5
- package/dist/core/cart/quantity.mjs +16 -5
- package/dist/core/cart/quantity.mjs.map +1 -1
- package/dist/core/cart/queries.d.mts +25 -19
- package/dist/core/cart/queries.mjs +15 -1
- package/dist/core/cart/queries.mjs.map +1 -1
- package/dist/core/cart/server-handlers.d.mts +82 -45
- package/dist/core/cart/server-handlers.mjs +4 -0
- package/dist/core/cart/server-handlers.mjs.map +1 -1
- package/dist/core/cart/state.d.mts +110 -64
- package/dist/core/cart/state.mjs +24 -6
- package/dist/core/cart/state.mjs.map +1 -1
- package/dist/core/collection/collection.d.mts +52 -53
- package/dist/core/collection/collection.mjs +5 -3
- package/dist/core/collection/collection.mjs.map +1 -1
- package/dist/core/collection/reconciler.d.mts +23 -16
- package/dist/core/collection/reconciler.mjs +8 -4
- package/dist/core/collection/reconciler.mjs.map +1 -1
- package/dist/core/collection/state.d.mts +31 -26
- package/dist/core/collection/state.mjs +4 -3
- package/dist/core/collection/state.mjs.map +1 -1
- package/dist/core/collection/url.d.mts +70 -29
- package/dist/core/collection/url.mjs +63 -25
- package/dist/core/collection/url.mjs.map +1 -1
- package/dist/core/constants.d.mts +1 -0
- package/dist/core/constants.mjs +1 -0
- package/dist/core/constants.mjs.map +1 -1
- package/dist/core/logging/logging.d.mts +14 -8
- package/dist/core/logging/logging.mjs +15 -10
- package/dist/core/logging/logging.mjs.map +1 -1
- package/dist/core/logging/types.d.mts +28 -11
- package/dist/core/logging/types.mjs.map +1 -1
- package/dist/core/money/format.d.mts +6 -4
- package/dist/core/money/format.mjs +19 -9
- package/dist/core/money/format.mjs.map +1 -1
- package/dist/core/money/types.d.mts +41 -25
- package/dist/core/predictive-search/form.d.mts +37 -7
- package/dist/core/predictive-search/form.mjs +21 -3
- package/dist/core/predictive-search/form.mjs.map +1 -1
- package/dist/core/predictive-search/queries.d.mts +18 -30
- package/dist/core/predictive-search/queries.mjs +7 -3
- package/dist/core/predictive-search/queries.mjs.map +1 -1
- package/dist/core/predictive-search/search.d.mts +25 -27
- package/dist/core/predictive-search/search.mjs +20 -19
- package/dist/core/predictive-search/search.mjs.map +1 -1
- package/dist/core/predictive-search/server-handlers.d.mts +25 -26
- package/dist/core/predictive-search/server-handlers.mjs +4 -0
- package/dist/core/predictive-search/server-handlers.mjs.map +1 -1
- package/dist/core/predictive-search/store.d.mts +65 -24
- package/dist/core/predictive-search/store.mjs +12 -1
- package/dist/core/predictive-search/store.mjs.map +1 -1
- package/dist/core/predictive-search/url.d.mts +36 -37
- package/dist/core/predictive-search/url.mjs +14 -6
- package/dist/core/predictive-search/url.mjs.map +1 -1
- package/dist/core/product/form.d.mts +63 -16
- package/dist/core/product/form.mjs +8 -2
- package/dist/core/product/form.mjs.map +1 -1
- package/dist/core/product/options.d.mts +8 -10
- package/dist/core/product/options.mjs +8 -10
- package/dist/core/product/options.mjs.map +1 -1
- package/dist/core/product/product-form.d.mts +77 -29
- package/dist/core/product/product-form.mjs +32 -10
- package/dist/core/product/product-form.mjs.map +1 -1
- package/dist/core/product/state.d.mts +55 -32
- package/dist/core/product/url.d.mts +19 -13
- package/dist/core/product/url.mjs +10 -10
- package/dist/core/product/url.mjs.map +1 -1
- package/dist/core/request-context.d.mts +77 -19
- package/dist/core/request-context.mjs +5 -1
- package/dist/core/request-context.mjs.map +1 -1
- package/dist/core/request-routing/handle-shopify-redirects.d.mts +12 -4
- package/dist/core/request-routing/handle-shopify-redirects.mjs +9 -2
- package/dist/core/request-routing/handle-shopify-redirects.mjs.map +1 -1
- package/dist/core/request-routing/handle-shopify-routes.d.mts +5 -5
- package/dist/core/request-routing/handle-shopify-routes.mjs +7 -5
- package/dist/core/request-routing/handle-shopify-routes.mjs.map +1 -1
- package/dist/core/request-routing/interceptors/proxy.mjs +79 -43
- package/dist/core/request-routing/interceptors/proxy.mjs.map +1 -1
- package/dist/core/request-routing/interceptors/ucp-profile-proxy.mjs +69 -0
- package/dist/core/request-routing/interceptors/ucp-profile-proxy.mjs.map +1 -0
- package/dist/core/request-routing/interceptors/url-redirects.mjs +6 -1
- package/dist/core/request-routing/interceptors/url-redirects.mjs.map +1 -1
- package/dist/core/request-routing/interceptors/well-known.mjs +1 -1
- package/dist/core/request-routing/interceptors/well-known.mjs.map +1 -1
- package/dist/core/request-routing/registered-routes.d.mts +12 -0
- package/dist/core/request-routing/registered-routes.mjs +12 -0
- package/dist/core/request-routing/registered-routes.mjs.map +1 -1
- package/dist/core/request-routing/route-types.d.mts +43 -18
- package/dist/core/shop-pay/shop-pay.d.mts +71 -35
- package/dist/core/shop-pay/shop-pay.mjs +36 -10
- package/dist/core/shop-pay/shop-pay.mjs.map +1 -1
- package/dist/core/shopify-scripts/index.d.mts +15 -5
- package/dist/core/shopify-scripts/index.mjs +15 -5
- package/dist/core/shopify-scripts/index.mjs.map +1 -1
- package/dist/core/shopify-scripts/initialize.d.mts +12 -4
- package/dist/core/shopify-scripts/initialize.mjs +12 -4
- package/dist/core/shopify-scripts/initialize.mjs.map +1 -1
- package/dist/core/shopify-scripts/types.d.mts +58 -25
- package/dist/core/standard-routes/build.d.mts +11 -12
- package/dist/core/standard-routes/build.mjs +11 -12
- package/dist/core/standard-routes/build.mjs.map +1 -1
- package/dist/core/standard-routes/types.d.mts +30 -24
- package/dist/core/url.mjs +2 -1
- package/dist/core/url.mjs.map +1 -1
- package/dist/customer-account/client.d.mts +36 -69
- package/dist/customer-account/client.mjs +7 -8
- package/dist/customer-account/client.mjs.map +1 -1
- package/dist/customer-account/errors.d.mts +45 -36
- package/dist/customer-account/errors.mjs +21 -36
- package/dist/customer-account/errors.mjs.map +1 -1
- package/dist/customer-account/graphql.d.mts +11 -23
- package/dist/customer-account/graphql.mjs +3 -9
- package/dist/customer-account/graphql.mjs.map +1 -1
- package/dist/customer-account/session.d.mts +130 -100
- package/dist/customer-account/session.mjs +32 -10
- package/dist/customer-account/session.mjs.map +1 -1
- package/dist/customer-account/type-resolver.d.mts +7 -8
- package/dist/development/client/client.d.mts +18 -15
- package/dist/development/client/client.mjs +8 -3
- package/dist/development/client/client.mjs.map +1 -1
- package/dist/development/client/errors.d.mts +35 -13
- package/dist/development/client/errors.mjs +23 -13
- package/dist/development/client/errors.mjs.map +1 -1
- package/dist/development/client/types.d.mts +140 -90
- package/dist/development/core/analytics/cart-tracker.d.mts +19 -10
- package/dist/development/core/analytics/cart-tracker.mjs +18 -10
- package/dist/development/core/analytics/cart-tracker.mjs.map +1 -1
- package/dist/development/core/analytics/events.d.mts +17 -11
- package/dist/development/core/analytics/events.mjs +13 -4
- package/dist/development/core/analytics/events.mjs.map +1 -1
- package/dist/development/core/analytics/types.d.mts +207 -118
- package/dist/development/core/analytics/utils/flatten-connection.d.mts +11 -9
- package/dist/development/core/analytics/utils/flatten-connection.mjs +7 -6
- package/dist/development/core/analytics/utils/flatten-connection.mjs.map +1 -1
- package/dist/development/core/cache/fetch-with-cache.d.mts +24 -4
- package/dist/development/core/cache/fetch-with-cache.mjs +17 -2
- package/dist/development/core/cache/fetch-with-cache.mjs.map +1 -1
- package/dist/development/core/cache/key.d.mts +2 -0
- package/dist/development/core/cache/key.mjs.map +1 -1
- package/dist/development/core/cache/run-with-cache.d.mts +56 -8
- package/dist/development/core/cache/run-with-cache.mjs +17 -6
- package/dist/development/core/cache/run-with-cache.mjs.map +1 -1
- package/dist/development/core/cache/store.d.mts +8 -6
- package/dist/development/core/cache/store.mjs.map +1 -1
- package/dist/development/core/cache/strategies.d.mts +27 -2
- package/dist/development/core/cache/strategies.mjs +6 -0
- package/dist/development/core/cache/strategies.mjs.map +1 -1
- package/dist/development/core/cart/actions.d.mts +37 -30
- package/dist/development/core/cart/actions.mjs +21 -9
- package/dist/development/core/cart/actions.mjs.map +1 -1
- package/dist/development/core/cart/attach-quantity-input.d.mts +8 -7
- package/dist/development/core/cart/attach-quantity-input.mjs +8 -7
- package/dist/development/core/cart/attach-quantity-input.mjs.map +1 -1
- package/dist/development/core/cart/buyer-identity-sync.d.mts +6 -5
- package/dist/development/core/cart/buyer-identity-sync.mjs.map +1 -1
- package/dist/development/core/cart/cart.d.mts +59 -46
- package/dist/development/core/cart/cart.mjs +46 -29
- package/dist/development/core/cart/cart.mjs.map +1 -1
- package/dist/development/core/cart/cookie.d.mts +8 -4
- package/dist/development/core/cart/cookie.mjs +8 -4
- package/dist/development/core/cart/cookie.mjs.map +1 -1
- package/dist/development/core/cart/form.d.mts +34 -20
- package/dist/development/core/cart/form.mjs +6 -4
- package/dist/development/core/cart/form.mjs.map +1 -1
- package/dist/development/core/cart/get-cart.d.mts +27 -15
- package/dist/development/core/cart/get-cart.mjs +13 -8
- package/dist/development/core/cart/get-cart.mjs.map +1 -1
- package/dist/development/core/cart/quantity.d.mts +16 -5
- package/dist/development/core/cart/quantity.mjs +16 -5
- package/dist/development/core/cart/quantity.mjs.map +1 -1
- package/dist/development/core/cart/queries.d.mts +25 -19
- package/dist/development/core/cart/queries.mjs +15 -1
- package/dist/development/core/cart/queries.mjs.map +1 -1
- package/dist/development/core/cart/server-handlers.d.mts +82 -45
- package/dist/development/core/cart/server-handlers.mjs +4 -0
- package/dist/development/core/cart/server-handlers.mjs.map +1 -1
- package/dist/development/core/cart/state.d.mts +110 -64
- package/dist/development/core/cart/state.mjs +24 -6
- package/dist/development/core/cart/state.mjs.map +1 -1
- package/dist/development/core/collection/collection.d.mts +52 -53
- package/dist/development/core/collection/collection.mjs +5 -3
- package/dist/development/core/collection/collection.mjs.map +1 -1
- package/dist/development/core/collection/reconciler.d.mts +23 -16
- package/dist/development/core/collection/reconciler.mjs +8 -4
- package/dist/development/core/collection/reconciler.mjs.map +1 -1
- package/dist/development/core/collection/state.d.mts +31 -26
- package/dist/development/core/collection/state.mjs +4 -3
- package/dist/development/core/collection/state.mjs.map +1 -1
- package/dist/development/core/collection/url.d.mts +70 -29
- package/dist/development/core/collection/url.mjs +63 -25
- package/dist/development/core/collection/url.mjs.map +1 -1
- package/dist/development/core/constants.mjs.map +1 -1
- package/dist/development/core/logging/logging.d.mts +14 -8
- package/dist/development/core/logging/logging.mjs +15 -10
- package/dist/development/core/logging/logging.mjs.map +1 -1
- package/dist/development/core/logging/types.d.mts +28 -11
- package/dist/development/core/logging/types.mjs.map +1 -1
- package/dist/development/core/money/format.d.mts +6 -4
- package/dist/development/core/money/format.mjs +19 -9
- package/dist/development/core/money/format.mjs.map +1 -1
- package/dist/development/core/money/types.d.mts +41 -25
- package/dist/development/core/predictive-search/form.d.mts +37 -7
- package/dist/development/core/predictive-search/form.mjs +21 -3
- package/dist/development/core/predictive-search/form.mjs.map +1 -1
- package/dist/development/core/predictive-search/queries.d.mts +18 -30
- package/dist/development/core/predictive-search/queries.mjs +7 -3
- package/dist/development/core/predictive-search/queries.mjs.map +1 -1
- package/dist/development/core/predictive-search/search.d.mts +25 -27
- package/dist/development/core/predictive-search/search.mjs +20 -19
- package/dist/development/core/predictive-search/search.mjs.map +1 -1
- package/dist/development/core/predictive-search/server-handlers.d.mts +25 -26
- package/dist/development/core/predictive-search/server-handlers.mjs +4 -0
- package/dist/development/core/predictive-search/server-handlers.mjs.map +1 -1
- package/dist/development/core/predictive-search/store.d.mts +65 -24
- package/dist/development/core/predictive-search/store.mjs +12 -1
- package/dist/development/core/predictive-search/store.mjs.map +1 -1
- package/dist/development/core/predictive-search/url.d.mts +36 -37
- package/dist/development/core/predictive-search/url.mjs +14 -6
- package/dist/development/core/predictive-search/url.mjs.map +1 -1
- package/dist/development/core/product/form.d.mts +63 -16
- package/dist/development/core/product/form.mjs +8 -2
- package/dist/development/core/product/form.mjs.map +1 -1
- package/dist/development/core/product/options.d.mts +8 -10
- package/dist/development/core/product/options.mjs +8 -10
- package/dist/development/core/product/options.mjs.map +1 -1
- package/dist/development/core/product/product-form.d.mts +77 -29
- package/dist/development/core/product/product-form.mjs +32 -10
- package/dist/development/core/product/product-form.mjs.map +1 -1
- package/dist/development/core/product/state.d.mts +55 -32
- package/dist/development/core/product/url.d.mts +19 -13
- package/dist/development/core/product/url.mjs +10 -10
- package/dist/development/core/product/url.mjs.map +1 -1
- package/dist/development/core/request-context.d.mts +77 -19
- package/dist/development/core/request-context.mjs +5 -1
- package/dist/development/core/request-context.mjs.map +1 -1
- package/dist/development/core/request-routing/handle-shopify-redirects.d.mts +12 -4
- package/dist/development/core/request-routing/handle-shopify-redirects.mjs +9 -2
- package/dist/development/core/request-routing/handle-shopify-redirects.mjs.map +1 -1
- package/dist/development/core/request-routing/handle-shopify-routes.development.d.mts +9 -1
- package/dist/development/core/request-routing/handle-shopify-routes.development.mjs +7 -0
- package/dist/development/core/request-routing/handle-shopify-routes.development.mjs.map +1 -1
- package/dist/development/core/request-routing/handle-shopify-routes.mjs +7 -5
- package/dist/development/core/request-routing/handle-shopify-routes.mjs.map +1 -1
- package/dist/development/core/request-routing/interceptors/proxy.mjs +79 -43
- package/dist/development/core/request-routing/interceptors/proxy.mjs.map +1 -1
- package/dist/development/core/request-routing/interceptors/ucp-profile-proxy.mjs +69 -0
- package/dist/development/core/request-routing/interceptors/ucp-profile-proxy.mjs.map +1 -0
- package/dist/development/core/request-routing/interceptors/url-redirects.mjs +6 -1
- package/dist/development/core/request-routing/interceptors/url-redirects.mjs.map +1 -1
- package/dist/development/core/request-routing/interceptors/well-known.mjs +1 -1
- package/dist/development/core/request-routing/interceptors/well-known.mjs.map +1 -1
- package/dist/development/core/request-routing/registered-routes.d.mts +12 -0
- package/dist/development/core/request-routing/registered-routes.mjs +12 -0
- package/dist/development/core/request-routing/registered-routes.mjs.map +1 -1
- package/dist/development/core/request-routing/route-types.d.mts +43 -18
- package/dist/development/core/shop-pay/shop-pay.d.mts +71 -35
- package/dist/development/core/shop-pay/shop-pay.mjs +36 -10
- package/dist/development/core/shop-pay/shop-pay.mjs.map +1 -1
- package/dist/development/core/shopify-scripts/index.d.mts +15 -5
- package/dist/development/core/shopify-scripts/index.mjs +15 -5
- package/dist/development/core/shopify-scripts/index.mjs.map +1 -1
- package/dist/development/core/shopify-scripts/initialize.d.mts +12 -4
- package/dist/development/core/shopify-scripts/initialize.mjs +12 -4
- package/dist/development/core/shopify-scripts/initialize.mjs.map +1 -1
- package/dist/development/core/shopify-scripts/types.d.mts +58 -25
- package/dist/development/core/standard-routes/build.d.mts +11 -12
- package/dist/development/core/standard-routes/build.mjs +11 -12
- package/dist/development/core/standard-routes/build.mjs.map +1 -1
- package/dist/development/core/standard-routes/types.d.mts +30 -24
- package/dist/development/core/url.mjs +2 -1
- package/dist/development/core/url.mjs.map +1 -1
- package/dist/development/customer-account/session.d.mts +45 -54
- package/dist/development/customer-account/session.mjs +20 -4
- package/dist/development/customer-account/session.mjs.map +1 -1
- package/dist/development/globals.d.mts +20 -11
- package/dist/development/graphql/generated/storefront-api-types.d.mts +127 -1
- package/dist/development/graphql/graphql.d.mts +17 -16
- package/dist/development/graphql/graphql.mjs +7 -8
- package/dist/development/graphql/graphql.mjs.map +1 -1
- package/dist/development/graphql/type-resolver.d.mts +15 -8
- package/dist/development/react/cart.d.mts +78 -32
- package/dist/development/react/cart.mjs +56 -29
- package/dist/development/react/cart.mjs.map +1 -1
- package/dist/development/react/collection.d.mts +40 -22
- package/dist/development/react/collection.mjs +27 -7
- package/dist/development/react/collection.mjs.map +1 -1
- package/dist/development/react/predictive-search.d.mts +65 -50
- package/dist/development/react/predictive-search.mjs +23 -21
- package/dist/development/react/predictive-search.mjs.map +1 -1
- package/dist/development/react/product.d.mts +45 -16
- package/dist/development/react/product.mjs +13 -11
- package/dist/development/react/product.mjs.map +1 -1
- package/dist/development/react/shop-pay.d.mts +15 -0
- package/dist/development/react/shop-pay.mjs +9 -0
- package/dist/development/react/shop-pay.mjs.map +1 -1
- package/dist/development/react/shopify-scripts.d.mts +23 -2
- package/dist/development/react/shopify-scripts.mjs +9 -0
- package/dist/development/react/shopify-scripts.mjs.map +1 -1
- package/dist/development/vue/cart.d.mts +28 -23
- package/dist/development/vue/cart.mjs +42 -25
- package/dist/development/vue/cart.mjs.map +1 -1
- package/dist/development/vue/collection.d.mts +20 -12
- package/dist/development/vue/collection.mjs +15 -7
- package/dist/development/vue/collection.mjs.map +1 -1
- package/dist/development/vue/index.d.mts +2 -2
- package/dist/development/vue/index.mjs +2 -2
- package/dist/development/vue/predictive-search.d.mts +31 -36
- package/dist/development/vue/predictive-search.mjs +16 -22
- package/dist/development/vue/predictive-search.mjs.map +1 -1
- package/dist/development/vue/product.d.mts +16 -15
- package/dist/development/vue/product.mjs +11 -11
- package/dist/development/vue/product.mjs.map +1 -1
- package/dist/development/vue/shop-pay.d.mts +14 -17
- package/dist/development/vue/shop-pay.mjs +8 -0
- package/dist/development/vue/shop-pay.mjs.map +1 -1
- package/dist/development/vue/shopify-scripts.d.mts +11 -0
- package/dist/development/vue/shopify-scripts.mjs +6 -0
- package/dist/development/vue/shopify-scripts.mjs.map +1 -1
- package/dist/globals.d.mts +20 -11
- package/dist/graphql/generated/storefront-api-types.d.mts +127 -1
- package/dist/graphql/graphql.d.mts +17 -16
- package/dist/graphql/graphql.mjs +7 -8
- package/dist/graphql/graphql.mjs.map +1 -1
- package/dist/graphql/type-resolver.d.mts +15 -8
- package/dist/react/cart.d.mts +78 -32
- package/dist/react/cart.mjs +56 -29
- package/dist/react/cart.mjs.map +1 -1
- package/dist/react/collection.d.mts +40 -22
- package/dist/react/collection.mjs +27 -7
- package/dist/react/collection.mjs.map +1 -1
- package/dist/react/predictive-search.d.mts +65 -50
- package/dist/react/predictive-search.mjs +23 -21
- package/dist/react/predictive-search.mjs.map +1 -1
- package/dist/react/product.d.mts +45 -16
- package/dist/react/product.mjs +13 -11
- package/dist/react/product.mjs.map +1 -1
- package/dist/react/shop-pay.d.mts +15 -0
- package/dist/react/shop-pay.mjs +9 -0
- package/dist/react/shop-pay.mjs.map +1 -1
- package/dist/react/shopify-scripts.d.mts +23 -2
- package/dist/react/shopify-scripts.mjs +9 -0
- package/dist/react/shopify-scripts.mjs.map +1 -1
- package/dist/ts-plugin/index.cjs +6 -0
- package/dist/ts-plugin/index.cjs.map +1 -1
- package/dist/ts-plugin/index.d.cts +6 -0
- package/dist/vite/local-https.d.mts +51 -17
- package/dist/vite/local-https.mjs +26 -5
- package/dist/vite/local-https.mjs.map +1 -1
- package/dist/vue/cart.d.mts +28 -23
- package/dist/vue/cart.mjs +42 -25
- package/dist/vue/cart.mjs.map +1 -1
- package/dist/vue/collection.d.mts +20 -12
- package/dist/vue/collection.mjs +15 -7
- package/dist/vue/collection.mjs.map +1 -1
- package/dist/vue/index.d.mts +2 -2
- package/dist/vue/index.mjs +2 -2
- package/dist/vue/predictive-search.d.mts +31 -36
- package/dist/vue/predictive-search.mjs +16 -22
- package/dist/vue/predictive-search.mjs.map +1 -1
- package/dist/vue/product.d.mts +16 -15
- package/dist/vue/product.mjs +11 -11
- package/dist/vue/product.mjs.map +1 -1
- package/dist/vue/shop-pay.d.mts +14 -17
- package/dist/vue/shop-pay.mjs +8 -0
- package/dist/vue/shop-pay.mjs.map +1 -1
- package/dist/vue/shopify-scripts.d.mts +11 -0
- package/dist/vue/shopify-scripts.mjs +6 -0
- package/dist/vue/shopify-scripts.mjs.map +1 -1
- package/package.json +3 -1
- package/skills/hydrogen-analytics/SKILL.md +1 -1
- package/skills/hydrogen-cart-ui/SKILL.md +28 -2
- package/skills/hydrogen-collection-browser/SKILL.md +9 -1
- package/skills/hydrogen-collection-browser/references/nextjs.md +1 -1
- package/skills/hydrogen-collection-browser/references/nuxt.md +1 -1
- package/skills/hydrogen-collection-browser/references/react.md +151 -32
- package/skills/hydrogen-customer-account/SKILL.md +2 -0
- package/skills/hydrogen-request-handlers/SKILL.md +21 -11
- package/skills/hydrogen-setup/references/analytics.md +14 -8
- package/skills/hydrogen-setup/steps/1-inspect-app.md +2 -2
- package/skills/hydrogen-setup/steps/6-product-detail-page.md +21 -11
- package/skills/hydrogen-storefront-client/SKILL.md +1 -1
- package/skills/hydrogen-storefront-client/references/caching.md +2 -0
- package/skills/hydrogen-variant-form/SKILL.md +25 -7
- package/skills/hydrogen-variant-form/references/nextjs.md +39 -4
- package/skills/hydrogen-variant-form/references/nuxt.md +83 -42
- package/skills/hydrogen-variant-form/references/react.md +45 -9
|
@@ -14,11 +14,12 @@ function getCartEndpoint() {
|
|
|
14
14
|
return cartEndpoint;
|
|
15
15
|
}
|
|
16
16
|
/**
|
|
17
|
-
*
|
|
18
|
-
* cart
|
|
17
|
+
* Returns a cart provider and cart hooks typed to the cart fragment in your cart server handlers.
|
|
18
|
+
* Pass `typeof cartServerHandlers` as the `THandlers` type argument. Every hook's cart state then includes your custom cart fields.
|
|
19
19
|
*
|
|
20
|
-
*
|
|
21
|
-
*
|
|
20
|
+
* On mount, the provider sends cart requests to `/api/cart` and connects the store. Every hook except `useOptionalCart` throws when you call it outside the provider.
|
|
21
|
+
*
|
|
22
|
+
* @returns The cart provider, the cart state hooks, and the cart actions and form hooks.
|
|
22
23
|
*
|
|
23
24
|
* @example
|
|
24
25
|
* ```tsx
|
|
@@ -33,6 +34,7 @@ function getCartEndpoint() {
|
|
|
33
34
|
* useCartForm,
|
|
34
35
|
* } = createCartComponents<typeof cartServerHandlers>();
|
|
35
36
|
* ```
|
|
37
|
+
* @publicDocs
|
|
36
38
|
*/
|
|
37
39
|
function createCartComponents() {
|
|
38
40
|
function TypedCartProvider({ initialData, children }) {
|
|
@@ -70,11 +72,9 @@ function useOptionalCartStore() {
|
|
|
70
72
|
return useContext(CartContext);
|
|
71
73
|
}
|
|
72
74
|
/**
|
|
73
|
-
*
|
|
75
|
+
* Creates a cart store and shares the store with the cart hooks inside the provider.
|
|
74
76
|
*
|
|
75
|
-
*
|
|
76
|
-
* All cart hooks (`useCart`, `useCartForm`, `useCartActions`) must be rendered
|
|
77
|
-
* inside this provider.
|
|
77
|
+
* Render every cart hook inside the provider. On mount, the provider sends cart requests to `/api/cart` and connects the store. On unmount, the provider destroys the store. The provider creates its store from the first initial data and ignores later changes.
|
|
78
78
|
*
|
|
79
79
|
* @example
|
|
80
80
|
* ```tsx
|
|
@@ -83,6 +83,9 @@ function useOptionalCartStore() {
|
|
|
83
83
|
* <App />
|
|
84
84
|
* </CartProvider>
|
|
85
85
|
* ```
|
|
86
|
+
* @param props - The initial cart data and the content that uses the cart hooks.
|
|
87
|
+
* @returns A context provider that shares the cart store with its children.
|
|
88
|
+
* @publicDocs
|
|
86
89
|
*/
|
|
87
90
|
function CartProvider({ initialData, children }) {
|
|
88
91
|
const store = useMemo(() => createCartStore({ initialData }), []);
|
|
@@ -99,11 +102,13 @@ function CartProvider({ initialData, children }) {
|
|
|
99
102
|
});
|
|
100
103
|
}
|
|
101
104
|
/**
|
|
102
|
-
*
|
|
105
|
+
* Returns the slice of cart state that the selector picks.
|
|
106
|
+
*
|
|
107
|
+
* The component re-renders when the selected value changes by reference, or when your `isEqual` function reports a change. A selector that returns the full state re-renders on every cart update.
|
|
103
108
|
*
|
|
104
|
-
*
|
|
105
|
-
*
|
|
106
|
-
*
|
|
109
|
+
* @param selector The function that picks a value from the cart state.
|
|
110
|
+
* @param isEqual The function that compares the previous and next selected values. Return `true` to skip the re-render.
|
|
111
|
+
* @returns The selected value.
|
|
107
112
|
*
|
|
108
113
|
* @example
|
|
109
114
|
* ```tsx
|
|
@@ -119,16 +124,17 @@ function CartProvider({ initialData, children }) {
|
|
|
119
124
|
* (a, b) => a.amount === b.amount,
|
|
120
125
|
* );
|
|
121
126
|
* ```
|
|
127
|
+
* @publicDocs
|
|
122
128
|
*/
|
|
123
129
|
function useCart(selector, isEqual) {
|
|
124
130
|
return useCartSelector(useCartStore(), selector, isEqual);
|
|
125
131
|
}
|
|
126
132
|
/**
|
|
127
|
-
* Returns cart
|
|
133
|
+
* Returns the cart action that reloads the cart after a cart change outside cart forms.
|
|
128
134
|
*
|
|
129
|
-
*
|
|
130
|
-
*
|
|
131
|
-
*
|
|
135
|
+
* Call `refresh()` after a server-side cart change that skips cart forms, such as a server action that creates the cart.
|
|
136
|
+
*
|
|
137
|
+
* @returns The `refresh()` action.
|
|
132
138
|
*
|
|
133
139
|
* @example
|
|
134
140
|
* ```tsx
|
|
@@ -139,17 +145,20 @@ function useCart(selector, isEqual) {
|
|
|
139
145
|
* refresh(); // re-fetch the cart
|
|
140
146
|
* }
|
|
141
147
|
* ```
|
|
148
|
+
* @publicDocs
|
|
142
149
|
*/
|
|
143
150
|
function useCartActions() {
|
|
144
151
|
const store = useCartStore("useCartActions");
|
|
145
152
|
return useMemo(() => ({ refresh: store.refresh }), [store]);
|
|
146
153
|
}
|
|
147
154
|
/**
|
|
148
|
-
*
|
|
155
|
+
* Publishes cart analytics events when the customer's cart changes.
|
|
156
|
+
*
|
|
157
|
+
* Call the hook once near the root of your app, inside `CartProvider`. The hook starts tracking on mount and stops on unmount. The hook publishes `cart_updated`, `product_added_to_cart`, and `product_removed_from_cart` events.
|
|
149
158
|
*
|
|
150
|
-
*
|
|
151
|
-
*
|
|
152
|
-
*
|
|
159
|
+
* The hook throws outside `CartProvider`, and when the Shopify analytics bus isn't available. Render `ShopifyScripts` before the hook runs.
|
|
160
|
+
*
|
|
161
|
+
* @returns Nothing. The hook tracks cart analytics for the lifetime of the calling component.
|
|
153
162
|
*
|
|
154
163
|
* @example
|
|
155
164
|
* ```tsx
|
|
@@ -158,14 +167,31 @@ function useCartActions() {
|
|
|
158
167
|
* return <Layout />;
|
|
159
168
|
* }
|
|
160
169
|
* ```
|
|
170
|
+
* @publicDocs
|
|
161
171
|
*/
|
|
162
172
|
function useCartAnalytics() {
|
|
163
173
|
const store = useCartStore("useCartAnalytics");
|
|
164
174
|
useEffect(() => trackCartAnalytics(store), [store]);
|
|
165
175
|
}
|
|
166
176
|
/**
|
|
167
|
-
*
|
|
168
|
-
*
|
|
177
|
+
* Returns a selected slice of cart state, or `undefined` when the component renders outside the cart provider.
|
|
178
|
+
*
|
|
179
|
+
* Use the hook in a component that also renders above the provider, such as a header cart badge on an error page.
|
|
180
|
+
* Wherever the provider always exists, use `useCart`, which throws when the provider is missing.
|
|
181
|
+
*
|
|
182
|
+
* @param selector The function that picks a value from the cart state.
|
|
183
|
+
* @param isEqual The function that compares the previous and next selected values. Return `true` to skip the re-render.
|
|
184
|
+
* @returns The selected value, or `undefined` outside the provider.
|
|
185
|
+
*
|
|
186
|
+
* @example
|
|
187
|
+
* ```tsx
|
|
188
|
+
* function CartBadge() {
|
|
189
|
+
* const count = useOptionalCart((state) => state.data.totalQuantity);
|
|
190
|
+
* if (count === undefined) return null;
|
|
191
|
+
* return <span>Cart ({count})</span>;
|
|
192
|
+
* }
|
|
193
|
+
* ```
|
|
194
|
+
* @publicDocs
|
|
169
195
|
*/
|
|
170
196
|
function useOptionalCart(selector, isEqual) {
|
|
171
197
|
const store = useOptionalCartStore();
|
|
@@ -198,14 +224,14 @@ function useCartSelector(store, selector, isEqual) {
|
|
|
198
224
|
/**
|
|
199
225
|
* Returns form props and a field register function for building cart forms.
|
|
200
226
|
*
|
|
201
|
-
* `formProps()`
|
|
202
|
-
*
|
|
203
|
-
* `
|
|
204
|
-
*
|
|
227
|
+
* Spread `formProps()` on the form. The form posts to the cart endpoint. On submit, the form props stop the browser's navigation and send the form through the cart store.
|
|
228
|
+
* The submit handler ignores a failed cart change. Read failures from the cart state's `errors`.
|
|
229
|
+
* To skip the cart submission, call `event.preventDefault()` in a `beforeSubmit` callback. The `afterSubmit` callback runs as soon as the cart change starts, before the server responds.
|
|
230
|
+
* The register function returns the HTML attributes for each cart field and action.
|
|
231
|
+
*
|
|
232
|
+
* Register the quantity field with `interactive: true` to submit the form when the quantity changes. The interactive input takes the quantity as a default value and stays uncontrolled.
|
|
205
233
|
*
|
|
206
|
-
*
|
|
207
|
-
* attributes include a `ref` callback that attaches {@link attachQuantityInput}
|
|
208
|
-
* for auto-submit-on-change behavior.
|
|
234
|
+
* @returns The form props function and the register function.
|
|
209
235
|
*
|
|
210
236
|
* @example
|
|
211
237
|
* ```tsx
|
|
@@ -224,6 +250,7 @@ function useCartSelector(store, selector, isEqual) {
|
|
|
224
250
|
* );
|
|
225
251
|
* }
|
|
226
252
|
* ```
|
|
253
|
+
* @publicDocs
|
|
227
254
|
*/
|
|
228
255
|
function useCartForm() {
|
|
229
256
|
const store = useCartStore("useCartForm");
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"cart.mjs","names":[],"sources":["../../../src/react/cart.tsx"],"sourcesContent":["\"use client\";\n\nimport {\n createContext,\n useContext,\n useEffect,\n useMemo,\n useRef,\n useSyncExternalStore,\n type FormHTMLAttributes,\n type ReactNode,\n type RefCallback,\n type SubmitEvent,\n} from \"react\";\n\nimport { trackCartAnalytics } from \"../core/analytics/cart-tracker\";\nimport { attachQuantityInput } from \"../core/cart/attach-quantity-input\";\nimport {\n configureCartEndpoint as configureCoreCartEndpoint,\n createCartStore,\n type CreateCartStoreOptions,\n type CartActions,\n type CartStore,\n} from \"../core/cart/cart\";\nimport { createCartFormRegister } from \"../core/cart/form\";\nimport type { CartDataFromHandlers } from \"../core/cart/server-handlers\";\nimport { type CartData, type CartState } from \"../core/cart/state\";\n\nconst DEFAULT_CART_ENDPOINT = \"/api/cart\";\n\nlet cartEndpoint = DEFAULT_CART_ENDPOINT;\n\nconst CartContext = createContext<CartStore | null>(null);\n\nexport function configureCartEndpoint(endpoint: string): void {\n cartEndpoint = endpoint;\n configureCoreCartEndpoint(endpoint);\n}\n\nexport function getCartEndpoint(): string {\n return cartEndpoint;\n}\n\ntype TypedCartProviderProps<TData extends CartData> = {\n initialData?: CartInitialData<TData>;\n children?: ReactNode;\n};\n\ntype CartInitialData<TData extends CartData = CartData> =\n CreateCartStoreOptions<TData>[\"initialData\"];\n\nexport type { CartActions };\n\ntype TypedCartComponents<TData extends CartData> = {\n CartProvider: (props: TypedCartProviderProps<TData>) => ReactNode;\n useCart: <S>(selector: (state: CartState<TData>) => S, isEqual?: (a: S, b: S) => boolean) => S;\n useSuspenseCart: <S>(\n selector: (state: CartState<TData>) => S,\n isEqual?: (a: S, b: S) => boolean,\n ) => S;\n useOptionalCart: <S>(\n selector: (state: CartState<TData>) => S,\n isEqual?: (a: S, b: S) => boolean,\n ) => S | undefined;\n useCartActions: typeof useCartActions;\n useCartForm: typeof useCartForm;\n};\n\n/**\n * Factory that returns typed cart components and hooks matched to your server handler's\n * cart query shape.\n *\n * The generic `THandlers` parameter is inferred from your `createCartServerHandlers`\n * call, so every hook's {@link CartState} carries your custom cart fields end-to-end.\n *\n * @example\n * ```tsx\n * import type { cartServerHandlers } from \"./server/cart.server\";\n *\n * const {\n * CartProvider,\n * useCart,\n * useSuspenseCart,\n * useOptionalCart,\n * useCartActions,\n * useCartForm,\n * } = createCartComponents<typeof cartServerHandlers>();\n * ```\n */\nexport function createCartComponents<THandlers>(): TypedCartComponents<\n CartDataFromHandlers<THandlers>\n> {\n type TData = CartDataFromHandlers<THandlers>;\n\n function TypedCartProvider({ initialData, children }: TypedCartProviderProps<TData>) {\n return (\n <CartProvider initialData={initialData as CartInitialData | undefined}>\n {children}\n </CartProvider>\n );\n }\n\n function useTypedCart<S>(\n selector: (state: CartState<TData>) => S,\n isEqual?: (a: S, b: S) => boolean,\n ): S {\n return useCart<CartData, S>((state) => selector(state as CartState<TData>), isEqual);\n }\n\n function useTypedOptionalCart<S>(\n selector: (state: CartState<TData>) => S,\n isEqual?: (a: S, b: S) => boolean,\n ): S | undefined {\n return useOptionalCart<CartData, S>((state) => selector(state as CartState<TData>), isEqual);\n }\n\n function useSuspenseCart<S>(\n selector: (state: CartState<TData>) => S,\n isEqual?: (a: S, b: S) => boolean,\n ): S {\n const readyPromise = useCart((state) => state.readyPromise);\n if (readyPromise) throw readyPromise;\n return useCart(selector, isEqual);\n }\n\n return {\n CartProvider: TypedCartProvider,\n useCart: useTypedCart,\n useSuspenseCart,\n useOptionalCart: useTypedOptionalCart,\n useCartActions,\n useCartForm,\n } as const;\n}\n\nexport function useCartStore(hookName = \"useCart\"): CartStore {\n const store = useContext(CartContext);\n if (!store) throw new Error(`${hookName} must be used inside <CartProvider>.`);\n return store;\n}\n\nfunction useOptionalCartStore(): CartStore | null {\n return useContext(CartContext);\n}\n\n/**\n * React context provider that creates and manages a {@link CartStore} instance.\n *\n * Calls {@link CartStore.connect} on mount and {@link CartStore.destroy} on unmount.\n * All cart hooks (`useCart`, `useCartForm`, `useCartActions`) must be rendered\n * inside this provider.\n *\n * @example\n * ```tsx\n * // In your root layout\n * <CartProvider initialData={{ cart: loaderData.cart }}>\n * <App />\n * </CartProvider>\n * ```\n */\nexport function CartProvider({\n initialData,\n children,\n}: {\n initialData?: CartInitialData;\n children?: ReactNode;\n}) {\n // oxlint-disable-next-line react-hooks/exhaustive-deps -- store is created once with the initial server data\n const store = useMemo(() => createCartStore({ initialData }), []);\n\n useEffect(() => {\n configureCoreCartEndpoint(cartEndpoint);\n store.connect();\n return () => {\n store.destroy();\n };\n }, [store]);\n\n return <CartContext.Provider value={store}>{children}</CartContext.Provider>;\n}\n\n/**\n * Subscribes to a slice of {@link CartState} via `useSyncExternalStore`.\n *\n * The component re-renders only when the selected value changes (by reference,\n * or by a custom `isEqual`). Always pass a selector — subscribing to the full\n * state object defeats memoization.\n *\n * @example\n * ```tsx\n * // Select cart lines\n * const lines = useCart((state) => state.data.lines.nodes);\n *\n * // Select pending state for a specific line\n * const isPending = useCart((state) => state.pending.lines.has(lineId));\n *\n * // Custom equality to avoid re-renders on unchanged totals\n * const total = useCart(\n * (state) => state.data.cost.totalAmount,\n * (a, b) => a.amount === b.amount,\n * );\n * ```\n */\nexport function useCart<TData extends CartData = CartData, S = unknown>(\n selector: (state: CartState<TData>) => S,\n isEqual?: (a: S, b: S) => boolean,\n): S {\n const store = useCartStore();\n return useCartSelector(store, selector, isEqual) as S;\n}\n\n/**\n * Returns cart actions for reconciling state after out-of-band mutations.\n *\n * Currently exposes {@link CartStore.refresh} — call it after server-side cart\n * mutations that bypass the form system (e.g. a server action that creates the\n * cart before the client knows about it).\n *\n * @example\n * ```tsx\n * const { refresh } = useCartActions();\n *\n * async function handleServerAction() {\n * await createCartOnServer();\n * refresh(); // re-fetch the cart\n * }\n * ```\n */\nexport function useCartActions(): CartActions {\n const store = useCartStore(\"useCartActions\");\n\n return useMemo(() => ({ refresh: store.refresh }), [store]);\n}\n\n/**\n * Subscribes the {@link CartStore} to the analytics event dispatcher.\n *\n * Call once near the root of your app — it subscribes on mount and\n * cleans up on unmount. Publishes `CART_UPDATED`, `PRODUCT_ADD_TO_CART`,\n * and `PRODUCT_REMOVED_FROM_CART` analytics events automatically.\n *\n * @example\n * ```tsx\n * function App() {\n * useCartAnalytics();\n * return <Layout />;\n * }\n * ```\n */\nexport function useCartAnalytics(): void {\n const store = useCartStore(\"useCartAnalytics\");\n\n useEffect(() => trackCartAnalytics(store), [store]);\n}\n\n/**\n * Like `useCart`, but returns `undefined` when rendered outside a `<CartProvider>`.\n * @internal\n */\nexport function useOptionalCart<TData extends CartData = CartData, S = unknown>(\n selector: (state: CartState<TData>) => S,\n isEqual?: (a: S, b: S) => boolean,\n): S | undefined {\n const store = useOptionalCartStore();\n if (!store) return undefined;\n return useCartSelector(store, selector, isEqual);\n}\n\nfunction useCartSelector<TData extends CartData = CartData, S = unknown>(\n store: CartStore,\n selector: (state: CartState<TData>) => S,\n isEqual?: (a: S, b: S) => boolean,\n): S {\n const cachedRef = useRef<{ state: unknown; selector: typeof selector; value: S } | null>(null);\n\n const getSnapshot = () => {\n const state = store.getState() as CartState<TData>;\n\n if (\n cachedRef.current &&\n cachedRef.current.state === state &&\n cachedRef.current.selector === selector\n ) {\n return cachedRef.current.value;\n }\n\n const next = selector(state);\n\n if (cachedRef.current && isEqual?.(cachedRef.current.value, next)) {\n cachedRef.current = { state, selector, value: cachedRef.current.value };\n return cachedRef.current.value;\n }\n\n cachedRef.current = { state, selector, value: next };\n return next;\n };\n\n return useSyncExternalStore(store.subscribe, getSnapshot, getSnapshot);\n}\n\n/**\n * Returns form props and a field register function for building cart forms.\n *\n * `formProps()` returns `<form>` attributes that intercept submission, call\n * {@link CartStore.handleFormSubmit}, and prevent the default browser navigation.\n * `register()` returns the correct HTML attributes for each field type — see\n * `CartFormRegister` for the full field/action list.\n *\n * When `register(\"quantity\", { interactive: true })` is called, the returned\n * attributes include a `ref` callback that attaches {@link attachQuantityInput}\n * for auto-submit-on-change behavior.\n *\n * @example\n * ```tsx\n * function LineItem({ line }) {\n * const { formProps, register } = useCartForm();\n *\n * return (\n * <form {...formProps()}>\n * <input {...register(\"lineId\", { value: line.id })} />\n * <input {...register(\"quantity\", { value: line.quantity, interactive: true })} />\n * <button {...register(\"set\")} />\n * <button {...register(\"increase\")}>+</button>\n * <button {...register(\"decrease\")}>−</button>\n * <button {...register(\"remove\")}>Remove</button>\n * </form>\n * );\n * }\n * ```\n */\nexport function useCartForm() {\n const store = useCartStore(\"useCartForm\");\n const coreRegister = useMemo(() => createCartFormRegister(), []);\n\n const register = useMemo(() => {\n type Register = typeof coreRegister;\n const wrapped = ((...args: Parameters<Register>) => {\n const result = (coreRegister as Function)(...args);\n const [field, opts] = args as [string, { interactive?: boolean }?];\n\n if (field === \"quantity\" && opts?.interactive) {\n let cleanup: (() => void) | null = null;\n const ref: RefCallback<HTMLInputElement> = (el) => {\n cleanup?.();\n cleanup = null;\n if (el) {\n const form = el.closest(\"form\");\n if (form) cleanup = attachQuantityInput(el, form);\n }\n };\n const { value: _, ...rest } = result;\n return { ...rest, defaultValue: result.value, ref };\n }\n\n return result;\n }) as Register;\n return wrapped;\n }, [coreRegister]);\n\n const formProps = (opts?: {\n beforeSubmit?: (e: SubmitEvent<HTMLFormElement>) => void;\n afterSubmit?: (e: SubmitEvent<HTMLFormElement>) => void;\n }): FormHTMLAttributes<HTMLFormElement> => ({\n onSubmit: (e: SubmitEvent<HTMLFormElement>) => {\n opts?.beforeSubmit?.(e);\n if (e.defaultPrevented) return;\n e.preventDefault();\n store.handleFormSubmit(e.nativeEvent).catch(() => {});\n opts?.afterSubmit?.(e);\n },\n method: \"post\",\n action: cartEndpoint,\n });\n\n return { formProps, register };\n}\n"],"mappings":"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"}
|
|
1
|
+
{"version":3,"file":"cart.mjs","names":[],"sources":["../../../src/react/cart.tsx"],"sourcesContent":["\"use client\";\n\nimport {\n createContext,\n useContext,\n useEffect,\n useMemo,\n useRef,\n useSyncExternalStore,\n type FormHTMLAttributes,\n type ReactNode,\n type RefCallback,\n type SubmitEvent,\n} from \"react\";\n\nimport { trackCartAnalytics } from \"../core/analytics/cart-tracker\";\nimport { attachQuantityInput } from \"../core/cart/attach-quantity-input\";\nimport {\n configureCartEndpoint as configureCoreCartEndpoint,\n createCartStore,\n type CreateCartStoreOptions,\n type CartActions,\n type CartStore,\n} from \"../core/cart/cart\";\nimport { createCartFormRegister, type CartFormRegister } from \"../core/cart/form\";\nimport type { CartDataFromHandlers } from \"../core/cart/server-handlers\";\nimport { type CartData, type CartState } from \"../core/cart/state\";\n\nconst DEFAULT_CART_ENDPOINT = \"/api/cart\";\n\nlet cartEndpoint = DEFAULT_CART_ENDPOINT;\n\nconst CartContext = createContext<CartStore | null>(null);\n\nexport function configureCartEndpoint(endpoint: string): void {\n cartEndpoint = endpoint;\n configureCoreCartEndpoint(endpoint);\n}\n\nexport function getCartEndpoint(): string {\n return cartEndpoint;\n}\n\n/**\n * Props for the cart provider that `createCartComponents` returns.\n */\ntype TypedCartProviderProps<TData extends CartData> = {\n /**\n * The `data` property of the cart GET handler's result, or a promise of that data. Without initial data, the store loads the cart from `/api/cart` after the provider mounts, and the server-rendered page shows an empty cart. The provider creates its store from the first value and ignores later changes.\n */\n initialData?: CartInitialData<TData>;\n /** Content that can read the cart through the cart hooks. */\n children?: ReactNode;\n};\n\ntype CartInitialData<TData extends CartData = CartData> =\n CreateCartStoreOptions<TData>[\"initialData\"];\n\nexport type { CartActions };\n\n/** A hook that returns the slice of cart state that your selector picks. */\ntype CartStateHook<TData extends CartData> =\n /**\n * @param selector - Picks the value to return from the cart state.\n * @param isEqual - Compares the previous and next selected values. Return `true` to skip the re-render. Without the function, the hook compares the values by reference.\n * @returns The selected value.\n */\n <S>(selector: (state: CartState<TData>) => S, isEqual?: (a: S, b: S) => boolean) => S;\n\n/** A hook that returns the slice of cart state that your selector picks, or `undefined` outside the cart provider. */\ntype OptionalCartStateHook<TData extends CartData> =\n /**\n * @param selector - Picks the value to return from the cart state.\n * @param isEqual - Compares the previous and next selected values. Return `true` to skip the re-render. Without the function, the hook compares the values by reference.\n * @returns The selected value, or `undefined` outside the cart provider.\n */\n <S>(selector: (state: CartState<TData>) => S, isEqual?: (a: S, b: S) => boolean) => S | undefined;\n\n/** Callbacks that run around a cart form submission. */\ninterface CartFormPropsOptions {\n /** Runs before the cart store handles the submission. Call `event.preventDefault()` to skip the cart submission. */\n beforeSubmit?: (e: SubmitEvent<HTMLFormElement>) => void;\n /** Runs as soon as the cart change starts, before the server responds. */\n afterSubmit?: (e: SubmitEvent<HTMLFormElement>) => void;\n}\n\n/** The form props function and the register function for a cart form. */\ninterface CartFormBindings {\n /** Returns the `method`, `action`, and `onSubmit` props to spread on the form. The form posts to the cart endpoint, and the cart store handles each submission. */\n formProps: (opts?: CartFormPropsOptions) => FormHTMLAttributes<HTMLFormElement>;\n /** Returns the attributes for a cart field or action button. Register the quantity field with `interactive: true` to submit the form when the quantity changes. The interactive input takes the quantity as a default value and stays uncontrolled. */\n register: CartFormRegister;\n}\n\n/**\n * The cart provider and hooks that `createCartComponents` returns, typed with the custom cart fields from your cart server handlers.\n */\ntype TypedCartComponents<TData extends CartData> = {\n /** Creates the cart store and shares the store with the cart hooks inside the provider. The provider connects the store on mount and destroys the store on unmount. */\n CartProvider: (props: TypedCartProviderProps<TData>) => ReactNode;\n /**\n * Returns the slice of cart state that the selector picks. The component re-renders when the selected value changes by reference, or when your `isEqual` function reports a change. A selector that returns the full state re-renders on every cart update.\n */\n useCart: CartStateHook<TData>;\n /** Suspends while the full cart loads, then returns the slice of cart state that the selector picks. Wrap the component in a `Suspense` boundary. */\n useSuspenseCart: CartStateHook<TData>;\n /**\n * Returns the slice of cart state that the selector picks, or `undefined` outside the provider. Use the hook in a component that also renders above the provider, such as a cart badge on an error page.\n */\n useOptionalCart: OptionalCartStateHook<TData>;\n /** Returns the `refresh()` action. Call `refresh()` after a server-side cart change that skips cart forms. */\n useCartActions: typeof useCartActions;\n /** Returns form props and a register function for cart forms. */\n useCartForm: typeof useCartForm;\n};\n\n/**\n * Returns a cart provider and cart hooks typed to the cart fragment in your cart server handlers.\n * Pass `typeof cartServerHandlers` as the `THandlers` type argument. Every hook's cart state then includes your custom cart fields.\n *\n * On mount, the provider sends cart requests to `/api/cart` and connects the store. Every hook except `useOptionalCart` throws when you call it outside the provider.\n *\n * @returns The cart provider, the cart state hooks, and the cart actions and form hooks.\n *\n * @example\n * ```tsx\n * import type { cartServerHandlers } from \"./server/cart.server\";\n *\n * const {\n * CartProvider,\n * useCart,\n * useSuspenseCart,\n * useOptionalCart,\n * useCartActions,\n * useCartForm,\n * } = createCartComponents<typeof cartServerHandlers>();\n * ```\n * @publicDocs\n */\nexport function createCartComponents<THandlers>(): TypedCartComponents<\n CartDataFromHandlers<THandlers>\n> {\n type TData = CartDataFromHandlers<THandlers>;\n\n function TypedCartProvider({ initialData, children }: TypedCartProviderProps<TData>) {\n return (\n <CartProvider initialData={initialData as CartInitialData | undefined}>\n {children}\n </CartProvider>\n );\n }\n\n function useTypedCart<S>(\n selector: (state: CartState<TData>) => S,\n isEqual?: (a: S, b: S) => boolean,\n ): S {\n return useCart<CartData, S>((state) => selector(state as CartState<TData>), isEqual);\n }\n\n function useTypedOptionalCart<S>(\n selector: (state: CartState<TData>) => S,\n isEqual?: (a: S, b: S) => boolean,\n ): S | undefined {\n return useOptionalCart<CartData, S>((state) => selector(state as CartState<TData>), isEqual);\n }\n\n function useSuspenseCart<S>(\n selector: (state: CartState<TData>) => S,\n isEqual?: (a: S, b: S) => boolean,\n ): S {\n const readyPromise = useCart((state) => state.readyPromise);\n if (readyPromise) throw readyPromise;\n return useCart(selector, isEqual);\n }\n\n return {\n CartProvider: TypedCartProvider,\n useCart: useTypedCart,\n useSuspenseCart,\n useOptionalCart: useTypedOptionalCart,\n useCartActions,\n useCartForm,\n } as const;\n}\n\nexport function useCartStore(hookName = \"useCart\"): CartStore {\n const store = useContext(CartContext);\n if (!store) throw new Error(`${hookName} must be used inside <CartProvider>.`);\n return store;\n}\n\nfunction useOptionalCartStore(): CartStore | null {\n return useContext(CartContext);\n}\n\n/**\n * Creates a cart store and shares the store with the cart hooks inside the provider.\n *\n * Render every cart hook inside the provider. On mount, the provider sends cart requests to `/api/cart` and connects the store. On unmount, the provider destroys the store. The provider creates its store from the first initial data and ignores later changes.\n *\n * @example\n * ```tsx\n * // In your root layout\n * <CartProvider initialData={{ cart: loaderData.cart }}>\n * <App />\n * </CartProvider>\n * ```\n * @param props - The initial cart data and the content that uses the cart hooks.\n * @returns A context provider that shares the cart store with its children.\n * @publicDocs\n */\nexport function CartProvider({\n initialData,\n children,\n}: {\n initialData?: CartInitialData;\n children?: ReactNode;\n}) {\n // oxlint-disable-next-line react-hooks/exhaustive-deps -- store is created once with the initial server data\n const store = useMemo(() => createCartStore({ initialData }), []);\n\n useEffect(() => {\n configureCoreCartEndpoint(cartEndpoint);\n store.connect();\n return () => {\n store.destroy();\n };\n }, [store]);\n\n return <CartContext.Provider value={store}>{children}</CartContext.Provider>;\n}\n\n/**\n * Returns the slice of cart state that the selector picks.\n *\n * The component re-renders when the selected value changes by reference, or when your `isEqual` function reports a change. A selector that returns the full state re-renders on every cart update.\n *\n * @param selector The function that picks a value from the cart state.\n * @param isEqual The function that compares the previous and next selected values. Return `true` to skip the re-render.\n * @returns The selected value.\n *\n * @example\n * ```tsx\n * // Select cart lines\n * const lines = useCart((state) => state.data.lines.nodes);\n *\n * // Select pending state for a specific line\n * const isPending = useCart((state) => state.pending.lines.has(lineId));\n *\n * // Custom equality to avoid re-renders on unchanged totals\n * const total = useCart(\n * (state) => state.data.cost.totalAmount,\n * (a, b) => a.amount === b.amount,\n * );\n * ```\n * @publicDocs\n */\nexport function useCart<TData extends CartData = CartData, S = unknown>(\n selector: (state: CartState<TData>) => S,\n isEqual?: (a: S, b: S) => boolean,\n): S {\n const store = useCartStore();\n return useCartSelector(store, selector, isEqual) as S;\n}\n\n/**\n * Returns the cart action that reloads the cart after a cart change outside cart forms.\n *\n * Call `refresh()` after a server-side cart change that skips cart forms, such as a server action that creates the cart.\n *\n * @returns The `refresh()` action.\n *\n * @example\n * ```tsx\n * const { refresh } = useCartActions();\n *\n * async function handleServerAction() {\n * await createCartOnServer();\n * refresh(); // re-fetch the cart\n * }\n * ```\n * @publicDocs\n */\nexport function useCartActions(): CartActions {\n const store = useCartStore(\"useCartActions\");\n\n return useMemo(() => ({ refresh: store.refresh }), [store]);\n}\n\n/**\n * Publishes cart analytics events when the customer's cart changes.\n *\n * Call the hook once near the root of your app, inside `CartProvider`. The hook starts tracking on mount and stops on unmount. The hook publishes `cart_updated`, `product_added_to_cart`, and `product_removed_from_cart` events.\n *\n * The hook throws outside `CartProvider`, and when the Shopify analytics bus isn't available. Render `ShopifyScripts` before the hook runs.\n *\n * @returns Nothing. The hook tracks cart analytics for the lifetime of the calling component.\n *\n * @example\n * ```tsx\n * function App() {\n * useCartAnalytics();\n * return <Layout />;\n * }\n * ```\n * @publicDocs\n */\nexport function useCartAnalytics(): void {\n const store = useCartStore(\"useCartAnalytics\");\n\n useEffect(() => trackCartAnalytics(store), [store]);\n}\n\n/**\n * Returns a selected slice of cart state, or `undefined` when the component renders outside the cart provider.\n *\n * Use the hook in a component that also renders above the provider, such as a header cart badge on an error page.\n * Wherever the provider always exists, use `useCart`, which throws when the provider is missing.\n *\n * @param selector The function that picks a value from the cart state.\n * @param isEqual The function that compares the previous and next selected values. Return `true` to skip the re-render.\n * @returns The selected value, or `undefined` outside the provider.\n *\n * @example\n * ```tsx\n * function CartBadge() {\n * const count = useOptionalCart((state) => state.data.totalQuantity);\n * if (count === undefined) return null;\n * return <span>Cart ({count})</span>;\n * }\n * ```\n * @publicDocs\n */\nexport function useOptionalCart<TData extends CartData = CartData, S = unknown>(\n selector: (state: CartState<TData>) => S,\n isEqual?: (a: S, b: S) => boolean,\n): S | undefined {\n const store = useOptionalCartStore();\n if (!store) return undefined;\n return useCartSelector(store, selector, isEqual);\n}\n\nfunction useCartSelector<TData extends CartData = CartData, S = unknown>(\n store: CartStore,\n selector: (state: CartState<TData>) => S,\n isEqual?: (a: S, b: S) => boolean,\n): S {\n const cachedRef = useRef<{ state: unknown; selector: typeof selector; value: S } | null>(null);\n\n const getSnapshot = () => {\n const state = store.getState() as CartState<TData>;\n\n if (\n cachedRef.current &&\n cachedRef.current.state === state &&\n cachedRef.current.selector === selector\n ) {\n return cachedRef.current.value;\n }\n\n const next = selector(state);\n\n if (cachedRef.current && isEqual?.(cachedRef.current.value, next)) {\n cachedRef.current = { state, selector, value: cachedRef.current.value };\n return cachedRef.current.value;\n }\n\n cachedRef.current = { state, selector, value: next };\n return next;\n };\n\n return useSyncExternalStore(store.subscribe, getSnapshot, getSnapshot);\n}\n\n/**\n * Returns form props and a field register function for building cart forms.\n *\n * Spread `formProps()` on the form. The form posts to the cart endpoint. On submit, the form props stop the browser's navigation and send the form through the cart store.\n * The submit handler ignores a failed cart change. Read failures from the cart state's `errors`.\n * To skip the cart submission, call `event.preventDefault()` in a `beforeSubmit` callback. The `afterSubmit` callback runs as soon as the cart change starts, before the server responds.\n * The register function returns the HTML attributes for each cart field and action.\n *\n * Register the quantity field with `interactive: true` to submit the form when the quantity changes. The interactive input takes the quantity as a default value and stays uncontrolled.\n *\n * @returns The form props function and the register function.\n *\n * @example\n * ```tsx\n * function LineItem({ line }) {\n * const { formProps, register } = useCartForm();\n *\n * return (\n * <form {...formProps()}>\n * <input {...register(\"lineId\", { value: line.id })} />\n * <input {...register(\"quantity\", { value: line.quantity, interactive: true })} />\n * <button {...register(\"set\")} />\n * <button {...register(\"increase\")}>+</button>\n * <button {...register(\"decrease\")}>−</button>\n * <button {...register(\"remove\")}>Remove</button>\n * </form>\n * );\n * }\n * ```\n * @publicDocs\n */\nexport function useCartForm(): CartFormBindings {\n const store = useCartStore(\"useCartForm\");\n const coreRegister = useMemo(() => createCartFormRegister(), []);\n\n const register = useMemo(() => {\n type Register = typeof coreRegister;\n const wrapped = ((...args: Parameters<Register>) => {\n const result = (coreRegister as Function)(...args);\n const [field, opts] = args as [string, { interactive?: boolean }?];\n\n if (field === \"quantity\" && opts?.interactive) {\n let cleanup: (() => void) | null = null;\n const ref: RefCallback<HTMLInputElement> = (el) => {\n cleanup?.();\n cleanup = null;\n if (el) {\n const form = el.closest(\"form\");\n if (form) cleanup = attachQuantityInput(el, form);\n }\n };\n const { value: _, ...rest } = result;\n return { ...rest, defaultValue: result.value, ref };\n }\n\n return result;\n }) as Register;\n return wrapped;\n }, [coreRegister]);\n\n const formProps = (opts?: CartFormPropsOptions): FormHTMLAttributes<HTMLFormElement> => ({\n onSubmit: (e: SubmitEvent<HTMLFormElement>) => {\n opts?.beforeSubmit?.(e);\n if (e.defaultPrevented) return;\n e.preventDefault();\n store.handleFormSubmit(e.nativeEvent).catch(() => {});\n opts?.afterSubmit?.(e);\n },\n method: \"post\",\n action: cartEndpoint,\n });\n\n return { formProps, register };\n}\n"],"mappings":"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"}
|
|
@@ -4,32 +4,34 @@ import * as _$react from "react";
|
|
|
4
4
|
import { FormHTMLAttributes, ReactNode, SubmitEvent } from "react";
|
|
5
5
|
|
|
6
6
|
//#region src/react/collection.d.ts
|
|
7
|
-
/**
|
|
7
|
+
/**
|
|
8
|
+
* Props for `CollectionProvider`.
|
|
9
|
+
*/
|
|
8
10
|
interface CollectionProviderProps {
|
|
9
|
-
/**
|
|
11
|
+
/** The collection handle and the search string that your loader fetched products for. */
|
|
10
12
|
data: CollectionData;
|
|
11
13
|
/**
|
|
12
|
-
*
|
|
13
|
-
*
|
|
14
|
+
* Current URL search string from your router, with or without a leading `?`. Defaults to an empty string.
|
|
15
|
+
* The provider reads the starting filters and sort from the string. When the string changes, including on back and forward navigation, the provider updates the filters and sort.
|
|
14
16
|
*/
|
|
15
17
|
urlSearch?: string;
|
|
16
18
|
/**
|
|
17
|
-
*
|
|
18
|
-
* search string. The consumer's only job is to navigate:
|
|
19
|
+
* Receives the new search string after the customer changes filters or sort. Navigate to the search string with your router.
|
|
19
20
|
*
|
|
20
|
-
*
|
|
21
|
-
* onChange={(search) =>
|
|
22
|
-
* navigate({ search }, { replace: searchParams.size > 0, preventScrollReset: true })
|
|
23
|
-
* }
|
|
24
|
-
* ```
|
|
21
|
+
* The search string starts with `?`, or is empty when no params remain.
|
|
25
22
|
*/
|
|
26
23
|
onChange?: (searchString: string) => void;
|
|
24
|
+
/** Content that reads and changes collection state through the collection hooks. */
|
|
27
25
|
children?: ReactNode;
|
|
28
26
|
}
|
|
29
27
|
/**
|
|
30
|
-
*
|
|
31
|
-
*
|
|
32
|
-
*
|
|
28
|
+
* Shares the customer's filter and sort choices with the collection hooks and keeps the choices in sync with the URL.
|
|
29
|
+
*
|
|
30
|
+
* The provider sets the status back to `idle` when your loader data matches the URL. When the collection handle changes, such as on navigation to a different collection, the provider reads the filters and sort from the URL again.
|
|
31
|
+
*
|
|
32
|
+
* @param props - The collection data, the URL search string, the change callback, and the content that uses the collection hooks.
|
|
33
|
+
* @returns A provider for the collection hooks.
|
|
34
|
+
* @publicDocs
|
|
33
35
|
*/
|
|
34
36
|
declare function CollectionProvider({
|
|
35
37
|
data,
|
|
@@ -38,35 +40,50 @@ declare function CollectionProvider({
|
|
|
38
40
|
children
|
|
39
41
|
}: CollectionProviderProps): _$react.FunctionComponentElement<_$react.ProviderProps<CollectionStore | null>>;
|
|
40
42
|
/**
|
|
41
|
-
*
|
|
43
|
+
* Returns the customer's filter and sort choices and the loading status, and re-renders the component when the state changes. The hook throws outside `CollectionProvider`.
|
|
44
|
+
*
|
|
45
|
+
* @returns The current collection state.
|
|
42
46
|
*
|
|
43
47
|
* @example
|
|
44
48
|
* ```tsx
|
|
45
49
|
* const { status, filters } = useCollection();
|
|
46
50
|
* ```
|
|
51
|
+
* @publicDocs
|
|
47
52
|
*/
|
|
48
53
|
declare function useCollection(): CollectionState;
|
|
49
54
|
/**
|
|
50
|
-
*
|
|
51
|
-
*
|
|
52
|
-
*
|
|
55
|
+
* With a selector, the hook returns the selected value and re-renders the component when the selected value changes.
|
|
56
|
+
* Pass an `isEqual` comparator to skip re-renders for equal values. The hook throws outside `CollectionProvider`.
|
|
57
|
+
*
|
|
58
|
+
* @returns The value your selector derives from the current collection state.
|
|
53
59
|
*
|
|
54
60
|
* @example
|
|
55
61
|
* ```tsx
|
|
56
62
|
* const status = useCollection(s => s.status);
|
|
57
63
|
* const filters = useCollection(s => s.filters, shallowEqual);
|
|
58
64
|
* ```
|
|
65
|
+
* @publicDocs
|
|
59
66
|
*/
|
|
60
67
|
declare function useCollection<S>(selector: (state: CollectionState) => S, isEqual?: (a: S, b: S) => boolean): S;
|
|
61
68
|
/**
|
|
62
|
-
* Returns methods that change filters and sort.
|
|
63
|
-
*
|
|
64
|
-
*
|
|
69
|
+
* Returns methods that change the filters and sort. After each change, `CollectionProvider` passes the new search string to its `onChange` callback.
|
|
70
|
+
*
|
|
71
|
+
* The hook throws outside `CollectionProvider`.
|
|
72
|
+
*
|
|
73
|
+
* @returns Methods that set, toggle, and reset filters, change the sort, and apply submitted filter forms.
|
|
74
|
+
*
|
|
75
|
+
* @publicDocs
|
|
65
76
|
*/
|
|
66
77
|
declare function useCollectionActions(): CollectionActions;
|
|
67
78
|
/**
|
|
68
|
-
* Returns
|
|
79
|
+
* Returns props that make a collection filter form update the results without a full page load.
|
|
80
|
+
*
|
|
81
|
+
* Spread the result of `formProps()` on the form. On submit, the props cancel the browser submission and apply the form's filter and sort fields.
|
|
82
|
+
* The `beforeSubmit` callback runs first, and calling `preventDefault()` in `beforeSubmit` skips the update. The `afterSubmit` callback runs after the update.
|
|
83
|
+
*
|
|
84
|
+
* Render the form with `method="get"` and an explicit `action`, which keeps the form working before JavaScript loads. On search pages, keep `q` as a hidden input inside the form. The hook throws outside `CollectionProvider`.
|
|
69
85
|
*
|
|
86
|
+
* @returns An object with a `formProps()` function. Call the function with optional `beforeSubmit` and `afterSubmit` callbacks, and spread the result on the form.
|
|
70
87
|
* @example
|
|
71
88
|
* ```tsx
|
|
72
89
|
* const { formProps } = useCollectionForm();
|
|
@@ -77,6 +94,7 @@ declare function useCollectionActions(): CollectionActions;
|
|
|
77
94
|
* </form>
|
|
78
95
|
* );
|
|
79
96
|
* ```
|
|
97
|
+
* @publicDocs
|
|
80
98
|
*/
|
|
81
99
|
declare function useCollectionForm(): {
|
|
82
100
|
formProps: (opts?: {
|
|
@@ -7,9 +7,13 @@ import { createContext, createElement, useCallback, useContext, useLayoutEffect,
|
|
|
7
7
|
//#region src/react/collection.tsx
|
|
8
8
|
const CollectionContext = createContext(null);
|
|
9
9
|
/**
|
|
10
|
-
*
|
|
11
|
-
*
|
|
12
|
-
*
|
|
10
|
+
* Shares the customer's filter and sort choices with the collection hooks and keeps the choices in sync with the URL.
|
|
11
|
+
*
|
|
12
|
+
* The provider sets the status back to `idle` when your loader data matches the URL. When the collection handle changes, such as on navigation to a different collection, the provider reads the filters and sort from the URL again.
|
|
13
|
+
*
|
|
14
|
+
* @param props - The collection data, the URL search string, the change callback, and the content that uses the collection hooks.
|
|
15
|
+
* @returns A provider for the collection hooks.
|
|
16
|
+
* @publicDocs
|
|
13
17
|
*/
|
|
14
18
|
function CollectionProvider({ data, urlSearch = "", onChange, children }) {
|
|
15
19
|
const onChangeRef = useRef(onChange);
|
|
@@ -50,6 +54,11 @@ function useRequiredStore(hookName) {
|
|
|
50
54
|
if (!store) throw new Error(`${hookName} must be used inside a <CollectionProvider>.`);
|
|
51
55
|
return store;
|
|
52
56
|
}
|
|
57
|
+
/**
|
|
58
|
+
* @param selector The function that derives the returned value from the collection state.
|
|
59
|
+
* @param isEqual The comparator that skips a re-render when it returns `true` for two derived values.
|
|
60
|
+
* @returns The collection state, or the selector's value when you pass a selector.
|
|
61
|
+
*/
|
|
53
62
|
function useCollection(selector, isEqual) {
|
|
54
63
|
const store = useRequiredStore("useCollection");
|
|
55
64
|
const cachedRef = useRef(null);
|
|
@@ -83,9 +92,13 @@ function useCollection(selector, isEqual) {
|
|
|
83
92
|
return useSyncExternalStore(store.subscribe, getSnapshot, getSnapshot);
|
|
84
93
|
}
|
|
85
94
|
/**
|
|
86
|
-
* Returns methods that change filters and sort.
|
|
87
|
-
*
|
|
88
|
-
*
|
|
95
|
+
* Returns methods that change the filters and sort. After each change, `CollectionProvider` passes the new search string to its `onChange` callback.
|
|
96
|
+
*
|
|
97
|
+
* The hook throws outside `CollectionProvider`.
|
|
98
|
+
*
|
|
99
|
+
* @returns Methods that set, toggle, and reset filters, change the sort, and apply submitted filter forms.
|
|
100
|
+
*
|
|
101
|
+
* @publicDocs
|
|
89
102
|
*/
|
|
90
103
|
function useCollectionActions() {
|
|
91
104
|
const store = useRequiredStore("useCollectionActions");
|
|
@@ -100,8 +113,14 @@ function useCollectionActions() {
|
|
|
100
113
|
}), [store]);
|
|
101
114
|
}
|
|
102
115
|
/**
|
|
103
|
-
* Returns
|
|
116
|
+
* Returns props that make a collection filter form update the results without a full page load.
|
|
117
|
+
*
|
|
118
|
+
* Spread the result of `formProps()` on the form. On submit, the props cancel the browser submission and apply the form's filter and sort fields.
|
|
119
|
+
* The `beforeSubmit` callback runs first, and calling `preventDefault()` in `beforeSubmit` skips the update. The `afterSubmit` callback runs after the update.
|
|
120
|
+
*
|
|
121
|
+
* Render the form with `method="get"` and an explicit `action`, which keeps the form working before JavaScript loads. On search pages, keep `q` as a hidden input inside the form. The hook throws outside `CollectionProvider`.
|
|
104
122
|
*
|
|
123
|
+
* @returns An object with a `formProps()` function. Call the function with optional `beforeSubmit` and `afterSubmit` callbacks, and spread the result on the form.
|
|
105
124
|
* @example
|
|
106
125
|
* ```tsx
|
|
107
126
|
* const { formProps } = useCollectionForm();
|
|
@@ -112,6 +131,7 @@ function useCollectionActions() {
|
|
|
112
131
|
* </form>
|
|
113
132
|
* );
|
|
114
133
|
* ```
|
|
134
|
+
* @publicDocs
|
|
115
135
|
*/
|
|
116
136
|
function useCollectionForm() {
|
|
117
137
|
const actions = useCollectionActions();
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"collection.mjs","names":[],"sources":["../../../src/react/collection.tsx"],"sourcesContent":["\"use client\";\n\nimport {\n createContext,\n createElement,\n useCallback,\n useContext,\n useLayoutEffect,\n useMemo,\n useRef,\n useSyncExternalStore,\n type FormHTMLAttributes,\n type ReactNode,\n type SubmitEvent,\n} from \"react\";\n\nimport {\n createCollectionStore,\n createCollectionReconciler,\n type CollectionActions,\n type CollectionData,\n type CollectionReconciler,\n type CollectionStore,\n} from \"../core/collection\";\nimport type { CollectionState } from \"../core/collection\";\n\nconst CollectionContext = createContext<CollectionStore | null>(null);\n\n/** Props for the {@link CollectionProvider} component. */\nexport interface CollectionProviderProps {\n /** Collection metadata from the framework data fetch (`handle`, `dataSearch`). */\n data: CollectionData;\n /**\n * Live URL search string from the framework router (with or without leading `?`).\n * Seeds the store on mount and keeps browse state in sync with back/forward.\n */\n urlSearch?: string;\n /**\n * Called after user-initiated filter/sort changes with a ready-to-use\n * search string. The consumer's only job is to navigate:\n *\n * ```tsx\n * onChange={(search) =>\n * navigate({ search }, { replace: searchParams.size > 0, preventScrollReset: true })\n * }\n * ```\n */\n onChange?: (searchString: string) => void;\n children?: ReactNode;\n}\n\n/**\n * Manages the lifecycle of a {@link CollectionStore}: creates on mount and\n * syncs with URL changes. Recreates the store when `data.handle` changes\n * (navigating to a different collection).\n */\nexport function CollectionProvider({\n data,\n urlSearch = \"\",\n onChange,\n children,\n}: CollectionProviderProps) {\n const onChangeRef = useRef(onChange);\n onChangeRef.current = onChange;\n\n const urlSearchRef = useRef(urlSearch);\n urlSearchRef.current = urlSearch;\n\n const dataRef = useRef(data);\n dataRef.current = data;\n\n const { handle: collectionHandle, dataSearch } = data;\n\n const store = useMemo(\n () => createCollectionStore({ data, urlSearch }),\n // oxlint-disable-next-line react-hooks/exhaustive-deps -- store recreated only when collection handle changes\n [data.handle],\n );\n\n const reconcilerRef = useRef<CollectionReconciler | null>(null);\n\n useLayoutEffect(() => {\n reconcilerRef.current = createCollectionReconciler(\n {\n getStore: () => store,\n readUrlSearch: () => urlSearchRef.current,\n emitChange: (s) => onChangeRef.current?.(s),\n },\n urlSearchRef.current,\n );\n store.setOnBrowseChange(() => reconcilerRef.current?.handleBrowseChange());\n return () => {\n store.setOnBrowseChange(null);\n };\n }, [store]);\n\n useLayoutEffect(() => {\n reconcilerRef.current?.reconcile(urlSearch, dataRef.current.dataSearch);\n }, [urlSearch, store, collectionHandle, dataSearch]);\n\n return createElement(CollectionContext.Provider, { value: store }, children);\n}\n\nfunction useRequiredStore(hookName: string): CollectionStore {\n const store = useContext(CollectionContext);\n if (!store) {\n throw new Error(`${hookName} must be used inside a <CollectionProvider>.`);\n }\n return store;\n}\n\n/**\n * Subscribes to the collection store and returns the full state snapshot.\n *\n * @example\n * ```tsx\n * const { status, filters } = useCollection();\n * ```\n */\nexport function useCollection(): CollectionState;\n/**\n * Subscribes to the collection store and returns a derived value via `selector`.\n * Optionally accepts an `isEqual` comparator to skip re-renders when the\n * derived value is structurally unchanged.\n *\n * @example\n * ```tsx\n * const status = useCollection(s => s.status);\n * const filters = useCollection(s => s.filters, shallowEqual);\n * ```\n */\nexport function useCollection<S>(\n selector: (state: CollectionState) => S,\n isEqual?: (a: S, b: S) => boolean,\n): S;\nexport function useCollection<S>(\n selector?: (state: CollectionState) => S,\n isEqual?: (a: S, b: S) => boolean,\n): CollectionState | S {\n const store = useRequiredStore(\"useCollection\");\n const cachedRef = useRef<{\n state: unknown;\n selector: typeof selector;\n value: CollectionState | S;\n } | null>(null);\n\n const getSnapshot = () => {\n const state = store.getState();\n\n if (\n cachedRef.current &&\n cachedRef.current.state === state &&\n cachedRef.current.selector === selector\n ) {\n return cachedRef.current.value;\n }\n\n if (!selector) {\n cachedRef.current = { state, selector, value: state };\n return state;\n }\n\n const next = selector(state);\n\n if (cachedRef.current && isEqual?.(cachedRef.current.value as S, next)) {\n cachedRef.current = { state, selector, value: cachedRef.current.value };\n return cachedRef.current.value;\n }\n\n cachedRef.current = { state, selector, value: next };\n return next;\n };\n\n return useSyncExternalStore(store.subscribe, getSnapshot, getSnapshot);\n}\n\n/**\n * Returns methods that change filters and sort. The store's `onBrowseChange`\n * callback (set by {@link CollectionProvider}) handles calling `onChange` with\n * a serialized search string.\n */\nexport function useCollectionActions(): CollectionActions {\n const store = useRequiredStore(\"useCollectionActions\");\n\n return useMemo(\n () => ({\n setFilters: store.setFilters,\n toggleFilter: store.toggleFilter,\n toggleFilterInput: store.toggleFilterInput,\n setSortKey: store.setSortKey,\n setSortByValue: store.setSortByValue,\n reset: store.reset,\n handleFormSubmit: store.handleFormSubmit,\n }),\n [store],\n );\n}\n\n/**\n * Returns form props for progressive-enhancement of collection filter forms.\n *\n * @example\n * ```tsx\n * const { formProps } = useCollectionForm();\n * return (\n * <form {...formProps()} action=\"/collections/shoes\">\n * <input type=\"checkbox\" name=\"filter.p.tag\" value=\"sale\" />\n * <button type=\"submit\">Apply</button>\n * </form>\n * );\n * ```\n */\nexport function useCollectionForm() {\n const actions = useCollectionActions();\n\n const formProps = useCallback(\n (opts?: {\n beforeSubmit?: (e: SubmitEvent<HTMLFormElement>) => void;\n afterSubmit?: (e: SubmitEvent<HTMLFormElement>) => void;\n }): FormHTMLAttributes<HTMLFormElement> => ({\n onSubmit: (e: SubmitEvent<HTMLFormElement>) => {\n opts?.beforeSubmit?.(e);\n if (e.defaultPrevented) return;\n e.preventDefault();\n actions.handleFormSubmit(e.nativeEvent);\n opts?.afterSubmit?.(e);\n },\n }),\n [actions],\n );\n\n return { formProps };\n}\n\nexport type { CollectionActions, CollectionData };\n"],"mappings":";;;;;;;AA0BA,MAAM,oBAAoB,cAAsC,KAAK;;;;;;AA8BrE,SAAgB,mBAAmB,EACjC,MACA,YAAY,IACZ,UACA,YAC0B;CAC1B,MAAM,cAAc,OAAO,SAAS;AACpC,aAAY,UAAU;CAEtB,MAAM,eAAe,OAAO,UAAU;AACtC,cAAa,UAAU;CAEvB,MAAM,UAAU,OAAO,KAAK;AAC5B,SAAQ,UAAU;CAElB,MAAM,EAAE,QAAQ,kBAAkB,eAAe;CAEjD,MAAM,QAAQ,cACN,sBAAsB;EAAE;EAAM;EAAW,CAAC,EAEhD,CAAC,KAAK,OAAO,CACd;CAED,MAAM,gBAAgB,OAAoC,KAAK;AAE/D,uBAAsB;AACpB,gBAAc,UAAU,2BACtB;GACE,gBAAgB;GAChB,qBAAqB,aAAa;GAClC,aAAa,MAAM,YAAY,UAAU,EAAE;GAC5C,EACD,aAAa,QACd;AACD,QAAM,wBAAwB,cAAc,SAAS,oBAAoB,CAAC;AAC1E,eAAa;AACX,SAAM,kBAAkB,KAAK;;IAE9B,CAAC,MAAM,CAAC;AAEX,uBAAsB;AACpB,gBAAc,SAAS,UAAU,WAAW,QAAQ,QAAQ,WAAW;IACtE;EAAC;EAAW;EAAO;EAAkB;EAAW,CAAC;AAEpD,QAAO,cAAc,kBAAkB,UAAU,EAAE,OAAO,OAAO,EAAE,SAAS;;AAG9E,SAAS,iBAAiB,UAAmC;CAC3D,MAAM,QAAQ,WAAW,kBAAkB;AAC3C,KAAI,CAAC,MACH,OAAM,IAAI,MAAM,GAAG,SAAS,8CAA8C;AAE5E,QAAO;;AA2BT,SAAgB,cACd,UACA,SACqB;CACrB,MAAM,QAAQ,iBAAiB,gBAAgB;CAC/C,MAAM,YAAY,OAIR,KAAK;CAEf,MAAM,oBAAoB;EACxB,MAAM,QAAQ,MAAM,UAAU;AAE9B,MACE,UAAU,WACV,UAAU,QAAQ,UAAU,SAC5B,UAAU,QAAQ,aAAa,SAE/B,QAAO,UAAU,QAAQ;AAG3B,MAAI,CAAC,UAAU;AACb,aAAU,UAAU;IAAE;IAAO;IAAU,OAAO;IAAO;AACrD,UAAO;;EAGT,MAAM,OAAO,SAAS,MAAM;AAE5B,MAAI,UAAU,WAAW,UAAU,UAAU,QAAQ,OAAY,KAAK,EAAE;AACtE,aAAU,UAAU;IAAE;IAAO;IAAU,OAAO,UAAU,QAAQ;IAAO;AACvE,UAAO,UAAU,QAAQ;;AAG3B,YAAU,UAAU;GAAE;GAAO;GAAU,OAAO;GAAM;AACpD,SAAO;;AAGT,QAAO,qBAAqB,MAAM,WAAW,aAAa,YAAY;;;;;;;AAQxE,SAAgB,uBAA0C;CACxD,MAAM,QAAQ,iBAAiB,uBAAuB;AAEtD,QAAO,eACE;EACL,YAAY,MAAM;EAClB,cAAc,MAAM;EACpB,mBAAmB,MAAM;EACzB,YAAY,MAAM;EAClB,gBAAgB,MAAM;EACtB,OAAO,MAAM;EACb,kBAAkB,MAAM;EACzB,GACD,CAAC,MAAM,CACR;;;;;;;;;;;;;;;;AAiBH,SAAgB,oBAAoB;CAClC,MAAM,UAAU,sBAAsB;AAkBtC,QAAO,EAAE,WAhBS,aACf,UAG2C,EAC1C,WAAW,MAAoC;AAC7C,QAAM,eAAe,EAAE;AACvB,MAAI,EAAE,iBAAkB;AACxB,IAAE,gBAAgB;AAClB,UAAQ,iBAAiB,EAAE,YAAY;AACvC,QAAM,cAAc,EAAE;IAEzB,GACD,CAAC,QAAQ,CAGO,EAAE"}
|
|
1
|
+
{"version":3,"file":"collection.mjs","names":[],"sources":["../../../src/react/collection.tsx"],"sourcesContent":["\"use client\";\n\nimport {\n createContext,\n createElement,\n useCallback,\n useContext,\n useLayoutEffect,\n useMemo,\n useRef,\n useSyncExternalStore,\n type FormHTMLAttributes,\n type ReactNode,\n type SubmitEvent,\n} from \"react\";\n\nimport {\n createCollectionStore,\n createCollectionReconciler,\n type CollectionActions,\n type CollectionData,\n type CollectionReconciler,\n type CollectionStore,\n} from \"../core/collection\";\nimport type { CollectionState } from \"../core/collection\";\n\nconst CollectionContext = createContext<CollectionStore | null>(null);\n\n/**\n * Props for `CollectionProvider`.\n */\nexport interface CollectionProviderProps {\n /** The collection handle and the search string that your loader fetched products for. */\n data: CollectionData;\n /**\n * Current URL search string from your router, with or without a leading `?`. Defaults to an empty string.\n * The provider reads the starting filters and sort from the string. When the string changes, including on back and forward navigation, the provider updates the filters and sort.\n */\n urlSearch?: string;\n /**\n * Receives the new search string after the customer changes filters or sort. Navigate to the search string with your router.\n *\n * The search string starts with `?`, or is empty when no params remain.\n */\n onChange?: (searchString: string) => void;\n /** Content that reads and changes collection state through the collection hooks. */\n children?: ReactNode;\n}\n\n/**\n * Shares the customer's filter and sort choices with the collection hooks and keeps the choices in sync with the URL.\n *\n * The provider sets the status back to `idle` when your loader data matches the URL. When the collection handle changes, such as on navigation to a different collection, the provider reads the filters and sort from the URL again.\n *\n * @param props - The collection data, the URL search string, the change callback, and the content that uses the collection hooks.\n * @returns A provider for the collection hooks.\n * @publicDocs\n */\nexport function CollectionProvider({\n data,\n urlSearch = \"\",\n onChange,\n children,\n}: CollectionProviderProps) {\n const onChangeRef = useRef(onChange);\n onChangeRef.current = onChange;\n\n const urlSearchRef = useRef(urlSearch);\n urlSearchRef.current = urlSearch;\n\n const dataRef = useRef(data);\n dataRef.current = data;\n\n const { handle: collectionHandle, dataSearch } = data;\n\n const store = useMemo(\n () => createCollectionStore({ data, urlSearch }),\n // oxlint-disable-next-line react-hooks/exhaustive-deps -- store recreated only when collection handle changes\n [data.handle],\n );\n\n const reconcilerRef = useRef<CollectionReconciler | null>(null);\n\n useLayoutEffect(() => {\n reconcilerRef.current = createCollectionReconciler(\n {\n getStore: () => store,\n readUrlSearch: () => urlSearchRef.current,\n emitChange: (s) => onChangeRef.current?.(s),\n },\n urlSearchRef.current,\n );\n store.setOnBrowseChange(() => reconcilerRef.current?.handleBrowseChange());\n return () => {\n store.setOnBrowseChange(null);\n };\n }, [store]);\n\n useLayoutEffect(() => {\n reconcilerRef.current?.reconcile(urlSearch, dataRef.current.dataSearch);\n }, [urlSearch, store, collectionHandle, dataSearch]);\n\n return createElement(CollectionContext.Provider, { value: store }, children);\n}\n\nfunction useRequiredStore(hookName: string): CollectionStore {\n const store = useContext(CollectionContext);\n if (!store) {\n throw new Error(`${hookName} must be used inside a <CollectionProvider>.`);\n }\n return store;\n}\n\n/**\n * Returns the customer's filter and sort choices and the loading status, and re-renders the component when the state changes. The hook throws outside `CollectionProvider`.\n *\n * @returns The current collection state.\n *\n * @example\n * ```tsx\n * const { status, filters } = useCollection();\n * ```\n * @publicDocs\n */\nexport function useCollection(): CollectionState;\n/**\n * With a selector, the hook returns the selected value and re-renders the component when the selected value changes.\n * Pass an `isEqual` comparator to skip re-renders for equal values. The hook throws outside `CollectionProvider`.\n *\n * @returns The value your selector derives from the current collection state.\n *\n * @example\n * ```tsx\n * const status = useCollection(s => s.status);\n * const filters = useCollection(s => s.filters, shallowEqual);\n * ```\n * @publicDocs\n */\nexport function useCollection<S>(\n selector: (state: CollectionState) => S,\n isEqual?: (a: S, b: S) => boolean,\n): S;\n/**\n * @param selector The function that derives the returned value from the collection state.\n * @param isEqual The comparator that skips a re-render when it returns `true` for two derived values.\n * @returns The collection state, or the selector's value when you pass a selector.\n */\nexport function useCollection<S>(\n selector?: (state: CollectionState) => S,\n isEqual?: (a: S, b: S) => boolean,\n): CollectionState | S {\n const store = useRequiredStore(\"useCollection\");\n const cachedRef = useRef<{\n state: unknown;\n selector: typeof selector;\n value: CollectionState | S;\n } | null>(null);\n\n const getSnapshot = () => {\n const state = store.getState();\n\n if (\n cachedRef.current &&\n cachedRef.current.state === state &&\n cachedRef.current.selector === selector\n ) {\n return cachedRef.current.value;\n }\n\n if (!selector) {\n cachedRef.current = { state, selector, value: state };\n return state;\n }\n\n const next = selector(state);\n\n if (cachedRef.current && isEqual?.(cachedRef.current.value as S, next)) {\n cachedRef.current = { state, selector, value: cachedRef.current.value };\n return cachedRef.current.value;\n }\n\n cachedRef.current = { state, selector, value: next };\n return next;\n };\n\n return useSyncExternalStore(store.subscribe, getSnapshot, getSnapshot);\n}\n\n/**\n * Returns methods that change the filters and sort. After each change, `CollectionProvider` passes the new search string to its `onChange` callback.\n *\n * The hook throws outside `CollectionProvider`.\n *\n * @returns Methods that set, toggle, and reset filters, change the sort, and apply submitted filter forms.\n *\n * @publicDocs\n */\nexport function useCollectionActions(): CollectionActions {\n const store = useRequiredStore(\"useCollectionActions\");\n\n return useMemo(\n () => ({\n setFilters: store.setFilters,\n toggleFilter: store.toggleFilter,\n toggleFilterInput: store.toggleFilterInput,\n setSortKey: store.setSortKey,\n setSortByValue: store.setSortByValue,\n reset: store.reset,\n handleFormSubmit: store.handleFormSubmit,\n }),\n [store],\n );\n}\n\n/**\n * Returns props that make a collection filter form update the results without a full page load.\n *\n * Spread the result of `formProps()` on the form. On submit, the props cancel the browser submission and apply the form's filter and sort fields.\n * The `beforeSubmit` callback runs first, and calling `preventDefault()` in `beforeSubmit` skips the update. The `afterSubmit` callback runs after the update.\n *\n * Render the form with `method=\"get\"` and an explicit `action`, which keeps the form working before JavaScript loads. On search pages, keep `q` as a hidden input inside the form. The hook throws outside `CollectionProvider`.\n *\n * @returns An object with a `formProps()` function. Call the function with optional `beforeSubmit` and `afterSubmit` callbacks, and spread the result on the form.\n * @example\n * ```tsx\n * const { formProps } = useCollectionForm();\n * return (\n * <form {...formProps()} action=\"/collections/shoes\">\n * <input type=\"checkbox\" name=\"filter.p.tag\" value=\"sale\" />\n * <button type=\"submit\">Apply</button>\n * </form>\n * );\n * ```\n * @publicDocs\n */\nexport function useCollectionForm() {\n const actions = useCollectionActions();\n\n const formProps = useCallback(\n (opts?: {\n beforeSubmit?: (e: SubmitEvent<HTMLFormElement>) => void;\n afterSubmit?: (e: SubmitEvent<HTMLFormElement>) => void;\n }): FormHTMLAttributes<HTMLFormElement> => ({\n onSubmit: (e: SubmitEvent<HTMLFormElement>) => {\n opts?.beforeSubmit?.(e);\n if (e.defaultPrevented) return;\n e.preventDefault();\n actions.handleFormSubmit(e.nativeEvent);\n opts?.afterSubmit?.(e);\n },\n }),\n [actions],\n );\n\n return { formProps };\n}\n\nexport type { CollectionActions, CollectionData };\n"],"mappings":";;;;;;;AA0BA,MAAM,oBAAoB,cAAsC,KAAK;;;;;;;;;;AAgCrE,SAAgB,mBAAmB,EACjC,MACA,YAAY,IACZ,UACA,YAC0B;CAC1B,MAAM,cAAc,OAAO,SAAS;AACpC,aAAY,UAAU;CAEtB,MAAM,eAAe,OAAO,UAAU;AACtC,cAAa,UAAU;CAEvB,MAAM,UAAU,OAAO,KAAK;AAC5B,SAAQ,UAAU;CAElB,MAAM,EAAE,QAAQ,kBAAkB,eAAe;CAEjD,MAAM,QAAQ,cACN,sBAAsB;EAAE;EAAM;EAAW,CAAC,EAEhD,CAAC,KAAK,OAAO,CACd;CAED,MAAM,gBAAgB,OAAoC,KAAK;AAE/D,uBAAsB;AACpB,gBAAc,UAAU,2BACtB;GACE,gBAAgB;GAChB,qBAAqB,aAAa;GAClC,aAAa,MAAM,YAAY,UAAU,EAAE;GAC5C,EACD,aAAa,QACd;AACD,QAAM,wBAAwB,cAAc,SAAS,oBAAoB,CAAC;AAC1E,eAAa;AACX,SAAM,kBAAkB,KAAK;;IAE9B,CAAC,MAAM,CAAC;AAEX,uBAAsB;AACpB,gBAAc,SAAS,UAAU,WAAW,QAAQ,QAAQ,WAAW;IACtE;EAAC;EAAW;EAAO;EAAkB;EAAW,CAAC;AAEpD,QAAO,cAAc,kBAAkB,UAAU,EAAE,OAAO,OAAO,EAAE,SAAS;;AAG9E,SAAS,iBAAiB,UAAmC;CAC3D,MAAM,QAAQ,WAAW,kBAAkB;AAC3C,KAAI,CAAC,MACH,OAAM,IAAI,MAAM,GAAG,SAAS,8CAA8C;AAE5E,QAAO;;;;;;;AAqCT,SAAgB,cACd,UACA,SACqB;CACrB,MAAM,QAAQ,iBAAiB,gBAAgB;CAC/C,MAAM,YAAY,OAIR,KAAK;CAEf,MAAM,oBAAoB;EACxB,MAAM,QAAQ,MAAM,UAAU;AAE9B,MACE,UAAU,WACV,UAAU,QAAQ,UAAU,SAC5B,UAAU,QAAQ,aAAa,SAE/B,QAAO,UAAU,QAAQ;AAG3B,MAAI,CAAC,UAAU;AACb,aAAU,UAAU;IAAE;IAAO;IAAU,OAAO;IAAO;AACrD,UAAO;;EAGT,MAAM,OAAO,SAAS,MAAM;AAE5B,MAAI,UAAU,WAAW,UAAU,UAAU,QAAQ,OAAY,KAAK,EAAE;AACtE,aAAU,UAAU;IAAE;IAAO;IAAU,OAAO,UAAU,QAAQ;IAAO;AACvE,UAAO,UAAU,QAAQ;;AAG3B,YAAU,UAAU;GAAE;GAAO;GAAU,OAAO;GAAM;AACpD,SAAO;;AAGT,QAAO,qBAAqB,MAAM,WAAW,aAAa,YAAY;;;;;;;;;;;AAYxE,SAAgB,uBAA0C;CACxD,MAAM,QAAQ,iBAAiB,uBAAuB;AAEtD,QAAO,eACE;EACL,YAAY,MAAM;EAClB,cAAc,MAAM;EACpB,mBAAmB,MAAM;EACzB,YAAY,MAAM;EAClB,gBAAgB,MAAM;EACtB,OAAO,MAAM;EACb,kBAAkB,MAAM;EACzB,GACD,CAAC,MAAM,CACR;;;;;;;;;;;;;;;;;;;;;;;AAwBH,SAAgB,oBAAoB;CAClC,MAAM,UAAU,sBAAsB;AAkBtC,QAAO,EAAE,WAhBS,aACf,UAG2C,EAC1C,WAAW,MAAoC;AAC7C,QAAM,eAAe,EAAE;AACvB,MAAI,EAAE,iBAAkB;AACxB,IAAE,gBAAgB;AAClB,UAAQ,iBAAiB,EAAE,YAAY;AACvC,QAAM,cAAc,EAAE;IAEzB,GACD,CAAC,QAAQ,CAGO,EAAE"}
|