@omnitend/dashboard-for-laravel 0.4.7
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +21 -0
- package/README.md +397 -0
- package/dist/components/base/DAccordion.vue.d.ts +12 -0
- package/dist/components/base/DAccordionItem.vue.d.ts +12 -0
- package/dist/components/base/DAlert.vue.d.ts +12 -0
- package/dist/components/base/DAvatar.vue.d.ts +12 -0
- package/dist/components/base/DBadge.vue.d.ts +12 -0
- package/dist/components/base/DBreadcrumb.vue.d.ts +12 -0
- package/dist/components/base/DButton.vue.d.ts +29 -0
- package/dist/components/base/DButtonGroup.vue.d.ts +12 -0
- package/dist/components/base/DButtonToolbar.vue.d.ts +12 -0
- package/dist/components/base/DCard.vue.d.ts +12 -0
- package/dist/components/base/DCarousel.vue.d.ts +12 -0
- package/dist/components/base/DCarouselSlide.vue.d.ts +12 -0
- package/dist/components/base/DCol.vue.d.ts +12 -0
- package/dist/components/base/DCollapse.vue.d.ts +12 -0
- package/dist/components/base/DContainer.vue.d.ts +12 -0
- package/dist/components/base/DDropdown.vue.d.ts +12 -0
- package/dist/components/base/DDropdownDivider.vue.d.ts +2 -0
- package/dist/components/base/DDropdownItem.vue.d.ts +12 -0
- package/dist/components/base/DForm.vue.d.ts +12 -0
- package/dist/components/base/DFormCheckbox.vue.d.ts +12 -0
- package/dist/components/base/DFormGroup.vue.d.ts +12 -0
- package/dist/components/base/DFormInput.vue.d.ts +2 -0
- package/dist/components/base/DFormInvalidFeedback.vue.d.ts +12 -0
- package/dist/components/base/DFormRadio.vue.d.ts +12 -0
- package/dist/components/base/DFormSelect.vue.d.ts +12 -0
- package/dist/components/base/DFormSpinbutton.vue.d.ts +12 -0
- package/dist/components/base/DFormTags.vue.d.ts +12 -0
- package/dist/components/base/DFormText.vue.d.ts +12 -0
- package/dist/components/base/DFormTextarea.vue.d.ts +2 -0
- package/dist/components/base/DImage.vue.d.ts +12 -0
- package/dist/components/base/DInputGroup.vue.d.ts +12 -0
- package/dist/components/base/DLink.vue.d.ts +12 -0
- package/dist/components/base/DListGroup.vue.d.ts +12 -0
- package/dist/components/base/DListGroupItem.vue.d.ts +12 -0
- package/dist/components/base/DModal.vue.d.ts +12 -0
- package/dist/components/base/DNav.vue.d.ts +12 -0
- package/dist/components/base/DNavItem.vue.d.ts +12 -0
- package/dist/components/base/DNavbar.vue.d.ts +12 -0
- package/dist/components/base/DNavbarBrand.vue.d.ts +12 -0
- package/dist/components/base/DNavbarNav.vue.d.ts +12 -0
- package/dist/components/base/DNavbarToggle.vue.d.ts +12 -0
- package/dist/components/base/DOffcanvas.vue.d.ts +12 -0
- package/dist/components/base/DOverlay.vue.d.ts +12 -0
- package/dist/components/base/DPagination.vue.d.ts +2 -0
- package/dist/components/base/DPlaceholder.vue.d.ts +12 -0
- package/dist/components/base/DPopover.vue.d.ts +12 -0
- package/dist/components/base/DProgress.vue.d.ts +12 -0
- package/dist/components/base/DRow.vue.d.ts +12 -0
- package/dist/components/base/DSpinner.vue.d.ts +2 -0
- package/dist/components/base/DTab.vue.d.ts +12 -0
- package/dist/components/base/DTable.vue.d.ts +26 -0
- package/dist/components/base/DTabs.vue.d.ts +12 -0
- package/dist/components/base/DToast.vue.d.ts +12 -0
- package/dist/components/base/DToaster.vue.d.ts +12 -0
- package/dist/components/base/DTooltip.vue.d.ts +12 -0
- package/dist/components/extended/DXBasicForm.vue.d.ts +39 -0
- package/dist/components/extended/DXDashboard.vue.d.ts +52 -0
- package/dist/components/extended/DXDashboardNavbar.vue.d.ts +53 -0
- package/dist/components/extended/DXDashboardSidebar.vue.d.ts +37 -0
- package/dist/components/extended/DXForm.vue.d.ts +31 -0
- package/dist/components/extended/DXTable.vue.d.ts +190 -0
- package/dist/composables/defineForm.d.ts +35 -0
- package/dist/composables/useForm.d.ts +46 -0
- package/dist/composables/useToast.d.ts +1 -0
- package/dist/dashboard-for-laravel.js +17748 -0
- package/dist/dashboard-for-laravel.js.map +1 -0
- package/dist/dashboard-for-laravel.umd.cjs +11 -0
- package/dist/dashboard-for-laravel.umd.cjs.map +1 -0
- package/dist/index.d.ts +73 -0
- package/dist/style.css +5 -0
- package/dist/types/index.d.ts +37 -0
- package/dist/types/navigation.d.ts +17 -0
- package/dist/utils/api.d.ts +30 -0
- package/docs/public/api-reference.json +1932 -0
- package/docs/public/docs-map.md +85 -0
- package/docs/public/llms.txt +110 -0
- package/package.json +116 -0
- package/resources/css/theme.scss +219 -0
- package/resources/js/components/base/DAccordion.vue +21 -0
- package/resources/js/components/base/DAccordionItem.vue +14 -0
- package/resources/js/components/base/DAlert.vue +14 -0
- package/resources/js/components/base/DAvatar.vue +21 -0
- package/resources/js/components/base/DBadge.vue +14 -0
- package/resources/js/components/base/DBreadcrumb.vue +21 -0
- package/resources/js/components/base/DButton.vue +58 -0
- package/resources/js/components/base/DButtonGroup.vue +21 -0
- package/resources/js/components/base/DButtonToolbar.vue +21 -0
- package/resources/js/components/base/DCard.vue +35 -0
- package/resources/js/components/base/DCarousel.vue +21 -0
- package/resources/js/components/base/DCarouselSlide.vue +14 -0
- package/resources/js/components/base/DCol.vue +14 -0
- package/resources/js/components/base/DCollapse.vue +34 -0
- package/resources/js/components/base/DContainer.vue +14 -0
- package/resources/js/components/base/DDropdown.vue +16 -0
- package/resources/js/components/base/DDropdownDivider.vue +7 -0
- package/resources/js/components/base/DDropdownItem.vue +14 -0
- package/resources/js/components/base/DForm.vue +21 -0
- package/resources/js/components/base/DFormCheckbox.vue +14 -0
- package/resources/js/components/base/DFormGroup.vue +11 -0
- package/resources/js/components/base/DFormInput.vue +7 -0
- package/resources/js/components/base/DFormInvalidFeedback.vue +16 -0
- package/resources/js/components/base/DFormRadio.vue +21 -0
- package/resources/js/components/base/DFormSelect.vue +14 -0
- package/resources/js/components/base/DFormSpinbutton.vue +21 -0
- package/resources/js/components/base/DFormTags.vue +21 -0
- package/resources/js/components/base/DFormText.vue +16 -0
- package/resources/js/components/base/DFormTextarea.vue +7 -0
- package/resources/js/components/base/DImage.vue +21 -0
- package/resources/js/components/base/DInputGroup.vue +21 -0
- package/resources/js/components/base/DLink.vue +21 -0
- package/resources/js/components/base/DListGroup.vue +21 -0
- package/resources/js/components/base/DListGroupItem.vue +14 -0
- package/resources/js/components/base/DModal.vue +11 -0
- package/resources/js/components/base/DNav.vue +14 -0
- package/resources/js/components/base/DNavItem.vue +14 -0
- package/resources/js/components/base/DNavbar.vue +21 -0
- package/resources/js/components/base/DNavbarBrand.vue +14 -0
- package/resources/js/components/base/DNavbarNav.vue +14 -0
- package/resources/js/components/base/DNavbarToggle.vue +14 -0
- package/resources/js/components/base/DOffcanvas.vue +11 -0
- package/resources/js/components/base/DOverlay.vue +21 -0
- package/resources/js/components/base/DPagination.vue +7 -0
- package/resources/js/components/base/DPlaceholder.vue +21 -0
- package/resources/js/components/base/DPopover.vue +21 -0
- package/resources/js/components/base/DProgress.vue +21 -0
- package/resources/js/components/base/DRow.vue +14 -0
- package/resources/js/components/base/DSpinner.vue +7 -0
- package/resources/js/components/base/DTab.vue +14 -0
- package/resources/js/components/base/DTable.vue +62 -0
- package/resources/js/components/base/DTabs.vue +21 -0
- package/resources/js/components/base/DToast.vue +16 -0
- package/resources/js/components/base/DToaster.vue +16 -0
- package/resources/js/components/base/DTooltip.vue +21 -0
- package/resources/js/components/extended/DXBasicForm.vue +177 -0
- package/resources/js/components/extended/DXDashboard.vue +208 -0
- package/resources/js/components/extended/DXDashboardNavbar.vue +112 -0
- package/resources/js/components/extended/DXDashboardSidebar.vue +233 -0
- package/resources/js/components/extended/DXForm.vue +44 -0
- package/resources/js/components/extended/DXTable.vue +1345 -0
- package/resources/js/composables/defineForm.ts +78 -0
- package/resources/js/composables/useForm.ts +272 -0
- package/resources/js/composables/useToast.ts +1 -0
- package/resources/js/index.ts +118 -0
- package/resources/js/types/index.ts +61 -0
- package/resources/js/types/navigation.ts +19 -0
- package/resources/js/utils/api.ts +182 -0
- package/scripts/mcp-server.mjs +359 -0
|
@@ -0,0 +1,208 @@
|
|
|
1
|
+
<template>
|
|
2
|
+
<div class="dashboard-layout d-flex" :data-dashboard-id="dashboardId">
|
|
3
|
+
<!-- Sidebar -->
|
|
4
|
+
<DXDashboardSidebar
|
|
5
|
+
:navigation="navigation"
|
|
6
|
+
:current-url="currentUrl"
|
|
7
|
+
:title="title"
|
|
8
|
+
:collapsed="collapsed"
|
|
9
|
+
:hidden="hidden"
|
|
10
|
+
@toggle="toggleSidebar"
|
|
11
|
+
>
|
|
12
|
+
<!-- Dynamically forward all sidebar-* slots by stripping the prefix -->
|
|
13
|
+
<template
|
|
14
|
+
v-for="(originalName, strippedName) in sidebarSlots"
|
|
15
|
+
:key="strippedName"
|
|
16
|
+
#[strippedName]="slotProps"
|
|
17
|
+
>
|
|
18
|
+
<slot :name="originalName" v-bind="slotProps" />
|
|
19
|
+
</template>
|
|
20
|
+
</DXDashboardSidebar>
|
|
21
|
+
|
|
22
|
+
<!-- Main Content Area -->
|
|
23
|
+
<div class="dashboard-content flex-grow-1">
|
|
24
|
+
<!-- Top Navbar -->
|
|
25
|
+
<DXDashboardNavbar
|
|
26
|
+
:page-title="pageTitle"
|
|
27
|
+
:user="user"
|
|
28
|
+
:logout-url="logoutUrl"
|
|
29
|
+
@toggle-sidebar="toggleSidebar"
|
|
30
|
+
>
|
|
31
|
+
<!-- Dynamically forward all navbar-* slots by stripping the prefix -->
|
|
32
|
+
<template
|
|
33
|
+
v-for="(originalName, strippedName) in navbarSlots"
|
|
34
|
+
:key="strippedName"
|
|
35
|
+
#[strippedName]="slotProps"
|
|
36
|
+
>
|
|
37
|
+
<slot :name="originalName" v-bind="slotProps" />
|
|
38
|
+
</template>
|
|
39
|
+
</DXDashboardNavbar>
|
|
40
|
+
|
|
41
|
+
<!-- Page Content -->
|
|
42
|
+
<main class="dashboard-main p-4">
|
|
43
|
+
<DContainer fluid>
|
|
44
|
+
<DRow class="justify-content-center">
|
|
45
|
+
<DCol cols="12" xl="10">
|
|
46
|
+
<slot />
|
|
47
|
+
</DCol>
|
|
48
|
+
</DRow>
|
|
49
|
+
</DContainer>
|
|
50
|
+
</main>
|
|
51
|
+
</div>
|
|
52
|
+
</div>
|
|
53
|
+
</template>
|
|
54
|
+
|
|
55
|
+
<script setup lang="ts">
|
|
56
|
+
import { ref, computed, useSlots } from 'vue';
|
|
57
|
+
import DXDashboardSidebar from './DXDashboardSidebar.vue';
|
|
58
|
+
import DXDashboardNavbar from './DXDashboardNavbar.vue';
|
|
59
|
+
import DContainer from '../base/DContainer.vue';
|
|
60
|
+
import DRow from '../base/DRow.vue';
|
|
61
|
+
import DCol from '../base/DCol.vue';
|
|
62
|
+
import type { Navigation } from '../../types/navigation';
|
|
63
|
+
|
|
64
|
+
const slots = useSlots();
|
|
65
|
+
|
|
66
|
+
interface Props {
|
|
67
|
+
/** Navigation structure for sidebar */
|
|
68
|
+
navigation: Navigation;
|
|
69
|
+
|
|
70
|
+
/** Current URL path for active state */
|
|
71
|
+
currentUrl: string;
|
|
72
|
+
|
|
73
|
+
/** Dashboard title shown in sidebar brand */
|
|
74
|
+
title?: string;
|
|
75
|
+
|
|
76
|
+
/** Page title shown in navbar */
|
|
77
|
+
pageTitle?: string;
|
|
78
|
+
|
|
79
|
+
/** User object for navbar dropdown */
|
|
80
|
+
user?: { name: string; email: string } | null;
|
|
81
|
+
|
|
82
|
+
/** Logout URL for navbar dropdown */
|
|
83
|
+
logoutUrl?: string;
|
|
84
|
+
|
|
85
|
+
/** LocalStorage key for sidebar state persistence */
|
|
86
|
+
storageKey?: string;
|
|
87
|
+
|
|
88
|
+
/**
|
|
89
|
+
* Unique ID for this dashboard instance (for nested dashboards)
|
|
90
|
+
* Used to scope visibility state when multiple dashboards exist on same page
|
|
91
|
+
* If not provided, uses global HTML class approach (for SSR compatibility)
|
|
92
|
+
*/
|
|
93
|
+
dashboardId?: string;
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
const props = withDefaults(defineProps<Props>(), {
|
|
97
|
+
title: 'Dashboard',
|
|
98
|
+
pageTitle: '',
|
|
99
|
+
user: null,
|
|
100
|
+
logoutUrl: '/logout',
|
|
101
|
+
storageKey: 'dashboard-sidebar-hidden',
|
|
102
|
+
dashboardId: '',
|
|
103
|
+
});
|
|
104
|
+
|
|
105
|
+
const collapsed = ref(false);
|
|
106
|
+
|
|
107
|
+
// Compute sidebar slots (strip 'sidebar-' prefix)
|
|
108
|
+
const sidebarSlots = computed(() => {
|
|
109
|
+
const result: Record<string, string> = {};
|
|
110
|
+
Object.keys(slots).forEach(name => {
|
|
111
|
+
if (name.startsWith('sidebar-')) {
|
|
112
|
+
const strippedName = name.substring(8); // Remove 'sidebar-' prefix
|
|
113
|
+
result[strippedName] = name;
|
|
114
|
+
}
|
|
115
|
+
});
|
|
116
|
+
return result;
|
|
117
|
+
});
|
|
118
|
+
|
|
119
|
+
// Compute navbar slots (strip 'navbar-' prefix)
|
|
120
|
+
const navbarSlots = computed(() => {
|
|
121
|
+
const result: Record<string, string> = {};
|
|
122
|
+
Object.keys(slots).forEach(name => {
|
|
123
|
+
if (name.startsWith('navbar-')) {
|
|
124
|
+
const strippedName = name.substring(7); // Remove 'navbar-' prefix
|
|
125
|
+
result[strippedName] = name;
|
|
126
|
+
}
|
|
127
|
+
});
|
|
128
|
+
return result;
|
|
129
|
+
});
|
|
130
|
+
|
|
131
|
+
// Initialize sidebar visibility from localStorage
|
|
132
|
+
const getInitialHiddenState = (): boolean => {
|
|
133
|
+
// Skip during SSR - no access to localStorage or document
|
|
134
|
+
if (typeof window === 'undefined') {
|
|
135
|
+
return !props.dashboardId; // Default: hidden for global, visible for scoped
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
try {
|
|
139
|
+
const savedHidden = localStorage.getItem(props.storageKey);
|
|
140
|
+
if (savedHidden !== null) {
|
|
141
|
+
const isHidden = JSON.parse(savedHidden);
|
|
142
|
+
|
|
143
|
+
// If no dashboard ID (global instance), update HTML class for SSR compatibility
|
|
144
|
+
if (!props.dashboardId) {
|
|
145
|
+
if (isHidden) {
|
|
146
|
+
document.documentElement.classList.remove('sidebar-visible');
|
|
147
|
+
} else {
|
|
148
|
+
document.documentElement.classList.add('sidebar-visible');
|
|
149
|
+
}
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
return isHidden;
|
|
153
|
+
}
|
|
154
|
+
} catch (error) {
|
|
155
|
+
console.error('Error loading sidebar state:', error);
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
// Default: hidden for global instances (docs), visible for scoped instances (examples)
|
|
159
|
+
if (!props.dashboardId) {
|
|
160
|
+
document.documentElement.classList.remove('sidebar-visible');
|
|
161
|
+
return true;
|
|
162
|
+
}
|
|
163
|
+
return false; // Show sidebar in scoped instances by default
|
|
164
|
+
};
|
|
165
|
+
|
|
166
|
+
const hidden = ref(getInitialHiddenState());
|
|
167
|
+
|
|
168
|
+
const toggleSidebar = () => {
|
|
169
|
+
hidden.value = !hidden.value;
|
|
170
|
+
|
|
171
|
+
// Skip during SSR
|
|
172
|
+
if (typeof window === 'undefined') return;
|
|
173
|
+
|
|
174
|
+
// If no dashboard ID (global instance), update HTML class
|
|
175
|
+
if (!props.dashboardId) {
|
|
176
|
+
if (hidden.value) {
|
|
177
|
+
document.documentElement.classList.remove('sidebar-visible');
|
|
178
|
+
} else {
|
|
179
|
+
document.documentElement.classList.add('sidebar-visible');
|
|
180
|
+
}
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
// Save to localStorage
|
|
184
|
+
try {
|
|
185
|
+
localStorage.setItem(props.storageKey, JSON.stringify(hidden.value));
|
|
186
|
+
} catch (error) {
|
|
187
|
+
console.error('Error saving sidebar state:', error);
|
|
188
|
+
}
|
|
189
|
+
};
|
|
190
|
+
</script>
|
|
191
|
+
|
|
192
|
+
<style scoped>
|
|
193
|
+
.dashboard-layout {
|
|
194
|
+
min-height: 100vh;
|
|
195
|
+
background-color: #f8f9fa;
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
.dashboard-content {
|
|
199
|
+
display: flex;
|
|
200
|
+
flex-direction: column;
|
|
201
|
+
overflow-x: hidden;
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
.dashboard-main {
|
|
205
|
+
flex: 1;
|
|
206
|
+
max-width: 100%;
|
|
207
|
+
}
|
|
208
|
+
</style>
|
|
@@ -0,0 +1,112 @@
|
|
|
1
|
+
<template>
|
|
2
|
+
<header class="dashboard-navbar bg-white border-bottom">
|
|
3
|
+
<DContainer fluid class="h-100">
|
|
4
|
+
<DRow class="h-100 align-items-center">
|
|
5
|
+
<DCol class="d-flex align-items-center gap-3">
|
|
6
|
+
<DButton
|
|
7
|
+
variant="link"
|
|
8
|
+
class="text-dark p-0"
|
|
9
|
+
@click="$emit('toggleSidebar')"
|
|
10
|
+
aria-label="Toggle sidebar"
|
|
11
|
+
>
|
|
12
|
+
<slot name="menu-icon">
|
|
13
|
+
<svg
|
|
14
|
+
xmlns="http://www.w3.org/2000/svg"
|
|
15
|
+
width="24"
|
|
16
|
+
height="24"
|
|
17
|
+
fill="currentColor"
|
|
18
|
+
viewBox="0 0 16 16"
|
|
19
|
+
>
|
|
20
|
+
<path
|
|
21
|
+
fill-rule="evenodd"
|
|
22
|
+
d="M2.5 12a.5.5 0 0 1 .5-.5h10a.5.5 0 0 1 0 1H3a.5.5 0 0 1-.5-.5m0-4a.5.5 0 0 1 .5-.5h10a.5.5 0 0 1 0 1H3a.5.5 0 0 1-.5-.5m0-4a.5.5 0 0 1 .5-.5h10a.5.5 0 0 1 0 1H3a.5.5 0 0 1-.5-.5"
|
|
23
|
+
/>
|
|
24
|
+
</svg>
|
|
25
|
+
</slot>
|
|
26
|
+
</DButton>
|
|
27
|
+
|
|
28
|
+
<h4 v-if="pageTitle" class="mb-0 fw-semibold d-none d-md-block">{{ pageTitle }}</h4>
|
|
29
|
+
|
|
30
|
+
<div class="flex-grow-1 d-flex justify-content-center">
|
|
31
|
+
<slot name="search" />
|
|
32
|
+
</div>
|
|
33
|
+
</DCol>
|
|
34
|
+
|
|
35
|
+
<DCol cols="auto">
|
|
36
|
+
<slot name="user-menu" :user="user">
|
|
37
|
+
<DDropdown
|
|
38
|
+
v-if="user"
|
|
39
|
+
variant="link"
|
|
40
|
+
class="text-dark"
|
|
41
|
+
menu-class="dropdown-menu-end"
|
|
42
|
+
no-caret
|
|
43
|
+
>
|
|
44
|
+
<template #button-content>
|
|
45
|
+
<slot name="user-icon" :initial="getUserInitial(user)">
|
|
46
|
+
<div class="user-avatar">
|
|
47
|
+
{{ getUserInitial(user) }}
|
|
48
|
+
</div>
|
|
49
|
+
</slot>
|
|
50
|
+
</template>
|
|
51
|
+
|
|
52
|
+
<slot name="user-menu-items" :user="user" />
|
|
53
|
+
</DDropdown>
|
|
54
|
+
</slot>
|
|
55
|
+
</DCol>
|
|
56
|
+
</DRow>
|
|
57
|
+
</DContainer>
|
|
58
|
+
</header>
|
|
59
|
+
</template>
|
|
60
|
+
|
|
61
|
+
<script setup lang="ts">
|
|
62
|
+
import DContainer from "../base/DContainer.vue";
|
|
63
|
+
import DRow from "../base/DRow.vue";
|
|
64
|
+
import DCol from "../base/DCol.vue";
|
|
65
|
+
import DButton from "../base/DButton.vue";
|
|
66
|
+
import DDropdown from "../base/DDropdown.vue";
|
|
67
|
+
|
|
68
|
+
withDefaults(
|
|
69
|
+
defineProps<{
|
|
70
|
+
user?: {
|
|
71
|
+
name: string;
|
|
72
|
+
email: string;
|
|
73
|
+
[key: string]: any;
|
|
74
|
+
} | null;
|
|
75
|
+
pageTitle?: string;
|
|
76
|
+
}>(),
|
|
77
|
+
{
|
|
78
|
+
user: null,
|
|
79
|
+
pageTitle: "",
|
|
80
|
+
},
|
|
81
|
+
);
|
|
82
|
+
|
|
83
|
+
defineEmits<{
|
|
84
|
+
toggleSidebar: [];
|
|
85
|
+
}>();
|
|
86
|
+
|
|
87
|
+
const getUserInitial = (user: { name: string } | null) => {
|
|
88
|
+
if (!user?.name) return "";
|
|
89
|
+
return user.name.charAt(0).toUpperCase();
|
|
90
|
+
};
|
|
91
|
+
</script>
|
|
92
|
+
|
|
93
|
+
<style scoped>
|
|
94
|
+
.dashboard-navbar {
|
|
95
|
+
position: sticky;
|
|
96
|
+
top: 0;
|
|
97
|
+
z-index: 1000;
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
.user-avatar {
|
|
101
|
+
width: 32px;
|
|
102
|
+
height: 32px;
|
|
103
|
+
border-radius: 50%;
|
|
104
|
+
background-color: var(--bs-dark);
|
|
105
|
+
color: var(--bs-white);
|
|
106
|
+
display: flex;
|
|
107
|
+
align-items: center;
|
|
108
|
+
justify-content: center;
|
|
109
|
+
font-weight: 600;
|
|
110
|
+
font-size: 14px;
|
|
111
|
+
}
|
|
112
|
+
</style>
|
|
@@ -0,0 +1,233 @@
|
|
|
1
|
+
<template>
|
|
2
|
+
<aside
|
|
3
|
+
ref="sidebarRef"
|
|
4
|
+
class="dashboard-sidebar text-white"
|
|
5
|
+
:class="{
|
|
6
|
+
'sidebar-collapsed': collapsed,
|
|
7
|
+
'sidebar-hidden': hidden
|
|
8
|
+
}"
|
|
9
|
+
>
|
|
10
|
+
<div class="sidebar-header p-3">
|
|
11
|
+
<div class="d-flex align-items-center justify-content-between">
|
|
12
|
+
<slot name="brand" :collapsed="collapsed" :title="title">
|
|
13
|
+
<div class="brand-container" :class="{ 'collapsed': collapsed }">
|
|
14
|
+
<div class="brand-initial">{{ brandInitial }}</div>
|
|
15
|
+
<h5 v-if="!collapsed" class="mb-0 fw-bold ms-2">
|
|
16
|
+
{{ title }}
|
|
17
|
+
</h5>
|
|
18
|
+
</div>
|
|
19
|
+
</slot>
|
|
20
|
+
</div>
|
|
21
|
+
</div>
|
|
22
|
+
|
|
23
|
+
<nav class="sidebar-nav p-3">
|
|
24
|
+
<template v-for="(group, groupIndex) in navigation" :key="groupIndex">
|
|
25
|
+
<div v-if="group.visible !== false" class="nav-group mb-3">
|
|
26
|
+
<div
|
|
27
|
+
v-if="group.label && !collapsed"
|
|
28
|
+
class="nav-group-label text-uppercase small fw-semibold mb-2 px-2"
|
|
29
|
+
>
|
|
30
|
+
{{ group.label }}
|
|
31
|
+
</div>
|
|
32
|
+
|
|
33
|
+
<div v-if="group.label && collapsed" class="nav-group-divider">
|
|
34
|
+
<hr class="my-2 border-secondary" />
|
|
35
|
+
</div>
|
|
36
|
+
|
|
37
|
+
<ul class="nav flex-column gap-1">
|
|
38
|
+
<li v-for="(item, itemIndex) in group.items" :key="itemIndex" class="nav-item">
|
|
39
|
+
<slot
|
|
40
|
+
name="link"
|
|
41
|
+
:item="item"
|
|
42
|
+
:is-active="isActive(item.url)"
|
|
43
|
+
:collapsed="collapsed"
|
|
44
|
+
>
|
|
45
|
+
<a
|
|
46
|
+
:href="item.url"
|
|
47
|
+
class="nav-link d-flex align-items-center gap-2 rounded"
|
|
48
|
+
:class="{
|
|
49
|
+
'active': isActive(item.url),
|
|
50
|
+
'justify-content-center': collapsed
|
|
51
|
+
}"
|
|
52
|
+
>
|
|
53
|
+
<component
|
|
54
|
+
v-if="item.icon"
|
|
55
|
+
:is="item.icon"
|
|
56
|
+
class="nav-icon"
|
|
57
|
+
style="width: 20px; height: 20px;"
|
|
58
|
+
/>
|
|
59
|
+
<span v-if="!collapsed" class="nav-label">{{ item.label }}</span>
|
|
60
|
+
<span
|
|
61
|
+
v-if="item.badge && !collapsed"
|
|
62
|
+
class="badge ms-auto"
|
|
63
|
+
:class="`bg-${item.badgeColor || 'primary'}`"
|
|
64
|
+
>
|
|
65
|
+
{{ item.badge }}
|
|
66
|
+
</span>
|
|
67
|
+
</a>
|
|
68
|
+
</slot>
|
|
69
|
+
</li>
|
|
70
|
+
</ul>
|
|
71
|
+
</div>
|
|
72
|
+
</template>
|
|
73
|
+
</nav>
|
|
74
|
+
</aside>
|
|
75
|
+
</template>
|
|
76
|
+
|
|
77
|
+
<script setup lang="ts">
|
|
78
|
+
import { computed, ref, onMounted, watch, nextTick } from 'vue';
|
|
79
|
+
import type { Navigation } from '../../types/navigation';
|
|
80
|
+
|
|
81
|
+
const props = withDefaults(defineProps<{
|
|
82
|
+
navigation: Navigation;
|
|
83
|
+
currentUrl: string;
|
|
84
|
+
collapsed?: boolean;
|
|
85
|
+
hidden?: boolean;
|
|
86
|
+
title?: string;
|
|
87
|
+
}>(), {
|
|
88
|
+
collapsed: false,
|
|
89
|
+
hidden: false,
|
|
90
|
+
title: 'Dashboard',
|
|
91
|
+
});
|
|
92
|
+
|
|
93
|
+
defineEmits<{
|
|
94
|
+
toggle: [];
|
|
95
|
+
}>();
|
|
96
|
+
|
|
97
|
+
const sidebarRef = ref<HTMLElement | null>(null);
|
|
98
|
+
|
|
99
|
+
const brandInitial = computed(() => {
|
|
100
|
+
return props.title.charAt(0).toUpperCase();
|
|
101
|
+
});
|
|
102
|
+
|
|
103
|
+
const isActive = (url: string): boolean => {
|
|
104
|
+
// Normalize URLs for comparison (remove trailing slash, lowercase)
|
|
105
|
+
const normalizeUrl = (u: string) => u.toLowerCase().replace(/\/$/, '');
|
|
106
|
+
return normalizeUrl(props.currentUrl) === normalizeUrl(url);
|
|
107
|
+
};
|
|
108
|
+
|
|
109
|
+
const scrollToActiveItem = async (smooth = false) => {
|
|
110
|
+
await nextTick();
|
|
111
|
+
if (!sidebarRef.value) return;
|
|
112
|
+
|
|
113
|
+
const activeLink = sidebarRef.value.querySelector('.nav-link.active') as HTMLElement;
|
|
114
|
+
if (activeLink) {
|
|
115
|
+
const sidebar = sidebarRef.value;
|
|
116
|
+
const linkRect = activeLink.getBoundingClientRect();
|
|
117
|
+
const sidebarRect = sidebar.getBoundingClientRect();
|
|
118
|
+
|
|
119
|
+
// Calculate the position to scroll to (center the active item)
|
|
120
|
+
const scrollTop = activeLink.offsetTop - (sidebarRect.height / 2) + (linkRect.height / 2);
|
|
121
|
+
|
|
122
|
+
sidebar.scrollTo({
|
|
123
|
+
top: scrollTop,
|
|
124
|
+
behavior: smooth ? 'smooth' : 'instant',
|
|
125
|
+
});
|
|
126
|
+
}
|
|
127
|
+
};
|
|
128
|
+
|
|
129
|
+
// Scroll to active item on initial mount (instant, no animation)
|
|
130
|
+
onMounted(() => {
|
|
131
|
+
scrollToActiveItem(false);
|
|
132
|
+
});
|
|
133
|
+
|
|
134
|
+
// Watch for URL changes (client-side routing) and scroll smoothly
|
|
135
|
+
watch(() => props.currentUrl, () => {
|
|
136
|
+
scrollToActiveItem(true);
|
|
137
|
+
});
|
|
138
|
+
</script>
|
|
139
|
+
|
|
140
|
+
<style scoped>
|
|
141
|
+
.dashboard-sidebar {
|
|
142
|
+
min-height: 100vh;
|
|
143
|
+
transition: width 0.3s ease;
|
|
144
|
+
position: sticky;
|
|
145
|
+
top: 0;
|
|
146
|
+
height: 100vh;
|
|
147
|
+
overflow-y: auto;
|
|
148
|
+
overflow-x: hidden;
|
|
149
|
+
flex-shrink: 0;
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
.sidebar-header {
|
|
153
|
+
display: flex;
|
|
154
|
+
align-items: center;
|
|
155
|
+
border-bottom: 1px solid rgba(255, 255, 255, 0.1);
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
.brand-container {
|
|
159
|
+
display: flex;
|
|
160
|
+
align-items: center;
|
|
161
|
+
justify-content: flex-start;
|
|
162
|
+
transition: all 0.3s ease;
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
.brand-container.collapsed {
|
|
166
|
+
justify-content: center;
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
.brand-initial {
|
|
170
|
+
width: 32px;
|
|
171
|
+
height: 32px;
|
|
172
|
+
flex-shrink: 0;
|
|
173
|
+
display: flex;
|
|
174
|
+
align-items: center;
|
|
175
|
+
justify-content: center;
|
|
176
|
+
font-weight: 700;
|
|
177
|
+
font-size: 1.25rem;
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
.sidebar-nav {
|
|
181
|
+
overflow-x: hidden;
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
.nav-group-label {
|
|
185
|
+
font-size: 0.75rem;
|
|
186
|
+
letter-spacing: 0.5px;
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
:deep(.nav-link) {
|
|
190
|
+
padding: 0.625rem 0.75rem;
|
|
191
|
+
transition: all 0.2s ease;
|
|
192
|
+
text-decoration: none;
|
|
193
|
+
white-space: nowrap;
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
:deep(.nav-link.active) {
|
|
197
|
+
font-weight: 500;
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
:deep(.nav-icon) {
|
|
201
|
+
flex-shrink: 0;
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
:deep(.nav-label) {
|
|
205
|
+
flex: 1;
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
.sidebar-collapsed :deep(.nav-link) {
|
|
209
|
+
padding: 0.625rem;
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
/* Custom scrollbar */
|
|
213
|
+
.dashboard-sidebar::-webkit-scrollbar {
|
|
214
|
+
width: 6px;
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
.dashboard-sidebar::-webkit-scrollbar-track {
|
|
218
|
+
background: rgba(0, 0, 0, 0.1);
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
.dashboard-sidebar::-webkit-scrollbar-thumb {
|
|
222
|
+
background: rgba(255, 255, 255, 0.2);
|
|
223
|
+
border-radius: 3px;
|
|
224
|
+
}
|
|
225
|
+
|
|
226
|
+
.dashboard-sidebar::-webkit-scrollbar-thumb:hover {
|
|
227
|
+
background: rgba(255, 255, 255, 0.3);
|
|
228
|
+
}
|
|
229
|
+
|
|
230
|
+
.sidebar-hidden {
|
|
231
|
+
display: none;
|
|
232
|
+
}
|
|
233
|
+
</style>
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
<template>
|
|
2
|
+
<OBasicForm
|
|
3
|
+
:form="form.form"
|
|
4
|
+
:fields="form.fields"
|
|
5
|
+
:submit-text="submitText"
|
|
6
|
+
:submit-loading-text="submitLoadingText"
|
|
7
|
+
:show-submit="showSubmit"
|
|
8
|
+
@submit="emit('submit')"
|
|
9
|
+
>
|
|
10
|
+
<!-- Pass through all slots -->
|
|
11
|
+
<template v-for="(_, name) in $slots" #[name]="slotProps">
|
|
12
|
+
<slot :name="name" v-bind="slotProps" />
|
|
13
|
+
</template>
|
|
14
|
+
</OBasicForm>
|
|
15
|
+
</template>
|
|
16
|
+
|
|
17
|
+
<script setup lang="ts">
|
|
18
|
+
import OBasicForm from "./DXBasicForm.vue";
|
|
19
|
+
import type { DefineFormReturn } from "../../composables/defineForm";
|
|
20
|
+
|
|
21
|
+
interface Props {
|
|
22
|
+
/** Form object from defineForm */
|
|
23
|
+
form: DefineFormReturn<any>;
|
|
24
|
+
|
|
25
|
+
/** Submit button text */
|
|
26
|
+
submitText?: string;
|
|
27
|
+
|
|
28
|
+
/** Submit button loading text */
|
|
29
|
+
submitLoadingText?: string;
|
|
30
|
+
|
|
31
|
+
/** Show submit button */
|
|
32
|
+
showSubmit?: boolean;
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
withDefaults(defineProps<Props>(), {
|
|
36
|
+
submitText: "Submit",
|
|
37
|
+
submitLoadingText: "Submitting...",
|
|
38
|
+
showSubmit: true,
|
|
39
|
+
});
|
|
40
|
+
|
|
41
|
+
const emit = defineEmits<{
|
|
42
|
+
submit: [];
|
|
43
|
+
}>();
|
|
44
|
+
</script>
|