feat: add @number-flow/react package and update plan settings UI

- Added @number-flow/react version 0.5.10 to dependencies.
- Updated the RateLimitNotice component's styling for improved visual consistency.
- Refactored the billing period selection in the SettingPlan component to use SegmentGroup for better user experience.
- Enhanced price display in PlanHeader using NumberFlow for formatted currency output.

Signed-off-by: Innei <tukon479@gmail.com>
This commit is contained in:
Innei 2025-10-31 14:55:43 +08:00
parent a1e2fd2c7f
commit 48bda630e6
No known key found for this signature in database
GPG Key ID: 0F62D33977F021F7
4 changed files with 116 additions and 60 deletions

View File

@ -30,6 +30,7 @@
"@lexical/markdown": "0.33.1",
"@lexical/react": "0.33.1",
"@lottiefiles/dotlottie-react": "0.17.5",
"@number-flow/react": "0.5.10",
"@openpanel/web": "1.0.1",
"@radix-ui/react-avatar": "1.1.10",
"@radix-ui/react-context-menu": "2.2.16",

View File

@ -84,7 +84,7 @@ export const RateLimitNotice: React.FC<RateLimitNoticeProps> = ({ error, classNa
className={cn("mb-3", className)}
onClick={() => settingModalPresent("plan")}
>
<div className="flex items-center gap-2 rounded-lg border border-border bg-fill/50 px-3 py-2 backdrop-blur-sm">
<div className="flex items-center gap-2 rounded-lg border border-border bg-material-ultra-thick px-3 py-2 backdrop-blur-background">
<i className="i-mgc-power size-4 flex-shrink-0 text-text" />
<span className="min-w-0 flex-1 truncate text-xs text-text-secondary">
{buildMessage()}

View File

@ -1,9 +1,11 @@
import { Button } from "@follow/components/ui/button/index.js"
import { SegmentGroup, SegmentItem } from "@follow/components/ui/segment/index.jsx"
import { UserRole } from "@follow/constants"
import { DEEPLINK_SCHEME, IN_ELECTRON } from "@follow/shared"
import { env } from "@follow/shared/env.desktop"
import { useUserRole, useWhoami } from "@follow/store/user/hooks"
import { cn } from "@follow/utils/utils"
import NumberFlow from "@number-flow/react"
import { useMutation, useQuery } from "@tanstack/react-query"
import { useState } from "react"
import { useTranslation } from "react-i18next"
@ -132,35 +134,23 @@ export function SettingPlan() {
<section className="mt-4 space-y-8">
{/* Billing Period Toggle */}
<div className="flex justify-center">
<div className="inline-flex rounded-lg bg-fill-secondary p-1">
<button
type="button"
onClick={() => setBillingPeriod("monthly")}
className={cn(
"rounded-md px-4 py-2 text-sm font-medium transition-all",
billingPeriod === "monthly"
? "bg-background text-text shadow-sm"
: "text-text-secondary hover:text-text",
)}
>
Monthly
</button>
<button
type="button"
onClick={() => setBillingPeriod("yearly")}
className={cn(
"rounded-md px-4 py-2 text-sm font-medium transition-all",
billingPeriod === "yearly"
? "bg-background text-text shadow-sm"
: "text-text-secondary hover:text-text",
)}
>
<span>Yearly</span>
{averageSavings > 0 && (
<span className="ml-2 text-xs font-medium text-green">Save {averageSavings}%</span>
)}
</button>
</div>
<SegmentGroup
value={billingPeriod}
onValueChanged={(value) => setBillingPeriod(value as "monthly" | "yearly")}
>
<SegmentItem value="monthly" label="Monthly" />
<SegmentItem
value="yearly"
label={
<span className="flex items-center gap-2">
<span>Yearly</span>
{averageSavings > 0 && (
<span className="text-xs font-semibold text-green">Save {averageSavings}%</span>
)}
</span>
}
/>
</SegmentGroup>
</div>
{/* Plans Grid */}
@ -243,9 +233,18 @@ const PlanCard = ({ plan, billingPeriod, isCurrentPlan, currentTier }: PlanCardP
// Use discount price if available, otherwise use regular price
const finalPrice = hasDiscount ? discountPrice : regularPrice
const formattedPrice = finalPrice === 0 ? "$0" : `$${finalPrice.toFixed(2)}`
const formattedRegularPrice =
hasDiscount && regularPrice > 0 ? `$${regularPrice.toFixed(2)}` : undefined
const regularPriceForStrike = hasDiscount && regularPrice > 0 ? regularPrice : undefined
// Calculate discount percentage
const discountPercentage = hasDiscount
? Math.round(((regularPrice - discountPrice) / regularPrice) * 100)
: 0
// Create discount description
const discountDescription =
hasDiscount && discountPercentage > 0
? `Early bird discount, ${discountPercentage}% off`
: undefined
// Get plan description from i18n
const planDescriptionKey = `plan.descriptions.${plan.role}` as const
@ -256,9 +255,10 @@ const PlanCard = ({ plan, billingPeriod, isCurrentPlan, currentTier }: PlanCardP
className={cn(
"group relative flex h-full flex-col overflow-hidden rounded-xl border transition-all duration-200",
actionType === "upgrade"
? "border-accent"
? "border-accent/40 bg-background shadow-sm hover:border-accent/60 hover:shadow-md"
: "border-fill-tertiary bg-background hover:border-fill-secondary",
plan.isComingSoon && "opacity-75",
isCurrentPlan && "border-blue/30",
)}
>
<PlanBadges isPopular={plan.isPopular || false} />
@ -266,11 +266,11 @@ const PlanCard = ({ plan, billingPeriod, isCurrentPlan, currentTier }: PlanCardP
<div className="flex h-full flex-col justify-between gap-4 p-4 @md:p-5">
<PlanHeader
title={plan.name}
price={formattedPrice}
regularPrice={formattedRegularPrice}
price={finalPrice}
regularPrice={regularPriceForStrike}
period={period}
description={planDescription}
discountDescription={plan.discountDescription}
discountDescription={discountDescription}
/>
<PlanAction
@ -293,8 +293,10 @@ const PlanCard = ({ plan, billingPeriod, isCurrentPlan, currentTier }: PlanCardP
/>
</div>
{/* Subtle gradient line at bottom */}
<div className="absolute inset-x-0 bottom-0 h-px bg-gradient-to-r from-transparent via-fill-tertiary to-transparent" />
{/* Subtle bottom accent line */}
{actionType === "upgrade" && (
<div className="absolute inset-x-0 bottom-0 h-px bg-gradient-to-r from-transparent via-accent/30 to-transparent" />
)}
</div>
)
}
@ -304,7 +306,7 @@ const PlanBadges = ({ isPopular }: { isPopular: boolean }) => (
<>
{isPopular && (
<div className="absolute -top-px right-4 z-10">
<div className="rounded-b-lg bg-gradient-to-r from-accent to-accent/80 px-1.5 py-1 text-caption font-medium text-white shadow-sm">
<div className="rounded-b-lg bg-gradient-to-r from-accent to-accent/90 px-2.5 py-1 text-caption font-medium text-white shadow-sm">
Most Popular
</div>
</div>
@ -321,31 +323,45 @@ const PlanHeader = ({
discountDescription,
}: {
title: string
price: string
regularPrice?: string
price: number
regularPrice?: number
period: string
description?: string
discountDescription?: string
}) => (
<div className="space-y-2">
<h3 className="text-base font-semibold @md:text-lg">{title}</h3>
<div className="space-y-1">
<div className="flex items-baseline gap-2">
<span className="text-xl font-bold">{price}</span>
<div className="space-y-2.5">
<h3 className="text-lg font-semibold">{title}</h3>
<div className="space-y-1.5">
<div className="flex items-baseline gap-1.5">
<NumberFlow
className="text-2xl font-bold"
value={price}
locales="en-US"
format={{ style: "currency", currency: "USD", trailingZeroDisplay: "stripIfInteger" }}
/>
{period && <span className="text-xs text-text-secondary @md:text-sm">/{period}</span>}
{regularPrice && (
<span className="text-sm text-text-tertiary line-through">{regularPrice}</span>
{typeof regularPrice === "number" && regularPrice > 0 && (
<span className="relative inline-block text-sm text-text-tertiary after:absolute after:inset-x-0 after:top-1/2 after:h-px after:w-full after:translate-y-1/2 after:bg-text-tertiary after:content-['']">
<NumberFlow
value={regularPrice}
locales="en-US"
format={{ style: "currency", currency: "USD", trailingZeroDisplay: "stripIfInteger" }}
/>
</span>
)}
</div>
{discountDescription && (
<div className="inline-flex items-center gap-1.5 rounded-md bg-green/10 px-2 py-1">
<span className="text-xs font-semibold text-green">{discountDescription}</span>
{typeof regularPrice === "number" && regularPrice > 0 && (
<div className="flex items-center gap-2">
{discountDescription && (
<span className="inline-flex items-center gap-1 rounded-md bg-green/10 px-1.5 py-0.5 text-xs font-medium text-green">
{discountDescription}
</span>
)}
</div>
)}
</div>
{description && (
<p className="text-xs leading-relaxed text-text-secondary @md:text-sm">{description}</p>
)}
{description && <p className="text-xs leading-relaxed text-text-secondary">{description}</p>}
</div>
)
@ -365,6 +381,7 @@ const PlanAction = ({
case "coming-soon": {
return {
text: "Coming Soon",
icon: "i-mgc-time-cute-re",
variant: "outline" as const,
disabled: true,
}
@ -372,6 +389,7 @@ const PlanAction = ({
case "current": {
return {
text: `Current Plan${onCancel ? " | Cancel" : ""}`,
icon: undefined,
variant: "outline" as const,
className: onCancel ? "" : "text-text-secondary",
disabled: onCancel ? false : true,
@ -380,6 +398,7 @@ const PlanAction = ({
case "in-trial": {
return {
text: "In Trial",
icon: "i-mgc-stopwatch-cute-re",
variant: "outline" as const,
disabled: false,
}
@ -387,16 +406,18 @@ const PlanAction = ({
case "upgrade": {
return {
text: "Upgrade",
icon: "i-mgc-arrow-up-cute-re",
className:
"bg-gradient-to-r from-accent to-accent/80 hover:from-accent/90 hover:to-accent/70",
"bg-gradient-to-r from-accent to-accent/90 text-white hover:from-accent/95 hover:to-accent/85",
disabled: false,
}
}
case "switch": {
return {
text: "Switch",
text: "Switch Plan",
icon: "i-mgc-transfer-cute-re",
className:
"bg-gradient-to-r from-accent to-accent/80 hover:from-accent/90 hover:to-accent/70",
"bg-gradient-to-r from-accent to-accent/90 text-white font-semibold hover:from-accent/95 hover:to-accent/85",
disabled: false,
}
}
@ -415,12 +436,18 @@ const PlanAction = ({
return (
<Button
variant={buttonConfig.variant}
buttonClassName={cn("w-full h-9 @md:h-10 text-xs @md:text-sm", buttonConfig.className)}
buttonClassName={cn(
"w-full h-9 text-sm font-medium transition-all duration-200",
buttonConfig.className,
)}
disabled={buttonConfig.disabled}
onClick={buttonConfig.disabled ? undefined : (onCancel ?? onSelect)}
isLoading={isLoading}
>
{buttonConfig.text}
<span className="flex items-center justify-center gap-1.5">
{buttonConfig.icon && <i className={cn(buttonConfig.icon, "text-sm")} />}
<span>{buttonConfig.text}</span>
</span>
</Button>
)
}

View File

@ -494,6 +494,9 @@ importers:
'@lottiefiles/dotlottie-react':
specifier: 0.17.5
version: 0.17.5(react@19.0.0)
'@number-flow/react':
specifier: 0.5.10
version: 0.5.10(react-dom@19.0.0(react@19.0.0))(react@19.0.0)
'@openpanel/web':
specifier: 1.0.1
version: 1.0.1
@ -4979,6 +4982,12 @@ packages:
engines: {node: ^12.13.0 || ^14.15.0 || >=16.0.0}
deprecated: This functionality has been moved to @npmcli/fs
'@number-flow/react@0.5.10':
resolution: {integrity: sha512-a8Wh5eNITn7Km4xbddAH7QH8eNmnduR6k34ER1hkHSGO4H2yU1DDnuAWLQM99vciGInFODemSc0tdxrXkJEpbA==}
peerDependencies:
react: 19.0.0
react-dom: 19.0.0
'@oclif/core@1.26.2':
resolution: {integrity: sha512-6jYuZgXvHfOIc9GIaS4T3CIKGTjPmfAxuMcbCbMRKJJl4aq/4xeRlEz0E8/hz8HxvxZBGvN2GwAUHlrGWQVrVw==}
engines: {node: '>=14.0.0'}
@ -10187,6 +10196,9 @@ packages:
jiti:
optional: true
esm-env@1.2.2:
resolution: {integrity: sha512-Epxrv+Nr/CaL4ZcFGPJIYLWFom+YeV1DqMLHJoEd9SYRxNbaFruBwfEX/kkHUJf55j2+TUbmDcmuilbP1TmXHA==}
espree@10.4.0:
resolution: {integrity: sha512-j6PAQ2uUr79PZhBjP5C5fhl8e39FmRnOjsD5lGnWrFU8i2G776tBK7+nP8KuQUTTyAZUwfQqXAgrVH5MbH9CYQ==}
engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0}
@ -13069,6 +13081,9 @@ packages:
nullthrows@1.1.1:
resolution: {integrity: sha512-2vPPEi+Z7WqML2jZYddDIfy5Dqb0r2fze2zTxNNknZaFpVHU3mFB3R+DWeJWGVx0ecvttSGlJTI+WG+8Z4cDWw==}
number-flow@0.5.8:
resolution: {integrity: sha512-FPr1DumWyGi5Nucoug14bC6xEz70A1TnhgSHhKyfqjgji2SOTz+iLJxKtv37N5JyJbteGYCm6NQ9p1O4KZ7iiA==}
nypm@0.6.2:
resolution: {integrity: sha512-7eM+hpOtrKrBDCh7Ypu2lJ9Z7PNZBdi/8AT3AX8xoCj43BBVHD0hPSTEvMtkMpfs8FCqBGhxB+uToIQimA111g==}
engines: {node: ^14.16.0 || >=16.10.0}
@ -22773,6 +22788,13 @@ snapshots:
mkdirp: 1.0.4
rimraf: 3.0.2
'@number-flow/react@0.5.10(react-dom@19.0.0(react@19.0.0))(react@19.0.0)':
dependencies:
esm-env: 1.2.2
number-flow: 0.5.8
react: 19.0.0
react-dom: 19.0.0(react@19.0.0)
'@oclif/core@1.26.2':
dependencies:
'@oclif/linewrap': 1.0.0
@ -29332,6 +29354,8 @@ snapshots:
transitivePeerDependencies:
- supports-color
esm-env@1.2.2: {}
espree@10.4.0:
dependencies:
acorn: 8.15.0
@ -33263,6 +33287,10 @@ snapshots:
nullthrows@1.1.1: {}
number-flow@0.5.8:
dependencies:
esm-env: 1.2.2
nypm@0.6.2:
dependencies:
citty: 0.1.6