Knossos Catch-up (#58)
* Knossos Catch-up * bump version * fix build * fix build again * Fix lint * downgrade pnpm * fix modals * fix btn style * add missing styles
This commit is contained in:
parent
bbb2539ace
commit
1db59fc052
28
.github/workflows/build.yml
vendored
28
.github/workflows/build.yml
vendored
@ -16,18 +16,26 @@ jobs:
|
||||
uses: actions/setup-node@v3
|
||||
with:
|
||||
node-version: 18.x
|
||||
- name: Get yarn cache
|
||||
id: yarn-cache
|
||||
run: echo "dir=$(yarn cache dir)" >> $GITHUB_OUTPUT
|
||||
- uses: actions/cache@v3
|
||||
- name: Install pnpm via corepack
|
||||
shell: bash
|
||||
run: |
|
||||
corepack enable
|
||||
corepack prepare --activate
|
||||
- name: Get pnpm store directory
|
||||
id: pnpm-cache
|
||||
shell: bash
|
||||
run: |
|
||||
echo "STORE_PATH=$(pnpm store path)" >> $GITHUB_OUTPUT
|
||||
- name: Setup pnpm cache
|
||||
uses: actions/cache@v3
|
||||
with:
|
||||
path: ${{ steps.yarn-cache.outputs.dir }}
|
||||
key: ${{ runner.os }}-yarn-${{ hashFiles('**/yarn.lock') }}
|
||||
path: ${{ steps.pnpm-cache.outputs.STORE_PATH }}
|
||||
key: ${{ runner.os }}-pnpm-store-${{ hashFiles('**/pnpm-lock.yaml') }}
|
||||
restore-keys: |
|
||||
${{ runner.os }}-yarn-
|
||||
${{ runner.os }}-pnpm-store-
|
||||
- name: Install dependencies
|
||||
run: yarn install --immutable --immutable-cache --check-cache
|
||||
run: pnpm install
|
||||
- name: Run Lint
|
||||
run: npm run lint
|
||||
run: pnpm lint
|
||||
- name: Build
|
||||
run: npm run build
|
||||
run: pnpm build
|
||||
|
||||
29
.github/workflows/publish.yml
vendored
29
.github/workflows/publish.yml
vendored
@ -17,15 +17,28 @@ jobs:
|
||||
uses: actions/setup-node@v3
|
||||
with:
|
||||
node-version: 18.x
|
||||
registry-url: https://registry.npmjs.org/
|
||||
|
||||
- name: Install pnpm via corepack
|
||||
shell: bash
|
||||
run: |
|
||||
corepack enable
|
||||
corepack prepare --activate
|
||||
- name: Get pnpm store directory
|
||||
id: pnpm-cache
|
||||
shell: bash
|
||||
run: |
|
||||
echo "STORE_PATH=$(pnpm store path)" >> $GITHUB_OUTPUT
|
||||
- name: Setup pnpm cache
|
||||
uses: actions/cache@v3
|
||||
with:
|
||||
path: ${{ steps.pnpm-cache.outputs.STORE_PATH }}
|
||||
key: ${{ runner.os }}-pnpm-store-${{ hashFiles('**/pnpm-lock.yaml') }}
|
||||
restore-keys: |
|
||||
${{ runner.os }}-pnpm-store-
|
||||
- name: Install dependencies
|
||||
run: yarn install --immutable --immutable-cache --check-cache
|
||||
|
||||
- name: Build package
|
||||
run: npm run build
|
||||
|
||||
run: pnpm install
|
||||
- name: Build
|
||||
run: pnpm build
|
||||
- name: Publish package
|
||||
run: npm publish
|
||||
run: pnpm publish
|
||||
env:
|
||||
NODE_AUTH_TOKEN: ${{ secrets.NPM_TOKEN }}
|
||||
|
||||
@ -2,13 +2,23 @@
|
||||
:::raw
|
||||
|
||||
<DemoContainer>
|
||||
<Button :action="() => this.$refs.reportModal.modal.show()">Show Modal</Button>
|
||||
<Button :action="() => this.$refs.reportModal.show()">Show Report Modal</Button>
|
||||
<Button :action="() => this.$refs.confirmModal.show()">Show Confirm Modal</Button>
|
||||
<ModalReport
|
||||
ref="reportModal"
|
||||
itemType="project"
|
||||
:reportTypes="['cringitude', 'rudeness', 'notgamer', 'windowsuser']"
|
||||
>
|
||||
</ModalReport>
|
||||
<ModalConfirm
|
||||
ref="confirmModal"
|
||||
title="Are you sure you want to delete this version?"
|
||||
description="This will remove this version forever (like really forever)."
|
||||
:has-to-type="true"
|
||||
proceed-label="Delete"
|
||||
confirmationText="Hello"
|
||||
>
|
||||
</ModalConfirm>
|
||||
</DemoContainer>
|
||||
:::
|
||||
|
||||
|
||||
5
lib/assets/icons/history.svg
Normal file
5
lib/assets/icons/history.svg
Normal file
@ -0,0 +1,5 @@
|
||||
<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">
|
||||
<path d="M3 3v5h5"></path>
|
||||
<path d="M3.05 13A9 9 0 1 0 6 5.3L3 8"></path>
|
||||
<path d="M12 7v5l4 2"></path>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 299 B |
@ -13,3 +13,6 @@
|
||||
|
||||
// Finally, apply accessibility-related global styling
|
||||
@import 'styles/accessibility';
|
||||
|
||||
// Fonts
|
||||
@import 'styles/inter';
|
||||
|
||||
@ -160,6 +160,14 @@
|
||||
margin-bottom: var(--gap-md);
|
||||
}
|
||||
}
|
||||
|
||||
ul,
|
||||
ol {
|
||||
ul,
|
||||
ol {
|
||||
margin-bottom: 0;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.input-group {
|
||||
@ -965,9 +973,10 @@ select {
|
||||
.multiselect__placeholder {
|
||||
color: var(--color-base);
|
||||
margin-left: 0.5rem;
|
||||
margin-bottom: 8px;
|
||||
opacity: 0.6;
|
||||
font-size: 1rem;
|
||||
line-height: 1.25rem;
|
||||
line-height: 16px;
|
||||
}
|
||||
}
|
||||
|
||||
@ -1016,3 +1025,38 @@ select {
|
||||
border-top-left-radius: var(--radius-md) !important;
|
||||
border-top-right-radius: var(--radius-md) !important;
|
||||
}
|
||||
|
||||
.input-group {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
grid-gap: var(--gap-sm);
|
||||
flex-wrap: wrap;
|
||||
max-width: 100%;
|
||||
align-items: center;
|
||||
|
||||
.multiselect {
|
||||
width: 15rem;
|
||||
}
|
||||
|
||||
input {
|
||||
flex-shrink: 2;
|
||||
}
|
||||
|
||||
&.shrink-first {
|
||||
:first-child {
|
||||
flex-shrink: 2;
|
||||
flex-grow: 1;
|
||||
flex-basis: min-content;
|
||||
}
|
||||
|
||||
:not(:first-child) {
|
||||
flex-shrink: 1;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.push-right:not(.input-group),
|
||||
.push-right.input-group > :first-child {
|
||||
margin-left: auto;
|
||||
margin-right: 0;
|
||||
}
|
||||
|
||||
40
lib/assets/styles/inter.scss
Normal file
40
lib/assets/styles/inter.scss
Normal file
@ -0,0 +1,40 @@
|
||||
@font-face {
|
||||
font-family: inter;
|
||||
font-style: normal;
|
||||
font-weight: 400;
|
||||
font-display: swap;
|
||||
src: url('https://cdn-raw.modrinth.com/fonts/inter/Inter-Regular.woff2?v=3.19') format('woff2'),
|
||||
url('https://cdn-raw.modrinth.com/fonts/inter/Inter-Regular.woff?v=3.19') format('woff');
|
||||
}
|
||||
@font-face {
|
||||
font-family: inter;
|
||||
font-style: normal;
|
||||
font-weight: 500;
|
||||
font-display: swap;
|
||||
src: url('https://cdn-raw.modrinth.com/fonts/inter/Inter-Medium.woff2?v=3.19') format('woff2'),
|
||||
url('https://cdn-raw.modrinth.com/fonts/inter/Inter-Medium.woff?v=3.19') format('woff');
|
||||
}
|
||||
@font-face {
|
||||
font-family: inter;
|
||||
font-style: normal;
|
||||
font-weight: 600;
|
||||
font-display: swap;
|
||||
src: url('https://cdn-raw.modrinth.com/fonts/inter/Inter-SemiBold.woff2?v=3.19') format('woff2'),
|
||||
url('https://cdn-raw.modrinth.com/fonts/inter/Inter-SemiBold.woff?v=3.19') format('woff');
|
||||
}
|
||||
@font-face {
|
||||
font-family: inter;
|
||||
font-style: normal;
|
||||
font-weight: 700;
|
||||
font-display: swap;
|
||||
src: url('https://cdn-raw.modrinth.com/fonts/inter/Inter-Bold.woff2?v=3.19') format('woff2'),
|
||||
url('https://cdn-raw.modrinth.com/fonts/inter/Inter-Bold.woff?v=3.19') format('woff');
|
||||
}
|
||||
@font-face {
|
||||
font-family: inter;
|
||||
font-style: normal;
|
||||
font-weight: 800;
|
||||
font-display: swap;
|
||||
src: url('https://cdn-raw.modrinth.com/fonts/inter/Inter-ExtraBold.woff2?v=3.19') format('woff2'),
|
||||
url('https://cdn-raw.modrinth.com/fonts/inter/Inter-ExtraBold.woff?v=3.19') format('woff');
|
||||
}
|
||||
@ -2,10 +2,13 @@
|
||||
<img
|
||||
v-if="src"
|
||||
ref="img"
|
||||
:class="`avatar size-${size} ${circle ? 'circle' : ''} ${noShadow ? 'no-shadow' : ''}`"
|
||||
:class="`avatar size-${size} ${circle ? 'circle' : ''} ${noShadow ? 'no-shadow' : ''} ${
|
||||
pixelated ? 'pixelated' : ''
|
||||
}`"
|
||||
:src="src"
|
||||
:alt="alt"
|
||||
:loading="loading"
|
||||
@load="updatePixelated"
|
||||
/>
|
||||
<svg
|
||||
v-else
|
||||
@ -60,21 +63,20 @@ export default {
|
||||
default: 'eager',
|
||||
},
|
||||
},
|
||||
mounted() {
|
||||
if (this.$refs.img && this.$refs.img.naturalWidth) {
|
||||
const isPixelated = () => {
|
||||
if (this.$refs.img.naturalWidth < 96 && this.$refs.img.naturalWidth > 0) {
|
||||
this.$refs.img.style.imageRendering = 'pixelated'
|
||||
}
|
||||
}
|
||||
|
||||
if (this.$refs.img.naturalWidth) {
|
||||
isPixelated()
|
||||
} else {
|
||||
this.$refs.img.onload = isPixelated
|
||||
}
|
||||
data() {
|
||||
return {
|
||||
pixelated: false,
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
updatePixelated() {
|
||||
if (this.$refs.img && this.$refs.img.naturalWidth && this.$refs.img.naturalWidth <= 96) {
|
||||
this.pixelated = true
|
||||
} else {
|
||||
this.pixelated = false
|
||||
}
|
||||
},
|
||||
},
|
||||
}
|
||||
</script>
|
||||
|
||||
@ -122,5 +124,9 @@ export default {
|
||||
&.no-shadow {
|
||||
box-shadow: none;
|
||||
}
|
||||
|
||||
&.pixelated {
|
||||
image-rendering: pixelated;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
|
||||
@ -91,6 +91,7 @@ defineProps({
|
||||
|
||||
&.type--accepted,
|
||||
&.type--admin,
|
||||
&.type--success,
|
||||
&.green {
|
||||
--badge-color: var(--color-green);
|
||||
}
|
||||
@ -98,12 +99,12 @@ defineProps({
|
||||
&.type--creator,
|
||||
&.type--approved,
|
||||
&.blue {
|
||||
color: var(--color-blue);
|
||||
--badge-color: var(--color-blue);
|
||||
}
|
||||
|
||||
&.type--unlisted,
|
||||
&.purple {
|
||||
color: var(--color-purple);
|
||||
--badge-color: var(--color-purple);
|
||||
}
|
||||
|
||||
&.type--private,
|
||||
|
||||
51
lib/components/base/Breadcrumbs.vue
Normal file
51
lib/components/base/Breadcrumbs.vue
Normal file
@ -0,0 +1,51 @@
|
||||
<template>
|
||||
<nav class="breadcrumbs">
|
||||
<template v-for="(link, index) in linkStack" :key="index">
|
||||
<RouterLink
|
||||
:to="link.href"
|
||||
class="breadcrumb goto-link"
|
||||
:class="{ trim: link.allowTrimming ? link.allowTrimming : false }"
|
||||
>
|
||||
{{ link.label }}
|
||||
</RouterLink>
|
||||
<ChevronRightIcon />
|
||||
</template>
|
||||
<span class="breadcrumb">{{ currentTitle }}</span>
|
||||
</nav>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { ChevronRightIcon } from '@/components'
|
||||
defineProps({
|
||||
linkStack: {
|
||||
type: Array,
|
||||
default: () => [],
|
||||
},
|
||||
currentTitle: {
|
||||
type: String,
|
||||
required: true,
|
||||
},
|
||||
})
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
.breadcrumbs {
|
||||
display: flex;
|
||||
margin-bottom: var(--gap-lg);
|
||||
align-items: center;
|
||||
flex-wrap: wrap;
|
||||
|
||||
svg {
|
||||
width: 1.25rem;
|
||||
height: 1.25rem;
|
||||
}
|
||||
|
||||
a.breadcrumb {
|
||||
padding-block: var(--gap-xs);
|
||||
&.trim {
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
}
|
||||
}
|
||||
}
|
||||
</style>
|
||||
@ -2,7 +2,13 @@
|
||||
<label :class="{ 'long-style': longStyle }" @drop.prevent="handleDrop" @dragover.prevent>
|
||||
<slot />
|
||||
{{ prompt }}
|
||||
<input type="file" :multiple="multiple" :accept="accept" @change="handleChange" />
|
||||
<input
|
||||
type="file"
|
||||
:multiple="multiple"
|
||||
:accept="accept"
|
||||
:disabled="disabled"
|
||||
@change="handleChange"
|
||||
/>
|
||||
</label>
|
||||
</template>
|
||||
|
||||
@ -42,6 +48,10 @@ export default defineComponent({
|
||||
type: Boolean,
|
||||
default: false,
|
||||
},
|
||||
disabled: {
|
||||
type: Boolean,
|
||||
default: false,
|
||||
},
|
||||
},
|
||||
emits: ['change'],
|
||||
data() {
|
||||
|
||||
@ -1,58 +1,66 @@
|
||||
<template>
|
||||
<div>
|
||||
<div v-if="shown">
|
||||
<div
|
||||
:class="{
|
||||
shown: shown,
|
||||
noblur: noblur,
|
||||
shown: actuallyShown,
|
||||
noblur: props.noblur,
|
||||
}"
|
||||
class="modal-overlay"
|
||||
@click="hide"
|
||||
/>
|
||||
<div class="modal-body" :class="{ shown: shown }">
|
||||
<div v-if="header" class="header">
|
||||
<h1>{{ header }}</h1>
|
||||
<button class="btn icon-only transparent" @click="hide">
|
||||
<XIcon />
|
||||
</button>
|
||||
</div>
|
||||
<div class="content">
|
||||
<slot />
|
||||
<div class="modal-container" :class="{ shown: actuallyShown }">
|
||||
<div class="modal-body">
|
||||
<div v-if="props.header" class="header">
|
||||
<h1>{{ props.header }}</h1>
|
||||
<button class="btn icon-only transparent" @click="hide">
|
||||
<XIcon />
|
||||
</button>
|
||||
</div>
|
||||
<div class="content">
|
||||
<slot />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div v-else></div>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { XIcon } from '@/components'
|
||||
</script>
|
||||
<script>
|
||||
import { defineComponent } from 'vue'
|
||||
import { ref } from 'vue'
|
||||
|
||||
export default defineComponent({
|
||||
props: {
|
||||
header: {
|
||||
type: String,
|
||||
default: null,
|
||||
},
|
||||
noblur: {
|
||||
type: Boolean,
|
||||
default: false,
|
||||
},
|
||||
const props = defineProps({
|
||||
header: {
|
||||
type: String,
|
||||
default: null,
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
shown: false,
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
show() {
|
||||
this.shown = true
|
||||
},
|
||||
hide() {
|
||||
this.shown = false
|
||||
},
|
||||
noblur: {
|
||||
type: Boolean,
|
||||
default: false,
|
||||
},
|
||||
})
|
||||
|
||||
const shown = ref(false)
|
||||
const actuallyShown = ref(false)
|
||||
|
||||
function show() {
|
||||
shown.value = true
|
||||
setTimeout(() => {
|
||||
actuallyShown.value = true
|
||||
}, 50)
|
||||
}
|
||||
|
||||
function hide() {
|
||||
actuallyShown.value = false
|
||||
setTimeout(() => {
|
||||
shown.value = false
|
||||
}, 300)
|
||||
}
|
||||
|
||||
defineExpose({
|
||||
show,
|
||||
hide,
|
||||
})
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
@ -64,7 +72,6 @@ export default defineComponent({
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
z-index: 20;
|
||||
|
||||
transition: all 0.3s ease-in-out;
|
||||
|
||||
&.shown {
|
||||
@ -79,48 +86,62 @@ export default defineComponent({
|
||||
}
|
||||
}
|
||||
|
||||
.modal-body {
|
||||
.modal-container {
|
||||
position: fixed;
|
||||
left: 50%;
|
||||
transform: translate(-50%, -50%);
|
||||
top: 0;
|
||||
left: 0;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
z-index: 21;
|
||||
box-shadow: var(--shadow-raised), var(--shadow-inset);
|
||||
border-radius: var(--radius-lg);
|
||||
max-height: calc(100% - 2 * var(--gap-lg));
|
||||
overflow-y: auto;
|
||||
width: 600px;
|
||||
visibility: hidden;
|
||||
pointer-events: none;
|
||||
|
||||
.header {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
background-color: var(--color-bg);
|
||||
padding: var(--gap-md) var(--gap-lg);
|
||||
|
||||
h1 {
|
||||
font-size: 1.25rem;
|
||||
color: var(--color-contrast);
|
||||
font-weight: bolder;
|
||||
&.shown {
|
||||
visibility: visible;
|
||||
.modal-body {
|
||||
opacity: 1;
|
||||
visibility: visible;
|
||||
transform: translateY(0);
|
||||
}
|
||||
}
|
||||
|
||||
.content {
|
||||
background-color: var(--color-raised-bg);
|
||||
}
|
||||
.modal-body {
|
||||
position: fixed;
|
||||
box-shadow: var(--shadow-raised), var(--shadow-inset);
|
||||
border-radius: var(--radius-lg);
|
||||
max-height: calc(100% - 2 * var(--gap-lg));
|
||||
overflow-y: auto;
|
||||
width: 600px;
|
||||
pointer-events: auto;
|
||||
|
||||
top: calc(100% + 400px);
|
||||
visibility: hidden;
|
||||
opacity: 0;
|
||||
transition: all 0.25s ease-in-out;
|
||||
.header {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
background-color: var(--color-bg);
|
||||
padding: var(--gap-md) var(--gap-lg);
|
||||
|
||||
&.shown {
|
||||
opacity: 1;
|
||||
visibility: visible;
|
||||
top: 50%;
|
||||
}
|
||||
h1 {
|
||||
font-weight: bold;
|
||||
font-size: 1.25rem;
|
||||
}
|
||||
}
|
||||
|
||||
@media screen and (max-width: 650px) {
|
||||
width: calc(100% - 2 * var(--gap-lg));
|
||||
.content {
|
||||
background-color: var(--color-raised-bg);
|
||||
}
|
||||
|
||||
transform: translateY(50vh);
|
||||
visibility: hidden;
|
||||
opacity: 0;
|
||||
transition: all 0.25s ease-in-out;
|
||||
|
||||
@media screen and (max-width: 650px) {
|
||||
width: calc(100% - 2 * var(--gap-lg));
|
||||
}
|
||||
}
|
||||
}
|
||||
</style>
|
||||
|
||||
122
lib/components/base/ModalConfirm.vue
Normal file
122
lib/components/base/ModalConfirm.vue
Normal file
@ -0,0 +1,122 @@
|
||||
<template>
|
||||
<Modal ref="modal" :header="props.title">
|
||||
<div class="modal-delete">
|
||||
<div class="markdown-body" v-html="renderString(props.description)" />
|
||||
<label v-if="props.hasToType" for="confirmation" class="confirmation-label">
|
||||
<span>
|
||||
<strong>To verify, type</strong>
|
||||
<em class="confirmation-text">{{ props.confirmationText }}</em>
|
||||
<strong>below:</strong>
|
||||
</span>
|
||||
</label>
|
||||
<div class="confirmation-input">
|
||||
<input
|
||||
v-if="props.hasToType"
|
||||
id="confirmation"
|
||||
v-model="confirmation_typed"
|
||||
type="text"
|
||||
placeholder="Type here..."
|
||||
@input="type"
|
||||
/>
|
||||
</div>
|
||||
<div class="input-group push-right">
|
||||
<button class="btn" @click="modal.hide()">
|
||||
<XIcon />
|
||||
Cancel
|
||||
</button>
|
||||
<button class="btn btn-danger" :disabled="action_disabled" @click="proceed">
|
||||
<TrashIcon />
|
||||
{{ props.proceedLabel }}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</Modal>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { renderString } from '@/helpers/parse'
|
||||
import { XIcon, TrashIcon, Modal } from '@/components'
|
||||
import { ref } from 'vue'
|
||||
|
||||
const props = defineProps({
|
||||
confirmationText: {
|
||||
type: String,
|
||||
default: '',
|
||||
},
|
||||
hasToType: {
|
||||
type: Boolean,
|
||||
default: false,
|
||||
},
|
||||
title: {
|
||||
type: String,
|
||||
default: 'No title defined',
|
||||
required: true,
|
||||
},
|
||||
description: {
|
||||
type: String,
|
||||
default: 'No description defined',
|
||||
required: true,
|
||||
},
|
||||
proceedLabel: {
|
||||
type: String,
|
||||
default: 'Proceed',
|
||||
},
|
||||
})
|
||||
|
||||
const emit = defineEmits(['proceed'])
|
||||
const modal = ref(null)
|
||||
|
||||
const action_disabled = ref(props.hasToType)
|
||||
const confirmation_typed = ref('')
|
||||
|
||||
function proceed() {
|
||||
modal.value.hide()
|
||||
emit('proceed')
|
||||
}
|
||||
|
||||
function type() {
|
||||
if (props.hasToType) {
|
||||
action_disabled.value =
|
||||
confirmation_typed.value.toLowerCase() !== props.confirmationText.toLowerCase()
|
||||
}
|
||||
}
|
||||
|
||||
function show() {
|
||||
modal.value.show()
|
||||
}
|
||||
|
||||
defineExpose({ show })
|
||||
</script>
|
||||
|
||||
<style scoped lang="scss">
|
||||
.modal-delete {
|
||||
padding: var(--gap-lg);
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
|
||||
.markdown-body {
|
||||
margin-bottom: 1rem;
|
||||
}
|
||||
|
||||
.confirmation-label {
|
||||
margin-bottom: 0.5rem;
|
||||
}
|
||||
|
||||
.confirmation-text {
|
||||
padding-right: 0.25ch;
|
||||
margin: 0 0.25rem;
|
||||
}
|
||||
|
||||
.confirmation-input {
|
||||
input {
|
||||
width: 20rem;
|
||||
max-width: 100%;
|
||||
}
|
||||
}
|
||||
|
||||
.button-group {
|
||||
margin-left: auto;
|
||||
margin-top: 1.5rem;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
@ -1,5 +1,5 @@
|
||||
<template>
|
||||
<Modal ref="modal" :header="`Report ${itemType}`">
|
||||
<Modal ref="modal" :header="`Report ${props.itemType}`">
|
||||
<div class="modal-report">
|
||||
<div class="markdown-body">
|
||||
<p>
|
||||
@ -10,7 +10,7 @@
|
||||
<router-link to="/legal/rules">Rules</router-link>
|
||||
. Rest assured, we’ll keep your identifying information private.
|
||||
</p>
|
||||
<p v-if="itemType === 'project' || itemType === 'version'">
|
||||
<p v-if="props.itemType === 'project' || props.itemType === 'version'">
|
||||
Please <strong>do not</strong> use this to report bugs with the project itself. This form
|
||||
is only for submitting a report to Modrinth staff. If the project has an Issues link or a
|
||||
Discord invite, consider reporting it there.
|
||||
@ -25,7 +25,7 @@
|
||||
<DropdownSelect
|
||||
id="report-type"
|
||||
v-model="reportType"
|
||||
:options="reportTypes"
|
||||
:options="props.reportTypes"
|
||||
default-value="Choose report type"
|
||||
class="multiselect"
|
||||
/>
|
||||
@ -41,7 +41,7 @@
|
||||
</div>
|
||||
<div v-else class="preview" v-html="renderString(body)"></div>
|
||||
</div>
|
||||
<div class="button-group">
|
||||
<div class="input-group push-right">
|
||||
<Button @click="cancel">
|
||||
<XIcon />
|
||||
Cancel
|
||||
@ -59,52 +59,46 @@ import { Modal, Chips, XIcon, CheckIcon, DropdownSelect } from '@/components'
|
||||
import { renderString } from '@/helpers/parse.js'
|
||||
import { ref } from 'vue'
|
||||
|
||||
const modal = ref('modal')
|
||||
defineExpose({
|
||||
modal: modal,
|
||||
const props = defineProps({
|
||||
itemType: {
|
||||
type: String,
|
||||
default: '',
|
||||
},
|
||||
itemId: {
|
||||
type: String,
|
||||
default: '',
|
||||
},
|
||||
reportTypes: {
|
||||
type: Array,
|
||||
default: () => [],
|
||||
},
|
||||
submitReport: {
|
||||
type: Function,
|
||||
default: () => {},
|
||||
},
|
||||
})
|
||||
</script>
|
||||
<script>
|
||||
export default {
|
||||
props: {
|
||||
itemType: {
|
||||
type: String,
|
||||
default: '',
|
||||
},
|
||||
itemId: {
|
||||
type: String,
|
||||
default: '',
|
||||
},
|
||||
reportTypes: {
|
||||
type: Array,
|
||||
default: () => [],
|
||||
},
|
||||
submitReport: {
|
||||
type: Function,
|
||||
default: () => {},
|
||||
},
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
reportType: '',
|
||||
body: '',
|
||||
bodyViewType: 'source',
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
renderString,
|
||||
cancel() {
|
||||
this.reportType = ''
|
||||
this.body = ''
|
||||
this.bodyViewType = 'source'
|
||||
|
||||
this.$refs.modal.hide()
|
||||
},
|
||||
show() {
|
||||
this.$refs.modal.show()
|
||||
},
|
||||
},
|
||||
const reportType = ref('')
|
||||
const body = ref('')
|
||||
const bodyViewType = ref('source')
|
||||
|
||||
const modal = ref(null)
|
||||
|
||||
function cancel() {
|
||||
reportType.value = ''
|
||||
body.value = ''
|
||||
bodyViewType.value = 'source'
|
||||
|
||||
modal.value.hide()
|
||||
}
|
||||
|
||||
function show() {
|
||||
modal.value.show()
|
||||
}
|
||||
|
||||
defineExpose({
|
||||
show,
|
||||
})
|
||||
</script>
|
||||
|
||||
<style scoped lang="scss">
|
||||
@ -126,13 +120,6 @@ export default {
|
||||
margin-bottom: 0.5rem;
|
||||
}
|
||||
|
||||
.button-group {
|
||||
margin-left: auto;
|
||||
display: flex;
|
||||
grid-gap: 0.5rem;
|
||||
flex-wrap: wrap;
|
||||
}
|
||||
|
||||
.text-input {
|
||||
height: 12rem;
|
||||
gap: 1rem;
|
||||
|
||||
@ -20,6 +20,8 @@ export { default as DropArea } from './base/DropArea.vue'
|
||||
export { default as Toggle } from './base/Toggle.vue'
|
||||
export { default as CopyCode } from './base/CopyCode.vue'
|
||||
export { default as Notifications } from './base/Notifications.vue'
|
||||
export { default as ModalConfirm } from './base/ModalConfirm.vue'
|
||||
export { default as Breadcrumbs } from './base/Breadcrumbs.vue'
|
||||
|
||||
export { default as Categories } from './search/Categories.vue'
|
||||
export { default as SearchFilter } from './search/SearchFilter.vue'
|
||||
@ -72,6 +74,7 @@ export { default as HammerIcon } from '@/assets/icons/hammer.svg'
|
||||
export { default as HashIcon } from '@/assets/icons/hash.svg'
|
||||
export { default as HeartIcon } from '@/assets/icons/heart.svg'
|
||||
export { default as HeartHandshakeIcon } from '@/assets/icons/heart-handshake.svg'
|
||||
export { default as HistoryIcon } from '@/assets/icons/history.svg'
|
||||
export { default as HomeIcon } from '@/assets/icons/home.svg'
|
||||
export { default as ImageIcon } from '@/assets/icons/image.svg'
|
||||
export { default as InfoIcon } from '@/assets/icons/info.svg'
|
||||
|
||||
@ -42,9 +42,12 @@ export default {
|
||||
:deep(span) {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
color: var(--color-icon);
|
||||
align-items: center;
|
||||
|
||||
&:not(.version-badge) {
|
||||
color: var(--color-gray);
|
||||
}
|
||||
|
||||
svg {
|
||||
width: 1rem;
|
||||
margin-right: 0.2rem;
|
||||
|
||||
@ -14,7 +14,9 @@ export const configuredXss = new xss.FilterXSS({
|
||||
kbd: ['id'],
|
||||
input: ['checked', 'disabled', 'type'],
|
||||
iframe: ['width', 'height', 'allowfullscreen', 'frameborder', 'start', 'end'],
|
||||
img: [...xss.whiteList.img, 'style'],
|
||||
img: [...xss.whiteList.img, 'usemap'],
|
||||
map: ['name'],
|
||||
area: [...xss.whiteList.a, 'coords'],
|
||||
a: [...xss.whiteList.a, 'rel'],
|
||||
},
|
||||
css: {
|
||||
@ -56,6 +58,42 @@ export const configuredXss = new xss.FilterXSS({
|
||||
return name + '="' + xss.escapeAttrValue(value) + '"'
|
||||
}
|
||||
},
|
||||
safeAttrValue(tag, name, value, cssFilter) {
|
||||
if (tag === 'img' && name === 'src' && !value.startsWith('data:')) {
|
||||
try {
|
||||
const url = new URL(value)
|
||||
|
||||
const allowedHostnames = [
|
||||
'imgur.com',
|
||||
'i.imgur.com',
|
||||
'cdn-raw.modrinth.com',
|
||||
'cdn.modrinth.com',
|
||||
'staging-cdn-raw.modrinth.com',
|
||||
'staging-cdn.modrinth.com',
|
||||
'github.com',
|
||||
'raw.githubusercontent.com',
|
||||
'img.shields.io',
|
||||
'i.postimg.cc',
|
||||
'wsrv.nl',
|
||||
'cf.way2muchnoise.eu',
|
||||
'bstats.org',
|
||||
]
|
||||
|
||||
if (!allowedHostnames.includes(url.hostname)) {
|
||||
return xss.safeAttrValue(
|
||||
tag,
|
||||
name,
|
||||
`https://wsrv.nl/?url=${encodeURIComponent(value)}&n=-1`,
|
||||
cssFilter
|
||||
)
|
||||
}
|
||||
} catch (err) {
|
||||
/* empty */
|
||||
}
|
||||
}
|
||||
|
||||
return xss.safeAttrValue(tag, name, value, cssFilter)
|
||||
},
|
||||
})
|
||||
|
||||
export const md = (options = {}) => {
|
||||
@ -96,47 +134,6 @@ export const md = (options = {}) => {
|
||||
return defaultLinkOpenRenderer(tokens, idx, options, env, self)
|
||||
}
|
||||
|
||||
const defaultImageRenderer =
|
||||
md.renderer.rules.image ||
|
||||
function (tokens, idx, options, _env, self) {
|
||||
return self.renderToken(tokens, idx, options)
|
||||
}
|
||||
|
||||
md.renderer.rules.image = function (tokens, idx, options, env, self) {
|
||||
const token = tokens[idx]
|
||||
const index = token.attrIndex('src')
|
||||
|
||||
if (index !== -1) {
|
||||
const src = token.attrs[index][1]
|
||||
|
||||
try {
|
||||
const url = new URL(src)
|
||||
|
||||
const allowedHostnames = [
|
||||
'imgur.com',
|
||||
'i.imgur.com',
|
||||
'cdn-raw.modrinth.com',
|
||||
'cdn.modrinth.com',
|
||||
'staging-cdn-raw.modrinth.com',
|
||||
'staging-cdn.modrinth.com',
|
||||
'github.com',
|
||||
'raw.githubusercontent.com',
|
||||
'img.shields.io',
|
||||
'i.postimg.cc',
|
||||
]
|
||||
|
||||
if (allowedHostnames.includes(url.hostname)) {
|
||||
return defaultImageRenderer(tokens, idx, options, env, self)
|
||||
}
|
||||
} catch (err) {
|
||||
/* empty */
|
||||
}
|
||||
token.attrs[index][1] = `https://wsrv.nl/?url=${encodeURIComponent(src)}`
|
||||
}
|
||||
|
||||
return defaultImageRenderer(tokens, idx, options, env, self)
|
||||
}
|
||||
|
||||
return md
|
||||
}
|
||||
|
||||
|
||||
@ -1,20 +1,20 @@
|
||||
export const formatNumber = (number) => {
|
||||
export const formatNumber = (number, abbreviate = true) => {
|
||||
const x = +number
|
||||
if (x >= 1000000) {
|
||||
if (x >= 1000000 && abbreviate) {
|
||||
return (x / 1000000).toFixed(2).toString() + 'M'
|
||||
} else if (x >= 10000) {
|
||||
return (x / 1000).toFixed(1).toString() + 'K'
|
||||
} else if (x >= 10000 && abbreviate) {
|
||||
return (x / 1000).toFixed(1).toString() + 'k'
|
||||
} else {
|
||||
return x.toString().replace(/\B(?=(\d{3})+(?!\d))/g, ',')
|
||||
}
|
||||
}
|
||||
|
||||
export function formatMoney(number) {
|
||||
export function formatMoney(number, abbreviate = false) {
|
||||
const x = +number
|
||||
if (x >= 1000000) {
|
||||
if (x >= 1000000 && abbreviate) {
|
||||
return '$' + (x / 1000000).toFixed(2).toString() + 'M'
|
||||
} else if (x >= 10000) {
|
||||
return '$' + (x / 1000).toFixed(1).toString() + 'K'
|
||||
} else if (x >= 10000 && abbreviate) {
|
||||
return '$' + (x / 1000).toFixed(2).toString() + 'k'
|
||||
} else {
|
||||
return (
|
||||
'$' +
|
||||
|
||||
34
package.json
34
package.json
@ -3,7 +3,8 @@
|
||||
"type": "module",
|
||||
"version": "0.4.21",
|
||||
"files": [
|
||||
"dist"
|
||||
"dist",
|
||||
"lib"
|
||||
],
|
||||
"main": "./dist/omorphia.umd.cjs",
|
||||
"module": "./dist/omorphia.js",
|
||||
@ -17,7 +18,7 @@
|
||||
"scripts": {
|
||||
"build": "vite build",
|
||||
"lint:js": "eslint --ext .js,.vue,.ts,.jsx,.tsx,.html,.vue .",
|
||||
"lint": "npm run lint:js && prettier --check .",
|
||||
"lint": "pnpm run lint:js && prettier --check .",
|
||||
"fix": "eslint --fix --ext .js,.vue,.ts,.jsx,.tsx,.html,.vue . && prettier --write .",
|
||||
"docs:dev": "vitepress dev docs",
|
||||
"docs:build": "vitepress build docs",
|
||||
@ -26,27 +27,28 @@
|
||||
"dependencies": {
|
||||
"dayjs": "^1.11.7",
|
||||
"floating-vue": "^2.0.0-beta.20",
|
||||
"highlight.js": "^11.7.0",
|
||||
"highlight.js": "^11.8.0",
|
||||
"markdown-it": "^13.0.1",
|
||||
"vue": "^3.2.45",
|
||||
"vue-router": "^4.1.6",
|
||||
"vue": "^3.3.4",
|
||||
"vue-router": "^4.2.1",
|
||||
"vue-select": "^4.0.0-beta.6",
|
||||
"xss": "^1.0.14"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@vitejs/plugin-vue": "^4.0.0",
|
||||
"eslint": "^8.33.0",
|
||||
"eslint-config-prettier": "^8.6.0",
|
||||
"eslint-plugin-vue": "^9.9.0",
|
||||
"@vitejs/plugin-vue": "^4.2.3",
|
||||
"eslint": "^8.41.0",
|
||||
"eslint-config-prettier": "^8.8.0",
|
||||
"eslint-plugin-prettier": "^4.2.1",
|
||||
"postcss": "^8.4.21",
|
||||
"eslint-plugin-vue": "^9.14.1",
|
||||
"postcss": "^8.4.24",
|
||||
"postcss-prefix-selector": "^1.16.0",
|
||||
"prettier": "^2.7.1",
|
||||
"sass": "^1.57.1",
|
||||
"sass-loader": "^13.2.0",
|
||||
"vite": "^4.0.0",
|
||||
"prettier": "^2.8.8",
|
||||
"sass": "^1.62.1",
|
||||
"sass-loader": "^13.3.1",
|
||||
"vite": "^4.3.9",
|
||||
"vite-plugin-eslint": "^1.8.1",
|
||||
"vite-svg-loader": "^4.0.0",
|
||||
"vitepress": "^1.0.0-alpha.46"
|
||||
}
|
||||
"vitepress": "^1.0.0-beta.1"
|
||||
},
|
||||
"packageManager": "pnpm@8.5.1"
|
||||
}
|
||||
|
||||
2459
pnpm-lock.yaml
generated
Normal file
2459
pnpm-lock.yaml
generated
Normal file
File diff suppressed because it is too large
Load Diff
Loading…
x
Reference in New Issue
Block a user