diff --git a/apps/frontend/AGENTS.md b/apps/frontend/AGENTS.md index 12568876752..b677a79f9b4 100644 --- a/apps/frontend/AGENTS.md +++ b/apps/frontend/AGENTS.md @@ -28,6 +28,12 @@ Nuxt 3 with SSR — pages are server-rendered and hydrated on the client. Uses ` Rule of thumb: if it doesn't depend on Nuxt-specific APIs or website-only features, it should be in `packages/ui`. +### Project Review Popout Windows + +Project review panels can be displayed in multiple browser documents while sharing the main window's Vue application and JavaScript context. Use an anchor, panel, or event target's `ownerDocument` for DOM queries and its `defaultView` for window behavior. Global `document` and `window` still identify the main window after a panel moves. + +Use the review shortcut context's listeners (`shortcuts.listen` or `useReviewEventListener`) for review interactions that must work in both the workspace and popouts. Shared overlay and modal behavior follows the multiple-document guidance in `packages/ui/AGENTS.md`. + ## Data Fetching Use `@modrinth/api-client` via `injectModrinthClient()` for all API calls. See `packages/api-client/AGENTS.md` for the full API client documentation. diff --git a/apps/frontend/COPYING.md b/apps/frontend/COPYING.md index 9a54fca3a2e..07c2bc471b1 100644 --- a/apps/frontend/COPYING.md +++ b/apps/frontend/COPYING.md @@ -21,6 +21,8 @@ This includes, but may not be limited to, the following files: - src/public/favicon-light-32x32.png - src/public/favicon-settings-32x32.png - src/public/favicon-light-settings-32x32.png +- src/public/favicon-review.svg +- src/public/favicon-light-review.svg - src/public/dev-favicons/\* - src/public/news/\* diff --git a/apps/frontend/nuxt.config.ts b/apps/frontend/nuxt.config.ts index 844a272baed..d8fff056805 100644 --- a/apps/frontend/nuxt.config.ts +++ b/apps/frontend/nuxt.config.ts @@ -5,6 +5,9 @@ import { fileURLToPath } from 'url' import svgLoader from 'vite-svg-loader' import { GenericModrinthClient, type Labrinth } from '../../packages/api-client/src/index.ts' +import moderationMessages, { + moderationMessagesDirectory, +} from '../../packages/moderation/vite-plugin' const STAGING_API_URL = 'https://staging-api.modrinth.com/v2/' const STAGING_SHARED_INSTANCES_API_URL = 'https://staging-shared-instances.modrinth.com' @@ -26,6 +29,7 @@ export default defineNuxtConfig({ srcDir: 'src/', alias: { '@modrinth/api-client': API_CLIENT_SOURCE, + '#messages': moderationMessagesDirectory, }, app: { buildAssetsDir: '/_assets/', @@ -82,6 +86,7 @@ export default defineNuxtConfig({ dedupe: ['vue'], }, plugins: [ + moderationMessages(), svgLoader({ svgoConfig: { plugins: [ diff --git a/apps/frontend/package.json b/apps/frontend/package.json index 21a4c43a171..6254af7eeb2 100644 --- a/apps/frontend/package.json +++ b/apps/frontend/package.json @@ -38,6 +38,9 @@ "wrangler": "^4.115.0" }, "dependencies": { + "@codemirror/commands": "^6.3.2", + "@codemirror/language": "^6.9.3", + "@codemirror/view": "^6.22.1", "@contentauth/c2pa-web": "^0.13.1", "@floating-ui/vue": "^2.0.1", "@formatjs/intl-localematcher": "^0.5.4", @@ -59,6 +62,7 @@ "ansi-to-html": "^0.7.2", "chart.js": "^4.5.1", "dayjs": "^1.11.7", + "dockview-vue": "^8.2.0", "dompurify": "^3.1.7", "fuse.js": "^6.6.2", "highlight.js": "^11.7.0", diff --git a/apps/frontend/src/assets/styles/layout.scss b/apps/frontend/src/assets/styles/layout.scss index d633ed8df98..83debb8ff82 100644 --- a/apps/frontend/src/assets/styles/layout.scss +++ b/apps/frontend/src/assets/styles/layout.scss @@ -111,18 +111,6 @@ 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 { @@ -152,18 +140,6 @@ 'sidebar' / 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) { &.sidebar { grid-template: diff --git a/apps/frontend/src/components/ProjectStatusStats.vue b/apps/frontend/src/components/ProjectStatusStats.vue new file mode 100644 index 00000000000..27bf7c5313c --- /dev/null +++ b/apps/frontend/src/components/ProjectStatusStats.vue @@ -0,0 +1,117 @@ + + + diff --git a/apps/frontend/src/components/project-review/description/index.vue b/apps/frontend/src/components/project-review/description/index.vue new file mode 100644 index 00000000000..192ee524bce --- /dev/null +++ b/apps/frontend/src/components/project-review/description/index.vue @@ -0,0 +1,102 @@ + + + diff --git a/apps/frontend/src/components/project-review/disclosures/index.vue b/apps/frontend/src/components/project-review/disclosures/index.vue new file mode 100644 index 00000000000..8498b47625f --- /dev/null +++ b/apps/frontend/src/components/project-review/disclosures/index.vue @@ -0,0 +1,70 @@ + + + diff --git a/apps/frontend/src/components/project-review/gallery/index.vue b/apps/frontend/src/components/project-review/gallery/index.vue new file mode 100644 index 00000000000..f54ca4af977 --- /dev/null +++ b/apps/frontend/src/components/project-review/gallery/index.vue @@ -0,0 +1,120 @@ + + + + + diff --git a/apps/frontend/src/components/project-review/image-viewer-actions.vue b/apps/frontend/src/components/project-review/image-viewer-actions.vue new file mode 100644 index 00000000000..a8ba9128f05 --- /dev/null +++ b/apps/frontend/src/components/project-review/image-viewer-actions.vue @@ -0,0 +1,87 @@ + + + diff --git a/apps/frontend/src/components/project-review/index.vue b/apps/frontend/src/components/project-review/index.vue new file mode 100644 index 00000000000..d3042dc401a --- /dev/null +++ b/apps/frontend/src/components/project-review/index.vue @@ -0,0 +1,367 @@ + + + diff --git a/apps/frontend/src/components/project-review/issue-list/index.vue b/apps/frontend/src/components/project-review/issue-list/index.vue new file mode 100644 index 00000000000..4e62bc601ae --- /dev/null +++ b/apps/frontend/src/components/project-review/issue-list/index.vue @@ -0,0 +1,137 @@ + + + diff --git a/apps/frontend/src/components/project-review/issue-list/issue-card.vue b/apps/frontend/src/components/project-review/issue-list/issue-card.vue new file mode 100644 index 00000000000..3e935a113e9 --- /dev/null +++ b/apps/frontend/src/components/project-review/issue-list/issue-card.vue @@ -0,0 +1,639 @@ + + + + + diff --git a/apps/frontend/src/components/project-review/issue-list/issue-picker.vue b/apps/frontend/src/components/project-review/issue-list/issue-picker.vue new file mode 100644 index 00000000000..ac93989ab87 --- /dev/null +++ b/apps/frontend/src/components/project-review/issue-list/issue-picker.vue @@ -0,0 +1,149 @@ + + + diff --git a/apps/frontend/src/components/project-review/issue-list/issue-toggles.vue b/apps/frontend/src/components/project-review/issue-list/issue-toggles.vue new file mode 100644 index 00000000000..b80446379ca --- /dev/null +++ b/apps/frontend/src/components/project-review/issue-list/issue-toggles.vue @@ -0,0 +1,260 @@ + + + diff --git a/apps/frontend/src/components/project-review/issue-list/re-review-issues.ts b/apps/frontend/src/components/project-review/issue-list/re-review-issues.ts new file mode 100644 index 00000000000..4dc72d7261f --- /dev/null +++ b/apps/frontend/src/components/project-review/issue-list/re-review-issues.ts @@ -0,0 +1,56 @@ +import { injectNotificationManager } from '@modrinth/ui' +import { type Ref, ref } from 'vue' + +import type { ProjectReviewPageContext } from '~/providers/project-review' +import type { createReviewMessages } from '~/providers/project-review/review-messages' +import type { createReviewSession } from '~/providers/project-review/review-session' + +export function useReReviewIssues( + { project, threadQuery }: Pick, + session: ReturnType, + messages: ReturnType, + pending: Ref, +) { + const { handleError } = injectNotificationManager() + const resetting = ref(false) + + async function resetIssues() { + const current = project.value + if (!current || pending.value || resetting.value) return + resetting.value = true + try { + if (current.thread_id) { + const result = await threadQuery.refetch({ throwOnError: true }) + if (result.data?.id !== current.thread_id) return + } + if (project.value?.id !== current.id || pending.value) return + for (const scope of [ + 'issue-active', + 'issues', + 'issue-text', + 'issue-select', + 'issue-order', + 'custom-issues', + 'custom-issue-counter', + 'previous-issue-applicability', + 'previous-facet-applicability', + 'previous-issue-selection', + 'previous-issue-copy', + ]) { + for (const id of Object.keys(session.read(current.id, scope))) { + session.write(current.id, scope, id, undefined) + if (scope !== 'previous-issue-applicability') messages.resetIssueMessage(id) + } + } + for (const issue of threadQuery.data.value?.issues ?? []) { + messages.resetIssueMessage(`previous:${issue.id}`) + } + } catch (error) { + handleError(error) + } finally { + resetting.value = false + } + } + + return { resetting, resetIssues } +} diff --git a/apps/frontend/src/components/project-review/issue-list/stories/IssueCatalogueRow.vue b/apps/frontend/src/components/project-review/issue-list/stories/IssueCatalogueRow.vue new file mode 100644 index 00000000000..1ae20d9c97c --- /dev/null +++ b/apps/frontend/src/components/project-review/issue-list/stories/IssueCatalogueRow.vue @@ -0,0 +1,245 @@ + + +