From 33167d88db48b96c089ec4dff7a0acfc3e69a2e8 Mon Sep 17 00:00:00 2001
From: Nanashi <sevenc7c@sevenc7c.com>
Date: Wed, 14 Feb 2024 03:23:32 +0900
Subject: [PATCH] =?UTF-8?q?[=E3=82=BD=E3=83=B3=E3=82=B0]=20Improve:=20?=
 =?UTF-8?q?=E3=82=A8=E3=83=B3=E3=82=B8=E3=83=B3=E8=B5=B7=E5=8B=95=E5=89=8D?=
 =?UTF-8?q?=E3=81=AE=E7=94=BB=E9=9D=A2=E3=82=92=E8=89=AF=E3=81=84=E6=84=9F?=
 =?UTF-8?q?=E3=81=98=E3=81=AB=E3=81=99=E3=82=8B=20(#1790)?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit

* Add: エンジン起動中のポップアップを追加

* Refactor: 共通化

* Change: slotを使わないように

* Update: スタイルを移動

* Add: スケルトンを追加

* Fix: 細かいところを修正

* Fix: スタイルが消えてたので修正

* Change: EngineStartupPopup -> EngineStartupOverlay

* Change: コンポーネントを分割

* Change: ToolBarは無効化で表現するように

* Fix: スクロールバーが消えてたのを修正

* Delete: 未使用のスタイルを削除

* Delete: 未使用のスタイルを削除

---------

Co-authored-by: Hiroshiba <hihokaruta@gmail.com>
---
 src/components/EngineStartupOverlay.vue       | 126 +++++++++++++++++
 .../MenuButton.vue}                           |  38 +++--
 .../CharacterMenuButton/SelectedCharacter.vue | 132 ++++++++++++++++++
 src/components/Sing/SingEditor.vue            |   3 +
 src/components/Sing/ToolBar.vue               | 100 +------------
 src/components/Talk/TalkEditor.vue            | 117 +---------------
 6 files changed, 286 insertions(+), 230 deletions(-)
 create mode 100644 src/components/EngineStartupOverlay.vue
 rename src/components/Sing/{CharacterMenuButton.vue => CharacterMenuButton/MenuButton.vue} (93%)
 create mode 100644 src/components/Sing/CharacterMenuButton/SelectedCharacter.vue

diff --git a/src/components/EngineStartupOverlay.vue b/src/components/EngineStartupOverlay.vue
new file mode 100644
index 0000000000..ba78ca8048
--- /dev/null
+++ b/src/components/EngineStartupOverlay.vue
@@ -0,0 +1,126 @@
+<template>
+  <!-- TODO: 複数エンジン対応 -->
+  <!-- TODO: allEngineStateが "ERROR" のときエラーになったエンジンを探してトーストで案内 -->
+  <div v-if="allEngineState === 'FAILED_STARTING'" class="waiting-engine">
+    <div>エンジンの起動に失敗しました。エンジンの再起動をお試しください。</div>
+  </div>
+  <div
+    v-else-if="
+      !props.isCompletedInitialStartup || allEngineState === 'STARTING'
+    "
+    class="waiting-engine"
+  >
+    <div>
+      <q-spinner color="primary" size="2.5rem" />
+      <div class="q-mt-xs">
+        {{
+          allEngineState === "STARTING"
+            ? "エンジン起動中・・・"
+            : "データ準備中・・・"
+        }}
+      </div>
+
+      <template v-if="isEngineWaitingLong">
+        <q-separator spaced />
+        エンジン起動に時間がかかっています。<br />
+        <q-btn
+          v-if="isMultipleEngine"
+          outline
+          :disable="reloadingLocked"
+          @click="reloadAppWithMultiEngineOffMode"
+        >
+          マルチエンジンをオフにして再読み込みする</q-btn
+        >
+        <q-btn v-else outline @click="openQa">Q&Aを見る</q-btn>
+      </template>
+    </div>
+  </div>
+</template>
+<script setup lang="ts">
+import { computed, ref, watch } from "vue";
+import { useStore } from "@/store";
+import { EngineState } from "@/store/type";
+
+const store = useStore();
+const props =
+  defineProps<{
+    isCompletedInitialStartup: boolean;
+  }>();
+
+const reloadingLocked = computed(() => store.state.reloadingLock);
+const isMultipleEngine = computed(() => store.state.engineIds.length > 1);
+
+// エンジン待機
+// TODO: 個別のエンジンの状態をUIで確認できるようにする
+const allEngineState = computed(() => {
+  const engineStates = store.state.engineStates;
+
+  let lastEngineState: EngineState | undefined = undefined;
+
+  // 登録されているすべてのエンジンについて状態を確認する
+  for (const engineId of store.state.engineIds) {
+    const engineState: EngineState | undefined = engineStates[engineId];
+    if (engineState == undefined)
+      throw new Error(`No such engineState set: engineId == ${engineId}`);
+
+    // FIXME: 1つでも接続テストに成功していないエンジンがあれば、暫定的に起動中とする
+    if (engineState === "STARTING") {
+      return engineState;
+    }
+
+    lastEngineState = engineState;
+  }
+
+  return lastEngineState; // FIXME: 暫定的に1つのエンジンの状態を返す
+});
+
+const isEngineWaitingLong = ref<boolean>(false);
+let engineTimer: number | undefined = undefined;
+watch(allEngineState, (newEngineState) => {
+  if (engineTimer != undefined) {
+    clearTimeout(engineTimer);
+    engineTimer = undefined;
+  }
+  if (newEngineState === "STARTING") {
+    isEngineWaitingLong.value = false;
+    engineTimer = window.setTimeout(() => {
+      isEngineWaitingLong.value = true;
+    }, 30000);
+  } else {
+    isEngineWaitingLong.value = false;
+  }
+});
+
+const reloadAppWithMultiEngineOffMode = () => {
+  store.dispatch("CHECK_EDITED_AND_NOT_SAVE", {
+    closeOrReload: "reload",
+    isMultiEngineOffMode: true,
+  });
+};
+
+const openQa = () => {
+  window.open("https://voicevox.hiroshiba.jp/qa/", "_blank");
+};
+</script>
+
+<style scoped lang="scss">
+@use '@/styles/colors' as colors;
+@use '@/styles/variables' as vars;
+
+.waiting-engine {
+  background-color: rgba(colors.$display-rgb, 0.15);
+  position: absolute;
+  inset: 0;
+  z-index: 10;
+  display: flex;
+  text-align: center;
+  align-items: center;
+  justify-content: center;
+  > div {
+    color: colors.$display;
+    background: colors.$surface;
+    border-radius: 6px;
+    padding: 14px;
+  }
+}
+</style>
diff --git a/src/components/Sing/CharacterMenuButton.vue b/src/components/Sing/CharacterMenuButton/MenuButton.vue
similarity index 93%
rename from src/components/Sing/CharacterMenuButton.vue
rename to src/components/Sing/CharacterMenuButton/MenuButton.vue
index a9e8b32598..0c9ff8165d 100644
--- a/src/components/Sing/CharacterMenuButton.vue
+++ b/src/components/Sing/CharacterMenuButton/MenuButton.vue
@@ -1,6 +1,10 @@
 <template>
-  <q-btn flat class="q-pa-none">
-    <slot></slot>
+  <q-btn flat class="q-pa-none" :disable="uiLocked">
+    <selected-character
+      :show-skeleton="showSkeleton"
+      :selected-character-info="selectedCharacterInfo"
+      :selected-singer="selectedSinger"
+    />
     <q-menu
       class="character-menu"
       transition-show="none"
@@ -134,15 +138,22 @@
   </q-btn>
 </template>
 
+<script lang="ts">
+export default {
+  name: "CharacterMenuButton",
+};
+</script>
 <script setup lang="ts">
 import { computed, ref } from "vue";
 import { debounce } from "quasar";
+import SelectedCharacter from "./SelectedCharacter.vue";
 import { useStore } from "@/store";
 import { base64ImageToUri } from "@/helpers/imageHelper";
 import { SpeakerId, StyleId } from "@/type/preload";
 import { getStyleDescription } from "@/sing/viewHelper";
 
 const store = useStore();
+const uiLocked = computed(() => store.getters.UI_LOCKED);
 
 const userOrderedCharacterInfos = computed(() => {
   return store.getters.USER_ORDERED_CHARACTER_INFOS("singerLike");
@@ -160,6 +171,7 @@ const reassignSubMenuOpen = debounce((idx: number) => {
   arr[idx] = true;
   subMenuOpenFlags.value = arr;
 }, 100);
+const showSkeleton = computed(() => selectedCharacterInfo.value == undefined);
 
 const changeStyleId = (speakerUuid: SpeakerId, styleId: StyleId) => {
   const engineId = store.state.engineIds.find((_engineId) =>
@@ -205,6 +217,10 @@ const selectedCharacterInfo = computed(() => {
   return store.getters.CHARACTER_INFO(singer.engineId, singer.styleId);
 });
 
+const selectedSinger = computed(() => {
+  return store.getters.SELECTED_TRACK.singer;
+});
+
 const selectedSpeakerUuid = computed(() => {
   return selectedCharacterInfo.value?.metas.speakerUuid;
 });
@@ -235,20 +251,6 @@ const engineIcons = computed(() =>
 @use '@/styles/variables' as vars;
 @use '@/styles/colors' as colors;
 
-.character-name {
-  position: absolute;
-  top: 0px;
-  left: 0px;
-  padding: 1px 24px 1px 8px;
-  background-image: linear-gradient(
-    90deg,
-    rgba(colors.$background-rgb, 0.5) 0%,
-    rgba(colors.$background-rgb, 0.5) 75%,
-    transparent 100%
-  );
-  overflow-wrap: anywhere;
-}
-
 .character-menu {
   .q-item {
     color: colors.$display;
@@ -261,10 +263,6 @@ const engineIcons = computed(() =>
       background-color: rgba(colors.$primary-rgb, 0.1);
     }
   }
-  .selected-character-item,
-  .opened-character-item {
-    background-color: rgba(colors.$primary-rgb, 0.2);
-  }
   .engine-icon {
     position: absolute;
     width: 13px;
diff --git a/src/components/Sing/CharacterMenuButton/SelectedCharacter.vue b/src/components/Sing/CharacterMenuButton/SelectedCharacter.vue
new file mode 100644
index 0000000000..ec16378ab5
--- /dev/null
+++ b/src/components/Sing/CharacterMenuButton/SelectedCharacter.vue
@@ -0,0 +1,132 @@
+<template>
+  <div v-if="props.showSkeleton" class="selected-character">
+    <q-skeleton class="character-avatar" type="QAvatar" size="52px" />
+    <div class="character-info">
+      <q-skeleton
+        class="character-name skeleton"
+        type="rect"
+        width="65px"
+        height="15px"
+      />
+      <q-skeleton
+        class="character-style"
+        type="rect"
+        width="110px"
+        height="12px"
+      />
+    </div>
+  </div>
+  <div v-else class="selected-character">
+    <q-avatar
+      v-if="selectedStyleIconPath"
+      class="character-avatar"
+      size="3.5rem"
+    >
+      <img :src="selectedStyleIconPath" class="character-avatar-icon" />
+    </q-avatar>
+    <div class="character-info">
+      <div class="character-name">
+        {{ selectedCharacterName }}
+      </div>
+      <div class="character-style">
+        {{ selectedCharacterStyleDescription }}
+      </div>
+    </div>
+    <q-icon
+      name="arrow_drop_down"
+      size="sm"
+      class="character-menu-dropdown-icon"
+    />
+  </div>
+</template>
+
+<script setup lang="ts">
+import { computed } from "vue";
+import { Singer } from "@/store/type";
+import { CharacterInfo } from "@/type/preload";
+import { getStyleDescription } from "@/sing/viewHelper";
+
+const props =
+  defineProps<{
+    showSkeleton: boolean;
+    selectedCharacterInfo: CharacterInfo | undefined;
+    selectedSinger: Singer | undefined;
+  }>();
+
+const selectedCharacterName = computed(() => {
+  return props.selectedCharacterInfo?.metas.speakerName;
+});
+const selectedCharacterStyleDescription = computed(() => {
+  const style = props.selectedCharacterInfo?.metas.styles.find((style) => {
+    return (
+      style.styleId === props.selectedSinger?.styleId &&
+      style.engineId === props.selectedSinger?.engineId
+    );
+  });
+  return style != undefined ? getStyleDescription(style) : "";
+});
+const selectedStyleIconPath = computed(() => {
+  if (!props.selectedCharacterInfo || !props.selectedSinger) {
+    return;
+  }
+  const styles = props.selectedCharacterInfo.metas.styles;
+  return styles?.find((style) => {
+    return (
+      style.styleId === props.selectedSinger?.styleId &&
+      style.engineId === props.selectedSinger?.engineId
+    );
+  })?.iconPath;
+});
+</script>
+
+<style scoped lang="scss">
+@use '@/styles/variables' as vars;
+@use '@/styles/colors' as colors;
+
+.selected-character {
+  align-items: center;
+  display: flex;
+  padding: 0.25rem 0.5rem 0.25rem 0.25rem;
+  position: relative;
+
+  .character-avatar-icon {
+    display: block;
+    height: 100%;
+    object-fit: cover;
+    width: 100%;
+  }
+
+  .character-info {
+    align-items: start;
+    display: flex;
+    flex-direction: column;
+    margin-left: 0.5rem;
+    text-align: left;
+    justify-content: center;
+    white-space: nowrap;
+  }
+  .character-name {
+    font-size: 0.875rem;
+    font-weight: bold;
+    line-height: 1rem;
+    padding-top: 0.5rem;
+
+    &.skeleton {
+      margin-top: 0.4rem;
+      margin-bottom: 0.2rem;
+    }
+  }
+
+  .character-style {
+    color: rgba(colors.$display-rgb, 0.6);
+    font-size: 0.75rem;
+    font-weight: bold;
+    line-height: 1rem;
+  }
+
+  .character-menu-dropdown-icon {
+    color: rgba(colors.$display-rgb, 0.8);
+    margin-left: 0.25rem;
+  }
+}
+</style>
diff --git a/src/components/Sing/SingEditor.vue b/src/components/Sing/SingEditor.vue
index e6df509cc2..59f45a43ec 100644
--- a/src/components/Sing/SingEditor.vue
+++ b/src/components/Sing/SingEditor.vue
@@ -2,6 +2,7 @@
   <menu-bar />
   <tool-bar />
   <div class="sing-main">
+    <engine-startup-overlay :is-completed-initial-startup="isEnginesReady" />
     <div v-if="nowAudioExporting" class="exporting-dialog">
       <div>
         <q-spinner color="primary" size="2.5rem" />
@@ -34,6 +35,7 @@ import {
   DEFAULT_BPM,
   DEFAULT_TPQN,
 } from "@/sing/storeHelper";
+import EngineStartupOverlay from "@/components/EngineStartupOverlay.vue";
 import { useStore } from "@/store";
 
 const props = withDefaults(
@@ -121,6 +123,7 @@ const unwatchIsEnginesReady = watch(
 .sing-main {
   display: flex;
   overflow: hidden;
+  position: relative;
 }
 
 .exporting-dialog {
diff --git a/src/components/Sing/ToolBar.vue b/src/components/Sing/ToolBar.vue
index b548367df9..f09484274f 100644
--- a/src/components/Sing/ToolBar.vue
+++ b/src/components/Sing/ToolBar.vue
@@ -2,30 +2,7 @@
   <q-toolbar class="sing-toolbar">
     <!-- configs for entire song -->
     <div class="sing-configs">
-      <character-menu-button class="q-mr-sm">
-        <div class="character-menu-toggle">
-          <q-avatar
-            v-if="selectedStyleIconPath"
-            class="character-avatar"
-            size="48px"
-          >
-            <img :src="selectedStyleIconPath" class="character-avatar-icon" />
-          </q-avatar>
-          <div class="character-info">
-            <div class="character-name">
-              {{ selectedCharacterName }}
-            </div>
-            <div class="character-style">
-              {{ selectedCharacterStyleDescription }}
-            </div>
-          </div>
-          <q-icon
-            name="arrow_drop_down"
-            size="sm"
-            class="character-menu-dropdown-icon"
-          />
-        </div>
-      </character-menu-button>
+      <character-menu-button />
       <q-input
         type="number"
         :model-value="keyShiftInputBuffer"
@@ -137,43 +114,10 @@ import {
   isValidBpm,
   isValidVoiceKeyShift,
 } from "@/sing/domain";
-import CharacterMenuButton from "@/components/Sing/CharacterMenuButton.vue";
-import { getStyleDescription } from "@/sing/viewHelper";
+import CharacterMenuButton from "@/components/Sing/CharacterMenuButton/MenuButton.vue";
 
 const store = useStore();
 
-const userOrderedCharacterInfos = computed(() =>
-  store.getters.USER_ORDERED_CHARACTER_INFOS("singerLike")
-);
-const selectedCharacterInfo = computed(() => {
-  const singer = store.getters.SELECTED_TRACK.singer;
-  if (!userOrderedCharacterInfos.value || !singer) {
-    return undefined;
-  }
-  return store.getters.CHARACTER_INFO(singer.engineId, singer.styleId);
-});
-const selectedCharacterName = computed(() => {
-  return selectedCharacterInfo.value?.metas.speakerName;
-});
-const selectedCharacterStyleDescription = computed(() => {
-  const style = selectedCharacterInfo.value?.metas.styles.find((style) => {
-    const singer = store.getters.SELECTED_TRACK.singer;
-    return (
-      style.styleId === singer?.styleId && style.engineId === singer?.engineId
-    );
-  });
-  return style != undefined ? getStyleDescription(style) : "";
-});
-const selectedStyleIconPath = computed(() => {
-  const styles = selectedCharacterInfo.value?.metas.styles;
-  const singer = store.getters.SELECTED_TRACK.singer;
-  return styles?.find((style) => {
-    return (
-      style.styleId === singer?.styleId && style.engineId === singer?.engineId
-    );
-  })?.iconPath;
-});
-
 const tempos = computed(() => store.state.tempos);
 const timeSignatures = computed(() => store.state.timeSignatures);
 const keyShift = computed(() => store.getters.SELECTED_TRACK.voiceKeyShift);
@@ -377,46 +321,6 @@ onUnmounted(() => {
   }
 }
 
-.character-menu-toggle {
-  align-items: center;
-  display: flex;
-  padding: 4px 8px 8px 8px;
-  position: relative;
-}
-.character-avatar-icon {
-  display: block;
-  height: 100%;
-  object-fit: cover;
-  width: 100%;
-}
-
-.character-info {
-  align-items: start;
-  display: flex;
-  flex-direction: column;
-  margin-left: 0.5rem;
-  text-align: left;
-  justify-content: center;
-  white-space: nowrap;
-}
-.character-name {
-  font-size: 0.875rem;
-  font-weight: bold;
-  line-height: 1rem;
-  padding-top: 4px;
-}
-
-.character-style {
-  color: rgba(colors.$display-rgb, 0.73);
-  font-size: 11px;
-  line-height: 1rem;
-  vertical-align: text-bottom;
-}
-
-.character-menu-dropdown-icon {
-  color: rgba(colors.$display-rgb, 0.73);
-  margin-left: 0.25rem;
-}
 .sing-toolbar {
   background: colors.$sing-toolbar;
   align-items: center;
diff --git a/src/components/Talk/TalkEditor.vue b/src/components/Talk/TalkEditor.vue
index 54e996276d..2a1289343f 100644
--- a/src/components/Talk/TalkEditor.vue
+++ b/src/components/Talk/TalkEditor.vue
@@ -7,45 +7,10 @@
     <q-page-container>
       <q-page class="main-row-panes">
         <progress-view />
+        <engine-startup-overlay
+          :is-completed-initial-startup="isCompletedInitialStartup"
+        />
 
-        <!-- TODO: 複数エンジン対応 -->
-        <!-- TODO: allEngineStateが "ERROR" のときエラーになったエンジンを探してトーストで案内 -->
-        <div v-if="allEngineState === 'FAILED_STARTING'" class="waiting-engine">
-          <div>
-            エンジンの起動に失敗しました。エンジンの再起動をお試しください。
-          </div>
-        </div>
-        <div
-          v-else-if="
-            !isCompletedInitialStartup || allEngineState === 'STARTING'
-          "
-          class="waiting-engine"
-        >
-          <div>
-            <q-spinner color="primary" size="2.5rem" />
-            <div class="q-mt-xs">
-              {{
-                allEngineState === "STARTING"
-                  ? "エンジン起動中・・・"
-                  : "データ準備中・・・"
-              }}
-            </div>
-
-            <template v-if="isEngineWaitingLong">
-              <q-separator spaced />
-              エンジン起動に時間がかかっています。<br />
-              <q-btn
-                v-if="isMultipleEngine"
-                outline
-                :disable="reloadingLocked"
-                @click="reloadAppWithMultiEngineOffMode"
-              >
-                マルチエンジンをオフにして再読み込みする</q-btn
-              >
-              <q-btn v-else outline @click="openQa">Q&Aを見る</q-btn>
-            </template>
-          </div>
-        </div>
         <q-splitter
           horizontal
           reverse
@@ -207,7 +172,8 @@ import DictionaryManageDialog from "@/components/Dialog/DictionaryManageDialog.v
 import EngineManageDialog from "@/components/Dialog/EngineManageDialog.vue";
 import ProgressView from "@/components/ProgressView.vue";
 import UpdateNotificationDialogContainer from "@/components/Dialog/UpdateNotificationDialog/Container.vue";
-import { AudioItem, EngineState } from "@/store/type";
+import EngineStartupOverlay from "@/components/EngineStartupOverlay.vue";
+import { AudioItem } from "@/store/type";
 import {
   AudioKey,
   HotkeyActionType,
@@ -229,9 +195,6 @@ const store = useStore();
 
 const audioKeys = computed(() => store.state.audioKeys);
 const uiLocked = computed(() => store.getters.UI_LOCKED);
-const reloadingLocked = computed(() => store.state.reloadingLock);
-
-const isMultipleEngine = computed(() => store.state.engineIds.length > 1);
 
 // hotkeys handled by Mousetrap
 const hotkeyMap = new Map<HotkeyActionType, () => HotkeyReturnType>([
@@ -594,47 +557,6 @@ const unwatchIsEnginesReady = watch(
   }
 );
 
-// エンジン待機
-// TODO: 個別のエンジンの状態をUIで確認できるようにする
-const allEngineState = computed(() => {
-  const engineStates = store.state.engineStates;
-
-  let lastEngineState: EngineState | undefined = undefined;
-
-  // 登録されているすべてのエンジンについて状態を確認する
-  for (const engineId of store.state.engineIds) {
-    const engineState: EngineState | undefined = engineStates[engineId];
-    if (engineState == undefined)
-      throw new Error(`No such engineState set: engineId == ${engineId}`);
-
-    // FIXME: 1つでも接続テストに成功していないエンジンがあれば、暫定的に起動中とする
-    if (engineState === "STARTING") {
-      return engineState;
-    }
-
-    lastEngineState = engineState;
-  }
-
-  return lastEngineState; // FIXME: 暫定的に1つのエンジンの状態を返す
-});
-
-const isEngineWaitingLong = ref<boolean>(false);
-let engineTimer: number | undefined = undefined;
-watch(allEngineState, (newEngineState) => {
-  if (engineTimer != undefined) {
-    clearTimeout(engineTimer);
-    engineTimer = undefined;
-  }
-  if (newEngineState === "STARTING") {
-    isEngineWaitingLong.value = false;
-    engineTimer = window.setTimeout(() => {
-      isEngineWaitingLong.value = true;
-    }, 30000);
-  } else {
-    isEngineWaitingLong.value = false;
-  }
-});
-
 // 代替ポート情報の変更を監視
 watch(
   () => [store.state.altPortInfos, store.state.isVuexReady],
@@ -660,17 +582,6 @@ watch(
   }
 );
 
-const reloadAppWithMultiEngineOffMode = () => {
-  store.dispatch("CHECK_EDITED_AND_NOT_SAVE", {
-    closeOrReload: "reload",
-    isMultiEngineOffMode: true,
-  });
-};
-
-const openQa = () => {
-  window.open("https://voicevox.hiroshiba.jp/qa/", "_blank");
-};
-
 // ライセンス表示
 const isHelpDialogOpenComputed = computed({
   get: () => store.state.isHelpDialogOpen,
@@ -864,24 +775,6 @@ const onAudioCellPaneClick = () => {
   }
 }
 
-.waiting-engine {
-  background-color: rgba(colors.$display-rgb, 0.15);
-  position: absolute;
-  inset: 0;
-  z-index: 10;
-  display: flex;
-  text-align: center;
-  align-items: center;
-  justify-content: center;
-
-  > div {
-    color: colors.$display;
-    background: colors.$surface;
-    border-radius: 6px;
-    padding: 14px;
-  }
-}
-
 .main-row-panes {
   flex-grow: 1;
   flex-shrink: 1;