@giddaa-housing/ui 3.2.0 → 3.3.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/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/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",
@@ -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,61 @@
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
+ type ScrollSpyProps = Omit<React.ComponentProps<"div">, "onChange"> & {
6
+ orientation?: "horizontal" | "vertical";
7
+ /** Controlled active section. */
8
+ value?: string;
9
+ /** Active section before the first scroll measurement lands. */
10
+ defaultValue?: string;
11
+ onValueChange?: (value: string) => void;
12
+ /**
13
+ * Distance in px between the top of the scrollport and the line a section
14
+ * has to cross to become active. Set it to the height of a sticky header —
15
+ * it also becomes each section's `scroll-margin-top`, so scrolling lands
16
+ * the heading below the header rather than under it.
17
+ */
18
+ scrollOffset?: number;
19
+ };
20
+ /**
21
+ * `Tabs` for a page that shows everything at once: every section stays mounted
22
+ * and visible, the triggers scroll to their section, and the active trigger
23
+ * tracks whichever section the reader is currently on.
24
+ *
25
+ * The parts mirror the `Tabs` API — `ScrollSpy` / `ScrollSpyList` /
26
+ * `ScrollSpyTrigger` / `ScrollSpyContent`, matched by `value` — and the list
27
+ * takes the same `variant` and `size` as `TabsList`, so the two read as one
28
+ * family. Overflow ("More" dropdowns, per-breakpoint trigger counts) is the
29
+ * consuming app's job, exactly as it is with `Tabs`;
30
+ * `scrollSpyTriggerClassName` is exported so an overflow button can match the
31
+ * real triggers.
32
+ */
33
+ declare function ScrollSpy({ className, orientation, value, defaultValue, onValueChange, scrollOffset, children, ...props }: ScrollSpyProps): React.JSX.Element;
34
+ type ScrollSpyListProps = React.ComponentProps<"nav"> & {
35
+ variant?: TabsListVariant;
36
+ size?: ComponentSize;
37
+ /**
38
+ * Keep the active trigger visible as the reader scrolls. Opt in when the
39
+ * list itself scrolls — without it, a strip narrower than its triggers looks
40
+ * frozen once the active one passes out of view. No-op when nothing
41
+ * overflows, and it never scrolls anything but the list.
42
+ */
43
+ scrollActiveIntoView?: boolean;
44
+ };
45
+ declare function ScrollSpyList({ className, variant, size, children, scrollActiveIntoView, "aria-label": ariaLabel, ...props }: ScrollSpyListProps): React.JSX.Element;
46
+ /**
47
+ * Trigger styling, exported so an overflow "More" button outside the list can
48
+ * render the same look — the `Tabs` trigger classes verbatim, so the two
49
+ * components stay pixel-identical.
50
+ */
51
+ declare const scrollSpyTriggerClassName: string;
52
+ type ScrollSpyTriggerProps = Omit<React.ComponentProps<"button">, "value"> & {
53
+ value: string;
54
+ };
55
+ declare function ScrollSpyTrigger({ className, value, type, onClick, ...props }: ScrollSpyTriggerProps): React.JSX.Element;
56
+ type ScrollSpyContentProps = Omit<React.ComponentProps<"section">, "value"> & {
57
+ value: string;
58
+ };
59
+ declare function ScrollSpyContent({ className, value, style, tabIndex, ...props }: ScrollSpyContentProps): React.JSX.Element;
60
+ //#endregion
61
+ export { ScrollSpy, ScrollSpyContent, type ScrollSpyContentProps, ScrollSpyList, type ScrollSpyListProps, type ScrollSpyProps, ScrollSpyTrigger, type ScrollSpyTriggerProps, scrollSpyTriggerClassName };
@@ -0,0 +1,295 @@
1
+ "use client";
2
+ import { t as cn } from "./cn-BI_4DMBf.js";
3
+ import { useComponentSize } from "./size-context.js";
4
+ import { t as useReducedMotion } from "./use-reduced-motion-BDvOK14x.js";
5
+ import { t as useUncontrolled } from "./use-uncontrolled-CLfM1XbB.js";
6
+ import { resolveTabsListVariant, tabsIndicatorClassName, tabsListVariants, tabsTriggerClassName } from "./tabs.js";
7
+ import { jsx, jsxs } from "react/jsx-runtime";
8
+ import * as React from "react";
9
+ //#region src/scroll-spy.tsx
10
+ const ScrollSpyContext = React.createContext(null);
11
+ /**
12
+ * The element this spy's sections actually scroll inside — a scrollable
13
+ * ancestor, or `document.scrollingElement` for the page. Everything is
14
+ * measured against it, so each instance answers only to its own scrollport:
15
+ * a spy in an open Sheet and a spy on the page behind it never see each
16
+ * other's scrolling.
17
+ *
18
+ * Returns `null` when nothing scrolls. A `position: fixed` ancestor with no
19
+ * scrollable element inside it — a short Dialog, say — is pinned to the
20
+ * viewport, so it neither moves with the page nor scrolls on its own. Falling
21
+ * back to the page there would read the page's scroll position and pin the
22
+ * last section active for a reader who never scrolled anything.
23
+ *
24
+ * Walked from a section rather than the root: the root often sits *outside*
25
+ * the scrolling region, as it does when a Sheet puts the list in its header
26
+ * and the sections in its scrolling body.
27
+ */
28
+ function getScrollport(element) {
29
+ let node = element;
30
+ while (node && node !== document.documentElement) {
31
+ const { overflowY, position } = getComputedStyle(node);
32
+ if (overflowY === "auto" || overflowY === "scroll") return node;
33
+ if (position === "fixed") return null;
34
+ node = node.parentElement;
35
+ }
36
+ return document.scrollingElement;
37
+ }
38
+ function useScrollSpyContext(part) {
39
+ const context = React.useContext(ScrollSpyContext);
40
+ if (!context) throw new Error(`\`${part}\` must be rendered inside \`ScrollSpy\`.`);
41
+ return context;
42
+ }
43
+ /**
44
+ * `Tabs` for a page that shows everything at once: every section stays mounted
45
+ * and visible, the triggers scroll to their section, and the active trigger
46
+ * tracks whichever section the reader is currently on.
47
+ *
48
+ * The parts mirror the `Tabs` API — `ScrollSpy` / `ScrollSpyList` /
49
+ * `ScrollSpyTrigger` / `ScrollSpyContent`, matched by `value` — and the list
50
+ * takes the same `variant` and `size` as `TabsList`, so the two read as one
51
+ * family. Overflow ("More" dropdowns, per-breakpoint trigger counts) is the
52
+ * consuming app's job, exactly as it is with `Tabs`;
53
+ * `scrollSpyTriggerClassName` is exported so an overflow button can match the
54
+ * real triggers.
55
+ */
56
+ function ScrollSpy({ className, orientation = "horizontal", value, defaultValue, onValueChange, scrollOffset = 0, children, ...props }) {
57
+ const baseId = React.useId();
58
+ const reducedMotion = useReducedMotion();
59
+ const [activeValue, setActiveValue] = useUncontrolled({
60
+ value,
61
+ defaultValue,
62
+ onChange: (next) => {
63
+ if (next !== void 0) onValueChange?.(next);
64
+ }
65
+ });
66
+ const sectionsRef = React.useRef(/* @__PURE__ */ new Map());
67
+ const [sectionsVersion, setSectionsVersion] = React.useState(0);
68
+ const activeRef = React.useRef(activeValue);
69
+ const commitRef = React.useRef(setActiveValue);
70
+ React.useEffect(() => {
71
+ activeRef.current = activeValue;
72
+ commitRef.current = setActiveValue;
73
+ });
74
+ const registerSection = React.useCallback((sectionValue, element) => {
75
+ if (element) sectionsRef.current.set(sectionValue, element);
76
+ else sectionsRef.current.delete(sectionValue);
77
+ setSectionsVersion((version) => version + 1);
78
+ }, []);
79
+ React.useEffect(() => {
80
+ let frame = 0;
81
+ const [firstSection] = sectionsRef.current.values();
82
+ const scrollport = getScrollport(firstSection ?? null);
83
+ const isPage = scrollport === document.scrollingElement;
84
+ const measure = () => {
85
+ frame = 0;
86
+ const sections = [...sectionsRef.current.entries()].map(([sectionValue, element]) => ({
87
+ value: sectionValue,
88
+ top: element.getBoundingClientRect().top
89
+ })).sort((a, b) => a.top - b.top);
90
+ const first = sections[0];
91
+ const last = sections.at(-1);
92
+ if (!(first && last)) return;
93
+ const originTop = scrollport && !isPage ? scrollport.getBoundingClientRect().top : 0;
94
+ const atBottom = scrollport ? scrollport.scrollTop + scrollport.clientHeight >= scrollport.scrollHeight - 2 : false;
95
+ let next = first.value;
96
+ if (atBottom) next = last.value;
97
+ else for (const section of sections) if (section.top - originTop <= scrollOffset + 2) next = section.value;
98
+ if (next !== activeRef.current) {
99
+ activeRef.current = next;
100
+ commitRef.current(next);
101
+ }
102
+ };
103
+ const schedule = () => {
104
+ if (!frame) frame = requestAnimationFrame(measure);
105
+ };
106
+ measure();
107
+ const scrollTarget = isPage ? window : scrollport;
108
+ scrollTarget?.addEventListener("scroll", schedule, { passive: true });
109
+ window.addEventListener("resize", schedule, { passive: true });
110
+ return () => {
111
+ if (frame) cancelAnimationFrame(frame);
112
+ scrollTarget?.removeEventListener("scroll", schedule);
113
+ window.removeEventListener("resize", schedule);
114
+ };
115
+ }, [scrollOffset, sectionsVersion]);
116
+ const scrollToSection = React.useCallback((sectionValue) => {
117
+ const element = sectionsRef.current.get(sectionValue);
118
+ if (!element) return;
119
+ activeRef.current = sectionValue;
120
+ commitRef.current(sectionValue);
121
+ element.scrollIntoView({
122
+ behavior: reducedMotion ? "auto" : "smooth",
123
+ block: "start"
124
+ });
125
+ element.focus({ preventScroll: true });
126
+ }, [reducedMotion]);
127
+ const context = React.useMemo(() => ({
128
+ activeValue,
129
+ scrollOffset,
130
+ registerSection,
131
+ scrollToSection,
132
+ getTriggerId: (sectionValue) => `${baseId}-trigger-${sectionValue}`,
133
+ getSectionId: (sectionValue) => `${baseId}-section-${sectionValue}`
134
+ }), [
135
+ activeValue,
136
+ baseId,
137
+ registerSection,
138
+ scrollOffset,
139
+ scrollToSection
140
+ ]);
141
+ return /* @__PURE__ */ jsx(ScrollSpyContext.Provider, {
142
+ value: context,
143
+ children: /* @__PURE__ */ jsx("div", {
144
+ "data-slot": "scroll-spy",
145
+ className: cn("group/tabs flex gap-3 data-[orientation=horizontal]:flex-col", className),
146
+ "data-orientation": orientation,
147
+ ...props,
148
+ children
149
+ })
150
+ });
151
+ }
152
+ /**
153
+ * Keeps `element` inside `list`'s own scrollport, on whichever axis actually
154
+ * overflows. Deliberately not `scrollIntoView`: that walks every scrollable
155
+ * ancestor, and the page is the very thing driving the active section here —
156
+ * nudging it back would fight the reader mid-scroll. This touches only the
157
+ * list's `scrollLeft`/`scrollTop`.
158
+ *
159
+ * "Nearest" semantics with a margin: an already-visible trigger doesn't move,
160
+ * and one scrolled to leaves a sliver of its neighbour showing rather than
161
+ * sitting flush against the edge.
162
+ */
163
+ const FOLLOW_MARGIN = 24;
164
+ function followActiveTrigger(list, element, behavior) {
165
+ const axes = [{
166
+ overflows: list.scrollWidth > list.clientWidth,
167
+ start: element.offsetLeft,
168
+ size: element.offsetWidth,
169
+ view: list.scrollLeft,
170
+ viewSize: list.clientWidth,
171
+ key: "left"
172
+ }, {
173
+ overflows: list.scrollHeight > list.clientHeight,
174
+ start: element.offsetTop,
175
+ size: element.offsetHeight,
176
+ view: list.scrollTop,
177
+ viewSize: list.clientHeight,
178
+ key: "top"
179
+ }];
180
+ const options = { behavior };
181
+ for (const axis of axes) {
182
+ if (!axis.overflows) continue;
183
+ const end = axis.start + axis.size;
184
+ const viewEnd = axis.view + axis.viewSize;
185
+ if (axis.start < axis.view) options[axis.key] = Math.max(0, axis.start - FOLLOW_MARGIN);
186
+ else if (end > viewEnd) options[axis.key] = end - axis.viewSize + FOLLOW_MARGIN;
187
+ }
188
+ if (options.left !== void 0 || options.top !== void 0) list.scrollTo(options);
189
+ }
190
+ function ScrollSpyList({ className, variant = "underline", size, children, scrollActiveIntoView = false, "aria-label": ariaLabel = "Section navigation", ...props }) {
191
+ const { activeValue } = useScrollSpyContext("ScrollSpyList");
192
+ const reducedMotion = useReducedMotion();
193
+ const hasFollowedRef = React.useRef(false);
194
+ const resolvedVariant = resolveTabsListVariant(variant);
195
+ const resolvedSize = useComponentSize(size);
196
+ const listRef = React.useRef(null);
197
+ const [indicator, setIndicator] = React.useState(null);
198
+ React.useEffect(() => {
199
+ const list = listRef.current;
200
+ if (!list) return;
201
+ const measure = () => {
202
+ const active = list.querySelector("[data-slot=\"scroll-spy-trigger\"][data-active]");
203
+ const next = active ? {
204
+ left: active.offsetLeft,
205
+ top: active.offsetTop,
206
+ width: active.offsetWidth,
207
+ height: active.offsetHeight
208
+ } : null;
209
+ setIndicator((current) => current?.left === next?.left && current?.top === next?.top && current?.width === next?.width && current?.height === next?.height ? current : next);
210
+ };
211
+ measure();
212
+ if (typeof ResizeObserver === "undefined") return;
213
+ const observer = new ResizeObserver(measure);
214
+ observer.observe(list);
215
+ for (const trigger of list.querySelectorAll("[data-slot=\"scroll-spy-trigger\"]")) observer.observe(trigger);
216
+ return () => observer.disconnect();
217
+ }, [activeValue]);
218
+ React.useEffect(() => {
219
+ const list = listRef.current;
220
+ if (!(scrollActiveIntoView && list)) return;
221
+ const active = list.querySelector("[data-slot=\"scroll-spy-trigger\"][data-active]");
222
+ if (!active) return;
223
+ const behavior = reducedMotion || !hasFollowedRef.current ? "auto" : "smooth";
224
+ hasFollowedRef.current = true;
225
+ followActiveTrigger(list, active, behavior);
226
+ }, [
227
+ activeValue,
228
+ reducedMotion,
229
+ scrollActiveIntoView
230
+ ]);
231
+ return /* @__PURE__ */ jsxs("nav", {
232
+ ref: listRef,
233
+ "data-slot": "scroll-spy-list",
234
+ "data-variant": resolvedVariant,
235
+ "data-size": resolvedSize,
236
+ "aria-label": ariaLabel,
237
+ className: cn(tabsListVariants({ variant: resolvedVariant }), className),
238
+ style: indicator ? {
239
+ "--active-tab-left": `${indicator.left}px`,
240
+ "--active-tab-top": `${indicator.top}px`,
241
+ "--active-tab-width": `${indicator.width}px`,
242
+ "--active-tab-height": `${indicator.height}px`
243
+ } : void 0,
244
+ ...props,
245
+ children: [children, indicator ? /* @__PURE__ */ jsx("span", {
246
+ "aria-hidden": true,
247
+ className: tabsIndicatorClassName
248
+ }) : null]
249
+ });
250
+ }
251
+ /**
252
+ * Trigger styling, exported so an overflow "More" button outside the list can
253
+ * render the same look — the `Tabs` trigger classes verbatim, so the two
254
+ * components stay pixel-identical.
255
+ */
256
+ const scrollSpyTriggerClassName = tabsTriggerClassName;
257
+ function ScrollSpyTrigger({ className, value, type = "button", onClick, ...props }) {
258
+ const { activeValue, getSectionId, getTriggerId, scrollToSection } = useScrollSpyContext("ScrollSpyTrigger");
259
+ const isActive = activeValue === value;
260
+ return /* @__PURE__ */ jsx("button", {
261
+ "data-slot": "scroll-spy-trigger",
262
+ id: getTriggerId(value),
263
+ type,
264
+ "aria-controls": getSectionId(value),
265
+ "aria-current": isActive ? "location" : void 0,
266
+ "data-active": isActive ? "" : void 0,
267
+ className: cn(scrollSpyTriggerClassName, "cursor-pointer", className),
268
+ onClick: (event) => {
269
+ onClick?.(event);
270
+ if (!event.defaultPrevented) scrollToSection(value);
271
+ },
272
+ ...props
273
+ });
274
+ }
275
+ function ScrollSpyContent({ className, value, style, tabIndex = -1, ...props }) {
276
+ const { activeValue, getSectionId, getTriggerId, registerSection, scrollOffset } = useScrollSpyContext("ScrollSpyContent");
277
+ return /* @__PURE__ */ jsx("section", {
278
+ ref: React.useCallback((element) => {
279
+ registerSection(value, element);
280
+ }, [registerSection, value]),
281
+ "data-slot": "scroll-spy-content",
282
+ "data-active": activeValue === value ? "" : void 0,
283
+ id: getSectionId(value),
284
+ "aria-labelledby": getTriggerId(value),
285
+ tabIndex,
286
+ className: cn("flex-1 text-sm outline-none", className),
287
+ style: {
288
+ scrollMarginTop: scrollOffset,
289
+ ...style
290
+ },
291
+ ...props
292
+ });
293
+ }
294
+ //#endregion
295
+ export { ScrollSpy, ScrollSpyContent, ScrollSpyList, ScrollSpyTrigger, scrollSpyTriggerClassName };