manager: design improved related to infopanel

This commit is contained in:
Jin Yi
2026-01-23 15:04:37 +09:00
parent 1580a539ff
commit 94331f3281
13 changed files with 297 additions and 186 deletions

View File

@@ -283,6 +283,9 @@
},
"manager": {
"title": "Nodes Manager",
"basicInfo": "Basic Info",
"actions": "Actions",
"selected": "Selected",
"legacyMenuNotAvailable": "Legacy manager menu is not available, defaulting to the new manager menu.",
"legacyManagerUI": "Use Legacy UI",
"legacyManagerUIDescription": "To use the legacy Manager UI, start ComfyUI with --enable-manager-legacy-ui",

View File

@@ -41,7 +41,8 @@
}
},
overlay: {
class: 'bg-comfy-input rounded-lg mt-1 shadow-lg border border-border-default'
class:
'bg-comfy-input rounded-lg mt-1 shadow-lg border border-border-default'
},
list: { class: 'p-1' },
option: {

View File

@@ -1,7 +1,7 @@
<template>
<Button
v-tooltip.top="$t('manager.tryUpdateTooltip')"
variant="inverted"
variant="primary"
:size
:disabled="isUpdating"
@click="tryUpdate"
@@ -11,6 +11,7 @@
duration="1s"
:size="size === 'sm' ? 12 : 16"
/>
<i v-else class="icon-[lucide--refresh-cw]" />
<span>{{ isUpdating ? t('g.updating') : t('manager.tryUpdate') }}</span>
</Button>
</template>

View File

@@ -1,9 +1,6 @@
<template>
<Button
variant="destructive"
:size
@click="uninstallItems"
>
<Button variant="destructive" :size @click="uninstallItems">
<i class="icon-[lucide--trash-2]" />
{{
nodePacks.length > 1
? t('manager.uninstallSelected')

View File

@@ -10,7 +10,9 @@
>
<DotSpinner v-if="isUpdating" duration="1s" :size="12" />
<i v-else class="icon-[lucide--refresh-cw]" />
<span>{{ $t('manager.updateAll') }}</span>
<span>{{
nodePacks.length > 1 ? $t('manager.updateAll') : $t('manager.update')
}}</span>
</Button>
</template>

View File

@@ -1,55 +1,110 @@
<template>
<template v-if="nodePack">
<div class="flex h-full flex-col overflow-hidden">
<div class="w-full px-6 pt-6">
<InfoPanelHeader
:node-packs="[nodePack]"
:has-conflict="hasCompatibilityIssues"
/>
</div>
<div
ref="scrollContainer"
class="scrollbar-hide flex-1 flex flex-col p-6 pt-2 text-sm"
>
<div class="mb-6">
<MetadataRow
v-if="!importFailed && isPackInstalled(nodePack.id)"
:label="t('manager.filter.enabled')"
class="flex"
style="align-items: center"
>
<PackEnableToggle
:node-pack="nodePack"
:has-conflict="hasCompatibilityIssues"
<div
ref="scrollContainer"
class="flex h-full flex-col overflow-y-auto scrollbar-custom"
>
<PropertiesAccordionItem v-if="!importFailed" :class="accordionClass">
<template #label>
<span class="text-xs uppercase font-inter">
{{ t('manager.actions') }}
</span>
</template>
<div class="flex flex-col gap-1 px-4">
<template v-if="canTryNightlyUpdate">
<PackTryUpdateButton :node-pack="nodePack" size="md" />
<PackUninstallButton :node-packs="[nodePack]" size="md" />
</template>
<template v-else-if="isUpdateAvailable">
<PackUpdateButton :node-packs="[nodePack]" size="md" />
<PackUninstallButton :node-packs="[nodePack]" size="md" />
</template>
<template v-else-if="isAllInstalled">
<PackUninstallButton :node-packs="[nodePack]" size="md" />
</template>
<template v-else>
<PackInstallButton
:node-packs="[nodePack]"
size="md"
:has-conflict="hasCompatibilityIssues || hasConflictInfo"
:conflict-info="conflictInfo"
/>
</MetadataRow>
<MetadataRow
v-for="item in infoItems"
v-show="item.value !== undefined && item.value !== null"
:key="item.key"
:label="item.label"
:value="item.value"
/>
<MetadataRow :label="t('g.status')">
<PackStatusMessage
:status-type="
nodePack.status as components['schemas']['NodeVersionStatus']
"
:has-compatibility-issues="hasCompatibilityIssues"
/>
</MetadataRow>
<MetadataRow :label="t('manager.version')">
<PackVersionBadge :node-pack="nodePack" :is-selected="true" />
</MetadataRow>
</template>
</div>
<div class="mb-6 flex-1 overflow-hidden">
<InfoTabs
</PropertiesAccordionItem>
<PropertiesAccordionItem :class="accordionClass">
<template #label>
<span class="text-xs uppercase font-inter">
{{ t('manager.basicInfo') }}
</span>
</template>
<ModelInfoField :label="t('g.name')">
<span class="text-muted-foreground">{{ nodePack.name }}</span>
</ModelInfoField>
<ModelInfoField
v-if="!importFailed && isPackInstalled(nodePack.id)"
:label="t('manager.filter.enabled')"
>
<PackEnableToggle
:node-pack="nodePack"
:has-compatibility-issues="hasCompatibilityIssues"
:conflict-result="conflictResult"
:has-conflict="hasCompatibilityIssues"
/>
</ModelInfoField>
<ModelInfoField
v-for="item in infoItems"
v-show="item.value !== undefined && item.value !== null"
:key="item.key"
:label="item.label"
>
<span class="text-muted-foreground">{{ item.value }}</span>
</ModelInfoField>
<ModelInfoField :label="t('g.status')">
<PackStatusMessage
:status-type="
nodePack.status as components['schemas']['NodeVersionStatus']
"
:has-compatibility-issues="hasCompatibilityIssues"
/>
</ModelInfoField>
<ModelInfoField :label="t('manager.version')">
<PackVersionBadge :node-pack="nodePack" :is-selected="true" />
</ModelInfoField>
</PropertiesAccordionItem>
<PropertiesAccordionItem :class="accordionClass">
<template #label>
<span class="text-xs uppercase font-inter">
{{ t('g.description') }}
</span>
</template>
<DescriptionTabPanel :node-pack="nodePack" />
</PropertiesAccordionItem>
<PropertiesAccordionItem
v-if="hasCompatibilityIssues"
:class="accordionClass"
>
<template #label>
<span class="text-xs uppercase font-inter">
{{ importFailed ? t('g.error') : t('g.warning') }}
</span>
</template>
<div class="px-4 py-2">
<WarningTabPanel :conflict-result="conflictResult" />
</div>
</div>
</PropertiesAccordionItem>
<PropertiesAccordionItem :class="accordionClass">
<template #label>
<span class="text-xs uppercase font-inter">
{{ t('g.nodes') }}
</span>
</template>
<div class="px-4 py-2">
<NodesTabPanel :node-pack="nodePack" :node-names="nodeNames" />
</div>
</PropertiesAccordionItem>
</div>
</template>
<template v-else>
@@ -60,23 +115,34 @@
</template>
<script setup lang="ts">
import { useScroll, whenever } from '@vueuse/core'
import { computed, provide, ref } from 'vue'
import { whenever } from '@vueuse/core'
import { computed, provide, ref, watch } from 'vue'
import { useI18n } from 'vue-i18n'
import PropertiesAccordionItem from '@/components/rightSidePanel/layout/PropertiesAccordionItem.vue'
import ModelInfoField from '@/platform/assets/components/modelInfo/ModelInfoField.vue'
import type { components } from '@/types/comfyRegistryTypes'
import { cn } from '@/utils/tailwindUtil'
import PackStatusMessage from '@/workbench/extensions/manager/components/manager/PackStatusMessage.vue'
import PackVersionBadge from '@/workbench/extensions/manager/components/manager/PackVersionBadge.vue'
import PackEnableToggle from '@/workbench/extensions/manager/components/manager/button/PackEnableToggle.vue'
import InfoPanelHeader from '@/workbench/extensions/manager/components/manager/infoPanel/InfoPanelHeader.vue'
import InfoTabs from '@/workbench/extensions/manager/components/manager/infoPanel/InfoTabs.vue'
import MetadataRow from '@/workbench/extensions/manager/components/manager/infoPanel/MetadataRow.vue'
import PackInstallButton from '@/workbench/extensions/manager/components/manager/button/PackInstallButton.vue'
import PackTryUpdateButton from '@/workbench/extensions/manager/components/manager/button/PackTryUpdateButton.vue'
import PackUninstallButton from '@/workbench/extensions/manager/components/manager/button/PackUninstallButton.vue'
import PackUpdateButton from '@/workbench/extensions/manager/components/manager/button/PackUpdateButton.vue'
import DescriptionTabPanel from '@/workbench/extensions/manager/components/manager/infoPanel/tabs/DescriptionTabPanel.vue'
import NodesTabPanel from '@/workbench/extensions/manager/components/manager/infoPanel/tabs/NodesTabPanel.vue'
import WarningTabPanel from '@/workbench/extensions/manager/components/manager/infoPanel/tabs/WarningTabPanel.vue'
import { usePackUpdateStatus } from '@/workbench/extensions/manager/composables/nodePack/usePackUpdateStatus'
import { useConflictDetection } from '@/workbench/extensions/manager/composables/useConflictDetection'
import { useImportFailedDetection } from '@/workbench/extensions/manager/composables/useImportFailedDetection'
import { useComfyManagerStore } from '@/workbench/extensions/manager/stores/comfyManagerStore'
import { useConflictDetectionStore } from '@/workbench/extensions/manager/stores/conflictDetectionStore'
import { IsInstallingKey } from '@/workbench/extensions/manager/types/comfyManagerTypes'
import type { ConflictDetectionResult } from '@/workbench/extensions/manager/types/conflictDetectionTypes'
import type {
ConflictDetail,
ConflictDetectionResult
} from '@/workbench/extensions/manager/types/conflictDetectionTypes'
import { ImportFailedKey } from '@/workbench/extensions/manager/types/importFailedTypes'
interface InfoItem {
@@ -91,7 +157,12 @@ const { nodePack } = defineProps<{
const scrollContainer = ref<HTMLElement | null>(null)
const { isPackInstalled } = useComfyManagerStore()
const accordionClass = cn(
'bg-modal-panel-background border-t border-border-default'
)
const managerStore = useComfyManagerStore()
const { isPackInstalled } = managerStore
const isInstalled = computed(() => isPackInstalled(nodePack.id))
const isInstalling = ref(false)
provide(IsInstallingKey, isInstalling)
@@ -99,7 +170,27 @@ whenever(isInstalled, () => {
isInstalling.value = false
})
const { canTryNightlyUpdate, isUpdateAvailable } = usePackUpdateStatus(
() => nodePack
)
const isAllInstalled = ref(false)
watch(
() => managerStore.installedPacks,
() => {
isAllInstalled.value = isPackInstalled(nodePack.id)
},
{ immediate: true }
)
const { checkNodeCompatibility } = useConflictDetection()
const conflictInfo = computed<ConflictDetail[]>(() => {
const compatibility = checkNodeCompatibility(nodePack)
return compatibility.conflicts ?? []
})
const hasConflictInfo = computed(() => conflictInfo.value.length > 0)
const { getConflictsForPackageByID } = useConflictDetectionStore()
const { t, d, n } = useI18n()
@@ -140,6 +231,12 @@ provide(ImportFailedKey, {
showImportFailedDialog
})
const nodeNames = computed(() => {
// @ts-expect-error comfy_nodes is an Algolia-specific field
const { comfy_nodes } = nodePack
return comfy_nodes ?? []
})
const infoItems = computed<InfoItem[]>(() => [
{
key: 'publisher',
@@ -162,20 +259,11 @@ const infoItems = computed<InfoItem[]>(() => [
}
])
const { y } = useScroll(scrollContainer, {
eventListenerOptions: {
passive: true
}
})
const onNodePackChange = () => {
y.value = 0
}
whenever(
() => nodePack.id,
(nodePackId, oldNodePackId) => {
if (nodePackId !== oldNodePackId) {
onNodePackChange()
if (nodePackId !== oldNodePackId && scrollContainer.value) {
scrollContainer.value.scrollTop = 0
}
},
{ immediate: true }

View File

@@ -1,6 +1,5 @@
<template>
<div v-if="nodePacks?.length" class="flex flex-col items-center">
<PackIcon :node-pack="nodePacks[0]" width="204" height="106" />
<p class="text-center text-base font-bold">{{ nodePacks[0].name }}</p>
<div v-if="!importFailed" class="flex justify-center gap-2">
<template v-if="canTryNightlyUpdate">
@@ -8,7 +7,11 @@
<PackUninstallButton :node-packs="nodePacks" size="md" />
</template>
<template v-else-if="isAllInstalled">
<PackUninstallButton v-bind="$attrs" size="md" :node-packs="nodePacks" />
<PackUninstallButton
v-bind="$attrs"
size="md"
:node-packs="nodePacks"
/>
</template>
<template v-else>
<PackInstallButton
@@ -37,7 +40,6 @@ import type { components } from '@/types/comfyRegistryTypes'
import PackInstallButton from '@/workbench/extensions/manager/components/manager/button/PackInstallButton.vue'
import PackTryUpdateButton from '@/workbench/extensions/manager/components/manager/button/PackTryUpdateButton.vue'
import PackUninstallButton from '@/workbench/extensions/manager/components/manager/button/PackUninstallButton.vue'
import PackIcon from '@/workbench/extensions/manager/components/manager/packIcon/PackIcon.vue'
import { usePackUpdateStatus } from '@/workbench/extensions/manager/composables/nodePack/usePackUpdateStatus'
import { useConflictDetection } from '@/workbench/extensions/manager/composables/useConflictDetection'
import { useComfyManagerStore } from '@/workbench/extensions/manager/stores/comfyManagerStore'

View File

@@ -1,64 +1,67 @@
<template>
<div v-if="nodePacks?.length" class="flex h-full flex-col">
<div class="flex-1 overflow-auto p-6">
<InfoPanelHeader :node-packs>
<template #thumbnail>
<PackIconStacked :node-packs="nodePacks" />
</template>
<template #title>
<div class="mt-5">
<span class="mr-2 inline-block text-base text-blue-500">{{
nodePacks.length
}}</span>
<span class="text-base">{{ $t('manager.packsSelected') }}</span>
</div>
</template>
<template #install-button>
<!-- Mixed: Don't show any button -->
<div v-if="isMixed" class="text-sm text-neutral-500">
{{ $t('manager.mixedSelectionMessage') }}
</div>
<!-- All installed: Show update (if nightly) and uninstall buttons -->
<div
v-else-if="isAllInstalled"
class="flex w-full justify-center gap-2"
>
<Button
v-if="hasNightlyPacks"
v-tooltip.top="$t('manager.tryUpdateTooltip')"
variant="textonly"
size="md"
:disabled="isUpdatingSelected"
@click="updateSelectedNightlyPacks"
>
<DotSpinner v-if="isUpdatingSelected" duration="1s" :size="16" />
<span>{{ updateSelectedLabel }}</span>
</Button>
<PackUninstallButton size="md" :node-packs="installedPacks" />
</div>
<!-- None installed: Show install button -->
<PackInstallButton
v-else-if="isNoneInstalled"
<div
v-if="nodePacks?.length"
class="flex h-full flex-col overflow-y-auto scrollbar-custom"
>
<PropertiesAccordionItem :class="accordionClass">
<template #label>
<span class="text-xs uppercase font-inter">
{{ t('manager.actions') }}
</span>
</template>
<div class="flex justify-center gap-2 px-4 py-2">
<!-- Mixed: Don't show any button -->
<div v-if="isMixed" class="text-sm text-neutral-500">
{{ $t('manager.mixedSelectionMessage') }}
</div>
<!-- All installed: Show update (if nightly) and uninstall buttons -->
<template v-else-if="isAllInstalled">
<Button
v-if="hasNightlyPacks"
v-tooltip.top="$t('manager.tryUpdateTooltip')"
variant="textonly"
size="md"
:node-packs="notInstalledPacks"
:has-conflict="hasConflicts"
:conflict-info="conflictInfo"
/>
:disabled="isUpdatingSelected"
@click="updateSelectedNightlyPacks"
>
<DotSpinner v-if="isUpdatingSelected" duration="1s" :size="16" />
<span>{{ updateSelectedLabel }}</span>
</Button>
<PackUninstallButton size="md" :node-packs="installedPacks" />
</template>
</InfoPanelHeader>
<div class="mb-6">
<MetadataRow :label="$t('g.status')">
<PackStatusMessage
:status-type="overallStatus"
:has-compatibility-issues="hasConflicts"
/>
</MetadataRow>
<MetadataRow
:label="$t('manager.totalNodes')"
:value="totalNodesCount"
<!-- None installed: Show install button -->
<PackInstallButton
v-else-if="isNoneInstalled"
size="md"
:node-packs="notInstalledPacks"
:has-conflict="hasConflicts"
:conflict-info="conflictInfo"
/>
</div>
</div>
</PropertiesAccordionItem>
<PropertiesAccordionItem :class="accordionClass">
<template #label>
<span class="text-xs uppercase font-inter">
{{ t('manager.basicInfo') }}
</span>
</template>
<ModelInfoField :label="t('manager.selected')">
<span>
<span class="font-bold text-blue-500">{{ nodePacks.length }}</span>
{{ t('manager.packsSelected') }}
</span>
</ModelInfoField>
<ModelInfoField :label="t('g.status')">
<PackStatusMessage
:status-type="overallStatus"
:has-compatibility-issues="hasConflicts"
/>
</ModelInfoField>
<ModelInfoField :label="t('manager.totalNodes')">
<span class="text-muted-foreground">{{ totalNodesCount }}</span>
</ModelInfoField>
</PropertiesAccordionItem>
</div>
<div v-else class="mx-8 mt-4 flex-1 overflow-hidden text-sm">
{{ $t('manager.infoPanelEmpty') }}
@@ -71,15 +74,15 @@ import { computed, onUnmounted, provide, ref, toRef } from 'vue'
import { useI18n } from 'vue-i18n'
import DotSpinner from '@/components/common/DotSpinner.vue'
import PropertiesAccordionItem from '@/components/rightSidePanel/layout/PropertiesAccordionItem.vue'
import Button from '@/components/ui/button/Button.vue'
import ModelInfoField from '@/platform/assets/components/modelInfo/ModelInfoField.vue'
import { useComfyRegistryStore } from '@/stores/comfyRegistryStore'
import type { components } from '@/types/comfyRegistryTypes'
import { cn } from '@/utils/tailwindUtil'
import PackStatusMessage from '@/workbench/extensions/manager/components/manager/PackStatusMessage.vue'
import PackInstallButton from '@/workbench/extensions/manager/components/manager/button/PackInstallButton.vue'
import PackUninstallButton from '@/workbench/extensions/manager/components/manager/button/PackUninstallButton.vue'
import InfoPanelHeader from '@/workbench/extensions/manager/components/manager/infoPanel/InfoPanelHeader.vue'
import MetadataRow from '@/workbench/extensions/manager/components/manager/infoPanel/MetadataRow.vue'
import PackIconStacked from '@/workbench/extensions/manager/components/manager/packIcon/PackIconStacked.vue'
import { usePacksSelection } from '@/workbench/extensions/manager/composables/nodePack/usePacksSelection'
import { usePacksStatus } from '@/workbench/extensions/manager/composables/nodePack/usePacksStatus'
import { useConflictDetection } from '@/workbench/extensions/manager/composables/useConflictDetection'
@@ -92,6 +95,11 @@ const { nodePacks } = defineProps<{
}>()
const { t } = useI18n()
const accordionClass = cn(
'bg-modal-panel-background border-t border-border-default'
)
const managerStore = useComfyManagerStore()
const nodePacksRef = toRef(() => nodePacks)

View File

@@ -10,9 +10,8 @@
:href="section.text"
target="_blank"
rel="noopener noreferrer"
class="flex items-center gap-2"
>
<i v-if="isGitHubLink(section.text)" class="pi pi-github text-base" />
<i v-if="isGitHubLink(section.text)" class="pi pi-github text-base pr-1 pb-1" />
<span class="break-all">{{ section.text }}</span>
</a>
<MarkdownText v-else :text="section.text" class="text-muted" />

View File

@@ -1,24 +1,57 @@
<template>
<div class="overflow-hidden">
<InfoTextSection
v-if="nodePack?.description"
:sections="descriptionSections"
/>
<p v-else class="text-sm text-muted italic">
{{ $t('manager.noDescription') }}
</p>
<div v-if="nodePack?.latest_version?.dependencies?.length">
<p class="mb-1">
{{ $t('manager.dependencies') }}
</p>
<div>
<ModelInfoField :label="t('g.description')">
<MarkdownText
v-if="nodePack.description"
:text="nodePack.description"
class="text-muted-foreground"
/>
<span v-else class="text-muted-foreground italic">
{{ t('manager.noDescription') }}
</span>
</ModelInfoField>
<ModelInfoField v-if="nodePack.repository" :label="t('manager.repository')">
<a
:href="nodePack.repository"
target="_blank"
rel="noopener noreferrer"
class="inline-flex items-center gap-1.5 text-muted-foreground no-underline transition-colors hover:text-foreground"
>
<i
v-if="isGitHubLink(nodePack.repository)"
class="pi pi-github text-base"
/>
<span class="break-all">{{ nodePack.repository }}</span>
<i class="icon-[lucide--external-link] size-4 shrink-0" />
</a>
</ModelInfoField>
<ModelInfoField v-if="licenseInfo" :label="t('manager.license')">
<a
v-if="licenseInfo.isUrl"
:href="licenseInfo.text"
target="_blank"
rel="noopener noreferrer"
class="inline-flex items-center gap-1.5 text-muted-foreground no-underline transition-colors hover:text-foreground"
>
<span class="break-all">{{ licenseInfo.text }}</span>
<i class="icon-[lucide--external-link] size-4 shrink-0" />
</a>
<span v-else class="text-muted-foreground break-all">
{{ licenseInfo.text }}
</span>
</ModelInfoField>
<ModelInfoField
v-if="nodePack.latest_version?.dependencies?.length"
:label="t('manager.dependencies')"
>
<div
v-for="(dep, index) in nodePack.latest_version.dependencies"
:key="index"
class="break-words text-muted"
class="break-words text-muted-foreground"
>
{{ dep }}
</div>
</div>
</ModelInfoField>
</div>
</template>
@@ -26,10 +59,10 @@
import { computed } from 'vue'
import { useI18n } from 'vue-i18n'
import ModelInfoField from '@/platform/assets/components/modelInfo/ModelInfoField.vue'
import type { components } from '@/types/comfyRegistryTypes'
import { isValidUrl } from '@/utils/formatUtil'
import InfoTextSection from '@/workbench/extensions/manager/components/manager/infoPanel/InfoTextSection.vue'
import type { TextSection } from '@/workbench/extensions/manager/components/manager/infoPanel/InfoTextSection.vue'
import MarkdownText from '@/workbench/extensions/manager/components/manager/infoPanel/MarkdownText.vue'
const { t } = useI18n()
@@ -37,6 +70,8 @@ const { nodePack } = defineProps<{
nodePack: components['schemas']['Node']
}>()
const isGitHubLink = (url: string): boolean => url.includes('github.com')
const isLicenseFile = (filename: string): boolean => {
// Match LICENSE, LICENSE.md, LICENSE.txt (case insensitive)
const licensePattern = /^license(\.md|\.txt)?$/i
@@ -118,33 +153,8 @@ const formatLicense = (
}
}
const descriptionSections = computed<TextSection[]>(() => {
const sections: TextSection[] = [
{
title: t('g.description'),
text: nodePack.description || t('manager.noDescription')
}
]
if (nodePack.repository) {
sections.push({
title: t('manager.repository'),
text: nodePack.repository,
isUrl: isValidUrl(nodePack.repository)
})
}
if (nodePack.license) {
const licenseInfo = formatLicense(nodePack.license)
if (licenseInfo && licenseInfo.text) {
sections.push({
title: t('manager.license'),
text: licenseInfo.text,
isUrl: licenseInfo.isUrl
})
}
}
return sections
const licenseInfo = computed(() => {
if (!nodePack.license) return null
return formatLicense(nodePack.license)
})
</script>

View File

@@ -3,15 +3,12 @@
<div
v-for="(conflict, index) in conflictResult?.conflicts || []"
:key="index"
class="rounded-md bg-secondary-background/60 px-2 py-1"
class="rounded-md bg-secondary-background/60"
>
<!-- Import failed conflicts show detailed error message -->
<template v-if="conflict.type === 'import_failed'">
<div
v-if="conflict.required_value"
class="overflow-x-hidden rounded px-2"
>
<p class="text-xs text-muted-foreground break-all font-mono">
<div v-if="conflict.required_value" class="overflow-x-hidden rounded">
<p class="m-0 text-xs text-muted-foreground break-all font-mono">
{{ conflict.required_value }}
</p>
</div>

View File

@@ -19,7 +19,9 @@
<div class="flex flex-1 flex-col rounded-lg min-h-0">
<div class="h-full w-full py-2 px-3">
<div class="flex h-full w-full flex-col gap-y-1">
<span class="truncate overflow-hidden text-xs font-bold text-ellipsis">
<span
class="truncate overflow-hidden text-xs font-bold text-ellipsis"
>
{{ nodePack.name }}
</span>
<p

View File

@@ -6,6 +6,7 @@
<i class="pi pi-download text-muted"></i>
<span>{{ formattedDownloads }}</span>
</div>
<div v-else></div>
<PackInstallButton
v-if="!isInstalled"
:node-packs="[nodePack]"