merge main into rh-test

This commit is contained in:
bymyself
2025-09-28 15:33:29 -07:00
parent 1c0f151d02
commit ff0c15b119
1317 changed files with 85439 additions and 18373 deletions

View File

@@ -0,0 +1,52 @@
/**
* Maps category IDs to their corresponding Lucide icon classes
*/
export const getCategoryIcon = (categoryId: string): string => {
const iconMap: Record<string, string> = {
// Main categories
all: 'icon-[lucide--list]',
'getting-started': 'icon-[lucide--graduation-cap]',
// Generation types
'generation-image': 'icon-[lucide--image]',
image: 'icon-[lucide--image]',
'generation-video': 'icon-[lucide--film]',
video: 'icon-[lucide--film]',
'generation-3d': 'icon-[lucide--box]',
'3d': 'icon-[lucide--box]',
'generation-audio': 'icon-[lucide--volume-2]',
audio: 'icon-[lucide--volume-2]',
'generation-llm': 'icon-[lucide--message-square-text]',
// API and models
'api-nodes': 'icon-[lucide--hand-coins]',
'closed-models': 'icon-[lucide--hand-coins]',
// LLMs and AI
llm: 'icon-[lucide--message-square-text]',
llms: 'icon-[lucide--message-square-text]',
'llm-api': 'icon-[lucide--message-square-text]',
// Performance and hardware
'small-models': 'icon-[lucide--zap]',
performance: 'icon-[lucide--zap]',
'mac-compatible': 'icon-[lucide--command]',
'runs-on-mac': 'icon-[lucide--command]',
// Training
'lora-training': 'icon-[lucide--dumbbell]',
training: 'icon-[lucide--dumbbell]',
// Extensions and tools
extensions: 'icon-[lucide--puzzle]',
tools: 'icon-[lucide--wrench]',
// Fallbacks for common patterns
upscaling: 'icon-[lucide--maximize-2]',
controlnet: 'icon-[lucide--sliders-horizontal]',
'area-composition': 'icon-[lucide--layout-grid]'
}
// Return mapped icon or fallback to folder
return iconMap[categoryId.toLowerCase()] || 'icon-[lucide--folder]'
}

View File

@@ -1,9 +1,20 @@
import { memoize } from 'es-toolkit/compat'
type RGB = { r: number; g: number; b: number }
export interface HSB {
h: number
s: number
b: number
}
type HSL = { h: number; s: number; l: number }
type HSLA = { h: number; s: number; l: number; a: number }
type ColorFormat = 'hex' | 'rgb' | 'rgba' | 'hsl' | 'hsla'
type ColorFormatInternal = 'hex' | 'rgb' | 'rgba' | 'hsl' | 'hsla'
export type ColorFormat = 'hex' | 'rgb' | 'hsb'
interface HSV {
h: number
s: number
v: number
}
export interface ColorAdjustOptions {
lightness?: number
@@ -59,7 +70,119 @@ export function hexToRgb(hex: string): RGB {
return { r, g, b }
}
const identifyColorFormat = (color: string): ColorFormat | null => {
export function rgbToHex({ r, g, b }: RGB): string {
const toHex = (n: number) =>
Math.max(0, Math.min(255, Math.round(n)))
.toString(16)
.padStart(2, '0')
return `#${toHex(r)}${toHex(g)}${toHex(b)}`
}
export function hsbToRgb({ h, s, b }: HSB): RGB {
// Normalize
const hh = ((h % 360) + 360) % 360
const ss = Math.max(0, Math.min(100, s)) / 100
const vv = Math.max(0, Math.min(100, b)) / 100
const c = vv * ss
const x = c * (1 - Math.abs(((hh / 60) % 2) - 1))
const m = vv - c
let rp = 0,
gp = 0,
bp = 0
if (hh < 60) {
rp = c
gp = x
bp = 0
} else if (hh < 120) {
rp = x
gp = c
bp = 0
} else if (hh < 180) {
rp = 0
gp = c
bp = x
} else if (hh < 240) {
rp = 0
gp = x
bp = c
} else if (hh < 300) {
rp = x
gp = 0
bp = c
} else {
rp = c
gp = 0
bp = x
}
return {
r: Math.floor((rp + m) * 255),
g: Math.floor((gp + m) * 255),
b: Math.floor((bp + m) * 255)
}
}
/**
* Normalize various color inputs (hex, rgb/rgba, hsl/hsla, hsb string/object)
* into lowercase #rrggbb. Falls back to #000000 on invalid inputs.
*/
export function parseToRgb(color: string): RGB {
const format = identifyColorFormat(color)
if (!format) return { r: 0, g: 0, b: 0 }
const hsla = parseToHSLA(color, format)
if (!isHSLA(hsla)) return { r: 0, g: 0, b: 0 }
// Convert HSL to RGB
const h = hsla.h / 360
const s = hsla.s / 100
const l = hsla.l / 100
const c = (1 - Math.abs(2 * l - 1)) * s
const x = c * (1 - Math.abs(((h * 6) % 2) - 1))
const m = l - c / 2
let r = 0,
g = 0,
b = 0
if (h < 1 / 6) {
r = c
g = x
b = 0
} else if (h < 2 / 6) {
r = x
g = c
b = 0
} else if (h < 3 / 6) {
r = 0
g = c
b = x
} else if (h < 4 / 6) {
r = 0
g = x
b = c
} else if (h < 5 / 6) {
r = x
g = 0
b = c
} else {
r = c
g = 0
b = x
}
return {
r: Math.round((r + m) * 255),
g: Math.round((g + m) * 255),
b: Math.round((b + m) * 255)
}
}
const identifyColorFormat = (color: string): ColorFormatInternal | null => {
if (!color) return null
if (color.startsWith('#') && (color.length === 4 || color.length === 7))
return 'hex'
@@ -80,7 +203,73 @@ const isHSLA = (color: unknown): color is HSLA => {
)
}
function parseToHSLA(color: string, format: ColorFormat): HSLA | null {
export function isColorFormat(v: unknown): v is ColorFormat {
return v === 'hex' || v === 'rgb' || v === 'hsb'
}
function isHSBObject(v: unknown): v is HSB {
if (!v || typeof v !== 'object') return false
const rec = v as Record<string, unknown>
return (
typeof rec.h === 'number' &&
Number.isFinite(rec.h) &&
typeof rec.s === 'number' &&
Number.isFinite(rec.s) &&
typeof (rec as Record<string, unknown>).b === 'number' &&
Number.isFinite((rec as Record<string, number>).b!)
)
}
function isHSVObject(v: unknown): v is HSV {
if (!v || typeof v !== 'object') return false
const rec = v as Record<string, unknown>
return (
typeof rec.h === 'number' &&
Number.isFinite(rec.h) &&
typeof rec.s === 'number' &&
Number.isFinite(rec.s) &&
typeof (rec as Record<string, unknown>).v === 'number' &&
Number.isFinite((rec as Record<string, number>).v!)
)
}
export function toHexFromFormat(val: unknown, format: ColorFormat): string {
if (format === 'hex' && typeof val === 'string') {
const raw = val.trim().toLowerCase()
if (!raw) return '#000000'
if (/^[0-9a-f]{3}$/.test(raw)) return `#${raw}`
if (/^#[0-9a-f]{3}$/.test(raw)) return raw
if (/^[0-9a-f]{6}$/.test(raw)) return `#${raw}`
if (/^#[0-9a-f]{6}$/.test(raw)) return raw
return '#000000'
}
if (format === 'rgb' && typeof val === 'string') {
const rgb = parseToRgb(val)
return rgbToHex(rgb).toLowerCase()
}
if (format === 'hsb') {
if (isHSBObject(val)) {
return rgbToHex(hsbToRgb(val)).toLowerCase()
}
if (isHSVObject(val)) {
const { h, s, v } = val
return rgbToHex(hsbToRgb({ h, s, b: v })).toLowerCase()
}
if (typeof val === 'string') {
const nums = val.match(/\d+(?:\.\d+)?/g)?.map(Number) || []
if (nums.length >= 3) {
return rgbToHex(
hsbToRgb({ h: nums[0], s: nums[1], b: nums[2] })
).toLowerCase()
}
}
}
return '#000000'
}
function parseToHSLA(color: string, format: ColorFormatInternal): HSLA | null {
let match: RegExpMatchArray | null
switch (format) {

View File

@@ -1,4 +1,4 @@
import { ElectronAPI } from '@comfyorg/comfyui-electron-types'
import type { ElectronAPI } from '@comfyorg/comfyui-electron-types'
export function isElectron() {
return 'electronAPI' in window && window.electronAPI !== undefined

View File

@@ -1,6 +1,6 @@
import type { ISerialisedGraph } from '@/lib/litegraph/src/litegraph'
import type { NodeId } from '@/platform/workflow/validation/schemas/workflowSchema'
import type { SystemStats } from '@/schemas/apiSchema'
import type { NodeId } from '@/schemas/comfyWorkflowSchema'
export interface ErrorReportData {
exceptionType: string

View File

@@ -10,7 +10,7 @@ import {
import type {
ComfyApiWorkflow,
ComfyWorkflowJSON
} from '@/schemas/comfyWorkflowSchema'
} from '@/platform/workflow/validation/schemas/workflowSchema'
import { ExecutableGroupNodeDTO, isGroupNode } from './executableGroupNodeDto'
import { compressWidgetInputSlots } from './litegraphUtil'

View File

@@ -1,4 +1,4 @@
import { ResultItem } from '@/schemas/apiSchema'
import type { ResultItem } from '@/schemas/apiSchema'
import type { operations } from '@/types/comfyRegistryTypes'
export function formatCamelCase(str: string): string {
@@ -33,10 +33,6 @@ export function appendJsonExt(path: string) {
return path
}
export function trimJsonExt(path?: string) {
return path?.replace(/\.json$/, '')
}
export function highlightQuery(text: string, query: string) {
if (!query) return text
@@ -80,28 +76,6 @@ export function formatSize(value?: number) {
return `${parseFloat((bytes / Math.pow(k, i)).toFixed(2))} ${sizes[i]}`
}
/**
* Finds the common directory prefix between two paths
* @example
* findCommonPrefix('a/b/c', 'a/b/d') // returns 'a/b'
* findCommonPrefix('x/y/z', 'a/b/c') // returns ''
* findCommonPrefix('a/b/c', 'a/b/c/d') // returns 'a/b/c'
*/
export function findCommonPrefix(path1: string, path2: string): string {
const parts1 = path1.split('/')
const parts2 = path2.split('/')
const commonParts: string[] = []
for (let i = 0; i < Math.min(parts1.length, parts2.length); i++) {
if (parts1[i] === parts2[i]) {
commonParts.push(parts1[i])
} else {
break
}
}
return commonParts.join('/')
}
/**
* Returns various filename components.
* Example:
@@ -109,7 +83,7 @@ export function findCommonPrefix(path1: string, path2: string): string {
* - filename: 'file'
* - suffix: 'txt'
*/
export function getFilenameDetails(fullFilename: string) {
function getFilenameDetails(fullFilename: string) {
if (fullFilename.includes('.')) {
return {
filename: fullFilename.split('.').slice(0, -1).join('.'),
@@ -390,59 +364,6 @@ export const downloadUrlToHfRepoUrl = (url: string): string => {
}
}
export const isSemVer = (
version: string
): version is `${number}.${number}.${number}` => {
const regex = /^\d+\.\d+\.\d+$/
return regex.test(version)
}
const normalizeVersion = (version: string) =>
version
.split(/[+.-]/)
.map(Number)
.filter((part) => !Number.isNaN(part))
export function compareVersions(
versionA: string | undefined,
versionB: string | undefined
): number {
versionA ??= '0.0.0'
versionB ??= '0.0.0'
const aParts = normalizeVersion(versionA)
const bParts = normalizeVersion(versionB)
for (let i = 0; i < Math.max(aParts.length, bParts.length); i++) {
const aPart = aParts[i] ?? 0
const bPart = bParts[i] ?? 0
if (aPart < bPart) return -1
if (aPart > bPart) return 1
}
return 0
}
/**
* Converts a currency amount to Metronome's integer representation.
* For USD, converts to cents (multiplied by 100).
* For all other currencies (including custom pricing units), returns the amount as is.
* This is specific to Metronome's API requirements.
*
* @param amount - The amount in currency to convert
* @param currency - The currency to convert
* @returns The amount in Metronome's integer format (cents for USD, base units for others)
* @example
* toMetronomeCurrency(1.23, 'usd') // returns 123 (cents)
* toMetronomeCurrency(1000, 'jpy') // returns 1000 (yen)
*/
export function toMetronomeCurrency(amount: number, currency: string): number {
if (currency === 'usd') {
return Math.round(amount * 100)
}
return amount
}
/**
* Converts Metronome's integer amount back to a formatted currency string.
* For USD, converts from cents to dollars.
@@ -525,7 +446,7 @@ export function formatVersionAnchor(version: string): string {
/**
* Supported locale types for the application (from OpenAPI schema)
*/
export type SupportedLocale = NonNullable<
type SupportedLocale = NonNullable<
operations['getReleaseNotes']['parameters']['query']['locale']
>

View File

@@ -1,4 +1,5 @@
import Fuse, { FuseOptionKey, FuseSearchOptions, IFuseOptions } from 'fuse.js'
import type { FuseOptionKey, FuseSearchOptions, IFuseOptions } from 'fuse.js'
import Fuse from 'fuse.js'
export type SearchAuxScore = number[]

View File

@@ -8,6 +8,23 @@ import { parseNodeLocatorId } from '@/types/nodeIdentification'
import { isSubgraphIoNode } from './typeGuardUtil'
interface NodeWithId {
id: string | number
subgraphId?: string | null
}
/**
* Constructs a locator ID from node data with optional subgraph context.
*
* @param nodeData - Node data containing id and optional subgraphId
* @returns The locator ID string
*/
export function getLocatorIdFromNodeData(nodeData: NodeWithId): string {
return nodeData.subgraphId
? `${nodeData.subgraphId}:${String(nodeData.id)}`
: String(nodeData.id)
}
/**
* Parses an execution ID into its component parts.
*
@@ -394,7 +411,7 @@ export function getAllNonIoNodesInSubgraph(subgraph: Subgraph): LGraphNode[] {
/**
* Options for traverseNodesDepthFirst function
*/
export interface TraverseNodesOptions<T> {
interface TraverseNodesOptions<T> {
/** Function called for each node during traversal */
visitor?: (node: LGraphNode, context: T) => T
/** Initial context value */
@@ -449,7 +466,7 @@ export function traverseNodesDepthFirst<T = void>(
/**
* Options for collectFromNodes function
*/
export interface CollectFromNodesOptions<T, C> {
interface CollectFromNodesOptions<T, C> {
/** Function that returns data to collect for each node */
collector?: (node: LGraphNode, context: C) => T | null
/** Function that builds context for child nodes */

45
src/utils/gridUtil.ts Normal file
View File

@@ -0,0 +1,45 @@
import type { CSSProperties } from 'vue'
interface GridOptions {
/** Minimum width for each grid item (default: 15rem) */
minWidth?: string
/** Maximum width for each grid item (default: 1fr) */
maxWidth?: string
/** Padding around the grid (default: 0) */
padding?: string
/** Gap between grid items (default: 1rem) */
gap?: string
/** Fixed number of columns (overrides auto-fill with minmax) */
columns?: number
}
/**
* Creates CSS grid styles for responsive grid layouts
* @param options Grid configuration options
* @returns CSS properties object for grid styling
*/
export function createGridStyle(options: GridOptions = {}): CSSProperties {
const {
minWidth = '15rem',
maxWidth = '1fr',
padding = '0',
gap = '1rem',
columns
} = options
// Runtime validation for columns
if (columns !== undefined && columns < 1) {
console.warn('createGridStyle: columns must be >= 1, defaulting to 1')
}
const gridTemplateColumns = columns
? `repeat(${Math.max(1, columns ?? 1)}, 1fr)`
: `repeat(auto-fill, minmax(${minWidth}, ${maxWidth}))`
return {
display: 'grid',
gridTemplateColumns,
padding,
gap
}
}

View File

@@ -32,7 +32,7 @@ import type {
ISerialisedNode
} from '@/lib/litegraph/src/types/serialisation'
export interface BadLinksData<T = ISerialisedGraph | LGraph> {
interface BadLinksData<T = ISerialisedGraph | LGraph> {
hasBadLinks: boolean
fixed: boolean
graph: T

View File

@@ -1,6 +1,7 @@
import _ from 'es-toolkit/compat'
import { ColorOption, LGraph, Reroute } from '@/lib/litegraph/src/litegraph'
import type { ColorOption, LGraph } from '@/lib/litegraph/src/litegraph'
import { Reroute } from '@/lib/litegraph/src/litegraph'
import {
LGraphGroup,
LGraphNode,

View File

@@ -1,10 +1,10 @@
import { transformInputSpecV1ToV2 } from '@/schemas/nodeDef/migration'
import {
import type {
ComfyNodeDef as ComfyNodeDefV2,
InputSpec
} from '@/schemas/nodeDef/nodeDefSchemaV2'
import { ComfyNodeDef as ComfyNodeDefV1 } from '@/schemas/nodeDefSchema'
import { components } from '@/types/comfyRegistryTypes'
import type { ComfyNodeDef as ComfyNodeDefV1 } from '@/schemas/nodeDefSchema'
import type { components } from '@/types/comfyRegistryTypes'
const registryToFrontendV2NodeOutputs = (
registryDef: components['schemas']['ComfyNode']

View File

@@ -1,3 +1,6 @@
import type { ReadOnlyRect } from '@/lib/litegraph/src/interfaces'
import type { Bounds } from '@/renderer/core/layout/types'
/**
* Finds the greatest common divisor (GCD) for two numbers.
*
@@ -19,3 +22,48 @@ export const gcd = (a: number, b: number): number => {
export const lcm = (a: number, b: number): number => {
return Math.abs(a * b) / gcd(a, b)
}
/**
* Computes the union (bounding box) of multiple rectangles using a single-pass algorithm.
*
* Finds the minimum and maximum x/y coordinates across all rectangles to create
* a single bounding rectangle that contains all input rectangles. Optimized for
* performance with V8-friendly tuple access patterns.
*
* @param rectangles - Array of rectangle tuples in [x, y, width, height] format
* @returns Bounds object with union rectangle, or null if no rectangles provided
*/
export function computeUnionBounds(
rectangles: readonly ReadOnlyRect[]
): Bounds | null {
const n = rectangles.length
if (n === 0) {
return null
}
const r0 = rectangles[0]
let minX = r0[0]
let minY = r0[1]
let maxX = minX + r0[2]
let maxY = minY + r0[3]
for (let i = 1; i < n; i++) {
const r = rectangles[i]
const x1 = r[0]
const y1 = r[1]
const x2 = x1 + r[2]
const y2 = y1 + r[3]
if (x1 < minX) minX = x1
if (y1 < minY) minY = y1
if (x2 > maxX) maxX = x2
if (y2 > maxY) maxY = y2
}
return {
x: minX,
y: minY,
width: maxX - minX,
height: maxY - minY
}
}

View File

@@ -6,7 +6,7 @@ import type {
NodeId,
Reroute,
WorkflowJSON04
} from '@/schemas/comfyWorkflowSchema'
} from '@/platform/workflow/validation/schemas/workflowSchema'
type RerouteNode = ComfyNode & {
type: 'Reroute'

View File

@@ -1,4 +1,4 @@
import type { ModelFile } from '@/schemas/comfyWorkflowSchema'
import type { ModelFile } from '@/platform/workflow/validation/schemas/workflowSchema'
/**
* Gets models from the node's `properties.models` field, excluding those

View File

@@ -1,4 +1,4 @@
import { TWidgetValue } from '@/lib/litegraph/src/litegraph'
import type { TWidgetValue } from '@/lib/litegraph/src/litegraph'
import type { InputSpec } from '@/schemas/nodeDef/nodeDefSchemaV2'
import type { ComfyNodeDefImpl } from '@/stores/nodeDefStore'

View File

@@ -15,7 +15,6 @@ import {
isFloatInputSpec,
isIntInputSpec
} from '@/schemas/nodeDefSchema'
import type { ComfyNodeDefImpl } from '@/stores/nodeDefStore'
import { lcm } from './mathUtil'
@@ -139,11 +138,3 @@ export const mergeInputSpec = (
return mergeCommonInputSpec(spec1, spec2)
}
/**
* Checks if a node definition represents a subgraph node.
* Subgraph nodes are created with category='subgraph' and python_module='nodes'.
*/
export const isSubgraphNode = (nodeDef: ComfyNodeDefImpl): boolean => {
return nodeDef.category === 'subgraph' && nodeDef.python_module === 'nodes'
}

View File

@@ -1,12 +1,14 @@
import type { ComfyNodeDefImpl } from '@/stores/nodeDefStore'
import { NodeSourceType, getNodeSource } from '@/types/nodeSource'
import { normalizePackId } from '@/utils/packUtils'
export function extractCustomNodeName(
pythonModule: string | undefined
): string | null {
const modules = pythonModule?.split('.') || []
if (modules.length >= 2 && modules[0] === 'custom_nodes') {
return modules[1].split('@')[0]
// Use normalizePackId to remove version suffix
return normalizePackId(modules[1])
}
return null
}

35
src/utils/packUtils.ts Normal file
View File

@@ -0,0 +1,35 @@
import { mapKeys } from 'es-toolkit/compat'
/**
* Normalizes a pack ID by removing the version suffix.
*
* ComfyUI-Manager returns pack IDs in different formats:
* - Enabled packs: "packname" (without version)
* - Disabled packs: "packname@1_0_3" (with version suffix)
* - Latest versions from registry: "packname" (without version)
*
* Since the pack object itself contains the version info (ver field),
* we normalize all pack IDs to just the base name for consistent access.
* This ensures we can always find a pack by its base name (nodePack.id)
* regardless of its enabled/disabled state.
*
* @param packId - The pack ID that may contain a version suffix
* @returns The normalized pack ID without version suffix
*/
export function normalizePackId(packId: string): string {
return packId.split('@')[0]
}
/**
* Normalizes all keys in a pack record by removing version suffixes.
* This is used when receiving pack data from the server to ensure
* consistent key format across the application.
*
* @param packs - Record of packs with potentially versioned keys
* @returns Record with normalized keys
*/
export function normalizePackKeys<T>(
packs: Record<string, T>
): Record<string, T> {
return mapKeys(packs, (_value, key) => normalizePackId(key))
}

View File

@@ -0,0 +1 @@
export { cn, type ClassValue } from '@comfyorg/tailwind-utils'

View File

@@ -129,7 +129,7 @@ export const findNodeByKey = <T extends TreeNode>(
* @param node - The node to clone.
* @returns A deep clone of the node.
*/
export function cloneTree<T extends TreeNode>(node: T): T {
function cloneTree<T extends TreeNode>(node: T): T {
const clone = { ...node }
// Clone children recursively

View File

@@ -1,6 +1,7 @@
import type { PrimitiveNode } from '@/extensions/core/widgetInputs'
import { LGraph, LGraphNode } from '@/lib/litegraph/src/litegraph'
import { Subgraph } from '@/lib/litegraph/src/litegraph'
import type { LGraph, LGraphNode } from '@/lib/litegraph/src/litegraph'
import type { INodeSlot } from '@/lib/litegraph/src/litegraph'
import type { Subgraph } from '@/lib/litegraph/src/litegraph'
export function isPrimitiveNode(
node: LGraphNode
@@ -39,3 +40,16 @@ export const isSubgraphIoNode = (
const nodeClass = node.constructor?.comfyClass
return nodeClass === 'SubgraphInputNode' || nodeClass === 'SubgraphOutputNode'
}
/**
* Type guard for slot objects (inputs/outputs)
*/
export const isSlotObject = (obj: unknown): obj is INodeSlot => {
return (
obj !== null &&
typeof obj === 'object' &&
'name' in obj &&
'type' in obj &&
'boundingRect' in obj
)
}

View File

@@ -4,16 +4,3 @@ export enum ValidationState {
VALID = 'VALID',
INVALID = 'INVALID'
}
export const mergeValidationStates = (states: ValidationState[]) => {
if (states.some((state) => state === ValidationState.INVALID)) {
return ValidationState.INVALID
}
if (states.some((state) => state === ValidationState.LOADING)) {
return ValidationState.LOADING
}
if (states.every((state) => state === ValidationState.VALID)) {
return ValidationState.VALID
}
return ValidationState.IDLE
}

View File

@@ -0,0 +1,71 @@
/**
* 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
/**
* 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>
}