Profile editing: Rearranging and adding fields (#38083)
Some checks failed
Bundler Audit / security (push) Has been cancelled
Check i18n / check-i18n (push) Has been cancelled
Chromatic / Check for relevant changes (push) Has been cancelled
CodeQL / Analyze (actions) (push) Has been cancelled
CodeQL / Analyze (javascript) (push) Has been cancelled
CodeQL / Analyze (ruby) (push) Has been cancelled
Crowdin / Upload translations / upload-translations (push) Has been cancelled
Check formatting / lint (push) Has been cancelled
CSS Linting / lint (push) Has been cancelled
Haml Linting / lint (push) Has been cancelled
JavaScript Linting / lint (push) Has been cancelled
Ruby Linting / lint (push) Has been cancelled
JavaScript Testing / test (push) Has been cancelled
Historical data migration test / test (14-alpine) (push) Has been cancelled
Historical data migration test / test (15-alpine) (push) Has been cancelled
Historical data migration test / test (16-alpine) (push) Has been cancelled
Historical data migration test / test (17-alpine) (push) Has been cancelled
Ruby Testing / build (production) (push) Has been cancelled
Ruby Testing / build (test) (push) Has been cancelled
Chromatic / Run Chromatic (push) Has been cancelled
Ruby Testing / test (.ruby-version) (push) Has been cancelled
Ruby Testing / test (3.2) (push) Has been cancelled
Ruby Testing / test (3.3) (push) Has been cancelled
Ruby Testing / End to End testing (.ruby-version) (push) Has been cancelled
Ruby Testing / End to End testing (3.2) (push) Has been cancelled
Ruby Testing / End to End testing (3.3) (push) Has been cancelled
Ruby Testing / Elastic Search integration testing (.ruby-version, docker.elastic.co/elasticsearch/elasticsearch:7.17.29) (push) Has been cancelled
Ruby Testing / Elastic Search integration testing (.ruby-version, docker.elastic.co/elasticsearch/elasticsearch:8.19.2) (push) Has been cancelled
Ruby Testing / Elastic Search integration testing (.ruby-version, opensearchproject/opensearch:2) (push) Has been cancelled
Ruby Testing / Elastic Search integration testing (3.2, docker.elastic.co/elasticsearch/elasticsearch:7.17.29) (push) Has been cancelled
Ruby Testing / Elastic Search integration testing (3.3, docker.elastic.co/elasticsearch/elasticsearch:7.17.29) (push) Has been cancelled
Build nightly container image / compute-suffix (push) Has been cancelled
Build nightly container image / build-image (push) Has been cancelled
Build nightly container image / build-image-streaming (push) Has been cancelled

This commit is contained in:
Echo
2026-03-06 13:57:06 +01:00
committed by GitHub
parent 11fb1df7a6
commit eb848d082a
17 changed files with 595 additions and 43 deletions

View File

@@ -26,13 +26,13 @@ export const Simple: Story = {};
export const WithMaxLength: Story = {
args: {
maxLength: 20,
counterMax: 20,
},
};
export const WithRecommended: Story = {
args: {
maxLength: 20,
counterMax: 20,
recommended: true,
},
};
@@ -52,7 +52,7 @@ export const TextArea: Story = {
value={value}
onChange={setValue}
label='Label'
maxLength={100}
counterMax={100}
/>
);
},

View File

@@ -26,7 +26,7 @@ import { TextInput } from './text_input_field';
export type EmojiInputProps = {
value?: string;
onChange?: Dispatch<SetStateAction<string>>;
maxLength?: number;
counterMax?: number;
recommended?: boolean;
} & Omit<CommonFieldWrapperProps, 'wrapperClassName'>;
@@ -39,6 +39,7 @@ export const EmojiTextInputField: FC<
hint,
hasError,
maxLength,
counterMax = maxLength,
recommended,
disabled,
...otherProps
@@ -49,7 +50,7 @@ export const EmojiTextInputField: FC<
label,
hint,
hasError,
maxLength,
counterMax,
recommended,
disabled,
inputRef,
@@ -63,6 +64,7 @@ export const EmojiTextInputField: FC<
<TextInput
{...inputProps}
{...otherProps}
maxLength={maxLength}
value={value}
ref={inputRef}
/>
@@ -78,7 +80,8 @@ export const EmojiTextAreaField: FC<
value,
label,
maxLength,
recommended = false,
counterMax = maxLength,
recommended,
disabled,
hint,
hasError,
@@ -90,7 +93,7 @@ export const EmojiTextAreaField: FC<
label,
hint,
hasError,
maxLength,
counterMax,
recommended,
disabled,
inputRef: textareaRef,
@@ -104,6 +107,7 @@ export const EmojiTextAreaField: FC<
<TextArea
{...otherProps}
{...inputProps}
maxLength={maxLength}
value={value}
ref={textareaRef}
/>
@@ -126,7 +130,7 @@ const EmojiFieldWrapper: FC<
children,
disabled,
inputRef,
maxLength,
counterMax,
recommended = false,
...otherProps
}) => {
@@ -159,10 +163,10 @@ const EmojiFieldWrapper: FC<
<>
{children({ ...inputProps, onChange: handleChange })}
<EmojiPickerButton onPick={handlePickEmoji} disabled={disabled} />
{maxLength && (
{counterMax && (
<CharacterCounter
currentString={value ?? ''}
maxLength={maxLength}
maxLength={counterMax}
recommended={recommended}
id={counterId}
/>

View File

@@ -0,0 +1,29 @@
import type { FC } from 'react';
import { EmojiHTML } from '@/mastodon/components/emoji/html';
import type { useElementHandledLink } from '@/mastodon/components/status/handled_link';
import type { FieldData } from '@/mastodon/reducers/slices/profile_edit';
import classes from '../styles.module.scss';
export const AccountField: FC<
FieldData & Partial<ReturnType<typeof useElementHandledLink>>
> = ({ onElement, ...field }) => {
return (
<>
<EmojiHTML
as='h2'
htmlString={field.name}
className={classes.fieldName}
onElement={onElement}
/>
<EmojiHTML
as='p'
htmlString={field.value}
className={classes.fieldValue}
onElement={onElement}
/>
</>
);
};

View File

@@ -21,6 +21,7 @@ import { useAppDispatch, useAppSelector } from '@/mastodon/store';
import { AccountEditColumn, AccountEditEmptyColumn } from './components/column';
import { EditButton } from './components/edit_button';
import { AccountField } from './components/field';
import { AccountFieldActions } from './components/field_actions';
import { AccountEditSection } from './components/section';
import classes from './styles.module.scss';
@@ -99,6 +100,16 @@ export const AccountEdit: FC = () => {
void dispatch(fetchProfile());
}, [dispatch]);
const maxFieldCount = useAppSelector(
(state) =>
(state.server.getIn([
'server',
'configuration',
'accounts',
'max_profile_fields',
]) as number | undefined) ?? 4,
);
const handleOpenModal = useCallback(
(type: ModalType, props?: Record<string, unknown>) => {
dispatch(openModal({ modalType: type, modalProps: props ?? {} }));
@@ -111,6 +122,12 @@ export const AccountEdit: FC = () => {
const handleBioEdit = useCallback(() => {
handleOpenModal('ACCOUNT_EDIT_BIO');
}, [handleOpenModal]);
const handleCustomFieldAdd = useCallback(() => {
handleOpenModal('ACCOUNT_EDIT_FIELD_EDIT');
}, [handleOpenModal]);
const handleCustomFieldReorder = useCallback(() => {
handleOpenModal('ACCOUNT_EDIT_FIELDS_REORDER');
}, [handleOpenModal]);
const handleCustomFieldsVerifiedHelp = useCallback(() => {
handleOpenModal('ACCOUNT_EDIT_VERIFY_LINKS');
}, [handleOpenModal]);
@@ -186,25 +203,44 @@ export const AccountEdit: FC = () => {
title={messages.customFieldsTitle}
description={messages.customFieldsPlaceholder}
showDescription={!hasFields}
>
<ol>
{profile.fields.map((field) => (
<li key={field.id} className={classes.field}>
<div>
<EmojiHTML
htmlString={field.name}
className={classes.fieldName}
{...htmlHandlers}
buttons={
<>
{profile.fields.length > 1 && (
<Button
className={classes.editButton}
onClick={handleCustomFieldReorder}
>
<FormattedMessage
id='account_edit.custom_fields.reorder_button'
defaultMessage='Reorder fields'
/>
<EmojiHTML htmlString={field.value} {...htmlHandlers} />
</div>
<AccountFieldActions
item={intl.formatMessage(messages.customFieldsName)}
id={field.id}
</Button>
)}
{hasFields && (
<EditButton
item={messages.customFieldsName}
onClick={handleCustomFieldAdd}
disabled={profile.fields.length >= maxFieldCount}
/>
</li>
))}
</ol>
)}
</>
}
>
{hasFields && (
<ol>
{profile.fields.map((field) => (
<li key={field.id} className={classes.field}>
<div>
<AccountField {...field} {...htmlHandlers} />
</div>
<AccountFieldActions
item={intl.formatMessage(messages.customFieldsName)}
id={field.id}
/>
</li>
))}
</ol>
)}
<Button
onClick={handleCustomFieldsVerifiedHelp}
className={classes.verifiedLinkHelpButton}

View File

@@ -4,11 +4,14 @@ import type { FC } from 'react';
import { defineMessages, useIntl } from 'react-intl';
import { EmojiTextAreaField } from '@/mastodon/components/form_fields';
import type { TextAreaProps } from '@/mastodon/components/form_fields/text_area_field';
import type { BaseConfirmationModalProps } from '@/mastodon/features/ui/components/confirmation_modals';
import { ConfirmationModal } from '@/mastodon/features/ui/components/confirmation_modals';
import { patchProfile } from '@/mastodon/reducers/slices/profile_edit';
import { useAppDispatch, useAppSelector } from '@/mastodon/store';
import classes from './styles.module.scss';
const messages = defineMessages({
addTitle: {
id: 'account_edit.bio_modal.add_title',
@@ -49,6 +52,12 @@ export const BioModal: FC<BaseConfirmationModalProps> = ({ onClose }) => {
}
}, [dispatch, isPending, newBio, onClose]);
// TypeScript isn't correctly picking up minRows when on the element directly.
const textAreaProps = {
autoSize: true,
minRows: 3,
} as const satisfies TextAreaProps;
return (
<ConfirmationModal
title={intl.formatMessage(bio ? messages.editTitle : messages.addTitle)}
@@ -66,9 +75,10 @@ export const BioModal: FC<BaseConfirmationModalProps> = ({ onClose }) => {
onChange={setNewBio}
aria-labelledby={titleId}
maxLength={maxLength}
className={classes.bioField}
{...textAreaProps}
// eslint-disable-next-line jsx-a11y/no-autofocus -- This is a modal, it's fine.
autoFocus
autoSize
/>
</ConfirmationModal>
);

View File

@@ -1,4 +1,4 @@
import { useCallback, useState } from 'react';
import { useCallback, useMemo, useState } from 'react';
import type { FC } from 'react';
import { defineMessages, FormattedMessage, useIntl } from 'react-intl';
@@ -6,6 +6,7 @@ import { defineMessages, FormattedMessage, useIntl } from 'react-intl';
import type { Map as ImmutableMap } from 'immutable';
import { Button } from '@/mastodon/components/button';
import { Callout } from '@/mastodon/components/callout';
import { EmojiTextInputField } from '@/mastodon/components/form_fields';
import {
removeField,
@@ -49,12 +50,18 @@ const messages = defineMessages({
id: 'account_edit.field_edit_modal.value_hint',
defaultMessage: 'E.g. “example.me”',
},
limitHeader: {
id: 'account_edit.field_edit_modal.limit_header',
defaultMessage: 'Recommended character limit exceeded',
},
save: {
id: 'account_edit.save',
defaultMessage: 'Save',
},
});
// We have two different values- the hard limit set by the server,
// and the soft limit for mobile display.
const selectFieldLimits = createAppSelector(
[
(state) =>
@@ -68,6 +75,13 @@ const selectFieldLimits = createAppSelector(
}),
);
const RECOMMENDED_LIMIT = 40;
const selectEmojiCodes = createAppSelector(
[(state) => state.custom_emojis],
(emojis) => emojis.map((emoji) => emoji.get('shortcode')).toArray(),
);
export const EditFieldModal: FC<DialogModalProps & { fieldKey?: string }> = ({
onClose,
fieldKey,
@@ -86,6 +100,16 @@ export const EditFieldModal: FC<DialogModalProps & { fieldKey?: string }> = ({
newLabel.length > nameLimit ||
newValue.length > valueLimit;
const customEmojiCodes = useAppSelector(selectEmojiCodes);
const hasLinkAndEmoji = useMemo(() => {
const text = `${newLabel} ${newValue}`; // Combine text, as we're searching it all.
const hasLink = /https?:\/\//.test(text);
const hasEmoji = customEmojiCodes.some((code) =>
text.includes(`:${code}:`),
);
return hasLink && hasEmoji;
}, [customEmojiCodes, newLabel, newValue]);
const dispatch = useAppDispatch();
const handleSave = useCallback(() => {
if (disabled || isPending) {
@@ -116,6 +140,8 @@ export const EditFieldModal: FC<DialogModalProps & { fieldKey?: string }> = ({
label={intl.formatMessage(messages.editLabelField)}
hint={intl.formatMessage(messages.editLabelHint)}
maxLength={nameLimit}
counterMax={RECOMMENDED_LIMIT}
recommended
/>
<EmojiTextInputField
@@ -124,7 +150,31 @@ export const EditFieldModal: FC<DialogModalProps & { fieldKey?: string }> = ({
label={intl.formatMessage(messages.editValueField)}
hint={intl.formatMessage(messages.editValueHint)}
maxLength={valueLimit}
counterMax={RECOMMENDED_LIMIT}
recommended
/>
{hasLinkAndEmoji && (
<Callout variant='warning'>
<FormattedMessage
id='account_edit.field_edit_modal.link_emoji_warning'
defaultMessage='We recommend against the use of custom emoji in combination with urls. Custom fields containing both will display as text only instead of as a link, in order to prevent user confusion.'
/>
</Callout>
)}
{(newLabel.length > RECOMMENDED_LIMIT ||
newValue.length > RECOMMENDED_LIMIT) && (
<Callout
variant='warning'
title={intl.formatMessage(messages.limitHeader)}
>
<FormattedMessage
id='account_edit.field_edit_modal.limit_message'
defaultMessage='Mobile users might not see your field in full.'
/>
</Callout>
)}
</ConfirmationModal>
);
};
@@ -165,11 +215,3 @@ export const DeleteFieldModal: FC<DialogModalProps & { fieldKey: string }> = ({
</DialogModal>
);
};
export const RearrangeFieldsModal: FC<DialogModalProps> = ({ onClose }) => {
return (
<DialogModal onClose={onClose} title='Not implemented yet'>
<p>Not implemented yet</p>
</DialogModal>
);
};

View File

@@ -0,0 +1,322 @@
import type { FC, KeyboardEventHandler } from 'react';
import { useState, useCallback, useMemo } from 'react';
import { defineMessages, useIntl } from 'react-intl';
import classNames from 'classnames';
import type {
DragEndEvent,
ScreenReaderInstructions,
Announcements,
Active,
} from '@dnd-kit/core';
import {
useSensors,
useSensor,
PointerSensor,
KeyboardSensor,
DndContext,
closestCenter,
} from '@dnd-kit/core';
import {
restrictToVerticalAxis,
restrictToParentElement,
} from '@dnd-kit/modifiers';
import {
arrayMove,
sortableKeyboardCoordinates,
SortableContext,
useSortable,
verticalListSortingStrategy,
} from '@dnd-kit/sortable';
import { CSS } from '@dnd-kit/utilities';
import { CustomEmojiProvider } from '@/mastodon/components/emoji/context';
import { normalizeKey } from '@/mastodon/components/hotkeys/utils';
import { Icon } from '@/mastodon/components/icon';
import type { FieldData } from '@/mastodon/reducers/slices/profile_edit';
import {
patchProfile,
selectFieldById,
} from '@/mastodon/reducers/slices/profile_edit';
import {
createAppSelector,
useAppDispatch,
useAppSelector,
} from '@/mastodon/store';
import DragIndicatorIcon from '@/material-icons/400-24px/drag_indicator.svg?react';
import { ConfirmationModal } from '../../ui/components/confirmation_modals';
import type { DialogModalProps } from '../../ui/components/dialog_modal';
import { AccountField } from '../components/field';
import classes from './styles.module.scss';
const messages = defineMessages({
rearrangeTitle: {
id: 'account_edit.field_reorder_modal.title',
defaultMessage: 'Rearrange fields',
},
handleLabel: {
id: 'account_edit.field_reorder_modal.handle_label',
defaultMessage: 'Drag field "{item}"',
},
screenReaderInstructions: {
id: 'account_edit.field_reorder_modal.drag_instructions',
defaultMessage:
'To rearrange custom fields, press space or enter. While dragging, use the arrow keys to move the field up or down. Press space or enter again to drop the field in its new position, or press escape to cancel.',
},
onDragStart: {
id: 'account_edit.field_reorder_modal.drag_start',
defaultMessage: 'Picked up field "{item}".',
},
onDragMove: {
id: 'account_edit.field_reorder_modal.drag_move',
defaultMessage: 'Field "{item}" was moved.',
},
onDragMoveOver: {
id: 'account_edit.field_reorder_modal.drag_over',
defaultMessage: 'Field "{item}" was moved over "{over}".',
},
onDragEnd: {
id: 'account_edit.field_reorder_modal.drag_end',
defaultMessage: 'Field "{item}" was dropped.',
},
onDragCancel: {
id: 'account_edit.field_reorder_modal.drag_cancel',
defaultMessage: 'Dragging was cancelled. Field "{item}" was dropped.',
},
save: {
id: 'account_edit.save',
defaultMessage: 'Save',
},
});
const selectFields = createAppSelector(
[(state) => state.profileEdit],
({ isPending, profile }) => ({
isPending: isPending,
fields: profile?.fields ?? [],
}),
);
export const ReorderFieldsModal: FC<DialogModalProps> = ({ onClose }) => {
const intl = useIntl();
const { fields, isPending } = useAppSelector(selectFields);
const [fieldKeys, setFieldKeys] = useState<string[]>(
fields.map((field) => field.id),
);
const [isDragging, setIsDragging] = useState(false);
const handleDragStart = useCallback(() => {
setIsDragging(true);
}, []);
const handleDragEnd = useCallback((event: DragEndEvent) => {
const { active, over } = event;
setFieldKeys((prev) => {
if (!over) {
return prev;
}
const oldIndex = prev.indexOf(active.id as string);
const newIndex = prev.indexOf(over.id as string);
return arrayMove(prev, oldIndex, newIndex);
});
setIsDragging(false);
}, []);
// Combines the Escape shortcut for closing the modal and for cancelling the drag, depending on the current state.
const handleEscape: KeyboardEventHandler = useCallback(
(event) => {
const key = normalizeKey(event.key);
if (key === 'Escape') {
// Stops propagation to avoid triggering the handler in ModalRoot.
event.stopPropagation();
// Trigger the drag cancel here, since onDragCancel triggers before this handler.
if (isDragging) {
setIsDragging(false);
} else {
onClose();
}
}
},
[isDragging, onClose],
);
const sensors = useSensors(
useSensor(PointerSensor, {
activationConstraint: {
distance: 5,
},
}),
useSensor(KeyboardSensor, {
coordinateGetter: sortableKeyboardCoordinates,
}),
);
const accessibility: {
screenReaderInstructions: ScreenReaderInstructions;
announcements: Announcements;
} = useMemo(
() => ({
screenReaderInstructions: {
draggable: intl.formatMessage(messages.screenReaderInstructions),
},
announcements: {
onDragStart({ active }) {
return intl.formatMessage(messages.onDragStart, {
item: labelFromActive(active),
});
},
onDragOver({ active, over }) {
if (over && active.id !== over.id) {
return intl.formatMessage(messages.onDragMoveOver, {
item: labelFromActive(active),
over: labelFromActive(over),
});
}
return intl.formatMessage(messages.onDragMove, {
item: labelFromActive(active),
});
},
onDragEnd({ active }) {
return intl.formatMessage(messages.onDragEnd, {
item: labelFromActive(active),
});
},
onDragCancel({ active }) {
return intl.formatMessage(messages.onDragCancel, {
item: labelFromActive(active),
});
},
},
}),
[intl],
);
const dispatch = useAppDispatch();
const handleSave = useCallback(() => {
const newFields: Pick<FieldData, 'name' | 'value'>[] = [];
for (const key of fieldKeys) {
const field = fields.find((f) => f.id === key);
if (!field) {
console.warn(`Field with id ${key} not found, closing modal.`);
onClose();
return;
}
newFields.push({ name: field.name, value: field.value });
void dispatch(patchProfile({ fields_attributes: newFields })).then(
onClose,
);
}
}, [dispatch, fieldKeys, fields, onClose]);
const emojis = useAppSelector((state) => state.custom_emojis);
return (
// Add a wrapper here in the capture phase, so that it can be intercepted before the window listener in ModalRoot.
<div onKeyUpCapture={handleEscape}>
<ConfirmationModal
onClose={onClose}
title={intl.formatMessage(messages.rearrangeTitle)}
confirm={intl.formatMessage(messages.save)}
onConfirm={handleSave}
className={classes.wrapper}
updating={isPending}
noFocusButton
>
<DndContext
sensors={sensors}
collisionDetection={closestCenter}
onDragStart={handleDragStart}
onDragEnd={handleDragEnd}
modifiers={[restrictToVerticalAxis, restrictToParentElement]}
accessibility={accessibility}
>
<SortableContext
items={fieldKeys}
strategy={verticalListSortingStrategy}
disabled={isPending}
>
<CustomEmojiProvider emojis={emojis}>
<ol>
{fieldKeys.map((key) => (
<ReorderFieldItem key={key} id={key} />
))}
</ol>
</CustomEmojiProvider>
</SortableContext>
</DndContext>
</ConfirmationModal>
</div>
);
};
const ReorderFieldItem: FC<{ id: string }> = ({ id }) => {
const intl = useIntl();
const field = useAppSelector((state) => selectFieldById(state, id));
const {
attributes,
listeners,
setNodeRef,
transform,
transition,
isDragging,
newIndex,
overIndex,
} = useSortable({
id,
data: {
label: field?.name ?? id,
},
});
if (!field) {
return null;
}
const style = {
transform: CSS.Translate.toString(transform),
transition,
};
return (
<li
ref={setNodeRef}
className={classNames(
classes.field,
isDragging && classes.fieldDragging,
!isDragging && newIndex > 0 && classes.fieldNotFirst,
!isDragging && newIndex + 1 === overIndex && classes.fieldActiveUnder,
)}
style={style}
>
<Icon
icon={DragIndicatorIcon}
id='drag'
className={classes.fieldHandle}
aria-label={intl.formatMessage(messages.handleLabel, {
item: field.name,
})}
{...listeners}
{...attributes}
/>
<AccountField {...field} />
</li>
);
};
function labelFromActive(item: Pick<Active, 'id' | 'data'>) {
if (item.data.current?.label) {
return item.data.current.label as string;
}
return item.id as string;
}

View File

@@ -1,5 +1,6 @@
export * from './bio_modal';
export * from './fields_modals';
export * from './fields_reorder_modal';
export * from './name_modal';
export * from './profile_display_modal';
export * from './verified_modal';

View File

@@ -66,7 +66,7 @@ export const NameModal: FC<BaseConfirmationModalProps> = ({ onClose }) => {
value={newName}
onChange={setNewName}
aria-labelledby={titleId}
maxLength={maxLength}
counterMax={maxLength}
label=''
// eslint-disable-next-line jsx-a11y/no-autofocus -- This is a modal, it's fine.
autoFocus

View File

@@ -2,6 +2,12 @@
display: flex;
gap: 16px;
flex-direction: column;
font-size: 15px;
}
.bioField {
// 160px is approx the height of the modal header and footer
max-height: calc(80vh - 160px);
}
.toggleInputWrapper {
@@ -14,6 +20,66 @@
}
}
.field {
padding: 12px 0;
display: grid;
grid-template-columns: auto 1fr;
grid-template-rows: auto auto;
column-gap: 12px;
touch-action: none;
position: relative;
}
.fieldNotFirst::before,
.fieldActiveUnder::after {
content: '';
position: absolute;
left: 0;
right: 0;
height: 1px;
background: var(--color-border-primary);
}
.fieldNotFirst::before {
top: 0;
}
.fieldActiveUnder::after {
bottom: -1px; // -1px to cover the border of the next field
}
.fieldHandle {
grid-row: span 2;
padding: 8px 0;
align-self: center;
border-radius: 4px;
background: var(--color-bg-secondary);
cursor: grab;
transition: background 0.2s ease-in-out;
&:hover {
background: var(--color-bg-brand-softer);
}
&:focus-visible {
outline: var(--outline-focus-default);
outline-offset: 2px;
}
}
.fieldDragging {
cursor: grabbing;
p {
opacity: 0.7;
}
.fieldHandle {
cursor: grabbing;
background: var(--color-bg-brand-soft);
}
}
.verifiedSteps {
font-size: 15px;

View File

@@ -33,6 +33,7 @@ export const VerifiedModal: FC<DialogModalProps> = ({ onClose }) => {
/>
}
noCancelButton
wrapperClassName={classes.wrapper}
>
<FormattedMessage
id='account_edit.verified_modal.details'

View File

@@ -35,11 +35,6 @@
}
}
.fieldName {
color: var(--color-text-secondary);
font-size: 13px;
}
.verifiedLinkHelpButton {
font-size: 13px;
font-weight: 600;
@@ -127,6 +122,11 @@
&:hover {
background-color: var(--color-bg-brand-softer);
}
&:disabled {
background-color: var(--color-bg-primary);
opacity: 0.5;
}
}
svg {
@@ -141,6 +141,18 @@
--hover-icon-color: var(--color-text-on-error-base);
}
// Field component
.fieldName {
color: var(--color-text-secondary);
font-size: 13px;
}
.fieldValue {
color: var(--color-text-primary);
font-size: 15px;
}
// Item list component
.itemList {

View File

@@ -153,6 +153,7 @@
"account_edit.column_title": "Edit Profile",
"account_edit.custom_fields.name": "field",
"account_edit.custom_fields.placeholder": "Add your pronouns, external links, or anything else youd like to share.",
"account_edit.custom_fields.reorder_button": "Reorder fields",
"account_edit.custom_fields.tip_content": "You can easily add credibility to your Mastodon account by verifying links to any websites you own.",
"account_edit.custom_fields.tip_title": "Tip: Adding verified links",
"account_edit.custom_fields.title": "Custom fields",
@@ -167,10 +168,21 @@
"account_edit.field_delete_modal.title": "Delete custom field?",
"account_edit.field_edit_modal.add_title": "Add custom field",
"account_edit.field_edit_modal.edit_title": "Edit custom field",
"account_edit.field_edit_modal.limit_header": "Recommended character limit exceeded",
"account_edit.field_edit_modal.limit_message": "Mobile users might not see your field in full.",
"account_edit.field_edit_modal.link_emoji_warning": "We recommend against the use of custom emoji in combination with urls. Custom fields containing both will display as text only instead of as a link, in order to prevent user confusion.",
"account_edit.field_edit_modal.name_hint": "E.g. “Personal website”",
"account_edit.field_edit_modal.name_label": "Label",
"account_edit.field_edit_modal.value_hint": "E.g. “example.me”",
"account_edit.field_edit_modal.value_label": "Value",
"account_edit.field_reorder_modal.drag_cancel": "Dragging was cancelled. Field \"{item}\" was dropped.",
"account_edit.field_reorder_modal.drag_end": "Field \"{item}\" was dropped.",
"account_edit.field_reorder_modal.drag_instructions": "To rearrange custom fields, press space or enter. While dragging, use the arrow keys to move the field up or down. Press space or enter again to drop the field in its new position, or press escape to cancel.",
"account_edit.field_reorder_modal.drag_move": "Field \"{item}\" was moved.",
"account_edit.field_reorder_modal.drag_over": "Field \"{item}\" was moved over \"{over}\".",
"account_edit.field_reorder_modal.drag_start": "Picked up field \"{item}\".",
"account_edit.field_reorder_modal.handle_label": "Drag field \"{item}\"",
"account_edit.field_reorder_modal.title": "Rearrange fields",
"account_edit.name_modal.add_title": "Add display name",
"account_edit.name_modal.edit_title": "Edit display name",
"account_edit.profile_tab.button_label": "Customize",

View File

@@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" height="24" viewBox="0 -960 960 960" width="24"><path d="M360-160q-33 0-56.5-23.5T280-240q0-33 23.5-56.5T360-320q33 0 56.5 23.5T440-240q0 33-23.5 56.5T360-160Zm240 0q-33 0-56.5-23.5T520-240q0-33 23.5-56.5T600-320q33 0 56.5 23.5T680-240q0 33-23.5 56.5T600-160ZM360-400q-33 0-56.5-23.5T280-480q0-33 23.5-56.5T360-560q33 0 56.5 23.5T440-480q0 33-23.5 56.5T360-400Zm240 0q-33 0-56.5-23.5T520-480q0-33 23.5-56.5T600-560q33 0 56.5 23.5T680-480q0 33-23.5 56.5T600-400ZM360-640q-33 0-56.5-23.5T280-720q0-33 23.5-56.5T360-800q33 0 56.5 23.5T440-720q0 33-23.5 56.5T360-640Zm240 0q-33 0-56.5-23.5T520-720q0-33 23.5-56.5T600-800q33 0 56.5 23.5T680-720q0 33-23.5 56.5T600-640Z"/></svg>

After

Width:  |  Height:  |  Size: 712 B

View File

@@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" height="24" viewBox="0 -960 960 960" width="24"><path d="M360-160q-33 0-56.5-23.5T280-240q0-33 23.5-56.5T360-320q33 0 56.5 23.5T440-240q0 33-23.5 56.5T360-160Zm240 0q-33 0-56.5-23.5T520-240q0-33 23.5-56.5T600-320q33 0 56.5 23.5T680-240q0 33-23.5 56.5T600-160ZM360-400q-33 0-56.5-23.5T280-480q0-33 23.5-56.5T360-560q33 0 56.5 23.5T440-480q0 33-23.5 56.5T360-400Zm240 0q-33 0-56.5-23.5T520-480q0-33 23.5-56.5T600-560q33 0 56.5 23.5T680-480q0 33-23.5 56.5T600-400ZM360-640q-33 0-56.5-23.5T280-720q0-33 23.5-56.5T360-800q33 0 56.5 23.5T440-720q0 33-23.5 56.5T360-640Zm240 0q-33 0-56.5-23.5T520-720q0-33 23.5-56.5T600-800q33 0 56.5 23.5T680-720q0 33-23.5 56.5T600-640Z"/></svg>

After

Width:  |  Height:  |  Size: 712 B