diff --git a/stylesheets/components/CircleCheckbox.scss b/stylesheets/components/CircleCheckbox.scss new file mode 100644 index 00000000000..cff1aacb396 --- /dev/null +++ b/stylesheets/components/CircleCheckbox.scss @@ -0,0 +1,92 @@ +// Copyright 2021 Signal Messenger, LLC +// SPDX-License-Identifier: AGPL-3.0-only + +.CircleCheckbox { + &__checkbox { + position: relative; + height: 20px; + width: 20px; + + input[type='checkbox'] { + cursor: pointer; + height: 0; + position: absolute; + width: 0; + + @include keyboard-mode { + &:focus { + &::before { + border-color: $color-ultramarine; + } + outline: none; + } + } + + &::before { + @include rounded-corners; + background: inherit; + content: ''; + display: block; + height: 20px; + position: absolute; + width: 20px; + + @include light-theme { + border: 1.5px solid $color-gray-25; + } + @include dark-theme { + border: 1.5px solid $color-gray-65; + } + } + + &:checked { + &::before { + background: $color-ultramarine; + border: 1.5px solid $color-ultramarine; + } + + &::after { + border: solid $color-white; + border-width: 0 2px 2px 0; + content: ''; + display: block; + height: 11px; + left: 7px; + position: absolute; + top: 3px; + transform: rotate(45deg); + width: 6px; + } + } + &:disabled { + cursor: inherit; + } + + @include light-theme { + &:disabled { + &::before { + border-color: $color-gray-15; + } + } + &:disabled:checked { + &::before { + background: $color-gray-15; + } + } + } + + @include dark-theme { + &:disabled { + &::before { + border-color: $color-gray-45; + } + } + &:disabled:checked { + &::before { + background: $color-gray-45; + } + } + } + } + } +} diff --git a/stylesheets/components/ListTile.scss b/stylesheets/components/ListTile.scss new file mode 100644 index 00000000000..ed64e3daee1 --- /dev/null +++ b/stylesheets/components/ListTile.scss @@ -0,0 +1,103 @@ +// Copyright 2022 Signal Messenger, LLC +// SPDX-License-Identifier: AGPL-3.0-only + +button.ListTile { + width: 100%; +} + +.ListTile { + display: flex; + align-items: center; + padding: 6px 14px; + user-select: none; + + // use a transparent border to inset the background + border: 2px solid transparent; + border-width: 2px 10px; + background-clip: padding-box; + border-radius: 20px / 12px; + + // reset button styles + background-color: transparent; + color: inherit; + box-sizing: border-box; + text-align: inherit; + + &--variant-panelrow { + padding: 8px 16px; + } + + &__content { + flex: 1; + display: flex; + flex-direction: column; + font-family: $inter; + + .ListTile[aria-disabled='true'] & { + opacity: 0.5; + } + } + + &__title { + display: -webkit-box; + -webkit-line-clamp: 1; + -webkit-box-orient: vertical; + overflow: hidden; + font-size: 14px; + line-height: 20px; + } + + &__subtitle { + display: -webkit-box; + -webkit-box-orient: vertical; + overflow: hidden; + font-size: 12px; + color: $color-gray-25; + line-height: 17px; + + &--max-lines-1 { + -webkit-line-clamp: 1; + } + &--max-lines-2 { + -webkit-line-clamp: 2; + } + &--max-lines-3 { + -webkit-line-clamp: 3; + } + + @include light-theme { + color: $color-gray-60; + } + @include dark-theme { + color: $color-gray-25; + } + } + + &[aria-disabled='true'] { + cursor: not-allowed; + } + + &__leading { + margin-right: 12px; + } + &__trailing { + margin-left: 12px; + } + + &--clickable { + cursor: pointer; + &:hover:not([aria-disabled='true']) { + @include light-theme { + background-color: $color-black-alpha-06; + } + + @include dark-theme { + background-color: $color-white-alpha-06; + } + + & .ConversationDetails-panel-row__actions { + opacity: 1; + } + } + } +} diff --git a/stylesheets/manifest.scss b/stylesheets/manifest.scss index e6314c2897d..cae5d8ecfde 100644 --- a/stylesheets/manifest.scss +++ b/stylesheets/manifest.scss @@ -49,6 +49,7 @@ @import './components/CallingToast.scss'; @import './components/ChatColorPicker.scss'; @import './components/Checkbox.scss'; +@import './components/CircleCheckbox.scss'; @import './components/CompositionArea.scss'; @import './components/CompositionTextArea.scss'; @import './components/ContactModal.scss'; @@ -87,6 +88,7 @@ @import './components/LeftPaneDialog.scss'; @import './components/LeftPaneSearchInput.scss'; @import './components/Lightbox.scss'; +@import './components/ListTile.scss'; @import './components/MediaEditor.scss'; @import './components/MediaQualitySelector.scss'; @import './components/MessageAudio.scss'; diff --git a/ts/components/CircleCheckbox.stories.tsx b/ts/components/CircleCheckbox.stories.tsx new file mode 100644 index 00000000000..12f7760582d --- /dev/null +++ b/ts/components/CircleCheckbox.stories.tsx @@ -0,0 +1,30 @@ +// Copyright 2021 Signal Messenger, LLC +// SPDX-License-Identifier: AGPL-3.0-only + +import React from 'react'; +import { action } from '@storybook/addon-actions'; + +import type { Props } from './CircleCheckbox'; +import { CircleCheckbox } from './CircleCheckbox'; + +const createProps = (): Props => ({ + checked: false, + name: 'check-me', + onChange: action('onChange'), +}); + +export default { + title: 'Components/CircleCheckbox', +}; + +export function Normal(): JSX.Element { + return ; +} + +export function Checked(): JSX.Element { + return ; +} + +export function Disabled(): JSX.Element { + return ; +} diff --git a/ts/components/CircleCheckbox.tsx b/ts/components/CircleCheckbox.tsx new file mode 100644 index 00000000000..2bbd4c3482b --- /dev/null +++ b/ts/components/CircleCheckbox.tsx @@ -0,0 +1,50 @@ +// Copyright 2021 Signal Messenger, LLC +// SPDX-License-Identifier: AGPL-3.0-only + +import React from 'react'; + +import { getClassNamesFor } from '../util/getClassNamesFor'; + +export type Props = { + id?: string; + checked?: boolean; + disabled?: boolean; + isRadio?: boolean; + name?: string; + onChange?: (value: boolean) => unknown; + onClick?: () => unknown; +}; + +/** + * A fancy checkbox + * + * It's only the checkbox, it does NOT produce a label. + * It is a controlled component, you must provide a value and + * update it yourself onClick/onChange. + */ +export function CircleCheckbox({ + id, + checked, + disabled, + isRadio, + name, + onChange, + onClick, +}: Props): JSX.Element { + const getClassName = getClassNamesFor('CircleCheckbox'); + + return ( +
+ onChange(ev.target.checked))} + onClick={onClick} + type={isRadio ? 'radio' : 'checkbox'} + /> +
+ ); +} diff --git a/ts/components/ListTile.stories.tsx b/ts/components/ListTile.stories.tsx new file mode 100644 index 00000000000..2bc4e6c7a3b --- /dev/null +++ b/ts/components/ListTile.stories.tsx @@ -0,0 +1,106 @@ +// Copyright 2022 Signal Messenger, LLC +// SPDX-License-Identifier: AGPL-3.0-only + +import type { Story } from '@storybook/react'; +import React from 'react'; +import { ListTile } from './ListTile'; +import type { Props } from './ListTile'; +import { Emojify } from './conversation/Emojify'; +import { CircleCheckbox } from './CircleCheckbox'; + +export default { + title: 'Components/ListTile', + component: ListTile, +}; + +const lorem = + 'Lorem ipsum dolor sit amet, consectetur adipiscing elit. Etiam feugiat quam vitae semper facilisis. Praesent eu efficitur dui. Donec semper mattis nisl non hendrerit.'; + +function TemplateList(width: number): Story { + // eslint-disable-next-line react/display-name + return args => { + return ( +
+ } + clickable + /> + } + clickable + /> + + + + } + subtitle="Clickable" + clickable + /> + } + subtitle={ + + } + /> + + + + +
+ ); + }; +} + +const circleAvatar = ( +
+); + +export const Item = TemplateList(400).bind({}); +Item.args = { + leading: circleAvatar, + title: , + subtitle: 'Hello my friend', + clickable: true, +}; + +export const PanelRow = TemplateList(800).bind({}); +PanelRow.args = { + leading: circleAvatar, + title: 'Some user', + subtitle: 'Hello my friend', + trailing:
Admin
, + clickable: false, + variant: 'panelrow', +}; diff --git a/ts/components/ListTile.tsx b/ts/components/ListTile.tsx new file mode 100644 index 00000000000..cf1a275653f --- /dev/null +++ b/ts/components/ListTile.tsx @@ -0,0 +1,114 @@ +// Copyright 2022 Signal Messenger, LLC +// SPDX-License-Identifier: AGPL-3.0-only + +import classNames from 'classnames'; +import React from 'react'; +import { getClassNamesFor } from '../util/getClassNamesFor'; + +export type Props = { + title: string | JSX.Element; + subtitle?: string | JSX.Element; + leading?: string | JSX.Element; + trailing?: string | JSX.Element; + onClick?: () => void; + onContextMenu?: (ev: React.MouseEvent) => void; + // show hover highlight, + // defaults to true if onClick is defined + clickable?: boolean; + // defaults to 2 + subtitleMaxLines?: 1 | 2 | 3; + // defaults to false + disabled?: boolean; + // defaults to item + variant?: 'item' | 'panelrow'; + // defaults to div + rootElement?: 'div' | 'button'; +}; + +const getClassName = getClassNamesFor('ListTile'); + +/** + * A single row that typically contains some text and leading/trailing icons/widgets + * + * Mostly intended for items on a list: conversations, contacts, groups, options, etc + * where all items have the same height. + * + * If wrapping with