mirror of
https://github.com/Comfy-Org/ComfyUI_frontend.git
synced 2026-02-21 15:24:09 +00:00
style: redesign TopUpCredits dialog (#7305)
Redesigned the TopUpCredits dialog to match Figma design specifications with proper layout, typography, colors and selection states. Updated dialog to use workflow-aware messaging, removed header, applied design system tokens, and integrated subscription renewal dates. Modified credit packages to use clean USD amounts with realistic video estimates and fixed button disabled states to show blue with 30% opacity per Figma design. | Before | After | | --------------------------------------------------------------------------------------------------------------------------------------------- | --------------------------------------------------------------------------------------------------------------------------------------------- | | <img width="675" height="863" alt="Screenshot from 2025-12-09 18-08-21" src="https://github.com/user-attachments/assets/331c7a48-74ae-4a58-b70f-aa476c3fc87c" /> | <img width="675" height="863" alt="Screenshot from 2025-12-09 18-06-23" src="https://github.com/user-attachments/assets/dcb7b358-6045-4c89-82ed-3283a20eea89" /> |
This commit is contained in:
@@ -7,7 +7,12 @@
|
||||
<Skeleton width="8rem" height="2rem" />
|
||||
</div>
|
||||
<div v-else class="flex items-center gap-1">
|
||||
<Tag severity="secondary" rounded class="p-1 text-amber-400">
|
||||
<Tag
|
||||
v-if="!showCreditsOnly"
|
||||
severity="secondary"
|
||||
rounded
|
||||
class="p-1 text-amber-400"
|
||||
>
|
||||
<template #icon>
|
||||
<i
|
||||
:class="
|
||||
@@ -18,7 +23,9 @@
|
||||
/>
|
||||
</template>
|
||||
</Tag>
|
||||
<div :class="textClass">{{ formattedBalance }}</div>
|
||||
<div :class="textClass">
|
||||
{{ showCreditsOnly ? formattedCreditsOnly : formattedBalance }}
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
@@ -32,8 +39,9 @@ import { formatCreditsFromCents } from '@/base/credits/comfyCredits'
|
||||
import { useFeatureFlags } from '@/composables/useFeatureFlags'
|
||||
import { useFirebaseAuthStore } from '@/stores/firebaseAuthStore'
|
||||
|
||||
const { textClass } = defineProps<{
|
||||
const { textClass, showCreditsOnly } = defineProps<{
|
||||
textClass?: string
|
||||
showCreditsOnly?: boolean
|
||||
}>()
|
||||
|
||||
const authStore = useFirebaseAuthStore()
|
||||
@@ -50,4 +58,14 @@ const formattedBalance = computed(() => {
|
||||
})
|
||||
return `${amount} ${t('credits.credits')}`
|
||||
})
|
||||
|
||||
const formattedCreditsOnly = computed(() => {
|
||||
// Backend returns cents despite the *_micros naming convention.
|
||||
const cents = authStore.balance?.amount_micros ?? 0
|
||||
const amount = formatCreditsFromCents({
|
||||
cents,
|
||||
locale: locale.value
|
||||
})
|
||||
return amount
|
||||
})
|
||||
</script>
|
||||
|
||||
@@ -1,35 +1,43 @@
|
||||
<template>
|
||||
<!-- New Credits Design (default) -->
|
||||
<div
|
||||
v-if="useNewDesign"
|
||||
class="flex w-96 flex-col gap-8 p-8 bg-node-component-surface rounded-2xl border border-border-primary"
|
||||
>
|
||||
<div v-if="useNewDesign" class="flex w-112 flex-col gap-8 p-8">
|
||||
<!-- Header -->
|
||||
<div class="flex flex-col gap-4">
|
||||
<h1 class="text-2xl font-semibold text-foreground-primary m-0">
|
||||
{{ $t('credits.topUp.addMoreCredits') }}
|
||||
<h1 class="text-2xl font-semibold text-white m-0">
|
||||
{{
|
||||
isInsufficientCredits
|
||||
? $t('credits.topUp.addMoreCreditsToRun')
|
||||
: $t('credits.topUp.addMoreCredits')
|
||||
}}
|
||||
</h1>
|
||||
<p class="text-sm text-foreground-secondary m-0">
|
||||
{{ $t('credits.topUp.creditsDescription') }}
|
||||
</p>
|
||||
<div v-if="isInsufficientCredits" class="flex flex-col gap-2">
|
||||
<p class="text-sm text-muted-foreground m-0 w-96">
|
||||
{{ $t('credits.topUp.insufficientWorkflowMessage') }}
|
||||
</p>
|
||||
</div>
|
||||
<div v-else class="flex flex-col gap-2">
|
||||
<p class="text-sm text-muted-foreground m-0">
|
||||
{{ $t('credits.topUp.creditsDescription') }}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Current Balance Section -->
|
||||
<div class="flex flex-col gap-4">
|
||||
<div class="flex items-baseline gap-2">
|
||||
<UserCredit text-class="text-3xl font-bold" />
|
||||
<span class="text-sm text-foreground-secondary">{{
|
||||
<UserCredit text-class="text-3xl font-bold" show-credits-only />
|
||||
<span class="text-sm text-muted-foreground">{{
|
||||
$t('credits.creditsAvailable')
|
||||
}}</span>
|
||||
</div>
|
||||
<div v-if="refreshDate" class="text-sm text-foreground-secondary">
|
||||
{{ $t('credits.refreshes', { date: refreshDate }) }}
|
||||
<div v-if="formattedRenewalDate" class="text-sm text-muted-foreground">
|
||||
{{ $t('credits.refreshes', { date: formattedRenewalDate }) }}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Credit Options Section -->
|
||||
<div class="flex flex-col gap-4">
|
||||
<span class="text-sm text-foreground-secondary">
|
||||
<span class="text-sm text-muted-foreground">
|
||||
{{ $t('credits.topUp.howManyCredits') }}
|
||||
</span>
|
||||
<div class="flex flex-col gap-2">
|
||||
@@ -42,7 +50,7 @@
|
||||
@select="selectedCredits = option.credits"
|
||||
/>
|
||||
</div>
|
||||
<div class="text-xs text-foreground-secondary">
|
||||
<div class="text-xs text-muted-foreground w-96">
|
||||
{{ $t('credits.topUp.templateNote') }}
|
||||
</div>
|
||||
</div>
|
||||
@@ -53,7 +61,8 @@
|
||||
:loading="loading"
|
||||
severity="primary"
|
||||
:label="$t('credits.topUp.buy')"
|
||||
class="w-full"
|
||||
:class="['w-full', { 'opacity-30': !selectedCredits || loading }]"
|
||||
:pt="{ label: { class: 'text-white' } }"
|
||||
@click="handleBuy"
|
||||
/>
|
||||
</div>
|
||||
@@ -121,6 +130,7 @@ import {
|
||||
import UserCredit from '@/components/common/UserCredit.vue'
|
||||
import { useFirebaseAuthActions } from '@/composables/auth/useFirebaseAuthActions'
|
||||
import { useFeatureFlags } from '@/composables/useFeatureFlags'
|
||||
import { useSubscription } from '@/platform/cloud/subscription/composables/useSubscription'
|
||||
import { useTelemetry } from '@/platform/telemetry'
|
||||
|
||||
import CreditTopUpOption from './credit/CreditTopUpOption.vue'
|
||||
@@ -132,18 +142,17 @@ interface CreditOption {
|
||||
}
|
||||
|
||||
const {
|
||||
refreshDate,
|
||||
isInsufficientCredits = false,
|
||||
amountOptions = [5, 10, 20, 50],
|
||||
preselectedAmountOption = 10
|
||||
} = defineProps<{
|
||||
refreshDate?: string
|
||||
isInsufficientCredits?: boolean
|
||||
amountOptions?: number[]
|
||||
preselectedAmountOption?: number
|
||||
}>()
|
||||
|
||||
const { flags } = useFeatureFlags()
|
||||
const { formattedRenewalDate } = useSubscription()
|
||||
// Use feature flag to determine design - defaults to true (new design)
|
||||
const useNewDesign = computed(() => flags.subscriptionTiersEnabled)
|
||||
|
||||
@@ -157,20 +166,20 @@ const loading = ref(false)
|
||||
|
||||
const creditOptions: CreditOption[] = [
|
||||
{
|
||||
credits: 1000,
|
||||
description: t('credits.topUp.videosEstimate', { count: 100 })
|
||||
credits: 1055, // $5.00
|
||||
description: t('credits.topUp.videosEstimate', { count: 41 })
|
||||
},
|
||||
{
|
||||
credits: 5000,
|
||||
description: t('credits.topUp.videosEstimate', { count: 500 })
|
||||
credits: 2110, // $10.00
|
||||
description: t('credits.topUp.videosEstimate', { count: 82 })
|
||||
},
|
||||
{
|
||||
credits: 10000,
|
||||
description: t('credits.topUp.videosEstimate', { count: 1000 })
|
||||
credits: 4220, // $20.00
|
||||
description: t('credits.topUp.videosEstimate', { count: 184 })
|
||||
},
|
||||
{
|
||||
credits: 20000,
|
||||
description: t('credits.topUp.videosEstimate', { count: 2000 })
|
||||
credits: 10550, // $50.00
|
||||
description: t('credits.topUp.videosEstimate', { count: 412 })
|
||||
}
|
||||
]
|
||||
|
||||
|
||||
@@ -3,19 +3,17 @@
|
||||
class="flex items-center justify-between p-2 rounded-lg cursor-pointer transition-all duration-200"
|
||||
:class="[
|
||||
selected
|
||||
? 'bg-surface-secondary border-2 border-primary'
|
||||
: 'bg-surface-tertiary border border-border-primary hover:bg-surface-secondary'
|
||||
? 'bg-secondary-background border-2 border-border-default'
|
||||
: 'bg-component-node-disabled hover:bg-secondary-background border-2 border-transparent'
|
||||
]"
|
||||
@click="$emit('select')"
|
||||
>
|
||||
<div class="flex flex-col">
|
||||
<span class="text-base font-medium text-foreground-primary">
|
||||
{{ formattedCredits }}
|
||||
</span>
|
||||
<span class="text-sm text-foreground-secondary">
|
||||
{{ description }}
|
||||
</span>
|
||||
</div>
|
||||
<span class="text-base font-bold text-white">
|
||||
{{ formattedCredits }}
|
||||
</span>
|
||||
<span class="text-sm font-normal text-white">
|
||||
{{ description }}
|
||||
</span>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
@@ -38,6 +36,10 @@ defineEmits<{
|
||||
const { locale } = useI18n()
|
||||
|
||||
const formattedCredits = computed(() => {
|
||||
return formatCredits({ value: credits, locale: locale.value })
|
||||
return formatCredits({
|
||||
value: credits,
|
||||
locale: locale.value,
|
||||
numberOptions: { minimumFractionDigits: 0, maximumFractionDigits: 0 }
|
||||
})
|
||||
})
|
||||
</script>
|
||||
|
||||
@@ -1847,6 +1847,8 @@
|
||||
"seeDetails": "See details",
|
||||
"topUp": "Top Up",
|
||||
"addMoreCredits": "Add more credits",
|
||||
"addMoreCreditsToRun": "Add more credits to run",
|
||||
"insufficientWorkflowMessage": "You don't have enough credits to run this workflow.",
|
||||
"creditsDescription": "Credits are used to run workflows or partner nodes.",
|
||||
"howManyCredits": "How many credits would you like to add?",
|
||||
"videosEstimate": "~{count} videos*",
|
||||
@@ -1869,7 +1871,7 @@
|
||||
"accountInitialized": "Account initialized",
|
||||
"unified": {
|
||||
"message": "Credits have been unified",
|
||||
"tooltip": "We've unified payments across Comfy. Everything now runs on Comfy Credits:\n- Partner Nodes (formerly API nodes)\n- Cloud workflows\n\nYour existing Partner node balance has been converted into credits.\nLearn more here."
|
||||
"tooltip": "We've unified payments across Comfy. Everything now runs on Comfy Credits:\n- Partner Nodes (formerly API nodes)\n- Cloud workflows\n\nYour existing Partner node balance has been converted into credits."
|
||||
}
|
||||
},
|
||||
"subscription": {
|
||||
|
||||
@@ -386,11 +386,12 @@ export const useDialogService = () => {
|
||||
return dialogStore.showDialog({
|
||||
key: 'top-up-credits',
|
||||
component: TopUpCreditsDialogContent,
|
||||
headerComponent: ComfyOrgHeader,
|
||||
props: options,
|
||||
dialogComponentProps: {
|
||||
headless: true,
|
||||
pt: {
|
||||
header: { class: 'p-3!' }
|
||||
header: { class: 'p-0! hidden' },
|
||||
content: { class: 'p-0! m-0!' }
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
@@ -32,16 +32,12 @@ describe('CreditTopUpOption', () => {
|
||||
expect(wrapper.text()).toContain('~500 videos*')
|
||||
})
|
||||
|
||||
it('applies selected styling when selected', () => {
|
||||
const wrapper = mountOption({ selected: true })
|
||||
expect(wrapper.find('div').classes()).toContain('bg-surface-secondary')
|
||||
expect(wrapper.find('div').classes()).toContain('border-primary')
|
||||
})
|
||||
|
||||
it('applies unselected styling when not selected', () => {
|
||||
const wrapper = mountOption({ selected: false })
|
||||
expect(wrapper.find('div').classes()).toContain('bg-surface-tertiary')
|
||||
expect(wrapper.find('div').classes()).toContain('border-border-primary')
|
||||
expect(wrapper.find('div').classes()).toContain(
|
||||
'bg-component-node-disabled'
|
||||
)
|
||||
expect(wrapper.find('div').classes()).toContain('border-transparent')
|
||||
})
|
||||
|
||||
it('emits select event when clicked', async () => {
|
||||
|
||||
Reference in New Issue
Block a user