diff --git a/pontoon/base/static/css/dark-theme.css b/pontoon/base/static/css/dark-theme.css index e752e2e164..df047b9695 100644 --- a/pontoon/base/static/css/dark-theme.css +++ b/pontoon/base/static/css/dark-theme.css @@ -23,17 +23,17 @@ --popup-background-2: #272a2f; --icon-background-2: #4d5967; - /* Homepage */ - --homepage-background-image: url(../img/background.svg); - --homepage-tour-button-background: #ffffff; - --homepage-tour-button-color: #000000; - /* Tooltip */ --tooltip-background: #000000dd; --tooltip-color: #ffffff; --tooltip-color-2: #888888; --tooltip-border: #4d5967; + /* Homepage */ + --homepage-background-image: url(../img/background.svg); + --homepage-tour-button-background: #ffffff; + --homepage-tour-button-color: #000000; + /* Translation status */ --status-translated: #7bc876; --status-translated-alt: #7bc876; diff --git a/translate/public/locale/en-US/translate.ftl b/translate/public/locale/en-US/translate.ftl index e3f1642c7f..3776682cf6 100644 --- a/translate/public/locale/en-US/translate.ftl +++ b/translate/public/locale/en-US/translate.ftl @@ -297,6 +297,16 @@ entitydetails-PluralString--singular = SINGULAR entitieslist-Entity--sibling-strings-title = .title = Click to reveal sibling strings +entitieslist-EntitiesList--clear-selected = CLEAR + .title = Uncheck selected strings + +entitieslist-EntitiesList--edit-selected = + EDIT { $count } { $count -> + [one] STRING + *[other] STRINGS + } + .title = Edit Selected Strings + ## Translation Form diff --git a/translate/src/App.css b/translate/src/App.css index 8d54517d58..e865a3d6a0 100644 --- a/translate/src/App.css +++ b/translate/src/App.css @@ -120,4 +120,31 @@ #app .entity-navigation button.previous { margin-right: 15px; } + + /* History */ + #app .entity-details .history { + background: var(--editor-menu-background); + } + + /* Helpers */ + #app > .main-content .third-column { + border-left: none; + } + + #app > .main-content .third-column .react-tabs span.count { + background: var(--light-grey-6); + box-shadow: 0 0 5px; + color: var(--light-grey-6); /* used as box-shadow color */ + font-size: 0; + height: 4px; + width: 4px; + padding: 0; + position: absolute; + } + + #app > .main-content .third-column .react-tabs span.count:has(.preferred), + #app > .main-content .third-column .react-tabs span.count:has(.pinned) { + background: var(--status-translated-alt); + color: var(--status-translated-alt); /* used as box-shadow color */ + } } diff --git a/translate/src/hooks/useNarrowScreen.ts b/translate/src/hooks/useNarrowScreen.ts new file mode 100644 index 0000000000..ca1444fb84 --- /dev/null +++ b/translate/src/hooks/useNarrowScreen.ts @@ -0,0 +1,21 @@ +import { useEffect, useState } from 'react'; + +const NARROW_SCREEN_MAX_WIDTH = 600; + +/** + * Return true if the screen is narrower than 600px. Useful in Responsive Web Design. + */ +export function useNarrowScreen(): boolean { + const [isNarrow, setIsNarrow] = useState( + window.innerWidth <= NARROW_SCREEN_MAX_WIDTH, + ); + + useEffect(() => { + const handleWindowResize = () => + setIsNarrow(window.innerWidth <= NARROW_SCREEN_MAX_WIDTH); + window.addEventListener('resize', handleWindowResize); + return () => window.removeEventListener('resize', handleWindowResize); + }, []); + + return isNarrow; +} diff --git a/translate/src/modules/entitieslist/components/EntitiesList.css b/translate/src/modules/entitieslist/components/EntitiesList.css index c09e869ee6..a6432936d9 100644 --- a/translate/src/modules/entitieslist/components/EntitiesList.css +++ b/translate/src/modules/entitieslist/components/EntitiesList.css @@ -41,3 +41,45 @@ font-size: 128px; margin-bottom: 20px; } + +.entities .toolbar { + position: sticky; + bottom: 0; + background: var(--tooltip-background); + border-top: 1px solid var(--light-grey-1); + box-sizing: border-box; + line-height: 23px; + padding: 10px 12px; + width: 100%; + height: auto; +} + +.entities .toolbar button { + background: none; + border: none; + color: var(--tooltip-color-2); + font-weight: 300; +} + +.entities .toolbar .clear-selected { + float: left; +} + +.entities .toolbar .clear-selected .fa { + padding-right: 6px; +} + +.entities .toolbar .edit-selected { + float: right; + text-align: right; +} + +.entities .toolbar .edit-selected .fa { + padding-left: 6px; +} + +.entities .toolbar .clear-selected:hover, +.entities .toolbar .edit-selected:hover, +.entities .toolbar .edit-selected .selected-count { + color: var(--status-translated); +} diff --git a/translate/src/modules/entitieslist/components/EntitiesList.tsx b/translate/src/modules/entitieslist/components/EntitiesList.tsx index a0ce5110c6..2e3b5dc6e5 100644 --- a/translate/src/modules/entitieslist/components/EntitiesList.tsx +++ b/translate/src/modules/entitieslist/components/EntitiesList.tsx @@ -1,7 +1,9 @@ +import { Localized } from '@fluent/react'; import React, { useCallback, useContext, useEffect, useRef } from 'react'; import useInfiniteScroll from 'react-infinite-scroll-hook'; import type { Entity as EntityType } from '~/api/entity'; +import { EntitiesList as EntitiesListContext } from '~/context/EntitiesList'; import { Locale } from '~/context/Locale'; import { Location } from '~/context/Location'; import { @@ -13,6 +15,7 @@ import { useEntities } from '~/modules/entities/hooks'; import { SkeletonLoader } from '~/modules/loaders'; import { ENTITY_NOT_FOUND } from '~/modules/notification/messages'; import { useAppDispatch, useAppSelector, useAppStore } from '~/hooks'; +import { useNarrowScreen } from '~/hooks/useNarrowScreen'; import { usePrevious } from '~/hooks/usePrevious'; import { checkSelection, @@ -29,6 +32,51 @@ import { Entity } from './Entity'; import { USER } from '~/modules/user'; import { ShowNotification } from '~/context/Notification'; +const EntitiesToolbar = ({ + count, + onEdit, + onClear, +}: { + count: number; + onEdit: () => void; + onClear: () => void; +}) => ( +
+ , + }} + > + + + , + stress: , + }} + vars={{ count }} + > + + +
+); + /** * Displays a list of entities and their current translation. * @@ -224,6 +272,12 @@ export function EntitiesList(): React.ReactElement<'div'> { ); } + const selectedEntitiesCount = batchactions.entities.length; + const isNarrowScreen = useNarrowScreen(); + const entitiesList = useContext(EntitiesListContext); + const quitBatchActions = useCallback(() => dispatch(resetSelection()), []); + const showBatchActions = useCallback(() => entitiesList.show(false), []); + return (
{hasNextPage && } + {selectedEntitiesCount === 0 || !isNarrowScreen ? null : ( + + )}
); } diff --git a/translate/src/modules/entitydetails/components/ContextIssueButton.css b/translate/src/modules/entitydetails/components/ContextIssueButton.css index 36dea8e26e..dc1fcc0562 100644 --- a/translate/src/modules/entitydetails/components/ContextIssueButton.css +++ b/translate/src/modules/entitydetails/components/ContextIssueButton.css @@ -1,4 +1,4 @@ -.metadata .source-string-comment .context-issue-button { +.context-issue-button { background: var(--dark-grey-1); border: 1px solid var(--dark-grey-2); border-radius: 4px; @@ -11,6 +11,6 @@ padding: 2px 4px; } -.metadata .source-string-comment .context-issue-button:hover { +.context-issue-button:hover { border-color: var(--translation-border); } diff --git a/translate/src/modules/entitydetails/components/ContextIssueButton.tsx b/translate/src/modules/entitydetails/components/ContextIssueButton.tsx index b687b6f19c..948387a616 100644 --- a/translate/src/modules/entitydetails/components/ContextIssueButton.tsx +++ b/translate/src/modules/entitydetails/components/ContextIssueButton.tsx @@ -9,15 +9,10 @@ type Props = { export function ContextIssueButton(props: Props): React.ReactElement<'div'> { return ( -
- - - -
+ + + ); } diff --git a/translate/src/modules/entitydetails/components/EntityDetails.css b/translate/src/modules/entitydetails/components/EntityDetails.css index 5f46318512..fe333362c8 100644 --- a/translate/src/modules/entitydetails/components/EntityDetails.css +++ b/translate/src/modules/entitydetails/components/EntityDetails.css @@ -13,6 +13,11 @@ width: 66.67%; } +.entity-details .main-column .original-string-panel { + border-bottom: 1px solid var(--main-border-1); + padding: 10px; +} + .entity-details .third-column { width: 33.33%; border-left: 1px solid var(--main-border-1); diff --git a/translate/src/modules/entitydetails/components/EntityDetails.tsx b/translate/src/modules/entitydetails/components/EntityDetails.tsx index cc16114c0b..c0ff95da8c 100644 --- a/translate/src/modules/entitydetails/components/EntityDetails.tsx +++ b/translate/src/modules/entitydetails/components/EntityDetails.tsx @@ -7,9 +7,11 @@ import React, { } from 'react'; import { EntityView, useActiveTranslation } from '~/context/EntityView'; +import { Locale } from '~/context/Locale'; import { Location } from '~/context/Location'; import { UnsavedActions } from '~/context/UnsavedChanges'; import { Editor } from '~/modules/editor/components/Editor'; +import { OriginalString } from '~/modules/originalstring'; import { TERM } from '~/modules/terms'; import { get as getTerms } from '~/modules/terms/actions'; import { USER } from '~/modules/user'; @@ -25,10 +27,13 @@ import { } from '~/modules/teamcomments/actions'; import { getPlainMessage } from '~/utils/message'; -import './EntityDetails.css'; +import { ContextIssueButton } from './ContextIssueButton'; import { EntityNavigation } from './EntityNavigation'; import { Helpers } from './Helpers'; import { Metadata } from './Metadata'; +import { Screenshots } from './Screenshots'; + +import './EntityDetails.css'; /** * Component showing details about an entity. @@ -88,21 +93,39 @@ export function EntityDetails(): React.ReactElement<'section'> | null { [dispatch], ); + const { code } = useContext(Locale); + + const openTeamComments = useCallback(() => { + const teamCommentsTab = commentTabRef.current; + + // FIXME: This is an ugly hack. + // https://github.com/mozilla/pontoon/issues/2300 + const index = teamCommentsTab?._reactInternalFiber.index ?? 0; + + setCommentTabIndex(index); + setContactPerson(selectedEntity.project.contact.name); + }, [selectedEntity, setCommentTabIndex, setContactPerson]); + + const showContextIssueButton = + user.isAuthenticated && selectedEntity.project.contact; + // No content while loading entity data return selectedEntity.pk === 0 ? null : (
- +
+ {showContextIssueButton && ( + + )} + + + +
diff --git a/translate/src/modules/entitydetails/components/Helpers.tsx b/translate/src/modules/entitydetails/components/Helpers.tsx index cba0547231..30b16824bb 100644 --- a/translate/src/modules/entitydetails/components/Helpers.tsx +++ b/translate/src/modules/entitydetails/components/Helpers.tsx @@ -5,6 +5,7 @@ import { Tab, TabList, TabPanel, Tabs } from 'react-tabs'; import type { Entity } from '~/api/entity'; import { HelperSelection } from '~/context/HelperSelection'; import type { Location } from '~/context/Location'; +import { useNarrowScreen } from '~/hooks/useNarrowScreen'; import type { TermState } from '~/modules/terms'; import type { UserState } from '~/modules/user'; import { Machinery, MachineryCount } from '~/modules/machinery'; @@ -56,6 +57,138 @@ export function Helpers({ const isTerminologyProject = parameters.project === 'terminology'; + function MachineryTab() { + return ( + <> + + {'MACHINERY'} + + + + ); + } + + function OtherLocalesTab() { + return ( + <> + {'LOCALES'} + + + ); + } + + function TermsTab() { + return ( + <> + {'TERMS'} + + + ); + } + + function CommentsTab() { + return ( + <> + {'COMMENTS'} + + + ); + } + + function MachineryPanel() { + return ( + <> + + + ); + } + + function OtherLocalesPanel() { + return ( + <> + + + ); + } + + function TermsPanel() { + return ( + <> + + + ); + } + + function CommentsPanel() { + return ( + <> + + + ); + } + + if (useNarrowScreen()) { + return ( + <> +
+ { + if (index === lastIndex) { + return false; + } else { + setTab(index); + } + setCommentTabIndex(index); + }} + > + + + + + + + + {isTerminologyProject ? null : ( + + + + )} + + + + + + + + + + + {isTerminologyProject ? null : ( + + + + )} + + + + +
+ + ); + } + return ( <>
@@ -66,33 +199,20 @@ export function Helpers({ {isTerminologyProject ? null : ( - - {'TERMS'} - - + )} - - {'COMMENTS'} - - + {isTerminologyProject ? null : ( - + )} - +
@@ -108,27 +228,17 @@ export function Helpers({ > - - {'MACHINERY'} - - + - - {'LOCALES'} - - + - + - + diff --git a/translate/src/modules/entitydetails/components/Metadata.css b/translate/src/modules/entitydetails/components/Metadata.css index 63cc58f13e..06481240a9 100644 --- a/translate/src/modules/entitydetails/components/Metadata.css +++ b/translate/src/modules/entitydetails/components/Metadata.css @@ -1,11 +1,9 @@ .metadata { - border-bottom: 1px solid var(--main-border-1); color: var(--translation-secondary-color); font-size: 12px; font-style: italic; min-height: 114px; line-height: 22px; - padding: 10px; } .metadata h2 { @@ -36,30 +34,6 @@ margin: 0 3px; } -.metadata .original { - color: var(--translation-color); - font-size: 14px; - font-style: normal; - line-height: 22px; - margin-top: -2px; /* Align with the source string comment button */ - padding-bottom: 6px; - text-align: start; - white-space: pre-wrap; -} - -.metadata .original .placeable { - cursor: pointer; -} - -.metadata .original .term { - background: inherit; - border-bottom: 1px solid var(--status-translated); - color: inherit; - cursor: pointer; - font-weight: normal; - font-style: inherit; -} - .metadata ul { list-style: none; margin: 0; diff --git a/translate/src/modules/entitydetails/components/Metadata.tsx b/translate/src/modules/entitydetails/components/Metadata.tsx index 02e6b6eecb..c6ba47208e 100644 --- a/translate/src/modules/entitydetails/components/Metadata.tsx +++ b/translate/src/modules/entitydetails/components/Metadata.tsx @@ -1,32 +1,22 @@ import { Localized } from '@fluent/react'; import parse from 'html-react-parser'; -import React, { useCallback, useContext, useLayoutEffect } from 'react'; +import React, { useContext, useLayoutEffect } from 'react'; // @ts-expect-error Working types are unavailable for react-linkify 0.2.2 import Linkify from 'react-linkify'; import type { Entity } from '~/api/entity'; -import { Locale } from '~/context/Locale'; -import type { TermState } from '~/modules/terms'; -import type { UserState } from '~/modules/user'; -import { OriginalString } from '~/modules/originalstring'; import type { TeamCommentState } from '~/modules/teamcomments'; -import { ContextIssueButton } from './ContextIssueButton'; +import { Locale } from '~/context/Locale'; import { FluentAttribute } from './FluentAttribute'; import { Property } from './Property'; -import { Screenshots } from './Screenshots'; import './Metadata.css'; type Props = { entity: Entity; - terms: TermState; teamComments: TeamCommentState; - user: UserState; - commentTabRef: React.RefObject<{ _reactInternalFiber: { index: number } }>; navigateToPath: (path: string) => void; - setCommentTabIndex: (id: number) => void; - setContactPerson: (contact: string) => void; }; const Datum = ({ @@ -230,34 +220,14 @@ const EntityContext = ({ * - a link to the project */ export function Metadata({ - commentTabRef, entity, navigateToPath, - setCommentTabIndex, - setContactPerson, - terms, teamComments, - user, }: Props): React.ReactElement { const { code } = useContext(Locale); - const openTeamComments = useCallback(() => { - const teamCommentsTab = commentTabRef.current; - const index = teamCommentsTab?._reactInternalFiber.index ?? 0; - setCommentTabIndex(index); - setContactPerson(entity.project.contact.name); - }, [entity, setCommentTabIndex, setContactPerson]); - - const contactPerson = entity.project.contact; - const showContextIssueButton = user.isAuthenticated && contactPerson; - return (
- {showContextIssueButton && ( - - )} - - diff --git a/translate/src/modules/originalstring/components/OriginalString.css b/translate/src/modules/originalstring/components/OriginalString.css new file mode 100644 index 0000000000..950b5c3d11 --- /dev/null +++ b/translate/src/modules/originalstring/components/OriginalString.css @@ -0,0 +1,22 @@ +.original-string-panel .original { + color: var(--translation-color); + font-size: 14px; + font-style: normal; + line-height: 22px; + margin: -2px 0 6px; /* Align with the source string comment button */ + text-align: start; + white-space: pre-wrap; +} + +.original-string-panel .original .placeable { + cursor: pointer; +} + +.original-string-panel .original .term { + background: inherit; + border-bottom: 1px solid var(--status-translated); + color: inherit; + cursor: pointer; + font-weight: normal; + font-style: inherit; +} diff --git a/translate/src/modules/originalstring/components/OriginalString.tsx b/translate/src/modules/originalstring/components/OriginalString.tsx index f2fc73c5a2..89157df8ed 100644 --- a/translate/src/modules/originalstring/components/OriginalString.tsx +++ b/translate/src/modules/originalstring/components/OriginalString.tsx @@ -19,6 +19,8 @@ import { PluralString } from './PluralString'; import { RichString } from './RichString'; import { TermsPopup } from './TermsPopup'; +import './OriginalString.css'; + type Props = { navigateToPath: (path: string) => void; terms: TermState; diff --git a/translate/src/modules/search/components/FiltersPanel.css b/translate/src/modules/search/components/FiltersPanel.css index 69c0dea574..cbbf52d4ae 100644 --- a/translate/src/modules/search/components/FiltersPanel.css +++ b/translate/src/modules/search/components/FiltersPanel.css @@ -229,7 +229,7 @@ .filters-panel .toolbar { position: sticky; bottom: 0; - background: #111111; + background: var(--tooltip-background); border-top: 1px solid var(--light-grey-1); box-sizing: border-box; line-height: 23px; @@ -240,7 +240,7 @@ .filters-panel .toolbar button { background: none; border: none; - color: var(--transla-grey-7); + color: var(--tooltip-color-2); font-weight: 300; } diff --git a/translate/src/modules/terms/components/TermCount.tsx b/translate/src/modules/terms/components/TermCount.tsx index a7ffefb656..291264f02b 100644 --- a/translate/src/modules/terms/components/TermCount.tsx +++ b/translate/src/modules/terms/components/TermCount.tsx @@ -15,5 +15,9 @@ export function TermCount(props: Props): null | React.ReactElement<'span'> { const termCount = terms.terms.length; + if (!termCount) { + return null; + } + return {termCount}; }