Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
122 commits
Select commit Hold shift + click to select a range
d324768
feat: base content card component
IMB11 Jan 16, 2026
b60127f
fix: tooltips + colors
IMB11 Jan 16, 2026
fd23751
feat: fix orgs
IMB11 Jan 16, 2026
5f55f53
feat: base content tab internals rewrite
IMB11 Jan 16, 2026
a27ae0d
feat: fix invalidmodal
IMB11 Jan 18, 2026
513e711
feat: add ContentModpackCard
IMB11 Jan 19, 2026
caa2bbc
fix: extract types
IMB11 Jan 19, 2026
01e2162
draft: layout
IMB11 Jan 19, 2026
dd663d6
feat: unlink modal
IMB11 Jan 19, 2026
1beb10b
feat: impl content tab
IMB11 Jan 19, 2026
92c21bc
fix: lint
IMB11 Jan 19, 2026
556bdc8
fix: toggling
IMB11 Jan 19, 2026
9db7dfc
temp: disable updating stuff
IMB11 Jan 19, 2026
d0a4ba9
feat: selection v-model
IMB11 Jan 19, 2026
fcdc1fd
feat: bulk selection
IMB11 Jan 19, 2026
ac6d15d
feat: mods tab rough draft
IMB11 Jan 19, 2026
c212d20
feat: use fuse.js
IMB11 Jan 20, 2026
84920e4
feat: add project combobox
tdgao Jan 19, 2026
ca6d194
clean up project combobox
tdgao Jan 19, 2026
c6c63df
feat: start install to play modal
tdgao Jan 19, 2026
e7d2f6d
fix: events
IMB11 Jan 20, 2026
c79e1cb
feat: use v-on
IMB11 Jan 20, 2026
9296c70
feat: bulk actions + fix floating action bar width
IMB11 Jan 20, 2026
fa64cf4
feat: figma alignments
IMB11 Jan 22, 2026
ce44757
feat: migrate toggle to tailwind
IMB11 Jan 22, 2026
0d02a76
fix: row borders
IMB11 Jan 22, 2026
7c1f75f
feat: disabled state
IMB11 Jan 22, 2026
40141f8
feat: virtual list impl for card table based on window scroll
IMB11 Jan 22, 2026
b3e61d2
fix: lint
IMB11 Jan 22, 2026
ec6a3d9
feat: virtualization + smaller contentcard items
IMB11 Jan 22, 2026
b0e172d
feat: use ContentCardTable + ContentCardItems
IMB11 Jan 22, 2026
65c3a2c
feat: fix gap + border issues on last elm
IMB11 Jan 22, 2026
8e25b33
feat: cleanup + use proper searching
IMB11 Jan 22, 2026
5d13fb3
fix: use TeleportOverflowMenu
IMB11 Jan 22, 2026
ecad8ca
fix: fallback to svg if src is invalid on avatar component
IMB11 Jan 22, 2026
b57716f
fix: storybook
IMB11 Jan 24, 2026
9a93997
feat: start on updater modal
IMB11 Jan 24, 2026
a5a9def
feat: finish content updater modal
IMB11 Jan 26, 2026
b6d995f
feat: i18n pass
IMB11 Jan 26, 2026
fa87ed9
feat: impl modal
IMB11 Jan 26, 2026
ecda4ab
feat(app): backend changes for content tab refactor (#5237)
IMB11 Jan 29, 2026
e030060
feat: include_changelog=false for updater modal
IMB11 Jan 29, 2026
7a54d6d
fix: hash overrides
IMB11 Jan 29, 2026
c1f8228
feat: update checking for modpack
IMB11 Jan 29, 2026
f43ff94
feat: qa
IMB11 Jan 29, 2026
4e151e5
feat: modpack content modal
IMB11 Jan 29, 2026
4b23d0f
fix: padding in table to match modals + tightness
IMB11 Jan 29, 2026
01b3a73
fix: lint
IMB11 Jan 29, 2026
df8e628
feat: delete modal
IMB11 Jan 30, 2026
5e23680
feat: fix toggle bugs
IMB11 Feb 1, 2026
1a01cf8
fix: prepr
IMB11 Feb 1, 2026
8e0b45b
Merge remote-tracking branch 'origin/main' into cal/content-tab-rewri…
IMB11 Feb 2, 2026
23fdd54
fix: duplicate messages
IMB11 Feb 2, 2026
dd5579d
qa: full width search
IMB11 Feb 3, 2026
c422ea9
qa: use bg-surface-1.5
IMB11 Feb 3, 2026
9fe30d7
qa: animation for filter pills
IMB11 Feb 3, 2026
dc035fb
qa: standardize hover colors
IMB11 Feb 3, 2026
29c7a14
fix: border-[1px] is border
IMB11 Feb 3, 2026
8cb279b
qa: mass de-select actually mass selecting
IMB11 Feb 3, 2026
93ba19a
qa: match figma designs for floating action bar
IMB11 Feb 3, 2026
051807c
Merge remote-tracking branch 'origin/main' into cal/content-tab-rewri…
IMB11 Feb 6, 2026
f235cbd
qa: modal fixes
IMB11 Feb 6, 2026
2b7f542
q: modal fixes x2
IMB11 Feb 6, 2026
9e623ba
fix: table border
IMB11 Feb 6, 2026
c738d70
qa: confirm modals
IMB11 Feb 6, 2026
91fd55f
qa: modal alignment
IMB11 Feb 6, 2026
a437d2c
qa: re-add stuck heading + dedupe logic
IMB11 Feb 6, 2026
eb31bf8
qa: dedupe virtual scrolling + remove dead components
IMB11 Feb 6, 2026
fd4b51f
qa: responsiveness for content table + link fixes
IMB11 Feb 6, 2026
54e0e63
qa: version column link, tooltips + lint fixes
IMB11 Feb 6, 2026
f13e97d
qa: instance busy protections
IMB11 Feb 6, 2026
19c1f2f
fix: installation freeze bug
IMB11 Feb 6, 2026
ecc9dd4
chore: remove old mods page
IMB11 Feb 6, 2026
0308dd8
refactor: deduplicate layout
IMB11 Feb 7, 2026
5fecbc0
chore: delete old content page(s)
IMB11 Feb 7, 2026
30e34ac
Merge branch 'main' into cal/content-tab-rewrite-hosting
IMB11 Feb 7, 2026
dd443eb
qa
IMB11 Feb 7, 2026
006ea25
qa
IMB11 Feb 7, 2026
562aeb0
qa
IMB11 Feb 7, 2026
50e9417
feat: sort btn - to iterate
IMB11 Feb 7, 2026
64f4ab8
fix: ml
IMB11 Feb 7, 2026
98e317c
feat: date added
IMB11 Feb 7, 2026
4699824
fix: lint
IMB11 Feb 7, 2026
0212f61
Merge remote-tracking branch 'origin/main' into cal/content-tab-rewri…
IMB11 Feb 7, 2026
cbbcf3c
fix: formatting.ts removal
IMB11 Feb 7, 2026
ae326bc
feat: get_dependencies_as_content_items
IMB11 Feb 10, 2026
56cc76a
qa: final QA changes
IMB11 Feb 11, 2026
3be13b8
refactor: deduplicate + polish content.rs
IMB11 Feb 11, 2026
e6cd3a8
feat: hook up content.vue with v1
IMB11 Feb 11, 2026
d8c6e2b
feat: hide v1 content api behind frontend feature flag
IMB11 Feb 11, 2026
cc528ed
fix: query keys + copy on empty state
IMB11 Feb 11, 2026
1892141
chore: i18n pass
IMB11 Feb 11, 2026
ed978de
feat: reimpl unlink + upload endpoint
IMB11 Feb 11, 2026
63d1a56
feat: use bulk endpoints v1
IMB11 Feb 11, 2026
a4093aa
fix: lint
IMB11 Feb 11, 2026
c252a2a
Merge remote-tracking branch 'origin/main' into cal/content-tab-rewri…
IMB11 Feb 11, 2026
b301790
fix: flags
IMB11 Feb 11, 2026
dd97560
fix: responsiveness via container queries
IMB11 Feb 11, 2026
e5bd244
fix: lint
IMB11 Feb 11, 2026
70ccf6a
qa: 1
IMB11 Feb 12, 2026
f64626e
qa: fixes
IMB11 Feb 12, 2026
da7eb2b
qa: fix ssr issues with browse content
IMB11 Feb 13, 2026
2fbb051
qa: header page divider
IMB11 Feb 13, 2026
b808c62
qa: modals
IMB11 Feb 16, 2026
65ffb2c
fix: prepr
IMB11 Feb 16, 2026
36cf29d
fix: issues
IMB11 Feb 16, 2026
890b325
fix: lint
IMB11 Feb 16, 2026
4236cea
fix: toggle v1 ff
IMB11 Feb 16, 2026
5362f77
Merge remote-tracking branch 'origin/main' into cal/content-tab-rewri…
IMB11 Feb 18, 2026
4bd5f63
qa: 5
IMB11 Feb 20, 2026
caf4116
qa: delete modal copy
IMB11 Feb 20, 2026
30d2ef3
feat: creation flow modals (#5383)
IMB11 Feb 27, 2026
24d0377
refactor: delete content v0 usages + impl
IMB11 Feb 27, 2026
47d955e
feat: qa + fixes
IMB11 Feb 27, 2026
d9bc481
feat: installing banner using state event
IMB11 Feb 27, 2026
0853035
feat: fix modpack card bugs + filtering issues
IMB11 Feb 27, 2026
c6f7997
refactor: delete backups v0 api module
IMB11 Feb 27, 2026
7d920d0
feat: v1 servers GET endpoint
IMB11 Feb 27, 2026
322a2e8
fix: backups
IMB11 Feb 27, 2026
3e34905
feat: swap to kyros upload v1 addon
IMB11 Feb 27, 2026
3b98ba6
Merge remote-tracking branch 'origin/main' into cal/content-tab-rewri…
IMB11 Feb 27, 2026
438e4ef
fix: use tanstack for loader.vue
IMB11 Feb 27, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
14 changes: 13 additions & 1 deletion CLAUDE.md
Original file line number Diff line number Diff line change
Expand Up @@ -67,10 +67,22 @@ Each project may have its own `CLAUDE.md` with detailed instructions:
- [`apps/labrinth/CLAUDE.md`](apps/labrinth/CLAUDE.md) — Backend API
- [`apps/frontend/CLAUDE.md`](apps/frontend/CLAUDE.md) - Frontend Website

## Skills (`.claude/skills/`)

Project-specific skill files with detailed patterns. Use them when the task matches:

- **`api-module`** — Adding a new API endpoint module to `packages/api-client` (types, module class, registry registration)
- **`cross-platform-pages`** — Building a page that needs to work in both the website (`apps/frontend`) and the desktop app (`apps/app-frontend`)
- **`dependency-injection`** — Creating or wiring up a `provide`/`inject` context for platform abstraction or deep component state sharing
- **`figma-mcp`** — Translating a Figma design into Vue components using the Figma MCP tools
- **`i18n-convert`** — Converting hardcoded English strings in Vue SFCs into the `@modrinth/ui` i18n system (`defineMessages`, `formatMessage`, `IntlFormatted`)
- **`multistage-modals`** — Building a wizard-like modal with multiple stages, progress tracking, and per-stage buttons using `MultiStageModal`
- **`tanstack-query`** — Fetching, caching, or mutating server data with `@tanstack/vue-query` (queries, mutations, invalidation, optimistic updates)

## Code Guidelines

### Comments
- DO NOT use "heading" comments like: // === Helper methods === .
- DO NOT use "heading" comments like: `=== Helper methods ===`.
- Use doc comments, but avoid inline comments unless ABSOLUTELY necessary for clarity. Code should aim to be self documenting!

## Bash Guidelines
Expand Down
5 changes: 3 additions & 2 deletions apps/app-frontend/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -28,17 +28,18 @@
"@tauri-apps/plugin-updater": "^2.7.1",
"@tauri-apps/plugin-window-state": "^2.2.2",
"@types/three": "^0.172.0",
"intl-messageformat": "^10.7.7",
"vue-i18n": "^10.0.0",
"@vueuse/core": "^11.1.0",
"dayjs": "^1.11.10",
"floating-vue": "^5.2.2",
"fuse.js": "^6.6.2",
"intl-messageformat": "^10.7.7",
"ofetch": "^1.3.4",
"pinia": "^3.0.0",
"posthog-js": "^1.158.2",
"three": "^0.172.0",
"vite-svg-loader": "^5.1.0",
"vue": "^3.5.13",
"vue-i18n": "^10.0.0",
"vue-multiselect": "3.0.0",
"vue-router": "^4.6.0",
"vue-virtual-scroller": "v2.0.0-beta.8"
Expand Down
35 changes: 25 additions & 10 deletions apps/app-frontend/src/App.vue
Original file line number Diff line number Diff line change
Expand Up @@ -31,12 +31,14 @@ import {
Button,
ButtonStyled,
commonMessages,
CreationFlowModal,
defineMessages,
I18nDebugPanel,
NewsArticleCard,
NotificationPanel,
OverflowMenu,
ProgressSpinner,
provideModalBehavior,
provideModrinthClient,
provideNotificationManager,
providePageContext,
Expand Down Expand Up @@ -64,7 +66,6 @@ import FriendsList from '@/components/ui/friends/FriendsList.vue'
import IncompatibilityWarningModal from '@/components/ui/install_flow/IncompatibilityWarningModal.vue'
import InstallConfirmModal from '@/components/ui/install_flow/InstallConfirmModal.vue'
import ModInstallModal from '@/components/ui/install_flow/ModInstallModal.vue'
import InstanceCreationModal from '@/components/ui/InstanceCreationModal.vue'
import MinecraftAuthErrorModal from '@/components/ui/minecraft-auth-error-modal/MinecraftAuthErrorModal.vue'
import AppSettingsModal from '@/components/ui/modal/AppSettingsModal.vue'
import AuthGrantFlowWaitModal from '@/components/ui/modal/AuthGrantFlowWaitModal.vue'
Expand All @@ -84,6 +85,7 @@ import { get_user } from '@/helpers/cache.js'
import { command_listener, warning_listener } from '@/helpers/events.js'
import { useFetch } from '@/helpers/fetch.js'
import { cancelLogin, get as getCreds, login, logout } from '@/helpers/mr_auth.ts'
import { create_profile_and_install_from_file } from '@/helpers/pack'
import { list } from '@/helpers/profile.js'
import { get as getSettings, set as setSettings } from '@/helpers/settings.ts'
import { get_opening_command, initialize_state } from '@/helpers/state'
Expand All @@ -100,11 +102,11 @@ import {
provideAppUpdateDownloadProgress,
subscribeToDownloadProgress,
} from '@/providers/download-progress.ts'
import { setupProviders } from '@/providers/setup'
import { useError } from '@/store/error.js'
import { useInstall } from '@/store/install.js'
import { useLoading, useTheming } from '@/store/state'

import { create_profile_and_install_from_file } from './helpers/pack'
import { generateSkinPreviews } from './helpers/rendering/batch-skin-renderer'
import { get_available_capes, get_available_skins } from './helpers/skins'
import { AppNotificationManager } from './providers/app-notifications'
Expand All @@ -129,6 +131,15 @@ providePageContext({
hierarchicalSidebarAvailable: ref(true),
showAds: ref(false),
})
provideModalBehavior({
noblur: computed(() => !themeStore.advancedRendering),
onShow: () => hide_ads_window(),
onHide: () => show_ads_window(),
})

const { installationModal, handleCreate, handleBrowseModpacks } =
setupProviders(notificationManager)

const news = ref([])
const availableSurvey = ref(false)

Expand Down Expand Up @@ -801,9 +812,13 @@ provideAppUpdateDownloadProgress(appUpdateDownload)
<Suspense>
<AuthGrantFlowWaitModal ref="modrinthLoginFlowWaitModal" @flow-cancel="cancelLogin" />
</Suspense>
<Suspense>
<InstanceCreationModal ref="installationModal" />
</Suspense>
<CreationFlowModal
ref="installationModal"
type="instance"
show-snapshot-toggle
@create="handleCreate"
@browse-modpacks="handleBrowseModpacks"
/>
<div
class="app-grid-navbar bg-bg-raised flex flex-col p-[0.5rem] pt-0 gap-[0.5rem] w-[--left-bar-width]"
>
Expand Down Expand Up @@ -849,7 +864,7 @@ provideAppUpdateDownloadProgress(appUpdateDownload)
</suspense>
<NavButton
v-tooltip.right="'Create new instance'"
:to="() => $refs.installationModal.show()"
:to="() => installationModal?.show()"
:disabled="offline"
>
<PlusIcon />
Expand Down Expand Up @@ -937,9 +952,9 @@ provideAppUpdateDownloadProgress(appUpdateDownload)
</NavButton>
</div>
<div data-tauri-drag-region class="app-grid-statusbar bg-bg-raised h-[--top-bar-height] flex">
<div data-tauri-drag-region class="flex p-3">
<ModrinthAppLogo class="h-full w-auto text-contrast pointer-events-none" />
<div data-tauri-drag-region class="flex items-center gap-1 ml-3">
<div data-tauri-drag-region class="flex min-w-0 flex-1 overflow-hidden p-3">
<ModrinthAppLogo class="h-full w-auto shrink-0 text-contrast pointer-events-none" />
<div data-tauri-drag-region class="flex shrink-0 items-center gap-1 ml-3">
<button
class="cursor-pointer p-0 m-0 text-contrast border-none outline-none bg-button-bg rounded-full flex items-center justify-center w-6 h-6 hover:brightness-75 transition-all"
@click="router.back()"
Expand All @@ -955,7 +970,7 @@ provideAppUpdateDownloadProgress(appUpdateDownload)
</div>
<Breadcrumbs class="pt-[2px]" />
</div>
<section data-tauri-drag-region class="flex ml-auto items-center">
<section data-tauri-drag-region class="flex shrink-0 ml-auto items-center">
<ButtonStyled
v-if="!forceSidebar && themeStore.toggleSidebar"
:type="sidebarToggled ? 'standard' : 'transparent'"
Expand Down
173 changes: 128 additions & 45 deletions apps/app-frontend/src/components/ui/Breadcrumbs.vue
Original file line number Diff line number Diff line change
@@ -1,64 +1,147 @@
<template>
<div data-tauri-drag-region class="flex items-center gap-1 pl-3">
<Button v-if="false" class="breadcrumbs__back transparent" icon-only @click="$router.back()">
<ChevronLeftIcon />
</Button>
<Button
v-if="false"
class="breadcrumbs__forward transparent"
icon-only
@click="$router.forward()"
<div
ref="outerRef"
data-tauri-drag-region
class="min-w-0 overflow-hidden pl-3"
:style="isOverflowing ? { '--scroll-distance': `-${overflowAmount}px` } : undefined"
@mouseenter="onMouseEnter"
@mouseleave="onMouseLeave"
>
<div
ref="innerRef"
data-tauri-drag-region
class="flex w-fit items-center gap-1"
:class="{ 'breadcrumbs-scroll': isAnimating }"
@animationiteration="onAnimationIteration"
>
<ChevronRightIcon />
</Button>
{{ breadcrumbData.resetToNames(breadcrumbs) }}
<template v-for="breadcrumb in breadcrumbs" :key="breadcrumb.name">
<router-link
v-if="breadcrumb.link"
:to="{
path: breadcrumb.link.replace('{id}', encodeURIComponent($route.params.id)),
query: breadcrumb.query,
}"
class="text-primary"
>{{
breadcrumb.name.charAt(0) === '?'
? breadcrumbData.getName(breadcrumb.name.slice(1))
: breadcrumb.name
}}
</router-link>
<span
v-else
data-tauri-drag-region
class="text-contrast font-semibold cursor-default select-none"
>{{
breadcrumb.name.charAt(0) === '?'
? breadcrumbData.getName(breadcrumb.name.slice(1))
: breadcrumb.name
}}</span
>
<ChevronRightIcon v-if="breadcrumb.link" data-tauri-drag-region class="w-5 h-5" />
</template>
{{ breadcrumbData.resetToNames(breadcrumbs) }}
<template v-for="breadcrumb in breadcrumbs" :key="breadcrumb.name">
<router-link
v-if="breadcrumb.link"
:to="{
path: breadcrumb.link.replace('{id}', encodeURIComponent($route.params.id as string)),
query: breadcrumb.query,
}"
class="shrink-0 whitespace-nowrap text-primary"
>
{{ resolveLabel(breadcrumb.name) }}
</router-link>
<span
v-else
data-tauri-drag-region
class="shrink-0 whitespace-nowrap text-contrast font-semibold cursor-default select-none"
>
{{ resolveLabel(breadcrumb.name) }}
</span>
<ChevronRightIcon v-if="breadcrumb.link" data-tauri-drag-region class="w-5 h-5 shrink-0" />
</template>
</div>
</div>
</template>

<script setup>
import { ChevronLeftIcon, ChevronRightIcon } from '@modrinth/assets'
import { Button } from '@modrinth/ui'
import { computed } from 'vue'
<script setup lang="ts">
import { ChevronRightIcon } from '@modrinth/assets'
import { computed, onBeforeUnmount, onMounted, ref, watch } from 'vue'
import { useRoute } from 'vue-router'

import { useBreadcrumbs } from '@/store/breadcrumbs'

const route = useRoute()
interface Breadcrumb {
name: string
link?: string
query?: Record<string, string>
}

const route = useRoute()
const breadcrumbData = useBreadcrumbs()
const breadcrumbs = computed(() => {

const breadcrumbs = computed<Breadcrumb[]>(() => {
const additionalContext =
route.meta.useContext === true
? breadcrumbData.context
: route.meta.useRootContext === true
? breadcrumbData.rootContext
: null
return additionalContext ? [additionalContext, ...route.meta.breadcrumb] : route.meta.breadcrumb
const crumbs = (route.meta.breadcrumb ?? []) as Breadcrumb[]
return additionalContext ? [additionalContext as Breadcrumb, ...crumbs] : crumbs
})

function resolveLabel(name: string): string {
return name.charAt(0) === '?' ? breadcrumbData.getName(name.slice(1)) : name
}

// Overflow detection
const outerRef = ref<HTMLDivElement | null>(null)
const innerRef = ref<HTMLDivElement | null>(null)
const isOverflowing = ref(false)
const isAnimating = ref(false)
const overflowAmount = ref(0)

let hovered = false
let stopping = false

function checkOverflow() {
if (!outerRef.value || !innerRef.value) return
const overflow = innerRef.value.scrollWidth - outerRef.value.clientWidth
isOverflowing.value = overflow > 0
overflowAmount.value = overflow + 12
}

function onMouseEnter() {
hovered = true
stopping = false
if (isOverflowing.value) {
isAnimating.value = true
}
}

function onMouseLeave() {
hovered = false
if (isAnimating.value) {
stopping = true
}
}

function onAnimationIteration() {
if (stopping && !hovered) {
isAnimating.value = false
stopping = false
}
}

let resizeObserver: ResizeObserver | null = null

onMounted(() => {
checkOverflow()
resizeObserver = new ResizeObserver(checkOverflow)
if (outerRef.value) resizeObserver.observe(outerRef.value)
if (innerRef.value) resizeObserver.observe(innerRef.value)
})

onBeforeUnmount(() => {
resizeObserver?.disconnect()
})

watch(breadcrumbs, () => {
requestAnimationFrame(checkOverflow)
})
</script>

<style scoped>
.breadcrumbs-scroll {
animation: breadcrumb-scroll 10s ease-in-out infinite;
}

@keyframes breadcrumb-scroll {
0% {
transform: translateX(0);
}
35%,
65% {
transform: translateX(var(--scroll-distance));
}
100% {
transform: translateX(0);
}
}
</style>
Loading
Loading