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

Merge upstream changes up to 366856f3bc
This commit is contained in:
Claire
2025-11-19 22:25:28 +01:00
committed by GitHub
153 changed files with 20551 additions and 579 deletions

View File

@@ -68,6 +68,7 @@ docker-compose.override.yml
# Ignore vendored CSS reset
app/javascript/styles/mastodon/reset.scss
app/javascript/styles_new/mastodon/reset.scss
# Ignore Javascript pending https://github.com/mastodon/mastodon/pull/23631
*.js

View File

@@ -24,7 +24,7 @@ gem 'ruby-vips', '~> 2.2', require: false
gem 'active_model_serializers', '~> 0.10'
gem 'addressable', '~> 2.8'
gem 'bootsnap', '~> 1.18.0', require: false
gem 'bootsnap', '~> 1.19.0', require: false
gem 'browser'
gem 'charlock_holmes', '~> 0.7.7'
gem 'chewy', '~> 7.3'

View File

@@ -129,7 +129,7 @@ GEM
binding_of_caller (1.0.1)
debug_inspector (>= 1.2.0)
blurhash (0.1.8)
bootsnap (1.18.6)
bootsnap (1.19.0)
msgpack (~> 1.2)
brakeman (7.1.1)
racc
@@ -349,7 +349,7 @@ GEM
azure-blob (~> 0.5.2)
hashie (~> 5.0)
jmespath (1.6.2)
json (2.15.2)
json (2.16.0)
json-canonicalization (1.0.0)
json-jwt (1.17.0)
activesupport (>= 4.2)
@@ -446,7 +446,7 @@ GEM
mime-types-data (3.2025.0924)
mini_mime (1.1.5)
mini_portile2 (2.8.9)
minitest (5.26.0)
minitest (5.26.1)
msgpack (1.8.0)
multi_json (1.17.0)
mutex_m (0.3.0)
@@ -759,7 +759,7 @@ GEM
rubocop-ast (>= 1.47.1, < 2.0)
ruby-progressbar (~> 1.7)
unicode-display_width (>= 2.4.0, < 4.0)
rubocop-ast (1.47.1)
rubocop-ast (1.48.0)
parser (>= 3.3.7.2)
prism (~> 1.4)
rubocop-capybara (2.22.1)
@@ -778,10 +778,10 @@ GEM
rack (>= 1.1)
rubocop (>= 1.75.0, < 2.0)
rubocop-ast (>= 1.44.0, < 2.0)
rubocop-rspec (3.7.0)
rubocop-rspec (3.8.0)
lint_roller (~> 1.1)
rubocop (~> 1.72, >= 1.72.1)
rubocop-rspec_rails (2.31.0)
rubocop (~> 1.81)
rubocop-rspec_rails (2.32.0)
lint_roller (~> 1.1)
rubocop (~> 1.72, >= 1.72.1)
rubocop-rspec (~> 3.5)
@@ -941,7 +941,7 @@ DEPENDENCIES
better_errors (~> 2.9)
binding_of_caller (~> 1.0)
blurhash (~> 0.1)
bootsnap (~> 1.18.0)
bootsnap (~> 1.19.0)
brakeman (~> 7.0)
browser
bundler-audit (~> 0.9)

View File

@@ -9,7 +9,7 @@ module Admin
@site_upload.destroy!
redirect_back fallback_location: admin_settings_path, notice: I18n.t('admin.site_uploads.destroyed_msg')
redirect_back_or_to admin_settings_path, notice: I18n.t('admin.site_uploads.destroyed_msg')
end
private

View File

@@ -6,11 +6,11 @@ module ThemeHelper
if theme == 'system'
''.html_safe.tap do |tags|
tags << vite_stylesheet_tag("skins/#{flavour}/mastodon-light", type: :virtual, media: 'not all and (prefers-color-scheme: dark)', crossorigin: 'anonymous')
tags << vite_stylesheet_tag("skins/#{flavour}/default", type: :virtual, media: '(prefers-color-scheme: dark)', crossorigin: 'anonymous')
tags << vite_stylesheet_tag(theme_path_for(flavour, 'mastodon-light'), type: :virtual, media: 'not all and (prefers-color-scheme: dark)', crossorigin: 'anonymous')
tags << vite_stylesheet_tag(theme_path_for(flavour, 'default'), type: :virtual, media: '(prefers-color-scheme: dark)', crossorigin: 'anonymous')
end
else
vite_stylesheet_tag "skins/#{flavour}/#{theme}", type: :virtual, media: 'all', crossorigin: 'anonymous'
vite_stylesheet_tag theme_path_for(flavour, theme), type: :virtual, media: 'all', crossorigin: 'anonymous'
end
end
@@ -57,4 +57,8 @@ module ThemeHelper
def theme_color_for(theme)
theme == 'mastodon-light' ? Themes::THEME_COLORS[:light] : Themes::THEME_COLORS[:dark]
end
def theme_path_for(flavour, theme)
"skins/#{flavour}/#{theme}"
end
end

View File

@@ -180,25 +180,24 @@ export function useHotkeys<T extends HTMLElement>(handlers: HandlerMap) {
if (shouldHandleEvent) {
const matchCandidates: {
handler: (event: KeyboardEvent) => void;
// A candidate will be have an undefined handler if it's matched,
// but handled in a parent component rather than this one.
handler: ((event: KeyboardEvent) => void) | undefined;
priority: number;
}[] = [];
(Object.keys(hotkeyMatcherMap) as HotkeyName[]).forEach(
(handlerName) => {
const handler = handlersRef.current[handlerName];
const hotkeyMatcher = hotkeyMatcherMap[handlerName];
if (handler) {
const hotkeyMatcher = hotkeyMatcherMap[handlerName];
const { isMatch, priority } = hotkeyMatcher(
event,
bufferedKeys.current,
);
const { isMatch, priority } = hotkeyMatcher(
event,
bufferedKeys.current,
);
if (isMatch) {
matchCandidates.push({ handler, priority });
}
if (isMatch) {
matchCandidates.push({ handler, priority });
}
},
);

View File

@@ -144,7 +144,7 @@ export const HandledLink: FC<HandledLinkProps & ComponentProps<'a'>> = ({
return (
<Link
className={classNames('mention hashtag', className)}
to={`/tags/${hashtag}`}
to={`/tags/${encodeURIComponent(hashtag)}`}
rel='tag'
data-menu-hashtag={hashtagAccountId}
>
@@ -194,7 +194,7 @@ export const HandledLink: FC<HandledLinkProps & ComponentProps<'a'>> = ({
return (
<a
{...props}
href={encodeURI(href)}
href={href}
title={href}
className={classNames('unhandled-link', className)}
target='_blank'

View File

@@ -13,6 +13,7 @@ import {
import { pasteLinkCompose } from 'flavours/glitch/actions/compose_typed';
import { openModal } from 'flavours/glitch/actions/modal';
import { PRIVATE_QUOTE_MODAL_ID } from 'flavours/glitch/features/ui/components/confirmation_modals/private_quote_notify';
import { me } from 'flavours/glitch/initial_state';
import { privacyPreference } from 'flavours/glitch/utils/privacy_preference';
import ComposeForm from '../components/compose_form';
@@ -73,6 +74,7 @@ const mapStateToProps = state => ({
quoteToPrivate:
!!state.getIn(['compose', 'quoted_status_id'])
&& state.getIn(['compose', 'privacy']) === 'private'
&& state.getIn(['statuses', state.getIn(['compose', 'quoted_status_id']), 'account']) !== me
&& !state.getIn(['settings', 'dismissed_banners', PRIVATE_QUOTE_MODAL_ID]),
isInReply: state.getIn(['compose', 'in_reply_to']) !== null,
lang: state.getIn(['compose', 'language']),

View File

@@ -1,6 +1,7 @@
import { initialState } from '@/flavours/glitch/initial_state';
import { toSupportedLocale } from './locale';
import type { LocaleOrCustom } from './types';
import { emojiLogger } from './utils';
// eslint-disable-next-line import/default -- Importing via worker loader.
import EmojiWorker from './worker?worker&inline';
@@ -24,19 +25,17 @@ export function initializeEmoji() {
}
if (worker) {
// Assign worker to const to make TS happy inside the event listener.
const thisWorker = worker;
const timeoutId = setTimeout(() => {
log('worker is not ready after timeout');
worker = null;
void fallbackLoad();
}, WORKER_TIMEOUT);
thisWorker.addEventListener('message', (event: MessageEvent<string>) => {
worker.addEventListener('message', (event: MessageEvent<string>) => {
const { data: message } = event;
if (message === 'ready') {
log('worker ready, loading data');
clearTimeout(timeoutId);
thisWorker.postMessage('custom');
messageWorker('custom');
void loadEmojiLocale(userLocale);
// Load English locale as well, because people are still used to
// using it from before we supported other locales.
@@ -55,20 +54,35 @@ export function initializeEmoji() {
async function fallbackLoad() {
log('falling back to main thread for loading');
const { importCustomEmojiData } = await import('./loader');
await importCustomEmojiData();
const emojis = await importCustomEmojiData();
if (emojis) {
log('loaded %d custom emojis', emojis.length);
}
await loadEmojiLocale(userLocale);
if (userLocale !== 'en') {
await loadEmojiLocale('en');
}
}
export async function loadEmojiLocale(localeString: string) {
async function loadEmojiLocale(localeString: string) {
const locale = toSupportedLocale(localeString);
const { importEmojiData, localeToPath } = await import('./loader');
if (worker) {
worker.postMessage(locale);
const path = await localeToPath(locale);
log('asking worker to load locale %s from %s', locale, path);
messageWorker(locale, path);
} else {
const { importEmojiData } = await import('./loader');
await importEmojiData(locale);
const emojis = await importEmojiData(locale);
if (emojis) {
log('loaded %d emojis to locale %s', emojis.length, locale);
}
}
}
function messageWorker(locale: LocaleOrCustom, path?: string) {
if (!worker) {
return;
}
worker.postMessage({ locale, path });
}

View File

@@ -8,44 +8,64 @@ import {
putLatestEtag,
} from './database';
import { toSupportedLocale, toSupportedLocaleOrCustom } from './locale';
import type { CustomEmojiData, LocaleOrCustom } from './types';
import { emojiLogger } from './utils';
import type { CustomEmojiData } from './types';
const log = emojiLogger('loader');
export async function importEmojiData(localeString: string) {
export async function importEmojiData(localeString: string, path?: string) {
const locale = toSupportedLocale(localeString);
const emojis = await fetchAndCheckEtag<CompactEmoji[]>(locale);
// Validate the provided path.
if (path && !/^[/a-z]*\/packs\/assets\/compact-\w+\.json$/.test(path)) {
throw new Error('Invalid path for emoji data');
} else {
// Otherwise get the path if not provided.
path ??= await localeToPath(locale);
}
const emojis = await fetchAndCheckEtag<CompactEmoji[]>(locale, path);
if (!emojis) {
return;
}
const flattenedEmojis: FlatCompactEmoji[] = flattenEmojiData(emojis);
log('loaded %d for %s locale', flattenedEmojis.length, locale);
await putEmojiData(flattenedEmojis, locale);
return flattenedEmojis;
}
export async function importCustomEmojiData() {
const emojis = await fetchAndCheckEtag<CustomEmojiData[]>('custom');
const emojis = await fetchAndCheckEtag<CustomEmojiData[]>(
'custom',
'/api/v1/custom_emojis',
);
if (!emojis) {
return;
}
log('loaded %d custom emojis', emojis.length);
await putCustomEmojiData(emojis);
return emojis;
}
async function fetchAndCheckEtag<ResultType extends object[]>(
localeOrCustom: LocaleOrCustom,
const modules = import.meta.glob<string>(
'../../../../../../node_modules/emojibase-data/**/compact.json',
{
query: '?url',
import: 'default',
},
);
export function localeToPath(locale: Locale) {
const key = `../../../../../../node_modules/emojibase-data/${locale}/compact.json`;
if (!modules[key] || typeof modules[key] !== 'function') {
throw new Error(`Unsupported locale: ${locale}`);
}
return modules[key]();
}
export async function fetchAndCheckEtag<ResultType extends object[]>(
localeString: string,
path: string,
): Promise<ResultType | null> {
const locale = toSupportedLocaleOrCustom(localeOrCustom);
const locale = toSupportedLocaleOrCustom(localeString);
// Use location.origin as this script may be loaded from a CDN domain.
const url = new URL(location.origin);
if (locale === 'custom') {
url.pathname = '/api/v1/custom_emojis';
} else {
const modulePath = await localeToPath(locale);
url.pathname = modulePath;
}
const url = new URL(path, location.origin);
const oldEtag = await loadLatestEtag(locale);
const response = await fetch(url, {
@@ -60,38 +80,20 @@ async function fetchAndCheckEtag<ResultType extends object[]>(
}
if (!response.ok) {
throw new Error(
`Failed to fetch emoji data for ${localeOrCustom}: ${response.statusText}`,
`Failed to fetch emoji data for ${locale}: ${response.statusText}`,
);
}
const data = (await response.json()) as ResultType;
if (!Array.isArray(data)) {
throw new Error(
`Unexpected data format for ${localeOrCustom}: expected an array`,
);
throw new Error(`Unexpected data format for ${locale}: expected an array`);
}
// Store the ETag for future requests
const etag = response.headers.get('ETag');
if (etag) {
await putLatestEtag(etag, localeOrCustom);
await putLatestEtag(etag, localeString);
}
return data;
}
const modules = import.meta.glob<string>(
'../../../../../../node_modules/emojibase-data/**/compact.json',
{
query: '?url',
import: 'default',
},
);
function localeToPath(locale: Locale) {
const key = `../../../../../../node_modules/emojibase-data/${locale}/compact.json`;
if (!modules[key] || typeof modules[key] !== 'function') {
throw new Error(`Unsupported locale: ${locale}`);
}
return modules[key]();
}

View File

@@ -162,7 +162,7 @@ describe('loadEmojiDataToState', () => {
const dbCall = vi
.spyOn(db, 'loadEmojiByHexcode')
.mockRejectedValue(new db.LocaleNotLoadedError('en'));
vi.spyOn(loader, 'importEmojiData').mockResolvedValueOnce();
vi.spyOn(loader, 'importEmojiData').mockResolvedValueOnce(undefined);
const consoleCall = vi
.spyOn(console, 'warn')
.mockImplementationOnce(() => null);

View File

@@ -1,18 +1,25 @@
import { importEmojiData, importCustomEmojiData } from './loader';
import { importCustomEmojiData, importEmojiData } from './loader';
addEventListener('message', handleMessage);
self.postMessage('ready'); // After the worker is ready, notify the main thread
function handleMessage(event: MessageEvent<string>) {
const { data: locale } = event;
void loadData(locale);
function handleMessage(event: MessageEvent<{ locale: string; path?: string }>) {
const {
data: { locale, path },
} = event;
void loadData(locale, path);
}
async function loadData(locale: string) {
if (locale !== 'custom') {
await importEmojiData(locale);
async function loadData(locale: string, path?: string) {
let importCount: number | undefined;
if (locale === 'custom') {
importCount = (await importCustomEmojiData())?.length;
} else if (path) {
importCount = (await importEmojiData(locale, path))?.length;
} else {
await importCustomEmojiData();
throw new Error('Path is required for loading locale emoji data');
}
if (importCount) {
self.postMessage(`loaded ${importCount} emojis into ${locale}`);
}
self.postMessage(`loaded ${locale}`);
}

View File

@@ -4,7 +4,7 @@
@typescript-eslint/no-unsafe-assignment */
import type { CSSProperties } from 'react';
import { useState, useRef, useCallback } from 'react';
import { useState, useRef, useCallback, useEffect } from 'react';
import { FormattedMessage } from 'react-intl';
@@ -57,6 +57,8 @@ export const DetailedStatus: React.FC<{
pictureInPicture: any;
onToggleHidden?: (status: any) => void;
onToggleMediaVisibility?: () => void;
ancestors?: number;
multiColumn?: boolean;
expanded: boolean;
}> = ({
status,
@@ -72,6 +74,8 @@ export const DetailedStatus: React.FC<{
pictureInPicture,
onToggleMediaVisibility,
onToggleHidden,
ancestors = 0,
multiColumn = false,
expanded,
}) => {
const properStatus = status?.get('reblog') ?? status;
@@ -136,6 +140,30 @@ export const DetailedStatus: React.FC<{
if (onTranslate) onTranslate(status);
}, [onTranslate, status]);
// The component is managed and will change if the status changes
// Ancestors can increase when loading a thread, in which case we want to scroll,
// or decrease if a post is deleted, in which case we don't want to mess with it
const previousAncestors = useRef(-1);
useEffect(() => {
if (nodeRef.current && previousAncestors.current < ancestors) {
nodeRef.current.scrollIntoView(true);
// In the single-column interface, `scrollIntoView` will put the post behind the header, so compensate for that.
if (!multiColumn) {
const offset = document
.querySelector('.column-header__wrapper')
?.getBoundingClientRect().bottom;
if (offset) {
const scrollingElement = document.scrollingElement ?? document.body;
scrollingElement.scrollBy(0, -offset);
}
}
}
previousAncestors.current = ancestors;
}, [ancestors, multiColumn]);
if (!properStatus) {
return null;
}

View File

@@ -162,7 +162,6 @@ class Status extends ImmutablePureComponent {
componentDidMount () {
attachFullscreenListener(this.onFullScreenChange);
this.props.dispatch(fetchStatus(this.props.params.statusId, { forceFetch: true }));
this._scrollStatusIntoView();
}
static getDerivedStateFromProps(props, state) {
@@ -512,35 +511,11 @@ class Status extends ImmutablePureComponent {
this.statusNode = c;
};
_scrollStatusIntoView () {
const { status, multiColumn } = this.props;
if (status) {
requestIdleCallback(() => {
this.statusNode?.scrollIntoView(true);
// In the single-column interface, `scrollIntoView` will put the post behind the header,
// so compensate for that.
if (!multiColumn) {
const offset = document.querySelector('.column-header__wrapper')?.getBoundingClientRect()?.bottom;
if (offset) {
const scrollingElement = document.scrollingElement || document.body;
scrollingElement.scrollBy(0, -offset);
}
}
});
}
}
componentDidUpdate (prevProps) {
const { status, ancestorsIds, descendantsIds } = this.props;
const { status, descendantsIds } = this.props;
const isSameStatus = status && (prevProps.status?.get('id') === status.get('id'));
if (status && (ancestorsIds.length > prevProps.ancestorsIds.length || !isSameStatus)) {
this._scrollStatusIntoView();
}
// Only highlight replies after the initial load
if (prevProps.descendantsIds.length && isSameStatus) {
const newRepliesIds = difference(descendantsIds, prevProps.descendantsIds);
@@ -653,6 +628,8 @@ class Status extends ImmutablePureComponent {
showMedia={this.state.showMedia}
onToggleMediaVisibility={this.handleToggleMediaVisibility}
pictureInPicture={pictureInPicture}
ancestors={this.props.ancestorsIds.length}
multiColumn={multiColumn}
/>
<ActionBar

View File

@@ -9,7 +9,6 @@ import { me, reduceMotion } from 'flavours/glitch/initial_state';
import ready from 'flavours/glitch/ready';
import { store } from 'flavours/glitch/store';
import { initializeEmoji } from './features/emoji';
import { isProduction, isDevelopment } from './utils/environment';
function main() {
@@ -30,6 +29,7 @@ function main() {
});
}
const { initializeEmoji } = await import('./features/emoji/index');
initializeEmoji();
const root = createRoot(mountNode);

View File

@@ -418,8 +418,8 @@ export const composeReducer = (state = initialState, action) => {
const isDirect = state.get('privacy') === 'direct';
return state
.set('quoted_status_id', isDirect ? null : status.get('id'))
.set('spoiler', status.get('sensitive'))
.set('spoiler_text', status.get('spoiler_text'))
.update('spoiler', spoiler => (spoiler) || !!status.get('spoiler_text'))
.update('spoiler_text', (spoiler_text) => spoiler_text || status.get('spoiler_text'))
.update('privacy', (visibility) => {
if (['public', 'unlisted'].includes(visibility) && status.get('visibility') === 'private') {
return 'private';

View File

@@ -327,9 +327,9 @@ $content-width: 840px;
font-weight: 700;
color: $primary-text-color;
text-transform: none;
padding-bottom: 0;
padding-top: 0;
margin-bottom: 0;
border-bottom: 0;
border-top: 0;
.comment {
display: block;

View File

@@ -180,25 +180,24 @@ export function useHotkeys<T extends HTMLElement>(handlers: HandlerMap) {
if (shouldHandleEvent) {
const matchCandidates: {
handler: (event: KeyboardEvent) => void;
// A candidate will be have an undefined handler if it's matched,
// but handled in a parent component rather than this one.
handler: ((event: KeyboardEvent) => void) | undefined;
priority: number;
}[] = [];
(Object.keys(hotkeyMatcherMap) as HotkeyName[]).forEach(
(handlerName) => {
const handler = handlersRef.current[handlerName];
const hotkeyMatcher = hotkeyMatcherMap[handlerName];
if (handler) {
const hotkeyMatcher = hotkeyMatcherMap[handlerName];
const { isMatch, priority } = hotkeyMatcher(
event,
bufferedKeys.current,
);
const { isMatch, priority } = hotkeyMatcher(
event,
bufferedKeys.current,
);
if (isMatch) {
matchCandidates.push({ handler, priority });
}
if (isMatch) {
matchCandidates.push({ handler, priority });
}
},
);

View File

@@ -553,7 +553,6 @@ class Status extends ImmutablePureComponent {
}
const {statusContentProps, hashtagBar} = getHashtagBarForStatus(status);
return (
<Hotkeys handlers={handlers} focusable={!unfocusable}>
<div className={classNames('status__wrapper', `status__wrapper-${status.get('visibility')}`, { 'status__wrapper-reply': !!status.get('in_reply_to_id'), unread, focusable: !this.props.muted })} tabIndex={this.props.muted || unfocusable ? null : 0} data-featured={featured ? 'true' : null} aria-label={textForScreenReader({intl, status, rebloggedByText, isQuote: isQuotedPost})} ref={this.handleRef} data-nosnippet={status.getIn(['account', 'noindex'], true) || undefined}>

View File

@@ -64,6 +64,7 @@ const StandaloneBoostButton: FC<ReblogButtonProps> = ({ status, counters }) => {
title={intl.formatMessage(meta ?? title)}
icon='retweet'
iconComponent={iconComponent}
className='status__action-bar__button'
onClick={!disabled ? handleClick : undefined}
counter={
counters
@@ -195,6 +196,7 @@ const BoostOrQuoteMenu: FC<ReblogButtonProps> = ({ status, counters }) => {
isMenuDisabled ? messages.all_disabled : messages.reblog_or_quote,
)}
icon='retweet'
className='status__action-bar__button'
iconComponent={boostIcon}
counter={
counters

View File

@@ -38,7 +38,7 @@ export const HandledLink: FC<HandledLinkProps & ComponentProps<'a'>> = ({
return (
<Link
className={classNames('mention hashtag', className)}
to={`/tags/${hashtag}`}
to={`/tags/${encodeURIComponent(hashtag)}`}
rel='tag'
data-menu-hashtag={hashtagAccountId}
>
@@ -71,7 +71,7 @@ export const HandledLink: FC<HandledLinkProps & ComponentProps<'a'>> = ({
return (
<a
{...props}
href={encodeURI(href)}
href={href}
title={href}
className={classNames('unhandled-link', className)}
target='_blank'

View File

@@ -406,15 +406,19 @@ class StatusActionBar extends ImmutablePureComponent {
status={status}
needsStatusRefresh={quickBoosting && status.get('quote_approval') === null}
items={menu}
icon='ellipsis-h'
iconComponent={MoreHorizIcon}
direction='right'
title={intl.formatMessage(messages.more)}
onOpen={() => {
dismissQuoteHint();
return true;
}}
/>
>
<IconButton
className='status__action-bar__button'
icon='ellipsis-h'
iconComponent={MoreHorizIcon}
title={intl.formatMessage(messages.more)}
/>
</Dropdown>
)}
</RemoveQuoteHint>
</div>

View File

@@ -104,17 +104,19 @@ export const RulesSection: FC<RulesSectionProps> = ({ isLoading = false }) => {
defaultMessage='Language'
/>
</label>
<select onChange={handleLocaleChange} id='language-select'>
{localeOptions.map((option) => (
<option
key={option.value}
value={option.value}
selected={option.value === selectedLocale}
>
{option.text}
</option>
))}
</select>
<div className='select-wrapper'>
<select onChange={handleLocaleChange} id='language-select'>
{localeOptions.map((option) => (
<option
key={option.value}
value={option.value}
selected={option.value === selectedLocale}
>
{option.text}
</option>
))}
</select>
</div>
</div>
)}
</Section>

View File

@@ -24,12 +24,12 @@ export default class FollowRequestNote extends ImmutablePureComponent {
</div>
<div className='follow-request-banner__action'>
<button type='button' className='button button-tertiary button--confirmation' onClick={onAuthorize}>
<button type='button' className='button button-secondary button--confirmation' onClick={onAuthorize}>
<Icon id='check' icon={CheckIcon} />
<FormattedMessage id='follow_request.authorize' defaultMessage='Authorize' />
</button>
<button type='button' className='button button-tertiary button--destructive' onClick={onReject}>
<button type='button' className='button button-secondary button--destructive' onClick={onReject}>
<Icon id='times' icon={CloseIcon} />
<FormattedMessage id='follow_request.reject' defaultMessage='Reject' />
</button>

View File

@@ -1,38 +0,0 @@
import PropTypes from 'prop-types';
import { PureComponent } from 'react';
const iconStyle = {
height: null,
lineHeight: '27px',
minWidth: `${18 * 1.28571429}px`,
};
export default class TextIconButton extends PureComponent {
static propTypes = {
label: PropTypes.string.isRequired,
title: PropTypes.string,
active: PropTypes.bool,
onClick: PropTypes.func.isRequired,
ariaControls: PropTypes.string,
};
render () {
const { label, title, active, ariaControls } = this.props;
return (
<button
type='button'
title={title}
aria-label={title}
className={`text-icon-button ${active ? 'active' : ''}`}
aria-expanded={active}
onClick={this.props.onClick}
aria-controls={ariaControls} style={iconStyle}
>
{label}
</button>
);
}
}

View File

@@ -13,6 +13,7 @@ import {
import { pasteLinkCompose } from 'mastodon/actions/compose_typed';
import { openModal } from 'mastodon/actions/modal';
import { PRIVATE_QUOTE_MODAL_ID } from 'mastodon/features/ui/components/confirmation_modals/private_quote_notify';
import { me } from 'mastodon/initial_state';
import ComposeForm from '../components/compose_form';
@@ -53,6 +54,7 @@ const mapStateToProps = state => ({
quoteToPrivate:
!!state.getIn(['compose', 'quoted_status_id'])
&& state.getIn(['compose', 'privacy']) === 'private'
&& state.getIn(['statuses', state.getIn(['compose', 'quoted_status_id']), 'account']) !== me
&& !state.getIn(['settings', 'dismissed_banners', PRIVATE_QUOTE_MODAL_ID]),
isInReply: state.getIn(['compose', 'in_reply_to']) !== null,
lang: state.getIn(['compose', 'language']),

View File

@@ -166,7 +166,7 @@ const Compose: React.FC<{ multiColumn: boolean }> = ({ multiColumn }) => {
<div className='drawer__inner'>
<ComposeFormContainer />
<div className='drawer__inner__mastodon'>
<div className='drawer__inner__mastodon with-zig-zag-decoration'>
<img alt='' draggable='false' src={mascot ?? elephantUIPlane} />
</div>
</div>

View File

@@ -1,6 +1,7 @@
import { initialState } from '@/mastodon/initial_state';
import { toSupportedLocale } from './locale';
import type { LocaleOrCustom } from './types';
import { emojiLogger } from './utils';
// eslint-disable-next-line import/default -- Importing via worker loader.
import EmojiWorker from './worker?worker&inline';
@@ -24,19 +25,17 @@ export function initializeEmoji() {
}
if (worker) {
// Assign worker to const to make TS happy inside the event listener.
const thisWorker = worker;
const timeoutId = setTimeout(() => {
log('worker is not ready after timeout');
worker = null;
void fallbackLoad();
}, WORKER_TIMEOUT);
thisWorker.addEventListener('message', (event: MessageEvent<string>) => {
worker.addEventListener('message', (event: MessageEvent<string>) => {
const { data: message } = event;
if (message === 'ready') {
log('worker ready, loading data');
clearTimeout(timeoutId);
thisWorker.postMessage('custom');
messageWorker('custom');
void loadEmojiLocale(userLocale);
// Load English locale as well, because people are still used to
// using it from before we supported other locales.
@@ -55,20 +54,35 @@ export function initializeEmoji() {
async function fallbackLoad() {
log('falling back to main thread for loading');
const { importCustomEmojiData } = await import('./loader');
await importCustomEmojiData();
const emojis = await importCustomEmojiData();
if (emojis) {
log('loaded %d custom emojis', emojis.length);
}
await loadEmojiLocale(userLocale);
if (userLocale !== 'en') {
await loadEmojiLocale('en');
}
}
export async function loadEmojiLocale(localeString: string) {
async function loadEmojiLocale(localeString: string) {
const locale = toSupportedLocale(localeString);
const { importEmojiData, localeToPath } = await import('./loader');
if (worker) {
worker.postMessage(locale);
const path = await localeToPath(locale);
log('asking worker to load locale %s from %s', locale, path);
messageWorker(locale, path);
} else {
const { importEmojiData } = await import('./loader');
await importEmojiData(locale);
const emojis = await importEmojiData(locale);
if (emojis) {
log('loaded %d emojis to locale %s', emojis.length, locale);
}
}
}
function messageWorker(locale: LocaleOrCustom, path?: string) {
if (!worker) {
return;
}
worker.postMessage({ locale, path });
}

View File

@@ -8,44 +8,64 @@ import {
putLatestEtag,
} from './database';
import { toSupportedLocale, toSupportedLocaleOrCustom } from './locale';
import type { CustomEmojiData, LocaleOrCustom } from './types';
import { emojiLogger } from './utils';
import type { CustomEmojiData } from './types';
const log = emojiLogger('loader');
export async function importEmojiData(localeString: string) {
export async function importEmojiData(localeString: string, path?: string) {
const locale = toSupportedLocale(localeString);
const emojis = await fetchAndCheckEtag<CompactEmoji[]>(locale);
// Validate the provided path.
if (path && !/^[/a-z]*\/packs\/assets\/compact-\w+\.json$/.test(path)) {
throw new Error('Invalid path for emoji data');
} else {
// Otherwise get the path if not provided.
path ??= await localeToPath(locale);
}
const emojis = await fetchAndCheckEtag<CompactEmoji[]>(locale, path);
if (!emojis) {
return;
}
const flattenedEmojis: FlatCompactEmoji[] = flattenEmojiData(emojis);
log('loaded %d for %s locale', flattenedEmojis.length, locale);
await putEmojiData(flattenedEmojis, locale);
return flattenedEmojis;
}
export async function importCustomEmojiData() {
const emojis = await fetchAndCheckEtag<CustomEmojiData[]>('custom');
const emojis = await fetchAndCheckEtag<CustomEmojiData[]>(
'custom',
'/api/v1/custom_emojis',
);
if (!emojis) {
return;
}
log('loaded %d custom emojis', emojis.length);
await putCustomEmojiData(emojis);
return emojis;
}
async function fetchAndCheckEtag<ResultType extends object[]>(
localeOrCustom: LocaleOrCustom,
const modules = import.meta.glob<string>(
'../../../../../node_modules/emojibase-data/**/compact.json',
{
query: '?url',
import: 'default',
},
);
export function localeToPath(locale: Locale) {
const key = `../../../../../node_modules/emojibase-data/${locale}/compact.json`;
if (!modules[key] || typeof modules[key] !== 'function') {
throw new Error(`Unsupported locale: ${locale}`);
}
return modules[key]();
}
export async function fetchAndCheckEtag<ResultType extends object[]>(
localeString: string,
path: string,
): Promise<ResultType | null> {
const locale = toSupportedLocaleOrCustom(localeOrCustom);
const locale = toSupportedLocaleOrCustom(localeString);
// Use location.origin as this script may be loaded from a CDN domain.
const url = new URL(location.origin);
if (locale === 'custom') {
url.pathname = '/api/v1/custom_emojis';
} else {
const modulePath = await localeToPath(locale);
url.pathname = modulePath;
}
const url = new URL(path, location.origin);
const oldEtag = await loadLatestEtag(locale);
const response = await fetch(url, {
@@ -60,38 +80,20 @@ async function fetchAndCheckEtag<ResultType extends object[]>(
}
if (!response.ok) {
throw new Error(
`Failed to fetch emoji data for ${localeOrCustom}: ${response.statusText}`,
`Failed to fetch emoji data for ${locale}: ${response.statusText}`,
);
}
const data = (await response.json()) as ResultType;
if (!Array.isArray(data)) {
throw new Error(
`Unexpected data format for ${localeOrCustom}: expected an array`,
);
throw new Error(`Unexpected data format for ${locale}: expected an array`);
}
// Store the ETag for future requests
const etag = response.headers.get('ETag');
if (etag) {
await putLatestEtag(etag, localeOrCustom);
await putLatestEtag(etag, localeString);
}
return data;
}
const modules = import.meta.glob<string>(
'../../../../../node_modules/emojibase-data/**/compact.json',
{
query: '?url',
import: 'default',
},
);
function localeToPath(locale: Locale) {
const key = `../../../../../node_modules/emojibase-data/${locale}/compact.json`;
if (!modules[key] || typeof modules[key] !== 'function') {
throw new Error(`Unsupported locale: ${locale}`);
}
return modules[key]();
}

View File

@@ -162,7 +162,7 @@ describe('loadEmojiDataToState', () => {
const dbCall = vi
.spyOn(db, 'loadEmojiByHexcode')
.mockRejectedValue(new db.LocaleNotLoadedError('en'));
vi.spyOn(loader, 'importEmojiData').mockResolvedValueOnce();
vi.spyOn(loader, 'importEmojiData').mockResolvedValueOnce(undefined);
const consoleCall = vi
.spyOn(console, 'warn')
.mockImplementationOnce(() => null);

View File

@@ -1,18 +1,25 @@
import { importEmojiData, importCustomEmojiData } from './loader';
import { importCustomEmojiData, importEmojiData } from './loader';
addEventListener('message', handleMessage);
self.postMessage('ready'); // After the worker is ready, notify the main thread
function handleMessage(event: MessageEvent<string>) {
const { data: locale } = event;
void loadData(locale);
function handleMessage(event: MessageEvent<{ locale: string; path?: string }>) {
const {
data: { locale, path },
} = event;
void loadData(locale, path);
}
async function loadData(locale: string) {
if (locale !== 'custom') {
await importEmojiData(locale);
async function loadData(locale: string, path?: string) {
let importCount: number | undefined;
if (locale === 'custom') {
importCount = (await importCustomEmojiData())?.length;
} else if (path) {
importCount = (await importEmojiData(locale, path))?.length;
} else {
await importCustomEmojiData();
throw new Error('Path is required for loading locale emoji data');
}
if (importCount) {
self.postMessage(`loaded ${importCount} emojis into ${locale}`);
}
self.postMessage(`loaded ${locale}`);
}

View File

@@ -75,7 +75,7 @@ export const DisabledAccountBanner: React.FC = () => {
</a>
<button
type='button'
className='button button--block button-tertiary'
className='button button--block button-secondary'
onClick={handleLogOutClick}
>
<FormattedMessage

View File

@@ -46,7 +46,7 @@ export const SignInBanner: React.FC = () => {
<a
href={sso_redirect}
data-method='post'
className='button button--block button-tertiary'
className='button button--block button-secondary'
>
<FormattedMessage
id='sign_in_banner.sso_redirect'
@@ -98,7 +98,7 @@ export const SignInBanner: React.FC = () => {
/>
</p>
{signupButton}
<a href='/auth/sign_in' className='button button--block button-tertiary'>
<a href='/auth/sign_in' className='button button--block button-secondary'>
<FormattedMessage id='sign_in_banner.sign_in' defaultMessage='Login' />
</a>
</div>

View File

@@ -4,7 +4,7 @@
@typescript-eslint/no-unsafe-assignment */
import type { CSSProperties } from 'react';
import { useState, useRef, useCallback } from 'react';
import { useState, useRef, useCallback, useEffect } from 'react';
import { FormattedMessage } from 'react-intl';
@@ -55,6 +55,8 @@ export const DetailedStatus: React.FC<{
pictureInPicture: any;
onToggleHidden?: (status: any) => void;
onToggleMediaVisibility?: () => void;
ancestors?: number;
multiColumn?: boolean;
}> = ({
status,
onOpenMedia,
@@ -69,6 +71,8 @@ export const DetailedStatus: React.FC<{
pictureInPicture,
onToggleMediaVisibility,
onToggleHidden,
ancestors = 0,
multiColumn = false,
}) => {
const properStatus = status?.get('reblog') ?? status;
const [height, setHeight] = useState(0);
@@ -123,6 +127,30 @@ export const DetailedStatus: React.FC<{
if (onTranslate) onTranslate(status);
}, [onTranslate, status]);
// The component is managed and will change if the status changes
// Ancestors can increase when loading a thread, in which case we want to scroll,
// or decrease if a post is deleted, in which case we don't want to mess with it
const previousAncestors = useRef(-1);
useEffect(() => {
if (nodeRef.current && previousAncestors.current < ancestors) {
nodeRef.current.scrollIntoView(true);
// In the single-column interface, `scrollIntoView` will put the post behind the header, so compensate for that.
if (!multiColumn) {
const offset = document
.querySelector('.column-header__wrapper')
?.getBoundingClientRect().bottom;
if (offset) {
const scrollingElement = document.scrollingElement ?? document.body;
scrollingElement.scrollBy(0, -offset);
}
}
}
previousAncestors.current = ancestors;
}, [ancestors, multiColumn]);
if (!properStatus) {
return null;
}

View File

@@ -164,8 +164,6 @@ class Status extends ImmutablePureComponent {
componentDidMount () {
attachFullscreenListener(this.onFullScreenChange);
this._scrollStatusIntoView();
}
UNSAFE_componentWillReceiveProps (nextProps) {
@@ -487,35 +485,11 @@ class Status extends ImmutablePureComponent {
this.statusNode = c;
};
_scrollStatusIntoView () {
const { status, multiColumn } = this.props;
if (status) {
requestIdleCallback(() => {
this.statusNode?.scrollIntoView(true);
// In the single-column interface, `scrollIntoView` will put the post behind the header,
// so compensate for that.
if (!multiColumn) {
const offset = document.querySelector('.column-header__wrapper')?.getBoundingClientRect()?.bottom;
if (offset) {
const scrollingElement = document.scrollingElement || document.body;
scrollingElement.scrollBy(0, -offset);
}
}
});
}
}
componentDidUpdate (prevProps) {
const { status, ancestorsIds, descendantsIds } = this.props;
const { status, descendantsIds } = this.props;
const isSameStatus = status && (prevProps.status?.get('id') === status.get('id'));
if (status && (ancestorsIds.length > prevProps.ancestorsIds.length || !isSameStatus)) {
this._scrollStatusIntoView();
}
// Only highlight replies after the initial load
if (prevProps.descendantsIds.length && isSameStatus) {
const newRepliesIds = difference(descendantsIds, prevProps.descendantsIds);
@@ -619,6 +593,8 @@ class Status extends ImmutablePureComponent {
showMedia={this.state.showMedia}
onToggleMediaVisibility={this.handleToggleMediaVisibility}
pictureInPicture={pictureInPicture}
ancestors={this.props.ancestorsIds.length}
multiColumn={multiColumn}
/>
<ActionBar

View File

@@ -98,7 +98,7 @@ class BundleColumnError extends PureComponent {
<div className='error-column__message__actions'>
{errorType === 'network' && <Button onClick={this.handleRetry}><FormattedMessage id='bundle_column_error.retry' defaultMessage='Try again' /></Button>}
{errorType === 'error' && <CopyButton value={stacktrace}><FormattedMessage id='bundle_column_error.copy_stacktrace' defaultMessage='Copy error report' /></CopyButton>}
<Link to='/' className={classNames('button', { 'button-tertiary': errorType !== 'routing' })}><FormattedMessage id='bundle_column_error.return' defaultMessage='Go back home' /></Link>
<Link to='/' className={classNames('button', { 'button-secondary': errorType !== 'routing' })}><FormattedMessage id='bundle_column_error.return' defaultMessage='Go back home' /></Link>
</div>
</div>
</div>

View File

@@ -46,7 +46,7 @@ export const ModalPlaceholder: React.FC<{
defaultMessage='Try again'
/>
</Button>
<Button onClick={handleClose} className='button button-tertiary'>
<Button onClick={handleClose} className='button button-secondary'>
<FormattedMessage
id='bundle_modal_error.close'
defaultMessage='Close'

View File

@@ -104,7 +104,7 @@ const LoginOrSignUp: React.FC = () => {
<a
href={sso_redirect}
data-method='post'
className='button button--block button-tertiary'
className='button button--block button-secondary'
>
<FormattedMessage
id='sign_in_banner.sso_redirect'
@@ -143,7 +143,7 @@ const LoginOrSignUp: React.FC = () => {
return (
<div className='ui__navigation-bar__sign-up'>
{signupButton}
<a href='/auth/sign_in' className='button button-tertiary'>
<a href='/auth/sign_in' className='button button-secondary'>
<FormattedMessage
id='sign_in_banner.sign_in'
defaultMessage='Login'

View File

@@ -903,6 +903,7 @@
"status.edited_x_times": "Upraveno {count, plural, one {{count}krát} few {{count}krát} many {{count}krát} other {{count}krát}}",
"status.embed": "Získejte kód pro vložení",
"status.favourite": "Oblíbit",
"status.favourites_count": "{count, plural, one {{counter} oblíbený} few {{counter} oblíbené} many {{counter} oblíbených} other {{counter} oblíbených}}",
"status.filter": "Filtrovat tento příspěvek",
"status.history.created": "Uživatel {name} vytvořil {date}",
"status.history.edited": "Uživatel {name} upravil {date}",
@@ -937,12 +938,14 @@
"status.quotes.empty": "Tento příspěvek zatím nikdo necitoval. Pokud tak někdo učiní, uvidíte to zde.",
"status.quotes.local_other_disclaimer": "Citace zamítnuté autorem nebudou zobrazeny.",
"status.quotes.remote_other_disclaimer": "Pouze citace z {domain} zde budou zaručeně ukázány. Citace zamítnuté autorem nebudou zobrazeny.",
"status.quotes_count": "{count, plural, one {{counter} citace} few {{counter} citace} many {{counter} citací} other {{counter} citací}}",
"status.read_more": "Číst více",
"status.reblog": "Boostnout",
"status.reblog_or_quote": "Boostnout nebo citovat",
"status.reblog_private": "Sdílejte znovu se svými sledujícími",
"status.reblogged_by": "Uživatel {name} boostnul",
"status.reblogs.empty": "Tento příspěvek ještě nikdo neboostnul. Pokud to někdo udělá, zobrazí se zde.",
"status.reblogs_count": "{count, plural, one {{counter} boost} few {{counter} boosty} many {{counter} boostů} other {{counter} boostů}}",
"status.redraft": "Smazat a přepsat",
"status.remove_bookmark": "Odstranit ze záložek",
"status.remove_favourite": "Odebrat z oblíbených",

View File

@@ -903,6 +903,7 @@
"status.edited_x_times": "Golygwyd {count, plural, one {{count} gwaith} other {{count} gwaith}}",
"status.embed": "Cael y cod mewnblannu",
"status.favourite": "Ffafrio",
"status.favourites_count": "{count, plural, one {{counter} ffefryn} other {{counter} ffefryn}}",
"status.filter": "Hidlo'r postiad hwn",
"status.history.created": "Crëwyd gan {name} {date}",
"status.history.edited": "Golygwyd gan {name} {date}",
@@ -937,12 +938,14 @@
"status.quotes.empty": "Does neb wedi dyfynnu'r postiad hwn eto. Pan fydd rhywun yn gwneud hynny, bydd yn ymddangos yma.",
"status.quotes.local_other_disclaimer": "Bydd dyfyniadau wedi'u gwrthod gan yr awdur ddim yn cael eu dangos.",
"status.quotes.remote_other_disclaimer": "Dim ond dyfyniadau o {domain} sy'n siŵr o gael eu dangos yma. Bydd dyfyniadau wedi'u gwrthod gan yr awdur ddim yn cael eu dangos.",
"status.quotes_count": "{count, plural, one {{counter} dyfyniad} other {{counter} dyfyniad}}",
"status.read_more": "Darllen rhagor",
"status.reblog": "Hybu",
"status.reblog_or_quote": "Hybu neu ddyfynnu",
"status.reblog_private": "Rhannwch eto gyda'ch dilynwyr",
"status.reblogged_by": "Hybodd {name}",
"status.reblogs.empty": "Does neb wedi hybio'r post yma eto. Pan y bydd rhywun yn gwneud, byddent yn ymddangos yma.",
"status.reblogs_count": "{count, plural, one {{counter} hwb} other {{counter} hwb}}",
"status.redraft": "Dileu ac ail lunio",
"status.remove_bookmark": "Tynnu nod tudalen",
"status.remove_favourite": "Tynnu o'r ffefrynnau",

View File

@@ -231,7 +231,7 @@
"confirmations.delete_list.title": "Slet liste?",
"confirmations.discard_draft.confirm": "Kassér og fortsæt",
"confirmations.discard_draft.edit.cancel": "Fortsæt redigering",
"confirmations.discard_draft.edit.message": "Hvis du fortsætter, kasseres alle ændringer, du har foretaget i det indlæg, du er i gang med at redigere.",
"confirmations.discard_draft.edit.message": "Hvis du fortsætter, vil alle ændringer, du har foretaget i det indlæg, du er er ved at redigere, blive slettet.",
"confirmations.discard_draft.edit.title": "Kassér ændringer til dit indlæg?",
"confirmations.discard_draft.post.cancel": "Genoptag udkast",
"confirmations.discard_draft.post.message": "Hvis du fortsætter, kasseres det indlæg, du er i gang med at udforme.",
@@ -507,7 +507,7 @@
"keyboard_shortcuts.pinned": "Åbn liste over fastgjorte indlæg",
"keyboard_shortcuts.profile": "Åbn forfatters profil",
"keyboard_shortcuts.quote": "Citér indlæg",
"keyboard_shortcuts.reply": "Besvar indlægget",
"keyboard_shortcuts.reply": "Besvar indlæg",
"keyboard_shortcuts.requests": "Åbn liste over følgeanmodninger",
"keyboard_shortcuts.search": "Fokusér søgebjælke",
"keyboard_shortcuts.spoilers": "Vis/skjul indholdsadvarsel-felt",
@@ -675,7 +675,7 @@
"notifications.column_settings.filter_bar.category": "Hurtigfiltreringsbjælke",
"notifications.column_settings.follow": "Nye følgere:",
"notifications.column_settings.follow_request": "Nye følgeanmodninger:",
"notifications.column_settings.group": "Gruppere",
"notifications.column_settings.group": "Gruppér",
"notifications.column_settings.mention": "Omtaler:",
"notifications.column_settings.poll": "Afstemningsresultater:",
"notifications.column_settings.push": "Push-notifikationer",
@@ -764,7 +764,7 @@
"privacy_policy.last_updated": "Senest opdateret {date}",
"privacy_policy.title": "Privatlivspolitik",
"quote_error.edit": "Citater kan ikke tilføjes ved redigering af et indlæg.",
"quote_error.poll": "Citering ikke tilladt i afstemninger.",
"quote_error.poll": "Citering er ikke tilladt med afstemninger.",
"quote_error.private_mentions": "Citering er ikke tilladt med direkte omtaler.",
"quote_error.quote": "Kun ét citat ad gangen er tilladt.",
"quote_error.unauthorized": "Du har ikke tilladelse til at citere dette indlæg.",
@@ -867,7 +867,7 @@
"search_results.title": "Søg efter \"{q}\"",
"server_banner.about_active_users": "Personer, som brugte denne server de seneste 30 dage (månedlige aktive brugere)",
"server_banner.active_users": "aktive brugere",
"server_banner.administered_by": "Håndteres af:",
"server_banner.administered_by": "Administreret af:",
"server_banner.is_one_of_many": "{domain} er en af de mange uafhængige Mastodon-servere, du kan bruge for at deltage i fediverset.",
"server_banner.server_stats": "Serverstatstik:",
"sign_in_banner.create_account": "Opret konto",
@@ -902,7 +902,7 @@
"status.edited": "Senest redigeret {date}",
"status.edited_x_times": "Redigeret {count, plural, one {{count} gang} other {{count} gange}}",
"status.embed": "Hent indlejringskode",
"status.favourite": "Favorit",
"status.favourite": "Favoritmarkér",
"status.favourites_count": "{count, plural, one {{counter} favorit} other {{counter} favoritter}}",
"status.filter": "Filtrér dette indlæg",
"status.history.created": "{name} oprettet {date}",
@@ -991,7 +991,7 @@
"units.short.million": "{count} mio.",
"units.short.thousand": "{count} tusind",
"upload_area.title": "Træk og slip for at uploade",
"upload_button.label": "Tilføj billed-, video- eller lydfil(er)",
"upload_button.label": "Tilføj billeder, en video- eller lydfil",
"upload_error.limit": "Grænse for filupload nået.",
"upload_error.poll": "Filupload ikke tilladt for afstemninger.",
"upload_error.quote": "Fil-upload ikke tilladt i citater.",

View File

@@ -242,7 +242,7 @@
"confirmations.follow_to_list.message": "Du musst {name} folgen, um das Profil zu einer Liste hinzufügen zu können.",
"confirmations.follow_to_list.title": "Profil folgen?",
"confirmations.logout.confirm": "Abmelden",
"confirmations.logout.message": "Möchtest du dich wirklich abmelden?",
"confirmations.logout.message": "Bist du sicher, dass du dich abmelden möchtest?",
"confirmations.logout.title": "Abmelden?",
"confirmations.missing_alt_text.confirm": "Bildbeschreibung hinzufügen",
"confirmations.missing_alt_text.message": "Dein Beitrag enthält Medien ohne Bildbeschreibung. Mit ALT-Texten erreichst Du auch Menschen, die blind oder sehbehindert sind.",
@@ -304,12 +304,12 @@
"domain_pill.activitypub_lets_connect": "Somit kannst du dich nicht nur auf Mastodon mit Leuten verbinden und mit ihnen interagieren, sondern über alle sozialen Apps hinweg.",
"domain_pill.activitypub_like_language": "ActivityPub ist sozusagen die Sprache, die Mastodon mit anderen sozialen Netzwerken spricht.",
"domain_pill.server": "Server",
"domain_pill.their_handle": "Deren Adresse:",
"domain_pill.their_server": "Deren digitale Heimat. Hier „leben“ alle Beiträge von diesem Profil.",
"domain_pill.their_username": "Deren eindeutigen Identität auf dem betreffenden Server. Es ist möglich, Profile mit dem gleichen Profilnamen auf verschiedenen Servern zu finden.",
"domain_pill.their_handle": "Die vollständige Adresse:",
"domain_pill.their_server": "Die digitale Heimat, in der sich alle Beiträge dieses Profils befinden.",
"domain_pill.their_username": "Die eindeutige Identifizierung auf einem Server. Es ist möglich, denselben Profilnamen auf verschiedenen Servern im Fediverse zu finden.",
"domain_pill.username": "Profilname",
"domain_pill.whats_in_a_handle": "Woraus besteht eine Adresse?",
"domain_pill.who_they_are": "Adressen teilen mit, wer jemand ist und wo sich jemand aufhält. Daher kannst du mit Leuten im gesamten Social Web interagieren, wenn es eine durch <button>ActivityPub angetriebene Plattform</button> ist.",
"domain_pill.who_they_are": "Adressen teilen mit, wer jemand ist und wo sich jemand im Fediverse aufhält. Daher kannst du mit Leuten im gesamten Social Web interagieren, wenn es eine durch <button>ActivityPub angetriebene Plattform</button> ist.",
"domain_pill.who_you_are": "Deine Adresse teilt mit, wer du bist und wo du dich aufhältst. Daher können andere Leute im gesamten Social Web mit dir interagieren, wenn es eine durch <button>ActivityPub angetriebene Plattform</button> ist.",
"domain_pill.your_handle": "Deine Adresse:",
"domain_pill.your_server": "Deine digitale Heimat. Hier „leben“ alle Beiträge von dir. Falls es dir hier nicht gefällt, kannst du jederzeit den Server wechseln und ebenso deine Follower übertragen.",
@@ -584,8 +584,8 @@
"navigation_bar.follows_and_followers": "Follower und Folge ich",
"navigation_bar.import_export": "Importieren und exportieren",
"navigation_bar.lists": "Listen",
"navigation_bar.live_feed_local": "Live-Feed (lokal)",
"navigation_bar.live_feed_public": "Live-Feed (öffentlich)",
"navigation_bar.live_feed_local": "Live-Feed (Dieser Server)",
"navigation_bar.live_feed_public": "Live-Feed (Alle Server)",
"navigation_bar.logout": "Abmelden",
"navigation_bar.moderation": "Moderation",
"navigation_bar.more": "Mehr",
@@ -693,7 +693,7 @@
"notifications.filter.follows": "Folgt",
"notifications.filter.mentions": "Erwähnungen",
"notifications.filter.polls": "Umfrageergebnisse",
"notifications.filter.statuses": "Neue Beiträge von Personen, denen du folgst",
"notifications.filter.statuses": "Neue Beiträge von Profilen, denen du folgst",
"notifications.grant_permission": "Berechtigung erteilen.",
"notifications.group": "{count} Benachrichtigungen",
"notifications.mark_as_read": "Alle Benachrichtigungen als gelesen markieren",
@@ -903,7 +903,7 @@
"status.edited_x_times": "{count, plural, one {{count}-mal} other {{count}-mal}} bearbeitet",
"status.embed": "Code zum Einbetten",
"status.favourite": "Favorisieren",
"status.favourites_count": "{count, plural, one {{counter} Favorit} other {{counter} Favoriten}}",
"status.favourites_count": "{count, plural, one {{counter} Mal favorisiert} other {{counter} Mal favorisiert}}",
"status.filter": "Beitrag filtern",
"status.history.created": "{name} erstellte {date}",
"status.history.edited": "{name} bearbeitete {date}",
@@ -938,14 +938,14 @@
"status.quotes.empty": "Diesen Beitrag hat bisher noch niemand zitiert. Sobald es jemand tut, wird das Profil hier erscheinen.",
"status.quotes.local_other_disclaimer": "Durch Autor*in abgelehnte Zitate werden nicht angezeigt.",
"status.quotes.remote_other_disclaimer": "Nur Zitate von {domain} werden hier garantiert angezeigt. Durch Autor*in abgelehnte Zitate werden nicht angezeigt.",
"status.quotes_count": "{count, plural, one {{counter} zitierter Beitrag} other {{counter} zitierte Beiträge}}",
"status.quotes_count": "{count, plural, one {{counter} Mal zitiert} other {{counter} Mal zitiert}}",
"status.read_more": "Gesamten Beitrag anschauen",
"status.reblog": "Teilen",
"status.reblog_or_quote": "Teilen oder zitieren",
"status.reblog_private": "Erneut mit deinen Followern teilen",
"status.reblogged_by": "{name} teilte",
"status.reblogs.empty": "Diesen Beitrag hat bisher noch niemand geteilt. Sobald es jemand tut, wird das Profil hier erscheinen.",
"status.reblogs_count": "{count, plural, one {{counter} geteilter Beitrag} other {{counter} geteilte Beiträge}}",
"status.reblogs_count": "{count, plural, one {{counter} Mal geteilt} other {{counter} Mal geteilt}}",
"status.redraft": "Löschen und neu erstellen",
"status.remove_bookmark": "Lesezeichen entfernen",
"status.remove_favourite": "Aus Favoriten entfernen",

View File

@@ -903,6 +903,7 @@
"status.edited_x_times": "Muudetud {count, plural, one{{count} kord} other {{count} korda}}",
"status.embed": "Hangi manustamiskood",
"status.favourite": "Lemmik",
"status.favourites_count": "{count, plural, one {{counter} lemmik} other {{counter} lemmikut}}",
"status.filter": "Filtreeri seda postitust",
"status.history.created": "{name} lõi {date}",
"status.history.edited": "{name} muutis {date}",
@@ -937,12 +938,14 @@
"status.quotes.empty": "Keegi pole seda postitust veel tsiteerinud. Kui keegi seda teeb, siis on ta nähtav siin.",
"status.quotes.local_other_disclaimer": "Autori poolt tagasilükatud tsitaate ei kuvata.",
"status.quotes.remote_other_disclaimer": "Kui kasutaja on {domain} domeenist, siis siin on tagatud vaid tema tsitaatide näitamine. Autori poolt tagasilükatud tsitaate ei kuvata.",
"status.quotes_count": "{count, plural, one {{counter} tsiteerimine} other {{counter} tsiteerimist}}",
"status.read_more": "Loe veel",
"status.reblog": "Jaga",
"status.reblog_or_quote": "Anna hoogu või tsiteeri",
"status.reblog_private": "Jaga uuesti oma jälgijatele",
"status.reblogged_by": "{name} jagas",
"status.reblogs.empty": "Keegi pole seda postitust veel jaganud. Kui keegi seda teeb, siis on ta nähtav siin.",
"status.reblogs_count": "{count, plural, one {{counter} jagamine} other {{counter} jagamist}}",
"status.redraft": "Kustuta & alga uuesti",
"status.remove_bookmark": "Eemalda järjehoidja",
"status.remove_favourite": "Eemalda lemmikute seast",

View File

@@ -233,7 +233,7 @@
"confirmations.discard_draft.edit.cancel": "Palaa muokkaamaan",
"confirmations.discard_draft.edit.message": "Jatkaminen tuhoaa kaikki muutokset, joita olet tehnyt julkaisuun, jota olet parhaillaan muokkaamassa.",
"confirmations.discard_draft.edit.title": "Hylätäänkö luonnosjulkaisusi muutokset?",
"confirmations.discard_draft.post.cancel": "Palaa lunnokseen",
"confirmations.discard_draft.post.cancel": "Palaa luonnokseen",
"confirmations.discard_draft.post.message": "Jatkaminen tuhoaa julkaisun, jota olet parhaillaan laatimassa.",
"confirmations.discard_draft.post.title": "Hylätäänkö luonnosjulkaisusi?",
"confirmations.discard_edit_media.confirm": "Hylkää",
@@ -903,7 +903,7 @@
"status.edited_x_times": "Muokattu {count, plural, one {{count} kerran} other {{count} kertaa}}",
"status.embed": "Hanki upotuskoodi",
"status.favourite": "Suosikki",
"status.favourites_count": "{count, plural, one {{counter} suosikki} other {{counter} suoksikkia}}",
"status.favourites_count": "{count, plural, one {{counter} suosikki} other {{counter} suosikkia}}",
"status.filter": "Suodata tämä julkaisu",
"status.history.created": "{name} loi {date}",
"status.history.edited": "{name} muokkasi {date}",

View File

@@ -23,7 +23,7 @@
"account.blocked": "Bloqué·e",
"account.blocking": "Bloqué",
"account.cancel_follow_request": "Retirer cette demande d'abonnement",
"account.copy": "Copier le lien vers le profil",
"account.copy": "Copier le lien du profil",
"account.direct": "Mention privée @{name}",
"account.disable_notifications": "Ne plus me notifier quand @{name} publie",
"account.domain_blocking": "Domaine bloqué",
@@ -45,7 +45,7 @@
"account.follow_request": "Demande dabonnement",
"account.follow_request_cancel": "Annuler la demande",
"account.follow_request_cancel_short": "Annuler",
"account.follow_request_short": "Requête",
"account.follow_request_short": "Demander à suivre",
"account.followers": "abonné·e·s",
"account.followers.empty": "Personne ne suit ce compte pour l'instant.",
"account.followers_counter": "{count, plural, one {{counter} abonné·e} other {{counter} abonné·e·s}}",
@@ -528,7 +528,7 @@
"limited_account_hint.action": "Afficher le profil quand même",
"limited_account_hint.title": "Ce profil a été masqué par la modération de {domain}.",
"link_preview.author": "Par {name}",
"link_preview.more_from_author": "Plus via {name}",
"link_preview.more_from_author": "Voir plus de {name}",
"link_preview.shares": "{count, plural, one {{counter} message} other {{counter} messages}}",
"lists.add_member": "Ajouter",
"lists.add_to_list": "Ajouter à la liste",
@@ -759,7 +759,7 @@
"privacy.quote.disabled": "{visibility}, citations désactivées",
"privacy.quote.limited": "{visibility}, citations limitées",
"privacy.unlisted.additional": "Se comporte exactement comme « public », sauf que le message n'apparaîtra pas dans les flux en direct, les hashtags, explorer ou la recherche Mastodon, même si vous les avez activé au niveau de votre compte.",
"privacy.unlisted.long": "Caché des résultats de recherche de Mastodon, aux tendances et aux échéanciers publics",
"privacy.unlisted.long": "Caché des résultats de recherche de Mastodon, des tendances et des fils publics",
"privacy.unlisted.short": "Public discret",
"privacy_policy.last_updated": "Dernière mise à jour {date}",
"privacy_policy.title": "Politique de confidentialité",
@@ -903,6 +903,7 @@
"status.edited_x_times": "Modifiée {count, plural, one {{count} fois} other {{count} fois}}",
"status.embed": "Obtenir le code d'intégration",
"status.favourite": "Ajouter aux favoris",
"status.favourites_count": "{count, plural, one {{counter} favori} other {{counter} favoris}}",
"status.filter": "Filtrer cette publication",
"status.history.created": "créé par {name} {date}",
"status.history.edited": "modifié par {name} {date}",
@@ -937,12 +938,14 @@
"status.quotes.empty": "Personne n'a encore cité ce message. Quand quelqu'un le fera, il apparaîtra ici.",
"status.quotes.local_other_disclaimer": "Les citations rejetées par l'auteur ne seront pas affichées.",
"status.quotes.remote_other_disclaimer": "Seules les citations de {domain} sont garanties d'être affichées ici. Les citations rejetées par l'auteur ne seront pas affichées.",
"status.quotes_count": "{count, plural, one {{counter} citation} other {{counter} citations}}",
"status.read_more": "En savoir plus",
"status.reblog": "Booster",
"status.reblog_or_quote": "Boost ou citation",
"status.reblog_private": "Partagez à nouveau avec vos abonnés",
"status.reblogged_by": "{name} a boosté",
"status.reblogs.empty": "Personne na encore boosté cette publication. Lorsque quelquun le fera, elle apparaîtra ici.",
"status.reblogs_count": "{count, plural, one {{counter} partage} other {{counter} partages}}",
"status.redraft": "Supprimer et réécrire",
"status.remove_bookmark": "Retirer des signets",
"status.remove_favourite": "Retirer des favoris",
@@ -1023,8 +1026,8 @@
"visibility_modal.helper.privacy_editing": "La visibilité ne peut pas être modifiée après la publication d'un message.",
"visibility_modal.helper.privacy_private_self_quote": "Les auto-citations de messages privés ne peuvent pas être rendues publiques.",
"visibility_modal.helper.private_quoting": "Les posts accessible uniquement par les followers sur Mastodon ne peuvent être cités par d'autres personnes.",
"visibility_modal.helper.unlisted_quoting": "Lorsque les gens vous citent, leur message sera également caché dans les calendriers tendances.",
"visibility_modal.instructions": "Contrôlez qui peut interagir avec ce post. Vous pouvez également appliquer ces paramètres à tous les futurs messages en allant dans <link>Préférences > Valeurs par d'éfaut de publication</link>.",
"visibility_modal.helper.unlisted_quoting": "Lorsque les gens vous citent, leur message sera également caché des fils tendances.",
"visibility_modal.instructions": "Contrôlez qui peut interagir avec ce post. Vous pouvez également appliquer ces paramètres à tous les futurs messages en allant dans <link>Préférences > Valeurs par défaut de publication</link>.",
"visibility_modal.privacy_label": "Visibilité",
"visibility_modal.quote_followers": "Abonné·e·s seulement",
"visibility_modal.quote_label": "Autoriser les citations pour",

View File

@@ -4,7 +4,7 @@
"about.default_locale": "Défaut",
"about.disclaimer": "Mastodon est un logiciel libre, open-source et une marque déposée de Mastodon gGmbH.",
"about.domain_blocks.no_reason_available": "Raison non disponible",
"about.domain_blocks.preamble": "Mastodon vous permet généralement de visualiser le contenu et d'interagir avec les utilisateurrices de n'importe quel autre serveur dans le fédivers. Voici les exceptions qui ont été faites sur ce serveur-là.",
"about.domain_blocks.preamble": "Mastodon vous permet généralement de visualiser le contenu et d'interagir avec les utilisateurs et utilisatrices de n'importe quel autre serveur dans le fédivers. Voici les exceptions qui ont été faites sur ce serveur.",
"about.domain_blocks.silenced.explanation": "Vous ne verrez généralement pas les profils et le contenu de ce serveur, à moins que vous ne les recherchiez explicitement ou que vous ne choisissiez de les suivre.",
"about.domain_blocks.silenced.title": "Limité",
"about.domain_blocks.suspended.explanation": "Aucune donnée de ce serveur ne sera traitée, enregistrée ou échangée, rendant impossible toute interaction ou communication avec les comptes de ce serveur.",
@@ -23,7 +23,7 @@
"account.blocked": "Bloqué·e",
"account.blocking": "Bloqué",
"account.cancel_follow_request": "Annuler l'abonnement",
"account.copy": "Copier le lien vers le profil",
"account.copy": "Copier le lien du profil",
"account.direct": "Mention privée @{name}",
"account.disable_notifications": "Ne plus me notifier quand @{name} publie quelque chose",
"account.domain_blocking": "Domaine bloqué",
@@ -45,7 +45,7 @@
"account.follow_request": "Demande dabonnement",
"account.follow_request_cancel": "Annuler la demande",
"account.follow_request_cancel_short": "Annuler",
"account.follow_request_short": "Requête",
"account.follow_request_short": "Demander à suivre",
"account.followers": "Abonné·e·s",
"account.followers.empty": "Personne ne suit cet·te utilisateur·rice pour linstant.",
"account.followers_counter": "{count, plural, one {{counter} abonné·e} other {{counter} abonné·e·s}}",
@@ -528,7 +528,7 @@
"limited_account_hint.action": "Afficher le profil quand même",
"limited_account_hint.title": "Ce profil a été masqué par la modération de {domain}.",
"link_preview.author": "Par {name}",
"link_preview.more_from_author": "Plus via {name}",
"link_preview.more_from_author": "Voir plus de {name}",
"link_preview.shares": "{count, plural, one {{counter} message} other {{counter} messages}}",
"lists.add_member": "Ajouter",
"lists.add_to_list": "Ajouter à la liste",
@@ -759,7 +759,7 @@
"privacy.quote.disabled": "{visibility}, citations désactivées",
"privacy.quote.limited": "{visibility}, citations limitées",
"privacy.unlisted.additional": "Se comporte exactement comme « public », sauf que le message n'apparaîtra pas dans les flux en direct, les hashtags, explorer ou la recherche Mastodon, même si vous les avez activé au niveau de votre compte.",
"privacy.unlisted.long": "Caché des résultats de recherche de Mastodon, aux tendances et aux échéanciers publics",
"privacy.unlisted.long": "Caché des résultats de recherche de Mastodon, des tendances et des fils publics",
"privacy.unlisted.short": "Public discret",
"privacy_policy.last_updated": "Dernière mise à jour {date}",
"privacy_policy.title": "Politique de confidentialité",
@@ -903,6 +903,7 @@
"status.edited_x_times": "Modifié {count, plural, one {{count} fois} other {{count} fois}}",
"status.embed": "Obtenir le code d'intégration",
"status.favourite": "Ajouter aux favoris",
"status.favourites_count": "{count, plural, one {{counter} favori} other {{counter} favoris}}",
"status.filter": "Filtrer ce message",
"status.history.created": "créé par {name} {date}",
"status.history.edited": "modifié par {name} {date}",
@@ -937,12 +938,14 @@
"status.quotes.empty": "Personne n'a encore cité ce message. Quand quelqu'un le fera, il apparaîtra ici.",
"status.quotes.local_other_disclaimer": "Les citations rejetées par l'auteur ne seront pas affichées.",
"status.quotes.remote_other_disclaimer": "Seules les citations de {domain} sont garanties d'être affichées ici. Les citations rejetées par l'auteur ne seront pas affichées.",
"status.quotes_count": "{count, plural, one {{counter} citation} other {{counter} citations}}",
"status.read_more": "Lire la suite",
"status.reblog": "Partager",
"status.reblog_or_quote": "Boost ou citation",
"status.reblog_private": "Partagez à nouveau avec vos abonnés",
"status.reblogged_by": "{name} a partagé",
"status.reblogs.empty": "Personne na encore partagé ce message. Lorsque quelquun le fera, il apparaîtra ici.",
"status.reblogs_count": "{count, plural, one {{counter} partage} other {{counter} partages}}",
"status.redraft": "Supprimer et réécrire",
"status.remove_bookmark": "Retirer des marque-pages",
"status.remove_favourite": "Retirer des favoris",
@@ -1023,8 +1026,8 @@
"visibility_modal.helper.privacy_editing": "La visibilité ne peut pas être modifiée après la publication d'un message.",
"visibility_modal.helper.privacy_private_self_quote": "Les auto-citations de messages privés ne peuvent pas être rendues publiques.",
"visibility_modal.helper.private_quoting": "Les posts accessible uniquement par les followers sur Mastodon ne peuvent être cités par d'autres personnes.",
"visibility_modal.helper.unlisted_quoting": "Lorsque les gens vous citent, leur message sera également caché dans les calendriers tendances.",
"visibility_modal.instructions": "Contrôlez qui peut interagir avec ce post. Vous pouvez également appliquer ces paramètres à tous les futurs messages en allant dans <link>Préférences > Valeurs par d'éfaut de publication</link>.",
"visibility_modal.helper.unlisted_quoting": "Lorsque les gens vous citent, leur message sera également caché des fils tendances.",
"visibility_modal.instructions": "Contrôlez qui peut interagir avec ce post. Vous pouvez également appliquer ces paramètres à tous les futurs messages en allant dans <link>Préférences > Valeurs par défaut de publication</link>.",
"visibility_modal.privacy_label": "Visibilité",
"visibility_modal.quote_followers": "Abonné·e·s seulement",
"visibility_modal.quote_label": "Autoriser les citations pour",

View File

@@ -189,7 +189,7 @@
"notification.update": "{name} nuntium correxit",
"notification_requests.accept": "Accipe",
"notification_requests.confirm_accept_multiple.message": "Tu es accepturus {count, plural, one {una notitia petitionem} other {# notitia petitiones}}. Certus esne procedere vis?",
"notification_requests.confirm_dismiss_multiple.message": "Tu {count, plural, one {unam petitionem notificationis} other {# petitiones notificationum}} abrogāre prōximum es. {count, plural, one {Illa} other {Eae}} facile accessū nōn erit. Certus es tē procedere velle?",
"notification_requests.confirm_dismiss_multiple.message": "Tu {count, plural, one {unam petitionem notificationis} other {# petitiones notificationum}} abrogāre prōximum es. {count, plural, one {it} other {Eae}} facile accessū nōn erit. Certus es tē procedere velle?",
"notifications.filter.all": "Omnia",
"notifications.filter.polls": "Eventus electionis",
"notifications.group": "{count} Notificātiōnēs",
@@ -246,19 +246,102 @@
"status.history.created": "{name} creatum {date}",
"status.history.edited": "{name} correxit {date}",
"status.open": "Expand this status",
"status.quotes.empty": "Nemo hanc commentationem adhuc citavit. Cum quis citaverit, hic apparebit.",
"status.quotes.local_other_disclaimer": "Citationes ab auctore reiec­tæ non monstrabuntur.",
"status.quotes.remote_other_disclaimer": "Tantum citae ex {domain} hic exhiberi praestantur. Citae ab auctore reiectae non exhibebuntur.",
"status.quotes_count": "{count, plural, one {{counter} citatio} other {{counter} citationes}}",
"status.read_more": "Plura lege",
"status.reblog": "Promovere",
"status.reblog_or_quote": "Promovere aut cita",
"status.reblog_private": "Iterum cum sectatoribus tuis communica",
"status.reblogged_by": "{name} adiuvavit",
"status.reblogs.empty": "Nemo hanc publicationem adhuc promovit. Cum quis eam promoveat, hic apparebunt.",
"status.reblogs_count": "{count, plural, one {{counter} incrementum} other {{counter} incrementa}}",
"status.redraft": "Dele et redig",
"status.remove_bookmark": "Tolle signum",
"status.remove_favourite": "Tolle ad delectis",
"status.remove_quote": "Tolle",
"status.replied_in_thread": "In filo responsum",
"status.replied_to": "{name} respondit",
"status.reply": "Respondere",
"status.replyAll": "Responde ad filum",
"status.report": "Referre @{name}",
"status.request_quote": "Pretium petere",
"status.revoke_quote": "Tolle nuntium meum ex nuntio @{name}",
"status.sensitive_warning": "Materia delicata",
"status.share": "Communica",
"status.show_less_all": "Omnibus minus monstra",
"status.show_more_all": "Omnibus plura monstra",
"status.show_original": "Monstra originalem",
"status.title.with_attachments": "{user} publicavit {attachmentCount, plural, one {unum annexum} other {{attachmentCount} annexa}}",
"status.translate": "Converte",
"status.translated_from_with": "Translatum ex {lang} per {provider}",
"status.uncached_media_warning": "Praevisum non praesto est",
"status.unmute_conversation": "Conversationem reserare",
"subscribed_languages.lead": "Tantum epistolae in linguis selectis in domo tua apparebunt et indices temporum post mutationem. Neminem eligatis qui epistolas in omnibus linguis recipiat.",
"subscribed_languages.save": "Servare mutationes",
"subscribed_languages.target": "Muta linguas subscriptas pro {target}",
"tabs_bar.home": "Domi",
"tabs_bar.menu": "Elenchus",
"tabs_bar.notifications": "Acta Vicimediorum",
"tabs_bar.publish": "Nova publicatio",
"tabs_bar.search": "Quaere",
"terms_of_service.effective_as_of": "Valet ex {date}",
"terms_of_service.title": "Termini servitii",
"terms_of_service.upcoming_changes_on": "Mutationes venturae die {date}",
"time_remaining.days": "{number, plural, one {# die} other {# dies}} restant",
"time_remaining.hours": "{number, plural, one {# hora} other {# horae}} restant",
"time_remaining.minutes": "{number, plural, one {# minutum} other {# minuta}} restant",
"time_remaining.moments": "Momenta reliqua",
"time_remaining.seconds": "{number, plural, one {# secundum} other {# secunda}} restant",
"trends.counter_by_accounts": "{count, plural, one {{counter} persōna} other {{counter} persōnae}} in {days, plural, one {diē prīdiē} other {diēbus praeteritīs {days}}}",
"trends.counter_by_accounts": "{count, plural, one {{counter} persōna} other {{counter} persōnae}} in {days, plural, one {days} other {diēbus praeteritīs {days}}}",
"trends.trending_now": "Nunc in usu",
"ui.beforeunload": "Si Mastodon discesseris, tua epitome peribit.",
"units.short.billion": "{count} millia milionum",
"units.short.million": "{count} milionum",
"units.short.thousand": "{count} millia",
"upload_button.label": "Imaginēs, vīdeō aut fīle audītūs adde",
"upload_area.title": "Trahe et depone ad imponendum",
"upload_button.label": "Adde imagines, pelliculam, aut fasciculum sonorum.",
"upload_error.limit": "Limes onerationis superatus est.",
"upload_error.poll": "Nullis suffragiis licet fascicula imponere.",
"upload_error.quote": "Nullum oneramentum fasciculi cum citationibus permittitur.",
"upload_form.drag_and_drop.instructions": "Ad annexum mediorum tollendum, preme clavem \"Space\" aut \"Enter\". Dum traheis, utere clavibus sagittariis ad annexum mediorum in quamlibet partem movendum. Preme iterum \"Space\" aut \"Enter\" ad annexum mediorum in novo loco deponendum, aut preme \"Escape\" ad desinendum.",
"upload_form.drag_and_drop.on_drag_cancel": "Tractatio revocata est. Adiunctum medium {item} demissum est.",
"upload_form.drag_and_drop.on_drag_end": "Adhaesum medium {item} demissum est.",
"upload_form.drag_and_drop.on_drag_over": "Adhaesum medium {item} motum est.",
"upload_form.drag_and_drop.on_drag_start": "Adhaesum medium {item} sublatum est.",
"upload_form.edit": "Recolere",
"upload_progress.label": "Uploading…"
"upload_progress.label": "Oneratur...",
"upload_progress.processing": "Processus…",
"username.taken": "Illud nomen usoris occupatum est. Aliud tenta.",
"video.close": "Pelliculam claude",
"video.download": "Prehendere fasciculus",
"video.exit_fullscreen": "Exitus ex plenum monitorium",
"video.expand": "Expande pelliculam",
"video.fullscreen": "Plenum monitorium",
"video.hide": "celare pellicula",
"video.mute": "Mutus",
"video.pause": "intermittere",
"video.play": "gignere",
"video.skip_backward": "Redire",
"video.skip_forward": "Progredi",
"video.unmute": "Sordes tollere",
"video.volume_down": "Volumen deminui",
"video.volume_up": "Volumen augete",
"visibility_modal.button_title": "Visibilitatem statuere",
"visibility_modal.direct_quote_warning.text": "Si praesentia configuramenta servaveris, sententia inserta in nexum convertetur.",
"visibility_modal.direct_quote_warning.title": "Citatio in mentitionibus privatis inseriri non possunt.",
"visibility_modal.header": "Visibilitas et interactio",
"visibility_modal.helper.direct_quoting": "Mentiones privatae in Mastodon scriptae ab aliis citari non possunt.",
"visibility_modal.helper.privacy_editing": "Visibilitas mutari non potest postquam nuntius publicatus est.",
"visibility_modal.helper.privacy_private_self_quote": "Citationes propriae nuntiorum privatorum publicari non possunt.",
"visibility_modal.helper.private_quoting": "Nuntii in Mastodon a sequacibus tantum scriptī ab aliis citari non possunt.",
"visibility_modal.helper.unlisted_quoting": "Cum te citant, eorum scriptum etiam ex indicibus popularibus celabitur.",
"visibility_modal.instructions": "Régula quis cum hoc scripto agere possit. Potes etiam ordinationes omnibus scriptis futuris adhibere, navigando ad <link>Praeferentias > Regulas scriptionis praedefinitas</link>.",
"visibility_modal.privacy_label": "Visibilitas",
"visibility_modal.quote_followers": "Sectatores tantum",
"visibility_modal.quote_label": "Quis citare potest",
"visibility_modal.quote_nobody": "Sicut me",
"visibility_modal.quote_public": "quisquis",
"visibility_modal.save": "Servare"
}

View File

@@ -638,12 +638,14 @@
"privacy.direct.long": "Visi ierakstā pieminētie",
"privacy.private.long": "Tikai Tavi sekotāji",
"privacy.private.short": "Sekotāji",
"privacy.public.long": "Jebkurš Mastodon un ārpus tā",
"privacy.public.long": "Jebkurš Mastodon platformā un ārpus tās",
"privacy.public.short": "Publisks",
"privacy.quote.anyone": "{visibility}, jebkurš var citēt",
"privacy.quote.disabled": "{visibility}, aizliegta citēšana",
"privacy.quote.limited": "{visibility}, ierobežota citēšana",
"privacy.unlisted.additional": "Šis uzvedas tieši kā publisks, izņemot to, ka ieraksts neparādīsies tiešraides barotnēs vai tēmturos, izpētē vai Mastodon meklēšanā, pat ja esi to norādījis visa konta ietvaros.",
"privacy.unlisted.long": "Netiks rādīts Mastodon meklēšanas rezultātos, populārākajos, un publiskajās laikjoslās",
"privacy.unlisted.short": "Klusi publisks",
"privacy_policy.last_updated": "Pēdējo reizi atjaunināta {date}",
"privacy_policy.title": "Privātuma politika",
"recommended": "Ieteicams",
@@ -837,7 +839,7 @@
"video.volume_down": "Pagriezt klusāk",
"video.volume_up": "Pagriezt skaļāk",
"visibility_modal.button_title": "Iestatīt redzamību",
"visibility_modal.header": "Redzamība un mijjiedarbība",
"visibility_modal.header": "Redzamība un mijiedarbība",
"visibility_modal.quote_followers": "Tikai sekotāji",
"visibility_modal.quote_label": "Kurš var citēt",
"visibility_modal.quote_nobody": "Tikai es",

View File

@@ -903,6 +903,7 @@
"status.edited_x_times": "有編輯 {count, plural, one {{count} kái} other {{count} kái}}",
"status.embed": "The̍h相tàu ê (embed)程式碼",
"status.favourite": "收藏",
"status.favourites_count": "{count, plural, one {{counter} 篇} other {{counter} 篇}} 收藏",
"status.filter": "過濾tsit 篇 PO文",
"status.history.created": "{name} 佇 {date} 建立",
"status.history.edited": "{name} 佇 {date} 編輯",
@@ -936,12 +937,14 @@
"status.quotes.empty": "Iáu無lâng引用tsit篇PO文。Nā是有lâng引用ē佇tsia顯示。.",
"status.quotes.local_other_disclaimer": "Hōo作者拒絕引用ê引文bē當顯示。",
"status.quotes.remote_other_disclaimer": "Kan-ta tuì {domain} 來ê引文tsiah保證佇tsia顯示。Hōo作者拒絕ê引文buē顯示。",
"status.quotes_count": "{count, plural, one {{counter} 篇} other {{counter} 篇}} 引用",
"status.read_more": "讀詳細",
"status.reblog": "轉送",
"status.reblog_or_quote": "轉送á是引用",
"status.reblog_private": "Koh再hām跟tuè ê分享",
"status.reblogged_by": "{name} kā轉送ah",
"status.reblogs.empty": "Iáu無lâng轉送tsit篇PO文。Nā是有lâng轉送ē佇tsia顯示。",
"status.reblogs_count": "{count, plural, one {{counter} 篇} other {{counter} 篇}} 轉送",
"status.redraft": "Thâi掉了後重寫",
"status.remove_bookmark": "Thâi掉冊籤",
"status.remove_favourite": "Tuì收藏內suá掉",
@@ -1014,6 +1017,8 @@
"video.volume_down": "變khah細聲",
"video.volume_up": "變khah大聲",
"visibility_modal.button_title": "設定通看ê程度",
"visibility_modal.direct_quote_warning.text": "Nā是lí儲存目前ê設定引用êPO文ē轉做連結。",
"visibility_modal.direct_quote_warning.title": "引用bē當tàu入去私人ê提起",
"visibility_modal.header": "通看ê程度kap互動",
"visibility_modal.helper.direct_quoting": "Mastodon頂發布ê私人提起bē當hōo別lâng引用。",
"visibility_modal.helper.privacy_editing": "Po文發布了後bē當改通看ê程度。",

View File

@@ -628,7 +628,7 @@
"notification.moderation_warning.action_disable": "Ваша учётная запись была отключена.",
"notification.moderation_warning.action_mark_statuses_as_sensitive": "Некоторые ваши посты были отмечены как содержимое деликатного характера.",
"notification.moderation_warning.action_none": "Модераторы вынесли вам предупреждение.",
"notification.moderation_warning.action_sensitive": "С этого момента все ваши новые посты будут отмечены как содержимое деликатного характера.",
"notification.moderation_warning.action_sensitive": "С этого момента все ваши посты будут отмечены как содержимое деликатного характера.",
"notification.moderation_warning.action_silence": "Ваша учётная запись была ограничена.",
"notification.moderation_warning.action_suspend": "Ваша учётная запись была заблокирована.",
"notification.own_poll": "Ваш опрос завершился",

View File

@@ -1027,7 +1027,7 @@
"visibility_modal.helper.privacy_private_self_quote": "Không thể công khai trích dẫn tút của bản thân hoặc tút riêng tư.",
"visibility_modal.helper.private_quoting": "Tút chỉ dành cho người theo dõi trên Mastodon không thể được người khác trích dẫn.",
"visibility_modal.helper.unlisted_quoting": "Khi ai đó trích dẫn bạn, tút của họ cũng sẽ bị ẩn khỏi bảng tin công khai.",
"visibility_modal.instructions": "Kiểm soát những ai có thể tương tác với tút này. Bạn cũng có thể áp dụng cài đặt cho tất cả các tút trong tương lai bằng cách điều hướng đến <link>Thiết lập > Đăng</link>.",
"visibility_modal.instructions": "Kiểm soát những ai có thể tương tác với tút này. Bạn cũng có thể đặt sẵn cho tất cả tút trong tương lai bằng cách truy cập <link>Thiết lập > Mặc định cho tút</link>.",
"visibility_modal.privacy_label": "Hiển thị",
"visibility_modal.quote_followers": "Chỉ người theo dõi",
"visibility_modal.quote_label": "Ai có thể trích dẫn",

View File

@@ -9,7 +9,6 @@ import { me, reduceMotion } from 'mastodon/initial_state';
import ready from 'mastodon/ready';
import { store } from 'mastodon/store';
import { initializeEmoji } from './features/emoji';
import { isProduction, isDevelopment } from './utils/environment';
function main() {
@@ -30,6 +29,7 @@ function main() {
});
}
const { initializeEmoji } = await import('./features/emoji/index');
initializeEmoji();
const root = createRoot(mountNode);

View File

@@ -341,8 +341,8 @@ export const composeReducer = (state = initialState, action) => {
const isDirect = state.get('privacy') === 'direct';
return state
.set('quoted_status_id', isDirect ? null : status.get('id'))
.set('spoiler', status.get('sensitive'))
.set('spoiler_text', status.get('spoiler_text'))
.update('spoiler', spoiler => (spoiler) || !!status.get('spoiler_text'))
.update('spoiler_text', (spoiler_text) => spoiler_text || status.get('spoiler_text'))
.update('privacy', (visibility) => {
if (['public', 'unlisted'].includes(visibility) && status.get('visibility') === 'private') {
return 'private';

View File

@@ -327,9 +327,9 @@ $content-width: 840px;
font-weight: 700;
color: $primary-text-color;
text-transform: none;
padding-bottom: 0;
padding-top: 0;
margin-bottom: 0;
border-bottom: 0;
border-top: 0;
.comment {
display: block;
@@ -1040,10 +1040,6 @@ a.name-tag,
margin-top: 15px;
}
.user-role {
color: var(--user-role-accent);
}
.applications-list {
.icon {
vertical-align: middle;

View File

@@ -0,0 +1,7 @@
@use 'mastodon/css_variables';
@use 'mastodon/variables';
@use 'common';
html {
color-scheme: dark;
}

View File

@@ -0,0 +1,24 @@
@use 'mastodon/mixins';
@use 'fonts/roboto';
@use 'fonts/roboto-mono';
@use 'mastodon/reset';
@use 'mastodon/basics';
@use 'mastodon/branding';
@use 'mastodon/containers';
@use 'mastodon/lists';
@use 'mastodon/widgets';
@use 'mastodon/forms';
@use 'mastodon/accounts';
@use 'mastodon/components';
@use 'mastodon/polls';
@use 'mastodon/modal';
@use 'mastodon/emoji_picker';
@use 'mastodon/annual_reports';
@use 'mastodon/about';
@use 'mastodon/tables';
@use 'mastodon/admin';
@use 'mastodon/dashboard';
@use 'mastodon/rtl';
@use 'mastodon/accessibility';
@use 'mastodon/rich_text';

View File

@@ -0,0 +1,8 @@
@use 'mastodon/css_variables';
@use 'mastodon/variables';
@use 'common';
@use 'contrast/diff';
html {
color-scheme: dark;
}

View File

@@ -0,0 +1,54 @@
:root {
/* TEXT TOKENS */
--color-text-primary: var(--color-grey-50);
--color-text-secondary: var(--color-grey-300);
--color-text-tertiary: var(--color-grey-400);
--color-text-brand: var(--color-indigo-300);
--color-text-status-links: var(--color-text-brand);
/* BORDER TOKENS */
--border-strength-primary: 18%;
}
.status__content a,
.reply-indicator__content a,
.edit-indicator__content a,
.link-footer a,
.status__content__read-more-button,
.status__content__translate-button {
text-decoration: underline;
&:hover,
&:focus,
&:active {
text-decoration: none;
}
&.mention {
text-decoration: none;
span {
text-decoration: underline;
}
&:hover,
&:focus,
&:active {
span {
text-decoration: none;
}
}
}
}
.link-button:disabled {
cursor: not-allowed;
&:hover,
&:focus,
&:active {
text-decoration: none !important;
}
}

View File

@@ -0,0 +1,14 @@
/* This is needed for the wicg-inert polyfill */
[inert] {
pointer-events: none;
cursor: default;
}
[inert],
[inert] * {
user-select: none;
-webkit-user-select: none;
-moz-user-select: none;
-ms-user-select: none;
}

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,8 @@
@font-face {
font-family: Inter;
src: url('../../fonts/inter/inter-variable-font-slnt-wght.woff2')
format('woff2-variations');
font-weight: 100 900;
font-style: normal;
mso-generic-font-family: swiss;
}

View File

@@ -0,0 +1,13 @@
@font-face {
font-family: mastodon-font-monospace;
src:
local('Roboto Mono'),
url('@/fonts/roboto-mono/robotomono-regular-webfont.woff2') format('woff2'),
url('@/fonts/roboto-mono/robotomono-regular-webfont.woff') format('woff'),
url('@/fonts/roboto-mono/robotomono-regular-webfont.ttf') format('truetype'),
url('@/fonts/roboto-mono/robotomono-regular-webfont.svg#roboto_monoregular')
format('svg');
font-weight: 400;
font-display: swap;
font-style: normal;
}

View File

@@ -0,0 +1,55 @@
@font-face {
font-family: mastodon-font-sans-serif;
src:
local('Roboto Italic'),
url('@/fonts/roboto/roboto-italic-webfont.woff2') format('woff2'),
url('@/fonts/roboto/roboto-italic-webfont.woff') format('woff'),
url('@/fonts/roboto/roboto-italic-webfont.ttf') format('truetype'),
url('@/fonts/roboto/roboto-italic-webfont.svg#roboto-italic-webfont')
format('svg');
font-weight: normal;
font-display: swap;
font-style: italic;
}
@font-face {
font-family: mastodon-font-sans-serif;
src:
local('Roboto Bold'),
url('@/fonts/roboto/roboto-bold-webfont.woff2') format('woff2'),
url('@/fonts/roboto/roboto-bold-webfont.woff') format('woff'),
url('@/fonts/roboto/roboto-bold-webfont.ttf') format('truetype'),
url('@/fonts/roboto/roboto-bold-webfont.svg#roboto-bold-webfont')
format('svg');
font-weight: bold;
font-display: swap;
font-style: normal;
}
@font-face {
font-family: mastodon-font-sans-serif;
src:
local('Roboto Medium'),
url('@/fonts/roboto/roboto-medium-webfont.woff2') format('woff2'),
url('@/fonts/roboto/roboto-medium-webfont.woff') format('woff'),
url('@/fonts/roboto/roboto-medium-webfont.ttf') format('truetype'),
url('@/fonts/roboto/roboto-medium-webfont.svg#roboto-medium-webfont')
format('svg');
font-weight: 500;
font-display: swap;
font-style: normal;
}
@font-face {
font-family: mastodon-font-sans-serif;
src:
local('Roboto'),
url('@/fonts/roboto/roboto-regular-webfont.woff2') format('woff2'),
url('@/fonts/roboto/roboto-regular-webfont.woff') format('woff'),
url('@/fonts/roboto/roboto-regular-webfont.ttf') format('truetype'),
url('@/fonts/roboto/roboto-regular-webfont.svg#roboto-regular-webfont')
format('svg');
font-weight: normal;
font-display: swap;
font-style: normal;
}

View File

@@ -0,0 +1,9 @@
@use 'mastodon-light/css_variables';
@use 'mastodon/variables' with (
$emojis-requiring-inversion: 'chains'
);
@use 'common';
html {
color-scheme: light;
}

View File

@@ -0,0 +1,214 @@
@use '../mastodon/theme_utils' as utils;
:root {
--color-black: #000;
--color-grey-950: #181821;
--color-grey-800: #292938;
--color-grey-700: #444664;
--color-grey-600: #545778;
--color-grey-500: #696d91;
--color-grey-400: #8b8dac;
--color-grey-300: #b4b6cb;
--color-grey-200: #d8d9e3;
--color-grey-100: #f0f0f5;
--color-grey-50: #f0f1ff;
--color-white: #fff;
--color-indigo-600: #6147e6;
--color-indigo-400: #8886ff;
--color-indigo-300: #a5abfd;
--color-indigo-200: #c8cdfe;
--color-indigo-100: #e0e3ff;
--color-indigo-50: #f0f1ff;
--color-red-500: #ff637e;
--color-red-600: #ec003f;
--color-yellow-400: #ffb900;
--color-yellow-600: #e17100;
--color-green-400: #05df72;
--color-green-600: #00a63e;
/* TEXT TOKENS */
--color-text-primary: var(--color-grey-950);
--color-text-secondary: var(--color-grey-600);
--color-text-tertiary: var(--color-grey-500);
--color-text-on-inverted: var(--color-white);
--color-text-brand: var(--color-indigo-600);
--color-text-brand-soft: color-mix(
in oklab,
var(--color-text-primary),
var(--color-text-brand)
);
--color-text-on-brand-base: var(--color-white);
--color-text-error: var(--color-red-600);
--color-text-on-error-base: var(--color-white);
--color-text-warning: var(--color-yellow-600);
--color-text-on-warning-base: var(--color-white);
--color-text-success: var(--color-green-600);
--color-text-on-success-base: var(--color-white);
--color-text-disabled: var(--color-grey-300);
--color-text-on-disabled: var(--color-grey-200);
--color-text-bookmark-highlight: var(--color-text-error);
--color-text-favourite-highlight: var(--color-text-warning);
--color-text-on-media: var(--color-white);
--color-text-status-links: var(--color-text-brand);
/* BACKGROUND TOKENS */
// Neutrals
--color-bg-primary: var(--color-white);
--overlay-strength-secondary: 5%;
--color-bg-secondary-base: var(--color-grey-600);
--color-bg-secondary: #{color-mix(
in oklab,
var(--color-bg-primary),
var(--color-bg-secondary-base) var(--overlay-strength-secondary)
)};
--color-bg-secondary-solid: #{color-mix(
in srgb,
var(--color-bg-primary),
var(--color-bg-secondary-base) var(--overlay-strength-secondary)
)};
--color-bg-tertiary: #{color-mix(
in oklab,
var(--color-bg-primary),
var(--color-bg-secondary-base) calc(2 * var(--overlay-strength-secondary))
)};
// Utility
--color-bg-ambient: var(--color-bg-primary);
--color-bg-elevated: var(--color-bg-primary);
--color-bg-inverted: var(--color-grey-950);
--color-bg-media-base: var(--color-black);
--color-bg-media-strength: 65%;
--color-bg-media: #{utils.css-alpha(
var(--color-bg-media-base),
var(--color-bg-media-strength)
)};
--color-bg-overlay: var(--color-bg-primary);
--color-bg-disabled: var(--color-grey-400);
// Brand
--overlay-strength-brand: 8%;
--color-bg-brand-base: var(--color-indigo-600);
--color-bg-brand-base-hover: color-mix(
in oklab,
var(--color-bg-brand-base),
black var(--overlay-strength-brand)
);
--color-bg-brand-soft: #{utils.css-alpha(
var(--color-bg-brand-base),
calc(var(--overlay-strength-brand) * 1.5)
)};
--color-bg-brand-softer: #{utils.css-alpha(
var(--color-bg-brand-base),
var(--overlay-strength-brand)
)};
// Error
--overlay-strength-error: 12%;
--color-bg-error-base: var(--color-red-600);
--color-bg-error-base-hover: color-mix(
in oklab,
var(--color-bg-error-base),
black var(--overlay-strength-error)
);
--color-bg-error-soft: #{utils.css-alpha(
var(--color-bg-error-base),
calc(var(--overlay-strength-error) * 1.5)
)};
--color-bg-error-softer: #{utils.css-alpha(
var(--color-bg-error-base),
var(--overlay-strength-error)
)};
// Warning
--overlay-strength-warning: 10%;
--color-bg-warning-base: var(--color-yellow-600);
--color-bg-warning-base-hover: color-mix(
in oklab,
var(--color-bg-warning-base),
black var(--overlay-strength-warning)
);
--color-bg-warning-soft: #{utils.css-alpha(
var(--color-bg-warning-base),
calc(var(--overlay-strength-warning) * 1.5)
)};
--color-bg-warning-softer: #{utils.css-alpha(
var(--color-bg-warning-base),
var(--overlay-strength-warning)
)};
// Success
--overlay-strength-success: 15%;
--color-bg-success-base: var(--color-green-600);
--color-bg-success-base-hover: color-mix(
in oklab,
var(--color-bg-success-base),
black var(--overlay-strength-success)
);
--color-bg-success-soft: #{utils.css-alpha(
var(--color-bg-success-base),
calc(var(--overlay-strength-success) * 1.5)
)};
--color-bg-success-softer: #{utils.css-alpha(
var(--color-bg-success-base),
var(--overlay-strength-success)
)};
/* BORDER TOKENS */
--border-strength-primary: 15%;
--color-border-primary: color-mix(
in oklab,
var(--color-bg-primary),
var(--color-grey-950) var(--border-strength-primary)
);
--color-border-media: rgb(252 248 255 / 15%);
--color-border-on-bg-secondary: var(--color-grey-200);
--color-border-on-bg-brand-softer: var(--color-indigo-200);
--color-border-on-bg-error-softer: #{utils.css-alpha(
var(--color-text-error),
50%
)};
--color-border-on-bg-warning-softer: #{utils.css-alpha(
var(--color-text-warning),
50%
)};
--color-border-on-bg-success-softer: #{utils.css-alpha(
var(--color-text-success),
50%
)};
--color-border-on-bg-inverted: var(--color-border-primary);
/* SHADOW TOKENS */
--shadow-strength-primary: 30%;
--color-shadow-primary: #{utils.css-alpha(
var(--color-black),
var(--shadow-strength-primary)
)};
--dropdown-shadow:
0 20px 25px -5px var(--color-shadow-primary),
0 8px 10px -6px var(--color-shadow-primary);
--overlay-icon-shadow: drop-shadow(0 0 8px var(--color-shadow-primary));
/* GRAPHS/CHARTS TOKENS */
--color-graph-primary-stroke: var(--color-text-brand);
--color-graph-primary-fill: var(--color-bg-brand-softer);
--color-graph-warning-stroke: var(--color-text-warning);
--color-graph-warning-fill: var(--color-bg-warning-softer);
--color-graph-disabled-stroke: var(--color-text-disabled);
--color-graph-disabled-fill: var(--color-bg-disabled);
/* LEGACY TOKENS */
--rich-text-container-color: rgb(255 216 231 / 100%);
--rich-text-text-color: rgb(114 47 83 / 100%);
--rich-text-decorations-color: rgb(255 175 212 / 100%);
/* MISCELLANEOUS */
--outline-focus-default: 2px solid var(--color-text-brand);
--avatar-border-radius: 8px;
}

View File

@@ -0,0 +1,45 @@
@mixin search-input {
outline: 0;
box-sizing: border-box;
width: 100%;
box-shadow: none;
font-family: inherit;
background: var(--color-bg-secondary);
color: var(--color-text-primary);
border-radius: 4px;
border: 1px solid var(--color-border-on-bg-secondary);
font-size: 17px;
line-height: normal;
margin: 0;
}
@mixin search-popout {
background: var(--color-bg-elevated);
border-radius: 4px;
padding: 10px 14px;
padding-bottom: 14px;
margin-top: 10px;
color: var(--color-text-secondary);
box-shadow: 2px 4px 15px var(--color-shadow-primary);
h4 {
text-transform: uppercase;
color: var(--color-text-secondary);
font-size: 13px;
font-weight: 500;
margin-bottom: 10px;
}
li {
padding: 4px 0;
}
ul {
margin-bottom: 10px;
}
em {
font-weight: 500;
color: var(--color-text-primary);
}
}

View File

@@ -0,0 +1,3 @@
@function css-alpha($base-color, $amount) {
@return #{rgb(from $base-color r g b / $amount)};
}

View File

@@ -0,0 +1,27 @@
// Keep this filter a SCSS variable rather than
// a CSS Custom Property due to this Safari bug:
// https://github.com/mdn/browser-compat-data/issues/25914#issuecomment-2676190245
$backdrop-blur-filter: blur(10px) saturate(180%) contrast(75%) brightness(70%);
// Language codes that uses CJK fonts
$cjk-langs: ja, ko, zh-CN, zh-HK, zh-TW;
// Variables for components
$media-modal-media-max-width: 100%;
// put margins on top and bottom of image to avoid the screen covered by image.
$media-modal-media-max-height: 80%;
$no-gap-breakpoint: 1175px;
$mobile-menu-breakpoint: 760px;
$mobile-breakpoint: 630px;
$no-columns-breakpoint: 600px;
$font-sans-serif: 'mastodon-font-sans-serif' !default;
$font-display: 'mastodon-font-display' !default;
$font-monospace: 'mastodon-font-monospace' !default;
$emojis-requiring-inversion: 'back' 'copyright' 'curly_loop' 'currency_exchange'
'end' 'heavy_check_mark' 'heavy_division_sign' 'heavy_dollar_sign'
'heavy_minus_sign' 'heavy_multiplication_x' 'heavy_plus_sign' 'on'
'registered' 'soon' 'spider' 'telephone_receiver' 'tm' 'top' 'wavy_dash' !default;

View File

@@ -0,0 +1,130 @@
@use 'variables' as *;
$maximum-width: 1235px;
$fluid-breakpoint: $maximum-width + 20px;
.container {
box-sizing: border-box;
max-width: $maximum-width;
margin: 0 auto;
position: relative;
@media screen and (max-width: $fluid-breakpoint) {
width: 100%;
padding: 0 10px;
}
}
.brand {
position: relative;
text-decoration: none;
}
.rules-list {
font-size: 15px;
line-height: 22px;
counter-reset: list-counter;
li {
position: relative;
border-bottom: 1px solid var(--color-border-primary);
padding: 1em 1.75em;
padding-inline-start: 3em;
font-weight: 500;
counter-increment: list-counter;
min-height: 4ch;
button {
background: transparent;
border: 0;
padding: 0;
margin: 0;
text-align: start;
font: inherit;
&:hover,
&:focus,
&:active {
background: transparent;
}
&[aria-expanded='false'] .rules-list__hint {
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
@supports (-webkit-line-clamp: 2) {
display: -webkit-box;
-webkit-line-clamp: 2;
-webkit-box-orient: vertical;
white-space: normal;
}
}
}
&::before {
content: counter(list-counter);
position: absolute;
inset-inline-start: 0;
top: 1em;
background: var(--color-bg-brand-base);
color: var(--color-text-on-brand-base);
border-radius: 50%;
width: 4ch;
height: 4ch;
font-weight: 500;
display: flex;
justify-content: center;
align-items: center;
}
&:last-child {
border-bottom: 0;
}
}
&__text {
color: var(--color-text-primary);
}
&__hint {
font-size: 14px;
font-weight: 400;
color: var(--color-text-secondary);
}
}
.rules-languages {
display: flex;
gap: 1rem;
align-items: center;
position: relative;
> label {
font-size: 14px;
font-weight: 600;
color: var(--color-text-primary);
}
select {
appearance: none;
box-sizing: border-box;
font-size: 14px;
color: var(--color-text-primary);
display: block;
width: 100%;
outline: 0;
font-family: inherit;
resize: vertical;
background: var(--color-bg-secondary);
border: 1px solid var(--color-border-primary);
border-radius: 4px;
padding-inline-start: 10px;
padding-inline-end: 30px;
height: 41px;
@media screen and (width <= 600px) {
font-size: 16px;
}
}
}

View File

@@ -0,0 +1,13 @@
@use 'variables' as *;
%emoji-color-inversion {
filter: invert(1);
}
.emojione {
@each $emoji in $emojis-requiring-inversion {
&[title=':#{$emoji}:'] {
@extend %emoji-color-inversion;
}
}
}

View File

@@ -0,0 +1,411 @@
@use 'sass:color';
@use 'variables' as *;
.card {
& > a {
display: block;
text-decoration: none;
color: inherit;
overflow: hidden;
border-radius: 4px;
&:hover,
&:active,
&:focus {
.card__bar {
background: var(--color-bg-brand-softer);
}
}
}
&__img {
height: 130px;
position: relative;
background: var(--color-bg-secondary);
border: 1px solid var(--color-border-primary);
border-bottom: none;
img {
display: block;
width: 100%;
height: 100%;
margin: 0;
object-fit: cover;
}
@media screen and (width <= 600px) {
height: 200px;
}
}
&__bar {
position: relative;
padding: 15px;
display: flex;
justify-content: flex-start;
align-items: center;
background: var(--color-bg-primary);
border: 1px solid var(--color-border-primary);
border-top: none;
.avatar {
flex: 0 0 auto;
width: 48px;
height: 48px;
padding-top: 2px;
img {
width: 100%;
height: 100%;
display: block;
margin: 0;
border-radius: 4px;
background: var(--color-bg-secondary);
object-fit: cover;
}
}
.display-name {
margin-inline-start: 15px;
text-align: start;
svg[data-hidden] {
display: none;
}
strong {
font-size: 15px;
color: var(--color-text-primary);
font-weight: 500;
overflow: hidden;
text-overflow: ellipsis;
}
span {
display: block;
font-size: 14px;
color: var(--color-text-secondary);
font-weight: 400;
overflow: hidden;
text-overflow: ellipsis;
}
}
}
}
.pagination {
padding: 30px 0;
text-align: center;
overflow: hidden;
a,
.current,
.newer,
.older,
.page,
.gap {
font-size: 14px;
color: var(--color-text-primary);
font-weight: 500;
display: inline-block;
padding: 6px 10px;
text-decoration: none;
}
.current {
color: var(--color-bg-inverted);
background: var(--color-text-on-inverted);
border-radius: 100px;
cursor: default;
margin: 0 10px;
}
.gap {
cursor: default;
}
.older,
.newer {
text-transform: uppercase;
color: var(--color-text-primary);
}
.older {
float: left;
padding-inline-start: 0;
}
.newer {
float: right;
padding-inline-end: 0;
}
.disabled {
cursor: default;
color: var(--color-text-disabled);
}
@media screen and (width <= 700px) {
padding: 30px 20px;
.page {
display: none;
}
.newer,
.older {
display: inline-block;
}
}
}
.nothing-here {
color: var(--color-text-secondary);
background: var(--color-bg-primary);
font-size: 14px;
font-weight: 500;
text-align: center;
display: flex;
justify-content: center;
align-items: center;
cursor: default;
border-radius: 4px;
padding: 20px;
min-height: 30vh;
border: 1px solid var(--color-border-primary);
@media screen and (min-width: ($no-gap-breakpoint - 1)) {
border-top: 0;
}
&--no-toolbar {
border-top: 1px solid var(--color-border-primary);
}
&--under-tabs {
border-radius: 0 0 4px 4px;
}
&--flexible {
box-sizing: border-box;
min-height: 100%;
}
}
.information-badge,
.simple_form .overridden,
.simple_form .recommended,
.simple_form .not_recommended {
display: inline-block;
padding: 4px 6px;
cursor: default;
border-radius: 4px;
font-size: 12px;
line-height: 12px;
font-weight: 500;
color: var(--color-text-primary);
text-overflow: ellipsis;
white-space: nowrap;
overflow: hidden;
}
.information-badge,
.simple_form .overridden,
.simple_form .recommended,
.simple_form .not_recommended {
background-color: var(--color-bg-secondary);
border: 1px solid var(--color-border-primary);
}
.information-badge {
&.superapp {
color: var(--color-text-success);
background-color: var(--color-bg-success-softer);
border-color: var(--color-border-on-bg-success-softer);
}
}
.account-role {
display: inline-flex;
padding: 4px;
padding-inline-end: 8px;
border: 1px solid var(--color-text-brand);
color: var(--color-text-brand);
font-weight: 500;
font-size: 12px;
letter-spacing: 0.5px;
line-height: 16px;
gap: 4px;
border-radius: 6px;
align-items: center;
svg {
width: auto;
height: 15px;
opacity: 0.85;
fill: currentColor;
}
&__domain {
font-weight: 400;
opacity: 0.75;
letter-spacing: 0;
}
}
.simple_form .not_recommended {
color: var(--color-text-error);
background-color: var(--color-bg-error-softer);
border-color: var(--color-border-on-bg-error-softer);
}
.account__header__fields {
max-width: 100vw;
padding: 0;
margin: 15px -15px -15px;
border: 0 none;
border-top: 1px solid var(--color-border-primary);
border-bottom: 1px solid var(--color-border-primary);
font-size: 14px;
line-height: 20px;
dl {
display: flex;
border-bottom: 1px solid var(--color-border-primary);
}
dt,
dd {
box-sizing: border-box;
padding: 14px;
text-align: center;
max-height: 48px;
overflow: hidden;
white-space: nowrap;
text-overflow: ellipsis;
}
dt {
font-weight: 500;
width: 120px;
flex: 0 0 auto;
color: var(--color-text-primary);
background: var(--color-bg-secondary);
}
dd {
flex: 1 1 auto;
color: var(--color-text-secondary);
}
a {
color: var(--color-text-brand);
text-decoration: none;
&:hover,
&:focus,
&:active {
text-decoration: underline;
}
}
.verified {
border: 1px solid var(--color-border-on-bg-success-softer);
background: var(--color-bg-success-softer);
a {
color: var(--color-text-success);
font-weight: 500;
}
&__mark {
color: var(--color-text-success);
}
}
dl:last-child {
border-bottom: 0;
}
}
.directory__tag .trends__item__current {
width: auto;
}
.pending-account {
&__header {
color: var(--color-text-secondary);
a {
color: var(--color-text-primary);
text-decoration: none;
&:hover,
&:active,
&:focus {
text-decoration: underline;
}
}
strong {
color: var(--color-text-primary);
font-weight: 700;
}
.warning-hint {
font-weight: normal !important;
}
}
&__body {
margin-top: 10px;
}
}
.batch-table__row--muted {
color: var(--color-text-tertiary);
}
.batch-table__row--muted .pending-account__header,
.batch-table__row--muted .accounts-table,
.batch-table__row--muted .name-tag {
&,
a,
strong {
color: var(--color-text-tertiary);
}
}
.batch-table__row--muted .name-tag .avatar {
opacity: 0.5;
}
.batch-table__row--muted .accounts-table {
tbody td.accounts-table__extra,
&__count,
&__count small {
color: var(--color-text-tertiary);
}
}
.batch-table__row--attention {
color: var(--color-text-warning);
}
.batch-table__row--attention .pending-account__header,
.batch-table__row--attention .accounts-table,
.batch-table__row--attention .name-tag {
&,
a,
strong {
color: var(--color-text-warning);
}
}
.batch-table__row--attention .accounts-table {
tbody td.accounts-table__extra,
&__count,
&__count small {
color: var(--color-text-warning);
}
}

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,342 @@
@use 'variables' as *;
:root {
--indigo-1: #17063b;
--indigo-2: #2f0c7a;
--indigo-3: #562cfc;
--indigo-5: #858afa;
--indigo-6: #cccfff;
--lime: #baff3b;
--goldenrod-2: #ffc954;
}
.annual-report {
flex: 0 0 auto;
background: var(--indigo-1);
padding: 24px;
&__header {
margin-bottom: 16px;
h1 {
font-size: 25px;
font-weight: 600;
line-height: 30px;
color: var(--lime);
margin-bottom: 8px;
}
p {
font-size: 16px;
font-weight: 600;
line-height: 20px;
color: var(--indigo-6);
}
}
&__bento {
display: grid;
gap: 8px;
grid-template-columns: minmax(0, 1fr) minmax(0, 1fr) minmax(0, 1fr);
grid-template-rows: minmax(0, auto) minmax(0, 1fr) minmax(0, auto) minmax(
0,
auto
);
&__box {
padding: 16px;
border-radius: 8px;
background: var(--indigo-2);
color: var(--indigo-5);
}
}
&__summary {
&__most-boosted-post {
grid-column: span 2;
grid-row: span 2;
padding: 0;
.status__content,
.content-warning {
color: var(--indigo-6);
}
.detailed-status {
border: 0;
}
.content-warning {
border: 0;
background: var(--indigo-1);
.link-button {
color: var(--indigo-5);
}
}
.detailed-status__meta__line {
border-bottom-color: var(--indigo-3);
}
.detailed-status__meta {
text-overflow: ellipsis;
overflow: hidden;
white-space: nowrap;
}
.detailed-status__meta,
.poll__footer,
.poll__link,
.detailed-status .logo,
.detailed-status__display-name {
color: var(--indigo-5);
}
.detailed-status__meta .animated-number,
.detailed-status__display-name strong {
color: var(--indigo-6);
}
.poll__chart {
background-color: var(--indigo-3);
&.leading {
background-color: var(--goldenrod-2);
}
}
.status-card,
.hashtag-bar {
display: none;
}
}
&__followers {
grid-column: span 1;
text-align: center;
position: relative;
overflow: hidden;
padding-block-start: 24px;
padding-block-end: 24px;
--sparkline-gradient-top: rgba(86, 44, 252, 50%);
--sparkline-gradient-bottom: rgba(86, 44, 252, 0%);
&__foreground {
width: 100%;
height: 100%;
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
gap: 8px;
position: relative;
z-index: 1;
}
&__number {
font-size: 31px;
font-weight: 600;
line-height: 37px;
color: var(--lime);
}
&__label {
font-size: 14px;
font-weight: 600;
line-height: 17px;
color: var(--indigo-6);
}
&__footnote {
display: block;
font-weight: 400;
opacity: 0.5;
}
svg {
position: absolute;
bottom: 0;
inset-inline-end: 0;
pointer-events: none;
z-index: 0;
height: 70%;
width: auto;
path:first-child {
fill: url('#gradient') !important;
fill-opacity: 1 !important;
}
path:last-child {
stroke: var(--color-graph-primary-stroke) !important;
fill: none !important;
}
}
}
&__archetype {
grid-column: span 1;
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
text-align: center;
gap: 8px;
padding: 0;
img {
display: block;
width: 100%;
height: auto;
border-radius: 8px;
}
&__label {
padding: 16px;
padding-bottom: 8px;
font-size: 14px;
line-height: 17px;
font-weight: 600;
color: var(--lime);
}
}
&__most-used-app {
grid-column: span 1;
text-align: center;
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
gap: 8px;
box-sizing: border-box;
&__label {
font-size: 14px;
line-height: 17px;
font-weight: 600;
color: var(--indigo-6);
}
&__icon {
font-size: 14px;
line-height: 17px;
font-weight: 600;
color: var(--goldenrod-2);
}
}
&__percentile {
grid-row: span 2;
display: flex;
flex-direction: column;
align-items: center;
justify-content: space-between;
text-align: center;
text-wrap: balance;
padding: 16px 8px;
&__label {
font-size: 14px;
line-height: 17px;
}
&__number {
font-size: 54px;
font-weight: 600;
line-height: 73px;
color: var(--goldenrod-2);
}
&__footnote {
font-size: 11px;
line-height: 14px;
opacity: 0.5;
}
}
&__new-posts {
grid-column: span 2;
text-align: center;
position: relative;
overflow: hidden;
&__label {
font-size: 20px;
font-weight: 600;
line-height: 24px;
color: var(--indigo-6);
z-index: 1;
position: relative;
}
&__number {
font-size: 76px;
font-weight: 600;
line-height: 91px;
color: var(--goldenrod-2);
z-index: 1;
position: relative;
}
svg {
position: absolute;
inset-inline-start: -7px;
top: -4px;
z-index: 0;
}
}
&__most-used-hashtag {
grid-column: span 2;
text-align: center;
overflow: hidden;
&__hashtag {
font-size: 42px;
font-weight: 600;
line-height: 58px;
color: var(--indigo-6);
margin-inline-start: -100%;
margin-inline-end: -100%;
}
&__label {
font-size: 14px;
font-weight: 600;
line-height: 17px;
}
}
}
}
.annual-report-modal {
max-width: 600px;
background: var(--indigo-1);
border-radius: 16px;
display: flex;
flex-direction: column;
overflow-y: auto;
.loading-indicator .circular-progress {
color: var(--lime);
}
@media screen and (max-width: $no-columns-breakpoint) {
border-bottom: 0;
border-radius: 16px 16px 0 0;
}
}
.notification-group--annual-report {
.notification-group__icon {
color: var(--lime);
}
.notification-group__main .link-button {
font-weight: 500;
color: var(--lime);
}
}

View File

@@ -0,0 +1,300 @@
@use 'variables' as *;
html.has-modal {
&,
body {
touch-action: none;
overscroll-behavior: none;
-webkit-overflow-scrolling: auto;
scrollbar-gutter: stable;
}
body {
overflow: hidden !important;
}
}
body {
font-family: $font-sans-serif, sans-serif;
background: var(--color-bg-ambient);
font-size: 13px;
line-height: 18px;
font-weight: 400;
color: var(--color-text-primary);
text-rendering: optimizelegibility;
// Disable kerning for Japanese text to preserve monospaced alignment for readability
&:not(:lang(ja)) {
font-feature-settings: 'kern';
}
text-size-adjust: none;
-webkit-tap-highlight-color: rgba(0, 0, 0, 0%);
-webkit-tap-highlight-color: transparent;
&.system-font {
// system-ui => standard property (Chrome/Android WebView 56+, Opera 43+, Safari 11+)
// -apple-system => Safari <11 specific
// BlinkMacSystemFont => Chrome <56 on macOS specific
// Segoe UI => Windows 7/8/10
// Oxygen => KDE
// Ubuntu => Unity/Ubuntu
// Cantarell => GNOME
// Fira Sans => Firefox OS
// Droid Sans => Older Androids (<4.0)
// Helvetica Neue => Older macOS <10.11
// $font-sans-serif => web-font (Roboto) fallback and newer Androids (>=4.0)
font-family:
system-ui,
-apple-system,
BlinkMacSystemFont,
'Segoe UI',
Oxygen,
Ubuntu,
Cantarell,
'Fira Sans',
'Droid Sans',
'Helvetica Neue',
$font-sans-serif,
sans-serif;
}
&.app-body {
padding: 0;
padding-left: env(safe-area-inset-left);
padding-right: env(safe-area-inset-right);
box-sizing: border-box;
&.layout-single-column {
height: auto;
min-height: 100vh;
min-height: 100dvh;
overflow-y: scroll;
}
&.layout-multiple-columns {
position: absolute;
width: 100%;
height: 100%;
padding-bottom: env(safe-area-inset-bottom);
}
}
&.player {
padding: 0;
margin: 0;
position: absolute;
width: 100%;
height: 100%;
overflow: hidden;
& > div {
height: 100%;
}
.video-player video {
width: 100%;
height: 100%;
max-height: 100vh;
}
.media-gallery {
margin-top: 0;
height: 100% !important;
border-radius: 0;
}
.media-gallery__item {
border-radius: 0;
}
}
&.embed {
margin: 0;
padding-bottom: 0;
overflow: hidden;
}
&.admin {
padding: 0;
background: var(--color-bg-primary);
}
&.error {
position: absolute;
text-align: center;
width: 100%;
height: 100%;
padding: 0;
display: flex;
justify-content: center;
align-items: center;
.dialog {
vertical-align: middle;
margin: 20px;
&__illustration {
img {
display: block;
max-width: 470px;
width: 100%;
height: auto;
margin-top: -120px;
margin-bottom: -45px;
}
}
h1 {
font-size: 20px;
line-height: 28px;
font-weight: 400;
}
}
}
}
a {
&:focus {
border-radius: 4px;
outline: var(--outline-focus-default);
}
&:focus:not(:focus-visible) {
outline: none;
}
}
button {
font-family: inherit;
cursor: pointer;
&:focus:not(:focus-visible) {
outline: none;
}
}
.app-holder {
&,
& > div,
& > noscript {
display: flex;
width: 100%;
align-items: center;
justify-content: center;
outline: 0 !important;
}
& > noscript {
min-height: 100vh;
min-height: 100dvh;
}
}
.layout-single-column .app-holder {
&,
& > div {
min-height: 100vh;
min-height: 100dvh;
}
}
.layout-multiple-columns .app-holder {
&,
& > div {
height: 100%;
}
}
.error-boundary,
.app-holder noscript {
flex-direction: column;
font-size: 16px;
font-weight: 400;
line-height: 1.7;
color: var(--color-text-error);
text-align: center;
& > div {
max-width: 500px;
}
p {
margin-bottom: 0.85em;
&:last-child {
margin-bottom: 0;
}
}
a {
color: var(--color-text-brand);
&:hover,
&:focus,
&:active {
text-decoration: none;
}
}
&__footer {
color: var(--color-text-secondary);
font-size: 13px;
a {
color: var(--color-text-secondary);
}
}
button {
display: inline;
border: 0;
background: transparent;
color: var(--color-text-secondary);
font: inherit;
padding: 0;
margin: 0;
line-height: inherit;
cursor: pointer;
outline: 0;
transition: color 300ms linear;
text-decoration: underline;
&:hover,
&:focus,
&:active {
text-decoration: none;
}
&.copied {
color: var(--mas-status-success-color);
transition: none;
}
}
}
.logo-resources {
// Not using display: none because of https://bugs.chromium.org/p/chromium/issues/detail?id=258029
visibility: hidden;
user-select: none;
pointer-events: none;
width: 0;
height: 0;
overflow: hidden;
position: absolute;
top: 0;
inset-inline-start: 0;
z-index: -1000;
}
// NoScript adds a __ns__pop2top class to the full ancestry of blocked elements,
// to set the z-index to a high value, which messes with modals and dropdowns.
// Blocked elements can in theory only be media and frames/embeds, so they
// should only appear in statuses, under divs and articles.
body,
div,
article {
.__ns__pop2top {
z-index: unset !important;
}
}

View File

@@ -0,0 +1,5 @@
@use 'variables' as *;
.logo {
color: var(--color-text-primary);
}

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,166 @@
@use 'variables' as *;
.container-alt {
width: 700px;
margin: 0 auto;
@media screen and (width <= 740px) {
width: 100%;
margin: 0;
}
}
.logo-container {
margin: 50px auto;
h1 {
display: flex;
justify-content: center;
align-items: center;
.logo {
height: 42px;
margin-inline-end: 10px;
}
a {
display: flex;
justify-content: center;
align-items: center;
color: var(--color-text-primary);
text-decoration: none;
outline: 0;
padding: 12px 16px;
line-height: 32px;
font-weight: 500;
font-size: 14px;
}
}
}
.compose-standalone {
.compose-form {
width: 400px;
margin: 0 auto;
padding: 10px 0;
padding-bottom: 20px;
box-sizing: border-box;
@media screen and (width <= 400px) {
width: 100%;
padding: 20px;
}
}
}
.account-header {
width: 400px;
margin: 0 auto;
display: flex;
align-items: center;
gap: 10px;
font-size: 14px;
line-height: 20px;
box-sizing: border-box;
padding: 20px 0;
margin-top: 40px;
margin-bottom: 10px;
border-bottom: 1px solid var(--color-border-primary);
@media screen and (width <= 440px) {
width: 100%;
margin: 0;
padding: 20px;
}
.avatar {
width: 48px;
height: 48px;
flex: 0 0 auto;
img {
width: 100%;
height: 100%;
display: block;
margin: 0;
border-radius: var(--avatar-border-radius);
}
}
.name {
flex: 1 1 auto;
color: var(--color-text-primary);
.username {
display: block;
font-size: 16px;
line-height: 24px;
text-overflow: ellipsis;
overflow: hidden;
color: var(--color-text-primary);
}
}
.logout-link {
display: block;
font-size: 32px;
line-height: 40px;
flex: 0 0 auto;
}
}
.redirect {
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
height: 100vh;
font-size: 14px;
line-height: 18px;
&__logo {
display: flex;
align-items: center;
justify-content: center;
margin-bottom: 30px;
img {
height: 48px;
}
}
&__message {
text-align: center;
h1 {
font-size: 17px;
line-height: 22px;
font-weight: 700;
margin-bottom: 30px;
}
p {
margin-bottom: 30px;
&:last-child {
margin-bottom: 0;
}
}
a {
color: var(--color-text-brand);
font-weight: 500;
text-decoration: none;
&:hover,
&:focus,
&:active {
text-decoration: underline;
}
}
}
&__link {
margin-top: 15px;
}
}

View File

@@ -0,0 +1,228 @@
@use 'theme_utils' as utils;
:root {
--color-black: #000;
--color-grey-950: #181821;
--color-grey-800: #292938;
--color-grey-700: #444664;
--color-grey-600: #545778;
--color-grey-500: #696d91;
--color-grey-400: #8b8dac;
--color-grey-300: #b4b6cb;
--color-grey-200: #d8d9e3;
--color-grey-100: #f0f0f5;
--color-grey-50: #f0f1ff;
--color-white: #fff;
--color-indigo-600: #6147e6;
--color-indigo-400: #8886ff;
--color-indigo-300: #a5abfd;
--color-indigo-200: #c8cdfe;
--color-indigo-100: #e0e3ff;
--color-indigo-50: #f0f1ff;
--color-red-500: #ff637e;
--color-red-600: #ec003f;
--color-yellow-400: #ffb900;
--color-yellow-600: #e17100;
--color-green-400: #05df72;
--color-green-600: #00a63e;
/* TEXT TOKENS */
--color-text-primary: var(--color-grey-50);
--color-text-secondary: var(--color-grey-400);
--color-text-tertiary: var(--color-grey-500);
--color-text-on-inverted: var(--color-grey-950);
--color-text-brand: var(--color-indigo-400);
--color-text-brand-soft: color-mix(
in oklab,
var(--color-text-primary),
var(--color-text-brand)
);
--color-text-on-brand-base: var(--color-white);
--color-text-error: var(--color-red-500);
--color-text-on-error-base: var(--color-white);
--color-text-warning: var(--color-yellow-400);
--color-text-on-warning-base: var(--color-white);
--color-text-success: var(--color-green-400);
--color-text-on-success-base: var(--color-white);
--color-text-disabled: var(--color-grey-600);
--color-text-on-disabled: var(--color-grey-400);
--color-text-bookmark-highlight: var(--color-text-error);
--color-text-favourite-highlight: var(--color-text-warning);
--color-text-on-media: var(--color-white);
--color-text-status-links: color-mix(
in oklab,
var(--color-text-primary),
var(--color-text-secondary)
);
/* BACKGROUND TOKENS */
// Neutrals
--color-bg-primary: var(--color-grey-950);
--overlay-strength-secondary: 10%;
--color-bg-secondary-base: var(--color-indigo-200);
--color-bg-secondary: #{utils.css-alpha(
var(--color-bg-secondary-base),
var(--overlay-strength-secondary)
)};
--color-bg-secondary-solid: color-mix(
in srgb,
var(--color-bg-primary),
var(--color-bg-secondary-base) var(--overlay-strength-secondary)
);
--color-bg-tertiary: color-mix(
in oklab,
var(--color-bg-primary),
var(--color-bg-secondary-base) calc(2 * var(--overlay-strength-secondary))
);
// Utility
--color-bg-ambient: var(--color-bg-primary);
--color-bg-elevated: var(--color-grey-800);
--color-bg-inverted: var(--color-grey-50);
--color-bg-media-base: var(--color-black);
--color-bg-media-strength: 65%;
--color-bg-media: #{utils.css-alpha(
var(--color-bg-media-base),
var(--color-bg-media-strength)
)};
--color-bg-overlay: var(--color-bg-primary);
--color-bg-disabled: var(--color-grey-700);
// Brand
--overlay-strength-brand: 10%;
--color-bg-brand-base: var(--color-indigo-600);
--color-bg-brand-base-hover: color-mix(
in oklab,
var(--color-bg-brand-base),
black var(--overlay-strength-brand)
);
--color-bg-brand-soft: #{utils.css-alpha(
var(--color-bg-brand-base),
calc(var(--overlay-strength-brand) * 1.5)
)};
--color-bg-brand-softer: #{utils.css-alpha(
var(--color-bg-brand-base),
var(--overlay-strength-brand)
)};
// Error
--overlay-strength-error: 12%;
--color-bg-error-base: var(--color-red-600);
--color-bg-error-base-hover: color-mix(
in oklab,
var(--color-bg-error-base),
black var(--overlay-strength-error)
);
--color-bg-error-soft: #{utils.css-alpha(
var(--color-bg-error-base),
calc(var(--overlay-strength-error) * 1.5)
)};
--color-bg-error-softer: #{utils.css-alpha(
var(--color-bg-error-base),
var(--overlay-strength-error)
)};
// Warning
--overlay-strength-warning: 10%;
--color-bg-warning-base: var(--color-yellow-600);
--color-bg-warning-base-hover: color-mix(
in oklab,
var(--color-bg-warning-base),
black var(--overlay-strength-warning)
);
--color-bg-warning-soft: #{utils.css-alpha(
var(--color-bg-warning-base),
calc(var(--overlay-strength-warning) * 1.5)
)};
--color-bg-warning-softer: #{utils.css-alpha(
var(--color-bg-warning-base),
var(--overlay-strength-warning)
)};
// Success
--overlay-strength-success: 15%;
--color-bg-success-base: var(--color-green-600);
--color-bg-success-base-hover: color-mix(
in oklab,
var(--color-bg-success-base),
black var(--overlay-strength-success)
);
--color-bg-success-soft: #{utils.css-alpha(
var(--color-bg-success-base),
calc(var(--overlay-strength-success) * 1.5)
)};
--color-bg-success-softer: #{utils.css-alpha(
var(--color-bg-success-base),
var(--overlay-strength-success)
)};
/* BORDER TOKENS */
--border-strength-primary: 18%;
--color-border-primary: #{utils.css-alpha(
var(--color-indigo-200),
var(--border-strength-primary)
)};
--color-border-media: rgb(252 248 255 / 15%);
--color-border-on-bg-secondary: #{utils.css-alpha(
var(--color-indigo-200),
calc(var(--border-strength-primary) / 1.5)
)};
--color-border-on-bg-brand-softer: var(--color-border-primary);
--color-border-on-bg-error-softer: #{utils.css-alpha(
var(--color-text-error),
50%
)};
--color-border-on-bg-warning-softer: #{utils.css-alpha(
var(--color-text-warning),
50%
)};
--color-border-on-bg-success-softer: #{utils.css-alpha(
var(--color-text-success),
50%
)};
--color-border-on-bg-inverted: var(--color-border-primary);
/* SHADOW TOKENS */
--shadow-strength-primary: 80%;
--color-shadow-primary: #{utils.css-alpha(
var(--color-black),
var(--shadow-strength-primary)
)};
--dropdown-shadow:
0 20px 25px -5px var(--color-shadow-primary),
0 8px 10px -6px var(--color-shadow-primary);
--overlay-icon-shadow: drop-shadow(0 0 8px var(--color-shadow-primary));
/* GRAPHS/CHARTS TOKENS */
--color-graph-primary-stroke: var(--color-text-brand);
--color-graph-primary-fill: var(--color-bg-brand-softer);
--color-graph-warning-stroke: var(--color-text-warning);
--color-graph-warning-fill: var(--color-bg-warning-softer);
--color-graph-disabled-stroke: var(--color-text-disabled);
--color-graph-disabled-fill: var(--color-bg-disabled);
/* LEGACY TOKENS */
--rich-text-container-color: rgb(87 24 60 / 100%);
--rich-text-text-color: rgb(255 175 212 / 100%);
--rich-text-decorations-color: rgb(128 58 95 / 100%);
/* MISCELLANEOUS */
--outline-focus-default: 2px solid var(--color-text-brand);
--avatar-border-radius: 8px;
}
body {
// Variable for easily inverting directional UI elements,
--text-x-direction: 1;
&.rtl {
--text-x-direction: -1;
}
}

View File

@@ -0,0 +1,120 @@
@use 'variables' as *;
.dashboard__counters {
display: flex;
flex-wrap: wrap;
margin: 0 -5px;
margin-bottom: 20px;
& > div {
box-sizing: border-box;
flex: 0 0 33.333%;
padding: 0 5px;
margin-bottom: 10px;
& > div,
& > a {
padding: 20px;
background: var(--color-bg-primary);
border-radius: 4px;
border: 1px solid var(--color-border-primary);
box-sizing: border-box;
height: 100%;
}
& > a {
text-decoration: none;
color: inherit;
display: block;
&:hover,
&:focus,
&:active {
background: var(--color-bg-brand-softer);
}
}
}
&__num,
&__text {
text-align: center;
font-weight: 500;
font-size: 24px;
color: var(--color-text-primary);
margin-bottom: 20px;
line-height: 30px;
}
&__text {
font-size: 18px;
}
&__label {
font-size: 14px;
color: var(--color-text-secondary);
text-align: center;
font-weight: 500;
}
}
.dashboard {
display: grid;
grid-template-columns: minmax(0, 1fr) minmax(0, 1fr) minmax(0, 1fr);
gap: 10px;
@media screen and (width <= 1350px) {
grid-template-columns: minmax(0, 1fr) minmax(0, 1fr);
}
&__item {
&--span-double-column {
grid-column: span 2;
}
&--span-double-row {
grid-row: span 2;
}
h4 {
padding-top: 20px;
}
}
&__quick-access {
display: flex;
align-items: baseline;
border-radius: 4px;
background: var(--color-bg-brand-base);
color: var(--color-text-on-brand-base);
transition: all 100ms ease-in;
font-size: 14px;
padding: 8px 16px;
text-decoration: none;
margin-bottom: 4px;
&:active,
&:focus,
&:hover {
background-color: var(--color-bg-brand-base-hover);
transition: all 200ms ease-out;
}
&.positive {
background: var(--color-bg-success-softer);
color: var(--color-text-success);
}
&.negative {
background: var(--color-bg-error-softer);
color: var(--color-text-error);
}
span {
flex: 1 1 auto;
}
strong {
font-weight: 700;
}
}
}

View File

@@ -0,0 +1,248 @@
@use 'variables' as *;
.emoji-mart {
font-size: 13px;
display: inline-block;
&,
* {
box-sizing: border-box;
line-height: 1.15;
}
.emoji-mart-emoji {
padding: 6px;
}
}
.emoji-mart-bar {
&:first-child {
background: var(--color-bg-tertiary);
border-bottom: 1px solid var(--color-border-primary);
}
}
.emoji-mart-anchors {
display: flex;
justify-content: space-between;
padding: 0 6px;
line-height: 0;
}
.emoji-mart-anchor {
position: relative;
flex: 1;
text-align: center;
padding: 12px 4px;
overflow: hidden;
transition: color 0.1s ease-out;
cursor: pointer;
background: transparent;
border: 0;
color: var(--color-text-secondary);
&:hover {
color: color-mix(
in oklab,
var(--color-text-primary),
var(--color-text-secondary)
);
}
}
.emoji-mart-anchor-selected {
color: var(--color-text-brand);
&:hover {
color: var(--color-text-brand-soft);
}
.emoji-mart-anchor-bar {
bottom: -1px;
}
}
.emoji-mart-anchor-bar {
position: absolute;
bottom: -5px;
inset-inline-start: 0;
width: 100%;
height: 4px;
background-color: var(--color-text-brand);
}
.emoji-mart-anchors {
i {
display: inline-block;
width: 100%;
max-width: 22px;
}
svg {
fill: currentColor;
max-height: 18px;
}
}
.emoji-mart-scroll {
overflow-y: scroll;
height: 270px;
max-height: 35vh;
padding: 0 6px 6px;
will-change: transform;
}
.emoji-mart-search {
padding: 10px;
padding-inline-end: 45px;
position: relative;
input {
font-size: 16px;
font-weight: 400;
padding: 7px 9px;
padding-inline-end: 25px;
font-family: inherit;
display: block;
width: 100%;
background: var(--color-bg-secondary);
color: var(--color-text-secondary);
border: 1px solid var(--color-border-primary);
border-radius: 4px;
&::-moz-focus-inner {
border: 0;
}
&:active,
&:focus {
outline: none !important;
border-width: 1px !important;
}
&::-webkit-search-cancel-button {
display: none;
}
}
}
.emoji-mart-search-icon {
position: absolute;
top: 18px;
inset-inline-end: 45px + 5px;
z-index: 2;
padding: 2px 5px 1px;
border: 0;
background: none;
transition: all 100ms linear;
transition-property: opacity;
pointer-events: auto;
&:disabled {
cursor: default;
pointer-events: none;
}
svg {
fill: currentColor;
}
}
.emoji-mart-category .emoji-mart-emoji {
cursor: pointer;
span {
z-index: 1;
position: relative;
text-align: center;
display: inline-flex !important;
align-items: center;
justify-content: center;
}
&:hover::before {
z-index: -1;
content: '';
position: absolute;
top: 0;
inset-inline-start: 0;
width: 100%;
height: 100%;
background-color: var(--color-bg-brand-softer);
border-radius: 100%;
}
}
.emoji-mart-category-label {
z-index: 2;
position: relative;
position: -webkit-sticky;
position: sticky;
top: 0;
span {
display: block;
width: 100%;
font-weight: 500;
padding: 5px 6px;
}
}
/* For screenreaders only, via https://stackoverflow.com/a/19758620 */
.emoji-mart-sr-only {
position: absolute;
width: 1px;
height: 1px;
padding: 0;
margin: -1px;
overflow: hidden;
clip-path: inset(50%);
border: 0;
}
.emoji-mart-category-list {
margin: 0;
padding: 0;
}
.emoji-mart-category-list li {
list-style: none;
margin: 0;
padding: 0;
display: inline-block;
}
.emoji-mart-emoji {
position: relative;
display: inline-block;
background: transparent;
border: 0;
padding: 0;
font-size: 0;
span {
width: 22px;
height: 22px;
}
}
.emoji-mart-no-results {
font-size: 14px;
color: var(--color-text-tertiary);
text-align: center;
padding: 5px 6px;
padding-top: 70px;
.emoji-mart-no-results-label {
margin-top: 0.2em;
}
.emoji-mart-emoji:hover::before {
cursor: default;
content: none;
}
}
.emoji-mart-preview {
display: none;
}

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,19 @@
.no-list {
list-style: none;
li {
display: inline-block;
margin: 0 5px;
}
}
.recovery-codes {
list-style: none;
margin: 0 auto;
li {
font-size: 125%;
line-height: 1.5;
letter-spacing: 1px;
}
}

View File

@@ -0,0 +1,53 @@
@use 'variables' as *;
.modal-layout {
background: var(--color-bg-brand-softer);
display: flex;
flex-direction: column;
height: 100vh;
padding: 0;
}
.modal-layout__mastodon {
display: flex;
flex: 1;
flex-direction: column;
justify-content: flex-end;
> div {
flex: 1;
max-height: 235px;
position: relative;
img {
max-height: 100%;
max-width: 100%;
height: 100%;
position: absolute;
bottom: 0;
inset-inline-start: 0;
}
}
}
@media screen and (width <= 600px) {
.account-header {
margin-top: 0;
}
}
.with-zig-zag-decoration {
&::after {
content: '';
position: absolute;
inset: auto 0 0;
height: 32px;
background-color: var(--color-bg-brand-softer);
/* Decorative zig-zag pattern at the bottom of the page */
mask-image: url('data:image/svg+xml;utf8,<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 234.80078 31.757813" width="234.80078" height="31.757812"><path d="M19.599609 0c-1.05 0-2.10039.375-2.90039 1.125L0 16.925781v14.832031h234.80078V17.025391l-16.5-15.900391c-1.6-1.5-4.20078-1.5-5.80078 0l-13.80078 13.099609c-1.6 1.5-4.19883 1.5-5.79883 0L179.09961 1.125c-1.6-1.5-4.19883-1.5-5.79883 0L159.5 14.224609c-1.6 1.5-4.20078 1.5-5.80078 0L139.90039 1.125c-1.6-1.5-4.20078-1.5-5.80078 0l-13.79883 13.099609c-1.6 1.5-4.20078 1.5-5.80078 0L100.69922 1.125c-1.600001-1.5-4.198829-1.5-5.798829 0l-13.59961 13.099609c-1.6 1.5-4.200781 1.5-5.800781 0L61.699219 1.125c-1.6-1.5-4.198828-1.5-5.798828 0L42.099609 14.224609c-1.6 1.5-4.198828 1.5-5.798828 0L22.5 1.125C21.7.375 20.649609 0 19.599609 0z" fill="black"/></svg>');
mask-position: bottom;
mask-repeat: repeat-x;
z-index: -1;
}
}

View File

@@ -0,0 +1,232 @@
@use 'sass:color';
@use 'variables' as *;
.poll {
margin-top: 16px;
font-size: 14px;
li {
margin-bottom: 10px;
position: relative;
}
&__chart {
border-radius: 4px;
display: block;
background: rgb(from var(--color-text-brand) r g b / 60%);
height: 5px;
min-width: 1%;
&.leading {
background: var(--color-text-brand);
}
}
progress {
border: 0;
display: block;
width: 100%;
height: 5px;
appearance: none;
background: transparent;
&::-webkit-progress-bar {
background: transparent;
}
// Those rules need to be entirely separate or they won't work, hence the
// duplication
&::-moz-progress-bar {
border-radius: 4px;
background: rgb(from var(--color-text-brand) r g b / 60%);
}
&::-webkit-progress-value {
border-radius: 4px;
background: rgb(from var(--color-text-brand) r g b / 60%);
}
}
&__option {
position: relative;
display: flex;
align-items: flex-start;
gap: 8px;
padding: 6px 0;
line-height: 18px;
cursor: default;
overflow: hidden;
&__text {
display: inline-block;
overflow-wrap: break-word;
max-width: calc(100% - 45px - 25px);
}
input[type='radio'],
input[type='checkbox'] {
display: none;
}
.autosuggest-input {
flex: 1 1 auto;
}
input[type='text'] {
display: block;
box-sizing: border-box;
width: 100%;
font-size: 14px;
color: var(--color-text-primary);
outline: 0;
font-family: inherit;
background: var(--color-bg-primary);
border: 1px solid var(--color-text-secondary);
border-radius: 4px;
padding: 8px 12px;
&:focus {
border-color: var(--color-text-brand);
}
@media screen and (width <= 600px) {
font-size: 16px;
line-height: 24px;
letter-spacing: 0.5px;
}
}
&.selectable {
cursor: pointer;
}
&.editable,
&.disabled {
align-items: center;
overflow: visible;
}
}
&__input {
display: block;
position: relative;
border: 1px solid var(--color-text-secondary);
box-sizing: border-box;
width: 17px;
height: 17px;
border-radius: 50%;
flex: 0 0 auto;
&.checkbox {
border-radius: 4px;
}
&:active,
&:focus,
&:hover {
border-color: var(--color-text-success);
border-width: 4px;
}
&.active {
background-color: var(--color-bg-success-base);
border-color: var(--color-text-success);
}
&::-moz-focus-inner {
outline: 0 !important;
border: 0;
}
&:focus,
&:active {
outline: 0 !important;
}
&.disabled {
border-color: var(--color-text-disabled);
&.active {
background: var(--color-text-disabled);
}
&:active,
&:focus,
&:hover {
border-color: var(--color-text-disabled);
border-width: 1px;
}
}
}
&__option.editable &__input,
&__option.disabled &__input {
&:active,
&:focus,
&:hover {
border-color: var(--color-text-primary);
border-width: 1px;
}
}
&__number {
display: inline-block;
width: 45px;
font-weight: 700;
flex: 0 0 45px;
}
&__voted {
padding: 0 5px;
display: inline-block;
&__mark {
font-size: 18px;
}
}
&__footer {
padding-top: 6px;
padding-bottom: 5px;
color: var(--color-text-tertiary);
}
&__link {
display: inline;
background: transparent;
padding: 0;
margin: 0;
border: 0;
color: var(--color-text-tertiary);
text-decoration: underline;
font-size: inherit;
&:hover {
text-decoration: none;
}
&:active,
&:focus {
background-color: var(--color-bg-secondary);
}
}
.button {
height: 36px;
padding: 0 16px;
margin-inline-end: 10px;
font-size: 14px;
}
}
.muted .poll {
color: var(--color-text-tertiary);
&__chart {
background: rgb(from var(--color-text-brand) r g b / 40%);
&.leading {
background: rgb(from var(--color-text-brand) r g b / 60%);
}
}
}

View File

@@ -0,0 +1,58 @@
/* http://meyerweb.com/eric/tools/css/reset/
v2.0 | 20110126
License: none (public domain)
*/
html, body, div, span, applet, object, iframe,
h1, h2, h3, h4, h5, h6, p, blockquote, pre,
a, abbr, acronym, address, big, cite, code,
del, dfn, em, img, ins, kbd, q, s, samp,
small, strike, strong, sub, sup, tt, var,
b, u, i, center,
dl, dt, dd, ol, ul, li,
fieldset, form, label, legend,
table, caption, tbody, tfoot, thead, tr, th, td,
article, aside, canvas, details, embed,
figure, figcaption, footer, header, hgroup,
menu, nav, output, ruby, section, summary,
time, mark, audio, video {
margin: 0;
padding: 0;
border: 0;
font-size: 100%;
font: inherit;
vertical-align: baseline;
}
/* HTML5 display-role reset for older browsers */
article, aside, details, figcaption, figure,
footer, header, hgroup, menu, nav, section {
display: block;
}
body {
line-height: 1;
}
ol, ul {
list-style: none;
}
blockquote, q {
quotes: none;
}
blockquote:before, blockquote:after,
q:before, q:after {
content: '';
content: none;
}
table {
border-collapse: collapse;
border-spacing: 0;
}
html:has(body.custom-scrollbars) {
scrollbar-color: var(--color-text-secondary) var(--color-bg-secondary);
}

View File

@@ -0,0 +1,116 @@
.status__content__text,
.e-content,
.edit-indicator__content,
.reply-indicator__content {
code {
background: var(--rich-text-container-color);
padding: 4px;
border-radius: 4px;
color: var(--rich-text-text-color);
font-size: 0.85em;
}
pre {
background: var(--rich-text-container-color);
padding: 8px;
border-radius: 4px;
color: var(--rich-text-text-color);
code {
padding: 0;
background: transparent;
}
}
pre,
blockquote {
margin-bottom: 22px;
white-space: pre-wrap;
unicode-bidi: plaintext;
&:last-child {
margin-bottom: 0;
}
}
blockquote {
padding-inline-start: 32px;
color: var(--rich-text-text-color);
white-space: normal;
position: relative;
&::before {
display: block;
content: '';
width: 24px;
height: 20px;
mask-image: url('@/images/quote.svg');
background-color: var(--rich-text-decorations-color);
position: absolute;
inset-inline-start: 0;
top: 0;
}
blockquote {
margin-top: 4px;
border-inline-start: 3px solid var(--rich-text-decorations-color);
padding-inline-start: 16px;
&::before {
display: none;
}
}
p:last-of-type {
margin-bottom: 0;
}
}
& > ul,
& > ol {
margin-bottom: 22px;
&:last-child {
margin-bottom: 0;
}
}
b,
strong {
font-weight: 700;
}
em,
i {
font-style: italic;
}
ul,
ol {
padding-inline-start: 24px;
li {
padding-inline-start: 8px;
&::marker {
text-align: end;
}
}
p {
margin: 0;
}
}
ul {
list-style-type: '';
li::marker {
text-align: start;
}
}
ol {
list-style-type: decimal;
}
}

View File

@@ -0,0 +1,50 @@
@use 'variables' as *;
body.rtl {
direction: rtl;
.reactions-bar {
direction: rtl;
}
.announcements__mastodon,
.drawer__inner__mastodon > img {
transform: scaleX(-1);
}
.compose-form .autosuggest-textarea__textarea {
padding-right: 10px;
padding-left: 10px + 22px;
}
.columns-area {
direction: rtl;
}
.account__avatar-wrapper {
float: right;
}
.column-header__setting-arrows {
float: left;
}
.admin-wrapper {
direction: rtl;
}
.react-swipeable-view-container > * {
direction: rtl;
}
.column-back-button__icon {
transform: scale(-1, 1);
}
.dismissable-banner,
.warning-banner {
&__action {
float: left;
}
}
}

View File

@@ -0,0 +1,375 @@
@use 'variables' as *;
.table {
width: 100%;
max-width: 100%;
border-spacing: 0;
border-collapse: collapse;
th,
td {
padding: 8px;
line-height: 18px;
vertical-align: top;
border-bottom: 1px solid var(--color-border-primary);
text-align: start;
background: var(--color-bg-primary);
&.critical {
font-weight: 700;
color: var(--color-text-warning);
}
}
& > thead > tr > th {
vertical-align: bottom;
font-weight: 500;
}
& > tbody > tr > th {
font-weight: 500;
}
& > tbody > tr:nth-child(odd) > td,
& > tbody > tr:nth-child(odd) > th {
background: var(--color-bg-primary);
}
& > tbody > tr:last-child > td,
& > tbody > tr:last-child > th {
border-bottom: 0;
}
a {
color: var(--color-text-secondary);
text-decoration: none;
&:hover {
color: var(--color-text-brand);
}
}
strong {
font-weight: 500;
@each $lang in $cjk-langs {
&:lang(#{$lang}) {
font-weight: 700;
}
}
}
&.inline-table {
& > tbody > tr:nth-child(odd) {
& > td,
& > th {
background: transparent;
}
}
& > tbody > tr:first-child {
& > td,
& > th {
border-top: 0;
}
}
}
&.horizontal-table {
border-collapse: collapse;
border-style: hidden;
& > tbody > tr > th,
& > tbody > tr > td {
padding: 11px 10px;
background: transparent;
border: 1px solid var(--color-border-primary);
color: var(--color-text-primary);
}
& > tbody > tr > th {
color: var(--color-text-secondary);
font-weight: 600;
}
}
&.batch-table {
& > thead > tr > th {
background: var(--color-bg-primary);
border-top: 1px solid var(--color-border-primary);
border-bottom: 1px solid var(--color-border-primary);
&:first-child {
border-radius: 4px 0 0;
border-inline-start: 1px solid var(--color-border-primary);
}
&:last-child {
border-radius: 0 4px 0 0;
border-inline-end: 1px solid var(--color-border-primary);
}
}
}
&--invites tbody td {
vertical-align: middle;
}
}
.table-wrapper {
overflow: auto;
margin-bottom: 20px;
}
samp {
font-family: $font-monospace, monospace;
}
button.table-action-link {
background: transparent;
border: 0;
font: inherit;
}
button.table-action-link,
a.table-action-link {
text-decoration: none;
display: inline-block;
margin-inline-end: 5px;
padding: 0 10px;
color: var(--color-text-secondary);
font-weight: 500;
white-space: nowrap;
&:hover {
color: var(--color-text-brand);
}
&:first-child {
padding-inline-start: 0;
}
}
.batch-table {
&--no-toolbar {
.batch-table__toolbar {
position: static;
height: 4px;
border-bottom: none;
}
}
&__toolbar,
&__row {
display: flex;
&__select {
box-sizing: border-box;
padding: 8px 16px;
cursor: pointer;
min-height: 100%;
input {
margin-top: 8px;
}
&--aligned {
display: flex;
align-items: center;
input {
margin-top: 0;
}
}
}
&__actions,
&__content {
padding: 8px 0;
padding-inline-end: 16px;
flex: 1 1 auto;
}
}
&__toolbar {
position: sticky;
top: 0;
z-index: 200;
border: 1px solid var(--color-border-primary);
background: var(--color-bg-primary);
border-radius: 4px 4px 0 0;
height: 47px;
align-items: center;
&__actions {
text-align: end;
padding-inline-end: 16px - 5px;
.table-action-link {
padding: 0;
}
}
}
&__select-all {
background: var(--color-bg-primary);
height: 47px;
align-items: center;
justify-content: center;
border: 1px solid var(--color-border-primary);
border-top: 0;
color: var(--color-text-primary);
display: none;
&.active {
display: flex;
}
.selected,
.not-selected {
display: none;
&.active {
display: block;
}
}
strong {
font-weight: 700;
}
span {
padding: 8px;
display: inline-block;
}
button {
background: transparent;
border: 0;
font: inherit;
color: var(--color-text-brand);
border-radius: 4px;
font-weight: 700;
padding: 8px;
&:hover,
&:focus,
&:active {
background: var(--color-bg-secondary);
}
}
}
&__form {
padding: 16px;
border: 1px solid var(--color-border-primary);
border-top: 0;
background: var(--color-bg-primary);
.fields-row {
padding-top: 0;
margin-bottom: 0;
}
}
&__row {
border: 1px solid var(--color-border-primary);
border-top: 0;
background: var(--color-bg-primary);
@media screen and (max-width: $no-gap-breakpoint) {
.optional &:first-child {
border-top: 1px solid var(--color-border-primary);
}
}
&:last-child {
border-radius: 0 0 4px 4px;
}
&__content {
padding-top: 12px;
padding-bottom: 16px;
overflow: hidden;
&--unpadded {
padding: 0;
}
&--padded {
padding: 12px 16px 16px;
}
&--with-image {
display: flex;
align-items: center;
}
&__image {
flex: 0 0 auto;
display: flex;
justify-content: center;
align-items: center;
margin-inline-end: 10px;
.emojione {
width: 32px;
height: 32px;
}
}
&__text {
flex: 1 1 auto;
}
&__quote {
padding: 12px;
padding-top: 0;
}
&__extra {
flex: 0 0 auto;
text-align: end;
color: var(--color-text-secondary);
font-weight: 500;
}
}
.directory__tag {
margin: 0;
width: 100%;
a {
background: transparent;
border-radius: 0;
}
}
}
&.optional .batch-table__toolbar,
&.optional .batch-table__row__select {
@media screen and (max-width: $no-gap-breakpoint) {
display: none;
}
}
// Reset the status card to not have borders, background or padding when
// inline in the table of statuses
.batch-table__row__content > .status__card {
border: none;
background: none;
padding: 0;
}
@media screen and (width <= 870px) {
.accounts-table tbody td.optional {
display: none;
}
}
}
.one-liner {
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
}

View File

@@ -0,0 +1,183 @@
@use 'sass:color';
@use 'variables' as *;
.directory {
&__tag {
box-sizing: border-box;
margin-bottom: 10px;
& > a,
& > div {
display: flex;
align-items: center;
justify-content: space-between;
border: 1px solid var(--color-border-primary);
border-radius: 4px;
padding: 15px;
text-decoration: none;
color: inherit;
box-shadow: 0 0 15px var(--color-shadow-primary);
}
& > a {
&:hover,
&:active,
&:focus {
background: var(--color-bg-primary);
}
}
&.active > a {
background: var(--color-bg-brand-base);
cursor: default;
}
&.disabled > div {
opacity: 0.5;
cursor: default;
}
h4 {
flex: 1 1 auto;
font-size: 18px;
font-weight: 700;
color: var(--color-text-primary);
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
.fa {
color: var(--color-text-secondary);
}
small {
display: block;
font-weight: 400;
font-size: 15px;
margin-top: 8px;
color: var(--color-text-secondary);
}
}
&.active h4 {
&,
.fa,
small,
.trends__item__current {
color: var(--color-text-primary);
}
}
.avatar-stack {
flex: 0 0 auto;
width: (36px + 4px) * 3;
}
&.active .avatar-stack .account__avatar {
border-color: var(--color-text-brand);
}
.trends__item__current {
padding-inline-end: 0;
}
}
}
.accounts-table {
width: 100%;
.account {
max-width: calc(56px + 30ch);
padding: 0;
border: 0;
}
strong {
font-weight: 700;
}
thead th {
text-align: center;
text-transform: uppercase;
color: var(--color-text-secondary);
font-weight: 700;
padding: 10px;
&:first-child {
text-align: start;
}
}
tbody td {
padding: 15px 0;
vertical-align: middle;
border-bottom: 1px solid var(--color-border-primary);
}
tbody tr:last-child td {
border-bottom: 0;
}
&__count {
width: 120px;
text-align: center;
font-size: 15px;
font-weight: 500;
color: var(--color-text-primary);
small {
display: block;
color: var(--color-text-secondary);
font-weight: 400;
font-size: 14px;
}
}
tbody td.accounts-table__extra {
width: 120px;
text-align: end;
color: var(--color-text-secondary);
padding-inline-end: 16px;
a {
text-decoration: none;
color: inherit;
&:focus,
&:hover,
&:active {
color: var(--color-text-brand);
}
}
}
&__comment {
width: 50%;
vertical-align: initial !important;
}
tbody td.accounts-table__interrelationships {
width: 21px;
padding-inline-end: 16px;
}
.icon {
&.active {
color: var(--color-text-brand);
}
&.passive {
color: var(--color-text-warning);
}
&.active.passive {
color: var(--color-text-success);
}
}
@media screen and (max-width: $no-gap-breakpoint) {
tbody td.optional {
display: none;
}
}
}

View File

@@ -31,9 +31,10 @@ class IpBlock < ApplicationRecord
after_commit :reset_cache
def to_log_human_identifier
def to_cidr
"#{ip}/#{ip.prefix}"
end
alias to_log_human_identifier to_cidr
class << self
def blocked?(remote_ip)

View File

@@ -9,6 +9,6 @@ class REST::Admin::IpBlockSerializer < ActiveModel::Serializer
end
def ip
"#{object.ip}/#{object.ip.prefix}"
object.to_cidr
end
end

View File

@@ -18,5 +18,5 @@
domain: @domain_block.domain
.actions
= link_to t('.cancel'), admin_instances_path, class: 'button button-tertiary'
= link_to t('.cancel'), admin_instances_path, class: 'button button-secondary'
= f.button :submit, t('.confirm'), class: 'button button--dangerous', name: :confirm

View File

@@ -3,7 +3,7 @@
= f.check_box :ip_block_ids, { multiple: true, include_hidden: false }, ip_block.id
.batch-table__row__content.pending-account
.pending-account__header
%samp= link_to "#{ip_block.ip}/#{ip_block.ip.prefix}", admin_accounts_path(ip: "#{ip_block.ip}/#{ip_block.ip.prefix}")
%samp= link_to ip_block.to_cidr, admin_accounts_path(ip: ip_block.to_cidr)
- if ip_block.comment.present?
·
= ip_block.comment

View File

@@ -76,7 +76,7 @@
%hr.spacer/
.actions
= link_to t('admin.reports.cancel'), admin_report_path(@report), class: 'button button-tertiary'
= link_to t('admin.reports.cancel'), admin_report_path(@report), class: 'button button-secondary'
= form.button t('admin.reports.confirm'),
name: :confirm,
class: 'button',

View File

@@ -1,7 +1,7 @@
.announcements-list__item
- if can?(:update, role)
= link_to edit_admin_role_path(role), class: 'announcements-list__item__title' do
%span.user-role
%span
= material_symbol 'group'
- if role.everyone?
@@ -10,13 +10,12 @@
= role.name
- else
%span.announcements-list__item__title
%span.user-role
= material_symbol 'group'
= material_symbol 'group'
- if role.everyone?
= t('admin.roles.everyone')
- else
= role.name
- if role.everyone?
= t('admin.roles.everyone')
- else
= role.name
.announcements-list__item__action-bar
.announcements-list__item__meta

View File

@@ -27,4 +27,4 @@
.stacked-actions
- accept_path = @invite_code.present? ? public_invite_url(invite_code: @invite_code, accept: @accept_token) : new_user_registration_path(accept: @accept_token)
= link_to t('auth.rules.accept'), accept_path, class: 'button'
= link_to t('auth.rules.back'), root_path, class: 'button button-tertiary'
= link_to t('auth.rules.back'), root_path, class: 'button button-secondary'

View File

@@ -1,7 +1,7 @@
- content_for :header_tags do
= flavoured_vite_typescript_tag 'public.tsx', crossorigin: 'anonymous'
- content_for :body_classes, 'modal-layout compose-standalone'
- content_for :body_classes, 'modal-layout with-zig-zag-decoration compose-standalone'
- content_for :content do
- if user_signed_in? && !@hide_header

View File

@@ -11,5 +11,5 @@
.simple_form
.actions
= link_to t('generic.cancel'), settings_import_path(@bulk_import), method: :delete, class: 'button button-tertiary'
= link_to t('generic.cancel'), settings_import_path(@bulk_import), method: :delete, class: 'button button-secondary'
= link_to t('generic.confirm'), confirm_settings_import_path(@bulk_import), method: :post, class: 'button'

View File

@@ -14,3 +14,9 @@ nan:
username: 用者ê名
user/invite_request:
text: 原因
errors:
models:
terms_of_service:
attributes:
effective_date:
too_soon: 傷緊ah著khah uànn佇 %{date}

View File

@@ -477,7 +477,7 @@ da:
no_file: Ingen fil valgt
export_domain_blocks:
import:
description_html: En liste over domæneblokeringer er ved at blive importeret. Gennemgå listen meget nøje, især hvis man ikke selv har oprettet den.
description_html: Du er ved at importere en liste over domæneblokeringer. Gennemgå denne liste meget omhyggeligt, især hvis du ikke selv har udarbejdet den.
existing_relationships_warning: Eksisterende følge-relationer
private_comment_description_html: 'For at man lettere kan holde styr på, hvorfra importerede blokeringer kommer, oprettes disse med flg. private kommentar: <q>%{comment}</q>'
private_comment_template: Importeret fra %{source} d. %{date}
@@ -514,7 +514,7 @@ da:
select_capabilities: Vælg kapaciteter
sign_in: Log ind
status: Status
title: Fediverse Auxiliary Service Providers
title: Udbydere af Fediverse-hjælpetjenester
title: FASP
follow_recommendations:
description_html: "<strong>Følg-anbefalinger hjælpe nye brugere til hurtigt at finde interessant indhold</strong>. Når en bruger ikke har interageret nok med andre til at generere personlige følg-anbefalinger, anbefales disse konti i stedet. De revurderes dagligt baseret på en blanding af konti med de flest nylige engagementer og fleste lokale følger-antal for et givet sprog."
@@ -1186,7 +1186,7 @@ da:
new_trending_statuses:
title: Indlæg, der trender
new_trending_tags:
title: Hashtags, der trender
title: Populære hashtags
subject: Nye tendenser klar til gennemgang på %{instance}
aliases:
add_new: Opret alias

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