diff --git a/starpilot/common/tests/test_lane_centering_galaxy.py b/starpilot/common/tests/test_lane_centering_galaxy.py index ffd1e78f4..aa48692ec 100644 --- a/starpilot/common/tests/test_lane_centering_galaxy.py +++ b/starpilot/common/tests/test_lane_centering_galaxy.py @@ -48,3 +48,5 @@ def test_lane_centering_galaxy_controls(): assert e2e_authority["min"] == 0.0 assert e2e_authority["max"] == 1.0 assert e2e_authority["step"] == 0.05 + assert e2e_authority["control"] == "slider" + assert len(e2e_authority["description_steps"]) == 5 diff --git a/starpilot/system/the_galaxy/assets/components/tools/device_settings.css b/starpilot/system/the_galaxy/assets/components/tools/device_settings.css index b65d368ac..5e66425e2 100644 --- a/starpilot/system/the_galaxy/assets/components/tools/device_settings.css +++ b/starpilot/system/the_galaxy/assets/components/tools/device_settings.css @@ -515,6 +515,35 @@ opacity: var(--disabled-opacity); } +.ds-slider-container { + width: 100%; +} + +@media (min-width: 768px) { + .ds-slider-container { + max-width: 420px; + } +} + +.ds-slider { + accent-color: var(--main-fg); + cursor: pointer; + width: 100%; +} + +.ds-slider:disabled { + cursor: not-allowed; + opacity: var(--disabled-opacity); +} + +.ds-slider-scale { + color: var(--text-muted); + display: flex; + font-size: var(--font-size-xs); + justify-content: space-between; + margin-top: 0.1rem; +} + /* ――― Favorite Slots ――― */ .ds-favorites-panel { display: grid; diff --git a/starpilot/system/the_galaxy/assets/components/tools/device_settings.js b/starpilot/system/the_galaxy/assets/components/tools/device_settings.js index 09916e757..07100bc57 100644 --- a/starpilot/system/the_galaxy/assets/components/tools/device_settings.js +++ b/starpilot/system/the_galaxy/assets/components/tools/device_settings.js @@ -73,6 +73,7 @@ const state = reactive({ fetched: false, activeSectionSlug: "", numericUpdating: {}, + sliderPreviewValues: {}, actionUpdating: {}, favoriteLoading: false, favoriteSaving: false, @@ -840,6 +841,16 @@ function getParamDisplayLabel(key) { return state.paramMetaByKey[key]?.label || key } +function getSliderDescription(param, value) { + const steps = Array.isArray(param.description_steps) ? param.description_steps : [] + if (!steps.length) return param.description || "" + + const numericValue = Number(value) + if (!Number.isFinite(numericValue)) return param.description || "" + const selected = steps.find(step => numericValue <= Number(step.max)) || steps[steps.length - 1] + return selected.description || param.description || "" +} + function confirmPandaFirmwareToggle(key, enabled) { if (!PANDA_FIRMWARE_TOGGLE_KEYS.has(key)) return true @@ -870,7 +881,12 @@ function syncNumericDisplay(param, rawValue) { async function updateNumericParam(param, numericValue, options = {}) { const key = param.key - const current = state.values[key] + const current = options.previousValue !== undefined ? options.previousValue : state.values[key] + if (Object.prototype.hasOwnProperty.call(state.sliderPreviewValues, key)) { + const nextPreviewValues = { ...state.sliderPreviewValues } + delete nextPreviewValues[key] + state.sliderPreviewValues = nextPreviewValues + } const successMessage = options.successMessage state.numericUpdating = { ...state.numericUpdating, [key]: true } state.values = { ...state.values, [key]: numericValue } @@ -905,6 +921,40 @@ async function updateNumericParam(param, numericValue, options = {}) { } } +function previewSliderParam(param, rawValue) { + if (isNumericUpdating(param.key)) return + + const bounds = numericBounds(param) + const precision = stepPrecision(bounds.step, param.precision) + const snapped = snapNumericToBoundsAndStep(rawValue, bounds, precision) + if (snapped === null) return + + state.sliderPreviewValues = { ...state.sliderPreviewValues, [param.key]: snapped } + syncNumericDisplay(param, snapped) +} + +function commitSliderParam(param, rawValue) { + if (isNumericUpdating(param.key)) return + + const bounds = numericBounds(param) + const precision = stepPrecision(bounds.step, param.precision) + const next = snapNumericToBoundsAndStep(rawValue, bounds, precision) + if (next === null) return + + const current = resolveCurrentNumericValue(param, bounds) + const previewValues = { ...state.sliderPreviewValues } + delete previewValues[param.key] + state.sliderPreviewValues = previewValues + + const epsilon = Math.pow(10, -(precision + 2)) + if (Math.abs(next - current) <= epsilon) { + syncNumericDisplay(param, current) + return + } + + updateNumericParam(param, next, { previousValue: current }) +} + function stepNumericParam(param, direction) { const bounds = numericBounds(param) const min = Number(bounds.min) @@ -1408,6 +1458,7 @@ function renderSettingRow(p) { } const isNumeric = p.ui_type === "numeric" + const isSlider = isNumeric && p.control === "slider" const isColor = p.ui_type === "color" const isAction = p.ui_type === "action" const isGroup = isGroupParam(p) @@ -1427,6 +1478,41 @@ function renderSettingRow(p) { ${() => state.actionUpdating[p.key] ? "Resetting..." : (p.action_label || "Run")} ` + } else if (isSlider) { + rowControl = html` +
+ ` } else if (isNumeric) { rowControl = html`