feat: new proj moderation page (#6044)
* feat: new proj moderation page * make requested changes * add boolean for showing delay message * fix server icon + shortened code * fix server icon * refactor admonitions * msg correction. * correction + change spam-notice * Separate status info from instruction details * Tweak timing delay msg, thread activity warning, and refer to moderation with consistent terms. * Whoops, actually updated msgs correctly now. * prepr + margin * split out strings, simplify code again * fix: a few more moderation fixes (#6048) * fix: move tooltip to button * fix: lock status buttons after pressing * fix: unlisted/withheld icon on legacy badge * prepprrr * fix banners, add some extra dev mode stuff * fix thread id copy padding * tweak: adjust some of the status change messages (#6041) * update messages & bunch of other stuff * rename toggle * change hover to 2.5, fix error size * private msg overlay --------- Co-authored-by: coolbot100s <76798835+coolbot100s@users.noreply.github.com>
This commit is contained in:
@@ -24,7 +24,7 @@
|
||||
{{ relativeTimeLabel }}
|
||||
</span>
|
||||
</div>
|
||||
<div class="font-normal text-contrast/85">
|
||||
<div class="font-normal text-contrast/85 leading-tight">
|
||||
<slot>{{ body }}</slot>
|
||||
</div>
|
||||
<div v-if="showActionsUnderneath || $slots.actions" class="mt-2">
|
||||
@@ -80,7 +80,7 @@ import ButtonStyled from './ButtonStyled.vue'
|
||||
|
||||
const props = withDefaults(
|
||||
defineProps<{
|
||||
type?: 'info' | 'warning' | 'critical' | 'success'
|
||||
type?: 'info' | 'warning' | 'critical' | 'success' | 'moderation'
|
||||
header?: string
|
||||
body?: string
|
||||
showActionsUnderneath?: boolean
|
||||
@@ -141,6 +141,7 @@ const typeClasses = {
|
||||
warning: 'border-brand-orange bg-bg-orange',
|
||||
critical: 'border-brand-red bg-bg-red',
|
||||
success: 'border-brand-green bg-bg-green',
|
||||
moderation: 'border-brand-orange bg-bg-orange',
|
||||
}
|
||||
|
||||
const iconClasses = {
|
||||
@@ -148,6 +149,7 @@ const iconClasses = {
|
||||
warning: 'text-brand-orange',
|
||||
critical: 'text-brand-red',
|
||||
success: 'text-brand-green',
|
||||
moderation: 'text-brand-orange',
|
||||
}
|
||||
|
||||
const buttonColors = {
|
||||
@@ -155,6 +157,7 @@ const buttonColors = {
|
||||
warning: 'orange',
|
||||
critical: 'red',
|
||||
success: 'green',
|
||||
moderation: 'orange',
|
||||
} as const
|
||||
|
||||
const progressTrackClasses = {
|
||||
@@ -162,6 +165,7 @@ const progressTrackClasses = {
|
||||
warning: 'bg-brand-orange/20',
|
||||
critical: 'bg-brand-red/20',
|
||||
success: 'bg-brand-green/20',
|
||||
moderation: 'bg-brand-orange/20',
|
||||
}
|
||||
|
||||
const progressFillClasses = {
|
||||
|
||||
@@ -21,10 +21,10 @@
|
||||
<CheckIcon aria-hidden="true" /> {{ formatMessage(messages.approvedLabel) }}
|
||||
</template>
|
||||
<template v-else-if="type === 'unlisted'">
|
||||
<EyeOffIcon aria-hidden="true" /> {{ formatMessage(messages.unlistedLabel) }}
|
||||
<LinkIcon aria-hidden="true" /> {{ formatMessage(messages.unlistedLabel) }}
|
||||
</template>
|
||||
<template v-else-if="type === 'withheld'">
|
||||
<EyeOffIcon aria-hidden="true" /> {{ formatMessage(messages.withheldLabel) }}
|
||||
<LinkIcon aria-hidden="true" /> {{ formatMessage(messages.withheldLabel) }}
|
||||
</template>
|
||||
<template v-else-if="type === 'private'">
|
||||
<LockIcon aria-hidden="true" /> {{ formatMessage(messages.privateLabel) }}
|
||||
@@ -89,9 +89,9 @@ import {
|
||||
BugIcon,
|
||||
CalendarIcon,
|
||||
CheckIcon,
|
||||
EyeOffIcon,
|
||||
FileTextIcon,
|
||||
GlobeIcon,
|
||||
LinkIcon,
|
||||
LockIcon,
|
||||
ModrinthIcon,
|
||||
ScaleIcon,
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
<template>
|
||||
<button
|
||||
class="group bg-transparent border-none p-0 m-0 flex items-center gap-3 checkbox-outer outline-offset-4 text-contrast"
|
||||
class="group bg-transparent border-none p-0 m-0 flex items-center text-left gap-3 checkbox-outer outline-offset-4 text-contrast"
|
||||
:disabled="disabled"
|
||||
:class="
|
||||
disabled
|
||||
@@ -13,7 +13,7 @@
|
||||
@click="toggle"
|
||||
>
|
||||
<span
|
||||
class="w-5 h-5 rounded-md flex items-center justify-center border-[1px] border-solid"
|
||||
class="w-5 h-5 rounded-md flex items-center justify-center border-[1px] border-solid shrink-0"
|
||||
:class="{
|
||||
'bg-brand border-button-border text-brand-inverted': modelValue,
|
||||
'bg-surface-2 border-surface-5 text-primary': !modelValue,
|
||||
|
||||
@@ -1,39 +1,41 @@
|
||||
<template>
|
||||
<NewModal ref="linkModal" header="Insert link">
|
||||
<NewModal ref="linkModal" :header="formatMessage(messages.linkModalHeader)" class="!w-[40rem]">
|
||||
<div class="modal-insert">
|
||||
<label class="label" for="insert-link-label">
|
||||
<span class="label__title">Label</span>
|
||||
<span class="label__title">{{ formatMessage(messages.linkModalLabelFieldTitle) }}</span>
|
||||
</label>
|
||||
<StyledInput
|
||||
id="insert-link-label"
|
||||
v-model="linkText"
|
||||
:icon="AlignLeftIcon"
|
||||
type="text"
|
||||
placeholder="Enter label..."
|
||||
:placeholder="formatMessage(messages.linkModalLabelFieldPlaceholder)"
|
||||
clearable
|
||||
wrapper-class="w-full"
|
||||
/>
|
||||
<label class="label" for="insert-link-url">
|
||||
<span class="label__title">URL<span class="required">*</span></span>
|
||||
<span class="label__title">
|
||||
{{ formatMessage(messages.urlLabel) }}<span class="required">*</span>
|
||||
</span>
|
||||
</label>
|
||||
<StyledInput
|
||||
id="insert-link-url"
|
||||
v-model="linkUrl"
|
||||
:icon="LinkIcon"
|
||||
type="text"
|
||||
placeholder="Enter the link's URL..."
|
||||
:placeholder="formatMessage(messages.linkModalUrlFieldPlaceholder)"
|
||||
clearable
|
||||
wrapper-class="w-full"
|
||||
@input="validateURL"
|
||||
/>
|
||||
<template v-if="linkValidationErrorMessage">
|
||||
<span class="label">
|
||||
<span class="label__title">Error</span>
|
||||
<span class="label__title">{{ formatMessage(messages.errorLabel) }}</span>
|
||||
<span class="label__description">{{ linkValidationErrorMessage }}</span>
|
||||
</span>
|
||||
</template>
|
||||
<span class="label">
|
||||
<span class="label__title">Preview</span>
|
||||
<span class="label__title">{{ formatMessage(messages.previewLabel) }}</span>
|
||||
<span class="label__description"></span>
|
||||
</span>
|
||||
<div class="markdown-body-wrapper">
|
||||
@@ -45,7 +47,9 @@
|
||||
</div>
|
||||
<div class="flex gap-2 justify-end mt-4">
|
||||
<ButtonStyled type="outlined">
|
||||
<button @click="() => linkModal?.hide()"><XIcon /> Cancel</button>
|
||||
<button @click="() => linkModal?.hide()">
|
||||
<XIcon /> {{ formatMessage(commonMessages.cancelButton) }}
|
||||
</button>
|
||||
</ButtonStyled>
|
||||
<ButtonStyled color="brand">
|
||||
<button
|
||||
@@ -57,18 +61,21 @@
|
||||
}
|
||||
"
|
||||
>
|
||||
<PlusIcon /> Insert
|
||||
<PlusIcon /> {{ formatMessage(messages.insertButton) }}
|
||||
</button>
|
||||
</ButtonStyled>
|
||||
</div>
|
||||
</div>
|
||||
</NewModal>
|
||||
<NewModal ref="imageModal" header="Insert image">
|
||||
<NewModal ref="imageModal" :header="formatMessage(messages.imageModalHeader)" class="!w-[40rem]">
|
||||
<div class="modal-insert">
|
||||
<label class="label" for="insert-image-alt">
|
||||
<span class="label__title">Description (alt text)<span class="required">*</span></span>
|
||||
<span class="label__title">
|
||||
{{ formatMessage(messages.imageModalDescriptionFieldTitle) }}
|
||||
<span class="required">*</span>
|
||||
</span>
|
||||
<span class="label__description">
|
||||
Describe the image completely as you would to someone who could not see the image.
|
||||
{{ formatMessage(messages.imageModalDescriptionFieldDescription) }}
|
||||
</span>
|
||||
</label>
|
||||
<StyledInput
|
||||
@@ -76,15 +83,22 @@
|
||||
v-model="linkText"
|
||||
:icon="AlignLeftIcon"
|
||||
type="text"
|
||||
placeholder="Describe the image..."
|
||||
:placeholder="formatMessage(messages.imageModalDescriptionFieldPlaceholder)"
|
||||
clearable
|
||||
wrapper-class="w-full"
|
||||
/>
|
||||
<label class="label" for="insert-link-url">
|
||||
<span class="label__title">URL<span class="required">*</span></span>
|
||||
<span class="label__title">
|
||||
{{ formatMessage(messages.urlLabel) }}<span class="required">*</span>
|
||||
</span>
|
||||
</label>
|
||||
<div v-if="props.onImageUpload" class="image-strategy-chips">
|
||||
<Chips v-model="imageUploadOption" :items="['upload', 'link']" />
|
||||
<Chips
|
||||
v-model="imageUploadOption"
|
||||
:items="['upload', 'link']"
|
||||
:format-label="formatImageUploadOption"
|
||||
:aria-label="formatMessage(messages.imageModalUploadModeLabel)"
|
||||
/>
|
||||
</div>
|
||||
<div
|
||||
v-if="props.onImageUpload && imageUploadOption === 'upload'"
|
||||
@@ -92,7 +106,7 @@
|
||||
>
|
||||
<FileInput
|
||||
accept="image/png,image/jpeg,image/gif,image/webp"
|
||||
prompt="Drag and drop to upload or click to select file"
|
||||
:prompt="formatMessage(messages.imageModalUploadPrompt)"
|
||||
long-style
|
||||
should-always-reset
|
||||
class="file-input"
|
||||
@@ -107,19 +121,19 @@
|
||||
v-model="linkUrl"
|
||||
:icon="ImageIcon"
|
||||
type="text"
|
||||
placeholder="Enter the image URL..."
|
||||
:placeholder="formatMessage(messages.imageModalUrlFieldPlaceholder)"
|
||||
clearable
|
||||
wrapper-class="w-full"
|
||||
@input="validateURL"
|
||||
/>
|
||||
<template v-if="linkValidationErrorMessage">
|
||||
<span class="label">
|
||||
<span class="label__title">Error</span>
|
||||
<span class="label__title">{{ formatMessage(messages.errorLabel) }}</span>
|
||||
<span class="label__description">{{ linkValidationErrorMessage }}</span>
|
||||
</span>
|
||||
</template>
|
||||
<span class="label">
|
||||
<span class="label__title">Preview</span>
|
||||
<span class="label__title">{{ formatMessage(messages.previewLabel) }}</span>
|
||||
<span class="label__description"></span>
|
||||
</span>
|
||||
<div class="markdown-body-wrapper">
|
||||
@@ -131,7 +145,9 @@
|
||||
</div>
|
||||
<div class="flex gap-2 justify-end mt-4">
|
||||
<ButtonStyled type="outlined">
|
||||
<button @click="() => imageModal?.hide()"><XIcon /> Cancel</button>
|
||||
<button @click="() => imageModal?.hide()">
|
||||
<XIcon /> {{ formatMessage(commonMessages.cancelButton) }}
|
||||
</button>
|
||||
</ButtonStyled>
|
||||
<ButtonStyled color="brand">
|
||||
<button
|
||||
@@ -143,36 +159,40 @@
|
||||
}
|
||||
"
|
||||
>
|
||||
<PlusIcon /> Insert
|
||||
<PlusIcon /> {{ formatMessage(messages.insertButton) }}
|
||||
</button>
|
||||
</ButtonStyled>
|
||||
</div>
|
||||
</div>
|
||||
</NewModal>
|
||||
<NewModal ref="videoModal" header="Insert YouTube video">
|
||||
<NewModal ref="videoModal" :header="formatMessage(messages.videoModalHeader)" class="!w-[40rem]">
|
||||
<div class="modal-insert">
|
||||
<label class="label" for="insert-video-url">
|
||||
<span class="label__title">YouTube video URL<span class="required">*</span></span>
|
||||
<span class="label__description"> Enter a valid link to a YouTube video. </span>
|
||||
<span class="label__title">
|
||||
{{ formatMessage(messages.videoModalUrlFieldTitle) }}<span class="required">*</span>
|
||||
</span>
|
||||
<span class="label__description">
|
||||
{{ formatMessage(messages.videoModalUrlFieldDescription) }}
|
||||
</span>
|
||||
</label>
|
||||
<StyledInput
|
||||
id="insert-video-url"
|
||||
v-model="linkUrl"
|
||||
:icon="YouTubeIcon"
|
||||
type="text"
|
||||
placeholder="Enter YouTube video URL"
|
||||
:placeholder="formatMessage(messages.videoModalUrlFieldPlaceholder)"
|
||||
clearable
|
||||
wrapper-class="w-full"
|
||||
@input="validateURL"
|
||||
/>
|
||||
<template v-if="linkValidationErrorMessage">
|
||||
<span class="label">
|
||||
<span class="label__title">Error</span>
|
||||
<span class="label__title">{{ formatMessage(messages.errorLabel) }}</span>
|
||||
<span class="label__description">{{ linkValidationErrorMessage }}</span>
|
||||
</span>
|
||||
</template>
|
||||
<span class="label">
|
||||
<span class="label__title">Preview</span>
|
||||
<span class="label__title">{{ formatMessage(messages.previewLabel) }}</span>
|
||||
<span class="label__description"></span>
|
||||
</span>
|
||||
|
||||
@@ -185,7 +205,9 @@
|
||||
</div>
|
||||
<div class="flex gap-2 justify-end mt-4">
|
||||
<ButtonStyled type="outlined">
|
||||
<button @click="() => videoModal?.hide()"><XIcon /> Cancel</button>
|
||||
<button @click="() => videoModal?.hide()">
|
||||
<XIcon /> {{ formatMessage(commonMessages.cancelButton) }}
|
||||
</button>
|
||||
</ButtonStyled>
|
||||
<ButtonStyled color="brand">
|
||||
<button
|
||||
@@ -197,37 +219,41 @@
|
||||
}
|
||||
"
|
||||
>
|
||||
<PlusIcon /> Insert
|
||||
<PlusIcon /> {{ formatMessage(messages.insertButton) }}
|
||||
</button>
|
||||
</ButtonStyled>
|
||||
</div>
|
||||
</div>
|
||||
</NewModal>
|
||||
<div class="block grow w-full">
|
||||
<div class="editor-action-row">
|
||||
<div class="editor-actions">
|
||||
<template
|
||||
v-for="(buttonGroup, _i) in Object.values(BUTTONS).filter((bg) => bg.display)"
|
||||
:key="_i"
|
||||
>
|
||||
<div class="divider"></div>
|
||||
<template v-for="button in buttonGroup.buttons" :key="button.label">
|
||||
<ButtonStyled circular>
|
||||
<button
|
||||
v-tooltip="button.label"
|
||||
:aria-label="button.label"
|
||||
:class="{ 'mobile-hidden-group': !!buttonGroup.hideOnMobile }"
|
||||
:disabled="previewMode || disabled"
|
||||
@click="() => button.action(editor)"
|
||||
>
|
||||
<component :is="button.icon" />
|
||||
</button>
|
||||
</ButtonStyled>
|
||||
<div class="editor-action-row w-full">
|
||||
<div class="w-full flex justify-between items-center flex-wrap gap-2">
|
||||
<div class="editor-actions">
|
||||
<template
|
||||
v-for="(buttonGroup, _i) in Object.values(BUTTONS).filter((bg) => bg.display)"
|
||||
:key="_i"
|
||||
>
|
||||
<div class="divider"></div>
|
||||
<template v-for="button in buttonGroup.buttons" :key="button.label.id">
|
||||
<ButtonStyled circular>
|
||||
<button
|
||||
v-tooltip="formatMessage(button.label)"
|
||||
:aria-label="formatMessage(button.label)"
|
||||
:class="{ 'mobile-hidden-group': !!buttonGroup.hideOnMobile }"
|
||||
:disabled="previewMode || disabled"
|
||||
@click="() => button.action(editor)"
|
||||
>
|
||||
<component :is="button.icon" />
|
||||
</button>
|
||||
</ButtonStyled>
|
||||
</template>
|
||||
</template>
|
||||
</template>
|
||||
<div class="preview">
|
||||
<Toggle id="preview" v-model="previewMode" />
|
||||
<label class="label" for="preview"> Preview </label>
|
||||
</div>
|
||||
<div class="flex items-center gap-2">
|
||||
<Toggle id="preview" v-model="previewMode" small />
|
||||
<label class="label" for="preview">
|
||||
{{ formatMessage(messages.editorPreviewToggleLabel) }}
|
||||
</label>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@@ -235,20 +261,29 @@
|
||||
<div v-if="!previewMode" class="info-blurb mt-2">
|
||||
<div class="info-blurb">
|
||||
<InfoIcon />
|
||||
<span
|
||||
>This editor supports
|
||||
<a
|
||||
class="markdown-resource-link"
|
||||
href="https://support.modrinth.com/en/articles/8801962-advanced-markdown-formatting"
|
||||
target="_blank"
|
||||
>Markdown formatting</a
|
||||
>.</span
|
||||
>
|
||||
<IntlFormatted :message-id="messages.editorMarkdownFormattingSupport">
|
||||
<template #markdown-link="{ children }">
|
||||
<a
|
||||
class="markdown-resource-link"
|
||||
href="https://support.modrinth.com/en/articles/8801962-advanced-markdown-formatting"
|
||||
target="_blank"
|
||||
>
|
||||
<component :is="() => children" />
|
||||
</a>
|
||||
</template>
|
||||
</IntlFormatted>
|
||||
</div>
|
||||
<div :class="{ hide: !props.maxLength }" class="max-length-label">
|
||||
<span>Max length: </span>
|
||||
<span>{{ formatMessage(messages.editorMaxLengthLabel) }} </span>
|
||||
<span>
|
||||
{{ props.maxLength ? `${currentValue?.length || 0}/${props.maxLength}` : 'Unlimited' }}
|
||||
{{
|
||||
props.maxLength
|
||||
? formatMessage(messages.editorMaxLengthValue, {
|
||||
currentLength: currentValue?.length || 0,
|
||||
maxLength: props.maxLength,
|
||||
})
|
||||
: formatMessage(messages.editorMaxLengthUnlimited)
|
||||
}}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
@@ -298,13 +333,214 @@ import { markdownCommands, modrinthMarkdownEditorKeymap } from '@modrinth/utils/
|
||||
import { renderHighlightedString } from '@modrinth/utils/highlightjs'
|
||||
import { type Component, computed, onBeforeUnmount, onMounted, ref, toRef, watch } from 'vue'
|
||||
|
||||
import { defineMessages, type MessageDescriptor, useVIntl } from '../../composables/i18n'
|
||||
import { commonMessages } from '../../utils/common-messages.ts'
|
||||
import NewModal from '../modal/NewModal.vue'
|
||||
import ButtonStyled from './ButtonStyled.vue'
|
||||
import Chips from './Chips.vue'
|
||||
import FileInput from './FileInput.vue'
|
||||
import IntlFormatted from './IntlFormatted.vue'
|
||||
import StyledInput from './StyledInput.vue'
|
||||
import Toggle from './Toggle.vue'
|
||||
|
||||
const { formatMessage } = useVIntl()
|
||||
|
||||
const messages = defineMessages({
|
||||
insertButton: {
|
||||
id: 'markdown-editor.insert-button',
|
||||
defaultMessage: 'Insert',
|
||||
},
|
||||
urlLabel: {
|
||||
id: 'markdown-editor.url-label',
|
||||
defaultMessage: 'URL',
|
||||
},
|
||||
errorLabel: {
|
||||
id: 'markdown-editor.error-label',
|
||||
defaultMessage: 'Error',
|
||||
},
|
||||
previewLabel: {
|
||||
id: 'markdown-editor.preview-label',
|
||||
defaultMessage: 'Preview',
|
||||
},
|
||||
linkModalHeader: {
|
||||
id: 'markdown-editor.link-modal.header',
|
||||
defaultMessage: 'Insert link',
|
||||
},
|
||||
linkModalLabelFieldTitle: {
|
||||
id: 'markdown-editor.link-modal.label-field.title',
|
||||
defaultMessage: 'Label',
|
||||
},
|
||||
linkModalLabelFieldPlaceholder: {
|
||||
id: 'markdown-editor.link-modal.label-field.placeholder',
|
||||
defaultMessage: 'Enter label...',
|
||||
},
|
||||
linkModalUrlFieldPlaceholder: {
|
||||
id: 'markdown-editor.link-modal.url-field.placeholder',
|
||||
defaultMessage: "Enter the link's URL...",
|
||||
},
|
||||
imageModalHeader: {
|
||||
id: 'markdown-editor.image-modal.header',
|
||||
defaultMessage: 'Insert image',
|
||||
},
|
||||
imageModalDescriptionFieldTitle: {
|
||||
id: 'markdown-editor.image-modal.description-field.title',
|
||||
defaultMessage: 'Description (alt text)',
|
||||
},
|
||||
imageModalDescriptionFieldDescription: {
|
||||
id: 'markdown-editor.image-modal.description-field.description',
|
||||
defaultMessage:
|
||||
'Describe the image completely as you would to someone who could not see the image.',
|
||||
},
|
||||
imageModalDescriptionFieldPlaceholder: {
|
||||
id: 'markdown-editor.image-modal.description-field.placeholder',
|
||||
defaultMessage: 'Describe the image...',
|
||||
},
|
||||
imageModalUploadModeLabel: {
|
||||
id: 'markdown-editor.image-modal.upload-mode.label',
|
||||
defaultMessage: 'Image source',
|
||||
},
|
||||
imageModalUploadModeUpload: {
|
||||
id: 'markdown-editor.image-modal.upload-mode.upload',
|
||||
defaultMessage: 'Upload',
|
||||
},
|
||||
imageModalUploadModeLink: {
|
||||
id: 'markdown-editor.image-modal.upload-mode.link',
|
||||
defaultMessage: 'Link',
|
||||
},
|
||||
imageModalUploadPrompt: {
|
||||
id: 'markdown-editor.image-modal.upload.prompt',
|
||||
defaultMessage: 'Drag and drop to upload or click to select file',
|
||||
},
|
||||
imageModalUrlFieldPlaceholder: {
|
||||
id: 'markdown-editor.image-modal.url-field.placeholder',
|
||||
defaultMessage: 'Enter the image URL...',
|
||||
},
|
||||
videoModalHeader: {
|
||||
id: 'markdown-editor.video-modal.header',
|
||||
defaultMessage: 'Insert YouTube video',
|
||||
},
|
||||
videoModalUrlFieldTitle: {
|
||||
id: 'markdown-editor.video-modal.url-field.title',
|
||||
defaultMessage: 'YouTube video URL',
|
||||
},
|
||||
videoModalUrlFieldDescription: {
|
||||
id: 'markdown-editor.video-modal.url-field.description',
|
||||
defaultMessage: 'Enter a valid link to a YouTube video.',
|
||||
},
|
||||
videoModalUrlFieldPlaceholder: {
|
||||
id: 'markdown-editor.video-modal.url-field.placeholder',
|
||||
defaultMessage: 'Enter YouTube video URL',
|
||||
},
|
||||
editorPreviewToggleLabel: {
|
||||
id: 'markdown-editor.preview-toggle.label',
|
||||
defaultMessage: 'Preview',
|
||||
},
|
||||
editorMarkdownFormattingSupport: {
|
||||
id: 'markdown-editor.markdown-formatting-support',
|
||||
defaultMessage: 'This editor supports <markdown-link>Markdown formatting</markdown-link>.',
|
||||
},
|
||||
editorMaxLengthLabel: {
|
||||
id: 'markdown-editor.max-length.label',
|
||||
defaultMessage: 'Max length:',
|
||||
},
|
||||
editorMaxLengthValue: {
|
||||
id: 'markdown-editor.max-length.value',
|
||||
defaultMessage: '{currentLength}/{maxLength}',
|
||||
},
|
||||
editorMaxLengthUnlimited: {
|
||||
id: 'markdown-editor.max-length.unlimited',
|
||||
defaultMessage: 'Unlimited',
|
||||
},
|
||||
editorPlaceholder: {
|
||||
id: 'markdown-editor.placeholder',
|
||||
defaultMessage: 'Write something...',
|
||||
},
|
||||
toolbarHeading1: {
|
||||
id: 'markdown-editor.toolbar.heading-1',
|
||||
defaultMessage: 'Heading 1',
|
||||
},
|
||||
toolbarHeading2: {
|
||||
id: 'markdown-editor.toolbar.heading-2',
|
||||
defaultMessage: 'Heading 2',
|
||||
},
|
||||
toolbarHeading3: {
|
||||
id: 'markdown-editor.toolbar.heading-3',
|
||||
defaultMessage: 'Heading 3',
|
||||
},
|
||||
toolbarBold: {
|
||||
id: 'markdown-editor.toolbar.bold',
|
||||
defaultMessage: 'Bold',
|
||||
},
|
||||
toolbarItalic: {
|
||||
id: 'markdown-editor.toolbar.italic',
|
||||
defaultMessage: 'Italic',
|
||||
},
|
||||
toolbarStrikethrough: {
|
||||
id: 'markdown-editor.toolbar.strikethrough',
|
||||
defaultMessage: 'Strikethrough',
|
||||
},
|
||||
toolbarCode: {
|
||||
id: 'markdown-editor.toolbar.code',
|
||||
defaultMessage: 'Code',
|
||||
},
|
||||
toolbarSpoiler: {
|
||||
id: 'markdown-editor.toolbar.spoiler',
|
||||
defaultMessage: 'Spoiler',
|
||||
},
|
||||
toolbarBulletedList: {
|
||||
id: 'markdown-editor.toolbar.bulleted-list',
|
||||
defaultMessage: 'Bulleted list',
|
||||
},
|
||||
toolbarOrderedList: {
|
||||
id: 'markdown-editor.toolbar.ordered-list',
|
||||
defaultMessage: 'Ordered list',
|
||||
},
|
||||
toolbarQuote: {
|
||||
id: 'markdown-editor.toolbar.quote',
|
||||
defaultMessage: 'Quote',
|
||||
},
|
||||
toolbarLink: {
|
||||
id: 'markdown-editor.toolbar.link',
|
||||
defaultMessage: 'Link',
|
||||
},
|
||||
toolbarImage: {
|
||||
id: 'markdown-editor.toolbar.image',
|
||||
defaultMessage: 'Image',
|
||||
},
|
||||
toolbarVideo: {
|
||||
id: 'markdown-editor.toolbar.video',
|
||||
defaultMessage: 'Video',
|
||||
},
|
||||
videoEmbedTitle: {
|
||||
id: 'markdown-editor.video-embed.title',
|
||||
defaultMessage: 'YouTube video player',
|
||||
},
|
||||
urlValidationErrorMalformed: {
|
||||
id: 'markdown-editor.url-validation-error.malformed',
|
||||
defaultMessage: 'Invalid URL. Make sure the URL is well-formed.',
|
||||
},
|
||||
urlValidationErrorUnsupportedProtocol: {
|
||||
id: 'markdown-editor.url-validation-error.unsupported-protocol',
|
||||
defaultMessage: 'Unsupported protocol. Use http or https.',
|
||||
},
|
||||
urlValidationErrorBlockedDomain: {
|
||||
id: 'markdown-editor.url-validation-error.blocked-domain',
|
||||
defaultMessage: 'Invalid URL. This domain is not allowed.',
|
||||
},
|
||||
uploadErrorNoHandler: {
|
||||
id: 'markdown-editor.upload-error.no-handler',
|
||||
defaultMessage: 'No image upload handler provided',
|
||||
},
|
||||
uploadErrorNoFile: {
|
||||
id: 'markdown-editor.upload-error.no-file',
|
||||
defaultMessage: 'No file provided',
|
||||
},
|
||||
defaultImageAltText: {
|
||||
id: 'markdown-editor.default-image-alt-text',
|
||||
defaultMessage: 'Replace this with a description',
|
||||
},
|
||||
})
|
||||
|
||||
const props = withDefaults(
|
||||
defineProps<{
|
||||
modelValue: string
|
||||
@@ -325,7 +561,7 @@ const props = withDefaults(
|
||||
disabled: false,
|
||||
headingButtons: true,
|
||||
onImageUpload: undefined,
|
||||
placeholder: 'Write something...',
|
||||
placeholder: undefined,
|
||||
maxLength: undefined,
|
||||
maxHeight: undefined,
|
||||
minHeight: undefined,
|
||||
@@ -338,6 +574,9 @@ let isDisabledCompartment: Compartment | null = null
|
||||
let editorThemeCompartment: Compartment | null = null
|
||||
|
||||
const emit = defineEmits(['update:modelValue'])
|
||||
const resolvedPlaceholder = computed(
|
||||
() => props.placeholder ?? formatMessage(messages.editorPlaceholder),
|
||||
)
|
||||
|
||||
onMounted(() => {
|
||||
const updateListener = EditorView.updateListener.of((update) => {
|
||||
@@ -393,7 +632,7 @@ onMounted(() => {
|
||||
uploadImagesFromList(clipboardData.files)
|
||||
.then(function (url) {
|
||||
const selection = markdownCommands.yankSelection(view)
|
||||
const altText = selection || 'Replace this with a description'
|
||||
const altText = selection || formatMessage(messages.defaultImageAltText)
|
||||
const linkMarkdown = ``
|
||||
return markdownCommands.replaceSelection(view, linkMarkdown)
|
||||
})
|
||||
@@ -466,7 +705,7 @@ onMounted(() => {
|
||||
addKeymap: false,
|
||||
}),
|
||||
keymap.of(historyKeymap),
|
||||
cm_placeholder(props.placeholder || ''),
|
||||
cm_placeholder(resolvedPlaceholder.value),
|
||||
inputFilter,
|
||||
isDisabledCompartment.of(disabledCompartment),
|
||||
editorThemeCompartment.of(theme),
|
||||
@@ -494,7 +733,7 @@ onBeforeUnmount(() => {
|
||||
})
|
||||
|
||||
type ButtonAction = {
|
||||
label: string
|
||||
label: MessageDescriptor
|
||||
icon: Component
|
||||
action: (editor: EditorView | null) => void
|
||||
}
|
||||
@@ -515,12 +754,12 @@ function runEditorCommand(command: (view: EditorView) => boolean, editor: Editor
|
||||
}
|
||||
|
||||
const composeCommandButton = (
|
||||
name: string,
|
||||
label: MessageDescriptor,
|
||||
icon: Component,
|
||||
command: (view: EditorView) => boolean,
|
||||
) => {
|
||||
return {
|
||||
label: name,
|
||||
label,
|
||||
icon,
|
||||
action: (e: EditorView | null) => runEditorCommand(command, e),
|
||||
}
|
||||
@@ -531,33 +770,41 @@ const BUTTONS: ButtonGroupMap = {
|
||||
display: props.headingButtons,
|
||||
hideOnMobile: false,
|
||||
buttons: [
|
||||
composeCommandButton('Heading 1', Heading1Icon, markdownCommands.toggleHeader),
|
||||
composeCommandButton('Heading 2', Heading2Icon, markdownCommands.toggleHeader2),
|
||||
composeCommandButton('Heading 3', Heading3Icon, markdownCommands.toggleHeader3),
|
||||
composeCommandButton(messages.toolbarHeading1, Heading1Icon, markdownCommands.toggleHeader),
|
||||
composeCommandButton(messages.toolbarHeading2, Heading2Icon, markdownCommands.toggleHeader2),
|
||||
composeCommandButton(messages.toolbarHeading3, Heading3Icon, markdownCommands.toggleHeader3),
|
||||
],
|
||||
},
|
||||
stylizing: {
|
||||
display: true,
|
||||
hideOnMobile: false,
|
||||
buttons: [
|
||||
composeCommandButton('Bold', BoldIcon, markdownCommands.toggleBold),
|
||||
composeCommandButton('Italic', ItalicIcon, markdownCommands.toggleItalic),
|
||||
composeCommandButton(messages.toolbarBold, BoldIcon, markdownCommands.toggleBold),
|
||||
composeCommandButton(messages.toolbarItalic, ItalicIcon, markdownCommands.toggleItalic),
|
||||
composeCommandButton(
|
||||
'Strikethrough',
|
||||
messages.toolbarStrikethrough,
|
||||
StrikethroughIcon,
|
||||
markdownCommands.toggleStrikethrough,
|
||||
),
|
||||
composeCommandButton('Code', CodeIcon, markdownCommands.toggleCodeBlock),
|
||||
composeCommandButton('Spoiler', ScanEyeIcon, markdownCommands.toggleSpoiler),
|
||||
composeCommandButton(messages.toolbarCode, CodeIcon, markdownCommands.toggleCodeBlock),
|
||||
composeCommandButton(messages.toolbarSpoiler, ScanEyeIcon, markdownCommands.toggleSpoiler),
|
||||
],
|
||||
},
|
||||
lists: {
|
||||
display: true,
|
||||
hideOnMobile: false,
|
||||
buttons: [
|
||||
composeCommandButton('Bulleted list', ListBulletedIcon, markdownCommands.toggleBulletList),
|
||||
composeCommandButton('Ordered list', ListOrderedIcon, markdownCommands.toggleOrderedList),
|
||||
composeCommandButton('Quote', TextQuoteIcon, markdownCommands.toggleQuote),
|
||||
composeCommandButton(
|
||||
messages.toolbarBulletedList,
|
||||
ListBulletedIcon,
|
||||
markdownCommands.toggleBulletList,
|
||||
),
|
||||
composeCommandButton(
|
||||
messages.toolbarOrderedList,
|
||||
ListOrderedIcon,
|
||||
markdownCommands.toggleOrderedList,
|
||||
),
|
||||
composeCommandButton(messages.toolbarQuote, TextQuoteIcon, markdownCommands.toggleQuote),
|
||||
],
|
||||
},
|
||||
components: {
|
||||
@@ -565,17 +812,17 @@ const BUTTONS: ButtonGroupMap = {
|
||||
hideOnMobile: false,
|
||||
buttons: [
|
||||
{
|
||||
label: 'Link',
|
||||
label: messages.toolbarLink,
|
||||
icon: LinkIcon,
|
||||
action: () => openLinkModal(),
|
||||
},
|
||||
{
|
||||
label: 'Image',
|
||||
label: messages.toolbarImage,
|
||||
icon: ImageIcon,
|
||||
action: () => openImageModal(),
|
||||
},
|
||||
{
|
||||
label: 'Video',
|
||||
label: messages.toolbarVideo,
|
||||
icon: YouTubeIcon,
|
||||
action: () => openVideoModal(),
|
||||
},
|
||||
@@ -693,12 +940,12 @@ function cleanUrl(input: string): string {
|
||||
try {
|
||||
url = new URL(input)
|
||||
} catch {
|
||||
throw new Error('Invalid URL. Make sure the URL is well-formed.')
|
||||
throw new Error(formatMessage(messages.urlValidationErrorMalformed))
|
||||
}
|
||||
|
||||
// Check for unsupported protocols
|
||||
if (url.protocol !== 'http:' && url.protocol !== 'https:') {
|
||||
throw new Error('Unsupported protocol. Use http or https.')
|
||||
throw new Error(formatMessage(messages.urlValidationErrorUnsupportedProtocol))
|
||||
}
|
||||
|
||||
// If the scheme is "http", automatically upgrade it to "https"
|
||||
@@ -709,7 +956,7 @@ function cleanUrl(input: string): string {
|
||||
// Block certain domains for compliance
|
||||
const blockedDomains = ['forgecdn', 'cdn.discordapp', 'media.discordapp']
|
||||
if (blockedDomains.some((domain) => url.hostname.includes(domain))) {
|
||||
throw new Error('Invalid URL. This domain is not allowed.')
|
||||
throw new Error(formatMessage(messages.urlValidationErrorBlockedDomain))
|
||||
}
|
||||
|
||||
return url.toString()
|
||||
@@ -733,7 +980,7 @@ const linkMarkdown = computed(() => {
|
||||
const uploadImagesFromList = async (files: FileList): Promise<string> => {
|
||||
const file = files[0]
|
||||
if (!props.onImageUpload) {
|
||||
throw new Error('No image upload handler provided')
|
||||
throw new Error(formatMessage(messages.uploadErrorNoHandler))
|
||||
}
|
||||
if (file) {
|
||||
try {
|
||||
@@ -746,7 +993,7 @@ const uploadImagesFromList = async (files: FileList): Promise<string> => {
|
||||
}
|
||||
}
|
||||
}
|
||||
throw new Error('No file provided')
|
||||
throw new Error(formatMessage(messages.uploadErrorNoFile))
|
||||
}
|
||||
|
||||
const handleImageUpload = async (files: FileList) => {
|
||||
@@ -765,6 +1012,15 @@ const handleImageUpload = async (files: FileList) => {
|
||||
}
|
||||
|
||||
const imageUploadOption = ref<string>('upload')
|
||||
function formatImageUploadOption(option: string) {
|
||||
if (option === 'upload') {
|
||||
return formatMessage(messages.imageModalUploadModeUpload)
|
||||
}
|
||||
if (option === 'link') {
|
||||
return formatMessage(messages.imageModalUploadModeLink)
|
||||
}
|
||||
return option
|
||||
}
|
||||
const imageMarkdown = computed(() => (linkMarkdown.value.length ? `!${linkMarkdown.value}` : ''))
|
||||
|
||||
const canInsertImage = computed(() => {
|
||||
@@ -781,7 +1037,7 @@ const youtubeRegex =
|
||||
const videoMarkdown = computed(() => {
|
||||
const match = youtubeRegex.exec(linkUrl.value)
|
||||
if (match) {
|
||||
return `<iframe width="560" height="315" src="https://www.youtube-nocookie.com/embed/${match[1]}" title="YouTube video player" frameborder="0" allow="accelerometer; autoplay; clipboard-write; encrypted-media; gyroscope; picture-in-picture; web-share" allowfullscreen></iframe>`
|
||||
return `<iframe width="560" height="315" src="https://www.youtube-nocookie.com/embed/${match[1]}" title="${formatMessage(messages.videoEmbedTitle)}" frameborder="0" allow="accelerometer; autoplay; clipboard-write; encrypted-media; gyroscope; picture-in-picture; web-share" allowfullscreen></iframe>`
|
||||
}
|
||||
return ''
|
||||
})
|
||||
@@ -924,11 +1180,13 @@ function openVideoModal() {
|
||||
}
|
||||
|
||||
.modal-insert {
|
||||
padding: var(--gap-lg);
|
||||
|
||||
.label {
|
||||
margin-block: var(--gap-lg) var(--gap-sm);
|
||||
display: block;
|
||||
|
||||
&:first-child {
|
||||
margin-top: 0;
|
||||
}
|
||||
}
|
||||
|
||||
.label__title {
|
||||
|
||||
@@ -2,8 +2,8 @@
|
||||
<nav
|
||||
v-if="filteredLinks.length > 1"
|
||||
ref="scrollContainer"
|
||||
class="relative flex w-fit overflow-x-auto rounded-full bg-bg-raised p-1 text-sm font-bold drop-shadow-xl"
|
||||
:class="{ 'shadow-sm': mode === 'navigation' }"
|
||||
class="relative flex w-fit overflow-x-auto rounded-full bg-bg-raised p-1 text-sm font-bold"
|
||||
:class="{ 'drop-shadow-xl': mode === 'navigation' }"
|
||||
>
|
||||
<template v-if="mode === 'navigation'">
|
||||
<RouterLink
|
||||
|
||||
@@ -25,6 +25,7 @@
|
||||
aria-modal="true"
|
||||
:aria-labelledby="headerId"
|
||||
class="modal-body flex flex-col bg-bg-raised rounded-2xl border border-solid border-surface-5"
|
||||
v-bind="$attrs"
|
||||
@keydown="handleKeyDown"
|
||||
>
|
||||
<div
|
||||
@@ -345,6 +346,10 @@ function handleKeyDown(event: KeyboardEvent) {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
defineOptions({
|
||||
inheritAttrs: false,
|
||||
})
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
|
||||
@@ -1,6 +1,10 @@
|
||||
<template>
|
||||
<div
|
||||
:class="['banner-grid relative border-b-2 border-solid border-0', containerClasses[variant]]"
|
||||
:class="[
|
||||
'banner-grid relative border-b-2 border-solid border-0 z-10',
|
||||
containerClasses[variant],
|
||||
{ 'no-actions': !$slots.actions, slim: slim },
|
||||
]"
|
||||
>
|
||||
<div
|
||||
:class="[
|
||||
@@ -16,12 +20,20 @@
|
||||
<slot name="description" />
|
||||
</div>
|
||||
|
||||
<div v-if="$slots.actions" class="grid-area-[actions]">
|
||||
<div v-if="$slots.actions" class="grid-area-[actions] flex items-center gap-2">
|
||||
<slot name="actions" />
|
||||
</div>
|
||||
|
||||
<div v-if="$slots.actions_right" class="grid-area-[actions_right]">
|
||||
<slot name="actions_right" />
|
||||
<div
|
||||
v-if="$slots.actions_right || $slots.actions_top_right"
|
||||
class="grid-area-[actions_right] flex flex-col gap-2 items-end"
|
||||
>
|
||||
<div v-if="$slots.actions_top_right" class="flex items-center gap-2 justify-end">
|
||||
<slot name="actions_top_right" />
|
||||
</div>
|
||||
<div v-if="$slots.actions_right" class="flex items-center gap-2 justify-end my-auto">
|
||||
<slot name="actions_right" />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
@@ -29,9 +41,15 @@
|
||||
<script lang="ts" setup>
|
||||
import { getSeverityIcon } from '../../utils'
|
||||
|
||||
defineProps<{
|
||||
variant: 'error' | 'warning' | 'info'
|
||||
}>()
|
||||
withDefaults(
|
||||
defineProps<{
|
||||
variant: 'error' | 'warning' | 'info'
|
||||
slim?: boolean
|
||||
}>(),
|
||||
{
|
||||
slim: false,
|
||||
},
|
||||
)
|
||||
|
||||
const containerClasses = {
|
||||
error: 'bg-banners-error-bg text-banners-error-text border-banners-error-border',
|
||||
@@ -58,6 +76,16 @@ const iconClasses = {
|
||||
padding-inline: max(calc((100% - 80rem) / 2 + var(--gap-md)), var(--gap-xl));
|
||||
}
|
||||
|
||||
.banner-grid.no-actions {
|
||||
grid-template-areas:
|
||||
'title actions_right'
|
||||
'description actions_right';
|
||||
}
|
||||
|
||||
.banner-grid.slim {
|
||||
@apply flex py-4 gap-2 items-center;
|
||||
}
|
||||
|
||||
.grid-area-\[title\] {
|
||||
grid-area: title;
|
||||
}
|
||||
|
||||
@@ -2036,6 +2036,150 @@
|
||||
"locale.zh-TW": {
|
||||
"defaultMessage": "Chinese (Traditional)"
|
||||
},
|
||||
"markdown-editor.default-image-alt-text": {
|
||||
"defaultMessage": "Replace this with a description"
|
||||
},
|
||||
"markdown-editor.error-label": {
|
||||
"defaultMessage": "Error"
|
||||
},
|
||||
"markdown-editor.image-modal.description-field.description": {
|
||||
"defaultMessage": "Describe the image completely as you would to someone who could not see the image."
|
||||
},
|
||||
"markdown-editor.image-modal.description-field.placeholder": {
|
||||
"defaultMessage": "Describe the image..."
|
||||
},
|
||||
"markdown-editor.image-modal.description-field.title": {
|
||||
"defaultMessage": "Description (alt text)"
|
||||
},
|
||||
"markdown-editor.image-modal.header": {
|
||||
"defaultMessage": "Insert image"
|
||||
},
|
||||
"markdown-editor.image-modal.upload-mode.label": {
|
||||
"defaultMessage": "Image source"
|
||||
},
|
||||
"markdown-editor.image-modal.upload-mode.link": {
|
||||
"defaultMessage": "Link"
|
||||
},
|
||||
"markdown-editor.image-modal.upload-mode.upload": {
|
||||
"defaultMessage": "Upload"
|
||||
},
|
||||
"markdown-editor.image-modal.upload.prompt": {
|
||||
"defaultMessage": "Drag and drop to upload or click to select file"
|
||||
},
|
||||
"markdown-editor.image-modal.url-field.placeholder": {
|
||||
"defaultMessage": "Enter the image URL..."
|
||||
},
|
||||
"markdown-editor.insert-button": {
|
||||
"defaultMessage": "Insert"
|
||||
},
|
||||
"markdown-editor.link-modal.header": {
|
||||
"defaultMessage": "Insert link"
|
||||
},
|
||||
"markdown-editor.link-modal.label-field.placeholder": {
|
||||
"defaultMessage": "Enter label..."
|
||||
},
|
||||
"markdown-editor.link-modal.label-field.title": {
|
||||
"defaultMessage": "Label"
|
||||
},
|
||||
"markdown-editor.link-modal.url-field.placeholder": {
|
||||
"defaultMessage": "Enter the link's URL..."
|
||||
},
|
||||
"markdown-editor.markdown-formatting-support": {
|
||||
"defaultMessage": "This editor supports <markdown-link>Markdown formatting</markdown-link>."
|
||||
},
|
||||
"markdown-editor.max-length.label": {
|
||||
"defaultMessage": "Max length:"
|
||||
},
|
||||
"markdown-editor.max-length.unlimited": {
|
||||
"defaultMessage": "Unlimited"
|
||||
},
|
||||
"markdown-editor.max-length.value": {
|
||||
"defaultMessage": "{currentLength}/{maxLength}"
|
||||
},
|
||||
"markdown-editor.placeholder": {
|
||||
"defaultMessage": "Write something..."
|
||||
},
|
||||
"markdown-editor.preview-label": {
|
||||
"defaultMessage": "Preview"
|
||||
},
|
||||
"markdown-editor.preview-toggle.label": {
|
||||
"defaultMessage": "Preview"
|
||||
},
|
||||
"markdown-editor.toolbar.bold": {
|
||||
"defaultMessage": "Bold"
|
||||
},
|
||||
"markdown-editor.toolbar.bulleted-list": {
|
||||
"defaultMessage": "Bulleted list"
|
||||
},
|
||||
"markdown-editor.toolbar.code": {
|
||||
"defaultMessage": "Code"
|
||||
},
|
||||
"markdown-editor.toolbar.heading-1": {
|
||||
"defaultMessage": "Heading 1"
|
||||
},
|
||||
"markdown-editor.toolbar.heading-2": {
|
||||
"defaultMessage": "Heading 2"
|
||||
},
|
||||
"markdown-editor.toolbar.heading-3": {
|
||||
"defaultMessage": "Heading 3"
|
||||
},
|
||||
"markdown-editor.toolbar.image": {
|
||||
"defaultMessage": "Image"
|
||||
},
|
||||
"markdown-editor.toolbar.italic": {
|
||||
"defaultMessage": "Italic"
|
||||
},
|
||||
"markdown-editor.toolbar.link": {
|
||||
"defaultMessage": "Link"
|
||||
},
|
||||
"markdown-editor.toolbar.ordered-list": {
|
||||
"defaultMessage": "Ordered list"
|
||||
},
|
||||
"markdown-editor.toolbar.quote": {
|
||||
"defaultMessage": "Quote"
|
||||
},
|
||||
"markdown-editor.toolbar.spoiler": {
|
||||
"defaultMessage": "Spoiler"
|
||||
},
|
||||
"markdown-editor.toolbar.strikethrough": {
|
||||
"defaultMessage": "Strikethrough"
|
||||
},
|
||||
"markdown-editor.toolbar.video": {
|
||||
"defaultMessage": "Video"
|
||||
},
|
||||
"markdown-editor.upload-error.no-file": {
|
||||
"defaultMessage": "No file provided"
|
||||
},
|
||||
"markdown-editor.upload-error.no-handler": {
|
||||
"defaultMessage": "No image upload handler provided"
|
||||
},
|
||||
"markdown-editor.url-label": {
|
||||
"defaultMessage": "URL"
|
||||
},
|
||||
"markdown-editor.url-validation-error.blocked-domain": {
|
||||
"defaultMessage": "Invalid URL. This domain is not allowed."
|
||||
},
|
||||
"markdown-editor.url-validation-error.malformed": {
|
||||
"defaultMessage": "Invalid URL. Make sure the URL is well-formed."
|
||||
},
|
||||
"markdown-editor.url-validation-error.unsupported-protocol": {
|
||||
"defaultMessage": "Unsupported protocol. Use http or https."
|
||||
},
|
||||
"markdown-editor.video-embed.title": {
|
||||
"defaultMessage": "YouTube video player"
|
||||
},
|
||||
"markdown-editor.video-modal.header": {
|
||||
"defaultMessage": "Insert YouTube video"
|
||||
},
|
||||
"markdown-editor.video-modal.url-field.description": {
|
||||
"defaultMessage": "Enter a valid link to a YouTube video."
|
||||
},
|
||||
"markdown-editor.video-modal.url-field.placeholder": {
|
||||
"defaultMessage": "Enter YouTube video URL"
|
||||
},
|
||||
"markdown-editor.video-modal.url-field.title": {
|
||||
"defaultMessage": "YouTube video URL"
|
||||
},
|
||||
"modal.add-payment-method.action": {
|
||||
"defaultMessage": "Add payment method"
|
||||
},
|
||||
|
||||
@@ -25,6 +25,8 @@ import {
|
||||
PayPalIcon,
|
||||
PlugIcon,
|
||||
PolygonIcon,
|
||||
ScaleIcon,
|
||||
ServerIcon,
|
||||
UnknownIcon,
|
||||
UpdatedIcon,
|
||||
USDCColorIcon,
|
||||
@@ -49,6 +51,7 @@ export const PROJECT_TYPE_ICONS: Record<ProjectType, Component> = {
|
||||
plugin: PlugIcon,
|
||||
datapack: BracesIcon,
|
||||
project: BoxIcon,
|
||||
minecraft_java_server: ServerIcon,
|
||||
}
|
||||
|
||||
export const PAYMENT_METHOD_ICONS: Record<string, Component> = {
|
||||
@@ -68,6 +71,7 @@ export const SEVERITY_ICONS: Record<string, Component> = {
|
||||
error: XCircleIcon,
|
||||
critical: XCircleIcon,
|
||||
success: CheckCircleIcon,
|
||||
moderation: ScaleIcon,
|
||||
}
|
||||
|
||||
export const PROJECT_STATUS_ICONS: Record<ProjectStatus, Component> = {
|
||||
|
||||
Reference in New Issue
Block a user