Add new links card and feature flag system for incremental dev. (#1714)

* Add new links card and feature flag system for incremental dev.

* Switch to env variable for dev flags

* Add members card

* fix order of creators card

* Fix owner icon color and bring org owner to top of list

* lint + other fixes

* Revamp feature flag system, add flag config page

* Add button to flags page in dev mode

* fix env overrides

* make typescript happy with the refs
This commit is contained in:
Prospector 2024-06-11 19:46:07 -07:00 committed by GitHub
parent 5b2d36e976
commit 1d9fe0c03d
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
32 changed files with 1325 additions and 310 deletions

View File

@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="lucide lucide-book-text"><path d="M4 19.5v-15A2.5 2.5 0 0 1 6.5 2H20v20H6.5a2.5 2.5 0 0 1 0-5H20"/><path d="M8 7h6"/><path d="M8 11h8"/></svg>

After

Width:  |  Height:  |  Size: 330 B

View File

@ -3,7 +3,7 @@
*/
.known-error .multiselect__tags {
border-color: var(--color-special-red) !important;
border-color: var(--color-red) !important;
background-color: var(--color-warning-bg) !important;
&::placeholder {
@ -104,7 +104,7 @@
font-weight: bold;
.required {
color: var(--color-special-red);
color: var(--color-red);
}
&.size-card-header {
@ -285,6 +285,10 @@
}
> :first-child {
:first-child {
margin-block-start: 0;
}
margin-block-start: 0;
}
@ -476,7 +480,7 @@
}
&.danger-button {
color: var(--color-special-red);
color: var(--color-red);
}
}
@ -636,12 +640,12 @@ tr.button-transparent {
}
.danger-button {
--background-color: var(--color-special-red);
--background-color: var(--color-red);
--text-color: var(--color-brand-inverted);
}
.moderation-button {
--background-color: var(--color-special-orange);
--background-color: var(--color-orange);
--text-color: var(--color-brand-inverted);
}
@ -670,7 +674,7 @@ tr.button-transparent {
}
.known-error .multiselect__tags {
border-color: var(--color-special-red) !important;
border-color: var(--color-red) !important;
background-color: var(--color-warning-bg) !important;
&::placeholder {
@ -913,7 +917,7 @@ tr.button-transparent {
.text-input-wrapper.known-error,
input.known-error,
textarea.known-error {
outline: 2px solid var(--color-special-red);
outline: 2px solid var(--color-red);
background-color: var(--color-warning-bg) !important;
&::placeholder {
@ -923,7 +927,7 @@ textarea.known-error {
.known-errors {
min-height: 0;
color: var(--color-special-red);
color: var(--color-red);
ul {
margin: 0;
@ -1269,3 +1273,179 @@ a.subtle-link {
svg.inline-svg {
vertical-align: middle;
}
// START STUFF FOR OMORPHIA
.experimental-styles-within {
.tag-list {
display: flex;
flex-wrap: wrap;
gap: var(--gap-4);
}
.tag-list__item {
background-color: var(--color-button-bg);
padding: var(--gap-2) var(--gap-6);
border-radius: var(--radius-max);
font-weight: var(--weight-bold);
font-size: var(--text-14);
display: flex;
gap: var(--gap-4);
align-items: center;
vertical-align: middle;
color: var(--_color, var(--color-secondary));
svg {
width: var(--icon-14);
height: var(--icon-14);
display: flex;
}
}
.status-list {
display: flex;
flex-direction: column;
gap: var(--gap-8);
padding-left: var(--gap-6);
color: var(--color-base);
font-weight: var(--weight-bold);
}
.status-list__item {
display: flex;
align-items: center;
gap: var(--gap-4);
svg {
width: var(--icon-16);
height: var(--icon-16);
margin-right: var(--gap-4);
}
span {
color: var(--color-secondary);
font-style: italic;
font-weight: var(--weight-normal);
}
}
.status-list__item--color-green svg {
color: var(--color-green);
}
.status-list__item--color-orange svg {
color: var(--color-orange);
}
.status-list__item--color-red svg {
color: var(--color-red);
}
.status-list__item--color-blue svg {
color: var(--color-blue);
}
.status-list__item--color-purple svg {
color: var(--color-purple);
}
&.flex-card,
.flex-card {
display: flex;
flex-direction: column;
gap: var(--gap-12);
padding: var(--gap-16) var(--gap-24);
h2 {
font-size: var(--text-18);
font-weight: var(--weight-extrabold);
color: var(--color-contrast);
margin: 0;
}
h3 {
font-size: var(--text-16);
font-weight: var(--weight-bold);
color: var(--color-base);
margin: 0;
}
> section {
display: flex;
flex-direction: column;
gap: var(--gap-8);
}
}
.list-style {
display: flex;
flex-direction: column;
gap: var(--gap-12);
font-weight: var(--weight-bold);
hr {
width: 100%;
border-color: var(--color-button-border);
margin-block: var(--gap-2);
}
}
.iconified-list-item {
display: flex;
gap: var(--gap-8);
vertical-align: middle;
align-items: center;
width: fit-content;
svg {
width: var(--icon-16);
height: var(--icon-16);
}
}
.links-list {
@extend .list-style;
> a {
@extend .iconified-list-item;
&:hover {
text-decoration: underline;
}
}
}
.details-list {
@extend .list-style;
}
.details-list__item {
@extend .iconified-list-item;
.details-list__item__text--style-secondary {
color: var(--color-secondary);
font-weight: var(--weight-normal);
font-size: var(--text-14);
}
}
.icon {
--_size: 1rem;
width: var(--_size, var(--icon-16)) !important;
height: var(--_size, var(--icon-16)) !important;
border: 1px solid var(--color-button-border);
&[data-size='32'] {
--_size: var(--icon-32);
}
&[data-shape='circle'] {
border-radius: var(--radius-max) !important;
}
&[data-shape='square'] {
border-radius: calc(2.25 * (var(--_size) / 16)) !important;
}
}
}

View File

@ -5,8 +5,130 @@ html {
--color-text-secondary: var(--color-icon);
}
// TO BE MOVED TO OMORPHIA
:root {
--gap-2: 0.125rem;
--gap-4: calc(2 * var(--gap-2));
--gap-6: calc(3 * var(--gap-2));
--gap-8: calc(2 * var(--gap-4));
--gap-12: calc(3 * var(--gap-4));
--gap-16: calc(2 * var(--gap-8));
--gap-24: calc(3 * var(--gap-8));
--radius-card: 1rem;
--radius-max: 999999999px;
--radius-btn: 0.75rem;
--radius-btn-large: 1rem;
--radius-btn-circle: var(--radius-max);
--text-14: 0.875rem;
--text-16: 1rem;
--text-18: 1.125rem;
--text-24: 1.5rem;
--text-32: 2rem;
--weight-normal: 500; // used for general body text
--weight-bold: 600; // used for text needing extra emphasis
--weight-extrabold: 800; // used for main titles and headings
--icon-14: 0.875rem; // used for icons inside a small container alongside a label
--icon-16: 1rem; // used for normal-sized icons alongside a label
--icon-20: 1.25rem; // used for icons in normal sized buttons
--icon-24: 1.5rem; // used for icons that are used as a primary label or in large buttons
--icon-32: 2rem;
}
.experimental-styles-within {
// Reset deprecated properties
--color-icon: initial !important;
--color-text: initial !important;
--color-text-inactive: initial !important;
--color-text-dark: initial !important;
--color-heading: initial !important;
--color-divider: initial !important;
--color-divider-dark: initial !important;
--color-text-inverted: initial !important;
--color-bg-inverted: initial !important;
--color-brand-green: initial !important;
--color-brand: var(--color-green) !important;
--color-brand-inverted: initial !important;
--tab-underline-hovered: initial !important;
--color-button-text: initial !important;
--color-button-bg-hover: initial !important;
--color-button-text-hover: initial !important;
--color-button-bg-active: initial !important;
--color-button-text-active: initial !important;
--color-grey-link: inherit !important;
--color-grey-link-hover: inherit !important; // DEPRECATED, use filters in future
--color-grey-link-active: inherit !important; // DEPRECATED, use filters in future
--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
}
.light-mode,
.light {
.experimental-styles-within,
&.experimental-styles-within {
--color-bg: #ebebeb;
--color-raised-bg: #ffffff;
--color-button-bg: #f5f5f5;
--color-base: #2c2e31;
--color-secondary: #484d54;
--color-accent-contrast: #ffffff;
--color-platform-fabric: #8a7b71;
--color-platform-quilt: #8b61b4;
--color-platform-forge: #5b6197;
--color-platform-neoforge: #dc895c;
--color-platform-liteloader: #4c90de;
--color-platform-bukkit: #e78362;
--color-platform-bungeecord: #c69e39;
--color-platform-folia: #6aa54f;
--color-platform-paper: #e67e7e;
--color-platform-purpur: #7763a3;
--color-platform-spigot: #cd7a21;
--color-platform-velocity: #4b98b0;
--color-platform-waterfall: #5f83cb;
--color-platform-sponge: #c49528;
--color-button-border: rgba(161, 161, 161, 0.35);
}
}
.dark-mode,
.dark {
.experimental-styles-within,
&.experimental-styles-within {
--color-button-bg: #33363d;
--color-platform-fabric: #dbb69b;
--color-platform-quilt: #c796f9;
--color-platform-forge: #959eef;
--color-platform-neoforge: #f99e6b;
--color-platform-liteloader: #7ab0ee;
--color-platform-bukkit: #f6af7b;
--color-platform-bungeecord: #d2c080;
--color-platform-folia: #a5e388;
--color-platform-paper: #eeaaaa;
--color-platform-purpur: #c3abf7;
--color-platform-spigot: #f1cc84;
--color-platform-velocity: #83d5ef;
--color-platform-waterfall: #78a4fb;
--color-platform-sponge: #f9e580;
--color-button-border: rgba(193, 190, 209, 0.12);
}
}
.light-mode {
--color-icon: #6b7280;
--color-secondary: #6b7280;
--color-icon: var(--color-secondary);
--color-text: hsl(221, 39%, 11%);
--color-text-inactive: hsl(215, 14%, 34%);
--color-text-dark: #1a202c;
@ -59,13 +181,6 @@ html {
--color-link-hover: #1a76e7;
--color-link-active: #146fd7;
--color-special-red: #cb2245;
--color-special-orange: #e08325;
--color-special-green: var(--color-brand-green);
--color-special-blue: #1f68c0;
--color-special-purple: #8e32f3;
--color-special-gray: #595b61;
--color-red-bg: rgba(204, 35, 69, 0.1);
--color-warning-bg: hsl(355, 70%, 88%);
@ -77,7 +192,7 @@ html {
--color-info-banner-text: var(--color-text);
--color-info-banner-bg: var(--color-ad);
--color-info-banner-side: var(--color-special-blue);
--color-info-banner-side: var(--color-blue);
--color-block-quote: var(--color-tooltip-bg);
--color-header-underline: var(--color-divider-dark);
@ -140,8 +255,12 @@ html {
--landing-raw-bg: #fff;
}
.dark-mode {
--color-icon: #96a2b0;
.dark,
.dark-mode,
.oled-mode,
.retro-mode {
--color-secondary: #96a2b0;
--color-icon: var(--color-secondary);
--color-text: var(--dark-color-text);
--color-text-inactive: #929aa3;
--color-text-dark: var(--dark-color-text-dark);
@ -154,13 +273,6 @@ html {
--color-text-inverted: var(--color-bg);
--color-bg-inverted: var(--color-text);
--color-special-red: #ff496e;
--color-special-orange: #ffa347;
--color-special-green: var(--color-brand-green);
--color-special-blue: #4f9cff;
--color-special-purple: #c78aff;
--color-special-gray: #9fa4b3;
--color-red-bg: rgba(255, 74, 110, 0.2);
--color-brand-green: #1bd96a;
@ -209,7 +321,7 @@ html {
--color-info-banner-text: var(--color-text);
--color-info-banner-bg: var(--color-ad);
--color-info-banner-side: var(--color-special-blue);
--color-info-banner-side: var(--color-blue);
--color-block-quote: var(--color-code-bg);
--color-header-underline: var(--color-divider-dark);
@ -274,7 +386,6 @@ html {
}
.oled-mode {
@extend .dark-mode;
--color-bg: #000000;
--color-raised-bg: #101013;

View File

@ -65,7 +65,7 @@ defineProps({
.badge {
font-weight: bold;
width: fit-content;
--badge-color: var(--color-special-gray);
--badge-color: var(--color-gray);
color: var(--badge-color);
white-space: nowrap;
@ -88,7 +88,7 @@ defineProps({
&.type--withheld,
&.type--rejected,
&.red {
--badge-color: var(--color-special-red);
--badge-color: var(--color-red);
}
&.type--pending,
@ -96,7 +96,7 @@ defineProps({
&.type--processing,
&.type--scheduled,
&.orange {
--badge-color: var(--color-special-orange);
--badge-color: var(--color-orange);
}
&.type--accepted,
@ -104,23 +104,23 @@ defineProps({
&.type--success,
&.type--approved-general,
&.green {
--badge-color: var(--color-special-green);
--badge-color: var(--color-green);
}
&.type--creator,
&.type--approved,
&.blue {
--badge-color: var(--color-special-blue);
--badge-color: var(--color-blue);
}
&.type--unlisted,
&.purple {
--badge-color: var(--color-special-purple);
--badge-color: var(--color-purple);
}
&.type--private,
&.gray {
--badge-color: var(--color-special-gray);
--badge-color: var(--color-gray);
}
}
</style>

View File

@ -249,7 +249,7 @@
>
<CheckIcon /> Mark as read
</button>
<CopyCode v-if="cosmetics.developerMode" :text="notification.id" />
<CopyCode v-if="flags.developerMode" :text="notification.id" />
</div>
<div v-else class="input-group">
<nuxt-link
@ -281,7 +281,7 @@
>
<CheckIcon /> Mark as read
</button>
<CopyCode v-if="cosmetics.developerMode" :text="notification.id" />
<CopyCode v-if="flags.developerMode" :text="notification.id" />
</div>
</div>
</div>
@ -335,7 +335,7 @@ const props = defineProps({
},
})
const cosmetics = useCosmetics()
const flags = useFeatureFlags()
const tags = useTags()
const type = computed(() =>
@ -549,7 +549,7 @@ function getMessages() {
}
.unknown-type {
color: var(--color-special-red);
color: var(--color-red);
}
.title-link {
@ -560,11 +560,11 @@ function getMessages() {
}
.moderation-color {
color: var(--color-special-orange);
color: var(--color-orange);
}
.creator-color {
color: var(--color-special-blue);
color: var(--color-blue);
}
}
</style>

View File

@ -26,8 +26,8 @@ function stopTimer(notif) {
</script>
<style lang="scss" scoped>
.vue-notification {
background: var(--color-special-blue) !important;
border-left: 5px solid var(--color-special-blue) !important;
background: var(--color-blue) !important;
border-left: 5px solid var(--color-blue) !important;
color: var(--color-brand-inverted) !important;
box-sizing: border-box;
@ -37,18 +37,18 @@ function stopTimer(notif) {
margin: 0 5px 5px;
&.success {
background: var(--color-special-green) !important;
border-left-color: var(--color-special-green) !important;
background: var(--color-green) !important;
border-left-color: var(--color-green) !important;
}
&.warn {
background: var(--color-special-orange) !important;
border-left-color: var(--color-special-orange) !important;
background: var(--color-orange) !important;
border-left-color: var(--color-orange) !important;
}
&.error {
background: var(--color-special-red) !important;
border-left-color: var(--color-special-red) !important;
background: var(--color-red) !important;
border-left-color: var(--color-red) !important;
}
}

View File

@ -414,7 +414,7 @@ export default {
svg {
width: auto;
color: var(--color-special-orange);
color: var(--color-orange);
height: 1.5rem;
margin-bottom: -0.25rem;
}

View File

@ -428,15 +428,15 @@ const submitForReview = async () => {
align-items: center;
.required {
color: var(--color-special-red);
color: var(--color-red);
}
.suggestion {
color: var(--color-special-purple);
color: var(--color-purple);
}
.review {
color: var(--color-special-orange);
color: var(--color-orange);
}
}
@ -467,7 +467,7 @@ const submitForReview = async () => {
.circle {
--circle-size: 2rem;
--background-color: var(--color-bg);
--content-color: var(--color-special-gray);
--content-color: var(--color-gray);
width: var(--circle-size);
height: var(--circle-size);
border-radius: 50%;
@ -483,19 +483,19 @@ const submitForReview = async () => {
}
&.required {
--content-color: var(--color-special-red);
--content-color: var(--color-red);
}
&.suggestion {
--content-color: var(--color-special-purple);
--content-color: var(--color-purple);
}
&.review {
--content-color: var(--color-special-orange);
--content-color: var(--color-orange);
}
&.done {
--background-color: var(--color-special-green);
--background-color: var(--color-green);
--content-color: var(--color-brand-inverted);
}
}

View File

@ -507,7 +507,7 @@ const defaultRanges: Record<number, [string, number] | string> = {
display: flex;
flex-direction: row;
justify-content: space-between;
align-items: start;
align-items: flex-start;
gap: var(--gap-md);
.chart-controls__buttons {

View File

@ -82,7 +82,7 @@
<span v-tooltip="$dayjs(report.created).format('MMMM D, YYYY [at] h:mm A')">{{
fromNow(report.created)
}}</span>
<CopyCode v-if="cosmetics.developerMode" :text="report.id" class="report-id" />
<CopyCode v-if="flags.developerMode" :text="report.id" class="report-id" />
</div>
</div>
</template>
@ -124,7 +124,7 @@ defineProps({
},
})
const cosmetics = useCosmetics()
const flags = useFeatureFlags()
</script>
<style lang="scss" scoped>

View File

@ -33,7 +33,7 @@
</div>
</div>
</Modal>
<div v-if="cosmetics.developerMode" class="thread-id">
<div v-if="flags.developerMode" class="thread-id">
Thread ID: <CopyCode :text="thread.id" />
</div>
<div v-if="sortedMessages.length > 0" class="messages universal-card recessed">
@ -247,7 +247,7 @@ const props = defineProps({
const emit = defineEmits(['update-thread'])
const app = useNuxtApp()
const cosmetics = useCosmetics()
const flags = useFeatureFlags()
const members = computed(() => {
const members = {}

View File

@ -289,7 +289,7 @@ a:active + .message__author a,
.moderation-color,
role-moderator {
color: var(--color-special-orange);
color: var(--color-orange);
}
.role-admin {
@ -297,11 +297,11 @@ role-moderator {
}
.reporter-icon {
color: var(--color-special-purple);
color: var(--color-purple);
}
.private-icon {
color: var(--color-special-gray);
color: var(--color-gray);
}
@media screen and (min-width: 600px) {

View File

@ -14,7 +14,6 @@ export const useCosmetics = () =>
projectLayout: false,
advancedRendering: true,
externalLinksNewTab: true,
developerMode: false,
notUsingBlockers: false,
hideModrinthAppPromos: false,
preferredDarkTheme: 'dark',
@ -38,6 +37,9 @@ export const useCosmetics = () =>
export const saveCosmetics = () => {
const cosmetics = useCosmetics()
console.log('SAVING COSMETICS:')
console.log(cosmetics)
const cosmeticsCookie = useCookie('cosmetics', {
maxAge: 60 * 60 * 24 * 365 * 10,
sameSite: 'lax',

105
composables/featureFlags.ts Normal file
View File

@ -0,0 +1,105 @@
import { CookieOptions } from '#app'
export type ProjectDisplayMode = 'list' | 'grid' | 'gallery'
export type DarkColorTheme = 'dark' | 'oled' | 'retro'
export interface NumberFlag {
min: number
max: number
}
export type BooleanFlag = boolean
export type RadioFlag = ProjectDisplayMode | DarkColorTheme
export type FlagValue = BooleanFlag /* | NumberFlag | RadioFlag */
const validateValues = <K extends PropertyKey>(flags: Record<K, FlagValue>) => flags
export const DEFAULT_FEATURE_FLAGS = validateValues({
// Developer flags
developerMode: false,
// In-development features, flags will be removed over time
newProjectLinks: true,
newProjectMembers: false,
newProjectDetails: true,
projectCompatibility: false,
removeFeaturedVersions: false,
// Alt layouts
// searchSidebarRight: false,
// projectSidebarRight: false,
// Feature toggles
// advancedRendering: true,
// externalLinksNewTab: true,
// notUsingBlockers: false,
// hideModrinthAppPromos: false,
// preferredDarkTheme: 'dark',
// hideStagingBanner: false,
// Project display modes
// modSearchDisplayMode: 'list',
// pluginSearchDisplayMode: 'list',
// resourcePackSearchDisplayMode: 'gallery',
// modpackSearchDisplayMode: 'list',
// shaderSearchDisplayMode: 'gallery',
// dataPackSearchDisplayMode: 'list',
// userProjectDisplayMode: 'list',
// collectionProjectDisplayMode: 'list',
} as const)
export type FeatureFlag = keyof typeof DEFAULT_FEATURE_FLAGS
export type AllFeatureFlags = {
[key in FeatureFlag]: (typeof DEFAULT_FEATURE_FLAGS)[key]
}
export type PartialFeatureFlags = Partial<AllFeatureFlags>
const COOKIE_OPTIONS: CookieOptions<PartialFeatureFlags> = {
maxAge: 60 * 60 * 24 * 365 * 10,
sameSite: 'lax',
secure: true,
httpOnly: false,
path: '/',
}
export const useFeatureFlags = () =>
useState<AllFeatureFlags>('featureFlags', () => {
const config = useRuntimeConfig()
const savedFlags = useCookie<PartialFeatureFlags>('featureFlags', COOKIE_OPTIONS)
if (!savedFlags.value) {
savedFlags.value = {}
}
const flags: AllFeatureFlags = JSON.parse(JSON.stringify(DEFAULT_FEATURE_FLAGS))
const overrides = config.public.featureFlagOverrides as PartialFeatureFlags
for (const key in overrides) {
if (key in flags) {
const flag = key as FeatureFlag
const value = overrides[flag] as (typeof flags)[FeatureFlag]
flags[flag] = value
}
}
for (const key in savedFlags.value) {
if (key in flags) {
const flag = key as FeatureFlag
const value = savedFlags.value[flag] as (typeof flags)[FeatureFlag]
flags[flag] = value
}
}
return flags
})
export const saveFeatureFlags = () => {
const flags = useFeatureFlags()
const cookie = useCookie<PartialFeatureFlags>('featureFlags', COOKIE_OPTIONS)
cookie.value = flags.value
}

View File

@ -81,3 +81,172 @@ export const PRIVATE_PROJECT_STATUSES = ['private', 'rejected', 'processing']
export const REJECTED_PROJECT_STATUSES = ['rejected', 'withheld']
export const UNDER_REVIEW_PROJECT_STATUSES = ['processing']
export const DRAFT_PROJECT_STATUSES = ['draft']
export function getVersionsToDisplay(project, overrideTags) {
const tags = overrideTags ?? useTags().value
const projectVersions = project.game_versions.slice()
const allVersions = tags.gameVersions.slice()
const allSnapshots = allVersions.filter((version) => version.version_type === 'snapshot')
const allReleases = allVersions.filter((version) => version.version_type === 'release')
const allLegacy = allVersions.filter(
(version) => version.version_type !== 'snapshot' && version.version_type !== 'release'
)
{
const indices = allVersions.reduce((map, gameVersion, index) => {
map[gameVersion.version] = index
return map
}, {})
projectVersions.sort((a, b) => indices[a] - indices[b])
}
const releaseVersions = projectVersions.filter((projVer) =>
allReleases.some((gameVer) => gameVer.version === projVer)
)
const latestReleaseVersionDate = Date.parse(
allReleases.find((version) => version.version === releaseVersions[0])?.date
)
const latestSnapshot = projectVersions.find((projVer) =>
allSnapshots.some(
(gameVer) =>
gameVer.version === projVer &&
(!latestReleaseVersionDate || latestReleaseVersionDate < Date.parse(gameVer.date))
)
)
const allReleasesGrouped = groupVersions(
allReleases.map((release) => release.version),
false
)
const projectVersionsGrouped = groupVersions(releaseVersions, true)
const releaseVersionsAsRanges = projectVersionsGrouped.map(({ major, minor }) => {
if (minor.length === 1) {
return formatVersion(major, minor[0])
}
if (
allReleasesGrouped
.find((x) => x.major === major)
.minor.every((value, index) => value === minor[index])
) {
return `${major}.x`
}
return `${formatVersion(major, minor[0])}${formatVersion(major, minor[minor.length - 1])}`
})
const legacyVersionsAsRanges = groupConsecutiveIndices(
projectVersions.filter((projVer) => allLegacy.some((gameVer) => gameVer.version === projVer)),
allLegacy
)
let output = [...legacyVersionsAsRanges]
// show all snapshots if there's no release versions
if (releaseVersionsAsRanges.length === 0) {
const snapshotVersionsAsRanges = groupConsecutiveIndices(
projectVersions.filter((projVer) =>
allSnapshots.some((gameVer) => gameVer.version === projVer)
),
allSnapshots
)
output = [...snapshotVersionsAsRanges, ...output]
} else {
output = [...releaseVersionsAsRanges, ...output]
}
if (latestSnapshot) {
output = [latestSnapshot, ...output]
}
return output
}
const mcVersionRegex = /^([0-9]+.[0-9]+)(.[0-9]+)?$/
function groupVersions(versions, consecutive = false) {
return versions
.slice()
.reverse()
.reduce((ranges, version) => {
const matchesVersion = version.match(mcVersionRegex)
if (matchesVersion) {
const majorVersion = matchesVersion[1]
const minorVersion = matchesVersion[2]
const minorNumeric = minorVersion ? parseInt(minorVersion.replace('.', '')) : 0
let prevInRange
if (
(prevInRange = ranges.find(
(x) => x.major === majorVersion && (!consecutive || x.minor.at(-1) === minorNumeric - 1)
))
) {
prevInRange.minor.push(minorNumeric)
return ranges
}
return [...ranges, { major: majorVersion, minor: [minorNumeric] }]
}
return ranges
}, [])
.reverse()
}
function groupConsecutiveIndices(versions, referenceList) {
if (!versions || versions.length === 0) {
return []
}
const referenceMap = new Map()
referenceList.forEach((item, index) => {
referenceMap.set(item.version, index)
})
const sortedList = versions.slice().sort((a, b) => referenceMap.get(a) - referenceMap.get(b))
const ranges = []
let start = sortedList[0]
let previous = sortedList[0]
for (let i = 1; i < sortedList.length; i++) {
const current = sortedList[i]
if (referenceMap.get(current) !== referenceMap.get(previous) + 1) {
ranges.push(validateRange(`${previous}${start}`))
start = current
}
previous = current
}
ranges.push(validateRange(`${previous}${start}`))
return ranges
}
function validateRange(range) {
switch (range) {
case 'rd-132211b1.8.1':
return 'All legacy versions'
case 'a1.0.4b1.8.1':
return 'All alpha and beta versions'
case 'a1.0.4a1.2.6':
return 'All alpha versions'
case 'b1.0b1.8.1':
return 'All beta versions'
case 'rd-132211inf20100618':
return 'All pre-alpha versions'
}
const splitRange = range.split('')
if (splitRange && splitRange[0] === splitRange[1]) {
return splitRange[0]
}
return range
}
function formatVersion(major, minor) {
return minor === 0 ? major : `${major}.${minor}`
}

View File

@ -125,6 +125,10 @@
<ModerationIcon class="icon" />
<span class="title">{{ formatMessage(commonMessages.moderationLabel) }}</span>
</NuxtLink>
<NuxtLink v-if="flags.developerMode" class="item button-transparent" to="/flags">
<ReportIcon class="icon" />
<span class="title">Feature flags</span>
</NuxtLink>
<NuxtLink
v-if="!cosmetics.hideModrinthAppPromos"
class="item button-transparent primary-color"
@ -228,6 +232,10 @@
<ModerationIcon aria-hidden="true" />
{{ formatMessage(commonMessages.moderationLabel) }}
</NuxtLink>
<NuxtLink v-if="flags.developerMode" class="iconified-button" to="/flags">
<ReportIcon aria-hidden="true" />
Feature flags
</NuxtLink>
</template>
<NuxtLink class="iconified-button" to="/settings">
<SettingsIcon aria-hidden="true" />
@ -411,7 +419,15 @@
</div>
</template>
<script setup>
import { LogInIcon, DownloadIcon, LibraryIcon, XIcon, IssuesIcon, Button } from 'omorphia'
import {
LogInIcon,
DownloadIcon,
LibraryIcon,
XIcon,
IssuesIcon,
Button,
ReportIcon,
} from 'omorphia'
import HamburgerIcon from '~/assets/images/utils/hamburger.svg'
import CrossIcon from '~/assets/images/utils/x.svg'
import SearchIcon from '~/assets/images/utils/search.svg'
@ -440,6 +456,7 @@ const { formatMessage } = useVIntl()
const app = useNuxtApp()
const auth = await useAuth()
const cosmetics = useCosmetics()
const flags = useFeatureFlags()
const tags = useTags()
const config = useRuntimeConfig()
@ -663,9 +680,10 @@ watch(
function developerModeIncrement() {
if (developerModeCounter.value >= 5) {
cosmetics.value.developerMode = !cosmetics.value.developerMode
flags.value.developerMode = !flags.value.developerMode
developerModeCounter.value = 0
if (cosmetics.value.developerMode) {
saveFeatureFlags()
if (flags.value.developerMode) {
app.$notify({
group: 'main',
title: 'Developer mode activated',

View File

@ -798,7 +798,7 @@
"message": "Enable or disable certain features on this device."
},
"settings.display.flags.title": {
"message": "Feature flags"
"message": "Toggle features"
},
"settings.display.project-list-layouts.datapack": {
"message": "Data Packs page"

View File

@ -288,6 +288,8 @@ export default defineNuxtConfig({
public: {
apiBaseUrl: getApiUrl(),
siteUrl: getDomain(),
production: isProduction(),
featureFlagOverrides: getFeatureFlagOverrides(),
owner: process.env.VERCEL_GIT_REPO_OWNER || 'modrinth',
slug: process.env.VERCEL_GIT_REPO_SLUG || 'knossos',
@ -380,6 +382,14 @@ function getApiUrl() {
return process.env.BROWSER_BASE_URL ?? globalThis.BROWSER_BASE_URL ?? STAGING_API_URL
}
function isProduction() {
return process.env.NODE_ENV === 'production'
}
function getFeatureFlagOverrides() {
return JSON.parse(process.env.FLAG_OVERRIDES ?? '{}')
}
function getDomain() {
if (process.env.NODE_ENV === 'production') {
if (process.env.SITE_URL) {

View File

@ -62,7 +62,7 @@
}/settings/license`"
label="License"
>
<LicenseIcon />
<CopyrightIcon />
</NavStackItem>
<NavStackItem
:link="`/${project.project_type}/${
@ -243,7 +243,7 @@
follower<span v-if="project.followers !== 1">s</span>
</div>
</div>
<div class="dates">
<div v-if="!flags.newProjectDetails" class="dates">
<div
v-tooltip="$dayjs(project.published).format('MMMM D, YYYY [at] h:mm A')"
class="date"
@ -269,8 +269,8 @@
<span class="label">Submitted</span>
<span class="value">{{ fromNow(project.queued) }}</span>
</div>
</div>
<hr class="card-divider" />
</div>
<div class="input-group">
<template v-if="auth.user">
<button
@ -333,8 +333,8 @@
:direction="cosmetics.projectLayout ? 'left' : 'right'"
>
<MoreHorizontalIcon />
<template #report> <ReportIcon /> Report</template>
<template #copy-id> <ClipboardCopyIcon /> Copy ID</template>
<template #report> <ReportIcon /> Report </template>
<template #copy-id> <ClipboardCopyIcon /> Copy ID </template>
</OverflowMenu>
</template>
<template v-else>
@ -360,13 +360,103 @@
:direction="cosmetics.projectLayout ? 'left' : 'right'"
>
<MoreHorizontalIcon />
<template #report> <ReportIcon /> Report</template>
<template #copy-id> <ClipboardCopyIcon /> Copy ID</template>
<template #report> <ReportIcon /> Report </template>
<template #copy-id> <ClipboardCopyIcon /> Copy ID </template>
</OverflowMenu>
</template>
</div>
</div>
</div>
<div
v-if="flags.projectCompatibility && versions.length > 0"
class="card flex-card experimental-styles-within"
>
<h2>Compatibility</h2>
<section>
<h3>Minecraft: Java Edition</h3>
<div class="tag-list">
<div
v-for="version in getVersionsToDisplay(project)"
:key="`version-tag-${version}`"
class="tag-list__item"
>
{{ version }}
</div>
</div>
</section>
<section>
<h3>Platforms</h3>
<div class="tag-list">
<div
v-for="platform in project.loaders"
:key="`platform-tag-${platform}`"
:class="`tag-list__item`"
:style="`--_color: var(--color-platform-${platform})`"
>
<svg v-html="tags.loaders.find((x) => x.name === platform).icon"></svg>
{{ $formatCategory(platform) }}
</div>
</div>
</section>
<section>
<h3>Environments</h3>
<div class="status-list">
<div class="status-list__item status-list__item--color-green">
<CheckIcon /> Singleplayer
</div>
<div
v-if="
project.client_side !== 'unsupported' && project.server_side !== 'unsupported'
"
class="status-list__item status-list__item--color-green"
>
<CheckIcon /> Client and server
</div>
<div
v-if="project.client_side === 'required' && project.server_side === 'unsupported'"
class="status-list__item status-list__item--color-green"
>
<CheckIcon /> Client
</div>
<div
v-if="project.server_side === 'required' && project.client_side === 'unsupported'"
class="status-list__item status-list__item--color-green"
>
<CheckIcon /> Server
</div>
<div
v-if="
project.client_side === 'optional' ||
(project.client_side === 'required' && project.server_side === 'optional')
"
class="status-list__item status-list__item--color-orange"
>
<CheckIcon /> Client <span>(Limited functionality)</span>
</div>
<div
v-if="
project.server_side === 'optional' ||
(project.server_side === 'required' && project.client_side === 'optional')
"
class="status-list__item status-list__item--color-orange"
>
<CheckIcon /> Server <span>(Limited functionality)</span>
</div>
<div
v-if="project.client_side === 'unsupported'"
class="status-list__item status-list__item--color-red"
>
<XIcon /> Client
</div>
<div
v-if="project.server_side === 'unsupported'"
class="status-list__item status-list__item--color-red"
>
<XIcon /> Server
</div>
</div>
</section>
</div>
</div>
<section class="normal-page__content">
<ProjectMemberHeader
@ -467,15 +557,114 @@
:route="route"
/>
</section>
<div class="universal-card normal-page__info">
<template
<div class="normal-page__info">
<div
v-if="
project.issues_url ||
flags.newProjectLinks &&
(project.issues_url ||
project.source_url ||
project.wiki_url ||
project.discord_url ||
project.donation_urls.length > 0)
"
class="card flex-card experimental-styles-within"
>
<h2>Links</h2>
<div class="links-list">
<a
v-if="project.issues_url"
:href="project.issues_url"
:target="$external()"
rel="noopener nofollow ugc"
>
<IssuesIcon aria-hidden="true" />
Report issues
<ExternalIcon aria-hidden="true" class="external-icon" />
</a>
<a
v-if="project.source_url"
:href="project.source_url"
:target="$external()"
rel="noopener nofollow ugc"
>
<CodeIcon aria-hidden="true" />
View source
<ExternalIcon aria-hidden="true" class="external-icon" />
</a>
<a
v-if="project.wiki_url"
:href="project.wiki_url"
:target="$external()"
rel="noopener nofollow ugc"
>
<WikiIcon aria-hidden="true" />
Visit wiki
<ExternalIcon aria-hidden="true" class="external-icon" />
</a>
<a
v-if="project.discord_url"
:href="project.discord_url"
:target="$external()"
rel="noopener nofollow ugc"
>
<DiscordIcon class="shrink" aria-hidden="true" />
Join Discord server
<ExternalIcon aria-hidden="true" class="external-icon" />
</a>
<hr
v-if="
(project.issues_url ||
project.source_url ||
project.wiki_url ||
project.discord_url) &&
project.donation_urls.length > 0
"
/>
<a
v-for="(donation, index) in project.donation_urls"
:key="index"
:href="donation.url"
:target="$external()"
rel="noopener nofollow ugc"
>
<BuyMeACoffeeLogo v-if="donation.id === 'bmac'" aria-hidden="true" />
<PatreonIcon v-else-if="donation.id === 'patreon'" aria-hidden="true" />
<KoFiIcon v-else-if="donation.id === 'ko-fi'" aria-hidden="true" />
<PayPalIcon v-else-if="donation.id === 'paypal'" aria-hidden="true" />
<OpenCollectiveIcon
v-else-if="donation.id === 'open-collective'"
aria-hidden="true"
/>
<HeartIcon v-else-if="donation.id === 'github'" />
<UnknownIcon v-else />
<span v-if="donation.id === 'bmac'">Buy Me a Coffee</span>
<span v-else-if="donation.id === 'patreon'">Donate on Patreon</span>
<span v-else-if="donation.id === 'paypal'">Donate on PayPal</span>
<span v-else-if="donation.id === 'ko-fi'">Donate on Ko-fi</span>
<span v-else-if="donation.id === 'github'">Sponsor on GitHub</span>
<span v-else>Donate</span>
<ExternalIcon aria-hidden="true" class="external-icon" />
</a>
</div>
</div>
<div
v-if="
showFeaturedVersions ||
!flags.newProjectLinks ||
!flags.newProjectDetails ||
!flags.newProjectMembers
"
class="universal-card"
>
<template
v-if="
!flags.newProjectLinks &&
(project.issues_url ||
project.source_url ||
project.wiki_url ||
project.discord_url ||
project.donation_urls.length > 0)
"
>
<h2 class="card-header">External resources</h2>
<div class="links">
@ -543,9 +732,12 @@
<span v-else>Donate</span>
</a>
</div>
<hr class="card-divider" />
<hr
v-if="showFeaturedVersions || !flags.newProjectMembers || !flags.newProjectDetails"
class="card-divider"
/>
</template>
<template v-if="featuredVersions.length > 0">
<template v-if="showFeaturedVersions">
<div class="featured-header">
<h2 class="card-header">Featured versions</h2>
<nuxt-link
@ -600,8 +792,9 @@
<Badge v-else-if="version.version_type === 'alpha'" type="alpha" color="red" />
</div>
</div>
<hr class="card-divider" />
<hr v-if="!flags.newProjectMembers || !flags.newProjectDetails" class="card-divider" />
</template>
<template v-if="!flags.newProjectMembers">
<h2 class="card-header">Project members</h2>
<nuxt-link
v-if="organization"
@ -626,14 +819,17 @@
<div class="member-info">
<p class="name">
{{ member.name }} <CrownIcon v-if="member.is_owner" v-tooltip="'Project owner'" />
{{ member.name }}
<CrownIcon v-if="member.is_owner" v-tooltip="'Project owner'" />
</p>
<p class="role">
{{ member.role }}
</p>
</div>
</nuxt-link>
<hr class="card-divider" />
<hr v-if="!flags.newProjectDetails" class="card-divider" />
</template>
<template v-if="!flags.newProjectDetails">
<h2 class="card-header">Technical information</h2>
<div class="infos">
<div class="info">
@ -727,6 +923,134 @@
</a>
</div>
</div>
</template>
</div>
<div v-if="flags.newProjectMembers" class="card flex-card experimental-styles-within">
<h2>Creators</h2>
<div class="details-list">
<template v-if="organization">
<nuxt-link
class="details-list__item details-list__item--type-large"
:to="`/organization/${organization.slug}`"
>
<Avatar
:src="organization.icon_url"
:alt="organization.name"
class="icon"
data-size="32"
data-shape="square"
/>
<div class="rows">
<span>
{{ organization.name }}
</span>
<span class="details-list__item__text--style-secondary">Organization</span>
</div>
</nuxt-link>
<hr />
</template>
<nuxt-link
v-for="member in members"
:key="`member-${member.id}`"
class="details-list__item details-list__item--type-large"
:to="'/user/' + member.user.username"
>
<Avatar
:src="member.avatar_url"
:alt="member.name"
class="icon"
data-size="32"
data-shape="circle"
/>
<div class="rows">
<span>
{{ member.name }}
<CrownIcon
v-if="member.is_owner"
v-tooltip="'Project owner'"
class="project-owner-icon"
/>
</span>
<span class="details-list__item__text--style-secondary">{{ member.role }}</span>
</div>
</nuxt-link>
</div>
</div>
<div v-if="flags.newProjectDetails" class="card flex-card experimental-styles-within">
<h2>Details</h2>
<div class="details-list">
<div class="details-list__item">
<LicenseIcon aria-hidden="true" />
<div>
Licensed
<a
v-if="project.license.url"
class="text-link"
:href="project.license.url"
:target="$external()"
rel="noopener nofollow ugc"
>
{{ licenseIdDisplay }} <ExternalIcon aria-hidden="true" class="external-icon" />
</a>
<span
v-else-if="
project.license.id === 'LicenseRef-All-Rights-Reserved' ||
!project.license.id.includes('LicenseRef')
"
class="text-link"
@click="getLicenseData()"
>
{{ licenseIdDisplay }}
</span>
<span v-else>{{ licenseIdDisplay }}</span>
</div>
</div>
<div
v-if="project.approved"
v-tooltip="$dayjs(project.approved).format('MMMM D, YYYY [at] h:mm A')"
class="details-list__item"
>
<CalendarIcon aria-hidden="true" />
<div>
Published
<span>{{ fromNow(project.approved) }}</span>
</div>
</div>
<div
v-else
v-tooltip="$dayjs(project.published).format('MMMM D, YYYY [at] h:mm A')"
class="details-list__item"
>
<CalendarIcon aria-hidden="true" />
<div>
Created
<span>{{ fromNow(project.published) }}</span>
</div>
</div>
<div
v-if="project.status === 'processing' && project.queued"
v-tooltip="$dayjs(project.queued).format('MMMM D, YYYY [at] h:mm A')"
class="details-list__item"
>
<ModeratorIcon aria-hidden="true" />
<div>
Submitted
<span>{{ fromNow(project.queued) }}</span>
</div>
</div>
<div
v-if="versions.length > 0 && project.updated"
v-tooltip="$dayjs(project.updated).format('MMMM D, YYYY [at] h:mm A')"
class="details-list__item"
>
<VersionIcon aria-hidden="true" />
<div>
Updated
<span>{{ fromNow(project.updated) }}</span>
</div>
</div>
</div>
</div>
</div>
</div>
<ModerationChecklist
@ -753,6 +1077,8 @@ import {
isRejected,
isUnderReview,
isStaff,
CheckIcon,
XIcon,
} from 'omorphia'
import CrownIcon from '~/assets/images/utils/crown.svg'
import CalendarIcon from '~/assets/images/utils/calendar.svg'
@ -790,7 +1116,8 @@ import UsersIcon from '~/assets/images/utils/users.svg'
import CategoriesIcon from '~/assets/images/utils/tags.svg'
import DescriptionIcon from '~/assets/images/utils/align-left.svg'
import LinksIcon from '~/assets/images/utils/link.svg'
import LicenseIcon from '~/assets/images/utils/copyright.svg'
import CopyrightIcon from '~/assets/images/utils/copyright.svg'
import LicenseIcon from '~/assets/images/utils/book-text.svg'
import GalleryIcon from '~/assets/images/utils/image.svg'
import VersionIcon from '~/assets/images/utils/version.svg'
import { reportProject } from '~/utils/report-helpers.ts'
@ -799,6 +1126,8 @@ import { userCollectProject } from '~/composables/user.js'
import CollectionCreateModal from '~/components/ui/CollectionCreateModal.vue'
import OrganizationIcon from '~/assets/images/utils/organization.svg'
import ModerationChecklist from '~/components/ui/ModerationChecklist.vue'
import ModeratorIcon from '~/assets/images/sidebar/admin.svg'
import { getVersionsToDisplay } from '~/helpers/projects.js'
const data = useNuxtApp()
const route = useRoute()
@ -808,6 +1137,7 @@ const auth = await useAuth()
const user = await useUser()
const cosmetics = useCosmetics()
const tags = useTags()
const flags = useFeatureFlags()
const displayCollectionsSearch = ref('')
const collections = computed(() =>
@ -927,8 +1257,14 @@ if (project.value.project_type !== route.params.type || route.params.id !== proj
// The rest of the members should be sorted by role, then by name
const members = computed(() => {
const acceptedMembers = allMembers.value.filter((x) => x.accepted)
const owner = acceptedMembers.find((x) => x.is_owner)
const rest = acceptedMembers.filter((x) => !x.is_owner) || []
const owner = acceptedMembers.find((x) =>
organization.value
? organization.value.members.some(
(orgMember) => orgMember.user.id === x.user.id && orgMember.is_owner
)
: x.is_owner
)
const rest = acceptedMembers.filter((x) => x.user.id !== owner.user.id) || []
rest.sort((a, b) => {
if (a.role === b.role) {
@ -1173,10 +1509,15 @@ if (process.client && history && history.state && history.state.showChecklist) {
showModerationChecklist.value = true
futureProjects.value = history.state.projects
}
const showFeaturedVersions = computed(
() => !flags.value.removeFeaturedVersions && featuredVersions.value.length > 0
)
</script>
<style lang="scss" scoped>
.header {
grid-area: header;
.title {
overflow-wrap: break-word;
margin: var(--spacing-card-xs) 0;
@ -1243,11 +1584,13 @@ if (process.client && history && history.state && history.state.showChecklist) {
.project__gallery {
display: none;
}
&.has-featured-image {
.project__gallery {
display: inline-block;
width: 100%;
height: 10rem;
img {
width: 100%;
height: 10rem;
@ -1256,6 +1599,7 @@ if (process.client && history && history.state && history.state.showChecklist) {
border-radius: var(--size-rounded-card) var(--size-rounded-card) 0 0;
}
}
.project__icon {
margin-top: calc(-3rem - var(--spacing-card-lg) - 4px);
margin-left: -4px;
@ -1263,11 +1607,13 @@ if (process.client && history && history.state && history.state.showChecklist) {
box-shadow: -2px -2px 0 2px var(--color-raised-bg), 2px -2px 0 2px var(--color-raised-bg);
}
}
.project__header__content {
margin: 0;
background: none;
border-radius: unset;
}
.input-group {
flex-wrap: nowrap;
}
@ -1468,6 +1814,7 @@ if (process.client && history && history.state && history.state.showChecklist) {
.modal-license {
padding: var(--spacing-card-bg);
}
.settings-header {
display: flex;
flex-direction: row;
@ -1495,6 +1842,7 @@ if (process.client && history && history.state && history.state.showChecklist) {
.popout-checkbox {
padding: var(--gap-sm) var(--gap-md);
white-space: nowrap;
&:hover {
filter: brightness(0.95);
}
@ -1531,4 +1879,12 @@ if (process.client && history && history.state && history.state.showChecklist) {
margin: var(--gap-sm) var(--gap-md);
padding: var(--gap-sm);
}
.normal-page__info:empty {
display: none;
}
.project-owner-icon {
color: var(--color-orange);
}
</style>

View File

@ -157,18 +157,18 @@ function switchPage(page) {
}
.changelog-bar {
--color: var(--color-special-green);
--color: var(--color-green);
&.alpha {
--color: var(--color-special-red);
--color: var(--color-red);
}
&.release {
--color: var(--color-special-green);
--color: var(--color-green);
}
&.beta {
--color: var(--color-special-orange);
--color: var(--color-orange);
}
left: 0;

View File

@ -204,12 +204,12 @@ svg {
}
&.bad {
color: var(--color-special-red);
color: var(--color-red);
}
}
.warning {
color: var(--color-special-orange);
color: var(--color-orange);
font-weight: bold;
}
</style>

View File

@ -421,11 +421,11 @@ svg {
}
&.bad {
color: var(--color-special-red);
color: var(--color-red);
}
&.warn {
color: var(--color-special-orange);
color: var(--color-orange);
}
}

View File

@ -519,7 +519,6 @@
<script setup>
import { Multiselect } from 'vue-multiselect'
import { Avatar, Badge, Card, Checkbox, TransferIcon, CheckIcon, UsersIcon } from 'omorphia'
import { defineProps, ref, watch } from 'vue'
import ModalConfirm from '~/components/ui/ModalConfirm.vue'
import DropdownIcon from '~/assets/images/utils/dropdown.svg'

View File

@ -1374,7 +1374,7 @@ useSeoMeta({
display: flex;
align-items: center;
padding: var(--gap-sm) 0;
color: var(--color-special-gray);
color: var(--color-gray);
&.important {
color: var(--color-contrast);

View File

@ -617,7 +617,7 @@ export default defineNuxtComponent({
gap: var(--spacing-card-xs);
svg {
color: var(--color-special-orange);
color: var(--color-orange);
}
}
@ -645,7 +645,7 @@ export default defineNuxtComponent({
}
.label-button[data-active='true'] {
--background-color: var(--color-special-red);
--background-color: var(--color-red);
--text-color: var(--color-brand-inverted);
}

View File

@ -506,7 +506,7 @@ async function withdraw() {
}
.invalid {
color: var(--color-special-red);
color: var(--color-red);
}
.confirm-text {

63
pages/flags.vue Normal file
View File

@ -0,0 +1,63 @@
<script setup lang="ts">
import { FeatureFlag, DEFAULT_FEATURE_FLAGS, saveFeatureFlags } from '~/composables/featureFlags.ts'
const flags = shallowReactive(useFeatureFlags().value)
</script>
<template>
<div class="page">
<h1>Feature flags</h1>
<div class="flags">
<div
v-for="flag in Object.keys(flags) as FeatureFlag[]"
:key="`flag-${flag}`"
class="adjacent-input small card"
>
<label :for="`toggle-${flag}`">
<span class="label__title">
{{ 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>
</label>
<input
:id="`toggle-${flag}`"
v-model="flags[flag]"
class="switch stylized-toggle"
type="checkbox"
@change="() => saveFeatureFlags()"
/>
</div>
</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

@ -233,11 +233,11 @@ async function goToProjects() {
}
.warning {
color: var(--color-special-orange);
color: var(--color-orange);
}
.danger {
color: var(--color-special-red);
color: var(--color-red);
font-weight: bold;
}

View File

@ -630,7 +630,7 @@ const onBulkEditLinks = useClientTry(async () => {
gap: var(--spacing-card-xs);
svg {
color: var(--color-special-orange);
color: var(--color-orange);
}
}
@ -658,7 +658,7 @@ const onBulkEditLinks = useClientTry(async () => {
}
.label-button[data-active='true'] {
--background-color: var(--color-special-red);
--background-color: var(--color-red);
--text-color: var(--color-brand-inverted);
}

View File

@ -1,6 +1,6 @@
<template>
<div>
<MessageBanner v-if="cosmetics.developerMode" message-type="warning" class="developer-message">
<MessageBanner v-if="flags.developerMode" message-type="warning" class="developer-message">
<CodeIcon />
<IntlFormatted :message-id="developerModeBanner.description">
<template #strong="{ children }">
@ -137,15 +137,15 @@
</div>
</section>
<section class="universal-card">
<h2>{{ formatMessage(featureFlags.title) }}</h2>
<p>{{ formatMessage(featureFlags.description) }}</p>
<h2>{{ formatMessage(toggleFeatures.title) }}</h2>
<p>{{ formatMessage(toggleFeatures.description) }}</p>
<div class="adjacent-input small">
<label for="advanced-rendering">
<span class="label__title">
{{ formatMessage(featureFlags.advancedRenderingTitle) }}
{{ formatMessage(toggleFeatures.advancedRenderingTitle) }}
</span>
<span class="label__description">
{{ formatMessage(featureFlags.advancedRenderingDescription) }}
{{ formatMessage(toggleFeatures.advancedRenderingDescription) }}
</span>
</label>
<input
@ -159,10 +159,10 @@
<div class="adjacent-input small">
<label for="external-links-new-tab">
<span class="label__title">
{{ formatMessage(featureFlags.externalLinksNewTabTitle) }}
{{ formatMessage(toggleFeatures.externalLinksNewTabTitle) }}
</span>
<span class="label__description">
{{ formatMessage(featureFlags.externalLinksNewTabDescription) }}
{{ formatMessage(toggleFeatures.externalLinksNewTabDescription) }}
</span>
</label>
<input
@ -176,10 +176,10 @@
<div class="adjacent-input small">
<label for="modrinth-app-promos">
<span class="label__title">
{{ formatMessage(featureFlags.hideModrinthAppPromosTitle) }}
{{ formatMessage(toggleFeatures.hideModrinthAppPromosTitle) }}
</span>
<span class="label__description">
{{ formatMessage(featureFlags.hideModrinthAppPromosDescription) }}
{{ formatMessage(toggleFeatures.hideModrinthAppPromosDescription) }}
</span>
</label>
<input
@ -193,10 +193,10 @@
<div class="adjacent-input small">
<label for="search-layout-toggle">
<span class="label__title">
{{ formatMessage(featureFlags.rightAlignedSearchSidebarTitle) }}
{{ formatMessage(toggleFeatures.rightAlignedSearchSidebarTitle) }}
</span>
<span class="label__description">
{{ formatMessage(featureFlags.rightAlignedSearchSidebarDescription) }}
{{ formatMessage(toggleFeatures.rightAlignedSearchSidebarDescription) }}
</span>
</label>
<input
@ -210,10 +210,10 @@
<div class="adjacent-input small">
<label for="project-layout-toggle">
<span class="label__title">
{{ formatMessage(featureFlags.rightAlignedProjectSidebarTitle) }}
{{ formatMessage(toggleFeatures.rightAlignedProjectSidebarTitle) }}
</span>
<span class="label__description">
{{ formatMessage(featureFlags.rightAlignedProjectSidebarDescription) }}
{{ formatMessage(toggleFeatures.rightAlignedProjectSidebarDescription) }}
</span>
</label>
<input
@ -331,10 +331,10 @@ const projectListLayouts = defineMessages({
},
})
const featureFlags = defineMessages({
const toggleFeatures = defineMessages({
title: {
id: 'settings.display.flags.title',
defaultMessage: 'Feature flags',
defaultMessage: 'Toggle features',
},
description: {
id: 'settings.display.flags.description',
@ -386,6 +386,7 @@ const featureFlags = defineMessages({
})
const cosmetics = useCosmetics()
const flags = useFeatureFlags()
const tags = useTags()
const systemTheme = ref('light')
@ -394,7 +395,7 @@ const theme = useTheme()
const themeOptions = computed(() => {
const options = ['system', 'light', 'dark', 'oled']
if (cosmetics.value.developerMode || theme.value.preference === 'retro') {
if (flags.value.developerMode || theme.value.preference === 'retro') {
options.push('retro')
}
return options
@ -430,8 +431,8 @@ function updateColorTheme(value) {
}
function disableDeveloperMode() {
cosmetics.value.developerMode = !cosmetics.value.developerMode
saveCosmetics()
flags.value.developerMode = !flags.value.developerMode
saveFeatureFlags()
addNotification({
group: 'main',
title: 'Developer mode deactivated',

View File

@ -414,10 +414,10 @@ function getItemLabel(locale: Locale) {
}
&.errored {
border-color: var(--color-special-red);
border-color: var(--color-red);
&:hover {
border-color: var(--color-special-red);
border-color: var(--color-red);
}
}
@ -473,7 +473,7 @@ function getItemLabel(locale: Locale) {
}
.language-load-error {
color: var(--color-special-red);
color: var(--color-red);
font-size: var(--font-size-sm);
margin-left: 0.3rem;
display: flex;

View File

@ -502,7 +502,7 @@ export default defineNuxtComponent({
// 5 wide
display: flex;
flex-wrap: wrap;
justify-content: start;
justify-content: flex-start;
grid-gap: var(--gap-sm);
margin-top: 0.5rem;