Compare commits

...

11 Commits

Author SHA1 Message Date
Benjamin Lu
4808018b10 remove barrel export 2025-07-02 01:41:46 -04:00
Benjamin Lu
3ad9c1bd7c "improve" typings 2025-07-01 15:13:41 -04:00
Benjamin Lu
9056a2e89c add AllowedToggleSwitchProps 2025-07-01 14:09:07 -04:00
Benjamin Lu
79b5c75b85 add interface for ToggleSwitchProps 2025-07-01 14:06:56 -04:00
Benjamin Lu
36955407e7 [feat] Add WidgetToggleSwitch Vue component
- New WidgetToggleSwitch component for boolean values
- Added export to vueWidgets index
2025-07-01 13:49:11 -04:00
bymyself
56f59103a5 [feat] Add Vue action widgets
- WidgetButton: Action button with Button component and callback handling
- WidgetFileUpload: File upload interface with FileUpload component
2025-06-24 12:33:42 -07:00
bymyself
8129ba2132 [feat] Add Vue visual widgets
- WidgetColorPicker: Color selection with ColorPicker component
- WidgetImage: Single image display with Image component
- WidgetImageCompare: Before/after comparison with ImageCompare component
- WidgetGalleria: Image gallery/carousel with Galleria component
- WidgetChart: Data visualization with Chart component
2025-06-24 12:33:27 -07:00
bymyself
346cac0889 [feat] Add Vue selection widgets
- WidgetSelect: Dropdown selection with Select component
- WidgetMultiSelect: Multiple selection with MultiSelect component
- WidgetSelectButton: Button group selection with SelectButton component
- WidgetTreeSelect: Hierarchical selection with TreeSelect component
2025-06-24 12:33:11 -07:00
bymyself
7573bca6a2 [feat] Add Vue input widgets
- WidgetInputText: Single-line text input with InputText component
- WidgetTextarea: Multi-line text input with Textarea component
- WidgetSlider: Numeric range input with Slider component
- WidgetToggleSwitch: Boolean toggle with ToggleSwitch component
2025-06-24 12:32:54 -07:00
bymyself
c25206ad3b [feat] Add Vue widget registry system
- Complete widget type enum with all 15 widget types
- Component mapping registry for dynamic widget rendering
- Helper function for type-safe widget component resolution
2025-06-24 12:32:41 -07:00
bymyself
36e4e79994 [feat] Add core Vue widget infrastructure
- SimplifiedWidget interface for Vue-based node widgets
- widgetPropFilter utility with component-specific exclusion lists
- Removes DOM manipulation and positioning concerns
- Provides clean API for value binding and prop filtering
2025-06-24 12:32:27 -07:00
19 changed files with 991 additions and 0 deletions

View File

@@ -0,0 +1,38 @@
<template>
<div class="flex flex-col gap-1">
<label v-if="widget.name" class="text-sm opacity-80">{{
widget.name
}}</label>
<Button v-bind="filteredProps" :disabled="readonly" @click="handleClick" />
</div>
</template>
<script setup lang="ts">
import Button from 'primevue/button'
import { computed } from 'vue'
import type { SimplifiedWidget } from '@/types/simplifiedWidget'
import {
BADGE_EXCLUDED_PROPS,
filterWidgetProps
} from '@/utils/widgetPropFilter'
// Button widgets don't have a v-model value, they trigger actions
const props = defineProps<{
widget: SimplifiedWidget<void>
readonly?: boolean
}>()
// Button specific excluded props
const BUTTON_EXCLUDED_PROPS = [...BADGE_EXCLUDED_PROPS, 'iconClass'] as const
const filteredProps = computed(() =>
filterWidgetProps(props.widget.options, BUTTON_EXCLUDED_PROPS)
)
const handleClick = () => {
if (!props.readonly && props.widget.callback) {
props.widget.callback()
}
}
</script>

View File

@@ -0,0 +1,65 @@
<template>
<div class="flex flex-col gap-1">
<label v-if="widget.name" class="text-sm opacity-80">{{
widget.name
}}</label>
<div class="p-4 border border-gray-300 dark-theme:border-gray-600 rounded">
<!-- Simple chart placeholder - can be enhanced with Chart.js when available -->
<div
v-if="!value || !Array.isArray(value.data)"
class="text-center text-gray-500 dark-theme:text-gray-400"
>
No chart data available
</div>
<div v-else class="space-y-2">
<div v-if="value.title" class="text-center font-semibold">
{{ value.title }}
</div>
<div class="space-y-1">
<div
v-for="(item, index) in value.data"
:key="index"
class="flex justify-between items-center"
>
<span class="text-sm">{{ item.label || `Item ${index + 1}` }}</span>
<div class="flex items-center gap-2">
<div
class="h-3 bg-blue-500 rounded"
:style="{
width: `${Math.max((item.value / maxValue) * 100, 5)}px`
}"
></div>
<span class="text-sm font-mono">{{ item.value }}</span>
</div>
</div>
</div>
</div>
</div>
</div>
</template>
<script setup lang="ts">
import { computed } from 'vue'
import type { SimplifiedWidget } from '@/types/simplifiedWidget'
interface ChartData {
title?: string
data: Array<{
label: string
value: number
}>
}
const value = defineModel<ChartData>({ required: true })
defineProps<{
widget: SimplifiedWidget<ChartData>
readonly?: boolean
}>()
const maxValue = computed(() => {
if (!value.value?.data?.length) return 1
return Math.max(...value.value.data.map((item) => item.value))
})
</script>

View File

@@ -0,0 +1,33 @@
<template>
<div class="flex flex-col gap-1">
<label v-if="widget.name" class="text-sm opacity-80">{{
widget.name
}}</label>
<ColorPicker v-model="value" v-bind="filteredProps" :disabled="readonly" />
</div>
</template>
<script setup lang="ts">
import ColorPicker from 'primevue/colorpicker'
import { computed } from 'vue'
import type { SimplifiedWidget } from '@/types/simplifiedWidget'
import {
PANEL_EXCLUDED_PROPS,
filterWidgetProps
} from '@/utils/widgetPropFilter'
const value = defineModel<string>({ required: true })
const props = defineProps<{
widget: SimplifiedWidget<string>
readonly?: boolean
}>()
// ColorPicker specific excluded props include panel/overlay classes
const COLOR_PICKER_EXCLUDED_PROPS = [...PANEL_EXCLUDED_PROPS] as const
const filteredProps = computed(() =>
filterWidgetProps(props.widget.options, COLOR_PICKER_EXCLUDED_PROPS)
)
</script>

View File

@@ -0,0 +1,66 @@
<template>
<div class="flex flex-col gap-1">
<label v-if="widget.name" class="text-sm opacity-80">{{
widget.name
}}</label>
<FileUpload
v-bind="filteredProps"
:disabled="readonly"
@upload="handleUpload"
@select="handleSelect"
@remove="handleRemove"
@clear="handleClear"
@error="handleError"
/>
</div>
</template>
<script setup lang="ts">
import FileUpload from 'primevue/fileupload'
import { computed } from 'vue'
import type { SimplifiedWidget } from '@/types/simplifiedWidget'
import {
STANDARD_EXCLUDED_PROPS,
filterWidgetProps
} from '@/utils/widgetPropFilter'
// FileUpload doesn't have a traditional v-model, it handles files through events
const props = defineProps<{
widget: SimplifiedWidget<File[] | null>
readonly?: boolean
}>()
const filteredProps = computed(() =>
filterWidgetProps(props.widget.options, STANDARD_EXCLUDED_PROPS)
)
const handleUpload = (event: any) => {
if (!props.readonly && props.widget.callback) {
props.widget.callback(event.files)
}
}
const handleSelect = (event: any) => {
if (!props.readonly && props.widget.callback) {
props.widget.callback(event.files)
}
}
const handleRemove = (event: any) => {
if (!props.readonly && props.widget.callback) {
props.widget.callback(event.files)
}
}
const handleClear = () => {
if (!props.readonly && props.widget.callback) {
props.widget.callback([])
}
}
const handleError = (event: any) => {
// Could be extended to handle error reporting
console.warn('File upload error:', event)
}
</script>

View File

@@ -0,0 +1,101 @@
<template>
<div class="flex flex-col gap-1">
<label v-if="widget.name" class="text-sm opacity-80">{{
widget.name
}}</label>
<Galleria
v-model:activeIndex="activeIndex"
:value="galleryImages"
v-bind="filteredProps"
:disabled="readonly"
:show-thumbnails="showThumbnails"
:show-indicators="showIndicators"
:show-nav-buttons="showNavButtons"
class="max-w-full"
>
<template #item="{ item }">
<img
:src="item.itemImageSrc || item.src || item"
:alt="item.alt || 'Gallery image'"
class="w-full h-auto max-h-64 object-contain"
/>
</template>
<template #thumbnail="{ item }">
<img
:src="item.thumbnailImageSrc || item.src || item"
:alt="item.alt || 'Gallery thumbnail'"
class="w-16 h-16 object-cover"
/>
</template>
</Galleria>
</div>
</template>
<script setup lang="ts">
import Galleria from 'primevue/galleria'
import { computed, ref } from 'vue'
import type { SimplifiedWidget } from '@/types/simplifiedWidget'
import {
GALLERIA_EXCLUDED_PROPS,
filterWidgetProps
} from '@/utils/widgetPropFilter'
interface GalleryImage {
itemImageSrc?: string
thumbnailImageSrc?: string
src?: string
alt?: string
}
type GalleryValue = string[] | GalleryImage[]
const value = defineModel<GalleryValue>({ required: true })
const props = defineProps<{
widget: SimplifiedWidget<GalleryValue>
readonly?: boolean
}>()
const activeIndex = ref(0)
const filteredProps = computed(() =>
filterWidgetProps(props.widget.options, GALLERIA_EXCLUDED_PROPS)
)
const galleryImages = computed(() => {
if (!value.value || !Array.isArray(value.value)) return []
return value.value.map((item, index) => {
if (typeof item === 'string') {
return {
itemImageSrc: item,
thumbnailImageSrc: item,
alt: `Image ${index + 1}`
}
}
return item
})
})
const showThumbnails = computed(() => {
return (
props.widget.options?.showThumbnails !== false &&
galleryImages.value.length > 1
)
})
const showIndicators = computed(() => {
return (
props.widget.options?.showIndicators !== false &&
galleryImages.value.length > 1
)
})
const showNavButtons = computed(() => {
return (
props.widget.options?.showNavButtons !== false &&
galleryImages.value.length > 1
)
})
</script>

View File

@@ -0,0 +1,29 @@
<template>
<div class="flex flex-col gap-1">
<label v-if="widget.name" class="text-sm opacity-80">{{
widget.name
}}</label>
<Image v-bind="filteredProps" :src="widget.value" />
</div>
</template>
<script setup lang="ts">
import Image from 'primevue/image'
import { computed } from 'vue'
import type { SimplifiedWidget } from '@/types/simplifiedWidget'
import {
IMAGE_EXCLUDED_PROPS,
filterWidgetProps
} from '@/utils/widgetPropFilter'
// Image widgets typically don't have v-model, they display a source URL/path
const props = defineProps<{
widget: SimplifiedWidget<string>
readonly?: boolean
}>()
const filteredProps = computed(() =>
filterWidgetProps(props.widget.options, IMAGE_EXCLUDED_PROPS)
)
</script>

View File

@@ -0,0 +1,172 @@
<template>
<div class="flex flex-col gap-1">
<label v-if="widget.name" class="text-sm opacity-80">{{
widget.name
}}</label>
<div
class="image-compare-container relative overflow-hidden rounded border border-gray-300 dark-theme:border-gray-600"
>
<div
v-if="!beforeImage || !afterImage"
class="p-4 text-center text-gray-500 dark-theme:text-gray-400"
>
Before and after images required
</div>
<div v-else class="relative">
<!-- After image (base layer) -->
<Image
v-bind="filteredProps"
:src="afterImage"
class="w-full h-auto"
:alt="afterAlt"
/>
<!-- Before image (overlay layer) -->
<div
class="absolute top-0 left-0 h-full overflow-hidden transition-all duration-300 ease-in-out"
:style="{ width: `${sliderPosition}%` }"
>
<Image
v-bind="filteredProps"
:src="beforeImage"
class="w-full h-auto"
:alt="beforeAlt"
/>
</div>
<!-- Slider handle -->
<div
class="absolute top-0 h-full w-0.5 bg-white shadow-lg cursor-col-resize z-10 transition-all duration-100"
:style="{ left: `${sliderPosition}%` }"
@mousedown="startDrag"
@touchstart="startDrag"
>
<div
class="absolute top-1/2 -translate-y-1/2 -translate-x-1/2 w-8 h-8 bg-white rounded-full shadow-md flex items-center justify-center"
>
<div class="w-4 h-4 flex items-center justify-center">
<div class="w-0.5 h-3 bg-gray-600 mr-0.5"></div>
<div class="w-0.5 h-3 bg-gray-600"></div>
</div>
</div>
</div>
<!-- Labels -->
<div
v-if="showLabels"
class="absolute top-2 left-2 px-2 py-1 bg-black bg-opacity-50 text-white text-xs rounded"
>
{{ beforeLabel }}
</div>
<div
v-if="showLabels"
class="absolute top-2 right-2 px-2 py-1 bg-black bg-opacity-50 text-white text-xs rounded"
>
{{ afterLabel }}
</div>
</div>
</div>
</div>
</template>
<script setup lang="ts">
import Image from 'primevue/image'
import { computed, onMounted, onUnmounted, ref } from 'vue'
import type { SimplifiedWidget } from '@/types/simplifiedWidget'
import {
IMAGE_EXCLUDED_PROPS,
filterWidgetProps
} from '@/utils/widgetPropFilter'
interface ImageCompareValue {
before: string
after: string
beforeAlt?: string
afterAlt?: string
beforeLabel?: string
afterLabel?: string
showLabels?: boolean
initialPosition?: number
}
// Image compare widgets typically don't have v-model, they display comparison
const props = defineProps<{
widget: SimplifiedWidget<ImageCompareValue>
readonly?: boolean
}>()
const sliderPosition = ref(50) // Default to 50% (middle)
const isDragging = ref(false)
const filteredProps = computed(() =>
filterWidgetProps(props.widget.options, IMAGE_EXCLUDED_PROPS)
)
const beforeImage = computed(() => {
const value = props.widget.value
return typeof value === 'string' ? value : value?.before
})
const afterImage = computed(() => {
const value = props.widget.value
return typeof value === 'string' ? value : value?.after
})
const beforeAlt = computed(
() => props.widget.value?.beforeAlt || 'Before image'
)
const afterAlt = computed(() => props.widget.value?.afterAlt || 'After image')
const beforeLabel = computed(() => props.widget.value?.beforeLabel || 'Before')
const afterLabel = computed(() => props.widget.value?.afterLabel || 'After')
const showLabels = computed(() => props.widget.value?.showLabels !== false)
onMounted(() => {
if (props.widget.value?.initialPosition !== undefined) {
sliderPosition.value = Math.max(
0,
Math.min(100, props.widget.value.initialPosition)
)
}
})
const startDrag = (event: MouseEvent | TouchEvent) => {
if (props.readonly) return
isDragging.value = true
event.preventDefault()
const handleMouseMove = (e: MouseEvent | TouchEvent) => {
if (!isDragging.value) return
updateSliderPosition(e)
}
const handleMouseUp = () => {
isDragging.value = false
document.removeEventListener('mousemove', handleMouseMove)
document.removeEventListener('mouseup', handleMouseUp)
document.removeEventListener('touchmove', handleMouseMove)
document.removeEventListener('touchend', handleMouseUp)
}
document.addEventListener('mousemove', handleMouseMove)
document.addEventListener('mouseup', handleMouseUp)
document.addEventListener('touchmove', handleMouseMove)
document.addEventListener('touchend', handleMouseUp)
}
const updateSliderPosition = (event: MouseEvent | TouchEvent) => {
const container = (event.target as HTMLElement).closest(
'.image-compare-container'
)
if (!container) return
const rect = container.getBoundingClientRect()
const clientX = 'touches' in event ? event.touches[0].clientX : event.clientX
const x = clientX - rect.left
const percentage = Math.max(0, Math.min(100, (x / rect.width) * 100))
sliderPosition.value = percentage
}
onUnmounted(() => {
isDragging.value = false
})
</script>

View File

@@ -0,0 +1,30 @@
<template>
<div class="flex flex-col gap-1">
<label v-if="widget.name" class="text-sm opacity-80">{{
widget.name
}}</label>
<InputText v-model="value" v-bind="filteredProps" :disabled="readonly" />
</div>
</template>
<script setup lang="ts">
import InputText from 'primevue/inputtext'
import { computed } from 'vue'
import type { SimplifiedWidget } from '@/types/simplifiedWidget'
import {
INPUT_EXCLUDED_PROPS,
filterWidgetProps
} from '@/utils/widgetPropFilter'
const value = defineModel<string>({ required: true })
const props = defineProps<{
widget: SimplifiedWidget<string>
readonly?: boolean
}>()
const filteredProps = computed(() =>
filterWidgetProps(props.widget.options, INPUT_EXCLUDED_PROPS)
)
</script>

View File

@@ -0,0 +1,36 @@
<template>
<div class="flex flex-col gap-1">
<label v-if="widget.name" class="text-sm opacity-80">{{
widget.name
}}</label>
<MultiSelect v-model="value" v-bind="filteredProps" :disabled="readonly" />
</div>
</template>
<script setup lang="ts">
import MultiSelect from 'primevue/multiselect'
import { computed } from 'vue'
import type { SimplifiedWidget } from '@/types/simplifiedWidget'
import {
PANEL_EXCLUDED_PROPS,
filterWidgetProps
} from '@/utils/widgetPropFilter'
const value = defineModel<any[]>({ required: true })
const props = defineProps<{
widget: SimplifiedWidget<any[]>
readonly?: boolean
}>()
// MultiSelect specific excluded props include overlay styles
const MULTISELECT_EXCLUDED_PROPS = [
...PANEL_EXCLUDED_PROPS,
'overlayStyle'
] as const
const filteredProps = computed(() =>
filterWidgetProps(props.widget.options, MULTISELECT_EXCLUDED_PROPS)
)
</script>

View File

@@ -0,0 +1,30 @@
<template>
<div class="flex flex-col gap-1">
<label v-if="widget.name" class="text-sm opacity-80">{{
widget.name
}}</label>
<Select v-model="value" v-bind="filteredProps" :disabled="readonly" />
</div>
</template>
<script setup lang="ts">
import Select from 'primevue/select'
import { computed } from 'vue'
import type { SimplifiedWidget } from '@/types/simplifiedWidget'
import {
PANEL_EXCLUDED_PROPS,
filterWidgetProps
} from '@/utils/widgetPropFilter'
const value = defineModel<any>({ required: true })
const props = defineProps<{
widget: SimplifiedWidget<any>
readonly?: boolean
}>()
const filteredProps = computed(() =>
filterWidgetProps(props.widget.options, PANEL_EXCLUDED_PROPS)
)
</script>

View File

@@ -0,0 +1,30 @@
<template>
<div class="flex flex-col gap-1">
<label v-if="widget.name" class="text-sm opacity-80">{{
widget.name
}}</label>
<SelectButton v-model="value" v-bind="filteredProps" :disabled="readonly" />
</div>
</template>
<script setup lang="ts">
import SelectButton from 'primevue/selectbutton'
import { computed } from 'vue'
import type { SimplifiedWidget } from '@/types/simplifiedWidget'
import {
STANDARD_EXCLUDED_PROPS,
filterWidgetProps
} from '@/utils/widgetPropFilter'
const value = defineModel<any>({ required: true })
const props = defineProps<{
widget: SimplifiedWidget<any>
readonly?: boolean
}>()
const filteredProps = computed(() =>
filterWidgetProps(props.widget.options, STANDARD_EXCLUDED_PROPS)
)
</script>

View File

@@ -0,0 +1,30 @@
<template>
<div class="flex flex-col gap-1">
<label v-if="widget.name" class="text-sm opacity-80">{{
widget.name
}}</label>
<Slider v-model="value" v-bind="filteredProps" :disabled="readonly" />
</div>
</template>
<script setup lang="ts">
import Slider from 'primevue/slider'
import { computed } from 'vue'
import type { SimplifiedWidget } from '@/types/simplifiedWidget'
import {
STANDARD_EXCLUDED_PROPS,
filterWidgetProps
} from '@/utils/widgetPropFilter'
const value = defineModel<number>({ required: true })
const props = defineProps<{
widget: SimplifiedWidget<number>
readonly?: boolean
}>()
const filteredProps = computed(() =>
filterWidgetProps(props.widget.options, STANDARD_EXCLUDED_PROPS)
)
</script>

View File

@@ -0,0 +1,30 @@
<template>
<div class="flex flex-col gap-1">
<label v-if="widget.name" class="text-sm opacity-80">{{
widget.name
}}</label>
<Textarea v-model="value" v-bind="filteredProps" :disabled="readonly" />
</div>
</template>
<script setup lang="ts">
import Textarea from 'primevue/textarea'
import { computed } from 'vue'
import type { SimplifiedWidget } from '@/types/simplifiedWidget'
import {
INPUT_EXCLUDED_PROPS,
filterWidgetProps
} from '@/utils/widgetPropFilter'
const value = defineModel<string>({ required: true })
const props = defineProps<{
widget: SimplifiedWidget<string>
readonly?: boolean
}>()
const filteredProps = computed(() =>
filterWidgetProps(props.widget.options, INPUT_EXCLUDED_PROPS)
)
</script>

View File

@@ -0,0 +1,51 @@
<template>
<div class="flex flex-col gap-1">
<label v-if="widget.name" class="text-sm opacity-80">{{
widget.name
}}</label>
<ToggleSwitch
v-model="value"
v-bind="filteredProps"
:disabled="readonly"
:pt="{
root: {
class:
'hover:outline hover:outline-1 hover:outline-[#5B5E7D] rounded-full'
},
slider: ({ props }) => ({
style: {
backgroundColor: props.modelValue ? '#0b8ce9' : '#0e0e12'
}
}),
handle: ({ props }) => ({
style: {
backgroundColor: props.modelValue ? '#ffffff' : '#5b5e7d'
}
})
}"
/>
</div>
</template>
<script setup lang="ts">
import ToggleSwitch from 'primevue/toggleswitch'
import { computed } from 'vue'
import type { SimplifiedWidget } from '@/types/simplifiedWidget'
import type { WidgetToggleSwitchProps } from '@/types/widgetPropTypes'
import {
INPUT_EXCLUDED_PROPS,
filterWidgetProps
} from '@/utils/widgetPropFilter'
const value = defineModel<boolean>({ required: true })
const props = defineProps<{
widget: SimplifiedWidget<boolean, WidgetToggleSwitchProps>
readonly?: boolean
}>()
const filteredProps = computed(() =>
filterWidgetProps(props.widget.options, INPUT_EXCLUDED_PROPS)
)
</script>

View File

@@ -0,0 +1,37 @@
<template>
<div class="flex flex-col gap-1">
<label v-if="widget.name" class="text-sm opacity-80">{{
widget.name
}}</label>
<TreeSelect v-model="value" v-bind="filteredProps" :disabled="readonly" />
</div>
</template>
<script setup lang="ts">
import TreeSelect from 'primevue/treeselect'
import { computed } from 'vue'
import type { SimplifiedWidget } from '@/types/simplifiedWidget'
import {
PANEL_EXCLUDED_PROPS,
filterWidgetProps
} from '@/utils/widgetPropFilter'
const value = defineModel<any>({ required: true })
const props = defineProps<{
widget: SimplifiedWidget<any>
readonly?: boolean
}>()
// TreeSelect specific excluded props
const TREE_SELECT_EXCLUDED_PROPS = [
...PANEL_EXCLUDED_PROPS,
'inputClass',
'inputStyle'
] as const
const filteredProps = computed(() =>
filterWidgetProps(props.widget.options, TREE_SELECT_EXCLUDED_PROPS)
)
</script>

View File

@@ -0,0 +1,80 @@
/**
* Widget type registry and component mapping for Vue-based widgets
*/
import type { Component } from 'vue'
// Component imports
import WidgetButton from './WidgetButton.vue'
import WidgetChart from './WidgetChart.vue'
import WidgetColorPicker from './WidgetColorPicker.vue'
import WidgetFileUpload from './WidgetFileUpload.vue'
import WidgetGalleria from './WidgetGalleria.vue'
import WidgetImage from './WidgetImage.vue'
import WidgetImageCompare from './WidgetImageCompare.vue'
import WidgetInputText from './WidgetInputText.vue'
import WidgetMultiSelect from './WidgetMultiSelect.vue'
import WidgetSelect from './WidgetSelect.vue'
import WidgetSelectButton from './WidgetSelectButton.vue'
import WidgetSlider from './WidgetSlider.vue'
import WidgetTextarea from './WidgetTextarea.vue'
import WidgetToggleSwitch from './WidgetToggleSwitch.vue'
import WidgetTreeSelect from './WidgetTreeSelect.vue'
/**
* Enum of all available widget types
*/
export enum WidgetType {
BUTTON = 'BUTTON',
STRING = 'STRING',
INT = 'INT',
FLOAT = 'FLOAT',
NUMBER = 'NUMBER',
BOOLEAN = 'BOOLEAN',
COMBO = 'COMBO',
COLOR = 'COLOR',
MULTISELECT = 'MULTISELECT',
SELECTBUTTON = 'SELECTBUTTON',
SLIDER = 'SLIDER',
TEXTAREA = 'TEXTAREA',
TOGGLESWITCH = 'TOGGLESWITCH',
CHART = 'CHART',
IMAGE = 'IMAGE',
IMAGECOMPARE = 'IMAGECOMPARE',
GALLERIA = 'GALLERIA',
FILEUPLOAD = 'FILEUPLOAD',
TREESELECT = 'TREESELECT'
}
/**
* Maps widget types to their corresponding Vue components
* Components will be added as they are implemented
*/
export const widgetTypeToComponent: Record<string, Component> = {
// Components will be uncommented as they are implemented
[WidgetType.BUTTON]: WidgetButton,
[WidgetType.STRING]: WidgetInputText,
[WidgetType.INT]: WidgetSlider,
[WidgetType.FLOAT]: WidgetSlider,
[WidgetType.NUMBER]: WidgetSlider, // For compatibility
[WidgetType.BOOLEAN]: WidgetToggleSwitch,
[WidgetType.COMBO]: WidgetSelect,
[WidgetType.COLOR]: WidgetColorPicker,
[WidgetType.MULTISELECT]: WidgetMultiSelect,
[WidgetType.SELECTBUTTON]: WidgetSelectButton,
[WidgetType.SLIDER]: WidgetSlider,
[WidgetType.TEXTAREA]: WidgetTextarea,
[WidgetType.TOGGLESWITCH]: WidgetToggleSwitch,
[WidgetType.CHART]: WidgetChart,
[WidgetType.IMAGE]: WidgetImage,
[WidgetType.IMAGECOMPARE]: WidgetImageCompare,
[WidgetType.GALLERIA]: WidgetGalleria,
[WidgetType.FILEUPLOAD]: WidgetFileUpload,
[WidgetType.TREESELECT]: WidgetTreeSelect
}
/**
* Helper function to get widget component by type
*/
export function getWidgetComponent(type: string): Component | undefined {
return widgetTypeToComponent[type]
}

View File

@@ -0,0 +1,27 @@
/**
* Simplified widget interface for Vue-based node rendering
* Removes all DOM manipulation and positioning concerns
*/
export interface SimplifiedWidget<T = any, O = Record<string, any>> {
/** Display name of the widget */
name: string
/** Widget type identifier (e.g., 'STRING', 'INT', 'COMBO') */
type: string
/** Current value of the widget */
value: T
/** Widget options including filtered PrimeVue props */
options?: O
/** Callback fired when value changes */
callback?: (value: T) => void
/** Optional serialization method for custom value handling */
serializeValue?: () => any
/** Optional method to compute widget size requirements */
computeSize?: () => { minHeight: number; maxHeight?: number }
}

View File

@@ -0,0 +1,30 @@
/**
* Type definitions for widget component props
*
* These interfaces define the subset of PrimeVue component props that are exposed
* for the node-based widget system. They exclude props that allow custom styling,
* colors, arbitrary CSS, or could create chaotic interfaces.
*
* Based on the design authority at:
* https://www.figma.com/design/CmhEJxo4oZSuYpqG1Yc39w/Nodes-V3?node-id=441-7806&m=dev
*/
import type { ToggleSwitchProps as PrimeVueToggleSwitchProps } from 'primevue/toggleswitch'
/**
* Widget ToggleSwitch Component
* Excludes: style, class, inputClass, inputStyle, dt, pt, ptOptions, unstyled
*
* These props are excluded from widget.options to prevent external styling overrides.
* The widget component itself can still use these props internally for consistent styling.
*/
export type WidgetToggleSwitchProps = Omit<
PrimeVueToggleSwitchProps,
| 'style'
| 'class'
| 'inputClass'
| 'inputStyle'
| 'dt'
| 'pt'
| 'ptOptions'
| 'unstyled'
>

View File

@@ -0,0 +1,76 @@
/**
* Widget prop filtering utilities
* Filters out style-related and customization props from PrimeVue components
* to maintain consistent widget appearance across the application
*/
// Props to exclude based on the widget interface specifications
export const STANDARD_EXCLUDED_PROPS = [
'style',
'class',
'dt',
'pt',
'ptOptions',
'unstyled'
] as const
export const INPUT_EXCLUDED_PROPS = [
...STANDARD_EXCLUDED_PROPS,
'inputClass',
'inputStyle'
] as const
export const PANEL_EXCLUDED_PROPS = [
...STANDARD_EXCLUDED_PROPS,
'panelClass',
'panelStyle',
'overlayClass'
] as const
export const IMAGE_EXCLUDED_PROPS = [
...STANDARD_EXCLUDED_PROPS,
'imageClass',
'imageStyle'
] as const
export const GALLERIA_EXCLUDED_PROPS = [
...STANDARD_EXCLUDED_PROPS,
'thumbnailsPosition',
'verticalThumbnailViewPortHeight',
'indicatorsPosition',
'maskClass',
'containerStyle',
'containerClass',
'galleriaClass'
] as const
export const BADGE_EXCLUDED_PROPS = [
...STANDARD_EXCLUDED_PROPS,
'badgeClass'
] as const
export const LABEL_EXCLUDED_PROPS = [
...PANEL_EXCLUDED_PROPS,
'labelStyle'
] as const
/**
* Filters widget props by excluding specified properties
* @param props - The props object to filter
* @param excludeList - List of property names to exclude
* @returns Filtered props object
*/
export function filterWidgetProps<T extends Record<string, any>>(
props: T | undefined,
excludeList: readonly string[]
): Partial<T> {
if (!props) return {}
const filtered: Record<string, any> = {}
for (const [key, value] of Object.entries(props)) {
if (!excludeList.includes(key)) {
filtered[key] = value
}
}
return filtered as Partial<T>
}