diff --git a/web/apps/app-listing.react.js b/web/apps/app-listing.react.js index 7fb5a5df7..44eb6b49b 100644 --- a/web/apps/app-listing.react.js +++ b/web/apps/app-listing.react.js @@ -1,83 +1,81 @@ // @flow import { faCheckCircle } from '@fortawesome/free-regular-svg-icons'; import { faPlusCircle } from '@fortawesome/free-solid-svg-icons'; import { FontAwesomeIcon } from '@fortawesome/react-fontawesome'; import classnames from 'classnames'; import * as React from 'react'; import { useDispatch } from 'react-redux'; import { disableAppActionType, enableAppActionType, } from 'lib/reducers/enabled-apps-reducer'; import type { SupportedApps } from 'lib/types/enabled-apps'; +import Button from '../components/button.react'; import SWMansionIcon from '../SWMansionIcon.react'; import css from './apps.css'; type Props = { +id: SupportedApps | 'chat', +readOnly: boolean, +enabled: boolean, +name: string, +icon: 'message-square' | 'calendar', +copy: string, }; function AppListing(props: Props): React.Node { const { id, readOnly, enabled, name, icon, copy } = props; const dispatch = useDispatch(); const switchAppState = React.useCallback( () => dispatch({ type: enabled ? disableAppActionType : enableAppActionType, payload: id, }), [dispatch, enabled, id], ); const actionButton = React.useMemo(() => { const switchIcon = enabled ? faCheckCircle : faPlusCircle; if (readOnly) { const readOnlyIconClasses = classnames( css.appListingIcon, css.appListingIconState, css.iconReadOnly, ); return (
); } - const iconClasses = classnames( - css.appListingIcon, - css.appListingIconState, - { - [css.iconEnabled]: enabled, - [css.iconDisabled]: !enabled, - }, - ); + const iconClasses = classnames({ + [css.appListingIconState]: true, + [css.iconEnabled]: enabled, + [css.iconDisabled]: !enabled, + }); return ( -
+
+ ); }, [enabled, readOnly, switchAppState]); return (
{name}
{copy}
{actionButton}
); } export default AppListing; diff --git a/web/apps/apps.css b/web/apps/apps.css index a7b27d37b..c037c411f 100644 --- a/web/apps/apps.css +++ b/web/apps/apps.css @@ -1,63 +1,64 @@ div.appsDirectoryContainer { display: flex; flex-direction: column; align-items: flex-start; } h4.appsHeader { color: var(--fg); padding: 20px 0 40px 40px; font-weight: var(--semi-bold); } div.appsDirectoryList { margin-left: 20px; display: flex; flex-direction: column; row-gap: 10px; } div.appListingContainer { color: var(--fg); display: flex; flex-direction: row; align-items: center; } div.appListingTextContainer { display: flex; flex-direction: column; flex: 1; } h5.appName { font-weight: var(--semi-bold); margin-bottom: 4px; } small.appCopy { font-size: var(--xs-font-12); } div.appListingIcon { padding: 0 20px; align-self: stretch; display: flex; align-items: center; } -div.appListingIconState { +.appListingIconState { + padding: 0 20px; font-size: var(--xl-font-20); } div.iconReadOnly { color: var(--app-list-icon-read-only-color); } -div.iconEnabled { +.iconEnabled { color: var(--app-list-icon-enabled-color); } -div.iconDisabled { +.iconDisabled { color: var(--app-list-icon-disabled-color); } diff --git a/web/chat/chat-thread-composer.css b/web/chat/chat-thread-composer.css index ce5024400..0c38c9021 100644 --- a/web/chat/chat-thread-composer.css +++ b/web/chat/chat-thread-composer.css @@ -1,74 +1,71 @@ div.threadSearchContainer { background-color: var(--thread-creation-search-container-bg); color: var(--fg); display: flex; flex-direction: column; max-height: 50%; overflow: auto; flex-shrink: 0; } div.fullHeight { flex-grow: 1; max-height: 100%; } div.userSelectedTags { display: flex; flex-wrap: wrap; flex-direction: row; align-items: center; gap: 4px; padding: 4px 12px; margin-bottom: 8px; } div.searchRow { display: flex; flex-direction: row; align-items: center; margin-right: 8px; } div.searchField { flex-grow: 1; } -div.closeSearch { - cursor: pointer; - display: flex; - align-items: center; +.closeSearch { color: var(--thread-creation-close-search-color); margin: 0 8px; } ul.searchResultsContainer { display: flex; flex-direction: column; overflow: auto; padding: 0 12px 8px; list-style-type: none; } .searchResultsItem { display: flex; } .searchResultsItem:hover { background-color: var(--thread-creation-search-item-bg-hover); } .searchResultsButton { justify-content: space-between; flex: 1; padding: 8px 12px; } div.userName { color: var(--fg); } div.userInfo { font-style: italic; color: var(--thread-creation-search-item-info-color); } diff --git a/web/chat/chat-thread-composer.react.js b/web/chat/chat-thread-composer.react.js index f121853f6..974919d08 100644 --- a/web/chat/chat-thread-composer.react.js +++ b/web/chat/chat-thread-composer.react.js @@ -1,187 +1,187 @@ // @flow import classNames from 'classnames'; import * as React from 'react'; import { useDispatch } from 'react-redux'; import { userSearchIndexForPotentialMembers } from 'lib/selectors/user-selectors'; import { getPotentialMemberItems } from 'lib/shared/search-utils'; import { threadIsPending } from 'lib/shared/thread-utils'; import type { AccountUserInfo, UserListItem } from 'lib/types/user-types'; import Button from '../components/button.react'; import Label from '../components/label.react'; import Search from '../components/search.react'; import type { InputState } from '../input/input-state'; import { updateNavInfoActionType } from '../redux/action-types'; import { useSelector } from '../redux/redux-utils'; import SWMansionIcon from '../SWMansionIcon.react'; import css from './chat-thread-composer.css'; type Props = { +userInfoInputArray: $ReadOnlyArray, +otherUserInfos: { [id: string]: AccountUserInfo }, +threadID: string, +inputState: InputState, }; type ActiveThreadBehavior = | 'reset-active-thread-if-pending' | 'keep-active-thread'; function ChatThreadComposer(props: Props): React.Node { const { userInfoInputArray, otherUserInfos, threadID, inputState } = props; const [usernameInputText, setUsernameInputText] = React.useState(''); const dispatch = useDispatch(); const userSearchIndex = useSelector(userSearchIndexForPotentialMembers); const userInfoInputIDs = React.useMemo( () => userInfoInputArray.map(userInfo => userInfo.id), [userInfoInputArray], ); const userListItems = React.useMemo( () => getPotentialMemberItems( usernameInputText, otherUserInfos, userSearchIndex, userInfoInputIDs, ), [usernameInputText, otherUserInfos, userSearchIndex, userInfoInputIDs], ); const onSelectUserFromSearch = React.useCallback( (id: string) => { const selectedUserIDs = userInfoInputArray.map(user => user.id); dispatch({ type: updateNavInfoActionType, payload: { selectedUserList: [...selectedUserIDs, id], }, }); setUsernameInputText(''); }, [dispatch, userInfoInputArray], ); const onRemoveUserFromSelected = React.useCallback( (id: string) => { const selectedUserIDs = userInfoInputArray.map(user => user.id); if (!selectedUserIDs.includes(id)) { return; } dispatch({ type: updateNavInfoActionType, payload: { selectedUserList: selectedUserIDs.filter(userID => userID !== id), }, }); }, [dispatch, userInfoInputArray], ); const userSearchResultList = React.useMemo(() => { if ( !userListItems.length || (!usernameInputText && userInfoInputArray.length) ) { return null; } return ( ); }, [ onSelectUserFromSearch, userInfoInputArray.length, userListItems, usernameInputText, ]); const hideSearch = React.useCallback( (threadBehavior: ActiveThreadBehavior = 'keep-active-thread') => { dispatch({ type: updateNavInfoActionType, payload: { chatMode: 'view', activeChatThreadID: threadBehavior === 'keep-active-thread' || !threadIsPending(threadID) ? threadID : null, }, }); }, [dispatch, threadID], ); const onCloseSearch = React.useCallback(() => { hideSearch('reset-active-thread-if-pending'); }, [hideSearch]); const tagsList = React.useMemo(() => { if (!userInfoInputArray?.length) { return null; } const labels = userInfoInputArray.map(user => { return ( ); }); return
{labels}
; }, [userInfoInputArray, onRemoveUserFromSelected]); React.useEffect(() => { if (!inputState) { return; } inputState.registerSendCallback(hideSearch); return () => inputState.unregisterSendCallback(hideSearch); }, [hideSearch, inputState]); const threadSearchContainerStyles = React.useMemo( () => classNames(css.threadSearchContainer, { [css.fullHeight]: !userInfoInputArray.length, }), [userInfoInputArray.length], ); return (
-
+
+
{tagsList} {userSearchResultList}
); } export default ChatThreadComposer; diff --git a/web/chat/chat-thread-list-item-menu.react.js b/web/chat/chat-thread-list-item-menu.react.js index fad35ca95..4d015b193 100644 --- a/web/chat/chat-thread-list-item-menu.react.js +++ b/web/chat/chat-thread-list-item-menu.react.js @@ -1,75 +1,76 @@ // @flow import classNames from 'classnames'; import * as React from 'react'; import useToggleUnreadStatus from 'lib/hooks/toggle-unread-status'; import type { ThreadInfo } from 'lib/types/thread-types'; +import Button from '../components/button.react'; import { useThreadIsActive } from '../selectors/nav-selectors'; import SWMansionIcon from '../SWMansionIcon.react'; import css from './chat-thread-list-item-menu.css'; type Props = { +threadInfo: ThreadInfo, +mostRecentNonLocalMessage: ?string, +renderStyle?: 'chat' | 'thread', }; function ChatThreadListItemMenu(props: Props): React.Node { const { renderStyle = 'chat', threadInfo, mostRecentNonLocalMessage } = props; const active = useThreadIsActive(threadInfo.id); const [menuVisible, setMenuVisible] = React.useState(false); const toggleMenu = React.useCallback( event => { event.stopPropagation(); setMenuVisible(!menuVisible); }, [menuVisible], ); const hideMenu = React.useCallback(() => { setMenuVisible(false); }, []); const toggleUnreadStatus = useToggleUnreadStatus( threadInfo, mostRecentNonLocalMessage, hideMenu, ); const onToggleUnreadStatusClicked = React.useCallback( event => { event.stopPropagation(); toggleUnreadStatus(); }, [toggleUnreadStatus], ); const toggleUnreadStatusButtonText = `Mark as ${ threadInfo.currentUser.unread ? 'read' : 'unread' }`; const menuIconSize = renderStyle === 'chat' ? 24 : 20; const menuCls = classNames(css.menu, { [css.menuSidebar]: renderStyle === 'thread', }); const btnCls = classNames(css.menuContent, { [css.menuContentVisible]: menuVisible, [css.active]: active, }); return (
- +
- +
); } export default ChatThreadListItemMenu; diff --git a/web/components/clear-search-button.react.js b/web/components/clear-search-button.react.js index dc32e3214..18f1f564e 100644 --- a/web/components/clear-search-button.react.js +++ b/web/components/clear-search-button.react.js @@ -1,26 +1,27 @@ // @flow import classNames from 'classnames'; import * as React from 'react'; import SWMansionIcon from '../SWMansionIcon.react'; +import Button from './button.react'; import css from './search.css'; type ClearSearchButtonProps = { +active: boolean, +onClick: () => void, }; function ClearSearchButton(props: ClearSearchButtonProps): React.Node { const { active, onClick } = props; const searchClassNames = classNames(css.clearSearch, { [css.clearSearchDisabled]: !active, }); return ( - + ); } export default ClearSearchButton; diff --git a/web/components/label.css b/web/components/label.css index 3ac7fc1ae..bced94463 100644 --- a/web/components/label.css +++ b/web/components/label.css @@ -1,16 +1,12 @@ div.label { line-height: 1.5; padding: 4px 8px; border-radius: 8px; display: flex; word-break: break-all; } button.close { - display: flex; - align-items: center; margin-left: 4px; - background: transparent; - border: none; color: inherit; } diff --git a/web/components/label.react.js b/web/components/label.react.js index cbe9bd559..7a8549ca0 100644 --- a/web/components/label.react.js +++ b/web/components/label.react.js @@ -1,53 +1,54 @@ // @flow import * as React from 'react'; import SWMansionIcon from '../SWMansionIcon.react'; +import Button from './button.react'; import css from './label.css'; type Props = { +size?: string | number, +color?: string, +bg?: string, +children: React.Node, +onClose?: () => mixed, }; function Label(props: Props): React.Node { const { size = '12px', color = 'var(--label-default-color)', bg = 'var(--label-default-bg)', children, onClose, } = props; const labelStyle = React.useMemo( () => ({ fontSize: size, color: color, background: bg, }), [bg, color, size], ); const closeButton = React.useMemo(() => { if (!onClose) { return null; } return ( - + ); }, [onClose, size]); return (
{children} {closeButton}
); } export default Label; diff --git a/web/components/menu-item.react.js b/web/components/menu-item.react.js index fff1c7fe6..d93abbfb7 100644 --- a/web/components/menu-item.react.js +++ b/web/components/menu-item.react.js @@ -1,36 +1,38 @@ // @flow import classNames from 'classnames'; import * as React from 'react'; import SWMansionIcon, { type Icon } from '../SWMansionIcon.react'; +import Button from './button.react'; import css from './menu.css'; type MenuItemProps = { +onClick?: () => mixed, +icon: Icon, +text: string, +dangerous?: boolean, }; function MenuItem(props: MenuItemProps): React.Node { const { onClick, icon, text, dangerous } = props; const itemClasses = classNames(css.menuAction, { [css.menuActionDangerous]: dangerous, }); + return ( - + ); } const MemoizedMenuItem: React.ComponentType = React.memo( MenuItem, ); export default MemoizedMenuItem; diff --git a/web/components/menu.css b/web/components/menu.css index 24429c33f..897874772 100644 --- a/web/components/menu.css +++ b/web/components/menu.css @@ -1,78 +1,74 @@ button.menuButton { background-color: transparent; border: none; cursor: pointer; color: inherit; } div.menuActionList { position: absolute; z-index: 4; display: flex; flex-direction: column; background-color: var(--menu-bg); color: var(--menu-color); border-radius: 4px; padding: 4px 0; line-height: var(--line-height-text); min-width: max-content; } div.menuActionListThreadActions { font-size: var(--m-font-16); top: 40px; right: -20px; } div.menuActionListMemberActions { font-size: var(--xs-font-12); background-color: var(--menu-bg-light); color: var(--menu-color-light); top: 0; right: 5px; } button.menuAction { color: inherit; z-index: 1; - background-color: transparent; padding: 12px 16px; line-height: 1.5; - border: none; - cursor: pointer; - display: flex; - align-items: center; font-size: inherit; + justify-content: start; } button.menuAction:hover { color: var(--menu-color-hover); } div.menuActionIcon { display: flex; justify-content: center; margin-right: 8px; height: 24px; width: 24px; } div.menuActionListMemberActions div.menuActionIcon { height: 18px; width: 18px; } button.menuActionDangerous { color: var(--menu-color-dangerous); } button.menuActionDangerous:hover { color: var(--menu-color-dangerous-hover); } hr.separator { height: 1px; background: var(--menu-separator-color); margin: 10px 16px; max-width: 130px; border: none; } diff --git a/web/components/search.css b/web/components/search.css index 9dd8ad7cd..c9818df56 100644 --- a/web/components/search.css +++ b/web/components/search.css @@ -1,42 +1,40 @@ div.searchContainer { background-color: var(--text-input-bg); display: flex; align-items: center; margin: 1rem; border-radius: 16px; padding: 8px; } div.searchIcon { display: flex; color: var(--search-icon-color); } input.searchInput { background-color: transparent; font-size: var(--m-font-16); line-height: 1.5; padding: 4px 12px; flex: 1; border: none; color: var(--search-input-color); outline: none; } input.searchInput::placeholder { color: var(--search-input-placeholder); } button.clearSearch { color: var(--search-clear-color); transition: ease-in-out 0.15s; - border: none; background: var(--search-clear-bg); border-radius: 50%; - display: flex; padding: 6px; } button.clearSearchDisabled { opacity: 0; } diff --git a/web/components/tabs-header.js b/web/components/tabs-header.js index e39fb8a59..c9fa39937 100644 --- a/web/components/tabs-header.js +++ b/web/components/tabs-header.js @@ -1,28 +1,29 @@ // @flow import classnames from 'classnames'; import * as React from 'react'; +import Button from './button.react'; import css from './tabs.css'; type Props = { - +children?: React.Node, + +children: React.Node, +isActive: boolean, +setTab: T => mixed, +id: T, }; function TabsHeader(props: Props): React.Node { const { children, isActive, setTab, id } = props; const headerClasses = classnames(css.tabHeader, { [css.backgroundTabHeader]: !isActive, }); const onClickSetTab = React.useCallback(() => setTab(id), [setTab, id]); return ( -
+
+ ); } export default TabsHeader; diff --git a/web/components/tabs.css b/web/components/tabs.css index e0737224f..b705bbfd8 100644 --- a/web/components/tabs.css +++ b/web/components/tabs.css @@ -1,31 +1,30 @@ div.tabsContainer { color: var(--fg); display: flex; flex-direction: column; overflow: hidden; max-height: 100%; flex: 1; } + div.tabsHeaderContainer { display: flex; } -div.tabHeader { +.tabHeader { flex: 1; padding: 16px; - display: flex; - justify-content: center; + font-size: var(--m-font-16); color: var(--tabs-header-active-color); border-bottom: 2px solid var(--tabs-header-active-border); } -div.backgroundTabHeader { - cursor: pointer; +.backgroundTabHeader { color: var(--tabs-header-background-color); border-bottom-color: var(--tabs-header-background-border); } -div.backgroundTabHeader:hover { +.backgroundTabHeader:hover { color: var(--tabs-header-background-color-hover); border-bottom-color: var(--tabs-header-background-border-hover); } diff --git a/web/media/media.css b/web/media/media.css index 726f8a7d1..4ca4ce173 100644 --- a/web/media/media.css +++ b/web/media/media.css @@ -1,106 +1,102 @@ span.clickable { cursor: pointer; } span.multimedia { display: inline-flex; align-items: center; justify-content: center; position: relative; vertical-align: top; } -span.multimedia > span.multimediaImage { - display: inline-flex; - align-items: center; - justify-content: center; +span.multimedia > .multimediaImage { position: relative; min-height: 50px; min-width: 50px; } -span.multimedia > span.multimediaImage > img { +span.multimedia > .multimediaImage > img { max-height: 200px; max-width: 100%; } -span.multimedia > span.multimediaImage > svg.removeUpload { +span.multimedia > .multimediaImage svg.removeUpload { display: none; position: absolute; - cursor: pointer; top: 3px; right: 3px; color: white; border-radius: 50%; box-shadow: inset 0 0 5px rgba(0, 0, 0, 0.5); background-color: rgba(34, 34, 34, 0.67); } -span.multimedia:hover > span.multimediaImage > svg.removeUpload { +span.multimedia:hover > .multimediaImage svg.removeUpload { display: inherit; } span.multimedia > svg.uploadError { position: absolute; top: 0; bottom: 0; left: 0; right: 0; margin: auto auto; color: white; border-radius: 50%; box-shadow: inset 0 0 5px rgba(0, 0, 0, 0.5); background-color: #dd2222; } span.multimedia > svg.progressIndicator { position: absolute; top: 0; bottom: 0; left: 0; right: 0; margin: auto auto; width: 50px; height: 50px; } :global(.CircularProgressbar-background) { fill: #666 !important; } :global(.CircularProgressbar-text) { fill: #fff !important; } :global(.CircularProgressbar-path) { stroke: #fff !important; } :global(.CircularProgressbar-trail) { stroke: transparent !important; } div.multimediaModalOverlay { position: fixed; left: 0; top: 0; z-index: 4; width: 100%; height: 100%; background-color: rgba(0, 0, 0, 0.9); overflow: auto; padding: 10px; box-sizing: border-box; display: flex; justify-content: center; } div.multimediaModalOverlay > img { object-fit: scale-down; width: auto; height: auto; max-width: 100%; max-height: 100%; } svg.closeMultimediaModal { position: absolute; cursor: pointer; top: 15px; right: 15px; color: white; border-radius: 50%; box-shadow: inset 0 0 5px rgba(0, 0, 0, 0.5); background-color: rgba(34, 34, 34, 0.67); height: 36px; width: 36px; } diff --git a/web/media/multimedia.react.js b/web/media/multimedia.react.js index e54abf373..4f0fbe4f8 100644 --- a/web/media/multimedia.react.js +++ b/web/media/multimedia.react.js @@ -1,126 +1,127 @@ // @flow import classNames from 'classnames'; import invariant from 'invariant'; import * as React from 'react'; import { CircularProgressbar } from 'react-circular-progressbar'; import 'react-circular-progressbar/dist/styles.css'; import { XCircle as XCircleIcon, AlertCircle as AlertCircleIcon, } from 'react-feather'; import { useModalContext } from 'lib/components/modal-provider.react'; +import Button from '../components/button.react'; import { type PendingMultimediaUpload } from '../input/input-state'; import css from './media.css'; import MultimediaModal from './multimedia-modal.react'; type BaseProps = { +uri: string, +pendingUpload?: ?PendingMultimediaUpload, +remove?: (uploadID: string) => void, +multimediaCSSClass: string, +multimediaImageCSSClass: string, }; type Props = { ...BaseProps, +pushModal: (modal: React.Node) => void, }; class Multimedia extends React.PureComponent { componentDidUpdate(prevProps: Props) { const { uri, pendingUpload } = this.props; if (uri === prevProps.uri) { return; } if ( (!pendingUpload || pendingUpload.uriIsReal) && (!prevProps.pendingUpload || !prevProps.pendingUpload.uriIsReal) ) { URL.revokeObjectURL(prevProps.uri); } } render(): React.Node { let progressIndicator, errorIndicator, removeButton; const { pendingUpload, remove } = this.props; if (pendingUpload) { const { progressPercent, failed } = pendingUpload; if (progressPercent !== 0 && progressPercent !== 1) { const outOfHundred = Math.floor(progressPercent * 100); const text = `${outOfHundred}%`; progressIndicator = ( ); } if (failed) { errorIndicator = ( ); } if (remove) { removeButton = ( - + ); } } const imageContainerClasses = [ css.multimediaImage, this.props.multimediaImageCSSClass, ]; imageContainerClasses.push(css.clickable); const containerClasses = [css.multimedia, this.props.multimediaCSSClass]; return ( - {removeButton} - + {progressIndicator} {errorIndicator} ); } remove: (event: SyntheticEvent) => void = event => { event.stopPropagation(); const { remove, pendingUpload } = this.props; invariant( remove && pendingUpload, 'Multimedia cannot be removed as either remove or pendingUpload ' + 'are unspecified', ); remove(pendingUpload.localID); }; - onClick: (event: SyntheticEvent) => void = event => { - event.stopPropagation(); - + onClick: () => void = () => { const { pushModal, uri } = this.props; pushModal(); }; } function ConnectedMultimediaContainer(props: BaseProps): React.Node { const modalContext = useModalContext(); return ; } export default ConnectedMultimediaContainer; diff --git a/web/modals/components/add-members-item.react.js b/web/modals/components/add-members-item.react.js index 365d10dde..3bd6bff55 100644 --- a/web/modals/components/add-members-item.react.js +++ b/web/modals/components/add-members-item.react.js @@ -1,50 +1,51 @@ // @flow import * as React from 'react'; import type { UserListItem } from 'lib/types/user-types'; +import Button from '../../components/button.react'; import css from './add-members.css'; type AddMembersItemProps = { +userInfo: UserListItem, +onClick: (userID: string) => void, +userAdded: boolean, }; function AddMemberItem(props: AddMembersItemProps): React.Node { const { userInfo, onClick, userAdded = false } = props; const canBeAdded = !userInfo.alertText; const onClickCallback = React.useCallback(() => { if (!canBeAdded) { return; } onClick(userInfo.id); }, [canBeAdded, onClick, userInfo.id]); const action = React.useMemo(() => { if (!canBeAdded) { return userInfo.alertTitle; } if (userAdded) { return Remove; } else { return 'Add'; } }, [canBeAdded, userAdded, userInfo.alertTitle]); return ( - + ); } export default AddMemberItem; diff --git a/web/modals/components/add-members.css b/web/modals/components/add-members.css index c5c2d6634..96498af39 100644 --- a/web/modals/components/add-members.css +++ b/web/modals/components/add-members.css @@ -1,41 +1,36 @@ div.addMemberItemsGroupHeader { font-size: var(--s-font-14); color: var(--add-members-group-header-color); margin: 16px; } button.addMemberItem { - display: flex; - flex-direction: row; justify-content: space-between; - align-items: center; color: var(--add-members-item-color); font-size: var(--l-font-18); - background-color: transparent; - border: none; width: 100%; } button.addMemberItem:hover { color: var(--add-members-item-color-hover); } button.addMemberItem:disabled { color: var(--add-members-item-disabled-color); cursor: not-allowed; } button.addMemberItem:hover:disabled { color: var(--add-members-item-disabled-color-hover); } button.addMemberItem .label { padding: 8px 16px; } button.addMemberItem .danger { color: var(--add-members-remove-pending-color); } button.addMemberItem:hover .danger { color: var(--add-members-remove-pending-color-hover); } diff --git a/web/modals/modal.css b/web/modals/modal.css index 2d64163bf..6bffc6511 100644 --- a/web/modals/modal.css +++ b/web/modals/modal.css @@ -1,43 +1,41 @@ div.modalContainer { display: flex; background-color: var(--modal-bg); border-radius: 8px; flex-direction: column; margin: 20px; overflow: hidden; } div.modalContainerSmall { width: 330px; } div.modalContainerLarge { width: 500px; } -span.modalClose { - display: flex; +.modalClose { color: var(--modal-close-color); } -span.modalClose:hover { - cursor: pointer; +.modalClose:hover { color: var(--modal-close-color-hover); } div.modalHeader { display: flex; justify-content: space-between; align-items: center; padding: 32px 32px 0 32px; } h2.title { font-size: 20px; font-weight: 500; line-height: 32px; color: var(--fg); display: flex; align-items: center; column-gap: 8px; } diff --git a/web/modals/modal.react.js b/web/modals/modal.react.js index 858fe8656..3f72f142b 100644 --- a/web/modals/modal.react.js +++ b/web/modals/modal.react.js @@ -1,79 +1,80 @@ // @flow import classNames from 'classnames'; import * as React from 'react'; import ModalOverlay from 'lib/components/modal-overlay.react'; +import Button from '../components/button.react'; import SWMansionIcon, { type Icon } from '../SWMansionIcon.react'; import css from './modal.css'; export type ModalSize = 'small' | 'large' | 'fit-content'; export type ModalOverridableProps = { +name: string, +icon?: Icon, +onClose: () => void, +withCloseButton?: boolean, +size?: ModalSize, }; type ModalProps = { ...ModalOverridableProps, +children?: React.Node, }; function Modal(props: ModalProps): React.Node { const { size = 'small', children, onClose, name, icon, withCloseButton = true, } = props; const modalContainerClasses = React.useMemo( () => classNames(css.modalContainer, { [css.modalContainerLarge]: size === 'large', [css.modalContainerSmall]: size === 'small', }), [size], ); const cornerCloseButton = React.useMemo(() => { if (!withCloseButton) { return null; } return ( - + ); }, [onClose, withCloseButton]); const headerIcon = React.useMemo(() => { if (!icon) { return null; } return ; }, [icon]); return (

{headerIcon} {name}

{cornerCloseButton}
{children}
); } export default Modal; diff --git a/web/modals/threads/color-selector-button.css b/web/modals/threads/color-selector-button.css index 1cf2e271c..dbdeff1fe 100644 --- a/web/modals/threads/color-selector-button.css +++ b/web/modals/threads/color-selector-button.css @@ -1,21 +1,17 @@ -div.container { +.container { height: 48px; width: 48px; border-radius: 24px; - cursor: pointer; - align-items: center; - justify-content: center; - display: flex; } -div.active, -div.container:hover { +.active, +.container:hover { background-color: var(--color-selector-active-bg); } div.colorSplotch { height: 32px; width: 32px; border-radius: 16px; cursor: pointer; } diff --git a/web/modals/threads/color-selector-button.react.js b/web/modals/threads/color-selector-button.react.js index e0869ab8f..452326aa0 100644 --- a/web/modals/threads/color-selector-button.react.js +++ b/web/modals/threads/color-selector-button.react.js @@ -1,40 +1,41 @@ // @flow import classNames from 'classnames'; import * as React from 'react'; import tinycolor from 'tinycolor2'; +import Button from '../../components/button.react'; import css from './color-selector-button.css'; type ColorSelectorButtonProps = { +color: string, +currentColor: string, +onColorSelection: (hex: string) => void, }; function ColorSelectorButton(props: ColorSelectorButtonProps): React.Node { const { color, currentColor, onColorSelection } = props; const active = tinycolor.equals(color, currentColor); const containerClassName = classNames(css.container, { [css.active]: active, }); const colorSplotchStyle = React.useMemo( () => ({ backgroundColor: `#${color}`, }), [color], ); const onColorSplotchClicked = React.useCallback(() => { onColorSelection(color); }, [onColorSelection, color]); return ( -
+ ); } export default ColorSelectorButton; diff --git a/web/modals/threads/sidebars/sidebar.react.js b/web/modals/threads/sidebars/sidebar.react.js index e30398bf0..bcaaf18fd 100644 --- a/web/modals/threads/sidebars/sidebar.react.js +++ b/web/modals/threads/sidebars/sidebar.react.js @@ -1,79 +1,80 @@ // @flow import * as React from 'react'; import { useModalContext } from 'lib/components/modal-provider.react'; import type { ChatThreadItem } from 'lib/selectors/chat-selectors'; import { getMessagePreview } from 'lib/shared/message-utils'; import { shortAbsoluteDate } from 'lib/utils/date-utils'; +import Button from '../../../components/button.react'; import { getDefaultTextMessageRules } from '../../../markdown/rules.react'; import { useSelector } from '../../../redux/redux-utils'; import { useOnClickThread } from '../../../selectors/nav-selectors'; import css from './sidebars-modal.css'; type Props = { +sidebar: ChatThreadItem, +isLastItem?: boolean, }; function Sidebar(props: Props): React.Node { const { sidebar, isLastItem } = props; const { threadInfo, lastUpdatedTime, mostRecentMessageInfo } = sidebar; const timeZone = useSelector(state => state.timeZone); const { popModal } = useModalContext(); const navigateToThread = useOnClickThread(threadInfo); const onClickThread = React.useCallback( event => { popModal(); navigateToThread(event); }, [popModal, navigateToThread], ); const lastActivity = React.useMemo( () => shortAbsoluteDate(lastUpdatedTime, timeZone), [lastUpdatedTime, timeZone], ); const lastMessage = React.useMemo(() => { if (!mostRecentMessageInfo) { return
No messages
; } const { message, username } = getMessagePreview( mostRecentMessageInfo, threadInfo, getDefaultTextMessageRules().simpleMarkdownRules, ); const previewText = username ? `${username}: ${message}` : message; return ( <>
{previewText}
{lastActivity}
); }, [lastActivity, mostRecentMessageInfo, threadInfo]); return ( - + ); } export default Sidebar; diff --git a/web/modals/threads/sidebars/sidebars-modal.css b/web/modals/threads/sidebars/sidebars-modal.css index faa7e4555..76aa6bdc8 100644 --- a/web/modals/threads/sidebars/sidebars-modal.css +++ b/web/modals/threads/sidebars/sidebars-modal.css @@ -1,71 +1,68 @@ div.sidebarListContainer { display: flex; flex-direction: column; line-height: var(--line-height-text); width: 383px; height: 458px; } div.sidebarList { overflow: auto; color: var(--sidebars-modal-color); } div.noSidebars { padding: 16px; text-align: center; color: var(--sidebars-modal-color); } button.sidebarContainer { - cursor: pointer; - display: flex; padding: 0 16px; column-gap: 8px; align-items: flex-start; width: 100%; - border: none; font-size: inherit; text-align: inherit; line-height: inherit; color: inherit; background: inherit; } button.sidebarContainer:hover { color: var(--sidebars-modal-color-hover); } div.sidebarInfo { flex: 1; display: flex; flex-direction: column; overflow: hidden; padding: 8px 0; } div.longTextEllipsis { text-overflow: ellipsis; overflow: hidden; white-space: nowrap; } div.lastMessage { display: flex; justify-content: space-between; column-gap: 14px; } div.noMessage { text-align: center; font-style: italic; } div.lastActivity { white-space: nowrap; } img.sidebarArrow { position: relative; top: -12px; } diff --git a/web/modals/threads/subchannels/subchannel.react.js b/web/modals/threads/subchannels/subchannel.react.js index d0a030c3f..16ad07fce 100644 --- a/web/modals/threads/subchannels/subchannel.react.js +++ b/web/modals/threads/subchannels/subchannel.react.js @@ -1,75 +1,76 @@ // @flow import * as React from 'react'; import { useModalContext } from 'lib/components/modal-provider.react'; import { type ChatThreadItem } from 'lib/selectors/chat-selectors'; import { getMessagePreview } from 'lib/shared/message-utils'; import { shortAbsoluteDate } from 'lib/utils/date-utils'; +import Button from '../../../components/button.react'; import { getDefaultTextMessageRules } from '../../../markdown/rules.react'; import { useSelector } from '../../../redux/redux-utils'; import { useOnClickThread } from '../../../selectors/nav-selectors'; import SWMansionIcon from '../../../SWMansionIcon.react'; import css from './subchannels-modal.css'; type Props = { +chatThreadItem: ChatThreadItem, }; function Subchannel(props: Props): React.Node { const { chatThreadItem } = props; const { threadInfo, mostRecentMessageInfo, lastUpdatedTimeIncludingSidebars, } = chatThreadItem; const timeZone = useSelector(state => state.timeZone); const { popModal } = useModalContext(); const navigateToThread = useOnClickThread(threadInfo); const onClickThread = React.useCallback( event => { popModal(); navigateToThread(event); }, [popModal, navigateToThread], ); const lastActivity = React.useMemo( () => shortAbsoluteDate(lastUpdatedTimeIncludingSidebars, timeZone), [lastUpdatedTimeIncludingSidebars, timeZone], ); const lastMessage = React.useMemo(() => { if (!mostRecentMessageInfo) { return
No messages
; } const { message, username } = getMessagePreview( mostRecentMessageInfo, threadInfo, getDefaultTextMessageRules().simpleMarkdownRules, ); const previewText = username ? `${username}: ${message}` : message; return ( <>
{previewText}
{lastActivity}
); }, [lastActivity, mostRecentMessageInfo, threadInfo]); return ( -
+
+ ); } export default Subchannel; diff --git a/web/modals/threads/subchannels/subchannels-modal.css b/web/modals/threads/subchannels/subchannels-modal.css index 064a8f3cf..9242aed33 100644 --- a/web/modals/threads/subchannels/subchannels-modal.css +++ b/web/modals/threads/subchannels/subchannels-modal.css @@ -1,54 +1,55 @@ div.subchannelsListContainer { display: flex; flex-direction: column; overflow: auto; - line-height: var(--line-height-text); - color: var(--subchannels-modal-color); row-gap: 8px; width: 383px; height: 458px; } div.noSubchannels { text-align: center; } -div.subchannelContainer { - cursor: pointer; - display: flex; +.subchannelContainer { + align-items: flex-start; + font-size: var(--m-font-18); + line-height: var(--line-height-text); + color: var(--subchannels-modal-color); padding: 8px 16px; column-gap: 8px; } -div.subchannelContainer:hover { +.subchannelContainer:hover { color: var(--subchannels-modal-color-hover); } div.subchannelInfo { flex: 1; display: flex; flex-direction: column; row-gap: 8px; overflow: hidden; } div.longTextEllipsis { + align-self: flex-start; text-overflow: ellipsis; overflow: hidden; white-space: nowrap; } div.lastMessage { display: flex; justify-content: space-between; column-gap: 14px; } div.noMessage { text-align: center; font-style: italic; } div.lastActivity { white-space: nowrap; } diff --git a/web/modals/threads/thread-picker-modal.css b/web/modals/threads/thread-picker-modal.css index 8c231a4a0..073b8d5b4 100644 --- a/web/modals/threads/thread-picker-modal.css +++ b/web/modals/threads/thread-picker-modal.css @@ -1,41 +1,45 @@ div.container { display: flex; flex-direction: column; overflow: hidden; margin: 16px; } div.contentContainer { overflow: scroll; height: 448px; } div.threadPickerOptionContainer { display: flex; - align-items: center; +} + +.threadPickerOptionButton { + flex: 1; + justify-content: left; padding: 12px 16px; - cursor: pointer; + font-size: var(--m-font-16); } -div.threadPickerOptionContainer:hover { +.threadPickerOptionButton:hover { background-color: var(--thread-hover-bg); border-radius: 8px; } div.threadSplotch { min-width: 40px; height: 40px; border-radius: 10px; } div.threadNameText { color: var(--shades-white-100); margin-left: 16px; } div.noResultsText { text-align: center; color: var(--shades-white-100); margin-top: 24px; font-weight: 500; } diff --git a/web/modals/threads/thread-picker-modal.react.js b/web/modals/threads/thread-picker-modal.react.js index fb60aac0c..36a8b67de 100644 --- a/web/modals/threads/thread-picker-modal.react.js +++ b/web/modals/threads/thread-picker-modal.react.js @@ -1,134 +1,136 @@ // @flow import invariant from 'invariant'; import * as React from 'react'; import { createSelector } from 'reselect'; import { threadSearchIndex } from 'lib/selectors/nav-selectors'; import { onScreenEntryEditableThreadInfos } from 'lib/selectors/thread-selectors'; import type { ThreadInfo } from 'lib/types/thread-types'; +import Button from '../../components/button.react'; import Search from '../../components/search.react'; import { useSelector } from '../../redux/redux-utils'; import Modal, { type ModalOverridableProps } from '../modal.react'; import css from './thread-picker-modal.css'; type OptionProps = { +threadInfo: ThreadInfo, +createNewEntry: (threadID: string) => void, +onCloseModal: () => void, }; function ThreadPickerOption(props: OptionProps) { const { threadInfo, createNewEntry, onCloseModal } = props; const onClickThreadOption = React.useCallback(() => { createNewEntry(threadInfo.id); onCloseModal(); }, [threadInfo.id, createNewEntry, onCloseModal]); const splotchColorStyle = React.useMemo( () => ({ backgroundColor: `#${threadInfo.color}`, }), [threadInfo.color], ); return ( -
-
-
{threadInfo.uiName}
+
+
); } type Props = { ...ModalOverridableProps, +createNewEntry: (threadID: string) => void, }; function ThreadPickerModal(props: Props): React.Node { const { createNewEntry, ...modalProps } = props; const onScreenThreadInfos = useSelector(onScreenEntryEditableThreadInfos); const searchIndex = useSelector(state => threadSearchIndex(state)); invariant( onScreenThreadInfos.length > 0, "ThreadPicker can't be open when onScreenThreadInfos is empty", ); const [searchText, setSearchText] = React.useState(''); const [searchResults, setSearchResults] = React.useState>( new Set(), ); const onChangeSearchText = React.useCallback( (text: string) => { const results = searchIndex.getSearchResults(text); setSearchText(text); setSearchResults(new Set(results)); }, [searchIndex], ); const listDataSelector = createSelector( state => state.onScreenThreadInfos, state => state.searchText, state => state.searchResults, ( threadInfos: $ReadOnlyArray, text: string, results: Set, ) => text ? threadInfos.filter(threadInfo => results.has(threadInfo.id)) : [...threadInfos], ); const threads = useSelector(() => listDataSelector({ onScreenThreadInfos, searchText, searchResults, }), ); const threadPickerContent = React.useMemo(() => { const options = threads.map(threadInfo => ( )); if (options.length === 0 && searchText.length > 0) { return (
No results for {searchText}
); } else { return options; } }, [threads, createNewEntry, modalProps.onClose, searchText]); return (
{threadPickerContent}
); } export default ThreadPickerModal; diff --git a/web/settings/relationship/add-users-list-item.react.js b/web/settings/relationship/add-users-list-item.react.js index c495aa2e5..2e6547ce6 100644 --- a/web/settings/relationship/add-users-list-item.react.js +++ b/web/settings/relationship/add-users-list-item.react.js @@ -1,28 +1,29 @@ // @flow import * as React from 'react'; import type { AccountUserInfo } from 'lib/types/user-types.js'; +import Button from '../../components/button.react'; import css from './add-users-list.css'; type Props = { +userInfo: AccountUserInfo, +selectUser: (userID: string) => mixed, }; function AddUsersListItem(props: Props): React.Node { const { userInfo, selectUser } = props; const addUser = React.useCallback(() => selectUser(userInfo.id), [ selectUser, userInfo.id, ]); return ( - + ); } export default AddUsersListItem; diff --git a/web/settings/relationship/add-users-list.css b/web/settings/relationship/add-users-list.css index 6cfe88d46..d161fc6fe 100644 --- a/web/settings/relationship/add-users-list.css +++ b/web/settings/relationship/add-users-list.css @@ -1,63 +1,59 @@ .container { height: 625px; display: flex; flex-direction: column; } .userTagsContainer { display: flex; flex-wrap: wrap; gap: 6px; margin: 8px; } .userRowsContainer { overflow: auto; display: flex; flex-direction: column; flex: 1; margin-bottom: 8px; } .addUserButton { - display: flex; - flex-direction: row; justify-content: space-between; padding: 16px; color: var(--relationship-modal-color); font-size: var(--l-font-18); line-height: var(--line-height-display); - background: transparent; - border: none; } .addUserButtonUsername { white-space: nowrap; overflow: hidden; text-overflow: ellipsis; } .buttons { display: flex; justify-content: space-between; } .confirmButtonContainer { display: flex; flex-direction: column; align-items: center; } .hidden { visibility: hidden; height: 0; } .error { padding-bottom: 8px; font-size: var(--s-font-14); line-height: var(--line-height-display); color: var(--error); padding-left: 6px; font-style: italic; }