increase frontend flexibility

This commit is contained in:
2025-11-30 11:59:26 -08:00
parent 94ed7e05e2
commit a110aa6376
18 changed files with 396 additions and 43 deletions

View File

@@ -0,0 +1,113 @@
<script setup lang="ts">
import { computed, onMounted, onUnmounted, useTemplateRef } from 'vue';
const props = defineProps<{
value: string;
}>();
const overlay = computed(() => {
return '0'.repeat(props.value.length);
});
const span = useTemplateRef<HTMLSpanElement>('data-span');
function copyHandler(event: ClipboardEvent) {
const selection = document.getSelection();
const span_value: HTMLSpanElement | null = span.value;
if (selection && span_value && selection.containsNode(span_value, true)) {
let copy_result = '';
for (let i = 0; i < selection.rangeCount; i++) {
const node_iter = document.createNodeIterator(
selection.getRangeAt(i).commonAncestorContainer,
NodeFilter.SHOW_ALL,
);
let found_start = false;
while (true) {
const node = node_iter.nextNode();
if (node) {
if (node == selection?.getRangeAt(i).startContainer) {
found_start = true;
}
if (found_start && node.nodeType == Node.TEXT_NODE) {
let append_to_copy = node.textContent?.trim() || '';
const parent = node.parentElement;
if (parent) {
const copy_value =
parent.getAttribute('copy-value');
if (copy_value) {
append_to_copy = copy_value;
}
}
if (node == selection?.getRangeAt(i).endContainer) {
append_to_copy = append_to_copy.substring(
0,
selection?.getRangeAt(i).endOffset,
);
}
if (node == selection?.getRangeAt(i).startContainer) {
append_to_copy = append_to_copy.substring(
selection?.getRangeAt(i).startOffset,
);
}
if (copy_result.length > 0) {
copy_result += ' ';
}
copy_result += append_to_copy;
}
if (node == selection?.getRangeAt(i).endContainer) {
break;
}
} else {
break;
}
}
}
event.clipboardData?.setData('text/plain', copy_result);
event.preventDefault();
event.stopImmediatePropagation();
}
}
onMounted(() => {
document.body.addEventListener('copy', copyHandler);
});
onUnmounted(() => {
document.body.removeEventListener('copy', copyHandler);
});
</script>
<template>
<span class="test monospace" :data-after-content="value">
<span ref="data-span" class="transparent" :copy-value="value">
{{ overlay }}
</span>
</span>
</template>
<style scoped lang="scss">
@use '@/assets/variables';
.monospace {
font-family: variables.$monospace-text-font;
}
.transparent {
color: transparent;
border: transparent;
background: transparent;
}
.test {
position: relative;
}
.test::after {
content: attr(data-after-content);
position: absolute;
top: 0;
left: 0;
height: 0;
width: 0;
}
</style>

View File

@@ -1,14 +1,20 @@
<script setup lang="ts">
import { computed, watch } from 'vue';
import CopyableDynamicSpan from '@/components/CopyableDynamicSpan.vue';
const props = defineProps<{
value: number;
max_width: number;
copyable?: boolean;
}>();
const emit = defineEmits<{
(e: 'update', value: string): void;
}>();
const copyable = computed(() => {
return props.copyable || false;
});
const display_value = computed(() => {
if (props.value == 0) {
return '0';
@@ -61,7 +67,12 @@ watch([display_value], ([display_str]) => {
</script>
<template>
{{ display_value }}
<template v-if="copyable">
<CopyableDynamicSpan :value="display_value"></CopyableDynamicSpan>
</template>
<template v-else>
{{ display_value }}
</template>
</template>
<style scoped lang="scss"></style>

View File

@@ -0,0 +1,66 @@
<script setup lang="ts">
import { useTelemetry } from '@/composables/telemetry.ts';
import { computed, inject, type ShallowRef } from 'vue';
import {
WEBSOCKET_SYMBOL,
type WebsocketHandle,
} from '@/composables/websocket.ts';
import NumericText from '@/components/NumericText.vue';
const max_update_rate = 50; // ms
const default_update_rate = 200; // ms
const props = defineProps<{
data: string;
max_update_period?: number;
}>();
const max_update_period = computed(() => {
return Math.min(
props.max_update_period || default_update_rate,
max_update_rate,
);
});
const { data: telemetry_data } = useTelemetry(() => props.data);
const websocket = inject<ShallowRef<WebsocketHandle>>(WEBSOCKET_SYMBOL)!;
const value = websocket.value.listen_to_telemetry(
telemetry_data,
max_update_period,
true,
);
const is_data_present = computed(() => {
return value.value != null;
});
const numeric_data = computed(() => {
const val = value.value;
if (val) {
const type = telemetry_data.value!.data_type;
const item_val = val.value[type];
if (typeof item_val == 'number') {
return item_val;
}
}
return null;
});
</script>
<template>
<span v-if="!is_data_present"> No Data </span>
<template v-else>
<NumericText
v-if="numeric_data"
:value="numeric_data"
:max_width="10"
></NumericText>
<span v-else>
Cannot Display Data of Type {{ telemetry_data!.data_type }}
</span>
</template>
</template>
<style scoped lang="scss">
@use '@/assets/variables';
</style>

View File

@@ -0,0 +1,19 @@
<script setup lang="ts">
defineProps<{
cols: number;
equal_col_width?: boolean;
}>();
</script>
<template>
<div
:class="`grid`"
:style="`grid-template-columns: repeat(${cols}, ${equal_col_width ? '1fr' : 'auto'});`"
>
<slot></slot>
</div>
</template>
<style scoped lang="scss">
@use '@/assets/variables';
</style>

View File

@@ -0,0 +1,33 @@
<script setup lang="ts">
import type { Direction } from '@/composables/Direction.ts';
import { Alignment } from '@/composables/Alignment.ts';
import { Justification } from '@/composables/Justification.ts';
import { computed } from 'vue';
import { GapSize } from '@/composables/GapSize.ts';
const props = defineProps<{
direction: Direction;
stretch?: boolean;
// Direction
justify?: Justification;
// Cross Direction
align?: Alignment;
gap?: GapSize;
}>();
const justification = computed(() => props.justify || Justification.Start);
const alignment = computed(() => props.align || Alignment.Start);
const gap_size = computed(() => props.gap || GapSize.Normal);
</script>
<template>
<div
:class="`${direction} linear ${stretch ? 'stretch' : ''} ${justification} ${alignment} ${gap_size}`"
>
<slot></slot>
</div>
</template>
<style scoped lang="scss">
@use '@/assets/variables';
</style>

View File

@@ -0,0 +1,30 @@
<script setup lang="ts">
import type { ScreenType } from '@/composables/ScreenType.ts';
defineProps<{
// Whether this should be limited to the height of the viewport
// This allows scroll bars to be pushed to inner components
limit?: boolean;
type: ScreenType;
}>();
</script>
<template>
<div
:class="`column stretch screen ${limit ? 'limited' : ''} content ${type}`"
>
<slot></slot>
</div>
</template>
<style scoped lang="scss">
@use '@/assets/variables';
.screen {
min-height: 100vh;
flex-grow: 1;
}
.screen.limited {
max-height: 100vh;
}
</style>