mirror of
https://github.com/willmiao/ComfyUI-Lora-Manager.git
synced 2026-03-24 14:42:11 -03:00
feat(testing): enhance test configuration and add Vue component tests
- Update package.json test script to run both JS and Vue tests - Simplify LoraCyclerLM output by removing redundant lora name fallback - Extend Vitest config to include TypeScript test files - Add Vue testing dependencies and setup for component testing - Implement comprehensive test suite for BatchQueueSimulator component - Add test setup file with global mocks for ComfyUI modules
This commit is contained in:
@@ -10,10 +10,18 @@
|
||||
:use-custom-clip-range="state.useCustomClipRange.value"
|
||||
:is-clip-strength-disabled="state.isClipStrengthDisabled.value"
|
||||
:is-loading="state.isLoading.value"
|
||||
:repeat-count="state.repeatCount.value"
|
||||
:repeat-used="state.displayRepeatUsed.value"
|
||||
:is-paused="state.isPaused.value"
|
||||
:is-workflow-executing="state.isWorkflowExecuting.value"
|
||||
:executing-repeat-step="state.executingRepeatStep.value"
|
||||
@update:current-index="handleIndexUpdate"
|
||||
@update:model-strength="state.modelStrength.value = $event"
|
||||
@update:clip-strength="state.clipStrength.value = $event"
|
||||
@update:use-custom-clip-range="handleUseCustomClipRangeChange"
|
||||
@update:repeat-count="handleRepeatCountChange"
|
||||
@toggle-pause="handleTogglePause"
|
||||
@reset-index="handleResetIndex"
|
||||
@refresh="handleRefresh"
|
||||
/>
|
||||
</div>
|
||||
@@ -31,6 +39,7 @@ type CyclerWidget = ComponentWidget<CyclerConfig>
|
||||
const props = defineProps<{
|
||||
widget: CyclerWidget
|
||||
node: { id: number; inputs?: any[]; widgets?: any[]; graph?: any }
|
||||
api?: any // ComfyUI API for execution events
|
||||
}>()
|
||||
|
||||
// State management
|
||||
@@ -39,6 +48,35 @@ const state = useLoraCyclerState(props.widget)
|
||||
// Symbol to track if the widget has been executed at least once
|
||||
const HAS_EXECUTED = Symbol('HAS_EXECUTED')
|
||||
|
||||
// Execution context queue for batch queue synchronization
|
||||
// In batch queue mode, all beforeQueued calls happen BEFORE any onExecuted calls,
|
||||
// so we need to snapshot the state at queue time and replay it during execution
|
||||
interface ExecutionContext {
|
||||
isPaused: boolean
|
||||
repeatUsed: number
|
||||
repeatCount: number
|
||||
shouldAdvanceDisplay: boolean
|
||||
displayRepeatUsed: number // Value to show in UI after completion
|
||||
}
|
||||
const executionQueue: ExecutionContext[] = []
|
||||
|
||||
// Track pending executions for batch queue support (deferred UI updates)
|
||||
// Uses FIFO order since executions are processed in the order they were queued
|
||||
interface PendingExecution {
|
||||
repeatUsed: number
|
||||
repeatCount: number
|
||||
shouldAdvanceDisplay: boolean
|
||||
displayRepeatUsed: number // Value to show in UI after completion
|
||||
output?: {
|
||||
nextIndex: number
|
||||
nextLoraName: string
|
||||
nextLoraFilename: string
|
||||
currentLoraName: string
|
||||
currentLoraFilename: string
|
||||
}
|
||||
}
|
||||
const pendingExecutions: PendingExecution[] = []
|
||||
|
||||
// Track last known pool config hash
|
||||
const lastPoolConfigHash = ref('')
|
||||
|
||||
@@ -62,6 +100,9 @@ const handleIndexUpdate = async (newIndex: number) => {
|
||||
state.executionIndex.value = null
|
||||
state.nextIndex.value = null
|
||||
|
||||
// Clear execution queue since user is manually changing state
|
||||
executionQueue.length = 0
|
||||
|
||||
state.setIndex(newIndex)
|
||||
|
||||
// Refresh list to update current LoRA display
|
||||
@@ -100,6 +141,79 @@ const handleRefresh = async () => {
|
||||
}
|
||||
}
|
||||
|
||||
// Handle repeat count change
|
||||
const handleRepeatCountChange = (newValue: number) => {
|
||||
state.repeatCount.value = newValue
|
||||
// Reset repeatUsed when changing repeat count
|
||||
state.repeatUsed.value = 0
|
||||
state.displayRepeatUsed.value = 0
|
||||
}
|
||||
|
||||
// Clear all pending items from server queue
|
||||
const clearPendingQueue = async () => {
|
||||
try {
|
||||
// Clear local execution queue
|
||||
executionQueue.length = 0
|
||||
|
||||
// Clear server queue (pending items only)
|
||||
await fetch('/queue', {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify({ clear: true })
|
||||
})
|
||||
|
||||
console.log('[LoraCyclerWidget] Cleared pending queue on pause')
|
||||
} catch (error) {
|
||||
console.error('[LoraCyclerWidget] Error clearing queue:', error)
|
||||
}
|
||||
}
|
||||
|
||||
// Handle pause toggle
|
||||
const handleTogglePause = async () => {
|
||||
const wasPaused = state.isPaused.value
|
||||
state.togglePause()
|
||||
|
||||
// When transitioning to paused state, clear pending queue
|
||||
if (!wasPaused && state.isPaused.value) {
|
||||
// Reset execution state so subsequent manual queues start fresh
|
||||
;(props.widget as any)[HAS_EXECUTED] = false
|
||||
state.executionIndex.value = null
|
||||
state.nextIndex.value = null
|
||||
|
||||
await clearPendingQueue()
|
||||
}
|
||||
}
|
||||
|
||||
// Handle reset index
|
||||
const handleResetIndex = async () => {
|
||||
// Reset execution state
|
||||
;(props.widget as any)[HAS_EXECUTED] = false
|
||||
state.executionIndex.value = null
|
||||
state.nextIndex.value = null
|
||||
|
||||
// Clear execution queue since user is resetting state
|
||||
executionQueue.length = 0
|
||||
|
||||
// Reset index and repeat state
|
||||
state.resetIndex()
|
||||
|
||||
// Refresh list to update current LoRA display
|
||||
try {
|
||||
const poolConfig = getPoolConfig()
|
||||
const loraList = await state.fetchCyclerList(poolConfig)
|
||||
|
||||
if (loraList.length > 0) {
|
||||
const currentLora = loraList[0]
|
||||
if (currentLora) {
|
||||
state.currentLoraName.value = currentLora.file_name
|
||||
state.currentLoraFilename.value = currentLora.file_name
|
||||
}
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('[LoraCyclerWidget] Error resetting index:', error)
|
||||
}
|
||||
}
|
||||
|
||||
// Check for pool config changes
|
||||
const checkPoolConfigChanges = async () => {
|
||||
if (!isMounted.value) return
|
||||
@@ -135,17 +249,66 @@ onMounted(async () => {
|
||||
|
||||
// Add beforeQueued hook to handle index shifting for batch queue synchronization
|
||||
// This ensures each execution uses a different LoRA in the cycle
|
||||
// Now with support for repeat count and pause features
|
||||
//
|
||||
// IMPORTANT: In batch queue mode, ALL beforeQueued calls happen BEFORE any execution.
|
||||
// We push an "execution context" snapshot to a queue so that onExecuted can use the
|
||||
// correct state values that were captured at queue time (not the live state).
|
||||
;(props.widget as any).beforeQueued = () => {
|
||||
if (state.isPaused.value) {
|
||||
// When paused: use current index, don't advance, don't count toward repeat limit
|
||||
// Push context indicating this execution should NOT advance display
|
||||
executionQueue.push({
|
||||
isPaused: true,
|
||||
repeatUsed: state.repeatUsed.value,
|
||||
repeatCount: state.repeatCount.value,
|
||||
shouldAdvanceDisplay: false,
|
||||
displayRepeatUsed: state.displayRepeatUsed.value // Keep current display value when paused
|
||||
})
|
||||
// CRITICAL: Clear execution_index when paused to force backend to use current_index
|
||||
// This ensures paused executions use the same LoRA regardless of any
|
||||
// execution_index set by previous non-paused beforeQueued calls
|
||||
const pausedConfig = state.buildConfig()
|
||||
pausedConfig.execution_index = null
|
||||
props.widget.value = pausedConfig
|
||||
return
|
||||
}
|
||||
|
||||
if ((props.widget as any)[HAS_EXECUTED]) {
|
||||
// After first execution: shift indices (previous next_index becomes execution_index)
|
||||
state.generateNextIndex()
|
||||
// After first execution: check repeat logic
|
||||
if (state.repeatUsed.value < state.repeatCount.value) {
|
||||
// Still repeating: increment repeatUsed, use same index
|
||||
state.repeatUsed.value++
|
||||
} else {
|
||||
// Repeat complete: reset repeatUsed to 1, advance to next index
|
||||
state.repeatUsed.value = 1
|
||||
state.generateNextIndex()
|
||||
}
|
||||
} else {
|
||||
// First execution: just initialize next_index (execution_index stays null)
|
||||
// This means first execution uses current_index from widget
|
||||
// First execution: initialize
|
||||
state.repeatUsed.value = 1
|
||||
state.initializeNextIndex()
|
||||
;(props.widget as any)[HAS_EXECUTED] = true
|
||||
}
|
||||
|
||||
// Determine if this execution should advance the display
|
||||
// (only when repeat cycle is complete for this queued item)
|
||||
const shouldAdvanceDisplay = state.repeatUsed.value >= state.repeatCount.value
|
||||
|
||||
// Calculate the display value to show after this execution completes
|
||||
// When advancing to a new LoRA: reset to 0 (fresh start for new LoRA)
|
||||
// When repeating same LoRA: show current repeat step
|
||||
const displayRepeatUsed = shouldAdvanceDisplay ? 0 : state.repeatUsed.value
|
||||
|
||||
// Push execution context snapshot to queue
|
||||
executionQueue.push({
|
||||
isPaused: false,
|
||||
repeatUsed: state.repeatUsed.value,
|
||||
repeatCount: state.repeatCount.value,
|
||||
shouldAdvanceDisplay,
|
||||
displayRepeatUsed
|
||||
})
|
||||
|
||||
// Update the widget value so the indices are included in the serialized config
|
||||
props.widget.value = state.buildConfig()
|
||||
}
|
||||
@@ -163,35 +326,62 @@ onMounted(async () => {
|
||||
}
|
||||
|
||||
// Override onExecuted to handle backend UI updates
|
||||
// This defers the UI update until workflow completes (via API events)
|
||||
const originalOnExecuted = (props.node as any).onExecuted?.bind(props.node)
|
||||
|
||||
;(props.node as any).onExecuted = function(output: any) {
|
||||
console.log("[LoraCyclerWidget] Node executed with output:", output)
|
||||
|
||||
// Update state from backend response (values are wrapped in arrays)
|
||||
if (output?.next_index !== undefined) {
|
||||
const val = Array.isArray(output.next_index) ? output.next_index[0] : output.next_index
|
||||
state.currentIndex.value = val
|
||||
}
|
||||
// Pop execution context from queue (FIFO order)
|
||||
const context = executionQueue.shift()
|
||||
|
||||
// Determine if we should advance the display index
|
||||
const shouldAdvanceDisplay = context
|
||||
? context.shouldAdvanceDisplay
|
||||
: (!state.isPaused.value && state.repeatUsed.value >= state.repeatCount.value)
|
||||
|
||||
// Extract output values
|
||||
const nextIndex = output?.next_index !== undefined
|
||||
? (Array.isArray(output.next_index) ? output.next_index[0] : output.next_index)
|
||||
: state.currentIndex.value
|
||||
const nextLoraName = output?.next_lora_name !== undefined
|
||||
? (Array.isArray(output.next_lora_name) ? output.next_lora_name[0] : output.next_lora_name)
|
||||
: ''
|
||||
const nextLoraFilename = output?.next_lora_filename !== undefined
|
||||
? (Array.isArray(output.next_lora_filename) ? output.next_lora_filename[0] : output.next_lora_filename)
|
||||
: ''
|
||||
const currentLoraName = output?.current_lora_name !== undefined
|
||||
? (Array.isArray(output.current_lora_name) ? output.current_lora_name[0] : output.current_lora_name)
|
||||
: ''
|
||||
const currentLoraFilename = output?.current_lora_filename !== undefined
|
||||
? (Array.isArray(output.current_lora_filename) ? output.current_lora_filename[0] : output.current_lora_filename)
|
||||
: ''
|
||||
|
||||
// Update total count immediately (doesn't need to wait for workflow completion)
|
||||
if (output?.total_count !== undefined) {
|
||||
const val = Array.isArray(output.total_count) ? output.total_count[0] : output.total_count
|
||||
state.totalCount.value = val
|
||||
}
|
||||
if (output?.current_lora_name !== undefined) {
|
||||
const val = Array.isArray(output.current_lora_name) ? output.current_lora_name[0] : output.current_lora_name
|
||||
state.currentLoraName.value = val
|
||||
}
|
||||
if (output?.current_lora_filename !== undefined) {
|
||||
const val = Array.isArray(output.current_lora_filename) ? output.current_lora_filename[0] : output.current_lora_filename
|
||||
state.currentLoraFilename.value = val
|
||||
}
|
||||
if (output?.next_lora_name !== undefined) {
|
||||
const val = Array.isArray(output.next_lora_name) ? output.next_lora_name[0] : output.next_lora_name
|
||||
state.currentLoraName.value = val
|
||||
}
|
||||
if (output?.next_lora_filename !== undefined) {
|
||||
const val = Array.isArray(output.next_lora_filename) ? output.next_lora_filename[0] : output.next_lora_filename
|
||||
state.currentLoraFilename.value = val
|
||||
|
||||
// Store pending update (will be applied on workflow completion)
|
||||
if (context) {
|
||||
pendingExecutions.push({
|
||||
repeatUsed: context.repeatUsed,
|
||||
repeatCount: context.repeatCount,
|
||||
shouldAdvanceDisplay,
|
||||
displayRepeatUsed: context.displayRepeatUsed,
|
||||
output: {
|
||||
nextIndex,
|
||||
nextLoraName,
|
||||
nextLoraFilename,
|
||||
currentLoraName,
|
||||
currentLoraFilename
|
||||
}
|
||||
})
|
||||
|
||||
// Update visual feedback state (don't update displayRepeatUsed yet - wait for workflow completion)
|
||||
state.executingRepeatStep.value = context.repeatUsed
|
||||
state.isWorkflowExecuting.value = true
|
||||
}
|
||||
|
||||
// Call original onExecuted if it exists
|
||||
@@ -200,11 +390,69 @@ onMounted(async () => {
|
||||
}
|
||||
}
|
||||
|
||||
// Set up execution tracking via API events
|
||||
if (props.api) {
|
||||
// Handle workflow completion events using FIFO order
|
||||
// Note: The 'executing' event doesn't contain prompt_id (only node ID as string),
|
||||
// so we use FIFO order instead of prompt_id matching since executions are processed
|
||||
// in the order they were queued
|
||||
const handleExecutionComplete = () => {
|
||||
// Process the first pending execution (FIFO order)
|
||||
if (pendingExecutions.length === 0) {
|
||||
return
|
||||
}
|
||||
|
||||
const pending = pendingExecutions.shift()!
|
||||
|
||||
// Apply UI update now that workflow is complete
|
||||
// Update repeat display (deferred like index updates)
|
||||
state.displayRepeatUsed.value = pending.displayRepeatUsed
|
||||
|
||||
if (pending.output) {
|
||||
if (pending.shouldAdvanceDisplay) {
|
||||
state.currentIndex.value = pending.output.nextIndex
|
||||
state.currentLoraName.value = pending.output.nextLoraName
|
||||
state.currentLoraFilename.value = pending.output.nextLoraFilename
|
||||
} else {
|
||||
// When not advancing, show current LoRA info
|
||||
state.currentLoraName.value = pending.output.currentLoraName
|
||||
state.currentLoraFilename.value = pending.output.currentLoraFilename
|
||||
}
|
||||
}
|
||||
|
||||
// Reset visual feedback if no more pending
|
||||
if (pendingExecutions.length === 0) {
|
||||
state.isWorkflowExecuting.value = false
|
||||
state.executingRepeatStep.value = 0
|
||||
}
|
||||
}
|
||||
|
||||
props.api.addEventListener('execution_success', handleExecutionComplete)
|
||||
props.api.addEventListener('execution_error', handleExecutionComplete)
|
||||
props.api.addEventListener('execution_interrupted', handleExecutionComplete)
|
||||
|
||||
// Store cleanup function for API listeners
|
||||
const apiCleanup = () => {
|
||||
props.api.removeEventListener('execution_success', handleExecutionComplete)
|
||||
props.api.removeEventListener('execution_error', handleExecutionComplete)
|
||||
props.api.removeEventListener('execution_interrupted', handleExecutionComplete)
|
||||
}
|
||||
|
||||
// Extend existing cleanup
|
||||
const existingCleanup = (props.widget as any).onRemoveCleanup
|
||||
;(props.widget as any).onRemoveCleanup = () => {
|
||||
existingCleanup?.()
|
||||
apiCleanup()
|
||||
}
|
||||
}
|
||||
|
||||
// Watch for connection changes by polling (since ComfyUI doesn't provide connection events)
|
||||
const checkInterval = setInterval(checkPoolConfigChanges, 1000)
|
||||
|
||||
// Cleanup on unmount (handled by Vue's effect scope)
|
||||
const existingCleanupForInterval = (props.widget as any).onRemoveCleanup
|
||||
;(props.widget as any).onRemoveCleanup = () => {
|
||||
existingCleanupForInterval?.()
|
||||
clearInterval(checkInterval)
|
||||
}
|
||||
})
|
||||
|
||||
@@ -6,15 +6,22 @@
|
||||
|
||||
<!-- Progress Display -->
|
||||
<div class="setting-section progress-section">
|
||||
<div class="progress-display">
|
||||
<div class="progress-display" :class="{ executing: isWorkflowExecuting }">
|
||||
<div class="progress-info">
|
||||
<span class="progress-label">Next LoRA:</span>
|
||||
<span class="progress-label">{{ isWorkflowExecuting ? 'Using LoRA:' : 'Next LoRA:' }}</span>
|
||||
<span class="progress-name" :title="currentLoraFilename">{{ currentLoraName || 'None' }}</span>
|
||||
</div>
|
||||
<div class="progress-counter">
|
||||
<span class="progress-index">{{ currentIndex }}</span>
|
||||
<span class="progress-separator">/</span>
|
||||
<span class="progress-total">{{ totalCount }}</span>
|
||||
|
||||
<!-- Repeat indicator (only shown when repeatCount > 1) -->
|
||||
<div v-if="repeatCount > 1" class="repeat-badge">
|
||||
<span class="repeat-badge-label">Rep</span>
|
||||
<span class="repeat-badge-value">{{ repeatUsed }}/{{ repeatCount }}</span>
|
||||
</div>
|
||||
|
||||
<button
|
||||
class="refresh-button"
|
||||
:disabled="isLoading"
|
||||
@@ -39,10 +46,11 @@
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Starting Index -->
|
||||
<!-- Starting Index with Advanced Controls -->
|
||||
<div class="setting-section">
|
||||
<label class="setting-label">Starting Index</label>
|
||||
<div class="index-input-container">
|
||||
<div class="index-controls-row">
|
||||
<!-- Index input -->
|
||||
<input
|
||||
type="number"
|
||||
class="index-input"
|
||||
@@ -57,6 +65,47 @@
|
||||
@pointerup.stop
|
||||
/>
|
||||
<span class="index-hint">1 - {{ totalCount || 1 }}</span>
|
||||
|
||||
<!-- Repeat control -->
|
||||
<span class="repeat-label">x</span>
|
||||
<input
|
||||
type="number"
|
||||
class="repeat-input"
|
||||
min="1"
|
||||
max="99"
|
||||
:value="repeatCount"
|
||||
@input="onRepeatInput"
|
||||
@blur="onRepeatBlur"
|
||||
@pointerdown.stop
|
||||
@pointermove.stop
|
||||
@pointerup.stop
|
||||
title="Repeat each LoRA this many times"
|
||||
/>
|
||||
<span class="repeat-hint">times</span>
|
||||
|
||||
<!-- Control buttons -->
|
||||
<button
|
||||
class="control-btn"
|
||||
:class="{ active: isPaused }"
|
||||
@click="$emit('toggle-pause')"
|
||||
:title="isPaused ? 'Continue iteration' : 'Pause iteration'"
|
||||
>
|
||||
<svg v-if="isPaused" viewBox="0 0 24 24" fill="currentColor" class="control-icon">
|
||||
<path d="M8 5v14l11-7z"/>
|
||||
</svg>
|
||||
<svg v-else viewBox="0 0 24 24" fill="currentColor" class="control-icon">
|
||||
<path d="M6 4h4v16H6zm8 0h4v16h-4z"/>
|
||||
</svg>
|
||||
</button>
|
||||
<button
|
||||
class="control-btn"
|
||||
@click="$emit('reset-index')"
|
||||
title="Reset to index 1"
|
||||
>
|
||||
<svg viewBox="0 0 24 24" fill="currentColor" class="control-icon">
|
||||
<path d="M12 5V1L7 6l5 5V7c3.31 0 6 2.69 6 6s-2.69 6-6 6-6-2.69-6-6H4c0 4.42 3.58 8 8 8s8-3.58 8-8-3.58-8-8-8z"/>
|
||||
</svg>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -123,6 +172,11 @@ const props = defineProps<{
|
||||
useCustomClipRange: boolean
|
||||
isClipStrengthDisabled: boolean
|
||||
isLoading: boolean
|
||||
repeatCount: number
|
||||
repeatUsed: number
|
||||
isPaused: boolean
|
||||
isWorkflowExecuting: boolean
|
||||
executingRepeatStep: number
|
||||
}>()
|
||||
|
||||
const emit = defineEmits<{
|
||||
@@ -130,11 +184,15 @@ const emit = defineEmits<{
|
||||
'update:modelStrength': [value: number]
|
||||
'update:clipStrength': [value: number]
|
||||
'update:useCustomClipRange': [value: boolean]
|
||||
'update:repeatCount': [value: number]
|
||||
'toggle-pause': []
|
||||
'reset-index': []
|
||||
'refresh': []
|
||||
}>()
|
||||
|
||||
// Temporary value for input while typing
|
||||
const tempIndex = ref<string>('')
|
||||
const tempRepeat = ref<string>('')
|
||||
|
||||
const onIndexInput = (event: Event) => {
|
||||
const input = event.target as HTMLInputElement
|
||||
@@ -154,6 +212,25 @@ const onIndexBlur = (event: Event) => {
|
||||
}
|
||||
tempIndex.value = ''
|
||||
}
|
||||
|
||||
const onRepeatInput = (event: Event) => {
|
||||
const input = event.target as HTMLInputElement
|
||||
tempRepeat.value = input.value
|
||||
}
|
||||
|
||||
const onRepeatBlur = (event: Event) => {
|
||||
const input = event.target as HTMLInputElement
|
||||
const value = parseInt(input.value, 10)
|
||||
|
||||
if (!isNaN(value)) {
|
||||
const clampedValue = Math.max(1, Math.min(value, 99))
|
||||
emit('update:repeatCount', clampedValue)
|
||||
input.value = clampedValue.toString()
|
||||
} else {
|
||||
input.value = props.repeatCount.toString()
|
||||
}
|
||||
tempRepeat.value = ''
|
||||
}
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
@@ -203,6 +280,17 @@ const onIndexBlur = (event: Event) => {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
transition: border-color 0.3s ease;
|
||||
}
|
||||
|
||||
.progress-display.executing {
|
||||
border-color: rgba(66, 153, 225, 0.5);
|
||||
animation: pulse 2s ease-in-out infinite;
|
||||
}
|
||||
|
||||
@keyframes pulse {
|
||||
0%, 100% { border-color: rgba(66, 153, 225, 0.3); }
|
||||
50% { border-color: rgba(66, 153, 225, 0.7); }
|
||||
}
|
||||
|
||||
.progress-info {
|
||||
@@ -243,6 +331,9 @@ const onIndexBlur = (event: Event) => {
|
||||
font-weight: 600;
|
||||
color: rgba(66, 153, 225, 1);
|
||||
font-family: 'SF Mono', 'Roboto Mono', monospace;
|
||||
min-width: 4ch;
|
||||
text-align: right;
|
||||
font-variant-numeric: tabular-nums;
|
||||
}
|
||||
|
||||
.progress-separator {
|
||||
@@ -256,6 +347,9 @@ const onIndexBlur = (event: Event) => {
|
||||
font-weight: 500;
|
||||
color: rgba(226, 232, 240, 0.6);
|
||||
font-family: 'SF Mono', 'Roboto Mono', monospace;
|
||||
min-width: 4ch;
|
||||
text-align: left;
|
||||
font-variant-numeric: tabular-nums;
|
||||
}
|
||||
|
||||
.refresh-button {
|
||||
@@ -303,16 +397,43 @@ const onIndexBlur = (event: Event) => {
|
||||
}
|
||||
}
|
||||
|
||||
/* Index Input */
|
||||
.index-input-container {
|
||||
/* Repeat Badge */
|
||||
.repeat-badge {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 4px;
|
||||
margin-left: 8px;
|
||||
padding: 2px 6px;
|
||||
background: rgba(245, 158, 11, 0.15);
|
||||
border: 1px solid rgba(245, 158, 11, 0.3);
|
||||
border-radius: 4px;
|
||||
}
|
||||
|
||||
.repeat-badge-label {
|
||||
font-size: 10px;
|
||||
color: rgba(253, 230, 138, 0.7);
|
||||
text-transform: uppercase;
|
||||
}
|
||||
|
||||
.repeat-badge-value {
|
||||
font-size: 12px;
|
||||
font-family: 'SF Mono', 'Roboto Mono', monospace;
|
||||
color: rgba(253, 230, 138, 1);
|
||||
min-width: 3ch;
|
||||
font-variant-numeric: tabular-nums;
|
||||
}
|
||||
|
||||
/* Index Controls Row */
|
||||
.index-controls-row {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
flex-wrap: wrap;
|
||||
}
|
||||
|
||||
.index-input {
|
||||
width: 80px;
|
||||
padding: 6px 10px;
|
||||
width: 60px;
|
||||
padding: 6px 8px;
|
||||
background: rgba(26, 32, 44, 0.9);
|
||||
border: 1px solid rgba(226, 232, 240, 0.2);
|
||||
border-radius: 6px;
|
||||
@@ -334,6 +455,75 @@ const onIndexBlur = (event: Event) => {
|
||||
.index-hint {
|
||||
font-size: 11px;
|
||||
color: rgba(226, 232, 240, 0.4);
|
||||
min-width: 7ch;
|
||||
font-variant-numeric: tabular-nums;
|
||||
}
|
||||
|
||||
/* Repeat Controls */
|
||||
.repeat-label {
|
||||
font-size: 13px;
|
||||
color: rgba(226, 232, 240, 0.6);
|
||||
margin-left: 4px;
|
||||
}
|
||||
|
||||
.repeat-input {
|
||||
width: 44px;
|
||||
padding: 6px 6px;
|
||||
background: rgba(26, 32, 44, 0.9);
|
||||
border: 1px solid rgba(226, 232, 240, 0.2);
|
||||
border-radius: 6px;
|
||||
color: #e4e4e7;
|
||||
font-size: 13px;
|
||||
font-family: 'SF Mono', 'Roboto Mono', monospace;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.repeat-input:focus {
|
||||
outline: none;
|
||||
border-color: rgba(66, 153, 225, 0.6);
|
||||
}
|
||||
|
||||
.repeat-hint {
|
||||
font-size: 11px;
|
||||
color: rgba(226, 232, 240, 0.4);
|
||||
}
|
||||
|
||||
/* Control Buttons */
|
||||
.control-btn {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
width: 24px;
|
||||
height: 24px;
|
||||
padding: 0;
|
||||
background: transparent;
|
||||
border: 1px solid rgba(255, 255, 255, 0.1);
|
||||
border-radius: 4px;
|
||||
color: rgba(226, 232, 240, 0.6);
|
||||
cursor: pointer;
|
||||
transition: all 0.2s;
|
||||
}
|
||||
|
||||
.control-btn:hover {
|
||||
background: rgba(66, 153, 225, 0.2);
|
||||
border-color: rgba(66, 153, 225, 0.4);
|
||||
color: rgba(191, 219, 254, 1);
|
||||
}
|
||||
|
||||
.control-btn.active {
|
||||
background: rgba(245, 158, 11, 0.2);
|
||||
border-color: rgba(245, 158, 11, 0.5);
|
||||
color: rgba(253, 230, 138, 1);
|
||||
}
|
||||
|
||||
.control-btn.active:hover {
|
||||
background: rgba(245, 158, 11, 0.3);
|
||||
border-color: rgba(245, 158, 11, 0.6);
|
||||
}
|
||||
|
||||
.control-icon {
|
||||
width: 14px;
|
||||
height: 14px;
|
||||
}
|
||||
|
||||
/* Slider Container */
|
||||
|
||||
Reference in New Issue
Block a user