keystone-design-bootstrap 1.0.8 → 1.0.10
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/package.json +8 -2
- package/src/design_system/elements/avatar/avatar-profile-photo.tsx +5 -2
- package/src/design_system/elements/avatar/avatar.tsx +2 -1
- package/src/design_system/elements/avatar/base-components/avatar-company-icon.tsx +5 -2
- package/src/design_system/elements/badges/avatar.tsx +2 -1
- package/src/design_system/elements/badges/badge-groups.tsx +3 -3
- package/src/design_system/elements/badges/badges.tsx +3 -2
- package/src/design_system/elements/button-group/button-group.tsx +3 -3
- package/src/design_system/elements/buttons/button-utility.tsx +2 -2
- package/src/design_system/elements/buttons/button.aman.tsx +2 -2
- package/src/design_system/elements/buttons/button.tsx +2 -2
- package/src/design_system/elements/buttons/round-button.tsx +2 -3
- package/src/design_system/elements/carousel/carousel-base.tsx +62 -31
- package/src/design_system/elements/carousel/carousel.tsx +62 -31
- package/src/design_system/elements/date-picker/date-input.tsx +1 -1
- package/src/design_system/elements/featured-icon/featured-icon.tsx +2 -2
- package/src/design_system/elements/index.tsx +39 -35
- package/src/design_system/elements/input/input.aman.tsx +1 -2
- package/src/design_system/elements/input/input.tsx +1 -3
- package/src/design_system/elements/map/GoogleMap.tsx +89 -30
- package/src/design_system/elements/markdown-renderer/MarkdownRenderer.tsx +40 -28
- package/src/design_system/elements/pagination/pagination-base.tsx +2 -9
- package/src/design_system/elements/photo-fallback/photo-fallback.tsx +25 -12
- package/src/design_system/elements/select/multi-select.tsx +4 -9
- package/src/design_system/elements/select/select.aman.tsx +0 -2
- package/src/design_system/elements/shared-assets/credit-card/credit-card.tsx +2 -2
- package/src/design_system/elements/tooltip/tooltip.tsx +1 -1
- package/src/design_system/elements/video-modal.tsx +1 -1
- package/src/design_system/hooks/use-breakpoint.ts +18 -15
- package/src/design_system/sections/about-home.aman.tsx +2 -2
- package/src/design_system/sections/about-home.tsx +15 -12
- package/src/design_system/sections/blog-cards.tsx +3 -3
- package/src/design_system/sections/blog-gallery.aman.tsx +1 -1
- package/src/design_system/sections/blog-gallery.tsx +3 -3
- package/src/design_system/sections/blog-home.aman.tsx +3 -3
- package/src/design_system/sections/blog-post.aman.tsx +1 -1
- package/src/design_system/sections/blog-post.tsx +30 -20
- package/src/design_system/sections/blog-section.aman.tsx +1 -1
- package/src/design_system/sections/blog-section.tsx +0 -4
- package/src/design_system/sections/contact-home.tsx +2 -2
- package/src/design_system/sections/contact-section.aman.tsx +0 -2
- package/src/design_system/sections/contact-section.tsx +3 -3
- package/src/design_system/sections/faq-grid.aman.tsx +1 -1
- package/src/design_system/sections/faq-home.aman.tsx +1 -1
- package/src/design_system/sections/feature-text.tsx +5 -4
- package/src/design_system/sections/footer-home.aman.tsx +1 -2
- package/src/design_system/sections/footer-home.tsx +26 -25
- package/src/design_system/sections/generic-header-component.tsx +3 -3
- package/src/design_system/sections/header-navigation.aman.tsx +19 -14
- package/src/design_system/sections/header-navigation.tsx +5 -4
- package/src/design_system/sections/hero-home.aman.tsx +2 -2
- package/src/design_system/sections/hero-home.tsx +1 -4
- package/src/design_system/sections/hero-location-detail.aman.tsx +3 -1
- package/src/design_system/sections/home-hero-component.tsx +7 -5
- package/src/design_system/sections/index.tsx +18 -10
- package/src/design_system/sections/job-gallery.aman.tsx +1 -6
- package/src/design_system/sections/job-gallery.tsx +3 -3
- package/src/design_system/sections/location-details-section.aman.tsx +1 -1
- package/src/design_system/sections/location-details-section.tsx +1 -1
- package/src/design_system/sections/location-grid.aman.tsx +1 -1
- package/src/design_system/sections/location-grid.tsx +1 -1
- package/src/design_system/sections/services-home.tsx +1 -1
- package/src/design_system/sections/social-media-grid.aman.tsx +6 -6
- package/src/design_system/sections/social-media-grid.tsx +4 -1
- package/src/design_system/sections/statistics-section.aman.tsx +1 -1
- package/src/design_system/sections/statistics-section.tsx +3 -3
- package/src/design_system/sections/team-grid.aman.tsx +1 -1
- package/src/design_system/sections/testimonials-grid.aman.tsx +3 -3
- package/src/design_system/sections/testimonials-home.aman.tsx +3 -3
- package/src/design_system/sections/values-section.aman.tsx +2 -3
- package/src/lib/component-registry.ts +11 -7
- package/src/lib/hooks/use-breakpoint.ts +18 -15
- package/src/lib/server-api.ts +1 -1
- package/src/pages/.gitkeep +3 -0
- package/src/types/api/blog-post.ts +1 -1
- package/src/types/api/contact.ts +1 -1
- package/src/types/api/team-member.ts +1 -1
- package/src/types/api/website-photos.ts +1 -0
- package/src/utils/countries.tsx +5 -1
- package/src/utils/is-react-component.ts +18 -9
- package/src/utils/photo-helpers.ts +4 -6
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "keystone-design-bootstrap",
|
|
3
|
-
"version": "1.0.
|
|
3
|
+
"version": "1.0.10",
|
|
4
4
|
"description": "Keystone Design Bootstrap - Sections, Elements, and Theme System for customer websites",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "./src/index.ts",
|
|
@@ -30,7 +30,11 @@
|
|
|
30
30
|
],
|
|
31
31
|
"scripts": {
|
|
32
32
|
"build": "tsup",
|
|
33
|
-
"dev": "tsup --watch"
|
|
33
|
+
"dev": "tsup --watch",
|
|
34
|
+
"typecheck": "tsc --noEmit",
|
|
35
|
+
"test": "npm run typecheck && npm run build",
|
|
36
|
+
"lint": "eslint .",
|
|
37
|
+
"lint:fix": "eslint . --fix"
|
|
34
38
|
},
|
|
35
39
|
"peerDependencies": {
|
|
36
40
|
"next": ">=15.0.0",
|
|
@@ -53,6 +57,8 @@
|
|
|
53
57
|
"@types/node": "^20",
|
|
54
58
|
"@types/react": "^19",
|
|
55
59
|
"@types/react-dom": "^19",
|
|
60
|
+
"eslint": "^9",
|
|
61
|
+
"eslint-config-next": "16.1.1",
|
|
56
62
|
"tsup": "^8.5.1",
|
|
57
63
|
"typescript": "^5"
|
|
58
64
|
}
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
"use client";
|
|
2
2
|
|
|
3
3
|
import { useState } from "react";
|
|
4
|
+
import Image from "next/image";
|
|
4
5
|
import { User01 } from "@untitledui/icons";
|
|
5
6
|
import { cx } from '../../../utils/cx';
|
|
6
7
|
import { type AvatarProps } from "./avatar";
|
|
@@ -61,10 +62,12 @@ export const AvatarProfilePhoto = ({
|
|
|
61
62
|
const renderMainContent = () => {
|
|
62
63
|
if (src && !isFailed) {
|
|
63
64
|
return (
|
|
64
|
-
<
|
|
65
|
+
<Image
|
|
65
66
|
src={src}
|
|
66
|
-
alt={alt}
|
|
67
|
+
alt={alt || ''}
|
|
67
68
|
onError={() => setIsFailed(true)}
|
|
69
|
+
width={100}
|
|
70
|
+
height={100}
|
|
68
71
|
className={cx(
|
|
69
72
|
"size-full rounded-full object-cover",
|
|
70
73
|
contrastBorder && "outline-1 -outline-offset-1 outline-avatar-contrast-border",
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
"use client";
|
|
2
2
|
|
|
3
3
|
import { type FC, type ReactNode, useState } from "react";
|
|
4
|
+
import Image from "next/image";
|
|
4
5
|
import { User01 } from "@untitledui/icons";
|
|
5
6
|
import { cx } from '../../../utils/cx';
|
|
6
7
|
import { AvatarOnlineIndicator, VerifiedTick } from "./base-components";
|
|
@@ -81,7 +82,7 @@ export const Avatar = ({
|
|
|
81
82
|
|
|
82
83
|
const renderMainContent = () => {
|
|
83
84
|
if (src && !isFailed) {
|
|
84
|
-
return <
|
|
85
|
+
return <Image data-avatar-img className="size-full rounded-full object-cover" src={src} alt={alt || ''} onError={() => setIsFailed(true)} width={100} height={100} />;
|
|
85
86
|
}
|
|
86
87
|
|
|
87
88
|
if (initials) {
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
"use client";
|
|
2
2
|
|
|
3
|
+
import Image from "next/image";
|
|
3
4
|
import { cx } from '../../../../utils/cx';
|
|
4
5
|
|
|
5
6
|
const sizes = {
|
|
@@ -18,9 +19,11 @@ interface AvatarCompanyIconProps {
|
|
|
18
19
|
}
|
|
19
20
|
|
|
20
21
|
export const AvatarCompanyIcon = ({ size, src, alt }: AvatarCompanyIconProps) => (
|
|
21
|
-
<
|
|
22
|
+
<Image
|
|
22
23
|
src={src}
|
|
23
|
-
alt={alt}
|
|
24
|
+
alt={alt || ""}
|
|
25
|
+
width={20}
|
|
26
|
+
height={20}
|
|
24
27
|
className={cx("bg-primary-25 absolute -right-0.5 -bottom-0.5 rounded-full object-cover ring-[1.5px] ring-bg-primary", sizes[size])}
|
|
25
28
|
/>
|
|
26
29
|
);
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
"use client";
|
|
2
2
|
|
|
3
3
|
import { type FC, type ReactNode, useState } from "react";
|
|
4
|
+
import Image from "next/image";
|
|
4
5
|
import { User01 } from "@untitledui/icons";
|
|
5
6
|
import { cx } from '../../../utils/cx';
|
|
6
7
|
import { AvatarOnlineIndicator, VerifiedTick } from '../avatar/base-components';
|
|
@@ -81,7 +82,7 @@ export const Avatar = ({
|
|
|
81
82
|
|
|
82
83
|
const renderMainContent = () => {
|
|
83
84
|
if (src && !isFailed) {
|
|
84
|
-
return <
|
|
85
|
+
return <Image data-avatar-img className="size-full rounded-full object-cover" src={src} alt={alt || ''} onError={() => setIsFailed(true)} width={100} height={100} />;
|
|
85
86
|
}
|
|
86
87
|
|
|
87
88
|
if (initials) {
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
"use client";
|
|
2
2
|
|
|
3
3
|
import type { FC, ReactNode } from "react";
|
|
4
|
-
import { isValidElement } from "react";
|
|
4
|
+
import { createElement, isValidElement } from "react";
|
|
5
5
|
import { ArrowRight } from "@untitledui/icons";
|
|
6
6
|
import { cx, sortCx } from '../../../utils/cx';
|
|
7
7
|
import { isReactComponent } from '../../../utils/is-react-component';
|
|
@@ -152,7 +152,7 @@ export const BadgeGroup = ({
|
|
|
152
152
|
{addonText}
|
|
153
153
|
|
|
154
154
|
{/* Trailing icon */}
|
|
155
|
-
{isReactComponent(IconTrailing) &&
|
|
155
|
+
{isReactComponent(IconTrailing) && createElement(IconTrailing, { className: iconClasses } as Record<string, unknown>)}
|
|
156
156
|
{isValidElement(IconTrailing) && IconTrailing}
|
|
157
157
|
</span>
|
|
158
158
|
</div>
|
|
@@ -169,7 +169,7 @@ export const BadgeGroup = ({
|
|
|
169
169
|
{children}
|
|
170
170
|
|
|
171
171
|
{/* Trailing icon */}
|
|
172
|
-
{isReactComponent(IconTrailing) &&
|
|
172
|
+
{isReactComponent(IconTrailing) && createElement(IconTrailing, { className: iconClasses } as Record<string, unknown>)}
|
|
173
173
|
{isValidElement(IconTrailing) && IconTrailing}
|
|
174
174
|
</div>
|
|
175
175
|
);
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
"use client";
|
|
2
2
|
|
|
3
3
|
import type { MouseEventHandler, ReactNode, HTMLAttributes } from "react";
|
|
4
|
+
import Image from "next/image";
|
|
4
5
|
import { X as CloseX } from "@untitledui/icons";
|
|
5
6
|
import { cx } from '../../../utils/cx';
|
|
6
7
|
import type { BadgeColors, BadgeTypeToColorMap, BadgeTypes, FlagTypes, IconComponentType, Sizes } from "./badge-types";
|
|
@@ -290,7 +291,7 @@ export const BadgeWithFlag = <T extends BadgeTypes>(props: BadgeWithFlagProps<T>
|
|
|
290
291
|
|
|
291
292
|
return (
|
|
292
293
|
<span className={cx(colors.common, sizes[type][size], colors.styles[color].root)}>
|
|
293
|
-
<
|
|
294
|
+
<Image src={`https://www.untitledui.com/images/flags/${flag}.svg`} className="size-4 max-w-none rounded-full" alt={`${flag} flag`} width={16} height={16} />
|
|
294
295
|
{children}
|
|
295
296
|
</span>
|
|
296
297
|
);
|
|
@@ -328,7 +329,7 @@ export const BadgeWithImage = <T extends BadgeTypes>(props: BadgeWithImageProps<
|
|
|
328
329
|
|
|
329
330
|
return (
|
|
330
331
|
<span className={cx(colors.common, sizes[type][size], colors.styles[color].root)}>
|
|
331
|
-
<
|
|
332
|
+
<Image src={imgSrc} className="size-4 max-w-none rounded-full" alt="Badge image" width={16} height={16} />
|
|
332
333
|
{children}
|
|
333
334
|
</span>
|
|
334
335
|
);
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
"use client";
|
|
2
2
|
|
|
3
|
-
import { type FC, type PropsWithChildren, type ReactNode, type RefAttributes, createContext, isValidElement, useContext } from "react";
|
|
3
|
+
import { type FC, type PropsWithChildren, type ReactNode, type RefAttributes, createContext, createElement, isValidElement, useContext } from "react";
|
|
4
4
|
import {
|
|
5
5
|
ToggleButton as AriaToggleButton,
|
|
6
6
|
ToggleButtonGroup as AriaToggleButtonGroup,
|
|
@@ -75,12 +75,12 @@ export const ButtonGroupItem = ({
|
|
|
75
75
|
data-icon-leading={IconLeading ? true : undefined}
|
|
76
76
|
className={cx(styles.common.root, styles.sizes[size].root, className)}
|
|
77
77
|
>
|
|
78
|
-
{isReactComponent(IconLeading) &&
|
|
78
|
+
{isReactComponent(IconLeading) && createElement(IconLeading, { className: cx(styles.common.icon, styles.sizes[size].icon) } as Record<string, unknown>)}
|
|
79
79
|
{isValidElement(IconLeading) && IconLeading}
|
|
80
80
|
|
|
81
81
|
{children}
|
|
82
82
|
|
|
83
|
-
{isReactComponent(IconTrailing) &&
|
|
83
|
+
{isReactComponent(IconTrailing) && createElement(IconTrailing, { className: cx(styles.common.icon, styles.sizes[size].icon) } as Record<string, unknown>)}
|
|
84
84
|
{isValidElement(IconTrailing) && IconTrailing}
|
|
85
85
|
</AriaToggleButton>
|
|
86
86
|
);
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
"use client";
|
|
2
2
|
|
|
3
3
|
import type { AnchorHTMLAttributes, ButtonHTMLAttributes, DetailedHTMLProps, FC, ReactNode } from "react";
|
|
4
|
-
import { isValidElement } from "react";
|
|
4
|
+
import { createElement, isValidElement } from "react";
|
|
5
5
|
import type { Placement } from "react-aria";
|
|
6
6
|
import type { ButtonProps as AriaButtonProps } from "react-aria-components";
|
|
7
7
|
import { Button as AriaButton, Link as AriaLink } from "react-aria-components";
|
|
@@ -99,7 +99,7 @@ export const ButtonUtility = ({
|
|
|
99
99
|
className,
|
|
100
100
|
)}
|
|
101
101
|
>
|
|
102
|
-
{isReactComponent(Icon) &&
|
|
102
|
+
{isReactComponent(Icon) && createElement(Icon, { 'data-icon': true } as Record<string, unknown>)}
|
|
103
103
|
{isValidElement(Icon) && Icon}
|
|
104
104
|
</Component>
|
|
105
105
|
);
|
|
@@ -135,7 +135,7 @@ export const Button = ({
|
|
|
135
135
|
)}
|
|
136
136
|
>
|
|
137
137
|
{isValidElement(IconLeading) && IconLeading}
|
|
138
|
-
{isReactComponent(IconLeading) &&
|
|
138
|
+
{isReactComponent(IconLeading) && React.createElement(IconLeading, { 'data-icon': 'leading', className: styles.common.icon } as Record<string, unknown>)}
|
|
139
139
|
|
|
140
140
|
{loading && (
|
|
141
141
|
<svg
|
|
@@ -165,7 +165,7 @@ export const Button = ({
|
|
|
165
165
|
)}
|
|
166
166
|
|
|
167
167
|
{isValidElement(IconTrailing) && IconTrailing}
|
|
168
|
-
{isReactComponent(IconTrailing) &&
|
|
168
|
+
{isReactComponent(IconTrailing) && React.createElement(IconTrailing, { 'data-icon': 'trailing', className: styles.common.icon } as Record<string, unknown>)}
|
|
169
169
|
</Component>
|
|
170
170
|
);
|
|
171
171
|
};
|
|
@@ -232,7 +232,7 @@ export const Button = ({
|
|
|
232
232
|
>
|
|
233
233
|
{/* Leading icon */}
|
|
234
234
|
{isValidElement(IconLeading) && IconLeading}
|
|
235
|
-
{isReactComponent(IconLeading) &&
|
|
235
|
+
{isReactComponent(IconLeading) && React.createElement(IconLeading, { 'data-icon': 'leading', className: styles.common.icon } as Record<string, unknown>)}
|
|
236
236
|
|
|
237
237
|
{loading && (
|
|
238
238
|
<svg
|
|
@@ -265,7 +265,7 @@ export const Button = ({
|
|
|
265
265
|
|
|
266
266
|
{/* Trailing icon */}
|
|
267
267
|
{isValidElement(IconTrailing) && IconTrailing}
|
|
268
|
-
{isReactComponent(IconTrailing) &&
|
|
268
|
+
{isReactComponent(IconTrailing) && React.createElement(IconTrailing, { 'data-icon': 'trailing', className: styles.common.icon } as Record<string, unknown>)}
|
|
269
269
|
</Component>
|
|
270
270
|
);
|
|
271
271
|
};
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
"use client";
|
|
2
2
|
|
|
3
3
|
import type { ComponentPropsWithRef, FC } from "react";
|
|
4
|
+
import { createElement } from "react";
|
|
4
5
|
import { Button } from '..';
|
|
5
6
|
import { cx } from '../../../utils/cx';
|
|
6
7
|
import { isReactComponent } from '../../../utils/is-react-component';
|
|
@@ -20,9 +21,7 @@ export const RoundButton = ({ icon: Icon, ...props }: RoundButtonProps) => {
|
|
|
20
21
|
)}
|
|
21
22
|
>
|
|
22
23
|
{props.children ??
|
|
23
|
-
(isReactComponent(Icon) ? (
|
|
24
|
-
<Icon className="size-5 text-fg-quaternary transition-inherit-all group-hover:text-fg-quaternary_hover md:size-6" />
|
|
25
|
-
) : null)}
|
|
24
|
+
(isReactComponent(Icon) ? createElement(Icon, { className: "size-5 text-fg-quaternary transition-inherit-all group-hover:text-fg-quaternary_hover md:size-6" } as Record<string, unknown>) : null)}
|
|
26
25
|
</Button>
|
|
27
26
|
);
|
|
28
27
|
};
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
"use client";
|
|
2
2
|
|
|
3
3
|
import type { CSSProperties, ComponentPropsWithRef, HTMLAttributes, KeyboardEvent, ReactNode, Ref } from "react";
|
|
4
|
-
import { cloneElement, createContext, isValidElement, useCallback, useContext, useEffect,
|
|
4
|
+
import { cloneElement, createContext, isValidElement, useCallback, useContext, useEffect, useRef, useSyncExternalStore } from "react";
|
|
5
5
|
import useEmblaCarousel, { type UseEmblaCarouselType } from "embla-carousel-react";
|
|
6
6
|
import { cx } from '../../../utils/cx';
|
|
7
7
|
|
|
@@ -52,6 +52,14 @@ export const useCarousel = () => {
|
|
|
52
52
|
return context;
|
|
53
53
|
};
|
|
54
54
|
|
|
55
|
+
// Stable default snapshot for SSR - must be a constant to avoid infinite loops
|
|
56
|
+
const DEFAULT_SNAPSHOT = {
|
|
57
|
+
canScrollPrev: false,
|
|
58
|
+
canScrollNext: false,
|
|
59
|
+
selectedIndex: 0,
|
|
60
|
+
scrollSnaps: [] as number[],
|
|
61
|
+
};
|
|
62
|
+
|
|
55
63
|
const CarouselRoot = ({ orientation = "horizontal", opts, setApi, plugins, className, children, ...props }: ComponentPropsWithRef<"div"> & CarouselProps) => {
|
|
56
64
|
const [carouselRef, api] = useEmblaCarousel(
|
|
57
65
|
{
|
|
@@ -60,24 +68,58 @@ const CarouselRoot = ({ orientation = "horizontal", opts, setApi, plugins, class
|
|
|
60
68
|
},
|
|
61
69
|
plugins,
|
|
62
70
|
);
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
const
|
|
66
|
-
|
|
71
|
+
|
|
72
|
+
// Cache the snapshot object to avoid creating new objects on every call
|
|
73
|
+
const snapshotRef = useRef(DEFAULT_SNAPSHOT);
|
|
74
|
+
|
|
75
|
+
const getSnapshot = useCallback(() => {
|
|
76
|
+
if (!api) {
|
|
77
|
+
return DEFAULT_SNAPSHOT;
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
const canScrollPrev = api.canScrollPrev();
|
|
81
|
+
const canScrollNext = api.canScrollNext();
|
|
82
|
+
const selectedIndex = api.selectedScrollSnap();
|
|
83
|
+
const scrollSnaps = api.scrollSnapList();
|
|
84
|
+
|
|
85
|
+
// Only create a new object if values have changed
|
|
86
|
+
if (
|
|
87
|
+
snapshotRef.current.canScrollPrev !== canScrollPrev ||
|
|
88
|
+
snapshotRef.current.canScrollNext !== canScrollNext ||
|
|
89
|
+
snapshotRef.current.selectedIndex !== selectedIndex ||
|
|
90
|
+
snapshotRef.current.scrollSnaps.length !== scrollSnaps.length ||
|
|
91
|
+
snapshotRef.current.scrollSnaps.some((val, idx) => val !== scrollSnaps[idx])
|
|
92
|
+
) {
|
|
93
|
+
snapshotRef.current = {
|
|
94
|
+
canScrollPrev,
|
|
95
|
+
canScrollNext,
|
|
96
|
+
selectedIndex,
|
|
97
|
+
scrollSnaps,
|
|
98
|
+
};
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
return snapshotRef.current;
|
|
102
|
+
}, [api]);
|
|
67
103
|
|
|
68
|
-
|
|
69
|
-
|
|
104
|
+
// Stable server snapshot - returns the same cached object reference for SSR
|
|
105
|
+
const getServerSnapshot = useCallback(() => DEFAULT_SNAPSHOT, []);
|
|
70
106
|
|
|
71
|
-
|
|
72
|
-
|
|
107
|
+
const subscribe = useCallback(
|
|
108
|
+
(onStoreChange: () => void) => {
|
|
109
|
+
if (!api) return () => {};
|
|
73
110
|
|
|
74
|
-
|
|
75
|
-
|
|
111
|
+
api.on("select", onStoreChange);
|
|
112
|
+
api.on("reInit", onStoreChange);
|
|
76
113
|
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
114
|
+
return () => {
|
|
115
|
+
api.off("select", onStoreChange);
|
|
116
|
+
api.off("reInit", onStoreChange);
|
|
117
|
+
};
|
|
118
|
+
},
|
|
119
|
+
[api],
|
|
120
|
+
);
|
|
121
|
+
|
|
122
|
+
const { canScrollPrev, canScrollNext, selectedIndex, scrollSnaps } = useSyncExternalStore(subscribe, getSnapshot, getServerSnapshot);
|
|
81
123
|
|
|
82
124
|
const scrollPrev = useCallback(() => {
|
|
83
125
|
api?.scrollPrev();
|
|
@@ -106,21 +148,6 @@ const CarouselRoot = ({ orientation = "horizontal", opts, setApi, plugins, class
|
|
|
106
148
|
setApi(api);
|
|
107
149
|
}, [api, setApi]);
|
|
108
150
|
|
|
109
|
-
useEffect(() => {
|
|
110
|
-
if (!api) return;
|
|
111
|
-
|
|
112
|
-
onInit(api);
|
|
113
|
-
onSelect(api);
|
|
114
|
-
|
|
115
|
-
api.on("reInit", onInit);
|
|
116
|
-
api.on("reInit", onSelect);
|
|
117
|
-
api.on("select", onSelect);
|
|
118
|
-
|
|
119
|
-
return () => {
|
|
120
|
-
api?.off("select", onSelect);
|
|
121
|
-
};
|
|
122
|
-
}, [api, onInit, onSelect]);
|
|
123
|
-
|
|
124
151
|
return (
|
|
125
152
|
<CarouselContext.Provider
|
|
126
153
|
value={{
|
|
@@ -192,7 +219,11 @@ const Trigger = ({ className, children, asChild, direction, style, ...props }: T
|
|
|
192
219
|
const handleClick = () => {
|
|
193
220
|
if (isDisabled) return;
|
|
194
221
|
|
|
195
|
-
direction === "prev"
|
|
222
|
+
if (direction === "prev") {
|
|
223
|
+
scrollPrev();
|
|
224
|
+
} else {
|
|
225
|
+
scrollNext();
|
|
226
|
+
}
|
|
196
227
|
};
|
|
197
228
|
|
|
198
229
|
const computedClassName = typeof className === "function" ? className({ isDisabled }) : className;
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
"use client";
|
|
2
2
|
|
|
3
3
|
import type { CSSProperties, ComponentPropsWithRef, HTMLAttributes, KeyboardEvent, ReactNode, Ref } from "react";
|
|
4
|
-
import { cloneElement, createContext, isValidElement, useCallback, useContext, useEffect,
|
|
4
|
+
import { cloneElement, createContext, isValidElement, useCallback, useContext, useEffect, useRef, useSyncExternalStore } from "react";
|
|
5
5
|
import useEmblaCarousel, { type UseEmblaCarouselType } from "embla-carousel-react";
|
|
6
6
|
import { cx } from '../../../utils/cx';
|
|
7
7
|
|
|
@@ -52,6 +52,14 @@ export const useCarousel = () => {
|
|
|
52
52
|
return context;
|
|
53
53
|
};
|
|
54
54
|
|
|
55
|
+
// Stable default snapshot for SSR - must be a constant to avoid infinite loops
|
|
56
|
+
const DEFAULT_SNAPSHOT = {
|
|
57
|
+
canScrollPrev: false,
|
|
58
|
+
canScrollNext: false,
|
|
59
|
+
selectedIndex: 0,
|
|
60
|
+
scrollSnaps: [] as number[],
|
|
61
|
+
};
|
|
62
|
+
|
|
55
63
|
const CarouselRoot = ({ orientation = "horizontal", opts, setApi, plugins, className, children, ...props }: ComponentPropsWithRef<"div"> & CarouselProps) => {
|
|
56
64
|
const [carouselRef, api] = useEmblaCarousel(
|
|
57
65
|
{
|
|
@@ -60,24 +68,58 @@ const CarouselRoot = ({ orientation = "horizontal", opts, setApi, plugins, class
|
|
|
60
68
|
},
|
|
61
69
|
plugins,
|
|
62
70
|
);
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
const
|
|
66
|
-
|
|
71
|
+
|
|
72
|
+
// Cache the snapshot object to avoid creating new objects on every call
|
|
73
|
+
const snapshotRef = useRef(DEFAULT_SNAPSHOT);
|
|
74
|
+
|
|
75
|
+
const getSnapshot = useCallback(() => {
|
|
76
|
+
if (!api) {
|
|
77
|
+
return DEFAULT_SNAPSHOT;
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
const canScrollPrev = api.canScrollPrev();
|
|
81
|
+
const canScrollNext = api.canScrollNext();
|
|
82
|
+
const selectedIndex = api.selectedScrollSnap();
|
|
83
|
+
const scrollSnaps = api.scrollSnapList();
|
|
84
|
+
|
|
85
|
+
// Only create a new object if values have changed
|
|
86
|
+
if (
|
|
87
|
+
snapshotRef.current.canScrollPrev !== canScrollPrev ||
|
|
88
|
+
snapshotRef.current.canScrollNext !== canScrollNext ||
|
|
89
|
+
snapshotRef.current.selectedIndex !== selectedIndex ||
|
|
90
|
+
snapshotRef.current.scrollSnaps.length !== scrollSnaps.length ||
|
|
91
|
+
snapshotRef.current.scrollSnaps.some((val, idx) => val !== scrollSnaps[idx])
|
|
92
|
+
) {
|
|
93
|
+
snapshotRef.current = {
|
|
94
|
+
canScrollPrev,
|
|
95
|
+
canScrollNext,
|
|
96
|
+
selectedIndex,
|
|
97
|
+
scrollSnaps,
|
|
98
|
+
};
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
return snapshotRef.current;
|
|
102
|
+
}, [api]);
|
|
67
103
|
|
|
68
|
-
|
|
69
|
-
|
|
104
|
+
// Stable server snapshot - returns the same cached object reference for SSR
|
|
105
|
+
const getServerSnapshot = useCallback(() => DEFAULT_SNAPSHOT, []);
|
|
70
106
|
|
|
71
|
-
|
|
72
|
-
|
|
107
|
+
const subscribe = useCallback(
|
|
108
|
+
(onStoreChange: () => void) => {
|
|
109
|
+
if (!api) return () => {};
|
|
73
110
|
|
|
74
|
-
|
|
75
|
-
|
|
111
|
+
api.on("select", onStoreChange);
|
|
112
|
+
api.on("reInit", onStoreChange);
|
|
76
113
|
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
114
|
+
return () => {
|
|
115
|
+
api.off("select", onStoreChange);
|
|
116
|
+
api.off("reInit", onStoreChange);
|
|
117
|
+
};
|
|
118
|
+
},
|
|
119
|
+
[api],
|
|
120
|
+
);
|
|
121
|
+
|
|
122
|
+
const { canScrollPrev, canScrollNext, selectedIndex, scrollSnaps } = useSyncExternalStore(subscribe, getSnapshot, getServerSnapshot);
|
|
81
123
|
|
|
82
124
|
const scrollPrev = useCallback(() => {
|
|
83
125
|
api?.scrollPrev();
|
|
@@ -106,21 +148,6 @@ const CarouselRoot = ({ orientation = "horizontal", opts, setApi, plugins, class
|
|
|
106
148
|
setApi(api);
|
|
107
149
|
}, [api, setApi]);
|
|
108
150
|
|
|
109
|
-
useEffect(() => {
|
|
110
|
-
if (!api) return;
|
|
111
|
-
|
|
112
|
-
onInit(api);
|
|
113
|
-
onSelect(api);
|
|
114
|
-
|
|
115
|
-
api.on("reInit", onInit);
|
|
116
|
-
api.on("reInit", onSelect);
|
|
117
|
-
api.on("select", onSelect);
|
|
118
|
-
|
|
119
|
-
return () => {
|
|
120
|
-
api?.off("select", onSelect);
|
|
121
|
-
};
|
|
122
|
-
}, [api, onInit, onSelect]);
|
|
123
|
-
|
|
124
151
|
return (
|
|
125
152
|
<CarouselContext.Provider
|
|
126
153
|
value={{
|
|
@@ -192,7 +219,11 @@ const Trigger = ({ className, children, asChild, direction, style, ...props }: T
|
|
|
192
219
|
const handleClick = () => {
|
|
193
220
|
if (isDisabled) return;
|
|
194
221
|
|
|
195
|
-
direction === "prev"
|
|
222
|
+
if (direction === "prev") {
|
|
223
|
+
scrollPrev();
|
|
224
|
+
} else {
|
|
225
|
+
scrollNext();
|
|
226
|
+
}
|
|
196
227
|
};
|
|
197
228
|
|
|
198
229
|
const computedClassName = typeof className === "function" ? className({ isDisabled }) : className;
|
|
@@ -4,7 +4,7 @@ import type { DateInputProps as AriaDateInputProps } from "react-aria-components
|
|
|
4
4
|
import { DateInput as AriaDateInput, DateSegment as AriaDateSegment } from "react-aria-components";
|
|
5
5
|
import { cx } from '../../../utils/cx';
|
|
6
6
|
|
|
7
|
-
|
|
7
|
+
type DateInputProps = Omit<AriaDateInputProps, "children">;
|
|
8
8
|
|
|
9
9
|
export const DateInput = (props: DateInputProps) => {
|
|
10
10
|
return (
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import type { FC, ReactNode, Ref } from "react";
|
|
2
|
-
import { isValidElement } from "react";
|
|
2
|
+
import { createElement, isValidElement } from "react";
|
|
3
3
|
import { cx, sortCx } from '../../../utils/cx';
|
|
4
4
|
import { isReactComponent } from '../../../utils/is-react-component';
|
|
5
5
|
|
|
@@ -145,7 +145,7 @@ export const FeaturedIcon = (props: FeaturedIconProps) => {
|
|
|
145
145
|
props.className,
|
|
146
146
|
)}
|
|
147
147
|
>
|
|
148
|
-
{isReactComponent(Icon) &&
|
|
148
|
+
{isReactComponent(Icon) && createElement(Icon, { 'data-icon': true, className: "z-1" } as Record<string, unknown>)}
|
|
149
149
|
{isValidElement(Icon) && <div className="z-1">{Icon}</div>}
|
|
150
150
|
|
|
151
151
|
{props.children}
|