Browse Source

Merge branch 'master' into prod

axolotle 3 years ago
parent
commit
ad0266dbf8
57 changed files with 1743 additions and 2612 deletions
  1. 134 839
      package-lock.json
  2. 2 0
      package.json
  3. 1 1
      public/index.html
  4. 43 17
      src/App.vue
  5. 37 0
      src/api/fragments/TextCardFields.gql
  6. 9 0
      src/api/fragments/TextTreeFields.gql
  7. 11 0
      src/api/fragments/TextrefTreeFields.gql
  8. 46 0
      src/api/queries/TextCard.gql
  9. 0 5
      src/api/queries/TextRef.gql
  10. 16 0
      src/api/queries/TextdepartRecursive.gql
  11. 7 0
      src/api/queries/TextdepartRecursiveWithDepth.gql
  12. 6 0
      src/api/queries/TextsDepart.gql
  13. 3 0
      src/api/queries/index.js
  14. 60 0
      src/assets/scss/_bootstrap.scss
  15. 28 0
      src/assets/scss/_variables.scss
  16. 0 1168
      src/assets/scss/app.scss
  17. 0 7
      src/assets/scss/base/_colors.scss
  18. 0 93
      src/assets/scss/base/_fonts.scss
  19. 0 105
      src/assets/scss/base/_grid-flex.scss
  20. 0 92
      src/assets/scss/base/_grid.scss
  21. 0 94
      src/assets/scss/base/_layout.scss
  22. 0 40
      src/assets/scss/base/_reset.scss
  23. 0 67
      src/assets/scss/base/_transitions.scss
  24. 0 10
      src/assets/scss/base/_variables.scss
  25. 0 0
      src/assets/scss/bootstrap-overrides/_mixins.scss
  26. 0 0
      src/assets/scss/bootstrap-overrides/_variables.scss
  27. 40 0
      src/assets/scss/globals.scss
  28. 41 0
      src/assets/scss/main.scss
  29. 176 0
      src/components/NodeMap.vue
  30. 53 0
      src/components/formItems/MultipleTagsSelect.vue
  31. 22 0
      src/components/globals/ComponentDebug.vue
  32. 62 0
      src/components/text/TextCard.vue
  33. 93 0
      src/components/text/TextCardBase.vue
  34. 20 0
      src/components/text/TextMiniCard.vue
  35. 82 0
      src/helpers/d3Data.js
  36. 20 2
      src/main.js
  37. 33 0
      src/messages/fr.json
  38. 34 0
      src/messages/index.js
  39. 12 0
      src/pages/Blog.vue
  40. 12 0
      src/pages/Contact.vue
  41. 12 0
      src/pages/Gallery.vue
  42. 3 16
      src/pages/Home.vue
  43. 12 0
      src/pages/Introduction.vue
  44. 12 0
      src/pages/Kit.vue
  45. 90 0
      src/pages/Library.vue
  46. 132 0
      src/pages/Map.vue
  47. 54 0
      src/pages/library/CardList.vue
  48. 12 0
      src/pages/library/CardMap.vue
  49. 94 0
      src/pages/library/LibraryOptions.vue
  50. 47 0
      src/pages/library/TreeMap.vue
  51. 3 0
      src/pages/library/index.js
  52. 2 15
      src/router/index.js
  53. 78 0
      src/router/routes.js
  54. 2 16
      src/store/index.js
  55. 0 24
      src/store/modules/corpus.js
  56. 80 0
      src/store/modules/texts.js
  57. 7 1
      vue.config.js

File diff suppressed because it is too large
+ 134 - 839
package-lock.json


+ 2 - 0
package.json

@@ -12,7 +12,9 @@
   },
   "dependencies": {
     "axios": "^0.21.1",
+    "bootstrap-vue": "^2.21.2",
     "core-js": "^3.6.5",
+    "d3": "^6.5.0",
     "vue": "^2.6.11",
     "vue-meta": "^2.4.0",
     "vue-router": "^3.2.0",

+ 1 - 1
public/index.html

@@ -5,7 +5,7 @@
     <meta http-equiv="X-UA-Compatible" content="IE=edge">
     <meta name="viewport" content="width=device-width,initial-scale=1.0">
     <link rel="icon" href="<%= BASE_URL %>favicon.ico">
-    <title>En Françai(s)</title>
+    <title>En françaiS au pluriel</title>
   </head>
   <body>
     <noscript>

+ 43 - 17
src/App.vue

@@ -1,18 +1,38 @@
 <template>
   <div id="app">
     <header>
-      <h1>
-        <router-link :to="{ name: 'home' }">En Français</router-link>
-      </h1>
+      <b-navbar>
+        <b-dropdown variant="link" no-caret>
+          <template #button-content>
+            <span class="navbar-toggler-icon" />
+            <span class="sr-only">Menu</span>
+          </template>
+
+          <b-dropdown-item v-for="name in subRoutes" :key="name" :to="{ name }">
+            {{ $t('sections.' + name) }}
+          </b-dropdown-item>
+        </b-dropdown>
+
+        <b-navbar-brand :to="{ name: 'home' }">
+          {{ $t('title') }}
+        </b-navbar-brand>
+
+        <b-nav class="ml-auto" pills>
+          <b-nav-item
+            v-for="name in mainRoutes" :key="name"
+            :to="{ name }" :active="$route.name === name"
+          >
+            {{ $t('sections.' + name) }}
+          </b-nav-item>
+        </b-nav>
+      </b-navbar>
+
+      <router-view name="options" />
     </header>
 
     <main id="main">
       <router-view />
     </main>
-
-    <footer>
-      <p>footer</p>
-    </footer>
   </div>
 </template>
 
@@ -20,18 +40,24 @@
 export default {
   name: 'App',
 
-  metaInfo: {
-    // if no subcomponents specify a metaInfo.title, this title will be used
-    title: 'Home',
-    // all titles will be injected into this template
-    titleTemplate: '%s | En Français',
-    meta: [
-      { charset: 'utf-8' },
-      { name: 'viewport', content: 'width=device-width, initial-scale=1' }
-    ]
+  metaInfo () {
+    return {
+      // if no subcomponents specify a metaInfo.title, try to get one from the route name.
+      title: this.$t('sections.' + this.$route.name, ''),
+      // all titles will be injected into this template
+      titleTemplate: '%s | ' + this.$t('title')
+    }
+  },
+
+  data () {
+    return {
+      mainRoutes: ['library', 'kit', 'gallery'],
+      subRoutes: ['home', 'introduction', 'blog', 'contact']
+    }
   }
 }
 </script>
 
-<style lang="scss" scoped>
+<style lang="scss">
+@import '@/assets/scss/main.scss';
 </style>

+ 37 - 0
src/api/fragments/TextCardFields.gql

@@ -0,0 +1,37 @@
+fragment TextCardFields on TextInterface {
+  id
+  bundle
+  title
+  content: texte
+  images {
+    id
+    url
+    alt
+  }
+  videos {
+    url
+  }
+  files: fichiers {
+    fid
+    filename
+    filemime
+    url
+  }
+  families: familles {
+    id
+    uuid
+    name
+  }
+  # field_titre
+  authors: auteurs {
+    id
+    uuid
+    name
+  }
+  edition {
+    id
+    uuid
+    name
+  }
+  type: __typename
+}

+ 9 - 0
src/api/fragments/TextTreeFields.gql

@@ -0,0 +1,9 @@
+fragment TextTreeFields on TextInterface {
+  id
+  title
+  families: familles {
+    id
+    name
+  }
+  type: __typename
+}

+ 11 - 0
src/api/fragments/TextrefTreeFields.gql

@@ -0,0 +1,11 @@
+#import "../fragments/TextTreeFields.gql"
+
+fragment TextrefTreeFields on Textref {
+  ...TextTreeFields
+  children: text_produits {
+    ...TextTreeFields
+    parents: text_de_depart {
+      ...TextTreeFields
+    }
+  }
+}

+ 46 - 0
src/api/queries/TextCard.gql

@@ -0,0 +1,46 @@
+#import "../fragments/TextCardFields.gql"
+
+query TextRef ($id: Int!) {
+  text: texte (id: $id) {
+    ...TextCardFields
+
+    ... on Textref {
+      children: text_produits {
+        id
+      }
+      siblings: text_en_rebond {
+        id
+        title
+        authors: auteurs {
+          name
+        }
+        edition {
+          name
+        }
+      }
+      tags {
+        id
+        name
+      }
+      # degres_detrangement: Int
+      notes {
+        note
+        number: numero
+      }
+    }
+
+    ... on Textprod {
+      tags: tagsprod {
+        id
+        name
+      }
+      notes {
+        note
+        number: numero
+        links: liens {
+          id
+        }
+      }
+    }
+  }
+}

+ 0 - 5
src/api/queries/TextRef.gql

@@ -1,5 +0,0 @@
-query TextRef ($id: Int!) {
-  textref (id: $id) {
-    title
-  }
-}

+ 16 - 0
src/api/queries/TextdepartRecursive.gql

@@ -0,0 +1,16 @@
+#import "../fragments/TextrefTreeFields.gql"
+
+query TextdepartRecursive($id: Int!) {
+  textref(id: $id) {
+    ...TextrefTreeFields
+    siblings: text_en_rebond {
+      ...TextrefTreeFields
+      siblings: text_en_rebond {
+        ...TextrefTreeFields
+        siblings: text_en_rebond {
+          ...TextrefTreeFields
+        }
+      }
+    }
+  }
+}

+ 7 - 0
src/api/queries/TextdepartRecursiveWithDepth.gql

@@ -0,0 +1,7 @@
+#import "../fragments/TextrefTreeFields.gql"
+
+query TextdepartRecursive($id: Int!) {
+  textref(id: $id) {
+INPUT
+  }
+}

+ 6 - 0
src/api/queries/TextsDepart.gql

@@ -0,0 +1,6 @@
+query TextsDepart {
+  textsdepart {
+    id,
+    title
+  }
+}

+ 3 - 0
src/api/queries/index.js

@@ -0,0 +1,3 @@
+export { default as TextsDepart } from './TextsDepart.gql'
+export { default as TextCard } from './TextCard.gql'
+export { default as TextdepartRecursive } from './TextdepartRecursive.gql'

+ 60 - 0
src/assets/scss/_bootstrap.scss

@@ -0,0 +1,60 @@
+/*!
+ * Import of all needed bootstrap files except variables, functions and mixins
+ */
+
+
+// ┌─╮╭─╮╭─╮╶┬╴╭─╴╶┬╴┌─╮╭─┐┌─╮
+// │╶┤│ ││ │ │ ╰─╮ │ ├┬╯├─┤├─╯
+// └─╯╰─╯╰─╯ ╵ ╶─╯ ╵ ╵ ╰╵ ╵╵
+
+// TODO OPTI: import only used
+
+@import "~bootstrap/scss/root";
+@import "~bootstrap/scss/reboot";
+@import "~bootstrap/scss/type";
+@import "~bootstrap/scss/images";
+@import "~bootstrap/scss/code";
+@import "~bootstrap/scss/grid";
+@import "~bootstrap/scss/tables";
+@import "~bootstrap/scss/forms";
+@import "~bootstrap/scss/buttons";
+@import "~bootstrap/scss/transitions";
+@import "~bootstrap/scss/dropdown";
+@import "~bootstrap/scss/button-group";
+@import "~bootstrap/scss/input-group";
+@import "~bootstrap/scss/custom-forms";
+@import "~bootstrap/scss/nav";
+@import "~bootstrap/scss/navbar";
+@import "~bootstrap/scss/card";
+@import "~bootstrap/scss/breadcrumb";
+@import "~bootstrap/scss/pagination";
+@import "~bootstrap/scss/badge";
+@import "~bootstrap/scss/jumbotron";
+@import "~bootstrap/scss/alert";
+@import "~bootstrap/scss/progress";
+@import "~bootstrap/scss/media";
+@import "~bootstrap/scss/list-group";
+@import "~bootstrap/scss/close";
+@import "~bootstrap/scss/toasts";
+@import "~bootstrap/scss/modal";
+@import "~bootstrap/scss/tooltip";
+@import "~bootstrap/scss/popover";
+@import "~bootstrap/scss/carousel";
+@import "~bootstrap/scss/spinners";
+@import "~bootstrap/scss/utilities";
+@import "~bootstrap/scss/print";
+
+
+// ┌─╮╭─╮╭─╮╶┬╴╭─╴╶┬╴┌─╮╭─┐┌─╮   ╷ ╷╷ ╷┌─╴
+// │╶┤│ ││ │ │ ╰─╮ │ ├┬╯├─┤├─╯╶─╴│╭╯│ │├─╴
+// └─╯╰─╯╰─╯ ╵ ╶─╯ ╵ ╵ ╰╵ ╵╵     ╰╯ ╰─╯╰─╴
+
+// TODO OPTI: import only used
+
+@import "~bootstrap-vue/src/utilities";
+
+// General styling needed for special form controls
+@import "~bootstrap-vue/src/custom-controls";
+
+// Include custom SCSS for components
+@import "~bootstrap-vue/src/components/index";

+ 28 - 0
src/assets/scss/_variables.scss

@@ -0,0 +1,28 @@
+// Texte de départ (id: 9)
+$color-starting: white;
+// Texte critique (id: 22)
+$color-critical: #ffba7d;
+// Constellation en écho (id: 63)
+$color-echo: #e191ff;
+// Réflexion théorique (id: 6)
+$color-thinking: #ffeb91;
+// Lecture rapprochée (id: 7)
+$color-reading: #a5a5ff;
+// Expérience sensible (id: 8)
+$color-experience: #cdae87;
+// Kit de désapprentissage (id: 23)
+$color-kit: #ff8873;
+
+$families: (
+  9: $color-starting,
+  22: $color-critical,
+  63: $color-echo,
+  6: $color-thinking,
+  7: $color-reading,
+  8: $color-experience,
+  23: $color-kit
+);
+
+@function setColorFromId($id) {
+  @return map-get($families, $id);
+}

+ 0 - 1168
src/assets/scss/app.scss

@@ -1,1168 +0,0 @@
-@import './base/reset';
-@import './base/variables';
-@import './base/colors';
-@import './base/grid-flex';
-@import './base/transitions';
-@import './base/layout';
-@import './base/fonts';
-
-/* The emerging W3C standard
-that is currently Firefox-only */
-* {
-  scrollbar-width: thin;
-  scrollbar-color: $grisclair rgba(255,255,255,0);
-}
-
-/* Works on Chrome/Edge/Safari */
-*::-webkit-scrollbar {
-  width: 12px;
-}
-*::-webkit-scrollbar-track {
-  background: rgba(255,255,255,0);
-}
-*::-webkit-scrollbar-thumb {
-  background-color: $grisclair;
-  border-radius: 20px;
-  border: none;
-}
-
-body{
-  color: #1a1a1a;
-}
-
-#root{
-}
-
-.red{
-  background-color: red;
-  color:white;
-
-}
-
-header[role="banner"]{
-  div.wrapper{
-    display: grid;
-    grid-template-columns: 1fr 1fr;
-  }
-  h1.site-title{
-    grid-column: 1;
-    margin:0;
-    font-size: 1em;
-  }
-  nav#header-menu{
-    grid-column: 2;
-    text-align: right;
-    >ul>li{
-      display: inline-block;
-      margin-right: 1em;
-      position: relative;
-      >ul{
-        position: absolute;
-        top:1em; right:-1em;
-        overflow: hidden;
-        padding-bottom: 0.5em;
-        background-color: white;
-        >li{
-          padding:0 1em;
-          // margin-right: -1em;
-          transition: height 0.3s ease-in-out;
-          height:0;
-          overflow: hidden;
-        }
-      }
-      //
-      // &:focus-within
-      // &:hover,
-      &.opened{
-        >ul>li{
-          height:1em;
-        }
-      }
-      &.has-submenu{
-        cursor: pointer;
-      }
-    }
-    li>span,li>a{
-      font-size: 0.9em;
-      color: $bleuroi;
-      text-transform: uppercase;
-
-    }
-  }
-}
-section[role="main-content"]{
-  #home{
-    header{
-      text-align: center;
-      h1{
-        color: $bleuroi;
-        font-size: 8em;
-        font-weight: 300;
-        margin:15vh 0 0;
-      }
-      h2{
-        color: $or;
-        font-size: 2em;
-        font-weight: 300;
-        margin:1em 0 0;
-        text-transform: uppercase;
-        letter-spacing: 0.2em;
-        sup{
-          // line-height: 5em;
-          vertical-align:text-top;
-          font-size: 0.7em
-        }
-      }
-    }
-    // $filet_space:8em;
-    // $decallage: 0.5em;
-    section{
-      padding-top: 8em;
-    }
-    @mixin teasersfilet($filet_space, $decallage){
-      &:before, &:after{
-        z-index: 0;
-        content: "";
-        position: absolute;
-        opacity: 0.4;
-      }
-      &:before{
-        border:1px solid $or;
-        width:calc(100% + #{$filet_space*2 + $decallage*2});
-        left:- $filet_space - $default_gap/2 -$decallage;
-        height:calc(100% + #{$filet_space});
-        top:- $filet_space / 2;
-      }
-      &:after{
-        border:1px solid $rouge;
-        width:calc(100% + #{$filet_space*2});
-        left:- $filet_space - $default_gap/2;
-        height:calc(100% + #{$filet_space + $decallage*2});
-        top:- $filet_space / 2 - $decallage;
-      }
-
-    }
-    div.teasers{
-      display: flex;
-      flex-direction: row;
-      flex-wrap: nowrap;
-      position: relative;
-      padding-right: 0;
-      article{
-        box-sizing: border-box;
-        flex-basis: percentage(2 / ( $default_sum - 6) );
-        padding-right: $default_gap;
-        @include fontsans;
-
-        h1{
-          color: $bleuroi;
-        }
-        p{
-          font-size: 0.882em;
-          line-height: 1.2;
-        }
-        span{
-          color:$rouge;
-          font-size:0.693em;
-        }
-      }
-      // filets decoratif
-      @include teasersfilet(8em, 0.5em);
-      }
-    // responsive
-    @media only screen and (max-width: $small-bp) {
-      header{
-        h1{
-          font-size: 5em;
-          margin:7vh 0 0;
-        }
-        h2{
-          font-size: 1em;
-        }
-      }
-      section{
-        padding-top: 4em;
-      }
-      div.teasers{
-        flex-direction: column;
-        // filets decoratif
-        @include teasersfilet(4em, 0.5em);
-      }
-    }
-  }
-
-  #list-corpus, .index{
-    >header>h1{
-      font-family: "noto_sans";
-      color: $rouge;
-      font-weight: 400;
-    }
-
-    article.item{
-      margin: 2em 0 0;
-      header h1{
-        font-size: 1.512em;
-        color: $bleuroi;
-        font-weight: 400;
-        margin:0;
-      }
-    }
-    ul.item-list{
-      li{
-        margin: 0 0 2em 0;
-        header{
-          h2{
-            margin:0.4em 0 0.2em;
-            @include title1blue;
-          }
-          h3{
-            margin:0.2em 0;
-            @include fontsans;
-            font-size: 0.756em;
-            font-weight: 500;
-          }
-          margin-bottom: 0.3em;
-        }
-        section.editions{
-          div.editions{
-            ol{
-              padding:0;
-              li{
-                margin:0.7em 1em;
-              }
-            }
-          }
-        }
-        h4{
-          margin:0.1em 0;
-          font-weight: 300;
-          @include fontsans;
-          font-size: 0.756em;
-          &.texts-quantity{
-            color: $rouge;
-            // &:after{
-            //   content: ">>";
-            //   margin:0 0 0 0.5em;
-            // }
-          }
-        }
-
-        ul {
-          li{
-            margin:0 0 0 1em;
-            h3{
-              margin: 0.5em 0;
-              font-weight: 400;
-              font-size: 1em;
-            }
-          }
-        }
-      }
-    }
-  }
-
-  #corpus{
-  }
-
-  .index{
-  }
-
-  .index-item{
-    header{
-      h1{
-        @include title1black;
-        margin:0 0 0.3em;
-      }
-      p{
-        margin: 0 0 0.5em 0;
-      }
-      h3{
-        @include title2black
-        margin:0;
-      }
-      .authors{
-        a{
-          @include title2black;
-          color: $bleuroi;
-        }
-        margin:0 0 0.3em;
-      }
-      .mdi{
-        color: $bleuroilight;
-        font-size: 0.7em;
-      }
-    }
-    .occurences{
-      >ul{
-        >li{
-          padding:0 0 2em 0;
-          h3{
-            @include title2black;
-            padding:0 0 0.5em 0;
-            color: $bleuroi;
-          }
-          >ul{
-            >li{
-              padding:0 0 0.5em 0;
-              section{
-                h4{
-                  @include title3black;
-                  display: inline-block;
-                }
-                span.open-close{
-                  cursor: pointer;
-                  display: inline-block;
-                  svg{
-                    transform: rotate(-90deg) scale(0.8);
-                    transition: transform 0.3s ease-in-out;
-                    path{
-                      fill:$bleuroi;
-                    }
-                  }
-                }
-                div.text{
-                  max-height: 0;
-                  transition: max-height 0.3s ease-in-out;
-                  overflow: hidden;
-                  box-sizing: content-box;
-                  p, h1, h2, h3, h4, h5, h6{
-                    margin: 0.5em 0 0 0;
-                  }
-                }
-                a.lire-plus{
-                  color: $bleuroi;
-                  opacity: 0;
-                  display: inline-block;
-                  height: 0;
-                  overflow: hidden;
-                  transition: height, opacity 0.3s ease-in-out;
-                }
-                &.opened{
-                  span.open-close{
-                    cursor: pointer;
-                    display: inline-block;
-                    svg{
-                      transform: scale(0.8) rotate(0);
-                    }
-                  }
-                  div.text{
-                    max-height:100px;
-                  }
-                  a.lire-plus{
-                    opacity: 1;
-                    height:1em;
-                  }
-                }
-              }
-            }
-          }
-        }
-      }
-    }
-    nav{
-      h3{
-        @include title2black;
-        margin:0 0 0.5em;
-      }
-      h4{
-        @include title3black;
-        margin:0 0 0.5em;
-        color: $bleuroi;
-      }
-      .attested-forms{
-        ul{
-          >li{
-            margin: 0 0 1em 0;
-            li{
-              margin: 0 0 0.5em 0;
-            }
-          }
-        }
-        a{
-          @include title4black;
-          color: $grisfonce;
-          font-weight: 400;
-        }
-      }
-    }
-  }
-
-  #edition{
-    >header{
-      position: relative;
-      h1{
-        @include title1black;
-      }
-      aside.index-tooltip{
-        z-index:10;
-        margin-top: -1.75em;
-        position:absolute;
-        text-align: right;
-        right: 2em;
-        h1 {
-          @include title2black;
-          margin:0 0 0.5em 0;
-        }
-        p{
-          margin:0 0 0.5em 0;
-        }
-        time{
-          font-weight: 600;
-        }
-        @media only screen and (max-width: $small-bp) {
-          background-color: #fff;
-          padding: 1em;
-          box-shadow: 0 0 10px $gris;
-          *{
-            pointer-events: none;
-          }
-        }
-      }
-    }
-
-    $pagenum_w:1em;
-    >section{
-      padding-right: 0;
-      >.wrapper{
-        padding-right: $pagenum_w*2;
-      }
-      div#text{
-        // .infinite-loading-container{
-        //   height:0;
-        //   overflow: hidden;
-        // }
-        div.tei{
-          position: relative;
-          width: calc(100% - #{$pagenum_w});
-
-          // @media only screen and (min-width: $small-bp + 1) {
-          padding-right: $pagenum_w;
-          border-left: 1px dotted $grisclair;
-          padding-left: 1em;
-          &.active{
-            border-left: 1px dotted $bleuroi;
-          }
-          // }
-          >h1{@include teititle1blue;}
-          span.placeName,
-          span.objectName,
-          span.persName{
-            font-weight: 600;
-          }
-
-          h1{ font-size: 1.512em; }
-          p{
-            margin-top: 0;
-            font-size: 1.134em;
-            line-height: 1.5;
-            span.persName,
-            span.placeName,
-            span.objectName{
-              font-weight: 600;
-            }
-            a{
-              font-weight: 600;
-              &.active-link{
-                color: $rouge;
-                // text-decoration: underline;
-              }
-              sup.mdi{
-                font-size: 0.630em;
-                vertical-align: super;
-                // line-height: 0.1;
-                padding: 0 0.2em;
-              }
-            }
-          }
-
-          span[role="pageNum"]{
-            font-size: 16px;
-            position: relative;
-            float:right;
-            width: $pagenum_w; height:0;
-            // margin-left:calc(100% - #{$pagenum_w * 2});
-            // margin-left: 100%;
-            margin-right: - $pagenum_w;
-            &:after{
-              content:attr(data-num);
-              border-top: 1px solid $bleuroilight;
-              position: absolute;
-              right:0;
-              font-size: 0.630em;
-              width: $pagenum_w*2; height: $pagenum_w;
-              line-height: $pagenum_w;
-              border-right: 1px solid $bleuroilight;
-              color: $bleuroi;
-              text-indent: $pagenum_w * 2.5;
-            }
-          }
-
-          a.text-item-link{
-            // float: left;
-            position: absolute;
-            top:0; left:0;
-            // display: block;
-            // width:1em; height:1em;
-            font-size: 0.630em;
-            .mdi{
-              color: $bleuroi;
-              pointer-events: none;
-            }
-          }
-          // front page
-          header{
-            padding-top: 1em;
-            h1{
-              @include teititlefrontblue;
-              .initial{
-                text-transform: uppercase;
-                font-size: 1.3em;
-                line-height: 1.3em;
-              }
-            }
-          }
-          .byline{
-            text-align: center;
-            font-style: italic;
-            padding-bottom: 1em;
-          }
-          .docImprint{
-            text-align: center;
-            // padding-bottom: 1em;
-          }
-          .imprimatur{
-            text-align: center;
-            font-style: italic;
-            padding-bottom: 1em;
-
-          }
-
-          figure{
-            // outline: 1px solid red;
-            background-color: $grisclair;
-            margin:1em 0;
-            img{
-              width: 100%;
-            }
-            figcaption{
-              @include fontcaption
-              padding: 0.5em;
-            }
-          }
-        }
-      }
-    }
-
-    >nav{
-      $pager_h:2em;
-      display: flex;
-      flex-direction: column;
-      span.nav-title{ display:none; }
-
-      section#toc{
-        box-sizing: content-box;
-        padding:0 0 1em 1.5em;
-        height:calc(100% - #{$pager_h});
-        overflow-x: hidden;
-        overflow-y: auto;
-        >ul{
-          ul{
-            li{
-              ul{
-                overflow: hidden;
-                max-height: 1000px;
-                transition: max-height 0.5s ease-in-out;
-                // transform: scaleY(1);
-                // transform-origin: top;
-                // transition: transform 0.3s ease-in-out;
-                &:not(.opened){
-                  // height:0;
-                  max-height:0;
-                  transition: max-height 0.5s cubic-bezier(0, 1.05, 0, 1);
-                  // transform: scaleY(0);
-                }
-                // &.opened{
-                //   border: 1px solid red;
-                // }
-                // padding-left: 1em;
-                border-left: 0.5px solid $grisclair;
-                // min-height: 1em;
-                margin-bottom: 0em;
-                li{
-                  // min-height: 1em;
-                  // border-left: 1px solid red;
-                  padding:0 0 0.2em 1em;
-                }
-              }
-            }
-          }
-          .toc-title{
-            @include title4black;
-            &.active,
-            &:hover{
-              color:$grisfonce;
-              font-weight: 600;
-            }
-            &.loaded{
-              color:$grisfonce;
-            }
-          }
-          // h2.toc-title{font-size: 0.882em;}
-          // h3.toc-title{font-size: 0.882em;}
-          // h4.toc-title{font-size: 0.882em;}
-          // h5.toc-title{font-size: 0.882em;}
-          // h6.toc-title{font-size: 0.882em;}
-          // span.toc-title{font-size: 0.882em;}
-        }
-      }
-
-      div#page-nav{
-        height:$pager_h;
-        overflow: hidden;
-        box-sizing: content-box;
-        padding:1em 0 0 1.5em;
-        select{
-          option{
-            padding:0;
-          }
-        }
-      }
-    }
-    // responsive
-    @media only screen and (max-width: $small-bp) {
-      position: relative;
-      >nav{
-        $top: 45px;
-        z-index: 2;
-        position: absolute;
-        top:$top;
-        right:0;
-        background-color: #fff;
-        width:percentage(10/$default_sum);
-        box-sizing: border-box;
-        padding-top: 1em;
-        padding-bottom: 1em;
-        height:calc(100% - #{$top});
-
-        transform: translateX(100%);
-        transition: transform 0.3s ease-in-out;
-
-        span.nav-title{
-          display: block;
-          position: absolute;
-          top:4.5em; left:-1.8em;
-          transform: rotateZ(-90deg);
-          transform-origin: center;
-          @include fontsans;
-          font-size: 0.600em;
-          cursor: pointer;
-          color: $bleuroi;
-          svg{
-            vertical-align: bottom;
-            transform-origin: center;
-            transform: scale(0.8) rotate(180deg);
-            transition: transform 0.3s ease-in-out;
-            path{
-              fill: $bleuroi;
-            }
-          }
-        }
-
-        &.opened{
-          box-shadow: -3px -3px 5px $grisclair;
-          transform: translateX(0);
-          span.nav-title{
-            svg{
-              transform: scale(0.7) rotate(0);
-            }
-          }
-        }
-
-      }
-    }
-  }
-
-  #text{
-    .tei{}}
-
-  #biblio{
-    .router-link-active{
-      font-weight: 600;
-    }
-    ul.item-list{
-      margin:0;
-      padding:0;
-      li{
-        padding:0;
-        margin:0 0 1.5em 0;
-        h2{
-          margin:0;
-          @include title2black;
-        }
-        p{
-          margin:0;
-        }
-      }
-    }
-  }
-}
-
-footer[role="tools"]{
-  $list-item-h: 7em;
-
-  @mixin resultItem{
-    box-sizing: border-box;
-    // we are only on 10 colls as 2 are occupied by sides
-    flex-basis: percentage(2/($default_sum - 2));
-    max-height: $list-item-h;
-    overflow: hidden;
-    padding-bottom: 1em;
-    padding-right: $default_gap;
-    article{
-      max-height: 100%;
-      overflow: hidden;
-    }
-    article.item{
-        h1{
-          @include title3black;
-          font-weight: 600;
-          max-width: 95%;
-        }
-        h2{
-          @include title3black;
-          text-transform: none;
-        }
-        span{
-          font-size: 0.882em;
-        }
-      // .preview{
-      //   font-size: 0.882em;
-      //   margin:0;
-      //   code{
-      //     @include fontserif;
-      //     background-color: lighten(desaturate($rouge,20%), 20%);
-      //     padding:0 0.2em;
-      //   }
-      // }
-    }
-
-  }
-
-  #history{
-    z-index: 8;
-    background-color: $or;
-    padding:1.2em $side-padding;
-    max-height: $list-item-h;
-    @include accordeon-transition($list-item-h);
-    >header{
-
-    }
-    .history-list{
-      overflow-x: hidden;
-      .wrapper{
-        height:100%;
-        // hidding the scrollbar
-        overflow-y: auto;
-        // width:calc(100% + 1em);
-        padding-right: 1em;
-        >ul{
-          padding:0;
-          display: flex;
-          flex-direction: row;
-          flex-wrap: wrap;
-        }
-      }
-      li.item{
-        @include resultItem;
-      }
-    }
-  }
-  #results{
-    z-index: 9;
-    background-color: $gris;
-    padding:1.2em $side-padding;
-    @media only screen and (max-width: $small-bp) {
-      padding:1.2em $side-padding/2;
-    }
-    max-height: $list-item-h * 3;
-    @include accordeon-transition($list-item-h * 3);
-    >header{
-      .search-keys{
-        font-size: 0.756em;
-        font-weight: 500;
-      }
-      .results-count{
-        font-size: 0.756em;
-      }
-    }
-    .results-list{
-      overflow-x: hidden;
-      .wrapper{
-        position:relative;
-        height:100%;
-        // hidding the scrollbar
-        overflow-y: auto;
-        // width:calc(100% + 1em);
-        padding-right: 1em;
-        >ul{
-          padding:0;
-          display: flex;
-          flex-direction: row;
-          flex-wrap: wrap;
-        }
-      }
-      li.result{
-        @include resultItem;
-      }
-      .infinite-loading-container{
-        // TODO: how to center the loading
-      }
-    }
-
-    >header, section.results-list{
-      transition: opacity 0.2s ease-in-out;
-    }
-    &.loading {
-      >header, section.results-list{
-        transition: opacity 0.5s ease-in-out;
-        opacity:0.5;
-        pointer-events: none;
-      }
-    }
-
-    // responsive
-    @media only screen and (max-width: $small-bp) {
-      position: relative;
-      >header{
-        padding:0 0 1em 0;
-        >*{
-          display: inline-block;
-          margin-right: 1em;
-        }
-        #sorting{
-          width:10em;
-        }
-      }
-      >section.results-list{
-        max-height: 15em;
-        li.result{
-          flex-basis: 33%;
-        }
-      }
-      >nav{
-        position: absolute;
-        top:1.2em; right:1.2em;
-      }
-    }
-  }
-  #footer-bottom{
-    z-index: 10;
-    padding:0 $side-padding;
-    @media only screen and (max-width: $small-bp) {
-      padding:0 $side-padding/2;
-    }
-    background-color: $bleuroi;
-    &>*{
-      // disable grid gap
-      padding-right: 0;
-    }
-    #footer-tabs{
-      ul{
-        padding:0; margin:0;
-        display: flex;
-        flex-direction: column;
-        li{
-          flex: 1 1 auto;
-          .wrapper{
-            box-sizing: border-box;
-            line-height: 0.6em;
-            height:2em;
-            width: calc(100% + $side-padding);
-            margin-left:-$side-padding;
-            padding:0.3em 0.5em 0.3em $side-padding;
-          }
-          &.history .wrapper{
-            background-color: $or;
-          }
-          &.results .wrapper{
-            background-color: $gris;
-          }
-          span{
-            font-size: 0.693em;
-            font-weight: 400;
-            text-transform: uppercase;
-            cursor: pointer;
-            @include fade-transition;
-          }
-        }
-      }
-    }
-    #search{
-      color: #fff;
-      background-color: $bleuroi;
-      form{
-        padding: 0;
-        // display: flex;
-        // flex-direction: row;
-        // flex-wrap: wrap;
-        fieldset{
-          padding:0.7em 1em;
-          border: none;
-          box-sizing:border-box;
-          // width correction as row is not the same width as others in the page
-          // flex-basis: percentage(2/($default_sum - 1));
-          // flex-basis: 17.667%;
-          // flex-basis: percentage(2 / 11);
-          &:not(:first-of-type){
-            border-left: 1px solid $grisclair;
-          }
-        }
-        fieldset.search{
-          display: inline-flex;
-          >div{
-            width:80%;
-            vertical-align: middle;
-          }
-          label[for="keys"]{
-            display: none;
-          }
-          input[type="text"]{
-            padding:0em 0.3em;
-            margin:0 0 0.3em 0;
-            box-sizing: border-box;
-            font-size: 0.756em;
-            line-height: 1;
-            width:100%;
-            height:1.4em;
-            border:none;
-            border-radius: 2px;
-          }
-          span.mdi{
-            display: inline-block;
-            margin:0 0 0 0.5em;
-            font-size: 1.2em;
-            line-height:1.1;
-            vertical-align:middle;
-            width:1.2em; height:1.2em;
-            border-radius: 0.6em;
-            background-color: #fff;
-            color: $bleuroi;
-            text-align: center;
-            font-weight: 700;
-            cursor: pointer;
-          }
-        }
-      }
-
-      fieldset.filters{
-        .vs__actions{
-          // background-color: $grisclair;
-          align-items:baseline;
-          padding-top:0.2em;
-        }
-      }
-      form{
-        transition: opacity 0.2s ease-in-out;
-      }
-      &.loading{
-        form{
-          opacity:0.5;
-          transition: opacity 0.5s ease-in-out;
-          pointer-events: none;
-        }
-      }
-
-      // responsive
-      @media only screen and (max-width: $small-bp) {
-        form{
-          fieldset{
-            &.search{
-              >div{
-                display: inline-flex;
-                flex-wrap: nowrap;
-                width:84%;
-                >*{
-                  flex-basis: 45%;
-                  margin: 0 0.5em 0 0;
-                  // box-sizing: content-box;
-                  // width:auto!important;
-                  &#keys[type="text"]{
-                    margin-right:1.5em;
-                  }
-                }
-              }
-              span.mdi{
-                width:1em; height:1em;
-                margin-top: -0.1em;
-                margin-left: 0;
-              }
-            }
-            &.filters{
-              border-left: none;
-              flex-basis: 32%;
-              padding: 0.2em 0 1em 1em;
-            }
-          }
-        }
-      }
-    }
-  }
-  h2{
-    margin:0;
-    font-size: 0.756em;
-    font-weight: 400;
-    text-transform: uppercase;
-    padding:0;
-  }
-}
-
-// vue-select
-.v-select{
-  padding:0;
-  div[role="combobox"]{
-    background-color: #fff;
-    padding:0;
-    border-radius: 2px;
-    border: none;
-  }
-  input[type="search"]{
-    margin:0;
-    padding:0;
-    -webkit-appearance:textfield;
-    -webkit-box-sizing:content-box;
-  }
-  input::-webkit-search-decoration,
-  input::-webkit-search-cancel-button {
-      display: none;
-  }
-  .vs__search{
-    &, &:focus{
-      font-size: 0.756em;
-      line-height: 1;
-      height:1.2em;
-      border:none;
-      box-sizing: border-box;
-    }
-  }
-  .vs__dropdown-toggle{
-    input::placeholder{background-color: #fff;}
-  }
-  .vs__selected-options{
-    background-color: #fff;
-  }
-  .vs__actions{
-    padding:1px 3px;
-    button.vs__clear{
-      line-height: 0.5;
-      // height:0;
-    }
-    svg[role="presentation"]{
-      transform: scale(0.8);
-      path{
-        fill: $bleuroi;
-      }
-    }
-  }
-  .vs__selected{
-    margin:0;
-    padding:0.2em 0;
-    line-height:1;
-    font-size: 0.756em;
-    background-color: #fff;
-    border:none;
-    align-items: middle;
-    box-sizing: content-box;
-    display: inline-block;
-    width: calc(100% - 12px);
-    // &>*:not(button){
-    //   display: inline-block;
-    //   width:70%;
-    // }
-    button{
-      svg{
-        transform: scale(0.8);
-        path{
-          fill: $bleuroi;
-        }
-      }
-      &.vs__deselect{
-        line-height: 0;
-      }
-    }
-
-  }
-
-  // border-radius: 2px;
-  // border: none;
-}
-
-ul[role="listbox"]{
-  @include fontsans;
-  padding:0;
-  margin:0;
-  border:none;
-  position: relative;
-  li{
-    box-sizing: content-box;
-    padding:0.3em;
-    margin:0;
-    font-size: 0.756em;
-    line-height: 1;
-    white-space: normal;
-    position: relative;
-    *{
-      max-width: 100%;
-    }
-  }
-}
-
-//  ___
-// |_ _|__ ___ _ _  ___
-//  | |/ _/ _ \ ' \(_-<
-// |___\__\___/_||_/__/
-
-span.mdi-close{
-  cursor: pointer;
-}
-
-@keyframes spin {
-    from {
-        transform:rotate(0deg);
-    }
-    to {
-        transform:rotate(360deg);
-    }
-}
-
-span.mdi-loading{
-  animation-name: spin;
-  animation-duration: 2000ms;
-  animation-iteration-count: infinite;
-  animation-timing-function: linear;
-}
-
-
- //  _                 _ _
- // | |   ___  __ _ __| (_)_ _  __ _
- // | |__/ _ \/ _` / _` | | ' \/ _` |
- // |____\___/\__,_\__,_|_|_||_\__, |
- //                            |___/
-
-span.loading{
-  @include fontsans;
-  font-size: 0.756em;
-  color: $grisfonce;
-  animation: pulseloading 4s infinite;
-}
-
-@keyframes pulseloading{
-  0% {
-    opacity: 1;
-  }
-  50%{
-    opacity: 0;
-  }
-  100% {
-    opacity: 1;
-  }
-}

+ 0 - 7
src/assets/scss/base/_colors.scss

@@ -1,7 +0,0 @@
-$bleuroi: rgb(61,82,102);
-$bleuroilight: lighten(rgb(61,82,102), 30%);
-$gris: rgb(200,204,204);
-$grisclair: rgb(230,234,234);
-$grisfonce: rgb(57,57,64);
-$or: rgb(179,161,125);
-$rouge: rgb(204,61,82);

+ 0 - 93
src/assets/scss/base/_fonts.scss

@@ -1,93 +0,0 @@
-@import '../fonts/libertinus/libertinus.css';
-@import '../fonts/notosans/notosans.css';
-// @import '../fonts/notosans/notosans-semicondensed.css';
-// @import '../fonts/notosans/notosans-condensed.css';
-// @import '../fonts/notosans/notosans-extracondensed.css';
-
-@mixin fontsans {
-  font-family: "noto_sans";
-}
-@mixin fontserif {
-  font-family: 'libertinus_serif';
-}
-
-body{
-  @include fontserif;
-  font-weight: 400;
-
-  // font-family: 'noto_sans';
-  // font-weight: 900;
-  // font-style: italic;
-}
-
-#header-menu,
-#footer-tabs,
-#search,
-footer[role="tools"] .row>header,
-footer[role="tools"] .row>nav{
-  @include fontsans;
-}
-@mixin title1blue {
-  @include fontserif;
-  font-size: 2.268em;
-  color: $bleuroi;
-  font-weight: 400;
-  margin:0;
-}
-
-@mixin title1black {
-  @include fontserif;
-  font-size: 1.512em;
-  color: $grisfonce;
-  font-weight: 400;
-  margin:0;
-}
-
-@mixin title2black {
-  @include fontserif;
-  font-size: 1.134em;
-  color: $grisfonce;
-  font-weight: 400;
-  margin:0;
-}
-
-@mixin title3black {
-  @include fontserif;
-  font-size: 1em;
-  color: $grisfonce;
-  font-weight: 400;
-  margin:0;
-}
-
-@mixin title4black {
-  @include fontserif;
-  font-size: 0.882em;;
-  color: $gris;
-  font-weight: 400;
-  margin:0;
-}
-
-// TEI
-
-@mixin teititlefrontblue {
-  @include fontserif;
-  font-size: 2.6em;
-  color: $bleuroi;
-  font-weight: 400;
-  margin:0;
-  text-align: center;
-}
-
-@mixin teititle1blue {
-  @include fontserif;
-  font-size: 1.8em;
-  color: $bleuroi;
-  font-weight: 400;
-  margin:0;
-}
-
-@mixin fontcaption {
-  @include fontsans;
-  font-size: 0.643em;
-  line-height: 1.1em;
-}

+ 0 - 105
src/assets/scss/base/_grid-flex.scss

@@ -1,105 +0,0 @@
-// http://www.thesassway.com/intermediate/simple-grid-mixins
-
-
-@mixin row() {
-  display:flex;
-  flex-direction: row;
-  flex-wrap: nowrap;
-  align-items: stretch;
-  @media only screen and (max-width: $small-bp) {
-    flex-wrap:wrap;
-  }
-}
-
-.row{
-  @include row;
-}
-.row-rl{
-  @include row;
-}
-
-// small
-.small-row {
-  @media only screen and (max-width: $small-bp) {
-    @include row;
-  }
-}
-
-// medium
-.med-row {
-  @media only screen and (min-width: $small-bp+1) and (max-width: $med-bp) {
-    @include row;
-  }
-}
-
-// large
-.large-row {
-  @media only screen and (min-width: $med-bp+1) {
-    @include row;
-  }
-}
-
-%col-reset {
-    box-sizing: border-box;
-}
-
-@mixin col($col, $offset: 0, $sum: $default_sum, $gap: $default_gap, $align: top) {
-  @extend %col-reset;
-  padding-left: $gap*$offset;
-  @if $col == $default_sum {
-    // if last col, then no gap
-    padding-right: 0;
-  }@else{
-    padding-right: $gap;
-  }
-  &:last-child{padding-right: 0;}
-
-  // no offset with flex ??
-  // margin-left: percentage(($col/$sum)*$offset);
-
-  // col width
-  flex-basis: percentage($col/$sum);
-}
-
-@for $c from 1 through $default_sum {
-  .col-#{$c} {
-      @include col($c);
-  }
-
-  // small
-  .small-col-#{$c} {
-    @media only screen and (max-width: $small-bp) {
-      @include col($c);
-    }
-  }
-
-  // medium
-  .med-col-#{$c} {
-    @media only screen and (min-width: $small-bp+1) and (max-width: $med-bp) {
-      @include col($c);
-    }
-  }
-
-  // large
-  .large-col-#{$c} {
-    @media only screen and (min-width: $med-bp+1) {
-      @include col($c);
-    }
-  }
-
-}
-
-@for $c from 1 through $default_sum - 1 {
-  @for $o from 1 through $default_sum - $c {
-    .col-#{$c}-offset-#{$o} {
-      @include col($c, $o);
-    }
-  }
-}
-
-// TODO: replace with align-self:flex-start or flex-end
-// .col.float-right{
-//   float: right;
-//   padding-right: 0;
-//   padding-left: $default_gap;
-// }

+ 0 - 92
src/assets/scss/base/_grid.scss

@@ -1,92 +0,0 @@
-// http://www.thesassway.com/intermediate/simple-grid-mixins
-
-
-@mixin row() {
-  // font-size: 0;
-  // white-space: nowrap;
-  position: relative;
-  // >*{
-  //   font-size: 16px;
-  // }
-  &:after{
-    content:"";
-    clear:both;
-    display: block;
-  }
-}
-
-%col-reset {
-    width: 100%;
-    // display: inline-block;
-    // white-space:normal;
-    // font-size: 16px;
-    float:left;
-    box-sizing: border-box;
-}
-
-@mixin col($col, $offset: 0, $sum: $default_sum, $gap: $default_gap, $align: top) {
-  @extend %col-reset;
-  padding-left: $gap*$offset;
-  @if $col == $default_sum {
-    padding-right: 0;
-  }@else{
-    padding-right: $gap;
-  }
-  &:last-child{padding-right: 0;}
-
-  margin-left: percentage(($col/$sum)*$offset);
-
-  // @media only screen and (min-width: 768px) {
-    width: percentage($col/$sum);
-    // vertical-align: $align;
-  // }
-}
-
-.row{
-  @include row;
-  // html:not(.js) &{
-  //   overflow-y: auto;
-  // }
-}
-
-@for $c from 1 through $default_sum {
-  .col-#{$c} {
-      @include col($c);
-  }
-
-  // small
-  .small-col-#{$c} {
-    @media only screen and (max-width: $small-bp) {
-      @include col($c);
-    }
-  }
-
-  // medium
-  .med-col-#{$c} {
-    @media only screen and (min-width: $small-bp+1) and (max-width: $med-bp) {
-      @include col($c);
-    }
-  }
-
-  // large
-  .large-col-#{$c} {
-    @media only screen and (min-width: $med-bp+1) {
-      @include col($c);
-    }
-  }
-
-}
-
-@for $c from 1 through $default_sum - 1 {
-  @for $o from 1 through $default_sum - $c {
-    .col-#{$c}-offset-#{$o} {
-      @include col($c, $o);
-    }
-  }
-}
-
-.col.float-right{
-  float: right;
-  padding-right: 0;
-  padding-left: $default_gap;
-}

+ 0 - 94
src/assets/scss/base/_layout.scss

@@ -1,94 +0,0 @@
-$side-padding:3em;
-
-body, html{
-  position: relative;
-  width: 100%;
-  height:100%;
-  font-family: sans-serif;
-  font-style: normal;
-  margin:0;
-  padding:0;
-}
-
-body{
-  overflow:hidden;
-}
-
-#root{
-  display: flex;
-  flex-direction: column;
-  width: 100vw;
-  height:100vh;
-  %layout-element{
-    width:100vw;
-    box-sizing:border-box;
-  }
-  header[role="banner"]{
-    z-index:10;
-    flex: 0 0 auto;
-    @extend %layout-element;
-    padding:1em $side-padding 1em $side-padding;
-    @media only screen and (max-width: $small-bp) {
-      padding:1em $side-padding/2 0 $side-padding/2;
-    }
-  }
-  section[role="main-content"]{
-    display: flex;
-    flex:1 1 auto;
-    @extend %layout-element;
-    overflow: hidden;
-    position: relative;
-    >.wrapper{
-      position: relative;
-      padding:0 $side-padding 0 $side-padding;
-      // height:100%; max-height:100%;
-      display: flex;
-      flex: 1;
-      overflow-y: hidden;
-      overflow-x: hidden;
-      >*{
-        @include fade-transition;
-      }
-      @media only screen and (max-width: $small-bp) {
-        overflow-y: auto;
-        padding:0 $side-padding/2 0 $side-padding/2;
-      }
-    }
-    .main-content-layout{
-      position: relative;
-      // https://stackoverflow.com/a/33644245
-      display: flex;
-      flex: 1;
-      >section{
-        max-height: 100%;
-      }
-      >header,
-      >section>.wrapper,
-      >nav{
-        box-sizing: border-box;
-        max-height: 100%;
-        padding-top:1em;
-      }
-      >section>.wrapper,
-      >nav{
-        overflow-x: hidden;
-        overflow-y: auto;
-        -webkit-overflow-scrolling: touch;
-      }
-    }
-    @media only screen and (max-width: $small-bp) {
-      .main-content-layout{
-        display: flex;
-        flex-direction: column;
-      }
-    }
-  }
-  footer[role="tools"]{
-    flex:0 0 auto;
-    @extend %layout-element;
-    // padding-bottom: 1em;
-    // >*{
-    //   padding:0.5em 1em;
-    // }
-  }
-}

+ 0 - 40
src/assets/scss/base/_reset.scss

@@ -1,40 +0,0 @@
-html, body{
-  font-size:16px;
-  line-height: 0.9;
-  margin:0; padding:0;
-  background: white;
-}
-
-a{
-	color: inherit;
-	text-decoration: none;
-}
-a, a:focus, a:active { outline: none; }
-a:focus{ -moz-outline-style: none; }
-
-ul {
-  padding:0;
-  margin:0;
-}
-li{
-  padding:0;
-  margin:0;
-  list-style: none;
-}
-
-.visualy-hidden {
-  position: absolute !important;
-  clip: rect(1px, 1px, 1px, 1px);
-  overflow: hidden;
-  height: 1px;
-  width: 1px;
-  word-wrap: normal;
-}
-.visualy-hidden:active,
-.visualy-hidden:focus {
-  position: static !important;
-  clip: auto;
-  overflow: visible;
-  height: auto;
-  width: auto;
-}

+ 0 - 67
src/assets/scss/base/_transitions.scss

@@ -1,67 +0,0 @@
-
-//  _                    _ _   _
-// | |_ _ _ __ _ _ _  __(_) |_(_)___ _ _  ___
-// |  _| '_/ _` | ' \(_-< |  _| / _ \ ' \(_-<
-//  \__|_| \__,_|_||_/__/_|\__|_\___/_||_/__/
-@mixin accordeon-transition($h){
-  &.accordeon-enter,
-  &.accordeon-leave-to{
-    // opacity: 0;
-    height:0.01vh;
-    padding-top: 0;
-    padding-bottom: 0;
-    // transform: translateY(100%);
-    // transform: translate3d(0, 100%, 0);
-  }
-  &.accordeon-enter-to,
-  &.accordeon-leave{
-    // opacity:1;
-    height:$h;
-    // transform: translateY(0%);
-    // transform: translate3d(0, 0, 0);
-  }
-  &.accordeon-enter-active{
-    transition: all 300ms ease-in-out;
-  }
-  &.accordeon-leave-active{
-    transition: all 200ms ease-in-out;
-  }
-
-}
-
-@mixin fade-transition() {
-  &.fade-enter,
-  &.fade-leave-to{
-    opacity: 0;
-  }
-  &.fade-enter-to,
-  &.fade-leave{
-    opacity:1;
-  }
-  &.fade-enter-active{
-    transition: all 300ms ease-in-out;
-  }
-  &.fade-leave-active{
-    transition: all 200ms ease-in-out;
-  }
-}
-
-.fade-enter-active{
-  transition: all 300ms ease-in-out;
-}
-.fade-leave-active{
-  transition: all 200ms ease-in-out;
-}
-
-.fade-enter,
-.fade-leave-active {
-  opacity: 0
-}
-
-// .edition-texts-enter-active, .edition-texts-leave-active {
-//   transition: all 1s;
-// }
-// .edition-texts-enter, .edition-texts-leave-to /* .list-leave-active below version 2.1.8 */ {
-//   max-height: 0;
-//   // transform: translateY(30px);
-// }

+ 0 - 10
src/assets/scss/base/_variables.scss

@@ -1,10 +0,0 @@
-$base_font_size:16px;
-
-// grid
-$default_gap: 1em;
-$default_sum: 12; // total number of columns
-
-$small-bp:768px;
-$med-bp:1080px;
-$ipad-bp: 1536px;
-$large-bp:1900px;

+ 0 - 0
src/assets/scss/bootstrap-overrides/_mixins.scss


+ 0 - 0
src/assets/scss/bootstrap-overrides/_variables.scss


+ 40 - 0
src/assets/scss/globals.scss

@@ -0,0 +1,40 @@
+/*
+  ╭─────────────────────────────────────────────────────────────────╮
+  │                                                                 │
+  │   /!\ DO NOT IMPORT OR DEFINE ACTUAL RULES INTO THIS FILE /!\   │
+  │                                                                 │
+  │  Only things that disappear after scss compilation is allowed.  │
+  │                                                                 │
+  ╰─────────────────────────────────────────────────────────────────╯
+
+  This file is magically imported into every components so that scss variables and
+  mixins can be accessed.
+  But if some rules are defined here, they will be copied into the final build as many
+  times as there are components…
+
+*/
+
+
+// Custom variables must be imported first.
+@import 'bootstrap-overrides/variables';
+@import 'variables';
+
+
+// ┌─╮╭─╮╭─╮╶┬╴╭─╴╶┬╴┌─╮╭─┐┌─╮
+// │╶┤│ ││ │ │ ╰─╮ │ ├┬╯├─┤├─╯
+// └─╯╰─╯╰─╯ ╵ ╶─╯ ╵ ╵ ╰╵ ╵╵
+
+@import '~bootstrap/scss/functions';
+@import '~bootstrap/scss/variables';
+@import '~bootstrap/scss/mixins';
+
+
+// ┌─╮╭─╮╭─╮╶┬╴╭─╴╶┬╴┌─╮╭─┐┌─╮   ╷ ╷╷ ╷┌─╴
+// │╶┤│ ││ │ │ ╰─╮ │ ├┬╯├─┤├─╯╶─╴│╭╯│ │├─╴
+// └─╯╰─╯╰─╯ ╵ ╶─╯ ╵ ╵ ╰╵ ╵╵     ╰╯ ╰─╯╰─╴
+
+@import "~bootstrap-vue/src/variables";
+
+
+// mixins and functions overides must be imported after bootstrap's definitions.
+@import 'bootstrap-overrides/mixins';

+ 41 - 0
src/assets/scss/main.scss

@@ -0,0 +1,41 @@
+@import 'globals';
+
+@import 'bootstrap';
+
+
+html, body, #app {
+  height: 100%;
+}
+
+#app {
+  display: flex;
+  flex-direction: column;
+}
+
+main {
+  height: 100%;
+}
+
+// Layouts
+
+.view {
+  display: flex;
+  flex-direction: column;
+  height: 100%;
+}
+
+.split-screen {
+  display: flex;
+  height: 100%;
+
+  & > * {
+    flex-basis: 50%;
+  }
+}
+
+.container-fill {
+  display: flex;
+  flex-direction: column;
+  width: 100%;
+  height: 100%;
+}

+ 176 - 0
src/components/NodeMap.vue

@@ -0,0 +1,176 @@
+<template>
+  <svg
+    width="100%" height="100%"
+    ref="svg" :id="id"
+  >
+    <template v-if="ready">
+      <path
+        v-for="(item, index) in links"
+        :key="`link_${index}`"
+        :d="lineGenerator([item.source, item.target])"
+        stroke="black"
+      />
+
+      <template v-for="(node, index) in nodes">
+        <circle
+          :key="'circle' + index"
+          :cx="node.x"
+          :cy="node.y"
+          :class="node.data.class + (node.data.first ? ' first' : '')"
+          :r="node.children ? 7 : 5"
+          tabindex="0"
+          @click="onNodeClick(node.data)"
+          @keyup.enter="onNodeClick(node.data)"
+        >
+          <title>{{ node.data.title }}</title>
+        </circle>
+
+        <text
+          v-if="showId"
+          :key="'text' + index" :x="node.x + 7" :y="node.y-7"
+        >{{ node.data.id }}</text>
+      </template>
+    </template>
+  </svg>
+</template>
+
+<script>
+import { line } from 'd3-shape'
+import { selectAll } from 'd3-selection'
+import { drag } from 'd3-drag'
+import {
+  forceSimulation, forceLink, forceManyBody,
+  forceX, forceY, forceCenter
+} from 'd3-force'
+
+
+export default {
+  /*
+    This component uses vue as the svg renderer and the rest is handle with d3 functions
+    (events, data computing, etc.)
+  */
+  name: 'NodeMap',
+
+  props: {
+    nodes: { type: Array, required: true },
+    links: { type: Array, required: true },
+    id: { type: String, default: 'node-map' },
+    showId: { type: Boolean, default: true }
+  },
+
+  data () {
+    return {
+      ready: false,
+      width: 100,
+      height: 100,
+      simulation: forceSimulation(),
+      lineGenerator: line().x(node => node.x).y(node => node.y)
+    }
+  },
+
+  methods: {
+    updateSize () {
+      const { width, height } = this.$el.getBoundingClientRect()
+      Object.assign(this.$data, { width, height })
+    },
+
+    init () {
+      this.updateSize()
+
+      this.nodes.forEach(node => {
+        this.$set(node, 'x', this.width * 0.5)
+        this.$set(node, 'y', this.height * 0.5)
+      })
+
+      this.setupForces()
+      // Wait for DOM update so d3 can select
+      this.$nextTick(this.initListeners)
+      this.ready = true
+    },
+
+    initListeners () {
+      // TODO replace with vue events ?
+
+      selectAll(`#${this.id} circle`)
+        .data(this.nodes)
+        .call(drag()
+          .on('start', this.onNodeDragStart)
+          .on('drag', this.onNodeDrag)
+          .on('end', this.onNodeDragEnd)
+        )
+    },
+
+    setupForces () {
+      this.simulation
+        .nodes(this.nodes)
+        .force('link', forceLink(this.links).id(d => d.id).distance(50))
+        .force('charge', forceManyBody().strength(-150))
+        .force('x', forceX())
+        .force('y', forceY())
+        .force('center', forceCenter(this.width * 0.5, this.height * 0.5))
+    },
+
+    // D3 DRAG EVENT HANDLERS
+
+    onNodeDragStart (e, node) {
+      if (!e.active) {
+        this.simulation.alphaTarget(0.8).restart()
+      }
+      node.fx = node.x
+      node.fy = node.y
+    },
+
+    onNodeDrag (e, node) {
+      node.fx = e.x
+      node.fy = e.y
+    },
+
+    onNodeDragEnd (e, node) {
+      if (!e.active) {
+        this.simulation.alphaTarget(0)
+      }
+      node.fx = null
+      node.fy = null
+    },
+
+    // BASIC HANDLERS
+
+    onNodeClick (nodeData) {
+      const { id, parents } = nodeData
+      if (parents) {
+        this.$emit('open', parents[0].id, id)
+      } else {
+        this.$emit('open', id)
+      }
+    }
+  },
+
+  async mounted () {
+    this.init()
+  }
+}
+</script>
+
+<style lang="scss" scoped>
+path {
+  stroke: grey;
+}
+
+text {
+  font-size: 0.7rem;
+  user-select: none;
+}
+
+@each $id in map-keys($families){
+  .family-#{$id} {
+    fill: setColorFromId($id);
+    @if $id == 9 {
+      stroke: black;
+    }
+  }
+}
+
+.first {
+  stroke-width: 2px;
+}
+</style>

+ 53 - 0
src/components/formItems/MultipleTagsSelect.vue

@@ -0,0 +1,53 @@
+<template>
+  <b-form-tags
+    v-bind="$attrs" v-on="$listeners"
+    :value="value"
+    no-outer-focus
+  >
+    <template v-slot="{ tags, disabled, addTag, removeTag }">
+      <b-dropdown
+        :text="buttonText" size="sm"
+        variant="outline-secondary" class="d-block rounded-0"
+      >
+        <b-dropdown-item-button
+          v-for="option in availableOptions" :key="option"
+          @click="addTag(option)"
+        >
+          {{ option }}
+        </b-dropdown-item-button>
+      </b-dropdown>
+
+      <ul v-if="tags.length > 0" class="list-inline d-inline-block">
+        <li v-for="tag in tags" :key="tag" class="list-inline-item">
+          <b-form-tag
+            :title="tag" :disabled="disabled" pill
+            @remove="removeTag(tag)"
+          >
+            {{ tag }}
+          </b-form-tag>
+        </li>
+      </ul>
+    </template>
+  </b-form-tags>
+</template>
+
+<script>
+export default {
+  name: 'MultipleTagsSelect',
+
+  props: {
+    value: { type: Array, required: true },
+    options: { type: Array, required: true },
+    buttonText: { type: String, required: true }
+  },
+
+  computed: {
+    availableOptions () {
+      return this.options.filter(opt => this.value.indexOf(opt) === -1)
+    }
+  }
+}
+</script>
+
+<style lang="scss" scoped>
+</style>

+ 22 - 0
src/components/globals/ComponentDebug.vue

@@ -0,0 +1,22 @@
+<template>
+  <div class="border p-3">
+    <div>
+      <span class="font-weight-bold">{{ component.$options.name }}</span>
+      PROPS: {{ component.$props }}
+    </div>
+    <slot name="default" />
+  </div>
+</template>
+
+<script>
+export default {
+  name: 'ComponentDebug',
+
+  props: {
+    component: { type: Object, required: true }
+  }
+}
+</script>
+
+<style lang="scss" scoped>
+</style>

+ 62 - 0
src/components/text/TextCard.vue

@@ -0,0 +1,62 @@
+<template>
+  <text-card-base v-bind="$attrs">
+    <template v-slot:header-extra="{ text }">
+      <b-nav class="ml-auto flex-nowrap" pills>
+        <template v-if="children">
+          <b-nav-item
+            v-for="child in text.children" :key="child.id"
+            @click="$emit('open', text.id, child.id)"
+            :active="children.includes(child.id)"
+          >
+            {{ child.id }}
+          </b-nav-item>
+        </template>
+
+        <b-nav-item>
+          <b-button-close @click="$emit('close', text.id)" />
+        </b-nav-item>
+      </b-nav>
+    </template>
+
+    <template v-slot:footer-extra="{ text, toCommaList }">
+      <b-button
+        v-if="text.siblings"
+        :id="'siblings-' + text.id"
+      >
+        {{ $t('siblings') }}
+      </b-button>
+      <b-popover
+        v-if="text.siblings"
+        :target="'siblings-' + text.id" triggers="click" placement="top"
+      >
+        <div v-for="sibling in text.siblings" :key="sibling.id">
+          <h6>
+            <span>{{ toCommaList(sibling.authors) }},</span>
+            <span>{{ sibling.title }},</span>
+            <span>{{ sibling.edition }}</span>
+          </h6>
+        </div>
+      </b-popover>
+    </template>
+  </text-card-base>
+</template>
+
+<script>
+import TextCardBase from './TextCardBase'
+
+
+export default {
+  name: 'TextCard',
+
+  components: {
+    TextCardBase
+  },
+
+  props: {
+    children: { type: Array, default: null }
+  }
+}
+</script>
+
+<style lang="scss" scoped>
+</style>

+ 93 - 0
src/components/text/TextCardBase.vue

@@ -0,0 +1,93 @@
+<template>
+  <b-card no-body tag="article" class="text-card">
+    <b-overlay class="h-100" :show="loading">
+      <div v-if="text" class="container-fill">
+        <b-card-header header-tag="header">
+          <h4>
+            <template v-if="text.type === 'Textref'">
+              <span>{{ toCommaList(text.authors) }},</span>
+              <span>{{ text.title }},</span>
+              <span>{{ text.edition }}</span>
+            </template>
+            <template v-else>
+              {{ text.title }}
+            </template>
+          </h4>
+
+          <slot name="header-extra" :text="text" />
+        </b-card-header>
+
+        <b-card-body v-html="text.content" :class="ellipsis ? 'ellipsis' : 'overflow-auto'" />
+
+        <b-card-footer>
+          <b-badge
+            v-for="tag in text.tags" :key="tag.id"
+            variant="dark" pill
+          >
+            {{ tag.name }}
+          </b-badge>
+
+          <slot name="footer-extra" v-bind="{ text, toCommaList }" />
+        </b-card-footer>
+      </div>
+    </b-overlay>
+  </b-card>
+</template>
+
+<script>
+export default {
+  name: 'TextCardBase',
+
+  props: {
+    id: { type: Number, required: true },
+    textData: { type: Object, default: null },
+    ellipsis: { type: Boolean, default: false }
+  },
+
+  data () {
+    return {
+      loading: this.textData === null,
+      text: this.textData
+    }
+  },
+
+  methods: {
+    toCommaList (arr) {
+      // FIXME TEMP some texts doesn't have authors
+      try {
+        return arr.map(({ name }) => name).join(', ')
+      } catch {
+        return arr
+      }
+    }
+  },
+
+  created () {
+    if (this.text !== null) return
+    this.$store.dispatch('GET_TEXT', { id: this.id }).then((text) => {
+      this.text = text
+      this.loading = false
+    })
+  }
+}
+</script>
+
+<style lang="scss" scoped>
+article {
+  height: 100%;
+}
+
+header {
+  display: flex;
+
+  h4 {
+    display: flex;
+    flex-direction: column;
+  }
+}
+
+.ellipsis {
+  max-height: 10rem;
+  overflow: hidden;
+}
+</style>

+ 20 - 0
src/components/text/TextMiniCard.vue

@@ -0,0 +1,20 @@
+<template>
+  <text-card-base v-bind="$attrs" ellipsis>
+  </text-card-base>
+</template>
+
+<script>
+import TextCardBase from './TextCardBase'
+
+
+export default {
+  name: 'TextMiniCard',
+
+  components: {
+    TextCardBase
+  }
+}
+</script>
+
+<style lang="scss" scoped>
+</style>

+ 82 - 0
src/helpers/d3Data.js

@@ -0,0 +1,82 @@
+import { hierarchy } from 'd3-hierarchy'
+
+function flatten (arr, accumulator = []) {
+  return arr.reduce((acc, child) => {
+    acc.push(child)
+    return child.children ? flatten(child.children, acc) : acc
+  }, accumulator)
+}
+
+function getLinked (text) {
+  const types = ['siblings', 'children', 'parents']
+  return types.reduce((acc, type) => {
+    // Handle `null` and `undefined`
+    return text[type] ? [...acc, ...text[type]] : acc
+  }, [])
+}
+
+export function toSingleManyData (rawData) {
+  rawData.first = true
+  const h = hierarchy(rawData, d => getLinked(d))
+  h.each(node => {
+    if (node.parent && node.children) {
+      const id = node.parent.data.id
+      node.children = node.children.filter(child => child.data.id !== id)
+    }
+  })
+
+  const nodes = h.descendants()
+  const links = h.links()
+  nodes.forEach(node => {
+    Object.assign(node.data, {
+      type: node.data.type.toLowerCase(),
+      class: 'family-' + node.data.families[0].id
+    })
+  })
+  return { nodes, links }
+}
+
+
+export function toManyManyData (rawData) {
+  rawData.first = true
+  const h = hierarchy(rawData, d => getLinked(d))
+  h.each(node => {
+    if (node.parent && node.children) {
+      const id = node.parent.data.id
+      // Remove reference of parent in child's children.
+      node.children = node.children.filter(child => child.data.id !== id)
+    }
+  })
+
+  const links = []
+  const nodes = flatten(h.descendants()).reduce((nodes, node) => {
+    const sameNode = nodes.find(n => node.data.id === n.data.id)
+    if (sameNode) {
+      if (!node.children) return nodes
+      node.children.forEach(child => {
+        if (!sameNode.children.find(c => child.data.id === c.data.id)) {
+          sameNode.children.push(child)
+        }
+      })
+    } else {
+      if (!node.children) node.children = []
+      nodes.push(node)
+    }
+    return nodes
+  }, []).map(({ data, children, depth }) => {
+    if (children) {
+      children.forEach(child => {
+        links.push({ source: data.id, target: child.data.id })
+      })
+    }
+    return {
+      id: data.id,
+      data: {
+        ...data,
+        type: data.type.toLowerCase(),
+        class: 'family-' + data.families[0].id
+      }
+    }
+  })
+  return { nodes, links }
+}

+ 20 - 2
src/main.js

@@ -1,14 +1,32 @@
 import Vue from 'vue'
+import BootstrapVue from 'bootstrap-vue'
 import Meta from 'vue-meta'
 import Vue2TouchEvents from 'vue2-touch-events'
+import VueMessages from './messages'
+import App from './App'
 import router from './router'
 import store from './store'
 
-import App from './App'
-
 
+Vue.use(BootstrapVue, {
+  BButton: { pill: true },
+  BCardHeader: { headerBgVariant: 'white' },
+  BCardFooter: { footerBgVariant: 'white' }
+})
 Vue.use(Meta)
 Vue.use(Vue2TouchEvents)
+Vue.use(VueMessages)
+
+
+// Register global components
+const requireComponent = require.context('@/components/globals', true, /\.(js|vue)$/i)
+// For each matching file name...
+requireComponent.keys().forEach((fileName) => {
+  // Get the component
+  const component = requireComponent(fileName).default
+  // Globally register the component
+  Vue.component(component.name, component)
+})
 
 
 new Vue({

+ 33 - 0
src/messages/fr.json

@@ -0,0 +1,33 @@
+{
+  "title": "En françaiS au pluriel",
+  "sections": {
+    "home": "Accueil",
+    "introduction": "Présentation",
+    "contact": "Contact",
+    "library": "Bibliothèque",
+    "kit": "Kit de désapprentissage",
+    "gallery": "Créations numériques",
+    "blog": "Blog"
+  },
+  "options": {
+    "display": {
+      "title": "Gestion de l'affichage",
+      "choices": {
+        "tree-map": "Arborescent",
+        "card-map": "Aléatoire",
+        "card-list": "Alphabétique"
+      }
+    },
+    "filters": {
+      "title": "Filtres",
+      "choices": {
+        "tags": "Tags",
+        "strangeness": "Degré d'étrangement"
+      }
+    },
+    "search": {
+      "title": "Rechercher"
+    }
+  },
+  "siblings": "Textes rebonds"
+}

+ 34 - 0
src/messages/index.js

@@ -0,0 +1,34 @@
+import messages from './fr.json'
+
+
+function getMessage (keyedPath, alt) {
+  let message
+  try {
+    message = keyedPath.split('.').reduce((parent, key) => {
+      return parent[key]
+    }, messages)
+  } catch {
+    // Silence errors like accessing a key on `undefined`.
+  }
+  return message === undefined
+    ? alt === undefined ? keyedPath : alt
+    : message
+}
+
+
+/*
+ * Allows separtion of static text content from its templates.
+ */
+export default class VueMessages {
+  static install (Vue) {
+    // Adds `this.$t('path.to.string', 'alt')` to every instances.
+    Vue.prototype.$t = getMessage
+
+    // Adds `v-t="'path.to.string'"` directive to every elements.
+    Vue.directive('t', {
+      bind (el, { value }) {
+        el.textContent = getMessage(value)
+      }
+    })
+  }
+}

+ 12 - 0
src/pages/Blog.vue

@@ -0,0 +1,12 @@
+<template>
+  <component-debug :component="this" />
+</template>
+
+<script>
+export default {
+  name: 'Blog'
+}
+</script>
+
+<style lang="scss" scoped>
+</style>

+ 12 - 0
src/pages/Contact.vue

@@ -0,0 +1,12 @@
+<template>
+  <component-debug :component="this" />
+</template>
+
+<script>
+export default {
+  name: 'Contact'
+}
+</script>
+
+<style lang="scss" scoped>
+</style>

+ 12 - 0
src/pages/Gallery.vue

@@ -0,0 +1,12 @@
+<template>
+  <component-debug :component="this" />
+</template>
+
+<script>
+export default {
+  name: 'Gallery'
+}
+</script>
+
+<style lang="scss" scoped>
+</style>

+ 3 - 16
src/pages/Home.vue

@@ -1,23 +1,10 @@
 <template>
-  <div class="home">
-    {{ text }}
-  </div>
+  <component-debug :component="this">
+  </component-debug>
 </template>
 
 <script>
 export default {
-  name: 'Home',
-
-  data () {
-    return {
-      text: null
-    }
-  },
-
-  created () {
-    this.$store.dispatch('GET_TEXT', 1).then(({ data }) => {
-      this.text = data.data.textref
-    })
-  }
+  name: 'Home'
 }
 </script>

+ 12 - 0
src/pages/Introduction.vue

@@ -0,0 +1,12 @@
+<template>
+  <component-debug :component="this" />
+</template>
+
+<script>
+export default {
+  name: 'Introduction'
+}
+</script>
+
+<style lang="scss" scoped>
+</style>

+ 12 - 0
src/pages/Kit.vue

@@ -0,0 +1,12 @@
+<template>
+  <component-debug :component="this" />
+</template>
+
+<script>
+export default {
+  name: 'Kit'
+}
+</script>
+
+<style lang="scss" scoped>
+</style>

+ 90 - 0
src/pages/Library.vue

@@ -0,0 +1,90 @@
+<template>
+  <div class="h-100 position-relative">
+    <!-- BACKGROUND (mode) -->
+    <component :is="mode" @open="openText" />
+
+    <!-- FOREGROUND (texts) -->
+    <section v-for="parent in parents" :key="parent.id" class="split-screen">
+      <text-card
+        :id="parent.id"
+        :children="parent.children"
+        @open="openText" @close="closeText"
+      />
+      <div>
+        <text-card
+          v-for="id in parent.children" :key="id"
+          :id="id"
+          @close="closeText(parent.id, $event)"
+        />
+      </div>
+    </section>
+  </div>
+</template>
+
+<script>
+import { CardList, CardMap, TreeMap } from './library'
+import TextCard from '@/components/text/TextCard'
+
+
+export default {
+  name: 'Library',
+
+  components: {
+    CardList,
+    CardMap,
+    TreeMap,
+    TextCard
+  },
+
+  props: {
+    mode: { type: String, required: true },
+    texts: { type: Array, required: true }
+  },
+
+  computed: {
+    parents () {
+      return this.texts.map(text => {
+        const [id, ...children] = text
+        return { id, children }
+      })
+    }
+  },
+
+  methods: {
+    openText (id, childId) {
+      const parent = this.parents.find(p => p.id === id)
+      if (parent && (childId === undefined || parent.children.includes(childId))) return
+
+      if (!parent) {
+        this.parents.push({ id, children: childId ? [childId] : [] })
+      } else if (childId) {
+        parent.children.push(childId)
+      }
+      this.updateQuery(this.parents)
+    },
+
+    closeText (id, childId) {
+      const parent = this.parents.find(p => p.id === id)
+      if (!childId) {
+        this.updateQuery(this.parents.filter(p => p !== parent))
+      } else {
+        parent.children = parent.children.filter(childId_ => childId_ !== childId)
+        this.updateQuery(this.parents)
+      }
+    },
+
+    updateQuery (parents) {
+      // Update the route's query (will not reload the page) and let vue determine what changed.
+      this.$router.push({
+        query: {
+          mode: this.mode,
+          texts: parents.map(parent => [parent.id, ...parent.children])
+        }
+      })
+    }
+  }
+}
+</script>
+
+<style lang="scss" scoped>
+</style>

+ 132 - 0
src/pages/Map.vue

@@ -0,0 +1,132 @@
+<template>
+  <b-overlay :show="loading" rounded="sm" class="h-100">
+    <div class="d-flex flex-column h-100 px-4 py-3">
+      <div id="maps">
+        <div>
+          <h4>Carte avec duplication</h4>
+          <div class="node-map">
+            <node-map
+              v-if="data"
+              v-bind="mapSingle"
+              :show-id="showId"
+            />
+          </div>
+        </div>
+        <div>
+          <h4>Carte avec liens multiples</h4>
+          <div class="node-map">
+            <node-map
+              v-if="data"
+              v-bind="mapMany"
+              :show-id="showId"
+              id="map-2"
+            />
+          </div>
+        </div>
+      </div>
+
+      <b-form class="mt-4">
+        <b-form-group label="Texte de départ :" label-cols="2" class="mb-2">
+          <b-form-select
+            v-model="textId"
+            @input="query"
+            :options="textsDepartOptions"
+          />
+        </b-form-group>
+
+        <b-form-group label="Profondeur :" label-cols="2" class="mb-2">
+          <b-form-spinbutton
+            v-model="depth"
+            @input="query"
+            min="0" max="6"
+            inline
+          />
+        </b-form-group>
+
+        <b-form-checkbox v-model="showId" name="check-button" switch>
+          Afficher les numéros
+        </b-form-checkbox>
+      </b-form>
+    </div>
+  </b-overlay>
+</template>
+
+<script>
+import { mapGetters } from 'vuex'
+
+import NodeMap from '@/components/NodeMap'
+import { toSingleManyData, toManyManyData } from '@/helpers/d3Data'
+
+
+export default {
+  name: 'Home',
+
+  components: {
+    NodeMap
+  },
+
+  data () {
+    return {
+      loading: false,
+      textId: undefined,
+      showId: true,
+      depth: 3,
+      data: null,
+      mapSingle: { nodes: null, links: null },
+      mapMany: { nodes: null, links: null }
+    }
+  },
+
+  computed: {
+    ...mapGetters(['textsDepartOptions'])
+  },
+
+  methods: {
+    query () {
+      const { textId: id, depth } = this
+      this.data = null
+      this.loading = true
+      this.$store.dispatch('GET_TREE_WITH_DEPTH', { id, depth }).then((data) => {
+        this.mapSingle = toSingleManyData(data)
+        this.mapMany = toManyManyData(data)
+        this.data = data
+        this.loading = false
+      })
+    }
+  },
+
+  created () {
+    this.loading = true
+    this.$store.dispatch('GET_TEXTS_DEPART').then(textsDepart => {
+      this.textId = textsDepart[0].id
+    })
+  }
+}
+</script>
+
+<style lang="scss" scoped>
+#maps {
+  width: 100%;
+  height: 100%;
+  display: flex;
+
+  & > div {
+    flex-basis: 50%;
+    display: flex;
+    flex-direction: column;
+
+    & + div .node-map {
+      border-left: none;
+    }
+  }
+
+  .node-map {
+    height: 100%;
+    border: 1px solid grey;
+  }
+
+}
+.col > * {
+  width: auto;
+}
+</style>

+ 54 - 0
src/pages/library/CardList.vue

@@ -0,0 +1,54 @@
+<template>
+  <div class="h-100 position-absolute overflow-auto">
+    <div v-for="(parents, char) in orderedParents" :key="char">
+      <h3>{{ char }}</h3>
+      <b-card-group deck>
+        <text-mini-card
+          v-for="parent in parents" :key="parent.id"
+          :id="parent.id"
+          :text-data="parent"
+          @click.native="$emit('open', parent.id)"
+        />
+      </b-card-group>
+    </div>
+  </div>
+</template>
+
+<script>
+import { mapGetters } from 'vuex'
+
+import TextMiniCard from '@/components/text/TextMiniCard'
+
+
+export default {
+  name: 'CardList',
+
+  components: {
+    TextMiniCard
+  },
+
+  computed: {
+    ...mapGetters(['orderedParents'])
+  },
+
+  created () {
+    this.$store.dispatch('GET_TEXTS_DEPART')
+  }
+}
+</script>
+
+<style lang="scss" scoped>
+.card-deck {
+  .card {
+    flex-basis: auto;
+    @include media-breakpoint-up(md) {
+      flex-basis: 50%;
+      max-width: calc(50% - 30px);
+    }
+    @include media-breakpoint-up(lg) {
+      flex-basis: 33%;
+      max-width: calc(33.3% - 30px);
+    }
+  }
+}
+</style>

+ 12 - 0
src/pages/library/CardMap.vue

@@ -0,0 +1,12 @@
+<template>
+  <component-debug :component="this" />
+</template>
+
+<script>
+export default {
+  name: 'CardMap'
+}
+</script>
+
+<style lang="scss" scoped>
+</style>

+ 94 - 0
src/pages/library/LibraryOptions.vue

@@ -0,0 +1,94 @@
+<template>
+  <div v-if="show" class="d-flex justify-content-between">
+    <b-form-group
+      :label="$t('options.display.title')"
+      v-slot="{ ariaDescribedby }"
+    >
+      <b-form-radio-group
+        id="mode-select"
+        v-model="currentMode" :options="modes"
+        name="mode-select" :aria-describedby="ariaDescribedby" buttons
+        size="sm"
+      />
+    </b-form-group>
+
+    <b-form-group
+      v-if="currentMode !== 'tree-map'"
+      :label="$t('options.filters.title')"
+    >
+      <b-form-group
+        :label="$t('options.filters.choices.tags')"
+        label-for="tags-select"
+      >
+        <multiple-tags-select
+          id="tags-select" :button-text="$t('options.filters.choices.tags')"
+          v-model="selectedTags" :options="tags"
+        />
+      </b-form-group>
+
+      <b-form-group
+        :label="$t('options.filters.choices.strangeness')"
+        label-for="strangeness-input"
+      >
+        <b-form-input
+          id="strangeness-input"
+          type="range" min="0" max="5"
+          v-model="strangeness"
+        />
+      </b-form-group>
+    </b-form-group>
+
+    <b-form-group
+      :label="$t('options.search.title')"
+      label-for="search-input"
+    >
+      <b-input-group size="sm" append="search-icon">
+        <b-form-input v-model="search" id="search-input" trim />
+      </b-input-group>
+    </b-form-group>
+  </div>
+</template>
+
+<script>
+import MultipleTagsSelect from '@/components/formItems/MultipleTagsSelect'
+
+export default {
+  name: 'LibraryOptions',
+
+  components: {
+    MultipleTagsSelect
+  },
+
+  props: {
+    show: { type: Boolean, required: true },
+    mode: { type: String, required: true },
+    tags: { type: Array, default: () => ([]) }
+  },
+
+  data () {
+    return {
+      modes: [
+        { text: this.$t('options.display.choices.tree-map'), value: 'tree-map' },
+        { text: this.$t('options.display.choices.card-map'), value: 'card-map' },
+        { text: this.$t('options.display.choices.card-list'), value: 'card-list' }
+      ],
+      currentMode: this.mode,
+      selectedTags: [],
+      strangeness: 0,
+      search: ''
+    }
+  },
+
+  watch: {
+    currentMode (mode) {
+      this.$router.push({ query: { mode } })
+    }
+  }
+}
+</script>
+
+<style lang="scss" scoped>
+::v-deep fieldset > div {
+  display: flex;
+}
+</style>

+ 47 - 0
src/pages/library/TreeMap.vue

@@ -0,0 +1,47 @@
+<template>
+  <b-overlay :show="loading" class="h-100 position-absolute">
+    <node-map
+      v-if="!loading"
+      v-bind="mapData"
+      :show-id="true"
+      v-on="$listeners"
+    />
+  </b-overlay>
+</template>
+
+<script>
+import NodeMap from '@/components/NodeMap'
+import { toManyManyData } from '@/helpers/d3Data'
+
+
+export default {
+  name: 'TreeMap',
+
+  components: {
+    NodeMap
+  },
+
+  props: {
+    id: { type: Number, default: 1 }
+  },
+
+  data () {
+    return {
+      loading: true,
+      depth: 3,
+      mapData: { nodes: null, links: null }
+    }
+  },
+
+  created () {
+    const { id, depth } = this
+    this.$store.dispatch('GET_TREE_WITH_DEPTH', { id, depth }).then((data) => {
+      this.mapData = toManyManyData(data)
+      this.loading = false
+    })
+  }
+}
+</script>
+
+<style lang="scss" scoped>
+</style>

+ 3 - 0
src/pages/library/index.js

@@ -0,0 +1,3 @@
+export { default as CardList } from './CardList'
+export { default as CardMap } from './CardMap'
+export { default as TreeMap } from './TreeMap'

+ 2 - 15
src/router/index.js

@@ -1,22 +1,9 @@
 import Vue from 'vue'
 import VueRouter from 'vue-router'
-import Home from '../pages/Home.vue'
+import routes from './routes'
 
-Vue.use(VueRouter)
 
-const routes = [
-  {
-    name: 'home',
-    path: '/',
-    component: Home
-  },
-  {
-    name: 'notfound',
-    path: '/404',
-    alias: '*',
-    component: () => import(/* webpackChunkName: "404" */ '../pages/NotFound.vue')
-  }
-]
+Vue.use(VueRouter)
 
 const router = new VueRouter({
   mode: 'history',

+ 78 - 0
src/router/routes.js

@@ -0,0 +1,78 @@
+import Home from '@/pages/Home'
+
+
+export default [
+  {
+    name: 'home',
+    path: '/',
+    component: Home
+  },
+
+  {
+    name: 'introduction',
+    path: '/intro',
+    component: () => import(/* webpackChunkName: "intro" */ '@/pages/Introduction')
+  },
+
+  {
+    name: 'contact',
+    path: '/contact',
+    component: () => import(/* webpackChunkName: "contact" */ '@/pages/Contact')
+  },
+
+  {
+    name: 'library',
+    path: '/library',
+    components: {
+      default: () => import(/* webpackChunkName: "library" */ '@/pages/Library'),
+      options: () => import(/* webpackChunkName: "library" */ '@/pages/library/LibraryOptions')
+    },
+    props: {
+      default: ({ query }) => {
+        let { mode = 'tree-map', texts = [] } = query
+        if (typeof texts === 'string') texts = [texts]
+        // In case of a reload or direct link, vue-router doesn't turn the query string into an array.
+        if (texts && texts.length && typeof texts[0] === 'string') {
+          texts = texts.map(text => text.split(',').map(id => parseInt(id)))
+        }
+        return { mode, texts }
+      },
+      options: ({ query }) => ({
+        show: !('texts' in query && query.texts.length),
+        mode: query.mode || 'tree-map'
+      })
+    }
+  },
+
+  {
+    name: 'kit',
+    path: '/kit',
+    component: () => import(/* webpackChunkName: "kit" */ '@/pages/Kit')
+  },
+
+  {
+    name: 'gallery',
+    path: '/gallery',
+    component: () => import(/* webpackChunkName: "gallery" */ '@/pages/Gallery')
+  },
+
+  {
+    name: 'blog',
+    path: '/blog',
+    component: () => import(/* webpackChunkName: "blog" */ '@/pages/Blog')
+  },
+
+  // TEMP
+  {
+    name: 'map',
+    path: '/map',
+    component: () => import(/* webpackChunkName: "test" */ '@/pages/Map')
+  },
+
+  {
+    name: 'notfound',
+    path: '/404',
+    alias: '*',
+    component: () => import(/* webpackChunkName: "404" */ '@/pages/NotFound.vue')
+  }
+]

+ 2 - 16
src/store/index.js

@@ -1,27 +1,13 @@
 import Vue from 'vue'
 import Vuex from 'vuex'
 
-import api from '@/api'
-import { print } from 'graphql/language/printer'
-import TextRef from '@/api/queries/TextRef.gql'
+import texts from './modules/texts'
 
 
 Vue.use(Vuex)
 
-
 export default new Vuex.Store({
   modules: {
-  },
-
-  state: {
-  },
-
-  mutations: {
-  },
-
-  actions: {
-    'GET_TEXT' ({ state }, id) {
-      return api.post('', { query: print(TextRef), variables: { id } })
-    }
+    texts
   }
 })

+ 0 - 24
src/store/modules/corpus.js

@@ -1,24 +0,0 @@
-
-export default {
-  namespaced: true,
-  // initial state
-  state: {
-    // items: [],
-  },
-  // getters
-  getters: {
-
-  },
-  // mutations
-  mutations: {
-
-  },
-  // actions
-  actions: {
-
-  },
-  // methods
-  methods: {
-
-  }
-}

+ 80 - 0
src/store/modules/texts.js

@@ -0,0 +1,80 @@
+import api from '@/api'
+import { print } from 'graphql/language/printer'
+import {
+  TextsDepart, TextCard, TextdepartRecursive
+} from '@/api/queries'
+import TextdepartRecursiveWithDepth from '@/api/queries/TextdepartRecursiveWithDepth.gql'
+
+export default {
+  state: {
+    textsDepart: undefined
+  },
+
+  mutations: {
+    'SET_TEXTS_DEPART' (state, texts) {
+      state.textsDepart = texts
+    }
+  },
+
+  actions: {
+    'GET_TEXTS_DEPART' ({ state, commit }) {
+      return api.post('', { query: print(TextsDepart) }).then(({ data }) => {
+        commit('SET_TEXTS_DEPART', data.data.textsdepart)
+        return state.textsDepart
+      })
+    },
+
+    'GET_TEXT' (store, { id }) {
+      return api.post('', { query: print(TextCard), variables: { id } })
+        .then(data => (data.data.data.text))
+    },
+
+    'GET_TREE' (store, id) {
+      return api.post('', { query: print(TextdepartRecursive), variables: { id } })
+        .then(({ data }) => (data.data.textref))
+    },
+
+    'GET_TREE_WITH_DEPTH' (store, { id, depth }) {
+      const baseQuery = print(TextdepartRecursiveWithDepth)
+      function formatQuery (str, depth) {
+        if (depth > 0) {
+          return formatQuery(
+            str.replace('INPUT', '...TextrefTreeFields\nsiblings: text_en_rebond {\nINPUT\n}'),
+            --depth
+          )
+        } else {
+          return str.replace('INPUT', '...TextrefTreeFields')
+        }
+      }
+      return api.post('', { query: formatQuery(baseQuery, depth), variables: { id } })
+        .then(({ data }) => (data.data.textref))
+    }
+  },
+
+  getters: {
+    textsDepartOptions: state => {
+      if (!state.textsDepart) return undefined
+      return state.textsDepart.map(({ id, title }) => ({
+        value: id,
+        text: `(${id}) ${title}`
+      }))
+    },
+
+    orderedParents: state => {
+      // FIXME duplicates references if multiple authors ?
+      if (!state.textsDepart) return undefined
+      return state.textsDepart.sort((a, b) => {
+        if (!b.authors) return -1
+        if (!a.authors) return +1
+        if (a.authors[0].name < b.authors[0].name) return -1
+        if (a.authors[0].name > b.authors[0].name) return 1
+        return 0
+      }).reduce((dict, text) => {
+        const firstChar = text.authors ? text.authors[0].name[0] : '&'
+        if (!(firstChar in dict)) dict[firstChar] = []
+        dict[firstChar].push(text)
+        return dict
+      }, {})
+    }
+  }
+}

+ 7 - 1
vue.config.js

@@ -8,7 +8,13 @@ module.exports = {
       .loader('graphql-tag/loader')
       .end()
   },
-
+  css: {
+    loaderOptions: {
+      sass: {
+        prependData: '@import "@/assets/scss/globals.scss";'
+      }
+    }
+  },
   publicPath: '/',
   devServer: {
     clientLogLevel: 'warning',

Some files were not shown because too many files changed in this diff