@giddaa-housing/ui 3.9.0 → 3.10.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/generated/shared.css +1 -1
- package/css/theme.css +15 -0
- package/dist/accordion.js +2 -2
- package/dist/calendar.js +1 -1
- package/dist/{combobox-Jfz8hryr.js → combobox-pFh77rhm.js} +5 -1
- package/dist/combobox.js +1 -1
- package/dist/date-picker.d.ts +1 -1
- package/dist/dialog.js +1 -1
- package/dist/empty-state.js +2 -2
- package/dist/{icons-C9JXR5Xv.d.ts → icons-e71GaFK-.d.ts} +2 -1
- package/dist/icons.d.ts +2 -2
- package/dist/icons.js +39 -1
- package/dist/input-group.js +1 -1
- package/dist/kanban.d.ts +214 -0
- package/dist/kanban.js +705 -0
- package/dist/media-gallery-hero.d.ts +7 -1
- package/dist/media-gallery-hero.js +24 -9
- package/dist/media-player.js +33 -2
- package/dist/{picker-qxWi9wZ1.d.ts → picker-QtFHPRqg.d.ts} +1 -1
- package/dist/rating.d.ts +1 -1
- package/dist/scroll-spy.d.ts +27 -4
- package/dist/scroll-spy.js +45 -7
- package/dist/section-jumper.d.ts +7 -1
- package/dist/section-jumper.js +86 -25
- package/dist/select.js +1 -1
- package/dist/styles.css +170 -29
- package/dist/tabs.d.ts +38 -2
- package/dist/tabs.js +76 -10
- package/dist/time-picker.d.ts +1 -1
- package/package.json +8 -1
|
@@ -17,6 +17,12 @@ type MediaGalleryItem = {
|
|
|
17
17
|
type MediaGalleryImagesProps = Omit<React.ComponentProps<"div">, "children"> & VariantProps<typeof mediaGalleryImagesVariants> & {
|
|
18
18
|
images: MediaGalleryItem[];
|
|
19
19
|
currentMedia?: number;
|
|
20
|
+
/**
|
|
21
|
+
* @deprecated No longer read. The mobile carousel counter counts its own
|
|
22
|
+
* slides, since the carousel carries every image. Pass a larger library
|
|
23
|
+
* figure to `MediaGalleryIndicator` instead, where it labels the link into
|
|
24
|
+
* the full gallery.
|
|
25
|
+
*/
|
|
20
26
|
totalMediaCount?: number;
|
|
21
27
|
showMobileIndicator?: boolean;
|
|
22
28
|
onMediaClick?: (index: number) => void;
|
|
@@ -50,7 +56,7 @@ declare function MediaGalleryTile({ className, image, index, isDesktopTile, onMe
|
|
|
50
56
|
onMediaClick?: (index: number) => void;
|
|
51
57
|
renderMedia?: (image: MediaGalleryItem, index: number) => React.ReactNode;
|
|
52
58
|
}): React.JSX.Element;
|
|
53
|
-
declare function MediaGalleryImages({ className, currentMedia, imageCount, images, onMediaClick, renderMedia, showMobileIndicator, totalMediaCount, ...props }: MediaGalleryImagesProps): React.JSX.Element;
|
|
59
|
+
declare function MediaGalleryImages({ className, currentMedia, imageCount, images, onMediaClick, renderMedia, showMobileIndicator, totalMediaCount: _totalMediaCount, ...props }: MediaGalleryImagesProps): React.JSX.Element;
|
|
54
60
|
declare function MediaGalleryVideo({ alt, autoPlay, className, controls, label, loop, muted, onPlayClick, playsInline, poster, renderMedia, src, ...props }: MediaGalleryVideoProps): React.JSX.Element;
|
|
55
61
|
declare function MediaGalleryReview({ bordered, className, size, ...props }: MediaGalleryReviewProps): React.JSX.Element;
|
|
56
62
|
declare function MediaGalleryIndicator({ "aria-label": ariaLabel, className, label, onClick, size, totalMediaCount, ...props }: MediaGalleryIndicatorProps): React.JSX.Element;
|
|
@@ -39,8 +39,13 @@ function getTileClassName(index, imageCount) {
|
|
|
39
39
|
if (imageCount === 4 && index === 2) return "md:col-start-3 md:row-start-1";
|
|
40
40
|
return "md:col-start-3 md:row-start-2";
|
|
41
41
|
}
|
|
42
|
-
|
|
43
|
-
|
|
42
|
+
/**
|
|
43
|
+
* `id` is the only field guaranteed unique, so everything else falls back to a
|
|
44
|
+
* positional key. A listing that reuses one photo twice, or has entries still
|
|
45
|
+
* waiting on a `src`, would otherwise hand React duplicate keys.
|
|
46
|
+
*/
|
|
47
|
+
function getMediaKey(image, index) {
|
|
48
|
+
return image.id ?? `${image.src ?? image.alt ?? "media"}-${index}`;
|
|
44
49
|
}
|
|
45
50
|
function MediaGalleryTile({ className, image, index, isDesktopTile, onMediaClick, renderMedia }) {
|
|
46
51
|
const content = renderMedia?.(image, index) ?? (image.src ? /* @__PURE__ */ jsx(Image, {
|
|
@@ -68,6 +73,13 @@ function MediaGalleryTile({ className, image, index, isDesktopTile, onMediaClick
|
|
|
68
73
|
children: content
|
|
69
74
|
});
|
|
70
75
|
}
|
|
76
|
+
/**
|
|
77
|
+
* Counts slides, not library size. The carousel carries every image it is
|
|
78
|
+
* given — unlike the desktop grid, which shows at most `imageCount` — so the
|
|
79
|
+
* denominator here is the number of slides and nothing else. A larger
|
|
80
|
+
* "View All N Photos" figure belongs on `MediaGalleryIndicator`, where it
|
|
81
|
+
* labels a link into the full gallery rather than a position in this one.
|
|
82
|
+
*/
|
|
71
83
|
function MobileGalleryCounter({ total }) {
|
|
72
84
|
const { selectedIndex } = useCarousel();
|
|
73
85
|
return /* @__PURE__ */ jsx(GalleryIndicator, {
|
|
@@ -77,7 +89,12 @@ function MobileGalleryCounter({ total }) {
|
|
|
77
89
|
totalPhotos: total
|
|
78
90
|
});
|
|
79
91
|
}
|
|
80
|
-
|
|
92
|
+
/**
|
|
93
|
+
* The mobile view of the hero. Unlike the desktop bento grid, which arranges a
|
|
94
|
+
* fixed 1–4 tiles, this carries every image it is given — `imageCount` caps the
|
|
95
|
+
* grid's layout, never this.
|
|
96
|
+
*/
|
|
97
|
+
function MobileMediaCarousel({ currentMedia, images, onMediaClick, renderMedia, showMobileIndicator }) {
|
|
81
98
|
const carouselImages = images.length > 0 ? images : [{}];
|
|
82
99
|
return /* @__PURE__ */ jsxs(Carousel, {
|
|
83
100
|
className: "absolute inset-0 z-0 block size-full rounded-none md:hidden",
|
|
@@ -95,20 +112,18 @@ function MobileMediaCarousel({ currentMedia, images, mediaTotal, onMediaClick, r
|
|
|
95
112
|
onMediaClick,
|
|
96
113
|
renderMedia
|
|
97
114
|
})
|
|
98
|
-
}, getMediaKey(image))) }), showMobileIndicator ? /* @__PURE__ */ jsx("div", {
|
|
115
|
+
}, getMediaKey(image, index))) }), showMobileIndicator ? /* @__PURE__ */ jsx("div", {
|
|
99
116
|
className: "absolute right-4 bottom-4 z-10",
|
|
100
|
-
children: /* @__PURE__ */ jsx(MobileGalleryCounter, { total:
|
|
117
|
+
children: /* @__PURE__ */ jsx(MobileGalleryCounter, { total: carouselImages.length })
|
|
101
118
|
}) : null]
|
|
102
119
|
});
|
|
103
120
|
}
|
|
104
|
-
function MediaGalleryImages({ className, currentMedia = 1, imageCount = 4, images, onMediaClick, renderMedia, showMobileIndicator = true, totalMediaCount, ...props }) {
|
|
121
|
+
function MediaGalleryImages({ className, currentMedia = 1, imageCount = 4, images, onMediaClick, renderMedia, showMobileIndicator = true, totalMediaCount: _totalMediaCount, ...props }) {
|
|
105
122
|
const resolvedImageCount = imageCount ?? 4;
|
|
106
|
-
const mediaTotal = totalMediaCount ?? Math.max(images.length, 1);
|
|
107
123
|
const visibleImages = getVisibleImages(images, resolvedImageCount);
|
|
108
124
|
return /* @__PURE__ */ jsxs(Fragment, { children: [/* @__PURE__ */ jsx(MobileMediaCarousel, {
|
|
109
125
|
currentMedia,
|
|
110
126
|
images,
|
|
111
|
-
mediaTotal,
|
|
112
127
|
onMediaClick,
|
|
113
128
|
renderMedia,
|
|
114
129
|
showMobileIndicator
|
|
@@ -124,7 +139,7 @@ function MediaGalleryImages({ className, currentMedia = 1, imageCount = 4, image
|
|
|
124
139
|
isDesktopTile: true,
|
|
125
140
|
onMediaClick,
|
|
126
141
|
renderMedia
|
|
127
|
-
}, getMediaKey(image)))
|
|
142
|
+
}, getMediaKey(image, index)))
|
|
128
143
|
})] });
|
|
129
144
|
}
|
|
130
145
|
function MediaGalleryVideo({ alt = "", autoPlay, className, controls, label = "Play Video", loop, muted, onPlayClick, playsInline = true, poster, renderMedia, src, ...props }) {
|
package/dist/media-player.js
CHANGED
|
@@ -36,6 +36,31 @@ function setRef(ref, value) {
|
|
|
36
36
|
}
|
|
37
37
|
if (ref) ref.current = value;
|
|
38
38
|
}
|
|
39
|
+
/**
|
|
40
|
+
* Stops playback when the player leaves the page.
|
|
41
|
+
*
|
|
42
|
+
* Removing a media element from the document does not reliably end playback:
|
|
43
|
+
* the spec only forcibly pauses it once nothing references the element, and a
|
|
44
|
+
* Picture-in-Picture window outlives its element entirely — so a route change
|
|
45
|
+
* can leave audio running with no UI to stop it. `media-chrome` only unsets the
|
|
46
|
+
* element from its store on unmount, so the pause has to happen here.
|
|
47
|
+
*
|
|
48
|
+
* The node is captured on attach rather than read back from the ref: React
|
|
49
|
+
* detaches refs before passive effect cleanups run, so by unmount time the
|
|
50
|
+
* component's own ref is already null.
|
|
51
|
+
*/
|
|
52
|
+
function useStopOnUnmount() {
|
|
53
|
+
const nodeRef = React.useRef(null);
|
|
54
|
+
React.useEffect(() => {
|
|
55
|
+
return () => {
|
|
56
|
+
const node = nodeRef.current;
|
|
57
|
+
if (!node || node.isConnected) return;
|
|
58
|
+
node.pause();
|
|
59
|
+
if (document.pictureInPictureElement === node) document.exitPictureInPicture().catch(() => {});
|
|
60
|
+
};
|
|
61
|
+
}, []);
|
|
62
|
+
return nodeRef;
|
|
63
|
+
}
|
|
39
64
|
function formatRate(value) {
|
|
40
65
|
if (!value || value === 1) return "1x";
|
|
41
66
|
return `${Number.parseFloat(value.toFixed(2))}x`;
|
|
@@ -238,6 +263,7 @@ function MediaPlayerRoot({ audio, children, className, onBlurCapture, onFocusCap
|
|
|
238
263
|
},
|
|
239
264
|
onPointerLeave: (event) => {
|
|
240
265
|
onPointerLeave?.(event);
|
|
266
|
+
if (event.pointerType === "touch") return;
|
|
241
267
|
setHoveringControls(false);
|
|
242
268
|
if (!alwaysVisibleRef.current) {
|
|
243
269
|
window.clearTimeout(hideTimerRef.current);
|
|
@@ -247,6 +273,7 @@ function MediaPlayerRoot({ audio, children, className, onBlurCapture, onFocusCap
|
|
|
247
273
|
onPointerMove: (event) => {
|
|
248
274
|
onPointerMove?.(event);
|
|
249
275
|
wake();
|
|
276
|
+
if (event.pointerType === "touch") return;
|
|
250
277
|
const rect = event.currentTarget.querySelector("[data-slot='media-player-controls']")?.getBoundingClientRect();
|
|
251
278
|
setHoveringControls(!!rect && event.clientX >= rect.left && event.clientX <= rect.right && event.clientY >= rect.top && event.clientY <= rect.bottom);
|
|
252
279
|
},
|
|
@@ -258,11 +285,13 @@ function MediaPlayerVideo({ className, ref, ...props }) {
|
|
|
258
285
|
const mediaRef = useMediaRef();
|
|
259
286
|
const mediaRefRef = React.useRef(mediaRef);
|
|
260
287
|
mediaRefRef.current = mediaRef;
|
|
288
|
+
const playingNodeRef = useStopOnUnmount();
|
|
261
289
|
return /* @__PURE__ */ jsx("video", {
|
|
262
290
|
ref: React.useCallback((node) => {
|
|
291
|
+
if (node) playingNodeRef.current = node;
|
|
263
292
|
mediaRefRef.current(node);
|
|
264
293
|
setRef(ref, node);
|
|
265
|
-
}, [ref]),
|
|
294
|
+
}, [playingNodeRef, ref]),
|
|
266
295
|
"data-slot": "media-player-video",
|
|
267
296
|
className: cn("block aspect-video size-full object-cover", "group-data-[fullscreen]/media-player:aspect-auto group-data-[fullscreen]/media-player:object-contain", className),
|
|
268
297
|
playsInline: true,
|
|
@@ -273,11 +302,13 @@ function MediaPlayerAudio({ className, ref, ...props }) {
|
|
|
273
302
|
const mediaRef = useMediaRef();
|
|
274
303
|
const mediaRefRef = React.useRef(mediaRef);
|
|
275
304
|
mediaRefRef.current = mediaRef;
|
|
305
|
+
const playingNodeRef = useStopOnUnmount();
|
|
276
306
|
return /* @__PURE__ */ jsx("audio", {
|
|
277
307
|
ref: React.useCallback((node) => {
|
|
308
|
+
if (node) playingNodeRef.current = node;
|
|
278
309
|
mediaRefRef.current(node);
|
|
279
310
|
setRef(ref, node);
|
|
280
|
-
}, [ref]),
|
|
311
|
+
}, [playingNodeRef, ref]),
|
|
281
312
|
"data-slot": "media-player-audio",
|
|
282
313
|
className: cn("sr-only", className),
|
|
283
314
|
...props
|
package/dist/rating.d.ts
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { n as ComponentSizeValue } from "./size-context-BVhHduLi.js";
|
|
2
|
-
import {
|
|
2
|
+
import { M as IconProps } from "./icons-e71GaFK-.js";
|
|
3
3
|
import * as React from "react";
|
|
4
4
|
//#region src/rating.d.ts
|
|
5
5
|
/** How finely a rating can be *set*. Display always honours the exact value. */
|
package/dist/scroll-spy.d.ts
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { t as ComponentSize } from "./size-context-BVhHduLi.js";
|
|
2
|
-
import { TabsListVariant } from "./tabs.js";
|
|
2
|
+
import { TabsCountProps, TabsListVariant } from "./tabs.js";
|
|
3
3
|
import * as React from "react";
|
|
4
4
|
//#region src/scroll-spy.d.ts
|
|
5
5
|
/**
|
|
@@ -43,8 +43,9 @@ type ScrollSpyProps = Omit<React.ComponentProps<"div">, "onChange"> & {
|
|
|
43
43
|
* and visible, the triggers scroll to their section, and the active trigger
|
|
44
44
|
* tracks whichever section the reader is currently on.
|
|
45
45
|
*
|
|
46
|
-
* The parts mirror the `Tabs` API — `ScrollSpy` /
|
|
47
|
-
* `
|
|
46
|
+
* The parts mirror the `Tabs` API one for one — `ScrollSpy` /
|
|
47
|
+
* `ScrollSpyListContainer` / `ScrollSpyList` / `ScrollSpyTrigger` /
|
|
48
|
+
* `ScrollSpyCount` / `ScrollSpyContent`, matched by `value` — and the list
|
|
48
49
|
* takes the same `variant` and `size` as `TabsList`, so the two read as one
|
|
49
50
|
* family. Overflow ("More" dropdowns, per-breakpoint trigger counts) is the
|
|
50
51
|
* consuming app's job, exactly as it is with `Tabs`;
|
|
@@ -52,6 +53,21 @@ type ScrollSpyProps = Omit<React.ComponentProps<"div">, "onChange"> & {
|
|
|
52
53
|
* real triggers.
|
|
53
54
|
*/
|
|
54
55
|
declare function ScrollSpy({ className, orientation, value, defaultValue, onValueChange, scrollOffset, children, ...props }: ScrollSpyProps): React.JSX.Element;
|
|
56
|
+
/**
|
|
57
|
+
* Container styling — `TabsListContainer`'s, matched to this component's own
|
|
58
|
+
* list slot so the rule tracks `ScrollSpyList`'s variant.
|
|
59
|
+
*/
|
|
60
|
+
declare const scrollSpyListContainerClassName: string;
|
|
61
|
+
/**
|
|
62
|
+
* Optional wrapper around `ScrollSpyList`, matching `TabsListContainer`. Use it
|
|
63
|
+
* when the bar needs padding the triggers shouldn't inherit, or when something
|
|
64
|
+
* other than a trigger has to sit in it.
|
|
65
|
+
*
|
|
66
|
+
* It takes over the `nav` landmark — the list inside drops its own role — so
|
|
67
|
+
* the region stays a single landmark however many controls join it. Label the
|
|
68
|
+
* container rather than the list.
|
|
69
|
+
*/
|
|
70
|
+
declare function ScrollSpyListContainer({ className, children, "aria-label": ariaLabel, ...props }: React.ComponentProps<"nav">): React.JSX.Element;
|
|
55
71
|
type ScrollSpyListProps = React.ComponentProps<"nav"> & {
|
|
56
72
|
variant?: TabsListVariant;
|
|
57
73
|
size?: ComponentSize;
|
|
@@ -74,9 +90,16 @@ type ScrollSpyTriggerProps = Omit<React.ComponentProps<"button">, "value"> & {
|
|
|
74
90
|
value: string;
|
|
75
91
|
};
|
|
76
92
|
declare function ScrollSpyTrigger({ className, value, type, onClick, ...props }: ScrollSpyTriggerProps): React.JSX.Element;
|
|
93
|
+
/**
|
|
94
|
+
* Count badge — `TabsCount` under this family's slot name, so a spy trigger and
|
|
95
|
+
* a tab trigger carry the identical chip and track the same variant and size.
|
|
96
|
+
* Place it inside a `ScrollSpyTrigger`, after the label.
|
|
97
|
+
*/
|
|
98
|
+
type ScrollSpyCountProps = TabsCountProps;
|
|
99
|
+
declare function ScrollSpyCount({ "data-slot": dataSlot, ...props }: TabsCountProps): React.JSX.Element;
|
|
77
100
|
type ScrollSpyContentProps = Omit<React.ComponentProps<"section">, "value"> & {
|
|
78
101
|
value: string;
|
|
79
102
|
};
|
|
80
103
|
declare function ScrollSpyContent({ className, value, style, tabIndex, ...props }: ScrollSpyContentProps): React.JSX.Element;
|
|
81
104
|
//#endregion
|
|
82
|
-
export { ScrollSpy, ScrollSpyContent, type ScrollSpyContentProps, ScrollSpyList, type ScrollSpyListProps, type ScrollSpyProps, ScrollSpyTrigger, type ScrollSpyTriggerProps, scrollSpyTriggerClassName, useOptionalScrollSpy, useScrollSpy };
|
|
105
|
+
export { ScrollSpy, ScrollSpyContent, type ScrollSpyContentProps, ScrollSpyCount, type ScrollSpyCountProps, ScrollSpyList, ScrollSpyListContainer, type ScrollSpyListProps, type ScrollSpyProps, ScrollSpyTrigger, type ScrollSpyTriggerProps, scrollSpyListContainerClassName, scrollSpyTriggerClassName, useOptionalScrollSpy, useScrollSpy };
|
package/dist/scroll-spy.js
CHANGED
|
@@ -3,7 +3,7 @@ import { cn } from "./utils/cn.js";
|
|
|
3
3
|
import { useComponentSize } from "./size-context.js";
|
|
4
4
|
import { useReducedMotion } from "./utils/use-reduced-motion.js";
|
|
5
5
|
import { useUncontrolled } from "./utils/use-uncontrolled.js";
|
|
6
|
-
import { resolveTabsListVariant, tabsIndicatorClassName, tabsListVariants, tabsTriggerClassName } from "./tabs.js";
|
|
6
|
+
import { TabsCount, TabsListContainerContext, resolveTabsListVariant, tabsIndicatorClassName, tabsListContainerBaseClassName, tabsListVariants, tabsTriggerClassName, useInTabsListContainer } from "./tabs.js";
|
|
7
7
|
import { jsx, jsxs } from "react/jsx-runtime";
|
|
8
8
|
import * as React from "react";
|
|
9
9
|
//#region src/scroll-spy.tsx
|
|
@@ -68,8 +68,9 @@ function useScrollSpy() {
|
|
|
68
68
|
* and visible, the triggers scroll to their section, and the active trigger
|
|
69
69
|
* tracks whichever section the reader is currently on.
|
|
70
70
|
*
|
|
71
|
-
* The parts mirror the `Tabs` API — `ScrollSpy` /
|
|
72
|
-
* `
|
|
71
|
+
* The parts mirror the `Tabs` API one for one — `ScrollSpy` /
|
|
72
|
+
* `ScrollSpyListContainer` / `ScrollSpyList` / `ScrollSpyTrigger` /
|
|
73
|
+
* `ScrollSpyCount` / `ScrollSpyContent`, matched by `value` — and the list
|
|
73
74
|
* takes the same `variant` and `size` as `TabsList`, so the two read as one
|
|
74
75
|
* family. Overflow ("More" dropdowns, per-breakpoint trigger counts) is the
|
|
75
76
|
* consuming app's job, exactly as it is with `Tabs`;
|
|
@@ -210,12 +211,39 @@ function followActiveTrigger(list, element, behavior) {
|
|
|
210
211
|
}
|
|
211
212
|
if (options.left !== void 0 || options.top !== void 0) list.scrollTo(options);
|
|
212
213
|
}
|
|
213
|
-
|
|
214
|
+
/**
|
|
215
|
+
* Container styling — `TabsListContainer`'s, matched to this component's own
|
|
216
|
+
* list slot so the rule tracks `ScrollSpyList`'s variant.
|
|
217
|
+
*/
|
|
218
|
+
const scrollSpyListContainerClassName = cn(tabsListContainerBaseClassName, "group-data-[orientation=horizontal]/tabs:has-[[data-slot=scroll-spy-list][data-variant=underline]]:border-b group-data-[orientation=horizontal]/tabs:has-[[data-slot=scroll-spy-list][data-variant=underline]]:border-line-subtle", "group-data-[orientation=vertical]/tabs:has-[[data-slot=scroll-spy-list][data-variant=underline]]:border-r group-data-[orientation=vertical]/tabs:has-[[data-slot=scroll-spy-list][data-variant=underline]]:border-line-subtle");
|
|
219
|
+
/**
|
|
220
|
+
* Optional wrapper around `ScrollSpyList`, matching `TabsListContainer`. Use it
|
|
221
|
+
* when the bar needs padding the triggers shouldn't inherit, or when something
|
|
222
|
+
* other than a trigger has to sit in it.
|
|
223
|
+
*
|
|
224
|
+
* It takes over the `nav` landmark — the list inside drops its own role — so
|
|
225
|
+
* the region stays a single landmark however many controls join it. Label the
|
|
226
|
+
* container rather than the list.
|
|
227
|
+
*/
|
|
228
|
+
function ScrollSpyListContainer({ className, children, "aria-label": ariaLabel = "Section navigation", ...props }) {
|
|
229
|
+
return /* @__PURE__ */ jsx(TabsListContainerContext.Provider, {
|
|
230
|
+
value: true,
|
|
231
|
+
children: /* @__PURE__ */ jsx("nav", {
|
|
232
|
+
"data-slot": "scroll-spy-list-container",
|
|
233
|
+
"aria-label": ariaLabel,
|
|
234
|
+
className: cn(scrollSpyListContainerClassName, className),
|
|
235
|
+
...props,
|
|
236
|
+
children
|
|
237
|
+
})
|
|
238
|
+
});
|
|
239
|
+
}
|
|
240
|
+
function ScrollSpyList({ className, variant = "underline", size, children, scrollActiveIntoView = false, "aria-label": ariaLabel, ...props }) {
|
|
214
241
|
const { activeValue } = useScrollSpyContext("ScrollSpyList");
|
|
215
242
|
const reducedMotion = useReducedMotion();
|
|
216
243
|
const hasFollowedRef = React.useRef(false);
|
|
217
244
|
const resolvedVariant = resolveTabsListVariant(variant);
|
|
218
245
|
const resolvedSize = useComponentSize(size);
|
|
246
|
+
const inContainer = useInTabsListContainer();
|
|
219
247
|
const listRef = React.useRef(null);
|
|
220
248
|
const [indicator, setIndicator] = React.useState(null);
|
|
221
249
|
React.useEffect(() => {
|
|
@@ -256,8 +284,12 @@ function ScrollSpyList({ className, variant = "underline", size, children, scrol
|
|
|
256
284
|
"data-slot": "scroll-spy-list",
|
|
257
285
|
"data-variant": resolvedVariant,
|
|
258
286
|
"data-size": resolvedSize,
|
|
259
|
-
"
|
|
260
|
-
|
|
287
|
+
role: inContainer ? "none" : void 0,
|
|
288
|
+
"aria-label": inContainer ? ariaLabel : ariaLabel ?? "Section navigation",
|
|
289
|
+
className: cn(tabsListVariants({
|
|
290
|
+
variant: resolvedVariant,
|
|
291
|
+
standalone: !inContainer
|
|
292
|
+
}), className),
|
|
261
293
|
style: indicator ? {
|
|
262
294
|
"--active-tab-left": `${indicator.left}px`,
|
|
263
295
|
"--active-tab-top": `${indicator.top}px`,
|
|
@@ -295,6 +327,12 @@ function ScrollSpyTrigger({ className, value, type = "button", onClick, ...props
|
|
|
295
327
|
...props
|
|
296
328
|
});
|
|
297
329
|
}
|
|
330
|
+
function ScrollSpyCount({ "data-slot": dataSlot = "scroll-spy-count", ...props }) {
|
|
331
|
+
return /* @__PURE__ */ jsx(TabsCount, {
|
|
332
|
+
"data-slot": dataSlot,
|
|
333
|
+
...props
|
|
334
|
+
});
|
|
335
|
+
}
|
|
298
336
|
function ScrollSpyContent({ className, value, style, tabIndex = -1, ...props }) {
|
|
299
337
|
const { activeValue, getSectionId, getTriggerId, registerSection, scrollOffset } = useScrollSpyContext("ScrollSpyContent");
|
|
300
338
|
return /* @__PURE__ */ jsx("section", {
|
|
@@ -315,4 +353,4 @@ function ScrollSpyContent({ className, value, style, tabIndex = -1, ...props })
|
|
|
315
353
|
});
|
|
316
354
|
}
|
|
317
355
|
//#endregion
|
|
318
|
-
export { ScrollSpy, ScrollSpyContent, ScrollSpyList, ScrollSpyTrigger, scrollSpyTriggerClassName, useOptionalScrollSpy, useScrollSpy };
|
|
356
|
+
export { ScrollSpy, ScrollSpyContent, ScrollSpyCount, ScrollSpyList, ScrollSpyListContainer, ScrollSpyTrigger, scrollSpyListContainerClassName, scrollSpyTriggerClassName, useOptionalScrollSpy, useScrollSpy };
|
package/dist/section-jumper.d.ts
CHANGED
|
@@ -23,9 +23,15 @@ type SectionJumperProps = Omit<React.ComponentProps<"div">, "children"> & {
|
|
|
23
23
|
defaultOpen?: boolean;
|
|
24
24
|
onOpenChange?: (open: boolean) => void;
|
|
25
25
|
mobileBreakpoint?: number;
|
|
26
|
+
/**
|
|
27
|
+
* Most dots the resting rail draws before it windows around the active
|
|
28
|
+
* section. Defaults to 12 on desktop and 7 on mobile, where the dots share
|
|
29
|
+
* a row with the section label. The menu always lists every section.
|
|
30
|
+
*/
|
|
31
|
+
maxDots?: number;
|
|
26
32
|
/** Sticky-header offset used by standalone target-id mode. */
|
|
27
33
|
scrollOffset?: number;
|
|
28
34
|
};
|
|
29
|
-
declare function SectionJumper({ className, items: itemsProp, label, open, defaultOpen, onOpenChange, mobileBreakpoint, scrollOffset: scrollOffsetProp, ...props }: SectionJumperProps): React.JSX.Element | null;
|
|
35
|
+
declare function SectionJumper({ className, items: itemsProp, label, open, defaultOpen, onOpenChange, mobileBreakpoint, maxDots, scrollOffset: scrollOffsetProp, ...props }: SectionJumperProps): React.JSX.Element | null;
|
|
30
36
|
//#endregion
|
|
31
37
|
export { SectionJumper, type SectionJumperItem, type SectionJumperProps, SectionJumperProvider, type SectionJumperProviderProps };
|
package/dist/section-jumper.js
CHANGED
|
@@ -153,17 +153,78 @@ function useStandaloneSectionJumper(items, scrollOffset, enabled) {
|
|
|
153
153
|
scrollToSection
|
|
154
154
|
]);
|
|
155
155
|
}
|
|
156
|
-
|
|
156
|
+
/**
|
|
157
|
+
* Most sections a rail can show before it stops being a glanceable indicator
|
|
158
|
+
* and starts being a list — at which point it is windowed. Mobile gets the
|
|
159
|
+
* smaller budget: its dots run across the screen rather than down it, sharing
|
|
160
|
+
* the row with the section label.
|
|
161
|
+
*/
|
|
162
|
+
const DEFAULT_MAX_DOTS = {
|
|
163
|
+
desktop: 12,
|
|
164
|
+
mobile: 7
|
|
165
|
+
};
|
|
166
|
+
/**
|
|
167
|
+
* The slice of sections the rail draws. Everything fits until there are more
|
|
168
|
+
* than `max`; past that the window slides to keep the active section centred,
|
|
169
|
+
* pinning at either end so the first and last sections stay reachable.
|
|
170
|
+
*/
|
|
171
|
+
function getDotWindow(count, activeIndex, max) {
|
|
172
|
+
if (count <= max) return {
|
|
173
|
+
start: 0,
|
|
174
|
+
end: count
|
|
175
|
+
};
|
|
176
|
+
const start = Math.max(0, Math.min(activeIndex - Math.floor(max / 2), count - max));
|
|
177
|
+
return {
|
|
178
|
+
start,
|
|
179
|
+
end: start + max
|
|
180
|
+
};
|
|
181
|
+
}
|
|
182
|
+
function SectionJumperDot({ active, edge, variant }) {
|
|
157
183
|
return /* @__PURE__ */ jsx("span", {
|
|
158
184
|
"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")
|
|
185
|
+
className: cn("block shrink-0 bg-line transition-[width,height,background-color] duration-(--duration-motion-surface) ease-gdt-out motion-reduce:transition-none", !active && (edge ? "size-1 rounded-full" : "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
186
|
});
|
|
161
187
|
}
|
|
188
|
+
/**
|
|
189
|
+
* The windowed rail, shared by both triggers — the dots are the only part of
|
|
190
|
+
* the two that differ by more than layout.
|
|
191
|
+
*/
|
|
192
|
+
function SectionJumperDots({ items, activeValue, max, variant }) {
|
|
193
|
+
const activeIndex = items.findIndex((item) => item.value === activeValue);
|
|
194
|
+
const { start, end } = getDotWindow(items.length, activeIndex < 0 ? 0 : activeIndex, max);
|
|
195
|
+
return items.slice(start, end).map((item, index) => /* @__PURE__ */ jsx(SectionJumperDot, {
|
|
196
|
+
active: item.value === activeValue,
|
|
197
|
+
edge: index === 0 && start > 0 || index === end - start - 1 && end < items.length,
|
|
198
|
+
variant
|
|
199
|
+
}, item.value));
|
|
200
|
+
}
|
|
162
201
|
function SectionJumperMenu({ items, onSelect, controller }) {
|
|
163
202
|
const { activeValue, getSectionId, getTriggerId } = controller;
|
|
203
|
+
const listRef = React.useRef(null);
|
|
204
|
+
React.useEffect(() => {
|
|
205
|
+
const list = listRef.current;
|
|
206
|
+
if (!list) return;
|
|
207
|
+
let positioned = false;
|
|
208
|
+
const position = () => {
|
|
209
|
+
if (positioned || list.scrollHeight <= list.clientHeight) return;
|
|
210
|
+
const active = list.querySelector("[data-active]");
|
|
211
|
+
if (!active) return;
|
|
212
|
+
positioned = true;
|
|
213
|
+
const top = active.offsetTop;
|
|
214
|
+
const bottom = top + active.offsetHeight;
|
|
215
|
+
if (top < list.scrollTop) list.scrollTop = Math.max(0, top - 8);
|
|
216
|
+
else if (bottom > list.scrollTop + list.clientHeight) list.scrollTop = bottom - list.clientHeight + 8;
|
|
217
|
+
};
|
|
218
|
+
position();
|
|
219
|
+
if (typeof ResizeObserver === "undefined") return;
|
|
220
|
+
const observer = new ResizeObserver(position);
|
|
221
|
+
observer.observe(list);
|
|
222
|
+
return () => observer.disconnect();
|
|
223
|
+
}, []);
|
|
164
224
|
return /* @__PURE__ */ jsx("nav", {
|
|
225
|
+
ref: listRef,
|
|
165
226
|
"aria-label": "Page sections",
|
|
166
|
-
className: "flex flex-col gap-0.5",
|
|
227
|
+
className: "flex min-h-0 flex-1 flex-col gap-0.5 overflow-y-auto overscroll-contain pt-2",
|
|
167
228
|
children: items.map((item) => {
|
|
168
229
|
const active = item.value === activeValue;
|
|
169
230
|
return /* @__PURE__ */ jsxs("button", {
|
|
@@ -185,7 +246,7 @@ function SectionJumperMenu({ items, onSelect, controller }) {
|
|
|
185
246
|
})
|
|
186
247
|
});
|
|
187
248
|
}
|
|
188
|
-
function SectionJumper({ className, items: itemsProp, label = "On this page", open, defaultOpen = false, onOpenChange, mobileBreakpoint = 1024, scrollOffset: scrollOffsetProp, ...props }) {
|
|
249
|
+
function SectionJumper({ className, items: itemsProp, label = "On this page", open, defaultOpen = false, onOpenChange, mobileBreakpoint = 1024, maxDots, scrollOffset: scrollOffsetProp, ...props }) {
|
|
189
250
|
const provider = React.useContext(SectionJumperProviderContext);
|
|
190
251
|
const items = itemsProp ?? provider?.items ?? [];
|
|
191
252
|
const scrollOffset = scrollOffsetProp ?? provider?.scrollOffset ?? 0;
|
|
@@ -211,32 +272,35 @@ function SectionJumper({ className, items: itemsProp, label = "On this page", op
|
|
|
211
272
|
type: "button",
|
|
212
273
|
"aria-label": `${label}${activeItem ? `, current section: ${String(activeItem.label)}` : ""}`,
|
|
213
274
|
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:
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
275
|
+
children: /* @__PURE__ */ jsx(SectionJumperDots, {
|
|
276
|
+
items,
|
|
277
|
+
activeValue,
|
|
278
|
+
max: maxDots ?? DEFAULT_MAX_DOTS.desktop,
|
|
279
|
+
variant: "desktop"
|
|
219
280
|
})
|
|
220
281
|
});
|
|
221
282
|
const mobileTrigger = /* @__PURE__ */ jsxs("button", {
|
|
222
283
|
type: "button",
|
|
223
|
-
|
|
284
|
+
"aria-label": `${label}${activeItem ? `, current section: ${String(activeItem.label)}` : ""}`,
|
|
285
|
+
className: "flex max-w-[min(22rem,calc(100vw-2rem))] 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
286
|
children: [
|
|
225
287
|
/* @__PURE__ */ jsx("span", {
|
|
226
288
|
"aria-hidden": true,
|
|
227
|
-
className: "flex items-center gap-1",
|
|
228
|
-
children:
|
|
229
|
-
|
|
289
|
+
className: "flex shrink-0 items-center gap-1",
|
|
290
|
+
children: /* @__PURE__ */ jsx(SectionJumperDots, {
|
|
291
|
+
items,
|
|
292
|
+
activeValue,
|
|
293
|
+
max: maxDots ?? DEFAULT_MAX_DOTS.mobile,
|
|
230
294
|
variant: "mobile"
|
|
231
|
-
}
|
|
295
|
+
})
|
|
232
296
|
}),
|
|
233
297
|
/* @__PURE__ */ jsx("span", {
|
|
234
|
-
className: "
|
|
298
|
+
className: "min-w-0 flex-1 truncate text-left",
|
|
235
299
|
children: activeItem?.label ?? label
|
|
236
300
|
}),
|
|
237
301
|
/* @__PURE__ */ jsx(ChevronDown, {
|
|
238
302
|
"aria-hidden": true,
|
|
239
|
-
className: cn("size-3 transition-transform", resolvedOpen && "rotate-180")
|
|
303
|
+
className: cn("size-3 shrink-0 transition-transform", resolvedOpen && "rotate-180")
|
|
240
304
|
})
|
|
241
305
|
]
|
|
242
306
|
});
|
|
@@ -256,17 +320,14 @@ function SectionJumper({ className, items: itemsProp, label = "On this page", op
|
|
|
256
320
|
side: isMobile ? "top" : "left",
|
|
257
321
|
sideOffset: 8,
|
|
258
322
|
showArrow: false,
|
|
259
|
-
className: cn("border border-line-subtle
|
|
323
|
+
className: cn("max-h-(--available-height) rounded-2xl border border-line-subtle px-4 pb-4 pt-3 shadow-2", isMobile ? "w-90" : "w-max min-w-60 max-w-[min(20rem,calc(100vw-2rem))]"),
|
|
260
324
|
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"),
|
|
325
|
+
className: cn("shrink-0 border-b border-line-subtle pb-2 font-semibold text-fg-primary", isMobile ? "text-gdt-md" : "text-gdt-sm"),
|
|
262
326
|
children: label
|
|
263
|
-
}), /* @__PURE__ */ jsx(
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
onSelect: select,
|
|
268
|
-
controller
|
|
269
|
-
})
|
|
327
|
+
}), /* @__PURE__ */ jsx(SectionJumperMenu, {
|
|
328
|
+
items,
|
|
329
|
+
onSelect: select,
|
|
330
|
+
controller
|
|
270
331
|
})]
|
|
271
332
|
})]
|
|
272
333
|
})
|
package/dist/select.js
CHANGED
|
@@ -29,7 +29,7 @@ const selectTriggerVariants = cva("group/select-trigger flex w-fit min-w-0 items
|
|
|
29
29
|
} },
|
|
30
30
|
defaultVariants: { size: "md" }
|
|
31
31
|
});
|
|
32
|
-
const selectContentVariants = cva("relative isolate z-50 flex max-h-(--available-height) w-(--anchor-width)
|
|
32
|
+
const selectContentVariants = cva("relative isolate z-50 flex max-h-(--available-height) w-(--anchor-width) origin-(--transform-origin) flex-col overflow-hidden border border-line-subtle bg-canvas text-fg-primary shadow-(--elevation-e1-shadow) transition-[opacity,scale,translate] duration-[var(--duration-motion-popup)] ease-gdt-out data-[align-trigger=true]:transition-none not-data-[align-trigger=true]:data-starting-style:opacity-0 not-data-[align-trigger=true]:data-starting-style:scale-95 not-data-[align-trigger=true]:data-ending-style:opacity-0 not-data-[align-trigger=true]:data-ending-style:scale-95 not-data-[align-trigger=true]:data-[side=bottom]:data-starting-style:translate-y-2 not-data-[align-trigger=true]:data-[side=bottom]:data-ending-style:translate-y-2 not-data-[align-trigger=true]:data-[side=top]:data-starting-style:-translate-y-2 not-data-[align-trigger=true]:data-[side=top]:data-ending-style:-translate-y-2 not-data-[align-trigger=true]:data-[side=left]:data-starting-style:translate-x-2 not-data-[align-trigger=true]:data-[side=left]:data-ending-style:translate-x-2 not-data-[align-trigger=true]:data-[side=right]:data-starting-style:-translate-x-2 not-data-[align-trigger=true]:data-[side=right]:data-ending-style:-translate-x-2 not-data-[align-trigger=true]:data-[side=inline-end]:data-starting-style:-translate-x-2 not-data-[align-trigger=true]:data-[side=inline-end]:data-ending-style:-translate-x-2 not-data-[align-trigger=true]:data-[side=inline-start]:data-starting-style:translate-x-2 not-data-[align-trigger=true]:data-[side=inline-start]:data-ending-style:translate-x-2 motion-reduce:not-data-[align-trigger=true]:data-starting-style:scale-100 motion-reduce:not-data-[align-trigger=true]:data-ending-style:scale-100 motion-reduce:not-data-[align-trigger=true]:data-[side=bottom]:data-starting-style:translate-y-0 motion-reduce:not-data-[align-trigger=true]:data-[side=bottom]:data-ending-style:translate-y-0 motion-reduce:not-data-[align-trigger=true]:data-[side=top]:data-starting-style:translate-y-0 motion-reduce:not-data-[align-trigger=true]:data-[side=top]:data-ending-style:translate-y-0 motion-reduce:not-data-[align-trigger=true]:data-[side=left]:data-starting-style:translate-x-0 motion-reduce:not-data-[align-trigger=true]:data-[side=left]:data-ending-style:translate-x-0 motion-reduce:not-data-[align-trigger=true]:data-[side=right]:data-starting-style:translate-x-0 motion-reduce:not-data-[align-trigger=true]:data-[side=right]:data-ending-style:translate-x-0 motion-reduce:not-data-[align-trigger=true]:data-[side=inline-end]:data-starting-style:translate-x-0 motion-reduce:not-data-[align-trigger=true]:data-[side=inline-end]:data-ending-style:translate-x-0 motion-reduce:not-data-[align-trigger=true]:data-[side=inline-start]:data-starting-style:translate-x-0 motion-reduce:not-data-[align-trigger=true]:data-[side=inline-start]:data-ending-style:translate-x-0", {
|
|
33
33
|
variants: { size: {
|
|
34
34
|
sm: "rounded-lg",
|
|
35
35
|
md: "rounded-xl",
|