mirror of
https://github.com/Comfy-Org/ComfyUI_frontend.git
synced 2026-03-01 19:20:10 +00:00
This pull request updates the design system color tokens and refactors node and widget component styles throughout the codebase to use new, more consistent CSS variables. The changes ensure that node and widget components are styled using unified design tokens, improving maintainability and theme support for both light and dark modes. **Design System Token Updates** * Added new component and node-related CSS variables for background, border, foreground, and widget states in both light and dark themes in `style.css`. [[1]](diffhunk://#diff-71b6b57a56095b04e47c797a5016149b76b27971cab04b93f033f1f846e0f5a0R246-R256) [[2]](diffhunk://#diff-71b6b57a56095b04e47c797a5016149b76b27971cab04b93f033f1f846e0f5a0R354-R364) * Introduced `--color-graphite-400` and adjusted several existing color assignments for better palette consistency. [[1]](diffhunk://#diff-71b6b57a56095b04e47c797a5016149b76b27971cab04b93f033f1f846e0f5a0R76) [[2]](diffhunk://#diff-71b6b57a56095b04e47c797a5016149b76b27971cab04b93f033f1f846e0f5a0L304-R316) * Updated semantic CSS variables to reference the new component/node tokens for easier usage in components. * Changed `--secondary-background-hover` to match `--secondary-background` for improved hover consistency. **Component Refactoring: Node and Widget Styles** * Refactored Vue component classes and inline styles to use the new CSS variables for node backgrounds, borders, and widget states, replacing legacy variables like `bg-node-component-surface` and `border-node-component-border` with `bg-component-node-background` and `border-component-node-border`. [[1]](diffhunk://#diff-a7744614cf842e54416047326db79ad81f7c7ab7bfb66ae2b46f5c73ac7d47f2L11-R14) [[2]](diffhunk://#diff-a7744614cf842e54416047326db79ad81f7c7ab7bfb66ae2b46f5c73ac7d47f2L39-R39) [[3]](diffhunk://#diff-a7744614cf842e54416047326db79ad81f7c7ab7bfb66ae2b46f5c73ac7d47f2L384-R384) [[4]](diffhunk://#diff-19537a67677431ecdc9aec43877d28814e37edf0e45b0b0b484ea08832cad299L5-R13) * Updated widget dropdowns, select, and input components to use `text-component-node-foreground-secondary` for icons and foregrounds, and new background variables for buttons and inputs. [[1]](diffhunk://#diff-489229f88dfdfd5d883a3ef7fad6effa0790a18a831d5a9d84642dfb246962a2L29-R29) [[2]](diffhunk://#diff-489229f88dfdfd5d883a3ef7fad6effa0790a18a831d5a9d84642dfb246962a2L100-R100) [[3]](diffhunk://#diff-661a09de2721335e118a693b25d09922ada0ccbd0a51284691ed784fbe18874eL13-R13) [[4]](diffhunk://#diff-2856391d03b0d38db1ed922b5034a05bc32e978c51f8175057d84cf82399d986L13-R13) [[5]](diffhunk://#diff-4ee47848821aff71b6da0a1bb7fb8976e7879d706f71ff2ab3c5b046f5ef528cL10-R10) [[6]](diffhunk://#diff-8b7ed2ce6194a262fb1e950294699cb8722630920362143a765802b602ae5fc8L106-R113) [[7]](diffhunk://#diff-8b7ed2ce6194a262fb1e950294699cb8722630920362143a765802b602ae5fc8L119-R123) [[8]](diffhunk://#diff-597a77456bf4b0c2d390fc46a930f37156b2f26ca030259b6703e5d39ff6b20eL37-R53) [[9]](diffhunk://#diff-29348fa2e5b8cec1301a99bdec241379aeefc1747cceeb0c39b7df452ca635ffL7-R7) **Service Layer Updates** * Updated the color palette service mapping to use the new CSS variable names for node and widget colors, ensuring consistency across the application. * https://github.com/user-attachments/assets/d9535f9a-b459-49bf-b2fe-ed872916fa4e These changes collectively modernize the styling approach for node and widget components, making it easier to maintain and extend theme support. --------- Co-authored-by: github-actions <github-actions@github.com>
327 lines
9.5 KiB
Vue
327 lines
9.5 KiB
Vue
<template>
|
|
<!-- Replace entire widget with image preview when image is loaded -->
|
|
<!-- Edge-to-edge: -mx-2 removes the parent's p-2 (8px) padding on each side -->
|
|
<div
|
|
v-if="hasImageFile"
|
|
class="relative -mx-2"
|
|
style="width: calc(100% + 1rem)"
|
|
>
|
|
<!-- Select section above image -->
|
|
<div class="mb-2 flex items-center justify-between gap-4 px-2">
|
|
<label
|
|
v-if="widget.name"
|
|
class="text-secondary min-w-[4em] truncate text-xs"
|
|
>{{ widget.name }}</label
|
|
>
|
|
<!-- Group select and folder button together on the right -->
|
|
<div class="flex items-center gap-1">
|
|
<!-- TODO: finish once we finish value bindings with Litegraph -->
|
|
<Select
|
|
:model-value="selectedFile?.name"
|
|
:options="[selectedFile?.name || '']"
|
|
:disabled="true"
|
|
:aria-label="`${$t('g.selectedFile')}: ${selectedFile?.name || $t('g.none')}`"
|
|
v-bind="transformCompatProps"
|
|
class="max-w-[20em] min-w-[8em] text-xs"
|
|
size="small"
|
|
:pt="{
|
|
option: 'text-xs',
|
|
dropdownIcon: 'text-component-node-foreground-secondary'
|
|
}"
|
|
/>
|
|
<Button
|
|
icon="pi pi-folder"
|
|
size="small"
|
|
class="!h-8 !w-8"
|
|
@click="triggerFileInput"
|
|
/>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Image preview -->
|
|
<!-- TODO: change hardcoded colors when design system incorporated -->
|
|
<div class="group relative">
|
|
<img :src="imageUrl" :alt="selectedFile?.name" class="h-auto w-full" />
|
|
<!-- Darkening overlay on hover -->
|
|
<div
|
|
class="bg-opacity-0 group-hover:bg-opacity-20 pointer-events-none absolute inset-0 bg-black transition-all duration-200"
|
|
/>
|
|
<!-- Control buttons in top right on hover -->
|
|
<div
|
|
class="absolute top-2 right-2 flex gap-1 opacity-0 transition-opacity duration-200 group-hover:opacity-100"
|
|
>
|
|
<!-- Edit button -->
|
|
<button
|
|
:aria-label="$t('g.editImage')"
|
|
class="flex h-6 w-6 items-center justify-center rounded border-none transition-all duration-150 focus:outline-none"
|
|
style="background-color: #262729"
|
|
@click="handleEdit"
|
|
>
|
|
<i class="pi pi-pencil text-xs text-white"></i>
|
|
</button>
|
|
<!-- Delete button -->
|
|
<button
|
|
:aria-label="$t('g.deleteImage')"
|
|
class="flex h-6 w-6 items-center justify-center rounded border-none transition-all duration-150 focus:outline-none"
|
|
style="background-color: #262729"
|
|
@click="clearFile"
|
|
>
|
|
<i class="pi pi-times text-xs text-white"></i>
|
|
</button>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Audio preview when audio file is loaded -->
|
|
<div
|
|
v-else-if="hasAudioFile"
|
|
class="relative -mx-2"
|
|
style="width: calc(100% + 1rem)"
|
|
>
|
|
<!-- Select section above audio player -->
|
|
<div class="mb-2 flex items-center justify-between gap-4 px-2">
|
|
<label
|
|
v-if="widget.name"
|
|
class="text-secondary min-w-[4em] truncate text-xs"
|
|
>{{ widget.name }}</label
|
|
>
|
|
<!-- Group select and folder button together on the right -->
|
|
<div class="flex items-center gap-1">
|
|
<Select
|
|
:model-value="selectedFile?.name"
|
|
:options="[selectedFile?.name || '']"
|
|
:disabled="true"
|
|
:aria-label="`${$t('g.selectedFile')}: ${selectedFile?.name || $t('g.none')}`"
|
|
v-bind="transformCompatProps"
|
|
class="max-w-[20em] min-w-[8em] text-xs"
|
|
size="small"
|
|
:pt="{
|
|
option: 'text-xs',
|
|
dropdownIcon: 'text-component-node-foreground-secondary'
|
|
}"
|
|
/>
|
|
<Button
|
|
icon="pi pi-folder"
|
|
size="small"
|
|
class="!h-8 !w-8"
|
|
@click="triggerFileInput"
|
|
/>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Audio player -->
|
|
<div class="group relative px-2">
|
|
<div
|
|
class="flex items-center gap-4 rounded-lg bg-charcoal-800 p-4"
|
|
style="border: 1px solid #262729"
|
|
>
|
|
<!-- Audio icon -->
|
|
<div class="flex-shrink-0">
|
|
<i class="pi pi-volume-up text-2xl opacity-60"></i>
|
|
</div>
|
|
|
|
<!-- File info and controls -->
|
|
<div class="flex-1">
|
|
<div class="mb-1 text-sm font-medium">{{ selectedFile?.name }}</div>
|
|
<div class="text-xs opacity-60">
|
|
{{
|
|
selectedFile ? (selectedFile.size / 1024).toFixed(1) + ' KB' : ''
|
|
}}
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Control buttons -->
|
|
<div class="flex gap-1">
|
|
<!-- Delete button -->
|
|
<button
|
|
:aria-label="$t('g.deleteAudioFile')"
|
|
class="flex h-8 w-8 items-center justify-center rounded border-none transition-all duration-150 hover:bg-charcoal-600 focus:outline-none"
|
|
@click="clearFile"
|
|
>
|
|
<i class="pi pi-times text-sm text-white"></i>
|
|
</button>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Show normal file upload UI when no image or audio is loaded -->
|
|
<div
|
|
v-else
|
|
class="flex w-full flex-col gap-1 rounded-lg border border-solid p-1"
|
|
:style="{ borderColor: '#262729' }"
|
|
>
|
|
<div
|
|
class="rounded-md border border-dashed p-1 transition-colors duration-200 hover:border-slate-300"
|
|
:style="{ borderColor: '#262729' }"
|
|
>
|
|
<div class="flex w-full flex-col items-center gap-2 py-4">
|
|
<span class="text-xs opacity-60"> {{ $t('Drop your file or') }} </span>
|
|
<div>
|
|
<Button
|
|
label="Browse Files"
|
|
size="small"
|
|
severity="secondary"
|
|
class="text-xs"
|
|
@click="triggerFileInput"
|
|
/>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
<!-- Hidden file input always available for both states -->
|
|
<input
|
|
ref="fileInputRef"
|
|
type="file"
|
|
class="hidden"
|
|
:accept="widget.options?.accept"
|
|
:aria-label="`${$t('g.upload')} ${widget.name || $t('g.file')}`"
|
|
:multiple="false"
|
|
@change="handleFileChange"
|
|
/>
|
|
</template>
|
|
|
|
<script setup lang="ts">
|
|
import Button from 'primevue/button'
|
|
import Select from 'primevue/select'
|
|
import { computed, onUnmounted, ref, watch } from 'vue'
|
|
|
|
import { useWidgetValue } from '@/composables/graph/useWidgetValue'
|
|
import { useTransformCompatOverlayProps } from '@/composables/useTransformCompatOverlayProps'
|
|
import type { SimplifiedWidget } from '@/types/simplifiedWidget'
|
|
|
|
const { widget, modelValue } = defineProps<{
|
|
widget: SimplifiedWidget<File[] | null>
|
|
modelValue: File[] | null
|
|
}>()
|
|
|
|
const emit = defineEmits<{
|
|
'update:modelValue': [value: File[] | null]
|
|
}>()
|
|
|
|
const { localValue, onChange } = useWidgetValue({
|
|
widget,
|
|
modelValue,
|
|
defaultValue: null,
|
|
emit
|
|
})
|
|
|
|
// Transform compatibility props for overlay positioning
|
|
const transformCompatProps = useTransformCompatOverlayProps()
|
|
|
|
const fileInputRef = ref<HTMLInputElement | null>(null)
|
|
|
|
// Since we only support single file, get the first file
|
|
const selectedFile = computed(() => {
|
|
const files = localValue.value || []
|
|
return files.length > 0 ? files[0] : null
|
|
})
|
|
|
|
// Quick file type detection for testing
|
|
const detectFileType = (file: File) => {
|
|
const type = file.type?.toLowerCase() || ''
|
|
const name = file.name?.toLowerCase() || ''
|
|
|
|
if (
|
|
type.startsWith('image/') ||
|
|
name.match(/\.(jpg|jpeg|png|gif|webp|svg)$/)
|
|
) {
|
|
return 'image'
|
|
}
|
|
if (type.startsWith('video/') || name.match(/\.(mp4|webm|ogg|mov)$/)) {
|
|
return 'video'
|
|
}
|
|
if (type.startsWith('audio/') || name.match(/\.(mp3|wav|ogg|flac)$/)) {
|
|
return 'audio'
|
|
}
|
|
if (type === 'application/pdf' || name.endsWith('.pdf')) {
|
|
return 'pdf'
|
|
}
|
|
if (type.includes('zip') || name.match(/\.(zip|rar|7z|tar|gz)$/)) {
|
|
return 'archive'
|
|
}
|
|
return 'file'
|
|
}
|
|
|
|
// Check if we have an image file
|
|
const hasImageFile = computed(() => {
|
|
return selectedFile.value && detectFileType(selectedFile.value) === 'image'
|
|
})
|
|
|
|
// Check if we have an audio file
|
|
const hasAudioFile = computed(() => {
|
|
return selectedFile.value && detectFileType(selectedFile.value) === 'audio'
|
|
})
|
|
|
|
// Get image URL for preview
|
|
const imageUrl = computed(() => {
|
|
if (hasImageFile.value && selectedFile.value) {
|
|
return URL.createObjectURL(selectedFile.value)
|
|
}
|
|
return ''
|
|
})
|
|
|
|
// // Get audio URL for playback
|
|
// const audioUrl = computed(() => {
|
|
// if (hasAudioFile.value && selectedFile.value) {
|
|
// return URL.createObjectURL(selectedFile.value)
|
|
// }
|
|
// return ''
|
|
// })
|
|
|
|
// Clean up image URL when file changes
|
|
watch(imageUrl, (newUrl, oldUrl) => {
|
|
if (oldUrl && oldUrl !== newUrl) {
|
|
URL.revokeObjectURL(oldUrl)
|
|
}
|
|
})
|
|
|
|
const triggerFileInput = () => {
|
|
fileInputRef.value?.click()
|
|
}
|
|
|
|
const handleFileChange = (event: Event) => {
|
|
const target = event.target as HTMLInputElement
|
|
if (target.files && target.files.length > 0) {
|
|
// Since we only support single file, take the first one
|
|
const file = target.files[0]
|
|
|
|
// Use the composable's onChange handler with an array
|
|
onChange([file])
|
|
|
|
// Reset input to allow selecting same file again
|
|
target.value = ''
|
|
}
|
|
}
|
|
|
|
const clearFile = () => {
|
|
// Clear the file
|
|
onChange(null)
|
|
|
|
// Reset file input
|
|
if (fileInputRef.value) {
|
|
fileInputRef.value.value = ''
|
|
}
|
|
}
|
|
|
|
const handleEdit = () => {
|
|
// TODO: hook up with maskeditor
|
|
}
|
|
|
|
// Clear file input when value is cleared externally
|
|
watch(localValue, (newValue) => {
|
|
if (!newValue || newValue.length === 0) {
|
|
if (fileInputRef.value) {
|
|
fileInputRef.value.value = ''
|
|
}
|
|
}
|
|
})
|
|
|
|
// Clean up image URL on unmount
|
|
onUnmounted(() => {
|
|
if (imageUrl.value) {
|
|
URL.revokeObjectURL(imageUrl.value)
|
|
}
|
|
})
|
|
</script>
|