aplance-ui 1.2.2 → 1.4.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (26) hide show
  1. package/package.json +1 -1
  2. package/registry/components/primitives/modal/modal.tsx +16 -10
  3. package/registry/components/primitives/popover/popover-context.ts +33 -28
  4. package/registry/components/primitives/popover/popover.tsx +145 -26
  5. package/registry/components/primitives/slider/slider.module.css +62 -5
  6. package/registry/components/primitives/slider/slider.tsx +192 -34
  7. package/registry/components/primitives/slider/types.ts +56 -3
  8. package/registry/components/primitives/slider/utils.ts +9 -0
  9. package/registry/components/ui/copy-button/copy-button.tsx +5 -0
  10. package/registry/components/ui/data-table/data-table.module.css +695 -613
  11. package/registry/components/ui/data-table/data-table.tsx +136 -112
  12. package/registry/components/ui/data-table/filter-modal.tsx +86 -0
  13. package/registry/components/ui/data-table/types.ts +8 -0
  14. package/registry/components/ui/data-table/useDataTable.tsx +1 -0
  15. package/registry/components/ui/data-table/utils.ts +26 -1
  16. package/registry/components/ui/input-fields/custom-select/custom-select.module.css +32 -1
  17. package/registry/components/ui/input-fields/custom-select/custom-select.tsx +500 -469
  18. package/registry/components/ui/input-fields/date-picker/date-picker.tsx +940 -933
  19. package/registry/components/ui/input-fields/date-picker/types.ts +2 -0
  20. package/registry/components/ui/input-fields/emoji-picker/emoji-picker.tsx +8 -0
  21. package/registry/components/ui/input-fields/input/input.tsx +80 -69
  22. package/registry/components/ui/tabs/tabs.module.css +22 -8
  23. package/registry/components/ui/tabs/tabs.tsx +29 -12
  24. package/registry/hooks/useHasMounted.ts +22 -0
  25. package/registry/hooks/useMediaQuery.ts +38 -0
  26. package/registry/registry.json +41 -4
@@ -10,6 +10,7 @@ import {
10
10
  useRef,
11
11
  useState,
12
12
  type CSSProperties,
13
+ type MouseEvent as ReactMouseEvent,
13
14
  type PointerEvent as ReactPointerEvent,
14
15
  type ReactNode,
15
16
  type TransitionEvent as ReactTransitionEvent,
@@ -21,11 +22,16 @@ import { useResizeObserver } from "../../../hooks/useResizeObserver";
21
22
  import { SliderContext, useSlider, type SliderApi } from "./slider-context";
22
23
  import type {
23
24
  SliderAutoplayOptions,
25
+ SliderArrowProps,
26
+ SliderDotsProps,
27
+ SliderIndicatorProps,
24
28
  SliderNavigationOptions,
29
+ SliderPaginationItems,
25
30
  SliderPaginationOptions,
26
31
  SliderPaginationType,
27
32
  SliderProps,
28
33
  SliderSlideState,
34
+ SliderVariantOptions,
29
35
  } from "./types";
30
36
  import {
31
37
  clamp,
@@ -41,13 +47,18 @@ export {
41
47
  sliderAlignments,
42
48
  sliderEffects,
43
49
  sliderPaginationTypes,
50
+ sliderVariants,
44
51
  } from "./types";
45
52
  export type {
46
53
  SliderAlign,
47
54
  SliderAutoplayOptions,
55
+ SliderArrowProps,
56
+ SliderDotsProps,
48
57
  SliderEffect,
49
58
  SliderHandle,
59
+ SliderIndicatorProps,
50
60
  SliderNavigationOptions,
61
+ SliderPaginationItems,
51
62
  SliderPaginationOptions,
52
63
  SliderPaginationType,
53
64
  SliderProps,
@@ -55,17 +66,65 @@ export type {
55
66
  SliderResponsiveOptions,
56
67
  SliderSlideState,
57
68
  SliderSlidesPerView,
69
+ SliderVariant,
70
+ SliderVariantOptions,
58
71
  } from "./types";
59
72
 
73
+ // Swallow the browser click after a swipe so linked slides stay draggable.
74
+ const DRAG_CLICK_TOLERANCE = 6;
75
+ const EMPTY_VARIANT_OPTIONS: SliderVariantOptions = {};
76
+
77
+ type SliderCssProperties = CSSProperties &
78
+ Record<`--slider-${string}`, string | number | undefined>;
79
+
80
+ const resolveVariantOptions = (
81
+ variant: "default" | "cards" | "coverflow",
82
+ options: SliderVariantOptions,
83
+ ): Required<SliderVariantOptions> => {
84
+ const offset = typeof options.offset === "number" && Number.isFinite(options.offset)
85
+ ? Math.max(0, options.offset)
86
+ : variant === "cards"
87
+ ? 6
88
+ : 0;
89
+ const scaleStep = typeof options.scaleStep === "number" && Number.isFinite(options.scaleStep)
90
+ ? clamp(options.scaleStep, 0, 1)
91
+ : variant === "cards"
92
+ ? 0.07
93
+ : 0.08;
94
+ const opacityStep = typeof options.opacityStep === "number" && Number.isFinite(options.opacityStep)
95
+ ? clamp(options.opacityStep, 0, 1)
96
+ : variant === "cards"
97
+ ? 0.16
98
+ : 0.2;
99
+ const rotate = typeof options.rotate === "number" && Number.isFinite(options.rotate)
100
+ ? clamp(options.rotate, 0, 90)
101
+ : 38;
102
+ const depth = typeof options.depth === "number" && Number.isFinite(options.depth)
103
+ ? Math.max(0, options.depth)
104
+ : 96;
105
+
106
+ return {
107
+ offset,
108
+ scaleStep,
109
+ opacityStep,
110
+ rotate,
111
+ depth,
112
+ };
113
+ };
114
+
60
115
  // ─── Slider ───────────────────────────────────────────────────────────────────
61
116
 
62
117
  export const Slider = ({
63
118
  children,
64
119
  className,
120
+ viewportClassName,
121
+ trackClassName,
65
122
  style,
66
123
  ref,
67
124
  ariaLabel = "Slider",
68
125
  effect: effectProp = "slide",
126
+ variant: variantProp = "default",
127
+ variantOptions: variantOptionsProp = EMPTY_VARIANT_OPTIONS,
69
128
  align: alignProp = "start",
70
129
  centered: centeredProp = false,
71
130
  slidesPerView: slidesPerViewProp = 1,
@@ -82,7 +141,8 @@ export const Slider = ({
82
141
  easing = "cubic-bezier(0.4, 0, 0.2, 1)",
83
142
  draggable = true,
84
143
  swipeThreshold = 30,
85
- dragIgnoreSelector = "a, button, input, textarea, select, summary, [role='button'], [data-slider-no-drag]",
144
+ // Linked slides stay draggable. Click suppression distinguishes swipes from taps.
145
+ dragIgnoreSelector = "button, input, textarea, select, summary, [role='button'], [data-slider-no-drag]",
86
146
  keyboard = false,
87
147
  mousewheel = false,
88
148
  autoHeight = false,
@@ -166,6 +226,8 @@ export const Slider = ({
166
226
  centered: centeredProp,
167
227
  align: alignProp,
168
228
  effect: effectProp,
229
+ variant: variantProp,
230
+ variantOptions: variantOptionsProp,
169
231
  },
170
232
  responsive,
171
233
  ),
@@ -177,6 +239,8 @@ export const Slider = ({
177
239
  slidesPerGroupProp,
178
240
  slidesPerViewProp,
179
241
  spaceBetweenProp,
242
+ variantProp,
243
+ variantOptionsProp,
180
244
  viewportWidth,
181
245
  ],
182
246
  );
@@ -200,6 +264,8 @@ export const Slider = ({
200
264
  const align = responsiveOptions.centered ? "center" : responsiveOptions.align;
201
265
  const effect = responsiveOptions.effect;
202
266
  const effectMode = effect;
267
+ const variant = effectMode === "slide" ? responsiveOptions.variant : "default";
268
+ const variantOptions = resolveVariantOptions(variant, responsiveOptions.variantOptions);
203
269
  const canLoop = loop && count > 1 && effectMode === "slide";
204
270
  const baseMaxIndex =
205
271
  effectMode === "slide" ? getSnapMax(count, slidesPerView) : Math.max(0, count - 1);
@@ -255,6 +321,10 @@ export const Slider = ({
255
321
  }
256
322
  }, [count, isControlled, loop, maxIndex]);
257
323
 
324
+ useLayoutEffect(() => {
325
+ if (autoHeight) measure();
326
+ }, [autoHeight, logicalIndex, measure]);
327
+
258
328
  // Clone N slides on each side so dragging/grouping past the edge always
259
329
  // has real DOM under the cursor. `+1` is a buffer for partial-view layouts;
260
330
  // `slidesPerView=auto` is unknowable up-front so we clone the full set.
@@ -294,6 +364,13 @@ export const Slider = ({
294
364
  const [isFocusWithin, setIsFocusWithin] = useState(false);
295
365
  const [autoplayStopped, setAutoplayStopped] = useState(false);
296
366
  const wheelLockRef = useRef(false);
367
+ const suppressClickRef = useRef(false);
368
+ // True once a pointer interaction has crossed the drag threshold and become a
369
+ // real drag (so we capture the pointer + move the track). A plain tap never
370
+ // flips this, so it never captures the pointer - which is what lets a card's
371
+ // <a> receive the click (a captured pointer makes the browser dispatch the
372
+ // `click` to the captured viewport, not the link, so navigation silently dies).
373
+ const dragActiveRef = useRef(false);
297
374
  const layoutSignatureRef = useRef("");
298
375
  const dragStartRef = useRef<{
299
376
  x: number;
@@ -310,6 +387,7 @@ export const Slider = ({
310
387
  slidesPerGroup,
311
388
  slidesPerView,
312
389
  spaceBetween,
390
+ variant,
313
391
  viewportWidth,
314
392
  ].join("|");
315
393
 
@@ -387,6 +465,9 @@ export const Slider = ({
387
465
  ? clamp(baseOffset - alignmentOffset, 0, maxScroll)
388
466
  : baseOffset - alignmentOffset;
389
467
  const translate = -targetOffset + dragOffset;
468
+ const visualPosition = visualIndex - dragOffset / Math.max(1, averageStride);
469
+ const visibleStart = targetOffset - dragOffset;
470
+ const visibleEnd = visibleStart + viewportWidth;
390
471
  const progress = snapCount <= 1 ? 1 : clamp(logicalIndex / (snapCount - 1), 0, 1);
391
472
  const canNavigateLoop = loop && count > 1;
392
473
  const isFirst = !canNavigateLoop && logicalIndex <= 0;
@@ -636,6 +717,8 @@ export const Slider = ({
636
717
  // ─── Pointer / keyboard / wheel ─────────────────────────────────────────────
637
718
 
638
719
  const handlePointerDown = (event: ReactPointerEvent<HTMLDivElement>) => {
720
+ suppressClickRef.current = false;
721
+ dragActiveRef.current = false;
639
722
  if (
640
723
  !draggable ||
641
724
  count <= 1 ||
@@ -647,6 +730,11 @@ export const Slider = ({
647
730
  if (event.pointerType === "mouse" && event.button !== 0) return;
648
731
 
649
732
  markInteraction();
733
+ // Record the START point only. We do NOT capture the pointer or enter the
734
+ // dragging state yet: a plain click must stay a plain click so the card's
735
+ // <a> receives it (a captured pointer makes the browser deliver the `click`
736
+ // to the captured viewport, not the link). The drag is promoted in
737
+ // `handlePointerMove` once the pointer travels past the threshold.
650
738
  dragStartRef.current = {
651
739
  x: event.clientX,
652
740
  y: event.clientY,
@@ -654,9 +742,6 @@ export const Slider = ({
654
742
  logicalIndex,
655
743
  };
656
744
  verticalLockRef.current = false;
657
- setIsDragging(true);
658
- setIsAnimating(false);
659
- event.currentTarget.setPointerCapture?.(event.pointerId);
660
745
  };
661
746
 
662
747
  const handlePointerMove = (event: ReactPointerEvent<HTMLDivElement>) => {
@@ -666,17 +751,31 @@ export const Slider = ({
666
751
  const dx = event.clientX - start.x;
667
752
  const dy = event.clientY - start.y;
668
753
 
669
- if (event.pointerType !== "mouse" && !verticalLockRef.current) {
754
+ // Before the drag is committed, a clearly-vertical touch is a page scroll -
755
+ // abandon the gesture so the page scrolls normally.
756
+ if (
757
+ event.pointerType !== "mouse" &&
758
+ !verticalLockRef.current &&
759
+ !dragActiveRef.current
760
+ ) {
670
761
  if (Math.abs(dy) > Math.abs(dx) && Math.abs(dy) > 8) {
671
762
  verticalLockRef.current = true;
672
763
  dragStartRef.current = null;
673
- setIsDragging(false);
674
- setDragOffset(0);
675
- event.currentTarget.releasePointerCapture?.(event.pointerId);
676
764
  return;
677
765
  }
678
766
  }
679
767
 
768
+ // Promote to a real drag only once past the threshold. Until then this is
769
+ // still a potential tap, so we neither capture the pointer nor move the
770
+ // track - which is what keeps link clicks working.
771
+ if (!dragActiveRef.current) {
772
+ if (Math.abs(dx) <= DRAG_CLICK_TOLERANCE) return;
773
+ dragActiveRef.current = true;
774
+ setIsDragging(true);
775
+ setIsAnimating(false);
776
+ event.currentTarget.setPointerCapture?.(event.pointerId);
777
+ }
778
+
680
779
  // Rubber-band at non-loop edges: dampen drag so users feel the bound
681
780
  // without breaking the snap-back on release.
682
781
  const shouldRubberBand =
@@ -690,9 +789,14 @@ export const Slider = ({
690
789
 
691
790
  const dx = clientX - start.x;
692
791
  dragStartRef.current = null;
792
+ dragActiveRef.current = false;
693
793
  setIsDragging(false);
694
794
  setDragOffset(0);
695
795
 
796
+ if (Math.abs(dx) > DRAG_CLICK_TOLERANCE) {
797
+ suppressClickRef.current = true;
798
+ }
799
+
696
800
  if (Math.abs(dx) < resolvedSwipeThreshold) return;
697
801
 
698
802
  const steps = Math.max(1, Math.round(Math.abs(dx) / Math.max(1, averageStride)));
@@ -700,16 +804,31 @@ export const Slider = ({
700
804
  else prev(steps);
701
805
  };
702
806
 
807
+ const releaseCapture = (event: ReactPointerEvent<HTMLDivElement>) => {
808
+ if (event.currentTarget.hasPointerCapture?.(event.pointerId)) {
809
+ event.currentTarget.releasePointerCapture?.(event.pointerId);
810
+ }
811
+ };
812
+
703
813
  const handlePointerUp = (event: ReactPointerEvent<HTMLDivElement>) => {
704
814
  finishDrag(event.clientX);
705
- event.currentTarget.releasePointerCapture?.(event.pointerId);
815
+ releaseCapture(event);
706
816
  };
707
817
 
708
818
  const handlePointerCancel = (event: ReactPointerEvent<HTMLDivElement>) => {
709
819
  dragStartRef.current = null;
820
+ dragActiveRef.current = false;
710
821
  setIsDragging(false);
711
822
  setDragOffset(0);
712
- event.currentTarget.releasePointerCapture?.(event.pointerId);
823
+ releaseCapture(event);
824
+ };
825
+
826
+ const handleClickCapture = (event: ReactMouseEvent<HTMLDivElement>) => {
827
+ if (suppressClickRef.current) {
828
+ event.preventDefault();
829
+ event.stopPropagation();
830
+ suppressClickRef.current = false;
831
+ }
713
832
  };
714
833
 
715
834
  const handleKeyDown = (event: React.KeyboardEvent<HTMLDivElement>) => {
@@ -845,9 +964,15 @@ export const Slider = ({
845
964
  className={clsx(
846
965
  s.container,
847
966
  effectMode !== "slide" && s.container_effect,
967
+ isDragging && s.container_dragging,
968
+ (!isMotionReady || transitionOff) && s.container_no_transition,
848
969
  className,
849
970
  )}
850
- style={style}
971
+ style={{
972
+ "--slider-transition-duration": `${transitionDuration}ms`,
973
+ "--slider-transition-easing": easing,
974
+ ...style,
975
+ } as SliderCssProperties}
851
976
  role="region"
852
977
  aria-roledescription="carousel"
853
978
  aria-label={ariaLabel}
@@ -876,7 +1001,11 @@ export const Slider = ({
876
1001
  >
877
1002
  <div
878
1003
  ref={viewportRef}
879
- className={s.viewport}
1004
+ className={clsx(
1005
+ s.viewport,
1006
+ variant === "coverflow" && s.viewport_coverflow,
1007
+ viewportClassName,
1008
+ )}
880
1009
  style={{
881
1010
  ...viewportStyle,
882
1011
  visibility: hasMeasured ? undefined : "hidden",
@@ -887,6 +1016,7 @@ export const Slider = ({
887
1016
  onPointerMove={handlePointerMove}
888
1017
  onPointerUp={handlePointerUp}
889
1018
  onPointerCancel={handlePointerCancel}
1019
+ onClickCapture={handleClickCapture}
890
1020
  onDragStart={(event) => event.preventDefault()}
891
1021
  >
892
1022
  <div
@@ -895,6 +1025,9 @@ export const Slider = ({
895
1025
  s.track,
896
1026
  effectMode === "fade" && s.track_fade,
897
1027
  effectMode === "scale" && s.track_scale,
1028
+ effectMode === "flip" && s.track_flip,
1029
+ variant === "coverflow" && s.track_coverflow,
1030
+ trackClassName,
898
1031
  )}
899
1032
  style={trackStyle}
900
1033
  onTransitionEnd={handleTrackTransitionEnd}
@@ -907,13 +1040,21 @@ export const Slider = ({
907
1040
  canLoop && (renderedIndex < cloneCount || renderedIndex >= cloneCount + count);
908
1041
  const visibleCount =
909
1042
  slidesPerView === "auto" ? 1 : Math.ceil(slidesPerView);
1043
+ const slideOffset = slideOffsets[renderedIndex];
1044
+ const slideSize = slideSizes[renderedIndex];
910
1045
  const isActive =
911
1046
  realIndex === logicalIndex &&
912
1047
  (effectMode === "slide" ? renderedIndex === visualIndex : !isClone);
913
1048
  const isVisible = effectMode !== "slide"
914
1049
  ? realIndex === logicalIndex
915
- : renderedIndex >= visualIndex &&
916
- renderedIndex < visualIndex + visibleCount;
1050
+ : hasMeasured &&
1051
+ viewportWidth > 0 &&
1052
+ slideOffset !== undefined &&
1053
+ slideSize !== undefined
1054
+ ? slideOffset + slideSize > visibleStart &&
1055
+ slideOffset < visibleEnd
1056
+ : renderedIndex >= visualIndex &&
1057
+ renderedIndex < visualIndex + visibleCount;
917
1058
  const state: SliderSlideState = {
918
1059
  index: realIndex,
919
1060
  renderedIndex,
@@ -921,6 +1062,24 @@ export const Slider = ({
921
1062
  isVisible,
922
1063
  isClone,
923
1064
  };
1065
+ const slideDistance = clamp(renderedIndex - visualPosition, -3, 3);
1066
+ const slideAbsDistance = Math.abs(slideDistance);
1067
+ const variantStyle = variant === "default"
1068
+ ? undefined
1069
+ : {
1070
+ "--slider-slide-offset": `${slideAbsDistance * variantOptions.offset}px`,
1071
+ "--slider-slide-scale": Math.max(
1072
+ variant === "cards" ? 0.86 : 0.82,
1073
+ 1 - slideAbsDistance * variantOptions.scaleStep,
1074
+ ),
1075
+ "--slider-slide-depth": `${slideAbsDistance * variantOptions.depth * -1}px`,
1076
+ "--slider-slide-rotate": `${slideDistance * variantOptions.rotate * -1}deg`,
1077
+ opacity: Math.max(
1078
+ variant === "cards" ? 0.64 : 0.5,
1079
+ 1 - slideAbsDistance * variantOptions.opacityStep,
1080
+ ),
1081
+ zIndex: Math.max(0, 10 - Math.round(slideAbsDistance)),
1082
+ } as SliderCssProperties;
924
1083
 
925
1084
  return (
926
1085
  <div
@@ -936,11 +1095,22 @@ export const Slider = ({
936
1095
  className={clsx(
937
1096
  s.slide,
938
1097
  effectMode !== "slide" && s.slide_overlay,
1098
+ variant === "cards" && s.slide_cards,
1099
+ variant === "coverflow" && s.slide_coverflow,
939
1100
  isActive && s.slide_active,
940
1101
  isVisible && s.slide_visible,
941
1102
  )}
942
- style={slideStyle}
943
- aria-hidden={isClone || (!isVisible && effectMode !== "slide") || undefined}
1103
+ style={variantStyle ? { ...slideStyle, ...variantStyle } : slideStyle}
1104
+ // Only CLONES are inert + hidden from AT (they're duplicate,
1105
+ // non-interactive copies). REAL slides stay interactive even
1106
+ // when scrolled out of view: they're clipped by the viewport's
1107
+ // `overflow`, so an off-screen card can't be clicked anyway,
1108
+ // and gating `inert` on the measurement-dependent `isVisible`
1109
+ // flag would intermittently make an on-screen card
1110
+ // non-clickable (the swipe/measure race the user hit - the
1111
+ // link shows on hover, but the click lands while inert).
1112
+ aria-hidden={isClone || undefined}
1113
+ inert={isClone ? true : undefined}
944
1114
  data-active={isActive ? "true" : undefined}
945
1115
  data-visible={isVisible ? "true" : undefined}
946
1116
  data-clone={isClone ? "true" : undefined}
@@ -1022,7 +1192,7 @@ const SliderNavButton = ({
1022
1192
  type SliderPaginationViewProps = {
1023
1193
  type: SliderPaginationType;
1024
1194
  clickable: boolean;
1025
- items: "snaps" | "slides";
1195
+ items: SliderPaginationItems;
1026
1196
  className?: string;
1027
1197
  };
1028
1198
 
@@ -1092,11 +1262,7 @@ export const SliderPrev = ({
1092
1262
  className,
1093
1263
  children,
1094
1264
  ariaLabel = "Previous slide",
1095
- }: {
1096
- className?: string;
1097
- children?: ReactNode;
1098
- ariaLabel?: string;
1099
- }) => {
1265
+ }: SliderArrowProps) => {
1100
1266
  const { prev, isFirst } = useSlider();
1101
1267
  return (
1102
1268
  <button
@@ -1116,11 +1282,7 @@ export const SliderNext = ({
1116
1282
  className,
1117
1283
  children,
1118
1284
  ariaLabel = "Next slide",
1119
- }: {
1120
- className?: string;
1121
- children?: ReactNode;
1122
- ariaLabel?: string;
1123
- }) => {
1285
+ }: SliderArrowProps) => {
1124
1286
  const { next, isLast } = useSlider();
1125
1287
  return (
1126
1288
  <button
@@ -1140,11 +1302,7 @@ export const SliderDots = ({
1140
1302
  className,
1141
1303
  clickable = true,
1142
1304
  items = "snaps",
1143
- }: {
1144
- className?: string;
1145
- clickable?: boolean;
1146
- items?: "snaps" | "slides";
1147
- }) => (
1305
+ }: SliderDotsProps) => (
1148
1306
  <SliderPaginationView
1149
1307
  type="dots"
1150
1308
  clickable={clickable}
@@ -1153,7 +1311,7 @@ export const SliderDots = ({
1153
1311
  />
1154
1312
  );
1155
1313
 
1156
- export const SliderFraction = ({ className }: { className?: string }) => (
1314
+ export const SliderFraction = ({ className }: SliderIndicatorProps) => (
1157
1315
  <SliderPaginationView
1158
1316
  type="fraction"
1159
1317
  clickable={false}
@@ -1162,7 +1320,7 @@ export const SliderFraction = ({ className }: { className?: string }) => (
1162
1320
  />
1163
1321
  );
1164
1322
 
1165
- export const SliderProgress = ({ className }: { className?: string }) => (
1323
+ export const SliderProgress = ({ className }: SliderIndicatorProps) => (
1166
1324
  <SliderPaginationView
1167
1325
  type="progress"
1168
1326
  clickable={false}
@@ -6,17 +6,35 @@ import type {
6
6
  } from "react";
7
7
  import type { SliderApi } from "./slider-context";
8
8
 
9
- export const sliderEffects = ["slide", "fade", "scale"] as const;
9
+ export const sliderEffects = ["slide", "fade", "scale", "flip"] as const;
10
10
  export type SliderEffect = (typeof sliderEffects)[number];
11
11
 
12
+ export const sliderVariants = ["default", "cards", "coverflow"] as const;
13
+ export type SliderVariant = (typeof sliderVariants)[number];
14
+
12
15
  export const sliderAlignments = ["start", "center", "end"] as const;
13
16
  export type SliderAlign = (typeof sliderAlignments)[number];
14
17
 
15
18
  export const sliderPaginationTypes = ["dots", "fraction", "progress"] as const;
16
19
  export type SliderPaginationType = (typeof sliderPaginationTypes)[number];
17
20
 
21
+ export type SliderPaginationItems = "snaps" | "slides";
22
+
18
23
  export type SliderSlidesPerView = number | "auto";
19
24
 
25
+ export type SliderVariantOptions = {
26
+ /** Vertical offset per slide distance in px. */
27
+ offset?: number;
28
+ /** Scale reduction per slide distance. */
29
+ scaleStep?: number;
30
+ /** Opacity reduction per slide distance. */
31
+ opacityStep?: number;
32
+ /** Coverflow rotation per slide distance in degrees. */
33
+ rotate?: number;
34
+ /** Coverflow depth reduction per slide distance in px. */
35
+ depth?: number;
36
+ };
37
+
20
38
  export type SliderResponsiveOptions = {
21
39
  slidesPerView?: SliderSlidesPerView;
22
40
  slidesPerGroup?: number;
@@ -24,6 +42,8 @@ export type SliderResponsiveOptions = {
24
42
  centered?: boolean;
25
43
  align?: SliderAlign;
26
44
  effect?: SliderEffect;
45
+ variant?: SliderVariant;
46
+ variantOptions?: SliderVariantOptions;
27
47
  };
28
48
 
29
49
  /** Responsive overrides keyed by minimum viewport width in px. */
@@ -55,7 +75,23 @@ export type SliderPaginationOptions = {
55
75
  /** When true, dots/fraction items are clickable. */
56
76
  clickable?: boolean;
57
77
  /** Paginate by snap pages or every real slide. */
58
- items?: "snaps" | "slides";
78
+ items?: SliderPaginationItems;
79
+ className?: string;
80
+ };
81
+
82
+ export type SliderArrowProps = {
83
+ className?: string;
84
+ children?: ReactNode;
85
+ ariaLabel?: string;
86
+ };
87
+
88
+ export type SliderDotsProps = {
89
+ className?: string;
90
+ clickable?: boolean;
91
+ items?: SliderPaginationItems;
92
+ };
93
+
94
+ export type SliderIndicatorProps = {
59
95
  className?: string;
60
96
  };
61
97
 
@@ -81,12 +117,29 @@ export type SliderHandle = {
81
117
  export type SliderProps = {
82
118
  children: ReactNode;
83
119
  className?: string;
120
+ /** Class merged onto the inner clipping viewport. Use it to add vertical
121
+ * breathing room (e.g. `padding-block`) when slides carry borders /
122
+ * shadows / hover states that would otherwise be shaved by the viewport's
123
+ * `overflow: hidden`. Keep it to vertical padding only - horizontal padding
124
+ * would skew the slide-width measurement. */
125
+ viewportClassName?: string;
126
+ /** Class merged onto the moving track (the flex strip that holds the slides).
127
+ * Use it when the slider must fill a definite-height parent (e.g. a
128
+ * full-screen lightbox): set the track to `height: 100%` so it - and the
129
+ * stretch-aligned slides - inherit the viewport height instead of collapsing
130
+ * to slide content. */
131
+ trackClassName?: string;
84
132
  style?: CSSProperties;
85
133
  ref?: Ref<SliderHandle>;
86
134
  ariaLabel?: string;
87
135
 
88
- /** Slide transition effect. `fade` and `scale` are single-view effects. */
136
+ /** Slide transition effect. `fade`, `scale`, and `flip` are single-view effects. */
89
137
  effect?: SliderEffect;
138
+ /** Visual treatment for slide layouts. `cards` and `coverflow` enhance the
139
+ * standard slide effect without changing navigation behavior. */
140
+ variant?: SliderVariant;
141
+ /** Optional tuning for `cards` and `coverflow` presentation variants. */
142
+ variantOptions?: SliderVariantOptions;
90
143
  /** Alignment of the active slide inside the viewport. */
91
144
  align?: SliderAlign;
92
145
  /** Center active slides. Alias for align="center". */
@@ -73,6 +73,15 @@ export const resolveResponsive = (
73
73
  if (options.effect !== undefined) {
74
74
  active.effect = options.effect;
75
75
  }
76
+ if (options.variant !== undefined) {
77
+ active.variant = options.variant;
78
+ }
79
+ if (options.variantOptions !== undefined) {
80
+ active.variantOptions = {
81
+ ...active.variantOptions,
82
+ ...options.variantOptions,
83
+ };
84
+ }
76
85
  }
77
86
 
78
87
  return active;
@@ -18,6 +18,9 @@ export type CopyButtonProps = {
18
18
  layout?: ButtonLayout;
19
19
  preview?: CopyPreview;
20
20
  previewMaxLength?: number;
21
+ /** Called with the value after a successful copy (e.g. to toast, or clear a
22
+ * one-time reveal). Not called if the copy fails. */
23
+ onCopy?: (value: string) => void;
21
24
  };
22
25
 
23
26
  const maskToken = (raw: string): string => {
@@ -44,6 +47,7 @@ export function CopyButton({
44
47
  layout,
45
48
  preview = "none",
46
49
  previewMaxLength = 24,
50
+ onCopy,
47
51
  }: CopyButtonProps) {
48
52
  const [copied, setCopied] = useState(false);
49
53
  const timeoutRef = useRef<number | null>(null);
@@ -82,6 +86,7 @@ export function CopyButton({
82
86
  const success = await copyWithFallback(value);
83
87
  if (!success) return;
84
88
 
89
+ onCopy?.(value);
85
90
  setCopied(true);
86
91
  if (timeoutRef.current !== null) {
87
92
  window.clearTimeout(timeoutRef.current);