feat: Allow for adjustable alignment of main content on certain pages for content moderators (#7034)

* Add type safety and validation for moderation settings

* Add ability for moderators to adjust page layout for discover, project, moderation pages

Primarily for changes to have more tooling present without overlapping important info
This commit is contained in:
Blodhgarm
2026-08-07 16:45:37 +00:00
committed by GitHub
parent 6ed044a54b
commit a71a361e0f
7 changed files with 129 additions and 27 deletions
@@ -111,6 +111,18 @@
max-width: 100%; max-width: 100%;
} }
} }
&.align-x {
margin: 0 auto;
}
&.align-l {
margin: 0 0 0 auto;
}
&.align-r {
margin: 0 auto 0 0;
}
} }
.normal-page__sidebar { .normal-page__sidebar {
@@ -140,6 +152,18 @@
'sidebar' 'sidebar'
/ 100%; / 100%;
&.align-x {
margin: 0 auto;
}
&.align-l {
margin: 0 0 0 auto;
}
&.align-r {
margin: 0 auto 0 0;
}
@media screen and (min-width: 1024px) { @media screen and (min-width: 1024px) {
&.sidebar { &.sidebar {
grid-template: grid-template:
+17 -13
View File
@@ -4,7 +4,7 @@
<ProjectBackgroundGradient :project="project" /> <ProjectBackgroundGradient :project="project" />
</Teleport> </Teleport>
<template v-if="isSettings"> <template v-if="isSettings">
<div v-if="canAccessSettings" class="normal-page no-sidebar"> <div v-if="canAccessSettings" class="normal-page no-sidebar" :class="`align-${marginTarget}`">
<div class="normal-page__header"> <div class="normal-page__header">
<div <div
class="mb-4 flex flex-wrap items-center gap-x-2 gap-y-3 border-0 border-b-[1px] border-solid border-divider pb-4 text-lg font-semibold" class="mb-4 flex flex-wrap items-center gap-x-2 gap-y-3 border-0 border-b-[1px] border-solid border-divider pb-4 text-lg font-semibold"
@@ -94,17 +94,20 @@
/> />
<div <div
class="new-page sidebar" class="new-page sidebar"
:class="{ :class="[
'alt-layout': cosmetics.leftContentLayout, {
'checklist-open': 'alt-layout': cosmetics.leftContentLayout,
showModerationChecklist && 'checklist-open':
!collapsedModerationChecklist && showModerationChecklist &&
!flags.alwaysShowChecklistAsPopup, !collapsedModerationChecklist &&
'checklist-collapsed': !flags.alwaysShowChecklistAsPopup,
showModerationChecklist && 'checklist-collapsed':
collapsedModerationChecklist && showModerationChecklist &&
!flags.alwaysShowChecklistAsPopup, collapsedModerationChecklist &&
}" !flags.alwaysShowChecklistAsPopup,
},
`align-${marginTarget}`,
]"
> >
<div <div
class="normal-page__header relative mb-4" class="normal-page__header relative mb-4"
@@ -511,7 +514,7 @@ import {
SettingsIcon, SettingsIcon,
XIcon, XIcon,
} from '@modrinth/assets' } from '@modrinth/assets'
import { moderationSettings } from '@modrinth/moderation' import { getMarginTarget, moderationSettings } from '@modrinth/moderation'
import { import {
Admonition, Admonition,
Avatar, Avatar,
@@ -583,6 +586,7 @@ const config = useRuntimeConfig()
const moderationQueue = useModerationQueue() const moderationQueue = useModerationQueue()
const keybinds = useModerationKeybinds() const keybinds = useModerationKeybinds()
const modSettings = useModerationSettings() const modSettings = useModerationSettings()
const marginTarget = computed(() => getMarginTarget(modSettings.value))
const notifications = injectNotificationManager() const notifications = injectNotificationManager()
const { addNotification } = notifications const { addNotification } = notifications
+7 -1
View File
@@ -1,12 +1,15 @@
<script setup lang="ts"> <script setup lang="ts">
import { getMarginTarget } from '@modrinth/moderation'
import { commonProjectTypeCategoryMessages, NavTabs, useVIntl } from '@modrinth/ui' import { commonProjectTypeCategoryMessages, NavTabs, useVIntl } from '@modrinth/ui'
const { formatMessage } = useVIntl() const { formatMessage } = useVIntl()
const flags = useFeatureFlags() const flags = useFeatureFlags()
const route = useRoute() const route = useRoute()
const modSettings = useModerationSettings()
const allowTabChanging = computed(() => !route.query.sid) const allowTabChanging = computed(() => !route.query.sid)
const marginTarget = computed(() => getMarginTarget(modSettings.value))
const selectableProjectTypes = [ const selectableProjectTypes = [
{ {
@@ -47,7 +50,10 @@ const selectableProjectTypes = [
] ]
</script> </script>
<template> <template>
<div class="mx-auto box-border flex w-full max-w-[1280px] flex-col gap-4 px-6 pb-6"> <div
class="box-border flex w-full max-w-[1280px] flex-col gap-4 px-6 pb-6"
:class="`m${marginTarget}-auto`"
>
<NavTabs <NavTabs
v-if="!flags.projectTypesPrimaryNav && allowTabChanging" v-if="!flags.projectTypesPrimaryNav && allowTabChanging"
:links="selectableProjectTypes" :links="selectableProjectTypes"
+8 -1
View File
@@ -1,5 +1,8 @@
<template> <template>
<div class="relative mx-auto mb-6 flex min-h-screen w-full max-w-[1280px] flex-col px-6"> <div
class="relativemb-6 flex min-h-screen w-full max-w-[1280px] flex-col px-6"
:class="`m${marginTarget}-auto`"
>
<h1>Moderation</h1> <h1>Moderation</h1>
<NavTabs :links="moderationLinks" class="mb-4 hidden sm:flex" /> <NavTabs :links="moderationLinks" class="mb-4 hidden sm:flex" />
<div class="mb-4 sm:hidden"> <div class="mb-4 sm:hidden">
@@ -16,6 +19,7 @@
<script setup lang="ts"> <script setup lang="ts">
import { FolderIcon, GlobeIcon, HashIcon, ReportIcon, ShieldCheckIcon } from '@modrinth/assets' import { FolderIcon, GlobeIcon, HashIcon, ReportIcon, ShieldCheckIcon } from '@modrinth/assets'
import { getMarginTarget } from '@modrinth/moderation'
import { Chips, defineMessages, NavTabs, useVIntl } from '@modrinth/ui' import { Chips, defineMessages, NavTabs, useVIntl } from '@modrinth/ui'
definePageMeta({ definePageMeta({
@@ -30,6 +34,9 @@ const { formatMessage } = useVIntl()
const route = useRoute() const route = useRoute()
const router = useRouter() const router = useRouter()
const modSettings = useModerationSettings()
const marginTarget = computed(() => getMarginTarget(modSettings.value))
const messages = defineMessages({ const messages = defineMessages({
projectsTitle: { projectsTitle: {
id: 'moderation.page.projects', id: 'moderation.page.projects',
+20 -7
View File
@@ -1,8 +1,8 @@
import type { EnumSettingDefinition, ToggleSettingDefinition } from '../types/settings.ts' import { setting } from '../types/settings.ts'
const settings = { const settings = {
General: { General: {
ChecklistPosition: { ChecklistPosition: setting.asEnum({
type: 'enum', type: 'enum',
id: 'checklist-position', id: 'checklist-position',
title: 'Checklist Position', title: 'Checklist Position',
@@ -12,21 +12,34 @@ const settings = {
{ value: 'right', label: 'Right' }, { value: 'right', label: 'Right' },
], ],
default: 'right', default: 'right',
} as EnumSettingDefinition, }),
ProjectKeybinds: { ProjectKeybinds: setting.asToggle({
type: 'toggle', type: 'toggle',
id: 'project-keybinds', id: 'project-keybinds',
title: 'Enable Project Keybinds', title: 'Enable Project Keybinds',
description: 'Weather certain keybinds should work without the checklist visible.', description: 'Weather certain keybinds should work without the checklist visible.',
default: false, default: false,
} as ToggleSettingDefinition, }),
PrivateMessageHighlight: { PrivateMessageHighlight: setting.asToggle({
type: 'toggle', type: 'toggle',
id: 'private-message-highlight', id: 'private-message-highlight',
title: 'Highlight Private Messages', title: 'Highlight Private Messages',
description: 'Whether private messages should be highlighted in the chat.', description: 'Whether private messages should be highlighted in the chat.',
default: true, default: true,
} as ToggleSettingDefinition, }),
AdjustPageAlignment: setting.asEnum({
type: 'enum',
id: 'adjust-page-alignment',
title: 'Adjust Page Alignment',
description:
'Whether the main page elements should be centered or offset opposite to the Checklist Position.',
entries: [
{ value: 'never', label: 'Never' },
{ value: 'checklist-present', label: 'Checklist Needed' },
{ value: 'always', label: 'Always' },
],
default: 'never',
}),
}, },
} as const } as const
+13 -2
View File
@@ -1,4 +1,5 @@
import type { SettingDefinitionBase } from '../types/settings.ts' import { isValidFor, type SettingDefinitionBase } from '../types/settings.ts'
import { moderationSettings } from '../index.ts'
export class Settings { export class Settings {
private readonly settings: { [id: string]: any } private readonly settings: { [id: string]: any }
@@ -13,7 +14,9 @@ export class Settings {
} }
get<T>(definition: SettingDefinitionBase<T>): T { get<T>(definition: SettingDefinitionBase<T>): T {
return this.settings[definition.id] ?? definition.default const value = this.settings[definition.id]
return (isValidFor(definition, value) ? value : undefined) ?? definition.default
} }
set<T>(definition: SettingDefinitionBase<T>, value?: T): void { set<T>(definition: SettingDefinitionBase<T>, value?: T): void {
@@ -23,3 +26,11 @@ export class Settings {
this.onChange() this.onChange()
} }
} }
export function getMarginTarget(settings: Settings): string {
return settings.get(moderationSettings.General.AdjustPageAlignment) == 'always'
? settings.get(moderationSettings.General.ChecklistPosition) == 'right'
? 'r'
: 'l'
: 'x'
}
+40 -3
View File
@@ -1,5 +1,5 @@
export interface SettingDefinitionBase<T> { export interface SettingDefinitionBase<T> {
type: string type: SettingDefinitionTypes
id: string id: string
title: string title: string
description: string description: string
@@ -11,16 +11,53 @@ export interface ToggleSettingDefinition extends SettingDefinitionBase<boolean>
type: 'toggle' type: 'toggle'
} }
export interface EnumSettingDefinition extends SettingDefinitionBase<string> { export interface EnumSettingDefinition<T extends string = string> extends SettingDefinitionBase<T> {
type: 'enum' type: 'enum'
entries: { entries: {
label: string label: string
value: string value: T
}[] }[]
} }
export interface StringSettingDefinition extends SettingDefinitionBase<string> { export interface StringSettingDefinition extends SettingDefinitionBase<string> {
type: 'string' type: 'string'
regex?: RegExp
}
export type SettingDefinitions = ReturnType<(typeof setting)[keyof typeof setting]>
export type SettingDefinitionTypes = SettingDefinitions['type']
export const setting = {
asEnum: <const E extends ReadonlyArray<{ label: string; value: string }>>(
data: EnumSettingDefinition<E[number]['value']> & { entries: E },
): EnumSettingDefinition<E[number]['value']> => {
return data as EnumSettingDefinition<E[number]['value']>
},
asToggle: (data: ToggleSettingDefinition) => data,
asString: (data: StringSettingDefinition) => data,
}
export function isValidFor(definitionBase: SettingDefinitionBase<any>, value: any): boolean {
if (value != null) {
// Tried my best with type safety but sadly having the `SettingDefinitions` as the type leads to issues handling types else where...
const definition = definitionBase as SettingDefinitions
if (
definition.type == 'enum' &&
definition.entries.map((entry) => entry.value).includes(value)
) {
return true
} else if (definition.type == 'toggle' && typeof value === 'boolean') {
return true
} else if (
definition.type == 'string' &&
typeof value === 'string' &&
(!definition.regex || value.match(definition.regex))
) {
return true
}
}
return false
} }
export type SettingDefinition = export type SettingDefinition =