@giddaa-housing/ui 3.5.0 → 3.7.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.
Files changed (108) hide show
  1. package/css/components.css +13 -0
  2. package/css/generated/shared.css +1 -1
  3. package/css/theme.css +11 -8
  4. package/dist/badge.d.ts +5 -1
  5. package/dist/badge.js +19 -5
  6. package/dist/bar-chart.d.ts +1 -1
  7. package/dist/bullet-chart.d.ts +1 -1
  8. package/dist/button-BS5KCibF.d.ts +33 -0
  9. package/dist/button-group.d.ts +1 -1
  10. package/dist/button.d.ts +1 -1
  11. package/dist/button.js +49 -21
  12. package/dist/calendar.js +1 -1
  13. package/dist/carousel.d.ts +4 -2
  14. package/dist/carousel.js +11 -14
  15. package/dist/chart.d.ts +1 -1
  16. package/dist/chat.d.ts +1 -1
  17. package/dist/checkbox.d.ts +2 -2
  18. package/dist/checkbox.js +20 -6
  19. package/dist/{combobox-B7cKFJ9V.js → combobox-D70DFHoM.js} +55 -10
  20. package/dist/combobox.d.ts +8 -3
  21. package/dist/combobox.js +2 -2
  22. package/dist/cta.d.ts +1 -1
  23. package/dist/data-table.d.ts +2 -2
  24. package/dist/date-picker.d.ts +3 -3
  25. package/dist/date-picker.js +1 -1
  26. package/dist/dialog.d.ts +6 -2
  27. package/dist/dialog.js +15 -12
  28. package/dist/dropdown-menu.d.ts +1 -1
  29. package/dist/field.d.ts +4 -1
  30. package/dist/field.js +28 -11
  31. package/dist/footer.d.ts +2 -2
  32. package/dist/funnel-chart.d.ts +1 -1
  33. package/dist/heatmap-chart.d.ts +1 -1
  34. package/dist/infotip.js +0 -1
  35. package/dist/input-group.d.ts +6 -6
  36. package/dist/input-group.js +43 -18
  37. package/dist/input-otp.d.ts +2 -2
  38. package/dist/input-otp.js +31 -14
  39. package/dist/input-size-context-BMhves-J.d.ts +13 -0
  40. package/dist/input-size-context.d.ts +2 -2
  41. package/dist/input-size-context.js +9 -2
  42. package/dist/input.d.ts +20 -2
  43. package/dist/input.js +30 -11
  44. package/dist/line-chart.d.ts +1 -1
  45. package/dist/mask-input.d.ts +1 -1
  46. package/dist/match.d.ts +14 -2
  47. package/dist/match.js +221 -48
  48. package/dist/media-gallery-hero.js +9 -7
  49. package/dist/media-player.js +3 -3
  50. package/dist/mobile-sidebar.js +7 -6
  51. package/dist/number-input.d.ts +2 -2
  52. package/dist/number-input.js +2 -2
  53. package/dist/pagination.d.ts +1 -1
  54. package/dist/phone-input.d.ts +20 -3
  55. package/dist/phone-input.js +137 -67
  56. package/dist/picker-BTXBrsl4.js +62 -0
  57. package/dist/{picker-CsnHtbe4.d.ts → picker-j3txxA5t.d.ts} +2 -2
  58. package/dist/pie-chart.d.ts +1 -1
  59. package/dist/{popover-DSlPeH7x.d.ts → popover-CNymD33m.d.ts} +1 -1
  60. package/dist/popover.d.ts +1 -1
  61. package/dist/property-listing-card.d.ts +7 -3
  62. package/dist/property-listing-card.js +25 -8
  63. package/dist/radar-chart.d.ts +1 -1
  64. package/dist/radio-group.d.ts +2 -2
  65. package/dist/radio-group.js +23 -9
  66. package/dist/rating.d.ts +60 -0
  67. package/dist/rating.js +187 -0
  68. package/dist/rich-text-editor.d.ts +1 -1
  69. package/dist/scatter-chart.d.ts +1 -1
  70. package/dist/scroll-spy.d.ts +8 -2
  71. package/dist/scroll-spy.js +13 -13
  72. package/dist/section-jumper.d.ts +31 -0
  73. package/dist/section-jumper.js +276 -0
  74. package/dist/select.d.ts +9 -4
  75. package/dist/select.js +148 -20
  76. package/dist/sheet.d.ts +5 -1
  77. package/dist/sheet.js +15 -12
  78. package/dist/sidebar.d.ts +2 -2
  79. package/dist/sidebar.js +1 -0
  80. package/dist/size-context-BVhHduLi.d.ts +33 -0
  81. package/dist/size-context.d.ts +2 -2
  82. package/dist/size-context.js +30 -1
  83. package/dist/slider.d.ts +5 -3
  84. package/dist/slider.js +25 -10
  85. package/dist/sonar.d.ts +4 -2
  86. package/dist/sonar.js +116 -28
  87. package/dist/sparkline.d.ts +1 -1
  88. package/dist/styles.css +1674 -252
  89. package/dist/switch.d.ts +2 -2
  90. package/dist/switch.js +20 -6
  91. package/dist/tabs.d.ts +1 -1
  92. package/dist/tag.d.ts +1 -1
  93. package/dist/textarea.d.ts +17 -1
  94. package/dist/textarea.js +23 -9
  95. package/dist/time-picker.d.ts +3 -3
  96. package/dist/time-picker.js +1 -1
  97. package/dist/tree-map.d.ts +1 -1
  98. package/dist/video-player-dialog.js +4 -3
  99. package/dist/waterfall-chart.d.ts +1 -1
  100. package/package.json +9 -1
  101. package/dist/button-DW5OijoR.d.ts +0 -17
  102. package/dist/dialog-nesting.d.ts +0 -9
  103. package/dist/dialog-nesting.js +0 -27
  104. package/dist/input-CfoEJ8A6.d.ts +0 -12
  105. package/dist/input-size-context-C-SFx9sx.d.ts +0 -10
  106. package/dist/picker-DmIiQKLJ.js +0 -45
  107. package/dist/size-context-BX6kAdVj.d.ts +0 -11
  108. package/dist/textarea-IS_D8pXf.d.ts +0 -11
package/dist/rating.js ADDED
@@ -0,0 +1,187 @@
1
+ "use client";
2
+ import { Star } from "./icons.js";
3
+ import { cn } from "./utils/cn.js";
4
+ import { responsiveValueClasses, useComponentSizeValue } from "./size-context.js";
5
+ import { useUncontrolled } from "./utils/use-uncontrolled.js";
6
+ import { Fragment, jsx, jsxs } from "react/jsx-runtime";
7
+ import * as React from "react";
8
+ //#region src/rating.tsx
9
+ const ratingIconSizes = {
10
+ sm: "size-4",
11
+ md: "size-5",
12
+ lg: "size-6"
13
+ };
14
+ const ratingGapSizes = {
15
+ sm: "gap-0.5",
16
+ md: "gap-1",
17
+ lg: "gap-1.5"
18
+ };
19
+ const responsiveIconSizes = {
20
+ base: ratingIconSizes,
21
+ md: {
22
+ sm: "md:size-4",
23
+ md: "md:size-5",
24
+ lg: "md:size-6"
25
+ },
26
+ lg: {
27
+ sm: "lg:size-4",
28
+ md: "lg:size-5",
29
+ lg: "lg:size-6"
30
+ }
31
+ };
32
+ const responsiveGapSizes = {
33
+ base: ratingGapSizes,
34
+ md: {
35
+ sm: "md:gap-0.5",
36
+ md: "md:gap-1",
37
+ lg: "md:gap-1.5"
38
+ },
39
+ lg: {
40
+ sm: "lg:gap-0.5",
41
+ md: "lg:gap-1",
42
+ lg: "lg:gap-1.5"
43
+ }
44
+ };
45
+ function clampRating(value, max) {
46
+ if (!Number.isFinite(value)) return 0;
47
+ return Math.min(max, Math.max(0, value));
48
+ }
49
+ /**
50
+ * How much of the star at `index` (1-based) the value fills, as a percentage.
51
+ * Rounded because `4.3 - 4` is `0.30000000000000027` in binary floating point,
52
+ * and that number should not reach the DOM as a width.
53
+ */
54
+ function getIconFill(index, value) {
55
+ const fill = Math.min(1, Math.max(0, value - (index - 1)));
56
+ return Math.round(fill * 1e4) / 100;
57
+ }
58
+ function formatRatingValue(value) {
59
+ return Number.isInteger(value) ? String(value) : value.toFixed(1);
60
+ }
61
+ function defaultItemLabel(value, max) {
62
+ return `${formatRatingValue(value)} of ${max}`;
63
+ }
64
+ /** Every value a reader can pick, low to high: `[0.5, 1, 1.5, …]` or `[1, 2, …]`. */
65
+ function getSelectableValues(max, precision) {
66
+ const steps = Math.round(max / precision);
67
+ return Array.from({ length: steps }, (_, step) => (step + 1) * precision);
68
+ }
69
+ function RatingIconPair({ fill, icon: Icon, sizeClassName }) {
70
+ return /* @__PURE__ */ jsxs(Fragment, { children: [/* @__PURE__ */ jsx(Icon, {
71
+ "aria-hidden": "true",
72
+ "data-slot": "rating-icon",
73
+ className: cn("block shrink-0 text-line-strong transition-colors", sizeClassName)
74
+ }), fill > 0 ? /* @__PURE__ */ jsx("span", {
75
+ "aria-hidden": "true",
76
+ "data-slot": "rating-icon-fill",
77
+ className: "pointer-events-none absolute inset-y-0 left-0 overflow-hidden text-line-focus",
78
+ style: { width: `${fill}%` },
79
+ children: /* @__PURE__ */ jsx(Icon, {
80
+ fill: "currentColor",
81
+ className: cn("block max-w-none shrink-0 transition-colors", sizeClassName)
82
+ })
83
+ }) : null] });
84
+ }
85
+ /**
86
+ * A star rating that reads and writes.
87
+ *
88
+ * Interactive ratings are a group of real radio inputs — one per selectable
89
+ * value — visually replaced by the icons. That is deliberate: it buys native
90
+ * arrow-key navigation, native form submission under `name`, and the
91
+ * "3 of 5, radio button 3 of 5" announcement, none of which a div with click
92
+ * handlers gets for free.
93
+ *
94
+ * ```tsx
95
+ * <Rating defaultValue={4} onValueChange={setRating} />
96
+ * <Rating value={4.3} readOnly />
97
+ * ```
98
+ */
99
+ function Rating({ value, defaultValue, onValueChange, max = 5, precision = 1, size, readOnly = false, disabled = false, required = false, name, id, itemLabel = defaultItemLabel, icon = Star, iconClassName, className, onBlur, ref, "aria-label": ariaLabel, "aria-labelledby": ariaLabelledBy, "aria-describedby": ariaDescribedBy, "aria-invalid": ariaInvalid }) {
100
+ const generatedName = React.useId();
101
+ const [current, setCurrent] = useUncontrolled({
102
+ value,
103
+ defaultValue,
104
+ finalValue: 0,
105
+ onChange: onValueChange
106
+ });
107
+ const [preview, setPreview] = React.useState(null);
108
+ const sizeValue = useComponentSizeValue(size);
109
+ const iconSizes = responsiveValueClasses(sizeValue, responsiveIconSizes);
110
+ const gapSizes = responsiveValueClasses(sizeValue, responsiveGapSizes);
111
+ const iconBox = cn(iconSizes.className, iconClassName);
112
+ const interactive = !readOnly;
113
+ const displayed = clampRating(interactive && preview !== null ? preview : current, max);
114
+ const icons = Array.from({ length: max }, (_, index) => index + 1);
115
+ const rootProps = {
116
+ "data-slot": "rating",
117
+ "data-size": iconSizes.base,
118
+ "data-size-md": iconSizes.responsive ? iconSizes.md : void 0,
119
+ "data-size-lg": iconSizes.responsive ? iconSizes.lg : void 0,
120
+ className: cn("inline-flex w-fit shrink-0 items-center", gapSizes.className, className)
121
+ };
122
+ if (!interactive) return /* @__PURE__ */ jsx("span", {
123
+ ...rootProps,
124
+ ref,
125
+ id,
126
+ "aria-label": ariaLabel ?? itemLabel(displayed, max),
127
+ "data-readonly": "true",
128
+ role: "img",
129
+ children: icons.map((index) => /* @__PURE__ */ jsx("span", {
130
+ className: cn("relative inline-flex shrink-0", iconBox),
131
+ children: /* @__PURE__ */ jsx(RatingIconPair, {
132
+ fill: getIconFill(index, displayed),
133
+ icon,
134
+ sizeClassName: iconBox
135
+ })
136
+ }, index))
137
+ });
138
+ const groupName = name ?? generatedName;
139
+ const selectable = getSelectableValues(max, precision);
140
+ return /* @__PURE__ */ jsx("div", {
141
+ ...rootProps,
142
+ ref,
143
+ id,
144
+ role: "radiogroup",
145
+ "aria-label": ariaLabelledBy ? void 0 : ariaLabel ?? "Rating",
146
+ "aria-labelledby": ariaLabelledBy,
147
+ "aria-describedby": ariaDescribedBy,
148
+ "aria-invalid": ariaInvalid || void 0,
149
+ "aria-required": required || void 0,
150
+ "data-disabled": disabled || void 0,
151
+ onBlur,
152
+ onPointerLeave: () => setPreview(null),
153
+ className: cn(rootProps.className, disabled && "cursor-not-allowed opacity-60"),
154
+ children: icons.map((index) => {
155
+ const values = selectable.filter((item) => item > index - 1 && item <= index);
156
+ return /* @__PURE__ */ jsxs("span", {
157
+ "data-slot": "rating-item",
158
+ className: cn("relative inline-flex shrink-0 transition-[scale] duration-[var(--duration-motion-press)] ease-gdt-out", !disabled && "active:scale-[0.97] motion-reduce:active:scale-100", iconBox),
159
+ children: [/* @__PURE__ */ jsx(RatingIconPair, {
160
+ fill: getIconFill(index, displayed),
161
+ icon,
162
+ sizeClassName: iconBox
163
+ }), values.map((item, position) => /* @__PURE__ */ jsx("label", {
164
+ "data-slot": "rating-control",
165
+ "data-value": item,
166
+ onPointerEnter: () => {
167
+ if (!disabled) setPreview(item);
168
+ },
169
+ className: cn("absolute inset-y-0 rounded-[3px]", "has-[:focus-visible]:ring-2 has-[:focus-visible]:ring-line-focus has-[:focus-visible]:ring-offset-2 has-[:focus-visible]:ring-offset-canvas", disabled ? "cursor-not-allowed" : "cursor-pointer", values.length === 1 ? "left-0 w-full" : position === 0 ? "left-0 w-1/2" : "right-0 w-1/2"),
170
+ children: /* @__PURE__ */ jsx("input", {
171
+ type: "radio",
172
+ className: "sr-only",
173
+ name: groupName,
174
+ value: item,
175
+ checked: current === item,
176
+ disabled,
177
+ required: required && item === selectable[0],
178
+ "aria-label": itemLabel(item, max),
179
+ onChange: () => setCurrent(item)
180
+ })
181
+ }, item))]
182
+ }, index);
183
+ })
184
+ });
185
+ }
186
+ //#endregion
187
+ export { Rating };
@@ -1,4 +1,4 @@
1
- import { t as ComponentSize } from "./size-context-BX6kAdVj.js";
1
+ import { t as ComponentSize } from "./size-context-BVhHduLi.js";
2
2
  import * as React from "react";
3
3
  //#region src/rich-text-editor.d.ts
4
4
  interface RichTextEditorProps {
@@ -1,4 +1,4 @@
1
- import { t as ComponentSize } from "./size-context-BX6kAdVj.js";
1
+ import { t as ComponentSize } from "./size-context-BVhHduLi.js";
2
2
  import { ChartConfig } from "./chart.js";
3
3
  import * as React from "react";
4
4
  import { CartesianGrid, Scatter, ScatterChart as ScatterChart$1, XAxis, YAxis } from "recharts";
@@ -1,4 +1,4 @@
1
- import { t as ComponentSize } from "./size-context-BX6kAdVj.js";
1
+ import { t as ComponentSize } from "./size-context-BVhHduLi.js";
2
2
  import { TabsListVariant } from "./tabs.js";
3
3
  import * as React from "react";
4
4
  //#region src/scroll-spy.d.ts
@@ -11,6 +11,12 @@ import * as React from "react";
11
11
  * or `Popover` works without threading anything down by hand. `registerSection`
12
12
  * is deliberately not exposed: sections are `ScrollSpyContent`'s to own.
13
13
  */
14
+ declare function useOptionalScrollSpy(): {
15
+ activeValue: string | undefined;
16
+ getSectionId: (value: string) => string;
17
+ getTriggerId: (value: string) => string;
18
+ scrollToSection: (value: string) => void;
19
+ } | null;
14
20
  declare function useScrollSpy(): {
15
21
  activeValue: string | undefined;
16
22
  getSectionId: (value: string) => string;
@@ -73,4 +79,4 @@ type ScrollSpyContentProps = Omit<React.ComponentProps<"section">, "value"> & {
73
79
  };
74
80
  declare function ScrollSpyContent({ className, value, style, tabIndex, ...props }: ScrollSpyContentProps): React.JSX.Element;
75
81
  //#endregion
76
- export { ScrollSpy, ScrollSpyContent, type ScrollSpyContentProps, ScrollSpyList, type ScrollSpyListProps, type ScrollSpyProps, ScrollSpyTrigger, type ScrollSpyTriggerProps, scrollSpyTriggerClassName, useScrollSpy };
82
+ export { ScrollSpy, ScrollSpyContent, type ScrollSpyContentProps, ScrollSpyList, type ScrollSpyListProps, type ScrollSpyProps, ScrollSpyTrigger, type ScrollSpyTriggerProps, scrollSpyTriggerClassName, useOptionalScrollSpy, useScrollSpy };
@@ -49,19 +49,19 @@ function useScrollSpyContext(part) {
49
49
  * or `Popover` works without threading anything down by hand. `registerSection`
50
50
  * is deliberately not exposed: sections are `ScrollSpyContent`'s to own.
51
51
  */
52
+ function useOptionalScrollSpy() {
53
+ const context = React.useContext(ScrollSpyContext);
54
+ return React.useMemo(() => context ? {
55
+ activeValue: context.activeValue,
56
+ getSectionId: context.getSectionId,
57
+ getTriggerId: context.getTriggerId,
58
+ scrollToSection: context.scrollToSection
59
+ } : null, [context]);
60
+ }
52
61
  function useScrollSpy() {
53
- const { activeValue, getSectionId, getTriggerId, scrollToSection } = useScrollSpyContext("useScrollSpy");
54
- return React.useMemo(() => ({
55
- activeValue,
56
- getSectionId,
57
- getTriggerId,
58
- scrollToSection
59
- }), [
60
- activeValue,
61
- getSectionId,
62
- getTriggerId,
63
- scrollToSection
64
- ]);
62
+ const value = useOptionalScrollSpy();
63
+ if (!value) throw new Error("`useScrollSpy` must be used inside `ScrollSpy`.");
64
+ return value;
65
65
  }
66
66
  /**
67
67
  * `Tabs` for a page that shows everything at once: every section stays mounted
@@ -315,4 +315,4 @@ function ScrollSpyContent({ className, value, style, tabIndex = -1, ...props })
315
315
  });
316
316
  }
317
317
  //#endregion
318
- export { ScrollSpy, ScrollSpyContent, ScrollSpyList, ScrollSpyTrigger, scrollSpyTriggerClassName, useScrollSpy };
318
+ export { ScrollSpy, ScrollSpyContent, ScrollSpyList, ScrollSpyTrigger, scrollSpyTriggerClassName, useOptionalScrollSpy, useScrollSpy };
@@ -0,0 +1,31 @@
1
+ import * as React from "react";
2
+ //#region src/section-jumper.d.ts
3
+ type SectionJumperItem = {
4
+ value: string;
5
+ label: React.ReactNode;
6
+ /** DOM id of a standalone destination. Defaults to `value`. */
7
+ targetId?: string;
8
+ };
9
+ type SectionJumperProviderProps = React.ComponentProps<"div"> & {
10
+ /** Heading selectors to discover inside the provider. */
11
+ selectors?: string[];
12
+ /** Prefix used for IDs generated on headings that do not already have one. */
13
+ idPrefix?: string;
14
+ /** Sticky-header offset used when tracking and scrolling to headings. */
15
+ scrollOffset?: number;
16
+ };
17
+ declare function SectionJumperProvider({ children, selectors, idPrefix, scrollOffset, ...props }: SectionJumperProviderProps): React.JSX.Element;
18
+ type SectionJumperProps = Omit<React.ComponentProps<"div">, "children"> & {
19
+ /** Explicit sections. Omit inside `SectionJumperProvider` to use discovered headings. */
20
+ items?: SectionJumperItem[];
21
+ label?: string;
22
+ open?: boolean;
23
+ defaultOpen?: boolean;
24
+ onOpenChange?: (open: boolean) => void;
25
+ mobileBreakpoint?: number;
26
+ /** Sticky-header offset used by standalone target-id mode. */
27
+ scrollOffset?: number;
28
+ };
29
+ declare function SectionJumper({ className, items: itemsProp, label, open, defaultOpen, onOpenChange, mobileBreakpoint, scrollOffset: scrollOffsetProp, ...props }: SectionJumperProps): React.JSX.Element | null;
30
+ //#endregion
31
+ export { SectionJumper, type SectionJumperItem, type SectionJumperProps, SectionJumperProvider, type SectionJumperProviderProps };
@@ -0,0 +1,276 @@
1
+ "use client";
2
+ import { ChevronDown } from "./icons.js";
3
+ import { cn } from "./utils/cn.js";
4
+ import { useReducedMotion } from "./utils/use-reduced-motion.js";
5
+ import { Popover, PopoverContent, PopoverTrigger } from "./popover.js";
6
+ import { useOptionalScrollSpy } from "./scroll-spy.js";
7
+ import { useIsMobile } from "./utils/use-is-mobile.js";
8
+ import { jsx, jsxs } from "react/jsx-runtime";
9
+ import * as React from "react";
10
+ //#region src/section-jumper.tsx
11
+ const SectionJumperProviderContext = React.createContext(null);
12
+ function SectionJumperProvider({ children, selectors = ["h2"], idPrefix = "section-jumper-heading", scrollOffset = 0, ...props }) {
13
+ const rootRef = React.useRef(null);
14
+ const [items, setItems] = React.useState([]);
15
+ const selector = selectors.join(",");
16
+ React.useEffect(() => {
17
+ const root = rootRef.current;
18
+ if (!root || !selector) return;
19
+ const generatedIds = /* @__PURE__ */ new Set();
20
+ const discover = () => {
21
+ const usedIds = /* @__PURE__ */ new Set();
22
+ const next = [...root.querySelectorAll(selector)].reduce((result, heading, index) => {
23
+ const label = heading.textContent?.trim();
24
+ if (!label) return result;
25
+ let targetId = heading.id;
26
+ if (!targetId) {
27
+ targetId = `${idPrefix}-${index + 1}`;
28
+ let suffix = 2;
29
+ while (usedIds.has(targetId) || document.getElementById(targetId) && document.getElementById(targetId) !== heading) targetId = `${idPrefix}-${index + 1}-${suffix++}`;
30
+ heading.id = targetId;
31
+ generatedIds.add(targetId);
32
+ }
33
+ usedIds.add(targetId);
34
+ result.push({
35
+ value: targetId,
36
+ label,
37
+ targetId
38
+ });
39
+ return result;
40
+ }, []);
41
+ setItems((current) => current.length === next.length && current.every((item, index) => item.value === next[index]?.value && item.label === next[index]?.label) ? current : next);
42
+ };
43
+ discover();
44
+ const observer = new MutationObserver(discover);
45
+ observer.observe(root, {
46
+ childList: true,
47
+ subtree: true,
48
+ characterData: true
49
+ });
50
+ return () => {
51
+ observer.disconnect();
52
+ for (const id of generatedIds) {
53
+ const heading = document.getElementById(id);
54
+ if (heading && root.contains(heading)) heading.removeAttribute("id");
55
+ }
56
+ };
57
+ }, [idPrefix, selector]);
58
+ const value = React.useMemo(() => ({
59
+ items,
60
+ scrollOffset
61
+ }), [items, scrollOffset]);
62
+ return /* @__PURE__ */ jsx(SectionJumperProviderContext.Provider, {
63
+ value,
64
+ children: /* @__PURE__ */ jsx("div", {
65
+ ref: rootRef,
66
+ "data-slot": "section-jumper-provider",
67
+ ...props,
68
+ children
69
+ })
70
+ });
71
+ }
72
+ function getSectionJumperScrollport(element) {
73
+ let parent = element.parentElement;
74
+ while (parent && parent !== document.documentElement) {
75
+ const { overflowY } = getComputedStyle(parent);
76
+ if (overflowY === "auto" || overflowY === "scroll") return parent;
77
+ parent = parent.parentElement;
78
+ }
79
+ return null;
80
+ }
81
+ function useStandaloneSectionJumper(items, scrollOffset, enabled) {
82
+ const reducedMotion = useReducedMotion();
83
+ const [activeValue, setActiveValue] = React.useState(items[0]?.value);
84
+ const itemsRef = React.useRef(items);
85
+ itemsRef.current = items;
86
+ const targetKey = items.map((item) => `${item.value}:${item.targetId ?? item.value}`).join("|");
87
+ React.useEffect(() => {
88
+ if (!enabled) return;
89
+ let frame = 0;
90
+ const measure = () => {
91
+ frame = 0;
92
+ const targets = itemsRef.current.map((item) => ({
93
+ value: item.value,
94
+ element: document.getElementById(item.targetId ?? item.value)
95
+ })).filter((target) => Boolean(target.element));
96
+ if (targets.length === 0) return;
97
+ const scrollport = getSectionJumperScrollport(targets[0].element);
98
+ const originTop = scrollport?.getBoundingClientRect().top ?? 0;
99
+ let next = targets[0]?.value;
100
+ for (const target of targets) if (target.element.getBoundingClientRect().top - originTop <= scrollOffset + 2) next = target.value;
101
+ const root = scrollport ?? document.scrollingElement;
102
+ if (root && root.scrollTop + root.clientHeight >= root.scrollHeight - 2) next = targets.at(-1)?.value ?? next;
103
+ setActiveValue(next);
104
+ };
105
+ const scheduleMeasure = () => {
106
+ if (!frame) frame = requestAnimationFrame(measure);
107
+ };
108
+ const firstTarget = itemsRef.current.map((item) => document.getElementById(item.targetId ?? item.value)).find((element) => Boolean(element));
109
+ const scrollTarget = (firstTarget ? getSectionJumperScrollport(firstTarget) : null) ?? window;
110
+ measure();
111
+ scrollTarget.addEventListener("scroll", scheduleMeasure, { passive: true });
112
+ window.addEventListener("resize", scheduleMeasure);
113
+ return () => {
114
+ if (frame) cancelAnimationFrame(frame);
115
+ scrollTarget.removeEventListener("scroll", scheduleMeasure);
116
+ window.removeEventListener("resize", scheduleMeasure);
117
+ };
118
+ }, [
119
+ enabled,
120
+ scrollOffset,
121
+ targetKey
122
+ ]);
123
+ const getTargetId = React.useCallback((value) => {
124
+ return itemsRef.current.find((candidate) => candidate.value === value)?.targetId ?? value;
125
+ }, []);
126
+ const scrollToSection = React.useCallback((value) => {
127
+ const element = document.getElementById(getTargetId(value));
128
+ if (!element) return;
129
+ setActiveValue(value);
130
+ const scrollport = getSectionJumperScrollport(element);
131
+ const behavior = reducedMotion ? "auto" : "smooth";
132
+ if (scrollport) scrollport.scrollTo({
133
+ top: element.getBoundingClientRect().top - scrollport.getBoundingClientRect().top + scrollport.scrollTop - scrollOffset,
134
+ behavior
135
+ });
136
+ else window.scrollTo({
137
+ top: element.getBoundingClientRect().top + window.scrollY - scrollOffset,
138
+ behavior
139
+ });
140
+ }, [
141
+ getTargetId,
142
+ reducedMotion,
143
+ scrollOffset
144
+ ]);
145
+ return React.useMemo(() => ({
146
+ activeValue,
147
+ getSectionId: getTargetId,
148
+ getTriggerId: (value) => `section-jumper-${value}`,
149
+ scrollToSection
150
+ }), [
151
+ activeValue,
152
+ getTargetId,
153
+ scrollToSection
154
+ ]);
155
+ }
156
+ function SectionJumperDot({ active, variant }) {
157
+ return /* @__PURE__ */ jsx("span", {
158
+ "aria-hidden": "true",
159
+ className: cn("block shrink-0 bg-line transition-[width,height,background-color] duration-(--duration-motion-surface) ease-gdt-out motion-reduce:transition-none", !active && "size-2 rounded-full", active && variant === "desktop" && "h-6 w-2 rounded-sm bg-surface-brand", active && variant === "menu" && "size-2 rounded-full bg-surface-brand", active && variant === "mobile" && "h-2 w-6 rounded-sm bg-surface-brand")
160
+ });
161
+ }
162
+ function SectionJumperMenu({ items, onSelect, controller }) {
163
+ const { activeValue, getSectionId, getTriggerId } = controller;
164
+ return /* @__PURE__ */ jsx("nav", {
165
+ "aria-label": "Page sections",
166
+ className: "flex flex-col gap-0.5",
167
+ children: items.map((item) => {
168
+ const active = item.value === activeValue;
169
+ return /* @__PURE__ */ jsxs("button", {
170
+ id: `${getTriggerId(item.value)}-jumper`,
171
+ type: "button",
172
+ "aria-controls": getSectionId(item.value),
173
+ "aria-current": active ? "location" : void 0,
174
+ "data-active": active ? "" : void 0,
175
+ className: cn("flex w-full cursor-pointer items-center gap-2.5 rounded-lg px-2.5 py-2 text-left text-gdt-sm font-medium text-fg-secondary outline-none transition-colors hover:bg-surface-raised focus-visible:shadow-[0_0_0_2px_var(--color-line-focus)]", active && "bg-surface-brand-subtle font-semibold text-fg-brand"),
176
+ onClick: () => onSelect(item.value),
177
+ children: [/* @__PURE__ */ jsx(SectionJumperDot, {
178
+ active,
179
+ variant: "menu"
180
+ }), /* @__PURE__ */ jsx("span", {
181
+ className: "min-w-0 flex-1 truncate",
182
+ children: item.label
183
+ })]
184
+ }, item.value);
185
+ })
186
+ });
187
+ }
188
+ function SectionJumper({ className, items: itemsProp, label = "On this page", open, defaultOpen = false, onOpenChange, mobileBreakpoint = 1024, scrollOffset: scrollOffsetProp, ...props }) {
189
+ const provider = React.useContext(SectionJumperProviderContext);
190
+ const items = itemsProp ?? provider?.items ?? [];
191
+ const scrollOffset = scrollOffsetProp ?? provider?.scrollOffset ?? 0;
192
+ const scrollSpy = useOptionalScrollSpy();
193
+ const hasExplicitTargets = items.some((item) => item.targetId !== void 0);
194
+ const standalone = useStandaloneSectionJumper(items, scrollOffset, hasExplicitTargets || !scrollSpy);
195
+ const controller = hasExplicitTargets || !scrollSpy ? standalone : scrollSpy;
196
+ const { activeValue, scrollToSection } = controller;
197
+ const isMobile = useIsMobile(mobileBreakpoint);
198
+ const [internalOpen, setInternalOpen] = React.useState(defaultOpen);
199
+ const resolvedOpen = open ?? internalOpen;
200
+ const activeItem = items.find((item) => item.value === activeValue);
201
+ const setOpen = React.useCallback((next) => {
202
+ if (open === void 0) setInternalOpen(next);
203
+ onOpenChange?.(next);
204
+ }, [onOpenChange, open]);
205
+ const select = React.useCallback((value) => {
206
+ scrollToSection(value);
207
+ setOpen(false);
208
+ }, [scrollToSection, setOpen]);
209
+ if (items.length === 0) return null;
210
+ const desktopTrigger = /* @__PURE__ */ jsx("button", {
211
+ type: "button",
212
+ "aria-label": `${label}${activeItem ? `, current section: ${String(activeItem.label)}` : ""}`,
213
+ className: "flex w-fit cursor-pointer flex-col items-center gap-3.5 rounded-2xl border border-transparent p-2 outline-none focus-visible:shadow-[0_0_0_2px_var(--color-line-focus)]",
214
+ children: items.map((item) => {
215
+ return /* @__PURE__ */ jsx(SectionJumperDot, {
216
+ active: item.value === activeValue,
217
+ variant: "desktop"
218
+ }, item.value);
219
+ })
220
+ });
221
+ const mobileTrigger = /* @__PURE__ */ jsxs("button", {
222
+ type: "button",
223
+ className: "flex cursor-pointer items-center gap-3 rounded-full border border-line-subtle bg-surface-overlay px-4 py-1 text-gdt-xs font-medium text-fg-brand shadow-2 outline-none focus-visible:shadow-[0_0_0_2px_var(--color-line-focus)]",
224
+ children: [
225
+ /* @__PURE__ */ jsx("span", {
226
+ "aria-hidden": true,
227
+ className: "flex items-center gap-1",
228
+ children: items.map((item) => /* @__PURE__ */ jsx(SectionJumperDot, {
229
+ active: item.value === activeValue,
230
+ variant: "mobile"
231
+ }, item.value))
232
+ }),
233
+ /* @__PURE__ */ jsx("span", {
234
+ className: "max-w-32 truncate",
235
+ children: activeItem?.label ?? label
236
+ }),
237
+ /* @__PURE__ */ jsx(ChevronDown, {
238
+ "aria-hidden": true,
239
+ className: cn("size-3 transition-transform", resolvedOpen && "rotate-180")
240
+ })
241
+ ]
242
+ });
243
+ return /* @__PURE__ */ jsx("div", {
244
+ "data-slot": "section-jumper",
245
+ className: cn("w-fit", className),
246
+ ...props,
247
+ children: /* @__PURE__ */ jsxs(Popover, {
248
+ open: resolvedOpen,
249
+ onOpenChange: setOpen,
250
+ children: [/* @__PURE__ */ jsx(PopoverTrigger, {
251
+ render: isMobile ? mobileTrigger : desktopTrigger,
252
+ openOnHover: !isMobile,
253
+ closeDelay: 100
254
+ }), /* @__PURE__ */ jsxs(PopoverContent, {
255
+ align: isMobile ? "center" : "end",
256
+ side: isMobile ? "top" : "left",
257
+ sideOffset: 8,
258
+ showArrow: false,
259
+ className: cn("border border-line-subtle shadow-2 px-4 pb-4 pt-3", isMobile ? "w-90 rounded-2xl" : "w-60 rounded-2xl"),
260
+ children: [/* @__PURE__ */ jsx("p", {
261
+ className: cn("border-b border-line-subtle pb-2 font-semibold text-fg-primary", isMobile ? "text-gdt-md" : "text-gdt-sm"),
262
+ children: label
263
+ }), /* @__PURE__ */ jsx("div", {
264
+ className: "pt-2",
265
+ children: /* @__PURE__ */ jsx(SectionJumperMenu, {
266
+ items,
267
+ onSelect: select,
268
+ controller
269
+ })
270
+ })]
271
+ })]
272
+ })
273
+ });
274
+ }
275
+ //#endregion
276
+ export { SectionJumper, SectionJumperProvider };
package/dist/select.d.ts CHANGED
@@ -1,8 +1,8 @@
1
- import { t as ComponentSize } from "./size-context-BX6kAdVj.js";
1
+ import { n as ComponentSizeValue } from "./size-context-BVhHduLi.js";
2
2
  import * as React from "react";
3
3
  import { Select as Select$1 } from "@base-ui/react/select";
4
4
  //#region src/select.d.ts
5
- type SelectSize = ComponentSize;
5
+ type SelectSize = ComponentSizeValue;
6
6
  declare function Select<Value, Multiple extends boolean | undefined = false>({ multiple, children, size, ...props }: Select$1.Root.Props<Value, Multiple> & {
7
7
  size?: SelectSize;
8
8
  }): React.JSX.Element;
@@ -11,11 +11,16 @@ declare function SelectValue({ className, ...props }: Select$1.Value.Props): Rea
11
11
  declare function SelectTrigger({ className, children, unstyled, ...props }: Select$1.Trigger.Props & {
12
12
  unstyled?: boolean;
13
13
  }): React.JSX.Element;
14
- declare function SelectContent({ className, children, side, sideOffset, align, alignOffset, alignItemWithTrigger, ...props }: Select$1.Popup.Props & Pick<Select$1.Positioner.Props, "align" | "alignOffset" | "side" | "sideOffset" | "alignItemWithTrigger">): React.JSX.Element;
14
+ declare function SelectContent({ className, children, header, footer, side, sideOffset, align, alignOffset, alignItemWithTrigger, ...props }: Select$1.Popup.Props & Pick<Select$1.Positioner.Props, "align" | "alignOffset" | "side" | "sideOffset" | "alignItemWithTrigger"> & {
15
+ header?: React.ReactNode;
16
+ footer?: React.ReactNode;
17
+ }): React.JSX.Element;
18
+ declare function SelectHeader({ className, type, ...props }: React.ComponentProps<"button">): React.JSX.Element;
19
+ declare function SelectFooter({ className, type, ...props }: React.ComponentProps<"button">): React.JSX.Element;
15
20
  declare function SelectLabel({ className, ...props }: Select$1.GroupLabel.Props): React.JSX.Element;
16
21
  declare function SelectItem({ className, children, ...props }: Select$1.Item.Props): React.JSX.Element;
17
22
  declare function SelectSeparator({ className, ...props }: Select$1.Separator.Props): React.JSX.Element;
18
23
  declare function SelectScrollUpButton({ className, ...props }: React.ComponentProps<typeof Select$1.ScrollUpArrow>): React.JSX.Element;
19
24
  declare function SelectScrollDownButton({ className, ...props }: React.ComponentProps<typeof Select$1.ScrollDownArrow>): React.JSX.Element;
20
25
  //#endregion
21
- export { Select, SelectContent, SelectGroup, SelectItem, SelectLabel, SelectScrollDownButton, SelectScrollUpButton, SelectSeparator, type SelectSize, SelectTrigger, SelectValue };
26
+ export { Select, SelectContent, SelectFooter, SelectGroup, SelectHeader, SelectItem, SelectLabel, SelectScrollDownButton, SelectScrollUpButton, SelectSeparator, type SelectSize, SelectTrigger, SelectValue };