From 48bda630e6e19505943d28ea0e1dfccff3348f87 Mon Sep 17 00:00:00 2001 From: Innei Date: Fri, 31 Oct 2025 14:55:43 +0800 Subject: [PATCH] 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 --- apps/desktop/layer/renderer/package.json | 1 + .../components/layouts/RateLimitNotice.tsx | 2 +- .../src/modules/settings/tabs/plan.tsx | 145 +++++++++++------- pnpm-lock.yaml | 28 ++++ 4 files changed, 116 insertions(+), 60 deletions(-) diff --git a/apps/desktop/layer/renderer/package.json b/apps/desktop/layer/renderer/package.json index 18289ea9e..a9ebb5338 100644 --- a/apps/desktop/layer/renderer/package.json +++ b/apps/desktop/layer/renderer/package.json @@ -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", diff --git a/apps/desktop/layer/renderer/src/modules/ai-chat/components/layouts/RateLimitNotice.tsx b/apps/desktop/layer/renderer/src/modules/ai-chat/components/layouts/RateLimitNotice.tsx index ffd4946bc..1c81c4cd2 100644 --- a/apps/desktop/layer/renderer/src/modules/ai-chat/components/layouts/RateLimitNotice.tsx +++ b/apps/desktop/layer/renderer/src/modules/ai-chat/components/layouts/RateLimitNotice.tsx @@ -84,7 +84,7 @@ export const RateLimitNotice: React.FC = ({ error, classNa className={cn("mb-3", className)} onClick={() => settingModalPresent("plan")} > -
+
{buildMessage()} diff --git a/apps/desktop/layer/renderer/src/modules/settings/tabs/plan.tsx b/apps/desktop/layer/renderer/src/modules/settings/tabs/plan.tsx index 11f1c02e8..07adee6e6 100644 --- a/apps/desktop/layer/renderer/src/modules/settings/tabs/plan.tsx +++ b/apps/desktop/layer/renderer/src/modules/settings/tabs/plan.tsx @@ -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() {
{/* Billing Period Toggle */}
-
- - -
+ setBillingPeriod(value as "monthly" | "yearly")} + > + + + Yearly + {averageSavings > 0 && ( + Save {averageSavings}% + )} + + } + /> +
{/* 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", )} > @@ -266,11 +266,11 @@ const PlanCard = ({ plan, billingPeriod, isCurrentPlan, currentTier }: PlanCardP
- {/* Subtle gradient line at bottom */} -
+ {/* Subtle bottom accent line */} + {actionType === "upgrade" && ( +
+ )}
) } @@ -304,7 +306,7 @@ const PlanBadges = ({ isPopular }: { isPopular: boolean }) => ( <> {isPopular && (
-
+
Most Popular
@@ -321,31 +323,45 @@ const PlanHeader = ({ discountDescription, }: { title: string - price: string - regularPrice?: string + price: number + regularPrice?: number period: string description?: string discountDescription?: string }) => ( -
-

{title}

-
-
- {price} +
+

{title}

+
+
+ {period && /{period}} - {regularPrice && ( - {regularPrice} + + {typeof regularPrice === "number" && regularPrice > 0 && ( + + + )}
- {discountDescription && ( -
- {discountDescription} + {typeof regularPrice === "number" && regularPrice > 0 && ( +
+ {discountDescription && ( + + {discountDescription} + + )}
)}
- {description && ( -

{description}

- )} + {description &&

{description}

}
) @@ -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 ( ) } diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 721e27072..35c65d038 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -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