diff --git a/website/resources/styles/styles.scss b/website/resources/styles/styles.scss index bbabae6..9423678 100644 --- a/website/resources/styles/styles.scss +++ b/website/resources/styles/styles.scss @@ -1,6 +1,9 @@ // Import Vuexy's full Vuetify component override chain @use "@core-scss/template/libs/vuetify"; +// Vertical sidebar navigation layout +@use "@layouts/styles/vertical-nav"; + // ━━━ Project-specific overrides ━━━ html { diff --git a/website/resources/ts/@layouts/components/VerticalNav.vue b/website/resources/ts/@layouts/components/VerticalNav.vue new file mode 100644 index 0000000..e0ebdaf --- /dev/null +++ b/website/resources/ts/@layouts/components/VerticalNav.vue @@ -0,0 +1,100 @@ + + + diff --git a/website/resources/ts/@layouts/components/VerticalNavGroup.vue b/website/resources/ts/@layouts/components/VerticalNavGroup.vue new file mode 100644 index 0000000..2fd4774 --- /dev/null +++ b/website/resources/ts/@layouts/components/VerticalNavGroup.vue @@ -0,0 +1,108 @@ + + + diff --git a/website/resources/ts/@layouts/components/VerticalNavLayout.vue b/website/resources/ts/@layouts/components/VerticalNavLayout.vue new file mode 100644 index 0000000..12621f4 --- /dev/null +++ b/website/resources/ts/@layouts/components/VerticalNavLayout.vue @@ -0,0 +1,107 @@ + + + diff --git a/website/resources/ts/@layouts/components/VerticalNavLink.vue b/website/resources/ts/@layouts/components/VerticalNavLink.vue new file mode 100644 index 0000000..a1fc003 --- /dev/null +++ b/website/resources/ts/@layouts/components/VerticalNavLink.vue @@ -0,0 +1,52 @@ + + + diff --git a/website/resources/ts/@layouts/components/VerticalNavSectionTitle.vue b/website/resources/ts/@layouts/components/VerticalNavSectionTitle.vue new file mode 100644 index 0000000..5a8feab --- /dev/null +++ b/website/resources/ts/@layouts/components/VerticalNavSectionTitle.vue @@ -0,0 +1,21 @@ + + + diff --git a/website/resources/ts/@layouts/styles/_vertical-nav.scss b/website/resources/ts/@layouts/styles/_vertical-nav.scss new file mode 100644 index 0000000..e8f7bc7 --- /dev/null +++ b/website/resources/ts/@layouts/styles/_vertical-nav.scss @@ -0,0 +1,311 @@ +@use "@configured-variables" as variables; +@use "variables" as layout-variables; + +// ━━━ Layout Wrapper ━━━ +.layout-wrapper.layout-nav-type-vertical { + display: flex; + min-block-size: 100vh; + flex: 1 1 auto; +} + +// ━━━ Vertical Nav (Sidebar) ━━━ +.layout-vertical-nav { + position: fixed; + inset-block: 0; + inset-inline-start: 0; + z-index: layout-variables.$layout-vertical-nav-z-index; + display: flex; + flex-direction: column; + inline-size: layout-variables.$layout-vertical-nav-width; + background-color: rgb(var(--v-theme-surface)); + border-inline-end: 1px solid rgba(var(--v-border-color), var(--v-border-opacity)); + transition: inline-size 0.25s ease-in-out, transform 0.25s ease-in-out, box-shadow 0.25s ease-in-out; + + // ━━━ Nav Header ━━━ + .nav-header { + display: flex; + align-items: center; + justify-content: space-between; + padding: 16px 16px 8px; + min-block-size: layout-variables.$layout-vertical-nav-navbar-height; + } + + .nav-header-logo { + display: flex; + align-items: center; + gap: 10px; + text-decoration: none; + overflow: hidden; + } + + .nav-logo-img { + block-size: 28px; + inline-size: auto; + flex-shrink: 0; + } + + .nav-logo-title { + font-size: 1.125rem; + font-weight: 700; + letter-spacing: 0.5px; + color: rgb(var(--v-theme-on-surface)); + white-space: nowrap; + transition: opacity 0.15s ease; + } + + .nav-collapse-btn { + flex-shrink: 0; + } + + // ━━━ Nav Items List ━━━ + .nav-items { + list-style: none; + padding: 0; + margin: 0; + overflow-y: auto; + overflow-x: hidden; + flex-grow: 1; + padding-block: 4px; + padding-inline: 12px; + } + + // ━━━ Nav Link ━━━ + .nav-link { + margin-block: 2px; + + .nav-link-btn { + display: flex; + align-items: center; + gap: 8px; + padding: 8px 12px; + border-radius: 6px; + text-decoration: none; + color: rgba(var(--v-theme-on-surface), 0.78); + transition: background-color 0.15s ease, color 0.15s ease; + white-space: nowrap; + overflow: hidden; + } + + .nav-item-icon { + flex-shrink: 0; + } + + .nav-item-title { + font-size: 0.9375rem; + line-height: 1.4; + flex-grow: 1; + overflow: hidden; + text-overflow: ellipsis; + } + + .nav-item-badge { + font-size: 0.75rem; + padding: 2px 6px; + border-radius: 4px; + background-color: rgb(var(--v-theme-error)); + color: rgb(var(--v-theme-on-error)); + line-height: 1; + flex-shrink: 0; + } + + &:not(.disabled) .nav-link-btn:hover { + background-color: rgba(var(--v-theme-on-surface), 0.06); + } + + &.active .nav-link-btn { + background: linear-gradient(270deg, rgb(var(--v-theme-primary)), rgba(var(--v-theme-primary), 0.7)); + color: rgb(var(--v-theme-on-primary)); + box-shadow: 0 2px 6px 0 rgba(var(--v-theme-primary), 0.4); + + .nav-item-icon { + color: inherit; + } + } + + &.disabled .nav-link-btn { + opacity: 0.4; + pointer-events: none; + } + } + + // ━━━ Nav Group ━━━ + .nav-group { + margin-block: 2px; + + .nav-group-label { + display: flex; + align-items: center; + gap: 8px; + padding: 8px 12px; + border-radius: 6px; + cursor: pointer; + color: rgba(var(--v-theme-on-surface), 0.78); + transition: background-color 0.15s ease; + white-space: nowrap; + overflow: hidden; + user-select: none; + } + + .nav-group-label:hover { + background-color: rgba(var(--v-theme-on-surface), 0.06); + } + + &.active > .nav-group-label { + color: rgb(var(--v-theme-primary)); + } + + .nav-group-arrow { + margin-inline-start: auto; + transition: transform 0.25s ease-in-out; + flex-shrink: 0; + } + + &.open > .nav-group-label .nav-group-arrow { + transform: rotate(90deg); + } + + .nav-group-children { + list-style: none; + padding: 0; + margin: 0; + padding-inline-start: 12px; + } + + &.disabled > .nav-group-label { + opacity: 0.4; + pointer-events: none; + } + } + + // ━━━ Section Title ━━━ + .nav-section-title { + margin-block: 12px 4px; + padding-inline: 12px; + + .nav-section-title-text { + font-size: 0.6875rem; + font-weight: 600; + letter-spacing: 1px; + text-transform: uppercase; + color: rgba(var(--v-theme-on-surface), 0.38); + white-space: nowrap; + overflow: hidden; + } + + .nav-section-title-icon { + color: rgba(var(--v-theme-on-surface), 0.38); + margin-inline: auto; + } + + &:first-child { + margin-block-start: 0; + } + } +} + +// ━━━ Collapsed / Mini Mode ━━━ +.layout-vertical-nav-collapsed .layout-vertical-nav:not(:hover) { + inline-size: layout-variables.$layout-vertical-nav-collapsed-width; +} + +.layout-vertical-nav-collapsed .layout-vertical-nav:hover { + box-shadow: 0 4px 25px 0 rgba(0, 0, 0, 0.25); +} + +// ━━━ Content Wrapper ━━━ +.layout-content-wrapper { + display: flex; + flex-direction: column; + flex-grow: 1; + min-block-size: 100vh; + padding-inline-start: layout-variables.$layout-vertical-nav-width; + transition: padding-inline-start 0.25s ease-in-out; +} + +.layout-vertical-nav-collapsed .layout-content-wrapper { + padding-inline-start: layout-variables.$layout-vertical-nav-collapsed-width; +} + +// ━━━ Navbar ━━━ +.layout-navbar { + position: sticky; + inset-block-start: 0; + z-index: layout-variables.$layout-vertical-nav-layout-navbar-z-index; + background-color: rgb(var(--v-theme-surface)); + border-block-end: 1px solid rgba(var(--v-border-color), var(--v-border-opacity)); + min-block-size: layout-variables.$layout-vertical-nav-navbar-height; + + .navbar-content { + block-size: 100%; + } +} + +// ━━━ Page Content ━━━ +.layout-page-content { + flex-grow: 1; + padding: 24px; +} + +// ━━━ Footer ━━━ +.layout-footer { + display: flex; + align-items: center; + justify-content: center; + min-block-size: layout-variables.$layout-vertical-nav-footer-height; + padding-inline: 24px; +} + +// ━━━ Overlay ━━━ +.layout-overlay { + position: fixed; + inset: 0; + z-index: layout-variables.$layout-overlay-z-index; + background-color: rgba(0, 0, 0, 0.5); + opacity: 0; + pointer-events: none; + transition: opacity 0.25s ease-in-out; + + &.visible { + opacity: 1; + pointer-events: auto; + } +} + +// ━━━ Responsive (Mobile Overlay) ━━━ +@media (max-width: 1279.98px) { + .layout-vertical-nav { + position: fixed; + transform: translateX(-100%); + + &.visible { + transform: translateX(0); + } + } + + .layout-content-wrapper { + padding-inline-start: 0 !important; + } + + .layout-vertical-nav-collapsed .layout-content-wrapper { + padding-inline-start: 0 !important; + } +} + +// ━━━ Transition for nav group expand/collapse ━━━ +.vertical-nav-group-enter-active, +.vertical-nav-group-leave-active { + overflow: hidden; + transition: max-height 0.3s ease-in-out, opacity 0.25s ease-in-out; +} + +.vertical-nav-group-enter-from, +.vertical-nav-group-leave-to { + max-height: 0; + opacity: 0; +} + +.vertical-nav-group-enter-to, +.vertical-nav-group-leave-from { + max-height: 500px; + opacity: 1; +} diff --git a/website/resources/ts/@layouts/types.ts b/website/resources/ts/@layouts/types.ts new file mode 100644 index 0000000..5518838 --- /dev/null +++ b/website/resources/ts/@layouts/types.ts @@ -0,0 +1,23 @@ +export interface NavLink { + title: string + to: string + icon?: string + badgeContent?: string + badgeClass?: string + disable?: boolean +} + +export interface NavGroup { + title: string + icon?: string + children: (NavLink | NavGroup)[] + badgeContent?: string + badgeClass?: string + disable?: boolean +} + +export interface NavSectionTitle { + heading: string +} + +export type VerticalNavItems = (NavLink | NavGroup | NavSectionTitle)[] diff --git a/website/resources/ts/Layouts/AccountLayout.vue b/website/resources/ts/Layouts/AccountLayout.vue index b63d45f..4baa464 100644 --- a/website/resources/ts/Layouts/AccountLayout.vue +++ b/website/resources/ts/Layouts/AccountLayout.vue @@ -1,15 +1,11 @@ diff --git a/website/resources/ts/Layouts/AdminLayout.vue b/website/resources/ts/Layouts/AdminLayout.vue index ed8254b..97980d5 100644 --- a/website/resources/ts/Layouts/AdminLayout.vue +++ b/website/resources/ts/Layouts/AdminLayout.vue @@ -1,15 +1,11 @@ diff --git a/website/resources/ts/navigation/account.ts b/website/resources/ts/navigation/account.ts index 515a3fc..14dcac8 100644 --- a/website/resources/ts/navigation/account.ts +++ b/website/resources/ts/navigation/account.ts @@ -1,16 +1,16 @@ -export interface NavItem { - title: string - href: string - icon: string - matchPrefix: string -} +import type { VerticalNavItems } from '@layouts/types' -export const accountNavItems: NavItem[] = [ - { title: 'Dashboard', href: '/dashboard', icon: 'tabler-smart-home', matchPrefix: '/dashboard' }, - { title: 'Services', href: '/services', icon: 'tabler-server', matchPrefix: '/services' }, - { title: 'Subscriptions', href: '/subscriptions', icon: 'tabler-receipt', matchPrefix: '/subscriptions' }, - { title: 'Billing', href: '/billing', icon: 'tabler-credit-card', matchPrefix: '/billing' }, - { title: 'Plans', href: '/plans', icon: 'tabler-package', matchPrefix: '/plans' }, - { title: 'Support', href: '/tickets', icon: 'tabler-headset', matchPrefix: '/tickets' }, - { title: 'Settings', href: '/profile', icon: 'tabler-settings', matchPrefix: '/profile' }, +export const accountNavItems: VerticalNavItems = [ + { heading: 'Account' }, + { title: 'Dashboard', to: '/dashboard', icon: 'tabler-smart-home' }, + { title: 'Services', to: '/services', icon: 'tabler-server' }, + { title: 'Subscriptions', to: '/subscriptions', icon: 'tabler-receipt' }, + + { heading: 'Billing' }, + { title: 'Billing', to: '/billing', icon: 'tabler-credit-card' }, + { title: 'Plans', to: '/plans', icon: 'tabler-package' }, + + { heading: 'Support' }, + { title: 'Tickets', to: '/tickets', icon: 'tabler-headset' }, + { title: 'Settings', to: '/profile', icon: 'tabler-settings' }, ] diff --git a/website/resources/ts/navigation/admin.ts b/website/resources/ts/navigation/admin.ts index fa4c488..22fc3e4 100644 --- a/website/resources/ts/navigation/admin.ts +++ b/website/resources/ts/navigation/admin.ts @@ -1,14 +1,23 @@ -import type { NavItem } from './account' +import type { VerticalNavItems } from '@layouts/types' -export const adminNavItems: NavItem[] = [ - { title: 'Dashboard', href: '/dashboard', icon: 'tabler-smart-home', matchPrefix: '/dashboard' }, - { title: 'Plans', href: '/plans', icon: 'tabler-package', matchPrefix: '/plans' }, - { title: 'Customers', href: '/customers', icon: 'tabler-users', matchPrefix: '/customers' }, - { title: 'Services', href: '/services', icon: 'tabler-server', matchPrefix: '/services' }, - { title: 'Orders', href: '/orders', icon: 'tabler-shopping-cart', matchPrefix: '/orders' }, - { title: 'Invoices', href: '/invoices', icon: 'tabler-file-invoice', matchPrefix: '/invoices' }, - { title: 'Coupons', href: '/coupons', icon: 'tabler-discount-2', matchPrefix: '/coupons' }, - { title: 'Tickets', href: '/tickets', icon: 'tabler-message-circle', matchPrefix: '/tickets' }, - { title: 'Audit Logs', href: '/audit-logs', icon: 'tabler-clipboard-list', matchPrefix: '/audit-logs' }, - { title: 'Settings', href: '/settings', icon: 'tabler-settings', matchPrefix: '/settings' }, +export const adminNavItems: VerticalNavItems = [ + { heading: 'Dashboard' }, + { title: 'Analytics', to: '/dashboard', icon: 'tabler-smart-home' }, + + { heading: 'Management' }, + { title: 'Customers', to: '/customers', icon: 'tabler-users' }, + { title: 'Plans', to: '/plans', icon: 'tabler-package' }, + { title: 'Services', to: '/services', icon: 'tabler-server' }, + { title: 'Orders', to: '/orders', icon: 'tabler-shopping-cart' }, + + { heading: 'Billing' }, + { title: 'Invoices', to: '/invoices', icon: 'tabler-file-invoice' }, + { title: 'Coupons', to: '/coupons', icon: 'tabler-discount-2' }, + + { heading: 'Support' }, + { title: 'Tickets', to: '/tickets', icon: 'tabler-message-circle' }, + + { heading: 'System' }, + { title: 'Audit Logs', to: '/audit-logs', icon: 'tabler-clipboard-list' }, + { title: 'Settings', to: '/settings', icon: 'tabler-settings' }, ]