You've already forked AstralRinth
forked from didirus/AstralRinth
NormalPage component w/ Collections refactor (#4873)
* Refactor search page, migrate to /discover/ * Add NormalPage component for common layouts, refactor Collections page as an example, misc ui pkg cleanup * intl:extract * lint * lint * remove old components * Refactor search page, migrate to /discover/ * Add NormalPage component for common layouts, refactor Collections page as an example, misc ui pkg cleanup * intl:extract * lint * lint * remove old components
This commit is contained in:
2
packages/ui/src/components/affiliate/index.ts
Normal file
2
packages/ui/src/components/affiliate/index.ts
Normal file
@@ -0,0 +1,2 @@
|
||||
export { default as AffiliateLinkCard } from './AffiliateLinkCard.vue'
|
||||
export { default as AffiliateLinkCreateModal } from './AffiliateLinkCreateModal.vue'
|
||||
3
packages/ui/src/components/base/HorizontalRule.vue
Normal file
3
packages/ui/src/components/base/HorizontalRule.vue
Normal file
@@ -0,0 +1,3 @@
|
||||
<template>
|
||||
<div class="h-[1px] w-full bg-divider"></div>
|
||||
</template>
|
||||
@@ -1,9 +1,9 @@
|
||||
<template>
|
||||
<div class="flex flex-wrap gap-2">
|
||||
<div class="flex flex-col gap-1">
|
||||
<button
|
||||
v-for="(item, index) in items"
|
||||
:key="`radio-button-${index}`"
|
||||
class="p-0 py-2 px-2 border-0 font-medium flex gap-2 transition-all items-center cursor-pointer active:scale-95 hover:bg-button-bg rounded-xl"
|
||||
class="p-0 py-2 px-2 w-fit border-0 font-medium flex gap-2 transition-all items-center cursor-pointer active:scale-95 hover:bg-button-bg rounded-xl"
|
||||
:class="{
|
||||
'text-contrast bg-button-bg': selected === item,
|
||||
'text-primary bg-transparent': selected !== item,
|
||||
|
||||
@@ -1,27 +0,0 @@
|
||||
<template>
|
||||
<div class="flex items-center gap-3">
|
||||
<slot></slot>
|
||||
<div class="flex flex-col">
|
||||
<span class="font-bold">{{ value }}</span>
|
||||
<span class="text-secondary">{{ label }}</span>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
defineProps({
|
||||
label: {
|
||||
type: String,
|
||||
required: true,
|
||||
},
|
||||
value: {
|
||||
type: String,
|
||||
required: true,
|
||||
},
|
||||
})
|
||||
</script>
|
||||
<style scoped>
|
||||
:slotted(*) {
|
||||
@apply h-6 w-6 text-secondary;
|
||||
}
|
||||
</style>
|
||||
56
packages/ui/src/components/base/index.ts
Normal file
56
packages/ui/src/components/base/index.ts
Normal file
@@ -0,0 +1,56 @@
|
||||
export { default as Accordion } from './Accordion.vue'
|
||||
export { default as Admonition } from './Admonition.vue'
|
||||
export { default as AppearingProgressBar } from './AppearingProgressBar.vue'
|
||||
export { default as AutoBrandIcon } from './AutoBrandIcon.vue'
|
||||
export { default as AutoLink } from './AutoLink.vue'
|
||||
export { default as Avatar } from './Avatar.vue'
|
||||
export { default as Badge } from './Badge.vue'
|
||||
export { default as BulletDivider } from './BulletDivider.vue'
|
||||
export { default as Button } from './Button.vue'
|
||||
export { default as ButtonStyled } from './ButtonStyled.vue'
|
||||
export { default as Card } from './Card.vue'
|
||||
export { default as Checkbox } from './Checkbox.vue'
|
||||
export { default as Chips } from './Chips.vue'
|
||||
export { default as Collapsible } from './Collapsible.vue'
|
||||
export { default as CollapsibleRegion } from './CollapsibleRegion.vue'
|
||||
export { default as Combobox } from './Combobox.vue'
|
||||
export { default as ContentPageHeader } from './ContentPageHeader.vue'
|
||||
export { default as CopyCode } from './CopyCode.vue'
|
||||
export { default as DoubleIcon } from './DoubleIcon.vue'
|
||||
export { default as DropArea } from './DropArea.vue'
|
||||
export { default as DropdownSelect } from './DropdownSelect.vue'
|
||||
export { default as EnvironmentIndicator } from './EnvironmentIndicator.vue'
|
||||
export { default as ErrorInformationCard } from './ErrorInformationCard.vue'
|
||||
export { default as FileInput } from './FileInput.vue'
|
||||
export type { FilterBarOption } from './FilterBar.vue'
|
||||
export { default as FilterBar } from './FilterBar.vue'
|
||||
export { default as HeadingLink } from './HeadingLink.vue'
|
||||
export { default as HorizontalRule } from './HorizontalRule.vue'
|
||||
export { default as IconSelect } from './IconSelect.vue'
|
||||
export type { JoinedButtonAction } from './JoinedButtons.vue'
|
||||
export { default as JoinedButtons } from './JoinedButtons.vue'
|
||||
export { default as LoadingIndicator } from './LoadingIndicator.vue'
|
||||
export { default as ManySelect } from './ManySelect.vue'
|
||||
export { default as MarkdownEditor } from './MarkdownEditor.vue'
|
||||
export { default as OptionGroup } from './OptionGroup.vue'
|
||||
export type { Option as OverflowMenuOption } from './OverflowMenu.vue'
|
||||
export { default as OverflowMenu } from './OverflowMenu.vue'
|
||||
export { default as Page } from './Page.vue'
|
||||
export { default as Pagination } from './Pagination.vue'
|
||||
export { default as PopoutMenu } from './PopoutMenu.vue'
|
||||
export { default as PreviewSelectButton } from './PreviewSelectButton.vue'
|
||||
export { default as ProgressBar } from './ProgressBar.vue'
|
||||
export { default as ProgressSpinner } from './ProgressSpinner.vue'
|
||||
export { default as ProjectCard } from './ProjectCard.vue'
|
||||
export { default as RadialHeader } from './RadialHeader.vue'
|
||||
export { default as RadioButtons } from './RadioButtons.vue'
|
||||
export { default as ScrollablePanel } from './ScrollablePanel.vue'
|
||||
export { default as ServerNotice } from './ServerNotice.vue'
|
||||
export { default as SettingsLabel } from './SettingsLabel.vue'
|
||||
export { default as SimpleBadge } from './SimpleBadge.vue'
|
||||
export { default as Slider } from './Slider.vue'
|
||||
export { default as SmartClickable } from './SmartClickable.vue'
|
||||
export { default as TagItem } from './TagItem.vue'
|
||||
export { default as Timeline } from './Timeline.vue'
|
||||
export { default as Toggle } from './Toggle.vue'
|
||||
export { default as UnsavedChangesPopup } from './UnsavedChangesPopup.vue'
|
||||
5
packages/ui/src/components/billing/index.ts
Normal file
5
packages/ui/src/components/billing/index.ts
Normal file
@@ -0,0 +1,5 @@
|
||||
export { default as AddPaymentMethodModal } from './AddPaymentMethodModal.vue'
|
||||
export { default as ModrinthServersPurchaseModal } from './ModrinthServersPurchaseModal.vue'
|
||||
export { default as PurchaseModal } from './PurchaseModal.vue'
|
||||
export { default as ServersSpecs } from './ServersSpecs.vue'
|
||||
export { default as ServersUpgradeModalWrapper } from './ServersUpgradeModalWrapper.vue'
|
||||
2
packages/ui/src/components/brand/index.ts
Normal file
2
packages/ui/src/components/brand/index.ts
Normal file
@@ -0,0 +1,2 @@
|
||||
export { default as AnimatedLogo } from './AnimatedLogo.vue'
|
||||
export { default as TextLogo } from './TextLogo.vue'
|
||||
1
packages/ui/src/components/changelog/index.ts
Normal file
1
packages/ui/src/components/changelog/index.ts
Normal file
@@ -0,0 +1 @@
|
||||
export { default as ChangelogEntry } from './ChangelogEntry.vue'
|
||||
2
packages/ui/src/components/chart/index.ts
Normal file
2
packages/ui/src/components/chart/index.ts
Normal file
@@ -0,0 +1,2 @@
|
||||
export { default as Chart } from './Chart.vue'
|
||||
export { default as CompactChart } from './CompactChart.vue'
|
||||
3
packages/ui/src/components/content/index.ts
Normal file
3
packages/ui/src/components/content/index.ts
Normal file
@@ -0,0 +1,3 @@
|
||||
export { default as ContentListPanel } from './ContentListPanel.vue'
|
||||
export type { Article as NewsArticle } from './NewsArticleCard.vue'
|
||||
export { default as NewsArticleCard } from './NewsArticleCard.vue'
|
||||
@@ -1,143 +1,16 @@
|
||||
// Base content
|
||||
export { default as Accordion } from './base/Accordion.vue'
|
||||
export { default as Admonition } from './base/Admonition.vue'
|
||||
export { default as AppearingProgressBar } from './base/AppearingProgressBar.vue'
|
||||
export { default as AutoBrandIcon } from './base/AutoBrandIcon.vue'
|
||||
export { default as AutoLink } from './base/AutoLink.vue'
|
||||
export { default as Avatar } from './base/Avatar.vue'
|
||||
export { default as Badge } from './base/Badge.vue'
|
||||
export { default as BulletDivider } from './base/BulletDivider.vue'
|
||||
export { default as Button } from './base/Button.vue'
|
||||
export { default as ButtonStyled } from './base/ButtonStyled.vue'
|
||||
export { default as Card } from './base/Card.vue'
|
||||
export { default as Checkbox } from './base/Checkbox.vue'
|
||||
export { default as Chips } from './base/Chips.vue'
|
||||
export { default as Collapsible } from './base/Collapsible.vue'
|
||||
export { default as CollapsibleRegion } from './base/CollapsibleRegion.vue'
|
||||
export { default as Combobox } from './base/Combobox.vue'
|
||||
export { default as ContentPageHeader } from './base/ContentPageHeader.vue'
|
||||
export { default as CopyCode } from './base/CopyCode.vue'
|
||||
export { default as DoubleIcon } from './base/DoubleIcon.vue'
|
||||
export { default as DropArea } from './base/DropArea.vue'
|
||||
export { default as DropdownSelect } from './base/DropdownSelect.vue'
|
||||
export { default as EnvironmentIndicator } from './base/EnvironmentIndicator.vue'
|
||||
export { default as ErrorInformationCard } from './base/ErrorInformationCard.vue'
|
||||
export { default as FileInput } from './base/FileInput.vue'
|
||||
export type { FilterBarOption } from './base/FilterBar.vue'
|
||||
export { default as FilterBar } from './base/FilterBar.vue'
|
||||
export { default as HeadingLink } from './base/HeadingLink.vue'
|
||||
export { default as IconSelect } from './base/IconSelect.vue'
|
||||
export type { JoinedButtonAction } from './base/JoinedButtons.vue'
|
||||
export { default as JoinedButtons } from './base/JoinedButtons.vue'
|
||||
export { default as LoadingIndicator } from './base/LoadingIndicator.vue'
|
||||
export { default as ManySelect } from './base/ManySelect.vue'
|
||||
export { default as MarkdownEditor } from './base/MarkdownEditor.vue'
|
||||
export { default as OptionGroup } from './base/OptionGroup.vue'
|
||||
export type { Option as OverflowMenuOption } from './base/OverflowMenu.vue'
|
||||
export { default as OverflowMenu } from './base/OverflowMenu.vue'
|
||||
export { default as Page } from './base/Page.vue'
|
||||
export { default as Pagination } from './base/Pagination.vue'
|
||||
export { default as PopoutMenu } from './base/PopoutMenu.vue'
|
||||
export { default as PreviewSelectButton } from './base/PreviewSelectButton.vue'
|
||||
export { default as ProgressBar } from './base/ProgressBar.vue'
|
||||
export { default as ProgressSpinner } from './base/ProgressSpinner.vue'
|
||||
export { default as ProjectCard } from './base/ProjectCard.vue'
|
||||
export { default as RadialHeader } from './base/RadialHeader.vue'
|
||||
export { default as RadioButtons } from './base/RadioButtons.vue'
|
||||
export { default as ScrollablePanel } from './base/ScrollablePanel.vue'
|
||||
export { default as ServerNotice } from './base/ServerNotice.vue'
|
||||
export { default as SettingsLabel } from './base/SettingsLabel.vue'
|
||||
export { default as SimpleBadge } from './base/SimpleBadge.vue'
|
||||
export { default as Slider } from './base/Slider.vue'
|
||||
export { default as SmartClickable } from './base/SmartClickable.vue'
|
||||
export { default as StatItem } from './base/StatItem.vue'
|
||||
export { default as TagItem } from './base/TagItem.vue'
|
||||
export { default as Timeline } from './base/Timeline.vue'
|
||||
export { default as Toggle } from './base/Toggle.vue'
|
||||
export { default as UnsavedChangesPopup } from './base/UnsavedChangesPopup.vue'
|
||||
|
||||
// Branding
|
||||
export { default as AnimatedLogo } from './brand/AnimatedLogo.vue'
|
||||
export { default as TextLogo } from './brand/TextLogo.vue'
|
||||
|
||||
// Changelog
|
||||
export { default as ChangelogEntry } from './changelog/ChangelogEntry.vue'
|
||||
|
||||
// Charts
|
||||
export { default as Chart } from './chart/Chart.vue'
|
||||
export { default as CompactChart } from './chart/CompactChart.vue'
|
||||
|
||||
// Content
|
||||
export { default as ContentListPanel } from './content/ContentListPanel.vue'
|
||||
export type { Article as NewsArticle } from './content/NewsArticleCard.vue'
|
||||
export { default as NewsArticleCard } from './content/NewsArticleCard.vue'
|
||||
|
||||
// Modals
|
||||
export { default as ConfirmModal } from './modal/ConfirmModal.vue'
|
||||
export { default as Modal } from './modal/Modal.vue'
|
||||
export { default as NewModal } from './modal/NewModal.vue'
|
||||
export { default as ShareModal } from './modal/ShareModal.vue'
|
||||
export type { Tab as TabbedModalTab } from './modal/TabbedModal.vue'
|
||||
export { default as TabbedModal } from './modal/TabbedModal.vue'
|
||||
|
||||
// Navigation
|
||||
export { default as Breadcrumbs } from './nav/Breadcrumbs.vue'
|
||||
export { default as NavItem } from './nav/NavItem.vue'
|
||||
export { default as NavRow } from './nav/NavRow.vue'
|
||||
export { default as NavStack } from './nav/NavStack.vue'
|
||||
export { default as NotificationPanel } from './nav/NotificationPanel.vue'
|
||||
export { default as PagewideBanner } from './nav/PagewideBanner.vue'
|
||||
|
||||
// Project
|
||||
export * from './affiliate'
|
||||
export * from './base'
|
||||
export * from './billing'
|
||||
export * from './brand'
|
||||
export * from './changelog'
|
||||
export * from './chart'
|
||||
export * from './content'
|
||||
export * from './modal'
|
||||
export * from './nav'
|
||||
export * from './page'
|
||||
export * from './project'
|
||||
|
||||
// Search
|
||||
export { default as BrowseFiltersPanel } from './search/BrowseFiltersPanel.vue'
|
||||
export { default as Categories } from './search/Categories.vue'
|
||||
export { default as SearchDropdown } from './search/SearchDropdown.vue'
|
||||
export { default as SearchFilter } from './search/SearchFilter.vue'
|
||||
export { default as SearchFilterControl } from './search/SearchFilterControl.vue'
|
||||
export { default as SearchFilterOption } from './search/SearchFilterOption.vue'
|
||||
export { default as SearchSidebarFilter } from './search/SearchSidebarFilter.vue'
|
||||
|
||||
// Affiliate
|
||||
export { default as AffiliateLinkCard } from './affiliate/AffiliateLinkCard.vue'
|
||||
export { default as AffiliateLinkCreateModal } from './affiliate/AffiliateLinkCreateModal.vue'
|
||||
|
||||
// Billing
|
||||
export { default as AddPaymentMethodModal } from './billing/AddPaymentMethodModal.vue'
|
||||
export { default as ModrinthServersPurchaseModal } from './billing/ModrinthServersPurchaseModal.vue'
|
||||
export { default as PurchaseModal } from './billing/PurchaseModal.vue'
|
||||
export { default as ServersUpgradeModalWrapper } from './billing/ServersUpgradeModalWrapper.vue'
|
||||
|
||||
// Skins
|
||||
export { default as CapeButton } from './skin/CapeButton.vue'
|
||||
export { default as CapeLikeTextButton } from './skin/CapeLikeTextButton.vue'
|
||||
export { default as SkinButton } from './skin/SkinButton.vue'
|
||||
export { default as SkinLikeTextButton } from './skin/SkinLikeTextButton.vue'
|
||||
export { default as SkinPreviewRenderer } from './skin/SkinPreviewRenderer.vue'
|
||||
|
||||
// Version
|
||||
export { default as VersionChannelIndicator } from './version/VersionChannelIndicator.vue'
|
||||
export { default as VersionFilterControl } from './version/VersionFilterControl.vue'
|
||||
export { default as VersionSummary } from './version/VersionSummary.vue'
|
||||
|
||||
// Settings
|
||||
export { default as ThemeSelector } from './settings/ThemeSelector.vue'
|
||||
|
||||
// Servers
|
||||
export { default as ServersSpecs } from './billing/ServersSpecs.vue'
|
||||
export { default as BackupCreateModal } from './servers/backups/BackupCreateModal.vue'
|
||||
export { default as BackupDeleteModal } from './servers/backups/BackupDeleteModal.vue'
|
||||
export { default as BackupItem } from './servers/backups/BackupItem.vue'
|
||||
export { default as BackupRenameModal } from './servers/backups/BackupRenameModal.vue'
|
||||
export { default as BackupRestoreModal } from './servers/backups/BackupRestoreModal.vue'
|
||||
export { default as BackupWarning } from './servers/backups/BackupWarning.vue'
|
||||
export { default as LoaderIcon } from './servers/icons/LoaderIcon.vue'
|
||||
export { default as ServerIcon } from './servers/icons/ServerIcon.vue'
|
||||
export { default as ServerInfoLabels } from './servers/labels/ServerInfoLabels.vue'
|
||||
export { default as MedalBackgroundImage } from './servers/marketing/MedalBackgroundImage.vue'
|
||||
export { default as MedalServerListing } from './servers/marketing/MedalServerListing.vue'
|
||||
export type { PendingChange } from './servers/ServerListing.vue'
|
||||
export { default as ServerListing } from './servers/ServerListing.vue'
|
||||
export { default as ServersPromo } from './servers/ServersPromo.vue'
|
||||
export * from './search'
|
||||
export * from './servers'
|
||||
export * from './settings'
|
||||
export * from './skin'
|
||||
export * from './version'
|
||||
|
||||
6
packages/ui/src/components/modal/index.ts
Normal file
6
packages/ui/src/components/modal/index.ts
Normal file
@@ -0,0 +1,6 @@
|
||||
export { default as ConfirmModal } from './ConfirmModal.vue'
|
||||
export { default as Modal } from './Modal.vue'
|
||||
export { default as NewModal } from './NewModal.vue'
|
||||
export { default as ShareModal } from './ShareModal.vue'
|
||||
export type { Tab as TabbedModalTab } from './TabbedModal.vue'
|
||||
export { default as TabbedModal } from './TabbedModal.vue'
|
||||
@@ -1,50 +0,0 @@
|
||||
<script setup>
|
||||
import Button from '../base/Button.vue'
|
||||
|
||||
defineProps({
|
||||
link: {
|
||||
type: String,
|
||||
default: null,
|
||||
},
|
||||
external: {
|
||||
type: Boolean,
|
||||
default: false,
|
||||
},
|
||||
action: {
|
||||
type: Function,
|
||||
default: null,
|
||||
},
|
||||
selected: {
|
||||
type: Boolean,
|
||||
default: false,
|
||||
},
|
||||
label: {
|
||||
type: String,
|
||||
required: true,
|
||||
},
|
||||
icon: {
|
||||
type: String,
|
||||
default: null,
|
||||
},
|
||||
})
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<Button
|
||||
:link="link"
|
||||
:external="external"
|
||||
:action="action"
|
||||
design="nav"
|
||||
class="quiet-disabled"
|
||||
:class="{
|
||||
selected: selected,
|
||||
}"
|
||||
:disabled="selected"
|
||||
:navlabel="label"
|
||||
>
|
||||
<slot />
|
||||
{{ label }}
|
||||
</Button>
|
||||
</template>
|
||||
|
||||
<style lang="scss" scoped></style>
|
||||
@@ -1,166 +0,0 @@
|
||||
<template>
|
||||
<nav class="navigation">
|
||||
<router-link
|
||||
v-for="(link, index) in filteredLinks"
|
||||
v-show="link.shown === undefined ? true : link.shown"
|
||||
:key="index"
|
||||
ref="linkElements"
|
||||
:to="query ? (link.href ? `?${query}=${link.href}` : '?') : link.href"
|
||||
class="nav-link button-animation"
|
||||
>
|
||||
<span>{{ link.label }}</span>
|
||||
</router-link>
|
||||
<div
|
||||
class="nav-indicator"
|
||||
:style="{
|
||||
left: positionToMoveX,
|
||||
top: positionToMoveY,
|
||||
width: sliderWidth,
|
||||
opacity: activeIndex === -1 ? 0 : 1,
|
||||
}"
|
||||
aria-hidden="true"
|
||||
/>
|
||||
</nav>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
export default {
|
||||
props: {
|
||||
links: {
|
||||
default: () => [],
|
||||
type: Array,
|
||||
},
|
||||
query: {
|
||||
default: null,
|
||||
type: String,
|
||||
},
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
sliderPositionX: 0,
|
||||
sliderPositionY: 18,
|
||||
selectedElementWidth: 0,
|
||||
activeIndex: -1,
|
||||
oldIndex: -1,
|
||||
}
|
||||
},
|
||||
computed: {
|
||||
filteredLinks() {
|
||||
return this.links.filter((x) => (x.shown === undefined ? true : x.shown))
|
||||
},
|
||||
positionToMoveX() {
|
||||
return `${this.sliderPositionX}px`
|
||||
},
|
||||
positionToMoveY() {
|
||||
return `${this.sliderPositionY}px`
|
||||
},
|
||||
sliderWidth() {
|
||||
return `${this.selectedElementWidth}px`
|
||||
},
|
||||
},
|
||||
watch: {
|
||||
'$route.path': {
|
||||
handler() {
|
||||
this.pickLink()
|
||||
},
|
||||
},
|
||||
'$route.query': {
|
||||
handler() {
|
||||
if (this.query) this.pickLink()
|
||||
},
|
||||
},
|
||||
},
|
||||
mounted() {
|
||||
window.addEventListener('resize', this.pickLink)
|
||||
this.pickLink()
|
||||
},
|
||||
unmounted() {
|
||||
window.removeEventListener('resize', this.pickLink)
|
||||
},
|
||||
methods: {
|
||||
pickLink() {
|
||||
this.activeIndex = this.query
|
||||
? this.filteredLinks.findIndex(
|
||||
(x) => (x.href === '' ? undefined : x.href) === this.$route.path[this.query],
|
||||
)
|
||||
: this.filteredLinks.findIndex((x) => x.href === decodeURIComponent(this.$route.path))
|
||||
|
||||
if (this.activeIndex !== -1) {
|
||||
this.startAnimation()
|
||||
} else {
|
||||
this.oldIndex = -1
|
||||
this.sliderPositionX = 0
|
||||
this.selectedElementWidth = 0
|
||||
}
|
||||
},
|
||||
startAnimation() {
|
||||
const el = this.$refs.linkElements[this.activeIndex].$el
|
||||
|
||||
this.sliderPositionX = el.offsetLeft
|
||||
this.sliderPositionY = el.offsetTop + el.offsetHeight
|
||||
this.selectedElementWidth = el.offsetWidth
|
||||
},
|
||||
},
|
||||
}
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
.navigation {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
align-items: center;
|
||||
grid-gap: 1rem;
|
||||
flex-wrap: wrap;
|
||||
position: relative;
|
||||
|
||||
.nav-link {
|
||||
text-transform: capitalize;
|
||||
font-weight: var(--font-weight-bold);
|
||||
color: var(--color-base);
|
||||
position: relative;
|
||||
|
||||
&:hover {
|
||||
color: var(--color-base);
|
||||
|
||||
&::after {
|
||||
opacity: 0.4;
|
||||
}
|
||||
}
|
||||
|
||||
&:active::after {
|
||||
opacity: 0.2;
|
||||
}
|
||||
|
||||
&.router-link-exact-active {
|
||||
color: var(--color-base);
|
||||
|
||||
&::after {
|
||||
opacity: 1;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
&.use-animation {
|
||||
.nav-link {
|
||||
&.is-active::after {
|
||||
opacity: 0;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.nav-indicator {
|
||||
position: absolute;
|
||||
height: 0.25rem;
|
||||
bottom: -5px;
|
||||
left: 0;
|
||||
width: 3rem;
|
||||
transition: all ease-in-out 0.2s;
|
||||
border-radius: var(--radius-max);
|
||||
background-color: var(--color-brand);
|
||||
|
||||
@media (prefers-reduced-motion) {
|
||||
transition: none !important;
|
||||
}
|
||||
}
|
||||
}
|
||||
</style>
|
||||
@@ -1,22 +0,0 @@
|
||||
<template>
|
||||
<div class="omorphia__navstack">
|
||||
<slot />
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
.omorphia__navstack {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
|
||||
:deep(.btn) {
|
||||
position: relative;
|
||||
width: 100%;
|
||||
|
||||
&.selected {
|
||||
background-color: var(--color-button-bg);
|
||||
font-weight: bold;
|
||||
}
|
||||
}
|
||||
}
|
||||
</style>
|
||||
3
packages/ui/src/components/nav/index.ts
Normal file
3
packages/ui/src/components/nav/index.ts
Normal file
@@ -0,0 +1,3 @@
|
||||
export { default as Breadcrumbs } from './Breadcrumbs.vue'
|
||||
export { default as NotificationPanel } from './NotificationPanel.vue'
|
||||
export { default as PagewideBanner } from './PagewideBanner.vue'
|
||||
78
packages/ui/src/components/page/NormalPage.vue
Normal file
78
packages/ui/src/components/page/NormalPage.vue
Normal file
@@ -0,0 +1,78 @@
|
||||
<script setup lang="ts">
|
||||
import { injectPageContext } from '@modrinth/ui'
|
||||
|
||||
defineProps<{
|
||||
sidebar?: 'right' | 'left'
|
||||
}>()
|
||||
const { hierarchicalSidebarAvailable } = injectPageContext()
|
||||
</script>
|
||||
<template>
|
||||
<div
|
||||
class="ui-normal-page"
|
||||
:class="{
|
||||
'ui-normal-page--sidebar-left': sidebar === 'left' && !hierarchicalSidebarAvailable,
|
||||
'ui-normal-page--sidebar-right': sidebar === 'right' && !hierarchicalSidebarAvailable,
|
||||
}"
|
||||
>
|
||||
<div class="ui-normal-page__header">
|
||||
<slot name="header" />
|
||||
</div>
|
||||
<div class="ui-normal-page__content">
|
||||
<slot />
|
||||
</div>
|
||||
<template v-if="sidebar">
|
||||
<template v-if="hierarchicalSidebarAvailable">
|
||||
<Teleport to="#sidebar-teleport-target">
|
||||
<slot name="sidebar" />
|
||||
</Teleport>
|
||||
</template>
|
||||
<template v-else>
|
||||
<div class="ui-normal-page__sidebar">
|
||||
<slot name="sidebar" />
|
||||
</div>
|
||||
</template>
|
||||
</template>
|
||||
</div>
|
||||
</template>
|
||||
<style scoped>
|
||||
.ui-normal-page {
|
||||
@apply grid gap-6 mx-auto py-4;
|
||||
width: min(calc(100% - 2rem), calc(80rem - 3rem));
|
||||
|
||||
grid-template:
|
||||
'header'
|
||||
'content'
|
||||
'sidebar'
|
||||
/ 100%;
|
||||
}
|
||||
|
||||
@media (width >= 64rem) {
|
||||
.ui-normal-page--sidebar-left {
|
||||
grid-template:
|
||||
'header header'
|
||||
'sidebar content'
|
||||
'sidebar dummy'
|
||||
/ 20rem 1fr;
|
||||
}
|
||||
|
||||
.ui-normal-page--sidebar-right {
|
||||
grid-template:
|
||||
'header header'
|
||||
'content sidebar'
|
||||
'dummy sidebar'
|
||||
/ 1fr 20rem;
|
||||
}
|
||||
}
|
||||
|
||||
.ui-normal-page__header {
|
||||
grid-area: header;
|
||||
}
|
||||
|
||||
.ui-normal-page__content {
|
||||
grid-area: content;
|
||||
}
|
||||
|
||||
.ui-normal-page__sidebar {
|
||||
grid-area: sidebar;
|
||||
}
|
||||
</style>
|
||||
20
packages/ui/src/components/page/SidebarCard.vue
Normal file
20
packages/ui/src/components/page/SidebarCard.vue
Normal file
@@ -0,0 +1,20 @@
|
||||
<script setup lang="ts">
|
||||
import { injectPageContext } from '../../providers'
|
||||
|
||||
const { hierarchicalSidebarAvailable } = injectPageContext()
|
||||
|
||||
defineProps<{
|
||||
title: string
|
||||
}>()
|
||||
</script>
|
||||
<template>
|
||||
<div
|
||||
class="flex flex-col gap-3 p-4"
|
||||
:class="{
|
||||
'card-shadow mb-4 last:mb-0 rounded-2xl bg-bg-raised': !hierarchicalSidebarAvailable,
|
||||
}"
|
||||
>
|
||||
<span class="font-semibold">{{ title }}</span>
|
||||
<slot />
|
||||
</div>
|
||||
</template>
|
||||
2
packages/ui/src/components/page/index.ts
Normal file
2
packages/ui/src/components/page/index.ts
Normal file
@@ -0,0 +1,2 @@
|
||||
export { default as NormalPage } from './NormalPage.vue'
|
||||
export { default as SidebarCard } from './SidebarCard.vue'
|
||||
@@ -1,107 +0,0 @@
|
||||
<template>
|
||||
<div>
|
||||
<Accordion
|
||||
v-for="filter in filters"
|
||||
:key="filter.id"
|
||||
v-model="filters"
|
||||
v-bind="$attrs"
|
||||
:button-class="buttonClass"
|
||||
:content-class="contentClass"
|
||||
open-by-default
|
||||
>
|
||||
<template #title>
|
||||
<slot name="header" :filter="filter">
|
||||
<h2>{{ filter.formatted_name }}</h2>
|
||||
</slot>
|
||||
</template>
|
||||
<template #default>
|
||||
<template v-for="option in filter.options" :key="`${filter.id}-${option}`">
|
||||
<slot name="option" :filter="filter" :option="option">
|
||||
<div>
|
||||
{{ option.formatted_name }}
|
||||
</div>
|
||||
</slot>
|
||||
</template>
|
||||
</template>
|
||||
</Accordion>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { computed } from 'vue'
|
||||
|
||||
import Accordion from '../base/Accordion.vue'
|
||||
|
||||
interface FilterOption<T> {
|
||||
id: string
|
||||
formatted_name: string
|
||||
data: T
|
||||
}
|
||||
|
||||
interface FilterType<T> {
|
||||
id: string
|
||||
formatted_name: string
|
||||
scrollable?: boolean
|
||||
options: FilterOption<T>[]
|
||||
}
|
||||
|
||||
interface GameVersion {
|
||||
version: string
|
||||
version_type: 'release' | 'snapshot' | 'alpha' | 'beta'
|
||||
date: string
|
||||
major: boolean
|
||||
}
|
||||
|
||||
type ProjectType = 'mod' | 'modpack' | 'resourcepack' | 'shader' | 'datapack' | 'plugin'
|
||||
|
||||
interface Platform {
|
||||
name: string
|
||||
icon: string
|
||||
supported_project_types: ProjectType[]
|
||||
default: boolean
|
||||
formatted_name: string
|
||||
}
|
||||
|
||||
const props = defineProps<{
|
||||
buttonClass?: string
|
||||
contentClass?: string
|
||||
gameVersions?: GameVersion[]
|
||||
platforms: Platform[]
|
||||
}>()
|
||||
|
||||
const filters = computed(() => {
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
const filters: FilterType<any>[] = [
|
||||
{
|
||||
id: 'platform',
|
||||
formatted_name: 'Platform',
|
||||
options:
|
||||
props.platforms
|
||||
.filter((x) => x.default && x.supported_project_types.includes('modpack'))
|
||||
.map((x) => ({
|
||||
id: x.name,
|
||||
formatted_name: x.formatted_name,
|
||||
data: x,
|
||||
})) || [],
|
||||
},
|
||||
{
|
||||
id: 'gameVersion',
|
||||
formatted_name: 'Game version',
|
||||
options:
|
||||
props.gameVersions
|
||||
?.filter((x) => x.major && x.version_type === 'release')
|
||||
.map((x) => ({
|
||||
id: x.version,
|
||||
formatted_name: x.version,
|
||||
data: x,
|
||||
})) || [],
|
||||
},
|
||||
]
|
||||
|
||||
return filters
|
||||
})
|
||||
|
||||
defineOptions({
|
||||
inheritAttrs: false,
|
||||
})
|
||||
</script>
|
||||
@@ -1,346 +0,0 @@
|
||||
<template>
|
||||
<div
|
||||
ref="dropdown"
|
||||
tabindex="0"
|
||||
role="combobox"
|
||||
:aria-expanded="dropdownVisible"
|
||||
class="animated-dropdown"
|
||||
@keydown.up.prevent="focusPreviousOption"
|
||||
@keydown.down.prevent="focusNextOptionOrOpen"
|
||||
>
|
||||
<div class="iconified-input">
|
||||
<SearchIcon />
|
||||
<input
|
||||
:value="modelValue"
|
||||
type="text"
|
||||
:name="name"
|
||||
:disabled="disabled"
|
||||
class="text-input"
|
||||
autocomplete="off"
|
||||
autocapitalize="off"
|
||||
:placeholder="placeholder"
|
||||
:class="{ down: !renderUp, up: renderUp }"
|
||||
@input="$emit('update:modelValue', $event.target.value)"
|
||||
@focus="onFocus"
|
||||
@blur="onBlur"
|
||||
@focusout="onBlur"
|
||||
@keydown.enter.prevent="$emit('enter')"
|
||||
/>
|
||||
<Button :disabled="disabled" class="r-btn" @click="() => $emit('update:modelValue', '')">
|
||||
<XIcon />
|
||||
</Button>
|
||||
</div>
|
||||
<div ref="dropdownOptions" class="options-wrapper" :class="{ down: !renderUp, up: renderUp }">
|
||||
<transition name="options">
|
||||
<div
|
||||
v-show="dropdownVisible"
|
||||
class="options"
|
||||
role="listbox"
|
||||
:class="{ down: !renderUp, up: renderUp }"
|
||||
>
|
||||
<div
|
||||
v-for="(option, index) in options"
|
||||
:key="index"
|
||||
ref="optionElements"
|
||||
tabindex="-1"
|
||||
role="option"
|
||||
class="option"
|
||||
@click="selectOption(option)"
|
||||
>
|
||||
<div class="project-label">
|
||||
<Avatar :src="option.icon" :circle="circledIcons" />
|
||||
<div class="text">
|
||||
<div class="title">
|
||||
{{ getOptionLabel(option.title) }}
|
||||
</div>
|
||||
<div class="author">
|
||||
{{ getOptionLabel(option.subtitle) }}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</transition>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { SearchIcon, XIcon } from '@modrinth/assets'
|
||||
import { ref } from 'vue'
|
||||
|
||||
import Avatar from '../base/Avatar.vue'
|
||||
import Button from '../base/Button.vue'
|
||||
|
||||
const props = defineProps({
|
||||
options: {
|
||||
type: Array,
|
||||
required: true,
|
||||
},
|
||||
name: {
|
||||
type: String,
|
||||
required: true,
|
||||
},
|
||||
placeholder: {
|
||||
type: [String, Number],
|
||||
default: null,
|
||||
},
|
||||
modelValue: {
|
||||
type: [String, Number, Object],
|
||||
default: null,
|
||||
},
|
||||
renderUp: {
|
||||
type: Boolean,
|
||||
default: false,
|
||||
},
|
||||
disabled: {
|
||||
type: Boolean,
|
||||
default: false,
|
||||
},
|
||||
displayName: {
|
||||
type: Function,
|
||||
default: undefined,
|
||||
},
|
||||
circledIcons: {
|
||||
type: Boolean,
|
||||
default: false,
|
||||
},
|
||||
})
|
||||
|
||||
function getOptionLabel(option) {
|
||||
return props.displayName?.(option) ?? option
|
||||
}
|
||||
|
||||
const emit = defineEmits(['input', 'onSelected', 'update:modelValue', 'enter'])
|
||||
|
||||
const dropdownVisible = ref(false)
|
||||
const focusedOptionIndex = ref(null)
|
||||
const dropdown = ref(null)
|
||||
const optionElements = ref(null)
|
||||
const dropdownOptions = ref(null)
|
||||
|
||||
const toggleDropdown = () => {
|
||||
if (!props.disabled) {
|
||||
dropdownVisible.value = !dropdownVisible.value
|
||||
dropdown.value.focus()
|
||||
}
|
||||
}
|
||||
|
||||
const selectOption = (option) => {
|
||||
emit('onSelected', option)
|
||||
console.log('onSelected', option)
|
||||
dropdownVisible.value = false
|
||||
}
|
||||
|
||||
const onFocus = () => {
|
||||
if (!props.disabled) {
|
||||
focusedOptionIndex.value = props.options.findIndex(
|
||||
(option) => option === props.modelValue.value,
|
||||
)
|
||||
dropdownVisible.value = true
|
||||
}
|
||||
}
|
||||
|
||||
const onBlur = (event) => {
|
||||
console.log(event)
|
||||
if (!isChildOfDropdown(event.relatedTarget)) {
|
||||
dropdownVisible.value = false
|
||||
}
|
||||
}
|
||||
|
||||
const focusPreviousOption = () => {
|
||||
if (!props.disabled) {
|
||||
if (!dropdownVisible.value) {
|
||||
toggleDropdown()
|
||||
}
|
||||
focusedOptionIndex.value =
|
||||
(focusedOptionIndex.value + props.options.length - 1) % props.options.length
|
||||
optionElements.value[focusedOptionIndex.value].focus()
|
||||
}
|
||||
}
|
||||
|
||||
const focusNextOptionOrOpen = () => {
|
||||
if (!props.disabled) {
|
||||
if (!dropdownVisible.value) {
|
||||
toggleDropdown()
|
||||
}
|
||||
focusedOptionIndex.value = (focusedOptionIndex.value + 1) % props.options.length
|
||||
optionElements.value[focusedOptionIndex.value].focus()
|
||||
}
|
||||
}
|
||||
|
||||
const isChildOfDropdown = (element) => {
|
||||
let currentNode = element
|
||||
while (currentNode) {
|
||||
if (currentNode === dropdownOptions.value) {
|
||||
return true
|
||||
}
|
||||
currentNode = currentNode.parentNode
|
||||
}
|
||||
return false
|
||||
}
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
.animated-dropdown {
|
||||
width: 20rem;
|
||||
height: 2.5rem;
|
||||
position: relative;
|
||||
display: inline-block;
|
||||
|
||||
&:focus {
|
||||
outline: 0;
|
||||
}
|
||||
|
||||
.selected {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
padding: var(--gap-sm) var(--gap-lg);
|
||||
background-color: var(--color-button-bg);
|
||||
gap: var(--gap-md);
|
||||
cursor: pointer;
|
||||
user-select: none;
|
||||
border-radius: var(--radius-md);
|
||||
box-shadow:
|
||||
var(--shadow-inset-sm),
|
||||
0 0 0 0 transparent;
|
||||
|
||||
&.disabled {
|
||||
cursor: not-allowed;
|
||||
filter: grayscale(50%);
|
||||
opacity: 0.5;
|
||||
}
|
||||
|
||||
&.render-up {
|
||||
border-radius: 0 0 var(--radius-md) var(--radius-md);
|
||||
}
|
||||
|
||||
&.render-down {
|
||||
border-radius: var(--radius-md) var(--radius-md) 0 0;
|
||||
}
|
||||
|
||||
&:focus {
|
||||
outline: 0;
|
||||
filter: brightness(1.25);
|
||||
transition: filter 0.1s ease-in-out;
|
||||
}
|
||||
}
|
||||
|
||||
.options {
|
||||
z-index: 10;
|
||||
max-height: 18rem;
|
||||
overflow-y: auto;
|
||||
|
||||
.option {
|
||||
background-color: var(--color-button-bg);
|
||||
display: flex;
|
||||
align-items: center;
|
||||
padding: var(--gap-md);
|
||||
cursor: pointer;
|
||||
user-select: none;
|
||||
|
||||
&:hover {
|
||||
filter: brightness(0.85);
|
||||
transition: filter 0.2s ease-in-out;
|
||||
}
|
||||
|
||||
&:focus {
|
||||
outline: 0;
|
||||
filter: brightness(0.85);
|
||||
transition: filter 0.2s ease-in-out;
|
||||
}
|
||||
|
||||
&.selected-option {
|
||||
background-color: var(--color-brand);
|
||||
color: var(--color-accent-contrast);
|
||||
font-weight: bolder;
|
||||
}
|
||||
|
||||
input {
|
||||
display: none;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.options-enter-active,
|
||||
.options-leave-active {
|
||||
transition: transform 0.2s ease;
|
||||
}
|
||||
|
||||
.options-enter-from,
|
||||
.options-leave-to {
|
||||
// this is not 100% due to a safari bug
|
||||
&.up {
|
||||
transform: translateY(99.999%);
|
||||
}
|
||||
|
||||
&.down {
|
||||
transform: translateY(-99.999%);
|
||||
}
|
||||
}
|
||||
|
||||
.options-enter-to,
|
||||
.options-leave-from {
|
||||
&.up {
|
||||
transform: translateY(0%);
|
||||
}
|
||||
}
|
||||
|
||||
.options-wrapper {
|
||||
position: absolute;
|
||||
width: 100%;
|
||||
overflow: auto;
|
||||
z-index: 9;
|
||||
|
||||
&.up {
|
||||
top: 0;
|
||||
transform: translateY(-99.999%);
|
||||
border-radius: var(--radius-md) var(--radius-md) 0 0;
|
||||
}
|
||||
|
||||
&.down {
|
||||
border-radius: 0 0 var(--radius-md) var(--radius-md);
|
||||
}
|
||||
}
|
||||
|
||||
.project-label {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
flex-direction: row;
|
||||
gap: var(--gap-md);
|
||||
color: var(--color-contrast);
|
||||
|
||||
.title {
|
||||
font-weight: bold;
|
||||
}
|
||||
}
|
||||
|
||||
.iconified-input {
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.text-input {
|
||||
box-shadow:
|
||||
var(--shadow-inset-sm),
|
||||
0 0 0 0 transparent !important;
|
||||
width: 100%;
|
||||
|
||||
transition: 0.05s;
|
||||
|
||||
&:focus {
|
||||
&.down {
|
||||
border-radius: var(--radius-md) var(--radius-md) 0 0 !important;
|
||||
}
|
||||
|
||||
&.up {
|
||||
border-radius: 0 0 var(--radius-md) var(--radius-md) !important;
|
||||
}
|
||||
}
|
||||
|
||||
&:not(:focus) {
|
||||
transition-delay: 0.2s;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
@@ -1,75 +0,0 @@
|
||||
<template>
|
||||
<Checkbox
|
||||
class="filter"
|
||||
:model-value="isActive"
|
||||
:description="displayName"
|
||||
@update:model-value="toggle"
|
||||
>
|
||||
<div class="filter-text">
|
||||
<div v-if="props.icon" aria-hidden="true" class="icon" v-html="props.icon" />
|
||||
<div v-else class="icon">
|
||||
<slot />
|
||||
</div>
|
||||
<span aria-hidden="true"> {{ props.displayName }}</span>
|
||||
</div>
|
||||
</Checkbox>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { computed } from 'vue'
|
||||
|
||||
import Checkbox from '../base/Checkbox.vue'
|
||||
|
||||
const props = defineProps({
|
||||
facetName: {
|
||||
type: String,
|
||||
default: '',
|
||||
},
|
||||
displayName: {
|
||||
type: String,
|
||||
default: '',
|
||||
},
|
||||
icon: {
|
||||
type: String,
|
||||
default: '',
|
||||
},
|
||||
activeFilters: {
|
||||
type: Array,
|
||||
default() {
|
||||
return []
|
||||
},
|
||||
},
|
||||
})
|
||||
|
||||
const isActive = computed(() => props.activeFilters.includes(props.facetName))
|
||||
const emit = defineEmits(['toggle'])
|
||||
|
||||
const toggle = () => {
|
||||
emit('toggle', props.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>
|
||||
4
packages/ui/src/components/search/index.ts
Normal file
4
packages/ui/src/components/search/index.ts
Normal file
@@ -0,0 +1,4 @@
|
||||
export { default as Categories } from './Categories.vue'
|
||||
export { default as SearchFilterControl } from './SearchFilterControl.vue'
|
||||
export { default as SearchFilterOption } from './SearchFilterOption.vue'
|
||||
export { default as SearchSidebarFilter } from './SearchSidebarFilter.vue'
|
||||
2
packages/ui/src/components/servers/icons/index.ts
Normal file
2
packages/ui/src/components/servers/icons/index.ts
Normal file
@@ -0,0 +1,2 @@
|
||||
export { default as LoaderIcon } from './LoaderIcon.vue'
|
||||
export { default as ServerIcon } from './ServerIcon.vue'
|
||||
7
packages/ui/src/components/servers/index.ts
Normal file
7
packages/ui/src/components/servers/index.ts
Normal file
@@ -0,0 +1,7 @@
|
||||
export * from './backups'
|
||||
export * from './icons'
|
||||
export * from './labels'
|
||||
export * from './marketing'
|
||||
export type { PendingChange } from './ServerListing.vue'
|
||||
export { default as ServerListing } from './ServerListing.vue'
|
||||
export { default as ServersPromo } from './ServersPromo.vue'
|
||||
1
packages/ui/src/components/servers/labels/index.ts
Normal file
1
packages/ui/src/components/servers/labels/index.ts
Normal file
@@ -0,0 +1 @@
|
||||
export { default as ServerInfoLabels } from './ServerInfoLabels.vue'
|
||||
2
packages/ui/src/components/servers/marketing/index.ts
Normal file
2
packages/ui/src/components/servers/marketing/index.ts
Normal file
@@ -0,0 +1,2 @@
|
||||
export { default as MedalBackgroundImage } from './MedalBackgroundImage.vue'
|
||||
export { default as MedalServerListing } from './MedalServerListing.vue'
|
||||
1
packages/ui/src/components/settings/index.ts
Normal file
1
packages/ui/src/components/settings/index.ts
Normal file
@@ -0,0 +1 @@
|
||||
export { default as ThemeSelector } from './ThemeSelector.vue'
|
||||
5
packages/ui/src/components/skin/index.ts
Normal file
5
packages/ui/src/components/skin/index.ts
Normal file
@@ -0,0 +1,5 @@
|
||||
export { default as CapeButton } from './CapeButton.vue'
|
||||
export { default as CapeLikeTextButton } from './CapeLikeTextButton.vue'
|
||||
export { default as SkinButton } from './SkinButton.vue'
|
||||
export { default as SkinLikeTextButton } from './SkinLikeTextButton.vue'
|
||||
export { default as SkinPreviewRenderer } from './SkinPreviewRenderer.vue'
|
||||
3
packages/ui/src/components/version/index.ts
Normal file
3
packages/ui/src/components/version/index.ts
Normal file
@@ -0,0 +1,3 @@
|
||||
export { default as VersionChannelIndicator } from './VersionChannelIndicator.vue'
|
||||
export { default as VersionFilterControl } from './VersionFilterControl.vue'
|
||||
export { default as VersionSummary } from './VersionSummary.vue'
|
||||
Reference in New Issue
Block a user