mirror of
https://github.com/DavidHDev/vue-bits.git
synced 2026-03-07 14:39:30 -07:00
documentation structure
This commit is contained in:
144
src/components/code/CliInstallation.vue
Normal file
144
src/components/code/CliInstallation.vue
Normal file
@@ -0,0 +1,144 @@
|
||||
<template>
|
||||
<div class="cli-installation">
|
||||
<h2 class="demo-title">One-Time Installation</h2>
|
||||
|
||||
<VCodeBlock v-if="command" :code="command" :persistent-copy-button="true" highlightjs lang="bash" theme="nord"
|
||||
:copy-button="true" class="code-block" />
|
||||
|
||||
<div class="cli-divider"></div>
|
||||
|
||||
<h2 class="demo-title">Full CLI Setup</h2>
|
||||
<p class="jsrepo-info">
|
||||
Vue Bits uses
|
||||
<a href="https://jsrepo.dev/" target="_blank" rel="noreferrer">jsrepo</a>
|
||||
to help you install components via CLI— you can set it up project-wide following the steps below.
|
||||
</p>
|
||||
|
||||
<Accordion expandIcon="pi pi-chevron-right" collapseIcon="pi pi-chevron-down">
|
||||
<AccordionPanel value="setup">
|
||||
<AccordionHeader>Setup Steps</AccordionHeader>
|
||||
<AccordionContent>
|
||||
<div class="setup-content">
|
||||
<p class="demo-extra-info">1. Initialize a config file for your project</p>
|
||||
|
||||
<div class="setup-option">
|
||||
<VCodeBlock :persistent-copy-button="true" code="npx jsrepo init https://vuebits.dev" highlightjs
|
||||
lang="bash" theme="nord" :copy-button="true" class="code-block" />
|
||||
</div>
|
||||
|
||||
<p class="demo-extra-info">2. Browse & add components from the list</p>
|
||||
<VCodeBlock :persistent-copy-button="true" code="npx jsrepo add" highlightjs lang="bash" theme="nord"
|
||||
:copy-button="true" class="code-block" />
|
||||
|
||||
<p class="demo-extra-info">3. Or just add a specific component</p>
|
||||
<VCodeBlock :persistent-copy-button="true" code="npx jsrepo add Animations/AnimatedContainer" highlightjs
|
||||
lang="bash" theme="nord" :copy-button="true" class="code-block" />
|
||||
</div>
|
||||
</AccordionContent>
|
||||
</AccordionPanel>
|
||||
</Accordion>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { VCodeBlock } from '@wdns/vue-code-block'
|
||||
import Accordion from 'primevue/accordion'
|
||||
import AccordionPanel from 'primevue/accordionpanel'
|
||||
import AccordionHeader from 'primevue/accordionheader'
|
||||
import AccordionContent from 'primevue/accordioncontent'
|
||||
|
||||
const { command } = defineProps<{
|
||||
command?: string
|
||||
}>()
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.cli-installation {
|
||||
padding-bottom: 1.2rem;
|
||||
font-size: 16px;
|
||||
}
|
||||
|
||||
.setup-content {
|
||||
padding: 1rem 0;
|
||||
}
|
||||
|
||||
.setup-option {
|
||||
margin-bottom: 1.5rem;
|
||||
}
|
||||
|
||||
.jsrepo-info {
|
||||
color: #a1a1aa;
|
||||
font-size: 14px;
|
||||
margin: 1rem 0;
|
||||
}
|
||||
|
||||
.jsrepo-info a {
|
||||
color: #27FF64;
|
||||
text-decoration: underline;
|
||||
}
|
||||
|
||||
.code-block {
|
||||
border-radius: 8px;
|
||||
overflow: hidden;
|
||||
border: 1px solid #142216;
|
||||
}
|
||||
|
||||
:deep(.p-accordion-header) {
|
||||
background: #0b0b0b;
|
||||
border: 1px solid #142216;
|
||||
border-radius: 20px;
|
||||
}
|
||||
|
||||
:deep(.p-accordionpanel) {
|
||||
border: none;
|
||||
}
|
||||
|
||||
:deep(.p-accordionheader-toggle-icon) {
|
||||
color: #fff !important;
|
||||
fill: #fff !important;
|
||||
}
|
||||
|
||||
:deep(.p-accordionheader-toggle-icon svg) {
|
||||
color: #fff !important;
|
||||
fill: #fff !important;
|
||||
}
|
||||
|
||||
:deep(.p-accordionheader-toggle-icon path) {
|
||||
fill: #fff !important;
|
||||
stroke: #fff !important;
|
||||
}
|
||||
|
||||
:deep(.p-accordion-content) {
|
||||
background: #0b0b0b;
|
||||
border: 1px solid #142216;
|
||||
border-top: none;
|
||||
border-radius: 0 0 20px 20px;
|
||||
}
|
||||
|
||||
:deep(.v-code-block) {
|
||||
background: #0b0b0b;
|
||||
font-weight: 400;
|
||||
font-size: 14px;
|
||||
}
|
||||
|
||||
:deep(.v-code-block--tab-highlightjs-github-dark-icon) {
|
||||
color: #999 !important;
|
||||
fill: #999 !important;
|
||||
}
|
||||
|
||||
:deep(.v-code-block--me-1) {
|
||||
margin-right: 0 !important;
|
||||
}
|
||||
|
||||
:deep(.v-code-block pre) {
|
||||
background: #0b0b0b;
|
||||
margin: 0;
|
||||
font-size: 14px;
|
||||
}
|
||||
|
||||
:deep(.v-code-block .hljs) {
|
||||
background: #0b0b0b;
|
||||
color: #fff;
|
||||
font-size: 14px;
|
||||
}
|
||||
</style>
|
||||
110
src/components/code/CodeExample.vue
Normal file
110
src/components/code/CodeExample.vue
Normal file
@@ -0,0 +1,110 @@
|
||||
<template>
|
||||
<div class="code-example">
|
||||
<div v-for="[name, snippet] in codeEntries" :key="name" class="code-section">
|
||||
<h2 v-if="shouldShowTitle()" class="demo-title">{{ getDisplayName(name) }}</h2>
|
||||
|
||||
<VCodeBlock v-if="snippet" :code="snippet" highlightjs :lang="getLanguage(name)" theme="nord"
|
||||
:copy-button="true" :persistent-copy-button="true" class="code-block" />
|
||||
|
||||
<div v-if="!snippet" class="no-code">
|
||||
<span>Nothing here yet!</span>
|
||||
<i class="pi pi-face-sad"></i>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { computed } from 'vue'
|
||||
import { VCodeBlock } from '@wdns/vue-code-block'
|
||||
import type { CodeObject } from '../../types/code'
|
||||
|
||||
const props = defineProps<{
|
||||
codeObject: CodeObject
|
||||
}>()
|
||||
|
||||
const skipKeys = [
|
||||
'cli'
|
||||
]
|
||||
|
||||
const codeEntries = computed(() => {
|
||||
return Object.entries(props.codeObject).filter(([name]) => !skipKeys.includes(name))
|
||||
})
|
||||
|
||||
const shouldShowTitle = () => {
|
||||
return true // Show titles for all sections
|
||||
}
|
||||
|
||||
const getDisplayName = (name: string) => {
|
||||
if (name === 'code') return 'Code'
|
||||
if (name === 'cli') return 'CLI Command'
|
||||
if (name === 'utility') return 'Utility'
|
||||
if (name === 'usage') return 'Usage'
|
||||
if (name === 'installation') return 'Installation'
|
||||
return name.charAt(0).toUpperCase() + name.slice(1)
|
||||
}
|
||||
|
||||
const getLanguage = (name: string) => {
|
||||
if (name === 'cli') return 'bash'
|
||||
if (name === 'code') return 'html'
|
||||
if (name === 'usage') return 'html'
|
||||
if (name === 'installation') return 'bash'
|
||||
return 'javascript'
|
||||
}
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.code-example {
|
||||
margin: 1.2rem 0;
|
||||
}
|
||||
|
||||
.code-section {
|
||||
margin-bottom: 2rem;
|
||||
}
|
||||
|
||||
.code-block {
|
||||
overflow: hidden;
|
||||
border: 1px solid #142216;
|
||||
border-radius: 15px;
|
||||
}
|
||||
|
||||
.no-code {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 0.5rem;
|
||||
color: #a1a1aa;
|
||||
font-style: italic;
|
||||
padding: 2rem;
|
||||
background: #0b0b0b;
|
||||
border: 1px solid #142216;
|
||||
border-radius: 15px;
|
||||
}
|
||||
|
||||
:deep(.v-code-block) {
|
||||
background: #0b0b0b;
|
||||
border-radius: 10px;
|
||||
font-size: 14px;
|
||||
}
|
||||
|
||||
:deep(.v-code-block pre) {
|
||||
background: #0b0b0b;
|
||||
margin: 0;
|
||||
font-size: 14px;
|
||||
font-weight: 400;
|
||||
}
|
||||
|
||||
:deep(.v-code-block--tab-highlightjs-github-dark-icon) {
|
||||
color: #999 !important;
|
||||
fill: #999 !important;
|
||||
}
|
||||
|
||||
:deep(.v-code-block--me-1) {
|
||||
margin-right: 0 !important;
|
||||
}
|
||||
|
||||
:deep(.v-code-block .hljs) {
|
||||
background: #0b0b0b;
|
||||
color: #fff;
|
||||
font-size: 14px;
|
||||
}
|
||||
</style>
|
||||
18
src/components/code/Dependencies.vue
Normal file
18
src/components/code/Dependencies.vue
Normal file
@@ -0,0 +1,18 @@
|
||||
<template>
|
||||
<div class="dependencies-container">
|
||||
<h2 class="demo-title">Dependencies</h2>
|
||||
<div class="demo-details">
|
||||
<span v-for="dep in dependencyList" :key="dep" class="dependency-tag">
|
||||
{{ dep }}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
interface Props {
|
||||
dependencyList: string[]
|
||||
}
|
||||
|
||||
defineProps<Props>()
|
||||
</script>
|
||||
47
src/components/common/ContributionSection.vue
Normal file
47
src/components/common/ContributionSection.vue
Normal file
@@ -0,0 +1,47 @@
|
||||
<template>
|
||||
<div class="contribute-container">
|
||||
<h2 class="demo-title-contribute">Help improve this component!</h2>
|
||||
<div class="contribute-buttons">
|
||||
<a
|
||||
:href="bugReportUrl"
|
||||
target="_blank"
|
||||
rel="noreferrer"
|
||||
class="contribute-button"
|
||||
>
|
||||
<i class="pi pi-exclamation-triangle"></i>
|
||||
<span>Report an issue</span>
|
||||
</a>
|
||||
<span class="contribute-separator">or</span>
|
||||
<a
|
||||
:href="featureRequestUrl"
|
||||
target="_blank"
|
||||
rel="noreferrer"
|
||||
class="contribute-button"
|
||||
>
|
||||
<i class="pi pi-lightbulb"></i>
|
||||
<span>Request a feature</span>
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { computed } from 'vue'
|
||||
import { useRoute } from 'vue-router'
|
||||
|
||||
const route = useRoute()
|
||||
|
||||
const bugReportUrl = computed(() => {
|
||||
const category = route.params.category
|
||||
const subcategory = route.params.subcategory
|
||||
const title = encodeURIComponent(`[BUG]: ${category}/${subcategory}`)
|
||||
return `https://github.com/DavidHDev/vue-bits/issues/new?template=1-bug-report.yml&title=${title}&labels=bug`
|
||||
})
|
||||
|
||||
const featureRequestUrl = computed(() => {
|
||||
const category = route.params.category
|
||||
const subcategory = route.params.subcategory
|
||||
const title = encodeURIComponent(`[FEAT]: ${category}/${subcategory}`)
|
||||
return `https://github.com/DavidHDev/vue-bits/issues/new?template=2-feature-request.yml&title=${title}&labels=enhancement`
|
||||
})
|
||||
</script>
|
||||
6
src/components/common/Customize.vue
Normal file
6
src/components/common/Customize.vue
Normal file
@@ -0,0 +1,6 @@
|
||||
<template>
|
||||
<div>
|
||||
<h2 class="demo-title">Customize</h2>
|
||||
<slot />
|
||||
</div>
|
||||
</template>
|
||||
118
src/components/common/PreviewSlider.vue
Normal file
118
src/components/common/PreviewSlider.vue
Normal file
@@ -0,0 +1,118 @@
|
||||
<template>
|
||||
<div class="preview-slider">
|
||||
<span class="slider-label">{{ title }}</span>
|
||||
<Slider
|
||||
:model-value="modelValue"
|
||||
@update:model-value="handleSliderChange"
|
||||
:min="min"
|
||||
:max="max"
|
||||
:step="step"
|
||||
:disabled="disabled"
|
||||
class="custom-slider"
|
||||
/>
|
||||
<span class="slider-value">{{ modelValue }}{{ valueUnit }}</span>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import Slider from 'primevue/slider'
|
||||
|
||||
defineProps<{
|
||||
title: string
|
||||
modelValue: number
|
||||
min?: number
|
||||
max?: number
|
||||
step?: number
|
||||
valueUnit?: string
|
||||
disabled?: boolean
|
||||
}>()
|
||||
|
||||
const emit = defineEmits<{
|
||||
'update:modelValue': [value: number]
|
||||
}>()
|
||||
|
||||
const handleSliderChange = (value: number | number[]) => {
|
||||
const numValue = Array.isArray(value) ? value[0] : value
|
||||
emit('update:modelValue', numValue)
|
||||
}
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.preview-slider {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 1rem;
|
||||
margin: 1.5rem 0;
|
||||
}
|
||||
|
||||
.slider-label {
|
||||
font-size: 14px;
|
||||
color: #fff;
|
||||
}
|
||||
|
||||
.custom-slider {
|
||||
width: 150px;
|
||||
}
|
||||
|
||||
.slider-value {
|
||||
font-size: 14px;
|
||||
color: #fff;
|
||||
min-width: 3rem;
|
||||
}
|
||||
|
||||
:deep(.p-slider) {
|
||||
background: #222;
|
||||
border-radius: 10px;
|
||||
height: 8px !important;
|
||||
border: 1px solid #333 !important;
|
||||
outline: none;
|
||||
box-shadow: none;
|
||||
}
|
||||
|
||||
:deep(.p-slider-range) {
|
||||
background: #fff;
|
||||
border-radius: 10px;
|
||||
border: none;
|
||||
}
|
||||
|
||||
:deep(.p-slider-handle) {
|
||||
background: #0b0b0b !important;
|
||||
border: 2px solid #fff !important;
|
||||
box-shadow: none !important;
|
||||
width: 1.25rem;
|
||||
height: 1.25rem;
|
||||
border-radius: 50%;
|
||||
transition: all 0.1s ease;
|
||||
outline: none !important;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
:deep(.p-slider-handle:hover) {
|
||||
transform: scale(1.1);
|
||||
border: 2px solid #fff !important;
|
||||
box-shadow: 0 0 8px rgba(82, 39, 255, 0.3) !important;
|
||||
}
|
||||
|
||||
:deep(.p-slider-handle:focus) {
|
||||
outline: none !important;
|
||||
border: 2px solid #fff !important;
|
||||
box-shadow: 0 0 8px rgba(82, 39, 255, 0.5) !important;
|
||||
}
|
||||
|
||||
:deep(.p-slider-handle:active) {
|
||||
transform: scale(1.05);
|
||||
transition: all 0.05s ease;
|
||||
border: 2px solid #fff !important;
|
||||
box-shadow: none !important;
|
||||
}
|
||||
|
||||
:deep(.p-slider-handle::before),
|
||||
:deep(.p-slider-handle::after) {
|
||||
display: none !important;
|
||||
}
|
||||
|
||||
:deep(.p-slider-handle > *) {
|
||||
border: none !important;
|
||||
outline: none !important;
|
||||
}
|
||||
</style>
|
||||
115
src/components/common/PreviewSwitch.vue
Normal file
115
src/components/common/PreviewSwitch.vue
Normal file
@@ -0,0 +1,115 @@
|
||||
<template>
|
||||
<div class="preview-switch">
|
||||
<span class="switch-label">{{ title }}</span>
|
||||
<ToggleSwitch
|
||||
:model-value="modelValue"
|
||||
@update:model-value="$emit('update:modelValue', $event)"
|
||||
:disabled="disabled"
|
||||
/>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import ToggleSwitch from 'primevue/toggleswitch'
|
||||
|
||||
defineProps<{
|
||||
title: string
|
||||
modelValue: boolean
|
||||
disabled?: boolean
|
||||
}>()
|
||||
|
||||
defineEmits<{
|
||||
'update:modelValue': [value: boolean]
|
||||
}>()
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.preview-switch {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 1rem;
|
||||
margin: 1.5rem 0;
|
||||
}
|
||||
|
||||
.switch-label {
|
||||
font-size: 14px;
|
||||
color: #fff;
|
||||
}
|
||||
|
||||
:deep(.p-toggleswitch) {
|
||||
width: 2.5rem;
|
||||
height: 1.25rem;
|
||||
border: 1px solid #333;
|
||||
border-radius: 10px;
|
||||
outline: none;
|
||||
box-shadow: none;
|
||||
}
|
||||
|
||||
:deep(.p-toggleswitch-slider) {
|
||||
background: #222;
|
||||
border-radius: 10px;
|
||||
transition: all 0.3s ease;
|
||||
border: none;
|
||||
}
|
||||
|
||||
:deep(.p-toggleswitch.p-toggleswitch-checked .p-toggleswitch-slider) {
|
||||
background: #fff;
|
||||
}
|
||||
|
||||
:deep(.p-toggleswitch-handle) {
|
||||
background: #0b0b0b;
|
||||
border: 2px solid #fff;
|
||||
width: 1rem;
|
||||
height: 1rem;
|
||||
border-radius: 50%;
|
||||
transition: all 0.1s ease;
|
||||
outline: none;
|
||||
box-shadow: none;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
:deep(.p-toggleswitch:hover .p-toggleswitch-slider) {
|
||||
background: #222 !important;
|
||||
}
|
||||
|
||||
:deep(.p-toggleswitch.p-toggleswitch-checked:hover .p-toggleswitch-slider) {
|
||||
background: #fff !important;
|
||||
}
|
||||
|
||||
:deep(.p-toggleswitch-handle:hover) {
|
||||
background: #0b0b0b !important;
|
||||
border: 2px solid #fff !important;
|
||||
box-shadow: none !important;
|
||||
transform: none !important;
|
||||
}
|
||||
|
||||
:deep(.p-toggleswitch-handle:focus) {
|
||||
outline: none !important;
|
||||
background: #0b0b0b !important;
|
||||
border: 2px solid #fff !important;
|
||||
box-shadow: none !important;
|
||||
transform: none !important;
|
||||
}
|
||||
|
||||
:deep(.p-toggleswitch-handle:active) {
|
||||
background: #0b0b0b !important;
|
||||
border: 2px solid #fff !important;
|
||||
box-shadow: none !important;
|
||||
transform: none !important;
|
||||
}
|
||||
|
||||
:deep(.p-toggleswitch:focus) {
|
||||
outline: none !important;
|
||||
box-shadow: none !important;
|
||||
}
|
||||
|
||||
:deep(.p-toggleswitch-handle::before),
|
||||
:deep(.p-toggleswitch-handle::after) {
|
||||
display: none;
|
||||
}
|
||||
|
||||
:deep(.p-toggleswitch-handle > *) {
|
||||
border: none;
|
||||
outline: none;
|
||||
}
|
||||
</style>
|
||||
131
src/components/common/PropTable.vue
Normal file
131
src/components/common/PropTable.vue
Normal file
@@ -0,0 +1,131 @@
|
||||
<template>
|
||||
<div class="prop-table-container">
|
||||
<h2 class="demo-title">Props</h2>
|
||||
<div class="table-wrapper">
|
||||
<DataTable :value="data" class="props-table">
|
||||
<Column field="name" header="Property">
|
||||
<template #body="{ data }">
|
||||
<div class="code-cell">{{ data.name }}</div>
|
||||
</template>
|
||||
</Column>
|
||||
<Column field="type" header="Type">
|
||||
<template #body="{ data }">
|
||||
<span class="type-text">{{ data.type }}</span>
|
||||
</template>
|
||||
</Column>
|
||||
<Column field="default" header="Default">
|
||||
<template #body="{ data }">
|
||||
<div class="code-cell">{{ data.default || '—' }}</div>
|
||||
</template>
|
||||
</Column>
|
||||
<Column field="description" header="Description">
|
||||
<template #body="{ data }">
|
||||
<div class="description-text">{{ data.description }}</div>
|
||||
</template>
|
||||
</Column>
|
||||
</DataTable>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import DataTable from 'primevue/datatable'
|
||||
import Column from 'primevue/column'
|
||||
|
||||
interface PropData {
|
||||
name: string
|
||||
type: string
|
||||
default: string
|
||||
description: string
|
||||
}
|
||||
|
||||
defineProps<{
|
||||
data: PropData[]
|
||||
}>()
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.prop-table-container {
|
||||
margin-top: 3rem;
|
||||
}
|
||||
|
||||
.table-wrapper {
|
||||
overflow-x: auto;
|
||||
margin-top: 1.5rem;
|
||||
}
|
||||
|
||||
.code-cell {
|
||||
font-family: monospace;
|
||||
padding: 0.25rem 0.5rem;
|
||||
border-radius: 5px;
|
||||
width: fit-content;
|
||||
font-weight: 500;
|
||||
color: #e9e9e9;
|
||||
background-color: #222;
|
||||
font-size: 14px;
|
||||
}
|
||||
|
||||
.type-text {
|
||||
font-family: monospace;
|
||||
font-weight: 500;
|
||||
font-size: 14px;
|
||||
color: #fff;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.description-text {
|
||||
max-width: 300px;
|
||||
color: #fff;
|
||||
font-weight: 400;
|
||||
line-height: 1.5;
|
||||
font-size: 14px;
|
||||
}
|
||||
|
||||
:deep(.p-datatable) {
|
||||
background: transparent;
|
||||
border: none;
|
||||
}
|
||||
|
||||
:deep(.p-datatable-header) {
|
||||
background: #111;
|
||||
border: 1px solid #142216;
|
||||
border-bottom: none;
|
||||
}
|
||||
|
||||
:deep(.p-datatable-thead > tr > th) {
|
||||
background: #111;
|
||||
border-right: 1px solid #142216;
|
||||
border-bottom: 1px solid #142216;
|
||||
color: #fff;
|
||||
font-weight: 600;
|
||||
text-transform: capitalize;
|
||||
letter-spacing: -0.5px;
|
||||
padding: 1rem;
|
||||
font-size: 14px;
|
||||
}
|
||||
|
||||
:deep(.p-datatable-thead > tr > th:last-child) {
|
||||
border-right: none;
|
||||
}
|
||||
|
||||
:deep(.p-datatable-tbody > tr > td) {
|
||||
background: #0b0b0b;
|
||||
border-right: 1px solid #142216;
|
||||
border-bottom: 1px solid #142216;
|
||||
color: #fff;
|
||||
padding: 1rem;
|
||||
font-size: 14px;
|
||||
}
|
||||
|
||||
:deep(.p-datatable-tbody > tr > td:last-child) {
|
||||
border-right: none;
|
||||
}
|
||||
|
||||
:deep(.p-datatable-tbody > tr:last-child > td) {
|
||||
border-bottom: none;
|
||||
}
|
||||
|
||||
:deep(.p-datatable-tbody > tr:hover) {
|
||||
background: #0b0b0b;
|
||||
}
|
||||
</style>
|
||||
39
src/components/common/RefreshButton.vue
Normal file
39
src/components/common/RefreshButton.vue
Normal file
@@ -0,0 +1,39 @@
|
||||
<template>
|
||||
<Button class="refresh-button" @click="$emit('refresh')" outlined rounded size="small">
|
||||
<i class="pi pi-refresh"></i>
|
||||
</Button>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import Button from 'primevue/button'
|
||||
|
||||
defineEmits<{
|
||||
refresh: []
|
||||
}>()
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.refresh-button {
|
||||
position: absolute;
|
||||
top: 0.75rem;
|
||||
right: 0.75rem;
|
||||
z-index: 2;
|
||||
background-color: #111;
|
||||
border: 1px solid #142216;
|
||||
color: white;
|
||||
border-radius: 12px;
|
||||
padding: 0.5rem;
|
||||
transition: background-color 0.3s ease;
|
||||
}
|
||||
|
||||
.refresh-button:hover {
|
||||
background-color: #222 !important;
|
||||
color: #fff !important;
|
||||
border: 1px solid #142216 !important;
|
||||
outline: 1px solid transparent !important;
|
||||
}
|
||||
|
||||
.refresh-button:active {
|
||||
background-color: #142216;
|
||||
}
|
||||
</style>
|
||||
166
src/components/common/TabbedLayout.vue
Normal file
166
src/components/common/TabbedLayout.vue
Normal file
@@ -0,0 +1,166 @@
|
||||
<template>
|
||||
<div class="tabbed-layout">
|
||||
<Tabs value="0">
|
||||
<TabList>
|
||||
<Tab value="0">
|
||||
<div class="tab-header">
|
||||
<i class="pi pi-eye"></i>
|
||||
<span>Preview</span>
|
||||
</div>
|
||||
</Tab>
|
||||
<Tab value="1">
|
||||
<div class="tab-header">
|
||||
<i class="pi pi-code"></i>
|
||||
<span>Code</span>
|
||||
</div>
|
||||
</Tab>
|
||||
<Tab value="2">
|
||||
<div class="tab-header">
|
||||
<i class="pi pi-box"></i>
|
||||
<span>CLI</span>
|
||||
</div>
|
||||
</Tab>
|
||||
<Tab value="3">
|
||||
<div class="tab-header">
|
||||
<i class="pi pi-heart"></i>
|
||||
<span>Contribute</span>
|
||||
</div>
|
||||
</Tab>
|
||||
</TabList>
|
||||
|
||||
<TabPanels>
|
||||
<TabPanel value="0">
|
||||
<slot name="preview" />
|
||||
</TabPanel>
|
||||
<TabPanel value="1">
|
||||
<slot name="code" />
|
||||
</TabPanel>
|
||||
<TabPanel value="2">
|
||||
<slot name="cli" />
|
||||
</TabPanel>
|
||||
<TabPanel value="3">
|
||||
<ContributionSection />
|
||||
</TabPanel>
|
||||
</TabPanels>
|
||||
</Tabs>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import Tabs from 'primevue/tabs'
|
||||
import TabList from 'primevue/tablist'
|
||||
import Tab from 'primevue/tab'
|
||||
import TabPanels from 'primevue/tabpanels'
|
||||
import TabPanel from 'primevue/tabpanel'
|
||||
import ContributionSection from './ContributionSection.vue'
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.tabbed-layout {
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.tab-header {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 0.5rem;
|
||||
padding: 0.5rem 1rem;
|
||||
border: 1px solid #142216;
|
||||
border-radius: 10px;
|
||||
font-size: 14px;
|
||||
height: 36px;
|
||||
color: #ffffff;
|
||||
background: transparent;
|
||||
transition: all 0.3s ease;
|
||||
}
|
||||
|
||||
.tab-header:hover {
|
||||
background: #142216;
|
||||
}
|
||||
|
||||
:deep(.p-tablist),
|
||||
:deep(.p-tablist-tab-list) {
|
||||
display: flex;
|
||||
gap: 0.5rem;
|
||||
align-items: flex-start;
|
||||
flex-wrap: wrap;
|
||||
border: none !important;
|
||||
background: transparent !important;
|
||||
}
|
||||
|
||||
:deep(.p-tablist) {
|
||||
justify-content: flex-start;
|
||||
}
|
||||
|
||||
:deep(.p-tablist .p-tab:nth-child(1)),
|
||||
:deep(.p-tablist .p-tab:nth-child(2)),
|
||||
:deep(.p-tablist .p-tab:nth-child(3)) {
|
||||
margin-right: 0;
|
||||
}
|
||||
|
||||
:deep(.p-tablist .p-tab:nth-child(4)) {
|
||||
margin-left: auto;
|
||||
}
|
||||
|
||||
:deep(.p-tab) {
|
||||
border: none !important;
|
||||
background: transparent !important;
|
||||
padding: 0;
|
||||
}
|
||||
|
||||
:deep(.p-tab:not(.p-disabled):focus) {
|
||||
box-shadow: none !important;
|
||||
}
|
||||
|
||||
:deep(.p-tab:hover) {
|
||||
background: transparent !important;
|
||||
border-bottom: none !important;
|
||||
}
|
||||
|
||||
:deep(.p-tablist-active-bar),
|
||||
:deep(.p-tab-indicator),
|
||||
:deep(.p-tab::before),
|
||||
:deep(.p-tab::after),
|
||||
:deep(.p-tab[aria-selected="true"]::before),
|
||||
:deep(.p-tab[aria-selected="true"]::after),
|
||||
:deep(.p-tablist::after),
|
||||
:deep(.p-tablist-tab-list::before),
|
||||
:deep(.p-tablist-tab-list::after),
|
||||
:deep(.p-tab .p-tab-header-action::before),
|
||||
:deep(.p-tab .p-tab-header-action::after) {
|
||||
display: none !important;
|
||||
}
|
||||
|
||||
:deep(.p-tab[aria-selected="true"]) {
|
||||
background: transparent !important;
|
||||
border-bottom: none !important;
|
||||
}
|
||||
|
||||
:deep(.p-tab[aria-selected="true"] .tab-header) {
|
||||
background: #333333;
|
||||
color: #A7EF9E;
|
||||
}
|
||||
|
||||
:deep(.p-tabpanels) {
|
||||
padding: 0;
|
||||
background: transparent;
|
||||
}
|
||||
|
||||
:deep(.p-tabpanel) {
|
||||
background: transparent;
|
||||
border: none;
|
||||
padding: 0;
|
||||
}
|
||||
|
||||
@media (max-width: 768px) {
|
||||
:deep(.p-tablist) {
|
||||
flex-direction: row;
|
||||
align-items: stretch;
|
||||
justify-content: flex-start;
|
||||
}
|
||||
|
||||
:deep(.p-tablist .p-tab:nth-child(4)) {
|
||||
margin-left: 0;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
@@ -11,7 +11,7 @@
|
||||
padding: 0 4em;
|
||||
height: 160px;
|
||||
margin: 0 auto;
|
||||
background: linear-gradient(to bottom, #0e0e0e, transparent);
|
||||
background: linear-gradient(to bottom, #0b0b0b, transparent);
|
||||
}
|
||||
|
||||
.header-container {
|
||||
@@ -125,7 +125,7 @@
|
||||
}
|
||||
|
||||
.cta-button span {
|
||||
background-color: #0e0e0e;
|
||||
background-color: #0b0b0b;
|
||||
margin-left: 1em;
|
||||
margin-right: calc(1em - 8px);
|
||||
padding-top: .1em;
|
||||
|
||||
@@ -175,7 +175,7 @@
|
||||
|
||||
.feature-card {
|
||||
user-select: none;
|
||||
background: #0e0e0e;
|
||||
background: #0b0b0b;
|
||||
border: 1px solid rgba(148, 184, 154, 0.2);
|
||||
border-radius: 16px;
|
||||
padding: 2rem;
|
||||
@@ -357,7 +357,7 @@
|
||||
left: 0;
|
||||
right: 0;
|
||||
height: 20%;
|
||||
background: linear-gradient(to top, #0e0e0e 0%, transparent 100%);
|
||||
background: linear-gradient(to top, #0b0b0b 0%, transparent 100%);
|
||||
border-radius: 0 0 8px 8px;
|
||||
pointer-events: none;
|
||||
z-index: 2;
|
||||
@@ -370,7 +370,7 @@
|
||||
left: 0;
|
||||
right: 0;
|
||||
height: 20%;
|
||||
background: linear-gradient(to bottom, #0e0e0e 0%, transparent 100%);
|
||||
background: linear-gradient(to bottom, #0b0b0b 0%, transparent 100%);
|
||||
border-radius: 8px 8px 0 0;
|
||||
pointer-events: none;
|
||||
z-index: 2;
|
||||
@@ -410,7 +410,7 @@
|
||||
left: 0;
|
||||
right: 0;
|
||||
height: 20%;
|
||||
background: linear-gradient(to top, #0e0e0e 0%, transparent 100%);
|
||||
background: linear-gradient(to top, #0b0b0b 0%, transparent 100%);
|
||||
border-radius: 0 0 8px 8px;
|
||||
pointer-events: none;
|
||||
z-index: 2;
|
||||
@@ -423,7 +423,7 @@
|
||||
left: 0;
|
||||
right: 0;
|
||||
height: 20%;
|
||||
background: linear-gradient(to bottom, #0e0e0e 0%, transparent 100%);
|
||||
background: linear-gradient(to bottom, #0b0b0b 0%, transparent 100%);
|
||||
border-radius: 8px 8px 0 0;
|
||||
pointer-events: none;
|
||||
z-index: 2;
|
||||
@@ -462,7 +462,7 @@
|
||||
left: 0;
|
||||
right: 0;
|
||||
height: 20%;
|
||||
background: linear-gradient(to top, #0e0e0e 0%, transparent 100%);
|
||||
background: linear-gradient(to top, #0b0b0b 0%, transparent 100%);
|
||||
border-radius: 0 0 8px 8px;
|
||||
pointer-events: none;
|
||||
z-index: 2;
|
||||
@@ -475,7 +475,7 @@
|
||||
left: 0;
|
||||
right: 0;
|
||||
height: 20%;
|
||||
background: linear-gradient(to bottom, #0e0e0e 0%, transparent 100%);
|
||||
background: linear-gradient(to bottom, #0b0b0b 0%, transparent 100%);
|
||||
border-radius: 8px 8px 0 0;
|
||||
pointer-events: none;
|
||||
z-index: 2;
|
||||
|
||||
@@ -3,7 +3,7 @@
|
||||
margin-top: 8rem;
|
||||
padding: 2.4rem;
|
||||
border-top: 1px solid rgba(149, 184, 148, 0.1);
|
||||
background: linear-gradient(to bottom, transparent, #0e0e0e);
|
||||
background: linear-gradient(to bottom, transparent, #0b0b0b);
|
||||
z-index: 220;
|
||||
}
|
||||
|
||||
|
||||
@@ -16,7 +16,7 @@
|
||||
<span>Browse Components</span>
|
||||
<div class="button-arrow-circle">
|
||||
<svg width="16" height="16" viewBox="0 0 16 16" fill="#ffffff" xmlns="http://www.w3.org/2000/svg">
|
||||
<path d="M6 12L10 8L6 4" stroke="#4c1d95" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" />
|
||||
<path d="M6 12L10 8L6 4" stroke="#0b0b0b" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" />
|
||||
</svg>
|
||||
</div>
|
||||
</router-link>
|
||||
|
||||
@@ -57,7 +57,7 @@
|
||||
}
|
||||
|
||||
.start-building-title {
|
||||
color: #0e0e0e;
|
||||
color: #0b0b0b;
|
||||
font-size: 2.6rem;
|
||||
font-weight: 600;
|
||||
margin: 0;
|
||||
@@ -65,7 +65,7 @@
|
||||
}
|
||||
|
||||
.start-building-subtitle {
|
||||
color: #0e0e0e;
|
||||
color: #0b0b0b;
|
||||
font-size: 1.2rem;
|
||||
font-weight: 500;
|
||||
margin: -1rem 0 0 0;
|
||||
@@ -76,8 +76,8 @@
|
||||
|
||||
.start-building-button {
|
||||
background: transparent;
|
||||
color: #0e0e0e;
|
||||
border: 2px solid #0e0e0e;
|
||||
color: #0b0b0b;
|
||||
border: 2px solid #0b0b0b;
|
||||
padding: .6rem 1.6rem;
|
||||
font-size: 1.1rem;
|
||||
font-weight: 600;
|
||||
@@ -87,7 +87,7 @@
|
||||
}
|
||||
|
||||
.start-building-button:hover {
|
||||
background: #0e0e0e;
|
||||
background: #0b0b0b;
|
||||
color: #27FF64;
|
||||
}
|
||||
|
||||
|
||||
@@ -1,34 +1,35 @@
|
||||
<template>
|
||||
<main class="app-container">
|
||||
<header>
|
||||
<h2>Header Component</h2>
|
||||
</header>
|
||||
<Header />
|
||||
<section class="category-wrapper">
|
||||
<aside>
|
||||
<h3>Sidebar Component</h3>
|
||||
</aside>
|
||||
<router-view />
|
||||
<Sidebar />
|
||||
<div class="category-page">
|
||||
<router-view />
|
||||
</div>
|
||||
</section>
|
||||
<Toast position="bottom-right"
|
||||
:closeButtonProps="{ style: { justifyContent: 'flex-end', right: '0', margin: '0', outline: 'none', border: 'none' } }"
|
||||
:pt="{
|
||||
message: {
|
||||
style: {
|
||||
borderRadius: '10px'
|
||||
}
|
||||
},
|
||||
messageContent: {
|
||||
style: {
|
||||
alignItems: 'center'
|
||||
}
|
||||
},
|
||||
messageIcon: {
|
||||
style: {
|
||||
display: 'none'
|
||||
}
|
||||
}
|
||||
}" />
|
||||
</main>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
// Layout component for category pages
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.app-container {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
.category-wrapper {
|
||||
display: flex;
|
||||
}
|
||||
|
||||
aside {
|
||||
width: 250px;
|
||||
background-color: #f5f5f5;
|
||||
padding: 1rem;
|
||||
}
|
||||
</style>
|
||||
import Header from '../navs/Header.vue';
|
||||
import Sidebar from '../navs/Sidebar.vue';
|
||||
</script>
|
||||
231
src/components/navs/Header.vue
Normal file
231
src/components/navs/Header.vue
Normal file
@@ -0,0 +1,231 @@
|
||||
<template>
|
||||
<div class="main-nav">
|
||||
<div class="nav-items">
|
||||
<router-link to="/" class="logo">
|
||||
<img :src="Logo" alt="Logo" />
|
||||
</router-link>
|
||||
|
||||
<button class="mobile-menu-button" aria-label="Open Menu" @click="toggleDrawer">
|
||||
<i class="pi pi-bars"></i>
|
||||
</button>
|
||||
|
||||
<div class="desktop-nav">
|
||||
<!-- TODO: Search Component -->
|
||||
|
||||
<FadeContent blur>
|
||||
<button class="cta-button-docs" @click="openGitHub">
|
||||
Star On GitHub
|
||||
<span class="star-count">
|
||||
<img :src="Star" alt="Star Icon" />
|
||||
{{ stars }}
|
||||
</span>
|
||||
</button>
|
||||
</FadeContent>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div v-if="isDrawerOpen" class="drawer-overlay" @click="closeDrawer">
|
||||
<div class="drawer-content" @click.stop>
|
||||
<div class="drawer-header">
|
||||
<img :src="Logo" alt="Logo" class="drawer-logo" />
|
||||
<button class="close-button" aria-label="Close Menu" @click="closeDrawer">
|
||||
<i class="pi pi-times"></i>
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<div class="drawer-body">
|
||||
<!-- Navigation Categories -->
|
||||
<div class="drawer-navigation">
|
||||
<div class="categories-container">
|
||||
<Category
|
||||
v-for="cat in CATEGORIES"
|
||||
:key="cat.name"
|
||||
:category="cat"
|
||||
:location="route"
|
||||
:handle-click="onNavClick"
|
||||
:handle-transition-navigation="handleMobileTransitionNavigation"
|
||||
:on-item-mouse-enter="() => {}"
|
||||
:on-item-mouse-leave="() => {}"
|
||||
:is-transitioning="isTransitioning"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="drawer-separator"></div>
|
||||
|
||||
<div class="drawer-section">
|
||||
<p class="section-title">Useful Links</p>
|
||||
<router-link to="/text-animations/split-text" @click="closeDrawer" class="drawer-link">
|
||||
Docs
|
||||
</router-link>
|
||||
<a href="https://github.com/DavidHDev/vue-bits" target="_blank" @click="closeDrawer" class="drawer-link">
|
||||
GitHub
|
||||
<i class="pi pi-arrow-up-right arrow-icon"></i>
|
||||
</a>
|
||||
</div>
|
||||
|
||||
<div class="drawer-separator"></div>
|
||||
|
||||
<div class="drawer-section">
|
||||
<p class="section-title">Other</p>
|
||||
<a href="https://davidhaz.com/" target="_blank" @click="closeDrawer" class="drawer-link">
|
||||
Who made this?
|
||||
<i class="pi pi-arrow-up-right arrow-icon"></i>
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { ref, onMounted, onUnmounted, computed, defineComponent, h } from 'vue'
|
||||
import { useRoute, useRouter } from 'vue-router'
|
||||
import { useStars } from '../../composables/useStars'
|
||||
import { CATEGORIES, NEW, UPDATED } from '../../constants/Categories'
|
||||
import FadeContent from '../../content/Animations/FadeContent/FadeContent.vue'
|
||||
import Logo from '../../assets/logos/vue-bits-logo.svg'
|
||||
import Star from '../../assets/common/star.svg'
|
||||
|
||||
const isDrawerOpen = ref(false)
|
||||
const isTransitioning = ref(false)
|
||||
const stars = useStars()
|
||||
const route = useRoute()
|
||||
const router = useRouter()
|
||||
|
||||
// Helper function
|
||||
const slug = (str: string) => str.replace(/\s+/g, "-").toLowerCase()
|
||||
|
||||
const toggleDrawer = () => {
|
||||
isDrawerOpen.value = !isDrawerOpen.value
|
||||
}
|
||||
|
||||
const closeDrawer = () => {
|
||||
isDrawerOpen.value = false
|
||||
}
|
||||
|
||||
const openGitHub = () => {
|
||||
window.open('https://github.com/DavidHDev/vue-bits', '_blank')
|
||||
}
|
||||
|
||||
const onNavClick = () => {
|
||||
closeDrawer()
|
||||
window.scrollTo(0, 0)
|
||||
}
|
||||
|
||||
const handleMobileTransitionNavigation = async (path: string) => {
|
||||
if (isTransitioning.value || route.path === path) return
|
||||
|
||||
closeDrawer()
|
||||
isTransitioning.value = true
|
||||
|
||||
try {
|
||||
await router.push(path)
|
||||
window.scrollTo(0, 0)
|
||||
} finally {
|
||||
isTransitioning.value = false
|
||||
}
|
||||
}
|
||||
|
||||
const handleKeyDown = (e: KeyboardEvent) => {
|
||||
if (e.key === 'Escape' && isDrawerOpen.value) {
|
||||
closeDrawer()
|
||||
}
|
||||
}
|
||||
|
||||
// Category Component
|
||||
const Category = defineComponent({
|
||||
name: 'Category',
|
||||
props: {
|
||||
category: {
|
||||
type: Object,
|
||||
required: true
|
||||
},
|
||||
location: {
|
||||
type: Object,
|
||||
required: true
|
||||
},
|
||||
handleClick: {
|
||||
type: Function,
|
||||
required: true
|
||||
},
|
||||
handleTransitionNavigation: {
|
||||
type: Function,
|
||||
required: true
|
||||
},
|
||||
onItemMouseEnter: {
|
||||
type: Function,
|
||||
required: true
|
||||
},
|
||||
onItemMouseLeave: {
|
||||
type: Function,
|
||||
required: true
|
||||
},
|
||||
isTransitioning: {
|
||||
type: Boolean,
|
||||
default: false
|
||||
}
|
||||
},
|
||||
setup(props) {
|
||||
interface ItemType {
|
||||
sub: string
|
||||
path: string
|
||||
isActive: boolean
|
||||
isNew: boolean
|
||||
isUpdated: boolean
|
||||
}
|
||||
|
||||
const items = computed(() =>
|
||||
props.category.subcategories.map((sub: string): ItemType => {
|
||||
const path = `/${slug(props.category.name)}/${slug(sub)}`
|
||||
const activePath = props.location.path
|
||||
return {
|
||||
sub,
|
||||
path,
|
||||
isActive: activePath === path,
|
||||
isNew: (NEW as string[]).includes(sub),
|
||||
isUpdated: (UPDATED as string[]).includes(sub),
|
||||
}
|
||||
})
|
||||
)
|
||||
|
||||
return () => h('div', { class: 'category' }, [
|
||||
h('p', { class: 'category-name' }, props.category.name),
|
||||
h('div', { class: 'category-items' },
|
||||
items.value.map(({ sub, path, isActive, isNew, isUpdated }: ItemType) => {
|
||||
return h('router-link', {
|
||||
key: path,
|
||||
to: path,
|
||||
class: [
|
||||
'sidebar-item',
|
||||
{ 'active-sidebar-item': isActive },
|
||||
{ 'transitioning': props.isTransitioning }
|
||||
],
|
||||
onClick: (e: Event) => {
|
||||
e.preventDefault()
|
||||
props.handleTransitionNavigation(path)
|
||||
},
|
||||
onMouseenter: (e: Event) => props.onItemMouseEnter(path, e),
|
||||
onMouseleave: props.onItemMouseLeave
|
||||
}, {
|
||||
default: () => [
|
||||
sub,
|
||||
isNew ? h('span', { class: 'new-tag' }, 'New') : null,
|
||||
isUpdated ? h('span', { class: 'updated-tag' }, 'Updated') : null
|
||||
].filter(Boolean)
|
||||
})
|
||||
})
|
||||
)
|
||||
])
|
||||
}
|
||||
})
|
||||
|
||||
onMounted(() => {
|
||||
document.addEventListener('keydown', handleKeyDown)
|
||||
})
|
||||
|
||||
onUnmounted(() => {
|
||||
document.removeEventListener('keydown', handleKeyDown)
|
||||
})
|
||||
</script>
|
||||
334
src/components/navs/Sidebar.vue
Normal file
334
src/components/navs/Sidebar.vue
Normal file
@@ -0,0 +1,334 @@
|
||||
<template>
|
||||
<!-- Mobile Drawer -->
|
||||
<div v-if="isDrawerOpen" class="drawer-overlay" @click="closeDrawer">
|
||||
<div class="drawer-content" :class="{ 'drawer-open': isDrawerOpen }" @click.stop>
|
||||
<div class="drawer-header sidebar-logo">
|
||||
<div class="drawer-header-content">
|
||||
<router-link to="/" @click="closeDrawer">
|
||||
<img :src="Logo" alt="Logo" class="drawer-logo" />
|
||||
</router-link>
|
||||
<button class="icon-button" aria-label="Close" @click="closeDrawer">
|
||||
<i class="pi pi-times"></i>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="drawer-body">
|
||||
<div class="categories-container">
|
||||
<Category v-for="cat in CATEGORIES" :key="cat.name" :category="cat" :location="route"
|
||||
:pending-active-path="pendingActivePath ?? undefined" :handle-click="onNavClick"
|
||||
:handle-transition-navigation="handleMobileTransitionNavigation" :on-item-mouse-enter="() => { }"
|
||||
:on-item-mouse-leave="() => { }" :is-transitioning="isTransitioning" />
|
||||
</div>
|
||||
|
||||
<div class="separator"></div>
|
||||
<div class="useful-links">
|
||||
<p class="useful-links-title">Useful Links</p>
|
||||
<div class="links-container">
|
||||
<a href="https://github.com/DavidHDev/vue-bits" target="_blank" @click="closeDrawer" class="useful-link">
|
||||
<span>GitHub</span>
|
||||
<i class="pi pi-arrow-up-right arrow-icon"></i>
|
||||
</a>
|
||||
<router-link to="/text-animations/split-text" @click="closeDrawer" class="useful-link">
|
||||
<span>Docs</span>
|
||||
<i class="pi pi-arrow-up-right arrow-icon"></i>
|
||||
</router-link>
|
||||
<a href="https://davidhaz.com/" target="_blank" @click="closeDrawer" class="useful-link">
|
||||
<span>Who made this?</span>
|
||||
<i class="pi pi-arrow-up-right arrow-icon"></i>
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Desktop Sidebar -->
|
||||
<nav ref="sidebarContainerRef" class="sidebar" :class="{ 'sidebar-no-fade': isScrolledToBottom }"
|
||||
@scroll="handleScroll">
|
||||
<div ref="sidebarRef" class="sidebar-content">
|
||||
<!-- Active line indicator -->
|
||||
<div class="active-line" :style="{
|
||||
transform: isLineVisible && linePosition !== null
|
||||
? `translateY(${linePosition - 8}px)`
|
||||
: 'translateY(-100px)',
|
||||
opacity: isLineVisible ? 1 : 0
|
||||
}"></div>
|
||||
|
||||
<!-- Hover line indicator -->
|
||||
<div class="hover-line" :style="{
|
||||
transform: hoverLinePosition !== null
|
||||
? `translateY(${hoverLinePosition - 8}px)`
|
||||
: 'translateY(-100px)',
|
||||
opacity: isHoverLineVisible ? 1 : 0
|
||||
}"></div>
|
||||
|
||||
<div class="categories-list">
|
||||
<Category v-for="cat in CATEGORIES" :key="cat.name" :category="cat" :location="route"
|
||||
:pending-active-path="pendingActivePath ?? undefined" :handle-click="scrollToTop"
|
||||
:handle-transition-navigation="handleTransitionNavigation" :on-item-mouse-enter="onItemEnter"
|
||||
:on-item-mouse-leave="onItemLeave" :is-transitioning="isTransitioning" />
|
||||
</div>
|
||||
</div>
|
||||
</nav>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { ref, onMounted, onUnmounted, nextTick, watch, defineComponent, h, computed } from 'vue'
|
||||
import { useRoute, useRouter } from 'vue-router'
|
||||
import { CATEGORIES, NEW, UPDATED } from '../../constants/Categories'
|
||||
import Logo from '../../assets/logos/vue-bits-logo.svg'
|
||||
import '../../css/sidebar.css'
|
||||
|
||||
const HOVER_TIMEOUT_DELAY = 150
|
||||
|
||||
// Reactive data
|
||||
const isDrawerOpen = ref(false)
|
||||
const linePosition = ref<number | null>(null)
|
||||
const isLineVisible = ref(false)
|
||||
const hoverLinePosition = ref<number | null>(null)
|
||||
const isHoverLineVisible = ref(false)
|
||||
const pendingActivePath = ref<string | null>(null)
|
||||
const isScrolledToBottom = ref(false)
|
||||
const isTransitioning = ref(false)
|
||||
|
||||
const sidebarRef = ref<HTMLDivElement>()
|
||||
const sidebarContainerRef = ref<HTMLDivElement>()
|
||||
|
||||
// Timeouts
|
||||
let hoverTimeoutRef: number | null = null
|
||||
let hoverDelayTimeoutRef: number | null = null
|
||||
|
||||
const route = useRoute()
|
||||
const router = useRouter()
|
||||
|
||||
// Helper functions
|
||||
const scrollToTop = () => window.scrollTo(0, 0)
|
||||
const slug = (str: string) => str.replace(/\s+/g, "-").toLowerCase()
|
||||
|
||||
const findActiveElement = () => {
|
||||
const activePath = pendingActivePath.value || route.path
|
||||
|
||||
for (const category of CATEGORIES) {
|
||||
const activeItem = category.subcategories.find((sub: string) => {
|
||||
const expectedPath = `/${slug(category.name)}/${slug(sub)}`
|
||||
return activePath === expectedPath
|
||||
})
|
||||
if (activeItem) {
|
||||
// Try to find the element within the sidebar
|
||||
const selector = `.sidebar a[href="${activePath}"]`
|
||||
const element = document.querySelector(selector) as HTMLElement
|
||||
return element
|
||||
}
|
||||
}
|
||||
return null
|
||||
}
|
||||
|
||||
const updateLinePosition = (el: HTMLElement | null) => {
|
||||
if (!el || !sidebarRef.value || !sidebarRef.value.offsetParent) return null
|
||||
const sidebarRect = sidebarRef.value.getBoundingClientRect()
|
||||
const elRect = el.getBoundingClientRect()
|
||||
return elRect.top - sidebarRect.top + elRect.height / 2
|
||||
}
|
||||
|
||||
const closeDrawer = () => {
|
||||
isDrawerOpen.value = false
|
||||
}
|
||||
|
||||
const onNavClick = () => {
|
||||
closeDrawer()
|
||||
scrollToTop()
|
||||
}
|
||||
|
||||
const handleTransitionNavigation = async (path: string) => {
|
||||
if (isTransitioning.value || route.path === path) return
|
||||
|
||||
pendingActivePath.value = path
|
||||
|
||||
// Simple navigation without transition for now
|
||||
// TODO: Implement transition when available
|
||||
await router.push(path)
|
||||
scrollToTop()
|
||||
pendingActivePath.value = null
|
||||
}
|
||||
|
||||
const handleMobileTransitionNavigation = async (path: string) => {
|
||||
if (isTransitioning.value || route.path === path) return
|
||||
|
||||
closeDrawer()
|
||||
pendingActivePath.value = path
|
||||
|
||||
// Simple navigation without transition for now
|
||||
// TODO: Implement transition when available
|
||||
await router.push(path)
|
||||
scrollToTop()
|
||||
pendingActivePath.value = null
|
||||
}
|
||||
|
||||
const onItemEnter = (path: string, e: Event) => {
|
||||
if (hoverTimeoutRef) clearTimeout(hoverTimeoutRef)
|
||||
if (hoverDelayTimeoutRef) clearTimeout(hoverDelayTimeoutRef)
|
||||
|
||||
const targetElement = e.currentTarget as HTMLElement
|
||||
const pos = updateLinePosition(targetElement)
|
||||
|
||||
if (pos !== null) {
|
||||
hoverLinePosition.value = pos
|
||||
}
|
||||
|
||||
hoverDelayTimeoutRef = setTimeout(() => {
|
||||
isHoverLineVisible.value = true
|
||||
}, 200)
|
||||
}
|
||||
|
||||
const onItemLeave = () => {
|
||||
if (hoverDelayTimeoutRef) clearTimeout(hoverDelayTimeoutRef)
|
||||
|
||||
hoverTimeoutRef = setTimeout(() => {
|
||||
isHoverLineVisible.value = false
|
||||
}, HOVER_TIMEOUT_DELAY)
|
||||
}
|
||||
|
||||
const handleScroll = () => {
|
||||
const sidebarElement = sidebarContainerRef.value
|
||||
if (!sidebarElement) return
|
||||
|
||||
const { scrollTop, scrollHeight, clientHeight } = sidebarElement
|
||||
const isAtBottom = scrollTop + clientHeight >= scrollHeight - 10
|
||||
isScrolledToBottom.value = isAtBottom
|
||||
}
|
||||
|
||||
const updateActiveLine = async () => {
|
||||
await nextTick()
|
||||
|
||||
// Wait a bit more to ensure DOM is fully updated
|
||||
setTimeout(() => {
|
||||
const activeEl = findActiveElement()
|
||||
|
||||
if (!activeEl) {
|
||||
isLineVisible.value = false
|
||||
return
|
||||
}
|
||||
|
||||
const pos = updateLinePosition(activeEl)
|
||||
if (pos !== null) {
|
||||
linePosition.value = pos
|
||||
isLineVisible.value = true
|
||||
} else {
|
||||
isLineVisible.value = false
|
||||
}
|
||||
}, 100)
|
||||
}
|
||||
|
||||
// Category Component
|
||||
const Category = defineComponent({
|
||||
name: 'Category',
|
||||
props: {
|
||||
category: {
|
||||
type: Object,
|
||||
required: true
|
||||
},
|
||||
location: {
|
||||
type: Object,
|
||||
required: true
|
||||
},
|
||||
pendingActivePath: {
|
||||
type: String,
|
||||
default: null
|
||||
},
|
||||
handleClick: {
|
||||
type: Function,
|
||||
required: true
|
||||
},
|
||||
handleTransitionNavigation: {
|
||||
type: Function,
|
||||
required: true
|
||||
},
|
||||
onItemMouseEnter: {
|
||||
type: Function,
|
||||
required: true
|
||||
},
|
||||
onItemMouseLeave: {
|
||||
type: Function,
|
||||
required: true
|
||||
},
|
||||
isTransitioning: {
|
||||
type: Boolean,
|
||||
default: false
|
||||
}
|
||||
},
|
||||
setup(props) {
|
||||
interface ItemType {
|
||||
sub: string
|
||||
path: string
|
||||
isActive: boolean
|
||||
isNew: boolean
|
||||
isUpdated: boolean
|
||||
}
|
||||
|
||||
const items = computed(() =>
|
||||
props.category.subcategories.map((sub: string): ItemType => {
|
||||
const path = `/${slug(props.category.name)}/${slug(sub)}`
|
||||
const activePath = props.pendingActivePath || props.location.path
|
||||
return {
|
||||
sub,
|
||||
path,
|
||||
isActive: activePath === path,
|
||||
isNew: (NEW as string[]).includes(sub),
|
||||
isUpdated: (UPDATED as string[]).includes(sub),
|
||||
}
|
||||
})
|
||||
)
|
||||
|
||||
return () => h('div', { class: 'category' }, [
|
||||
h('p', { class: 'category-name' }, props.category.name),
|
||||
h('div', { class: 'category-items' },
|
||||
items.value.map(({ sub, path, isActive, isNew, isUpdated }: ItemType) => {
|
||||
return h('router-link', {
|
||||
key: path,
|
||||
to: path,
|
||||
class: [
|
||||
'sidebar-item',
|
||||
{ 'active-sidebar-item': isActive },
|
||||
{ 'transitioning': props.isTransitioning }
|
||||
],
|
||||
onClick: (e: Event) => {
|
||||
e.preventDefault()
|
||||
props.handleTransitionNavigation(path)
|
||||
},
|
||||
onMouseenter: (e: Event) => props.onItemMouseEnter(path, e),
|
||||
onMouseleave: props.onItemMouseLeave
|
||||
}, {
|
||||
default: () => [
|
||||
sub,
|
||||
isNew ? h('span', { class: 'new-tag' }, 'New') : null,
|
||||
isUpdated ? h('span', { class: 'updated-tag' }, 'Updated') : null
|
||||
].filter(Boolean)
|
||||
})
|
||||
})
|
||||
)
|
||||
])
|
||||
}
|
||||
})
|
||||
|
||||
// Watchers
|
||||
watch(() => route.path, updateActiveLine)
|
||||
watch(pendingActivePath, updateActiveLine)
|
||||
|
||||
// Lifecycle
|
||||
onMounted(() => {
|
||||
updateActiveLine()
|
||||
if (sidebarContainerRef.value) {
|
||||
sidebarContainerRef.value.addEventListener('scroll', handleScroll)
|
||||
handleScroll()
|
||||
}
|
||||
})
|
||||
|
||||
onUnmounted(() => {
|
||||
if (hoverTimeoutRef) clearTimeout(hoverTimeoutRef)
|
||||
if (hoverDelayTimeoutRef) clearTimeout(hoverDelayTimeoutRef)
|
||||
if (sidebarContainerRef.value) {
|
||||
sidebarContainerRef.value.removeEventListener('scroll', handleScroll)
|
||||
}
|
||||
})
|
||||
</script>
|
||||
Reference in New Issue
Block a user