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:
parent
5b2d36e976
commit
1d9fe0c03d
1
assets/images/utils/book-text.svg
Normal file
1
assets/images/utils/book-text.svg
Normal 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 |
@ -3,7 +3,7 @@
|
|||||||
*/
|
*/
|
||||||
|
|
||||||
.known-error .multiselect__tags {
|
.known-error .multiselect__tags {
|
||||||
border-color: var(--color-special-red) !important;
|
border-color: var(--color-red) !important;
|
||||||
background-color: var(--color-warning-bg) !important;
|
background-color: var(--color-warning-bg) !important;
|
||||||
|
|
||||||
&::placeholder {
|
&::placeholder {
|
||||||
@ -104,7 +104,7 @@
|
|||||||
font-weight: bold;
|
font-weight: bold;
|
||||||
|
|
||||||
.required {
|
.required {
|
||||||
color: var(--color-special-red);
|
color: var(--color-red);
|
||||||
}
|
}
|
||||||
|
|
||||||
&.size-card-header {
|
&.size-card-header {
|
||||||
@ -285,6 +285,10 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
> :first-child {
|
> :first-child {
|
||||||
|
:first-child {
|
||||||
|
margin-block-start: 0;
|
||||||
|
}
|
||||||
|
|
||||||
margin-block-start: 0;
|
margin-block-start: 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -476,7 +480,7 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
&.danger-button {
|
&.danger-button {
|
||||||
color: var(--color-special-red);
|
color: var(--color-red);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -636,12 +640,12 @@ tr.button-transparent {
|
|||||||
}
|
}
|
||||||
|
|
||||||
.danger-button {
|
.danger-button {
|
||||||
--background-color: var(--color-special-red);
|
--background-color: var(--color-red);
|
||||||
--text-color: var(--color-brand-inverted);
|
--text-color: var(--color-brand-inverted);
|
||||||
}
|
}
|
||||||
|
|
||||||
.moderation-button {
|
.moderation-button {
|
||||||
--background-color: var(--color-special-orange);
|
--background-color: var(--color-orange);
|
||||||
--text-color: var(--color-brand-inverted);
|
--text-color: var(--color-brand-inverted);
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -670,7 +674,7 @@ tr.button-transparent {
|
|||||||
}
|
}
|
||||||
|
|
||||||
.known-error .multiselect__tags {
|
.known-error .multiselect__tags {
|
||||||
border-color: var(--color-special-red) !important;
|
border-color: var(--color-red) !important;
|
||||||
background-color: var(--color-warning-bg) !important;
|
background-color: var(--color-warning-bg) !important;
|
||||||
|
|
||||||
&::placeholder {
|
&::placeholder {
|
||||||
@ -913,7 +917,7 @@ tr.button-transparent {
|
|||||||
.text-input-wrapper.known-error,
|
.text-input-wrapper.known-error,
|
||||||
input.known-error,
|
input.known-error,
|
||||||
textarea.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;
|
background-color: var(--color-warning-bg) !important;
|
||||||
|
|
||||||
&::placeholder {
|
&::placeholder {
|
||||||
@ -923,7 +927,7 @@ textarea.known-error {
|
|||||||
|
|
||||||
.known-errors {
|
.known-errors {
|
||||||
min-height: 0;
|
min-height: 0;
|
||||||
color: var(--color-special-red);
|
color: var(--color-red);
|
||||||
|
|
||||||
ul {
|
ul {
|
||||||
margin: 0;
|
margin: 0;
|
||||||
@ -1269,3 +1273,179 @@ a.subtle-link {
|
|||||||
svg.inline-svg {
|
svg.inline-svg {
|
||||||
vertical-align: middle;
|
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;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
@ -5,8 +5,130 @@ html {
|
|||||||
--color-text-secondary: var(--color-icon);
|
--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 {
|
.light-mode {
|
||||||
--color-icon: #6b7280;
|
--color-secondary: #6b7280;
|
||||||
|
--color-icon: var(--color-secondary);
|
||||||
--color-text: hsl(221, 39%, 11%);
|
--color-text: hsl(221, 39%, 11%);
|
||||||
--color-text-inactive: hsl(215, 14%, 34%);
|
--color-text-inactive: hsl(215, 14%, 34%);
|
||||||
--color-text-dark: #1a202c;
|
--color-text-dark: #1a202c;
|
||||||
@ -59,13 +181,6 @@ html {
|
|||||||
--color-link-hover: #1a76e7;
|
--color-link-hover: #1a76e7;
|
||||||
--color-link-active: #146fd7;
|
--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-red-bg: rgba(204, 35, 69, 0.1);
|
||||||
|
|
||||||
--color-warning-bg: hsl(355, 70%, 88%);
|
--color-warning-bg: hsl(355, 70%, 88%);
|
||||||
@ -77,7 +192,7 @@ html {
|
|||||||
|
|
||||||
--color-info-banner-text: var(--color-text);
|
--color-info-banner-text: var(--color-text);
|
||||||
--color-info-banner-bg: var(--color-ad);
|
--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-block-quote: var(--color-tooltip-bg);
|
||||||
--color-header-underline: var(--color-divider-dark);
|
--color-header-underline: var(--color-divider-dark);
|
||||||
@ -140,8 +255,12 @@ html {
|
|||||||
--landing-raw-bg: #fff;
|
--landing-raw-bg: #fff;
|
||||||
}
|
}
|
||||||
|
|
||||||
.dark-mode {
|
.dark,
|
||||||
--color-icon: #96a2b0;
|
.dark-mode,
|
||||||
|
.oled-mode,
|
||||||
|
.retro-mode {
|
||||||
|
--color-secondary: #96a2b0;
|
||||||
|
--color-icon: var(--color-secondary);
|
||||||
--color-text: var(--dark-color-text);
|
--color-text: var(--dark-color-text);
|
||||||
--color-text-inactive: #929aa3;
|
--color-text-inactive: #929aa3;
|
||||||
--color-text-dark: var(--dark-color-text-dark);
|
--color-text-dark: var(--dark-color-text-dark);
|
||||||
@ -154,13 +273,6 @@ html {
|
|||||||
--color-text-inverted: var(--color-bg);
|
--color-text-inverted: var(--color-bg);
|
||||||
--color-bg-inverted: var(--color-text);
|
--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-red-bg: rgba(255, 74, 110, 0.2);
|
||||||
|
|
||||||
--color-brand-green: #1bd96a;
|
--color-brand-green: #1bd96a;
|
||||||
@ -209,7 +321,7 @@ html {
|
|||||||
|
|
||||||
--color-info-banner-text: var(--color-text);
|
--color-info-banner-text: var(--color-text);
|
||||||
--color-info-banner-bg: var(--color-ad);
|
--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-block-quote: var(--color-code-bg);
|
||||||
--color-header-underline: var(--color-divider-dark);
|
--color-header-underline: var(--color-divider-dark);
|
||||||
@ -274,7 +386,6 @@ html {
|
|||||||
}
|
}
|
||||||
|
|
||||||
.oled-mode {
|
.oled-mode {
|
||||||
@extend .dark-mode;
|
|
||||||
--color-bg: #000000;
|
--color-bg: #000000;
|
||||||
--color-raised-bg: #101013;
|
--color-raised-bg: #101013;
|
||||||
|
|
||||||
|
|||||||
@ -65,7 +65,7 @@ defineProps({
|
|||||||
.badge {
|
.badge {
|
||||||
font-weight: bold;
|
font-weight: bold;
|
||||||
width: fit-content;
|
width: fit-content;
|
||||||
--badge-color: var(--color-special-gray);
|
--badge-color: var(--color-gray);
|
||||||
color: var(--badge-color);
|
color: var(--badge-color);
|
||||||
white-space: nowrap;
|
white-space: nowrap;
|
||||||
|
|
||||||
@ -88,7 +88,7 @@ defineProps({
|
|||||||
&.type--withheld,
|
&.type--withheld,
|
||||||
&.type--rejected,
|
&.type--rejected,
|
||||||
&.red {
|
&.red {
|
||||||
--badge-color: var(--color-special-red);
|
--badge-color: var(--color-red);
|
||||||
}
|
}
|
||||||
|
|
||||||
&.type--pending,
|
&.type--pending,
|
||||||
@ -96,7 +96,7 @@ defineProps({
|
|||||||
&.type--processing,
|
&.type--processing,
|
||||||
&.type--scheduled,
|
&.type--scheduled,
|
||||||
&.orange {
|
&.orange {
|
||||||
--badge-color: var(--color-special-orange);
|
--badge-color: var(--color-orange);
|
||||||
}
|
}
|
||||||
|
|
||||||
&.type--accepted,
|
&.type--accepted,
|
||||||
@ -104,23 +104,23 @@ defineProps({
|
|||||||
&.type--success,
|
&.type--success,
|
||||||
&.type--approved-general,
|
&.type--approved-general,
|
||||||
&.green {
|
&.green {
|
||||||
--badge-color: var(--color-special-green);
|
--badge-color: var(--color-green);
|
||||||
}
|
}
|
||||||
|
|
||||||
&.type--creator,
|
&.type--creator,
|
||||||
&.type--approved,
|
&.type--approved,
|
||||||
&.blue {
|
&.blue {
|
||||||
--badge-color: var(--color-special-blue);
|
--badge-color: var(--color-blue);
|
||||||
}
|
}
|
||||||
|
|
||||||
&.type--unlisted,
|
&.type--unlisted,
|
||||||
&.purple {
|
&.purple {
|
||||||
--badge-color: var(--color-special-purple);
|
--badge-color: var(--color-purple);
|
||||||
}
|
}
|
||||||
|
|
||||||
&.type--private,
|
&.type--private,
|
||||||
&.gray {
|
&.gray {
|
||||||
--badge-color: var(--color-special-gray);
|
--badge-color: var(--color-gray);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
</style>
|
</style>
|
||||||
|
|||||||
@ -249,7 +249,7 @@
|
|||||||
>
|
>
|
||||||
<CheckIcon /> Mark as read
|
<CheckIcon /> Mark as read
|
||||||
</button>
|
</button>
|
||||||
<CopyCode v-if="cosmetics.developerMode" :text="notification.id" />
|
<CopyCode v-if="flags.developerMode" :text="notification.id" />
|
||||||
</div>
|
</div>
|
||||||
<div v-else class="input-group">
|
<div v-else class="input-group">
|
||||||
<nuxt-link
|
<nuxt-link
|
||||||
@ -281,7 +281,7 @@
|
|||||||
>
|
>
|
||||||
<CheckIcon /> Mark as read
|
<CheckIcon /> Mark as read
|
||||||
</button>
|
</button>
|
||||||
<CopyCode v-if="cosmetics.developerMode" :text="notification.id" />
|
<CopyCode v-if="flags.developerMode" :text="notification.id" />
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@ -335,7 +335,7 @@ const props = defineProps({
|
|||||||
},
|
},
|
||||||
})
|
})
|
||||||
|
|
||||||
const cosmetics = useCosmetics()
|
const flags = useFeatureFlags()
|
||||||
const tags = useTags()
|
const tags = useTags()
|
||||||
|
|
||||||
const type = computed(() =>
|
const type = computed(() =>
|
||||||
@ -549,7 +549,7 @@ function getMessages() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
.unknown-type {
|
.unknown-type {
|
||||||
color: var(--color-special-red);
|
color: var(--color-red);
|
||||||
}
|
}
|
||||||
|
|
||||||
.title-link {
|
.title-link {
|
||||||
@ -560,11 +560,11 @@ function getMessages() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
.moderation-color {
|
.moderation-color {
|
||||||
color: var(--color-special-orange);
|
color: var(--color-orange);
|
||||||
}
|
}
|
||||||
|
|
||||||
.creator-color {
|
.creator-color {
|
||||||
color: var(--color-special-blue);
|
color: var(--color-blue);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
</style>
|
</style>
|
||||||
|
|||||||
@ -26,8 +26,8 @@ function stopTimer(notif) {
|
|||||||
</script>
|
</script>
|
||||||
<style lang="scss" scoped>
|
<style lang="scss" scoped>
|
||||||
.vue-notification {
|
.vue-notification {
|
||||||
background: var(--color-special-blue) !important;
|
background: var(--color-blue) !important;
|
||||||
border-left: 5px solid var(--color-special-blue) !important;
|
border-left: 5px solid var(--color-blue) !important;
|
||||||
color: var(--color-brand-inverted) !important;
|
color: var(--color-brand-inverted) !important;
|
||||||
|
|
||||||
box-sizing: border-box;
|
box-sizing: border-box;
|
||||||
@ -37,18 +37,18 @@ function stopTimer(notif) {
|
|||||||
margin: 0 5px 5px;
|
margin: 0 5px 5px;
|
||||||
|
|
||||||
&.success {
|
&.success {
|
||||||
background: var(--color-special-green) !important;
|
background: var(--color-green) !important;
|
||||||
border-left-color: var(--color-special-green) !important;
|
border-left-color: var(--color-green) !important;
|
||||||
}
|
}
|
||||||
|
|
||||||
&.warn {
|
&.warn {
|
||||||
background: var(--color-special-orange) !important;
|
background: var(--color-orange) !important;
|
||||||
border-left-color: var(--color-special-orange) !important;
|
border-left-color: var(--color-orange) !important;
|
||||||
}
|
}
|
||||||
|
|
||||||
&.error {
|
&.error {
|
||||||
background: var(--color-special-red) !important;
|
background: var(--color-red) !important;
|
||||||
border-left-color: var(--color-special-red) !important;
|
border-left-color: var(--color-red) !important;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@ -414,7 +414,7 @@ export default {
|
|||||||
|
|
||||||
svg {
|
svg {
|
||||||
width: auto;
|
width: auto;
|
||||||
color: var(--color-special-orange);
|
color: var(--color-orange);
|
||||||
height: 1.5rem;
|
height: 1.5rem;
|
||||||
margin-bottom: -0.25rem;
|
margin-bottom: -0.25rem;
|
||||||
}
|
}
|
||||||
|
|||||||
@ -428,15 +428,15 @@ const submitForReview = async () => {
|
|||||||
align-items: center;
|
align-items: center;
|
||||||
|
|
||||||
.required {
|
.required {
|
||||||
color: var(--color-special-red);
|
color: var(--color-red);
|
||||||
}
|
}
|
||||||
|
|
||||||
.suggestion {
|
.suggestion {
|
||||||
color: var(--color-special-purple);
|
color: var(--color-purple);
|
||||||
}
|
}
|
||||||
|
|
||||||
.review {
|
.review {
|
||||||
color: var(--color-special-orange);
|
color: var(--color-orange);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -467,7 +467,7 @@ const submitForReview = async () => {
|
|||||||
.circle {
|
.circle {
|
||||||
--circle-size: 2rem;
|
--circle-size: 2rem;
|
||||||
--background-color: var(--color-bg);
|
--background-color: var(--color-bg);
|
||||||
--content-color: var(--color-special-gray);
|
--content-color: var(--color-gray);
|
||||||
width: var(--circle-size);
|
width: var(--circle-size);
|
||||||
height: var(--circle-size);
|
height: var(--circle-size);
|
||||||
border-radius: 50%;
|
border-radius: 50%;
|
||||||
@ -483,19 +483,19 @@ const submitForReview = async () => {
|
|||||||
}
|
}
|
||||||
|
|
||||||
&.required {
|
&.required {
|
||||||
--content-color: var(--color-special-red);
|
--content-color: var(--color-red);
|
||||||
}
|
}
|
||||||
|
|
||||||
&.suggestion {
|
&.suggestion {
|
||||||
--content-color: var(--color-special-purple);
|
--content-color: var(--color-purple);
|
||||||
}
|
}
|
||||||
|
|
||||||
&.review {
|
&.review {
|
||||||
--content-color: var(--color-special-orange);
|
--content-color: var(--color-orange);
|
||||||
}
|
}
|
||||||
|
|
||||||
&.done {
|
&.done {
|
||||||
--background-color: var(--color-special-green);
|
--background-color: var(--color-green);
|
||||||
--content-color: var(--color-brand-inverted);
|
--content-color: var(--color-brand-inverted);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -507,7 +507,7 @@ const defaultRanges: Record<number, [string, number] | string> = {
|
|||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: row;
|
flex-direction: row;
|
||||||
justify-content: space-between;
|
justify-content: space-between;
|
||||||
align-items: start;
|
align-items: flex-start;
|
||||||
gap: var(--gap-md);
|
gap: var(--gap-md);
|
||||||
|
|
||||||
.chart-controls__buttons {
|
.chart-controls__buttons {
|
||||||
|
|||||||
@ -82,7 +82,7 @@
|
|||||||
<span v-tooltip="$dayjs(report.created).format('MMMM D, YYYY [at] h:mm A')">{{
|
<span v-tooltip="$dayjs(report.created).format('MMMM D, YYYY [at] h:mm A')">{{
|
||||||
fromNow(report.created)
|
fromNow(report.created)
|
||||||
}}</span>
|
}}</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>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
@ -124,7 +124,7 @@ defineProps({
|
|||||||
},
|
},
|
||||||
})
|
})
|
||||||
|
|
||||||
const cosmetics = useCosmetics()
|
const flags = useFeatureFlags()
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<style lang="scss" scoped>
|
<style lang="scss" scoped>
|
||||||
|
|||||||
@ -33,7 +33,7 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</Modal>
|
</Modal>
|
||||||
<div v-if="cosmetics.developerMode" class="thread-id">
|
<div v-if="flags.developerMode" class="thread-id">
|
||||||
Thread ID: <CopyCode :text="thread.id" />
|
Thread ID: <CopyCode :text="thread.id" />
|
||||||
</div>
|
</div>
|
||||||
<div v-if="sortedMessages.length > 0" class="messages universal-card recessed">
|
<div v-if="sortedMessages.length > 0" class="messages universal-card recessed">
|
||||||
@ -247,7 +247,7 @@ const props = defineProps({
|
|||||||
const emit = defineEmits(['update-thread'])
|
const emit = defineEmits(['update-thread'])
|
||||||
|
|
||||||
const app = useNuxtApp()
|
const app = useNuxtApp()
|
||||||
const cosmetics = useCosmetics()
|
const flags = useFeatureFlags()
|
||||||
|
|
||||||
const members = computed(() => {
|
const members = computed(() => {
|
||||||
const members = {}
|
const members = {}
|
||||||
|
|||||||
@ -289,7 +289,7 @@ a:active + .message__author a,
|
|||||||
|
|
||||||
.moderation-color,
|
.moderation-color,
|
||||||
role-moderator {
|
role-moderator {
|
||||||
color: var(--color-special-orange);
|
color: var(--color-orange);
|
||||||
}
|
}
|
||||||
|
|
||||||
.role-admin {
|
.role-admin {
|
||||||
@ -297,11 +297,11 @@ role-moderator {
|
|||||||
}
|
}
|
||||||
|
|
||||||
.reporter-icon {
|
.reporter-icon {
|
||||||
color: var(--color-special-purple);
|
color: var(--color-purple);
|
||||||
}
|
}
|
||||||
|
|
||||||
.private-icon {
|
.private-icon {
|
||||||
color: var(--color-special-gray);
|
color: var(--color-gray);
|
||||||
}
|
}
|
||||||
|
|
||||||
@media screen and (min-width: 600px) {
|
@media screen and (min-width: 600px) {
|
||||||
|
|||||||
@ -14,7 +14,6 @@ export const useCosmetics = () =>
|
|||||||
projectLayout: false,
|
projectLayout: false,
|
||||||
advancedRendering: true,
|
advancedRendering: true,
|
||||||
externalLinksNewTab: true,
|
externalLinksNewTab: true,
|
||||||
developerMode: false,
|
|
||||||
notUsingBlockers: false,
|
notUsingBlockers: false,
|
||||||
hideModrinthAppPromos: false,
|
hideModrinthAppPromos: false,
|
||||||
preferredDarkTheme: 'dark',
|
preferredDarkTheme: 'dark',
|
||||||
@ -38,6 +37,9 @@ export const useCosmetics = () =>
|
|||||||
export const saveCosmetics = () => {
|
export const saveCosmetics = () => {
|
||||||
const cosmetics = useCosmetics()
|
const cosmetics = useCosmetics()
|
||||||
|
|
||||||
|
console.log('SAVING COSMETICS:')
|
||||||
|
console.log(cosmetics)
|
||||||
|
|
||||||
const cosmeticsCookie = useCookie('cosmetics', {
|
const cosmeticsCookie = useCookie('cosmetics', {
|
||||||
maxAge: 60 * 60 * 24 * 365 * 10,
|
maxAge: 60 * 60 * 24 * 365 * 10,
|
||||||
sameSite: 'lax',
|
sameSite: 'lax',
|
||||||
|
|||||||
105
composables/featureFlags.ts
Normal file
105
composables/featureFlags.ts
Normal 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
|
||||||
|
}
|
||||||
@ -81,3 +81,172 @@ export const PRIVATE_PROJECT_STATUSES = ['private', 'rejected', 'processing']
|
|||||||
export const REJECTED_PROJECT_STATUSES = ['rejected', 'withheld']
|
export const REJECTED_PROJECT_STATUSES = ['rejected', 'withheld']
|
||||||
export const UNDER_REVIEW_PROJECT_STATUSES = ['processing']
|
export const UNDER_REVIEW_PROJECT_STATUSES = ['processing']
|
||||||
export const DRAFT_PROJECT_STATUSES = ['draft']
|
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-132211–b1.8.1':
|
||||||
|
return 'All legacy versions'
|
||||||
|
case 'a1.0.4–b1.8.1':
|
||||||
|
return 'All alpha and beta versions'
|
||||||
|
case 'a1.0.4–a1.2.6':
|
||||||
|
return 'All alpha versions'
|
||||||
|
case 'b1.0–b1.8.1':
|
||||||
|
return 'All beta versions'
|
||||||
|
case 'rd-132211–inf20100618':
|
||||||
|
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}`
|
||||||
|
}
|
||||||
|
|||||||
@ -125,6 +125,10 @@
|
|||||||
<ModerationIcon class="icon" />
|
<ModerationIcon class="icon" />
|
||||||
<span class="title">{{ formatMessage(commonMessages.moderationLabel) }}</span>
|
<span class="title">{{ formatMessage(commonMessages.moderationLabel) }}</span>
|
||||||
</NuxtLink>
|
</NuxtLink>
|
||||||
|
<NuxtLink v-if="flags.developerMode" class="item button-transparent" to="/flags">
|
||||||
|
<ReportIcon class="icon" />
|
||||||
|
<span class="title">Feature flags</span>
|
||||||
|
</NuxtLink>
|
||||||
<NuxtLink
|
<NuxtLink
|
||||||
v-if="!cosmetics.hideModrinthAppPromos"
|
v-if="!cosmetics.hideModrinthAppPromos"
|
||||||
class="item button-transparent primary-color"
|
class="item button-transparent primary-color"
|
||||||
@ -228,6 +232,10 @@
|
|||||||
<ModerationIcon aria-hidden="true" />
|
<ModerationIcon aria-hidden="true" />
|
||||||
{{ formatMessage(commonMessages.moderationLabel) }}
|
{{ formatMessage(commonMessages.moderationLabel) }}
|
||||||
</NuxtLink>
|
</NuxtLink>
|
||||||
|
<NuxtLink v-if="flags.developerMode" class="iconified-button" to="/flags">
|
||||||
|
<ReportIcon aria-hidden="true" />
|
||||||
|
Feature flags
|
||||||
|
</NuxtLink>
|
||||||
</template>
|
</template>
|
||||||
<NuxtLink class="iconified-button" to="/settings">
|
<NuxtLink class="iconified-button" to="/settings">
|
||||||
<SettingsIcon aria-hidden="true" />
|
<SettingsIcon aria-hidden="true" />
|
||||||
@ -411,7 +419,15 @@
|
|||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
<script setup>
|
<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 HamburgerIcon from '~/assets/images/utils/hamburger.svg'
|
||||||
import CrossIcon from '~/assets/images/utils/x.svg'
|
import CrossIcon from '~/assets/images/utils/x.svg'
|
||||||
import SearchIcon from '~/assets/images/utils/search.svg'
|
import SearchIcon from '~/assets/images/utils/search.svg'
|
||||||
@ -440,6 +456,7 @@ const { formatMessage } = useVIntl()
|
|||||||
const app = useNuxtApp()
|
const app = useNuxtApp()
|
||||||
const auth = await useAuth()
|
const auth = await useAuth()
|
||||||
const cosmetics = useCosmetics()
|
const cosmetics = useCosmetics()
|
||||||
|
const flags = useFeatureFlags()
|
||||||
const tags = useTags()
|
const tags = useTags()
|
||||||
|
|
||||||
const config = useRuntimeConfig()
|
const config = useRuntimeConfig()
|
||||||
@ -663,9 +680,10 @@ watch(
|
|||||||
|
|
||||||
function developerModeIncrement() {
|
function developerModeIncrement() {
|
||||||
if (developerModeCounter.value >= 5) {
|
if (developerModeCounter.value >= 5) {
|
||||||
cosmetics.value.developerMode = !cosmetics.value.developerMode
|
flags.value.developerMode = !flags.value.developerMode
|
||||||
developerModeCounter.value = 0
|
developerModeCounter.value = 0
|
||||||
if (cosmetics.value.developerMode) {
|
saveFeatureFlags()
|
||||||
|
if (flags.value.developerMode) {
|
||||||
app.$notify({
|
app.$notify({
|
||||||
group: 'main',
|
group: 'main',
|
||||||
title: 'Developer mode activated',
|
title: 'Developer mode activated',
|
||||||
|
|||||||
@ -798,7 +798,7 @@
|
|||||||
"message": "Enable or disable certain features on this device."
|
"message": "Enable or disable certain features on this device."
|
||||||
},
|
},
|
||||||
"settings.display.flags.title": {
|
"settings.display.flags.title": {
|
||||||
"message": "Feature flags"
|
"message": "Toggle features"
|
||||||
},
|
},
|
||||||
"settings.display.project-list-layouts.datapack": {
|
"settings.display.project-list-layouts.datapack": {
|
||||||
"message": "Data Packs page"
|
"message": "Data Packs page"
|
||||||
|
|||||||
@ -288,6 +288,8 @@ export default defineNuxtConfig({
|
|||||||
public: {
|
public: {
|
||||||
apiBaseUrl: getApiUrl(),
|
apiBaseUrl: getApiUrl(),
|
||||||
siteUrl: getDomain(),
|
siteUrl: getDomain(),
|
||||||
|
production: isProduction(),
|
||||||
|
featureFlagOverrides: getFeatureFlagOverrides(),
|
||||||
|
|
||||||
owner: process.env.VERCEL_GIT_REPO_OWNER || 'modrinth',
|
owner: process.env.VERCEL_GIT_REPO_OWNER || 'modrinth',
|
||||||
slug: process.env.VERCEL_GIT_REPO_SLUG || 'knossos',
|
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
|
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() {
|
function getDomain() {
|
||||||
if (process.env.NODE_ENV === 'production') {
|
if (process.env.NODE_ENV === 'production') {
|
||||||
if (process.env.SITE_URL) {
|
if (process.env.SITE_URL) {
|
||||||
|
|||||||
@ -62,7 +62,7 @@
|
|||||||
}/settings/license`"
|
}/settings/license`"
|
||||||
label="License"
|
label="License"
|
||||||
>
|
>
|
||||||
<LicenseIcon />
|
<CopyrightIcon />
|
||||||
</NavStackItem>
|
</NavStackItem>
|
||||||
<NavStackItem
|
<NavStackItem
|
||||||
:link="`/${project.project_type}/${
|
:link="`/${project.project_type}/${
|
||||||
@ -243,7 +243,7 @@
|
|||||||
follower<span v-if="project.followers !== 1">s</span>
|
follower<span v-if="project.followers !== 1">s</span>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="dates">
|
<div v-if="!flags.newProjectDetails" class="dates">
|
||||||
<div
|
<div
|
||||||
v-tooltip="$dayjs(project.published).format('MMMM D, YYYY [at] h:mm A')"
|
v-tooltip="$dayjs(project.published).format('MMMM D, YYYY [at] h:mm A')"
|
||||||
class="date"
|
class="date"
|
||||||
@ -269,8 +269,8 @@
|
|||||||
<span class="label">Submitted</span>
|
<span class="label">Submitted</span>
|
||||||
<span class="value">{{ fromNow(project.queued) }}</span>
|
<span class="value">{{ fromNow(project.queued) }}</span>
|
||||||
</div>
|
</div>
|
||||||
|
<hr class="card-divider" />
|
||||||
</div>
|
</div>
|
||||||
<hr class="card-divider" />
|
|
||||||
<div class="input-group">
|
<div class="input-group">
|
||||||
<template v-if="auth.user">
|
<template v-if="auth.user">
|
||||||
<button
|
<button
|
||||||
@ -333,8 +333,8 @@
|
|||||||
:direction="cosmetics.projectLayout ? 'left' : 'right'"
|
:direction="cosmetics.projectLayout ? 'left' : 'right'"
|
||||||
>
|
>
|
||||||
<MoreHorizontalIcon />
|
<MoreHorizontalIcon />
|
||||||
<template #report> <ReportIcon /> Report</template>
|
<template #report> <ReportIcon /> Report </template>
|
||||||
<template #copy-id> <ClipboardCopyIcon /> Copy ID</template>
|
<template #copy-id> <ClipboardCopyIcon /> Copy ID </template>
|
||||||
</OverflowMenu>
|
</OverflowMenu>
|
||||||
</template>
|
</template>
|
||||||
<template v-else>
|
<template v-else>
|
||||||
@ -360,13 +360,103 @@
|
|||||||
:direction="cosmetics.projectLayout ? 'left' : 'right'"
|
:direction="cosmetics.projectLayout ? 'left' : 'right'"
|
||||||
>
|
>
|
||||||
<MoreHorizontalIcon />
|
<MoreHorizontalIcon />
|
||||||
<template #report> <ReportIcon /> Report</template>
|
<template #report> <ReportIcon /> Report </template>
|
||||||
<template #copy-id> <ClipboardCopyIcon /> Copy ID</template>
|
<template #copy-id> <ClipboardCopyIcon /> Copy ID </template>
|
||||||
</OverflowMenu>
|
</OverflowMenu>
|
||||||
</template>
|
</template>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</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>
|
</div>
|
||||||
<section class="normal-page__content">
|
<section class="normal-page__content">
|
||||||
<ProjectMemberHeader
|
<ProjectMemberHeader
|
||||||
@ -467,47 +557,49 @@
|
|||||||
:route="route"
|
:route="route"
|
||||||
/>
|
/>
|
||||||
</section>
|
</section>
|
||||||
<div class="universal-card normal-page__info">
|
<div class="normal-page__info">
|
||||||
<template
|
<div
|
||||||
v-if="
|
v-if="
|
||||||
project.issues_url ||
|
flags.newProjectLinks &&
|
||||||
project.source_url ||
|
(project.issues_url ||
|
||||||
project.wiki_url ||
|
project.source_url ||
|
||||||
project.discord_url ||
|
project.wiki_url ||
|
||||||
project.donation_urls.length > 0
|
project.discord_url ||
|
||||||
|
project.donation_urls.length > 0)
|
||||||
"
|
"
|
||||||
|
class="card flex-card experimental-styles-within"
|
||||||
>
|
>
|
||||||
<h2 class="card-header">External resources</h2>
|
<h2>Links</h2>
|
||||||
<div class="links">
|
<div class="links-list">
|
||||||
<a
|
<a
|
||||||
v-if="project.issues_url"
|
v-if="project.issues_url"
|
||||||
:href="project.issues_url"
|
:href="project.issues_url"
|
||||||
class="title"
|
|
||||||
:target="$external()"
|
:target="$external()"
|
||||||
rel="noopener nofollow ugc"
|
rel="noopener nofollow ugc"
|
||||||
>
|
>
|
||||||
<IssuesIcon aria-hidden="true" />
|
<IssuesIcon aria-hidden="true" />
|
||||||
<span>Issues</span>
|
Report issues
|
||||||
|
<ExternalIcon aria-hidden="true" class="external-icon" />
|
||||||
</a>
|
</a>
|
||||||
<a
|
<a
|
||||||
v-if="project.source_url"
|
v-if="project.source_url"
|
||||||
:href="project.source_url"
|
:href="project.source_url"
|
||||||
class="title"
|
|
||||||
:target="$external()"
|
:target="$external()"
|
||||||
rel="noopener nofollow ugc"
|
rel="noopener nofollow ugc"
|
||||||
>
|
>
|
||||||
<CodeIcon aria-hidden="true" />
|
<CodeIcon aria-hidden="true" />
|
||||||
<span>Source</span>
|
View source
|
||||||
|
<ExternalIcon aria-hidden="true" class="external-icon" />
|
||||||
</a>
|
</a>
|
||||||
<a
|
<a
|
||||||
v-if="project.wiki_url"
|
v-if="project.wiki_url"
|
||||||
:href="project.wiki_url"
|
:href="project.wiki_url"
|
||||||
class="title"
|
|
||||||
:target="$external()"
|
:target="$external()"
|
||||||
rel="noopener nofollow ugc"
|
rel="noopener nofollow ugc"
|
||||||
>
|
>
|
||||||
<WikiIcon aria-hidden="true" />
|
<WikiIcon aria-hidden="true" />
|
||||||
<span>Wiki</span>
|
Visit wiki
|
||||||
|
<ExternalIcon aria-hidden="true" class="external-icon" />
|
||||||
</a>
|
</a>
|
||||||
<a
|
<a
|
||||||
v-if="project.discord_url"
|
v-if="project.discord_url"
|
||||||
@ -516,8 +608,18 @@
|
|||||||
rel="noopener nofollow ugc"
|
rel="noopener nofollow ugc"
|
||||||
>
|
>
|
||||||
<DiscordIcon class="shrink" aria-hidden="true" />
|
<DiscordIcon class="shrink" aria-hidden="true" />
|
||||||
<span>Discord</span>
|
Join Discord server
|
||||||
|
<ExternalIcon aria-hidden="true" class="external-icon" />
|
||||||
</a>
|
</a>
|
||||||
|
<hr
|
||||||
|
v-if="
|
||||||
|
(project.issues_url ||
|
||||||
|
project.source_url ||
|
||||||
|
project.wiki_url ||
|
||||||
|
project.discord_url) &&
|
||||||
|
project.donation_urls.length > 0
|
||||||
|
"
|
||||||
|
/>
|
||||||
<a
|
<a
|
||||||
v-for="(donation, index) in project.donation_urls"
|
v-for="(donation, index) in project.donation_urls"
|
||||||
:key="index"
|
:key="index"
|
||||||
@ -536,195 +638,417 @@
|
|||||||
<HeartIcon v-else-if="donation.id === 'github'" />
|
<HeartIcon v-else-if="donation.id === 'github'" />
|
||||||
<UnknownIcon v-else />
|
<UnknownIcon v-else />
|
||||||
<span v-if="donation.id === 'bmac'">Buy Me a Coffee</span>
|
<span v-if="donation.id === 'bmac'">Buy Me a Coffee</span>
|
||||||
<span v-else-if="donation.id === 'patreon'">Patreon</span>
|
<span v-else-if="donation.id === 'patreon'">Donate on Patreon</span>
|
||||||
<span v-else-if="donation.id === 'paypal'">PayPal</span>
|
<span v-else-if="donation.id === 'paypal'">Donate on PayPal</span>
|
||||||
<span v-else-if="donation.id === 'ko-fi'">Ko-fi</span>
|
<span v-else-if="donation.id === 'ko-fi'">Donate on Ko-fi</span>
|
||||||
<span v-else-if="donation.id === 'github'">GitHub Sponsors</span>
|
<span v-else-if="donation.id === 'github'">Sponsor on GitHub</span>
|
||||||
<span v-else>Donate</span>
|
<span v-else>Donate</span>
|
||||||
|
<ExternalIcon aria-hidden="true" class="external-icon" />
|
||||||
</a>
|
</a>
|
||||||
</div>
|
</div>
|
||||||
<hr class="card-divider" />
|
</div>
|
||||||
</template>
|
<div
|
||||||
<template v-if="featuredVersions.length > 0">
|
v-if="
|
||||||
<div class="featured-header">
|
showFeaturedVersions ||
|
||||||
<h2 class="card-header">Featured versions</h2>
|
!flags.newProjectLinks ||
|
||||||
<nuxt-link
|
!flags.newProjectDetails ||
|
||||||
v-if="route.name !== 'type-id-versions' && (versions.length > 0 || currentMember)"
|
!flags.newProjectMembers
|
||||||
:to="`/${project.project_type}/${
|
"
|
||||||
project.slug ? project.slug : project.id
|
class="universal-card"
|
||||||
}/versions#all-versions`"
|
>
|
||||||
class="goto-link"
|
<template
|
||||||
>
|
v-if="
|
||||||
See all
|
!flags.newProjectLinks &&
|
||||||
<ChevronRightIcon class="featured-header-chevron" aria-hidden="true" />
|
(project.issues_url ||
|
||||||
</nuxt-link>
|
project.source_url ||
|
||||||
</div>
|
project.wiki_url ||
|
||||||
<div
|
project.discord_url ||
|
||||||
v-for="version in featuredVersions"
|
project.donation_urls.length > 0)
|
||||||
:key="version.id"
|
|
||||||
class="featured-version button-transparent"
|
|
||||||
@click="
|
|
||||||
$router.push(
|
|
||||||
`/${project.project_type}/${
|
|
||||||
project.slug ? project.slug : project.id
|
|
||||||
}/version/${encodeURI(version.displayUrlEnding)}`
|
|
||||||
)
|
|
||||||
"
|
"
|
||||||
>
|
>
|
||||||
<a
|
<h2 class="card-header">External resources</h2>
|
||||||
v-tooltip="
|
<div class="links">
|
||||||
version.primaryFile.filename + ' (' + $formatBytes(version.primaryFile.size) + ')'
|
|
||||||
"
|
|
||||||
:href="version.primaryFile.url"
|
|
||||||
class="download square-button brand-button"
|
|
||||||
:aria-label="`Download ${version.name}`"
|
|
||||||
@click.stop="(event) => event.stopPropagation()"
|
|
||||||
>
|
|
||||||
<DownloadIcon aria-hidden="true" />
|
|
||||||
</a>
|
|
||||||
<div class="info">
|
|
||||||
<nuxt-link
|
|
||||||
:to="`/${project.project_type}/${
|
|
||||||
project.slug ? project.slug : project.id
|
|
||||||
}/version/${encodeURI(version.displayUrlEnding)}`"
|
|
||||||
class="top"
|
|
||||||
>
|
|
||||||
{{ version.name }}
|
|
||||||
</nuxt-link>
|
|
||||||
<div v-if="version.game_versions.length > 0" class="game-version item">
|
|
||||||
{{ version.loaders.map((x) => $formatCategory(x)).join(', ') }}
|
|
||||||
{{ $formatVersion(version.game_versions) }}
|
|
||||||
</div>
|
|
||||||
<Badge v-if="version.version_type === 'release'" type="release" color="green" />
|
|
||||||
<Badge v-else-if="version.version_type === 'beta'" type="beta" color="orange" />
|
|
||||||
<Badge v-else-if="version.version_type === 'alpha'" type="alpha" color="red" />
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<hr class="card-divider" />
|
|
||||||
</template>
|
|
||||||
<h2 class="card-header">Project members</h2>
|
|
||||||
<nuxt-link
|
|
||||||
v-if="organization"
|
|
||||||
class="team-member columns button-transparent"
|
|
||||||
:to="`/organization/${organization.slug}`"
|
|
||||||
>
|
|
||||||
<Avatar :src="organization.icon_url" :alt="organization.name" size="sm" />
|
|
||||||
<div class="member-info">
|
|
||||||
<p class="name">
|
|
||||||
{{ organization.name }}
|
|
||||||
</p>
|
|
||||||
<p class="role"><OrganizationIcon /> Organization</p>
|
|
||||||
</div>
|
|
||||||
</nuxt-link>
|
|
||||||
<nuxt-link
|
|
||||||
v-for="member in members"
|
|
||||||
:key="member.user.id"
|
|
||||||
class="team-member columns button-transparent"
|
|
||||||
:to="'/user/' + member.user.username"
|
|
||||||
>
|
|
||||||
<Avatar :src="member.avatar_url" :alt="member.username" size="sm" circle />
|
|
||||||
|
|
||||||
<div class="member-info">
|
|
||||||
<p class="name">
|
|
||||||
{{ 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" />
|
|
||||||
<h2 class="card-header">Technical information</h2>
|
|
||||||
<div class="infos">
|
|
||||||
<div class="info">
|
|
||||||
<div class="key">License</div>
|
|
||||||
<div class="value lowercase">
|
|
||||||
<a
|
<a
|
||||||
v-if="project.license.url"
|
v-if="project.issues_url"
|
||||||
class="text-link"
|
:href="project.issues_url"
|
||||||
:href="project.license.url"
|
class="title"
|
||||||
|
:target="$external()"
|
||||||
rel="noopener nofollow ugc"
|
rel="noopener nofollow ugc"
|
||||||
>
|
>
|
||||||
{{ licenseIdDisplay }}
|
<IssuesIcon aria-hidden="true" />
|
||||||
|
<span>Issues</span>
|
||||||
</a>
|
</a>
|
||||||
<span
|
<a
|
||||||
v-else-if="
|
v-if="project.source_url"
|
||||||
project.license.id === 'LicenseRef-All-Rights-Reserved' ||
|
:href="project.source_url"
|
||||||
!project.license.id.includes('LicenseRef')
|
class="title"
|
||||||
"
|
:target="$external()"
|
||||||
class="text-link"
|
rel="noopener nofollow ugc"
|
||||||
@click="getLicenseData()"
|
|
||||||
>
|
>
|
||||||
{{ licenseIdDisplay }}
|
<CodeIcon aria-hidden="true" />
|
||||||
</span>
|
<span>Source</span>
|
||||||
<span v-else>{{ licenseIdDisplay }}</span>
|
</a>
|
||||||
|
<a
|
||||||
|
v-if="project.wiki_url"
|
||||||
|
:href="project.wiki_url"
|
||||||
|
class="title"
|
||||||
|
:target="$external()"
|
||||||
|
rel="noopener nofollow ugc"
|
||||||
|
>
|
||||||
|
<WikiIcon aria-hidden="true" />
|
||||||
|
<span>Wiki</span>
|
||||||
|
</a>
|
||||||
|
<a
|
||||||
|
v-if="project.discord_url"
|
||||||
|
:href="project.discord_url"
|
||||||
|
:target="$external()"
|
||||||
|
rel="noopener nofollow ugc"
|
||||||
|
>
|
||||||
|
<DiscordIcon class="shrink" aria-hidden="true" />
|
||||||
|
<span>Discord</span>
|
||||||
|
</a>
|
||||||
|
<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'">Patreon</span>
|
||||||
|
<span v-else-if="donation.id === 'paypal'">PayPal</span>
|
||||||
|
<span v-else-if="donation.id === 'ko-fi'">Ko-fi</span>
|
||||||
|
<span v-else-if="donation.id === 'github'">GitHub Sponsors</span>
|
||||||
|
<span v-else>Donate</span>
|
||||||
|
</a>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
<hr
|
||||||
<div
|
v-if="showFeaturedVersions || !flags.newProjectMembers || !flags.newProjectDetails"
|
||||||
v-if="
|
class="card-divider"
|
||||||
project.project_type !== 'resourcepack' &&
|
/>
|
||||||
project.project_type !== 'plugin' &&
|
</template>
|
||||||
project.project_type !== 'shader' &&
|
<template v-if="showFeaturedVersions">
|
||||||
project.project_type !== 'datapack'
|
<div class="featured-header">
|
||||||
"
|
<h2 class="card-header">Featured versions</h2>
|
||||||
class="info"
|
<nuxt-link
|
||||||
>
|
v-if="route.name !== 'type-id-versions' && (versions.length > 0 || currentMember)"
|
||||||
<div class="key">Client side</div>
|
:to="`/${project.project_type}/${
|
||||||
<div class="value">
|
project.slug ? project.slug : project.id
|
||||||
{{ project.client_side }}
|
}/versions#all-versions`"
|
||||||
|
class="goto-link"
|
||||||
|
>
|
||||||
|
See all
|
||||||
|
<ChevronRightIcon class="featured-header-chevron" aria-hidden="true" />
|
||||||
|
</nuxt-link>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
<div
|
||||||
<div
|
v-for="version in featuredVersions"
|
||||||
v-if="
|
:key="version.id"
|
||||||
project.project_type !== 'resourcepack' &&
|
class="featured-version button-transparent"
|
||||||
project.project_type !== 'plugin' &&
|
@click="
|
||||||
project.project_type !== 'shader' &&
|
$router.push(
|
||||||
project.project_type !== 'datapack'
|
`/${project.project_type}/${
|
||||||
"
|
project.slug ? project.slug : project.id
|
||||||
class="info"
|
}/version/${encodeURI(version.displayUrlEnding)}`
|
||||||
>
|
)
|
||||||
<div class="key">Server side</div>
|
|
||||||
<div class="value">
|
|
||||||
{{ project.server_side }}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div class="info">
|
|
||||||
<div class="key">Project ID</div>
|
|
||||||
<div class="value lowercase">
|
|
||||||
<CopyCode :text="project.id" />
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div class="input-group">
|
|
||||||
<a
|
|
||||||
v-if="
|
|
||||||
config.public.apiBaseUrl.startsWith('https://api.modrinth.com') &&
|
|
||||||
config.public.siteUrl !== 'https://modrinth.com'
|
|
||||||
"
|
"
|
||||||
class="iconified-button"
|
|
||||||
:href="`https://modrinth.com/${project.project_type}/${
|
|
||||||
project.slug ? project.slug : project.id
|
|
||||||
}`"
|
|
||||||
rel="noopener nofollow"
|
|
||||||
target="_blank"
|
|
||||||
>
|
>
|
||||||
<ExternalIcon aria-hidden="true" />
|
<a
|
||||||
View on modrinth.com
|
v-tooltip="
|
||||||
</a>
|
version.primaryFile.filename + ' (' + $formatBytes(version.primaryFile.size) + ')'
|
||||||
<a
|
"
|
||||||
v-else-if="
|
:href="version.primaryFile.url"
|
||||||
config.public.apiBaseUrl.startsWith('https://staging-api.modrinth.com') &&
|
class="download square-button brand-button"
|
||||||
config.public.siteUrl !== 'https://staging.modrinth.com'
|
:aria-label="`Download ${version.name}`"
|
||||||
"
|
@click.stop="(event) => event.stopPropagation()"
|
||||||
class="iconified-button"
|
>
|
||||||
:href="`https://staging.modrinth.com/${project.project_type}/${
|
<DownloadIcon aria-hidden="true" />
|
||||||
project.slug ? project.slug : project.id
|
</a>
|
||||||
}`"
|
<div class="info">
|
||||||
rel="noopener nofollow"
|
<nuxt-link
|
||||||
target="_blank"
|
:to="`/${project.project_type}/${
|
||||||
|
project.slug ? project.slug : project.id
|
||||||
|
}/version/${encodeURI(version.displayUrlEnding)}`"
|
||||||
|
class="top"
|
||||||
|
>
|
||||||
|
{{ version.name }}
|
||||||
|
</nuxt-link>
|
||||||
|
<div v-if="version.game_versions.length > 0" class="game-version item">
|
||||||
|
{{ version.loaders.map((x) => $formatCategory(x)).join(', ') }}
|
||||||
|
{{ $formatVersion(version.game_versions) }}
|
||||||
|
</div>
|
||||||
|
<Badge v-if="version.version_type === 'release'" type="release" color="green" />
|
||||||
|
<Badge v-else-if="version.version_type === 'beta'" type="beta" color="orange" />
|
||||||
|
<Badge v-else-if="version.version_type === 'alpha'" type="alpha" color="red" />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<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"
|
||||||
|
class="team-member columns button-transparent"
|
||||||
|
:to="`/organization/${organization.slug}`"
|
||||||
>
|
>
|
||||||
<ExternalIcon aria-hidden="true" />
|
<Avatar :src="organization.icon_url" :alt="organization.name" size="sm" />
|
||||||
View on staging.modrinth.com
|
<div class="member-info">
|
||||||
</a>
|
<p class="name">
|
||||||
|
{{ organization.name }}
|
||||||
|
</p>
|
||||||
|
<p class="role"><OrganizationIcon /> Organization</p>
|
||||||
|
</div>
|
||||||
|
</nuxt-link>
|
||||||
|
<nuxt-link
|
||||||
|
v-for="member in members"
|
||||||
|
:key="member.user.id"
|
||||||
|
class="team-member columns button-transparent"
|
||||||
|
:to="'/user/' + member.user.username"
|
||||||
|
>
|
||||||
|
<Avatar :src="member.avatar_url" :alt="member.username" size="sm" circle />
|
||||||
|
|
||||||
|
<div class="member-info">
|
||||||
|
<p class="name">
|
||||||
|
{{ member.name }}
|
||||||
|
<CrownIcon v-if="member.is_owner" v-tooltip="'Project owner'" />
|
||||||
|
</p>
|
||||||
|
<p class="role">
|
||||||
|
{{ member.role }}
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</nuxt-link>
|
||||||
|
<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">
|
||||||
|
<div class="key">License</div>
|
||||||
|
<div class="value lowercase">
|
||||||
|
<a
|
||||||
|
v-if="project.license.url"
|
||||||
|
class="text-link"
|
||||||
|
:href="project.license.url"
|
||||||
|
rel="noopener nofollow ugc"
|
||||||
|
>
|
||||||
|
{{ licenseIdDisplay }}
|
||||||
|
</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.project_type !== 'resourcepack' &&
|
||||||
|
project.project_type !== 'plugin' &&
|
||||||
|
project.project_type !== 'shader' &&
|
||||||
|
project.project_type !== 'datapack'
|
||||||
|
"
|
||||||
|
class="info"
|
||||||
|
>
|
||||||
|
<div class="key">Client side</div>
|
||||||
|
<div class="value">
|
||||||
|
{{ project.client_side }}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div
|
||||||
|
v-if="
|
||||||
|
project.project_type !== 'resourcepack' &&
|
||||||
|
project.project_type !== 'plugin' &&
|
||||||
|
project.project_type !== 'shader' &&
|
||||||
|
project.project_type !== 'datapack'
|
||||||
|
"
|
||||||
|
class="info"
|
||||||
|
>
|
||||||
|
<div class="key">Server side</div>
|
||||||
|
<div class="value">
|
||||||
|
{{ project.server_side }}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="info">
|
||||||
|
<div class="key">Project ID</div>
|
||||||
|
<div class="value lowercase">
|
||||||
|
<CopyCode :text="project.id" />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="input-group">
|
||||||
|
<a
|
||||||
|
v-if="
|
||||||
|
config.public.apiBaseUrl.startsWith('https://api.modrinth.com') &&
|
||||||
|
config.public.siteUrl !== 'https://modrinth.com'
|
||||||
|
"
|
||||||
|
class="iconified-button"
|
||||||
|
:href="`https://modrinth.com/${project.project_type}/${
|
||||||
|
project.slug ? project.slug : project.id
|
||||||
|
}`"
|
||||||
|
rel="noopener nofollow"
|
||||||
|
target="_blank"
|
||||||
|
>
|
||||||
|
<ExternalIcon aria-hidden="true" />
|
||||||
|
View on modrinth.com
|
||||||
|
</a>
|
||||||
|
<a
|
||||||
|
v-else-if="
|
||||||
|
config.public.apiBaseUrl.startsWith('https://staging-api.modrinth.com') &&
|
||||||
|
config.public.siteUrl !== 'https://staging.modrinth.com'
|
||||||
|
"
|
||||||
|
class="iconified-button"
|
||||||
|
:href="`https://staging.modrinth.com/${project.project_type}/${
|
||||||
|
project.slug ? project.slug : project.id
|
||||||
|
}`"
|
||||||
|
rel="noopener nofollow"
|
||||||
|
target="_blank"
|
||||||
|
>
|
||||||
|
<ExternalIcon aria-hidden="true" />
|
||||||
|
View on staging.modrinth.com
|
||||||
|
</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>
|
||||||
</div>
|
</div>
|
||||||
@ -753,6 +1077,8 @@ import {
|
|||||||
isRejected,
|
isRejected,
|
||||||
isUnderReview,
|
isUnderReview,
|
||||||
isStaff,
|
isStaff,
|
||||||
|
CheckIcon,
|
||||||
|
XIcon,
|
||||||
} from 'omorphia'
|
} from 'omorphia'
|
||||||
import CrownIcon from '~/assets/images/utils/crown.svg'
|
import CrownIcon from '~/assets/images/utils/crown.svg'
|
||||||
import CalendarIcon from '~/assets/images/utils/calendar.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 CategoriesIcon from '~/assets/images/utils/tags.svg'
|
||||||
import DescriptionIcon from '~/assets/images/utils/align-left.svg'
|
import DescriptionIcon from '~/assets/images/utils/align-left.svg'
|
||||||
import LinksIcon from '~/assets/images/utils/link.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 GalleryIcon from '~/assets/images/utils/image.svg'
|
||||||
import VersionIcon from '~/assets/images/utils/version.svg'
|
import VersionIcon from '~/assets/images/utils/version.svg'
|
||||||
import { reportProject } from '~/utils/report-helpers.ts'
|
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 CollectionCreateModal from '~/components/ui/CollectionCreateModal.vue'
|
||||||
import OrganizationIcon from '~/assets/images/utils/organization.svg'
|
import OrganizationIcon from '~/assets/images/utils/organization.svg'
|
||||||
import ModerationChecklist from '~/components/ui/ModerationChecklist.vue'
|
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 data = useNuxtApp()
|
||||||
const route = useRoute()
|
const route = useRoute()
|
||||||
@ -808,6 +1137,7 @@ const auth = await useAuth()
|
|||||||
const user = await useUser()
|
const user = await useUser()
|
||||||
const cosmetics = useCosmetics()
|
const cosmetics = useCosmetics()
|
||||||
const tags = useTags()
|
const tags = useTags()
|
||||||
|
const flags = useFeatureFlags()
|
||||||
|
|
||||||
const displayCollectionsSearch = ref('')
|
const displayCollectionsSearch = ref('')
|
||||||
const collections = computed(() =>
|
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
|
// The rest of the members should be sorted by role, then by name
|
||||||
const members = computed(() => {
|
const members = computed(() => {
|
||||||
const acceptedMembers = allMembers.value.filter((x) => x.accepted)
|
const acceptedMembers = allMembers.value.filter((x) => x.accepted)
|
||||||
const owner = acceptedMembers.find((x) => x.is_owner)
|
const owner = acceptedMembers.find((x) =>
|
||||||
const rest = acceptedMembers.filter((x) => !x.is_owner) || []
|
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) => {
|
rest.sort((a, b) => {
|
||||||
if (a.role === b.role) {
|
if (a.role === b.role) {
|
||||||
@ -1173,10 +1509,15 @@ if (process.client && history && history.state && history.state.showChecklist) {
|
|||||||
showModerationChecklist.value = true
|
showModerationChecklist.value = true
|
||||||
futureProjects.value = history.state.projects
|
futureProjects.value = history.state.projects
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const showFeaturedVersions = computed(
|
||||||
|
() => !flags.value.removeFeaturedVersions && featuredVersions.value.length > 0
|
||||||
|
)
|
||||||
</script>
|
</script>
|
||||||
<style lang="scss" scoped>
|
<style lang="scss" scoped>
|
||||||
.header {
|
.header {
|
||||||
grid-area: header;
|
grid-area: header;
|
||||||
|
|
||||||
.title {
|
.title {
|
||||||
overflow-wrap: break-word;
|
overflow-wrap: break-word;
|
||||||
margin: var(--spacing-card-xs) 0;
|
margin: var(--spacing-card-xs) 0;
|
||||||
@ -1243,11 +1584,13 @@ if (process.client && history && history.state && history.state.showChecklist) {
|
|||||||
.project__gallery {
|
.project__gallery {
|
||||||
display: none;
|
display: none;
|
||||||
}
|
}
|
||||||
|
|
||||||
&.has-featured-image {
|
&.has-featured-image {
|
||||||
.project__gallery {
|
.project__gallery {
|
||||||
display: inline-block;
|
display: inline-block;
|
||||||
width: 100%;
|
width: 100%;
|
||||||
height: 10rem;
|
height: 10rem;
|
||||||
|
|
||||||
img {
|
img {
|
||||||
width: 100%;
|
width: 100%;
|
||||||
height: 10rem;
|
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;
|
border-radius: var(--size-rounded-card) var(--size-rounded-card) 0 0;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
.project__icon {
|
.project__icon {
|
||||||
margin-top: calc(-3rem - var(--spacing-card-lg) - 4px);
|
margin-top: calc(-3rem - var(--spacing-card-lg) - 4px);
|
||||||
margin-left: -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);
|
box-shadow: -2px -2px 0 2px var(--color-raised-bg), 2px -2px 0 2px var(--color-raised-bg);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
.project__header__content {
|
.project__header__content {
|
||||||
margin: 0;
|
margin: 0;
|
||||||
background: none;
|
background: none;
|
||||||
border-radius: unset;
|
border-radius: unset;
|
||||||
}
|
}
|
||||||
|
|
||||||
.input-group {
|
.input-group {
|
||||||
flex-wrap: nowrap;
|
flex-wrap: nowrap;
|
||||||
}
|
}
|
||||||
@ -1468,6 +1814,7 @@ if (process.client && history && history.state && history.state.showChecklist) {
|
|||||||
.modal-license {
|
.modal-license {
|
||||||
padding: var(--spacing-card-bg);
|
padding: var(--spacing-card-bg);
|
||||||
}
|
}
|
||||||
|
|
||||||
.settings-header {
|
.settings-header {
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: row;
|
flex-direction: row;
|
||||||
@ -1495,6 +1842,7 @@ if (process.client && history && history.state && history.state.showChecklist) {
|
|||||||
.popout-checkbox {
|
.popout-checkbox {
|
||||||
padding: var(--gap-sm) var(--gap-md);
|
padding: var(--gap-sm) var(--gap-md);
|
||||||
white-space: nowrap;
|
white-space: nowrap;
|
||||||
|
|
||||||
&:hover {
|
&:hover {
|
||||||
filter: brightness(0.95);
|
filter: brightness(0.95);
|
||||||
}
|
}
|
||||||
@ -1531,4 +1879,12 @@ if (process.client && history && history.state && history.state.showChecklist) {
|
|||||||
margin: var(--gap-sm) var(--gap-md);
|
margin: var(--gap-sm) var(--gap-md);
|
||||||
padding: var(--gap-sm);
|
padding: var(--gap-sm);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.normal-page__info:empty {
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.project-owner-icon {
|
||||||
|
color: var(--color-orange);
|
||||||
|
}
|
||||||
</style>
|
</style>
|
||||||
|
|||||||
@ -157,18 +157,18 @@ function switchPage(page) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
.changelog-bar {
|
.changelog-bar {
|
||||||
--color: var(--color-special-green);
|
--color: var(--color-green);
|
||||||
|
|
||||||
&.alpha {
|
&.alpha {
|
||||||
--color: var(--color-special-red);
|
--color: var(--color-red);
|
||||||
}
|
}
|
||||||
|
|
||||||
&.release {
|
&.release {
|
||||||
--color: var(--color-special-green);
|
--color: var(--color-green);
|
||||||
}
|
}
|
||||||
|
|
||||||
&.beta {
|
&.beta {
|
||||||
--color: var(--color-special-orange);
|
--color: var(--color-orange);
|
||||||
}
|
}
|
||||||
|
|
||||||
left: 0;
|
left: 0;
|
||||||
|
|||||||
@ -204,12 +204,12 @@ svg {
|
|||||||
}
|
}
|
||||||
|
|
||||||
&.bad {
|
&.bad {
|
||||||
color: var(--color-special-red);
|
color: var(--color-red);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
.warning {
|
.warning {
|
||||||
color: var(--color-special-orange);
|
color: var(--color-orange);
|
||||||
font-weight: bold;
|
font-weight: bold;
|
||||||
}
|
}
|
||||||
</style>
|
</style>
|
||||||
|
|||||||
@ -421,11 +421,11 @@ svg {
|
|||||||
}
|
}
|
||||||
|
|
||||||
&.bad {
|
&.bad {
|
||||||
color: var(--color-special-red);
|
color: var(--color-red);
|
||||||
}
|
}
|
||||||
|
|
||||||
&.warn {
|
&.warn {
|
||||||
color: var(--color-special-orange);
|
color: var(--color-orange);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@ -519,7 +519,6 @@
|
|||||||
<script setup>
|
<script setup>
|
||||||
import { Multiselect } from 'vue-multiselect'
|
import { Multiselect } from 'vue-multiselect'
|
||||||
import { Avatar, Badge, Card, Checkbox, TransferIcon, CheckIcon, UsersIcon } from 'omorphia'
|
import { Avatar, Badge, Card, Checkbox, TransferIcon, CheckIcon, UsersIcon } from 'omorphia'
|
||||||
import { defineProps, ref, watch } from 'vue'
|
|
||||||
|
|
||||||
import ModalConfirm from '~/components/ui/ModalConfirm.vue'
|
import ModalConfirm from '~/components/ui/ModalConfirm.vue'
|
||||||
import DropdownIcon from '~/assets/images/utils/dropdown.svg'
|
import DropdownIcon from '~/assets/images/utils/dropdown.svg'
|
||||||
|
|||||||
@ -1374,7 +1374,7 @@ useSeoMeta({
|
|||||||
display: flex;
|
display: flex;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
padding: var(--gap-sm) 0;
|
padding: var(--gap-sm) 0;
|
||||||
color: var(--color-special-gray);
|
color: var(--color-gray);
|
||||||
|
|
||||||
&.important {
|
&.important {
|
||||||
color: var(--color-contrast);
|
color: var(--color-contrast);
|
||||||
|
|||||||
@ -617,7 +617,7 @@ export default defineNuxtComponent({
|
|||||||
gap: var(--spacing-card-xs);
|
gap: var(--spacing-card-xs);
|
||||||
|
|
||||||
svg {
|
svg {
|
||||||
color: var(--color-special-orange);
|
color: var(--color-orange);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -645,7 +645,7 @@ export default defineNuxtComponent({
|
|||||||
}
|
}
|
||||||
|
|
||||||
.label-button[data-active='true'] {
|
.label-button[data-active='true'] {
|
||||||
--background-color: var(--color-special-red);
|
--background-color: var(--color-red);
|
||||||
--text-color: var(--color-brand-inverted);
|
--text-color: var(--color-brand-inverted);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@ -506,7 +506,7 @@ async function withdraw() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
.invalid {
|
.invalid {
|
||||||
color: var(--color-special-red);
|
color: var(--color-red);
|
||||||
}
|
}
|
||||||
|
|
||||||
.confirm-text {
|
.confirm-text {
|
||||||
|
|||||||
63
pages/flags.vue
Normal file
63
pages/flags.vue
Normal 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>
|
||||||
@ -233,11 +233,11 @@ async function goToProjects() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
.warning {
|
.warning {
|
||||||
color: var(--color-special-orange);
|
color: var(--color-orange);
|
||||||
}
|
}
|
||||||
|
|
||||||
.danger {
|
.danger {
|
||||||
color: var(--color-special-red);
|
color: var(--color-red);
|
||||||
font-weight: bold;
|
font-weight: bold;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@ -630,7 +630,7 @@ const onBulkEditLinks = useClientTry(async () => {
|
|||||||
gap: var(--spacing-card-xs);
|
gap: var(--spacing-card-xs);
|
||||||
|
|
||||||
svg {
|
svg {
|
||||||
color: var(--color-special-orange);
|
color: var(--color-orange);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -658,7 +658,7 @@ const onBulkEditLinks = useClientTry(async () => {
|
|||||||
}
|
}
|
||||||
|
|
||||||
.label-button[data-active='true'] {
|
.label-button[data-active='true'] {
|
||||||
--background-color: var(--color-special-red);
|
--background-color: var(--color-red);
|
||||||
--text-color: var(--color-brand-inverted);
|
--text-color: var(--color-brand-inverted);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@ -1,6 +1,6 @@
|
|||||||
<template>
|
<template>
|
||||||
<div>
|
<div>
|
||||||
<MessageBanner v-if="cosmetics.developerMode" message-type="warning" class="developer-message">
|
<MessageBanner v-if="flags.developerMode" message-type="warning" class="developer-message">
|
||||||
<CodeIcon />
|
<CodeIcon />
|
||||||
<IntlFormatted :message-id="developerModeBanner.description">
|
<IntlFormatted :message-id="developerModeBanner.description">
|
||||||
<template #strong="{ children }">
|
<template #strong="{ children }">
|
||||||
@ -137,15 +137,15 @@
|
|||||||
</div>
|
</div>
|
||||||
</section>
|
</section>
|
||||||
<section class="universal-card">
|
<section class="universal-card">
|
||||||
<h2>{{ formatMessage(featureFlags.title) }}</h2>
|
<h2>{{ formatMessage(toggleFeatures.title) }}</h2>
|
||||||
<p>{{ formatMessage(featureFlags.description) }}</p>
|
<p>{{ formatMessage(toggleFeatures.description) }}</p>
|
||||||
<div class="adjacent-input small">
|
<div class="adjacent-input small">
|
||||||
<label for="advanced-rendering">
|
<label for="advanced-rendering">
|
||||||
<span class="label__title">
|
<span class="label__title">
|
||||||
{{ formatMessage(featureFlags.advancedRenderingTitle) }}
|
{{ formatMessage(toggleFeatures.advancedRenderingTitle) }}
|
||||||
</span>
|
</span>
|
||||||
<span class="label__description">
|
<span class="label__description">
|
||||||
{{ formatMessage(featureFlags.advancedRenderingDescription) }}
|
{{ formatMessage(toggleFeatures.advancedRenderingDescription) }}
|
||||||
</span>
|
</span>
|
||||||
</label>
|
</label>
|
||||||
<input
|
<input
|
||||||
@ -159,10 +159,10 @@
|
|||||||
<div class="adjacent-input small">
|
<div class="adjacent-input small">
|
||||||
<label for="external-links-new-tab">
|
<label for="external-links-new-tab">
|
||||||
<span class="label__title">
|
<span class="label__title">
|
||||||
{{ formatMessage(featureFlags.externalLinksNewTabTitle) }}
|
{{ formatMessage(toggleFeatures.externalLinksNewTabTitle) }}
|
||||||
</span>
|
</span>
|
||||||
<span class="label__description">
|
<span class="label__description">
|
||||||
{{ formatMessage(featureFlags.externalLinksNewTabDescription) }}
|
{{ formatMessage(toggleFeatures.externalLinksNewTabDescription) }}
|
||||||
</span>
|
</span>
|
||||||
</label>
|
</label>
|
||||||
<input
|
<input
|
||||||
@ -176,10 +176,10 @@
|
|||||||
<div class="adjacent-input small">
|
<div class="adjacent-input small">
|
||||||
<label for="modrinth-app-promos">
|
<label for="modrinth-app-promos">
|
||||||
<span class="label__title">
|
<span class="label__title">
|
||||||
{{ formatMessage(featureFlags.hideModrinthAppPromosTitle) }}
|
{{ formatMessage(toggleFeatures.hideModrinthAppPromosTitle) }}
|
||||||
</span>
|
</span>
|
||||||
<span class="label__description">
|
<span class="label__description">
|
||||||
{{ formatMessage(featureFlags.hideModrinthAppPromosDescription) }}
|
{{ formatMessage(toggleFeatures.hideModrinthAppPromosDescription) }}
|
||||||
</span>
|
</span>
|
||||||
</label>
|
</label>
|
||||||
<input
|
<input
|
||||||
@ -193,10 +193,10 @@
|
|||||||
<div class="adjacent-input small">
|
<div class="adjacent-input small">
|
||||||
<label for="search-layout-toggle">
|
<label for="search-layout-toggle">
|
||||||
<span class="label__title">
|
<span class="label__title">
|
||||||
{{ formatMessage(featureFlags.rightAlignedSearchSidebarTitle) }}
|
{{ formatMessage(toggleFeatures.rightAlignedSearchSidebarTitle) }}
|
||||||
</span>
|
</span>
|
||||||
<span class="label__description">
|
<span class="label__description">
|
||||||
{{ formatMessage(featureFlags.rightAlignedSearchSidebarDescription) }}
|
{{ formatMessage(toggleFeatures.rightAlignedSearchSidebarDescription) }}
|
||||||
</span>
|
</span>
|
||||||
</label>
|
</label>
|
||||||
<input
|
<input
|
||||||
@ -210,10 +210,10 @@
|
|||||||
<div class="adjacent-input small">
|
<div class="adjacent-input small">
|
||||||
<label for="project-layout-toggle">
|
<label for="project-layout-toggle">
|
||||||
<span class="label__title">
|
<span class="label__title">
|
||||||
{{ formatMessage(featureFlags.rightAlignedProjectSidebarTitle) }}
|
{{ formatMessage(toggleFeatures.rightAlignedProjectSidebarTitle) }}
|
||||||
</span>
|
</span>
|
||||||
<span class="label__description">
|
<span class="label__description">
|
||||||
{{ formatMessage(featureFlags.rightAlignedProjectSidebarDescription) }}
|
{{ formatMessage(toggleFeatures.rightAlignedProjectSidebarDescription) }}
|
||||||
</span>
|
</span>
|
||||||
</label>
|
</label>
|
||||||
<input
|
<input
|
||||||
@ -331,10 +331,10 @@ const projectListLayouts = defineMessages({
|
|||||||
},
|
},
|
||||||
})
|
})
|
||||||
|
|
||||||
const featureFlags = defineMessages({
|
const toggleFeatures = defineMessages({
|
||||||
title: {
|
title: {
|
||||||
id: 'settings.display.flags.title',
|
id: 'settings.display.flags.title',
|
||||||
defaultMessage: 'Feature flags',
|
defaultMessage: 'Toggle features',
|
||||||
},
|
},
|
||||||
description: {
|
description: {
|
||||||
id: 'settings.display.flags.description',
|
id: 'settings.display.flags.description',
|
||||||
@ -386,6 +386,7 @@ const featureFlags = defineMessages({
|
|||||||
})
|
})
|
||||||
|
|
||||||
const cosmetics = useCosmetics()
|
const cosmetics = useCosmetics()
|
||||||
|
const flags = useFeatureFlags()
|
||||||
const tags = useTags()
|
const tags = useTags()
|
||||||
|
|
||||||
const systemTheme = ref('light')
|
const systemTheme = ref('light')
|
||||||
@ -394,7 +395,7 @@ const theme = useTheme()
|
|||||||
|
|
||||||
const themeOptions = computed(() => {
|
const themeOptions = computed(() => {
|
||||||
const options = ['system', 'light', 'dark', 'oled']
|
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')
|
options.push('retro')
|
||||||
}
|
}
|
||||||
return options
|
return options
|
||||||
@ -430,8 +431,8 @@ function updateColorTheme(value) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
function disableDeveloperMode() {
|
function disableDeveloperMode() {
|
||||||
cosmetics.value.developerMode = !cosmetics.value.developerMode
|
flags.value.developerMode = !flags.value.developerMode
|
||||||
saveCosmetics()
|
saveFeatureFlags()
|
||||||
addNotification({
|
addNotification({
|
||||||
group: 'main',
|
group: 'main',
|
||||||
title: 'Developer mode deactivated',
|
title: 'Developer mode deactivated',
|
||||||
|
|||||||
@ -414,10 +414,10 @@ function getItemLabel(locale: Locale) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
&.errored {
|
&.errored {
|
||||||
border-color: var(--color-special-red);
|
border-color: var(--color-red);
|
||||||
|
|
||||||
&:hover {
|
&:hover {
|
||||||
border-color: var(--color-special-red);
|
border-color: var(--color-red);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -473,7 +473,7 @@ function getItemLabel(locale: Locale) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
.language-load-error {
|
.language-load-error {
|
||||||
color: var(--color-special-red);
|
color: var(--color-red);
|
||||||
font-size: var(--font-size-sm);
|
font-size: var(--font-size-sm);
|
||||||
margin-left: 0.3rem;
|
margin-left: 0.3rem;
|
||||||
display: flex;
|
display: flex;
|
||||||
|
|||||||
@ -502,7 +502,7 @@ export default defineNuxtComponent({
|
|||||||
// 5 wide
|
// 5 wide
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-wrap: wrap;
|
flex-wrap: wrap;
|
||||||
justify-content: start;
|
justify-content: flex-start;
|
||||||
|
|
||||||
grid-gap: var(--gap-sm);
|
grid-gap: var(--gap-sm);
|
||||||
margin-top: 0.5rem;
|
margin-top: 0.5rem;
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user