@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.
- package/css/components.css +13 -0
- package/css/generated/shared.css +1 -1
- package/css/theme.css +11 -8
- package/dist/badge.d.ts +5 -1
- package/dist/badge.js +19 -5
- package/dist/bar-chart.d.ts +1 -1
- package/dist/bullet-chart.d.ts +1 -1
- package/dist/button-BS5KCibF.d.ts +33 -0
- package/dist/button-group.d.ts +1 -1
- package/dist/button.d.ts +1 -1
- package/dist/button.js +49 -21
- package/dist/calendar.js +1 -1
- package/dist/carousel.d.ts +4 -2
- package/dist/carousel.js +11 -14
- package/dist/chart.d.ts +1 -1
- package/dist/chat.d.ts +1 -1
- package/dist/checkbox.d.ts +2 -2
- package/dist/checkbox.js +20 -6
- package/dist/{combobox-B7cKFJ9V.js → combobox-D70DFHoM.js} +55 -10
- package/dist/combobox.d.ts +8 -3
- package/dist/combobox.js +2 -2
- package/dist/cta.d.ts +1 -1
- package/dist/data-table.d.ts +2 -2
- package/dist/date-picker.d.ts +3 -3
- package/dist/date-picker.js +1 -1
- package/dist/dialog.d.ts +6 -2
- package/dist/dialog.js +15 -12
- package/dist/dropdown-menu.d.ts +1 -1
- package/dist/field.d.ts +4 -1
- package/dist/field.js +28 -11
- package/dist/footer.d.ts +2 -2
- package/dist/funnel-chart.d.ts +1 -1
- package/dist/heatmap-chart.d.ts +1 -1
- package/dist/infotip.js +0 -1
- package/dist/input-group.d.ts +6 -6
- package/dist/input-group.js +43 -18
- package/dist/input-otp.d.ts +2 -2
- package/dist/input-otp.js +31 -14
- package/dist/input-size-context-BMhves-J.d.ts +13 -0
- package/dist/input-size-context.d.ts +2 -2
- package/dist/input-size-context.js +9 -2
- package/dist/input.d.ts +20 -2
- package/dist/input.js +30 -11
- package/dist/line-chart.d.ts +1 -1
- package/dist/mask-input.d.ts +1 -1
- package/dist/match.d.ts +14 -2
- package/dist/match.js +221 -48
- package/dist/media-gallery-hero.js +9 -7
- package/dist/media-player.js +3 -3
- package/dist/mobile-sidebar.js +7 -6
- package/dist/number-input.d.ts +2 -2
- package/dist/number-input.js +2 -2
- package/dist/pagination.d.ts +1 -1
- package/dist/phone-input.d.ts +20 -3
- package/dist/phone-input.js +137 -67
- package/dist/picker-BTXBrsl4.js +62 -0
- package/dist/{picker-CsnHtbe4.d.ts → picker-j3txxA5t.d.ts} +2 -2
- package/dist/pie-chart.d.ts +1 -1
- package/dist/{popover-DSlPeH7x.d.ts → popover-CNymD33m.d.ts} +1 -1
- package/dist/popover.d.ts +1 -1
- package/dist/property-listing-card.d.ts +7 -3
- package/dist/property-listing-card.js +25 -8
- package/dist/radar-chart.d.ts +1 -1
- package/dist/radio-group.d.ts +2 -2
- package/dist/radio-group.js +23 -9
- package/dist/rating.d.ts +60 -0
- package/dist/rating.js +187 -0
- package/dist/rich-text-editor.d.ts +1 -1
- package/dist/scatter-chart.d.ts +1 -1
- package/dist/scroll-spy.d.ts +8 -2
- package/dist/scroll-spy.js +13 -13
- package/dist/section-jumper.d.ts +31 -0
- package/dist/section-jumper.js +276 -0
- package/dist/select.d.ts +9 -4
- package/dist/select.js +148 -20
- package/dist/sheet.d.ts +5 -1
- package/dist/sheet.js +15 -12
- package/dist/sidebar.d.ts +2 -2
- package/dist/sidebar.js +1 -0
- package/dist/size-context-BVhHduLi.d.ts +33 -0
- package/dist/size-context.d.ts +2 -2
- package/dist/size-context.js +30 -1
- package/dist/slider.d.ts +5 -3
- package/dist/slider.js +25 -10
- package/dist/sonar.d.ts +4 -2
- package/dist/sonar.js +116 -28
- package/dist/sparkline.d.ts +1 -1
- package/dist/styles.css +1674 -252
- package/dist/switch.d.ts +2 -2
- package/dist/switch.js +20 -6
- package/dist/tabs.d.ts +1 -1
- package/dist/tag.d.ts +1 -1
- package/dist/textarea.d.ts +17 -1
- package/dist/textarea.js +23 -9
- package/dist/time-picker.d.ts +3 -3
- package/dist/time-picker.js +1 -1
- package/dist/tree-map.d.ts +1 -1
- package/dist/video-player-dialog.js +4 -3
- package/dist/waterfall-chart.d.ts +1 -1
- package/package.json +9 -1
- package/dist/button-DW5OijoR.d.ts +0 -17
- package/dist/dialog-nesting.d.ts +0 -9
- package/dist/dialog-nesting.js +0 -27
- package/dist/input-CfoEJ8A6.d.ts +0 -12
- package/dist/input-size-context-C-SFx9sx.d.ts +0 -10
- package/dist/picker-DmIiQKLJ.js +0 -45
- package/dist/size-context-BX6kAdVj.d.ts +0 -11
- 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 };
|
package/dist/scatter-chart.d.ts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { t as ComponentSize } from "./size-context-
|
|
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";
|
package/dist/scroll-spy.d.ts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { t as ComponentSize } from "./size-context-
|
|
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 };
|
package/dist/scroll-spy.js
CHANGED
|
@@ -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
|
|
54
|
-
|
|
55
|
-
|
|
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 {
|
|
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 =
|
|
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">
|
|
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 };
|