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

@@ -2,23 +2,47 @@
display: flex; display: flex;
flex-direction: column; flex-direction: column;
flex-wrap: nowrap; flex-wrap: nowrap;
justify-content: flex-start;
align-items: flex-start;
align-content: flex-start; align-content: flex-start;
} }
.gap_half { .align-start {
gap: 0.5em; align-items: flex-start;
}
.align-center {
align-items: center;
}
.align-end {
align-items: flex-end;
}
.justify-start {
justify-content: flex-start;
}
.justify-center {
justify-content: center;
}
.justify-between {
justify-content: space-between;
}
.justify-end {
justify-content: flex-end;
}
.gap-half {
gap: 0.0625em;
}
.gap-full {
gap: 0.125em;
}
.gap-wide {
gap: 0.25em;
} }
.row { .row {
display: flex; display: flex;
flex-direction: row; flex-direction: row;
flex-wrap: nowrap; flex-wrap: nowrap;
justify-content: flex-start;
align-items: flex-start;
align-content: center; align-content: center;
gap: 1em;
} }
.center { .center {
@@ -27,15 +51,6 @@
align-content: center; align-content: center;
} }
.screen {
min-height: 100vh;
flex-grow: 1;
}
.screen.limited {
max-height: 100vh;
}
.content { .content {
padding: 20vh 10vw; padding: 20vh 10vw;
} }
@@ -54,26 +69,6 @@
padding-bottom: 5vw; padding-bottom: 5vw;
} }
//.content {
// margin: 20vh 10vw;
// max-width: 1200px;
//}
//
//.content.full {
// margin: 5vh 5vw;
// max-width: none;
//}
//.screen.content {
// width: calc(100% - 20vw);
// height: calc(100vh - 40vh);
//}
//.screen.content.full {
// width: calc(100% - 10vw);
// height: calc(100vh - 10vh);
//}
.stretch { .stretch {
align-items: stretch; align-items: stretch;
} }
@@ -105,3 +100,13 @@
.scroll { .scroll {
overflow: auto; overflow: auto;
} }
.grid {
display: grid;
grid-auto-flow: row;
grid-auto-rows: 1fr;
row-gap: 0.125em;
column-gap: 0.5em;
justify-items: stretch;
align-items: center;
}

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

View File

@@ -0,0 +1,5 @@
export enum Alignment {
Start = 'align-start',
Center = 'align-center',
End = 'align-end',
}

View File

@@ -0,0 +1,4 @@
export enum Direction {
Row = 'row',
Column = 'column',
}

View File

@@ -0,0 +1,6 @@
export enum GapSize {
None = '',
Thin = 'gap-half',
Normal = 'gap-full',
Wide = 'gap-wide',
}

View File

@@ -0,0 +1,6 @@
export enum Justification {
Start = 'justify-start',
Center = 'justify-center',
Between = 'justify-between',
End = 'justify-end',
}

View File

@@ -0,0 +1,7 @@
export enum ScreenType {
Standard = '',
Page = 'page',
Wide = 'wide',
Tall = 'tall',
WideTall = 'wide tall',
}

View File

@@ -43,6 +43,12 @@ export function getPanelHeirarchy(): PanelHeirarchyChildren {
type: PanelHeirarchyType.LEAF, type: PanelHeirarchyType.LEAF,
}); });
result.push({
name: 'Panel Test',
to: { name: 'panel_test' },
type: PanelHeirarchyType.LEAF,
});
return result; return result;
} }

View File

@@ -23,6 +23,11 @@ const router = createRouter({
name: 'chart', name: 'chart',
component: () => import('../views/ChartView.vue'), component: () => import('../views/ChartView.vue'),
}, },
{
path: '/panel_test',
name: 'panel_test',
component: () => import('../views/PanelTest.vue'),
},
], ],
}); });

View File

@@ -4,6 +4,9 @@ import InlineIcon from '@/components/InlineIcon.vue';
import ChartDefinitionView from '@/views/ChartDefinitionView.vue'; import ChartDefinitionView from '@/views/ChartDefinitionView.vue';
import type { TelemetryDefinition } from '@/composables/telemetry.ts'; import type { TelemetryDefinition } from '@/composables/telemetry.ts';
import ChartRenderView from '@/views/ChartRenderView.vue'; import ChartRenderView from '@/views/ChartRenderView.vue';
import ScreenLayout from '@/components/layout/ScreenLayout.vue';
import { Direction } from '@/composables/Direction.ts';
import { ScreenType } from '@/composables/ScreenType.ts';
const settingsOpen = ref(true); const settingsOpen = ref(true);
const settings = ref<TelemetryDefinition[][][]>([]); const settings = ref<TelemetryDefinition[][][]>([]);
@@ -17,7 +20,7 @@ function closeSettings() {
</script> </script>
<template> <template>
<div class="column stretch screen content tall wide"> <ScreenLayout :direction="Direction.Column" :type="ScreenType.WideTall">
<template v-if="!settingsOpen"> <template v-if="!settingsOpen">
<div class="settings column" @click="openSettings"> <div class="settings column" @click="openSettings">
<InlineIcon icon="hamburger menu"></InlineIcon> <InlineIcon icon="hamburger menu"></InlineIcon>
@@ -30,7 +33,7 @@ function closeSettings() {
</div> </div>
<ChartDefinitionView v-model="settings"></ChartDefinitionView> <ChartDefinitionView v-model="settings"></ChartDefinitionView>
</template> </template>
</div> </ScreenLayout>
</template> </template>
<style scoped lang="scss"> <style scoped lang="scss">

View File

@@ -8,6 +8,9 @@ import {
import PanelHeirarchy from '@/components/PanelHeirarchy.vue'; import PanelHeirarchy from '@/components/PanelHeirarchy.vue';
import router from '@/router'; import router from '@/router';
import TextInput from '@/components/TextInput.vue'; import TextInput from '@/components/TextInput.vue';
import ScreenLayout from '@/components/layout/ScreenLayout.vue';
import { Direction } from '@/composables/Direction.ts';
import { ScreenType } from '@/composables/ScreenType.ts';
const searchValue = ref(''); const searchValue = ref('');
@@ -30,7 +33,7 @@ function onEnter() {
</script> </script>
<template> <template>
<div class="column stretch screen limited content page"> <ScreenLayout :direction="Direction.Column" :type="ScreenType.Page" limit>
<div class="row"> <div class="row">
<TextInput <TextInput
autofocus autofocus
@@ -52,7 +55,7 @@ function onEnter() {
</div> </div>
</div> </div>
</div> </div>
</div> </ScreenLayout>
</template> </template>
<style scoped lang="scss"> <style scoped lang="scss">

View File

@@ -0,0 +1,28 @@
<script setup lang="ts">
import GridLayout from '@/components/layout/GridLayout.vue';
import TelemetryValue from '@/components/TelemetryValue.vue';
</script>
<template>
<GridLayout :cols="2" equal_col_width>
<span class="justify-right"> simple_producer/cos </span>
<TelemetryValue data="simple_producer/cos"></TelemetryValue>
<span class="justify-right"> simple_producer/sin </span>
<TelemetryValue data="simple_producer/sin"></TelemetryValue>
<span class="justify-right"> simple_producer/cos2 </span>
<TelemetryValue data="simple_producer/cos2"></TelemetryValue>
<span class="justify-right"> simple_producer/sin2 </span>
<TelemetryValue data="simple_producer/sin2"></TelemetryValue>
</GridLayout>
</template>
<style scoped lang="scss">
@use '@/assets/variables';
.justify-right {
justify-self: end;
}
</style>

View File

@@ -5,6 +5,9 @@ import TelemetryList from '@/components/TelemetryList.vue';
import type { TelemetryDefinition } from '@/composables/telemetry'; import type { TelemetryDefinition } from '@/composables/telemetry';
import TelemetryInfo from '@/components/TelemetryInfo.vue'; import TelemetryInfo from '@/components/TelemetryInfo.vue';
import FlexDivider from '@/components/FlexDivider.vue'; import FlexDivider from '@/components/FlexDivider.vue';
import ScreenLayout from '@/components/layout/ScreenLayout.vue';
import { Direction } from '@/composables/Direction.ts';
import { ScreenType } from '@/composables/ScreenType.ts';
const searchValue = ref(''); const searchValue = ref('');
@@ -13,7 +16,7 @@ const mousedover = ref<TelemetryDefinition | null>(null);
</script> </script>
<template> <template>
<div class="row stretch screen limited content"> <ScreenLayout :direction="Direction.Row" :type="ScreenType.Standard" limit>
<div class="column grow2 stretch no-min-height no-basis"> <div class="column grow2 stretch no-min-height no-basis">
<div class="row"> <div class="row">
<TextInput <TextInput
@@ -44,7 +47,7 @@ const mousedover = ref<TelemetryDefinition | null>(null);
:selection="selected" :selection="selected"
></TelemetryInfo> ></TelemetryInfo>
</div> </div>
</div> </ScreenLayout>
</template> </template>
<style lang="scss"> <style lang="scss">