pgo-uiux2 1.0.0
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/.env +1 -0
- package/.env.production +1 -0
- package/.prettierrc +13 -0
- package/.vscode/extensions.json +3 -0
- package/BUTTON_GUIDE.md +257 -0
- package/README.md +49 -0
- package/THEME_REFERENCE.md +310 -0
- package/eslint.config.ts +27 -0
- package/index.html +13 -0
- package/package.json +85 -0
- package/public/favicon.ico +0 -0
- package/src/App.vue +368 -0
- package/src/assets/fonts/Faruma.ttf +0 -0
- package/src/components/examples/AppBarExample.vue +101 -0
- package/src/components/examples/AvatarExample.vue +47 -0
- package/src/components/examples/BannerExample.vue +287 -0
- package/src/components/examples/BaseInputExample.vue +25 -0
- package/src/components/examples/BreadcrumbExample.vue +53 -0
- package/src/components/examples/CardExample.vue +77 -0
- package/src/components/examples/ChipExample.vue +225 -0
- package/src/components/examples/DatePickerExample.vue +31 -0
- package/src/components/examples/DropdownExample.vue +84 -0
- package/src/components/examples/EditorExample.vue +200 -0
- package/src/components/examples/ExpansionPanelExample.vue +42 -0
- package/src/components/examples/FileUploadExample.vue +40 -0
- package/src/components/examples/FormExample.vue +121 -0
- package/src/components/examples/HugeTest.vue +8 -0
- package/src/components/examples/LayoutContainerExample.vue +80 -0
- package/src/components/examples/ModalExample.vue +82 -0
- package/src/components/examples/NavDrawerExample.vue +170 -0
- package/src/components/examples/NumberFieldExample.vue +145 -0
- package/src/components/examples/RadioButtonExample.vue +161 -0
- package/src/components/examples/SearchExample.vue +322 -0
- package/src/components/examples/SelectExample.vue +121 -0
- package/src/components/examples/StackedTableViewExample.vue +53 -0
- package/src/components/examples/TabExample.vue +336 -0
- package/src/components/examples/TableExample.vue +228 -0
- package/src/components/examples/TextFieldExample.vue +181 -0
- package/src/components/examples/TextareaExample.vue +173 -0
- package/src/components/examples/ThemeToggle.vue +50 -0
- package/src/components/examples/TimelineExample.vue +66 -0
- package/src/components/examples/TipTapEditorExample.vue +20 -0
- package/src/components/examples/TooltipExample.vue +53 -0
- package/src/components/examples/VueDatePickerShowcase.vue +214 -0
- package/src/components/examples/_DatePickerExample.vue +33 -0
- package/src/components/examples/__FormExample.vue +77 -0
- package/src/components/index.ts +25 -0
- package/src/components/pgo/AppBar.vue +347 -0
- package/src/components/pgo/Avatar.vue +139 -0
- package/src/components/pgo/Banner.vue +300 -0
- package/src/components/pgo/Breadcrumb.vue +101 -0
- package/src/components/pgo/Button.vue +171 -0
- package/src/components/pgo/Card.vue +178 -0
- package/src/components/pgo/ConfirmationModel.vue +32 -0
- package/src/components/pgo/DataTable.vue +845 -0
- package/src/components/pgo/DatePicker/CalendarPanel.vue +43 -0
- package/src/components/pgo/DatePicker/__DatePicker.vue +122 -0
- package/src/components/pgo/DatePicker/types.ts +11 -0
- package/src/components/pgo/DatePicker/useCalendar.ts +39 -0
- package/src/components/pgo/DatePicker/useDatePicker.ts +31 -0
- package/src/components/pgo/Deprecated/ToastContainer.vue +51 -0
- package/src/components/pgo/Deprecated/ToastItem.vue +55 -0
- package/src/components/pgo/Dropdown.vue +296 -0
- package/src/components/pgo/DropdownItem.vue +40 -0
- package/src/components/pgo/Editor.vue +511 -0
- package/src/components/pgo/ExpansionPanel.vue +185 -0
- package/src/components/pgo/Footer.vue +39 -0
- package/src/components/pgo/HeroIcon.vue +124 -0
- package/src/components/pgo/InputSearch.vue +194 -0
- package/src/components/pgo/LayoutContainer.vue +104 -0
- package/src/components/pgo/Main.vue +37 -0
- package/src/components/pgo/Modal.vue +273 -0
- package/src/components/pgo/NavDrawer.vue +127 -0
- package/src/components/pgo/NavDrawerItem.vue +161 -0
- package/src/components/pgo/NavigationDrawer.vue +849 -0
- package/src/components/pgo/OLDNavDrawer.vue +661 -0
- package/src/components/pgo/OldAppBar.vue +223 -0
- package/src/components/pgo/PApp.vue +102 -0
- package/src/components/pgo/Pagination.vue +242 -0
- package/src/components/pgo/Search copy.vue +310 -0
- package/src/components/pgo/Search.vue +411 -0
- package/src/components/pgo/StackedTableView.vue +167 -0
- package/src/components/pgo/Tab.vue +617 -0
- package/src/components/pgo/TestInput.vue +395 -0
- package/src/components/pgo/Timeline.vue +367 -0
- package/src/components/pgo/TimelineItem.vue +80 -0
- package/src/components/pgo/TipTapEditor.vue +315 -0
- package/src/components/pgo/Tooltip.NOTES.md +12 -0
- package/src/components/pgo/Tooltip.PROPS.md +21 -0
- package/src/components/pgo/Tooltip.vue +281 -0
- package/src/components/pgo/base/Base.vue +444 -0
- package/src/components/pgo/buttons/Chip.vue +324 -0
- package/src/components/pgo/buttons/ChipGroup.vue +224 -0
- package/src/components/pgo/buttons/Radio.vue +424 -0
- package/src/components/pgo/filters/FilterSection.vue +188 -0
- package/src/components/pgo/filters/Searchbar.vue +216 -0
- package/src/components/pgo/forms/DynamicForm.vue +45 -0
- package/src/components/pgo/forms/Form.vue +132 -0
- package/src/components/pgo/index.ts +15 -0
- package/src/components/pgo/inputs/Checkbox.vue +320 -0
- package/src/components/pgo/inputs/DatePicker.vue +395 -0
- package/src/components/pgo/inputs/FileUpload.vue +326 -0
- package/src/components/pgo/inputs/NumberField.vue +243 -0
- package/src/components/pgo/inputs/Radio.vue +162 -0
- package/src/components/pgo/inputs/RadioGroup.vue +188 -0
- package/src/components/pgo/inputs/Select.vue +535 -0
- package/src/components/pgo/inputs/TextField.vue +194 -0
- package/src/components/pgo/inputs/Textarea.vue +181 -0
- package/src/main.js +12 -0
- package/src/pgo-components/_index.js +31 -0
- package/src/pgo-components/assets/fonts/Faruma.ttf +0 -0
- package/src/pgo-components/assets/fonts/logo.png +0 -0
- package/src/pgo-components/composables/useTheme.js +10 -0
- package/src/pgo-components/directives/tooltip-directive.ts +393 -0
- package/src/pgo-components/index.js +96 -0
- package/src/pgo-components/lib/componentConfig.js +147 -0
- package/src/pgo-components/lib/core/composables/_useCalendar.ts +127 -0
- package/src/pgo-components/lib/core/composables/useDefaults.ts +15 -0
- package/src/pgo-components/lib/core/composables/useLanguageSelect.js +0 -0
- package/src/pgo-components/lib/core/composables/useRtl.ts +12 -0
- package/src/pgo-components/lib/core/defaults/createDefaults.ts +5 -0
- package/src/pgo-components/lib/core/defaults/defaults.ts +7 -0
- package/src/pgo-components/lib/core/rtl/rtl.ts +3 -0
- package/src/pgo-components/lib/core/rtl/setRtl.ts +19 -0
- package/src/pgo-components/lib/drawerState.ts +3 -0
- package/src/pgo-components/lib/i18n/defaultLables.js +71 -0
- package/src/pgo-components/lib/i18n/i18nPlugin.js +52 -0
- package/src/pgo-components/lib/i18n/useI18n.js +35 -0
- package/src/pgo-components/lib/index.ts +38 -0
- package/src/pgo-components/pages/Component.vue +7 -0
- package/src/pgo-components/pages/ComponentRenderer.vue +85 -0
- package/src/pgo-components/pages/Home.vue +130 -0
- package/src/pgo-components/pages/ListView.vue +370 -0
- package/src/pgo-components/pages/Page1.vue +296 -0
- package/src/pgo-components/pages/_Page1.vue +180 -0
- package/src/pgo-components/plugins/SnackBar.vue +251 -0
- package/src/pgo-components/plugins/SnackBarContainer.vue +53 -0
- package/src/pgo-components/plugins/SnackBarPlugin.ts +136 -0
- package/src/pgo-components/plugins/theme-plugin.js +114 -0
- package/src/pgo-components/plugins/types.ts +46 -0
- package/src/pgo-components/plugins/useSnackBar.js +11 -0
- package/src/pgo-components/plugins/useSnackBar.ts +21 -0
- package/src/pgo-components/plugins/validation-plugin.js +11 -0
- package/src/pgo-components/services/Entry.json +813 -0
- package/src/pgo-components/services/axios.js +54 -0
- package/src/pgo-components/services/data.json +90 -0
- package/src/pgo-components/services/person.json +260 -0
- package/src/pgo-components/services/toast.ts +44 -0
- package/src/pgo-components/styles/global.css +234 -0
- package/src/pgo-components/styles/reset.css +96 -0
- package/src/pgo-components/styles/tokens.css +18 -0
- package/src/pgo-components/styles/utilities/border-radius.css +57 -0
- package/src/pgo-components/styles/utilities/borders.css +85 -0
- package/src/pgo-components/styles/utilities/colors.css +38 -0
- package/src/pgo-components/styles/utilities/cursor.css +19 -0
- package/src/pgo-components/styles/utilities/display.css +78 -0
- package/src/pgo-components/styles/utilities/elevation.css +33 -0
- package/src/pgo-components/styles/utilities/flex.css +403 -0
- package/src/pgo-components/styles/utilities/float.css +41 -0
- package/src/pgo-components/styles/utilities/hover.css +9 -0
- package/src/pgo-components/styles/utilities/index.css +18 -0
- package/src/pgo-components/styles/utilities/opacity.css +27 -0
- package/src/pgo-components/styles/utilities/overflow.css +26 -0
- package/src/pgo-components/styles/utilities/palette.css +515 -0
- package/src/pgo-components/styles/utilities/position.css +14 -0
- package/src/pgo-components/styles/utilities/sizing.css +70 -0
- package/src/pgo-components/styles/utilities/spacing.css +578 -0
- package/src/pgo-components/styles/utilities/transitions.css +58 -0
- package/src/pgo-components/styles/utilities/typography.css +91 -0
- package/src/pgo-components/styles/utilities/z-index.css +11 -0
- package/src/pgo-components/tokens/index.js +337 -0
- package/src/router/index.js +88 -0
- package/src/shims-vue.d.ts +14 -0
- package/src/validations/validationRules.js +50 -0
- package/tailwind.config.js +73 -0
- package/test.php +5 -0
- package/tsconfig.json +25 -0
- package/ui +31 -0
- package/ui.pgo.mv.conf +18 -0
- package/vite.config.js +42 -0
|
@@ -0,0 +1,849 @@
|
|
|
1
|
+
<template>
|
|
2
|
+
<div class="relative">
|
|
3
|
+
<!-- Scrim for temporary drawer -->
|
|
4
|
+
<div
|
|
5
|
+
v-if="isOpen && temporary"
|
|
6
|
+
class="fixed inset-0"
|
|
7
|
+
:class="scrimClass"
|
|
8
|
+
:style="[scrimStyle, scrimThemeStyles]"
|
|
9
|
+
@click="onScrimClick"
|
|
10
|
+
></div>
|
|
11
|
+
|
|
12
|
+
<!-- Drawer container -->
|
|
13
|
+
<nav
|
|
14
|
+
ref="navRef"
|
|
15
|
+
:class="[
|
|
16
|
+
zClass,
|
|
17
|
+
'transform transition-transform duration-200 ease-in-out',
|
|
18
|
+
containerBaseClasses,
|
|
19
|
+
locationClass,
|
|
20
|
+
railClass,
|
|
21
|
+
expandedClass,
|
|
22
|
+
color,
|
|
23
|
+
widthClass,
|
|
24
|
+
!isOpen && !permanent ? hiddenClass : 'translate-x-0',
|
|
25
|
+
props.location === 'right' ? 'rounded-l-lg' : 'rounded-r-lg'
|
|
26
|
+
]"
|
|
27
|
+
:style="[zStyle, themeStyles, topPadding ? { paddingTop: topPadding + 'px' } : undefined]"
|
|
28
|
+
:aria-hidden="!isOpen && !permanent"
|
|
29
|
+
@click="onMouseEnter"
|
|
30
|
+
|
|
31
|
+
>
|
|
32
|
+
<div class="flex h-full flex-col" :style="{ minHeight: '100vh' }">
|
|
33
|
+
<div class="flex items-center justify-between vts-px-4 vts-py-3">
|
|
34
|
+
<slot name="prepend" v-if="showPrepend">
|
|
35
|
+
<!-- Default prepend content: app title -->
|
|
36
|
+
<span class="font-semibold">Menu</span>
|
|
37
|
+
</slot>
|
|
38
|
+
<div class="flex items-center vts-ga-2">
|
|
39
|
+
<slot name="append"></slot>
|
|
40
|
+
<button v-if="!permanent && !temporary" @click="toggleOpen" :class="['vts-pa-2', hoverButtonStyles]" aria-label="Toggle drawer">
|
|
41
|
+
<!-- FIX: for better code -->
|
|
42
|
+
<HeroIcon v-if="props.location === 'left' && !globalRtl && props.rail && collapsed" name="chevron-right" size="20" />
|
|
43
|
+
<HeroIcon v-if="props.location === 'left' && !globalRtl && props.rail && !collapsed" name="chevron-left" size="20" />
|
|
44
|
+
|
|
45
|
+
<HeroIcon v-if="props.location === 'right' && globalRtl && props.rail && collapsed" name="chevron-left" size="20" />
|
|
46
|
+
<HeroIcon v-if="props.location === 'right' && globalRtl && props.rail && !collapsed" name="chevron-right" size="20" />
|
|
47
|
+
|
|
48
|
+
<!-- <HeroIcon :name="locationIcon" size="20" /> -->
|
|
49
|
+
</button>
|
|
50
|
+
<button v-if="temporary" @click="close" :class="['ml-2 rounded p-2', hoverButtonStyles]" aria-label="Close drawer">
|
|
51
|
+
<svg xmlns="http://www.w3.org/2000/svg" class="h-5 w-5" viewBox="0 0 20 20" fill="currentColor">
|
|
52
|
+
<path d="M6 6a1 1 0 100-2 1 1 0 000 2zm0 6a1 1 0 110-2 1 1 0 010 2zm0 6a1 1 0 110-2 1 1 0 010 2z" />
|
|
53
|
+
</svg>
|
|
54
|
+
</button>
|
|
55
|
+
</div>
|
|
56
|
+
</div>
|
|
57
|
+
|
|
58
|
+
<div class="flex-1 overflow-auto">
|
|
59
|
+
<!-- If data-driven items provided, render grouped items and support filtering & ordering -->
|
|
60
|
+
<div v-if="props.items">
|
|
61
|
+
<div v-if="props.filterable && !(props.rail && collapsed)" class="vts-px-3 vts-py-2">
|
|
62
|
+
<input v-model="filterQuery" :placeholder="props.searchPlaceholder" class="w-full vts-px-4 vts-py-2" :style="searchInputStyles" />
|
|
63
|
+
</div>
|
|
64
|
+
|
|
65
|
+
<div v-for="group in groupedItems" :key="group.group" class="vts-py-2">
|
|
66
|
+
<div
|
|
67
|
+
v-if="group.group"
|
|
68
|
+
:class="['group-header vts-px-3 vts-pt-3 vts-pb-1 text-lg uppercase flex items-center justify-between', groupHeaderStyles]"
|
|
69
|
+
role="button"
|
|
70
|
+
:tabindex="props.collapsibleGroups ? 0 : -1"
|
|
71
|
+
@click.prevent="toggleGroup(group.group)"
|
|
72
|
+
@keydown.enter.prevent="toggleGroup(group.group)"
|
|
73
|
+
@keydown.space.prevent="toggleGroup(group.group)"
|
|
74
|
+
:aria-expanded="isGroupOpen(group.group)"
|
|
75
|
+
>
|
|
76
|
+
<span class="flex items-center vts-ga-2">
|
|
77
|
+
<!-- render group parent element (link or button) when enabled -->
|
|
78
|
+
<template v-if="props.renderGroupParent">
|
|
79
|
+
<component
|
|
80
|
+
:is="props.groupParents && props.groupParents[group.group] && props.groupParents[group.group].href ? 'a' : 'button'"
|
|
81
|
+
:href="(props.groupParents && props.groupParents[group.group] && props.groupParents[group.group].href) || undefined"
|
|
82
|
+
class="group-parent flex items-center vts-ga-3 vts-px-0 vts-py-0 w-full text-left"
|
|
83
|
+
@click.stop="e => onGroupParentClick(e, group.group)"
|
|
84
|
+
@keydown="e => onGroupParentKey(e, group.group)"
|
|
85
|
+
>
|
|
86
|
+
<span
|
|
87
|
+
v-if="props.groupParents && props.groupParents[group.group] && props.groupParents[group.group].icon"
|
|
88
|
+
class="vts-mr-2"
|
|
89
|
+
>
|
|
90
|
+
<HeroIcon :name="props.groupParents[group.group].icon" size="18" />
|
|
91
|
+
</span>
|
|
92
|
+
<span v-else-if="props.groupIcons && props.groupIcons[group.group]" class="vts-mr-2">
|
|
93
|
+
<HeroIcon :name="props.groupIcons[group.group]" size="18" />
|
|
94
|
+
</span>
|
|
95
|
+
<span v-if="!props.rail || !collapsed" :class="['text-lg uppercase', groupHeaderStyles]">
|
|
96
|
+
{{
|
|
97
|
+
(props.groupParents && props.groupParents[group.group] && props.groupParents[group.group].label) || group.group
|
|
98
|
+
}}
|
|
99
|
+
</span>
|
|
100
|
+
</component>
|
|
101
|
+
</template>
|
|
102
|
+
<template v-else>
|
|
103
|
+
<span v-if="props.groupIcons && props.groupIcons[group.group]" class="vts-mr-2">
|
|
104
|
+
<HeroIcon :name="props.groupIcons[group.group]" size="18" />
|
|
105
|
+
</span>
|
|
106
|
+
<span v-if="!props.rail || !collapsed" :class="['text-lg uppercase', groupHeaderStyles]">
|
|
107
|
+
{{ group.group }}
|
|
108
|
+
</span>
|
|
109
|
+
</template>
|
|
110
|
+
</span>
|
|
111
|
+
<button
|
|
112
|
+
v-if="props.collapsibleGroups && (!props.rail || !collapsed)"
|
|
113
|
+
@click.stop.prevent="toggleGroup(group.group)"
|
|
114
|
+
@keydown.enter.stop.prevent="toggleGroup(group.group)"
|
|
115
|
+
@keydown.space.stop.prevent="toggleGroup(group.group)"
|
|
116
|
+
:class="['vts-px-2 vts-py-1 rounded focus:outline-none', groupHeaderStyles]"
|
|
117
|
+
:style="{ opacity: 0.7 }"
|
|
118
|
+
:aria-expanded="isGroupOpen(group.group)"
|
|
119
|
+
>
|
|
120
|
+
<HeroIcon name="chevron-down" size="16" :class="{ 'rotate-180': isGroupOpen(group.group) }" />
|
|
121
|
+
</button>
|
|
122
|
+
</div>
|
|
123
|
+
<ul class="vts-py-1" v-show="isGroupOpen(group.group)">
|
|
124
|
+
<NavDrawerItem
|
|
125
|
+
v-for="item in group.items"
|
|
126
|
+
:key="item.id"
|
|
127
|
+
:id="item.id"
|
|
128
|
+
:href="item.href"
|
|
129
|
+
:draggable="props.draggable && !item.disabled"
|
|
130
|
+
:dragging="String(draggingId) === String(item.id)"
|
|
131
|
+
:is-drag-over="String(dragOverId) === String(item.id)"
|
|
132
|
+
:drag-pos="dragOverPos"
|
|
133
|
+
@dragstart="handleDragStart"
|
|
134
|
+
@dragover="handleDragOver"
|
|
135
|
+
@dragleave="
|
|
136
|
+
e => {
|
|
137
|
+
if (String(dragOverId) === String(item.id)) {
|
|
138
|
+
dragOverId = null
|
|
139
|
+
dragOverPos = null
|
|
140
|
+
}
|
|
141
|
+
}
|
|
142
|
+
"
|
|
143
|
+
@drop="handleDrop"
|
|
144
|
+
>
|
|
145
|
+
<HeroIcon v-if="item.icon" :name="item.icon" size="20" class="rail-icon mr-2" />
|
|
146
|
+
<span class="rail-label">{{ item.label }}</span>
|
|
147
|
+
</NavDrawerItem>
|
|
148
|
+
</ul>
|
|
149
|
+
</div>
|
|
150
|
+
</div>
|
|
151
|
+
<div v-else>
|
|
152
|
+
<slot />
|
|
153
|
+
</div>
|
|
154
|
+
</div>
|
|
155
|
+
|
|
156
|
+
<div class="vts-px-4 vts-py-3">
|
|
157
|
+
<slot name="footer"></slot>
|
|
158
|
+
</div>
|
|
159
|
+
</div>
|
|
160
|
+
</nav>
|
|
161
|
+
</div>
|
|
162
|
+
</template>
|
|
163
|
+
|
|
164
|
+
<script setup lang="ts">
|
|
165
|
+
import { ref, watch, computed, onMounted, onBeforeUnmount, onUpdated, inject, toRaw } from 'vue'
|
|
166
|
+
import { nextTick } from 'vue'
|
|
167
|
+
import type { PropType } from 'vue'
|
|
168
|
+
import HeroIcon from './HeroIcon.vue'
|
|
169
|
+
import NavDrawerItem from './NavDrawerItem.vue'
|
|
170
|
+
import { globalRtl } from '../../pgo-components/lib/core/rtl/rtl'
|
|
171
|
+
|
|
172
|
+
const themeStyles = computed(() => ({
|
|
173
|
+
backgroundColor: 'var(--vts-color-surface)',
|
|
174
|
+
color: 'var(--vts-color-text)',
|
|
175
|
+
borderRight: '1px solid var(--vts-color-border)',
|
|
176
|
+
boxShadow: 'var(--vts-elevation-2)'
|
|
177
|
+
}))
|
|
178
|
+
|
|
179
|
+
const scrimThemeStyles = computed(() => ({
|
|
180
|
+
backgroundColor: 'var(--vts-color-overlay)'
|
|
181
|
+
}))
|
|
182
|
+
|
|
183
|
+
const searchInputStyles = computed(() => ({
|
|
184
|
+
backgroundColor: 'var(--vts-color-surface)',
|
|
185
|
+
color: 'var(--vts-color-text)',
|
|
186
|
+
border: '1px solid var(--vts-color-border)',
|
|
187
|
+
borderRadius: 'var(--vts-radius-sm)'
|
|
188
|
+
}))
|
|
189
|
+
|
|
190
|
+
const hoverButtonStyles = 'hover:bg-[var(--vts-color-surface)]'
|
|
191
|
+
const groupHeaderStyles = 'text-[var(--vts-color-textSecondary)]'
|
|
192
|
+
|
|
193
|
+
const props = defineProps({
|
|
194
|
+
id: {
|
|
195
|
+
type: String,
|
|
196
|
+
default: () => `drawer-${Math.random().toString(36).slice(2, 9)}`
|
|
197
|
+
},
|
|
198
|
+
modelValue: { type: Boolean, default: false },
|
|
199
|
+
permanent: { type: Boolean, default: true },
|
|
200
|
+
temporary: { type: Boolean, default: false },
|
|
201
|
+
persistent: { type: Boolean, default: false },
|
|
202
|
+
rail: { type: Boolean, default: false },
|
|
203
|
+
hoverExpand: { type: Boolean, default: false },
|
|
204
|
+
isStuck: { type: Boolean, default: false },
|
|
205
|
+
belowAppbar: { type: Boolean, default: false },
|
|
206
|
+
location: { type: String as PropType<'left' | 'right'>, default: 'left' },
|
|
207
|
+
// color: { type: String, default: 'bg-white dark:bg-gray-900' },
|
|
208
|
+
color: { type: String },
|
|
209
|
+
width: { type: [String, Number], default: 'w-64' },
|
|
210
|
+
railWidth: { type: [String, Number], default: 'w-16' },
|
|
211
|
+
closeOnScrimClick: { type: Boolean, default: true },
|
|
212
|
+
expandOnHover: { type: Boolean, default: false },
|
|
213
|
+
overlayAppBar: { type: Boolean, default: true }, // if false, drawer will appear under app bar
|
|
214
|
+
zIndex: { type: [String, Number], default: undefined },
|
|
215
|
+
appBarOffset: { type: Boolean, default: false },
|
|
216
|
+
appBarHeight: { type: [String, Number], default: undefined },
|
|
217
|
+
// data-driven items
|
|
218
|
+
items: {
|
|
219
|
+
type: Array as PropType<Array<Record<string, any>>>,
|
|
220
|
+
default: undefined
|
|
221
|
+
},
|
|
222
|
+
filterable: { type: Boolean, default: false },
|
|
223
|
+
searchPlaceholder: { type: String, default: 'Search' },
|
|
224
|
+
draggable: { type: Boolean, default: false },
|
|
225
|
+
groupBy: {
|
|
226
|
+
type: [String, Function] as PropType<string | ((item: any) => string) | undefined>,
|
|
227
|
+
default: undefined
|
|
228
|
+
},
|
|
229
|
+
allowCrossGroupReorder: { type: Boolean, default: false },
|
|
230
|
+
collapsibleGroups: { type: Boolean, default: true },
|
|
231
|
+
groupDefaultOpen: { type: Boolean, default: true },
|
|
232
|
+
groupIcons: {
|
|
233
|
+
type: Object as PropType<Record<string, string> | undefined>,
|
|
234
|
+
default: undefined
|
|
235
|
+
},
|
|
236
|
+
renderGroupParent: { type: Boolean, default: false },
|
|
237
|
+
groupParents: {
|
|
238
|
+
type: Object as PropType<Record<string, { label?: string; href?: string; icon?: string }>>,
|
|
239
|
+
default: undefined
|
|
240
|
+
}
|
|
241
|
+
})
|
|
242
|
+
|
|
243
|
+
const locationIcon = computed(() => {
|
|
244
|
+
const isLeft = props.location === 'left'
|
|
245
|
+
|
|
246
|
+
// Case 1: Rail + closed → always show expand icon
|
|
247
|
+
if (props.rail && !props.modelValue && !hoverOpen.value) {
|
|
248
|
+
return isLeft ? 'chevron-right' : 'chevron-left'
|
|
249
|
+
}
|
|
250
|
+
|
|
251
|
+
// Case 2: Rail + open but NOT hover-expanded → still expand icon
|
|
252
|
+
if (props.rail && props.modelValue && !hoverOpen.value) {
|
|
253
|
+
return isLeft ? 'chevron-right' : 'chevron-left'
|
|
254
|
+
}
|
|
255
|
+
|
|
256
|
+
// Case 3: Expanded rail OR normal drawer → collapse icon
|
|
257
|
+
return isLeft ? 'chevron-left' : 'chevron-right'
|
|
258
|
+
})
|
|
259
|
+
|
|
260
|
+
const emit = defineEmits(['update:modelValue', 'open', 'close', 'update:items', 'reorder', 'group-toggle'])
|
|
261
|
+
|
|
262
|
+
const internalOpen = ref(Boolean(props.modelValue))
|
|
263
|
+
const navRef = ref<HTMLElement | null>(null)
|
|
264
|
+
// local copy of items so we can reorder and filter
|
|
265
|
+
const localItems = ref<Array<Record<string, any>>>(props.items ? [...props.items] : [])
|
|
266
|
+
const filterQuery = ref('')
|
|
267
|
+
// sync prop items with local items
|
|
268
|
+
watch(
|
|
269
|
+
() => props.items,
|
|
270
|
+
i => {
|
|
271
|
+
localItems.value = props.items ? [...props.items] : []
|
|
272
|
+
}
|
|
273
|
+
)
|
|
274
|
+
|
|
275
|
+
watch(
|
|
276
|
+
() => props.overlayAppBar,
|
|
277
|
+
v => {
|
|
278
|
+
computeTopPadding()
|
|
279
|
+
}
|
|
280
|
+
)
|
|
281
|
+
|
|
282
|
+
// derived filtered items
|
|
283
|
+
const filteredItems = computed(() => {
|
|
284
|
+
if (!props.items) return []
|
|
285
|
+
if (!filterQuery.value || filterQuery.value.trim().length === 0) return localItems.value
|
|
286
|
+
const q = filterQuery.value.toLowerCase()
|
|
287
|
+
return localItems.value.filter(it => {
|
|
288
|
+
const label = String(it.label || it.name || '')
|
|
289
|
+
return label.toLowerCase().includes(q) || (it.tags && String(it.tags).toLowerCase().includes(q))
|
|
290
|
+
})
|
|
291
|
+
})
|
|
292
|
+
|
|
293
|
+
// compute groups
|
|
294
|
+
const groupedItems = computed(() => {
|
|
295
|
+
if (!props.items) return []
|
|
296
|
+
if (!props.groupBy) return [{ group: null, items: filteredItems.value }]
|
|
297
|
+
const groups: Record<string, Array<any>> = {}
|
|
298
|
+
const getter = typeof props.groupBy === 'function' ? props.groupBy : (it: any) => it[props.groupBy as string]
|
|
299
|
+
filteredItems.value.forEach(it => {
|
|
300
|
+
const key = getter(it) || 'Ungrouped'
|
|
301
|
+
if (!groups[key]) groups[key] = []
|
|
302
|
+
groups[key].push(it)
|
|
303
|
+
})
|
|
304
|
+
return Object.keys(groups).map(g => ({ group: g, items: groups[g] }))
|
|
305
|
+
})
|
|
306
|
+
|
|
307
|
+
// Track open/closed state for groups
|
|
308
|
+
const groupOpenMap = ref<Record<string, boolean>>({})
|
|
309
|
+
|
|
310
|
+
// init or sync group states when groupedItems change
|
|
311
|
+
watch(groupedItems, groups => {
|
|
312
|
+
groups.forEach(g => {
|
|
313
|
+
const key = String(g.group || 'Ungrouped')
|
|
314
|
+
if (groupOpenMap.value[key] === undefined) {
|
|
315
|
+
groupOpenMap.value[key] = Boolean(props.groupDefaultOpen)
|
|
316
|
+
}
|
|
317
|
+
})
|
|
318
|
+
})
|
|
319
|
+
|
|
320
|
+
function isGroupOpen(key: string | null) {
|
|
321
|
+
if (!props.collapsibleGroups) return true
|
|
322
|
+
const k = String(key || 'Ungrouped')
|
|
323
|
+
return Boolean(groupOpenMap.value[k])
|
|
324
|
+
}
|
|
325
|
+
|
|
326
|
+
function toggleGroup(key: string | null) {
|
|
327
|
+
if (!props.collapsibleGroups) return
|
|
328
|
+
const k = String(key || 'Ungrouped')
|
|
329
|
+
groupOpenMap.value[k] = !Boolean(groupOpenMap.value[k])
|
|
330
|
+
emit('group-toggle', { group: key, open: groupOpenMap.value[k] })
|
|
331
|
+
}
|
|
332
|
+
|
|
333
|
+
function onGroupParentClick(e: Event, key: string | null) {
|
|
334
|
+
const parent = props.groupParents && key ? props.groupParents[key] : undefined
|
|
335
|
+
if (!parent || !parent.href) {
|
|
336
|
+
e.preventDefault()
|
|
337
|
+
toggleGroup(key)
|
|
338
|
+
}
|
|
339
|
+
}
|
|
340
|
+
|
|
341
|
+
function onGroupParentKey(e: KeyboardEvent, key: string | null) {
|
|
342
|
+
// Only toggle when Enter/Space and the group parent has no href (acting as a button)
|
|
343
|
+
if (!key) return
|
|
344
|
+
const parent = props.groupParents && key ? props.groupParents[key] : undefined
|
|
345
|
+
if (!(e.key === 'Enter' || e.key === ' ')) return
|
|
346
|
+
if (!parent || !parent.href) {
|
|
347
|
+
e.preventDefault()
|
|
348
|
+
e.stopPropagation()
|
|
349
|
+
toggleGroup(key)
|
|
350
|
+
}
|
|
351
|
+
}
|
|
352
|
+
|
|
353
|
+
// Normalize list items: wrap plain text nodes in `.rail-label` spans, mark icons with `.rail-icon` for consistent styling.
|
|
354
|
+
function normalizeNavItems() {
|
|
355
|
+
const nav = navRef.value
|
|
356
|
+
if (!nav) return
|
|
357
|
+
// If items are in a ul, normalize li elements. Otherwise, normalize anchors and buttons direct under nav
|
|
358
|
+
const items = nav.querySelectorAll('li')
|
|
359
|
+
items.forEach(li => {
|
|
360
|
+
// Add rail-icon class to first svg or component
|
|
361
|
+
const firstSvg = li.querySelector('svg') as SVGElement | null
|
|
362
|
+
if (firstSvg && !firstSvg.classList.contains('rail-icon')) {
|
|
363
|
+
firstSvg.classList.add('rail-icon')
|
|
364
|
+
}
|
|
365
|
+
|
|
366
|
+
// Wrap text nodes in .rail-label spans (if not already wrapped)
|
|
367
|
+
const childNodes = Array.from(li.childNodes)
|
|
368
|
+
childNodes.forEach(node => {
|
|
369
|
+
if (node.nodeType === Node.TEXT_NODE) {
|
|
370
|
+
const text = node.textContent?.trim()
|
|
371
|
+
if (text && text.length) {
|
|
372
|
+
// create span. Avoid double-wrapping by checking parent
|
|
373
|
+
const span = document.createElement('span')
|
|
374
|
+
span.className = 'rail-label'
|
|
375
|
+
span.textContent = text
|
|
376
|
+
li.replaceChild(span, node)
|
|
377
|
+
}
|
|
378
|
+
} else if (node.nodeType === Node.ELEMENT_NODE) {
|
|
379
|
+
const el = node as HTMLElement
|
|
380
|
+
// If element already is a label or icon, skip
|
|
381
|
+
if (el.classList.contains('rail-label') || el.classList.contains('rail-icon')) return
|
|
382
|
+
// If element contains text nodes, wrap them
|
|
383
|
+
const subChildren = Array.from(el.childNodes)
|
|
384
|
+
subChildren.forEach(sub => {
|
|
385
|
+
if (sub.nodeType === Node.TEXT_NODE) {
|
|
386
|
+
const txt = sub.textContent?.trim()
|
|
387
|
+
if (txt && txt.length) {
|
|
388
|
+
const span = document.createElement('span')
|
|
389
|
+
span.className = 'rail-label'
|
|
390
|
+
span.textContent = txt
|
|
391
|
+
el.replaceChild(span, sub)
|
|
392
|
+
}
|
|
393
|
+
}
|
|
394
|
+
})
|
|
395
|
+
}
|
|
396
|
+
})
|
|
397
|
+
})
|
|
398
|
+
|
|
399
|
+
// Normalize anchors directly under nav (wrap anchors/buttons directly under nav in an li)
|
|
400
|
+
const directAnchors = nav.querySelectorAll(':scope > a, :scope > button')
|
|
401
|
+
directAnchors.forEach(a => {
|
|
402
|
+
// ensure role and class
|
|
403
|
+
if (!a.classList.contains('nav-drawer-link')) a.classList.add('nav-drawer-link')
|
|
404
|
+
const existingParent = a.parentElement
|
|
405
|
+
if (existingParent && existingParent.tagName.toLowerCase() !== 'li') {
|
|
406
|
+
const li = document.createElement('li')
|
|
407
|
+
li.className = 'nav-drawer-item'
|
|
408
|
+
existingParent.replaceChild(li, a)
|
|
409
|
+
li.appendChild(a)
|
|
410
|
+
}
|
|
411
|
+
})
|
|
412
|
+
}
|
|
413
|
+
|
|
414
|
+
// Drag & drop helpers for data-driven items
|
|
415
|
+
let draggingId: string | number | null = null
|
|
416
|
+
let dragOverId: string | number | null = null
|
|
417
|
+
let dragOverPos: 'before' | 'after' | null = null
|
|
418
|
+
|
|
419
|
+
function findItemIndexById(id: string | number | null, arr = localItems.value) {
|
|
420
|
+
if (id === null || id === undefined) return -1
|
|
421
|
+
return arr.findIndex(it => String(it.id) === String(id))
|
|
422
|
+
}
|
|
423
|
+
|
|
424
|
+
function handleDragStart(ctx: { id: string | number }) {
|
|
425
|
+
draggingId = ctx.id
|
|
426
|
+
}
|
|
427
|
+
|
|
428
|
+
function handleDragOver(ctx: { id: string | number; event?: DragEvent }) {
|
|
429
|
+
dragOverId = ctx.id
|
|
430
|
+
// compute position relative to target element using event
|
|
431
|
+
if (ctx.event && ctx.event.clientY !== undefined) {
|
|
432
|
+
const el = document.querySelector(`[data-drawer-item-id='${ctx.id}']`) as HTMLElement | null
|
|
433
|
+
if (el) {
|
|
434
|
+
const rect = el.getBoundingClientRect()
|
|
435
|
+
const mid = rect.top + rect.height / 2
|
|
436
|
+
dragOverPos = ctx.event.clientY < mid ? 'before' : 'after'
|
|
437
|
+
// set CSS classes by forcing a reactivity value
|
|
438
|
+
}
|
|
439
|
+
} else {
|
|
440
|
+
dragOverPos = 'after'
|
|
441
|
+
}
|
|
442
|
+
}
|
|
443
|
+
|
|
444
|
+
function handleDrop(ctx: { id: string | number; sourceId?: string | number; event?: DragEvent }) {
|
|
445
|
+
// if no items are passed, nothing to do
|
|
446
|
+
if (!props.items || !props.draggable) return
|
|
447
|
+
const sourceId = ctx.sourceId !== undefined ? ctx.sourceId : draggingId
|
|
448
|
+
const targetId = ctx.id
|
|
449
|
+
if (!sourceId || !targetId) return
|
|
450
|
+
if (String(sourceId) === String(targetId)) return
|
|
451
|
+
|
|
452
|
+
const fromIndex = findItemIndexById(sourceId)
|
|
453
|
+
const toIndex = findItemIndexById(targetId)
|
|
454
|
+
if (fromIndex === -1 || toIndex === -1) return
|
|
455
|
+
|
|
456
|
+
// move
|
|
457
|
+
const copy = [...localItems.value]
|
|
458
|
+
const [moved] = copy.splice(fromIndex, 1)
|
|
459
|
+
copy.splice(toIndex, 0, moved)
|
|
460
|
+
localItems.value = copy
|
|
461
|
+
emit('update:items', copy)
|
|
462
|
+
emit('reorder', { items: copy })
|
|
463
|
+
// reset drag id & pos
|
|
464
|
+
draggingId = null
|
|
465
|
+
dragOverId = null
|
|
466
|
+
dragOverPos = null
|
|
467
|
+
}
|
|
468
|
+
|
|
469
|
+
function handleOutsideClick(e: MouseEvent) {
|
|
470
|
+
if (!navRef.value) return
|
|
471
|
+
if (!navRef.value.contains(e.target as Node)) {
|
|
472
|
+
onMouseLeave() // 👈 Trigger your function
|
|
473
|
+
}
|
|
474
|
+
}
|
|
475
|
+
|
|
476
|
+
watch(
|
|
477
|
+
() => props.modelValue,
|
|
478
|
+
v => {
|
|
479
|
+
internalOpen.value = Boolean(v)
|
|
480
|
+
updateLayout()
|
|
481
|
+
}
|
|
482
|
+
)
|
|
483
|
+
|
|
484
|
+
watch(
|
|
485
|
+
() => props.permanent,
|
|
486
|
+
v => {
|
|
487
|
+
updateLayout()
|
|
488
|
+
}
|
|
489
|
+
)
|
|
490
|
+
|
|
491
|
+
watch(internalOpen, v => {
|
|
492
|
+
emit('update:modelValue', v)
|
|
493
|
+
if (v) emit('open')
|
|
494
|
+
else emit('close')
|
|
495
|
+
})
|
|
496
|
+
|
|
497
|
+
// compute open: if permanent -> always open
|
|
498
|
+
const isOpen = computed(() => {
|
|
499
|
+
if (props.permanent) return true
|
|
500
|
+
return internalOpen.value
|
|
501
|
+
})
|
|
502
|
+
|
|
503
|
+
const toggleOpen = () => {
|
|
504
|
+
if (props.permanent) return
|
|
505
|
+
internalOpen.value = !internalOpen.value
|
|
506
|
+
}
|
|
507
|
+
|
|
508
|
+
const open = () => {
|
|
509
|
+
if (props.permanent) return
|
|
510
|
+
internalOpen.value = true
|
|
511
|
+
}
|
|
512
|
+
|
|
513
|
+
const close = () => {
|
|
514
|
+
if (props.permanent) return
|
|
515
|
+
internalOpen.value = false
|
|
516
|
+
}
|
|
517
|
+
|
|
518
|
+
const onScrimClick = () => {
|
|
519
|
+
if (props.closeOnScrimClick) close()
|
|
520
|
+
}
|
|
521
|
+
|
|
522
|
+
// keyboard ESC to close for temporary/persistent
|
|
523
|
+
const onKey = (e: KeyboardEvent) => {
|
|
524
|
+
if (e.key === 'Escape' && (props.temporary || props.persistent)) {
|
|
525
|
+
close()
|
|
526
|
+
}
|
|
527
|
+
}
|
|
528
|
+
|
|
529
|
+
onMounted(() => {
|
|
530
|
+
document.addEventListener('keydown', onKey)
|
|
531
|
+
document.addEventListener('click', handleOutsideClick)
|
|
532
|
+
// run normalization so rail mode works with existing markup
|
|
533
|
+
normalizeNavItems()
|
|
534
|
+
})
|
|
535
|
+
|
|
536
|
+
onUpdated(() => {
|
|
537
|
+
// re-run normalization when slot content or children update
|
|
538
|
+
normalizeNavItems()
|
|
539
|
+
})
|
|
540
|
+
|
|
541
|
+
onBeforeUnmount(() => {
|
|
542
|
+
document.removeEventListener('keydown', onKey)
|
|
543
|
+
document.removeEventListener('click', handleOutsideClick)
|
|
544
|
+
window.removeEventListener('resize', onWindowResize)
|
|
545
|
+
})
|
|
546
|
+
|
|
547
|
+
// Focus trap for temporary/persistent mode when open
|
|
548
|
+
let focusTrapHandler: ((e: KeyboardEvent) => void) | null = null
|
|
549
|
+
function getFocusable(el: HTMLElement) {
|
|
550
|
+
const selectors = 'a[href], button:not([disabled]), textarea, input, select, [tabindex]:not([tabindex="-1"])'
|
|
551
|
+
return Array.from(el.querySelectorAll(selectors)) as HTMLElement[]
|
|
552
|
+
}
|
|
553
|
+
|
|
554
|
+
watch(isOpen, open => {
|
|
555
|
+
if (open && (props.temporary || props.persistent)) {
|
|
556
|
+
const nav = navRef.value as HTMLElement | null
|
|
557
|
+
if (!nav) return
|
|
558
|
+
const focusables = getFocusable(nav)
|
|
559
|
+
if (focusables.length) {
|
|
560
|
+
focusables[0].focus()
|
|
561
|
+
} else {
|
|
562
|
+
nav.setAttribute('tabindex', '-1')
|
|
563
|
+
nav.focus()
|
|
564
|
+
}
|
|
565
|
+
|
|
566
|
+
focusTrapHandler = (e: KeyboardEvent) => {
|
|
567
|
+
if (e.key !== 'Tab') return
|
|
568
|
+
const focusEls = getFocusable(nav)
|
|
569
|
+
if (!focusEls.length) return
|
|
570
|
+
const first = focusEls[0]
|
|
571
|
+
const last = focusEls[focusEls.length - 1]
|
|
572
|
+
if (e.shiftKey && document.activeElement === first) {
|
|
573
|
+
e.preventDefault()
|
|
574
|
+
last.focus()
|
|
575
|
+
} else if (!e.shiftKey && document.activeElement === last) {
|
|
576
|
+
e.preventDefault()
|
|
577
|
+
first.focus()
|
|
578
|
+
}
|
|
579
|
+
}
|
|
580
|
+
document.addEventListener('keydown', focusTrapHandler)
|
|
581
|
+
} else {
|
|
582
|
+
if (focusTrapHandler) {
|
|
583
|
+
document.removeEventListener('keydown', focusTrapHandler)
|
|
584
|
+
focusTrapHandler = null
|
|
585
|
+
}
|
|
586
|
+
}
|
|
587
|
+
// ensure nav items normalized on open state change (e.g., rail toggled)
|
|
588
|
+
normalizeNavItems()
|
|
589
|
+
})
|
|
590
|
+
|
|
591
|
+
// hover expand behavior
|
|
592
|
+
const hoverOpen = ref(false)
|
|
593
|
+
|
|
594
|
+
const allowHoverExpand = computed(() => {
|
|
595
|
+
// In rail mode, expand on hover is always enabled (UX requirement)
|
|
596
|
+
if (props.rail) return true
|
|
597
|
+
return Boolean(props.hoverExpand || props.expandOnHover)
|
|
598
|
+
})
|
|
599
|
+
|
|
600
|
+
let hoverTimeout: number | undefined
|
|
601
|
+
|
|
602
|
+
const onMouseEnter = () => {
|
|
603
|
+
if (allowHoverExpand.value) {
|
|
604
|
+
hoverTimeout && window.clearTimeout(hoverTimeout)
|
|
605
|
+
hoverOpen.value = true
|
|
606
|
+
}
|
|
607
|
+
}
|
|
608
|
+
|
|
609
|
+
const onMouseLeave = () => {
|
|
610
|
+
if (allowHoverExpand.value) {
|
|
611
|
+
hoverTimeout = window.setTimeout(() => {
|
|
612
|
+
hoverOpen.value = false
|
|
613
|
+
}, 150)
|
|
614
|
+
}
|
|
615
|
+
}
|
|
616
|
+
|
|
617
|
+
const collapsed = computed(() => props.rail && !hoverOpen.value)
|
|
618
|
+
|
|
619
|
+
// classes
|
|
620
|
+
const locationClass = computed(() => (props.location === 'right' ? 'right-0' : 'left-0'))
|
|
621
|
+
const widthClass = computed(() => {
|
|
622
|
+
if (collapsed.value) return typeof props.railWidth === 'number' ? `w-[${props.railWidth}px]` : String(props.railWidth)
|
|
623
|
+
return typeof props.width === 'number' ? `w-[${props.width}px]` : String(props.width)
|
|
624
|
+
})
|
|
625
|
+
|
|
626
|
+
const containerBaseClasses = computed(() => {
|
|
627
|
+
const base = ['fixed', 'h-full', 'overflow-hidden', 'flex', 'flex-col']
|
|
628
|
+
if (props.isStuck) base.push('top-0')
|
|
629
|
+
if (props.location === 'right') base.push('right-0')
|
|
630
|
+
else base.push('left-0')
|
|
631
|
+
if (props.rail) base.push('transition-all')
|
|
632
|
+
watch(
|
|
633
|
+
() => props.rail,
|
|
634
|
+
v => {
|
|
635
|
+
normalizeNavItems()
|
|
636
|
+
}
|
|
637
|
+
)
|
|
638
|
+
return base.join(' ')
|
|
639
|
+
})
|
|
640
|
+
|
|
641
|
+
const railClass = computed(() => (props.rail ? 'drawer-rail' : ''))
|
|
642
|
+
const expandedClass = computed(() => (props.rail && !collapsed.value ? 'drawer-expanded' : ''))
|
|
643
|
+
const showPrepend = computed(() => !(props.rail && collapsed.value))
|
|
644
|
+
|
|
645
|
+
const hiddenClass = computed(() => {
|
|
646
|
+
if (props.location === 'right') return 'translate-x-full'
|
|
647
|
+
return '-translate-x-full'
|
|
648
|
+
})
|
|
649
|
+
|
|
650
|
+
// z-index control for drawer and scrim
|
|
651
|
+
const zClass = computed(() => {
|
|
652
|
+
if (props.zIndex !== undefined && typeof props.zIndex === 'string') {
|
|
653
|
+
// allow passing 'z-40' style classes
|
|
654
|
+
if ((props.zIndex as string).startsWith('z-')) return props.zIndex as string
|
|
655
|
+
}
|
|
656
|
+
return props.overlayAppBar ? 'z-50' : 'z-10'
|
|
657
|
+
})
|
|
658
|
+
|
|
659
|
+
const zStyle = computed(() => {
|
|
660
|
+
if (props.zIndex !== undefined && typeof props.zIndex === 'number') {
|
|
661
|
+
return { zIndex: props.zIndex }
|
|
662
|
+
}
|
|
663
|
+
return undefined
|
|
664
|
+
})
|
|
665
|
+
|
|
666
|
+
const scrimStyle = computed(() => {
|
|
667
|
+
if (props.zIndex !== undefined && typeof props.zIndex === 'number') {
|
|
668
|
+
return { zIndex: (props.zIndex as number) - 1 }
|
|
669
|
+
}
|
|
670
|
+
return undefined
|
|
671
|
+
})
|
|
672
|
+
|
|
673
|
+
// Calculate top padding so content appears below app bar if drawer sits behind it
|
|
674
|
+
const topPadding = ref(0)
|
|
675
|
+
const computeTopPadding = () => {
|
|
676
|
+
if (!props.appBarOffset && props.overlayAppBar) {
|
|
677
|
+
topPadding.value = 0
|
|
678
|
+
// console.log('No app bar offset, top padding set to 0')
|
|
679
|
+
return
|
|
680
|
+
}
|
|
681
|
+
if (props.appBarHeight !== undefined && props.appBarHeight !== null) {
|
|
682
|
+
if (typeof props.appBarHeight === 'number') topPadding.value = props.appBarHeight as number
|
|
683
|
+
else if (typeof props.appBarHeight === 'string') {
|
|
684
|
+
const px = Number(String(props.appBarHeight).replace(/px$/i, ''))
|
|
685
|
+
topPadding.value = Number.isFinite(px) ? px : 0
|
|
686
|
+
}
|
|
687
|
+
console.log('Using provided appBarHeight for top padding:', topPadding.value)
|
|
688
|
+
return
|
|
689
|
+
}
|
|
690
|
+
// detect appbar element
|
|
691
|
+
if (typeof document !== 'undefined') {
|
|
692
|
+
const el = document.querySelector('[data-component="appbar"]') as HTMLElement | null
|
|
693
|
+
if (!el) {
|
|
694
|
+
topPadding.value = 0
|
|
695
|
+
return
|
|
696
|
+
}
|
|
697
|
+
const rect = el.getBoundingClientRect()
|
|
698
|
+
topPadding.value = Math.round(rect.height)
|
|
699
|
+
console.log('Computed top padding for drawer:', topPadding.value)
|
|
700
|
+
}
|
|
701
|
+
}
|
|
702
|
+
|
|
703
|
+
// compute initial padding
|
|
704
|
+
computeTopPadding()
|
|
705
|
+
// recompute on resize
|
|
706
|
+
let resizeTimer: number | undefined
|
|
707
|
+
const onWindowResize = () => {
|
|
708
|
+
resizeTimer && window.clearTimeout(resizeTimer)
|
|
709
|
+
resizeTimer = window.setTimeout(() => {
|
|
710
|
+
computeTopPadding()
|
|
711
|
+
}, 100)
|
|
712
|
+
}
|
|
713
|
+
onMounted(() => {
|
|
714
|
+
window.addEventListener('resize', onWindowResize)
|
|
715
|
+
})
|
|
716
|
+
onBeforeUnmount(() => {
|
|
717
|
+
window.removeEventListener('resize', onWindowResize)
|
|
718
|
+
})
|
|
719
|
+
|
|
720
|
+
const scrimClass = computed(() => {
|
|
721
|
+
if (props.zIndex !== undefined && typeof props.zIndex === 'number') {
|
|
722
|
+
// scrim will be one less than drawer z-index — but since we're using inline style, we won't set class
|
|
723
|
+
return ''
|
|
724
|
+
}
|
|
725
|
+
return props.overlayAppBar ? 'z-40' : 'z-0'
|
|
726
|
+
})
|
|
727
|
+
|
|
728
|
+
// always allow overlay toggling for temporary models
|
|
729
|
+
|
|
730
|
+
const layout = inject('layout', null)
|
|
731
|
+
// const horizontalOffset = inject("horizontalOffset", ref(0));
|
|
732
|
+
const el = ref<HTMLElement | null>(null)
|
|
733
|
+
const w = ref(0)
|
|
734
|
+
|
|
735
|
+
function updateLayout() {
|
|
736
|
+
if (!el.value) return
|
|
737
|
+
w.value = el.value.getBoundingClientRect().width
|
|
738
|
+
|
|
739
|
+
if (layout && !props.rail) {
|
|
740
|
+
layout.register('drawer', props.id, {
|
|
741
|
+
mode: props.modelValue || props.permanent ? 'permanent' : 'rail',
|
|
742
|
+
right: props.location === 'right' ? true : false,
|
|
743
|
+
width: w.value
|
|
744
|
+
})
|
|
745
|
+
}
|
|
746
|
+
}
|
|
747
|
+
|
|
748
|
+
onMounted(() => {
|
|
749
|
+
el.value = navRef.value
|
|
750
|
+
updateLayout()
|
|
751
|
+
|
|
752
|
+
const ro = new ResizeObserver(updateLayout)
|
|
753
|
+
if (el.value) ro.observe(el.value)
|
|
754
|
+
|
|
755
|
+
onBeforeUnmount(() => {
|
|
756
|
+
ro.disconnect()
|
|
757
|
+
if (layout) {
|
|
758
|
+
layout.unregister('drawer', props.id)
|
|
759
|
+
}
|
|
760
|
+
})
|
|
761
|
+
})
|
|
762
|
+
</script>
|
|
763
|
+
|
|
764
|
+
<style scoped>
|
|
765
|
+
/* small helpers */
|
|
766
|
+
.w-rail {
|
|
767
|
+
width: var(--rail-width, 4rem);
|
|
768
|
+
}
|
|
769
|
+
|
|
770
|
+
/* rail mode: show icon only for items by hiding labels visually but keeping them accessible */
|
|
771
|
+
.drawer-rail:not(.drawer-expanded) :deep(.rail-label) {
|
|
772
|
+
/* sr-only (visually hidden but accessible) */
|
|
773
|
+
position: absolute !important;
|
|
774
|
+
width: 1px;
|
|
775
|
+
height: 1px;
|
|
776
|
+
padding: 0;
|
|
777
|
+
margin: -1px;
|
|
778
|
+
overflow: hidden;
|
|
779
|
+
clip: rect(0 0 0 0);
|
|
780
|
+
white-space: nowrap;
|
|
781
|
+
border: 0;
|
|
782
|
+
}
|
|
783
|
+
|
|
784
|
+
.drawer-rail:not(.drawer-expanded) :deep(.rail-icon) {
|
|
785
|
+
margin-right: 0 !important;
|
|
786
|
+
display: inline-flex;
|
|
787
|
+
justify-content: center;
|
|
788
|
+
align-items: center;
|
|
789
|
+
}
|
|
790
|
+
|
|
791
|
+
.drawer-rail:not(.drawer-expanded) :deep(ul li) {
|
|
792
|
+
display: flex;
|
|
793
|
+
align-items: center;
|
|
794
|
+
justify-content: center;
|
|
795
|
+
padding-left: 0.5rem;
|
|
796
|
+
padding-right: 0.5rem;
|
|
797
|
+
}
|
|
798
|
+
|
|
799
|
+
.drawer-rail:not(.drawer-expanded) :deep(ul li .rail-icon) {
|
|
800
|
+
width: 100%;
|
|
801
|
+
display: flex;
|
|
802
|
+
justify-content: center;
|
|
803
|
+
}
|
|
804
|
+
|
|
805
|
+
/* center group header icon when collapsed */
|
|
806
|
+
.drawer-rail:not(.drawer-expanded) :deep(.group-header) {
|
|
807
|
+
display: flex;
|
|
808
|
+
align-items: center;
|
|
809
|
+
justify-content: center;
|
|
810
|
+
padding-left: 0.25rem;
|
|
811
|
+
padding-right: 0.25rem;
|
|
812
|
+
}
|
|
813
|
+
|
|
814
|
+
.drawer-rail:not(.drawer-expanded) :deep(.group-header) .text-lg {
|
|
815
|
+
display: none;
|
|
816
|
+
}
|
|
817
|
+
|
|
818
|
+
.drawer-rail:not(.drawer-expanded) :deep(.group-header) .mr-2 {
|
|
819
|
+
margin-right: 0;
|
|
820
|
+
}
|
|
821
|
+
|
|
822
|
+
/* rail label animation & transitions */
|
|
823
|
+
.drawer-rail :deep(.rail-label) {
|
|
824
|
+
opacity: 1;
|
|
825
|
+
max-width: 1000px;
|
|
826
|
+
transform: translateX(0);
|
|
827
|
+
}
|
|
828
|
+
|
|
829
|
+
.drawer-rail:not(.drawer-expanded) :deep(.rail-label) {
|
|
830
|
+
opacity: 0 !important;
|
|
831
|
+
/* visually hidden when collapsed */
|
|
832
|
+
max-width: 0 !important;
|
|
833
|
+
transform: translateX(-6px);
|
|
834
|
+
}
|
|
835
|
+
|
|
836
|
+
/* smooth container width transition */
|
|
837
|
+
.drawer-rail {
|
|
838
|
+
transition: width 700ms ease;
|
|
839
|
+
}
|
|
840
|
+
|
|
841
|
+
.drawer-expanded {
|
|
842
|
+
transition: width 700ms ease;
|
|
843
|
+
}
|
|
844
|
+
|
|
845
|
+
/* rotate chevron with a smooth transition for toggle buttons */
|
|
846
|
+
button[aria-expanded] :deep(svg) {
|
|
847
|
+
transition: transform 700ms ease;
|
|
848
|
+
}
|
|
849
|
+
</style>
|