@giddaa-housing/ui 3.2.0 → 3.4.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/css/theme.css CHANGED
@@ -141,6 +141,7 @@
141
141
  --animate-accordion-up: accordion-up 0.2s ease-out;
142
142
  --animate-sonar-glow: sonar-glow 1.5s cubic-bezier(0, 0, 0.2, 1) infinite;
143
143
  --animate-round-sonar-glow: round-sonar-glow 1.5s ease-out infinite;
144
+ --animate-sonar-wave: sonar-wave 1.5s cubic-bezier(0, 0, 0.2, 1) infinite;
144
145
  --animate-spin-slow: spin 3s linear infinite;
145
146
  }
146
147
 
@@ -175,6 +176,7 @@
175
176
  --animate-bouncing-loader: none;
176
177
  --animate-sonar-glow: none;
177
178
  --animate-round-sonar-glow: none;
179
+ --animate-sonar-wave: none;
178
180
  --animate-spin-slow: none;
179
181
  }
180
182
  }
@@ -280,3 +282,21 @@
280
282
  opacity: 0;
281
283
  }
282
284
  }
285
+
286
+ /* Drives `Sonar`. Spread rather than `transform: scale()` on purpose: spread
287
+ grows the ring by the same number of pixels on every side and traces the
288
+ element's own border-radius, so one keyframe fits a 8px dot and a wide
289
+ button alike. Scaling would expand a wide element far more horizontally
290
+ than vertically — which is why sonar-glow above needs its per-axis
291
+ --sonar-glow-scale-x/y knobs. */
292
+ @keyframes sonar-wave {
293
+ from {
294
+ box-shadow: 0 0 0 0 currentColor;
295
+ opacity: 0.55;
296
+ }
297
+
298
+ to {
299
+ box-shadow: 0 0 0 var(--sonar-spread, 0.625rem) currentColor;
300
+ opacity: 0;
301
+ }
302
+ }
package/dist/accordion.js CHANGED
@@ -1,3 +1,4 @@
1
+ "use client";
1
2
  import { ChevronRight } from "./icons.js";
2
3
  import { t as cn } from "./cn-BI_4DMBf.js";
3
4
  import { Accordion as Accordion$1 } from "@base-ui/react/accordion";
package/dist/alert.js CHANGED
@@ -3,7 +3,7 @@ import { t as cn } from "./cn-BI_4DMBf.js";
3
3
  import { jsx } from "react/jsx-runtime";
4
4
  import { cva } from "class-variance-authority";
5
5
  //#region src/alert.tsx
6
- const alertVariants = cva("grid w-full grid-cols-[0_minmax(0,1fr)_auto] items-start gap-y-1 rounded-xl border-2 px-4 py-3.5 text-gdt-sm transition-[background-color,border-color,color,box-shadow] has-[>svg]:grid-cols-[1.25rem_minmax(0,1fr)_auto] has-[>svg]:gap-x-3 [&>svg]:col-start-1 [&>svg]:row-span-2 [&>svg]:mt-0.5 [&>svg]:size-5 [&>svg]:text-current", {
6
+ const alertVariants = cva("group/alert grid w-full grid-cols-[0_minmax(0,1fr)_auto] items-start gap-y-1 rounded-xl border-2 px-4 py-3.5 text-gdt-sm transition-[background-color,border-color,color,box-shadow] has-[>svg]:grid-cols-[1.25rem_minmax(0,1fr)_auto] has-[>svg]:gap-x-3 [&>svg]:col-start-1 [&>svg]:row-span-2 [&>svg]:mt-0.5 [&>svg]:size-5 [&>svg]:text-current", {
7
7
  variants: { variant: {
8
8
  success: "border-status-success bg-surface-brand-subtle text-status-success",
9
9
  info: "border-status-info bg-surface text-status-info",
@@ -33,14 +33,14 @@ function AlertTitle({ className, ...props }) {
33
33
  function AlertDescription({ className, ...props }) {
34
34
  return /* @__PURE__ */ jsx("div", {
35
35
  "data-slot": "alert-description",
36
- className: cn("col-start-2 row-start-2 text-fg-secondary [&_p]:leading-relaxed", className),
36
+ className: cn("col-start-2 row-start-1 text-fg-secondary group-has-[[data-slot=alert-title]]/alert:row-start-2 [&_p]:leading-relaxed", className),
37
37
  ...props
38
38
  });
39
39
  }
40
40
  function AlertActions({ className, ...props }) {
41
41
  return /* @__PURE__ */ jsx("div", {
42
42
  "data-slot": "alert-actions",
43
- className: cn("col-start-2 row-start-3 mt-2 flex shrink-0 items-center gap-2 justify-self-start sm:col-start-3 sm:row-span-2 sm:row-start-1 sm:mt-[-0.125rem] sm:justify-self-end", className),
43
+ className: cn("col-start-2 row-start-2 mt-2 flex shrink-0 items-center gap-2 justify-self-start max-sm:group-has-[[data-slot=alert-title]]/alert:row-start-3 sm:col-start-3 sm:row-span-2 sm:row-start-1 sm:mt-[-0.125rem] sm:justify-self-end", className),
44
44
  ...props
45
45
  });
46
46
  }
package/dist/badge.js CHANGED
@@ -36,6 +36,11 @@ const badgeVariants = cva("group/badge inline-flex w-fit shrink-0 items-center j
36
36
  false: ""
37
37
  }
38
38
  },
39
+ compoundVariants: [{
40
+ variant: "outline",
41
+ border: false,
42
+ class: "border-line"
43
+ }],
39
44
  defaultVariants: {
40
45
  variant: "default",
41
46
  size: "md",
package/dist/button.js CHANGED
@@ -22,8 +22,8 @@ const buttonVariants = cva("group/button inline-flex shrink-0 items-center justi
22
22
  danger: "bg-action-danger text-fg-on-brand hover:bg-action-danger-hover active:bg-action-danger-pressed data-focused:bg-action-danger-hover data-pressed:bg-action-danger-pressed",
23
23
  whatsapp: "bg-whatsapp-green text-black [&_svg]:text-white hover:brightness-95 active:brightness-90 data-focused:brightness-95 data-pressed:brightness-90",
24
24
  "glass-overlay": "bg-surface-glass-overlay-button text-fg-on-brand shadow-2 backdrop-blur-sm active:scale-[0.97] motion-reduce:active:scale-100",
25
- "link-brand": "border-transparent bg-transparent px-0 text-fg-brand hover:underline active:underline active:opacity-70",
26
- "link-neutral": "border-transparent bg-transparent px-0 text-fg-secondary hover:underline active:underline active:opacity-70"
25
+ "link-brand": "border-transparent bg-transparent text-fg-brand hover:underline active:underline active:opacity-70",
26
+ "link-neutral": "border-transparent bg-transparent text-fg-secondary hover:underline active:underline active:opacity-70"
27
27
  },
28
28
  size: {
29
29
  sm: "h-8 min-w-8 gap-2 px-3.5 text-gdt-xs [&_svg:not([class*='size-'])]:size-4",
@@ -37,6 +37,10 @@ const buttonVariants = cva("group/button inline-flex shrink-0 items-center justi
37
37
  "icon-xl": "size-14 text-gdt-lg [&_svg:not([class*='size-'])]:size-5"
38
38
  }
39
39
  },
40
+ compoundVariants: [{
41
+ variant: ["link-brand", "link-neutral"],
42
+ class: "h-auto min-h-0 w-auto min-w-0 shrink justify-start rounded-none px-0 py-0 text-left whitespace-normal underline-offset-4 active:not-aria-[haspopup]:translate-y-0"
43
+ }],
40
44
  defaultVariants: {
41
45
  variant: "primary",
42
46
  size: "md"
package/dist/footer.d.ts CHANGED
@@ -1,5 +1,6 @@
1
1
  import { n as InputSize } from "./input-CfoEJ8A6.js";
2
2
  import { useRender } from "@base-ui/react/use-render";
3
+ import { VariantProps } from "class-variance-authority";
3
4
  import * as React from "react";
4
5
  //#region src/footer.d.ts
5
6
  type FooterRootProps = React.ComponentProps<"footer"> & {
@@ -20,10 +21,14 @@ declare function FooterSection({ className, ...props }: React.ComponentProps<"di
20
21
  declare function FooterSectionTitle({ className, ...props }: React.ComponentProps<"h3">): React.JSX.Element;
21
22
  declare function FooterLinkList({ className, ...props }: React.ComponentProps<"ul">): React.JSX.Element;
22
23
  declare function FooterLinkItem({ className, ...props }: React.ComponentProps<"li">): React.JSX.Element;
23
- type FooterLinkProps = useRender.ComponentProps<"a"> & {
24
+ declare const footerLinkVariants: (props?: ({
25
+ variant?: "brand" | "default" | null | undefined;
26
+ } & import("class-variance-authority/types").ClassProp) | undefined) => string;
27
+ type FooterLinkVariant = NonNullable<VariantProps<typeof footerLinkVariants>["variant"]>;
28
+ type FooterLinkProps = useRender.ComponentProps<"a"> & VariantProps<typeof footerLinkVariants> & {
24
29
  showArrow?: boolean;
25
30
  };
26
- declare function FooterLink({ className, children, render, showArrow, ...props }: FooterLinkProps): React.ReactElement<unknown, string | React.JSXElementConstructor<any>>;
31
+ declare function FooterLink({ className, children, render, showArrow, variant, ...props }: FooterLinkProps): React.ReactElement<unknown, string | React.JSXElementConstructor<any>>;
27
32
  declare function FooterContactGrid({ className, ...props }: React.ComponentProps<"div">): React.JSX.Element;
28
33
  declare function FooterContact({ className, ...props }: React.ComponentProps<"address">): React.JSX.Element;
29
34
  declare function FooterContactTitle({ className, ...props }: React.ComponentProps<"h3">): React.JSX.Element;
@@ -39,4 +44,4 @@ declare function FooterNewsletterControl({ className, children, size, ...props }
39
44
  declare function FooterBottom({ className, ...props }: React.ComponentProps<"div">): React.JSX.Element;
40
45
  declare function FooterLegalLinks({ className, ...props }: React.ComponentProps<"nav">): React.JSX.Element;
41
46
  //#endregion
42
- export { FooterBody, FooterBottom, FooterContact, FooterContactGrid, FooterContactTitle, FooterCta, FooterCtaActions, FooterCtaContent, FooterCtaDescription, FooterCtaTitle, FooterDescription, FooterHeader, FooterLegalLinks, FooterLink, FooterLinkItem, FooterLinkList, type FooterLinkProps, FooterNav, FooterNewsletter, FooterNewsletterControl, FooterNewsletterDescription, FooterNewsletterTitle, FooterRoot, type FooterRootProps, FooterSection, FooterSectionTitle, FooterSocialItem, FooterSocialLink, FooterSocialList, FooterTitle };
47
+ export { FooterBody, FooterBottom, FooterContact, FooterContactGrid, FooterContactTitle, FooterCta, FooterCtaActions, FooterCtaContent, FooterCtaDescription, FooterCtaTitle, FooterDescription, FooterHeader, FooterLegalLinks, FooterLink, FooterLinkItem, FooterLinkList, type FooterLinkProps, type FooterLinkVariant, FooterNav, FooterNewsletter, FooterNewsletterControl, FooterNewsletterDescription, FooterNewsletterTitle, FooterRoot, type FooterRootProps, FooterSection, FooterSectionTitle, FooterSocialItem, FooterSocialLink, FooterSocialList, FooterTitle, footerLinkVariants };
package/dist/footer.js CHANGED
@@ -6,6 +6,7 @@ import { Input } from "./input.js";
6
6
  import { Fragment, jsx, jsxs } from "react/jsx-runtime";
7
7
  import { mergeProps } from "@base-ui/react/merge-props";
8
8
  import { useRender } from "@base-ui/react/use-render";
9
+ import { cva } from "class-variance-authority";
9
10
  //#region src/footer.tsx
10
11
  function FooterRoot({ className, containerClassName, children, ...props }) {
11
12
  return /* @__PURE__ */ jsx("footer", {
@@ -117,18 +118,28 @@ function FooterLinkItem({ className, ...props }) {
117
118
  ...props
118
119
  });
119
120
  }
120
- function FooterLink({ className, children, render, showArrow = false, ...props }) {
121
+ const footerLinkVariants = cva("inline-flex items-center gap-gdt-sm underline-offset-4 transition-colors hover:underline focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-line-focus focus-visible:ring-offset-2", {
122
+ variants: { variant: {
123
+ default: "text-fg-secondary hover:text-fg-brand",
124
+ brand: "text-fg-brand"
125
+ } },
126
+ defaultVariants: { variant: "default" }
127
+ });
128
+ function FooterLink({ className, children, render, showArrow = false, variant = "default", ...props }) {
121
129
  return useRender({
122
130
  defaultTagName: "a",
123
131
  props: mergeProps({
124
- className: cn("inline-flex items-center gap-gdt-sm text-fg-secondary underline-offset-4 transition-colors hover:text-fg-brand hover:underline focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-line-focus focus-visible:ring-offset-2", className),
132
+ className: cn(footerLinkVariants({ variant }), className),
125
133
  children: /* @__PURE__ */ jsxs(Fragment, { children: [/* @__PURE__ */ jsx("span", { children }), showArrow && /* @__PURE__ */ jsx(ChevronRight, {
126
134
  "aria-hidden": "true",
127
135
  className: "size-3.5"
128
136
  })] })
129
137
  }, props),
130
138
  render,
131
- state: { slot: "footer-link" }
139
+ state: {
140
+ slot: "footer-link",
141
+ variant
142
+ }
132
143
  });
133
144
  }
134
145
  function FooterContactGrid({ className, ...props }) {
@@ -229,4 +240,4 @@ function FooterLegalLinks({ className, ...props }) {
229
240
  });
230
241
  }
231
242
  //#endregion
232
- export { FooterBody, FooterBottom, FooterContact, FooterContactGrid, FooterContactTitle, FooterCta, FooterCtaActions, FooterCtaContent, FooterCtaDescription, FooterCtaTitle, FooterDescription, FooterHeader, FooterLegalLinks, FooterLink, FooterLinkItem, FooterLinkList, FooterNav, FooterNewsletter, FooterNewsletterControl, FooterNewsletterDescription, FooterNewsletterTitle, FooterRoot, FooterSection, FooterSectionTitle, FooterSocialItem, FooterSocialLink, FooterSocialList, FooterTitle };
243
+ export { FooterBody, FooterBottom, FooterContact, FooterContactGrid, FooterContactTitle, FooterCta, FooterCtaActions, FooterCtaContent, FooterCtaDescription, FooterCtaTitle, FooterDescription, FooterHeader, FooterLegalLinks, FooterLink, FooterLinkItem, FooterLinkList, FooterNav, FooterNewsletter, FooterNewsletterControl, FooterNewsletterDescription, FooterNewsletterTitle, FooterRoot, FooterSection, FooterSectionTitle, FooterSocialItem, FooterSocialLink, FooterSocialList, FooterTitle, footerLinkVariants };
@@ -8,8 +8,8 @@ import { ReviewBlock } from "./review-block.js";
8
8
  import { Fragment, jsx, jsxs } from "react/jsx-runtime";
9
9
  import { cva } from "class-variance-authority";
10
10
  //#region src/media-gallery-hero.tsx
11
- const mediaGalleryHeroVariants = cva("relative isolate h-[350px] w-full overflow-hidden rounded-[32px] bg-surface-raised md:h-[380px] md:rounded-[40px] lg:h-[480px]");
12
- const mediaGalleryImagesVariants = cva("absolute inset-0 z-0 hidden size-full min-h-0 min-w-0 bg-surface-raised md:grid", {
11
+ const mediaGalleryHeroVariants = cva("relative isolate h-[350px] w-full overflow-hidden rounded-[32px] md:h-[380px] md:rounded-[40px] lg:h-[480px]");
12
+ const mediaGalleryImagesVariants = cva("absolute inset-0 z-0 hidden size-full min-h-0 min-w-0 md:grid", {
13
13
  variants: { imageCount: {
14
14
  1: "",
15
15
  2: "gap-2 grid-cols-[minmax(0,3fr)_minmax(0,2fr)]",
@@ -77,7 +77,7 @@ function MobileGalleryCounter({ total }) {
77
77
  function MobileMediaCarousel({ currentMedia, images, mediaTotal, onMediaClick, renderMedia, showMobileIndicator }) {
78
78
  const carouselImages = images.length > 0 ? images : [{}];
79
79
  return /* @__PURE__ */ jsxs(Carousel, {
80
- className: "absolute inset-0 z-0 block size-full rounded-[32px] bg-surface-raised md:hidden",
80
+ className: "absolute inset-0 z-0 block size-full rounded-[32px] md:hidden",
81
81
  imageSize: "full",
82
82
  opts: {
83
83
  align: "start",
@@ -3,32 +3,100 @@ import * as React from "react";
3
3
  //#region src/price-tag.d.ts
4
4
  declare const priceTagVariants: (props?: ({
5
5
  size?: "lg" | "md" | "sm" | "xs" | "xxs" | null | undefined;
6
+ hasDescription?: boolean | null | undefined;
6
7
  hasMonthlyPayment?: boolean | null | undefined;
7
8
  } & import("class-variance-authority/types").ClassProp) | undefined) => string;
8
9
  type PriceTagSize = NonNullable<VariantProps<typeof priceTagVariants>["size"]>;
10
+ /**
11
+ * A currency in the dropdown.
12
+ *
13
+ * `currency`, `value` and `description` are all PriceTag needs, and none of
14
+ * them are interpreted — `description` in particular is printed as given, so
15
+ * the wording is yours. The rest are legacy and go in the next major.
16
+ */
9
17
  type PriceTagCurrencyOption = {
10
- code: string;
11
- amount: string;
18
+ /** Shown at the leading edge, and matched against `selectedCurrency`. */
19
+ currency?: string;
20
+ /** The price, exactly as it should read. */
21
+ value?: React.ReactNode;
22
+ /** A second line under the price. Your words, printed as given. */
23
+ description?: React.ReactNode;
24
+ /** @deprecated Use `currency`. Removed in the next major. */
25
+ code?: string;
26
+ /** @deprecated Use `value`. Removed in the next major. */
27
+ amount?: string;
28
+ /** @deprecated Format before you pass it. Removed in the next major. */
12
29
  shortAmount?: string;
30
+ /** @deprecated Use `description`. Removed in the next major. */
13
31
  monthlyPayment?: string;
32
+ /** @deprecated Use `description`. Removed in the next major. */
14
33
  monthlyPaymentLabel?: string;
15
34
  };
35
+ /**
36
+ * A currency option after the legacy fields have been folded in — what
37
+ * `onCurrencyChange` and `renderMenuOption` receive. The original object is
38
+ * spread in, so code still reading `code` or `amount` off it keeps working.
39
+ *
40
+ * Generic over your own option type, which `PriceTag` infers from `currencies`.
41
+ * Anything extra you hang on an option — a rate, an id, a flag — arrives here
42
+ * typed, so `renderMenuOption` can use it without a cast.
43
+ */
44
+ type PriceTagMenuOption<TOption extends PriceTagCurrencyOption = PriceTagCurrencyOption> = TOption & {
45
+ currency: string;
46
+ value: React.ReactNode;
47
+ description?: React.ReactNode;
48
+ /** Whether this is the row `selectedCurrency` points at. */
49
+ selected: boolean;
50
+ };
51
+ /** @deprecated Format before you pass it. Removed in the next major. */
16
52
  type PriceTagFormat = "full" | "shortened";
17
- type PriceTagProps = Omit<React.ComponentProps<"button">, "children"> & {
18
- amount: string;
19
- shortAmount?: string;
20
- format?: PriceTagFormat;
53
+ type PriceTagProps<TOption extends PriceTagCurrencyOption = PriceTagCurrencyOption> = Omit<React.ComponentProps<"button">, "children" | "value"> & {
54
+ /** The price, exactly as it should read. */
55
+ value?: React.ReactNode;
56
+ /** A second line under the price. Your words, printed as given. */
57
+ description?: React.ReactNode;
21
58
  size?: PriceTagSize;
22
- monthlyPayment?: string;
23
- monthlyPaymentLabel?: string;
24
- currencies?: PriceTagCurrencyOption[];
59
+ /**
60
+ * Providing this turns the tag into a dropdown trigger. Options carry
61
+ * whatever else you need — `TOption` is inferred from here, and the
62
+ * callbacks below receive it intact.
63
+ */
64
+ currencies?: TOption[];
25
65
  selectedCurrency?: string;
26
- onCurrencyChange?: (currency: PriceTagCurrencyOption) => void;
66
+ onCurrencyChange?: (option: PriceTagMenuOption<NoInfer<TOption>>) => void;
67
+ /**
68
+ * Replace a row's contents. PriceTag keeps the button, its selected state
69
+ * and its keyboard behaviour, so a custom row can't quietly lose them.
70
+ */
71
+ renderMenuOption?: (option: PriceTagMenuOption<NoInfer<TOption>>) => React.ReactNode;
27
72
  open?: boolean;
28
73
  defaultOpen?: boolean;
29
74
  onOpenChange?: (open: boolean) => void;
30
75
  popoverClassName?: string;
76
+ /** @deprecated Use `value`. Removed in the next major. */
77
+ amount?: string;
78
+ /** @deprecated Format before you pass it. Removed in the next major. */
79
+ shortAmount?: string;
80
+ /** @deprecated Format before you pass it. Removed in the next major. */
81
+ format?: PriceTagFormat;
82
+ /** @deprecated Use `description`. Removed in the next major. */
83
+ monthlyPayment?: string;
84
+ /** @deprecated Use `description`. Removed in the next major. */
85
+ monthlyPaymentLabel?: string;
31
86
  };
32
- declare function PriceTag({ amount, shortAmount, format, size, monthlyPayment, monthlyPaymentLabel, currencies, selectedCurrency, onCurrencyChange, open, defaultOpen, onOpenChange, className, popoverClassName, type, ...props }: PriceTagProps): React.JSX.Element;
87
+ /**
88
+ * Pill-shaped price display, optionally a trigger for a currency dropdown.
89
+ *
90
+ * Display only: it prints `value` and `description` as given and never
91
+ * composes a sentence, picks a format, or decides that a line is too small to
92
+ * be worth showing. Wording, currency conversion and number formatting all
93
+ * belong to the app, which is the only place that knows the locale, the
94
+ * product's voice, and what the second line is actually for.
95
+ *
96
+ * The legacy `amount` / `shortAmount` / `format` / `monthlyPayment` props still
97
+ * work and keep their old behaviour exactly, including the size threshold that
98
+ * hid the monthly line below `md`. They go in the next major.
99
+ */
100
+ declare function PriceTag<TOption extends PriceTagCurrencyOption = PriceTagCurrencyOption>({ value, description, size, currencies, selectedCurrency, onCurrencyChange, renderMenuOption, open, defaultOpen, onOpenChange, className, popoverClassName, type, amount, shortAmount, format, monthlyPayment, monthlyPaymentLabel, ...props }: PriceTagProps<TOption>): React.JSX.Element;
33
101
  //#endregion
34
- export { PriceTag, type PriceTagCurrencyOption, type PriceTagFormat, type PriceTagProps, type PriceTagSize, priceTagVariants };
102
+ export { PriceTag, type PriceTagCurrencyOption, type PriceTagFormat, type PriceTagMenuOption, type PriceTagProps, type PriceTagSize, priceTagVariants };
package/dist/price-tag.js CHANGED
@@ -1,7 +1,7 @@
1
1
  "use client";
2
2
  import { Check, ChevronDown } from "./icons.js";
3
3
  import { t as cn } from "./cn-BI_4DMBf.js";
4
- import { jsx, jsxs } from "react/jsx-runtime";
4
+ import { Fragment, jsx, jsxs } from "react/jsx-runtime";
5
5
  import { cva } from "class-variance-authority";
6
6
  import { Popover } from "@base-ui/react/popover";
7
7
  //#region src/price-tag.tsx
@@ -14,6 +14,12 @@ const priceTagVariants = cva("group/price-tag inline-flex w-fit shrink-0 items-c
14
14
  md: "min-h-10.5 gap-1.5 px-5 py-2 text-gdt-md [&>svg]:size-4",
15
15
  lg: "min-h-13 gap-2 px-7 py-3 text-gdt-lg [&>svg]:size-4.5"
16
16
  },
17
+ /** Two lines sit top-aligned against the chevron rather than centred. */
18
+ hasDescription: {
19
+ true: "items-start",
20
+ false: ""
21
+ },
22
+ /** @deprecated Use `hasDescription`. Removed in the next major. */
17
23
  hasMonthlyPayment: {
18
24
  true: "items-start",
19
25
  false: ""
@@ -21,37 +27,102 @@ const priceTagVariants = cva("group/price-tag inline-flex w-fit shrink-0 items-c
21
27
  },
22
28
  defaultVariants: {
23
29
  size: "md",
24
- hasMonthlyPayment: false
30
+ hasDescription: false
25
31
  }
26
32
  });
33
+ /** The description is quieter than the price, and quietest at the largest size. */
34
+ const descriptionSizeClassName = {
35
+ xxs: "text-gdt-subtext",
36
+ xs: "text-gdt-subtext",
37
+ sm: "text-gdt-subtext",
38
+ md: "text-gdt-subtext",
39
+ lg: "text-gdt-xs"
40
+ };
27
41
  function getDisplayAmount({ amount, shortAmount, format }) {
28
42
  return format === "shortened" && shortAmount ? shortAmount : amount;
29
43
  }
30
- function PriceTag({ amount, shortAmount, format = "full", size = "md", monthlyPayment, monthlyPaymentLabel = "Low as", currencies, selectedCurrency, onCurrencyChange, open, defaultOpen, onOpenChange, className, popoverClassName, type = "button", ...props }) {
44
+ /**
45
+ * Folds the legacy fields into the current three. New fields win outright, so
46
+ * an option can be migrated one at a time.
47
+ */
48
+ function resolveMenuOption(option, format, selectedCurrency) {
49
+ const currency = option.currency ?? option.code ?? "";
50
+ const value = option.value ?? getDisplayAmount({
51
+ amount: option.amount,
52
+ shortAmount: option.shortAmount,
53
+ format
54
+ });
55
+ const description = option.description ?? (option.monthlyPayment ? `${option.monthlyPaymentLabel ?? "As low as"} ${option.monthlyPayment} per month` : void 0);
56
+ return {
57
+ ...option,
58
+ currency,
59
+ value,
60
+ description,
61
+ selected: currency === selectedCurrency
62
+ };
63
+ }
64
+ function DefaultMenuOption({ option }) {
65
+ return /* @__PURE__ */ jsxs(Fragment, { children: [
66
+ /* @__PURE__ */ jsx("span", {
67
+ className: "min-w-10 text-gdt-sm font-semibold text-fg-brand",
68
+ children: option.currency
69
+ }),
70
+ /* @__PURE__ */ jsxs("span", {
71
+ className: "ml-auto flex min-w-0 flex-col items-end",
72
+ children: [/* @__PURE__ */ jsx("span", {
73
+ className: "text-gdt-sm font-bold text-fg-primary",
74
+ children: option.value
75
+ }), option.description ? /* @__PURE__ */ jsx("span", {
76
+ className: "text-gdt-xs text-fg-secondary",
77
+ children: option.description
78
+ }) : null]
79
+ }),
80
+ option.selected ? /* @__PURE__ */ jsx(Check, {
81
+ "aria-hidden": "true",
82
+ className: "size-4 shrink-0 text-fg-brand"
83
+ }) : null
84
+ ] });
85
+ }
86
+ /**
87
+ * Pill-shaped price display, optionally a trigger for a currency dropdown.
88
+ *
89
+ * Display only: it prints `value` and `description` as given and never
90
+ * composes a sentence, picks a format, or decides that a line is too small to
91
+ * be worth showing. Wording, currency conversion and number formatting all
92
+ * belong to the app, which is the only place that knows the locale, the
93
+ * product's voice, and what the second line is actually for.
94
+ *
95
+ * The legacy `amount` / `shortAmount` / `format` / `monthlyPayment` props still
96
+ * work and keep their old behaviour exactly, including the size threshold that
97
+ * hid the monthly line below `md`. They go in the next major.
98
+ */
99
+ function PriceTag({ value, description, size = "md", currencies, selectedCurrency, onCurrencyChange, renderMenuOption, open, defaultOpen, onOpenChange, className, popoverClassName, type = "button", amount, shortAmount, format = "full", monthlyPayment, monthlyPaymentLabel = "Low as", ...props }) {
31
100
  const hasDropdown = Boolean(currencies?.length);
32
- const displayMonthlyPayment = (size === "md" || size === "lg") && monthlyPayment ? `${monthlyPaymentLabel} ${monthlyPayment} monthly` : void 0;
101
+ const resolvedValue = value ?? getDisplayAmount({
102
+ amount,
103
+ shortAmount,
104
+ format
105
+ });
106
+ const legacyDescription = monthlyPayment && (size === "md" || size === "lg") ? `${monthlyPaymentLabel} ${monthlyPayment} monthly` : void 0;
107
+ const resolvedDescription = description ?? legacyDescription;
33
108
  const pill = /* @__PURE__ */ jsxs("span", {
34
109
  className: "flex min-w-0 flex-col gap-1",
35
- children: [/* @__PURE__ */ jsx("span", { children: getDisplayAmount({
36
- amount,
37
- shortAmount,
38
- format
39
- }) }), displayMonthlyPayment ? /* @__PURE__ */ jsx("span", {
40
- className: cn("hidden text-gdt-subtext font-medium leading-none opacity-90", {
41
- "inline-block": size === "md",
42
- "inline-block text-gdt-xs": size === "lg"
43
- }),
44
- children: displayMonthlyPayment
110
+ children: [/* @__PURE__ */ jsx("span", { children: resolvedValue }), resolvedDescription ? /* @__PURE__ */ jsx("span", {
111
+ className: cn("font-medium leading-none opacity-90", descriptionSizeClassName[size]),
112
+ children: resolvedDescription
45
113
  }) : null]
46
114
  });
115
+ const pillClassName = cn(priceTagVariants({
116
+ size,
117
+ hasDescription: Boolean(resolvedDescription)
118
+ }), className);
47
119
  if (!hasDropdown) return /* @__PURE__ */ jsx("span", {
48
120
  "data-slot": "price-tag",
49
- className: cn(priceTagVariants({
50
- size,
51
- hasMonthlyPayment: Boolean(displayMonthlyPayment)
52
- }), className),
121
+ className: pillClassName,
122
+ ...props,
53
123
  children: pill
54
124
  });
125
+ const options = (currencies ?? []).map((currency) => resolveMenuOption(currency, format, selectedCurrency));
55
126
  return /* @__PURE__ */ jsxs(Popover.Root, {
56
127
  open,
57
128
  defaultOpen,
@@ -60,10 +131,7 @@ function PriceTag({ amount, shortAmount, format = "full", size = "md", monthlyPa
60
131
  "data-slot": "price-tag",
61
132
  "data-interactive": "true",
62
133
  type,
63
- className: cn(priceTagVariants({
64
- size,
65
- hasMonthlyPayment: Boolean(displayMonthlyPayment)
66
- }), className),
134
+ className: pillClassName,
67
135
  ...props,
68
136
  children: [pill, /* @__PURE__ */ jsx(ChevronDown, {
69
137
  "aria-hidden": "true",
@@ -75,41 +143,14 @@ function PriceTag({ amount, shortAmount, format = "full", size = "md", monthlyPa
75
143
  children: /* @__PURE__ */ jsx(Popover.Popup, {
76
144
  "data-slot": "price-tag-popover",
77
145
  className: cn("z-50 flex w-[17.25rem] max-w-[calc(100vw-2rem)] origin-(--transform-origin) flex-col rounded-xl border border-line-subtle bg-surface-overlay p-2 text-fg-primary shadow-3 outline-hidden transition-[opacity,scale,translate] duration-[var(--duration-motion-popup)] ease-gdt-out data-starting-style:opacity-0 data-starting-style:scale-95 data-ending-style:opacity-0 data-ending-style:scale-95 data-[side=bottom]:data-starting-style:translate-y-2 data-[side=bottom]:data-ending-style:translate-y-2 data-[side=top]:data-starting-style:-translate-y-2 data-[side=top]:data-ending-style:-translate-y-2 motion-reduce:data-starting-style:scale-100 motion-reduce:data-ending-style:scale-100 motion-reduce:data-[side=bottom]:data-starting-style:translate-y-0 motion-reduce:data-[side=bottom]:data-ending-style:translate-y-0 motion-reduce:data-[side=top]:data-starting-style:translate-y-0 motion-reduce:data-[side=top]:data-ending-style:translate-y-0", popoverClassName),
78
- children: currencies?.map((currency) => {
79
- const active = currency.code === selectedCurrency;
80
- const optionAmount = getDisplayAmount({
81
- amount: currency.amount,
82
- shortAmount: currency.shortAmount,
83
- format
84
- });
85
- const optionMonthlyPayment = currency.monthlyPayment ? `${currency.monthlyPaymentLabel ?? "As low as"} ${currency.monthlyPayment} per month` : void 0;
86
- return /* @__PURE__ */ jsxs("button", {
87
- type: "button",
88
- "data-active": active ? "true" : void 0,
89
- className: "flex min-h-14 w-full items-center gap-3 rounded-lg px-3 text-start outline-none transition-colors hover:bg-surface-brand-subtle focus-visible:bg-surface-brand-subtle focus-visible:shadow-[0px_0px_0px_2px_var(--color-line-focus)] data-[active=true]:bg-surface-brand-subtle",
90
- onClick: () => onCurrencyChange?.(currency),
91
- children: [
92
- /* @__PURE__ */ jsx("span", {
93
- className: "min-w-10 text-gdt-sm font-semibold text-fg-brand",
94
- children: currency.code
95
- }),
96
- /* @__PURE__ */ jsxs("span", {
97
- className: "ml-auto flex min-w-0 flex-col items-end",
98
- children: [/* @__PURE__ */ jsx("span", {
99
- className: "text-gdt-sm font-bold text-fg-primary",
100
- children: optionAmount
101
- }), optionMonthlyPayment ? /* @__PURE__ */ jsx("span", {
102
- className: "text-gdt-xs text-fg-secondary",
103
- children: optionMonthlyPayment
104
- }) : null]
105
- }),
106
- active ? /* @__PURE__ */ jsx(Check, {
107
- "aria-hidden": "true",
108
- className: "size-4 shrink-0 text-fg-brand"
109
- }) : null
110
- ]
111
- }, currency.code);
112
- })
146
+ children: options.map((option) => /* @__PURE__ */ jsx("button", {
147
+ type: "button",
148
+ "data-slot": "price-tag-option",
149
+ "data-active": option.selected ? "true" : void 0,
150
+ className: "flex min-h-14 w-full items-center gap-3 rounded-lg px-3 text-start outline-none transition-colors hover:bg-surface-brand-subtle focus-visible:bg-surface-brand-subtle focus-visible:shadow-[0px_0px_0px_2px_var(--color-line-focus)] data-[active=true]:bg-surface-brand-subtle",
151
+ onClick: () => onCurrencyChange?.(option),
152
+ children: renderMenuOption ? renderMenuOption(option) : /* @__PURE__ */ jsx(DefaultMenuOption, { option })
153
+ }, option.currency))
113
154
  })
114
155
  }) })]
115
156
  });
@@ -62,7 +62,7 @@ function PropertyListingCardMediaTop({ className, ...props }) {
62
62
  function PropertyListingCardOptionsBar({ className, render, ...props }) {
63
63
  return useRender({
64
64
  defaultTagName: "a",
65
- props: mergeProps({ className: cn("absolute inset-x-0 bottom-0 z-10 flex min-h-9.5 items-center justify-center gap-1 bg-black/80 px-4 py-1.5 text-center text-gdt-subtext font-extrabold tracking-wide text-white uppercase transition-[background-color] hover:bg-black/90 focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-line-focus", className) }, props),
65
+ props: mergeProps({ className: cn("absolute inset-x-0 bottom-0 z-10 flex min-h-9.5 items-center justify-center gap-1 bg-black/70 px-4 py-1.5 text-center text-gdt-subtext font-extrabold tracking-wide text-white uppercase transition-[background-color] hover:bg-black/80 focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-line-focus", "supports-backdrop-filter:bg-black/40 supports-backdrop-filter:backdrop-blur-[15px] supports-backdrop-filter:hover:bg-black/55", className) }, props),
66
66
  render,
67
67
  state: { slot: "property-listing-card-options-bar" }
68
68
  });
@@ -0,0 +1,76 @@
1
+ import { t as ComponentSize } from "./size-context-BX6kAdVj.js";
2
+ import { TabsListVariant } from "./tabs.js";
3
+ import * as React from "react";
4
+ //#region src/scroll-spy.d.ts
5
+ /**
6
+ * Read and drive the spy from anywhere inside a `ScrollSpy` — an overflow
7
+ * "More" menu, a floating progress rail, a heading that echoes the section
8
+ * being read.
9
+ *
10
+ * Context reaches through portals, so a consumer inside an open `DropdownMenu`
11
+ * or `Popover` works without threading anything down by hand. `registerSection`
12
+ * is deliberately not exposed: sections are `ScrollSpyContent`'s to own.
13
+ */
14
+ declare function useScrollSpy(): {
15
+ activeValue: string | undefined;
16
+ getSectionId: (value: string) => string;
17
+ getTriggerId: (value: string) => string;
18
+ scrollToSection: (value: string) => void;
19
+ };
20
+ type ScrollSpyProps = Omit<React.ComponentProps<"div">, "onChange"> & {
21
+ orientation?: "horizontal" | "vertical";
22
+ /** Controlled active section. */
23
+ value?: string;
24
+ /** Active section before the first scroll measurement lands. */
25
+ defaultValue?: string;
26
+ onValueChange?: (value: string) => void;
27
+ /**
28
+ * Distance in px between the top of the scrollport and the line a section
29
+ * has to cross to become active. Set it to the height of a sticky header —
30
+ * it also becomes each section's `scroll-margin-top`, so scrolling lands
31
+ * the heading below the header rather than under it.
32
+ */
33
+ scrollOffset?: number;
34
+ };
35
+ /**
36
+ * `Tabs` for a page that shows everything at once: every section stays mounted
37
+ * and visible, the triggers scroll to their section, and the active trigger
38
+ * tracks whichever section the reader is currently on.
39
+ *
40
+ * The parts mirror the `Tabs` API — `ScrollSpy` / `ScrollSpyList` /
41
+ * `ScrollSpyTrigger` / `ScrollSpyContent`, matched by `value` — and the list
42
+ * takes the same `variant` and `size` as `TabsList`, so the two read as one
43
+ * family. Overflow ("More" dropdowns, per-breakpoint trigger counts) is the
44
+ * consuming app's job, exactly as it is with `Tabs`;
45
+ * `scrollSpyTriggerClassName` is exported so an overflow button can match the
46
+ * real triggers.
47
+ */
48
+ declare function ScrollSpy({ className, orientation, value, defaultValue, onValueChange, scrollOffset, children, ...props }: ScrollSpyProps): React.JSX.Element;
49
+ type ScrollSpyListProps = React.ComponentProps<"nav"> & {
50
+ variant?: TabsListVariant;
51
+ size?: ComponentSize;
52
+ /**
53
+ * Keep the active trigger visible as the reader scrolls. Opt in when the
54
+ * list itself scrolls — without it, a strip narrower than its triggers looks
55
+ * frozen once the active one passes out of view. No-op when nothing
56
+ * overflows, and it never scrolls anything but the list.
57
+ */
58
+ scrollActiveIntoView?: boolean;
59
+ };
60
+ declare function ScrollSpyList({ className, variant, size, children, scrollActiveIntoView, "aria-label": ariaLabel, ...props }: ScrollSpyListProps): React.JSX.Element;
61
+ /**
62
+ * Trigger styling, exported so an overflow "More" button outside the list can
63
+ * render the same look — the `Tabs` trigger classes verbatim, so the two
64
+ * components stay pixel-identical.
65
+ */
66
+ declare const scrollSpyTriggerClassName: string;
67
+ type ScrollSpyTriggerProps = Omit<React.ComponentProps<"button">, "value"> & {
68
+ value: string;
69
+ };
70
+ declare function ScrollSpyTrigger({ className, value, type, onClick, ...props }: ScrollSpyTriggerProps): React.JSX.Element;
71
+ type ScrollSpyContentProps = Omit<React.ComponentProps<"section">, "value"> & {
72
+ value: string;
73
+ };
74
+ declare function ScrollSpyContent({ className, value, style, tabIndex, ...props }: ScrollSpyContentProps): React.JSX.Element;
75
+ //#endregion
76
+ export { ScrollSpy, ScrollSpyContent, type ScrollSpyContentProps, ScrollSpyList, type ScrollSpyListProps, type ScrollSpyProps, ScrollSpyTrigger, type ScrollSpyTriggerProps, scrollSpyTriggerClassName, useScrollSpy };