Browse Source

Reformat codebase with Prettier (#7912)

- Automated reformatting
- Manually change code where Prettier reformatting causes issues
- Revert "Disable Prettier formatting in CI for now"
Thibaud Colas 3 years ago
parent
commit
af942a27e4
100 changed files with 6787 additions and 6477 deletions
  1. 31 32
      .circleci/config.yml
  2. 121 123
      .eslintrc.js
  3. 10 10
      .github/workflows/codeql-analysis.yml
  4. 27 28
      .github/workflows/test.yml
  5. 2 2
      .readthedocs.yml
  6. 8 10
      client/.storybook/main.js
  7. 2 2
      client/.storybook/preview.js
  8. 71 72
      client/scss/components/_breadcrumb.scss
  9. 98 99
      client/scss/components/_bulk_actions.scss
  10. 13 13
      client/scss/components/_button-select.scss
  11. 509 512
      client/scss/components/_button.scss
  12. 93 89
      client/scss/components/_chooser.scss
  13. 91 91
      client/scss/components/_comments-controls.scss
  14. 53 53
      client/scss/components/_comments-notification-dropdown.scss
  15. 280 279
      client/scss/components/_dropdown.legacy.scss
  16. 64 64
      client/scss/components/_dropdown.scss
  17. 139 139
      client/scss/components/_footer.scss
  18. 346 342
      client/scss/components/_forms.scss
  19. 71 71
      client/scss/components/_grid.legacy.scss
  20. 172 170
      client/scss/components/_header.scss
  21. 55 55
      client/scss/components/_help-block.scss
  22. 14 14
      client/scss/components/_human-readable-date.scss
  23. 91 92
      client/scss/components/_icons.scss
  24. 27 28
      client/scss/components/_indicator.scss
  25. 5 5
      client/scss/components/_link.legacy.scss
  26. 573 581
      client/scss/components/_listing.scss
  27. 30 30
      client/scss/components/_loading-mask.scss
  28. 97 97
      client/scss/components/_logo.scss
  29. 445 449
      client/scss/components/_main-nav.scss
  30. 13 13
      client/scss/components/_media-placeholder.scss
  31. 11 11
      client/scss/components/_messages.capability.scss
  32. 58 58
      client/scss/components/_messages.scss
  33. 6 6
      client/scss/components/_messages.status.scss
  34. 94 90
      client/scss/components/_modals.scss
  35. 13 13
      client/scss/components/_privacy-indicator.scss
  36. 21 21
      client/scss/components/_progressbar.scss
  37. 12 12
      client/scss/components/_skiplink.scss
  38. 33 33
      client/scss/components/_status-tag.scss
  39. 90 87
      client/scss/components/_switch.scss
  40. 132 132
      client/scss/components/_tabs.scss
  41. 42 42
      client/scss/components/_tag.scss
  42. 60 60
      client/scss/components/_tooltips.scss
  43. 34 34
      client/scss/components/_workflow-tasks.scss
  44. 3 3
      client/scss/components/browser-message.scss
  45. 0 8
      client/scss/core.scss
  46. 22 22
      client/scss/elements/_elements.scss
  47. 191 194
      client/scss/elements/_forms.scss
  48. 31 7
      client/scss/elements/_root.scss
  49. 42 42
      client/scss/elements/_typography.scss
  50. 31 31
      client/scss/objects/_avatar.scss
  51. 9 10
      client/scss/objects/_objects.scss
  52. 1 1
      client/scss/overrides/_pages.homepage.scss
  53. 1 1
      client/scss/overrides/_pages.page-explorer.scss
  54. 34 35
      client/scss/overrides/_utilities.dropdowns.scss
  55. 2 2
      client/scss/overrides/_utilities.focus.scss
  56. 19 19
      client/scss/overrides/_utilities.hidden.scss
  57. 20 21
      client/scss/overrides/_utilities.legacy.scss
  58. 1 1
      client/scss/overrides/_utilities.text.legacy.scss
  59. 3 3
      client/scss/overrides/_utilities.text.scss
  60. 2 2
      client/scss/overrides/_utilities.visuallyhidden.scss
  61. 268 261
      client/scss/overrides/_vendor.datetimepicker.scss
  62. 24 23
      client/scss/overrides/_vendor.tagit.scss
  63. 86 86
      client/scss/settings/_variables.icons.scss
  64. 30 11
      client/scss/settings/_variables.scss
  65. 0 7
      client/scss/sidebar.scss
  66. 13 9
      client/scss/tools/_functions.breakpoints.scss
  67. 14 15
      client/scss/tools/_mixins.breakpoints.scss
  68. 66 68
      client/scss/tools/_mixins.general.scss
  69. 34 34
      client/scss/tools/_mixins.grid.scss
  70. 51 45
      client/scss/tools/_various.colors.scss
  71. 7 8
      client/src/api/admin.test.js
  72. 26 11
      client/src/api/admin.ts
  73. 14 11
      client/src/api/client.js
  74. 6 2
      client/src/components/Button/Button.test.js
  75. 2 6
      client/src/components/Button/Button.tsx
  76. 8 3
      client/src/components/CommentApp/__fixtures__/state.tsx
  77. 12 8
      client/src/components/CommentApp/actions/comments.ts
  78. 1 1
      client/src/components/CommentApp/actions/settings.ts
  79. 4 3
      client/src/components/CommentApp/components/Comment/index.stories.tsx
  80. 108 77
      client/src/components/CommentApp/components/Comment/index.tsx
  81. 144 144
      client/src/components/CommentApp/components/Comment/style.scss
  82. 74 37
      client/src/components/CommentApp/components/CommentHeader/index.tsx
  83. 123 116
      client/src/components/CommentApp/components/CommentHeader/style.scss
  84. 2 1
      client/src/components/CommentApp/components/CommentReply/index.stories.tsx
  85. 71 45
      client/src/components/CommentApp/components/CommentReply/index.tsx
  86. 100 100
      client/src/components/CommentApp/components/CommentReply/style.scss
  87. 53 45
      client/src/components/CommentApp/components/TextArea/index.tsx
  88. 100 100
      client/src/components/CommentApp/main.scss
  89. 83 62
      client/src/components/CommentApp/main.tsx
  90. 17 14
      client/src/components/CommentApp/selectors/index.ts
  91. 63 54
      client/src/components/CommentApp/selectors/selectors.test.ts
  92. 16 8
      client/src/components/CommentApp/state/comments.test.ts
  93. 136 133
      client/src/components/CommentApp/state/comments.ts
  94. 12 9
      client/src/components/CommentApp/state/settings.ts
  95. 23 14
      client/src/components/CommentApp/utils/layout.ts
  96. 25 17
      client/src/components/CommentApp/utils/storybook.tsx
  97. 23 23
      client/src/components/Draftail/CommentableEditor/CommentableEditor.scss
  98. 15 15
      client/src/components/Draftail/CommentableEditor/CommentableEditor.test.tsx
  99. 353 276
      client/src/components/Draftail/CommentableEditor/CommentableEditor.tsx
  100. 11 9
      client/src/components/Draftail/DraftUtils.js

+ 31 - 32
.circleci/config.yml

@@ -12,9 +12,9 @@ jobs:
           key: pipenv-v1-{{ checksum "setup.py" }}
       # Only install if .venv wasn’t cached.
       - run: |
-            if [[ ! -e ".venv" ]]; then
-                pipenv install -e .[testing]
-            fi
+          if [[ ! -e ".venv" ]]; then
+              pipenv install -e .[testing]
+          fi
       - save_cache:
           key: pipenv-v1-{{ checksum "setup.py" }}
           paths:
@@ -39,9 +39,9 @@ jobs:
           key: frontend-v1-{{ checksum "package-lock.json" }}
       # Only install if node_modules wasn’t cached.
       - run: |
-            if [[ ! -e "node_modules" ]]; then
-                npm install --no-save --no-optional --no-audit --no-fund --progress=false
-            fi
+          if [[ ! -e "node_modules" ]]; then
+              npm install --no-save --no-optional --no-audit --no-fund --progress=false
+          fi
       - save_cache:
           paths:
             - node_modules
@@ -54,8 +54,7 @@ jobs:
             - wagtail
       - run: npm run lint:js
       - run: npm run lint:css
-      # TODO Remove || true and enforce a successful exit code.
-      - run: npm run lint:format || true
+      - run: npm run lint:format
       - run: npm run test:unit:coverage -- --runInBand
       - run: bash <(curl -s https://codecov.io/bash) -F frontend
 
@@ -73,9 +72,9 @@ jobs:
           key: pipenv-v1-{{ checksum "setup.py" }}
       # Only install if .venv wasn’t cached.
       - run: |
-            if [[ ! -e ".venv" ]]; then
-                pipenv install -e .[testing]
-            fi
+          if [[ ! -e ".venv" ]]; then
+              pipenv install -e .[testing]
+          fi
       - save_cache:
           key: pipenv-v1-{{ checksum "setup.py" }}
           paths:
@@ -84,9 +83,9 @@ jobs:
           key: ui_tests-npm_integration-v1-{{ checksum "client/tests/integration/package-lock.json" }}
       # Only install if node_modules wasn’t cached.
       - run: |
-            if [[ ! -e "client/tests/integration/node_modules" ]]; then
-                npm --prefix ./client/tests/integration ci
-            fi
+          if [[ ! -e "client/tests/integration/node_modules" ]]; then
+              npm --prefix ./client/tests/integration ci
+          fi
       - save_cache:
           key: ui_tests-npm_integration-v1-{{ checksum "client/tests/integration/package-lock.json" }}
           paths:
@@ -123,22 +122,22 @@ jobs:
       - run: python scripts/nightly/upload.py
 
 workflows:
-    version: 2
-    test:
-      jobs:
-        - backend
-        - frontend
-        - ui_tests:
-            requires:
-              - frontend
+  version: 2
+  test:
+    jobs:
+      - backend
+      - frontend
+      - ui_tests:
+          requires:
+            - frontend
 
-    nightly:
-      jobs:
-        - nightly-build
-      triggers:
-        - schedule:
-            cron: "0 0 * * *"
-            filters:
-              branches:
-                only:
-                  - main
+  nightly:
+    jobs:
+      - nightly-build
+    triggers:
+      - schedule:
+          cron: '0 0 * * *'
+          filters:
+            branches:
+              only:
+                - main

+ 121 - 123
.eslintrc.js

@@ -1,83 +1,81 @@
 // Rules which have been enforced in configuration upgrades and flag issues in existing code.
 // We need to consider whether to disable those rules permanently, or fix the issues.
 const legacyCode = {
-  "class-methods-use-this": "off",
-  "constructor-super": "off",
-  "default-param-last": "off",
-  "import/extensions": "off",
-  "import/first": "off",
-  "import/newline-after-import": "off",
-  "import/no-extraneous-dependencies": "off",
-  "import/no-unresolved": "off",
-  "import/no-useless-path-segments": "off",
-  "import/order": "off",
-  "import/prefer-default-export": "off",
-  "jsx-a11y/alt-text": "off",
-  "jsx-a11y/anchor-is-valid": "off",
-  "jsx-a11y/click-events-have-key-events": "off",
-  "jsx-a11y/interactive-supports-focus": "off",
-  "jsx-a11y/no-noninteractive-element-interactions": "off",
-  "jsx-a11y/role-supports-aria-props": "off",
-  "lines-between-class-members": "off",
-  "max-classes-per-file": "off",
-  "no-await-in-loop": "off",
-  "no-continue": "off",
-  "no-else-return": "off",
-  "no-extra-boolean-cast": "off",
-  "no-import-assign": "off",
-  "no-lonely-if": "off",
-  "no-plusplus": "off",
-  "no-prototype-builtins": "off",
-  "no-restricted-syntax": "off",
-  "no-this-before-super": "off",
-  "operator-assignment": "off",
-  "prefer-destructuring": "off",
-  "prefer-object-spread": "off",
-  "prefer-promise-reject-errors": "off",
-  "react-hooks/exhaustive-deps": "off",
-  "react-hooks/rules-of-hooks": "off",
-  "react/button-has-type": "off",
-  "react/destructuring-assignment": "off",
-  "react/forbid-prop-types": "off",
-  "react/function-component-definition": "off",
-  "react/jsx-curly-brace-presence": "off",
-  "react/jsx-filename-extension": "off",
-  "react/jsx-no-useless-fragment": "off",
-  "react/jsx-props-no-spreading": "off",
-  "react/no-danger": "off",
-  "react/no-deprecated": "off",
-  "react/require-default-props": "off",
-}
+  'class-methods-use-this': 'off',
+  'constructor-super': 'off',
+  'default-param-last': 'off',
+  'import/extensions': 'off',
+  'import/first': 'off',
+  'import/newline-after-import': 'off',
+  'import/no-extraneous-dependencies': 'off',
+  'import/no-unresolved': 'off',
+  'import/no-useless-path-segments': 'off',
+  'import/order': 'off',
+  'import/prefer-default-export': 'off',
+  'jsx-a11y/alt-text': 'off',
+  'jsx-a11y/anchor-is-valid': 'off',
+  'jsx-a11y/click-events-have-key-events': 'off',
+  'jsx-a11y/interactive-supports-focus': 'off',
+  'jsx-a11y/no-noninteractive-element-interactions': 'off',
+  'jsx-a11y/role-supports-aria-props': 'off',
+  'lines-between-class-members': 'off',
+  'max-classes-per-file': 'off',
+  'no-await-in-loop': 'off',
+  'no-continue': 'off',
+  'no-else-return': 'off',
+  'no-extra-boolean-cast': 'off',
+  'no-import-assign': 'off',
+  'no-lonely-if': 'off',
+  'no-plusplus': 'off',
+  'no-prototype-builtins': 'off',
+  'no-restricted-syntax': 'off',
+  'no-this-before-super': 'off',
+  'operator-assignment': 'off',
+  'prefer-destructuring': 'off',
+  'prefer-object-spread': 'off',
+  'prefer-promise-reject-errors': 'off',
+  'react-hooks/exhaustive-deps': 'off',
+  'react-hooks/rules-of-hooks': 'off',
+  'react/button-has-type': 'off',
+  'react/destructuring-assignment': 'off',
+  'react/forbid-prop-types': 'off',
+  'react/function-component-definition': 'off',
+  'react/jsx-curly-brace-presence': 'off',
+  'react/jsx-filename-extension': 'off',
+  'react/jsx-no-useless-fragment': 'off',
+  'react/jsx-props-no-spreading': 'off',
+  'react/no-danger': 'off',
+  'react/no-deprecated': 'off',
+  'react/require-default-props': 'off',
+};
 
 module.exports = {
-  "extends": [
-    "@wagtail/eslint-config-wagtail",
-    "plugin:@typescript-eslint/recommended"
+  extends: [
+    '@wagtail/eslint-config-wagtail',
+    'plugin:@typescript-eslint/recommended',
   ],
-  "parser": "@typescript-eslint/parser",
-  "plugins": [
-    "@typescript-eslint"
-  ],
-  "env": {
-    "jest": true,
-    "browser": true,
+  parser: '@typescript-eslint/parser',
+  plugins: ['@typescript-eslint'],
+  env: {
+    jest: true,
+    browser: true,
   },
-  "rules": {
-    "no-underscore-dangle": ["error", { "allow": ["__REDUX_DEVTOOLS_EXTENSION__"] }],
+  rules: {
+    'no-underscore-dangle': [
+      'error',
+      { allow: ['__REDUX_DEVTOOLS_EXTENSION__'] },
+    ],
     // note you must disable the base rule as it can report incorrect errors
-    "no-use-before-define": "off",
-    "@typescript-eslint/no-use-before-define": ["error"],
+    'no-use-before-define': 'off',
+    '@typescript-eslint/no-use-before-define': ['error'],
 
-    "@typescript-eslint/explicit-module-boundary-types": "off",
-    "@typescript-eslint/explicit-member-accessibility": "off",
-    "@typescript-eslint/explicit-function-return-type": "off",
-    "@typescript-eslint/no-explicit-any": "off",
-    'react/jsx-filename-extension': [
-      "error",
-      { extensions: ['.js', '.tsx'] },
-    ],
+    '@typescript-eslint/explicit-module-boundary-types': 'off',
+    '@typescript-eslint/explicit-member-accessibility': 'off',
+    '@typescript-eslint/explicit-function-return-type': 'off',
+    '@typescript-eslint/no-explicit-any': 'off',
+    'react/jsx-filename-extension': ['error', { extensions: ['.js', '.tsx'] }],
     'import/extensions': [
-      "error",
+      'error',
       'always',
       {
         ignorePackages: true,
@@ -91,66 +89,66 @@ module.exports = {
     ],
     ...legacyCode,
   },
-  "overrides": [
+  overrides: [
     {
       // Rules we don’t want to enforce for test and tooling code.
-      "files": ["*.test.ts", "*.test.tsx", "*.test.js", "webpack.config.js"],
-      "rules": {
-        "@typescript-eslint/no-empty-function": "off",
-        "@typescript-eslint/no-var-requires": "off"
-      }
+      files: ['*.test.ts', '*.test.tsx', '*.test.js', 'webpack.config.js'],
+      rules: {
+        '@typescript-eslint/no-empty-function': 'off',
+        '@typescript-eslint/no-var-requires': 'off',
+      },
     },
     {
-      "files": ["docs/_static/**"],
-      "globals": { "$": "readonly" }
+      files: ['docs/_static/**'],
+      globals: { $: 'readonly' },
     },
     {
-      "files": ["wagtail/**/**"],
-      "globals": {
-        "$": "readonly",
-        "addMessage": "readonly",
-        "buildExpandingFormset": "readonly",
-        "cancelSpinner": "readonly",
-        "escapeHtml": "readonly",
-        "insertRichTextDeleteControl": "readonly",
-        "jQuery": "readonly",
-        "jsonData": "readonly",
-        "ModalWorkflow": "readonly",
-        "DOCUMENT_CHOOSER_MODAL_ONLOAD_HANDLERS": "writable",
-        "EMBED_CHOOSER_MODAL_ONLOAD_HANDLERS": "writable",
-        "IMAGE_CHOOSER_MODAL_ONLOAD_HANDLERS": "writable",
-        "QUERY_CHOOSER_MODAL_ONLOAD_HANDLERS": "writable",
-        "SNIPPET_CHOOSER_MODAL_ONLOAD_HANDLERS": "writable"
+      files: ['wagtail/**/**'],
+      globals: {
+        $: 'readonly',
+        addMessage: 'readonly',
+        buildExpandingFormset: 'readonly',
+        cancelSpinner: 'readonly',
+        escapeHtml: 'readonly',
+        insertRichTextDeleteControl: 'readonly',
+        jQuery: 'readonly',
+        jsonData: 'readonly',
+        ModalWorkflow: 'readonly',
+        DOCUMENT_CHOOSER_MODAL_ONLOAD_HANDLERS: 'writable',
+        EMBED_CHOOSER_MODAL_ONLOAD_HANDLERS: 'writable',
+        IMAGE_CHOOSER_MODAL_ONLOAD_HANDLERS: 'writable',
+        QUERY_CHOOSER_MODAL_ONLOAD_HANDLERS: 'writable',
+        SNIPPET_CHOOSER_MODAL_ONLOAD_HANDLERS: 'writable',
       },
-      "rules": {
-        "@typescript-eslint/no-unused-vars": "off",
-        "@typescript-eslint/no-use-before-define": "off",
-        "camelcase": [
-          "error",
+      rules: {
+        '@typescript-eslint/no-unused-vars': 'off',
+        '@typescript-eslint/no-use-before-define': 'off',
+        'camelcase': [
+          'error',
           {
-            "allow": [
-              "__unused_webpack_module",
-              "__webpack_modules__",
-              "__webpack_require__"
+            allow: [
+              '__unused_webpack_module',
+              '__webpack_modules__',
+              '__webpack_require__',
             ],
-            "properties": "never"
-          }
+            properties: 'never',
+          },
         ],
-        "consistent-return": "off",
-        "func-names": "off",
-        "id-length": "off",
-        "indent": "off",
-        "key-spacing": "off",
-        "new-cap": "off",
-        "newline-per-chained-call": "off",
-        "no-param-reassign": "off",
-        "no-underscore-dangle": "off",
-        "object-shorthand": "off",
-        "prefer-arrow-callback": "off",
-        "quote-props": "off",
-        "space-before-function-paren": "off",
-        "vars-on-top": "off"
-      }
-    }
-  ]
-}
+        'consistent-return': 'off',
+        'func-names': 'off',
+        'id-length': 'off',
+        'indent': 'off',
+        'key-spacing': 'off',
+        'new-cap': 'off',
+        'newline-per-chained-call': 'off',
+        'no-param-reassign': 'off',
+        'no-underscore-dangle': 'off',
+        'object-shorthand': 'off',
+        'prefer-arrow-callback': 'off',
+        'quote-props': 'off',
+        'space-before-function-paren': 'off',
+        'vars-on-top': 'off',
+      },
+    },
+  ],
+};

+ 10 - 10
.github/workflows/codeql-analysis.yml

@@ -1,4 +1,4 @@
-name: "CodeQL"
+name: 'CodeQL'
 
 on:
   schedule:
@@ -13,16 +13,16 @@ jobs:
       fail-fast: false
       matrix:
         # https://docs.github.com/en/free-pro-team@latest/github/finding-security-vulnerabilities-and-errors-in-your-code/configuring-code-scanning#changing-the-languages-that-are-analyzed
-        language: [ 'javascript', 'python' ]
+        language: ['javascript', 'python']
 
     steps:
-    - name: Checkout repository
-      uses: actions/checkout@v2
+      - name: Checkout repository
+        uses: actions/checkout@v2
 
-    - name: Initialize CodeQL
-      uses: github/codeql-action/init@v1
-      with:
-        languages: ${{ matrix.language }}
+      - name: Initialize CodeQL
+        uses: github/codeql-action/init@v1
+        with:
+          languages: ${{ matrix.language }}
 
-    - name: Perform CodeQL Analysis
-      uses: github/codeql-action/analyze@v1
+      - name: Perform CodeQL Analysis
+        uses: github/codeql-action/analyze@v1

+ 27 - 28
.github/workflows/test.yml

@@ -3,11 +3,10 @@ name: Wagtail CI
 on:
   push:
     paths-ignore:
-    - 'docs/**'
+      - 'docs/**'
   pull_request:
     paths-ignore:
-    - 'docs/**'
-
+      - 'docs/**'
 
 # Our test suite should cover:
 # - all supported databases against current Python and Django
@@ -36,8 +35,8 @@ jobs:
     strategy:
       matrix:
         include:
-          - python: "3.9"
-            django: "Django>=4.0,<4.1"
+          - python: '3.9'
+            django: 'Django>=4.0,<4.1'
 
     steps:
       - uses: actions/checkout@v2
@@ -62,22 +61,22 @@ jobs:
     strategy:
       matrix:
         include:
-          - python: "3.7"
-            django: "Django>=3.2,<3.3"
+          - python: '3.7'
+            django: 'Django>=3.2,<3.3'
             experimental: false
-          - python: "3.10"
-            django: "Django>=4.0,<4.1"
+          - python: '3.10'
+            django: 'Django>=4.0,<4.1'
             notz: notz
             experimental: false
-          - python: "3.10"
-            django: "Django>=4.0,<4.1"
+          - python: '3.10'
+            django: 'Django>=4.0,<4.1'
             experimental: false
             emailuser: emailuser
-          - python: "3.10"
-            django: "git+https://github.com/django/django.git@stable/4.0.x#egg=Django"
+          - python: '3.10'
+            django: 'git+https://github.com/django/django.git@stable/4.0.x#egg=Django'
             experimental: true
-          - python: "3.10"
-            django: "git+https://github.com/django/django.git@main#egg=Django"
+          - python: '3.10'
+            django: 'git+https://github.com/django/django.git@main#egg=Django'
             experimental: true
 
     services:
@@ -116,11 +115,11 @@ jobs:
     strategy:
       matrix:
         include:
-          - python: "3.8"
-            django: "Django>=3.2,<3.3"
+          - python: '3.8'
+            django: 'Django>=3.2,<3.3'
             experimental: false
-          - python: "3.9"
-            django: "Django>=4.0,<4.1"
+          - python: '3.9'
+            django: 'Django>=4.0,<4.1'
             experimental: false
 
     services:
@@ -150,7 +149,7 @@ jobs:
           ./runtests.py
         env:
           DATABASE_ENGINE: django.db.backends.mysql
-          DATABASE_HOST: "127.0.0.1"
+          DATABASE_HOST: '127.0.0.1'
           DATABASE_USER: root
 
   # https://github.com/elastic/elastic-github-actions doesn't work for Elasticsearch 5,
@@ -160,8 +159,8 @@ jobs:
     strategy:
       matrix:
         include:
-          - python: "3.7"
-            django: "Django>=3.2,<3.3"
+          - python: '3.7'
+            django: 'Django>=3.2,<3.3'
     steps:
       - name: Configure sysctl limits
         run: |
@@ -200,8 +199,8 @@ jobs:
     strategy:
       matrix:
         include:
-          - python: "3.9"
-            django: "Django>=4.0,<4.1"
+          - python: '3.9'
+            django: 'Django>=4.0,<4.1'
             emailuser: emailuser
     steps:
       - name: Configure sysctl limits
@@ -244,8 +243,8 @@ jobs:
     strategy:
       matrix:
         include:
-          - python: "3.7"
-            django: "Django>=3.2,<3.3"
+          - python: '3.7'
+            django: 'Django>=3.2,<3.3'
 
     services:
       postgres:
@@ -293,8 +292,8 @@ jobs:
     strategy:
       matrix:
         include:
-          - python: "3.8"
-            django: "Django>=4.0,<4.1"
+          - python: '3.8'
+            django: 'Django>=4.0,<4.1'
             experimental: false
 
     services:

+ 2 - 2
.readthedocs.yml

@@ -1,4 +1,4 @@
 python:
-    version: 3.7
-    pip_install: true
+  version: 3.7
+  pip_install: true
 requirements_file: null

+ 8 - 10
client/.storybook/main.js

@@ -1,9 +1,7 @@
 module.exports = {
-  "stories": [
-    "../src/**/*.stories.@(js|jsx|ts|tsx)"
-  ],
-  "core": {
-    "builder": "webpack5"
+  stories: ['../src/**/*.stories.@(js|jsx|ts|tsx)'],
+  core: {
+    builder: 'webpack5',
   },
   webpackFinal: (config) => {
     config.resolve.fallback.crypto = false;
@@ -12,17 +10,17 @@ module.exports = {
       {
         test: /\.(scss|css)$/,
         use: [
-          "style-loader",
-          "css-loader",
+          'style-loader',
+          'css-loader',
           {
-            loader: "postcss-loader",
+            loader: 'postcss-loader',
             options: {
               postcssOptions: {
-                plugins: ["autoprefixer", "cssnano"],
+                plugins: ['autoprefixer', 'cssnano'],
               },
             },
           },
-          "sass-loader",
+          'sass-loader',
         ],
       },
     ];

+ 2 - 2
client/.storybook/preview.js

@@ -4,11 +4,11 @@ import '../../wagtail/admin/static_src/wagtailadmin/scss/core.scss';
 import '../../wagtail/admin/static_src/wagtailadmin/scss/sidebar.scss';
 
 export const parameters = {
-  actions: { argTypesRegex: "^on[A-Z].*" },
+  actions: { argTypesRegex: '^on[A-Z].*' },
   controls: {
     matchers: {
       color: /(background|color)$/i,
       date: /Date$/,
     },
   },
-}
+};

+ 71 - 72
client/scss/components/_breadcrumb.scss

@@ -1,90 +1,89 @@
 .breadcrumb {
-    @include unlist();
-    @include clearfix();
-    padding-top: 1.4em;
-    font-size: 0.85em;
-    line-height: 1.5em;
-    margin-left: 2em;
-    background: $color-teal;
+  @include unlist();
+  @include clearfix();
+  padding-top: 1.4em;
+  font-size: 0.85em;
+  line-height: 1.5em;
+  margin-left: 2em;
+  background: $color-teal;
 
-    li,
-    .breadcrumb-item {
-        display: block;
-        float: left;
-        position: relative;
-        text-decoration: none;
-        white-space: nowrap;
-        padding: 4px;
+  li,
+  .breadcrumb-item {
+    display: block;
+    float: left;
+    position: relative;
+    text-decoration: none;
+    white-space: nowrap;
+    padding: 4px;
 
-        &:hover {
-            background: $color-teal-dark;
-        }
-
-        &.breadcrumb-dropdown {
-            padding: 0;
-            font-size: initial;
-        }
+    &:hover {
+      background: $color-teal-dark;
+    }
 
-        .t-default .u-btn-current {
-            color: inherit;
-            background: rgba(0, 91, 94, 0.6);
-            font-size: 1.15em;
-            border: 0;
-            line-height: 1.6;
-        }
+    &.breadcrumb-dropdown {
+      padding: 0;
+      font-size: initial;
     }
 
-    li > a,
-    .breadcrumb-link {
-        color: $color-white;
-        display: block;
-        padding: calc(0.5em - 4px) 1em;
-        white-space: nowrap;
-        line-height: 1.6em;
+    .t-default .u-btn-current {
+      color: inherit;
+      background: rgba(0, 91, 94, 0.6);
+      font-size: 1.15em;
+      border: 0;
+      line-height: 1.6;
+    }
+  }
 
-        &:hover {
-            background: $color-teal-dark;
-            color: $color-white;
+  li > a,
+  .breadcrumb-link {
+    color: $color-white;
+    display: block;
+    padding: calc(0.5em - 4px) 1em;
+    white-space: nowrap;
+    line-height: 1.6em;
 
-            .arrow_right_icon {
-                color: $color-teal;
-            }
-        }
+    &:hover {
+      background: $color-teal-dark;
+      color: $color-white;
 
-        .title {
-            display: inline-block;
-            max-width: 11.8em;
-            white-space: nowrap;
-            text-overflow: ellipsis;
-            overflow: hidden;
-            vertical-align: bottom;
-        }
+      .arrow_right_icon {
+        color: $color-teal;
+      }
     }
 
-    .home_icon {
-        @include svg-icon(1em);
-        transform: scale(1.5) translate(0, 0.1em);
+    .title {
+      display: inline-block;
+      max-width: 11.8em;
+      white-space: nowrap;
+      text-overflow: ellipsis;
+      overflow: hidden;
+      vertical-align: bottom;
     }
+  }
 
-    .arrow_right_icon {
-        @include svg-icon(1em);
-        color: $color-teal-dark;
-        transform: scale(1.75) translate(0.3em, 0.1em);
-    }
+  .home_icon {
+    @include svg-icon(1em);
+    transform: scale(1.5) translate(0, 0.1em);
+  }
 
-    @include media-breakpoint-up(sm) {
-        padding-top: 0;
-        background: $color-teal-darker;
-        margin-left: -($desktop-nice-padding);
-        margin-right: -($desktop-nice-padding);
+  .arrow_right_icon {
+    @include svg-icon(1em);
+    color: $color-teal-dark;
+    transform: scale(1.75) translate(0.3em, 0.1em);
+  }
 
-        .home_icon {
-            margin-left: 1.25em;
-        }
+  @include media-breakpoint-up(sm) {
+    padding-top: 0;
+    background: $color-teal-darker;
+    margin-left: -($desktop-nice-padding);
+    margin-right: -($desktop-nice-padding);
 
-        .arrow_right_icon {
-            color: $color-teal;
-        }
+    .home_icon {
+      margin-left: 1.25em;
     }
-}
 
+    .arrow_right_icon {
+      color: $color-teal;
+    }
+  }
+}

+ 98 - 99
client/scss/components/_bulk_actions.scss

@@ -1,109 +1,108 @@
 .bulk-actions-filter-checkbox {
-    .table-headers & {
-        > div {
-            display: flex;
-            align-items: center;
-        }
-
-        .c-dropdown__button {
-            padding-left: 0.3rem;
-        }
-
-        .bulk-actions-choices,
-        .bulk-actions-choices > ul {
-            display: flex;
-            align-items: center;
-        }
-
-        .bulk-actions-choices li {
-            margin: 0 0.5em;
-        }
-
-        .bulk-actions-choices span {
-            text-transform: none;
-        }
+  .table-headers & {
+    > div {
+      display: flex;
+      align-items: center;
+    }
+
+    .c-dropdown__button {
+      padding-left: 0.3rem;
+    }
+
+    .bulk-actions-choices,
+    .bulk-actions-choices > ul {
+      display: flex;
+      align-items: center;
+    }
+
+    .bulk-actions-choices li {
+      margin: 0 0.5em;
     }
-}
 
+    .bulk-actions-choices span {
+      text-transform: none;
+    }
+  }
+}
 
 .bulk-actions-choices {
-    &.footer {
-        @include transition(transform 0.1s ease 0.1s);
-
-        &.hidden {
-            transform: translateY(200px);
-            visibility: hidden;
-        }
-
-        .button {
-            font-weight: 600;
-        }
-
-        .bulk-actions-more {
-            .button {
-                border: 0;
-            }
-
-            .button:not(:hover) {
-                color: $color-teal;
-            }
-
-            .c-dropdown__button {
-                text-transform: uppercase;
-            }
-
-            .c-dropdown__menu {
-                bottom: 50px;
-                flex-direction: column;
-            }
-        }
-
-        .bulk-actions-more.is-open {
-            .c-dropdown__menu.u-toggle {
-                display: flex;
-            }
-        }
+  &.footer {
+    @include transition(transform 0.1s ease 0.1s);
+
+    &.hidden {
+      transform: translateY(200px);
+      visibility: hidden;
     }
 
-    .footer__container {
+    .button {
+      font-weight: 600;
+    }
+
+    .bulk-actions-more {
+      .button {
+        border: 0;
+      }
+
+      .button:not(:hover) {
+        color: $color-teal;
+      }
+
+      .c-dropdown__button {
+        text-transform: uppercase;
+      }
+
+      .c-dropdown__menu {
+        bottom: 50px;
+        flex-direction: column;
+      }
+    }
+
+    .bulk-actions-more.is-open {
+      .c-dropdown__menu.u-toggle {
         display: flex;
-        justify-content: space-around;
-        width: 100%;
-        align-items: center;
-        padding: 1.25em;
-        border-radius: 4px 4px 0 0;
-        margin-left: 30px;
-
-        input[type='checkbox'] {
-            margin-right: 1.25em;
-        }
-
-        .bulk-actions-buttons {
-            border-left: 1px solid $color-grey-2;
-            padding-left: 1.5em;
-
-            .bulk-action-btn {
-                max-width: 160px;
-                overflow-x: hidden;
-                text-overflow: ellipsis;
-            }
-        }
-
-        .num-objects {
-            text-transform: none;
-            margin: 0 5px;
-        }
-
-        .num-objects-in-listing {
-            color: $color-teal-light;
-            background-color: transparent;
-            border: 0;
-            font-family: $font-sans;
-            padding: 0;
-        }
-
-        .button:not(:hover) {
-            background-color: $color-white;
-        }
+      }
+    }
+  }
+
+  .footer__container {
+    display: flex;
+    justify-content: space-around;
+    width: 100%;
+    align-items: center;
+    padding: 1.25em;
+    border-radius: 4px 4px 0 0;
+    margin-left: 30px;
+
+    input[type='checkbox'] {
+      margin-right: 1.25em;
+    }
+
+    .bulk-actions-buttons {
+      border-left: 1px solid $color-grey-2;
+      padding-left: 1.5em;
+
+      .bulk-action-btn {
+        max-width: 160px;
+        overflow-x: hidden;
+        text-overflow: ellipsis;
+      }
+    }
+
+    .num-objects {
+      text-transform: none;
+      margin: 0 5px;
+    }
+
+    .num-objects-in-listing {
+      color: $color-teal-light;
+      background-color: transparent;
+      border: 0;
+      font-family: $font-sans;
+      padding: 0;
+    }
+
+    .button:not(:hover) {
+      background-color: $color-white;
     }
+  }
 }

+ 13 - 13
client/scss/components/_button-select.scss

@@ -1,21 +1,21 @@
 .button-select {
-    &__option {
-        display: block;
-        width: 100%;
-        margin-bottom: 10px;
+  &__option {
+    display: block;
+    width: 100%;
+    margin-bottom: 10px;
 
-        background-color: #fff;
-        border-color: $color-teal;
-        color: #262626;
+    background-color: #fff;
+    border-color: $color-teal;
+    color: #262626;
 
-        &--selected {
-            background-color: $color-teal;
-            color: #fff;
-        }
+    &--selected {
+      background-color: $color-teal;
+      color: #fff;
     }
+  }
 }
 
 .button-select .button-select__option {
-    /* override default margin from horizontally-aligned buttons */
-    margin-left: 0;
+  /* override default margin from horizontally-aligned buttons */
+  margin-left: 0;
 }

+ 509 - 512
client/scss/components/_button.scss

@@ -1,4 +1,4 @@
-@use "sass:color";
+@use 'sass:color';
 // Core button style
 // Note that these styles include methods to render buttons the same x-browser, described here:
 // http: //cbjdigital.com/blog/2010/08/bulletproof_css_input_button_heights
@@ -6,18 +6,196 @@
 // input[type=reset],
 // input[type=button],
 .button {
-    border-radius: 3px;
-    font-family: $font-sans;
+  border-radius: 3px;
+  font-family: $font-sans;
+  width: auto;
+  height: 2.4em;
+  padding: 0 1em;
+  font-size: 0.9em;
+  font-weight: normal;
+  vertical-align: middle;
+  display: inline-block;
+  background-color: $color-button;
+  border: 1px solid $color-button;
+  color: $color-white;
+  text-decoration: none;
+  text-transform: uppercase;
+  white-space: nowrap;
+  position: relative;
+  overflow: hidden;
+  box-sizing: border-box;
+  -webkit-font-smoothing: auto;
+  // stylelint-disable-next-line property-no-vendor-prefix
+  -moz-appearance: none;
+
+  transition: background-color 0.1s ease;
+
+  &:hover {
+    color: $color-teal;
+  }
+
+  &.yes {
+    background-color: $color-button-yes;
+    border: 1px solid $color-button-yes;
+
+    &.button-secondary {
+      border: 1px solid $color-button-yes;
+      color: $color-button-yes;
+      background-color: transparent;
+    }
+
+    &:hover {
+      color: $color-white;
+      border-color: transparent;
+      background-color: $color-button-yes-hover;
+    }
+
+    &.button-nobg:hover {
+      color: $color-button-yes;
+      background-color: transparent;
+    }
+  }
+
+  &.warning {
+    background-color: $color-button-warning;
+    border: 1px solid $color-button-warning;
+
+    &.button-secondary {
+      border: 1px solid $color-button-warning;
+      color: $color-button-warning;
+      background-color: transparent;
+    }
+
+    &:hover {
+      color: $color-white;
+      border-color: transparent;
+      background-color: $color-button-warning-hover;
+    }
+
+    &.button-nobg:hover {
+      color: $color-button-warning;
+      background-color: transparent;
+    }
+  }
+
+  &.no,
+  &.serious {
+    background-color: $color-button-no;
+    border: 1px solid $color-button-no;
+
+    &.button-secondary {
+      border: 1px solid $color-button-no;
+      color: $color-button-no;
+      background-color: transparent;
+    }
+
+    &:hover {
+      color: $color-white;
+      border-color: transparent;
+      background-color: $color-button-no-hover;
+    }
+
+    &.button-nobg:hover {
+      color: $color-button-no;
+      background-color: transparent;
+    }
+  }
+
+  &.button-nobg {
+    border: 0;
+    background-color: transparent;
+  }
+
+  &.bicolor {
+    border: 1px solid transparent;
+    padding-left: 3.5em;
+
+    &:before {
+      // iconfont
+      font-size: 1rem;
+      position: absolute;
+      left: 0;
+      top: 0;
+      width: 2em;
+      line-height: 1.85em;
+      height: 100%;
+      text-align: center;
+      background-color: rgba(0, 0, 0, 0.2);
+      display: block;
+      border-top-left-radius: inherit;
+      border-bottom-left-radius: inherit;
+    }
+
+    .icon-wrapper {
+      background-color: rgba(0, 0, 0, 0.2);
+      display: block;
+      position: absolute;
+      left: 0;
+      top: 0;
+      width: 3em;
+      line-height: 1.85em;
+      height: 100%;
+      text-align: center;
+      border-top-left-radius: inherit;
+      border-bottom-left-radius: inherit;
+    }
+
+    &.button--icon {
+      &:before {
+        display: none; // TODO: remove once the icon font styles are gone
+      }
+
+      .icon {
+        @include svg-icon(1rem);
+        padding: 0.75em;
+      }
+    }
+
+    &.button--icon-flipped {
+      .icon {
+        transform: scaleX(-1);
+      }
+    }
+
+    &.button-secondary {
+      border: 1px solid rgba(0, 0, 0, 0.2);
+    }
+  }
+
+  &.button-small.bicolor {
+    padding-left: 3.5em;
+
+    .icon-wrapper {
+      width: 2em;
+    }
+
+    &.button--icon .icon {
+      @include svg-icon(0.9rem);
+      padding: 0.25em;
+    }
+  }
+
+  // + input[type=submit],
+  // + input[type=reset],
+  // + input[type=button],
+  + .button {
+    // + button {
+    margin-left: 1em;
+  }
+
+  // A completely unstyled button
+  &.unbutton {
+    border-radius: 0;
     width: auto;
-    height: 2.4em;
-    padding: 0 1em;
-    font-size: 0.9em;
+    height: auto;
+    padding: 0;
+    font-size: inherit;
     font-weight: normal;
     vertical-align: middle;
-    display: inline-block;
-    background-color: $color-button;
-    border: 1px solid $color-button;
-    color: $color-white;
+    display: inline;
+    background-color: transparent;
+    border: 0;
+    color: inherit;
     text-decoration: none;
     text-transform: uppercase;
     white-space: nowrap;
@@ -28,620 +206,439 @@
     // stylelint-disable-next-line property-no-vendor-prefix
     -moz-appearance: none;
 
-    transition: background-color 0.1s ease;
-
-    &:hover {
-        color: $color-teal;
+    &:hover,
+    &:focus,
+    &:active {
+      background-color: transparent;
     }
+  }
 
+  // stylelint-disable-next-line no-duplicate-selectors
+  &:hover {
+    background-color: $color-button-hover;
+    color: $color-white;
+    border-color: transparent;
 
-    &.yes {
-        background-color: $color-button-yes;
-        border: 1px solid $color-button-yes;
-
-        &.button-secondary {
-            border: 1px solid $color-button-yes;
-            color: $color-button-yes;
-            background-color: transparent;
-        }
-
-        &:hover {
-            color: $color-white;
-            border-color: transparent;
-            background-color: $color-button-yes-hover;
-        }
+    &.hover-no {
+      background-color: $color-button-no;
+    }
+  }
 
-        &.button-nobg:hover {
-            color: $color-button-yes;
-            background-color: transparent;
-        }
+  &.button-longrunning {
+    span {
+      // iconfont
+      @include transition(all 0.3s ease);
+      transform: scale(0.9);
+      display: inline-block;
+      height: 0.9em;
+      position: relative;
+      opacity: 0;
+      width: 0;
+      visibility: hidden;
+      text-align: center;
+      padding-right: 0;
     }
 
-    &.warning {
-        background-color: $color-button-warning;
-        border: 1px solid $color-button-warning;
+    em {
+      font-style: normal;
+    }
 
-        &.button-secondary {
-            border: 1px solid $color-button-warning;
-            color: $color-button-warning;
-            background-color: transparent;
-        }
+    &.button-longrunning-active span {
+      // iconfont
+      transform: scale(1);
+      visibility: visible;
+      width: 1em;
+      height: 1em;
+      opacity: 0.8;
+      padding-right: 0.5em;
+    }
 
-        &:hover {
-            color: $color-white;
-            border-color: transparent;
-            background-color: $color-button-warning-hover;
-        }
+    span.icon-spinner:after {
+      // iconfont
+      text-align: center;
+      position: absolute;
+      left: 0;
+      margin: 0;
+      line-height: 1em;
+      display: inline-block;
+      font-size: 1em;
+    }
 
-        &.button-nobg:hover {
-            color: $color-button-warning;
-            background-color: transparent;
-        }
+    svg.icon-spinner {
+      @include transition(all 0.3s ease);
+      display: none;
     }
 
+    &.button-longrunning-active svg.icon-spinner {
+      @include svg-icon();
 
-    &.no,
-    &.serious {
-        background-color: $color-button-no;
-        border: 1px solid $color-button-no;
+      transform: scale(1);
+      display: inline-block;
+      opacity: 0.8;
+      padding: 0;
+      margin-right: 0.5em;
+    }
 
-        &.button-secondary {
-            border: 1px solid $color-button-no;
-            color: $color-button-no;
-            background-color: transparent;
-        }
+    &.button-longrunning-active .button-longrunning__icon {
+      display: none;
+    }
+  }
 
-        &:hover {
-            color: $color-white;
-            border-color: transparent;
-            background-color: $color-button-no-hover;
-        }
+  &:disabled,
+  &[disabled],
+  &.disabled {
+    background-color: $color-grey-3;
+    border-color: $color-grey-3;
+    color: $color-grey-2;
+    cursor: default;
+  }
+
+  &.button-secondary:disabled,
+  &.button-secondary[disabled],
+  &.button-secondary.disabled {
+    background-color: $color-white;
+    border-color: $color-grey-3;
+    color: $color-grey-3;
+  }
+
+  &.button-nostroke {
+    border: 0;
+  }
 
-        &.button-nobg:hover {
-            color: $color-button-no;
-            background-color: transparent;
-        }
-    }
+  &.button-strokeonhover {
+    border: 1px solid transparent;
 
-    &.button-nobg {
-        border: 0;
-        background-color: transparent;
+    &:hover {
+      border-color: $color-grey-2;
     }
 
-    &.bicolor {
-        border: 1px solid transparent;
-        padding-left: 3.5em;
-
-        &:before {  // iconfont
-            font-size: 1rem;
-            position: absolute;
-            left: 0;
-            top: 0;
-            width: 2em;
-            line-height: 1.85em;
-            height: 100%;
-            text-align: center;
-            background-color: rgba(0, 0, 0, 0.2);
-            display: block;
-            border-top-left-radius: inherit;
-            border-bottom-left-radius: inherit;
-        }
-
-        .icon-wrapper {
-            background-color: rgba(0, 0, 0, 0.2);
-            display: block;
-            position: absolute;
-            left: 0;
-            top: 0;
-            width: 3em;
-            line-height: 1.85em;
-            height: 100%;
-            text-align: center;
-            border-top-left-radius: inherit;
-            border-bottom-left-radius: inherit;
-        }
-
-        &.button--icon {
-            &:before {
-                display: none;  // TODO: remove once the icon font styles are gone
-            }
-
-            .icon {
-                @include svg-icon(1rem);
-                padding: 0.75em;
-            }
-        }
-
-        &.button--icon-flipped {
-            .icon {
-                transform: scaleX(-1);
-            }
-        }
-
-        &.button-secondary {
-            border: 1px solid rgba(0, 0, 0, 0.2);
-        }
+    &.button-nobg:hover {
+      background-color: transparent;
     }
+  }
 
-    &.button-small.bicolor {
-        padding-left: 3.5em;
-
-        .icon-wrapper {
-            width: 2em;
-        }
-
-        &.button--icon .icon {
-            @include svg-icon(0.9rem);
-            padding: 0.25em;
-        }
-    }
-
-
-    // + input[type=submit],
-    // + input[type=reset],
-    // + input[type=button],
-    + .button {
-        // + button {
-        margin-left: 1em;
-    }
-
-    // A completely unstyled button
-    &.unbutton {
-        border-radius: 0;
-        width: auto;
-        height: auto;
-        padding: 0;
-        font-size: inherit;
-        font-weight: normal;
-        vertical-align: middle;
-        display: inline;
-        background-color: transparent;
-        border: 0;
-        color: inherit;
-        text-decoration: none;
-        text-transform: uppercase;
-        white-space: nowrap;
-        position: relative;
-        overflow: hidden;
-        box-sizing: border-box;
-        -webkit-font-smoothing: auto;
-        // stylelint-disable-next-line property-no-vendor-prefix
-        -moz-appearance: none;
-
-        &:hover,
-        &:focus,
-        &:active {
-            background-color: transparent;
-        }
-    }
-
-    // stylelint-disable-next-line no-duplicate-selectors
-    &:hover {
-        background-color: $color-button-hover;
-        color: $color-white;
-        border-color: transparent;
-
-        &.hover-no {
-            background-color: $color-button-no;
-        }
-    }
-
-    &.button-longrunning {
-        span {  // iconfont
-            @include transition(all 0.3s ease);
-            transform: scale(0.9);
-            display: inline-block;
-            height: 0.9em;
-            position: relative;
-            opacity: 0;
-            width: 0;
-            visibility: hidden;
-            text-align: center;
-            padding-right: 0;
-        }
-
-        em {
-            font-style: normal;
-        }
-
-        &.button-longrunning-active span { // iconfont
-            transform: scale(1);
-            visibility: visible;
-            width: 1em;
-            height: 1em;
-            opacity: 0.8;
-            padding-right: 0.5em;
-        }
-
-        span.icon-spinner:after { // iconfont
-            text-align: center;
-            position: absolute;
-            left: 0;
-            margin: 0;
-            line-height: 1em;
-            display: inline-block;
-            font-size: 1em;
-        }
-
-        svg.icon-spinner {
-            @include transition(all 0.3s ease);
-            display: none;
-        }
-
-        &.button-longrunning-active svg.icon-spinner {
-            @include svg-icon();
-
-            transform: scale(1);
-            display: inline-block;
-            opacity: 0.8;
-            padding: 0;
-            margin-right: 0.5em;
-        }
-
-        &.button-longrunning-active .button-longrunning__icon {
-            display: none;
-        }
-    }
-
-    &:disabled,
-    &[disabled],
-    &.disabled {
-        background-color: $color-grey-3;
-        border-color: $color-grey-3;
-        color: $color-grey-2;
-        cursor: default;
-    }
-
-    &.button-secondary:disabled,
-    &.button-secondary[disabled],
-    &.button-secondary.disabled {
-        background-color: $color-white;
-        border-color: $color-grey-3;
-        color: $color-grey-3;
-    }
-
-    &.button-nostroke {
-        border: 0;
-    }
-
-    &.button-strokeonhover {
-        border: 1px solid transparent;
-
-        &:hover {
-            border-color: $color-grey-2;
-        }
-
-        &.button-nobg:hover {
-            background-color: transparent;
-        }
-    }
-
-    &.text-notransform {
-        text-transform: initial;
-    }
+  &.text-notransform {
+    text-transform: initial;
+  }
 
-    &.button--icon {
-        .icon {
-            @include svg-icon(1.5em);
-        }
+  &.button--icon {
+    .icon {
+      @include svg-icon(1.5em);
     }
+  }
 
-    @include media-breakpoint-up(sm) {
-        font-size: 0.95em;
-        padding: 0 1.4em;
-        height: 3em;
+  @include media-breakpoint-up(sm) {
+    font-size: 0.95em;
+    padding: 0 1.4em;
+    height: 3em;
 
-        &.bicolor {
-            padding-left: 3.7em;
+    &.bicolor {
+      padding-left: 3.7em;
 
-            &:before {
-                width: 2em;
-                line-height: 2.2em;
-                font-size: 1.1rem;
-            }
-        }
+      &:before {
+        width: 2em;
+        line-height: 2.2em;
+        font-size: 1.1rem;
+      }
+    }
 
-        &.button-small.bicolor {
-            // line-height: 2.2em;
-            padding-left: 3em;
+    &.button-small.bicolor {
+      // line-height: 2.2em;
+      padding-left: 3em;
 
-            &:before {
-                width: 1.8em;
-                line-height: 1.65em;
-            }
-        }
+      &:before {
+        width: 1.8em;
+        line-height: 1.65em;
+      }
     }
+  }
 }
 
-
 .button-small {
-    padding: 0 0.8em;
-    height: 2em;
-    font-size: 0.95em;
+  padding: 0 0.8em;
+  height: 2em;
+  font-size: 0.95em;
 }
 
 .button-secondary {
-    color: $color-button;
-    background-color: transparent;
+  color: $color-button;
+  background-color: transparent;
 }
 
 // Buttons which are only an icon
-.button.icon.text-replace {  // iconfont
-    font-size: 0; // unavoidable duplication of setting in icons.scss
-    width: 1.8rem;
-    height: 1.8rem;
-
-    &:before {
-        line-height: 1.7em;
-    }
+.button.icon.text-replace {
+  // iconfont
+  font-size: 0; // unavoidable duplication of setting in icons.scss
+  width: 1.8rem;
+  height: 1.8rem;
 
-    @include media-breakpoint-up(sm) {
-        width: 2.2rem;
-        height: 2.2rem;
+  &:before {
+    line-height: 1.7em;
+  }
 
-        &:before {
-            line-height: 2.1em;
-        }
+  @include media-breakpoint-up(sm) {
+    width: 2.2rem;
+    height: 2.2rem;
 
+    &:before {
+      line-height: 2.1em;
+    }
 
-        &.button-small {
-            height: 1.8rem;
-            width: 1.8rem;
+    &.button-small {
+      height: 1.8rem;
+      width: 1.8rem;
 
-            // stylelint-disable-next-line max-nesting-depth
-            &:before {
-                line-height: 1.7em;
-            }
-        }
+      // stylelint-disable-next-line max-nesting-depth
+      &:before {
+        line-height: 1.7em;
+      }
     }
+  }
 }
 
 .button--icon.text-replace {
-    font-size: 0;
-    text-align: center;
+  font-size: 0;
+  text-align: center;
 
-    .icon {
-        font-size: initial;
-        @include svg-icon(1rem, middle);
-        padding: 0.5em;
-    }
+  .icon {
+    font-size: initial;
+    @include svg-icon(1rem, middle);
+    padding: 0.5em;
+  }
 
-    &.button-small {
-        line-height: 1.7rem;
-        height: 1.8rem;
-        width: 1.8rem;
+  &.button-small {
+    line-height: 1.7rem;
+    height: 1.8rem;
+    width: 1.8rem;
 
-        .icon {
-            padding: 0.25em;
-        }
+    .icon {
+      padding: 0.25em;
     }
+  }
 
-    @include media-breakpoint-up(sm) {
-        width: 2.2rem;
-        height: 2.2rem;
-    }
+  @include media-breakpoint-up(sm) {
+    width: 2.2rem;
+    height: 2.2rem;
+  }
 }
 
 button.button.bicolor .icon-wrapper {
-    line-height: 1.65em;  // work around differences in a and button elements
+  line-height: 1.65em; // work around differences in a and button elements
 }
 
 .button-neutral {
-    color: $color-grey-2;
+  color: $color-grey-2;
 
-    &:hover {
-        color: $color-teal;
-    }
+  &:hover {
+    color: $color-teal;
+  }
 }
 
 .yes {
-    background-color: $color-button-yes;
-    border: 1px solid $color-button-yes;
+  background-color: $color-button-yes;
+  border: 1px solid $color-button-yes;
 
-    &.button-secondary {
-        border: 1px solid $color-button-yes;
-        color: $color-button-yes;
-        background-color: transparent;
-    }
+  &.button-secondary {
+    border: 1px solid $color-button-yes;
+    color: $color-button-yes;
+    background-color: transparent;
+  }
 
-    &:hover {
-        color: $color-white;
-        border-color: transparent;
-        background-color: $color-button-yes-hover;
-    }
+  &:hover {
+    color: $color-white;
+    border-color: transparent;
+    background-color: $color-button-yes-hover;
+  }
 
-    &.button-nobg:hover {
-        color: $color-button-yes;
-        background-color: transparent;
-    }
+  &.button-nobg:hover {
+    color: $color-button-yes;
+    background-color: transparent;
+  }
 }
 
 .no,
 .serious {
-    background-color: $color-button-no;
-    border: 1px solid $color-button-no;
+  background-color: $color-button-no;
+  border: 1px solid $color-button-no;
 
-    &.button-secondary {
-        border: 1px solid $color-button-no;
-        color: $color-button-no;
-        background-color: transparent;
-    }
+  &.button-secondary {
+    border: 1px solid $color-button-no;
+    color: $color-button-no;
+    background-color: transparent;
+  }
 
-    &:hover {
-        color: $color-white;
-        border-color: transparent;
-        background-color: $color-button-no-hover;
-    }
+  &:hover {
+    color: $color-white;
+    border-color: transparent;
+    background-color: $color-button-no-hover;
+  }
 
-    &.button-nobg:hover {
-        color: $color-button-no;
-        background-color: transparent;
-    }
+  &.button-nobg:hover {
+    color: $color-button-no;
+    background-color: transparent;
+  }
 }
 
 .button-nobg {
-    border: 0;
-    background-color: transparent;
+  border: 0;
+  background-color: transparent;
 }
 
 .bicolor {
-    border: 0;
-    padding-left: 3.5em;
-
-    &:before {
-        font-size: 1rem;
-        position: absolute;
-        left: 0;
-        top: 0;
-        width: 2em;
-        line-height: 1.85em;
-        height: 100%;
-        text-align: center;
-        background-color: rgba(0, 0, 0, 0.2);
-        display: block;
-    }
+  border: 0;
+  padding-left: 3.5em;
+
+  &:before {
+    font-size: 1rem;
+    position: absolute;
+    left: 0;
+    top: 0;
+    width: 2em;
+    line-height: 1.85em;
+    height: 100%;
+    text-align: center;
+    background-color: rgba(0, 0, 0, 0.2);
+    display: block;
+  }
 }
 
 .button-small.bicolor {
-    padding-left: 3.5em;
+  padding-left: 3.5em;
 
-    &:before {
-        width: 2em;
-        font-size: 0.8rem;
-        line-height: 1.65em;
-    }
+  &:before {
+    width: 2em;
+    font-size: 0.8rem;
+    line-height: 1.65em;
+  }
 }
 
-
 a.button {
-    line-height: 2.4em;
-    height: auto;
+  line-height: 2.4em;
+  height: auto;
 
-    &.button-small {
-        line-height: 1.85em;
-    }
+  &.button-small {
+    line-height: 1.85em;
+  }
 
-    @include media-breakpoint-up(sm) {
-        line-height: 2.9em;
-    }
+  @include media-breakpoint-up(sm) {
+    line-height: 2.9em;
+  }
 }
 
 // Special styles to counteract Firefox's completely unwarranted assumptions about button styles
-input[type=submit],
-input[type=reset],
-input[type=button],
+input[type='submit'],
+input[type='reset'],
+input[type='button'],
 button {
-    padding: 0 1em;
+  padding: 0 1em;
 
-    @include media-breakpoint-up(sm) {
-        &.button-small {
-            height: 2em;
-        }
+  @include media-breakpoint-up(sm) {
+    &.button-small {
+      height: 2em;
     }
+  }
 }
 
 .button-group {
-    @include clearfix;
+  @include clearfix;
 
-    input[type=submit],
-    input[type=reset],
-    input[type=button],
-    .button,
-    button {
-        border-radius: 0;
-        float: left;
-        margin-right: 1px;
-        margin-left: 0;
-
-        &:only-child {
-            border-radius: 3px;
-        }
-
-        &:first-child {
-            border-radius: 3px 0 0 3px;
-        }
-
-        &:last-child {
-            border-radius: 0 3px 3px 0;
-            margin-right: 0;
-        }
-    }
-
-    &.button-group-square {
-        &,
-        input[type=submit],
-        input[type=reset],
-        input[type=button],
-        .button,
-        button {
-            border-radius: 0;
-        }
-    }
-}
-
-
-.multiple {
-    padding: 0;
-    max-width: 1024px - 50px;
-    overflow: hidden;
+  input[type='submit'],
+  input[type='reset'],
+  input[type='button'],
+  .button,
+  button {
+    border-radius: 0;
+    float: left;
+    margin-right: 1px;
+    margin-left: 0;
 
-    > li {
-        @include row();
-        border-radius: 2px;
-        position: relative;
-        overflow: hidden;
-        background-color: $color-white;
-        padding: 1em 10em 1em 1.5em; // 10em padding leaves room for controls
-        margin-bottom: 1em;
-        border: 1px solid color.adjust($color-grey-4, $lightness: 3%); // really trying to avoid creating more greys, but this one is better than grey 4 or 5
+    &:only-child {
+      border-radius: 3px;
     }
 
-    &.moving {
-        position: relative;
+    &:first-child {
+      border-radius: 3px 0 0 3px;
     }
 
-    li.moving {
-        position: absolute;
-        width: 100%;
+    &:last-child {
+      border-radius: 0 3px 3px 0;
+      margin-right: 0;
     }
+  }
 
-    fieldset {
-        padding-top: 0;
-        padding-bottom: 0;
+  &.button-group-square {
+    &,
+    input[type='submit'],
+    input[type='reset'],
+    input[type='button'],
+    .button,
+    button {
+      border-radius: 0;
     }
+  }
+}
 
-    // Object controls
-    .controls {
-        position: absolute;
-        z-index: 1;
-        right: 1em;
-        top: 1em;
-        color: $color-white;
+.multiple {
+  padding: 0;
+  max-width: 1024px - 50px;
+  overflow: hidden;
 
-        li {
-            float: left;
-            margin-right: 1px;
+  > li {
+    @include row();
+    border-radius: 2px;
+    position: relative;
+    overflow: hidden;
+    background-color: $color-white;
+    padding: 1em 10em 1em 1.5em; // 10em padding leaves room for controls
+    margin-bottom: 1em;
+    border: 1px solid color.adjust($color-grey-4, $lightness: 3%); // really trying to avoid creating more greys, but this one is better than grey 4 or 5
+  }
 
-            &:last-child {
-                margin-right: 0;
-            }
-        }
+  &.moving {
+    position: relative;
+  }
+
+  li.moving {
+    position: absolute;
+    width: 100%;
+  }
+
+  fieldset {
+    padding-top: 0;
+    padding-bottom: 0;
+  }
+
+  // Object controls
+  .controls {
+    position: absolute;
+    z-index: 1;
+    right: 1em;
+    top: 1em;
+    color: $color-white;
 
-        .disabled {
-            display: none;
-            visibility: hidden;
-        }
+    li {
+      float: left;
+      margin-right: 1px;
+
+      &:last-child {
+        margin-right: 0;
+      }
     }
 
+    .disabled {
+      display: none;
+      visibility: hidden;
+    }
+  }
 }
 
 // wrapper around add button for multiple objects
 .add {
-    font-weight: 700;
-    cursor: pointer;
-    margin-top: 0;
-    margin-bottom: 0;
-    padding-top: 1em;
-    padding-bottom: 2em;
-    clear: both;
+  font-weight: 700;
+  cursor: pointer;
+  margin-top: 0;
+  margin-bottom: 0;
+  padding-top: 1em;
+  padding-bottom: 2em;
+  clear: both;
 }

+ 93 - 89
client/scss/components/_chooser.scss

@@ -9,84 +9,88 @@ overridden here? hmm.
 */
 
 .chooser {
-    // We show the 'chosen' state...
-    @include clearfix();
-
-    input[type=text] {
-        float: left;
-        width: 50%;
-        margin-right: 1em;
+  // We show the 'chosen' state...
+  @include clearfix();
+
+  input[type='text'] {
+    float: left;
+    width: 50%;
+    margin-right: 1em;
+  }
+
+  .chosen {
+    display: block;
+  }
+
+  .unchosen,
+  .chosen {
+    position: relative;
+
+    .icon {
+      color: $color-grey-3;
+      @include svg-icon(2.5em);
+      vertical-align: middle;
+      margin-right: 0.625rem;
     }
 
-    .chosen {
-        display: block;
-    }
-
-    .unchosen,
-    .chosen {
-        position: relative;
-
-        .icon {
-            color: $color-grey-3;
-            @include svg-icon(2.5em);
-            vertical-align: middle;
-            margin-right: 0.625rem;
-        }
-
-        // TODO: [icon-font] remove when the Wagtail icon font is removed
-        &:before {
-            vertical-align: middle;
-            font-family: $font-wagtail-icons;
-            content: '';
-            // position: relative
-            display: inline-block;
-            // float: left;
-            color: $color-grey-3;
-            line-height: 1em;
-            font-size: 2.5em;
-            margin-right: 0.3em;
-        }
+    // TODO: [icon-font] remove when the Wagtail icon font is removed
+    &:before {
+      vertical-align: middle;
+      font-family: $font-wagtail-icons;
+      content: '';
+      // position: relative
+      display: inline-block;
+      // float: left;
+      color: $color-grey-3;
+      line-height: 1em;
+      font-size: 2.5em;
+      margin-right: 0.3em;
     }
+  }
 
-    .unchosen {
-        display: none;
-    }
+  .unchosen {
+    display: none;
+  }
 
-    .actions {
-        @include clearfix;
-        overflow: hidden;
+  .actions {
+    @include clearfix;
+    overflow: hidden;
 
-        li {
-            float: left;
-            margin: 0.3em;
-        }
+    li {
+      float: left;
+      margin: 0.3em;
     }
+  }
 
-    // ...unless the .page-chooser has the 'blank' class set
-    &.blank {
-        .chosen { display: none; }
+  // ...unless the .page-chooser has the 'blank' class set
+  &.blank {
+    .chosen {
+      display: none;
+    }
 
-        .unchosen { display: block; }
+    .unchosen {
+      display: block;
     }
+  }
 }
 
 // standard way of doing a chooser where the chosen object's title is overlaid
 .page-chooser,
 .snippet-chooser,
 .document-chooser {
-    .chosen {
-        .title {
-            color: $color-grey-1;
-            // display: block;
-            padding-left: 1em;
-            display: inline-block;
-        }
-
-        .actions {
-            clear: both;
-            padding-top: 0.6em;
-        }
+  .chosen {
+    .title {
+      color: $color-grey-1;
+      // display: block;
+      padding-left: 1em;
+      display: inline-block;
+    }
+
+    .actions {
+      clear: both;
+      padding-top: 0.6em;
     }
+  }
 }
 
 // TODO: [icon-font] remove when the Wagtail icon font is removed
@@ -94,37 +98,37 @@ overridden here? hmm.
 .snippet-chooser,
 .document-chooser,
 .image-chooser {
-    .unchosen,
-    .chosen {
-        &:before {
-            display: none;
-        }
+  .unchosen,
+  .chosen {
+    &:before {
+      display: none;
     }
+  }
 }
 
 .image-chooser {
-    .chosen {
-        padding-left: $thumbnail-width;
-
-        &:before {
-            display: inline-block;
-        }
-
-        .preview-image {
-            float: left;
-            margin-left: -($thumbnail-width);
-            margin-right: 1em;
-            max-width: $thumbnail-width;
-
-            // Resize standard Wagtail thumbnail size (165x165) to 130 for space-saving purposes.
-            // We could request a 130x130 rendition, but that's just unnecessary and burdens installations
-            // where images are store off-site with higher rendering times.
-            img {
-                max-width: $thumbnail-width;
-                max-height: $thumbnail-width;
-                height: auto;
-                width: auto;
-            }
-        }
+  .chosen {
+    padding-left: $thumbnail-width;
+
+    &:before {
+      display: inline-block;
+    }
+
+    .preview-image {
+      float: left;
+      margin-left: -($thumbnail-width);
+      margin-right: 1em;
+      max-width: $thumbnail-width;
+
+      // Resize standard Wagtail thumbnail size (165x165) to 130 for space-saving purposes.
+      // We could request a 130x130 rendition, but that's just unnecessary and burdens installations
+      // where images are store off-site with higher rendering times.
+      img {
+        max-width: $thumbnail-width;
+        max-height: $thumbnail-width;
+        height: auto;
+        width: auto;
+      }
     }
+  }
 }

+ 91 - 91
client/scss/components/_comments-controls.scss

@@ -1,113 +1,113 @@
 .comments-controls {
-    position: relative;
-    display: flex;
-    justify-content: flex-end;
-    padding-right: 2px;
-    height: 42px;
-    box-sizing: border-box;
+  position: relative;
+  display: flex;
+  justify-content: flex-end;
+  padding-right: 2px;
+  height: 42px;
+  box-sizing: border-box;
 
-    @include media-breakpoint-up(sm) {
-        padding-right: 30px;
-        height: 43px;
-    }
+  @include media-breakpoint-up(sm) {
+    padding-right: 30px;
+    height: 43px;
+  }
 }
 
 .comments-toggle {
-    $root: &;
-    float: none;
-    position: relative;
-    cursor: pointer;
-    display: flex;
-    width: auto;
-    align-items: center;
-
-    // Wagtail adds some top padding to labels on mobile
-    padding: 0;
+  $root: &;
+  float: none;
+  position: relative;
+  cursor: pointer;
+  display: flex;
+  width: auto;
+  align-items: center;
 
-    &--active,
-    &:hover {
-        #{$root}__label {
-            opacity: 1;
-        }
+  // Wagtail adds some top padding to labels on mobile
+  padding: 0;
 
-        #{$root}__icon {
-            color: $color-white;
-        }
+  &--active,
+  &:hover {
+    #{$root}__label {
+      opacity: 1;
     }
 
-    &__icon {
-        position: absolute;
-        left: -12px;
-        bottom: 3px;
-        width: 52px;
-        height: 52px;
-        color: $color-teal-dark;
-        transition: color 100ms cubic-bezier(0.4, 0, 0.2, 1);
+    #{$root}__icon {
+      color: $color-white;
     }
+  }
 
-    &__count {
-        position: absolute;
-        top: -1px;
-        right: -8px;
-        width: 19px;
-        height: 19px;
-        box-sizing: border-box;
-        border-radius: 50%;
-        background-color: $color-salmon;
-        border: 1px solid $color-teal;
-        color: $color-white;
-        font-size: 9px;
-        font-weight: 700;
-        text-align: center;
-        line-height: 17px;
+  &__icon {
+    position: absolute;
+    left: -12px;
+    bottom: 3px;
+    width: 52px;
+    height: 52px;
+    color: $color-teal-dark;
+    transition: color 100ms cubic-bezier(0.4, 0, 0.2, 1);
+  }
 
-        &:empty {
-            display: none;
-        }
-    }
+  &__count {
+    position: absolute;
+    top: -1px;
+    right: -8px;
+    width: 19px;
+    height: 19px;
+    box-sizing: border-box;
+    border-radius: 50%;
+    background-color: $color-salmon;
+    border: 1px solid $color-teal;
+    color: $color-white;
+    font-size: 9px;
+    font-weight: 700;
+    text-align: center;
+    line-height: 17px;
 
-    &__icon-wrapper {
-        width: 28px;
-        height: 52px;
-        position: relative;
+    &:empty {
+      display: none;
     }
+  }
 
-    &__label {
-        font-size: 12px;
-        text-transform: uppercase;
-        color: $color-white;
-        font-weight: 400;
-        margin-right: 10px;
-        opacity: 0;
-        pointer-events: none;
-        transition: opacity 100ms cubic-bezier(0.4, 0, 0.2, 1);
-    }
+  &__icon-wrapper {
+    width: 28px;
+    height: 52px;
+    position: relative;
+  }
 
-    [type=checkbox] {
-        position: absolute;
-        opacity: 0;
-        pointer-events: none;
-        top: -20px;
-    }
+  &__label {
+    font-size: 12px;
+    text-transform: uppercase;
+    color: $color-white;
+    font-weight: 400;
+    margin-right: 10px;
+    opacity: 0;
+    pointer-events: none;
+    transition: opacity 100ms cubic-bezier(0.4, 0, 0.2, 1);
+  }
 
-    [type=checkbox]:checked + &__icon {
-        opacity: 1;
-    }
+  [type='checkbox'] {
+    position: absolute;
+    opacity: 0;
+    pointer-events: none;
+    top: -20px;
+  }
+
+  [type='checkbox']:checked + &__icon {
+    opacity: 1;
+  }
 }
 
 .comment-notifications-toggle {
-    label {
-        padding: 0;
-        margin: 0;
-        padding-top: 5px;
-        font-weight: normal;
-        font-size: 13px;
-        color: $color-white;
-        display: flex;
-        justify-content: space-between;
-    }
+  label {
+    padding: 0;
+    margin: 0;
+    padding-top: 5px;
+    font-weight: normal;
+    font-size: 13px;
+    color: $color-white;
+    display: flex;
+    justify-content: space-between;
+  }
 
-    .switch__toggle {
-        margin-left: 15px;
-    }
+  .switch__toggle {
+    margin-left: 15px;
+  }
 }

+ 53 - 53
client/scss/components/_comments-notification-dropdown.scss

@@ -1,66 +1,66 @@
 .comment-notifications-toggle-button {
-    $root: &;
-    padding: 0 17px;
-    margin: 0;
-    display: flex;
-    align-items: center;
-    border: 0;
-    background-color: transparent;
+  $root: &;
+  padding: 0 17px;
+  margin: 0;
+  display: flex;
+  align-items: center;
+  border: 0;
+  background-color: transparent;
 
-    &--active,
-    &:hover {
-        #{$root}__icon {
-            color: $color-white;
-        }
+  &--active,
+  &:hover {
+    #{$root}__icon {
+      color: $color-white;
     }
+  }
 
-    &--icon-toggle {
-        #{$root}__icon {
-            transform: rotate(180deg) translate3d(3px, 10px, 0);
-        }
+  &--icon-toggle {
+    #{$root}__icon {
+      transform: rotate(180deg) translate3d(3px, 10px, 0);
     }
+  }
 
-    &__icon {
-        width: 15px;
-        height: 18px;
-        color: $color-teal-dark;
-        transition: color 100ms cubic-bezier(0.4, 0, 0.2, 1);
-    }
+  &__icon {
+    width: 15px;
+    height: 18px;
+    color: $color-teal-dark;
+    transition: color 100ms cubic-bezier(0.4, 0, 0.2, 1);
+  }
 }
 
 .comment-notifications-dropdown {
-    position: absolute;
-    display: none;
-    bottom: -92px;
-    z-index: 51;
-    background-color: $color-text-base;
-    padding: 20px;
-    border-radius: 6px;
-    min-width: 260px;
-    box-sizing: border-box;
-    border: 1px solid $color-text-base;
+  position: absolute;
+  display: none;
+  bottom: -92px;
+  z-index: 51;
+  background-color: $color-text-base;
+  padding: 20px;
+  border-radius: 6px;
+  min-width: 260px;
+  box-sizing: border-box;
+  border: 1px solid $color-text-base;
 
-    &__title {
-        font-size: 12px;
-        text-transform: uppercase;
-        font-weight: 700;
-        color: $color-white;
-    }
+  &__title {
+    font-size: 12px;
+    text-transform: uppercase;
+    font-weight: 700;
+    color: $color-white;
+  }
 
-    &--active {
-        display: block;
-    }
+  &--active {
+    display: block;
+  }
 
-    &::before {
-        content: '';
-        position: absolute;
-        top: -8px;
-        width: 0;
-        height: 0;
-        z-index: 2;
-        right: 18px;
-        border-style: solid;
-        border-width: 0 8px 8px 8px;
-        border-color: transparent transparent $color-text-base transparent;
-    }
+  &::before {
+    content: '';
+    position: absolute;
+    top: -8px;
+    width: 0;
+    height: 0;
+    z-index: 2;
+    right: 18px;
+    border-style: solid;
+    border-width: 0 8px 8px 8px;
+    border-color: transparent transparent $color-text-base transparent;
+  }
 }

+ 280 - 279
client/scss/components/_dropdown.legacy.scss

@@ -1,342 +1,343 @@
 .dropdown {
-    @include clearfix();
-    position: relative;
-
-    input[type=submit],
-    input[type=reset],
-    input[type=button],
-    button,
-    .button {
-        padding: 0 5em 0 1em;
-        display: block;
-        width: 100%;
-        height: 3em;
-        line-height: 3em;
-        text-align: left;
-        float: left;
+  @include clearfix();
+  position: relative;
+
+  input[type='submit'],
+  input[type='reset'],
+  input[type='button'],
+  button,
+  .button {
+    padding: 0 5em 0 1em;
+    display: block;
+    width: 100%;
+    height: 3em;
+    line-height: 3em;
+    text-align: left;
+    float: left;
+  }
+
+  .action-secondary {
+    opacity: 0.8;
+  }
+
+  input[type='submit'],
+  input[type='reset'],
+  input[type='button'],
+  button {
+    line-height: inherit;
+  }
+
+  ul {
+    @include unlist();
+    background-color: $color-teal;
+    position: absolute;
+    overflow: hidden;
+    top: 100%;
+    left: -2000px;
+    z-index: 500;
+    opacity: 0;
+
+    li {
+      float: none;
+      border-color: rgba(255, 255, 255, 0.2);
+      border-style: solid;
+      border-width: 1px 0 0;
+      overflow: hidden;
+
+      a:focus,
+      button:focus {
+        border: 3px solid $color-focus-outline;
+      }
     }
 
-    .action-secondary {
-        opacity: 0.8;
-    }
+    // Media for Windows High Contrast
+    @media (forced-colors: $media-forced-colours) {
+      li {
+        border-width: 1px;
+      }
+
+      li:hover {
+        border-color: Highlight;
+      }
+
+      li a,
+      li button {
+        forced-color-adjust: none;
+        background-color: $color-black;
+        border-color: $color-white;
+        color: $color-white;
+      }
 
-    input[type=submit],
-    input[type=reset],
-    input[type=button],
-    button {
-        line-height: inherit;
+      li a:focus,
+      li button:focus {
+        background-color: $color-black;
+        forced-color-adjust: none;
+        border: 4px solid #0ff;
+        color: $color-white;
+      }
     }
 
-    ul {
-        @include unlist();
-        background-color: $color-teal;
-        position: absolute;
-        overflow: hidden;
-        top: 100%;
-        left: -2000px;
-        z-index: 500;
-        opacity: 0;
-
-        li {
-            float: none;
-            border-color: rgba(255, 255, 255, 0.2);
-            border-style: solid;
-            border-width: 1px 0 0;
-            overflow: hidden;
-
-            a:focus,
-            button:focus {
-                border: 3px solid $color-focus-outline;
-            }
-        }
-
-        // Media for Windows High Contrast
-        @media (forced-colors: $media-forced-colours) {
-            li {
-                border-width: 1px;
-            }
-
-            li:hover {
-                border-color: Highlight;
-            }
-
-            li a,
-            li button {
-                forced-color-adjust: none;
-                background-color: $color-black;
-                border-color: $color-white;
-                color: $color-white;
-            }
-
-            li a:focus,
-            li button:focus {
-                background-color: $color-black;
-                forced-color-adjust: none;
-                border: 4px solid #0ff;
-                color: $color-white;
-            }
-        }
-
-        a {
-            box-sizing: border-box;
-            white-space: nowrap;
-            position: relative;
-            text-decoration: none;
-            text-transform: uppercase;
-            display: block;
-            color: $color-white;
-            padding: 1em;
-            font-weight: normal;
-
-            &:hover {
-                background-color: $color-teal-darker;
-            }
-
-            &.icon {
-                padding-right: 5em;
-
-                // stylelint-disable-next-line max-nesting-depth
-                &:before,
-                &:after {
-                    right: 1em;
-                }
-            }
-
-            &.shortcut {
-                padding-right: 7em;
-            }
-        }
-
-        a,
-        input[type=submit],
-        input[type=reset],
-        input[type=button],
-        .button,
-        button {
-            border-radius: 0;
-            font-size: 0.95em;
-            -webkit-font-smoothing: auto;
-        }
+    a {
+      box-sizing: border-box;
+      white-space: nowrap;
+      position: relative;
+      text-decoration: none;
+      text-transform: uppercase;
+      display: block;
+      color: $color-white;
+      padding: 1em;
+      font-weight: normal;
+
+      &:hover {
+        background-color: $color-teal-darker;
+      }
 
-        label {
-            padding: 1.3em;
-        }
+      &.icon {
+        padding-right: 5em;
 
-        .kbd {
-            position: absolute;
-            right: 1em;
-            font-weight: 600;
-            font-size: 0.8em;
-            color: rgba(0, 0, 0, 0.3);
+        // stylelint-disable-next-line max-nesting-depth
+        &:before,
+        &:after {
+          right: 1em;
         }
+      }
 
+      &.shortcut {
+        padding-right: 7em;
+      }
     }
 
-    &.open ul {
-        box-shadow: 0 3px 3px 0 rgba(0, 0, 0, 0.2);
-        opacity: 1;
-        left: 0;
-        display: block;
+    a,
+    input[type='submit'],
+    input[type='reset'],
+    input[type='button'],
+    .button,
+    button {
+      border-radius: 0;
+      font-size: 0.95em;
+      -webkit-font-smoothing: auto;
     }
 
-    &.match-width ul {
-        width: 100%;
-        min-width: 110px;
-
-        li {
-            white-space: nowrap;
-        }
+    label {
+      padding: 1.3em;
     }
 
-    &.dropup ul {
-        box-shadow: 0 -3px 3px 0 rgba(0, 0, 0, 0.2);
-        top: auto;
-        bottom: 100%;
-
-        li {
-            border-width: 0 0 1px;
-        }
+    .kbd {
+      position: absolute;
+      right: 1em;
+      font-weight: 600;
+      font-size: 0.8em;
+      color: rgba(0, 0, 0, 0.3);
     }
+  }
 
-    .dropdown-toggle {
-        color: $color-white;
-        text-transform: uppercase;
-        background-color: $color-teal;
-        line-height: 2.8em;
-        cursor: pointer;
-        height: 100%;
-        border-left: 1px solid rgba(255, 255, 255, 0.2);
-        position: absolute;
-        right: 0;
-        padding: 0 0.5em;
-        text-align: center;
+  &.open ul {
+    box-shadow: 0 3px 3px 0 rgba(0, 0, 0, 0.2);
+    opacity: 1;
+    left: 0;
+    display: block;
+  }
 
-        &:before,
-        &:after {
-            margin: 0;
-        }
+  &.match-width ul {
+    width: 100%;
+    min-width: 110px;
 
-        &:before {
-            width: 1em;
-            font-size: 1.2rem;
-        }
+    li {
+      white-space: nowrap;
+    }
+  }
 
-        &:hover {
-            background-color: $color-teal-darker;
-        }
+  &.dropup ul {
+    box-shadow: 0 -3px 3px 0 rgba(0, 0, 0, 0.2);
+    top: auto;
+    bottom: 100%;
 
-        svg.icon { // TODO: remove svg qualifier once the icon font styles are gone
-            @include svg-icon(1.3em);
-        }
+    li {
+      border-width: 0 0 1px;
+    }
+  }
+
+  .dropdown-toggle {
+    color: $color-white;
+    text-transform: uppercase;
+    background-color: $color-teal;
+    line-height: 2.8em;
+    cursor: pointer;
+    height: 100%;
+    border-left: 1px solid rgba(255, 255, 255, 0.2);
+    position: absolute;
+    right: 0;
+    padding: 0 0.5em;
+    text-align: center;
+
+    &:before,
+    &:after {
+      margin: 0;
     }
 
-    .bicolor + .dropdown-toggle {
-        background-color: $color-teal-darker;
-
-        &:hover {
-            background-color: $color-teal-dark;
-        }
+    &:before {
+      width: 1em;
+      font-size: 1.2rem;
     }
 
-    &.open .dropdown-toggle {
-        background-color: $color-teal-darker;
+    &:hover {
+      background-color: $color-teal-darker;
     }
 
-    .bicolor:hover {
-        background-color: $color-teal-dark;
+    svg.icon {
+      // TODO: remove svg qualifier once the icon font styles are gone
+      @include svg-icon(1.3em);
     }
+  }
 
-    // Styles for dropdowns which are also buttons e.g page editor
-    &.dropdown-button {
-        // Media for Windows High Contrast
-        @media (forced-colors: $media-forced-colours) {
-            button {
-                border-color: ActiveText;
-            }
-
-            button:hover {
-                border-color: Highlight;
-            }
-
-            a.button.bicolor.button:hover {
-                border-color: Highlight;
-            }
-        }
+  .bicolor + .dropdown-toggle {
+    background-color: $color-teal-darker;
 
-        .dropdown-toggle {
-            border-radius: 0 3px 3px 0;
-            // Media for Windows High Contrast
-            @media (forced-colors: $media-forced-colours) {
-                background: transparent;
-                box-sizing: border-box;
-                border: 1px solid ActiveText;
-            }
-        }
+    &:hover {
+      background-color: $color-teal-dark;
+    }
+  }
+
+  &.open .dropdown-toggle {
+    background-color: $color-teal-darker;
+  }
+
+  .bicolor:hover {
+    background-color: $color-teal-dark;
+  }
+
+  // Styles for dropdowns which are also buttons e.g page editor
+  &.dropdown-button {
+    // Media for Windows High Contrast
+    @media (forced-colors: $media-forced-colours) {
+      button {
+        border-color: ActiveText;
+      }
+
+      button:hover {
+        border-color: Highlight;
+      }
+
+      a.button.bicolor.button:hover {
+        border-color: Highlight;
+      }
+    }
 
-        .dropdown-toggle:hover {
-            // Media for Windows High Contrast
-            @media (forced-colors: $media-forced-colours) {
-                background-color: transparent;
-                border: 1px solid Highlight;
-            }
-        }
+    .dropdown-toggle {
+      border-radius: 0 3px 3px 0;
+      // Media for Windows High Contrast
+      @media (forced-colors: $media-forced-colours) {
+        background: transparent;
+        box-sizing: border-box;
+        border: 1px solid ActiveText;
+      }
+    }
 
-        &.open {
-            > input[type=button],
-            > input[type=submit],
-            > button,
-            > .button {
-                border-radius: 3px 3px 0 0;
-            }
-
-            .dropdown-toggle {
-                border-radius: 0 3px 0 0;
-            }
-        }
+    .dropdown-toggle:hover {
+      // Media for Windows High Contrast
+      @media (forced-colors: $media-forced-colours) {
+        background-color: transparent;
+        border: 1px solid Highlight;
+      }
     }
 
-    &.dropdown-button--white {
-        ul {
-            background-color: $color-grey-3;
-        }
+    &.open {
+      > input[type='button'],
+      > input[type='submit'],
+      > button,
+      > .button {
+        border-radius: 3px 3px 0 0;
+      }
+
+      .dropdown-toggle {
+        border-radius: 0 3px 0 0;
+      }
+    }
+  }
 
-        li a,
-        li .button {
-            background-color: $color-white;
-            color: $color-button;
-            border: 0;
+  &.dropdown-button--white {
+    ul {
+      background-color: $color-grey-3;
+    }
 
-            &:hover {
-                background-color: $color-grey-4;
-            }
+    li a,
+    li .button {
+      background-color: $color-white;
+      color: $color-button;
+      border: 0;
 
-            &.no {
-                color: $color-button-no;
-            }
+      &:hover {
+        background-color: $color-grey-4;
+      }
 
-            &.warning {
-                color: $color-button-warning;
-            }
-        }
-    }
+      &.no {
+        color: $color-button-no;
+      }
 
-    &.dropup.dropdown-button {
-        &.open {
-            > input[type=button],
-            > input[type=submit],
-            > button,
-            > .button {
-                border-radius: 0 0 3px 3px;
-            }
-
-            .dropdown-toggle {
-                border-radius: 0 0 3px;
-            }
-        }
+      &.warning {
+        color: $color-button-warning;
+      }
+    }
+  }
+
+  &.dropup.dropdown-button {
+    &.open {
+      > input[type='button'],
+      > input[type='submit'],
+      > button,
+      > .button {
+        border-radius: 0 0 3px 3px;
+      }
+
+      .dropdown-toggle {
+        border-radius: 0 0 3px;
+      }
     }
+  }
 }
 
 .dropdown.white {
-    ul {
-        background-color: $color-white;
+  ul {
+    background-color: $color-white;
 
-        li {
-            border-top: 1px solid rgba(0, 0, 0, 0.1);
-        }
+    li {
+      border-top: 1px solid rgba(0, 0, 0, 0.1);
+    }
 
-        a {
-            color: $color-grey-2;
+    a {
+      color: $color-grey-2;
 
-            &:hover {
-                background-color: $color-grey-3;
-            }
-        }
+      &:hover {
+        background-color: $color-grey-3;
+      }
     }
+  }
 }
 
 .dropdown.warning {
-    ul {
-        background-color: $color-button-warning;
-    }
+  ul {
+    background-color: $color-button-warning;
+  }
 
-    .dropdown-toggle {
-        background-color: $color-button-warning;
+  .dropdown-toggle {
+    background-color: $color-button-warning;
 
-        &:hover {
-            background-color: $color-button-warning-hover;
-        }
+    &:hover {
+      background-color: $color-button-warning-hover;
     }
+  }
 }
 
 // Transitions
 // stylelint-disable-next-line no-duplicate-selectors
 .dropdown ul {
-    @include transition(opacity 0.2s linear);
+  @include transition(opacity 0.2s linear);
 }
 
 .dropdown-button {
-    .button svg.icon { // TODO: leave only class when iconfont styles are removed
-        @include svg-icon();
-        margin-right: 0.5em;
-    }
+  .button svg.icon {
+    // TODO: leave only class when iconfont styles are removed
+    @include svg-icon();
+    margin-right: 0.5em;
+  }
 }

+ 64 - 64
client/scss/components/_dropdown.scss

@@ -5,105 +5,105 @@
 // .c-dropdown {
 // }
 .c-dropdown__button {
-    display: inline-block;
-    box-sizing: border-box;
-    padding-left: 0.5rem;
-    padding-right: 0.25rem;
-    // Make this the same as the other buttons
-    line-height: 1.85;
-    border: solid 1px transparent;
-    border-radius: 2px;
-    font-size: 0.95em;
-    cursor: pointer;
-    -webkit-font-smoothing: subpixel-antialiased;
-    user-select: none;
+  display: inline-block;
+  box-sizing: border-box;
+  padding-left: 0.5rem;
+  padding-right: 0.25rem;
+  // Make this the same as the other buttons
+  line-height: 1.85;
+  border: solid 1px transparent;
+  border-radius: 2px;
+  font-size: 0.95em;
+  cursor: pointer;
+  -webkit-font-smoothing: subpixel-antialiased;
+  user-select: none;
 }
 
 .c-dropdown--large .c-dropdown__button {
-    line-height: 2.9em;
-    padding-left: 0.5rem;
-    padding-right: 0.5rem;
+  line-height: 2.9em;
+  padding-left: 0.5rem;
+  padding-right: 0.5rem;
 
-    .icon-site {
-        padding-right: 0.2rem;
-    }
+  .icon-site {
+    padding-right: 0.2rem;
+  }
 }
 
 .c-dropdown__icon {
-    padding-left: 0.4rem;
-    padding-right: 0.4rem;
+  padding-left: 0.4rem;
+  padding-right: 0.4rem;
 }
 
 .c-dropdown__toggle {
-    display: inline-block;
+  display: inline-block;
 }
 
 .c-dropdown__togle--icon {
-    &:before {
-        display: none; // TODO: remove when iconfont styles are removed
-    }
+  &:before {
+    display: none; // TODO: remove when iconfont styles are removed
+  }
 
-    .icon {
-        @include svg-icon(1em, middle);
-    }
+  .icon {
+    @include svg-icon(1em, middle);
+  }
 
-    .icon-arrow-up {
-        display: none;
-    }
+  .icon-arrow-up {
+    display: none;
+  }
 }
 
 .is-open .c-dropdown__togle--icon {
-    .icon-arrow-up {
-        display: inline-block;
-    }
+  .icon-arrow-up {
+    display: inline-block;
+  }
 
-    .icon-arrow-down {
-        display: none;
-    }
+  .icon-arrow-down {
+    display: none;
+  }
 }
 
 .c-dropdown__menu.c-dropdown__menu {
-    margin-top: 0.75rem;
-    padding: 0.75rem 1rem;
-    min-width: 8rem;
-    text-transform: none;
-    position: absolute;
-    z-index: 1000;
-    animation: dropdownIn 0.1s ease-out backwards;
-    list-style: none;
-    // Override any right alignment that might've been set by a parent element
-    // (such as the snippets header)
-    text-align: left;
+  margin-top: 0.75rem;
+  padding: 0.75rem 1rem;
+  min-width: 8rem;
+  text-transform: none;
+  position: absolute;
+  z-index: 1000;
+  animation: dropdownIn 0.1s ease-out backwards;
+  list-style: none;
+  // Override any right alignment that might've been set by a parent element
+  // (such as the snippets header)
+  text-align: left;
 }
 
 .c-dropdown__item {
-    margin-bottom: 0.375rem;
-    font-size: 0.8rem;
+  margin-bottom: 0.375rem;
+  font-size: 0.8rem;
 
-    &:hover {
-        .c-dropdown__indicator {
-            opacity: 0.6;
-        }
+  &:hover {
+    .c-dropdown__indicator {
+      opacity: 0.6;
     }
+  }
 }
 
 .c-dropdown__item:last-child {
-    margin-bottom: 0;
+  margin-bottom: 0;
 }
 
 .c-dropdown__divider {
-    border-color: #555;
-    border-style: dotted;
-    margin-top: 12px;
-    margin-bottom: 12px;
+  border-color: #555;
+  border-style: dotted;
+  margin-top: 12px;
+  margin-bottom: 12px;
 }
 
 @keyframes dropdownIn {
-    0% {
-        opacity: 0;
-    }
+  0% {
+    opacity: 0;
+  }
 
-    100% {
-        opacity: 1;
-    }
+  100% {
+    opacity: 1;
+  }
 }

+ 139 - 139
client/scss/components/_footer.scss

@@ -1,167 +1,167 @@
-@use "sass:math";
+@use 'sass:math';
 
 .footer {
-    $border-curvature: 3px;
-    @include transition(bottom 0.5s ease 1s);
-    @include row();
+  $border-curvature: 3px;
+  @include transition(bottom 0.5s ease 1s);
+  @include row();
 
-    ul {
-        @include unlist();
-    }
+  ul {
+    @include unlist();
+  }
 
-    li {
-        float: left;
+  li {
+    float: left;
 
-        .dropdown li,  // dropdown li
+    .dropdown li,  // dropdown li
         &:last-child {
-            margin-right: 0;
-        }
+      margin-right: 0;
+    }
+  }
+
+  &__container {
+    border-radius: $border-curvature $border-curvature 0 0;
+    background: $color-grey-1;
+    color: $color-white;
+    margin-top: 0;
+    margin-right: 0;
+    transition: transform 1s;
+
+    &:first-child {
+      margin-top: 0;
+      box-shadow: 0 0 2px rgba(255, 255, 255, 0.5);
     }
 
-    &__container {
-        border-radius: $border-curvature $border-curvature 0 0;
-        background: $color-grey-1;
-        color: $color-white;
-        margin-top: 0;
-        margin-right: 0;
-        transition: transform 1s;
+    &.footer__container--hidden {
+      transform: translateY(100%);
+    }
 
-        &:first-child {
-            margin-top: 0;
-            box-shadow: 0 0 2px rgba(255, 255, 255, 0.5);
-        }
+    li {
+      margin-right: 1em;
+    }
+  }
 
-        &.footer__container--hidden {
-            transform: translateY(100%);
-        }
+  &__save-warning {
+    font-size: 0.95em;
+    display: flex;
+    align-items: center;
 
-        li {
-            margin-right: 1em;
-        }
+    .icon {
+      font-size: 1.2em;
+      margin-right: 0.5em;
     }
 
-    &__save-warning {
-        font-size: 0.95em;
-        display: flex;
-        align-items: center;
+    p {
+      margin: -0.2em 0 0 0;
+    }
+  }
 
-        .icon {
-            font-size: 1.2em;
-            margin-right: 0.5em;
-        }
+  &__emphasise-span-tags span {
+    color: $color-orange;
+  }
 
-        p {
-            margin: -0.2em 0 0 0;
-        }
-    }
+  .actions {
+    width: 250px;
 
-    &__emphasise-span-tags span {
-        color: $color-orange;
+    &--primary {
+      width: 350px;
     }
 
-    .actions {
-        width: 250px;
+    .dropdown {
+      input[type='submit'],
+      input[type='reset'],
+      input[type='button'],
+      button,
+      .button {
+        padding-right: 2.6em;
+      }
+    }
+  }
+
+  .preview .dropdown {
+    width: 250px;
+  }
+
+  .meta {
+    float: right;
+    text-align: right;
+    padding: 7px math.div($grid-gutter-width, 2);
+    font-size: 0.85em;
+
+    p {
+      margin: 0;
+      margin-right: $grid-gutter-width;
+      white-space: nowrap;
+    }
 
-        &--primary {
-            width: 350px;
-        }
+    a {
+      color: inherit;
 
-        .dropdown {
-            input[type=submit],
-            input[type=reset],
-            input[type=button],
-            button,
-            .button {
-                padding-right: 2.6em;
-            }
-        }
+      &:hover {
+        color: $color-link;
+      }
     }
+  }
 
+  @include media-breakpoint-down(xs) {
+    .actions,
+    .preview,
+    &__container,
     .preview .dropdown {
-        width: 250px;
+      width: 100%;
     }
 
+    margin-top: $mobile-nice-padding;
+
     .meta {
-        float: right;
-        text-align: right;
-        padding: 7px math.div($grid-gutter-width, 2);
-        font-size: 0.85em;
-
-        p {
-            margin: 0;
-            margin-right: $grid-gutter-width;
-            white-space: nowrap;
-        }
-
-        a {
-            color: inherit;
-
-            &:hover {
-                color: $color-link;
-            }
-        }
-    }
-
-    @include media-breakpoint-down(xs) {
-        .actions,
-        .preview,
-        &__container,
-        .preview .dropdown {
-            width: 100%;
-        }
-
-        margin-top: $mobile-nice-padding;
-
-        .meta {
-            p {
-                white-space: normal;
-                width: 100%;
-            }
-
-            .avatar {
-                left: auto;
-            }
-        }
-
-        &__container {
-            &:not(:first-child) {
-                border-radius: 0;
-            }
-
-            &--hidden {
-                display: none;
-            }
-        }
-
-        &__save-warning {
-            display: flex;
-            flex-direction: row;
-            justify-content: center;
-        }
-    }
-
-    @include media-breakpoint-up(sm) {
-        margin-left: calc(#{$desktop-nice-padding} - 0.75em);
-        margin-right: $desktop-nice-padding;
-        width: auto;
-        position: fixed;
-        bottom: 0;
-
-        > ul {
-            display: flex;
-        }
-
-        &__container {
-            padding: 0.75em;
-            margin-right: 0;
-
-            &:not(:first-child) {
-                margin-left: -$border-curvature;
-            }
-        }
-
-        &__save-warning {
-            margin-right: 50px;
-        }
+      p {
+        white-space: normal;
+        width: 100%;
+      }
+
+      .avatar {
+        left: auto;
+      }
+    }
+
+    &__container {
+      &:not(:first-child) {
+        border-radius: 0;
+      }
+
+      &--hidden {
+        display: none;
+      }
+    }
+
+    &__save-warning {
+      display: flex;
+      flex-direction: row;
+      justify-content: center;
+    }
+  }
+
+  @include media-breakpoint-up(sm) {
+    margin-left: calc(#{$desktop-nice-padding} - 0.75em);
+    margin-right: $desktop-nice-padding;
+    width: auto;
+    position: fixed;
+    bottom: 0;
+
+    > ul {
+      display: flex;
+    }
+
+    &__container {
+      padding: 0.75em;
+      margin-right: 0;
+
+      &:not(:first-child) {
+        margin-left: -$border-curvature;
+      }
+    }
+
+    &__save-warning {
+      margin-right: 50px;
     }
+  }
 }

+ 346 - 342
client/scss/components/_forms.scss

@@ -1,5 +1,5 @@
-@use "sass:map";
-@use "sass:math";
+@use 'sass:map';
+@use 'sass:math';
 // stylelint-disable scss/comment-no-empty
 // These are the generic stylings for forms of any type.
 // If you're styling something specific to the page editing interface,
@@ -26,12 +26,12 @@
 //     }
 // }
 .plain-checkbox-label {
-    // cancel heavy / floated label styles, for labels that should appear inline against checkboxes
+  // cancel heavy / floated label styles, for labels that should appear inline against checkboxes
 
-    float: none;
-    color: inherit;
-    font-weight: inherit;
-    font-size: inherit;
+  float: none;
+  color: inherit;
+  font-weight: inherit;
+  font-size: inherit;
 }
 
 // TODO: mixin,
@@ -76,7 +76,7 @@
 
 // Reset the arrow on `<select>`s in IE10+.
 select::-ms-expand {
-    display: none;
+  display: none;
 }
 
 // select boxes
@@ -84,67 +84,67 @@ select::-ms-expand {
 .choice_field .input,
 .model_choice_field .input,
 .typed_choice_field .input {
-    position: relative;
-
-    // Add select arrow back on browsers where native ui has been removed
-    select ~ span:after {
-        border-radius: 0 6px 6px 0;
-        z-index: 0;
-        position: absolute;
-        right: 0;
-        top: 1px;
-        bottom: 0;
-        width: 1.5em;
-        font-family: $font-wagtail-icons;
-        content: map.get($icons, 'arrow-down');
-        border: 1px solid $color-input-border;
-        border-width: 0 0 0 1px;
-        text-align: center;
-        line-height: 1.4em;
-        font-size: 3em;
-        pointer-events: none;
-        color: $color-grey-3;
-        margin: 0 1px 1px 0;
+  position: relative;
+
+  // Add select arrow back on browsers where native ui has been removed
+  select ~ span:after {
+    border-radius: 0 6px 6px 0;
+    z-index: 0;
+    position: absolute;
+    right: 0;
+    top: 1px;
+    bottom: 0;
+    width: 1.5em;
+    font-family: $font-wagtail-icons;
+    content: map.get($icons, 'arrow-down');
+    border: 1px solid $color-input-border;
+    border-width: 0 0 0 1px;
+    text-align: center;
+    line-height: 1.4em;
+    font-size: 3em;
+    pointer-events: none;
+    color: $color-grey-3;
+    margin: 0 1px 1px 0;
 
-        .ie & {
-            display: none;
-        }
+    .ie & {
+      display: none;
     }
+  }
 
-    // Override default select padding so the chevron will overlap with long option text
-    select {
-        padding-right: 5em;
-    }
+  // Override default select padding so the chevron will overlap with long option text
+  select {
+    padding-right: 5em;
+  }
 }
 
 // Other text
 .help,
 .error-message {
-    border: 1px solid transparent; // ensure visible separation in Windows High Contrast mode
-    font-size: 0.85em;
-    font-weight: normal;
-    margin: 0.5em 0 0;
+  border: 1px solid transparent; // ensure visible separation in Windows High Contrast mode
+  font-size: 0.85em;
+  font-weight: normal;
+  margin: 0.5em 0 0;
 }
 
 .error-message {
-    font-size: 1em;
-    font-weight: bold;
-    color: $color-text-error;
-
-    @media (forced-colors: $media-forced-colours) {
-        forced-color-adjust: none;
-        color: $color-text-error-forced-color;
-    }
-
-    &::before {
-        font-family: $font-wagtail-icons;
-        vertical-align: -10%;
-        content: map.get($icons, 'cross');
-    }
+  font-size: 1em;
+  font-weight: bold;
+  color: $color-text-error;
+
+  @media (forced-colors: $media-forced-colours) {
+    forced-color-adjust: none;
+    color: $color-text-error-forced-color;
+  }
+
+  &::before {
+    font-family: $font-wagtail-icons;
+    vertical-align: -10%;
+    content: map.get($icons, 'cross');
+  }
 }
 
 .help {
-    color: $color-grey-2;
+  color: $color-grey-2;
 }
 
 fieldset:hover > .help,
@@ -152,26 +152,26 @@ fieldset:hover > .help,
 .field:focus + .help,
 .field:hover + .help,
 li.focused > .help {
-    opacity: 1;
+  opacity: 1;
 }
 
 .required .field > label:after,
 label.required:after {
-    content: '*';
-    color: $color-red;
-    font-weight: bold;
-    display: inline-block;
-    margin-left: 0.5em;
-    line-height: 1em;
-    font-size: 13px;
+  content: '*';
+  color: $color-red;
+  font-weight: bold;
+  display: inline-block;
+  margin-left: 0.5em;
+  line-height: 1em;
+  font-size: 13px;
 }
 
 .error input,
 .error textarea,
 .error select,
 .error .tagit {
-    border-color: $color-red;
-    background-color: $color-input-error-bg;
+  border-color: $color-red;
+  background-color: $color-input-error-bg;
 }
 
 // Layouts for particular kinds of of fields
@@ -179,7 +179,7 @@ label.required:after {
 // permanently show checkbox/radio help as they have no focus state
 .boolean_field .help,
 .radio .help {
-    opacity: 1;
+  opacity: 1;
 }
 
 // This is expected to go on the parent of the input/select/textarea
@@ -189,86 +189,86 @@ label.required:after {
 .time_field,
 .date_time_field,
 .url_field {
-    .input {
-        position: relative;
-
-        &:before,
-        &:after {
-            font-family: $font-wagtail-icons;
-            position: absolute;
-            top: 0.5em;
-            line-height: 100%;
-            font-size: 2em;
-            color: $color-grey-3;
-        }
-
-        &:before {
-            left: 0.3em;
-        }
+  .input {
+    position: relative;
 
-        &:after {
-            right: 0.5em;
-        }
+    &:before,
+    &:after {
+      font-family: $font-wagtail-icons;
+      position: absolute;
+      top: 0.5em;
+      line-height: 100%;
+      font-size: 2em;
+      color: $color-grey-3;
     }
 
-    input:not([type=radio]),
-    input:not([type=checkbox]),
-    input:not([type=submit]),
-    input:not([type=button]) {
-        padding-left: 2.5em;
+    &:before {
+      left: 0.3em;
     }
 
-    // smaller fields required slight repositioning of icons
-    &.field-small {
-        .input {
-            &:before,
-            &:after {
-                font-size: 1.3rem; // REMs are necessary here because IE doesn't treat generated content correctly
-                top: 0.3em;
-            }
-
-            &:before {
-                left: 0.5em;
-            }
-
-            &:after {
-                right: 0.5em;
-            }
-        }
+    &:after {
+      right: 0.5em;
     }
+  }
 
-    // special case for search spinners
-    &.icon-spinner:after {
-        color: $color-teal;
-        opacity: 0.8;
-        text-align: center;
+  input:not([type='radio']),
+  input:not([type='checkbox']),
+  input:not([type='submit']),
+  input:not([type='button']) {
+    padding-left: 2.5em;
+  }
+
+  // smaller fields required slight repositioning of icons
+  &.field-small {
+    .input {
+      &:before,
+      &:after {
+        font-size: 1.3rem; // REMs are necessary here because IE doesn't treat generated content correctly
         top: 0.3em;
+      }
+
+      &:before {
+        left: 0.5em;
+      }
+
+      &:after {
+        right: 0.5em;
+      }
     }
+  }
+
+  // special case for search spinners
+  &.icon-spinner:after {
+    color: $color-teal;
+    opacity: 0.8;
+    text-align: center;
+    top: 0.3em;
+  }
 }
 
 .date_field,
 .date_time_field {
-    .input:before {
-        content: map.get($icons, 'date');
-    }
+  .input:before {
+    content: map.get($icons, 'date');
+  }
 }
 
 .time_field {
-    .input:before {
-        content: map.get($icons, 'time');
-    }
+  .input:before {
+    content: map.get($icons, 'time');
+  }
 }
 
 .url_field {
-    .input:before {
-        content: map.get($icons, 'link');
-    }
+  .input:before {
+    content: map.get($icons, 'link');
+  }
 }
 
 .daterange_field {
-    input:last-of-type {
-        margin-top: 1.2em; // Mirrors the label 1.2em top padding.
-    }
+  input:last-of-type {
+    margin-top: 1.2em; // Mirrors the label 1.2em top padding.
+  }
 }
 
 // This is specifically for list of radios/checkboxes
@@ -276,301 +276,305 @@ label.required:after {
 .checkbox_select_multiple .input li,
 .multiple_choice_field .input li,
 .choice_field .input li {
-    label {
-        display: block;
-        width: auto;
-        float: none;
-        padding-top: 0; // Negates padding added to label for the group of fields as a whole
-        padding-bottom: 0.8em;
-    }
+  label {
+    display: block;
+    width: auto;
+    float: none;
+    padding-top: 0; // Negates padding added to label for the group of fields as a whole
+    padding-bottom: 0.8em;
+  }
 }
 
 .fields > li,
 .field-col {
-    @include clearfix();
-    padding-top: 0.5em;
-    padding-bottom: 1.2em;
+  @include clearfix();
+  padding-top: 0.5em;
+  padding-bottom: 1.2em;
 }
 
 .field-row {
-    @include clearfix();
+  @include clearfix();
 
-    // negative margin the bottom so it doesn't add too much space
-    margin-bottom: -1.2em;
+  // negative margin the bottom so it doesn't add too much space
+  margin-bottom: -1.2em;
 }
 
 .input {
-    clear: both;
+  clear: both;
 }
 
 // field sizing and alignment
 .field-small {
-    input,
-    textarea,
-    select,
-    .halloeditor,
-    .tagit {
-        border-radius: 3px;
-        padding: 0.4em 1em;
-    }
+  input,
+  textarea,
+  select,
+  .halloeditor,
+  .tagit {
+    border-radius: 3px;
+    padding: 0.4em 1em;
+  }
 }
 
 .field {
-    &.col1,
-    &.col2,
-    &.col3,
-    &.col4,
-    &.col5,
-    &.col6,
-    &.col7,
-    &.col8,
-    &.col9,
-    &.col10,
-    &.col11,
-    &.col12 { clear: both;}
+  &.col1,
+  &.col2,
+  &.col3,
+  &.col4,
+  &.col5,
+  &.col6,
+  &.col7,
+  &.col8,
+  &.col9,
+  &.col10,
+  &.col11,
+  &.col12 {
+    clear: both;
+  }
 }
 
 li.inline .field {
-    &.col1,
-    &.col2,
-    &.col3,
-    &.col4,
-    &.col5,
-    &.col6,
-    &.col7,
-    &.col8,
-    &.col9,
-    &.col10,
-    &.col11,
-    &.col12 { clear: none;}
+  &.col1,
+  &.col2,
+  &.col3,
+  &.col4,
+  &.col5,
+  &.col6,
+  &.col7,
+  &.col8,
+  &.col9,
+  &.col10,
+  &.col11,
+  &.col12 {
+    clear: none;
+  }
 }
 
 // solve gutter issues of inline fields
 ul.inline li:first-child,
 li.inline:first-child {
-    margin-left: math.div(-$grid-gutter-width, 2);
+  margin-left: math.div(-$grid-gutter-width, 2);
 }
 
 // search-bars
 .search-bar {
-    .required .field > label:after {
-        display: none;
-    }
+  .required .field > label:after {
+    display: none;
+  }
 
-    .button-filter {
-        height: 2.71em;
-        border-color: transparent;
-    }
+  .button-filter {
+    height: 2.71em;
+    border-color: transparent;
+  }
 }
 
 // file drop zones
 .drop-zone {
-    border-radius: 5px;
-    border: 2px dashed $color-grey-4;
-    padding: $mobile-nice-padding;
-    background-color: $color-grey-5;
-    margin-bottom: 1em;
-    text-align: center;
-
-    .drop-zone-help {
-        border: 0;
-    }
-
-    &.hovered {
-        border-color: $color-teal;
-        background-color: $color-input-focus;
-    }
+  border-radius: 5px;
+  border: 2px dashed $color-grey-4;
+  padding: $mobile-nice-padding;
+  background-color: $color-grey-5;
+  margin-bottom: 1em;
+  text-align: center;
+
+  .drop-zone-help {
+    border: 0;
+  }
+
+  &.hovered {
+    border-color: $color-teal;
+    background-color: $color-input-focus;
+  }
 }
 
 // Transitions
 // stylelint-disable-next-line no-duplicate-selectors
 .help {
-    @include transition(opacity 0.2s ease);
+  @include transition(opacity 0.2s ease);
 }
 
 .label-uppercase {
-    .field > label {
-        text-transform: uppercase;
-    }
+  .field > label {
+    text-transform: uppercase;
+  }
 }
 
 @include media-breakpoint-up(sm) {
-    .help {
-        opacity: 1;
-    }
+  .help {
+    opacity: 1;
+  }
 
-    .fields {
-        max-width: 800px;
-    }
+  .fields {
+    max-width: 800px;
+  }
 
-    .field {
-        @include row();
-    }
+  .field {
+    @include row();
+  }
 
-    .field-content {
-        @include column(10, 0);
-    }
+  .field-content {
+    @include column(10, 0);
+  }
 
-    .field-col {
-        float: left;
-        padding-left: 0;
+  .field-col {
+    float: left;
+    padding-left: 0;
 
-        // anything less than 4 columns or greater than 6 is impractical
-        &.col4 {
-            label {
-                @include column(2, 0, 4);
-            }
+    // anything less than 4 columns or greater than 6 is impractical
+    &.col4 {
+      label {
+        @include column(2, 0, 4);
+      }
 
-            .field-content {
-                @include column(2, $padding, 4);
-                padding-left: 0;
-            }
-        }
+      .field-content {
+        @include column(2, $padding, 4);
+        padding-left: 0;
+      }
+    }
 
-        &.col6 {
-            label {
-                @include column(2, 0, 6);
-            }
+    &.col6 {
+      label {
+        @include column(2, 0, 6);
+      }
 
-            .field-content {
-                @include column(4, $padding, 6);
-                padding-left: 0;
-            }
-        }
+      .field-content {
+        @include column(4, $padding, 6);
+        padding-left: 0;
+      }
     }
+  }
 
-    .label-above {
-        .field > label,
-        .field > .field-content {
-            display: block;
-            padding: 0 0 0.8em;
-            float: none;
-            width: auto;
-        }
+  .label-above {
+    .field > label,
+    .field > .field-content {
+      display: block;
+      padding: 0 0 0.8em;
+      float: none;
+      width: auto;
     }
+  }
 }
 
 .field-comment-control {
-    display: none;
+  display: none;
 }
 
 .tab-content--comments-enabled {
-    .field {
-        position: relative;
-    }
+  .field {
+    position: relative;
+  }
+
+  .field-content {
+    padding-right: 45px;
+
+    @include media-breakpoint-up(sm) {
+      padding-right: 60px;
+    }
+  }
+
+  .widget-draftail_rich_text_area .field-content {
+    padding-right: 0;
+  }
+
+  .field-comment-control {
+    position: absolute;
+    display: block;
+    top: 0;
+    right: 0;
+    height: 100%;
+    line-height: 100%;
+
+    &--object {
+      right: 20px;
+
+      @include media-breakpoint-up(lg) {
+        right: 350px;
+      }
+    }
+
+    button {
+      @include transition(opacity 0.2s ease);
+      border: 0;
+      background: none;
+      width: 30px;
+      height: 30px;
+      padding: 0;
+      border-radius: 3px;
+      position: absolute;
+      top: 50%;
+      right: 0;
+
+      @include media-breakpoint-up(sm) {
+        right: 10px;
+      }
+
+      // Hide by default, reveal on hover of parent
+      @include media-breakpoint-up(md) {
+        opacity: 0;
+        pointer-events: none;
+        transform: translateY(-50%);
+        right: 0;
+      }
+
+      .icon-reversed {
+        display: none;
+      }
 
-    .field-content {
-        padding-right: 45px;
+      &:hover {
+        cursor: pointer;
 
-        @include media-breakpoint-up(sm) {
-            padding-right: 60px;
+        // stylelint-disable max-nesting-depth
+        .icon-default {
+          display: none;
         }
-    }
 
-    .widget-draftail_rich_text_area .field-content {
-        padding-right: 0;
-    }
-
-    .field-comment-control {
-        position: absolute;
-        display: block;
-        top: 0;
-        right: 0;
-        height: 100%;
-        line-height: 100%;
+        .icon-reversed {
+          display: block;
+        }
+      }
 
-        &--object {
-            right: 20px;
+      &:focus {
+        opacity: 1;
+        pointer-events: initial;
+      }
 
-            @include media-breakpoint-up(lg) {
-                right: 350px;
-            }
-        }
+      > svg {
+        width: 30px;
+        height: 30px;
+        color: $color-teal;
 
-        button {
-            @include transition(opacity 0.2s ease);
-            border: 0;
-            background: none;
-            width: 30px;
-            height: 30px;
-            padding: 0;
-            border-radius: 3px;
-            position: absolute;
-            top: 50%;
-            right: 0;
-
-            @include media-breakpoint-up(sm) {
-                right: 10px;
-            }
-
-            // Hide by default, reveal on hover of parent
-            @include media-breakpoint-up(md) {
-                opacity: 0;
-                pointer-events: none;
-                transform: translateY(-50%);
-                right: 0;
-            }
-
-            .icon-reversed {
-                display: none;
-            }
-
-            &:hover {
-                cursor: pointer;
-
-                // stylelint-disable max-nesting-depth
-                .icon-default {
-                    display: none;
-                }
-
-                .icon-reversed {
-                    display: block;
-                }
-            }
-
-            &:focus {
-                opacity: 1;
-                pointer-events: initial;
-            }
-
-            > svg {
-                width: 30px;
-                height: 30px;
-                color: $color-teal;
-
-                @media (forced-colors: $media-forced-colours) {
-                    color: ButtonText;
-                    border: 1px solid;
-                }
-            }
+        @media (forced-colors: $media-forced-colours) {
+          color: ButtonText;
+          border: 1px solid;
         }
+      }
     }
+  }
 
-    .field-row .field-comment-control {
-        top: 0;
-    }
+  .field-row .field-comment-control {
+    top: 0;
+  }
 
-    .field:not(.block_field) {
-        &:hover {
-            .field-comment-control button {
-                opacity: 1;
-                pointer-events: initial;
-            }
-        }
+  .field:not(.block_field) {
+    &:hover {
+      .field-comment-control button {
+        opacity: 1;
+        pointer-events: initial;
+      }
     }
+  }
 
-    .object {
-        &:hover {
-            .field-comment-control--object button {
-                opacity: 1;
-                pointer-events: initial;
-            }
-        }
+  .object {
+    &:hover {
+      .field-comment-control--object button {
+        opacity: 1;
+        pointer-events: initial;
+      }
     }
+  }
 
-    .object.model_choice_field {
-        .object-help {
-            right: 153px;
-        }
+  .object.model_choice_field {
+    .object-help {
+      right: 153px;
     }
+  }
 }

+ 71 - 71
client/scss/components/_grid.legacy.scss

@@ -1,101 +1,101 @@
 .wrapper {
-    @include clearfix();
-    height: 100vh;
-    transition: transform 0.2s ease;
+  @include clearfix();
+  height: 100vh;
+  transition: transform 0.2s ease;
 }
 
 .content-wrapper {
-    box-sizing: border-box;
-    width: 100%;
-    height: 100%; // this has no effect on desktop, but on mobile it helps aesthetics of menu popout action
-    float: left;
-    position: relative;
-    background-color: $color-grey-4;
-    border-bottom: 1px solid $color-grey-3;
+  box-sizing: border-box;
+  width: 100%;
+  height: 100%; // this has no effect on desktop, but on mobile it helps aesthetics of menu popout action
+  float: left;
+  position: relative;
+  background-color: $color-grey-4;
+  border-bottom: 1px solid $color-grey-3;
 }
 
 .content {
-    @include row();
-    background: $color-white;
-    border-top: 0 solid $color-grey-5; // this top border provides space for the floating logo to toggle the menu
-    min-height: 100%;
-    position: relative; // yuk. necessary for positions for jquery ui widgets
-
-    @include media-breakpoint-up(sm) {
-        padding-bottom: 4em;
-    }
+  @include row();
+  background: $color-white;
+  border-top: 0 solid $color-grey-5; // this top border provides space for the floating logo to toggle the menu
+  min-height: 100%;
+  position: relative; // yuk. necessary for positions for jquery ui widgets
+
+  @include media-breakpoint-up(sm) {
+    padding-bottom: 4em;
+  }
 }
 
 @include media-breakpoint-up(sm) {
-    .content-wrapper {
-        border-bottom-right-radius: 5px;
-    }
-
-    .content {
-        border-top: 0;
-        background-color: none;
-        padding-top: 0;
-    }
+  .content-wrapper {
+    border-bottom-right-radius: 5px;
+  }
+
+  .content {
+    border-top: 0;
+    background-color: none;
+    padding-top: 0;
+  }
 }
 
 .row {
-    @include clearfix();
+  @include clearfix();
 }
 
 @include media-breakpoint-up(sm) {
-    .col1 {
-        @include column(1);
-    }
+  .col1 {
+    @include column(1);
+  }
 
-    .col2 {
-        @include column(2);
-    }
+  .col2 {
+    @include column(2);
+  }
 
-    .col3 {
-        @include column(3);
-    }
+  .col3 {
+    @include column(3);
+  }
 
-    .col4 {
-        @include column(4);
-    }
+  .col4 {
+    @include column(4);
+  }
 
-    .col5 {
-        @include column(5);
-    }
+  .col5 {
+    @include column(5);
+  }
 
-    .col6 {
-        @include column(6);
-    }
+  .col6 {
+    @include column(6);
+  }
 
-    .col7 {
-        @include column(7);
-    }
+  .col7 {
+    @include column(7);
+  }
 
-    .col8 {
-        @include column(8);
-    }
+  .col8 {
+    @include column(8);
+  }
 
-    .col9 {
-        @include column(9);
-    }
+  .col9 {
+    @include column(9);
+  }
 
-    .col10 {
-        @include column(10);
-    }
+  .col10 {
+    @include column(10);
+  }
 
-    .col11 {
-        @include column(11);
-    }
+  .col11 {
+    @include column(11);
+  }
 
-    .col12 {
-        @include column(12);
-    }
+  .col12 {
+    @include column(12);
+  }
 
-    .row {
-        @include row();
-    }
+  .row {
+    @include row();
+  }
 
-    .row-flush {
-        @include row-flush();
-    }
+  .row-flush {
+    @include row-flush();
+  }
 }

+ 172 - 170
client/scss/components/_header.scss

@@ -1,236 +1,238 @@
-@use "sass:math";
+@use 'sass:math';
 
 header {
-    padding-top: 1em;
-    padding-bottom: 1em;
-    background-color: $color-header-bg;
-    margin-bottom: 2em;
+  padding-top: 1em;
+  padding-bottom: 1em;
+  background-color: $color-header-bg;
+  margin-bottom: 2em;
+  color: $color-white;
+
+  a {
     color: $color-white;
+  }
 
-    a {
-        color: $color-white;
-    }
+  h1,
+  h2 {
+    margin: 0;
+    color: $color-white;
+  }
+
+  h1 {
+    padding: 0.2em 0;
 
-    h1,
-    h2 {
-        margin: 0;
-        color: $color-white;
+    &.icon:before {
+      width: 1em;
+      display: none;
+      margin-right: 0.4em;
+      font-size: 1.5em;
     }
+  }
 
-    h1 {
-        padding: 0.2em 0;
+  .col {
+    float: left;
+    margin-right: 2em;
+  }
 
-        &.icon:before {
-            width: 1em;
-            display: none;
-            margin-right: 0.4em;
-            font-size: 1.5em;
-        }
-    }
+  .left {
+    float: left;
 
-    .col {
-        float: left;
-        margin-right: 2em;
+    .hasform &:first-child {
+      padding-bottom: 0.5em;
+      float: none;
     }
+  }
 
-    .left {
-        float: left;
+  .right {
+    text-align: right;
+    float: right;
+  }
 
-        .hasform &:first-child {
-            padding-bottom: 0.5em;
-            float: none;
-        }
-    }
+  // For case where content below header should merge with it
+  &.merged {
+    margin-bottom: 0;
+  }
 
-    .right {
-        text-align: right;
-        float: right;
-    }
+  &.tab-merged {
+    padding-left: $desktop-nice-padding;
+    padding-right: $desktop-nice-padding;
 
-    // For case where content below header should merge with it
-    &.merged {
-        margin-bottom: 0;
+    .right:last-child {
+      padding-right: 0;
     }
 
-    &.tab-merged {
-        padding-left: $desktop-nice-padding;
-        padding-right: $desktop-nice-padding;
+    @include media-breakpoint-down(xs) {
+      .breadcrumb {
+        padding-left: calc(#{$desktop-nice-padding} - 8px);
+      }
+    }
+    @include media-breakpoint-up(sm) {
+      .breadcrumb {
+        margin-left: -$desktop-nice-padding;
+        margin-right: -$desktop-nice-padding;
+        padding-left: math.div($desktop-nice-padding, 2);
+      }
+    }
+  }
 
-        .right:last-child {
-            padding-right: 0;
-        }
+  &.header-with-breadcrumb {
+    padding-top: 0;
 
-        @include media-breakpoint-down(xs) {
-            .breadcrumb {
-                padding-left: calc(#{$desktop-nice-padding} - 8px);
-            }
-        }
-        @include media-breakpoint-up(sm) {
-            .breadcrumb {
-                margin-left: -$desktop-nice-padding;
-                margin-right: -$desktop-nice-padding;
-                padding-left: math.div($desktop-nice-padding, 2);
-            }
-        }
+    .breadcrumb {
+      margin-bottom: 1rem;
+      padding-left: math.div(
+        $desktop-nice-padding,
+        2
+      ); // rather than padding-left: revert;
     }
+  }
 
-    &.header-with-breadcrumb {
-        padding-top: 0;
+  &.tab-merged,
+  &.no-border {
+    border: 0;
 
-        .breadcrumb {
-            margin-bottom: 1rem;
-            padding-left: math.div($desktop-nice-padding, 2); // rather than padding-left: revert;
-        }
-    }
+    @include media-breakpoint-down(xs) {
+      // To all hamburger menu to be visible
+      padding-left: 1.6em;
+      padding-right: 1.6em;
+      padding-top: 11px;
 
-    &.tab-merged,
-    &.no-border {
-        border: 0;
+      .nice-padding {
+        margin-left: -$desktop-nice-padding;
+      }
+    }
+  }
 
-        @include media-breakpoint-down(xs) {
-            // To all hamburger menu to be visible
-            padding-left: 1.6em;
-            padding-right: 1.6em;
-            padding-top: 11px;
+  &.merged.no-border {
+    padding-bottom: 0;
+  }
 
-            .nice-padding {
-                margin-left: -$desktop-nice-padding;
-            }
-        }
+  &.no-v-padding {
+    padding-top: 0;
+    padding-bottom: 0;
+  }
 
-    }
+  .button {
+    background-color: $color-teal-darker;
 
-    &.merged.no-border {
-        padding-bottom: 0;
+    &:hover {
+      background-color: $color-teal-dark;
     }
+  }
 
-    &.no-v-padding {
-        padding-top: 0;
-        padding-bottom: 0;
-    }
+  label {
+    @include visuallyhidden();
+  }
 
-    .button {
-        background-color: $color-teal-darker;
+  input[type='text'],
+  select {
+    border-width: 0;
 
-        &:hover {
-            background-color: $color-teal-dark;
-        }
+    &:focus {
+      background-color: $color-white;
     }
+  }
 
-    label {
-        @include visuallyhidden();
-    }
+  .error-message {
+    color: inherit;
+  }
 
-    input[type=text],
-    select {
-        border-width: 0;
+  .fields {
+    margin-top: -0.5em;
 
-        &:focus {
-            background-color: $color-white;
-        }
+    li {
+      padding-bottom: 0;
     }
 
-    .error-message {
-        color: inherit;
+    .field {
+      padding: 0;
     }
+  }
 
-    .fields {
-        margin-top: -0.5em;
-
-        li {
-            padding-bottom: 0;
-        }
+  .field-content {
+    width: auto;
+    padding: 0;
+  }
 
-        .field {
-            padding: 0;
-        }
+  .last-updated {
+    ul {
+      padding: 0;
     }
 
-    .field-content {
-        width: auto;
-        padding: 0;
+    li {
+      display: inline;
+      margin-right: 2em;
     }
 
-    .last-updated {
-        ul {
-            padding: 0;
-        }
-
-        li {
-            display: inline;
-            margin-right: 2em;
-        }
-
-        .avatar.small {
-            margin-left: 0;
-        }
+    .avatar.small {
+      margin-left: 0;
+    }
 
-        a {
-            font-weight: bold;
-        }
+    a {
+      font-weight: bold;
     }
+  }
 }
 
 // Media for Windows High Contrast
 @media (forced-colors: $media-forced-colours) {
-    header .field-content {
-        border: 0.1em solid $system-color-link-text;
-    }
+  header .field-content {
+    border: 0.1em solid $system-color-link-text;
+  }
 }
 
 @include media-breakpoint-up(sm) {
-    header {
-        padding-top: 1.5em;
-        padding-bottom: 1.5em;
+  header {
+    padding-top: 1.5em;
+    padding-bottom: 1.5em;
 
-        .left {
-            float: left;
-            margin-right: 0;
+    .left {
+      float: left;
+      margin-right: 0;
 
-            &:first-child {
-                padding-bottom: 0;
-                float: left;
-            }
-        }
+      &:first-child {
+        padding-bottom: 0;
+        float: left;
+      }
+    }
 
-        .second {
-            clear: none;
+    .second {
+      clear: none;
 
-            .right,
-            .left {
-                float: right;
-            }
-        }
+      .right,
+      .left {
+        float: right;
+      }
+    }
 
-        h1.icon:before {
-            display: inline-block;
-        }
+    h1.icon:before {
+      display: inline-block;
+    }
 
-        .col3 {
-            @include column(3);
-        }
+    .col3 {
+      @include column(3);
+    }
 
-        .col3.actionbutton {
-            width: auto;
-        }
+    .col3.actionbutton {
+      width: auto;
+    }
 
-        .col6 {
-            @include column(6);
-        }
+    .col6 {
+      @include column(6);
+    }
 
-        .col9 {
-            @include column(9);
-        }
+    .col9 {
+      @include column(9);
     }
+  }
 }
 
 .header-title {
-    @include media-breakpoint-down(xs) {
-        padding-left: $mobile-nav-indent;
-    }
-
-    &-icon {
-        @include svg-icon();
-        margin-right: 0.5em;
-    }
+  @include media-breakpoint-down(xs) {
+    padding-left: $mobile-nav-indent;
+  }
+
+  &-icon {
+    @include svg-icon();
+    margin-right: 0.5em;
+  }
 }

+ 55 - 55
client/scss/components/_help-block.scss

@@ -1,88 +1,88 @@
-@use "sass:color";
-@use "sass:map";
+@use 'sass:color';
+@use 'sass:map';
 // Help text formatters
 .help-block {
-    padding: 1em;
-    margin: 1em 0;
-    clear: both;
-    color: $color-text-base;
+  padding: 1em;
+  margin: 1em 0;
+  clear: both;
+  color: $color-text-base;
 
-    p {
-        margin-top: 0;
+  p {
+    margin-top: 0;
 
-        &:last-child {
-            margin-bottom: 0;
-        }
+    &:last-child {
+      margin-bottom: 0;
     }
+  }
 
-    a {
-        color: $color-link;
-    }
+  a {
+    color: $color-link;
+  }
 }
 
 .help-info,
 .help-warning,
 .help-critical {
-    border-radius: 3px;
-    padding-left: 3.5em;
-    position: relative;
+  border-radius: 3px;
+  padding-left: 3.5em;
+  position: relative;
 
-    &:before {
-        font-family: $font-wagtail-icons;
-        position: absolute;
-        left: 1em;
-        top: 0.7em;
-        content: map.get($icons, 'help');
-        font-size: 1.4em;
-    }
+  &:before {
+    font-family: $font-wagtail-icons;
+    position: absolute;
+    left: 1em;
+    top: 0.7em;
+    content: map.get($icons, 'help');
+    font-size: 1.4em;
+  }
 }
 
 .help-info {
-    background-color: color.adjust($color-blue, $lightness: 30%);
+  background-color: color.adjust($color-blue, $lightness: 30%);
 
-    &:before {
-        color: $color-blue;
-    }
+  &:before {
+    color: $color-blue;
+  }
 }
 
 .help-warning {
-    background-color: color.adjust($color-orange, $lightness: 30%);
+  background-color: color.adjust($color-orange, $lightness: 30%);
 
-    &:before {
-        color: $color-orange;
-        content: map.get($icons, 'warning');
-    }
+  &:before {
+    color: $color-orange;
+    content: map.get($icons, 'warning');
+  }
 }
 
 .help-critical {
-    background-color: color.adjust($color-red, $lightness: 40%);
+  background-color: color.adjust($color-red, $lightness: 40%);
 
-    &:before {
-        color: $color-red;
-        content: map.get($icons, 'warning');
-    }
+  &:before {
+    color: $color-red;
+    content: map.get($icons, 'warning');
+  }
 }
 
 // Media for Windows High Contrast
 @media (forced-colors: $media-forced-colours) {
-    .help-block {
-        forced-color-adjust: none;
-        border: 1px solid $system-color-link-text; // ensure visible separation in Windows High Contrast mode
-        background-color: transparent;
-        color: $color-white;
+  .help-block {
+    forced-color-adjust: none;
+    border: 1px solid $system-color-link-text; // ensure visible separation in Windows High Contrast mode
+    background-color: transparent;
+    color: $color-white;
 
-        &:before {
-            color: currentColor;
-        }
+    &:before {
+      color: currentColor;
     }
+  }
 
-    .help-warning {
-        color: $color-text-warning-forced-color;
-        border-color: $color-text-warning-forced-color;
-    }
+  .help-warning {
+    color: $color-text-warning-forced-color;
+    border-color: $color-text-warning-forced-color;
+  }
 
-    .help-critical {
-        color: $color-text-error-forced-color;
-        border-color: $color-text-error-forced-color;
-    }
+  .help-critical {
+    color: $color-text-error-forced-color;
+    border-color: $color-text-error-forced-color;
+  }
 }

+ 14 - 14
client/scss/components/_human-readable-date.scss

@@ -1,21 +1,21 @@
 // Displays 'timesince' formatted date with full date on hover
 .human-readable-date {
-    overflow: hidden;
-    display: block;
-    position: relative;
+  overflow: hidden;
+  display: block;
+  position: relative;
 
-    &:before {
-        position: absolute;
-        display: none;
-        content: attr(title);
-    }
+  &:before {
+    position: absolute;
+    display: none;
+    content: attr(title);
+  }
 
-    &:hover {
-        visibility: hidden;
+  &:hover {
+    visibility: hidden;
 
-        &:before {
-            visibility: visible;
-            display: block;
-        }
+    &:before {
+      visibility: visible;
+      display: block;
     }
+  }
 }

+ 91 - 92
client/scss/components/_icons.scss

@@ -1,25 +1,25 @@
-@use "sass:string";
+@use 'sass:string';
 
 @font-face {
-    font-family: 'wagtail';
-    src: url('../fonts/wagtail.woff') format('woff');
-    font-weight: normal;
-    font-style: normal;
+  font-family: 'wagtail';
+  src: url('../fonts/wagtail.woff') format('woff');
+  font-weight: normal;
+  font-style: normal;
 }
 
 // Set SVG icons to use the current text color in the location they appear as
 // their default fill color. Can be overridden for a specific icon by either
 // the color or fill properties.
 .icon {
-    fill: currentColor;
+  fill: currentColor;
 }
 
 .icon.teal {
-    color: $color-teal;
+  color: $color-teal;
 }
 
 .icon.white {
-    color: #fff;
+  color: #fff;
 }
 
 .icon:before,
@@ -27,14 +27,14 @@
 .hallotoolbar [class^='icon-'],
 .hallotoolbar [class*=' icon-']:before,
 .hallotoolbar [class^='icon-']:before {
-    @include icon(); // from _mixins.scss
+  @include icon(); // from _mixins.scss
 }
 
 // stylelint-disable-next-line no-duplicate-selectors
 .icon:after,
 .hallotoolbar [class^='icon-']:after,
 .hallotoolbar [class^='icon-']:after {
-    text-align: right;
+  text-align: right;
 }
 
 // stylelint-disable-next-line no-duplicate-selectors
@@ -42,150 +42,149 @@
 .hallotoolbar [class*=' icon-']:before,
 .hallotoolbar [class*=' icon-']:before,
 .hallotoolbar [class^='icon-']:before {
-    vertical-align: -10%;
-    margin-right: 0;
+  vertical-align: -10%;
+  margin-right: 0;
 }
 
-
 // =============================================================================
 // Icon factory methods
 // =============================================================================
 
 @each $icon, $content in $icons {
-    .icon-#{$icon}:before {
-        content: string.quote(#{$content});
-    }
+  .icon-#{$icon}:before {
+    content: string.quote(#{$content});
+  }
 }
 
 @each $icon, $content in $icons-after {
-    .icon-#{$icon}:after {
-        content: string.quote(#{$content});
-    }
+  .icon-#{$icon}:after {
+    content: string.quote(#{$content});
+  }
 }
 
-
 // =============================================================================
 // Custom config for various icons
 // =============================================================================
 .icon-download {
-    // Credit: Icon made by Freepik from Flaticon.com
+  // Credit: Icon made by Freepik from Flaticon.com
 }
 
 .icon-view:before,
-.icon-no-view:before {  // icon-font
-    vertical-align: -3.5px;
-    font-size: 1.1rem;
+.icon-no-view:before {
+  // icon-font
+  vertical-align: -3.5px;
+  font-size: 1.1rem;
 }
 
 .icon-spinner:after,
-.icon-spinner:before {  // iconfont
-    width: 1em;
-    animation: spin-wag 0.5s infinite linear;
-    display: inline-block;
+.icon-spinner:before {
+  // iconfont
+  width: 1em;
+  animation: spin-wag 0.5s infinite linear;
+  display: inline-block;
 }
 
-svg.icon-spinner { // TODO: leave only class when iconfont styles are removed
-    animation: spin-wag 0.5s infinite linear;
+svg.icon-spinner {
+  // TODO: leave only class when iconfont styles are removed
+  animation: spin-wag 0.5s infinite linear;
 }
 
 .icon-horizontalrule:before {
-    font-family: $font-sans;
+  font-family: $font-sans;
 }
 
-
 .icon-larger:before {
-    font-size: 1.5em;
+  font-size: 1.5em;
 }
 
-.icon.text-replace { // iconfont
-    font-size: 0;
-    line-height: 0;
-    overflow: hidden;
+.icon.text-replace {
+  // iconfont
+  font-size: 0;
+  line-height: 0;
+  overflow: hidden;
 
-    &:before {
-        margin-right: 0;
-        font-size: 1rem;
-        display: inline-block;
-        width: 100%;
-        line-height: 1.2em;
-        text-align: center;
-        vertical-align: middle;
-    }
+  &:before {
+    margin-right: 0;
+    font-size: 1rem;
+    display: inline-block;
+    width: 100%;
+    line-height: 1.2em;
+    text-align: center;
+    vertical-align: middle;
+  }
 }
 
 .text-replace {
-    font-size: 0;
-    line-height: 0;
-    overflow: hidden;
+  font-size: 0;
+  line-height: 0;
+  overflow: hidden;
 
-    .icon {
-        @include svg-icon(1rem, middle);
-    }
+  .icon {
+    @include svg-icon(1rem, middle);
+  }
 }
 
 @keyframes spin-wag {
-    0% {
-        transform: rotate(0deg);
-    }
+  0% {
+    transform: rotate(0deg);
+  }
 
-    100% {
-        transform: rotate(360deg);
-    }
+  100% {
+    transform: rotate(360deg);
+  }
 }
 
-
-
 .icon-spinner:after {
-    display: inline-block;
-    line-height: 1;
+  display: inline-block;
+  line-height: 1;
 }
 
 // CSS-only circled question mark.
 // <span class="icon-help-inverse" aria-hidden="true"></span>
 .icon-help-inverse {
-    $size: 15px;
+  $size: 15px;
 
-    &:before {
-        display: inline-block;
-        width: $size;
-        height: $size;
-        line-height: $size;
-        font-size: 1.1em;
-        text-align: center;
-        border-radius: 100%;
-        color: $color-grey-2;
-        border: 1px solid currentColor;
-    }
+  &:before {
+    display: inline-block;
+    width: $size;
+    height: $size;
+    line-height: $size;
+    font-size: 1.1em;
+    text-align: center;
+    border-radius: 100%;
+    color: $color-grey-2;
+    border: 1px solid currentColor;
+  }
 }
 
 // stylelint-disable-next-line no-duplicate-selectors
 .icon {
-    &.initial {
-        @include svg-icon(1em);
-        vertical-align: initial;
-    }
+  &.initial {
+    @include svg-icon(1em);
+    vertical-align: initial;
+  }
 
-    &.default {
-        @include svg-icon(1.5em);
-    }
+  &.default {
+    @include svg-icon(1.5em);
+  }
 
-    &--flipped {
-        transform: scaleX(-1);
-    }
+  &--flipped {
+    transform: scaleX(-1);
+  }
 }
 
 .icon.locale-error {
-    vertical-align: text-top;
-    margin-right: 0.5em;
-    width: 1.5em;
-    height: 1.5em;
-    color: $color-red;
+  vertical-align: text-top;
+  margin-right: 0.5em;
+  width: 1.5em;
+  height: 1.5em;
+  color: $color-red;
 }
 
 // Media for Windows High Contrast mode
 
 @media (forced-colors: $media-forced-colours) {
-    .icon {
-        fill: $system-color-link-text;
-    }
+  .icon {
+    fill: $system-color-link-text;
+  }
 }

+ 27 - 28
client/scss/components/_indicator.scss

@@ -1,9 +1,8 @@
-@use "sass:math";
+@use 'sass:math';
 // =============================================================================
 // Indicator light
 // =============================================================================
 
-
 // =============================================================================
 // Indicator light
 // =============================================================================
@@ -11,47 +10,47 @@ $c-indicator-size: 0.625em;
 $c-indicator-margin: 0.25rem;
 
 .c-indicator {
-    display: inline-block;
-    border-radius: 50rem;
-    width: $c-indicator-size;
-    height: $c-indicator-size;
-    margin-top: -0.125rem;
-    margin-right: $c-indicator-margin;
-    font-size: 1rem;
-    vertical-align: middle;
+  display: inline-block;
+  border-radius: 50rem;
+  width: $c-indicator-size;
+  height: $c-indicator-size;
+  margin-top: -0.125rem;
+  margin-right: $c-indicator-margin;
+  font-size: 1rem;
+  vertical-align: middle;
 }
 
 // =============================================================================
 // States
 // =============================================================================
 .is-absent .c-indicator {
-    background: $color-state-absent;
+  background: $color-state-absent;
 }
 
 .is-live .c-indicator {
-    background: $color-state-live;
+  background: $color-state-live;
 }
 
 .is-draft .c-indicator {
-    background: $color-state-draft;
+  background: $color-state-draft;
 }
 
 // This is hipster. But it works.
 .is-live\+draft .c-indicator {
-    background: $color-state-draft;
-    position: relative;
+  background: $color-state-draft;
+  position: relative;
 
-    &:before {
-        content: '';
-        width: math.div($c-indicator-size, 2);
-        height: $c-indicator-size;
-        position: absolute;
-        top: 0;
-        left: 0;
-        border-bottom-left-radius: 50rem;
-        border-top-left-radius: 50rem;
-        background: $color-state-live;
-        transform: rotate(45deg);
-        transform-origin: 100% 50%;
-    }
+  &:before {
+    content: '';
+    width: math.div($c-indicator-size, 2);
+    height: $c-indicator-size;
+    position: absolute;
+    top: 0;
+    left: 0;
+    border-bottom-left-radius: 50rem;
+    border-top-left-radius: 50rem;
+    background: $color-state-live;
+    transform: rotate(45deg);
+    transform-origin: 100% 50%;
+  }
 }

+ 5 - 5
client/scss/components/_link.legacy.scss

@@ -1,12 +1,12 @@
 // makes a link look like regular text
 .nolink {
-    color: $color-text-base;
+  color: $color-text-base;
 
-    &:hover {
-        color: $color-teal;
-    }
+  &:hover {
+    color: $color-teal;
+  }
 }
 
 a.underlined {
-    border-bottom: 1px solid currentColor;
+  border-bottom: 1px solid currentColor;
 }

+ 573 - 581
client/scss/components/_listing.scss

@@ -1,792 +1,784 @@
 // General listings, like for pages, images or snippets
 ul.listing {
-    @include unlist();
+  @include unlist();
 }
 
 .listing {
-    margin-bottom: 2em;
+  margin-bottom: 2em;
+  color: $color-text-base;
+  font-size: 0.95em;
+
+  ul {
+    list-style-type: none;
+    padding-left: 0;
+    // @include unlist();
+  }
+
+  > li {
+    padding: 1em 0;
+    border-bottom: 1px dashed $color-input-border;
+  }
+
+  h3 {
+    margin: 0;
+    font-size: 1em;
+  }
+
+  td,
+  th {
+    padding: 1.2em 1em;
+
+    &.no-padding {
+      padding: 0;
+    }
+  }
+
+  &.small td,
+  th {
+    padding: 0.6em 1em;
+  }
+
+  thead {
+    font-size: 1.1em;
     color: $color-text-base;
-    font-size: 0.95em;
-
-    ul {
-        list-style-type: none;
-        padding-left: 0;
-        // @include unlist();
-    }
+    border-bottom: 1px solid $color-grey-4;
 
-    > li {
-        padding: 1em 0;
-        border-bottom: 1px dashed $color-input-border;
+    th {
+      font-size: 0.9em;
+      text-align: left;
+      font-weight: normal;
+      white-space: nowrap;
+      text-transform: uppercase;
     }
 
-    h3 {
-        margin: 0;
-        font-size: 1em;
+    th.children {
+      border: 0;
     }
 
-    td,
-    th {
-        padding: 1.2em 1em;
+    th a {
+      text-decoration: none;
+      color: inherit;
+      position: relative;
 
-        &.no-padding {
-            padding: 0;
-        }
+      &.icon:after {
+        opacity: 0.5;
+        right: 0;
+      }
     }
+  }
 
-    &.small td,
-    th {
-        padding: 0.6em 1em;
-    }
+  &.full-width td:first-child,
+  &.full-width th:first-child {
+    padding-left: 20px;
+  }
 
-    thead {
-        font-size: 1.1em;
-        color: $color-text-base;
-        border-bottom: 1px solid $color-grey-4;
+  &.full-width {
+    margin-bottom: -3em; // this negates the padding added to the bottom of .content
+  }
 
-        th {
-            font-size: 0.9em;
-            text-align: left;
-            font-weight: normal;
-            white-space: nowrap;
-            text-transform: uppercase;
-        }
+  &.full-width th {
+    background-color: $color-thead-bg;
+  }
 
-        th.children {
-            border: 0;
-        }
+  .table-headers {
+    border-bottom: 1px solid $color-grey-4;
+  }
 
-        th a {
-            text-decoration: none;
-            color: inherit;
-            position: relative;
+  tbody {
+    border-bottom: 1px dashed $color-input-border;
 
-            &.icon:after {
-                opacity: 0.5;
-                right: 0;
-            }
-        }
-    }
+    tr {
+      border-top: 1px dashed $color-input-border;
 
-    &.full-width td:first-child,
-    &.full-width th:first-child {
-        padding-left: 20px;
-    }
+      &:first-child {
+        border-top: 1px dashed $color-input-border;
+      }
 
-    &.full-width {
-        margin-bottom: -3em; // this negates the padding added to the bottom of .content
+      &:hover {
+        background-color: #fcfcfc;
+      }
     }
 
-    &.full-width th {
-        background-color: $color-thead-bg;
-    }
+    tr.selected {
+      background-color: #c8eae9;
 
-    .table-headers {
-        border-bottom: 1px solid  $color-grey-4;
+      &:hover {
+        background-color: #b5e3e2;
+      }
     }
+  }
 
-    tbody {
-        border-bottom: 1px dashed $color-input-border;
+  &.full-width tbody {
+    border: 0;
+  }
 
+  &.chooser {
+    tbody .title a {
+      @include transition(none);
+      display: block;
+    }
 
-        tr {
-            border-top: 1px dashed $color-input-border;
-
-            &:first-child {
-                border-top: 1px dashed $color-input-border;
-            }
+    tbody tr:hover {
+      background-color: $color-teal;
+      color: $color-white;
 
-            &:hover {
-                background-color: #fcfcfc;
-            }
-        }
+      .title a,
+      .title a:hover {
+        color: $color-white;
+      }
 
-        tr.selected {
-            background-color: #c8eae9;
+      .parent a {
+        color: $color-white;
+      }
 
-            &:hover {
-                background-color: #b5e3e2;
-            }
-        }
+      .status-tag {
+        border-color: $color-white;
+      }
     }
 
-    &.full-width tbody {
-        border: 0;
+    tbody tr.disabled td {
+      opacity: 0.25;
     }
 
-    &.chooser {
-        tbody .title a {
-            @include transition(none);
-            display: block;
-        }
-
-        tbody tr:hover {
-            background-color: $color-teal;
-            color: $color-white;
-
-            .title a,
-            .title a:hover {
-                color: $color-white;
-            }
+    tbody tr.disabled td.children {
+      opacity: 1;
+    }
 
-            .parent a {
-                color: $color-white;
-            }
+    tbody tr.disabled:hover {
+      background-color: inherit;
+      color: inherit;
 
-            .status-tag {
-                border-color: $color-white;
-            }
-        }
+      .title {
+        cursor: not-allowed;
+      }
 
-        tbody tr.disabled td {
-            opacity: 0.25;
-        }
+      .status-tag {
+        border-color: inherit;
+      }
+    }
+  }
 
-        tbody tr.disabled td.children {
-            opacity: 1;
-        }
+  &.small tbody tr {
+    font-size: 1em;
+  }
 
-        tbody tr.disabled:hover {
-            background-color: inherit;
-            color: inherit;
+  &.full-width .divider td {
+    padding-left: 20px;
+  }
 
-            .title {
-                cursor: not-allowed;
-            }
+  // specific columns
+  .bulk {
+    padding-right: 0;
 
-            .status-tag {
-                border-color: inherit;
-            }
-        }
+    label {
+      font-size: 1em;
+      display: block;
+      width: 100%;
+      position: relative;
     }
 
-    &.small tbody tr {
-        font-size: 1em;
+    label span {
+      @include visuallyhidden();
     }
 
-    &.full-width .divider td {
-        padding-left: 20px;
+    input {
+      margin-top: 3px;
     }
+  }
 
-    // specific columns
-    .bulk {
-        padding-right: 0;
+  .title {
+    word-break: break-word;
 
-        label {
-            font-size: 1em;
-            display: block;
-            width: 100%;
-            position: relative;
-        }
+    .title-wrapper,
+    h2 {
+      text-transform: none;
+      margin: 0;
+      font-size: 1.15em;
+      font-weight: 600;
+      color: $color-text-base;
+      line-height: 1.5em;
 
-        label span {
-            @include visuallyhidden();
-        }
+      a {
+        color: inherit;
+        text-decoration: none;
 
-        input {
-            margin-top: 3px;
+        // stylelint-disable max-nesting-depth
+        &:hover {
+          color: $color-link;
         }
+      }
     }
+  }
 
-    .title {
-        word-break: break-word;
-
-        .title-wrapper,
-        h2 {
-            text-transform: none;
-            margin: 0;
-            font-size: 1.15em;
-            font-weight: 600;
-            color: $color-text-base;
-            line-height: 1.5em;
-
-            a {
-                color: inherit;
-                text-decoration: none;
-
-                // stylelint-disable max-nesting-depth
-                &:hover {
-                    color: $color-link;
-                }
-            }
-        }
+  .actions {
+    @include clearfix();
+    margin-top: 0.8em;
+    text-transform: uppercase;
+    margin-bottom: -0.5em;
+    font-size: 0.8rem;
+
+    a {
+      text-decoration: none;
     }
 
-    .actions {
-        @include clearfix();
-        margin-top: 0.8em;
-        text-transform: uppercase;
-        margin-bottom: -0.5em;
-        font-size: 0.8rem;
-
-        a {
-            text-decoration: none;
-        }
+    > li {
+      float: left;
+      padding: 0 0.5em 0 0;
+      margin: 0 0 0.5em;
+
+      // line-height: 1em;
+    }
+  }
 
-        > li {
-            float: left;
-            padding: 0 0.5em 0 0;
-            margin: 0 0 0.5em;
+  &--inline-actions .actions {
+    display: inline-block;
+    margin-top: 0;
+    vertical-align: inherit;
+  }
 
-            // line-height: 1em;
-        }
+  .button-secondary {
+    border-color: $color-grey-3;
+    background: $color-white;
+
+    &.no:hover {
+      border-color: $color-button-no-hover;
+      background-color: $color-button-no-hover;
+      color: $color-white;
     }
 
-    &--inline-actions .actions {
-        display: inline-block;
-        margin-top: 0;
-        vertical-align: inherit;
+    &:hover {
+      border-color: $color-teal;
+      background-color: $color-teal;
     }
+  }
 
-    .button-secondary {
-        border-color: $color-grey-3;
-        background: $color-white;
+  // stylelint-disable-next-line no-duplicate-selectors
+  .button-secondary {
+    background-color: $color-white;
+  }
 
-        &.no:hover {
-            border-color: $color-button-no-hover;
-            background-color: $color-button-no-hover;
-            color: $color-white;
-        }
+  .moderate-actions form {
+    float: left;
+    margin: 0 1em 1em 0;
+  }
 
-        &:hover {
-            border-color: $color-teal;
-            background-color: $color-teal;
-        }
-    }
+  .children,
+  .no-children {
+    padding: 0;
 
-    // stylelint-disable-next-line no-duplicate-selectors
-    .button-secondary {
-        background-color: $color-white;
+    &:hover {
+      background-color: $color-grey-5;
     }
 
-    .moderate-actions form {
-        float: left;
-        margin: 0 1em 1em 0;
+    a {
+      display: block;
+      padding: 2em 0;
     }
+  }
 
-    .children,
-    .no-children {
-        padding: 0;
+  .children a {
+    color: $color-teal;
+    display: block;
 
-        &:hover {
-            background-color: $color-grey-5;
-        }
+    &:before {
+      font-size: 3rem;
+    }
+  }
 
-        a {
-            display: block;
-            padding: 2em 0;
-        }
+  .no-children a {
+    color: $color-grey-3;
+    display: block;
+
+    &:before {
+      font-size: 1.5rem;
     }
 
-    .children a {
-        color: $color-teal;
-        display: block;
+    &:hover,
+    &:focus {
+      color: $color-teal;
+    }
 
-        &:before {
-            font-size: 3rem;
-        }
+    &:focus {
+      opacity: 1; //opacity is already changed on hover on the parent tr
     }
+  }
 
-    .no-children a {
-        color: $color-grey-3;
-        display: block;
+  &.small .children a:before {
+    font-size: 30px;
+  }
 
-        &:before {
-            font-size: 1.5rem;
-        }
+  th.ord {
+    text-align: center;
 
-        &:hover,
-        &:focus {
-            color: $color-teal;
-        }
+    .icon {
+      width: 15px;
+      height: 18px;
+      vertical-align: middle;
 
-        &:focus {
-            opacity: 1; //opacity is already changed on hover on the parent tr
-        }
+      &::before {
+        margin-right: 2px;
+      }
     }
 
-    &.small .children a:before {
-        font-size: 30px;
+    &--active a,
+    a:hover {
+      color: $color-teal;
     }
 
-    th.ord {
-        text-align: center;
-
-        .icon {
-            width: 15px;
-            height: 18px;
-            vertical-align: middle;
-
-            &::before {
-                margin-right: 2px;
-            }
-        }
+    &--active .icon {
+      opacity: 1;
+    }
+  }
 
-        &--active a,
-        a:hover {
-            color: $color-teal;
-        }
+  .handle {
+    cursor: move;
+    width: 20px;
 
-        &--active .icon {
-            opacity: 1;
-        }
+    &:before {
+      font-size: 20px;
+      color: $color-grey-3;
+      width: 1em;
     }
 
-    .handle {
-        cursor: move;
-        width: 20px;
+    &:hover:before {
+      color: $color-text-base;
+    }
+  }
 
-        &:before {
-            font-size: 20px;
-            color: $color-grey-3;
-            width: 1em;
-        }
+  .ui-sortable-helper {
+    border: 1px dashed $color-input-border;
+    border-width: 1px 0;
 
-        &:hover:before {
-            color: $color-text-base;
-        }
+    td {
+      display: none;
     }
 
-    .ui-sortable-helper {
-        border: 1px dashed $color-input-border;
-        border-width: 1px 0;
+    .ord,
+    .title {
+      display: table-cell;
+    }
+  }
 
-        td {
-            display: none;
+  .dropzone {
+    height: 80px;
+    background-color: $color-grey-1;
 
-        }
+    &:hover {
+      background-color: $color-grey-1;
+    }
 
-        .ord,
-        .title {
-            display: table-cell;
-        }
+    td {
+      padding: 0;
     }
+  }
 
-    .dropzone {
-        height: 80px;
-        background-color: $color-grey-1;
+  table .no-results-message {
+    padding-left: 20px;
+  }
 
-        &:hover {
-            background-color: $color-grey-1;
-        }
+  .unpublished .title-wrapper {
+    opacity: 0.7;
+  }
 
-        td {
-            padding: 0;
-        }
-    }
+  .index {
+    background-color: $color-grey-4;
 
-    table .no-results-message {
-        padding-left: 20px;
-    }
+    .title .title-wrapper {
+      font-size: 1.2em;
+      opacity: 1;
+
+      a {
+        @include transition(opacity 0.2s ease);
+      }
 
-    .unpublished .title-wrapper {
+      a:hover {
         opacity: 0.7;
+      }
     }
 
-    .index {
-        background-color: $color-grey-4;
+    .actions {
+      margin-top: 1em;
+    }
 
-        .title .title-wrapper {
-            font-size: 1.2em;
-            opacity: 1;
+    .button {
+      background-color: $color-white;
+      color: $color-teal;
+      border-color: rgba(0, 0, 0, 0.35);
 
-            a {
-                @include transition(opacity 0.2s ease);
-            }
+      &:hover {
+        color: $color-white;
+        background: $color-teal-darker;
+        border-color: $color-teal-darker;
+      }
 
-            a:hover {
-                opacity: 0.7;
-            }
-        }
+      &:active {
+        color: $color-white;
+        background: #333;
+        border-color: #333;
+      }
 
-        .actions {
-            margin-top: 1em;
-        }
+      &.bicolor {
+        background: $color-teal-darker;
 
-        .button {
-            background-color: $color-white;
-            color: $color-teal;
-            border-color: rgba(0, 0, 0, 0.35);
-
-            &:hover {
-                color: $color-white;
-                background: $color-teal-darker;
-                border-color: $color-teal-darker;
-            }
-
-            &:active {
-                color: $color-white;
-                background: #333;
-                border-color: #333;
-            }
-
-            &.bicolor {
-                background: $color-teal-darker;
-
-                &:active {
-                    color: $color-white;
-                    background: #484848;
-                    border-color: #333;
-                }
-            }
+        &:active {
+          color: $color-white;
+          background: #484848;
+          border-color: #333;
         }
+      }
     }
+  }
 
-    .indicator {
-        margin-right: 0;
-        font-size: 1em;
-        opacity: 0.7;
-    }
+  .indicator {
+    margin-right: 0;
+    font-size: 1em;
+    opacity: 0.7;
+  }
 
-    &.images img {
-        @include transition(border-color 0.2s ease);
-        border: 3px solid $color-white;
-    }
+  &.images img {
+    @include transition(border-color 0.2s ease);
+    border: 3px solid $color-white;
+  }
 }
 
 .image-choice {
-    // Force the link to be displayed as a block, so its focus outline has the right shape.
-    display: block;
-    color: inherit;
-    overflow-wrap: break-word;
-    word-wrap: break-word;
+  // Force the link to be displayed as a block, so its focus outline has the right shape.
+  display: block;
+  color: inherit;
+  overflow-wrap: break-word;
+  word-wrap: break-word;
 }
 
 // stylelint-disable-next-line no-duplicate-selectors
 ul.listing {
-    border-top: 1px dashed $color-input-border;
-    margin-bottom: 2em;
+  border-top: 1px dashed $color-input-border;
+  margin-bottom: 2em;
 }
 
 table.listing {
-    width: 100%;
+  width: 100%;
 }
 
 // explorer specific tweaks
 .page-explorer .listing {
-    position: relative;
+  position: relative;
 
-    .index {
-        color: $color-white;
-        background-color: $color-header-bg;
+  .index {
+    color: $color-white;
+    background-color: $color-header-bg;
 
-        td {
-            padding-top: 1.5em;
-            padding-bottom: 1.5em;
-        }
+    td {
+      padding-top: 1.5em;
+      padding-bottom: 1.5em;
+    }
 
-        .privacy-indicator {
-            font-size: 1em;
-            opacity: 1;
-            position: absolute;
-            right: 10%;
-            top: 2em;
-        }
+    .privacy-indicator {
+      font-size: 1em;
+      opacity: 1;
+      position: absolute;
+      right: 10%;
+      top: 2em;
+    }
 
-        .locked-indicator {
-            font-size: 0.8em;
-        }
+    .locked-indicator {
+      font-size: 0.8em;
+    }
 
-        .title {
-            h2 {
-                color: $color-white;
-                font-size: 1.8em;
-                font-weight: 500;
+    .title {
+      h2 {
+        color: $color-white;
+        font-size: 1.8em;
+        font-weight: 500;
 
-                a:hover {
-                    color: $color-white;
-                }
-            }
+        a:hover {
+          color: $color-white;
         }
+      }
+    }
 
-        .button {
-            background-color: transparent;
-            color: $color-white;
-            border-color: rgba(0, 0, 0, 0.35);
+    .button {
+      background-color: transparent;
+      color: $color-white;
+      border-color: rgba(0, 0, 0, 0.35);
 
-            &:hover {
-                color: $color-white;
-                background: $color-teal-darker;
-            }
+      &:hover {
+        color: $color-white;
+        background: $color-teal-darker;
+      }
 
-            &.bicolor {
-                background: $color-teal-darker;
-            }
-        }
+      &.bicolor {
+        background: $color-teal-darker;
+      }
     }
+  }
 
-    .table-headers {
-        height: 35px;
+  .table-headers {
+    height: 35px;
 
-        .title {
-            padding-left: 0;
-        }
+    .title {
+      padding-left: 0;
     }
+  }
 
-    tbody .title {
-        padding-left: 0;
-    }
+  tbody .title {
+    padding-left: 0;
+  }
 }
 
 .pagination {
-    text-align: center;
+  text-align: center;
 
-    p {
-        margin: 0;
-    }
+  p {
+    margin: 0;
+  }
 
-    ul {
-        @include unlist();
-        margin-top: -1.7em;
-    }
+  ul {
+    @include unlist();
+    margin-top: -1.7em;
+  }
 
-    li {
-        line-height: 1em;
-    }
+  li {
+    line-height: 1em;
+  }
 
-    .prev {
-        float: left;
-    }
+  .prev {
+    float: left;
+  }
 
-    .next {
-        float: right;
-    }
+  .next {
+    float: right;
+  }
 }
 
 .listing.full-width + .pagination {
-    margin-top: 3em;
-    border-top: 1px dashed #d9d9d9;
-    padding: 2em 50px 0;
+  margin-top: 3em;
+  border-top: 1px dashed #d9d9d9;
+  padding: 2em 50px 0;
 }
 
-
 // listing filters
 .listing-filter {
-    @include clearfix();
-    background-color: $color-grey-5;
-    border-width: 1px 0;
-    margin: 3em 0;
+  @include clearfix();
+  background-color: $color-grey-5;
+  border-width: 1px 0;
+  margin: 3em 0;
 }
 
 .filter-title {
-    float: left;
-    text-transform: uppercase;
-    font-size: 0.95em;
-    padding: 1em;
-    margin: 0 1em 0 0;
-    background-color: $color-grey-4;
+  float: left;
+  text-transform: uppercase;
+  font-size: 0.95em;
+  padding: 1em;
+  margin: 0 1em 0 0;
+  background-color: $color-grey-4;
 }
 
 .filter-options {
-    @include unlist();
-    @include clearfix();
-    overflow: hidden;
+  @include unlist();
+  @include clearfix();
+  overflow: hidden;
 
-    li {
-        padding: 0.8em;
-        float: left;
-    }
+  li {
+    padding: 0.8em;
+    float: left;
+  }
 
-    &__icon {
-        width: 1em;
-        height: 1em;
-        margin-right: 0.2em;
-        vertical-align: middle;
-        position: relative;
-        top: -1px;
-    }
+  &__icon {
+    width: 1em;
+    height: 1em;
+    margin-right: 0.2em;
+    vertical-align: middle;
+    position: relative;
+    top: -1px;
+  }
 }
 
-
 @include media-breakpoint-up(sm) {
-    .listing {
-        &.horiz {
-            display: flex;
-            flex-wrap: wrap;
-        }
+  .listing {
+    &.horiz {
+      display: flex;
+      flex-wrap: wrap;
+    }
 
-        &.images {
-            border: 1px solid $color-grey-4;
-            border-width: 0 0 0 1px;
-
-            > li {
-                padding: 1.5em;
-                width: 200px;
-                height: auto;
-                text-align: center;
-                margin-top: -1px;
-                border: 1px solid $color-grey-4;
-                border-width: 1px 1px 1px 0;
-
-                .bulk-action-checkbox {
-                    float: left;
-                    margin: -0.5em 0.5em 0.5em -0.75em;
-                }
-
-                .bulk-action-checkbox + .image-choice {
-                    clear: both;
-                    margin-top: 1em;
-                }
-
-                .image {
-                    text-align: center;
-                    height: 180px;
-
-                    &:before {
-                        content: '';
-                        display: inline-block;
-                        height: 100%;
-                        vertical-align: middle;
-                        margin-right: -0.25em;
-                    }
-
-                    img {
-                        display: inline-block;
-                        vertical-align: middle;
-                    }
-                }
-
-                &:hover {
-                    background-color: #fdfdfd;
-
-                    img {
-                        border-color: $color-teal;
-                    }
-                }
-            }
-        }
+    &.images {
+      border: 1px solid $color-grey-4;
+      border-width: 0 0 0 1px;
 
-        .actions {
-            visibility: hidden;
+      > li {
+        padding: 1.5em;
+        width: 200px;
+        height: auto;
+        text-align: center;
+        margin-top: -1px;
+        border: 1px solid $color-grey-4;
+        border-width: 1px 1px 1px 0;
+
+        .bulk-action-checkbox {
+          float: left;
+          margin: -0.5em 0.5em 0.5em -0.75em;
         }
 
-        .index .actions {
-            visibility: visible;
+        .bulk-action-checkbox + .image-choice {
+          clear: both;
+          margin-top: 1em;
         }
 
+        .image {
+          text-align: center;
+          height: 180px;
 
+          &:before {
+            content: '';
+            display: inline-block;
+            height: 100%;
+            vertical-align: middle;
+            margin-right: -0.25em;
+          }
 
-        td:hover .actions,
-        td:focus-within .actions {
-            visibility: visible;
+          img {
+            display: inline-block;
+            vertical-align: middle;
+          }
         }
 
-        .bulk-action-checkbox {
-            opacity: 0;
+        &:hover {
+          background-color: #fdfdfd;
 
-            &.show,
-            &:checked {
-                opacity: 1;
-            }
+          img {
+            border-color: $color-teal;
+          }
         }
+      }
+    }
 
-        .no-children {
-            border-color: transparent;
+    .actions {
+      visibility: hidden;
+    }
 
-            a {
-                opacity: 0;
-            }
-        }
+    .index .actions {
+      visibility: visible;
+    }
 
-        tr:hover,
-        tr:focus-within {
-            .no-children a,
-            .bulk-action-checkbox {
-                opacity: 1;
-            }
-        }
+    td:hover .actions,
+    td:focus-within .actions {
+      visibility: visible;
+    }
 
-        // used on the image listing
-        li:hover,
-        li:focus-within {
-            .bulk-action-checkbox {
-                opacity: 1;
-            }
-        }
+    .bulk-action-checkbox {
+      opacity: 0;
 
-        tr:hover .children {
-            background-color: $color-teal;
+      &.show,
+      &:checked {
+        opacity: 1;
+      }
+    }
 
-            a:before {
-                color: $color-white;
-            }
-        }
+    .no-children {
+      border-color: transparent;
 
-        td.children:hover {
-            background-color: $color-teal-darker;
-        }
+      a {
+        opacity: 0;
+      }
+    }
 
-        table .no-results-message {
-            padding-left: 50px;
-        }
+    tr:hover,
+    tr:focus-within {
+      .no-children a,
+      .bulk-action-checkbox {
+        opacity: 1;
+      }
+    }
 
-        &.full-width td:first-child,
-        &.full-width th:first-child {
-            padding-left: 25px;
-        }
+    // used on the image listing
+    li:hover,
+    li:focus-within {
+      .bulk-action-checkbox {
+        opacity: 1;
+      }
+    }
 
-        &.full-width .divider td {
-            padding-left: 50px;
-        }
+    tr:hover .children {
+      background-color: $color-teal;
+
+      a:before {
+        color: $color-white;
+      }
+    }
+
+    td.children:hover {
+      background-color: $color-teal-darker;
     }
-}
 
+    table .no-results-message {
+      padding-left: 50px;
+    }
+
+    &.full-width td:first-child,
+    &.full-width th:first-child {
+      padding-left: 25px;
+    }
+
+    &.full-width .divider td {
+      padding-left: 50px;
+    }
+  }
+}
 
 // State
 .listing__item--active {
-    > .actions {
-        visibility: visible;
-    }
+  > .actions {
+    visibility: visible;
+  }
 }
 
 // stylelint-disable no-duplicate-selectors
 
 // Transitions
 .listing {
-    thead .dropdown ul {
-        @include transition(none);
-    }
-
-    .children,
-    .no-children {
-        @include transition(background-color 0.2s ease);
-    }
-
-    .children a,
-    .no-children a {
-        @include transition(all 0.2s ease);
-    }
+  thead .dropdown ul {
+    @include transition(none);
+  }
+
+  .children,
+  .no-children {
+    @include transition(background-color 0.2s ease);
+  }
+
+  .children a,
+  .no-children a {
+    @include transition(all 0.2s ease);
+  }
 }
 
 // Ordering
 td.ord {
-    .handle {
-        // Align with the row's title text, and the column's label.
-        margin-top: -28px;
-        margin-left: 13px;
-    }
+  .handle {
+    // Align with the row's title text, and the column's label.
+    margin-top: -28px;
+    margin-left: 13px;
+  }
 }
 
 table.listing {
-    th.ordered {
-        color: $color-teal;
-
-        &.ascending {
-            &:before {
-                content: '\25B2'; // up arrow
-                display: inline-block;
-                height: 100%;
-                vertical-align: middle;
-            }
-        }
+  th.ordered {
+    color: $color-teal;
 
-        &.descending {
-            &:before {
-                content: '\25BC'; // down arrow
-                display: inline-block;
-                height: 100%;
-                vertical-align: middle;
-            }
-        }
+    &.ascending {
+      &:before {
+        content: '\25B2'; // up arrow
+        display: inline-block;
+        height: 100%;
+        vertical-align: middle;
+      }
+    }
 
+    &.descending {
+      &:before {
+        content: '\25BC'; // down arrow
+        display: inline-block;
+        height: 100%;
+        vertical-align: middle;
+      }
     }
+  }
 }

+ 30 - 30
client/scss/components/_loading-mask.scss

@@ -1,37 +1,37 @@
-@use "sass:map";
+@use 'sass:map';
 // Loading mask: overlays a certain area with a loading spinner and a faded out cover to prevent interaction
 .loading-mask {
-    &.loading {
-        position: relative;
+  &.loading {
+    position: relative;
 
-        &:before,
-        &:after {
-            position: absolute;
-            display: block;
-        }
+    &:before,
+    &:after {
+      position: absolute;
+      display: block;
+    }
 
-        &:before {
-            content: '';
-            top: -5px;
-            left: -5px;
-            bottom: -5px;
-            right: -5px;
-            z-index: 1;
-            background-color: rgba(255, 255, 255, 0.5);
-        }
+    &:before {
+      content: '';
+      top: -5px;
+      left: -5px;
+      bottom: -5px;
+      right: -5px;
+      z-index: 1;
+      background-color: rgba(255, 255, 255, 0.5);
+    }
 
-        &:after {
-            font-size: 30px;
-            width: 30px;
-            line-height: 30px;
-            left: 50%;
-            top: 50%;
-            margin: -15px 0 0 -15px;
-            font-family: $font-wagtail-icons;
-            animation: spin-wag 0.5s infinite linear;
-            content: map.get($icons, 'spinner');
-            z-index: 2;
-            color: $color-teal;
-        }
+    &:after {
+      font-size: 30px;
+      width: 30px;
+      line-height: 30px;
+      left: 50%;
+      top: 50%;
+      margin: -15px 0 0 -15px;
+      font-family: $font-wagtail-icons;
+      animation: spin-wag 0.5s infinite linear;
+      content: map.get($icons, 'spinner');
+      z-index: 2;
+      color: $color-teal;
     }
+  }
 }

+ 97 - 97
client/scss/components/_logo.scss

@@ -1,129 +1,129 @@
 @keyframes tail-wag {
-    from {
-        transform: rotate(-3deg);
-    }
+  from {
+    transform: rotate(-3deg);
+  }
 
-    to {
-        transform: rotate(7deg);
-    }
+  to {
+    transform: rotate(7deg);
+  }
 }
 
 .logo {
-    display: flex;
-    align-items: center;
-    padding: 0.6em 1.2em;
-    color: #aaa;
-    -webkit-font-smoothing: auto;
-    position: relative;
-
-    &:hover {
-        color: $color-white;
-    }
-
-    @include media-breakpoint-up(sm) {
-        display: block;
-        margin: 2em auto;
-        text-align: center;
-    }
+  display: flex;
+  align-items: center;
+  padding: 0.6em 1.2em;
+  color: #aaa;
+  -webkit-font-smoothing: auto;
+  position: relative;
+
+  &:hover {
+    color: $color-white;
+  }
+
+  @include media-breakpoint-up(sm) {
+    display: block;
+    margin: 2em auto;
+    text-align: center;
+  }
 }
 
 .wagtail-logo-container__mobile {
-    margin-right: 10px;
-    background-color: #555;
-    border-radius: 50%;
-    padding: 5px 7.5px;
-
-    .wagtail-logo {
-        width: 20px;
-        float: left;
-        border: 0;
-    }
+  margin-right: 10px;
+  background-color: #555;
+  border-radius: 50%;
+  padding: 5px 7.5px;
+
+  .wagtail-logo {
+    width: 20px;
+    float: left;
+    border: 0;
+  }
 }
 
 .wagtail-logo-container__desktop {
-    position: relative;
-    width: 100px;
+  position: relative;
+  width: 100px;
+  height: 100px;
+  background-color: #555;
+  border-radius: 50%;
+  margin: 0 auto;
+  transition: all 0.25s cubic-bezier(0.28, 0.15, 0, 2.1);
+
+  .page404__bg & {
+    background-color: transparent;
+  }
+
+  .wagtail-logo-container-inner {
+    width: 52px;
     height: 100px;
-    background-color: #555;
-    border-radius: 50%;
-    margin: 0 auto;
-    transition: all 0.25s cubic-bezier(0.28, 0.15, 0, 2.1);
+    margin: auto;
+    position: relative;
 
     .page404__bg & {
-        background-color: transparent;
+      width: auto;
+      height: auto;
+      position: static;
+    }
+  }
+
+  .wagtail-logo {
+    display: block;
+    left: 0;
+    top: 0;
+    width: 100%;
+    height: 100%;
+    position: absolute;
+    transition: inherit;
+
+    &.wagtail-logo__eye--open {
+      // stylelint-disable-next-line declaration-no-important
+      display: inline !important; // doesn't work without `!important`, likely a specificity issue
     }
 
-    .wagtail-logo-container-inner {
-        width: 52px;
-        height: 100px;
-        margin: auto;
-        position: relative;
+    &.wagtail-logo__eye--closed {
+      // stylelint-disable-next-line declaration-no-important
+      display: none !important;
+    }
+  }
 
-        .page404__bg & {
-            width: auto;
-            height: auto;
-            position: static;
-        }
+  // Wagtail 'serious' animation (nod):
+  &.logo-serious {
+    &:hover {
+      transform: rotate(4deg);
     }
+  }
 
-    .wagtail-logo {
-        display: block;
-        left: 0;
-        top: 0;
-        width: 100%;
-        height: 100%;
-        position: absolute;
-        transition: inherit;
+  // Wagtail 'playful' animation (tail-wag, triggered by JS in base.html):
+  &.logo-playful {
+    &:hover {
+      transform: rotate(8deg);
+      transition: transform 1.2s ease;
+
+      .wagtail-logo {
+        // stylelint-disable max-nesting-depth
+        &.wagtail-logo__tail {
+          animation: tail-wag 0.09s alternate;
+          animation-iteration-count: infinite;
+        }
 
         &.wagtail-logo__eye--open {
-            // stylelint-disable-next-line declaration-no-important
-            display: inline !important; // doesn't work without `!important`, likely a specificity issue
+          // stylelint-disable-next-line declaration-no-important
+          display: none !important;
         }
 
         &.wagtail-logo__eye--closed {
-            // stylelint-disable-next-line declaration-no-important
-            display: none !important;
-        }
-    }
-
-    // Wagtail 'serious' animation (nod):
-    &.logo-serious {
-        &:hover {
-            transform: rotate(4deg);
-        }
-    }
-
-    // Wagtail 'playful' animation (tail-wag, triggered by JS in base.html):
-    &.logo-playful {
-        &:hover {
-            transform: rotate(8deg);
-            transition: transform 1.2s ease;
-
-            .wagtail-logo {
-                // stylelint-disable max-nesting-depth
-                &.wagtail-logo__tail {
-                    animation: tail-wag 0.09s alternate;
-                    animation-iteration-count: infinite;
-                }
-
-                &.wagtail-logo__eye--open {
-                    // stylelint-disable-next-line declaration-no-important
-                    display: none !important;
-                }
-
-                &.wagtail-logo__eye--closed {
-                    // stylelint-disable-next-line declaration-no-important
-                    display: inline !important;
-                }
-            }
+          // stylelint-disable-next-line declaration-no-important
+          display: inline !important;
         }
+      }
     }
+  }
 }
 
 // Media for Windows High Contrast mode
 
 @media (forced-colors: $media-forced-colours) {
-    .wagtail-logo-container__desktop {
-        background-color: $system-color-link-text;
-    }
+  .wagtail-logo-container__desktop {
+    background-color: $system-color-link-text;
+  }
 }

+ 445 - 449
client/scss/components/_main-nav.scss

@@ -1,518 +1,516 @@
-@use "sass:map";
+@use 'sass:map';
 
 .nav-wrapper {
-    position: relative;
-    margin-left: -$menu-width;
-    width: $menu-width;
-    float: left;
-    display: flex;
-    flex-direction: column;
-    height: 100%;
+  position: relative;
+  margin-left: -$menu-width;
+  width: $menu-width;
+  float: left;
+  display: flex;
+  flex-direction: column;
+  height: 100%;
+  background: $nav-grey-1;
+
+  .inner {
     background: $nav-grey-1;
+    border-right: 1px solid transparent; // ensure visible separation in Windows High Contrast mode
 
-    .inner {
-        background: $nav-grey-1;
-        border-right: 1px solid transparent; // ensure visible separation in Windows High Contrast mode
-
-        @include media-breakpoint-up(sm) {
-            // On medium, make it possible for the nav links to scroll.
-            display: flex;
-            flex-flow: column nowrap;
-        }
+    @include media-breakpoint-up(sm) {
+      // On medium, make it possible for the nav links to scroll.
+      display: flex;
+      flex-flow: column nowrap;
     }
+  }
 }
 
 .nav-toggle.icon {
-    position: absolute;
-    padding-left: $mobile-nice-padding;
-    cursor: pointer;
-    border: 1px solid transparent; // ensure visible separation in Windows High Contrast mode
-    background-color: transparent;
+  position: absolute;
+  padding-left: $mobile-nice-padding;
+  cursor: pointer;
+  border: 1px solid transparent; // ensure visible separation in Windows High Contrast mode
+  background-color: transparent;
 
-    &:before {
-        position: relative;
-        top: 3px;
-        font-size: 40px;
-        color: $color-white;
-        line-height: 40px;
-        content: '\2261';
-    }
+  &:before {
+    position: relative;
+    top: 3px;
+    font-size: 40px;
+    color: $color-white;
+    line-height: 40px;
+    content: '\2261';
+  }
 }
 
 .nav-main {
-    ul,
-    li {
-        margin: 0;
-        padding: 0;
-        list-style-type: none;
-    }
-
-    li {
-        @include transition(border-color 0.2s ease);
-        position: relative;
-    }
-
-    a {
-        @include transition(border-color 0.2s ease);
-        -webkit-font-smoothing: auto;
-        text-decoration: none;
-        display: block;
-        color: $color-menu-text;
-        padding: 0.8em 1.7em;
-        font-size: 1em;
-        font-weight: normal;
-        // Note, font-weights lower than normal,
-        // and font-size smaller than 1em (80% ~= 12.8px),
-        // makes the strokes thinner than 1px on non-retina screens
-        // making the text semi-transparent
-        &:hover,
-        &:focus {
-            background-color: $nav-item-hover-bg;
-            color: $color-white;
-            text-shadow: -1px -1px 0 rgba(0, 0, 0, 0.3);
-        }
-    }
+  ul,
+  li {
+    margin: 0;
+    padding: 0;
+    list-style-type: none;
+  }
 
-    .menu-item a {
-        position: relative;
-        white-space: nowrap;
-        border-left: 3px solid transparent;
+  li {
+    @include transition(border-color 0.2s ease);
+    position: relative;
+  }
 
-        &:before {
-            font-size: 1rem;
-            vertical-align: -15%;
-            margin-right: 0.5em;
-        }
+  a {
+    @include transition(border-color 0.2s ease);
+    -webkit-font-smoothing: auto;
+    text-decoration: none;
+    display: block;
+    color: $color-menu-text;
+    padding: 0.8em 1.7em;
+    font-size: 1em;
+    font-weight: normal;
+    // Note, font-weights lower than normal,
+    // and font-size smaller than 1em (80% ~= 12.8px),
+    // makes the strokes thinner than 1px on non-retina screens
+    // making the text semi-transparent
+    &:hover,
+    &:focus {
+      background-color: $nav-item-hover-bg;
+      color: $color-white;
+      text-shadow: -1px -1px 0 rgba(0, 0, 0, 0.3);
+    }
+  }
+
+  .menu-item a {
+    position: relative;
+    white-space: nowrap;
+    border-left: 3px solid transparent;
 
-        // only really used for spinners and settings menu
-        &:after {
-            font-size: 1.5em;
-            margin: 0;
-            position: absolute;
-            right: 0.5em;
-            top: 0.5em;
-            margin-top: 0;
-        }
+    &:before {
+      font-size: 1rem;
+      vertical-align: -15%;
+      margin-right: 0.5em;
+    }
 
+    // only really used for spinners and settings menu
+    &:after {
+      font-size: 1.5em;
+      margin: 0;
+      position: absolute;
+      right: 0.5em;
+      top: 0.5em;
+      margin-top: 0;
     }
+  }
 
-    .menu-active {
-        background: $nav-item-active-bg;
-        text-shadow: -1px -1px 0 rgba(0, 0, 0, 0.3);
+  .menu-active {
+    background: $nav-item-active-bg;
+    text-shadow: -1px -1px 0 rgba(0, 0, 0, 0.3);
 
-        > a {
-            border-left-color: $color-salmon;
-            color: $color-white;
-        }
+    > a {
+      border-left-color: $color-salmon;
+      color: $color-white;
     }
+  }
 
-    .nav-footer-submenu {
-        a {
-            border-left: 3px solid transparent;
-            overflow: hidden;
-            text-overflow: ellipsis;
-            white-space: nowrap;
-
-            &:before {
-                font-size: 1rem;
-                margin-right: 0.5em;
-                vertical-align: -10%;
-            }
-        }
-    }
+  .nav-footer-submenu {
+    a {
+      border-left: 3px solid transparent;
+      overflow: hidden;
+      text-overflow: ellipsis;
+      white-space: nowrap;
 
-    .account {
-        display: none;
+      &:before {
+        font-size: 1rem;
+        margin-right: 0.5em;
+        vertical-align: -10%;
+      }
     }
+  }
 
-    *:focus {
-        @include show-focus-outline-inside;
-    }
+  .account {
+    display: none;
+  }
+
+  *:focus {
+    @include show-focus-outline-inside;
+  }
 }
 
 .icon--menuitem {
-    width: 1.25em;
-    height: 1.25em;
-    margin-right: 0.5em;
-    vertical-align: text-top;
+  width: 1.25em;
+  height: 1.25em;
+  margin-right: 0.5em;
+  vertical-align: text-top;
 }
 
 .icon--submenu-trigger {
-    // The menus are collapsible on desktop only.
-    display: none;
-    @include media-breakpoint-up(sm) {
-        display: block;
-        width: 1.5em;
-        height: 1.5em;
-        position: absolute;
-        top: 0.8125em;
-        right: 0.5em;
-        @include transition(transform 0.3s ease);
-
-        .menu-item.submenu-active & {
-            transform-origin: 50% 50%;
-            transform: rotate(180deg);
-        }
+  // The menus are collapsible on desktop only.
+  display: none;
+  @include media-breakpoint-up(sm) {
+    display: block;
+    width: 1.5em;
+    height: 1.5em;
+    position: absolute;
+    top: 0.8125em;
+    right: 0.5em;
+    @include transition(transform 0.3s ease);
+
+    .menu-item.submenu-active & {
+      transform-origin: 50% 50%;
+      transform: rotate(180deg);
     }
+  }
 }
 
 .icon--submenu-header {
-    display: block;
-    width: 4rem;
-    height: 4rem;
-    margin: 0 auto 0.8em;
-    opacity: 0.15;
+  display: block;
+  width: 4rem;
+  height: 4rem;
+  margin: 0 auto 0.8em;
+  opacity: 0.15;
 }
 
 .nav-submenu {
-    background: $nav-submenu-bg;
-
-    h2 {
-        display: none;
-    }
-
-    .menu-item a {
-        white-space: normal;
-        padding: 0.9em 1.7em 0.9em 4.5em;
+  background: $nav-submenu-bg;
 
-        &:before {
-            margin-left: -1.5em;
-        }
+  h2 {
+    display: none;
+  }
 
-        .icon--menuitem {
-            margin-left: -1.75em;
-        }
+  .menu-item a {
+    white-space: normal;
+    padding: 0.9em 1.7em 0.9em 4.5em;
 
-        &:hover {
-            background-color: rgba(100, 100, 100, 0.2);
-        }
+    &:before {
+      margin-left: -1.5em;
     }
 
-    li {
-        border: 0;
+    .icon--menuitem {
+      margin-left: -1.75em;
     }
 
-    &__footer {
-        margin: 0;
-        padding: 0.9em 1.7em;
-        text-align: center;
-        color: $color-menu-text;
+    &:hover {
+      background-color: rgba(100, 100, 100, 0.2);
     }
+  }
+
+  li {
+    border: 0;
+  }
+
+  &__footer {
+    margin: 0;
+    padding: 0.9em 1.7em;
+    text-align: center;
+    color: $color-menu-text;
+  }
 }
 
 .nav-search {
-    position: relative;
-    padding: 0 1em 1em;
-    margin: 0;
-    width: 100%;
-    box-sizing: border-box;
+  position: relative;
+  padding: 0 1em 1em;
+  margin: 0;
+  width: 100%;
+  box-sizing: border-box;
+
+  label {
+    @include visuallyhidden();
+  }
+
+  input,
+  button {
+    border-radius: 0;
+    font-size: 1em;
+    border: 0;
+  }
+
+  input {
+    cursor: pointer;
+    border: 1px solid $nav-search-border;
+    background-color: $nav-search-bg;
+    color: $nav-search-color;
+    padding: 0.8em 2.5em 0.8em 1em;
+    font-weight: 600;
 
-    label {
-        @include visuallyhidden();
+    &:hover {
+      background-color: $nav-search-hover-bg;
     }
 
-    input,
-    button {
-        border-radius: 0;
-        font-size: 1em;
-        border: 0;
+    &:active,
+    &:focus {
+      background-color: $nav-search-focus-bg;
+      color: $nav-search-focus-color;
     }
 
-    input {
-        cursor: pointer;
-        border: 1px solid $nav-search-border;
-        background-color: $nav-search-bg;
-        color: $nav-search-color;
-        padding: 0.8em 2.5em 0.8em 1em;
-        font-weight: 600;
-
-        &:hover {
-            background-color: $nav-search-hover-bg;
-        }
-
-        &:active,
-        &:focus {
-            background-color: $nav-search-focus-bg;
-            color: $nav-search-focus-color;
-        }
-
-        &::placeholder {
-            color: $color-menu-text;
-        }
+    &::placeholder {
+      color: $color-menu-text;
     }
+  }
 
-    button {
-        background-color: transparent;
-        position: absolute;
-        top: 0;
-        right: 1em;
-        bottom: 0;
-        padding: 0;
-        width: 3em;
-
-        &:hover {
-            background-color: $nav-item-hover-bg;
-        }
+  button {
+    background-color: transparent;
+    position: absolute;
+    top: 0;
+    right: 1em;
+    bottom: 0;
+    padding: 0;
+    width: 3em;
 
-        &:active {
-            background-color: $nav-item-active-bg;
-        }
+    &:hover {
+      background-color: $nav-item-hover-bg;
+    }
 
-        &:before {
-            font-family: $font-wagtail-icons;
-            font-weight: 200;
-            text-transform: none;
-            content: map.get($icons, 'search');
-            display: block;
-            height: 100%;
-            line-height: 3.3em;
-            padding: 0 1em;
-        }
+    &:active {
+      background-color: $nav-item-active-bg;
     }
+
+    &:before {
+      font-family: $font-wagtail-icons;
+      font-weight: 200;
+      text-transform: none;
+      content: map.get($icons, 'search');
+      display: block;
+      height: 100%;
+      line-height: 3.3em;
+      padding: 0 1em;
+    }
+  }
 }
 
 // Navigation open condition
 body.nav-open {
-    .wrapper {
-        transform: translate3d($menu-width, 0, 0);
-    }
+  .wrapper {
+    transform: translate3d($menu-width, 0, 0);
+  }
 
-    .content-wrapper {
-        position: fixed;
-    }
+  .content-wrapper {
+    position: fixed;
+  }
 
-    footer {
-        bottom: 1px;
-    }
+  footer {
+    bottom: 1px;
+  }
 }
 
 // Explorer open condition, widens navigation area
 body.explorer-open {
-    .wrapper {
-        transform: translate3d($menu-width-max, 0, 0);
-    }
+  .wrapper {
+    transform: translate3d($menu-width-max, 0, 0);
+  }
 
-    .nav-wrapper {
-        margin-left: -$menu-width-max;
-        width: $menu-width-max;
-    }
+  .nav-wrapper {
+    margin-left: -$menu-width-max;
+    width: $menu-width-max;
+  }
 
-    .nav-main {
-        display: none;
-    }
+  .nav-main {
+    display: none;
+  }
 }
 
 @include media-breakpoint-up(sm) {
-    .wrapper,
-    body.nav-open .wrapper {
-        transform: none;
-        padding-left: $menu-width;
+  .wrapper,
+  body.nav-open .wrapper {
+    transform: none;
+    padding-left: $menu-width;
 
-        @include transition(padding-left $menu-transition-duration ease);
-    }
+    @include transition(padding-left $menu-transition-duration ease);
+  }
 
-    body.sidebar-collapsed .wrapper {
-        padding-left: $menu-width-slim;
-    }
+  body.sidebar-collapsed .wrapper {
+    padding-left: $menu-width-slim;
+  }
 
-    .nav-wrapper {
-        // height and position necessary to force it to 100% height of screen (with some JS help)
-        position: absolute;
-        left: 0;
-        height: 100%;
-        margin-left: 0;
-
-        .inner {
-            height: 100%;
-            position: fixed;
-            width: $menu-width;
-            z-index: $nav-wrapper-inner-z-index;
-        }
-    }
+  .nav-wrapper {
+    // height and position necessary to force it to 100% height of screen (with some JS help)
+    position: absolute;
+    left: 0;
+    height: 100%;
+    margin-left: 0;
 
-    .nav-toggle.unbutton {
-        display: none;
+    .inner {
+      height: 100%;
+      position: fixed;
+      width: $menu-width;
+      z-index: $nav-wrapper-inner-z-index;
     }
+  }
 
-    .nav-main {
-        overflow: auto;
-        margin-bottom: $nav-footer-closed-height;
-        @include transition(margin-bottom 0.2s ease);
-
-        .nav-footer {
-            position: fixed;
-            width: $menu-width;
-            bottom: 0;
-            background-color: $nav-footer-submenu-bg;
-        }
+  .nav-toggle.unbutton {
+    display: none;
+  }
 
-        .nav-footer-submenu {
-            @include transition(max-height 0.2s ease);
-            max-height: 0;
-        }
+  .nav-main {
+    overflow: auto;
+    margin-bottom: $nav-footer-closed-height;
+    @include transition(margin-bottom 0.2s ease);
 
-        &--open-footer {
-            margin-bottom: $nav-footer-open-height;
+    .nav-footer {
+      position: fixed;
+      width: $menu-width;
+      bottom: 0;
+      background-color: $nav-footer-submenu-bg;
+    }
 
-            .nav-footer-submenu {
-                max-height: $nav-footer-submenu-height;
-            }
-        }
+    .nav-footer-submenu {
+      @include transition(max-height 0.2s ease);
+      max-height: 0;
+    }
 
-        .account {
-            @include clearfix;
-            background: $nav-footer-account-bg;
-            color: $color-menu-text;
-            text-transform: uppercase;
-            display: block;
-            cursor: pointer;
-
-            &:hover {
-                background-color: rgba(100, 100, 100, 0.15);
-                color: $color-white;
-                text-shadow: -1px -1px 0 rgba(0, 0, 0, 0.3);
-            }
-
-            .avatar {
-                float: left;
-                margin-right: 0.9em;
-
-                &:before {
-                    color: inherit;
-                    border-color: inherit;
-                }
-            }
-
-            em {
-                box-sizing: border-box;
-                padding-right: 1.8em;
-                margin-top: 1.2em;
-                font-style: normal;
-                font-weight: 700;
-                width: 110px;
-                overflow: hidden;
-                white-space: nowrap;
-                text-overflow: ellipsis;
-                float: left;
-
-                &:after {
-                    font-size: 1.5em;
-                    position: absolute;
-                    right: 0.25em;
-                }
-            }
-        }
+    &--open-footer {
+      margin-bottom: $nav-footer-open-height;
+
+      .nav-footer-submenu {
+        max-height: $nav-footer-submenu-height;
+      }
     }
 
-    .nav-submenu {
-        transform: translate3d(0, 0, 0);
-        position: fixed;
-        height: 100vh;
-        width: 0;
-        padding: 0;
-        top: 0;
-        left: $menu-width;
+    .account {
+      @include clearfix;
+      background: $nav-footer-account-bg;
+      color: $color-menu-text;
+      text-transform: uppercase;
+      display: block;
+      cursor: pointer;
+
+      &:hover {
+        background-color: rgba(100, 100, 100, 0.15);
+        color: $color-white;
+        text-shadow: -1px -1px 0 rgba(0, 0, 0, 0.3);
+      }
+
+      .avatar {
+        float: left;
+        margin-right: 0.9em;
+
+        &:before {
+          color: inherit;
+          border-color: inherit;
+        }
+      }
+
+      em {
+        box-sizing: border-box;
+        padding-right: 1.8em;
+        margin-top: 1.2em;
+        font-style: normal;
+        font-weight: 700;
+        width: 110px;
         overflow: hidden;
-        display: flex;
-        flex-direction: column;
+        white-space: nowrap;
+        text-overflow: ellipsis;
+        float: left;
 
-        h2,
-        &__list {
-            width: $menu-width;
-        }
+        &:after {
+          font-size: 1.5em;
+          position: absolute;
+          right: 0.25em;
+        }
+      }
+    }
+  }
+
+  .nav-submenu {
+    transform: translate3d(0, 0, 0);
+    position: fixed;
+    height: 100vh;
+    width: 0;
+    padding: 0;
+    top: 0;
+    left: $menu-width;
+    overflow: hidden;
+    display: flex;
+    flex-direction: column;
 
-        h2 {
-            display: block;
-            padding: 0.2em 0;
-            font-size: 1.2em;
-            font-weight: 500;
-            text-transform: none;
-            text-align: center;
-            color: $color-menu-text;
-
-            &:before {
-                font-size: 4em;
-                display: block;
-                text-align: center;
-                margin: 0 0 0.2em;
-                width: 100%;
-                opacity: 0.15;
-            }
-        }
+    h2,
+    &__list {
+      width: $menu-width;
+    }
 
-        &__list {
-            overflow: auto;
-            flex-grow: 1;
-        }
+    h2 {
+      display: block;
+      padding: 0.2em 0;
+      font-size: 1.2em;
+      font-weight: 500;
+      text-transform: none;
+      text-align: center;
+      color: $color-menu-text;
+
+      &:before {
+        font-size: 4em;
+        display: block;
+        text-align: center;
+        margin: 0 0 0.2em;
+        width: 100%;
+        opacity: 0.15;
+      }
+    }
 
-        &__footer {
-            line-height: $nav-footer-closed-height;
-            padding: 0;
-        }
+    &__list {
+      overflow: auto;
+      flex-grow: 1;
+    }
 
+    &__footer {
+      line-height: $nav-footer-closed-height;
+      padding: 0;
     }
+  }
 
-    li.submenu-active {
-        background: $nav-submenu-bg;
+  li.submenu-active {
+    background: $nav-submenu-bg;
 
-        > a {
-            text-shadow: -1px -1px 0 rgba(0, 0, 0, 0.3);
+    > a {
+      text-shadow: -1px -1px 0 rgba(0, 0, 0, 0.3);
 
-            &:hover {
-                background-color: transparent;
-            }
-        }
+      &:hover {
+        background-color: transparent;
+      }
+    }
 
-        .nav-submenu {
-            @include transition(width 0.2s ease);
-            box-shadow: 2px 0 2px rgba(0, 0, 0, 0.35);
-            width: $menu-width;
+    .nav-submenu {
+      @include transition(width 0.2s ease);
+      box-shadow: 2px 0 2px rgba(0, 0, 0, 0.35);
+      width: $menu-width;
 
-            a {
-                padding-left: 3.5em;
-            }
-        }
+      a {
+        padding-left: 3.5em;
+      }
     }
+  }
 
-    body.nav-open {
-        .content-wrapper {
-            position: relative;
-        }
+  body.nav-open {
+    .content-wrapper {
+      position: relative;
     }
+  }
 
-    body.explorer-open {
-        overflow: hidden;
+  body.explorer-open {
+    overflow: hidden;
 
-        &:after {
-            opacity: 1;
-            visibility: visible;
-        }
+    &:after {
+      opacity: 1;
+      visibility: visible;
+    }
 
-        .wrapper {
-            transform: none;
-        }
+    .wrapper {
+      transform: none;
+    }
 
-        .nav-wrapper {
-            margin-left: 0;
-            width: $menu-width;
-        }
+    .nav-wrapper {
+      margin-left: 0;
+      width: $menu-width;
+    }
 
-        .nav-main {
-            display: block;
-        }
+    .nav-main {
+      display: block;
     }
+  }
 }
 
 ///////////////
 // Z-indexes //
 ///////////////
 .nav-toggle {
-    z-index: 5;
+  z-index: 5;
 }
 
 // stylelint-disable-next-line no-duplicate-selectors
 .nav-wrapper {
-    z-index: 2;
+  z-index: 2;
 }
 
 // Avoiding a stacking context for the content-wrapper saves us a world
@@ -525,80 +523,78 @@ body.explorer-open {
 // }
 // stylelint-disable-next-line no-duplicate-selectors
 .nav-submenu {
-    z-index: 6;
+  z-index: 6;
 }
 
 footer,
 .logo {
-    z-index: 100;
+  z-index: 100;
 }
 
 @include media-breakpoint-up(sm) {
-    .nav-main {
-        .nav-footer {
-            z-index: 2;
-        }
-    }
-
-    .nav-submenu {
-        z-index: 500;
-    }
-
-    // Allows overspill of messages banner onto left menu, but also explorer
-    // to spill over main content
-    .nav-wrapper {
-        z-index: auto;
-    }
-
-    // footer is z-index: 100, so ensure the navigation sits on top of it.
-    .nav-wrapper.submenu-active {
-        z-index: 200;
-    }
+  .nav-main {
+    .nav-footer {
+      z-index: 2;
+    }
+  }
+
+  .nav-submenu {
+    z-index: 500;
+  }
+
+  // Allows overspill of messages banner onto left menu, but also explorer
+  // to spill over main content
+  .nav-wrapper {
+    z-index: auto;
+  }
+
+  // footer is z-index: 100, so ensure the navigation sits on top of it.
+  .nav-wrapper.submenu-active {
+    z-index: 200;
+  }
 }
 
-
 ///////////////////////
 // Media query hacks //
 ///////////////////////
 
 // to detect IE10+ which doesn't support 3d transform of static elements and needs a fallback
 // stylelint-disable scss/media-feature-value-dollar-variable
-@media all and (-ms-high-contrast: none),
-    all and (-ms-high-contrast: active) {
+@media all and (-ms-high-contrast: none), all and (-ms-high-contrast: active) {
+  .wrapper {
+    @include transition(left 0.2s ease);
+    left: 0;
+  }
+
+  body.nav-open {
     .wrapper {
-        @include transition(left 0.2s ease);
-        left: 0;
+      transform: none;
+      left: $menu-width;
+      position: relative;
     }
+  }
 
-    body.nav-open {
-        .wrapper {
-            transform: none;
-            left: $menu-width;
-            position: relative;
-        }
+  body.explorer-open {
+    .wrapper {
+      transform: none;
+      left: $menu-width-max;
     }
 
-    body.explorer-open {
-        .wrapper {
-            transform: none;
-            left: $menu-width-max;
-        }
-
-        .nav-wrapper {
-            width: $menu-width-max;
-        }
+    .nav-wrapper {
+      width: $menu-width-max;
     }
+  }
 }
 
 @media all and (min-width: breakpoint-min(sm)) and (-ms-high-contrast: none),
-    all and (min-width: breakpoint-min(sm)) and (-ms-high-contrast: active) {
-    body.explorer-open {
-        .wrapper {
-            left: 0;
-        }
+  all and (min-width: breakpoint-min(sm)) and (-ms-high-contrast: active) {
+  body.explorer-open {
+    .wrapper {
+      left: 0;
+    }
 
-        .nav-wrapper {
-            width: $menu-width;
-        }
+    .nav-wrapper {
+      width: $menu-width;
     }
+  }
 }

+ 13 - 13
client/scss/components/_media-placeholder.scss

@@ -1,17 +1,17 @@
 .media-placeholder {
-    width: 600px;
-    height: 400px;
-    background-color: #ccc;
-    padding: 5px;
+  width: 600px;
+  height: 400px;
+  background-color: #ccc;
+  padding: 5px;
 
-    h3,
-    p {
-        margin: 0;
-    }
+  h3,
+  p {
+    margin: 0;
+  }
 
-    img {
-        max-width: 350px;
-        max-height: 350px;
-        margin: 20px;
-    }
+  img {
+    max-width: 350px;
+    max-height: 350px;
+    margin: 20px;
+  }
 }

+ 11 - 11
client/scss/components/_messages.capability.scss

@@ -1,14 +1,14 @@
 .capabilitymessage {
-    display: block;
-    background-color: $color-red;
-    color: $color-white;
-    padding: 1em 2em;
-    margin: 0;
-    position: relative;
-    text-align: center;
+  display: block;
+  background-color: $color-red;
+  color: $color-white;
+  padding: 1em 2em;
+  margin: 0;
+  position: relative;
+  text-align: center;
 
-    a {
-        color: $color-white;
-        text-decoration: underline;
-    }
+  a {
+    color: $color-white;
+    text-decoration: underline;
+  }
 }

+ 58 - 58
client/scss/components/_messages.scss

@@ -2,85 +2,85 @@
 // for display on the next page visited. These appear as an animated banner at the top of the page.
 // For inline help text, see typography.scss
 .messages {
-    position: relative;
-    background-color: $color-grey-1;
+  position: relative;
+  background-color: $color-grey-1;
 
-    .buttons {
-        margin-left: 1em;
-    }
+  .buttons {
+    margin-left: 1em;
+  }
 
-    > ul {
-        @include unlistimmediate();
-        position: relative;
-        top: -100px;
-        opacity: 0;
-    }
+  > ul {
+    @include unlistimmediate();
+    position: relative;
+    top: -100px;
+    opacity: 0;
+  }
 
-    > ul > li {
-        // @include nice-padding;
-        padding: 1.6em 3em 1.6em 1.6em;
-        color: $color-white;
-    }
+  > ul > li {
+    // @include nice-padding;
+    padding: 1.6em 3em 1.6em 1.6em;
+    color: $color-white;
+  }
 
-    > ul > li:before {
-        @include font-smoothing;
-        margin-right: 0.5em;
-        font-size: 1.5em;
-        vertical-align: middle;
-    }
+  > ul > li:before {
+    @include font-smoothing;
+    margin-right: 0.5em;
+    font-size: 1.5em;
+    vertical-align: middle;
+  }
 
-    &-icon {
-        vertical-align: text-top;
-        margin-right: 0.5em;
-        width: 1.5em;
-        height: 1.5em;
-    }
+  &-icon {
+    vertical-align: text-top;
+    margin-right: 0.5em;
+    width: 1.5em;
+    height: 1.5em;
+  }
 
-    .error {
-        background-color: $color-red-dark;
-    }
+  .error {
+    background-color: $color-red-dark;
+  }
 
-    .warning {
-        background-color: $color-orange-dark;
-    }
+  .warning {
+    background-color: $color-orange-dark;
+  }
 
-    .success {
-        background-color: $color-green-dark;
-    }
+  .success {
+    background-color: $color-green-dark;
+  }
 
-    .success .button:hover {
-        background-color: $color-teal-dark;
-    }
+  .success .button:hover {
+    background-color: $color-teal-dark;
+  }
 
-    .button-secondary {
-        border-color: rgba(255, 255, 255, 0.5);
-        color: $color-white;
+  .button-secondary {
+    border-color: rgba(255, 255, 255, 0.5);
+    color: $color-white;
 
-        &:hover {
-            border-color: transparent;
-        }
+    &:hover {
+      border-color: transparent;
     }
+  }
 
-    .errorlist {
-        margin: 0.5em 0 0 1em;
-    }
+  .errorlist {
+    margin: 0.5em 0 0 1em;
+  }
 }
 
 .messages.new > ul {
-    transition: none;
-    top: -100px;
+  transition: none;
+  top: -100px;
 }
 
 .ready .messages > ul,
 .messages.appear > ul {
-    transition: top 0.5s ease, opacity 0.5s ease, max-height 1.2s ease;
-    opacity: 1;
-    top: 0;
+  transition: top 0.5s ease, opacity 0.5s ease, max-height 1.2s ease;
+  opacity: 1;
+  top: 0;
 }
 
 @include media-breakpoint-up(sm) {
-    .messages > ul > li {
-        padding-left: 1.6em;
-        padding-right: 3em;
-    }
+  .messages > ul > li {
+    padding-left: 1.6em;
+    padding-right: 3em;
+  }
 }

+ 6 - 6
client/scss/components/_messages.status.scss

@@ -1,9 +1,9 @@
 .status-msg {
-    &.success {
-        color: $color-green-dark;
-    }
+  &.success {
+    color: $color-green-dark;
+  }
 
-    &.failure {
-        color: $color-red-dark;
-    }
+  &.failure {
+    color: $color-red-dark;
+  }
 }

+ 94 - 90
client/scss/components/_modals.scss

@@ -1,136 +1,140 @@
 $zindex-modal-background: 500;
 
 .fade {
-    @include transition(opacity 0.15s linear);
-    opacity: 0;
+  @include transition(opacity 0.15s linear);
+  opacity: 0;
 
-    &.in {
-        opacity: 1;
-    }
+  &.in {
+    opacity: 1;
+  }
 }
 
 // Kill the scroll on the body
 .modal-open {
-    overflow: hidden;
+  overflow: hidden;
 
-    .content-wrapper {
-        transform: none;
-    }
+  .content-wrapper {
+    transform: none;
+  }
 }
 
 // Container that the modal scrolls within
 .modal {
-    box-sizing: border-box;
-    display: none;
-    overflow: auto;
-    overflow-y: scroll;
-    position: fixed;
-    top: 0;
-    right: 0;
-    bottom: 0;
-    left: 0;
-    z-index: $zindex-modal-background;
+  box-sizing: border-box;
+  display: none;
+  overflow: auto;
+  overflow-y: scroll;
+  position: fixed;
+  top: 0;
+  right: 0;
+  bottom: 0;
+  left: 0;
+  z-index: $zindex-modal-background;
 }
 
 // Shell div to position the modal with bottom padding
 .modal-dialog {
-    box-sizing: border-box;
-    margin-left: auto;
-    margin-right: auto;
-    padding: 0;
-    z-index: ($zindex-modal-background + 10);
-    height: 90%;
-    width: 85%;
-
-    &:before {
-        content: '';
-        display: inline-block;
-        height: 100%;
-        vertical-align: middle;
-        margin-right: -0.25em;
-    }
+  box-sizing: border-box;
+  margin-left: auto;
+  margin-right: auto;
+  padding: 0;
+  z-index: ($zindex-modal-background + 10);
+  height: 90%;
+  width: 85%;
+
+  &:before {
+    content: '';
+    display: inline-block;
+    height: 100%;
+    vertical-align: middle;
+    margin-right: -0.25em;
+  }
 }
 
 // Actual modal
 .modal-content {
-    box-sizing: border-box;
-    border-radius: 3px;
-    width: 98.7%;
-    position: relative;
-    background-color: $color-white;
-    margin-top: 2em;
-    padding-bottom: 3em;
-    display: inline-block;
-    vertical-align: middle;
-    overflow: hidden;
+  box-sizing: border-box;
+  border-radius: 3px;
+  width: 98.7%;
+  position: relative;
+  background-color: $color-white;
+  margin-top: 2em;
+  padding-bottom: 3em;
+  display: inline-block;
+  vertical-align: middle;
+  overflow: hidden;
 }
 
 // Modal background
 .modal-backdrop {
-    position: fixed;
-    top: 0;
-    right: 0;
-    bottom: 0;
-    left: 0;
-    z-index: ($zindex-modal-background - 10);
-    background-color: $color-black;
-
-    // Fade for backdrop
-    &.fade { opacity: 0; }
-
-    &.in { opacity: 0.5; }
+  position: fixed;
+  top: 0;
+  right: 0;
+  bottom: 0;
+  left: 0;
+  z-index: ($zindex-modal-background - 10);
+  background-color: $color-black;
+
+  // Fade for backdrop
+  &.fade {
+    opacity: 0;
+  }
+
+  &.in {
+    opacity: 0.5;
+  }
 }
 
 .modal .close {
-    padding: 0;
-    position: absolute;
-    width: 50px;
-    height: 50px;
-    top: 10px;
-    right: 10px;
-    z-index: 1;
+  padding: 0;
+  position: absolute;
+  width: 50px;
+  height: 50px;
+  top: 10px;
+  right: 10px;
+  z-index: 1;
 }
 
 // Where all modal content resides
 .modal-body {
-    position: relative;
-    padding-bottom: 2em;
+  position: relative;
+  padding-bottom: 2em;
 
-    header {
-        padding-left: 2em;
-        padding-right: 100px;
+  header {
+    padding-left: 2em;
+    padding-right: 100px;
 
-        &.tab-merged {
-            padding-left: 1.6em;
-        }
+    &.tab-merged {
+      padding-left: 1.6em;
     }
+  }
 
-    .header-title {
-        // stylelint-disable-next-line declaration-no-important
-        padding-left: 0 !important;
-        margin-left: -36px;
-    }
+  .header-title {
+    // stylelint-disable-next-line declaration-no-important
+    padding-left: 0 !important;
+    margin-left: -36px;
+  }
 
-    .tab-merged .header-title {
-        margin-left: 0;
-    }
+  .tab-merged .header-title {
+    margin-left: 0;
+  }
 }
 
 @include media-breakpoint-up(sm) {
-    .modal-dialog {
-        padding: 0 0 2em $menu-width;
-    }
+  .modal-dialog {
+    padding: 0 0 2em $menu-width;
+  }
 
-    .modal-body {
-        header.tab-merged {
-            padding-left: $desktop-nice-padding;
-        }
+  .modal-body {
+    header.tab-merged {
+      padding-left: $desktop-nice-padding;
     }
+  }
 }
 
 @include media-breakpoint-up(xl) {
-    .modal-dialog {
-        max-width: 100em;
-        padding: 0 0 2em;
-    }
+  .modal-dialog {
+    max-width: 100em;
+    padding: 0 0 2em;
+  }
 }

+ 13 - 13
client/scss/components/_privacy-indicator.scss

@@ -1,20 +1,20 @@
 .privacy-indicator {
-    .label-private,
-    .label-public {
-        &:before {
-            font-size: 1.5em;
-        }
+  .label-private,
+  .label-public {
+    &:before {
+      font-size: 1.5em;
     }
+  }
 
-    &.public {
-        .label-private {
-            display: none;
-        }
+  &.public {
+    .label-private {
+      display: none;
     }
+  }
 
-    &.private {
-        .label-public {
-            display: none;
-        }
+  &.private {
+    .label-public {
+      display: none;
     }
+  }
 }

+ 21 - 21
client/scss/components/_progressbar.scss

@@ -1,25 +1,25 @@
 .progress {
-    border-radius: 1.2em;
-    background-color: $color-teal-dark;
-    border: 1px solid $color-teal;
-    opacity: 0;
+  border-radius: 1.2em;
+  background-color: $color-teal-dark;
+  border: 1px solid $color-teal;
+  opacity: 0;
 
-    &.active {
-        @include transition(opacity 0.3s ease);
-        opacity: 1;
-    }
+  &.active {
+    @include transition(opacity 0.3s ease);
+    opacity: 1;
+  }
 
-    .bar {
-        @include transition(width 0.3s ease);
-        border-radius: 1.5em;
-        overflow: hidden;
-        box-sizing: border-box;
-        text-align: right;
-        line-height: 1.2em;
-        color: $color-white;
-        font-size: 0.85em;
-        background-color: $color-teal;
-        height: 1.2em;
-        padding-right: 1em;
-    }
+  .bar {
+    @include transition(width 0.3s ease);
+    border-radius: 1.5em;
+    overflow: hidden;
+    box-sizing: border-box;
+    text-align: right;
+    line-height: 1.2em;
+    color: $color-white;
+    font-size: 0.85em;
+    background-color: $color-teal;
+    height: 1.2em;
+    padding-right: 1em;
+  }
 }

+ 12 - 12
client/scss/components/_skiplink.scss

@@ -1,16 +1,16 @@
 .skiplink {
-    display: block;
-    position: fixed;
-    top: -1000rem;
-    left: 1rem;
-    z-index: 3000;
+  display: block;
+  position: fixed;
+  top: -1000rem;
+  left: 1rem;
+  z-index: 3000;
 
-    &:focus {
-        top: 1rem;
-    }
+  &:focus {
+    top: 1rem;
+  }
 
-    &.button {
-        background: $color-green-darker;
-        border: $color-green-darker;
-    }
+  &.button {
+    background: $color-green-darker;
+    border: $color-green-darker;
+  }
 }

+ 33 - 33
client/scss/components/_status-tag.scss

@@ -1,50 +1,50 @@
-@use "sass:color";
+@use 'sass:color';
 
 .status-tag {
-    border-radius: 2px;
-    text-align: center;
-    display: inline-block;
-    text-transform: uppercase;
-    padding: 0 0.5em;
-    border: 1px solid color.adjust($color-grey-2, $lightness: 30%);
-    color: color.adjust($color-grey-2, $lightness: 30%);
-    -webkit-font-smoothing: auto;
-    line-height: 19px;
-    font-size: 0.8em;
-    margin: 0 0.5em 0.5em;
-    background: #fff url('#{$images-root}bg-dark-diag.svg');
+  border-radius: 2px;
+  text-align: center;
+  display: inline-block;
+  text-transform: uppercase;
+  padding: 0 0.5em;
+  border: 1px solid color.adjust($color-grey-2, $lightness: 30%);
+  color: color.adjust($color-grey-2, $lightness: 30%);
+  -webkit-font-smoothing: auto;
+  line-height: 19px;
+  font-size: 0.8em;
+  margin: 0 0.5em 0.5em;
+  background: #fff url('#{$images-root}bg-dark-diag.svg');
 
-    &.primary {
-        color: $color-grey-2;
-        border: 1px solid $color-grey-2;
-        background: #fff;
-    }
+  &.primary {
+    color: $color-grey-2;
+    border: 1px solid $color-grey-2;
+    background: #fff;
+  }
 
-    &.disabled {
-        pointer-events: none;
-    }
+  &.disabled {
+    pointer-events: none;
+  }
 
-    &--label {
-        color: $color-grey-2;
-        background: $color-grey-4;
-        border: $color-grey-4;
-        font-weight: 500;
-    }
+  &--label {
+    color: $color-grey-2;
+    background: $color-grey-4;
+    border: $color-grey-4;
+    font-weight: 500;
+  }
 }
 
 .listing .index .status-tag--label {
-    border: 1px solid;
+  border: 1px solid;
 }
 
 button.status-tag:hover,
 a.status-tag:hover,
 a.status-tag.primary:hover {
-    border-color: $color-teal;
-    color: $color-teal;
+  border-color: $color-teal;
+  color: $color-teal;
 }
 
 button.status-tag:hover {
-    border-color: $color-teal-dark;
-    background-color: $color-teal-darker;
-    color: $color-white;
+  border-color: $color-teal-dark;
+  background-color: $color-teal-darker;
+  color: $color-white;
 }

+ 90 - 87
client/scss/components/_switch.scss

@@ -1,4 +1,4 @@
-@use "sass:math";
+@use 'sass:math';
 
 $switch-width: 40px;
 $switch-height: 20px;
@@ -12,98 +12,101 @@ $switch-outline-radius: $switch-border-radius + $switch-outline;
 $switch-color-middle-grey: #777;
 
 .switch {
-    display: inline-flex;
-    align-items: center;
-    margin: 5px 0;
-
-    // Disable forms styling that's applied to the <label> tag
-    width: unset;
-    float: unset;
-
-    &__toggle {
-        position: relative;
-        cursor: pointer;
-
-        &::before,
-        &::after {
-            content: '';
-            transition: all 100ms cubic-bezier(0.4, 0, 0.2, 1);
-            display: block;
-        }
-
-        &::before {
-            height: $switch-height;
-            width: $switch-width;
-            border-radius: $switch-border-radius;
-            background: $switch-color-middle-grey;
-            border: $switch-border solid $switch-color-middle-grey;
-        }
-
-        &::after {
-            box-sizing: border-box;
-            position: absolute;
-            top: 50%;
-            transform: translate($switch-border, -50%);
-            height: $switch-height;
-            width: $switch-height;
-            border: $switch-border solid $color-white;
-            border-radius: 50%;
-            background-color: $color-white;
-        }
+  display: inline-flex;
+  align-items: center;
+  margin: 5px 0;
+
+  // Disable forms styling that's applied to the <label> tag
+  width: unset;
+  float: unset;
+
+  &__toggle {
+    position: relative;
+    cursor: pointer;
+
+    &::before,
+    &::after {
+      content: '';
+      transition: all 100ms cubic-bezier(0.4, 0, 0.2, 1);
+      display: block;
     }
 
-    [type=checkbox]:checked + &__toggle::before {
-        background: $color-teal;
-        border-color: $color-teal;
+    &::before {
+      height: $switch-height;
+      width: $switch-width;
+      border-radius: $switch-border-radius;
+      background: $switch-color-middle-grey;
+      border: $switch-border solid $switch-color-middle-grey;
     }
 
-    [type=checkbox]:checked + &__toggle::after {
-        transform: translate(calc(#{$switch-width} + #{$switch-border} - 100%), -50%);
+    &::after {
+      box-sizing: border-box;
+      position: absolute;
+      top: 50%;
+      transform: translate($switch-border, -50%);
+      height: $switch-height;
+      width: $switch-height;
+      border: $switch-border solid $color-white;
+      border-radius: 50%;
+      background-color: $color-white;
     }
-
-    [type=checkbox]:disabled + &__toggle {
-        cursor: not-allowed;
-        filter: grayscale(100%);
-        opacity: 0.3;
-    }
-
-    [type=checkbox]:disabled + &__toggle::after {
-        opacity: 0.5;
-        box-shadow: none;
-    }
-
-    [type=checkbox]:focus + &__toggle {
-        outline: $color-focus-outline solid $switch-outline;
-        -moz-outline-radius: $switch-outline-radius;
-    }
-
-    [type=checkbox] {
-        position: absolute;
-        opacity: 0;
-        pointer-events: none;
+  }
+
+  [type='checkbox']:checked + &__toggle::before {
+    background: $color-teal;
+    border-color: $color-teal;
+  }
+
+  [type='checkbox']:checked + &__toggle::after {
+    transform: translate(
+      calc(#{$switch-width} + #{$switch-border} - 100%),
+      -50%
+    );
+  }
+
+  [type='checkbox']:disabled + &__toggle {
+    cursor: not-allowed;
+    filter: grayscale(100%);
+    opacity: 0.3;
+  }
+
+  [type='checkbox']:disabled + &__toggle::after {
+    opacity: 0.5;
+    box-shadow: none;
+  }
+
+  [type='checkbox']:focus + &__toggle {
+    outline: $color-focus-outline solid $switch-outline;
+    -moz-outline-radius: $switch-outline-radius;
+  }
+
+  [type='checkbox'] {
+    position: absolute;
+    opacity: 0;
+    pointer-events: none;
+  }
+
+  // Colour changes for when displaying on teal background
+  &--teal-background {
+    $background: #03b0b1;
+
+    .switch__toggle {
+      &::before {
+        background-color: #b9b9b9;
+        border: $switch-border solid #b9b9b9;
+        opacity: 0.6;
+      }
+
+      &::after {
+        background-color: $color-white;
+      }
     }
 
-    // Colour changes for when displaying on teal background
-    &--teal-background {
-        $background: #03b0b1;
-
-        .switch__toggle {
-            &::before {
-                background-color: #b9b9b9;
-                border: $switch-border solid #b9b9b9;
-                opacity: 0.6;
-            }
-
-            &::after {
-                background-color: $color-white;
-            }
-        }
-
-        [type=checkbox]:checked + .switch__toggle::before {
-            // Override the white-background styling
-            background-color: $background;
-            border-color: $background;
-            opacity: 1;
-        }
+    [type='checkbox']:checked + .switch__toggle::before {
+      // Override the white-background styling
+      background-color: $background;
+      border-color: $background;
+      opacity: 1;
     }
+  }
 }

+ 132 - 132
client/scss/components/_tabs.scss

@@ -1,161 +1,161 @@
 .tab-nav {
-    @include row();
+  @include row();
+  padding: 0;
+  background: $color-grey-4;
+
+  li {
+    list-style-type: none;
+    width: 33%;
+    float: left;
     padding: 0;
-    background: $color-grey-4;
-
-    li {
-        list-style-type: none;
-        width: 33%;
-        float: left;
-        padding: 0;
-        position: relative;
-        margin-right: 2px;
-
-        &:first-of-type {
-            padding-left: $desktop-nice-padding;
-            margin-left: 0;
-        }
-    }
+    position: relative;
+    margin-right: 2px;
+
+    &:first-of-type {
+      padding-left: $desktop-nice-padding;
+      margin-left: 0;
+    }
+  }
+
+  h2 {
+    margin: 0;
+    font-size: inherit;
+  }
+
+  a {
+    @include transition(border-color 0.2s ease);
+    background-color: $color-teal-darker;
+    text-transform: uppercase;
+    font-weight: 600;
+    text-decoration: none;
+    display: block;
+    padding: 0.6em 0.7em 0.8em;
+    color: $color-white;
+    border-top: 0.3em solid $color-teal-darker;
+    max-height: 1.44em;
+    overflow: hidden;
+
+    &:hover {
+      color: $color-white;
+      border-top-color: rgba(0, 0, 0, 0.35);
+    }
+  }
+
+  a.errors {
+    &:after {
+      border-radius: 50px;
+      box-shadow: 1px 2px 2px rgba(0, 0, 0, 0.1);
+      position: absolute;
+      right: -0.5em;
+      top: -0.5em;
+      z-index: 5;
+      min-width: 0.9em;
+      color: $color-white;
+      background: $color-red;
+      content: attr(data-count);
+      padding: 0 0.3em;
+      line-height: 1.4em;
+      text-align: center;
+      font-size: 0.8em;
+    }
+  }
+
+  li.active a {
+    box-shadow: none;
+    color: $color-grey-1;
+    background-color: $color-white;
+    border-top: 0.3em solid $color-grey-1;
+  }
+
+  // For cases where tab-nav should merge with header
+  &.merged {
+    margin-top: 0;
+    background-color: $color-header-bg;
+  }
+
+  li.right {
+    float: right;
+    margin-right: 0;
+    margin-left: 2px;
+  }
+
+  li.wide {
+    width: unset;
+  }
+
+  .right {
+    max-height: 1.44em;
+    overflow: visible;
+  }
+}
 
-    h2 {
-        margin: 0;
-        font-size: inherit;
-    }
+.tab-content {
+  > section {
+    display: none;
+    padding-top: 1em;
 
-    a {
-        @include transition(border-color 0.2s ease);
-        background-color: $color-teal-darker;
-        text-transform: uppercase;
-        font-weight: 600;
-        text-decoration: none;
-        display: block;
-        padding: 0.6em 0.7em 0.8em;
-        color: $color-white;
-        border-top: 0.3em solid $color-teal-darker;
-        max-height: 1.44em;
-        overflow: hidden;
-
-        &:hover {
-            color: $color-white;
-            border-top-color: rgba(0, 0, 0, 0.35);
-        }
+    &.active {
+      display: block;
     }
+  }
 
-    a.errors {
-        &:after {
-            border-radius: 50px;
-            box-shadow: 1px 2px 2px rgba(0, 0, 0, 0.1);
-            position: absolute;
-            right: -0.5em;
-            top: -0.5em;
-            z-index: 5;
-            min-width: 0.9em;
-            color: $color-white;
-            background: $color-red;
-            content: attr(data-count);
-            padding: 0 0.3em;
-            line-height: 1.4em;
-            text-align: center;
-            font-size: 0.8em;
-        }
-    }
+  .page-locked & {
+    cursor: not-allowed;
+    user-select: none;
 
-    li.active a {
-        box-shadow: none;
-        color: $color-grey-1;
-        background-color: $color-white;
-        border-top: 0.3em solid $color-grey-1;
+    > * {
+      pointer-events: none;
     }
+  }
+}
 
+@include media-breakpoint-up(sm) {
+  .tab-nav {
     // For cases where tab-nav should merge with header
     &.merged {
-        margin-top: 0;
-        background-color: $color-header-bg;
-    }
-
-    li.right {
-        float: right;
-        margin-right: 0;
-        margin-left: 2px;
+      background-color: $color-header-bg;
     }
 
-    li.wide {
-        width: unset;
-    }
-
-    .right {
-        max-height: 1.44em;
-        overflow: visible;
-    }
-}
-
-.tab-content {
-    > section {
-        display: none;
-        padding-top: 1em;
-
-        &.active {
-            display: block;
-        }
+    li {
+      width: auto;
+      padding: 0;
     }
 
-    .page-locked & {
-        cursor: not-allowed;
-        user-select: none;
-
-        > * {
-            pointer-events: none;
-        }
+    a {
+      padding-left: $mobile-nice-padding;
+      padding-right: $mobile-nice-padding;
     }
-}
 
-@include media-breakpoint-up(sm) {
-    .tab-nav {
-        // For cases where tab-nav should merge with header
-        &.merged {
-            background-color: $color-header-bg;
-        }
-
-        li {
-            width: auto;
-            padding: 0;
-        }
-
-        a {
-            padding-left: $mobile-nice-padding;
-            padding-right: $mobile-nice-padding;
-        }
-
-        li.settings a {
-            padding-left: 2em;
-            padding-right: 2em;
-        }
+    li.settings a {
+      padding-left: 2em;
+      padding-right: 2em;
     }
+  }
 
-    .modal-content .tab-nav li {
-        padding: 0;
-        min-width: 0;
+  .modal-content .tab-nav li {
+    padding: 0;
+    min-width: 0;
 
-        &:first-of-type {
-            padding-left: $desktop-nice-padding;
-        }
+    &:first-of-type {
+      padding-left: $desktop-nice-padding;
     }
+  }
 }
 
 @include media-breakpoint-down(xs) {
-    // To allow tabs on the edit page to be editable
-    .tab-nav li:first-of-type {
-        padding-left: 1.6em;
-    }
-
-    .tab-nav li {
-        width: auto;
-    }
+  // To allow tabs on the edit page to be editable
+  .tab-nav li:first-of-type {
+    padding-left: 1.6em;
+  }
+
+  .tab-nav li {
+    width: auto;
+  }
 }
 
 // Media for Windows High Contrast
 @media (forced-colors: $media-forced-colours) {
-    .tab-nav li.active a {
-        border-bottom: 0.3em solid $system-color-link-text;
-    }
+  .tab-nav li.active a {
+    border-bottom: 0.3em solid $system-color-link-text;
+  }
 }

+ 42 - 42
client/scss/components/_tag.scss

@@ -1,61 +1,61 @@
-@use "sass:map";
+@use 'sass:map';
 // free tagging tags from taggit
 .tag {
-    border-radius: 2px;
-    background-color: $color-teal;
-    padding: 0.2em 0.5em;
+  border-radius: 2px;
+  background-color: $color-teal;
+  padding: 0.2em 0.5em;
+  color: $color-white;
+  line-height: 2em;
+  white-space: nowrap;
+
+  &:before {
+    font-family: $font-wagtail-icons;
+    display: inline-block;
     color: $color-white;
-    line-height: 2em;
-    white-space: nowrap;
+    content: map.get($icons, 'tag');
+    padding-right: 0.5em;
+  }
 
-    &:before {
-        font-family: $font-wagtail-icons;
-        display: inline-block;
-        color: $color-white;
-        content: map.get($icons, 'tag');
-        padding-right: 0.5em;
-    }
-
-    .taglist & {
-        margin-right: 0.8em;
-    }
+  .taglist & {
+    margin-right: 0.8em;
+  }
 }
 
 a.tag:hover {
-    background-color: $color-teal-darker;
-    color: $color-white;
+  background-color: $color-teal-darker;
+  color: $color-white;
 }
 
 .taglist {
-    font-size: 0.9em;
-    line-height: 2.4em;
+  font-size: 0.9em;
+  line-height: 2.4em;
 }
 
 .tagfilter {
-    legend {
-        @include visuallyvisible;
-
-        @include media-breakpoint-up(sm) {
-            @include column(2);
-            padding-left: 0;
-        }
-
-        font-weight: 700;
-        color: #333;
-        font-size: 1.1em;
-        display: block;
-        padding: 0 0 0.8em;
-    }
+  legend {
+    @include visuallyvisible;
 
-    a {
-        font-size: 0.9em;
+    @include media-breakpoint-up(sm) {
+      @include column(2);
+      padding-left: 0;
     }
 
-    .button.bicolor.icon-cross {
-        padding-left: 2em;
+    font-weight: 700;
+    color: #333;
+    font-size: 1.1em;
+    display: block;
+    padding: 0 0 0.8em;
+  }
+
+  a {
+    font-size: 0.9em;
+  }
 
-        &:before {
-            background-color: transparent;
-        }
+  .button.bicolor.icon-cross {
+    padding-left: 2em;
+
+    &:before {
+      background-color: transparent;
     }
+  }
 }

+ 60 - 60
client/scss/components/_tooltips.scss

@@ -1,108 +1,108 @@
 // From Bootstrap v3.0.0
 .tooltip {
-    position: absolute;
-    z-index: 1030;
-    display: block;
-    font-size: 12px;
-    line-height: 1.4;
-    opacity: 0;
-    visibility: visible;
+  position: absolute;
+  z-index: 1030;
+  display: block;
+  font-size: 12px;
+  line-height: 1.4;
+  opacity: 0;
+  visibility: visible;
 }
 
 .tooltip.in {
-    opacity: 0.9;
+  opacity: 0.9;
 }
 
 .tooltip.top {
-    padding: 5px 0;
+  padding: 5px 0;
 }
 
 .tooltip.right {
-    padding: 0 5px;
+  padding: 0 5px;
 }
 
 .tooltip.bottom {
-    padding: 5px 0;
+  padding: 5px 0;
 }
 
 .tooltip.left {
-    padding: 0 5px;
+  padding: 0 5px;
 }
 
 .tooltip-inner {
-    max-width: 200px;
-    padding: 3px 8px;
-    color: #fff;
-    text-align: center;
-    text-decoration: none;
-    background-color: #000;
-    border-radius: 4px;
+  max-width: 200px;
+  padding: 3px 8px;
+  color: #fff;
+  text-align: center;
+  text-decoration: none;
+  background-color: #000;
+  border-radius: 4px;
 }
 
 .tooltip-arrow {
-    position: absolute;
-    width: 0;
-    height: 0;
-    border-color: transparent;
-    border-style: solid;
+  position: absolute;
+  width: 0;
+  height: 0;
+  border-color: transparent;
+  border-style: solid;
 }
 
 .tooltip.top .tooltip-arrow {
-    bottom: 0;
-    left: 50%;
-    margin-left: -5px;
-    border-top-color: #000;
-    border-width: 5px 5px 0;
+  bottom: 0;
+  left: 50%;
+  margin-left: -5px;
+  border-top-color: #000;
+  border-width: 5px 5px 0;
 }
 
 .tooltip.top-left .tooltip-arrow {
-    bottom: 0;
-    left: 5px;
-    border-top-color: #000;
-    border-width: 5px 5px 0;
+  bottom: 0;
+  left: 5px;
+  border-top-color: #000;
+  border-width: 5px 5px 0;
 }
 
 .tooltip.top-right .tooltip-arrow {
-    right: 5px;
-    bottom: 0;
-    border-top-color: #000;
-    border-width: 5px 5px 0;
+  right: 5px;
+  bottom: 0;
+  border-top-color: #000;
+  border-width: 5px 5px 0;
 }
 
 .tooltip.right .tooltip-arrow {
-    top: 50%;
-    left: 0;
-    margin-top: -5px;
-    border-right-color: #000;
-    border-width: 5px 5px 5px 0;
+  top: 50%;
+  left: 0;
+  margin-top: -5px;
+  border-right-color: #000;
+  border-width: 5px 5px 5px 0;
 }
 
 .tooltip.left .tooltip-arrow {
-    top: 50%;
-    right: 0;
-    margin-top: -5px;
-    border-left-color: #000;
-    border-width: 5px 0 5px 5px;
+  top: 50%;
+  right: 0;
+  margin-top: -5px;
+  border-left-color: #000;
+  border-width: 5px 0 5px 5px;
 }
 
 .tooltip.bottom .tooltip-arrow {
-    top: 0;
-    left: 50%;
-    margin-left: -5px;
-    border-bottom-color: #000;
-    border-width: 0 5px 5px;
+  top: 0;
+  left: 50%;
+  margin-left: -5px;
+  border-bottom-color: #000;
+  border-width: 0 5px 5px;
 }
 
 .tooltip.bottom-left .tooltip-arrow {
-    top: 0;
-    left: 5px;
-    border-bottom-color: #000;
-    border-width: 0 5px 5px;
+  top: 0;
+  left: 5px;
+  border-bottom-color: #000;
+  border-width: 0 5px 5px;
 }
 
 .tooltip.bottom-right .tooltip-arrow {
-    top: 0;
-    right: 5px;
-    border-bottom-color: #000;
-    border-width: 0 5px 5px;
+  top: 0;
+  right: 5px;
+  border-bottom-color: #000;
+  border-width: 0 5px 5px;
 }

+ 34 - 34
client/scss/components/_workflow-tasks.scss

@@ -1,42 +1,42 @@
 .workflow-tasks {
-    $task-width: 117px;
-    $task-height: 56px;
+  $task-width: 117px;
+  $task-height: 56px;
 
-    list-style-type: none;
+  list-style-type: none;
 
-    &__task {
-        display: inline-block;
-        background-color: #f8ffff;
-        border: 2px solid #7ebebe;
-        border-radius: 5px;
-        color: #007d7e;
-        box-sizing: border-box;
-        width: $task-width;
-        height: $task-height;
-        margin: 7px;
-        padding-left: 9px;
-        padding-right: 9px;
-    }
+  &__task {
+    display: inline-block;
+    background-color: #f8ffff;
+    border: 2px solid #7ebebe;
+    border-radius: 5px;
+    color: #007d7e;
+    box-sizing: border-box;
+    width: $task-width;
+    height: $task-height;
+    margin: 7px;
+    padding-left: 9px;
+    padding-right: 9px;
+  }
 
-    &__step {
-        font-size: 10px;
-        text-transform: uppercase;
-        margin-top: 3px;
-    }
+  &__step {
+    font-size: 10px;
+    text-transform: uppercase;
+    margin-top: 3px;
+  }
 
-    &__name {
-        font-size: 16px;
-        font-weight: bold;
-        margin: 0;
+  &__name {
+    font-size: 16px;
+    font-weight: bold;
+    margin: 0;
 
-        overflow: hidden;
-        white-space: nowrap;
-        text-overflow: ellipsis;
-    }
+    overflow: hidden;
+    white-space: nowrap;
+    text-overflow: ellipsis;
+  }
 
-    &__extra-tasks {
-        display: inline-block;
-        height: $task-height;
-        vertical-align: middle;
-    }
+  &__extra-tasks {
+    display: inline-block;
+    height: $task-height;
+    vertical-align: middle;
+  }
 }

+ 3 - 3
client/scss/components/browser-message.scss

@@ -1,5 +1,5 @@
 @include media-breakpoint-up(sm) {
-    .browsermessage {
-        margin: 0 0 0 -150px;
-    }
+  .browsermessage {
+    margin: 0 0 0 -150px;
+  }
 }

+ 0 - 8
client/scss/core.scss

@@ -41,7 +41,6 @@ OTHER PREFIXES
 
 ==============================================================================*/
 
-
 /* SETTINGS
 These are variables, maps, and fonts.
 * No CSS should be produced by these files
@@ -49,7 +48,6 @@ These are variables, maps, and fonts.
 
 @import 'settings';
 
-
 /* TOOLS
 These are functions and mixins.
 * No CSS should be produced by these files.
@@ -57,7 +55,6 @@ These are functions and mixins.
 
 @import 'tools';
 
-
 /* GENERIC
 This is for resets and other rules that affect large collections of bare elements.
 * Changes to them should be very rare.
@@ -65,7 +62,6 @@ This is for resets and other rules that affect large collections of bare element
 
 // @import 'generic/generic';
 
-
 /* ELEMENTS
 These are base styles for bare HTML elements.
 * Changes to them should be very rare.
@@ -76,7 +72,6 @@ These are base styles for bare HTML elements.
 @import 'elements/forms';
 @import 'elements/root';
 
-
 /* OBJECTS
 These are classes related to layout, known as 'objects' in ITCSS or OOCSS.
 * This is for grids, wrappers, and other non-consmetic layout utilities.
@@ -86,7 +81,6 @@ These are classes related to layout, known as 'objects' in ITCSS or OOCSS.
 @import 'objects/objects';
 @import 'objects/avatar';
 
-
 /* COMPONENTS
 These are classes for components.
 * These classes (unless legacy) are prefixed with `.c-`.
@@ -147,7 +141,6 @@ These are classes that provide overrides.
 @import 'overrides/vendor.datetimepicker';
 @import 'overrides/vendor.tagit';
 
-
 // UTILITIES: classes that do one simple thing.
 @import 'overrides/utilities.hidden';
 @import 'overrides/utilities.text';
@@ -159,7 +152,6 @@ These are classes that provide overrides.
 @import 'overrides/utilities.text.legacy';
 @import 'overrides/utilities.legacy';
 
-
 // PAGES: page-specific overrides
 @import 'overrides/pages.homepage';
 @import 'overrides/pages.page-explorer';

+ 22 - 22
client/scss/elements/_elements.scss

@@ -1,35 +1,35 @@
 html {
-    background: $color-grey-4;
-    height: 100%;
+  background: $color-grey-4;
+  height: 100%;
 }
 
 body {
-    overflow-x: hidden;
-    position: relative;
+  overflow-x: hidden;
+  position: relative;
 
-    &:after {
-        content: '';
-        position: fixed;
-        transition: visibility 0s linear 0s, opacity 0.2s ease-out;
-        background: rgba(255, 255, 255, 0.5);
-        width: 100%;
-        height: 100%;
-        top: 0;
-        left: 0;
-        z-index: 5;
-        opacity: 0;
-        visibility: hidden;
-    }
+  &:after {
+    content: '';
+    position: fixed;
+    transition: visibility 0s linear 0s, opacity 0.2s ease-out;
+    background: rgba(255, 255, 255, 0.5);
+    width: 100%;
+    height: 100%;
+    top: 0;
+    left: 0;
+    z-index: 5;
+    opacity: 0;
+    visibility: hidden;
+  }
 }
 
 hr {
-    border: 1px solid $color-grey-4;
-    border-width: 1px 0 0;
-    margin: 1.5em 0;
+  border: 1px solid $color-grey-4;
+  border-width: 1px 0 0;
+  margin: 1.5em 0;
 }
 
 // general image style
 img {
-    max-width: 100%;
-    height: auto;
+  max-width: 100%;
+  height: auto;
 }

+ 191 - 194
client/scss/elements/_forms.scss

@@ -1,242 +1,239 @@
-@use "sass:map";
+@use 'sass:map';
 // These are the generic stylings for forms of any type.
 // If you're styling something specific to the page editing interface,
 // it probably ought to go in layouts/page-editor.scss
 form {
-    ul,
+  ul,
+  li {
+    list-style-type: none;
+  }
 
-    li {
-        list-style-type: none;
-    }
-
-    ul {
-        margin: 0;
-        padding: 0;
-    }
+  ul {
+    margin: 0;
+    padding: 0;
+  }
 }
 
 fieldset {
-    border: 0;
-    padding: 0 0 2em;
-    margin: 0;
+  border: 0;
+  padding: 0 0 2em;
+  margin: 0;
 }
 
 legend {
-    @include visuallyhidden();
+  @include visuallyhidden();
 }
 
 label,
 .label {
-    text-transform: none;
-    font-weight: bold;
-    color: $color-grey-1;
-    font-size: 1.1em;
-    display: block;
-    padding: 0 0 0.8em;
-    margin: 0;
-    line-height: 1.3em;
-
-    .checkbox &,
-    .radio & {
-        display: inline;
-    }
-
-    &.no-float {
-        float: none;
-    }
-
-    &.disabled {
-        opacity: 0.7;
-        cursor: not-allowed;
+  text-transform: none;
+  font-weight: bold;
+  color: $color-grey-1;
+  font-size: 1.1em;
+  display: block;
+  padding: 0 0 0.8em;
+  margin: 0;
+  line-height: 1.3em;
+
+  .checkbox &,
+  .radio & {
+    display: inline;
+  }
+
+  &.no-float {
+    float: none;
+  }
+
+  &.disabled {
+    opacity: 0.7;
+    cursor: not-allowed;
+  }
+
+  @include media-breakpoint-up(sm) {
+    @include column(2);
+    padding-top: 1.2em;
+    padding-left: 0;
+
+    .radio_select &,
+    .multiple_choice_field &,
+    .model_multiple_choice_field &,
+    .checkbox_select_multiple &,
+    .boolean_field &,
+    .model_choice_field &,
+    .image_field & {
+      padding-top: 0;
     }
 
-    @include media-breakpoint-up(sm) {
-        @include column(2);
-        padding-top: 1.2em;
-        padding-left: 0;
-
-        .radio_select &,
-        .multiple_choice_field &,
-        .model_multiple_choice_field &,
-        .checkbox_select_multiple &,
-        .boolean_field &,
-        .model_choice_field &,
-        .image_field & {
-            padding-top: 0;
-        }
-
-        // Horrid specificity war
-        .model_choice_field.select & {
-            padding-top: 1.2em;
-        }
+    // Horrid specificity war
+    .model_choice_field.select & {
+      padding-top: 1.2em;
     }
+  }
 }
 
-input:not([type=submit]),
+input:not([type='submit']),
 textarea,
 select,
 .halloeditor,
 .tagit {
-    appearance: none;
-    box-sizing: border-box;
-    border-radius: 6px;
-    width: 100%;
-    font-family: $font-sans;
-    border: 1px solid $color-input-border;
-    padding: 0.9em 1.2em;
-    background-color: $color-fieldset-hover;
-    color: $color-text-input;
-    font-size: 1.2em;
-    font-weight: 300;
-
-    &:hover {
-        background-color: $color-white;
-    }
-
-    &:focus {
-        background-color: $color-input-focus;
-        border-color: $color-input-focus-border;
-    }
-
-    &:disabled,
-    &[disabled],
-    &:disabled:hover,
-    &[disabled]:hover {
-        background-color: $color-grey-4;
-        cursor: not-allowed;
-        color: $color-grey-2;
-    }
+  appearance: none;
+  box-sizing: border-box;
+  border-radius: 6px;
+  width: 100%;
+  font-family: $font-sans;
+  border: 1px solid $color-input-border;
+  padding: 0.9em 1.2em;
+  background-color: $color-fieldset-hover;
+  color: $color-text-input;
+  font-size: 1.2em;
+  font-weight: 300;
+
+  &:hover {
+    background-color: $color-white;
+  }
+
+  &:focus {
+    background-color: $color-input-focus;
+    border-color: $color-input-focus-border;
+  }
+
+  &:disabled,
+  &[disabled],
+  &:disabled:hover,
+  &[disabled]:hover {
+    background-color: $color-grey-4;
+    cursor: not-allowed;
+    color: $color-grey-2;
+  }
 }
 
 @media (forced-colors: $media-forced-colours) {
-    .tagit,
-    .field-content .tagit .tagit-choice,
-    .tagit .tagit-new .ui-widget-content {
-        box-shadow: inset 1000px 0 0 0 $color-black;
-        color: $color-white;
-        forced-color-adjust: none;
-    }
+  .tagit,
+  .field-content .tagit .tagit-choice,
+  .tagit .tagit-new .ui-widget-content {
+    box-shadow: inset 1000px 0 0 0 $color-black;
+    color: $color-white;
+    forced-color-adjust: none;
+  }
 
-    .tagit span.tagit-label:before,
-    .tagit span.tagit-label {
-        color: $color-black;
-        forced-color-adjust: none;
-    }
+  .tagit span.tagit-label:before,
+  .tagit span.tagit-label {
+    color: $color-black;
+    forced-color-adjust: none;
+  }
 }
 
 // Reset the arrow on `<select>`s in IE10+.
 select::-ms-expand {
-    display: none;
+  display: none;
 }
 
 .file_field {
-    .input {
-        label {
-            float: none;
-            display: inline;
-            padding: 0;
-        }
-
-        input[type=checkbox] {
-            margin-top: 5px;
-        }
-
-        a {
-            &:after {
-                content: ' ';
-                display: block;
-            }
-        }
+  .input {
+    label {
+      float: none;
+      display: inline;
+      padding: 0;
     }
-}
-
-
-// radio and check boxes
-input[type=radio],
-input[type=checkbox] {
-    border-radius: 0;
-    cursor: pointer;
-    border: 0;
-    padding: 0;
-}
 
-input[type=radio] {
-    height: 12px;
-    width: auto;
-    position: relative;
-    margin-right: 27px;
-}
-
-input[type=radio]:before {
-    border-radius: 100%;
-    font-family: $font-wagtail-icons;
-    font-style: normal;
-    text-align: center;
-    position: absolute;
-    top: -5px;
-    left: -2px;
-    cursor: pointer;
-    display: block;
-    content: map.get($icons, 'radio-full');
-    width: 1em;
-    height: 1em;
-    line-height: 1.1em;
-    padding: 4px;
-    background-color: $color-white;
-    color: $color-grey-4;
-    border: 1px solid $color-grey-4;
-}
-
-input[type=radio]:checked:before {
-    content: map.get($icons, 'radio-full');
-    color: $color-teal;
-}
-
-input[type=checkbox] {
-    height: 12px;
-    width: 22px;
-    position: relative;
-    margin-right: 5px;
-}
-
-input[type=checkbox]:before {
-    font-family: $font-wagtail-icons;
-    font-style: normal;
-    text-align: center;
-    position: absolute;
-    top: -5px;
-    cursor: pointer;
-    display: block;
-    content: '';
-    line-height: 20px;
-    width: 20px;
-    height: 20px;
-    background-color: $color-white;
-    border: 1px solid $color-grey-4;
-    color: $color-teal;
-}
+    input[type='checkbox'] {
+      margin-top: 5px;
+    }
 
-input[type=checkbox]:checked:before {
-    content: map.get($icons, 'tick');
+    a {
+      &:after {
+        content: ' ';
+        display: block;
+      }
+    }
+  }
 }
 
-input[type=checkbox][disabled]:before {
-    cursor: not-allowed;
+// radio and check boxes
+input[type='radio'],
+input[type='checkbox'] {
+  border-radius: 0;
+  cursor: pointer;
+  border: 0;
+  padding: 0;
+}
+
+input[type='radio'] {
+  height: 12px;
+  width: auto;
+  position: relative;
+  margin-right: 27px;
+}
+
+input[type='radio']:before {
+  border-radius: 100%;
+  font-family: $font-wagtail-icons;
+  font-style: normal;
+  text-align: center;
+  position: absolute;
+  top: -5px;
+  left: -2px;
+  cursor: pointer;
+  display: block;
+  content: map.get($icons, 'radio-full');
+  width: 1em;
+  height: 1em;
+  line-height: 1.1em;
+  padding: 4px;
+  background-color: $color-white;
+  color: $color-grey-4;
+  border: 1px solid $color-grey-4;
+}
+
+input[type='radio']:checked:before {
+  content: map.get($icons, 'radio-full');
+  color: $color-teal;
+}
+
+input[type='checkbox'] {
+  height: 12px;
+  width: 22px;
+  position: relative;
+  margin-right: 5px;
+}
+
+input[type='checkbox']:before {
+  font-family: $font-wagtail-icons;
+  font-style: normal;
+  text-align: center;
+  position: absolute;
+  top: -5px;
+  cursor: pointer;
+  display: block;
+  content: '';
+  line-height: 20px;
+  width: 20px;
+  height: 20px;
+  background-color: $color-white;
+  border: 1px solid $color-grey-4;
+  color: $color-teal;
+}
+
+input[type='checkbox']:checked:before {
+  content: map.get($icons, 'tick');
+}
+
+input[type='checkbox'][disabled]:before {
+  cursor: not-allowed;
 }
 
-
 // Special styles to counteract Firefox's completely unwarranted assumptions about button styles
-input[type=submit],
-input[type=reset],
-input[type=button],
+input[type='submit'],
+input[type='reset'],
+input[type='button'],
 button {
-    padding: 0 1em;
+  padding: 0 1em;
 
-    @include media-breakpoint-up(sm) {
-        &.button-small {
-            height: 2em;
-        }
+  @include media-breakpoint-up(sm) {
+    &.button-small {
+      height: 2em;
     }
+  }
 }
 
 // Transitions
@@ -244,5 +241,5 @@ fieldset,
 input,
 textarea,
 select {
-    @include transition(background-color 0.2s ease);
+  @include transition(background-color 0.2s ease);
 }

+ 31 - 7
client/scss/elements/_root.scss

@@ -1,10 +1,34 @@
 :root {
-    @include define-color('color-primary', #007d7e);
-    @include define-color('color-primary-darker', css-darken(css-adjust-hue(get-color('color-primary'), 1), 4%));
-    @include define-color('color-primary-dark', css-darken(css-adjust-hue(get-color('color-primary'), 1), 7%));
-    @include define-color('color-primary-lighter', css-lighten(css-desaturate(css-adjust-hue(get-color('color-primary'), 1), 46%), 48%));
-    @include define-color('color-primary-light', css-lighten(css-desaturate(css-adjust-hue(get-color('color-primary'), 1), 44%), 58%));
+  @include define-color('color-primary', #007d7e);
+  @include define-color(
+    'color-primary-darker',
+    css-darken(css-adjust-hue(get-color('color-primary'), 1), 4%)
+  );
+  @include define-color(
+    'color-primary-dark',
+    css-darken(css-adjust-hue(get-color('color-primary'), 1), 7%)
+  );
+  @include define-color(
+    'color-primary-lighter',
+    css-lighten(
+      css-desaturate(css-adjust-hue(get-color('color-primary'), 1), 46%),
+      48%
+    )
+  );
+  @include define-color(
+    'color-primary-light',
+    css-lighten(
+      css-desaturate(css-adjust-hue(get-color('color-primary'), 1), 44%),
+      58%
+    )
+  );
 
-    @include define-color('color-input-focus', css-lighten(css-desaturate(get-color('color-primary'), 40%), 72%));
-    @include define-color('color-input-focus-border', css-lighten(css-saturate(get-color('color-primary'), 12%), 10%));
+  @include define-color(
+    'color-input-focus',
+    css-lighten(css-desaturate(get-color('color-primary'), 40%), 72%)
+  );
+  @include define-color(
+    'color-input-focus-border',
+    css-lighten(css-saturate(get-color('color-primary'), 12%), 10%)
+  );
 }

+ 42 - 42
client/scss/elements/_typography.scss

@@ -1,9 +1,9 @@
 body {
-    -webkit-font-smoothing: antialiased; // Do not remove!
-    font-family: $font-sans;
-    font-size: 85%;
-    line-height: 1.5em;
-    color: $color-text-base;
+  -webkit-font-smoothing: antialiased; // Do not remove!
+  font-family: $font-sans;
+  font-size: 85%;
+  line-height: 1.5em;
+  color: $color-text-base;
 }
 
 h1,
@@ -12,75 +12,75 @@ h3,
 h4,
 h5,
 h6 {
-    font-weight: normal;
+  font-weight: normal;
 }
 
 h1 {
-    line-height: 1.3em;
-    font-size: 1.5em;
-    text-transform: uppercase;
-    color: $color-grey-1;
-    font-weight: 700;
+  line-height: 1.3em;
+  font-size: 1.5em;
+  text-transform: uppercase;
+  color: $color-grey-1;
+  font-weight: 700;
 
-    span {
-        text-transform: none;
-        font-weight: 300;
-    }
+  span {
+    text-transform: none;
+    font-weight: 300;
+  }
 }
 
 h2 {
-    text-transform: uppercase;
-    font-size: 1.3em;
-    font-family: $font-sans;
-    font-weight: 600;
-    color: $color-grey-2;
+  text-transform: uppercase;
+  font-size: 1.3em;
+  font-family: $font-sans;
+  font-weight: 600;
+  color: $color-grey-2;
 }
 
 p {
-    margin-top: 0;
+  margin-top: 0;
 }
 
 a {
-    // @include transition(color 0.2s ease, background-color 0.2s ease);
-    color: $color-link;
-    text-decoration: none;
+  // @include transition(color 0.2s ease, background-color 0.2s ease);
+  color: $color-link;
+  text-decoration: none;
 
-    &:hover {
-        color: $color-link-hover;
-    }
+  &:hover {
+    color: $color-link-hover;
+  }
 }
 
 code {
-    box-shadow: inset 0 0 4px 0 rgba(0, 0, 0, 0.2);
-    background-color: $color-fieldset-hover;
-    padding: 2px 5px;
+  box-shadow: inset 0 0 4px 0 rgba(0, 0, 0, 0.2);
+  background-color: $color-fieldset-hover;
+  padding: 2px 5px;
 }
 
 kbd {
-    border-radius: 3px;
-    font-family: $font-sans;
-    border: 1px solid $color-grey-2;
-    border-color: rgba(0, 0, 0, 0.2);
-    padding: 0.3em 0.5em;
+  border-radius: 3px;
+  font-family: $font-sans;
+  border: 1px solid $color-grey-2;
+  border-color: rgba(0, 0, 0, 0.2);
+  padding: 0.3em 0.5em;
 }
 
 dl,
 dt,
 dd {
-    padding: 0;
-    margin: 0;
+  padding: 0;
+  margin: 0;
 }
 
 dl {
-    margin-top: 1em;
+  margin-top: 1em;
 }
 
 dt {
-    color: $color-grey-2;
-    text-transform: uppercase;
-    font-size: 0.9em;
+  color: $color-grey-2;
+  text-transform: uppercase;
+  font-size: 0.9em;
 }
 
 dd {
-    margin-bottom: 1em;
+  margin-bottom: 1em;
 }

+ 31 - 31
client/scss/objects/_avatar.scss

@@ -1,40 +1,40 @@
 // user avatars
 .avatar {
-    border-radius: 100%;
-    position: relative;
-    display: inline-block;
-    vertical-align: middle;
-    text-align: center;
-    overflow: hidden;
-    width: 50px;
-    height: 50px;
+  border-radius: 100%;
+  position: relative;
+  display: inline-block;
+  vertical-align: middle;
+  text-align: center;
+  overflow: hidden;
+  width: 50px;
+  height: 50px;
 
-    img {
-        position: absolute;
-        z-index: 2;
-        top: 0;
-        left: 0;
-        right: 0;
-        border: 0;
-    }
+  img {
+    position: absolute;
+    z-index: 2;
+    top: 0;
+    left: 0;
+    right: 0;
+    border: 0;
+  }
 
-    &.small {
-        vertical-align: middle;
-        margin: 0 0.5em;
-        width: 25px;
-        height: 25px;
-    }
+  &.small {
+    vertical-align: middle;
+    margin: 0 0.5em;
+    width: 25px;
+    height: 25px;
+  }
 
-    &.large {
-        width: 100px;
-        height: 100px;
-    }
+  &.large {
+    width: 100px;
+    height: 100px;
+  }
 
-    &.square {
-        border-radius: 0;
+  &.square {
+    border-radius: 0;
 
-        &:before {
-            border-radius: 0;
-        }
+    &:before {
+      border-radius: 0;
     }
+  }
 }

+ 9 - 10
client/scss/objects/_objects.scss

@@ -1,16 +1,15 @@
 .o-pill {
-    display: inline-block;
-    padding: 0.2em 0.5em;
-    border-radius: 0.25em;
-    vertical-align: middle;
-    line-height: 1.5;
+  display: inline-block;
+  padding: 0.2em 0.5em;
+  border-radius: 0.25em;
+  vertical-align: middle;
+  line-height: 1.5;
 }
 
-
 // For dropdowns
 .o-icon {
-    display: inline-block;
-    vertical-align: middle;
-    line-height: 1;
-    margin-top: -0.25rem;
+  display: inline-block;
+  vertical-align: middle;
+  line-height: 1;
+  margin-top: -0.25rem;
 }

+ 1 - 1
client/scss/overrides/_pages.homepage.scss

@@ -1,3 +1,3 @@
 .homepage h1 {
-    text-transform: none;
+  text-transform: none;
 }

+ 1 - 1
client/scss/overrides/_pages.page-explorer.scss

@@ -1,3 +1,3 @@
 .page-explorer h2 {
-    text-transform: none;
+  text-transform: none;
 }

+ 34 - 35
client/scss/overrides/_utilities.dropdowns.scss

@@ -2,20 +2,20 @@
 //  Arrows
 // =============================================================================
 .u-arrow:before {
-    content: '';
-    border: solid 0.35rem transparent;
-    display: block;
-    position: absolute;
+  content: '';
+  border: solid 0.35rem transparent;
+  display: block;
+  position: absolute;
 }
 
 .u-arrow--tl:before {
-    bottom: 100%;
-    left: 1rem;
+  bottom: 100%;
+  left: 1rem;
 }
 
 .dropup .u-arrow--tl:before {
-    top: 100%;
-    transform: rotateZ(180deg);
+  top: 100%;
+  transform: rotateZ(180deg);
 }
 
 // =============================================================================
@@ -26,85 +26,84 @@
 
 // }
 .t-default .u-btn-current {
-    border-color: rgba(0, 0, 0, 0.15);
-    color: $color-teal;
+  border-color: rgba(0, 0, 0, 0.15);
+  color: $color-teal;
 }
 
 .t-default .u-btn-current:hover {
-    background: $color-teal;
-    color: #fff;
-    border-color: $color-teal;
+  background: $color-teal;
+  color: #fff;
+  border-color: $color-teal;
 }
 
 .t-default .u-btn-current:active {
-    background: #333;
-    color: #fff;
-    border-color: #333;
+  background: #333;
+  color: #fff;
+  border-color: #333;
 }
 
 .t-inverted .u-btn-current {
-    border-color: rgba(0, 0, 0, 0.35);
-    color: #fff;
+  border-color: rgba(0, 0, 0, 0.35);
+  color: #fff;
 }
 
 .t-inverted .u-btn-current:hover {
-    background-color: $color-teal-darker;
-    border-color: rgba(0, 0, 0, 0.35);
+  background-color: $color-teal-darker;
+  border-color: rgba(0, 0, 0, 0.35);
 }
 
 .t-inverted .u-btn-current:active {
-    border-color: rgba(0, 0, 0, 0.35);
-    background: #333;
-    color: #fff;
+  border-color: rgba(0, 0, 0, 0.35);
+  background: #333;
+  color: #fff;
 }
 
-
 // =============================================================================
 // Dark theme
 // =============================================================================
 .t-dark .u-link {
-    color: #fff;
+  color: #fff;
 }
 
 .t-dark .u-link:hover {
-    color: #aaa;
+  color: #aaa;
 }
 
 .t-dark .u-background {
-    background: #333;
+  background: #333;
 }
 
 .t-dark .u-arrow:before {
-    border-bottom-color: #333;
+  border-bottom-color: #333;
 }
 
 // =============================================================================
 // Light theme
 // =============================================================================
 .t-light .u-link {
-    color: #333;
+  color: #333;
 }
 
 .t-light .u-link:hover {
-    color: #aaa;
+  color: #aaa;
 }
 
 .t-light .u-background {
-    background: #fff;
-    border-color: #ccc;
+  background: #fff;
+  border-color: #ccc;
 }
 
 .t-light .u-arrow:before {
-    border-bottom-color: #fff;
+  border-bottom-color: #fff;
 }
 
 // =============================================================================
 // States
 // =============================================================================
 .u-toggle {
-    display: none;
+  display: none;
 }
 
 .is-open .u-toggle {
-    display: block;
+  display: block;
 }

+ 2 - 2
client/scss/overrides/_utilities.focus.scss

@@ -3,9 +3,9 @@
 // without individual components having to explicitly define focus styles.
 // Using !important because we want to enforce only one style is used across the UI.
 .focus-outline-on *:focus {
-    outline: $focus-outline-width solid $color-focus-outline !important;
+  outline: $focus-outline-width solid $color-focus-outline !important;
 }
 
 .focus-outline-off *:focus {
-    outline: none !important;
+  outline: none !important;
 }

+ 19 - 19
client/scss/overrides/_utilities.hidden.scss

@@ -1,40 +1,40 @@
 // stylelint-disable declaration-no-important
 .u-hidden {
-    display: none !important;
+  display: none !important;
 }
 
 .u-hidden\@sm {
-    @include media-breakpoint-up(sm) {
-        display: none !important;
-    }
+  @include media-breakpoint-up(sm) {
+    display: none !important;
+  }
 }
 
 .u-hidden\@xs {
-    @include media-breakpoint-down(xs) {
-        display: none !important;
-    }
+  @include media-breakpoint-down(xs) {
+    display: none !important;
+  }
 }
 
 .u-inline\@sm {
-    @include media-breakpoint-up(sm) {
-        display: inline !important;
-    }
+  @include media-breakpoint-up(sm) {
+    display: inline !important;
+  }
 }
 
 .u-inline\@xs {
-    @include media-breakpoint-down(xs) {
-        display: inline !important;
-    }
+  @include media-breakpoint-down(xs) {
+    display: inline !important;
+  }
 }
 
 .u-block\@sm {
-    @include media-breakpoint-up(sm) {
-        display: block !important;
-    }
+  @include media-breakpoint-up(sm) {
+    display: block !important;
+  }
 }
 
 .u-block\@xs {
-    @include media-breakpoint-down(xs) {
-        display: block !important;
-    }
+  @include media-breakpoint-down(xs) {
+    display: block !important;
+  }
 }

+ 20 - 21
client/scss/overrides/_utilities.legacy.scss

@@ -1,55 +1,54 @@
 .clearfix {
-    @include clearfix();
+  @include clearfix();
 }
 
 .nice-padding {
-    padding-left: $mobile-nice-padding;
-    padding-right: $mobile-nice-padding;
+  padding-left: $mobile-nice-padding;
+  padding-right: $mobile-nice-padding;
 
-    @include media-breakpoint-up(sm) {
-        padding-left: $desktop-nice-padding;
-        padding-right: $desktop-nice-padding;
-    }
+  @include media-breakpoint-up(sm) {
+    padding-left: $desktop-nice-padding;
+    padding-right: $desktop-nice-padding;
+  }
 }
 
 @include media-breakpoint-up(sm) {
-    .divider-before {
-        border-left: 1px solid $color-grey-4;
-    }
-
-    .divider-after {
-        border-right: 1px solid $color-grey-4;
-    }
+  .divider-before {
+    border-left: 1px solid $color-grey-4;
+  }
 
+  .divider-after {
+    border-right: 1px solid $color-grey-4;
+  }
 }
 
 body.reordering {
-    overflow: visible;
+  overflow: visible;
 }
 
 // Show a transparency grid in background
 .show-transparency {
-    background: url('#{$images-root}transparency.svg');
+  background: url('#{$images-root}transparency.svg');
 }
 
 // make a block-level element inline
 .inline {
-    display: inline;
+  display: inline;
 }
 
 .inline-block {
-    display: inline-block;
+  display: inline-block;
 }
 
 .block {
-    display: block;
+  display: block;
 }
 
 .unlist {
-    @include unlist();
+  @include unlist();
 }
 
 // utility class to allow things to be scrollable if their contents can't wrap more nicely
 .overflow {
-    overflow: auto;
+  overflow: auto;
 }

+ 1 - 1
client/scss/overrides/_utilities.text.legacy.scss

@@ -1,3 +1,3 @@
 .unbold {
-    font-weight: normal;
+  font-weight: normal;
 }

+ 3 - 3
client/scss/overrides/_utilities.text.scss

@@ -1,11 +1,11 @@
 .u-text-transform-uppercase {
-    text-transform: uppercase;
+  text-transform: uppercase;
 }
 
 .u-text-weight-normal {
-    font-weight: normal;
+  font-weight: normal;
 }
 
 .u-para {
-    margin-bottom: 1rem;
+  margin-bottom: 1rem;
 }

+ 2 - 2
client/scss/overrides/_utilities.visuallyhidden.scss

@@ -1,7 +1,7 @@
 .visuallyvisible {
-    @include visuallyvisible;
+  @include visuallyvisible;
 }
 
 .visuallyhidden {
-    @include visuallyhidden;
+  @include visuallyhidden;
 }

+ 268 - 261
client/scss/overrides/_vendor.datetimepicker.scss

@@ -1,328 +1,335 @@
-@use "sass:map";
+@use 'sass:map';
 
 .xdsoft_datetimepicker {
-    box-shadow: 0 5px 10px -5px rgba(0, 0, 0, 0.4);
-    background: $color-white;
-    border: 1px solid $color-input-focus-border;
-    padding: 8px;
-    padding-left: 0;
-    padding-top: 2px;
-    position: absolute;
-    z-index: 5;
+  box-shadow: 0 5px 10px -5px rgba(0, 0, 0, 0.4);
+  background: $color-white;
+  border: 1px solid $color-input-focus-border;
+  padding: 8px;
+  padding-left: 0;
+  padding-top: 2px;
+  position: absolute;
+  z-index: 5;
+  box-sizing: border-box;
+  display: none;
+
+  * {
     box-sizing: border-box;
-    display: none;
+    padding: 0;
+    margin: 0;
+  }
 
-    * {
-        box-sizing: border-box;
-        padding: 0;
-        margin: 0;
-    }
+  iframe {
+    position: absolute;
+    left: 0;
+    top: 0;
+    width: 75px;
+    height: 210px;
+    background: transparent;
+    border: 0;
+  }
+
+  .xdsoft_datepicker,
+  .xdsoft_timepicker {
+    display: none;
 
-    iframe {
-        position: absolute;
-        left: 0;
-        top: 0;
-        width: 75px;
-        height: 210px;
-        background: transparent;
-        border: 0;
+    &.active {
+      display: block;
     }
+  }
 
-    .xdsoft_datepicker,
-    .xdsoft_timepicker {
-        display: none;
+  .xdsoft_datepicker {
+    float: left;
+    margin-left: 8px;
+  }
 
-        &.active {
-            display: block;
-        }
-    }
+  .xdsoft_datepicker.active + .xdsoft_timepicker {
+    margin-top: 8px;
+    margin-bottom: 3px;
+  }
 
-    .xdsoft_datepicker {
-        float: left;
-        margin-left: 8px;
-    }
+  .xdsoft_mounthpicker {
+    position: relative;
+    text-align: center;
+  }
 
-    .xdsoft_datepicker.active + .xdsoft_timepicker {
-        margin-top: 8px;
-        margin-bottom: 3px;
+  .xdsoft_next,
+  .xdsoft_prev,
+  .xdsoft_today_button {
+    background-color: transparent;
+    cursor: pointer;
+    display: block;
+    border: 0;
+    overflow: hidden;
+    padding: 5px 0;
+    position: relative;
+    white-space: nowrap;
+    width: 2em;
+    color: $color-teal;
+    text-transform: none;
+    text-align: center;
+
+    &:before {
+      font-size: 1.5em;
+      font-family: $font-wagtail-icons;
+      width: 1em;
+      line-height: 1.3em;
+      text-align: center;
+      margin: 0;
     }
 
-    .xdsoft_mounthpicker {
-        position: relative;
-        text-align: center;
+    &:hover {
+      color: $color-teal-darker;
     }
+  }
 
-    .xdsoft_next,
-    .xdsoft_prev,
-    .xdsoft_today_button {
-        background-color: transparent;
-        cursor: pointer;
-        display: block;
-        border: 0;
-        overflow: hidden;
-        padding: 5px 0;
-        position: relative;
-        white-space: nowrap;
-        width: 2em;
-        color: $color-teal;
-        text-transform: none;
-        text-align: center;
+  .xdsoft_prev {
+    float: left;
 
-        &:before {
-            font-size: 1.5em;
-            font-family: $font-wagtail-icons;
-            width: 1em;
-            line-height: 1.3em;
-            text-align: center;
-            margin: 0;
-        }
-
-        &:hover {
-            color: $color-teal-darker;
-        }
+    &:before {
+      content: map.get($icons, 'arrow-left');
     }
+  }
 
-    .xdsoft_prev {
-        float: left;
+  .xdsoft_today_button {
+    float: left;
+    margin-left: 5px;
 
-        &:before {
-            content: map.get($icons, 'arrow-left');
-        }
+    &:before {
+      content: map.get($icons, 'home');
     }
+  }
 
-    .xdsoft_today_button {
-        float: left;
-        margin-left: 5px;
+  .xdsoft_next {
+    float: right;
 
-        &:before {
-            content: map.get($icons, 'home');
-        }
+    &:before {
+      content: map.get($icons, 'arrow-right');
     }
+  }
 
-    .xdsoft_next {
-        float: right;
+  .xdsoft_timepicker {
+    min-width: 70px;
+    float: left;
+    text-align: center;
+    margin-left: 8px;
+    margin-top: 0;
 
-        &:before {
-            content: map.get($icons, 'arrow-right');
-        }
+    .xdsoft_prev,
+    .xdsoft_next {
+      float: none;
+      height: 1.5em;
+      display: block;
+      text-align: center;
+      width: 100%;
+      padding: 0;
+
+      &:before {
+        width: 100%;
+      }
     }
 
-    .xdsoft_timepicker {
-        min-width: 70px;
-        float: left;
-        text-align: center;
-        margin-left: 8px;
-        margin-top: 0;
-
-        .xdsoft_prev,
-        .xdsoft_next {
-            float: none;
-            height: 1.5em;
-            display: block;
-            text-align: center;
-            width: 100%;
-            padding: 0;
-
-            &:before {
-                width: 100%;
-            }
-        }
-
-        .xdsoft_prev:before {
-            content: map.get($icons, 'arrow-up');
-        }
-
-        .xdsoft_next:before {
-            content: map.get($icons, 'arrow-down');
-        }
+    .xdsoft_prev:before {
+      content: map.get($icons, 'arrow-up');
+    }
 
-        .xdsoft_time_box {
-            position: relative;
-            border: 1px solid #ccc;
-            height: 170px;
-            overflow: hidden;
-            border-bottom: 1px solid #ddd;
-
-            > div > div {
-                background: #f5f5f5;
-                border-top: 1px solid #ddd;
-                color: #666;
-                font-size: 1em;
-                text-align: center;
-                border-collapse: collapse;
-                cursor: pointer;
-                border-bottom-width: 0;
-                height: 2.3em;
-                line-height: 2.3em;
-                padding-left: 0.6em;
-                padding-right: 0.6em;
-
-                // stylelint-disable-next-line max-nesting-depth
-                &:first-child {
-                    border-top-width: 0;
-                }
-            }
-        }
+    .xdsoft_next:before {
+      content: map.get($icons, 'arrow-down');
     }
 
-    .xdsoft_label {
-        display: inline;
-        position: relative;
-        z-index: 9999;
-        margin: 0;
-        padding: 5px 3px;
-        font-size: 14px;
-        line-height: 20px;
-        font-weight: bold;
-        background-color: $color-white;
-        float: left;
-        width: 182px;
+    .xdsoft_time_box {
+      position: relative;
+      border: 1px solid #ccc;
+      height: 170px;
+      overflow: hidden;
+      border-bottom: 1px solid #ddd;
+
+      > div > div {
+        background: #f5f5f5;
+        border-top: 1px solid #ddd;
+        color: #666;
+        font-size: 1em;
         text-align: center;
+        border-collapse: collapse;
         cursor: pointer;
-
-        &:hover {
-            text-decoration: underline;
-        }
-
-        > .xdsoft_select {
-            border: 1px solid #ccc;
-            position: absolute;
-            right: 0;
-            top: 30px;
-            z-index: 101;
-            display: none;
-            background: $color-white;
-            max-height: 160px;
-            overflow-y: hidden;
-
-            &.xdsoft_monthselect {right: -7px;}
-
-            &.xdsoft_yearselect {right: 2px;}
-
-            > div > .xdsoft_option:hover {
-                color: $color-white;
-                background: #ff8000;
-            }
-
-            > div > .xdsoft_option {
-                padding: 2px 15px 2px 5px;
-            }
-
-            > div > .xdsoft_option.xdsoft_current {
-                background: #3af;
-                color: $color-white;
-                font-weight: 700;
-            }
+        border-bottom-width: 0;
+        height: 2.3em;
+        line-height: 2.3em;
+        padding-left: 0.6em;
+        padding-right: 0.6em;
+
+        // stylelint-disable-next-line max-nesting-depth
+        &:first-child {
+          border-top-width: 0;
         }
-
+      }
     }
+  }
 
-    .xdsoft_month {
-        width: 90px;
-        text-align: right;
-    }
+  .xdsoft_label {
+    display: inline;
+    position: relative;
+    z-index: 9999;
+    margin: 0;
+    padding: 5px 3px;
+    font-size: 14px;
+    line-height: 20px;
+    font-weight: bold;
+    background-color: $color-white;
+    float: left;
+    width: 182px;
+    text-align: center;
+    cursor: pointer;
 
-    .xdsoft_year {
-        width: 56px;
+    &:hover {
+      text-decoration: underline;
     }
 
-    .xdsoft_calendar {
-        clear: both;
+    > .xdsoft_select {
+      border: 1px solid #ccc;
+      position: absolute;
+      right: 0;
+      top: 30px;
+      z-index: 101;
+      display: none;
+      background: $color-white;
+      max-height: 160px;
+      overflow-y: hidden;
+
+      &.xdsoft_monthselect {
+        right: -7px;
+      }
+
+      &.xdsoft_yearselect {
+        right: 2px;
+      }
+
+      > div > .xdsoft_option:hover {
+        color: $color-white;
+        background: #ff8000;
+      }
 
-        table {
-            border-collapse: collapse;
-        }
+      > div > .xdsoft_option {
+        padding: 2px 15px 2px 5px;
+      }
 
-        td > div {
-            padding-right: 5px;
-        }
+      > div > .xdsoft_option.xdsoft_current {
+        background: #3af;
+        color: $color-white;
+        font-weight: 700;
+      }
+    }
+  }
 
-        td,
-        th {
-            width: 14.285%;
-            border: 1px solid #ddd;
-            color: #666;
-            font-size: 12px;
-            text-align: right;
-            padding: 5px 7px;
-            border-collapse: collapse;
-            cursor: pointer;
-            height: 25px;
-        }
+  .xdsoft_month {
+    width: 90px;
+    text-align: right;
+  }
 
-        td {
-            background-color: $color-white;
-        }
+  .xdsoft_year {
+    width: 56px;
+  }
 
-        th {
-            background: #f1f1f1;
-            font-weight: 700;
-            font-size: 0.85em;
-            text-align: center;
-            cursor: default;
-        }
-    }
+  .xdsoft_calendar {
+    clear: both;
 
-    .xdsoft_calendar td.xdsoft_default,
-    .xdsoft_calendar td.xdsoft_current,
-    .xdsoft_timepicker .xdsoft_time_box > div > div.xdsoft_current {
-        background: $color-salmon;
-        color: $color-white;
-        font-weight: 700;
+    table {
+      border-collapse: collapse;
     }
 
-    .xdsoft_calendar td.xdsoft_other_month,
-    .xdsoft_calendar td.xdsoft_disabled,
-    .xdsoft_time_box > div > div.xdsoft_disabled {
-        opacity: 0.5;
-        background: $color-grey-3;
+    td > div {
+      padding-right: 5px;
     }
 
-    .xdsoft_calendar td.xdsoft_other_month.xdsoft_disabled {
-        opacity: 0.2;
+    td,
+    th {
+      width: 14.285%;
+      border: 1px solid #ddd;
+      color: #666;
+      font-size: 12px;
+      text-align: right;
+      padding: 5px 7px;
+      border-collapse: collapse;
+      cursor: pointer;
+      height: 25px;
     }
 
-    .xdsoft_calendar td:hover,
-    .xdsoft_timepicker .xdsoft_time_box > div > div:hover {
-        color: $color-white;
-        background: $color-teal;
+    td {
+      background-color: $color-white;
     }
 
-    .xdsoft_calendar td.xdsoft_today {
-        font-weight: 700;
+    th {
+      background: #f1f1f1;
+      font-weight: 700;
+      font-size: 0.85em;
+      text-align: center;
+      cursor: default;
     }
+  }
+
+  .xdsoft_calendar td.xdsoft_default,
+  .xdsoft_calendar td.xdsoft_current,
+  .xdsoft_timepicker .xdsoft_time_box > div > div.xdsoft_current {
+    background: $color-salmon;
+    color: $color-white;
+    font-weight: 700;
+  }
+
+  .xdsoft_calendar td.xdsoft_other_month,
+  .xdsoft_calendar td.xdsoft_disabled,
+  .xdsoft_time_box > div > div.xdsoft_disabled {
+    opacity: 0.5;
+    background: $color-grey-3;
+  }
+
+  .xdsoft_calendar td.xdsoft_other_month.xdsoft_disabled {
+    opacity: 0.2;
+  }
+
+  .xdsoft_calendar td:hover,
+  .xdsoft_timepicker .xdsoft_time_box > div > div:hover {
+    color: $color-white;
+    background: $color-teal;
+  }
+
+  .xdsoft_calendar td.xdsoft_today {
+    font-weight: 700;
+  }
 }
 
 .xdsoft_noselect {
-    user-select: none;
+  user-select: none;
 }
 
-.xdsoft_noselect::selection { background: transparent; }
+.xdsoft_noselect::selection {
+  background: transparent;
+}
 
-.xdsoft_noselect::-moz-selection { background: transparent; }
+.xdsoft_noselect::-moz-selection {
+  background: transparent;
+}
 
 .xdsoft_datetimepicker.xdsoft_inline {
-    display: inline-block;
-    position: static;
-    box-shadow: none;
+  display: inline-block;
+  position: static;
+  box-shadow: none;
 }
 
 .xdsoft_scroller_box {
-    position: relative;
+  position: relative;
 }
 
 .xdsoft_scrollbar {
-    position: absolute;
-    width: 7px;
-    right: 0;
-    top: 0;
-    bottom: 0;
-    cursor: pointer;
-
-    > .xdsoft_scroller {
-        // stylelint-disable-next-line declaration-no-important
-        background: #ccc !important;
-        height: 20px;
-        border-radius: 3px;
-    }
+  position: absolute;
+  width: 7px;
+  right: 0;
+  top: 0;
+  bottom: 0;
+  cursor: pointer;
+
+  > .xdsoft_scroller {
+    // stylelint-disable-next-line declaration-no-important
+    background: #ccc !important;
+    height: 20px;
+    border-radius: 3px;
+  }
 }

+ 24 - 23
client/scss/overrides/_vendor.tagit.scss

@@ -1,44 +1,45 @@
-@use "sass:map";
+@use 'sass:map';
 // taggit tagging
 .tagit {
-    padding: 0.6em 1.2em;
+  padding: 0.6em 1.2em;
 
-    .tagit-choice {
-        border: 0;
-    }
+  .tagit-choice {
+    border: 0;
+  }
 }
 
 // Additional specificity (.admin_tag_widget ) required to override tagit stylesheets,
 // which get added after the core CSS, and otherwise trump our styles.
 .admin_tag_widget ul.tagit input[type='text'] {
-    padding: 0.2em 0.5em;
+  padding: 0.2em 0.5em;
 }
 
 // Additional specificity (.admin_tag_widget ) required to override tagit stylesheets,
 // which get added after the core CSS, and otherwise trump our styles.
 .admin_tag_widget ul.tagit li.tagit-choice-editable {
-    padding: 0 23px 0 0;
+  padding: 0 23px 0 0;
 }
 
-.ui-front { // provided by jqueryui but not high enough an index
-    z-index: 1000;
+.ui-front {
+  // provided by jqueryui but not high enough an index
+  z-index: 1000;
 }
 
 .tagit-close {
-    .ui-icon-close {
-        margin-left: 1em;
-        text-indent: 0;
-        background: none;
-    }
+  .ui-icon-close {
+    margin-left: 1em;
+    text-indent: 0;
+    background: none;
+  }
 
-    .ui-icon-close:before {
-        font-family: $font-wagtail-icons;
-        display: block;
-        color: $color-grey-3;
-        content: map.get($icons, 'cross');
-    }
+  .ui-icon-close:before {
+    font-family: $font-wagtail-icons;
+    display: block;
+    color: $color-grey-3;
+    content: map.get($icons, 'cross');
+  }
 
-    .ui-icon-close:hover:before {
-        color: $color-red;
-    }
+  .ui-icon-close:hover:before {
+    color: $color-red;
+  }
 }

+ 86 - 86
client/scss/settings/_variables.icons.scss

@@ -1,91 +1,91 @@
-@use "sass:map";
+@use 'sass:map';
 $icons: (
-    'arrow-down-big': '\e030',
-    'arrow-down': '\e01a',
-    'arrow-left': '\e022',
-    'arrow-right': '\e017',
-    'arrow-up-big': '\e02f',
-    'arrow-up': '\e010',
-    'arrows-up-down': '\e016',
-    'bin': '\e038',
-    'bold': '\e026',
-    'chain-broken': '\e047',
-    'code': '\e001',
-    'cog': '\e020',
-    'cogs': '\e00c',
-    'collapse-down': '\e03f',
-    'collapse-up': '\e03e',
-    'cross': '\e012',
-    'date': '\e045',
-    'doc-empty-inverse': '\e00d',
-    'doc-empty': '\e00e',
-    'doc-full-inverse': '\e01b',
-    'doc-full': '\e018',
-    'download': '\e044',
-    'duplicate': '\e902',
-    'edit': '\e00f',
-    'folder-inverse': '\e014',
-    'folder-open-1': '\e013',
-    'folder-open-inverse': '\e01f',
-    'folder': '\e01c',
-    'form': '\e00b',
-    'grip': '\e03b',
-    'group': '\e031',
-    'help': '\e041',
-    // help-inverse directly renders the corresponding character.
-    'help-inverse': '?',
-    'home': '\e035',
-    // horizontalrule is not rendered as an icon font – it uses a unicode dash character rendered with a fallback font.
-    'horizontalrule': '\2014',
-    'image': '\e019',
-    'italic': '\e027',
-    'link': '\e02c',
-    'list-ol': '\e029',
-    'list-ul': '\e028',
-    'locked': '\e009',
-    'logout': '\e049',
-    'mail': '\e015',
-    'media': '\e032',
-    'no-view': '\e006',
-    'openquote': '\e000',
-    'order-down': '\e036',
-    'order-up': '\e037',
-    'order': '\e034',
-    'password': '\e033',
-    'pick': '\e03d',
-    'pilcrow': '\e002',
-    'placeholder': '\e003',
-    'plus-inverse': '\e024',
-    'plus': '\e01d',
-    'radio-empty': '\e02e',
-    'radio-full': '\e02d',
-    'redirect': '\e03c',
-    'repeat': '\e02b',
-    'search': '\e011',
-    'site': '\e007',
-    'snippet': '\e025',
-    'spinner': '\e03a',
-    'strikethrough': '\e04a',
-    'subscript': '\e04c',
-    'success': '\e043',
-    'superscript': '\e04b',
-    'table': '\e048',
-    'tag': '\e01e',
-    'tick-inverse': '\e023',
-    'tick': '\e021',
-    'time': '\e008',
-    'title': '\e046',
-    'undo': '\e02a',
-    'unlocked': '\e00a',
-    'user': '\e004',
-    'view': '\e005',
-    'wagtail-inverse': '\e040',
-    'wagtail': '\e039',
-    'warning': '\e042',
+  'arrow-down-big': '\e030',
+  'arrow-down': '\e01a',
+  'arrow-left': '\e022',
+  'arrow-right': '\e017',
+  'arrow-up-big': '\e02f',
+  'arrow-up': '\e010',
+  'arrows-up-down': '\e016',
+  'bin': '\e038',
+  'bold': '\e026',
+  'chain-broken': '\e047',
+  'code': '\e001',
+  'cog': '\e020',
+  'cogs': '\e00c',
+  'collapse-down': '\e03f',
+  'collapse-up': '\e03e',
+  'cross': '\e012',
+  'date': '\e045',
+  'doc-empty-inverse': '\e00d',
+  'doc-empty': '\e00e',
+  'doc-full-inverse': '\e01b',
+  'doc-full': '\e018',
+  'download': '\e044',
+  'duplicate': '\e902',
+  'edit': '\e00f',
+  'folder-inverse': '\e014',
+  'folder-open-1': '\e013',
+  'folder-open-inverse': '\e01f',
+  'folder': '\e01c',
+  'form': '\e00b',
+  'grip': '\e03b',
+  'group': '\e031',
+  'help': '\e041',
+  // help-inverse directly renders the corresponding character.
+  'help-inverse': '?',
+  'home': '\e035',
+  // horizontalrule is not rendered as an icon font – it uses a unicode dash character rendered with a fallback font.
+  'horizontalrule': '\2014',
+  'image': '\e019',
+  'italic': '\e027',
+  'link': '\e02c',
+  'list-ol': '\e029',
+  'list-ul': '\e028',
+  'locked': '\e009',
+  'logout': '\e049',
+  'mail': '\e015',
+  'media': '\e032',
+  'no-view': '\e006',
+  'openquote': '\e000',
+  'order-down': '\e036',
+  'order-up': '\e037',
+  'order': '\e034',
+  'password': '\e033',
+  'pick': '\e03d',
+  'pilcrow': '\e002',
+  'placeholder': '\e003',
+  'plus-inverse': '\e024',
+  'plus': '\e01d',
+  'radio-empty': '\e02e',
+  'radio-full': '\e02d',
+  'redirect': '\e03c',
+  'repeat': '\e02b',
+  'search': '\e011',
+  'site': '\e007',
+  'snippet': '\e025',
+  'spinner': '\e03a',
+  'strikethrough': '\e04a',
+  'subscript': '\e04c',
+  'success': '\e043',
+  'superscript': '\e04b',
+  'table': '\e048',
+  'tag': '\e01e',
+  'tick-inverse': '\e023',
+  'tick': '\e021',
+  'time': '\e008',
+  'title': '\e046',
+  'undo': '\e02a',
+  'unlocked': '\e00a',
+  'user': '\e004',
+  'view': '\e005',
+  'wagtail-inverse': '\e040',
+  'wagtail': '\e039',
+  'warning': '\e042',
 );
 
 $icons-after: (
-    'arrow-down-after': map.get($icons, 'arrow-down'),
-    'arrow-right-after': map.get($icons, 'arrow-right'),
-    'arrow-up-after': map.get($icons, 'arrow-up'),
+  'arrow-down-after': map.get($icons, 'arrow-down'),
+  'arrow-right-after': map.get($icons, 'arrow-right'),
+  'arrow-up-after': map.get($icons, 'arrow-up'),
 );

+ 30 - 11
client/scss/settings/_variables.scss

@@ -1,4 +1,4 @@
-@use "sass:color";
+@use 'sass:color';
 // paths
 
 // We can't use absolute paths here, because those are dependent on Django's
@@ -19,11 +19,15 @@ $desktop-nice-padding: 50px;
 
 // screen breakpoints
 $breakpoints: (
-    xs: 0,
-    sm: 50em,    // 800px
-    md: 56.25em, // 900px
-    lg: 75em,    // 1200px
-    xl: 100em,   // 1440px
+  xs: 0,
+  sm: 50em,
+  // 800px
+  md: 56.25em,
+  // 900px
+  lg: 75em,
+  // 1200px
+  xl: 100em,
+  // 1440px
 );
 
 // colours
@@ -62,7 +66,10 @@ $color-fieldset-hover: $color-grey-5;
 $color-input-border: $color-grey-4;
 $color-input-focus: var(--color-input-focus);
 $color-input-focus-border: var(--color-input-focus-border);
-$color-input-error-bg: color.adjust(color.adjust($color-red, $saturation: 28%), $lightness: 45%);
+$color-input-error-bg: color.adjust(
+  color.adjust($color-red, $saturation: 28%),
+  $lightness: 45%
+);
 
 $color-button: $color-teal;
 $color-button-hover: $color-teal-darker;
@@ -71,7 +78,10 @@ $color-button-yes-hover: color.adjust($color-button-yes, $lightness: -8%);
 $color-button-no: $color-red-dark;
 $color-button-no-hover: color.adjust($color-button-no, $lightness: -20%);
 $color-button-warning: $color-orange-dark;
-$color-button-warning-hover: color.adjust($color-button-warning, $lightness: -20%);
+$color-button-warning-hover: color.adjust(
+  $color-button-warning,
+  $lightness: -20%
+);
 
 $color-link: $color-teal-darker;
 $color-link-hover: $color-teal-dark;
@@ -95,7 +105,8 @@ $system-color-link-text: LinkText;
 $system-color-button-text: ButtonText;
 
 // Fonts
-$font-sans: -apple-system, BlinkMacSystemFont, "Segoe UI", Helvetica, Arial, sans-serif, "Apple Color Emoji", "Segoe UI Emoji";
+$font-sans: -apple-system, BlinkMacSystemFont, 'Segoe UI', Helvetica, Arial,
+  sans-serif, 'Apple Color Emoji', 'Segoe UI Emoji';
 // Legacy icon font, to be removed in the near future.
 $font-wagtail-icons: wagtail;
 
@@ -145,5 +156,13 @@ $nav-search-focus-bg: $nav-item-hover-bg;
 
 // Form Errors
 $color-text-error: color.change($color-red, $saturation: 69%, $lightness: 52%);
-$color-text-error-forced-color: color.change($color-red, $saturation: 100%, $lightness: 50%);
-$color-text-warning-forced-color: color.change($color-orange, $saturation: 100%, $lightness: 70%);
+$color-text-error-forced-color: color.change(
+  $color-red,
+  $saturation: 100%,
+  $lightness: 50%
+);
+$color-text-warning-forced-color: color.change(
+  $color-orange,
+  $saturation: 100%,
+  $lightness: 70%
+);

+ 0 - 7
client/scss/sidebar.scss

@@ -10,7 +10,6 @@ see core.scss for details.
 
 ==============================================================================*/
 
-
 /* SETTINGS
 These are variables, maps, and fonts.
 * No CSS should be produced by these files
@@ -18,7 +17,6 @@ These are variables, maps, and fonts.
 
 @import 'settings';
 
-
 /* TOOLS
 These are functions and mixins.
 * No CSS should be produced by these files.
@@ -26,7 +24,6 @@ These are functions and mixins.
 
 @import 'tools';
 
-
 /* GENERIC
 This is for resets and other rules that affect large collections of bare elements.
 * Changes to them should be very rare.
@@ -34,7 +31,6 @@ This is for resets and other rules that affect large collections of bare element
 
 @import 'generic/generic';
 
-
 /* ELEMENTS
 These are base styles for bare HTML elements.
 * Changes to them should be very rare.
@@ -45,7 +41,6 @@ These are base styles for bare HTML elements.
 @import 'elements/forms';
 @import 'elements/root';
 
-
 /* OBJECTS
 These are classes related to layout, known as 'objects' in ITCSS or OOCSS.
 * This is for grids, wrappers, and other non-consmetic layout utilities.
@@ -55,7 +50,6 @@ These are classes related to layout, known as 'objects' in ITCSS or OOCSS.
 @import 'objects/objects';
 @import 'objects/avatar';
 
-
 /* COMPONENTS
 These are classes for components.
 * These classes (unless legacy) are prefixed with `.c-`.
@@ -69,6 +63,5 @@ These are classes for components.
 @import '../src/components/PageExplorer/PageExplorer';
 @import '../src/components/Sidebar/Sidebar';
 
-
 // Legacy
 @import 'components/icons';

+ 13 - 9
client/scss/tools/_functions.breakpoints.scss

@@ -1,5 +1,5 @@
-@use "sass:list";
-@use "sass:map";
+@use 'sass:list';
+@use 'sass:map';
 // Based upon the fine work and thoughts from Bootstrap v4.
 // Copyright 2011-2018 The Bootstrap Authors
 // Copyright 2011-2018 Twitter, Inc.
@@ -9,23 +9,27 @@
 //    >> breakpoint-next(sm)
 //    md
 @function breakpoint-next($name) {
-    $breakpoint-names: map.keys($breakpoints);
-    $n: list.index($breakpoint-names, $name);
-    @return if($n < list.length($breakpoint-names), list.nth($breakpoint-names, $n + 1), null);
+  $breakpoint-names: map.keys($breakpoints);
+  $n: list.index($breakpoint-names, $name);
+  @return if(
+    $n < list.length($breakpoint-names),
+    list.nth($breakpoint-names, $n + 1),
+    null
+  );
 }
 
 // Minimum breakpoint width. Null for the smallest (first) breakpoint.
 //    >> breakpoint-min(sm)
 //    50em
 @function breakpoint-min($name) {
-    $min: map.get($breakpoints, $name);
-    @return if($min != 0, $min, null);
+  $min: map.get($breakpoints, $name);
+  @return if($min != 0, $min, null);
 }
 
 // Maximum breakpoint width. Null for the largest (last) breakpoint.
 //    >> breakpoint-max(sm)
 //    56.1875em
 @function breakpoint-max($name) {
-    $next: breakpoint-next($name);
-    @return if($next, breakpoint-min($next) - 0.0625em, null);
+  $next: breakpoint-next($name);
+  @return if($next, breakpoint-min($next) - 0.0625em, null);
 }

+ 14 - 15
client/scss/tools/_mixins.breakpoints.scss

@@ -3,29 +3,28 @@
 // Copyright 2011-2018 Twitter, Inc.
 // Licensed under MIT (https://github.com/twbs/bootstrap/blob/main/LICENSE)
 
-
 // Media of at least the minimum breakpoint width. No query for the smallest breakpoint.
 // Makes the @content apply to the given breakpoint and wider.
 @mixin media-breakpoint-up($name) {
-    $min: breakpoint-min($name);
-    @if $min {
-        @media screen and (min-width: $min) {
-            @content;
-        }
-    } @else {
-        @content;
+  $min: breakpoint-min($name);
+  @if $min {
+    @media screen and (min-width: $min) {
+      @content;
     }
+  } @else {
+    @content;
+  }
 }
 
 // Media of at most the maximum breakpoint width. No query for the largest breakpoint.
 // Makes the @content apply to the given breakpoint and narrower.
 @mixin media-breakpoint-down($name) {
-    $max: breakpoint-max($name);
-    @if $max {
-        @media screen and (max-width: $max) {
-            @content;
-        }
-    } @else {
-        @content;
+  $max: breakpoint-max($name);
+  @if $max {
+    @media screen and (max-width: $max) {
+      @content;
     }
+  } @else {
+    @content;
+  }
 }

+ 66 - 68
client/scss/tools/_mixins.general.scss

@@ -6,111 +6,109 @@
 
 // Turns on font-smoothing when used. Use sparingly.
 @mixin font-smoothing {
-    -webkit-font-smoothing: antialiased;
-    -moz-osx-font-smoothing: grayscale;
+  -webkit-font-smoothing: antialiased;
+  -moz-osx-font-smoothing: grayscale;
 }
 
 @mixin clearfix() {
-    &:before,
-    &:after {
-        content: ' ';
-        display: table;
-    }
-
-    &:after {
-        clear: both;
-    }
+  &:before,
+  &:after {
+    content: ' ';
+    display: table;
+  }
+
+  &:after {
+    clear: both;
+  }
 }
 
 @mixin unlist() {
-    margin-top: 0;
-    margin-bottom: 0;
-    padding-left: 0;
+  margin-top: 0;
+  margin-bottom: 0;
+  padding-left: 0;
+  list-style-type: none;
+  font-style: normal;
+
+  li {
     list-style-type: none;
     font-style: normal;
-
-    li {
-        list-style-type: none;
-        font-style: normal;
-    }
+  }
 }
 
-
 // remove list styles, but only for the immediate element -
 // allow nested lists inside it to keep the default style
 
 @mixin unlistimmediate() {
-    margin-top: 0;
-    margin-bottom: 0;
-    padding-left: 0;
+  margin-top: 0;
+  margin-bottom: 0;
+  padding-left: 0;
+  list-style-type: none;
+  font-style: normal;
+
+  > li {
     list-style-type: none;
     font-style: normal;
-
-    > li {
-        list-style-type: none;
-        font-style: normal;
-    }
+  }
 }
 
 @mixin transition($transition...) {
-    body.ready & {
-        transition: $transition;
-    }
+  body.ready & {
+    transition: $transition;
+  }
 }
 
 @mixin visuallyhidden {
-    border: 0;
-    clip: rect(0 0 0 0);
-    height: 1px;
-    margin: -1px;
-    overflow: hidden;
-    padding: 0;
-    position: absolute;
-    width: 1px;
+  border: 0;
+  clip: rect(0 0 0 0);
+  height: 1px;
+  margin: -1px;
+  overflow: hidden;
+  padding: 0;
+  position: absolute;
+  width: 1px;
 }
 
-
 @mixin visuallyvisible {
-    clip: auto;
-    height: auto;
-    width: auto;
-    margin: initial;
-    overflow: visible;
-    position: initial;
+  clip: auto;
+  height: auto;
+  width: auto;
+  margin: initial;
+  overflow: visible;
+  position: initial;
 }
 
-@mixin svg-icon ($size: 1.5em, $position: text-top) {
-    width: $size;
-    height: $size;
-    vertical-align: $position;
+@mixin svg-icon($size: 1.5em, $position: text-top) {
+  width: $size;
+  height: $size;
+  vertical-align: $position;
 }
 
-@mixin icon () {
-    @include font-smoothing;
-    font-family: $font-wagtail-icons;
-    font-style: normal;
-    font-weight: normal;
-    font-variant: normal;
-    text-transform: none;
-    speak: none;
-    text-decoration: none;
-    width: 1.3em;
-    line-height: 1em;
-    text-align: left;
-    vertical-align: middle;
-    margin-right: 0.2em;
+@mixin icon() {
+  @include font-smoothing;
+  font-family: $font-wagtail-icons;
+  font-style: normal;
+  font-weight: normal;
+  font-variant: normal;
+  text-transform: none;
+  speak: none;
+  text-decoration: none;
+  width: 1.3em;
+  line-height: 1em;
+  text-align: left;
+  vertical-align: middle;
+  margin-right: 0.2em;
 }
 
 // Applies given rules on hover, except for touch screens.
 // Relies on feature detection to add a no-touch class on the html element.
 @mixin hover {
-    .no-touch &:hover {
-        @content;
-    }
+  .no-touch &:hover {
+    @content;
+  }
 }
 
 // Where included, show the focus outline within focusable items instead of around them.
 // This is useful when focusable items are tightly packed and there is no space in-between.
 @mixin show-focus-outline-inside {
-    outline-offset: -1 * $focus-outline-width;
+  outline-offset: -1 * $focus-outline-width;
 }

+ 34 - 34
client/scss/tools/_mixins.grid.scss

@@ -1,75 +1,75 @@
-@use "sass:math";
+@use 'sass:math';
 
 // Utility variable - you should never need to modify this
 $padding: math.div($grid-gutter-width, 2);
 
 // Our row container
 @mixin row($padding: 0) {
-    @include clearfix();
-    box-sizing: border-box;
-    display: block;
-    margin-right: auto;
-    margin-left: auto;
-    padding-right: $padding;
-    padding-left: $padding;
+  @include clearfix();
+  box-sizing: border-box;
+  display: block;
+  margin-right: auto;
+  margin-left: auto;
+  padding-right: $padding;
+  padding-left: $padding;
 }
 
 @mixin row-flush() {
-    margin-left: -$padding;
-    margin-right: -$padding;
+  margin-left: -$padding;
+  margin-right: -$padding;
 }
 
 // Our column container
 @mixin column($x, $padding: $padding, $grid-columns: $grid-columns) {
-    box-sizing: border-box;
-    display: inline;
-    float: left;
-    width: 100% * math.div($x, $grid-columns);
-    padding-right: $padding;
-    padding-left: $padding;
+  box-sizing: border-box;
+  display: inline;
+  float: left;
+  width: 100% * math.div($x, $grid-columns);
+  padding-right: $padding;
+  padding-left: $padding;
 }
 
 @mixin table-column($x, $padding: $padding, $grid-columns: $grid-columns) {
-    box-sizing: border-box;
-    width: 100% * math.div($x, $grid-columns);
+  box-sizing: border-box;
+  width: 100% * math.div($x, $grid-columns);
 }
 
 // Push adds left padding
 @mixin push($offset: 1, $grid-columns: $grid-columns) {
-    margin-left: 100% * math.div($offset, $grid-columns);
+  margin-left: 100% * math.div($offset, $grid-columns);
 }
 
 @mixin push-padding($offset: 1, $grid-columns: $grid-columns) {
-    padding-left: 100% * math.div($offset, $grid-columns);
+  padding-left: 100% * math.div($offset, $grid-columns);
 }
 
 // Pull adds right padding
 @mixin pull($offset: 1, $grid-columns: $grid-columns) {
-    margin-right: 100% * math.div($offset, $grid-columns);
+  margin-right: 100% * math.div($offset, $grid-columns);
 }
 
 @mixin pull-padding($offset: 1, $grid-columns: $grid-columns) {
-    padding-right: 100% * math.div($offset, $grid-columns);
+  padding-right: 100% * math.div($offset, $grid-columns);
 }
 
 // only used in places where padding not applied to same elements as row or row-flush
 // most of the time this class should be applied directly to the html elements
 @mixin nice-padding {
-    padding-left: $mobile-nice-padding;
-    padding-right: $mobile-nice-padding;
+  padding-left: $mobile-nice-padding;
+  padding-right: $mobile-nice-padding;
 
-    @include media-breakpoint-up(sm) {
-        padding-left: $desktop-nice-padding;
-        padding-right: $desktop-nice-padding;
-    }
+  @include media-breakpoint-up(sm) {
+    padding-left: $desktop-nice-padding;
+    padding-right: $desktop-nice-padding;
+  }
 }
 
 @mixin nice-margin {
-    margin-left: $mobile-nice-padding;
-    margin-right: $mobile-nice-padding;
+  margin-left: $mobile-nice-padding;
+  margin-right: $mobile-nice-padding;
 
-    @include media-breakpoint-up(sm) {
-        margin-left: $desktop-nice-padding;
-        margin-right: $desktop-nice-padding;
-    }
+  @include media-breakpoint-up(sm) {
+    margin-left: $desktop-nice-padding;
+    margin-right: $desktop-nice-padding;
+  }
 }

+ 51 - 45
client/scss/tools/_various.colors.scss

@@ -1,67 +1,73 @@
-@use "sass:math";
-@use "sass:color";
-@use "sass:list";
-@use "sass:meta";
+@use 'sass:math';
+@use 'sass:color';
+@use 'sass:list';
+@use 'sass:meta';
 
 // $color is either a color or an hsl tuple
 @mixin define-color($name, $color) {
-    $h: null;
-    $s: null;
-    $l: null;
+  $h: null;
+  $s: null;
+  $l: null;
 
-    @if meta.type-of($color) == color {
-        $h: math.div(color.hue($color), 1deg); // Cast to unitless
-        $s: color.saturation($color);
-        $l: color.lightness($color);
-    } @else {
-        $h: list.nth($color, 1);
-        $s: list.nth($color, 2);
-        $l: list.nth($color, 3);
-    }
+  @if meta.type-of($color) == color {
+    $h: math.div(color.hue($color), 1deg); // Cast to unitless
+    $s: color.saturation($color);
+    $l: color.lightness($color);
+  } @else {
+    $h: list.nth($color, 1);
+    $s: list.nth($color, 2);
+    $l: list.nth($color, 3);
+  }
 
-    --#{$name}-hue: #{$h};
-    --#{$name}-saturation: #{$s};
-    --#{$name}-lightness: #{$l};
-    --#{$name}: hsl(#{ var(--#{$name}-hue), var(--#{$name}-saturation), var(--#{$name}-lightness) });
+  --#{$name}-hue: #{$h};
+  --#{$name}-saturation: #{$s};
+  --#{$name}-lightness: #{$l};
+  // Prettier causes a linting issue when reformatting this.
+  /* prettier-ignore */
+  --#{$name}: hsl(#{ var(--#{$name}-hue), var(--#{$name}-saturation), var(--#{$name}-lightness) });
 }
 
 @function get-color($name) {
-    @return (var(--#{$name}-hue), var(--#{$name}-saturation), var(--#{$name}-lightness));
+  @return (
+    var(--#{$name}-hue),
+    var(--#{$name}-saturation),
+    var(--#{$name}-lightness)
+  );
 }
 
 @function css-darken($hsl-tuple, $darken-by) {
-    $h: list.nth($hsl-tuple, 1);
-    $s: list.nth($hsl-tuple, 2);
-    $l: list.nth($hsl-tuple, 3);
-    @return ($h, $s, calc(#{$l} - #{$darken-by + 0%}));
+  $h: list.nth($hsl-tuple, 1);
+  $s: list.nth($hsl-tuple, 2);
+  $l: list.nth($hsl-tuple, 3);
+  @return ($h, $s, calc(#{$l} - #{$darken-by + 0%}));
 }
 @function css-lighten($hsl-tuple, $lighten-by) {
-    $h: list.nth($hsl-tuple, 1);
-    $s: list.nth($hsl-tuple, 2);
-    $l: list.nth($hsl-tuple, 3);
-    @return ($h, $s, calc(#{$l} + #{$lighten-by + 0%}));
+  $h: list.nth($hsl-tuple, 1);
+  $s: list.nth($hsl-tuple, 2);
+  $l: list.nth($hsl-tuple, 3);
+  @return ($h, $s, calc(#{$l} + #{$lighten-by + 0%}));
 }
 @function css-saturate($hsl-tuple, $saturate-by) {
-    $h: list.nth($hsl-tuple, 1);
-    $s: list.nth($hsl-tuple, 2);
-    $l: list.nth($hsl-tuple, 3);
-    @return ($h, calc(#{$s} + #{$saturate-by + 0%}), $l);
+  $h: list.nth($hsl-tuple, 1);
+  $s: list.nth($hsl-tuple, 2);
+  $l: list.nth($hsl-tuple, 3);
+  @return ($h, calc(#{$s} + #{$saturate-by + 0%}), $l);
 }
 @function css-desaturate($hsl-tuple, $desaturate-by) {
-    $h: list.nth($hsl-tuple, 1);
-    $s: list.nth($hsl-tuple, 2);
-    $l: list.nth($hsl-tuple, 3);
-    @return ($h, calc(#{$s} - #{$desaturate-by + 0%}), $l);
+  $h: list.nth($hsl-tuple, 1);
+  $s: list.nth($hsl-tuple, 2);
+  $l: list.nth($hsl-tuple, 3);
+  @return ($h, calc(#{$s} - #{$desaturate-by + 0%}), $l);
 }
 @function css-adjust-hue($hsl-tuple, $adjust-by) {
-    $h: list.nth($hsl-tuple, 1);
-    $s: list.nth($hsl-tuple, 2);
-    $l: list.nth($hsl-tuple, 3);
-    @return (calc(#{$h} + #{$adjust-by}), $s, $l);
+  $h: list.nth($hsl-tuple, 1);
+  $s: list.nth($hsl-tuple, 2);
+  $l: list.nth($hsl-tuple, 3);
+  @return (calc(#{$h} + #{$adjust-by}), $s, $l);
 }
 @function css-transparentize($hsl-tuple, $alpha) {
-    $h: list.nth($hsl-tuple, 1);
-    $s: list.nth($hsl-tuple, 2);
-    $l: list.nth($hsl-tuple, 3);
-    @return ($h, $s, $l, $alpha);
+  $h: list.nth($hsl-tuple, 1);
+  $s: list.nth($hsl-tuple, 2);
+  $l: list.nth($hsl-tuple, 3);
+  @return ($h, $s, $l, $alpha);
 }

+ 7 - 8
client/src/api/admin.test.js

@@ -8,10 +8,7 @@ const stubResult = {
       verbose_name: 'Test',
     },
   },
-  items: [
-    { meta: { type: 'test' } },
-    { meta: { type: 'foo' } },
-  ],
+  items: [{ meta: { type: 'test' } }, { meta: { type: 'foo' } }],
 };
 
 client.get = jest.fn(() => Promise.resolve(stubResult));
@@ -20,27 +17,29 @@ describe('admin API', () => {
   describe('getPageChildren', () => {
     it('works', () => {
       getPageChildren(3);
-      expect(client.get).toBeCalledWith(`${ADMIN_API.PAGES}?child_of=3&for_explorer=1&fields=parent`);
+      expect(client.get).toBeCalledWith(
+        `${ADMIN_API.PAGES}?child_of=3&for_explorer=1&fields=parent`,
+      );
     });
 
     it('#fields', () => {
       getPageChildren(3, { fields: ['title', 'latest_revision_created_at'] });
       expect(client.get).toBeCalledWith(
-        `${ADMIN_API.PAGES}?child_of=3&for_explorer=1&fields=parent,title%2Clatest_revision_created_at`
+        `${ADMIN_API.PAGES}?child_of=3&for_explorer=1&fields=parent,title%2Clatest_revision_created_at`,
       );
     });
 
     it('#onlyWithChildren', () => {
       getPageChildren(3, { onlyWithChildren: true });
       expect(client.get).toBeCalledWith(
-        `${ADMIN_API.PAGES}?child_of=3&for_explorer=1&fields=parent&has_children=1`
+        `${ADMIN_API.PAGES}?child_of=3&for_explorer=1&fields=parent&has_children=1`,
       );
     });
 
     it('#offset', () => {
       getPageChildren(3, { offset: 5 });
       expect(client.get).toBeCalledWith(
-        `${ADMIN_API.PAGES}?child_of=3&for_explorer=1&fields=parent&offset=5`
+        `${ADMIN_API.PAGES}?child_of=3&for_explorer=1&fields=parent&offset=5`,
       );
     });
   });

+ 26 - 11
client/src/api/admin.ts

@@ -8,9 +8,9 @@ export interface WagtailPageAPI {
     status: {
       status: string;
       live: boolean;
-       
+
       has_unpublished_changes: boolean;
-    }
+    };
     children: any;
     parent: {
       id: number;
@@ -18,13 +18,12 @@ export interface WagtailPageAPI {
     locale?: string;
     translations?: any;
   };
-   
+
   admin_display_title?: string;
 }
 
 interface WagtailPageListAPI {
   meta: {
-     
     total_count: number;
   };
   items: WagtailPageAPI[];
@@ -42,12 +41,17 @@ interface GetPageChildrenOptions {
   offset?: number;
 }
 
-type GetPageChildren = (id: number, options: GetPageChildrenOptions) => Promise<WagtailPageListAPI>;
+type GetPageChildren = (
+  id: number,
+  options: GetPageChildrenOptions,
+) => Promise<WagtailPageListAPI>;
 export const getPageChildren: GetPageChildren = (id, options = {}) => {
   let url = `${ADMIN_API.PAGES}?child_of=${id}&for_explorer=1`;
 
   if (options.fields) {
-    url += `&fields=parent,${window.encodeURIComponent(options.fields.join(','))}`;
+    url += `&fields=parent,${window.encodeURIComponent(
+      options.fields.join(','),
+    )}`;
   } else {
     url += '&fields=parent';
   }
@@ -70,12 +74,17 @@ interface GetPageTranslationsOptions {
   onlyWithChildren?: boolean;
   offset?: number;
 }
-type GetPageTranslations = (id: number, options: GetPageTranslationsOptions) => Promise<WagtailPageListAPI>;
+type GetPageTranslations = (
+  id: number,
+  options: GetPageTranslationsOptions,
+) => Promise<WagtailPageListAPI>;
 export const getPageTranslations: GetPageTranslations = (id, options = {}) => {
   let url = `${ADMIN_API.PAGES}?translation_of=${id}&limit=20`;
 
   if (options.fields) {
-    url += `&fields=parent,${global.encodeURIComponent(options.fields.join(','))}`;
+    url += `&fields=parent,${global.encodeURIComponent(
+      options.fields.join(','),
+    )}`;
   } else {
     url += '&fields=parent';
   }
@@ -96,14 +105,20 @@ interface GetAllPageTranslationsOptions {
   onlyWithChildren?: boolean;
 }
 
-export const getAllPageTranslations = async (id: number, options: GetAllPageTranslationsOptions) => {
+export const getAllPageTranslations = async (
+  id: number,
+  options: GetAllPageTranslationsOptions,
+) => {
   const items: WagtailPageAPI[] = [];
   let iterLimit = 100;
 
   for (;;) {
-    const page = await getPageTranslations(id, { offset: items.length, ...options });
+    const page = await getPageTranslations(id, {
+      offset: items.length,
+      ...options,
+    });
 
-    page.items.forEach(item => items.push(item));
+    page.items.forEach((item) => items.push(item));
 
     if (items.length >= page.meta.total_count || iterLimit-- <= 0) {
       return items;

+ 14 - 11
client/src/api/client.js

@@ -3,7 +3,7 @@ const Headers = global.Headers;
 
 const REQUEST_TIMEOUT = 15000;
 
-const checkStatus = (response) =>  {
+const checkStatus = (response) => {
   if (response.status >= 200 && response.status < 300) {
     return response;
   }
@@ -13,7 +13,7 @@ const checkStatus = (response) =>  {
   throw error;
 };
 
-const parseJSON = response => response.json();
+const parseJSON = (response) => response.json();
 
 // Response timeout cancelling the promise (not the request).
 // See https://github.com/github/fetch/issues/175#issuecomment-216791333.
@@ -23,13 +23,16 @@ const timeout = (ms, promise) => {
       reject(new Error('Response timeout'));
     }, ms);
 
-    promise.then((res) => {
-      clearTimeout(timeoutId);
-      resolve(res);
-    }, (err) => {
-      clearTimeout(timeoutId);
-      reject(err);
-    });
+    promise.then(
+      (res) => {
+        clearTimeout(timeoutId);
+        resolve(res);
+      },
+      (err) => {
+        clearTimeout(timeoutId);
+        reject(err);
+      },
+    );
   });
 
   return race;
@@ -46,7 +49,7 @@ const request = (method, url) => {
       'Accept': 'application/json',
       'Content-Type': 'application/json',
     }),
-    method: method
+    method: method,
   };
 
   return timeout(REQUEST_TIMEOUT, fetch(url, options))
@@ -54,4 +57,4 @@ const request = (method, url) => {
     .then(parseJSON);
 };
 
-export const get = url => request('GET', url);
+export const get = (url) => request('GET', url);

+ 6 - 2
client/src/components/Button/Button.test.js

@@ -17,7 +17,9 @@ describe('Button', () => {
   });
 
   it('#accessibleLabel', () => {
-    expect(shallow(<Button accessibleLabel="I am here in the shadows" />)).toMatchSnapshot();
+    expect(
+      shallow(<Button accessibleLabel="I am here in the shadows" />),
+    ).toMatchSnapshot();
   });
 
   it('#dialogTrigger', () => {
@@ -25,7 +27,9 @@ describe('Button', () => {
   });
 
   it('#target', () => {
-    expect(shallow(<Button target="_blank" rel="noopener noreferrer" />)).toMatchSnapshot();
+    expect(
+      shallow(<Button target="_blank" rel="noopener noreferrer" />),
+    ).toMatchSnapshot();
   });
 
   it('is clickable', () => {

+ 2 - 6
client/src/components/Button/Button.tsx

@@ -1,5 +1,3 @@
- 
-
 import * as React from 'react';
 
 const handleClick = (
@@ -7,7 +5,7 @@ const handleClick = (
   onClick: ((e: React.MouseEvent) => void) | undefined,
   preventDefault: boolean,
   navigate: (url: string) => Promise<void>,
-  e: React.MouseEvent
+  e: React.MouseEvent,
 ) => {
   if (preventDefault && href === '#') {
     e.preventDefault();
@@ -52,9 +50,7 @@ const Button: React.FunctionComponent<ButtonProps> = ({
 }) => {
   const hasText = React.Children.count(children) > 0;
   const accessibleElt = accessibleLabel ? (
-    <span className="visuallyhidden">
-      {accessibleLabel}
-    </span>
+    <span className="visuallyhidden">{accessibleLabel}</span>
   ) : null;
 
   return (

+ 8 - 3
client/src/components/CommentApp/__fixtures__/state.tsx

@@ -1,6 +1,5 @@
 import type { Comment, CommentReply, CommentsState } from '../state/comments';
 
-
 const remoteReply: CommentReply = {
   localId: 2,
   remoteId: 2,
@@ -41,7 +40,10 @@ const remoteComment: Comment = {
   newReply: '',
   newText: '',
   remoteReplyCount: 1,
-  replies: new Map([[remoteReply.localId, remoteReply], [localReply.localId, localReply]]),
+  replies: new Map([
+    [remoteReply.localId, remoteReply],
+    [localReply.localId, localReply],
+  ]),
 };
 
 const localComment: Comment = {
@@ -68,5 +70,8 @@ export const basicCommentsState: CommentsState = {
   forceFocus: false,
   pinnedComment: 1,
   remoteCommentCount: 1,
-  comments: new Map([[remoteComment.localId, remoteComment], [localComment.localId, localComment]]),
+  comments: new Map([
+    [remoteComment.localId, remoteComment],
+    [localComment.localId, localComment],
+  ]),
 };

+ 12 - 8
client/src/components/CommentApp/actions/comments.ts

@@ -90,7 +90,7 @@ export function addComment(comment: Comment): AddCommentAction {
 
 export function updateComment(
   commentId: number,
-  update: CommentUpdate
+  update: CommentUpdate,
 ): UpdateCommentAction {
   return {
     type: UPDATE_COMMENT,
@@ -113,22 +113,24 @@ export function resolveComment(commentId: number): ResolveCommentAction {
   };
 }
 
-
 export function setFocusedComment(
   commentId: number | null,
-  { updatePinnedComment, forceFocus } = { updatePinnedComment: false, forceFocus: false }
+  { updatePinnedComment, forceFocus } = {
+    updatePinnedComment: false,
+    forceFocus: false,
+  },
 ): SetFocusedCommentAction {
   return {
     type: SET_FOCUSED_COMMENT,
     commentId,
     updatePinnedComment,
-    forceFocus
+    forceFocus,
   };
 }
 
 export function addReply(
   commentId: number,
-  reply: CommentReply
+  reply: CommentReply,
 ): AddReplyAction {
   return {
     type: ADD_REPLY,
@@ -140,7 +142,7 @@ export function addReply(
 export function updateReply(
   commentId: number,
   replyId: number,
-  update: CommentReplyUpdate
+  update: CommentReplyUpdate,
 ): UpdateReplyAction {
   return {
     type: UPDATE_REPLY,
@@ -152,7 +154,7 @@ export function updateReply(
 
 export function deleteReply(
   commentId: number,
-  replyId: number
+  replyId: number,
 ): DeleteReplyAction {
   return {
     type: DELETE_REPLY,
@@ -161,7 +163,9 @@ export function deleteReply(
   };
 }
 
-export function invalidateContentPath(contentPath: string): InvalidateContentPathAction {
+export function invalidateContentPath(
+  contentPath: string,
+): InvalidateContentPathAction {
   return {
     type: INVALIDATE_CONTENT_PATH,
     contentPath,

+ 1 - 1
client/src/components/CommentApp/actions/settings.ts

@@ -10,7 +10,7 @@ export interface UpdateGlobalSettingsAction {
 export type Action = UpdateGlobalSettingsAction;
 
 export function updateGlobalSettings(
-  update: SettingsStateUpdate
+  update: SettingsStateUpdate,
 ): UpdateGlobalSettingsAction {
   return {
     type: UPDATE_GLOBAL_SETTINGS,

+ 4 - 3
client/src/components/CommentApp/components/Comment/index.stories.tsx

@@ -41,7 +41,8 @@ export function commentFromSomeoneElse() {
     author: {
       id: 2,
       name: 'Someone else',
-      avatarUrl: 'https://gravatar.com/avatar/31c3d5cc27d1faa321c2413589e8a53f?s=200&d=robohash&r=x',
+      avatarUrl:
+        'https://gravatar.com/avatar/31c3d5cc27d1faa321c2413589e8a53f?s=200&d=robohash&r=x',
     },
   });
 
@@ -72,8 +73,8 @@ export function commentFromSomeoneWithAReallyLongName() {
     author: {
       id: 1,
       name: 'This person has a really long name and it should wrap to the next line',
-      avatarUrl: 'https://gravatar.com/avatar/31c3d5cc27d1faa321c2413589e8a53f?s=200&d=robohash&r=x',
-
+      avatarUrl:
+        'https://gravatar.com/avatar/31c3d5cc27d1faa321c2413589e8a53f?s=200&d=robohash&r=x',
     },
   });
 

+ 108 - 77
client/src/components/CommentApp/components/Comment/index.tsx

@@ -1,5 +1,3 @@
-
-
 import React from 'react';
 import ReactDOM from 'react-dom';
 import FocusTrap from 'focus-trap-react';
@@ -12,20 +10,20 @@ import {
   deleteComment,
   resolveComment,
   setFocusedComment,
-  addReply
+  addReply,
 } from '../../actions/comments';
 import { LayoutController } from '../../utils/layout';
 import { getNextReplyId } from '../../utils/sequences';
 import CommentReplyComponent from '../CommentReply';
 import type { TranslatableStrings } from '../../main';
-import { CommentHeader }  from '../CommentHeader';
+import { CommentHeader } from '../CommentHeader';
 import TextArea from '../TextArea';
 
 async function saveComment(comment: Comment, store: Store) {
   store.dispatch(
     updateComment(comment.localId, {
       mode: 'saving',
-    })
+    }),
   );
 
   try {
@@ -36,7 +34,7 @@ async function saveComment(comment: Comment, store: Store) {
         remoteId: comment.remoteId,
         author: comment.author,
         date: comment.date,
-      })
+      }),
     );
   } catch (err) {
     /* eslint-disable-next-line no-console */
@@ -44,7 +42,7 @@ async function saveComment(comment: Comment, store: Store) {
     store.dispatch(
       updateComment(comment.localId, {
         mode: 'save_error',
-      })
+      }),
     );
   }
 }
@@ -53,7 +51,7 @@ async function doDeleteComment(comment: Comment, store: Store) {
   store.dispatch(
     updateComment(comment.localId, {
       mode: 'deleting',
-    })
+    }),
   );
 
   try {
@@ -64,15 +62,13 @@ async function doDeleteComment(comment: Comment, store: Store) {
     store.dispatch(
       updateComment(comment.localId, {
         mode: 'delete_error',
-      })
+      }),
     );
   }
 }
 
 function doResolveComment(comment: Comment, store: Store) {
-  store.dispatch(
-    resolveComment(comment.localId)
-  );
+  store.dispatch(resolveComment(comment.localId));
 }
 
 export interface CommentProps {
@@ -99,7 +95,7 @@ export default class CommentComponent extends React.Component<CommentProps> {
       store.dispatch(
         updateComment(comment.localId, {
           newReply: value,
-        })
+        }),
       );
     };
 
@@ -116,7 +112,7 @@ export default class CommentComponent extends React.Component<CommentProps> {
       store.dispatch(
         updateComment(comment.localId, {
           newReply: '',
-        })
+        }),
       );
     };
 
@@ -126,7 +122,7 @@ export default class CommentComponent extends React.Component<CommentProps> {
       store.dispatch(
         updateComment(comment.localId, {
           newReply: '',
-        })
+        }),
       );
 
       store.dispatch(setFocusedComment(null));
@@ -152,7 +148,7 @@ export default class CommentComponent extends React.Component<CommentProps> {
             reply={reply}
             strings={strings}
             isFocused={isFocused}
-          />
+          />,
         );
       }
     }
@@ -210,7 +206,7 @@ export default class CommentComponent extends React.Component<CommentProps> {
       store.dispatch(
         updateComment(comment.localId, {
           newText: value,
-        })
+        }),
       );
     };
 
@@ -244,7 +240,7 @@ export default class CommentComponent extends React.Component<CommentProps> {
             onChange={onChangeText}
             placeholder="Enter your comments..."
             additionalAttributes={{
-              'aria-describedby': descriptionId
+              'aria-describedby': descriptionId,
             }}
           />
           <div className="comment__actions">
@@ -275,7 +271,7 @@ export default class CommentComponent extends React.Component<CommentProps> {
       store.dispatch(
         updateComment(comment.localId, {
           newText: value,
-        })
+        }),
       );
     };
 
@@ -292,7 +288,7 @@ export default class CommentComponent extends React.Component<CommentProps> {
         updateComment(comment.localId, {
           mode: 'default',
           newText: comment.text,
-        })
+        }),
       );
     };
 
@@ -313,7 +309,7 @@ export default class CommentComponent extends React.Component<CommentProps> {
             className="comment__input"
             value={comment.newText}
             additionalAttributes={{
-              'aria-describedby': descriptionId
+              'aria-describedby': descriptionId,
             }}
             onChange={onChangeText}
           />
@@ -368,7 +364,12 @@ export default class CommentComponent extends React.Component<CommentProps> {
 
     return (
       <>
-        <CommentHeader commentReply={comment} store={store} strings={strings} focused={isFocused} />
+        <CommentHeader
+          commentReply={comment}
+          store={store}
+          strings={strings}
+          focused={isFocused}
+        />
         <p className="comment__text">{comment.text}</p>
         {this.renderReplies({ hideNewReply: true })}
         <div className="comment__error">
@@ -400,13 +401,18 @@ export default class CommentComponent extends React.Component<CommentProps> {
       store.dispatch(
         updateComment(comment.localId, {
           mode: 'default',
-        })
+        }),
       );
     };
 
     return (
       <>
-        <CommentHeader commentReply={comment} store={store} strings={strings} focused={isFocused} />
+        <CommentHeader
+          commentReply={comment}
+          store={store}
+          strings={strings}
+          focused={isFocused}
+        />
         <p className="comment__text">{comment.text}</p>
         <div className="comment__confirm-delete">
           {strings.CONFIRM_DELETE_COMMENT}
@@ -435,7 +441,12 @@ export default class CommentComponent extends React.Component<CommentProps> {
 
     return (
       <>
-        <CommentHeader commentReply={comment} store={store} strings={strings} focused={isFocused} />
+        <CommentHeader
+          commentReply={comment}
+          store={store}
+          strings={strings}
+          focused={isFocused}
+        />
         <p className="comment__text">{comment.text}</p>
         <div className="comment__progress">{strings.DELETING}</div>
         {this.renderReplies({ hideNewReply: true })}
@@ -458,13 +469,18 @@ export default class CommentComponent extends React.Component<CommentProps> {
       store.dispatch(
         updateComment(comment.localId, {
           mode: 'default',
-        })
+        }),
       );
     };
 
     return (
       <>
-        <CommentHeader commentReply={comment} store={store} strings={strings} focused={isFocused} />
+        <CommentHeader
+          commentReply={comment}
+          store={store}
+          strings={strings}
+          focused={isFocused}
+        />
         <p className="comment__text">{comment.text}</p>
         {this.renderReplies({ hideNewReply: true })}
         <div className="comment__error">
@@ -494,13 +510,16 @@ export default class CommentComponent extends React.Component<CommentProps> {
     // Show edit/delete buttons if this comment was authored by the current user
     let onEdit;
     let onDelete;
-    if (comment.author === null || this.props.user && this.props.user.id === comment.author.id) {
+    if (
+      comment.author === null ||
+      (this.props.user && this.props.user.id === comment.author.id)
+    ) {
       onEdit = () => {
         store.dispatch(
           updateComment(comment.localId, {
             mode: 'editing',
             newText: comment.text,
-          })
+          }),
         );
       };
 
@@ -508,7 +527,7 @@ export default class CommentComponent extends React.Component<CommentProps> {
         store.dispatch(
           updateComment(comment.localId, {
             mode: 'delete_confirm',
-          })
+          }),
         );
       };
     }
@@ -534,14 +553,14 @@ export default class CommentComponent extends React.Component<CommentProps> {
           focused={isFocused}
         />
         <p className="comment__text">{comment.text}</p>
-        {notice &&
+        {notice && (
           <div className="comment__notice-placeholder">
             <div className="comment__notice" role="status">
               <Icon name="info-circle" />
               {notice}
             </div>
           </div>
-        }
+        )}
         {this.renderReplies()}
       </>
     );
@@ -551,78 +570,90 @@ export default class CommentComponent extends React.Component<CommentProps> {
     let inner: React.ReactFragment;
 
     switch (this.props.comment.mode) {
-    case 'creating':
-      inner = this.renderCreating();
-      break;
+      case 'creating':
+        inner = this.renderCreating();
+        break;
 
-    case 'editing':
-      inner = this.renderEditing();
-      break;
+      case 'editing':
+        inner = this.renderEditing();
+        break;
 
-    case 'saving':
-      inner = this.renderSaving();
-      break;
+      case 'saving':
+        inner = this.renderSaving();
+        break;
 
-    case 'save_error':
-      inner = this.renderSaveError();
-      break;
+      case 'save_error':
+        inner = this.renderSaveError();
+        break;
 
-    case 'delete_confirm':
-      inner = this.renderDeleteConfirm();
-      break;
+      case 'delete_confirm':
+        inner = this.renderDeleteConfirm();
+        break;
 
-    case 'deleting':
-      inner = this.renderDeleting();
-      break;
+      case 'deleting':
+        inner = this.renderDeleting();
+        break;
 
-    case 'delete_error':
-      inner = this.renderDeleteError();
-      break;
+      case 'delete_error':
+        inner = this.renderDeleteError();
+        break;
 
-    default:
-      inner = this.renderDefault();
-      break;
+      default:
+        inner = this.renderDefault();
+        break;
     }
 
     const onClick = () => {
       this.props.store.dispatch(
-        setFocusedComment(this.props.comment.localId,
-          { updatePinnedComment: false, forceFocus: this.props.isFocused && this.props.forceFocus }
-        )
+        setFocusedComment(this.props.comment.localId, {
+          updatePinnedComment: false,
+          forceFocus: this.props.isFocused && this.props.forceFocus,
+        }),
       );
     };
 
     const onDoubleClick = () => {
       this.props.store.dispatch(
-        setFocusedComment(this.props.comment.localId, { updatePinnedComment: true, forceFocus: true })
+        setFocusedComment(this.props.comment.localId, {
+          updatePinnedComment: true,
+          forceFocus: true,
+        }),
       );
     };
 
     const top = this.props.layout.getCommentPosition(
-      this.props.comment.localId
+      this.props.comment.localId,
     );
 
     return (
       <FocusTrap
-        focusTrapOptions={{
-          preventScroll: true,
-          clickOutsideDeactivates: true,
-          onDeactivate: () => {
-            this.props.store.dispatch(
-              setFocusedComment(null, { updatePinnedComment: true, forceFocus: false })
-            );
-          },
-          initialFocus: '[data-focus-target="true"]',
-        } as any} // For some reason, the types for FocusTrap props don't yet include preventScroll.
+        focusTrapOptions={
+          {
+            preventScroll: true,
+            clickOutsideDeactivates: true,
+            onDeactivate: () => {
+              this.props.store.dispatch(
+                setFocusedComment(null, {
+                  updatePinnedComment: true,
+                  forceFocus: false,
+                }),
+              );
+            },
+            initialFocus: '[data-focus-target="true"]',
+          } as any
+        } // For some reason, the types for FocusTrap props don't yet include preventScroll.
         active={this.props.isFocused && this.props.forceFocus}
       >
         <li
           tabIndex={-1}
-          data-focus-target={this.props.isFocused && !['creating', 'editing'].includes(this.props.comment.mode)}
-          key={this.props.comment.localId}
-          className={
-            `comment comment--mode-${this.props.comment.mode} ${this.props.isFocused ? 'comment--focused' : ''}`
+          data-focus-target={
+            this.props.isFocused &&
+            !['creating', 'editing'].includes(this.props.comment.mode)
           }
+          key={this.props.comment.localId}
+          className={`comment comment--mode-${this.props.comment.mode} ${
+            this.props.isFocused ? 'comment--focused' : ''
+          }`}
           style={{
             position: 'absolute',
             top: `${top}px`,
@@ -648,7 +679,7 @@ export default class CommentComponent extends React.Component<CommentProps> {
       if (this.props.isVisible) {
         this.props.layout.setCommentHeight(
           this.props.comment.localId,
-          element.offsetHeight
+          element.offsetHeight,
         );
       }
     }
@@ -666,7 +697,7 @@ export default class CommentComponent extends React.Component<CommentProps> {
     if (this.props.isVisible && element instanceof HTMLElement) {
       this.props.layout.setCommentHeight(
         this.props.comment.localId,
-        element.offsetHeight
+        element.offsetHeight,
       );
     }
   }

+ 144 - 144
client/src/components/CommentApp/components/Comment/style.scss

@@ -1,151 +1,151 @@
 .comment {
-    @include box;
-
+  @include box;
+
+  width: calc(100vw - 40px);
+  max-width: calc(100vw - 19%);
+  display: block;
+  transition: top 0.5s ease 0s, right 0.5s ease 0s, height 0.5s ease 0s;
+  pointer-events: auto;
+  box-sizing: border-box;
+  padding-bottom: 0;
+  right: -2000px;
+
+  @include media-breakpoint-up(sm) {
     width: calc(100vw - 40px);
-    max-width: calc(100vw - 19%);
-    display: block;
-    transition: top 0.5s ease 0s, right 0.5s ease 0s, height 0.5s ease 0s;
-    pointer-events: auto;
-    box-sizing: border-box;
-    padding-bottom: 0;
-    right: -2000px;
-
-    @include media-breakpoint-up(sm) {
-        width: calc(100vw - 40px);
-        max-width: 400px;
-        left: initial;
-    }
-
-    @include media-breakpoint-up(md) {
-        max-width: 200px;
-        right: 0;
-    }
-
-    @include media-breakpoint-up(lg) {
-        max-width: 275px;
-    }
-
-    &--focused {
-        right: 35px;
-
-        @include media-breakpoint-up(md) {
-            right: 50px;
-        }
-    }
+    max-width: 400px;
+    left: initial;
+  }
 
-    &__text {
-        color: $color-box-text;
-        font-size: 13px;
-        line-height: 19px;
-        margin-bottom: 0;
-        padding-top: 10px;
-        padding-bottom: 10px;
-
-        &--mode-deleting {
-            color: $color-grey-1;
-        }
-    }
+  @include media-breakpoint-up(md) {
+    max-width: 200px;
+    right: 0;
+  }
 
-    form {
-        border-top: 1px solid $color-comment-separator;
-    }
+  @include media-breakpoint-up(lg) {
+    max-width: 275px;
+  }
 
-    &--mode-creating form {
-        border-top: 0;
-        margin-top: 10px;
-    }
+  &--focused {
+    right: 35px;
 
-    &--mode-editing form {
-        margin-top: 10px;
-    }
-
-    &--mode-deleting &__text {
-        color: $color-grey-3;
-    }
-
-    &__replies {
-        list-style-type: none;
-        padding: 0;
-        margin: 0;
-    }
-
-    &__button {
-        @include button;
-    }
-
-    &__actions,
-    &__reply-actions {
-        padding-bottom: 10px;
-    }
-
-    &__actions &__button,
-    &__reply-actions &__button {
-        margin-right: 10px;
-        margin-top: 10px;
-    }
-
-    &__confirm-delete &__button {
-        margin-left: 10px;
-        margin-bottom: 10px;
-    }
-
-    &__confirm-delete,
-    &__error {
-        color: $color-box-text;
-        font-weight: bold;
-        font-size: 13px;
-        margin-top: 10px;
-
-        button {
-            float: right;
-        }
-
-        &::after {
-            display: block;
-            content: ' ';
-            clear: both;
-        }
-    }
-
-    &__error {
-        color: $color-white;
-        background-color: $color-red-dark;
-        border-radius: 3px;
-        padding: 5px;
-        padding-left: 10px;
-        height: 26px;
-        line-height: 26px;
-        vertical-align: middle;
-
-        button {
-            height: 26px;
-            float: right;
-            margin-left: 5px;
-            color: $color-white;
-            background-color: $color-red-very-dark;
-            border-color: $color-red-very-dark;
-            padding: 2px;
-            padding-left: 10px;
-            padding-right: 10px;
-            font-size: 0.65em;
-            font-weight: bold;
-        }
-
-        &::after {
-            display: block;
-            content: '';
-            clear: both;
-        }
-    }
-
-    &__progress {
-        margin-top: 20px;
-        font-weight: bold;
-        font-size: 13px;
-    }
-
-    &__reply-input {
-        /* stylelint-disable-next-line declaration-no-important */
-        margin-top: 20px !important;
-    }
+    @include media-breakpoint-up(md) {
+      right: 50px;
+    }
+  }
+
+  &__text {
+    color: $color-box-text;
+    font-size: 13px;
+    line-height: 19px;
+    margin-bottom: 0;
+    padding-top: 10px;
+    padding-bottom: 10px;
+
+    &--mode-deleting {
+      color: $color-grey-1;
+    }
+  }
+
+  form {
+    border-top: 1px solid $color-comment-separator;
+  }
+
+  &--mode-creating form {
+    border-top: 0;
+    margin-top: 10px;
+  }
+
+  &--mode-editing form {
+    margin-top: 10px;
+  }
+
+  &--mode-deleting &__text {
+    color: $color-grey-3;
+  }
+
+  &__replies {
+    list-style-type: none;
+    padding: 0;
+    margin: 0;
+  }
+
+  &__button {
+    @include button;
+  }
+
+  &__actions,
+  &__reply-actions {
+    padding-bottom: 10px;
+  }
+
+  &__actions &__button,
+  &__reply-actions &__button {
+    margin-right: 10px;
+    margin-top: 10px;
+  }
+
+  &__confirm-delete &__button {
+    margin-left: 10px;
+    margin-bottom: 10px;
+  }
+
+  &__confirm-delete,
+  &__error {
+    color: $color-box-text;
+    font-weight: bold;
+    font-size: 13px;
+    margin-top: 10px;
+
+    button {
+      float: right;
+    }
+
+    &::after {
+      display: block;
+      content: ' ';
+      clear: both;
+    }
+  }
+
+  &__error {
+    color: $color-white;
+    background-color: $color-red-dark;
+    border-radius: 3px;
+    padding: 5px;
+    padding-left: 10px;
+    height: 26px;
+    line-height: 26px;
+    vertical-align: middle;
+
+    button {
+      height: 26px;
+      float: right;
+      margin-left: 5px;
+      color: $color-white;
+      background-color: $color-red-very-dark;
+      border-color: $color-red-very-dark;
+      padding: 2px;
+      padding-left: 10px;
+      padding-right: 10px;
+      font-size: 0.65em;
+      font-weight: bold;
+    }
+
+    &::after {
+      display: block;
+      content: '';
+      clear: both;
+    }
+  }
+
+  &__progress {
+    margin-top: 20px;
+    font-weight: bold;
+    font-size: 13px;
+  }
+
+  &__reply-input {
+    /* stylelint-disable-next-line declaration-no-important */
+    margin-top: 20px !important;
+  }
 }

+ 74 - 37
client/src/components/CommentApp/components/CommentHeader/index.tsx

@@ -1,5 +1,3 @@
- 
-
 import dateFormat from 'dateformat';
 import React, { FunctionComponent, useState, useEffect, useRef } from 'react';
 import Icon from '../../../Icon/Icon';
@@ -11,41 +9,39 @@ import { Author } from '../../state/comments';
 
 // Details/Summary components that just become <details>/<summary> tags
 // except for IE11 where they become <div> tags to allow us to style them
-const Details: React.FunctionComponent<React.ComponentPropsWithoutRef<'details'>> = (
-  ({ children, open, ...extraProps }) => {
-    if (IS_IE11) {
-      return (
-        <div className={'details-fallback' + (open ? ' details-fallback--open' : '')} {...extraProps}>
-          {children}
-        </div>
-      );
-    }
-
+const Details: React.FunctionComponent<
+  React.ComponentPropsWithoutRef<'details'>
+> = ({ children, open, ...extraProps }) => {
+  if (IS_IE11) {
     return (
-      <details open={open} {...extraProps}>
+      <div
+        className={'details-fallback' + (open ? ' details-fallback--open' : '')}
+        {...extraProps}
+      >
         {children}
-      </details>
+      </div>
     );
   }
-);
 
-const Summary: React.FunctionComponent<React.ComponentPropsWithoutRef<'summary'>> = ({ children, ...extraProps }) => {
+  return (
+    <details open={open} {...extraProps}>
+      {children}
+    </details>
+  );
+};
+
+const Summary: React.FunctionComponent<
+  React.ComponentPropsWithoutRef<'summary'>
+> = ({ children, ...extraProps }) => {
   if (IS_IE11) {
     return (
-      <button
-        className="details-fallback__summary"
-        {...extraProps}
-      >
+      <button className="details-fallback__summary" {...extraProps}>
         {children}
       </button>
     );
   }
 
-  return (
-    <summary {...extraProps}>
-      {children}
-    </summary>
-  );
+  return <summary {...extraProps}>{children}</summary>;
 };
 
 interface CommentReply {
@@ -65,7 +61,14 @@ interface CommentHeaderProps {
 }
 
 export const CommentHeader: FunctionComponent<CommentHeaderProps> = ({
-  commentReply, store, strings, onResolve, onEdit, onDelete, descriptionId, focused
+  commentReply,
+  store,
+  strings,
+  onResolve,
+  onEdit,
+  onDelete,
+  descriptionId,
+  focused,
 }) => {
   const { author, date } = commentReply;
 
@@ -115,7 +118,11 @@ export const CommentHeader: FunctionComponent<CommentHeaderProps> = ({
   }, [menuOpen]);
 
   const handleClickOutside = (e: MouseEvent) => {
-    if (menuContainerRef.current && e.target instanceof Node && !menuContainerRef.current.contains(e.target)) {
+    if (
+      menuContainerRef.current &&
+      e.target instanceof Node &&
+      !menuContainerRef.current.contains(e.target)
+    ) {
       setMenuOpen(false);
     }
   };
@@ -130,8 +137,11 @@ export const CommentHeader: FunctionComponent<CommentHeaderProps> = ({
   return (
     <div className="comment-header">
       <div className="comment-header__actions">
-        {(onEdit || onDelete || onResolve) &&
-          <div className="comment-header__action comment-header__action--more" ref={menuContainerRef}>
+        {(onEdit || onDelete || onResolve) && (
+          <div
+            className="comment-header__action comment-header__action--more"
+            ref={menuContainerRef}
+          >
             <Details open={menuOpen} onClick={toggleMenu}>
               <Summary
                 aria-label={strings.MORE_ACTIONS}
@@ -143,20 +153,47 @@ export const CommentHeader: FunctionComponent<CommentHeaderProps> = ({
                 <Icon name="ellipsis-v" />
               </Summary>
 
-              <div className="comment-header__more-actions" role="menu" ref={menuRef}>
-                {onEdit && <button type="button" role="menuitem" onClick={onClickEdit}>{strings.EDIT}</button>}
-                {onDelete && <button type="button" role="menuitem" onClick={onClickDelete}>{strings.DELETE}</button>}
-                {onResolve && <button type="button" role="menuitem" onClick={onClickResolve}>{strings.RESOLVE}</button>}
+              <div
+                className="comment-header__more-actions"
+                role="menu"
+                ref={menuRef}
+              >
+                {onEdit && (
+                  <button type="button" role="menuitem" onClick={onClickEdit}>
+                    {strings.EDIT}
+                  </button>
+                )}
+                {onDelete && (
+                  <button type="button" role="menuitem" onClick={onClickDelete}>
+                    {strings.DELETE}
+                  </button>
+                )}
+                {onResolve && (
+                  <button
+                    type="button"
+                    role="menuitem"
+                    onClick={onClickResolve}
+                  >
+                    {strings.RESOLVE}
+                  </button>
+                )}
               </div>
             </Details>
           </div>
-        }
+        )}
       </div>
-      {author && author.avatarUrl &&
-        <img className="comment-header__avatar" src={author.avatarUrl} role="presentation" />}
+      {author && author.avatarUrl && (
+        <img
+          className="comment-header__avatar"
+          src={author.avatarUrl}
+          role="presentation"
+        />
+      )}
       <span id={descriptionId}>
         <p className="comment-header__author">{author ? author.name : ''}</p>
-        <p className="comment-header__date">{dateFormat(date, 'd mmm yyyy HH:MM')}</p>
+        <p className="comment-header__date">
+          {dateFormat(date, 'd mmm yyyy HH:MM')}
+        </p>
       </span>
     </div>
   );

+ 123 - 116
client/src/components/CommentApp/components/CommentHeader/style.scss

@@ -1,144 +1,151 @@
 .comment-header {
-    position: relative;
-
-    &__avatar {
-        position: absolute;
-        width: 30px;
-        height: 30px;
-        border-radius: 15px;
+  position: relative;
+
+  &__avatar {
+    position: absolute;
+    width: 30px;
+    height: 30px;
+    border-radius: 15px;
+  }
+
+  &__author,
+  &__date {
+    max-width: calc(
+      100% - 110px
+    ); // Leave room for actions to the right and avatar to the left
+    margin: 0;
+    margin-left: 45px;
+    font-size: 11px;
+    line-height: 15px;
+  }
+
+  &__date {
+    color: $color-grey-25;
+  }
+
+  &__actions {
+    position: absolute;
+    right: 0;
+  }
+
+  &__action {
+    float: left;
+    margin-left: 5px;
+    border-radius: 5px;
+    width: 30px;
+    height: 30px;
+
+    &:hover {
+      background-color: $color-grey-7;
     }
 
-    &__author,
-    &__date {
-        max-width: calc(100% - 110px);  // Leave room for actions to the right and avatar to the left
-        margin: 0;
-        margin-left: 45px;
-        font-size: 11px;
-        line-height: 15px;
+    > button,
+    > details > summary,
+    .details-fallback > .details-fallback__summary {
+      // IE11 uses divs instead with these classes
+      // Hides triangle on Firefox
+      list-style-type: none;
+      // Hides triangle on Chrome
+      &::-webkit-details-marker {
+        display: none;
+      }
+      width: 30px;
+      height: 30px;
+      position: relative;
+      background-color: unset;
+      border: unset;
+      -moz-outline-radius: 10px;
+      padding: 0;
+      box-sizing: border-box;
+
+      svg {
+        position: absolute;
+        top: 7.5px;
+        left: 7.5px;
+        width: 15px;
+        height: 15px;
+      }
+
+      &:hover {
+        cursor: pointer;
+      }
     }
 
-    &__date {
-        color: $color-grey-25;
-    }
+    > details,
+    > .details-fallback {
+      // IE11 uses divs instead with these classes
+      position: relative;
 
-    &__actions {
+      > div {
         position: absolute;
         right: 0;
+        top: 35px;
+      }
     }
 
-    &__action {
-        float: left;
-        margin-left: 5px;
-        border-radius: 5px;
-        width: 30px;
-        height: 30px;
+    &--more {
+      > button,
+      > details > summary,
+      > .details-fallback > .details-fallback__summary {
+        // IE11 uses divs instead with these classes
+        color: #767676;
 
+        // stylelint-disable-next-line max-nesting-depth
         &:hover {
-            background-color: $color-grey-7;
-        }
-
-        > button,
-        > details > summary,
-        .details-fallback > .details-fallback__summary {  // IE11 uses divs instead with these classes
-            // Hides triangle on Firefox
-            list-style-type: none;
-            // Hides triangle on Chrome
-            &::-webkit-details-marker { display: none; }
-            width: 30px;
-            height: 30px;
-            position: relative;
-            background-color: unset;
-            border: unset;
-            -moz-outline-radius: 10px;
-            padding: 0;
-            box-sizing: border-box;
-
-            svg {
-                position: absolute;
-                top: 7.5px;
-                left: 7.5px;
-                width: 15px;
-                height: 15px;
-            }
-
-            &:hover {
-                cursor: pointer;
-            }
-        }
-
-        > details,
-        > .details-fallback {  // IE11 uses divs instead with these classes
-            position: relative;
-
-            > div {
-                position: absolute;
-                right: 0;
-                top: 35px;
-            }
-        }
-
-        &--more {
-            > button,
-            > details > summary,
-            > .details-fallback > .details-fallback__summary {  // IE11 uses divs instead with these classes
-                color: #767676;
-
-                // stylelint-disable-next-line max-nesting-depth
-                &:hover {
-                    color: $color-grey-25;
-                }
-            }
+          color: $color-grey-25;
         }
+      }
+    }
+  }
+
+  &__more-actions {
+    background-color: #333;
+    color: $color-grey-5;
+    text-transform: none;
+    position: absolute;
+    z-index: 1000;
+    list-style: none;
+    text-align: left;
+    border-radius: 3px;
+
+    &:before {
+      content: '';
+      border: 6px solid transparent;
+      border-bottom-color: #333;
+      display: block;
+      position: absolute;
+      bottom: 100%;
+      right: 9px;
     }
 
-    &__more-actions {
-        background-color: #333;
-        color: $color-grey-5;
-        text-transform: none;
-        position: absolute;
-        z-index: 1000;
-        list-style: none;
-        text-align: left;
-        border-radius: 3px;
-
-        &:before {
-            content: '';
-            border: 6px solid transparent;
-            border-bottom-color: #333;
-            display: block;
-            position: absolute;
-            bottom: 100%;
-            right: 9px;
-        }
-
-        button {
-            display: block;
-            background: none;
-            border: 0;
-            color: #fff;
-            padding: 5px 10px;
-            font-size: 13px;
-            width: 100px;
-            text-align: left;
-
-            &:hover {
-                color: #aaa;
-                cursor: pointer;
-            }
-        }
+    button {
+      display: block;
+      background: none;
+      border: 0;
+      color: #fff;
+      padding: 5px 10px;
+      font-size: 13px;
+      width: 100px;
+      text-align: left;
+
+      &:hover {
+        color: #aaa;
+        cursor: pointer;
+      }
     }
+  }
 }
 
 .comment--mode-deleting .comment-header,
 .comment-reply--mode-deleting .comment-header {
-    opacity: 0.5;
+  opacity: 0.5;
 }
 
 // IE11 only uses these classes
 .details-fallback .comment-header__more-actions {
-    display: none;
+  display: none;
 }
 
 .details-fallback--open .comment-header__more-actions {
-    display: block;
+  display: block;
 }

+ 2 - 1
client/src/components/CommentApp/components/CommentReply/index.stories.tsx

@@ -41,7 +41,8 @@ export function replyFromSomeoneElse() {
     author: {
       id: 2,
       name: 'Someone else',
-      avatarUrl: 'https://gravatar.com/avatar/31c3d5cc27d1faa321c2413589e8a53f?s=200&d=robohash&r=x',
+      avatarUrl:
+        'https://gravatar.com/avatar/31c3d5cc27d1faa321c2413589e8a53f?s=200&d=robohash&r=x',
     },
   });
 

+ 71 - 45
client/src/components/CommentApp/components/CommentReply/index.tsx

@@ -1,24 +1,22 @@
- 
-
 import React from 'react';
 
 import type { Store } from '../../state';
 import type { Comment, CommentReply, Author } from '../../state/comments';
 import { updateReply, deleteReply } from '../../actions/comments';
 import type { TranslatableStrings } from '../../main';
-import { CommentHeader }  from '../CommentHeader';
+import { CommentHeader } from '../CommentHeader';
 import TextArea from '../TextArea';
 import Icon from '../../../Icon/Icon';
 
 export async function saveCommentReply(
   comment: Comment,
   reply: CommentReply,
-  store: Store
+  store: Store,
 ) {
   store.dispatch(
     updateReply(comment.localId, reply.localId, {
       mode: 'saving',
-    })
+    }),
   );
 
   try {
@@ -27,7 +25,7 @@ export async function saveCommentReply(
         mode: 'default',
         text: reply.newText,
         author: reply.author,
-      })
+      }),
     );
   } catch (err) {
     /* eslint-disable-next-line no-console */
@@ -35,7 +33,7 @@ export async function saveCommentReply(
     store.dispatch(
       updateReply(comment.localId, reply.localId, {
         mode: 'save_error',
-      })
+      }),
     );
   }
 }
@@ -43,12 +41,12 @@ export async function saveCommentReply(
 async function deleteCommentReply(
   comment: Comment,
   reply: CommentReply,
-  store: Store
+  store: Store,
 ) {
   store.dispatch(
     updateReply(comment.localId, reply.localId, {
       mode: 'deleting',
-    })
+    }),
   );
 
   try {
@@ -57,7 +55,7 @@ async function deleteCommentReply(
     store.dispatch(
       updateReply(comment.localId, reply.localId, {
         mode: 'delete_error',
-      })
+      }),
     );
   }
 }
@@ -79,7 +77,7 @@ export default class CommentReplyComponent extends React.Component<CommentReplyP
       store.dispatch(
         updateReply(comment.localId, reply.localId, {
           newText: value,
-        })
+        }),
       );
     };
 
@@ -95,7 +93,7 @@ export default class CommentReplyComponent extends React.Component<CommentReplyP
         updateReply(comment.localId, reply.localId, {
           mode: 'default',
           newText: reply.text,
-        })
+        }),
       );
     };
 
@@ -139,7 +137,12 @@ export default class CommentReplyComponent extends React.Component<CommentReplyP
 
     return (
       <>
-        <CommentHeader commentReply={reply} store={store} strings={strings} focused={isFocused} />
+        <CommentHeader
+          commentReply={reply}
+          store={store}
+          strings={strings}
+          focused={isFocused}
+        />
         <p className="comment-reply__text">{reply.text}</p>
         <div className="comment-reply__progress">{strings.SAVING}</div>
       </>
@@ -157,7 +160,12 @@ export default class CommentReplyComponent extends React.Component<CommentReplyP
 
     return (
       <>
-        <CommentHeader commentReply={reply} store={store} strings={strings} focused={isFocused} />
+        <CommentHeader
+          commentReply={reply}
+          store={store}
+          strings={strings}
+          focused={isFocused}
+        />
         <p className="comment-reply__text">{reply.text}</p>
         <div className="comment-reply__error">
           {strings.SAVE_ERROR}
@@ -188,13 +196,18 @@ export default class CommentReplyComponent extends React.Component<CommentReplyP
       store.dispatch(
         updateReply(comment.localId, reply.localId, {
           mode: 'default',
-        })
+        }),
       );
     };
 
     return (
       <>
-        <CommentHeader commentReply={reply} store={store} strings={strings} focused={isFocused} />
+        <CommentHeader
+          commentReply={reply}
+          store={store}
+          strings={strings}
+          focused={isFocused}
+        />
         <p className="comment-reply__text">{reply.text}</p>
         <div className="comment-reply__confirm-delete">
           {strings.CONFIRM_DELETE_COMMENT}
@@ -222,7 +235,12 @@ export default class CommentReplyComponent extends React.Component<CommentReplyP
 
     return (
       <>
-        <CommentHeader commentReply={reply} store={store} strings={strings} focused={isFocused} />
+        <CommentHeader
+          commentReply={reply}
+          store={store}
+          strings={strings}
+          focused={isFocused}
+        />
         <p className="comment-reply__text">{reply.text}</p>
         <div className="comment-reply__progress">{strings.DELETING}</div>
       </>
@@ -244,13 +262,18 @@ export default class CommentReplyComponent extends React.Component<CommentReplyP
       store.dispatch(
         updateReply(comment.localId, reply.localId, {
           mode: 'default',
-        })
+        }),
       );
     };
 
     return (
       <>
-        <CommentHeader commentReply={reply} store={store} strings={strings} focused={isFocused} />
+        <CommentHeader
+          commentReply={reply}
+          store={store}
+          strings={strings}
+          focused={isFocused}
+        />
         <p className="comment-reply__text">{reply.text}</p>
         <div className="comment-reply__error">
           {strings.DELETE_ERROR}
@@ -279,13 +302,16 @@ export default class CommentReplyComponent extends React.Component<CommentReplyP
     // Show edit/delete buttons if this reply was authored by the current user
     let onEdit;
     let onDelete;
-    if (reply.author === null || this.props.user && this.props.user.id === reply.author.id) {
+    if (
+      reply.author === null ||
+      (this.props.user && this.props.user.id === reply.author.id)
+    ) {
       onEdit = () => {
         store.dispatch(
           updateReply(comment.localId, reply.localId, {
             mode: 'editing',
             newText: reply.text,
-          })
+          }),
         );
       };
 
@@ -293,7 +319,7 @@ export default class CommentReplyComponent extends React.Component<CommentReplyP
         store.dispatch(
           updateReply(comment.localId, reply.localId, {
             mode: 'delete_confirm',
-          })
+          }),
         );
       };
     }
@@ -315,14 +341,14 @@ export default class CommentReplyComponent extends React.Component<CommentReplyP
           focused={isFocused}
         />
         <p className="comment-reply__text">{reply.text}</p>
-        {notice &&
+        {notice && (
           <div className="comment__notice-placeholder">
             <div className="comment__notice" role="status">
               <Icon name="info-circle" />
               {notice}
             </div>
           </div>
-        }
+        )}
       </>
     );
   }
@@ -331,33 +357,33 @@ export default class CommentReplyComponent extends React.Component<CommentReplyP
     let inner: React.ReactFragment;
 
     switch (this.props.reply.mode) {
-    case 'editing':
-      inner = this.renderEditing();
-      break;
+      case 'editing':
+        inner = this.renderEditing();
+        break;
 
-    case 'saving':
-      inner = this.renderSaving();
-      break;
+      case 'saving':
+        inner = this.renderSaving();
+        break;
 
-    case 'save_error':
-      inner = this.renderSaveError();
-      break;
+      case 'save_error':
+        inner = this.renderSaveError();
+        break;
 
-    case 'delete_confirm':
-      inner = this.renderDeleteConfirm();
-      break;
+      case 'delete_confirm':
+        inner = this.renderDeleteConfirm();
+        break;
 
-    case 'deleting':
-      inner = this.renderDeleting();
-      break;
+      case 'deleting':
+        inner = this.renderDeleting();
+        break;
 
-    case 'delete_error':
-      inner = this.renderDeleteError();
-      break;
+      case 'delete_error':
+        inner = this.renderDeleteError();
+        break;
 
-    default:
-      inner = this.renderDefault();
-      break;
+      default:
+        inner = this.renderDefault();
+        break;
     }
 
     return (

+ 100 - 100
client/src/components/CommentApp/components/CommentReply/style.scss

@@ -1,109 +1,109 @@
 .comment-reply {
-    padding-top: 20px;
-    pointer-events: auto;
-    position: relative;
-    border-top: 1px solid $color-comment-separator;
-
-    &__text {
-        color: $color-box-text;
-        font-size: 13px;
-        line-height: 19px;
-        margin-bottom: 0;
-        padding-top: 10px;
-        padding-bottom: 10px;
-
-        &--mode-deleting {
-            color: $color-grey-1;
-        }
+  padding-top: 20px;
+  pointer-events: auto;
+  position: relative;
+  border-top: 1px solid $color-comment-separator;
+
+  &__text {
+    color: $color-box-text;
+    font-size: 13px;
+    line-height: 19px;
+    margin-bottom: 0;
+    padding-top: 10px;
+    padding-bottom: 10px;
+
+    &--mode-deleting {
+      color: $color-grey-1;
     }
-
-    &--mode-deleting &__avatar {
-        opacity: 0.5;
-    }
-
-    &--mode-deleting &__text {
-        color: $color-grey-3;
-    }
-
-    form {
-        margin-top: 10px;
-    }
-
-    &__button {
-        @include button;
-    }
-
-    &__actions,
-    &__confirm-delete,
-    &__progress,
-    &__error {
-        &::after {
-            display: block;
-            content: '';
-            clear: both;
-        }
-    }
-
-    &__actions {
-        padding-bottom: 10px;
-    }
-
-    &__actions &__button {
-        margin-right: 10px;
-        margin-top: 10px;
+  }
+
+  &--mode-deleting &__avatar {
+    opacity: 0.5;
+  }
+
+  &--mode-deleting &__text {
+    color: $color-grey-3;
+  }
+
+  form {
+    margin-top: 10px;
+  }
+
+  &__button {
+    @include button;
+  }
+
+  &__actions,
+  &__confirm-delete,
+  &__progress,
+  &__error {
+    &::after {
+      display: block;
+      content: '';
+      clear: both;
     }
-
-    &__confirm-delete &__button {
-        margin-left: 10px;
-        margin-bottom: 10px;
+  }
+
+  &__actions {
+    padding-bottom: 10px;
+  }
+
+  &__actions &__button {
+    margin-right: 10px;
+    margin-top: 10px;
+  }
+
+  &__confirm-delete &__button {
+    margin-left: 10px;
+    margin-bottom: 10px;
+  }
+
+  &__confirm-delete,
+  &__error {
+    color: $color-box-text;
+    font-weight: bold;
+    font-size: 13px;
+    margin-top: 10px;
+
+    button {
+      float: right;
     }
 
-    &__confirm-delete,
-    &__error {
-        color: $color-box-text;
-        font-weight: bold;
-        font-size: 13px;
-        margin-top: 10px;
-
-        button {
-            float: right;
-        }
-
-        &::after {
-            display: block;
-            content: ' ';
-            clear: both;
-        }
+    &::after {
+      display: block;
+      content: ' ';
+      clear: both;
     }
-
-    &__error {
-        color: $color-white;
-        background-color: $color-red-dark;
-        border-radius: 3px;
-        padding: 5px;
-        padding-left: 10px;
-        height: 26px;
-        line-height: 26px;
-        vertical-align: middle;
-
-        button {
-            height: 26px;
-            float: right;
-            margin-left: 5px;
-            color: $color-white;
-            background-color: $color-red-very-dark;
-            border-color: $color-red-very-dark;
-            padding: 2px;
-            padding-left: 10px;
-            padding-right: 10px;
-            font-size: 0.65em;
-            font-weight: bold;
-        }
+  }
+
+  &__error {
+    color: $color-white;
+    background-color: $color-red-dark;
+    border-radius: 3px;
+    padding: 5px;
+    padding-left: 10px;
+    height: 26px;
+    line-height: 26px;
+    vertical-align: middle;
+
+    button {
+      height: 26px;
+      float: right;
+      margin-left: 5px;
+      color: $color-white;
+      background-color: $color-red-very-dark;
+      border-color: $color-red-very-dark;
+      padding: 2px;
+      padding-left: 10px;
+      padding-right: 10px;
+      font-size: 0.65em;
+      font-weight: bold;
     }
+  }
 
-    &__progress {
-        margin-top: 20px;
-        font-weight: bold;
-        font-size: 13px;
-    }
+  &__progress {
+    margin-top: 20px;
+    font-weight: bold;
+    font-size: 13px;
+  }
 }

+ 53 - 45
client/src/components/CommentApp/components/TextArea/index.tsx

@@ -7,56 +7,64 @@ export interface TextAreaProps {
   onChange?(newValue: string): void;
   focusOnMount?: boolean;
   focusTarget?: boolean;
-  additionalAttributes?: React.ComponentPropsWithoutRef<'textarea'>
+  additionalAttributes?: React.ComponentPropsWithoutRef<'textarea'>;
 }
 
-const TextArea = React.forwardRef<HTMLTextAreaElement | null, TextAreaProps>(({
-  value,
-  className,
-  placeholder,
-  onChange,
-  focusOnMount,
-  focusTarget = false,
-  additionalAttributes = {}
-}, ref) => {
-  const onChangeValue = (e: React.ChangeEvent<HTMLTextAreaElement>) => {
-    if (onChange) {
-      onChange(e.target.value);
-    }
-  };
+const TextArea = React.forwardRef<HTMLTextAreaElement | null, TextAreaProps>(
+  (
+    {
+      value,
+      className,
+      placeholder,
+      onChange,
+      focusOnMount,
+      focusTarget = false,
+      additionalAttributes = {},
+    },
+    ref,
+  ) => {
+    const onChangeValue = (e: React.ChangeEvent<HTMLTextAreaElement>) => {
+      if (onChange) {
+        onChange(e.target.value);
+      }
+    };
 
-  // Resize the textarea whenever the value is changed
-  const textAreaElement = React.useRef<HTMLTextAreaElement>(null);
-  React.useImperativeHandle<HTMLTextAreaElement | null, HTMLTextAreaElement | null>(ref, () => textAreaElement.current);
+    // Resize the textarea whenever the value is changed
+    const textAreaElement = React.useRef<HTMLTextAreaElement>(null);
+    React.useImperativeHandle<
+      HTMLTextAreaElement | null,
+      HTMLTextAreaElement | null
+    >(ref, () => textAreaElement.current);
 
-  React.useEffect(() => {
-    if (textAreaElement.current) {
-      textAreaElement.current.style.height = '';
-      textAreaElement.current.style.height =
-        textAreaElement.current.scrollHeight + 'px';
-    }
-  }, [value, textAreaElement]);
+    React.useEffect(() => {
+      if (textAreaElement.current) {
+        textAreaElement.current.style.height = '';
+        textAreaElement.current.style.height =
+          textAreaElement.current.scrollHeight + 'px';
+      }
+    }, [value, textAreaElement]);
 
-  // Focus the textarea when it is mounted
-  React.useEffect(() => {
-    if (focusOnMount && textAreaElement.current) {
-      textAreaElement.current.focus();
-    }
-  }, [textAreaElement]);
+    // Focus the textarea when it is mounted
+    React.useEffect(() => {
+      if (focusOnMount && textAreaElement.current) {
+        textAreaElement.current.focus();
+      }
+    }, [textAreaElement]);
 
-  return (
-    <textarea
-      data-focus-target={focusTarget}
-      rows={1}
-      style={{ resize: 'none', overflowY: 'hidden' }}
-      className={className}
-      placeholder={placeholder}
-      ref={textAreaElement}
-      onChange={onChangeValue}
-      value={value}
-      {...additionalAttributes}
-    />
-  );
-});
+    return (
+      <textarea
+        data-focus-target={focusTarget}
+        rows={1}
+        style={{ resize: 'none', overflowY: 'hidden' }}
+        className={className}
+        placeholder={placeholder}
+        ref={textAreaElement}
+        onChange={onChangeValue}
+        value={value}
+        {...additionalAttributes}
+      />
+    );
+  },
+);
 
 export default TextArea;

+ 100 - 100
client/src/components/CommentApp/main.scss

@@ -23,125 +23,125 @@ $box-border-radius: 5px;
 $box-padding: 10px;
 
 @mixin focus-outline {
-    outline: $color-focus-outline solid 3px;
+  outline: $color-focus-outline solid 3px;
 }
 
 @mixin box {
-    background-color: $color-box-background;
-    border: 1px solid $color-box-border;
-    padding: $box-padding;
-    font-size: 11px;
-    border-radius: $box-border-radius;
+  background-color: $color-box-background;
+  border: 1px solid $color-box-border;
+  padding: $box-padding;
+  font-size: 11px;
+  border-radius: $box-border-radius;
+  color: $color-box-text;
+
+  &--focused {
+    border-color: #bbb;
+    box-shadow: 3px 2px 3px -1px rgba(0, 0, 0, 0.1);
+  }
+
+  textarea {
+    font-family: $font-sans;
+    margin: 0;
+    padding: 10px;
+    width: 100%;
+    background-color: $color-textarea-background;
+    border: 1px solid $color-textarea-border;
+    box-sizing: border-box;
+    border-radius: 5px;
+    -moz-outline-radius: 8px;
     color: $color-box-text;
 
-    &--focused {
-        border-color: #bbb;
-        box-shadow: 3px 2px 3px -1px rgba(0, 0, 0, 0.1);
+    &::placeholder {
+      color: $color-textarea-placeholder-text;
+      opacity: 1;
     }
 
-    textarea {
-        font-family: $font-sans;
-        margin: 0;
-        padding: 10px;
-        width: 100%;
-        background-color: $color-textarea-background;
-        border: 1px solid $color-textarea-border;
-        box-sizing: border-box;
-        border-radius: 5px;
-        -moz-outline-radius: 8px;
-        color: $color-box-text;
-
-        &::placeholder {
-            color: $color-textarea-placeholder-text;
-            opacity: 1;
-        }
-
-        &:focus {
-            background-color: $color-textarea-background-focused;
-            border-color: $color-textarea-border-focused;
-            outline: unset;
-        }
+    &:focus {
+      background-color: $color-textarea-background-focused;
+      border-color: $color-textarea-border-focused;
+      outline: unset;
     }
+  }
 
-    *:focus {
-        @include focus-outline;
-    }
+  *:focus {
+    @include focus-outline;
+  }
 
-    &__notice-placeholder {
-        position: relative;
-        padding-bottom: 40px;
-    }
+  &__notice-placeholder {
+    position: relative;
+    padding-bottom: 40px;
+  }
 
-    &__notice {
-        background-color: $color-amber-1;
-        position: absolute;
-        left: -$box-padding;
-        bottom: 0;
-        width: calc(100% + #{$box-padding} * 2);
-        padding: 5px 10px;
-        box-sizing: border-box;
-
-        svg.icon {
-            color: $color-amber-0;
-            width: 14px;
-            height: 14px;
-            margin-right: 10px;
-            vertical-align: text-bottom;
-        }
+  &__notice {
+    background-color: $color-amber-1;
+    position: absolute;
+    left: -$box-padding;
+    bottom: 0;
+    width: calc(100% + #{$box-padding} * 2);
+    padding: 5px 10px;
+    box-sizing: border-box;
+
+    svg.icon {
+      color: $color-amber-0;
+      width: 14px;
+      height: 14px;
+      margin-right: 10px;
+      vertical-align: text-bottom;
     }
+  }
 
-    > :last-child &__notice {
-        bottom: -$box-padding;
-        border-bottom-left-radius: $box-border-radius;
-        border-bottom-right-radius: $box-border-radius;
-    }
+  > :last-child &__notice {
+    bottom: -$box-padding;
+    border-bottom-left-radius: $box-border-radius;
+    border-bottom-right-radius: $box-border-radius;
+  }
 }
 
 @mixin button {
-    background-color: inherit;
-    border: 1px solid $color-grey-3;
-    border-radius: 3px;
-    -moz-outline-radius: 6px;
-    color: $color-teal;
-    cursor: pointer;
-    text-transform: uppercase;
-    font-family: inherit;
-    font-size: 12px;
-    font-weight: bold;
-    height: 25px;
-    padding-left: 5px;
-    padding-right: 5px;
-
-    &--primary {
-        color: $color-white;
-        border: 1px solid $color-teal;
-        background-color: $color-teal;
-    }
-
-    &--red {
-        color: $color-white;
-        border: 1px solid $color-red-very-dark;
-        background-color: $color-red-very-dark;
-    }
-
-    &:disabled {
-        opacity: 0.3;
-    }
-
-    // Disable Firefox's focus styling because we add our own.
-    &::-moz-focus-inner {
-        border: 0;
-    }
+  background-color: inherit;
+  border: 1px solid $color-grey-3;
+  border-radius: 3px;
+  -moz-outline-radius: 6px;
+  color: $color-teal;
+  cursor: pointer;
+  text-transform: uppercase;
+  font-family: inherit;
+  font-size: 12px;
+  font-weight: bold;
+  height: 25px;
+  padding-left: 5px;
+  padding-right: 5px;
+
+  &--primary {
+    color: $color-white;
+    border: 1px solid $color-teal;
+    background-color: $color-teal;
+  }
+
+  &--red {
+    color: $color-white;
+    border: 1px solid $color-red-very-dark;
+    background-color: $color-red-very-dark;
+  }
+
+  &:disabled {
+    opacity: 0.3;
+  }
+
+  // Disable Firefox's focus styling because we add our own.
+  &::-moz-focus-inner {
+    border: 0;
+  }
 }
 
 .comments-list {
-    width: 400px;
-    position: absolute;
-    top: 30px;
-    right: 30px;
-    z-index: 50;
-    font-family: $font-sans;
-    pointer-events: none;
+  width: 400px;
+  position: absolute;
+  top: 30px;
+  right: 30px;
+  z-index: 50;
+  font-family: $font-sans;
+  pointer-events: none;
 }
 
 // stylelint-disable no-invalid-position-at-import-rule

+ 83 - 62
client/src/components/CommentApp/main.tsx

@@ -14,7 +14,7 @@ import {
   setFocusedComment,
   updateComment,
   commentActionFunctions,
-  invalidateContentPath
+  invalidateContentPath,
 } from './actions/comments';
 import { updateGlobalSettings } from './actions/settings';
 import {
@@ -24,7 +24,7 @@ import {
   selectEnabled,
   selectFocused,
   selectIsDirty,
-  selectCommentCount
+  selectCommentCount,
 } from './selectors';
 import CommentComponent from './components/Comment';
 import { CommentFormSetComponent } from './components/Form';
@@ -72,7 +72,6 @@ export const defaultStrings = {
   SAVE_PAGE_TO_SAVE_REPLY: 'Save the page to save this reply',
 };
 
- 
 // This is done as this is serialized pretty directly from the Django model
 export interface InitialCommentReply {
   pk: number;
@@ -97,9 +96,14 @@ export interface InitialComment {
 }
 /* eslint-enable */
 
- 
-const getAuthor = (authors: Map<string, {name: string, avatar_url: string}>, id: any): Author => {
-  const authorData = getOrDefault(authors, String(id), { name: '', avatar_url: '' });
+const getAuthor = (
+  authors: Map<string, { name: string; avatar_url: string }>,
+  id: any,
+): Author => {
+  const authorData = getOrDefault(authors, String(id), {
+    name: '',
+    avatar_url: '',
+  });
 
   return {
     id,
@@ -112,7 +116,7 @@ function renderCommentsUi(
   store: Store,
   layout: LayoutController,
   comments: Comment[],
-  strings: TranslatableStrings
+  strings: TranslatableStrings,
 ): React.ReactElement {
   const state = store.getState();
   const { commentsEnabled, user, currentTab } = state.settings;
@@ -123,7 +127,9 @@ function renderCommentsUi(
     commentsToRender = [];
   }
   // Hide all resolved/deleted comments
-  commentsToRender = commentsToRender.filter(({ deleted, resolved }) => !(deleted || resolved));
+  commentsToRender = commentsToRender.filter(
+    ({ deleted, resolved }) => !(deleted || resolved),
+  );
   const commentsRendered = commentsToRender.map((comment) => (
     <CommentComponent
       key={comment.localId}
@@ -137,9 +143,7 @@ function renderCommentsUi(
       strings={strings}
     />
   ));
-  return (
-    <ol className="comments-list">{commentsRendered}</ol>
-  );
+  return <ol className="comments-list">{commentsRendered}</ol>;
   /* eslint-enable react/no-danger */
 }
 
@@ -148,47 +152,39 @@ export class CommentApp {
   layout: LayoutController;
   utils = {
     selectCommentsForContentPathFactory,
-    selectCommentFactory
-  }
+    selectCommentFactory,
+  };
   selectors = {
     selectComments,
     selectEnabled,
     selectFocused,
     selectIsDirty,
-    selectCommentCount
-  }
+    selectCommentCount,
+  };
   actions = commentActionFunctions;
 
   constructor() {
     this.store = createStore(reducer, {
-      settings: INITIAL_SETTINGS_STATE
+      settings: INITIAL_SETTINGS_STATE,
     });
     this.layout = new LayoutController();
   }
-   
-  setUser(userId: any, authors: Map<string, {name: string, avatar_url: string}>) {
+
+  setUser(
+    userId: any,
+    authors: Map<string, { name: string; avatar_url: string }>,
+  ) {
     this.store.dispatch(
       updateGlobalSettings({
-        user: getAuthor(authors, userId)
-      })
+        user: getAuthor(authors, userId),
+      }),
     );
   }
-  updateAnnotation(
-    annotation: Annotation,
-    commentId: number
-  ) {
+  updateAnnotation(annotation: Annotation, commentId: number) {
     this.attachAnnotationLayout(annotation, commentId);
-    this.store.dispatch(
-      updateComment(
-        commentId,
-        { annotation: annotation }
-      )
-    );
+    this.store.dispatch(updateComment(commentId, { annotation: annotation }));
   }
-  attachAnnotationLayout(
-    annotation: Annotation,
-    commentId: number
-  ) {
+  attachAnnotationLayout(annotation: Annotation, commentId: number) {
     // Attach an annotation to an existing comment in the layout
 
     // const layout engine know the annotation so it would position the comment correctly
@@ -214,19 +210,26 @@ export class CommentApp {
           Date.now(),
           {
             mode: 'creating',
-          }
-        )
-      )
+          },
+        ),
+      ),
     );
 
     // Focus and pin the comment
-    this.store.dispatch(setFocusedComment(commentId, { updatePinnedComment: true, forceFocus: true }));
+    this.store.dispatch(
+      setFocusedComment(commentId, {
+        updatePinnedComment: true,
+        forceFocus: true,
+      }),
+    );
     return commentId;
   }
   setVisible(visible: boolean) {
-    this.store.dispatch(updateGlobalSettings({
-      commentsEnabled: visible,
-    }));
+    this.store.dispatch(
+      updateGlobalSettings({
+        commentsEnabled: visible,
+      }),
+    );
   }
   invalidateContentPath(contentPath: string) {
     // Called when a given content path on the form is no longer valid (eg, a block has been deleted)
@@ -237,9 +240,9 @@ export class CommentApp {
     outputElement: HTMLElement,
     userId: any,
     initialComments: InitialComment[],
-     
-    authors: Map<string, {name: string, avatar_url: string}>,
-    translationStrings: TranslatableStrings | null
+
+    authors: Map<string, { name: string; avatar_url: string }>,
+    translationStrings: TranslatableStrings | null,
   ) {
     let pinnedComment: number | null = null;
     this.setUser(userId, authors);
@@ -258,14 +261,18 @@ export class CommentApp {
 
     const render = () => {
       const state = this.store.getState();
-      const commentList: Comment[] = Array.from(state.comments.comments.values());
+      const commentList: Comment[] = Array.from(
+        state.comments.comments.values(),
+      );
 
       ReactDOM.render(
         <CommentFormSetComponent
-          comments={commentList.filter(comment => comment.mode !== 'creating')}
+          comments={commentList.filter(
+            (comment) => comment.mode !== 'creating',
+          )}
           remoteCommentCount={state.comments.remoteCommentCount}
         />,
-        outputElement
+        outputElement,
       );
 
       // Check if the pinned comment has changed
@@ -287,10 +294,10 @@ export class CommentApp {
           if (this.layout.refreshLayout()) {
             ReactDOM.render(
               renderCommentsUi(this.store, this.layout, commentList, strings),
-              element
+              element,
             );
           }
-        }
+        },
       );
     };
 
@@ -312,10 +319,10 @@ export class CommentApp {
               remoteId: comment.pk,
               text: comment.text,
               deleted: comment.deleted,
-              resolved: comment.resolved
-            }
-          )
-        )
+              resolved: comment.resolved,
+            },
+          ),
+        ),
       );
 
       // Create replies
@@ -330,18 +337,23 @@ export class CommentApp {
               {
                 remoteId: reply.pk,
                 text: reply.text,
-                deleted: reply.deleted
-              }
-            )
-          )
+                deleted: reply.deleted,
+              },
+            ),
+          ),
         );
       }
 
       // If this is the initial focused comment. Focus and pin it
-       
+
       // TODO: Scroll to this comment
       if (initialFocusedCommentId && comment.pk === initialFocusedCommentId) {
-        this.store.dispatch(setFocusedComment(commentId, { updatePinnedComment: true, forceFocus: true }));
+        this.store.dispatch(
+          setFocusedComment(commentId, {
+            updatePinnedComment: true,
+            forceFocus: true,
+          }),
+        );
       }
     }
 
@@ -353,10 +365,17 @@ export class CommentApp {
     document.body.addEventListener('mousedown', (e) => {
       if (e.target instanceof HTMLElement) {
         // ignore if click target is a comment or an annotation
-        if (!e.target.closest('#comments, [data-annotation], [data-comment-add]')) {
+        if (
+          !e.target.closest('#comments, [data-annotation], [data-comment-add]')
+        ) {
           // Running store.dispatch directly here seems to prevent the event from being handled anywhere else
           setTimeout(() => {
-            this.store.dispatch(setFocusedComment(null, { updatePinnedComment: true, forceFocus: false }));
+            this.store.dispatch(
+              setFocusedComment(null, {
+                updatePinnedComment: true,
+                forceFocus: false,
+              }),
+            );
           }, 200);
         }
       }
@@ -365,7 +384,9 @@ export class CommentApp {
     document.body.addEventListener('commentAnchorVisibilityChange', () => {
       // If any streamfield blocks or panels have collapsed or expanded
       // check if we need to rerender
-      this.layout.refreshDesiredPositions(this.store.getState().settings.currentTab);
+      this.layout.refreshDesiredPositions(
+        this.store.getState().settings.currentTab,
+      );
       if (this.layout.refreshLayout()) {
         render();
       }

+ 17 - 14
client/src/components/CommentApp/selectors/index.ts

@@ -4,14 +4,16 @@ import type { State } from '../state';
 
 export const selectComments = (state: State) => state.comments.comments;
 export const selectFocused = (state: State) => state.comments.focusedComment;
-export const selectRemoteCommentCount = (state: State) => state.comments.remoteCommentCount;
+export const selectRemoteCommentCount = (state: State) =>
+  state.comments.remoteCommentCount;
 
 export function selectCommentsForContentPathFactory(contentpath: string) {
   return createSelector(selectComments, (comments) =>
     [...comments.values()].filter(
       (comment: Comment) =>
-        comment.contentpath === contentpath && !(comment.deleted || comment.resolved)
-    )
+        comment.contentpath === contentpath &&
+        !(comment.deleted || comment.resolved),
+    ),
   );
 }
 
@@ -22,9 +24,7 @@ export function selectCommentFactory(localId: number) {
       return undefined;
     }
     return comment;
-  }
-
-  );
+  });
 }
 
 export const selectEnabled = (state: State) => state.settings.commentsEnabled;
@@ -36,20 +36,23 @@ export const selectIsDirty = createSelector(
     if (remoteCommentCount !== comments.size) {
       return true;
     }
-    return Array.from(comments.values()).some(comment => {
-      if (comment.deleted ||
+    return Array.from(comments.values()).some((comment) => {
+      if (
+        comment.deleted ||
         comment.resolved ||
         comment.replies.size !== comment.remoteReplyCount ||
         comment.originalText !== comment.text
       ) {
         return true;
       }
-      return Array.from(comment.replies.values()).some(reply => reply.deleted || reply.originalText !== reply.text);
+      return Array.from(comment.replies.values()).some(
+        (reply) => reply.deleted || reply.originalText !== reply.text,
+      );
     });
-  });
+  },
+);
 
-export const selectCommentCount = (state: State) => (
+export const selectCommentCount = (state: State) =>
   [...state.comments.comments.values()].filter(
-    (comment: Comment) => !comment.deleted && !comment.resolved
-  ).length
-);
+    (comment: Comment) => !comment.deleted && !comment.resolved,
+  ).length;

+ 63 - 54
client/src/components/CommentApp/selectors/selectors.test.ts

@@ -18,12 +18,10 @@ test('Select comments for contentpath', () => {
     comments: basicCommentsState,
     settings: INITIAL_SETTINGS_STATE,
   };
-  const testContentPathSelector = selectCommentsForContentPathFactory(
-    'test_contentpath'
-  );
-  const testContentPathSelector2 = selectCommentsForContentPathFactory(
-    'test_contentpath_2'
-  );
+  const testContentPathSelector =
+    selectCommentsForContentPathFactory('test_contentpath');
+  const testContentPathSelector2 =
+    selectCommentsForContentPathFactory('test_contentpath_2');
   const selectedComments = testContentPathSelector(state);
   expect(selectedComments.length).toBe(1);
   expect(selectedComments[0].contentpath).toBe('test_contentpath');
@@ -35,79 +33,90 @@ test('Select comments for contentpath', () => {
 test('Select is dirty', () => {
   const state = {
     comments: INITIAL_COMMENTS_STATE,
-    settings: INITIAL_SETTINGS_STATE
+    settings: INITIAL_SETTINGS_STATE,
   };
-  const stateWithUnsavedComment = reducer(state, actions.addComment(newComment(
-    'test_contentpath',
-    'test_position',
-    1,
-    null,
-    null,
-    0,
-    {
-      remoteId: null,
-      text: 'my new comment'
-    }
-  )));
+  const stateWithUnsavedComment = reducer(
+    state,
+    actions.addComment(
+      newComment('test_contentpath', 'test_position', 1, null, null, 0, {
+        remoteId: null,
+        text: 'my new comment',
+      }),
+    ),
+  );
 
   expect(selectIsDirty(stateWithUnsavedComment)).toBe(true);
 
-  const stateWithSavedComment = reducer(state, actions.addComment(newComment(
-    'test_contentpath',
-    'test_position',
-    1,
-    null,
-    null,
-    0,
-    {
-      remoteId: 1,
-      text: 'my saved comment'
-    }
-  )));
+  const stateWithSavedComment = reducer(
+    state,
+    actions.addComment(
+      newComment('test_contentpath', 'test_position', 1, null, null, 0, {
+        remoteId: 1,
+        text: 'my saved comment',
+      }),
+    ),
+  );
 
   expect(selectIsDirty(stateWithSavedComment)).toBe(false);
 
-  const stateWithDeletedComment = reducer(stateWithSavedComment, actions.deleteComment(1));
+  const stateWithDeletedComment = reducer(
+    stateWithSavedComment,
+    actions.deleteComment(1),
+  );
 
   expect(selectIsDirty(stateWithDeletedComment)).toBe(true);
 
-  const stateWithResolvedComment = reducer(stateWithSavedComment, actions.updateComment(1, { resolved: true }));
+  const stateWithResolvedComment = reducer(
+    stateWithSavedComment,
+    actions.updateComment(1, { resolved: true }),
+  );
 
   expect(selectIsDirty(stateWithResolvedComment)).toBe(true);
 
-  const stateWithEditedComment = reducer(stateWithSavedComment, actions.updateComment(1, { text: 'edited_text' }));
+  const stateWithEditedComment = reducer(
+    stateWithSavedComment,
+    actions.updateComment(1, { text: 'edited_text' }),
+  );
 
   expect(selectIsDirty(stateWithEditedComment)).toBe(true);
 
-  const stateWithUnsavedReply = reducer(stateWithSavedComment, actions.addReply(1, newCommentReply(
-    2,
-    null,
-    0,
-    {
-      remoteId: null,
-      text: 'new reply'
-    }
-  )));
+  const stateWithUnsavedReply = reducer(
+    stateWithSavedComment,
+    actions.addReply(
+      1,
+      newCommentReply(2, null, 0, {
+        remoteId: null,
+        text: 'new reply',
+      }),
+    ),
+  );
 
   expect(selectIsDirty(stateWithUnsavedReply)).toBe(true);
 
-  const stateWithSavedReply = reducer(stateWithSavedComment, actions.addReply(1, newCommentReply(
-    2,
-    null,
-    0,
-    {
-      remoteId: 2,
-      text: 'new saved reply'
-    }
-  )));
+  const stateWithSavedReply = reducer(
+    stateWithSavedComment,
+    actions.addReply(
+      1,
+      newCommentReply(2, null, 0, {
+        remoteId: 2,
+        text: 'new saved reply',
+      }),
+    ),
+  );
 
   expect(selectIsDirty(stateWithSavedReply)).toBe(false);
 
-  const stateWithDeletedReply = reducer(stateWithSavedReply, actions.deleteReply(1, 2));
+  const stateWithDeletedReply = reducer(
+    stateWithSavedReply,
+    actions.deleteReply(1, 2),
+  );
 
   expect(selectIsDirty(stateWithDeletedReply)).toBe(true);
 
-  const stateWithEditedReply = reducer(stateWithSavedReply, actions.updateReply(1, 2, { text: 'edited_text' }));
+  const stateWithEditedReply = reducer(
+    stateWithSavedReply,
+    actions.updateReply(1, 2, { text: 'edited_text' }),
+  );
 
   expect(selectIsDirty(stateWithEditedReply)).toBe(true);
 });

+ 16 - 8
client/src/components/CommentApp/state/comments.test.ts

@@ -41,7 +41,7 @@ test('New comment added to state', () => {
   const newState = reducer(basicCommentsState, commentAction);
   expect(newState.comments.get(newComment.localId)).toBe(newComment);
   expect(newState.remoteCommentCount).toBe(
-    basicCommentsState.remoteCommentCount
+    basicCommentsState.remoteCommentCount,
   );
 });
 
@@ -68,13 +68,13 @@ test('Remote comment added to state', () => {
   const newState = reducer(basicCommentsState, commentAction);
   expect(newState.comments.get(newComment.localId)).toBe(newComment);
   expect(newState.remoteCommentCount).toBe(
-    basicCommentsState.remoteCommentCount + 1
+    basicCommentsState.remoteCommentCount + 1,
   );
 });
 
 test('Existing comment updated', () => {
   const commentUpdate: CommentUpdate = {
-    mode: 'editing'
+    mode: 'editing',
   };
   const updateAction = actions.updateComment(1, commentUpdate);
   const newState = reducer(basicCommentsState, updateAction);
@@ -111,7 +111,7 @@ test('Remote comment deleted', () => {
   expect(newState.focusedComment).toBe(null);
   expect(newState.pinnedComment).toBe(null);
   expect(newState.remoteCommentCount).toBe(
-    basicCommentsState.remoteCommentCount
+    basicCommentsState.remoteCommentCount,
   );
 });
 
@@ -127,12 +127,15 @@ test('Remote comment resolved', () => {
   expect(newState.focusedComment).toBe(null);
   expect(newState.pinnedComment).toBe(null);
   expect(newState.remoteCommentCount).toBe(
-    basicCommentsState.remoteCommentCount
+    basicCommentsState.remoteCommentCount,
   );
 });
 
 test('Comment focused', () => {
-  const focusAction = actions.setFocusedComment(4, { updatePinnedComment: true, forceFocus: true });
+  const focusAction = actions.setFocusedComment(4, {
+    updatePinnedComment: true,
+    forceFocus: true,
+  });
   const newState = reducer(basicCommentsState, focusAction);
   expect(newState.focusedComment).toBe(4);
   expect(newState.pinnedComment).toBe(4);
@@ -140,7 +143,10 @@ test('Comment focused', () => {
 });
 
 test('Invalid comment not focused', () => {
-  const focusAction = actions.setFocusedComment(9000, { updatePinnedComment: true, forceFocus: true });
+  const focusAction = actions.setFocusedComment(9000, {
+    updatePinnedComment: true,
+    forceFocus: true,
+  });
   const newState = reducer(basicCommentsState, focusAction);
   expect(newState.focusedComment).toBe(basicCommentsState.focusedComment);
   expect(newState.pinnedComment).toBe(basicCommentsState.pinnedComment);
@@ -194,7 +200,9 @@ test('Remote reply added', () => {
     expect(stateReply).toBeDefined();
     expect(stateReply).toBe(reply);
     if (originalComment) {
-      expect(comment.remoteReplyCount).toBe(originalComment.remoteReplyCount + 1);
+      expect(comment.remoteReplyCount).toBe(
+        originalComment.remoteReplyCount + 1,
+      );
     }
   }
 });

+ 136 - 133
client/src/components/CommentApp/state/comments.ts

@@ -54,8 +54,8 @@ export function newCommentReply(
     remoteId = null,
     mode = 'default',
     text = '',
-    deleted = false
-  }: NewReplyOptions
+    deleted = false,
+  }: NewReplyOptions,
 ): CommentReply {
   return {
     localId,
@@ -130,7 +130,7 @@ export function newComment(
     resolved = false,
     deleted = false,
     replies = new Map(),
-  }: NewCommentOptions
+  }: NewCommentOptions,
 ): Comment {
   return {
     contentpath,
@@ -150,7 +150,7 @@ export function newComment(
     resolved,
     remoteReplyCount: Array.from(replies.values()).reduce(
       (n, reply) => (reply.remoteId !== null ? n + 1 : n),
-      0
+      0,
     ),
   };
 }
@@ -174,142 +174,145 @@ export const INITIAL_STATE: CommentsState = {
   remoteCommentCount: 0,
 };
 
-export const reducer = produce((draft: CommentsState, action: actions.Action) => {
-  /* eslint-disable no-param-reassign */
-  const deleteComment = (comment: Comment) => {
-    if (!comment.remoteId) {
-      // If the comment doesn't exist in the database, there's no need to keep it around locally
-      draft.comments.delete(comment.localId);
-    } else {
-      comment.deleted = true;
-    }
+export const reducer = produce(
+  (draft: CommentsState, action: actions.Action) => {
+    /* eslint-disable no-param-reassign */
+    const deleteComment = (comment: Comment) => {
+      if (!comment.remoteId) {
+        // If the comment doesn't exist in the database, there's no need to keep it around locally
+        draft.comments.delete(comment.localId);
+      } else {
+        comment.deleted = true;
+      }
 
-    // Unset focusedComment if the focused comment is the one being deleted
-    if (draft.focusedComment === comment.localId) {
-      draft.focusedComment = null;
-      draft.forceFocus = false;
-    }
-    if (draft.pinnedComment === comment.localId) {
-      draft.pinnedComment = null;
-    }
-  };
+      // Unset focusedComment if the focused comment is the one being deleted
+      if (draft.focusedComment === comment.localId) {
+        draft.focusedComment = null;
+        draft.forceFocus = false;
+      }
+      if (draft.pinnedComment === comment.localId) {
+        draft.pinnedComment = null;
+      }
+    };
 
-  const resolveComment = (comment: Comment) => {
-    if (!comment.remoteId) {
-      // If the comment doesn't exist in the database, there's no need to keep it around locally
-      draft.comments.delete(comment.localId);
-    } else {
-      comment.resolved = true;
-    }
-    // Unset focusedComment if the focused comment is the one being resolved
-    if (draft.focusedComment === comment.localId) {
-      draft.focusedComment = null;
-    }
-    if (draft.pinnedComment === comment.localId) {
-      draft.pinnedComment = null;
-    }
-  };
+    const resolveComment = (comment: Comment) => {
+      if (!comment.remoteId) {
+        // If the comment doesn't exist in the database, there's no need to keep it around locally
+        draft.comments.delete(comment.localId);
+      } else {
+        comment.resolved = true;
+      }
+      // Unset focusedComment if the focused comment is the one being resolved
+      if (draft.focusedComment === comment.localId) {
+        draft.focusedComment = null;
+      }
+      if (draft.pinnedComment === comment.localId) {
+        draft.pinnedComment = null;
+      }
+    };
 
-  switch (action.type) {
-  case actions.ADD_COMMENT: {
-    draft.comments.set(action.comment.localId, action.comment);
-    if (action.comment.remoteId) {
-      draft.remoteCommentCount += 1;
-    }
-    break;
-  }
-  case actions.UPDATE_COMMENT: {
-    const comment = draft.comments.get(action.commentId);
-    if (comment) {
-      if (action.update.newText && action.update.newText.length === 0) {
+    switch (action.type) {
+      case actions.ADD_COMMENT: {
+        draft.comments.set(action.comment.localId, action.comment);
+        if (action.comment.remoteId) {
+          draft.remoteCommentCount += 1;
+        }
         break;
       }
-      update(comment, action.update);
-    }
-    break;
-  }
-  case actions.DELETE_COMMENT: {
-    const comment = draft.comments.get(action.commentId);
-    if (!comment) {
-      break;
-    }
-
-    deleteComment(comment);
-    break;
-  }
-  case actions.RESOLVE_COMMENT: {
-    const comment = draft.comments.get(action.commentId);
-    if (!comment) {
-      break;
-    }
+      case actions.UPDATE_COMMENT: {
+        const comment = draft.comments.get(action.commentId);
+        if (comment) {
+          if (action.update.newText && action.update.newText.length === 0) {
+            break;
+          }
+          update(comment, action.update);
+        }
+        break;
+      }
+      case actions.DELETE_COMMENT: {
+        const comment = draft.comments.get(action.commentId);
+        if (!comment) {
+          break;
+        }
 
-    resolveComment(comment);
-    break;
-  }
-  case actions.SET_FOCUSED_COMMENT: {
-    if ((action.commentId === null) || (draft.comments.has(action.commentId))) {
-      draft.focusedComment = action.commentId;
-      if (action.updatePinnedComment) {
-        draft.pinnedComment = action.commentId;
+        deleteComment(comment);
+        break;
       }
-      draft.forceFocus = action.forceFocus;
-    }
-    break;
-  }
-  case actions.ADD_REPLY: {
-    const comment = draft.comments.get(action.commentId);
-    if ((!comment) || action.reply.text.length === 0) {
-      break;
-    }
-    if (action.reply.remoteId) {
-      comment.remoteReplyCount += 1;
-    }
-    comment.replies.set(action.reply.localId, action.reply);
-    break;
-  }
-  case actions.UPDATE_REPLY: {
-    const comment = draft.comments.get(action.commentId);
-    if (!comment) {
-      break;
-    }
-    const reply = comment.replies.get(action.replyId);
-    if (!reply) {
-      break;
-    }
-    if (action.update.newText && action.update.newText.length === 0) {
-      break;
-    }
-    update(reply, action.update);
-    break;
-  }
-  case actions.DELETE_REPLY: {
-    const comment = draft.comments.get(action.commentId);
-    if (!comment) {
-      break;
-    }
-    const reply = comment.replies.get(action.replyId);
-    if (!reply) {
-      break;
-    }
-    if (!reply.remoteId) {
-      // The reply doesn't exist in the database, so we don't need to store it locally
-      comment.replies.delete(reply.localId);
-    } else {
-      reply.deleted = true;
-    }
-    break;
-  }
-  case actions.INVALIDATE_CONTENT_PATH: {
-    // Delete any comments that exist in the contentpath
-    const comments = draft.comments;
-    for (const comment of comments.values()) {
-      if (comment.contentpath.startsWith(action.contentPath)) {
+      case actions.RESOLVE_COMMENT: {
+        const comment = draft.comments.get(action.commentId);
+        if (!comment) {
+          break;
+        }
+
         resolveComment(comment);
+        break;
       }
+      case actions.SET_FOCUSED_COMMENT: {
+        if (action.commentId === null || draft.comments.has(action.commentId)) {
+          draft.focusedComment = action.commentId;
+          if (action.updatePinnedComment) {
+            draft.pinnedComment = action.commentId;
+          }
+          draft.forceFocus = action.forceFocus;
+        }
+        break;
+      }
+      case actions.ADD_REPLY: {
+        const comment = draft.comments.get(action.commentId);
+        if (!comment || action.reply.text.length === 0) {
+          break;
+        }
+        if (action.reply.remoteId) {
+          comment.remoteReplyCount += 1;
+        }
+        comment.replies.set(action.reply.localId, action.reply);
+        break;
+      }
+      case actions.UPDATE_REPLY: {
+        const comment = draft.comments.get(action.commentId);
+        if (!comment) {
+          break;
+        }
+        const reply = comment.replies.get(action.replyId);
+        if (!reply) {
+          break;
+        }
+        if (action.update.newText && action.update.newText.length === 0) {
+          break;
+        }
+        update(reply, action.update);
+        break;
+      }
+      case actions.DELETE_REPLY: {
+        const comment = draft.comments.get(action.commentId);
+        if (!comment) {
+          break;
+        }
+        const reply = comment.replies.get(action.replyId);
+        if (!reply) {
+          break;
+        }
+        if (!reply.remoteId) {
+          // The reply doesn't exist in the database, so we don't need to store it locally
+          comment.replies.delete(reply.localId);
+        } else {
+          reply.deleted = true;
+        }
+        break;
+      }
+      case actions.INVALIDATE_CONTENT_PATH: {
+        // Delete any comments that exist in the contentpath
+        const comments = draft.comments;
+        for (const comment of comments.values()) {
+          if (comment.contentpath.startsWith(action.contentPath)) {
+            resolveComment(comment);
+          }
+        }
+        break;
+      }
+      default:
+        break;
     }
-    break;
-  }
-  default:
-    break;
-  }
-}, INITIAL_STATE);
+  },
+  INITIAL_STATE,
+);

+ 12 - 9
client/src/components/CommentApp/state/settings.ts

@@ -18,12 +18,15 @@ export const INITIAL_STATE: SettingsState = {
   currentTab: null,
 };
 
-export const reducer = produce((draft: SettingsState, action: actions.Action) => {
-  switch (action.type) {
-  case actions.UPDATE_GLOBAL_SETTINGS:
-    update(draft, action.update);
-    break;
-  default:
-    break;
-  }
-}, INITIAL_STATE);
+export const reducer = produce(
+  (draft: SettingsState, action: actions.Action) => {
+    switch (action.type) {
+      case actions.UPDATE_GLOBAL_SETTINGS:
+        update(draft, action.update);
+        break;
+      default:
+        break;
+    }
+  },
+  INITIAL_STATE,
+);

+ 23 - 14
client/src/components/CommentApp/utils/layout.ts

@@ -51,11 +51,15 @@ export class LayoutController {
       return;
     }
 
-    const currentNodeTop = annotation.getAnchorNode(commentId === this.pinnedComment).getBoundingClientRect().top;
+    const currentNodeTop = annotation
+      .getAnchorNode(commentId === this.pinnedComment)
+      .getBoundingClientRect().top;
 
     this.commentDesiredPositions.set(
       commentId,
-      currentNodeTop !== 0 ? currentNodeTop + document.documentElement.scrollTop + OFFSET : 0
+      currentNodeTop !== 0
+        ? currentNodeTop + document.documentElement.scrollTop + OFFSET
+        : 0,
     );
   }
 
@@ -102,32 +106,35 @@ export class LayoutController {
         height: getOrDefault(this.commentHeights, commentId, 0),
         comments: [commentId],
         containsPinnedComment:
-            this.pinnedComment !== null && commentId === this.pinnedComment,
+          this.pinnedComment !== null && commentId === this.pinnedComment,
         pinnedCommentPosition: 0,
-      })
+      }),
     );
 
     // Group blocks by tabs
     const blocksByTab: Map<string | null, Block[]> = new Map();
-    allBlocks.forEach(block => {
+    allBlocks.forEach((block) => {
       const blocks = blocksByTab.get(block.tab) || [];
       blocks.push(block);
       blocksByTab.set(block.tab, blocks);
     });
 
     // Get location of pinned comment
-    const pinnedCommentPosition = this.pinnedComment ?
-      this.commentDesiredPositions.get(this.pinnedComment) : undefined;
-    const pinnedCommentTab = this.pinnedComment ?
-      this.commentTabs.get(this.pinnedComment) : undefined;
+    const pinnedCommentPosition = this.pinnedComment
+      ? this.commentDesiredPositions.get(this.pinnedComment)
+      : undefined;
+    const pinnedCommentTab = this.pinnedComment
+      ? this.commentTabs.get(this.pinnedComment)
+      : undefined;
 
     // For each tab, resolve positions of all the comments
     Array.from(blocksByTab.entries()).forEach(([tab, blocks]) => {
-      const pinnedCommentOnThisTab = this.pinnedComment && pinnedCommentTab === tab;
+      const pinnedCommentOnThisTab =
+        this.pinnedComment && pinnedCommentTab === tab;
 
       // Sort blocks
       blocks.sort(
-        (block, comparisonBlock) => block.position - comparisonBlock.position
+        (block, comparisonBlock) => block.position - comparisonBlock.position,
       );
 
       // Resolve overlapping blocks
@@ -173,8 +180,7 @@ export class LayoutController {
                 previousBlock.containsPinnedComment
               ) {
                 previousBlock.position =
-                  pinnedCommentPosition -
-                  previousBlock.pinnedCommentPosition;
+                  pinnedCommentPosition - previousBlock.pinnedCommentPosition;
               }
 
               continue;
@@ -212,7 +218,10 @@ export class LayoutController {
   }
 
   getCommentVisible(tab: string | null, commentId: number): boolean {
-    return this.getCommentTabVisible(tab, commentId) && getOrDefault(this.commentDesiredPositions, commentId, 1) > 0;
+    return (
+      this.getCommentTabVisible(tab, commentId) &&
+      getOrDefault(this.commentDesiredPositions, commentId, 1) > 0
+    );
   }
 
   getCommentPosition(commentId: number) {

+ 25 - 17
client/src/components/CommentApp/utils/storybook.tsx

@@ -1,11 +1,7 @@
 import React from 'react';
 
 import { Store } from '../state';
-import {
-  addComment,
-  setFocusedComment,
-  addReply,
-} from '../actions/comments';
+import { addComment, setFocusedComment, addReply } from '../actions/comments';
 import {
   Author,
   Comment,
@@ -35,7 +31,7 @@ export function RenderCommentsForStorybook({
   const layout = new LayoutController();
 
   const commentsToRender: Comment[] = Array.from(
-    state.comments.comments.values()
+    state.comments.comments.values(),
   );
 
   const commentsRendered = commentsToRender.map((comment) => (
@@ -47,7 +43,8 @@ export function RenderCommentsForStorybook({
         author || {
           id: 1,
           name: 'Admin',
-          avatarUrl: 'https://gravatar.com/avatar/e31ec811942afbf7b9ce0ac5affe426f?s=200&d=robohash&r=x',
+          avatarUrl:
+            'https://gravatar.com/avatar/e31ec811942afbf7b9ce0ac5affe426f?s=200&d=robohash&r=x',
         }
       }
       comment={comment}
@@ -57,9 +54,7 @@ export function RenderCommentsForStorybook({
     />
   ));
 
-  return (
-    <ol className="comments-list">{commentsRendered}</ol>
-  );
+  return <ol className="comments-list">{commentsRendered}</ol>;
 }
 
 interface AddTestCommentOptions extends NewCommentOptions {
@@ -69,7 +64,7 @@ interface AddTestCommentOptions extends NewCommentOptions {
 
 export function addTestComment(
   store: Store,
-  options: AddTestCommentOptions
+  options: AddTestCommentOptions,
 ): number {
   const commentId = getNextCommentId();
 
@@ -78,7 +73,8 @@ export function addTestComment(
   const author = options.author || {
     id: 1,
     name: 'Admin',
-    avatarUrl: 'https://gravatar.com/avatar/e31ec811942afbf7b9ce0ac5affe426f?s=200&d=robohash&r=x',
+    avatarUrl:
+      'https://gravatar.com/avatar/e31ec811942afbf7b9ce0ac5affe426f?s=200&d=robohash&r=x',
   };
 
   // We must have a remoteId unless the comment is being created
@@ -93,8 +89,16 @@ export function addTestComment(
 
   store.dispatch(
     addComment(
-      newComment('test', '', commentId, null, author, Date.now(), addCommentOptions)
-    )
+      newComment(
+        'test',
+        '',
+        commentId,
+        null,
+        author,
+        Date.now(),
+        addCommentOptions,
+      ),
+    ),
   );
 
   if (options.focused) {
@@ -112,13 +116,14 @@ interface AddTestReplyOptions extends NewReplyOptions {
 export function addTestReply(
   store: Store,
   commentId: number,
-  options: AddTestReplyOptions
+  options: AddTestReplyOptions,
 ) {
   const addReplyOptions = options;
   const author = options.author || {
     id: 1,
     name: 'Admin',
-    avatarUrl: 'https://gravatar.com/avatar/e31ec811942afbf7b9ce0ac5affe426f?s=200&d=robohash&r=x',
+    avatarUrl:
+      'https://gravatar.com/avatar/e31ec811942afbf7b9ce0ac5affe426f?s=200&d=robohash&r=x',
   };
 
   if (!options.remoteId) {
@@ -126,6 +131,9 @@ export function addTestReply(
   }
 
   store.dispatch(
-    addReply(commentId, newCommentReply(1, author, Date.now(), addReplyOptions))
+    addReply(
+      commentId,
+      newCommentReply(1, author, Date.now(), addReplyOptions),
+    ),
   );
 }

+ 23 - 23
client/src/components/Draftail/CommentableEditor/CommentableEditor.scss

@@ -1,35 +1,35 @@
 .Draftail-Toolbar {
-    display: flex;
-    flex-wrap: wrap;
+  display: flex;
+  flex-wrap: wrap;
 
-    .Draftail-ToolbarGroup:last-child {
-        flex-grow: 1;
-    }
+  .Draftail-ToolbarGroup:last-child {
+    flex-grow: 1;
+  }
 
-    .Draftail-CommentControl {
-        float: right;
-        color: $color-teal;
-    }
+  .Draftail-CommentControl {
+    float: right;
+    color: $color-teal;
+  }
 }
 
 .Draftail-CommentControl .Draftail-ToolbarButton {
-    .icon-comment-large-outline {
-        display: block;
-    }
+  .icon-comment-large-outline {
+    display: block;
+  }
 
-    .icon-comment-large-reversed {
-        display: none;
-    }
+  .icon-comment-large-reversed {
+    display: none;
+  }
 
-    &:hover {
-        border-color: transparent;
+  &:hover {
+    border-color: transparent;
 
-        .icon-comment-large-outline {
-            display: none;
-        }
+    .icon-comment-large-outline {
+      display: none;
+    }
 
-        .icon-comment-large-reversed {
-            display: block;
-        }
+    .icon-comment-large-reversed {
+      display: block;
     }
+  }
 }

+ 15 - 15
client/src/components/Draftail/CommentableEditor/CommentableEditor.test.tsx

@@ -77,7 +77,7 @@ describe('CommentableEditor', () => {
   const contentpath = 'test-contentpath';
   const getComments = (app: CommentApp) =>
     app.utils.selectCommentsForContentPathFactory(contentpath)(
-      app.store.getState()
+      app.store.getState(),
     );
   beforeAll(() => {
     const commentsElement = document.createElement('div');
@@ -113,7 +113,7 @@ describe('CommentableEditor', () => {
     commentApp.setVisible(true);
     const editor = mount(getEditorComponent(commentApp));
     const controls = editor.findWhere(
-      (n) => n.name() === 'ToolbarButton' && n.prop('name') === 'comment'
+      (n) => n.name() === 'ToolbarButton' && n.prop('name') === 'comment',
     );
     expect(controls).toHaveLength(1);
     editor.unmount();
@@ -122,7 +122,7 @@ describe('CommentableEditor', () => {
     commentApp.store.dispatch(updateGlobalSettings({ commentsEnabled: false }));
     const editor = mount(getEditorComponent(commentApp));
     const controls = editor.findWhere(
-      (n) => n.name() === 'ToolbarButton' && n.prop('name') === 'comment'
+      (n) => n.name() === 'ToolbarButton' && n.prop('name') === 'comment',
     );
     expect(controls).toHaveLength(0);
     editor.unmount();
@@ -130,8 +130,8 @@ describe('CommentableEditor', () => {
   it('can update comment positions', () => {
     commentApp.store.dispatch(
       commentApp.actions.addComment(
-        newComment('test-contentpath', 'old_position', 1, null, null, 0, {})
-      )
+        newComment('test-contentpath', 'old_position', 1, null, null, 0, {}),
+      ),
     );
     // Test that a comment with no annotation will not have its position updated
     updateCommentPositions({
@@ -140,7 +140,7 @@ describe('CommentableEditor', () => {
       commentApp: commentApp,
     });
     expect(commentApp.store.getState().comments.comments.get(1)?.position).toBe(
-      'old_position'
+      'old_position',
     );
 
     commentApp.updateAnnotation(new DraftailInlineAnnotation(fieldNode), 1);
@@ -152,7 +152,7 @@ describe('CommentableEditor', () => {
       commentApp: commentApp,
     });
     expect(commentApp.store.getState().comments.comments.get(1)?.position).toBe(
-      '[]'
+      '[]',
     );
 
     // Test that a comment with a style range has that style range recorded accurately in the state
@@ -162,7 +162,7 @@ describe('CommentableEditor', () => {
       commentApp: commentApp,
     });
     expect(commentApp.store.getState().comments.comments.get(1)?.position).toBe(
-      '[{"key":"a","start":0,"end":1}]'
+      '[{"key":"a","start":0,"end":1}]',
     );
   });
   it('can add comments to editor', () => {
@@ -175,9 +175,9 @@ describe('CommentableEditor', () => {
           null,
           null,
           0,
-          {}
-        )
-      )
+          {},
+        ),
+      ),
     );
     // Test that comment styles are correctly added to the editor,
     // and the comments in the state have annotations assigned
@@ -185,7 +185,7 @@ describe('CommentableEditor', () => {
       createEditorStateFromRaw(content).getCurrentContent(),
       getComments(commentApp),
       commentApp,
-      () => new DraftailInlineAnnotation(fieldNode)
+      () => new DraftailInlineAnnotation(fieldNode),
     );
     newContentState.getFirstBlock().findStyleRanges(
       (metadata) => !metadata.getStyle().isEmpty(),
@@ -194,14 +194,14 @@ describe('CommentableEditor', () => {
           newContentState
             .getFirstBlock()
             .getInlineStyleAt(start)
-            .has('COMMENT-1')
+            .has('COMMENT-1'),
         ).toBe(true);
         expect(start).toBe(0);
         expect(end).toBe(1);
-      }
+      },
     );
     expect(
-      commentApp.store.getState().comments.comments.get(1)?.annotation
+      commentApp.store.getState().comments.comments.get(1)?.annotation,
     ).not.toBe(null);
   });
   it('can find the least common comment id', () => {

+ 353 - 276
client/src/components/Draftail/CommentableEditor/CommentableEditor.tsx

@@ -17,11 +17,19 @@ import {
   Modifier,
   RawDraftContentState,
   RichUtils,
-  SelectionState
+  SelectionState,
 } from 'draft-js';
 import type { DraftEditorLeaf } from 'draft-js/lib/DraftEditorLeaf.react';
 import { filterInlineStyles } from 'draftjs-filters';
-import React, { MutableRefObject, ReactNode, ReactText, useEffect, useMemo, useRef, useState } from 'react';
+import React, {
+  MutableRefObject,
+  ReactNode,
+  ReactText,
+  useEffect,
+  useMemo,
+  useRef,
+  useState,
+} from 'react';
 import { useSelector, shallowEqual } from 'react-redux';
 
 import { STRINGS } from '../../../config/wagtailConfig';
@@ -56,10 +64,10 @@ export class DraftailInlineAnnotation implements Annotation {
    * Create an inline annotation
    * @param {Element} field - an element to provide the fallback position for comments without any inline decorators
    */
-  field: Element
-  decoratorRefs: Map<DecoratorRef, BlockKey>
-  focusedBlockKey: BlockKey
-  cachedMedianRef: DecoratorRef | null
+  field: Element;
+  decoratorRefs: Map<DecoratorRef, BlockKey>;
+  focusedBlockKey: BlockKey;
+  cachedMedianRef: DecoratorRef | null;
 
   constructor(field: Element) {
     this.field = field;
@@ -90,7 +98,8 @@ export class DraftailInlineAnnotation implements Annotation {
   }
   static getMedianRef(refArray: Array<DecoratorRef>) {
     const refs = refArray.sort(
-      (firstRef, secondRef) => this.getHeightForRef(firstRef) - this.getHeightForRef(secondRef)
+      (firstRef, secondRef) =>
+        this.getHeightForRef(firstRef) - this.getHeightForRef(secondRef),
     );
     const length = refs.length;
     if (length > 0) {
@@ -111,13 +120,13 @@ export class DraftailInlineAnnotation implements Annotation {
       // if the highlight has somehow been split up
       medianRef = DraftailInlineAnnotation.getMedianRef(
         Array.from(this.decoratorRefs.keys()).filter(
-          (ref) => this.decoratorRefs.get(ref) === this.focusedBlockKey
-        )
+          (ref) => this.decoratorRefs.get(ref) === this.focusedBlockKey,
+        ),
       );
     } else if (!this.cachedMedianRef) {
       // Our cache is empty - try to update it
       medianRef = DraftailInlineAnnotation.getMedianRef(
-        Array.from(this.decoratorRefs.keys())
+        Array.from(this.decoratorRefs.keys()),
       );
       this.cachedMedianRef = medianRef;
     } else {
@@ -130,22 +139,28 @@ export class DraftailInlineAnnotation implements Annotation {
   }
 }
 
-
-function applyInlineStyleToRange({ contentState, style, blockKey, start, end }:
-  {contentState: ContentState,
-    style: string,
-    blockKey: BlockKey,
-    start: number,
-    end: number}
-) {
-  return Modifier.applyInlineStyle(contentState,
+function applyInlineStyleToRange({
+  contentState,
+  style,
+  blockKey,
+  start,
+  end,
+}: {
+  contentState: ContentState;
+  style: string;
+  blockKey: BlockKey;
+  start: number;
+  end: number;
+}) {
+  return Modifier.applyInlineStyle(
+    contentState,
     new SelectionState({
       anchorKey: blockKey,
       anchorOffset: start,
       focusKey: blockKey,
-      focusOffset: end
+      focusOffset: end,
     }),
-    style
+    style,
   );
 }
 
@@ -158,11 +173,16 @@ function getFullSelectionState(contentState: ContentState) {
     anchorKey: contentState.getFirstBlock().getKey(),
     anchorOffset: 0,
     focusKey: lastBlock.getKey(),
-    focusOffset: lastBlock.getLength()
+    focusOffset: lastBlock.getLength(),
   });
 }
 
-function addNewComment(editorState: EditorState, fieldNode: Element, commentApp: CommentApp, contentPath: string) {
+function addNewComment(
+  editorState: EditorState,
+  fieldNode: Element,
+  commentApp: CommentApp,
+  contentPath: string,
+) {
   let state = editorState;
   const annotation = new DraftailInlineAnnotation(fieldNode);
   const commentId = commentApp.makeComment(annotation, contentPath, '[]');
@@ -170,40 +190,47 @@ function addNewComment(editorState: EditorState, fieldNode: Element, commentApp:
   // If the selection is collapsed, add the comment highlight on the whole field
   state = EditorState.acceptSelection(
     editorState,
-    selection.isCollapsed() ? getFullSelectionState(editorState.getCurrentContent()) : selection
+    selection.isCollapsed()
+      ? getFullSelectionState(editorState.getCurrentContent())
+      : selection,
   );
 
-  return (
-    EditorState.acceptSelection(
-      RichUtils.toggleInlineStyle(
-        state,
-        `${COMMENT_STYLE_IDENTIFIER}${commentId}`
-      ),
-      selection
-    )
+  return EditorState.acceptSelection(
+    RichUtils.toggleInlineStyle(
+      state,
+      `${COMMENT_STYLE_IDENTIFIER}${commentId}`,
+    ),
+    selection,
   );
 }
 
 interface ControlProps {
-  getEditorState: () => EditorState,
-  onChange: (editorState: EditorState) => void
+  getEditorState: () => EditorState;
+  onChange: (editorState: EditorState) => void;
 }
 
-function getCommentControl(commentApp: CommentApp, contentPath: string, fieldNode: Element) {
+function getCommentControl(
+  commentApp: CommentApp,
+  contentPath: string,
+  fieldNode: Element,
+) {
   return ({ getEditorState, onChange }: ControlProps) => (
     <span className="Draftail-CommentControl" data-comment-add>
       <ToolbarButton
         name="comment"
         active={false}
-        title={`${STRINGS.ADD_A_COMMENT}\n${IS_MAC_OS ? '⌘ + Alt + M' : 'Ctrl + Alt + M'}`}
+        title={`${STRINGS.ADD_A_COMMENT}\n${
+          IS_MAC_OS ? '⌘ + Alt + M' : 'Ctrl + Alt + M'
+        }`}
         icon={
           <>
-            <Icon name="comment-large-outline" /> <Icon name="comment-large-reversed" />
+            <Icon name="comment-large-outline" />{' '}
+            <Icon name="comment-large-reversed" />
           </>
         }
         onClick={() => {
           onChange(
-            addNewComment(getEditorState(), fieldNode, commentApp, contentPath)
+            addNewComment(getEditorState(), fieldNode, commentApp, contentPath),
           );
         }}
       />
@@ -222,50 +249,59 @@ function getIdForCommentStyle(style: string) {
 function findCommentStyleRanges(
   contentBlock: ContentBlock,
   callback: (start: number, end: number) => void,
-  filterFn?: (metadata: CharacterMetadata) => boolean) {
+  filterFn?: (metadata: CharacterMetadata) => boolean,
+) {
   // Find comment style ranges that do not overlap an existing entity
-  const filterFunction = filterFn || ((metadata: CharacterMetadata) => metadata.getStyle().some(styleIsComment));
+  const filterFunction =
+    filterFn ||
+    ((metadata: CharacterMetadata) => metadata.getStyle().some(styleIsComment));
   const entityRanges: Array<[number, number]> = [];
   contentBlock.findEntityRanges(
-    character => character.getEntity() !== null,
-    (start, end) => entityRanges.push([start, end])
+    (character) => character.getEntity() !== null,
+    (start, end) => entityRanges.push([start, end]),
   );
-  contentBlock.findStyleRanges(
-    filterFunction,
-    (start, end) => {
-      const interferingEntityRanges = entityRanges.filter(value => value[1] > start).filter(value => value[0] < end);
-      let currentPosition = start;
-      interferingEntityRanges.forEach((value) => {
-        const [entityStart, entityEnd] = value;
-        if (entityStart > currentPosition) {
-          callback(currentPosition, entityStart);
-        }
-        currentPosition = entityEnd;
-      });
-      if (currentPosition < end) {
-        callback(start, end);
+  contentBlock.findStyleRanges(filterFunction, (start, end) => {
+    const interferingEntityRanges = entityRanges
+      .filter((value) => value[1] > start)
+      .filter((value) => value[0] < end);
+    let currentPosition = start;
+    interferingEntityRanges.forEach((value) => {
+      const [entityStart, entityEnd] = value;
+      if (entityStart > currentPosition) {
+        callback(currentPosition, entityStart);
       }
+      currentPosition = entityEnd;
+    });
+    if (currentPosition < end) {
+      callback(start, end);
     }
-  );
+  });
 }
 
-
-export function updateCommentPositions({ editorState, comments, commentApp }:
-  {
-    editorState: EditorState,
-    comments: Array<Comment>,
-    commentApp: CommentApp
-  }) {
+export function updateCommentPositions({
+  editorState,
+  comments,
+  commentApp,
+}: {
+  editorState: EditorState;
+  comments: Array<Comment>;
+  commentApp: CommentApp;
+}) {
   // Construct a map of comment id -> array of style ranges
   const commentPositions = new Map();
 
-  editorState.getCurrentContent().getBlocksAsArray().forEach(
-    (block) => {
+  editorState
+    .getCurrentContent()
+    .getBlocksAsArray()
+    .forEach((block) => {
       const key = block.getKey();
-      block.findStyleRanges((metadata) => metadata.getStyle().some(styleIsComment),
+      block.findStyleRanges(
+        (metadata) => metadata.getStyle().some(styleIsComment),
         (start, end) => {
-          block.getInlineStyleAt(start).filter(styleIsComment).forEach(
-            (style) => {
+          block
+            .getInlineStyleAt(start)
+            .filter(styleIsComment)
+            .forEach((style) => {
               // We have already filtered out any undefined styles, so cast here
               const id = getIdForCommentStyle(style as string);
               let existingPosition = commentPositions.get(id);
@@ -275,29 +311,30 @@ export function updateCommentPositions({ editorState, comments, commentApp }:
               existingPosition.push({
                 key: key,
                 start: start,
-                end: end
+                end: end,
               });
               commentPositions.set(id, existingPosition);
-            }
-          );
-        });
-    }
-  );
-
-
-  comments.filter(comment => comment.annotation).forEach((comment) => {
-    // if a comment has an annotation - ie the field has it inserted - update its position
-    const newPosition = commentPositions.get(comment.localId);
-    const serializedNewPosition = newPosition ? JSON.stringify(newPosition) : '[]';
-    if (comment.position !== serializedNewPosition) {
-      commentApp.store.dispatch(
-        commentApp.actions.updateComment(
-          comment.localId,
-          { position: serializedNewPosition }
-        )
+            });
+        },
       );
-    }
-  });
+    });
+
+  comments
+    .filter((comment) => comment.annotation)
+    .forEach((comment) => {
+      // if a comment has an annotation - ie the field has it inserted - update its position
+      const newPosition = commentPositions.get(comment.localId);
+      const serializedNewPosition = newPosition
+        ? JSON.stringify(newPosition)
+        : '[]';
+      if (comment.position !== serializedNewPosition) {
+        commentApp.store.dispatch(
+          commentApp.actions.updateComment(comment.localId, {
+            position: serializedNewPosition,
+          }),
+        );
+      }
+    });
 }
 
 /**
@@ -305,7 +342,9 @@ export function updateCommentPositions({ editorState, comments, commentApp }:
  * has the fewest style ranges within the block, or null if no comment exists at the offset
  */
 export function findLeastCommonCommentId(block: ContentBlock, offset: number) {
-  const styles = block.getInlineStyleAt(offset).filter(styleIsComment) as Immutable.OrderedSet<string>;
+  const styles = block
+    .getInlineStyleAt(offset)
+    .filter(styleIsComment) as Immutable.OrderedSet<string>;
   let styleToUse: string;
   const styleCount = styles.count();
   if (styleCount === 0) {
@@ -322,15 +361,20 @@ export function findLeastCommonCommentId(block: ContentBlock, offset: number) {
     // this casting should be removed
     let styleFreq = styles.map((style) => {
       let counter = 0;
-      findCommentStyleRanges(block,
-        () => { counter = counter + 1; },
-        (metadata) => metadata.getStyle().some(rangeStyle => rangeStyle === style)
+      findCommentStyleRanges(
+        block,
+        () => {
+          counter = counter + 1;
+        },
+        (metadata) =>
+          metadata.getStyle().some((rangeStyle) => rangeStyle === style),
       );
       return [style, counter];
     }) as unknown as Immutable.OrderedSet<[string, number]>;
 
-    styleFreq =  styleFreq.sort(
-      (firstStyleCount, secondStyleCount) => firstStyleCount[1] - secondStyleCount[1]
+    styleFreq = styleFreq.sort(
+      (firstStyleCount, secondStyleCount) =>
+        firstStyleCount[1] - secondStyleCount[1],
     ) as Immutable.OrderedSet<[string, number]>;
 
     styleToUse = styleFreq.first()[0];
@@ -341,8 +385,8 @@ export function findLeastCommonCommentId(block: ContentBlock, offset: number) {
 }
 
 interface DecoratorProps {
-  contentState: ContentState,
-  children?: Array<DraftEditorLeaf>
+  contentState: ContentState;
+  children?: Array<DraftEditorLeaf>;
 }
 
 function getCommentDecorator(commentApp: CommentApp) {
@@ -358,11 +402,10 @@ function getCommentDecorator(commentApp: CommentApp) {
     const blockKey: BlockKey = children[0].props.block.getKey();
     const start: number = children[0].props.start;
 
-    const commentId = useMemo(
-      () => {
-        const block = contentState.getBlockForKey(blockKey);
-        return findLeastCommonCommentId(block, start);
-      }, [blockKey, start]);
+    const commentId = useMemo(() => {
+      const block = contentState.getBlockForKey(blockKey);
+      return findLeastCommonCommentId(block, start);
+    }, [blockKey, start]);
     const annotationNode = useRef(null);
     useEffect(() => {
       // Add a ref to the annotation, allowing the comment to float alongside the attached text.
@@ -379,9 +422,7 @@ function getCommentDecorator(commentApp: CommentApp) {
     }, [commentId, annotationNode, blockKey]);
 
     if (!enabled) {
-      return <>
-        {children}
-      </>;
+      return <>{children}</>;
     }
 
     const onClick = () => {
@@ -390,7 +431,11 @@ function getCommentDecorator(commentApp: CommentApp) {
         return;
       }
       const annotation = commentApp.layout.commentAnnotations.get(commentId);
-      if (annotation && annotation instanceof DraftailInlineAnnotation  && annotationNode) {
+      if (
+        annotation &&
+        annotation instanceof DraftailInlineAnnotation &&
+        annotationNode
+      ) {
         annotation.setFocusedBlockKey(blockKey);
       }
 
@@ -398,8 +443,8 @@ function getCommentDecorator(commentApp: CommentApp) {
       commentApp.store.dispatch(
         commentApp.actions.setFocusedComment(commentId, {
           updatePinnedComment: true,
-          forceFocus: false
-        })
+          forceFocus: false,
+        }),
       );
     };
     return (
@@ -417,7 +462,10 @@ function getCommentDecorator(commentApp: CommentApp) {
   return CommentDecorator;
 }
 
-function forceResetEditorState(editorState: EditorState, replacementContent?: ContentState) {
+function forceResetEditorState(
+  editorState: EditorState,
+  replacementContent?: ContentState,
+) {
   const content = replacementContent || editorState.getCurrentContent();
   const state = EditorState.set(
     EditorState.createWithContent(content, editorState.getDecorator()),
@@ -425,8 +473,8 @@ function forceResetEditorState(editorState: EditorState, replacementContent?: Co
       selection: editorState.getSelection(),
       undoStack: editorState.getUndoStack(),
       redoStack: editorState.getRedoStack(),
-      inlineStyleOverride: editorState.getInlineStyleOverride()
-    }
+      inlineStyleOverride: editorState.getInlineStyleOverride(),
+    },
   );
   return EditorState.acceptSelection(state, state.getSelection());
 }
@@ -435,39 +483,43 @@ export function addCommentsToEditor(
   contentState: ContentState,
   comments: Comment[],
   commentApp: CommentApp,
-  getAnnotation: () => Annotation
+  getAnnotation: () => Annotation,
 ) {
   let newContentState = contentState;
-  comments.filter(comment => !comment.annotation).forEach((comment) => {
-    commentApp.updateAnnotation(getAnnotation(), comment.localId);
-    const style = `${COMMENT_STYLE_IDENTIFIER}${comment.localId}`;
-    try {
-      const positions = JSON.parse(comment.position);
-      positions.forEach((position) => {
-        newContentState = applyInlineStyleToRange({
-          contentState: newContentState,
-          blockKey: position.key,
-          start: position.start,
-          end: position.end,
-          style
+  comments
+    .filter((comment) => !comment.annotation)
+    .forEach((comment) => {
+      commentApp.updateAnnotation(getAnnotation(), comment.localId);
+      const style = `${COMMENT_STYLE_IDENTIFIER}${comment.localId}`;
+      try {
+        const positions = JSON.parse(comment.position);
+        positions.forEach((position) => {
+          newContentState = applyInlineStyleToRange({
+            contentState: newContentState,
+            blockKey: position.key,
+            start: position.start,
+            end: position.end,
+            style,
+          });
         });
-      });
-    } catch (err) {
-      /* eslint-disable no-console */
-      console.error(`Error loading comment position for comment ${comment.localId}`);
-      console.error(err);
-      /* esline-enable no-console */
-    }
-  });
+      } catch (err) {
+        /* eslint-disable no-console */
+        console.error(
+          `Error loading comment position for comment ${comment.localId}`,
+        );
+        console.error(err);
+        /* esline-enable no-console */
+      }
+    });
   return newContentState;
 }
 
-type Direction = 'RTL' | 'LTR'
+type Direction = 'RTL' | 'LTR';
 
 function handleArrowAtContentEnd(
   state: EditorState,
   setEditorState: (newState: EditorState) => void,
-  direction: Direction
+  direction: Direction,
 ) {
   // If at the end of content and pressing in the same direction as the text, remove the comment style from
   // further typing
@@ -476,49 +528,53 @@ function handleArrowAtContentEnd(
   const lastBlock = newState.getCurrentContent().getLastBlock();
   const textDirection = newState.getDirectionMap().get(lastBlock.getKey());
 
-  if (!(
-    textDirection === direction
-    && selection.isCollapsed()
-    && selection.getAnchorKey() === lastBlock.getKey()
-    && selection.getAnchorOffset() === lastBlock.getLength()
-  )) {
+  if (
+    !(
+      textDirection === direction &&
+      selection.isCollapsed() &&
+      selection.getAnchorKey() === lastBlock.getKey() &&
+      selection.getAnchorOffset() === lastBlock.getLength()
+    )
+  ) {
     return;
   }
   setEditorState(
     EditorState.setInlineStyleOverride(
       newState,
-      newState.getCurrentInlineStyle().filter(style => !styleIsComment(style)) as DraftInlineStyle
-    )
+      newState
+        .getCurrentInlineStyle()
+        .filter((style) => !styleIsComment(style)) as DraftInlineStyle,
+    ),
   );
 }
 
 interface InlineStyle {
-  label?: string,
-  description?: string,
-  icon?: string | string[] | Node,
-  type: string,
-  style?: Record<string, string | number | ReactText | undefined >
+  label?: string;
+  description?: string;
+  icon?: string | string[] | Node;
+  type: string;
+  style?: Record<string, string | number | ReactText | undefined>;
 }
 
 interface ColorConfigProp {
-  standardHighlight: string,
-  overlappingHighlight: string,
-  focusedHighlight: string
+  standardHighlight: string;
+  overlappingHighlight: string;
+  focusedHighlight: string;
 }
 
 interface CommentableEditorProps {
-  commentApp: CommentApp,
-  fieldNode: Element,
-  contentPath: string,
-  rawContentState: RawDraftContentState,
-  onSave: (rawContent: RawDraftContentState) => void,
-  inlineStyles: Array<InlineStyle>,
-  editorRef: (editor: ReactNode) => void
-  colorConfig: ColorConfigProp
-  isCommentShortcut: (e: React.KeyboardEvent) => boolean
+  commentApp: CommentApp;
+  fieldNode: Element;
+  contentPath: string;
+  rawContentState: RawDraftContentState;
+  onSave: (rawContent: RawDraftContentState) => void;
+  inlineStyles: Array<InlineStyle>;
+  editorRef: (editor: ReactNode) => void;
+  colorConfig: ColorConfigProp;
+  isCommentShortcut: (e: React.KeyboardEvent) => boolean;
   // Unfortunately the EditorPlugin type isn't exported in our version of 'draft-js-plugins-editor'
-  plugins?: Record<string, unknown>[]
-  controls?: Array<(props: ControlProps) => JSX.Element>
+  plugins?: Record<string, unknown>[];
+  controls?: Array<(props: ControlProps) => JSX.Element>;
 }
 
 function CommentableEditor({
@@ -536,33 +592,35 @@ function CommentableEditor({
   ...options
 }: CommentableEditorProps) {
   const [editorState, setEditorState] = useState(() =>
-    createEditorStateFromRaw(rawContentState)
+    createEditorStateFromRaw(rawContentState),
   );
   const CommentControl = useMemo(
     () => getCommentControl(commentApp, contentPath, fieldNode),
-    [commentApp, contentPath, fieldNode]
+    [commentApp, contentPath, fieldNode],
   );
   const commentsSelector = useMemo(
     () => commentApp.utils.selectCommentsForContentPathFactory(contentPath),
-    [contentPath, commentApp]
+    [contentPath, commentApp],
+  );
+  const CommentDecorator = useMemo(
+    () => getCommentDecorator(commentApp),
+    [commentApp],
   );
-  const CommentDecorator = useMemo(() => getCommentDecorator(commentApp), [
-    commentApp,
-  ]);
   const comments = useSelector(commentsSelector, shallowEqual);
   const enabled = useSelector(commentApp.selectors.selectEnabled);
   const focusedId = useSelector(commentApp.selectors.selectFocused);
 
-  const ids = useMemo(() => comments.map((comment) => comment.localId), [
-    comments,
-  ]);
+  const ids = useMemo(
+    () => comments.map((comment) => comment.localId),
+    [comments],
+  );
 
   const commentStyles: Array<InlineStyle> = useMemo(
     () =>
       ids.map((id) => ({
-        type: `${COMMENT_STYLE_IDENTIFIER}${id}`
+        type: `${COMMENT_STYLE_IDENTIFIER}${id}`,
       })),
-    [ids]
+    [ids],
   );
 
   const [uniqueStyleId, setUniqueStyleId] = useState(0);
@@ -574,16 +632,17 @@ function CommentableEditor({
     // Only trigger a focus-related rerender if the current focused comment is inside the field, or the previous one was
     const validFocusChange =
       previousFocused !== focusedId &&
-      ((previousFocused && previousIds && previousIds.includes(previousFocused)) ||
-        focusedId && ids.includes(focusedId));
+      ((previousFocused &&
+        previousIds &&
+        previousIds.includes(previousFocused)) ||
+        (focusedId && ids.includes(focusedId)));
 
     if (
       !validFocusChange &&
       previousEnabled === enabled &&
-      (
-        previousIds === ids ||
-        (previousIds.length === ids.length && previousIds.every((value, index) => value === ids[index]))
-      )
+      (previousIds === ids ||
+        (previousIds.length === ids.length &&
+          previousIds.every((value, index) => value === ids[index])))
     ) {
       return;
     }
@@ -593,7 +652,7 @@ function CommentableEditor({
       inlineStyles
         .map((style) => style.type)
         .concat(ids.map((id) => `${COMMENT_STYLE_IDENTIFIER}${id}`)),
-      editorState.getCurrentContent()
+      editorState.getCurrentContent(),
     );
     // Force reset the editor state to ensure redecoration, and apply a new (blank) inline style to force
     // inline style rerender. This must be entirely new for the rerender to trigger, hence the unique
@@ -606,9 +665,9 @@ function CommentableEditor({
         Modifier.applyInlineStyle(
           filteredContent,
           getFullSelectionState(filteredContent),
-          `STYLE_RERENDER_${uniqueStyleId}`
-        )
-      )
+          `STYLE_RERENDER_${uniqueStyleId}`,
+        ),
+      ),
     );
     setUniqueStyleId((id) => (id + 1) % 200);
   }, [focusedId, enabled, inlineStyles, ids, editorState]);
@@ -617,7 +676,10 @@ function CommentableEditor({
     // if there are any comments without annotations, we need to add them to the EditorState
     const contentState = editorState.getCurrentContent();
     const newContentState = addCommentsToEditor(
-      contentState, comments, commentApp, () => new DraftailInlineAnnotation(fieldNode)
+      contentState,
+      comments,
+      commentApp,
+      () => new DraftailInlineAnnotation(fieldNode),
     );
     if (contentState !== newContentState) {
       setEditorState(forceResetEditorState(editorState, newContentState));
@@ -633,19 +695,16 @@ function CommentableEditor({
       editorState,
       filterInlineStyles(
         inlineStyles.map((style) => style.type),
-        editorState.getCurrentContent()
+        editorState.getCurrentContent(),
       ),
-      'change-inline-style'
-    );
-    timeoutRef.current = window.setTimeout(
-      () => {
-        onSave(serialiseEditorStateToRaw(filteredEditorState));
-
-        // Next, update comment positions in the redux store
-        updateCommentPositions({ editorState, comments, commentApp });
-      },
-      250
+      'change-inline-style',
     );
+    timeoutRef.current = window.setTimeout(() => {
+      onSave(serialiseEditorStateToRaw(filteredEditorState));
+
+      // Next, update comment positions in the redux store
+      updateCommentPositions({ editorState, comments, commentApp });
+    }, 250);
     return () => {
       window.clearTimeout(timeoutRef.current);
     };
@@ -659,21 +718,27 @@ function CommentableEditor({
         if (['undo', 'redo'].includes(state.getLastChangeType())) {
           const filteredContent = filterInlineStyles(
             inlineStyles
-              .map(style => style.type)
-              .concat(ids.map(id => `${COMMENT_STYLE_IDENTIFIER}${id}`)),
-            state.getCurrentContent()
+              .map((style) => style.type)
+              .concat(ids.map((id) => `${COMMENT_STYLE_IDENTIFIER}${id}`)),
+            state.getCurrentContent(),
           );
           newEditorState = forceResetEditorState(state, filteredContent);
         } else if (state.getLastChangeType() === 'split-block') {
           const content = newEditorState.getCurrentContent();
           const selection = newEditorState.getSelection();
-          const style = content.getBlockForKey(selection.getAnchorKey()).getInlineStyleAt(selection.getAnchorOffset());
+          const style = content
+            .getBlockForKey(selection.getAnchorKey())
+            .getInlineStyleAt(selection.getAnchorOffset());
           // If starting a new paragraph (and not splitting an existing comment)
           // ensure any new text entered doesn't get a comment style
-          if (!style.some(styleName => styleIsComment(styleName))) {
+          if (!style.some((styleName) => styleIsComment(styleName))) {
             newEditorState = EditorState.setInlineStyleOverride(
               newEditorState,
-              newEditorState.getCurrentInlineStyle().filter(styleName => !styleIsComment(styleName)) as DraftInlineStyle
+              newEditorState
+                .getCurrentInlineStyle()
+                .filter(
+                  (styleName) => !styleIsComment(styleName),
+                ) as DraftInlineStyle,
             );
           }
         }
@@ -682,89 +747,101 @@ function CommentableEditor({
       editorState={editorState}
       controls={enabled ? controls.concat([CommentControl]) : controls}
       inlineStyles={inlineStyles.concat(commentStyles)}
-      plugins={plugins.concat([{
-        decorators: [{
-          strategy: (
-            block: ContentBlock, callback: (start: number, end: number) => void
-          ) => findCommentStyleRanges(block, callback),
-          component: CommentDecorator,
-        }],
-        keyBindingFn: (e: React.KeyboardEvent) => {
-          if (isCommentShortcut(e)) {
-            return 'comment';
-          }
-          return undefined;
-        },
-        onRightArrow: (_: React.KeyboardEvent, { getEditorState }) => {
-          // In later versions of draft-js, this is deprecated and can be handled via handleKeyCommand instead
-          // when draftail upgrades, this logic can be moved there
-
-          handleArrowAtContentEnd(getEditorState(), setEditorState, 'LTR');
-        },
-        onLeftArrow: (_: React.KeyboardEvent, { getEditorState }) => {
-          // In later versions of draft-js, this is deprecated and can be handled via handleKeyCommand instead
-          // when draftail upgrades, this logic can be moved there
-
-          handleArrowAtContentEnd(getEditorState(), setEditorState, 'RTL');
-        },
-        handleKeyCommand: (command: string, state: EditorState) => {
-          if (enabled && command === 'comment') {
-            const selection = state.getSelection();
-            const content = state.getCurrentContent();
-            if (selection.isCollapsed()) {
-              // We might be trying to focus an existing comment - check if we're in a comment range
-              const id = findLeastCommonCommentId(
-                content.getBlockForKey(selection.getAnchorKey()),
-                selection.getAnchorOffset()
-              );
-              if (id) {
-                // Focus the comment
-                commentApp.store.dispatch(
-                  commentApp.actions.setFocusedComment(id, { updatePinnedComment: true, forceFocus: true })
+      plugins={plugins.concat([
+        {
+          decorators: [
+            {
+              strategy: (
+                block: ContentBlock,
+                callback: (start: number, end: number) => void,
+              ) => findCommentStyleRanges(block, callback),
+              component: CommentDecorator,
+            },
+          ],
+          keyBindingFn: (e: React.KeyboardEvent) => {
+            if (isCommentShortcut(e)) {
+              return 'comment';
+            }
+            return undefined;
+          },
+          onRightArrow: (_: React.KeyboardEvent, { getEditorState }) => {
+            // In later versions of draft-js, this is deprecated and can be handled via handleKeyCommand instead
+            // when draftail upgrades, this logic can be moved there
+
+            handleArrowAtContentEnd(getEditorState(), setEditorState, 'LTR');
+          },
+          onLeftArrow: (_: React.KeyboardEvent, { getEditorState }) => {
+            // In later versions of draft-js, this is deprecated and can be handled via handleKeyCommand instead
+            // when draftail upgrades, this logic can be moved there
+
+            handleArrowAtContentEnd(getEditorState(), setEditorState, 'RTL');
+          },
+          handleKeyCommand: (command: string, state: EditorState) => {
+            if (enabled && command === 'comment') {
+              const selection = state.getSelection();
+              const content = state.getCurrentContent();
+              if (selection.isCollapsed()) {
+                // We might be trying to focus an existing comment - check if we're in a comment range
+                const id = findLeastCommonCommentId(
+                  content.getBlockForKey(selection.getAnchorKey()),
+                  selection.getAnchorOffset(),
                 );
-                return 'handled';
+                if (id) {
+                  // Focus the comment
+                  commentApp.store.dispatch(
+                    commentApp.actions.setFocusedComment(id, {
+                      updatePinnedComment: true,
+                      forceFocus: true,
+                    }),
+                  );
+                  return 'handled';
+                }
               }
+              // Otherwise, add a new comment
+              setEditorState(
+                addNewComment(state, fieldNode, commentApp, contentPath),
+              );
+              return 'handled';
             }
-            // Otherwise, add a new comment
-            setEditorState(addNewComment(state, fieldNode, commentApp, contentPath));
-            return 'handled';
-          }
-          return 'not-handled';
-        },
-        customStyleFn: (styleSet: DraftInlineStyle) => {
-          if (!enabled) {
-            return undefined;
-          }
-          // Use of casting in this function is due to issue #1563 in immutable-js, which causes operations like
-          // map and filter to lose type information on the results. It should be fixed in v4: when we upgrade,
-          // this casting should be removed
-          const localCommentStyles = styleSet.filter(styleIsComment) as Immutable.OrderedSet<string>;
-          const numStyles = localCommentStyles.count();
-          if (numStyles > 0) {
-            // There is at least one comment in the range
-            const commentIds = localCommentStyles.map(
-              style => getIdForCommentStyle(style as string)
-            ) as unknown as Immutable.OrderedSet<number>;
-            let background = standardHighlight;
-            if (focusedId && commentIds.has(focusedId)) {
-              // Use the focused colour if one of the comments is focused
-              background = focusedHighlight;
+            return 'not-handled';
+          },
+          customStyleFn: (styleSet: DraftInlineStyle) => {
+            if (!enabled) {
+              return undefined;
+            }
+            // Use of casting in this function is due to issue #1563 in immutable-js, which causes operations like
+            // map and filter to lose type information on the results. It should be fixed in v4: when we upgrade,
+            // this casting should be removed
+            const localCommentStyles = styleSet.filter(
+              styleIsComment,
+            ) as Immutable.OrderedSet<string>;
+            const numStyles = localCommentStyles.count();
+            if (numStyles > 0) {
+              // There is at least one comment in the range
+              const commentIds = localCommentStyles.map((style) =>
+                getIdForCommentStyle(style as string),
+              ) as unknown as Immutable.OrderedSet<number>;
+              let background = standardHighlight;
+              if (focusedId && commentIds.has(focusedId)) {
+                // Use the focused colour if one of the comments is focused
+                background = focusedHighlight;
+                return {
+                  'background-color': background,
+                  'color': standardHighlight,
+                };
+              } else if (numStyles > 1) {
+                // Otherwise if we're in a region with overlapping comments, use a slightly darker colour than usual
+                // to indicate that
+                background = overlappingHighlight;
+              }
               return {
                 'background-color': background,
-                'color': standardHighlight
               };
-            } else if (numStyles > 1) {
-              // Otherwise if we're in a region with overlapping comments, use a slightly darker colour than usual
-              // to indicate that
-              background = overlappingHighlight;
             }
-            return {
-              'background-color': background
-            };
-          }
-          return undefined;
-        }
-      }])}
+            return undefined;
+          },
+        },
+      ])}
       {...options}
     />
   );

+ 11 - 9
client/src/components/Draftail/DraftUtils.js

@@ -1,15 +1,14 @@
-
 /**
-* Returns collection of currently selected blocks.
-* See https://github.com/jpuri/draftjs-utils/blob/e81c0ae19c3b0fdef7e0c1b70d924398956be126/js/block.js#L19.
-*/
+ * Returns collection of currently selected blocks.
+ * See https://github.com/jpuri/draftjs-utils/blob/e81c0ae19c3b0fdef7e0c1b70d924398956be126/js/block.js#L19.
+ */
 const getSelectedBlocksList = (editorState) => {
   const selectionState = editorState.getSelection();
   const content = editorState.getCurrentContent();
   const startKey = selectionState.getStartKey();
   const endKey = selectionState.getEndKey();
   const blockMap = content.getBlockMap();
-  const blocks =  blockMap
+  const blocks = blockMap
     .toSeq()
     .skipUntil((_, k) => k === startKey)
     .takeUntil((_, k) => k === endKey)
@@ -18,9 +17,9 @@ const getSelectedBlocksList = (editorState) => {
 };
 
 /**
-* Returns the currently selected text in the editor.
-* See https://github.com/jpuri/draftjs-utils/blob/e81c0ae19c3b0fdef7e0c1b70d924398956be126/js/block.js#L106.
-*/
+ * Returns the currently selected text in the editor.
+ * See https://github.com/jpuri/draftjs-utils/blob/e81c0ae19c3b0fdef7e0c1b70d924398956be126/js/block.js#L106.
+ */
 export const getSelectionText = (editorState) => {
   const selection = editorState.getSelection();
   let start = selection.getAnchorOffset();
@@ -36,7 +35,10 @@ export const getSelectionText = (editorState) => {
   let selectedText = '';
   for (let i = 0; i < selectedBlocks.size; i += 1) {
     const blockStart = i === 0 ? start : 0;
-    const blockEnd = i === (selectedBlocks.size - 1) ? end : selectedBlocks.get(i).getText().length;
+    const blockEnd =
+      i === selectedBlocks.size - 1
+        ? end
+        : selectedBlocks.get(i).getText().length;
     selectedText += selectedBlocks.get(i).getText().slice(blockStart, blockEnd);
   }
 

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