This commit is contained in:
2026-03-28 00:59:59 -03:00
parent f6ef95ebea
commit 8a90436f33
7 changed files with 260 additions and 195 deletions

View File

@@ -0,0 +1,145 @@
<script setup lang="ts">
import { computed } from 'vue'
export interface ConfigField {
name: string
type: string
default: unknown
description: string
min: number | null
max: number | null
options: string[] | null
}
const props = defineProps<{
fields: ConfigField[]
values: Record<string, unknown>
}>()
const emit = defineEmits<{
'update': [name: string, value: unknown]
'reset': []
}>()
const numericFields = computed(() => props.fields.filter(f => f.type === 'int' || f.type === 'float'))
const boolFields = computed(() => props.fields.filter(f => f.type === 'bool'))
function onInput(name: string, value: unknown) {
emit('update', name, value)
}
</script>
<template>
<div class="param-editor">
<!-- Boolean fields -->
<label v-for="f in boolFields" :key="f.name" class="param-field bool-field">
<input
type="checkbox"
:checked="!!values[f.name]"
@change="(e) => onInput(f.name, (e.target as HTMLInputElement).checked)"
/>
<span class="field-label" :title="f.description">{{ f.name.replace(/_/g, ' ') }}</span>
</label>
<!-- Numeric fields (range sliders) -->
<div v-for="f in numericFields" :key="f.name" class="param-field">
<div class="field-header">
<span class="field-label" :title="f.description">{{ f.name.replace(/^edge_/, '').replace(/_/g, ' ') }}</span>
<span class="field-value">{{ values[f.name] }}</span>
</div>
<input
type="range"
:min="f.min ?? 0"
:max="f.max ?? 500"
:step="f.type === 'float' ? 0.01 : 1"
:value="values[f.name] as number"
@input="(e) => onInput(f.name, Number((e.target as HTMLInputElement).value))"
/>
<div class="field-range">
<span>{{ f.min ?? 0 }}</span>
<span>{{ f.max ?? 500 }}</span>
</div>
</div>
</div>
</template>
<style scoped>
.param-editor {
display: flex;
flex-direction: column;
gap: var(--space-2);
}
.param-field {
display: flex;
flex-direction: column;
gap: 2px;
}
.bool-field {
flex-direction: row;
align-items: center;
gap: 6px;
cursor: pointer;
}
.field-header {
display: flex;
justify-content: space-between;
align-items: center;
}
.field-label {
color: var(--text-secondary);
font-size: 10px;
text-transform: capitalize;
}
.field-value {
font-weight: 600;
font-size: 10px;
color: var(--text-primary);
min-width: 30px;
text-align: right;
}
.field-range {
display: flex;
justify-content: space-between;
font-size: 9px;
color: var(--text-dim);
}
input[type="range"] {
-webkit-appearance: none;
appearance: none;
width: 100%;
height: 4px;
background: var(--surface-3);
border-radius: 2px;
outline: none;
}
input[type="range"]::-webkit-slider-thumb {
-webkit-appearance: none;
appearance: none;
width: 12px;
height: 12px;
border-radius: 50%;
background: var(--text-primary);
cursor: pointer;
}
input[type="range"]::-moz-range-thumb {
width: 12px;
height: 12px;
border-radius: 50%;
background: var(--text-primary);
cursor: pointer;
border: none;
}
input[type="checkbox"] {
accent-color: #00bcd4;
}
</style>

View File

@@ -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) }
})

View File

@@ -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<void>,
options: EditorExecutionOptions = {},
) {
const debounceMs = options.debounceMs ?? 150
const loading = ref(false)
const error = ref<string | null>(null)
const autoApply = ref(true)
const execTimeMs = ref<number | null>(null)
let debounceTimer: ReturnType<typeof setTimeout> | 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,
}
}

View File

@@ -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'