Merge pull request #3441 from ClearlyClaire/glitch-soc/merge-upstream

Merge upstream changes up to 89b7a3d7fb
This commit is contained in:
Claire
2026-03-14 12:55:51 +01:00
committed by GitHub
107 changed files with 1545 additions and 201 deletions

View File

@@ -11,11 +11,14 @@ module BrandingHelper
end end
def _logo_as_symbol_wordmark 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 end
def _logo_as_symbol_icon 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 end
def render_logo def render_logo

View File

@@ -0,0 +1,25 @@
import type { Meta, StoryObj } from '@storybook/react-vite';
import { A11yLiveRegion } from '.';
const meta = {
title: 'Components/A11yLiveRegion',
component: A11yLiveRegion,
} satisfies Meta<typeof A11yLiveRegion>;
export default meta;
type Story = StoryObj<typeof meta>;
export const Polite: Story = {
args: {
children: "This field can't be empty.",
},
};
export const Assertive: Story = {
args: {
...Polite.args,
role: 'alert',
},
};

View File

@@ -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 (
<Component
role={role}
aria-live={role === 'alert' ? 'assertive' : 'polite'}
ref={ref}
{...props}
>
{children}
</Component>
);
},
);

View File

@@ -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<typeof CalloutInline>;
export default meta;
type Story = StoryObj<typeof meta>;
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',
},
};

View File

@@ -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<FieldStatus['variant'], React.FunctionComponent> = {
error: ErrorIcon,
warning: WarningIcon,
info: InfoIcon,
success: CheckIcon,
};
export const CalloutInline: FC<
Partial<FieldStatus> & React.ComponentPropsWithoutRef<'div'>
> = ({ variant = 'error', message, className, children, ...props }) => {
return (
<div
{...props}
className={classNames(className, classes.wrapper)}
data-variant={variant}
>
<Icon id={variant} icon={iconMap[variant]} className={classes.icon} />
{message ?? children}
</div>
);
};

View File

@@ -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;
}

View File

@@ -76,7 +76,7 @@ export const Optional: Story = {
export const WithError: Story = { export const WithError: Story = {
args: { args: {
required: false, required: false,
hasError: true, status: 'error',
}, },
}; };

View File

@@ -13,12 +13,12 @@ type Props = Omit<ComponentPropsWithoutRef<'input'>, 'type'> & {
export const CheckboxField = forwardRef< export const CheckboxField = forwardRef<
HTMLInputElement, HTMLInputElement,
Props & CommonFieldWrapperProps Props & CommonFieldWrapperProps
>(({ id, label, hint, hasError, required, ...otherProps }, ref) => ( >(({ id, label, hint, status, required, ...otherProps }, ref) => (
<FormFieldWrapper <FormFieldWrapper
label={label} label={label}
hint={hint} hint={hint}
required={required} required={required}
hasError={hasError} status={status}
inputId={id} inputId={id}
inputPlacement='inline-start' inputPlacement='inline-start'
> >

View File

@@ -86,14 +86,14 @@ interface Props<T extends ComboboxItem>
*/ */
export const ComboboxFieldWithRef = <T extends ComboboxItem>( export const ComboboxFieldWithRef = <T extends ComboboxItem>(
{ id, label, hint, hasError, required, ...otherProps }: Props<T>, { id, label, hint, status, required, ...otherProps }: Props<T>,
ref: React.ForwardedRef<HTMLInputElement>, ref: React.ForwardedRef<HTMLInputElement>,
) => ( ) => (
<FormFieldWrapper <FormFieldWrapper
label={label} label={label}
hint={hint} hint={hint}
required={required} required={required}
hasError={hasError} status={status}
inputId={id} inputId={id}
> >
{(inputProps) => <Combobox {...otherProps} {...inputProps} ref={ref} />} {(inputProps) => <Combobox {...otherProps} {...inputProps} ref={ref} />}

View File

@@ -22,7 +22,7 @@ interface CopyLinkFieldProps extends CommonFieldWrapperProps, TextInputProps {
export const CopyLinkField = forwardRef<HTMLInputElement, CopyLinkFieldProps>( export const CopyLinkField = forwardRef<HTMLInputElement, CopyLinkFieldProps>(
( (
{ id, label, hint, hasError, value, required, className, ...otherProps }, { id, label, hint, status, value, required, className, ...otherProps },
ref, ref,
) => { ) => {
const intl = useIntl(); const intl = useIntl();
@@ -48,7 +48,7 @@ export const CopyLinkField = forwardRef<HTMLInputElement, CopyLinkFieldProps>(
label={label} label={label}
hint={hint} hint={hint}
required={required} required={required}
hasError={hasError} status={status}
inputId={id} inputId={id}
> >
{(inputProps) => ( {(inputProps) => (

View File

@@ -37,7 +37,7 @@ export const EmojiTextInputField: FC<
value, value,
label, label,
hint, hint,
hasError, status,
maxLength, maxLength,
counterMax = maxLength, counterMax = maxLength,
recommended, recommended,
@@ -49,7 +49,7 @@ export const EmojiTextInputField: FC<
const wrapperProps = { const wrapperProps = {
label, label,
hint, hint,
hasError, status,
counterMax, counterMax,
recommended, recommended,
disabled, disabled,
@@ -84,7 +84,7 @@ export const EmojiTextAreaField: FC<
recommended, recommended,
disabled, disabled,
hint, hint,
hasError, status,
...otherProps ...otherProps
}) => { }) => {
const textareaRef = useRef<HTMLTextAreaElement>(null); const textareaRef = useRef<HTMLTextAreaElement>(null);
@@ -92,7 +92,7 @@ export const EmojiTextAreaField: FC<
const wrapperProps = { const wrapperProps = {
label, label,
hint, hint,
hasError, status,
counterMax, counterMax,
recommended, recommended,
disabled, disabled,

View File

@@ -1,7 +1,9 @@
.fieldset { .fieldset {
--container-gap: 12px;
display: flex; display: flex;
flex-direction: column; flex-direction: column;
gap: 12px; gap: var(--container-gap);
color: var(--color-text-primary); color: var(--color-text-primary);
font-size: 15px; font-size: 15px;
} }
@@ -17,3 +19,11 @@
column-gap: 24px; 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));
}
}

View File

@@ -3,14 +3,19 @@
import type { ReactNode, FC } from 'react'; import type { ReactNode, FC } from 'react';
import { createContext, useId } 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 classes from './fieldset.module.scss';
import { getFieldStatus } from './form_field_wrapper';
import formFieldWrapperClasses from './form_field_wrapper.module.scss'; import formFieldWrapperClasses from './form_field_wrapper.module.scss';
interface FieldsetProps { interface FieldsetProps {
legend: ReactNode; legend: ReactNode;
hint?: ReactNode; hint?: ReactNode;
name?: string; name?: string;
hasError?: boolean; status?: FieldStatus | FieldStatus['variant'];
layout?: 'vertical' | 'horizontal'; layout?: 'vertical' | 'horizontal';
children: ReactNode; children: ReactNode;
} }
@@ -26,22 +31,33 @@ export const Fieldset: FC<FieldsetProps> = ({
legend, legend,
hint, hint,
name, name,
hasError, status,
layout, layout,
children, children,
}) => { }) => {
const uniqueId = useId(); const uniqueId = useId();
const labelId = `${uniqueId}-label`; const labelId = `${uniqueId}-label`;
const hintId = `${uniqueId}-hint`; const hintId = `${uniqueId}-hint`;
const statusId = `${uniqueId}-status`;
const fieldsetName = name || `${uniqueId}-fieldset-name`; const fieldsetName = name || `${uniqueId}-fieldset-name`;
const hasHint = !!hint; const hasHint = !!hint;
const fieldStatus = getFieldStatus(status);
const hasStatusMessage = !!fieldStatus?.message;
const descriptionIds = [
hasHint ? hintId : '',
hasStatusMessage ? statusId : '',
]
.filter((id) => !!id)
.join(' ');
return ( return (
<fieldset <fieldset
className={classes.fieldset} className={classes.fieldset}
data-has-error={hasError} data-has-error={status === 'error'}
aria-labelledby={labelId} aria-labelledby={labelId}
aria-describedby={hintId} aria-describedby={descriptionIds}
> >
<div className={formFieldWrapperClasses.labelWrapper}> <div className={formFieldWrapperClasses.labelWrapper}>
<div id={labelId} className={formFieldWrapperClasses.label}> <div id={labelId} className={formFieldWrapperClasses.label}>
@@ -59,6 +75,11 @@ export const Fieldset: FC<FieldsetProps> = ({
{children} {children}
</FieldsetNameContext.Provider> </FieldsetNameContext.Provider>
</div> </div>
{/* Live region must be rendered even when empty */}
<A11yLiveRegion className={classes.status} id={statusId}>
{hasStatusMessage && <CalloutInline {...fieldStatus} />}
</A11yLiveRegion>
</fieldset> </fieldset>
); );
}; };

View File

@@ -46,6 +46,14 @@
font-size: 13px; 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 { .inputWrapper {
display: block; display: block;
} }

View File

@@ -7,6 +7,10 @@ import { FormattedMessage } from 'react-intl';
import classNames from 'classnames'; 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 { FieldsetNameContext } from './fieldset';
import classes from './form_field_wrapper.module.scss'; import classes from './form_field_wrapper.module.scss';
@@ -20,7 +24,7 @@ interface FieldWrapperProps {
label: ReactNode; label: ReactNode;
hint?: ReactNode; hint?: ReactNode;
required?: boolean; required?: boolean;
hasError?: boolean; status?: FieldStatus['variant'] | FieldStatus;
inputId?: string; inputId?: string;
describedById?: string; describedById?: string;
inputPlacement?: 'inline-start' | 'inline-end'; inputPlacement?: 'inline-start' | 'inline-end';
@@ -33,7 +37,7 @@ interface FieldWrapperProps {
*/ */
export type CommonFieldWrapperProps = Pick< export type CommonFieldWrapperProps = Pick<
FieldWrapperProps, FieldWrapperProps,
'label' | 'hint' | 'hasError' 'label' | 'hint' | 'status'
> & { wrapperClassName?: string }; > & { wrapperClassName?: string };
/** /**
@@ -48,27 +52,31 @@ export const FormFieldWrapper: FC<FieldWrapperProps> = ({
hint, hint,
describedById, describedById,
required, required,
hasError, status,
inputPlacement, inputPlacement,
children, children,
className, className,
}) => { }) => {
const uniqueId = useId(); const uniqueId = useId();
const inputId = inputIdProp || `${uniqueId}-input`; const inputId = inputIdProp || `${uniqueId}-input`;
const statusId = `${inputIdProp || uniqueId}-status`;
const hintId = `${inputIdProp || uniqueId}-hint`; const hintId = `${inputIdProp || uniqueId}-hint`;
const hasHint = !!hint; const hasHint = !!hint;
const fieldStatus = getFieldStatus(status);
const hasStatusMessage = !!fieldStatus?.message;
const hasParentFieldset = !!useContext(FieldsetNameContext); const hasParentFieldset = !!useContext(FieldsetNameContext);
const descriptionIds =
[hasHint ? hintId : '', hasStatusMessage ? statusId : '', describedById]
.filter((id) => !!id)
.join(' ') || undefined;
const inputProps: InputProps = { const inputProps: InputProps = {
required, required,
id: inputId, id: inputId,
'aria-describedby': descriptionIds,
}; };
if (hasHint) {
inputProps['aria-describedby'] = describedById
? `${describedById} ${hintId}`
: hintId;
}
const input = ( const input = (
<div className={classes.inputWrapper}>{children(inputProps)}</div> <div className={classes.inputWrapper}>{children(inputProps)}</div>
@@ -77,7 +85,7 @@ export const FormFieldWrapper: FC<FieldWrapperProps> = ({
return ( return (
<div <div
className={classNames(classes.wrapper, className)} className={classNames(classes.wrapper, className)}
data-has-error={hasError} data-has-error={fieldStatus?.variant === 'error'}
data-input-placement={inputPlacement} data-input-placement={inputPlacement}
> >
{inputPlacement === 'inline-start' && input} {inputPlacement === 'inline-start' && input}
@@ -100,6 +108,11 @@ export const FormFieldWrapper: FC<FieldWrapperProps> = ({
</div> </div>
{inputPlacement !== 'inline-start' && input} {inputPlacement !== 'inline-start' && input}
{/* Live region must be rendered even when empty */}
<A11yLiveRegion className={classes.status} id={statusId}>
{hasStatusMessage && <CalloutInline {...fieldStatus} />}
</A11yLiveRegion>
</div> </div>
); );
}; };
@@ -121,3 +134,19 @@ const RequiredMark: FC<{ required?: boolean }> = ({ required }) =>
<FormattedMessage id='form_field.optional' defaultMessage='(optional)' /> <FormattedMessage id='form_field.optional' defaultMessage='(optional)' />
</> </>
); );
export function getFieldStatus(status: FieldWrapperProps['status']) {
if (!status) {
return null;
}
if (typeof status === 'string') {
const fieldStatus: FieldStatus = {
variant: status,
message: '',
};
return fieldStatus;
}
return status;
}

View File

@@ -71,7 +71,7 @@ export const Optional: Story = {
export const WithError: Story = { export const WithError: Story = {
args: { args: {
required: false, required: false,
hasError: true, status: 'error',
}, },
}; };

View File

@@ -15,7 +15,7 @@ type Props = Omit<ComponentPropsWithoutRef<'input'>, 'type'> & {
export const RadioButtonField = forwardRef< export const RadioButtonField = forwardRef<
HTMLInputElement, HTMLInputElement,
Props & CommonFieldWrapperProps Props & CommonFieldWrapperProps
>(({ id, label, hint, hasError, required, ...otherProps }, ref) => { >(({ id, label, hint, status, required, ...otherProps }, ref) => {
const fieldsetName = useContext(FieldsetNameContext); const fieldsetName = useContext(FieldsetNameContext);
return ( return (
@@ -23,7 +23,7 @@ export const RadioButtonField = forwardRef<
label={label} label={label}
hint={hint} hint={hint}
required={required} required={required}
hasError={hasError} status={status}
inputId={id} inputId={id}
inputPlacement='inline-start' inputPlacement='inline-start'
> >

View File

@@ -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;
}
}

View File

@@ -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<typeof RangeInputField>;
export default meta;
type Story = StoryObj<typeof meta>;
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' },
],
},
};

View File

@@ -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<HTMLInputElement, Props>(
(
{ id, label, hint, status, required, wrapperClassName, ...otherProps },
ref,
) => (
<FormFieldWrapper
label={label}
hint={hint}
required={required}
status={status}
inputId={id}
className={wrapperClassName}
>
{(inputProps) => <RangeInput {...otherProps} {...inputProps} ref={ref} />}
</FormFieldWrapper>
),
);
RangeInputField.displayName = 'RangeInputField';
export const RangeInput = forwardRef<HTMLInputElement, RangeInputProps>(
({ className, markers, id, ...otherProps }, ref) => {
const markersId = useId();
if (!markers) {
return (
<input
{...otherProps}
type='range'
className={classNames(className, classes.input)}
ref={ref}
/>
);
}
return (
<>
<input
{...otherProps}
type='range'
className={classNames(className, classes.input)}
ref={ref}
list={markersId}
/>
<datalist id={markersId} className={classes.markers}>
{markers.map((marker) => {
const value = typeof marker === 'number' ? marker : marker.value;
return (
<option
key={value}
value={value}
label={typeof marker !== 'number' ? marker.label : undefined}
/>
);
})}
</datalist>
</>
);
},
);
RangeInput.displayName = 'RangeInput';

View File

@@ -51,7 +51,7 @@ export const Optional: Story = {
export const WithError: Story = { export const WithError: Story = {
args: { args: {
required: false, required: false,
hasError: true, status: 'error',
}, },
}; };

View File

@@ -19,12 +19,12 @@ interface Props
*/ */
export const SelectField = forwardRef<HTMLSelectElement, Props>( export const SelectField = forwardRef<HTMLSelectElement, Props>(
({ id, label, hint, required, hasError, children, ...otherProps }, ref) => ( ({ id, label, hint, required, status, children, ...otherProps }, ref) => (
<FormFieldWrapper <FormFieldWrapper
label={label} label={label}
hint={hint} hint={hint}
required={required} required={required}
hasError={hasError} status={status}
inputId={id} inputId={id}
> >
{(inputProps) => ( {(inputProps) => (

View File

@@ -38,7 +38,17 @@ export const Optional: Story = {
export const WithError: Story = { export const WithError: Story = {
args: { args: {
required: false, 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',
},
}, },
}; };

View File

@@ -26,14 +26,14 @@ export const TextAreaField = forwardRef<
TextAreaProps & CommonFieldWrapperProps TextAreaProps & CommonFieldWrapperProps
>( >(
( (
{ id, label, hint, required, hasError, wrapperClassName, ...otherProps }, { id, label, hint, required, status, wrapperClassName, ...otherProps },
ref, ref,
) => ( ) => (
<FormFieldWrapper <FormFieldWrapper
label={label} label={label}
hint={hint} hint={hint}
required={required} required={required}
hasError={hasError} status={status}
inputId={id} inputId={id}
className={wrapperClassName} className={wrapperClassName}
> >

View File

@@ -29,16 +29,16 @@
color: var(--color-text-secondary); color: var(--color-text-secondary);
} }
&:focus {
outline-color: var(--color-text-brand);
}
&:focus:user-invalid, &:focus:user-invalid,
&:required:user-invalid, &:required:user-invalid,
[data-has-error='true'] & { [data-has-error='true'] & {
outline-color: var(--color-text-error); outline-color: var(--color-text-error);
} }
&:focus {
outline-color: var(--color-text-brand);
}
&:required:user-valid { &:required:user-valid {
outline-color: var(--color-text-success); outline-color: var(--color-text-success);
} }

View File

@@ -40,7 +40,17 @@ export const Optional: Story = {
export const WithError: Story = { export const WithError: Story = {
args: { args: {
required: false, required: false,
hasError: true, status: 'error',
},
};
export const WithWarning: Story = {
args: {
required: false,
status: {
variant: 'warning',
message: 'Special characters are not allowed',
},
}, },
}; };

View File

@@ -25,14 +25,14 @@ interface Props extends TextInputProps, CommonFieldWrapperProps {}
export const TextInputField = forwardRef<HTMLInputElement, Props>( export const TextInputField = forwardRef<HTMLInputElement, Props>(
( (
{ id, label, hint, hasError, required, wrapperClassName, ...otherProps }, { id, label, hint, status, required, wrapperClassName, ...otherProps },
ref, ref,
) => ( ) => (
<FormFieldWrapper <FormFieldWrapper
label={label} label={label}
hint={hint} hint={hint}
required={required} required={required}
hasError={hasError} status={status}
inputId={id} inputId={id}
className={wrapperClassName} className={wrapperClassName}
> >

View File

@@ -45,7 +45,7 @@ export const Optional: Story = {
export const WithError: Story = { export const WithError: Story = {
args: { args: {
required: false, required: false,
hasError: true, status: 'error',
}, },
}; };

View File

@@ -14,12 +14,12 @@ type Props = Omit<ComponentPropsWithoutRef<'input'>, 'type'> & {
export const ToggleField = forwardRef< export const ToggleField = forwardRef<
HTMLInputElement, HTMLInputElement,
Props & CommonFieldWrapperProps Props & CommonFieldWrapperProps
>(({ id, label, hint, hasError, required, ...otherProps }, ref) => ( >(({ id, label, hint, status, required, ...otherProps }, ref) => (
<FormFieldWrapper <FormFieldWrapper
label={label} label={label}
hint={hint} hint={hint}
required={required} required={required}
hasError={hasError} status={status}
inputId={id} inputId={id}
inputPlacement='inline-end' inputPlacement='inline-end'
> >

View File

@@ -107,7 +107,7 @@
} }
$button-breakpoint: 420px; $button-breakpoint: 420px;
$button-fallback-breakpoint: #{$button-breakpoint} + 55px; $button-fallback-breakpoint: $button-breakpoint + 55px;
.buttonsDesktop { .buttonsDesktop {
@container (width < #{$button-breakpoint}) { @container (width < #{$button-breakpoint}) {
@@ -132,7 +132,7 @@ $button-fallback-breakpoint: #{$button-breakpoint} + 55px;
} }
@supports (not (container-type: inline-size)) { @supports (not (container-type: inline-size)) {
@media (min-width: (#{$button-fallback-breakpoint} + 1px)) { @media (min-width: ($button-fallback-breakpoint + 1px)) {
display: none; display: none;
} }
} }

View File

@@ -141,8 +141,11 @@ const InnerNodeModal: FC<{
onChange={handleChange} onChange={handleChange}
label={intl.formatMessage(messages.fieldLabel)} label={intl.formatMessage(messages.fieldLabel)}
className={classes.noteInput} className={classes.noteInput}
hasError={state === 'error'} status={
hint={errorText} 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. // eslint-disable-next-line jsx-a11y/no-autofocus -- We want to focus here as it's a modal.
autoFocus autoFocus
/> />

View File

@@ -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 { useHistory } from 'react-router-dom';
import { isFulfilled } from '@reduxjs/toolkit'; import { isFulfilled } from '@reduxjs/toolkit';
import { inputToHashtag } from '@/flavours/glitch/utils/hashtags'; import {
hasSpecialCharacters,
inputToHashtag,
} from '@/flavours/glitch/utils/hashtags';
import type { import type {
ApiCreateCollectionPayload, ApiCreateCollectionPayload,
ApiUpdateCollectionPayload, ApiUpdateCollectionPayload,
@@ -31,6 +34,7 @@ import classes from './styles.module.scss';
import { WizardStepHeader } from './wizard_step_header'; import { WizardStepHeader } from './wizard_step_header';
export const CollectionDetails: React.FC = () => { export const CollectionDetails: React.FC = () => {
const intl = useIntl();
const dispatch = useAppDispatch(); const dispatch = useAppDispatch();
const history = useHistory(); const history = useHistory();
const { id, name, description, topic, discoverable, sensitive, accountIds } = const { id, name, description, topic, discoverable, sensitive, accountIds } =
@@ -152,6 +156,11 @@ export const CollectionDetails: React.FC = () => {
], ],
); );
const topicHasSpecialCharacters = useMemo(
() => hasSpecialCharacters(topic),
[topic],
);
return ( return (
<form onSubmit={handleSubmit} className={classes.form}> <form onSubmit={handleSubmit} className={classes.form}>
<FormStack className={classes.formFieldStack}> <FormStack className={classes.formFieldStack}>
@@ -224,6 +233,18 @@ export const CollectionDetails: React.FC = () => {
autoCorrect='off' autoCorrect='off'
spellCheck='false' spellCheck='false'
maxLength={40} maxLength={40}
status={
topicHasSpecialCharacters
? {
variant: 'warning',
message: intl.formatMessage({
id: 'collections.topic_special_chars_hint',
defaultMessage:
'Special characters will be removed when saving',
}),
}
: undefined
}
/> />
<Fieldset <Fieldset

View File

@@ -233,7 +233,7 @@ export const Profile: React.FC<{
} }
value={displayName} value={displayName}
onChange={handleDisplayNameChange} onChange={handleDisplayNameChange}
hasError={!!errors?.display_name} status={errors?.display_name ? 'error' : undefined}
id='display_name' id='display_name'
/> />
</div> </div>
@@ -255,7 +255,7 @@ export const Profile: React.FC<{
} }
value={note} value={note}
onChange={handleNoteChange} onChange={handleNoteChange}
hasError={!!errors?.note} status={errors?.note ? 'error' : undefined}
id='note' id='note'
/> />
</div> </div>

View File

@@ -23,6 +23,7 @@ import {
createAppSelector, createAppSelector,
createDataLoadingThunk, createDataLoadingThunk,
} from '@/flavours/glitch/store/typed_functions'; } from '@/flavours/glitch/store/typed_functions';
import { inputToHashtag } from '@/flavours/glitch/utils/hashtags';
type QueryStatus = 'idle' | 'loading' | 'error'; type QueryStatus = 'idle' | 'loading' | 'error';
@@ -82,7 +83,7 @@ const collectionSlice = createSlice({
id: collection?.id ?? null, id: collection?.id ?? null,
name: collection?.name ?? '', name: collection?.name ?? '',
description: collection?.description ?? '', description: collection?.description ?? '',
topic: collection?.tag?.name ?? '', topic: inputToHashtag(collection?.tag?.name ?? ''),
language: collection?.language ?? '', language: collection?.language ?? '',
discoverable: collection?.discoverable ?? true, discoverable: collection?.discoverable ?? true,
sensitive: collection?.sensitive ?? false, sensitive: collection?.sensitive ?? false,

View File

@@ -8763,7 +8763,7 @@ noscript {
gap: 8px; gap: 8px;
$button-breakpoint: 420px; $button-breakpoint: 420px;
$button-fallback-breakpoint: #{$button-breakpoint} + 55px; $button-fallback-breakpoint: $button-breakpoint + 55px;
&--desktop { &--desktop {
margin-top: 55px; margin-top: 55px;
@@ -8787,7 +8787,7 @@ noscript {
} }
@supports (not (container-type: inline-size)) { @supports (not (container-type: inline-size)) {
@media (min-width: (#{$button-fallback-breakpoint} + 1px)) { @media (min-width: ($button-fallback-breakpoint + 1px)) {
display: none; display: none;
} }
} }

View File

@@ -13,28 +13,28 @@
.logo-container { .logo-container {
margin: 50px auto; margin: 50px auto;
h1 { a {
display: flex; display: flex;
justify-content: center; justify-content: center;
align-items: 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 { &:focus-visible {
height: 42px; outline: var(--outline-focus-default);
margin-inline-end: 10px;
} }
}
a { .logo {
display: flex; height: 42px;
justify-content: center; margin-inline-end: 10px;
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;
}
} }
} }

View File

@@ -77,7 +77,6 @@ code {
.input { .input {
margin-bottom: 16px; margin-bottom: 16px;
overflow: hidden;
&:last-child { &:last-child {
margin-bottom: 0; margin-bottom: 0;
@@ -472,13 +471,19 @@ code {
} }
} }
.input.radio_buttons .radio label { .input.radio_buttons .radio {
margin-bottom: 5px; label {
font-family: inherit; margin-bottom: 5px;
font-size: 14px; font-family: inherit;
color: var(--color-text-primary); font-size: 14px;
display: block; color: var(--color-text-primary);
width: auto; display: block;
width: auto;
}
input[type='radio'] {
accent-color: var(--color-text-brand);
}
} }
.check_boxes { .check_boxes {
@@ -504,6 +509,12 @@ code {
} }
} }
label.checkbox {
input[type='checkbox'] {
accent-color: var(--color-text-brand);
}
}
.input.static .label_input__wrapper { .input.static .label_input__wrapper {
font-size: 14px; font-size: 14px;
padding: 10px; padding: 10px;
@@ -524,13 +535,20 @@ code {
color: var(--color-text-primary); color: var(--color-text-primary);
display: block; display: block;
width: 100%; width: 100%;
outline: 0;
font-family: inherit; font-family: inherit;
resize: vertical; resize: vertical;
background: var(--color-bg-secondary); background: var(--color-bg-secondary);
border: 1px solid var(--color-border-primary); border: 1px solid var(--color-border-primary);
border-radius: 4px; border-radius: 4px;
padding: 10px 16px; 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 { &:invalid {
box-shadow: none; box-shadow: none;
@@ -614,6 +632,11 @@ code {
margin-inline-end: 0; margin-inline-end: 0;
} }
&:focus-visible {
outline: var(--outline-focus-default);
outline-offset: 2px;
}
&:active, &:active,
&:focus, &:focus,
&:hover { &:hover {
@@ -654,6 +677,11 @@ code {
padding-inline-end: 30px; padding-inline-end: 30px;
height: 41px; height: 41px;
&:focus-visible {
outline: var(--outline-focus-default);
outline-offset: 2px;
}
@media screen and (width <= 600px) { @media screen and (width <= 600px) {
font-size: 16px; font-size: 16px;
} }

View File

@@ -59,3 +59,8 @@ export const inputToHashtag = (input: string): string => {
return `#${words.join('')}${trailingSpace}`; 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);
};

View File

@@ -0,0 +1,25 @@
import type { Meta, StoryObj } from '@storybook/react-vite';
import { A11yLiveRegion } from '.';
const meta = {
title: 'Components/A11yLiveRegion',
component: A11yLiveRegion,
} satisfies Meta<typeof A11yLiveRegion>;
export default meta;
type Story = StoryObj<typeof meta>;
export const Polite: Story = {
args: {
children: "This field can't be empty.",
},
};
export const Assertive: Story = {
args: {
...Polite.args,
role: 'alert',
},
};

View File

@@ -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 (
<Component
role={role}
aria-live={role === 'alert' ? 'assertive' : 'polite'}
ref={ref}
{...props}
>
{children}
</Component>
);
},
);

View File

@@ -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<typeof CalloutInline>;
export default meta;
type Story = StoryObj<typeof meta>;
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',
},
};

View File

@@ -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<FieldStatus['variant'], React.FunctionComponent> = {
error: ErrorIcon,
warning: WarningIcon,
info: InfoIcon,
success: CheckIcon,
};
export const CalloutInline: FC<
Partial<FieldStatus> & React.ComponentPropsWithoutRef<'div'>
> = ({ variant = 'error', message, className, children, ...props }) => {
return (
<div
{...props}
className={classNames(className, classes.wrapper)}
data-variant={variant}
>
<Icon id={variant} icon={iconMap[variant]} className={classes.icon} />
{message ?? children}
</div>
);
};

View File

@@ -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;
}

View File

@@ -76,7 +76,7 @@ export const Optional: Story = {
export const WithError: Story = { export const WithError: Story = {
args: { args: {
required: false, required: false,
hasError: true, status: 'error',
}, },
}; };

View File

@@ -13,12 +13,12 @@ type Props = Omit<ComponentPropsWithoutRef<'input'>, 'type'> & {
export const CheckboxField = forwardRef< export const CheckboxField = forwardRef<
HTMLInputElement, HTMLInputElement,
Props & CommonFieldWrapperProps Props & CommonFieldWrapperProps
>(({ id, label, hint, hasError, required, ...otherProps }, ref) => ( >(({ id, label, hint, status, required, ...otherProps }, ref) => (
<FormFieldWrapper <FormFieldWrapper
label={label} label={label}
hint={hint} hint={hint}
required={required} required={required}
hasError={hasError} status={status}
inputId={id} inputId={id}
inputPlacement='inline-start' inputPlacement='inline-start'
> >

View File

@@ -86,14 +86,14 @@ interface Props<T extends ComboboxItem>
*/ */
export const ComboboxFieldWithRef = <T extends ComboboxItem>( export const ComboboxFieldWithRef = <T extends ComboboxItem>(
{ id, label, hint, hasError, required, ...otherProps }: Props<T>, { id, label, hint, status, required, ...otherProps }: Props<T>,
ref: React.ForwardedRef<HTMLInputElement>, ref: React.ForwardedRef<HTMLInputElement>,
) => ( ) => (
<FormFieldWrapper <FormFieldWrapper
label={label} label={label}
hint={hint} hint={hint}
required={required} required={required}
hasError={hasError} status={status}
inputId={id} inputId={id}
> >
{(inputProps) => <Combobox {...otherProps} {...inputProps} ref={ref} />} {(inputProps) => <Combobox {...otherProps} {...inputProps} ref={ref} />}

View File

@@ -22,7 +22,7 @@ interface CopyLinkFieldProps extends CommonFieldWrapperProps, TextInputProps {
export const CopyLinkField = forwardRef<HTMLInputElement, CopyLinkFieldProps>( export const CopyLinkField = forwardRef<HTMLInputElement, CopyLinkFieldProps>(
( (
{ id, label, hint, hasError, value, required, className, ...otherProps }, { id, label, hint, status, value, required, className, ...otherProps },
ref, ref,
) => { ) => {
const intl = useIntl(); const intl = useIntl();
@@ -48,7 +48,7 @@ export const CopyLinkField = forwardRef<HTMLInputElement, CopyLinkFieldProps>(
label={label} label={label}
hint={hint} hint={hint}
required={required} required={required}
hasError={hasError} status={status}
inputId={id} inputId={id}
> >
{(inputProps) => ( {(inputProps) => (

View File

@@ -37,7 +37,7 @@ export const EmojiTextInputField: FC<
value, value,
label, label,
hint, hint,
hasError, status,
maxLength, maxLength,
counterMax = maxLength, counterMax = maxLength,
recommended, recommended,
@@ -49,7 +49,7 @@ export const EmojiTextInputField: FC<
const wrapperProps = { const wrapperProps = {
label, label,
hint, hint,
hasError, status,
counterMax, counterMax,
recommended, recommended,
disabled, disabled,
@@ -84,7 +84,7 @@ export const EmojiTextAreaField: FC<
recommended, recommended,
disabled, disabled,
hint, hint,
hasError, status,
...otherProps ...otherProps
}) => { }) => {
const textareaRef = useRef<HTMLTextAreaElement>(null); const textareaRef = useRef<HTMLTextAreaElement>(null);
@@ -92,7 +92,7 @@ export const EmojiTextAreaField: FC<
const wrapperProps = { const wrapperProps = {
label, label,
hint, hint,
hasError, status,
counterMax, counterMax,
recommended, recommended,
disabled, disabled,

View File

@@ -1,7 +1,9 @@
.fieldset { .fieldset {
--container-gap: 12px;
display: flex; display: flex;
flex-direction: column; flex-direction: column;
gap: 12px; gap: var(--container-gap);
color: var(--color-text-primary); color: var(--color-text-primary);
font-size: 15px; font-size: 15px;
} }
@@ -17,3 +19,11 @@
column-gap: 24px; 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));
}
}

View File

@@ -3,14 +3,19 @@
import type { ReactNode, FC } from 'react'; import type { ReactNode, FC } from 'react';
import { createContext, useId } 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 classes from './fieldset.module.scss';
import { getFieldStatus } from './form_field_wrapper';
import formFieldWrapperClasses from './form_field_wrapper.module.scss'; import formFieldWrapperClasses from './form_field_wrapper.module.scss';
interface FieldsetProps { interface FieldsetProps {
legend: ReactNode; legend: ReactNode;
hint?: ReactNode; hint?: ReactNode;
name?: string; name?: string;
hasError?: boolean; status?: FieldStatus | FieldStatus['variant'];
layout?: 'vertical' | 'horizontal'; layout?: 'vertical' | 'horizontal';
children: ReactNode; children: ReactNode;
} }
@@ -26,22 +31,33 @@ export const Fieldset: FC<FieldsetProps> = ({
legend, legend,
hint, hint,
name, name,
hasError, status,
layout, layout,
children, children,
}) => { }) => {
const uniqueId = useId(); const uniqueId = useId();
const labelId = `${uniqueId}-label`; const labelId = `${uniqueId}-label`;
const hintId = `${uniqueId}-hint`; const hintId = `${uniqueId}-hint`;
const statusId = `${uniqueId}-status`;
const fieldsetName = name || `${uniqueId}-fieldset-name`; const fieldsetName = name || `${uniqueId}-fieldset-name`;
const hasHint = !!hint; const hasHint = !!hint;
const fieldStatus = getFieldStatus(status);
const hasStatusMessage = !!fieldStatus?.message;
const descriptionIds = [
hasHint ? hintId : '',
hasStatusMessage ? statusId : '',
]
.filter((id) => !!id)
.join(' ');
return ( return (
<fieldset <fieldset
className={classes.fieldset} className={classes.fieldset}
data-has-error={hasError} data-has-error={status === 'error'}
aria-labelledby={labelId} aria-labelledby={labelId}
aria-describedby={hintId} aria-describedby={descriptionIds}
> >
<div className={formFieldWrapperClasses.labelWrapper}> <div className={formFieldWrapperClasses.labelWrapper}>
<div id={labelId} className={formFieldWrapperClasses.label}> <div id={labelId} className={formFieldWrapperClasses.label}>
@@ -59,6 +75,11 @@ export const Fieldset: FC<FieldsetProps> = ({
{children} {children}
</FieldsetNameContext.Provider> </FieldsetNameContext.Provider>
</div> </div>
{/* Live region must be rendered even when empty */}
<A11yLiveRegion className={classes.status} id={statusId}>
{hasStatusMessage && <CalloutInline {...fieldStatus} />}
</A11yLiveRegion>
</fieldset> </fieldset>
); );
}; };

View File

@@ -46,6 +46,14 @@
font-size: 13px; 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 { .inputWrapper {
display: block; display: block;
} }

View File

@@ -7,6 +7,10 @@ import { FormattedMessage } from 'react-intl';
import classNames from 'classnames'; 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 { FieldsetNameContext } from './fieldset';
import classes from './form_field_wrapper.module.scss'; import classes from './form_field_wrapper.module.scss';
@@ -20,7 +24,7 @@ interface FieldWrapperProps {
label: ReactNode; label: ReactNode;
hint?: ReactNode; hint?: ReactNode;
required?: boolean; required?: boolean;
hasError?: boolean; status?: FieldStatus['variant'] | FieldStatus;
inputId?: string; inputId?: string;
describedById?: string; describedById?: string;
inputPlacement?: 'inline-start' | 'inline-end'; inputPlacement?: 'inline-start' | 'inline-end';
@@ -33,7 +37,7 @@ interface FieldWrapperProps {
*/ */
export type CommonFieldWrapperProps = Pick< export type CommonFieldWrapperProps = Pick<
FieldWrapperProps, FieldWrapperProps,
'label' | 'hint' | 'hasError' 'label' | 'hint' | 'status'
> & { wrapperClassName?: string }; > & { wrapperClassName?: string };
/** /**
@@ -48,27 +52,31 @@ export const FormFieldWrapper: FC<FieldWrapperProps> = ({
hint, hint,
describedById, describedById,
required, required,
hasError, status,
inputPlacement, inputPlacement,
children, children,
className, className,
}) => { }) => {
const uniqueId = useId(); const uniqueId = useId();
const inputId = inputIdProp || `${uniqueId}-input`; const inputId = inputIdProp || `${uniqueId}-input`;
const statusId = `${inputIdProp || uniqueId}-status`;
const hintId = `${inputIdProp || uniqueId}-hint`; const hintId = `${inputIdProp || uniqueId}-hint`;
const hasHint = !!hint; const hasHint = !!hint;
const fieldStatus = getFieldStatus(status);
const hasStatusMessage = !!fieldStatus?.message;
const hasParentFieldset = !!useContext(FieldsetNameContext); const hasParentFieldset = !!useContext(FieldsetNameContext);
const descriptionIds =
[hasHint ? hintId : '', hasStatusMessage ? statusId : '', describedById]
.filter((id) => !!id)
.join(' ') || undefined;
const inputProps: InputProps = { const inputProps: InputProps = {
required, required,
id: inputId, id: inputId,
'aria-describedby': descriptionIds,
}; };
if (hasHint) {
inputProps['aria-describedby'] = describedById
? `${describedById} ${hintId}`
: hintId;
}
const input = ( const input = (
<div className={classes.inputWrapper}>{children(inputProps)}</div> <div className={classes.inputWrapper}>{children(inputProps)}</div>
@@ -77,7 +85,7 @@ export const FormFieldWrapper: FC<FieldWrapperProps> = ({
return ( return (
<div <div
className={classNames(classes.wrapper, className)} className={classNames(classes.wrapper, className)}
data-has-error={hasError} data-has-error={fieldStatus?.variant === 'error'}
data-input-placement={inputPlacement} data-input-placement={inputPlacement}
> >
{inputPlacement === 'inline-start' && input} {inputPlacement === 'inline-start' && input}
@@ -100,6 +108,11 @@ export const FormFieldWrapper: FC<FieldWrapperProps> = ({
</div> </div>
{inputPlacement !== 'inline-start' && input} {inputPlacement !== 'inline-start' && input}
{/* Live region must be rendered even when empty */}
<A11yLiveRegion className={classes.status} id={statusId}>
{hasStatusMessage && <CalloutInline {...fieldStatus} />}
</A11yLiveRegion>
</div> </div>
); );
}; };
@@ -121,3 +134,19 @@ const RequiredMark: FC<{ required?: boolean }> = ({ required }) =>
<FormattedMessage id='form_field.optional' defaultMessage='(optional)' /> <FormattedMessage id='form_field.optional' defaultMessage='(optional)' />
</> </>
); );
export function getFieldStatus(status: FieldWrapperProps['status']) {
if (!status) {
return null;
}
if (typeof status === 'string') {
const fieldStatus: FieldStatus = {
variant: status,
message: '',
};
return fieldStatus;
}
return status;
}

View File

@@ -71,7 +71,7 @@ export const Optional: Story = {
export const WithError: Story = { export const WithError: Story = {
args: { args: {
required: false, required: false,
hasError: true, status: 'error',
}, },
}; };

View File

@@ -15,7 +15,7 @@ type Props = Omit<ComponentPropsWithoutRef<'input'>, 'type'> & {
export const RadioButtonField = forwardRef< export const RadioButtonField = forwardRef<
HTMLInputElement, HTMLInputElement,
Props & CommonFieldWrapperProps Props & CommonFieldWrapperProps
>(({ id, label, hint, hasError, required, ...otherProps }, ref) => { >(({ id, label, hint, status, required, ...otherProps }, ref) => {
const fieldsetName = useContext(FieldsetNameContext); const fieldsetName = useContext(FieldsetNameContext);
return ( return (
@@ -23,7 +23,7 @@ export const RadioButtonField = forwardRef<
label={label} label={label}
hint={hint} hint={hint}
required={required} required={required}
hasError={hasError} status={status}
inputId={id} inputId={id}
inputPlacement='inline-start' inputPlacement='inline-start'
> >

View File

@@ -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;
}
}

View File

@@ -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<typeof RangeInputField>;
export default meta;
type Story = StoryObj<typeof meta>;
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' },
],
},
};

View File

@@ -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<HTMLInputElement, Props>(
(
{ id, label, hint, status, required, wrapperClassName, ...otherProps },
ref,
) => (
<FormFieldWrapper
label={label}
hint={hint}
required={required}
status={status}
inputId={id}
className={wrapperClassName}
>
{(inputProps) => <RangeInput {...otherProps} {...inputProps} ref={ref} />}
</FormFieldWrapper>
),
);
RangeInputField.displayName = 'RangeInputField';
export const RangeInput = forwardRef<HTMLInputElement, RangeInputProps>(
({ className, markers, id, ...otherProps }, ref) => {
const markersId = useId();
if (!markers) {
return (
<input
{...otherProps}
type='range'
className={classNames(className, classes.input)}
ref={ref}
/>
);
}
return (
<>
<input
{...otherProps}
type='range'
className={classNames(className, classes.input)}
ref={ref}
list={markersId}
/>
<datalist id={markersId} className={classes.markers}>
{markers.map((marker) => {
const value = typeof marker === 'number' ? marker : marker.value;
return (
<option
key={value}
value={value}
label={typeof marker !== 'number' ? marker.label : undefined}
/>
);
})}
</datalist>
</>
);
},
);
RangeInput.displayName = 'RangeInput';

View File

@@ -51,7 +51,7 @@ export const Optional: Story = {
export const WithError: Story = { export const WithError: Story = {
args: { args: {
required: false, required: false,
hasError: true, status: 'error',
}, },
}; };

View File

@@ -19,12 +19,12 @@ interface Props
*/ */
export const SelectField = forwardRef<HTMLSelectElement, Props>( export const SelectField = forwardRef<HTMLSelectElement, Props>(
({ id, label, hint, required, hasError, children, ...otherProps }, ref) => ( ({ id, label, hint, required, status, children, ...otherProps }, ref) => (
<FormFieldWrapper <FormFieldWrapper
label={label} label={label}
hint={hint} hint={hint}
required={required} required={required}
hasError={hasError} status={status}
inputId={id} inputId={id}
> >
{(inputProps) => ( {(inputProps) => (

View File

@@ -38,7 +38,17 @@ export const Optional: Story = {
export const WithError: Story = { export const WithError: Story = {
args: { args: {
required: false, 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',
},
}, },
}; };

View File

@@ -26,14 +26,14 @@ export const TextAreaField = forwardRef<
TextAreaProps & CommonFieldWrapperProps TextAreaProps & CommonFieldWrapperProps
>( >(
( (
{ id, label, hint, required, hasError, wrapperClassName, ...otherProps }, { id, label, hint, required, status, wrapperClassName, ...otherProps },
ref, ref,
) => ( ) => (
<FormFieldWrapper <FormFieldWrapper
label={label} label={label}
hint={hint} hint={hint}
required={required} required={required}
hasError={hasError} status={status}
inputId={id} inputId={id}
className={wrapperClassName} className={wrapperClassName}
> >

View File

@@ -29,16 +29,16 @@
color: var(--color-text-secondary); color: var(--color-text-secondary);
} }
&:focus {
outline-color: var(--color-text-brand);
}
&:focus:user-invalid, &:focus:user-invalid,
&:required:user-invalid, &:required:user-invalid,
[data-has-error='true'] & { [data-has-error='true'] & {
outline-color: var(--color-text-error); outline-color: var(--color-text-error);
} }
&:focus {
outline-color: var(--color-text-brand);
}
&:required:user-valid { &:required:user-valid {
outline-color: var(--color-text-success); outline-color: var(--color-text-success);
} }

View File

@@ -40,7 +40,17 @@ export const Optional: Story = {
export const WithError: Story = { export const WithError: Story = {
args: { args: {
required: false, required: false,
hasError: true, status: 'error',
},
};
export const WithWarning: Story = {
args: {
required: false,
status: {
variant: 'warning',
message: 'Special characters are not allowed',
},
}, },
}; };

View File

@@ -25,14 +25,14 @@ interface Props extends TextInputProps, CommonFieldWrapperProps {}
export const TextInputField = forwardRef<HTMLInputElement, Props>( export const TextInputField = forwardRef<HTMLInputElement, Props>(
( (
{ id, label, hint, hasError, required, wrapperClassName, ...otherProps }, { id, label, hint, status, required, wrapperClassName, ...otherProps },
ref, ref,
) => ( ) => (
<FormFieldWrapper <FormFieldWrapper
label={label} label={label}
hint={hint} hint={hint}
required={required} required={required}
hasError={hasError} status={status}
inputId={id} inputId={id}
className={wrapperClassName} className={wrapperClassName}
> >

View File

@@ -45,7 +45,7 @@ export const Optional: Story = {
export const WithError: Story = { export const WithError: Story = {
args: { args: {
required: false, required: false,
hasError: true, status: 'error',
}, },
}; };

View File

@@ -14,12 +14,12 @@ type Props = Omit<ComponentPropsWithoutRef<'input'>, 'type'> & {
export const ToggleField = forwardRef< export const ToggleField = forwardRef<
HTMLInputElement, HTMLInputElement,
Props & CommonFieldWrapperProps Props & CommonFieldWrapperProps
>(({ id, label, hint, hasError, required, ...otherProps }, ref) => ( >(({ id, label, hint, status, required, ...otherProps }, ref) => (
<FormFieldWrapper <FormFieldWrapper
label={label} label={label}
hint={hint} hint={hint}
required={required} required={required}
hasError={hasError} status={status}
inputId={id} inputId={id}
inputPlacement='inline-end' inputPlacement='inline-end'
> >

View File

@@ -107,7 +107,7 @@
} }
$button-breakpoint: 420px; $button-breakpoint: 420px;
$button-fallback-breakpoint: #{$button-breakpoint} + 55px; $button-fallback-breakpoint: $button-breakpoint + 55px;
.buttonsDesktop { .buttonsDesktop {
@container (width < #{$button-breakpoint}) { @container (width < #{$button-breakpoint}) {
@@ -132,7 +132,7 @@ $button-fallback-breakpoint: #{$button-breakpoint} + 55px;
} }
@supports (not (container-type: inline-size)) { @supports (not (container-type: inline-size)) {
@media (min-width: (#{$button-fallback-breakpoint} + 1px)) { @media (min-width: ($button-fallback-breakpoint + 1px)) {
display: none; display: none;
} }
} }

View File

@@ -141,8 +141,11 @@ const InnerNodeModal: FC<{
onChange={handleChange} onChange={handleChange}
label={intl.formatMessage(messages.fieldLabel)} label={intl.formatMessage(messages.fieldLabel)}
className={classes.noteInput} className={classes.noteInput}
hasError={state === 'error'} status={
hint={errorText} 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. // eslint-disable-next-line jsx-a11y/no-autofocus -- We want to focus here as it's a modal.
autoFocus autoFocus
/> />

View File

@@ -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 { useHistory } from 'react-router-dom';
import { isFulfilled } from '@reduxjs/toolkit'; import { isFulfilled } from '@reduxjs/toolkit';
import { inputToHashtag } from '@/mastodon/utils/hashtags'; import {
hasSpecialCharacters,
inputToHashtag,
} from '@/mastodon/utils/hashtags';
import type { import type {
ApiCreateCollectionPayload, ApiCreateCollectionPayload,
ApiUpdateCollectionPayload, ApiUpdateCollectionPayload,
@@ -31,6 +34,7 @@ import classes from './styles.module.scss';
import { WizardStepHeader } from './wizard_step_header'; import { WizardStepHeader } from './wizard_step_header';
export const CollectionDetails: React.FC = () => { export const CollectionDetails: React.FC = () => {
const intl = useIntl();
const dispatch = useAppDispatch(); const dispatch = useAppDispatch();
const history = useHistory(); const history = useHistory();
const { id, name, description, topic, discoverable, sensitive, accountIds } = const { id, name, description, topic, discoverable, sensitive, accountIds } =
@@ -152,6 +156,11 @@ export const CollectionDetails: React.FC = () => {
], ],
); );
const topicHasSpecialCharacters = useMemo(
() => hasSpecialCharacters(topic),
[topic],
);
return ( return (
<form onSubmit={handleSubmit} className={classes.form}> <form onSubmit={handleSubmit} className={classes.form}>
<FormStack className={classes.formFieldStack}> <FormStack className={classes.formFieldStack}>
@@ -224,6 +233,18 @@ export const CollectionDetails: React.FC = () => {
autoCorrect='off' autoCorrect='off'
spellCheck='false' spellCheck='false'
maxLength={40} maxLength={40}
status={
topicHasSpecialCharacters
? {
variant: 'warning',
message: intl.formatMessage({
id: 'collections.topic_special_chars_hint',
defaultMessage:
'Special characters will be removed when saving',
}),
}
: undefined
}
/> />
<Fieldset <Fieldset

View File

@@ -230,7 +230,7 @@ export const Profile: React.FC<{
} }
value={displayName} value={displayName}
onChange={handleDisplayNameChange} onChange={handleDisplayNameChange}
hasError={!!errors?.display_name} status={errors?.display_name ? 'error' : undefined}
id='display_name' id='display_name'
/> />
</div> </div>
@@ -252,7 +252,7 @@ export const Profile: React.FC<{
} }
value={note} value={note}
onChange={handleNoteChange} onChange={handleNoteChange}
hasError={!!errors?.note} status={errors?.note ? 'error' : undefined}
id='note' id='note'
/> />
</div> </div>

View File

@@ -184,6 +184,11 @@
"account_edit.field_reorder_modal.drag_start": "Samlede feltet \"{item}\" op.", "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.handle_label": "Træk feltet \"{item}\"",
"account_edit.field_reorder_modal.title": "Omarrangér felter", "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.add_title": "Tilføj visningsnavn",
"account_edit.name_modal.edit_title": "Rediger visningsnavn", "account_edit.name_modal.edit_title": "Rediger visningsnavn",
"account_edit.profile_tab.button_label": "Tilpas", "account_edit.profile_tab.button_label": "Tilpas",

View File

@@ -174,7 +174,7 @@
"account_edit.field_edit_modal.name_hint": "z. B. „Meine Website“", "account_edit.field_edit_modal.name_hint": "z. B. „Meine Website“",
"account_edit.field_edit_modal.name_label": "Beschriftung", "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.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_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_cancel": "Das Ziehen wurde abgebrochen und das Feld „{item}“ wurde abgelegt.",
"account_edit.field_reorder_modal.drag_end": "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.drag_start": "Das Feld „{item}“ wurde ausgewählt.",
"account_edit.field_reorder_modal.handle_label": "Das Feld „{item}“ verschieben", "account_edit.field_reorder_modal.handle_label": "Das Feld „{item}“ verschieben",
"account_edit.field_reorder_modal.title": "Felder neu anordnen", "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.add_title": "Anzeigenamen hinzufügen",
"account_edit.name_modal.edit_title": "Anzeigenamen bearbeiten", "account_edit.name_modal.edit_title": "Anzeigenamen bearbeiten",
"account_edit.profile_tab.button_label": "Anpassen", "account_edit.profile_tab.button_label": "Anpassen",

View File

@@ -184,6 +184,11 @@
"account_edit.field_reorder_modal.drag_start": "Το πεδίο \"{item}\" σηκώθηκε.", "account_edit.field_reorder_modal.drag_start": "Το πεδίο \"{item}\" σηκώθηκε.",
"account_edit.field_reorder_modal.handle_label": "Μετακίνηση πεδίου \"{item}\"", "account_edit.field_reorder_modal.handle_label": "Μετακίνηση πεδίου \"{item}\"",
"account_edit.field_reorder_modal.title": "Αναδιάταξη πεδίων", "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.add_title": "Προσθήκη εμφανιζόμενου ονόματος",
"account_edit.name_modal.edit_title": "Επεξεργασία εμφανιζόμενου ονόματος", "account_edit.name_modal.edit_title": "Επεξεργασία εμφανιζόμενου ονόματος",
"account_edit.profile_tab.button_label": "Προσαρμογή", "account_edit.profile_tab.button_label": "Προσαρμογή",

View File

@@ -184,6 +184,11 @@
"account_edit.field_reorder_modal.drag_start": "Picked up field \"{item}\".", "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.handle_label": "Drag field \"{item}\"",
"account_edit.field_reorder_modal.title": "Rearrange fields", "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.add_title": "Add display name",
"account_edit.name_modal.edit_title": "Edit display name", "account_edit.name_modal.edit_title": "Edit display name",
"account_edit.profile_tab.button_label": "Customise", "account_edit.profile_tab.button_label": "Customise",

View File

@@ -374,6 +374,7 @@
"collections.search_accounts_max_reached": "You have added the maximum number of accounts", "collections.search_accounts_max_reached": "You have added the maximum number of accounts",
"collections.sensitive": "Sensitive", "collections.sensitive": "Sensitive",
"collections.topic_hint": "Add a hashtag that helps others understand the main topic of this collection.", "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_collection": "View collection",
"collections.view_other_collections_by_user": "View other collections by this user", "collections.view_other_collections_by_user": "View other collections by this user",
"collections.visibility_public": "Public", "collections.visibility_public": "Public",

View File

@@ -184,6 +184,11 @@
"account_edit.field_reorder_modal.drag_start": "Campo elegido «{item}».", "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.handle_label": "Arrastrá el campo «{item}»",
"account_edit.field_reorder_modal.title": "Reordená los campos", "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.add_title": "Agregar nombre a mostrar",
"account_edit.name_modal.edit_title": "Editar nombre a mostrar", "account_edit.name_modal.edit_title": "Editar nombre a mostrar",
"account_edit.profile_tab.button_label": "Personalizar", "account_edit.profile_tab.button_label": "Personalizar",
@@ -961,12 +966,14 @@
"notifications_permission_banner.title": "No te pierdas nada", "notifications_permission_banner.title": "No te pierdas nada",
"onboarding.follows.back": "Volver", "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.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.search": "Buscar",
"onboarding.follows.title": "Para comenzar, empezá a seguir cuentas", "onboarding.follows.title": "Para comenzar, empezá a seguir cuentas",
"onboarding.profile.discoverable": "Hacer que mi perfil sea detectable", "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.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": "Nombre para mostrar",
"onboarding.profile.display_name_hint": "Tu nombre completo o tu pseudónimo…", "onboarding.profile.display_name_hint": "Tu nombre completo o tu pseudónimo…",
"onboarding.profile.finish": "Finalizar",
"onboarding.profile.note": "Biografía", "onboarding.profile.note": "Biografía",
"onboarding.profile.note_hint": "Podés @mencionar otras cuentas o usar #etiquetas…", "onboarding.profile.note_hint": "Podés @mencionar otras cuentas o usar #etiquetas…",
"onboarding.profile.title": "Configuración del perfil", "onboarding.profile.title": "Configuración del perfil",

View File

@@ -184,6 +184,11 @@
"account_edit.field_reorder_modal.drag_start": "Recogido el campo «{item}».", "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.handle_label": "Arrastra el campo «{item}»",
"account_edit.field_reorder_modal.title": "Reorganizar campos", "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.add_title": "Añadir nombre para mostrar",
"account_edit.name_modal.edit_title": "Editar nombre para mostrar", "account_edit.name_modal.edit_title": "Editar nombre para mostrar",
"account_edit.profile_tab.button_label": "Personalizar", "account_edit.profile_tab.button_label": "Personalizar",

View File

@@ -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.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_hint": "Ej. \"Web personal\"",
"account_edit.field_edit_modal.name_label": "Etiqueta", "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_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_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.", "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.drag_start": "Campo \"{item}\" seleccionado.",
"account_edit.field_reorder_modal.handle_label": "Arrastra el campo \"{item}\"", "account_edit.field_reorder_modal.handle_label": "Arrastra el campo \"{item}\"",
"account_edit.field_reorder_modal.title": "Reorganizar campos", "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.add_title": "Añadir nombre para mostrar",
"account_edit.name_modal.edit_title": "Editar nombre para mostrar", "account_edit.name_modal.edit_title": "Editar nombre para mostrar",
"account_edit.profile_tab.button_label": "Personalizar", "account_edit.profile_tab.button_label": "Personalizar",
@@ -337,14 +344,14 @@
"collections.create_collection": "Crear colección", "collections.create_collection": "Crear colección",
"collections.delete_collection": "Eliminar colección", "collections.delete_collection": "Eliminar colección",
"collections.description_length_hint": "Limitado a 100 caracteres", "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.accounts_heading": "Cuentas",
"collections.detail.author_added_you": "{author} te añadió a esta colección", "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_author": "Seleccionado por {author}",
"collections.detail.curated_by_you": "Seleccionado por ti", "collections.detail.curated_by_you": "Seleccionado por ti",
"collections.detail.loading": "Cargando colección…", "collections.detail.loading": "Cargando colección…",
"collections.detail.other_accounts_in_collection": "Otros en esta 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.sensitive_note": "Esta colección contiene cuentas y contenido que puede ser sensible para algunos usuarios.",
"collections.detail.share": "Compartir esta colección", "collections.detail.share": "Compartir esta colección",
"collections.edit_details": "Editar detalles", "collections.edit_details": "Editar detalles",
@@ -360,9 +367,9 @@
"collections.old_last_post_note": "Última publicación hace más de una semana", "collections.old_last_post_note": "Última publicación hace más de una semana",
"collections.remove_account": "Quitar esta cuenta", "collections.remove_account": "Quitar esta cuenta",
"collections.report_collection": "Informar de esta colección", "collections.report_collection": "Informar de esta colección",
"collections.revoke_collection_inclusion": "Eliminarme de esta colección", "collections.revoke_collection_inclusion": "Sácame de esta colección",
"collections.revoke_inclusion.confirmation": "Ha sido eliminado de\"{collection}\"", "collections.revoke_inclusion.confirmation": "Has salido de la \"{collection}\"",
"collections.revoke_inclusion.error": "Se ha producido un error. Inténtelo de nuevo más tarde.", "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_label": "Buscar cuentas para añadir…",
"collections.search_accounts_max_reached": "Has añadido el número máximo de cuentas", "collections.search_accounts_max_reached": "Has añadido el número máximo de cuentas",
"collections.sensitive": "Sensible", "collections.sensitive": "Sensible",
@@ -486,9 +493,9 @@
"confirmations.remove_from_followers.confirm": "Eliminar seguidor", "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.message": "{name} dejará de seguirte. ¿Estás seguro de que quieres continuar?",
"confirmations.remove_from_followers.title": "¿Eliminar seguidor?", "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.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.confirm": "Eliminar publicación",
"confirmations.revoke_quote.message": "Esta acción no tiene vuelta atrás.", "confirmations.revoke_quote.message": "Esta acción no tiene vuelta atrás.",
"confirmations.revoke_quote.title": "¿Eliminar la publicación?", "confirmations.revoke_quote.title": "¿Eliminar la publicación?",
@@ -959,12 +966,14 @@
"notifications_permission_banner.title": "Nunca te pierdas nada", "notifications_permission_banner.title": "Nunca te pierdas nada",
"onboarding.follows.back": "Atrás", "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.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.search": "Buscar",
"onboarding.follows.title": "Sigue personas para comenzar", "onboarding.follows.title": "Sigue personas para comenzar",
"onboarding.profile.discoverable": "Hacer que mi perfil aparezca en búsquedas", "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.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": "Nombre para mostrar",
"onboarding.profile.display_name_hint": "Tu nombre completo o tu apodo…", "onboarding.profile.display_name_hint": "Tu nombre completo o tu apodo…",
"onboarding.profile.finish": "Terminar",
"onboarding.profile.note": "Biografía", "onboarding.profile.note": "Biografía",
"onboarding.profile.note_hint": "Puedes @mencionar a otras personas o #etiquetas…", "onboarding.profile.note_hint": "Puedes @mencionar a otras personas o #etiquetas…",
"onboarding.profile.title": "Configuración del perfil", "onboarding.profile.title": "Configuración del perfil",

View File

@@ -184,6 +184,11 @@
"account_edit.field_reorder_modal.drag_start": "Valittu kenttä ”{item}”.", "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.handle_label": "Siirrä kenttää ”{item}”",
"account_edit.field_reorder_modal.title": "Järjestele kenttiä", "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.add_title": "Lisää näyttönimi",
"account_edit.name_modal.edit_title": "Muokkaa näyttönimeä", "account_edit.name_modal.edit_title": "Muokkaa näyttönimeä",
"account_edit.profile_tab.button_label": "Mukauta", "account_edit.profile_tab.button_label": "Mukauta",
@@ -225,8 +230,8 @@
"alert.rate_limited.title": "Pyyntömäärää rajoitettu", "alert.rate_limited.title": "Pyyntömäärää rajoitettu",
"alert.unexpected.message": "Tapahtui odottamaton virhe.", "alert.unexpected.message": "Tapahtui odottamaton virhe.",
"alert.unexpected.title": "Hups!", "alert.unexpected.title": "Hups!",
"alt_text_badge.title": "Vaihtoehtoinen teksti", "alt_text_badge.title": "Tekstivastine",
"alt_text_modal.add_alt_text": "Lisää vaihtoehtoinen teksti", "alt_text_modal.add_alt_text": "Lisää tekstivastine",
"alt_text_modal.add_text_from_image": "Lisää teksti kuvasta", "alt_text_modal.add_text_from_image": "Lisää teksti kuvasta",
"alt_text_modal.cancel": "Peruuta", "alt_text_modal.cancel": "Peruuta",
"alt_text_modal.change_thumbnail": "Vaihda pikkukuva", "alt_text_modal.change_thumbnail": "Vaihda pikkukuva",
@@ -468,10 +473,10 @@
"confirmations.logout.confirm": "Kirjaudu ulos", "confirmations.logout.confirm": "Kirjaudu ulos",
"confirmations.logout.message": "Haluatko varmasti kirjautua ulos?", "confirmations.logout.message": "Haluatko varmasti kirjautua ulos?",
"confirmations.logout.title": "Kirjaudutaanko ulos?", "confirmations.logout.title": "Kirjaudutaanko ulos?",
"confirmations.missing_alt_text.confirm": "Lisää vaihtoehtoinen teksti", "confirmations.missing_alt_text.confirm": "Lisää tekstivastine",
"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.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.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.mute.confirm": "Mykistä",
"confirmations.private_quote_notify.cancel": "Takaisin muokkaukseen", "confirmations.private_quote_notify.cancel": "Takaisin muokkaukseen",
"confirmations.private_quote_notify.confirm": "Julkaise", "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.not_following_title": "Sivuutetaanko ilmoitukset käyttäjiltä, joita et seuraa?",
"ignore_notifications_modal.private_mentions_title": "Sivuutetaanko ilmoitukset pyytämättömistä yksityismaininnoista?", "ignore_notifications_modal.private_mentions_title": "Sivuutetaanko ilmoitukset pyytämättömistä yksityismaininnoista?",
"info_button.label": "Ohje", "info_button.label": "Ohje",
"info_button.what_is_alt_text": "<h1>Mikä vaihtoehtoinen teksti on?</h1> <p>Vaihtoehtoinen teksti tarjoaa kuvauksen kuvista ihmisille, joilla on näkövamma tai matalan kaistanleveyden yhteys tai jotka kaipaavat lisäkontekstia.</p> <p>Voit parantaa saavutettavuutta ja ymmärrettävyyttä kaikkien näkökulmasta kirjoittamalla selkeän, tiiviin ja objektiivisen vaihtoehtoisen tekstin.</p> <ul> <li>Ota mukaan tärkeät elementit</li> <li>Tiivistä kuvissa oleva teksti</li> <li>Käytä tavallisia lauserakenteita</li> <li>Vältä turhaa tietoa</li> <li>Keskity trendeihin ja keskeisiin tuloksiin monimutkaisissa visuaalisissa esityksissä (kuten kaavioissa tai kartoissa)</li> </ul>", "info_button.what_is_alt_text": "<h1>Mikä tekstivastine on?</h1> <p>Tekstivastine tarjoaa kuvauksen kuvista ihmisille, joilla on näkövamma tai matalan kaistanleveyden yhteys tai jotka kaipaavat lisäkontekstia.</p> <p>Voit parantaa saavutettavuutta ja ymmärrettävyyttä kaikkien näkökulmasta kirjoittamalla selkeän, tiiviin ja objektiivisen tekstivastineen.</p> <ul> <li>Ota mukaan tärkeät elementit</li> <li>Tiivistä kuvissa oleva teksti</li> <li>Käytä tavallisia lauserakenteita</li> <li>Vältä turhaa tietoa</li> <li>Keskity trendeihin ja keskeisiin tuloksiin monimutkaisissa visuaalisissa esityksissä (kuten kaavioissa tai kartoissa)</li> </ul>",
"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.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.go": "Siirry",
"interaction_modal.no_account_yet": "Eikö sinulla ole vielä tiliä?", "interaction_modal.no_account_yet": "Eikö sinulla ole vielä tiliä?",

View File

@@ -184,6 +184,11 @@
"account_edit.field_reorder_modal.drag_start": "Champ « {item} » sélectionné.", "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.handle_label": "Faites glisser le champ « {item} »",
"account_edit.field_reorder_modal.title": "Réorganiser les champs", "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 limage",
"account_edit.image_edit.replace_button": "Remplacer l'image",
"account_edit.name_modal.add_title": "Ajouter un nom public", "account_edit.name_modal.add_title": "Ajouter un nom public",
"account_edit.name_modal.edit_title": "Modifier le nom public", "account_edit.name_modal.edit_title": "Modifier le nom public",
"account_edit.profile_tab.button_label": "Personnaliser", "account_edit.profile_tab.button_label": "Personnaliser",
@@ -961,12 +966,14 @@
"notifications_permission_banner.title": "Ne rien rater", "notifications_permission_banner.title": "Ne rien rater",
"onboarding.follows.back": "Retour", "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.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.search": "Recherche",
"onboarding.follows.title": "Suivre des personnes pour commencer", "onboarding.follows.title": "Suivre des personnes pour commencer",
"onboarding.profile.discoverable": "Permettre de découvrir mon profil", "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.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": "Nom affiché",
"onboarding.profile.display_name_hint": "Votre nom complet ou votre nom rigolo…", "onboarding.profile.display_name_hint": "Votre nom complet ou votre nom rigolo…",
"onboarding.profile.finish": "Terminer",
"onboarding.profile.note": "Bio", "onboarding.profile.note": "Bio",
"onboarding.profile.note_hint": "Vous pouvez @mentionner d'autres personnes ou #hashtags…", "onboarding.profile.note_hint": "Vous pouvez @mentionner d'autres personnes ou #hashtags…",
"onboarding.profile.title": "Configuration du profil", "onboarding.profile.title": "Configuration du profil",

View File

@@ -184,6 +184,11 @@
"account_edit.field_reorder_modal.drag_start": "Champ « {item} » sélectionné.", "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.handle_label": "Faites glisser le champ « {item} »",
"account_edit.field_reorder_modal.title": "Réorganiser les champs", "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 limage",
"account_edit.image_edit.replace_button": "Remplacer l'image",
"account_edit.name_modal.add_title": "Ajouter un nom public", "account_edit.name_modal.add_title": "Ajouter un nom public",
"account_edit.name_modal.edit_title": "Modifier le nom public", "account_edit.name_modal.edit_title": "Modifier le nom public",
"account_edit.profile_tab.button_label": "Personnaliser", "account_edit.profile_tab.button_label": "Personnaliser",
@@ -961,12 +966,14 @@
"notifications_permission_banner.title": "Toujours au courant", "notifications_permission_banner.title": "Toujours au courant",
"onboarding.follows.back": "Retour", "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.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.search": "Recherche",
"onboarding.follows.title": "Suivre des personnes pour commencer", "onboarding.follows.title": "Suivre des personnes pour commencer",
"onboarding.profile.discoverable": "Permettre de découvrir mon profil", "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.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": "Nom affiché",
"onboarding.profile.display_name_hint": "Votre nom complet ou votre nom rigolo…", "onboarding.profile.display_name_hint": "Votre nom complet ou votre nom rigolo…",
"onboarding.profile.finish": "Terminer",
"onboarding.profile.note": "Biographie", "onboarding.profile.note": "Biographie",
"onboarding.profile.note_hint": "Vous pouvez @mentionner d'autres personnes ou #hashtags…", "onboarding.profile.note_hint": "Vous pouvez @mentionner d'autres personnes ou #hashtags…",
"onboarding.profile.title": "Configuration du profil", "onboarding.profile.title": "Configuration du profil",

View File

@@ -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.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_hint": "M.sh. “Suíomh Gréasáin pearsanta”",
"account_edit.field_edit_modal.name_label": "Lipéad", "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_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_cancel": "Cuireadh an tarraingt ar ceal. Baineadh an réimse \"{item}\".",
"account_edit.field_reorder_modal.drag_end": "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.drag_start": "Réimse \"{item}\" bailithe.",
"account_edit.field_reorder_modal.handle_label": "Tarraing réimse \"{item}\"", "account_edit.field_reorder_modal.handle_label": "Tarraing réimse \"{item}\"",
"account_edit.field_reorder_modal.title": "Athshocraigh réimsí", "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.add_title": "Cuir ainm taispeána leis",
"account_edit.name_modal.edit_title": "Cuir ainm taispeána in eagar", "account_edit.name_modal.edit_title": "Cuir ainm taispeána in eagar",
"account_edit.profile_tab.button_label": "Saincheap", "account_edit.profile_tab.button_label": "Saincheap",
@@ -959,12 +966,14 @@
"notifications_permission_banner.title": "Ná caill aon rud go deo", "notifications_permission_banner.title": "Ná caill aon rud go deo",
"onboarding.follows.back": "Ar ais", "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.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.search": "Cuardach",
"onboarding.follows.title": "Lean daoine le tosú", "onboarding.follows.title": "Lean daoine le tosú",
"onboarding.profile.discoverable": "Déan mo phróifíl a fháil amach", "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, dfhéadfadh do phoist a bheith le feiceáil i dtorthaí cuardaigh agus treochtaí, agus dfhéadfaí do phróifíl a mholadh do dhaoine a bhfuil na leasanna céanna acu leat.", "onboarding.profile.discoverable_hint": "Nuair a roghnaíonn tú infhionnachtana ar Mastodon, dfhéadfadh do phoist a bheith le feiceáil i dtorthaí cuardaigh agus treochtaí, agus dfhé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": "Ainm taispeána",
"onboarding.profile.display_name_hint": "Dainm iomlán nó dainm spraíúil…", "onboarding.profile.display_name_hint": "Dainm iomlán nó dainm spraíúil…",
"onboarding.profile.finish": "Críochnaigh",
"onboarding.profile.note": "Bith", "onboarding.profile.note": "Bith",
"onboarding.profile.note_hint": "Is féidir leat @ daoine eile a lua nó #hashtags…", "onboarding.profile.note_hint": "Is féidir leat @ daoine eile a lua nó #hashtags…",
"onboarding.profile.title": "Socrú próifíle", "onboarding.profile.title": "Socrú próifíle",

View File

@@ -184,6 +184,11 @@
"account_edit.field_reorder_modal.drag_start": "Seleccionado o campo \"{item}\".", "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.handle_label": "Arrastra o campo \"{item}\"",
"account_edit.field_reorder_modal.title": "Ordear campos", "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.add_title": "Engadir nome público",
"account_edit.name_modal.edit_title": "Editar o nome público", "account_edit.name_modal.edit_title": "Editar o nome público",
"account_edit.profile_tab.button_label": "Personalizar", "account_edit.profile_tab.button_label": "Personalizar",

View File

@@ -173,6 +173,8 @@
"account_edit.field_edit_modal.link_emoji_warning": "אנו ממליצים נגד שימוש באמוג'י ייחודיים ביחד עם URL. שדות מיוחדים שמכילים את שניהם יופיעו כמלל בלבד ולא כקישור, כדי למנוע בלבול משתמשים.", "account_edit.field_edit_modal.link_emoji_warning": "אנו ממליצים נגד שימוש באמוג'י ייחודיים ביחד עם URL. שדות מיוחדים שמכילים את שניהם יופיעו כמלל בלבד ולא כקישור, כדי למנוע בלבול משתמשים.",
"account_edit.field_edit_modal.name_hint": "למשל \"אתר אישי\"", "account_edit.field_edit_modal.name_hint": "למשל \"אתר אישי\"",
"account_edit.field_edit_modal.name_label": "תווית", "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_edit_modal.value_label": "ערך",
"account_edit.field_reorder_modal.drag_cancel": "הגרירה בוטלה. השדה \"{item}\" נעזב.", "account_edit.field_reorder_modal.drag_cancel": "הגרירה בוטלה. השדה \"{item}\" נעזב.",
"account_edit.field_reorder_modal.drag_end": "השדה \"{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.drag_start": "השדה \"{item}\" נבחר.",
"account_edit.field_reorder_modal.handle_label": "הזזת השדה \"{item}\"", "account_edit.field_reorder_modal.handle_label": "הזזת השדה \"{item}\"",
"account_edit.field_reorder_modal.title": "סידור שדות", "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.add_title": "הוספת שם תצוגה",
"account_edit.name_modal.edit_title": "עריכת שם תצוגה", "account_edit.name_modal.edit_title": "עריכת שם תצוגה",
"account_edit.profile_tab.button_label": "התאמה אישית", "account_edit.profile_tab.button_label": "התאמה אישית",
@@ -959,12 +966,14 @@
"notifications_permission_banner.title": "לעולם אל תחמיץ דבר", "notifications_permission_banner.title": "לעולם אל תחמיץ דבר",
"onboarding.follows.back": "בחזרה", "onboarding.follows.back": "בחזרה",
"onboarding.follows.empty": "למצער, תוצאות לחיפושך אינן בנמצא. ניתן להשתמש בחיפוש או בדף החקירות לשם מציאת אנשים ולעקבם. אפשר גם לנסות שוב אחר כך.", "onboarding.follows.empty": "למצער, תוצאות לחיפושך אינן בנמצא. ניתן להשתמש בחיפוש או בדף החקירות לשם מציאת אנשים ולעקבם. אפשר גם לנסות שוב אחר כך.",
"onboarding.follows.next": "להמשיך ליצירת הפרופיל שלך",
"onboarding.follows.search": "חיפוש", "onboarding.follows.search": "חיפוש",
"onboarding.follows.title": "כדי להתחיל, יש לעקוב אחרי אנשים", "onboarding.follows.title": "כדי להתחיל, יש לעקוב אחרי אנשים",
"onboarding.profile.discoverable": "כלול את הפרופיל שלי בעמודת התגליות", "onboarding.profile.discoverable": "כלול את הפרופיל שלי בעמודת התגליות",
"onboarding.profile.discoverable_hint": "כשתבחרו להכלל ב\"תגליות\" על מסטודון, ההודעות שלכם עשויות להופיע בתוצאות חיפוש ועמודות \"נושאים חמים\", והפרופיל יוצע לאחרים עם תחומי עניין משותפים לכם.", "onboarding.profile.discoverable_hint": "כשתבחרו להכלל ב\"תגליות\" על מסטודון, ההודעות שלכם עשויות להופיע בתוצאות חיפוש ועמודות \"נושאים חמים\", והפרופיל יוצע לאחרים עם תחומי עניין משותפים לכם.",
"onboarding.profile.display_name": "שם להצגה", "onboarding.profile.display_name": "שם להצגה",
"onboarding.profile.display_name_hint": "שמך המלא או כינוי הכיף שלך…", "onboarding.profile.display_name_hint": "שמך המלא או כינוי הכיף שלך…",
"onboarding.profile.finish": "סיום",
"onboarding.profile.note": "אודות", "onboarding.profile.note": "אודות",
"onboarding.profile.note_hint": "ניתן @לאזכר משתמשים אחרים או #תגיות…", "onboarding.profile.note_hint": "ניתן @לאזכר משתמשים אחרים או #תגיות…",
"onboarding.profile.title": "הגדרת פרופיל", "onboarding.profile.title": "הגדרת פרופיל",

View File

@@ -184,6 +184,11 @@
"account_edit.field_reorder_modal.drag_start": "A(z) „{item}” mező áthelyezéshez felvéve.", "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.handle_label": "A(z) „{item}” mező húzása",
"account_edit.field_reorder_modal.title": "Mezők átrendezése", "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.add_title": "Megjelenítendő név hozzáadása",
"account_edit.name_modal.edit_title": "Megjelenítendő név szerkesztése", "account_edit.name_modal.edit_title": "Megjelenítendő név szerkesztése",
"account_edit.profile_tab.button_label": "Testreszabás", "account_edit.profile_tab.button_label": "Testreszabás",
@@ -961,12 +966,14 @@
"notifications_permission_banner.title": "Soha ne mulassz el semmit", "notifications_permission_banner.title": "Soha ne mulassz el semmit",
"onboarding.follows.back": "Vissza", "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.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.search": "Keresés",
"onboarding.follows.title": "A kezdéshez kezdj el embereket követni", "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": "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.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": "Megjelenített név",
"onboarding.profile.display_name_hint": "Teljes neved vagy vicces neved…", "onboarding.profile.display_name_hint": "Teljes neved vagy vicces neved…",
"onboarding.profile.finish": "Befejezés",
"onboarding.profile.note": "Bemutatkozás", "onboarding.profile.note": "Bemutatkozás",
"onboarding.profile.note_hint": "Megemlíthetsz @másokat vagy #hashtag-eket…", "onboarding.profile.note_hint": "Megemlíthetsz @másokat vagy #hashtag-eket…",
"onboarding.profile.title": "Profilbeállítás", "onboarding.profile.title": "Profilbeállítás",

View File

@@ -184,6 +184,11 @@
"account_edit.field_reorder_modal.drag_start": "Náði reitnum \"{item}\".", "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.handle_label": "Dragðu reitinn \"{item}\"",
"account_edit.field_reorder_modal.title": "Endurraða gagnasviðum", "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.add_title": "Bættu við birtingarnafni",
"account_edit.name_modal.edit_title": "Breyta birtingarnafni", "account_edit.name_modal.edit_title": "Breyta birtingarnafni",
"account_edit.profile_tab.button_label": "Sérsníða", "account_edit.profile_tab.button_label": "Sérsníða",
@@ -961,12 +966,14 @@
"notifications_permission_banner.title": "Aldrei missa af neinu", "notifications_permission_banner.title": "Aldrei missa af neinu",
"onboarding.follows.back": "Til baka", "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.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.search": "Leita",
"onboarding.follows.title": "Þú ættir að fylgjast með fólki til að komast í gang", "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": "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.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": "Birtingarnafn",
"onboarding.profile.display_name_hint": "Fullt nafn þitt eða eitthvað til gamans…", "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": "Æviágrip",
"onboarding.profile.note_hint": "Þú getur @minnst á annað fólk eða #myllumerki…", "onboarding.profile.note_hint": "Þú getur @minnst á annað fólk eða #myllumerki…",
"onboarding.profile.title": "Uppsetning notandasniðs", "onboarding.profile.title": "Uppsetning notandasniðs",

View File

@@ -184,6 +184,11 @@
"account_edit.field_reorder_modal.drag_start": "Campo \"{item}\" selezionato.", "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.handle_label": "Trascina il campo \"{item}\"",
"account_edit.field_reorder_modal.title": "Riorganizza i campi", "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.add_title": "Aggiungi il nome mostrato",
"account_edit.name_modal.edit_title": "Modifica il nome mostrato", "account_edit.name_modal.edit_title": "Modifica il nome mostrato",
"account_edit.profile_tab.button_label": "Personalizza", "account_edit.profile_tab.button_label": "Personalizza",

View File

@@ -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.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_hint": "Ex.: \"Site pessoal\"",
"account_edit.field_edit_modal.name_label": "Rótulo", "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_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_cancel": "O arrastamento foi cancelado. O campo \"{item}\" foi largado.",
"account_edit.field_reorder_modal.drag_end": "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.drag_start": "Apanhou o campo \"{item}\".",
"account_edit.field_reorder_modal.handle_label": "Arrastar o campo \"{item}\"", "account_edit.field_reorder_modal.handle_label": "Arrastar o campo \"{item}\"",
"account_edit.field_reorder_modal.title": "Reordenar campos", "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.add_title": "Adicionar nome a mostrar",
"account_edit.name_modal.edit_title": "Editar o nome a mostrar", "account_edit.name_modal.edit_title": "Editar o nome a mostrar",
"account_edit.profile_tab.button_label": "Personalizar", "account_edit.profile_tab.button_label": "Personalizar",
@@ -958,12 +964,14 @@
"notifications_permission_banner.title": "Nunca percas nada", "notifications_permission_banner.title": "Nunca percas nada",
"onboarding.follows.back": "Voltar", "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.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.search": "Pesquisar",
"onboarding.follows.title": "Segue pessoas para começar", "onboarding.follows.title": "Segue pessoas para começar",
"onboarding.profile.discoverable": "Permitir que o meu perfil seja descoberto", "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.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": "Nome a apresentar",
"onboarding.profile.display_name_hint": "O teu nome completo ou o teu nome divertido…", "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": "Biografia",
"onboarding.profile.note_hint": "Podes @mencionar outras pessoas e usar #etiquetas…", "onboarding.profile.note_hint": "Podes @mencionar outras pessoas e usar #etiquetas…",
"onboarding.profile.title": "Configuração do perfil", "onboarding.profile.title": "Configuração do perfil",

View File

@@ -183,6 +183,11 @@
"account_edit.field_reorder_modal.drag_start": "U mor fusha “{item}”.", "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.handle_label": "Tërhiqni fushën “{item}”",
"account_edit.field_reorder_modal.title": "Risistemoni fusha", "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.add_title": "Shtoni emër në ekran",
"account_edit.name_modal.edit_title": "Përpunoni emër në ekran", "account_edit.name_modal.edit_title": "Përpunoni emër në ekran",
"account_edit.profile_tab.button_label": "Përshtateni", "account_edit.profile_tab.button_label": "Përshtateni",
@@ -955,12 +960,14 @@
"notifications_permission_banner.title": "Mos tju shpëtojë gjë", "notifications_permission_banner.title": "Mos tju shpëtojë gjë",
"onboarding.follows.back": "Mbrapsht", "onboarding.follows.back": "Mbrapsht",
"onboarding.follows.empty": "Mjerisht, smund 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.empty": "Mjerisht, smund 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.search": "Kërkoni",
"onboarding.follows.title": "Që tia filloni, ndiqni persona", "onboarding.follows.title": "Që tia filloni, ndiqni persona",
"onboarding.profile.discoverable": "Bëje profilin tim të zbulueshëm", "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 tu sugjerohet njerëzve me interesa të ngjashme me ju.", "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 tu sugjerohet njerëzve me interesa të ngjashme me ju.",
"onboarding.profile.display_name": "Emër në ekran", "onboarding.profile.display_name": "Emër në ekran",
"onboarding.profile.display_name_hint": "Emri juaj i plotë, ose çtë doni…", "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": "Jetëshkrim",
"onboarding.profile.note_hint": "Mund të @përmendni persona të tjerë, ose #hashtagë…", "onboarding.profile.note_hint": "Mund të @përmendni persona të tjerë, ose #hashtagë…",
"onboarding.profile.title": "Udjisje profili", "onboarding.profile.title": "Udjisje profili",

View File

@@ -167,6 +167,11 @@
"account_edit.field_edit_modal.name_label": "Etikett", "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.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.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_edit.profile_tab.button_label": "Anpassa",
"account_note.placeholder": "Klicka för att lägga till anteckning", "account_note.placeholder": "Klicka för att lägga till anteckning",
"admin.dashboard.daily_retention": "Användarlojalitet per dag efter registrering", "admin.dashboard.daily_retention": "Användarlojalitet per dag efter registrering",

View File

@@ -184,6 +184,11 @@
"account_edit.field_reorder_modal.drag_start": "\"{item}\" alanı seçildi.", "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.handle_label": "\"{item}\" alanını sürükle",
"account_edit.field_reorder_modal.title": "Alanları yeniden düzenle", "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.add_title": "Görünen ad ekle",
"account_edit.name_modal.edit_title": "Görünen adı düzenle", "account_edit.name_modal.edit_title": "Görünen adı düzenle",
"account_edit.profile_tab.button_label": "Özelleştir", "account_edit.profile_tab.button_label": "Özelleştir",
@@ -961,12 +966,14 @@
"notifications_permission_banner.title": "Hiçbir şeyi kaçırmayın", "notifications_permission_banner.title": "Hiçbir şeyi kaçırmayın",
"onboarding.follows.back": "Geri", "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.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.search": "Ara",
"onboarding.follows.title": "Başlamak için insanları takip edin", "onboarding.follows.title": "Başlamak için insanları takip edin",
"onboarding.profile.discoverable": "Profilimi keşfedilebilir yap", "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.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": "Görünen isim",
"onboarding.profile.display_name_hint": "Tam adınız veya kullanıcı adınız…", "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": "Kişisel bilgiler",
"onboarding.profile.note_hint": "Diğer insanlara @değinebilir veya #etiketler kullanabilirsiniz…", "onboarding.profile.note_hint": "Diğer insanlara @değinebilir veya #etiketler kullanabilirsiniz…",
"onboarding.profile.title": "Profilini ayarla", "onboarding.profile.title": "Profilini ayarla",

View File

@@ -184,6 +184,11 @@
"account_edit.field_reorder_modal.drag_start": "Đã chọn trường \"{item}\".", "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.handle_label": "Kéo trường \"{item}\"",
"account_edit.field_reorder_modal.title": "Sắp xếp lại trường", "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.add_title": "Thêm tên gọi",
"account_edit.name_modal.edit_title": "Sửa 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", "account_edit.profile_tab.button_label": "Tùy chỉnh",

View File

@@ -173,6 +173,8 @@
"account_edit.field_edit_modal.link_emoji_warning": "我们建议不要同时使用自定义表情和网址。同时包含两者的自定义字段将会显示为纯文本而不是链接形式,以避免用户混淆。", "account_edit.field_edit_modal.link_emoji_warning": "我们建议不要同时使用自定义表情和网址。同时包含两者的自定义字段将会显示为纯文本而不是链接形式,以避免用户混淆。",
"account_edit.field_edit_modal.name_hint": "例如:“个人网站”", "account_edit.field_edit_modal.name_hint": "例如:“个人网站”",
"account_edit.field_edit_modal.name_label": "标签", "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_edit_modal.value_label": "值",
"account_edit.field_reorder_modal.drag_cancel": "拖拽已终止。字段“{item}”已被丢弃。", "account_edit.field_reorder_modal.drag_cancel": "拖拽已终止。字段“{item}”已被丢弃。",
"account_edit.field_reorder_modal.drag_end": "字段“{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.drag_start": "已选中字段“{item}”。",
"account_edit.field_reorder_modal.handle_label": "拖拽字段“{item}”", "account_edit.field_reorder_modal.handle_label": "拖拽字段“{item}”",
"account_edit.field_reorder_modal.title": "重新排列字段", "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.add_title": "添加显示名称",
"account_edit.name_modal.edit_title": "编辑显示名称", "account_edit.name_modal.edit_title": "编辑显示名称",
"account_edit.profile_tab.button_label": "自定义", "account_edit.profile_tab.button_label": "自定义",
@@ -959,12 +966,14 @@
"notifications_permission_banner.title": "精彩不容错过", "notifications_permission_banner.title": "精彩不容错过",
"onboarding.follows.back": "返回", "onboarding.follows.back": "返回",
"onboarding.follows.empty": "很抱歉,现在无法显示任何结果。你可以尝试使用搜索或浏览探索页面来查找要关注的人,或稍后再试。", "onboarding.follows.empty": "很抱歉,现在无法显示任何结果。你可以尝试使用搜索或浏览探索页面来查找要关注的人,或稍后再试。",
"onboarding.follows.next": "下一步:设置你的个人资料",
"onboarding.follows.search": "搜索", "onboarding.follows.search": "搜索",
"onboarding.follows.title": "关注用户,玩转 Mastodon", "onboarding.follows.title": "关注用户,玩转 Mastodon",
"onboarding.profile.discoverable": "让我的账号可被他人发现", "onboarding.profile.discoverable": "让我的账号可被他人发现",
"onboarding.profile.discoverable_hint": "当你在 Mastodon 上启用发现功能时,你的嘟文可能会出现在搜索结果与热门中,你的账号可能会被推荐给与你兴趣相似的人。", "onboarding.profile.discoverable_hint": "当你在 Mastodon 上启用发现功能时,你的嘟文可能会出现在搜索结果与热门中,你的账号可能会被推荐给与你兴趣相似的人。",
"onboarding.profile.display_name": "昵称", "onboarding.profile.display_name": "昵称",
"onboarding.profile.display_name_hint": "你的全名或昵称…", "onboarding.profile.display_name_hint": "你的全名或昵称…",
"onboarding.profile.finish": "完成",
"onboarding.profile.note": "简介", "onboarding.profile.note": "简介",
"onboarding.profile.note_hint": "你可以提及 @其他人 或使用 #话题…", "onboarding.profile.note_hint": "你可以提及 @其他人 或使用 #话题…",
"onboarding.profile.title": "设置个人资料", "onboarding.profile.title": "设置个人资料",

View File

@@ -184,6 +184,11 @@
"account_edit.field_reorder_modal.drag_start": "已選取欄位「{item}」。", "account_edit.field_reorder_modal.drag_start": "已選取欄位「{item}」。",
"account_edit.field_reorder_modal.handle_label": "拖放欄位「{item}」", "account_edit.field_reorder_modal.handle_label": "拖放欄位「{item}」",
"account_edit.field_reorder_modal.title": "重新整理欄位", "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.add_title": "新增顯示名稱",
"account_edit.name_modal.edit_title": "編輯顯示名稱", "account_edit.name_modal.edit_title": "編輯顯示名稱",
"account_edit.profile_tab.button_label": "自訂", "account_edit.profile_tab.button_label": "自訂",

View File

@@ -23,6 +23,7 @@ import {
createAppSelector, createAppSelector,
createDataLoadingThunk, createDataLoadingThunk,
} from '@/mastodon/store/typed_functions'; } from '@/mastodon/store/typed_functions';
import { inputToHashtag } from '@/mastodon/utils/hashtags';
type QueryStatus = 'idle' | 'loading' | 'error'; type QueryStatus = 'idle' | 'loading' | 'error';
@@ -82,7 +83,7 @@ const collectionSlice = createSlice({
id: collection?.id ?? null, id: collection?.id ?? null,
name: collection?.name ?? '', name: collection?.name ?? '',
description: collection?.description ?? '', description: collection?.description ?? '',
topic: collection?.tag?.name ?? '', topic: inputToHashtag(collection?.tag?.name ?? ''),
language: collection?.language ?? '', language: collection?.language ?? '',
discoverable: collection?.discoverable ?? true, discoverable: collection?.discoverable ?? true,
sensitive: collection?.sensitive ?? false, sensitive: collection?.sensitive ?? false,

View File

@@ -59,3 +59,8 @@ export const inputToHashtag = (input: string): string => {
return `#${words.join('')}${trailingSpace}`; 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);
};

View File

@@ -8478,7 +8478,7 @@ noscript {
gap: 8px; gap: 8px;
$button-breakpoint: 420px; $button-breakpoint: 420px;
$button-fallback-breakpoint: #{$button-breakpoint} + 55px; $button-fallback-breakpoint: $button-breakpoint + 55px;
&--desktop { &--desktop {
margin-top: 55px; margin-top: 55px;
@@ -8502,7 +8502,7 @@ noscript {
} }
@supports (not (container-type: inline-size)) { @supports (not (container-type: inline-size)) {
@media (min-width: (#{$button-fallback-breakpoint} + 1px)) { @media (min-width: ($button-fallback-breakpoint + 1px)) {
display: none; display: none;
} }
} }

View File

@@ -13,28 +13,28 @@
.logo-container { .logo-container {
margin: 50px auto; margin: 50px auto;
h1 { a {
display: flex; display: flex;
justify-content: center; justify-content: center;
align-items: 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 { &:focus-visible {
height: 42px; outline: var(--outline-focus-default);
margin-inline-end: 10px;
} }
}
a { .logo {
display: flex; height: 42px;
justify-content: center; margin-inline-end: 10px;
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;
}
} }
} }

View File

@@ -77,7 +77,6 @@ code {
.input { .input {
margin-bottom: 16px; margin-bottom: 16px;
overflow: hidden;
&:last-child { &:last-child {
margin-bottom: 0; margin-bottom: 0;
@@ -471,13 +470,19 @@ code {
} }
} }
.input.radio_buttons .radio label { .input.radio_buttons .radio {
margin-bottom: 5px; label {
font-family: inherit; margin-bottom: 5px;
font-size: 14px; font-family: inherit;
color: var(--color-text-primary); font-size: 14px;
display: block; color: var(--color-text-primary);
width: auto; display: block;
width: auto;
}
input[type='radio'] {
accent-color: var(--color-text-brand);
}
} }
.check_boxes { .check_boxes {
@@ -503,6 +508,12 @@ code {
} }
} }
label.checkbox {
input[type='checkbox'] {
accent-color: var(--color-text-brand);
}
}
.input.static .label_input__wrapper { .input.static .label_input__wrapper {
font-size: 14px; font-size: 14px;
padding: 10px; padding: 10px;
@@ -523,13 +534,20 @@ code {
color: var(--color-text-primary); color: var(--color-text-primary);
display: block; display: block;
width: 100%; width: 100%;
outline: 0;
font-family: inherit; font-family: inherit;
resize: vertical; resize: vertical;
background: var(--color-bg-secondary); background: var(--color-bg-secondary);
border: 1px solid var(--color-border-primary); border: 1px solid var(--color-border-primary);
border-radius: 4px; border-radius: 4px;
padding: 10px 16px; 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 { &:invalid {
box-shadow: none; box-shadow: none;
@@ -613,6 +631,11 @@ code {
margin-inline-end: 0; margin-inline-end: 0;
} }
&:focus-visible {
outline: var(--outline-focus-default);
outline-offset: 2px;
}
&:active, &:active,
&:focus, &:focus,
&:hover { &:hover {
@@ -653,6 +676,11 @@ code {
padding-inline-end: 30px; padding-inline-end: 30px;
height: 41px; height: 41px;
&:focus-visible {
outline: var(--outline-focus-default);
outline-offset: 2px;
}
@media screen and (width <= 600px) { @media screen and (width <= 600px) {
font-size: 16px; font-size: 16px;
} }

View File

@@ -25,7 +25,10 @@ class AccountMigration < ApplicationRecord
before_validation :set_target_account before_validation :set_target_account
before_validation :set_followers_count before_validation :set_followers_count
attribute :current_username, :string
normalizes :acct, with: ->(acct) { acct.strip.delete_prefix('@') } normalizes :acct, with: ->(acct) { acct.strip.delete_prefix('@') }
normalizes :current_username, with: ->(value) { value.strip.delete_prefix('@') }
validates :acct, presence: true, domain: { acct: true } validates :acct, presence: true, domain: { acct: true }
validate :validate_migration_cooldown validate :validate_migration_cooldown
@@ -33,7 +36,7 @@ class AccountMigration < ApplicationRecord
scope :within_cooldown, -> { where(created_at: cooldown_duration_ago..) } scope :within_cooldown, -> { where(created_at: cooldown_duration_ago..) }
attr_accessor :current_password, :current_username attr_accessor :current_password
def self.cooldown_duration_ago def self.cooldown_duration_ago
Time.current - COOLDOWN_PERIOD Time.current - COOLDOWN_PERIOD

Some files were not shown because too many files have changed in this diff Show More