feat(lora-pool): add folder filtering and preview tooltip enhancements

- Add include/exclude folder modals for advanced filtering
- Implement folder tree search with auto-expand functionality
- Add hover tooltip to preview header showing matching LoRA thumbnails
- Format match count with locale string for better readability
- Prevent event propagation on refresh button click
- Improve folder tree component with expand/collapse controls
This commit is contained in:
Will Miao
2026-01-12 10:08:16 +08:00
parent 9719dd4d07
commit 65cede7335
10 changed files with 1070 additions and 437 deletions

View File

@@ -47,6 +47,24 @@
@close="modalState.closeModal"
@update:selected="state.excludeTags.value = $event"
/>
<FoldersModal
:visible="modalState.isModalOpen('includeFolders')"
:folders="state.folderTree.value"
:selected="state.includeFolders.value"
variant="include"
@close="modalState.closeModal"
@update:selected="state.includeFolders.value = $event"
/>
<FoldersModal
:visible="modalState.isModalOpen('excludeFolders')"
:folders="state.folderTree.value"
:selected="state.excludeFolders.value"
variant="exclude"
@close="modalState.closeModal"
@update:selected="state.excludeFolders.value = $event"
/>
</div>
</template>
@@ -55,6 +73,7 @@ import { onMounted } from 'vue'
import LoraPoolSummaryView from './lora-pool/LoraPoolSummaryView.vue'
import BaseModelModal from './lora-pool/modals/BaseModelModal.vue'
import TagsModal from './lora-pool/modals/TagsModal.vue'
import FoldersModal from './lora-pool/modals/FoldersModal.vue'
import { useLoraPoolState } from '../composables/useLoraPoolState'
import { useModalState, type ModalType } from '../composables/useModalState'
import type { ComponentWidget, LoraPoolConfig, LegacyLoraPoolConfig } from '../composables/types'

View File

@@ -1,12 +1,16 @@
<template>
<div class="preview">
<div class="preview__header">
<span class="preview__title">Matching LoRAs: {{ matchCount }}</span>
<div
class="preview__header"
@mouseenter="showTooltip = true"
@mouseleave="showTooltip = false"
>
<span class="preview__title">Matching LoRAs: {{ matchCount.toLocaleString() }}</span>
<button
type="button"
class="preview__refresh"
:class="{ 'preview__refresh--loading': isLoading }"
@click="$emit('refresh')"
@click.stop="$emit('refresh')"
:disabled="isLoading"
>
<svg class="preview__refresh-icon" viewBox="0 0 16 16" fill="currentColor">
@@ -16,43 +20,44 @@
</button>
</div>
<div v-if="items.length > 0" class="preview__list">
<div
v-for="item in items.slice(0, 5)"
:key="item.file_path"
class="preview__item"
>
<img
v-if="item.preview_url"
:src="item.preview_url"
class="preview__thumb"
@error="onImageError"
/>
<div v-else class="preview__thumb preview__thumb--placeholder">
<svg viewBox="0 0 16 16" fill="currentColor">
<path d="M6.002 5.5a1.5 1.5 0 1 1-3 0 1.5 1.5 0 0 1 3 0z"/>
<path d="M2.002 1a2 2 0 0 0-2 2v10a2 2 0 0 0 2 2h12a2 2 0 0 0 2-2V3a2 2 0 0 0-2-2h-12zm12 1a1 1 0 0 1 1 1v6.5l-3.777-1.947a.5.5 0 0 0-.577.093l-3.71 3.71-2.66-1.772a.5.5 0 0 0-.63.062L1.002 12V3a1 1 0 0 1 1-1h12z"/>
</svg>
<!-- Hover tooltip with preview items -->
<Transition name="tooltip">
<div v-if="showTooltip && items.length > 0" class="preview__tooltip">
<div class="preview__tooltip-content">
<div
v-for="item in items.slice(0, 5)"
:key="item.file_path"
class="preview__item"
>
<img
v-if="item.preview_url"
:src="item.preview_url"
class="preview__thumb"
@error="onImageError"
/>
<div v-else class="preview__thumb preview__thumb--placeholder">
<svg viewBox="0 0 16 16" fill="currentColor">
<path d="M6.002 5.5a1.5 1.5 0 1 1-3 0 1.5 1.5 0 0 1 3 0z"/>
<path d="M2.002 1a2 2 0 0 0-2 2v10a2 2 0 0 0 2 2h12a2 2 0 0 0 2-2V3a2 2 0 0 0-2-2h-12zm12 1a1 1 0 0 1 1 1v6.5l-3.777-1.947a.5.5 0 0 0-.577.093l-3.71 3.71-2.66-1.772a.5.5 0 0 0-.63.062L1.002 12V3a1 1 0 0 1 1-1h12z"/>
</svg>
</div>
<span class="preview__name">{{ item.model_name || item.file_name }}</span>
</div>
<div v-if="matchCount > 5" class="preview__more">
+{{ (matchCount - 5).toLocaleString() }} more
</div>
</div>
<span class="preview__name">{{ item.model_name || item.file_name }}</span>
</div>
</Transition>
<div v-if="matchCount > 5" class="preview__more">
+{{ matchCount - 5 }} more
</div>
</div>
<div v-else-if="!isLoading" class="preview__empty">
<div v-if="items.length === 0 && !isLoading" class="preview__empty">
No matching LoRAs
</div>
<div v-else class="preview__loading">
Loading...
</div>
</div>
</template>
<script setup lang="ts">
import { ref } from 'vue'
import type { LoraItem } from '../../composables/types'
defineProps<{
@@ -65,6 +70,8 @@ defineEmits<{
refresh: []
}>()
const showTooltip = ref(false)
const onImageError = (event: Event) => {
const img = event.target as HTMLImageElement
img.style.display = 'none'
@@ -75,13 +82,14 @@ const onImageError = (event: Event) => {
.preview {
padding-top: 12px;
border-top: 1px solid var(--border-color, #444);
position: relative;
}
.preview__header {
display: flex;
align-items: center;
justify-content: space-between;
margin-bottom: 10px;
cursor: default;
}
.preview__title {
@@ -128,7 +136,22 @@ const onImageError = (event: Event) => {
to { transform: rotate(360deg); }
}
.preview__list {
/* Tooltip styles */
.preview__tooltip {
position: absolute;
bottom: 100%;
left: 0;
right: 0;
margin-bottom: 8px;
z-index: 100;
}
.preview__tooltip-content {
background: var(--comfy-menu-bg, #1a1a1a);
border: 1px solid var(--border-color, #444);
border-radius: 6px;
padding: 8px;
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.3);
display: flex;
flex-direction: column;
gap: 4px;
@@ -179,15 +202,26 @@ const onImageError = (event: Event) => {
color: var(--fg-color, #fff);
opacity: 0.5;
text-align: center;
padding: 6px;
padding: 4px;
}
.preview__empty,
.preview__loading {
.preview__empty {
font-size: 11px;
color: var(--fg-color, #fff);
opacity: 0.4;
text-align: center;
padding: 16px;
padding: 8px 0 0 0;
}
/* Tooltip transitions */
.tooltip-enter-active,
.tooltip-leave-active {
transition: opacity 0.15s ease, transform 0.15s ease;
}
.tooltip-enter-from,
.tooltip-leave-to {
opacity: 0;
transform: translateY(4px);
}
</style>

View File

@@ -1,15 +1,5 @@
<template>
<div class="summary-view">
<!-- Header with filter count badge -->
<div class="summary-view__header">
<div class="summary-view__badge">
<svg class="summary-view__badge-icon" viewBox="0 0 16 16" fill="currentColor">
<path d="M1.5 1.5A.5.5 0 0 1 2 1h12a.5.5 0 0 1 .5.5v2a.5.5 0 0 1-.128.334L10 8.692V13.5a.5.5 0 0 1-.342.474l-3 1A.5.5 0 0 1 6 14.5V8.692L1.628 3.834A.5.5 0 0 1 1.5 3.5v-2z"/>
</svg>
<span class="summary-view__count">{{ matchCount.toLocaleString() }}</span>
</div>
</div>
<!-- Filter sections -->
<div class="summary-view__filters">
<BaseModelSection
@@ -30,6 +20,8 @@
:exclude-folders="excludeFolders"
@update:include-folders="$emit('update:includeFolders', $event)"
@update:exclude-folders="$emit('update:excludeFolders', $event)"
@edit-include="$emit('open-modal', 'includeFolders')"
@edit-exclude="$emit('open-modal', 'excludeFolders')"
/>
<LicenseSection
@@ -95,33 +87,6 @@ defineEmits<{
height: 100%;
}
.summary-view__header {
display: flex;
justify-content: flex-end;
margin-bottom: 12px;
}
.summary-view__badge {
display: inline-flex;
align-items: center;
gap: 4px;
padding: 4px 10px;
background: rgba(66, 153, 225, 0.15);
border: 1px solid rgba(66, 153, 225, 0.4);
border-radius: 4px;
color: #4299e1;
}
.summary-view__badge-icon {
width: 12px;
height: 12px;
}
.summary-view__count {
font-size: 12px;
font-weight: 600;
}
.summary-view__filters {
flex: 1;
overflow-y: auto;

View File

@@ -0,0 +1,217 @@
<template>
<div class="tree-node">
<div
class="tree-node__item"
:class="[
`tree-node__item--${variant}`,
{ 'tree-node__item--selected': isSelected }
]"
:style="{ paddingLeft: `${depth * 16 + 8}px` }"
@click="handleRowClick"
>
<!-- Expand/collapse toggle -->
<button
v-if="hasChildren"
type="button"
class="tree-node__toggle"
@click.stop="$emit('toggle-expand', node.key)"
>
<svg
class="tree-node__toggle-icon"
:class="{ 'tree-node__toggle-icon--expanded': isExpanded }"
viewBox="0 0 16 16"
fill="currentColor"
>
<path d="M4.646 1.646a.5.5 0 0 1 .708 0l6 6a.5.5 0 0 1 0 .708l-6 6a.5.5 0 0 1-.708-.708L10.293 8 4.646 2.354a.5.5 0 0 1 0-.708z"/>
</svg>
</button>
<span v-else class="tree-node__toggle-spacer"></span>
<!-- Checkbox -->
<label class="tree-node__checkbox-label">
<input
type="checkbox"
class="tree-node__checkbox"
:checked="isSelected"
@change="$emit('toggle-select', node.key)"
/>
<span class="tree-node__checkbox-visual" :class="`tree-node__checkbox-visual--${variant}`">
<svg v-if="isSelected" class="tree-node__check-icon" viewBox="0 0 16 16" fill="currentColor">
<path d="M13.854 3.646a.5.5 0 0 1 0 .708l-7 7a.5.5 0 0 1-.708 0l-3.5-3.5a.5.5 0 1 1 .708-.708L6.5 10.293l6.646-6.647a.5.5 0 0 1 .708 0z"/>
</svg>
</span>
</label>
<!-- Folder icon -->
<svg class="tree-node__folder-icon" viewBox="0 0 16 16" fill="currentColor">
<path d="M.54 3.87.5 3a2 2 0 0 1 2-2h3.672a2 2 0 0 1 1.414.586l.828.828A2 2 0 0 0 9.828 3H14a2 2 0 0 1 2 2v1.5a.5.5 0 0 1-1 0V5a1 1 0 0 0-1-1H9.828a3 3 0 0 1-2.12-.879l-.83-.828A1 1 0 0 0 6.172 2H2.5a1 1 0 0 0-1 .981l.006.139C1.72 3.042 1.95 3 2.19 3h5.396l.707.707a1 1 0 0 0 .707.293H14.5a.5.5 0 0 1 .5.5v2a.5.5 0 0 1-1 0V5H9a2 2 0 0 1-1.414-.586l-.828-.828A1 1 0 0 0 6.172 3H2.19a1.5 1.5 0 0 0-1.69.87z"/>
<path d="M1.5 4.5h13a.5.5 0 0 1 .5.5v8a2 2 0 0 1-2 2H3a2 2 0 0 1-2-2V5a.5.5 0 0 1 .5-.5z"/>
</svg>
<!-- Folder name -->
<span class="tree-node__label">{{ node.label }}</span>
</div>
<!-- Children (recursive) -->
<div v-if="hasChildren && isExpanded" class="tree-node__children">
<FolderTreeNode
v-for="child in node.children"
:key="child.key"
:node="child"
:selected="selected"
:expanded="expanded"
:variant="variant"
:depth="depth + 1"
@toggle-expand="$emit('toggle-expand', $event)"
@toggle-select="$emit('toggle-select', $event)"
/>
</div>
</div>
</template>
<script setup lang="ts">
import { computed } from 'vue'
import type { FolderTreeNode as FolderTreeNodeType } from '../../../composables/types'
const props = defineProps<{
node: FolderTreeNodeType
selected: string[]
expanded: Set<string>
variant: 'include' | 'exclude'
depth: number
}>()
const emit = defineEmits<{
'toggle-expand': [key: string]
'toggle-select': [key: string]
}>()
const hasChildren = computed(() => props.node.children && props.node.children.length > 0)
const isExpanded = computed(() => props.expanded.has(props.node.key))
const isSelected = computed(() => props.selected.includes(props.node.key))
// Handle row click - toggle selection unless clicking checkbox directly
const handleRowClick = (e: MouseEvent) => {
const target = e.target as HTMLElement
if (target.closest('.tree-node__checkbox-label')) {
return
}
emit('toggle-select', props.node.key)
}
</script>
<style scoped>
.tree-node__item {
display: flex;
align-items: center;
gap: 8px;
padding: 6px 8px;
border-radius: 4px;
cursor: pointer;
transition: background 0.15s;
}
.tree-node__item:hover {
background: var(--comfy-input-bg, #333);
}
.tree-node__toggle {
width: 16px;
height: 16px;
display: flex;
align-items: center;
justify-content: center;
background: transparent;
border: none;
color: var(--fg-color, #fff);
cursor: pointer;
opacity: 0.5;
padding: 0;
flex-shrink: 0;
}
.tree-node__toggle:hover {
opacity: 1;
}
.tree-node__toggle-icon {
width: 10px;
height: 10px;
transition: transform 0.15s;
}
.tree-node__toggle-icon--expanded {
transform: rotate(90deg);
}
.tree-node__toggle-spacer {
width: 16px;
flex-shrink: 0;
}
.tree-node__checkbox-label {
display: flex;
align-items: center;
cursor: pointer;
}
.tree-node__checkbox {
position: absolute;
opacity: 0;
pointer-events: none;
}
.tree-node__checkbox-visual {
display: flex;
align-items: center;
justify-content: center;
width: 16px;
height: 16px;
background: var(--comfy-input-bg, #333);
border: 1px solid var(--border-color, #555);
border-radius: 3px;
flex-shrink: 0;
transition: all 0.15s;
}
.tree-node__item:hover .tree-node__checkbox-visual {
border-color: var(--fg-color, #fff);
}
.tree-node__checkbox:checked + .tree-node__checkbox-visual--include {
background: #4299e1;
border-color: #4299e1;
}
.tree-node__checkbox:checked + .tree-node__checkbox-visual--exclude {
background: #ef4444;
border-color: #ef4444;
}
.tree-node__check-icon {
width: 10px;
height: 10px;
color: #fff;
}
.tree-node__folder-icon {
width: 14px;
height: 14px;
color: var(--fg-color, #fff);
opacity: 0.6;
flex-shrink: 0;
}
.tree-node__label {
flex: 1;
font-size: 13px;
color: var(--fg-color, #fff);
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}
.tree-node__children {
/* Children already indented via padding */
}
</style>

View File

@@ -0,0 +1,173 @@
<template>
<ModalWrapper
:visible="visible"
:title="variant === 'include' ? 'Include Folders' : 'Exclude Folders'"
:subtitle="variant === 'include' ? 'Select folders to include in the filter' : 'Select folders to exclude from the filter'"
@close="$emit('close')"
>
<template #search>
<div class="search-container">
<svg class="search-icon" viewBox="0 0 16 16" fill="currentColor">
<path d="M11.742 10.344a6.5 6.5 0 1 0-1.397 1.398h-.001c.03.04.062.078.098.115l3.85 3.85a1 1 0 0 0 1.415-1.414l-3.85-3.85a1.007 1.007 0 0 0-.115-.1zM12 6.5a5.5 5.5 0 1 1-11 0 5.5 5.5 0 0 1 11 0z"/>
</svg>
<input
v-model="searchQuery"
type="text"
class="search-input"
placeholder="Search folders..."
/>
</div>
</template>
<div class="folder-tree">
<template v-if="filteredFolders.length > 0">
<FolderTreeNode
v-for="node in filteredFolders"
:key="node.key"
:node="node"
:selected="selected"
:expanded="expandedKeys"
:variant="variant"
:depth="0"
@toggle-expand="toggleExpand"
@toggle-select="toggleSelect"
/>
</template>
<div v-else class="no-results">
No folders found
</div>
</div>
</ModalWrapper>
</template>
<script setup lang="ts">
import { ref, computed, watch } from 'vue'
import ModalWrapper from './ModalWrapper.vue'
import FolderTreeNode from './FolderTreeNode.vue'
import type { FolderTreeNode as FolderTreeNodeType } from '../../../composables/types'
const props = defineProps<{
visible: boolean
folders: FolderTreeNodeType[]
selected: string[]
variant: 'include' | 'exclude'
}>()
const emit = defineEmits<{
close: []
'update:selected': [value: string[]]
}>()
const searchQuery = ref('')
const expandedKeys = ref<Set<string>>(new Set())
// Filter folders based on search query
const filteredFolders = computed(() => {
if (!searchQuery.value) {
return props.folders
}
const query = searchQuery.value.toLowerCase()
return filterTree(props.folders, query)
})
// Recursively filter the tree, keeping matching nodes and their ancestors
const filterTree = (nodes: FolderTreeNodeType[], query: string): FolderTreeNodeType[] => {
const result: FolderTreeNodeType[] = []
for (const node of nodes) {
const matches = node.key.toLowerCase().includes(query) || node.label.toLowerCase().includes(query)
const filteredChildren = node.children ? filterTree(node.children, query) : []
if (matches || filteredChildren.length > 0) {
result.push({
...node,
children: filteredChildren.length > 0 ? filteredChildren : node.children
})
// Auto-expand nodes when searching
if (searchQuery.value && filteredChildren.length > 0) {
expandedKeys.value.add(node.key)
}
}
}
return result
}
// Toggle expanded state
const toggleExpand = (key: string) => {
if (expandedKeys.value.has(key)) {
expandedKeys.value.delete(key)
} else {
expandedKeys.value.add(key)
}
// Force reactivity update
expandedKeys.value = new Set(expandedKeys.value)
}
// Toggle selection
const toggleSelect = (key: string) => {
const newSelected = props.selected.includes(key)
? props.selected.filter(k => k !== key)
: [...props.selected, key]
emit('update:selected', newSelected)
}
// Reset expanded state when modal opens
watch(() => props.visible, (isVisible) => {
if (isVisible) {
searchQuery.value = ''
// Auto-expand first level
expandedKeys.value = new Set()
}
})
</script>
<style scoped>
.search-container {
position: relative;
}
.search-icon {
position: absolute;
left: 10px;
top: 50%;
transform: translateY(-50%);
width: 14px;
height: 14px;
color: var(--fg-color, #fff);
opacity: 0.5;
}
.search-input {
width: 100%;
padding: 8px 12px 8px 32px;
background: var(--comfy-input-bg, #333);
border: 1px solid var(--border-color, #444);
border-radius: 6px;
color: var(--fg-color, #fff);
font-size: 13px;
outline: none;
}
.search-input:focus {
border-color: var(--fg-color, #fff);
}
.search-input::placeholder {
color: var(--fg-color, #fff);
opacity: 0.4;
}
.folder-tree {
display: flex;
flex-direction: column;
}
.no-results {
padding: 20px;
text-align: center;
color: var(--fg-color, #fff);
opacity: 0.5;
font-size: 13px;
}
</style>

View File

@@ -8,32 +8,30 @@
<div class="section__column">
<div class="section__column-header">
<span class="section__column-title section__column-title--include">INCLUDE</span>
</div>
<div class="section__input-row">
<input
v-model="includeInput"
type="text"
class="section__input"
placeholder="Path..."
@keydown.enter="addInclude"
/>
<button
type="button"
class="section__add-btn section__add-btn--include"
@click="addInclude"
class="section__edit-btn section__edit-btn--include"
@click="$emit('edit-include')"
>
+
<svg viewBox="0 0 16 16" fill="currentColor">
<path d="M12.854.146a.5.5 0 0 0-.707 0L10.5 1.793 14.207 5.5l1.647-1.646a.5.5 0 0 0 0-.708l-3-3zm.646 6.061L9.793 2.5 3.293 9H3.5a.5.5 0 0 1 .5.5v.5h.5a.5.5 0 0 1 .5.5v.5h.5a.5.5 0 0 1 .5.5v.5h.5a.5.5 0 0 1 .5.5v.207l6.5-6.5zm-7.468 7.468A.5.5 0 0 1 6 13.5V13h-.5a.5.5 0 0 1-.5-.5V12h-.5a.5.5 0 0 1-.5-.5V11h-.5a.5.5 0 0 1-.5-.5V10h-.5a.499.499 0 0 1-.175-.032l-.179.178a.5.5 0 0 0-.11.168l-2 5a.5.5 0 0 0 .65.65l5-2a.5.5 0 0 0 .168-.11l.178-.178z"/>
</svg>
</button>
</div>
<div v-if="includeFolders.length > 0" class="section__paths">
<FilterChip
v-for="path in includeFolders"
:key="path"
:label="truncatePath(path)"
variant="path"
removable
@remove="removeInclude(path)"
/>
<div class="section__content">
<div v-if="includeFolders.length > 0" class="section__paths">
<FilterChip
v-for="path in includeFolders"
:key="path"
:label="truncatePath(path)"
variant="path"
removable
@remove="removeInclude(path)"
/>
</div>
<div v-else class="section__empty">
No folders selected
</div>
</div>
</div>
@@ -41,32 +39,30 @@
<div class="section__column">
<div class="section__column-header">
<span class="section__column-title section__column-title--exclude">EXCLUDE</span>
</div>
<div class="section__input-row">
<input
v-model="excludeInput"
type="text"
class="section__input"
placeholder="Path..."
@keydown.enter="addExclude"
/>
<button
type="button"
class="section__add-btn section__add-btn--exclude"
@click="addExclude"
class="section__edit-btn section__edit-btn--exclude"
@click="$emit('edit-exclude')"
>
+
<svg viewBox="0 0 16 16" fill="currentColor">
<path d="M12.854.146a.5.5 0 0 0-.707 0L10.5 1.793 14.207 5.5l1.647-1.646a.5.5 0 0 0 0-.708l-3-3zm.646 6.061L9.793 2.5 3.293 9H3.5a.5.5 0 0 1 .5.5v.5h.5a.5.5 0 0 1 .5.5v.5h.5a.5.5 0 0 1 .5.5v.5h.5a.5.5 0 0 1 .5.5v.207l6.5-6.5zm-7.468 7.468A.5.5 0 0 1 6 13.5V13h-.5a.5.5 0 0 1-.5-.5V12h-.5a.5.5 0 0 1-.5-.5V11h-.5a.5.5 0 0 1-.5-.5V10h-.5a.499.499 0 0 1-.175-.032l-.179.178a.5.5 0 0 0-.11.168l-2 5a.5.5 0 0 0 .65.65l5-2a.5.5 0 0 0 .168-.11l.178-.178z"/>
</svg>
</button>
</div>
<div v-if="excludeFolders.length > 0" class="section__paths">
<FilterChip
v-for="path in excludeFolders"
:key="path"
:label="truncatePath(path)"
variant="path"
removable
@remove="removeExclude(path)"
/>
<div class="section__content">
<div v-if="excludeFolders.length > 0" class="section__paths">
<FilterChip
v-for="path in excludeFolders"
:key="path"
:label="truncatePath(path)"
variant="path"
removable
@remove="removeExclude(path)"
/>
</div>
<div v-else class="section__empty">
No folders selected
</div>
</div>
</div>
</div>
@@ -74,7 +70,6 @@
</template>
<script setup lang="ts">
import { ref } from 'vue'
import FilterChip from '../shared/FilterChip.vue'
const props = defineProps<{
@@ -85,36 +80,19 @@ const props = defineProps<{
const emit = defineEmits<{
'update:includeFolders': [value: string[]]
'update:excludeFolders': [value: string[]]
'edit-include': []
'edit-exclude': []
}>()
const includeInput = ref('')
const excludeInput = ref('')
const truncatePath = (path: string) => {
if (path.length <= 20) return path
return '...' + path.slice(-17)
}
const addInclude = () => {
const path = includeInput.value.trim()
if (path && !props.includeFolders.includes(path)) {
emit('update:includeFolders', [...props.includeFolders, path])
includeInput.value = ''
}
}
const removeInclude = (path: string) => {
emit('update:includeFolders', props.includeFolders.filter(p => p !== path))
}
const addExclude = () => {
const path = excludeInput.value.trim()
if (path && !props.excludeFolders.includes(path)) {
emit('update:excludeFolders', [...props.excludeFolders, path])
excludeInput.value = ''
}
}
const removeExclude = (path: string) => {
emit('update:excludeFolders', props.excludeFolders.filter(p => p !== path))
}
@@ -149,6 +127,9 @@ const removeExclude = (path: string) => {
}
.section__column-header {
display: flex;
align-items: center;
justify-content: space-between;
margin-bottom: 6px;
}
@@ -167,68 +148,58 @@ const removeExclude = (path: string) => {
color: #ef4444;
}
.section__input-row {
display: flex;
gap: 4px;
}
.section__input {
flex: 1;
min-width: 0;
padding: 6px 8px;
background: var(--comfy-input-bg, #333);
border: 1px solid var(--border-color, #444);
border-radius: 4px;
color: var(--fg-color, #fff);
font-size: 11px;
outline: none;
}
.section__input:focus {
border-color: var(--fg-color, #fff);
}
.section__input::placeholder {
color: var(--fg-color, #fff);
opacity: 0.4;
}
.section__add-btn {
width: 28px;
height: 28px;
.section__edit-btn {
width: 20px;
height: 20px;
display: flex;
align-items: center;
justify-content: center;
background: var(--comfy-input-bg, #333);
border: 1px solid var(--border-color, #444);
border-radius: 4px;
background: transparent;
border: none;
color: var(--fg-color, #fff);
font-size: 16px;
font-weight: 500;
cursor: pointer;
opacity: 0.5;
border-radius: 3px;
padding: 0;
transition: all 0.15s;
}
.section__add-btn:hover {
border-color: var(--fg-color, #fff);
.section__edit-btn svg {
width: 12px;
height: 12px;
}
.section__add-btn--include:hover {
background: rgba(66, 153, 225, 0.2);
border-color: #4299e1;
.section__edit-btn:hover {
opacity: 1;
background: var(--comfy-input-bg, #333);
}
.section__edit-btn--include:hover {
color: #4299e1;
}
.section__add-btn--exclude:hover {
background: rgba(239, 68, 68, 0.2);
border-color: #ef4444;
.section__edit-btn--exclude:hover {
color: #ef4444;
}
.section__content {
min-height: 22px;
}
.section__paths {
display: flex;
flex-wrap: wrap;
gap: 4px;
margin-top: 6px;
min-height: 22px;
}
.section__empty {
font-size: 10px;
color: var(--fg-color, #fff);
opacity: 0.3;
font-style: italic;
min-height: 22px;
display: flex;
align-items: center;
}
</style>

View File

@@ -1,6 +1,6 @@
import { ref, computed } from 'vue'
export type ModalType = 'baseModels' | 'includeTags' | 'excludeTags' | null
export type ModalType = 'baseModels' | 'includeTags' | 'excludeTags' | 'includeFolders' | 'excludeFolders' | null
export function useModalState() {
const activeModal = ref<ModalType>(null)

View File

@@ -38,7 +38,7 @@ function createLoraPoolWidget(node) {
// Per dev guide: providing getMinHeight via options allows the system to
// skip expensive DOM measurements during rendering loop, improving performance
getMinHeight() {
return 700
return 400
}
}
)
@@ -62,7 +62,7 @@ function createLoraPoolWidget(node) {
widget.computeLayoutSize = () => {
const minWidth = 500
const minHeight = 700
const minHeight = 400
return { minHeight, minWidth }
}

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long