From 12ce2dc9545fd7a1981360271511ba83f3514494 Mon Sep 17 00:00:00 2001
From: Julien Touchais <5978-julien.touchais@users.noreply.forgemia.inra.fr>
Date: Tue, 23 Apr 2024 10:29:17 +0200
Subject: [PATCH 1/9] fix(target-view): :adhesive_bandage: use good
 modification colors

---
 src/components/SequenceBoard.vue | 12 ++++++++----
 src/views/TargetView.vue         | 31 +++++++++++++------------------
 2 files changed, 21 insertions(+), 22 deletions(-)

diff --git a/src/components/SequenceBoard.vue b/src/components/SequenceBoard.vue
index ef96ffd..d0a03bf 100644
--- a/src/components/SequenceBoard.vue
+++ b/src/components/SequenceBoard.vue
@@ -41,6 +41,10 @@ export interface highlightGroupModel {
   end: number
   /** Color(s) to use to highlight the group. */
   color: TailwindDefaultColorNameModel
+  /** Group name. */
+  name?: string
+  /** Group type. */
+  type?: string
 }
 
 /**
@@ -149,12 +153,12 @@ const isInAnyHighlightedGroup = (position: number): boolean =>
  * Set of all the boundary positions (start & end) of all highlighted groups on
  * the sequence.
  */
-const highlightedGroupsBoundaries = computed(
+const highlightedGroupsBoundaryPositions = computed(
   () =>
     props.highlightedGroups &&
     Object.values(props.highlightedGroups).reduce(
-      (highlightedGroupsBoundaries, highlightedGroup) =>
-        highlightedGroupsBoundaries
+      (highlightedGroupsBoundaryPositions, highlightedGroup) =>
+        highlightedGroupsBoundaryPositions
           .add(highlightedGroup.start)
           .add(highlightedGroup.end),
       new Set<number>()
@@ -168,7 +172,7 @@ const highlightedGroupsBoundaries = computed(
  * @returns `true` if the nucleotide is the start of an highlighted group, `false` otherwise.
  */
 const isHighlightedGroupBoundary = (position: number): boolean =>
-  !!highlightedGroupsBoundaries.value?.has(position)
+  !!highlightedGroupsBoundaryPositions.value?.has(position)
 
 /**
  * Composes the Tailwind color name in which to paint a nucleotide based on its
diff --git a/src/views/TargetView.vue b/src/views/TargetView.vue
index a0d5414..dbbc451 100644
--- a/src/views/TargetView.vue
+++ b/src/views/TargetView.vue
@@ -45,6 +45,7 @@ import { FeatureType, Strand } from '@/gql/codegen/graphql'
 import { formatSpeciesName, separateThousands } from '@/utils/textFormatting'
 import { targetByIdQuery } from '@/gql/queries'
 import { isDefined } from '@/typings/typeUtils'
+import { getModificationColor } from '@/utils/colors'
 
 /**
  * Utility constant to get the icon component corresponding to each strand value
@@ -230,23 +231,13 @@ const interactionList = computed<InteractionCardModel[] | undefined>(() => {
 })
 
 /**
- * Positions of the modifications, when known (only modifications when positive
- * modification position)
+ * Target modifications, filtered by keeping only ones with positive position
  */
-const modificationPositions = computed(() =>
+const filteredModifications = computed(() =>
   _uniq(
     target.value?.modifications.filter(
       (modification) => modification.position >= 0
     )
-  ).reduce(
-    (
-      modificationPositions: { [modificationId: string]: number },
-      modification
-    ) => ({
-      ...modificationPositions,
-      [modification.id]: modification.position
-    }),
-    {}
   )
 )
 
@@ -254,16 +245,20 @@ const modificationPositions = computed(() =>
  * Sequence chunks to highlight on the sequence of the target
  */
 const sequenceChunks = computed(() =>
-  Object.entries(modificationPositions.value).reduce(
+  filteredModifications.value.reduce(
     (
       sequenceChunks: { [groupId: string]: highlightGroupModel },
-      [modificationId, modificationPosition]
+      modification
     ) => ({
       ...sequenceChunks,
-      [modificationId]: {
-        start: modificationPosition,
-        end: modificationPosition,
-        color: 'slate' as TailwindDefaultColorNameModel
+      [modification.id]: {
+        start: modification.position,
+        end: modification.position,
+        color: modification.type
+          ? getModificationColor(modification.type)
+          : ('slate' as TailwindDefaultColorNameModel),
+        name: modification.name,
+        type: modification.type || undefined
       }
     }),
     {}
-- 
GitLab


From 73ebfcc5c743f379b5ec100c6dd69a3a002e7df8 Mon Sep 17 00:00:00 2001
From: Julien Touchais <5978-julien.touchais@users.noreply.forgemia.inra.fr>
Date: Wed, 24 Apr 2024 09:37:09 +0200
Subject: [PATCH 2/9] feat(base-components): :sparkles: add a 'longFormat'
 version for FormattedModificationType

---
 src/components/FormattedModificationType.vue | 8 +++++---
 src/views/GuideView.vue                      | 5 ++++-
 src/views/TargetView.vue                     | 2 +-
 3 files changed, 10 insertions(+), 5 deletions(-)

diff --git a/src/components/FormattedModificationType.vue b/src/components/FormattedModificationType.vue
index b3f2574..c5fe663 100644
--- a/src/components/FormattedModificationType.vue
+++ b/src/components/FormattedModificationType.vue
@@ -5,14 +5,16 @@
 import { ModifType } from '@/gql/codegen/graphql'
 
 defineProps<{
-  /** The code of the modification type to format */
-  typeCode?: ModifType
+  /** The code of the modification type to format. */
+  typeCode: ModifType
+  /** If to use a long format or a short (default) one. */
+  longFormat?: boolean
 }>()
 </script>
 
 <template>
   <span v-if="typeCode === ModifType.TwoPOMe"
-    >2'-<em class="italic">O</em>-me</span
+    >2'-<em class="italic">O</em>-{{ longFormat ? 'methylation' : 'me' }}</span
   >
   <span v-else-if="typeCode === ModifType.Psi">Pseudouridylation</span>
   <span v-else><em>Unknown type</em></span>
diff --git a/src/views/GuideView.vue b/src/views/GuideView.vue
index 5287bf4..f4952a8 100644
--- a/src/views/GuideView.vue
+++ b/src/views/GuideView.vue
@@ -664,7 +664,10 @@ const ontologyLinks = computed(() => ({
 
             <template #item-label-2="{ currentValue }">
               <strong>{{ currentValue.modification.name }}</strong>
-              <em class="italic text-slate-400">
+              <em
+                v-if="currentValue.modification.type"
+                class="italic text-slate-400"
+              >
                 {{ ' - ' }}
                 <FormattedModificationType
                   :type-code="currentValue.modification.type"
diff --git a/src/views/TargetView.vue b/src/views/TargetView.vue
index dbbc451..7ae33c2 100644
--- a/src/views/TargetView.vue
+++ b/src/views/TargetView.vue
@@ -600,7 +600,7 @@ const ontologyLinks = computed(() => ({
           >
             <template #item-label-1="{ currentValue }">
               <strong>{{ currentValue.modification.name }}</strong>
-              <em class="italic text-slate-400">
+              <em v-if="currentValue.modification.type" class="italic text-slate-400">
                 {{ ' - ' }}
                 <FormattedModificationType
                   :type-code="currentValue.modification.type"
-- 
GitLab


From 60257b52d82787830fd536285acc53bc182b3952 Mon Sep 17 00:00:00 2001
From: Julien Touchais <5978-julien.touchais@users.noreply.forgemia.inra.fr>
Date: Wed, 24 Apr 2024 09:48:40 +0200
Subject: [PATCH 3/9] feat(linear-sequence): :sparkles: use a Prime's ToolBar
 for controls

---
 src/components/SequenceBoard.vue | 95 +++++++++++++++++---------------
 1 file changed, 51 insertions(+), 44 deletions(-)

diff --git a/src/components/SequenceBoard.vue b/src/components/SequenceBoard.vue
index d0a03bf..04276e9 100644
--- a/src/components/SequenceBoard.vue
+++ b/src/components/SequenceBoard.vue
@@ -8,6 +8,7 @@ import { ref, computed, onMounted, watch } from 'vue'
  */
 import Dropdown from 'primevue/dropdown'
 import Button from 'primevue/button'
+import Toolbar from 'primevue/toolbar'
 import IconFa6SolidDna from '~icons/fa6-solid/dna'
 import IconFa6SolidCircleCheck from '~icons/fa6-solid/circle-check'
 import IconFa6RegularCopy from '~icons/fa6-regular/copy'
@@ -535,51 +536,57 @@ watch(isSequenceContainerElementVisible, (visibility) => {
 </script>
 
 <template>
-  <div class="controls mb-4 flex items-center justify-between">
-    <label class="flex flex-col items-start text-sm">
-      Group length
-      <Dropdown
-        v-model="nucleotideGroupsSize"
-        :options="[5, 10, 20, 50, 100]"
-        class="mt-2 w-52"
-        @hide="updateAndRedraw"
-      >
-        <template #option="{ option }">{{ option }} nucleotides</template>
-        <template #value="{ value }">{{ value }} nucleotides</template>
-      </Dropdown>
-    </label>
-    <Button
-      outlined
-      severity="secondary"
-      :class="[
-        clipboardState === ClipboardStateModel.PostCopy &&
-          '!bg-lime-100 !text-lime-700',
-        'flex min-w-[18ch] justify-center'
-      ]"
-      :loading="clipboardState === ClipboardStateModel.Busy"
-      @click="sequenceToClipboard"
-    >
-      <span
-        v-if="clipboardState === ClipboardStateModel.Busy"
-        class="mr-2 h-5 overflow-hidden text-xl"
+  <Toolbar>
+    <template #start>
+      <label class="flex flex-col items-start text-sm">
+        Group length
+        <Dropdown
+          v-model="nucleotideGroupsSize"
+          :options="[5, 10, 20, 50, 100]"
+          class="mt-2 w-52"
+          @hide="updateAndRedraw"
+        >
+          <template #option="{ option }">{{ option }} nucleotides</template>
+          <template #value="{ value }">{{ value }} nucleotides</template>
+        </Dropdown>
+      </label>
+    </template>
+
+    <template #end>
+      <Button
+        outlined
+        severity="secondary"
+        :class="[
+          clipboardState === ClipboardStateModel.PostCopy &&
+            '!bg-lime-100 !text-lime-700',
+          'flex min-w-[18ch] justify-center'
+        ]"
+        :loading="clipboardState === ClipboardStateModel.Busy"
+        @click="sequenceToClipboard"
       >
-        <icon-fa6-solid-dna class="animate-[dnaSpin_.5s_linear_infinite]" />
-        <icon-fa6-solid-dna class="animate-[dnaSpin_.5s_linear_infinite]" />
-      </span>
-      <icon-fa6-solid-circle-check
-        v-else-if="clipboardState === ClipboardStateModel.PostCopy"
-        class="mr-2 text-xl"
-      />
-      <icon-fa6-regular-copy v-else class="mr-2 text-xl" />
-      {{
-        clipboardState === ClipboardStateModel.Busy
-          ? 'Copying...'
-          : clipboardState === ClipboardStateModel.PostCopy
-          ? 'Copied !'
-          : 'Copy sequence'
-      }}
-    </Button>
-  </div>
+        <span
+          v-if="clipboardState === ClipboardStateModel.Busy"
+          class="mr-2 h-5 overflow-hidden text-xl"
+        >
+          <icon-fa6-solid-dna class="animate-[dnaSpin_.5s_linear_infinite]" />
+          <icon-fa6-solid-dna class="animate-[dnaSpin_.5s_linear_infinite]" />
+        </span>
+        <icon-fa6-solid-circle-check
+          v-else-if="clipboardState === ClipboardStateModel.PostCopy"
+          class="mr-2 text-xl"
+        />
+        <icon-fa6-regular-copy v-else class="mr-2 text-xl" />
+        {{
+          clipboardState === ClipboardStateModel.Busy
+            ? 'Copying...'
+            : clipboardState === ClipboardStateModel.PostCopy
+            ? 'Copied !'
+            : 'Copy sequence'
+        }}
+      </Button>
+    </template>
+  </Toolbar>
+
   <div ref="sequenceAndLabelsContainerElement" class="relative px-8">
     <span class="absolute left-0 top-8 italic text-slate-400">5' &rarr;</span>
     <div
-- 
GitLab


From fc7681a5b8257cf1c0730be70cc7a5870e1e83d9 Mon Sep 17 00:00:00 2001
From: Julien Touchais <5978-julien.touchais@users.noreply.forgemia.inra.fr>
Date: Wed, 24 Apr 2024 09:49:13 +0200
Subject: [PATCH 4/9] feat(base-components): :sparkles: add a component to
 display captions

---
 src/components/BaseLegendButtonOverlay.vue | 142 +++++++++++++++++++++
 1 file changed, 142 insertions(+)
 create mode 100644 src/components/BaseLegendButtonOverlay.vue

diff --git a/src/components/BaseLegendButtonOverlay.vue b/src/components/BaseLegendButtonOverlay.vue
new file mode 100644
index 0000000..69ac0b8
--- /dev/null
+++ b/src/components/BaseLegendButtonOverlay.vue
@@ -0,0 +1,142 @@
+<script setup lang="ts">
+/**
+ * Vue imports
+ */
+import { ref } from 'vue'
+/**
+ * Component imports
+ */
+import Button from 'primevue/button'
+import OverlayPanel from 'primevue/overlaypanel'
+import IconCarbonLegend from '~icons/carbon/legend'
+/**
+ * Types imports
+ */
+import type { TailwindDefaultColorNameModel } from '@/typings/styleTypes'
+
+/**
+ * An legend item.
+ */
+export interface LegendItemModel {
+  /** Item id. */
+  id: string
+  /** Item title. */
+  title: string
+  /** Item description. */
+  description?: string
+  /** Tailwind color name of the color to legend. */
+  color?: TailwindDefaultColorNameModel
+}
+
+/**
+ * Component props.
+ */
+defineProps<{
+  /** Text to display on the button which reveals the legend. */
+  buttonText: string
+  /** The legend items. */
+  items: LegendItemModel[]
+  /** The opacity to apply on the legend overlay. */
+  opacity?: number
+}>()
+
+/**
+ * Component slots.
+ */
+defineSlots<{
+  /** Custom button template. */
+  button: () => any
+  /** Custom whole legend template. */
+  legend: () => any
+  /** Custom whole legend items template. */
+  item: (props: {
+    /** The item to customise. */
+    item: LegendItemModel
+  }) => any
+  /** Custom legend items icon template (what is being captioned). */
+  'item-icon': (props: {
+    /** The item of which to customise the icon. */
+    item: LegendItemModel
+  }) => any
+  /** Custom legend items title template. */
+  'item-title': (props: {
+    /** The item of which to customise the title. */ item: LegendItemModel
+  }) => any
+  /** Custom legend items title template. */
+  'item-description': (props: {
+    /** The item of which to customise the description. */ item: LegendItemModel
+  }) => any
+}>()
+
+/**
+ * Template ref to the OverlayPanel component containing the legend.
+ */
+const legendOverlayPanelComponent = ref<OverlayPanel>()
+
+/**
+ * Toggles the visibility of the legend overlay.
+ * @param event The event triggering the function call (used to retrieve the DOM
+ * element to which link the overlay — probably a button).
+ */
+const toggleLegend = (event: Event) => {
+  legendOverlayPanelComponent.value?.toggle(event)
+}
+</script>
+
+<template>
+  <slot name="button">
+    <Button
+      severity="secondary"
+      outlined
+      class="flex gap-2 !shadow-none"
+      @click="toggleLegend"
+    >
+      <icon-carbon-legend class="text-xl" />
+      {{ buttonText }}
+    </Button>
+  </slot>
+
+  <OverlayPanel
+    ref="legendOverlayPanelComponent"
+    :style="{ opacity: opacity && 0 <= opacity && opacity <= 1 ? opacity : 1 }"
+  >
+    <slot name="legend">
+      <ul class="flex max-w-lg flex-col gap-3">
+        <li
+          v-for="(legendItem, index) in items"
+          :key="index"
+          class="items-top flex gap-3"
+        >
+          <slot name="item" :item="legendItem">
+            <slot name="item-icon" :item="legendItem">
+              <svg
+                class="min-w-max"
+                viewBox="0 0 32 32"
+                width="1.25em"
+                height="1.25em"
+              >
+                <circle
+                  :class="`fill-${legendItem.color}-100 stroke-${legendItem.color}-600`"
+                  cx="16"
+                  cy="16"
+                  r="14"
+                  stroke-width="4"
+                />
+              </svg>
+            </slot>
+            <div class="-mt-0.5 flex flex-col italic text-slate-400">
+              <slot name="item-title" :item="legendItem">
+                <em class="font-bold not-italic text-slate-600">
+                  {{ legendItem.title }}
+                </em>
+              </slot>
+              <slot name="item-description" :item="legendItem">
+                {{ legendItem.description }}
+              </slot>
+            </div>
+          </slot>
+        </li>
+      </ul>
+    </slot>
+  </OverlayPanel>
+</template>
-- 
GitLab


From b7e113bade9bafb8d0f5cbbbe98f0682b2d0e3dc Mon Sep 17 00:00:00 2001
From: Julien Touchais <5978-julien.touchais@users.noreply.forgemia.inra.fr>
Date: Wed, 24 Apr 2024 09:50:04 +0200
Subject: [PATCH 5/9] feat(linear-sequence): :sparkles: add optional captions

---
 src/components/SequenceBoard.vue | 47 +++++++++++++++++++++++++++++++-
 1 file changed, 46 insertions(+), 1 deletion(-)

diff --git a/src/components/SequenceBoard.vue b/src/components/SequenceBoard.vue
index 04276e9..5583f97 100644
--- a/src/components/SequenceBoard.vue
+++ b/src/components/SequenceBoard.vue
@@ -6,6 +6,7 @@ import { ref, computed, onMounted, watch } from 'vue'
 /**
  * Components imports
  */
+import BaseLegendButtonOverlay from '@/components/BaseLegendButtonOverlay.vue'
 import Dropdown from 'primevue/dropdown'
 import Button from 'primevue/button'
 import Toolbar from 'primevue/toolbar'
@@ -25,7 +26,8 @@ import { inRange as _inRange } from 'lodash-es'
 /**
  * Types imports
  */
-import { type TailwindDefaultColorNameModel } from '@/typings/styleTypes'
+import type { TailwindDefaultColorNameModel } from '@/typings/styleTypes'
+import type { LegendItemModel } from '@/components/BaseLegendButtonOverlay.vue'
 /**
  * Utils imports
  */
@@ -68,6 +70,32 @@ const props = defineProps<{
   sequence: string
   /** Groups to highlight on the sequence. */
   highlightedGroups?: { [groupId: string]: highlightGroupModel }
+  /** The items of the legend to display for the highlighted groups. */
+  legendItems?: LegendItemModel[]
+}>()
+
+/**
+ * Component slots.
+ */
+defineSlots<{
+  /** Custom whole legend items template. */
+  'legend-item': (props: {
+    /** The item to customise. */
+    item: LegendItemModel
+  }) => any
+  /** Custom legend items icon template (what is being captioned). */
+  'legend-item-icon': (props: {
+    /** The item of which to customise the icon. */
+    item: LegendItemModel
+  }) => any
+  /** Custom legend items title template. */
+  'legend-item-title': (props: {
+    /** The item of which to customise the title. */ item: LegendItemModel
+  }) => any
+  /** Custom legend items title template. */
+  'legend-item-description': (props: {
+    /** The item of which to customise the description. */ item: LegendItemModel
+  }) => any
 }>()
 
 /**
@@ -552,6 +580,23 @@ watch(isSequenceContainerElementVisible, (visibility) => {
       </label>
     </template>
 
+    <template v-if="legendItems" #center>
+      <BaseLegendButtonOverlay button-text="Legend" :items="legendItems">
+        <template #item="{ item }">
+          <slot name="legend-item" :item="item" />
+        </template>
+        <template #item-icon="{ item }">
+          <slot name="legend-item-icon" :item="item" />
+        </template>
+        <template #item-title="{ item }">
+          <slot name="legend-item-title" :item="item" />
+        </template>
+        <template #item-description="{ item }">
+          <slot name="legend-item-description" :item="item" />
+        </template>
+      </BaseLegendButtonOverlay>
+    </template>
+
     <template #end>
       <Button
         outlined
-- 
GitLab


From fd4ffd9e27b9a7dcd7c008c64c7ac5d6f3fd6dfe Mon Sep 17 00:00:00 2001
From: Julien Touchais <5978-julien.touchais@users.noreply.forgemia.inra.fr>
Date: Wed, 24 Apr 2024 09:50:46 +0200
Subject: [PATCH 6/9] feat(guide-view): :sparkles: add captions to linear
 sequence on guide view

---
 src/views/GuideView.vue | 45 +++++++++++++++++++++++++++++++++++++++--
 1 file changed, 43 insertions(+), 2 deletions(-)

diff --git a/src/views/GuideView.vue b/src/views/GuideView.vue
index f4952a8..4cab84f 100644
--- a/src/views/GuideView.vue
+++ b/src/views/GuideView.vue
@@ -36,7 +36,13 @@ import { useTitle } from '@vueuse/core'
 import type { TailwindDefaultColorNameModel } from '@/typings/styleTypes'
 import type { InteractionCardModel } from '@/components/InteractionCard.vue'
 import type { highlightGroupModel } from '@/components/SequenceBoard.vue'
-import { FeatureType, GuideType, Strand } from '@/gql/codegen/graphql'
+import {
+  FeatureType,
+  GuideType,
+  SequenceType,
+  Strand
+} from '@/gql/codegen/graphql'
+import type { LegendItemModel } from '@/components/BaseLegendButtonOverlay.vue'
 /**
  * Utils imports
  */
@@ -59,6 +65,30 @@ const STRAND_CODE_TO_ICON_COMPONENT = {
   [Strand.NotStranded]: IconFa6SolidCircleXmark
 }
 
+/**
+ * The legend to display on the sequence board.
+ */
+const GUIDE_LEGEND_ITEMS: LegendItemModel[] = [
+  {
+    id: 'CBox',
+    title: 'C box',
+    color: getBoxColor(SequenceType.CBox)
+  },
+  {
+    id: 'CBox',
+    title: 'D box',
+    color: getBoxColor(SequenceType.DBox)
+  },
+  {
+    id: 'ModFacing',
+    title: 'Facing the modification',
+    description:
+      'In the duplex, the nucleotide which is in front of the modified one on\
+      the target.',
+    color: 'slate'
+  }
+]
+
 /**
  * Component props.
  */
@@ -628,7 +658,18 @@ const ontologyLinks = computed(() => ({
         v-if="guide?.seq"
         :sequence="guide.seq.replace(/T/g, 'U')"
         :highlighted-groups="sequenceChunks"
-      />
+        :legend-items="GUIDE_LEGEND_ITEMS"
+      >
+        <template #legend-item-title="{ item }">
+          <span
+            v-if="item.id === 'CBox' || item.id === 'DBox'"
+            class="font-bold not-italic text-slate-600"
+          >
+            <span class="italic">{{ item.title[0] }}</span
+            >{{ item.title.slice(1) }}
+          </span>
+        </template>
+      </SequenceBoard>
     </Panel>
 
     <Panel
-- 
GitLab


From 4dc1e4a0d59e4953098f2c25706d500e46a81001 Mon Sep 17 00:00:00 2001
From: Julien Touchais <5978-julien.touchais@users.noreply.forgemia.inra.fr>
Date: Wed, 24 Apr 2024 09:51:08 +0200
Subject: [PATCH 7/9] feat(target-view): :sparkles: add captions to linear
 sequence

---
 src/views/TargetView.vue | 37 +++++++++++++++++++++++++++++++++++--
 1 file changed, 35 insertions(+), 2 deletions(-)

diff --git a/src/views/TargetView.vue b/src/views/TargetView.vue
index 7ae33c2..b93eadb 100644
--- a/src/views/TargetView.vue
+++ b/src/views/TargetView.vue
@@ -38,7 +38,7 @@ import type { TailwindDefaultColorNameModel } from '@/typings/styleTypes'
 import type { InteractionCardModel } from '@/components/InteractionCard.vue'
 import type { C4GGraphModel, C4GNodeModel } from '@/typings/Codev4GraphFormat'
 import type { highlightGroupModel } from '@/components/SequenceBoard.vue'
-import { FeatureType, Strand } from '@/gql/codegen/graphql'
+import { FeatureType, ModifType, Strand } from '@/gql/codegen/graphql'
 /**
  * Utils imports
  */
@@ -46,6 +46,7 @@ import { formatSpeciesName, separateThousands } from '@/utils/textFormatting'
 import { targetByIdQuery } from '@/gql/queries'
 import { isDefined } from '@/typings/typeUtils'
 import { getModificationColor } from '@/utils/colors'
+import type { LegendItemModel } from '@/components/BaseLegendButtonOverlay.vue'
 
 /**
  * Utility constant to get the icon component corresponding to each strand value
@@ -57,6 +58,28 @@ const STRAND_CODE_TO_ICON_COMPONENT = {
   [Strand.NotStranded]: IconFa6SolidCircleXmark
 }
 
+/**
+ * The legend to display on the sequence board.
+ */
+const TARGET_LEGEND_ITEMS: LegendItemModel[] = [
+  {
+    id: 'Nm',
+    title: 'Nm',
+    color: getModificationColor(ModifType.TwoPOMe)
+  },
+  {
+    id: 'Psi',
+    title: 'Pseudouridylation',
+    color: getModificationColor(ModifType.Psi)
+  },
+  {
+    id: 'Other',
+    title: 'Unknown',
+    description: 'Modification of a different type than those listed above.',
+    color: getModificationColor(ModifType.Other)
+  }
+]
+
 /**
  * Component props
  */
@@ -571,7 +594,17 @@ const ontologyLinks = computed(() => ({
         v-if="target?.seq"
         :sequence="target.seq.replace(/T/g, 'U')"
         :highlighted-groups="sequenceChunks"
-      />
+        :legend-items="TARGET_LEGEND_ITEMS"
+      >
+        <template #legend-item-title="{ item }">
+          <FormattedModificationType
+            v-if="item.id === 'Nm'"
+            class="font-bold not-italic text-slate-600"
+            :type-code="ModifType.TwoPOMe"
+            long-format
+          />
+        </template>
+      </SequenceBoard>
     </Panel>
 
     <Panel
-- 
GitLab


From 11c928251707aba1b3a0b9073ad9b63651e10991 Mon Sep 17 00:00:00 2001
From: Julien Touchais <5978-julien.touchais@users.noreply.forgemia.inra.fr>
Date: Wed, 24 Apr 2024 10:16:19 +0200
Subject: [PATCH 8/9] docs(config): :bulb: add BaseLegendButtonOverlay to
 Tailwind whitelist

---
 tailwind.config.js | 8 ++++++--
 1 file changed, 6 insertions(+), 2 deletions(-)

diff --git a/tailwind.config.js b/tailwind.config.js
index 092303f..0e42ba7 100644
--- a/tailwind.config.js
+++ b/tailwind.config.js
@@ -40,11 +40,15 @@ export default {
       variants: ['hover']
     },
     {
-      // InteractionCardCD, InteractionCardHACA
-      pattern: /^fill-[a-z]+-(1|6)00$/
+      // InteractionCardCD, InteractionCardHACA, BaseLegendButtonOverlay
+      pattern: /^fill-[a-z]+-100$/
     },
     {
       // InteractionCardCD, InteractionCardHACA
+      pattern: /^fill-[a-z]+-600$/
+    },
+    {
+      // InteractionCardCD, InteractionCardHACA, BaseLegendButtonOverlay
       pattern: /^stroke-[a-z]+-600$/
     },
     'brightness-90', // ChromosomeMagnify
-- 
GitLab


From b8191950785593b65f73fc0bd870378815247216 Mon Sep 17 00:00:00 2001
From: Julien Touchais <5978-julien.touchais@users.noreply.forgemia.inra.fr>
Date: Wed, 24 Apr 2024 12:58:51 +0200
Subject: [PATCH 9/9] feat(2D-struct): :sparkles: add captions

---
 src/components/BaseLegendButtonOverlay.vue | 109 +++++++++++----------
 src/components/SecondaryStructure.vue      |  60 +++++++++++-
 2 files changed, 115 insertions(+), 54 deletions(-)

diff --git a/src/components/BaseLegendButtonOverlay.vue b/src/components/BaseLegendButtonOverlay.vue
index 69ac0b8..346c550 100644
--- a/src/components/BaseLegendButtonOverlay.vue
+++ b/src/components/BaseLegendButtonOverlay.vue
@@ -84,59 +84,62 @@ const toggleLegend = (event: Event) => {
 </script>
 
 <template>
-  <slot name="button">
-    <Button
-      severity="secondary"
-      outlined
-      class="flex gap-2 !shadow-none"
-      @click="toggleLegend"
+  <div>
+    <slot name="button">
+      <Button
+        severity="secondary"
+        outlined
+        class="flex gap-2 !shadow-none"
+        @click="toggleLegend"
+      >
+        <icon-carbon-legend class="text-xl" />
+        {{ buttonText }}
+      </Button>
+    </slot>
+    <OverlayPanel
+      ref="legendOverlayPanelComponent"
+      :style="{
+        opacity: opacity && 0 <= opacity && opacity <= 1 ? opacity : 1
+      }"
     >
-      <icon-carbon-legend class="text-xl" />
-      {{ buttonText }}
-    </Button>
-  </slot>
-
-  <OverlayPanel
-    ref="legendOverlayPanelComponent"
-    :style="{ opacity: opacity && 0 <= opacity && opacity <= 1 ? opacity : 1 }"
-  >
-    <slot name="legend">
-      <ul class="flex max-w-lg flex-col gap-3">
-        <li
-          v-for="(legendItem, index) in items"
-          :key="index"
-          class="items-top flex gap-3"
-        >
-          <slot name="item" :item="legendItem">
-            <slot name="item-icon" :item="legendItem">
-              <svg
-                class="min-w-max"
-                viewBox="0 0 32 32"
-                width="1.25em"
-                height="1.25em"
-              >
-                <circle
-                  :class="`fill-${legendItem.color}-100 stroke-${legendItem.color}-600`"
-                  cx="16"
-                  cy="16"
-                  r="14"
-                  stroke-width="4"
-                />
-              </svg>
-            </slot>
-            <div class="-mt-0.5 flex flex-col italic text-slate-400">
-              <slot name="item-title" :item="legendItem">
-                <em class="font-bold not-italic text-slate-600">
-                  {{ legendItem.title }}
-                </em>
+      <slot name="legend">
+        <ul class="flex max-w-lg flex-col gap-3">
+          <li
+            v-for="(legendItem, index) in items"
+            :key="index"
+            class="items-top flex gap-3"
+          >
+            <slot name="item" :item="legendItem">
+              <slot name="item-icon" :item="legendItem">
+                <svg
+                  class="min-w-max"
+                  viewBox="0 0 32 32"
+                  width="1.25em"
+                  height="1.25em"
+                >
+                  <circle
+                    :class="`fill-${legendItem.color}-100 stroke-${legendItem.color}-600`"
+                    cx="16"
+                    cy="16"
+                    r="14"
+                    stroke-width="4"
+                  />
+                </svg>
               </slot>
-              <slot name="item-description" :item="legendItem">
-                {{ legendItem.description }}
-              </slot>
-            </div>
-          </slot>
-        </li>
-      </ul>
-    </slot>
-  </OverlayPanel>
+              <div class="-mt-0.5 flex flex-col italic text-slate-400">
+                <slot name="item-title" :item="legendItem">
+                  <em class="font-bold not-italic text-slate-600">
+                    {{ legendItem.title }}
+                  </em>
+                </slot>
+                <slot name="item-description" :item="legendItem">
+                  {{ legendItem.description }}
+                </slot>
+              </div>
+            </slot>
+          </li>
+        </ul>
+      </slot>
+    </OverlayPanel>
+  </div>
 </template>
diff --git a/src/components/SecondaryStructure.vue b/src/components/SecondaryStructure.vue
index 364952f..5f81c83 100644
--- a/src/components/SecondaryStructure.vue
+++ b/src/components/SecondaryStructure.vue
@@ -6,7 +6,9 @@ import { ref, onMounted, computed } from 'vue'
 /**
  * Components imports
  */
-import BaseDescribedChip from './BaseDescribedChip.vue'
+import BaseDescribedChip from '@/components/BaseDescribedChip.vue'
+import BaseLegendButtonOverlay from '@/components/BaseLegendButtonOverlay.vue'
+import FormattedModificationType from '@/components/FormattedModificationType.vue'
 import Button from 'primevue/button'
 import SelectButton from 'primevue/selectbutton'
 import Dialog from 'primevue/dialog'
@@ -37,6 +39,7 @@ import type {
   DataUrlType as G6DataUrlType
 } from '@antv/g6'
 import { ModifType } from '@/gql/codegen/graphql'
+import type { LegendItemModel } from './BaseLegendButtonOverlay.vue'
 /**
  * Utils imports
  */
@@ -78,6 +81,28 @@ const GRAPH_CAPTURE_OPTIONS = [
  */
 const GRAPH_DOWNLOAD_FORMAT: G6DataUrlType = 'image/png'
 
+/**
+ * The legend to display on the sequence board.
+ */
+const SECONDARY_STRUCTURE_LEGEND_ITEMS: LegendItemModel[] = [
+  {
+    id: 'Nm',
+    title: 'Nm',
+    color: getModificationColor(ModifType.TwoPOMe)
+  },
+  {
+    id: 'Psi',
+    title: 'Pseudouridylation',
+    color: getModificationColor(ModifType.Psi)
+  },
+  {
+    id: 'Other',
+    title: 'Unknown',
+    description: 'Modification of a different type than those listed above.',
+    color: getModificationColor(ModifType.Other)
+  }
+]
+
 /**
  * Applies modifications to the node object based on the metadata of the
  * modification of the nucleotide represented by that node, and return a node
@@ -589,6 +614,39 @@ onMounted(() => {
         <icon-fluent-scan-camera28-regular class="mr-2 text-3xl" />
         Capture
       </Button>
+      <BaseLegendButtonOverlay
+        button-text="Legend"
+        :items="SECONDARY_STRUCTURE_LEGEND_ITEMS"
+        class="!absolute left-4 top-4"
+      >
+        <template #item-icon="{ item }">
+          <svg
+            class="min-w-max"
+            viewBox="0 0 32 32"
+            width="1.25em"
+            height="1.25em"
+          >
+            <rect
+              :class="`fill-${item.color}-100 stroke-${item.color}-600`"
+              x="2"
+              y="2"
+              width="24"
+              height="24"
+              rx="4"
+              stroke-width="4"
+            />
+          </svg>
+        </template>
+
+        <template #item-title="{ item }">
+          <FormattedModificationType
+            v-if="item.id === 'Nm'"
+            class="font-bold not-italic text-slate-600"
+            :type-code="ModifType.TwoPOMe"
+            long-format
+          />
+        </template>
+      </BaseLegendButtonOverlay>
     </div>
     <p
       v-if="graphMetadata.software?.name"
-- 
GitLab