documentation structure

This commit is contained in:
David Haz
2025-07-08 23:34:52 +03:00
parent 9ddb731258
commit 660e4fd701
46 changed files with 3488 additions and 79 deletions

View 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 &amp; 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>

View 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>

View 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>

View 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>

View File

@@ -0,0 +1,6 @@
<template>
<div>
<h2 class="demo-title">Customize</h2>
<slot />
</div>
</template>

View 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>

View 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>

View 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>

View 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>

View 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>

View File

@@ -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;

View File

@@ -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;

View File

@@ -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;
}

View File

@@ -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>

View File

@@ -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;
}

View File

@@ -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>

View 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>

View 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>