Explorar o código

[web] Migrate PDF Logs to BS5 (#21062)

* [web] Migrate Logs components JSX to Bootstrap 5

* [web] Migrate logs.less to logs.scss

* [web] Remove unused class names

* [storybook] Define default Bootstrap version in Storybook

This prevents some warning in the console

* [storybook] Update pdf-preview.stories.jsx

* [storybook] Add pdf-log-entry.stories.tsx

* [storybook] Force re-renders when switching BS version

* [web] Keep files dropdown menu in bounds

* [web] Make files dropdown items not bold in BS5

* [web] Revert unrelated change

* [web] Fixup PreviewLogsPaneMaxEntries

* [web] Add style for log-entry-content-link

* [web] Replace log-entry by OLNotification in `PdfCodeCheckFailedNotice`

* [web] Use `BootstrapVersionSwitcher` instead of `isBootstrap5`

* [web] Rename `DropdownBS3` to `BS3Dropdown`

* [web] Reuse variables for `toolbar-height` and `toolbar-small-height`

* [web] Set `id` on `DropdownToggle` not `Dropdown`

* [web] Set `log-entry-btn-expand-collapse` in BS3 only

* [web] Remove `block: true` from StartFreeTrialButton in BS3

* [web] Remove unnecessary CSS in `.log-entry-header-link`

* [web] Use semantic color names

* Migrate the downloadable pdf file list to Bootstrap 5

* Remove nested BootstrapVersionSwitcher, fix "key" prop

* Update roles to: `<li role="menuitem">` `<a role="link">`

* Update `log-entry-header-link`: variant ghost and fix colors

---------

Co-authored-by: Rebeka <o.dekany@gmail.com>
GitOrigin-RevId: 89848970ab5d8a8c135335386caf24363f69a34c
Antoine Clausse hai 1 ano
pai
achega
30860ae9f9
Modificáronse 22 ficheiros con 578 adicións e 239 borrados
  1. 5 1
      services/web/.storybook/preview.tsx
  2. 3 0
      services/web/.storybook/utils/with-bootstrap-switcher.tsx
  3. 20 7
      services/web/frontend/js/features/pdf-preview/components/pdf-clear-cache-button.jsx
  4. 18 10
      services/web/frontend/js/features/pdf-preview/components/pdf-code-check-failed-notice.jsx
  5. 44 17
      services/web/frontend/js/features/pdf-preview/components/pdf-download-files-button.jsx
  6. 90 28
      services/web/frontend/js/features/pdf-preview/components/pdf-file-list.jsx
  7. 9 6
      services/web/frontend/js/features/pdf-preview/components/pdf-log-entry-raw-content.jsx
  8. 10 8
      services/web/frontend/js/features/pdf-preview/components/pdf-preview-error.jsx
  9. 8 3
      services/web/frontend/js/features/pdf-preview/components/stop-on-first-error-prompt.jsx
  10. 7 11
      services/web/frontend/js/features/pdf-preview/components/timeout-upgrade-prompt-new.tsx
  11. 21 11
      services/web/frontend/js/features/preview/components/preview-log-entry-header.jsx
  12. 17 6
      services/web/frontend/js/features/preview/components/preview-logs-pane-max-entries.jsx
  13. 2 0
      services/web/frontend/js/ide/human-readable-logs/HumanReadableLogsHints.jsx
  14. 75 0
      services/web/frontend/stories/pdf-log-entry.stories.tsx
  15. 23 7
      services/web/frontend/stories/pdf-preview.stories.jsx
  16. 0 39
      services/web/frontend/stylesheets/app/editor/logs.less
  17. 1 80
      services/web/frontend/stylesheets/app/editor/pdf.less
  18. 2 0
      services/web/frontend/stylesheets/bootstrap-5/abstracts/variables.scss
  19. 1 0
      services/web/frontend/stylesheets/bootstrap-5/pages/all.scss
  20. 203 0
      services/web/frontend/stylesheets/bootstrap-5/pages/editor/logs.scss
  21. 14 1
      services/web/frontend/stylesheets/bootstrap-5/pages/editor/pdf.scss
  22. 5 4
      services/web/frontend/stylesheets/bootstrap-5/pages/editor/toolbar.scss

+ 5 - 1
services/web/.storybook/preview.tsx

@@ -177,7 +177,11 @@ const preview: Preview = {
       return (
         <>
           {activeStyle && <style>{activeStyle.default}</style>}
-          <Story {...context} />
+          <Story
+            {...context}
+            // force re-renders when switching between Bootstrap versions
+            key={bootstrapVersion}
+          />
         </>
       )
     },

+ 3 - 0
services/web/.storybook/utils/with-bootstrap-switcher.tsx

@@ -9,6 +9,9 @@ export const bsVersionDecorator: Meta = {
       description: 'Bootstrap version for components',
       control: { type: 'inline-radio' },
       options: ['3', '5'],
+      table: {
+        defaultValue: { summary: '3' },
+      },
     },
   },
   args: {

+ 20 - 7
services/web/frontend/js/features/pdf-preview/components/pdf-clear-cache-button.jsx

@@ -1,8 +1,9 @@
 import Icon from '../../../shared/components/icon'
-import { Button } from 'react-bootstrap'
+import OLButton from '@/features/ui/components/ol/ol-button'
 import { useTranslation } from 'react-i18next'
 import { memo } from 'react'
 import { useDetachCompileContext as useCompileContext } from '../../../shared/context/detach-compile-context'
+import BootstrapVersionSwitcher from '@/features/ui/components/bootstrap-5/bootstrap-version-switcher'
 
 function PdfClearCacheButton() {
   const { compiling, clearCache, clearingCache } = useCompileContext()
@@ -10,17 +11,29 @@ function PdfClearCacheButton() {
   const { t } = useTranslation()
 
   return (
-    <Button
-      bsSize="small"
-      bsStyle="danger"
+    <OLButton
+      size="sm"
+      variant="danger"
       className="logs-pane-actions-clear-cache"
       onClick={() => clearCache()}
+      isLoading={clearingCache}
       disabled={clearingCache || compiling}
+      leadingIcon="delete"
     >
-      {clearingCache ? <Icon type="refresh" spin /> : <Icon type="trash-o" />}
-      &nbsp;
+      <BootstrapVersionSwitcher
+        bs3={
+          <>
+            {clearingCache ? (
+              <Icon type="refresh" spin />
+            ) : (
+              <Icon type="trash-o" />
+            )}
+            &nbsp;
+          </>
+        }
+      />
       <span>{t('clear_cached_files')}</span>
-    </Button>
+    </OLButton>
   )
 }
 

+ 18 - 10
services/web/frontend/js/features/pdf-preview/components/pdf-code-check-failed-notice.jsx

@@ -1,21 +1,29 @@
 import { memo } from 'react'
 import { useTranslation } from 'react-i18next'
 import Icon from '../../../shared/components/icon'
+import OLNotification from '@/features/ui/components/ol/ol-notification'
+import { bsVersion } from '@/features/utils/bootstrap-5'
+import BootstrapVersionSwitcher from '@/features/ui/components/bootstrap-5/bootstrap-version-switcher'
 
 function PdfCodeCheckFailedNotice() {
   const { t } = useTranslation()
-
   return (
-    <div className="log-entry">
-      <div className="log-entry-header log-entry-header-error">
-        <div className="log-entry-header-icon-container">
-          <Icon type="exclamation-triangle" fw />
-        </div>
-        <h3 className="log-entry-header-title">
+    <OLNotification
+      type="error"
+      content={
+        <>
+          <BootstrapVersionSwitcher
+            bs3={
+              <>
+                <Icon type="exclamation-triangle" fw />{' '}
+              </>
+            }
+          />
           {t('code_check_failed_explanation')}
-        </h3>
-      </div>
-    </div>
+        </>
+      }
+      className={bsVersion({ bs5: 'm-0', bs3: 'mb-2' })}
+    />
   )
 }
 

+ 44 - 17
services/web/frontend/js/features/pdf-preview/components/pdf-download-files-button.jsx

@@ -1,9 +1,16 @@
-import { Dropdown } from 'react-bootstrap'
+import { Dropdown as BS3Dropdown } from 'react-bootstrap'
+
+import {
+  Dropdown,
+  DropdownMenu,
+  DropdownToggle,
+} from '@/features/ui/components/bootstrap-5/dropdown-menu'
 import PdfFileList from './pdf-file-list'
 import ControlledDropdown from '../../../shared/components/controlled-dropdown'
 import { memo } from 'react'
 import { useTranslation } from 'react-i18next'
 import { useDetachCompileContext as useCompileContext } from '../../../shared/context/detach-compile-context'
+import BootstrapVersionSwitcher from '@/features/ui/components/bootstrap-5/bootstrap-version-switcher'
 
 function PdfDownloadFilesButton() {
   const { compiling, fileList } = useCompileContext()
@@ -11,22 +18,42 @@ function PdfDownloadFilesButton() {
   const { t } = useTranslation()
 
   return (
-    <ControlledDropdown
-      id="dropdown-files-logs-pane"
-      dropup
-      pullRight
-      disabled={compiling || !fileList}
-    >
-      <Dropdown.Toggle
-        className="dropdown-toggle btn-secondary-info btn-secondary"
-        title={t('other_logs_and_files')}
-        bsSize="small"
-        bsStyle={null}
-      />
-      <Dropdown.Menu id="dropdown-files-logs-pane-list">
-        <PdfFileList fileList={fileList} />
-      </Dropdown.Menu>
-    </ControlledDropdown>
+    <BootstrapVersionSwitcher
+      bs3={
+        <ControlledDropdown
+          id="dropdown-files-logs-pane"
+          dropup
+          pullRight
+          disabled={compiling || !fileList}
+        >
+          <BS3Dropdown.Toggle
+            className="dropdown-toggle btn-secondary-info btn-secondary"
+            title={t('other_logs_and_files')}
+            bsSize="small"
+            bsStyle={null}
+          />
+          <BS3Dropdown.Menu id="dropdown-files-logs-pane-list">
+            <PdfFileList fileList={fileList} />
+          </BS3Dropdown.Menu>
+        </ControlledDropdown>
+      }
+      bs5={
+        <Dropdown drop="up">
+          <DropdownToggle
+            id="dropdown-files-logs-pane"
+            variant="secondary"
+            title={t('other_logs_and_files')}
+            size="sm"
+            disabled={compiling || !fileList}
+          >
+            {t('other_logs_and_files')}
+          </DropdownToggle>
+          <DropdownMenu id="dropdown-files-logs-pane-list">
+            <PdfFileList fileList={fileList} />
+          </DropdownMenu>
+        </Dropdown>
+      }
+    />
   )
 }
 

+ 90 - 28
services/web/frontend/js/features/pdf-preview/components/pdf-file-list.jsx

@@ -1,7 +1,13 @@
-import { MenuItem } from 'react-bootstrap'
+import { MenuItem as BS3MenuItem } from 'react-bootstrap'
 import { useTranslation } from 'react-i18next'
 import { memo } from 'react'
 import PropTypes from 'prop-types'
+import BootstrapVersionSwitcher from '@/features/ui/components/bootstrap-5/bootstrap-version-switcher'
+import {
+  DropdownDivider,
+  DropdownHeader,
+  DropdownItem,
+} from '@/features/ui/components/bootstrap-5/dropdown-menu'
 
 function PdfFileList({ fileList }) {
   const { t } = useTranslation()
@@ -15,37 +21,93 @@ function PdfFileList({ fileList }) {
   }
 
   return (
-    <>
-      <MenuItem header>{t('other_output_files')}</MenuItem>
+    <BootstrapVersionSwitcher
+      bs3={
+        <>
+          <BS3MenuItem header>{t('other_output_files')}</BS3MenuItem>
 
-      {fileList.top.map(file => (
-        <MenuItem download={basename(file)} href={file.url} key={file.path}>
-          <b>{file.path}</b>
-        </MenuItem>
-      ))}
+          {fileList.top.map(file => (
+            <BS3MenuItem
+              download={basename(file)}
+              href={file.url}
+              key={file.path}
+            >
+              <b>{file.path}</b>
+            </BS3MenuItem>
+          ))}
 
-      {fileList.other.length > 0 && fileList.top.length > 0 && (
-        <MenuItem divider />
-      )}
+          {fileList.other.length > 0 && fileList.top.length > 0 && (
+            <BS3MenuItem divider />
+          )}
 
-      {fileList.other.map(file => (
-        <MenuItem download={basename(file)} href={file.url} key={file.path}>
-          <b>{file.path}</b>
-        </MenuItem>
-      ))}
+          {fileList.other.map(file => (
+            <BS3MenuItem
+              download={basename(file)}
+              href={file.url}
+              key={file.path}
+            >
+              <b>{file.path}</b>
+            </BS3MenuItem>
+          ))}
 
-      {fileList.archive?.fileCount > 0 && (
-        <MenuItem
-          download={basename(fileList.archive)}
-          href={fileList.archive.url}
-          key={fileList.archive.path}
-        >
-          <b>
-            {t('download_all')} ({fileList.archive.fileCount})
-          </b>
-        </MenuItem>
-      )}
-    </>
+          {fileList.archive?.fileCount && fileList.archive?.fileCount > 0 && (
+            <BS3MenuItem
+              download={basename(fileList.archive)}
+              href={fileList.archive.url}
+            >
+              <b>
+                {t('download_all')} ({fileList.archive.fileCount})
+              </b>
+            </BS3MenuItem>
+          )}
+        </>
+      }
+      bs5={
+        <>
+          <DropdownHeader>{t('other_output_files')}</DropdownHeader>
+
+          {fileList.top.map(file => (
+            <li key={file.path} role="menuitem">
+              <DropdownItem
+                role="link"
+                download={basename(file)}
+                href={file.url}
+              >
+                {file.path}
+              </DropdownItem>
+            </li>
+          ))}
+
+          {fileList.other.length > 0 && fileList.top.length > 0 && (
+            <DropdownDivider divider />
+          )}
+
+          {fileList.other.map(file => (
+            <li key={file.path} role="menuitem">
+              <DropdownItem
+                role="link"
+                download={basename(file)}
+                href={file.url}
+              >
+                {file.path}
+              </DropdownItem>
+            </li>
+          ))}
+
+          {fileList.archive?.fileCount > 0 && (
+            <li role="menuitem">
+              <DropdownItem
+                role="link"
+                download={basename(fileList.archive)}
+                href={fileList.archive.url}
+              >
+                {t('download_all')} ({fileList.archive.fileCount})
+              </DropdownItem>
+            </li>
+          )}
+        </>
+      }
+    />
   )
 }
 

+ 9 - 6
services/web/frontend/js/features/pdf-preview/components/pdf-log-entry-raw-content.jsx

@@ -2,7 +2,7 @@ import { useCallback, useState } from 'react'
 import { useResizeObserver } from '../../../shared/hooks/use-resize-observer'
 import { useTranslation } from 'react-i18next'
 import classNames from 'classnames'
-import { Button } from 'react-bootstrap'
+import OLButton from '@/features/ui/components/ol/ol-button'
 import Icon from '../../../shared/components/icon'
 import PropTypes from 'prop-types'
 
@@ -44,10 +44,13 @@ export default function PdfLogEntryRawContent({
             'log-entry-content-button-container-collapsed': !expanded,
           })}
         >
-          <Button
-            bsSize="xs"
-            bsStyle={null}
-            className="log-entry-btn-expand-collapse btn-secondary"
+          <OLButton
+            variant="secondary"
+            size="sm"
+            bs3Props={{
+              bsSize: 'xsmall',
+              className: 'log-entry-btn-expand-collapse',
+            }}
             onClick={() => setExpanded(value => !value)}
           >
             {expanded ? (
@@ -59,7 +62,7 @@ export default function PdfLogEntryRawContent({
                 <Icon type="angle-down" /> {t('expand')}
               </>
             )}
-          </Button>
+          </OLButton>
         </div>
       )}
     </div>

+ 10 - 8
services/web/frontend/js/features/pdf-preview/components/pdf-preview-error.jsx

@@ -1,7 +1,7 @@
 import PropTypes from 'prop-types'
 import { useTranslation, Trans } from 'react-i18next'
 import { memo, useCallback } from 'react'
-import { Button } from 'react-bootstrap'
+import OLButton from '@/features/ui/components/ol/ol-button'
 import PdfLogEntry from './pdf-log-entry'
 import { useDetachCompileContext as useCompileContext } from '../../../shared/context/detach-compile-context'
 import { useStopOnFirstError } from '../../../shared/hooks/use-stop-on-first-error'
@@ -23,9 +23,10 @@ function PdfPreviewError({ error }) {
                 i18nKey="something_went_wrong_rendering_pdf_expected"
                 components={[
                   // eslint-disable-next-line react/jsx-key
-                  <Button
-                    bsSize="xs"
-                    bsStyle="info"
+                  <OLButton
+                    variant="info"
+                    size="sm"
+                    bs3Props={{ bsSize: 'xsmall' }}
                     onClick={() => startCompile()}
                   />,
                 ]}
@@ -157,7 +158,7 @@ function PdfPreviewError({ error }) {
         <ErrorLogEntry title={t('no_pdf_error_title')}>
           {t('no_pdf_error_explanation')}
 
-          <ul className="log-entry-formatted-content-list">
+          <ul className="my-1 ps-3">
             <li>{t('no_pdf_error_reason_unrecoverable_error')}</li>
             <li>
               <Trans
@@ -282,9 +283,10 @@ function TimedOutLogEntry() {
                 i18nKey="project_timed_out_enable_stop_on_first_error"
                 components={[
                   // eslint-disable-next-line react/jsx-key
-                  <Button
-                    bsSize="xs"
-                    bsStyle="info"
+                  <OLButton
+                    variant="info"
+                    size="sm"
+                    bs3Props={{ bsSize: 'xsmall' }}
                     onClick={handleEnableStopOnFirstErrorClick}
                   />,
                 ]}

+ 8 - 3
services/web/frontend/js/features/pdf-preview/components/stop-on-first-error-prompt.jsx

@@ -1,6 +1,6 @@
 import { useCallback } from 'react'
 import { useTranslation, Trans } from 'react-i18next'
-import { Button } from 'react-bootstrap'
+import OLButton from '@/features/ui/components/ol/ol-button'
 import PdfLogEntry from './pdf-log-entry'
 import { useDetachCompileContext as useCompileContext } from '../../../shared/context/detach-compile-context'
 import { useStopOnFirstError } from '../../../shared/hooks/use-stop-on-first-error'
@@ -28,9 +28,14 @@ export default function StopOnFirstErrorPrompt() {
             // eslint-disable-next-line react/jsx-key
             components={[<strong />]}
           />{' '}
-          <Button bsSize="xs" bsStyle="info" onClick={handleDisableButtonClick}>
+          <OLButton
+            variant="info"
+            size="sm"
+            onClick={handleDisableButtonClick}
+            bs3Props={{ bsSize: 'xsmall' }}
+          >
             {t('disable_stop_on_first_error')}
-          </Button>
+          </OLButton>
         </>
       }
       level="info"

+ 7 - 11
services/web/frontend/js/features/pdf-preview/components/timeout-upgrade-prompt-new.tsx

@@ -4,7 +4,7 @@ import StartFreeTrialButton from '../../../shared/components/start-free-trial-bu
 import { memo, useCallback } from 'react'
 import PdfLogEntry from './pdf-log-entry'
 import { useStopOnFirstError } from '../../../shared/hooks/use-stop-on-first-error'
-import { Button } from 'react-bootstrap'
+import OLButton from '@/features/ui/components/ol/ol-button'
 import * as eventTracking from '../../../infrastructure/event-tracking'
 import { useFeatureFlag } from '@/shared/context/split-test-context'
 import getMeta from '@/utils/meta'
@@ -84,13 +84,7 @@ const CompileTimeout = memo(function CompileTimeout({
               <p className="text-center">
                 <StartFreeTrialButton
                   source="compile-timeout"
-                  buttonProps={{
-                    variant: 'primary',
-                    className: 'row-spaced-small',
-                    bs3Props: {
-                      block: true,
-                    },
-                  }}
+                  buttonProps={{ variant: 'primary', className: 'w-100' }}
                 >
                   {hasNewPaywallCta
                     ? t('get_more_compile_time')
@@ -178,10 +172,12 @@ const PreventTimeoutHelpMessage = memo(function PreventTimeoutHelpMessage({
                     i18nKey="enable_stop_on_first_error_under_recompile_dropdown_menu"
                     components={[
                       // eslint-disable-next-line react/jsx-key
-                      <Button
-                        bsSize="xs"
-                        bsStyle="info-ghost-inline"
+                      <OLButton
+                        variant="link"
+                        className="btn-inline-link fw-bold"
+                        size="sm"
                         onClick={handleEnableStopOnFirstErrorClick}
+                        bs3Props={{ bsSize: 'xsmall' }}
                       />,
                       // eslint-disable-next-line react/jsx-key
                       <strong />,

+ 21 - 11
services/web/frontend/js/features/preview/components/preview-log-entry-header.jsx

@@ -3,8 +3,11 @@ import classNames from 'classnames'
 import { useState, useRef } from 'react'
 import { useTranslation } from 'react-i18next'
 import useResizeObserver from '../hooks/use-resize-observer'
-import Tooltip from '../../../shared/components/tooltip'
+import OLTooltip from '@/features/ui/components/ol/ol-tooltip'
 import Icon from '../../../shared/components/icon'
+import OLButton from '@/features/ui/components/ol/ol-button'
+import BootstrapVersionSwitcher from '@/features/ui/components/bootstrap-5/bootstrap-version-switcher'
+import MaterialIcon from '@/shared/components/material-icon'
 
 function PreviewLogEntryHeader({
   sourceLocation,
@@ -72,18 +75,25 @@ function PreviewLogEntryHeader({
   // This essentially tells the browser that, althought the text is laid out from right-to-left,
   // the wrapped portion of text should follow left-to-right writing rules.
   const locationLink = locationLinkText ? (
-    <button
+    <OLButton
+      variant="ghost"
       className={logEntryLocationBtnClasses}
-      type="button"
       aria-label={headerLogLocationTitle}
       onClick={onSourceLocationClick}
     >
-      <Icon type="chain" />
-      &nbsp;
+      <BootstrapVersionSwitcher
+        bs3={
+          <>
+            <Icon type="chain" />
+            &nbsp;
+          </>
+        }
+        bs5={<MaterialIcon type="link" />}
+      />
       <span ref={logLocationSpanRef} className="log-entry-header-link-location">
         {`\u202A${locationLinkText}\u202C`}
       </span>
-    </button>
+    </OLButton>
   ) : null
 
   const headerTitleText = logType ? `${logType} ${headerTitle}` : headerTitle
@@ -95,26 +105,26 @@ function PreviewLogEntryHeader({
       ) : null}
       <h3 className="log-entry-header-title">{headerTitleText}</h3>
       {locationSpanOverflown && locationLinkText ? (
-        <Tooltip
+        <OLTooltip
           id={locationLinkText}
           description={locationLinkText}
           overlayProps={{ placement: 'left' }}
           tooltipProps={{ className: 'log-location-tooltip' }}
         >
           {locationLink}
-        </Tooltip>
+        </OLTooltip>
       ) : (
         locationLink
       )}
       {showCloseButton ? (
-        <button
+        <OLButton
+          variant="link"
           className="btn-inline-link log-entry-header-link"
-          type="button"
           aria-label={t('dismiss_error_popup')}
           onClick={onClose}
         >
           <span aria-hidden="true">&times;</span>
-        </button>
+        </OLButton>
       ) : null}
     </header>
   )

+ 17 - 6
services/web/frontend/js/features/preview/components/preview-logs-pane-max-entries.jsx

@@ -1,11 +1,13 @@
 import { useCallback } from 'react'
 import PropTypes from 'prop-types'
 import { Trans, useTranslation } from 'react-i18next'
-import { Button } from 'react-bootstrap'
+import OLButton from '@/features/ui/components/ol/ol-button'
 import PreviewLogEntryHeader from './preview-log-entry-header'
 import Icon from '../../../shared/components/icon'
 import { useDetachCompileContext as useCompileContext } from '../../../shared/context/detach-compile-context'
 import { useStopOnFirstError } from '../../../shared/hooks/use-stop-on-first-error'
+import MaterialIcon from '@/shared/components/material-icon'
+import BootstrapVersionSwitcher from '@/features/ui/components/bootstrap-5/bootstrap-version-switcher'
 
 function PreviewLogsPaneMaxEntries({ totalEntries, entriesShown, hasErrors }) {
   const { t } = useTranslation()
@@ -34,16 +36,22 @@ function PreviewLogsPaneMaxEntries({ totalEntries, entriesShown, hasErrors }) {
           {hasErrors && !stoppedOnFirstError ? (
             <>
               <p>
-                <Icon type="lightbulb-o" />
+                <BootstrapVersionSwitcher
+                  bs3={<Icon type="lightbulb-o" />}
+                  bs5={
+                    <MaterialIcon type="lightbulb" className="align-middle" />
+                  }
+                />
                 &nbsp;
                 <strong>{t('tip')}: </strong>
                 <Trans
                   i18nKey="log_entry_maximum_entries_enable_stop_on_first_error"
                   components={[
-                    <Button
+                    <OLButton
+                      variant="info"
+                      size="sm"
                       key="enable-stop-on-first-error"
-                      bsSize="xs"
-                      bsStyle="info"
+                      bs3Props={{ bsSize: 'xsmall' }}
                       onClick={handleEnableStopOnFirstErrorClick}
                     />,
                     // eslint-disable-next-line jsx-a11y/anchor-has-content
@@ -58,7 +66,10 @@ function PreviewLogsPaneMaxEntries({ totalEntries, entriesShown, hasErrors }) {
             </>
           ) : (
             <p>
-              <Icon type="lightbulb-o" />
+              <BootstrapVersionSwitcher
+                bs3={<Icon type="lightbulb-o" />}
+                bs5={<MaterialIcon type="lightbulb" className="align-middle" />}
+              />
               &nbsp;
               <strong>{t('tip')}: </strong>
               {t('log_entry_maximum_entries_see_full_logs')}

+ 2 - 0
services/web/frontend/js/ide/human-readable-logs/HumanReadableLogsHints.jsx

@@ -508,6 +508,8 @@ const hints = {
   },
 }
 
+export const ruleIds = Object.keys(hints)
+
 if (!getMeta('ol-wikiEnabled')) {
   Object.keys(hints).forEach(ruleId => {
     hints[ruleId].extraInfoURL = null

+ 75 - 0
services/web/frontend/stories/pdf-log-entry.stories.tsx

@@ -0,0 +1,75 @@
+import PdfLogEntry from '@/features/pdf-preview/components/pdf-log-entry'
+import type { Meta, StoryObj } from '@storybook/react'
+import { bsVersionDecorator } from '../../.storybook/utils/with-bootstrap-switcher'
+import { ruleIds } from '@/ide/human-readable-logs/HumanReadableLogsHints'
+import { ScopeDecorator } from './decorators/scope'
+import { useMeta } from './hooks/use-meta'
+import { FC, ReactNode } from 'react'
+import { useScope } from './hooks/use-scope'
+import { EditorView } from '@codemirror/view'
+
+const fakeSourceLocation = {
+  file: 'file.tex',
+  line: 12,
+  column: 5,
+}
+
+const fakeLogEntry = {
+  key: 'fake',
+  ruleId: 'hint_misplaced_alignment_tab_character',
+  message: 'Fake message',
+  messageComponent: 'Fake message component',
+  content: 'Fake content',
+  type: 'Error: ',
+  level: 'error',
+  contentDetails: ['Fake detail 1', 'Fake detail 2'],
+  file: 'fake.tex',
+  line: 12,
+  column: 5,
+  raw: 'Fake raw',
+}
+
+const fakeArgs = {
+  headerTitle: 'PDF Preview',
+  formattedContent: 'This is a log entry',
+  level: 'error' as const,
+  extraInfoURL: 'https://example.com',
+  showCloseButton: true,
+  showSourceLocationLink: true,
+  rawContent: 'This is a raw log entry',
+  contentDetails: ['detail 1', 'detail 2'],
+  ruleId: 'hint_misplaced_alignment_tab_character' as const,
+  sourceLocation: fakeSourceLocation,
+  logEntry: fakeLogEntry,
+  logType: 'Fake type',
+}
+
+const meta: Meta<typeof PdfLogEntry> = {
+  title: 'Editor / PDF Preview / Logs',
+  component: PdfLogEntry,
+  // @ts-ignore
+  decorators: [ScopeDecorator],
+  argTypes: {
+    ruleId: { control: 'select', options: [...ruleIds, 'other'] },
+    ...bsVersionDecorator.argTypes,
+  },
+  args: fakeArgs,
+}
+
+export default meta
+
+type Story = StoryObj<typeof PdfLogEntry>
+
+const Provider: FC<{ children: ReactNode }> = ({ children }) => {
+  useMeta({ 'ol-showAiErrorAssistant': true })
+  useScope({ 'editor.view': new EditorView({ doc: '\\begin{document' }) })
+  return <div className="logs-pane p-2">{children}</div>
+}
+
+export const PdfLogEntryWithControls: Story = {
+  render: args => (
+    <Provider>
+      <PdfLogEntry {...args} />
+    </Provider>
+  ),
+}

+ 23 - 7
services/web/frontend/stories/pdf-preview.stories.jsx

@@ -23,6 +23,11 @@ import { cloneDeep } from 'lodash'
 import { ScopeDecorator } from './decorators/scope'
 import { PdfPreviewProvider } from '@/features/pdf-preview/components/pdf-preview-provider'
 import { bsVersionDecorator } from '../../.storybook/utils/with-bootstrap-switcher'
+import {
+  Dropdown,
+  DropdownMenu,
+} from '@/features/ui/components/bootstrap-5/dropdown-menu'
+import BootstrapVersionSwitcher from '@/features/ui/components/bootstrap-5/bootstrap-version-switcher'
 
 export default {
   title: 'Editor / PDF Preview',
@@ -286,7 +291,7 @@ export const DisplayError = () => {
   })
 
   return (
-    <>
+    <div className="logs-pane">
       {compileErrors.map(error => (
         <div
           key={error}
@@ -296,7 +301,7 @@ export const DisplayError = () => {
           <PdfPreviewError error={error} />
         </div>
       ))}
-    </>
+    </div>
   )
 }
 
@@ -320,11 +325,22 @@ export const FileList = () => {
   }, [])
 
   return (
-    <div className="dropdown open">
-      <div className="dropdown-menu">
-        <PdfFileList fileList={fileList} />
-      </div>
-    </div>
+    <BootstrapVersionSwitcher
+      bs3={
+        <div className="dropdown open">
+          <div className="dropdown-menu">
+            <PdfFileList fileList={fileList} />
+          </div>
+        </div>
+      }
+      bs5={
+        <Dropdown>
+          <DropdownMenu id="dropdown-files-logs-pane-list" show>
+            <PdfFileList fileList={fileList} />
+          </DropdownMenu>
+        </Dropdown>
+      }
+    />
   )
 }
 

+ 0 - 39
services/web/frontend/stylesheets/app/editor/logs.less

@@ -32,11 +32,6 @@
   overflow: hidden;
 }
 
-.log-entry-first-error-popup {
-  border-radius: 0;
-  overflow: auto;
-}
-
 .log-entry-header {
   padding: 3px @padding-sm;
   display: flex;
@@ -182,40 +177,6 @@
   }
 }
 
-.log-entry-formatted-content-list {
-  margin: @margin-xs 0;
-  padding-left: @padding-md;
-}
-
-.first-error-popup {
-  position: absolute;
-  z-index: 1;
-  top: @toolbar-small-height + 2px;
-  right: @padding-xs;
-  width: 90%;
-  max-width: 450px;
-  box-shadow: 0 4px 4px rgba(0, 0, 0, 0.25);
-  animation: fade-in 0.15s linear 0s 1 none;
-  background-color: #fff;
-  &::before {
-    content: '';
-    .triangle(top, @padding-sm, @padding-xs, @ol-red);
-    top: -@padding-xs;
-    right: @padding-xl;
-  }
-}
-
-.first-error-popup-actions {
-  display: flex;
-  justify-content: space-between;
-  padding: 0 @padding-sm @padding-sm @padding-sm;
-  border-radius: 0 0 @border-radius-base @border-radius-base;
-}
-
-.first-error-btn {
-  .no-outline-ring-on-click;
-}
-
 .log-location-tooltip {
   word-break: break-all;
   &.tooltip.in {

+ 1 - 80
services/web/frontend/stylesheets/app/editor/pdf.less

@@ -113,14 +113,12 @@
 }
 
 .pdf-viewer,
-.pdf-logs,
 .pdf-errors,
 .pdf-uncompiled {
   .full-size;
   top: @pdf-top-offset;
 }
 
-.pdf-logs,
 .pdf-errors,
 .pdf-uncompiled,
 .pdf-validation-problems {
@@ -327,72 +325,6 @@
   }
 }
 
-.pdf-logs {
-  overflow: auto;
-  .alert {
-    font-size: 0.9rem;
-    margin-bottom: @line-height-computed / 2;
-    cursor: pointer;
-    .line-no {
-      float: right;
-      color: @log-line-no-color;
-      font-weight: 700;
-
-      .fa {
-        opacity: 0;
-      }
-    }
-    .entry-message {
-      font-weight: 700;
-      //font-family: @font-family-monospace;
-    }
-    .entry-content {
-      white-space: pre-wrap;
-      font-size: 0.8rem;
-      //font-family: @font-family-monospace;
-    }
-
-    &:hover .line-no {
-      color: inherit;
-      .fa {
-        opacity: 1;
-      }
-    }
-
-    &.alert-danger {
-      background-color: tint(@alert-danger-bg, 15%);
-      &:hover {
-        background-color: @alert-danger-bg;
-      }
-    }
-
-    &.alert-warning {
-      background-color: tint(@alert-warning-bg, 15%);
-      &:hover {
-        background-color: @alert-warning-bg;
-      }
-    }
-
-    &.alert-info {
-      background-color: tint(@alert-info-bg, 15%);
-      &:hover {
-        background-color: @alert-info-bg;
-      }
-    }
-  }
-  pre {
-    font-size: 12px;
-    white-space: pre-wrap;
-  }
-  .dropdown {
-    position: relative;
-  }
-  .force-recompile {
-    margin-top: 10px;
-    text-align: right;
-  }
-}
-
 .synctex-controls {
   margin-right: -8px;
   position: absolute;
@@ -443,9 +375,6 @@
 }
 
 .editor-dark {
-  .pdf-logs {
-    background-color: lighten(@editor-dark-background-color, 10%);
-  }
   .pdfjs-viewer {
     background-color: lighten(@editor-dark-background-color, 10%);
   }
@@ -604,21 +533,13 @@
 
 @editor-and-logs-pane-toolbars-height: @toolbar-small-height + @toolbar-height;
 @btn-small-height: (@padding-small-vertical * 2)+ (@font-size-small *
-      @line-height-small);
+      @line-height-small); // 5px * 2 + 14px * 1.5 = 31px
 
-#download-dropdown-list,
 #dropdown-files-logs-pane-list {
   overflow-y: auto;
   .dropdown-header {
     white-space: nowrap;
   }
-}
-#download-dropdown-list {
-  max-height: calc(
-    ~'100vh - ' @editor-and-logs-pane-toolbars-height ~' - ' @margin-md
-  );
-}
-#dropdown-files-logs-pane-list {
   max-height: calc(
     ~'100vh - ' @editor-and-logs-pane-toolbars-height ~' - ' @btn-small-height ~' - '
       @margin-md

+ 2 - 0
services/web/frontend/stylesheets/bootstrap-5/abstracts/variables.scss

@@ -3,6 +3,8 @@ $footer-height: 50px;
 
 // Header
 $header-height: 68px;
+$toolbar-height: 40px;
+$toolbar-small-height: 32px;
 
 // Forms
 $form-group-margin-bottom: $spacing-06;

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

@@ -9,6 +9,7 @@
 @import 'editor/hotkeys';
 @import 'editor/left-menu';
 @import 'editor/loading-screen';
+@import 'editor/logs';
 @import 'editor/outline';
 @import 'editor/file-tree';
 @import 'editor/file-view';

+ 203 - 0
services/web/frontend/stylesheets/bootstrap-5/pages/editor/logs.scss

@@ -0,0 +1,203 @@
+@import '../../foundations/colors';
+
+.logs-pane {
+  position: absolute;
+  inset: 0;
+  overflow-y: auto;
+  background-color: var(--bg-dark-secondary);
+  z-index: 11; // above the PDF viewer + controls
+  top: var(--toolbar-small-height);
+
+  .logs-pane-content {
+    display: flex;
+    flex-direction: column;
+    padding: 10px;
+    gap: 10px;
+    min-height: 100%;
+  }
+
+  .logs-pane-actions {
+    display: flex;
+    flex-wrap: wrap;
+    place-content: flex-end flex-end;
+    padding: var(--spacing-03) 0;
+    flex-grow: 1;
+    align-items: flex-end;
+    gap: var(--spacing-04);
+  }
+
+  .log-entry {
+    border-radius: var(--border-radius-base);
+    overflow: hidden;
+  }
+
+  .log-entry-header {
+    padding: var(--spacing-02) var(--spacing-04);
+    display: flex;
+    align-items: flex-start;
+    gap: var(--spacing-04);
+    border-radius: var(--border-radius-base) var(--border-radius-base) 0 0;
+    color: var(--content-primary-dark);
+
+    .material-symbols {
+      @include body-base;
+    }
+  }
+
+  .log-entry-header-error {
+    background-color: var(--content-danger);
+  }
+
+  .log-entry-header-link-error {
+    @include ol-button-variant(
+      $color: var(--content-primary-dark),
+      $background: var(--bg-danger-02),
+      $hover-background: var(--red-70)
+    );
+  }
+
+  .log-entry-header-warning {
+    background-color: var(--content-warning-dark);
+  }
+
+  .log-entry-header-link-warning {
+    @include ol-button-variant(
+      $color: var(--content-primary-dark),
+      $background: var(--bg-warning-01),
+      $hover-background: var(--bg-warning-02)
+    );
+  }
+
+  .log-entry-header-typesetting {
+    background-color: var(--blue-50);
+  }
+
+  .log-entry-header-link-typesetting {
+    @include ol-button-variant(
+      $color: var(--content-primary-dark),
+      $background: var(--blue-60),
+      $hover-background: var(--blue-70)
+    );
+  }
+
+  .log-entry-header-raw,
+  .log-entry-header-info {
+    background-color: var(--bg-dark-tertiary);
+  }
+
+  .log-entry-header-link-raw,
+  .log-entry-header-link-info {
+    @include ol-button-variant(
+      $color: var(--content-primary-dark),
+      $background: var(--bg-dark-secondary),
+      $hover-background: var(--bg-dark-primary)
+    );
+  }
+
+  .log-entry-header-success {
+    background-color: var(--green-50);
+  }
+
+  .log-entry-header-link-success {
+    @include ol-button-variant(
+      $color: var(--content-primary-dark),
+      $background: var(--green-60),
+      $hover-background: var(--green-70)
+    );
+  }
+
+  .log-entry-header-title {
+    @include body-base;
+
+    flex-grow: 1;
+    font-weight: bold;
+    margin: 0;
+    color: var(--content-primary-dark);
+  }
+
+  .log-entry-header-link {
+    color: var(--content-primary-dark);
+    border-width: 0;
+    max-width: 33%;
+    text-decoration: none; // needed for the "close button"
+    padding: 0 var(--spacing-03);
+
+    .button-content {
+      min-width: 0; // needed to display the ellipsis on overflow
+    }
+  }
+
+  .log-entry-header-link-location {
+    white-space: nowrap;
+    direction: rtl;
+    text-overflow: ellipsis;
+    overflow: hidden;
+  }
+
+  .log-entry-content {
+    background-color: var(--bg-light-primary);
+    padding: var(--spacing-04);
+  }
+
+  .log-entry-content-raw {
+    font-size: var(--font-size-01);
+    color: var(--content-secondary);
+    padding: var(--spacing-03);
+    margin: 0;
+    white-space: pre-wrap;
+  }
+
+  .log-entry-content-button-container {
+    position: relative;
+    height: 40px;
+    margin-top: 0;
+    transition:
+      margin 0.15s ease-in-out,
+      opacity 0.15s ease-in-out;
+    padding-bottom: var(--spacing-04);
+    text-align: center;
+    background-image: linear-gradient(
+      0deg,
+      var(--bg-light-tertiary) 0%,
+      transparent 100%
+    );
+    border-radius: 0 0 var(--border-radius-base) var(--border-radius-base);
+  }
+
+  .log-entry-content-button-container-collapsed {
+    margin-top: -40px;
+  }
+
+  .log-entry-content-raw-container {
+    background-color: var(--bg-light-tertiary);
+    border-radius: var(--border-radius-base);
+    overflow: hidden;
+    margin-top: var(--spacing-03);
+  }
+}
+
+.btn-secondary-compile-timeout-override {
+  color: var(--content-primary);
+  background-color: var(--bg-light-primary);
+  border-color: var(--border-primary);
+  border-width: 1px;
+}
+
+.log-entry-formatted-content,
+.log-entry-content-link {
+  font-size: var(--font-size-02);
+  margin-top: var(--spacing-02);
+
+  &:first-of-type {
+    margin-top: 0;
+  }
+}
+
+.log-location-tooltip {
+  word-break: break-all;
+
+  & > .tooltip-inner {
+    max-width: 450px;
+    text-align: left;
+  }
+}

+ 14 - 1
services/web/frontend/stylesheets/bootstrap-5/pages/editor/pdf.scss

@@ -136,7 +136,6 @@
 }
 
 .pdf-viewer,
-.pdf-logs,
 .pdf-errors,
 .pdf-uncompiled {
   @extend .full-size;
@@ -378,3 +377,17 @@
     max-width: none;
   }
 }
+
+#dropdown-files-logs-pane-list {
+  overflow-y: auto;
+
+  .dropdown-header {
+    white-space: nowrap;
+  }
+
+  // This keeps the dropdown menu inside the Logs div.
+  // "spacing-11" is there to compensate the button height and the margin around the logs.
+  max-height: calc(
+    100vh - #{$toolbar-small-height + $toolbar-height + $spacing-11}
+  );
+}

+ 5 - 4
services/web/frontend/stylesheets/bootstrap-5/pages/editor/toolbar.scss

@@ -1,3 +1,5 @@
+@import '../../abstracts/variables';
+
 :root {
   --toolbar-border-color: var(--neutral-80);
   --toolbar-header-bg-color: var(--neutral-90);
@@ -7,7 +9,8 @@
   --toolbar-btn-hover-color: var(--white);
   --toolbar-btn-active-color: var(--white);
   --toolbar-btn-active-bg-color: var(--green-50);
-  --toolbar-small-height: 32px;
+  --toolbar-height: #{$toolbar-height};
+  --toolbar-small-height: #{$toolbar-small-height};
   --toolbar-alt-bg-color: var(--neutral-80);
   --formatting-btn-color: var(--white);
   --formatting-btn-bg: var(--neutral-80);
@@ -208,8 +211,6 @@
   }
 
   &.toolbar-header {
-    --toolbar-height: 40px;
-
     align-items: stretch;
     background-color: var(--toolbar-header-bg-color);
     position: absolute;
@@ -315,7 +316,7 @@
 }
 
 .toolbar-editor {
-  height: 32px;
+  height: var(--toolbar-small-height);
   background-color: var(--editor-toolbar-bg);
   padding: 0 5px;
   overflow: hidden;