mirror of
https://github.com/Comfy-Org/ComfyUI_frontend.git
synced 2026-04-30 11:11:53 +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" />
|
<Skeleton width="8rem" height="2rem" />
|
||||||
</div>
|
</div>
|
||||||
<div v-else class="flex items-center gap-1">
|
<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>
|
<template #icon>
|
||||||
<i
|
<i
|
||||||
:class="
|
:class="
|
||||||
@@ -18,7 +23,9 @@
|
|||||||
/>
|
/>
|
||||||
</template>
|
</template>
|
||||||
</Tag>
|
</Tag>
|
||||||
<div :class="textClass">{{ formattedBalance }}</div>
|
<div :class="textClass">
|
||||||
|
{{ showCreditsOnly ? formattedCreditsOnly : formattedBalance }}
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
@@ -32,8 +39,9 @@ import { formatCreditsFromCents } from '@/base/credits/comfyCredits'
|
|||||||
import { useFeatureFlags } from '@/composables/useFeatureFlags'
|
import { useFeatureFlags } from '@/composables/useFeatureFlags'
|
||||||
import { useFirebaseAuthStore } from '@/stores/firebaseAuthStore'
|
import { useFirebaseAuthStore } from '@/stores/firebaseAuthStore'
|
||||||
|
|
||||||
const { textClass } = defineProps<{
|
const { textClass, showCreditsOnly } = defineProps<{
|
||||||
textClass?: string
|
textClass?: string
|
||||||
|
showCreditsOnly?: boolean
|
||||||
}>()
|
}>()
|
||||||
|
|
||||||
const authStore = useFirebaseAuthStore()
|
const authStore = useFirebaseAuthStore()
|
||||||
@@ -50,4 +58,14 @@ const formattedBalance = computed(() => {
|
|||||||
})
|
})
|
||||||
return `${amount} ${t('credits.credits')}`
|
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>
|
</script>
|
||||||
|
|||||||
@@ -1,35 +1,43 @@
|
|||||||
<template>
|
<template>
|
||||||
<!-- New Credits Design (default) -->
|
<!-- New Credits Design (default) -->
|
||||||
<div
|
<div v-if="useNewDesign" class="flex w-112 flex-col gap-8 p-8">
|
||||||
v-if="useNewDesign"
|
|
||||||
class="flex w-96 flex-col gap-8 p-8 bg-node-component-surface rounded-2xl border border-border-primary"
|
|
||||||
>
|
|
||||||
<!-- Header -->
|
<!-- Header -->
|
||||||
<div class="flex flex-col gap-4">
|
<div class="flex flex-col gap-4">
|
||||||
<h1 class="text-2xl font-semibold text-foreground-primary m-0">
|
<h1 class="text-2xl font-semibold text-white m-0">
|
||||||
{{ $t('credits.topUp.addMoreCredits') }}
|
{{
|
||||||
|
isInsufficientCredits
|
||||||
|
? $t('credits.topUp.addMoreCreditsToRun')
|
||||||
|
: $t('credits.topUp.addMoreCredits')
|
||||||
|
}}
|
||||||
</h1>
|
</h1>
|
||||||
<p class="text-sm text-foreground-secondary m-0">
|
<div v-if="isInsufficientCredits" class="flex flex-col gap-2">
|
||||||
{{ $t('credits.topUp.creditsDescription') }}
|
<p class="text-sm text-muted-foreground m-0 w-96">
|
||||||
</p>
|
{{ $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>
|
</div>
|
||||||
|
|
||||||
<!-- Current Balance Section -->
|
<!-- Current Balance Section -->
|
||||||
<div class="flex flex-col gap-4">
|
<div class="flex flex-col gap-4">
|
||||||
<div class="flex items-baseline gap-2">
|
<div class="flex items-baseline gap-2">
|
||||||
<UserCredit text-class="text-3xl font-bold" />
|
<UserCredit text-class="text-3xl font-bold" show-credits-only />
|
||||||
<span class="text-sm text-foreground-secondary">{{
|
<span class="text-sm text-muted-foreground">{{
|
||||||
$t('credits.creditsAvailable')
|
$t('credits.creditsAvailable')
|
||||||
}}</span>
|
}}</span>
|
||||||
</div>
|
</div>
|
||||||
<div v-if="refreshDate" class="text-sm text-foreground-secondary">
|
<div v-if="formattedRenewalDate" class="text-sm text-muted-foreground">
|
||||||
{{ $t('credits.refreshes', { date: refreshDate }) }}
|
{{ $t('credits.refreshes', { date: formattedRenewalDate }) }}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- Credit Options Section -->
|
<!-- Credit Options Section -->
|
||||||
<div class="flex flex-col gap-4">
|
<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') }}
|
{{ $t('credits.topUp.howManyCredits') }}
|
||||||
</span>
|
</span>
|
||||||
<div class="flex flex-col gap-2">
|
<div class="flex flex-col gap-2">
|
||||||
@@ -42,7 +50,7 @@
|
|||||||
@select="selectedCredits = option.credits"
|
@select="selectedCredits = option.credits"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
<div class="text-xs text-foreground-secondary">
|
<div class="text-xs text-muted-foreground w-96">
|
||||||
{{ $t('credits.topUp.templateNote') }}
|
{{ $t('credits.topUp.templateNote') }}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -53,7 +61,8 @@
|
|||||||
:loading="loading"
|
:loading="loading"
|
||||||
severity="primary"
|
severity="primary"
|
||||||
:label="$t('credits.topUp.buy')"
|
:label="$t('credits.topUp.buy')"
|
||||||
class="w-full"
|
:class="['w-full', { 'opacity-30': !selectedCredits || loading }]"
|
||||||
|
:pt="{ label: { class: 'text-white' } }"
|
||||||
@click="handleBuy"
|
@click="handleBuy"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
@@ -121,6 +130,7 @@ import {
|
|||||||
import UserCredit from '@/components/common/UserCredit.vue'
|
import UserCredit from '@/components/common/UserCredit.vue'
|
||||||
import { useFirebaseAuthActions } from '@/composables/auth/useFirebaseAuthActions'
|
import { useFirebaseAuthActions } from '@/composables/auth/useFirebaseAuthActions'
|
||||||
import { useFeatureFlags } from '@/composables/useFeatureFlags'
|
import { useFeatureFlags } from '@/composables/useFeatureFlags'
|
||||||
|
import { useSubscription } from '@/platform/cloud/subscription/composables/useSubscription'
|
||||||
import { useTelemetry } from '@/platform/telemetry'
|
import { useTelemetry } from '@/platform/telemetry'
|
||||||
|
|
||||||
import CreditTopUpOption from './credit/CreditTopUpOption.vue'
|
import CreditTopUpOption from './credit/CreditTopUpOption.vue'
|
||||||
@@ -132,18 +142,17 @@ interface CreditOption {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const {
|
const {
|
||||||
refreshDate,
|
|
||||||
isInsufficientCredits = false,
|
isInsufficientCredits = false,
|
||||||
amountOptions = [5, 10, 20, 50],
|
amountOptions = [5, 10, 20, 50],
|
||||||
preselectedAmountOption = 10
|
preselectedAmountOption = 10
|
||||||
} = defineProps<{
|
} = defineProps<{
|
||||||
refreshDate?: string
|
|
||||||
isInsufficientCredits?: boolean
|
isInsufficientCredits?: boolean
|
||||||
amountOptions?: number[]
|
amountOptions?: number[]
|
||||||
preselectedAmountOption?: number
|
preselectedAmountOption?: number
|
||||||
}>()
|
}>()
|
||||||
|
|
||||||
const { flags } = useFeatureFlags()
|
const { flags } = useFeatureFlags()
|
||||||
|
const { formattedRenewalDate } = useSubscription()
|
||||||
// Use feature flag to determine design - defaults to true (new design)
|
// Use feature flag to determine design - defaults to true (new design)
|
||||||
const useNewDesign = computed(() => flags.subscriptionTiersEnabled)
|
const useNewDesign = computed(() => flags.subscriptionTiersEnabled)
|
||||||
|
|
||||||
@@ -157,20 +166,20 @@ const loading = ref(false)
|
|||||||
|
|
||||||
const creditOptions: CreditOption[] = [
|
const creditOptions: CreditOption[] = [
|
||||||
{
|
{
|
||||||
credits: 1000,
|
credits: 1055, // $5.00
|
||||||
description: t('credits.topUp.videosEstimate', { count: 100 })
|
description: t('credits.topUp.videosEstimate', { count: 41 })
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
credits: 5000,
|
credits: 2110, // $10.00
|
||||||
description: t('credits.topUp.videosEstimate', { count: 500 })
|
description: t('credits.topUp.videosEstimate', { count: 82 })
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
credits: 10000,
|
credits: 4220, // $20.00
|
||||||
description: t('credits.topUp.videosEstimate', { count: 1000 })
|
description: t('credits.topUp.videosEstimate', { count: 184 })
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
credits: 20000,
|
credits: 10550, // $50.00
|
||||||
description: t('credits.topUp.videosEstimate', { count: 2000 })
|
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="flex items-center justify-between p-2 rounded-lg cursor-pointer transition-all duration-200"
|
||||||
:class="[
|
:class="[
|
||||||
selected
|
selected
|
||||||
? 'bg-surface-secondary border-2 border-primary'
|
? 'bg-secondary-background border-2 border-border-default'
|
||||||
: 'bg-surface-tertiary border border-border-primary hover:bg-surface-secondary'
|
: 'bg-component-node-disabled hover:bg-secondary-background border-2 border-transparent'
|
||||||
]"
|
]"
|
||||||
@click="$emit('select')"
|
@click="$emit('select')"
|
||||||
>
|
>
|
||||||
<div class="flex flex-col">
|
<span class="text-base font-bold text-white">
|
||||||
<span class="text-base font-medium text-foreground-primary">
|
{{ formattedCredits }}
|
||||||
{{ formattedCredits }}
|
</span>
|
||||||
</span>
|
<span class="text-sm font-normal text-white">
|
||||||
<span class="text-sm text-foreground-secondary">
|
{{ description }}
|
||||||
{{ description }}
|
</span>
|
||||||
</span>
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
@@ -38,6 +36,10 @@ defineEmits<{
|
|||||||
const { locale } = useI18n()
|
const { locale } = useI18n()
|
||||||
|
|
||||||
const formattedCredits = computed(() => {
|
const formattedCredits = computed(() => {
|
||||||
return formatCredits({ value: credits, locale: locale.value })
|
return formatCredits({
|
||||||
|
value: credits,
|
||||||
|
locale: locale.value,
|
||||||
|
numberOptions: { minimumFractionDigits: 0, maximumFractionDigits: 0 }
|
||||||
|
})
|
||||||
})
|
})
|
||||||
</script>
|
</script>
|
||||||
|
|||||||
@@ -1847,6 +1847,8 @@
|
|||||||
"seeDetails": "See details",
|
"seeDetails": "See details",
|
||||||
"topUp": "Top Up",
|
"topUp": "Top Up",
|
||||||
"addMoreCredits": "Add more credits",
|
"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.",
|
"creditsDescription": "Credits are used to run workflows or partner nodes.",
|
||||||
"howManyCredits": "How many credits would you like to add?",
|
"howManyCredits": "How many credits would you like to add?",
|
||||||
"videosEstimate": "~{count} videos*",
|
"videosEstimate": "~{count} videos*",
|
||||||
@@ -1869,7 +1871,7 @@
|
|||||||
"accountInitialized": "Account initialized",
|
"accountInitialized": "Account initialized",
|
||||||
"unified": {
|
"unified": {
|
||||||
"message": "Credits have been 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": {
|
"subscription": {
|
||||||
|
|||||||
@@ -386,11 +386,12 @@ export const useDialogService = () => {
|
|||||||
return dialogStore.showDialog({
|
return dialogStore.showDialog({
|
||||||
key: 'top-up-credits',
|
key: 'top-up-credits',
|
||||||
component: TopUpCreditsDialogContent,
|
component: TopUpCreditsDialogContent,
|
||||||
headerComponent: ComfyOrgHeader,
|
|
||||||
props: options,
|
props: options,
|
||||||
dialogComponentProps: {
|
dialogComponentProps: {
|
||||||
|
headless: true,
|
||||||
pt: {
|
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*')
|
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', () => {
|
it('applies unselected styling when not selected', () => {
|
||||||
const wrapper = mountOption({ selected: false })
|
const wrapper = mountOption({ selected: false })
|
||||||
expect(wrapper.find('div').classes()).toContain('bg-surface-tertiary')
|
expect(wrapper.find('div').classes()).toContain(
|
||||||
expect(wrapper.find('div').classes()).toContain('border-border-primary')
|
'bg-component-node-disabled'
|
||||||
|
)
|
||||||
|
expect(wrapper.find('div').classes()).toContain('border-transparent')
|
||||||
})
|
})
|
||||||
|
|
||||||
it('emits select event when clicked', async () => {
|
it('emits select event when clicked', async () => {
|
||||||
|
|||||||
Reference in New Issue
Block a user