diff --git a/app/helpers/branding_helper.rb b/app/helpers/branding_helper.rb index 8201f36e3c..ef6d33ae5c 100644 --- a/app/helpers/branding_helper.rb +++ b/app/helpers/branding_helper.rb @@ -11,11 +11,14 @@ module BrandingHelper end def _logo_as_symbol_wordmark - content_tag(:svg, tag.use(href: '#logo-symbol-wordmark'), viewBox: '0 0 261 66', class: 'logo logo--wordmark') + tag.svg(viewBox: '0 0 261 66', class: 'logo logo--wordmark') do + tag.title('Mastodon') + + tag.use(href: '#logo-symbol-wordmark') + end end def _logo_as_symbol_icon - content_tag(:svg, tag.use(href: '#logo-symbol-icon'), viewBox: '0 0 79 79', class: 'logo logo--icon') + tag.svg(tag.use(href: '#logo-symbol-icon'), viewBox: '0 0 79 79', class: 'logo logo--icon') end def render_logo diff --git a/app/javascript/flavours/glitch/components/a11y_live_region/a11y_live_region.stories.tsx b/app/javascript/flavours/glitch/components/a11y_live_region/a11y_live_region.stories.tsx new file mode 100644 index 0000000000..00804d685b --- /dev/null +++ b/app/javascript/flavours/glitch/components/a11y_live_region/a11y_live_region.stories.tsx @@ -0,0 +1,25 @@ +import type { Meta, StoryObj } from '@storybook/react-vite'; + +import { A11yLiveRegion } from '.'; + +const meta = { + title: 'Components/A11yLiveRegion', + component: A11yLiveRegion, +} satisfies Meta; + +export default meta; + +type Story = StoryObj; + +export const Polite: Story = { + args: { + children: "This field can't be empty.", + }, +}; + +export const Assertive: Story = { + args: { + ...Polite.args, + role: 'alert', + }, +}; diff --git a/app/javascript/flavours/glitch/components/a11y_live_region/index.tsx b/app/javascript/flavours/glitch/components/a11y_live_region/index.tsx new file mode 100644 index 0000000000..51fee5e4b9 --- /dev/null +++ b/app/javascript/flavours/glitch/components/a11y_live_region/index.tsx @@ -0,0 +1,28 @@ +import { polymorphicForwardRef } from '@/types/polymorphic'; + +/** + * A live region is a content region that announces changes of its contents + * to users of assistive technology like screen readers. + * + * Dynamically added warnings, errors, or live status updates should be wrapped + * in a live region to ensure they are not missed when they appear. + * + * **Important:** + * Live regions must be present in the DOM _before_ + * the to-be announced content is rendered into it. + */ + +export const A11yLiveRegion = polymorphicForwardRef<'div'>( + ({ role = 'status', as: Component = 'div', children, ...props }, ref) => { + return ( + + {children} + + ); + }, +); diff --git a/app/javascript/flavours/glitch/components/callout_inline/callout_inline.stories.tsx b/app/javascript/flavours/glitch/components/callout_inline/callout_inline.stories.tsx new file mode 100644 index 0000000000..f18af41dc0 --- /dev/null +++ b/app/javascript/flavours/glitch/components/callout_inline/callout_inline.stories.tsx @@ -0,0 +1,39 @@ +import type { Meta, StoryObj } from '@storybook/react-vite'; + +import { CalloutInline } from '.'; + +const meta = { + title: 'Components/CalloutInline', + args: { + children: 'Contents here', + }, + component: CalloutInline, +} satisfies Meta; + +export default meta; + +type Story = StoryObj; + +export const Error: Story = { + args: { + variant: 'error', + }, +}; + +export const Warning: Story = { + args: { + variant: 'warning', + }, +}; + +export const Success: Story = { + args: { + variant: 'success', + }, +}; + +export const Info: Story = { + args: { + variant: 'info', + }, +}; diff --git a/app/javascript/flavours/glitch/components/callout_inline/index.tsx b/app/javascript/flavours/glitch/components/callout_inline/index.tsx new file mode 100644 index 0000000000..e2e6791963 --- /dev/null +++ b/app/javascript/flavours/glitch/components/callout_inline/index.tsx @@ -0,0 +1,39 @@ +import type { FC } from 'react'; + +import classNames from 'classnames'; + +import CheckIcon from '@/material-icons/400-24px/check.svg?react'; +import ErrorIcon from '@/material-icons/400-24px/error.svg?react'; +import InfoIcon from '@/material-icons/400-24px/info.svg?react'; +import WarningIcon from '@/material-icons/400-24px/warning.svg?react'; + +import { Icon } from '../icon'; + +import classes from './styles.module.css'; + +export interface FieldStatus { + variant: 'error' | 'warning' | 'info' | 'success'; + message?: string; +} + +const iconMap: Record = { + error: ErrorIcon, + warning: WarningIcon, + info: InfoIcon, + success: CheckIcon, +}; + +export const CalloutInline: FC< + Partial & React.ComponentPropsWithoutRef<'div'> +> = ({ variant = 'error', message, className, children, ...props }) => { + return ( +
+ + {message ?? children} +
+ ); +}; diff --git a/app/javascript/flavours/glitch/components/callout_inline/styles.module.css b/app/javascript/flavours/glitch/components/callout_inline/styles.module.css new file mode 100644 index 0000000000..8d32f7df9b --- /dev/null +++ b/app/javascript/flavours/glitch/components/callout_inline/styles.module.css @@ -0,0 +1,29 @@ +.wrapper { + display: flex; + align-items: start; + gap: 4px; + font-size: 13px; + font-weight: 500; + + &[data-variant='success'] { + color: var(--color-text-success); + } + + &[data-variant='warning'] { + color: var(--color-text-warning); + } + + &[data-variant='error'] { + color: var(--color-text-error); + } + + &[data-variant='info'] { + color: var(--color-text-primary); + } +} + +.icon { + width: 16px; + height: 16px; + margin-top: 1px; +} diff --git a/app/javascript/flavours/glitch/components/form_fields/checkbox_field.stories.tsx b/app/javascript/flavours/glitch/components/form_fields/checkbox_field.stories.tsx index 4d208cf21b..16b3a53f0b 100644 --- a/app/javascript/flavours/glitch/components/form_fields/checkbox_field.stories.tsx +++ b/app/javascript/flavours/glitch/components/form_fields/checkbox_field.stories.tsx @@ -76,7 +76,7 @@ export const Optional: Story = { export const WithError: Story = { args: { required: false, - hasError: true, + status: 'error', }, }; diff --git a/app/javascript/flavours/glitch/components/form_fields/checkbox_field.tsx b/app/javascript/flavours/glitch/components/form_fields/checkbox_field.tsx index 2b6933c847..c08b81ca36 100644 --- a/app/javascript/flavours/glitch/components/form_fields/checkbox_field.tsx +++ b/app/javascript/flavours/glitch/components/form_fields/checkbox_field.tsx @@ -13,12 +13,12 @@ type Props = Omit, 'type'> & { export const CheckboxField = forwardRef< HTMLInputElement, Props & CommonFieldWrapperProps ->(({ id, label, hint, hasError, required, ...otherProps }, ref) => ( +>(({ id, label, hint, status, required, ...otherProps }, ref) => ( diff --git a/app/javascript/flavours/glitch/components/form_fields/combobox_field.tsx b/app/javascript/flavours/glitch/components/form_fields/combobox_field.tsx index 3a011e5782..494003a22b 100644 --- a/app/javascript/flavours/glitch/components/form_fields/combobox_field.tsx +++ b/app/javascript/flavours/glitch/components/form_fields/combobox_field.tsx @@ -86,14 +86,14 @@ interface Props */ export const ComboboxFieldWithRef = ( - { id, label, hint, hasError, required, ...otherProps }: Props, + { id, label, hint, status, required, ...otherProps }: Props, ref: React.ForwardedRef, ) => ( {(inputProps) => } diff --git a/app/javascript/flavours/glitch/components/form_fields/copy_link_field.tsx b/app/javascript/flavours/glitch/components/form_fields/copy_link_field.tsx index b12bb8f5ad..6404e5ed56 100644 --- a/app/javascript/flavours/glitch/components/form_fields/copy_link_field.tsx +++ b/app/javascript/flavours/glitch/components/form_fields/copy_link_field.tsx @@ -22,7 +22,7 @@ interface CopyLinkFieldProps extends CommonFieldWrapperProps, TextInputProps { export const CopyLinkField = forwardRef( ( - { id, label, hint, hasError, value, required, className, ...otherProps }, + { id, label, hint, status, value, required, className, ...otherProps }, ref, ) => { const intl = useIntl(); @@ -48,7 +48,7 @@ export const CopyLinkField = forwardRef( label={label} hint={hint} required={required} - hasError={hasError} + status={status} inputId={id} > {(inputProps) => ( diff --git a/app/javascript/flavours/glitch/components/form_fields/emoji_text_field.tsx b/app/javascript/flavours/glitch/components/form_fields/emoji_text_field.tsx index 93deaf0dd6..5eb3e24ae4 100644 --- a/app/javascript/flavours/glitch/components/form_fields/emoji_text_field.tsx +++ b/app/javascript/flavours/glitch/components/form_fields/emoji_text_field.tsx @@ -37,7 +37,7 @@ export const EmojiTextInputField: FC< value, label, hint, - hasError, + status, maxLength, counterMax = maxLength, recommended, @@ -49,7 +49,7 @@ export const EmojiTextInputField: FC< const wrapperProps = { label, hint, - hasError, + status, counterMax, recommended, disabled, @@ -84,7 +84,7 @@ export const EmojiTextAreaField: FC< recommended, disabled, hint, - hasError, + status, ...otherProps }) => { const textareaRef = useRef(null); @@ -92,7 +92,7 @@ export const EmojiTextAreaField: FC< const wrapperProps = { label, hint, - hasError, + status, counterMax, recommended, disabled, diff --git a/app/javascript/flavours/glitch/components/form_fields/fieldset.module.scss b/app/javascript/flavours/glitch/components/form_fields/fieldset.module.scss index f222762af5..2751b3c8a0 100644 --- a/app/javascript/flavours/glitch/components/form_fields/fieldset.module.scss +++ b/app/javascript/flavours/glitch/components/form_fields/fieldset.module.scss @@ -1,7 +1,9 @@ .fieldset { + --container-gap: 12px; + display: flex; flex-direction: column; - gap: 12px; + gap: var(--container-gap); color: var(--color-text-primary); font-size: 15px; } @@ -17,3 +19,11 @@ column-gap: 24px; } } + +.status { + // If there's no content, we need to compensate for the parent's + // flex gap to avoid extra spacing below the field. + &:empty { + margin-top: calc(-1 * var(--container-gap)); + } +} diff --git a/app/javascript/flavours/glitch/components/form_fields/fieldset.tsx b/app/javascript/flavours/glitch/components/form_fields/fieldset.tsx index d52a95130b..6a8aa8cccd 100644 --- a/app/javascript/flavours/glitch/components/form_fields/fieldset.tsx +++ b/app/javascript/flavours/glitch/components/form_fields/fieldset.tsx @@ -3,14 +3,19 @@ import type { ReactNode, FC } from 'react'; import { createContext, useId } from 'react'; +import { A11yLiveRegion } from 'flavours/glitch/components/a11y_live_region'; +import type { FieldStatus } from 'flavours/glitch/components/callout_inline'; +import { CalloutInline } from 'flavours/glitch/components/callout_inline'; + import classes from './fieldset.module.scss'; +import { getFieldStatus } from './form_field_wrapper'; import formFieldWrapperClasses from './form_field_wrapper.module.scss'; interface FieldsetProps { legend: ReactNode; hint?: ReactNode; name?: string; - hasError?: boolean; + status?: FieldStatus | FieldStatus['variant']; layout?: 'vertical' | 'horizontal'; children: ReactNode; } @@ -26,22 +31,33 @@ export const Fieldset: FC = ({ legend, hint, name, - hasError, + status, layout, children, }) => { const uniqueId = useId(); const labelId = `${uniqueId}-label`; const hintId = `${uniqueId}-hint`; + const statusId = `${uniqueId}-status`; const fieldsetName = name || `${uniqueId}-fieldset-name`; const hasHint = !!hint; + const fieldStatus = getFieldStatus(status); + const hasStatusMessage = !!fieldStatus?.message; + + const descriptionIds = [ + hasHint ? hintId : '', + hasStatusMessage ? statusId : '', + ] + .filter((id) => !!id) + .join(' '); + return (
@@ -59,6 +75,11 @@ export const Fieldset: FC = ({ {children}
+ + {/* Live region must be rendered even when empty */} + + {hasStatusMessage && } +
); }; diff --git a/app/javascript/flavours/glitch/components/form_fields/form_field_wrapper.module.scss b/app/javascript/flavours/glitch/components/form_fields/form_field_wrapper.module.scss index faeb48aae4..cff93be8a6 100644 --- a/app/javascript/flavours/glitch/components/form_fields/form_field_wrapper.module.scss +++ b/app/javascript/flavours/glitch/components/form_fields/form_field_wrapper.module.scss @@ -46,6 +46,14 @@ font-size: 13px; } +.status { + // If there's no content, we need to compensate for the parent's + // flex gap to avoid extra spacing below the field. + &:empty { + margin-top: calc(-1 * var(--form-field-label-gap)); + } +} + .inputWrapper { display: block; } diff --git a/app/javascript/flavours/glitch/components/form_fields/form_field_wrapper.tsx b/app/javascript/flavours/glitch/components/form_fields/form_field_wrapper.tsx index 6454153ab8..24ec3764ee 100644 --- a/app/javascript/flavours/glitch/components/form_fields/form_field_wrapper.tsx +++ b/app/javascript/flavours/glitch/components/form_fields/form_field_wrapper.tsx @@ -7,6 +7,10 @@ import { FormattedMessage } from 'react-intl'; import classNames from 'classnames'; +import { A11yLiveRegion } from 'flavours/glitch/components/a11y_live_region'; +import type { FieldStatus } from 'flavours/glitch/components/callout_inline'; +import { CalloutInline } from 'flavours/glitch/components/callout_inline'; + import { FieldsetNameContext } from './fieldset'; import classes from './form_field_wrapper.module.scss'; @@ -20,7 +24,7 @@ interface FieldWrapperProps { label: ReactNode; hint?: ReactNode; required?: boolean; - hasError?: boolean; + status?: FieldStatus['variant'] | FieldStatus; inputId?: string; describedById?: string; inputPlacement?: 'inline-start' | 'inline-end'; @@ -33,7 +37,7 @@ interface FieldWrapperProps { */ export type CommonFieldWrapperProps = Pick< FieldWrapperProps, - 'label' | 'hint' | 'hasError' + 'label' | 'hint' | 'status' > & { wrapperClassName?: string }; /** @@ -48,27 +52,31 @@ export const FormFieldWrapper: FC = ({ hint, describedById, required, - hasError, + status, inputPlacement, children, className, }) => { const uniqueId = useId(); const inputId = inputIdProp || `${uniqueId}-input`; + const statusId = `${inputIdProp || uniqueId}-status`; const hintId = `${inputIdProp || uniqueId}-hint`; const hasHint = !!hint; + const fieldStatus = getFieldStatus(status); + const hasStatusMessage = !!fieldStatus?.message; const hasParentFieldset = !!useContext(FieldsetNameContext); + const descriptionIds = + [hasHint ? hintId : '', hasStatusMessage ? statusId : '', describedById] + .filter((id) => !!id) + .join(' ') || undefined; + const inputProps: InputProps = { required, id: inputId, + 'aria-describedby': descriptionIds, }; - if (hasHint) { - inputProps['aria-describedby'] = describedById - ? `${describedById} ${hintId}` - : hintId; - } const input = (
{children(inputProps)}
@@ -77,7 +85,7 @@ export const FormFieldWrapper: FC = ({ return (
{inputPlacement === 'inline-start' && input} @@ -100,6 +108,11 @@ export const FormFieldWrapper: FC = ({
{inputPlacement !== 'inline-start' && input} + + {/* Live region must be rendered even when empty */} + + {hasStatusMessage && } + ); }; @@ -121,3 +134,19 @@ const RequiredMark: FC<{ required?: boolean }> = ({ required }) => ); + +export function getFieldStatus(status: FieldWrapperProps['status']) { + if (!status) { + return null; + } + + if (typeof status === 'string') { + const fieldStatus: FieldStatus = { + variant: status, + message: '', + }; + return fieldStatus; + } + + return status; +} diff --git a/app/javascript/flavours/glitch/components/form_fields/radio_button_field.stories.tsx b/app/javascript/flavours/glitch/components/form_fields/radio_button_field.stories.tsx index 95687abff3..1292b85724 100644 --- a/app/javascript/flavours/glitch/components/form_fields/radio_button_field.stories.tsx +++ b/app/javascript/flavours/glitch/components/form_fields/radio_button_field.stories.tsx @@ -71,7 +71,7 @@ export const Optional: Story = { export const WithError: Story = { args: { required: false, - hasError: true, + status: 'error', }, }; diff --git a/app/javascript/flavours/glitch/components/form_fields/radio_button_field.tsx b/app/javascript/flavours/glitch/components/form_fields/radio_button_field.tsx index 51f52168e0..cbc9020ca7 100644 --- a/app/javascript/flavours/glitch/components/form_fields/radio_button_field.tsx +++ b/app/javascript/flavours/glitch/components/form_fields/radio_button_field.tsx @@ -15,7 +15,7 @@ type Props = Omit, 'type'> & { export const RadioButtonField = forwardRef< HTMLInputElement, Props & CommonFieldWrapperProps ->(({ id, label, hint, hasError, required, ...otherProps }, ref) => { +>(({ id, label, hint, status, required, ...otherProps }, ref) => { const fieldsetName = useContext(FieldsetNameContext); return ( @@ -23,7 +23,7 @@ export const RadioButtonField = forwardRef< label={label} hint={hint} required={required} - hasError={hasError} + status={status} inputId={id} inputPlacement='inline-start' > diff --git a/app/javascript/flavours/glitch/components/form_fields/range_input.module.scss b/app/javascript/flavours/glitch/components/form_fields/range_input.module.scss new file mode 100644 index 0000000000..cbace07dcc --- /dev/null +++ b/app/javascript/flavours/glitch/components/form_fields/range_input.module.scss @@ -0,0 +1,128 @@ +/* + Inspired by: + https://danielstern.ca/range.css + https://css-tricks.com/styling-cross-browser-compatible-range-inputs-css/ +*/ + +.input { + --color-bg-thumb: var(--color-bg-brand-base); + --color-bg-thumb-hover: var(--color-bg-brand-base-hover); + --color-bg-track: var(--color-bg-secondary); + + width: 100%; + margin: 6px 0; + background-color: transparent; + appearance: none; + + &:focus { + outline: none; + } + + // Thumb + + &::-webkit-slider-thumb { + margin-top: -6px; + width: 16px; + height: 16px; + background: var(--color-bg-thumb); + border: 0; + border-radius: 50px; + cursor: pointer; + -webkit-appearance: none; + } + + &::-moz-range-thumb { + width: 16px; + height: 16px; + background: var(--color-bg-thumb); + border: 0; + border-radius: 50px; + cursor: pointer; + } + + &::-ms-thumb { + width: 16px; + height: 16px; + background: var(--color-bg-thumb); + border: 0; + border-radius: 50px; + cursor: pointer; + margin-top: 0; // Needed to keep the Edge thumb centred + } + + &:focus, + &:hover { + &::-webkit-slider-thumb { + background: var(--color-bg-thumb-hover); + } + + &::-moz-range-thumb { + background: var(--color-bg-thumb-hover); + } + + &::-ms-thumb { + background: var(--color-bg-thumb-hover); + } + } + + &:focus-visible { + &::-webkit-slider-thumb { + outline: var(--outline-focus-default); + outline-offset: 2px; + } + + &::-moz-range-thumb { + outline: var(--outline-focus-default); + outline-offset: 2px; + } + + &::-ms-thumb { + outline: var(--outline-focus-default); + outline-offset: 2px; + } + } + + // Track + + &::-webkit-slider-runnable-track { + background: var(--color-bg-track); + border: 0; + border-radius: 1.3px; + width: 100%; + height: 4px; + cursor: pointer; + } + + &::-moz-range-track { + background: var(--color-bg-track); + border: 0; + border-radius: 1.3px; + width: 100%; + height: 4px; + cursor: pointer; + } + + &::-ms-track { + background: var(--color-bg-track); + border: 0; + color: transparent; + width: 100%; + height: 4px; + cursor: pointer; + } +} + +.markers { + display: flex; + flex-direction: column; + justify-content: space-between; + writing-mode: vertical-lr; + width: 100%; + font-size: 11px; + color: var(--color-text-secondary); + user-select: none; + + option { + padding: 0; + } +} diff --git a/app/javascript/flavours/glitch/components/form_fields/range_input_field.stories.tsx b/app/javascript/flavours/glitch/components/form_fields/range_input_field.stories.tsx new file mode 100644 index 0000000000..672228ab8c --- /dev/null +++ b/app/javascript/flavours/glitch/components/form_fields/range_input_field.stories.tsx @@ -0,0 +1,32 @@ +import type { Meta, StoryObj } from '@storybook/react-vite'; + +import { RangeInputField } from './range_input_field'; + +const meta = { + title: 'Components/Form Fields/RangeInputField', + component: RangeInputField, + args: { + label: 'Label', + hint: 'This is a description of this form field', + checked: false, + disabled: false, + }, +} satisfies Meta; + +export default meta; + +type Story = StoryObj; + +export const Simple: Story = {}; + +export const Markers: Story = { + args: { + markers: [ + { value: 0, label: 'None' }, + { value: 25, label: 'Some' }, + { value: 50, label: 'Half' }, + { value: 75, label: 'Most' }, + { value: 100, label: 'All' }, + ], + }, +}; diff --git a/app/javascript/flavours/glitch/components/form_fields/range_input_field.tsx b/app/javascript/flavours/glitch/components/form_fields/range_input_field.tsx new file mode 100644 index 0000000000..8fb2620339 --- /dev/null +++ b/app/javascript/flavours/glitch/components/form_fields/range_input_field.tsx @@ -0,0 +1,86 @@ +import type { ComponentPropsWithoutRef } from 'react'; +import { forwardRef, useId } from 'react'; + +import classNames from 'classnames'; + +import { FormFieldWrapper } from './form_field_wrapper'; +import type { CommonFieldWrapperProps } from './form_field_wrapper'; +import classes from './range_input.module.scss'; + +export type RangeInputProps = Omit< + ComponentPropsWithoutRef<'input'>, + 'type' | 'list' +> & { + markers?: { value: number; label: string }[] | number[]; +}; + +interface Props extends RangeInputProps, CommonFieldWrapperProps {} + +/** + * A simple form field for single-line text. + * + * Accepts an optional `hint` and can be marked as required + * or optional (by explicitly setting `required={false}`) + */ + +export const RangeInputField = forwardRef( + ( + { id, label, hint, status, required, wrapperClassName, ...otherProps }, + ref, + ) => ( + + {(inputProps) => } + + ), +); + +RangeInputField.displayName = 'RangeInputField'; + +export const RangeInput = forwardRef( + ({ className, markers, id, ...otherProps }, ref) => { + const markersId = useId(); + + if (!markers) { + return ( + + ); + } + return ( + <> + + + {markers.map((marker) => { + const value = typeof marker === 'number' ? marker : marker.value; + return ( + + + ); + }, +); + +RangeInput.displayName = 'RangeInput'; diff --git a/app/javascript/flavours/glitch/components/form_fields/select_field.stories.tsx b/app/javascript/flavours/glitch/components/form_fields/select_field.stories.tsx index 469238dd44..c215a6e04a 100644 --- a/app/javascript/flavours/glitch/components/form_fields/select_field.stories.tsx +++ b/app/javascript/flavours/glitch/components/form_fields/select_field.stories.tsx @@ -51,7 +51,7 @@ export const Optional: Story = { export const WithError: Story = { args: { required: false, - hasError: true, + status: 'error', }, }; diff --git a/app/javascript/flavours/glitch/components/form_fields/select_field.tsx b/app/javascript/flavours/glitch/components/form_fields/select_field.tsx index 59854b578e..7c1bfdf47d 100644 --- a/app/javascript/flavours/glitch/components/form_fields/select_field.tsx +++ b/app/javascript/flavours/glitch/components/form_fields/select_field.tsx @@ -19,12 +19,12 @@ interface Props */ export const SelectField = forwardRef( - ({ id, label, hint, required, hasError, children, ...otherProps }, ref) => ( + ({ id, label, hint, required, status, children, ...otherProps }, ref) => ( {(inputProps) => ( diff --git a/app/javascript/flavours/glitch/components/form_fields/text_area_field.stories.tsx b/app/javascript/flavours/glitch/components/form_fields/text_area_field.stories.tsx index 190239aee2..f06d7bbdcf 100644 --- a/app/javascript/flavours/glitch/components/form_fields/text_area_field.stories.tsx +++ b/app/javascript/flavours/glitch/components/form_fields/text_area_field.stories.tsx @@ -38,7 +38,17 @@ export const Optional: Story = { export const WithError: Story = { args: { required: false, - hasError: true, + status: { variant: 'error', message: "This field can't be empty" }, + }, +}; + +export const WithWarning: Story = { + args: { + required: false, + status: { + variant: 'warning', + message: 'Special characters are not allowed', + }, }, }; diff --git a/app/javascript/flavours/glitch/components/form_fields/text_area_field.tsx b/app/javascript/flavours/glitch/components/form_fields/text_area_field.tsx index 1e4bacc041..1284aa9276 100644 --- a/app/javascript/flavours/glitch/components/form_fields/text_area_field.tsx +++ b/app/javascript/flavours/glitch/components/form_fields/text_area_field.tsx @@ -26,14 +26,14 @@ export const TextAreaField = forwardRef< TextAreaProps & CommonFieldWrapperProps >( ( - { id, label, hint, required, hasError, wrapperClassName, ...otherProps }, + { id, label, hint, required, status, wrapperClassName, ...otherProps }, ref, ) => ( diff --git a/app/javascript/flavours/glitch/components/form_fields/text_input.module.scss b/app/javascript/flavours/glitch/components/form_fields/text_input.module.scss index 289ff1333a..f432f57055 100644 --- a/app/javascript/flavours/glitch/components/form_fields/text_input.module.scss +++ b/app/javascript/flavours/glitch/components/form_fields/text_input.module.scss @@ -29,16 +29,16 @@ color: var(--color-text-secondary); } - &:focus { - outline-color: var(--color-text-brand); - } - &:focus:user-invalid, &:required:user-invalid, [data-has-error='true'] & { outline-color: var(--color-text-error); } + &:focus { + outline-color: var(--color-text-brand); + } + &:required:user-valid { outline-color: var(--color-text-success); } diff --git a/app/javascript/flavours/glitch/components/form_fields/text_input_field.stories.tsx b/app/javascript/flavours/glitch/components/form_fields/text_input_field.stories.tsx index 8e8d7e9923..702597a0c1 100644 --- a/app/javascript/flavours/glitch/components/form_fields/text_input_field.stories.tsx +++ b/app/javascript/flavours/glitch/components/form_fields/text_input_field.stories.tsx @@ -40,7 +40,17 @@ export const Optional: Story = { export const WithError: Story = { args: { required: false, - hasError: true, + status: 'error', + }, +}; + +export const WithWarning: Story = { + args: { + required: false, + status: { + variant: 'warning', + message: 'Special characters are not allowed', + }, }, }; diff --git a/app/javascript/flavours/glitch/components/form_fields/text_input_field.tsx b/app/javascript/flavours/glitch/components/form_fields/text_input_field.tsx index 7c61bd91ef..2a47b33db9 100644 --- a/app/javascript/flavours/glitch/components/form_fields/text_input_field.tsx +++ b/app/javascript/flavours/glitch/components/form_fields/text_input_field.tsx @@ -25,14 +25,14 @@ interface Props extends TextInputProps, CommonFieldWrapperProps {} export const TextInputField = forwardRef( ( - { id, label, hint, hasError, required, wrapperClassName, ...otherProps }, + { id, label, hint, status, required, wrapperClassName, ...otherProps }, ref, ) => ( diff --git a/app/javascript/flavours/glitch/components/form_fields/toggle_field.stories.tsx b/app/javascript/flavours/glitch/components/form_fields/toggle_field.stories.tsx index 924c18aa74..295600a3fd 100644 --- a/app/javascript/flavours/glitch/components/form_fields/toggle_field.stories.tsx +++ b/app/javascript/flavours/glitch/components/form_fields/toggle_field.stories.tsx @@ -45,7 +45,7 @@ export const Optional: Story = { export const WithError: Story = { args: { required: false, - hasError: true, + status: 'error', }, }; diff --git a/app/javascript/flavours/glitch/components/form_fields/toggle_field.tsx b/app/javascript/flavours/glitch/components/form_fields/toggle_field.tsx index 6cafbcdc36..75fdb8f21b 100644 --- a/app/javascript/flavours/glitch/components/form_fields/toggle_field.tsx +++ b/app/javascript/flavours/glitch/components/form_fields/toggle_field.tsx @@ -14,12 +14,12 @@ type Props = Omit, 'type'> & { export const ToggleField = forwardRef< HTMLInputElement, Props & CommonFieldWrapperProps ->(({ id, label, hint, hasError, required, ...otherProps }, ref) => ( +>(({ id, label, hint, status, required, ...otherProps }, ref) => ( diff --git a/app/javascript/flavours/glitch/features/account_timeline/components/redesign.module.scss b/app/javascript/flavours/glitch/features/account_timeline/components/redesign.module.scss index 0d9036265a..e761c761d9 100644 --- a/app/javascript/flavours/glitch/features/account_timeline/components/redesign.module.scss +++ b/app/javascript/flavours/glitch/features/account_timeline/components/redesign.module.scss @@ -107,7 +107,7 @@ } $button-breakpoint: 420px; -$button-fallback-breakpoint: #{$button-breakpoint} + 55px; +$button-fallback-breakpoint: $button-breakpoint + 55px; .buttonsDesktop { @container (width < #{$button-breakpoint}) { @@ -132,7 +132,7 @@ $button-fallback-breakpoint: #{$button-breakpoint} + 55px; } @supports (not (container-type: inline-size)) { - @media (min-width: (#{$button-fallback-breakpoint} + 1px)) { + @media (min-width: ($button-fallback-breakpoint + 1px)) { display: none; } } diff --git a/app/javascript/flavours/glitch/features/account_timeline/modals/note_modal.tsx b/app/javascript/flavours/glitch/features/account_timeline/modals/note_modal.tsx index 3c55d15024..2a45d5fab4 100644 --- a/app/javascript/flavours/glitch/features/account_timeline/modals/note_modal.tsx +++ b/app/javascript/flavours/glitch/features/account_timeline/modals/note_modal.tsx @@ -141,8 +141,11 @@ const InnerNodeModal: FC<{ onChange={handleChange} label={intl.formatMessage(messages.fieldLabel)} className={classes.noteInput} - hasError={state === 'error'} - hint={errorText} + status={ + state === 'error' + ? { variant: 'error', message: errorText } + : undefined + } // eslint-disable-next-line jsx-a11y/no-autofocus -- We want to focus here as it's a modal. autoFocus /> diff --git a/app/javascript/flavours/glitch/features/collections/editor/details.tsx b/app/javascript/flavours/glitch/features/collections/editor/details.tsx index 4e886f985a..22dda49795 100644 --- a/app/javascript/flavours/glitch/features/collections/editor/details.tsx +++ b/app/javascript/flavours/glitch/features/collections/editor/details.tsx @@ -1,12 +1,15 @@ -import { useCallback } from 'react'; +import { useCallback, useMemo } from 'react'; -import { FormattedMessage } from 'react-intl'; +import { FormattedMessage, useIntl } from 'react-intl'; import { useHistory } from 'react-router-dom'; import { isFulfilled } from '@reduxjs/toolkit'; -import { inputToHashtag } from '@/flavours/glitch/utils/hashtags'; +import { + hasSpecialCharacters, + inputToHashtag, +} from '@/flavours/glitch/utils/hashtags'; import type { ApiCreateCollectionPayload, ApiUpdateCollectionPayload, @@ -31,6 +34,7 @@ import classes from './styles.module.scss'; import { WizardStepHeader } from './wizard_step_header'; export const CollectionDetails: React.FC = () => { + const intl = useIntl(); const dispatch = useAppDispatch(); const history = useHistory(); const { id, name, description, topic, discoverable, sensitive, accountIds } = @@ -152,6 +156,11 @@ export const CollectionDetails: React.FC = () => { ], ); + const topicHasSpecialCharacters = useMemo( + () => hasSpecialCharacters(topic), + [topic], + ); + return (
@@ -224,6 +233,18 @@ export const CollectionDetails: React.FC = () => { autoCorrect='off' spellCheck='false' maxLength={40} + status={ + topicHasSpecialCharacters + ? { + variant: 'warning', + message: intl.formatMessage({ + id: 'collections.topic_special_chars_hint', + defaultMessage: + 'Special characters will be removed when saving', + }), + } + : undefined + } />
@@ -255,7 +255,7 @@ export const Profile: React.FC<{ } value={note} onChange={handleNoteChange} - hasError={!!errors?.note} + status={errors?.note ? 'error' : undefined} id='note' /> diff --git a/app/javascript/flavours/glitch/reducers/slices/collections.ts b/app/javascript/flavours/glitch/reducers/slices/collections.ts index 5db5530894..8d0e9ad147 100644 --- a/app/javascript/flavours/glitch/reducers/slices/collections.ts +++ b/app/javascript/flavours/glitch/reducers/slices/collections.ts @@ -23,6 +23,7 @@ import { createAppSelector, createDataLoadingThunk, } from '@/flavours/glitch/store/typed_functions'; +import { inputToHashtag } from '@/flavours/glitch/utils/hashtags'; type QueryStatus = 'idle' | 'loading' | 'error'; @@ -82,7 +83,7 @@ const collectionSlice = createSlice({ id: collection?.id ?? null, name: collection?.name ?? '', description: collection?.description ?? '', - topic: collection?.tag?.name ?? '', + topic: inputToHashtag(collection?.tag?.name ?? ''), language: collection?.language ?? '', discoverable: collection?.discoverable ?? true, sensitive: collection?.sensitive ?? false, diff --git a/app/javascript/flavours/glitch/styles/mastodon/components.scss b/app/javascript/flavours/glitch/styles/mastodon/components.scss index a0fb345833..da20098f3a 100644 --- a/app/javascript/flavours/glitch/styles/mastodon/components.scss +++ b/app/javascript/flavours/glitch/styles/mastodon/components.scss @@ -8763,7 +8763,7 @@ noscript { gap: 8px; $button-breakpoint: 420px; - $button-fallback-breakpoint: #{$button-breakpoint} + 55px; + $button-fallback-breakpoint: $button-breakpoint + 55px; &--desktop { margin-top: 55px; @@ -8787,7 +8787,7 @@ noscript { } @supports (not (container-type: inline-size)) { - @media (min-width: (#{$button-fallback-breakpoint} + 1px)) { + @media (min-width: ($button-fallback-breakpoint + 1px)) { display: none; } } diff --git a/app/javascript/flavours/glitch/styles/mastodon/containers.scss b/app/javascript/flavours/glitch/styles/mastodon/containers.scss index 57c62a29e3..5e199273e0 100644 --- a/app/javascript/flavours/glitch/styles/mastodon/containers.scss +++ b/app/javascript/flavours/glitch/styles/mastodon/containers.scss @@ -13,28 +13,28 @@ .logo-container { margin: 50px auto; - h1 { + a { display: flex; justify-content: center; align-items: center; + width: min-content; + margin: 0 auto; + padding: 12px 16px; + color: var(--color-text-primary); + text-decoration: none; + outline: 0; + line-height: 32px; + font-weight: 500; + font-size: 14px; - .logo { - height: 42px; - margin-inline-end: 10px; + &:focus-visible { + outline: var(--outline-focus-default); } + } - a { - display: flex; - justify-content: center; - align-items: center; - color: var(--color-text-primary); - text-decoration: none; - outline: 0; - padding: 12px 16px; - line-height: 32px; - font-weight: 500; - font-size: 14px; - } + .logo { + height: 42px; + margin-inline-end: 10px; } } diff --git a/app/javascript/flavours/glitch/styles/mastodon/forms.scss b/app/javascript/flavours/glitch/styles/mastodon/forms.scss index 09a5f9721e..b96ecca18a 100644 --- a/app/javascript/flavours/glitch/styles/mastodon/forms.scss +++ b/app/javascript/flavours/glitch/styles/mastodon/forms.scss @@ -77,7 +77,6 @@ code { .input { margin-bottom: 16px; - overflow: hidden; &:last-child { margin-bottom: 0; @@ -472,13 +471,19 @@ code { } } - .input.radio_buttons .radio label { - margin-bottom: 5px; - font-family: inherit; - font-size: 14px; - color: var(--color-text-primary); - display: block; - width: auto; + .input.radio_buttons .radio { + label { + margin-bottom: 5px; + font-family: inherit; + font-size: 14px; + color: var(--color-text-primary); + display: block; + width: auto; + } + + input[type='radio'] { + accent-color: var(--color-text-brand); + } } .check_boxes { @@ -504,6 +509,12 @@ code { } } + label.checkbox { + input[type='checkbox'] { + accent-color: var(--color-text-brand); + } + } + .input.static .label_input__wrapper { font-size: 14px; padding: 10px; @@ -524,13 +535,20 @@ code { color: var(--color-text-primary); display: block; width: 100%; - outline: 0; font-family: inherit; resize: vertical; background: var(--color-bg-secondary); border: 1px solid var(--color-border-primary); border-radius: 4px; padding: 10px 16px; + outline: var(--outline-focus-default); + outline-offset: -2px; + outline-color: transparent; + transition: outline-color 0.15s ease-out; + + &:focus { + outline: var(--outline-focus-default); + } &:invalid { box-shadow: none; @@ -614,6 +632,11 @@ code { margin-inline-end: 0; } + &:focus-visible { + outline: var(--outline-focus-default); + outline-offset: 2px; + } + &:active, &:focus, &:hover { @@ -654,6 +677,11 @@ code { padding-inline-end: 30px; height: 41px; + &:focus-visible { + outline: var(--outline-focus-default); + outline-offset: 2px; + } + @media screen and (width <= 600px) { font-size: 16px; } diff --git a/app/javascript/flavours/glitch/utils/hashtags.ts b/app/javascript/flavours/glitch/utils/hashtags.ts index d14efe5db3..ff90a88465 100644 --- a/app/javascript/flavours/glitch/utils/hashtags.ts +++ b/app/javascript/flavours/glitch/utils/hashtags.ts @@ -59,3 +59,8 @@ export const inputToHashtag = (input: string): string => { return `#${words.join('')}${trailingSpace}`; }; + +export const hasSpecialCharacters = (input: string) => { + // Regex matches any character NOT a letter/digit, except the # + return /[^a-zA-Z0-9# ]/.test(input); +}; diff --git a/app/javascript/mastodon/components/a11y_live_region/a11y_live_region.stories.tsx b/app/javascript/mastodon/components/a11y_live_region/a11y_live_region.stories.tsx new file mode 100644 index 0000000000..00804d685b --- /dev/null +++ b/app/javascript/mastodon/components/a11y_live_region/a11y_live_region.stories.tsx @@ -0,0 +1,25 @@ +import type { Meta, StoryObj } from '@storybook/react-vite'; + +import { A11yLiveRegion } from '.'; + +const meta = { + title: 'Components/A11yLiveRegion', + component: A11yLiveRegion, +} satisfies Meta; + +export default meta; + +type Story = StoryObj; + +export const Polite: Story = { + args: { + children: "This field can't be empty.", + }, +}; + +export const Assertive: Story = { + args: { + ...Polite.args, + role: 'alert', + }, +}; diff --git a/app/javascript/mastodon/components/a11y_live_region/index.tsx b/app/javascript/mastodon/components/a11y_live_region/index.tsx new file mode 100644 index 0000000000..51fee5e4b9 --- /dev/null +++ b/app/javascript/mastodon/components/a11y_live_region/index.tsx @@ -0,0 +1,28 @@ +import { polymorphicForwardRef } from '@/types/polymorphic'; + +/** + * A live region is a content region that announces changes of its contents + * to users of assistive technology like screen readers. + * + * Dynamically added warnings, errors, or live status updates should be wrapped + * in a live region to ensure they are not missed when they appear. + * + * **Important:** + * Live regions must be present in the DOM _before_ + * the to-be announced content is rendered into it. + */ + +export const A11yLiveRegion = polymorphicForwardRef<'div'>( + ({ role = 'status', as: Component = 'div', children, ...props }, ref) => { + return ( + + {children} + + ); + }, +); diff --git a/app/javascript/mastodon/components/callout_inline/callout_inline.stories.tsx b/app/javascript/mastodon/components/callout_inline/callout_inline.stories.tsx new file mode 100644 index 0000000000..f18af41dc0 --- /dev/null +++ b/app/javascript/mastodon/components/callout_inline/callout_inline.stories.tsx @@ -0,0 +1,39 @@ +import type { Meta, StoryObj } from '@storybook/react-vite'; + +import { CalloutInline } from '.'; + +const meta = { + title: 'Components/CalloutInline', + args: { + children: 'Contents here', + }, + component: CalloutInline, +} satisfies Meta; + +export default meta; + +type Story = StoryObj; + +export const Error: Story = { + args: { + variant: 'error', + }, +}; + +export const Warning: Story = { + args: { + variant: 'warning', + }, +}; + +export const Success: Story = { + args: { + variant: 'success', + }, +}; + +export const Info: Story = { + args: { + variant: 'info', + }, +}; diff --git a/app/javascript/mastodon/components/callout_inline/index.tsx b/app/javascript/mastodon/components/callout_inline/index.tsx new file mode 100644 index 0000000000..e2e6791963 --- /dev/null +++ b/app/javascript/mastodon/components/callout_inline/index.tsx @@ -0,0 +1,39 @@ +import type { FC } from 'react'; + +import classNames from 'classnames'; + +import CheckIcon from '@/material-icons/400-24px/check.svg?react'; +import ErrorIcon from '@/material-icons/400-24px/error.svg?react'; +import InfoIcon from '@/material-icons/400-24px/info.svg?react'; +import WarningIcon from '@/material-icons/400-24px/warning.svg?react'; + +import { Icon } from '../icon'; + +import classes from './styles.module.css'; + +export interface FieldStatus { + variant: 'error' | 'warning' | 'info' | 'success'; + message?: string; +} + +const iconMap: Record = { + error: ErrorIcon, + warning: WarningIcon, + info: InfoIcon, + success: CheckIcon, +}; + +export const CalloutInline: FC< + Partial & React.ComponentPropsWithoutRef<'div'> +> = ({ variant = 'error', message, className, children, ...props }) => { + return ( +
+ + {message ?? children} +
+ ); +}; diff --git a/app/javascript/mastodon/components/callout_inline/styles.module.css b/app/javascript/mastodon/components/callout_inline/styles.module.css new file mode 100644 index 0000000000..8d32f7df9b --- /dev/null +++ b/app/javascript/mastodon/components/callout_inline/styles.module.css @@ -0,0 +1,29 @@ +.wrapper { + display: flex; + align-items: start; + gap: 4px; + font-size: 13px; + font-weight: 500; + + &[data-variant='success'] { + color: var(--color-text-success); + } + + &[data-variant='warning'] { + color: var(--color-text-warning); + } + + &[data-variant='error'] { + color: var(--color-text-error); + } + + &[data-variant='info'] { + color: var(--color-text-primary); + } +} + +.icon { + width: 16px; + height: 16px; + margin-top: 1px; +} diff --git a/app/javascript/mastodon/components/form_fields/checkbox_field.stories.tsx b/app/javascript/mastodon/components/form_fields/checkbox_field.stories.tsx index 4d208cf21b..16b3a53f0b 100644 --- a/app/javascript/mastodon/components/form_fields/checkbox_field.stories.tsx +++ b/app/javascript/mastodon/components/form_fields/checkbox_field.stories.tsx @@ -76,7 +76,7 @@ export const Optional: Story = { export const WithError: Story = { args: { required: false, - hasError: true, + status: 'error', }, }; diff --git a/app/javascript/mastodon/components/form_fields/checkbox_field.tsx b/app/javascript/mastodon/components/form_fields/checkbox_field.tsx index 2b6933c847..c08b81ca36 100644 --- a/app/javascript/mastodon/components/form_fields/checkbox_field.tsx +++ b/app/javascript/mastodon/components/form_fields/checkbox_field.tsx @@ -13,12 +13,12 @@ type Props = Omit, 'type'> & { export const CheckboxField = forwardRef< HTMLInputElement, Props & CommonFieldWrapperProps ->(({ id, label, hint, hasError, required, ...otherProps }, ref) => ( +>(({ id, label, hint, status, required, ...otherProps }, ref) => ( diff --git a/app/javascript/mastodon/components/form_fields/combobox_field.tsx b/app/javascript/mastodon/components/form_fields/combobox_field.tsx index 89193ed9d5..8ce7161657 100644 --- a/app/javascript/mastodon/components/form_fields/combobox_field.tsx +++ b/app/javascript/mastodon/components/form_fields/combobox_field.tsx @@ -86,14 +86,14 @@ interface Props */ export const ComboboxFieldWithRef = ( - { id, label, hint, hasError, required, ...otherProps }: Props, + { id, label, hint, status, required, ...otherProps }: Props, ref: React.ForwardedRef, ) => ( {(inputProps) => } diff --git a/app/javascript/mastodon/components/form_fields/copy_link_field.tsx b/app/javascript/mastodon/components/form_fields/copy_link_field.tsx index ad93e3a065..d772315ade 100644 --- a/app/javascript/mastodon/components/form_fields/copy_link_field.tsx +++ b/app/javascript/mastodon/components/form_fields/copy_link_field.tsx @@ -22,7 +22,7 @@ interface CopyLinkFieldProps extends CommonFieldWrapperProps, TextInputProps { export const CopyLinkField = forwardRef( ( - { id, label, hint, hasError, value, required, className, ...otherProps }, + { id, label, hint, status, value, required, className, ...otherProps }, ref, ) => { const intl = useIntl(); @@ -48,7 +48,7 @@ export const CopyLinkField = forwardRef( label={label} hint={hint} required={required} - hasError={hasError} + status={status} inputId={id} > {(inputProps) => ( diff --git a/app/javascript/mastodon/components/form_fields/emoji_text_field.tsx b/app/javascript/mastodon/components/form_fields/emoji_text_field.tsx index a57c4d1dd4..af9e3d5280 100644 --- a/app/javascript/mastodon/components/form_fields/emoji_text_field.tsx +++ b/app/javascript/mastodon/components/form_fields/emoji_text_field.tsx @@ -37,7 +37,7 @@ export const EmojiTextInputField: FC< value, label, hint, - hasError, + status, maxLength, counterMax = maxLength, recommended, @@ -49,7 +49,7 @@ export const EmojiTextInputField: FC< const wrapperProps = { label, hint, - hasError, + status, counterMax, recommended, disabled, @@ -84,7 +84,7 @@ export const EmojiTextAreaField: FC< recommended, disabled, hint, - hasError, + status, ...otherProps }) => { const textareaRef = useRef(null); @@ -92,7 +92,7 @@ export const EmojiTextAreaField: FC< const wrapperProps = { label, hint, - hasError, + status, counterMax, recommended, disabled, diff --git a/app/javascript/mastodon/components/form_fields/fieldset.module.scss b/app/javascript/mastodon/components/form_fields/fieldset.module.scss index f222762af5..2751b3c8a0 100644 --- a/app/javascript/mastodon/components/form_fields/fieldset.module.scss +++ b/app/javascript/mastodon/components/form_fields/fieldset.module.scss @@ -1,7 +1,9 @@ .fieldset { + --container-gap: 12px; + display: flex; flex-direction: column; - gap: 12px; + gap: var(--container-gap); color: var(--color-text-primary); font-size: 15px; } @@ -17,3 +19,11 @@ column-gap: 24px; } } + +.status { + // If there's no content, we need to compensate for the parent's + // flex gap to avoid extra spacing below the field. + &:empty { + margin-top: calc(-1 * var(--container-gap)); + } +} diff --git a/app/javascript/mastodon/components/form_fields/fieldset.tsx b/app/javascript/mastodon/components/form_fields/fieldset.tsx index d52a95130b..26381ca834 100644 --- a/app/javascript/mastodon/components/form_fields/fieldset.tsx +++ b/app/javascript/mastodon/components/form_fields/fieldset.tsx @@ -3,14 +3,19 @@ import type { ReactNode, FC } from 'react'; import { createContext, useId } from 'react'; +import { A11yLiveRegion } from 'mastodon/components/a11y_live_region'; +import type { FieldStatus } from 'mastodon/components/callout_inline'; +import { CalloutInline } from 'mastodon/components/callout_inline'; + import classes from './fieldset.module.scss'; +import { getFieldStatus } from './form_field_wrapper'; import formFieldWrapperClasses from './form_field_wrapper.module.scss'; interface FieldsetProps { legend: ReactNode; hint?: ReactNode; name?: string; - hasError?: boolean; + status?: FieldStatus | FieldStatus['variant']; layout?: 'vertical' | 'horizontal'; children: ReactNode; } @@ -26,22 +31,33 @@ export const Fieldset: FC = ({ legend, hint, name, - hasError, + status, layout, children, }) => { const uniqueId = useId(); const labelId = `${uniqueId}-label`; const hintId = `${uniqueId}-hint`; + const statusId = `${uniqueId}-status`; const fieldsetName = name || `${uniqueId}-fieldset-name`; const hasHint = !!hint; + const fieldStatus = getFieldStatus(status); + const hasStatusMessage = !!fieldStatus?.message; + + const descriptionIds = [ + hasHint ? hintId : '', + hasStatusMessage ? statusId : '', + ] + .filter((id) => !!id) + .join(' '); + return (
@@ -59,6 +75,11 @@ export const Fieldset: FC = ({ {children}
+ + {/* Live region must be rendered even when empty */} + + {hasStatusMessage && } +
); }; diff --git a/app/javascript/mastodon/components/form_fields/form_field_wrapper.module.scss b/app/javascript/mastodon/components/form_fields/form_field_wrapper.module.scss index faeb48aae4..cff93be8a6 100644 --- a/app/javascript/mastodon/components/form_fields/form_field_wrapper.module.scss +++ b/app/javascript/mastodon/components/form_fields/form_field_wrapper.module.scss @@ -46,6 +46,14 @@ font-size: 13px; } +.status { + // If there's no content, we need to compensate for the parent's + // flex gap to avoid extra spacing below the field. + &:empty { + margin-top: calc(-1 * var(--form-field-label-gap)); + } +} + .inputWrapper { display: block; } diff --git a/app/javascript/mastodon/components/form_fields/form_field_wrapper.tsx b/app/javascript/mastodon/components/form_fields/form_field_wrapper.tsx index 6454153ab8..7cd6d67614 100644 --- a/app/javascript/mastodon/components/form_fields/form_field_wrapper.tsx +++ b/app/javascript/mastodon/components/form_fields/form_field_wrapper.tsx @@ -7,6 +7,10 @@ import { FormattedMessage } from 'react-intl'; import classNames from 'classnames'; +import { A11yLiveRegion } from 'mastodon/components/a11y_live_region'; +import type { FieldStatus } from 'mastodon/components/callout_inline'; +import { CalloutInline } from 'mastodon/components/callout_inline'; + import { FieldsetNameContext } from './fieldset'; import classes from './form_field_wrapper.module.scss'; @@ -20,7 +24,7 @@ interface FieldWrapperProps { label: ReactNode; hint?: ReactNode; required?: boolean; - hasError?: boolean; + status?: FieldStatus['variant'] | FieldStatus; inputId?: string; describedById?: string; inputPlacement?: 'inline-start' | 'inline-end'; @@ -33,7 +37,7 @@ interface FieldWrapperProps { */ export type CommonFieldWrapperProps = Pick< FieldWrapperProps, - 'label' | 'hint' | 'hasError' + 'label' | 'hint' | 'status' > & { wrapperClassName?: string }; /** @@ -48,27 +52,31 @@ export const FormFieldWrapper: FC = ({ hint, describedById, required, - hasError, + status, inputPlacement, children, className, }) => { const uniqueId = useId(); const inputId = inputIdProp || `${uniqueId}-input`; + const statusId = `${inputIdProp || uniqueId}-status`; const hintId = `${inputIdProp || uniqueId}-hint`; const hasHint = !!hint; + const fieldStatus = getFieldStatus(status); + const hasStatusMessage = !!fieldStatus?.message; const hasParentFieldset = !!useContext(FieldsetNameContext); + const descriptionIds = + [hasHint ? hintId : '', hasStatusMessage ? statusId : '', describedById] + .filter((id) => !!id) + .join(' ') || undefined; + const inputProps: InputProps = { required, id: inputId, + 'aria-describedby': descriptionIds, }; - if (hasHint) { - inputProps['aria-describedby'] = describedById - ? `${describedById} ${hintId}` - : hintId; - } const input = (
{children(inputProps)}
@@ -77,7 +85,7 @@ export const FormFieldWrapper: FC = ({ return (
{inputPlacement === 'inline-start' && input} @@ -100,6 +108,11 @@ export const FormFieldWrapper: FC = ({
{inputPlacement !== 'inline-start' && input} + + {/* Live region must be rendered even when empty */} + + {hasStatusMessage && } + ); }; @@ -121,3 +134,19 @@ const RequiredMark: FC<{ required?: boolean }> = ({ required }) => ); + +export function getFieldStatus(status: FieldWrapperProps['status']) { + if (!status) { + return null; + } + + if (typeof status === 'string') { + const fieldStatus: FieldStatus = { + variant: status, + message: '', + }; + return fieldStatus; + } + + return status; +} diff --git a/app/javascript/mastodon/components/form_fields/radio_button_field.stories.tsx b/app/javascript/mastodon/components/form_fields/radio_button_field.stories.tsx index 95687abff3..1292b85724 100644 --- a/app/javascript/mastodon/components/form_fields/radio_button_field.stories.tsx +++ b/app/javascript/mastodon/components/form_fields/radio_button_field.stories.tsx @@ -71,7 +71,7 @@ export const Optional: Story = { export const WithError: Story = { args: { required: false, - hasError: true, + status: 'error', }, }; diff --git a/app/javascript/mastodon/components/form_fields/radio_button_field.tsx b/app/javascript/mastodon/components/form_fields/radio_button_field.tsx index 51f52168e0..cbc9020ca7 100644 --- a/app/javascript/mastodon/components/form_fields/radio_button_field.tsx +++ b/app/javascript/mastodon/components/form_fields/radio_button_field.tsx @@ -15,7 +15,7 @@ type Props = Omit, 'type'> & { export const RadioButtonField = forwardRef< HTMLInputElement, Props & CommonFieldWrapperProps ->(({ id, label, hint, hasError, required, ...otherProps }, ref) => { +>(({ id, label, hint, status, required, ...otherProps }, ref) => { const fieldsetName = useContext(FieldsetNameContext); return ( @@ -23,7 +23,7 @@ export const RadioButtonField = forwardRef< label={label} hint={hint} required={required} - hasError={hasError} + status={status} inputId={id} inputPlacement='inline-start' > diff --git a/app/javascript/mastodon/components/form_fields/range_input.module.scss b/app/javascript/mastodon/components/form_fields/range_input.module.scss new file mode 100644 index 0000000000..cbace07dcc --- /dev/null +++ b/app/javascript/mastodon/components/form_fields/range_input.module.scss @@ -0,0 +1,128 @@ +/* + Inspired by: + https://danielstern.ca/range.css + https://css-tricks.com/styling-cross-browser-compatible-range-inputs-css/ +*/ + +.input { + --color-bg-thumb: var(--color-bg-brand-base); + --color-bg-thumb-hover: var(--color-bg-brand-base-hover); + --color-bg-track: var(--color-bg-secondary); + + width: 100%; + margin: 6px 0; + background-color: transparent; + appearance: none; + + &:focus { + outline: none; + } + + // Thumb + + &::-webkit-slider-thumb { + margin-top: -6px; + width: 16px; + height: 16px; + background: var(--color-bg-thumb); + border: 0; + border-radius: 50px; + cursor: pointer; + -webkit-appearance: none; + } + + &::-moz-range-thumb { + width: 16px; + height: 16px; + background: var(--color-bg-thumb); + border: 0; + border-radius: 50px; + cursor: pointer; + } + + &::-ms-thumb { + width: 16px; + height: 16px; + background: var(--color-bg-thumb); + border: 0; + border-radius: 50px; + cursor: pointer; + margin-top: 0; // Needed to keep the Edge thumb centred + } + + &:focus, + &:hover { + &::-webkit-slider-thumb { + background: var(--color-bg-thumb-hover); + } + + &::-moz-range-thumb { + background: var(--color-bg-thumb-hover); + } + + &::-ms-thumb { + background: var(--color-bg-thumb-hover); + } + } + + &:focus-visible { + &::-webkit-slider-thumb { + outline: var(--outline-focus-default); + outline-offset: 2px; + } + + &::-moz-range-thumb { + outline: var(--outline-focus-default); + outline-offset: 2px; + } + + &::-ms-thumb { + outline: var(--outline-focus-default); + outline-offset: 2px; + } + } + + // Track + + &::-webkit-slider-runnable-track { + background: var(--color-bg-track); + border: 0; + border-radius: 1.3px; + width: 100%; + height: 4px; + cursor: pointer; + } + + &::-moz-range-track { + background: var(--color-bg-track); + border: 0; + border-radius: 1.3px; + width: 100%; + height: 4px; + cursor: pointer; + } + + &::-ms-track { + background: var(--color-bg-track); + border: 0; + color: transparent; + width: 100%; + height: 4px; + cursor: pointer; + } +} + +.markers { + display: flex; + flex-direction: column; + justify-content: space-between; + writing-mode: vertical-lr; + width: 100%; + font-size: 11px; + color: var(--color-text-secondary); + user-select: none; + + option { + padding: 0; + } +} diff --git a/app/javascript/mastodon/components/form_fields/range_input_field.stories.tsx b/app/javascript/mastodon/components/form_fields/range_input_field.stories.tsx new file mode 100644 index 0000000000..672228ab8c --- /dev/null +++ b/app/javascript/mastodon/components/form_fields/range_input_field.stories.tsx @@ -0,0 +1,32 @@ +import type { Meta, StoryObj } from '@storybook/react-vite'; + +import { RangeInputField } from './range_input_field'; + +const meta = { + title: 'Components/Form Fields/RangeInputField', + component: RangeInputField, + args: { + label: 'Label', + hint: 'This is a description of this form field', + checked: false, + disabled: false, + }, +} satisfies Meta; + +export default meta; + +type Story = StoryObj; + +export const Simple: Story = {}; + +export const Markers: Story = { + args: { + markers: [ + { value: 0, label: 'None' }, + { value: 25, label: 'Some' }, + { value: 50, label: 'Half' }, + { value: 75, label: 'Most' }, + { value: 100, label: 'All' }, + ], + }, +}; diff --git a/app/javascript/mastodon/components/form_fields/range_input_field.tsx b/app/javascript/mastodon/components/form_fields/range_input_field.tsx new file mode 100644 index 0000000000..8fb2620339 --- /dev/null +++ b/app/javascript/mastodon/components/form_fields/range_input_field.tsx @@ -0,0 +1,86 @@ +import type { ComponentPropsWithoutRef } from 'react'; +import { forwardRef, useId } from 'react'; + +import classNames from 'classnames'; + +import { FormFieldWrapper } from './form_field_wrapper'; +import type { CommonFieldWrapperProps } from './form_field_wrapper'; +import classes from './range_input.module.scss'; + +export type RangeInputProps = Omit< + ComponentPropsWithoutRef<'input'>, + 'type' | 'list' +> & { + markers?: { value: number; label: string }[] | number[]; +}; + +interface Props extends RangeInputProps, CommonFieldWrapperProps {} + +/** + * A simple form field for single-line text. + * + * Accepts an optional `hint` and can be marked as required + * or optional (by explicitly setting `required={false}`) + */ + +export const RangeInputField = forwardRef( + ( + { id, label, hint, status, required, wrapperClassName, ...otherProps }, + ref, + ) => ( + + {(inputProps) => } + + ), +); + +RangeInputField.displayName = 'RangeInputField'; + +export const RangeInput = forwardRef( + ({ className, markers, id, ...otherProps }, ref) => { + const markersId = useId(); + + if (!markers) { + return ( + + ); + } + return ( + <> + + + {markers.map((marker) => { + const value = typeof marker === 'number' ? marker : marker.value; + return ( + + + ); + }, +); + +RangeInput.displayName = 'RangeInput'; diff --git a/app/javascript/mastodon/components/form_fields/select_field.stories.tsx b/app/javascript/mastodon/components/form_fields/select_field.stories.tsx index 469238dd44..c215a6e04a 100644 --- a/app/javascript/mastodon/components/form_fields/select_field.stories.tsx +++ b/app/javascript/mastodon/components/form_fields/select_field.stories.tsx @@ -51,7 +51,7 @@ export const Optional: Story = { export const WithError: Story = { args: { required: false, - hasError: true, + status: 'error', }, }; diff --git a/app/javascript/mastodon/components/form_fields/select_field.tsx b/app/javascript/mastodon/components/form_fields/select_field.tsx index 59854b578e..7c1bfdf47d 100644 --- a/app/javascript/mastodon/components/form_fields/select_field.tsx +++ b/app/javascript/mastodon/components/form_fields/select_field.tsx @@ -19,12 +19,12 @@ interface Props */ export const SelectField = forwardRef( - ({ id, label, hint, required, hasError, children, ...otherProps }, ref) => ( + ({ id, label, hint, required, status, children, ...otherProps }, ref) => ( {(inputProps) => ( diff --git a/app/javascript/mastodon/components/form_fields/text_area_field.stories.tsx b/app/javascript/mastodon/components/form_fields/text_area_field.stories.tsx index 190239aee2..f06d7bbdcf 100644 --- a/app/javascript/mastodon/components/form_fields/text_area_field.stories.tsx +++ b/app/javascript/mastodon/components/form_fields/text_area_field.stories.tsx @@ -38,7 +38,17 @@ export const Optional: Story = { export const WithError: Story = { args: { required: false, - hasError: true, + status: { variant: 'error', message: "This field can't be empty" }, + }, +}; + +export const WithWarning: Story = { + args: { + required: false, + status: { + variant: 'warning', + message: 'Special characters are not allowed', + }, }, }; diff --git a/app/javascript/mastodon/components/form_fields/text_area_field.tsx b/app/javascript/mastodon/components/form_fields/text_area_field.tsx index 1e4bacc041..1284aa9276 100644 --- a/app/javascript/mastodon/components/form_fields/text_area_field.tsx +++ b/app/javascript/mastodon/components/form_fields/text_area_field.tsx @@ -26,14 +26,14 @@ export const TextAreaField = forwardRef< TextAreaProps & CommonFieldWrapperProps >( ( - { id, label, hint, required, hasError, wrapperClassName, ...otherProps }, + { id, label, hint, required, status, wrapperClassName, ...otherProps }, ref, ) => ( diff --git a/app/javascript/mastodon/components/form_fields/text_input.module.scss b/app/javascript/mastodon/components/form_fields/text_input.module.scss index 289ff1333a..f432f57055 100644 --- a/app/javascript/mastodon/components/form_fields/text_input.module.scss +++ b/app/javascript/mastodon/components/form_fields/text_input.module.scss @@ -29,16 +29,16 @@ color: var(--color-text-secondary); } - &:focus { - outline-color: var(--color-text-brand); - } - &:focus:user-invalid, &:required:user-invalid, [data-has-error='true'] & { outline-color: var(--color-text-error); } + &:focus { + outline-color: var(--color-text-brand); + } + &:required:user-valid { outline-color: var(--color-text-success); } diff --git a/app/javascript/mastodon/components/form_fields/text_input_field.stories.tsx b/app/javascript/mastodon/components/form_fields/text_input_field.stories.tsx index 8e8d7e9923..702597a0c1 100644 --- a/app/javascript/mastodon/components/form_fields/text_input_field.stories.tsx +++ b/app/javascript/mastodon/components/form_fields/text_input_field.stories.tsx @@ -40,7 +40,17 @@ export const Optional: Story = { export const WithError: Story = { args: { required: false, - hasError: true, + status: 'error', + }, +}; + +export const WithWarning: Story = { + args: { + required: false, + status: { + variant: 'warning', + message: 'Special characters are not allowed', + }, }, }; diff --git a/app/javascript/mastodon/components/form_fields/text_input_field.tsx b/app/javascript/mastodon/components/form_fields/text_input_field.tsx index f23a5da62f..d7d07833d3 100644 --- a/app/javascript/mastodon/components/form_fields/text_input_field.tsx +++ b/app/javascript/mastodon/components/form_fields/text_input_field.tsx @@ -25,14 +25,14 @@ interface Props extends TextInputProps, CommonFieldWrapperProps {} export const TextInputField = forwardRef( ( - { id, label, hint, hasError, required, wrapperClassName, ...otherProps }, + { id, label, hint, status, required, wrapperClassName, ...otherProps }, ref, ) => ( diff --git a/app/javascript/mastodon/components/form_fields/toggle_field.stories.tsx b/app/javascript/mastodon/components/form_fields/toggle_field.stories.tsx index 924c18aa74..295600a3fd 100644 --- a/app/javascript/mastodon/components/form_fields/toggle_field.stories.tsx +++ b/app/javascript/mastodon/components/form_fields/toggle_field.stories.tsx @@ -45,7 +45,7 @@ export const Optional: Story = { export const WithError: Story = { args: { required: false, - hasError: true, + status: 'error', }, }; diff --git a/app/javascript/mastodon/components/form_fields/toggle_field.tsx b/app/javascript/mastodon/components/form_fields/toggle_field.tsx index 6cafbcdc36..75fdb8f21b 100644 --- a/app/javascript/mastodon/components/form_fields/toggle_field.tsx +++ b/app/javascript/mastodon/components/form_fields/toggle_field.tsx @@ -14,12 +14,12 @@ type Props = Omit, 'type'> & { export const ToggleField = forwardRef< HTMLInputElement, Props & CommonFieldWrapperProps ->(({ id, label, hint, hasError, required, ...otherProps }, ref) => ( +>(({ id, label, hint, status, required, ...otherProps }, ref) => ( diff --git a/app/javascript/mastodon/features/account_timeline/components/redesign.module.scss b/app/javascript/mastodon/features/account_timeline/components/redesign.module.scss index 0d9036265a..e761c761d9 100644 --- a/app/javascript/mastodon/features/account_timeline/components/redesign.module.scss +++ b/app/javascript/mastodon/features/account_timeline/components/redesign.module.scss @@ -107,7 +107,7 @@ } $button-breakpoint: 420px; -$button-fallback-breakpoint: #{$button-breakpoint} + 55px; +$button-fallback-breakpoint: $button-breakpoint + 55px; .buttonsDesktop { @container (width < #{$button-breakpoint}) { @@ -132,7 +132,7 @@ $button-fallback-breakpoint: #{$button-breakpoint} + 55px; } @supports (not (container-type: inline-size)) { - @media (min-width: (#{$button-fallback-breakpoint} + 1px)) { + @media (min-width: ($button-fallback-breakpoint + 1px)) { display: none; } } diff --git a/app/javascript/mastodon/features/account_timeline/modals/note_modal.tsx b/app/javascript/mastodon/features/account_timeline/modals/note_modal.tsx index 45fe4d7105..d108a14fd6 100644 --- a/app/javascript/mastodon/features/account_timeline/modals/note_modal.tsx +++ b/app/javascript/mastodon/features/account_timeline/modals/note_modal.tsx @@ -141,8 +141,11 @@ const InnerNodeModal: FC<{ onChange={handleChange} label={intl.formatMessage(messages.fieldLabel)} className={classes.noteInput} - hasError={state === 'error'} - hint={errorText} + status={ + state === 'error' + ? { variant: 'error', message: errorText } + : undefined + } // eslint-disable-next-line jsx-a11y/no-autofocus -- We want to focus here as it's a modal. autoFocus /> diff --git a/app/javascript/mastodon/features/collections/editor/details.tsx b/app/javascript/mastodon/features/collections/editor/details.tsx index f59bd4de51..f4891cad70 100644 --- a/app/javascript/mastodon/features/collections/editor/details.tsx +++ b/app/javascript/mastodon/features/collections/editor/details.tsx @@ -1,12 +1,15 @@ -import { useCallback } from 'react'; +import { useCallback, useMemo } from 'react'; -import { FormattedMessage } from 'react-intl'; +import { FormattedMessage, useIntl } from 'react-intl'; import { useHistory } from 'react-router-dom'; import { isFulfilled } from '@reduxjs/toolkit'; -import { inputToHashtag } from '@/mastodon/utils/hashtags'; +import { + hasSpecialCharacters, + inputToHashtag, +} from '@/mastodon/utils/hashtags'; import type { ApiCreateCollectionPayload, ApiUpdateCollectionPayload, @@ -31,6 +34,7 @@ import classes from './styles.module.scss'; import { WizardStepHeader } from './wizard_step_header'; export const CollectionDetails: React.FC = () => { + const intl = useIntl(); const dispatch = useAppDispatch(); const history = useHistory(); const { id, name, description, topic, discoverable, sensitive, accountIds } = @@ -152,6 +156,11 @@ export const CollectionDetails: React.FC = () => { ], ); + const topicHasSpecialCharacters = useMemo( + () => hasSpecialCharacters(topic), + [topic], + ); + return ( @@ -224,6 +233,18 @@ export const CollectionDetails: React.FC = () => { autoCorrect='off' spellCheck='false' maxLength={40} + status={ + topicHasSpecialCharacters + ? { + variant: 'warning', + message: intl.formatMessage({ + id: 'collections.topic_special_chars_hint', + defaultMessage: + 'Special characters will be removed when saving', + }), + } + : undefined + } />
@@ -252,7 +252,7 @@ export const Profile: React.FC<{ } value={note} onChange={handleNoteChange} - hasError={!!errors?.note} + status={errors?.note ? 'error' : undefined} id='note' /> diff --git a/app/javascript/mastodon/locales/da.json b/app/javascript/mastodon/locales/da.json index 3dde4254e7..7911a597b2 100644 --- a/app/javascript/mastodon/locales/da.json +++ b/app/javascript/mastodon/locales/da.json @@ -184,6 +184,11 @@ "account_edit.field_reorder_modal.drag_start": "Samlede feltet \"{item}\" op.", "account_edit.field_reorder_modal.handle_label": "Træk feltet \"{item}\"", "account_edit.field_reorder_modal.title": "Omarrangér felter", + "account_edit.image_edit.add_button": "Tilføj billede", + "account_edit.image_edit.alt_add_button": "Tilføj alt-tekst", + "account_edit.image_edit.alt_edit_button": "Rediger alt-tekst", + "account_edit.image_edit.remove_button": "Fjern billede", + "account_edit.image_edit.replace_button": "Erstat billede", "account_edit.name_modal.add_title": "Tilføj visningsnavn", "account_edit.name_modal.edit_title": "Rediger visningsnavn", "account_edit.profile_tab.button_label": "Tilpas", diff --git a/app/javascript/mastodon/locales/de.json b/app/javascript/mastodon/locales/de.json index 3b2d249828..cc9217dfdd 100644 --- a/app/javascript/mastodon/locales/de.json +++ b/app/javascript/mastodon/locales/de.json @@ -174,7 +174,7 @@ "account_edit.field_edit_modal.name_hint": "z. B. „Meine Website“", "account_edit.field_edit_modal.name_label": "Beschriftung", "account_edit.field_edit_modal.url_warning": "Um einen Link hinzuzufügen, füge {protocol} an den Anfang ein.", - "account_edit.field_edit_modal.value_hint": "z. B. „https://example.me“", + "account_edit.field_edit_modal.value_hint": "z. B. „https://beispiel.tld“", "account_edit.field_edit_modal.value_label": "Inhalt", "account_edit.field_reorder_modal.drag_cancel": "Das Ziehen wurde abgebrochen und das Feld „{item}“ wurde abgelegt.", "account_edit.field_reorder_modal.drag_end": "Das Feld „{item}“ wurde abgelegt.", @@ -184,6 +184,11 @@ "account_edit.field_reorder_modal.drag_start": "Das Feld „{item}“ wurde ausgewählt.", "account_edit.field_reorder_modal.handle_label": "Das Feld „{item}“ verschieben", "account_edit.field_reorder_modal.title": "Felder neu anordnen", + "account_edit.image_edit.add_button": "Bild hinzufügen", + "account_edit.image_edit.alt_add_button": "Bildbeschreibung hinzufügen", + "account_edit.image_edit.alt_edit_button": "Bildbeschreibung bearbeiten", + "account_edit.image_edit.remove_button": "Bild entfernen", + "account_edit.image_edit.replace_button": "Bild ersetzen", "account_edit.name_modal.add_title": "Anzeigenamen hinzufügen", "account_edit.name_modal.edit_title": "Anzeigenamen bearbeiten", "account_edit.profile_tab.button_label": "Anpassen", diff --git a/app/javascript/mastodon/locales/el.json b/app/javascript/mastodon/locales/el.json index 100fce7e9a..4b33a257fd 100644 --- a/app/javascript/mastodon/locales/el.json +++ b/app/javascript/mastodon/locales/el.json @@ -184,6 +184,11 @@ "account_edit.field_reorder_modal.drag_start": "Το πεδίο \"{item}\" σηκώθηκε.", "account_edit.field_reorder_modal.handle_label": "Μετακίνηση πεδίου \"{item}\"", "account_edit.field_reorder_modal.title": "Αναδιάταξη πεδίων", + "account_edit.image_edit.add_button": "Προσθήκη εικόνας", + "account_edit.image_edit.alt_add_button": "Προσθήκη εναλλακτικού κειμένου", + "account_edit.image_edit.alt_edit_button": "Επεξεργασία εναλλακτικού κειμένου", + "account_edit.image_edit.remove_button": "Αφαίρεση εικόνας", + "account_edit.image_edit.replace_button": "Αντικατάσταση εικόνας", "account_edit.name_modal.add_title": "Προσθήκη εμφανιζόμενου ονόματος", "account_edit.name_modal.edit_title": "Επεξεργασία εμφανιζόμενου ονόματος", "account_edit.profile_tab.button_label": "Προσαρμογή", diff --git a/app/javascript/mastodon/locales/en-GB.json b/app/javascript/mastodon/locales/en-GB.json index 25cb548c19..abb0bcef2d 100644 --- a/app/javascript/mastodon/locales/en-GB.json +++ b/app/javascript/mastodon/locales/en-GB.json @@ -184,6 +184,11 @@ "account_edit.field_reorder_modal.drag_start": "Picked up field \"{item}\".", "account_edit.field_reorder_modal.handle_label": "Drag field \"{item}\"", "account_edit.field_reorder_modal.title": "Rearrange fields", + "account_edit.image_edit.add_button": "Add image", + "account_edit.image_edit.alt_add_button": "Add alt text", + "account_edit.image_edit.alt_edit_button": "Edit alt text", + "account_edit.image_edit.remove_button": "Remove image", + "account_edit.image_edit.replace_button": "Replace image", "account_edit.name_modal.add_title": "Add display name", "account_edit.name_modal.edit_title": "Edit display name", "account_edit.profile_tab.button_label": "Customise", diff --git a/app/javascript/mastodon/locales/en.json b/app/javascript/mastodon/locales/en.json index 7567c489c2..db5c1313a5 100644 --- a/app/javascript/mastodon/locales/en.json +++ b/app/javascript/mastodon/locales/en.json @@ -374,6 +374,7 @@ "collections.search_accounts_max_reached": "You have added the maximum number of accounts", "collections.sensitive": "Sensitive", "collections.topic_hint": "Add a hashtag that helps others understand the main topic of this collection.", + "collections.topic_special_chars_hint": "Special characters will be removed when saving", "collections.view_collection": "View collection", "collections.view_other_collections_by_user": "View other collections by this user", "collections.visibility_public": "Public", diff --git a/app/javascript/mastodon/locales/es-AR.json b/app/javascript/mastodon/locales/es-AR.json index 51e30303e0..d2edfca143 100644 --- a/app/javascript/mastodon/locales/es-AR.json +++ b/app/javascript/mastodon/locales/es-AR.json @@ -184,6 +184,11 @@ "account_edit.field_reorder_modal.drag_start": "Campo elegido «{item}».", "account_edit.field_reorder_modal.handle_label": "Arrastrá el campo «{item}»", "account_edit.field_reorder_modal.title": "Reordená los campos", + "account_edit.image_edit.add_button": "Agregar imagen", + "account_edit.image_edit.alt_add_button": "Agregar texto alternativo", + "account_edit.image_edit.alt_edit_button": "Editar texto alternativo", + "account_edit.image_edit.remove_button": "Quitar imagen", + "account_edit.image_edit.replace_button": "Reemplazar imagen", "account_edit.name_modal.add_title": "Agregar nombre a mostrar", "account_edit.name_modal.edit_title": "Editar nombre a mostrar", "account_edit.profile_tab.button_label": "Personalizar", @@ -961,12 +966,14 @@ "notifications_permission_banner.title": "No te pierdas nada", "onboarding.follows.back": "Volver", "onboarding.follows.empty": "Desafortunadamente, no se pueden mostrar resultados en este momento. Podés intentar usar la búsqueda o navegar por la página de exploración para encontrar cuentas a las que seguir, o intentarlo de nuevo más tarde.", + "onboarding.follows.next": "Siguiente: Configurá tu perfil", "onboarding.follows.search": "Buscar", "onboarding.follows.title": "Para comenzar, empezá a seguir cuentas", "onboarding.profile.discoverable": "Hacer que mi perfil sea detectable", "onboarding.profile.discoverable_hint": "Cuando optás por ser detectable en Mastodon, tus mensajes pueden aparecer en los resultados de búsqueda y de tendencia, y tu perfil puede ser sugerido a personas con intereses similares a los tuyos.", "onboarding.profile.display_name": "Nombre para mostrar", "onboarding.profile.display_name_hint": "Tu nombre completo o tu pseudónimo…", + "onboarding.profile.finish": "Finalizar", "onboarding.profile.note": "Biografía", "onboarding.profile.note_hint": "Podés @mencionar otras cuentas o usar #etiquetas…", "onboarding.profile.title": "Configuración del perfil", diff --git a/app/javascript/mastodon/locales/es-MX.json b/app/javascript/mastodon/locales/es-MX.json index 6d99e2dbf5..9c6bd2e370 100644 --- a/app/javascript/mastodon/locales/es-MX.json +++ b/app/javascript/mastodon/locales/es-MX.json @@ -184,6 +184,11 @@ "account_edit.field_reorder_modal.drag_start": "Recogido el campo «{item}».", "account_edit.field_reorder_modal.handle_label": "Arrastra el campo «{item}»", "account_edit.field_reorder_modal.title": "Reorganizar campos", + "account_edit.image_edit.add_button": "Añadir imagen", + "account_edit.image_edit.alt_add_button": "Añadir texto alternativo", + "account_edit.image_edit.alt_edit_button": "Editar texto alternativo", + "account_edit.image_edit.remove_button": "Eliminar imagen", + "account_edit.image_edit.replace_button": "Reemplazar imagen", "account_edit.name_modal.add_title": "Añadir nombre para mostrar", "account_edit.name_modal.edit_title": "Editar nombre para mostrar", "account_edit.profile_tab.button_label": "Personalizar", diff --git a/app/javascript/mastodon/locales/es.json b/app/javascript/mastodon/locales/es.json index 3bf85dc816..abf1791b0f 100644 --- a/app/javascript/mastodon/locales/es.json +++ b/app/javascript/mastodon/locales/es.json @@ -173,6 +173,8 @@ "account_edit.field_edit_modal.link_emoji_warning": "Recomendamos no usar emojis personalizados combinados con enlaces. Los campos personalizados que contengan ambos solo se mostrarán como texto en vez de un enlace, para evitar confusiones.", "account_edit.field_edit_modal.name_hint": "Ej. \"Web personal\"", "account_edit.field_edit_modal.name_label": "Etiqueta", + "account_edit.field_edit_modal.url_warning": "Para añadir un enlace, incluye {protocol} al principio.", + "account_edit.field_edit_modal.value_hint": "Ejemplo: “https://example.me”", "account_edit.field_edit_modal.value_label": "Valor", "account_edit.field_reorder_modal.drag_cancel": "El arrastre se ha cancelado. El campo \"{item}\" se ha soltado.", "account_edit.field_reorder_modal.drag_end": "El campo \"{item}\" se ha soltado.", @@ -182,6 +184,11 @@ "account_edit.field_reorder_modal.drag_start": "Campo \"{item}\" seleccionado.", "account_edit.field_reorder_modal.handle_label": "Arrastra el campo \"{item}\"", "account_edit.field_reorder_modal.title": "Reorganizar campos", + "account_edit.image_edit.add_button": "Añadir imagen", + "account_edit.image_edit.alt_add_button": "Añadir texto alternativo", + "account_edit.image_edit.alt_edit_button": "Editar texto alternativo", + "account_edit.image_edit.remove_button": "Quitar imagen", + "account_edit.image_edit.replace_button": "Sustituir imagen", "account_edit.name_modal.add_title": "Añadir nombre para mostrar", "account_edit.name_modal.edit_title": "Editar nombre para mostrar", "account_edit.profile_tab.button_label": "Personalizar", @@ -337,14 +344,14 @@ "collections.create_collection": "Crear colección", "collections.delete_collection": "Eliminar colección", "collections.description_length_hint": "Limitado a 100 caracteres", - "collections.detail.accept_inclusion": "Aceptar", + "collections.detail.accept_inclusion": "De acuerdo", "collections.detail.accounts_heading": "Cuentas", "collections.detail.author_added_you": "{author} te añadió a esta colección", "collections.detail.curated_by_author": "Seleccionado por {author}", "collections.detail.curated_by_you": "Seleccionado por ti", "collections.detail.loading": "Cargando colección…", "collections.detail.other_accounts_in_collection": "Otros en esta colección:", - "collections.detail.revoke_inclusion": "Eliminar", + "collections.detail.revoke_inclusion": "Sácame de aquí", "collections.detail.sensitive_note": "Esta colección contiene cuentas y contenido que puede ser sensible para algunos usuarios.", "collections.detail.share": "Compartir esta colección", "collections.edit_details": "Editar detalles", @@ -360,9 +367,9 @@ "collections.old_last_post_note": "Última publicación hace más de una semana", "collections.remove_account": "Quitar esta cuenta", "collections.report_collection": "Informar de esta colección", - "collections.revoke_collection_inclusion": "Eliminarme de esta colección", - "collections.revoke_inclusion.confirmation": "Ha sido eliminado de\"{collection}\"", - "collections.revoke_inclusion.error": "Se ha producido un error. Inténtelo de nuevo más tarde.", + "collections.revoke_collection_inclusion": "Sácame de esta colección", + "collections.revoke_inclusion.confirmation": "Has salido de la \"{collection}\"", + "collections.revoke_inclusion.error": "Se ha producido un error, inténtalo de nuevo más tarde.", "collections.search_accounts_label": "Buscar cuentas para añadir…", "collections.search_accounts_max_reached": "Has añadido el número máximo de cuentas", "collections.sensitive": "Sensible", @@ -486,9 +493,9 @@ "confirmations.remove_from_followers.confirm": "Eliminar seguidor", "confirmations.remove_from_followers.message": "{name} dejará de seguirte. ¿Estás seguro de que quieres continuar?", "confirmations.remove_from_followers.title": "¿Eliminar seguidor?", - "confirmations.revoke_collection_inclusion.confirm": "Eliminar", + "confirmations.revoke_collection_inclusion.confirm": "Sácame", "confirmations.revoke_collection_inclusion.message": "Esta acción es permanente, y el curador no podrá volver a añadirle a la colección más adelante.", - "confirmations.revoke_collection_inclusion.title": "¿Eliminarse de esta colección?", + "confirmations.revoke_collection_inclusion.title": "¿Salirse de esta colección?", "confirmations.revoke_quote.confirm": "Eliminar publicación", "confirmations.revoke_quote.message": "Esta acción no tiene vuelta atrás.", "confirmations.revoke_quote.title": "¿Eliminar la publicación?", @@ -959,12 +966,14 @@ "notifications_permission_banner.title": "Nunca te pierdas nada", "onboarding.follows.back": "Atrás", "onboarding.follows.empty": "Desafortunadamente, no se pueden mostrar resultados en este momento. Puedes intentar usar la búsqueda o navegar por la página de exploración para encontrar personas a las que seguir, o inténtalo de nuevo más tarde.", + "onboarding.follows.next": "Siguiente: Configura tu perfil", "onboarding.follows.search": "Buscar", "onboarding.follows.title": "Sigue personas para comenzar", "onboarding.profile.discoverable": "Hacer que mi perfil aparezca en búsquedas", "onboarding.profile.discoverable_hint": "Cuando permites que tu perfil aparezca en búsquedas en Mastodon, tus publicaciones podrán aparecer en los resultados de búsqueda y en tendencias, y tu perfil podrá recomendarse a gente con intereses similares a los tuyos.", "onboarding.profile.display_name": "Nombre para mostrar", "onboarding.profile.display_name_hint": "Tu nombre completo o tu apodo…", + "onboarding.profile.finish": "Terminar", "onboarding.profile.note": "Biografía", "onboarding.profile.note_hint": "Puedes @mencionar a otras personas o #etiquetas…", "onboarding.profile.title": "Configuración del perfil", diff --git a/app/javascript/mastodon/locales/fi.json b/app/javascript/mastodon/locales/fi.json index f7f529a639..04be6b3223 100644 --- a/app/javascript/mastodon/locales/fi.json +++ b/app/javascript/mastodon/locales/fi.json @@ -184,6 +184,11 @@ "account_edit.field_reorder_modal.drag_start": "Valittu kenttä ”{item}”.", "account_edit.field_reorder_modal.handle_label": "Siirrä kenttää ”{item}”", "account_edit.field_reorder_modal.title": "Järjestele kenttiä", + "account_edit.image_edit.add_button": "Lisää kuva", + "account_edit.image_edit.alt_add_button": "Lisää tekstivastine", + "account_edit.image_edit.alt_edit_button": "Muokkaa tekstivastinetta", + "account_edit.image_edit.remove_button": "Poista kuva", + "account_edit.image_edit.replace_button": "Korvaa kuva", "account_edit.name_modal.add_title": "Lisää näyttönimi", "account_edit.name_modal.edit_title": "Muokkaa näyttönimeä", "account_edit.profile_tab.button_label": "Mukauta", @@ -225,8 +230,8 @@ "alert.rate_limited.title": "Pyyntömäärää rajoitettu", "alert.unexpected.message": "Tapahtui odottamaton virhe.", "alert.unexpected.title": "Hups!", - "alt_text_badge.title": "Vaihtoehtoinen teksti", - "alt_text_modal.add_alt_text": "Lisää vaihtoehtoinen teksti", + "alt_text_badge.title": "Tekstivastine", + "alt_text_modal.add_alt_text": "Lisää tekstivastine", "alt_text_modal.add_text_from_image": "Lisää teksti kuvasta", "alt_text_modal.cancel": "Peruuta", "alt_text_modal.change_thumbnail": "Vaihda pikkukuva", @@ -468,10 +473,10 @@ "confirmations.logout.confirm": "Kirjaudu ulos", "confirmations.logout.message": "Haluatko varmasti kirjautua ulos?", "confirmations.logout.title": "Kirjaudutaanko ulos?", - "confirmations.missing_alt_text.confirm": "Lisää vaihtoehtoinen teksti", - "confirmations.missing_alt_text.message": "Julkaisussasi on mediaa ilman vaihtoehtoista tekstiä. Kuvausten lisääminen auttaa tekemään sisällöstäsi saavutettavamman useammille ihmisille.", + "confirmations.missing_alt_text.confirm": "Lisää tekstivastine", + "confirmations.missing_alt_text.message": "Julkaisussasi on mediaa ilman tekstivastinetta. Kuvausten lisääminen auttaa tekemään sisällöstäsi saavutettavamman useammille ihmisille.", "confirmations.missing_alt_text.secondary": "Julkaise silti", - "confirmations.missing_alt_text.title": "Lisätäänkö vaihtoehtoinen teksti?", + "confirmations.missing_alt_text.title": "Lisätäänkö tekstivastine?", "confirmations.mute.confirm": "Mykistä", "confirmations.private_quote_notify.cancel": "Takaisin muokkaukseen", "confirmations.private_quote_notify.confirm": "Julkaise", @@ -706,7 +711,7 @@ "ignore_notifications_modal.not_following_title": "Sivuutetaanko ilmoitukset käyttäjiltä, joita et seuraa?", "ignore_notifications_modal.private_mentions_title": "Sivuutetaanko ilmoitukset pyytämättömistä yksityismaininnoista?", "info_button.label": "Ohje", - "info_button.what_is_alt_text": "

Mikä vaihtoehtoinen teksti on?

Vaihtoehtoinen teksti tarjoaa kuvauksen kuvista ihmisille, joilla on näkövamma tai matalan kaistanleveyden yhteys tai jotka kaipaavat lisäkontekstia.

Voit parantaa saavutettavuutta ja ymmärrettävyyttä kaikkien näkökulmasta kirjoittamalla selkeän, tiiviin ja objektiivisen vaihtoehtoisen tekstin.

  • Ota mukaan tärkeät elementit
  • Tiivistä kuvissa oleva teksti
  • Käytä tavallisia lauserakenteita
  • Vältä turhaa tietoa
  • Keskity trendeihin ja keskeisiin tuloksiin monimutkaisissa visuaalisissa esityksissä (kuten kaavioissa tai kartoissa)
", + "info_button.what_is_alt_text": "

Mikä tekstivastine on?

Tekstivastine tarjoaa kuvauksen kuvista ihmisille, joilla on näkövamma tai matalan kaistanleveyden yhteys tai jotka kaipaavat lisäkontekstia.

Voit parantaa saavutettavuutta ja ymmärrettävyyttä kaikkien näkökulmasta kirjoittamalla selkeän, tiiviin ja objektiivisen tekstivastineen.

  • Ota mukaan tärkeät elementit
  • Tiivistä kuvissa oleva teksti
  • Käytä tavallisia lauserakenteita
  • Vältä turhaa tietoa
  • Keskity trendeihin ja keskeisiin tuloksiin monimutkaisissa visuaalisissa esityksissä (kuten kaavioissa tai kartoissa)
", "interaction_modal.action": "Jotta voit olla vuorovaikutuksessa käyttäjän {name} julkaisun kanssa, sinun on kirjauduttava sisään tilillesi käyttämälläsi Mastodon-palvelimella.", "interaction_modal.go": "Siirry", "interaction_modal.no_account_yet": "Eikö sinulla ole vielä tiliä?", diff --git a/app/javascript/mastodon/locales/fr-CA.json b/app/javascript/mastodon/locales/fr-CA.json index 007baa530c..ffbd596f25 100644 --- a/app/javascript/mastodon/locales/fr-CA.json +++ b/app/javascript/mastodon/locales/fr-CA.json @@ -184,6 +184,11 @@ "account_edit.field_reorder_modal.drag_start": "Champ « {item} » sélectionné.", "account_edit.field_reorder_modal.handle_label": "Faites glisser le champ « {item} »", "account_edit.field_reorder_modal.title": "Réorganiser les champs", + "account_edit.image_edit.add_button": "Ajouter une image", + "account_edit.image_edit.alt_add_button": "Ajouter un texte alternatif", + "account_edit.image_edit.alt_edit_button": "Modifier le texte alternatif", + "account_edit.image_edit.remove_button": "Supprimer l’image", + "account_edit.image_edit.replace_button": "Remplacer l'image", "account_edit.name_modal.add_title": "Ajouter un nom public", "account_edit.name_modal.edit_title": "Modifier le nom public", "account_edit.profile_tab.button_label": "Personnaliser", @@ -961,12 +966,14 @@ "notifications_permission_banner.title": "Ne rien rater", "onboarding.follows.back": "Retour", "onboarding.follows.empty": "Malheureusement, aucun résultat ne peut être affiché pour le moment. Vous pouvez essayer de rechercher ou de parcourir la page \"Explorer\" pour trouver des personnes à suivre, ou réessayer plus tard.", + "onboarding.follows.next": "Suivant : configurer votre profil", "onboarding.follows.search": "Recherche", "onboarding.follows.title": "Suivre des personnes pour commencer", "onboarding.profile.discoverable": "Permettre de découvrir mon profil", "onboarding.profile.discoverable_hint": "Lorsque vous acceptez d'être découvert sur Mastodon, vos messages peuvent apparaître dans les résultats de recherche et les tendances, et votre profil peut être suggéré à des personnes ayant des intérêts similaires aux vôtres.", "onboarding.profile.display_name": "Nom affiché", "onboarding.profile.display_name_hint": "Votre nom complet ou votre nom rigolo…", + "onboarding.profile.finish": "Terminer", "onboarding.profile.note": "Bio", "onboarding.profile.note_hint": "Vous pouvez @mentionner d'autres personnes ou #hashtags…", "onboarding.profile.title": "Configuration du profil", diff --git a/app/javascript/mastodon/locales/fr.json b/app/javascript/mastodon/locales/fr.json index 6eb2e0a216..f1a8f511bb 100644 --- a/app/javascript/mastodon/locales/fr.json +++ b/app/javascript/mastodon/locales/fr.json @@ -184,6 +184,11 @@ "account_edit.field_reorder_modal.drag_start": "Champ « {item} » sélectionné.", "account_edit.field_reorder_modal.handle_label": "Faites glisser le champ « {item} »", "account_edit.field_reorder_modal.title": "Réorganiser les champs", + "account_edit.image_edit.add_button": "Ajouter une image", + "account_edit.image_edit.alt_add_button": "Ajouter un texte alternatif", + "account_edit.image_edit.alt_edit_button": "Modifier le texte alternatif", + "account_edit.image_edit.remove_button": "Supprimer l’image", + "account_edit.image_edit.replace_button": "Remplacer l'image", "account_edit.name_modal.add_title": "Ajouter un nom public", "account_edit.name_modal.edit_title": "Modifier le nom public", "account_edit.profile_tab.button_label": "Personnaliser", @@ -961,12 +966,14 @@ "notifications_permission_banner.title": "Toujours au courant", "onboarding.follows.back": "Retour", "onboarding.follows.empty": "Malheureusement, aucun résultat ne peut être affiché pour le moment. Vous pouvez essayer d'utiliser la recherche ou parcourir la page de découverte pour trouver des personnes à suivre, ou réessayez plus tard.", + "onboarding.follows.next": "Suivant : configurer votre profil", "onboarding.follows.search": "Recherche", "onboarding.follows.title": "Suivre des personnes pour commencer", "onboarding.profile.discoverable": "Permettre de découvrir mon profil", "onboarding.profile.discoverable_hint": "Lorsque vous acceptez d'être découvert sur Mastodon, vos messages peuvent apparaître dans les résultats de recherche et les tendances, et votre profil peut être suggéré à des personnes ayant des intérêts similaires aux vôtres.", "onboarding.profile.display_name": "Nom affiché", "onboarding.profile.display_name_hint": "Votre nom complet ou votre nom rigolo…", + "onboarding.profile.finish": "Terminer", "onboarding.profile.note": "Biographie", "onboarding.profile.note_hint": "Vous pouvez @mentionner d'autres personnes ou #hashtags…", "onboarding.profile.title": "Configuration du profil", diff --git a/app/javascript/mastodon/locales/ga.json b/app/javascript/mastodon/locales/ga.json index 3374b69c41..ca1155df87 100644 --- a/app/javascript/mastodon/locales/ga.json +++ b/app/javascript/mastodon/locales/ga.json @@ -173,6 +173,8 @@ "account_edit.field_edit_modal.link_emoji_warning": "Molaimid gan emoji saincheaptha a úsáid i gcomhar le Urlanna. Taispeánfar réimsí saincheaptha ina bhfuil an dá cheann mar théacs amháin seachas mar nasc, chun mearbhall úsáideoirí a sheachaint.", "account_edit.field_edit_modal.name_hint": "M.sh. “Suíomh Gréasáin pearsanta”", "account_edit.field_edit_modal.name_label": "Lipéad", + "account_edit.field_edit_modal.url_warning": "Chun nasc a chur leis, cuir {protocol} ag an tús le do thoil.", + "account_edit.field_edit_modal.value_hint": "M.sh. “https://example.me”", "account_edit.field_edit_modal.value_label": "Luach", "account_edit.field_reorder_modal.drag_cancel": "Cuireadh an tarraingt ar ceal. Baineadh an réimse \"{item}\".", "account_edit.field_reorder_modal.drag_end": "Baineadh an réimse \"{item}\".", @@ -182,6 +184,11 @@ "account_edit.field_reorder_modal.drag_start": "Réimse \"{item}\" bailithe.", "account_edit.field_reorder_modal.handle_label": "Tarraing réimse \"{item}\"", "account_edit.field_reorder_modal.title": "Athshocraigh réimsí", + "account_edit.image_edit.add_button": "Cuir íomhá leis", + "account_edit.image_edit.alt_add_button": "Cuir téacs alt leis", + "account_edit.image_edit.alt_edit_button": "Cuir téacs alt in eagar", + "account_edit.image_edit.remove_button": "Bain íomhá", + "account_edit.image_edit.replace_button": "Athsholáthair íomhá", "account_edit.name_modal.add_title": "Cuir ainm taispeána leis", "account_edit.name_modal.edit_title": "Cuir ainm taispeána in eagar", "account_edit.profile_tab.button_label": "Saincheap", @@ -959,12 +966,14 @@ "notifications_permission_banner.title": "Ná caill aon rud go deo", "onboarding.follows.back": "Ar ais", "onboarding.follows.empty": "Ar an drochuair, ní féidir aon torthaí a thaispeáint faoi láthair. Is féidir leat triail a bhaint as cuardach nó brabhsáil ar an leathanach taiscéalaíochta chun teacht ar dhaoine le leanúint, nó bain triail eile as níos déanaí.", + "onboarding.follows.next": "Ar Aghaidh: Socraigh do phróifíl", "onboarding.follows.search": "Cuardach", "onboarding.follows.title": "Lean daoine le tosú", "onboarding.profile.discoverable": "Déan mo phróifíl a fháil amach", "onboarding.profile.discoverable_hint": "Nuair a roghnaíonn tú infhionnachtana ar Mastodon, d’fhéadfadh do phoist a bheith le feiceáil i dtorthaí cuardaigh agus treochtaí, agus d’fhéadfaí do phróifíl a mholadh do dhaoine a bhfuil na leasanna céanna acu leat.", "onboarding.profile.display_name": "Ainm taispeána", "onboarding.profile.display_name_hint": "D’ainm iomlán nó d’ainm spraíúil…", + "onboarding.profile.finish": "Críochnaigh", "onboarding.profile.note": "Bith", "onboarding.profile.note_hint": "Is féidir leat @ daoine eile a lua nó #hashtags…", "onboarding.profile.title": "Socrú próifíle", diff --git a/app/javascript/mastodon/locales/gl.json b/app/javascript/mastodon/locales/gl.json index 4a7780ff84..975c89e75a 100644 --- a/app/javascript/mastodon/locales/gl.json +++ b/app/javascript/mastodon/locales/gl.json @@ -184,6 +184,11 @@ "account_edit.field_reorder_modal.drag_start": "Seleccionado o campo \"{item}\".", "account_edit.field_reorder_modal.handle_label": "Arrastra o campo \"{item}\"", "account_edit.field_reorder_modal.title": "Ordear campos", + "account_edit.image_edit.add_button": "Engadir imaxe", + "account_edit.image_edit.alt_add_button": "Engadir descrición", + "account_edit.image_edit.alt_edit_button": "Editar descrición", + "account_edit.image_edit.remove_button": "Retirar a imaxe", + "account_edit.image_edit.replace_button": "Substituír a imaxe", "account_edit.name_modal.add_title": "Engadir nome público", "account_edit.name_modal.edit_title": "Editar o nome público", "account_edit.profile_tab.button_label": "Personalizar", diff --git a/app/javascript/mastodon/locales/he.json b/app/javascript/mastodon/locales/he.json index c3f7f81818..12adfb5ce2 100644 --- a/app/javascript/mastodon/locales/he.json +++ b/app/javascript/mastodon/locales/he.json @@ -173,6 +173,8 @@ "account_edit.field_edit_modal.link_emoji_warning": "אנו ממליצים נגד שימוש באמוג'י ייחודיים ביחד עם URL. שדות מיוחדים שמכילים את שניהם יופיעו כמלל בלבד ולא כקישור, כדי למנוע בלבול משתמשים.", "account_edit.field_edit_modal.name_hint": "למשל \"אתר אישי\"", "account_edit.field_edit_modal.name_label": "תווית", + "account_edit.field_edit_modal.url_warning": "כדי להוסיף קישור, אנא הכלילו {protocol} בהתחלה.", + "account_edit.field_edit_modal.value_hint": "למשל “https://example.me”", "account_edit.field_edit_modal.value_label": "ערך", "account_edit.field_reorder_modal.drag_cancel": "הגרירה בוטלה. השדה \"{item}\" נעזב.", "account_edit.field_reorder_modal.drag_end": "השדה \"{item}\" נעזב.", @@ -182,6 +184,11 @@ "account_edit.field_reorder_modal.drag_start": "השדה \"{item}\" נבחר.", "account_edit.field_reorder_modal.handle_label": "הזזת השדה \"{item}\"", "account_edit.field_reorder_modal.title": "סידור שדות", + "account_edit.image_edit.add_button": "הוספת תמונה", + "account_edit.image_edit.alt_add_button": "הוספת מלל חלופי", + "account_edit.image_edit.alt_edit_button": "עריכת מלל חלופי", + "account_edit.image_edit.remove_button": "הסרת תמונה", + "account_edit.image_edit.replace_button": "החלפת תמונה", "account_edit.name_modal.add_title": "הוספת שם תצוגה", "account_edit.name_modal.edit_title": "עריכת שם תצוגה", "account_edit.profile_tab.button_label": "התאמה אישית", @@ -959,12 +966,14 @@ "notifications_permission_banner.title": "לעולם אל תחמיץ דבר", "onboarding.follows.back": "בחזרה", "onboarding.follows.empty": "למצער, תוצאות לחיפושך אינן בנמצא. ניתן להשתמש בחיפוש או בדף החקירות לשם מציאת אנשים ולעקבם. אפשר גם לנסות שוב אחר כך.", + "onboarding.follows.next": "להמשיך ליצירת הפרופיל שלך", "onboarding.follows.search": "חיפוש", "onboarding.follows.title": "כדי להתחיל, יש לעקוב אחרי אנשים", "onboarding.profile.discoverable": "כלול את הפרופיל שלי בעמודת התגליות", "onboarding.profile.discoverable_hint": "כשתבחרו להכלל ב\"תגליות\" על מסטודון, ההודעות שלכם עשויות להופיע בתוצאות חיפוש ועמודות \"נושאים חמים\", והפרופיל יוצע לאחרים עם תחומי עניין משותפים לכם.", "onboarding.profile.display_name": "שם להצגה", "onboarding.profile.display_name_hint": "שמך המלא או כינוי הכיף שלך…", + "onboarding.profile.finish": "סיום", "onboarding.profile.note": "אודות", "onboarding.profile.note_hint": "ניתן @לאזכר משתמשים אחרים או #תגיות…", "onboarding.profile.title": "הגדרת פרופיל", diff --git a/app/javascript/mastodon/locales/hu.json b/app/javascript/mastodon/locales/hu.json index f58a33925d..7fcc82d1da 100644 --- a/app/javascript/mastodon/locales/hu.json +++ b/app/javascript/mastodon/locales/hu.json @@ -184,6 +184,11 @@ "account_edit.field_reorder_modal.drag_start": "A(z) „{item}” mező áthelyezéshez felvéve.", "account_edit.field_reorder_modal.handle_label": "A(z) „{item}” mező húzása", "account_edit.field_reorder_modal.title": "Mezők átrendezése", + "account_edit.image_edit.add_button": "Kép hozzáadása", + "account_edit.image_edit.alt_add_button": "Helyettesítő szöveg hozzáadása", + "account_edit.image_edit.alt_edit_button": "Helyettesítő szöveg szerkesztése", + "account_edit.image_edit.remove_button": "Kép eltávolítása", + "account_edit.image_edit.replace_button": "Kép cseréje", "account_edit.name_modal.add_title": "Megjelenítendő név hozzáadása", "account_edit.name_modal.edit_title": "Megjelenítendő név szerkesztése", "account_edit.profile_tab.button_label": "Testreszabás", @@ -961,12 +966,14 @@ "notifications_permission_banner.title": "Soha ne mulassz el semmit", "onboarding.follows.back": "Vissza", "onboarding.follows.empty": "Sajnos jelenleg nem jeleníthető meg eredmény. Kipróbálhatod a keresést vagy böngészheted a felfedező oldalon a követni kívánt személyeket, vagy próbáld meg később.", + "onboarding.follows.next": "Következik: A profil beállítása", "onboarding.follows.search": "Keresés", "onboarding.follows.title": "A kezdéshez kezdj el embereket követni", "onboarding.profile.discoverable": "Saját profil beállítása felfedezhetőként", "onboarding.profile.discoverable_hint": "A Mastodonon a felfedezhetőség választása esetén a saját bejegyzéseid megjelenhetnek a keresési eredmények és a felkapott tartalmak között, valamint a profilod a hozzád hasonló érdeklődési körrel rendelkező embereknél is ajánlásra kerülhet.", "onboarding.profile.display_name": "Megjelenített név", "onboarding.profile.display_name_hint": "Teljes neved vagy vicces neved…", + "onboarding.profile.finish": "Befejezés", "onboarding.profile.note": "Bemutatkozás", "onboarding.profile.note_hint": "Megemlíthetsz @másokat vagy #hashtag-eket…", "onboarding.profile.title": "Profilbeállítás", diff --git a/app/javascript/mastodon/locales/is.json b/app/javascript/mastodon/locales/is.json index 603cf573cc..60ae1b6db6 100644 --- a/app/javascript/mastodon/locales/is.json +++ b/app/javascript/mastodon/locales/is.json @@ -184,6 +184,11 @@ "account_edit.field_reorder_modal.drag_start": "Náði reitnum \"{item}\".", "account_edit.field_reorder_modal.handle_label": "Dragðu reitinn \"{item}\"", "account_edit.field_reorder_modal.title": "Endurraða gagnasviðum", + "account_edit.image_edit.add_button": "Bæta við mynd", + "account_edit.image_edit.alt_add_button": "Bæta við hjálpartexta", + "account_edit.image_edit.alt_edit_button": "Breyta hjálpartexta", + "account_edit.image_edit.remove_button": "Fjarlægja mynd", + "account_edit.image_edit.replace_button": "Skipta um mynd", "account_edit.name_modal.add_title": "Bættu við birtingarnafni", "account_edit.name_modal.edit_title": "Breyta birtingarnafni", "account_edit.profile_tab.button_label": "Sérsníða", @@ -961,12 +966,14 @@ "notifications_permission_banner.title": "Aldrei missa af neinu", "onboarding.follows.back": "Til baka", "onboarding.follows.empty": "Því miður er ekki hægt að birta neinar niðurstöður í augnablikinu. Þú getur reynt að nota leitina eða skoðað könnunarsíðuna til að finna fólk til að fylgjast með, nú eða prófað aftur síðar.", + "onboarding.follows.next": "Næsta: Settu upp notandasniðið þitt", "onboarding.follows.search": "Leita", "onboarding.follows.title": "Þú ættir að fylgjast með fólki til að komast í gang", "onboarding.profile.discoverable": "Gera notandasniðið mitt uppgötvanlegt", "onboarding.profile.discoverable_hint": "Þegar þú velur að hægt sé að uppgötva þig á Mastodon, munu færslurnar þínar birtast í leitarniðurstöðum og vinsældalistum, auk þess sem stungið verður upp á notandasniðinu þínu við fólk sem er með svipuð áhugamál og þú.", "onboarding.profile.display_name": "Birtingarnafn", "onboarding.profile.display_name_hint": "Fullt nafn þitt eða eitthvað til gamans…", + "onboarding.profile.finish": "Ljúka", "onboarding.profile.note": "Æviágrip", "onboarding.profile.note_hint": "Þú getur @minnst á annað fólk eða #myllumerki…", "onboarding.profile.title": "Uppsetning notandasniðs", diff --git a/app/javascript/mastodon/locales/it.json b/app/javascript/mastodon/locales/it.json index 384723db6d..8a675829d8 100644 --- a/app/javascript/mastodon/locales/it.json +++ b/app/javascript/mastodon/locales/it.json @@ -184,6 +184,11 @@ "account_edit.field_reorder_modal.drag_start": "Campo \"{item}\" selezionato.", "account_edit.field_reorder_modal.handle_label": "Trascina il campo \"{item}\"", "account_edit.field_reorder_modal.title": "Riorganizza i campi", + "account_edit.image_edit.add_button": "Aggiungi un'immagine", + "account_edit.image_edit.alt_add_button": "Aggiungi il testo alternativo", + "account_edit.image_edit.alt_edit_button": "Modifica il testo alternativo", + "account_edit.image_edit.remove_button": "Rimuovi l'immagine", + "account_edit.image_edit.replace_button": "Sostituisci l'immagine", "account_edit.name_modal.add_title": "Aggiungi il nome mostrato", "account_edit.name_modal.edit_title": "Modifica il nome mostrato", "account_edit.profile_tab.button_label": "Personalizza", diff --git a/app/javascript/mastodon/locales/pt-PT.json b/app/javascript/mastodon/locales/pt-PT.json index 7bca941543..62f51fc30c 100644 --- a/app/javascript/mastodon/locales/pt-PT.json +++ b/app/javascript/mastodon/locales/pt-PT.json @@ -173,6 +173,7 @@ "account_edit.field_edit_modal.link_emoji_warning": "Não recomendamos o uso de emojis personalizados em combinação com URLs. Campos personalizados que contenham ambos serão exibidos apenas como texto, em vez de como hiperligação, para evitar confusão aos utilizadores.", "account_edit.field_edit_modal.name_hint": "Ex.: \"Site pessoal\"", "account_edit.field_edit_modal.name_label": "Rótulo", + "account_edit.field_edit_modal.value_hint": "Ex.: \"https://exemplo.eu\"", "account_edit.field_edit_modal.value_label": "Valor", "account_edit.field_reorder_modal.drag_cancel": "O arrastamento foi cancelado. O campo \"{item}\" foi largado.", "account_edit.field_reorder_modal.drag_end": "O campo \"{item}\" foi largado.", @@ -182,6 +183,11 @@ "account_edit.field_reorder_modal.drag_start": "Apanhou o campo \"{item}\".", "account_edit.field_reorder_modal.handle_label": "Arrastar o campo \"{item}\"", "account_edit.field_reorder_modal.title": "Reordenar campos", + "account_edit.image_edit.add_button": "Adicionar imagem", + "account_edit.image_edit.alt_add_button": "Adicionar texto alternativo", + "account_edit.image_edit.alt_edit_button": "Editar texto alternativo", + "account_edit.image_edit.remove_button": "Remover imagem", + "account_edit.image_edit.replace_button": "Substituir imagem", "account_edit.name_modal.add_title": "Adicionar nome a mostrar", "account_edit.name_modal.edit_title": "Editar o nome a mostrar", "account_edit.profile_tab.button_label": "Personalizar", @@ -958,12 +964,14 @@ "notifications_permission_banner.title": "Nunca percas nada", "onboarding.follows.back": "Voltar", "onboarding.follows.empty": "Infelizmente não é possível mostrar resultados neste momento. Podes tentar pesquisar ou navegar na página \"Explorar\" para encontrares pessoas para seguires ou tentar novamente mais tarde.", + "onboarding.follows.next": "A seguir: configure o seu perfil", "onboarding.follows.search": "Pesquisar", "onboarding.follows.title": "Segue pessoas para começar", "onboarding.profile.discoverable": "Permitir que o meu perfil seja descoberto", "onboarding.profile.discoverable_hint": "Quando opta pela possibilidade de ser descoberto no Mastodon, as suas mensagens podem aparecer nos resultados de pesquisa e nos destaques, e o seu perfil pode ser sugerido a pessoas com interesses semelhantes aos seus.", "onboarding.profile.display_name": "Nome a apresentar", "onboarding.profile.display_name_hint": "O teu nome completo ou o teu nome divertido…", + "onboarding.profile.finish": "Terminar", "onboarding.profile.note": "Biografia", "onboarding.profile.note_hint": "Podes @mencionar outras pessoas e usar #etiquetas…", "onboarding.profile.title": "Configuração do perfil", diff --git a/app/javascript/mastodon/locales/sq.json b/app/javascript/mastodon/locales/sq.json index 55d52f66c2..ad4471483e 100644 --- a/app/javascript/mastodon/locales/sq.json +++ b/app/javascript/mastodon/locales/sq.json @@ -183,6 +183,11 @@ "account_edit.field_reorder_modal.drag_start": "U mor fusha “{item}”.", "account_edit.field_reorder_modal.handle_label": "Tërhiqni fushën “{item}”", "account_edit.field_reorder_modal.title": "Risistemoni fusha", + "account_edit.image_edit.add_button": "Shtoni figurë", + "account_edit.image_edit.alt_add_button": "Shtoni tekst alternativ", + "account_edit.image_edit.alt_edit_button": "Përpunoni tekst alternativ", + "account_edit.image_edit.remove_button": "Hiqe figurën", + "account_edit.image_edit.replace_button": "Zëvendësoje figurën", "account_edit.name_modal.add_title": "Shtoni emër në ekran", "account_edit.name_modal.edit_title": "Përpunoni emër në ekran", "account_edit.profile_tab.button_label": "Përshtateni", @@ -955,12 +960,14 @@ "notifications_permission_banner.title": "Mos t’ju shpëtojë gjë", "onboarding.follows.back": "Mbrapsht", "onboarding.follows.empty": "Mjerisht, s’mund të shfaqen përfundime tani. Mund të provoni të përdorni kërkimin, ose të shfletoni faqen e eksplorimit, që të gjeni persona për ndjekje, ose të riprovoni më vonë.", + "onboarding.follows.next": "Pasuesi: Ujdisni profilin tuaj", "onboarding.follows.search": "Kërkoni", "onboarding.follows.title": "Që t’ia filloni, ndiqni persona", "onboarding.profile.discoverable": "Bëje profilin tim të zbulueshëm", "onboarding.profile.discoverable_hint": "Kur zgjidhni të jeni i zbulueshëm në Mastodon, postimet tuaja mund të shfaqen në përfundime kërkimesh dhe gjëra në modë dhe profili juaj mund t’u sugjerohet njerëzve me interesa të ngjashme me ju.", "onboarding.profile.display_name": "Emër në ekran", "onboarding.profile.display_name_hint": "Emri juaj i plotë, ose ç’të doni…", + "onboarding.profile.finish": "Përfundoje", "onboarding.profile.note": "Jetëshkrim", "onboarding.profile.note_hint": "Mund të @përmendni persona të tjerë, ose #hashtagë…", "onboarding.profile.title": "Udjisje profili", diff --git a/app/javascript/mastodon/locales/sv.json b/app/javascript/mastodon/locales/sv.json index 158feb561c..7997992887 100644 --- a/app/javascript/mastodon/locales/sv.json +++ b/app/javascript/mastodon/locales/sv.json @@ -167,6 +167,11 @@ "account_edit.field_edit_modal.name_label": "Etikett", "account_edit.field_edit_modal.url_warning": "För att lägga till en länk, vänligen inkludera {protocol} i början.", "account_edit.field_edit_modal.value_hint": "T.ex. \"https://example.me”", + "account_edit.image_edit.add_button": "Lägg till bild", + "account_edit.image_edit.alt_add_button": "Lägg till alternativtext", + "account_edit.image_edit.alt_edit_button": "Redigera alternativtext", + "account_edit.image_edit.remove_button": "Ta bort bild", + "account_edit.image_edit.replace_button": "Ersätt bild", "account_edit.profile_tab.button_label": "Anpassa", "account_note.placeholder": "Klicka för att lägga till anteckning", "admin.dashboard.daily_retention": "Användarlojalitet per dag efter registrering", diff --git a/app/javascript/mastodon/locales/tr.json b/app/javascript/mastodon/locales/tr.json index e84d8a5462..4494be38e9 100644 --- a/app/javascript/mastodon/locales/tr.json +++ b/app/javascript/mastodon/locales/tr.json @@ -184,6 +184,11 @@ "account_edit.field_reorder_modal.drag_start": "\"{item}\" alanı seçildi.", "account_edit.field_reorder_modal.handle_label": "\"{item}\" alanını sürükle", "account_edit.field_reorder_modal.title": "Alanları yeniden düzenle", + "account_edit.image_edit.add_button": "Görsel ekle", + "account_edit.image_edit.alt_add_button": "Alternatif metin ekle", + "account_edit.image_edit.alt_edit_button": "Alternatif metni düzenle", + "account_edit.image_edit.remove_button": "Görseli kaldır", + "account_edit.image_edit.replace_button": "Görseli değiştir", "account_edit.name_modal.add_title": "Görünen ad ekle", "account_edit.name_modal.edit_title": "Görünen adı düzenle", "account_edit.profile_tab.button_label": "Özelleştir", @@ -961,12 +966,14 @@ "notifications_permission_banner.title": "Hiçbir şeyi kaçırmayın", "onboarding.follows.back": "Geri", "onboarding.follows.empty": "Maalesef şu an bir sonuç gösterilemiyor. Takip edilecek kişileri bulmak için arama veya keşfet sayfasına gözatmayı kullanabilirsiniz veya daha sonra tekrar deneyin.", + "onboarding.follows.next": "Sonraki: Profilinizi ayarlayın", "onboarding.follows.search": "Ara", "onboarding.follows.title": "Başlamak için insanları takip edin", "onboarding.profile.discoverable": "Profilimi keşfedilebilir yap", "onboarding.profile.discoverable_hint": "Mastodon'da keşfedilebilirliği etkinleştirdiğinizde, gönderileriniz arama sonuçlarında ve trendlerde görünebilir aynı zamanda profiliniz sizinle benzer ilgi alanlarına sahip kişilere önerilebilir.", "onboarding.profile.display_name": "Görünen isim", "onboarding.profile.display_name_hint": "Tam adınız veya kullanıcı adınız…", + "onboarding.profile.finish": "Tamamla", "onboarding.profile.note": "Kişisel bilgiler", "onboarding.profile.note_hint": "Diğer insanlara @değinebilir veya #etiketler kullanabilirsiniz…", "onboarding.profile.title": "Profilini ayarla", diff --git a/app/javascript/mastodon/locales/vi.json b/app/javascript/mastodon/locales/vi.json index 911d1c0fcb..34b8770b81 100644 --- a/app/javascript/mastodon/locales/vi.json +++ b/app/javascript/mastodon/locales/vi.json @@ -184,6 +184,11 @@ "account_edit.field_reorder_modal.drag_start": "Đã chọn trường \"{item}\".", "account_edit.field_reorder_modal.handle_label": "Kéo trường \"{item}\"", "account_edit.field_reorder_modal.title": "Sắp xếp lại trường", + "account_edit.image_edit.add_button": "Thêm ảnh", + "account_edit.image_edit.alt_add_button": "Thêm văn bản thay thế", + "account_edit.image_edit.alt_edit_button": "Sửa văn bản thay thế", + "account_edit.image_edit.remove_button": "Gỡ ảnh", + "account_edit.image_edit.replace_button": "Thay thế ảnh", "account_edit.name_modal.add_title": "Thêm tên gọi", "account_edit.name_modal.edit_title": "Sửa tên gọi", "account_edit.profile_tab.button_label": "Tùy chỉnh", diff --git a/app/javascript/mastodon/locales/zh-CN.json b/app/javascript/mastodon/locales/zh-CN.json index 9002fd1b01..7e3282fc19 100644 --- a/app/javascript/mastodon/locales/zh-CN.json +++ b/app/javascript/mastodon/locales/zh-CN.json @@ -173,6 +173,8 @@ "account_edit.field_edit_modal.link_emoji_warning": "我们建议不要同时使用自定义表情和网址。同时包含两者的自定义字段将会显示为纯文本而不是链接形式,以避免用户混淆。", "account_edit.field_edit_modal.name_hint": "例如:“个人网站”", "account_edit.field_edit_modal.name_label": "标签", + "account_edit.field_edit_modal.url_warning": "要添加链接,请在开头加上 {protocol}。", + "account_edit.field_edit_modal.value_hint": "例如:“https://example.me”", "account_edit.field_edit_modal.value_label": "值", "account_edit.field_reorder_modal.drag_cancel": "拖拽已终止。字段“{item}”已被丢弃。", "account_edit.field_reorder_modal.drag_end": "字段“{item}”已被丢弃。", @@ -182,6 +184,11 @@ "account_edit.field_reorder_modal.drag_start": "已选中字段“{item}”。", "account_edit.field_reorder_modal.handle_label": "拖拽字段“{item}”", "account_edit.field_reorder_modal.title": "重新排列字段", + "account_edit.image_edit.add_button": "添加图片", + "account_edit.image_edit.alt_add_button": "添加替代文本", + "account_edit.image_edit.alt_edit_button": "编辑替代文本", + "account_edit.image_edit.remove_button": "移除图片", + "account_edit.image_edit.replace_button": "替换图片", "account_edit.name_modal.add_title": "添加显示名称", "account_edit.name_modal.edit_title": "编辑显示名称", "account_edit.profile_tab.button_label": "自定义", @@ -959,12 +966,14 @@ "notifications_permission_banner.title": "精彩不容错过", "onboarding.follows.back": "返回", "onboarding.follows.empty": "很抱歉,现在无法显示任何结果。你可以尝试使用搜索或浏览探索页面来查找要关注的人,或稍后再试。", + "onboarding.follows.next": "下一步:设置你的个人资料", "onboarding.follows.search": "搜索", "onboarding.follows.title": "关注用户,玩转 Mastodon", "onboarding.profile.discoverable": "让我的账号可被他人发现", "onboarding.profile.discoverable_hint": "当你在 Mastodon 上启用发现功能时,你的嘟文可能会出现在搜索结果与热门中,你的账号可能会被推荐给与你兴趣相似的人。", "onboarding.profile.display_name": "昵称", "onboarding.profile.display_name_hint": "你的全名或昵称…", + "onboarding.profile.finish": "完成", "onboarding.profile.note": "简介", "onboarding.profile.note_hint": "你可以提及 @其他人 或使用 #话题…", "onboarding.profile.title": "设置个人资料", diff --git a/app/javascript/mastodon/locales/zh-TW.json b/app/javascript/mastodon/locales/zh-TW.json index 7dffcfc992..4258450790 100644 --- a/app/javascript/mastodon/locales/zh-TW.json +++ b/app/javascript/mastodon/locales/zh-TW.json @@ -184,6 +184,11 @@ "account_edit.field_reorder_modal.drag_start": "已選取欄位「{item}」。", "account_edit.field_reorder_modal.handle_label": "拖放欄位「{item}」", "account_edit.field_reorder_modal.title": "重新整理欄位", + "account_edit.image_edit.add_button": "新增圖片", + "account_edit.image_edit.alt_add_button": "新增 ALT 說明文字", + "account_edit.image_edit.alt_edit_button": "編輯 ALT 說明文字", + "account_edit.image_edit.remove_button": "移除圖片", + "account_edit.image_edit.replace_button": "替換圖片", "account_edit.name_modal.add_title": "新增顯示名稱", "account_edit.name_modal.edit_title": "編輯顯示名稱", "account_edit.profile_tab.button_label": "自訂", diff --git a/app/javascript/mastodon/reducers/slices/collections.ts b/app/javascript/mastodon/reducers/slices/collections.ts index a534a13440..47bbd5e6ba 100644 --- a/app/javascript/mastodon/reducers/slices/collections.ts +++ b/app/javascript/mastodon/reducers/slices/collections.ts @@ -23,6 +23,7 @@ import { createAppSelector, createDataLoadingThunk, } from '@/mastodon/store/typed_functions'; +import { inputToHashtag } from '@/mastodon/utils/hashtags'; type QueryStatus = 'idle' | 'loading' | 'error'; @@ -82,7 +83,7 @@ const collectionSlice = createSlice({ id: collection?.id ?? null, name: collection?.name ?? '', description: collection?.description ?? '', - topic: collection?.tag?.name ?? '', + topic: inputToHashtag(collection?.tag?.name ?? ''), language: collection?.language ?? '', discoverable: collection?.discoverable ?? true, sensitive: collection?.sensitive ?? false, diff --git a/app/javascript/mastodon/utils/hashtags.ts b/app/javascript/mastodon/utils/hashtags.ts index d14efe5db3..ff90a88465 100644 --- a/app/javascript/mastodon/utils/hashtags.ts +++ b/app/javascript/mastodon/utils/hashtags.ts @@ -59,3 +59,8 @@ export const inputToHashtag = (input: string): string => { return `#${words.join('')}${trailingSpace}`; }; + +export const hasSpecialCharacters = (input: string) => { + // Regex matches any character NOT a letter/digit, except the # + return /[^a-zA-Z0-9# ]/.test(input); +}; diff --git a/app/javascript/styles/mastodon/components.scss b/app/javascript/styles/mastodon/components.scss index a89c9cca5c..765bf4dcf6 100644 --- a/app/javascript/styles/mastodon/components.scss +++ b/app/javascript/styles/mastodon/components.scss @@ -8478,7 +8478,7 @@ noscript { gap: 8px; $button-breakpoint: 420px; - $button-fallback-breakpoint: #{$button-breakpoint} + 55px; + $button-fallback-breakpoint: $button-breakpoint + 55px; &--desktop { margin-top: 55px; @@ -8502,7 +8502,7 @@ noscript { } @supports (not (container-type: inline-size)) { - @media (min-width: (#{$button-fallback-breakpoint} + 1px)) { + @media (min-width: ($button-fallback-breakpoint + 1px)) { display: none; } } diff --git a/app/javascript/styles/mastodon/containers.scss b/app/javascript/styles/mastodon/containers.scss index 57c62a29e3..5e199273e0 100644 --- a/app/javascript/styles/mastodon/containers.scss +++ b/app/javascript/styles/mastodon/containers.scss @@ -13,28 +13,28 @@ .logo-container { margin: 50px auto; - h1 { + a { display: flex; justify-content: center; align-items: center; + width: min-content; + margin: 0 auto; + padding: 12px 16px; + color: var(--color-text-primary); + text-decoration: none; + outline: 0; + line-height: 32px; + font-weight: 500; + font-size: 14px; - .logo { - height: 42px; - margin-inline-end: 10px; + &:focus-visible { + outline: var(--outline-focus-default); } + } - a { - display: flex; - justify-content: center; - align-items: center; - color: var(--color-text-primary); - text-decoration: none; - outline: 0; - padding: 12px 16px; - line-height: 32px; - font-weight: 500; - font-size: 14px; - } + .logo { + height: 42px; + margin-inline-end: 10px; } } diff --git a/app/javascript/styles/mastodon/forms.scss b/app/javascript/styles/mastodon/forms.scss index 3305d1c3cd..c25bdfc820 100644 --- a/app/javascript/styles/mastodon/forms.scss +++ b/app/javascript/styles/mastodon/forms.scss @@ -77,7 +77,6 @@ code { .input { margin-bottom: 16px; - overflow: hidden; &:last-child { margin-bottom: 0; @@ -471,13 +470,19 @@ code { } } - .input.radio_buttons .radio label { - margin-bottom: 5px; - font-family: inherit; - font-size: 14px; - color: var(--color-text-primary); - display: block; - width: auto; + .input.radio_buttons .radio { + label { + margin-bottom: 5px; + font-family: inherit; + font-size: 14px; + color: var(--color-text-primary); + display: block; + width: auto; + } + + input[type='radio'] { + accent-color: var(--color-text-brand); + } } .check_boxes { @@ -503,6 +508,12 @@ code { } } + label.checkbox { + input[type='checkbox'] { + accent-color: var(--color-text-brand); + } + } + .input.static .label_input__wrapper { font-size: 14px; padding: 10px; @@ -523,13 +534,20 @@ code { color: var(--color-text-primary); display: block; width: 100%; - outline: 0; font-family: inherit; resize: vertical; background: var(--color-bg-secondary); border: 1px solid var(--color-border-primary); border-radius: 4px; padding: 10px 16px; + outline: var(--outline-focus-default); + outline-offset: -2px; + outline-color: transparent; + transition: outline-color 0.15s ease-out; + + &:focus { + outline: var(--outline-focus-default); + } &:invalid { box-shadow: none; @@ -613,6 +631,11 @@ code { margin-inline-end: 0; } + &:focus-visible { + outline: var(--outline-focus-default); + outline-offset: 2px; + } + &:active, &:focus, &:hover { @@ -653,6 +676,11 @@ code { padding-inline-end: 30px; height: 41px; + &:focus-visible { + outline: var(--outline-focus-default); + outline-offset: 2px; + } + @media screen and (width <= 600px) { font-size: 16px; } diff --git a/app/models/account_migration.rb b/app/models/account_migration.rb index b4ab0d604b..c2c6355a18 100644 --- a/app/models/account_migration.rb +++ b/app/models/account_migration.rb @@ -25,7 +25,10 @@ class AccountMigration < ApplicationRecord before_validation :set_target_account before_validation :set_followers_count + attribute :current_username, :string + normalizes :acct, with: ->(acct) { acct.strip.delete_prefix('@') } + normalizes :current_username, with: ->(value) { value.strip.delete_prefix('@') } validates :acct, presence: true, domain: { acct: true } validate :validate_migration_cooldown @@ -33,7 +36,7 @@ class AccountMigration < ApplicationRecord scope :within_cooldown, -> { where(created_at: cooldown_duration_ago..) } - attr_accessor :current_password, :current_username + attr_accessor :current_password def self.cooldown_duration_ago Time.current - COOLDOWN_PERIOD diff --git a/app/views/auth/sessions/new.html.haml b/app/views/auth/sessions/new.html.haml index 67b958d38c..91505820d4 100644 --- a/app/views/auth/sessions/new.html.haml +++ b/app/views/auth/sessions/new.html.haml @@ -12,6 +12,7 @@ - if use_seamless_external_login? = f.input :email, autofocus: true, + required: true, hint: false, input_html: { 'aria-label': t('simple_form.labels.defaults.username_or_email') }, label: t('simple_form.labels.defaults.username_or_email'), @@ -19,12 +20,14 @@ - else = f.input :email, autofocus: true, + required: true, hint: false, input_html: { 'aria-label': t('simple_form.labels.defaults.email') }, label: t('simple_form.labels.defaults.email'), wrapper: :with_label .fields-group = f.input :password, + required: true, hint: false, input_html: { 'aria-label': t('simple_form.labels.defaults.password'), autocomplete: 'current-password' }, label: t('simple_form.labels.defaults.password'), diff --git a/app/views/layouts/auth.html.haml b/app/views/layouts/auth.html.haml index 1d106b7b13..9f6b32a8f9 100644 --- a/app/views/layouts/auth.html.haml +++ b/app/views/layouts/auth.html.haml @@ -4,12 +4,11 @@ - content_for :content do .container-alt .logo-container - %h1 - - if within_authorization_flow? + - if within_authorization_flow? + = logo_as_symbol(:wordmark) + - else + = link_to root_path do = logo_as_symbol(:wordmark) - - else - = link_to root_path do - = logo_as_symbol(:wordmark) .form-container = render 'flashes' diff --git a/config/locales/simple_form.fi.yml b/config/locales/simple_form.fi.yml index 9854ef31a7..1187ffdf05 100644 --- a/config/locales/simple_form.fi.yml +++ b/config/locales/simple_form.fi.yml @@ -257,7 +257,7 @@ fi: setting_emoji_style: Emojityyli setting_expand_spoilers: Laajenna aina sisältövaroituksilla merkityt julkaisut setting_hide_network: Piilota verkostotietosi - setting_missing_alt_text_modal: Varoita ennen kuin julkaisen mediaa ilman vaihtoehtoista tekstiä + setting_missing_alt_text_modal: Varoita ennen kuin julkaisen mediaa ilman tekstivastinetta setting_quick_boosting: Ota nopea tehostus käyttöön setting_reduce_motion: Vähennä animaatioiden liikettä setting_system_font_ui: Käytä järjestelmän oletusfonttia diff --git a/spec/models/account_migration_spec.rb b/spec/models/account_migration_spec.rb index b92771e8f5..1bb238f7ef 100644 --- a/spec/models/account_migration_spec.rb +++ b/spec/models/account_migration_spec.rb @@ -7,6 +7,10 @@ RSpec.describe AccountMigration do describe 'acct' do it { is_expected.to normalize(:acct).from(' @username@domain ').to('username@domain') } end + + describe 'current_username' do + it { is_expected.to normalize(:current_username).from(' @username ').to('username') } + end end describe 'Validations' do diff --git a/spec/requests/username_rewrites_spec.rb b/spec/requests/username_rewrites_spec.rb new file mode 100644 index 0000000000..9e025a5f0d --- /dev/null +++ b/spec/requests/username_rewrites_spec.rb @@ -0,0 +1,53 @@ +# frozen_string_literal: true + +require 'rails_helper' + +RSpec.describe 'Username URL rewrites' do + describe 'GET /users/:username' do + it 'redirects to at-username page variation' do + get '/users/username' + + expect(response) + .to have_http_status(301) + .and redirect_to('/@username') + expect(response.headers) + .to include('Vary' => 'Origin, Accept') + end + end + + describe 'GET /users/:username/following' do + it 'redirects to at-username page variation' do + get '/users/username/following' + + expect(response) + .to have_http_status(301) + .and redirect_to('/@username/following') + expect(response.headers) + .to include('Vary' => 'Origin, Accept') + end + end + + describe 'GET /users/:username/followers' do + it 'redirects to at-username page variation' do + get '/users/username/followers' + + expect(response) + .to have_http_status(301) + .and redirect_to('/@username/followers') + expect(response.headers) + .to include('Vary' => 'Origin, Accept') + end + end + + describe 'GET /users/:username/statuses/:id' do + it 'redirects to at-username page variation' do + get '/users/username/statuses/123456' + + expect(response) + .to have_http_status(301) + .and redirect_to('/@username/123456') + expect(response.headers) + .to include('Vary' => 'Origin, Accept') + end + end +end diff --git a/spec/system/settings/migrations_spec.rb b/spec/system/settings/migrations_spec.rb index fecde36f35..d95636a609 100644 --- a/spec/system/settings/migrations_spec.rb +++ b/spec/system/settings/migrations_spec.rb @@ -33,20 +33,36 @@ RSpec.describe 'Settings Migrations' do end describe 'Creating migrations' do - let(:user) { Fabricate(:user, password: '12345678') } + let(:user) { Fabricate(:user, password:) } + let(:password) { '12345678' } before { sign_in(user) } context 'when migration account is changed' do let(:acct) { Fabricate(:account, also_known_as: [ActivityPub::TagManager.instance.uri_for(user.account)]) } - it 'updates moved to account' do - visit settings_migration_path + context 'when user has encrypted password' do + it 'updates moved to account' do + visit settings_migration_path - expect { fill_in_and_submit } - .to(change { user.account.reload.moved_to_account_id }.to(acct.id)) - expect(page) - .to have_content(I18n.t('settings.migrate')) + expect { fill_in_and_submit } + .to(change { user.account.reload.moved_to_account_id }.to(acct.id)) + expect(page) + .to have_content(I18n.t('settings.migrate')) + end + end + + context 'when user has blank encrypted password value' do + before { user.update! encrypted_password: '' } + + it 'updates moved to account using at-username value' do + visit settings_migration_path + + expect { fill_in_and_submit_via_username("@#{user.account.username}") } + .to(change { user.account.reload.moved_to_account_id }.to(acct.id)) + expect(page) + .to have_content(I18n.t('settings.migrate')) + end end end @@ -92,8 +108,18 @@ RSpec.describe 'Settings Migrations' do def fill_in_and_submit fill_in 'account_migration_acct', with: acct.username - fill_in 'account_migration_current_password', with: '12345678' + if block_given? + yield + else + fill_in 'account_migration_current_password', with: password + end click_on I18n.t('migrations.proceed_with_move') end + + def fill_in_and_submit_via_username(username) + fill_in_and_submit do + fill_in 'account_migration_current_username', with: username + end + end end end diff --git a/yarn.lock b/yarn.lock index 3b8fea5ffe..191d587972 100644 --- a/yarn.lock +++ b/yarn.lock @@ -8594,10 +8594,10 @@ __metadata: languageName: node linkType: hard -"immutable@npm:^5.0.2": - version: 5.0.3 - resolution: "immutable@npm:5.0.3" - checksum: 10c0/3269827789e1026cd25c2ea97f0b2c19be852ffd49eda1b674b20178f73d84fa8d945ad6f5ac5bc4545c2b4170af9f6e1f77129bc1cae7974a4bf9b04a9cdfb9 +"immutable@npm:^5.1.5": + version: 5.1.5 + resolution: "immutable@npm:5.1.5" + checksum: 10c0/8017ece1578e3c5939ba3305176aee059def1b8a90c7fa2a347ef583ebbd38cbe77ce1bbd786a5fab57e2da00bbcb0493b92e4332cdc4e1fe5cfb09a4688df31 languageName: node linkType: hard @@ -12568,19 +12568,19 @@ __metadata: linkType: hard "sass@npm:^1.62.1, sass@npm:^1.70.0": - version: 1.97.3 - resolution: "sass@npm:1.97.3" + version: 1.98.0 + resolution: "sass@npm:1.98.0" dependencies: "@parcel/watcher": "npm:^2.4.1" chokidar: "npm:^4.0.0" - immutable: "npm:^5.0.2" + immutable: "npm:^5.1.5" source-map-js: "npm:>=0.6.2 <2.0.0" dependenciesMeta: "@parcel/watcher": optional: true bin: sass: sass.js - checksum: 10c0/67f6b5d220f20c1c23a8b16dda5fd1c5d119ad5caf8195b185d553b5b239fb188a3787f04fc00171c62515f2c4e5e0eb5ad4992a80f8543428556883c1240ba3 + checksum: 10c0/9e91daa20f970fefb364ac31289f070636da7aa7eaeb43e371ea98fa98085a6dbc2d3d058504226a02d07717faf0a4ce8d41b579ecb428c4a9d96b4dc1944a95 languageName: node linkType: hard