diff --git a/ui/detection-app/src/App.vue b/ui/detection-app/src/App.vue
index b07be31..2e38800 100644
--- a/ui/detection-app/src/App.vue
+++ b/ui/detection-app/src/App.vue
@@ -138,7 +138,7 @@ function onJobStarted(newJobId: string) {
-
+
diff --git a/ui/detection-app/src/components/StageConfigSliders.vue b/ui/detection-app/src/components/StageConfigSliders.vue
index e4a0fdd..19d50d2 100644
--- a/ui/detection-app/src/components/StageConfigSliders.vue
+++ b/ui/detection-app/src/components/StageConfigSliders.vue
@@ -1,5 +1,7 @@
+
+
+
+
+
+
+
+
+
+
onInput(f.name, Number((e.target as HTMLInputElement).value))"
+ />
+
+ {{ f.min ?? 0 }}
+ {{ f.max ?? 500 }}
+
+
+
+
+
+
diff --git a/ui/framework/src/components/SplitPane.vue b/ui/framework/src/components/SplitPane.vue
index af77b73..7a1b0f4 100644
--- a/ui/framework/src/components/SplitPane.vue
+++ b/ui/framework/src/components/SplitPane.vue
@@ -64,9 +64,11 @@ const isHorizontal = computed(() => props.direction === 'horizontal')
const sizedStyle = computed(() => {
if (props.sizeMode === 'px') {
+ const sizeStr = size.value + 'px'
+ const minStr = props.min + 'px'
return isHorizontal.value
- ? { width: size.value + 'px', flexShrink: '0' }
- : { height: size.value + 'px', flexShrink: '0' }
+ ? { width: sizeStr, minWidth: minStr, flexShrink: '0' }
+ : { height: sizeStr, minHeight: minStr, flexShrink: '0' }
}
return { flex: String(size.value) }
})
diff --git a/ui/framework/src/composables/useEditorExecution.ts b/ui/framework/src/composables/useEditorExecution.ts
new file mode 100644
index 0000000..e968408
--- /dev/null
+++ b/ui/framework/src/composables/useEditorExecution.ts
@@ -0,0 +1,57 @@
+import { ref } from 'vue'
+
+export interface EditorExecutionOptions {
+ /** Debounce delay in ms for auto-apply. Default: 150 */
+ debounceMs?: number
+}
+
+/**
+ * Generic editor execution pattern — debounced apply with auto-apply toggle,
+ * loading/error/timing state tracking.
+ *
+ * The caller provides the actual execution function. This composable handles
+ * the orchestration: debounce, auto-apply, loading state, timing.
+ */
+export function useEditorExecution(
+ executeFn: () => Promise,
+ options: EditorExecutionOptions = {},
+) {
+ const debounceMs = options.debounceMs ?? 150
+
+ const loading = ref(false)
+ const error = ref(null)
+ const autoApply = ref(true)
+ const execTimeMs = ref(null)
+
+ let debounceTimer: ReturnType | null = null
+
+ async function apply() {
+ loading.value = true
+ error.value = null
+ execTimeMs.value = null
+ const t0 = performance.now()
+ try {
+ await executeFn()
+ execTimeMs.value = Math.round(performance.now() - t0)
+ } catch (e) {
+ error.value = String(e)
+ } finally {
+ loading.value = false
+ }
+ }
+
+ function onParameterChange() {
+ if (!autoApply.value) return
+ if (debounceTimer) clearTimeout(debounceTimer)
+ debounceTimer = setTimeout(() => apply(), debounceMs)
+ }
+
+ return {
+ loading,
+ error,
+ autoApply,
+ execTimeMs,
+ apply,
+ onParameterChange,
+ }
+}
diff --git a/ui/framework/src/index.ts b/ui/framework/src/index.ts
index ddcaa5f..8d97ff1 100644
--- a/ui/framework/src/index.ts
+++ b/ui/framework/src/index.ts
@@ -4,12 +4,16 @@ export { SSEDataSource } from './datasources/SSEDataSource'
export { StaticDataSource } from './datasources/StaticDataSource'
export { useDataSource } from './composables/useDataSource'
export { useRegistry } from './composables/useRegistry'
+export { useEditorExecution } from './composables/useEditorExecution'
+export type { EditorExecutionOptions } from './composables/useEditorExecution'
// Components
export { default as Panel } from './components/Panel.vue'
export { default as LayoutGrid } from './components/LayoutGrid.vue'
export { default as ResizeHandle } from './components/ResizeHandle.vue'
export { default as SplitPane } from './components/SplitPane.vue'
+export { default as ParameterEditor } from './components/ParameterEditor.vue'
+export type { ConfigField } from './components/ParameterEditor.vue'
// Renderers
export { default as LogRenderer } from './renderers/LogRenderer.vue'