mirror of
https://github.com/modrinth/code.git
synced 2026-08-28 02:24:56 +00:00
project disclosures frontend (#6955)
* begin project disclosures * new project settings header * begin disclosure settings, edit content rules * togglecards * update phrasing of the rules, prepr * more disclosure settings structuring * add functionality toggle * implement functionality with staging api * improve project settings head titles * feat(labrinth): project disclosures model * feat(labrinth): project disclosures database model * feat(labrinth): project disclosures get endpoint * feat(labrinth): censor user ids if set by moderator * feat(labrinth): wrap disclosures in struct * feat(labrinth): edit project disclosures endpoint * style(labrinth): cargo fmt * style(labrinth): fix typo * feat(labrinth): add fields for ai content disclosure * fix(labrinth): field typo * chore(labrinth): update query cache * feat(labrinth): index disclosures in search * refactor(labrinth): use enum instead of bools * feat(labrinth): trigger incremental index on disclosure change * feat(labrinth): change archived status to disclosure * feat(labrinth): use disclosure for archival status * fix(labrinth): error type for deserialization * fix(labrinth): migration timestamp * fix(labrinth): add disclosures to elasticsearch schema * disclosures in search * update photosensitivity warning copy * update archiving, move general project settings to v3 (start redesign there), use project id lookups for stable cache keys, advanced in server search * blue archive banner * display AI use types * improve labels * add modrinth to link * add placeholder missing disclosure report type * add AI metadata checking to block image uploads with notice * update copy * update section 4 of rules * Update rule 6 layout * fix(labrinth): don't remove archival disclosure when changing status via v3 api * feat(labrinth): derive str for ai usages and telemtry consent * feat(labrinth): include ai usages and telemetry consent in disclosure types * Update moderation checklist (#7056) * fix: missing delphi severity (#7054) * Utils nav, new disclosures stage, ai button in rules * add prefix to collect * Finish up new disclosures stage * update r4 messages * new r4 msg, update showcase clarity message * fix: version upload failing to detect mrpack loader (#7063) * fix: mrpack exporting with zip64 (#7064) * fix: action bar max width (#7048) * fix: action bar max width * fix: width * changelog * Rule placeholders with subsections & anchors, update messages. * update new messages to account for new rule and placeholder layout * Add nag for content disclosures --------- Co-authored-by: ThatGravyBoat <gravy@thatgravyboat.tech> Co-authored-by: chyzman <chyzalt@gmail.com> Co-authored-by: Truman Gao <106889354+tdgao@users.noreply.github.com> Co-authored-by: Prospector <6166773+Prospector@users.noreply.github.com> * prepr * refactor(labrinth): move disclosure parsing to document creation * fix(labrinth): prevent removing moderator added archival disclosures * fix(labrinth): dedupe ai usages * fix(labrinth): auth logic on archived disclosure removal * feat(labrinth): add interactions field * style(labrinth): cargo fmt * feat(labrinth): granular disclosure locking * add 5.8, move 5.9, update messages and placeholders accordingly. * move disclosures stage earlier * include consent model info in telemetry disclosure msgs * prepr + update date * typo * qa pass * search suboptions * add empty state * checkboxes in column * persistent advanced filters * blog draft * prepr * support soft deletion * reload instead of optimistically updating * update blog * TOS update + minor verbiage adjustment * 45 day grace --------- Co-authored-by: sychic <47618543+Sychic@users.noreply.github.com> Co-authored-by: coolbot <76798835+coolbot100s@users.noreply.github.com> Co-authored-by: ThatGravyBoat <gravy@thatgravyboat.tech> Co-authored-by: chyzman <chyzalt@gmail.com> Co-authored-by: Truman Gao <106889354+tdgao@users.noreply.github.com>
This commit is contained in:
co-authored by
ThatGravyBoat
chyzman
Truman Gao
sychic
coolbot
parent
06b7c44dcc
commit
755825b09a
@@ -1,7 +1,8 @@
|
||||
<template>
|
||||
<div
|
||||
:class="[
|
||||
'relative grid grid-cols-[1.5rem_minmax(0,1fr)_auto] items-start gap-x-2 rounded-2xl border border-solid p-4 text-contrast',
|
||||
'relative grid grid-cols-[1.5rem_minmax(0,1fr)_auto] gap-x-2 rounded-2xl border border-solid p-4 text-contrast',
|
||||
centerContent ? 'items-center' : 'items-start',
|
||||
progress != null ? 'overflow-hidden pb-5' : '',
|
||||
typeClasses[type],
|
||||
]"
|
||||
@@ -13,7 +14,7 @@
|
||||
class="col-start-2 min-w-0"
|
||||
:class="
|
||||
inlineActions && !showActionsUnderneath && $slots.actions
|
||||
? 'flex flex-wrap items-start gap-x-4 gap-y-3'
|
||||
? ['flex flex-wrap gap-x-4 gap-y-3', centerContent ? 'items-center' : 'items-start']
|
||||
: 'flex flex-1 flex-col gap-2'
|
||||
"
|
||||
>
|
||||
@@ -39,7 +40,7 @@
|
||||
{{ relativeTimeLabel }}
|
||||
</span>
|
||||
</div>
|
||||
<div class="font-normal text-contrast/85 leading-tight">
|
||||
<div v-if="!!$slots.default || body" class="font-normal text-contrast/85 leading-tight">
|
||||
<slot>{{ body }}</slot>
|
||||
</div>
|
||||
</div>
|
||||
@@ -119,6 +120,7 @@ const props = withDefaults(
|
||||
waiting?: boolean
|
||||
/** Accepts a Date, an ISO string, or a millisecond Unix timestamp. */
|
||||
timestamp?: Date | string | number
|
||||
centerContent?: boolean
|
||||
}>(),
|
||||
{
|
||||
type: 'info',
|
||||
@@ -131,6 +133,7 @@ const props = withDefaults(
|
||||
progressColor: undefined,
|
||||
waiting: false,
|
||||
timestamp: undefined,
|
||||
centerContent: false,
|
||||
},
|
||||
)
|
||||
|
||||
|
||||
@@ -1,11 +1,20 @@
|
||||
<script setup lang="ts">
|
||||
import IntlMessageFormat, { type FormatXMLElementFn, type PrimitiveType } from 'intl-messageformat'
|
||||
import { computed, markRaw, useSlots, type VNode } from 'vue'
|
||||
import { computed, defineComponent, markRaw, type PropType, useSlots, type VNode } from 'vue'
|
||||
|
||||
import type { MessageDescriptor } from '../../composables/i18n'
|
||||
import { injectI18nDebug } from '../../composables/i18n-debug'
|
||||
import { injectI18n } from '../../providers/i18n'
|
||||
|
||||
const VNodeRenderer = defineComponent({
|
||||
props: {
|
||||
node: { type: Object as PropType<VNode>, required: true },
|
||||
},
|
||||
setup(props) {
|
||||
return () => props.node
|
||||
},
|
||||
})
|
||||
|
||||
const props = defineProps<{
|
||||
messageId: MessageDescriptor
|
||||
values?: Record<string, PrimitiveType>
|
||||
@@ -49,11 +58,13 @@ const formattedParts = computed(() => {
|
||||
slotHandlers[normalizedName] = (chunks) => {
|
||||
const slot = slots[slotName]
|
||||
if (slot) {
|
||||
return markRaw(
|
||||
slot({
|
||||
children: chunks,
|
||||
}),
|
||||
) as VNode[]
|
||||
const nodes = slot({
|
||||
children: chunks,
|
||||
})
|
||||
if (Array.isArray(nodes) && nodes.length === 1) {
|
||||
return markRaw(nodes[0]) as VNode
|
||||
}
|
||||
return markRaw(nodes) as VNode[]
|
||||
}
|
||||
return markRaw(chunks) as VNode[]
|
||||
}
|
||||
@@ -71,33 +82,35 @@ const formattedParts = computed(() => {
|
||||
...slotHandlers,
|
||||
})
|
||||
|
||||
// ensure result array items are marked as raw if they're VNodes
|
||||
// prevents VNodes from entering the reactive system and SSR payload
|
||||
if (Array.isArray(result)) {
|
||||
return result.map((part) =>
|
||||
typeof part === 'object' && part !== null ? markRaw(part) : part,
|
||||
)
|
||||
}
|
||||
return [typeof result === 'object' && result !== null ? markRaw(result) : result]
|
||||
return toFormattedParts(result)
|
||||
} catch {
|
||||
return [msg]
|
||||
}
|
||||
})
|
||||
|
||||
function toFormattedParts(value: unknown): unknown[] {
|
||||
if (Array.isArray(value)) {
|
||||
return value.flatMap(toFormattedParts)
|
||||
}
|
||||
if (typeof value === 'object' && value !== null) {
|
||||
return [markRaw(value)]
|
||||
}
|
||||
return [value]
|
||||
}
|
||||
|
||||
function isVNodePart(part: unknown): part is VNode {
|
||||
return typeof part === 'object' && part !== null
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<span
|
||||
v-if="debugEnabled && !debugKeyReveal"
|
||||
:data-i18n-key="messageId.id"
|
||||
style="display: contents"
|
||||
:data-i18n-key="debugEnabled && !debugKeyReveal ? messageId.id : undefined"
|
||||
>
|
||||
<template v-for="(part, index) in formattedParts" :key="index">
|
||||
<component :is="() => part" v-if="typeof part === 'object'" />
|
||||
<VNodeRenderer v-if="isVNodePart(part)" :node="part" />
|
||||
<template v-else>{{ part }}</template>
|
||||
</template>
|
||||
</span>
|
||||
<template v-for="(part, index) in formattedParts" v-else :key="index">
|
||||
<component :is="() => part" v-if="typeof part === 'object'" />
|
||||
<template v-else>{{ part }}</template>
|
||||
</template>
|
||||
</template>
|
||||
|
||||
@@ -0,0 +1,53 @@
|
||||
<script setup lang="ts">
|
||||
import { defineMessage, useVIntl } from '../../composables/i18n'
|
||||
const { formatMessage } = useVIntl()
|
||||
|
||||
withDefaults(
|
||||
defineProps<{
|
||||
title?: string
|
||||
titleFor?: string
|
||||
description?: string
|
||||
optional?: boolean
|
||||
}>(),
|
||||
{
|
||||
optional: false,
|
||||
},
|
||||
)
|
||||
|
||||
const optionalMessage = defineMessage({
|
||||
id: 'settings.form.group.optional',
|
||||
defaultMessage: '(optional)',
|
||||
})
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="flex flex-col gap-2">
|
||||
<div
|
||||
v-if="title || description || $slots.title || $slots.description"
|
||||
:class="description || $slots.description ? 'flex flex-col gap-1' : undefined"
|
||||
>
|
||||
<label
|
||||
v-if="title && titleFor"
|
||||
:for="titleFor"
|
||||
class="m-0 leading-normal text-contrast font-semibold"
|
||||
>
|
||||
{{ title }}
|
||||
<span v-if="optional" class="text-secondary font-normal">
|
||||
{{ formatMessage(optionalMessage) }}
|
||||
</span>
|
||||
</label>
|
||||
<p v-else-if="title" class="m-0 leading-normal text-contrast font-semibold">
|
||||
{{ title }}
|
||||
<span v-if="optional" class="text-secondary font-normal">
|
||||
{{ formatMessage(optionalMessage) }}
|
||||
</span>
|
||||
</p>
|
||||
<slot name="title" />
|
||||
<p v-if="description" class="m-0 leading-normal text-primary">
|
||||
{{ description }}
|
||||
</p>
|
||||
<slot name="description" />
|
||||
</div>
|
||||
<slot />
|
||||
</div>
|
||||
</template>
|
||||
@@ -0,0 +1,12 @@
|
||||
<script setup lang="ts">
|
||||
import { TriangleAlertIcon } from '@modrinth/assets'
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="flex flex-wrap items-center gap-1.5 text-orange">
|
||||
<TriangleAlertIcon aria-hidden="true" class="size-4 shrink-0" />
|
||||
<div class="min-w-0">
|
||||
<slot />
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
@@ -0,0 +1,32 @@
|
||||
<script setup lang="ts">
|
||||
import type { Component } from 'vue'
|
||||
|
||||
defineProps<{
|
||||
title: string
|
||||
icon?: Component
|
||||
description?: string
|
||||
}>()
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div
|
||||
class="grid w-full grid-cols-[1fr_auto] items-center gap-6 p-4 rounded-2xl border border-solid border-surface-4 bg-surface-3"
|
||||
>
|
||||
<div>
|
||||
<h3 class="mb-1 mt-0 flex items-center gap-2 text-lg font-semibold text-contrast">
|
||||
<component :is="icon" v-if="icon" class="size-5 shrink-0 text-primary" aria-hidden="true" />
|
||||
{{ title }}
|
||||
</h3>
|
||||
<div
|
||||
v-if="description || $slots.default"
|
||||
class="flex flex-col gap-2 leading-normal [&>p]:m-0"
|
||||
>
|
||||
<p v-if="description">{{ description }}</p>
|
||||
<slot />
|
||||
</div>
|
||||
</div>
|
||||
<div>
|
||||
<slot name="actions" />
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
@@ -0,0 +1,36 @@
|
||||
<script setup lang="ts">
|
||||
import type { Component } from 'vue'
|
||||
|
||||
import ToggleCard from './ToggleCard.vue'
|
||||
|
||||
defineProps<{
|
||||
title: string
|
||||
icon?: Component
|
||||
description?: string
|
||||
disabled?: boolean
|
||||
toggleDisabled?: boolean
|
||||
}>()
|
||||
|
||||
const enabled = defineModel<boolean>({ required: true })
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<ToggleCard v-model="enabled" :disabled="disabled" :toggle-disabled="toggleDisabled">
|
||||
<h3 class="mb-1 mt-0 flex items-center gap-2 text-lg font-semibold text-contrast">
|
||||
<component :is="icon" v-if="icon" class="size-5 shrink-0 text-primary" aria-hidden="true" />
|
||||
{{ title }}
|
||||
</h3>
|
||||
<div v-if="description || $slots.default" class="flex flex-col gap-2 leading-normal [&>p]:m-0">
|
||||
<p v-if="description">{{ description }}</p>
|
||||
<slot />
|
||||
</div>
|
||||
<template v-if="$slots.expanded" #expanded>
|
||||
<div class="flex flex-col gap-4">
|
||||
<slot name="expanded" />
|
||||
</div>
|
||||
</template>
|
||||
<template v-if="$slots.footer" #footer>
|
||||
<slot name="footer" />
|
||||
</template>
|
||||
</ToggleCard>
|
||||
</template>
|
||||
@@ -5,7 +5,8 @@
|
||||
v-bind="$attrs"
|
||||
class="smart-clickable__contents"
|
||||
:class="{
|
||||
'pointer-events-none': !!$slots.clickable,
|
||||
'smart-clickable__contents--disabled': disabled,
|
||||
'pointer-events-none': !!$slots.clickable && !disabled,
|
||||
}"
|
||||
>
|
||||
<slot />
|
||||
@@ -14,6 +15,10 @@
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
defineProps<{
|
||||
disabled?: boolean
|
||||
}>()
|
||||
|
||||
defineOptions({
|
||||
inheritAttrs: false,
|
||||
})
|
||||
@@ -49,8 +54,7 @@ defineOptions({
|
||||
// When clickable is being hovered or focus-visible, give contents an effect
|
||||
:first-child:hover + .smart-clickable__contents,
|
||||
:first-child:focus-visible + .smart-clickable__contents,
|
||||
.smart-clickable__contents:hover,
|
||||
.smart-clickable__contents:focus-within {
|
||||
.smart-clickable__contents:hover {
|
||||
// Utility classes for contents
|
||||
:deep(.smart-clickable\:underline-on-hover) {
|
||||
text-decoration: underline;
|
||||
@@ -78,8 +82,8 @@ defineOptions({
|
||||
}
|
||||
|
||||
// When clickable is being clicked, give contents an effect
|
||||
:first-child:active + .smart-clickable__contents {
|
||||
scale: 0.97;
|
||||
:first-child:active + .smart-clickable__contents:not(.smart-clickable__contents--disabled) {
|
||||
scale: 0.98;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
|
||||
@@ -0,0 +1,85 @@
|
||||
<script setup lang="ts">
|
||||
import { SmartClickable, Toggle } from '@modrinth/ui'
|
||||
import { computed, useId } from 'vue'
|
||||
|
||||
const props = defineProps<{
|
||||
disabled?: boolean
|
||||
toggleDisabled?: boolean
|
||||
}>()
|
||||
|
||||
const value = defineModel<boolean>({ required: true })
|
||||
|
||||
const baseId = useId()
|
||||
const toggleId = computed(() => `toggle-card-toggle-${baseId}`)
|
||||
const isToggleLocked = computed(() => !!props.disabled || (!!props.toggleDisabled && value.value))
|
||||
|
||||
function toggle() {
|
||||
if (isToggleLocked.value) return
|
||||
value.value = !value.value
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<SmartClickable
|
||||
class="flex w-full flex-col overflow-clip rounded-2xl border border-solid border-surface-4 bg-surface-3"
|
||||
:disabled="isToggleLocked"
|
||||
>
|
||||
<template #clickable>
|
||||
<button
|
||||
aria-hidden="true"
|
||||
tabindex="-1"
|
||||
:disabled="isToggleLocked"
|
||||
class="flex h-full w-full"
|
||||
:class="isToggleLocked ? 'cursor-not-allowed' : 'cursor-pointer'"
|
||||
@click="toggle"
|
||||
/>
|
||||
</template>
|
||||
<div class="grid w-full grid-cols-[1fr_auto] items-center gap-6 p-4">
|
||||
<div>
|
||||
<slot :toggle-id="toggleId" />
|
||||
</div>
|
||||
<div>
|
||||
<slot name="toggle" :disabled="isToggleLocked">
|
||||
<Toggle
|
||||
:id="toggleId"
|
||||
v-model="value"
|
||||
:disabled="isToggleLocked"
|
||||
class="smart-clickable:allow-pointer-events"
|
||||
/>
|
||||
</slot>
|
||||
</div>
|
||||
</div>
|
||||
<Transition name="toggle-card-content">
|
||||
<div v-if="value && $slots.expanded" class="smart-clickable:allow-pointer-events">
|
||||
<div>
|
||||
<div class="border-0 border-t border-solid border-surface-4 bg-surface-2 p-4">
|
||||
<slot name="expanded" />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</Transition>
|
||||
<div
|
||||
v-if="$slots.footer"
|
||||
class="smart-clickable:allow-pointer-events border-0 border-t bg-surface-3 border-solid border-surface-4 px-4 py-3"
|
||||
>
|
||||
<slot name="footer" />
|
||||
</div>
|
||||
</SmartClickable>
|
||||
</template>
|
||||
<style scoped>
|
||||
.toggle-card-content-enter-active,
|
||||
.toggle-card-content-leave-active {
|
||||
display: grid;
|
||||
grid-template-rows: 1fr;
|
||||
transition: grid-template-rows 0.25s var(--ease-out-expo);
|
||||
|
||||
& > div {
|
||||
grid-row: 1 / span 2;
|
||||
}
|
||||
}
|
||||
|
||||
.toggle-card-content-enter-from,
|
||||
.toggle-card-content-leave-to {
|
||||
grid-template-rows: 0fr;
|
||||
}
|
||||
</style>
|
||||
@@ -48,8 +48,10 @@ const props = withDefaults(
|
||||
},
|
||||
)
|
||||
|
||||
const shown = computed(() =>
|
||||
Object.keys(props.modified).some((key) => !isEqual(props.original[key], props.modified[key])),
|
||||
const shown = computed(
|
||||
() =>
|
||||
props.saving ||
|
||||
Object.keys(props.modified).some((key) => !isEqual(props.original[key], props.modified[key])),
|
||||
)
|
||||
|
||||
function localizeIfPossible(message: MessageDescriptor | string) {
|
||||
|
||||
@@ -45,6 +45,8 @@ const typeClasses: Record<ButtonType, string> = {
|
||||
base: 'button-frame--base bg-surface-4 text-contrast [&>svg]:text-primary',
|
||||
colored:
|
||||
'button-frame--colored bg-[--button-color] text-[var(--color-accent-contrast)] [&>svg]:text-inherit',
|
||||
'colored-text':
|
||||
'button-frame--colored-text bg-surface-4 text-[--button-color] [&>svg]:text-inherit',
|
||||
outlined:
|
||||
'button-frame--outlined bg-transparent text-[var(--button-color,var(--color-contrast))] [&>svg]:text-[var(--button-color,var(--color-base))]',
|
||||
quiet: 'button-frame--quiet bg-transparent [&>svg]:text-inherit',
|
||||
@@ -100,7 +102,12 @@ const classes = computed(() => [
|
||||
])
|
||||
const style = computed((): CSSProperties | undefined => {
|
||||
if ((props.type === 'outlined' || props.type === 'quiet') && !props.color) return undefined
|
||||
if (props.type !== 'colored' && props.type !== 'outlined' && props.type !== 'quiet')
|
||||
if (
|
||||
props.type !== 'colored' &&
|
||||
props.type !== 'colored-text' &&
|
||||
props.type !== 'outlined' &&
|
||||
props.type !== 'quiet'
|
||||
)
|
||||
return undefined
|
||||
|
||||
return {
|
||||
@@ -125,7 +132,8 @@ defineExpose({ element })
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
.button-frame--base {
|
||||
.button-frame--base,
|
||||
.button-frame--colored-text {
|
||||
box-shadow:
|
||||
inset 0 0 0 1px var(--surface-5),
|
||||
0 1px 1px rgba(0, 0, 0, 0.12);
|
||||
|
||||
@@ -3,7 +3,7 @@ import type { RouteLocationRaw } from 'vue-router'
|
||||
|
||||
import type { AnchoredTeleportPlacement } from '../../../utils/use-anchored-teleport'
|
||||
|
||||
export type ButtonType = 'base' | 'colored' | 'outlined' | 'quiet'
|
||||
export type ButtonType = 'base' | 'colored' | 'colored-text' | 'outlined' | 'quiet'
|
||||
|
||||
export type ButtonSize = 'xs' | 'sm' | 'md' | 'lg' | 'xl'
|
||||
|
||||
@@ -35,6 +35,10 @@ export type ButtonVisualProps = {
|
||||
type: 'colored'
|
||||
color?: ButtonColor
|
||||
}
|
||||
| {
|
||||
type: 'colored-text'
|
||||
color?: ButtonColor
|
||||
}
|
||||
| {
|
||||
type: 'quiet'
|
||||
color?: ButtonColor
|
||||
|
||||
@@ -107,7 +107,11 @@ export { default as RadioButtons } from './RadioButtons.vue'
|
||||
export { default as ReadyTransition } from './ReadyTransition.vue'
|
||||
export { default as ScrollablePanel } from './ScrollablePanel.vue'
|
||||
export { default as ServerNotice } from './ServerNotice.vue'
|
||||
export { default as SettingsFormGroup } from './SettingsFormGroup.vue'
|
||||
export { default as SettingsInlineWarning } from './SettingsInlineWarning.vue'
|
||||
export { default as SettingsLabel } from './SettingsLabel.vue'
|
||||
export { default as SettingsOptionCard } from './SettingsOptionCard.vue'
|
||||
export { default as SettingsToggleCard } from './SettingsToggleCard.vue'
|
||||
export { default as SimpleBadge } from './SimpleBadge.vue'
|
||||
export { default as Slider } from './Slider.vue'
|
||||
export { default as SmartClickable } from './SmartClickable.vue'
|
||||
@@ -132,4 +136,5 @@ export type {
|
||||
export { default as TimeFramePicker } from './TimeFramePicker.vue'
|
||||
export { default as Timeline } from './Timeline.vue'
|
||||
export { default as Toggle } from './Toggle.vue'
|
||||
export { default as ToggleCard } from './ToggleCard.vue'
|
||||
export { default as UnsavedChangesPopup } from './UnsavedChangesPopup.vue'
|
||||
|
||||
Reference in New Issue
Block a user