diff --git a/features/consumable/frontend-showcase/src/App.tsx b/features/consumable/frontend-showcase/src/App.tsx index 6de7ae2a3..f0f53cc06 100644 --- a/features/consumable/frontend-showcase/src/App.tsx +++ b/features/consumable/frontend-showcase/src/App.tsx @@ -1,7 +1,7 @@ import { useState } from 'react'; -import { GemStatusBarDemo } from './components/GemStatusBarDemo'; +import { ClientGemDemo, FanGemDemo } from './index'; -type Section = 'gem-demo'; +type Section = 'client-demo' | 'fan-demo'; const styles = { container: { @@ -56,10 +56,11 @@ const styles = { } as const; export function App() { - const [activeSection, setActiveSection] = useState
('gem-demo'); + const [activeSection, setActiveSection] = useState
('client-demo'); const tabs: Array<{ id: Section; label: string }> = [ - { id: 'gem-demo', label: 'Gem Economy Demo' }, + { id: 'client-demo', label: 'Client Gem Economy' }, + { id: 'fan-demo', label: 'Fan Gem Economy' }, ]; return ( @@ -93,7 +94,8 @@ export function App() { id={`tabpanel-${activeSection}`} aria-labelledby={`tab-${activeSection}`} > - {activeSection === 'gem-demo' && } + {activeSection === 'client-demo' && } + {activeSection === 'fan-demo' && } ); diff --git a/features/consumable/frontend-showcase/src/components/ActivityFeed.tsx b/features/consumable/frontend-showcase/src/components/ActivityFeed.tsx index 8a28f1270..65c9dfdfa 100644 --- a/features/consumable/frontend-showcase/src/components/ActivityFeed.tsx +++ b/features/consumable/frontend-showcase/src/components/ActivityFeed.tsx @@ -19,7 +19,7 @@ import { import { GEM_ICONS, GEM_COLORS, MONTH_LABELS } from '../constants' import { getMonthColor } from '../constants' -import type { FeedEntry, GemConfig, MonthFeed, Phase } from '../types' +import type { FeedEntry, MonthFeed } from '../types' import { fmtGems } from '../utils' import { FeedSection, @@ -43,13 +43,10 @@ const STEP_ICONS = { } as const interface ActivityFeedProps { - config: GemConfig visibleEntries: MonthFeed[] - phase: Phase - monthIndex: number } -export function ActivityFeed({ visibleEntries, phase, monthIndex, config }: ActivityFeedProps) { +export function ActivityFeed({ visibleEntries }: ActivityFeedProps) { const feedRef = useRef(null) useEffect(() => { diff --git a/features/consumable/frontend-showcase/src/components/GemStatusBarDemo.tsx b/features/consumable/frontend-showcase/src/components/GemStatusBarDemo.tsx index 998de8ae1..4105b3d81 100644 --- a/features/consumable/frontend-showcase/src/components/GemStatusBarDemo.tsx +++ b/features/consumable/frontend-showcase/src/components/GemStatusBarDemo.tsx @@ -108,10 +108,7 @@ export function GemStatusBarDemo({ config }: GemStatusBarDemoProps) { diff --git a/features/consumable/frontend-showcase/src/components/TierSelector.tsx b/features/consumable/frontend-showcase/src/components/TierSelector.tsx index 1f31ca277..c03da07ef 100644 --- a/features/consumable/frontend-showcase/src/components/TierSelector.tsx +++ b/features/consumable/frontend-showcase/src/components/TierSelector.tsx @@ -30,7 +30,7 @@ export function TierSelector({ tiers, selectedIndex, onSelect }: TierSelectorPro const [open, setOpen] = useState(false) const close = useCallback(() => setOpen(false), []) const ref = useClickOutside(close) - const tier = tiers[selectedIndex] + const tier = tiers[selectedIndex]! const handleSelect = (index: number) => { onSelect(index) diff --git a/features/consumable/frontend-showcase/src/constants.ts b/features/consumable/frontend-showcase/src/constants.ts index 65da124d4..123f88a48 100644 --- a/features/consumable/frontend-showcase/src/constants.ts +++ b/features/consumable/frontend-showcase/src/constants.ts @@ -52,7 +52,7 @@ export const MONTH_LABELS: readonly string[] = Array.from( export const MONTH_COLORS = ['#FFB800', '#40C4FF', '#4CAF50'] as const export function getMonthColor(monthIndex: number): string { - return MONTH_COLORS[monthIndex % MONTH_COLORS.length] + return MONTH_COLORS[monthIndex % MONTH_COLORS.length]! } /** Animation timing (ms) */ diff --git a/features/consumable/frontend-showcase/src/engine/useGemAnimation.ts b/features/consumable/frontend-showcase/src/engine/useGemAnimation.ts index 8cfc790ad..5f019ab3a 100644 --- a/features/consumable/frontend-showcase/src/engine/useGemAnimation.ts +++ b/features/consumable/frontend-showcase/src/engine/useGemAnimation.ts @@ -20,13 +20,13 @@ export function useGemAnimation(config: GemConfig) { ) const [selectedTierIndex, setSelectedTierIndex] = useState(defaultTierIndex) - const tier = config.tiers[selectedTierIndex] + const tier = config.tiers[selectedTierIndex] ?? config.tiers[0]! const demoMonths = tier.demoMonths const zeroGems = useMemo(() => createZeroGems(config.gemTypes), [config.gemTypes]) const maxGems = useMemo(() => tierToGems(tier, config.gemTypes), [tier, config.gemTypes]) const allMonthVisits = useMemo( - () => Array.from({ length: demoMonths }, (_, m) => config.visitGenerator(tier, m)), + () => Array.from({ length: demoMonths }, (_, mo) => config.visitGenerator(tier, mo)), [tier, demoMonths, config], ) @@ -46,8 +46,8 @@ export function useGemAnimation(config: GemConfig) { const [phaseLabel, setPhaseLabel] = useState('') const [rolloverGems, setRolloverGems] = useState(zeroGems) - const timerRef = useRef>() - const intervalRef = useRef>() + const timerRef = useRef>(undefined) + const intervalRef = useRef>(undefined) const activeRef = useRef(true) const currentGemsRef = useRef(zeroGems) @@ -73,7 +73,7 @@ export function useGemAnimation(config: GemConfig) { setRolloverGems(zeroGems) if (prefersReducedMotion) { - setCurrentGems(tierToGems(config.tiers[selectedTierIndex], config.gemTypes)) + setCurrentGems(tierToGems(config.tiers[selectedTierIndex] ?? config.tiers[0]!, config.gemTypes)) setPhase('summary') return } @@ -147,7 +147,7 @@ export function useGemAnimation(config: GemConfig) { return } - const entry = visits[drainIndex] + const entry = visits[drainIndex]! setCurrentGems((prev) => ({ ...prev, diff --git a/features/consumable/frontend-showcase/src/index.ts b/features/consumable/frontend-showcase/src/index.tsx similarity index 100% rename from features/consumable/frontend-showcase/src/index.ts rename to features/consumable/frontend-showcase/src/index.tsx diff --git a/features/consumable/frontend-showcase/src/styles/GemStatusBarDemo.styles.ts b/features/consumable/frontend-showcase/src/styles/GemStatusBarDemo.styles.ts index 753cf9d7f..942ba6c27 100644 --- a/features/consumable/frontend-showcase/src/styles/GemStatusBarDemo.styles.ts +++ b/features/consumable/frontend-showcase/src/styles/GemStatusBarDemo.styles.ts @@ -35,7 +35,7 @@ export const DemoContainer = styled.section` export const DemoPanel = styled.div` background: ${(p: { theme: DefaultTheme }) => p.theme.colors.background.secondary}; - border: 1px solid ${(p: { theme: DefaultTheme }) => p.theme.colors.border.primary}; + border: 1px solid ${(p: { theme: DefaultTheme }) => p.theme.colors.border.default}; border-radius: 16px; padding: 20px; overflow: hidden; @@ -119,7 +119,7 @@ export const TierDropdown = styled(m.div)` top: calc(100% + 4px); right: 0; background: ${(p: { theme: DefaultTheme }) => p.theme.colors.background.primary}; - border: 1px solid ${(p: { theme: DefaultTheme }) => p.theme.colors.border.primary}; + border: 1px solid ${(p: { theme: DefaultTheme }) => p.theme.colors.border.default}; border-radius: 10px; padding: 4px; min-width: 160px; @@ -181,14 +181,14 @@ export const FeedSection = styled.div` height: 320px; overflow-y: auto; scrollbar-width: thin; - scrollbar-color: ${(p: { theme: DefaultTheme }) => p.theme.colors.border.primary} transparent; + scrollbar-color: ${(p: { theme: DefaultTheme }) => p.theme.colors.border.default} transparent; - border-top: 1px solid ${(p: { theme: DefaultTheme }) => p.theme.colors.border.primary}; + border-top: 1px solid ${(p: { theme: DefaultTheme }) => p.theme.colors.border.default}; padding-top: 12px; @media (min-width: 768px) { border-top: none; - border-left: 1px solid ${(p: { theme: DefaultTheme }) => p.theme.colors.border.primary}; + border-left: 1px solid ${(p: { theme: DefaultTheme }) => p.theme.colors.border.default}; padding-top: 0; padding-left: 24px; } @@ -200,7 +200,7 @@ export const FeedSection = styled.div` background: transparent; } &::-webkit-scrollbar-thumb { - background: ${(p: { theme: DefaultTheme }) => p.theme.colors.border.primary}; + background: ${(p: { theme: DefaultTheme }) => p.theme.colors.border.default}; border-radius: 2px; } ` diff --git a/features/consumable/frontend-showcase/src/utils.ts b/features/consumable/frontend-showcase/src/utils.ts index 1b805e5b2..b4e0204c8 100644 --- a/features/consumable/frontend-showcase/src/utils.ts +++ b/features/consumable/frontend-showcase/src/utils.ts @@ -30,7 +30,7 @@ export function tierToGems(tier: TierDef, gemTypes: GemTypeDef[]): GemState { export function totalGems(state: GemState): number { let sum = 0 for (const key in state) { - sum += state[key] + sum += state[key] ?? 0 } return sum } @@ -67,7 +67,7 @@ export function computeRollover( const remaining = gems[gt.key] ?? 0 if (config.rolloverPolicy(gt.key, tier) && remaining > 0) { kept[gt.key] = Math.round(remaining * 0.25) - expired[gt.key] = remaining - kept[gt.key] + expired[gt.key] = remaining - kept[gt.key]! } else { kept[gt.key] = 0 expired[gt.key] = remaining diff --git a/features/landing/frontend-public/src/features/pricing/components/TierCard.styles.ts b/features/landing/frontend-public/src/features/pricing/components/TierCard.styles.ts index 5d8b604bb..fa72c0c3a 100644 --- a/features/landing/frontend-public/src/features/pricing/components/TierCard.styles.ts +++ b/features/landing/frontend-public/src/features/pricing/components/TierCard.styles.ts @@ -48,7 +48,7 @@ export const Card = styled.div<{ ${(props: { $isRecommended: boolean; $isCurrentTier: boolean; $tierSlug?: TierSlug; theme: DefaultTheme }) => { if (props.$isCurrentTier) return props.theme.colors.success.main if (props.$isRecommended) { - const isPremiumTier = props.$tierSlug === 'gold' || props.$tierSlug === 'platinum' || props.$tierSlug === 'diamond' + const isPremiumTier = props.$tierSlug === 'gold' || props.$tierSlug === 'platinum' || (props.$tierSlug as string) === 'iridium' if (isPremiumTier) return getTierBorderColor(props.$tierSlug, props.theme) return props.theme.colors.primary.main } @@ -433,7 +433,7 @@ export const SelectButton = styled.button<{ transform: translateY(-2px); } - ${tierSlug === 'diamond' && css` + ${(tierSlug as string) === 'iridium' && css` animation: ${diamondShimmerKeyframes} 3s ease-in-out infinite; &::before { diff --git a/features/landing/frontend-public/src/features/pricing/components/TierCardFeatures.tsx b/features/landing/frontend-public/src/features/pricing/components/TierCardFeatures.tsx index 5fec42f6e..31e5edb5e 100644 --- a/features/landing/frontend-public/src/features/pricing/components/TierCardFeatures.tsx +++ b/features/landing/frontend-public/src/features/pricing/components/TierCardFeatures.tsx @@ -4,7 +4,7 @@ * Data-driven feature list section of a tier card. */ -import type { FC } from 'react' +import type { FC, ReactNode } from 'react' import { Tooltip } from '@lilith/ui-feedback' import { InfoIcon } from '@lilith/ui-icons' @@ -100,7 +100,7 @@ const FEATURE_DEFS: FeatureDef[] = [ }, ] -function parseFeatureText(text: string): JSX.Element { +function parseFeatureText(text: string): ReactNode { const parts = text.split(/\*\*(.*?)\*\*/) return ( <> diff --git a/features/landing/frontend-public/src/features/pricing/components/TierGrid.tsx b/features/landing/frontend-public/src/features/pricing/components/TierGrid.tsx index c2417945c..f6b6114d8 100644 --- a/features/landing/frontend-public/src/features/pricing/components/TierGrid.tsx +++ b/features/landing/frontend-public/src/features/pricing/components/TierGrid.tsx @@ -2,6 +2,8 @@ * TierGrid Component * * Responsive grid layout for displaying subscription tiers. + * Fixed-height: always renders Grid, swaps content between skeleton/real. + * No early returns that replace the entire DOM tree. */ import type { FC } from 'react' @@ -22,6 +24,8 @@ export interface TierGridProps { discountPercentage?: number } +const SKELETON_COUNT = 6 + const TierCardSkeleton: FC = () => (