diff --git a/services/web/frontend/js/features/project-list/components/add-affiliation.tsx b/services/web/frontend/js/features/project-list/components/add-affiliation.tsx index 151ecf9548..de4a130bfc 100644 --- a/services/web/frontend/js/features/project-list/components/add-affiliation.tsx +++ b/services/web/frontend/js/features/project-list/components/add-affiliation.tsx @@ -1,8 +1,8 @@ import { useTranslation } from 'react-i18next' -import { Button } from 'react-bootstrap' import { useProjectListContext } from '../context/project-list-context' import getMeta from '../../../utils/meta' import classNames from 'classnames' +import OLButton from '@/features/ui/components/ol/ol-button' export function useAddAffiliation() { const { totalProjectsCount } = useProjectListContext() @@ -26,18 +26,14 @@ function AddAffiliation({ className }: AddAffiliationProps) { return null } - const classes = classNames('text-centered', 'add-affiliation', className) + const classes = classNames('text-center', 'add-affiliation', className) return (

{t('are_you_affiliated_with_an_institution')}

- +
) } diff --git a/services/web/frontend/js/features/project-list/components/color-picker/color-picker.tsx b/services/web/frontend/js/features/project-list/components/color-picker/color-picker.tsx index 4160e2fece..ab0a373b2e 100644 --- a/services/web/frontend/js/features/project-list/components/color-picker/color-picker.tsx +++ b/services/web/frontend/js/features/project-list/components/color-picker/color-picker.tsx @@ -1,9 +1,9 @@ -import Icon from '../../../../shared/components/icon' import useSelectColor from '../../hooks/use-select-color' import { SketchPicker } from 'react-color' import { useEffect, useState } from 'react' import { useTranslation } from 'react-i18next' -import Tooltip from '../../../../shared/components/tooltip' +import OLTooltip from '@/features/ui/components/ol/ol-tooltip' +import MaterialIcon from '@/shared/components/material-icon' const PRESET_COLORS: ReadonlyArray<{ color: string; name: string }> = [ { color: '#A7B1C2', name: 'Grey' }, @@ -48,7 +48,7 @@ function ColorPickerItem({ color, name }: ColorPickerItemProps) { {t('select_color', { name })} {!pickingCustomColor && color === selectedColor && ( - + )} ) @@ -95,19 +95,25 @@ function MoreButton() { tabIndex={0} onKeyDown={handleKeyDown} > - - {isCustomColorSelected ? ( - - ) : showCustomPicker ? ( - - ) : ( - - )} - +
+ {isCustomColorSelected ? ( + + ) : showCustomPicker ? ( + + ) : ( + + )} +
+ {showCustomPicker && ( <> diff --git a/services/web/frontend/js/features/project-list/components/modals/create-tag-modal.tsx b/services/web/frontend/js/features/project-list/components/modals/create-tag-modal.tsx index bd906687f4..cf2eb62e97 100644 --- a/services/web/frontend/js/features/project-list/components/modals/create-tag-modal.tsx +++ b/services/web/frontend/js/features/project-list/components/modals/create-tag-modal.tsx @@ -1,8 +1,6 @@ import { useCallback, useEffect, useState } from 'react' -import { Button, ControlLabel, Form, FormGroup, Modal } from 'react-bootstrap' import { useTranslation } from 'react-i18next' import { Tag } from '../../../../../../app/src/Features/Tags/types' -import AccessibleModal from '../../../../shared/components/accessible-modal' import useAsync from '../../../../shared/hooks/use-async' import { useProjectListContext } from '../../context/project-list-context' import { useRefWithAutoFocus } from '../../../../shared/hooks/use-ref-with-auto-focus' @@ -11,6 +9,18 @@ import { createTag } from '../../util/api' import { MAX_TAG_LENGTH } from '../../util/tag' import { ColorPicker } from '../color-picker/color-picker' import { debugConsole } from '@/utils/debugging' +import OLModal, { + OLModalBody, + OLModalFooter, + OLModalHeader, + OLModalTitle, +} from '@/features/ui/components/ol/ol-modal' +import OLFormGroup from '@/features/ui/components/ol/ol-form-group' +import OLFormLabel from '@/features/ui/components/ol/ol-form-label' +import OLButton from '@/features/ui/components/ol/ol-button' +import OLNotification from '@/features/ui/components/ol/ol-notification' +import OLFormControl from '@/features/ui/components/ol/ol-form-control' +import OLForm from '@/features/ui/components/ol/ol-form' type CreateTagModalProps = { id: string @@ -30,7 +40,7 @@ export default function CreateTagModal({ const { tags } = useProjectListContext() const { selectedColor } = useSelectColor() const { t } = useTranslation() - const { isError, runAsync, status } = useAsync() + const { isLoading, isError, runAsync, status } = useAsync() const { autoFocusedRef } = useRefWithAutoFocus() const [tagName, setTagName] = useState() @@ -69,60 +79,63 @@ export default function CreateTagModal({ } return ( - - - {t('create_new_tag')} - + + + {t('create_new_tag')} + - -
- - - + + + {t('new_tag_name')} + setTagName(e.target.value)} ref={autoFocusedRef} required type="text" /> - - -
-
- - + + {validationError && ( -
- {validationError} -
+ )} {isError && ( -
- - {t('generic_something_went_wrong')} - -
+ )} - - -
-
+ {t('create')} + + + ) } diff --git a/services/web/frontend/js/features/project-list/components/modals/delete-tag-modal.tsx b/services/web/frontend/js/features/project-list/components/modals/delete-tag-modal.tsx index 0fa2e8c4eb..b66b2a5b8d 100644 --- a/services/web/frontend/js/features/project-list/components/modals/delete-tag-modal.tsx +++ b/services/web/frontend/js/features/project-list/components/modals/delete-tag-modal.tsx @@ -1,11 +1,17 @@ import { useCallback } from 'react' -import { Button, Modal } from 'react-bootstrap' import { useTranslation } from 'react-i18next' import { Tag } from '../../../../../../app/src/Features/Tags/types' -import AccessibleModal from '../../../../shared/components/accessible-modal' import useAsync from '../../../../shared/hooks/use-async' import { deleteTag } from '../../util/api' import { debugConsole } from '@/utils/debugging' +import OLModal, { + OLModalBody, + OLModalFooter, + OLModalHeader, + OLModalTitle, +} from '@/features/ui/components/ol/ol-modal' +import OLButton from '@/features/ui/components/ol/ol-button' +import OLNotification from '@/features/ui/components/ol/ol-notification' type DeleteTagModalProps = { id: string @@ -39,42 +45,40 @@ export default function DeleteTagModal({ } return ( - - - {t('delete_tag')} - + + + {t('delete_tag')} + - + {t('about_to_delete_tag')}
  • {tag.name}
-
- - {isError && ( -
- - {t('generic_something_went_wrong')} - -
+ )} - - -
-
+ {t('delete')} + + + ) } diff --git a/services/web/frontend/js/features/project-list/components/modals/edit-tag-modal.tsx b/services/web/frontend/js/features/project-list/components/modals/edit-tag-modal.tsx index 31593d1df2..621f3ed8d8 100644 --- a/services/web/frontend/js/features/project-list/components/modals/edit-tag-modal.tsx +++ b/services/web/frontend/js/features/project-list/components/modals/edit-tag-modal.tsx @@ -1,8 +1,6 @@ import { useCallback, useEffect, useState } from 'react' -import { Button, ControlLabel, Form, FormGroup, Modal } from 'react-bootstrap' import { useTranslation } from 'react-i18next' import { Tag } from '../../../../../../app/src/Features/Tags/types' -import AccessibleModal from '../../../../shared/components/accessible-modal' import useAsync from '../../../../shared/hooks/use-async' import { useProjectListContext } from '../../context/project-list-context' import { useRefWithAutoFocus } from '../../../../shared/hooks/use-ref-with-auto-focus' @@ -11,6 +9,17 @@ import { editTag } from '../../util/api' import { getTagColor, MAX_TAG_LENGTH } from '../../util/tag' import { ColorPicker } from '../color-picker/color-picker' import { debugConsole } from '@/utils/debugging' +import OLModal, { + OLModalBody, + OLModalFooter, + OLModalHeader, + OLModalTitle, +} from '@/features/ui/components/ol/ol-modal' +import OLForm from '@/features/ui/components/ol/ol-form' +import OLFormGroup from '@/features/ui/components/ol/ol-form-group' +import OLFormLabel from '@/features/ui/components/ol/ol-form-label' +import OLButton from '@/features/ui/components/ol/ol-button' +import OLNotification from '@/features/ui/components/ol/ol-notification' type EditTagModalProps = { id: string @@ -77,14 +86,14 @@ export function EditTagModal({ id, tag, onEdit, onClose }: EditTagModalProps) { } return ( - - - {t('edit_tag')} - + + + {t('edit_tag')} + - -
- + + + setNewTagName(e.target.value)} /> - - -
-
- - + + {validationError && ( -
- {validationError} -
+ )} {isError && ( -
- - {t('generic_something_went_wrong')} - -
+ )} - - -
-
+ {t('save')} + + + ) } diff --git a/services/web/frontend/js/features/project-list/components/sidebar/sidebar-filters.tsx b/services/web/frontend/js/features/project-list/components/sidebar/sidebar-filters.tsx index 8dc982f0ce..325456f6cb 100644 --- a/services/web/frontend/js/features/project-list/components/sidebar/sidebar-filters.tsx +++ b/services/web/frontend/js/features/project-list/components/sidebar/sidebar-filters.tsx @@ -1,5 +1,4 @@ import { useTranslation } from 'react-i18next' -import { Button } from 'react-bootstrap' import { Filter, useProjectListContext, @@ -19,9 +18,9 @@ export function SidebarFilter({ filter, text }: SidebarFilterProps) { {isActive => (
  • - +
  • )}
    diff --git a/services/web/frontend/js/features/project-list/components/sidebar/tags-list.tsx b/services/web/frontend/js/features/project-list/components/sidebar/tags-list.tsx index 084e102417..6e868a9204 100644 --- a/services/web/frontend/js/features/project-list/components/sidebar/tags-list.tsx +++ b/services/web/frontend/js/features/project-list/components/sidebar/tags-list.tsx @@ -1,7 +1,5 @@ import { sortBy } from 'lodash' -import { Button } from 'react-bootstrap' import { useTranslation } from 'react-i18next' -import Icon from '../../../../shared/components/icon' import MaterialIcon from '../../../../shared/components/material-icon' import { UNCATEGORIZED_KEY, @@ -9,6 +7,13 @@ import { } from '../../context/project-list-context' import useTag from '../../hooks/use-tag' import { getTagColor } from '../../util/tag' +import { + Dropdown, + DropdownItem, + DropdownMenu, + DropdownToggle, +} from '@/features/ui/components/bootstrap-5/dropdown-menu' +import BootstrapVersionSwitcher from '@/features/ui/components/bootstrap-5/bootstrap-version-switcher' export default function TagsList() { const { t } = useTranslation() @@ -35,14 +40,10 @@ export default function TagsList() {

    {t('organize_projects')}

  • - +
  • {sortBy(tags, tag => tag.name?.toLowerCase()).map(tag => { return ( @@ -50,19 +51,19 @@ export default function TagsList() { className={`tag ${selectedTagId === tag._id ? 'active' : ''}`} key={tag._id} > - - - -
      -
    • - + + + + + + handleEditTag(e, tag._id)} + > + {t('edit')} + + handleDeleteTag(e, tag._id)} + > + {t('delete')} + + + + } + bs3={ + + -
    • -
    • - -
    • -
    -
    + + +
      +
    • + +
    • +
    • + +
    • +
    +
    + } + /> ) })} @@ -109,16 +140,16 @@ export default function TagsList() { selectedTagId === UNCATEGORIZED_KEY ? 'active' : '' }`} > - + )} diff --git a/services/web/frontend/js/features/ui/components/types/dropdown-menu-props.ts b/services/web/frontend/js/features/ui/components/types/dropdown-menu-props.ts index 6c9a483c06..9b413cb71c 100644 --- a/services/web/frontend/js/features/ui/components/types/dropdown-menu-props.ts +++ b/services/web/frontend/js/features/ui/components/types/dropdown-menu-props.ts @@ -12,6 +12,7 @@ export type DropdownProps = { | { xxl: 'start' | 'end' } as?: ElementType children: ReactNode + className?: string onSelect?: (eventKey: any, event: object) => any onToggle?: (show: boolean) => void show?: boolean diff --git a/services/web/frontend/stylesheets/app/project-list-react.less b/services/web/frontend/stylesheets/app/project-list-react.less index db2ae13872..285711284e 100644 --- a/services/web/frontend/stylesheets/app/project-list-react.less +++ b/services/web/frontend/stylesheets/app/project-list-react.less @@ -941,26 +941,29 @@ .color-picker-more { color: @gray-dark; - margin: 6px 0 0 7px; + margin: 3px; + font-weight: bold; @media (max-width: @screen-xs-max) { - margin: 8px 0 0 9px; + margin: 5px; } } .color-picker-more-open { color: @gray-dark; - margin: 5px 0 0 5px; + margin: 3px; + font-weight: bold; @media (max-width: @screen-xs-max) { - margin: 7px 0 0 7px; + margin: 5px; } } } .color-picker-item-icon { - margin: 6px 0 0 6px; + margin: 4px; color: @white; + font-weight: bold; } @media (max-width: @screen-xs-max) { @@ -969,7 +972,7 @@ margin: 24px 24px; .color-picker-item-icon { - margin: 8px 0 0 8px; + margin: 6px; color: @white; } } diff --git a/services/web/frontend/stylesheets/bootstrap-5/components/dropdown-menu.scss b/services/web/frontend/stylesheets/bootstrap-5/components/dropdown-menu.scss index b07e61e410..9d0a8ac0f2 100644 --- a/services/web/frontend/stylesheets/bootstrap-5/components/dropdown-menu.scss +++ b/services/web/frontend/stylesheets/bootstrap-5/components/dropdown-menu.scss @@ -4,6 +4,7 @@ .dropdown-header { @include body-xs; + padding: var(--spacing-05) var(--spacing-06) var(--spacing-02) var(--spacing-04); } @@ -12,16 +13,20 @@ @include shadow-md; min-width: 240px; + + &.sm { + min-width: 160px; + } } .dropdown-item { @include body-sm; --bs-dropdown-item-border-radius: var(--border-radius-base); + display: grid; grid-auto-flow: column; - justify-content: start; - align-content: center; + place-content: center start; min-height: 44px; // a minimum height of 44px to be accessible for touch screens position: relative; diff --git a/services/web/frontend/stylesheets/bootstrap-5/pages/project-list.scss b/services/web/frontend/stylesheets/bootstrap-5/pages/project-list.scss index 23af3a2e9a..600c02fc12 100644 --- a/services/web/frontend/stylesheets/bootstrap-5/pages/project-list.scss +++ b/services/web/frontend/stylesheets/bootstrap-5/pages/project-list.scss @@ -1,11 +1,13 @@ .project-list-empty-col { display: flex; height: 100%; - flex-direction: column; + flex-flow: column nowrap; flex-wrap: nowrap; + .row:first-child { flex-grow: 1; /* fill vertical space so notifications are pushed to bottom */ } + .card-body { // h2 + .card-thin top padding padding-bottom: calc(var(--line-height-03) + var(--line-height-03) / 2); @@ -47,8 +49,7 @@ .project-list-sidebar-react { flex-grow: 1; - padding-left: var(--spacing-06); - padding-right: var(--spacing-06); + padding: var(--spacing-08) var(--spacing-06); -ms-overflow-style: -ms-autohiding-scrollbar; padding-top: var(--spacing-08); padding-bottom: var(--spacing-08); @@ -61,6 +62,7 @@ button { white-space: normal; word-wrap: anywhere; + // prevents buttons from expanding sidebar width } @@ -81,7 +83,8 @@ .welcome-new-wrapper { .welcome-title { - @include heading-xl(); + @include heading-xl; + margin-top: var(--spacing-08); } @@ -160,6 +163,196 @@ padding: var(--spacing-08) var(--spacing-06); } + ul.project-list-filters { + margin: var(--spacing-05) calc(-1 * var(--spacing-06)); + + .subdued { + color: var(--content-disabled); + } + + > li { + cursor: pointer; + position: relative; + + > button { + width: 100%; + font-weight: normal; + text-align: left; + color: var(--white); + background-color: transparent; + border-radius: unset; + border: none; + border-bottom: solid 1px transparent; + padding: var(--spacing-03) var(--spacing-06); + + &:hover { + background-color: var(--neutral-70); + text-decoration: none; + } + + &:focus { + text-decoration: none; + outline: none; + } + } + + &.separator { + padding: var(--spacing-03) var(--spacing-06); + cursor: auto; + } + } + + > li.active { + border-radius: 0; + + > button { + background-color: var(--neutral-90); + font-weight: 700; + color: var(--white); + + .subdued { + color: var(--white); + } + } + } + + h2 { + font-size: var(--font-size-02); + margin-bottom: var(--spacing-00); + color: var(--content-disabled); + text-transform: uppercase; + padding: var(--spacing-03) var(--spacing-00); + } + + > li.tag { + &.active { + .tag-menu > button { + color: var(--white); + border-color: var(--white); + + &:hover { + background-color: var(--neutral-90); + } + } + } + + &.untagged { + button.tag-name { + span.name { + font-style: italic; + padding-left: 0; + } + } + } + + &:hover { + &:not(.active) { + background-color: var(--neutral-70); + } + + .tag-menu { + display: block; + } + } + + &:not(.active) { + .tag-menu > a:hover { + background-color: var(--neutral-90); + } + } + + button.tag-name { + position: relative; + padding: var(--spacing-03) var(--spacing-09) var(--spacing-03) + var(--spacing-06); + display: flex; + align-items: center; + word-wrap: anywhere; + + .tag-list-icon { + vertical-align: sub; + font-weight: bold; + } + + span.name { + padding-left: 0.5em; + line-height: 1.4; + } + } + } + + .tag-menu { + button.dropdown-toggle { + border: 1px solid var(--white); + border-radius: var(--border-radius-base); + background-color: transparent; + color: var(--white); + display: block; + width: 16px; + height: 16px; + position: relative; + padding: var(--spacing-01) var(--spacing-03); + + &::after { + position: absolute; + top: 4px; + left: -2px; + } + } + + display: none; + width: auto; + position: absolute; + top: 50%; + margin-top: -8px; // Half the element height. + right: 4px; + + &.open { + display: block; + } + + button.tag-action { + border-radius: unset; + width: 100%; + background-color: transparent; + border-color: transparent; + color: var(--neutral-70); + text-align: left; + font-weight: normal; + + &:hover { + color: var(--white); + background-color: var(--bg-accent-01); + } + + &:active { + outline: none; + } + } + } + } +} + +.add-affiliation-mobile-wrapper { + padding: var(--spacing-07) 0; +} + +.add-affiliation { + .progress { + height: var(--spacing-05); + margin-bottom: var(--spacing-03); + } + + p { + margin-bottom: var(--spacing-03); + } + + &.is-mobile p { + @include body-xs; + + white-space: normal; + } + .project-dash-table { width: 100%; table-layout: fixed; @@ -231,7 +424,8 @@ .dash-cell-date-owner { font-size: $font-size-sm; - @include text-truncate(); + + @include text-truncate; } @include media-breakpoint-up(sm) { @@ -417,7 +611,7 @@ background-color: var(--bg-dark-tertiary); border-color: transparent; color: var(--neutral-20); - box-shadow: 2px 4px 6px rgba(0, 0, 0, 0.25); + box-shadow: 2px 4px 6px rgb(0 0 0 / 25%); border-radius: var(--border-radius-base); @include media-breakpoint-up(md) { @@ -426,8 +620,8 @@ button.close { @extend .text-white; + padding: 0; - -webkit-appearance: none; } } @@ -474,3 +668,84 @@ form.project-search { .project-search-clear-btn { @include reset-button; } + +.color-picker-item { + height: 28px; + width: 28px; + cursor: pointer; + position: relative; + outline: none; + border-radius: var(--border-radius-base); + margin: 0 var(--spacing-06) 0 0; + display: inline-block; + vertical-align: middle; + + &:focus-visible { + box-shadow: + 0 0 0 2px var(--white), + 0 0 0 3px var(--blue-50), + 0 0 0 5px var(--blue-30); + } + + &.more-button { + border: 1px solid var(--neutral-70); + + .color-picker-more { + color: var(--neutral-70); + margin: 3px; // it's centered, no matching spacing variable + font-weight: bold; + + @include media-breakpoint-down(sm) { + margin: 5px; // it's centered, no matching spacing variable + } + } + + .color-picker-more-open { + color: var(--neutral-70); + margin: 3px; + font-weight: bold; + + @include media-breakpoint-down(sm) { + margin: 5px; + } + } + } + + .color-picker-item-icon { + margin: 3px; // it's centered, no matching spacing variable + color: var(--white); + font-weight: bold; + } + + @include media-breakpoint-down(sm) { + height: 32px; + width: 32px; + margin: var(--spacing-08); + + .color-picker-item-icon { + margin: 5px; // it's centered, no matching spacing variable + color: var(--white); + } + } +} + +.color-picker-more-wrapper { + position: relative; + display: inline-block; + + .custom-picker { + position: absolute; + user-select: none; + z-index: 1; + + @include media-breakpoint-down(sm) { + top: 56px; + left: 24px; + } + } + + .popover-backdrop { + position: fixed; + inset: 0; + } +}