@tinybigui/react 0.21.1 → 0.22.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/dist/index.js CHANGED
@@ -7716,13 +7716,14 @@ var DialogPanel = ({
7716
7716
  headlineId,
7717
7717
  contentId,
7718
7718
  onClose,
7719
- onTransitionEnd,
7719
+ onAnimationEnd,
7720
7720
  variant,
7721
7721
  isDismissable,
7722
7722
  wrapperClassName,
7723
7723
  className,
7724
7724
  animationState,
7725
7725
  getAnimationClassName,
7726
+ icon,
7726
7727
  children
7727
7728
  }) => {
7728
7729
  const panelRef = useRef(null);
@@ -7745,9 +7746,10 @@ var DialogPanel = ({
7745
7746
  panelRef
7746
7747
  );
7747
7748
  const panelClassName = cn(className, getAnimationClassName?.(animationState));
7749
+ const hasIcon = icon !== void 0 && icon !== null && variant === "basic";
7748
7750
  return (
7749
7751
  // Centering/positioning wrapper — structural only, no ARIA role
7750
- /* @__PURE__ */ jsx("div", { className: wrapperClassName, children: /* @__PURE__ */ jsx(
7752
+ /* @__PURE__ */ jsx("div", { className: wrapperClassName, children: /* @__PURE__ */ jsxs(
7751
7753
  "div",
7752
7754
  {
7753
7755
  ...mergeProps(overlayProps, dialogProps),
@@ -7756,8 +7758,12 @@ var DialogPanel = ({
7756
7758
  className: panelClassName,
7757
7759
  "data-animation-state": animationState,
7758
7760
  "data-variant": variant,
7759
- onTransitionEnd,
7760
- children
7761
+ "data-with-icon": hasIcon ? "" : void 0,
7762
+ onAnimationEnd,
7763
+ children: [
7764
+ hasIcon && icon,
7765
+ children
7766
+ ]
7761
7767
  }
7762
7768
  ) })
7763
7769
  );
@@ -7770,9 +7776,11 @@ var DialogHeadless = forwardRef(
7770
7776
  defaultOpen = false,
7771
7777
  onOpenChange,
7772
7778
  "aria-label": ariaLabel,
7779
+ icon,
7773
7780
  children,
7774
7781
  className,
7775
- scrimClassName,
7782
+ wrapperClassName,
7783
+ getScrimClassName,
7776
7784
  getAnimationClassName
7777
7785
  }, _ref) {
7778
7786
  const state = useOverlayTriggerState({
@@ -7806,7 +7814,7 @@ var DialogHeadless = forwardRef(
7806
7814
  closedRef.current = true;
7807
7815
  setAnimationState("exited");
7808
7816
  }
7809
- }, 150);
7817
+ }, 250);
7810
7818
  }
7811
7819
  }, [isOpen, animationState]);
7812
7820
  useEffect(
@@ -7817,16 +7825,20 @@ var DialogHeadless = forwardRef(
7817
7825
  },
7818
7826
  []
7819
7827
  );
7820
- const handleTransitionEnd = useCallback(() => {
7821
- if (animationState === "exiting" && !closedRef.current) {
7822
- if (exitFallbackRef.current !== null) {
7823
- clearTimeout(exitFallbackRef.current);
7824
- exitFallbackRef.current = null;
7828
+ const handleAnimationEnd = useCallback(
7829
+ (e) => {
7830
+ if (e.target !== e.currentTarget) return;
7831
+ if (animationState === "exiting" && !closedRef.current) {
7832
+ if (exitFallbackRef.current !== null) {
7833
+ clearTimeout(exitFallbackRef.current);
7834
+ exitFallbackRef.current = null;
7835
+ }
7836
+ closedRef.current = true;
7837
+ setAnimationState("exited");
7825
7838
  }
7826
- closedRef.current = true;
7827
- setAnimationState("exited");
7828
- }
7829
- }, [animationState]);
7839
+ },
7840
+ [animationState]
7841
+ );
7830
7842
  const baseId = useId();
7831
7843
  const headlineId = `${baseId}-dialog-headline`;
7832
7844
  const contentId = `${baseId}-dialog-content`;
@@ -7841,6 +7853,8 @@ var DialogHeadless = forwardRef(
7841
7853
  close();
7842
7854
  }
7843
7855
  }, [variant, close]);
7856
+ const resolvedWrapperClass = wrapperClassName ?? (variant === "basic" ? "fixed inset-0 z-50 flex items-center justify-center px-4" : "fixed inset-0 z-50");
7857
+ const resolvedScrimClass = getScrimClassName?.(animationState) ?? "fixed inset-0 z-40 bg-scrim/32";
7844
7858
  if (!isOpen && animationState === "exited") {
7845
7859
  return null;
7846
7860
  }
@@ -7849,7 +7863,7 @@ var DialogHeadless = forwardRef(
7849
7863
  "div",
7850
7864
  {
7851
7865
  "data-testid": "dialog-scrim",
7852
- className: scrimClassName,
7866
+ className: resolvedScrimClass,
7853
7867
  onClick: handleScrimClick,
7854
7868
  "aria-hidden": "true"
7855
7869
  }
@@ -7861,13 +7875,14 @@ var DialogHeadless = forwardRef(
7861
7875
  headlineId,
7862
7876
  contentId,
7863
7877
  onClose: close,
7864
- onTransitionEnd: handleTransitionEnd,
7878
+ onAnimationEnd: handleAnimationEnd,
7865
7879
  variant,
7866
7880
  isDismissable: variant === "basic",
7867
- wrapperClassName: variant === "basic" ? "fixed inset-0 z-50 flex items-center justify-center px-4" : "fixed inset-0 z-50",
7881
+ wrapperClassName: resolvedWrapperClass,
7868
7882
  className,
7869
7883
  animationState,
7870
7884
  getAnimationClassName,
7885
+ icon,
7871
7886
  children
7872
7887
  }
7873
7888
  ) })
@@ -7877,16 +7892,28 @@ var DialogHeadless = forwardRef(
7877
7892
  }
7878
7893
  );
7879
7894
  DialogHeadless.displayName = "DialogHeadless";
7880
- var dialogScrimVariants = cva([
7881
- "fixed",
7882
- "inset-0",
7883
- "z-40",
7884
- "bg-scrim",
7885
- "opacity-32",
7886
- "transition-opacity",
7887
- "duration-medium2",
7888
- "ease-standard"
7889
- ]);
7895
+ var dialogScrimVariants = cva(
7896
+ [
7897
+ "fixed",
7898
+ "inset-0",
7899
+ "z-40",
7900
+ // MD3 scrim: bg-scrim at 32% opacity — always set so instant-show works in reduced-motion
7901
+ "bg-scrim/32"
7902
+ ],
7903
+ {
7904
+ variants: {
7905
+ animationState: {
7906
+ entering: ["opacity-0"],
7907
+ visible: ["animate-md-fade-in"],
7908
+ exiting: ["animate-md-fade-out"],
7909
+ exited: ["opacity-0", "pointer-events-none"]
7910
+ }
7911
+ },
7912
+ defaultVariants: {
7913
+ animationState: "entering"
7914
+ }
7915
+ }
7916
+ );
7890
7917
  var dialogPanelVariants = cva(
7891
7918
  [
7892
7919
  // Stacking above scrim
@@ -7896,9 +7923,10 @@ var dialogPanelVariants = cva(
7896
7923
  // Flex column layout for slots
7897
7924
  "flex",
7898
7925
  "flex-col",
7899
- // Transition for animation state changes
7900
- "transition-[opacity,transform]",
7901
- "will-change-[opacity,transform]"
7926
+ // Compositor hint for keyframe animation
7927
+ "will-change-[opacity,transform]",
7928
+ // group scope — lets child slots consume data-with-icon via group-data-[with-icon]/dialog:
7929
+ "group/dialog"
7902
7930
  ],
7903
7931
  {
7904
7932
  variants: {
@@ -7912,11 +7940,11 @@ var dialogPanelVariants = cva(
7912
7940
  "min-w-70",
7913
7941
  "max-w-dialog-max",
7914
7942
  "w-full",
7915
- // Internal spacing
7943
+ // Internal spacing: 24dp padding, headline mb-4, content mb-6, actions pt-3
7916
7944
  "pt-6",
7917
7945
  "pb-3",
7918
7946
  "px-6",
7919
- // Positioned in viewport center
7947
+ // Positioning (centering wrapper handles the viewport centering)
7920
7948
  "relative"
7921
7949
  ],
7922
7950
  fullscreen: [
@@ -7927,7 +7955,6 @@ var dialogPanelVariants = cva(
7927
7955
  "rounded-none",
7928
7956
  // No elevation shadow on fullscreen
7929
7957
  "shadow-none",
7930
- // Positioned to fill portal
7931
7958
  "relative"
7932
7959
  ]
7933
7960
  }
@@ -7937,7 +7964,7 @@ var dialogPanelVariants = cva(
7937
7964
  }
7938
7965
  }
7939
7966
  );
7940
- cva([], {
7967
+ var dialogWrapperVariants = cva([], {
7941
7968
  variants: {
7942
7969
  variant: {
7943
7970
  basic: ["fixed", "inset-0", "z-50", "flex", "items-center", "justify-center", "px-4"],
@@ -7951,9 +7978,13 @@ cva([], {
7951
7978
  var dialogAnimationVariants = cva("", {
7952
7979
  variants: {
7953
7980
  animationState: {
7981
+ // initial mount frame before the animation starts — rendered invisible
7954
7982
  entering: [],
7983
+ // entry animation active
7955
7984
  visible: [],
7985
+ // exit animation active
7956
7986
  exiting: [],
7987
+ // fully dismissed; portal gate will remove the element
7957
7988
  exited: []
7958
7989
  },
7959
7990
  variant: {
@@ -7962,53 +7993,55 @@ var dialogAnimationVariants = cva("", {
7962
7993
  }
7963
7994
  },
7964
7995
  compoundVariants: [
7965
- // Basic: entering — start scaled down + transparent
7996
+ // ── Basic ────────────────────────────────────────────────────────────────
7997
+ // entering: start invisible (animate-md-scale-in keyframe starts from scale(0.85)/opacity:0)
7966
7998
  {
7967
7999
  animationState: "entering",
7968
8000
  variant: "basic",
7969
- className: ["scale-90", "opacity-0"]
8001
+ className: ["opacity-0"]
7970
8002
  },
7971
- // Basic: visible scale to full + fade in
8003
+ // visible: composite scale-in keyframe (expressive-fast-spatial 350ms)
7972
8004
  {
7973
8005
  animationState: "visible",
7974
8006
  variant: "basic",
7975
- className: ["scale-100", "opacity-100", "duration-medium4", "ease-emphasized-decelerate"]
8007
+ className: ["animate-md-scale-in"]
7976
8008
  },
7977
- // Basic: exiting fade out (scale stays at 1)
8009
+ // exiting: composite scale-out keyframe (emphasized-accelerate 200ms)
7978
8010
  {
7979
8011
  animationState: "exiting",
7980
8012
  variant: "basic",
7981
- className: ["scale-100", "opacity-0", "duration-short2", "ease-emphasized-accelerate"]
8013
+ className: ["animate-md-scale-out"]
7982
8014
  },
7983
- // Basic: exited fully transparent
8015
+ // exited: keep hidden until portal gate removes it
7984
8016
  {
7985
8017
  animationState: "exited",
7986
8018
  variant: "basic",
7987
- className: ["scale-100", "opacity-0"]
8019
+ className: ["opacity-0", "pointer-events-none"]
7988
8020
  },
7989
- // Fullscreen: entering — start below viewport + transparent
8021
+ // ── Fullscreen ───────────────────────────────────────────────────────────
8022
+ // entering: start off-screen below (slide-in-bottom starts from translateY(100%)/opacity:0)
7990
8023
  {
7991
8024
  animationState: "entering",
7992
8025
  variant: "fullscreen",
7993
- className: ["translate-y-full", "opacity-0"]
8026
+ className: ["opacity-0"]
7994
8027
  },
7995
- // Fullscreen: visible slide up + fade in
8028
+ // visible: composite slide-in-bottom keyframe (standard-default-spatial 500ms)
7996
8029
  {
7997
8030
  animationState: "visible",
7998
8031
  variant: "fullscreen",
7999
- className: ["translate-y-0", "opacity-100", "duration-medium4", "ease-emphasized-decelerate"]
8032
+ className: ["animate-md-slide-in-bottom"]
8000
8033
  },
8001
- // Fullscreen: exiting slide down + fade out
8034
+ // exiting: composite slide-out-bottom keyframe (emphasized-accelerate 200ms)
8002
8035
  {
8003
8036
  animationState: "exiting",
8004
8037
  variant: "fullscreen",
8005
- className: ["translate-y-full", "opacity-0", "duration-short2", "ease-emphasized-accelerate"]
8038
+ className: ["animate-md-slide-out-bottom"]
8006
8039
  },
8007
- // Fullscreen: exited fully off-screen
8040
+ // exited: keep hidden until portal gate removes it
8008
8041
  {
8009
8042
  animationState: "exited",
8010
8043
  variant: "fullscreen",
8011
- className: ["translate-y-full", "opacity-0"]
8044
+ className: ["opacity-0", "pointer-events-none"]
8012
8045
  }
8013
8046
  ],
8014
8047
  defaultVariants: {
@@ -8016,10 +8049,26 @@ var dialogAnimationVariants = cva("", {
8016
8049
  variant: "basic"
8017
8050
  }
8018
8051
  });
8052
+ cva([
8053
+ // Center the icon in the panel
8054
+ "flex",
8055
+ "items-center",
8056
+ "justify-center",
8057
+ // Bottom margin separating icon from headline
8058
+ "mb-4",
8059
+ // MD3 spec: icon color = secondary
8060
+ "text-secondary",
8061
+ // 24dp icon size (children — typically an SVG — should be 24×24)
8062
+ "size-6"
8063
+ ]);
8019
8064
  var dialogHeadlineVariants = cva(["text-headline-small", "text-on-surface"], {
8020
8065
  variants: {
8021
8066
  variant: {
8022
- basic: ["mb-4"],
8067
+ basic: [
8068
+ "mb-4",
8069
+ // Center headline text when hero icon is present
8070
+ "group-data-[with-icon]/dialog:text-center"
8071
+ ],
8023
8072
  fullscreen: [
8024
8073
  // Top app bar row in fullscreen: flex, items-center, gap
8025
8074
  "flex",
@@ -8044,7 +8093,19 @@ var dialogHeadlineTitleVariants = cva([
8044
8093
  "truncate"
8045
8094
  ]);
8046
8095
  var dialogContentVariants = cva(
8047
- ["text-body-medium", "text-on-surface-variant", "overflow-y-auto", "flex-1"],
8096
+ [
8097
+ "text-body-medium",
8098
+ "text-on-surface-variant",
8099
+ "overflow-y-auto",
8100
+ "flex-1",
8101
+ // Center supporting text when hero icon is present
8102
+ "group-data-[with-icon]/dialog:text-center",
8103
+ // Scroll dividers — activated by DialogContent's scroll handler
8104
+ "data-[scroll-divider-top]:border-t",
8105
+ "data-[scroll-divider-top]:border-outline-variant",
8106
+ "data-[scroll-divider-bottom]:border-b",
8107
+ "data-[scroll-divider-bottom]:border-outline-variant"
8108
+ ],
8048
8109
  {
8049
8110
  variants: {
8050
8111
  variant: {
@@ -8071,11 +8132,31 @@ var Dialog = forwardRef(function Dialog2({
8071
8132
  defaultOpen = false,
8072
8133
  onOpenChange,
8073
8134
  "aria-label": ariaLabel,
8135
+ icon,
8074
8136
  children,
8075
8137
  className
8076
8138
  }, _ref) {
8077
- const panelClassName = cn(dialogPanelVariants({ variant }), className);
8078
- const scrimClass = dialogScrimVariants();
8139
+ const reducedMotion = useReducedMotion();
8140
+ const panelClassName = cn(
8141
+ dialogPanelVariants({ variant }),
8142
+ reducedMotion && "transition-none",
8143
+ className
8144
+ );
8145
+ const wrapperClassName = dialogWrapperVariants({ variant });
8146
+ const getScrimClassName = useCallback(
8147
+ (state) => {
8148
+ if (reducedMotion) return "fixed inset-0 z-40 bg-scrim/32";
8149
+ return dialogScrimVariants({ animationState: state });
8150
+ },
8151
+ [reducedMotion]
8152
+ );
8153
+ const getAnimationClassName = useCallback(
8154
+ (state) => {
8155
+ if (reducedMotion) return "";
8156
+ return dialogAnimationVariants({ animationState: state, variant });
8157
+ },
8158
+ [reducedMotion, variant]
8159
+ );
8079
8160
  return /* @__PURE__ */ jsx(
8080
8161
  DialogHeadless,
8081
8162
  {
@@ -8084,9 +8165,11 @@ var Dialog = forwardRef(function Dialog2({
8084
8165
  ...defaultOpen !== void 0 ? { defaultOpen } : {},
8085
8166
  ...onOpenChange !== void 0 ? { onOpenChange } : {},
8086
8167
  ...ariaLabel ? { "aria-label": ariaLabel } : {},
8168
+ ...icon !== void 0 ? { icon } : {},
8087
8169
  className: panelClassName,
8088
- scrimClassName: scrimClass,
8089
- getAnimationClassName: (state) => dialogAnimationVariants({ animationState: state, variant }),
8170
+ wrapperClassName,
8171
+ getScrimClassName,
8172
+ getAnimationClassName,
8090
8173
  children
8091
8174
  }
8092
8175
  );
@@ -8097,7 +8180,7 @@ var DialogHeadline = forwardRef(
8097
8180
  const { headlineId, variant } = useDialogContext();
8098
8181
  if (variant === "fullscreen") {
8099
8182
  return (
8100
- // Top app bar row for fullscreen variant
8183
+ // Top app bar row for fullscreen variant — always has border-b border-outline-variant
8101
8184
  /* @__PURE__ */ jsxs("div", { className: cn(dialogHeadlineVariants({ variant: "fullscreen" }), className), children: [
8102
8185
  closeButton,
8103
8186
  /* @__PURE__ */ jsx("h2", { id: headlineId, className: dialogHeadlineTitleVariants(), children }),
@@ -8105,21 +8188,72 @@ var DialogHeadline = forwardRef(
8105
8188
  ] })
8106
8189
  );
8107
8190
  }
8108
- return /* @__PURE__ */ jsx(
8109
- "h2",
8110
- {
8111
- ref,
8112
- id: headlineId,
8113
- className: cn(dialogHeadlineVariants({ variant: "basic" }), className),
8114
- children
8115
- }
8191
+ return (
8192
+ // Basic variant: text-headline-small, text-on-surface, mb-4
8193
+ // group-data-[with-icon]/dialog:text-center is applied via the CVA base classes
8194
+ // when the parent panel root has data-with-icon set by DialogHeadless.
8195
+ /* @__PURE__ */ jsx(
8196
+ "h2",
8197
+ {
8198
+ ref,
8199
+ id: headlineId,
8200
+ className: cn(dialogHeadlineVariants({ variant: "basic" }), className),
8201
+ children
8202
+ }
8203
+ )
8116
8204
  );
8117
8205
  }
8118
8206
  );
8119
8207
  DialogHeadline.displayName = "DialogHeadline";
8120
- var DialogContent = forwardRef(function DialogContent2({ children, className }, ref) {
8208
+ var DialogContent = forwardRef(function DialogContent2({ children, className }, forwardedRef) {
8121
8209
  const { contentId, variant } = useDialogContext();
8122
- return /* @__PURE__ */ jsx("div", { ref, id: contentId, className: cn(dialogContentVariants({ variant }), className), children });
8210
+ const internalRef = useRef(null);
8211
+ const setRef = useCallback(
8212
+ (node) => {
8213
+ internalRef.current = node;
8214
+ if (typeof forwardedRef === "function") {
8215
+ forwardedRef(node);
8216
+ } else if (forwardedRef !== null && forwardedRef !== void 0) {
8217
+ forwardedRef.current = node;
8218
+ }
8219
+ },
8220
+ [forwardedRef]
8221
+ );
8222
+ const updateDividers = useCallback(() => {
8223
+ const el = internalRef.current;
8224
+ if (!el) return;
8225
+ const isScrollable = el.scrollHeight > el.clientHeight;
8226
+ if (!isScrollable) {
8227
+ el.removeAttribute("data-scroll-divider-top");
8228
+ el.removeAttribute("data-scroll-divider-bottom");
8229
+ return;
8230
+ }
8231
+ const scrolledFromTop = el.scrollTop > 1;
8232
+ const scrolledToBottom = el.scrollTop + el.clientHeight >= el.scrollHeight - 1;
8233
+ if (scrolledFromTop) {
8234
+ el.setAttribute("data-scroll-divider-top", "");
8235
+ } else {
8236
+ el.removeAttribute("data-scroll-divider-top");
8237
+ }
8238
+ if (!scrolledToBottom) {
8239
+ el.setAttribute("data-scroll-divider-bottom", "");
8240
+ } else {
8241
+ el.removeAttribute("data-scroll-divider-bottom");
8242
+ }
8243
+ }, []);
8244
+ useEffect(() => {
8245
+ const el = internalRef.current;
8246
+ if (!el) return;
8247
+ updateDividers();
8248
+ el.addEventListener("scroll", updateDividers, { passive: true });
8249
+ const observer = new ResizeObserver(updateDividers);
8250
+ observer.observe(el);
8251
+ return () => {
8252
+ el.removeEventListener("scroll", updateDividers);
8253
+ observer.disconnect();
8254
+ };
8255
+ }, [updateDividers]);
8256
+ return /* @__PURE__ */ jsx("div", { ref: setRef, id: contentId, className: cn(dialogContentVariants({ variant }), className), children });
8123
8257
  });
8124
8258
  DialogContent.displayName = "DialogContent";
8125
8259
  var DialogActions = forwardRef(function DialogActions2({ children, className }, ref) {