@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
|
@@ -3,25 +3,27 @@ import { ProductFilter } from "../../vendor/standard-events.mjs";
|
|
|
3
3
|
import { CollectionState } from "./state.mjs";
|
|
4
4
|
|
|
5
5
|
//#region src/core/collection/collection.d.ts
|
|
6
|
-
/**
|
|
6
|
+
/** The collection handle and the search string that your loader fetched products for. */
|
|
7
7
|
type CollectionData = {
|
|
8
|
-
/** URL-safe collection slug (e.g. `"shoes"`). */handle: string;
|
|
9
8
|
/**
|
|
10
|
-
*
|
|
11
|
-
*
|
|
9
|
+
* Collection handle, such as `shoes`.
|
|
10
|
+
*
|
|
11
|
+
* On search pages, pass a handle such as `search:${term}` to keep separate filter and sort state for each search term. When the handle changes, the collection provider reads the filters and sort from the URL again.
|
|
12
|
+
*/
|
|
13
|
+
handle: string;
|
|
14
|
+
/**
|
|
15
|
+
* Search string that your loader used for the Storefront API query, with or without a leading `?`.
|
|
16
|
+
*
|
|
17
|
+
* Pass the exact string. The status stays `loading` until this string and the URL search hold the same filters, sort key, and sort direction.
|
|
12
18
|
*/
|
|
13
19
|
dataSearch: string;
|
|
14
20
|
};
|
|
15
21
|
/**
|
|
16
|
-
*
|
|
22
|
+
* Holds the customer's filter and sort choices for a collection or search page, and reports when new results are loading.
|
|
17
23
|
*
|
|
18
|
-
*
|
|
19
|
-
* data (products, productsCount, availableFilters) lives in the framework's
|
|
20
|
-
* loader data, not here.
|
|
24
|
+
* Each change updates the state right away and sets the status to `loading`. Your app then navigates, your loader fetches the new products, and you call `settle()` when the data arrives. The collection provider navigates and settles the store for you.
|
|
21
25
|
*
|
|
22
|
-
*
|
|
23
|
-
* The framework adapter navigates (URL change), the framework loader re-runs,
|
|
24
|
-
* and the adapter calls {@link settle} when fresh data arrives.
|
|
26
|
+
* The store keeps the sort direction only for the price, title, created, and ID sort keys. For other sort keys, `reverse` reads `false`. The store keeps at most one availability filter. Adding a second availability filter removes both availability filters.
|
|
25
27
|
*
|
|
26
28
|
* @example
|
|
27
29
|
* ```ts
|
|
@@ -35,104 +37,100 @@ type CollectionData = {
|
|
|
35
37
|
* ```
|
|
36
38
|
*/
|
|
37
39
|
type CollectionStore = {
|
|
38
|
-
/** Returns the current
|
|
40
|
+
/** Returns the current filter and sort state. */getState(): CollectionState;
|
|
39
41
|
/**
|
|
40
|
-
*
|
|
41
|
-
* @param listener - Callback
|
|
42
|
+
* Calls the listener on every state change. Returns a function that unsubscribes the listener.
|
|
43
|
+
* @param listener - Callback that receives the new state.
|
|
42
44
|
*/
|
|
43
45
|
subscribe(listener: (state: CollectionState) => void): () => void;
|
|
44
46
|
/**
|
|
45
|
-
* Replaces the active
|
|
46
|
-
* Sets `status: "loading"` — call {@link settle} when the framework fetch completes.
|
|
47
|
+
* Replaces the active filters and sets the status to `loading`.
|
|
47
48
|
*/
|
|
48
49
|
setFilters(filters: ProductFilter[]): void;
|
|
49
50
|
/**
|
|
50
|
-
* Adds the filter
|
|
51
|
-
* Sets `status: "loading"` — call {@link settle} when the framework fetch completes.
|
|
51
|
+
* Adds the filter when it's inactive and removes the filter when it's active, then sets the status to `loading`.
|
|
52
52
|
*/
|
|
53
53
|
toggleFilter(filter: ProductFilter): void;
|
|
54
54
|
/**
|
|
55
|
-
* Changes the sort
|
|
56
|
-
*
|
|
55
|
+
* Changes the sort key and direction, then sets the status to `loading`.
|
|
56
|
+
* Omit `reverse` to sort in ascending order.
|
|
57
57
|
*/
|
|
58
58
|
setSortKey(sortKey: ProductCollectionSortKeys, reverse?: boolean): void;
|
|
59
59
|
/**
|
|
60
|
-
*
|
|
61
|
-
* Sets `status: "loading"` — call {@link settle} when the framework fetch completes.
|
|
60
|
+
* Clears all filters, restores the collection's default sort, and sets the status to `loading`.
|
|
62
61
|
*/
|
|
63
62
|
reset(): void;
|
|
64
63
|
/**
|
|
65
|
-
* Returns `true` when URL params
|
|
64
|
+
* Returns `true` when the URL search params hold the same filters and sort as the store.
|
|
66
65
|
*/
|
|
67
66
|
matchesParams(searchParams: URLSearchParams): boolean;
|
|
68
67
|
/**
|
|
69
|
-
*
|
|
70
|
-
*
|
|
68
|
+
* Updates the store from URL search params that differ from the current state.
|
|
69
|
+
* Call the method when the URL changes outside the store, such as on back or forward navigation. The method sets the status to `loading` and skips the browse change callback.
|
|
71
70
|
*/
|
|
72
71
|
syncFromParams(searchParams: URLSearchParams): void;
|
|
73
72
|
/**
|
|
74
|
-
*
|
|
75
|
-
* Resets `status` to `"idle"`.
|
|
73
|
+
* Sets the status to `idle`. Call the method when your loader returns the new products.
|
|
76
74
|
*/
|
|
77
75
|
settle(): void;
|
|
78
76
|
/**
|
|
79
|
-
*
|
|
80
|
-
*
|
|
77
|
+
* Returns the filters and sort as URL search params.
|
|
78
|
+
* The result holds only the `filter.*` and `sort_by` keys.
|
|
81
79
|
*/
|
|
82
80
|
serializeToParams(): URLSearchParams;
|
|
83
81
|
/**
|
|
84
|
-
*
|
|
85
|
-
*
|
|
82
|
+
* Returns a query string with the store's filters and sort, minus the given filter.
|
|
83
|
+
* Use the query string in remove-filter links and buttons.
|
|
84
|
+
*
|
|
85
|
+
* The string starts with `?` and leaves out URL params that the store doesn't manage.
|
|
86
86
|
*/
|
|
87
87
|
getFilterRemovalUrl(filter: ProductFilter): string;
|
|
88
88
|
/**
|
|
89
|
-
*
|
|
90
|
-
*
|
|
91
|
-
* submits the form natively and navigates away.
|
|
89
|
+
* Applies a submitted filter form to the store.
|
|
90
|
+
* The form's filter fields replace the active filters. The sort changes only when the form includes a `sort_by` field.
|
|
92
91
|
*
|
|
93
|
-
*
|
|
92
|
+
* Call `event.preventDefault()` first, or the browser submits the form and leaves the page. The method throws a `TypeError` when the event target isn't a form element.
|
|
94
93
|
*/
|
|
95
94
|
handleFormSubmit(event: SubmitEvent): void;
|
|
96
95
|
/**
|
|
97
|
-
*
|
|
98
|
-
*
|
|
96
|
+
* Sets the callback that runs after each filter or sort change. Navigate to the new URL in the callback.
|
|
97
|
+
* Pass `null` to remove the callback.
|
|
99
98
|
*/
|
|
100
99
|
setOnBrowseChange(callback: (() => void) | null): void;
|
|
101
100
|
/**
|
|
102
|
-
*
|
|
103
|
-
*
|
|
104
|
-
* No-op if `input` is not valid JSON.
|
|
101
|
+
* Toggles the filter in a filter value's `input` JSON string from the Storefront API.
|
|
102
|
+
* The method does nothing when the string isn't valid JSON.
|
|
105
103
|
*/
|
|
106
104
|
toggleFilterInput(input: string): void;
|
|
107
105
|
/**
|
|
108
|
-
*
|
|
109
|
-
*
|
|
106
|
+
* Applies the sort key and direction from a Liquid-compatible `sort_by` value, such as `price-ascending`.
|
|
107
|
+
* An unrecognized value sets the sort key to `COLLECTION_DEFAULT`.
|
|
110
108
|
*/
|
|
111
109
|
setSortByValue(sortByValue: string): void;
|
|
112
110
|
};
|
|
113
|
-
/**
|
|
111
|
+
/** Collection store methods that change the filters and sort. The `useCollectionActions` hook returns these methods. */
|
|
114
112
|
type CollectionActions = Pick<CollectionStore, "setFilters" | "toggleFilter" | "toggleFilterInput" | "setSortKey" | "setSortByValue" | "reset" | "handleFormSubmit">;
|
|
115
|
-
/** Options for
|
|
113
|
+
/** Options for `createCollectionStore`. */
|
|
116
114
|
type CreateCollectionStoreOptions = {
|
|
117
|
-
/**
|
|
115
|
+
/** The collection handle and the search string that your loader fetched products for. */data: CollectionData;
|
|
118
116
|
/**
|
|
119
|
-
*
|
|
120
|
-
*
|
|
117
|
+
* Current URL search string from your router, with or without a leading `?`.
|
|
118
|
+
* Defaults to the `dataSearch` string from `data`.
|
|
121
119
|
*/
|
|
122
120
|
urlSearch?: string;
|
|
123
121
|
/**
|
|
124
|
-
*
|
|
125
|
-
*
|
|
126
|
-
* {@link CollectionStore.syncFromParams} or {@link CollectionStore.settle}.
|
|
122
|
+
* Runs after each filter or sort change from a store method. Navigate to the new URL in the callback.
|
|
123
|
+
* Syncing from URL params and settling the store don't run the callback.
|
|
127
124
|
*/
|
|
128
125
|
onBrowseChange?: () => void;
|
|
129
126
|
};
|
|
130
127
|
/**
|
|
131
|
-
* Creates a
|
|
128
|
+
* Creates a store that holds the customer's filter and sort choices for one collection. Your loader fetches the products.
|
|
132
129
|
*
|
|
133
|
-
* The store
|
|
134
|
-
* Server response data lives in the framework's loader.
|
|
130
|
+
* The store reads its starting filters and sort from `urlSearch`, or from `dataSearch` when you omit `urlSearch`. In React, use `CollectionProvider`, which creates the store for you and keeps the store in sync with the URL.
|
|
135
131
|
*
|
|
132
|
+
* @param options The collection data, the current URL search string, and the browse change callback.
|
|
133
|
+
* @returns A store with methods that change the filters and sort and report the loading status.
|
|
136
134
|
* @example
|
|
137
135
|
* ```ts
|
|
138
136
|
* const store = createCollectionStore({
|
|
@@ -143,6 +141,7 @@ type CreateCollectionStoreOptions = {
|
|
|
143
141
|
*
|
|
144
142
|
* store.subscribe((state) => renderFilters(state.filters));
|
|
145
143
|
* ```
|
|
144
|
+
* @publicDocs
|
|
146
145
|
*/
|
|
147
146
|
declare function createCollectionStore(options: CreateCollectionStoreOptions): CollectionStore;
|
|
148
147
|
//#endregion
|
|
@@ -4,11 +4,12 @@ import { collectionParamsMatchState, filterEquals, getFilterRemovalUrl, isDirect
|
|
|
4
4
|
|
|
5
5
|
//#region src/core/collection/collection.ts
|
|
6
6
|
/**
|
|
7
|
-
* Creates a
|
|
7
|
+
* Creates a store that holds the customer's filter and sort choices for one collection. Your loader fetches the products.
|
|
8
8
|
*
|
|
9
|
-
* The store
|
|
10
|
-
* Server response data lives in the framework's loader.
|
|
9
|
+
* The store reads its starting filters and sort from `urlSearch`, or from `dataSearch` when you omit `urlSearch`. In React, use `CollectionProvider`, which creates the store for you and keeps the store in sync with the URL.
|
|
11
10
|
*
|
|
11
|
+
* @param options The collection data, the current URL search string, and the browse change callback.
|
|
12
|
+
* @returns A store with methods that change the filters and sort and report the loading status.
|
|
12
13
|
* @example
|
|
13
14
|
* ```ts
|
|
14
15
|
* const store = createCollectionStore({
|
|
@@ -19,6 +20,7 @@ import { collectionParamsMatchState, filterEquals, getFilterRemovalUrl, isDirect
|
|
|
19
20
|
*
|
|
20
21
|
* store.subscribe((state) => renderFilters(state.filters));
|
|
21
22
|
* ```
|
|
23
|
+
* @publicDocs
|
|
22
24
|
*/
|
|
23
25
|
function createCollectionStore(options) {
|
|
24
26
|
const context = {
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"collection.mjs","names":[],"sources":["../../../../src/core/collection/collection.ts"],"sourcesContent":["import type { ProductCollectionSortKeys } from \"../../graphql/generated/storefront-api-types\";\nimport { createObservable } from \"../observable\";\nimport type { CollectionState, ProductFilter } from \"./state\";\nimport { createInitialCollectionState } from \"./state\";\nimport {\n collectionParamsMatchState,\n filterEquals,\n getFilterRemovalUrl,\n isDirectionalSortKey,\n parseCollectionParams,\n parseSortByValue,\n serializeCollectionParams,\n} from \"./url\";\n\n/** Snapshot from the framework loader for a single collection fetch. */\nexport type CollectionData = {\n /** URL-safe collection slug (e.g. `\"shoes\"`). */\n handle: string;\n /**\n * Search string this data snapshot was fetched for (with or without leading `?`).\n * Compared against live {@link CreateCollectionStoreOptions.urlSearch} before settling.\n */\n dataSearch: string;\n};\n\n/**\n * A reactive, framework-agnostic store for collection browse state.\n *\n * Manages filters and sorting — the user's *intent*. Server response\n * data (products, productsCount, availableFilters) lives in the framework's\n * loader data, not here.\n *\n * Mutations are synchronous state changes that set `status: \"loading\"`.\n * The framework adapter navigates (URL change), the framework loader re-runs,\n * and the adapter calls {@link settle} when fresh data arrives.\n *\n * @example\n * ```ts\n * const store = createCollectionStore({\n * data: { handle: \"shoes\", dataSearch: \"\" },\n * });\n *\n * store.setFilters([{ tag: \"sale\" }]); // status → \"loading\"\n * // ...framework fetches...\n * store.settle(); // status → \"idle\"\n * ```\n */\nexport type CollectionStore = {\n /** Returns the current snapshot of collection browse state. */\n getState(): CollectionState;\n\n /**\n * Registers a listener invoked on every state change. Returns an unsubscribe function.\n * @param listener - Callback receiving the new state snapshot\n */\n subscribe(listener: (state: CollectionState) => void): () => void;\n\n /**\n * Replaces the active product filters.\n * Sets `status: \"loading\"` — call {@link settle} when the framework fetch completes.\n */\n setFilters(filters: ProductFilter[]): void;\n\n /**\n * Adds the filter if not active, removes it if already active.\n * Sets `status: \"loading\"` — call {@link settle} when the framework fetch completes.\n */\n toggleFilter(filter: ProductFilter): void;\n\n /**\n * Changes the sort order.\n * Sets `status: \"loading\"` — call {@link settle} when the framework fetch completes.\n */\n setSortKey(sortKey: ProductCollectionSortKeys, reverse?: boolean): void;\n\n /**\n * Resets filters and sort to defaults.\n * Sets `status: \"loading\"` — call {@link settle} when the framework fetch completes.\n */\n reset(): void;\n\n /**\n * Returns `true` when URL params and current browse state describe the same filters and sort.\n */\n matchesParams(searchParams: URLSearchParams): boolean;\n\n /**\n * Applies URL search params to the store when they differ from current state.\n * Used when the framework router reports an external URL change (back/forward).\n */\n syncFromParams(searchParams: URLSearchParams): void;\n\n /**\n * Signals that the framework fetch completed and fresh data is available.\n * Resets `status` to `\"idle\"`.\n */\n settle(): void;\n\n /**\n * Serializes the store's current filter/sort state into URL search params.\n * Only includes store-owned keys (`filter.*`, `sort_by`).\n */\n serializeToParams(): URLSearchParams;\n\n /**\n * Builds a URL string that removes the given filter from the current params.\n * Useful for rendering \"remove filter\" links/buttons.\n */\n getFilterRemovalUrl(filter: ProductFilter): string;\n\n /**\n * Parses `FormData` from the submitted form into collection params and applies them.\n * Doesn't cancel the event: call `event.preventDefault()` first, or the browser\n * submits the form natively and navigates away.\n *\n * @throws {TypeError} If `event.target` is not an `HTMLFormElement`.\n */\n handleFormSubmit(event: SubmitEvent): void;\n\n /**\n * Replaces the callback that runs after user filter/sort changes. Framework\n * adapters use this to trigger navigation. Pass `null` to remove.\n */\n setOnBrowseChange(callback: (() => void) | null): void;\n\n /**\n * Parses a Storefront API filter input JSON string (e.g. from\n * `FilterValue.input`) and toggles the resulting filter.\n * No-op if `input` is not valid JSON.\n */\n toggleFilterInput(input: string): void;\n\n /**\n * Parses a Liquid-compatible `sort_by` value (e.g. `\"price-ascending\"`) and\n * applies the corresponding sort key and direction.\n */\n setSortByValue(sortByValue: string): void;\n};\n\n/** Mutation methods exposed by the collection store. */\nexport type CollectionActions = Pick<\n CollectionStore,\n | \"setFilters\"\n | \"toggleFilter\"\n | \"toggleFilterInput\"\n | \"setSortKey\"\n | \"setSortByValue\"\n | \"reset\"\n | \"handleFormSubmit\"\n>;\n\n/** Options for creating a new {@link CollectionStore}. */\nexport type CreateCollectionStoreOptions = {\n /** Collection metadata from the framework loader. */\n data: CollectionData;\n /**\n * Live URL search string from the framework router (with or without leading `?`).\n * Falls back to `data.dataSearch` when omitted.\n */\n urlSearch?: string;\n /**\n * Called after user-initiated filter/sort changes.\n * Framework adapters use this to trigger navigation. Not called by\n * {@link CollectionStore.syncFromParams} or {@link CollectionStore.settle}.\n */\n onBrowseChange?: () => void;\n};\n\ntype CollectionStoreContext = {\n observable: ReturnType<typeof createObservable<CollectionState>>;\n handle: string;\n onBrowseChange: (() => void) | null;\n};\n\n/**\n * Creates a new collection store for the given collection handle.\n *\n * The store manages browse intent (filters, sort) only.\n * Server response data lives in the framework's loader.\n *\n * @example\n * ```ts\n * const store = createCollectionStore({\n * data: loaderData.collection,\n * urlSearch: window.location.search,\n * onBrowseChange: () => navigate({ search: store.serializeToParams().toString() }),\n * });\n *\n * store.subscribe((state) => renderFilters(state.filters));\n * ```\n */\nexport function createCollectionStore(options: CreateCollectionStoreOptions): CollectionStore {\n const initialState = buildInitialState(options);\n\n const context: CollectionStoreContext = {\n observable: createObservable<CollectionState>(initialState),\n handle: options.data.handle,\n onBrowseChange: options.onBrowseChange ?? null,\n };\n\n return {\n getState: () => context.observable.state,\n subscribe: (listener) => context.observable.subscribe(listener),\n setFilters: (filters) => applyBrowseChange(context, { filters: normalizeFilters(filters) }),\n toggleFilter: (filter) => toggleFilter(context, filter),\n setSortKey: (sortKey, reverse) =>\n applyBrowseChange(context, { sortKey, reverse: reverse ?? false }),\n reset: () => resetStore(context),\n matchesParams: (searchParams) => matchesParams(context, searchParams),\n syncFromParams: (searchParams) => syncFromParams(context, searchParams),\n settle: () => settleStore(context),\n serializeToParams: () => serializeCollectionParams(context.observable.state),\n getFilterRemovalUrl: (filter) =>\n getFilterRemovalUrl(serializeCollectionParams(context.observable.state), filter),\n handleFormSubmit: (event) => handleFormSubmit(context, event),\n setOnBrowseChange: (callback) => {\n context.onBrowseChange = callback;\n },\n toggleFilterInput: (input) => toggleFilterInput(context, input),\n setSortByValue: (sortByValue) => setSortByValue(context, sortByValue),\n };\n}\n\nfunction buildInitialState(options: CreateCollectionStoreOptions): CollectionState {\n const base = createInitialCollectionState(options.data.handle);\n\n const search = options.urlSearch ?? options.data.dataSearch;\n if (search) {\n const parsed = parseCollectionParams(new URLSearchParams(search));\n base.filters = parsed.filters;\n base.sortKey = parsed.sortKey;\n base.reverse = normalizeReverse(parsed.sortKey, parsed.reverse);\n }\n\n return base;\n}\n\ntype BrowseChange = Partial<Pick<CollectionState, \"filters\" | \"sortKey\" | \"reverse\">>;\n\n/**\n * Drops `reverse` for sort keys that do not support direction.\n * Only `PRICE`, `TITLE`, `CREATED`, and `ID` honor ascending/descending.\n *\n * @example normalizeReverse(\"BEST_SELLING\", true) // → false\n * @example normalizeReverse(\"PRICE\", true) // → true\n */\nfunction normalizeReverse(\n sortKey: ProductCollectionSortKeys | undefined,\n reverse: boolean,\n): boolean {\n if (!sortKey || !isDirectionalSortKey(sortKey)) return false;\n return reverse;\n}\n\nfunction applyBrowseChange(\n context: CollectionStoreContext,\n change: BrowseChange,\n notify = true,\n): void {\n context.observable.setState((prev) => {\n const merged = { ...prev, ...change, status: \"loading\" as const };\n merged.reverse = normalizeReverse(merged.sortKey, merged.reverse);\n return merged;\n });\n if (notify) {\n context.onBrowseChange?.();\n }\n}\n\nfunction toggleFilter(context: CollectionStoreContext, filter: ProductFilter): void {\n const current = context.observable.state.filters;\n const isActive = current.some((f) => filterEquals(f, filter));\n\n const next = isActive\n ? current.filter((f) => !filterEquals(f, filter))\n : normalizeFilters([...current, filter]);\n\n applyBrowseChange(context, { filters: next });\n}\n\nfunction normalizeFilters(filters: ProductFilter[]): ProductFilter[] {\n const normalized: ProductFilter[] = [];\n\n for (const filter of filters) {\n if (filter.available != null) {\n // Availability is a single boolean facet. Duplicate availability params\n // (filter.v.availability=1&filter.v.availability=0) mean no filter, so\n // keeping both values here prevents the provider from settling navigation.\n const existingAvailabilityIndex = normalized.findIndex((candidate) => {\n return candidate.available != null;\n });\n\n if (existingAvailabilityIndex !== -1) {\n normalized.splice(existingAvailabilityIndex, 1);\n continue;\n }\n }\n\n normalized.push(filter);\n }\n\n return normalized;\n}\n\nfunction resetStore(context: CollectionStoreContext): void {\n const initial = createInitialCollectionState(context.handle);\n applyBrowseChange(context, {\n filters: initial.filters,\n sortKey: initial.sortKey,\n reverse: initial.reverse,\n });\n}\n\nfunction matchesParams(context: CollectionStoreContext, searchParams: URLSearchParams): boolean {\n return collectionParamsMatchState(searchParams, context.observable.state);\n}\n\nfunction syncFromParams(context: CollectionStoreContext, searchParams: URLSearchParams): void {\n if (matchesParams(context, searchParams)) return;\n\n const parsed = parseCollectionParams(searchParams);\n\n // URL caught up to an in-flight browse change — align state without re-entering loading.\n if (context.observable.state.status === \"loading\") {\n context.observable.setState((prev) => ({\n ...prev,\n filters: parsed.filters,\n sortKey: parsed.sortKey,\n reverse: normalizeReverse(parsed.sortKey, parsed.reverse),\n status: \"loading\",\n }));\n return;\n }\n\n /**\n * URL already changes, so we don't need to notify to call onBrowseChange\n * again which would cause a double navigate.\n */\n applyBrowseChange(\n context,\n {\n filters: parsed.filters,\n sortKey: parsed.sortKey,\n reverse: parsed.reverse,\n },\n false,\n );\n}\n\nfunction settleStore(context: CollectionStoreContext): void {\n context.observable.setState((prev) => {\n if (prev.status === \"idle\") return prev;\n return { ...prev, status: \"idle\" };\n });\n}\n\nfunction handleFormSubmit(context: CollectionStoreContext, event: SubmitEvent): void {\n if (!(event.target instanceof HTMLFormElement)) {\n throw new TypeError(`Expected event.target to be an HTMLFormElement, got ${event.target}`);\n }\n\n const formData = new FormData(event.target);\n const formParams = new URLSearchParams();\n\n for (const [key, value] of formData.entries()) {\n if (typeof value === \"string\") {\n formParams.append(key, value);\n }\n }\n\n const parsed = parseCollectionParams(formParams);\n\n const change: BrowseChange = {\n filters: normalizeFilters(parsed.filters),\n };\n\n if (formParams.has(\"sort_by\")) {\n change.sortKey = parsed.sortKey;\n change.reverse = parsed.reverse;\n }\n\n applyBrowseChange(context, change);\n}\n\nfunction toggleFilterInput(context: CollectionStoreContext, input: string): void {\n let parsed: ProductFilter;\n try {\n parsed = JSON.parse(input) as ProductFilter;\n } catch {\n return;\n }\n toggleFilter(context, parsed);\n}\n\nfunction setSortByValue(context: CollectionStoreContext, sortByValue: string): void {\n const { sortKey, reverse } = parseSortByValue(sortByValue);\n applyBrowseChange(context, {\n sortKey: sortKey ?? \"COLLECTION_DEFAULT\",\n reverse: reverse ?? false,\n });\n}\n"],"mappings":"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"}
|
|
1
|
+
{"version":3,"file":"collection.mjs","names":[],"sources":["../../../../src/core/collection/collection.ts"],"sourcesContent":["import type { ProductCollectionSortKeys } from \"../../graphql/generated/storefront-api-types\";\nimport { createObservable } from \"../observable\";\nimport type { CollectionState, ProductFilter } from \"./state\";\nimport { createInitialCollectionState } from \"./state\";\nimport {\n collectionParamsMatchState,\n filterEquals,\n getFilterRemovalUrl,\n isDirectionalSortKey,\n parseCollectionParams,\n parseSortByValue,\n serializeCollectionParams,\n} from \"./url\";\n\n/** The collection handle and the search string that your loader fetched products for. */\nexport type CollectionData = {\n /**\n * Collection handle, such as `shoes`.\n *\n * On search pages, pass a handle such as `search:${term}` to keep separate filter and sort state for each search term. When the handle changes, the collection provider reads the filters and sort from the URL again.\n */\n handle: string;\n /**\n * Search string that your loader used for the Storefront API query, with or without a leading `?`.\n *\n * Pass the exact string. The status stays `loading` until this string and the URL search hold the same filters, sort key, and sort direction.\n */\n dataSearch: string;\n};\n\n/**\n * Holds the customer's filter and sort choices for a collection or search page, and reports when new results are loading.\n *\n * Each change updates the state right away and sets the status to `loading`. Your app then navigates, your loader fetches the new products, and you call `settle()` when the data arrives. The collection provider navigates and settles the store for you.\n *\n * The store keeps the sort direction only for the price, title, created, and ID sort keys. For other sort keys, `reverse` reads `false`. The store keeps at most one availability filter. Adding a second availability filter removes both availability filters.\n *\n * @example\n * ```ts\n * const store = createCollectionStore({\n * data: { handle: \"shoes\", dataSearch: \"\" },\n * });\n *\n * store.setFilters([{ tag: \"sale\" }]); // status → \"loading\"\n * // ...framework fetches...\n * store.settle(); // status → \"idle\"\n * ```\n */\nexport type CollectionStore = {\n /** Returns the current filter and sort state. */\n getState(): CollectionState;\n\n /**\n * Calls the listener on every state change. Returns a function that unsubscribes the listener.\n * @param listener - Callback that receives the new state.\n */\n subscribe(listener: (state: CollectionState) => void): () => void;\n\n /**\n * Replaces the active filters and sets the status to `loading`.\n */\n setFilters(filters: ProductFilter[]): void;\n\n /**\n * Adds the filter when it's inactive and removes the filter when it's active, then sets the status to `loading`.\n */\n toggleFilter(filter: ProductFilter): void;\n\n /**\n * Changes the sort key and direction, then sets the status to `loading`.\n * Omit `reverse` to sort in ascending order.\n */\n setSortKey(sortKey: ProductCollectionSortKeys, reverse?: boolean): void;\n\n /**\n * Clears all filters, restores the collection's default sort, and sets the status to `loading`.\n */\n reset(): void;\n\n /**\n * Returns `true` when the URL search params hold the same filters and sort as the store.\n */\n matchesParams(searchParams: URLSearchParams): boolean;\n\n /**\n * Updates the store from URL search params that differ from the current state.\n * Call the method when the URL changes outside the store, such as on back or forward navigation. The method sets the status to `loading` and skips the browse change callback.\n */\n syncFromParams(searchParams: URLSearchParams): void;\n\n /**\n * Sets the status to `idle`. Call the method when your loader returns the new products.\n */\n settle(): void;\n\n /**\n * Returns the filters and sort as URL search params.\n * The result holds only the `filter.*` and `sort_by` keys.\n */\n serializeToParams(): URLSearchParams;\n\n /**\n * Returns a query string with the store's filters and sort, minus the given filter.\n * Use the query string in remove-filter links and buttons.\n *\n * The string starts with `?` and leaves out URL params that the store doesn't manage.\n */\n getFilterRemovalUrl(filter: ProductFilter): string;\n\n /**\n * Applies a submitted filter form to the store.\n * The form's filter fields replace the active filters. The sort changes only when the form includes a `sort_by` field.\n *\n * Call `event.preventDefault()` first, or the browser submits the form and leaves the page. The method throws a `TypeError` when the event target isn't a form element.\n */\n handleFormSubmit(event: SubmitEvent): void;\n\n /**\n * Sets the callback that runs after each filter or sort change. Navigate to the new URL in the callback.\n * Pass `null` to remove the callback.\n */\n setOnBrowseChange(callback: (() => void) | null): void;\n\n /**\n * Toggles the filter in a filter value's `input` JSON string from the Storefront API.\n * The method does nothing when the string isn't valid JSON.\n */\n toggleFilterInput(input: string): void;\n\n /**\n * Applies the sort key and direction from a Liquid-compatible `sort_by` value, such as `price-ascending`.\n * An unrecognized value sets the sort key to `COLLECTION_DEFAULT`.\n */\n setSortByValue(sortByValue: string): void;\n};\n\n/** Collection store methods that change the filters and sort. The `useCollectionActions` hook returns these methods. */\nexport type CollectionActions = Pick<\n CollectionStore,\n | \"setFilters\"\n | \"toggleFilter\"\n | \"toggleFilterInput\"\n | \"setSortKey\"\n | \"setSortByValue\"\n | \"reset\"\n | \"handleFormSubmit\"\n>;\n\n/** Options for `createCollectionStore`. */\nexport type CreateCollectionStoreOptions = {\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 `?`.\n * Defaults to the `dataSearch` string from `data`.\n */\n urlSearch?: string;\n /**\n * Runs after each filter or sort change from a store method. Navigate to the new URL in the callback.\n * Syncing from URL params and settling the store don't run the callback.\n */\n onBrowseChange?: () => void;\n};\n\ntype CollectionStoreContext = {\n observable: ReturnType<typeof createObservable<CollectionState>>;\n handle: string;\n onBrowseChange: (() => void) | null;\n};\n\n/**\n * Creates a store that holds the customer's filter and sort choices for one collection. Your loader fetches the products.\n *\n * The store reads its starting filters and sort from `urlSearch`, or from `dataSearch` when you omit `urlSearch`. In React, use `CollectionProvider`, which creates the store for you and keeps the store in sync with the URL.\n *\n * @param options The collection data, the current URL search string, and the browse change callback.\n * @returns A store with methods that change the filters and sort and report the loading status.\n * @example\n * ```ts\n * const store = createCollectionStore({\n * data: loaderData.collection,\n * urlSearch: window.location.search,\n * onBrowseChange: () => navigate({ search: store.serializeToParams().toString() }),\n * });\n *\n * store.subscribe((state) => renderFilters(state.filters));\n * ```\n * @publicDocs\n */\nexport function createCollectionStore(options: CreateCollectionStoreOptions): CollectionStore {\n const initialState = buildInitialState(options);\n\n const context: CollectionStoreContext = {\n observable: createObservable<CollectionState>(initialState),\n handle: options.data.handle,\n onBrowseChange: options.onBrowseChange ?? null,\n };\n\n return {\n getState: () => context.observable.state,\n subscribe: (listener) => context.observable.subscribe(listener),\n setFilters: (filters) => applyBrowseChange(context, { filters: normalizeFilters(filters) }),\n toggleFilter: (filter) => toggleFilter(context, filter),\n setSortKey: (sortKey, reverse) =>\n applyBrowseChange(context, { sortKey, reverse: reverse ?? false }),\n reset: () => resetStore(context),\n matchesParams: (searchParams) => matchesParams(context, searchParams),\n syncFromParams: (searchParams) => syncFromParams(context, searchParams),\n settle: () => settleStore(context),\n serializeToParams: () => serializeCollectionParams(context.observable.state),\n getFilterRemovalUrl: (filter) =>\n getFilterRemovalUrl(serializeCollectionParams(context.observable.state), filter),\n handleFormSubmit: (event) => handleFormSubmit(context, event),\n setOnBrowseChange: (callback) => {\n context.onBrowseChange = callback;\n },\n toggleFilterInput: (input) => toggleFilterInput(context, input),\n setSortByValue: (sortByValue) => setSortByValue(context, sortByValue),\n };\n}\n\nfunction buildInitialState(options: CreateCollectionStoreOptions): CollectionState {\n const base = createInitialCollectionState(options.data.handle);\n\n const search = options.urlSearch ?? options.data.dataSearch;\n if (search) {\n const parsed = parseCollectionParams(new URLSearchParams(search));\n base.filters = parsed.filters;\n base.sortKey = parsed.sortKey;\n base.reverse = normalizeReverse(parsed.sortKey, parsed.reverse);\n }\n\n return base;\n}\n\ntype BrowseChange = Partial<Pick<CollectionState, \"filters\" | \"sortKey\" | \"reverse\">>;\n\n/**\n * Drops `reverse` for sort keys that do not support direction.\n * Only `PRICE`, `TITLE`, `CREATED`, and `ID` honor ascending/descending.\n *\n * @example normalizeReverse(\"BEST_SELLING\", true) // → false\n * @example normalizeReverse(\"PRICE\", true) // → true\n */\nfunction normalizeReverse(\n sortKey: ProductCollectionSortKeys | undefined,\n reverse: boolean,\n): boolean {\n if (!sortKey || !isDirectionalSortKey(sortKey)) return false;\n return reverse;\n}\n\nfunction applyBrowseChange(\n context: CollectionStoreContext,\n change: BrowseChange,\n notify = true,\n): void {\n context.observable.setState((prev) => {\n const merged = { ...prev, ...change, status: \"loading\" as const };\n merged.reverse = normalizeReverse(merged.sortKey, merged.reverse);\n return merged;\n });\n if (notify) {\n context.onBrowseChange?.();\n }\n}\n\nfunction toggleFilter(context: CollectionStoreContext, filter: ProductFilter): void {\n const current = context.observable.state.filters;\n const isActive = current.some((f) => filterEquals(f, filter));\n\n const next = isActive\n ? current.filter((f) => !filterEquals(f, filter))\n : normalizeFilters([...current, filter]);\n\n applyBrowseChange(context, { filters: next });\n}\n\nfunction normalizeFilters(filters: ProductFilter[]): ProductFilter[] {\n const normalized: ProductFilter[] = [];\n\n for (const filter of filters) {\n if (filter.available != null) {\n // Availability is a single boolean facet. Duplicate availability params\n // (filter.v.availability=1&filter.v.availability=0) mean no filter, so\n // keeping both values here prevents the provider from settling navigation.\n const existingAvailabilityIndex = normalized.findIndex((candidate) => {\n return candidate.available != null;\n });\n\n if (existingAvailabilityIndex !== -1) {\n normalized.splice(existingAvailabilityIndex, 1);\n continue;\n }\n }\n\n normalized.push(filter);\n }\n\n return normalized;\n}\n\nfunction resetStore(context: CollectionStoreContext): void {\n const initial = createInitialCollectionState(context.handle);\n applyBrowseChange(context, {\n filters: initial.filters,\n sortKey: initial.sortKey,\n reverse: initial.reverse,\n });\n}\n\nfunction matchesParams(context: CollectionStoreContext, searchParams: URLSearchParams): boolean {\n return collectionParamsMatchState(searchParams, context.observable.state);\n}\n\nfunction syncFromParams(context: CollectionStoreContext, searchParams: URLSearchParams): void {\n if (matchesParams(context, searchParams)) return;\n\n const parsed = parseCollectionParams(searchParams);\n\n // URL caught up to an in-flight browse change — align state without re-entering loading.\n if (context.observable.state.status === \"loading\") {\n context.observable.setState((prev) => ({\n ...prev,\n filters: parsed.filters,\n sortKey: parsed.sortKey,\n reverse: normalizeReverse(parsed.sortKey, parsed.reverse),\n status: \"loading\",\n }));\n return;\n }\n\n /**\n * URL already changes, so we don't need to notify to call onBrowseChange\n * again which would cause a double navigate.\n */\n applyBrowseChange(\n context,\n {\n filters: parsed.filters,\n sortKey: parsed.sortKey,\n reverse: parsed.reverse,\n },\n false,\n );\n}\n\nfunction settleStore(context: CollectionStoreContext): void {\n context.observable.setState((prev) => {\n if (prev.status === \"idle\") return prev;\n return { ...prev, status: \"idle\" };\n });\n}\n\nfunction handleFormSubmit(context: CollectionStoreContext, event: SubmitEvent): void {\n if (!(event.target instanceof HTMLFormElement)) {\n throw new TypeError(`Expected event.target to be an HTMLFormElement, got ${event.target}`);\n }\n\n const formData = new FormData(event.target);\n const formParams = new URLSearchParams();\n\n for (const [key, value] of formData.entries()) {\n if (typeof value === \"string\") {\n formParams.append(key, value);\n }\n }\n\n const parsed = parseCollectionParams(formParams);\n\n const change: BrowseChange = {\n filters: normalizeFilters(parsed.filters),\n };\n\n if (formParams.has(\"sort_by\")) {\n change.sortKey = parsed.sortKey;\n change.reverse = parsed.reverse;\n }\n\n applyBrowseChange(context, change);\n}\n\nfunction toggleFilterInput(context: CollectionStoreContext, input: string): void {\n let parsed: ProductFilter;\n try {\n parsed = JSON.parse(input) as ProductFilter;\n } catch {\n return;\n }\n toggleFilter(context, parsed);\n}\n\nfunction setSortByValue(context: CollectionStoreContext, sortByValue: string): void {\n const { sortKey, reverse } = parseSortByValue(sortByValue);\n applyBrowseChange(context, {\n sortKey: sortKey ?? \"COLLECTION_DEFAULT\",\n reverse: reverse ?? false,\n });\n}\n"],"mappings":";;;;;;;;;;;;;;;;;;;;;;;;AA6LA,SAAgB,sBAAsB,SAAwD;CAG5F,MAAM,UAAkC;EACtC,YAAY,iBAHO,kBAAkB,QAGqB,CAAC;EAC3D,QAAQ,QAAQ,KAAK;EACrB,gBAAgB,QAAQ,kBAAkB;EAC3C;AAED,QAAO;EACL,gBAAgB,QAAQ,WAAW;EACnC,YAAY,aAAa,QAAQ,WAAW,UAAU,SAAS;EAC/D,aAAa,YAAY,kBAAkB,SAAS,EAAE,SAAS,iBAAiB,QAAQ,EAAE,CAAC;EAC3F,eAAe,WAAW,aAAa,SAAS,OAAO;EACvD,aAAa,SAAS,YACpB,kBAAkB,SAAS;GAAE;GAAS,SAAS,WAAW;GAAO,CAAC;EACpE,aAAa,WAAW,QAAQ;EAChC,gBAAgB,iBAAiB,cAAc,SAAS,aAAa;EACrE,iBAAiB,iBAAiB,eAAe,SAAS,aAAa;EACvE,cAAc,YAAY,QAAQ;EAClC,yBAAyB,0BAA0B,QAAQ,WAAW,MAAM;EAC5E,sBAAsB,WACpB,oBAAoB,0BAA0B,QAAQ,WAAW,MAAM,EAAE,OAAO;EAClF,mBAAmB,UAAU,iBAAiB,SAAS,MAAM;EAC7D,oBAAoB,aAAa;AAC/B,WAAQ,iBAAiB;;EAE3B,oBAAoB,UAAU,kBAAkB,SAAS,MAAM;EAC/D,iBAAiB,gBAAgB,eAAe,SAAS,YAAY;EACtE;;AAGH,SAAS,kBAAkB,SAAwD;CACjF,MAAM,OAAO,6BAA6B,QAAQ,KAAK,OAAO;CAE9D,MAAM,SAAS,QAAQ,aAAa,QAAQ,KAAK;AACjD,KAAI,QAAQ;EACV,MAAM,SAAS,sBAAsB,IAAI,gBAAgB,OAAO,CAAC;AACjE,OAAK,UAAU,OAAO;AACtB,OAAK,UAAU,OAAO;AACtB,OAAK,UAAU,iBAAiB,OAAO,SAAS,OAAO,QAAQ;;AAGjE,QAAO;;;;;;;;;AAYT,SAAS,iBACP,SACA,SACS;AACT,KAAI,CAAC,WAAW,CAAC,qBAAqB,QAAQ,CAAE,QAAO;AACvD,QAAO;;AAGT,SAAS,kBACP,SACA,QACA,SAAS,MACH;AACN,SAAQ,WAAW,UAAU,SAAS;EACpC,MAAM,SAAS;GAAE,GAAG;GAAM,GAAG;GAAQ,QAAQ;GAAoB;AACjE,SAAO,UAAU,iBAAiB,OAAO,SAAS,OAAO,QAAQ;AACjE,SAAO;GACP;AACF,KAAI,OACF,SAAQ,kBAAkB;;AAI9B,SAAS,aAAa,SAAiC,QAA6B;CAClF,MAAM,UAAU,QAAQ,WAAW,MAAM;AAOzC,mBAAkB,SAAS,EAAE,SANZ,QAAQ,MAAM,MAAM,aAAa,GAAG,OAAO,CAEvC,GACjB,QAAQ,QAAQ,MAAM,CAAC,aAAa,GAAG,OAAO,CAAC,GAC/C,iBAAiB,CAAC,GAAG,SAAS,OAAO,CAAC,EAEE,CAAC;;AAG/C,SAAS,iBAAiB,SAA2C;CACnE,MAAM,aAA8B,EAAE;AAEtC,MAAK,MAAM,UAAU,SAAS;AAC5B,MAAI,OAAO,aAAa,MAAM;GAI5B,MAAM,4BAA4B,WAAW,WAAW,cAAc;AACpE,WAAO,UAAU,aAAa;KAC9B;AAEF,OAAI,8BAA8B,IAAI;AACpC,eAAW,OAAO,2BAA2B,EAAE;AAC/C;;;AAIJ,aAAW,KAAK,OAAO;;AAGzB,QAAO;;AAGT,SAAS,WAAW,SAAuC;CACzD,MAAM,UAAU,6BAA6B,QAAQ,OAAO;AAC5D,mBAAkB,SAAS;EACzB,SAAS,QAAQ;EACjB,SAAS,QAAQ;EACjB,SAAS,QAAQ;EAClB,CAAC;;AAGJ,SAAS,cAAc,SAAiC,cAAwC;AAC9F,QAAO,2BAA2B,cAAc,QAAQ,WAAW,MAAM;;AAG3E,SAAS,eAAe,SAAiC,cAAqC;AAC5F,KAAI,cAAc,SAAS,aAAa,CAAE;CAE1C,MAAM,SAAS,sBAAsB,aAAa;AAGlD,KAAI,QAAQ,WAAW,MAAM,WAAW,WAAW;AACjD,UAAQ,WAAW,UAAU,UAAU;GACrC,GAAG;GACH,SAAS,OAAO;GAChB,SAAS,OAAO;GAChB,SAAS,iBAAiB,OAAO,SAAS,OAAO,QAAQ;GACzD,QAAQ;GACT,EAAE;AACH;;;;;;AAOF,mBACE,SACA;EACE,SAAS,OAAO;EAChB,SAAS,OAAO;EAChB,SAAS,OAAO;EACjB,EACD,MACD;;AAGH,SAAS,YAAY,SAAuC;AAC1D,SAAQ,WAAW,UAAU,SAAS;AACpC,MAAI,KAAK,WAAW,OAAQ,QAAO;AACnC,SAAO;GAAE,GAAG;GAAM,QAAQ;GAAQ;GAClC;;AAGJ,SAAS,iBAAiB,SAAiC,OAA0B;AACnF,KAAI,EAAE,MAAM,kBAAkB,iBAC5B,OAAM,IAAI,UAAU,uDAAuD,MAAM,SAAS;CAG5F,MAAM,WAAW,IAAI,SAAS,MAAM,OAAO;CAC3C,MAAM,aAAa,IAAI,iBAAiB;AAExC,MAAK,MAAM,CAAC,KAAK,UAAU,SAAS,SAAS,CAC3C,KAAI,OAAO,UAAU,SACnB,YAAW,OAAO,KAAK,MAAM;CAIjC,MAAM,SAAS,sBAAsB,WAAW;CAEhD,MAAM,SAAuB,EAC3B,SAAS,iBAAiB,OAAO,QAAQ,EAC1C;AAED,KAAI,WAAW,IAAI,UAAU,EAAE;AAC7B,SAAO,UAAU,OAAO;AACxB,SAAO,UAAU,OAAO;;AAG1B,mBAAkB,SAAS,OAAO;;AAGpC,SAAS,kBAAkB,SAAiC,OAAqB;CAC/E,IAAI;AACJ,KAAI;AACF,WAAS,KAAK,MAAM,MAAM;SACpB;AACN;;AAEF,cAAa,SAAS,OAAO;;AAG/B,SAAS,eAAe,SAAiC,aAA2B;CAClF,MAAM,EAAE,SAAS,YAAY,iBAAiB,YAAY;AAC1D,mBAAkB,SAAS;EACzB,SAAS,WAAW;EACpB,SAAS,WAAW;EACrB,CAAC"}
|
|
@@ -2,38 +2,45 @@ import { CollectionStore } from "./collection.mjs";
|
|
|
2
2
|
|
|
3
3
|
//#region src/core/collection/reconciler.d.ts
|
|
4
4
|
/**
|
|
5
|
-
*
|
|
6
|
-
* push navigation side-effects without depending on any specific framework.
|
|
5
|
+
* Functions that connect a collection reconciler to your router and your collection store.
|
|
7
6
|
*/
|
|
8
7
|
type ReconcilerCallbacks = {
|
|
9
|
-
/** Returns the
|
|
10
|
-
readUrlSearch: () => string; /**
|
|
8
|
+
/** Returns the collection store. */getStore: () => CollectionStore; /** Returns the current URL search string from your router. */
|
|
9
|
+
readUrlSearch: () => string; /** Navigates your router to the search string. The string starts with `?`, or is empty when no params remain. */
|
|
11
10
|
emitChange: (searchString: string) => void;
|
|
12
11
|
};
|
|
13
12
|
/**
|
|
14
|
-
*
|
|
15
|
-
* sync during browse-change chains. Created by `createCollectionReconciler`.
|
|
13
|
+
* Keeps the URL, your loader data, and a collection store in sync while the customer changes filters and sort.
|
|
16
14
|
*/
|
|
17
15
|
type CollectionReconciler = {
|
|
18
16
|
/**
|
|
19
|
-
*
|
|
20
|
-
*
|
|
21
|
-
*
|
|
17
|
+
* Brings the store in line with the URL. Call the method each time the URL search or the `dataSearch` string changes.
|
|
18
|
+
*
|
|
19
|
+
* The method settles the store when your loader data matches the URL. During rapid filter changes, the method skips intermediate URLs. A URL that the reconciler didn't request counts as external navigation, such as back or forward.
|
|
22
20
|
*/
|
|
23
21
|
reconcile(urlSearch: string, dataSearch: string): void;
|
|
24
22
|
/**
|
|
25
|
-
*
|
|
26
|
-
*
|
|
23
|
+
* Builds the next search string from the store's filters and sort, and passes the string to `emitChange`. Register the method as the store's browse change callback.
|
|
24
|
+
*
|
|
25
|
+
* The new search string keeps every URL param that the store doesn't manage and drops the `before` and `after` pagination cursors.
|
|
26
|
+
*/
|
|
27
|
+
handleBrowseChange(): void;
|
|
28
|
+
/**
|
|
29
|
+
* Clears pending navigation. Call the method when you replace the store.
|
|
30
|
+
* @param newPrevUrlSearch - Current URL search string, which the next `reconcile()` call compares against.
|
|
27
31
|
*/
|
|
28
|
-
handleBrowseChange(): void; /** Clears pending navigation state (call when the store is recreated). */
|
|
29
32
|
reset(newPrevUrlSearch: string): void;
|
|
30
33
|
};
|
|
31
34
|
/**
|
|
32
|
-
* Creates a
|
|
33
|
-
*
|
|
35
|
+
* Creates a reconciler that keeps the URL, your loader data, and a collection store in sync while the customer changes filters and sort.
|
|
36
|
+
*
|
|
37
|
+
* Use a reconciler to connect a collection store to a router that has no Hydrogen binding. In React, use `CollectionProvider`.
|
|
38
|
+
*
|
|
39
|
+
* @param callbacks The functions that return the store and the URL search string and navigate your router.
|
|
40
|
+
* @param initialPrevUrlSearch The current URL search string, which the first `reconcile()` call compares against. Defaults to an empty string.
|
|
41
|
+
* @returns A reconciler. Call `reconcile()` on URL changes, and register `handleBrowseChange()` as the store's browse change callback.
|
|
34
42
|
*
|
|
35
|
-
*
|
|
36
|
-
* lives in one place.
|
|
43
|
+
* @publicDocs
|
|
37
44
|
*/
|
|
38
45
|
declare function createCollectionReconciler(callbacks: ReconcilerCallbacks, initialPrevUrlSearch?: string): CollectionReconciler;
|
|
39
46
|
//#endregion
|
|
@@ -2,11 +2,15 @@ import { collectionSearchEqual, mergeCollectionParams, normalizeCollectionSearch
|
|
|
2
2
|
|
|
3
3
|
//#region src/core/collection/reconciler.ts
|
|
4
4
|
/**
|
|
5
|
-
* Creates a
|
|
6
|
-
* keeping the URL, server data, and store in sync during browse-change chains.
|
|
5
|
+
* Creates a reconciler that keeps the URL, your loader data, and a collection store in sync while the customer changes filters and sort.
|
|
7
6
|
*
|
|
8
|
-
*
|
|
9
|
-
*
|
|
7
|
+
* Use a reconciler to connect a collection store to a router that has no Hydrogen binding. In React, use `CollectionProvider`.
|
|
8
|
+
*
|
|
9
|
+
* @param callbacks The functions that return the store and the URL search string and navigate your router.
|
|
10
|
+
* @param initialPrevUrlSearch The current URL search string, which the first `reconcile()` call compares against. Defaults to an empty string.
|
|
11
|
+
* @returns A reconciler. Call `reconcile()` on URL changes, and register `handleBrowseChange()` as the store's browse change callback.
|
|
12
|
+
*
|
|
13
|
+
* @publicDocs
|
|
10
14
|
*/
|
|
11
15
|
function createCollectionReconciler(callbacks, initialPrevUrlSearch = "") {
|
|
12
16
|
let pendingFilter = null;
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"reconciler.mjs","names":[],"sources":["../../../../src/core/collection/reconciler.ts"],"sourcesContent":["import type { CollectionStore } from \"./collection\";\nimport { collectionSearchEqual, mergeCollectionParams, normalizeCollectionSearch } from \"./url\";\n\n/**\n * Adapter-supplied hooks that let the reconciler read framework state and\n * push navigation side-effects without depending on any specific framework.\n */\nexport type ReconcilerCallbacks = {\n /** Returns the current collection store instance. */\n getStore: () => CollectionStore;\n /** Reads the live URL search string from the framework router. */\n readUrlSearch: () => string;\n /** Delivers a serialized search string for the framework adapter to handle. */\n emitChange: (searchString: string) => void;\n};\n\n/**\n * State machine that keeps the URL, server data, and `CollectionStore` in\n * sync during browse-change chains. Created by `createCollectionReconciler`.\n */\nexport type CollectionReconciler = {\n /**\n * URL ↔ store reconciliation state machine. Call whenever `urlSearch` or\n * `dataSearch` changes. Store changes go through\n * {@link CollectionReconciler.handleBrowseChange} instead.\n */\n reconcile(urlSearch: string, dataSearch: string): void;\n /**\n * Callback for {@link CollectionStore.setOnBrowseChange}. Merges current\n * URL params with store state, tracks the pending navigation, and emits.\n */\n handleBrowseChange(): void;\n /** Clears pending navigation state (call when the store is recreated). */\n reset(newPrevUrlSearch: string): void;\n};\n\n/**\n * Creates a framework-agnostic reconciler that manages the state machine for\n * keeping the URL, server data, and store in sync during browse-change chains.\n *\n * Framework adapters delegate to this so the reconciliation logic\n * lives in one place.\n */\nexport function createCollectionReconciler(\n callbacks: ReconcilerCallbacks,\n initialPrevUrlSearch = \"\",\n): CollectionReconciler {\n let pendingFilter: string | null = null;\n let pendingUrls = new Set<string>();\n let prevUrlSearch = initialPrevUrlSearch;\n\n return {\n handleBrowseChange() {\n const store = callbacks.getStore();\n const merged = mergeCollectionParams(\n new URLSearchParams(callbacks.readUrlSearch()),\n store.getState(),\n );\n const searchString = merged.toString();\n pendingFilter = searchString;\n pendingUrls.add(searchString);\n callbacks.emitChange(searchString ? `?${searchString}` : \"\");\n },\n\n reset(newPrevUrlSearch: string) {\n pendingFilter = null;\n pendingUrls = new Set();\n prevUrlSearch = newPrevUrlSearch;\n },\n\n // State machine — four states:\n //\n // ┌─────────────────────────────────────────────────────────────────────┐\n // │ State A: No pending navigation (pendingFilter === null) │\n // │ • URL matches store → try settle if server data caught up │\n // │ • URL differs from store → syncFromParams, then try settle │\n // ├─────────────────────────────────────────────────────────────────────┤\n // │ State B: Pending — URL matches target (pendingTargetMatchesUrl) │\n // │ • Server data matches URL → settle, clear pending state │\n // │ • Server data stale (intermediate) → re-dispatch to target │\n // │ • Server data not yet arrived → wait (keep pending set) │\n // ├─────────────────────────────────────────────────────────────────────┤\n // │ State C: Pending — URL is intermediate (in pendingUrls) │\n // │ • Ignore URL change; if router settled here with server data │\n // │ confirming the intermediate, re-dispatch to the real target │\n // ├─────────────────────────────────────────────────────────────────────┤\n // │ State D: Pending — URL is unknown (not in pendingUrls) │\n // │ • External navigation (back/forward, <Link>) │\n // │ • Abandon pending state, fall through to State A │\n // └─────────────────────────────────────────────────────────────────────┘\n //\n // Rapid toggles (A on → B on → A off) produce multiple in-flight navigations.\n // pendingFilter tracks the latest target; pendingUrls tracks every URL\n // dispatched in the chain so intermediates can be distinguished from external navs.\n reconcile(urlSearch: string, dataSearch: string) {\n const store = callbacks.getStore();\n\n const pendingSearchEquals = (search: string): boolean =>\n [...pendingUrls].some((pending) => collectionSearchEqual(pending, search));\n\n const pendingTargetMatchesUrl =\n pendingFilter != null && collectionSearchEqual(pendingFilter, urlSearch);\n\n const trySettleFromServer = (): boolean => {\n if (store.getState().status !== \"loading\") return false;\n if (!collectionSearchEqual(dataSearch, urlSearch)) return false;\n\n const incomingParams = new URLSearchParams(urlSearch);\n if (!store.matchesParams(incomingParams)) {\n store.syncFromParams(incomingParams);\n }\n\n store.settle();\n return true;\n };\n\n const clearPendingIfSettled = (): boolean => {\n if (!trySettleFromServer()) return false;\n pendingFilter = null;\n pendingUrls.clear();\n return true;\n };\n\n if (clearPendingIfSettled()) return;\n\n const normalizedUrlSearch = normalizeCollectionSearch(urlSearch);\n const normalizedPrevUrlSearch = normalizeCollectionSearch(prevUrlSearch);\n const urlSearchChanged = normalizedUrlSearch !== normalizedPrevUrlSearch;\n prevUrlSearch = urlSearch;\n\n const reDispatchToPendingTarget = () => {\n if (pendingFilter == null) return;\n callbacks.emitChange(pendingFilter ? `?${pendingFilter}` : \"\");\n };\n\n const routerSettledOnIntermediateWithStaleTarget =\n pendingFilter != null &&\n pendingSearchEquals(urlSearch) &&\n !pendingTargetMatchesUrl &&\n collectionSearchEqual(dataSearch, urlSearch);\n\n const serverConfirmedIntermediateWhileTargeting =\n pendingFilter != null &&\n pendingSearchEquals(dataSearch) &&\n !collectionSearchEqual(dataSearch, pendingFilter) &&\n !collectionSearchEqual(dataSearch, urlSearch);\n\n if (pendingTargetMatchesUrl) {\n if (trySettleFromServer()) {\n pendingFilter = null;\n pendingUrls.clear();\n } else if (\n store.getState().status === \"loading\" &&\n serverConfirmedIntermediateWhileTargeting\n ) {\n reDispatchToPendingTarget();\n }\n return;\n }\n\n if (pendingFilter != null) {\n if (!urlSearchChanged) {\n if (clearPendingIfSettled()) return;\n if (routerSettledOnIntermediateWithStaleTarget) {\n reDispatchToPendingTarget();\n }\n return;\n }\n\n if (pendingSearchEquals(urlSearch)) {\n if (clearPendingIfSettled()) return;\n if (routerSettledOnIntermediateWithStaleTarget) {\n reDispatchToPendingTarget();\n }\n return;\n }\n\n pendingFilter = null;\n pendingUrls.clear();\n }\n\n const incomingParams = new URLSearchParams(urlSearch);\n\n if (store.matchesParams(incomingParams)) {\n trySettleFromServer();\n return;\n }\n\n store.syncFromParams(incomingParams);\n trySettleFromServer();\n },\n };\n}\n"],"mappings":";;;;;;;;;;AA2CA,SAAgB,2BACd,WACA,uBAAuB,IACD;CACtB,IAAI,gBAA+B;CACnC,IAAI,8BAAc,IAAI,KAAa;CACnC,IAAI,gBAAgB;AAEpB,QAAO;EACL,qBAAqB;GACnB,MAAM,QAAQ,UAAU,UAAU;GAKlC,MAAM,eAJS,sBACb,IAAI,gBAAgB,UAAU,eAAe,CAAC,EAC9C,MAAM,UAAU,CAES,CAAC,UAAU;AACtC,mBAAgB;AAChB,eAAY,IAAI,aAAa;AAC7B,aAAU,WAAW,eAAe,IAAI,iBAAiB,GAAG;;EAG9D,MAAM,kBAA0B;AAC9B,mBAAgB;AAChB,iCAAc,IAAI,KAAK;AACvB,mBAAgB;;EA2BlB,UAAU,WAAmB,YAAoB;GAC/C,MAAM,QAAQ,UAAU,UAAU;GAElC,MAAM,uBAAuB,WAC3B,CAAC,GAAG,YAAY,CAAC,MAAM,YAAY,sBAAsB,SAAS,OAAO,CAAC;GAE5E,MAAM,0BACJ,iBAAiB,QAAQ,sBAAsB,eAAe,UAAU;GAE1E,MAAM,4BAAqC;AACzC,QAAI,MAAM,UAAU,CAAC,WAAW,UAAW,QAAO;AAClD,QAAI,CAAC,sBAAsB,YAAY,UAAU,CAAE,QAAO;IAE1D,MAAM,iBAAiB,IAAI,gBAAgB,UAAU;AACrD,QAAI,CAAC,MAAM,cAAc,eAAe,CACtC,OAAM,eAAe,eAAe;AAGtC,UAAM,QAAQ;AACd,WAAO;;GAGT,MAAM,8BAAuC;AAC3C,QAAI,CAAC,qBAAqB,CAAE,QAAO;AACnC,oBAAgB;AAChB,gBAAY,OAAO;AACnB,WAAO;;AAGT,OAAI,uBAAuB,CAAE;GAI7B,MAAM,mBAFsB,0BAA0B,UAEV,KADZ,0BAA0B,cACc;AACxE,mBAAgB;GAEhB,MAAM,kCAAkC;AACtC,QAAI,iBAAiB,KAAM;AAC3B,cAAU,WAAW,gBAAgB,IAAI,kBAAkB,GAAG;;GAGhE,MAAM,6CACJ,iBAAiB,QACjB,oBAAoB,UAAU,IAC9B,CAAC,2BACD,sBAAsB,YAAY,UAAU;GAE9C,MAAM,4CACJ,iBAAiB,QACjB,oBAAoB,WAAW,IAC/B,CAAC,sBAAsB,YAAY,cAAc,IACjD,CAAC,sBAAsB,YAAY,UAAU;AAE/C,OAAI,yBAAyB;AAC3B,QAAI,qBAAqB,EAAE;AACzB,qBAAgB;AAChB,iBAAY,OAAO;eAEnB,MAAM,UAAU,CAAC,WAAW,aAC5B,0CAEA,4BAA2B;AAE7B;;AAGF,OAAI,iBAAiB,MAAM;AACzB,QAAI,CAAC,kBAAkB;AACrB,SAAI,uBAAuB,CAAE;AAC7B,SAAI,2CACF,4BAA2B;AAE7B;;AAGF,QAAI,oBAAoB,UAAU,EAAE;AAClC,SAAI,uBAAuB,CAAE;AAC7B,SAAI,2CACF,4BAA2B;AAE7B;;AAGF,oBAAgB;AAChB,gBAAY,OAAO;;GAGrB,MAAM,iBAAiB,IAAI,gBAAgB,UAAU;AAErD,OAAI,MAAM,cAAc,eAAe,EAAE;AACvC,yBAAqB;AACrB;;AAGF,SAAM,eAAe,eAAe;AACpC,wBAAqB;;EAExB"}
|
|
1
|
+
{"version":3,"file":"reconciler.mjs","names":[],"sources":["../../../../src/core/collection/reconciler.ts"],"sourcesContent":["import type { CollectionStore } from \"./collection\";\nimport { collectionSearchEqual, mergeCollectionParams, normalizeCollectionSearch } from \"./url\";\n\n/**\n * Functions that connect a collection reconciler to your router and your collection store.\n */\nexport type ReconcilerCallbacks = {\n /** Returns the collection store. */\n getStore: () => CollectionStore;\n /** Returns the current URL search string from your router. */\n readUrlSearch: () => string;\n /** Navigates your router to the search string. The string starts with `?`, or is empty when no params remain. */\n emitChange: (searchString: string) => void;\n};\n\n/**\n * Keeps the URL, your loader data, and a collection store in sync while the customer changes filters and sort.\n */\nexport type CollectionReconciler = {\n /**\n * Brings the store in line with the URL. Call the method each time the URL search or the `dataSearch` string changes.\n *\n * The method settles the store when your loader data matches the URL. During rapid filter changes, the method skips intermediate URLs. A URL that the reconciler didn't request counts as external navigation, such as back or forward.\n */\n reconcile(urlSearch: string, dataSearch: string): void;\n /**\n * Builds the next search string from the store's filters and sort, and passes the string to `emitChange`. Register the method as the store's browse change callback.\n *\n * The new search string keeps every URL param that the store doesn't manage and drops the `before` and `after` pagination cursors.\n */\n handleBrowseChange(): void;\n /**\n * Clears pending navigation. Call the method when you replace the store.\n * @param newPrevUrlSearch - Current URL search string, which the next `reconcile()` call compares against.\n */\n reset(newPrevUrlSearch: string): void;\n};\n\n/**\n * Creates a reconciler that keeps the URL, your loader data, and a collection store in sync while the customer changes filters and sort.\n *\n * Use a reconciler to connect a collection store to a router that has no Hydrogen binding. In React, use `CollectionProvider`.\n *\n * @param callbacks The functions that return the store and the URL search string and navigate your router.\n * @param initialPrevUrlSearch The current URL search string, which the first `reconcile()` call compares against. Defaults to an empty string.\n * @returns A reconciler. Call `reconcile()` on URL changes, and register `handleBrowseChange()` as the store's browse change callback.\n *\n * @publicDocs\n */\nexport function createCollectionReconciler(\n callbacks: ReconcilerCallbacks,\n initialPrevUrlSearch = \"\",\n): CollectionReconciler {\n let pendingFilter: string | null = null;\n let pendingUrls = new Set<string>();\n let prevUrlSearch = initialPrevUrlSearch;\n\n return {\n handleBrowseChange() {\n const store = callbacks.getStore();\n const merged = mergeCollectionParams(\n new URLSearchParams(callbacks.readUrlSearch()),\n store.getState(),\n );\n const searchString = merged.toString();\n pendingFilter = searchString;\n pendingUrls.add(searchString);\n callbacks.emitChange(searchString ? `?${searchString}` : \"\");\n },\n\n reset(newPrevUrlSearch: string) {\n pendingFilter = null;\n pendingUrls = new Set();\n prevUrlSearch = newPrevUrlSearch;\n },\n\n // State machine — four states:\n //\n // ┌─────────────────────────────────────────────────────────────────────┐\n // │ State A: No pending navigation (pendingFilter === null) │\n // │ • URL matches store → try settle if server data caught up │\n // │ • URL differs from store → syncFromParams, then try settle │\n // ├─────────────────────────────────────────────────────────────────────┤\n // │ State B: Pending — URL matches target (pendingTargetMatchesUrl) │\n // │ • Server data matches URL → settle, clear pending state │\n // │ • Server data stale (intermediate) → re-dispatch to target │\n // │ • Server data not yet arrived → wait (keep pending set) │\n // ├─────────────────────────────────────────────────────────────────────┤\n // │ State C: Pending — URL is intermediate (in pendingUrls) │\n // │ • Ignore URL change; if router settled here with server data │\n // │ confirming the intermediate, re-dispatch to the real target │\n // ├─────────────────────────────────────────────────────────────────────┤\n // │ State D: Pending — URL is unknown (not in pendingUrls) │\n // │ • External navigation (back/forward, <Link>) │\n // │ • Abandon pending state, fall through to State A │\n // └─────────────────────────────────────────────────────────────────────┘\n //\n // Rapid toggles (A on → B on → A off) produce multiple in-flight navigations.\n // pendingFilter tracks the latest target; pendingUrls tracks every URL\n // dispatched in the chain so intermediates can be distinguished from external navs.\n reconcile(urlSearch: string, dataSearch: string) {\n const store = callbacks.getStore();\n\n const pendingSearchEquals = (search: string): boolean =>\n [...pendingUrls].some((pending) => collectionSearchEqual(pending, search));\n\n const pendingTargetMatchesUrl =\n pendingFilter != null && collectionSearchEqual(pendingFilter, urlSearch);\n\n const trySettleFromServer = (): boolean => {\n if (store.getState().status !== \"loading\") return false;\n if (!collectionSearchEqual(dataSearch, urlSearch)) return false;\n\n const incomingParams = new URLSearchParams(urlSearch);\n if (!store.matchesParams(incomingParams)) {\n store.syncFromParams(incomingParams);\n }\n\n store.settle();\n return true;\n };\n\n const clearPendingIfSettled = (): boolean => {\n if (!trySettleFromServer()) return false;\n pendingFilter = null;\n pendingUrls.clear();\n return true;\n };\n\n if (clearPendingIfSettled()) return;\n\n const normalizedUrlSearch = normalizeCollectionSearch(urlSearch);\n const normalizedPrevUrlSearch = normalizeCollectionSearch(prevUrlSearch);\n const urlSearchChanged = normalizedUrlSearch !== normalizedPrevUrlSearch;\n prevUrlSearch = urlSearch;\n\n const reDispatchToPendingTarget = () => {\n if (pendingFilter == null) return;\n callbacks.emitChange(pendingFilter ? `?${pendingFilter}` : \"\");\n };\n\n const routerSettledOnIntermediateWithStaleTarget =\n pendingFilter != null &&\n pendingSearchEquals(urlSearch) &&\n !pendingTargetMatchesUrl &&\n collectionSearchEqual(dataSearch, urlSearch);\n\n const serverConfirmedIntermediateWhileTargeting =\n pendingFilter != null &&\n pendingSearchEquals(dataSearch) &&\n !collectionSearchEqual(dataSearch, pendingFilter) &&\n !collectionSearchEqual(dataSearch, urlSearch);\n\n if (pendingTargetMatchesUrl) {\n if (trySettleFromServer()) {\n pendingFilter = null;\n pendingUrls.clear();\n } else if (\n store.getState().status === \"loading\" &&\n serverConfirmedIntermediateWhileTargeting\n ) {\n reDispatchToPendingTarget();\n }\n return;\n }\n\n if (pendingFilter != null) {\n if (!urlSearchChanged) {\n if (clearPendingIfSettled()) return;\n if (routerSettledOnIntermediateWithStaleTarget) {\n reDispatchToPendingTarget();\n }\n return;\n }\n\n if (pendingSearchEquals(urlSearch)) {\n if (clearPendingIfSettled()) return;\n if (routerSettledOnIntermediateWithStaleTarget) {\n reDispatchToPendingTarget();\n }\n return;\n }\n\n pendingFilter = null;\n pendingUrls.clear();\n }\n\n const incomingParams = new URLSearchParams(urlSearch);\n\n if (store.matchesParams(incomingParams)) {\n trySettleFromServer();\n return;\n }\n\n store.syncFromParams(incomingParams);\n trySettleFromServer();\n },\n };\n}\n"],"mappings":";;;;;;;;;;;;;;AAiDA,SAAgB,2BACd,WACA,uBAAuB,IACD;CACtB,IAAI,gBAA+B;CACnC,IAAI,8BAAc,IAAI,KAAa;CACnC,IAAI,gBAAgB;AAEpB,QAAO;EACL,qBAAqB;GACnB,MAAM,QAAQ,UAAU,UAAU;GAKlC,MAAM,eAJS,sBACb,IAAI,gBAAgB,UAAU,eAAe,CAAC,EAC9C,MAAM,UAAU,CAES,CAAC,UAAU;AACtC,mBAAgB;AAChB,eAAY,IAAI,aAAa;AAC7B,aAAU,WAAW,eAAe,IAAI,iBAAiB,GAAG;;EAG9D,MAAM,kBAA0B;AAC9B,mBAAgB;AAChB,iCAAc,IAAI,KAAK;AACvB,mBAAgB;;EA2BlB,UAAU,WAAmB,YAAoB;GAC/C,MAAM,QAAQ,UAAU,UAAU;GAElC,MAAM,uBAAuB,WAC3B,CAAC,GAAG,YAAY,CAAC,MAAM,YAAY,sBAAsB,SAAS,OAAO,CAAC;GAE5E,MAAM,0BACJ,iBAAiB,QAAQ,sBAAsB,eAAe,UAAU;GAE1E,MAAM,4BAAqC;AACzC,QAAI,MAAM,UAAU,CAAC,WAAW,UAAW,QAAO;AAClD,QAAI,CAAC,sBAAsB,YAAY,UAAU,CAAE,QAAO;IAE1D,MAAM,iBAAiB,IAAI,gBAAgB,UAAU;AACrD,QAAI,CAAC,MAAM,cAAc,eAAe,CACtC,OAAM,eAAe,eAAe;AAGtC,UAAM,QAAQ;AACd,WAAO;;GAGT,MAAM,8BAAuC;AAC3C,QAAI,CAAC,qBAAqB,CAAE,QAAO;AACnC,oBAAgB;AAChB,gBAAY,OAAO;AACnB,WAAO;;AAGT,OAAI,uBAAuB,CAAE;GAI7B,MAAM,mBAFsB,0BAA0B,UAEV,KADZ,0BAA0B,cACc;AACxE,mBAAgB;GAEhB,MAAM,kCAAkC;AACtC,QAAI,iBAAiB,KAAM;AAC3B,cAAU,WAAW,gBAAgB,IAAI,kBAAkB,GAAG;;GAGhE,MAAM,6CACJ,iBAAiB,QACjB,oBAAoB,UAAU,IAC9B,CAAC,2BACD,sBAAsB,YAAY,UAAU;GAE9C,MAAM,4CACJ,iBAAiB,QACjB,oBAAoB,WAAW,IAC/B,CAAC,sBAAsB,YAAY,cAAc,IACjD,CAAC,sBAAsB,YAAY,UAAU;AAE/C,OAAI,yBAAyB;AAC3B,QAAI,qBAAqB,EAAE;AACzB,qBAAgB;AAChB,iBAAY,OAAO;eAEnB,MAAM,UAAU,CAAC,WAAW,aAC5B,0CAEA,4BAA2B;AAE7B;;AAGF,OAAI,iBAAiB,MAAM;AACzB,QAAI,CAAC,kBAAkB;AACrB,SAAI,uBAAuB,CAAE;AAC7B,SAAI,2CACF,4BAA2B;AAE7B;;AAGF,QAAI,oBAAoB,UAAU,EAAE;AAClC,SAAI,uBAAuB,CAAE;AAC7B,SAAI,2CACF,4BAA2B;AAE7B;;AAGF,oBAAgB;AAChB,gBAAY,OAAO;;GAGrB,MAAM,iBAAiB,IAAI,gBAAgB,UAAU;AAErD,OAAI,MAAM,cAAc,eAAe,EAAE;AACvC,yBAAqB;AACrB;;AAGF,SAAM,eAAe,eAAe;AACpC,wBAAqB;;EAExB"}
|
|
@@ -2,68 +2,73 @@ import { ProductCollectionSortKeys } from "../../graphql/generated/storefront-ap
|
|
|
2
2
|
import { ProductFilter } from "../../vendor/standard-events.mjs";
|
|
3
3
|
|
|
4
4
|
//#region src/core/collection/state.d.ts
|
|
5
|
-
/** How
|
|
5
|
+
/** How the storefront displays a filter's values. */
|
|
6
6
|
type FilterPresentation = "IMAGE" | "SWATCH" | "TEXT";
|
|
7
|
-
/** The input
|
|
7
|
+
/** The kind of input a filter takes, such as a boolean toggle, a multi-select list, or a price range. */
|
|
8
8
|
type FilterType = "BOOLEAN" | "LIST" | "PRICE_RANGE" | (string & {});
|
|
9
|
+
/** The ID, label, product count, and filter input that every filter value has. */
|
|
9
10
|
interface BaseAvailableFilterValue {
|
|
11
|
+
/** ID of the filter value. */
|
|
10
12
|
id: string;
|
|
11
|
-
/**
|
|
13
|
+
/** Label to display, such as "Red" or "Nike". */
|
|
12
14
|
label: string;
|
|
13
|
-
/** Number of products
|
|
15
|
+
/** Number of products in the current results that match the value. */
|
|
14
16
|
count: number;
|
|
15
|
-
/** JSON
|
|
17
|
+
/** Filter input as a JSON string. Pass the string to `toggleFilterInput()` or `isFilterInputActive`. */
|
|
16
18
|
input: string;
|
|
17
19
|
}
|
|
20
|
+
/** Copies a field from your query's value type when your query selects the field. Otherwise, the type adds no fields. */
|
|
18
21
|
type PickIfPresent<TValue, TKey extends PropertyKey> = TKey extends keyof TValue ? Pick<TValue, TKey> : {};
|
|
19
22
|
/**
|
|
20
|
-
* A
|
|
23
|
+
* A value that a customer can select within an available filter.
|
|
21
24
|
*
|
|
22
|
-
* Pass your Storefront API query value type as `TValue` to
|
|
23
|
-
* visual fields such as `swatch`.
|
|
25
|
+
* Pass your Storefront API query's value type as `TValue` to include the `swatch` field when your query selects it.
|
|
24
26
|
*/
|
|
25
27
|
type AvailableFilterValue<TValue extends BaseAvailableFilterValue = BaseAvailableFilterValue> = BaseAvailableFilterValue & PickIfPresent<TValue, "swatch">;
|
|
26
28
|
/**
|
|
27
|
-
* A filter
|
|
28
|
-
*
|
|
29
|
+
* A filter that the Storefront API returns for the current collection or search results.
|
|
30
|
+
* Render filter controls, such as checkboxes, swatches, and sliders, from the filter's values.
|
|
31
|
+
*
|
|
32
|
+
* @publicDocs
|
|
29
33
|
*/
|
|
30
34
|
interface AvailableFilter<TValue extends BaseAvailableFilterValue = BaseAvailableFilterValue> {
|
|
35
|
+
/** ID of the filter. */
|
|
31
36
|
id: string;
|
|
32
|
-
/**
|
|
37
|
+
/** Name to display, such as "Color", "Size", or "Price". */
|
|
33
38
|
label: string;
|
|
34
|
-
/**
|
|
39
|
+
/** Kind of input, such as a boolean toggle, a multi-select list, or a price range. */
|
|
35
40
|
type: FilterType;
|
|
36
41
|
/**
|
|
37
|
-
*
|
|
38
|
-
* filters
|
|
42
|
+
* How the storefront displays the filter's values. The Storefront API
|
|
43
|
+
* returns a value only for `LIST` filters and `null` for other filters.
|
|
39
44
|
*/
|
|
40
45
|
presentation?: FilterPresentation | null;
|
|
46
|
+
/** Values that a customer can select, each with a label, a product count, and a filter input. */
|
|
41
47
|
values: AvailableFilterValue<TValue>[];
|
|
42
48
|
}
|
|
43
49
|
/**
|
|
44
|
-
*
|
|
45
|
-
* wants to see (filters, sort), not what the server returned.
|
|
50
|
+
* The customer's filter and sort choices and the loading status.
|
|
46
51
|
*
|
|
47
|
-
*
|
|
48
|
-
* in the framework's loader data, not in the store.
|
|
52
|
+
* Products, product counts, available filters, and the collection ID come from your loader data.
|
|
49
53
|
*/
|
|
50
54
|
interface CollectionState {
|
|
51
|
-
/**
|
|
55
|
+
/** Collection handle, such as `shoes`. */
|
|
52
56
|
handle: string;
|
|
53
|
-
/** Active product filters,
|
|
57
|
+
/** Active product filters, matching the `filter.*` URL params. */
|
|
54
58
|
filters: ProductFilter[];
|
|
55
|
-
/** Storefront API sort key
|
|
59
|
+
/** Storefront API sort key, or `undefined` for the collection's default sort. */
|
|
56
60
|
sortKey: ProductCollectionSortKeys | undefined;
|
|
57
|
-
/**
|
|
61
|
+
/** `true` for a descending sort. The URL's sort value then ends in `-descending`. */
|
|
58
62
|
reverse: boolean;
|
|
59
|
-
/** `
|
|
63
|
+
/** Reads `loading` from a filter or sort change until the store settles, and `idle` when the results match the filters and sort. Show a loading state while the value is `loading`. */
|
|
60
64
|
status: "idle" | "loading";
|
|
61
65
|
}
|
|
62
66
|
/**
|
|
63
|
-
* Creates
|
|
64
|
-
* All browse fields start at their zero values.
|
|
67
|
+
* Creates collection state with no filters and the default sort for a collection handle.
|
|
65
68
|
*
|
|
66
|
-
* @param handle -
|
|
69
|
+
* @param handle - Collection handle, such as `shoes`.
|
|
70
|
+
* @returns A state with no filters, the collection's default sort, an ascending direction, and an idle status.
|
|
71
|
+
* @publicDocs
|
|
67
72
|
*/
|
|
68
73
|
declare function createInitialCollectionState(handle: string): CollectionState;
|
|
69
74
|
//#endregion
|
|
@@ -1,9 +1,10 @@
|
|
|
1
1
|
//#region src/core/collection/state.ts
|
|
2
2
|
/**
|
|
3
|
-
* Creates
|
|
4
|
-
* All browse fields start at their zero values.
|
|
3
|
+
* Creates collection state with no filters and the default sort for a collection handle.
|
|
5
4
|
*
|
|
6
|
-
* @param handle -
|
|
5
|
+
* @param handle - Collection handle, such as `shoes`.
|
|
6
|
+
* @returns A state with no filters, the collection's default sort, an ascending direction, and an idle status.
|
|
7
|
+
* @publicDocs
|
|
7
8
|
*/
|
|
8
9
|
function createInitialCollectionState(handle) {
|
|
9
10
|
return {
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"state.mjs","names":[],"sources":["../../../../src/core/collection/state.ts"],"sourcesContent":["import type { ProductFilter } from \"../../../vendor/standard-events\";\nimport type { ProductCollectionSortKeys } from \"../../graphql/generated/storefront-api-types\";\n\nexport type { ProductCollectionSortKeys, ProductFilter };\n\n/** How
|
|
1
|
+
{"version":3,"file":"state.mjs","names":[],"sources":["../../../../src/core/collection/state.ts"],"sourcesContent":["import type { ProductFilter } from \"../../../vendor/standard-events\";\nimport type { ProductCollectionSortKeys } from \"../../graphql/generated/storefront-api-types\";\n\nexport type { ProductCollectionSortKeys, ProductFilter };\n\n/** How the storefront displays a filter's values. */\nexport type FilterPresentation = \"IMAGE\" | \"SWATCH\" | \"TEXT\";\n\n/** The kind of input a filter takes, such as a boolean toggle, a multi-select list, or a price range. */\nexport type FilterType = \"BOOLEAN\" | \"LIST\" | \"PRICE_RANGE\" | (string & {});\n\n/** The ID, label, product count, and filter input that every filter value has. */\ninterface BaseAvailableFilterValue {\n /** ID of the filter value. */\n id: string;\n /** Label to display, such as \"Red\" or \"Nike\". */\n label: string;\n /** Number of products in the current results that match the value. */\n count: number;\n /** Filter input as a JSON string. Pass the string to `toggleFilterInput()` or `isFilterInputActive`. */\n input: string;\n}\n\n/** Copies a field from your query's value type when your query selects the field. Otherwise, the type adds no fields. */\ntype PickIfPresent<TValue, TKey extends PropertyKey> = TKey extends keyof TValue\n ? Pick<TValue, TKey>\n : {};\n\n/**\n * A value that a customer can select within an available filter.\n *\n * Pass your Storefront API query's value type as `TValue` to include the `swatch` field when your query selects it.\n */\nexport type AvailableFilterValue<\n TValue extends BaseAvailableFilterValue = BaseAvailableFilterValue,\n> = BaseAvailableFilterValue & PickIfPresent<TValue, \"swatch\">;\n\n/**\n * A filter that the Storefront API returns for the current collection or search results.\n * Render filter controls, such as checkboxes, swatches, and sliders, from the filter's values.\n *\n * @publicDocs\n */\nexport interface AvailableFilter<\n TValue extends BaseAvailableFilterValue = BaseAvailableFilterValue,\n> {\n /** ID of the filter. */\n id: string;\n /** Name to display, such as \"Color\", \"Size\", or \"Price\". */\n label: string;\n /** Kind of input, such as a boolean toggle, a multi-select list, or a price range. */\n type: FilterType;\n /**\n * How the storefront displays the filter's values. The Storefront API\n * returns a value only for `LIST` filters and `null` for other filters.\n */\n presentation?: FilterPresentation | null;\n /** Values that a customer can select, each with a label, a product count, and a filter input. */\n values: AvailableFilterValue<TValue>[];\n}\n\n/**\n * The customer's filter and sort choices and the loading status.\n *\n * Products, product counts, available filters, and the collection ID come from your loader data.\n */\nexport interface CollectionState {\n /** Collection handle, such as `shoes`. */\n handle: string;\n\n /** Active product filters, matching the `filter.*` URL params. */\n filters: ProductFilter[];\n /** Storefront API sort key, or `undefined` for the collection's default sort. */\n sortKey: ProductCollectionSortKeys | undefined;\n /** `true` for a descending sort. The URL's sort value then ends in `-descending`. */\n reverse: boolean;\n /** Reads `loading` from a filter or sort change until the store settles, and `idle` when the results match the filters and sort. Show a loading state while the value is `loading`. */\n status: \"idle\" | \"loading\";\n}\n\n/**\n * Creates collection state with no filters and the default sort for a collection handle.\n *\n * @param handle - Collection handle, such as `shoes`.\n * @returns A state with no filters, the collection's default sort, an ascending direction, and an idle status.\n * @publicDocs\n */\nexport function createInitialCollectionState(handle: string): CollectionState {\n return {\n handle,\n filters: [],\n sortKey: undefined,\n reverse: false,\n status: \"idle\",\n };\n}\n"],"mappings":";;;;;;;;AAuFA,SAAgB,6BAA6B,QAAiC;AAC5E,QAAO;EACL;EACA,SAAS,EAAE;EACX,SAAS;EACT,SAAS;EACT,QAAQ;EACT"}
|