feat: widget styles for V3 UI (#5320)

* feat: widget input text style

* feat: widget select button style

* feat: the selection style of LGraphNode

* feat(V3 UI style): color picker + file upload + input text + multi select + select + select button + slider + textarea + tree select

* feat: placeholder

* fix: filter multi select options

* fix: direct binding, no transform for select button widget
This commit is contained in:
Rizumu Ayaka
2025-09-05 01:54:26 +08:00
committed by GitHub
parent f83801e998
commit 1dbbf20124
13 changed files with 205 additions and 61 deletions

View File

@@ -10,7 +10,8 @@
cn(
'bg-white dark-theme:bg-[#15161A]',
'min-w-[445px]',
'lg-node absolute border-2 border-solid rounded-2xl',
'lg-node absolute border border-solid rounded-2xl',
'outline outline-transparent outline-2 hover:outline-black dark-theme:hover:outline-white',
{
'border-blue-500 ring-2 ring-blue-300': selected,
'border-[#e1ded5] dark-theme:border-[#292A30]': !selected,
@@ -19,8 +20,7 @@
'border-red-500 bg-red-50': error,
'will-change-transform': isDragging
},
lodCssClass,
'hover:border-green-500'
lodCssClass
)
"
:style="[

View File

@@ -1,17 +1,24 @@
<!-- Needs custom color picker for alpha support -->
<template>
<div class="flex flex-col gap-1">
<label v-if="widget.name" class="text-sm opacity-80">{{
widget.name
}}</label>
<ColorPicker
v-model="localValue"
v-bind="filteredProps"
:disabled="readonly"
inline
@update:model-value="onChange"
/>
</div>
<WidgetLayoutField :widget="widget">
<label
:class="
cn(WidgetInputBaseClass, 'flex items-center gap-2 w-full px-4 py-2')
"
>
<ColorPicker
v-model="localValue"
v-bind="filteredProps"
:disabled="readonly"
class="w-8 h-4 !rounded-full overflow-hidden border-none"
:pt="{
preview: '!w-full !h-full !border-none'
}"
@update:model-value="onChange"
/>
<span class="text-xs">#{{ localValue }}</span>
</label>
</WidgetLayoutField>
</template>
<script setup lang="ts">
@@ -20,11 +27,15 @@ import { computed } from 'vue'
import { useWidgetValue } from '@/composables/graph/useWidgetValue'
import type { SimplifiedWidget } from '@/types/simplifiedWidget'
import { cn } from '@/utils/tailwindUtil'
import {
PANEL_EXCLUDED_PROPS,
filterWidgetProps
} from '@/utils/widgetPropFilter'
import { WidgetInputBaseClass } from './layout'
import WidgetLayoutField from './layout/WidgetLayoutField.vue'
const props = defineProps<{
widget: SimplifiedWidget<string>
modelValue: string

View File

@@ -157,6 +157,7 @@
<Button
label="Browse Files"
size="small"
severity="secondary"
class="text-xs"
:disabled="readonly"
@click="triggerFileInput"

View File

@@ -4,7 +4,7 @@
v-model="localValue"
v-bind="filteredProps"
:disabled="readonly"
class="w-full text-xs"
:class="cn(WidgetInputBaseClass, 'w-full text-xs py-2 px-4')"
size="small"
@update:model-value="onChange"
/>
@@ -17,11 +17,13 @@ import { computed } from 'vue'
import { useStringWidgetValue } from '@/composables/graph/useWidgetValue'
import type { SimplifiedWidget } from '@/types/simplifiedWidget'
import { cn } from '@/utils/tailwindUtil'
import {
INPUT_EXCLUDED_PROPS,
filterWidgetProps
} from '@/utils/widgetPropFilter'
import { WidgetInputBaseClass } from './layout'
import WidgetLayoutField from './layout/WidgetLayoutField.vue'
const props = defineProps<{

View File

@@ -4,8 +4,9 @@
v-model="localValue"
v-bind="filteredProps"
:disabled="readonly"
class="w-full text-xs"
:class="cn(WidgetInputBaseClass, 'w-full text-xs')"
size="small"
display="chip"
:pt="{
option: 'text-xs'
}"
@@ -20,11 +21,13 @@ import { computed } from 'vue'
import { useWidgetValue } from '@/composables/graph/useWidgetValue'
import type { SimplifiedWidget } from '@/types/simplifiedWidget'
import { cn } from '@/utils/tailwindUtil'
import {
PANEL_EXCLUDED_PROPS,
filterWidgetProps
} from '@/utils/widgetPropFilter'
import { WidgetInputBaseClass } from './layout'
import WidgetLayoutField from './layout/WidgetLayoutField.vue'
const props = defineProps<{
@@ -51,7 +54,18 @@ const MULTISELECT_EXCLUDED_PROPS = [
'overlayStyle'
] as const
const filteredProps = computed(() =>
filterWidgetProps(props.widget.options, MULTISELECT_EXCLUDED_PROPS)
)
const filteredProps = computed(() => {
const filtered = filterWidgetProps(
props.widget.options,
MULTISELECT_EXCLUDED_PROPS
)
// Ensure options array is available for MultiSelect
const values = props.widget.options?.values
if (values && Array.isArray(values)) {
filtered.options = values
}
return filtered
})
</script>

View File

@@ -5,7 +5,7 @@
:options="selectOptions"
v-bind="filteredProps"
:disabled="readonly"
class="w-full text-xs bg-[#F9F8F4] dark-theme:bg-[#0E0E12] border-[#E1DED5] dark-theme:border-[#15161C] !rounded-lg"
:class="cn(WidgetInputBaseClass, 'w-full text-xs')"
size="small"
:pt="{
option: 'text-xs'
@@ -21,11 +21,13 @@ import { computed } from 'vue'
import { useWidgetValue } from '@/composables/graph/useWidgetValue'
import type { SimplifiedWidget } from '@/types/simplifiedWidget'
import { cn } from '@/utils/tailwindUtil'
import {
PANEL_EXCLUDED_PROPS,
filterWidgetProps
} from '@/utils/widgetPropFilter'
import { WidgetInputBaseClass } from './layout'
import WidgetLayoutField from './layout/WidgetLayoutField.vue'
const props = defineProps<{

View File

@@ -1,62 +1,36 @@
<template>
<WidgetLayoutField :widget="widget">
<SelectButton
<FormSelectButton
v-model="localValue"
v-bind="filteredProps"
:options="widget.options?.values || []"
:disabled="readonly"
class="w-full text-xs"
:pt="{
pcToggleButton: {
label: 'text-xs'
}
}"
class="w-full"
@update:model-value="onChange"
/>
</WidgetLayoutField>
</template>
<script setup lang="ts">
import SelectButton from 'primevue/selectbutton'
import { computed } from 'vue'
import { useWidgetValue } from '@/composables/graph/useWidgetValue'
import { useStringWidgetValue } from '@/composables/graph/useWidgetValue'
import type { SimplifiedWidget } from '@/types/simplifiedWidget'
import {
STANDARD_EXCLUDED_PROPS,
filterWidgetProps
} from '@/utils/widgetPropFilter'
import FormSelectButton from './form/FormSelectButton.vue'
import WidgetLayoutField from './layout/WidgetLayoutField.vue'
const props = defineProps<{
widget: SimplifiedWidget<any>
modelValue: any
widget: SimplifiedWidget<string>
modelValue: string
readonly?: boolean
}>()
const emit = defineEmits<{
'update:modelValue': [value: any]
'update:modelValue': [value: string]
}>()
// Use the composable for consistent widget value handling
const { localValue, onChange } = useWidgetValue({
widget: props.widget,
modelValue: props.modelValue,
defaultValue: null,
const { localValue, onChange } = useStringWidgetValue(
props.widget,
props.modelValue,
emit
})
const filteredProps = computed(() =>
filterWidgetProps(props.widget.options, STANDARD_EXCLUDED_PROPS)
)
</script>
<style scoped>
:deep(.p-selectbutton) {
border: 1px solid transparent;
}
:deep(.p-selectbutton:hover) {
border-color: currentColor;
}
</style>

View File

@@ -1,7 +1,9 @@
<template>
<WidgetLayoutField :widget="widget">
<div
class="flex items-center gap-2 w-full rounded-lg pl-4 pr-2 bg-[#F9F8F4] dark-theme:bg-[#0E0E12] border-[#E1DED5] dark-theme:border-[#15161C] border-solid border"
:class="
cn(WidgetInputBaseClass, 'flex items-center gap-2 w-full pl-4 pr-2')
"
>
<Slider
v-model="localValue"
@@ -33,11 +35,13 @@ import { computed, ref, watch } from 'vue'
import { useNumberWidgetValue } from '@/composables/graph/useWidgetValue'
import type { SimplifiedWidget } from '@/types/simplifiedWidget'
import { cn } from '@/utils/tailwindUtil'
import {
STANDARD_EXCLUDED_PROPS,
filterWidgetProps
} from '@/utils/widgetPropFilter'
import { WidgetInputBaseClass } from './layout'
import WidgetLayoutField from './layout/WidgetLayoutField.vue'
const props = defineProps<{

View File

@@ -3,7 +3,8 @@
v-model="localValue"
v-bind="filteredProps"
:disabled="readonly"
class="w-full text-xs"
:class="cn(WidgetInputBaseClass, 'w-full text-xs')"
:placeholder="placeholder || widget.name || ''"
size="small"
rows="3"
@update:model-value="onChange"
@@ -16,15 +17,19 @@ import { computed } from 'vue'
import { useStringWidgetValue } from '@/composables/graph/useWidgetValue'
import type { SimplifiedWidget } from '@/types/simplifiedWidget'
import { cn } from '@/utils/tailwindUtil'
import {
INPUT_EXCLUDED_PROPS,
filterWidgetProps
} from '@/utils/widgetPropFilter'
import { WidgetInputBaseClass } from './layout'
const props = defineProps<{
widget: SimplifiedWidget<string>
modelValue: string
readonly?: boolean
placeholder?: string
}>()
const emit = defineEmits<{

View File

@@ -4,7 +4,7 @@
v-model="localValue"
v-bind="filteredProps"
:disabled="readonly"
class="w-full text-xs"
:class="cn(WidgetInputBaseClass, 'w-full text-xs')"
size="small"
@update:model-value="onChange"
/>
@@ -17,11 +17,13 @@ import { computed } from 'vue'
import { useWidgetValue } from '@/composables/graph/useWidgetValue'
import type { SimplifiedWidget } from '@/types/simplifiedWidget'
import { cn } from '@/utils/tailwindUtil'
import {
PANEL_EXCLUDED_PROPS,
filterWidgetProps
} from '@/utils/widgetPropFilter'
import { WidgetInputBaseClass } from './layout'
import WidgetLayoutField from './layout/WidgetLayoutField.vue'
const props = defineProps<{

View File

@@ -0,0 +1,108 @@
<template>
<div
:class="
cn(
WidgetInputBaseClass,
'p-1 inline-flex justify-center items-center gap-1'
)
"
>
<button
v-for="(option, index) in options"
:key="getOptionValue(option, index)"
:class="
cn(
'flex-1 h-6 px-5 py-[5px] rounded flex justify-center items-center gap-1 transition-all duration-150 ease-in-out',
'bg-transparent border-none',
'text-center text-xs font-normal',
{
'bg-white': isSelected(option) && !disabled,
'hover:bg-zinc-200/50': !isSelected(option) && !disabled,
'opacity-50 cursor-not-allowed': disabled,
'cursor-pointer': !disabled
},
{
'text-neutral-900': isSelected(option) && !disabled,
'text-zinc-500': !isSelected(option) || disabled
}
)
"
:disabled="disabled"
@click="handleSelect(option)"
>
{{ getOptionLabel(option) }}
</button>
</div>
</template>
<script
setup
lang="ts"
generic="T extends string | number | { label: string; value: any }"
>
import { cn } from '@/utils/tailwindUtil'
import { WidgetInputBaseClass } from '../layout'
interface Props {
modelValue: string | null | undefined
options: T[] // Now using generic type instead of any[]
optionLabel?: string // PrimeVue compatible prop
optionValue?: string // PrimeVue compatible prop
disabled?: boolean
}
interface Emits {
'update:modelValue': [value: string]
}
const props = withDefaults(defineProps<Props>(), {
disabled: false
})
const emit = defineEmits<Emits>()
// handle both string/number arrays and object arrays with PrimeVue compatibility
const getOptionValue = (option: T, index: number): string => {
if (typeof option === 'object' && option !== null) {
// Use PrimeVue optionValue prop if provided, otherwise fallback to common fields
const valueField = props.optionValue ?? 'value'
const value =
(option as any)[valueField] ??
(option as any).value ??
(option as any).name ??
(option as any).label ??
index
return String(value)
}
return String(option)
}
// for display with PrimeVue compatibility
const getOptionLabel = (option: T): string => {
if (typeof option === 'object' && option !== null) {
// Use PrimeVue optionLabel prop if provided, otherwise fallback to common fields
const labelField = props.optionLabel ?? 'label'
return (
(option as any)[labelField] ??
(option as any).label ??
(option as any).name ??
(option as any).value ??
String(option)
)
}
return String(option)
}
const isSelected = (option: T): boolean => {
const optionValue = getOptionValue(option, props.options.indexOf(option))
return optionValue === String(props.modelValue ?? '')
}
const handleSelect = (option: T) => {
if (props.disabled) return
const optionValue = getOptionValue(option, props.options.indexOf(option))
emit('update:modelValue', optionValue)
}
</script>

View File

@@ -0,0 +1,14 @@
export const WidgetInputBaseClass = [
// Background
'bg-zinc-500/10',
// Outline
'border-none',
'outline',
'outline-1',
'outline-offset-[-1px]',
'outline-zinc-300/10',
// Rounded
'!rounded-lg',
// Hover
'hover:outline-blue-500/80'
].join(' ')

View File

@@ -23,6 +23,12 @@ const TYPE_TO_ENUM_MAP: Record<string, string> = {
// Selection
combo: WidgetType.COMBO,
COMBO: WidgetType.COMBO,
selectbutton: WidgetType.SELECTBUTTON,
SELECTBUTTON: WidgetType.SELECTBUTTON,
multiselect: WidgetType.MULTISELECT,
MULTISELECT: WidgetType.MULTISELECT,
treeselect: WidgetType.TREESELECT,
TREESELECT: WidgetType.TREESELECT,
// Boolean
toggle: WidgetType.TOGGLESWITCH,
@@ -32,6 +38,7 @@ const TYPE_TO_ENUM_MAP: Record<string, string> = {
// Multiline text
multiline: WidgetType.TEXTAREA,
textarea: WidgetType.TEXTAREA,
TEXTAREA: WidgetType.TEXTAREA,
customtext: WidgetType.TEXTAREA,
MARKDOWN: WidgetType.MARKDOWN,