Compare commits

...
Author SHA1 Message Date
bflatastic 57c109c0bc upgrade button commented out 2025-06-20 12:35:54 +07:00
bflatastic 181db30992 layout fix upgrade button 2025-06-20 12:23:46 +07:00
bflatastic 66ffe52cc2 minor library modal squished buttons fix 2025-06-20 12:20:11 +07:00
bflatastic f99c86b173 styling update 2025-06-20 11:51:06 +07:00
bflatastic b003ccef27 tier naming update 2025-06-20 11:51:06 +07:00
bflatastic 2f9b979de1 pricing modal 2025-06-20 11:51:06 +07:00
27 changed files with 757 additions and 8 deletions
@@ -6,6 +6,8 @@ import {
faFilm,
faPaintbrush,
faImage,
faDiamond,
faGem,
} from "@fortawesome/pro-solid-svg-icons";
import { Button } from "@storyteller/ui-button";
import { AuthButtons } from "./AuthButtons";
@@ -34,6 +36,7 @@ import {
is3DSceneLoaded,
set3DPageMounted,
} from "~/pages/PageEnigma/Editor/editor";
import { usePricingModalStore } from "@storyteller/ui-pricing-modal";
interface Props {
pageName: string;
}
@@ -92,6 +95,8 @@ export const TopBar = ({ pageName }: Props) => {
);
};
const { toggleModal } = usePricingModalStore();
return (
<>
<header className="fixed left-0 top-0 z-[60] w-full border-b border-white/5 bg-ui-background">
@@ -141,6 +146,14 @@ export const TopBar = ({ pageName }: Props) => {
<div className="flex justify-end gap-3.5">
<div className="flex gap-2">
{/* <Button
variant="primary"
icon={faGem}
onClick={toggleModal}
className="shadow-md shadow-primary-500/50 transition-all duration-300 hover:shadow-md hover:shadow-primary-500/75"
>
Upgrade Now
</Button> */}
<Tooltip content="Settings" position="bottom" delay={300}>
<Button
variant="secondary"
@@ -27,6 +27,7 @@ import { useGenerationCompleteEvent } from "./TauriEvents/generations/useGenerat
import { PageEditor } from "~/pages/PageEnigma/PageEditor";
import { GalleryDragComponent } from "@storyteller/ui-gallery-modal";
import { LoadingDots } from "@storyteller/ui-loading";
import { PricingModal } from "@storyteller/ui-pricing-modal";
import {
isActionReminderOpen,
@@ -132,6 +133,8 @@ export const PageEnigma = ({ sceneToken }: { sceneToken?: string }) => {
openAiLogo={currentReminderModalProps.openAiLogo}
/>
)}
<PricingModal />
</EngineProvider>
);
};
@@ -29,6 +29,12 @@ export default {
white: colors.white,
gray: colors.gray,
black: colors.black,
blue: colors.blue,
green: colors.green,
yellow: colors.yellow,
purple: colors.purple,
pink: colors.pink,
orange: colors.orange,
primary: {
DEFAULT: "#2d81ff",
"50": "#eef6ff",
+8 -2
View File
@@ -65,10 +65,10 @@
"path": "../../libs/components/button-dropdown"
},
{
"path": "../../libs/tauri-api"
"path": "../../libs/components/action-reminder-modal"
},
{
"path": "../../libs/components/action-reminder-modal"
"path": "../../libs/components/pricing-modal"
},
{
"path": "../../libs/components/toaster"
@@ -82,6 +82,9 @@
{
"path": "../../libs/components/promptbox"
},
{
"path": "../../libs/tauri-api"
},
{
"path": "../../libs/components/popover"
},
@@ -106,6 +109,9 @@
{
"path": "../../libs/components/gravatar"
},
{
"path": "../../libs/components/login-modal"
},
{
"path": "../../libs/components/select"
},
@@ -2,6 +2,15 @@
"files": [],
"include": [],
"references": [
{
"path": "../../tauri-api"
},
{
"path": "../../api"
},
{
"path": "../image-input"
},
{
"path": "../gallery-modal"
},
@@ -43,6 +43,15 @@
"src/**/*.tsx"
],
"references": [
{
"path": "../../tauri-api/tsconfig.lib.json"
},
{
"path": "../../api/tsconfig.lib.json"
},
{
"path": "../image-input/tsconfig.lib.json"
},
{
"path": "../gallery-modal/tsconfig.lib.json"
},
@@ -376,7 +376,7 @@ export const GalleryModal = React.memo(
)}
<div className="flex h-full flex-col">
<div className="border-b border-white/10 p-4 py-3">
<div className="grid grid-cols-2 items-center">
<div className="flex justify-between items-center">
<div className="flex items-center gap-4">
<h2 className="text-xl font-semibold">
{mode === "select" ? "Select Images" : "My Library"}
@@ -2,6 +2,9 @@
"files": [],
"include": [],
"references": [
{
"path": "../button"
},
{
"path": "./tsconfig.lib.json"
},
@@ -41,5 +41,10 @@
"src/**/*.jsx",
"src/**/*.ts",
"src/**/*.tsx"
],
"references": [
{
"path": "../button/tsconfig.lib.json"
}
]
}
@@ -2,6 +2,9 @@
"files": [],
"include": [],
"references": [
{
"path": "../loading-spinner"
},
{
"path": "../../tauri-api"
},
@@ -9,7 +12,7 @@
"path": "../button"
},
{
"path": "../close-button"
"path": "../modal"
},
{
"path": "./tsconfig.lib.json"
@@ -43,6 +43,9 @@
"src/**/*.tsx"
],
"references": [
{
"path": "../loading-spinner/tsconfig.lib.json"
},
{
"path": "../../tauri-api/tsconfig.lib.json"
},
@@ -50,7 +53,7 @@
"path": "../button/tsconfig.lib.json"
},
{
"path": "../close-button/tsconfig.lib.json"
"path": "../modal/tsconfig.lib.json"
}
]
}
@@ -0,0 +1,12 @@
{
"presets": [
[
"@nx/react/babel",
{
"runtime": "automatic",
"useBuiltIns": "usage"
}
]
],
"plugins": []
}
@@ -0,0 +1,7 @@
# @storyteller/ui-pricing-modal
This library was generated with [Nx](https://nx.dev).
## Running unit tests
Run `nx test @storyteller/ui-pricing-modal` to execute the unit tests via [Vitest](https://vitest.dev/).
@@ -0,0 +1,17 @@
{
"name": "@storyteller/ui-pricing-modal",
"version": "0.0.1",
"type": "module",
"main": "./dist/index.js",
"module": "./dist/index.js",
"types": "./dist/index.d.ts",
"exports": {
"./package.json": "./package.json",
".": {
"development": "./src/index.ts",
"types": "./dist/index.d.ts",
"import": "./dist/index.js",
"default": "./dist/index.js"
}
}
}
@@ -0,0 +1,3 @@
export * from "./lib/pricing-modal";
export * from "./lib/pricing-config";
export * from "./lib/pricing-modal-store";
@@ -0,0 +1,95 @@
export interface PricingFeature {
text: string;
included: boolean;
tooltip?: string;
}
export interface PricingTier {
id: string;
name: string;
monthlyPrice: number;
yearlyPrice: number;
originalMonthlyPrice?: number;
originalYearlyPrice?: number;
monthlyPriceId?: string;
yearlyPriceId?: string;
features: PricingFeature[];
isPopular?: boolean;
colorScheme: "dark" | "green" | "purple" | "orange";
}
export const pricingConfig = {
header: {
title: "Purchase a subscription",
subtitle:
"Upgrade to gain access to Pro features and generate more, faster.",
},
yearlyDiscount: 20,
tiers: [
{
id: "free",
name: "Free",
monthlyPrice: 0,
yearlyPrice: 0,
features: [
{ text: "Free daily generations", included: true },
{ text: "Limited access to ArtCraft tools", included: true },
],
colorScheme: "dark" as const,
},
{
id: "plus",
name: "Plus",
monthlyPrice: 8,
yearlyPrice: 96,
originalMonthlyPrice: 10,
originalYearlyPrice: 120,
monthlyPriceId: "price_1234567890abcdef", //dummy
yearlyPriceId: "price_abcdef1234567890", //dummy
features: [
{ text: "~1,010 Flux images", included: true },
{ text: "~36,000 real-time images", included: true },
{ text: "~180 enhanced images", included: true },
{ text: "~6 training jobs", included: true },
{ text: "Commercial license", included: true },
],
colorScheme: "green" as const,
},
{
id: "pro",
name: "Pro",
monthlyPrice: 28,
yearlyPrice: 336,
originalMonthlyPrice: 35,
originalYearlyPrice: 420,
monthlyPriceId: "price_fedcba0987654321", //dummy
yearlyPriceId: "price_123456789fedcba0", //dummy
features: [
{ text: "~5,048 Flux images", included: true },
{ text: "~180,000 real-time images", included: true },
{ text: "~900 enhanced images", included: true },
{ text: "~30 training jobs", included: true },
{ text: "Commercial license", included: true },
],
colorScheme: "purple" as const,
},
{
id: "max",
name: "Max",
monthlyPrice: 48,
yearlyPrice: 576,
originalMonthlyPrice: 60,
originalYearlyPrice: 720,
monthlyPriceId: "price_9876543210fedcba", //dummy
yearlyPriceId: "price_abcdef9876543210", //dummy
features: [
{ text: "~15,142 Flux images", included: true },
{ text: "~540,000 real-time images", included: true },
{ text: "~2,700 enhanced images", included: true },
{ text: "~90 training jobs", included: true },
{ text: "Commercial license", included: true },
],
colorScheme: "orange" as const,
},
] as PricingTier[],
};
@@ -0,0 +1,42 @@
import { create } from "zustand";
interface SubscriptionState {
currentPlanId: string;
hasActiveSubscription: boolean;
customerId?: string;
subscriptionId?: string;
billingCycle?: "monthly" | "yearly";
}
interface PricingModalState {
isOpen: boolean;
subscription: SubscriptionState;
openModal: () => void;
closeModal: () => void;
toggleModal: () => void;
setSubscription: (subscription: Partial<SubscriptionState>) => void;
updateCurrentPlan: (planId: string, hasActiveSubscription: boolean) => void;
}
export const usePricingModalStore = create<PricingModalState>((set) => ({
isOpen: false,
subscription: {
currentPlanId: "free",
hasActiveSubscription: false,
},
openModal: () => set({ isOpen: true }),
closeModal: () => set({ isOpen: false }),
toggleModal: () => set((state) => ({ isOpen: !state.isOpen })),
setSubscription: (subscription) =>
set((state) => ({
subscription: { ...state.subscription, ...subscription },
})),
updateCurrentPlan: (planId, hasActiveSubscription) =>
set((state) => ({
subscription: {
...state.subscription,
currentPlanId: planId,
hasActiveSubscription,
},
})),
}));
@@ -0,0 +1,7 @@
/*
* Replace this with your own classes
*
* e.g.
* .container {
* }
*/
@@ -0,0 +1,12 @@
import { render } from '@testing-library/react';
import PricingModal from './pricing-modal';
describe('PricingModal', () => {
it('should render successfully', () => {
const { baseElement } = render(<PricingModal />);
expect(baseElement).toBeTruthy();
});
});
@@ -0,0 +1,316 @@
import { Modal } from "@storyteller/ui-modal";
import { Button } from "@storyteller/ui-button";
import { useState } from "react";
import { twMerge } from "tailwind-merge";
import { pricingConfig, PricingTier } from "./pricing-config";
import { usePricingModalStore } from "./pricing-modal-store";
import { TabSelector } from "@storyteller/ui-tab-selector";
const billingTabs = [
{ id: "yearly", label: "Yearly" },
{ id: "monthly", label: "Monthly" },
];
interface PricingModalProps {
currentPlanId?: string;
hasActiveSubscription?: boolean;
}
export function PricingModal({
currentPlanId,
hasActiveSubscription,
}: PricingModalProps = {}) {
const { isOpen, closeModal, subscription } = usePricingModalStore();
// Use props if provided or fall back to store
const activePlanId = currentPlanId ?? subscription.currentPlanId;
const hasActiveSub =
hasActiveSubscription ?? subscription.hasActiveSubscription;
const [billingType, setBillingType] = useState("yearly");
const isYearly = billingType === "yearly";
const handleUpgrade = async (tierId: string) => {
// TODO: Implement Stripe checkout
const tier = pricingConfig.tiers.find((t) => t.id === tierId);
const priceId = isYearly ? tier?.yearlyPriceId : tier?.monthlyPriceId;
console.log(
`Upgrading to ${tierId} plan with ${
isYearly ? "yearly" : "monthly"
} billing`,
{ priceId, tierId }
);
// Example Stripe checkout implementation:
// await stripe.redirectToCheckout({
// lineItems: [{ price: priceId, quantity: 1 }],
// mode: 'subscription',
// successUrl: `${window.location.origin}/success`,
// cancelUrl: `${window.location.origin}/cancel`,
// });
};
const handleManageSubscription = () => {
// TODO: Redirect to Stripe customer portal
console.log("Managing subscription");
};
const tierHierarchy = { free: 0, basic: 1, pro: 2, max: 3 };
const isCurrentPlan = (tierId: string) => {
return tierId === activePlanId;
};
const getButtonText = (tier: PricingTier) => {
if (isCurrentPlan(tier.id)) {
return "Current Plan";
}
if (activePlanId && activePlanId !== "free") {
const currentTierLevel =
tierHierarchy[activePlanId as keyof typeof tierHierarchy];
const thisTierLevel =
tierHierarchy[tier.id as keyof typeof tierHierarchy];
if (thisTierLevel < currentTierLevel) {
return "Switch Plan";
}
}
return "Upgrade Plan";
};
const getColorSchemeClasses = (
colorScheme: PricingTier["colorScheme"],
isCurrent: boolean
) => {
const baseClasses =
"relative rounded-xl border p-8 h-full flex flex-col transition-all duration-300 ring-[6px] ring-transparent";
switch (colorScheme) {
case "dark":
return twMerge(
baseClasses,
"bg-[#2C2C2C] border-gray-600",
isCurrent && "ring-white/70"
);
case "green":
// Basic
return twMerge(
baseClasses,
"bg-gradient-to-b from-[#002D23] via-[#006B54] to-[#00D28B] border-[#00a873]",
"hover:shadow-[0_0_30px_rgba(0,210,139,0.4)]",
isCurrent && "ring-[#00D28B] shadow-[0_0_30px_rgba(0,210,139,0.4)]"
);
case "purple":
// Pro
return twMerge(
baseClasses,
"bg-gradient-to-b from-[#2D004D] via-[#6400A8] to-[#C03FFF] border-[#9D4CFF]",
"hover:shadow-[0_0_30px_rgba(192,63,255,0.4)]",
isCurrent && "ring-[#C03FFF] shadow-[0_0_30px_rgba(192,63,255,0.4)]"
);
case "orange":
// Max
return twMerge(
baseClasses,
"bg-gradient-to-b from-[#332100] via-[#B35C00] to-[#FFB347] border-[#FF8C00]",
"hover:shadow-[0_0_30px_rgba(255,179,71,0.4)]",
isCurrent && "ring-[#FFB347] shadow-[0_0_30px_rgba(255,179,71,0.4)]"
);
default:
return twMerge(
baseClasses,
"bg-[#2C2C2C] border-gray-600",
"hover:shadow-[0_0_30px_rgba(255,255,255,0.3)]",
isCurrent && "ring-white/70"
);
}
};
const formatPrice = (tier: PricingTier) => {
if (tier.monthlyPrice === 0) {
return {
current: "$0",
original: null,
};
}
if (isYearly) {
const discountedMonthlyPrice = Math.round(tier.yearlyPrice / 12);
const originalMonthlyPrice = tier.originalYearlyPrice
? Math.round(tier.originalYearlyPrice / 12)
: null;
return {
current: `$${discountedMonthlyPrice}`,
original: originalMonthlyPrice ? `$${originalMonthlyPrice}` : null,
};
} else {
const monthlyPrice = tier.originalMonthlyPrice || tier.monthlyPrice;
return {
current: `$${monthlyPrice}`,
original: null,
};
}
};
return (
<Modal
isOpen={isOpen}
onClose={closeModal}
className="rounded-xl bg-[#1A1A1A] max-h-[90vh] max-w-screen-2xl overflow-y-auto flex flex-col"
allowBackgroundInteraction={true}
showClose={true}
closeOnOutsideClick={true}
resizable={false}
backdropClassName="bg-black/80"
>
<div className="p-16 py-24 flex-1 overflow-y-auto min-h-0">
{/* Header */}
<div className="text-center mb-10">
<h1 className="text-5xl font-bold text-white mb-4">
{pricingConfig.header.title}
</h1>
<p className="text-gray-400 text-lg mb-6">
{pricingConfig.header.subtitle}
</p>
{/* Billing Toggle */}
<div className="flex items-center justify-center gap-4 mb-8 relative w-fit mx-auto">
<TabSelector
tabs={billingTabs}
activeTab={billingType}
onTabChange={setBillingType}
className="w-fit"
tabClassName="w-24 text-md"
indicatorClassName="bg-white"
selectedTabClassName="text-black"
/>
<span className="bg-primary text-white px-3 py-0.5 rounded-full text-sm font-medium -top-3 -left-6 absolute pointer-events-none">
-{pricingConfig.yearlyDiscount}%
</span>
</div>
</div>
{/* Pricing Tiers */}
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-4 gap-10">
{pricingConfig.tiers.map((tier) => {
const pricing = formatPrice(tier);
return (
<div
key={tier.id}
className={getColorSchemeClasses(
tier.colorScheme,
isCurrentPlan(tier.id)
)}
>
{/* Current Plan Badge */}
{isCurrentPlan(tier.id) && (
<div className="absolute -top-4 right-5 bg-white text-black px-3 py-1 rounded-full text-md font-semibold shadow-xl">
Active
</div>
)}
{/* Tier Header */}
<div className="text-center mb-8">
<h3 className="text-4xl font-bold text-white mb-4">
{tier.name}
</h3>
<div className="flex items-baseline justify-center gap-2">
{pricing.original && (
<span className="text-white/60 line-through text-2xl">
{pricing.original}
</span>
)}
<span className="text-4xl font-bold text-white">
{pricing.current}
</span>
<span className="text-white/60 text-sm">/month</span>
</div>
<p className="text-gray-300 text-xs mt-1">
{isYearly ? "billed yearly" : "billed monthly"}
</p>
</div>
{/* Features */}
<div className="flex-1 space-y-3 mb-6">
{tier.features.map((feature, index) => (
<div key={index} className="flex items-start gap-3">
<div
className={twMerge(
"w-5 h-5 rounded-full flex items-center justify-center flex-shrink-0 mt-0.5",
feature.included
? "bg-white text-black"
: "bg-transparent border border-gray-400"
)}
>
{feature.included && (
<svg
className="w-3 h-3"
fill="currentColor"
viewBox="0 0 20 20"
>
<path
fillRule="evenodd"
d="M16.707 5.293a1 1 0 010 1.414l-8 8a1 1 0 01-1.414 0l-4-4a1 1 0 011.414-1.414L8 12.586l7.293-7.293a1 1 0 011.414 0z"
clipRule="evenodd"
/>
</svg>
)}
</div>
<span
className={twMerge(
"text-sm mt-0.5",
feature.included ? "text-white" : "text-gray-400"
)}
>
{feature.text}
</span>
</div>
))}
</div>
{/* CTA Button */}
<Button
onClick={() => handleUpgrade(tier.id)}
disabled={isCurrentPlan(tier.id)}
className="w-full bg-white text-black hover:bg-white/90 h-12 rounded-xl"
>
{getButtonText(tier)}
</Button>
</div>
);
})}
</div>
{/* Manage Subscription Button - only show if user has a paid plan */}
{hasActiveSub && activePlanId !== "free" && (
<div className="flex justify-center mt-8">
<Button
onClick={handleManageSubscription}
className="bg-transparent border border-white/25 text-white hover:bg-white/10 px-8 py-3 rounded-xl"
>
Manage your subscription
</Button>
</div>
)}
</div>
</Modal>
);
}
// Additional interfaces for Stripe integration
export interface SubscriptionData {
currentPlanId: string;
hasActiveSubscription: boolean;
customerId?: string;
subscriptionId?: string;
billingCycle?: "monthly" | "yearly";
nextBillingDate?: Date;
}
export default PricingModal;
@@ -0,0 +1,19 @@
{
"files": [],
"include": [],
"references": [
{
"path": "../button"
},
{
"path": "../modal"
},
{
"path": "./tsconfig.lib.json"
},
{
"path": "./tsconfig.spec.json"
}
],
"extends": "../../../tsconfig.base.json"
}
@@ -0,0 +1,53 @@
{
"extends": "../../../tsconfig.base.json",
"compilerOptions": {
"outDir": "dist",
"types": [
"node",
"@nx/react/typings/cssmodule.d.ts",
"@nx/react/typings/image.d.ts",
"vite/client"
],
"rootDir": "src",
"jsx": "react-jsx",
"tsBuildInfoFile": "dist/tsconfig.lib.tsbuildinfo"
},
"exclude": [
"out-tsc",
"dist",
"**/*.spec.ts",
"**/*.test.ts",
"**/*.spec.tsx",
"**/*.test.tsx",
"**/*.spec.js",
"**/*.test.js",
"**/*.spec.jsx",
"**/*.test.jsx",
"vite.config.ts",
"vite.config.mts",
"vitest.config.ts",
"vitest.config.mts",
"src/**/*.test.ts",
"src/**/*.spec.ts",
"src/**/*.test.tsx",
"src/**/*.spec.tsx",
"src/**/*.test.js",
"src/**/*.spec.js",
"src/**/*.test.jsx",
"src/**/*.spec.jsx"
],
"include": [
"src/**/*.js",
"src/**/*.jsx",
"src/**/*.ts",
"src/**/*.tsx"
],
"references": [
{
"path": "../button/tsconfig.lib.json"
},
{
"path": "../modal/tsconfig.lib.json"
}
]
}
@@ -0,0 +1,34 @@
{
"extends": "../../../tsconfig.base.json",
"compilerOptions": {
"outDir": "./out-tsc/vitest",
"types": [
"vitest/globals",
"vitest/importMeta",
"vite/client",
"node",
"vitest"
],
"jsx": "react-jsx"
},
"include": [
"vite.config.ts",
"vite.config.mts",
"vitest.config.ts",
"vitest.config.mts",
"src/**/*.test.ts",
"src/**/*.spec.ts",
"src/**/*.test.tsx",
"src/**/*.spec.tsx",
"src/**/*.test.js",
"src/**/*.spec.js",
"src/**/*.test.jsx",
"src/**/*.spec.jsx",
"src/**/*.d.ts"
],
"references": [
{
"path": "./tsconfig.lib.json"
}
]
}
@@ -0,0 +1,49 @@
/// <reference types='vitest' />
import { defineConfig } from 'vite';
import react from '@vitejs/plugin-react';
import dts from 'vite-plugin-dts';
import * as path from 'path';
export default defineConfig(() => ({
root: __dirname,
cacheDir: '../../../node_modules/.vite/libs/components/pricing-modal',
plugins: [react(), dts({ entryRoot: 'src', tsconfigPath: path.join(__dirname, 'tsconfig.lib.json') })],
// Uncomment this if you are using workers.
// worker: {
// plugins: [ nxViteTsPaths() ],
// },
// Configuration for building your library.
// See: https://vitejs.dev/guide/build.html#library-mode
build: {
outDir: './dist',
emptyOutDir: true,
reportCompressedSize: true,
commonjsOptions: {
transformMixedEsModules: true,
},
lib: {
// Could also be a dictionary or array of multiple entry points.
entry: 'src/index.ts',
name: '@storyteller/ui-pricing-modal',
fileName: 'index',
// Change this to the formats you want to support.
// Don't forget to update your package.json as well.
formats: ['es' as const]
},
rollupOptions: {
// External packages that should not be bundled into your library.
external: ['react','react-dom','react/jsx-runtime']
},
},
test: {
watch: false,
globals: true,
environment: 'jsdom',
include: ['{src,tests}/**/*.{test,spec}.{js,mjs,cjs,ts,mts,cts,jsx,tsx}'],
reporters: ['default'],
coverage: {
reportsDirectory: './test-output/vitest/coverage',
provider: 'v8' as const,
}
},
}));
@@ -15,6 +15,9 @@ interface TabSelectorProps {
className?: string;
disabled?: boolean;
disabledMessage?: string;
tabClassName?: string;
indicatorClassName?: string;
selectedTabClassName?: string;
}
export const TabSelector: React.FC<TabSelectorProps> = ({
@@ -24,6 +27,9 @@ export const TabSelector: React.FC<TabSelectorProps> = ({
className,
disabled,
disabledMessage,
tabClassName,
indicatorClassName,
selectedTabClassName,
}) => {
// Find the index of the active tab
const selectedIndex = tabs.findIndex((tab) => tab.id === activeTab);
@@ -63,7 +69,10 @@ export const TabSelector: React.FC<TabSelectorProps> = ({
<TabList className="glass glass-no-hover relative inline-flex min-w-fit overflow-x-auto rounded-lg p-0.5 py-1 !shadow-none">
{/* Animated indicator */}
<div
className="absolute top-1 z-10 h-[calc(100%-8px)] rounded-md bg-primary/30 transition-all duration-200 ease-in-out"
className={twMerge(
"absolute top-1 z-10 h-[calc(100%-8px)] rounded-md bg-primary/30 transition-all duration-200 ease-in-out",
indicatorClassName
)}
style={{
left: indicatorStyle.left,
width: indicatorStyle.width,
@@ -83,8 +92,11 @@ export const TabSelector: React.FC<TabSelectorProps> = ({
twMerge(
"relative z-20 mx-0.5 min-w-max rounded-md border-2 border-transparent px-4 py-0.5 text-center text-sm font-semibold transition-all duration-200 ease-in-out",
"focus-visible:outline-none focus-visible:ring-0",
selected ? "text-white" : "text-gray-300 hover:text-white",
disabled ? "cursor-not-allowed opacity-60" : ""
selected
? twMerge("text-white", selectedTabClassName)
: "text-gray-300 hover:text-white",
disabled ? "cursor-not-allowed opacity-60" : "",
tabClassName
)
}
>
+8
View File
@@ -1466,6 +1466,10 @@
"name": "@storyteller/ui-popover",
"version": "0.0.1"
},
"libs/components/pricing-modal": {
"name": "@storyteller/ui-pricing-modal",
"version": "0.0.1"
},
"libs/components/promptbox": {
"name": "@storyteller/ui-promptbox",
"version": "0.0.1"
@@ -9765,6 +9769,10 @@
"resolved": "libs/components/popover",
"link": true
},
"node_modules/@storyteller/ui-pricing-modal": {
"resolved": "libs/components/pricing-modal",
"link": true
},
"node_modules/@storyteller/ui-promptbox": {
"resolved": "libs/components/promptbox",
"link": true
+3
View File
@@ -140,6 +140,9 @@
},
{
"path": "./libs/components/image-input"
},
{
"path": "./libs/components/pricing-modal"
}
]
}