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.
Files changed (81) hide show
  1. package/package.json +8 -2
  2. package/src/design_system/elements/avatar/avatar-profile-photo.tsx +5 -2
  3. package/src/design_system/elements/avatar/avatar.tsx +2 -1
  4. package/src/design_system/elements/avatar/base-components/avatar-company-icon.tsx +5 -2
  5. package/src/design_system/elements/badges/avatar.tsx +2 -1
  6. package/src/design_system/elements/badges/badge-groups.tsx +3 -3
  7. package/src/design_system/elements/badges/badges.tsx +3 -2
  8. package/src/design_system/elements/button-group/button-group.tsx +3 -3
  9. package/src/design_system/elements/buttons/button-utility.tsx +2 -2
  10. package/src/design_system/elements/buttons/button.aman.tsx +2 -2
  11. package/src/design_system/elements/buttons/button.tsx +2 -2
  12. package/src/design_system/elements/buttons/round-button.tsx +2 -3
  13. package/src/design_system/elements/carousel/carousel-base.tsx +62 -31
  14. package/src/design_system/elements/carousel/carousel.tsx +62 -31
  15. package/src/design_system/elements/date-picker/date-input.tsx +1 -1
  16. package/src/design_system/elements/featured-icon/featured-icon.tsx +2 -2
  17. package/src/design_system/elements/index.tsx +39 -35
  18. package/src/design_system/elements/input/input.aman.tsx +1 -2
  19. package/src/design_system/elements/input/input.tsx +1 -3
  20. package/src/design_system/elements/map/GoogleMap.tsx +89 -30
  21. package/src/design_system/elements/markdown-renderer/MarkdownRenderer.tsx +40 -28
  22. package/src/design_system/elements/pagination/pagination-base.tsx +2 -9
  23. package/src/design_system/elements/photo-fallback/photo-fallback.tsx +25 -12
  24. package/src/design_system/elements/select/multi-select.tsx +4 -9
  25. package/src/design_system/elements/select/select.aman.tsx +0 -2
  26. package/src/design_system/elements/shared-assets/credit-card/credit-card.tsx +2 -2
  27. package/src/design_system/elements/tooltip/tooltip.tsx +1 -1
  28. package/src/design_system/elements/video-modal.tsx +1 -1
  29. package/src/design_system/hooks/use-breakpoint.ts +18 -15
  30. package/src/design_system/sections/about-home.aman.tsx +2 -2
  31. package/src/design_system/sections/about-home.tsx +15 -12
  32. package/src/design_system/sections/blog-cards.tsx +3 -3
  33. package/src/design_system/sections/blog-gallery.aman.tsx +1 -1
  34. package/src/design_system/sections/blog-gallery.tsx +3 -3
  35. package/src/design_system/sections/blog-home.aman.tsx +3 -3
  36. package/src/design_system/sections/blog-post.aman.tsx +1 -1
  37. package/src/design_system/sections/blog-post.tsx +30 -20
  38. package/src/design_system/sections/blog-section.aman.tsx +1 -1
  39. package/src/design_system/sections/blog-section.tsx +0 -4
  40. package/src/design_system/sections/contact-home.tsx +2 -2
  41. package/src/design_system/sections/contact-section.aman.tsx +0 -2
  42. package/src/design_system/sections/contact-section.tsx +3 -3
  43. package/src/design_system/sections/faq-grid.aman.tsx +1 -1
  44. package/src/design_system/sections/faq-home.aman.tsx +1 -1
  45. package/src/design_system/sections/feature-text.tsx +5 -4
  46. package/src/design_system/sections/footer-home.aman.tsx +1 -2
  47. package/src/design_system/sections/footer-home.tsx +26 -25
  48. package/src/design_system/sections/generic-header-component.tsx +3 -3
  49. package/src/design_system/sections/header-navigation.aman.tsx +19 -14
  50. package/src/design_system/sections/header-navigation.tsx +5 -4
  51. package/src/design_system/sections/hero-home.aman.tsx +2 -2
  52. package/src/design_system/sections/hero-home.tsx +1 -4
  53. package/src/design_system/sections/hero-location-detail.aman.tsx +3 -1
  54. package/src/design_system/sections/home-hero-component.tsx +7 -5
  55. package/src/design_system/sections/index.tsx +18 -10
  56. package/src/design_system/sections/job-gallery.aman.tsx +1 -6
  57. package/src/design_system/sections/job-gallery.tsx +3 -3
  58. package/src/design_system/sections/location-details-section.aman.tsx +1 -1
  59. package/src/design_system/sections/location-details-section.tsx +1 -1
  60. package/src/design_system/sections/location-grid.aman.tsx +1 -1
  61. package/src/design_system/sections/location-grid.tsx +1 -1
  62. package/src/design_system/sections/services-home.tsx +1 -1
  63. package/src/design_system/sections/social-media-grid.aman.tsx +6 -6
  64. package/src/design_system/sections/social-media-grid.tsx +4 -1
  65. package/src/design_system/sections/statistics-section.aman.tsx +1 -1
  66. package/src/design_system/sections/statistics-section.tsx +3 -3
  67. package/src/design_system/sections/team-grid.aman.tsx +1 -1
  68. package/src/design_system/sections/testimonials-grid.aman.tsx +3 -3
  69. package/src/design_system/sections/testimonials-home.aman.tsx +3 -3
  70. package/src/design_system/sections/values-section.aman.tsx +2 -3
  71. package/src/lib/component-registry.ts +11 -7
  72. package/src/lib/hooks/use-breakpoint.ts +18 -15
  73. package/src/lib/server-api.ts +1 -1
  74. package/src/pages/.gitkeep +3 -0
  75. package/src/types/api/blog-post.ts +1 -1
  76. package/src/types/api/contact.ts +1 -1
  77. package/src/types/api/team-member.ts +1 -1
  78. package/src/types/api/website-photos.ts +1 -0
  79. package/src/utils/countries.tsx +5 -1
  80. package/src/utils/is-react-component.ts +18 -9
  81. 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.8",
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
- <img
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 <img data-avatar-img className="size-full rounded-full object-cover" src={src} alt={alt} onError={() => setIsFailed(true)} />;
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
- <img
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 <img data-avatar-img className="size-full rounded-full object-cover" src={src} alt={alt} onError={() => setIsFailed(true)} />;
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) && <IconTrailing className={iconClasses} />}
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) && <IconTrailing className={iconClasses} />}
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
- <img src={`https://www.untitledui.com/images/flags/${flag}.svg`} className="size-4 max-w-none rounded-full" alt={`${flag} flag`} />
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
- <img src={imgSrc} className="size-4 max-w-none rounded-full" alt="Badge image" />
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) && <IconLeading className={cx(styles.common.icon, styles.sizes[size].icon)} />}
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) && <IconTrailing className={cx(styles.common.icon, styles.sizes[size].icon)} />}
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) && <Icon data-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) && <IconLeading data-icon="leading" className={styles.common.icon} />}
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) && <IconTrailing data-icon="trailing" className={styles.common.icon} />}
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) && <IconLeading data-icon="leading" className={styles.common.icon} />}
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) && <IconTrailing data-icon="trailing" className={styles.common.icon} />}
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, useState } from "react";
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
- const [canScrollPrev, setCanScrollPrev] = useState(false);
64
- const [canScrollNext, setCanScrollNext] = useState(false);
65
- const [selectedIndex, setSelectedIndex] = useState(0);
66
- const [scrollSnaps, setScrollSnaps] = useState<number[]>([]);
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
- const onInit = useCallback((api: CarouselApi) => {
69
- if (!api) return;
104
+ // Stable server snapshot - returns the same cached object reference for SSR
105
+ const getServerSnapshot = useCallback(() => DEFAULT_SNAPSHOT, []);
70
106
 
71
- setScrollSnaps(api.scrollSnapList());
72
- }, []);
107
+ const subscribe = useCallback(
108
+ (onStoreChange: () => void) => {
109
+ if (!api) return () => {};
73
110
 
74
- const onSelect = useCallback((api: CarouselApi) => {
75
- if (!api) return;
111
+ api.on("select", onStoreChange);
112
+ api.on("reInit", onStoreChange);
76
113
 
77
- setCanScrollPrev(api.canScrollPrev());
78
- setCanScrollNext(api.canScrollNext());
79
- setSelectedIndex(api.selectedScrollSnap());
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" ? scrollPrev() : scrollNext();
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, useState } from "react";
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
- const [canScrollPrev, setCanScrollPrev] = useState(false);
64
- const [canScrollNext, setCanScrollNext] = useState(false);
65
- const [selectedIndex, setSelectedIndex] = useState(0);
66
- const [scrollSnaps, setScrollSnaps] = useState<number[]>([]);
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
- const onInit = useCallback((api: CarouselApi) => {
69
- if (!api) return;
104
+ // Stable server snapshot - returns the same cached object reference for SSR
105
+ const getServerSnapshot = useCallback(() => DEFAULT_SNAPSHOT, []);
70
106
 
71
- setScrollSnaps(api.scrollSnapList());
72
- }, []);
107
+ const subscribe = useCallback(
108
+ (onStoreChange: () => void) => {
109
+ if (!api) return () => {};
73
110
 
74
- const onSelect = useCallback((api: CarouselApi) => {
75
- if (!api) return;
111
+ api.on("select", onStoreChange);
112
+ api.on("reInit", onStoreChange);
76
113
 
77
- setCanScrollPrev(api.canScrollPrev());
78
- setCanScrollNext(api.canScrollNext());
79
- setSelectedIndex(api.selectedScrollSnap());
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" ? scrollPrev() : scrollNext();
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
- interface DateInputProps extends Omit<AriaDateInputProps, "children"> {}
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) && <Icon data-icon className="z-1" />}
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}