Merge branch 'embedded-status' into 'develop'
Native status embeds from Soapbox See merge request soapbox-pub/soapbox-fe!1730
This commit is contained in:
commit
6ee6c8aa44
|
@ -1,5 +1,8 @@
|
|||
import loadPolyfills from './soapbox/load_polyfills';
|
||||
|
||||
// Load iframe event listener
|
||||
require('./soapbox/iframe');
|
||||
|
||||
// @ts-ignore
|
||||
require.context('./images/', true);
|
||||
|
||||
|
|
|
@ -0,0 +1,63 @@
|
|||
import React, { useCallback, useEffect, useRef, useState } from 'react';
|
||||
|
||||
interface ISafeEmbed {
|
||||
/** Styles for the outer frame element. */
|
||||
className?: string,
|
||||
/** Space-separate list of restrictions to ALLOW for the iframe. */
|
||||
sandbox?: string,
|
||||
/** Unique title for the iframe. */
|
||||
title: string,
|
||||
/** HTML body to embed. */
|
||||
html?: string,
|
||||
}
|
||||
|
||||
/** Safely embeds arbitrary HTML content on the page (by putting it in an iframe). */
|
||||
const SafeEmbed: React.FC<ISafeEmbed> = ({
|
||||
className,
|
||||
sandbox,
|
||||
title,
|
||||
html,
|
||||
}) => {
|
||||
const iframe = useRef<HTMLIFrameElement>(null);
|
||||
const [height, setHeight] = useState<number | undefined>(undefined);
|
||||
|
||||
const handleMessage = useCallback((e: MessageEvent) => {
|
||||
if (e.data?.type === 'setHeight') {
|
||||
setHeight(e.data?.height);
|
||||
}
|
||||
}, []);
|
||||
|
||||
useEffect(() => {
|
||||
const iframeDocument = iframe.current?.contentWindow?.document;
|
||||
|
||||
if (iframeDocument && html) {
|
||||
iframeDocument.open();
|
||||
iframeDocument.write(html);
|
||||
iframeDocument.close();
|
||||
iframeDocument.body.style.margin = '0';
|
||||
|
||||
iframe.current?.contentWindow?.addEventListener('message', handleMessage);
|
||||
|
||||
const innerFrame = iframeDocument.querySelector('iframe');
|
||||
if (innerFrame) {
|
||||
innerFrame.width = '100%';
|
||||
}
|
||||
}
|
||||
|
||||
return () => {
|
||||
iframe.current?.contentWindow?.removeEventListener('message', handleMessage);
|
||||
};
|
||||
}, [iframe.current, html]);
|
||||
|
||||
return (
|
||||
<iframe
|
||||
ref={iframe}
|
||||
className={className}
|
||||
sandbox={sandbox}
|
||||
height={height}
|
||||
title={title}
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
||||
export default SafeEmbed;
|
|
@ -18,6 +18,7 @@ import EmojiButtonWrapper from 'soapbox/components/emoji-button-wrapper';
|
|||
import StatusActionButton from 'soapbox/components/status-action-button';
|
||||
import DropdownMenuContainer from 'soapbox/containers/dropdown_menu_container';
|
||||
import { useAppDispatch, useAppSelector, useFeatures, useOwnAccount, useSettings, useSoapboxConfig } from 'soapbox/hooks';
|
||||
import { isLocal } from 'soapbox/utils/accounts';
|
||||
import { getReactForStatus, reduceEmoji } from 'soapbox/utils/emoji_reacts';
|
||||
|
||||
import type { Menu } from 'soapbox/components/dropdown_menu';
|
||||
|
@ -362,7 +363,7 @@ const StatusActionBar: React.FC<IStatusActionBar> = ({
|
|||
icon: require('@tabler/icons/link.svg'),
|
||||
});
|
||||
|
||||
if (features.embeds) {
|
||||
if (features.embeds && isLocal(status.account as Account)) {
|
||||
menu.push({
|
||||
text: intl.formatMessage(messages.embed),
|
||||
action: handleEmbed,
|
||||
|
|
|
@ -18,7 +18,7 @@ import StatusActionBar from './status-action-bar';
|
|||
import StatusMedia from './status-media';
|
||||
import StatusReplyMentions from './status-reply-mentions';
|
||||
import StatusContent from './status_content';
|
||||
import { HStack, Text } from './ui';
|
||||
import { Card, HStack, Text } from './ui';
|
||||
|
||||
import type { Map as ImmutableMap } from 'immutable';
|
||||
import type {
|
||||
|
@ -47,7 +47,9 @@ export interface IStatus {
|
|||
featured?: boolean,
|
||||
hideActionBar?: boolean,
|
||||
hoverable?: boolean,
|
||||
variant?: 'default' | 'rounded',
|
||||
withDismiss?: boolean,
|
||||
accountAction?: React.ReactElement,
|
||||
}
|
||||
|
||||
const Status: React.FC<IStatus> = (props) => {
|
||||
|
@ -64,8 +66,10 @@ const Status: React.FC<IStatus> = (props) => {
|
|||
unread,
|
||||
group,
|
||||
hideActionBar,
|
||||
variant = 'rounded',
|
||||
withDismiss,
|
||||
} = props;
|
||||
|
||||
const intl = useIntl();
|
||||
const history = useHistory();
|
||||
const dispatch = useAppDispatch();
|
||||
|
@ -293,6 +297,8 @@ const Status: React.FC<IStatus> = (props) => {
|
|||
|
||||
const statusUrl = `/@${actualStatus.getIn(['account', 'acct'])}/posts/${actualStatus.id}`;
|
||||
|
||||
const accountAction = props.accountAction || reblogElement;
|
||||
|
||||
return (
|
||||
<HotKeys handlers={handlers} data-testid='status'>
|
||||
<div
|
||||
|
@ -316,8 +322,10 @@ const Status: React.FC<IStatus> = (props) => {
|
|||
</div>
|
||||
)}
|
||||
|
||||
<div
|
||||
<Card
|
||||
variant={variant}
|
||||
className={classNames('status__wrapper', `status-${actualStatus.visibility}`, {
|
||||
'py-6 sm:p-5': variant === 'rounded',
|
||||
'status-reply': !!status.in_reply_to_id,
|
||||
muted,
|
||||
read: unread === false,
|
||||
|
@ -332,8 +340,8 @@ const Status: React.FC<IStatus> = (props) => {
|
|||
id={String(actualStatus.getIn(['account', 'id']))}
|
||||
timestamp={actualStatus.created_at}
|
||||
timestampUrl={statusUrl}
|
||||
action={reblogElement}
|
||||
hideActions={!reblogElement}
|
||||
action={accountAction}
|
||||
hideActions={!accountAction}
|
||||
showEdit={!!actualStatus.edited_at}
|
||||
showProfileHoverCard={hoverable}
|
||||
withLinkToProfile={hoverable}
|
||||
|
@ -376,7 +384,7 @@ const Status: React.FC<IStatus> = (props) => {
|
|||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</Card>
|
||||
</div>
|
||||
</HotKeys>
|
||||
);
|
||||
|
|
|
@ -18,7 +18,7 @@ const messages = defineMessages({
|
|||
|
||||
interface ICard {
|
||||
/** The type of card. */
|
||||
variant?: 'rounded',
|
||||
variant?: 'default' | 'rounded',
|
||||
/** Card size preset. */
|
||||
size?: 'md' | 'lg' | 'xl',
|
||||
/** Extra classnames for the <div> element. */
|
||||
|
@ -28,12 +28,11 @@ interface ICard {
|
|||
}
|
||||
|
||||
/** An opaque backdrop to hold a collection of related elements. */
|
||||
const Card = React.forwardRef<HTMLDivElement, ICard>(({ children, variant, size = 'md', className, ...filteredProps }, ref): JSX.Element => (
|
||||
const Card = React.forwardRef<HTMLDivElement, ICard>(({ children, variant = 'default', size = 'md', className, ...filteredProps }, ref): JSX.Element => (
|
||||
<div
|
||||
ref={ref}
|
||||
{...filteredProps}
|
||||
className={classNames({
|
||||
'space-y-4': true,
|
||||
'bg-white dark:bg-primary-900 text-gray-900 dark:text-gray-100 shadow-lg dark:shadow-none overflow-hidden': variant === 'rounded',
|
||||
[sizes[size]]: variant === 'rounded',
|
||||
}, className)}
|
||||
|
|
|
@ -18,6 +18,7 @@ import GdprBanner from 'soapbox/components/gdpr-banner';
|
|||
import Helmet from 'soapbox/components/helmet';
|
||||
import LoadingScreen from 'soapbox/components/loading-screen';
|
||||
import AuthLayout from 'soapbox/features/auth_layout';
|
||||
import EmbeddedStatus from 'soapbox/features/embedded-status';
|
||||
import PublicLayout from 'soapbox/features/public_layout';
|
||||
import BundleContainer from 'soapbox/features/ui/containers/bundle_container';
|
||||
import {
|
||||
|
@ -148,6 +149,12 @@ const SoapboxMount = () => {
|
|||
<Route path='/verify' component={AuthLayout} />
|
||||
)}
|
||||
|
||||
<Route
|
||||
path='/embed/:statusId'
|
||||
render={(props) => <EmbeddedStatus params={props.match.params} />}
|
||||
/>
|
||||
<Redirect from='/@:username/:statusId/embed' to='/embed/:statusId' />
|
||||
|
||||
<Route path='/reset-password' component={AuthLayout} />
|
||||
<Route path='/edit-password' component={AuthLayout} />
|
||||
<Route path='/invite/:token' component={AuthLayout} />
|
||||
|
|
|
@ -53,7 +53,7 @@ const Ad: React.FC<IAd> = ({ card, impression }) => {
|
|||
|
||||
return (
|
||||
<div className='relative'>
|
||||
<Card className='p-5' variant='rounded'>
|
||||
<Card className='py-6 sm:p-5' variant='rounded'>
|
||||
<Stack space={4}>
|
||||
<HStack alignItems='center' space={3}>
|
||||
<Avatar src={instance.thumbnail} size={42} />
|
||||
|
|
|
@ -0,0 +1,77 @@
|
|||
import React, { useEffect, useState } from 'react';
|
||||
import { useHistory } from 'react-router-dom';
|
||||
|
||||
import { fetchStatus } from 'soapbox/actions/statuses';
|
||||
import MissingIndicator from 'soapbox/components/missing_indicator';
|
||||
import SiteLogo from 'soapbox/components/site-logo';
|
||||
import Status from 'soapbox/components/status';
|
||||
import { Spinner } from 'soapbox/components/ui';
|
||||
import { useAppDispatch, useAppSelector } from 'soapbox/hooks';
|
||||
import { iframeId } from 'soapbox/iframe';
|
||||
import { makeGetStatus } from 'soapbox/selectors';
|
||||
|
||||
interface IEmbeddedStatus {
|
||||
params: {
|
||||
statusId: string,
|
||||
},
|
||||
}
|
||||
|
||||
const getStatus = makeGetStatus();
|
||||
|
||||
/** Status to be presented in an iframe for embeds on external websites. */
|
||||
const EmbeddedStatus: React.FC<IEmbeddedStatus> = ({ params }) => {
|
||||
const dispatch = useAppDispatch();
|
||||
const history = useHistory();
|
||||
const status = useAppSelector(state => getStatus(state, { id: params.statusId }));
|
||||
|
||||
const [loading, setLoading] = useState(true);
|
||||
|
||||
useEffect(() => {
|
||||
// Prevent navigation for UX and security.
|
||||
// https://stackoverflow.com/a/71531211
|
||||
history.block();
|
||||
|
||||
dispatch(fetchStatus(params.statusId))
|
||||
.then(() => setLoading(false))
|
||||
.catch(() => setLoading(false));
|
||||
}, []);
|
||||
|
||||
useEffect(() => {
|
||||
window.parent.postMessage({
|
||||
type: 'setHeight',
|
||||
id: iframeId,
|
||||
height: document.getElementsByTagName('html')[0].scrollHeight,
|
||||
}, '*');
|
||||
}, [status, loading]);
|
||||
|
||||
const logo = (
|
||||
<div className='flex align-middle justify-center ml-4'>
|
||||
<SiteLogo className='max-h-[20px] max-w-[112px]' />
|
||||
</div>
|
||||
);
|
||||
|
||||
const renderInner = () => {
|
||||
if (loading) {
|
||||
return <Spinner />;
|
||||
} else if (status) {
|
||||
return <Status status={status} accountAction={logo} variant='default' />;
|
||||
} else {
|
||||
return <MissingIndicator nested />;
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<a
|
||||
className='block bg-white dark:bg-primary-900'
|
||||
href={status?.url || '#'}
|
||||
onClick={e => e.stopPropagation()}
|
||||
target='_blank'
|
||||
>
|
||||
<div className='p-4 sm:p-6 max-w-3xl pointer-events-none'>
|
||||
{renderInner()}
|
||||
</div>
|
||||
</a>
|
||||
);
|
||||
};
|
||||
|
||||
export default EmbeddedStatus;
|
|
@ -60,7 +60,7 @@ const Settings = () => {
|
|||
|
||||
return (
|
||||
<Column label={intl.formatMessage(messages.settings)} transparent withHeader={false}>
|
||||
<Card variant='rounded'>
|
||||
<Card className='space-y-4' variant='rounded'>
|
||||
<CardHeader>
|
||||
<CardTitle title={intl.formatMessage(messages.profile)} />
|
||||
</CardHeader>
|
||||
|
|
|
@ -380,9 +380,9 @@ const Thread: React.FC<IThread> = (props) => {
|
|||
|
||||
return (
|
||||
<PendingStatus
|
||||
className='thread__status'
|
||||
key={id}
|
||||
idempotencyKey={idempotencyKey}
|
||||
thread
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
|
|
@ -0,0 +1,65 @@
|
|||
import React, { useEffect } from 'react';
|
||||
import { FormattedMessage } from 'react-intl';
|
||||
|
||||
import { closeModal } from 'soapbox/actions/modals';
|
||||
import SafeEmbed from 'soapbox/components/safe-embed';
|
||||
import { Modal, Stack, Text, Input, Divider } from 'soapbox/components/ui';
|
||||
import { useAppDispatch } from 'soapbox/hooks';
|
||||
import useEmbed from 'soapbox/queries/embed';
|
||||
|
||||
interface IEmbedModal {
|
||||
url: string,
|
||||
onError: (error: any) => void,
|
||||
}
|
||||
|
||||
const EmbedModal: React.FC<IEmbedModal> = ({ url, onError }) => {
|
||||
const dispatch = useAppDispatch();
|
||||
const { data: embed, error, isError } = useEmbed(url);
|
||||
|
||||
useEffect(() => {
|
||||
if (error && isError) {
|
||||
onError(error);
|
||||
}
|
||||
}, [isError]);
|
||||
|
||||
const handleInputClick: React.MouseEventHandler<HTMLInputElement> = (e) => {
|
||||
e.currentTarget.select();
|
||||
};
|
||||
|
||||
const handleClose = () => {
|
||||
dispatch(closeModal('EMBED'));
|
||||
};
|
||||
|
||||
return (
|
||||
<Modal
|
||||
title={<FormattedMessage id='status.embed' defaultMessage='Embed post' />}
|
||||
onClose={handleClose}
|
||||
>
|
||||
<Stack space={4}>
|
||||
<Text theme='muted'>
|
||||
<FormattedMessage id='embed.instructions' defaultMessage='Embed this post on your website by copying the code below.' />
|
||||
</Text>
|
||||
|
||||
<Input
|
||||
type='text'
|
||||
readOnly
|
||||
value={embed?.html || ''}
|
||||
onClick={handleInputClick}
|
||||
/>
|
||||
</Stack>
|
||||
|
||||
<div className='py-9'>
|
||||
<Divider />
|
||||
</div>
|
||||
|
||||
<SafeEmbed
|
||||
className='rounded-xl overflow-hidden w-full'
|
||||
sandbox='allow-same-origin allow-scripts'
|
||||
title='embedded-status'
|
||||
html={embed?.html}
|
||||
/>
|
||||
</Modal>
|
||||
);
|
||||
};
|
||||
|
||||
export default EmbedModal;
|
|
@ -1,83 +0,0 @@
|
|||
import React, { useState, useEffect, useRef } from 'react';
|
||||
import { FormattedMessage } from 'react-intl';
|
||||
|
||||
import api from 'soapbox/api';
|
||||
import { Modal, Stack, Text, Input } from 'soapbox/components/ui';
|
||||
import { useAppDispatch } from 'soapbox/hooks';
|
||||
|
||||
import type { RootState } from 'soapbox/store';
|
||||
|
||||
const fetchEmbed = (url: string) => {
|
||||
return (dispatch: any, getState: () => RootState) => {
|
||||
return api(getState).get('/api/oembed', { params: { url } });
|
||||
};
|
||||
};
|
||||
|
||||
interface IEmbedModal {
|
||||
url: string,
|
||||
onError: (error: any) => void,
|
||||
}
|
||||
|
||||
const EmbedModal: React.FC<IEmbedModal> = ({ url, onError }) => {
|
||||
const dispatch = useAppDispatch();
|
||||
|
||||
const iframe = useRef<HTMLIFrameElement>(null);
|
||||
const [oembed, setOembed] = useState<any>(null);
|
||||
|
||||
useEffect(() => {
|
||||
|
||||
dispatch(fetchEmbed(url)).then(({ data }) => {
|
||||
if (!iframe.current?.contentWindow) return;
|
||||
setOembed(data);
|
||||
|
||||
const iframeDocument = iframe.current.contentWindow.document;
|
||||
|
||||
iframeDocument.open();
|
||||
iframeDocument.write(data.html);
|
||||
iframeDocument.close();
|
||||
|
||||
const innerFrame = iframeDocument.querySelector('iframe');
|
||||
|
||||
iframeDocument.body.style.margin = '0';
|
||||
|
||||
if (innerFrame) {
|
||||
innerFrame.width = '100%';
|
||||
}
|
||||
}).catch(error => {
|
||||
onError(error);
|
||||
});
|
||||
}, [!!iframe.current]);
|
||||
|
||||
const handleInputClick: React.MouseEventHandler<HTMLInputElement> = (e) => {
|
||||
e.currentTarget.select();
|
||||
};
|
||||
|
||||
return (
|
||||
<Modal title={<FormattedMessage id='status.embed' defaultMessage='Embed' />}>
|
||||
<Stack space={4}>
|
||||
<Stack>
|
||||
<Text theme='muted' size='sm'>
|
||||
<FormattedMessage id='embed.instructions' defaultMessage='Embed this post on your website by copying the code below.' />
|
||||
</Text>
|
||||
|
||||
<Input
|
||||
type='text'
|
||||
readOnly
|
||||
value={oembed?.html || ''}
|
||||
onClick={handleInputClick}
|
||||
/>
|
||||
</Stack>
|
||||
|
||||
<iframe
|
||||
className='inline-flex rounded-xl overflow-hidden max-w-full'
|
||||
frameBorder='0'
|
||||
ref={iframe}
|
||||
sandbox='allow-same-origin'
|
||||
title='preview'
|
||||
/>
|
||||
</Stack>
|
||||
</Modal>
|
||||
);
|
||||
};
|
||||
|
||||
export default EmbedModal;
|
|
@ -3,7 +3,7 @@ import React from 'react';
|
|||
|
||||
import StatusReplyMentions from 'soapbox/components/status-reply-mentions';
|
||||
import StatusContent from 'soapbox/components/status_content';
|
||||
import { HStack } from 'soapbox/components/ui';
|
||||
import { Card, HStack } from 'soapbox/components/ui';
|
||||
import AccountContainer from 'soapbox/containers/account_container';
|
||||
import PlaceholderCard from 'soapbox/features/placeholder/components/placeholder_card';
|
||||
import PlaceholderMediaGallery from 'soapbox/features/placeholder/components/placeholder_media_gallery';
|
||||
|
@ -24,6 +24,7 @@ interface IPendingStatus {
|
|||
className?: string,
|
||||
idempotencyKey: string,
|
||||
muted?: boolean,
|
||||
thread?: boolean,
|
||||
}
|
||||
|
||||
interface IPendingStatusMedia {
|
||||
|
@ -44,7 +45,7 @@ const PendingStatusMedia: React.FC<IPendingStatusMedia> = ({ status }) => {
|
|||
}
|
||||
};
|
||||
|
||||
const PendingStatus: React.FC<IPendingStatus> = ({ idempotencyKey, className, muted }) => {
|
||||
const PendingStatus: React.FC<IPendingStatus> = ({ idempotencyKey, className, muted, thread = false }) => {
|
||||
const status = useAppSelector((state) => {
|
||||
const pendingStatus = state.pending_statuses.get(idempotencyKey);
|
||||
return pendingStatus ? buildStatus(state, pendingStatus, idempotencyKey) : null;
|
||||
|
@ -58,7 +59,10 @@ const PendingStatus: React.FC<IPendingStatus> = ({ idempotencyKey, className, mu
|
|||
return (
|
||||
<div className={classNames('opacity-50', className)}>
|
||||
<div className={classNames('status', { 'status-reply': !!status.in_reply_to_id, muted })} data-id={status.id}>
|
||||
<div className={classNames('status__wrapper', `status-${status.visibility}`, { 'status-reply': !!status.in_reply_to_id })} tabIndex={muted ? undefined : 0}>
|
||||
<Card
|
||||
className={classNames('py-6 sm:p-5', `status-${status.visibility}`, { 'status-reply': !!status.in_reply_to_id })}
|
||||
variant={thread ? 'default' : 'rounded'}
|
||||
>
|
||||
<div className='mb-4'>
|
||||
<HStack justifyContent='between' alignItems='start'>
|
||||
<AccountContainer
|
||||
|
@ -88,7 +92,7 @@ const PendingStatus: React.FC<IPendingStatus> = ({ idempotencyKey, className, mu
|
|||
|
||||
{/* TODO */}
|
||||
{/* <PlaceholderActionBar /> */}
|
||||
</div>
|
||||
</Card>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
|
|
|
@ -191,7 +191,7 @@ export function EditFederationModal() {
|
|||
}
|
||||
|
||||
export function EmbedModal() {
|
||||
return import(/* webpackChunkName: "modals/embed_modal" */'../components/embed_modal');
|
||||
return import(/* webpackChunkName: "modals/embed_modal" */'../components/embed-modal');
|
||||
}
|
||||
|
||||
export function ComponentModal() {
|
||||
|
|
|
@ -0,0 +1,14 @@
|
|||
/** ID of this iframe (given by embed.js) when embedded on a page. */
|
||||
let iframeId: any;
|
||||
|
||||
/** Receive iframe messages. */
|
||||
// https://github.com/mastodon/mastodon/pull/4853
|
||||
const handleMessage = (e: MessageEvent) => {
|
||||
if (e.data?.type === 'setHeight') {
|
||||
iframeId = e.data?.id;
|
||||
}
|
||||
};
|
||||
|
||||
window.addEventListener('message', handleMessage);
|
||||
|
||||
export { iframeId };
|
|
@ -986,7 +986,7 @@
|
|||
"status.delete": "Delete",
|
||||
"status.detailed_status": "Detailed conversation view",
|
||||
"status.direct": "Direct message @{name}",
|
||||
"status.embed": "Embed",
|
||||
"status.embed": "Embed post",
|
||||
"status.favourite": "Like",
|
||||
"status.filtered": "Filtered",
|
||||
"status.load_more": "Load more",
|
||||
|
|
|
@ -0,0 +1,30 @@
|
|||
import { useQuery } from '@tanstack/react-query';
|
||||
|
||||
import { useApi } from 'soapbox/hooks';
|
||||
|
||||
type Embed = {
|
||||
type: string,
|
||||
version: string,
|
||||
author_name: string,
|
||||
author_url: string,
|
||||
provider_name: string,
|
||||
provider_url: string,
|
||||
cache_age: number,
|
||||
html: string,
|
||||
width: number,
|
||||
height: number,
|
||||
}
|
||||
|
||||
/** Fetch OEmbed information for a status by its URL. */
|
||||
// https://github.com/mastodon/mastodon/blob/main/app/controllers/api/oembed_controller.rb
|
||||
// https://github.com/mastodon/mastodon/blob/main/app/serializers/oembed_serializer.rb
|
||||
export default function useEmbed(url: string) {
|
||||
const api = useApi();
|
||||
|
||||
const getEmbed = async() => {
|
||||
const { data } = await api.get('/api/oembed', { params: { url } });
|
||||
return data;
|
||||
};
|
||||
|
||||
return useQuery<Embed>(['embed', url], getEmbed);
|
||||
}
|
|
@ -177,10 +177,6 @@
|
|||
}
|
||||
}
|
||||
|
||||
.status__wrapper {
|
||||
@apply bg-white dark:bg-primary-900 px-4 py-6 shadow-xl dark:shadow-none sm:p-5 sm:rounded-xl;
|
||||
}
|
||||
|
||||
[column-type=filled] .status__wrapper,
|
||||
[column-type=filled] .status-placeholder {
|
||||
@apply rounded-none shadow-none p-4;
|
||||
|
|
Loading…
Reference in New Issue