feat(registry): implement responsive UI with glassmorphism and header fix

This commit is contained in:
MivoDev
2026-01-18 15:56:41 +07:00
parent e84ff3224a
commit 9558373ff9
20 changed files with 540 additions and 22590 deletions

View File

@@ -1,58 +1,58 @@
{
"hash": "351ebb31",
"configHash": "9f61585a",
"lockfileHash": "a8ce03f4",
"browserHash": "3a62ec11",
"hash": "531e103b",
"configHash": "fbad939b",
"lockfileHash": "1aaf5e24",
"browserHash": "cbba353b",
"optimized": {
"vue": {
"src": "../../../../node_modules/vue/dist/vue.runtime.esm-bundler.js",
"src": "../../../node_modules/vue/dist/vue.runtime.esm-bundler.js",
"file": "vue.js",
"fileHash": "5b1d304c",
"fileHash": "17910767",
"needsInterop": false
},
"vitepress > @vue/devtools-api": {
"src": "../../../../node_modules/@vue/devtools-api/dist/index.js",
"src": "../../../node_modules/@vue/devtools-api/dist/index.js",
"file": "vitepress___@vue_devtools-api.js",
"fileHash": "b482c46a",
"fileHash": "f94d00b9",
"needsInterop": false
},
"vitepress > @vueuse/core": {
"src": "../../../../node_modules/@vueuse/core/index.mjs",
"src": "../../../node_modules/@vueuse/core/index.mjs",
"file": "vitepress___@vueuse_core.js",
"fileHash": "a5c5890d",
"fileHash": "8d0ef6a2",
"needsInterop": false
},
"vitepress > @vueuse/integrations/useFocusTrap": {
"src": "../../../../node_modules/@vueuse/integrations/useFocusTrap.mjs",
"src": "../../../node_modules/@vueuse/integrations/useFocusTrap.mjs",
"file": "vitepress___@vueuse_integrations_useFocusTrap.js",
"fileHash": "95f44bf0",
"fileHash": "ca9c2986",
"needsInterop": false
},
"vitepress > mark.js/src/vanilla.js": {
"src": "../../../../node_modules/mark.js/src/vanilla.js",
"src": "../../../node_modules/mark.js/src/vanilla.js",
"file": "vitepress___mark__js_src_vanilla__js.js",
"fileHash": "9dc093d4",
"fileHash": "da4cb2aa",
"needsInterop": false
},
"vitepress > minisearch": {
"src": "../../../../node_modules/minisearch/dist/es/index.js",
"src": "../../../node_modules/minisearch/dist/es/index.js",
"file": "vitepress___minisearch.js",
"fileHash": "55d2ce5f",
"fileHash": "3714d5a0",
"needsInterop": false
},
"lucide-vue-next": {
"src": "../../../../node_modules/lucide-vue-next/dist/esm/lucide-vue-next.js",
"src": "../../../node_modules/lucide-vue-next/dist/esm/lucide-vue-next.js",
"file": "lucide-vue-next.js",
"fileHash": "d70b9ca5",
"fileHash": "1758a082",
"needsInterop": false
}
},
"chunks": {
"chunk-2CLQ7TTZ": {
"file": "chunk-2CLQ7TTZ.js"
"chunk-LJKO4TMH": {
"file": "chunk-LJKO4TMH.js"
},
"chunk-LE5NDSFD": {
"file": "chunk-LE5NDSFD.js"
"chunk-QE257C5J": {
"file": "chunk-QE257C5J.js"
},
"chunk-PZ5AY32C": {
"file": "chunk-PZ5AY32C.js"

File diff suppressed because it is too large Load Diff

File diff suppressed because one or more lines are too long

File diff suppressed because it is too large Load Diff

File diff suppressed because one or more lines are too long

View File

@@ -1,6 +1,6 @@
import {
h
} from "./chunk-LE5NDSFD.js";
} from "./chunk-QE257C5J.js";
import {
__export
} from "./chunk-PZ5AY32C.js";

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

View File

@@ -281,8 +281,8 @@ import {
watchTriggerable,
watchWithFilter,
whenever
} from "./chunk-2CLQ7TTZ.js";
import "./chunk-LE5NDSFD.js";
} from "./chunk-LJKO4TMH.js";
import "./chunk-QE257C5J.js";
import "./chunk-PZ5AY32C.js";
export {
DefaultMagicKeysAliasMap,

View File

@@ -3,13 +3,13 @@ import {
toArray,
tryOnScopeDispose,
unrefElement
} from "./chunk-2CLQ7TTZ.js";
} from "./chunk-LJKO4TMH.js";
import {
computed,
shallowRef,
toValue,
watch
} from "./chunk-LE5NDSFD.js";
} from "./chunk-QE257C5J.js";
import "./chunk-PZ5AY32C.js";
// node_modules/tabbable/dist/index.esm.js

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

View File

@@ -170,7 +170,7 @@ import {
withMemo,
withModifiers,
withScopeId
} from "./chunk-LE5NDSFD.js";
} from "./chunk-QE257C5J.js";
import "./chunk-PZ5AY32C.js";
export {
BaseTransition,

View File

@@ -4,6 +4,7 @@ export const navEn: DefaultTheme.NavItem[] = [
{ text: 'Home', link: '/' },
{ text: 'Guide', link: '/docs/guide/installation' },
{ text: 'Manual', link: '/docs/manual/' },
{ text: 'Plugins', link: '/plugins/' },
{
text: 'Community',
items: [
@@ -19,6 +20,7 @@ export const navId: DefaultTheme.NavItem[] = [
{ text: 'Beranda', link: '/id/' },
{ text: 'Panduan', link: '/id/docs/guide/installation' },
{ text: 'Buku Manual', link: '/id/docs/manual/' },
{ text: 'Plugin', link: '/plugins/' },
{
text: 'Komunitas',
items: [

View File

@@ -0,0 +1,446 @@
<script setup>
import { ref, computed, onMounted } from 'vue'
const props = defineProps({
title: { type: String, default: 'Plugin Registry' },
description: { type: String, default: 'Explore extensions for Mivo.' }
})
const plugins = ref([])
const loading = ref(true)
const searchQuery = ref('')
const activeCategory = ref('All')
// Fetch data
onMounted(async () => {
try {
const res = await fetch('/plugins.json')
plugins.value = await res.json()
} catch (e) {
console.error("Failed to load plugins", e)
} finally {
loading.value = false
}
})
// Categories
const categories = computed(() => {
if (!plugins.value.length) return ['All']
const cats = new Set(plugins.value.map(p => p.category).filter(Boolean))
return ['All', ...Array.from(cats).sort()]
})
// Filter Logic
const filteredPlugins = computed(() => {
return plugins.value.filter(p => {
const matchesSearch = p.name.toLowerCase().includes(searchQuery.value.toLowerCase()) ||
p.description.toLowerCase().includes(searchQuery.value.toLowerCase()) ||
(p.tags && p.tags.some(t => t.toLowerCase().includes(searchQuery.value.toLowerCase())))
const matchesCategory = activeCategory.value === 'All' || p.category === activeCategory.value
return matchesSearch && matchesCategory
})
})
function copyToClipboard(text) {
navigator.clipboard.writeText(text)
alert('Clipboard action')
}
</script>
<template>
<div class="registry-container">
<!-- Unified Header -->
<div class="registry-header">
<h1 class="page-title">{{ title }}</h1>
<p class="page-description">{{ description }}</p>
</div>
<div class="plugin-registry space-y-8">
<!-- Search & Filter Controls -->
<div class="controls-wrapper glass-panel">
<div class="search-container">
<div class="search-icon">
<svg xmlns="http://www.w3.org/2000/svg" width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><circle cx="11" cy="11" r="8"></circle><line x1="21" y1="21" x2="16.65" y2="16.65"></line></svg>
</div>
<input
v-model="searchQuery"
type="text"
placeholder="Find plugins..."
class="search-input"
/>
</div>
<div class="vr hidden sm:block"></div>
<div class="categories">
<button
v-for="cat in categories"
:key="cat"
@click="activeCategory = cat"
:class="{ active: activeCategory === cat }"
class="category-pill"
>
{{ cat }}
</button>
</div>
</div>
<div v-if="loading" class="loading-state">
<div class="spinner"></div>
<span>Loading registry...</span>
</div>
<div v-else class="plugin-grid transition-opacity duration-500" :class="{ 'opacity-0': loading }">
<div v-for="plugin in filteredPlugins" :key="plugin.id" class="VPFeature plugin-card group">
<div class="card-body">
<div class="card-header">
<div class="plugin-icon-wrapper">
<span class="plugin-initial">{{ plugin.name.charAt(0) }}</span>
</div>
<div class="plugin-title-block">
<h3 class="plugin-title group-hover:text-brand transition-colors">{{ plugin.name }}</h3>
<div class="plugin-meta-row">
<a :href="plugin.repo" target="_blank" class="plugin-author" title="View Author">
<svg xmlns="http://www.w3.org/2000/svg" width="12" height="12" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M20 21v-2a4 4 0 0 0-4-4H8a4 4 0 0 0-4 4v2"></path><circle cx="12" cy="7" r="4"></circle></svg>
{{ plugin.author }}
</a>
<span class="dot"></span>
<span class="version">{{ plugin.version }}</span>
</div>
</div>
</div>
<p class="plugin-description">{{ plugin.description }}</p>
<div class="plugin-tags">
<span v-for="tag in plugin.tags" :key="tag" class="tag-pill">{{ tag }}</span>
</div>
</div>
<div class="card-footer">
<a :href="plugin.readme" target="_blank" class="VPButton alt action-btn">Docs</a>
<a :href="plugin.download" class="VPButton brand action-btn">Download</a>
</div>
</div>
</div>
<div v-if="!loading && filteredPlugins.length === 0" class="empty-state">
<div class="empty-icon-wrapper">
<svg xmlns="http://www.w3.org/2000/svg" width="48" height="48" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"><circle cx="11" cy="11" r="8"></circle><line x1="21" y1="21" x2="16.65" y2="16.65"></line></svg>
</div>
<p class="empty-text">No plugins found matching "{{ searchQuery }}"</p>
<button @click="searchQuery = ''; activeCategory = 'All'" class="link-btn">Clear filters</button>
</div>
</div>
</div>
</template>
<style scoped>
/* Container & Typography */
.registry-container {
max-width: 1280px; /* XL breakpoint */
margin: 0 auto;
width: 100%;
padding: 2rem 0;
}
.registry-header {
margin-bottom: 3rem;
text-align: left;
}
.page-title {
font-size: 32px;
font-weight: 800;
letter-spacing: -0.02em;
line-height: 1.2;
margin-bottom: 0.75rem;
background: -webkit-linear-gradient(315deg, var(--vp-c-brand-1) 25%, var(--vp-c-text-1));
-webkit-background-clip: text;
-webkit-text-fill-color: transparent;
}
.dark .page-title {
background: -webkit-linear-gradient(315deg, #fff 25%, var(--vp-c-brand-1));
-webkit-background-clip: text;
-webkit-text-fill-color: transparent;
}
.page-description {
font-size: 18px;
color: var(--vp-c-text-2);
max-width: 600px;
line-height: 1.6;
}
.plugin-registry {
/* No margin top needed as header handles spacing */
min-height: 50vh;
}
/* Glass Control Panel */
.glass-panel {
background: var(--vp-c-bg-alt);
border: 1px solid var(--vp-c-divider);
border-radius: 12px; /* rounded-xl */
padding: 8px;
display: flex;
flex-direction: column;
gap: 12px;
margin-bottom: 32px; /* Increased separation */
box-shadow: 0 4px 6px -1px rgba(0, 0, 0, 0.05);
}
@media (min-width: 640px) {
.glass-panel {
flex-direction: row;
align-items: center;
padding: 8px 16px;
}
}
.search-container {
position: relative;
flex: 1;
}
.search-icon {
position: absolute;
left: 10px;
top: 50%;
transform: translateY(-50%);
color: var(--vp-c-text-2);
pointer-events: none;
}
.search-input {
width: 100%;
padding: 8px 12px 8px 36px;
border-radius: 8px;
border: 1px solid transparent;
background: transparent;
color: var(--vp-c-text-1);
font-size: 14px;
transition: all 0.2s;
}
.search-input:focus {
background: var(--vp-c-bg);
border-color: var(--vp-c-brand-1);
outline: none;
box-shadow: 0 0 0 2px rgba(var(--vp-c-brand-1), 0.1);
}
/* Vertical Separator */
.vr {
width: 1px;
height: 24px;
background-color: var(--vp-c-divider);
margin: 0 8px;
}
.categories {
display: flex;
gap: 6px;
overflow-x: auto;
padding: 4px 0;
-ms-overflow-style: none;
scrollbar-width: none;
}
.category-pill {
padding: 4px 12px;
border-radius: 6px; /* Slightly softer than pill */
border: 1px solid transparent;
background: transparent;
color: var(--vp-c-text-2);
font-size: 13px;
font-weight: 500;
white-space: nowrap;
transition: all 0.2s;
}
.category-pill:hover {
background: var(--vp-c-bg-elv);
color: var(--vp-c-text-1);
}
.category-pill.active {
background: var(--vp-c-brand-1);
color: var(--vp-c-bg);
box-shadow: 0 2px 4px rgba(0,0,0,0.1);
}
/* Grid & Cards */
.plugin-grid {
display: grid;
grid-template-columns: 1fr;
gap: 20px;
}
@media (min-width: 768px) {
.plugin-grid {
grid-template-columns: repeat(2, 1fr);
}
}
@media (min-width: 1024px) {
.plugin-grid {
grid-template-columns: repeat(3, 1fr);
}
}
.plugin-card {
display: flex;
flex-direction: column;
height: 100%;
padding: 24px;
/* .VPFeature provides main styling */
}
.card-header {
display: flex;
gap: 16px;
align-items: flex-start;
margin-bottom: 16px;
}
.plugin-icon-wrapper {
width: 44px;
height: 44px;
background: linear-gradient(135deg, var(--vp-c-brand-1), var(--vp-c-brand-2));
border-radius: 10px;
display: flex;
align-items: center;
justify-content: center;
flex-shrink: 0;
box-shadow: 0 4px 10px rgba(0,0,0,0.1);
}
.plugin-initial {
font-size: 20px;
font-weight: 700;
color: #fff;
text-shadow: 0 2px 4px rgba(0,0,0,0.2);
}
.dark .plugin-initial {
color: #000;
text-shadow: none;
}
.plugin-title-block {
flex: 1;
min-width: 0;
}
.plugin-title {
margin: 0 !important;
font-size: 17px;
font-weight: 600;
line-height: 1.3;
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
}
.group-hover\:text-brand:hover {
color: var(--vp-c-brand-1);
}
.plugin-meta-row {
display: flex;
align-items: center;
gap: 6px;
margin-top: 4px;
font-size: 13px;
color: var(--vp-c-text-2);
}
.plugin-author {
display: flex;
align-items: center;
gap: 4px;
color: inherit;
text-decoration: none !important;
transition: color 0.2s;
}
.plugin-author:hover {
color: var(--vp-c-brand-1);
}
.dot {
opacity: 0.5;
}
.plugin-description {
font-size: 14px;
color: var(--vp-c-text-2);
line-height: 1.6;
margin: 0 0 20px 0;
display: -webkit-box;
-webkit-line-clamp: 2;
-webkit-box-orient: vertical;
overflow: hidden;
min-height: 44px; /* Maintain height alignment */
}
.plugin-tags {
display: flex;
flex-wrap: wrap;
gap: 6px;
margin-bottom: 20px;
}
.tag-pill {
font-size: 11px;
padding: 2px 8px;
border-radius: 6px;
background: var(--vp-c-bg); /* Contrast against card bg */
color: var(--vp-c-text-2);
border: 1px solid var(--vp-c-divider);
}
.card-footer {
margin-top: auto;
display: flex;
gap: 10px;
}
.action-btn {
flex: 1;
display: inline-flex;
align-items: center;
justify-content: center;
padding: 8px 16px;
font-size: 13px;
font-weight: 500;
border-radius: 6px;
cursor: pointer;
text-decoration: none !important;
transition: all 0.2s;
}
.empty-state {
padding: 4rem 2rem;
text-align: center;
background: var(--vp-c-bg-alt);
border-radius: 12px;
border: 1px dashed var(--vp-c-divider);
}
.empty-icon-wrapper {
color: var(--vp-c-text-3);
margin-bottom: 1rem;
display: inline-block;
}
.empty-text {
font-size: 16px;
color: var(--vp-c-text-2);
margin-bottom: 1rem;
}
</style>

View File

@@ -3,6 +3,7 @@ import type { EnhanceAppContext } from 'vitepress'
import Layout from './Layout.vue'
import Icon from './components/Icon.vue'
import 'flag-icons/css/flag-icons.min.css'
import PluginRegistry from './components/PluginRegistry.vue'
import './input.css'
export default {
@@ -10,5 +11,6 @@ export default {
Layout: Layout,
enhanceApp({ app }: EnhanceAppContext) {
app.component('Icon', Icon)
app.component('PluginRegistry', PluginRegistry)
}
}

10
id/plugins/index.md Normal file
View File

@@ -0,0 +1,10 @@
---
title: Direktori Plugin
description: Jelajahi dan unduh ekstensi untuk Mivo.
layout: page
---
<PluginRegistry
title="Direktori Plugin Mivo"
description="Jelajahi plugin resmi dan komunitas untuk memperluas fungsionalitas Mivo."
/>

10
plugins/index.md Normal file
View File

@@ -0,0 +1,10 @@
---
title: Plugins Registry
description: Browse and download extensions for Mivo.
layout: page
---
<PluginRegistry
title="Mivo Plugin Registry"
description="Explore official and community plugins to extend Mivo's functionality."
/>

37
public/plugins.json Normal file
View File

@@ -0,0 +1,37 @@
[
{
"id": "plugin-mivo-theme",
"name": "Mivo Theme Downloader",
"description": "Allows downloading the Captive Portal theme with auto-configuration.",
"version": "1.0.0",
"author": "DyzulkDev",
"category": "Hotspot Tools",
"scope": "Session",
"tags": [
"theme",
"downloader",
"hotspot",
"captive-portal"
],
"repo": "https://github.com/mivodev/plugin-mivo-theme",
"download": "https://github.com/mivodev/plugin-mivo-theme/archive/refs/heads/main.zip",
"readme": "https://raw.githubusercontent.com/mivodev/plugin-mivo-theme/main/README.md"
},
{
"id": "plugin-backup-manager",
"name": "Backup Manager (Demo)",
"description": "Automated daily backups to Google Drive.",
"version": "2.1.0",
"author": "MivoTeam",
"category": "System Tools",
"scope": "Global",
"tags": [
"backup",
"security",
"cloud"
],
"repo": "#",
"download": "#",
"readme": "#"
}
]