feat: shared components for worlds + p2p instances (#5135)

* feat: base content card component

* fix: tooltips + colors

* feat: fix orgs

* feat: add ContentModpackCard

* fix: extract types

* feat: selection v-model

* add show icon in selected for combobox with stories

* feat: add project combobox

* clean up project combobox

* feat: start install to play modal

* fix: events

* feat: figma alignments

* feat: migrate toggle to tailwind

* fix: row borders

* feat: disabled state

* feat: virtual list impl for card table based on window scroll

* fix: lint

* feat: virtualization + smaller contentcard items

* feat: fix gap + border issues on last elm

* fix: use TeleportOverflowMenu

* fix: hasUpdate type

* fix: fallback to svg if src is invalid on avatar component

* fix: storybook

* feat: start on updater modal

* feat: finish content updater modal

* feat: i18n pass

* remove install to play modal from ui package

* pnpm prepr

* feat: reusable table component

* feat: add column width prop for table and fix stories

* feat: add table overflow menu story example

* feat: add surface-1.5 and use in table

* chore: export table in index

* fix: allow more loose typing on columns

* feat: update table component to derive key from column instead of data

* feat: surface 1.5 for oled + refactor story for contentcardtable + yeet sorting funcs

* fix: lint

* feat: add no padding story for new modal

---------

Signed-off-by: Calum H. <contact@cal.engineer>
Co-authored-by: tdgao <mr.trumgao@gmail.com>
This commit is contained in:
Calum H.
2026-01-28 20:09:24 +00:00
committed by GitHub
parent 728f8db7b9
commit 78aca7e5c0
52 changed files with 4097 additions and 939 deletions

View File

@@ -223,10 +223,7 @@
:where(input) {
box-sizing: border-box;
max-height: 40px;
&:not(.stylized-toggle) {
max-width: 100%;
}
max-width: 100%;
}
:where(.adjacent-input, &.adjacent-input) {
@@ -271,10 +268,6 @@
&:not(&.small) {
flex-direction: column;
align-items: flex-start;
.stylized-toggle {
flex-basis: 0;
}
}
}
}
@@ -650,64 +643,6 @@ tr.button-transparent {
}
}
.switch {
-webkit-appearance: none;
-moz-appearance: none;
appearance: none;
-webkit-tap-highlight-color: transparent;
cursor: pointer;
&:focus {
//outline: 0; Bad for accessibility
}
}
.stylized-toggle {
@extend .button-base;
box-sizing: content-box;
min-height: 32px;
height: 32px;
width: 52px;
max-width: 52px;
border-radius: var(--size-rounded-max);
display: inline-block;
position: relative;
margin: 0;
transition: all 0.2s ease;
background: var(--color-button-bg);
&:after {
content: '';
position: absolute;
top: 7px;
left: 7px;
width: 18px;
height: 18px;
border-radius: 50%;
background: var(--color-toggle-handle);
transition: all 0.2s cubic-bezier(0.5, 0.1, 0.75, 1.35);
outline: 2px solid transparent;
@media (prefers-reduced-motion) {
transition: none;
}
}
&:checked {
background-color: var(--color-brand);
&:after {
transform: translatex(20px);
background: var(--color-brand-inverted);
}
}
&:hover &:focus {
background: var(--color-button-bg);
}
}
.textarea-wrapper {
display: flex;
flex-direction: column;

View File

@@ -60,8 +60,6 @@ html {
--color-button-bg-active: #c3c6cb;
--color-button-text-active: var(--color-button-text-hover);
--color-toggle-handle: var(--color-icon);
--color-dropdown-bg: var(--color-button-bg);
--color-dropdown-text: var(--color-button-text);
@@ -177,8 +175,6 @@ html {
--color-button-bg-active: #616570;
--color-button-text-active: var(--color-button-text-hover);
--color-toggle-handle: var(--color-button-text);
--color-dropdown-bg: var(--color-button-bg);
--color-dropdown-text: var(--color-button-text);

View File

@@ -32,12 +32,7 @@
<label class="w-full text-lg font-bold text-contrast" for="modpack-hard-reset">
Erase all data
</label>
<input
id="modpack-hard-reset"
v-model="hardReset"
class="switch stylized-toggle shrink-0"
type="checkbox"
/>
<Toggle id="modpack-hard-reset" v-model="hardReset" class="shrink-0" />
</div>
<div>
If enabled, existing mods, worlds, and configurations, will be deleted before installing
@@ -69,7 +64,7 @@
<script setup lang="ts">
import { DownloadIcon, XIcon } from '@modrinth/assets'
import { ButtonStyled, Combobox, injectNotificationManager, NewModal } from '@modrinth/ui'
import { ButtonStyled, Combobox, injectNotificationManager, NewModal, Toggle } from '@modrinth/ui'
import { ModrinthServersFetchError } from '@modrinth/utils'
import type { ModrinthServer } from '~/composables/servers/modrinth-servers.ts'
@@ -158,9 +153,3 @@ const hide = () => modal.value?.hide()
defineExpose({ show, hide })
</script>
<style scoped>
.stylized-toggle:checked::after {
background: var(--color-accent-contrast) !important;
}
</style>

View File

@@ -53,12 +53,7 @@
<label class="w-full text-lg font-bold text-contrast" for="hard-reset">
Erase all data
</label>
<input
id="hard-reset"
v-model="hardReset"
class="switch stylized-toggle shrink-0"
type="checkbox"
/>
<Toggle id="hard-reset" v-model="hardReset" class="shrink-0" />
</div>
<div>
Removes all data on your server, including your worlds, mods, and configuration
@@ -128,6 +123,7 @@ import {
ButtonStyled,
injectNotificationManager,
NewModal,
Toggle,
} from '@modrinth/ui'
import { ModrinthServersFetchError } from '@modrinth/utils'
import { onMounted, onUnmounted } from 'vue'
@@ -255,9 +251,3 @@ const hide = () => mrpackModal.value?.hide()
defineExpose({ show, hide })
</script>
<style scoped>
.stylized-toggle:checked::after {
background: var(--color-accent-contrast) !important;
}
</style>

View File

@@ -143,12 +143,7 @@
<label class="w-full text-lg font-bold text-contrast" for="hard-reset">
Erase all data
</label>
<input
id="hard-reset"
v-model="hardReset"
class="switch stylized-toggle shrink-0"
type="checkbox"
/>
<Toggle id="hard-reset" v-model="hardReset" class="shrink-0" />
</div>
<div>
Removes all data on your server, including your worlds, mods, and configuration files,
@@ -542,9 +537,3 @@ const hide = () => versionSelectModal.value?.hide()
defineExpose({ show, hide })
</script>
<style scoped>
.stylized-toggle:checked::after {
background: var(--color-accent-contrast) !important;
}
</style>

View File

@@ -274,10 +274,6 @@ watch(
</script>
<style scoped>
.stylized-toggle:checked::after {
background: var(--color-accent-contrast) !important;
}
.button-base:active {
scale: none !important;
}

View File

@@ -351,12 +351,10 @@
monetization weights to this user on the project.
</span>
</label>
<input
<Toggle
:id="`member-${allOrgMembers[index].user.username}-override-perms`"
v-model="allOrgMembers[index].override"
class="switch stylized-toggle"
type="checkbox"
:disabled="(currentMember?.permissions & EDIT_MEMBER) !== EDIT_MEMBER"
:disabled="(props.currentMember?.permissions & EDIT_MEMBER) !== EDIT_MEMBER"
/>
</div>
<div class="adjacent-input">
@@ -535,6 +533,7 @@ import {
ConfirmModal,
injectNotificationManager,
injectProjectPageContext,
Toggle,
} from '@modrinth/ui'
import { Multiselect } from 'vue-multiselect'

View File

@@ -26,6 +26,7 @@ import {
SearchFilterControl,
SearchSidebarFilter,
type SortType,
Toggle,
useSearch,
} from '@modrinth/ui'
import { capitalizeString, cycleValue, type Mod as InstallableMod } from '@modrinth/utils'
@@ -466,13 +467,7 @@ useSeoMeta({
</div>
<div class="flex flex-row items-center justify-between gap-2 px-6">
<label for="erase-data-on-install"> Erase all data on install </label>
<input
id="erase-data-on-install"
v-model="eraseDataOnInstall"
label="Erase all data on install"
class="switch stylized-toggle flex-none"
type="checkbox"
/>
<Toggle id="erase-data-on-install" v-model="eraseDataOnInstall" class="flex-none" />
</div>
<div class="px-6 py-4 text-sm">
If enabled, existing mods, worlds, and configurations, will be deleted before installing
@@ -921,8 +916,4 @@ useSeoMeta({
mask-image: linear-gradient(to bottom, black, transparent);
opacity: 0.25;
}
.stylized-toggle:checked::after {
background: var(--color-accent-contrast) !important;
}
</style>

View File

@@ -1,67 +1,76 @@
<script setup lang="ts">
import { SearchIcon } from '@modrinth/assets'
import { Toggle } from '@modrinth/ui'
import Fuse from 'fuse.js'
import { computed, ref, shallowReactive } from 'vue'
import {
DEFAULT_FEATURE_FLAGS,
type FeatureFlag,
saveFeatureFlags,
useFeatureFlags,
} from '~/composables/featureFlags.ts'
const flags = shallowReactive(useFeatureFlags().value)
const searchQuery = ref('')
const allFlags = computed(() => Object.keys(flags) as FeatureFlag[])
const fuse = computed(
() =>
new Fuse(allFlags.value, {
threshold: 0.4,
}),
)
const filteredFlags = computed(() => {
if (!searchQuery.value.trim()) {
return allFlags.value
}
return fuse.value.search(searchQuery.value).map((result) => result.item)
})
</script>
<template>
<div class="page">
<h1>Feature flags</h1>
<div class="flags">
<div class="mx-auto my-4 box-border w-[calc(100%-2rem)] max-w-[800px]">
<h1 class="mb-4 text-2xl font-bold text-contrast">Feature flags</h1>
<div class="relative mb-2">
<SearchIcon
class="pointer-events-none absolute left-3 top-1/2 size-5 -translate-y-1/2 text-secondary"
/>
<input
v-model="searchQuery"
type="search"
placeholder="Search flags..."
class="w-full rounded-xl bg-bg-raised py-2 pl-10 pr-4"
/>
</div>
<div class="flex flex-col gap-2">
<div
v-for="flag in Object.keys(flags) as FeatureFlag[]"
v-for="flag in filteredFlags"
:key="`flag-${flag}`"
class="adjacent-input small card"
class="flex flex-row flex-wrap items-center gap-2 rounded-2xl bg-bg-raised p-4"
>
<label :for="`toggle-${flag}`">
<span class="label__title">
<label :for="`toggle-${flag}`" class="flex-1">
<span class="block font-semibold capitalize">
{{ flag.replaceAll('_', ' ') }}
</span>
<span class="label__description">
<p>
Default:
<span
:style="`color:var(--color-${
DEFAULT_FEATURE_FLAGS[flag] === false ? 'red' : 'green'
})`"
>{{ DEFAULT_FEATURE_FLAGS[flag] }}</span
>
</p>
</span>
<p class="m-0 text-secondary">
Default:
<span :class="DEFAULT_FEATURE_FLAGS[flag] === false ? 'text-red' : 'text-green'">
{{ DEFAULT_FEATURE_FLAGS[flag] }}
</span>
</p>
</label>
<input
<Toggle
:id="`toggle-${flag}`"
v-model="flags[flag]"
class="switch stylized-toggle"
type="checkbox"
@change="() => saveFeatureFlags()"
@update:model-value="() => saveFeatureFlags()"
/>
</div>
<p v-if="filteredFlags.length === 0" class="text-center text-secondary">
No flags found matching "{{ searchQuery }}"
</p>
</div>
</div>
</template>
<style lang="scss" scoped>
.page {
width: calc(100% - 2 * var(--spacing-card-md));
max-width: 800px;
margin-inline: auto;
box-sizing: border-box;
margin-block: var(--spacing-card-md);
}
.flags {
}
.label__title {
text-transform: capitalize;
}
.label__description p {
margin: 0;
}
</style>

View File

@@ -242,13 +242,11 @@
>
</div>
<input
<Toggle
:id="`toggle-${getStableModKey(mod)}`"
:checked="!mod.disabled"
:model-value="!mod.disabled"
:disabled="mod.changing"
class="switch stylized-toggle"
type="checkbox"
@change="toggleMod(mod)"
@update:model-value="toggleMod(mod)"
/>
</div>
</div>
@@ -353,7 +351,13 @@ import {
TrashIcon,
WrenchIcon,
} from '@modrinth/assets'
import { Avatar, ButtonStyled, injectModrinthClient, injectNotificationManager } from '@modrinth/ui'
import {
Avatar,
ButtonStyled,
injectModrinthClient,
injectNotificationManager,
Toggle,
} from '@modrinth/ui'
import type { Mod } from '@modrinth/utils'
import { computed, onMounted, onUnmounted, ref, watch } from 'vue'
@@ -699,8 +703,4 @@ const filteredMods = computed(() => {
height: 1px;
visibility: hidden;
}
.stylized-toggle:checked::after {
background: var(--color-accent-contrast) !important;
}
</style>

View File

@@ -21,11 +21,10 @@
</div>
<span>{{ prefConfig.description }}</span>
</label>
<input
<Toggle
:id="`pref-${key}`"
v-model="newUserPreferences[key]"
class="switch stylized-toggle flex-none"
type="checkbox"
class="flex-none"
:disabled="prefConfig.implemented === false"
/>
</div>
@@ -42,7 +41,7 @@
</template>
<script setup lang="ts">
import { injectNotificationManager } from '@modrinth/ui'
import { injectNotificationManager, Toggle } from '@modrinth/ui'
import { useStorage } from '@vueuse/core'
import SaveBanner from '~/components/ui/servers/SaveBanner.vue'
@@ -117,9 +116,3 @@ const resetPreferences = () => {
newUserPreferences.value = { ...userPreferences.value }
}
</script>
<style scoped>
.stylized-toggle:checked::after {
background: var(--color-accent-contrast) !important;
}
</style>

View File

@@ -62,11 +62,9 @@
/>
</div>
<div v-else-if="typeof property === 'boolean'" class="flex justify-end">
<input
<Toggle
:id="`server-property-${index}`"
v-model="liveProperties[index]"
class="switch stylized-toggle"
type="checkbox"
:aria-labelledby="`property-label-${index}`"
/>
</div>
@@ -134,7 +132,7 @@
<script setup lang="ts">
import { EyeIcon, SearchIcon } from '@modrinth/assets'
import { Combobox, injectModrinthClient, injectNotificationManager } from '@modrinth/ui'
import { Combobox, injectModrinthClient, injectNotificationManager, Toggle } from '@modrinth/ui'
import Fuse from 'fuse.js'
import { computed, inject, ref, watch } from 'vue'
@@ -331,9 +329,3 @@ const isComplexProperty = (property: any): boolean => {
)
}
</script>
<style scoped>
.stylized-toggle:checked::after {
background: var(--color-accent-contrast) !important;
}
</style>

View File

@@ -69,12 +69,7 @@
</span>
</div>
<div class="flex items-center gap-2">
<input
id="show-all-versions"
v-model="showAllVersions"
class="switch stylized-toggle flex-none"
type="checkbox"
/>
<Toggle id="show-all-versions" v-model="showAllVersions" class="flex-none" />
<label for="show-all-versions" class="text-sm">Show all Java versions</label>
</div>
<Combobox
@@ -113,7 +108,7 @@
<script setup lang="ts">
import { IssuesIcon, UpdatedIcon } from '@modrinth/assets'
import { ButtonStyled, Combobox, injectNotificationManager } from '@modrinth/ui'
import { ButtonStyled, Combobox, injectNotificationManager, Toggle } from '@modrinth/ui'
import SaveBanner from '~/components/ui/servers/SaveBanner.vue'
import type { ModrinthServer } from '~/composables/servers/modrinth-servers.ts'
@@ -232,9 +227,3 @@ function resetToDefault() {
invocation.value = originalInvocation.value ?? ''
}
</script>
<style scoped>
.stylized-toggle:checked::after {
background: var(--color-accent-contrast) !important;
}
</style>

View File

@@ -119,85 +119,78 @@
<section class="universal-card">
<h2 class="text-2xl">{{ formatMessage(toggleFeatures.title) }}</h2>
<p class="mb-4">{{ formatMessage(toggleFeatures.description) }}</p>
<div class="adjacent-input small">
<label for="advanced-rendering">
<span class="label__title">
{{ formatMessage(toggleFeatures.advancedRenderingTitle) }}
</span>
<span class="label__description">
{{ formatMessage(toggleFeatures.advancedRenderingDescription) }}
</span>
</label>
<input
id="advanced-rendering"
v-model="cosmetics.advancedRendering"
class="switch stylized-toggle"
type="checkbox"
/>
</div>
<div class="adjacent-input small">
<label for="external-links-new-tab">
<span class="label__title">
{{ formatMessage(toggleFeatures.externalLinksNewTabTitle) }}
</span>
<span class="label__description">
{{ formatMessage(toggleFeatures.externalLinksNewTabDescription) }}
</span>
</label>
<input
id="external-links-new-tab"
v-model="cosmetics.externalLinksNewTab"
class="switch stylized-toggle"
type="checkbox"
/>
</div>
<div v-if="false" class="adjacent-input small">
<label for="modrinth-app-promos">
<span class="label__title">
{{ formatMessage(toggleFeatures.hideModrinthAppPromosTitle) }}
</span>
<span class="label__description">
{{ formatMessage(toggleFeatures.hideModrinthAppPromosDescription) }}
</span>
</label>
<input
id="modrinth-app-promos"
v-model="cosmetics.hideModrinthAppPromos"
class="switch stylized-toggle"
type="checkbox"
/>
</div>
<div class="adjacent-input small">
<label for="search-layout-toggle">
<span class="label__title">
{{ formatMessage(toggleFeatures.rightAlignedFiltersSidebarTitle) }}
</span>
<span class="label__description">
{{ formatMessage(toggleFeatures.rightAlignedFiltersSidebarDescription) }}
</span>
</label>
<input
id="search-layout-toggle"
v-model="cosmetics.rightSearchLayout"
class="switch stylized-toggle"
type="checkbox"
/>
</div>
<div class="adjacent-input small">
<label for="project-layout-toggle">
<span class="label__title">
{{ formatMessage(toggleFeatures.leftAlignedContentSidebarTitle) }}
</span>
<span class="label__description">
{{ formatMessage(toggleFeatures.leftAlignedContentSidebarDescription) }}
</span>
</label>
<input
id="project-layout-toggle"
v-model="cosmetics.leftContentLayout"
class="switch stylized-toggle"
type="checkbox"
/>
<div class="flex flex-col gap-4">
<div class="flex flex-row flex-wrap items-center justify-between gap-2">
<label for="advanced-rendering" class="flex-1">
<span class="block font-semibold text-contrast">
{{ formatMessage(toggleFeatures.advancedRenderingTitle) }}
</span>
<span class="text-secondary">
{{ formatMessage(toggleFeatures.advancedRenderingDescription) }}
</span>
</label>
<Toggle id="advanced-rendering" v-model="cosmetics.advancedRendering" class="shrink-0" />
</div>
<div class="flex flex-row flex-wrap items-center justify-between gap-2">
<label for="external-links-new-tab" class="flex-1">
<span class="block font-semibold text-contrast">
{{ formatMessage(toggleFeatures.externalLinksNewTabTitle) }}
</span>
<span class="text-secondary">
{{ formatMessage(toggleFeatures.externalLinksNewTabDescription) }}
</span>
</label>
<Toggle
id="external-links-new-tab"
v-model="cosmetics.externalLinksNewTab"
class="shrink-0"
/>
</div>
<div v-if="false" class="flex flex-row flex-wrap items-center justify-between gap-2">
<label for="modrinth-app-promos" class="flex-1">
<span class="block font-semibold text-contrast">
{{ formatMessage(toggleFeatures.hideModrinthAppPromosTitle) }}
</span>
<span class="text-secondary">
{{ formatMessage(toggleFeatures.hideModrinthAppPromosDescription) }}
</span>
</label>
<Toggle
id="modrinth-app-promos"
v-model="cosmetics.hideModrinthAppPromos"
class="shrink-0"
/>
</div>
<div class="flex flex-row flex-wrap items-center justify-between gap-2">
<label for="search-layout-toggle" class="flex-1">
<span class="block font-semibold text-contrast">
{{ formatMessage(toggleFeatures.rightAlignedFiltersSidebarTitle) }}
</span>
<span class="text-secondary">
{{ formatMessage(toggleFeatures.rightAlignedFiltersSidebarDescription) }}
</span>
</label>
<Toggle
id="search-layout-toggle"
v-model="cosmetics.rightSearchLayout"
class="shrink-0"
/>
</div>
<div class="flex flex-row flex-wrap items-center justify-between gap-2">
<label for="project-layout-toggle" class="">
<span class="block font-semibold text-contrast">
{{ formatMessage(toggleFeatures.leftAlignedContentSidebarTitle) }}
</span>
<span class="text-secondary">
{{ formatMessage(toggleFeatures.leftAlignedContentSidebarDescription) }}
</span>
</label>
<Toggle
id="project-layout-toggle"
v-model="cosmetics.leftContentLayout"
class="shrink-0"
/>
</div>
</div>
</section>
</div>
@@ -212,6 +205,7 @@ import {
IntlFormatted,
normalizeChildren,
ThemeSelector,
Toggle,
useVIntl,
} from '@modrinth/ui'
import { formatProjectType } from '@modrinth/utils'