Przeglądaj źródła

Merge pull request #16971 from overleaf/td-bootstrap-5-base

Bootstrap 5 base

GitOrigin-RevId: 2ac614298d31a9cf65430a46d726648a742044f9
Tim Down 2 lat temu
rodzic
commit
07e16838b8
25 zmienionych plików z 1543 dodań i 24 usunięć
  1. 510 20
      package-lock.json
  2. 3 0
      package.json
  3. 12 2
      services/web/.storybook/preview.tsx
  4. 14 1
      services/web/app/src/infrastructure/ExpressLocals.js
  5. 1 0
      services/web/app/views/layout-base.pug
  6. 32 0
      services/web/frontend/js/features/ui/components/bootstrap-5/button.tsx
  7. 15 0
      services/web/frontend/js/features/ui/components/types/button-props.ts
  8. 21 0
      services/web/frontend/stories/ui/button.stories.tsx
  9. 1 0
      services/web/frontend/stylesheets/bootstrap-5/css-variables/themes/ieee.css
  10. 1 0
      services/web/frontend/stylesheets/bootstrap-5/css-variables/themes/light.css
  11. 12 0
      services/web/frontend/stylesheets/bootstrap-5/foundations/border-radius.scss
  12. 500 0
      services/web/frontend/stylesheets/bootstrap-5/foundations/colours.scss
  13. 38 0
      services/web/frontend/stylesheets/bootstrap-5/foundations/spacing.scss
  14. 104 0
      services/web/frontend/stylesheets/bootstrap-5/foundations/typography.scss
  15. 7 0
      services/web/frontend/stylesheets/bootstrap-5/main-ieee-style.scss
  16. 4 0
      services/web/frontend/stylesheets/bootstrap-5/main-light-style.scss
  17. 34 0
      services/web/frontend/stylesheets/bootstrap-5/main-style.scss
  18. 3 0
      services/web/frontend/stylesheets/bootstrap-5/scss/bootstrap-rule-overrides.scss
  19. 124 0
      services/web/frontend/stylesheets/bootstrap-5/scss/bootstrap-rule-overrides/buttons.scss
  20. 8 0
      services/web/frontend/stylesheets/bootstrap-5/scss/bootstrap-variable-overrides.scss
  21. 9 0
      services/web/frontend/stylesheets/bootstrap-5/scss/bootstrap-variable-overrides/buttons.scss
  22. 11 0
      services/web/frontend/stylesheets/bootstrap-5/scss/bootstrap-variable-overrides/fonts.scss
  23. 30 0
      services/web/frontend/stylesheets/bootstrap-5/scss/bootstrap.scss
  24. 3 1
      services/web/package.json
  25. 46 0
      services/web/webpack.config.js

+ 510 - 20
package-lock.json

@@ -50,6 +50,9 @@
         "eslint-plugin-prettier": "^4.0.0",
         "eslint-plugin-prettier": "^4.0.0",
         "eslint-plugin-promise": "^6.0.0",
         "eslint-plugin-promise": "^6.0.0",
         "prettier": "2.5.1",
         "prettier": "2.5.1",
+        "resolve-url-loader": "^5.0.0",
+        "sass": "^1.69.5",
+        "sass-loader": "^13.3.2",
         "typescript": "^5.0.4"
         "typescript": "^5.0.4"
       }
       }
     },
     },
@@ -9023,9 +9026,9 @@
       }
       }
     },
     },
     "node_modules/@popperjs/core": {
     "node_modules/@popperjs/core": {
-      "version": "2.11.6",
-      "resolved": "https://registry.npmjs.org/@popperjs/core/-/core-2.11.6.tgz",
-      "integrity": "sha512-50/17A98tWUfQ176raKiOGXuYpLyyVMkxxG6oylzL3BPOlA6ADGdK7EYunSa4I064xerltq9TGXs8HmOk5E+vw==",
+      "version": "2.11.8",
+      "resolved": "https://registry.npmjs.org/@popperjs/core/-/core-2.11.8.tgz",
+      "integrity": "sha512-P1st0aksCrn9sGZhp8GMYwBnQsbvAWsZAX44oXNNvLHGqAOcoVxmjZiohstwQ7SqKnbR47akdNi+uleWD8+g6A==",
       "dev": true,
       "dev": true,
       "funding": {
       "funding": {
         "type": "opencollective",
         "type": "opencollective",
@@ -9808,6 +9811,21 @@
         "react-dom": "^16.8.0 || 17.x"
         "react-dom": "^16.8.0 || 17.x"
       }
       }
     },
     },
+    "node_modules/@react-aria/ssr": {
+      "version": "3.9.1",
+      "resolved": "https://registry.npmjs.org/@react-aria/ssr/-/ssr-3.9.1.tgz",
+      "integrity": "sha512-NqzkLFP8ZVI4GSorS0AYljC13QW2sc8bDqJOkBvkAt3M8gbcAXJWVRGtZBCRscki9RZF+rNlnPdg0G0jYkhJcg==",
+      "dev": true,
+      "dependencies": {
+        "@swc/helpers": "^0.5.0"
+      },
+      "engines": {
+        "node": ">= 12"
+      },
+      "peerDependencies": {
+        "react": "^16.8.0 || ^17.0.0-rc.1 || ^18.0.0"
+      }
+    },
     "node_modules/@react-dnd/asap": {
     "node_modules/@react-dnd/asap": {
       "version": "5.0.2",
       "version": "5.0.2",
       "resolved": "https://registry.npmjs.org/@react-dnd/asap/-/asap-5.0.2.tgz",
       "resolved": "https://registry.npmjs.org/@react-dnd/asap/-/asap-5.0.2.tgz",
@@ -9866,6 +9884,67 @@
         "@codemirror/view": "^6.0.3"
         "@codemirror/view": "^6.0.3"
       }
       }
     },
     },
+    "node_modules/@restart/hooks": {
+      "version": "0.4.16",
+      "resolved": "https://registry.npmjs.org/@restart/hooks/-/hooks-0.4.16.tgz",
+      "integrity": "sha512-f7aCv7c+nU/3mF7NWLtVVr0Ra80RqsO89hO72r+Y/nvQr5+q0UFGkocElTH6MJApvReVh6JHUFYn2cw1WdHF3w==",
+      "dev": true,
+      "dependencies": {
+        "dequal": "^2.0.3"
+      },
+      "peerDependencies": {
+        "react": ">=16.8.0"
+      }
+    },
+    "node_modules/@restart/ui": {
+      "version": "1.6.6",
+      "resolved": "https://registry.npmjs.org/@restart/ui/-/ui-1.6.6.tgz",
+      "integrity": "sha512-eC3puKuWE1SRYbojWHXnvCNHGgf3uzHCb6JOhnF4OXPibOIPEkR1sqDSkL643ydigxwh+ruCa1CmYHlzk7ikKA==",
+      "dev": true,
+      "dependencies": {
+        "@babel/runtime": "^7.21.0",
+        "@popperjs/core": "^2.11.6",
+        "@react-aria/ssr": "^3.5.0",
+        "@restart/hooks": "^0.4.9",
+        "@types/warning": "^3.0.0",
+        "dequal": "^2.0.3",
+        "dom-helpers": "^5.2.0",
+        "uncontrollable": "^8.0.1",
+        "warning": "^4.0.3"
+      },
+      "peerDependencies": {
+        "react": ">=16.14.0",
+        "react-dom": ">=16.14.0"
+      }
+    },
+    "node_modules/@restart/ui/node_modules/dom-helpers": {
+      "version": "5.2.1",
+      "resolved": "https://registry.npmjs.org/dom-helpers/-/dom-helpers-5.2.1.tgz",
+      "integrity": "sha512-nRCa7CK3VTrM2NmGkIy4cbK7IZlgBE/PYMn55rrXefr5xXDP0LdtfPnblFDoVdcAfslJ7or6iqAUnx0CCGIWQA==",
+      "dev": true,
+      "dependencies": {
+        "@babel/runtime": "^7.8.7",
+        "csstype": "^3.0.2"
+      }
+    },
+    "node_modules/@restart/ui/node_modules/uncontrollable": {
+      "version": "8.0.4",
+      "resolved": "https://registry.npmjs.org/uncontrollable/-/uncontrollable-8.0.4.tgz",
+      "integrity": "sha512-ulRWYWHvscPFc0QQXvyJjY6LIXU56f0h8pQFvhxiKk5V1fcI8gp9Ht9leVAhrVjzqMw0BgjspBINx9r6oyJUvQ==",
+      "dev": true,
+      "peerDependencies": {
+        "react": ">=16.14.0"
+      }
+    },
+    "node_modules/@restart/ui/node_modules/warning": {
+      "version": "4.0.3",
+      "resolved": "https://registry.npmjs.org/warning/-/warning-4.0.3.tgz",
+      "integrity": "sha512-rpJyN222KWIvHJ/F53XSZv0Zl/accqHR8et1kpaMTD/fLCRxtV8iX8czMzY7sVZupTI3zcUTg8eycS2kNF9l6w==",
+      "dev": true,
+      "dependencies": {
+        "loose-envify": "^1.0.0"
+      }
+    },
     "node_modules/@sentry/browser": {
     "node_modules/@sentry/browser": {
       "version": "7.8.1",
       "version": "7.8.1",
       "resolved": "https://registry.npmjs.org/@sentry/browser/-/browser-7.8.1.tgz",
       "resolved": "https://registry.npmjs.org/@sentry/browser/-/browser-7.8.1.tgz",
@@ -15042,6 +15121,15 @@
       "integrity": "sha512-9F4ys4C74eSTEUNndnER3VJ15oru2NumfQxS8geE+f3eB5xvfxpWyqE5XlVnxb/R14uoXi6SLbBwwiDSkv+XEw==",
       "integrity": "sha512-9F4ys4C74eSTEUNndnER3VJ15oru2NumfQxS8geE+f3eB5xvfxpWyqE5XlVnxb/R14uoXi6SLbBwwiDSkv+XEw==",
       "dev": true
       "dev": true
     },
     },
+    "node_modules/@swc/helpers": {
+      "version": "0.5.6",
+      "resolved": "https://registry.npmjs.org/@swc/helpers/-/helpers-0.5.6.tgz",
+      "integrity": "sha512-aYX01Ke9hunpoCexYAgQucEpARGQ5w/cqHFrIR+e9gdKb1QWTsVJuTJ2ozQzIAxLyRQe/m+2RqzkyOOGiMKRQA==",
+      "dev": true,
+      "dependencies": {
+        "tslib": "^2.4.0"
+      }
+    },
     "node_modules/@swc/types": {
     "node_modules/@swc/types": {
       "version": "0.1.5",
       "version": "0.1.5",
       "resolved": "https://registry.npmjs.org/@swc/types/-/types-0.1.5.tgz",
       "resolved": "https://registry.npmjs.org/@swc/types/-/types-0.1.5.tgz",
@@ -15380,9 +15468,9 @@
       }
       }
     },
     },
     "node_modules/@types/bootstrap": {
     "node_modules/@types/bootstrap": {
-      "version": "5.2.6",
-      "resolved": "https://registry.npmjs.org/@types/bootstrap/-/bootstrap-5.2.6.tgz",
-      "integrity": "sha512-BlAc3YATdasbHoxMoBWODrSF6qwQO/E9X8wVxCCSa6rWjnaZfpkr2N6pUMCY6jj2+wf0muUtLySbvU9etX6YqA==",
+      "version": "5.2.10",
+      "resolved": "https://registry.npmjs.org/@types/bootstrap/-/bootstrap-5.2.10.tgz",
+      "integrity": "sha512-F2X+cd6551tep0MvVZ6nM8v7XgGN/twpdNDjqS1TUM7YFNEtQYWk+dKAnH+T1gr6QgCoGMPl487xw/9hXooa2g==",
       "dev": true,
       "dev": true,
       "dependencies": {
       "dependencies": {
         "@popperjs/core": "^2.9.2"
         "@popperjs/core": "^2.9.2"
@@ -15982,6 +16070,15 @@
         "@types/react": "*"
         "@types/react": "*"
       }
       }
     },
     },
+    "node_modules/@types/react-transition-group": {
+      "version": "4.4.10",
+      "resolved": "https://registry.npmjs.org/@types/react-transition-group/-/react-transition-group-4.4.10.tgz",
+      "integrity": "sha512-hT/+s0VQs2ojCX823m60m5f0sL5idt9SO6Tj6Dg+rdphGPIeJbJ6CxvBYkgkGKrYeDjvIpKTR38UzmtHJOGW3Q==",
+      "dev": true,
+      "dependencies": {
+        "@types/react": "*"
+      }
+    },
     "node_modules/@types/reactcss": {
     "node_modules/@types/reactcss": {
       "version": "1.2.6",
       "version": "1.2.6",
       "resolved": "https://registry.npmjs.org/@types/reactcss/-/reactcss-1.2.6.tgz",
       "resolved": "https://registry.npmjs.org/@types/reactcss/-/reactcss-1.2.6.tgz",
@@ -16136,6 +16233,12 @@
       "resolved": "https://registry.npmjs.org/@types/validator/-/validator-13.7.15.tgz",
       "resolved": "https://registry.npmjs.org/@types/validator/-/validator-13.7.15.tgz",
       "integrity": "sha512-yeinDVQunb03AEP8luErFcyf/7Lf7AzKCD0NXfgVoGCCQDNpZET8Jgq74oBgqKld3hafLbfzt/3inUdQvaFeXQ=="
       "integrity": "sha512-yeinDVQunb03AEP8luErFcyf/7Lf7AzKCD0NXfgVoGCCQDNpZET8Jgq74oBgqKld3hafLbfzt/3inUdQvaFeXQ=="
     },
     },
+    "node_modules/@types/warning": {
+      "version": "3.0.3",
+      "resolved": "https://registry.npmjs.org/@types/warning/-/warning-3.0.3.tgz",
+      "integrity": "sha512-D1XC7WK8K+zZEveUPY+cf4+kgauk8N4eHr/XIHXGlGYkHLud6hK9lYfZk1ry1TNh798cZUCgb6MqGEG8DkJt6Q==",
+      "dev": true
+    },
     "node_modules/@types/webidl-conversions": {
     "node_modules/@types/webidl-conversions": {
       "version": "7.0.0",
       "version": "7.0.0",
       "resolved": "https://registry.npmjs.org/@types/webidl-conversions/-/webidl-conversions-7.0.0.tgz",
       "resolved": "https://registry.npmjs.org/@types/webidl-conversions/-/webidl-conversions-7.0.0.tgz",
@@ -16871,6 +16974,19 @@
         "node": ">= 10.0.0"
         "node": ">= 10.0.0"
       }
       }
     },
     },
+    "node_modules/adjust-sourcemap-loader": {
+      "version": "4.0.0",
+      "resolved": "https://registry.npmjs.org/adjust-sourcemap-loader/-/adjust-sourcemap-loader-4.0.0.tgz",
+      "integrity": "sha512-OXwN5b9pCUXNQHJpwwD2qP40byEmSgzj8B4ydSN0uMNYWiFmJ6x6KwUllMmfk8Rwu/HJDFR7U8ubsWBoN0Xp0A==",
+      "dev": true,
+      "dependencies": {
+        "loader-utils": "^2.0.0",
+        "regex-parser": "^2.2.11"
+      },
+      "engines": {
+        "node": ">=8.9"
+      }
+    },
     "node_modules/agent-base": {
     "node_modules/agent-base": {
       "version": "6.0.2",
       "version": "6.0.2",
       "resolved": "https://registry.npmjs.org/agent-base/-/agent-base-6.0.2.tgz",
       "resolved": "https://registry.npmjs.org/agent-base/-/agent-base-6.0.2.tgz",
@@ -18517,6 +18633,26 @@
         "node": ">=6"
         "node": ">=6"
       }
       }
     },
     },
+    "node_modules/bootstrap-5": {
+      "name": "bootstrap",
+      "version": "5.3.2",
+      "resolved": "https://registry.npmjs.org/bootstrap/-/bootstrap-5.3.2.tgz",
+      "integrity": "sha512-D32nmNWiQHo94BKHLmOrdjlL05q1c8oxbtBphQFb9Z5to6eGRDCm0QgeaZ4zFBHzfg2++rqa2JkqCcxDy0sH0g==",
+      "dev": true,
+      "funding": [
+        {
+          "type": "github",
+          "url": "https://github.com/sponsors/twbs"
+        },
+        {
+          "type": "opencollective",
+          "url": "https://opencollective.com/bootstrap"
+        }
+      ],
+      "peerDependencies": {
+        "@popperjs/core": "^2.11.8"
+      }
+    },
     "node_modules/bowser": {
     "node_modules/bowser": {
       "version": "2.11.0",
       "version": "2.11.0",
       "resolved": "https://registry.npmjs.org/bowser/-/bowser-2.11.0.tgz",
       "resolved": "https://registry.npmjs.org/bowser/-/bowser-2.11.0.tgz",
@@ -19497,9 +19633,9 @@
       "integrity": "sha512-0TNiGstbQmCFwt4akjjBg5pLRTSyj/PkWQ1ZoO2zntmg9yLqSRxwEa4iCfQLGjqhiqBfOJa7W/E8wfGrTDmlZQ=="
       "integrity": "sha512-0TNiGstbQmCFwt4akjjBg5pLRTSyj/PkWQ1ZoO2zntmg9yLqSRxwEa4iCfQLGjqhiqBfOJa7W/E8wfGrTDmlZQ=="
     },
     },
     "node_modules/classnames": {
     "node_modules/classnames": {
-      "version": "2.3.1",
-      "resolved": "https://registry.npmjs.org/classnames/-/classnames-2.3.1.tgz",
-      "integrity": "sha512-OlQdbZ7gLfGarSqxesMesDa5uz7KFbID8Kpq/SxIoNGDqY8lSYs0D+hhtBXhcdB3rcbXArFr7vlHheLk1voeNA=="
+      "version": "2.5.1",
+      "resolved": "https://registry.npmjs.org/classnames/-/classnames-2.5.1.tgz",
+      "integrity": "sha512-saHYOzhIQs6wy2sVxTM6bUDsQO4F50V9RQ22qBpEdCW+I+/Wmke2HOl6lS6dTpdxVhb88/I6+Hs+438c3lfUow=="
     },
     },
     "node_modules/clean-css": {
     "node_modules/clean-css": {
       "version": "5.3.0",
       "version": "5.3.0",
@@ -27497,6 +27633,12 @@
         "node": ">=0.10.0"
         "node": ">=0.10.0"
       }
       }
     },
     },
+    "node_modules/immutable": {
+      "version": "4.3.5",
+      "resolved": "https://registry.npmjs.org/immutable/-/immutable-4.3.5.tgz",
+      "integrity": "sha512-8eabxkth9gZatlwl5TBuJnCsoTADlL6ftEr7A4qgdaTsPyreilDSnUk57SO+jfKcNtxPa22U5KK6DSeAYhpBJw==",
+      "dev": true
+    },
     "node_modules/import-fresh": {
     "node_modules/import-fresh": {
       "version": "3.3.0",
       "version": "3.3.0",
       "resolved": "https://registry.npmjs.org/import-fresh/-/import-fresh-3.3.0.tgz",
       "resolved": "https://registry.npmjs.org/import-fresh/-/import-fresh-3.3.0.tgz",
@@ -36186,6 +36328,72 @@
         "react-dom": ">=16.3.0"
         "react-dom": ">=16.3.0"
       }
       }
     },
     },
+    "node_modules/react-bootstrap-5": {
+      "name": "react-bootstrap",
+      "version": "2.10.1",
+      "resolved": "https://registry.npmjs.org/react-bootstrap/-/react-bootstrap-2.10.1.tgz",
+      "integrity": "sha512-J3OpRZIvCTQK+Tg/jOkRUvpYLHMdGeU9KqFUBQrV0d/Qr/3nsINpiOJyZMWnM5SJ3ctZdhPA6eCIKpEJR3Ellg==",
+      "dev": true,
+      "dependencies": {
+        "@babel/runtime": "^7.22.5",
+        "@restart/hooks": "^0.4.9",
+        "@restart/ui": "^1.6.6",
+        "@types/react-transition-group": "^4.4.6",
+        "classnames": "^2.3.2",
+        "dom-helpers": "^5.2.1",
+        "invariant": "^2.2.4",
+        "prop-types": "^15.8.1",
+        "prop-types-extra": "^1.1.0",
+        "react-transition-group": "^4.4.5",
+        "uncontrollable": "^7.2.1",
+        "warning": "^4.0.3"
+      },
+      "peerDependencies": {
+        "@types/react": ">=16.14.8",
+        "react": ">=16.14.0",
+        "react-dom": ">=16.14.0"
+      },
+      "peerDependenciesMeta": {
+        "@types/react": {
+          "optional": true
+        }
+      }
+    },
+    "node_modules/react-bootstrap-5/node_modules/dom-helpers": {
+      "version": "5.2.1",
+      "resolved": "https://registry.npmjs.org/dom-helpers/-/dom-helpers-5.2.1.tgz",
+      "integrity": "sha512-nRCa7CK3VTrM2NmGkIy4cbK7IZlgBE/PYMn55rrXefr5xXDP0LdtfPnblFDoVdcAfslJ7or6iqAUnx0CCGIWQA==",
+      "dev": true,
+      "dependencies": {
+        "@babel/runtime": "^7.8.7",
+        "csstype": "^3.0.2"
+      }
+    },
+    "node_modules/react-bootstrap-5/node_modules/react-transition-group": {
+      "version": "4.4.5",
+      "resolved": "https://registry.npmjs.org/react-transition-group/-/react-transition-group-4.4.5.tgz",
+      "integrity": "sha512-pZcd1MCJoiKiBR2NRxeCRg13uCXbydPnmB4EOeRrY7480qNWO8IIgQG6zlDkm6uRMsURXPuKq0GWtiM59a5Q6g==",
+      "dev": true,
+      "dependencies": {
+        "@babel/runtime": "^7.5.5",
+        "dom-helpers": "^5.0.1",
+        "loose-envify": "^1.4.0",
+        "prop-types": "^15.6.2"
+      },
+      "peerDependencies": {
+        "react": ">=16.6.0",
+        "react-dom": ">=16.6.0"
+      }
+    },
+    "node_modules/react-bootstrap-5/node_modules/warning": {
+      "version": "4.0.3",
+      "resolved": "https://registry.npmjs.org/warning/-/warning-4.0.3.tgz",
+      "integrity": "sha512-rpJyN222KWIvHJ/F53XSZv0Zl/accqHR8et1kpaMTD/fLCRxtV8iX8czMzY7sVZupTI3zcUTg8eycS2kNF9l6w==",
+      "dev": true,
+      "dependencies": {
+        "loose-envify": "^1.0.0"
+      }
+    },
     "node_modules/react-chartjs-2": {
     "node_modules/react-chartjs-2": {
       "version": "5.0.1",
       "version": "5.0.1",
       "resolved": "https://registry.npmjs.org/react-chartjs-2/-/react-chartjs-2-5.0.1.tgz",
       "resolved": "https://registry.npmjs.org/react-chartjs-2/-/react-chartjs-2-5.0.1.tgz",
@@ -37062,6 +37270,12 @@
         "@babel/runtime": "^7.8.4"
         "@babel/runtime": "^7.8.4"
       }
       }
     },
     },
+    "node_modules/regex-parser": {
+      "version": "2.3.0",
+      "resolved": "https://registry.npmjs.org/regex-parser/-/regex-parser-2.3.0.tgz",
+      "integrity": "sha512-TVILVSz2jY5D47F4mA4MppkBrafEaiUWJO/TcZHEIuI13AqoZMkK1WMA4Om1YkYbTx+9Ki1/tSUXbceyr9saRg==",
+      "dev": true
+    },
     "node_modules/regexp.prototype.flags": {
     "node_modules/regexp.prototype.flags": {
       "version": "1.5.1",
       "version": "1.5.1",
       "resolved": "https://registry.npmjs.org/regexp.prototype.flags/-/regexp.prototype.flags-1.5.1.tgz",
       "resolved": "https://registry.npmjs.org/regexp.prototype.flags/-/regexp.prototype.flags-1.5.1.tgz",
@@ -37399,6 +37613,22 @@
         "node": ">= 10.13.0"
         "node": ">= 10.13.0"
       }
       }
     },
     },
+    "node_modules/resolve-url-loader": {
+      "version": "5.0.0",
+      "resolved": "https://registry.npmjs.org/resolve-url-loader/-/resolve-url-loader-5.0.0.tgz",
+      "integrity": "sha512-uZtduh8/8srhBoMx//5bwqjQ+rfYOUq8zC9NrMUGtjBiGTtFJM42s58/36+hTqeqINcnYe08Nj3LkK9lW4N8Xg==",
+      "dev": true,
+      "dependencies": {
+        "adjust-sourcemap-loader": "^4.0.0",
+        "convert-source-map": "^1.7.0",
+        "loader-utils": "^2.0.0",
+        "postcss": "^8.2.14",
+        "source-map": "0.6.1"
+      },
+      "engines": {
+        "node": ">=12"
+      }
+    },
     "node_modules/restore-cursor": {
     "node_modules/restore-cursor": {
       "version": "3.1.0",
       "version": "3.1.0",
       "resolved": "https://registry.npmjs.org/restore-cursor/-/restore-cursor-3.1.0.tgz",
       "resolved": "https://registry.npmjs.org/restore-cursor/-/restore-cursor-3.1.0.tgz",
@@ -37723,6 +37953,60 @@
         "node": ">=0.10.0"
         "node": ">=0.10.0"
       }
       }
     },
     },
+    "node_modules/sass": {
+      "version": "1.70.0",
+      "resolved": "https://registry.npmjs.org/sass/-/sass-1.70.0.tgz",
+      "integrity": "sha512-uUxNQ3zAHeAx5nRFskBnrWzDUJrrvpCPD5FNAoRvTi0WwremlheES3tg+56PaVtCs5QDRX5CBLxxKMDJMEa1WQ==",
+      "dev": true,
+      "dependencies": {
+        "chokidar": ">=3.0.0 <4.0.0",
+        "immutable": "^4.0.0",
+        "source-map-js": ">=0.6.2 <2.0.0"
+      },
+      "bin": {
+        "sass": "sass.js"
+      },
+      "engines": {
+        "node": ">=14.0.0"
+      }
+    },
+    "node_modules/sass-loader": {
+      "version": "13.3.3",
+      "resolved": "https://registry.npmjs.org/sass-loader/-/sass-loader-13.3.3.tgz",
+      "integrity": "sha512-mt5YN2F1MOZr3d/wBRcZxeFgwgkH44wVc2zohO2YF6JiOMkiXe4BYRZpSu2sO1g71mo/j16txzUhsKZlqjVGzA==",
+      "dev": true,
+      "dependencies": {
+        "neo-async": "^2.6.2"
+      },
+      "engines": {
+        "node": ">= 14.15.0"
+      },
+      "funding": {
+        "type": "opencollective",
+        "url": "https://opencollective.com/webpack"
+      },
+      "peerDependencies": {
+        "fibers": ">= 3.1.0",
+        "node-sass": "^4.0.0 || ^5.0.0 || ^6.0.0 || ^7.0.0 || ^8.0.0 || ^9.0.0",
+        "sass": "^1.3.0",
+        "sass-embedded": "*",
+        "webpack": "^5.0.0"
+      },
+      "peerDependenciesMeta": {
+        "fibers": {
+          "optional": true
+        },
+        "node-sass": {
+          "optional": true
+        },
+        "sass": {
+          "optional": true
+        },
+        "sass-embedded": {
+          "optional": true
+        }
+      }
+    },
     "node_modules/sax": {
     "node_modules/sax": {
       "version": "1.2.1",
       "version": "1.2.1",
       "resolved": "https://registry.npmjs.org/sax/-/sax-1.2.1.tgz",
       "resolved": "https://registry.npmjs.org/sax/-/sax-1.2.1.tgz",
@@ -46059,7 +46343,7 @@
         "@testing-library/react": "^12.1.5",
         "@testing-library/react": "^12.1.5",
         "@testing-library/react-hooks": "^8.0.1",
         "@testing-library/react-hooks": "^8.0.1",
         "@testing-library/user-event": "^14.4.3",
         "@testing-library/user-event": "^14.4.3",
-        "@types/bootstrap": "^5.2.6",
+        "@types/bootstrap": "^5.2.10",
         "@types/chai": "^4.3.0",
         "@types/chai": "^4.3.0",
         "@types/events": "^3.0.0",
         "@types/events": "^3.0.0",
         "@types/express": "^4.17.13",
         "@types/express": "^4.17.13",
@@ -46095,6 +46379,7 @@
         "babel-plugin-module-resolver": "^5.0.0",
         "babel-plugin-module-resolver": "^5.0.0",
         "backbone": "^1.3.3",
         "backbone": "^1.3.3",
         "bootstrap": "^3.4.1",
         "bootstrap": "^3.4.1",
+        "bootstrap-5": "npm:bootstrap@^5.3.2",
         "c8": "^7.2.0",
         "c8": "^7.2.0",
         "chai": "^4.3.6",
         "chai": "^4.3.6",
         "chai-as-promised": "^7.1.1",
         "chai-as-promised": "^7.1.1",
@@ -46152,6 +46437,7 @@
         "qrcode": "^1.4.4",
         "qrcode": "^1.4.4",
         "react": "^17.0.2",
         "react": "^17.0.2",
         "react-bootstrap": "^0.33.1",
         "react-bootstrap": "^0.33.1",
+        "react-bootstrap-5": "npm:react-bootstrap@^2.10.0",
         "react-chartjs-2": "^5.0.1",
         "react-chartjs-2": "^5.0.1",
         "react-color": "^2.19.3",
         "react-color": "^2.19.3",
         "react-dnd": "^16.0.1",
         "react-dnd": "^16.0.1",
@@ -54796,7 +55082,7 @@
         "@testing-library/react": "^12.1.5",
         "@testing-library/react": "^12.1.5",
         "@testing-library/react-hooks": "^8.0.1",
         "@testing-library/react-hooks": "^8.0.1",
         "@testing-library/user-event": "^14.4.3",
         "@testing-library/user-event": "^14.4.3",
-        "@types/bootstrap": "^5.2.6",
+        "@types/bootstrap": "^5.2.10",
         "@types/chai": "^4.3.0",
         "@types/chai": "^4.3.0",
         "@types/events": "^3.0.0",
         "@types/events": "^3.0.0",
         "@types/express": "^4.17.13",
         "@types/express": "^4.17.13",
@@ -54840,6 +55126,7 @@
         "bcrypt": "^5.0.0",
         "bcrypt": "^5.0.0",
         "body-parser": "^1.19.0",
         "body-parser": "^1.19.0",
         "bootstrap": "^3.4.1",
         "bootstrap": "^3.4.1",
+        "bootstrap-5": "npm:bootstrap@^5.3.2",
         "bowser": "^2.11.0",
         "bowser": "^2.11.0",
         "bull": "^3.18.0",
         "bull": "^3.18.0",
         "bunyan": "^1.8.15",
         "bunyan": "^1.8.15",
@@ -54957,6 +55244,7 @@
         "rate-limiter-flexible": "^2.4.1",
         "rate-limiter-flexible": "^2.4.1",
         "react": "^17.0.2",
         "react": "^17.0.2",
         "react-bootstrap": "^0.33.1",
         "react-bootstrap": "^0.33.1",
+        "react-bootstrap-5": "npm:react-bootstrap@^2.10.0",
         "react-chartjs-2": "^5.0.1",
         "react-chartjs-2": "^5.0.1",
         "react-color": "^2.19.3",
         "react-color": "^2.19.3",
         "react-dnd": "^16.0.1",
         "react-dnd": "^16.0.1",
@@ -56167,9 +56455,9 @@
       }
       }
     },
     },
     "@popperjs/core": {
     "@popperjs/core": {
-      "version": "2.11.6",
-      "resolved": "https://registry.npmjs.org/@popperjs/core/-/core-2.11.6.tgz",
-      "integrity": "sha512-50/17A98tWUfQ176raKiOGXuYpLyyVMkxxG6oylzL3BPOlA6ADGdK7EYunSa4I064xerltq9TGXs8HmOk5E+vw==",
+      "version": "2.11.8",
+      "resolved": "https://registry.npmjs.org/@popperjs/core/-/core-2.11.8.tgz",
+      "integrity": "sha512-P1st0aksCrn9sGZhp8GMYwBnQsbvAWsZAX44oXNNvLHGqAOcoVxmjZiohstwQ7SqKnbR47akdNi+uleWD8+g6A==",
       "dev": true
       "dev": true
     },
     },
     "@protobufjs/aspromise": {
     "@protobufjs/aspromise": {
@@ -56619,6 +56907,15 @@
         "tslib": "^2.3.0"
         "tslib": "^2.3.0"
       }
       }
     },
     },
+    "@react-aria/ssr": {
+      "version": "3.9.1",
+      "resolved": "https://registry.npmjs.org/@react-aria/ssr/-/ssr-3.9.1.tgz",
+      "integrity": "sha512-NqzkLFP8ZVI4GSorS0AYljC13QW2sc8bDqJOkBvkAt3M8gbcAXJWVRGtZBCRscki9RZF+rNlnPdg0G0jYkhJcg==",
+      "dev": true,
+      "requires": {
+        "@swc/helpers": "^0.5.0"
+      }
+    },
     "@react-dnd/asap": {
     "@react-dnd/asap": {
       "version": "5.0.2",
       "version": "5.0.2",
       "resolved": "https://registry.npmjs.org/@react-dnd/asap/-/asap-5.0.2.tgz",
       "resolved": "https://registry.npmjs.org/@react-dnd/asap/-/asap-5.0.2.tgz",
@@ -56658,6 +56955,60 @@
       "from": "@replit/codemirror-vim@overleaf/codemirror-vim#53ce7363f80fba133d51ee861ca5e9b49af8e96b",
       "from": "@replit/codemirror-vim@overleaf/codemirror-vim#53ce7363f80fba133d51ee861ca5e9b49af8e96b",
       "requires": {}
       "requires": {}
     },
     },
+    "@restart/hooks": {
+      "version": "0.4.16",
+      "resolved": "https://registry.npmjs.org/@restart/hooks/-/hooks-0.4.16.tgz",
+      "integrity": "sha512-f7aCv7c+nU/3mF7NWLtVVr0Ra80RqsO89hO72r+Y/nvQr5+q0UFGkocElTH6MJApvReVh6JHUFYn2cw1WdHF3w==",
+      "dev": true,
+      "requires": {
+        "dequal": "^2.0.3"
+      }
+    },
+    "@restart/ui": {
+      "version": "1.6.6",
+      "resolved": "https://registry.npmjs.org/@restart/ui/-/ui-1.6.6.tgz",
+      "integrity": "sha512-eC3puKuWE1SRYbojWHXnvCNHGgf3uzHCb6JOhnF4OXPibOIPEkR1sqDSkL643ydigxwh+ruCa1CmYHlzk7ikKA==",
+      "dev": true,
+      "requires": {
+        "@babel/runtime": "^7.21.0",
+        "@popperjs/core": "^2.11.6",
+        "@react-aria/ssr": "^3.5.0",
+        "@restart/hooks": "^0.4.9",
+        "@types/warning": "^3.0.0",
+        "dequal": "^2.0.3",
+        "dom-helpers": "^5.2.0",
+        "uncontrollable": "^8.0.1",
+        "warning": "^4.0.3"
+      },
+      "dependencies": {
+        "dom-helpers": {
+          "version": "5.2.1",
+          "resolved": "https://registry.npmjs.org/dom-helpers/-/dom-helpers-5.2.1.tgz",
+          "integrity": "sha512-nRCa7CK3VTrM2NmGkIy4cbK7IZlgBE/PYMn55rrXefr5xXDP0LdtfPnblFDoVdcAfslJ7or6iqAUnx0CCGIWQA==",
+          "dev": true,
+          "requires": {
+            "@babel/runtime": "^7.8.7",
+            "csstype": "^3.0.2"
+          }
+        },
+        "uncontrollable": {
+          "version": "8.0.4",
+          "resolved": "https://registry.npmjs.org/uncontrollable/-/uncontrollable-8.0.4.tgz",
+          "integrity": "sha512-ulRWYWHvscPFc0QQXvyJjY6LIXU56f0h8pQFvhxiKk5V1fcI8gp9Ht9leVAhrVjzqMw0BgjspBINx9r6oyJUvQ==",
+          "dev": true,
+          "requires": {}
+        },
+        "warning": {
+          "version": "4.0.3",
+          "resolved": "https://registry.npmjs.org/warning/-/warning-4.0.3.tgz",
+          "integrity": "sha512-rpJyN222KWIvHJ/F53XSZv0Zl/accqHR8et1kpaMTD/fLCRxtV8iX8czMzY7sVZupTI3zcUTg8eycS2kNF9l6w==",
+          "dev": true,
+          "requires": {
+            "loose-envify": "^1.0.0"
+          }
+        }
+      }
+    },
     "@sentry/browser": {
     "@sentry/browser": {
       "version": "7.8.1",
       "version": "7.8.1",
       "resolved": "https://registry.npmjs.org/@sentry/browser/-/browser-7.8.1.tgz",
       "resolved": "https://registry.npmjs.org/@sentry/browser/-/browser-7.8.1.tgz",
@@ -60496,6 +60847,15 @@
       "integrity": "sha512-9F4ys4C74eSTEUNndnER3VJ15oru2NumfQxS8geE+f3eB5xvfxpWyqE5XlVnxb/R14uoXi6SLbBwwiDSkv+XEw==",
       "integrity": "sha512-9F4ys4C74eSTEUNndnER3VJ15oru2NumfQxS8geE+f3eB5xvfxpWyqE5XlVnxb/R14uoXi6SLbBwwiDSkv+XEw==",
       "dev": true
       "dev": true
     },
     },
+    "@swc/helpers": {
+      "version": "0.5.6",
+      "resolved": "https://registry.npmjs.org/@swc/helpers/-/helpers-0.5.6.tgz",
+      "integrity": "sha512-aYX01Ke9hunpoCexYAgQucEpARGQ5w/cqHFrIR+e9gdKb1QWTsVJuTJ2ozQzIAxLyRQe/m+2RqzkyOOGiMKRQA==",
+      "dev": true,
+      "requires": {
+        "tslib": "^2.4.0"
+      }
+    },
     "@swc/types": {
     "@swc/types": {
       "version": "0.1.5",
       "version": "0.1.5",
       "resolved": "https://registry.npmjs.org/@swc/types/-/types-0.1.5.tgz",
       "resolved": "https://registry.npmjs.org/@swc/types/-/types-0.1.5.tgz",
@@ -60751,9 +61111,9 @@
       }
       }
     },
     },
     "@types/bootstrap": {
     "@types/bootstrap": {
-      "version": "5.2.6",
-      "resolved": "https://registry.npmjs.org/@types/bootstrap/-/bootstrap-5.2.6.tgz",
-      "integrity": "sha512-BlAc3YATdasbHoxMoBWODrSF6qwQO/E9X8wVxCCSa6rWjnaZfpkr2N6pUMCY6jj2+wf0muUtLySbvU9etX6YqA==",
+      "version": "5.2.10",
+      "resolved": "https://registry.npmjs.org/@types/bootstrap/-/bootstrap-5.2.10.tgz",
+      "integrity": "sha512-F2X+cd6551tep0MvVZ6nM8v7XgGN/twpdNDjqS1TUM7YFNEtQYWk+dKAnH+T1gr6QgCoGMPl487xw/9hXooa2g==",
       "dev": true,
       "dev": true,
       "requires": {
       "requires": {
         "@popperjs/core": "^2.9.2"
         "@popperjs/core": "^2.9.2"
@@ -61352,6 +61712,15 @@
         "@types/react": "*"
         "@types/react": "*"
       }
       }
     },
     },
+    "@types/react-transition-group": {
+      "version": "4.4.10",
+      "resolved": "https://registry.npmjs.org/@types/react-transition-group/-/react-transition-group-4.4.10.tgz",
+      "integrity": "sha512-hT/+s0VQs2ojCX823m60m5f0sL5idt9SO6Tj6Dg+rdphGPIeJbJ6CxvBYkgkGKrYeDjvIpKTR38UzmtHJOGW3Q==",
+      "dev": true,
+      "requires": {
+        "@types/react": "*"
+      }
+    },
     "@types/reactcss": {
     "@types/reactcss": {
       "version": "1.2.6",
       "version": "1.2.6",
       "resolved": "https://registry.npmjs.org/@types/reactcss/-/reactcss-1.2.6.tgz",
       "resolved": "https://registry.npmjs.org/@types/reactcss/-/reactcss-1.2.6.tgz",
@@ -61506,6 +61875,12 @@
       "resolved": "https://registry.npmjs.org/@types/validator/-/validator-13.7.15.tgz",
       "resolved": "https://registry.npmjs.org/@types/validator/-/validator-13.7.15.tgz",
       "integrity": "sha512-yeinDVQunb03AEP8luErFcyf/7Lf7AzKCD0NXfgVoGCCQDNpZET8Jgq74oBgqKld3hafLbfzt/3inUdQvaFeXQ=="
       "integrity": "sha512-yeinDVQunb03AEP8luErFcyf/7Lf7AzKCD0NXfgVoGCCQDNpZET8Jgq74oBgqKld3hafLbfzt/3inUdQvaFeXQ=="
     },
     },
+    "@types/warning": {
+      "version": "3.0.3",
+      "resolved": "https://registry.npmjs.org/@types/warning/-/warning-3.0.3.tgz",
+      "integrity": "sha512-D1XC7WK8K+zZEveUPY+cf4+kgauk8N4eHr/XIHXGlGYkHLud6hK9lYfZk1ry1TNh798cZUCgb6MqGEG8DkJt6Q==",
+      "dev": true
+    },
     "@types/webidl-conversions": {
     "@types/webidl-conversions": {
       "version": "7.0.0",
       "version": "7.0.0",
       "resolved": "https://registry.npmjs.org/@types/webidl-conversions/-/webidl-conversions-7.0.0.tgz",
       "resolved": "https://registry.npmjs.org/@types/webidl-conversions/-/webidl-conversions-7.0.0.tgz",
@@ -62072,6 +62447,16 @@
       "integrity": "sha512-4B/qKCfeE/ODUaAUpSwfzazo5x29WD4r3vXiWsB7I2mSDAihwEqKO+g8GELZUQSSAo5e1XTYh3ZVfLyxBc12nA==",
       "integrity": "sha512-4B/qKCfeE/ODUaAUpSwfzazo5x29WD4r3vXiWsB7I2mSDAihwEqKO+g8GELZUQSSAo5e1XTYh3ZVfLyxBc12nA==",
       "dev": true
       "dev": true
     },
     },
+    "adjust-sourcemap-loader": {
+      "version": "4.0.0",
+      "resolved": "https://registry.npmjs.org/adjust-sourcemap-loader/-/adjust-sourcemap-loader-4.0.0.tgz",
+      "integrity": "sha512-OXwN5b9pCUXNQHJpwwD2qP40byEmSgzj8B4ydSN0uMNYWiFmJ6x6KwUllMmfk8Rwu/HJDFR7U8ubsWBoN0Xp0A==",
+      "dev": true,
+      "requires": {
+        "loader-utils": "^2.0.0",
+        "regex-parser": "^2.2.11"
+      }
+    },
     "agent-base": {
     "agent-base": {
       "version": "6.0.2",
       "version": "6.0.2",
       "resolved": "https://registry.npmjs.org/agent-base/-/agent-base-6.0.2.tgz",
       "resolved": "https://registry.npmjs.org/agent-base/-/agent-base-6.0.2.tgz",
@@ -63355,6 +63740,13 @@
       "integrity": "sha512-yN5oZVmRCwe5aKwzRj6736nSmKDX7pLYwsXiCj/EYmo16hODaBiT4En5btW/jhBF/seV+XMx3aYwukYC3A49DA==",
       "integrity": "sha512-yN5oZVmRCwe5aKwzRj6736nSmKDX7pLYwsXiCj/EYmo16hODaBiT4En5btW/jhBF/seV+XMx3aYwukYC3A49DA==",
       "dev": true
       "dev": true
     },
     },
+    "bootstrap-5": {
+      "version": "npm:bootstrap@5.3.2",
+      "resolved": "https://registry.npmjs.org/bootstrap/-/bootstrap-5.3.2.tgz",
+      "integrity": "sha512-D32nmNWiQHo94BKHLmOrdjlL05q1c8oxbtBphQFb9Z5to6eGRDCm0QgeaZ4zFBHzfg2++rqa2JkqCcxDy0sH0g==",
+      "dev": true,
+      "requires": {}
+    },
     "bowser": {
     "bowser": {
       "version": "2.11.0",
       "version": "2.11.0",
       "resolved": "https://registry.npmjs.org/bowser/-/bowser-2.11.0.tgz",
       "resolved": "https://registry.npmjs.org/bowser/-/bowser-2.11.0.tgz",
@@ -64109,9 +64501,9 @@
       "integrity": "sha512-0TNiGstbQmCFwt4akjjBg5pLRTSyj/PkWQ1ZoO2zntmg9yLqSRxwEa4iCfQLGjqhiqBfOJa7W/E8wfGrTDmlZQ=="
       "integrity": "sha512-0TNiGstbQmCFwt4akjjBg5pLRTSyj/PkWQ1ZoO2zntmg9yLqSRxwEa4iCfQLGjqhiqBfOJa7W/E8wfGrTDmlZQ=="
     },
     },
     "classnames": {
     "classnames": {
-      "version": "2.3.1",
-      "resolved": "https://registry.npmjs.org/classnames/-/classnames-2.3.1.tgz",
-      "integrity": "sha512-OlQdbZ7gLfGarSqxesMesDa5uz7KFbID8Kpq/SxIoNGDqY8lSYs0D+hhtBXhcdB3rcbXArFr7vlHheLk1voeNA=="
+      "version": "2.5.1",
+      "resolved": "https://registry.npmjs.org/classnames/-/classnames-2.5.1.tgz",
+      "integrity": "sha512-saHYOzhIQs6wy2sVxTM6bUDsQO4F50V9RQ22qBpEdCW+I+/Wmke2HOl6lS6dTpdxVhb88/I6+Hs+438c3lfUow=="
     },
     },
     "clean-css": {
     "clean-css": {
       "version": "5.3.0",
       "version": "5.3.0",
@@ -70211,6 +70603,12 @@
       "dev": true,
       "dev": true,
       "optional": true
       "optional": true
     },
     },
+    "immutable": {
+      "version": "4.3.5",
+      "resolved": "https://registry.npmjs.org/immutable/-/immutable-4.3.5.tgz",
+      "integrity": "sha512-8eabxkth9gZatlwl5TBuJnCsoTADlL6ftEr7A4qgdaTsPyreilDSnUk57SO+jfKcNtxPa22U5KK6DSeAYhpBJw==",
+      "dev": true
+    },
     "import-fresh": {
     "import-fresh": {
       "version": "3.3.0",
       "version": "3.3.0",
       "resolved": "https://registry.npmjs.org/import-fresh/-/import-fresh-3.3.0.tgz",
       "resolved": "https://registry.npmjs.org/import-fresh/-/import-fresh-3.3.0.tgz",
@@ -77759,6 +78157,59 @@
         "warning": "^3.0.0"
         "warning": "^3.0.0"
       }
       }
     },
     },
+    "react-bootstrap-5": {
+      "version": "npm:react-bootstrap@2.10.1",
+      "resolved": "https://registry.npmjs.org/react-bootstrap/-/react-bootstrap-2.10.1.tgz",
+      "integrity": "sha512-J3OpRZIvCTQK+Tg/jOkRUvpYLHMdGeU9KqFUBQrV0d/Qr/3nsINpiOJyZMWnM5SJ3ctZdhPA6eCIKpEJR3Ellg==",
+      "dev": true,
+      "requires": {
+        "@babel/runtime": "^7.22.5",
+        "@restart/hooks": "^0.4.9",
+        "@restart/ui": "^1.6.6",
+        "@types/react-transition-group": "^4.4.6",
+        "classnames": "^2.3.2",
+        "dom-helpers": "^5.2.1",
+        "invariant": "^2.2.4",
+        "prop-types": "^15.8.1",
+        "prop-types-extra": "^1.1.0",
+        "react-transition-group": "^4.4.5",
+        "uncontrollable": "^7.2.1",
+        "warning": "^4.0.3"
+      },
+      "dependencies": {
+        "dom-helpers": {
+          "version": "5.2.1",
+          "resolved": "https://registry.npmjs.org/dom-helpers/-/dom-helpers-5.2.1.tgz",
+          "integrity": "sha512-nRCa7CK3VTrM2NmGkIy4cbK7IZlgBE/PYMn55rrXefr5xXDP0LdtfPnblFDoVdcAfslJ7or6iqAUnx0CCGIWQA==",
+          "dev": true,
+          "requires": {
+            "@babel/runtime": "^7.8.7",
+            "csstype": "^3.0.2"
+          }
+        },
+        "react-transition-group": {
+          "version": "4.4.5",
+          "resolved": "https://registry.npmjs.org/react-transition-group/-/react-transition-group-4.4.5.tgz",
+          "integrity": "sha512-pZcd1MCJoiKiBR2NRxeCRg13uCXbydPnmB4EOeRrY7480qNWO8IIgQG6zlDkm6uRMsURXPuKq0GWtiM59a5Q6g==",
+          "dev": true,
+          "requires": {
+            "@babel/runtime": "^7.5.5",
+            "dom-helpers": "^5.0.1",
+            "loose-envify": "^1.4.0",
+            "prop-types": "^15.6.2"
+          }
+        },
+        "warning": {
+          "version": "4.0.3",
+          "resolved": "https://registry.npmjs.org/warning/-/warning-4.0.3.tgz",
+          "integrity": "sha512-rpJyN222KWIvHJ/F53XSZv0Zl/accqHR8et1kpaMTD/fLCRxtV8iX8czMzY7sVZupTI3zcUTg8eycS2kNF9l6w==",
+          "dev": true,
+          "requires": {
+            "loose-envify": "^1.0.0"
+          }
+        }
+      }
+    },
     "react-chartjs-2": {
     "react-chartjs-2": {
       "version": "5.0.1",
       "version": "5.0.1",
       "resolved": "https://registry.npmjs.org/react-chartjs-2/-/react-chartjs-2-5.0.1.tgz",
       "resolved": "https://registry.npmjs.org/react-chartjs-2/-/react-chartjs-2-5.0.1.tgz",
@@ -78433,6 +78884,12 @@
         "@babel/runtime": "^7.8.4"
         "@babel/runtime": "^7.8.4"
       }
       }
     },
     },
+    "regex-parser": {
+      "version": "2.3.0",
+      "resolved": "https://registry.npmjs.org/regex-parser/-/regex-parser-2.3.0.tgz",
+      "integrity": "sha512-TVILVSz2jY5D47F4mA4MppkBrafEaiUWJO/TcZHEIuI13AqoZMkK1WMA4Om1YkYbTx+9Ki1/tSUXbceyr9saRg==",
+      "dev": true
+    },
     "regexp.prototype.flags": {
     "regexp.prototype.flags": {
       "version": "1.5.1",
       "version": "1.5.1",
       "resolved": "https://registry.npmjs.org/regexp.prototype.flags/-/regexp.prototype.flags-1.5.1.tgz",
       "resolved": "https://registry.npmjs.org/regexp.prototype.flags/-/regexp.prototype.flags-1.5.1.tgz",
@@ -78692,6 +79149,19 @@
         "value-or-function": "^4.0.0"
         "value-or-function": "^4.0.0"
       }
       }
     },
     },
+    "resolve-url-loader": {
+      "version": "5.0.0",
+      "resolved": "https://registry.npmjs.org/resolve-url-loader/-/resolve-url-loader-5.0.0.tgz",
+      "integrity": "sha512-uZtduh8/8srhBoMx//5bwqjQ+rfYOUq8zC9NrMUGtjBiGTtFJM42s58/36+hTqeqINcnYe08Nj3LkK9lW4N8Xg==",
+      "dev": true,
+      "requires": {
+        "adjust-sourcemap-loader": "^4.0.0",
+        "convert-source-map": "^1.7.0",
+        "loader-utils": "^2.0.0",
+        "postcss": "^8.2.14",
+        "source-map": "0.6.1"
+      }
+    },
     "restore-cursor": {
     "restore-cursor": {
       "version": "3.1.0",
       "version": "3.1.0",
       "resolved": "https://registry.npmjs.org/restore-cursor/-/restore-cursor-3.1.0.tgz",
       "resolved": "https://registry.npmjs.org/restore-cursor/-/restore-cursor-3.1.0.tgz",
@@ -78943,6 +79413,26 @@
         }
         }
       }
       }
     },
     },
+    "sass": {
+      "version": "1.70.0",
+      "resolved": "https://registry.npmjs.org/sass/-/sass-1.70.0.tgz",
+      "integrity": "sha512-uUxNQ3zAHeAx5nRFskBnrWzDUJrrvpCPD5FNAoRvTi0WwremlheES3tg+56PaVtCs5QDRX5CBLxxKMDJMEa1WQ==",
+      "dev": true,
+      "requires": {
+        "chokidar": ">=3.0.0 <4.0.0",
+        "immutable": "^4.0.0",
+        "source-map-js": ">=0.6.2 <2.0.0"
+      }
+    },
+    "sass-loader": {
+      "version": "13.3.3",
+      "resolved": "https://registry.npmjs.org/sass-loader/-/sass-loader-13.3.3.tgz",
+      "integrity": "sha512-mt5YN2F1MOZr3d/wBRcZxeFgwgkH44wVc2zohO2YF6JiOMkiXe4BYRZpSu2sO1g71mo/j16txzUhsKZlqjVGzA==",
+      "dev": true,
+      "requires": {
+        "neo-async": "^2.6.2"
+      }
+    },
     "sax": {
     "sax": {
       "version": "1.2.1",
       "version": "1.2.1",
       "resolved": "https://registry.npmjs.org/sax/-/sax-1.2.1.tgz",
       "resolved": "https://registry.npmjs.org/sax/-/sax-1.2.1.tgz",

+ 3 - 0
package.json

@@ -18,6 +18,9 @@
     "eslint-plugin-prettier": "^4.0.0",
     "eslint-plugin-prettier": "^4.0.0",
     "eslint-plugin-promise": "^6.0.0",
     "eslint-plugin-promise": "^6.0.0",
     "prettier": "2.5.1",
     "prettier": "2.5.1",
+    "resolve-url-loader": "^5.0.0",
+    "sass": "^1.69.5",
+    "sass-loader": "^13.3.2",
     "typescript": "^5.0.4"
     "typescript": "^5.0.4"
   },
   },
   "scripts": {
   "scripts": {

+ 12 - 2
services/web/.storybook/preview.tsx

@@ -55,6 +55,8 @@ const preview: Preview = {
       // render stories in iframes, to isolate modals
       // render stories in iframes, to isolate modals
       inlineStories: false,
       inlineStories: false,
     },
     },
+    // Default to Bootstrap 3 styles
+    bootstrap5: false,
   },
   },
   globalTypes: {
   globalTypes: {
     theme: {
     theme: {
@@ -78,15 +80,23 @@ const preview: Preview = {
       return {
       return {
         // NOTE: this uses `${theme}style.less` rather than `${theme}.less`
         // NOTE: this uses `${theme}style.less` rather than `${theme}.less`
         // so that webpack only bundles files ending with "style.less"
         // so that webpack only bundles files ending with "style.less"
-        activeStyle: await import(
+        bootstrap3Style: await import(
           `!!to-string-loader!css-loader!less-loader!../../../services/web/frontend/stylesheets/${theme}style.less`
           `!!to-string-loader!css-loader!less-loader!../../../services/web/frontend/stylesheets/${theme}style.less`
         ),
         ),
+        // NOTE: this uses `${theme}style.scss` rather than `${theme}.scss`
+        // so that webpack only bundles files ending with "style.scss"
+        bootstrap5Style: await import(
+          `!!to-string-loader!css-loader!resolve-url-loader!sass-loader!../../../services/web/frontend/stylesheets/bootstrap-5/${theme}style.scss`
+        ),
       }
       }
     },
     },
   ],
   ],
   decorators: [
   decorators: [
     (Story, context) => {
     (Story, context) => {
-      const { activeStyle } = context.loaded
+      const { bootstrap3Style, bootstrap5Style } = context.loaded
+      const activeStyle = context.parameters.bootstrap5
+        ? bootstrap5Style
+        : bootstrap3Style
 
 
       return (
       return (
         <>
         <>

+ 14 - 1
services/web/app/src/infrastructure/ExpressLocals.js

@@ -213,7 +213,13 @@ module.exports = function (webRouter, privateApiRouter, publicApiRouter) {
     }
     }
 
 
     res.locals.buildCssPath = function (themeModifier = '') {
     res.locals.buildCssPath = function (themeModifier = '') {
-      return res.locals.buildStylesheetPath(`main-${themeModifier}style.css`)
+      // Pick which main stylesheet to use based on Bootstrap version
+      const bootstrap5Modifier =
+        res.locals.bootstrapVersion === 5 ? '-bootstrap-5' : ''
+
+      return res.locals.buildStylesheetPath(
+        `main-${themeModifier}style${bootstrap5Modifier}.css`
+      )
     }
     }
 
 
     res.locals.buildImgPath = function (imgFile) {
     res.locals.buildImgPath = function (imgFile) {
@@ -362,6 +368,13 @@ module.exports = function (webRouter, privateApiRouter, publicApiRouter) {
     next()
     next()
   })
   })
 
 
+  webRouter.use(function (req, res, next) {
+    // Set the Bootstrap version to 3 in all cases for now. This will come from
+    // a split test/feature flag in future.
+    res.locals.bootstrapVersion = 3
+    next()
+  })
+
   webRouter.use(function (req, res, next) {
   webRouter.use(function (req, res, next) {
     res.locals.ExposedSettings = {
     res.locals.ExposedSettings = {
       isOverleaf: Settings.overleaf != null,
       isOverleaf: Settings.overleaf != null,

+ 1 - 0
services/web/app/views/layout-base.pug

@@ -61,6 +61,7 @@ html(
 			meta(name="ol-isManagedAccount" data-type="boolean" content=isManagedAccount)
 			meta(name="ol-isManagedAccount" data-type="boolean" content=isManagedAccount)
 			each restriction in userRestrictions || []
 			each restriction in userRestrictions || []
 				meta(name='ol-cannot-' + restriction data-type="boolean" content=true)
 				meta(name='ol-cannot-' + restriction data-type="boolean" content=true)
+			meta(name="ol-bootstrapVersion" data-type="json" content=bootstrapVersion)
 
 
 		block head-scripts
 		block head-scripts
 
 

+ 32 - 0
services/web/frontend/js/features/ui/components/bootstrap-5/button.tsx

@@ -0,0 +1,32 @@
+import { Button as BootstrapButton } from 'react-bootstrap-5'
+import type { ButtonProps } from '@/features/ui/components/types/button-props'
+
+const sizeClasses = new Map<ButtonProps['size'], string>([
+  ['small', 'btn-sm'],
+  ['default', ''],
+  ['large', 'btn-lg'],
+])
+
+// TODO: Display a spinner when `loading` is true
+function Button({
+  variant = 'primary',
+  size = 'default',
+  disabled = false,
+  loading = false,
+  children,
+}: ButtonProps) {
+  const sizeClass = sizeClasses.get(size)
+
+  return (
+    <BootstrapButton
+      variant={variant}
+      className={sizeClass}
+      disabled={disabled}
+      {...(loading ? { 'data-ol-loading': true } : null)}
+    >
+      {children}
+    </BootstrapButton>
+  )
+}
+
+export default Button

+ 15 - 0
services/web/frontend/js/features/ui/components/types/button-props.ts

@@ -0,0 +1,15 @@
+import type { ReactNode } from 'react'
+
+export type ButtonProps = {
+  variant?:
+    | 'primary'
+    | 'secondary'
+    | 'ghost'
+    | 'danger'
+    | 'danger-ghost'
+    | 'premium'
+  size?: 'small' | 'default' | 'large'
+  disabled?: boolean
+  loading?: boolean
+  children: ReactNode
+}

+ 21 - 0
services/web/frontend/stories/ui/button.stories.tsx

@@ -0,0 +1,21 @@
+import Button from '@/features/ui/components/bootstrap-5/button'
+import type { Meta } from '@storybook/react'
+
+type Args = React.ComponentProps<typeof Button>
+
+export const NewButton = (args: Args) => {
+  return <Button {...args} />
+}
+
+const meta: Meta<typeof Button> = {
+  title: 'Shared / Components / Bootstrap 5 / Button',
+  component: Button,
+  args: {
+    children: 'A Bootstrap 5 button',
+  },
+  parameters: {
+    bootstrap5: true,
+  },
+}
+
+export default meta

+ 1 - 0
services/web/frontend/stylesheets/bootstrap-5/css-variables/themes/ieee.css

@@ -0,0 +1 @@
+/* IEEE CSS variable overrides go in here */

+ 1 - 0
services/web/frontend/stylesheets/bootstrap-5/css-variables/themes/light.css

@@ -0,0 +1 @@
+/* Light theme CSS variable overrides go in here */

+ 12 - 0
services/web/frontend/stylesheets/bootstrap-5/foundations/border-radius.scss

@@ -0,0 +1,12 @@
+// This file provides both Sass and CSS variables for border radiuses
+$border-radius-base: 4px;
+$border-radius-medium: 8px;
+$border-radius-large: 16px;
+$border-radius-full: 9999px;
+
+:root {
+  --border-radius-base: #{$border-radius-base};
+  --border-radius-medium: #{$border-radius-medium};
+  --border-radius-large: #{$border-radius-large};
+  --border-radius-full: #{$border-radius-full};
+}

+ 500 - 0
services/web/frontend/stylesheets/bootstrap-5/foundations/colours.scss

@@ -0,0 +1,500 @@
+// This file provides CSS and Sass variables for colours as both RGB triples,
+// which can be combined with an alpha value, and actual colour values.
+
+// Note that colours used by Bootstrap's Sass are used in calculations and
+// therefore cannot contain CSS variables
+
+/* ====== RGB triples, for use with alpha values ====== */
+
+/* Neutral */
+$white-rgb: 255 255 255;
+$neutral-10-rgb: 244 245 246;
+$neutral-20-rgb: 231 233 238;
+$neutral-30-rgb: 208 213 221;
+$neutral-40-rgb: 175 181 192;
+$neutral-50-rgb: 141 150 165;
+$neutral-60-rgb: 103 114 131;
+$neutral-70-rgb: 73 83 101;
+$neutral-80-rgb: 47 58 76;
+$neutral-90-rgb: 27 34 44;
+
+/* Green */
+$green-10-rgb: 235 246 234;
+$green-20-rgb: 187 219 184;
+$green-30-rgb: 140 202 134;
+$green-40-rgb: 91 181 83;
+$green-50-rgb: 19 138 7;
+$green-60-rgb: 36 107 30;
+$green-70-rgb: 31 89 25;
+
+/* Blue */
+$blue-10-rgb: 241 244 249;
+$blue-20-rgb: 195 208 227;
+$blue-30-rgb: 151 182 229;
+$blue-40-rgb: 101 151 224;
+$blue-50-rgb: 50 101 178;
+$blue-60-rgb: 40 81 143;
+$blue-70-rgb: 33 68 117;
+
+/* Red */
+$red-10-rgb: 249 241 241;
+$red-20-rgb: 245 190 186;
+$red-30-rgb: 229 157 154;
+$red-40-rgb: 227 109 102;
+$red-50-rgb: 184 58 51;
+$red-60-rgb: 148 47 42;
+$red-70-rgb: 120 39 34;
+
+/* Yellow */
+$yellow-10-rgb: 252 241 227;
+$yellow-20-rgb: 252 196 131;
+$yellow-30-rgb: 247 164 69;
+$yellow-40-rgb: 222 128 20;
+$yellow-50-rgb: 143 85 20;
+$yellow-60-rgb: 122 67 4;
+$yellow-70-rgb: 99 58 11;
+
+/* ====== Full RGB colour values ====== */
+
+/* Neutral */
+$white: rgb($white-rgb);
+$neutral-10: rgb($neutral-10-rgb);
+$neutral-20: rgb($neutral-20-rgb);
+$neutral-30: rgb($neutral-30-rgb);
+$neutral-40: rgb($neutral-40-rgb);
+$neutral-50: rgb($neutral-50-rgb);
+$neutral-60: rgb($neutral-60-rgb);
+$neutral-70: rgb($neutral-70-rgb);
+$neutral-80: rgb($neutral-80-rgb);
+$neutral-90: rgb($neutral-90-rgb);
+
+/* Green */
+$green-10: rgb($green-10-rgb);
+$green-20: rgb($green-20-rgb);
+$green-30: rgb($green-30-rgb);
+$green-40: rgb($green-40-rgb);
+$green-50: rgb($green-50-rgb);
+$green-60: rgb($green-60-rgb);
+$green-70: rgb($green-70-rgb);
+
+/* Blue */
+$blue-10: rgb($blue-10-rgb);
+$blue-20: rgb($blue-20-rgb);
+$blue-30: rgb($blue-30-rgb);
+$blue-40: rgb($blue-40-rgb);
+$blue-50: rgb($blue-50-rgb);
+$blue-60: rgb($blue-60-rgb);
+$blue-70: rgb($blue-70-rgb);
+
+/* Red */
+$red-10: rgb($red-10-rgb);
+$red-20: rgb($red-20-rgb);
+$red-30: rgb($red-30-rgb);
+$red-40: rgb($red-40-rgb);
+$red-50: rgb($red-50-rgb);
+$red-60: rgb($red-60-rgb);
+$red-70: rgb($red-70-rgb);
+
+/* Yellow */
+$yellow-10: rgb($yellow-10-rgb);
+$yellow-20: rgb($yellow-20-rgb);
+$yellow-30: rgb($yellow-30-rgb);
+$yellow-40: rgb($yellow-40-rgb);
+$yellow-50: rgb($yellow-50-rgb);
+$yellow-60: rgb($yellow-60-rgb);
+$yellow-70: rgb($yellow-70-rgb);
+
+/* ====== RGB triples for semantic colour variables, for use with alpha values ====== */
+$bg-light-primary-rgb: $white-rgb;
+$bg-light-secondary-rgb: $neutral-10-rgb;
+$bg-light-tertiary-rgb: $neutral-20-rgb;
+$bg-light-disabled-rgb: $neutral-20-rgb;
+
+$bg-dark-primary-rgb: $neutral-90-rgb;
+$bg-dark-secondary-rgb: $neutral-80-rgb;
+$bg-dark-tertiary-rgb: $neutral-70-rgb;
+$bg-dark-disabled-rgb: $neutral-70-rgb;
+
+$bg-accent-01-rgb: $green-50-rgb;
+$bg-accent-02-rgb: $green-60-rgb;
+$bg-accent-03-rgb: $green-10-rgb;
+
+$bg-danger-01-rgb: $red-50-rgb;
+$bg-danger-02-rgb: $red-60-rgb;
+$bg-danger-03-rgb: $red-10-rgb;
+
+$bg-warning-01-rgb: $yellow-50-rgb;
+$bg-warning-02-rgb: $yellow-60-rgb;
+$bg-warning-03-rgb: $yellow-10-rgb;
+
+$bg-info-01-rgb: $blue-50-rgb;
+$bg-info-02-rgb: $blue-60-rgb;
+$bg-info-03-rgb: $blue-10-rgb;
+
+$content-primary-rgb: $neutral-90-rgb;
+$content-secondary-rgb: $neutral-70-rgb;
+$content-disabled-rgb: $neutral-40-rgb;
+$content-placeholder-rgb: $neutral-60-rgb;
+$content-danger-rgb: $red-50-rgb;
+$content-warning-rgb: $yellow-50-rgb;
+$content-positive-rgb: $green-50-rgb;
+
+$border-primary-rgb: $neutral-60-rgb;
+$border-hover-rgb: $neutral-70-rgb;
+$border-disabled-rgb: $neutral-20-rgb;
+$border-active-rgb: $blue-50-rgb;
+$border-danger-rgb: $red-50-rgb;
+$border-divider-rgb: $neutral-20-rgb;
+
+$link-web-rgb: $green-60-rgb;
+$link-web-hover-rgb: $green-70-rgb;
+$link-web-visited-rgb: $green-70-rgb;
+$link-ui-rgb: $blue-50-rgb;
+$link-ui-hover-rgb: $blue-60-rgb;
+$link-ui-visited-rgb: $blue-60-rgb;
+
+$content-primary-dark-rgb: $white-rgb;
+$content-secondary-dark-rgb: $neutral-20-rgb;
+$content-disabled-dark-rgb: $neutral-60-rgb;
+$content-placeholder-dark-rgb: $neutral-50-rgb;
+$content-danger-dark-rgb: $red-40-rgb;
+$content-warning-dark-rgb: $yellow-40-rgb;
+$content-positive-dark-rgb: $green-40-rgb;
+
+$border-primary-dark-rgb: $neutral-30-rgb;
+$border-hover-dark-rgb: $neutral-20-rgb;
+$border-disabled-dark-rgb: $neutral-80-rgb;
+$border-active-dark-rgb: $blue-30-rgb;
+$border-danger-dark-rgb: $red-40-rgb;
+$border-divider-dark-rgb: $neutral-80-rgb;
+
+$link-web-dark-rgb: $green-30-rgb;
+$link-web-hover-dark-rgb: $green-40-rgb;
+$link-web-visited-dark-rgb: $green-40-rgb;
+$link-ui-dark-rgb: $blue-30-rgb;
+$link-ui-hover-dark-rgb: $blue-40-rgb;
+$link-ui-visited-dark-rgb: $blue-40-rgb;
+
+/* ====== Full RGB colour values for semantic colour variables ====== */
+$bg-light-primary: rgb($bg-light-primary-rgb);
+$bg-light-secondary: rgb($bg-light-secondary-rgb);
+$bg-light-tertiary: rgb($bg-light-tertiary-rgb);
+$bg-light-disabled: rgb($bg-light-disabled-rgb);
+
+$bg-dark-primary: rgb($bg-dark-primary-rgb);
+$bg-dark-secondary: rgb($bg-dark-secondary-rgb);
+$bg-dark-tertiary: rgb($bg-dark-tertiary-rgb);
+$bg-dark-disabled: rgb($bg-dark-disabled-rgb);
+
+$bg-accent-01: rgb($bg-accent-01-rgb);
+$bg-accent-02: rgb($bg-accent-02-rgb);
+$bg-accent-03: rgb($bg-accent-03-rgb);
+
+$bg-danger-01: rgb($bg-danger-01-rgb);
+$bg-danger-02: rgb($bg-danger-02-rgb);
+$bg-danger-03: rgb($bg-danger-03-rgb);
+
+$bg-warning-01: rgb($bg-warning-01-rgb);
+$bg-warning-02: rgb($bg-warning-02-rgb);
+$bg-warning-03: rgb($bg-warning-03-rgb);
+
+$bg-info-01: rgb($bg-info-01-rgb);
+$bg-info-02: rgb($bg-info-02-rgb);
+$bg-info-03: rgb($bg-info-03-rgb);
+
+$content-primary: rgb($content-primary-rgb);
+$content-secondary: rgb($content-secondary-rgb);
+$content-disabled: rgb($content-disabled-rgb);
+$content-placeholder: rgb($content-placeholder-rgb);
+$content-danger: rgb($content-danger-rgb);
+$content-warning: rgb($content-warning-rgb);
+$content-positive: rgb($content-positive-rgb);
+
+$border-primary: rgb($border-primary-rgb);
+$border-hover: rgb($border-hover-rgb);
+$border-disabled: rgb($border-disabled-rgb);
+$border-active: rgb($border-active-rgb);
+$border-danger: rgb($border-danger-rgb);
+$border-divider: rgb($border-divider-rgb);
+
+$link-web: rgb($link-web-rgb);
+$link-web-hover: rgb($link-web-hover-rgb);
+$link-web-visited: rgb($link-web-visited-rgb);
+$link-ui: rgb($link-ui-rgb);
+$link-ui-hover: rgb($link-ui-hover-rgb);
+$link-ui-visited: rgb($link-ui-visited-rgb);
+
+$content-primary-dark: rgb($content-primary-dark-rgb);
+$content-secondary-dark: rgb($content-secondary-dark-rgb);
+$content-disabled-dark: rgb($content-disabled-dark-rgb);
+$content-placeholder-dark: rgb($content-placeholder-dark-rgb);
+$content-danger-dark: rgb($content-danger-dark-rgb);
+$content-warning-dark: rgb($content-warning-dark-rgb);
+$content-positive-dark: rgb($content-positive-dark-rgb);
+
+$border-primary-dark: rgb($border-primary-dark-rgb);
+$border-hover-dark: rgb($border-hover-dark-rgb);
+$border-disabled-dark: rgb($border-disabled-dark-rgb);
+$border-active-dark: rgb($border-active-dark-rgb);
+$border-danger-dark: rgb($border-danger-dark-rgb);
+$border-divider-dark: rgb($border-divider-dark-rgb);
+
+$link-web-dark: rgb($link-web-dark-rgb);
+$link-web-hover-dark: rgb($link-web-hover-dark-rgb);
+$link-web-visited-dark: rgb($link-web-visited-dark-rgb);
+$link-ui-dark: rgb($link-ui-dark-rgb);
+$link-ui-hover-dark: rgb($link-ui-hover-dark-rgb);
+$link-ui-visited-dark: rgb($link-ui-visited-dark-rgb);
+
+:root {
+  /* ====== RGB triples, for use with alpha values ====== */
+
+  /* Neutral */
+  --white-rgb: #{$white-rgb};
+  --neutral-10-rgb: #{$neutral-10-rgb};
+  --neutral-20-rgb: #{$neutral-20-rgb};
+  --neutral-30-rgb: #{$neutral-30-rgb};
+  --neutral-40-rgb: #{$neutral-40-rgb};
+  --neutral-50-rgb: #{$neutral-50-rgb};
+  --neutral-60-rgb: #{$neutral-60-rgb};
+  --neutral-70-rgb: #{$neutral-70-rgb};
+  --neutral-80-rgb: #{$neutral-80-rgb};
+  --neutral-90-rgb: #{$neutral-90-rgb};
+
+  /* Green */
+  --green-10-rgb: #{$green-10-rgb};
+  --green-20-rgb: #{$green-20-rgb};
+  --green-30-rgb: #{$green-30-rgb};
+  --green-40-rgb: #{$green-40-rgb};
+  --green-50-rgb: #{$green-50-rgb};
+  --green-60-rgb: #{$green-60-rgb};
+  --green-70-rgb: #{$green-70-rgb};
+
+  /* Blue */
+  --blue-10-rgb: #{$blue-10-rgb};
+  --blue-20-rgb: #{$blue-20-rgb};
+  --blue-30-rgb: #{$blue-30-rgb};
+  --blue-40-rgb: #{$blue-40-rgb};
+  --blue-50-rgb: #{$blue-50-rgb};
+  --blue-60-rgb: #{$blue-60-rgb};
+  --blue-70-rgb: #{$blue-70-rgb};
+
+  /* Red */
+  --red-10-rgb: #{$red-10-rgb};
+  --red-20-rgb: #{$red-20-rgb};
+  --red-30-rgb: #{$red-30-rgb};
+  --red-40-rgb: #{$red-40-rgb};
+  --red-50-rgb: #{$red-50-rgb};
+  --red-60-rgb: #{$red-60-rgb};
+  --red-70-rgb: #{$red-70-rgb};
+
+  /* Yellow */
+  --yellow-10-rgb: #{$yellow-10-rgb};
+  --yellow-20-rgb: #{$yellow-20-rgb};
+  --yellow-30-rgb: #{$yellow-30-rgb};
+  --yellow-40-rgb: #{$yellow-40-rgb};
+  --yellow-50-rgb: #{$yellow-50-rgb};
+  --yellow-60-rgb: #{$yellow-60-rgb};
+  --yellow-70-rgb: #{$yellow-70-rgb};
+
+  /* ====== Full RGB colour values ====== */
+
+  /* Neutral */
+  --white: rgb(var(--white-rgb));
+  --neutral-10: rgb(var(--neutral-10-rgb));
+  --neutral-20: rgb(var(--neutral-20-rgb));
+  --neutral-30: rgb(var(--neutral-30-rgb));
+  --neutral-40: rgb(var(--neutral-40-rgb));
+  --neutral-50: rgb(var(--neutral-50-rgb));
+  --neutral-60: rgb(var(--neutral-60-rgb));
+  --neutral-70: rgb(var(--neutral-70-rgb));
+  --neutral-80: rgb(var(--neutral-80-rgb));
+  --neutral-90: rgb(var(--neutral-90-rgb));
+
+  /* Green */
+  --green-10: rgb(var(--green-10-rgb));
+  --green-20: rgb(var(--green-20-rgb));
+  --green-30: rgb(var(--green-30-rgb));
+  --green-40: rgb(var(--green-40-rgb));
+  --green-50: rgb(var(--green-50-rgb));
+  --green-60: rgb(var(--green-60-rgb));
+  --green-70: rgb(var(--green-70-rgb));
+
+  /* Blue */
+  --blue-10: rgb(var(--blue-10-rgb));
+  --blue-20: rgb(var(--blue-20-rgb));
+  --blue-30: rgb(var(--blue-30-rgb));
+  --blue-40: rgb(var(--blue-40-rgb));
+  --blue-50: rgb(var(--blue-50-rgb));
+  --blue-60: rgb(var(--blue-60-rgb));
+  --blue-70: rgb(var(--blue-70-rgb));
+
+  /* Red */
+  --red-10: rgb(var(--red-10-rgb));
+  --red-20: rgb(var(--red-20-rgb));
+  --red-30: rgb(var(--red-30-rgb));
+  --red-40: rgb(var(--red-40-rgb));
+  --red-50: rgb(var(--red-50-rgb));
+  --red-60: rgb(var(--red-60-rgb));
+  --red-70: rgb(var(--red-70-rgb));
+
+  /* Yellow */
+  --yellow-10: rgb(var(--yellow-10-rgb));
+  --yellow-20: rgb(var(--yellow-20-rgb));
+  --yellow-30: rgb(var(--yellow-30-rgb));
+  --yellow-40: rgb(var(--yellow-40-rgb));
+  --yellow-50: rgb(var(--yellow-50-rgb));
+  --yellow-60: rgb(var(--yellow-60-rgb));
+  --yellow-70: rgb(var(--yellow-70-rgb));
+
+  /* ====== RGB triples for semantic colour variables, for use with alpha values ====== */
+  --bg-light-primary-rgb: var(--white-rgb);
+  --bg-light-secondary-rgb: var(--neutral-10-rgb);
+  --bg-light-tertiary-rgb: var(--neutral-20-rgb);
+  --bg-light-disabled-rgb: var(--neutral-20-rgb);
+
+  --bg-dark-primary-rgb: var(--neutral-90-rgb);
+  --bg-dark-secondary-rgb: var(--neutral-80-rgb);
+  --bg-dark-tertiary-rgb: var(--neutral-70-rgb);
+  --bg-dark-disabled-rgb: var(--neutral-70-rgb);
+
+  --bg-accent-01-rgb: var(--green-50-rgb);
+  --bg-accent-02-rgb: var(--green-60-rgb);
+  --bg-accent-03-rgb: var(--green-10-rgb);
+
+  --bg-danger-01-rgb: var(--red-50-rgb);
+  --bg-danger-02-rgb: var(--red-60-rgb);
+  --bg-danger-03-rgb: var(--red-10-rgb);
+
+  --bg-warning-01-rgb: var(--yellow-50-rgb);
+  --bg-warning-02-rgb: var(--yellow-60-rgb);
+  --bg-warning-03-rgb: var(--yellow-10-rgb);
+
+  --bg-info-01-rgb: var(--blue-50-rgb);
+  --bg-info-02-rgb: var(--blue-60-rgb);
+  --bg-info-03-rgb: var(--blue-10-rgb);
+
+  --content-primary-rgb: var(--neutral-90-rgb);
+  --content-secondary-rgb: var(--neutral-70-rgb);
+  --content-disabled-rgb: var(--neutral-40-rgb);
+  --content-placeholder-rgb: var(--neutral-60-rgb);
+  --content-danger-rgb: var(--red-50-rgb);
+  --content-warning-rgb: var(--yellow-50-rgb);
+  --content-positive-rgb: var(--green-50-rgb);
+
+  --border-primary-rgb: var(--neutral-60-rgb);
+  --border-hover-rgb: var(--neutral-70-rgb);
+  --border-disabled-rgb: var(--neutral-20-rgb);
+  --border-active-rgb: var(--blue-50-rgb);
+  --border-danger-rgb: var(--red-50-rgb);
+  --border-divider-rgb: var(--neutral-20-rgb);
+
+  --link-web-rgb: var(--green-60-rgb);
+  --link-web-hover-rgb: var(--green-70-rgb);
+  --link-web-visited-rgb: var(--green-70-rgb);
+  --link-ui-rgb: var(--blue-50-rgb);
+  --link-ui-hover-rgb: var(--blue-60-rgb);
+  --link-ui-visited-rgb: var(--blue-60-rgb);
+
+  --content-primary-dark-rgb: var(--white-rgb);
+  --content-secondary-dark-rgb: var(--neutral-20-rgb);
+  --content-disabled-dark-rgb: var(--neutral-60-rgb);
+  --content-placeholder-dark-rgb: var(--neutral-50-rgb);
+  --content-danger-dark-rgb: var(--red-40-rgb);
+  --content-warning-dark-rgb: var(--yellow-40-rgb);
+  --content-positive-dark-rgb: var(--green-40-rgb);
+
+  --border-primary-dark-rgb: var(--neutral-30-rgb);
+  --border-hover-dark-rgb: var(--neutral-20-rgb);
+  --border-disabled-dark-rgb: var(--neutral-80-rgb);
+  --border-active-dark-rgb: var(--blue-30-rgb);
+  --border-danger-dark-rgb: var(--red-40-rgb);
+  --border-divider-dark-rgb: var(--neutral-80-rgb);
+
+  --link-web-dark-rgb: var(--green-30-rgb);
+  --link-web-hover-dark-rgb: var(--green-40-rgb);
+  --link-web-visited-dark-rgb: var(--green-40-rgb);
+  --link-ui-dark-rgb: var(--blue-30-rgb);
+  --link-ui-hover-dark-rgb: var(--blue-40-rgb);
+  --link-ui-visited-dark-rgb: var(--blue-40-rgb);
+
+
+  /* ====== Full RGB colour values for semantic colour variables ====== */
+  --bg-light-primary: rgb(var(--bg-light-primary-rgb));
+  --bg-light-secondary: rgb(var(--bg-light-secondary-rgb));
+  --bg-light-tertiary: rgb(var(--bg-light-tertiary-rgb));
+  --bg-light-disabled: rgb(var(--bg-light-disabled-rgb));
+
+  --bg-dark-primary: rgb(var(--bg-dark-primary-rgb));
+  --bg-dark-secondary: rgb(var(--bg-dark-secondary-rgb));
+  --bg-dark-tertiary: rgb(var(--bg-dark-tertiary-rgb));
+  --bg-dark-disabled: rgb(var(--bg-dark-disabled-rgb));
+
+  --bg-accent-01: rgb(var(--bg-accent-01-rgb));
+  --bg-accent-02: rgb(var(--bg-accent-02-rgb));
+  --bg-accent-03: rgb(var(--bg-accent-03-rgb));
+
+  --bg-danger-01: rgb(var(--bg-danger-01-rgb));
+  --bg-danger-02: rgb(var(--bg-danger-02-rgb));
+  --bg-danger-03: rgb(var(--bg-danger-03-rgb));
+
+  --bg-warning-01: rgb(var(--bg-warning-01-rgb));
+  --bg-warning-02: rgb(var(--bg-warning-02-rgb));
+  --bg-warning-03: rgb(var(--bg-warning-03-rgb));
+
+  --bg-info-01: rgb(var(--bg-info-01-rgb));
+  --bg-info-02: rgb(var(--bg-info-02-rgb));
+  --bg-info-03: rgb(var(--bg-info-03-rgb));
+
+  --content-primary: rgb(var(--content-primary-rgb));
+  --content-secondary: rgb(var(--content-secondary-rgb));
+  --content-disabled: rgb(var(--content-disabled-rgb));
+  --content-placeholder: rgb(var(--content-placeholder-rgb));
+  --content-danger: rgb(var(--content-danger-rgb));
+  --content-warning: rgb(var(--content-warning-rgb));
+  --content-positive: rgb(var(--content-positive-rgb));
+
+  --border-primary: rgb(var(--border-primary-rgb));
+  --border-hover: rgb(var(--border-hover-rgb));
+  --border-disabled: rgb(var(--border-disabled-rgb));
+  --border-active: rgb(var(--border-active-rgb));
+  --border-danger: rgb(var(--border-danger-rgb));
+  --border-divider: rgb(var(--border-divider-rgb));
+
+  --link-web: rgb(var(--link-web-rgb));
+  --link-web-hover: rgb(var(--link-web-hover-rgb));
+  --link-web-visited: rgb(var(--link-web-visited-rgb));
+  --link-ui: rgb(var(--link-ui-rgb));
+  --link-ui-hover: rgb(var(--link-ui-hover-rgb));
+  --link-ui-visited: rgb(var(--link-ui-visited-rgb));
+
+  --content-primary-dark: rgb(var(--content-primary-dark-rgb));
+  --content-secondary-dark: rgb(var(--content-secondary-dark-rgb));
+  --content-disabled-dark: rgb(var(--content-disabled-dark-rgb));
+  --content-placeholder-dark: rgb(var(--content-placeholder-dark-rgb));
+  --content-danger-dark: rgb(var(--content-danger-dark-rgb));
+  --content-warning-dark: rgb(var(--content-warning-dark-rgb));
+  --content-positive-dark: rgb(var(--content-positive-dark-rgb));
+
+  --border-primary-dark: rgb(var(--border-primary-dark-rgb));
+  --border-hover-dark: rgb(var(--border-hover-dark-rgb));
+  --border-disabled-dark: rgb(var(--border-disabled-dark-rgb));
+  --border-active-dark: rgb(var(--border-active-dark-rgb));
+  --border-danger-dark: rgb(var(--border-danger-dark-rgb));
+  --border-divider-dark: rgb(var(--border-divider-dark-rgb));
+
+  --link-web-dark: rgb(var(--link-web-dark-rgb));
+  --link-web-hover-dark: rgb(var(--link-web-hover-dark-rgb));
+  --link-web-visited-dark: rgb(var(--link-web-visited-dark-rgb));
+  --link-ui-dark: rgb(var(--link-ui-dark-rgb));
+  --link-ui-hover-dark: rgb(var(--link-ui-hover-dark-rgb));
+  --link-ui-visited-dark: rgb(var(--link-ui-visited-dark-rgb));
+
+  // Gradients
+  --premium-gradient: linear-gradient(
+      246deg,
+      var(--blue-70) 0%,
+      #254c84 29%,
+      var(--blue-40) 97%
+  );
+}

+ 38 - 0
services/web/frontend/stylesheets/bootstrap-5/foundations/spacing.scss

@@ -0,0 +1,38 @@
+// This file provided both Sass and CSS variables for spacing
+$spacing-00: 0;
+$spacing-01: 2px;
+$spacing-02: 4px;
+$spacing-03: 6px;
+$spacing-04: 8px;
+$spacing-05: 12px;
+$spacing-06: 16px;
+$spacing-07: 20px;
+$spacing-08: 24px;
+$spacing-09: 32px;
+$spacing-10: 40px;
+$spacing-11: 48px;
+$spacing-12: 56px;
+$spacing-13: 64px;
+$spacing-14: 72px;
+$spacing-15: 80px;
+$spacing-16: 96px;
+
+:root {
+  --spacing-00: #{$spacing-00};
+  --spacing-01: #{$spacing-01};
+  --spacing-02: #{$spacing-02};
+  --spacing-03: #{$spacing-03};
+  --spacing-04: #{$spacing-04};
+  --spacing-05: #{$spacing-05};
+  --spacing-06: #{$spacing-06};
+  --spacing-07: #{$spacing-07};
+  --spacing-08: #{$spacing-08};
+  --spacing-09: #{$spacing-09};
+  --spacing-10: #{$spacing-10};
+  --spacing-11: #{$spacing-11};
+  --spacing-12: #{$spacing-12};
+  --spacing-13: #{$spacing-13};
+  --spacing-14: #{$spacing-14};
+  --spacing-15: #{$spacing-15};
+  --spacing-16: #{$spacing-16};
+}

+ 104 - 0
services/web/frontend/stylesheets/bootstrap-5/foundations/typography.scss

@@ -0,0 +1,104 @@
+// This file provides CSS variables for font size and line height, plus Sass variables for base text size for Bootstrap
+@use 'sass:math';
+
+$default-font-size: 1rem;
+$default-line-height: 1.5;
+
+:root {
+  --font-size-01: 0.75rem; // 12px
+  --font-size-02: 0.875rem; // 14px
+  --font-size-03: 1rem; // 16px
+  --font-size-04: 1.125rem; // 18px
+  --font-size-05: 1.25rem; // 20px
+  --font-size-06: 1.5rem; // 24px
+  --font-size-07: 1.875rem; // 30px
+  --font-size-08: 2.25rem; // 36px
+  --font-size-09: 3rem; // 48px
+  --font-size-10: 3.25rem; // 52px
+  --font-size-11: 3.75rem; // 60px
+  --font-size-12: 4.5rem; // 72px
+  --font-size-13: 6em; // 96px
+
+  --line-height-01: 1rem; // 16px
+  --line-height-02: 1.25rem; // 20px
+  --line-height-03: 1.5rem; // 24px
+  --line-height-04: 1.75rem; // 28px
+  --line-height-05: 2rem; // 32px
+  --line-height-06: 2.5rem; // 40px
+  --line-height-07: 3rem; // 48px
+  --line-height-08: 4rem; // 64px
+  --line-height-09: 4.25rem; // 68px
+  --line-height-10: 5rem; // 80px
+  --line-height-11: 6rem; // 96px
+  --line-height-12: 8rem; // 128px
+}
+
+// Semantic styles, as Sass mixins
+@mixin display-lg {
+  font-size: var(--font-size-13);
+  line-height: var(--line-height-12);
+}
+
+@mixin display-md {
+  font-size: var(--font-size-12);
+  line-height: var(--line-height-11);
+}
+@mixin display-sm {
+  font-size: var(--font-size-11);
+  line-height: var(--line-height-10);
+}
+
+@mixin display-xs {
+  font-size: var(--font-size-10);
+  line-height: var(--line-height-09);
+}
+
+@mixin heading-2xl {
+  font-size: var(--font-size-09);
+  line-height: var(--line-height-08);
+}
+
+@mixin heading-xl {
+  font-size: var(--font-size-08);
+  line-height: var(--line-height-07);
+}
+
+@mixin heading-lg {
+  font-size: var(--font-size-07);
+  line-height: var(--line-height-06);
+}
+
+@mixin heading-md {
+  font-size: var(--font-size-06);
+  line-height: var(--line-height-05);
+}
+
+@mixin heading-sm {
+  font-size: var(--font-size-05);
+  line-height: var(--line-height-04);
+}
+
+@mixin heading-xs {
+  font-size: var(--font-size-04);
+  line-height: var(--line-height-03);
+}
+
+@mixin body-lg {
+  font-size: var(--font-size-04);
+  line-height: var(--line-height-03);
+}
+
+@mixin body-base {
+  font-size: var(--font-size-03);
+  line-height: var(--line-height-03);
+}
+
+@mixin body-sm {
+  font-size: var(--font-size-02);
+  line-height: var(--line-height-02);
+}
+
+@mixin body-xs {
+  font-size: var(--font-size-01);
+  line-height: var(--line-height-01);
+}

+ 7 - 0
services/web/frontend/stylesheets/bootstrap-5/main-ieee-style.scss

@@ -0,0 +1,7 @@
+@import 'main-style';
+
+// IEEE-specific theme overrides
+@import 'css-variables/themes/ieee';
+
+// IEEE-specific rules
+$ieee-wedge: 30px;

+ 4 - 0
services/web/frontend/stylesheets/bootstrap-5/main-light-style.scss

@@ -0,0 +1,4 @@
+@import 'main-style';
+@import 'css-variables/themes/light';
+
+$is-overleaf-light: true

+ 34 - 0
services/web/frontend/stylesheets/bootstrap-5/main-style.scss

@@ -0,0 +1,34 @@
+// Web fonts
+@import '../../fonts/lato.css';
+@import '../../fonts/merriweather.css';
+@import '../../fonts/source-code-pro.css';
+@import '../../fonts/stix-two-math.css';
+@import '../../fonts/noto-serif.css';
+@import '../../fonts/material-symbols.css';
+@import '../../fonts/font-awesome.css';
+
+$is-overleaf-light: false;
+
+// Vendor CSS
+// TODO Bootstrap 5: Check whether this works with Bootstrap 5, and whether we can replace it
+@import '../vendor/select/select.css';
+
+// Sass and CSS variables from Overleaf foundations
+@import 'foundations/colours';
+@import 'foundations/spacing';
+@import 'foundations/typography';
+@import 'foundations/border-radius';
+
+// Boostrap-related
+
+// Note that files containing Bootstrap or Sass files that interact with
+// Bootstrap's Sass variable must use @import rather than @use because
+// Bootstrap relies on its variables, mixins etc. all being global.
+
+// Include Bootstrap 5 itself, plus overrides of its Sass variable
+@import 'scss/bootstrap';
+
+// Override and extend Bootstrap styles. This can include overriding CSS
+// variables in the usual CSS way, and can also refer to (but not override)
+// Bootstrap Sass variables.
+@import 'scss/bootstrap-rule-overrides';

+ 3 - 0
services/web/frontend/stylesheets/bootstrap-5/scss/bootstrap-rule-overrides.scss

@@ -0,0 +1,3 @@
+// This file is included after Bootstrap and is used to override non-default
+// Bootstrap variables and styles
+@import 'bootstrap-rule-overrides/buttons';

+ 124 - 0
services/web/frontend/stylesheets/bootstrap-5/scss/bootstrap-rule-overrides/buttons.scss

@@ -0,0 +1,124 @@
+@mixin ol-button-size($font-size, $line-height, $padding-y, $padding-x: var(--spacing-06)) {
+  --bs-btn-font-size: #{$font-size};
+  --bs-btn-line-height: #{$line-height};
+  --bs-btn-padding-y: #{$padding-y};
+  --bs-btn-padding-x: #{$padding-x};
+}
+
+@mixin ol-button-variant(
+  $color,
+  $background,
+  $border: $background,
+  $hover-background: $background,
+  $hover-border: $hover-background,
+  $borderless: true
+) {
+  --bs-btn-color: #{$color};
+  --bs-btn-bg: #{$background};
+  --bs-btn-border-color: #{$border};
+  --bs-btn-hover-color: #{$color};
+  --bs-btn-hover-bg: #{$hover-background};
+  --bs-btn-hover-border-color: #{$hover-border};
+  --bs-btn-active-color: #{$color};
+  --bs-btn-active-bg: #{$hover-background};
+  --bs-btn-active-border-color: #{$hover-border};
+  --bs-btn-disabled-color: var(--content-disabled);
+  --bs-btn-disabled-bg: var(--bg-light-disabled);
+  --bs-btn-disabled-border-color: var(--bg-light-disabled);
+
+  @if $borderless {
+    --bs-btn-border-width: 0;
+  }
+
+  // Use the default state colors when in a loading state
+  &[data-ol-loading='true'] {
+    color: var(--bs-btn-color);
+    background-color: var(--bs-btn-bg);
+    border-color: var(--bs-btn-border-color);
+  }
+}
+
+.btn {
+  // Focus style for all buttons
+  --bs-btn-focus-box-shadow: 0 0 0 2px var(--border-active-dark);
+
+  // Sizes
+
+  // Default size
+  @include ol-button-size(
+    $font-size: var(--font-size-03),
+    $line-height: var(--line-height-03),
+    $padding-y: var(--spacing-03),
+  );
+
+  &.btn-lg {
+    @include ol-button-size(
+      $font-size: var(--font-size-03),
+      $line-height: var(--line-height-03),
+      $padding-y: var(--spacing-05),
+    );
+  }
+
+  &.btn-sm {
+    @include ol-button-size(
+      $font-size: var(--font-size-02),
+      $line-height: var(--line-height-02),
+      $padding-y: var(--spacing-01),
+      $padding-x: var(--spacing-05),
+    );
+  }
+
+  // Variants
+  &.btn-primary {
+    @include ol-button-variant(
+      $color: var(--content-primary-dark),
+      $background: var(--bg-accent-01),
+      $hover-background: var(--bg-accent-02),
+      $hover-border: var(--bg-accent-02),
+    );
+  }
+
+  &.btn-secondary {
+    @include ol-button-variant(
+      $color: var(--content-primary),
+      $background: var(--bg-light-primary),
+      $border: var(--border-primary),
+      $hover-background: var(--bg-light-tertiary),
+      $hover-border: var(--border-primary),
+      $borderless: false,
+    );
+  }
+
+  &.btn-ghost {
+    @include ol-button-variant(
+      $color: var(--content-primary),
+      $background: var(--bg-light-primary),
+      $hover-background: var(--bg-light-tertiary),
+    )
+  }
+
+  &.btn-danger {
+    @include ol-button-variant(
+      $color: var(--content-primary-dark),
+      $background: var(--bg-danger-01),
+      $border: var(--bg-danger-01),
+      $hover-background: var(--bg-danger-02),
+    )
+  }
+
+  &.btn-danger-ghost {
+    @include ol-button-variant(
+      $color: var(--content-danger),
+      $background: var(--bg-light-primary),
+      $hover-background: var(--bg-danger-03),
+    )
+  }
+
+  &.btn-premium {
+    @include ol-button-variant(
+      $color: var(--content-primary-dark),
+      $background: var(--blue-70),
+    );
+    background: var(--premium-gradient);
+  }
+}

+ 8 - 0
services/web/frontend/stylesheets/bootstrap-5/scss/bootstrap-variable-overrides.scss

@@ -0,0 +1,8 @@
+// This file is included before Bootstrap and is used to override Bootstrap
+// default variables and styles
+@import 'bootstrap-variable-overrides/fonts';
+@import 'bootstrap-variable-overrides/buttons';
+
+// This isn't great but is necessary to persuade Bootstrap to render white text
+// on our green buttons
+$min-contrast-ratio: 3;

+ 9 - 0
services/web/frontend/stylesheets/bootstrap-5/scss/bootstrap-variable-overrides/buttons.scss

@@ -0,0 +1,9 @@
+// This file provides overrides for Bootstrap 5's default button-related Sass variables
+
+$btn-font-family: $font-family-sans-serif;
+$btn-font-weight: 700;
+$btn-padding-x: $spacing-06;
+$btn-padding-y: $spacing-02;
+$btn-border-radius: $border-radius-full;
+$btn-border-radius-lg: $border-radius-full;
+$btn-border-radius-sm: $border-radius-full;

+ 11 - 0
services/web/frontend/stylesheets/bootstrap-5/scss/bootstrap-variable-overrides/fonts.scss

@@ -0,0 +1,11 @@
+// This file provides overrides for Bootstrap 5's default font-related Sass variables
+
+$font-family-sans-serif: 'Lato', sans-serif;
+$font-family-serif: 'Merriweather', serif;
+$font-family-monospace: 'Menlo', 'Monaco', 'Consolas', 'Courier New', monospace;
+$font-size-base: $default-font-size;
+$line-height-base: $default-line-height;
+
+// Colours
+$primary: $bg-accent-01;
+$secondary: $bg-light-primary;

+ 30 - 0
services/web/frontend/stylesheets/bootstrap-5/scss/bootstrap.scss

@@ -0,0 +1,30 @@
+// Only include the parts of Bootstrap we need to minimize bundle size
+
+// Include functions first (enabling manipulation of colors, SVGs, calc, etc. in the variable overrides)
+@import 'bootstrap-5/scss/functions';
+
+// Overrides for Bootstrap 5 Sass variables. These have to be included before
+// Bootstrap itself because Bootstrap uses them to create the CSS variables it
+// uses, and in calculations to determine, for example, what colour text to use
+// on a button based on contrast.
+@import 'bootstrap-variable-overrides';
+
+// Include remainder of required Bootstrap stylesheets (including any separate color mode stylesheets)
+@import 'bootstrap-5/scss/variables';
+@import 'bootstrap-5/scss/variables-dark';
+
+// Include remainder of required parts
+@import 'bootstrap-5/scss/maps';
+@import 'bootstrap-5/scss/mixins';
+@import 'bootstrap-5/scss/root';
+
+// Include any other optional parts as needed, including components
+@import 'bootstrap-5/scss/utilities';
+@import 'bootstrap-5/scss/reboot';
+@import 'bootstrap-5/scss/type';
+@import 'bootstrap-5/scss/images';
+@import 'bootstrap-5/scss/containers';
+@import 'bootstrap-5/scss/grid';
+@import 'bootstrap-5/scss/helpers';
+@import 'bootstrap-5/scss/buttons';
+@import 'bootstrap-5/scss/modal';

+ 3 - 1
services/web/package.json

@@ -227,7 +227,7 @@
     "@testing-library/react": "^12.1.5",
     "@testing-library/react": "^12.1.5",
     "@testing-library/react-hooks": "^8.0.1",
     "@testing-library/react-hooks": "^8.0.1",
     "@testing-library/user-event": "^14.4.3",
     "@testing-library/user-event": "^14.4.3",
-    "@types/bootstrap": "^5.2.6",
+    "@types/bootstrap": "^5.2.10",
     "@types/chai": "^4.3.0",
     "@types/chai": "^4.3.0",
     "@types/events": "^3.0.0",
     "@types/events": "^3.0.0",
     "@types/express": "^4.17.13",
     "@types/express": "^4.17.13",
@@ -263,6 +263,7 @@
     "babel-plugin-module-resolver": "^5.0.0",
     "babel-plugin-module-resolver": "^5.0.0",
     "backbone": "^1.3.3",
     "backbone": "^1.3.3",
     "bootstrap": "^3.4.1",
     "bootstrap": "^3.4.1",
+    "bootstrap-5": "npm:bootstrap@^5.3.2",
     "c8": "^7.2.0",
     "c8": "^7.2.0",
     "chai": "^4.3.6",
     "chai": "^4.3.6",
     "chai-as-promised": "^7.1.1",
     "chai-as-promised": "^7.1.1",
@@ -320,6 +321,7 @@
     "qrcode": "^1.4.4",
     "qrcode": "^1.4.4",
     "react": "^17.0.2",
     "react": "^17.0.2",
     "react-bootstrap": "^0.33.1",
     "react-bootstrap": "^0.33.1",
+    "react-bootstrap-5": "npm:react-bootstrap@^2.10.0",
     "react-chartjs-2": "^5.0.1",
     "react-chartjs-2": "^5.0.1",
     "react-color": "^2.19.3",
     "react-color": "^2.19.3",
     "react-dnd": "^16.0.1",
     "react-dnd": "^16.0.1",

+ 46 - 0
services/web/webpack.config.js

@@ -25,6 +25,12 @@ const entryPoints = {
   'main-style': './frontend/stylesheets/main-style.less',
   'main-style': './frontend/stylesheets/main-style.less',
   'main-ieee-style': './frontend/stylesheets/main-ieee-style.less',
   'main-ieee-style': './frontend/stylesheets/main-ieee-style.less',
   'main-light-style': './frontend/stylesheets/main-light-style.less',
   'main-light-style': './frontend/stylesheets/main-light-style.less',
+  'main-style-bootstrap-5':
+    './frontend/stylesheets/bootstrap-5/main-style.scss',
+  'main-ieee-style-bootstrap-5':
+    './frontend/stylesheets/bootstrap-5/main-ieee-style.scss',
+  'main-light-style-bootstrap-5':
+    './frontend/stylesheets/bootstrap-5/main-light-style.scss',
 }
 }
 
 
 // Add entrypoints for each "page"
 // Add entrypoints for each "page"
@@ -167,6 +173,46 @@ module.exports = {
           { loader: 'less-loader' },
           { loader: 'less-loader' },
         ],
         ],
       },
       },
+      {
+        // Pass Sass files through sass-loader/css-loader/mini-css-extract-
+        // plugin (note: run in reverse order)
+        test: /\.s[ac]ss$/,
+        use: [
+          // Allows the CSS to be extracted to a separate .css file
+          { loader: MiniCssExtractPlugin.loader },
+          // Resolves any CSS dependencies (e.g. url())
+          { loader: 'css-loader' },
+          // Resolve relative paths sensibly in SASS
+          { loader: 'resolve-url-loader' },
+          {
+            // Runs autoprefixer on CSS via postcss
+            loader: 'postcss-loader',
+            options: {
+              postcssOptions: {
+                plugins: ['autoprefixer'],
+              },
+            },
+          },
+          // Compile Sass off the main event loop
+          {
+            loader: 'thread-loader',
+            options: {
+              // keep workers alive for dev-server, and shut them down when not needed
+              poolTimeout:
+                process.env.NODE_ENV === 'development' ? 10 * 60 * 1000 : 500,
+              // bring up more workers after they timed out
+              poolRespawn: true,
+              // limit concurrency (one per entrypoint and let the small includes queue up)
+              workers: 6,
+            },
+          },
+          // Compiles Sass to CSS
+          {
+            loader: 'sass-loader',
+            options: { sourceMap: true }, // sourceMap: true is required for resolve-url-loader
+          },
+        ],
+      },
       {
       {
         // Pass CSS files through css-loader & mini-css-extract-plugin (note: run in reverse order)
         // Pass CSS files through css-loader & mini-css-extract-plugin (note: run in reverse order)
         test: /\.css$/i,
         test: /\.css$/i,