Bringing in emotes, stickers, and refactors from ody #7435
17 changed files with 1458 additions and 323 deletions
|
@ -2239,5 +2239,85 @@
|
||||||
"A channel is required to repost on LBRY": "A channel is required to repost on LBRY",
|
"A channel is required to repost on LBRY": "A channel is required to repost on LBRY",
|
||||||
"Failed to view lbry://@gatogalactico#9/gato-galactico-e-as-estrelas-ninja-dos#1, please try again. If this problem persists, visit https://lbry.com/faq/support for support.": "Failed to view lbry://@gatogalactico#9/gato-galactico-e-as-estrelas-ninja-dos#1, please try again. If this problem persists, visit https://lbry.com/faq/support for support.",
|
"Failed to view lbry://@gatogalactico#9/gato-galactico-e-as-estrelas-ninja-dos#1, please try again. If this problem persists, visit https://lbry.com/faq/support for support.": "Failed to view lbry://@gatogalactico#9/gato-galactico-e-as-estrelas-ninja-dos#1, please try again. If this problem persists, visit https://lbry.com/faq/support for support.",
|
||||||
"Admin": "Admin",
|
"Admin": "Admin",
|
||||||
|
":ALIEN:": ":ALIEN:",
|
||||||
|
":ANGRY_2:": ":ANGRY_2:",
|
||||||
|
":ANGRY_3:": ":ANGRY_3:",
|
||||||
|
":ANGRY_4:": ":ANGRY_4:",
|
||||||
|
":ANGRY_1:": ":ANGRY_1:",
|
||||||
|
":BLIND:": ":BLIND:",
|
||||||
|
":BLOCK:": ":BLOCK:",
|
||||||
|
":BOMB:": ":BOMB:",
|
||||||
|
":BRAIN_CHIP:": ":BRAIN_CHIP:",
|
||||||
|
":CONFIRM:": ":CONFIRM:",
|
||||||
|
":CONFUSED_1:": ":CONFUSED_1:",
|
||||||
|
":CONFUSED_2:": ":CONFUSED_2:",
|
||||||
|
":COOKING_SOMETHING_NICE:": ":COOKING_SOMETHING_NICE:",
|
||||||
|
":CRY_2:": ":CRY_2:",
|
||||||
|
":CRY_3:": ":CRY_3:",
|
||||||
|
":CRY_4:": ":CRY_4:",
|
||||||
|
":CRY_5:": ":CRY_5:",
|
||||||
|
":CRY_1:": ":CRY_1:",
|
||||||
|
":SPACE_DOGE:": ":SPACE_DOGE:",
|
||||||
|
":DONUT:": ":DONUT:",
|
||||||
|
":EGGPLANT_WITH_CONDOM:": ":EGGPLANT_WITH_CONDOM:",
|
||||||
|
":EGGPLANT:": ":EGGPLANT:",
|
||||||
|
":FIRE_UP:": ":FIRE_UP:",
|
||||||
|
":FLAT_EARTH:": ":FLAT_EARTH:",
|
||||||
|
":FLYING_SAUCER:": ":FLYING_SAUCER:",
|
||||||
|
":HEART_CHOPPER:": ":HEART_CHOPPER:",
|
||||||
|
":HYPER_TROLL:": ":HYPER_TROLL:",
|
||||||
|
":ICE_CREAM:": ":ICE_CREAM:",
|
||||||
|
":IDK:": ":IDK:",
|
||||||
|
":ILLUMINATI_1:": ":ILLUMINATI_1:",
|
||||||
|
":ILLUMINATI_2:": ":ILLUMINATI_2:",
|
||||||
|
":KISS_2:": ":KISS_2:",
|
||||||
|
":KISS_1:": ":KISS_1:",
|
||||||
|
":LASER_GUN:": ":LASER_GUN:",
|
||||||
|
":LAUGHING_2:": ":LAUGHING_2:",
|
||||||
|
":LAUGHING_1:": ":LAUGHING_1:",
|
||||||
|
":LOLLIPOP:": ":LOLLIPOP:",
|
||||||
|
":LOVE_2:": ":LOVE_2:",
|
||||||
|
":LOVE_1:": ":LOVE_1:",
|
||||||
|
":MONSTER:": ":MONSTER:",
|
||||||
|
":MUSHROOM:": ":MUSHROOM:",
|
||||||
|
":NAIL_IT:": ":NAIL_IT:",
|
||||||
|
":NO:": ":NO:",
|
||||||
|
":OUCH:": ":OUCH:",
|
||||||
|
":PREACE:": ":PREACE:",
|
||||||
|
":PIZZA:": ":PIZZA:",
|
||||||
|
":RABBIT_HOLE:": ":RABBIT_HOLE:",
|
||||||
|
":RAINBOW_PUKE_1:": ":RAINBOW_PUKE_1:",
|
||||||
|
":RAINBOW_PUKE_2:": ":RAINBOW_PUKE_2:",
|
||||||
|
":SPACE_RESITAS:": ":SPACE_RESITAS:",
|
||||||
|
":ROCK:": ":ROCK:",
|
||||||
|
":SAD:": ":SAD:",
|
||||||
|
":SALTY:": ":SALTY:",
|
||||||
|
":SCARY:": ":SCARY:",
|
||||||
|
":SLEEP:": ":SLEEP:",
|
||||||
|
":SLIME_DOWN:": ":SLIME_DOWN:",
|
||||||
|
":SMELLY_SOCKS:": ":SMELLY_SOCKS:",
|
||||||
|
":SMILE_2:": ":SMILE_2:",
|
||||||
|
":SMILE_1:": ":SMILE_1:",
|
||||||
|
":SPACE_CHAD:": ":SPACE_CHAD:",
|
||||||
|
":SPACE_JULIAN:": ":SPACE_JULIAN:",
|
||||||
|
":SPACE_TOM:": ":SPACE_TOM:",
|
||||||
|
":SPACE_WOJAK_1:": ":SPACE_WOJAK_1:",
|
||||||
|
":SPOCK:": ":SPOCK:",
|
||||||
|
":STAR:": ":STAR:",
|
||||||
|
":SUNNY_DAY:": ":SUNNY_DAY:",
|
||||||
|
":SUPRISED:": ":SUPRISED:",
|
||||||
|
":SWEET:": ":SWEET:",
|
||||||
|
":THINKING_1:": ":THINKING_1:",
|
||||||
|
":THINKING_2:": ":THINKING_2:",
|
||||||
|
":THUMB_DOWN:": ":THUMB_DOWN:",
|
||||||
|
":THUMB_UP_1:": ":THUMB_UP_1:",
|
||||||
|
":THUMB_UP_2:": ":THUMB_UP_2:",
|
||||||
|
":TINFOIL_HAT:": ":TINFOIL_HAT:",
|
||||||
|
":TROLL_KING:": ":TROLL_KING:",
|
||||||
|
":UFO:": ":UFO:",
|
||||||
|
":WAITING:": ":WAITING:",
|
||||||
|
":WHAT:": ":WHAT:",
|
||||||
|
":WOODOO_DOLL:": ":WOODOO_DOLL:",
|
||||||
|
"Global Emotes": "Global Emotes",
|
||||||
"--end--": "--end--"
|
"--end--": "--end--"
|
||||||
}
|
}
|
||||||
|
|
68
ui/component/commentCreate/emote-selector.jsx
Normal file
68
ui/component/commentCreate/emote-selector.jsx
Normal file
|
@ -0,0 +1,68 @@
|
||||||
|
// @flow
|
||||||
|
import 'scss/component/_emote-selector.scss';
|
||||||
|
import { EMOTES_24px as EMOTES } from 'constants/emotes';
|
||||||
|
import * as ICONS from 'constants/icons';
|
||||||
|
import Button from 'component/button';
|
||||||
|
import EMOJIS from 'emoji-dictionary';
|
||||||
|
import OptimizedImage from 'component/optimizedImage';
|
||||||
|
import React from 'react';
|
||||||
|
|
||||||
|
const OLD_QUICK_EMOJIS = [
|
||||||
|
EMOJIS.getUnicode('rocket'),
|
||||||
|
EMOJIS.getUnicode('jeans'),
|
||||||
|
EMOJIS.getUnicode('fire'),
|
||||||
|
EMOJIS.getUnicode('heart'),
|
||||||
|
EMOJIS.getUnicode('open_mouth'),
|
||||||
|
];
|
||||||
|
|
||||||
|
type Props = { commentValue: string, setCommentValue: (string) => void, closeSelector: () => void };
|
||||||
|
|
||||||
|
export default function EmoteSelector(props: Props) {
|
||||||
|
const { commentValue, setCommentValue, closeSelector } = props;
|
||||||
|
|
||||||
|
function addEmoteToComment(emote: string) {
|
||||||
|
setCommentValue(
|
||||||
|
commentValue + (commentValue && commentValue.charAt(commentValue.length - 1) !== ' ' ? ` ${emote} ` : `${emote} `)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="emoteSelector">
|
||||||
|
<Button button="close" icon={ICONS.REMOVE} onClick={closeSelector} />
|
||||||
|
|
||||||
|
<div className="emoteSelector__list">
|
||||||
|
<div className="emoteSelector__listRow">
|
||||||
|
<div className="emoteSelector__listRowTitle">{__('Global Emotes')}</div>
|
||||||
|
|
||||||
|
<div className="emoteSelector__listRowItems">
|
||||||
|
{OLD_QUICK_EMOJIS.map((emoji) => (
|
||||||
|
<Button
|
||||||
|
key={emoji}
|
||||||
|
label={emoji}
|
||||||
|
title={`:${EMOJIS.getName(emoji)}:`}
|
||||||
|
button="alt"
|
||||||
|
className="button--file-action"
|
||||||
|
onClick={() => addEmoteToComment(emoji)}
|
||||||
|
/>
|
||||||
|
))}
|
||||||
|
{EMOTES.map((emote) => {
|
||||||
|
const emoteName = emote.name.toLowerCase();
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Button
|
||||||
|
key={String(emote)}
|
||||||
|
title={emoteName}
|
||||||
|
button="alt"
|
||||||
|
className="button--file-action"
|
||||||
|
onClick={() => addEmoteToComment(emoteName)}
|
||||||
|
>
|
||||||
|
<OptimizedImage src={emote.url} waitLoad />
|
||||||
|
</Button>
|
||||||
|
);
|
||||||
|
})}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
|
@ -1,5 +1,6 @@
|
||||||
// @flow
|
// @flow
|
||||||
import { FF_MAX_CHARS_IN_COMMENT } from 'constants/form-field';
|
import { FF_MAX_CHARS_IN_COMMENT } from 'constants/form-field';
|
||||||
|
import 'scss/component/_comment-create.scss';
|
||||||
import { FormField, Form } from 'component/common/form';
|
import { FormField, Form } from 'component/common/form';
|
||||||
import { getChannelIdFromClaim } from 'util/claim';
|
import { getChannelIdFromClaim } from 'util/claim';
|
||||||
import { Lbryio } from 'lbryinc';
|
import { Lbryio } from 'lbryinc';
|
||||||
|
@ -12,6 +13,7 @@ import ChannelMentionSuggestions from 'component/channelMentionSuggestions';
|
||||||
import ChannelThumbnail from 'component/channelThumbnail';
|
import ChannelThumbnail from 'component/channelThumbnail';
|
||||||
import classnames from 'classnames';
|
import classnames from 'classnames';
|
||||||
import CreditAmount from 'component/common/credit-amount';
|
import CreditAmount from 'component/common/credit-amount';
|
||||||
|
import EmoteSelector from './emote-selector';
|
||||||
import Empty from 'component/common/empty';
|
import Empty from 'component/common/empty';
|
||||||
import I18nMessage from 'component/i18nMessage';
|
import I18nMessage from 'component/i18nMessage';
|
||||||
import Icon from 'component/common/icon';
|
import Icon from 'component/common/icon';
|
||||||
|
@ -102,6 +104,7 @@ export function CommentCreate(props: Props) {
|
||||||
const [deletedComment, setDeletedComment] = React.useState(false);
|
const [deletedComment, setDeletedComment] = React.useState(false);
|
||||||
const [pauseQuickSend, setPauseQuickSend] = React.useState(false);
|
const [pauseQuickSend, setPauseQuickSend] = React.useState(false);
|
||||||
const [shouldDisableReviewButton, setShouldDisableReviewButton] = React.useState();
|
const [shouldDisableReviewButton, setShouldDisableReviewButton] = React.useState();
|
||||||
|
const [showEmotes, setShowEmotes] = React.useState(false);
|
||||||
|
|
||||||
const selectedMentionIndex =
|
const selectedMentionIndex =
|
||||||
commentValue.indexOf('@', selectionIndex) === selectionIndex
|
commentValue.indexOf('@', selectionIndex) === selectionIndex
|
||||||
|
@ -199,12 +202,6 @@ export function CommentCreate(props: Props) {
|
||||||
window.removeEventListener('keydown', altEnterListener);
|
window.removeEventListener('keydown', altEnterListener);
|
||||||
}
|
}
|
||||||
|
|
||||||
function handleSubmit() {
|
|
||||||
if (activeChannelClaim && commentValue.length) {
|
|
||||||
handleCreateComment();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
function handleSupportComment() {
|
function handleSupportComment() {
|
||||||
if (!activeChannelClaim) {
|
if (!activeChannelClaim) {
|
||||||
return;
|
return;
|
||||||
|
@ -362,6 +359,7 @@ export function CommentCreate(props: Props) {
|
||||||
* @param {string} [environment] Optional environment for Stripe (test|live)
|
* @param {string} [environment] Optional environment for Stripe (test|live)
|
||||||
*/
|
*/
|
||||||
function handleCreateComment(txid, payment_intent_id, environment) {
|
function handleCreateComment(txid, payment_intent_id, environment) {
|
||||||
|
setShowEmotes(false);
|
||||||
setIsSubmitting(true);
|
setIsSubmitting(true);
|
||||||
|
|
||||||
createComment(commentValue, claimId, parentId, txid, payment_intent_id, environment)
|
createComment(commentValue, claimId, parentId, txid, payment_intent_id, environment)
|
||||||
|
@ -494,13 +492,20 @@ export function CommentCreate(props: Props) {
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Form
|
<Form
|
||||||
onSubmit={handleSubmit}
|
|
||||||
className={classnames('comment__create', {
|
className={classnames('comment__create', {
|
||||||
'comment__create--reply': isReply,
|
'comment__create--reply': isReply,
|
||||||
'comment__create--nested-reply': isNested,
|
'comment__create--nested-reply': isNested,
|
||||||
'comment__create--bottom': bottom,
|
'comment__create--bottom': bottom,
|
||||||
})}
|
})}
|
||||||
>
|
>
|
||||||
|
{showEmotes && (
|
||||||
|
<EmoteSelector
|
||||||
|
commentValue={commentValue}
|
||||||
|
setCommentValue={setCommentValue}
|
||||||
|
closeSelector={() => setShowEmotes(false)}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
|
||||||
{!advancedEditor && (
|
{!advancedEditor && (
|
||||||
<ChannelMentionSuggestions
|
<ChannelMentionSuggestions
|
||||||
uri={uri}
|
uri={uri}
|
||||||
|
@ -524,6 +529,7 @@ export function CommentCreate(props: Props) {
|
||||||
}
|
}
|
||||||
quickActionLabel={isReply ? undefined : advancedEditor ? __('Simple Editor') : __('Advanced Editor')}
|
quickActionLabel={isReply ? undefined : advancedEditor ? __('Simple Editor') : __('Advanced Editor')}
|
||||||
quickActionHandler={() => setAdvancedEditor(!advancedEditor)}
|
quickActionHandler={() => setAdvancedEditor(!advancedEditor)}
|
||||||
|
openEmoteMenu={() => setShowEmotes(!showEmotes)}
|
||||||
onFocus={onTextareaFocus}
|
onFocus={onTextareaFocus}
|
||||||
onBlur={onTextareaBlur}
|
onBlur={onTextareaBlur}
|
||||||
placeholder={__('Say something about this...')}
|
placeholder={__('Say something about this...')}
|
||||||
|
@ -579,6 +585,7 @@ export function CommentCreate(props: Props) {
|
||||||
? __('Commenting...')
|
? __('Commenting...')
|
||||||
: __('Comment --[button to submit something]--')
|
: __('Comment --[button to submit something]--')
|
||||||
}
|
}
|
||||||
|
onClick={() => activeChannelClaim && commentValue.length && handleCreateComment()}
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
{!supportDisabled && !claimIsMine && (
|
{!supportDisabled && !claimIsMine && (
|
||||||
|
|
|
@ -1,33 +1,23 @@
|
||||||
// @flow
|
// @flow
|
||||||
import type { ElementRef, Node } from 'react';
|
import 'easymde/dist/easymde.min.css';
|
||||||
|
import { FF_MAX_CHARS_DEFAULT } from 'constants/form-field';
|
||||||
|
import { openEditorMenu, stopContextMenu } from 'util/context-menu';
|
||||||
|
import * as ICONS from 'constants/icons';
|
||||||
|
import Button from 'component/button';
|
||||||
|
import MarkdownPreview from 'component/common/markdown-preview';
|
||||||
import React from 'react';
|
import React from 'react';
|
||||||
import ReactDOMServer from 'react-dom/server';
|
import ReactDOMServer from 'react-dom/server';
|
||||||
import SimpleMDE from 'react-simplemde-editor';
|
import SimpleMDE from 'react-simplemde-editor';
|
||||||
import MarkdownPreview from 'component/common/markdown-preview';
|
import type { ElementRef, Node } from 'react';
|
||||||
import { openEditorMenu, stopContextMenu } from 'util/context-menu';
|
|
||||||
import { FF_MAX_CHARS_DEFAULT } from 'constants/form-field';
|
|
||||||
import 'easymde/dist/easymde.min.css';
|
|
||||||
import Button from 'component/button';
|
|
||||||
import emoji from 'emoji-dictionary';
|
|
||||||
|
|
||||||
const QUICK_EMOJIS = [
|
|
||||||
emoji.getUnicode('rocket'),
|
|
||||||
emoji.getUnicode('jeans'),
|
|
||||||
emoji.getUnicode('fire'),
|
|
||||||
emoji.getUnicode('heart'),
|
|
||||||
emoji.getUnicode('open_mouth'),
|
|
||||||
];
|
|
||||||
|
|
||||||
type Props = {
|
type Props = {
|
||||||
name: string,
|
name: string,
|
||||||
label?: string | Node,
|
label?: string | Node,
|
||||||
render?: () => React$Node,
|
|
||||||
prefix?: string,
|
prefix?: string,
|
||||||
postfix?: string,
|
postfix?: string,
|
||||||
error?: string | boolean,
|
error?: string | boolean,
|
||||||
helper?: string | React$Node,
|
helper?: string | React$Node,
|
||||||
type?: string,
|
type?: string,
|
||||||
onChange?: (any) => any,
|
|
||||||
defaultValue?: string | number,
|
defaultValue?: string | number,
|
||||||
placeholder?: string | number,
|
placeholder?: string | number,
|
||||||
children?: React$Node,
|
children?: React$Node,
|
||||||
|
@ -43,18 +33,17 @@ type Props = {
|
||||||
min?: number,
|
min?: number,
|
||||||
max?: number,
|
max?: number,
|
||||||
quickActionLabel?: string,
|
quickActionLabel?: string,
|
||||||
quickActionHandler?: (any) => any,
|
|
||||||
disabled?: boolean,
|
disabled?: boolean,
|
||||||
onChange: (any) => void,
|
|
||||||
value?: string | number,
|
value?: string | number,
|
||||||
noEmojis?: boolean,
|
noEmojis?: boolean,
|
||||||
|
render?: () => React$Node,
|
||||||
|
onChange?: (any) => any,
|
||||||
|
quickActionHandler?: (any) => any,
|
||||||
|
openEmoteMenu?: () => void,
|
||||||
};
|
};
|
||||||
|
|
||||||
export class FormField extends React.PureComponent<Props> {
|
export class FormField extends React.PureComponent<Props> {
|
||||||
static defaultProps = {
|
static defaultProps = { labelOnLeft: false, blockWrap: true };
|
||||||
labelOnLeft: false,
|
|
||||||
blockWrap: true,
|
|
||||||
};
|
|
||||||
|
|
||||||
input: { current: ElementRef<any> };
|
input: { current: ElementRef<any> };
|
||||||
|
|
||||||
|
@ -67,14 +56,11 @@ export class FormField extends React.PureComponent<Props> {
|
||||||
const { autoFocus } = this.props;
|
const { autoFocus } = this.props;
|
||||||
const input = this.input.current;
|
const input = this.input.current;
|
||||||
|
|
||||||
if (input && autoFocus) {
|
if (input && autoFocus) input.focus();
|
||||||
input.focus();
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
render() {
|
render() {
|
||||||
const {
|
const {
|
||||||
render,
|
|
||||||
label,
|
label,
|
||||||
prefix,
|
prefix,
|
||||||
postfix,
|
postfix,
|
||||||
|
@ -92,11 +78,24 @@ export class FormField extends React.PureComponent<Props> {
|
||||||
charCount,
|
charCount,
|
||||||
textAreaMaxLength,
|
textAreaMaxLength,
|
||||||
quickActionLabel,
|
quickActionLabel,
|
||||||
quickActionHandler,
|
|
||||||
noEmojis,
|
noEmojis,
|
||||||
|
render,
|
||||||
|
quickActionHandler,
|
||||||
|
openEmoteMenu,
|
||||||
...inputProps
|
...inputProps
|
||||||
} = this.props;
|
} = this.props;
|
||||||
|
|
||||||
const errorMessage = typeof error === 'object' ? error.message : error;
|
const errorMessage = typeof error === 'object' ? error.message : error;
|
||||||
|
|
||||||
|
// Ideally, the character count should (and can) be appended to the
|
||||||
|
// SimpleMDE's "options::status" bar. However, I couldn't figure out how
|
||||||
|
// to pass the current value to it's callback, nor query the current
|
||||||
|
// text length from the callback. So, we'll use our own widget.
|
||||||
|
const hasCharCount = charCount !== undefined && charCount >= 0;
|
||||||
|
const countInfo = hasCharCount && textAreaMaxLength !== undefined && (
|
||||||
|
<span className="comment__char-count-mde">{`${charCount || '0'}/${textAreaMaxLength}`}</span>
|
||||||
|
);
|
||||||
|
|
||||||
const Wrapper = blockWrap
|
const Wrapper = blockWrap
|
||||||
? ({ children: innerChildren }) => <fieldset-section class="radio">{innerChildren}</fieldset-section>
|
? ({ children: innerChildren }) => <fieldset-section class="radio">{innerChildren}</fieldset-section>
|
||||||
: ({ children: innerChildren }) => <span className="radio">{innerChildren}</span>;
|
: ({ children: innerChildren }) => <span className="radio">{innerChildren}</span>;
|
||||||
|
@ -108,32 +107,15 @@ export class FormField extends React.PureComponent<Props> {
|
||||||
</div>
|
</div>
|
||||||
) : null;
|
) : null;
|
||||||
|
|
||||||
let input;
|
const inputSimple = (type: string) => (
|
||||||
if (type) {
|
<>
|
||||||
if (type === 'radio') {
|
<input id={name} type={type} {...inputProps} />
|
||||||
input = (
|
|
||||||
<Wrapper>
|
|
||||||
<input id={name} type="radio" {...inputProps} />
|
|
||||||
<label htmlFor={name}>{label}</label>
|
<label htmlFor={name}>{label}</label>
|
||||||
</Wrapper>
|
</>
|
||||||
);
|
);
|
||||||
} else if (type === 'checkbox') {
|
|
||||||
input = (
|
const inputSelect = (selectClass: string) => (
|
||||||
<div className="checkbox">
|
<fieldset-section class={selectClass}>
|
||||||
<input id={name} type="checkbox" {...inputProps} />
|
|
||||||
<label htmlFor={name}>{label}</label>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
} else if (type === 'range') {
|
|
||||||
input = (
|
|
||||||
<div>
|
|
||||||
<input id={name} type="range" {...inputProps} />
|
|
||||||
<label htmlFor={name}>{label}</label>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
} else if (type === 'select') {
|
|
||||||
input = (
|
|
||||||
<fieldset-section>
|
|
||||||
{(label || errorMessage) && (
|
{(label || errorMessage) && (
|
||||||
<label htmlFor={name}>{errorMessage ? <span className="error__text">{errorMessage}</span> : label}</label>
|
<label htmlFor={name}>{errorMessage ? <span className="error__text">{errorMessage}</span> : label}</label>
|
||||||
)}
|
)}
|
||||||
|
@ -142,21 +124,21 @@ export class FormField extends React.PureComponent<Props> {
|
||||||
</select>
|
</select>
|
||||||
</fieldset-section>
|
</fieldset-section>
|
||||||
);
|
);
|
||||||
} else if (type === 'select-tiny') {
|
|
||||||
input = (
|
const input = () => {
|
||||||
<fieldset-section class="select--slim">
|
switch (type) {
|
||||||
{(label || errorMessage) && (
|
case 'radio':
|
||||||
<label htmlFor={name}>{errorMessage ? <span className="error__text">{errorMessage}</span> : label}</label>
|
return <Wrapper>{inputSimple('radio')}</Wrapper>;
|
||||||
)}
|
case 'checkbox':
|
||||||
<select id={name} {...inputProps}>
|
return <div className="checkbox">{inputSimple('checkbox')}</div>;
|
||||||
{children}
|
case 'range':
|
||||||
</select>
|
return <div>{inputSimple('range')}</div>;
|
||||||
</fieldset-section>
|
case 'select':
|
||||||
);
|
return inputSelect('');
|
||||||
} else if (type === 'markdown') {
|
case 'select-tiny':
|
||||||
const handleEvents = {
|
return inputSelect('select--slim');
|
||||||
contextmenu: openEditorMenu,
|
case 'markdown':
|
||||||
};
|
const handleEvents = { contextmenu: openEditorMenu };
|
||||||
|
|
||||||
const getInstance = (editor) => {
|
const getInstance = (editor) => {
|
||||||
// SimpleMDE max char check
|
// SimpleMDE max char check
|
||||||
|
@ -164,9 +146,9 @@ export class FormField extends React.PureComponent<Props> {
|
||||||
if (textAreaMaxLength && changes.update) {
|
if (textAreaMaxLength && changes.update) {
|
||||||
var str = changes.text.join('\n');
|
var str = changes.text.join('\n');
|
||||||
var delta = str.length - (instance.indexFromPos(changes.to) - instance.indexFromPos(changes.from));
|
var delta = str.length - (instance.indexFromPos(changes.to) - instance.indexFromPos(changes.from));
|
||||||
if (delta <= 0) {
|
|
||||||
return;
|
if (delta <= 0) return;
|
||||||
}
|
|
||||||
delta = instance.getValue().length + delta - textAreaMaxLength;
|
delta = instance.getValue().length + delta - textAreaMaxLength;
|
||||||
if (delta > 0) {
|
if (delta > 0) {
|
||||||
str = str.substr(0, str.length - delta);
|
str = str.substr(0, str.length - delta);
|
||||||
|
@ -208,22 +190,11 @@ export class FormField extends React.PureComponent<Props> {
|
||||||
}, 25);
|
}, 25);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
} catch (err) {
|
} catch (e) {} // Do nothing (revert to original behavior)
|
||||||
// Do nothing (revert to original behavior)
|
|
||||||
}
|
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
// Ideally, the character count should (and can) be appended to the
|
return (
|
||||||
// SimpleMDE's "options::status" bar. However, I couldn't figure out how
|
|
||||||
// to pass the current value to it's callback, nor query the current
|
|
||||||
// text length from the callback. So, we'll use our own widget.
|
|
||||||
const hasCharCount = charCount !== undefined && charCount >= 0;
|
|
||||||
const countInfo = hasCharCount && textAreaMaxLength !== undefined && (
|
|
||||||
<span className="comment__char-count-mde">{`${charCount || '0'}/${textAreaMaxLength}`}</span>
|
|
||||||
);
|
|
||||||
|
|
||||||
input = (
|
|
||||||
<div className="form-field--SimpleMDE" onContextMenu={stopContextMenu}>
|
<div className="form-field--SimpleMDE" onContextMenu={stopContextMenu}>
|
||||||
<fieldset-section>
|
<fieldset-section>
|
||||||
<div className="form-field__two-column">
|
<div className="form-field__two-column">
|
||||||
|
@ -251,18 +222,12 @@ export class FormField extends React.PureComponent<Props> {
|
||||||
</fieldset-section>
|
</fieldset-section>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
} else if (type === 'textarea') {
|
case 'textarea':
|
||||||
const hasCharCount = charCount !== undefined && charCount >= 0;
|
return (
|
||||||
const countInfo = hasCharCount && textAreaMaxLength !== undefined && (
|
|
||||||
<span className="comment__char-count">{`${charCount || '0'}/${textAreaMaxLength}`}</span>
|
|
||||||
);
|
|
||||||
input = (
|
|
||||||
<fieldset-section>
|
<fieldset-section>
|
||||||
{(label || quickAction) && (
|
{(label || quickAction) && (
|
||||||
<div className="form-field__two-column">
|
<div className="form-field__two-column">
|
||||||
<div>
|
|
||||||
<label htmlFor={name}>{label}</label>
|
<label htmlFor={name}>{label}</label>
|
||||||
</div>
|
|
||||||
{quickAction}
|
{quickAction}
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
@ -275,28 +240,20 @@ export class FormField extends React.PureComponent<Props> {
|
||||||
/>
|
/>
|
||||||
<div className="form-field__textarea-info">
|
<div className="form-field__textarea-info">
|
||||||
{!noEmojis && (
|
{!noEmojis && (
|
||||||
<div className="form-field__quick-emojis">
|
|
||||||
{QUICK_EMOJIS.map((emoji) => (
|
|
||||||
<Button
|
<Button
|
||||||
key={emoji}
|
type="alt"
|
||||||
disabled={inputProps.disabled}
|
className="button--file-action"
|
||||||
type="button"
|
title="Emotes"
|
||||||
className="button--emoji"
|
onClick={openEmoteMenu}
|
||||||
label={emoji}
|
icon={ICONS.EMOJI}
|
||||||
onClick={() => {
|
iconSize={20}
|
||||||
inputProps.onChange({
|
|
||||||
target: { value: inputProps.value ? `${inputProps.value} ${emoji}` : emoji },
|
|
||||||
});
|
|
||||||
}}
|
|
||||||
/>
|
/>
|
||||||
))}
|
|
||||||
</div>
|
|
||||||
)}
|
)}
|
||||||
{countInfo}
|
{countInfo}
|
||||||
</div>
|
</div>
|
||||||
</fieldset-section>
|
</fieldset-section>
|
||||||
);
|
);
|
||||||
} else {
|
default:
|
||||||
const inputElement = <input type={type} id={name} {...inputProps} ref={this.input} />;
|
const inputElement = <input type={type} id={name} {...inputProps} ref={this.input} />;
|
||||||
const inner = inputButton ? (
|
const inner = inputButton ? (
|
||||||
<input-submit>
|
<input-submit>
|
||||||
|
@ -307,8 +264,7 @@ export class FormField extends React.PureComponent<Props> {
|
||||||
inputElement
|
inputElement
|
||||||
);
|
);
|
||||||
|
|
||||||
input = (
|
return (
|
||||||
<React.Fragment>
|
|
||||||
<fieldset-section>
|
<fieldset-section>
|
||||||
{(label || errorMessage) && (
|
{(label || errorMessage) && (
|
||||||
<label htmlFor={name}>
|
<label htmlFor={name}>
|
||||||
|
@ -318,17 +274,15 @@ export class FormField extends React.PureComponent<Props> {
|
||||||
{prefix && <label htmlFor={name}>{prefix}</label>}
|
{prefix && <label htmlFor={name}>{prefix}</label>}
|
||||||
{inner}
|
{inner}
|
||||||
</fieldset-section>
|
</fieldset-section>
|
||||||
</React.Fragment>
|
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<React.Fragment>
|
<>
|
||||||
{input}
|
{type && input()}
|
||||||
|
|
||||||
{helper && <div className="form-field__help">{helper}</div>}
|
{helper && <div className="form-field__help">{helper}</div>}
|
||||||
</React.Fragment>
|
</>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -2014,4 +2014,12 @@ export const icons = {
|
||||||
<line x1="19" y1="20.5" x2="20" y2="20.5" />
|
<line x1="19" y1="20.5" x2="20" y2="20.5" />
|
||||||
</g>
|
</g>
|
||||||
),
|
),
|
||||||
|
[ICONS.EMOJI]: buildIcon(
|
||||||
|
<g>
|
||||||
|
<circle cx="12" cy="12" r="10" />
|
||||||
|
<path d="M8 14s1.5 2 4 2 4-2 4-2" />
|
||||||
|
<line x1="9" y1="9" x2="9.01" y2="9" />
|
||||||
|
<line x1="15" y1="9" x2="15.01" y2="9" />
|
||||||
|
</g>
|
||||||
|
),
|
||||||
};
|
};
|
||||||
|
|
|
@ -10,13 +10,18 @@ import remarkFrontMatter from 'remark-frontmatter';
|
||||||
import reactRenderer from 'remark-react';
|
import reactRenderer from 'remark-react';
|
||||||
import MarkdownLink from 'component/markdownLink';
|
import MarkdownLink from 'component/markdownLink';
|
||||||
import defaultSchema from 'hast-util-sanitize/lib/github.json';
|
import defaultSchema from 'hast-util-sanitize/lib/github.json';
|
||||||
import { formatedLinks, inlineLinks } from 'util/remark-lbry';
|
import { formattedLinks, inlineLinks } from 'util/remark-lbry';
|
||||||
import { formattedTimestamp, inlineTimestamp } from 'util/remark-timestamp';
|
import { formattedTimestamp, inlineTimestamp } from 'util/remark-timestamp';
|
||||||
|
import { formattedEmote, inlineEmote } from 'util/remark-emote';
|
||||||
import ZoomableImage from 'component/zoomableImage';
|
import ZoomableImage from 'component/zoomableImage';
|
||||||
import { CHANNEL_STAKED_LEVEL_VIDEO_COMMENTS } from 'config';
|
import { CHANNEL_STAKED_LEVEL_VIDEO_COMMENTS } from 'config';
|
||||||
import Button from 'component/button';
|
import Button from 'component/button';
|
||||||
import * as ICONS from 'constants/icons';
|
import * as ICONS from 'constants/icons';
|
||||||
import { parse } from 'node-html-parser';
|
import { parse } from 'node-html-parser';
|
||||||
|
import OptimizedImage from 'component/optimizedImage';
|
||||||
|
|
||||||
|
const RE_EMOTE = /:\+1:|:-1:|:[\w-]+:/;
|
||||||
|
|
||||||
type SimpleTextProps = {
|
type SimpleTextProps = {
|
||||||
children?: React.Node,
|
children?: React.Node,
|
||||||
};
|
};
|
||||||
|
@ -42,6 +47,7 @@ type MarkdownProps = {
|
||||||
className?: string,
|
className?: string,
|
||||||
parentCommentId?: string,
|
parentCommentId?: string,
|
||||||
isMarkdownPost?: boolean,
|
isMarkdownPost?: boolean,
|
||||||
|
disableTimestamps?: boolean,
|
||||||
stakedLevel?: number,
|
stakedLevel?: number,
|
||||||
};
|
};
|
||||||
|
|
||||||
|
@ -93,10 +99,15 @@ const SimpleLink = (props: SimpleLinkProps) => {
|
||||||
|
|
||||||
const SimpleImageLink = (props: ImageLinkProps) => {
|
const SimpleImageLink = (props: ImageLinkProps) => {
|
||||||
const { src, title, alt, helpText } = props;
|
const { src, title, alt, helpText } = props;
|
||||||
|
|
||||||
if (!src) {
|
if (!src) {
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (title && RE_EMOTE.test(title) && src.includes('static.odycdn.com/emoticons')) {
|
||||||
|
return <OptimizedImage title={title} src={src} />;
|
||||||
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Button
|
<Button
|
||||||
button="link"
|
button="link"
|
||||||
|
@ -132,9 +143,20 @@ function isStakeEnoughForPreview(stakedLevel) {
|
||||||
// ****************************************************************************
|
// ****************************************************************************
|
||||||
|
|
||||||
const MarkdownPreview = (props: MarkdownProps) => {
|
const MarkdownPreview = (props: MarkdownProps) => {
|
||||||
const { content, strip, simpleLinks, noDataStore, className, parentCommentId, isMarkdownPost, stakedLevel } = props;
|
const {
|
||||||
|
content,
|
||||||
|
strip,
|
||||||
|
simpleLinks,
|
||||||
|
noDataStore,
|
||||||
|
className,
|
||||||
|
parentCommentId,
|
||||||
|
isMarkdownPost,
|
||||||
|
disableTimestamps,
|
||||||
|
stakedLevel,
|
||||||
|
} = props;
|
||||||
|
|
||||||
const strippedContent = content
|
const strippedContent = content
|
||||||
? content.replace(REPLACE_REGEX, (iframeHtml, y, iframeSrc) => {
|
? content.replace(REPLACE_REGEX, (iframeHtml) => {
|
||||||
// Let the browser try to create an iframe to see if the markup is valid
|
// Let the browser try to create an iframe to see if the markup is valid
|
||||||
let lbrySrc;
|
let lbrySrc;
|
||||||
try {
|
try {
|
||||||
|
@ -152,6 +174,10 @@ const MarkdownPreview = (props: MarkdownProps) => {
|
||||||
})
|
})
|
||||||
: '';
|
: '';
|
||||||
|
|
||||||
|
const initialQuote = strippedContent.split(' ').find((word) => word.length > 0 || word.charAt(0) === '>');
|
||||||
|
let stripQuote;
|
||||||
|
if (initialQuote && initialQuote.charAt(0) === '>') stripQuote = true;
|
||||||
|
|
||||||
const remarkOptions: Object = {
|
const remarkOptions: Object = {
|
||||||
sanitize: schema,
|
sanitize: schema,
|
||||||
fragment: React.Fragment,
|
fragment: React.Fragment,
|
||||||
|
@ -183,10 +209,22 @@ const MarkdownPreview = (props: MarkdownProps) => {
|
||||||
};
|
};
|
||||||
|
|
||||||
// Strip all content and just render text
|
// Strip all content and just render text
|
||||||
if (strip) {
|
if (strip || stripQuote) {
|
||||||
// Remove new lines and extra space
|
// Remove new lines and extra space
|
||||||
remarkOptions.remarkReactComponents.p = SimpleText;
|
remarkOptions.remarkReactComponents.p = SimpleText;
|
||||||
return (
|
return stripQuote ? (
|
||||||
|
<span dir="auto" className="markdown-preview">
|
||||||
|
<blockquote>
|
||||||
|
{
|
||||||
|
remark()
|
||||||
|
.use(remarkStrip)
|
||||||
|
.use(remarkFrontMatter, ['yaml'])
|
||||||
|
.use(reactRenderer, remarkOptions)
|
||||||
|
.processSync(content).contents
|
||||||
|
}
|
||||||
|
</blockquote>
|
||||||
|
</span>
|
||||||
|
) : (
|
||||||
<span dir="auto" className="markdown-preview">
|
<span dir="auto" className="markdown-preview">
|
||||||
{
|
{
|
||||||
remark()
|
remark()
|
||||||
|
@ -206,11 +244,13 @@ const MarkdownPreview = (props: MarkdownProps) => {
|
||||||
.use(remarkAttr, remarkAttrOpts)
|
.use(remarkAttr, remarkAttrOpts)
|
||||||
// Remark plugins for lbry urls
|
// Remark plugins for lbry urls
|
||||||
// Note: The order is important
|
// Note: The order is important
|
||||||
.use(formatedLinks)
|
.use(formattedLinks)
|
||||||
.use(inlineLinks)
|
.use(inlineLinks)
|
||||||
.use(isMarkdownPost ? null : inlineTimestamp)
|
.use(disableTimestamps || isMarkdownPost ? null : inlineTimestamp)
|
||||||
.use(isMarkdownPost ? null : formattedTimestamp)
|
.use(disableTimestamps || isMarkdownPost ? null : formattedTimestamp)
|
||||||
// Emojis
|
// Emojis
|
||||||
|
.use(inlineEmote)
|
||||||
|
.use(formattedEmote)
|
||||||
.use(remarkEmoji)
|
.use(remarkEmoji)
|
||||||
// Render new lines without needing spaces.
|
// Render new lines without needing spaces.
|
||||||
.use(remarkBreaks)
|
.use(remarkBreaks)
|
||||||
|
|
|
@ -10,10 +10,11 @@ function scaleToDevicePixelRatio(value: number, window: any) {
|
||||||
type Props = {
|
type Props = {
|
||||||
src: string,
|
src: string,
|
||||||
objectFit?: string,
|
objectFit?: string,
|
||||||
|
waitLoad?: boolean,
|
||||||
};
|
};
|
||||||
|
|
||||||
function OptimizedImage(props: Props) {
|
function OptimizedImage(props: Props) {
|
||||||
const { objectFit, src, ...imgProps } = props;
|
const { objectFit, src, waitLoad, ...imgProps } = props;
|
||||||
const [optimizedSrc, setOptimizedSrc] = React.useState('');
|
const [optimizedSrc, setOptimizedSrc] = React.useState('');
|
||||||
const ref = React.useRef<any>();
|
const ref = React.useRef<any>();
|
||||||
|
|
||||||
|
@ -101,8 +102,12 @@ function OptimizedImage(props: Props) {
|
||||||
<img
|
<img
|
||||||
ref={ref}
|
ref={ref}
|
||||||
{...imgProps}
|
{...imgProps}
|
||||||
|
style={{ visibility: waitLoad ? 'hidden' : 'visible' }}
|
||||||
src={optimizedSrc}
|
src={optimizedSrc}
|
||||||
onLoad={() => adjustOptimizationIfNeeded(ref.current, objectFit, src)}
|
onLoad={() => {
|
||||||
|
if (waitLoad) ref.current.style.visibility = 'visible';
|
||||||
|
adjustOptimizationIfNeeded(ref.current, objectFit, src);
|
||||||
|
}}
|
||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
96
ui/constants/emotes.js
Normal file
96
ui/constants/emotes.js
Normal file
|
@ -0,0 +1,96 @@
|
||||||
|
// @flow
|
||||||
|
|
||||||
|
const buildCDNUrl = (path: string) => `https://static.odycdn.com/emoticons/${path}`;
|
||||||
|
|
||||||
|
const buildEmote = (name: string, path: string) => ({
|
||||||
|
name: __(`:${name}:`),
|
||||||
|
url: buildCDNUrl(path),
|
||||||
|
});
|
||||||
|
|
||||||
|
const getEmotes = (px: string, multiplier: string) => [
|
||||||
|
buildEmote('ALIEN', `${px}/Alien${multiplier}.png`),
|
||||||
|
buildEmote('ANGRY_2', `${px}/angry%202${multiplier}.png`),
|
||||||
|
buildEmote('ANGRY_3', `${px}/angry%203${multiplier}.png`),
|
||||||
|
buildEmote('ANGRY_4', `${px}/angry%204${multiplier}.png`),
|
||||||
|
buildEmote('ANGRY_1', `${px}/angry${multiplier}.png`),
|
||||||
|
buildEmote('BLIND', `${px}/blind${multiplier}.png`),
|
||||||
|
buildEmote('BLOCK', `${px}/block${multiplier}.png`),
|
||||||
|
buildEmote('BOMB', `${px}/bomb${multiplier}.png`),
|
||||||
|
buildEmote('BRAIN_CHIP', `${px}/Brain%20chip${multiplier}.png`),
|
||||||
|
buildEmote('CONFIRM', `${px}/CONFIRM${multiplier}.png`),
|
||||||
|
buildEmote('CONFUSED_1', `${px}/confused-1${multiplier}.png`),
|
||||||
|
buildEmote('CONFUSED_2', `${px}/confused${multiplier}.png`),
|
||||||
|
buildEmote('COOKING_SOMETHING_NICE', `${px}/cooking%20something%20nice${multiplier}.png`),
|
||||||
|
buildEmote('CRY_2', `${px}/cry%202${multiplier}.png`),
|
||||||
|
buildEmote('CRY_3', `${px}/cry%203${multiplier}.png`),
|
||||||
|
buildEmote('CRY_4', `${px}/cry%204${multiplier}.png`),
|
||||||
|
buildEmote('CRY_5', `${px}/cry%205${multiplier}.png`),
|
||||||
|
buildEmote('CRY_1', `${px}/cry${multiplier}.png`),
|
||||||
|
buildEmote('SPACE_DOGE', `${px}/doge${multiplier}.png`),
|
||||||
|
buildEmote('DONUT', `${px}/donut${multiplier}.png`),
|
||||||
|
buildEmote('EGGPLANT_WITH_CONDOM', `${px}/eggplant%20with%20condom${multiplier}.png`),
|
||||||
|
buildEmote('EGGPLANT', `${px}/eggplant${multiplier}.png`),
|
||||||
|
buildEmote('FIRE_UP', `${px}/fire%20up${multiplier}.png`),
|
||||||
|
buildEmote('FLAT_EARTH', `${px}/Flat%20earth${multiplier}.png`),
|
||||||
|
buildEmote('FLYING_SAUCER', `${px}/Flying%20saucer${multiplier}.png`),
|
||||||
|
buildEmote('HEART_CHOPPER', `${px}/heart%20chopper${multiplier}.png`),
|
||||||
|
buildEmote('HYPER_TROLL', `${px}/HyperTroll${multiplier}.png`),
|
||||||
|
buildEmote('ICE_CREAM', `${px}/ice%20cream${multiplier}.png`),
|
||||||
|
buildEmote('IDK', `${px}/IDK${multiplier}.png`),
|
||||||
|
buildEmote('ILLUMINATI_1', `${px}/Illuminati-1${multiplier}.png`),
|
||||||
|
buildEmote('ILLUMINATI_2', `${px}/Illuminati${multiplier}.png`),
|
||||||
|
buildEmote('KISS_2', `${px}/kiss%202${multiplier}.png`),
|
||||||
|
buildEmote('KISS_1', `${px}/kiss${multiplier}.png`),
|
||||||
|
buildEmote('LASER_GUN', `${px}/laser%20gun${multiplier}.png`),
|
||||||
|
buildEmote('LAUGHING_2', `${px}/Laughing 2${multiplier}.png`),
|
||||||
|
buildEmote('LAUGHING_1', `${px}/Laughing${multiplier}.png`),
|
||||||
|
buildEmote('LOLLIPOP', `${px}/Lollipop${multiplier}.png`),
|
||||||
|
buildEmote('LOVE_2', `${px}/Love%202${multiplier}.png`),
|
||||||
|
buildEmote('LOVE_1', `${px}/Love${multiplier}.png`),
|
||||||
|
buildEmote('MONSTER', `${px}/Monster${multiplier}.png`),
|
||||||
|
buildEmote('MUSHROOM', `${px}/mushroom${multiplier}.png`),
|
||||||
|
buildEmote('NAIL_IT', `${px}/Nail%20It${multiplier}.png`),
|
||||||
|
buildEmote('NO', `${px}/NO${multiplier}.png`),
|
||||||
|
buildEmote('OUCH', `${px}/ouch${multiplier}.png`),
|
||||||
|
buildEmote('PREACE', `${px}/peace${multiplier}.png`),
|
||||||
|
buildEmote('PIZZA', `${px}/pizza${multiplier}.png`),
|
||||||
|
buildEmote('RABBIT_HOLE', `${px}/rabbit%20hole${multiplier}.png`),
|
||||||
|
buildEmote('RAINBOW_PUKE_1', `${px}/rainbow%20puke-1${multiplier}.png`),
|
||||||
|
buildEmote('RAINBOW_PUKE_2', `${px}/rainbow%20puke${multiplier}.png`),
|
||||||
|
buildEmote('SPACE_RESITAS', `${px}/resitas${multiplier}.png`),
|
||||||
|
buildEmote('ROCK', `${px}/ROCK${multiplier}.png`),
|
||||||
|
buildEmote('SAD', `${px}/sad${multiplier}.png`),
|
||||||
|
buildEmote('SALTY', `${px}/salty${multiplier}.png`),
|
||||||
|
buildEmote('SCARY', `${px}/scary${multiplier}.png`),
|
||||||
|
buildEmote('SLEEP', `${px}/Sleep${multiplier}.png`),
|
||||||
|
buildEmote('SLIME_DOWN', `${px}/slime%20down${multiplier}.png`),
|
||||||
|
buildEmote('SMELLY_SOCKS', `${px}/smelly%20socks${multiplier}.png`),
|
||||||
|
buildEmote('SMILE_2', `${px}/smile%202${multiplier}.png`),
|
||||||
|
buildEmote('SMILE_1', `${px}/smile${multiplier}.png`),
|
||||||
|
buildEmote('SPACE_CHAD', `${px}/space%20chad${multiplier}.png`),
|
||||||
|
buildEmote('SPACE_JULIAN', `${px}/Space%20Julian${multiplier}.png`),
|
||||||
|
buildEmote('SPACE_TOM', `${px}/space%20Tom${multiplier}.png`),
|
||||||
|
buildEmote('SPACE_WOJAK_1', `${px}/space%20wojak-1${multiplier}.png`),
|
||||||
|
buildEmote('ANGRY_3', `${px}/space%20wojak${multiplier}.png`),
|
||||||
|
buildEmote('SPOCK', `${px}/SPOCK${multiplier}.png`),
|
||||||
|
buildEmote('STAR', `${px}/Star${multiplier}.png`),
|
||||||
|
buildEmote('SUNNY_DAY', `${px}/sunny%20day${multiplier}.png`),
|
||||||
|
buildEmote('SUPRISED', `${px}/surprised${multiplier}.png`),
|
||||||
|
buildEmote('SWEET', `${px}/sweet${multiplier}.png`),
|
||||||
|
buildEmote('THINKING_1', `${px}/thinking-1${multiplier}.png`),
|
||||||
|
buildEmote('THINKING_2', `${px}/thinking${multiplier}.png`),
|
||||||
|
buildEmote('THUMB_DOWN', `${px}/thumb%20down${multiplier}.png`),
|
||||||
|
buildEmote('THUMB_UP_1', `${px}/thumb%20up-1${multiplier}.png`),
|
||||||
|
buildEmote('THUMB_UP_2', `${px}/thumb%20up${multiplier}.png`),
|
||||||
|
buildEmote('TINFOIL_HAT', `${px}/tin%20hat${multiplier}.png`),
|
||||||
|
buildEmote('TROLL_KING', `${px}/Troll%20king${multiplier}.png`),
|
||||||
|
buildEmote('UFO', `${px}/ufo${multiplier}.png`),
|
||||||
|
buildEmote('WAITING', `${px}/waiting${multiplier}.png`),
|
||||||
|
buildEmote('WHAT', `${px}/what_${multiplier}.png`),
|
||||||
|
buildEmote('WOODOO_DOLL', `${px}/woodo%20doll${multiplier}.png`),
|
||||||
|
];
|
||||||
|
|
||||||
|
export const EMOTES_24px = getEmotes('24%20px', '');
|
||||||
|
export const EMOTES_36px = getEmotes('36px', '@1.5x');
|
||||||
|
export const EMOTES_48px = getEmotes('48%20px', '@2x');
|
||||||
|
export const EMOTES_72px = getEmotes('72%20px', '@3x');
|
|
@ -178,3 +178,4 @@ export const LIFE = 'Life';
|
||||||
export const ARTISTS = 'Artists';
|
export const ARTISTS = 'Artists';
|
||||||
export const MYSTERIES = 'Mysteries';
|
export const MYSTERIES = 'Mysteries';
|
||||||
export const TECHNOLOGY = 'Technology';
|
export const TECHNOLOGY = 'Technology';
|
||||||
|
export const EMOJI = 'Emoji';
|
||||||
|
|
|
@ -549,12 +549,6 @@
|
||||||
.button--highlighted {
|
.button--highlighted {
|
||||||
border: 1px solid var(--color-border);
|
border: 1px solid var(--color-border);
|
||||||
}
|
}
|
||||||
|
|
||||||
.button--emoji {
|
|
||||||
font-size: 1.1rem;
|
|
||||||
border-radius: 3rem;
|
|
||||||
}
|
|
||||||
|
|
||||||
.button__content {
|
.button__content {
|
||||||
display: flex;
|
display: flex;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
|
|
82
ui/scss/component/_comment-create.scss
Normal file
82
ui/scss/component/_comment-create.scss
Normal file
|
@ -0,0 +1,82 @@
|
||||||
|
@import '../init/vars';
|
||||||
|
|
||||||
|
$thumbnailWidth: 1.5rem;
|
||||||
|
$thumbnailWidthSmall: 1rem;
|
||||||
|
|
||||||
|
.comment__create {
|
||||||
|
font-size: var(--font-small);
|
||||||
|
position: relative;
|
||||||
|
|
||||||
|
fieldset-section,
|
||||||
|
.form-field--SimpleMDE {
|
||||||
|
margin-top: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.form-field__two-column {
|
||||||
|
display: flex;
|
||||||
|
justify-content: space-between;
|
||||||
|
align-items: flex-end;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.comment__create--reply {
|
||||||
|
margin-top: var(--spacing-m);
|
||||||
|
position: relative;
|
||||||
|
}
|
||||||
|
|
||||||
|
.content_comment {
|
||||||
|
position: relative;
|
||||||
|
}
|
||||||
|
|
||||||
|
.comment__create--nested-reply {
|
||||||
|
margin-top: var(--spacing-s);
|
||||||
|
margin-left: calc((#{$thumbnailWidthSmall} + var(--spacing-xs)) * 2 + var(--spacing-m) + 4px);
|
||||||
|
|
||||||
|
@media (min-width: $breakpoint-small) {
|
||||||
|
margin-left: calc((#{$thumbnailWidth} + var(--spacing-m)) * 2 + var(--spacing-m) + 4px);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.comment__create--bottom {
|
||||||
|
padding-bottom: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.comment-new__label-wrapper {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: row;
|
||||||
|
justify-content: flex-start;
|
||||||
|
align-items: baseline;
|
||||||
|
flex-wrap: wrap;
|
||||||
|
width: 100%;
|
||||||
|
|
||||||
|
@media (min-width: $breakpoint-small) {
|
||||||
|
fieldset-section {
|
||||||
|
max-width: 10rem;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.comment-new__label {
|
||||||
|
white-space: nowrap;
|
||||||
|
margin-right: var(--spacing-xs);
|
||||||
|
}
|
||||||
|
|
||||||
|
.comment__sc-preview {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
border: 1px solid var(--color-border);
|
||||||
|
border-radius: var(--border-radius);
|
||||||
|
padding: var(--spacing-s);
|
||||||
|
margin: var(--spacing-s) 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.comment__sc-preview-amount {
|
||||||
|
margin-right: var(--spacing-m);
|
||||||
|
font-size: var(--font-large);
|
||||||
|
}
|
||||||
|
|
||||||
|
.comment--min-amount-notice {
|
||||||
|
.icon {
|
||||||
|
margin-bottom: -3px; // TODO fix few instances of these (find "-2px")
|
||||||
|
}
|
||||||
|
}
|
|
@ -32,29 +32,6 @@ $thumbnailWidthSmall: 1rem;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
.comment__create {
|
|
||||||
font-size: var(--font-small);
|
|
||||||
position: relative;
|
|
||||||
|
|
||||||
fieldset-section,
|
|
||||||
.form-field--SimpleMDE {
|
|
||||||
margin-top: 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
.form-field__two-column {
|
|
||||||
column-count: 2;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
.comment__create--reply {
|
|
||||||
margin-top: var(--spacing-m);
|
|
||||||
position: relative;
|
|
||||||
}
|
|
||||||
|
|
||||||
.comment__create--bottom {
|
|
||||||
padding-bottom: 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
.comment {
|
.comment {
|
||||||
width: 100%;
|
width: 100%;
|
||||||
display: flex;
|
display: flex;
|
||||||
|
@ -90,10 +67,6 @@ $thumbnailWidthSmall: 1rem;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
.content_comment {
|
|
||||||
position: relative;
|
|
||||||
}
|
|
||||||
|
|
||||||
.comment__thumbnail-wrapper {
|
.comment__thumbnail-wrapper {
|
||||||
flex: 0;
|
flex: 0;
|
||||||
margin-top: var(--spacing-xxs);
|
margin-top: var(--spacing-xxs);
|
||||||
|
@ -136,24 +109,10 @@ $thumbnailWidthSmall: 1rem;
|
||||||
opacity: 0.6;
|
opacity: 0.6;
|
||||||
}
|
}
|
||||||
|
|
||||||
.comment__sc-preview {
|
|
||||||
display: flex;
|
|
||||||
align-items: center;
|
|
||||||
border: 1px solid var(--color-border);
|
|
||||||
border-radius: var(--border-radius);
|
|
||||||
padding: var(--spacing-s);
|
|
||||||
margin: var(--spacing-s) 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
.comment__edit-input {
|
.comment__edit-input {
|
||||||
margin-top: var(--spacing-xxs);
|
margin-top: var(--spacing-xxs);
|
||||||
}
|
}
|
||||||
|
|
||||||
.comment__sc-preview-amount {
|
|
||||||
margin-right: var(--spacing-m);
|
|
||||||
font-size: var(--font-large);
|
|
||||||
}
|
|
||||||
|
|
||||||
.comment__threadline {
|
.comment__threadline {
|
||||||
@extend .button--alt;
|
@extend .button--alt;
|
||||||
height: auto;
|
height: auto;
|
||||||
|
@ -173,26 +132,6 @@ $thumbnailWidthSmall: 1rem;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
.comment-new__label-wrapper {
|
|
||||||
display: flex;
|
|
||||||
flex-direction: row;
|
|
||||||
justify-content: flex-start;
|
|
||||||
align-items: baseline;
|
|
||||||
flex-wrap: wrap;
|
|
||||||
width: 100%;
|
|
||||||
|
|
||||||
@media (min-width: $breakpoint-small) {
|
|
||||||
fieldset-section {
|
|
||||||
max-width: 10rem;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
.comment-new__label {
|
|
||||||
white-space: nowrap;
|
|
||||||
margin-right: var(--spacing-xs);
|
|
||||||
}
|
|
||||||
|
|
||||||
.comment--highlighted {
|
.comment--highlighted {
|
||||||
background: var(--color-comment-highlighted);
|
background: var(--color-comment-highlighted);
|
||||||
box-shadow: 0 0 0 5px var(--color-comment-highlighted);
|
box-shadow: 0 0 0 5px var(--color-comment-highlighted);
|
||||||
|
@ -429,8 +368,7 @@ $thumbnailWidthSmall: 1rem;
|
||||||
@extend .comment__action;
|
@extend .comment__action;
|
||||||
}
|
}
|
||||||
|
|
||||||
.comment__action--nested,
|
.comment__action--nested {
|
||||||
.comment__create--nested-reply {
|
|
||||||
margin-top: var(--spacing-s);
|
margin-top: var(--spacing-s);
|
||||||
margin-left: calc((#{$thumbnailWidthSmall} + var(--spacing-xs)) * 2 + var(--spacing-m) + 4px);
|
margin-left: calc((#{$thumbnailWidthSmall} + var(--spacing-xs)) * 2 + var(--spacing-m) + 4px);
|
||||||
|
|
||||||
|
@ -485,12 +423,6 @@ $thumbnailWidthSmall: 1rem;
|
||||||
opacity: 0.5;
|
opacity: 0.5;
|
||||||
}
|
}
|
||||||
|
|
||||||
.comment--min-amount-notice {
|
|
||||||
.icon {
|
|
||||||
margin-bottom: -3px; // TODO fix few instances of these (find "-2px")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
.comments-own {
|
.comments-own {
|
||||||
.section__actions {
|
.section__actions {
|
||||||
align-items: flex-start;
|
align-items: flex-start;
|
||||||
|
|
40
ui/scss/component/_emote-selector.scss
Normal file
40
ui/scss/component/_emote-selector.scss
Normal file
|
@ -0,0 +1,40 @@
|
||||||
|
@import '../init/vars';
|
||||||
|
|
||||||
|
.emoteSelector {
|
||||||
|
animation: menu-animate-in var(--animation-duration) var(--animation-style);
|
||||||
|
border: 1px solid var(--color-border);
|
||||||
|
border-radius: var(--border-radius);
|
||||||
|
margin-bottom: var(--spacing-m);
|
||||||
|
}
|
||||||
|
|
||||||
|
.emoteSelector__list {
|
||||||
|
display: flex;
|
||||||
|
flex-wrap: wrap;
|
||||||
|
overflow-y: scroll;
|
||||||
|
overflow-x: hidden;
|
||||||
|
max-height: 25vh;
|
||||||
|
padding: var(--spacing-s);
|
||||||
|
|
||||||
|
.emoteSelector__listRowItems {
|
||||||
|
display: flex;
|
||||||
|
flex-wrap: wrap;
|
||||||
|
|
||||||
|
.button--file-action {
|
||||||
|
margin: var(--spacing-xxs);
|
||||||
|
padding: var(--spacing-xs);
|
||||||
|
|
||||||
|
.button__content {
|
||||||
|
justify-content: center;
|
||||||
|
align-items: center;
|
||||||
|
align-content: center;
|
||||||
|
width: 1.5rem;
|
||||||
|
height: 1.5rem;
|
||||||
|
|
||||||
|
span {
|
||||||
|
margin: auto;
|
||||||
|
font-size: var(--font-large);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -448,9 +448,7 @@ fieldset-group {
|
||||||
}
|
}
|
||||||
|
|
||||||
.form-field__quick-action {
|
.form-field__quick-action {
|
||||||
float: right;
|
|
||||||
font-size: var(--font-xsmall);
|
font-size: var(--font-xsmall);
|
||||||
margin-top: 2.5%;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.form-field__textarea-info {
|
.form-field__textarea-info {
|
||||||
|
@ -462,12 +460,6 @@ fieldset-group {
|
||||||
margin-bottom: var(--spacing-s);
|
margin-bottom: var(--spacing-s);
|
||||||
}
|
}
|
||||||
|
|
||||||
.form-field__quick-emojis {
|
|
||||||
> *:not(:last-child) {
|
|
||||||
margin-right: var(--spacing-s);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
fieldset-section {
|
fieldset-section {
|
||||||
.form-field__internal-option {
|
.form-field__internal-option {
|
||||||
margin-top: var(--spacing-s);
|
margin-top: var(--spacing-s);
|
||||||
|
|
125
ui/util/remark-emote.js
Normal file
125
ui/util/remark-emote.js
Normal file
|
@ -0,0 +1,125 @@
|
||||||
|
import { EMOTES_24px as EMOTES } from 'constants/emotes';
|
||||||
|
import visit from 'unist-util-visit';
|
||||||
|
|
||||||
|
const EMOTE_NODE_TYPE = 'emote';
|
||||||
|
const RE_EMOTE = /:\+1:|:-1:|:[\w-]+:/;
|
||||||
|
|
||||||
|
// ***************************************************************************
|
||||||
|
// Tokenize emote
|
||||||
|
// ***************************************************************************
|
||||||
|
|
||||||
|
function findNextEmote(value, fromIndex, strictlyFromIndex) {
|
||||||
|
let begin = 0;
|
||||||
|
|
||||||
|
while (begin < value.length) {
|
||||||
|
const match = value.substring(begin).match(RE_EMOTE);
|
||||||
|
|
||||||
|
if (!match) return null;
|
||||||
|
|
||||||
|
match.index += begin;
|
||||||
|
|
||||||
|
if (strictlyFromIndex && match.index !== fromIndex) {
|
||||||
|
if (match.index > fromIndex) {
|
||||||
|
// Already gone past desired index. Skip the rest.
|
||||||
|
return null;
|
||||||
|
} else {
|
||||||
|
// Next match might fit 'fromIndex'.
|
||||||
|
begin = match.index + match[0].length;
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (fromIndex > 0 && fromIndex > match.index && fromIndex < match.index + match[0].length) {
|
||||||
|
// Skip previously-rejected word
|
||||||
|
// This assumes that a non-zero 'fromIndex' means that a previous lookup has failed.
|
||||||
|
begin = match.index + match[0].length;
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
const str = match[0];
|
||||||
|
|
||||||
|
if (EMOTES.some(({ name }) => str.toUpperCase() === name)) {
|
||||||
|
// Profit!
|
||||||
|
return { text: str, index: match.index };
|
||||||
|
}
|
||||||
|
|
||||||
|
if (strictlyFromIndex && match.index >= fromIndex) {
|
||||||
|
return null; // Since it failed and we've gone past the desired index, skip the rest.
|
||||||
|
}
|
||||||
|
|
||||||
|
begin = match.index + match[0].length;
|
||||||
|
}
|
||||||
|
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
function locateEmote(value, fromIndex) {
|
||||||
|
const emote = findNextEmote(value, fromIndex, false);
|
||||||
|
return emote ? emote.index : -1;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Generate 'emote' markdown node
|
||||||
|
const createEmoteNode = (text) => ({
|
||||||
|
type: EMOTE_NODE_TYPE,
|
||||||
|
value: text,
|
||||||
|
children: [{ type: 'text', value: text }],
|
||||||
|
});
|
||||||
|
|
||||||
|
// Generate a markdown image from emote
|
||||||
|
function tokenizeEmote(eat, value, silent) {
|
||||||
|
if (silent) return true;
|
||||||
|
|
||||||
|
const emote = findNextEmote(value, 0, true);
|
||||||
|
if (emote) {
|
||||||
|
try {
|
||||||
|
const text = emote.text;
|
||||||
|
return eat(text)(createEmoteNode(text));
|
||||||
|
} catch (e) {}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
tokenizeEmote.locator = locateEmote;
|
||||||
|
|
||||||
|
export function inlineEmote() {
|
||||||
|
const Parser = this.Parser;
|
||||||
|
const tokenizers = Parser.prototype.inlineTokenizers;
|
||||||
|
const methods = Parser.prototype.inlineMethods;
|
||||||
|
|
||||||
|
// Add an inline tokenizer (defined in the following example).
|
||||||
|
tokenizers.emote = tokenizeEmote;
|
||||||
|
|
||||||
|
// Run it just before `text`.
|
||||||
|
methods.splice(methods.indexOf('text'), 0, 'emote');
|
||||||
|
}
|
||||||
|
|
||||||
|
// ***************************************************************************
|
||||||
|
// Format emote
|
||||||
|
// ***************************************************************************
|
||||||
|
|
||||||
|
const transformer = (node, index, parent) => {
|
||||||
|
if (node.type === EMOTE_NODE_TYPE && parent && parent.type === 'paragraph') {
|
||||||
|
const emoteStr = node.value;
|
||||||
|
const emote = EMOTES.find(({ name }) => emoteStr.toUpperCase() === name);
|
||||||
|
|
||||||
|
node.type = 'image';
|
||||||
|
node.url = emote.url;
|
||||||
|
node.title = emoteStr;
|
||||||
|
node.children = [{ type: 'text', value: emoteStr }];
|
||||||
|
if (!node.data || !node.data.hProperties) {
|
||||||
|
// Create new node data
|
||||||
|
node.data = {
|
||||||
|
hProperties: { emote: true },
|
||||||
|
};
|
||||||
|
} else if (node.data.hProperties) {
|
||||||
|
// Don't overwrite current attributes
|
||||||
|
node.data.hProperties = {
|
||||||
|
emote: true,
|
||||||
|
...node.data.hProperties,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const transform = (tree) => visit(tree, [EMOTE_NODE_TYPE], transformer);
|
||||||
|
|
||||||
|
export const formattedEmote = () => transform;
|
|
@ -149,7 +149,7 @@ const transform = (tree) => {
|
||||||
visit(tree, ['link'], visitor);
|
visit(tree, ['link'], visitor);
|
||||||
};
|
};
|
||||||
|
|
||||||
export const formatedLinks = () => transform;
|
export const formattedLinks = () => transform;
|
||||||
|
|
||||||
// Main module
|
// Main module
|
||||||
export function inlineLinks() {
|
export function inlineLinks() {
|
||||||
|
|
711
web/scss/themes/odysee/component/_form-field.scss
Normal file
711
web/scss/themes/odysee/component/_form-field.scss
Normal file
|
@ -0,0 +1,711 @@
|
||||||
|
@import '../init/mixins';
|
||||||
|
|
||||||
|
input,
|
||||||
|
textarea,
|
||||||
|
select,
|
||||||
|
.date-picker-input {
|
||||||
|
height: var(--height-input);
|
||||||
|
border-radius: var(--border-radius);
|
||||||
|
border: 1px solid;
|
||||||
|
color: var(--color-input);
|
||||||
|
border-color: var(--color-input-border);
|
||||||
|
background-color: var(--color-input-bg);
|
||||||
|
padding-right: var(--spacing-s);
|
||||||
|
padding-left: var(--spacing-s);
|
||||||
|
|
||||||
|
&:focus {
|
||||||
|
@include focus;
|
||||||
|
}
|
||||||
|
|
||||||
|
&::placeholder {
|
||||||
|
color: var(--color-input-placeholder);
|
||||||
|
opacity: 0.4;
|
||||||
|
}
|
||||||
|
|
||||||
|
&:disabled {
|
||||||
|
opacity: 0.4;
|
||||||
|
|
||||||
|
& + label {
|
||||||
|
opacity: 0.4;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
&[type='range'] {
|
||||||
|
height: auto;
|
||||||
|
height: 0.5rem;
|
||||||
|
background-color: var(--color-secondary);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
checkbox-element,
|
||||||
|
radio-element,
|
||||||
|
select {
|
||||||
|
cursor: pointer;
|
||||||
|
}
|
||||||
|
|
||||||
|
select {
|
||||||
|
background-image: var(--select-toggle-background);
|
||||||
|
background-position: 99% center;
|
||||||
|
background-repeat: no-repeat;
|
||||||
|
background-size: 1rem;
|
||||||
|
padding-right: var(--spacing-l);
|
||||||
|
padding-left: var(--spacing-s);
|
||||||
|
font-weight: bold;
|
||||||
|
}
|
||||||
|
|
||||||
|
fieldset-group {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: row;
|
||||||
|
justify-content: space-between;
|
||||||
|
|
||||||
|
&.fieldset-group--smushed {
|
||||||
|
fieldset-section + fieldset-section {
|
||||||
|
margin-top: 0;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fieldset-section,
|
||||||
|
fieldset-group,
|
||||||
|
form,
|
||||||
|
.checkbox,
|
||||||
|
.radio,
|
||||||
|
.form-field--SimpleMDE,
|
||||||
|
.form-field__help {
|
||||||
|
+ fieldset-section,
|
||||||
|
+ fieldset-group,
|
||||||
|
+ form,
|
||||||
|
+ .checkbox,
|
||||||
|
+ .radio,
|
||||||
|
+ .form-field--SimpleMDE {
|
||||||
|
margin-top: var(--spacing-l);
|
||||||
|
}
|
||||||
|
|
||||||
|
+ .form-field__help {
|
||||||
|
margin-top: var(--spacing-s);
|
||||||
|
}
|
||||||
|
|
||||||
|
&:last-child {
|
||||||
|
margin-bottom: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
input,
|
||||||
|
select {
|
||||||
|
width: 100%;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fieldset-section,
|
||||||
|
.checkbox,
|
||||||
|
.radio {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
}
|
||||||
|
|
||||||
|
label {
|
||||||
|
font-size: var(--font-small);
|
||||||
|
color: var(--color-input-label);
|
||||||
|
display: inline-block;
|
||||||
|
margin-bottom: 0.1rem;
|
||||||
|
|
||||||
|
.icon__lbc {
|
||||||
|
margin-bottom: 4px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
input-submit {
|
||||||
|
display: flex;
|
||||||
|
|
||||||
|
& > *:first-child,
|
||||||
|
& > *:nth-child(2) {
|
||||||
|
margin: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
& > *:first-child {
|
||||||
|
border-top-right-radius: 0;
|
||||||
|
border-bottom-right-radius: 0;
|
||||||
|
border-right: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
& > *:nth-child(2) {
|
||||||
|
border-top-left-radius: 0;
|
||||||
|
border-bottom-left-radius: 0;
|
||||||
|
border: 1px solid var(--color-border);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.checkbox,
|
||||||
|
.radio {
|
||||||
|
position: relative;
|
||||||
|
|
||||||
|
input[type='checkbox'],
|
||||||
|
input[type='radio'] {
|
||||||
|
height: var(--height-checkbox);
|
||||||
|
width: var(--height-checkbox);
|
||||||
|
position: absolute;
|
||||||
|
border: none;
|
||||||
|
left: 0;
|
||||||
|
padding: 0;
|
||||||
|
background-color: transparent;
|
||||||
|
|
||||||
|
&:disabled + label {
|
||||||
|
cursor: default;
|
||||||
|
pointer-events: none;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
label {
|
||||||
|
position: relative;
|
||||||
|
display: inline-block;
|
||||||
|
margin: 0;
|
||||||
|
font-size: var(--font-base);
|
||||||
|
padding-left: calc(var(--height-checkbox) + var(--spacing-s));
|
||||||
|
min-height: var(--height-checkbox);
|
||||||
|
|
||||||
|
&::before {
|
||||||
|
background-color: var(--color-input-toggle-bg);
|
||||||
|
}
|
||||||
|
|
||||||
|
&:hover {
|
||||||
|
&::before {
|
||||||
|
background-color: var(--color-input-toggle-bg-hover);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
label::before,
|
||||||
|
label::after {
|
||||||
|
position: absolute;
|
||||||
|
content: '';
|
||||||
|
}
|
||||||
|
|
||||||
|
// Hide the checkmark by default
|
||||||
|
input[type='checkbox'] + label::after,
|
||||||
|
input[type='radio'] + label::after {
|
||||||
|
content: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Unhide on the checked state
|
||||||
|
input[type='checkbox']:checked + label::after,
|
||||||
|
input[type='radio']:checked + label::after {
|
||||||
|
content: '';
|
||||||
|
}
|
||||||
|
|
||||||
|
input[type='checkbox']:focus + label::before,
|
||||||
|
input[type='radio']:focus + label::before {
|
||||||
|
@include focus;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.checkbox {
|
||||||
|
// Outer box of the fake checkbox
|
||||||
|
label::before {
|
||||||
|
height: var(--height-checkbox);
|
||||||
|
width: var(--height-checkbox);
|
||||||
|
border: 1px solid var(--color-input-border);
|
||||||
|
border-radius: var(--border-radius);
|
||||||
|
left: 0px;
|
||||||
|
top: -1px;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Checkmark of the fake checkbox
|
||||||
|
label::after {
|
||||||
|
height: 6px;
|
||||||
|
width: 12px;
|
||||||
|
border-left: 2px solid;
|
||||||
|
border-bottom: 2px solid;
|
||||||
|
border-color: var(--color-input-toggle);
|
||||||
|
border-left-color: var(--color-input-toggle);
|
||||||
|
transform: rotate(-45deg);
|
||||||
|
left: 6px;
|
||||||
|
top: 6px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.radio {
|
||||||
|
input[type='radio'] {
|
||||||
|
border-radius: 50%;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Outer box of the fake radio
|
||||||
|
label::before {
|
||||||
|
height: var(--height-radio);
|
||||||
|
width: var(--height-radio);
|
||||||
|
border: 1px solid var(--color-input-border);
|
||||||
|
border-radius: calc(var(--height-radio) * 0.5);
|
||||||
|
left: 0px;
|
||||||
|
top: -1px;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Checkmark of the fake radio
|
||||||
|
label::after {
|
||||||
|
height: 12px;
|
||||||
|
width: 12px;
|
||||||
|
border-radius: 50%;
|
||||||
|
background-color: var(--color-primary);
|
||||||
|
left: 6px;
|
||||||
|
top: 5px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.range__label {
|
||||||
|
display: flex;
|
||||||
|
justify-content: space-between;
|
||||||
|
width: 100%;
|
||||||
|
margin-bottom: var(--spacing-m);
|
||||||
|
|
||||||
|
> * {
|
||||||
|
width: 33%;
|
||||||
|
text-align: center;
|
||||||
|
|
||||||
|
&:first-of-type {
|
||||||
|
text-align: left;
|
||||||
|
}
|
||||||
|
&:last-of-type {
|
||||||
|
text-align: right;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.fieldset-group {
|
||||||
|
@extend fieldset-group;
|
||||||
|
}
|
||||||
|
|
||||||
|
.fieldset-section {
|
||||||
|
@extend fieldset-section;
|
||||||
|
}
|
||||||
|
|
||||||
|
.input-submit {
|
||||||
|
@extend input-submit;
|
||||||
|
}
|
||||||
|
|
||||||
|
input-submit {
|
||||||
|
align-items: center;
|
||||||
|
|
||||||
|
input {
|
||||||
|
z-index: 2;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
input[type='number'] {
|
||||||
|
width: 8rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
fieldset-group {
|
||||||
|
+ fieldset-group {
|
||||||
|
margin-top: var(--spacing-s);
|
||||||
|
}
|
||||||
|
|
||||||
|
&.fieldset-group--smushed {
|
||||||
|
justify-content: flex-start;
|
||||||
|
|
||||||
|
fieldset-section {
|
||||||
|
width: auto;
|
||||||
|
margin: 0;
|
||||||
|
|
||||||
|
&:first-child {
|
||||||
|
input,
|
||||||
|
select {
|
||||||
|
border-right: none;
|
||||||
|
border-top-right-radius: 0;
|
||||||
|
border-bottom-right-radius: 0;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
&:nth-of-type(2) {
|
||||||
|
input,
|
||||||
|
select {
|
||||||
|
border-top-left-radius: 0;
|
||||||
|
border-bottom-left-radius: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
label {
|
||||||
|
margin-left: var(--spacing-s);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
&.fieldgroup--paginate {
|
||||||
|
padding-bottom: var(--spacing-l);
|
||||||
|
margin-top: var(--spacing-l);
|
||||||
|
align-items: flex-end;
|
||||||
|
justify-content: center;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// This is a special case where the prefix appears "inside" the input
|
||||||
|
// It would be way simpler to just use position: absolute and give it a width
|
||||||
|
// but the width can change when we use it for the name prefix
|
||||||
|
// lbry:// {input}, lbry://@short {input}, @lbry://longername {input}
|
||||||
|
// The spacing/alignment isn't very robust and will probably need to be changed
|
||||||
|
// if we use this in more places
|
||||||
|
&.fieldset-group--disabled-prefix {
|
||||||
|
align-items: flex-end;
|
||||||
|
|
||||||
|
label {
|
||||||
|
min-height: 18px;
|
||||||
|
white-space: nowrap;
|
||||||
|
// Set width 0 and overflow visible so the label can act as if it's the input label and not a random text node in a side by side div
|
||||||
|
overflow: visible;
|
||||||
|
width: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
fieldset-section:first-child {
|
||||||
|
max-width: 40%;
|
||||||
|
|
||||||
|
.form-field__prefix {
|
||||||
|
white-space: nowrap;
|
||||||
|
text-overflow: ellipsis;
|
||||||
|
overflow: hidden;
|
||||||
|
padding: 0.5rem;
|
||||||
|
height: var(--height-input);
|
||||||
|
border: 1px solid;
|
||||||
|
border-top-left-radius: var(--border-radius);
|
||||||
|
border-bottom-left-radius: var(--border-radius);
|
||||||
|
border-color: var(--color-input-border);
|
||||||
|
border-right-color: var(--color-input-prefix-border);
|
||||||
|
color: var(--color-text);
|
||||||
|
background-color: var(--color-input-prefix-bg);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fieldset-section:last-child {
|
||||||
|
width: 100%;
|
||||||
|
|
||||||
|
label {
|
||||||
|
// Overwrite the input's label to wrap instead. This is usually
|
||||||
|
// an error message, which could be long in other languages.
|
||||||
|
width: 100%;
|
||||||
|
white-space: normal;
|
||||||
|
}
|
||||||
|
|
||||||
|
input {
|
||||||
|
border-left: 0;
|
||||||
|
border-top-left-radius: 0;
|
||||||
|
border-bottom-left-radius: 0;
|
||||||
|
border-color: var(--color-input-border);
|
||||||
|
padding-left: var(--spacing-xs);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.form-field--copyable {
|
||||||
|
padding: 0.2rem 0.75rem;
|
||||||
|
text-overflow: ellipsis;
|
||||||
|
user-select: text;
|
||||||
|
cursor: default;
|
||||||
|
}
|
||||||
|
|
||||||
|
.form-field--short {
|
||||||
|
width: 100%;
|
||||||
|
@media (min-width: $breakpoint-small) {
|
||||||
|
width: 25em;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.form-field--price-amount {
|
||||||
|
max-width: 6em;
|
||||||
|
}
|
||||||
|
|
||||||
|
.form-field--price-amount--auto {
|
||||||
|
width: auto;
|
||||||
|
min-width: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
|
.form-field--address {
|
||||||
|
min-width: 18em;
|
||||||
|
@media (max-width: $breakpoint-xxsmall) {
|
||||||
|
min-width: 10em;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.form-field__help {
|
||||||
|
@extend .help;
|
||||||
|
}
|
||||||
|
|
||||||
|
.form-field__help + .checkbox,
|
||||||
|
.form-field__help + .radio {
|
||||||
|
margin-top: var(--spacing-l);
|
||||||
|
}
|
||||||
|
|
||||||
|
.form-field__conjuction {
|
||||||
|
padding-top: 1rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.form-field__two-column {
|
||||||
|
@media (min-width: $breakpoint-small) {
|
||||||
|
column-count: 2;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.form-field__quick-action {
|
||||||
|
font-size: var(--font-xsmall);
|
||||||
|
}
|
||||||
|
|
||||||
|
.form-field__textarea-info {
|
||||||
|
display: flex;
|
||||||
|
flex-wrap: wrap;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: space-between;
|
||||||
|
margin-top: var(--spacing-xxs);
|
||||||
|
margin-bottom: var(--spacing-s);
|
||||||
|
}
|
||||||
|
|
||||||
|
.form-field__quick-emojis {
|
||||||
|
> *:not(:last-child) {
|
||||||
|
margin-right: var(--spacing-s);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fieldset-section {
|
||||||
|
.form-field__internal-option {
|
||||||
|
margin-top: var(--spacing-s);
|
||||||
|
margin-left: 2.2rem;
|
||||||
|
|
||||||
|
&:first-of-type {
|
||||||
|
margin-top: var(--spacing-s); // Extra specificity needed here since _section.scss is applied after this file
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.select--slim {
|
||||||
|
margin-bottom: var(--spacing-xxs);
|
||||||
|
|
||||||
|
@media (min-width: $breakpoint-small) {
|
||||||
|
max-width: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
select {
|
||||||
|
max-height: 1.5rem !important;
|
||||||
|
padding: 0 var(--spacing-xs);
|
||||||
|
padding-right: var(--spacing-l);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#automatic_dark_mode_range_start,
|
||||||
|
#automatic_dark_mode_range_end {
|
||||||
|
min-width: 6em;
|
||||||
|
}
|
||||||
|
|
||||||
|
.date-picker-input {
|
||||||
|
font-weight: bold;
|
||||||
|
|
||||||
|
.react-datetime-picker__wrapper {
|
||||||
|
border: 0;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.form-field-date-picker {
|
||||||
|
margin-bottom: var(--spacing-l);
|
||||||
|
|
||||||
|
label {
|
||||||
|
display: block;
|
||||||
|
}
|
||||||
|
|
||||||
|
.controls {
|
||||||
|
display: flex;
|
||||||
|
|
||||||
|
.date-picker-input,
|
||||||
|
.button--link {
|
||||||
|
margin-right: var(--spacing-m);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.react-datetime-picker__button {
|
||||||
|
svg {
|
||||||
|
stroke: var(--color-text);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.react-datetime-picker__button:enabled:hover .react-datetime-picker__button__icon,
|
||||||
|
.react-datetime-picker__button:enabled:focus .react-datetime-picker__button__icon {
|
||||||
|
stroke: var(--color-primary);
|
||||||
|
}
|
||||||
|
|
||||||
|
.react-date-picker__calendar {
|
||||||
|
z-index: 1000;
|
||||||
|
}
|
||||||
|
|
||||||
|
.react-calendar {
|
||||||
|
width: 350px;
|
||||||
|
max-width: 100%;
|
||||||
|
background: var(--color-card-background);
|
||||||
|
border: 1px solid #a0a096;
|
||||||
|
font-family: inherit;
|
||||||
|
line-height: 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
.react-calendar--doubleView {
|
||||||
|
width: 700px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.react-calendar--doubleView .react-calendar__viewContainer {
|
||||||
|
display: flex;
|
||||||
|
margin: -0.5em;
|
||||||
|
}
|
||||||
|
|
||||||
|
.react-calendar--doubleView .react-calendar__viewContainer > * {
|
||||||
|
width: 50%;
|
||||||
|
margin: 0.5em;
|
||||||
|
}
|
||||||
|
|
||||||
|
.react-calendar,
|
||||||
|
.react-calendar *,
|
||||||
|
.react-calendar *:before,
|
||||||
|
.react-calendar *:after {
|
||||||
|
-moz-box-sizing: border-box;
|
||||||
|
-webkit-box-sizing: border-box;
|
||||||
|
box-sizing: border-box;
|
||||||
|
padding: 2px 1px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.react-calendar button {
|
||||||
|
margin: 0;
|
||||||
|
border: 0;
|
||||||
|
outline: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.react-calendar button:enabled:hover {
|
||||||
|
cursor: pointer;
|
||||||
|
}
|
||||||
|
|
||||||
|
.react-calendar__navigation {
|
||||||
|
height: 44px;
|
||||||
|
margin-bottom: 1em;
|
||||||
|
color: var(--color-text);
|
||||||
|
}
|
||||||
|
|
||||||
|
.react-calendar__navigation__label {
|
||||||
|
color: var(--color-text);
|
||||||
|
}
|
||||||
|
|
||||||
|
.react-calendar__navigation button {
|
||||||
|
min-width: 44px;
|
||||||
|
background: none;
|
||||||
|
color: var(--color-text);
|
||||||
|
}
|
||||||
|
|
||||||
|
.react-calendar__navigation button:enabled:hover,
|
||||||
|
.react-calendar__navigation button:enabled:focus {
|
||||||
|
background: var(--color-button-alt-bg-hover);
|
||||||
|
}
|
||||||
|
|
||||||
|
.react-calendar__navigation button[disabled] {
|
||||||
|
color: var(--color-text);
|
||||||
|
}
|
||||||
|
|
||||||
|
.react-calendar__month-view__weekdays {
|
||||||
|
text-align: center;
|
||||||
|
text-transform: uppercase;
|
||||||
|
font-weight: bold;
|
||||||
|
font-size: 0.75em;
|
||||||
|
color: var(--color-text-alt);
|
||||||
|
}
|
||||||
|
|
||||||
|
.react-calendar__month-view__weekdays__weekday {
|
||||||
|
padding: 0.5em;
|
||||||
|
}
|
||||||
|
|
||||||
|
.react-calendar__month-view__weekNumbers {
|
||||||
|
font-weight: bold;
|
||||||
|
}
|
||||||
|
|
||||||
|
.react-calendar__month-view__weekNumbers .react-calendar__tile {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
font-size: 0.75em;
|
||||||
|
padding: calc(0.75em / 0.75) calc(0.5em / 0.75);
|
||||||
|
}
|
||||||
|
|
||||||
|
.react-calendar__month-view__days__day,
|
||||||
|
.react-calendar__month-view__days__day--weekend {
|
||||||
|
color: var(--color-text);
|
||||||
|
font-weight: normal;
|
||||||
|
}
|
||||||
|
|
||||||
|
.react-calendar__month-view__days__day--neighboringMonth {
|
||||||
|
color: var(--color-gray-5);
|
||||||
|
}
|
||||||
|
|
||||||
|
.react-calendar__year-view .react-calendar__tile,
|
||||||
|
.react-calendar__decade-view .react-calendar__tile,
|
||||||
|
.react-calendar__century-view .react-calendar__tile {
|
||||||
|
padding: 2em 0.5em;
|
||||||
|
}
|
||||||
|
|
||||||
|
.react-calendar__tile {
|
||||||
|
max-width: 100%;
|
||||||
|
text-align: center;
|
||||||
|
padding: 0.75em 0.5em;
|
||||||
|
background: none;
|
||||||
|
border-radius: var(--border-radius);
|
||||||
|
color: var(--color-text);
|
||||||
|
}
|
||||||
|
|
||||||
|
.react-calendar__tile:enabled:hover,
|
||||||
|
.react-calendar__tile:enabled:focus {
|
||||||
|
background: var(--color-button-alt-bg-hover);
|
||||||
|
}
|
||||||
|
|
||||||
|
.react-calendar__tile--now {
|
||||||
|
background: var(--color-button-secondary-bg);
|
||||||
|
}
|
||||||
|
|
||||||
|
.react-calendar__tile--now:enabled:hover,
|
||||||
|
.react-calendar__tile--now:enabled:focus {
|
||||||
|
background: var(--color-button-secondary-bg-hover);
|
||||||
|
}
|
||||||
|
|
||||||
|
.react-calendar__tile--hasActive {
|
||||||
|
color: var(--color-button-primary-text);
|
||||||
|
background: var(--color-button-primary-bg);
|
||||||
|
}
|
||||||
|
|
||||||
|
.react-calendar__tile--hasActive:enabled:hover,
|
||||||
|
.react-calendar__tile--hasActive:enabled:focus {
|
||||||
|
background: var(--color-button-primary-bg-hover);
|
||||||
|
}
|
||||||
|
|
||||||
|
.react-calendar__tile--active {
|
||||||
|
color: var(--color-button-primary-text);
|
||||||
|
background: var(--color-button-primary-bg);
|
||||||
|
}
|
||||||
|
|
||||||
|
.react-calendar__tile--active:enabled:hover,
|
||||||
|
.react-calendar__tile--active:enabled:focus {
|
||||||
|
background: var(--color-button-primary-bg-hover);
|
||||||
|
}
|
||||||
|
|
||||||
|
.react-calendar--selectRange .react-calendar__tile--hover {
|
||||||
|
background-color: #e6e6e6;
|
||||||
|
}
|
||||||
|
|
||||||
|
.react-datetime-picker__inputGroup__amPm {
|
||||||
|
background: var(--color-input-bg);
|
||||||
|
}
|
||||||
|
|
||||||
|
.react-datetime-picker__inputGroup__leadingZero {
|
||||||
|
// Not perfect, but good enough for our standard zoom levels.
|
||||||
|
margin-bottom: 1px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.react-datetime-picker__inputGroup__input--hasLeadingZero {
|
||||||
|
margin-left: -0.54em;
|
||||||
|
padding-left: calc(1px + 0.54em);
|
||||||
|
}
|
||||||
|
|
||||||
|
.react-calendar__month-view__days__day--neighboringMonth {
|
||||||
|
color: var(--color-gray-5);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.form-field-calendar {
|
||||||
|
border-radius: var(--border-radius);
|
||||||
|
border: 1px solid var(--color-border);
|
||||||
|
margin-left: calc(var(--spacing-xs) * -1);
|
||||||
|
margin-bottom: var(--spacing-xs);
|
||||||
|
animation: menu-animate-in var(--animation-duration) var(--animation-style);
|
||||||
|
box-shadow: 3px 3px rgba(0, 0, 0, 0.1);
|
||||||
|
}
|
Loading…
Reference in a new issue