diff --git a/packages/ui/src/components/base/BasicMarkdownText.vue b/packages/ui/src/components/base/BasicMarkdownText.vue
new file mode 100644
index 0000000000..c6c2d25771
--- /dev/null
+++ b/packages/ui/src/components/base/BasicMarkdownText.vue
@@ -0,0 +1,47 @@
+
+
+
+
+
+
+
diff --git a/packages/ui/src/components/base/index.ts b/packages/ui/src/components/base/index.ts
index 2127b5a3a3..3a82053838 100644
--- a/packages/ui/src/components/base/index.ts
+++ b/packages/ui/src/components/base/index.ts
@@ -7,6 +7,7 @@ export { default as AutoLink } from './AutoLink.vue'
export { default as Avatar } from './Avatar.vue'
export { default as Badge } from './Badge.vue'
export { default as BaseTerminal } from './BaseTerminal.vue'
+export { default as BasicMarkdownText } from './BasicMarkdownText.vue'
export { default as BigOptionButton } from './BigOptionButton.vue'
export { default as BulletDivider } from './BulletDivider.vue'
export { default as Button } from './buttons/Button.vue'
diff --git a/packages/ui/src/components/project/ArchivedProjectBanner.vue b/packages/ui/src/components/project/ArchivedProjectBanner.vue
index 876414472a..0cc6b6921e 100644
--- a/packages/ui/src/components/project/ArchivedProjectBanner.vue
+++ b/packages/ui/src/components/project/ArchivedProjectBanner.vue
@@ -3,6 +3,7 @@ import { ArchiveIcon } from '@modrinth/assets'
import { computed } from 'vue'
import Admonition from '#ui/components/base/Admonition.vue'
+import BasicMarkdownText from '#ui/components/base/BasicMarkdownText.vue'
import { defineMessages, useVIntl } from '#ui/composables/i18n'
const props = defineProps<{
@@ -32,6 +33,9 @@ const messages = defineMessages({
- {{ trimmedReason ?? formatMessage(messages.body, { title }) }}
+
+
+
+ {{ formatMessage(messages.body, { title }) }}
diff --git a/packages/ui/src/components/project/ProjectSidebarDetails.vue b/packages/ui/src/components/project/ProjectSidebarDetails.vue
index 814d608c81..8be4b125e9 100644
--- a/packages/ui/src/components/project/ProjectSidebarDetails.vue
+++ b/packages/ui/src/components/project/ProjectSidebarDetails.vue
@@ -14,7 +14,7 @@
{{ formatMessage(commonMessages.detailsLabel) }}
@@ -23,7 +23,7 @@
{{ capitalizeString(formatMessage(messages.photosensitivityTitle)) }}
- {{ photosensitivityDisclosure.note }}
+
@@ -34,7 +34,7 @@
{{ capitalizeString(aiGeneratedLabel) }}
- {{ aiDisclosure.note }}
+
@@ -45,7 +45,7 @@
{{ capitalizeString(formatMessage(messages.advertisingTitle)) }}
- {{ advertisingDisclosure.note }}
+
@@ -60,7 +60,7 @@
:key="`${feature}-${index}`"
class="text-sm text-secondary"
>
- {{ feature }}
+
@@ -81,7 +81,7 @@
:key="`${entry}-${index}`"
class="text-sm text-secondary"
>
- {{ entry }}
+
@@ -92,7 +92,7 @@
{{ capitalizeString(formatMessage(messages.systemInteractionsTitle)) }}
- {{ systemInteractionsDisclosure.note }}
+
@@ -130,7 +130,7 @@
{{ capitalizeString(formatMessage(messages.derivativeWork)) }}
@@ -139,14 +139,36 @@
:href="source.link"
:target="linkTarget"
rel="noopener nofollow ugc"
- class="text-blue text-sm flex items-center gap-1 hover:underline"
+ class="text-blue text-sm min-w-0 break-words hover:underline"
>
{{ source.label }}
- {{ source.label }}
- {{ source.note }}
+
+
+
+
+
+
+
@@ -207,6 +229,7 @@ import {
CalendarIcon,
CircleDollarSignIcon,
CircuitBoardIcon,
+ DropdownIcon,
ExternalIcon,
EyeIcon,
GitForkIcon,
@@ -226,7 +249,7 @@ import { defineMessage, defineMessages, useVIntl } from '../../composables/i18n'
import { injectModrinthClient } from '../../providers'
import { commonMessages } from '../../utils/common-messages'
import { getActiveDisclosures } from '../../utils/disclosures'
-import { Avatar, IntlFormatted } from '../base'
+import { Avatar, BasicMarkdownText, IntlFormatted } from '../base'
import { NewModal } from '../modal'
const LICENSE_STALE_TIME = 1000 * 60 * 10
@@ -285,6 +308,14 @@ const messages = defineMessages({
id: 'project.disclosure.derivative-work.title',
defaultMessage: 'This is a derivative work of:',
},
+ showMoreDerivativeSources: {
+ id: 'project.disclosure.derivative-work.show-more',
+ defaultMessage: 'Show {count} more',
+ },
+ showLessDerivativeSources: {
+ id: 'project.disclosure.derivative-work.show-fewer',
+ defaultMessage: 'Show fewer',
+ },
telemetryTitle: {
id: 'project.disclosure.telemetry.title',
defaultMessage:
@@ -322,6 +353,28 @@ const derivativeWorkDisclosure = computed(() => findDisclosure('derivative_work'
const photosensitivityDisclosure = computed(() => findDisclosure('epilepsy_triggers'))
const systemInteractionsDisclosure = computed(() => findDisclosure('system_interactions'))
+const DERIVATIVE_SOURCES_PREVIEW_LIMIT = 3
+const showAllDerivativeSources = ref(false)
+
+const visibleDerivativeSources = computed(() => {
+ const sources = derivativeWorkDisclosure.value?.sources ?? []
+ if (showAllDerivativeSources.value || sources.length <= DERIVATIVE_SOURCES_PREVIEW_LIMIT) {
+ return sources
+ }
+ return sources.slice(0, DERIVATIVE_SOURCES_PREVIEW_LIMIT)
+})
+
+const hasMoreDerivativeSources = computed(
+ () => (derivativeWorkDisclosure.value?.sources.length ?? 0) > DERIVATIVE_SOURCES_PREVIEW_LIMIT,
+)
+
+const hiddenDerivativeSourcesCount = computed(() =>
+ Math.max(
+ 0,
+ (derivativeWorkDisclosure.value?.sources.length ?? 0) - DERIVATIVE_SOURCES_PREVIEW_LIMIT,
+ ),
+)
+
const aiUseLabels = {
code: defineMessage({
id: 'project.disclosure.ai-generated-content.use.code',
diff --git a/packages/ui/src/locales/en-US/index.json b/packages/ui/src/locales/en-US/index.json
index e975188b4c..aaa994bd13 100644
--- a/packages/ui/src/locales/en-US/index.json
+++ b/packages/ui/src/locales/en-US/index.json
@@ -3386,6 +3386,12 @@
"project.disclosure.ai-generated-content.use.text": {
"defaultMessage": "text"
},
+ "project.disclosure.derivative-work.show-fewer": {
+ "defaultMessage": "Show fewer"
+ },
+ "project.disclosure.derivative-work.show-more": {
+ "defaultMessage": "Show {count} more"
+ },
"project.disclosure.derivative-work.title": {
"defaultMessage": "This is a derivative work of:"
},
diff --git a/packages/utils/parse.ts b/packages/utils/parse.ts
index e398d46b6a..848b9a2e6b 100644
--- a/packages/utils/parse.ts
+++ b/packages/utils/parse.ts
@@ -229,3 +229,54 @@ export const md = (options = {}) => {
}
export const renderString = (string: string) => configuredXss.process(md().render(string))
+
+const basicMarkdownXss = new FilterXSS({
+ whiteList: {
+ a: ['href', 'target', 'rel'],
+ strong: [],
+ em: [],
+ code: [],
+ br: [],
+ },
+ stripIgnoreTag: true,
+ stripIgnoreTagBody: ['script', 'style'],
+})
+
+export const renderBasicInlineMarkdown = (
+ string: string,
+ options: {
+ target?: string
+ } = {},
+) => {
+ const instance = new MarkdownIt({
+ html: false,
+ linkify: true,
+ breaks: true,
+ })
+
+ instance.disable(['image', 'strikethrough'])
+
+ instance.linkify.set({
+ fuzzyLink: false,
+ fuzzyIP: false,
+ })
+
+ const defaultLinkOpenRenderer =
+ instance.renderer.rules.link_open ||
+ function (tokens, idx, options, _env, self) {
+ return self.renderToken(tokens, idx, options)
+ }
+
+ instance.renderer.rules.link_open = function (tokens, idx, renderOptions, env, self) {
+ const token = tokens[idx]
+ token.attrSet('rel', 'noopener nofollow ugc')
+
+ if (options.target) {
+ token.attrSet('target', options.target)
+ }
+
+ return defaultLinkOpenRenderer(tokens, idx, renderOptions, env, self)
+ }
+
+ return basicMarkdownXss.process(instance.renderInline(string))
+}