mirror of
https://github.com/bilawalsidhu/gods-eye-view.git
synced 2026-11-04 10:24:02 +00:00
feat(kit): render the sliders with UiSlider
UiSlider takes v-model, min, max, step and disabled over Core's native range input, emits numbers, and with `hold` never rewrites a focused handle and takes the model back on blur. It is a functional render function, so the portable style parameter rows use it too. The Display, Radio, RTL-SDR, Cockpit, mission replay and style parameter sliders render with it; server-rendered markup keeps their default values.
This commit is contained in:
1 parent
fa2dc9fc87
commit
5019bd9e8a
18 files changed
+241
-50
No files matched your search
+3
-1
@@ -62,7 +62,9 @@
|
||||
the tuner slider's events call `RadioControls`, and the local RTL-SDR
|
||||
card's controls `LocalSdrControls`, through the radio store's new
|
||||
`connect`/`act`; the card labels its gain options itself. `act` returns
|
||||
what the answering action returned. `syncScopeControls` (`./ui/display`)
|
||||
what the answering action returned. The Display, Radio, RTL-SDR, Cockpit,
|
||||
mission replay and style parameter sliders are the kit's `UiSlider`, a
|
||||
native range input that emits numbers. `syncScopeControls` (`./ui/display`)
|
||||
and `shouldHideCollapsedRightPanels` (`./ui/layout`) are deprecated: the
|
||||
Display view and the rail now own what they did. `./ui/input` imports Vue
|
||||
(samehkhamis).
|
||||
|
||||
@@ -56,6 +56,17 @@ Home and End), since each of Core's groups keeps its own keyboard policy, and
|
||||
once and asks for a change only when it lands on another option. Every
|
||||
segmented group in Core is a `UiSegmented`; the old `segmentedControl.js`
|
||||
adapter is gone.
|
||||
`UiSlider` keeps the native range input where ui-vue draws a
|
||||
`role="slider"` track, so Core's keyboard, form behaviour and styling stay;
|
||||
it takes ui-vue's `v-model`, `min`, `max`, `step` and `disabled`, emits a
|
||||
number, and adds `hold` (never rewrite a focused handle, and take the model
|
||||
back on blur). It is a functional render-function component, so the
|
||||
portable `./ui/input` rows use it without a component compiler. The
|
||||
Display, Radio, RTL-SDR, Cockpit, mission replay and style parameter
|
||||
sliders are `UiSlider`s; Street Level's date slider, Bhote Koshi's split
|
||||
and progress ranges and the rail card settings' ranges keep their own
|
||||
inputs for now. Merging the kits means settling native input against the
|
||||
drawn track.
|
||||
|
||||
Controls share one skin, so every panel reads the same. Every scrolling
|
||||
surface carries the kit's `.ui-scroll` (ui-vue's name), which draws the thin
|
||||
|
||||
@@ -1128,6 +1128,8 @@
|
||||
"src/ui/kit/UiRail.test.mjs",
|
||||
"src/ui/kit/UiSegmented.vue",
|
||||
"src/ui/kit/UiSegmented.test.mjs",
|
||||
"src/ui/kit/UiSlider.js",
|
||||
"src/ui/kit/UiSlider.test.mjs",
|
||||
"src/ui/displayPanelActions.test.mjs",
|
||||
"src/testSupport/vueDom.mjs",
|
||||
"build/vueLoader.js",
|
||||
|
||||
@@ -133,7 +133,8 @@
|
||||
"src/ui/stores/panelChrome.js",
|
||||
"src/ui/rail/PanelCollapseButton.vue",
|
||||
"src/ui/rail/panelState.js",
|
||||
"src/sdr/gain.js"
|
||||
"src/sdr/gain.js",
|
||||
"src/ui/kit/UiSlider.js"
|
||||
],
|
||||
"external": ["vue", "pinia"]
|
||||
},
|
||||
@@ -568,7 +569,9 @@
|
||||
"src/i18n/locales/ru.json",
|
||||
"src/i18n/locales/uk.json",
|
||||
"src/i18n/locales/zh-CN.json",
|
||||
"src/i18n/locales/zh-Hant.json"
|
||||
"src/i18n/locales/zh-Hant.json",
|
||||
"src/ui/rail/fieldValue.js",
|
||||
"src/ui/kit/UiSlider.js"
|
||||
],
|
||||
"external": ["vue"]
|
||||
},
|
||||
@@ -1061,7 +1064,9 @@
|
||||
"src/ui/stores/awarenessDirection.js",
|
||||
"src/ui/stores/hud.js",
|
||||
"src/ui/stores/panelChrome.js",
|
||||
"src/ui/rail/panelState.js"
|
||||
"src/ui/rail/panelState.js",
|
||||
"src/ui/rail/fieldValue.js",
|
||||
"src/ui/kit/UiSlider.js"
|
||||
],
|
||||
"external": ["cesium", "vue", "pinia"]
|
||||
},
|
||||
@@ -3069,7 +3074,9 @@
|
||||
"src/ui/stores/firstRun.js",
|
||||
"src/firstRunCopy.js",
|
||||
"src/ui/stores/panelChrome.js",
|
||||
"src/ui/rail/panelState.js"
|
||||
"src/ui/rail/panelState.js",
|
||||
"src/ui/rail/fieldValue.js",
|
||||
"src/ui/kit/UiSlider.js"
|
||||
],
|
||||
"external": [
|
||||
"hls.js",
|
||||
@@ -3843,7 +3850,9 @@
|
||||
"src/i18n/locales/zh-Hant.json",
|
||||
"src/scopeMaskPolicy.js",
|
||||
"src/ui/displayControls.js",
|
||||
"src/visualStylePolicy.js"
|
||||
"src/visualStylePolicy.js",
|
||||
"src/ui/rail/fieldValue.js",
|
||||
"src/ui/kit/UiSlider.js"
|
||||
],
|
||||
"external": ["cesium", "vue", "pinia"]
|
||||
},
|
||||
|
||||
@@ -270,7 +270,7 @@ test('Radio volume and mission speed share the Sharpen slider visual language',
|
||||
}
|
||||
assert.match(
|
||||
missionDetail,
|
||||
/id="space-mission-replay-speed" class="gev-quantitative-slider" type="range" min="0\.25" max="4" step="0\.25" value="1"/,
|
||||
/<UiSlider id="space-mission-replay-speed" hold min="0\.25" max="4" step="0\.25"[^>]*:model-value="m \? m\.replay\.speed : 1"/,
|
||||
);
|
||||
assert.match(missionDetail, /class="gev-slider-value"[^>]*data-mission-replay-speed-output/);
|
||||
assert.match(css, /\.gev-quantitative-slider\s*\{[\s\S]*?min-width: 0;[\s\S]*?height: 18px;/);
|
||||
|
||||
@@ -658,7 +658,7 @@ test('the Display selector, voice schema and final stylesheet expose Cyber', ()
|
||||
assert.match(display, /id="cyber-sonar-toggle"/);
|
||||
assert.match(
|
||||
display,
|
||||
/id="cyber-sonar-opacity"[^>]*value="84"[^>]*aria-label="Cyber contact and label opacity"/,
|
||||
/id="cyber-sonar-opacity"[^>]*:model-value="sonar\?\.opacity \?\? 84" aria-label="Cyber contact and label opacity"/,
|
||||
);
|
||||
assert.match(display, /id="cyber-sonar-sector"/);
|
||||
assert.deepEqual(setHud.parameters.properties.layout.enum, HUD_LAYOUTS);
|
||||
|
||||
@@ -87,7 +87,7 @@ test('buttons, sliders, the layout select and the Scope checkbox call their acti
|
||||
['setSonarOpacity', 60],
|
||||
['setSonarSector', 30],
|
||||
// Density passes the slider's own value, as it always has.
|
||||
['setDensity', '75'],
|
||||
['setDensity', 75],
|
||||
['setFade', 12],
|
||||
['setOutsideOpacity', 3],
|
||||
['setScopeFeather', 150],
|
||||
|
||||
@@ -0,0 +1,63 @@
|
||||
import { h, withDirectives } from 'vue';
|
||||
import { vFieldValue } from '../rail/fieldValue.js';
|
||||
import { vRangeValue } from '../rail/rangeValue.js';
|
||||
|
||||
// Server-rendered markup carries the model as the input's value.
|
||||
const ssrValue = ({ value }) => (value == null ? {} : { value });
|
||||
const LIVE = { ...vRangeValue, getSSRProps: ssrValue };
|
||||
const HOLD = { ...vFieldValue, getSSRProps: ssrValue };
|
||||
|
||||
/** Put the model back on a held input that lost focus. */
|
||||
function takeModel(input, props) {
|
||||
const next = props.modelValue;
|
||||
if (next != null && input.value !== String(next)) input.value = String(next);
|
||||
}
|
||||
|
||||
/**
|
||||
* Core's slider, with @halfpixel/ui-vue UiSlider's v-model, min, max, step
|
||||
* and disabled so the two kits can merge. Core keeps the native range input
|
||||
* (its keyboard, form and styling) where ui-vue draws a `role="slider"`
|
||||
* track. The input carries Core's `gev-quantitative-slider` class ahead of
|
||||
* the caller's, the caller's attributes come first, as in the markup this
|
||||
* replaced, and the accessible name comes as an `aria-label` attribute so
|
||||
* the attribute pass translates it. The value emits as a number.
|
||||
*
|
||||
* The input takes the model's value only when it differs (`v-range-value`);
|
||||
* with `hold`, never while it is focused (`v-field-value`), so a render
|
||||
* cannot move a handle a person is dragging, and it takes the model back
|
||||
* when it loses focus: a model left unchanged renders nothing. Owners take
|
||||
* every value their slider can reach, some only after a delay, so an input
|
||||
* is never put back on its own. It is a functional component written as a
|
||||
* render function, so portable entries need no component compiler. Not
|
||||
* mirrored yet: `label`, `format`, `markers` and `endLabels`.
|
||||
*/
|
||||
export default function UiSlider(props, { attrs, emit }) {
|
||||
const { class: className, ...rest } = attrs;
|
||||
return withDirectives(
|
||||
h('input', {
|
||||
...rest,
|
||||
class: ['gev-quantitative-slider', className],
|
||||
type: 'range',
|
||||
min: props.min,
|
||||
max: props.max,
|
||||
step: props.step,
|
||||
disabled: props.disabled,
|
||||
onInput: (event) => emit('update:modelValue', Number(event.target.value)),
|
||||
onBlur: props.hold
|
||||
? (event) => takeModel(event.target, props)
|
||||
: undefined,
|
||||
}),
|
||||
[[props.hold ? HOLD : LIVE, props.modelValue]],
|
||||
);
|
||||
}
|
||||
|
||||
UiSlider.props = {
|
||||
modelValue: { type: [Number, String], default: undefined },
|
||||
min: { type: [Number, String], required: true },
|
||||
max: { type: [Number, String], required: true },
|
||||
step: { type: [Number, String], default: undefined },
|
||||
disabled: { type: Boolean, default: false },
|
||||
hold: { type: Boolean, default: false },
|
||||
};
|
||||
UiSlider.emits = ['update:modelValue'];
|
||||
UiSlider.inheritAttrs = false;
|
||||
@@ -0,0 +1,107 @@
|
||||
import { after, before, test } from 'node:test';
|
||||
import assert from 'node:assert/strict';
|
||||
import { installVueDom } from '../../testSupport/vueDom.mjs';
|
||||
|
||||
let mount;
|
||||
let UiSlider;
|
||||
let uninstall;
|
||||
before(async () => {
|
||||
uninstall = installVueDom();
|
||||
({ mount } = await import('@vue/test-utils'));
|
||||
UiSlider = (await import('./UiSlider.js')).default;
|
||||
});
|
||||
after(() => uninstall());
|
||||
|
||||
function mountSlider(props = {}, attrs = {}) {
|
||||
return mount(UiSlider, {
|
||||
attachTo: document.body,
|
||||
props: { min: 0, max: 100, modelValue: 40, ...props },
|
||||
attrs,
|
||||
});
|
||||
}
|
||||
|
||||
test('a native range input with Core’s class beside the caller’s', () => {
|
||||
const wrapper = mountSlider(
|
||||
{ step: 5 },
|
||||
{ id: 'vol', class: 'pp-slider', 'aria-label': 'Volume' },
|
||||
);
|
||||
const input = wrapper.get('input');
|
||||
assert.equal(input.attributes('type'), 'range');
|
||||
assert.equal(input.attributes('class'), 'gev-quantitative-slider pp-slider');
|
||||
assert.equal(input.attributes('id'), 'vol');
|
||||
assert.equal(input.attributes('aria-label'), 'Volume');
|
||||
assert.equal(input.attributes('step'), '5');
|
||||
assert.equal(input.attributes('value'), undefined);
|
||||
assert.equal(input.element.value, '40');
|
||||
wrapper.unmount();
|
||||
});
|
||||
|
||||
test('no step attribute unless one is given, and disabled reaches the input', () => {
|
||||
const wrapper = mountSlider({ disabled: true });
|
||||
const input = wrapper.get('input');
|
||||
assert.equal(input.attributes('step'), undefined);
|
||||
assert.equal(input.element.disabled, true);
|
||||
wrapper.unmount();
|
||||
});
|
||||
|
||||
test('a drag emits the value as a number', async () => {
|
||||
const wrapper = mountSlider({ min: 0.25, max: 4, step: 0.25, modelValue: 1 });
|
||||
const input = wrapper.get('input');
|
||||
input.element.value = '2.5';
|
||||
await input.trigger('input');
|
||||
assert.deepEqual(wrapper.emitted('update:modelValue'), [[2.5]]);
|
||||
wrapper.unmount();
|
||||
});
|
||||
|
||||
test('the model moves the handle, even while focused, unless it holds', async () => {
|
||||
const live = mountSlider();
|
||||
live.element.focus();
|
||||
await live.setProps({ modelValue: 70 });
|
||||
assert.equal(live.element.value, '70');
|
||||
live.unmount();
|
||||
|
||||
const held = mountSlider({ hold: true });
|
||||
held.element.focus();
|
||||
held.element.value = '55';
|
||||
await held.setProps({ modelValue: 70 });
|
||||
assert.equal(held.element.value, '55');
|
||||
held.element.blur();
|
||||
await held.setProps({ modelValue: 80 });
|
||||
assert.equal(held.element.value, '80');
|
||||
held.unmount();
|
||||
});
|
||||
|
||||
test('a held slider with no model keeps its value', async () => {
|
||||
const wrapper = mountSlider({ hold: true, modelValue: 30 });
|
||||
await wrapper.setProps({ modelValue: null });
|
||||
assert.equal(wrapper.element.value, '30');
|
||||
wrapper.unmount();
|
||||
});
|
||||
|
||||
test('a held handle takes the model back when it loses focus', async () => {
|
||||
const wrapper = mountSlider({ hold: true, modelValue: 60 });
|
||||
const input = wrapper.get('input');
|
||||
input.element.focus();
|
||||
input.element.value = '35';
|
||||
await input.trigger('input');
|
||||
await wrapper.setProps({ modelValue: 60 });
|
||||
assert.equal(input.element.value, '35');
|
||||
input.element.blur();
|
||||
await input.trigger('blur');
|
||||
assert.equal(input.element.value, '60');
|
||||
wrapper.unmount();
|
||||
});
|
||||
|
||||
test('an input stays until the owner answers, however late', async () => {
|
||||
for (const hold of [false, true]) {
|
||||
const wrapper = mountSlider({ hold, modelValue: 40 });
|
||||
const input = wrapper.get('input');
|
||||
input.element.value = '70';
|
||||
await input.trigger('input');
|
||||
await new Promise((resolve) => setTimeout(resolve, 0));
|
||||
assert.equal(input.element.value, '70', `hold: ${hold}`);
|
||||
await wrapper.setProps({ modelValue: 70 });
|
||||
assert.equal(input.element.value, '70', `hold: ${hold}`);
|
||||
wrapper.unmount();
|
||||
}
|
||||
});
|
||||
@@ -111,7 +111,7 @@ test('before the layer presents, the roster loads and no mission shows', (t) =>
|
||||
);
|
||||
assert.equal($('#space-mission-panel').hidden, true);
|
||||
assert.equal($('[data-mission-replay-speed-output]').textContent, '1×');
|
||||
assert.equal($('[data-mission-replay-speed]').getAttribute('value'), '1');
|
||||
assert.equal($('[data-mission-replay-speed]').value, '1');
|
||||
});
|
||||
|
||||
test('the roster lists missions and drives selection and preview', async (t) => {
|
||||
@@ -316,7 +316,7 @@ test('the replay controls follow the layer and call its commands', async (t) =>
|
||||
['replay'],
|
||||
['toggle'],
|
||||
['cancel'],
|
||||
['speed', '3'],
|
||||
['speed', 3],
|
||||
['focus'],
|
||||
['previous'],
|
||||
['next'],
|
||||
|
||||
@@ -64,7 +64,7 @@ test('the panel’s Enable, transport and volume call RadioControls', (t) => {
|
||||
['toggleRadioPlayback'],
|
||||
['cycleRadio', 1],
|
||||
['stopRadio'],
|
||||
['setRadioVolume', '35'],
|
||||
['setRadioVolume', 35],
|
||||
]);
|
||||
});
|
||||
|
||||
|
||||
@@ -68,7 +68,7 @@ test('the card’s buttons, gain, tuning and volume call LocalSdrControls', (t)
|
||||
['setSdrGain', '20.7'],
|
||||
['tuneSdr', '101.1'],
|
||||
['tuneSdr', '95.3'],
|
||||
['setSdrVolume', '40'],
|
||||
['setSdrVolume', 40],
|
||||
]);
|
||||
});
|
||||
|
||||
|
||||
+14
-18
@@ -1,6 +1,6 @@
|
||||
import { h, render, withDirectives } from 'vue';
|
||||
import { h, render } from 'vue';
|
||||
import { formatNumber, t } from '../i18n/index.js';
|
||||
import { vRangeValue } from './rail/rangeValue.js';
|
||||
import UiSlider from './kit/UiSlider.js';
|
||||
|
||||
/** A slider value in the UI language: 0.85 in English, 0,85 in Spanish. */
|
||||
const formatParam = (value, digits) =>
|
||||
@@ -11,8 +11,8 @@ const formatParam = (value, digits) =>
|
||||
});
|
||||
|
||||
/**
|
||||
* The generated rows: a label, the shared native slider and its value in the
|
||||
* UI language. A render function, so the portable input package needs no
|
||||
* The generated rows: a label, the kit's slider and its value in the UI
|
||||
* language. A render function, so the portable input package needs no
|
||||
* single-file-component compiler.
|
||||
*/
|
||||
const StyleParameterRows = {
|
||||
@@ -24,18 +24,15 @@ const StyleParameterRows = {
|
||||
return this.rows.map((row) =>
|
||||
h('div', { key: row.name, class: 'param-slider-row' }, [
|
||||
h('span', { class: 'param-label' }, row.label),
|
||||
withDirectives(
|
||||
h('input', {
|
||||
type: 'range',
|
||||
class: 'gev-quantitative-slider param-slider',
|
||||
min: row.min,
|
||||
max: row.max,
|
||||
step: row.step,
|
||||
'aria-label': row.label,
|
||||
onInput: (event) => this.onInput(row.name, event.target.value),
|
||||
}),
|
||||
[[vRangeValue, row.value]],
|
||||
),
|
||||
h(UiSlider, {
|
||||
class: 'param-slider',
|
||||
min: row.min,
|
||||
max: row.max,
|
||||
step: row.step,
|
||||
'aria-label': row.label,
|
||||
modelValue: row.value,
|
||||
'onUpdate:modelValue': (value) => this.onInput(row.name, value),
|
||||
}),
|
||||
h('span', { class: 'param-value' }, row.text),
|
||||
]),
|
||||
);
|
||||
@@ -82,9 +79,8 @@ export function createStyleParameters({ container }) {
|
||||
value: readValue(name),
|
||||
text: '',
|
||||
}));
|
||||
onInput = (name, raw) => {
|
||||
onInput = (name, value) => {
|
||||
if (destroyed || current !== generation) return;
|
||||
const value = parseFloat(raw);
|
||||
writeValue(name, value);
|
||||
rows = rows.map((row) =>
|
||||
row.name === name
|
||||
|
||||
@@ -3,7 +3,7 @@ import { storeToRefs } from 'pinia';
|
||||
import { computed } from 'vue';
|
||||
import { t } from '../../../i18n/index.js';
|
||||
import { vClassToggle } from '../../rail/classToggle.js';
|
||||
import { vFieldValue } from '../../rail/fieldValue.js';
|
||||
import UiSlider from '../../kit/UiSlider.js';
|
||||
import { useRadioStore } from '../../stores/radio.js';
|
||||
|
||||
// The Cockpit's Display and Radio launchers and their popovers. RadioControls
|
||||
@@ -60,7 +60,7 @@ const radioLabel = computed(() =>
|
||||
</div>
|
||||
<label class="cockpit-radio-volume" for="cockpit-radio-volume">
|
||||
<span data-i18n>VOLUME</span>
|
||||
<input id="cockpit-radio-volume" @input="act('setRadioVolume', $event.target.value)" class="gev-quantitative-slider" type="range" min="0" max="100" value="80" aria-label="Cockpit Radio volume" :disabled="cockpit ? cockpit.volume.disabled : true" v-field-value="cockpit ? cockpit.volume.value : 80" data-i18n-aria-label />
|
||||
<UiSlider id="cockpit-radio-volume" hold @update:model-value="act('setRadioVolume', $event)" min="0" max="100" aria-label="Cockpit Radio volume" :disabled="cockpit ? cockpit.volume.disabled : true" :model-value="cockpit ? cockpit.volume.value : 80" data-i18n-aria-label />
|
||||
<output id="cockpit-radio-volume-value" class="gev-slider-value" for="cockpit-radio-volume">{{ cockpit ? `${cockpit.volume.value}%` : '80%' }}</output>
|
||||
</label>
|
||||
</div>
|
||||
|
||||
@@ -6,6 +6,7 @@ import { vPanelState } from '../../rail/panelState.js';
|
||||
import PanelCollapseButton from '../../rail/PanelCollapseButton.vue';
|
||||
import { vRangeValue } from '../../rail/rangeValue.js';
|
||||
import UiSegmented from '../../kit/UiSegmented.vue';
|
||||
import UiSlider from '../../kit/UiSlider.js';
|
||||
import GeoImport from './GeoImport.vue';
|
||||
import { useDisplayStore } from '../../stores/display.js';
|
||||
|
||||
@@ -17,7 +18,6 @@ const display = useDisplayStore();
|
||||
const { view, models3d, draw } = storeToRefs(display);
|
||||
// A control's change goes to its owner (DisplayBindings or the draw tool).
|
||||
const act = (name, ...args) => display.act(name, ...args);
|
||||
const integer = (event) => parseInt(event.target.value, 10);
|
||||
const pct = (value) => `${value}%`;
|
||||
const hud = computed(() => view.value?.hud ?? null);
|
||||
const sonar = computed(() => view.value?.sonar ?? null);
|
||||
@@ -80,27 +80,27 @@ const allocationOptions = [
|
||||
<output id="cyber-sonar-status" class="cyber-sonar-control" role="status"></output>
|
||||
<div class="pp-slider-row cyber-sonar-control cyber-sonar-setting" id="cyber-sonar-rings-row">
|
||||
<label class="pp-slider-mini-label" for="cyber-sonar-rings" data-i18n>Rings</label>
|
||||
<input type="range" class="gev-quantitative-slider pp-slider" id="cyber-sonar-rings" @input="act('setSonarRings', integer($event))" min="3" max="12" step="1" value="8" v-range-value="sonar?.rings ?? 8" aria-label="Sonar ring count" data-i18n-aria-label />
|
||||
<UiSlider class="pp-slider" id="cyber-sonar-rings" @update:model-value="act('setSonarRings', $event)" min="3" max="12" step="1" :model-value="sonar?.rings ?? 8" aria-label="Sonar ring count" data-i18n-aria-label />
|
||||
<span class="pp-slider-value" id="cyber-sonar-rings-value">{{ sonar?.rings ?? 8 }}</span>
|
||||
</div>
|
||||
<div class="pp-slider-row cyber-sonar-control cyber-sonar-setting" id="cyber-sonar-range-row">
|
||||
<label class="pp-slider-mini-label" for="cyber-sonar-range" data-i18n>Range</label>
|
||||
<input type="range" class="gev-quantitative-slider pp-slider" id="cyber-sonar-range" @input="act('setSonarRange', integer($event))" min="60" max="120" step="1" value="100" v-range-value="sonar?.range ?? 100" aria-label="Sonar ring range" data-i18n-aria-label />
|
||||
<UiSlider class="pp-slider" id="cyber-sonar-range" @update:model-value="act('setSonarRange', $event)" min="60" max="120" step="1" :model-value="sonar?.range ?? 100" aria-label="Sonar ring range" data-i18n-aria-label />
|
||||
<span class="pp-slider-value" id="cyber-sonar-range-value">{{ pct(sonar?.range ?? 100) }}</span>
|
||||
</div>
|
||||
<div class="pp-slider-row cyber-sonar-control cyber-sonar-setting" id="cyber-sonar-intensity-row">
|
||||
<label class="pp-slider-mini-label" for="cyber-sonar-intensity" data-i18n>Power</label>
|
||||
<input type="range" class="gev-quantitative-slider pp-slider" id="cyber-sonar-intensity" @input="act('setSonarIntensity', integer($event))" min="0" max="100" step="1" value="70" v-range-value="sonar?.intensity ?? 70" aria-label="Sonar intensity" data-i18n-aria-label />
|
||||
<UiSlider class="pp-slider" id="cyber-sonar-intensity" @update:model-value="act('setSonarIntensity', $event)" min="0" max="100" step="1" :model-value="sonar?.intensity ?? 70" aria-label="Sonar intensity" data-i18n-aria-label />
|
||||
<span class="pp-slider-value" id="cyber-sonar-intensity-value">{{ pct(sonar?.intensity ?? 70) }}</span>
|
||||
</div>
|
||||
<div class="pp-slider-row cyber-sonar-control cyber-sonar-setting" id="cyber-sonar-opacity-row">
|
||||
<label class="pp-slider-mini-label" for="cyber-sonar-opacity" data-i18n>Opacity</label>
|
||||
<input type="range" class="gev-quantitative-slider pp-slider" id="cyber-sonar-opacity" @input="act('setSonarOpacity', integer($event))" min="35" max="100" step="1" value="84" v-range-value="sonar?.opacity ?? 84" aria-label="Cyber contact and label opacity" title="Minimum opacity for map dots, icons, models, brackets, and labels between sonar passes" data-i18n-aria-label data-i18n-title />
|
||||
<UiSlider class="pp-slider" id="cyber-sonar-opacity" @update:model-value="act('setSonarOpacity', $event)" min="35" max="100" step="1" :model-value="sonar?.opacity ?? 84" aria-label="Cyber contact and label opacity" title="Minimum opacity for map dots, icons, models, brackets, and labels between sonar passes" data-i18n-aria-label data-i18n-title />
|
||||
<span class="pp-slider-value" id="cyber-sonar-opacity-value">{{ pct(sonar?.opacity ?? 84) }}</span>
|
||||
</div>
|
||||
<div class="pp-slider-row cyber-sonar-control cyber-sonar-setting" id="cyber-sonar-sector-row">
|
||||
<label class="pp-slider-mini-label" for="cyber-sonar-sector" data-i18n>Sector</label>
|
||||
<input type="range" class="gev-quantitative-slider pp-slider" id="cyber-sonar-sector" @input="act('setSonarSector', integer($event))" min="8" max="60" step="1" value="24" v-range-value="sonar?.sector ?? 24" aria-label="Sonar sector width" data-i18n-aria-label />
|
||||
<UiSlider class="pp-slider" id="cyber-sonar-sector" @update:model-value="act('setSonarSector', $event)" min="8" max="60" step="1" :model-value="sonar?.sector ?? 24" aria-label="Sonar sector width" data-i18n-aria-label />
|
||||
<span class="pp-slider-value" id="cyber-sonar-sector-value">{{ `${sonar?.sector ?? 24}°` }}</span>
|
||||
</div>
|
||||
</div>
|
||||
@@ -111,7 +111,7 @@ const allocationOptions = [
|
||||
</button>
|
||||
<div class="pp-slider-row detection-slider-row" :class="{ visible: detection?.rowsVisible }" id="detection-slider-row">
|
||||
<label class="pp-slider-mini-label" for="detection-density-slider" data-i18n>Density</label>
|
||||
<input type="range" class="gev-quantitative-slider pp-slider" id="detection-density-slider" @input="act('setDensity', $event.target.value)" min="0" max="100" step="25" value="50" v-range-value="detection?.densityPct ?? 50" aria-label="Detection label density" data-i18n-aria-label />
|
||||
<UiSlider class="pp-slider" id="detection-density-slider" @update:model-value="act('setDensity', $event)" min="0" max="100" step="25" :model-value="detection?.densityPct ?? 50" aria-label="Detection label density" data-i18n-aria-label />
|
||||
<span class="pp-slider-value" id="detection-density-value">{{ pct(detection?.densityPct ?? 50) }}</span>
|
||||
</div>
|
||||
<div class="pp-slider-row detection-allocation-row" :class="{ visible: detection?.rowsVisible }" id="detection-allocation-row">
|
||||
@@ -120,12 +120,12 @@ const allocationOptions = [
|
||||
</div>
|
||||
<div class="pp-slider-row detection-fade-row" :class="{ visible: detection?.rowsVisible }" id="detection-fade-row">
|
||||
<label class="pp-slider-mini-label" for="detection-fade-slider" data-i18n>Fade</label>
|
||||
<input type="range" class="gev-quantitative-slider pp-slider" id="detection-fade-slider" @input="act('setFade', integer($event))" min="0" max="40" step="1" value="7" v-range-value="detection?.fadePct ?? 7" aria-label="Detection fade distance" title="World-overlay fade distance outside the keyhole as a percentage of its radius" data-i18n-aria-label data-i18n-title />
|
||||
<UiSlider class="pp-slider" id="detection-fade-slider" @update:model-value="act('setFade', $event)" min="0" max="40" step="1" :model-value="detection?.fadePct ?? 7" aria-label="Detection fade distance" title="World-overlay fade distance outside the keyhole as a percentage of its radius" data-i18n-aria-label data-i18n-title />
|
||||
<span class="pp-slider-value" id="detection-fade-value">{{ pct(detection?.fadePct ?? 7) }}</span>
|
||||
</div>
|
||||
<div class="pp-slider-row detection-opacity-row" :class="{ visible: detection?.rowsVisible }" id="detection-opacity-row">
|
||||
<label class="pp-slider-mini-label" for="detection-opacity-slider" data-i18n>Outside</label>
|
||||
<input type="range" class="gev-quantitative-slider pp-slider" id="detection-opacity-slider" @input="act('setOutsideOpacity', integer($event))" min="0" max="100" step="1" value="1" v-range-value="detection?.outsideOpacityPct ?? 1" aria-label="Detection opacity outside the keyhole" title="World-overlay label and card opacity beyond the fade distance" data-i18n-aria-label data-i18n-title />
|
||||
<UiSlider class="pp-slider" id="detection-opacity-slider" @update:model-value="act('setOutsideOpacity', $event)" min="0" max="100" step="1" :model-value="detection?.outsideOpacityPct ?? 1" aria-label="Detection opacity outside the keyhole" title="World-overlay label and card opacity beyond the fade distance" data-i18n-aria-label data-i18n-title />
|
||||
<span class="pp-slider-value" id="detection-opacity-value">{{ pct(detection?.outsideOpacityPct ?? 1) }}</span>
|
||||
</div>
|
||||
</div>
|
||||
@@ -160,7 +160,7 @@ const allocationOptions = [
|
||||
</label>
|
||||
<div class="pp-slider-row visible" id="scope-slider-row">
|
||||
<span class="pp-slider-mini-label" data-i18n>Feather</span>
|
||||
<input type="range" class="gev-quantitative-slider pp-slider" id="scope-feather-slider" @input="act('setScopeFeather', integer($event))" min="0" max="200" value="100" v-range-value="scopeFeather" :disabled="!scopeOn" aria-label="Scope edge feather" title="Scope edge feather as a percentage of the keyhole radius" data-i18n-aria-label data-i18n-title />
|
||||
<UiSlider class="pp-slider" id="scope-feather-slider" @update:model-value="act('setScopeFeather', $event)" min="0" max="200" :model-value="scopeFeather" :disabled="!scopeOn" aria-label="Scope edge feather" title="Scope edge feather as a percentage of the keyhole radius" data-i18n-aria-label data-i18n-title />
|
||||
<span class="pp-slider-value" id="scope-feather-value">{{ pct(scopeFeather) }}</span>
|
||||
</div>
|
||||
</div>
|
||||
@@ -203,7 +203,7 @@ const allocationOptions = [
|
||||
<span class="pp-label" data-i18n>Bloom</span>
|
||||
</button>
|
||||
<div class="pp-slider-row" :class="{ visible: view?.bloom.active }" id="bloom-slider-row">
|
||||
<input type="range" class="gev-quantitative-slider pp-slider" id="bloom-intensity-slider" @input="act('setBloomIntensity', integer($event))" min="0" max="200" value="0" v-range-value="view?.bloom.intensity ?? 0" aria-label="Bloom intensity" data-i18n-aria-label />
|
||||
<UiSlider class="pp-slider" id="bloom-intensity-slider" @update:model-value="act('setBloomIntensity', $event)" min="0" max="200" :model-value="view?.bloom.intensity ?? 0" aria-label="Bloom intensity" data-i18n-aria-label />
|
||||
<span class="pp-slider-value" id="bloom-intensity-value">{{ pct(view?.bloom.intensity ?? 0) }}</span>
|
||||
</div>
|
||||
</div>
|
||||
@@ -213,7 +213,7 @@ const allocationOptions = [
|
||||
<span class="pp-label" data-i18n>Sharpen</span>
|
||||
</button>
|
||||
<div class="pp-slider-row" :class="{ visible: view?.sharpen.active }" id="sharpen-slider-row">
|
||||
<input type="range" class="gev-quantitative-slider pp-slider" id="sharpen-intensity-slider" @input="act('setSharpenIntensity', integer($event))" min="0" max="100" value="49" v-range-value="view?.sharpen.intensity ?? 49" aria-label="Sharpen intensity" data-i18n-aria-label />
|
||||
<UiSlider class="pp-slider" id="sharpen-intensity-slider" @update:model-value="act('setSharpenIntensity', $event)" min="0" max="100" :model-value="view?.sharpen.intensity ?? 49" aria-label="Sharpen intensity" data-i18n-aria-label />
|
||||
<span class="pp-slider-value" id="sharpen-intensity-value">{{ pct(view?.sharpen.intensity ?? 49) }}</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
import { storeToRefs } from 'pinia';
|
||||
import { nextTick, ref, watch } from 'vue';
|
||||
import { t } from '../../../i18n/index.js';
|
||||
import { vFieldValue } from '../../rail/fieldValue.js';
|
||||
import UiSlider from '../../kit/UiSlider.js';
|
||||
import { useContextStore } from '../../stores/context.js';
|
||||
|
||||
// The selected space mission inside Context. The launches layer presents it
|
||||
@@ -62,7 +62,7 @@ watch(
|
||||
</section>
|
||||
<div class="mission-replay-speed-control" :hidden="m && !m.replay.available">
|
||||
<div class="mission-replay-speed-header"><label for="space-mission-replay-speed">{{ t('REPLAY SPEED') }}</label><output class="gev-slider-value" for="space-mission-replay-speed" data-mission-replay-speed-output>{{ m ? m.replay.speedText : '1×' }}</output></div>
|
||||
<input id="space-mission-replay-speed" class="gev-quantitative-slider" type="range" min="0.25" max="4" step="0.25" value="1" data-mission-replay-speed :aria-label="t('Replay speed multiplier')" v-field-value="m?.replay.speed" :style="m && { '--replay-speed-progress': m.replay.speedProgress }" @input="layer?.setReplaySpeed($event.currentTarget.value)">
|
||||
<UiSlider id="space-mission-replay-speed" hold min="0.25" max="4" step="0.25" data-mission-replay-speed :aria-label="t('Replay speed multiplier')" :model-value="m ? m.replay.speed : 1" :style="m && { '--replay-speed-progress': m.replay.speedProgress }" @update:model-value="layer?.setReplaySpeed($event)" />
|
||||
<div class="mission-replay-speed-scale" aria-hidden="true"><span>0.25×</span><span>1×</span><span>4×</span></div>
|
||||
</div>
|
||||
<div class="mission-action-row">
|
||||
|
||||
@@ -4,7 +4,7 @@ import { computed } from 'vue';
|
||||
import { t } from '../../../i18n/index.js';
|
||||
import RailPanel from '../../rail/RailPanel.vue';
|
||||
import { vClassToggle } from '../../rail/classToggle.js';
|
||||
import { vFieldValue } from '../../rail/fieldValue.js';
|
||||
import UiSlider from '../../kit/UiSlider.js';
|
||||
import { vRangeValue } from '../../rail/rangeValue.js';
|
||||
import { useRadioStore } from '../../stores/radio.js';
|
||||
import RadioTuner from './RadioTuner.vue';
|
||||
@@ -58,7 +58,7 @@ const radioWords = {
|
||||
</div>
|
||||
<label class="radio-volume-row" for="radio-volume">
|
||||
<span data-i18n>VOLUME</span>
|
||||
<input id="radio-volume" @input="act('setRadioVolume', $event.target.value)" class="gev-quantitative-slider" type="range" min="0" max="100" value="80" aria-label="Radio volume" :disabled="radio?.volume.disabled" v-field-value="radio ? radio.volume.value : 80" data-i18n-aria-label />
|
||||
<UiSlider id="radio-volume" hold @update:model-value="act('setRadioVolume', $event)" min="0" max="100" aria-label="Radio volume" :disabled="radio?.volume.disabled" :model-value="radio ? radio.volume.value : 80" data-i18n-aria-label />
|
||||
<output id="radio-volume-value" class="gev-slider-value" for="radio-volume">{{ radio ? `${radio.volume.value}%` : '80%' }}</output>
|
||||
</label>
|
||||
<div id="radio-playback-state" class="radio-playback-state" :class="{ error: radio?.playback.error }" role="status" aria-live="polite">{{ radio ? radio.playback.text : t('Radio off') }}</div>
|
||||
|
||||
@@ -6,6 +6,7 @@ import { R820T_GAIN_STEPS_DB } from '../../../sdr/gain.js';
|
||||
import { localSdrGainOptionLabel } from '../../localSdrPresentation.js';
|
||||
import { vClassToggle } from '../../rail/classToggle.js';
|
||||
import { vFieldValue } from '../../rail/fieldValue.js';
|
||||
import UiSlider from '../../kit/UiSlider.js';
|
||||
import { useRadioStore } from '../../stores/radio.js';
|
||||
|
||||
// The local RTL-SDR card in the Radio panel. LocalSdrControls presents it and
|
||||
@@ -60,7 +61,7 @@ const tune = () => act('tuneSdr', frequency.value.value);
|
||||
</div>
|
||||
<label class="radio-volume-row" for="sdr-volume">
|
||||
<span data-i18n>SDR VOL</span>
|
||||
<input id="sdr-volume" @input="act('setSdrVolume', $event.target.value)" class="gev-quantitative-slider" type="range" min="0" max="100" value="80" aria-label="Local SDR volume" :disabled="sdr?.volumeDisabled" v-field-value="sdr ? sdr.volume : 80" data-i18n-aria-label />
|
||||
<UiSlider id="sdr-volume" hold @update:model-value="act('setSdrVolume', $event)" min="0" max="100" aria-label="Local SDR volume" :disabled="sdr?.volumeDisabled" :model-value="sdr ? sdr.volume : 80" data-i18n-aria-label />
|
||||
<output id="sdr-volume-value" class="gev-slider-value" for="sdr-volume">{{ sdr ? sdr.volumeText : '80%' }}</output>
|
||||
</label>
|
||||
<div id="sdr-status" class="radio-playback-state" :class="{ error: sdr?.statusError }" role="status" aria-live="polite">{{ sdr ? sdr.statusText : t('Connect an RTL-SDR to begin.') }}</div>
|
||||
|
||||
Reference in new issue
Block a user