@reblu/site-blocks 0.4.4 → 0.5.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.d.cts CHANGED
@@ -1,5 +1,5 @@
1
1
  import * as react_jsx_runtime from 'react/jsx-runtime';
2
- import { PageDetail, PageListItem, ShopProductListItem } from '@reblu/site-contracts';
2
+ import { PageDetail, PageListItem, ShopProductListItem, ShopFacets } from '@reblu/site-contracts';
3
3
 
4
4
  declare function readingTime(content: string | null | undefined): number | null;
5
5
 
@@ -277,4 +277,110 @@ interface ProductCardProps {
277
277
  }
278
278
  declare function ProductCard({ product, href, currencyCode, locale, className }: ProductCardProps): react_jsx_runtime.JSX.Element;
279
279
 
280
- export { ALLOWED_YOUTUBE_HOSTS, MarkdownRenderer, type MarkdownRendererProps, ProductCard, type ProductCardProps, ProfileCard, type ProfileCardProps, SafeHtml, type SafeHtmlProps, type ShareUrls, Specialty, type SpecialtyProps, Treatment, type TreatmentProps, buildShareUrls, formatPrice, isAllowedYouTubeHost, markdownToSafeHtml, readingTime, sanitizeHtml };
280
+ /**
281
+ * `ShopFilters` — plug-and-play SITE filter chrome over `GET /api/site/shop/facets`
282
+ * (REB-809). Which axes appear, their order and labels come entirely from
283
+ * `facets` — nexus config, not this component — so no SITE hand-builds shop
284
+ * filter UI again (the gap this issue closes: dermamedicin's SITE hardcoded
285
+ * `BRAND_AXIS = 'brand'` while the tenant's real axis key is `marca`).
286
+ *
287
+ * RSC-safe by construction (REB-647 lesson): every option always renders a real
288
+ * `<a href>` built by the `hrefs` prop, so importing and rendering this from an
289
+ * actual Server Component — no `onChange`, navigation only — never crashes and
290
+ * never attaches an event handler. `onChange` is strictly additive: when
291
+ * provided, a click is intercepted (`preventDefault` + call `onChange`) instead
292
+ * of navigating. Attaching a function prop to a host element only works when
293
+ * the call site itself is (or is inside) a Client Component — pass `onChange`
294
+ * only from a `'use client'` boundary; from a Server Component, omit it and let
295
+ * `hrefs` alone drive navigation (a real page load / your router's own Link).
296
+ *
297
+ * ── Single-select everywhere, on purpose ──────────────────────────────────
298
+ * `facets.taxonomies[].selectionMode` (REB-809/REB-801) reflects how many terms
299
+ * a PRODUCT may carry on that axis — it is NOT how many the storefront filter
300
+ * may select at once. `GET /api/site/shop/products`'s `taxonomy[<key>]` query
301
+ * param accepts exactly one term slug per axis today (`parseListParams` in
302
+ * base-api collapses any repeat), so every taxonomy axis renders single-select
303
+ * filter UI here regardless of `selectionMode` — clicking the active term's own
304
+ * link clears the axis, clicking another replaces it. Wiring `selectionMode`
305
+ * into a true multi-value taxonomy filter needs the query contract to grow an
306
+ * array form first; that is out of this component's scope.
307
+ *
308
+ * Attribute facets (`facets.attributes`) are genuinely multi-select already —
309
+ * `attr[<key>]` repeats to OR-match several values — so those render as a
310
+ * checkbox-shaped list where more than one option may be active at once.
311
+ *
312
+ * Tokens only: no brand aesthetic, every color/spacing class is a design
313
+ * token so the same component renders correctly for every tenant theme.
314
+ */
315
+ /** One price band. `min`/`max` are integer cents; either may be omitted for an open end. */
316
+ interface ShopFilterPriceBand {
317
+ min?: number;
318
+ max?: number;
319
+ /** Overrides the auto-generated `formatPrice`-based label. */
320
+ label?: string;
321
+ }
322
+ /** Sensible EUR defaults — override via the `priceBands` prop for another currency/locale. */
323
+ declare const DEFAULT_SHOP_PRICE_BANDS: ShopFilterPriceBand[];
324
+ /** The filter selection currently reflected in the URL (or component state). */
325
+ interface ShopFiltersActive {
326
+ /** axis key → active term slug. */
327
+ taxonomy?: Record<string, string>;
328
+ /** field key → active value(s). */
329
+ attr?: Record<string, string | string[]>;
330
+ priceMin?: number;
331
+ priceMax?: number;
332
+ onSale?: boolean;
333
+ }
334
+ /**
335
+ * Href generators — the RSC-safe navigation seam. A consumer typically supplies
336
+ * these from `@reblu/site-next`'s shop URL helpers (`parseShopSearchParams` /
337
+ * `serializeShopParams`), which preserve every other current param and reset
338
+ * pagination on change.
339
+ */
340
+ interface ShopFiltersHrefs {
341
+ /** `value` undefined clears the axis (renders the "all" option's href). */
342
+ taxonomy: (axisKey: string, value: string | undefined) => string;
343
+ /** Href toggling `value` in/out of `fieldKey`'s active value set. */
344
+ attr: (fieldKey: string, value: string) => string;
345
+ price: (min: number | undefined, max: number | undefined) => string;
346
+ onSale: (nextActive: boolean) => string;
347
+ clearAll: () => string;
348
+ }
349
+ interface ShopFiltersLabels {
350
+ /** The "show every term of this axis" option. */
351
+ allTerms: string;
352
+ clearAll: string;
353
+ onSale: string;
354
+ price: string;
355
+ /** Open-ended "up to X" band, e.g. `(formatted) => \`Under ${formatted}\`` (the default). */
356
+ priceUnder: (formattedMax: string) => string;
357
+ /** Open-ended "X or more" band, e.g. `(formatted) => \`${formatted}+\`` (the default). */
358
+ priceOver: (formattedMin: string) => string;
359
+ /** Bounded band, e.g. `(min, max) => \`${min} – ${max}\`` (the default). */
360
+ priceRange: (formattedMin: string, formattedMax: string) => string;
361
+ }
362
+ interface ShopFiltersProps {
363
+ facets: ShopFacets;
364
+ hrefs: ShopFiltersHrefs;
365
+ active?: ShopFiltersActive;
366
+ /** Client-only: intercepts navigation with a callback instead. See the module doc. */
367
+ onChange?: (patch: Partial<ShopFiltersActive>) => void;
368
+ priceBands?: ShopFilterPriceBand[];
369
+ labels?: Partial<ShopFiltersLabels>;
370
+ /** Forwarded to `formatPrice` for price-band labels. Defaults match `ProductCard`'s (EUR/es-ES). */
371
+ currencyCode?: string;
372
+ locale?: string;
373
+ className?: string;
374
+ }
375
+ /**
376
+ * The exact "would `ShopFilters` render anything at all" predicate — exported
377
+ * so a consuming page can decide whether to render a wrapping `<aside>` (or
378
+ * similar landmark) around it WITHOUT hand-copying this logic (a copy that
379
+ * can silently drift from the component's own decision). True when `facets`
380
+ * has at least one populated dimension, OR `active` has a selection to clear
381
+ * (see R3-null-render-strands-active-filters above).
382
+ */
383
+ declare function shouldRenderShopFilters(facets: ShopFacets, active?: ShopFiltersActive): boolean;
384
+ declare function ShopFilters({ facets, hrefs, active, onChange, priceBands, labels, currencyCode, locale, className, }: ShopFiltersProps): react_jsx_runtime.JSX.Element | null;
385
+
386
+ export { ALLOWED_YOUTUBE_HOSTS, DEFAULT_SHOP_PRICE_BANDS, MarkdownRenderer, type MarkdownRendererProps, ProductCard, type ProductCardProps, ProfileCard, type ProfileCardProps, SafeHtml, type SafeHtmlProps, type ShareUrls, type ShopFilterPriceBand, ShopFilters, type ShopFiltersActive, type ShopFiltersHrefs, type ShopFiltersLabels, type ShopFiltersProps, Specialty, type SpecialtyProps, Treatment, type TreatmentProps, buildShareUrls, formatPrice, isAllowedYouTubeHost, markdownToSafeHtml, readingTime, sanitizeHtml, shouldRenderShopFilters };
package/dist/index.d.ts CHANGED
@@ -1,5 +1,5 @@
1
1
  import * as react_jsx_runtime from 'react/jsx-runtime';
2
- import { PageDetail, PageListItem, ShopProductListItem } from '@reblu/site-contracts';
2
+ import { PageDetail, PageListItem, ShopProductListItem, ShopFacets } from '@reblu/site-contracts';
3
3
 
4
4
  declare function readingTime(content: string | null | undefined): number | null;
5
5
 
@@ -277,4 +277,110 @@ interface ProductCardProps {
277
277
  }
278
278
  declare function ProductCard({ product, href, currencyCode, locale, className }: ProductCardProps): react_jsx_runtime.JSX.Element;
279
279
 
280
- export { ALLOWED_YOUTUBE_HOSTS, MarkdownRenderer, type MarkdownRendererProps, ProductCard, type ProductCardProps, ProfileCard, type ProfileCardProps, SafeHtml, type SafeHtmlProps, type ShareUrls, Specialty, type SpecialtyProps, Treatment, type TreatmentProps, buildShareUrls, formatPrice, isAllowedYouTubeHost, markdownToSafeHtml, readingTime, sanitizeHtml };
280
+ /**
281
+ * `ShopFilters` — plug-and-play SITE filter chrome over `GET /api/site/shop/facets`
282
+ * (REB-809). Which axes appear, their order and labels come entirely from
283
+ * `facets` — nexus config, not this component — so no SITE hand-builds shop
284
+ * filter UI again (the gap this issue closes: dermamedicin's SITE hardcoded
285
+ * `BRAND_AXIS = 'brand'` while the tenant's real axis key is `marca`).
286
+ *
287
+ * RSC-safe by construction (REB-647 lesson): every option always renders a real
288
+ * `<a href>` built by the `hrefs` prop, so importing and rendering this from an
289
+ * actual Server Component — no `onChange`, navigation only — never crashes and
290
+ * never attaches an event handler. `onChange` is strictly additive: when
291
+ * provided, a click is intercepted (`preventDefault` + call `onChange`) instead
292
+ * of navigating. Attaching a function prop to a host element only works when
293
+ * the call site itself is (or is inside) a Client Component — pass `onChange`
294
+ * only from a `'use client'` boundary; from a Server Component, omit it and let
295
+ * `hrefs` alone drive navigation (a real page load / your router's own Link).
296
+ *
297
+ * ── Single-select everywhere, on purpose ──────────────────────────────────
298
+ * `facets.taxonomies[].selectionMode` (REB-809/REB-801) reflects how many terms
299
+ * a PRODUCT may carry on that axis — it is NOT how many the storefront filter
300
+ * may select at once. `GET /api/site/shop/products`'s `taxonomy[<key>]` query
301
+ * param accepts exactly one term slug per axis today (`parseListParams` in
302
+ * base-api collapses any repeat), so every taxonomy axis renders single-select
303
+ * filter UI here regardless of `selectionMode` — clicking the active term's own
304
+ * link clears the axis, clicking another replaces it. Wiring `selectionMode`
305
+ * into a true multi-value taxonomy filter needs the query contract to grow an
306
+ * array form first; that is out of this component's scope.
307
+ *
308
+ * Attribute facets (`facets.attributes`) are genuinely multi-select already —
309
+ * `attr[<key>]` repeats to OR-match several values — so those render as a
310
+ * checkbox-shaped list where more than one option may be active at once.
311
+ *
312
+ * Tokens only: no brand aesthetic, every color/spacing class is a design
313
+ * token so the same component renders correctly for every tenant theme.
314
+ */
315
+ /** One price band. `min`/`max` are integer cents; either may be omitted for an open end. */
316
+ interface ShopFilterPriceBand {
317
+ min?: number;
318
+ max?: number;
319
+ /** Overrides the auto-generated `formatPrice`-based label. */
320
+ label?: string;
321
+ }
322
+ /** Sensible EUR defaults — override via the `priceBands` prop for another currency/locale. */
323
+ declare const DEFAULT_SHOP_PRICE_BANDS: ShopFilterPriceBand[];
324
+ /** The filter selection currently reflected in the URL (or component state). */
325
+ interface ShopFiltersActive {
326
+ /** axis key → active term slug. */
327
+ taxonomy?: Record<string, string>;
328
+ /** field key → active value(s). */
329
+ attr?: Record<string, string | string[]>;
330
+ priceMin?: number;
331
+ priceMax?: number;
332
+ onSale?: boolean;
333
+ }
334
+ /**
335
+ * Href generators — the RSC-safe navigation seam. A consumer typically supplies
336
+ * these from `@reblu/site-next`'s shop URL helpers (`parseShopSearchParams` /
337
+ * `serializeShopParams`), which preserve every other current param and reset
338
+ * pagination on change.
339
+ */
340
+ interface ShopFiltersHrefs {
341
+ /** `value` undefined clears the axis (renders the "all" option's href). */
342
+ taxonomy: (axisKey: string, value: string | undefined) => string;
343
+ /** Href toggling `value` in/out of `fieldKey`'s active value set. */
344
+ attr: (fieldKey: string, value: string) => string;
345
+ price: (min: number | undefined, max: number | undefined) => string;
346
+ onSale: (nextActive: boolean) => string;
347
+ clearAll: () => string;
348
+ }
349
+ interface ShopFiltersLabels {
350
+ /** The "show every term of this axis" option. */
351
+ allTerms: string;
352
+ clearAll: string;
353
+ onSale: string;
354
+ price: string;
355
+ /** Open-ended "up to X" band, e.g. `(formatted) => \`Under ${formatted}\`` (the default). */
356
+ priceUnder: (formattedMax: string) => string;
357
+ /** Open-ended "X or more" band, e.g. `(formatted) => \`${formatted}+\`` (the default). */
358
+ priceOver: (formattedMin: string) => string;
359
+ /** Bounded band, e.g. `(min, max) => \`${min} – ${max}\`` (the default). */
360
+ priceRange: (formattedMin: string, formattedMax: string) => string;
361
+ }
362
+ interface ShopFiltersProps {
363
+ facets: ShopFacets;
364
+ hrefs: ShopFiltersHrefs;
365
+ active?: ShopFiltersActive;
366
+ /** Client-only: intercepts navigation with a callback instead. See the module doc. */
367
+ onChange?: (patch: Partial<ShopFiltersActive>) => void;
368
+ priceBands?: ShopFilterPriceBand[];
369
+ labels?: Partial<ShopFiltersLabels>;
370
+ /** Forwarded to `formatPrice` for price-band labels. Defaults match `ProductCard`'s (EUR/es-ES). */
371
+ currencyCode?: string;
372
+ locale?: string;
373
+ className?: string;
374
+ }
375
+ /**
376
+ * The exact "would `ShopFilters` render anything at all" predicate — exported
377
+ * so a consuming page can decide whether to render a wrapping `<aside>` (or
378
+ * similar landmark) around it WITHOUT hand-copying this logic (a copy that
379
+ * can silently drift from the component's own decision). True when `facets`
380
+ * has at least one populated dimension, OR `active` has a selection to clear
381
+ * (see R3-null-render-strands-active-filters above).
382
+ */
383
+ declare function shouldRenderShopFilters(facets: ShopFacets, active?: ShopFiltersActive): boolean;
384
+ declare function ShopFilters({ facets, hrefs, active, onChange, priceBands, labels, currencyCode, locale, className, }: ShopFiltersProps): react_jsx_runtime.JSX.Element | null;
385
+
386
+ export { ALLOWED_YOUTUBE_HOSTS, DEFAULT_SHOP_PRICE_BANDS, MarkdownRenderer, type MarkdownRendererProps, ProductCard, type ProductCardProps, ProfileCard, type ProfileCardProps, SafeHtml, type SafeHtmlProps, type ShareUrls, type ShopFilterPriceBand, ShopFilters, type ShopFiltersActive, type ShopFiltersHrefs, type ShopFiltersLabels, type ShopFiltersProps, Specialty, type SpecialtyProps, Treatment, type TreatmentProps, buildShareUrls, formatPrice, isAllowedYouTubeHost, markdownToSafeHtml, readingTime, sanitizeHtml, shouldRenderShopFilters };
package/dist/index.js CHANGED
@@ -13401,12 +13401,201 @@ function ProductCard({ product, href, currencyCode = "EUR", locale, className })
13401
13401
  }
13402
13402
  );
13403
13403
  }
13404
+
13405
+ // src/components/shop-filters.tsx
13406
+ import { cn as cn7 } from "@reblu/site-ui/utils";
13407
+ import { jsx as jsx7, jsxs as jsxs6 } from "react/jsx-runtime";
13408
+ var DEFAULT_SHOP_PRICE_BANDS = [
13409
+ { max: 2500 },
13410
+ { min: 2500, max: 5e3 },
13411
+ { min: 5e3, max: 1e4 },
13412
+ { min: 1e4 }
13413
+ ];
13414
+ var DEFAULT_LABELS = {
13415
+ allTerms: "All",
13416
+ clearAll: "Clear filters",
13417
+ onSale: "On sale",
13418
+ price: "Price",
13419
+ priceUnder: (formattedMax) => `Under ${formattedMax}`,
13420
+ priceOver: (formattedMin) => `${formattedMin}+`,
13421
+ priceRange: (formattedMin, formattedMax) => `${formattedMin} \u2013 ${formattedMax}`
13422
+ };
13423
+ function toValueArray(value) {
13424
+ if (value === void 0) return [];
13425
+ return Array.isArray(value) ? value : [value];
13426
+ }
13427
+ function hasAnyActiveFilter(active) {
13428
+ return Object.keys(active.taxonomy ?? {}).length > 0 || Object.keys(active.attr ?? {}).length > 0 || hasActivePriceFilter(active) || Boolean(active.onSale);
13429
+ }
13430
+ function hasActivePriceFilter(active) {
13431
+ return active.priceMin != null || active.priceMax != null;
13432
+ }
13433
+ function shouldRenderShopFilters(facets, active = {}) {
13434
+ const hasFacetContent = facets.taxonomies.some((axis) => axis.terms.length > 0) || facets.attributes.some((attr) => attr.options.length > 0) || facets.price.max > facets.price.min || facets.onSale.count > 0;
13435
+ return hasFacetContent || hasAnyActiveFilter(active);
13436
+ }
13437
+ function priceBandLabel(band, t, currencyCode, locale) {
13438
+ if (band.label) return band.label;
13439
+ if (band.min === void 0 && band.max !== void 0) {
13440
+ return t.priceUnder(formatPrice(band.max, currencyCode, locale));
13441
+ }
13442
+ if (band.max === void 0 && band.min !== void 0) {
13443
+ return t.priceOver(formatPrice(band.min, currencyCode, locale));
13444
+ }
13445
+ if (band.min !== void 0 && band.max !== void 0) {
13446
+ return t.priceRange(formatPrice(band.min, currencyCode, locale), formatPrice(band.max, currencyCode, locale));
13447
+ }
13448
+ return "";
13449
+ }
13450
+ function OptionLink({ href, active, label, onActivate }) {
13451
+ return /* @__PURE__ */ jsx7(
13452
+ "a",
13453
+ {
13454
+ href,
13455
+ "aria-current": active ? "true" : void 0,
13456
+ onClick: onActivate ? (event) => {
13457
+ event.preventDefault();
13458
+ onActivate();
13459
+ } : void 0,
13460
+ className: cn7(
13461
+ "inline-flex items-center rounded-full border px-3 py-1 text-sm transition-colors focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring",
13462
+ active ? "border-transparent bg-primary text-primary-foreground" : "border-border text-muted-foreground hover:bg-muted hover:text-foreground"
13463
+ ),
13464
+ children: label
13465
+ }
13466
+ );
13467
+ }
13468
+ function FilterFieldset({ legend, children: children2 }) {
13469
+ return /* @__PURE__ */ jsxs6("fieldset", { className: "space-y-2", children: [
13470
+ /* @__PURE__ */ jsx7("legend", { className: "text-sm font-medium text-foreground", children: legend }),
13471
+ /* @__PURE__ */ jsx7("div", { className: "flex flex-wrap gap-2", children: children2 })
13472
+ ] });
13473
+ }
13474
+ function ShopFilters({
13475
+ facets,
13476
+ hrefs,
13477
+ active = {},
13478
+ onChange,
13479
+ priceBands = DEFAULT_SHOP_PRICE_BANDS,
13480
+ labels,
13481
+ currencyCode,
13482
+ locale,
13483
+ className
13484
+ }) {
13485
+ const t = { ...DEFAULT_LABELS, ...labels };
13486
+ const visibleTaxonomies = facets.taxonomies.filter((axis) => axis.terms.length > 0);
13487
+ const visibleAttributes = facets.attributes.filter((attr) => attr.options.length > 0);
13488
+ const hasPriceSpread = facets.price.max > facets.price.min;
13489
+ const showPriceGroup = hasPriceSpread || hasActivePriceFilter(active);
13490
+ const hasOnSale = facets.onSale.count > 0;
13491
+ if (!shouldRenderShopFilters(facets, active)) {
13492
+ return null;
13493
+ }
13494
+ return /* @__PURE__ */ jsxs6("div", { className: cn7("space-y-6", className), "data-slot": "shop-filters", children: [
13495
+ visibleTaxonomies.map((axis) => {
13496
+ const activeSlug = active.taxonomy?.[axis.key];
13497
+ return /* @__PURE__ */ jsxs6(FilterFieldset, { legend: axis.label, children: [
13498
+ /* @__PURE__ */ jsx7(
13499
+ OptionLink,
13500
+ {
13501
+ href: hrefs.taxonomy(axis.key, void 0),
13502
+ active: activeSlug === void 0,
13503
+ label: t.allTerms,
13504
+ onActivate: onChange ? () => {
13505
+ const next2 = { ...active.taxonomy };
13506
+ delete next2[axis.key];
13507
+ onChange({ taxonomy: next2 });
13508
+ } : void 0
13509
+ }
13510
+ ),
13511
+ axis.terms.map((term) => {
13512
+ const isActive = activeSlug === term.slug;
13513
+ return /* @__PURE__ */ jsx7(
13514
+ OptionLink,
13515
+ {
13516
+ href: hrefs.taxonomy(axis.key, isActive ? void 0 : term.slug),
13517
+ active: isActive,
13518
+ label: `${term.name} (${term.count})`,
13519
+ onActivate: onChange ? () => {
13520
+ const next2 = { ...active.taxonomy };
13521
+ if (isActive) delete next2[axis.key];
13522
+ else next2[axis.key] = term.slug;
13523
+ onChange({ taxonomy: next2 });
13524
+ } : void 0
13525
+ },
13526
+ term.slug
13527
+ );
13528
+ })
13529
+ ] }, axis.key);
13530
+ }),
13531
+ visibleAttributes.map((attr) => {
13532
+ const activeValues = toValueArray(active.attr?.[attr.key]);
13533
+ return /* @__PURE__ */ jsx7(FilterFieldset, { legend: attr.label, children: attr.options.map((opt) => {
13534
+ const isActive = activeValues.includes(opt.value);
13535
+ return /* @__PURE__ */ jsx7(
13536
+ OptionLink,
13537
+ {
13538
+ href: hrefs.attr(attr.key, opt.value),
13539
+ active: isActive,
13540
+ label: `${opt.label} (${opt.count})`,
13541
+ onActivate: onChange ? () => {
13542
+ const next2 = isActive ? activeValues.filter((v2) => v2 !== opt.value) : [...activeValues, opt.value];
13543
+ const attrNext = { ...active.attr };
13544
+ if (next2.length === 0) delete attrNext[attr.key];
13545
+ else attrNext[attr.key] = next2;
13546
+ onChange({ attr: attrNext });
13547
+ } : void 0
13548
+ },
13549
+ opt.value
13550
+ );
13551
+ }) }, attr.key);
13552
+ }),
13553
+ showPriceGroup && /* @__PURE__ */ jsx7(FilterFieldset, { legend: t.price, children: priceBands.map((band, i) => {
13554
+ const isActive = active.priceMin === band.min && active.priceMax === band.max;
13555
+ const nextMin = isActive ? void 0 : band.min;
13556
+ const nextMax = isActive ? void 0 : band.max;
13557
+ return /* @__PURE__ */ jsx7(
13558
+ OptionLink,
13559
+ {
13560
+ href: hrefs.price(nextMin, nextMax),
13561
+ active: isActive,
13562
+ label: priceBandLabel(band, t, currencyCode, locale),
13563
+ onActivate: onChange ? () => onChange({ priceMin: nextMin, priceMax: nextMax }) : void 0
13564
+ },
13565
+ i
13566
+ );
13567
+ }) }),
13568
+ hasOnSale && /* @__PURE__ */ jsx7(
13569
+ OptionLink,
13570
+ {
13571
+ href: hrefs.onSale(!active.onSale),
13572
+ active: Boolean(active.onSale),
13573
+ label: `${t.onSale} (${facets.onSale.count})`,
13574
+ onActivate: onChange ? () => onChange({ onSale: !active.onSale }) : void 0
13575
+ }
13576
+ ),
13577
+ /* @__PURE__ */ jsx7(
13578
+ "a",
13579
+ {
13580
+ href: hrefs.clearAll(),
13581
+ onClick: onChange ? (event) => {
13582
+ event.preventDefault();
13583
+ onChange({ taxonomy: {}, attr: {}, priceMin: void 0, priceMax: void 0, onSale: false });
13584
+ } : void 0,
13585
+ className: "text-sm font-medium text-muted-foreground underline-offset-4 hover:text-foreground hover:underline",
13586
+ children: t.clearAll
13587
+ }
13588
+ )
13589
+ ] });
13590
+ }
13404
13591
  export {
13405
13592
  ALLOWED_YOUTUBE_HOSTS,
13593
+ DEFAULT_SHOP_PRICE_BANDS,
13406
13594
  MarkdownRenderer,
13407
13595
  ProductCard,
13408
13596
  ProfileCard,
13409
13597
  SafeHtml,
13598
+ ShopFilters,
13410
13599
  Specialty,
13411
13600
  Treatment,
13412
13601
  buildShareUrls,
@@ -13414,6 +13603,7 @@ export {
13414
13603
  isAllowedYouTubeHost,
13415
13604
  markdownToSafeHtml,
13416
13605
  readingTime,
13417
- sanitizeHtml
13606
+ sanitizeHtml,
13607
+ shouldRenderShopFilters
13418
13608
  };
13419
13609
  //# sourceMappingURL=index.js.map