start new website + layouts lib

This commit is contained in:
Prospector
2026-08-28 12:42:40 -07:00
parent 0ab9100c46
commit 2f77572b22
53 changed files with 5850 additions and 653 deletions
+1 -1
View File
@@ -14,7 +14,7 @@ Read `packages/ui/AGENTS.md` in full.
3. Treat the result as reference code and adapt it to the Modrinth codebase.
4. Map Figma color variables to the applicable `surface-*` and `text-*` tokens. Do not use aliased Figma names directly.
5. Reuse applicable components from `packages/ui/src/components/` before creating new ones. Also refer to `standards/frontend/COMPONENT_STRUCTURE.md`
6. Read `packages/assets/styles/variables.scss` when Figma does not supply a required token.
6. Read `packages/assets/styles/variables.css` when Figma does not supply a required token.
7. Use exact spacing values from the design.
8. Implement the result as a Vue SFC with Tailwind classes and the existing component library.
+2
View File
@@ -35,6 +35,8 @@
"i18n-ally.localesPaths": [
"packages/ui/src/locales",
"apps/frontend/src/locales",
"apps/website/src/locales",
"packages/layouts/src/locales",
"packages/moderation/src/locales"
],
"i18n-ally.pathMatcher": "{locale}/index.{ext}",
@@ -184,7 +184,7 @@ img {
}
}
@import '@modrinth/assets/omorphia.scss';
@import '@modrinth/assets/omorphia.css';
input {
border-radius: var(--size-rounded-sm);
+2 -2
View File
@@ -25,8 +25,8 @@ export default defineConfig({
replacesTitle: true,
},
customCss: [
'@modrinth/assets/styles/variables.scss',
'@modrinth/assets/styles/inter.scss',
'@modrinth/assets/styles/variables.css',
'@modrinth/assets/styles/inter.css',
'./src/styles/modrinth.css',
],
plugins: [
+1 -1
View File
@@ -1,4 +1,4 @@
@import '@modrinth/assets/styles/reset.scss';
@import '@modrinth/assets/styles/reset.css';
html {
--dark-color-text: #b0bac5;
+24
View File
@@ -0,0 +1,24 @@
# Nuxt dev/build outputs
.output
.data
.nuxt
.nitro
.cache
dist
# Node dependencies
node_modules
# Logs
logs
*.log
# Misc
.DS_Store
.fleet
.idea
# Local env files
.env
.env.*
!.env.example
+7
View File
@@ -0,0 +1,7 @@
**/.nuxt
**/dist
**/.output
**/.data
**/.wrangler
src/locales/**
src/assets/**/*.svg
+75
View File
@@ -0,0 +1,75 @@
# Nuxt Minimal Starter
Look at the [Nuxt documentation](https://nuxt.com/docs/getting-started/introduction) to learn more.
## Setup
Make sure to install dependencies:
```bash
# npm
npm install
# pnpm
pnpm install
# yarn
yarn install
# bun
bun install
```
## Development Server
Start the development server on `http://localhost:3000`:
```bash
# npm
npm run dev
# pnpm
pnpm dev
# yarn
yarn dev
# bun
bun run dev
```
## Production
Build the application for production:
```bash
# npm
npm run build
# pnpm
pnpm build
# yarn
yarn build
# bun
bun run build
```
Locally preview production build:
```bash
# npm
npm run preview
# pnpm
pnpm preview
# yarn
yarn preview
# bun
bun run preview
```
Check out the [deployment documentation](https://nuxt.com/docs/getting-started/deployment) for more information.
+9
View File
@@ -0,0 +1,9 @@
import config from '@modrinth/tooling-config/eslint/nuxt.mjs'
export default config.append([
{
rules: {
'import/no-unresolved': 'off',
'no-undef': 'off',
},
},
])
+28
View File
@@ -0,0 +1,28 @@
// https://nuxt.com/docs/api/configuration/nuxt-config
export default defineNuxtConfig({
srcDir: 'src/',
compatibilityDate: '2025-07-15',
devtools: { enabled: true },
devServer: {
port: 42069,
},
app: {
head: {
title: 'Modrinth 2',
htmlAttrs: {
lang: 'en',
},
link: [{ rel: 'icon', type: 'image/x-icon', href: '/favicon.ico' }],
},
},
future: {
compatibilityVersion: 5,
},
css: ['~/assets/styles/styles.css'],
postcss: {
plugins: {
tailwindcss: {},
autoprefixer: {},
},
},
})
+34
View File
@@ -0,0 +1,34 @@
{
"name": "@modrinth/website",
"type": "module",
"private": true,
"scripts": {
"build": "nuxt build",
"dev": "nuxt dev",
"generate": "nuxt generate",
"preview": "nuxt preview",
"postinstall": "nuxt prepare",
"lint": "eslint . && prettier --check .",
"fix": "eslint . --fix && prettier --write .",
"intl:extract": "formatjs extract \"src/**/*.{vue,ts,tsx,js,jsx,mts,cts,mjs,cjs}\" --ignore \"**/*.d.ts\" --ignore node_modules --out-file src/locales/en-US/index.json --format crowdin --preserve-whitespace",
"intl:prune-local": "pnpm -w scripts i18n-icu-contract prune-local --scope apps/website"
},
"dependencies": {
"@modrinth/assets": "workspace:*",
"@modrinth/layouts": "workspace:*",
"@modrinth/ui": "workspace:*",
"intl-messageformat": "^10.7.7",
"lru-cache": "^11.2.4",
"nuxt": "^4.5.2",
"vue": "^3.5.41",
"vue-router": "^5.2.0"
},
"devDependencies": {
"@formatjs/cli": "^6.2.12",
"@modrinth/tooling-config": "workspace:*",
"autoprefixer": "^10.5.4",
"postcss": "^8.5.26",
"tailwindcss": "^3.4.19",
"vite-tsconfig-paths": "^6.1.1"
}
}
+41
View File
@@ -0,0 +1,41 @@
<script setup lang="ts">
import { NuxtModrinthClient } from '@modrinth/api-client'
import { provideModrinthClient, providePageContext } from '@modrinth/ui'
provideModrinthClient(
new NuxtModrinthClient({
labrinthBaseUrl: 'https://api.modrinth.com',
}),
)
providePageContext({
hierarchicalSidebarAvailable: ref(false),
showAds: ref(false),
adConsentAvailable: ref(false),
floatingActionBarOffsets: {
left: ref(0),
right: ref(0),
},
})
</script>
<template>
<div class="page-layout">
<NuxtPage />
</div>
</template>
<style scoped>
.page-layout {
@apply max-w-[min(80rem,100%-1.5rem)] mx-auto h-[100dvh] pt-6;
&:has(#page-sidebar:not(:empty)) {
@apply grid grid-cols-[1fr_300px];
grid-template-areas: 'anything sidebar';
}
}
</style>
<style>
#page-sidebar {
@apply pl-4 border-l border-surface-5;
grid-area: sidebar;
}
</style>
+182
View File
@@ -0,0 +1,182 @@
@import '@modrinth/ui/src/styles/tailwind-utilities.css';
@import '@modrinth/assets/omorphia.css';
@tailwind base;
@tailwind components;
@tailwind utilities;
:root {
font-family: var(--font-standard, sans-serif), sans-serif;
color-scheme: dark;
--view-width: calc(100% - 5rem);
--expanded-view-width: calc(100% - 13rem);
--medal-promotion-bg: #000;
--medal-promotion-bg-orange: rgba(208, 246, 255, 0.25);
--medal-promotion-text-orange: #42abff;
--medal-promotion-bg-gradient: linear-gradient(
90deg,
rgba(66, 170, 255, 0.15),
rgba(0, 0, 0, 0) 100%
);
}
html.light-mode {
color-scheme: light;
}
body {
position: fixed;
width: 100%;
height: 100%;
overflow: hidden;
}
* {
box-sizing: border-box;
}
.card-divider {
background-color: var(--color-button-bg);
border: none;
color: var(--color-button-bg);
height: 1px;
margin: var(--gap-sm) 0;
}
.no-wrap {
white-space: nowrap;
}
.no-select {
-webkit-user-select: none;
-ms-user-select: none;
user-select: none;
}
a {
color: inherit;
text-decoration: none;
-webkit-font-smoothing: antialiased;
will-change: filter;
}
.badge {
display: flex;
border-radius: var(--radius-md);
white-space: nowrap;
align-items: center;
background-color: var(--color-bg);
padding-block: var(--gap-sm);
padding-inline: var(--gap-lg);
width: min-content;
svg {
width: 1.1rem;
height: 1.1rem;
margin-right: 0.5rem;
}
&.featured {
background-color: var(--color-brand-highlight);
color: var(--color-contrast);
}
}
* {
scrollbar-width: auto;
scrollbar-color: var(--color-scrollbar) var(--color-bg);
}
/* Chrome, Edge, and Safari */
*::-webkit-scrollbar {
width: 16px;
border: 3px solid transparent;
opacity: 0.5;
transition: opacity 0.2s ease-in-out;
}
*::-webkit-scrollbar:hover {
opacity: 1;
}
*::-webkit-scrollbar-track {
background: transparent;
}
*::-webkit-scrollbar-thumb {
background-color: var(--color-scrollbar);
border-radius: var(--radius-lg);
border: 5px solid transparent;
background-clip: content-box;
}
.highlighted {
box-shadow: 0 0 1rem var(--color-brand) !important;
}
.gecko {
background-color: var(--color-raised-bg);
box-shadow: none !important;
}
img {
user-select: none;
-webkit-user-select: none;
-moz-user-select: none;
-ms-user-select: none;
}
.card-shadow {
box-shadow: var(--shadow-card);
}
input {
border-radius: var(--size-rounded-sm);
box-sizing: border-box;
border: 2px solid transparent;
/* safari iOS rounds inputs by default. set the appearance to none to prevent this */
appearance: none !important;
}
pre {
font-weight: var(--font-weight-regular);
}
input,
textarea {
background: var(--color-button-bg);
color: var(--color-text);
padding: 0.5rem 1rem;
font-weight: var(--font-weight-medium);
border: none;
outline: 2px solid transparent;
box-shadow:
var(--shadow-inset-sm),
0 0 0 0 transparent;
transition: box-shadow 0.1s ease-in-out;
min-height: 36px;
&:focus,
&:focus-visible {
box-shadow:
inset 0 0 0 transparent,
0 0 0 0.25rem var(--color-brand-shadow);
color: var(--color-button-text-active);
}
&:disabled,
&[disabled='true'] {
opacity: 0.6;
pointer-events: none;
cursor: not-allowed;
}
&:focus::placeholder {
opacity: 0.8;
}
&::placeholder {
color: var(--color-button-text);
opacity: 0.6;
}
}
@@ -0,0 +1,2 @@
{
}
+9
View File
@@ -0,0 +1,9 @@
<script setup lang="ts"></script>
<template>
all the fun stuff:
<ul>
<li>
<NuxtLink to="/user/Prospector">User page</NuxtLink>
</li>
</ul>
</template>
+8
View File
@@ -0,0 +1,8 @@
<script setup lang="ts">
import { UserPage } from '@modrinth/layouts'
const { id } = useRoute().params
</script>
<template>
<UserPage :user-id="id" />
</template>
+237
View File
@@ -0,0 +1,237 @@
import { layoutsLocaleModules } from '@modrinth/layouts'
import {
type CrowdinMessages,
I18N_INJECTION_KEY,
type I18nContext,
LOCALES,
transformCrowdinMessages,
uiLocaleModules,
useDebugLogger,
} from '@modrinth/ui'
import IntlMessageFormat from 'intl-messageformat'
import { LRUCache } from 'lru-cache'
const debug = useDebugLogger('i18n')
const DEFAULT_LOCALE = 'en-US'
const websiteLocaleModules = import.meta.glob<{ default: CrowdinMessages }>(
'../locales/*/index.json',
{ eager: false },
)
const messageCache = new LRUCache<string, Record<string, string>>({ max: 10 })
const formatterCache = new LRUCache<string, IntlMessageFormat>({ max: 1000 })
const loadingPromises = new Map<string, Promise<void>>()
type LocaleModules = Record<string, () => Promise<{ default: CrowdinMessages }>>
function findLocaleLoader(modules: LocaleModules, code: string) {
for (const [path, loader] of Object.entries(modules)) {
if (path.endsWith(`/${code}/index.json`)) {
return loader
}
}
return undefined
}
function formatIcuMessage(msg: string, locale: string, values: Record<string, unknown>) {
const cacheKey = `${locale}:${msg}`
let formatter = formatterCache.get(cacheKey)
try {
if (!formatter) {
formatter = new IntlMessageFormat(msg, locale)
formatterCache.set(cacheKey, formatter)
}
const result = formatter.format(values)
if (import.meta.dev && typeof result !== 'string') {
debug('formatIcuMessage: format returned non-string', typeof result)
}
return result as string
} catch {
return null
}
}
async function loadLocale(code: string): Promise<void> {
if (messageCache.has(code)) {
debug('loadLocale: already cached', code)
return
}
const existing = loadingPromises.get(code)
if (existing) {
debug('loadLocale: already loading', code)
return existing
}
debug('loadLocale: starting', code)
const promise = (async () => {
const websiteLoader = findLocaleLoader(websiteLocaleModules, code)
const uiLoader = findLocaleLoader(uiLocaleModules, code)
const layoutsLoader = findLocaleLoader(layoutsLocaleModules, code)
debug('loadLocale: loaders found', {
code,
website: !!websiteLoader,
ui: !!uiLoader,
layouts: !!layoutsLoader,
})
const [uiData, layoutsData, websiteData] = await Promise.all([
uiLoader?.().catch((e) => {
debug('loadLocale: ui loader failed', code, e)
return null
}),
layoutsLoader?.().catch((e) => {
debug('loadLocale: layouts loader failed', code, e)
return null
}),
websiteLoader?.().catch((e) => {
debug('loadLocale: website loader failed', code, e)
return null
}),
])
debug('loadLocale: data loaded', {
code,
uiKeys: uiData ? Object.keys(uiData.default).length : 0,
layoutsKeys: layoutsData ? Object.keys(layoutsData.default).length : 0,
websiteKeys: websiteData ? Object.keys(websiteData.default).length : 0,
})
const mergedMessages: Record<string, string> = {}
if (uiData) Object.assign(mergedMessages, transformCrowdinMessages(uiData.default))
if (layoutsData) Object.assign(mergedMessages, transformCrowdinMessages(layoutsData.default))
if (websiteData) Object.assign(mergedMessages, transformCrowdinMessages(websiteData.default))
debug('loadLocale: merged', code, 'total keys:', Object.keys(mergedMessages).length)
if (Object.keys(mergedMessages).length > 0) {
messageCache.set(code, mergedMessages)
}
})()
loadingPromises.set(code, promise)
try {
await promise
} finally {
loadingPromises.delete(code)
}
}
function parseAcceptLanguage(header: string): string | null {
try {
for (const lang of header
.split(',')
.map((l) => l.split(';')[0]?.trim())
.filter(Boolean)) {
const exact = LOCALES.find((loc) => loc.code === lang)
if (exact) return exact.code
const prefix = LOCALES.find((loc) => loc.code.startsWith(lang.split('-')[0] + '-'))
if (prefix) return prefix.code
}
} catch {
// Malformed header, ignore
}
return null
}
export default defineNuxtPlugin({
name: 'i18n',
enforce: 'pre',
async setup(nuxtApp) {
const locale = useState<string>('i18n-locale', () => DEFAULT_LOCALE)
const localeDir = computed(
() => LOCALES.find((entry) => entry.code === locale.value)?.dir ?? 'ltr',
)
function t(key: string, values?: Record<string, unknown>): string {
const currentLocale = locale.value
const localeMessages = messageCache.get(currentLocale)
const fallbackMessages = messageCache.get(DEFAULT_LOCALE)
const msg = localeMessages?.[key] ?? fallbackMessages?.[key]
if (!msg) {
debug('t: key not found', {
key,
locale: currentLocale,
hasLocaleMessages: !!localeMessages,
hasFallbackMessages: !!fallbackMessages,
})
return key
}
if (!values || Object.keys(values).length === 0) return msg
const formatted = formatIcuMessage(msg, currentLocale, values)
if (formatted !== null) return formatted
const fallbackMsg = fallbackMessages?.[key]
if (fallbackMsg && fallbackMsg !== msg) {
return formatIcuMessage(fallbackMsg, DEFAULT_LOCALE, values) ?? fallbackMsg
}
return msg
}
async function setLocale(
newLocale: string,
{ persist = true }: { persist?: boolean } = {},
): Promise<void> {
debug('setLocale: called', { newLocale, currentLocale: locale.value })
if (!LOCALES.some((l) => l.code === newLocale)) {
debug('setLocale: invalid locale', newLocale)
return
}
await loadLocale(newLocale)
debug('setLocale: loaded', {
newLocale,
cacheHas: messageCache.has(newLocale),
cacheKeys: messageCache.get(newLocale)
? Object.keys(messageCache.get(newLocale)!).length
: 0,
})
locale.value = newLocale
if (persist) {
useCookie('locale', { maxAge: 31536000, path: '/' }).value = newLocale
}
}
const cookieLocale = useCookie('locale').value
let detectedLocale = DEFAULT_LOCALE
if (cookieLocale && LOCALES.some((l) => l.code === cookieLocale)) {
detectedLocale = cookieLocale
debug('init: locale from cookie', detectedLocale)
} else if (import.meta.server) {
const acceptLang = useRequestHeaders(['accept-language'])['accept-language']
if (acceptLang) {
detectedLocale = parseAcceptLanguage(acceptLang) ?? DEFAULT_LOCALE
debug('init: locale from Accept-Language', detectedLocale)
}
}
debug('init: detected locale', { detectedLocale, cookieLocale, isServer: import.meta.server })
await loadLocale(DEFAULT_LOCALE)
if (detectedLocale !== DEFAULT_LOCALE) await loadLocale(detectedLocale)
locale.value = detectedLocale
debug('init: complete', { locale: locale.value })
useHead({
htmlAttrs: {
lang: locale,
dir: localeDir,
},
})
const context: I18nContext = { locale, t, setLocale }
nuxtApp.vueApp.provide(I18N_INJECTION_KEY, context)
},
})
+38
View File
@@ -0,0 +1,38 @@
// https://tanstack.com/query/v5/docs/framework/vue/examples/nuxt3
import type { DehydratedState, VueQueryPluginOptions } from '@tanstack/vue-query'
import { dehydrate, hydrate, QueryClient, VueQueryPlugin } from '@tanstack/vue-query'
import { defineNuxtPlugin, useState } from '#imports'
export default defineNuxtPlugin((nuxt) => {
const vueQueryState = useState<DehydratedState | null>('vue-query')
const queryClient = new QueryClient({
defaultOptions: {
queries: {
staleTime: 10000,
retry: (failureCount, error) => {
const status = (error as any)?.statusCode ?? (error as any)?.status
if (status !== undefined && status >= 400 && status < 500 && status !== 429) return false
return failureCount < 3
},
},
},
})
const options: VueQueryPluginOptions = { queryClient }
nuxt.vueApp.use(VueQueryPlugin, options)
// Expose queryClient for middleware and composables
nuxt.provide('queryClient', queryClient)
if (import.meta.server) {
nuxt.hooks.hook('app:rendered', () => {
vueQueryState.value = dehydrate(queryClient)
})
}
if (import.meta.client) {
hydrate(queryClient, vueQueryState.value)
}
})
Binary file not shown.

After

Width:  |  Height:  |  Size: 4.2 KiB

+2
View File
@@ -0,0 +1,2 @@
User-Agent: *
Disallow:
+14
View File
@@ -0,0 +1,14 @@
import preset from '@modrinth/tooling-config/tailwind/tailwind-preset.ts'
import type { Config } from 'tailwindcss'
const config: Config = {
content: [
'./src/**/*.{js,vue,ts,mdx}',
'./src/app.vue',
'../../packages/**/*.{js,vue,ts}',
'!../../packages/**/node_modules/**',
],
presets: [preset],
}
export default config
+18
View File
@@ -0,0 +1,18 @@
{
// https://nuxt.com/docs/guide/concepts/typescript
"files": [],
"references": [
{
"path": "./.nuxt/tsconfig.app.json"
},
{
"path": "./.nuxt/tsconfig.server.json"
},
{
"path": "./.nuxt/tsconfig.shared.json"
},
{
"path": "./.nuxt/tsconfig.node.json"
}
]
}
+10
View File
@@ -12,6 +12,11 @@ files:
translation: /apps/frontend/src/locales/%locale%/%original_file_name%
skip_untranslated_strings: true
# - source: /apps/website/src/locales/en-US/*.json
# dest: /website/%original_file_name%
# translation: /apps/website/src/locales/%locale%/%original_file_name%
# skip_untranslated_strings: true
- source: /packages/ui/src/locales/en-US/*.json
dest: /ui/%original_file_name%
translation: /packages/ui/src/locales/%locale%/%original_file_name%
@@ -22,5 +27,10 @@ files:
translation: /packages/moderation/src/locales/%locale%/%original_file_name%
skip_untranslated_strings: true
- source: /packages/layouts/src/locales/en-US/*.json
dest: /layouts/%original_file_name%
translation: /packages/layouts/src/locales/%locale%/%original_file_name%
skip_untranslated_strings: true
project_id_env: CROWDIN_PROJECT_ID
api_token_env: CROWDIN_PERSONAL_TOKEN
+76 -75
View File
@@ -1,77 +1,78 @@
{
"name": "@modrinth/monorepo",
"version": "0.0.0",
"private": true,
"scripts": {
"web:dev": "pnpm i18n:coverage && turbo run dev --filter=@modrinth/frontend",
"web:build": "turbo run build --filter=@modrinth/frontend",
"app:dev": "turbo run dev --filter=@modrinth/app",
"app:build": "turbo run build --filter=@modrinth/app",
"docs:dev": "turbo run dev --filter=@modrinth/docs",
"pages:build": "pnpm i18n:coverage && NITRO_PRESET=cloudflare-pages pnpm --filter frontend run build",
"build": "turbo run build --continue",
"lint": "turbo run lint lint:ancillary --continue",
"lint:ancillary": "prettier --check .github *.*",
"test": "turbo run test --continue",
"fix": "turbo run fix fix:ancillary --continue",
"fix:ancillary": "prettier --write .github *.*",
"ci": "turbo run lint test --continue",
"prepr": "turbo run prepr --continue",
"prepr:frontend": "turbo run prepr --filter=@modrinth/frontend --filter=@modrinth/app-frontend",
"prepr:frontend:lib": "turbo run prepr --filter=@modrinth/ui --filter=@modrinth/moderation --filter=@modrinth/assets --filter=@modrinth/blog --filter=@modrinth/api-client --filter=@modrinth/utils --filter=@modrinth/tooling-config",
"prepr:frontend:web": "turbo run prepr --filter=@modrinth/frontend",
"prepr:frontend:app": "turbo run prepr --filter=@modrinth/app-frontend",
"storybook": "pnpm i18n:coverage && pnpm --filter @modrinth/ui storybook",
"build-storybook": "pnpm i18n:coverage && pnpm --filter @modrinth/ui build-storybook",
"icons:add": "pnpm --filter @modrinth/assets icons:add",
"i18n:coverage": "node scripts/run.mjs coverage-i18n --write packages/ui/src/layouts/wrapped/settings/language-settings/language-settings-coverage.generated.ts --quiet",
"changelog:collect": "node scripts/run.mjs collect-changelog",
"changelog:combine-for-app": "node scripts/run.mjs build-theseus-release-notes",
"scripts": "node scripts/run.mjs"
},
"devDependencies": {
"@clack/prompts": "^1.0.0",
"@crowdin/crowdin-api-client": "^1.55.2",
"@formatjs/icu-messageformat-parser": "^2.11.4",
"@modrinth/tooling-config": "workspace:*",
"@tailwindcss/container-queries": "^0.1.1",
"@types/node": "^24",
"@typescript-eslint/typescript-estree": "^8.64.0",
"@vue/compiler-dom": "^3.5.26",
"@vue/compiler-sfc": "^3.5.26",
"chalk": "^5.6.2",
"if-ci": "^3.0.0",
"keep-a-changelog": "^3.0.2",
"prettier": "^3.3.2",
"turbo": "^2.5.4",
"vue": "^3.5.13",
"yaml": "^2.8.2"
},
"packageManager": "pnpm@10.33.2",
"engines": {
"node": ">=24.15.0"
},
"pnpm": {
"patchedDependencies": {
"readable-stream@2.3.8": "patches/readable-stream@2.3.8.patch"
},
"peerDependencyRules": {
"allowedVersions": {
"vite": "8",
"esbuild": "0"
}
},
"onlyBuiltDependencies": [
"@parcel/watcher",
"@sentry/cli",
"core-js",
"esbuild",
"protobufjs",
"sharp",
"unrs-resolver",
"vue-demi",
"workerd"
]
},
"prettier": "@modrinth/tooling-config/prettier.config.cjs"
"name": "@modrinth/monorepo",
"version": "0.0.0",
"private": true,
"scripts": {
"new:dev": "turbo run dev --filter=@modrinth/website",
"web:dev": "pnpm i18n:coverage && turbo run dev --filter=@modrinth/frontend",
"web:build": "turbo run build --filter=@modrinth/frontend",
"app:dev": "turbo run dev --filter=@modrinth/app",
"app:build": "turbo run build --filter=@modrinth/app",
"docs:dev": "turbo run dev --filter=@modrinth/docs",
"pages:build": "pnpm i18n:coverage && NITRO_PRESET=cloudflare-pages pnpm --filter frontend run build",
"build": "turbo run build --continue",
"lint": "turbo run lint lint:ancillary --continue",
"lint:ancillary": "prettier --check .github *.*",
"test": "turbo run test --continue",
"fix": "turbo run fix fix:ancillary --continue",
"fix:ancillary": "prettier --write .github *.*",
"ci": "turbo run lint test --continue",
"prepr": "turbo run prepr --continue",
"prepr:frontend": "turbo run prepr --filter=@modrinth/frontend --filter=@modrinth/app-frontend --filter=@modrinth/website",
"prepr:frontend:lib": "turbo run prepr --filter=@modrinth/ui --filter=@modrinth/moderation --filter=@modrinth/layouts --filter=@modrinth/assets --filter=@modrinth/blog --filter=@modrinth/api-client --filter=@modrinth/utils --filter=@modrinth/tooling-config",
"prepr:frontend:web": "turbo run prepr --filter=@modrinth/frontend --filter=@modrinth/website",
"prepr:frontend:app": "turbo run prepr --filter=@modrinth/app-frontend",
"storybook": "pnpm i18n:coverage && pnpm --filter @modrinth/ui storybook",
"build-storybook": "pnpm i18n:coverage && pnpm --filter @modrinth/ui build-storybook",
"icons:add": "pnpm --filter @modrinth/assets icons:add",
"i18n:coverage": "node scripts/run.mjs coverage-i18n --write packages/ui/src/layouts/wrapped/settings/language-settings/language-settings-coverage.generated.ts --quiet",
"changelog:collect": "node scripts/run.mjs collect-changelog",
"changelog:combine-for-app": "node scripts/run.mjs build-theseus-release-notes",
"scripts": "node scripts/run.mjs"
},
"devDependencies": {
"@clack/prompts": "^1.0.0",
"@crowdin/crowdin-api-client": "^1.55.2",
"@formatjs/icu-messageformat-parser": "^2.11.4",
"@modrinth/tooling-config": "workspace:*",
"@tailwindcss/container-queries": "^0.1.1",
"@types/node": "^24",
"@typescript-eslint/typescript-estree": "^8.64.0",
"@vue/compiler-dom": "^3.5.26",
"@vue/compiler-sfc": "^3.5.26",
"chalk": "^5.6.2",
"if-ci": "^3.0.0",
"keep-a-changelog": "^3.0.2",
"prettier": "^3.3.2",
"turbo": "^2.5.4",
"vue": "^3.5.13",
"yaml": "^2.8.2"
},
"packageManager": "pnpm@10.33.2",
"engines": {
"node": ">=24.15.0"
},
"pnpm": {
"patchedDependencies": {
"readable-stream@2.3.8": "patches/readable-stream@2.3.8.patch"
},
"peerDependencyRules": {
"allowedVersions": {
"vite": "8",
"esbuild": "0"
}
},
"onlyBuiltDependencies": [
"@parcel/watcher",
"@sentry/cli",
"core-js",
"esbuild",
"protobufjs",
"sharp",
"unrs-resolver",
"vue-demi",
"workerd"
]
},
"prettier": "@modrinth/tooling-config/prettier.config.cjs"
}
+1 -1
View File
@@ -6,7 +6,7 @@
* and automatically generated through the "pnpm run fix" command.
*/
import './omorphia.scss'
import './omorphia.css'
import _FourOhFourNotFound from './branding/404.svg?component'
// Branding
+18
View File
@@ -0,0 +1,18 @@
/* Create the variables used by everything */
@import './styles/variables.css';
/* Normalize styling across browsers to a standard stripped down form */
@import './styles/normalize.css';
/* Add Omorphia base classes */
@import './styles/classes.css';
/* Apply default styles */
@import './styles/defaults.css';
/* Apply code block highlighting styles */
@import './styles/highlightjs.css';
/* Finally, apply accessibility-related global styling */
@import './styles/accessibility.css';
/* Fonts */
@import './styles/inter.css';
-18
View File
@@ -1,18 +0,0 @@
// Create the variables used by everything
@import 'styles/variables';
// Normalize styling across browsers to a standard stripped down form
@import 'styles/normalize';
// Add Omorphia base classes
@import 'styles/classes';
// Apply default styles
@import 'styles/defaults';
// Apply code block highlighting styles
@import 'styles/highlightjs';
// Finally, apply accessibility-related global styling
@import 'styles/accessibility';
// Fonts
@import 'styles/inter';
@@ -1,13 +1,15 @@
// Bodies + Generic Global Styles
/* Bodies + Generic Global Styles */
.universal-labels {
.universal-labels,
.universal-body,
.card {
label,
.label {
:where(.label__title) {
display: block;
margin-block: var(--gap-md) var(--gap-sm);
// Same styling as h3
/* Same styling as h3 */
color: var(--color-contrast);
font-size: 1.17rem;
font-weight: bold;
@@ -38,9 +40,8 @@
}
}
.universal-body {
@extend .universal-labels;
.universal-body,
.card {
> :where(input + *, .input-group + *, .chips + *, .input-div + *) {
margin-block-start: var(--gap-md);
}
@@ -101,7 +102,7 @@
}
@media screen and (max-width: 750px) {
&:not(&.small) {
&:not(.small) {
flex-direction: column;
align-items: start;
}
@@ -187,28 +188,30 @@
}
}
// CLICKABLES/BUTTONS
/* CLICKABLES/BUTTONS */
a:not(.no-click-animation),
.clickable {
.clickable,
.button-base,
.btn {
transition:
opacity 0.5s ease-in-out,
filter 0.2s ease-in-out,
scale 0.05s ease-in-out,
outline 0.2s ease-in-out;
&:active:not(&:disabled) {
&:active:not(:disabled) {
scale: 0.95;
}
}
.button-base {
@extend .clickable;
.button-base,
.btn {
font-weight: 500;
border: none;
outline: 2px solid transparent;
&:focus-visible:not(&:disabled),
&:hover:not(&:disabled) {
&:focus-visible:not(:disabled),
&:hover:not(:disabled) {
cursor: pointer;
&:not(.btn-outline.btn-hover-filled, .btn-transparent) {
@@ -216,7 +219,7 @@ a:not(.no-click-animation),
}
}
&:active:not(&:disabled, .btn-outline.btn-hover-filled, .btn-transparent) {
&:active:not(:disabled, .btn-outline.btn-hover-filled, .btn-transparent) {
filter: brightness(0.8);
}
@@ -240,17 +243,17 @@ a:not(.no-click-animation),
transform 0.05s ease-in-out,
outline 0.2s ease-in-out;
&:focus-visible:not(&.disabled),
&:hover:not(&.disabled) {
&:focus-visible:not(.disabled),
&:hover:not(.disabled) {
filter: brightness(0.85);
}
&:active:not(&.disabled) {
&:active:not(.disabled) {
filter: brightness(0.8);
}
// For some reason this within the above block makes it universal and not only applied to children. SCSS bug maybe?
&:active:not(&.disabled) button:not(&:disabled) {
/* For some reason this within the above block makes it universal and not only applied to children. SCSS bug maybe? */
&:active:not(.disabled) button:not(:disabled) {
transform: scale(0.95);
}
@@ -269,8 +272,6 @@ a:not(.no-click-animation),
}
.btn {
@extend .button-base;
--_text-color: var(--color-base);
--_background-color: var(--color-button-bg);
--_accent-color: var(--color-base);
@@ -296,8 +297,8 @@ a:not(.no-click-animation),
color: var(--color-base);
}
&:focus-visible:not(&:disabled),
&:hover:not(&:disabled) {
&:focus-visible:not(:disabled),
&:hover:not(:disabled) {
background-color: var(--color-button-bg);
&.btn-hover-filled,
@@ -319,8 +320,8 @@ a:not(.no-click-animation),
border-color: var(--color-contrast);
}
&:focus-visible:not(&:disabled),
&:hover:not(&:disabled) {
&:focus-visible:not(:disabled),
&:hover:not(:disabled) {
&.btn-hover-filled,
&.btn-hover-filled-only {
border-color: var(--_accent-color);
@@ -493,8 +494,10 @@ a:not(.no-click-animation),
}
}
// CARDS
.base-card {
/* CARDS */
.base-card,
.card,
.navigation-card {
padding: 1rem;
position: relative;
min-height: var(--font-size-2xl);
@@ -517,7 +520,7 @@ a:not(.no-click-animation),
z-index: 2;
}
&:where(&.warning, &.information) {
&:where(.warning, .information) {
padding: 1.5rem;
line-height: 1.5;
min-height: 0;
@@ -529,14 +532,7 @@ a:not(.no-click-animation),
}
}
.card {
@extend .base-card;
@extend .universal-body;
}
.navigation-card {
@extend .base-card;
padding-inline: var(--gap-xl);
padding-block: var(--gap-md);
@@ -547,7 +543,7 @@ a:not(.no-click-animation),
row-gap: 0.5rem;
}
// UTILITY
/* UTILITY */
.wrap-as-needed {
overflow-wrap: break-word;
@@ -572,7 +568,7 @@ a:not(.no-click-animation),
margin: var(--gap-sm) 0;
}
// TABLE
/* TABLE */
.table {
display: grid;
grid-template-rows: repeat(auto-fill, auto);
@@ -634,10 +630,10 @@ a:not(.no-click-animation),
}
}
// CUSTOM COMPONENTS
// TODO: MOST OF THESE SHOULD BE MOVED TO AN OMORPHIA COMPONENT
/* CUSTOM COMPONENTS */
/* TODO: MOST OF THESE SHOULD BE MOVED TO AN OMORPHIA COMPONENT */
// TOOLTIPS
/* TOOLTIPS */
.v-popper__popper.v-popper--theme-dropdown,
.v-popper__popper.v-popper--theme-dropdown.v-popper--theme-ribbit-popout {
@@ -690,7 +686,7 @@ a:not(.no-click-animation),
}
.v-popper__popper.v-popper__popper--hide-to .v-popper__wrapper {
//transform: scale(.9);
/* transform: scale(.9); */
}
.v-popper__popper.v-popper--theme-tooltip {
@@ -817,7 +813,7 @@ a:not(.no-click-animation),
}
}
// MARKDOWN
/* MARKDOWN */
.markdown-body {
h1:first-child {
@@ -862,7 +858,8 @@ a:not(.no-click-animation),
}
h4 {
@apply mt-5 mb-3;
margin-top: 1.25rem;
margin-bottom: 0.75rem;
}
h1,
@@ -912,21 +909,25 @@ a:not(.no-click-animation),
height: auto;
}
html:not(.dark-mode, .oled-mode) & img[src$='#gh-dark-mode-only'] {
html:not(.dark-mode, .oled-mode, .retro-mode) & img[src$='#gh-dark-mode-only'] {
display: none;
}
html:is(.dark-mode, .oled-mode) & img[src$='#gh-light-mode-only'] {
html:is(.dark-mode, .oled-mode, .retro-mode) & img[src$='#gh-light-mode-only'] {
display: none;
}
pre {
@apply bg-surface-2 rounded-xl p-4 border border-solid border-surface-5;
background-color: var(--surface-2);
border-radius: 0.75rem;
padding: 1rem;
border: 1px solid var(--surface-5);
margin-top: 1rem;
overflow-x: auto;
code {
@apply bg-transparent border-0;
background-color: transparent;
border-width: 0;
font-size: 80%;
padding: 0;
border-radius: 0;
@@ -934,10 +935,10 @@ a:not(.no-click-animation),
}
code {
@apply bg-surface-3 rounded-md p-4 border border-solid border-surface-5;
padding: 0.2em 0.4em;
font-size: 80%;
border-radius: var(--radius-sm);
border: 1px solid var(--surface-5);
background-color: var(--color-button-bg);
color: var(--color-contrast);
}
@@ -988,19 +989,32 @@ a:not(.no-click-animation),
}
details {
@apply border border-solid border-surface-5 bg-surface-3 rounded-xl p-2;
border: 1px solid var(--surface-5);
background-color: var(--surface-3);
border-radius: 0.75rem;
padding: 0.5rem;
display: grid;
grid-template-rows: min-content 0fr;
transition: grid-template-rows 0.3s var(--ease-out-expo);
overflow: clip;
summary {
@apply bg-surface-4 hover:bg-surface-5 p-2 flex items-center gap-1 text-contrast -m-2 border border-solid border-transparent;
padding: 0.5rem;
display: flex;
align-items: center;
gap: 0.25rem;
color: var(--color-text-primary);
margin: -0.5rem;
border: 1px solid transparent;
font-weight: 600;
cursor: pointer;
background-color: var(--color-button-bg);
list-style: none;
&:hover {
background-color: var(--surface-5);
}
&::-webkit-details-marker {
display: none;
}
@@ -1010,7 +1024,7 @@ a:not(.no-click-animation),
}
&::before {
@apply text-primary;
color: var(--color-text-default);
content: '';
flex-shrink: 0;
width: 1.25rem;
@@ -1025,15 +1039,15 @@ a:not(.no-click-animation),
}
> :nth-child(2) {
@apply mt-5;
margin-top: 1.25rem;
}
> :nth-child(n + 2) {
@apply mx-1;
margin-inline: 0.25rem;
}
> :last-child:not(summary) {
@apply mb-1;
margin-bottom: 0.25rem;
}
&::details-content {
@@ -1047,7 +1061,7 @@ a:not(.no-click-animation),
grid-template-rows: min-content 1fr;
> summary {
@apply border-b-surface-5;
border-bottom-color: var(--surface-5);
&::before {
transform: rotate(180deg);
@@ -1094,7 +1108,7 @@ select {
-moz-border-radius: 3px;
-webkit-border-radius: 3px;
border-radius: 3px;
background: #cccccc url(http://dropdown/arrow/url/) no-repeat right center;
background: #cccccc url('http://dropdown/arrow/url/') no-repeat right center;
color: #000000;
border: 1px solid #000000;
}
@@ -1,12 +1,12 @@
@import 'reset';
@import './reset.css';
// Use border box on everything to preserve everyone's sanity
/* Use border box on everything to preserve everyone's sanity */
* {
box-sizing: border-box;
}
body {
// Defaults
/* Defaults */
background-color: var(--color-bg);
color: var(--color-base);
--font-standard:
@@ -19,18 +19,18 @@ body {
margin: 0;
padding: 0;
// Font Sizes
--font-size-xxs: 0.625rem; //10px
--font-size-xs: 0.75rem; //12px
--font-size-sm: 0.875rem; //14px
--font-size-nm: 1rem; //16px
--font-size-md: 1.125rem; //18px
--font-size-lg: 1.25rem; //20px
--font-size-xl: 1.5rem; //24px
--font-size-2xl: 2rem; //32px
--font-size-3xl: 3rem; //48px
/* Font Sizes */
--font-size-xxs: 0.625rem; /* 10px */
--font-size-xs: 0.75rem; /* 12px */
--font-size-sm: 0.875rem; /* 14px */
--font-size-nm: 1rem; /* 16px */
--font-size-md: 1.125rem; /* 18px */
--font-size-lg: 1.25rem; /* 20px */
--font-size-xl: 1.5rem; /* 24px */
--font-size-2xl: 2rem; /* 32px */
--font-size-3xl: 3rem; /* 48px */
// Font Weights
/* Font Weights */
--font-weight-regular: 400;
--font-weight-medium: 500;
--font-weight-bold: 700;
@@ -101,7 +101,7 @@ svg {
transform 0.05s ease-in-out,
outline 0.2s ease-in-out;
&:active:not(&:disabled) {
&:active:not(:disabled) {
transform: scale(0.95);
}
}
@@ -135,7 +135,7 @@ h3 {
color: var(--color-contrast);
}
// Scrollbar styles
/* Scrollbar styles */
::-webkit-scrollbar {
width: 0.75rem;
height: 0.75rem;
@@ -149,7 +149,7 @@ h3 {
background: var(--color-scrollbar);
}
// Firefox scrollbar
/* Firefox scrollbar */
* {
scrollbar-width: thin;
scrollbar-color: var(--color-scrollbar) transparent;
@@ -9,3 +9,7 @@ input[type='button'] {
cursor: pointer;
outline: 2px solid transparent;
}
:where(*) {
border: 0 solid currentColor;
}
@@ -1,4 +1,6 @@
.light-properties {
html,
.light-mode,
.light {
--surface-1: #ebebeb;
--surface-1-5: #ededed;
--surface-2: #f5f5f5;
@@ -89,7 +91,7 @@
--color-text-default: #2c2e31;
--color-text-tertiary: #484d54;
// ===
/* === */
--color-bg: var(--surface-1);
--color-raised-bg: var(--surface-3);
@@ -188,7 +190,6 @@
}
html {
@extend .light-properties;
--dark-color-base: #b0bac5;
--dark-color-contrast: #ecf9fb;
@@ -214,22 +215,19 @@ html {
--color-ad-highlight: var(--color-purple);
--color-link: var(--color-blue) !important;
--color-link-hover: var(--color-blue) !important; // DEPRECATED, use filters in future
--color-link-active: var(--color-blue) !important; // DEPRECATED, use filters in future
--color-link-hover: var(--color-blue) !important; /* DEPRECATED, use filters in future */
--color-link-active: var(--color-blue) !important; /* DEPRECATED, use filters in future */
--shadow-button: 0 1px 3px 0 rgba(0, 0, 0, 0.05), 0 1px 2px 0 rgba(0, 0, 0, 0.15);
--color-focus-ring: #ea80ff;
}
.light-mode,
.light {
@extend .light-properties;
}
.dark-mode,
.dark,
:root[data-theme='dark'] {
:root[data-theme='dark'],
.oled-mode,
.retro-mode {
--surface-1: #16181c;
--surface-1-5: #1a1c20;
--surface-2: #1d1f23;
@@ -320,7 +318,7 @@ html {
--color-text-default: #b0bac5;
--color-text-tertiary: #96a2b0;
// ===
/* === */
--color-bg: var(--surface-1);
--color-raised-bg: var(--surface-3);
@@ -408,7 +406,6 @@ html {
}
.oled-mode {
@extend .dark-mode;
--surface-1: #000000;
--surface-1-5: #050506;
--surface-2: #09090a;
@@ -434,7 +431,6 @@ html {
}
.retro-mode {
@extend .dark-mode;
--surface-1: #191917;
--surface-2: rgb(22, 22, 21);
--surface-2-5: #3a3c3e;
+2
View File
@@ -0,0 +1,2 @@
src/locales/**
storybook-static/**
+20
View File
@@ -0,0 +1,20 @@
# Copying
The source code of Modrinth's Layouts library is licensed under the GNU General Public License, Version 3 only, which is provided in the file [LICENSE](./LICENSE). However, some files listed below are licensed under a different license.
## Modrinth logo
The use of Modrinth branding elements, including but not limited to the wrench-in-labyrinth logo, the landing image, and any variations thereof, is strictly prohibited without explicit written permission from Rinth, Inc. This includes trademarks, logos, or other branding elements.
> All rights reserved. © 2020-2025 Rinth, Inc.
This includes, but may not be limited to, the following files:
- src/components/brand/\*
- src/components/servers/ModrinthServersIcon.vue
## External logos
The following files are owned by their respective copyright holders and must be used within each of their Brand Guidelines:
- src/components/servers/MedalBackgroundImage.vue
+2
View File
@@ -0,0 +1,2 @@
import config from '@modrinth/tooling-config/eslint/nuxt.mjs'
export default config
+2
View File
@@ -0,0 +1,2 @@
export * from './src/locales'
export * from './src/pages'
+101
View File
@@ -0,0 +1,101 @@
{
"name": "@modrinth/layouts",
"version": "0.0.0",
"type": "module",
"private": true,
"main": "./index.ts",
"types": "./index.ts",
"imports": {
"#layouts/*": "./src/*"
},
"exports": {
".": {
"types": "./index.ts",
"default": "./index.ts"
},
"./pages": {
"types": "./src/pages/index.ts",
"default": "./src/pages/index.ts"
},
"./src/*": "./src/*"
},
"scripts": {
"lint": "eslint . && prettier --check .",
"fix": "eslint . --fix && prettier --write .",
"intl:extract": "formatjs extract \"src/**/*.{vue,ts,tsx,js,jsx,mts,cts,mjs,cjs}\" --ignore \"src/**/*.d.ts\" --out-file src/locales/en-US/index.json --preserve-whitespace",
"intl:prune-local": "pnpm -w scripts i18n-icu-contract prune-local --scope packages/layouts"
},
"devDependencies": {
"@formatjs/cli": "^6.2.12",
"@modrinth/tooling-config": "workspace:*",
"@storybook/addon-a11y": "^10.1.10",
"@storybook/addon-themes": "^10.1.10",
"@storybook/builder-vite": "^10.1.10",
"@storybook/vue3-vite": "^10.1.10",
"@stripe/stripe-js": "^7.3.1",
"@tailwindcss/vite": "^4.1.18",
"@vitejs/plugin-vue": "^5.2.1",
"eslint-plugin-storybook": "^10.1.10",
"storybook": "^10.1.10",
"stripe": "^18.1.1",
"tailwindcss": "^3.4.4",
"typescript": "^5.4.5",
"vite": "^5.4.6",
"vite-svg-loader": "^5.1.0",
"vue": "^3.5.13",
"vue-component-type-helpers": "^3.1.8",
"vue-router": "^4.6.0",
"wrangler": "^4.65.0"
},
"dependencies": {
"@codemirror/commands": "^6.3.2",
"@codemirror/lang-markdown": "^6.2.3",
"@codemirror/language": "^6.9.3",
"@codemirror/state": "^6.3.2",
"@codemirror/view": "^6.22.1",
"@intercom/messenger-js-sdk": "^0.0.14",
"@modrinth/api-client": "workspace:*",
"@modrinth/assets": "workspace:*",
"@modrinth/blog": "workspace:*",
"@modrinth/utils": "workspace:*",
"@modrinth/ui": "workspace:*",
"@tanstack/vue-query": "5.90.7",
"@tresjs/cientos": "^4.3.0",
"@tresjs/core": "^4.3.4",
"@tresjs/post-processing": "^2.4.0",
"@types/dompurify": "^3.0.5",
"@types/markdown-it": "^14.1.1",
"@types/three": "^0.172.0",
"@vintl/how-ago": "^3.0.1",
"@vueuse/core": "^11.1.0",
"@xterm/addon-fit": "^0.11.0",
"@xterm/addon-search": "^0.16.0",
"@xterm/xterm": "^6.0.0",
"ace-builds": "^1.43.5",
"apexcharts": "^4.0.0",
"dayjs": "^1.11.10",
"dompurify": "^3.1.7",
"es-toolkit": "^1.44.0",
"flatpickr": "^4.6.13",
"fabric": "^7.4.0",
"floating-vue": "^5.2.2",
"fuse.js": "^6.6.2",
"highlight.js": "^11.9.0",
"intl-messageformat": "^10.7.7",
"jszip": "^3.10.1",
"lru-cache": "^11.2.4",
"markdown-it": "^13.0.2",
"motion-v": "^2.2.1",
"overlayscrollbars": "^2.15.1",
"postprocessing": "^6.37.6",
"qrcode.vue": "^3.4.1",
"three": "^0.172.0",
"vue-i18n": "^10.0.0",
"vue-select": "4.0.0-beta.6",
"vue-typed-virtual-list": "^1.0.10",
"vue3-ace-editor": "^2.2.4",
"vue3-apexcharts": "^1.5.2",
"xss": "^1.0.14"
},
"web-types": "../../web-types.json"
}
@@ -0,0 +1,15 @@
<script setup lang="ts">
import { injectPageContext } from '@modrinth/ui'
const { hierarchicalSidebarAvailable } = injectPageContext()
</script>
<template>
<Teleport v-if="hierarchicalSidebarAvailable" to="#sidebar">
<aside id="page-sidebar">
<slot />
</aside>
</Teleport>
<aside v-else id="page-sidebar">
<slot />
</aside>
</template>
@@ -0,0 +1,6 @@
<script setup lang="ts"></script>
<template>
<div>
<slot />
</div>
</template>
+6
View File
@@ -0,0 +1,6 @@
import type { CrowdinMessages } from '@modrinth/ui'
export const layoutsLocaleModules = import.meta.glob<{ default: CrowdinMessages }>(
'./locales/*/index.json',
{ eager: false },
)
@@ -0,0 +1,2 @@
{
}
+1
View File
@@ -0,0 +1 @@
export { default as UserPage } from './user'
+28
View File
@@ -0,0 +1,28 @@
<script setup lang="ts">
import { injectModrinthClient } from '@modrinth/ui'
import { useQuery } from '@tanstack/vue-query'
import Sidebar from '../../components/Sidebar.vue'
import SidebarCard from '../../components/SidebarCard.vue'
const props = defineProps<{
userId: string
}>()
const api = injectModrinthClient()
const { data: user } = useQuery({
queryKey: ['user', props.userId],
queryFn: () => api.labrinth.users_v3.get(props.userId),
enabled: !!props.userId,
})
</script>
<template>
<div class="flex flex-col gap-2">
<img :src="user?.avatar_url" class="size-10 rounded-full" />
<div>Hello user {{ user?.username }}!</div>
</div>
<Sidebar>
<SidebarCard> Sidebar stuff!</SidebarCard>
<SidebarCard> the stuff in the sidebar goes here</SidebarCard>
</Sidebar>
</template>
+9
View File
@@ -0,0 +1,9 @@
import preset from '@modrinth/tooling-config/tailwind/tailwind-preset.ts'
import type { Config } from 'tailwindcss'
const config: Config = {
content: ['./src/**/*.{js,vue,ts,mdx}', './.storybook/**/*.{ts,js}'],
presets: [preset],
}
export default config
+1 -1
View File
@@ -1,5 +1,5 @@
import 'floating-vue/dist/style.css'
import '../../assets/styles/defaults.scss'
import '../../assets/styles/defaults.css'
// frontend css imports
// import '../../../apps/frontend/src/assets/styles/global.scss'
// import '../../../apps/frontend/src/assets/styles/tailwind.css'
+2 -2
View File
@@ -38,7 +38,7 @@ All frontend packages share a Tailwind preset at `packages/tooling-config/tailwi
import preset from '@modrinth/tooling-config/tailwind/tailwind-preset.ts'
```
CSS custom properties are defined in `packages/assets/styles/variables.scss` with light, dark, and OLED theme variants.
CSS custom properties are defined in `packages/assets/styles/variables.css` with light, dark, and OLED theme variants.
### Color Usage Rules
@@ -61,7 +61,7 @@ CSS custom properties are defined in `packages/assets/styles/variables.scss` wit
| `text-primary` | Default body text |
| `text-secondary` | Reduced emphasis, secondary info |
**Brand and semantic colors** not all exposed as Figma variables — refer to `packages/assets/styles/variables.scss` for the full set:
**Brand and semantic colors** not all exposed as Figma variables — refer to `packages/assets/styles/variables.css` for the full set:
- `bg-{color}`, `text-{color}` etc. — Primary brand colors
- `bg-{color}-highlight` — 25% opacity semantic highlights
@@ -111,24 +111,6 @@ function getPreviewClass(option: T): string {
grid-template-columns: repeat(auto-fit, minmax(12rem, 1fr));
gap: var(--gap-lg);
.preview {
&.light-mode {
@extend .light-mode;
}
&.dark-mode {
@extend .dark-mode;
}
&.oled-mode {
@extend .oled-mode;
}
&.retro-mode {
@extend .retro-mode;
}
}
.preview .example-card {
margin: 0;
padding: 1rem;
+4693 -443
View File
File diff suppressed because it is too large Load Diff
+16 -2
View File
@@ -69,8 +69,20 @@ const PRODUCT_SCOPES: Record<
catalogScopes: ['apps/app-frontend', 'packages/ui'],
},
website: {
sourceDirectories: ['apps/frontend/src', 'packages/moderation/src', 'packages/ui/src'],
catalogScopes: ['packages/ui', 'packages/moderation', 'apps/frontend'],
sourceDirectories: [
'apps/frontend/src',
// 'apps/website/src',
'packages/layouts/src',
'packages/moderation/src',
'packages/ui/src',
],
catalogScopes: [
'packages/ui',
'packages/moderation',
'packages/layouts',
'apps/frontend',
// 'apps/website',
],
},
}
@@ -765,8 +777,10 @@ function main() {
const scanDirs = [
'apps/frontend/src',
'apps/app-frontend/src',
// 'apps/website/src',
'packages/ui/src',
'packages/moderation/src',
'packages/layouts/src',
]
if (!jsonOutput && !quiet) {
+2
View File
@@ -85,6 +85,8 @@
"apps/frontend/src/locales/**/*.json",
"packages/moderation/src/**/*.vue",
"packages/moderation/src/locales/**/*.json",
"packages/layouts/src/**/*.vue",
"packages/layouts/src/locales/**/*.json",
"packages/ui/src/**/*.vue",
"packages/ui/src/locales/**/*.json"
],