ChatDev/frontend/src/components/DynamicFormField.vue

1605 lines
43 KiB
Vue
Executable File
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

<template>
<div class="form-field" v-show="isVisible">
<template v-if="field.childNode">
<div v-if="recursive" class="form-group form-group-inline child-node-group">
<label>
{{ $te('schema.' + field.name) ? $t('schema.' + field.name) : (field.displayName || field.name) }}
<span v-if="field.required" class="required-asterisk">*</span>
<RichTooltip
v-if="field.description"
:content="{ description: $te('schema_desc.' + field.name) ? $t('schema_desc.' + field.name) : field.description }"
placement="top"
>
<span class="help-icon" tabindex="0">?</span>
</RichTooltip>
</label>
<div class="child-node-container">
<div class="child-node-controls">
<button @click="$emit('open-child-modal', field)" class="add-child-button">
<span class="plus-icon" aria-hidden="true">
<!-- Edit icon when configured -->
<svg
v-if="!isList && hasChildValue"
width="16"
height="16"
viewBox="0 0 16 16"
xmlns="http://www.w3.org/2000/svg"
>
<rect
x="3"
y="2"
width="8"
height="11"
rx="1"
ry="1"
fill="none"
stroke="currentColor"
stroke-width="1.2"
/>
<path
fill="currentColor"
d="M10.9 3.1a.9.9 0 0 1 1.27 0l.73.73a.9.9 0 0 1 0 1.27l-4.3 4.3-1.7.5.5-1.7 4.3-4.3z"
/>
</svg>
<!-- Configure icon when not yet configured -->
<svg
v-else
width="16"
height="16"
viewBox="0 0 16 16"
xmlns="http://www.w3.org/2000/svg"
>
<path
fill="currentColor"
d="M8 2.5a.75.75 0 0 1 .75.75v4h4a.75.75 0 0 1 0 1.5h-4v4a.75.75 0 0 1-1.5 0v-4h-4a.75.75 0 0 1 0-1.5h4v-4A.75.75 0 0 1 8 2.5z"
/>
</svg>
</span>
{{ childNodeButtonLabel }}
</button>
<!-- Show clear button only when child is already configured ("Edit" mode) -->
<button
v-if="hasChildValue"
class="clear-child-button"
@click.stop="$emit('clear-child-entry', field.name)"
:title="$t('dynamic_form_field.clear_configuration')"
>
×
</button>
</div>
<div
v-if="isList && formData[field.name] && formData[field.name].length"
class="child-node-list"
>
<div
v-for="(childEntry, childIndex) in formData[field.name]"
:key="childIndex"
class="child-node-item-wrapper"
>
<div
class="child-node-item"
@click="$emit('open-child-modal', field, childIndex)"
>
<span class="child-node-name">
{{ describeChildEntry(childEntry, childIndex) }}
</span>
<div class="child-node-actions">
<button
class="delete-child-button"
@click.stop="$emit('delete-child-entry', field.name, childIndex)"
:title="$t('dynamic_form_field.delete_child')"
>
×
</button>
</div>
</div>
<div
v-if="childSummaries[field.name] && childSummaries[field.name][childIndex]"
class="child-node-summary"
>
{{ childSummaries[field.name][childIndex] }}
</div>
</div>
</div>
<div
v-else-if="!isList && hasChildValue && childSummaries[field.name]"
class="child-node-summary"
>
{{ childSummaries[field.name] }}
</div>
</div>
</div>
</template>
<template v-else-if="hasChildRoutes">
<!-- Logic for expanded inline config will go here in FormGenerator usage or via prop controls -->
<!-- But for now, we implement standard behavior. The expanding logic will be handled by the parent using slots or a different prop -->
<!-- Standard Popup Style (if not expanded) -->
<div v-if="!expandInline" class="form-group form-group-inline child-node-group">
<label>
{{ $te('schema.' + field.name) ? $t('schema.' + field.name) : (field.displayName || field.name) }}
<span v-if="field.required" class="required-asterisk">*</span>
<RichTooltip
v-if="field.description"
:content="{ description: $te('schema_desc.' + field.name) ? $t('schema_desc.' + field.name) : field.description }"
placement="top"
>
<span class="help-icon" tabindex="0">?</span>
</RichTooltip>
</label>
<div class="child-node-container">
<div class="child-node-controls">
<button
@click="$emit('open-conditional-child-modal', field)"
class="add-child-button"
:disabled="!canOpenConditionalChildModal"
>
<span class="plus-icon" aria-hidden="true">
<!-- Edit icon when a child config already exists -->
<svg
v-if="formData[field.name]"
width="16"
height="16"
viewBox="0 0 16 16"
xmlns="http://www.w3.org/2000/svg"
>
<rect
x="3"
y="2"
width="8"
height="11"
rx="1"
ry="1"
fill="none"
stroke="currentColor"
stroke-width="1.2"
/>
<path
fill="currentColor"
d="M10.9 3.1a.9.9 0 0 1 1.27 0l.73.73a.9.9 0 0 1 0 1.27l-4.3 4.3-1.7.5.5-1.7 4.3-4.3z"
/>
</svg>
<!-- Configure/Add icon when not yet configured -->
<svg
v-else
width="16"
height="16"
viewBox="0 0 16 16"
xmlns="http://www.w3.org/2000/svg"
>
<path
fill="currentColor"
d="M8 2.5a.75.75 0 0 1 .75.75v4h4a.75.75 0 0 1 0 1.5h-4v4a.75.75 0 0 1-1.5 0v-4h-4a.75.75 0 0 1 0-1.5h4v-4A.75.75 0 0 1 8 2.5z"
/>
</svg>
</span>
{{ conditionalChildButtonLabel }}
</button>
<button
v-if="hasChildValue"
class="clear-child-button"
@click.stop="$emit('clear-child-entry', field.name)"
title="Clear configuration"
>
×
</button>
</div>
<div
v-if="!canOpenConditionalChildModal"
class="child-node-hint"
>
{{ $t('dynamic_form_field.select_type_and_configure') }}
</div>
<div
v-if="hasChildValue && childSummaries[field.name]"
class="child-node-summary"
>
{{ childSummaries[field.name] }}
</div>
</div>
</div>
<!-- Expanded Inline Style is NOT rendered here. The parent will check `expandInline`
and simply NOT render this button-based UI, and instead render the inline form.
Wait, if I am extracting this, I should support both or let parent handle the expanded part adjacent to this component?
The requirement says: "display type selection" (which is a standard field),
then "display expanded config below".
So the "config" field (which is `hasChildRoutes`) should be rendered DIFFERENTLY if expanded.
If it's expanded, we might just want to render NOTHING for this field here,
and let the parent render the separator + sub-form?
OR we render the separator + sub-form HERE?
But complex recursion logic is easier in parent.
DECISION: If `expandInline` is true, this component renders NOTHING for the `childRoutes` field,
because the content is rendered by the parent (the "separator" + "inline fields").
Actually, the `type` field is a separate field. This component renders it.
The `config` field IS the field with `childRoutes`.
So if `expandInline` is true, we render NOTHING?
Yes. The parent will detect this field and render the specialized inline UI instead.
However, I need to make sure I don't break existing logic.
If I pass `expandInline` prop:
-->
<div v-else-if="expandInline">
<!-- Render nothing, parent handles it -->
</div>
</template>
<template v-else>
<!-- Choice dropdown fields-->
<div v-if="field.enum && !isList" class="form-group">
<label :for="`${modalId}-${field.name}`">
{{ $te('schema.' + field.name) ? $t('schema.' + field.name) : (field.displayName || field.name) }}
<span v-if="field.required" class="required-asterisk">*</span>
<RichTooltip
v-if="field.description"
:content="{ description: $te('schema_desc.' + field.name) ? $t('schema_desc.' + field.name) : field.description }"
placement="top"
>
<span class="help-icon" tabindex="0">?</span>
</RichTooltip>
</label>
<div ref="selectWrapper" class="custom-select-wrapper" :class="{ 'select-disabled': isReadOnly }" @click="toggleDropdown">
<input
:id="`${modalId}-${field.name}`"
:value="inputValue"
class="form-input custom-select-input"
readonly
:class="{'input-readonly': isReadOnly}"
:placeholder="$t('dynamic_form_field.type_to_filter')"
autocomplete="off"
/>
<Teleport to="body">
<div v-if="showDropdown && !isReadOnly" class="custom-select-dropdown" :style="dropdownStyle">
<div
v-if="!field.required"
class="custom-select-option"
:class="{ 'option-selected': formData[field.name] === null }"
@mousedown.stop="selectOption(null)"
>
None
</div>
<div
v-for="option in filteredOptions"
:key="typeof option === 'string' ? option : option.value"
class="custom-select-option"
:class="{ 'option-selected': formData[field.name] === (typeof option === 'string' ? option : option.value) }"
:title="typeof option === 'string' ? '' : (option.description || '')"
@mousedown.stop="selectOption(typeof option === 'string' ? option : option.value)"
>
{{
typeof option === 'string'
? option
: (option.label || option.value)
}}
</div>
<div v-if="filteredOptions.length === 0" class="custom-select-no-results">
{{ $t('dynamic_form_field.no_options_found') }}
</div>
</div>
</Teleport>
<div class="custom-select-arrow" :class="{ 'arrow-open': showDropdown }">
</div>
</div>
</div>
<!-- Multi-choice fields -->
<div v-else-if="field.enum && isList" class="form-group">
<label>
{{ $te('schema.' + field.name) ? $t('schema.' + field.name) : (field.displayName || field.name) }}
<span v-if="field.required" class="required-asterisk">*</span>
<RichTooltip
v-if="field.description"
:content="{ description: $te('schema_desc.' + field.name) ? $t('schema_desc.' + field.name) : field.description }"
placement="top"
>
<span class="help-icon" tabindex="0">?</span>
</RichTooltip>
</label>
<div class="multi-select-options">
<label
v-for="option in (field.enumOptions || field.enum)"
:key="typeof option === 'string' ? option : option.value"
class="multi-select-option"
:title="typeof option === 'string' ? '' : (option.description || '')"
>
<input
type="checkbox"
:value="typeof option === 'string' ? option : option.value"
v-model="internalFormData[field.name]"
/>
<span class="option-label">
{{
typeof option === 'string'
? option
: (option.label || option.value)
}}
</span>
</label>
</div>
</div>
<!-- Boolean switch fields -->
<div v-else-if="field.type === 'bool' && !isList" class="form-group">
<div class="switch-wrapper">
<label :for="`${modalId}-${field.name}`" class="switch-label-text">
{{ $te('schema.' + field.name) ? $t('schema.' + field.name) : (field.displayName || field.name) }}
<span v-if="field.required" class="required-asterisk">*</span>
<RichTooltip
v-if="field.description"
:content="{ description: $te('schema_desc.' + field.name) ? $t('schema_desc.' + field.name) : field.description }"
placement="top"
>
<span class="help-icon" tabindex="0">?</span>
</RichTooltip>
</label>
<label class="switch-container">
<input
type="checkbox"
:id="`${modalId}-${field.name}`"
:checked="formData[field.name] === true"
@change="onBooleanSwitchChange($event.target.checked)"
:disabled="isReadOnly"
/>
<span class="switch-slider"></span>
</label>
</div>
</div>
<!-- String input fields -->
<div v-else-if="field.type === 'str' && !isList" class="form-group">
<label :for="`${modalId}-${field.name}`">
{{ $te('schema.' + field.name) ? $t('schema.' + field.name) : (field.displayName || field.name) }}
<span v-if="field.required" class="required-asterisk">*</span>
<RichTooltip
v-if="field.description"
:content="{ description: $te('schema_desc.' + field.name) ? $t('schema_desc.' + field.name) : field.description }"
placement="top"
>
<span class="help-icon" tabindex="0">?</span>
</RichTooltip>
</label>
<input
:id="`${modalId}-${field.name}`"
:value="formData[field.name]"
@input="onInput($event.target.value)"
type="text"
class="form-input"
:readonly="isReadOnly"
:class="{'input-readonly': isReadOnly}"
/>
</div>
<!-- Multiline text fields -->
<div v-else-if="field.type === 'text' && !isList" class="form-group">
<label :for="`${modalId}-${field.name}`">
{{ $te('schema.' + field.name) ? $t('schema.' + field.name) : (field.displayName || field.name) }}
<span v-if="field.required" class="required-asterisk">*</span>
<RichTooltip
v-if="field.description"
:content="{ description: $te('schema_desc.' + field.name) ? $t('schema_desc.' + field.name) : field.description }"
placement="top"
>
<span class="help-icon" tabindex="0">?</span>
</RichTooltip>
</label>
<textarea
:id="`${modalId}-${field.name}`"
:value="formData[field.name]"
@input="onInput($event.target.value)"
class="form-textarea"
rows="4"
:readonly="isReadOnly"
:class="{'input-readonly': isReadOnly}"
/>
</div>
<!-- Integer input fields -->
<div v-else-if="field.type === 'int' && !isList" class="form-group">
<label :for="`${modalId}-${field.name}`">
{{ $te('schema.' + field.name) ? $t('schema.' + field.name) : (field.displayName || field.name) }}
<span v-if="field.required" class="required-asterisk">*</span>
<RichTooltip
v-if="field.description"
:content="{ description: $te('schema_desc.' + field.name) ? $t('schema_desc.' + field.name) : field.description }"
placement="top"
>
<span class="help-icon" tabindex="0">?</span>
</RichTooltip>
</label>
<input
:id="`${modalId}-${field.name}`"
:value="formData[field.name]"
@input="onInputNumber($event.target.value)"
type="number"
step="1"
class="form-input"
:readonly="isReadOnly"
:class="{'input-readonly': isReadOnly}"
/>
</div>
<!-- Float input fields -->
<div v-else-if="field.type === 'float' && !isList" class="form-group">
<label :for="`${modalId}-${field.name}`">
{{ $te('schema.' + field.name) ? $t('schema.' + field.name) : (field.displayName || field.name) }}
<span v-if="field.required" class="required-asterisk">*</span>
<RichTooltip
v-if="field.description"
:content="{ description: $te('schema_desc.' + field.name) ? $t('schema_desc.' + field.name) : field.description }"
placement="top"
>
<span class="help-icon" tabindex="0">?</span>
</RichTooltip>
</label>
<input
:id="`${modalId}-${field.name}`"
:value="formData[field.name]"
@input="onInputNumber($event.target.value)"
type="number"
step="any"
class="form-input"
:readonly="isReadOnly"
:class="{'input-readonly': isReadOnly}"
/>
</div>
<!-- Key-value fields -->
<div v-else-if="field.type === 'dict[str, Any]' || field.type === 'dict[str, str]'" class="form-group form-group-inline">
<label>
{{ $te('schema.' + field.name) ? $t('schema.' + field.name) : (field.displayName || field.name) }}
<span v-if="field.required" class="required-asterisk">*</span>
<RichTooltip
v-if="field.description"
:content="{ description: $te('schema_desc.' + field.name) ? $t('schema_desc.' + field.name) : field.description }"
placement="top"
>
<span class="help-icon" tabindex="0">?</span>
</RichTooltip>
</label>
<div class="vars-container">
<button @click="$emit('open-var-modal', field.name)" class="add-var-button">
<span class="plus-icon" aria-hidden="true">
<svg
width="16"
height="16"
viewBox="0 0 16 16"
xmlns="http://www.w3.org/2000/svg"
>
<path
fill="currentColor"
d="M8 2.5a.75.75 0 0 1 .75.75v4h4a.75.75 0 0 1 0 1.5h-4v4a.75.75 0 0 1-1.5 0v-4h-4a.75.75 0 0 1 0-1.5h4v-4A.75.75 0 0 1 8 2.5z"
/>
</svg>
</span>
{{ $t('dynamic_form_field.add_key_value') }}
</button>
<div
v-if="formData[field.name] && hasDictEntries(formData[field.name])"
class="vars-list"
>
<div
v-for="(varValue, varKey) in formData[field.name]"
:key="varKey"
class="var-item"
@click="$emit('edit-var', field.name, varKey)"
>
<span class="var-name">{{ varKey }}</span>
<span class="var-separator">|</span>
<span class="var-value">{{ varValue }}</span>
<button
class="delete-var-button"
@click.stop="$emit('delete-var', field.name, varKey)"
:title="$t('dynamic_form_field.delete_variable')"
>
×
</button>
</div>
</div>
</div>
</div>
<!-- List of strings field -->
<div v-else-if="isList && field.type.includes('str')" class="form-group form-group-inline">
<label>
{{ $te('schema.' + field.name) ? $t('schema.' + field.name) : (field.displayName || field.name) }}
<span v-if="field.required" class="required-asterisk">*</span>
<RichTooltip
v-if="field.description"
:content="{ description: $te('schema_desc.' + field.name) ? $t('schema_desc.' + field.name) : field.description }"
placement="top"
>
<span class="help-icon" tabindex="0">?</span>
</RichTooltip>
</label>
<div class="list-container">
<button @click="$emit('open-list-item-modal', field.name)" class="add-list-button">
<span class="plus-icon" aria-hidden="true">
<svg
width="16"
height="16"
viewBox="0 0 16 16"
xmlns="http://www.w3.org/2000/svg"
>
<path
fill="currentColor"
d="M8 2.5a.75.75 0 0 1 .75.75v4h4a.75.75 0 0 1 0 1.5h-4v4a.75.75 0 0 1-1.5 0v-4h-4a.75.75 0 0 1 0-1.5h4v-4A.75.75 0 0 1 8 2.5z"
/>
</svg>
</span>
Add Entry
</button>
<div v-if="formData[field.name] && formData[field.name].length > 0" class="list-items">
<div
v-for="(item, index) in formData[field.name]"
:key="index"
class="list-item"
>
<span class="item-value">{{ item }}</span>
<button
class="delete-item-button"
@click.stop="$emit('delete-list-item', field.name, index)"
:title="$t('dynamic_form_field.delete_item')"
>
×
</button>
</div>
</div>
</div>
</div>
</template>
</div>
</template>
<script setup>
import { computed, ref, reactive, watch, onUnmounted } from 'vue'
import { useI18n } from 'vue-i18n'
import RichTooltip from './RichTooltip.vue'
const { t } = useI18n()
const props = defineProps({
field: {
type: Object,
required: true
},
modalId: {
type: String,
required: true
},
formData: {
type: Object,
required: true
},
childSummaries: {
type: Object,
default: () => ({})
},
recursive: {
type: Boolean,
default: false
},
isVisible: {
type: Boolean,
default: true
},
// If true, and this field is a childRoute config, render nothing (handled by parent inline)
expandInline: {
type: Boolean,
default: false
},
// Helpers passed from parent or reimplemented
canOpenConditionalChildModal: {
type: Boolean,
default: true
},
conditionalChildButtonLabel: {
type: String,
default: 'Configure'
},
activeChildRoute: {
type: Object,
default: null
},
isReadOnly: {
type: Boolean,
default: false
}
})
const isReadOnly = computed(() => props.isReadOnly)
const emit = defineEmits([
'update:form-data',
'open-child-modal',
'clear-child-entry',
'delete-child-entry',
'open-conditional-child-modal',
'handle-enum-change',
'open-var-modal',
'edit-var',
'delete-var',
'open-list-item-modal',
'delete-list-item'
])
// Computed property to allow v-model binding with mutation of prop object (common in complex forms)
// OR better, just use props.formData[field.name] directly as Vue 3 proxies allow it, though strict mode warns.
// For FormGenerator, we generally mutate the reactive modal object.
const internalFormData = computed(() => props.formData)
// Custom select filtering state
const showDropdown = ref(false)
const filterText = ref('')
const selectWrapper = ref(null)
const dropdownRect = reactive({
top: 0,
left: 0,
width: 0
})
const updateDropdownPosition = () => {
if (selectWrapper.value) {
const rect = selectWrapper.value.getBoundingClientRect()
dropdownRect.top = rect.bottom
dropdownRect.left = rect.left
dropdownRect.width = rect.width
}
}
const dropdownStyle = computed(() => ({
position: 'fixed',
top: `${dropdownRect.top + 4}px`,
left: `${dropdownRect.left}px`,
width: `${dropdownRect.width}px`,
zIndex: 10000
}))
const toggleDropdown = () => {
if (props.isReadOnly) return
if (!showDropdown.value) {
updateDropdownPosition()
showDropdown.value = true
// Add escape listener
window.addEventListener('scroll', closeOnScroll, true)
window.addEventListener('mousedown', handleOutsideClick)
} else {
closeDropdown()
}
}
const closeDropdown = () => {
showDropdown.value = false
window.removeEventListener('scroll', closeOnScroll, true)
window.removeEventListener('mousedown', handleOutsideClick)
}
const closeOnScroll = () => {
if (showDropdown.value) closeDropdown()
}
const handleOutsideClick = (e) => {
if (selectWrapper.value && !selectWrapper.value.contains(e.target)) {
closeDropdown()
}
}
const isList = computed(() => {
return props.field.type && props.field.type.includes('list[')
})
const hasChildRoutes = computed(() => {
return Array.isArray(props.field?.childRoutes) && props.field.childRoutes.length > 0
})
const hasChildValue = computed(() => {
const value = props.formData?.[props.field.name]
if (Array.isArray(value)) {
return value.length > 0
}
return value !== null && value !== undefined && value !== ''
})
const childNodeButtonLabel = computed(() => {
if (isList.value) {
return t('dynamic_form_field.add_entry')
}
return props.formData[props.field.name] ? t('dynamic_form_field.edit_child', { child: props.field.childNode }) : t('dynamic_form_field.configure_child', { child: props.field.childNode })
})
// Filtered options for custom select
const filteredOptions = computed(() => {
const options = props.field.enumOptions || props.field.enum || []
if (!filterText.value.trim()) {
return options
}
const filter = filterText.value.toLowerCase()
return options.filter(option => {
const label = typeof option === 'string' ? option : (option.label || option.value)
return label.toLowerCase().includes(filter)
})
})
// Computed input value for custom select
const inputValue = computed(() => {
if (showDropdown.value) {
return filterText.value
}
return getSelectedLabel()
})
// Helpers
const hasDictEntries = (value) => {
if (!value || typeof value !== 'object' || Array.isArray(value)) {
return false
}
return Object.keys(value).length > 0
}
const describeChildEntry = (entry, index) => {
if (entry && typeof entry === 'object') {
if (entry.id) return entry.id
if (entry.name) return entry.name
if (entry.type) return entry.type
}
return `${props.field.childNode} #${index + 1}`
}
// Input handlers
const onInput = (value) => {
props.formData[props.field.name] = value
}
const onInputNumber = (value) => {
// Check if empty string
if (value === "") {
props.formData[props.field.name] = null
return
}
props.formData[props.field.name] = Number(value)
}
const onBooleanSwitchChange = (checked) => {
props.formData[props.field.name] = checked
}
// Custom select methods
const onFilterInput = (event) => {
filterText.value = event.target.value
showDropdown.value = true
}
const handleInputBlur = () => {
// Delay hiding dropdown to allow option selection
setTimeout(() => {
showDropdown.value = false
filterText.value = ''
}, 200)
}
const selectOption = (value) => {
props.formData[props.field.name] = value
closeDropdown()
emit('handle-enum-change', props.field)
}
const getSelectedLabel = () => {
const currentValue = props.formData[props.field.name]
if (currentValue === null || currentValue === undefined) {
return ''
}
const options = props.field.enumOptions || props.field.enum || []
const selectedOption = options.find(option => {
const optionValue = typeof option === 'string' ? option : option.value
return optionValue === currentValue
})
if (selectedOption) {
return typeof selectedOption === 'string'
? selectedOption
: (selectedOption.label || selectedOption.value)
}
return currentValue
}
</script>
<style scoped>
.form-field {
margin-bottom: 18px;
}
.form-group {
margin-bottom: 18px;
}
.form-group label {
display: block;
margin-bottom: 6px;
color: #f2f2f2;
font-weight: 500;
font-size: 13px;
}
.form-group-inline {
display: flex;
align-items: flex-start;
gap: 14px;
}
.form-group-inline label {
margin-bottom: 0;
flex-shrink: 0;
min-width: 96px;
}
.form-group-inline > *:not(label) {
flex: 1;
}
.required-asterisk {
color: #ff6b6b;
margin-left: 4px;
}
.help-icon {
display: inline-flex;
align-items: center;
justify-content: center;
width: 14px;
height: 14px;
margin-left: 6px;
border-radius: 999px;
border: 1px solid rgba(255, 255, 255, 0.3);
font-size: 11px;
line-height: 1;
cursor: default;
color: rgba(242, 242, 242, 0.7);
background-color: transparent;
transition: all 0.2s ease;
}
.help-icon:hover {
border-color: #a0c4ff;
color: #a0c4ff;
}
.form-input,
.form-select,
.form-textarea,
.multi-select-options input[type='checkbox'] {
font-family: 'Inter', sans-serif;
}
.form-input {
width: 100%;
padding: 9px 11px;
border-radius: 8px;
border: 1px solid rgba(255, 255, 255, 0.14);
background-color: rgba(10, 10, 10, 0.6);
color: #f2f2f2;
font-size: 13px;
box-sizing: border-box;
transition: border-color 0.2s ease, background-color 0.2s ease, box-shadow 0.2s ease;
}
.form-textarea {
width: 100%;
padding: 9px 11px;
border-radius: 8px;
border: 1px solid rgba(255, 255, 255, 0.14);
background-color: rgba(10, 10, 10, 0.6);
color: #f2f2f2;
font-size: 13px;
box-sizing: border-box;
min-height: 80px;
resize: vertical;
transition: border-color 0.2s ease, background-color 0.2s ease, box-shadow 0.2s ease;
}
.form-textarea:focus {
outline: none;
border-color: #a0c4ff;
background-color: rgba(15, 15, 15, 0.85);
box-shadow: 0 0 0 1px rgba(160, 196, 255, 0.5);
}
.form-input:focus {
outline: none;
border-color: #a0c4ff;
background-color: rgba(15, 15, 15, 0.85);
box-shadow: 0 0 0 1px rgba(160, 196, 255, 0.5);
}
.form-input::placeholder {
color: rgba(255, 255, 255, 0.35);
}
.form-select {
width: 100%;
padding: 9px 11px;
border-radius: 8px;
border: 1px solid rgba(255, 255, 255, 0.14);
background-color: rgba(10, 10, 10, 0.6);
color: #f2f2f2;
font-size: 13px;
box-sizing: border-box;
cursor: pointer;
transition: border-color 0.2s ease, background-color 0.2s ease;
}
.form-select:focus {
outline: none;
border-color: #a0c4ff;
background-color: rgba(15, 15, 15, 0.85);
}
/* Switch Styles */
.switch-wrapper {
display: flex;
justify-content: space-between;
align-items: center;
min-height: 38px; /* Match input height roughly */
}
.switch-label-text {
margin-bottom: 0 !important; /* Override label margin */
}
.switch-container {
position: relative;
display: inline-block;
width: 44px;
height: 24px;
margin-left: 12px;
}
.switch-container input {
opacity: 0;
width: 0;
height: 0;
}
.switch-slider {
position: absolute;
cursor: pointer;
top: 0;
left: 0;
right: 0;
bottom: 0;
background-color: rgba(255, 255, 255, 0.14);
transition: .4s;
border-radius: 24px;
border: 1px solid rgba(255, 255, 255, 0.14);
}
.switch-slider:before {
position: absolute;
content: "";
height: 18px;
width: 18px;
left: 2px;
bottom: 2px;
background-color: white;
transition: .4s;
border-radius: 50%;
}
input:checked + .switch-slider {
background-color: #a0c4ff;
border-color: #4facfe;
}
input:focus + .switch-slider {
box-shadow: 0 0 1px #4facfe;
}
input:checked + .switch-slider:before {
transform: translateX(20px);
}
.form-select option {
background-color: #252525;
color: #f2f2f2;
}
.multi-select-options {
display: flex;
flex-direction: column;
gap: 6px;
}
.multi-select-option {
display: inline-flex;
align-items: center;
gap: 6px;
padding: 6px 10px;
border-radius: 999px;
background-color: rgba(255, 255, 255, 0.04);
border: 1px solid rgba(255, 255, 255, 0.14);
font-size: 12px;
color: rgba(242, 242, 242, 0.9);
cursor: pointer;
transition: background-color 0.2s ease, border-color 0.2s ease;
}
.multi-select-option:hover {
background-color: rgba(255, 255, 255, 0.08);
border-color: rgba(160, 196, 255, 0.6);
}
.option-label {
white-space: nowrap;
}
/* Variables styles */
.vars-container {
display: flex;
flex-direction: column;
gap: 10px;
}
.input-readonly {
background-color: rgba(255, 255, 255, 0.05); /* Slightly lighter background to indicate disabled-ish but readable */
border-color: rgba(255, 255, 255, 0.05);
cursor: text; /* Allow text selection */
color: rgba(242, 242, 242, 0.6);
}
.input-readonly:focus {
border-color: rgba(255, 255, 255, 0.05);
box-shadow: none;
}
.vars-list {
display: flex;
flex-direction: column;
gap: 8px;
}
.var-name {
flex: 0 0 auto;
font-weight: 600;
min-width: 90px;
margin-left: 10px;
font-size: 12px;
}
.var-separator {
font-size: 12px;
color: #858585;
margin: 0 5px;
}
.var-value {
flex: 1;
color: #d2d2d2;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
font-size: 13px;
}
.delete-var-button {
background: transparent;
border: none;
color: #999;
font-size: 20px;
cursor: pointer;
padding: 0;
width: 24px;
height: 24px;
display: flex;
align-items: center;
justify-content: center;
transition: color 0.2s ease;
line-height: 1;
margin-left: 8px;
}
.delete-var-button:hover {
color: #ff6b6b;
}
/* List items styles */
.list-container {
display: flex;
flex-direction: column;
gap: 10px;
}
.list-items {
display: flex;
flex-direction: column;
gap: 8px;
}
.var-item,
.list-item {
display: flex;
align-items: center;
justify-content: space-between;
max-width: 420px;
padding: 7px 12px;
background-color: rgba(90, 90, 90, 0.9);
border: 1px solid rgba(255, 255, 255, 0.14);
border-radius: 10px;
color: #f2f2f2;
cursor: pointer;
transition: all 0.2s ease;
font-size: 13px;
gap: 12px;
}
.var-item:hover,
.list-item:hover {
background-color: rgba(110, 110, 110, 0.95);
border-color: rgba(160, 196, 255, 0.7);
}
.item-value {
flex: 1;
}
.delete-item-button {
background: transparent;
border: none;
color: #999;
font-size: 20px;
cursor: pointer;
padding: 0;
width: 24px;
height: 24px;
display: flex;
align-items: center;
justify-content: center;
transition: color 0.2s ease;
line-height: 1;
margin-left: 8px;
}
.delete-item-button:hover {
color: #ff6b6b;
}
.child-node-container {
display: flex;
flex-direction: column;
gap: 10px;
width: 100%;
}
.child-node-controls {
display: flex;
align-items: center;
gap: 5px;
}
.clear-child-button {
background: transparent;
border: none;
color: #999;
font-size: 18px;
cursor: pointer;
padding: 0;
width: 24px;
height: 24px;
display: flex;
align-items: center;
justify-content: center;
transition: color 0.2s ease;
line-height: 1;
}
.clear-child-button:hover {
color: #ff6b6b;
}
.add-child-button,
.add-list-button,
.add-var-button {
position: relative;
display: inline-flex;
align-items: center;
gap: 8px;
padding: 6px 12px;
border-radius: 10px;
border: 1px solid rgba(255, 255, 255, 0.16);
background-color: rgba(255, 255, 255, 0.14);
color: rgba(242, 242, 242, 0.9);
cursor: pointer;
font-size: 13px;
transition: all 0.2s ease;
}
.add-child-button:hover,
.add-list-button:hover,
.add-var-button:hover {
background-color: rgba(255, 255, 255, 0.08);
}
.add-child-button .plus-icon,
.add-list-button .plus-icon,
.add-var-button .plus-icon {
display: inline-flex;
align-items: center;
justify-content: center;
line-height: 0;
}
.add-child-button .plus-icon svg,
.add-list-button .plus-icon svg,
.add-var-button .plus-icon svg {
display: block;
}
.add-child-button:disabled {
opacity: 0.5;
cursor: not-allowed;
border-color: rgba(255, 255, 255, 0.1);
background-color: rgba(74, 74, 74, 0.8);
color: rgba(200, 200, 200, 0.7);
}
.child-node-list {
display: flex;
flex-direction: column;
gap: 8px;
}
.child-node-item {
display: flex;
align-items: center;
justify-content: space-between;
padding: 9px 14px;
background-color: rgba(90, 90, 90, 0.9);
border: 1px solid rgba(255, 255, 255, 0.14);
border-radius: 10px;
color: #f2f2f2;
font-size: 13px;
gap: 12px;
cursor: pointer;
transition: background-color 0.2s ease, border-color 0.2s ease;
}
.child-node-item:hover {
background-color: rgba(110, 110, 110, 0.95);
border-color: rgba(160, 196, 255, 0.7);
}
.child-node-name {
flex: 1;
}
.child-node-actions {
display: flex;
gap: 8px;
}
.child-node-item-wrapper {
display: flex;
flex-direction: column;
gap: 6px;
}
.child-node-summary {
padding: 6px 12px;
margin-left: 0;
background-color: rgba(60, 60, 60, 0.6);
border: 1px solid rgba(255, 255, 255, 0.08);
border-radius: 8px;
color: rgba(200, 200, 200, 0.85);
font-size: 12px;
line-height: 1.4;
word-wrap: break-word;
overflow-wrap: break-word;
white-space: pre-line;
}
.edit-child-button,
.delete-child-button {
padding: 6px 10px;
border-radius: 999px;
border: none;
cursor: pointer;
font-size: 12px;
font-weight: 600;
transition: background-color 0.2s ease, color 0.2s ease;
}
.edit-child-button {
background-color: rgba(117, 117, 117, 0.95);
color: #f2f2f2;
}
.edit-child-button:hover {
background-color: rgba(136, 136, 136, 0.98);
}
.delete-child-button {
background-color: rgba(90, 90, 90, 0.9);
color: #f2f2f2;
}
.delete-child-button:hover {
background-color: rgba(110, 110, 110, 0.95);
}
.child-node-hint {
padding: 6px 8px;
color: rgba(189, 189, 189, 0.9);
font-size: 12px;
}
/* Custom select styles */
.custom-select-wrapper {
position: relative;
height: 38px; /* Fixed height to prevent absolute child from stretching parent */
}
.custom-select-input {
cursor: pointer;
height: 100%;
}
.custom-select-input:focus {
cursor: text;
}
.custom-select-arrow {
position: absolute;
right: 12px;
margin-left: 8px;
}
.delete-item-button:hover {
color: #ff6b6b;
}
.child-node-container {
display: flex;
flex-direction: column;
gap: 10px;
width: 100%;
}
.child-node-controls {
display: flex;
align-items: center;
gap: 5px;
}
.clear-child-button {
background: transparent;
border: none;
color: #999;
font-size: 18px;
cursor: pointer;
padding: 0;
width: 24px;
height: 24px;
display: flex;
align-items: center;
justify-content: center;
transition: color 0.2s ease;
line-height: 1;
}
.clear-child-button:hover {
color: #ff6b6b;
}
.add-child-button,
.add-list-button,
.add-var-button {
position: relative;
display: inline-flex;
align-items: center;
gap: 8px;
padding: 6px 12px;
border-radius: 10px;
border: 1px solid rgba(255, 255, 255, 0.16);
background-color: rgba(255, 255, 255, 0.14);
color: rgba(242, 242, 242, 0.9);
cursor: pointer;
font-size: 13px;
transition: all 0.2s ease;
}
.add-child-button:hover,
.add-list-button:hover,
.add-var-button:hover {
background-color: rgba(255, 255, 255, 0.08);
}
.add-child-button .plus-icon,
.add-list-button .plus-icon,
.add-var-button .plus-icon {
display: inline-flex;
align-items: center;
justify-content: center;
line-height: 0;
}
.add-child-button .plus-icon svg,
.add-list-button .plus-icon svg,
.add-var-button .plus-icon svg {
display: block;
}
.add-child-button:disabled {
opacity: 0.5;
cursor: not-allowed;
border-color: rgba(255, 255, 255, 0.1);
background-color: rgba(74, 74, 74, 0.8);
color: rgba(200, 200, 200, 0.7);
}
.child-node-list {
display: flex;
flex-direction: column;
gap: 8px;
}
.child-node-item {
display: flex;
align-items: center;
justify-content: space-between;
padding: 9px 14px;
background-color: rgba(90, 90, 90, 0.9);
border: 1px solid rgba(255, 255, 255, 0.14);
border-radius: 10px;
color: #f2f2f2;
font-size: 13px;
gap: 12px;
cursor: pointer;
transition: background-color 0.2s ease, border-color 0.2s ease;
}
.child-node-item:hover {
background-color: rgba(110, 110, 110, 0.95);
border-color: rgba(160, 196, 255, 0.7);
}
.child-node-name {
flex: 1;
}
.child-node-actions {
display: flex;
gap: 8px;
}
.child-node-item-wrapper {
display: flex;
flex-direction: column;
gap: 6px;
}
.child-node-summary {
padding: 6px 12px;
margin-left: 0;
background-color: rgba(60, 60, 60, 0.6);
border: 1px solid rgba(255, 255, 255, 0.08);
border-radius: 8px;
color: rgba(200, 200, 200, 0.85);
font-size: 12px;
line-height: 1.4;
word-wrap: break-word;
overflow-wrap: break-word;
white-space: pre-line;
}
.edit-child-button,
.delete-child-button {
padding: 6px 10px;
border-radius: 999px;
border: none;
cursor: pointer;
font-size: 12px;
font-weight: 600;
transition: background-color 0.2s ease, color 0.2s ease;
}
.edit-child-button {
background-color: rgba(117, 117, 117, 0.95);
color: #f2f2f2;
}
.edit-child-button:hover {
background-color: rgba(136, 136, 136, 0.98);
}
.delete-child-button {
background-color: rgba(90, 90, 90, 0.9);
color: #f2f2f2;
}
.delete-child-button:hover {
background-color: rgba(110, 110, 110, 0.95);
}
.child-node-hint {
padding: 6px 8px;
color: rgba(189, 189, 189, 0.9);
font-size: 12px;
}
/* Custom select styles */
.custom-select-wrapper {
position: relative;
height: 38px; /* Fixed height to prevent absolute child from stretching parent */
}
.custom-select-input {
cursor: pointer;
height: 100%;
}
.custom-select-input:focus {
cursor: text;
}
.custom-select-arrow {
position: absolute;
right: 12px;
top: 50%;
transform: translateY(-50%);
color: rgba(242, 242, 242, 0.7);
font-size: 12px;
pointer-events: none;
transition: transform 0.2s ease;
}
.custom-select-arrow.arrow-open {
transform: translateY(-50%) rotate(180deg);
}
:global(.custom-select-dropdown) {
position: fixed;
z-index: 10000;
max-height: 200px;
overflow-y: auto;
background-color: #1e1e1e;
border: 1px solid rgba(255, 255, 255, 0.2);
border-radius: 8px;
box-shadow: 0 10px 30px rgba(0, 0, 0, 0.8);
box-sizing: border-box;
}
:global(.custom-select-option) {
padding: 10px 14px;
cursor: pointer;
color: #f2f2f2;
font-size: 13px;
transition: background-color 0.2s ease;
}
:global(.custom-select-option:hover) {
background-color: rgba(255, 255, 255, 0.1);
}
:global(.custom-select-option.option-selected) {
background-color: rgba(160, 196, 255, 0.2);
color: #a0c4ff;
font-weight: 500;
}
:global(.custom-select-no-results) {
padding: 10px 12px;
color: rgba(189, 189, 189, 0.7);
font-size: 12px;
text-align: center;
font-style: italic;
}
.select-disabled .custom-select-input {
cursor: not-allowed;
}
</style>