Explorar o código

Merge pull request #20824 from overleaf/ii-bs5-share-modal

[web] BS5 share modal

GitOrigin-RevId: 40a33e06eab720b568d31aefa021682535b6934e
ilkin-overleaf hai 1 ano
pai
achega
4f838ccacf
Modificáronse 44 ficheiros con 1233 adicións e 604 borrados
  1. 30 10
      services/web/frontend/js/features/share-project-modal/components/add-collaborators-upgrade.tsx
  2. 17 13
      services/web/frontend/js/features/share-project-modal/components/add-collaborators.jsx
  3. 47 30
      services/web/frontend/js/features/share-project-modal/components/edit-member.jsx
  4. 32 21
      services/web/frontend/js/features/share-project-modal/components/invite.jsx
  5. 39 34
      services/web/frontend/js/features/share-project-modal/components/link-sharing.jsx
  6. 7 6
      services/web/frontend/js/features/share-project-modal/components/owner-info.jsx
  7. 9 9
      services/web/frontend/js/features/share-project-modal/components/restricted-link-sharing/access-levels-changed.tsx
  8. 9 9
      services/web/frontend/js/features/share-project-modal/components/restricted-link-sharing/add-collaborators-upgrade.tsx
  9. 17 13
      services/web/frontend/js/features/share-project-modal/components/restricted-link-sharing/add-collaborators.jsx
  10. 8 9
      services/web/frontend/js/features/share-project-modal/components/restricted-link-sharing/collaborators-limit-upgrade.tsx
  11. 61 23
      services/web/frontend/js/features/share-project-modal/components/restricted-link-sharing/edit-member.tsx
  12. 20 15
      services/web/frontend/js/features/share-project-modal/components/restricted-link-sharing/editor-over-limit-modal-content.tsx
  13. 3 3
      services/web/frontend/js/features/share-project-modal/components/restricted-link-sharing/editor-over-limit-modal.tsx
  14. 32 21
      services/web/frontend/js/features/share-project-modal/components/restricted-link-sharing/invite.jsx
  15. 69 59
      services/web/frontend/js/features/share-project-modal/components/restricted-link-sharing/link-sharing.jsx
  16. 14 8
      services/web/frontend/js/features/share-project-modal/components/restricted-link-sharing/owner-info.jsx
  17. 173 100
      services/web/frontend/js/features/share-project-modal/components/restricted-link-sharing/select-collaborators.jsx
  18. 6 5
      services/web/frontend/js/features/share-project-modal/components/restricted-link-sharing/send-invites-notice.tsx
  19. 3 3
      services/web/frontend/js/features/share-project-modal/components/restricted-link-sharing/send-invites.jsx
  20. 50 31
      services/web/frontend/js/features/share-project-modal/components/restricted-link-sharing/share-project-modal-content.tsx
  21. 46 36
      services/web/frontend/js/features/share-project-modal/components/restricted-link-sharing/transfer-ownership-modal.jsx
  22. 14 8
      services/web/frontend/js/features/share-project-modal/components/restricted-link-sharing/view-member.jsx
  23. 20 15
      services/web/frontend/js/features/share-project-modal/components/restricted-link-sharing/view-only-access-modal-content.tsx
  24. 3 3
      services/web/frontend/js/features/share-project-modal/components/restricted-link-sharing/view-only-access-modal.tsx
  25. 100 25
      services/web/frontend/js/features/share-project-modal/components/select-collaborators.jsx
  26. 6 5
      services/web/frontend/js/features/share-project-modal/components/send-invites-notice.jsx
  27. 3 3
      services/web/frontend/js/features/share-project-modal/components/send-invites.jsx
  28. 51 32
      services/web/frontend/js/features/share-project-modal/components/share-project-modal-content.tsx
  29. 46 36
      services/web/frontend/js/features/share-project-modal/components/transfer-ownership-modal.jsx
  30. 7 6
      services/web/frontend/js/features/share-project-modal/components/view-member.jsx
  31. 7 3
      services/web/frontend/js/features/ui/components/bootstrap-5/icon-button.tsx
  32. 1 0
      services/web/frontend/js/features/ui/components/ol/ol-button.tsx
  33. 2 2
      services/web/frontend/js/features/ui/components/ol/ol-col.tsx
  34. 1 0
      services/web/frontend/js/features/ui/components/types/button-props.ts
  35. 1 0
      services/web/frontend/js/shared/components/copy-to-clipboard.tsx
  36. 1 7
      services/web/frontend/stylesheets/app/editor/share.less
  37. 6 0
      services/web/frontend/stylesheets/bootstrap-5/base/base.scss
  38. 1 0
      services/web/frontend/stylesheets/bootstrap-5/components/all.scss
  39. 1 1
      services/web/frontend/stylesheets/bootstrap-5/components/button.scss
  40. 5 0
      services/web/frontend/stylesheets/bootstrap-5/components/form.scss
  41. 7 0
      services/web/frontend/stylesheets/bootstrap-5/components/loading-spinner.scss
  42. 2 0
      services/web/frontend/stylesheets/bootstrap-5/pages/all.scss
  43. 188 0
      services/web/frontend/stylesheets/bootstrap-5/pages/editor/share.scss
  44. 68 0
      services/web/frontend/stylesheets/bootstrap-5/pages/editor/tags-input.scss

+ 30 - 10
services/web/frontend/js/features/share-project-modal/components/add-collaborators-upgrade.tsx

@@ -1,11 +1,13 @@
 import { useState } from 'react'
 import { useTranslation } from 'react-i18next'
-import { Button } from 'react-bootstrap'
 import { useUserContext } from '../../../shared/context/user-context'
 import { upgradePlan } from '../../../main/account-upgrade'
 import StartFreeTrialButton from '../../../shared/components/start-free-trial-button'
 import Icon from '../../../shared/components/icon'
 import { useFeatureFlag } from '../../../shared/context/split-test-context'
+import BootstrapVersionSwitcher from '@/features/ui/components/bootstrap-5/bootstrap-version-switcher'
+import MaterialIcon from '@/shared/components/material-icon'
+import OLButton from '@/features/ui/components/ol/ol-button'
 
 export default function AddCollaboratorsUpgrade() {
   const { t } = useTranslation()
@@ -21,34 +23,52 @@ export default function AddCollaboratorsUpgrade() {
       </p>
       <ul className="list-unstyled">
         <li>
-          <Icon type="check" />
+          <BootstrapVersionSwitcher
+            bs3={<Icon type="check" />}
+            bs5={<MaterialIcon type="check" className="align-text-bottom" />}
+          />
           &nbsp;
           {t('unlimited_projects')}
         </li>
         <li>
-          <Icon type="check" />
+          <BootstrapVersionSwitcher
+            bs3={<Icon type="check" />}
+            bs5={<MaterialIcon type="check" className="align-text-bottom" />}
+          />
           &nbsp;
           {t('collabs_per_proj', {
             collabcount: 'Multiple',
           })}
         </li>
         <li>
-          <Icon type="check" />
+          <BootstrapVersionSwitcher
+            bs3={<Icon type="check" />}
+            bs5={<MaterialIcon type="check" className="align-text-bottom" />}
+          />
           &nbsp;
           {t('full_doc_history')}
         </li>
         <li>
-          <Icon type="check" />
+          <BootstrapVersionSwitcher
+            bs3={<Icon type="check" />}
+            bs5={<MaterialIcon type="check" className="align-text-bottom" />}
+          />
           &nbsp;
           {t('sync_to_dropbox')}
         </li>
         <li>
-          <Icon type="check" />
+          <BootstrapVersionSwitcher
+            bs3={<Icon type="check" />}
+            bs5={<MaterialIcon type="check" className="align-text-bottom" />}
+          />
           &nbsp;
           {t('sync_to_github')}
         </li>
         <li>
-          <Icon type="check" />
+          <BootstrapVersionSwitcher
+            bs3={<Icon type="check" />}
+            bs5={<MaterialIcon type="check" className="align-text-bottom" />}
+          />
           &nbsp;
           {t('compile_larger_projects')}
         </li>
@@ -65,15 +85,15 @@ export default function AddCollaboratorsUpgrade() {
               : t('start_free_trial')}
           </StartFreeTrialButton>
         ) : (
-          <Button
-            bsStyle="primary"
+          <OLButton
+            variant="primary"
             onClick={() => {
               upgradePlan('project-sharing')
               setStartedFreeTrial(true)
             }}
           >
             {t('upgrade')}
-          </Button>
+          </OLButton>
         )}
       </p>
       {startedFreeTrial && (

+ 17 - 13
services/web/frontend/js/features/share-project-modal/components/add-collaborators.jsx

@@ -1,6 +1,5 @@
 import { useState, useMemo, useCallback } from 'react'
 import { useTranslation } from 'react-i18next'
-import { Form, FormGroup, FormControl, Button } from 'react-bootstrap'
 import { useMultipleSelection } from 'downshift'
 import { useShareProjectContext } from './share-project-modal'
 import SelectCollaborators from './select-collaborators'
@@ -10,6 +9,10 @@ import useIsMounted from '../../../shared/hooks/use-is-mounted'
 import { useProjectContext } from '../../../shared/context/project-context'
 import { sendMB } from '../../../infrastructure/event-tracking'
 import ClickableElementEnhancer from '@/shared/components/clickable-element-enhancer'
+import OLForm from '@/features/ui/components/ol/ol-form'
+import OLFormGroup from '@/features/ui/components/ol/ol-form-group'
+import OLFormSelect from '@/features/ui/components/ol/ol-form-select'
+import OLButton from '@/features/ui/components/ol/ol-button'
 
 export default function AddCollaborators() {
   const [privileges, setPrivileges] = useState('readAndWrite')
@@ -139,38 +142,39 @@ export default function AddCollaborators() {
   ])
 
   return (
-    <Form>
-      <FormGroup>
+    <OLForm>
+      <OLFormGroup>
         <SelectCollaborators
           loading={!nonMemberContacts}
           options={nonMemberContacts || []}
           placeholder="joe@example.com, sue@example.com, …"
           multipleSelectionProps={multipleSelectionProps}
         />
-      </FormGroup>
+      </OLFormGroup>
 
-      <FormGroup>
+      <OLFormGroup>
         <div className="pull-right">
-          <FormControl
-            componentClass="select"
+          <OLFormSelect
             className="privileges"
-            bsSize="sm"
             value={privileges}
             onChange={event => setPrivileges(event.target.value)}
+            bs3Props={{
+              bsSize: 'sm',
+            }}
           >
             <option value="readAndWrite">{t('can_edit')}</option>
             <option value="readOnly">{t('read_only')}</option>
-          </FormControl>
+          </OLFormSelect>
           <span>&nbsp;&nbsp;</span>
           <ClickableElementEnhancer
-            as={Button}
+            as={OLButton}
             onClick={handleSubmit}
-            bsStyle="primary"
+            variant="primary"
           >
             {t('share')}
           </ClickableElementEnhancer>
         </div>
-      </FormGroup>
-    </Form>
+      </OLFormGroup>
+    </OLForm>
   )
 }

+ 47 - 30
services/web/frontend/js/features/share-project-modal/components/edit-member.jsx

@@ -4,11 +4,17 @@ import { useTranslation } from 'react-i18next'
 import { useShareProjectContext } from './share-project-modal'
 import TransferOwnershipModal from './transfer-ownership-modal'
 import { removeMemberFromProject, updateMember } from '../utils/api'
-import { Button, Col, Form, FormControl, FormGroup } from 'react-bootstrap'
-import Tooltip from '../../../shared/components/tooltip'
 import Icon from '../../../shared/components/icon'
 import { useProjectContext } from '../../../shared/context/project-context'
 import { sendMB } from '../../../infrastructure/event-tracking'
+import OLFormGroup from '@/features/ui/components/ol/ol-form-group'
+import OLCol from '@/features/ui/components/ol/ol-col'
+import OLFormSelect from '@/features/ui/components/ol/ol-form-select'
+import OLTooltip from '@/features/ui/components/ol/ol-tooltip'
+import OLButton from '@/features/ui/components/ol/ol-button'
+import MaterialIcon from '@/shared/components/material-icon'
+import BootstrapVersionSwitcher from '@/features/ui/components/bootstrap-5/bootstrap-version-switcher'
+import { bsVersion } from '@/features/utils/bootstrap-5'
 
 export default function EditMember({ member }) {
   const [privileges, setPrivileges] = useState(member.privileges)
@@ -53,20 +59,24 @@ export default function EditMember({ member }) {
   }
 
   return (
-    <Form horizontal id="share-project-form" onSubmit={handleSubmit}>
-      <FormGroup className="project-member row">
-        <Col xs={7}>
-          <FormControl.Static>{member.email}</FormControl.Static>
-        </Col>
+    <form
+      id="share-project-form"
+      className={bsVersion({ bs3: 'form-horizontal' })}
+      onSubmit={handleSubmit}
+    >
+      <OLFormGroup className="project-member row">
+        <OLCol xs={7} className={bsVersion({ bs3: 'pt-1', bs5: 'pt-2' })}>
+          {member.email}
+        </OLCol>
 
-        <Col xs={3}>
+        <OLCol xs={3}>
           <SelectPrivilege
             value={privileges}
             handleChange={event => setPrivileges(event.target.value)}
           />
-        </Col>
+        </OLCol>
 
-        <Col xs={2}>
+        <OLCol xs={2}>
           {privileges === member.privileges ? (
             <RemoveMemberAction member={member} />
           ) : (
@@ -74,9 +84,9 @@ export default function EditMember({ member }) {
               handleReset={() => setPrivileges(member.privileges)}
             />
           )}
-        </Col>
-      </FormGroup>
-    </Form>
+        </OLCol>
+      </OLFormGroup>
+    </form>
   )
 }
 EditMember.propTypes = {
@@ -91,17 +101,18 @@ function SelectPrivilege({ value, handleChange }) {
   const { t } = useTranslation()
 
   return (
-    <FormControl
-      componentClass="select"
+    <OLFormSelect
       className="privileges"
-      bsSize="sm"
       value={value}
       onChange={handleChange}
+      bs3Props={{
+        bsSize: 'sm',
+      }}
     >
       <option value="owner">{t('owner')}</option>
       <option value="readAndWrite">{t('can_edit')}</option>
       <option value="readOnly">{t('read_only')}</option>
-    </FormControl>
+    </OLFormSelect>
   )
 }
 
@@ -134,23 +145,25 @@ function RemoveMemberAction({ member }) {
   }
 
   return (
-    <FormControl.Static className="text-center">
-      <Tooltip
+    <div className="text-center">
+      <OLTooltip
         id="remove-collaborator"
         description={t('remove_collaborator')}
         overlayProps={{ placement: 'bottom' }}
       >
-        <Button
-          type="button"
-          bsStyle="link"
+        <OLButton
+          variant="link"
           onClick={handleClick}
           className="remove-button"
           aria-label={t('remove_collaborator')}
         >
-          <Icon type="times" />
-        </Button>
-      </Tooltip>
-    </FormControl.Static>
+          <BootstrapVersionSwitcher
+            bs3={<Icon type="times" />}
+            bs5={<MaterialIcon type="clear" />}
+          />
+        </OLButton>
+      </OLTooltip>
+    </div>
   )
 }
 
@@ -167,15 +180,19 @@ function ChangePrivilegesActions({ handleReset }) {
 
   return (
     <div className="text-center">
-      <Button type="submit" bsSize="sm" bsStyle="primary">
+      <OLButton type="submit" size="sm" variant="primary">
         {t('change_or_cancel-change')}
-      </Button>
+      </OLButton>
       <div className="text-sm">
         {t('change_or_cancel-or')}
         &nbsp;
-        <Button type="button" className="btn-inline-link" onClick={handleReset}>
+        <OLButton
+          variant="link"
+          className="btn-inline-link"
+          onClick={handleReset}
+        >
           {t('change_or_cancel-cancel')}
-        </Button>
+        </OLButton>
       </div>
     </div>
   )

+ 32 - 21
services/web/frontend/js/features/share-project-modal/components/invite.jsx

@@ -2,19 +2,25 @@ import { useCallback } from 'react'
 import PropTypes from 'prop-types'
 import { useShareProjectContext } from './share-project-modal'
 import Icon from '../../../shared/components/icon'
-import { Button, Col, Row } from 'react-bootstrap'
-import Tooltip from '../../../shared/components/tooltip'
 import { useTranslation } from 'react-i18next'
 import MemberPrivileges from './member-privileges'
 import { resendInvite, revokeInvite } from '../utils/api'
 import { useProjectContext } from '../../../shared/context/project-context'
 import { sendMB } from '../../../infrastructure/event-tracking'
+import OLRow from '@/features/ui/components/ol/ol-row'
+import OLCol from '@/features/ui/components/ol/ol-col'
+import OLTooltip from '@/features/ui/components/ol/ol-tooltip'
+import OLButton from '@/features/ui/components/ol/ol-button'
+import MaterialIcon from '@/shared/components/material-icon'
+import BootstrapVersionSwitcher from '@/features/ui/components/bootstrap-5/bootstrap-version-switcher'
+import { bsVersion } from '@/features/utils/bootstrap-5'
+import classnames from 'classnames'
 
 export default function Invite({ invite, isProjectOwner }) {
   const { t } = useTranslation()
   return (
-    <Row className="project-invite">
-      <Col xs={7}>
+    <OLRow className="project-invite">
+      <OLCol xs={7}>
         <div>{invite.email}</div>
 
         <div className="small">
@@ -22,18 +28,18 @@ export default function Invite({ invite, isProjectOwner }) {
           .&nbsp;
           {isProjectOwner && <ResendInvite invite={invite} />}
         </div>
-      </Col>
+      </OLCol>
 
-      <Col xs={3} className="text-left">
+      <OLCol xs={3} className="text-start">
         <MemberPrivileges privileges={invite.privileges} />
-      </Col>
+      </OLCol>
 
       {isProjectOwner && (
-        <Col xs={2} className="text-center">
+        <OLCol xs={2} className="text-center">
           <RevokeInvite invite={invite} />
-        </Col>
+        </OLCol>
       )}
-    </Row>
+    </OLRow>
   )
 }
 
@@ -71,15 +77,15 @@ function ResendInvite({ invite }) {
   )
 
   return (
-    <Button
-      bsStyle="link"
+    <OLButton
+      variant="link"
       className="btn-inline-link"
       onClick={handleClick}
       disabled={inFlight}
       // ref={buttonRef}
     >
       {t('resend')}
-    </Button>
+    </OLButton>
   )
 }
 
@@ -109,21 +115,26 @@ function RevokeInvite({ invite }) {
   }
 
   return (
-    <Tooltip
+    <OLTooltip
       id="revoke-invite"
       description={t('revoke_invite')}
       overlayProps={{ placement: 'bottom' }}
     >
-      <Button
-        type="button"
-        bsStyle="link"
+      <OLButton
+        variant="link"
         onClick={handleClick}
         aria-label={t('revoke')}
-        className="btn-inline-link"
+        className={classnames(
+          'btn-inline-link',
+          bsVersion({ bs5: 'text-decoration-none' })
+        )}
       >
-        <Icon type="times" />
-      </Button>
-    </Tooltip>
+        <BootstrapVersionSwitcher
+          bs3={<Icon type="times" />}
+          bs5={<MaterialIcon type="clear" />}
+        />
+      </OLButton>
+    </OLTooltip>
   )
 }
 

+ 39 - 34
services/web/frontend/js/features/share-project-modal/components/link-sharing.jsx

@@ -1,8 +1,6 @@
 import { useCallback, useState, useEffect } from 'react'
 import PropTypes from 'prop-types'
-import { Button, Col, Row } from 'react-bootstrap'
 import { useTranslation } from 'react-i18next'
-import Tooltip from '../../../shared/components/tooltip'
 import Icon from '../../../shared/components/icon'
 import { useShareProjectContext } from './share-project-modal'
 import { setProjectAccessLevel } from '../utils/api'
@@ -15,6 +13,12 @@ import { getJSON } from '../../../infrastructure/fetch-json'
 import useAbortController from '../../../shared/hooks/use-abort-controller'
 import { debugConsole } from '@/utils/debugging'
 import getMeta from '@/utils/meta'
+import OLRow from '@/features/ui/components/ol/ol-row'
+import OLCol from '@/features/ui/components/ol/ol-col'
+import OLButton from '@/features/ui/components/ol/ol-button'
+import OLTooltip from '@/features/ui/components/ol/ol-tooltip'
+import BootstrapVersionSwitcher from '@/features/ui/components/bootstrap-5/bootstrap-version-switcher'
+import MaterialIcon from '@/shared/components/material-icon'
 
 export default function LinkSharing() {
   const [inflight, setInflight] = useState(false)
@@ -84,13 +88,12 @@ export default function LinkSharing() {
 function PrivateSharing({ setAccessLevel, inflight, projectId }) {
   const { t } = useTranslation()
   return (
-    <Row className="public-access-level">
-      <Col xs={12} className="text-center">
+    <OLRow className="public-access-level">
+      <OLCol xs={12} className="text-center">
         {t('link_sharing_is_off')}
         <span>&nbsp;&nbsp;</span>
-        <Button
-          type="button"
-          bsStyle="link"
+        <OLButton
+          variant="link"
           className="btn-inline-link"
           onClick={() => {
             setAccessLevel('tokenBased')
@@ -99,11 +102,11 @@ function PrivateSharing({ setAccessLevel, inflight, projectId }) {
           disabled={inflight}
         >
           {t('turn_on_link_sharing')}
-        </Button>
+        </OLButton>
         <span>&nbsp;&nbsp;</span>
         <LinkSharingInfo />
-      </Col>
-    </Row>
+      </OLCol>
+    </OLRow>
   )
 }
 
@@ -128,22 +131,22 @@ function TokenBasedSharing({ setAccessLevel, inflight }) {
   }, [projectId, signal])
 
   return (
-    <Row className="public-access-level">
-      <Col xs={12} className="text-center">
+    <OLRow className="public-access-level">
+      <OLCol xs={12} className="text-center">
         <strong>{t('link_sharing_is_on')}</strong>
         <span>&nbsp;&nbsp;</span>
-        <Button
-          bsStyle="link"
+        <OLButton
+          variant="link"
           className="btn-inline-link"
           onClick={() => setAccessLevel('private')}
           disabled={inflight}
         >
           {t('turn_off_link_sharing')}
-        </Button>
+        </OLButton>
         <span>&nbsp;&nbsp;</span>
         <LinkSharingInfo />
-      </Col>
-      <Col xs={12} className="access-token-display-area">
+      </OLCol>
+      <OLCol xs={12} className="access-token-display-area">
         <div className="access-token-wrapper">
           <strong>{t('anyone_with_link_can_edit')}</strong>
           <AccessToken
@@ -162,8 +165,8 @@ function TokenBasedSharing({ setAccessLevel, inflight }) {
             tooltipId="tooltip-copy-link-ro"
           />
         </div>
-      </Col>
-    </Row>
+      </OLCol>
+    </OLRow>
   )
 }
 
@@ -176,26 +179,25 @@ function LegacySharing({ accessLevel, setAccessLevel, inflight }) {
   const { t } = useTranslation()
 
   return (
-    <Row className="public-access-level">
-      <Col xs={12} className="text-center">
+    <OLRow className="public-access-level">
+      <OLCol xs={12} className="text-center">
         <strong>
           {accessLevel === 'readAndWrite' && t('this_project_is_public')}
           {accessLevel === 'readOnly' && t('this_project_is_public_read_only')}
         </strong>
         <span>&nbsp;&nbsp;</span>
-        <Button
-          type="button"
-          bsStyle="link"
+        <OLButton
+          variant="link"
           className="btn-inline-link"
           onClick={() => setAccessLevel('private')}
           disabled={inflight}
         >
           {t('make_private')}
-        </Button>
+        </OLButton>
         <span>&nbsp;&nbsp;</span>
         <LinkSharingInfo />
-      </Col>
-    </Row>
+      </OLCol>
+    </OLRow>
   )
 }
 
@@ -220,8 +222,8 @@ export function ReadOnlyTokenLink() {
   }, [projectId, signal])
 
   return (
-    <Row className="public-access-level">
-      <Col xs={12} className="access-token-display-area">
+    <OLRow className="public-access-level">
+      <OLCol className="access-token-display-area">
         <div className="access-token-wrapper">
           <strong>{t('anyone_with_link_can_view')}</strong>
           <AccessToken
@@ -231,8 +233,8 @@ export function ReadOnlyTokenLink() {
             tooltipId="tooltip-copy-link-ro"
           />
         </div>
-      </Col>
-    </Row>
+      </OLCol>
+    </OLRow>
   )
 }
 
@@ -275,7 +277,7 @@ function LinkSharingInfo() {
   const { t } = useTranslation()
 
   return (
-    <Tooltip
+    <OLTooltip
       id="link-sharing-info"
       description={t('learn_more_about_link_sharing')}
     >
@@ -284,8 +286,11 @@ function LinkSharingInfo() {
         target="_blank"
         rel="noopener"
       >
-        <Icon type="question-circle" />
+        <BootstrapVersionSwitcher
+          bs3={<Icon type="question-circle" />}
+          bs5={<MaterialIcon type="help" className="align-middle" />}
+        />
       </a>
-    </Tooltip>
+    </OLTooltip>
   )
 }

+ 7 - 6
services/web/frontend/js/features/share-project-modal/components/owner-info.jsx

@@ -1,17 +1,18 @@
 import { useProjectContext } from '../../../shared/context/project-context'
-import { Col, Row } from 'react-bootstrap'
 import { useTranslation } from 'react-i18next'
+import OLRow from '@/features/ui/components/ol/ol-row'
+import OLCol from '@/features/ui/components/ol/ol-col'
 
 export default function OwnerInfo() {
   const { t } = useTranslation()
   const { owner } = useProjectContext()
 
   return (
-    <Row className="project-member">
-      <Col xs={7}>{owner?.email}</Col>
-      <Col xs={3} className="text-left">
+    <OLRow className="project-member">
+      <OLCol xs={7}>{owner?.email}</OLCol>
+      <OLCol xs={3} className="text-start">
         {t('owner')}
-      </Col>
-    </Row>
+      </OLCol>
+    </OLRow>
   )
 }

+ 9 - 9
services/web/frontend/js/features/share-project-modal/components/restricted-link-sharing/access-levels-changed.tsx

@@ -1,11 +1,11 @@
 import { useTranslation } from 'react-i18next'
-import { Button } from 'react-bootstrap'
 import Notification from '@/shared/components/notification'
 import { upgradePlan } from '../../../../main/account-upgrade'
 import { useProjectContext } from '@/shared/context/project-context'
 import { useUserContext } from '@/shared/context/user-context'
 import { sendMB } from '@/infrastructure/event-tracking'
 import StartFreeTrialButton from '@/shared/components/start-free-trial-button'
+import OLButton from '@/features/ui/components/ol/ol-button'
 
 type AccessLevelsChangedProps = {
   somePendingEditorsResolved: boolean
@@ -50,20 +50,20 @@ export default function AccessLevelsChanged({
                 {t('upgrade')}
               </StartFreeTrialButton>
             ) : (
-              <Button
-                bsSize="sm"
-                className="btn-secondary"
+              <OLButton
+                variant="secondary"
+                size="sm"
                 onClick={() => {
                   upgradePlan('project-sharing')
                 }}
               >
                 {t('upgrade')}
-              </Button>
+              </OLButton>
             )}
-            <Button
+            <OLButton
+              variant="link"
+              size="sm"
               href="https://www.overleaf.com/blog/changes-to-project-sharing"
-              bsSize="sm"
-              className="btn-link"
               target="_blank"
               rel="noreferrer"
               onClick={() => {
@@ -75,7 +75,7 @@ export default function AccessLevelsChanged({
               }}
             >
               {t('read_more')}
-            </Button>
+            </OLButton>
           </div>
         }
       />

+ 9 - 9
services/web/frontend/js/features/share-project-modal/components/restricted-link-sharing/add-collaborators-upgrade.tsx

@@ -1,5 +1,4 @@
 import { useTranslation } from 'react-i18next'
-import { Button } from 'react-bootstrap'
 import Notification from '@/shared/components/notification'
 import { upgradePlan } from '../../../../main/account-upgrade'
 import { linkSharingEnforcementDate } from '../../utils/link-sharing'
@@ -7,6 +6,7 @@ import { useProjectContext } from '@/shared/context/project-context'
 import { useUserContext } from '@/shared/context/user-context'
 import { sendMB } from '@/infrastructure/event-tracking'
 import StartFreeTrialButton from '@/shared/components/start-free-trial-button'
+import OLButton from '@/features/ui/components/ol/ol-button'
 
 export default function AddCollaboratorsUpgrade() {
   const { t } = useTranslation()
@@ -40,20 +40,20 @@ export default function AddCollaboratorsUpgrade() {
                 {t('upgrade')}
               </StartFreeTrialButton>
             ) : (
-              <Button
-                bsSize="sm"
-                className="btn-secondary"
+              <OLButton
+                variant="secondary"
+                size="sm"
                 onClick={() => {
                   upgradePlan('project-sharing')
                 }}
               >
                 {t('upgrade')}
-              </Button>
+              </OLButton>
             )}
-            <Button
+            <OLButton
+              variant="link"
+              size="sm"
               href="https://www.overleaf.com/blog/changes-to-project-sharing"
-              bsSize="sm"
-              className="btn-link"
               target="_blank"
               rel="noreferrer"
               onClick={() => {
@@ -65,7 +65,7 @@ export default function AddCollaboratorsUpgrade() {
               }}
             >
               {t('read_more')}
-            </Button>
+            </OLButton>
           </div>
         }
       />

+ 17 - 13
services/web/frontend/js/features/share-project-modal/components/restricted-link-sharing/add-collaborators.jsx

@@ -1,6 +1,5 @@
 import { useEffect, useState, useMemo, useCallback } from 'react'
 import { useTranslation } from 'react-i18next'
-import { Form, FormGroup, FormControl, Button } from 'react-bootstrap'
 import { useMultipleSelection } from 'downshift'
 import { useShareProjectContext } from './share-project-modal'
 import SelectCollaborators from './select-collaborators'
@@ -11,6 +10,10 @@ import { useProjectContext } from '@/shared/context/project-context'
 import { sendMB } from '@/infrastructure/event-tracking'
 import ClickableElementEnhancer from '@/shared/components/clickable-element-enhancer'
 import PropTypes from 'prop-types'
+import OLForm from '@/features/ui/components/ol/ol-form'
+import OLFormGroup from '@/features/ui/components/ol/ol-form-group'
+import OLFormSelect from '@/features/ui/components/ol/ol-form-select'
+import OLButton from '@/features/ui/components/ol/ol-button'
 
 export default function AddCollaborators({ readOnly }) {
   const [privileges, setPrivileges] = useState('readAndWrite')
@@ -146,41 +149,42 @@ export default function AddCollaborators({ readOnly }) {
   ])
 
   return (
-    <Form className="add-collabs">
-      <FormGroup>
+    <OLForm className="add-collabs">
+      <OLFormGroup>
         <SelectCollaborators
           loading={!nonMemberContacts}
           options={nonMemberContacts || []}
           placeholder="Email, comma separated"
           multipleSelectionProps={multipleSelectionProps}
         />
-      </FormGroup>
+      </OLFormGroup>
 
-      <FormGroup>
+      <OLFormGroup>
         <div className="pull-right">
-          <FormControl
-            componentClass="select"
+          <OLFormSelect
             className="privileges"
-            bsSize="sm"
             value={privileges}
             onChange={event => setPrivileges(event.target.value)}
+            bs3Props={{
+              bsSize: 'sm',
+            }}
           >
             <option disabled={readOnly} value="readAndWrite">
               {t('can_edit')}
             </option>
             <option value="readOnly">{t('can_view')}</option>
-          </FormControl>
+          </OLFormSelect>
           <span>&nbsp;&nbsp;</span>
           <ClickableElementEnhancer
-            as={Button}
+            as={OLButton}
             onClick={handleSubmit}
-            bsStyle="primary"
+            variant="primary"
           >
             {t('invite')}
           </ClickableElementEnhancer>
         </div>
-      </FormGroup>
-    </Form>
+      </OLFormGroup>
+    </OLForm>
   )
 }
 

+ 8 - 9
services/web/frontend/js/features/share-project-modal/components/restricted-link-sharing/collaborators-limit-upgrade.tsx

@@ -1,4 +1,3 @@
-import { Button } from 'react-bootstrap'
 import { useTranslation } from 'react-i18next'
 import Notification from '@/shared/components/notification'
 import { upgradePlan } from '@/main/account-upgrade'
@@ -6,6 +5,7 @@ import { useProjectContext } from '@/shared/context/project-context'
 import { useUserContext } from '@/shared/context/user-context'
 import StartFreeTrialButton from '@/shared/components/start-free-trial-button'
 import getMeta from '@/utils/meta'
+import OLButton from '@/features/ui/components/ol/ol-button'
 
 export default function CollaboratorsLimitUpgrade() {
   const { t } = useTranslation()
@@ -44,15 +44,14 @@ export default function CollaboratorsLimitUpgrade() {
                 {t('upgrade')}
               </StartFreeTrialButton>
             ) : (
-              <Button
-                bsSize="medium"
-                className="btn-premium"
+              <OLButton
+                variant="premium"
                 onClick={() => {
                   upgradePlan('project-sharing')
                 }}
               >
                 {t('upgrade')}
-              </Button>
+              </OLButton>
             )
           }
         />
@@ -78,15 +77,15 @@ export default function CollaboratorsLimitUpgrade() {
                 {t('upgrade')}
               </StartFreeTrialButton>
             ) : (
-              <Button
-                bsSize="sm"
-                className="btn-secondary"
+              <OLButton
+                size="sm"
+                variant="secondary"
                 onClick={() => {
                   upgradePlan('project-sharing')
                 }}
               >
                 {t('upgrade')}
-              </Button>
+              </OLButton>
             )
           }
         />

+ 61 - 23
services/web/frontend/js/features/share-project-modal/components/restricted-link-sharing/edit-member.tsx

@@ -1,10 +1,9 @@
-import { useState, useEffect, useMemo, MouseEventHandler } from 'react'
+import { useState, useEffect, useMemo } from 'react'
 import PropTypes from 'prop-types'
 import { useTranslation } from 'react-i18next'
 import { useShareProjectContext } from './share-project-modal'
 import TransferOwnershipModal from './transfer-ownership-modal'
 import { removeMemberFromProject, updateMember } from '../../utils/api'
-import { Button, Col, Form, FormGroup } from 'react-bootstrap'
 import Icon from '@/shared/components/icon'
 import { useProjectContext } from '@/shared/context/project-context'
 import { sendMB } from '@/infrastructure/event-tracking'
@@ -12,6 +11,13 @@ import { Select } from '@/shared/components/select'
 import type { ProjectContextMember } from '@/shared/context/types/project-context'
 import { PermissionsLevel } from '@/features/ide-react/types/permissions'
 import { linkSharingEnforcementDate } from '../../utils/link-sharing'
+import OLButton from '@/features/ui/components/ol/ol-button'
+import OLFormGroup from '@/features/ui/components/ol/ol-form-group'
+import OLCol from '@/features/ui/components/ol/ol-col'
+import MaterialIcon from '@/shared/components/material-icon'
+import BootstrapVersionSwitcher from '@/features/ui/components/bootstrap-5/bootstrap-version-switcher'
+import { bsVersion } from '@/features/utils/bootstrap-5'
+import classnames from 'classnames'
 
 type PermissionsOption = PermissionsLevel | 'removeAccess' | 'downgraded'
 
@@ -115,22 +121,44 @@ export default function EditMember({
   }
 
   return (
-    <Form
-      horizontal
+    <form
+      className={bsVersion({ bs3: 'form-horizontal' })}
       id="share-project-form"
       onSubmit={e => {
         e.preventDefault()
         commitPrivilegeChange(privileges)
       }}
     >
-      <FormGroup className="project-member">
-        <Col xs={7}>
+      <OLFormGroup
+        className={classnames('project-member', bsVersion({ bs5: 'row' }))}
+      >
+        <OLCol xs={7}>
           <div className="project-member-email-icon">
-            <Icon
-              type={
-                shouldWarnMember() || member.pendingEditor ? 'warning' : 'user'
+            <BootstrapVersionSwitcher
+              bs3={
+                <Icon
+                  type={
+                    shouldWarnMember() || member.pendingEditor
+                      ? 'warning'
+                      : 'user'
+                  }
+                  fw
+                />
+              }
+              bs5={
+                <MaterialIcon
+                  type={
+                    shouldWarnMember() || member.pendingEditor
+                      ? 'warning'
+                      : 'person'
+                  }
+                  className={
+                    shouldWarnMember() || member.pendingEditor
+                      ? 'text-warning'
+                      : undefined
+                  }
+                />
               }
-              fw
             />
             <div className="email-warning">
               {member.email}
@@ -146,18 +174,23 @@ export default function EditMember({
               )}
             </div>
           </div>
-        </Col>
+        </OLCol>
 
-        <Col xs={1}>
+        <OLCol xs={2}>
           {privileges !== member.privileges && privilegeChangePending && (
             <ChangePrivilegesActions
               handleReset={() => setPrivileges(member.privileges)}
             />
           )}
-        </Col>
+        </OLCol>
 
-        <Col xs={4} className="project-member-select">
-          {hasBeenDowngraded && <Icon type="warning" fw />}
+        <OLCol xs={3} className="project-member-select">
+          {hasBeenDowngraded && (
+            <BootstrapVersionSwitcher
+              bs3={<Icon type="warning" fw />}
+              bs5={<MaterialIcon type="warning" className="text-warning" />}
+            />
+          )}
 
           <SelectPrivilege
             value={privileges}
@@ -169,9 +202,9 @@ export default function EditMember({
             hasBeenDowngraded={hasBeenDowngraded}
             canAddCollaborators={canAddCollaborators}
           />
-        </Col>
-      </FormGroup>
-    </Form>
+        </OLCol>
+      </OLFormGroup>
+    </form>
   )
 }
 EditMember.propTypes = {
@@ -262,8 +295,9 @@ function SelectPrivilege({
 }
 
 type ChangePrivilegesActionsProps = {
-  handleReset: MouseEventHandler<Button>
+  handleReset: React.ComponentProps<typeof OLButton>['onClick']
 }
+
 function ChangePrivilegesActions({
   handleReset,
 }: ChangePrivilegesActionsProps) {
@@ -271,15 +305,19 @@ function ChangePrivilegesActions({
 
   return (
     <div className="text-center">
-      <Button type="submit" bsSize="sm" bsStyle="primary">
+      <OLButton type="submit" size="sm" variant="primary">
         {t('change_or_cancel-change')}
-      </Button>
+      </OLButton>
       <div className="text-sm">
         {t('change_or_cancel-or')}
         &nbsp;
-        <Button type="button" className="btn-inline-link" onClick={handleReset}>
+        <OLButton
+          variant="link"
+          className="btn-inline-link"
+          onClick={handleReset}
+        >
           {t('change_or_cancel-cancel')}
-        </Button>
+        </OLButton>
       </div>
     </div>
   )

+ 20 - 15
services/web/frontend/js/features/share-project-modal/components/restricted-link-sharing/editor-over-limit-modal-content.tsx

@@ -1,7 +1,13 @@
-import { Button, Modal } from 'react-bootstrap'
 import { useTranslation } from 'react-i18next'
 import { linkSharingEnforcementDate } from '../../utils/link-sharing'
 import { sendMB } from '@/infrastructure/event-tracking'
+import OLButton from '@/features/ui/components/ol/ol-button'
+import {
+  OLModalBody,
+  OLModalFooter,
+  OLModalHeader,
+  OLModalTitle,
+} from '@/features/ui/components/ol/ol-modal'
 
 type EditorOverLimitModalContentProps = {
   handleHide: () => void
@@ -14,22 +20,21 @@ export default function EditorOverLimitModalContent({
 
   return (
     <>
-      <Modal.Header closeButton>
-        <Modal.Title>{t('do_you_need_edit_access')}</Modal.Title>
-      </Modal.Header>
+      <OLModalHeader closeButton>
+        <OLModalTitle>{t('do_you_need_edit_access')}</OLModalTitle>
+      </OLModalHeader>
 
-      <Modal.Body>
+      <OLModalBody>
         <p>
           {t('this_project_has_more_than_max_collabs', {
             linkSharingDate: linkSharingEnforcementDate,
           })}
         </p>
         <p>{t('to_keep_edit_access')}</p>
-      </Modal.Body>
-      <Modal.Footer>
-        <Button
-          bsStyle={null}
-          className="btn-secondary"
+      </OLModalBody>
+      <OLModalFooter>
+        <OLButton
+          variant="secondary"
           href="/blog/changes-to-project-sharing"
           target="_blank"
           rel="noreferrer"
@@ -41,9 +46,9 @@ export default function EditorOverLimitModalContent({
           }}
         >
           {t('learn_more')}
-        </Button>
-        <Button
-          className="btn-primary"
+        </OLButton>
+        <OLButton
+          variant="primary"
           onClick={() => {
             sendMB('notification-click', {
               name: 'link-sharing-collaborator-limit',
@@ -53,8 +58,8 @@ export default function EditorOverLimitModalContent({
           }}
         >
           {t('ok')}
-        </Button>
-      </Modal.Footer>
+        </OLButton>
+      </OLModalFooter>
     </>
   )
 }

+ 3 - 3
services/web/frontend/js/features/share-project-modal/components/restricted-link-sharing/editor-over-limit-modal.tsx

@@ -1,10 +1,10 @@
 import { useEffect, useState } from 'react'
-import AccessibleModal from '@/shared/components/accessible-modal'
 import EditorOverLimitModalContent from './editor-over-limit-modal-content'
 import customLocalStorage from '@/infrastructure/local-storage'
 import { useProjectContext } from '@/shared/context/project-context'
 import { useEditorContext } from '@/shared/context/editor-context'
 import { sendMB } from '@/infrastructure/event-tracking'
+import OLModal from '@/features/ui/components/ol/ol-modal'
 
 const EditorOverLimitModal = () => {
   const [show, setShow] = useState(false)
@@ -55,7 +55,7 @@ const EditorOverLimitModal = () => {
   }, [features, isProjectOwner, members, permissionsLevel, projectId])
 
   return show ? (
-    <AccessibleModal
+    <OLModal
       animation
       show={show}
       onHide={() => {
@@ -67,7 +67,7 @@ const EditorOverLimitModal = () => {
       id="editor-over-limit-modal"
     >
       <EditorOverLimitModalContent handleHide={handleHide} />
-    </AccessibleModal>
+    </OLModal>
   ) : null
 }
 

+ 32 - 21
services/web/frontend/js/features/share-project-modal/components/restricted-link-sharing/invite.jsx

@@ -2,37 +2,43 @@ import { useCallback } from 'react'
 import PropTypes from 'prop-types'
 import { useShareProjectContext } from './share-project-modal'
 import Icon from '@/shared/components/icon'
-import { Button, Col, Row } from 'react-bootstrap'
-import Tooltip from '@/shared/components/tooltip'
 import { useTranslation } from 'react-i18next'
 import MemberPrivileges from './member-privileges'
 import { resendInvite, revokeInvite } from '../../utils/api'
 import { useProjectContext } from '@/shared/context/project-context'
 import { sendMB } from '@/infrastructure/event-tracking'
+import OLRow from '@/features/ui/components/ol/ol-row'
+import OLCol from '@/features/ui/components/ol/ol-col'
+import OLTooltip from '@/features/ui/components/ol/ol-tooltip'
+import OLButton from '@/features/ui/components/ol/ol-button'
+import MaterialIcon from '@/shared/components/material-icon'
+import BootstrapVersionSwitcher from '@/features/ui/components/bootstrap-5/bootstrap-version-switcher'
+import { bsVersion } from '@/features/utils/bootstrap-5'
+import classnames from 'classnames'
 
 export default function Invite({ invite, isProjectOwner }) {
   const { t } = useTranslation()
   return (
-    <Row className="project-invite">
-      <Col xs={8}>
+    <OLRow className="project-invite">
+      <OLCol xs={8}>
         <div>{invite.email}</div>
         <div className="small">
           {t('invite_not_accepted')}
           .&nbsp;
           {isProjectOwner && <ResendInvite invite={invite} />}
         </div>
-      </Col>
+      </OLCol>
 
-      <Col xs={3} className="text-right">
+      <OLCol xs={3} className="text-end">
         <MemberPrivileges privileges={invite.privileges} />
-      </Col>
+      </OLCol>
 
       {isProjectOwner && (
-        <Col xs={1} className="text-center">
+        <OLCol xs={1} className="text-center">
           <RevokeInvite invite={invite} />
-        </Col>
+        </OLCol>
       )}
-    </Row>
+    </OLRow>
   )
 }
 
@@ -70,15 +76,15 @@ function ResendInvite({ invite }) {
   )
 
   return (
-    <Button
-      bsStyle="link"
+    <OLButton
+      variant="link"
       className="btn-inline-link"
       onClick={handleClick}
       disabled={inFlight}
       // ref={buttonRef}
     >
       {t('resend')}
-    </Button>
+    </OLButton>
   )
 }
 
@@ -108,21 +114,26 @@ function RevokeInvite({ invite }) {
   }
 
   return (
-    <Tooltip
+    <OLTooltip
       id="revoke-invite"
       description={t('revoke_invite')}
       overlayProps={{ placement: 'bottom' }}
     >
-      <Button
-        type="button"
-        bsStyle="link"
+      <OLButton
+        variant="link"
         onClick={handleClick}
         aria-label={t('revoke')}
-        className="btn-inline-link"
+        className={classnames(
+          'btn-inline-link',
+          bsVersion({ bs5: 'text-decoration-none' })
+        )}
       >
-        <Icon type="times" />
-      </Button>
-    </Tooltip>
+        <BootstrapVersionSwitcher
+          bs3={<Icon type="times" />}
+          bs5={<MaterialIcon type="clear" />}
+        />
+      </OLButton>
+    </OLTooltip>
   )
 }
 

+ 69 - 59
services/web/frontend/js/features/share-project-modal/components/restricted-link-sharing/link-sharing.jsx

@@ -1,8 +1,6 @@
 import { useCallback, useState, useEffect } from 'react'
 import PropTypes from 'prop-types'
-import { Button, Col, Row } from 'react-bootstrap'
 import { useTranslation } from 'react-i18next'
-import Tooltip from '@/shared/components/tooltip'
 import Icon from '@/shared/components/icon'
 import { useShareProjectContext } from './share-project-modal'
 import { setProjectAccessLevel } from '../../utils/api'
@@ -15,6 +13,12 @@ import { getJSON } from '../../../../infrastructure/fetch-json'
 import useAbortController from '@/shared/hooks/use-abort-controller'
 import { debugConsole } from '@/utils/debugging'
 import getMeta from '@/utils/meta'
+import OLRow from '@/features/ui/components/ol/ol-row'
+import OLCol from '@/features/ui/components/ol/ol-col'
+import OLButton from '@/features/ui/components/ol/ol-button'
+import OLTooltip from '@/features/ui/components/ol/ol-tooltip'
+import BootstrapVersionSwitcher from '@/features/ui/components/bootstrap-5/bootstrap-version-switcher'
+import MaterialIcon from '@/shared/components/material-icon'
 
 export default function LinkSharing() {
   const [inflight, setInflight] = useState(false)
@@ -88,13 +92,12 @@ export default function LinkSharing() {
 function PrivateSharing({ setAccessLevel, inflight, projectId, setShowLinks }) {
   const { t } = useTranslation()
   return (
-    <Row className="public-access-level">
-      <Col xs={12} className="text-center">
+    <OLRow className="public-access-level">
+      <OLCol xs={12} className="text-center">
         <strong>{t('link_sharing_is_off_short')}</strong>
         <span>&nbsp;&nbsp;</span>
-        <Button
-          type="button"
-          bsStyle="link"
+        <OLButton
+          variant="link"
           className="btn-inline-link"
           onClick={() => {
             setAccessLevel('tokenBased')
@@ -104,11 +107,11 @@ function PrivateSharing({ setAccessLevel, inflight, projectId, setShowLinks }) {
           disabled={inflight}
         >
           {t('turn_on_link_sharing')}
-        </Button>
+        </OLButton>
         <span>&nbsp;&nbsp;</span>
         <LinkSharingInfo />
-      </Col>
-    </Row>
+      </OLCol>
+    </OLRow>
   )
 }
 
@@ -139,53 +142,58 @@ function TokenBasedSharing({
   }, [projectId, signal])
 
   return (
-    <Row className="public-access-level">
-      <Col xs={12} className="text-center">
+    <OLRow className="public-access-level">
+      <OLCol xs={12} className="text-center">
         <strong>{t('link_sharing_is_on')}</strong>
         <span>&nbsp;&nbsp;</span>
-        <Button
-          bsStyle="link"
+        <OLButton
+          variant="link"
           className="btn-inline-link"
           onClick={() => setAccessLevel('private')}
           disabled={inflight}
         >
           {t('turn_off_link_sharing')}
-        </Button>
+        </OLButton>
         <span>&nbsp;&nbsp;</span>
         <LinkSharingInfo />
-        <Button
-          bsStyle="link"
-          className="btn-chevron"
+        <OLButton
+          variant="link"
+          className="btn-chevron align-middle"
           onClick={() => setShowLinks(!showLinks)}
         >
-          <Icon type={showLinks ? 'chevron-up' : 'chevron-down'} fw />
-        </Button>
-      </Col>
-      {showLinks && (
-        <>
-          <Col xs={12} className="access-token-display-area">
-            <div className="access-token-wrapper">
-              <strong>{t('anyone_with_link_can_edit')}</strong>
-              <AccessToken
-                token={tokens?.readAndWrite}
-                tokenHashPrefix={tokens?.readAndWriteHashPrefix}
-                path="/"
-                tooltipId="tooltip-copy-link-rw"
-              />
-            </div>
-            <div className="access-token-wrapper">
-              <strong>{t('anyone_with_link_can_view')}</strong>
-              <AccessToken
-                token={tokens?.readOnly}
-                tokenHashPrefix={tokens?.readOnlyHashPrefix}
-                path="/read/"
-                tooltipId="tooltip-copy-link-ro"
+          <BootstrapVersionSwitcher
+            bs3={<Icon type={showLinks ? 'chevron-up' : 'chevron-down'} fw />}
+            bs5={
+              <MaterialIcon
+                type={showLinks ? 'keyboard_arrow_up' : 'keyboard_arrow_down'}
               />
-            </div>
-          </Col>
-        </>
+            }
+          />
+        </OLButton>
+      </OLCol>
+      {showLinks && (
+        <OLCol xs={12} className="access-token-display-area">
+          <div className="access-token-wrapper">
+            <strong>{t('anyone_with_link_can_edit')}</strong>
+            <AccessToken
+              token={tokens?.readAndWrite}
+              tokenHashPrefix={tokens?.readAndWriteHashPrefix}
+              path="/"
+              tooltipId="tooltip-copy-link-rw"
+            />
+          </div>
+          <div className="access-token-wrapper">
+            <strong>{t('anyone_with_link_can_view')}</strong>
+            <AccessToken
+              token={tokens?.readOnly}
+              tokenHashPrefix={tokens?.readOnlyHashPrefix}
+              path="/read/"
+              tooltipId="tooltip-copy-link-ro"
+            />
+          </div>
+        </OLCol>
       )}
-    </Row>
+    </OLRow>
   )
 }
 
@@ -200,26 +208,25 @@ function LegacySharing({ accessLevel, setAccessLevel, inflight }) {
   const { t } = useTranslation()
 
   return (
-    <Row className="public-access-level">
-      <Col xs={12} className="text-center">
+    <OLRow className="public-access-level">
+      <OLCol xs={12} className="text-center">
         <strong>
           {accessLevel === 'readAndWrite' && t('this_project_is_public')}
           {accessLevel === 'readOnly' && t('this_project_is_public_read_only')}
         </strong>
         <span>&nbsp;&nbsp;</span>
-        <Button
-          type="button"
-          bsStyle="link"
+        <OLButton
+          variant="link"
           className="btn-inline-link"
           onClick={() => setAccessLevel('private')}
           disabled={inflight}
         >
           {t('make_private')}
-        </Button>
+        </OLButton>
         <span>&nbsp;&nbsp;</span>
         <LinkSharingInfo />
-      </Col>
-    </Row>
+      </OLCol>
+    </OLRow>
   )
 }
 
@@ -244,8 +251,8 @@ export function ReadOnlyTokenLink() {
   }, [projectId, signal])
 
   return (
-    <Row className="public-access-level">
-      <Col xs={12} className="access-token-display-area">
+    <OLRow className="public-access-level">
+      <OLCol className="access-token-display-area">
         <div className="access-token-wrapper">
           <strong>{t('anyone_with_link_can_view')}</strong>
           <AccessToken
@@ -255,8 +262,8 @@ export function ReadOnlyTokenLink() {
             tooltipId="tooltip-copy-link-ro"
           />
         </div>
-      </Col>
-    </Row>
+      </OLCol>
+    </OLRow>
   )
 }
 
@@ -299,7 +306,7 @@ function LinkSharingInfo() {
   const { t } = useTranslation()
 
   return (
-    <Tooltip
+    <OLTooltip
       id="link-sharing-info"
       description={t('learn_more_about_link_sharing')}
     >
@@ -308,8 +315,11 @@ function LinkSharingInfo() {
         target="_blank"
         rel="noopener"
       >
-        <Icon type="question-circle" />
+        <BootstrapVersionSwitcher
+          bs3={<Icon type="question-circle" />}
+          bs5={<MaterialIcon type="help" className="align-middle" />}
+        />
       </a>
-    </Tooltip>
+    </OLTooltip>
   )
 }

+ 14 - 8
services/web/frontend/js/features/share-project-modal/components/restricted-link-sharing/owner-info.jsx

@@ -1,23 +1,29 @@
 import { useProjectContext } from '@/shared/context/project-context'
-import { Col, Row } from 'react-bootstrap'
 import { useTranslation } from 'react-i18next'
 import Icon from '@/shared/components/icon'
+import OLRow from '@/features/ui/components/ol/ol-row'
+import OLCol from '@/features/ui/components/ol/ol-col'
+import BootstrapVersionSwitcher from '@/features/ui/components/bootstrap-5/bootstrap-version-switcher'
+import MaterialIcon from '@/shared/components/material-icon'
 
 export default function OwnerInfo() {
   const { t } = useTranslation()
   const { owner } = useProjectContext()
 
   return (
-    <Row className="project-member">
-      <Col xs={8}>
+    <OLRow className="project-member">
+      <OLCol xs={8}>
         <div className="project-member-email-icon">
-          <Icon type="user" fw />
+          <BootstrapVersionSwitcher
+            bs3={<Icon type="user" fw />}
+            bs5={<MaterialIcon type="person" />}
+          />
           <div className="email-warning">{owner?.email}</div>
         </div>
-      </Col>
-      <Col xs={4} className="text-right">
+      </OLCol>
+      <OLCol xs={4} className="text-end">
         {t('owner')}
-      </Col>
-    </Row>
+      </OLCol>
+    </OLRow>
   )
 }

+ 173 - 100
services/web/frontend/js/features/share-project-modal/components/restricted-link-sharing/select-collaborators.jsx

@@ -6,6 +6,12 @@ import { useCombobox } from 'downshift'
 import classnames from 'classnames'
 
 import Icon from '@/shared/components/icon'
+import MaterialIcon from '@/shared/components/material-icon'
+import Tag from '@/features/ui/components/bootstrap-5/tag'
+import { DropdownItem } from '@/features/ui/components/bootstrap-5/dropdown-menu'
+import BootstrapVersionSwitcher from '@/features/ui/components/bootstrap-5/bootstrap-version-switcher'
+import { bsVersion } from '@/features/utils/bootstrap-5'
+import { Spinner } from 'react-bootstrap-5'
 
 // Unicode characters in these Unicode groups:
 //  "General Punctuation — Spaces"
@@ -156,98 +162,123 @@ export default function SelectCollaborators({
           {t('add_people')}
           &nbsp;
         </strong>
-        {loading && <Icon type="refresh" spin />}
+        {loading && (
+          <BootstrapVersionSwitcher
+            bs3={<Icon type="refresh" spin />}
+            bs5={
+              <Spinner
+                animation="border"
+                aria-hidden="true"
+                size="sm"
+                role="status"
+              />
+            }
+          />
+        )}
       </label>
 
       <div className="host">
         {/* eslint-disable-next-line jsx-a11y/click-events-have-key-events,jsx-a11y/no-static-element-interactions */}
-        <div {...getComboboxProps()} className="tags" onClick={focusInput}>
-          <div className="tags-main">
-            {selectedItems.map((selectedItem, index) => (
-              <SelectedItem
-                key={`selected-item-${index}`}
-                removeSelectedItem={removeSelectedItem}
-                selectedItem={selectedItem}
-                focusInput={focusInput}
-                index={index}
-                getSelectedItemProps={getSelectedItemProps}
-              />
-            ))}
-
-            <input
-              {...getInputProps(
-                getDropdownProps({
-                  className: classnames({
-                    input: true,
-                    'invalid-tag': !isValidInput,
-                  }),
-                  type: 'email',
-                  placeholder,
-                  size: inputValue.length
-                    ? inputValue.length + 5
-                    : placeholder.length,
-                  ref: inputRef,
-                  // preventKeyAction: showDropdown,
-                  onBlur: () => {
-                    addNewItem(inputValue, false)
-                  },
-                  onChange: e => {
-                    setInputValue(e.target.value)
-                  },
-                  onClick: () => focusInput,
-                  onKeyDown: event => {
-                    switch (event.key) {
-                      case 'Enter':
-                        // Enter: always prevent form submission
+        <div
+          {...getComboboxProps()}
+          className="tags form-control"
+          onClick={focusInput}
+        >
+          {selectedItems.map((selectedItem, index) => (
+            <SelectedItem
+              key={`selected-item-${index}`}
+              removeSelectedItem={removeSelectedItem}
+              selectedItem={selectedItem}
+              focusInput={focusInput}
+              index={index}
+              getSelectedItemProps={getSelectedItemProps}
+            />
+          ))}
+
+          <input
+            {...getInputProps(
+              getDropdownProps({
+                className: classnames('input', {
+                  'invalid-tag': !isValidInput,
+                }),
+                type: 'email',
+                placeholder,
+                size: inputValue.length
+                  ? inputValue.length + 5
+                  : placeholder.length,
+                ref: inputRef,
+                // preventKeyAction: showDropdown,
+                onBlur: () => {
+                  addNewItem(inputValue, false)
+                },
+                onChange: e => {
+                  setInputValue(e.target.value)
+                },
+                onClick: () => focusInput,
+                onKeyDown: event => {
+                  switch (event.key) {
+                    case 'Enter':
+                      // Enter: always prevent form submission
+                      event.preventDefault()
+                      event.stopPropagation()
+                      break
+
+                    case 'Tab':
+                      // Tab: if the dropdown isn't open, try to create a new item using inputValue and prevent blur if successful
+                      if (!isOpen && addNewItem(inputValue)) {
                         event.preventDefault()
                         event.stopPropagation()
-                        break
-
-                      case 'Tab':
-                        // Tab: if the dropdown isn't open, try to create a new item using inputValue and prevent blur if successful
-                        if (!isOpen && addNewItem(inputValue)) {
-                          event.preventDefault()
-                          event.stopPropagation()
-                        }
-                        break
-
-                      case ',':
-                        // comma: try to create a new item using inputValue
-                        event.preventDefault()
-                        addNewItem(inputValue)
-                        break
-                    }
-                  },
-                  onPaste: event => {
-                    const data =
-                      // modern browsers
-                      event.clipboardData?.getData('text/plain') ??
-                      // IE11
-                      window.clipboardData?.getData('text')
-
-                    if (data) {
-                      const emails = data
-                        .split(/[\r\n,; ]+/)
-                        .filter(item => item.includes('@'))
-
-                      if (emails.length) {
-                        // pasted comma-separated email addresses
-                        event.preventDefault()
+                      }
+                      break
+
+                    case ',':
+                      // comma: try to create a new item using inputValue
+                      event.preventDefault()
+                      addNewItem(inputValue)
+                      break
+                  }
+                },
+                onPaste: event => {
+                  const data =
+                    // modern browsers
+                    event.clipboardData?.getData('text/plain') ??
+                    // IE11
+                    window.clipboardData?.getData('text')
+
+                  if (data) {
+                    const emails = data
+                      .split(/[\r\n,; ]+/)
+                      .filter(item => item.includes('@'))
 
-                        for (const email of emails) {
-                          addNewItem(email)
-                        }
+                    if (emails.length) {
+                      // pasted comma-separated email addresses
+                      event.preventDefault()
+
+                      for (const email of emails) {
+                        addNewItem(email)
                       }
                     }
-                  },
-                })
-              )}
-            />
-          </div>
+                  }
+                },
+              })
+            )}
+          />
         </div>
 
-        <div className={classnames({ autocomplete: isOpen })}>
-          <ul {...getMenuProps()} className="suggestion-list">
+        <div
+          className={bsVersion({ bs3: classnames({ autocomplete: isOpen }) })}
+        >
+          <ul
+            {...getMenuProps()}
+            className={classnames(
+              bsVersion({
+                bs3: 'suggestion-list',
+                bs5: classnames('dropdown-menu select-dropdown-menu', {
+                  show: isOpen,
+                }),
+              })
+            )}
+          >
             {isOpen &&
               filteredOptions.map((item, index) => (
                 <Option
@@ -280,15 +311,36 @@ SelectCollaborators.propTypes = {
 function Option({ selected, item, getItemProps, index }) {
   return (
     <li
-      className={classnames('suggestion-item', { selected })}
+      className={bsVersion({
+        bs3: classnames('suggestion-item', { selected }),
+      })}
       {...getItemProps({ item, index })}
     >
-      <Icon type="user" fw />
-      &nbsp;
-      {item.display}
+      <BootstrapVersionSwitcher
+        bs3={
+          <>
+            <Icon type="user" fw />
+            &nbsp;
+            {item.display}
+          </>
+        }
+        bs5={
+          <DropdownItem
+            as="span"
+            role={undefined}
+            leadingIcon="person"
+            className={classnames({
+              active: selected,
+            })}
+          >
+            {item.display}
+          </DropdownItem>
+        }
+      />
     </li>
   )
 }
+
 Option.propTypes = {
   selected: PropTypes.bool.isRequired,
   item: PropTypes.shape({
@@ -318,23 +370,44 @@ function SelectedItem({
   )
 
   return (
-    <span
-      className="tag-item"
-      {...getSelectedItemProps({ selectedItem, index })}
-    >
-      <Icon type="user" fw />
-      <span>{selectedItem.display}</span>
-      <button
-        type="button"
-        className="remove-button btn-inline-link"
-        aria-label={t('remove')}
-        onClick={handleClick}
-      >
-        <Icon type="close" fw />
-      </button>
-    </span>
+    <BootstrapVersionSwitcher
+      bs3={
+        <span
+          className="tag-item"
+          {...getSelectedItemProps({ selectedItem, index })}
+        >
+          <Icon type="user" fw />
+          <span>{selectedItem.display}</span>
+          <button
+            type="button"
+            className="remove-button btn-inline-link"
+            aria-label={t('remove')}
+            onClick={handleClick}
+          >
+            <Icon type="close" fw />
+          </button>
+        </span>
+      }
+      bs5={
+        <Tag
+          prepend={
+            <BootstrapVersionSwitcher
+              bs3={<Icon type="user" fw />}
+              bs5={<MaterialIcon type="person" />}
+            />
+          }
+          closeBtnProps={{
+            onClick: handleClick,
+          }}
+          {...getSelectedItemProps({ selectedItem, index })}
+        >
+          {selectedItem.display}
+        </Tag>
+      }
+    />
   )
 }
+
 SelectedItem.propTypes = {
   focusInput: PropTypes.func.isRequired,
   removeSelectedItem: PropTypes.func.isRequired,

+ 6 - 5
services/web/frontend/js/features/share-project-modal/components/restricted-link-sharing/send-invites-notice.tsx

@@ -1,9 +1,10 @@
-import { Col, Row } from 'react-bootstrap'
 import { useTranslation } from 'react-i18next'
 import { useProjectContext } from '@/shared/context/project-context'
 import Notification from '@/shared/components/notification'
 import { PublicAccessLevel } from '../../../../../../types/public-access-level'
 import { useEditorContext } from '@/shared/context/editor-context'
+import OLRow from '@/features/ui/components/ol/ol-row'
+import OLCol from '@/features/ui/components/ol/ol-col'
 
 export default function SendInvitesNotice() {
   const { publicAccessLevel } = useProjectContext()
@@ -25,11 +26,11 @@ export default function SendInvitesNotice() {
           }
         />
       )}
-      <Row className="public-access-level public-access-level--notice">
-        <Col xs={12} className="text-center">
+      <OLRow className="public-access-level public-access-level-notice">
+        <OLCol className="text-center">
           <AccessLevel level={publicAccessLevel} />
-        </Col>
-      </Row>
+        </OLCol>
+      </OLRow>
     </div>
   )
 }

+ 3 - 3
services/web/frontend/js/features/share-project-modal/components/restricted-link-sharing/send-invites.jsx

@@ -1,9 +1,9 @@
-import { Row } from 'react-bootstrap'
 import AddCollaborators from './add-collaborators'
 import AddCollaboratorsUpgrade from './add-collaborators-upgrade'
 import CollaboratorsLimitUpgrade from './collaborators-limit-upgrade'
 import AccessLevelsChanged from './access-levels-changed'
 import PropTypes from 'prop-types'
+import OLRow from '@/features/ui/components/ol/ol-row'
 
 export default function SendInvites({
   canAddCollaborators,
@@ -12,7 +12,7 @@ export default function SendInvites({
   somePendingEditorsResolved,
 }) {
   return (
-    <Row className="invite-controls">
+    <OLRow className="invite-controls">
       {hasExceededCollaboratorLimit && !haveAnyEditorsBeenDowngraded && (
         <AddCollaboratorsUpgrade />
       )}
@@ -27,7 +27,7 @@ export default function SendInvites({
         !hasExceededCollaboratorLimit &&
         !haveAnyEditorsBeenDowngraded && <CollaboratorsLimitUpgrade />}
       <AddCollaborators readOnly={!canAddCollaborators} />
-    </Row>
+    </OLRow>
   )
 }
 

+ 50 - 31
services/web/frontend/js/features/share-project-modal/components/restricted-link-sharing/share-project-modal-content.tsx

@@ -1,11 +1,20 @@
-import { Button, Modal, Grid } from 'react-bootstrap'
 import { useTranslation } from 'react-i18next'
 import Icon from '@/shared/components/icon'
-import AccessibleModal from '@/shared/components/accessible-modal'
 import { useEditorContext } from '@/shared/context/editor-context'
 import { lazy, Suspense } from 'react'
 import { FullSizeLoadingSpinner } from '@/shared/components/loading-spinner'
 import ClickableElementEnhancer from '@/shared/components/clickable-element-enhancer'
+import OLModal, {
+  OLModalBody,
+  OLModalFooter,
+  OLModalHeader,
+  OLModalTitle,
+} from '@/features/ui/components/ol/ol-modal'
+import OLNotification from '@/features/ui/components/ol/ol-notification'
+import OLButton from '@/features/ui/components/ol/ol-button'
+import BootstrapVersionSwitcher from '@/features/ui/components/bootstrap-5/bootstrap-version-switcher'
+import { bsVersion } from '@/features/utils/bootstrap-5'
+import { Spinner } from 'react-bootstrap-5'
 
 const ReadOnlyTokenLink = lazy(() =>
   import('./link-sharing').then(({ ReadOnlyTokenLink }) => ({
@@ -36,13 +45,13 @@ export default function ShareProjectModalContent({
   const { isRestrictedTokenMember } = useEditorContext()
 
   return (
-    <AccessibleModal show={show} onHide={cancel} animation={animation}>
-      <Modal.Header closeButton>
-        <Modal.Title>{t('share_project')}</Modal.Title>
-      </Modal.Header>
+    <OLModal show={show} onHide={cancel} animation={animation}>
+      <OLModalHeader closeButton>
+        <OLModalTitle>{t('share_project')}</OLModalTitle>
+      </OLModalHeader>
 
-      <Modal.Body className="modal-body-share modal-link-share-new">
-        <Grid fluid>
+      <OLModalBody className="modal-body-share modal-link-share-new">
+        <div className="container-fluid">
           <Suspense fallback={<FullSizeLoadingSpinner minHeight="15rem" />}>
             {isRestrictedTokenMember ? (
               <ReadOnlyTokenLink />
@@ -50,33 +59,43 @@ export default function ShareProjectModalContent({
               <ShareModalBody />
             )}
           </Suspense>
-        </Grid>
-      </Modal.Body>
-
-      <Modal.Footer className="modal-footer-share">
-        <div className="modal-footer-left">
-          {inFlight && <Icon type="refresh" spin />}
           {error && (
-            <span className="text-danger error">
-              <ErrorMessage error={error} />
-            </span>
+            <OLNotification
+              type="error"
+              content={<ErrorMessage error={error} />}
+              className="mb-0 mt-3"
+            />
           )}
         </div>
-
-        <div className="modal-footer-right">
-          <ClickableElementEnhancer
-            onClick={cancel}
-            as={Button}
-            type="button"
-            bsStyle={null}
-            className="btn-secondary"
-            disabled={inFlight}
-          >
-            {t('close')}
-          </ClickableElementEnhancer>
+      </OLModalBody>
+
+      <OLModalFooter>
+        <div className={bsVersion({ bs3: 'pull-left', bs5: 'me-auto' })}>
+          {inFlight && (
+            <BootstrapVersionSwitcher
+              bs3={<Icon type="refresh" spin />}
+              bs5={
+                <Spinner
+                  animation="border"
+                  aria-hidden="true"
+                  size="sm"
+                  role="status"
+                />
+              }
+            />
+          )}
         </div>
-      </Modal.Footer>
-    </AccessibleModal>
+
+        <ClickableElementEnhancer
+          onClick={cancel}
+          as={OLButton}
+          variant="secondary"
+          disabled={inFlight}
+        >
+          {t('close')}
+        </ClickableElementEnhancer>
+      </OLModalFooter>
+    </OLModal>
   )
 }
 

+ 46 - 36
services/web/frontend/js/features/share-project-modal/components/restricted-link-sharing/transfer-ownership-modal.jsx

@@ -1,12 +1,21 @@
 import { useState } from 'react'
-import { Modal, Button } from 'react-bootstrap'
 import { Trans, useTranslation } from 'react-i18next'
 import PropTypes from 'prop-types'
 import Icon from '@/shared/components/icon'
 import { transferProjectOwnership } from '../../utils/api'
-import AccessibleModal from '@/shared/components/accessible-modal'
 import { useProjectContext } from '@/shared/context/project-context'
 import { useLocation } from '@/shared/hooks/use-location'
+import OLModal, {
+  OLModalBody,
+  OLModalFooter,
+  OLModalHeader,
+  OLModalTitle,
+} from '@/features/ui/components/ol/ol-modal'
+import OLNotification from '@/features/ui/components/ol/ol-notification'
+import OLButton from '@/features/ui/components/ol/ol-button'
+import BootstrapVersionSwitcher from '@/features/ui/components/bootstrap-5/bootstrap-version-switcher'
+import { bsVersion } from '@/features/utils/bootstrap-5'
+import { Spinner } from 'react-bootstrap-5'
 
 export default function TransferOwnershipModal({ member, cancel }) {
   const { t } = useTranslation()
@@ -32,11 +41,11 @@ export default function TransferOwnershipModal({ member, cancel }) {
   }
 
   return (
-    <AccessibleModal show onHide={cancel}>
-      <Modal.Header closeButton>
-        <Modal.Title>{t('change_project_owner')}</Modal.Title>
-      </Modal.Header>
-      <Modal.Body>
+    <OLModal show onHide={cancel}>
+      <OLModalHeader closeButton>
+        <OLModalTitle>{t('change_project_owner')}</OLModalTitle>
+      </OLModalHeader>
+      <OLModalBody>
         <p>
           <Trans
             i18nKey="project_ownership_transfer_confirmation_1"
@@ -47,37 +56,38 @@ export default function TransferOwnershipModal({ member, cancel }) {
           />
         </p>
         <p>{t('project_ownership_transfer_confirmation_2')}</p>
-      </Modal.Body>
-      <Modal.Footer>
-        <div className="modal-footer-left">
-          {inflight && <Icon type="refresh" spin />}
-          {error && (
-            <span className="text-danger">
-              {t('generic_something_went_wrong')}
-            </span>
+        {error && (
+          <OLNotification
+            type="error"
+            content={t('generic_something_went_wrong')}
+            className="mb-0 mt-3"
+          />
+        )}
+      </OLModalBody>
+      <OLModalFooter>
+        <div className={bsVersion({ bs3: 'pull-left', bs5: 'me-auto' })}>
+          {inflight && (
+            <BootstrapVersionSwitcher
+              bs3={<Icon type="refresh" spin />}
+              bs5={
+                <Spinner
+                  animation="border"
+                  aria-hidden="true"
+                  size="sm"
+                  role="status"
+                />
+              }
+            />
           )}
         </div>
-        <div className="modal-footer-right">
-          <Button
-            type="button"
-            bsStyle={null}
-            className="btn-secondary"
-            onClick={cancel}
-            disabled={inflight}
-          >
-            {t('cancel')}
-          </Button>
-          <Button
-            type="button"
-            bsStyle="primary"
-            onClick={confirm}
-            disabled={inflight}
-          >
-            {t('change_owner')}
-          </Button>
-        </div>
-      </Modal.Footer>
-    </AccessibleModal>
+        <OLButton variant="secondary" onClick={cancel} disabled={inflight}>
+          {t('cancel')}
+        </OLButton>
+        <OLButton variant="primary" onClick={confirm} disabled={inflight}>
+          {t('change_owner')}
+        </OLButton>
+      </OLModalFooter>
+    </OLModal>
   )
 }
 TransferOwnershipModal.propTypes = {

+ 14 - 8
services/web/frontend/js/features/share-project-modal/components/restricted-link-sharing/view-member.jsx

@@ -1,21 +1,27 @@
 import PropTypes from 'prop-types'
-import { Col, Row } from 'react-bootstrap'
 import MemberPrivileges from './member-privileges'
 import Icon from '@/shared/components/icon'
+import OLRow from '@/features/ui/components/ol/ol-row'
+import OLCol from '@/features/ui/components/ol/ol-col'
+import MaterialIcon from '@/shared/components/material-icon'
+import BootstrapVersionSwitcher from '@/features/ui/components/bootstrap-5/bootstrap-version-switcher'
 
 export default function ViewMember({ member }) {
   return (
-    <Row className="project-member">
-      <Col xs={8}>
+    <OLRow className="project-member">
+      <OLCol xs={8}>
         <div className="project-member-email-icon">
-          <Icon type="user" fw />
+          <BootstrapVersionSwitcher
+            bs3={<Icon type="user" fw />}
+            bs5={<MaterialIcon type="person" />}
+          />
           <div className="email-warning">{member.email}</div>
         </div>
-      </Col>
-      <Col xs={4} className="text-right">
+      </OLCol>
+      <OLCol xs={4} className="text-end">
         <MemberPrivileges privileges={member.privileges} />
-      </Col>
-    </Row>
+      </OLCol>
+    </OLRow>
   )
 }
 

+ 20 - 15
services/web/frontend/js/features/share-project-modal/components/restricted-link-sharing/view-only-access-modal-content.tsx

@@ -1,6 +1,12 @@
-import { Button, Modal } from 'react-bootstrap'
 import { useTranslation } from 'react-i18next'
 import { sendMB } from '@/infrastructure/event-tracking'
+import OLButton from '@/features/ui/components/ol/ol-button'
+import {
+  OLModalBody,
+  OLModalFooter,
+  OLModalHeader,
+  OLModalTitle,
+} from '@/features/ui/components/ol/ol-modal'
 
 type ViewOnlyAccessModalContentProps = {
   handleHide: () => void
@@ -13,18 +19,17 @@ export default function ViewOnlyAccessModalContent({
 
   return (
     <>
-      <Modal.Header closeButton>
-        <Modal.Title>{t('view_only_access')}</Modal.Title>
-      </Modal.Header>
+      <OLModalHeader closeButton>
+        <OLModalTitle>{t('view_only_access')}</OLModalTitle>
+      </OLModalHeader>
 
-      <Modal.Body>
+      <OLModalBody>
         <p>{t('this_project_already_has_maximum_editors')}</p>
         <p>{t('please_ask_the_project_owner_to_upgrade_more_editors')}</p>
-      </Modal.Body>
-      <Modal.Footer>
-        <Button
-          bsStyle={null}
-          className="btn-secondary"
+      </OLModalBody>
+      <OLModalFooter>
+        <OLButton
+          variant="secondary"
           href="/blog/changes-to-project-sharing"
           target="_blank"
           rel="noreferrer"
@@ -36,9 +41,9 @@ export default function ViewOnlyAccessModalContent({
           }}
         >
           {t('learn_more')}
-        </Button>
-        <Button
-          className="btn-primary"
+        </OLButton>
+        <OLButton
+          variant="primary"
           onClick={() => {
             sendMB('notification-click', {
               name: 'link-sharing-collaborator-limit',
@@ -48,8 +53,8 @@ export default function ViewOnlyAccessModalContent({
           }}
         >
           {t('ok')}
-        </Button>
-      </Modal.Footer>
+        </OLButton>
+      </OLModalFooter>
     </>
   )
 }

+ 3 - 3
services/web/frontend/js/features/share-project-modal/components/restricted-link-sharing/view-only-access-modal.tsx

@@ -1,10 +1,10 @@
 import { useEffect, useState } from 'react'
-import AccessibleModal from '@/shared/components/accessible-modal'
 import ViewOnlyAccessModalContent from './view-only-access-modal-content'
 import customLocalStorage from '@/infrastructure/local-storage'
 import { useProjectContext } from '@/shared/context/project-context'
 import { useEditorContext } from '@/shared/context/editor-context'
 import { sendMB } from '@/infrastructure/event-tracking'
+import OLModal from '@/features/ui/components/ol/ol-modal'
 
 const ViewOnlyAccessModal = () => {
   const [show, setShow] = useState(false)
@@ -60,7 +60,7 @@ const ViewOnlyAccessModal = () => {
   ])
 
   return show ? (
-    <AccessibleModal
+    <OLModal
       animation
       show={show}
       onHide={() => {
@@ -72,7 +72,7 @@ const ViewOnlyAccessModal = () => {
       id="editor-over-limit-modal"
     >
       <ViewOnlyAccessModalContent handleHide={handleHide} />
-    </AccessibleModal>
+    </OLModal>
   ) : null
 }
 

+ 100 - 25
services/web/frontend/js/features/share-project-modal/components/select-collaborators.jsx

@@ -6,6 +6,12 @@ import { useCombobox } from 'downshift'
 import classnames from 'classnames'
 
 import Icon from '../../../shared/components/icon'
+import MaterialIcon from '@/shared/components/material-icon'
+import Tag from '@/features/ui/components/bootstrap-5/tag'
+import { DropdownItem } from '@/features/ui/components/bootstrap-5/dropdown-menu'
+import BootstrapVersionSwitcher from '@/features/ui/components/bootstrap-5/bootstrap-version-switcher'
+import { bsVersion } from '@/features/utils/bootstrap-5'
+import { Spinner } from 'react-bootstrap-5'
 
 // Unicode characters in these Unicode groups:
 //  "General Punctuation — Spaces"
@@ -154,12 +160,28 @@ export default function SelectCollaborators({
       <label className="small" {...getLabelProps()}>
         {t('share_with_your_collabs')}
         &nbsp;
-        {loading && <Icon type="refresh" spin />}
+        {loading && (
+          <BootstrapVersionSwitcher
+            bs3={<Icon type="refresh" spin />}
+            bs5={
+              <Spinner
+                animation="border"
+                aria-hidden="true"
+                size="sm"
+                role="status"
+              />
+            }
+          />
+        )}
       </label>
 
       <div className="host">
         {/* eslint-disable-next-line jsx-a11y/click-events-have-key-events,jsx-a11y/no-static-element-interactions */}
-        <div {...getComboboxProps()} className="tags" onClick={focusInput}>
+        <div
+          {...getComboboxProps()}
+          className="tags form-control"
+          onClick={focusInput}
+        >
           {selectedItems.map((selectedItem, index) => (
             <SelectedItem
               key={`selected-item-${index}`}
@@ -174,8 +196,7 @@ export default function SelectCollaborators({
           <input
             {...getInputProps(
               getDropdownProps({
-                className: classnames({
-                  input: true,
+                className: classnames('input', {
                   'invalid-tag': !isValidInput,
                 }),
                 type: 'email',
@@ -241,8 +262,20 @@ export default function SelectCollaborators({
           />
         </div>
 
-        <div className={classnames({ autocomplete: isOpen })}>
-          <ul {...getMenuProps()} className="suggestion-list">
+        <div
+          className={bsVersion({ bs3: classnames({ autocomplete: isOpen }) })}
+        >
+          <ul
+            {...getMenuProps()}
+            className={classnames(
+              bsVersion({
+                bs3: 'suggestion-list',
+                bs5: classnames('dropdown-menu select-dropdown-menu', {
+                  show: isOpen,
+                }),
+              })
+            )}
+          >
             {isOpen &&
               filteredOptions.map((item, index) => (
                 <Option
@@ -275,15 +308,36 @@ SelectCollaborators.propTypes = {
 function Option({ selected, item, getItemProps, index }) {
   return (
     <li
-      className={classnames('suggestion-item', { selected })}
+      className={bsVersion({
+        bs3: classnames('suggestion-item', { selected }),
+      })}
       {...getItemProps({ item, index })}
     >
-      <Icon type="user" fw />
-      &nbsp;
-      {item.display}
+      <BootstrapVersionSwitcher
+        bs3={
+          <>
+            <Icon type="user" fw />
+            &nbsp;
+            {item.display}
+          </>
+        }
+        bs5={
+          <DropdownItem
+            as="span"
+            role={undefined}
+            leadingIcon="person"
+            className={classnames({
+              active: selected,
+            })}
+          >
+            {item.display}
+          </DropdownItem>
+        }
+      />
     </li>
   )
 }
+
 Option.propTypes = {
   selected: PropTypes.bool.isRequired,
   item: PropTypes.shape({
@@ -313,23 +367,44 @@ function SelectedItem({
   )
 
   return (
-    <span
-      className="tag-item"
-      {...getSelectedItemProps({ selectedItem, index })}
-    >
-      <Icon type="user" fw />
-      <span>{selectedItem.display}</span>
-      <button
-        type="button"
-        className="remove-button btn-inline-link"
-        aria-label={t('remove')}
-        onClick={handleClick}
-      >
-        <Icon type="close" fw />
-      </button>
-    </span>
+    <BootstrapVersionSwitcher
+      bs3={
+        <span
+          className="tag-item"
+          {...getSelectedItemProps({ selectedItem, index })}
+        >
+          <Icon type="user" fw />
+          <span>{selectedItem.display}</span>
+          <button
+            type="button"
+            className="remove-button btn-inline-link"
+            aria-label={t('remove')}
+            onClick={handleClick}
+          >
+            <Icon type="close" fw />
+          </button>
+        </span>
+      }
+      bs5={
+        <Tag
+          prepend={
+            <BootstrapVersionSwitcher
+              bs3={<Icon type="user" fw />}
+              bs5={<MaterialIcon type="person" />}
+            />
+          }
+          closeBtnProps={{
+            onClick: handleClick,
+          }}
+          {...getSelectedItemProps({ selectedItem, index })}
+        >
+          {selectedItem.display}
+        </Tag>
+      }
+    />
   )
 }
+
 SelectedItem.propTypes = {
   focusInput: PropTypes.func.isRequired,
   removeSelectedItem: PropTypes.func.isRequired,

+ 6 - 5
services/web/frontend/js/features/share-project-modal/components/send-invites-notice.jsx

@@ -1,17 +1,18 @@
-import { Col, Row } from 'react-bootstrap'
 import PropTypes from 'prop-types'
 import { useTranslation } from 'react-i18next'
 import { useProjectContext } from '../../../shared/context/project-context'
+import OLRow from '@/features/ui/components/ol/ol-row'
+import OLCol from '@/features/ui/components/ol/ol-col'
 
 export default function SendInvitesNotice() {
   const { publicAccessLevel } = useProjectContext()
 
   return (
-    <Row className="public-access-level public-access-level--notice">
-      <Col xs={12} className="text-center">
+    <OLRow className="public-access-level public-access-level-notice">
+      <OLCol className="text-center">
         <AccessLevel level={publicAccessLevel} />
-      </Col>
-    </Row>
+      </OLCol>
+    </OLRow>
   )
 }
 

+ 3 - 3
services/web/frontend/js/features/share-project-modal/components/send-invites.jsx

@@ -1,13 +1,13 @@
-import { Row } from 'react-bootstrap'
 import AddCollaborators from './add-collaborators'
 import AddCollaboratorsUpgrade from './add-collaborators-upgrade'
 import PropTypes from 'prop-types'
+import OLRow from '@/features/ui/components/ol/ol-row'
 
 export default function SendInvites({ canAddCollaborators }) {
   return (
-    <Row className="invite-controls">
+    <OLRow className="invite-controls">
       {canAddCollaborators ? <AddCollaborators /> : <AddCollaboratorsUpgrade />}
-    </Row>
+    </OLRow>
   )
 }
 

+ 51 - 32
services/web/frontend/js/features/share-project-modal/components/share-project-modal-content.tsx

@@ -1,11 +1,20 @@
-import { Button, Modal, Grid } from 'react-bootstrap'
 import { useTranslation } from 'react-i18next'
 import Icon from '../../../shared/components/icon'
-import AccessibleModal from '../../../shared/components/accessible-modal'
-import { useEditorContext } from '../../../shared/context/editor-context'
+import { useEditorContext } from '@/shared/context/editor-context'
 import { lazy, Suspense } from 'react'
 import { FullSizeLoadingSpinner } from '@/shared/components/loading-spinner'
 import ClickableElementEnhancer from '@/shared/components/clickable-element-enhancer'
+import OLModal, {
+  OLModalBody,
+  OLModalFooter,
+  OLModalHeader,
+  OLModalTitle,
+} from '@/features/ui/components/ol/ol-modal'
+import OLNotification from '@/features/ui/components/ol/ol-notification'
+import OLButton from '@/features/ui/components/ol/ol-button'
+import BootstrapVersionSwitcher from '@/features/ui/components/bootstrap-5/bootstrap-version-switcher'
+import { bsVersion } from '@/features/utils/bootstrap-5'
+import { Spinner } from 'react-bootstrap-5'
 
 const ReadOnlyTokenLink = lazy(() =>
   import('./link-sharing').then(({ ReadOnlyTokenLink }) => ({
@@ -36,13 +45,13 @@ export default function ShareProjectModalContent({
   const { isRestrictedTokenMember } = useEditorContext()
 
   return (
-    <AccessibleModal show={show} onHide={cancel} animation={animation}>
-      <Modal.Header closeButton>
-        <Modal.Title>{t('share_project')}</Modal.Title>
-      </Modal.Header>
+    <OLModal show={show} onHide={cancel} animation={animation}>
+      <OLModalHeader closeButton>
+        <OLModalTitle>{t('share_project')}</OLModalTitle>
+      </OLModalHeader>
 
-      <Modal.Body className="modal-body-share">
-        <Grid fluid>
+      <OLModalBody className="modal-body-share">
+        <div className="container-fluid">
           <Suspense fallback={<FullSizeLoadingSpinner minHeight="15rem" />}>
             {isRestrictedTokenMember ? (
               <ReadOnlyTokenLink />
@@ -50,33 +59,43 @@ export default function ShareProjectModalContent({
               <ShareModalBody />
             )}
           </Suspense>
-        </Grid>
-      </Modal.Body>
-
-      <Modal.Footer className="modal-footer-share">
-        <div className="modal-footer-left">
-          {inFlight && <Icon type="refresh" spin />}
           {error && (
-            <span className="text-danger error">
-              <ErrorMessage error={error} />
-            </span>
+            <OLNotification
+              type="error"
+              content={<ErrorMessage error={error} />}
+              className="mb-0 mt-3"
+            />
           )}
         </div>
-
-        <div className="modal-footer-right">
-          <ClickableElementEnhancer
-            onClick={cancel}
-            as={Button}
-            type="button"
-            bsStyle={null}
-            className="btn-secondary"
-            disabled={inFlight}
-          >
-            {t('close')}
-          </ClickableElementEnhancer>
+      </OLModalBody>
+
+      <OLModalFooter>
+        <div className={bsVersion({ bs3: 'pull-left', bs5: 'me-auto' })}>
+          {inFlight && (
+            <BootstrapVersionSwitcher
+              bs3={<Icon type="refresh" spin />}
+              bs5={
+                <Spinner
+                  animation="border"
+                  aria-hidden="true"
+                  size="sm"
+                  role="status"
+                />
+              }
+            />
+          )}
         </div>
-      </Modal.Footer>
-    </AccessibleModal>
+
+        <ClickableElementEnhancer
+          onClick={cancel}
+          as={OLButton}
+          variant="secondary"
+          disabled={inFlight}
+        >
+          {t('close')}
+        </ClickableElementEnhancer>
+      </OLModalFooter>
+    </OLModal>
   )
 }
 

+ 46 - 36
services/web/frontend/js/features/share-project-modal/components/transfer-ownership-modal.jsx

@@ -1,12 +1,21 @@
 import { useState } from 'react'
-import { Modal, Button } from 'react-bootstrap'
 import { Trans, useTranslation } from 'react-i18next'
 import PropTypes from 'prop-types'
 import Icon from '../../../shared/components/icon'
 import { transferProjectOwnership } from '../utils/api'
-import AccessibleModal from '../../../shared/components/accessible-modal'
 import { useProjectContext } from '../../../shared/context/project-context'
 import { useLocation } from '../../../shared/hooks/use-location'
+import OLModal, {
+  OLModalBody,
+  OLModalFooter,
+  OLModalHeader,
+  OLModalTitle,
+} from '@/features/ui/components/ol/ol-modal'
+import OLNotification from '@/features/ui/components/ol/ol-notification'
+import OLButton from '@/features/ui/components/ol/ol-button'
+import BootstrapVersionSwitcher from '@/features/ui/components/bootstrap-5/bootstrap-version-switcher'
+import { bsVersion } from '@/features/utils/bootstrap-5'
+import { Spinner } from 'react-bootstrap-5'
 
 export default function TransferOwnershipModal({ member, cancel }) {
   const { t } = useTranslation()
@@ -32,11 +41,11 @@ export default function TransferOwnershipModal({ member, cancel }) {
   }
 
   return (
-    <AccessibleModal show onHide={cancel}>
-      <Modal.Header closeButton>
-        <Modal.Title>{t('change_project_owner')}</Modal.Title>
-      </Modal.Header>
-      <Modal.Body>
+    <OLModal show onHide={cancel}>
+      <OLModalHeader closeButton>
+        <OLModalTitle>{t('change_project_owner')}</OLModalTitle>
+      </OLModalHeader>
+      <OLModalBody>
         <p>
           <Trans
             i18nKey="project_ownership_transfer_confirmation_1"
@@ -47,37 +56,38 @@ export default function TransferOwnershipModal({ member, cancel }) {
           />
         </p>
         <p>{t('project_ownership_transfer_confirmation_2')}</p>
-      </Modal.Body>
-      <Modal.Footer>
-        <div className="modal-footer-left">
-          {inflight && <Icon type="refresh" spin />}
-          {error && (
-            <span className="text-danger">
-              {t('generic_something_went_wrong')}
-            </span>
+        {error && (
+          <OLNotification
+            type="error"
+            content={t('generic_something_went_wrong')}
+            className="mb-0 mt-3"
+          />
+        )}
+      </OLModalBody>
+      <OLModalFooter>
+        <div className={bsVersion({ bs3: 'pull-left', bs5: 'me-auto' })}>
+          {inflight && (
+            <BootstrapVersionSwitcher
+              bs3={<Icon type="refresh" spin />}
+              bs5={
+                <Spinner
+                  animation="border"
+                  aria-hidden="true"
+                  size="sm"
+                  role="status"
+                />
+              }
+            />
           )}
         </div>
-        <div className="modal-footer-right">
-          <Button
-            type="button"
-            bsStyle={null}
-            className="btn-secondary"
-            onClick={cancel}
-            disabled={inflight}
-          >
-            {t('cancel')}
-          </Button>
-          <Button
-            type="button"
-            bsStyle="primary"
-            onClick={confirm}
-            disabled={inflight}
-          >
-            {t('change_owner')}
-          </Button>
-        </div>
-      </Modal.Footer>
-    </AccessibleModal>
+        <OLButton variant="secondary" onClick={cancel} disabled={inflight}>
+          {t('cancel')}
+        </OLButton>
+        <OLButton variant="primary" onClick={confirm} disabled={inflight}>
+          {t('change_owner')}
+        </OLButton>
+      </OLModalFooter>
+    </OLModal>
   )
 }
 TransferOwnershipModal.propTypes = {

+ 7 - 6
services/web/frontend/js/features/share-project-modal/components/view-member.jsx

@@ -1,15 +1,16 @@
 import PropTypes from 'prop-types'
-import { Col, Row } from 'react-bootstrap'
 import MemberPrivileges from './member-privileges'
+import OLRow from '@/features/ui/components/ol/ol-row'
+import OLCol from '@/features/ui/components/ol/ol-col'
 
 export default function ViewMember({ member }) {
   return (
-    <Row className="project-member">
-      <Col xs={7}>{member.email}</Col>
-      <Col xs={3}>
+    <OLRow className="project-member">
+      <OLCol xs={7}>{member.email}</OLCol>
+      <OLCol xs={3}>
         <MemberPrivileges privileges={member.privileges} />
-      </Col>
-    </Row>
+      </OLCol>
+    </OLRow>
   )
 }
 

+ 7 - 3
services/web/frontend/js/features/ui/components/bootstrap-5/icon-button.tsx

@@ -6,11 +6,15 @@ import type { IconButtonProps } from '@/features/ui/components/types/icon-button
 
 const IconButton = forwardRef<HTMLButtonElement, IconButtonProps>(
   (
-    { accessibilityLabel, icon, isLoading = false, size = 'default', ...props },
+    { accessibilityLabel, icon, isLoading = false, size, className, ...props },
     ref
   ) => {
-    const iconButtonClassName = `icon-button-${size}`
-    const iconSizeClassName = size === 'large' ? 'icon-large' : 'icon-small'
+    const iconButtonClassName = classNames(className, {
+      'icon-button': !size,
+      'icon-button-small': size === 'sm',
+      'icon-button-large': size === 'lg',
+    })
+    const iconSizeClassName = size === 'lg' ? 'icon-large' : 'icon-small'
     const materialIconClassName = classNames(iconSizeClassName, {
       'button-content-hidden': isLoading,
     })

+ 1 - 0
services/web/frontend/js/features/ui/components/ol/ol-button.tsx

@@ -38,6 +38,7 @@ export function bs3ButtonProps(props: ButtonProps) {
     target: props.target,
     rel: props.rel,
     onClick: props.onClick,
+    onMouseDown: props.onMouseDown as BS3ButtonProps['onMouseDown'],
     type: props.type,
     draggable: props.draggable,
     download: props.download,

+ 2 - 2
services/web/frontend/js/features/ui/components/ol/ol-col.tsx

@@ -11,8 +11,8 @@ function OLCol(props: OLColProps) {
 
   const getBs3Sizes = (obj: typeof rest) => {
     const bs5ToBs3SizeMap = {
-      xs: undefined,
-      sm: undefined,
+      xs: 'xs',
+      sm: 'xs',
       md: 'sm',
       lg: 'md',
       xl: 'lg',

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

@@ -14,6 +14,7 @@ export type ButtonProps = {
   isLoading?: boolean
   loadingLabel?: string
   onClick?: React.MouseEventHandler<HTMLButtonElement>
+  onMouseDown?: React.MouseEventHandler<HTMLButtonElement>
   onMouseOver?: React.MouseEventHandler<HTMLButtonElement>
   onMouseOut?: React.MouseEventHandler<HTMLButtonElement>
   onFocus?: React.FocusEventHandler<HTMLButtonElement>

+ 1 - 0
services/web/frontend/js/shared/components/copy-to-clipboard.tsx

@@ -73,6 +73,7 @@ const IconButton: FC<{
     <OLIconButton
       onClick={handleClick}
       variant="link"
+      size="sm"
       accessibilityLabel={t('copy')}
       className="copy-button"
       bs3Props={{ bsSize: 'xsmall' }}

+ 1 - 7
services/web/frontend/stylesheets/app/editor/share.less

@@ -62,7 +62,7 @@
     }
   }
 
-  .public-access-level.public-access-level--notice {
+  .public-access-level.public-access-level-notice {
     background-color: @gray-lightest;
     border-bottom: none;
     margin-top: @margin-md;
@@ -167,12 +167,6 @@
     }
   }
 }
-.modal-footer-share {
-  .modal-footer-left {
-    max-width: 70%;
-    text-align: left;
-  }
-}
 
 .copy-button:focus-within {
   outline: none;

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

@@ -39,3 +39,9 @@
   animation-name: bounce;
   transform-origin: center bottom;
 }
+
+.recaptcha-branding {
+  padding: var(--spacing-05) var(--spacing-05) 0 var(--spacing-05);
+  text-align: center;
+  font-size: $font-size-sm;
+}

+ 1 - 0
services/web/frontend/stylesheets/bootstrap-5/components/all.scss

@@ -26,3 +26,4 @@
 @import 'select';
 @import 'link';
 @import 'pagination';
+@import 'loading-spinner';

+ 1 - 1
services/web/frontend/stylesheets/bootstrap-5/components/button.scss

@@ -203,7 +203,7 @@
   padding: var(--spacing-01);
 }
 
-.icon-button-default {
+.icon-button {
   padding: var(--spacing-04);
 }
 

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

@@ -184,3 +184,8 @@
     margin-left: var(--spacing-04);
   }
 }
+
+%input-focus-style {
+  border-color: $input-focus-border-color;
+  box-shadow: $form-check-input-focus-box-shadow;
+}

+ 7 - 0
services/web/frontend/stylesheets/bootstrap-5/components/loading-spinner.scss

@@ -0,0 +1,7 @@
+.full-size-loading-spinner-container {
+  width: 100%;
+  height: 100%;
+  display: flex;
+  align-items: center;
+  justify-content: center;
+}

+ 2 - 0
services/web/frontend/stylesheets/bootstrap-5/pages/all.scss

@@ -15,5 +15,7 @@
 @import 'subscription';
 @import 'editor/pdf';
 @import 'editor/compile-button';
+@import 'editor/share';
+@import 'editor/tags-input';
 @import 'website-redesign';
 @import 'group-settings';

+ 188 - 0
services/web/frontend/stylesheets/bootstrap-5/pages/editor/share.scss

@@ -0,0 +1,188 @@
+.modal-body-share {
+  h3 {
+    border-bottom: 1px solid var(--neutral-30);
+    padding-bottom: calc(var(--line-height-03) / 4);
+    margin: 0;
+    font-size: var(--font-size-03);
+  }
+
+  .project-member.form-group {
+    margin-bottom: 0;
+  }
+
+  .project-member .remove-button {
+    font-size: inherit;
+    text-decoration: none;
+  }
+
+  .project-invite,
+  .public-access-level {
+    font-size: var(--font-size-02);
+    padding: calc(var(--line-height-03) / 2) 0;
+    border-bottom: 1px solid var(--neutral-30);
+  }
+
+  .public-access-level {
+    margin-top: calc(var(--line-height-03) / 4);
+    font-size: var(--font-size-02);
+    padding-bottom: var(--spacing-07);
+
+    .access-token-display-area {
+      margin-top: calc(var(--line-height-03) / 4);
+
+      .access-token-wrapper {
+        padding-top: calc(var(--line-height-03) / 4);
+
+        .access-token {
+          margin-top: calc(var(--line-height-03) / 4);
+          background-color: var(--neutral-10);
+          border: 1px solid var(--neutral-30);
+          padding: var(--spacing-03) var(--spacing-05);
+          display: flex;
+          align-items: center;
+          justify-content: space-between;
+
+          code {
+            font-size: inherit;
+          }
+        }
+      }
+    }
+
+    .btn-chevron {
+      padding: 0 calc(var(--line-height-03) / 2);
+      text-decoration: none;
+      color: var(--neutral-70);
+    }
+  }
+
+  .public-access-level.public-access-level-notice {
+    background-color: var(--neutral-10);
+    border-bottom: none;
+    margin-top: var(--spacing-07);
+    padding-top: var(--spacing-07);
+  }
+
+  .project-member,
+  .project-invite {
+    &:hover {
+      background-color: var(--neutral-10);
+    }
+  }
+
+  .project-member {
+    padding: calc(var(--line-height-03) / 2) 0;
+    font-size: var(--font-size-03);
+
+    .select-wrapper {
+      width: 100px;
+      margin-left: auto;
+
+      .select-trigger {
+        padding: 0 var(--spacing-05);
+        color: var(--neutral-70);
+        border: none;
+        background-color: transparent;
+
+        &:focus {
+          box-shadow: none;
+        }
+      }
+
+      .dropdown-menu {
+        li:last-child .dropdown-item {
+          color: var(--bs-danger);
+        }
+      }
+    }
+
+    .project-member-email-icon {
+      display: grid;
+      grid-template-columns: 2em auto;
+      align-items: center;
+      padding-bottom: var(--spacing-03);
+
+      .subtitle {
+        font-size: $font-size-sm;
+      }
+    }
+  }
+
+  .project-member .text-left,
+  .project-invite .text-left {
+    padding-left: var(--spacing-08);
+  }
+
+  .invite-controls {
+    .small {
+      padding: var(--spacing-01);
+      margin-bottom: 0;
+    }
+
+    padding: calc(var(--line-height-03) / 2);
+    background-color: var(--neutral-10);
+    margin-top: calc(var(--line-height-03) / 2);
+
+    form {
+      .form-group {
+        margin-bottom: calc(var(--line-height-03) / 2);
+
+        &:last-child {
+          margin-bottom: 0;
+        }
+      }
+
+      .privileges {
+        display: inline-block;
+        width: auto;
+      }
+
+      .tags-new .privileges {
+        background: transparent;
+        width: auto;
+        height: 30px;
+        font-size: var(--font-size-02);
+        border: none;
+        border-right: 5px solid transparent;
+      }
+    }
+
+    .add-collaborators-upgrade {
+      display: flex;
+      flex-direction: column;
+      align-items: center;
+      margin-bottom: var(--spacing-08);
+
+      .upgrade-actions {
+        display: flex;
+        gap: var(--spacing-07);
+      }
+    }
+  }
+}
+
+.copy-button:focus-within {
+  outline: none;
+}
+
+.modal-link-share-new {
+  .invite-controls {
+    padding: 0;
+    background-color: transparent;
+    margin-top: 0;
+  }
+
+  .public-access-level {
+    border: none;
+  }
+
+  .invite-warning {
+    margin-bottom: calc(var(--line-height-03) / 2);
+  }
+
+  .project-member-select {
+    display: flex;
+    align-items: center;
+    padding: 0;
+  }
+}

+ 68 - 0
services/web/frontend/stylesheets/bootstrap-5/pages/editor/tags-input.scss

@@ -0,0 +1,68 @@
+.tags-input *,
+.tags-input *::before,
+.tags-input *::after {
+  box-sizing: border-box;
+}
+
+.tags-input label.small {
+  font-weight: normal;
+}
+
+.tags-input .host {
+  position: relative;
+  height: 100%;
+}
+
+.tags-input .host:active,
+.tags-input[disabled] .host:focus {
+  outline: none;
+}
+
+.tags-input .tags {
+  appearance: textfield;
+  overflow: hidden;
+  word-wrap: break-word;
+  cursor: text;
+  background-color: var(--white);
+  height: 100%;
+  display: flex;
+  flex-wrap: wrap;
+
+  .badge-tag {
+    margin: var(--spacing-01);
+
+    .badge-tag-content {
+      max-width: initial;
+
+      .badge-content {
+        white-space: initial;
+        word-break: break-word;
+        text-align: initial;
+      }
+    }
+  }
+}
+
+.tags-input .tags:focus-within {
+  @extend %input-focus-style;
+}
+
+.tags-input .tags .input {
+  border: 0;
+  outline: none;
+  flex-grow: 1;
+  overflow: hidden;
+  text-overflow: ellipsis;
+
+  &::placeholder {
+    color: $input-placeholder-color;
+  }
+}
+
+.tags-input .tags .input.invalid-tag {
+  color: var(--bs-danger);
+}
+
+.tags-input .tags .input::-ms-clear {
+  display: none;
+}