Merge pull request #23 from modrinth/final-components
Final few components
This commit is contained in:
commit
f874941e4d
@ -22,6 +22,8 @@ export default {
|
||||
{ text: 'Card', link: '/components/card' },
|
||||
{ text: 'Checkbox', link: '/components/checkbox' },
|
||||
{ text: 'Chips', link: '/components/chips' },
|
||||
{ text: 'File Input', link: '/components/file-input' },
|
||||
{ text: 'Drop Area', link: '/components/drop-area' },
|
||||
{ text: 'Icons', link: '/components/icons' },
|
||||
{ text: 'Pagination', link: '/components/pagination' },
|
||||
{ text: 'Project Card', link: '/components/project-card' },
|
||||
@ -31,6 +33,7 @@ export default {
|
||||
{ text: 'Text Logo', link: '/components/text-logo' },
|
||||
{ text: 'Slider', link: '/components/slider' },
|
||||
{ text: 'Text Inputs', link: '/components/text-inputs' },
|
||||
{ text: 'Search Filter', link: '/components/search-filter' },
|
||||
],
|
||||
},
|
||||
],
|
||||
|
||||
@ -1,5 +1,5 @@
|
||||
<template>
|
||||
<div class="demo bg-raised radius-lg padding-md"><slot /></div>
|
||||
<div class="demo"><slot /></div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
@ -8,8 +8,11 @@ export default {}
|
||||
|
||||
<style lang="scss" scoped>
|
||||
.demo {
|
||||
background: var(--color-raised-bg);
|
||||
border: 1px solid var(--color-button-bg);
|
||||
border-radius: var(--gap-lg);
|
||||
display: flex;
|
||||
padding: var(--gap-md);
|
||||
gap: var(--gap-md);
|
||||
align-items: center;
|
||||
flex-wrap: wrap;
|
||||
|
||||
@ -22,3 +22,26 @@
|
||||
class="tags"
|
||||
/>
|
||||
</DemoContainer>
|
||||
|
||||
```vue
|
||||
<Categories
|
||||
:categories="[{
|
||||
name: 'magic',
|
||||
icon: '<svg viewBox=\'0 0 24 24\' fill=\'none\' stroke=\'currentColor\' stroke-width=\'2\' stroke-linecap=\'round\' stroke-linejoin=\'round\'><path d=\'M15 4V2\'></path><path d=\'M15 16v-2\'></path><path d=\'M8 9h2\'></path><path d=\'M20 9h2\'></path><path d=\'M17.8 11.8 19 13\'></path><path d=\'M15 9h0\'></path><path d=\'M17.8 6.2 19 5\'></path><path d=\'m3 21 9-9\'></path><path d=\'M12.2 6.2 11 5\'></path></svg>',
|
||||
},
|
||||
{
|
||||
icon: '<svg viewBox=\'0 0 24 24\' fill=\'none\' stroke=\'currentColor\' stroke-width=\'2\' stroke-linecap=\'round\' stroke-linejoin=\'round\'><rect x=\'2\' y=\'7\' width=\'20\' height=\'14\' rx=\'2\' ry=\'2\'/><path d=\'M16 21V5a2 2 0 0 0-2-2h-4a2 2 0 0 0-2 2v16\'/></svg>',
|
||||
name: 'utility',
|
||||
},
|
||||
{
|
||||
icon: '<svg xmlns=\'http://www.w3.org/2000/svg\' xml:space=\'preserve\' fill-rule=\'evenodd\' stroke-linecap=\'round\' stroke-linejoin=\'round\' clip-rule=\'evenodd\' viewBox=\'0 0 24 24\'>\n <path fill=\'none\' d=\'M0 0h24v24H0z\'/>\n <path fill=\'none\' stroke=\'currentColor\' stroke-width=\'23\' d=\'m820 761-85.6-87.6c-4.6-4.7-10.4-9.6-25.9 1-19.9 13.6-8.4 21.9-5.2 25.4 8.2 9 84.1 89 97.2 104 2.5 2.8-20.3-22.5-6.5-39.7 5.4-7 18-12 26-3 6.5 7.3 10.7 18-3.4 29.7-24.7 20.4-102 82.4-127 103-12.5 10.3-28.5 2.3-35.8-6-7.5-8.9-30.6-34.6-51.3-58.2-5.5-6.3-4.1-19.6 2.3-25 35-30.3 91.9-73.8 111.9-90.8\' transform=\'matrix(.08671 0 0 .0867 -49.8 -56)\'/>\n</svg>',
|
||||
name: 'fabric',
|
||||
},
|
||||
{
|
||||
icon: '<svg xml:space=\'preserve\' fill-rule=\'evenodd\' stroke-linecap=\'round\' stroke-linejoin=\'round\' stroke-miterlimit=\'1.5\' clip-rule=\'evenodd\' viewBox=\'0 0 24 24\'>\n <path fill=\'none\' d=\'M0 0h24v24H0z\'></path>\n <path fill=\'none\' stroke=\'currentColor\' stroke-width=\'2\' d=\'M2 7.5h8v-2h12v2s-7 3.4-7 6 3.1 3.1 3.1 3.1l.9 3.9H5l1-4.1s3.8.1 4-2.9c.2-2.7-6.5-.7-8-6Z\'></path>\n</svg>',
|
||||
name: 'forge'
|
||||
}]"
|
||||
type="mod"
|
||||
class="tags"
|
||||
/>
|
||||
```
|
||||
|
||||
11
docs/components/drop-area.md
Normal file
11
docs/components/drop-area.md
Normal file
@ -0,0 +1,11 @@
|
||||
# Drop Area
|
||||
|
||||
<DemoContainer>
|
||||
<InfoIcon /> Click to choose a file or drag one onto this page
|
||||
<DropArea accept="*" />
|
||||
</DemoContainer>
|
||||
|
||||
```vue
|
||||
<InfoIcon /> Click to choose a file or drag one onto this page
|
||||
<DropArea accept="*" />
|
||||
```
|
||||
23
docs/components/file-input.md
Normal file
23
docs/components/file-input.md
Normal file
@ -0,0 +1,23 @@
|
||||
# File Input
|
||||
|
||||
<DemoContainer>
|
||||
<FileInput
|
||||
:max-size="262144"
|
||||
accept="image/png,image/jpeg,image/gif,image/webp"
|
||||
class="btn"
|
||||
prompt="Upload icon"
|
||||
>
|
||||
<UploadIcon />
|
||||
</FileInput>
|
||||
</DemoContainer>
|
||||
|
||||
```vue
|
||||
<FileInput
|
||||
:max-size="262144"
|
||||
accept="image/png,image/jpeg,image/gif,image/webp"
|
||||
class="btn"
|
||||
prompt="Upload icon"
|
||||
>
|
||||
<UploadIcon />
|
||||
</FileInput>
|
||||
```
|
||||
53
docs/components/search-filter.md
Normal file
53
docs/components/search-filter.md
Normal file
@ -0,0 +1,53 @@
|
||||
# Search Filter
|
||||
|
||||
<script setup>
|
||||
import { ref } from "vue";
|
||||
|
||||
const activeFilters = ref([]);
|
||||
|
||||
function toggleFilter(filter) {
|
||||
const index = activeFilters.value.indexOf(filter);
|
||||
if (index !== -1) {
|
||||
activeFilters.value.splice(index, 1)
|
||||
} else {
|
||||
activeFilters.value.push(filter)
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<DemoContainer>
|
||||
<SearchFilter
|
||||
:active-filters="activeFilters"
|
||||
display-name="Client"
|
||||
facet-name="client"
|
||||
@toggle="toggleFilter"
|
||||
>
|
||||
<ClientIcon aria-hidden="true" />
|
||||
</SearchFilter>
|
||||
</DemoContainer>
|
||||
|
||||
```vue
|
||||
<script setup>
|
||||
import { ref } from "vue";
|
||||
|
||||
const activeFilters = ref([]);
|
||||
|
||||
function toggleFilter(filter) {
|
||||
const index = activeFilters.value.indexOf(filter);
|
||||
if (index !== -1) {
|
||||
activeFilters.value.splice(index, 1)
|
||||
} else {
|
||||
activeFilters.value.push(filter)
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<SearchFilter
|
||||
:active-filters="activeFilters"
|
||||
display-name="Client"
|
||||
facet-name="client"
|
||||
@toggle="toggleFilter"
|
||||
>
|
||||
<ClientIcon aria-hidden="true" />
|
||||
</SearchFilter>
|
||||
```
|
||||
87
lib/components/base/DropArea.vue
Normal file
87
lib/components/base/DropArea.vue
Normal file
@ -0,0 +1,87 @@
|
||||
<template>
|
||||
<div
|
||||
ref="drop_area"
|
||||
class="drop-area"
|
||||
@drop.stop.prevent="
|
||||
(event) => {
|
||||
$refs.drop_area.style.visibility = 'hidden'
|
||||
if (event.dataTransfer && event.dataTransfer.files && fileAllowed) {
|
||||
$emit('change', event.dataTransfer.files)
|
||||
}
|
||||
}
|
||||
"
|
||||
@dragenter.prevent="allowDrag"
|
||||
@dragover.prevent="allowDrag"
|
||||
@dragleave.prevent="$refs.drop_area.style.visibility = 'hidden'"
|
||||
/>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import { defineComponent } from 'vue'
|
||||
|
||||
export default defineComponent({
|
||||
props: {
|
||||
accept: {
|
||||
type: String,
|
||||
default: '',
|
||||
},
|
||||
},
|
||||
emits: ['change'],
|
||||
data() {
|
||||
return {
|
||||
fileAllowed: false,
|
||||
}
|
||||
},
|
||||
mounted() {
|
||||
document.addEventListener('dragenter', this.allowDrag)
|
||||
},
|
||||
methods: {
|
||||
allowDrag(event) {
|
||||
const file = event.dataTransfer?.items[0]
|
||||
if (
|
||||
file &&
|
||||
this.accept
|
||||
.split(',')
|
||||
.reduce((acc, t) => acc || file.type.startsWith(t) || file.type === t || t === '*', false)
|
||||
) {
|
||||
this.fileAllowed = true
|
||||
event.dataTransfer.dropEffect = 'copy'
|
||||
event.preventDefault()
|
||||
if (this.$refs.drop_area) {
|
||||
this.$refs.drop_area.style.visibility = 'visible'
|
||||
}
|
||||
} else {
|
||||
this.fileAllowed = false
|
||||
if (this.$refs.drop_area) {
|
||||
this.$refs.drop_area.style.visibility = 'hidden'
|
||||
}
|
||||
}
|
||||
},
|
||||
},
|
||||
})
|
||||
</script>
|
||||
<style lang="scss" scoped>
|
||||
.drop-area {
|
||||
position: fixed;
|
||||
top: 0;
|
||||
left: 0;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
z-index: 10;
|
||||
visibility: hidden;
|
||||
background-color: hsla(0, 0%, 0%, 0.5);
|
||||
transition: visibility 0.2s ease-in-out, background-color 0.1s ease-in-out;
|
||||
display: flex;
|
||||
&::before {
|
||||
--indent: 4rem;
|
||||
content: ' ';
|
||||
position: relative;
|
||||
top: var(--indent);
|
||||
left: var(--indent);
|
||||
width: calc(100% - (2 * var(--indent)));
|
||||
height: calc(100% - (2 * var(--indent)));
|
||||
border-radius: 1rem;
|
||||
border: 0.25rem dashed var(--color-button-bg);
|
||||
}
|
||||
}
|
||||
</style>
|
||||
96
lib/components/base/FileInput.vue
Normal file
96
lib/components/base/FileInput.vue
Normal file
@ -0,0 +1,96 @@
|
||||
<template>
|
||||
<label :class="{ 'long-style': longStyle }" @drop.prevent="handleDrop" @dragover.prevent>
|
||||
<slot />
|
||||
{{ prompt }}
|
||||
<input type="file" :multiple="multiple" :accept="accept" @change="handleChange" />
|
||||
</label>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import { fileIsValid } from '@/components/utils'
|
||||
import { defineComponent } from 'vue'
|
||||
export default defineComponent({
|
||||
props: {
|
||||
prompt: {
|
||||
type: String,
|
||||
default: 'Select file',
|
||||
},
|
||||
multiple: {
|
||||
type: Boolean,
|
||||
default: false,
|
||||
},
|
||||
accept: {
|
||||
type: String,
|
||||
default: null,
|
||||
},
|
||||
/**
|
||||
* The max file size in bytes
|
||||
*/
|
||||
maxSize: {
|
||||
type: Number,
|
||||
default: null,
|
||||
},
|
||||
showIcon: {
|
||||
type: Boolean,
|
||||
default: true,
|
||||
},
|
||||
shouldAlwaysReset: {
|
||||
type: Boolean,
|
||||
default: false,
|
||||
},
|
||||
longStyle: {
|
||||
type: Boolean,
|
||||
default: false,
|
||||
},
|
||||
},
|
||||
emits: ['change'],
|
||||
data() {
|
||||
return {
|
||||
files: [],
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
addFiles(files, shouldNotReset) {
|
||||
if (!shouldNotReset || this.shouldAlwaysReset) {
|
||||
this.files = files
|
||||
}
|
||||
const validationOptions = { maxSize: this.maxSize, alertOnInvalid: true }
|
||||
this.files = [...this.files].filter((file) => fileIsValid(file, validationOptions))
|
||||
if (this.files.length > 0) {
|
||||
this.$emit('change', this.files)
|
||||
}
|
||||
},
|
||||
handleDrop(e) {
|
||||
this.addFiles(e.dataTransfer.files)
|
||||
},
|
||||
handleChange(e) {
|
||||
this.addFiles(e.target.files)
|
||||
},
|
||||
},
|
||||
})
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
label {
|
||||
flex-direction: unset;
|
||||
max-height: unset;
|
||||
svg {
|
||||
height: 1rem;
|
||||
}
|
||||
input {
|
||||
display: none;
|
||||
}
|
||||
&.long-style {
|
||||
display: flex;
|
||||
padding: 1.5rem 2rem;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
grid-gap: 0.5rem;
|
||||
background-color: var(--color-button-bg);
|
||||
border-radius: var(--radius-sm);
|
||||
border: dashed 0.3rem var(--color-contrast);
|
||||
cursor: pointer;
|
||||
color: var(--color-accent-contrast);
|
||||
}
|
||||
}
|
||||
</style>
|
||||
@ -11,8 +11,11 @@ export { default as TextLogo } from './brand/TextLogo.vue'
|
||||
export { default as Pagination } from './base/Pagination.vue'
|
||||
export { default as ProjectCard } from './base/ProjectCard.vue'
|
||||
export { default as EnvironmentIndicator } from './base/EnvironmentIndicator.vue'
|
||||
export { default as FileInput } from './base/FileInput.vue'
|
||||
export { default as DropArea } from './base/DropArea.vue'
|
||||
|
||||
export { default as Categories } from './search/Categories.vue'
|
||||
export { default as SearchFilter } from './search/SearchFilter.vue'
|
||||
|
||||
export { default as NavItem } from './nav/NavItem.vue'
|
||||
export { default as NavRow } from './nav/NavRow.vue'
|
||||
|
||||
@ -39,7 +39,7 @@ export default {
|
||||
flex-wrap: wrap;
|
||||
gap: var(--gap-sm);
|
||||
|
||||
span ::v-deep {
|
||||
:deep(span) {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
color: var(--color-icon);
|
||||
|
||||
73
lib/components/search/SearchFilter.vue
Normal file
73
lib/components/search/SearchFilter.vue
Normal file
@ -0,0 +1,73 @@
|
||||
<template>
|
||||
<Checkbox
|
||||
class="filter"
|
||||
:model-value="activeFilters.includes(facetName)"
|
||||
:description="displayName"
|
||||
@update:model-value="toggle()"
|
||||
>
|
||||
<div class="filter-text">
|
||||
<div v-if="icon" aria-hidden="true" class="icon" v-html="icon" />
|
||||
<div v-else class="icon">
|
||||
<slot />
|
||||
</div>
|
||||
<span aria-hidden="true"> {{ displayName }}</span>
|
||||
</div>
|
||||
</Checkbox>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import { defineComponent } from 'vue'
|
||||
import Checkbox from '@/components/base/Checkbox.vue'
|
||||
export default defineComponent({
|
||||
components: {
|
||||
Checkbox,
|
||||
},
|
||||
props: {
|
||||
facetName: {
|
||||
type: String,
|
||||
default: '',
|
||||
},
|
||||
displayName: {
|
||||
type: String,
|
||||
default: '',
|
||||
},
|
||||
icon: {
|
||||
type: String,
|
||||
default: '',
|
||||
},
|
||||
activeFilters: {
|
||||
type: Array,
|
||||
default() {
|
||||
return []
|
||||
},
|
||||
},
|
||||
},
|
||||
emits: ['toggle'],
|
||||
methods: {
|
||||
toggle() {
|
||||
this.$emit('toggle', this.facetName)
|
||||
},
|
||||
},
|
||||
})
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
.filter {
|
||||
margin-bottom: 0.5rem;
|
||||
:deep(.filter-text) {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
.icon {
|
||||
height: 1rem;
|
||||
svg {
|
||||
margin-right: 0.25rem;
|
||||
width: 1rem;
|
||||
height: 1rem;
|
||||
}
|
||||
}
|
||||
}
|
||||
span {
|
||||
user-select: none;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
@ -189,3 +189,34 @@ export function cycleValue(value, values) {
|
||||
const index = values.indexOf(value) + 1
|
||||
return values[index % values.length]
|
||||
}
|
||||
|
||||
export const fileIsValid = (file, validationOptions) => {
|
||||
const { maxSize, alertOnInvalid } = validationOptions
|
||||
if (maxSize !== null && maxSize !== undefined && file.size > maxSize) {
|
||||
if (alertOnInvalid) {
|
||||
alert(`File ${file.name} is too big! Must be less than ${formatBytes(maxSize)}`)
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
return true
|
||||
}
|
||||
|
||||
export const acceptFileFromProjectType = (projectType) => {
|
||||
switch (projectType) {
|
||||
case 'mod':
|
||||
return '.jar,.zip,.litemod,application/java-archive,application/x-java-archive,application/zip'
|
||||
case 'plugin':
|
||||
return '.jar,.zip,application/java-archive,application/x-java-archive,application/zip'
|
||||
case 'resourcepack':
|
||||
return '.zip,application/zip'
|
||||
case 'shader':
|
||||
return '.zip,application/zip'
|
||||
case 'datapack':
|
||||
return '.zip,application/zip'
|
||||
case 'modpack':
|
||||
return '.mrpack,application/x-modrinth-modpack+zip,application/zip'
|
||||
default:
|
||||
return '*'
|
||||
}
|
||||
}
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user