@tinybigui/react 0.11.1 → 0.11.2

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/README.md CHANGED
@@ -12,7 +12,7 @@ A modern, accessible React component library implementing Google's Material Desi
12
12
 
13
13
  ## ✅ Status
14
14
 
15
- > **Latest Release: v0.11.1** (2026-06-09)
15
+ > **Latest Release: v0.11.2** (2026-06-09)
16
16
  >
17
17
  > **29 MD3 components** published to npm with full TypeScript support and WCAG 2.1 AA accessibility.
18
18
  >
@@ -149,13 +149,13 @@ See [THEMING.md](./THEMING.md) for the full customization guide.
149
149
 
150
150
  ### Phase 2: Navigation ✅
151
151
 
152
- | Component | Status | Description |
153
- | ------------------ | ------ | ------------------------------------------------------------------- |
154
- | `AppBar` | ✅ | M3 expressive flexible slot architecture, subtitle growth (v0.10.0) |
155
- | `Tabs` | ✅ | Primary and secondary tab variants |
156
- | `NavigationDrawer` | ✅ | Modal and standard navigation drawer |
157
- | `NavigationBar` | ✅ | Bottom navigation with badges |
158
- | `Search` | ✅ | SearchBar and SearchView overlay |
152
+ | Component | Status | Description |
153
+ | ------------------ | ------ | -------------------------------------------------------------------- |
154
+ | `AppBar` | ✅ | M3 expressive flexible slot architecture, subtitle growth (v0.10.0) |
155
+ | `Tabs` | ✅ | MD3 expressive variants-vs-states, content-width indicator (v0.11.2) |
156
+ | `NavigationDrawer` | ✅ | Modal and standard navigation drawer |
157
+ | `NavigationBar` | ✅ | Bottom navigation with badges |
158
+ | `Search` | ✅ | SearchBar and SearchView overlay |
159
159
 
160
160
  ### Phase 3: Feedback ✅
161
161
 
package/dist/index.cjs CHANGED
@@ -3672,10 +3672,9 @@ var Tabs = React.forwardRef(
3672
3672
  Tabs.displayName = "Tabs";
3673
3673
  var tabListVariants = classVarianceAuthority.cva(
3674
3674
  [
3675
- // Base classes
3676
3675
  "relative flex",
3677
3676
  "bg-surface",
3678
- // Bottom divider line (MD3 spec)
3677
+ // MD3: 1dp bottom divider in outline-variant color
3679
3678
  "border-b border-outline-variant"
3680
3679
  ],
3681
3680
  {
@@ -3692,108 +3691,110 @@ var tabListVariants = classVarianceAuthority.cva(
3692
3691
  );
3693
3692
  var tabVariants = classVarianceAuthority.cva(
3694
3693
  [
3695
- // Base layout
3694
+ // Layout
3696
3695
  "relative flex flex-col items-center justify-center",
3697
3696
  "min-h-12 px-4",
3698
3697
  "cursor-pointer select-none",
3698
+ // Clip state layer to tab boundary
3699
3699
  "overflow-hidden",
3700
- // Typography: MD3 Title Small
3701
- "text-sm font-medium tracking-[0.1px]",
3702
- // Transition
3703
- "transition-colors duration-200",
3704
- // Focus visible
3700
+ // MD3 Title Small typography
3701
+ "text-title-small font-medium tracking-[0.1px]",
3702
+ // Effects transition for color changes (not spatial)
3703
+ "transition-colors duration-spring-standard-fast-effects ease-spring-standard-fast-effects",
3704
+ // Remove default button focus outline — handled via group-data-[focus-visible]/tab
3705
3705
  "focus-visible:outline-none",
3706
- // State layer via before pseudo-element
3707
- "before:absolute before:inset-0 before:transition-opacity before:duration-200",
3708
- "before:bg-current before:opacity-0",
3709
- "hover:before:opacity-8",
3710
- "active:before:opacity-12",
3711
- "focus-visible:before:opacity-12"
3706
+ // Disabled self-targeting data-[x]: selectors (not group)
3707
+ "data-[disabled]:opacity-38 data-[disabled]:cursor-not-allowed data-[disabled]:pointer-events-none",
3708
+ // Icon+label stacked: 64dp height
3709
+ "data-[with-icon]:data-[with-label]:min-h-16"
3712
3710
  ],
3713
3711
  {
3714
3712
  variants: {
3715
3713
  /**
3716
- * Tab variant (Primary or Secondary)
3714
+ * Design-time variant: affects active label/icon color and state-layer color.
3715
+ * primary: active → text-primary
3716
+ * secondary: active → text-on-surface
3717
3717
  */
3718
3718
  variant: {
3719
- primary: "",
3720
- secondary: ""
3721
- },
3722
- /**
3723
- * Selected state
3724
- */
3725
- selected: {
3726
- true: "",
3727
- false: ""
3728
- },
3729
- /**
3730
- * Disabled state
3731
- */
3732
- disabled: {
3733
- true: "opacity-38 cursor-not-allowed pointer-events-none",
3734
- false: ""
3719
+ primary: [
3720
+ // Inactive: on-surface-variant (base)
3721
+ "text-on-surface-variant",
3722
+ // Active: text-primary
3723
+ "group-data-[selected]/tab:text-primary"
3724
+ // Disabled active: on-surface/38 inherits from opacity-38 on root
3725
+ ],
3726
+ secondary: [
3727
+ // Inactive: on-surface-variant (base)
3728
+ "text-on-surface-variant",
3729
+ // Active: on-surface
3730
+ "group-data-[selected]/tab:text-on-surface"
3731
+ ]
3735
3732
  },
3736
3733
  /**
3737
- * Layout determines min-width behavior
3734
+ * Design-time variant: affects flex sizing in the tab row.
3738
3735
  */
3739
3736
  layout: {
3740
3737
  fixed: "flex-1",
3741
3738
  scrollable: "min-w-[90px] shrink-0"
3742
3739
  }
3743
3740
  },
3744
- compoundVariants: [
3745
- // Primary + selected
3746
- {
3747
- variant: "primary",
3748
- selected: true,
3749
- disabled: false,
3750
- className: "text-primary"
3751
- },
3752
- // Primary + unselected
3753
- {
3754
- variant: "primary",
3755
- selected: false,
3756
- disabled: false,
3757
- className: "text-on-surface-variant"
3758
- },
3759
- // Secondary + selected
3760
- {
3761
- variant: "secondary",
3762
- selected: true,
3763
- disabled: false,
3764
- className: "text-on-surface"
3765
- },
3766
- // Secondary + unselected
3767
- {
3768
- variant: "secondary",
3769
- selected: false,
3770
- disabled: false,
3771
- className: "text-on-surface-variant"
3772
- }
3773
- ],
3741
+ // compoundVariants intentionally empty — state combinations are handled
3742
+ // via group-data-[selected]/tab:group-data-[...]/tab: in state slot
3743
+ compoundVariants: [],
3774
3744
  defaultVariants: {
3775
3745
  variant: "primary",
3776
- selected: false,
3777
- disabled: false,
3778
3746
  layout: "fixed"
3779
3747
  }
3780
3748
  }
3781
3749
  );
3750
+ var tabStateLayerVariants = classVarianceAuthority.cva(
3751
+ [
3752
+ "absolute inset-0 pointer-events-none opacity-0",
3753
+ // Effects transition for opacity — no spatial overshoot
3754
+ "transition-opacity duration-spring-standard-fast-effects ease-spring-standard-fast-effects",
3755
+ // Hover: 8%
3756
+ "group-data-[hovered]/tab:opacity-8",
3757
+ // Focus: 10%
3758
+ "group-data-[focus-visible]/tab:opacity-10",
3759
+ // Pressed: 10%, doubled selector beats hover at equal cascade position
3760
+ "group-data-[pressed]/tab:group-data-[pressed]/tab:opacity-10",
3761
+ // No state layer when disabled
3762
+ "group-data-[disabled]/tab:hidden"
3763
+ ],
3764
+ {
3765
+ variants: {
3766
+ variant: {
3767
+ primary: [
3768
+ // Inactive state-layer color
3769
+ "bg-on-surface",
3770
+ // Active state-layer color (higher cascade position wins over base bg-on-surface)
3771
+ "group-data-[selected]/tab:bg-primary"
3772
+ ],
3773
+ secondary: [
3774
+ // Secondary: always on-surface
3775
+ "bg-on-surface"
3776
+ ]
3777
+ }
3778
+ },
3779
+ defaultVariants: {
3780
+ variant: "primary"
3781
+ }
3782
+ }
3783
+ );
3782
3784
  var tabIndicatorVariants = classVarianceAuthority.cva(
3783
3785
  [
3784
- // Base: absolutely positioned at bottom
3785
3786
  "absolute bottom-0 left-0",
3786
3787
  "pointer-events-none",
3787
- // Transition using MD3 motion tokens (medium2 duration, emphasized easing)
3788
+ // Spatial spring indicator position/width are spatial (not effects)
3788
3789
  "transition-[left,width]",
3789
- "duration-medium2",
3790
- "ease-emphasized"
3790
+ "duration-spring-standard-default-spatial",
3791
+ "ease-spring-standard-default-spatial"
3791
3792
  ],
3792
3793
  {
3793
3794
  variants: {
3794
3795
  variant: {
3795
- primary: ["h-[3px]", "bg-primary", "rounded-t-sm"],
3796
- secondary: ["h-[2px]", "bg-on-surface-variant"]
3796
+ primary: ["h-[3px]", "bg-primary", "rounded-tl-sm rounded-tr-sm"],
3797
+ secondary: ["h-[2px]", "bg-primary"]
3797
3798
  }
3798
3799
  },
3799
3800
  defaultVariants: {
@@ -3801,20 +3802,28 @@ var tabIndicatorVariants = classVarianceAuthority.cva(
3801
3802
  }
3802
3803
  }
3803
3804
  );
3804
- var tabPanelVariants = classVarianceAuthority.cva(
3805
+ var tabIconVariants = classVarianceAuthority.cva(
3805
3806
  [
3806
- // Base panel styles
3807
- "outline-none",
3808
- "focus-visible:ring-2 focus-visible:ring-primary focus-visible:ring-offset-2"
3807
+ "relative inline-flex items-center justify-center",
3808
+ // MD3 tab icon size: 24dp
3809
+ "size-6",
3810
+ // Effects transition for color (inherited from parent, guard here too)
3811
+ "transition-colors duration-spring-standard-fast-effects ease-spring-standard-fast-effects"
3809
3812
  ],
3810
3813
  {
3811
- variants: {},
3812
- defaultVariants: {}
3814
+ variants: {
3815
+ hasLabel: {
3816
+ true: "mb-1",
3817
+ false: ""
3818
+ }
3819
+ },
3820
+ defaultVariants: {
3821
+ hasLabel: false
3822
+ }
3813
3823
  }
3814
3824
  );
3815
3825
  var tabBadgeVariants = classVarianceAuthority.cva(
3816
3826
  [
3817
- // Base badge
3818
3827
  "absolute",
3819
3828
  "inline-flex items-center justify-center",
3820
3829
  "bg-error text-on-error",
@@ -3824,7 +3833,7 @@ var tabBadgeVariants = classVarianceAuthority.cva(
3824
3833
  {
3825
3834
  variants: {
3826
3835
  type: {
3827
- dot: ["top-1 right-1", "w-1.5 h-1.5", "rounded-full"],
3836
+ dot: ["top-0 right-0", "w-1.5 h-1.5", "rounded-full"],
3828
3837
  count: ["-top-1 -right-1", "min-w-[16px] h-4", "px-1", "rounded-full", "text-[11px]"]
3829
3838
  }
3830
3839
  },
@@ -3833,20 +3842,10 @@ var tabBadgeVariants = classVarianceAuthority.cva(
3833
3842
  }
3834
3843
  }
3835
3844
  );
3836
- var tabIconVariants = classVarianceAuthority.cva(
3837
- ["relative", "inline-flex items-center justify-center", "w-6 h-6"],
3838
- {
3839
- variants: {
3840
- hasLabel: {
3841
- true: "mb-1",
3842
- false: ""
3843
- }
3844
- },
3845
- defaultVariants: {
3846
- hasLabel: false
3847
- }
3848
- }
3849
- );
3845
+ var tabPanelVariants = classVarianceAuthority.cva([
3846
+ "outline-none",
3847
+ "focus-visible:ring-2 focus-visible:ring-primary focus-visible:ring-offset-2"
3848
+ ]);
3850
3849
  var TabList = React.forwardRef(
3851
3850
  ({ children, className }, forwardedRef) => {
3852
3851
  const { state } = useHeadlessTabsContext("TabList");
@@ -3925,18 +3924,41 @@ var TabList = React.forwardRef(
3925
3924
  const selectedTab = container.querySelector('[aria-selected="true"]');
3926
3925
  if (!selectedTab) return;
3927
3926
  const containerRect = container.getBoundingClientRect();
3928
- const tabRect = selectedTab.getBoundingClientRect();
3929
- const newLeft = tabRect.left - containerRect.left + container.scrollLeft;
3930
- const newWidth = tabRect.width;
3931
- setIndicatorStyle((prev) => {
3932
- if (prev.left === newLeft && prev.width === newWidth) return prev;
3933
- return { left: newLeft, width: newWidth };
3934
- });
3927
+ if (variant === "primary") {
3928
+ const contentEl = selectedTab.querySelector("[data-tab-content]");
3929
+ if (!contentEl) return;
3930
+ const contentRect = contentEl.getBoundingClientRect();
3931
+ const newLeft = contentRect.left - containerRect.left + container.scrollLeft;
3932
+ const newWidth = contentRect.width;
3933
+ setIndicatorStyle((prev) => {
3934
+ if (prev.left === newLeft && prev.width === newWidth) return prev;
3935
+ return { left: newLeft, width: newWidth };
3936
+ });
3937
+ } else {
3938
+ const tabRect = selectedTab.getBoundingClientRect();
3939
+ const newLeft = tabRect.left - containerRect.left + container.scrollLeft;
3940
+ const newWidth = tabRect.width;
3941
+ setIndicatorStyle((prev) => {
3942
+ if (prev.left === newLeft && prev.width === newWidth) return prev;
3943
+ return { left: newLeft, width: newWidth };
3944
+ });
3945
+ }
3935
3946
  setIndicatorReady(true);
3936
- }, [ref]);
3947
+ }, [ref, variant]);
3937
3948
  React.useLayoutEffect(() => {
3938
3949
  updateIndicator();
3939
3950
  }, [state.selectedKey, updateIndicator]);
3951
+ React.useEffect(() => {
3952
+ const container = ref.current;
3953
+ if (!container || typeof ResizeObserver === "undefined") return;
3954
+ const observer = new ResizeObserver(() => {
3955
+ updateIndicator();
3956
+ });
3957
+ observer.observe(container);
3958
+ return () => {
3959
+ observer.disconnect();
3960
+ };
3961
+ }, [ref, updateIndicator]);
3940
3962
  const mergedTabListProps = { ...tabListProps, onKeyDown: handleKeyDown };
3941
3963
  return /* @__PURE__ */ jsxRuntime.jsxs("div", { ...mergedTabListProps, ref, className: cn(tabListVariants({ layout }), className), children: [
3942
3964
  children,
@@ -3951,7 +3973,6 @@ var TabList = React.forwardRef(
3951
3973
  !indicatorReady && "opacity-0"
3952
3974
  ),
3953
3975
  style: {
3954
- // Dynamic left/width values from DOM measurements
3955
3976
  left: `${indicatorStyle.left}px`,
3956
3977
  width: `${indicatorStyle.width}px`
3957
3978
  }
@@ -3979,10 +4000,12 @@ var Tab = React.forwardRef(
3979
4000
  const {
3980
4001
  tabProps,
3981
4002
  isSelected,
3982
- isDisabled: ariaIsDisabled
4003
+ isDisabled: ariaIsDisabled,
4004
+ isPressed
3983
4005
  } = reactAria.useTab({ key: id, isDisabled }, state, ref);
3984
4006
  const { isFocusVisible, focusProps } = reactAria.useFocusRing();
3985
4007
  const finalIsDisabled = isDisabled || ariaIsDisabled;
4008
+ const { isHovered, hoverProps } = reactAria.useHover({ isDisabled: finalIsDisabled });
3986
4009
  const { onMouseDown: handleRipple, ripples } = useRipple({
3987
4010
  disabled: finalIsDisabled || disableRipple
3988
4011
  });
@@ -3996,7 +4019,7 @@ var Tab = React.forwardRef(
3996
4019
  state.selectionManager.setFocusedKey(id);
3997
4020
  }
3998
4021
  }, [state.selectionManager, id, finalIsDisabled]);
3999
- const mergedProps = utils.mergeProps(tabProps, focusProps, {
4022
+ const mergedProps = utils.mergeProps(tabProps, focusProps, hoverProps, {
4000
4023
  onMouseDown: disableRipple ? void 0 : handleRipple,
4001
4024
  onClick: handleClick,
4002
4025
  onFocus: handleFocus
@@ -4013,47 +4036,56 @@ var Tab = React.forwardRef(
4013
4036
  type: "button",
4014
4037
  "data-key": String(id),
4015
4038
  tabIndex: finalIsDisabled ? -1 : isSelected ? 0 : -1,
4016
- className: cn(
4017
- tabVariants({
4018
- variant,
4019
- selected: isSelected,
4020
- disabled: finalIsDisabled,
4021
- layout
4022
- }),
4023
- isFocusVisible && "outline-primary outline-2 outline-offset-2",
4024
- hasLabel && hasIcon && "min-h-16",
4025
- className
4026
- ),
4039
+ className: cn(tabVariants({ variant, layout }), className),
4040
+ ...getInteractionDataAttributes({
4041
+ isHovered,
4042
+ isFocusVisible,
4043
+ isPressed,
4044
+ isSelected,
4045
+ isDisabled: finalIsDisabled
4046
+ }),
4047
+ "data-with-icon": hasIcon ? "" : void 0,
4048
+ "data-with-label": hasLabel ? "" : void 0,
4027
4049
  ...htmlProps,
4028
4050
  children: [
4029
4051
  !disableRipple && ripples,
4030
- hasIcon && /* @__PURE__ */ jsxRuntime.jsxs("span", { className: cn(tabIconVariants({ hasLabel })), children: [
4031
- icon,
4032
- badgeDisplay && /* @__PURE__ */ jsxRuntime.jsx(
4033
- "span",
4034
- {
4035
- "data-badge-type": isDotBadge ? "dot" : "count",
4036
- "aria-hidden": "true",
4037
- className: cn(tabBadgeVariants({ type: isDotBadge ? "dot" : "count" })),
4038
- children: !isDotBadge && badgeDisplay
4039
- }
4040
- )
4041
- ] }),
4042
- hasLabel && /* @__PURE__ */ jsxRuntime.jsxs("span", { className: "relative z-10 truncate", children: [
4043
- label,
4044
- !hasIcon && badgeDisplay && /* @__PURE__ */ jsxRuntime.jsx(
4045
- "span",
4046
- {
4047
- "data-badge-type": isDotBadge ? "dot" : "count",
4048
- "aria-hidden": "true",
4049
- className: cn(
4050
- "relative ml-1",
4051
- tabBadgeVariants({ type: isDotBadge ? "dot" : "count" })
4052
- ),
4053
- children: !isDotBadge && badgeDisplay
4054
- }
4055
- )
4056
- ] })
4052
+ /* @__PURE__ */ jsxRuntime.jsx("span", { className: cn(tabStateLayerVariants({ variant })), "aria-hidden": "true" }),
4053
+ /* @__PURE__ */ jsxRuntime.jsxs(
4054
+ "span",
4055
+ {
4056
+ "data-tab-content": true,
4057
+ className: "relative z-10 inline-flex flex-col items-center justify-center",
4058
+ children: [
4059
+ hasIcon && /* @__PURE__ */ jsxRuntime.jsxs("span", { className: cn(tabIconVariants({ hasLabel })), children: [
4060
+ icon,
4061
+ badgeDisplay && /* @__PURE__ */ jsxRuntime.jsx(
4062
+ "span",
4063
+ {
4064
+ "data-badge-type": isDotBadge ? "dot" : "count",
4065
+ "aria-hidden": "true",
4066
+ className: cn(tabBadgeVariants({ type: isDotBadge ? "dot" : "count" })),
4067
+ children: !isDotBadge && badgeDisplay
4068
+ }
4069
+ )
4070
+ ] }),
4071
+ hasLabel && /* @__PURE__ */ jsxRuntime.jsxs("span", { className: "truncate", children: [
4072
+ label,
4073
+ !hasIcon && badgeDisplay && /* @__PURE__ */ jsxRuntime.jsx(
4074
+ "span",
4075
+ {
4076
+ "data-badge-type": isDotBadge ? "dot" : "count",
4077
+ "aria-hidden": "true",
4078
+ className: cn(
4079
+ "relative ml-1",
4080
+ tabBadgeVariants({ type: isDotBadge ? "dot" : "count" })
4081
+ ),
4082
+ children: !isDotBadge && badgeDisplay
4083
+ }
4084
+ )
4085
+ ] })
4086
+ ]
4087
+ }
4088
+ )
4057
4089
  ]
4058
4090
  }
4059
4091
  );