diff --git a/examples/vite/src/index.scss b/examples/vite/src/index.scss index e5ad46e40..54a8f367a 100644 --- a/examples/vite/src/index.scss +++ b/examples/vite/src/index.scss @@ -97,16 +97,26 @@ body { max-width: 360px; } - @media (max-width: 960px) { - .str-chat__channel-list { - flex-basis: 260px; - max-width: 260px; + .str-chat__chat-view__threads { + .str-chat__thread-container { + flex: 1 1 auto; + min-width: 360px; + max-width: none; } + } - .str-chat__thread-list-container, - .str-chat__thread-container { - flex-basis: 320px; - max-width: 320px; + @media (max-width: 1100px) { + .str-chat__container:has(.str-chat__thread-container) > .str-chat__main-panel { + width: 0; + min-width: 0; + flex: 0 0 0; + overflow: hidden; + } + + .str-chat__container:has(.str-chat__thread-container) > .str-chat__thread-container { + flex: 1 1 auto; + min-width: 360px; + max-width: none; } } diff --git a/examples/vite/src/stream-imports-layout.scss b/examples/vite/src/stream-imports-layout.scss index 84456cc38..388903a36 100644 --- a/examples/vite/src/stream-imports-layout.scss +++ b/examples/vite/src/stream-imports-layout.scss @@ -8,7 +8,7 @@ //@use 'stream-chat-react/dist/scss/v2/Autocomplete/Autocomplete-layout'; //@use 'stream-chat-react/dist/scss/v2/AudioRecorder/AudioRecorder-layout'; @use 'stream-chat-react/dist/scss/v2/BaseImage/BaseImage-layout'; -@use 'stream-chat-react/dist/scss/v2/Channel/Channel-layout'; +//@use 'stream-chat-react/dist/scss/v2/Channel/Channel-layout'; @use 'stream-chat-react/dist/scss/v2/ChannelHeader/ChannelHeader-layout'; @use 'stream-chat-react/dist/scss/v2/ChannelList/ChannelList-layout'; @use 'stream-chat-react/dist/scss/v2/ChannelPreview/ChannelPreview-layout'; @@ -18,8 +18,8 @@ //@use 'stream-chat-react/dist/scss/v2/Dialog/Dialog-layout'; @use 'stream-chat-react/dist/scss/v2/DragAndDropContainer/DragAndDropContainer-layout'; @use 'stream-chat-react/dist/scss/v2/DropzoneContainer/DropzoneContainer-layout'; // X -@use 'stream-chat-react/dist/scss/v2/EditMessageForm/EditMessageForm-layout'; -@use 'stream-chat-react/dist/scss/v2/Form/Form-layout'; +//@use 'stream-chat-react/dist/scss/v2/EditMessageForm/EditMessageForm-layout'; +//@use 'stream-chat-react/dist/scss/v2/Form/Form-layout'; @use 'stream-chat-react/dist/scss/v2/ImageCarousel/ImageCarousel-layout'; //@use 'stream-chat-react/dist/scss/v2/Icon/Icon-layout'; @use 'stream-chat-react/dist/scss/v2/InfiniteScrollPaginator/InfiniteScrollPaginator-layout'; @@ -38,7 +38,7 @@ @use 'stream-chat-react/dist/scss/v2/Notification/MessageNotification-layout'; @use 'stream-chat-react/dist/scss/v2/Notification/NotificationList-layout'; @use 'stream-chat-react/dist/scss/v2/Notification/Notification-layout'; -@use 'stream-chat-react/dist/scss/v2/Poll/Poll-layout'; +//@use 'stream-chat-react/dist/scss/v2/Poll/Poll-layout'; @use 'stream-chat-react/dist/scss/v2/Search/Search-layout'; @use 'stream-chat-react/dist/scss/v2/Thread/Thread-layout'; @use 'stream-chat-react/dist/scss/v2/Tooltip/Tooltip-layout'; diff --git a/examples/vite/src/stream-imports-theme.scss b/examples/vite/src/stream-imports-theme.scss index 727c02e92..f93d94dd7 100644 --- a/examples/vite/src/stream-imports-theme.scss +++ b/examples/vite/src/stream-imports-theme.scss @@ -8,7 +8,7 @@ //@use 'stream-chat-react/dist/scss/v2/AudioRecorder/AudioRecorder-theme'; //@use 'stream-chat-react/dist/scss/v2/Autocomplete/Autocomplete-theme'; @use 'stream-chat-react/dist/scss/v2/BaseImage/BaseImage-theme'; -@use 'stream-chat-react/dist/scss/v2/Channel/Channel-theme.scss'; +//@use 'stream-chat-react/dist/scss/v2/Channel/Channel-theme.scss'; @use 'stream-chat-react/dist/scss/v2/ChannelHeader/ChannelHeader-theme'; @use 'stream-chat-react/dist/scss/v2/ChannelList/ChannelList-theme'; @use 'stream-chat-react/dist/scss/v2/ChannelPreview/ChannelPreview-theme'; @@ -16,7 +16,7 @@ //@use 'stream-chat-react/dist/scss/v2/Dialog/Dialog-theme'; @use 'stream-chat-react/dist/scss/v2/DragAndDropContainer/DragAndDropContainer-theme'; @use 'stream-chat-react/dist/scss/v2/DropzoneContainer/DropzoneContainer-theme'; -@use 'stream-chat-react/dist/scss/v2/Form/Form-theme'; +//@use 'stream-chat-react/dist/scss/v2/Form/Form-theme'; //@use 'stream-chat-react/dist/scss/v2/Icon/Icon-theme'; @use 'stream-chat-react/dist/scss/v2/ImageCarousel/ImageCarousel-theme'; @use 'stream-chat-react/dist/scss/v2/LoadingIndicator/LoadingIndicator-theme'; @@ -32,7 +32,7 @@ @use 'stream-chat-react/dist/scss/v2/Notification/MessageNotification-theme'; @use 'stream-chat-react/dist/scss/v2/Notification/NotificationList-theme'; @use 'stream-chat-react/dist/scss/v2/Notification/Notification-theme'; -@use 'stream-chat-react/dist/scss/v2/Poll/Poll-theme'; +//@use 'stream-chat-react/dist/scss/v2/Poll/Poll-theme'; @use 'stream-chat-react/dist/scss/v2/Search/Search-theme'; @use 'stream-chat-react/dist/scss/v2/Thread/Thread-theme'; @use 'stream-chat-react/dist/scss/v2/Tooltip/Tooltip-theme'; diff --git a/src/components/Attachment/styling/Giphy.scss b/src/components/Attachment/styling/Giphy.scss index d311e3d76..acb4841b3 100644 --- a/src/components/Attachment/styling/Giphy.scss +++ b/src/components/Attachment/styling/Giphy.scss @@ -39,11 +39,6 @@ background-color: var(--badge-bg-overlay, rgba(0, 0, 0, 0.75)); color: var(--badge-text-on-accent, #fff); - font-feature-settings: - 'liga' off, - 'clig' off; - - /* metadata/emphasis */ font-size: var(--typography-font-size-xs, 12px); font-weight: var(--typography-font-weight-semi-bold, 600); line-height: var(--typography-line-height-tight, 16px); diff --git a/src/components/Avatar/Avatar.tsx b/src/components/Avatar/Avatar.tsx index d0748cbad..e2defda74 100644 --- a/src/components/Avatar/Avatar.tsx +++ b/src/components/Avatar/Avatar.tsx @@ -5,7 +5,7 @@ import React, { useMemo, useState, } from 'react'; -import { IconPeople } from '../Icons'; +import { IconPerson } from '../Icons'; export type AvatarProps = { /** URL of the avatar image */ @@ -99,7 +99,7 @@ export const Avatar = ({ {sizeAwareInitials} )} - {!sizeAwareInitials.length && } + {!sizeAwareInitials.length && } )} diff --git a/src/components/Avatar/AvatarStack.tsx b/src/components/Avatar/AvatarStack.tsx index 3b4a8c80a..14c2feb4b 100644 --- a/src/components/Avatar/AvatarStack.tsx +++ b/src/components/Avatar/AvatarStack.tsx @@ -1,4 +1,3 @@ -/* eslint-disable arrow-body-style */ import { type ComponentProps, type ElementType } from 'react'; import { useComponentContext } from '../../context'; import { type AvatarProps, Avatar as DefaultAvatar } from './Avatar'; diff --git a/src/components/Avatar/styling/Avatar.scss b/src/components/Avatar/styling/Avatar.scss index 45c53d6d3..73457fd07 100644 --- a/src/components/Avatar/styling/Avatar.scss +++ b/src/components/Avatar/styling/Avatar.scss @@ -26,7 +26,7 @@ height: 100%; } - .str-chat__icon--people { + .str-chat__icon--person { width: var(--avatar-icon-size); height: var(--avatar-icon-size); stroke-width: var(--avatar-icon-stroke-width); @@ -127,4 +127,3 @@ } } } - diff --git a/src/components/Button/styling/Button.scss b/src/components/Button/styling/Button.scss index 60a48d57d..68082c5a4 100644 --- a/src/components/Button/styling/Button.scss +++ b/src/components/Button/styling/Button.scss @@ -13,8 +13,8 @@ justify-content: center; gap: var(--spacing-xs); - line-height: var(--typography-line-height-normal); - font-weight: var(--typography-font-weight-semibold); + font: var(--str-chat__body-emphasis-text); + text-transform: capitalize; &.str-chat__button--solid { &.str-chat__button--primary { diff --git a/src/components/Channel/styling/Channel.scss b/src/components/Channel/styling/Channel.scss new file mode 100644 index 000000000..27aae65a2 --- /dev/null +++ b/src/components/Channel/styling/Channel.scss @@ -0,0 +1,241 @@ +@use '../../../styling/utils'; + +.str-chat__channel { + height: 100%; + display: flex; + flex-direction: column; + position: relative; + + .str-chat__container { + height: 100%; + display: flex; + + .str-chat__main-panel { + position: relative; + height: 100%; + display: flex; + flex-direction: column; + width: 100%; + min-width: 0; + } + } +} + +.str-chat__empty-channel { + --str-chat-icon-height: calc(var(--str-chat__spacing-px) * 136); + @include utils.empty-layout; + position: relative; + + .str-chat__empty-channel-notifications { + position: absolute; + inset-block-end: var(--str-chat__spacing-2); + } +} + +.str-chat__loading-channel { + $text-height: calc(var(--str-chat__spacing-px) * 16); + height: 100%; + display: flex; + flex-direction: column; + + .str-chat__loading-channel-header { + @include utils.header-layout; + + .str-chat__loading-channel-header-avatar { + flex-shrink: 0; + width: calc(var(--str-chat__spacing-px) * 40); + height: calc(var(--str-chat__spacing-px) * 40); + border-radius: var(--str-chat__avatar-border-radius); + } + + .str-chat__loading-channel-header-end { + @include utils.header-text-layout; + + .str-chat__loading-channel-header-name { + border-radius: var(--str-chat__border-radius-xs); + height: $text-height; + width: calc(var(--str-chat__spacing-px) * 170); + } + + .str-chat__loading-channel-header-info { + border-radius: var(--str-chat__border-radius-xs); + height: $text-height; + width: calc(var(--str-chat__spacing-px) * 66); + } + } + } + + .str-chat__loading-channel-message-list { + /* stylelint-disable */ + height: 100%; + @include utils.message-list-spacing; + + .str-chat__loading-channel-message { + display: flex; + width: 100%; + column-gap: var(--str-chat__spacing-2); + padding: var(--str-chat__spacing-4) 0; + + .str-chat__loading-channel-message-avatar { + flex-shrink: 0; + width: calc(var(--str-chat__spacing-px) * 49); + height: calc(var(--str-chat__spacing-px) * 49); + } + + .str-chat__loading-channel-message-end { + display: flex; + flex-direction: column; + width: 100%; + row-gap: var(--str-chat__spacing-2); + + .str-chat__loading-channel-message-last-row { + display: flex; + width: 100%; + column-gap: var(--str-chat__spacing-2); + } + } + + .str-chat__loading-channel-message-sender { + height: $text-height; + width: calc(var(--str-chat__spacing-px) * 66); + } + + .str-chat__loading-channel-message-text { + height: $text-height; + width: 100%; + } + + .str-chat__loading-channel-message-date { + height: $text-height; + width: calc(var(--str-chat__spacing-px) * 50); + } + + &:nth-of-type(2) { + flex-direction: row-reverse; + + .str-chat__loading-channel-message-sender { + align-self: end; + } + + .str-chat__loading-channel-message-last-row { + flex-direction: row-reverse; + } + } + } + } + + .str-chat__loading-channel-message-input-row { + display: flex; + column-gap: var(--str-chat__spacing-2); + padding: var(--str-chat__spacing-2); + + .str-chat__loading-channel-message-input { + width: 100%; + height: calc(var(--str-chat__spacing-px) * 36); + } + + .str-chat__loading-channel-message-send { + height: calc(var(--str-chat__spacing-px) * 36); + width: calc(var(--str-chat__spacing-px) * 36); + } + } +} + +.str-chat { + /* The border radius used for the borders of the component */ + --str-chat__channel-border-radius: 0; + + /* The text/icon color of the component */ + --str-chat__channel-color: var(--str-chat__text-color); + + /* The background color of the component */ + --str-chat__channel-background-color: var(--str-chat__background-color); + + /* Box shadow applied to the component */ + --str-chat__channel-box-shadow: none; + + /* Top border of the component */ + --str-chat__channel-border-block-start: none; + + /* Bottom border of the component */ + --str-chat__channel-border-block-end: none; + + /* Left (right in RTL layout) border of the component */ + --str-chat__channel-border-inline-start: none; + + /* Right (left in RTL layout) border of the component */ + --str-chat__channel-border-inline-end: none; + + /* The icon color used when no channel is selected */ + --str-chat__channel-empty-indicator-color: var(--str-chat__disabled-color); + + /* The text color used when no channel is selected */ + --str-chat__channel-empty-color: var(--str-chat__text-low-emphasis-color); + + /* The color of the loading indicator */ + --str-chat__channel-loading-state-color: var(--str-chat__disabled-color); +} + +.str-chat__channel { + @include utils.component-layer-overrides('channel'); +} + +.str-chat__empty-channel { + --str-chat-icon-color: var(--str-chat__channel-empty-color); + @include utils.component-layer-overrides('channel'); + @include utils.empty-theme('channel'); + + .str-chat__empty-channel-text { + color: var(--str-chat__channel-empty-color); + } +} + +.str-chat__loading-channel { + @include utils.loading-animation; + + .str-chat__loading-channel-header { + background-color: var(--str-chat__channel-header-background-color); + + .str-chat__loading-channel-header-avatar { + @include utils.loading-item-background('channel'); + border-radius: var(--str-chat__avatar-border-radius); + } + + .str-chat__loading-channel-header-name, + .str-chat__loading-channel-header-info { + @include utils.loading-item-background('channel'); + border-radius: var(--str-chat__border-radius-xs); + } + } + + .str-chat__loading-channel-message-list { + background-color: var(--str-chat__message-list-background-color); + + .str-chat__loading-channel-message-avatar { + @include utils.loading-item-background('channel'); + border-radius: var(--str-chat__avatar-border-radius); + } + + .str-chat__loading-channel-message-sender, + .str-chat__loading-channel-message-text, + .str-chat__loading-channel-message-date { + @include utils.loading-item-background('channel'); + border-radius: var(--str-chat__message-bubble-border-radius); + } + } + + .str-chat__loading-channel-message-input-row { + .str-chat__loading-channel-message-input, + .str-chat__loading-channel-message-send { + @include utils.loading-item-background('channel'); + } + + .str-chat__loading-channel-message-input { + border-radius: var(--str-chat__message-textarea-border-radius); + } + + .str-chat__loading-channel-message-send { + border-radius: var(--str-chat__message-send-border-radius); + } + } +} diff --git a/src/components/Channel/styling/index.scss b/src/components/Channel/styling/index.scss new file mode 100644 index 000000000..efed3be8b --- /dev/null +++ b/src/components/Channel/styling/index.scss @@ -0,0 +1 @@ +@use 'Channel'; diff --git a/src/components/DateSeparator/styling/DateSeparator.scss b/src/components/DateSeparator/styling/DateSeparator.scss index e242b6f76..2b83e822f 100644 --- a/src/components/DateSeparator/styling/DateSeparator.scss +++ b/src/components/DateSeparator/styling/DateSeparator.scss @@ -34,4 +34,4 @@ font-weight: var(--typography-font-weight-semi-bold); line-height: var(--typography-line-height-tight); } -} \ No newline at end of file +} diff --git a/src/components/Dialog/styling/Dialog.scss b/src/components/Dialog/styling/Dialog.scss index 541118540..ada37e2ff 100644 --- a/src/components/Dialog/styling/Dialog.scss +++ b/src/components/Dialog/styling/Dialog.scss @@ -19,7 +19,7 @@ width: 100%; .str-chat__dialog__body { - padding: 2rem 1rem; + padding: 0 var(--spacing-xl); overflow-y: auto; .str-chat__dialog__title { @@ -30,11 +30,11 @@ .str-chat__dialog__controls { display: flex; justify-content: flex-end; - gap: 1.25rem; - padding: 2.25rem 1.25rem; + gap: var(--spacing-xs); + padding: var(--spacing-xl); .str-chat__dialog__controls-button { - @include utils.button-reset; + font: var(--str-chat__caption-emphasis-text); } } } diff --git a/src/components/Form/FieldError.tsx b/src/components/Form/FieldError.tsx index e7ff1b635..39a547ee6 100644 --- a/src/components/Form/FieldError.tsx +++ b/src/components/Form/FieldError.tsx @@ -1,10 +1,12 @@ import clsx from 'clsx'; -import type { ComponentProps } from 'react'; +import type { ComponentProps, ReactNode } from 'react'; import React from 'react'; -type FieldErrorProps = ComponentProps<'div'> & { - text?: string; +export type FieldErrorProps = ComponentProps<'div'> & { + /** Error message (string or custom content e.g. icon + text) */ + text?: ReactNode; }; + export const FieldError = ({ className, text, ...props }: FieldErrorProps) => (
{text} diff --git a/src/components/Form/NumericInput.tsx b/src/components/Form/NumericInput.tsx new file mode 100644 index 000000000..61ebf8f3a --- /dev/null +++ b/src/components/Form/NumericInput.tsx @@ -0,0 +1,163 @@ +import clsx from 'clsx'; +import React, { forwardRef, useCallback } from 'react'; +import type { ChangeEvent, ComponentProps, KeyboardEvent } from 'react'; +import { useStableId } from '../UtilityComponents/useStableId'; +import { IconPlus } from '../Icons'; +import { Button } from '../Button'; + +export type NumericInputProps = Omit< + ComponentProps<'input'>, + 'className' | 'type' | 'value' | 'onChange' +> & { + value: string; + onChange: (e: ChangeEvent) => void; + /** Optional label above the input */ + label?: string; + /** Minimum value (stepper) */ + min?: number; + /** Maximum value (stepper) */ + max?: number; + /** Step for increment/decrement (default 1) */ + step?: number; + className?: string; +}; + +const parseNumeric = (s: string): number | null => { + const trimmed = s.trim(); + if (trimmed === '') return null; + const n = Number(trimmed); + return Number.isFinite(n) ? n : null; +}; + +const clamp = (n: number, min: number, max: number): number => + Math.min(Math.max(n, min), max); + +export const NumericInput = forwardRef( + function NumericInput( + { + className, + disabled = false, + id: idProp, + label, + max, + min, + onChange, + step = 1, + value, + ...inputProps + }, + ref, + ) { + const generatedId = useStableId(); + const id = idProp ?? generatedId; + + const num = parseNumeric(value); + const minDef = min ?? -Infinity; + const maxDef = max ?? Infinity; + const atMin = num !== null && num <= minDef; + const atMax = num !== null && num >= maxDef; + + const handleInputChange = useCallback( + (e: ChangeEvent) => { + const next = e.target.value; + if (next === '' || /^\d+$/.test(next)) { + onChange(e); + } + }, + [onChange], + ); + + const createChangeEvent = useCallback( + (newValue: string): ChangeEvent => + ({ + currentTarget: { value: newValue }, + target: { value: newValue }, + }) as ChangeEvent, + [], + ); + + const handleDecrement = useCallback(() => { + if (disabled || atMin) return; + const next = num !== null ? clamp(num - step, minDef, maxDef) : minDef; + onChange(createChangeEvent(String(next))); + }, [disabled, atMin, num, step, minDef, maxDef, onChange, createChangeEvent]); + + const handleIncrement = useCallback(() => { + if (disabled || atMax) return; + const next = num !== null ? clamp(num + step, minDef, maxDef) : minDef; + onChange(createChangeEvent(String(next))); + }, [disabled, atMax, num, step, minDef, maxDef, onChange, createChangeEvent]); + + const handleKeyDown = useCallback( + (e: KeyboardEvent) => { + if (e.key === 'ArrowDown') { + e.preventDefault(); + handleDecrement(); + } else if (e.key === 'ArrowUp') { + e.preventDefault(); + handleIncrement(); + } + }, + [handleDecrement, handleIncrement], + ); + + return ( +
+ {!!label && ( + + )} +
+ + + +
+
+ ); + }, +); diff --git a/src/components/Form/SwitchField.tsx b/src/components/Form/SwitchField.tsx index 5641bc30f..3c5f5ce1d 100644 --- a/src/components/Form/SwitchField.tsx +++ b/src/components/Form/SwitchField.tsx @@ -1,55 +1,165 @@ import clsx from 'clsx'; import type { ComponentProps, - ElementRef, KeyboardEventHandler, PropsWithChildren, + ReactNode, } from 'react'; import React, { useRef } from 'react'; -export type SwitchFieldProps = PropsWithChildren>; +export type SwitchFieldProps = Omit< + PropsWithChildren>, + 'children' +> & { + /** Main label content when title/description are not used */ + children?: ReactNode; + /** Optional description line below title */ + description?: string; + /** Class applied to the root div element of the SwitchField component */ + fieldClassName?: string; + /** Optional title line */ + title?: string; +}; + +export const SwitchField = ({ + children, + description, + fieldClassName, + title, + ...props +}: SwitchFieldProps) => { + const inputRef = useRef(null); -export const SwitchField = ({ children, ...props }: SwitchFieldProps) => { - const inputRef = useRef>(null); - const handleKeyUp: KeyboardEventHandler = (event) => { - if (![' ', 'Enter'].includes(event.key) || !inputRef.current) return; - event.preventDefault(); + const handleSwitchKeyDown: KeyboardEventHandler = (e) => { + if (![' ', 'Enter'].includes(e.key) || !inputRef.current) return; + e.preventDefault(); inputRef.current.click(); }; + const id = props.id; + return (
-
+ ); }; diff --git a/src/components/MessageInput/styling/MessageComposer.scss b/src/components/MessageInput/styling/MessageComposer.scss index 4261c41e3..65c8612d8 100644 --- a/src/components/MessageInput/styling/MessageComposer.scss +++ b/src/components/MessageInput/styling/MessageComposer.scss @@ -35,30 +35,24 @@ --str-chat__cooldown-border-inline-start: 0; --str-chat__cooldown-border-inline-end: 0; --str-chat__cooldown-box-shadow: none; + --str-chat__message-composer-max-width: 768px; .str-chat__message-composer-container { width: 100%; display: flex; + flex-direction: column; + align-items: center; + gap: var(--spacing-xs); justify-content: center; + padding: var(--spacing-xs); min-width: 0; } - /* - Styles for floating like composer - */ - .str-chat__message-composer--floating { - position: fixed; - bottom: 0; - background-color: var(--base-transparent-0); - // todo: variable exists only in Figma, not added to tokens repo - box-shadow: var(--shadow-web-light-elevation-2); - } .str-chat__message-composer { display: flex; align-items: end; width: 100%; - max-width: 768px; - padding: var(--spacing-xs); + max-width: var(--str-chat__message-composer-max-width); gap: var(--spacing-xs); min-width: 0; flex-shrink: 1; @@ -195,7 +189,9 @@ .str-chat__send-to-channel-checkbox__container { width: 100%; display: flex; - padding: 0.5rem 0.75rem; + align-items: flex-start; + padding-inline: 0.75rem; + max-width: var(--str-chat__message-composer-max-width); .str-chat__send-to-channel-checkbox__field { display: flex; diff --git a/src/components/MessageInput/styling/RemoveAttachmentPreviewButton.scss b/src/components/MessageInput/styling/RemoveAttachmentPreviewButton.scss index c6bd56367..cbd2673a1 100644 --- a/src/components/MessageInput/styling/RemoveAttachmentPreviewButton.scss +++ b/src/components/MessageInput/styling/RemoveAttachmentPreviewButton.scss @@ -8,4 +8,8 @@ border: 3px solid var(--str-chat__attachment-preview-close-icon-color); color: var(--str-chat__attachment-preview-close-icon-color); border-radius: var(--radius-max); + + svg { + color: var(--control-play-control-icon); + } } diff --git a/src/components/Modal/GlobalModal.tsx b/src/components/Modal/GlobalModal.tsx index 9b43e34f5..fbd8854b4 100644 --- a/src/components/Modal/GlobalModal.tsx +++ b/src/components/Modal/GlobalModal.tsx @@ -1,12 +1,9 @@ import clsx from 'clsx'; import type { PropsWithChildren } from 'react'; -import { useCallback } from 'react'; -import React, { useEffect, useRef } from 'react'; +import React, { useCallback, useEffect, useRef } from 'react'; import { FocusScope } from '@react-aria/focus'; -import { CloseIconRound } from './icons'; - -import { modalDialogManagerId, useTranslationContext } from '../../context'; +import { modalDialogManagerId } from '../../context'; import { DialogPortalEntry, modalDialogId, @@ -22,8 +19,6 @@ export const GlobalModal = ({ onCloseAttempt, open, }: PropsWithChildren) => { - const { t } = useTranslationContext('Modal'); - const dialog = useModalDialog(); const isOpen = useModalDialogIsOpen(); const innerRef = useRef(null); @@ -81,14 +76,6 @@ export const GlobalModal = ({ onClick={handleClick} > -
) => { - const { t } = useTranslationContext('Modal'); - const innerRef = useRef(null); const closeButtonRef = useRef(null); @@ -77,13 +70,6 @@ export const Modal = ({ onClick={handleClick} > -
)}
{title}
- {close && + )}
); diff --git a/src/components/Modal/__tests__/GlobalModal.test.js b/src/components/Modal/__tests__/GlobalModal.test.js index f916c1028..f45ddde76 100644 --- a/src/components/Modal/__tests__/GlobalModal.test.js +++ b/src/components/Modal/__tests__/GlobalModal.test.js @@ -6,7 +6,6 @@ import '@testing-library/jest-dom'; import { GlobalModal } from '../GlobalModal'; import { ModalDialogManagerProvider } from '../../../context'; -const CLOSE_BUTTON_SELECTOR = '.str-chat__modal__close-button'; const OVERLAY_SELECTOR = '.str-chat__modal'; const renderComponent = ({ props } = {}) => diff --git a/src/components/Modal/__tests__/Modal.test.js b/src/components/Modal/__tests__/Modal.test.js index 0188cf933..ef2286d2c 100644 --- a/src/components/Modal/__tests__/Modal.test.js +++ b/src/components/Modal/__tests__/Modal.test.js @@ -5,8 +5,6 @@ import '@testing-library/jest-dom'; import { Modal } from '../Modal'; -const CLOSE_BUTTON_SELECTOR = '.str-chat__modal__close-button'; - describe('Modal', () => { afterEach(cleanup); diff --git a/src/components/Modal/styling/Modal.scss b/src/components/Modal/styling/Modal.scss index ae46f6865..e64ee27e9 100644 --- a/src/components/Modal/styling/Modal.scss +++ b/src/components/Modal/styling/Modal.scss @@ -11,14 +11,29 @@ height: 100%; z-index: 100; + .str-chat__modal__inner { + @include utils.flex-col-center; + position: relative; + padding: var(--str-chat__spacing-8) var(--str-chat__spacing-4); + max-height: 80%; + min-width: 0; + min-height: 0; + } + .str-chat__modal-header { display: flex; align-items: center; + gap: var(--spacing-md); width: 100%; - padding: 1.25rem 1rem; + padding: var(--spacing-xl); + + .str-chat__modal-header__title { + flex: 1; + font: var(--str-chat__heading-sm-text); + color: var(--text-primary); + } - button.str-chat__modal-header__go-back-button, - .str-chat__modal__close-button { + button.str-chat__modal-header__go-back-button { padding: 1rem; background-size: 0.875rem; background-repeat: no-repeat; @@ -28,62 +43,21 @@ button.str-chat__modal-header__go-back-button { background-image: var(--str-chat__arrow-left-icon); } - - .str-chat__modal-header__close-button { - @include utils.button-reset; - cursor: pointer; - background-image: var(--str-chat__close-icon); - background-repeat: no-repeat; - height: 0.875rem; - width: 0.875rem; - } - - .str-chat__modal-header__title { - flex: 1; - } - } - - button.str-chat__modal__close-button { - @include utils.unset-button; - margin: var(--str-chat__spacing-2); - cursor: pointer; - } - - .str-chat__modal__close-button { - --str-chat-icon-height: calc(var(--str-chat__spacing-px) * 28); - @include utils.flex-row-center; - padding: var(--str-chat__spacing-2); - position: absolute; - inset-block-start: 0; - inset-inline-end: 0; - cursor: pointer; - } - - .str-chat__modal__inner { - @include utils.flex-col-center; - padding: var(--str-chat__spacing-8) var(--str-chat__spacing-4); - max-height: 80%; - min-width: 0; - min-height: 0; } } -.str-chat__modal--close { - display: none; -} - .str-chat { /* The border radius used for the borders of the content area of the component of the content area of the component */ - --str-chat__modal-border-radius: var(--str-chat__border-radius-sm); + --str-chat__modal-border-radius: var(--radius-xl); /* The text/icon color of the content area of the component */ --str-chat__modal-color: var(--str-chat__text-color); /* The background color of the content area of the component */ - --str-chat__modal-background-color: var(--str-chat__secondary-background-color); + --str-chat__modal-background-color: var(--background-elevation-elevation-1); /* The overlay color of the component */ - --str-chat__modal-overlay-color: var(--str-chat__secondary-overlay-color); + --str-chat__modal-overlay-color: var(--background-core-scrim); /* The backdrop filter applied to the component */ --str-chat__modal-overlay-backdrop-filter: blur(3px); @@ -101,8 +75,9 @@ --str-chat__modal-border-inline-end: none; /* Box shadow applied to the content area of the component */ - --str-chat__modal-box-shadow: none; + --str-chat__modal-box-shadow: var(--str-chat__box-shadow-elevation-1); + // todo: remove these two variables? /* The background color of the close button */ --str-chat__modal-close-icon-background: var(--str-chat__text-low-emphasis-color); @@ -117,31 +92,4 @@ .str-chat__modal__inner { @include utils.component-layer-overrides('modal'); } - - .str-chat__modal-header { - .str-chat__modal-header__title { - font: var(--str-chat__subtitle2-medium-text); - } - } - - .str-chat__modal__close-button { - --str-chat-icon-color: var(--str-chat__modal-close-icon-color); - @include utils.button-reset; - - .str-chat__icon { - background-color: var(--str-chat__modal-close-icon-background); - border-radius: 999px; - } - - svg { - path { - fill: var(--str-chat__modal-close-icon-color); - } - - rect, - circle { - fill: var(--str-chat__modal-close-icon-background); - } - } - } } diff --git a/src/components/Poll/PollActions/PollAction.tsx b/src/components/Poll/PollActions/PollAction.tsx index bd23f4635..290415fd4 100644 --- a/src/components/Poll/PollActions/PollAction.tsx +++ b/src/components/Poll/PollActions/PollAction.tsx @@ -2,12 +2,19 @@ import type { PropsWithChildren } from 'react'; import React from 'react'; import { Modal as DefaultModal } from '../../Modal'; import { useComponentContext } from '../../../context'; +import { Button } from '../../Button'; +import clsx from 'clsx'; export type PollActionProps = { buttonText: string; closeModal: () => void; modalIsOpen: boolean; openModal: () => void; + /** + * Additional actions are shown based on the poll settings defined by the creator. + * Examples are "Suggest an option", "Add a comment", "View N comment(s)". + */ + isAdditionalAction?: boolean; modalClassName?: string; }; @@ -15,6 +22,7 @@ export const PollAction = ({ buttonText, children, closeModal, + isAdditionalAction, modalClassName, modalIsOpen, openModal, @@ -22,9 +30,18 @@ export const PollAction = ({ const { Modal = DefaultModal } = useComponentContext(); return ( <> - + {children} diff --git a/src/components/Poll/PollActions/PollActions.tsx b/src/components/Poll/PollActions/PollActions.tsx index e6e79ddd8..3df4cb535 100644 --- a/src/components/Poll/PollActions/PollActions.tsx +++ b/src/components/Poll/PollActions/PollActions.tsx @@ -93,6 +93,28 @@ export const PollActions = ({ return (
+ {!is_closed && created_by_id === client.user?.id && ( + setModalOpen('end-vote')} + > + + + )} + + setModalOpen('view-results')} + > + + + {options.length > MAX_OPTIONS_DISPLAYED && ( setModalOpen('add-comment')} @@ -140,6 +164,7 @@ export const PollActions = ({ setModalOpen('view-comments')} @@ -150,28 +175,6 @@ export const PollActions = ({ /> )} - - setModalOpen('view-results')} - > - - - - {!is_closed && created_by_id === client.user?.id && ( - setModalOpen('end-vote')} - > - - - )}
); }; diff --git a/src/components/Poll/PollCreationDialog/MultipleAnswersField.tsx b/src/components/Poll/PollCreationDialog/MultipleAnswersField.tsx index 68667d42d..132320c90 100644 --- a/src/components/Poll/PollCreationDialog/MultipleAnswersField.tsx +++ b/src/components/Poll/PollCreationDialog/MultipleAnswersField.tsx @@ -1,7 +1,7 @@ import clsx from 'clsx'; -import React, { useMemo } from 'react'; -import { SimpleSwitchField } from '../../Form/SwitchField'; -import { FieldError } from '../../Form/FieldError'; +import React, { useMemo, useState } from 'react'; +import { NumericInput } from '../../Form/NumericInput'; +import { SwitchField, SwitchFieldLabel } from '../../Form/SwitchField'; import { useTranslationContext } from '../../../context'; import { useMessageComposer } from '../../MessageInput'; import { useStateStore } from '../../../store'; @@ -20,6 +20,7 @@ export const MultipleAnswersField = () => { pollComposer.state, pollComposerStateSelector, ); + const [voteLimitEnabled, setVoteLimitEnabled] = useState(false); const knownValidationErrors = useMemo>( () => ({ @@ -29,58 +30,65 @@ export const MultipleAnswersField = () => { [t], ); + const multipleVotesEnabled = !enforce_unique_vote; + const errorText = error && knownValidationErrors[error]; + return ( -
- + { + setVoteLimitEnabled(false); pollComposer.updateFields({ enforce_unique_vote: !e.target.checked }); }} + title={t('Multiple votes')} /> - {!enforce_unique_vote && ( -
{ + setVoteLimitEnabled((prev) => !prev); + pollComposer.updateFields({ max_votes_allowed: '2' }); + }} > -
- - { - pollComposer.handleFieldBlur('max_votes_allowed'); - }} - onChange={(e) => { - const nativeFieldValidation = !e.target.validity.valid - ? { - max_votes_allowed: t('Only numbers are allowed'), - } - : undefined; - pollComposer.updateFields( - { - max_votes_allowed: !nativeFieldValidation - ? e.target.value - : pollComposer.max_votes_allowed, - }, - nativeFieldValidation, - ); - }} - placeholder={t('Maximum number of votes (from 2 to 10)')} - type='text' - value={max_votes_allowed} +
+ + {voteLimitEnabled && ( + { + pollComposer.handleFieldBlur('max_votes_allowed'); + }} + onChange={(e) => { + const raw = e.target.value; + const nativeFieldValidation = + raw !== '' && !/^\d+$/.test(raw) + ? { max_votes_allowed: t('Only numbers are allowed') } + : undefined; + pollComposer.updateFields( + { + max_votes_allowed: nativeFieldValidation + ? pollComposer.max_votes_allowed + : raw, + }, + nativeFieldValidation, + ); + }} + value={max_votes_allowed ?? ''} + /> + )}
-
+ )}
); diff --git a/src/components/Poll/PollCreationDialog/NameField.tsx b/src/components/Poll/PollCreationDialog/NameField.tsx index ac9cfc0a2..36ea3a564 100644 --- a/src/components/Poll/PollCreationDialog/NameField.tsx +++ b/src/components/Poll/PollCreationDialog/NameField.tsx @@ -1,6 +1,5 @@ import React, { useMemo } from 'react'; -import clsx from 'clsx'; -import { FieldError } from '../../Form/FieldError'; +import { TextInput } from '../../Form'; import { useTranslationContext } from '../../../context'; import { useMessageComposer } from '../../MessageInput'; import { useStateStore } from '../../../store'; @@ -23,36 +22,36 @@ export const NameField = () => { ); return ( -
- -
- - { - pollComposer.handleFieldBlur('name'); - }} - onChange={(e) => { - pollComposer.updateFields({ name: e.target.value }); - }} - placeholder={t('Ask a question')} - type='text' - value={name} - /> -
-
+ //
+ + {knownValidationErrors[error] ?? t('Error')} + + ) : undefined + } + id='name' + label={t('Question')} + onBlur={() => { + pollComposer.handleFieldBlur('name'); + }} + onChange={(e) => { + pollComposer.updateFields({ name: e.target.value }); + }} + placeholder={t('Ask a question')} + type='text' + value={name} + /> + //
); }; diff --git a/src/components/Poll/PollCreationDialog/OptionFieldSet.tsx b/src/components/Poll/PollCreationDialog/OptionFieldSet.tsx index 9c50cf3d8..afc0b56da 100644 --- a/src/components/Poll/PollCreationDialog/OptionFieldSet.tsx +++ b/src/components/Poll/PollCreationDialog/OptionFieldSet.tsx @@ -1,11 +1,13 @@ import clsx from 'clsx'; import React, { useCallback, useMemo } from 'react'; -import { FieldError } from '../../Form/FieldError'; -import { DragAndDropContainer } from '../../DragAndDrop/DragAndDropContainer'; +import { TextInput } from '../../Form/TextInput'; import { useTranslationContext } from '../../../context'; import { useMessageComposer } from '../../MessageInput'; import { useStateStore } from '../../../store'; import type { PollComposerState } from 'stream-chat'; +import { IconCircleMinus, IconDotGrid2x3 } from '../../Icons'; +import { Button, type ButtonProps } from '../../Button'; +import { TextInputFieldSet } from '../../Form/TextInputFieldSet'; const pollComposerStateSelector = (state: PollComposerState) => ({ errors: state.errors.options, @@ -36,59 +38,91 @@ export const OptionFieldSet = () => { [pollComposer], ); + const clearOption = useCallback( + (removedOptionId: string) => { + pollComposer.updateFields({ + options: pollComposer.options.filter((option) => option.id !== removedOptionId), + }); + }, + [pollComposer], + ); + const draggable = options.length > 1; return ( -
- {t('Options')} - - {options.map((option, i) => { - const error = errors?.[option.id]; - return ( -
-
- - { - pollComposer.handleFieldBlur('options'); - }} - onChange={(e) => { - pollComposer.updateFields({ - options: { index: i, text: e.target.value }, - }); - }} - onKeyUp={(event) => { - const isFocusedLastOptionField = i === options.length - 1; - if (event.key === 'Enter' && !isFocusedLastOptionField) { - const nextInputId = options[i + 1].id; - document.getElementById(nextInputId)?.focus(); - } - }} - placeholder={t('Add an option')} - type='text' - value={option.text} - /> -
- {draggable &&
} -
- ); - })} - -
+ + {options.map((option, i) => { + const error = errors?.[option.id]; + return ( +
+ + ) : undefined + } + message={ + error ? ( + + {knownValidationErrors[error] ?? t('Error')} + + ) : undefined + } + onBlur={() => { + pollComposer.handleFieldBlur('options'); + }} + onChange={(e) => { + pollComposer.updateFields({ + options: { index: i, text: e.target.value }, + }); + }} + onKeyUp={(event) => { + const isFocusedLastOptionField = i === options.length - 1; + if (event.key === 'Enter' && !isFocusedLastOptionField) { + const nextInputId = options[i + 1].id; + document.getElementById(nextInputId)?.focus(); + } + }} + placeholder={t('Add an option')} + trailing={ + option.text ? ( + clearOption(option.id)} /> + ) : undefined + } + type='text' + value={option.text} + /> +
+ ); + })} +
); }; + +const RemoveOptionButton = ({ className, ...props }: ButtonProps) => ( + +); diff --git a/src/components/Poll/PollCreationDialog/PollCreationDialog.tsx b/src/components/Poll/PollCreationDialog/PollCreationDialog.tsx index d089a1805..577f3ad9e 100644 --- a/src/components/Poll/PollCreationDialog/PollCreationDialog.tsx +++ b/src/components/Poll/PollCreationDialog/PollCreationDialog.tsx @@ -6,7 +6,7 @@ import { NameField } from './NameField'; import { OptionFieldSet } from './OptionFieldSet'; import { PollCreationDialogControls } from './PollCreationDialogControls'; import { ModalHeader } from '../../Modal/ModalHeader'; -import { SimpleSwitchField } from '../../Form/SwitchField'; +import { SwitchField } from '../../Form/SwitchField'; import { useMessageComposer } from '../../MessageInput'; import { useTranslationContext } from '../../../context'; import { useStateStore } from '../../../store'; @@ -42,37 +42,42 @@ export const PollCreationDialog = ({ close }: PollCreationDialogProps) => {
- - - pollComposer.updateFields({ - voting_visibility: e.target.checked - ? VotingVisibility.anonymous - : VotingVisibility.public, - }) - } - /> - - pollComposer.updateFields({ - allow_user_suggested_options: e.target.checked, - }) - } - /> - - pollComposer.updateFields({ allow_answers: e.target.checked }) - } - /> +
+ + + pollComposer.updateFields({ + voting_visibility: e.target.checked + ? VotingVisibility.anonymous + : VotingVisibility.public, + }) + } + title={t('Anonymous poll')} + /> + + pollComposer.updateFields({ + allow_user_suggested_options: e.target.checked, + }) + } + title={t('Suggest an option')} + /> + + pollComposer.updateFields({ allow_answers: e.target.checked }) + } + title={t('Add a comment')} + /> +
diff --git a/src/components/Poll/PollCreationDialog/PollCreationDialogControls.tsx b/src/components/Poll/PollCreationDialog/PollCreationDialogControls.tsx index 458251461..f4b67a628 100644 --- a/src/components/Poll/PollCreationDialog/PollCreationDialogControls.tsx +++ b/src/components/Poll/PollCreationDialog/PollCreationDialogControls.tsx @@ -1,6 +1,9 @@ import React from 'react'; import { useCanCreatePoll, useMessageComposer } from '../../MessageInput'; import { useMessageInputContext, useTranslationContext } from '../../../context'; +import { Button } from '../../Button'; +import clsx from 'clsx'; +import { IconPaperPlane } from '../../Icons'; export type PollCreationDialogControlsProps = { close: () => void; @@ -16,8 +19,13 @@ export const PollCreationDialogControls = ({ return (
- - + + {t('Send poll')} +
); }; diff --git a/src/components/Poll/PollOptionSelector.tsx b/src/components/Poll/PollOptionSelector.tsx index cdce442ed..15545da9b 100644 --- a/src/components/Poll/PollOptionSelector.tsx +++ b/src/components/Poll/PollOptionSelector.tsx @@ -1,16 +1,17 @@ import clsx from 'clsx'; import debounce from 'lodash.debounce'; import React, { useMemo } from 'react'; +import type { PollOption, PollState, PollVote, VotingVisibility } from 'stream-chat'; import { isVoteAnswer } from 'stream-chat'; -import { Avatar } from '../Avatar'; +import { AvatarStack as DefaultAvatarStack } from '../Avatar'; import { useChannelStateContext, + useComponentContext, useMessageContext, usePollContext, useTranslationContext, } from '../../context'; import { useStateStore } from '../../store'; -import type { PollOption, PollState, PollVote, VotingVisibility } from 'stream-chat'; type AmountBarProps = { amount: number; @@ -69,7 +70,7 @@ export const PollOptionSelector = ({ const { t } = useTranslationContext(); const { channelCapabilities = {} } = useChannelStateContext('PollOptionsShortlist'); const { message } = useMessageContext(); - + const { AvatarStack = DefaultAvatarStack } = useComponentContext(); const { poll } = usePollContext(); const { is_closed, @@ -79,6 +80,7 @@ export const PollOptionSelector = ({ vote_counts_by_option, voting_visibility, } = useStateStore(poll.state, pollStateSelector); + const canCastVote = channelCapabilities['cast-poll-vote'] && !is_closed; const winningOptionCount = maxVotedOptionIds[0] ? vote_counts_by_option[maxVotedOptionIds[0]] @@ -96,6 +98,20 @@ export const PollOptionSelector = ({ [canCastVote, message.id, option.id, ownVotesByOptionId, poll], ); + const avatarDisplayInfo = useMemo( + () => + latest_votes_by_option?.[option.id] && + (latest_votes_by_option[option.id] as PollVote[]) + .filter((vote) => !!vote.user && !isVoteAnswer(vote)) + .slice(0, displayAvatarCount) + .map(({ user }) => ({ + id: user!.id, // eslint-disable-line @typescript-eslint/no-non-null-assertion + imageUrl: user!.image, // eslint-disable-line @typescript-eslint/no-non-null-assertion + userName: user!.name, // eslint-disable-line @typescript-eslint/no-non-null-assertion + })), + [displayAvatarCount, latest_votes_by_option, option.id], + ); + return (
{option.text}

{displayAvatarCount && voting_visibility === 'public' && (
- {latest_votes_by_option?.[option.id] && - (latest_votes_by_option[option.id] as PollVote[]) - .filter((vote) => !!vote.user && !isVoteAnswer(vote)) - .slice(0, displayAvatarCount) - .map(({ user }) => ( - - ))} +
)}
diff --git a/src/components/Poll/styling/Poll.scss b/src/components/Poll/styling/Poll.scss new file mode 100644 index 000000000..186c83118 --- /dev/null +++ b/src/components/Poll/styling/Poll.scss @@ -0,0 +1,522 @@ +@use '../../../styling/utils'; + +.str-chat { + .str-chat__poll { + $checkmark_size: 24px; + display: flex; + flex-direction: column; + gap: var(--spacing-xl); + padding: var(--spacing-xs); + min-width: 260px; + max-width: 400px; + font-size: var(--typography-font-size-sm); + font-weight: var(--typography-font-weight-regular); + line-height: var(--typography-line-height-tight); + + button { + @include utils.button-reset; + cursor: pointer; + } + + .str-chat__checkmark { + grid-column: 1; + grid-row: span 2; + align-self: center; + height: $checkmark_size; + width: $checkmark_size; + } + + .str-chat__checkmark--checked { + height: calc($checkmark_size + 1px); + width: calc($checkmark_size + 1px); + background-image: url('data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIGZpbGw9Im5vbmUiPjxwYXRoIGZpbGw9IiNmZmYiIGQ9Im00IDkuNC00LTRMMS40IDQgNCA2LjYgMTAuNiAwIDEyIDEuNGwtOCA4WiIvPjwvc3ZnPg=='); + background-repeat: no-repeat; + background-position: center; + background-size: 11px 10px; + } + + .str-chat__poll-header { + .str-chat__poll-title { + font-size: var(--typography-font-size-md); + font-weight: var(--typography-font-weight-semi-bold); + line-height: var(--typography-line-height-normal); + } + } + + .str-chat__poll-actions { + display: flex; + flex-direction: column; + align-items: center; + gap: var(--spacing-xs); + } + } + + .str-chat__button.str-chat__poll-action { + width: 100%; + color: var(--str-chat__primary-color); + border: 1px solid var(--chat-border-on-chat-incoming); + font-size: var(--typography-font-size-sm); + padding: var(--button-padding-y-sm) var(--button-padding-x-with-label-sm); + + &.str-chat__poll-action--additional { + border: none; + } + } + + .str-chat__poll-results-modal, + .str-chat__poll-answer-list-modal, + .str-chat__add-poll-answer-modal, + .str-chat__suggest-poll-option-modal, + .str-chat__poll-options-modal { + button { + @include utils.button-reset; + cursor: pointer; + } + } + + .str-chat__poll-option-list--full, + .str-chat__modal__poll-results { + .str-chat__amount-bar { + display: none; + } + } + + .str-chat__poll-option { + cursor: pointer; + + &.str-chat__poll-option--full-vote-list { + cursor: default; + height: 100%; + padding: 0; + } + + .str-chat__poll-option-data { + flex: 1; + display: flex; + align-items: center; + gap: var(--spacing-sm); + + p { + margin: 0; + flex: 1; + } + + .str-chat__poll-option-voters { + --str-chat__avatar-size: 1.175rem; + display: flex; + } + + .str-chat__poll-option-vote-count { + font-size: var(--typography-font-size-xs); + line-height: var(--typography-line-height-tight); + } + } + } + + .str-chat__poll-option-list--full { + .str-chat__poll-option { + display: flex; + flex-direction: row; + padding: 1rem 0.75rem; + + &:nth-of-type(1) { + padding-top: 1rem; + border-top-left-radius: var(--str-chat__border-radius-sm); + border-top-right-radius: var(--str-chat__border-radius-sm); + } + + &:last-child { + padding-bottom: 1rem; + border-bottom-left-radius: var(--str-chat__border-radius-sm); + border-bottom-right-radius: var(--str-chat__border-radius-sm); + } + } + } + + .str-chat__poll-option-list:not(.str-chat__poll-option-list--full) { + display: flex; + flex-direction: column; + gap: var(--spacing-md); + + .str-chat__poll-option { + display: grid; + grid-template-columns: minmax(0, auto) 1fr; + grid-template-rows: 1fr auto; + row-gap: var(--spacing-xs); + + &.str-chat__poll-option--votable { + column-gap: var(--spacing-sm); + } + + .str-chat__poll-option-data { + grid-column: 2 / 3; + grid-row: 1 / 2; + } + + .str-chat__poll-option__votes-bar { + grid-column: 2 / 3; + grid-row: 2 / 3; + height: 8px; + width: 100%; + } + } + } + + .str-chat__message--me { + .str-chat__poll { + .str-chat__poll-option-list:not(.str-chat__poll-option-list--full) { + .str-chat__poll-option__votes-bar { + background: linear-gradient( + to right, + var(--chat-poll-progress-fill-outgoing) + var(--str-chat__amount-bar-fulfillment), + var(--chat-poll-progress-track-outgoing) + var(--str-chat__amount-bar-fulfillment) + ); + } + + .str-chat__poll-option__votes-bar--winner { + background: linear-gradient( + to right, + var(--chat-poll-progress-fill-outgoing) + var(--str-chat__amount-bar-fulfillment), + var(--chat-poll-progress-track-outgoing) + var(--str-chat__amount-bar-fulfillment) + ); + } + } + + .str-chat__poll-actions { + .str-chat__poll-action { + border: 1px solid var(--chat-border-on-chat-outgoing); + + &.str-chat__poll-action--additional { + border: none; + } + } + } + } + } + + .str-chat__modal__poll-results { + .str-chat__poll-option { + display: flex; + flex-direction: column; + } + } + + .str-chat-react__modal.str-chat__poll-action-modal, + .str-chat__poll-actions .str-chat__modal { + .str-chat__modal__inner { + $content-offset-inline: 1rem; + padding: 0 0 0.5rem; + overflow: hidden; + max-width: 400px; + + .str-chat__tooltip { + max-width: 300px; + } + + .str-chat__modal__suggest-poll-option { + .str-chat__form-field-error { + height: 1rem; + } + + .str-chat__dialog__controls { + padding-bottom: 0; + } + } + + .str-chat__modal__poll-answer-list, + .str-chat__modal__poll-option-list, + .str-chat__modal__poll-results { + display: flex; + flex-direction: column; + width: 100%; + height: 100%; + min-height: 400px; + } + + .str-chat__modal__poll-answer-list, + .str-chat__poll-option--full-vote-list { + .str-chat__loading-indicator-placeholder { + display: flex; + justify-content: center; + align-items: center; + width: 100%; + height: 40px; + } + } + + .str-chat__modal__poll-option-list__title, + .str-chat__modal__poll-results__title { + padding: 1.175rem 1rem; + } + + .str-chat__modal__poll-answer-list__body, + .str-chat__modal__poll-results__body { + display: flex; + flex-direction: column; + min-height: 0; + padding-bottom: 1rem; + } + + .str-chat__modal__poll-results__body, + .str-chat__modal__poll-option-list__body, + .str-chat__poll-answer-list, + .str-chat__modal__poll-results__option-list { + display: flex; + flex-direction: column; + flex: 1; + max-height: 100%; + min-height: 0; + } + + .str-chat__poll-answer-list { + padding-bottom: 0; + } + + .str-chat__modal__poll-results__body, + .str-chat__modal__poll-option-list__body, + .str-chat__poll-answer-list { + overflow-y: auto; + padding: 0 $content-offset-inline 1.25rem; + } + + .str-chat__poll-answer-list, + .str-chat__modal__poll-results__option-list { + gap: 0.5rem; + } + + .str-chat__modal__poll-results__body, + .str-chat__modal__poll-option-list__body { + gap: 2rem; + } + + .str-chat__poll-option__show-all-votes-button { + padding-bottom: 1rem; + } + + .str-chat__poll-answer { + display: flex; + flex-direction: column; + gap: 1rem; + padding: 0.75rem 1rem; + + .str-chat__poll-answer__text { + margin: 0; + } + } + + .str-chat__checkmark { + margin-right: 1rem; + } + + .str-chat__poll-option__header { + display: flex; + align-items: flex-start; + gap: 0.25rem; + width: 100%; + padding: 0.75rem 1rem; + + .str-chat__poll-option__option-text { + flex: 1; + } + } + + .str-chat__poll-vote { + display: flex; + justify-content: space-between; + align-items: center; + gap: 0.5rem; + white-space: nowrap; + padding-block: 0.375rem; + + .str-chat__poll-vote__author { + display: flex; + align-items: center; + gap: calc(var(--str-chat__spacing-px) * 5); + min-width: 0; + + .str-chat__poll-vote__author__name { + @include utils.ellipsis-text; + max-width: 130px; + min-width: 0; + } + } + } + + .str-chat__poll-result-option-vote-counter { + display: flex; + gap: 0.375rem; + + .str-chat__poll-result-winning-option-icon { + height: 1.25rem; + width: 1.25rem; + background-image: var(--str-chat__winning-poll-option-icon); + } + } + } + } + + .str-chat__poll-vote-listing { + padding: 0 1rem 0.75rem; + } + + .str-chat__modal__poll-results--option-detail { + .str-chat__modal-header__title { + padding-inline: 1rem; + flex: 1; + } + + .str-chat__modal__poll-results__body { + padding-inline: 1rem; + } + } + + .str-chat__quoted-poll-preview { + display: flex; + flex-wrap: wrap; + gap: 0.5rem; + align-items: flex-start; + } + + .str-chat__modal.str-chat__create-poll-modal { + .str-chat__modal__inner { + padding: 0; + max-height: unset; + display: block; + } + } + + .str-chat__poll { + .str-chat__checkmark { + border-radius: var(--str-chat__border-radius-circle); + border: 1px solid var(--str-chat__disabled-color); + } + + .str-chat__checkmark--checked { + background-color: var(--str-chat__primary-color); + border: none; + } + + .str-chat__poll-option-list { + .str-chat__poll-option { + &.str-chat__poll-option--votable:hover { + cursor: pointer; + } + + .str-chat__poll-option__votes-bar { + background: linear-gradient( + to right, + var(--chat-poll-progress-fill-incoming) + var(--str-chat__amount-bar-fulfillment), + var(--chat-poll-progress-track-incoming) + var(--str-chat__amount-bar-fulfillment) + ); + border-radius: calc(var(--str-chat__spacing-px) * 4); + } + + .str-chat__poll-option__votes-bar--winner { + background: linear-gradient( + to right, + var(--chat-poll-progress-fill-incoming) + var(--str-chat__amount-bar-fulfillment), + var(--str-chat__surface-color) var(--str-chat__amount-bar-fulfillment) + ); + } + } + } + } + + .str-chat__poll-option-list--full, + .str-chat__poll-answer, + .str-chat__modal__poll-option-list__title, + .str-chat__modal__poll-results .str-chat__modal__poll-results__title, + .str-chat__modal__poll-results .str-chat__poll-option { + border-radius: 0.75rem; + } + + .str-chat__poll-option-list--full { + overflow: clip; + } + + .str-chat__poll--closed { + .str-chat__poll-option { + &:hover { + cursor: unset; + } + } + } + + .str-chat__modal, + .str-chat__poll-actions .str-chat__modal { + .str-chat__poll-answer__text, + .str-chat__modal__poll-option-list__title, + .str-chat__modal__poll-results__title { + font: var(--str-chat__subtitle-medium-text); + } + + .str-chat__poll-option-list--full, + .str-chat__poll-answer, + .str-chat__modal__poll-option-list__title, + .str-chat__modal__poll-results__title, + .str-chat__poll-option { + background-color: var(--str-chat__tertiary-surface-color); + } + + .str-chat__poll-option-list--full { + .str-chat__poll-option-text { + font-weight: 500; + } + + .str-chat__poll-option--votable:hover { + background-color: var(--str-chat__secondary-surface-color); + } + } + + .str-chat__poll-option { + .str-chat__poll-option__header { + font: var(--str-chat__subtitle-text); + + .str-chat__poll-option__option-text { + font: var(--str-chat__subtitle-medium-text); + } + } + + .str-chat__poll-option__show-all-votes-button { + font: var(--str-chat__subtitle-text); + color: var(--str-chat__primary-color); + } + } + } + + .str-chat__poll-vote { + .str-chat__poll-vote__author__name { + text-transform: capitalize; + } + + .str-chat__poll-vote__timestamp { + color: var(--str-chat__text-low-emphasis-color); + } + } + + .str-chat__quoted-poll-preview { + font: var(--str-chat__body-medium-text); + } + + @media only screen and (max-device-width: 768px) { + .str-chat__modal--open .str-chat__modal__inner { + width: 90%; + } + + .str-chat__create-poll-modal, + .str-chat__poll-answer-list-modal, + .str-chat__poll-results-modal { + .str-chat__modal__inner { + height: 90%; + max-height: unset; + } + } + } +} diff --git a/src/components/Poll/styling/PollCreationDialog.scss b/src/components/Poll/styling/PollCreationDialog.scss new file mode 100644 index 000000000..f2ed0e4a0 --- /dev/null +++ b/src/components/Poll/styling/PollCreationDialog.scss @@ -0,0 +1,101 @@ +@use '../../../styling/utils'; + +.str-chat__poll-creation-dialog { + + .str-chat__dialog__body { + flex: 1 1; + + form { + display: flex; + flex-direction: column; + gap: var(--spacing-xl); + } + } + + .str-chat__form__input-field__value input, + .str-chat__form__input-field__value.str-chat__form-text-input .str-chat__form-text-input__wrapper { + width: 100%; + } + + .str-chat__form__input-field__value.str-chat__form-text-input .str-chat__form-text-input__wrapper { + background-color: transparent; + } + + .str-chat__form__switch-field__label { + width: 100%; + } + + .str-chat__multiple-answers-field__votes-limit-field { + padding-top: 0; + + .str-chat__multiple-answers-field__votes-limit-field__numeric-field { + display: flex; + align-items: center; + gap: var(--spacing-md); + width: 100%; + + .str-chat__form__switch-field__label { + flex: 1; + } + } + } +} + +.str-chat__poll-creation-dialog { + display: flex; + flex-direction: column; + width: min(480px, 100vw); + height: min(640px, 100vh); + + .str-chat__form__input-field--has-error:not(:has(.str-chat__form-text-input)) { + border: 1px solid var(--str-chat__message-error-message-color); + } + + .str-chat__form__expandable-field .str-chat__form__input-field--has-error, + .str-chat__form__switch-field__expanded-content .str-chat__form__input-field--has-error { + border: none; + + .str-chat__form__input-field__value { + border: none; + } + } + + .str-chat__poll-creation-dialog__features-selectors { + display: flex; + flex-direction: column; + gap: var(--spacing-sm); + align-items: center; + } + + .str-chat__form__expandable-field { + padding: 1rem; + display: flex; + flex-direction: column; + gap: 1rem; + + .str-chat__form__input-field { + width: 100%; + + .str-chat__form__input-field__value { + padding: 0; + + .str-chat__form-field-error { + height: 1rem; + } + } + } + } + + /* Inline expanded content (same row as switch): natural width */ + .str-chat__form__switch-field__expanded-content .str-chat__form__input-field { + width: auto; + + .str-chat__form__input-field__value { + padding: 0; + + .str-chat__form-field-error { + height: 1rem; + } + } + } +} diff --git a/src/components/Poll/styling/index.scss b/src/components/Poll/styling/index.scss new file mode 100644 index 000000000..d73bc5f19 --- /dev/null +++ b/src/components/Poll/styling/index.scss @@ -0,0 +1,2 @@ +@use 'Poll'; +@use 'PollCreationDialog'; diff --git a/src/components/Thread/styling/Thread.scss b/src/components/Thread/styling/Thread.scss new file mode 100644 index 000000000..54ab1fcca --- /dev/null +++ b/src/components/Thread/styling/Thread.scss @@ -0,0 +1,149 @@ +@use '../../../styling/utils'; + +// FIXME: figure out why does this "container" exist when __thread is completely valid +.str-chat__thread-container { + position: relative; + height: 100%; + display: flex; + flex-direction: column; + width: 100%; + + .str-chat__thread-header { + @include utils.header-layout; + + .str-chat__thread-header-details { + @include utils.header-text-layout; + + .str-chat__thread-header-name, + .str-chat__thread-header-reply-count, + .str-chat__thread-header-channel-name, + .str-chat__thread-header-subtitle, + .str-chat__thread-header-title { + @include utils.ellipsis-text; + } + + .str-chat__thread-header-subtitle { + @include utils.prevent-glitch-text-overflow; + } + } + + .str-chat__close-thread-button { + $icon-size: calc(var(--str-chat__spacing-px) * 21); + display: flex; + align-items: flex-start; + justify-content: flex-end; + width: calc(var(--str-chat__spacing-px) * 40); + height: calc(var(--str-chat__spacing-px) * 40); + cursor: pointer; + line-height: $icon-size; + font-size: $icon-size; + + svg { + height: $icon-size; + width: $icon-size; + } + } + } +} + +.str-chat__thread { + .str-chat__main-panel-inner { + height: auto; + } +} + +.str-chat__thread--virtualized { + .str-chat__main-panel-inner { + height: 100%; + + // the first message in virtualized thread has to be separated from the top by padding, not margin + .str-chat__virtual-list-message-wrapper:first-of-type { + padding-block-start: var(--str-chat__spacing-4); + } + } +} + +.str-chat__parent-message-li { + padding: var(--str-chat__spacing-2); +} + +.str-chat { + /* The border radius used for the borders of the component */ + --str-chat__thread-border-radius: 0; + + /* The text/icon color of the component */ + --str-chat__thread-color: var(--str-chat__text-color); + + /* The background color of the component */ + --str-chat__thread-background-color: var(--str-chat__secondary-background-color); + + /* Top border of the component */ + --str-chat__thread-border-block-start: none; + + /* Bottom border of the component */ + --str-chat__thread-border-block-end: none; + + /* Left (right in RTL layout) border of the component */ + --str-chat__thread-border-inline-start: 1px solid var(--str-chat__surface-color); + + /* Right (left in RTL layout) border of the component */ + --str-chat__thread-border-inline-end: none; + + /* Box shadow applied to the component */ + --str-chat__thread-box-shadow: none; + + /* The border radius used for the borders of the thread header */ + --str-chat__thread-header-border-radius: 0; + + /* The text/icon color of the thread header */ + --str-chat__thread-header-color: var(--str-chat__text-color); + + /* The background color of the thread header */ + --str-chat__thread-header-background-color: var(--str-chat__secondary-background-color); + + /* Top border of the thread header */ + --str-chat__thread-header-border-block-start: none; + + /* Bottom border of the thread header */ + --str-chat__thread-header-border-block-end: none; + + /* Left (right in RTL layout) border of the thread header */ + --str-chat__thread-header-border-inline-start: none; + + /* Right (left in RTL layout) border of the thread header */ + --str-chat__thread-header-border-inline-end: none; + + /* Box shadow applied to the thread header */ + --str-chat__thread-header-box-shadow: none; + + /* The text/icon color used to display less emphasized text in the channel header */ + --str-chat__thread-header-info-color: var(--str-chat__text-low-emphasis-color); +} + +.str-chat__thread-container { + @include utils.component-layer-overrides('thread'); + + .str-chat__thread-header { + @include utils.component-layer-overrides('thread-header'); + + .str-chat__thread-header-name, + .str-chat__thread-header-title { + font: var(--str-chat__subtitle-medium-text); + } + + .str-chat__thread-header-channel-name, + .str-chat__thread-header-subtitle { + font: var(--str-chat__body-text); + color: var(--str-chat__thread-header-info-color); + } + + .str-chat__close-thread-button { + background-color: transparent; + border: none; + + svg path { + fill: var(--str-chat__thread-color); + } + } + } +} diff --git a/src/components/Thread/styling/index.scss b/src/components/Thread/styling/index.scss new file mode 100644 index 000000000..99a8b1908 --- /dev/null +++ b/src/components/Thread/styling/index.scss @@ -0,0 +1 @@ +@use 'Thread'; diff --git a/src/components/index.ts b/src/components/index.ts index b1d878253..e8605f942 100644 --- a/src/components/index.ts +++ b/src/components/index.ts @@ -15,6 +15,7 @@ export * from './Dialog'; export * from './EmptyStateIndicator'; export * from './EventComponent'; export * from './FileIcon'; +export * from './Form'; export * from './Gallery'; export * from './Icons'; export * from './InfiniteScrollPaginator'; diff --git a/src/i18n/de.json b/src/i18n/de.json index 5da8de346..b93dfa39e 100644 --- a/src/i18n/de.json +++ b/src/i18n/de.json @@ -32,6 +32,7 @@ "Allow access to microphone": "Zugriff auf Mikrofon erlauben", "Allow comments": "Kommentare erlauben", "Allow option suggestion": "Optionsvorschläge erlauben", + "Allow voters to add comments": "Abstimmenden das Hinzufügen von Kommentaren erlauben", "Also send as a direct message": "Auch als Direktnachricht senden", "Also send in channel": "Auch im Kanal senden", "An error has occurred during recording": "Ein Fehler ist während der Aufnahme aufgetreten", @@ -80,6 +81,7 @@ "Cannot seek in the recording": "In der Aufnahme kann nicht gesucht werden", "Channel Missing": "Kanal fehlt", "Channels": "Kanäle", + "Choose between 2 to 10 options": "Wähle zwischen 2 und 10 Optionen", "Close": "Schließen", "Close emoji picker": "Emoji-Auswahl schließen", "Commands": "Befehle", @@ -138,8 +140,11 @@ "Generating...": "Generieren...", "giphy-command-args": "[Text]", "giphy-command-description": "Poste ein zufälliges Gif in den Kanal", + "Hide who voted": "Verbergen, wer abgestimmt hat", "Instant commands": "Instant commands", "Latest Messages": "Neueste Nachrichten", + "Let others add options": "Andere Optionen hinzufügen lassen", + "Limit votes per person": "Stimmen pro Person begrenzen", "live": "live", "Live for {{duration}}": "Live für {{duration}}", "Live location": "Live-Standort", @@ -158,7 +163,7 @@ "Message was blocked by moderation policies": "Nachricht wurde durch moderationsrichtlinien blockiert", "Messages have been marked unread.": "Nachrichten wurden als ungelesen markiert.", "Missing permissions to upload the attachment": "Fehlende Berechtigungen zum Hochladen des Anhangs", - "Multiple answers": "Mehrere Antworten", + "Multiple votes": "Mehrfachstimmen", "Mute": "Stummschalten", "mute-command-args": "[@Benutzername]", "mute-command-description": "Stummschalten eines Benutzers", @@ -215,6 +220,7 @@ "searchResultsCount_other": "{{ count }} Ergebnisse", "See all options ({{count}})_one": "Alle Optionen anzeigen ({{count}})", "See all options ({{count}})_other": "Alle Optionen anzeigen ({{count}})", + "Select more than one option": "Mehr als eine Option auswählen", "Select one": "Eine auswählen", "Select one or more": "Eine oder mehrere auswählen", "Select up to {{count}}_one": "Bis zu {{count}} auswählen", @@ -222,6 +228,7 @@ "Send": "Senden", "Send Anyway": "Trotzdem senden", "Send message request failed": "Senden der Nachrichtenanfrage fehlgeschlagen", + "Send poll": "Umfrage senden", "Sending...": "Senden...", "Sent": "Gesendet", "Share": "Teilen", diff --git a/src/i18n/en.json b/src/i18n/en.json index 97b4b28c5..4bc95a417 100644 --- a/src/i18n/en.json +++ b/src/i18n/en.json @@ -32,6 +32,7 @@ "Allow access to microphone": "Allow access to microphone", "Allow comments": "Allow comments", "Allow option suggestion": "Allow option suggestion", + "Allow voters to add comments": "Allow voters to add comments", "Also send as a direct message": "Also send as a direct message", "Also send in channel": "Also send in channel", "An error has occurred during recording": "An error has occurred during recording", @@ -80,6 +81,7 @@ "Cannot seek in the recording": "Cannot seek in the recording", "Channel Missing": "Channel Missing", "Channels": "Channels", + "Choose between 2 to 10 options": "Choose between 2 to 10 options", "Close": "Close", "Close emoji picker": "Close emoji picker", "Commands": "Commands", @@ -138,8 +140,11 @@ "Generating...": "Generating...", "giphy-command-args": "[text]", "giphy-command-description": "Post a random gif to the channel", + "Hide who voted": "Hide who voted", "Instant commands": "Instant commands", "Latest Messages": "Latest Messages", + "Let others add options": "Let others add options", + "Limit votes per person": "Limit votes per person", "live": "live", "Live for {{duration}}": "Live for {{duration}}", "Live location": "Live location", @@ -158,7 +163,7 @@ "Message was blocked by moderation policies": "Message was blocked by moderation policies", "Messages have been marked unread.": "Messages have been marked unread.", "Missing permissions to upload the attachment": "Missing permissions to upload the attachment", - "Multiple answers": "Multiple answers", + "Multiple votes": "Multiple votes", "Mute": "Mute", "mute-command-args": "[@username]", "mute-command-description": "Mute a user", @@ -215,6 +220,7 @@ "searchResultsCount_other": "{{ count }} results", "See all options ({{count}})_one": "See all options ({{count}})", "See all options ({{count}})_other": "See all options ({{count}})", + "Select more than one option": "Select more than one option", "Select one": "Select one", "Select one or more": "Select one or more", "Select up to {{count}}_one": "Select up to {{count}}", @@ -222,6 +228,7 @@ "Send": "Send", "Send Anyway": "Send Anyway", "Send message request failed": "Send message request failed", + "Send poll": "Send poll", "Sending...": "Sending...", "Sent": "Sent", "Share": "Share", diff --git a/src/i18n/es.json b/src/i18n/es.json index 7281a5489..f80eb0773 100644 --- a/src/i18n/es.json +++ b/src/i18n/es.json @@ -37,6 +37,7 @@ "Allow access to microphone": "Permitir acceso al micrófono", "Allow comments": "Permitir comentarios", "Allow option suggestion": "Permitir sugerencia de opciones", + "Allow voters to add comments": "Permitir que los votantes añadan comentarios", "Also send as a direct message": "También enviar como mensaje directo", "Also send in channel": "También enviar en el canal", "An error has occurred during recording": "Se ha producido un error durante la grabación", @@ -85,6 +86,7 @@ "Cannot seek in the recording": "No se puede buscar en la grabación", "Channel Missing": "Falta canal", "Channels": "Canales", + "Choose between 2 to 10 options": "Elige entre 2 y 10 opciones", "Close": "Cerrar", "Close emoji picker": "Cerrar el selector de emojis", "Commands": "Comandos", @@ -143,8 +145,11 @@ "Generating...": "Generando...", "giphy-command-args": "[texto]", "giphy-command-description": "Publicar un gif aleatorio en el canal", + "Hide who voted": "Ocultar quién votó", "Instant commands": "Comandos instantáneos", "Latest Messages": "Últimos mensajes", + "Let others add options": "Permitir que otros añadan opciones", + "Limit votes per person": "Limitar votos por persona", "live": "En vivo", "Live for {{duration}}": "En vivo durante {{duration}}", "Live location": "Ubicación en vivo", @@ -163,7 +168,7 @@ "Message was blocked by moderation policies": "El mensaje fue bloqueado por las políticas de moderación", "Messages have been marked unread.": "Los mensajes han sido marcados como no leídos.", "Missing permissions to upload the attachment": "Faltan permisos para subir el archivo adjunto", - "Multiple answers": "Múltiples respuestas", + "Multiple votes": "Votos múltiples", "Mute": "Silenciar", "mute-command-args": "[@usuario]", "mute-command-description": "Silenciar a un usuario", @@ -223,6 +228,7 @@ "See all options ({{count}})_one": "Ver todas las opciones ({{count}})", "See all options ({{count}})_many": "Ver todas las opciones ({{count}})", "See all options ({{count}})_other": "Ver todas las opciones ({{count}})", + "Select more than one option": "Seleccionar más de una opción", "Select one": "Seleccionar uno", "Select one or more": "Seleccionar uno o más", "Select up to {{count}}_one": "Selecciona hasta {{count}}", @@ -231,6 +237,7 @@ "Send": "Enviar", "Send Anyway": "Enviar de todos modos", "Send message request failed": "Error al enviar la solicitud de mensaje", + "Send poll": "Enviar encuesta", "Sending...": "Enviando...", "Sent": "Enviado", "Share": "Compartir", diff --git a/src/i18n/fr.json b/src/i18n/fr.json index 06630dc5e..c841fef02 100644 --- a/src/i18n/fr.json +++ b/src/i18n/fr.json @@ -37,6 +37,7 @@ "Allow access to microphone": "Autoriser l'accès au microphone", "Allow comments": "Autoriser les commentaires", "Allow option suggestion": "Autoriser la suggestion d'options", + "Allow voters to add comments": "Permettre aux votants d'ajouter des commentaires", "Also send as a direct message": "Également envoyer en message direct", "Also send in channel": "Également envoyer dans le canal", "An error has occurred during recording": "Une erreur s'est produite pendant l'enregistrement", @@ -85,6 +86,7 @@ "Cannot seek in the recording": "Impossible de rechercher dans l'enregistrement", "Channel Missing": "Canal Manquant", "Channels": "Canaux", + "Choose between 2 to 10 options": "Choisir entre 2 et 10 options", "Close": "Fermer", "Close emoji picker": "Fermer le sélecteur d'émojis", "Commands": "Commandes", @@ -143,8 +145,11 @@ "Generating...": "Génération...", "giphy-command-args": "[texte]", "giphy-command-description": "Poster un GIF aléatoire dans le canal", + "Hide who voted": "Masquer qui a voté", "Instant commands": "Commandes instantanées", "Latest Messages": "Derniers messages", + "Let others add options": "Permettre à d'autres d'ajouter des options", + "Limit votes per person": "Limiter les votes par personne", "live": "en direct", "Live for {{duration}}": "En direct pendant {{duration}}", "Live location": "Emplacement en direct", @@ -163,7 +168,7 @@ "Message was blocked by moderation policies": "Le message a été bloqué par les politiques de modération", "Messages have been marked unread.": "Les messages ont été marqués comme non lus.", "Missing permissions to upload the attachment": "Autorisations manquantes pour télécharger la pièce jointe", - "Multiple answers": "Réponses multiples", + "Multiple votes": "Votes multiples", "Mute": "Muet", "mute-command-args": "[@nomdutilisateur]", "mute-command-description": "Muter un utilisateur", @@ -223,6 +228,7 @@ "See all options ({{count}})_one": "Voir toutes les options ({{count}})", "See all options ({{count}})_many": "Voir toutes les options ({{count}})", "See all options ({{count}})_other": "Voir toutes les options ({{count}})", + "Select more than one option": "Sélectionner plus d'une option", "Select one": "Sélectionner un", "Select one or more": "Sélectionner un ou plusieurs", "Select up to {{count}}_one": "Sélectionner jusqu'à {{count}}", @@ -231,6 +237,7 @@ "Send": "Envoyer", "Send Anyway": "Envoyer quand même", "Send message request failed": "Échec de la demande d'envoi de message", + "Send poll": "Envoyer le sondage", "Sending...": "Envoi en cours...", "Sent": "Envoyé", "Share": "Partager", diff --git a/src/i18n/hi.json b/src/i18n/hi.json index b8d732dae..e952214a8 100644 --- a/src/i18n/hi.json +++ b/src/i18n/hi.json @@ -32,6 +32,7 @@ "Allow access to microphone": "माइक्रोफ़ोन तक पहुँच दें", "Allow comments": "टिप्पणियाँ की अनुमति दें", "Allow option suggestion": "विकल्प सुझाव की अनुमति दें", + "Allow voters to add comments": "मतदाताओं को टिप्पणी जोड़ने दें", "Also send as a direct message": "सीधे संदेश के रूप में भी भेजें", "Also send in channel": "चैनल में भी भेजें", "An error has occurred during recording": "रेकॉर्डिंग के दौरान एक त्रुटि आ गई है", @@ -80,6 +81,7 @@ "Cannot seek in the recording": "रेकॉर्डिंग में खोज नहीं की जा सकती", "Channel Missing": "चैनल उपलब्ध नहीं है", "Channels": "चैनल", + "Choose between 2 to 10 options": "2 से 10 विकल्प चुनें", "Close": "बंद करे", "Close emoji picker": "इमोजी पिकर बंद करें", "Commands": "कमांड", @@ -139,8 +141,11 @@ "Generating...": "बना रहा है...", "giphy-command-args": "[पाठ]", "giphy-command-description": "चैनल पर एक क्रॉफिल जीआइएफ पोस्ट करें", + "Hide who voted": "किसने वोट दिया छिपाएं", "Instant commands": "तत्काल कमांड", "Latest Messages": "नवीनतम संदेश", + "Let others add options": "दूसरों को विकल्प जोड़ने दें", + "Limit votes per person": "प्रति व्यक्ति वोट सीमित करें", "live": "लाइव", "Live for {{duration}}": "{{duration}} के लिए लाइव", "Live location": "लाइव स्थान", @@ -159,7 +164,7 @@ "Message was blocked by moderation policies": "संदेश को मॉडरेशन नीतियों द्वारा ब्लॉक कर दिया गया है", "Messages have been marked unread.": "संदेशों को अपठित चिह्नित किया गया है।", "Missing permissions to upload the attachment": "अटैचमेंट अपलोड करने के लिए अनुमतियां गायब", - "Multiple answers": "कई उत्तर", + "Multiple votes": "कई वोट", "Mute": "म्यूट करे", "mute-command-args": "[@उपयोगकर्तनाम]", "mute-command-description": "एक उपयोगकर्ता को म्यूट करें", @@ -216,6 +221,7 @@ "searchResultsCount_other": "{{ count }} परिणाम", "See all options ({{count}})_one": "सभी विकल्प देखें ({{count}})", "See all options ({{count}})_other": "सभी विकल्प देखें ({{count}})", + "Select more than one option": "एक से अधिक विकल्प चुनें", "Select one": "एक चुनें", "Select one or more": "एक या अधिक चुनें", "Select up to {{count}}_one": "अधिकतम {{count}} तक चुनें", @@ -223,6 +229,7 @@ "Send": "भेजे", "Send Anyway": "वैसे भी भेजें", "Send message request failed": "संदेश भेजने का अनुरोध विफल रहा", + "Send poll": "पोल भेजें", "Sending...": "भेजा जा रहा है", "Sent": "भेजा गया", "Share": "साझा करें", diff --git a/src/i18n/it.json b/src/i18n/it.json index ad9c70f9f..aaf68fa21 100644 --- a/src/i18n/it.json +++ b/src/i18n/it.json @@ -37,6 +37,7 @@ "Allow access to microphone": "Consenti l'accesso al microfono", "Allow comments": "Consenti i commenti", "Allow option suggestion": "Consenti il suggerimento di opzioni", + "Allow voters to add comments": "Consenti ai votanti di aggiungere commenti", "Also send as a direct message": "Invia anche come messaggio diretto", "Also send in channel": "Invia anche nel canale", "An error has occurred during recording": "Si è verificato un errore durante la registrazione", @@ -85,6 +86,7 @@ "Cannot seek in the recording": "Impossibile cercare nella registrazione", "Channel Missing": "Il canale non esiste", "Channels": "Canali", + "Choose between 2 to 10 options": "Scegli tra 2 e 10 opzioni", "Close": "Chiudi", "Close emoji picker": "Chiudi il selettore di emoji", "Commands": "Comandi", @@ -143,8 +145,11 @@ "Generating...": "Generando...", "giphy-command-args": "[testo]", "giphy-command-description": "Pubblica un gif casuale sul canale", + "Hide who voted": "Nascondi chi ha votato", "Instant commands": "Comandi istantanei", "Latest Messages": "Ultimi messaggi", + "Let others add options": "Lascia che altri aggiungano opzioni", + "Limit votes per person": "Limita i voti per persona", "live": "live", "Live for {{duration}}": "Live per {{duration}}", "Live location": "Posizione live", @@ -163,7 +168,7 @@ "Message was blocked by moderation policies": "Il messaggio è stato bloccato dalle politiche di moderazione", "Messages have been marked unread.": "I messaggi sono stati contrassegnati come non letti.", "Missing permissions to upload the attachment": "Autorizzazioni mancanti per caricare l'allegato", - "Multiple answers": "Risposte multiple", + "Multiple votes": "Voti multipli", "Mute": "Silenzia", "mute-command-args": "[@nomeutente]", "mute-command-description": "Silenzia un utente", @@ -223,6 +228,7 @@ "See all options ({{count}})_one": "Vedi tutte le opzioni ({{count}})", "See all options ({{count}})_many": "Vedi tutte le opzioni ({{count}})", "See all options ({{count}})_other": "Vedi tutte le opzioni ({{count}})", + "Select more than one option": "Seleziona più di un'opzione", "Select one": "Seleziona uno", "Select one or more": "Seleziona uno o più", "Select up to {{count}}_one": "Seleziona fino a {{count}}", @@ -231,6 +237,7 @@ "Send": "Invia", "Send Anyway": "Invia comunque", "Send message request failed": "Richiesta di invio messaggio non riuscita", + "Send poll": "Invia sondaggio", "Sending...": "Invio in corso...", "Sent": "Inviato", "Share": "Condividi", diff --git a/src/i18n/ja.json b/src/i18n/ja.json index 9f4c4e58a..b1ad1bfe2 100644 --- a/src/i18n/ja.json +++ b/src/i18n/ja.json @@ -32,6 +32,7 @@ "Allow access to microphone": "マイクロフォンへのアクセスを許可する", "Allow comments": "コメントを許可", "Allow option suggestion": "オプションの提案を許可", + "Allow voters to add comments": "投票者にコメントを追加することを許可する", "Also send as a direct message": "ダイレクトメッセージとしても送信", "Also send in channel": "チャンネルにも送信", "An error has occurred during recording": "録音中にエラーが発生しました", @@ -80,6 +81,7 @@ "Cannot seek in the recording": "録音中にシークできません", "Channel Missing": "チャネルがありません", "Channels": "チャンネル", + "Choose between 2 to 10 options": "2〜10の選択肢から選ぶ", "Close": "閉める", "Close emoji picker": "絵文字ピッカーを閉める", "Commands": "コマンド", @@ -138,8 +140,11 @@ "Generating...": "生成中...", "giphy-command-args": "[テキスト]", "giphy-command-description": "チャンネルにランダムなGIFを投稿する", + "Hide who voted": "誰が投票したかを非表示にする", "Instant commands": "インスタントコマンド", "Latest Messages": "最新のメッセージ", + "Let others add options": "他の人が選択肢を追加できるようにする", + "Limit votes per person": "1人あたりの投票数を制限する", "live": "ライブ", "Live for {{duration}}": "{{duration}}間ライブ", "Live location": "ライブ位置情報", @@ -158,7 +163,7 @@ "Message was blocked by moderation policies": "メッセージはモデレーションポリシーによってブロックされました", "Messages have been marked unread.": "メッセージは未読としてマークされました。", "Missing permissions to upload the attachment": "添付ファイルをアップロードするための許可がありません", - "Multiple answers": "複数回答", + "Multiple votes": "複数投票", "Mute": "無音", "mute-command-args": "[@ユーザ名]", "mute-command-description": "ユーザーをミュートする", @@ -215,6 +220,7 @@ "searchResultsCount_other": "{{ count }}件の結果", "See all options ({{count}})_one": "すべてのオプションを見る ({{count}})", "See all options ({{count}})_other": "すべてのオプションを見る ({{count}})", + "Select more than one option": "複数の選択肢を選ぶ", "Select one": "1つ選択", "Select one or more": "1つ以上選択", "Select up to {{count}}_one": "最大{{count}}まで選択", @@ -222,6 +228,7 @@ "Send": "送信", "Send Anyway": "とにかく送信する", "Send message request failed": "メッセージ送信リクエストが失敗しました", + "Send poll": "アンケートを送信", "Sending...": "送信中...", "Sent": "送信済み", "Share": "共有", diff --git a/src/i18n/ko.json b/src/i18n/ko.json index 9e37a9b42..e0cb63e82 100644 --- a/src/i18n/ko.json +++ b/src/i18n/ko.json @@ -32,6 +32,7 @@ "Allow access to microphone": "마이크로폰에 대한 액세스 허용", "Allow comments": "댓글 허용", "Allow option suggestion": "옵션 제안 허용", + "Allow voters to add comments": "투표자가 댓글을 추가할 수 있도록 허용", "Also send as a direct message": "다이렉트 메시지로도 보내기", "Also send in channel": "채널에도 보내기", "An error has occurred during recording": "녹음 중 오류가 발생했습니다", @@ -80,6 +81,7 @@ "Cannot seek in the recording": "녹음에서 찾을 수 없습니다", "Channel Missing": "채널 누락", "Channels": "채널", + "Choose between 2 to 10 options": "2~10개의 선택지 중에서 선택", "Close": "닫기", "Close emoji picker": "이모티콘 선택기 닫기", "Commands": "명령어", @@ -138,8 +140,11 @@ "Generating...": "생성 중...", "giphy-command-args": "[텍스트]", "giphy-command-description": "채널에 무작위 GIF 게시", + "Hide who voted": "누가 투표했는지 숨기기", "Instant commands": "즉시 명령어", "Latest Messages": "최신 메시지", + "Let others add options": "다른 사람이 선택지를 추가할 수 있도록 허용", + "Limit votes per person": "1인당 투표 수 제한", "live": "라이브", "Live for {{duration}}": "{{duration}} 동안 라이브", "Live location": "라이브 위치", @@ -158,7 +163,7 @@ "Message was blocked by moderation policies": "메시지가 관리 정책에 의해 차단되었습니다.", "Messages have been marked unread.": "메시지가 읽지 않음으로 표시되었습니다.", "Missing permissions to upload the attachment": "첨부 파일을 업로드하려면 권한이 필요합니다", - "Multiple answers": "복수 응답", + "Multiple votes": "복수 투표", "Mute": "무음", "mute-command-args": "[@사용자이름]", "mute-command-description": "사용자 음소거", @@ -215,6 +220,7 @@ "searchResultsCount_other": "{{ count }}개 결과", "See all options ({{count}})_one": "모든 옵션 보기 ({{count}})", "See all options ({{count}})_other": "모든 옵션 보기 ({{count}})", + "Select more than one option": "하나 이상의 선택지 선택", "Select one": "하나 선택", "Select one or more": "하나 이상 선택", "Select up to {{count}}_one": "{{count}}개까지 선택", @@ -222,6 +228,7 @@ "Send": "보내다", "Send Anyway": "어쨌든 보내기", "Send message request failed": "메시지 보내기 요청 실패", + "Send poll": "투표 보내기", "Sending...": "배상중...", "Sent": "전송됨", "Share": "공유", diff --git a/src/i18n/nl.json b/src/i18n/nl.json index 3bf733df6..7448d9053 100644 --- a/src/i18n/nl.json +++ b/src/i18n/nl.json @@ -32,6 +32,7 @@ "Allow access to microphone": "Toegang tot microfoon toestaan", "Allow comments": "Sta opmerkingen toe", "Allow option suggestion": "Sta optie-suggesties toe", + "Allow voters to add comments": "Sta kiezers toe om opmerkingen toe te voegen", "Also send as a direct message": "Ook als direct bericht versturen", "Also send in channel": "Ook in kanaal versturen", "An error has occurred during recording": "Er is een fout opgetreden tijdens het opnemen", @@ -80,6 +81,7 @@ "Cannot seek in the recording": "Kan niet zoeken in de opname", "Channel Missing": "Kanaal niet gevonden", "Channels": "Kanalen", + "Choose between 2 to 10 options": "Kies tussen 2 en 10 opties", "Close": "Sluit", "Close emoji picker": "Sluit de emoji-kiezer", "Commands": "Commando's", @@ -138,8 +140,11 @@ "Generating...": "Genereren...", "giphy-command-args": "[tekst]", "giphy-command-description": "Plaats een willekeurige gif in het kanaal", + "Hide who voted": "Verberg wie heeft gestemd", "Instant commands": "Snelle opdrachten", "Latest Messages": "Laatste berichten", + "Let others add options": "Laat anderen opties toevoegen", + "Limit votes per person": "Stemmen per persoon beperken", "live": "live", "Live for {{duration}}": "Live voor {{duration}}", "Live location": "Live locatie", @@ -158,7 +163,7 @@ "Message was blocked by moderation policies": "Bericht is geblokkeerd door moderatiebeleid", "Messages have been marked unread.": "Berichten zijn gemarkeerd als ongelezen.", "Missing permissions to upload the attachment": "Missende toestemmingen om de bijlage te uploaden", - "Multiple answers": "Meerdere antwoorden", + "Multiple votes": "Meerdere stemmen", "Mute": "Dempen", "mute-command-args": "[@gebruikersnaam]", "mute-command-description": "Een gebruiker dempen", @@ -215,6 +220,7 @@ "searchResultsCount_other": "{{ count }} resultaten", "See all options ({{count}})_one": "Bekijk alle opties ({{count}})", "See all options ({{count}})_other": "Bekijk alle opties ({{count}})", + "Select more than one option": "Selecteer meer dan één optie", "Select one": "Selecteer er een", "Select one or more": "Selecteer een of meer", "Select up to {{count}}_one": "Selecteer tot {{count}}", @@ -222,6 +228,7 @@ "Send": "Verstuur", "Send Anyway": "Toch versturen", "Send message request failed": "Verzoek om bericht te verzenden mislukt", + "Send poll": "Peiling versturen", "Sending...": "Aan het verzenden...", "Sent": "Verzonden", "Share": "Delen", diff --git a/src/i18n/pt.json b/src/i18n/pt.json index ec268017f..266f58a9e 100644 --- a/src/i18n/pt.json +++ b/src/i18n/pt.json @@ -37,6 +37,7 @@ "Allow access to microphone": "Permitir acesso ao microfone", "Allow comments": "Permitir comentários", "Allow option suggestion": "Permitir sugestão de opção", + "Allow voters to add comments": "Permitir que os votantes adicionem comentários", "Also send as a direct message": "Também enviar como mensagem direta", "Also send in channel": "Também enviar no canal", "An error has occurred during recording": "Ocorreu um erro durante a gravação", @@ -85,6 +86,7 @@ "Cannot seek in the recording": "Não é possível buscar na gravação", "Channel Missing": "Canal ausente", "Channels": "Canais", + "Choose between 2 to 10 options": "Escolha entre 2 a 10 opções", "Close": "Fechar", "Close emoji picker": "Fechar seletor de emoji", "Commands": "Comandos", @@ -143,8 +145,11 @@ "Generating...": "Gerando...", "giphy-command-args": "[texto]", "giphy-command-description": "Postar um gif aleatório no canal", + "Hide who voted": "Ocultar quem votou", "Instant commands": "Comandos instantâneos", "Latest Messages": "Mensagens mais recentes", + "Let others add options": "Permitir que outros adicionem opções", + "Limit votes per person": "Limitar votos por pessoa", "live": "ao vivo", "Live for {{duration}}": "Ao vivo por {{duration}}", "Live location": "Localização ao vivo", @@ -163,7 +168,7 @@ "Message was blocked by moderation policies": "A mensagem foi bloqueada pelas políticas de moderação", "Messages have been marked unread.": "Mensagens foram marcadas como não lidas.", "Missing permissions to upload the attachment": "Faltando permissões para enviar o anexo", - "Multiple answers": "Múltiplas respostas", + "Multiple votes": "Votos múltiplos", "Mute": "Silenciar", "mute-command-args": "[@nomedeusuário]", "mute-command-description": "Silenciar um usuário", @@ -223,6 +228,7 @@ "See all options ({{count}})_one": "Ver todas as opções ({{count}})", "See all options ({{count}})_many": "Ver todas as opções ({{count}})", "See all options ({{count}})_other": "Ver todas as opções ({{count}})", + "Select more than one option": "Selecionar mais de uma opção", "Select one": "Selecionar um", "Select one or more": "Selecionar um ou mais", "Select up to {{count}}_one": "Selecionar até {{count}}", @@ -231,6 +237,7 @@ "Send": "Enviar", "Send Anyway": "Enviar de qualquer forma", "Send message request failed": "O pedido de envio da mensagem falhou", + "Send poll": "Enviar enquete", "Sending...": "Enviando...", "Sent": "Enviado", "Share": "Compartilhar", diff --git a/src/i18n/ru.json b/src/i18n/ru.json index 38c76dee7..62540a4dd 100644 --- a/src/i18n/ru.json +++ b/src/i18n/ru.json @@ -42,6 +42,7 @@ "Allow access to microphone": "Разрешить доступ к микрофону", "Allow comments": "Разрешить комментарии", "Allow option suggestion": "Разрешить предложение вариантов", + "Allow voters to add comments": "Разрешить участникам голосования добавлять комментарии", "Also send as a direct message": "Также отправить как личное сообщение", "Also send in channel": "Также отправить в канал", "An error has occurred during recording": "Произошла ошибка во время записи", @@ -90,6 +91,7 @@ "Cannot seek in the recording": "Невозможно осуществить поиск в записи", "Channel Missing": "Канал не найден", "Channels": "Каналы", + "Choose between 2 to 10 options": "Выберите от 2 до 10 вариантов", "Close": "Закрыть", "Close emoji picker": "Закрыть окно выбора смайлов", "Commands": "Команды", @@ -148,8 +150,11 @@ "Generating...": "Генерирую...", "giphy-command-args": "[текст]", "giphy-command-description": "Опубликовать случайную GIF-анимацию в канале", + "Hide who voted": "Скрыть, кто голосовал", "Instant commands": "Мгновенные команды", "Latest Messages": "Последние сообщения", + "Let others add options": "Разрешить другим добавлять варианты", + "Limit votes per person": "Ограничить голоса на человека", "live": "В прямом эфире", "Live for {{duration}}": "В прямом эфире {{duration}}", "Live location": "Местоположение в прямом эфире", @@ -168,7 +173,7 @@ "Message was blocked by moderation policies": "Сообщение было заблокировано модерацией", "Messages have been marked unread.": "Сообщения были отмечены как непрочитанные.", "Missing permissions to upload the attachment": "Отсутствуют разрешения для загрузки вложения", - "Multiple answers": "Несколько ответов", + "Multiple votes": "Несколько голосов", "Mute": "Отключить уведомления", "mute-command-args": "[@имяпользователя]", "mute-command-description": "Выключить микрофон у пользователя", @@ -231,6 +236,7 @@ "See all options ({{count}})_few": "Посмотреть все варианты ({{count}})", "See all options ({{count}})_many": "Посмотреть все варианты ({{count}})", "See all options ({{count}})_other": "Посмотреть все варианты ({{count}})", + "Select more than one option": "Выберите более одного варианта", "Select one": "Выберите один", "Select one or more": "Выберите один или несколько", "Select up to {{count}}_one": "Выберите до {{count}}", @@ -240,6 +246,7 @@ "Send": "Отправить", "Send Anyway": "Мне всё равно, отправить", "Send message request failed": "Не удалось отправить запрос на отправку сообщения", + "Send poll": "Отправить опрос", "Sending...": "Отправка...", "Sent": "Отправлено", "Share": "Поделиться", diff --git a/src/i18n/tr.json b/src/i18n/tr.json index 24c2de5be..e60dc4194 100644 --- a/src/i18n/tr.json +++ b/src/i18n/tr.json @@ -32,6 +32,7 @@ "Allow access to microphone": "Mikrofona erişime izin ver", "Allow comments": "Yorumlara izin ver", "Allow option suggestion": "Seçenek önerisine izin ver", + "Allow voters to add comments": "Oy verenlerin yorum eklemesine izin ver", "Also send as a direct message": "Ayrıca doğrudan mesaj olarak gönder", "Also send in channel": "Ayrıca kanala gönder", "An error has occurred during recording": "Kayıt sırasında bir hata oluştu", @@ -80,6 +81,7 @@ "Cannot seek in the recording": "Kayıtta arama yapılamıyor", "Channel Missing": "Kanal bulunamıyor", "Channels": "Kanallar", + "Choose between 2 to 10 options": "2 ile 10 seçenek arasından seçin", "Close": "Kapat", "Close emoji picker": "Emoji seçiciyi kapat", "Commands": "Komutlar", @@ -138,8 +140,11 @@ "Generating...": "Oluşturuluyor...", "giphy-command-args": "[metin]", "giphy-command-description": "Rastgele bir gif'i kanala gönder", + "Hide who voted": "Kimin oy verdiğini gizle", "Instant commands": "Anlık komutlar", "Latest Messages": "Son Mesajlar", + "Let others add options": "Başkalarının seçenek eklemesine izin ver", + "Limit votes per person": "Kişi başına oy sınırı", "live": "canlı", "Live for {{duration}}": "{{duration}} boyunca canlı", "Live location": "Canlı konum", @@ -158,7 +163,7 @@ "Message was blocked by moderation policies": "Mesaj moderasyon politikaları tarafından engellendi", "Messages have been marked unread.": "Mesajlar okunmamış olarak işaretlendi.", "Missing permissions to upload the attachment": "Ek yüklemek için izinler eksik", - "Multiple answers": "Çoklu cevaplar", + "Multiple votes": "Çoklu oy", "Mute": "Sessiz", "mute-command-args": "[@kullanıcıadı]", "mute-command-description": "Bir kullanıcının sesini kapat", @@ -215,6 +220,7 @@ "searchResultsCount_other": "{{ count }} sonuç", "See all options ({{count}})_one": "Tüm seçenekleri göster ({{count}})", "See all options ({{count}})_other": "Tüm seçenekleri göster ({{count}})", + "Select more than one option": "Birden fazla seçenek seçin", "Select one": "Birini seçin", "Select one or more": "Bir veya daha fazlasını seçin", "Select up to {{count}}_one": "En fazla {{count}}'yi seçin", @@ -222,6 +228,7 @@ "Send": "Gönder", "Send Anyway": "Yine de gönder", "Send message request failed": "Mesaj gönderme isteği başarısız oldu", + "Send poll": "Anketi gönder", "Sending...": "Gönderiliyor...", "Sent": "Gönderildi", "Share": "Paylaş", diff --git a/src/styling/_global-theme-variables.scss b/src/styling/_global-theme-variables.scss index 2873919ec..860f0a9ac 100644 --- a/src/styling/_global-theme-variables.scss +++ b/src/styling/_global-theme-variables.scss @@ -37,6 +37,37 @@ var(--typography-font-family-sans), system-ui, -apple-system, BlinkMacSystemFont, Segoe UI, Roboto, Oxygen-Sans, Ubuntu, Cantarell, Helvetica Neue, sans-serif; + --str-chat__metadata-default-text: normal + var(--typography-font-weight-regular) + var(--typography-font-size-xs) / + var(--typography-line-height-tight) + var(--str-chat__font-family); + + --str-chat__caption-emphasis-text: normal + var(--typography-font-weight-semi-bold) + var(--typography-font-size-sm) / + var(--typography-line-height-tight) + var(--str-chat__font-family); + + --str-chat__body-emphasis-text: normal + var(--typography-font-weight-semi-bold) + var(--typography-font-size-md) / + var(--typography-line-height-normal) + var(--str-chat__font-family); + + --str-chat__heading-xs-text: normal + var(--typography-font-weight-medium) + var(--typography-font-size-sm) / + var(--typography-line-height-tight) + var(--str-chat__font-family); + + --str-chat__heading-sm-text: normal + var(--typography-font-weight-semi-bold) + var(--typography-font-size-md) / + var(--typography-line-height-normal) + var(--str-chat__font-family); + + // todo: adapt the old text variables to so that they use the new semantic text variables /* The font used for caption texts */ --str-chat__caption-text: 0.75rem/1.3 var(--str-chat__font-family); @@ -158,6 +189,8 @@ /* If a component has a box shadow applied to it, this will be the color used for the shadow */ --str-chat__box-shadow-color: rgba(0, 0, 0, 0.18); + --str-chat__box-shadow-elevation-1: 0 0 0 1px rgba(0, 0, 0, 0.05), 0 6px 12px 0 rgba(0, 0, 0, 0.16), 0 20px 32px 0 rgba(0, 0, 0, 0.12); + /* Used for online indicator and success messages */ --str-chat__info-color: var(--str-chat__green500); } diff --git a/src/styling/_utils.scss b/src/styling/_utils.scss index 6527626a2..07e390237 100644 --- a/src/styling/_utils.scss +++ b/src/styling/_utils.scss @@ -114,6 +114,105 @@ } } +@mixin header-layout { + display: flex; + padding: var(--str-chat__spacing-2); + column-gap: var(--str-chat__spacing-4); + align-items: center; +} + +@mixin header-text-layout { + display: flex; + flex-direction: column; + overflow-y: hidden; // for Edge + overflow-x: hidden; // for ellipsis text + flex: 1; + row-gap: var(--str-chat__spacing-1_5); +} + +@mixin empty-layout { + height: 100%; + display: flex; + flex-direction: column; + align-items: center; + justify-content: center; + padding: var(--str-chat__spacing-4); + + svg { + width: calc(var(--str-chat__spacing-px) * 136); + height: calc(var(--str-chat__spacing-px) * 136); + } +} + +@mixin empty-theme($component-name) { + font: var(--str-chat__headline-text); + text-align: center; + + svg path { + fill: var(--str-chat__#{$component-name}-empty-indicator-color); + } +} + +@mixin message-list-spacing { + $spacing: var(--str-chat__spacing-2); + padding: 0 $spacing; + + // Need this trick to be able to apply full-width background color on hover to messages / full-width separator to thread header + .str-chat__li { + margin-inline: calc(-1 * #{$spacing}); + padding-inline: $spacing; + } + + .str-chat__parent-message-li { + margin-inline: calc(-1 * #{$spacing}); + } + + @media only screen and (min-device-width: 768px) { + $spacing: min(var(--str-chat__spacing-10), 4%); + + padding: 0 $spacing; + + .str-chat__li { + margin-inline: calc(-1 * #{$spacing}); + padding-inline: $spacing; + } + + .str-chat__parent-message-li { + margin-inline: calc(-1 * #{$spacing} - 2px); + } + } +} + +@mixin loading-item-background($component-name) { + background-image: linear-gradient( + -90deg, + var(--str-chat__#{$component-name}-loading-state-color) 0%, + var(--str-chat__#{$component-name}-loading-state-color) 100% + ); +} + +@mixin loading-animation { + animation: pulsate 1s linear 0s infinite alternate; + + &:nth-of-type(2) { + animation: pulsate 1s linear 0.3334s infinite alternate; + } + + &:last-of-type { + animation: pulsate 1s linear 0.6667s infinite alternate; + } + + @keyframes pulsate { + from { + opacity: 0.5; + } + + to { + opacity: 1; + } + } +} + @mixin component-layer-overrides($component-name, $important: '') { background: var(--str-chat__#{$component-name}-background-color) #{$important}; color: var(--str-chat__#{$component-name}-color) #{$important}; diff --git a/src/styling/index.scss b/src/styling/index.scss index 15da8e8a8..ab73b32ae 100644 --- a/src/styling/index.scss +++ b/src/styling/index.scss @@ -16,7 +16,9 @@ // Base components @use '../components/Button/styling/Button'; +@use '../components/Form/styling' as Form; @use '../components/Dialog/styling' as Dialog; +@use '../components/Modal/styling' as Modal; // Specific components @use '../components/Attachment/styling' as Attachment; @@ -24,6 +26,7 @@ @use '../components/Avatar/styling/Avatar' as Avatar; @use '../components/Avatar/styling/AvatarStack' as AvatarStack; @use '../components/Avatar/styling/GroupAvatar' as GroupAvatar; +@use '../components/Channel/styling' as Channel; @use '../components/ChatView/styling' as ChatView; @use '../components/DateSeparator/styling' as DateSeparator; @use '../components/Gallery/styling' as Gallery; @@ -31,10 +34,11 @@ @use '../components/Message/styling' as Message; @use '../components/MessageActions/styling' as MessageActions; @use '../components/MessageInput/styling' as MessageComposer; -@use '../components/Modal/styling' as Modal; +@use '../components/Poll/styling' as Poll; @use '../components/Reactions/styling/ReactionList' as ReactionList; @use '../components/Reactions/styling/ReactionSelector' as ReactionSelector; @use '../components/TextareaComposer/styling' as TextareaComposer; +@use '../components/Thread/styling' as Thread; @use '../components/VideoPlayer/styling' as VideoPlayer; // Layers have to be kept the last diff --git a/src/styling/variables.css b/src/styling/variables.css index 866642968..eb52ebbb8 100644 --- a/src/styling/variables.css +++ b/src/styling/variables.css @@ -228,21 +228,27 @@ --button-padding-y-lg: 14px; --button-padding-y-md: 10px; --button-padding-y-sm: 6px; + --button-padding-y-xs: 4px; --button-padding-x-icon-only-lg: 14px; --button-padding-x-icon-only-md: 10px; --button-padding-x-icon-only-sm: 6px; + --button-padding-x-icon-only-xs: 4px; --button-padding-x-with-label-lg: 16px; --button-padding-x-with-label-md: 16px; --button-padding-x-with-label-sm: 16px; + --button-padding-x-with-label-xs: 12px; --background-core-hover: rgba(30, 37, 43, 0.05); /** Hover feedback overlay. */ --background-core-pressed: rgba(30, 37, 43, 0.1); /** Pressed feedback overlay. */ --background-core-selected: rgba(30, 37, 43, 0.15); /** Selected overlay. */ --background-core-scrim: rgba(0, 0, 0, 0.25); /** Dimmed overlay for modals. */ --background-core-overlay: rgba(255, 255, 255, 0.75); /** Selected overlay. */ + --background-core-overlay-light: rgba(255, 255, 255, 0.75); /** Selected overlay. */ + --background-core-overlay-dark: rgba(0, 0, 0, 0.25); /** Selected overlay. */ --border-core-opacity-10: rgba(0, 0, 0, 0.1); /** Image frame border treatment. */ --border-core-opacity-25: rgba(0, 0, 0, 0.25); /** Image frame border treatment. */ --system-bg-blur: rgba(255, 255, 255, 0.01); --system-scrollbar: rgba(0, 0, 0, 0.5); + --badge-bg-overlay: rgba(0, 0, 0, 0.75); --typography-font-family-sans: var(--font-family-geist); --typography-font-family-mono: var(--font-family-geist-mono); --typography-font-size-xxs: var(--font-size-size-10); /** Micro text such as timestamps or subtle metadata. */ @@ -286,6 +292,7 @@ --button-visual-height-sm: var(--size-32); --button-visual-height-md: var(--size-40); --button-visual-height-lg: var(--size-48); + --button-visual-height-xs: var(--size-24); /** * Minimum interactive hit target size. * @@ -313,40 +320,36 @@ --icon-stroke-default: var(--w150); --icon-stroke-emphasis: var(--w200); --background-core-disabled: var(--slate-100); /** Optional disabled background for inputs, buttons, or chips. */ - --background-core-surface: var(--slate-200); /** Standard section background. */ - --background-core-surface-subtle: var(--slate-100); /** Very light section background. */ - --background-core-surface-strong: var(--slate-300); /** Stronger section background. */ + --background-core-surface: var(--slate-100); /** Standard section background. */ + --background-core-surface-subtle: var(--slate-50); /** Very light section background. */ + --background-core-surface-strong: var(--slate-150); /** Stronger section background. */ + --background-core-inverse: var(--slate-900); /** Inverse background used for elevated, transient, or high-attention UI surfaces that sit on top of the default app background. */ + --background-core-highlight: var(--yellow-50); --background-elevation-elevation-0: var(--base-white); /** Flat surfaces. */ --background-elevation-elevation-1: var(--base-white); /** Slightly elevated surfaces. */ --background-elevation-elevation-2: var(--base-white); /** Card-like elements. */ --background-elevation-elevation-3: var(--base-white); /** Popovers. */ --background-elevation-elevation-4: var(--base-white); /** Dialogs, modals. */ - --border-utility-disabled: var(--slate-200); /** Optional disabled background for inputs, buttons, or chips. */ + --border-utility-disabled: var(--slate-100); /** Optional disabled background for inputs, buttons, or chips. */ --border-core-default: var(--slate-150); /** Standard surface border. */ --border-core-subtle: var(--slate-100); /** Very light separators. */ - --border-core-strong: var(--slate-200); /** Stronger surface border. */ + --border-core-strong: var(--slate-300); /** Stronger surface border. */ --border-core-on-dark: var(--base-white); /** Used on dark backgrounds. */ --border-core-on-accent: var(--base-white); /** Borders on accent backgrounds. */ - --chat-bg-incoming: var(--slate-100); /** Incoming bubble background. */ - --chat-bg-attachment-incoming: var(--slate-150); /** Attachment card in incoming bubble. */ - --chat-border-outgoing: var(--base-transparent-0); - --chat-border-incoming: var(--base-transparent-0); - --chat-border-on-chat-incoming: var(--slate-400); + --border-core-on-surface: var(--slate-200); --chat-reply-indicator-incoming: var(--slate-400); /** Reply indicator shading for incoming. */ --chat-waveform-bar: var(--border-core-opacity-25); - --chat-poll-progress-track-incoming: var(--slate-600); - --chat-poll-progress-fill-incoming: var(--slate-300); - --chat-thread-connector-incoming: var(--slate-100); --system-text: var(--base-black); --control-radiocheck-bg: var(--base-transparent-0); - --control-progress-bar-track: var(--slate-500); - --control-progress-bar-fill: var(--slate-200); + --control-progress-bar-track: var(--slate-200); + --control-progress-bar-fill: var(--slate-500); --text-primary: var(--slate-900); /** Main text color. */ --text-secondary: var(--slate-700); /** Secondary metadata text. */ --text-tertiary: var(--slate-500); /** Lowest priority text. */ --text-inverse: var(--base-white); /** Text on dark or accent backgrounds. */ - --text-disabled: var(--slate-400); /** Disabled text. */ + --text-disabled: var(--slate-300); /** Disabled text. */ --text-on-accent: var(--base-white); /** Text on dark or accent backgrounds. */ + --text-on-dark: var(--base-white); /** Text on dark or accent backgrounds. */ --avatar-palette-bg-1: var(--blue-100); --avatar-palette-bg-2: var(--cyan-100); --avatar-palette-bg-3: var(--green-100); @@ -359,7 +362,7 @@ --avatar-palette-text-5: var(--yellow-800); --avatar-bg-placeholder: var(--slate-100); --avatar-text-placeholder: var(--slate-500); - --accent-success: var(--green-300); /** For success states and positive actions. */ + --accent-success: var(--green-400); /** For success states and positive actions. */ --accent-warning: var(--yellow-400); /** Warning or caution messages. */ --accent-error: var(--red-500); /** Destructive actions and error states. */ --accent-neutral: var(--slate-500); /** Neutral accent for low-priority badges. */ @@ -375,6 +378,9 @@ --brand-700: var(--blue-700); --brand-800: var(--blue-800); --brand-900: var(--blue-900); + --skeleton-gradient-default-base: var(--slate-50); /** Base color for the default skeleton loading gradient. Used as the background tone for placeholder surfaces. */ + --skeleton-gradient-default-highlight: var(--base-white); /** Highlight color for the default skeleton loading gradient. Used for the moving shimmer to indicate loading activity. */ + --skeleton-gradient-accent-highlight: var(--base-white); /** Highlight color for the accent skeleton loading gradient. Used for the shimmer on accent placeholders. */ --device-radius: var(--radius-md); --message-bubble-radius-group-top: var(--radius-2xl); --message-bubble-radius-group-middle: var(--radius-2xl); @@ -387,32 +393,48 @@ --composer-bg: var(--background-elevation-elevation-1); /** Composer container background. */ --button-primary-text-on-accent: var(--text-on-accent); --button-primary-border: var(--brand-200); + --button-primary-text-on-dark: var(--text-on-dark); + --button-primary-border-on-dark: var(--border-core-on-dark); --button-secondary-text: var(--text-primary); --button-secondary-bg-liquid-glass: var(--background-elevation-elevation-0); --button-secondary-border: var(--border-core-default); - --button-secondary-bg: var(--background-core-surface-subtle); + --button-secondary-bg: var(--background-core-surface); --button-secondary-text-on-accent: var(--text-primary); + --button-secondary-text-on-dark: var(--text-on-dark); + --button-secondary-border-on-dark: var(--border-core-on-dark); --button-destructive-text: var(--accent-error); --button-destructive-bg: var(--accent-error); --button-destructive-text-on-accent: var(--text-on-accent); --button-destructive-bg-liquid-glass: var(--background-elevation-elevation-0); --button-destructive-border: var(--accent-error); + --button-destructive-text-on-dark: var(--text-on-dark); + --button-destructive-border-on-dark: var(--text-on-dark); --background-core-app: var(--background-elevation-elevation-0); /** Global application background. */ - --border-utility-focus: var(--brand-300); /** Focus ring or focus border. */ + --border-utility-focus: var(--brand-150); /** Focus ring or focus border. */ --border-utility-error: var(--accent-error); /** Error state. */ --border-utility-warning: var(--accent-warning); /** Warning borders. */ --border-utility-success: var(--accent-success); /** Success borders. */ + --chat-bg-incoming: var(--background-core-surface); /** Incoming bubble background. */ --chat-bg-outgoing: var(--brand-100); /** Outgoing bubble background. */ + --chat-bg-attachment-incoming: var(--background-core-surface-strong); /** Attachment card in incoming bubble. */ --chat-bg-attachment-outgoing: var(--brand-150); /** Attachment card in outgoing bubble. */ --chat-bg-typing-indicator: var(--accent-neutral); /** Typing indicator chip. */ - --chat-text-message: var(--text-primary); /** Message body text. */ --chat-text-timestamp: var(--text-tertiary); /** Time labels. */ --chat-text-username: var(--text-secondary); /** Username label. */ --chat-text-reaction: var(--text-secondary); /** Reaction count text. */ --chat-text-system: var(--text-secondary); /** System messages like date separators. */ + --chat-text-incoming: var(--text-primary); /** Message body text. */ + --chat-text-outgoing: var(--brand-900); /** Message body text. */ + --chat-border-outgoing: var(--brand-100); + --chat-border-incoming: var(--border-core-subtle); --chat-border-on-chat-outgoing: var(--brand-300); + --chat-border-on-chat-incoming: var(--border-core-strong); --chat-reply-indicator-outgoing: var(--brand-400); /** Reply indicator shading for outgoing. */ - --chat-poll-progress-fill-outgoing: var(--brand-200); + --chat-poll-progress-track-incoming: var(--control-progress-bar-track); + --chat-poll-progress-track-outgoing: var(--brand-200); + --chat-poll-progress-fill-incoming: var(--control-progress-bar-fill); + --chat-thread-connector-incoming: var(--border-core-default); + --chat-thread-connector-outgoing: var(--brand-150); --input-border-default: var(--border-core-default); /** Default border of the chat input. Uses the standard border role from foundations. In high-contrast always black. */ --input-border-hover: var(--border-core-strong); /** Optional hover border when the input is hovered or highlighted. Slightly stronger than default. */ --input-text-default: var(--text-primary); /** Main text inside the chat input. */ @@ -420,7 +442,8 @@ --input-text-icon: var(--text-tertiary); /** Icons inside the input area (attach, emoji, camera, send when idle). Matches secondary text strength. */ --input-text-disabled: var(--text-disabled); /** Placeholder text for the input. Lower emphasis than main text. */ --input-send-icon-disabled: var(--text-disabled); /** Send icon when disabled (e.g. empty input). */ - --reaction-bg: var(--background-elevation-elevation-1); /** Reaction bar background. */ + --input-option-card-bg: var(--background-core-surface-subtle); + --reaction-bg: var(--background-elevation-elevation-3); /** Reaction bar background. */ --reaction-border: var(--border-core-default); /** Border around unselected reaction chips. Subtle in normal modes, strong in high-contrast for visibility. */ --reaction-text: var(--text-primary); /** Count label next to the emoji inside the reaction chip. Uses secondary text so it does not compete with message text. */ --reaction-emoji: var(--text-primary); /** Emoji color inside reaction chips. Uses primary text color so the emoji stays clearly legible. */ @@ -430,32 +453,34 @@ --badge-border: var(--border-core-on-dark); --badge-bg-error: var(--accent-error); --badge-bg-neutral: var(--accent-neutral); - --badge-text: var(--text-on-accent); - --badge-text-inverse: var(--text-primary); - --badge-bg-default: var(--background-elevation-elevation-1); - --badge-bg-inverse: var(--accent-black); + --badge-text: var(--text-primary); + --badge-text-inverse: var(--text-on-dark); + --badge-bg-default: var(--background-elevation-elevation-2); + --badge-bg-inverse: var(--background-core-inverse); + --badge-text-on-accent: var(--text-on-accent); --control-radiocheck-border: var(--border-core-default); - --control-radiocheck-icon-selected: var(--text-inverse); + --control-radiocheck-icon-selected: var(--text-on-dark); --control-remove-control-bg: var(--accent-black); --control-remove-control-icon: var(--text-on-accent); --control-remove-control-border: var(--border-core-on-dark); - --control-play-control-bg: var(--background-elevation-elevation-1); - --control-play-control-icon: var(--text-primary); - --control-play-control-border: var(--border-core-default); - --control-play-control-bg-inverse: var(--accent-black); - --control-play-control-icon-inverse: var(--text-on-accent); + --control-play-control-bg: var(--accent-black); + --control-play-control-icon: var(--text-on-accent); --control-toggle-switch-knob: var(--background-elevation-elevation-4); --control-toggle-switch-bg: var(--background-core-surface-strong); --control-toggle-switch-bg-disabled: var(--background-core-disabled); + --control-playback-toggle-border: var(--border-core-default); + --control-playback-toggle-text: var(--text-primary); --avatar-bg-default: var(--avatar-palette-bg-1); --avatar-text-default: var(--avatar-palette-text-1); --accent-primary: var(--brand-500); /** Main brand accent for interactive elements. */ + --chip-bg: var(--brand-100); + --chip-text: var(--brand-900); + --skeleton-gradient-accent-base: var(--brand-50); /** Base color for the accent skeleton loading gradient. Used on higher-contrast or accent surfaces (e.g. outgoing messages). */ --button-primary-bg: var(--accent-primary); --button-primary-text: var(--accent-primary); --border-utility-selected: var(--accent-primary); /** Focus ring or focus border. */ --chat-waveform-bar-playing: var(--accent-primary); - --chat-poll-progress-track-outgoing: var(--accent-primary); - --chat-thread-connector-outgoing: var(--chat-bg-outgoing); + --chat-poll-progress-fill-outgoing: var(--accent-primary); --input-send-icon: var(--accent-primary); /** Default send icon color in the input. Uses the brand accent. */ --system-caret: var(--accent-primary); --badge-bg-primary: var(--accent-primary); @@ -465,4 +490,5 @@ --chat-text-mention: var(--text-link); /** Mention styling. */ --chat-text-link: var(--text-link); /** Links inside message bubbles. */ --input-border-focus: var(--border-utility-selected); /** Focus border when the input is focused. Uses the shared focus state token (brand in normal modes, black in high-contrast). */ -} \ No newline at end of file + --input-border-selected: var(--border-utility-selected); /** Focus border when the input is focused. Uses the shared focus state token (brand in normal modes, black in high-contrast). */ +}