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,661 @@
|
|
|
1
|
+
<template>
|
|
2
|
+
<div class="relative">
|
|
3
|
+
<!-- Scrim for temporary drawer -->
|
|
4
|
+
<div
|
|
5
|
+
v-if="isOpen && temporary"
|
|
6
|
+
class="fixed inset-0 bg-black bg-opacity-40"
|
|
7
|
+
:class="scrimClass"
|
|
8
|
+
:style="scrimStyle"
|
|
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
|
+
'shadow-lg',
|
|
27
|
+
]"
|
|
28
|
+
:style="[zStyle, topPadding ? { paddingTop: topPadding + 'px' } : undefined]"
|
|
29
|
+
:aria-hidden="!isOpen && !permanent"
|
|
30
|
+
@mouseenter="onMouseEnter"
|
|
31
|
+
@mouseleave="onMouseLeave"
|
|
32
|
+
>
|
|
33
|
+
<div class="flex h-full flex-col" :style="{ minHeight: '100vh' }">
|
|
34
|
+
<div class="flex items-center justify-between px-4 py-3">
|
|
35
|
+
<slot name="prepend" v-if="showPrepend">
|
|
36
|
+
<!-- Default prepend content: app title -->
|
|
37
|
+
<span class="font-semibold">Menu</span>
|
|
38
|
+
</slot>
|
|
39
|
+
<div class="flex items-center gap-2">
|
|
40
|
+
<slot name="append"></slot>
|
|
41
|
+
<button
|
|
42
|
+
v-if="!permanent && !temporary"
|
|
43
|
+
@click="toggleOpen"
|
|
44
|
+
class="rounded p-2 hover:bg-gray-100"
|
|
45
|
+
aria-label="Toggle drawer"
|
|
46
|
+
>
|
|
47
|
+
|
|
48
|
+
<HeroIcon v-if="props.location === 'left'" name="chevron-left" size="20" />
|
|
49
|
+
<HeroIcon v-if="props.location === 'right'" name="chevron-right" size="20" />
|
|
50
|
+
</button>
|
|
51
|
+
<button v-if="temporary" @click="close" class="ml-2 rounded p-2 hover:bg-gray-100" aria-label="Close drawer">
|
|
52
|
+
<svg xmlns="http://www.w3.org/2000/svg" class="h-5 w-5" viewBox="0 0 20 20" fill="currentColor"><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" /></svg>
|
|
53
|
+
</button>
|
|
54
|
+
</div>
|
|
55
|
+
</div>
|
|
56
|
+
|
|
57
|
+
<div class="flex-1 overflow-auto">
|
|
58
|
+
<!-- If data-driven items provided, render grouped items and support filtering & ordering -->
|
|
59
|
+
<div v-if="props.items">
|
|
60
|
+
<div v-if="props.filterable && !(props.rail && collapsed)" class="px-3 py-2">
|
|
61
|
+
<input v-model="filterQuery" :placeholder="props.searchPlaceholder" class="w-full px-4 py-2 border border-gray-200" />
|
|
62
|
+
</div>
|
|
63
|
+
|
|
64
|
+
<div v-for="(group) in groupedItems" :key="group.group" class="py-2">
|
|
65
|
+
<div
|
|
66
|
+
v-if="group.group"
|
|
67
|
+
class="group-header px-3 pt-3 pb-1 text-lg text-gray-500 uppercase flex items-center justify-between"
|
|
68
|
+
role="button"
|
|
69
|
+
:tabindex="props.collapsibleGroups ? 0 : -1"
|
|
70
|
+
@click.prevent="toggleGroup(group.group)"
|
|
71
|
+
@keydown.enter.prevent="toggleGroup(group.group)"
|
|
72
|
+
@keydown.space.prevent="toggleGroup(group.group)"
|
|
73
|
+
:aria-expanded="isGroupOpen(group.group)"
|
|
74
|
+
>
|
|
75
|
+
<span class="flex items-center gap-2">
|
|
76
|
+
<!-- render group parent element (link or button) when enabled -->
|
|
77
|
+
<template v-if="props.renderGroupParent">
|
|
78
|
+
<component
|
|
79
|
+
:is="(props.groupParents && props.groupParents[group.group] && props.groupParents[group.group].href) ? 'a' : 'button'"
|
|
80
|
+
:href="(props.groupParents && props.groupParents[group.group] && props.groupParents[group.group].href) || undefined"
|
|
81
|
+
class="group-parent flex items-center gap-3 px-0 py-0 w-full text-left"
|
|
82
|
+
@click.stop="(e) => onGroupParentClick(e, group.group)"
|
|
83
|
+
@keydown="(e) => onGroupParentKey(e, group.group)"
|
|
84
|
+
>
|
|
85
|
+
<span v-if="props.groupParents && props.groupParents[group.group] && props.groupParents[group.group].icon" class="mr-2"><HeroIcon :name="props.groupParents[group.group].icon" size="18" /></span>
|
|
86
|
+
<span v-else-if="props.groupIcons && props.groupIcons[group.group]" class="mr-2"><HeroIcon :name="props.groupIcons[group.group]" size="18" /></span>
|
|
87
|
+
<span v-if="!props.rail || !collapsed" class="text-lg text-gray-500 uppercase">{{ (props.groupParents && props.groupParents[group.group] && props.groupParents[group.group].label) || group.group }}</span>
|
|
88
|
+
</component>
|
|
89
|
+
</template>
|
|
90
|
+
<template v-else>
|
|
91
|
+
<span v-if="props.groupIcons && props.groupIcons[group.group]" class="mr-2"><HeroIcon :name="props.groupIcons[group.group]" size="18" /></span>
|
|
92
|
+
<span v-if="!props.rail || !collapsed" class="text-lg text-gray-500 uppercase">{{ group.group }}</span>
|
|
93
|
+
</template>
|
|
94
|
+
</span>
|
|
95
|
+
<button
|
|
96
|
+
v-if="props.collapsibleGroups && (!props.rail || !collapsed)"
|
|
97
|
+
@click.stop.prevent="toggleGroup(group.group)"
|
|
98
|
+
@keydown.enter.stop.prevent="toggleGroup(group.group)"
|
|
99
|
+
@keydown.space.stop.prevent="toggleGroup(group.group)"
|
|
100
|
+
class="text-gray-400 hover:text-gray-600 px-2 py-1 rounded focus:outline-none"
|
|
101
|
+
:aria-expanded="isGroupOpen(group.group)"
|
|
102
|
+
>
|
|
103
|
+
<HeroIcon name="chevron-down" size="16" :class="{ 'rotate-180': isGroupOpen(group.group) }" />
|
|
104
|
+
</button>
|
|
105
|
+
</div>
|
|
106
|
+
<ul class="py-1" v-show="isGroupOpen(group.group)">
|
|
107
|
+
<NavDrawerItem
|
|
108
|
+
v-for="item in group.items"
|
|
109
|
+
:key="item.id"
|
|
110
|
+
:id="item.id"
|
|
111
|
+
:href="item.href"
|
|
112
|
+
:draggable="props.draggable && !item.disabled"
|
|
113
|
+
:dragging="String(draggingId) === String(item.id)"
|
|
114
|
+
:is-drag-over="String(dragOverId) === String(item.id)"
|
|
115
|
+
:drag-pos="dragOverPos"
|
|
116
|
+
@dragstart="handleDragStart"
|
|
117
|
+
@dragover="handleDragOver"
|
|
118
|
+
@dragleave="(e) => { if (String(dragOverId) === String(item.id)) { dragOverId = null; dragOverPos = null } }"
|
|
119
|
+
@drop="handleDrop"
|
|
120
|
+
>
|
|
121
|
+
<HeroIcon v-if="item.icon" :name="item.icon" size="20" class="rail-icon mr-2" />
|
|
122
|
+
<span class="rail-label">{{ item.label }}</span>
|
|
123
|
+
</NavDrawerItem>
|
|
124
|
+
</ul>
|
|
125
|
+
</div>
|
|
126
|
+
</div>
|
|
127
|
+
<div v-else>
|
|
128
|
+
<slot />
|
|
129
|
+
</div>
|
|
130
|
+
</div>
|
|
131
|
+
|
|
132
|
+
<div class="px-4 py-3">
|
|
133
|
+
<slot name="footer"></slot>
|
|
134
|
+
</div>
|
|
135
|
+
</div>
|
|
136
|
+
</nav>
|
|
137
|
+
</div>
|
|
138
|
+
</template>
|
|
139
|
+
|
|
140
|
+
<script setup lang="ts">
|
|
141
|
+
import { ref, watch, computed, onMounted, onBeforeUnmount, onUpdated } from 'vue'
|
|
142
|
+
import type { PropType } from 'vue'
|
|
143
|
+
import HeroIcon from './HeroIcon.vue'
|
|
144
|
+
import NavDrawerItem from './NavDrawerItem.vue'
|
|
145
|
+
|
|
146
|
+
const props = defineProps({
|
|
147
|
+
modelValue: { type: Boolean, default: false },
|
|
148
|
+
permanent: { type: Boolean, default: false },
|
|
149
|
+
temporary: { type: Boolean, default: false },
|
|
150
|
+
persistent: { type: Boolean, default: false },
|
|
151
|
+
rail: { type: Boolean, default: false },
|
|
152
|
+
hoverExpand: { type: Boolean, default: false },
|
|
153
|
+
isStuck: { type: Boolean, default: false },
|
|
154
|
+
location: { type: String as PropType<'left' | 'right'>, default: 'left' },
|
|
155
|
+
color: { type: String, default: 'bg-white dark:bg-gray-900' },
|
|
156
|
+
width: { type: [String, Number], default: 'w-64' },
|
|
157
|
+
railWidth: { type: [String, Number], default: 'w-16' },
|
|
158
|
+
closeOnScrimClick: { type: Boolean, default: true },
|
|
159
|
+
expandOnHover: { type: Boolean, default: false },
|
|
160
|
+
overlayAppBar: { type: Boolean, default: true }, // if false, drawer will appear under app bar
|
|
161
|
+
zIndex: { type: [String, Number], default: undefined },
|
|
162
|
+
appBarOffset: { type: Boolean, default: false },
|
|
163
|
+
appBarHeight: { type: [String, Number], default: undefined },
|
|
164
|
+
// data-driven items
|
|
165
|
+
items: { type: Array as PropType<Array<Record<string, any>>>, default: undefined },
|
|
166
|
+
filterable: { type: Boolean, default: false },
|
|
167
|
+
searchPlaceholder: { type: String, default: 'Search' },
|
|
168
|
+
draggable: { type: Boolean, default: false },
|
|
169
|
+
groupBy: { type: [String, Function] as PropType<string | ((item: any) => string) | undefined>, default: undefined },
|
|
170
|
+
allowCrossGroupReorder: { type: Boolean, default: false },
|
|
171
|
+
collapsibleGroups: { type: Boolean, default: true },
|
|
172
|
+
groupDefaultOpen: { type: Boolean, default: true },
|
|
173
|
+
groupIcons: { type: Object as PropType<Record<string, string> | undefined>, default: undefined },
|
|
174
|
+
renderGroupParent: { type: Boolean, default: false },
|
|
175
|
+
groupParents: { type: Object as PropType<Record<string, { label?: string; href?: string; icon?: string }>>, default: undefined },
|
|
176
|
+
})
|
|
177
|
+
|
|
178
|
+
const emit = defineEmits(['update:modelValue', 'open', 'close', 'update:items', 'reorder', 'group-toggle'])
|
|
179
|
+
|
|
180
|
+
const internalOpen = ref(Boolean(props.modelValue))
|
|
181
|
+
const navRef = ref<HTMLElement | null>(null)
|
|
182
|
+
// local copy of items so we can reorder and filter
|
|
183
|
+
const localItems = ref<Array<Record<string, any>>>(props.items ? [...props.items] : [])
|
|
184
|
+
const filterQuery = ref('')
|
|
185
|
+
// sync prop items with local items
|
|
186
|
+
watch(() => props.items, (i) => {
|
|
187
|
+
localItems.value = props.items ? [...props.items] : []
|
|
188
|
+
})
|
|
189
|
+
|
|
190
|
+
// derived filtered items
|
|
191
|
+
const filteredItems = computed(() => {
|
|
192
|
+
if (!props.items) return []
|
|
193
|
+
if (!filterQuery.value || filterQuery.value.trim().length === 0) return localItems.value
|
|
194
|
+
const q = filterQuery.value.toLowerCase()
|
|
195
|
+
return localItems.value.filter(it => {
|
|
196
|
+
const label = String(it.label || it.name || '')
|
|
197
|
+
return label.toLowerCase().includes(q) || (it.tags && String(it.tags).toLowerCase().includes(q))
|
|
198
|
+
})
|
|
199
|
+
})
|
|
200
|
+
|
|
201
|
+
// compute groups
|
|
202
|
+
const groupedItems = computed(() => {
|
|
203
|
+
if (!props.items) return []
|
|
204
|
+
if (!props.groupBy) return [{ group: null, items: filteredItems.value }]
|
|
205
|
+
const groups: Record<string, Array<any>> = {}
|
|
206
|
+
const getter = typeof props.groupBy === 'function' ? props.groupBy : (it: any) => it[props.groupBy as string]
|
|
207
|
+
filteredItems.value.forEach((it) => {
|
|
208
|
+
const key = getter(it) || 'Ungrouped'
|
|
209
|
+
if (!groups[key]) groups[key] = []
|
|
210
|
+
groups[key].push(it)
|
|
211
|
+
})
|
|
212
|
+
return Object.keys(groups).map(g => ({ group: g, items: groups[g] }))
|
|
213
|
+
})
|
|
214
|
+
|
|
215
|
+
// Track open/closed state for groups
|
|
216
|
+
const groupOpenMap = ref<Record<string, boolean>>({})
|
|
217
|
+
|
|
218
|
+
// init or sync group states when groupedItems change
|
|
219
|
+
watch(groupedItems, (groups) => {
|
|
220
|
+
groups.forEach((g) => {
|
|
221
|
+
const key = String(g.group || 'Ungrouped')
|
|
222
|
+
if (groupOpenMap.value[key] === undefined) {
|
|
223
|
+
groupOpenMap.value[key] = Boolean(props.groupDefaultOpen)
|
|
224
|
+
}
|
|
225
|
+
})
|
|
226
|
+
})
|
|
227
|
+
|
|
228
|
+
function isGroupOpen(key: string | null) {
|
|
229
|
+
if (!props.collapsibleGroups) return true
|
|
230
|
+
const k = String(key || 'Ungrouped')
|
|
231
|
+
return Boolean(groupOpenMap.value[k])
|
|
232
|
+
}
|
|
233
|
+
|
|
234
|
+
function toggleGroup(key: string | null) {
|
|
235
|
+
if (!props.collapsibleGroups) return
|
|
236
|
+
const k = String(key || 'Ungrouped')
|
|
237
|
+
groupOpenMap.value[k] = !Boolean(groupOpenMap.value[k])
|
|
238
|
+
emit('group-toggle', { group: key, open: groupOpenMap.value[k] })
|
|
239
|
+
}
|
|
240
|
+
|
|
241
|
+
function onGroupParentClick(e: Event, key: string | null) {
|
|
242
|
+
const parent = props.groupParents && key ? props.groupParents[key] : undefined
|
|
243
|
+
if (!parent || !parent.href) {
|
|
244
|
+
e.preventDefault()
|
|
245
|
+
toggleGroup(key)
|
|
246
|
+
}
|
|
247
|
+
}
|
|
248
|
+
|
|
249
|
+
function onGroupParentKey(e: KeyboardEvent, key: string | null) {
|
|
250
|
+
// Only toggle when Enter/Space and the group parent has no href (acting as a button)
|
|
251
|
+
if (!key) return
|
|
252
|
+
const parent = props.groupParents && key ? props.groupParents[key] : undefined
|
|
253
|
+
if (!(e.key === 'Enter' || e.key === ' ')) return
|
|
254
|
+
if (!parent || !parent.href) {
|
|
255
|
+
e.preventDefault()
|
|
256
|
+
e.stopPropagation()
|
|
257
|
+
toggleGroup(key)
|
|
258
|
+
}
|
|
259
|
+
}
|
|
260
|
+
|
|
261
|
+
// Normalize list items: wrap plain text nodes in `.rail-label` spans, mark icons with `.rail-icon` for consistent styling.
|
|
262
|
+
function normalizeNavItems() {
|
|
263
|
+
const nav = navRef.value
|
|
264
|
+
if (!nav) return
|
|
265
|
+
// If items are in a ul, normalize li elements. Otherwise, normalize anchors and buttons direct under nav
|
|
266
|
+
const items = nav.querySelectorAll('li')
|
|
267
|
+
items.forEach((li) => {
|
|
268
|
+
// Add rail-icon class to first svg or component
|
|
269
|
+
const firstSvg = li.querySelector('svg') as SVGElement | null
|
|
270
|
+
if (firstSvg && !firstSvg.classList.contains('rail-icon')) {
|
|
271
|
+
firstSvg.classList.add('rail-icon')
|
|
272
|
+
}
|
|
273
|
+
|
|
274
|
+
// Wrap text nodes in .rail-label spans (if not already wrapped)
|
|
275
|
+
const childNodes = Array.from(li.childNodes)
|
|
276
|
+
childNodes.forEach((node) => {
|
|
277
|
+
if (node.nodeType === Node.TEXT_NODE) {
|
|
278
|
+
const text = node.textContent?.trim()
|
|
279
|
+
if (text && text.length) {
|
|
280
|
+
// create span. Avoid double-wrapping by checking parent
|
|
281
|
+
const span = document.createElement('span')
|
|
282
|
+
span.className = 'rail-label'
|
|
283
|
+
span.textContent = text
|
|
284
|
+
li.replaceChild(span, node)
|
|
285
|
+
}
|
|
286
|
+
} else if (node.nodeType === Node.ELEMENT_NODE) {
|
|
287
|
+
const el = node as HTMLElement
|
|
288
|
+
// If element already is a label or icon, skip
|
|
289
|
+
if (el.classList.contains('rail-label') || el.classList.contains('rail-icon')) return
|
|
290
|
+
// If element contains text nodes, wrap them
|
|
291
|
+
const subChildren = Array.from(el.childNodes)
|
|
292
|
+
subChildren.forEach((sub) => {
|
|
293
|
+
if (sub.nodeType === Node.TEXT_NODE) {
|
|
294
|
+
const txt = sub.textContent?.trim()
|
|
295
|
+
if (txt && txt.length) {
|
|
296
|
+
const span = document.createElement('span')
|
|
297
|
+
span.className = 'rail-label'
|
|
298
|
+
span.textContent = txt
|
|
299
|
+
el.replaceChild(span, sub)
|
|
300
|
+
}
|
|
301
|
+
}
|
|
302
|
+
})
|
|
303
|
+
}
|
|
304
|
+
|
|
305
|
+
|
|
306
|
+
})
|
|
307
|
+
})
|
|
308
|
+
|
|
309
|
+
// Normalize anchors directly under nav (wrap anchors/buttons directly under nav in an li)
|
|
310
|
+
const directAnchors = nav.querySelectorAll(':scope > a, :scope > button')
|
|
311
|
+
directAnchors.forEach((a) => {
|
|
312
|
+
// ensure role and class
|
|
313
|
+
if (!a.classList.contains('nav-drawer-link')) a.classList.add('nav-drawer-link')
|
|
314
|
+
const existingParent = a.parentElement
|
|
315
|
+
if (existingParent && existingParent.tagName.toLowerCase() !== 'li') {
|
|
316
|
+
const li = document.createElement('li')
|
|
317
|
+
li.className = 'nav-drawer-item'
|
|
318
|
+
existingParent.replaceChild(li, a)
|
|
319
|
+
li.appendChild(a)
|
|
320
|
+
}
|
|
321
|
+
})
|
|
322
|
+
}
|
|
323
|
+
|
|
324
|
+
// Drag & drop helpers for data-driven items
|
|
325
|
+
let draggingId: string | number | null = null
|
|
326
|
+
let dragOverId: string | number | null = null
|
|
327
|
+
let dragOverPos: 'before' | 'after' | null = null
|
|
328
|
+
|
|
329
|
+
function findItemIndexById(id: string | number | null, arr = localItems.value) {
|
|
330
|
+
if (id === null || id === undefined) return -1
|
|
331
|
+
return arr.findIndex((it) => String(it.id) === String(id))
|
|
332
|
+
}
|
|
333
|
+
|
|
334
|
+
function handleDragStart(ctx: { id: string | number }) {
|
|
335
|
+
draggingId = ctx.id
|
|
336
|
+
}
|
|
337
|
+
|
|
338
|
+
function handleDragOver(ctx: { id: string | number, event?: DragEvent }) {
|
|
339
|
+
dragOverId = ctx.id
|
|
340
|
+
// compute position relative to target element using event
|
|
341
|
+
if (ctx.event && ctx.event.clientY !== undefined) {
|
|
342
|
+
const el = document.querySelector(`[data-drawer-item-id='${ctx.id}']`) as HTMLElement | null
|
|
343
|
+
if (el) {
|
|
344
|
+
const rect = el.getBoundingClientRect()
|
|
345
|
+
const mid = rect.top + rect.height / 2
|
|
346
|
+
dragOverPos = ctx.event.clientY < mid ? 'before' : 'after'
|
|
347
|
+
// set CSS classes by forcing a reactivity value
|
|
348
|
+
}
|
|
349
|
+
} else {
|
|
350
|
+
dragOverPos = 'after'
|
|
351
|
+
}
|
|
352
|
+
}
|
|
353
|
+
|
|
354
|
+
function handleDrop(ctx: { id: string | number, sourceId?: string | number, event?: DragEvent }) {
|
|
355
|
+
// if no items are passed, nothing to do
|
|
356
|
+
if (!props.items || !props.draggable) return
|
|
357
|
+
const sourceId = ctx.sourceId !== undefined ? ctx.sourceId : draggingId
|
|
358
|
+
const targetId = ctx.id
|
|
359
|
+
if (!sourceId || !targetId) return
|
|
360
|
+
if (String(sourceId) === String(targetId)) return
|
|
361
|
+
|
|
362
|
+
const fromIndex = findItemIndexById(sourceId)
|
|
363
|
+
const toIndex = findItemIndexById(targetId)
|
|
364
|
+
if (fromIndex === -1 || toIndex === -1) return
|
|
365
|
+
|
|
366
|
+
// move
|
|
367
|
+
const copy = [...localItems.value]
|
|
368
|
+
const [moved] = copy.splice(fromIndex, 1)
|
|
369
|
+
copy.splice(toIndex, 0, moved)
|
|
370
|
+
localItems.value = copy
|
|
371
|
+
emit('update:items', copy)
|
|
372
|
+
emit('reorder', { items: copy })
|
|
373
|
+
// reset drag id & pos
|
|
374
|
+
draggingId = null
|
|
375
|
+
dragOverId = null
|
|
376
|
+
dragOverPos = null
|
|
377
|
+
}
|
|
378
|
+
|
|
379
|
+
watch(() => props.modelValue, (v) => {
|
|
380
|
+
internalOpen.value = Boolean(v)
|
|
381
|
+
})
|
|
382
|
+
|
|
383
|
+
watch(internalOpen, (v) => {
|
|
384
|
+
emit('update:modelValue', v)
|
|
385
|
+
if (v) emit('open')
|
|
386
|
+
else emit('close')
|
|
387
|
+
})
|
|
388
|
+
|
|
389
|
+
// compute open: if permanent -> always open
|
|
390
|
+
const isOpen = computed(() => {
|
|
391
|
+
if (props.permanent) return true
|
|
392
|
+
return internalOpen.value
|
|
393
|
+
})
|
|
394
|
+
|
|
395
|
+
const toggleOpen = () => {
|
|
396
|
+
if (props.permanent) return
|
|
397
|
+
internalOpen.value = !internalOpen.value
|
|
398
|
+
}
|
|
399
|
+
|
|
400
|
+
const open = () => {
|
|
401
|
+
if (props.permanent) return
|
|
402
|
+
internalOpen.value = true
|
|
403
|
+
}
|
|
404
|
+
|
|
405
|
+
const close = () => {
|
|
406
|
+
if (props.permanent) return
|
|
407
|
+
internalOpen.value = false
|
|
408
|
+
}
|
|
409
|
+
|
|
410
|
+
const onScrimClick = () => {
|
|
411
|
+
if (props.closeOnScrimClick) close()
|
|
412
|
+
}
|
|
413
|
+
|
|
414
|
+
// keyboard ESC to close for temporary/persistent
|
|
415
|
+
const onKey = (e: KeyboardEvent) => {
|
|
416
|
+
if (e.key === 'Escape' && (props.temporary || props.persistent)) {
|
|
417
|
+
close()
|
|
418
|
+
}
|
|
419
|
+
}
|
|
420
|
+
|
|
421
|
+
onMounted(() => {
|
|
422
|
+
document.addEventListener('keydown', onKey)
|
|
423
|
+
// run normalization so rail mode works with existing markup
|
|
424
|
+
normalizeNavItems()
|
|
425
|
+
})
|
|
426
|
+
|
|
427
|
+
onUpdated(() => {
|
|
428
|
+
// re-run normalization when slot content or children update
|
|
429
|
+
normalizeNavItems()
|
|
430
|
+
})
|
|
431
|
+
|
|
432
|
+
onBeforeUnmount(() => {
|
|
433
|
+
document.removeEventListener('keydown', onKey)
|
|
434
|
+
window.removeEventListener('resize', onWindowResize)
|
|
435
|
+
})
|
|
436
|
+
|
|
437
|
+
// Focus trap for temporary/persistent mode when open
|
|
438
|
+
let focusTrapHandler: ((e: KeyboardEvent) => void) | null = null
|
|
439
|
+
function getFocusable(el: HTMLElement) {
|
|
440
|
+
const selectors = 'a[href], button:not([disabled]), textarea, input, select, [tabindex]:not([tabindex="-1"])'
|
|
441
|
+
return Array.from(el.querySelectorAll(selectors)) as HTMLElement[]
|
|
442
|
+
}
|
|
443
|
+
|
|
444
|
+
watch(isOpen, (open) => {
|
|
445
|
+
if (open && (props.temporary || props.persistent)) {
|
|
446
|
+
const nav = navRef.value as HTMLElement | null
|
|
447
|
+
if (!nav) return
|
|
448
|
+
const focusables = getFocusable(nav)
|
|
449
|
+
if (focusables.length) {
|
|
450
|
+
focusables[0].focus()
|
|
451
|
+
} else {
|
|
452
|
+
nav.setAttribute('tabindex', '-1')
|
|
453
|
+
nav.focus()
|
|
454
|
+
}
|
|
455
|
+
|
|
456
|
+
focusTrapHandler = (e: KeyboardEvent) => {
|
|
457
|
+
if (e.key !== 'Tab') return
|
|
458
|
+
const focusEls = getFocusable(nav)
|
|
459
|
+
if (!focusEls.length) return
|
|
460
|
+
const first = focusEls[0]
|
|
461
|
+
const last = focusEls[focusEls.length - 1]
|
|
462
|
+
if (e.shiftKey && document.activeElement === first) {
|
|
463
|
+
e.preventDefault()
|
|
464
|
+
last.focus()
|
|
465
|
+
} else if (!e.shiftKey && document.activeElement === last) {
|
|
466
|
+
e.preventDefault()
|
|
467
|
+
first.focus()
|
|
468
|
+
}
|
|
469
|
+
}
|
|
470
|
+
document.addEventListener('keydown', focusTrapHandler)
|
|
471
|
+
} else {
|
|
472
|
+
if (focusTrapHandler) {
|
|
473
|
+
document.removeEventListener('keydown', focusTrapHandler)
|
|
474
|
+
focusTrapHandler = null
|
|
475
|
+
}
|
|
476
|
+
}
|
|
477
|
+
// ensure nav items normalized on open state change (e.g., rail toggled)
|
|
478
|
+
normalizeNavItems()
|
|
479
|
+
})
|
|
480
|
+
|
|
481
|
+
// hover expand behavior
|
|
482
|
+
const hoverOpen = ref(false)
|
|
483
|
+
const allowHoverExpand = computed(() => {
|
|
484
|
+
// In rail mode, expand on hover is always enabled (UX requirement)
|
|
485
|
+
if (props.rail) return true
|
|
486
|
+
return Boolean(props.hoverExpand || props.expandOnHover)
|
|
487
|
+
})
|
|
488
|
+
let hoverTimeout: number | undefined
|
|
489
|
+
const onMouseEnter = () => {
|
|
490
|
+
if (allowHoverExpand.value) {
|
|
491
|
+
hoverTimeout && window.clearTimeout(hoverTimeout)
|
|
492
|
+
hoverOpen.value = true
|
|
493
|
+
}
|
|
494
|
+
}
|
|
495
|
+
const onMouseLeave = () => {
|
|
496
|
+
if (allowHoverExpand.value) {
|
|
497
|
+
hoverTimeout = window.setTimeout(() => {
|
|
498
|
+
hoverOpen.value = false
|
|
499
|
+
}, 150)
|
|
500
|
+
}
|
|
501
|
+
}
|
|
502
|
+
|
|
503
|
+
const collapsed = computed(() => props.rail && !hoverOpen.value)
|
|
504
|
+
|
|
505
|
+
// classes
|
|
506
|
+
const locationClass = computed(() => props.location === 'right' ? 'right-0' : 'left-0')
|
|
507
|
+
const widthClass = computed(() => {
|
|
508
|
+
if (collapsed.value) return typeof props.railWidth === 'number' ? `w-[${props.railWidth}px]` : String(props.railWidth)
|
|
509
|
+
return typeof props.width === 'number' ? `w-[${props.width}px]` : String(props.width)
|
|
510
|
+
})
|
|
511
|
+
|
|
512
|
+
const containerBaseClasses = computed(() => {
|
|
513
|
+
const base = ['fixed', 'h-full', 'overflow-hidden', 'flex', 'flex-col', 'bg-white']
|
|
514
|
+
if (props.isStuck) base.push('top-0')
|
|
515
|
+
if (props.location === 'right') base.push('right-0')
|
|
516
|
+
else base.push('left-0')
|
|
517
|
+
if (props.rail) base.push('transition-all')
|
|
518
|
+
watch(() => props.rail, (v) => {
|
|
519
|
+
normalizeNavItems()
|
|
520
|
+
})
|
|
521
|
+
return base.join(' ')
|
|
522
|
+
})
|
|
523
|
+
|
|
524
|
+
const railClass = computed(() => props.rail ? 'drawer-rail' : '')
|
|
525
|
+
const expandedClass = computed(() => (props.rail && !collapsed.value) ? 'drawer-expanded' : '')
|
|
526
|
+
const showPrepend = computed(() => !(props.rail && collapsed.value))
|
|
527
|
+
|
|
528
|
+
const hiddenClass = computed(() => {
|
|
529
|
+
if (props.location === 'right') return 'translate-x-full'
|
|
530
|
+
return '-translate-x-full'
|
|
531
|
+
})
|
|
532
|
+
|
|
533
|
+
// z-index control for drawer and scrim
|
|
534
|
+
const zClass = computed(() => {
|
|
535
|
+
if (props.zIndex !== undefined && typeof props.zIndex === 'string') {
|
|
536
|
+
// allow passing 'z-40' style classes
|
|
537
|
+
if ((props.zIndex as string).startsWith('z-')) return props.zIndex as string
|
|
538
|
+
}
|
|
539
|
+
return props.overlayAppBar ? 'z-50' : 'z-10'
|
|
540
|
+
})
|
|
541
|
+
|
|
542
|
+
const zStyle = computed(() => {
|
|
543
|
+
if (props.zIndex !== undefined && typeof props.zIndex === 'number') {
|
|
544
|
+
return { zIndex: props.zIndex }
|
|
545
|
+
}
|
|
546
|
+
return undefined
|
|
547
|
+
})
|
|
548
|
+
|
|
549
|
+
const scrimStyle = computed(() => {
|
|
550
|
+
if (props.zIndex !== undefined && typeof props.zIndex === 'number') {
|
|
551
|
+
return { zIndex: (props.zIndex as number) - 1 }
|
|
552
|
+
}
|
|
553
|
+
return undefined
|
|
554
|
+
})
|
|
555
|
+
|
|
556
|
+
// Calculate top padding so content appears below app bar if drawer sits behind it
|
|
557
|
+
const topPadding = ref(0)
|
|
558
|
+
const computeTopPadding = () => {
|
|
559
|
+
if (!props.appBarOffset && props.overlayAppBar) {
|
|
560
|
+
topPadding.value = 0
|
|
561
|
+
return
|
|
562
|
+
}
|
|
563
|
+
if (props.appBarHeight !== undefined && props.appBarHeight !== null) {
|
|
564
|
+
if (typeof props.appBarHeight === 'number') topPadding.value = props.appBarHeight as number
|
|
565
|
+
else if (typeof props.appBarHeight === 'string') {
|
|
566
|
+
const px = Number(String(props.appBarHeight).replace(/px$/i, ''))
|
|
567
|
+
topPadding.value = Number.isFinite(px) ? px : 0
|
|
568
|
+
}
|
|
569
|
+
return
|
|
570
|
+
}
|
|
571
|
+
// detect appbar element
|
|
572
|
+
if (typeof document !== 'undefined') {
|
|
573
|
+
const el = document.querySelector('[data-component="appbar"]') as HTMLElement | null
|
|
574
|
+
if (!el) {
|
|
575
|
+
topPadding.value = 0
|
|
576
|
+
return
|
|
577
|
+
}
|
|
578
|
+
const rect = el.getBoundingClientRect()
|
|
579
|
+
topPadding.value = Math.round(rect.height)
|
|
580
|
+
}
|
|
581
|
+
}
|
|
582
|
+
|
|
583
|
+
// compute initial padding
|
|
584
|
+
computeTopPadding()
|
|
585
|
+
// recompute on resize
|
|
586
|
+
let resizeTimer: number | undefined
|
|
587
|
+
const onWindowResize = () => {
|
|
588
|
+
resizeTimer && window.clearTimeout(resizeTimer)
|
|
589
|
+
resizeTimer = window.setTimeout(() => {
|
|
590
|
+
computeTopPadding()
|
|
591
|
+
}, 100)
|
|
592
|
+
}
|
|
593
|
+
onMounted(() => {
|
|
594
|
+
window.addEventListener('resize', onWindowResize)
|
|
595
|
+
})
|
|
596
|
+
onBeforeUnmount(() => {
|
|
597
|
+
window.removeEventListener('resize', onWindowResize)
|
|
598
|
+
})
|
|
599
|
+
|
|
600
|
+
const scrimClass = computed(() => {
|
|
601
|
+
if (props.zIndex !== undefined && typeof props.zIndex === 'number') {
|
|
602
|
+
// scrim will be one less than drawer z-index — but since we're using inline style, we won't set class
|
|
603
|
+
return ''
|
|
604
|
+
}
|
|
605
|
+
return props.overlayAppBar ? 'z-40' : 'z-0'
|
|
606
|
+
})
|
|
607
|
+
|
|
608
|
+
// always allow overlay toggling for temporary models
|
|
609
|
+
|
|
610
|
+
</script>
|
|
611
|
+
|
|
612
|
+
<style scoped>
|
|
613
|
+
/* small helpers */
|
|
614
|
+
.w-rail {
|
|
615
|
+
width: var(--rail-width, 4rem);
|
|
616
|
+
}
|
|
617
|
+
|
|
618
|
+
/* rail mode: show icon only for items by hiding labels visually but keeping them accessible */
|
|
619
|
+
.drawer-rail:not(.drawer-expanded) :deep(.rail-label) {
|
|
620
|
+
/* sr-only (visually hidden but accessible) */
|
|
621
|
+
position: absolute !important;
|
|
622
|
+
width: 1px;
|
|
623
|
+
height: 1px;
|
|
624
|
+
padding: 0;
|
|
625
|
+
margin: -1px;
|
|
626
|
+
overflow: hidden;
|
|
627
|
+
clip: rect(0 0 0 0);
|
|
628
|
+
white-space: nowrap;
|
|
629
|
+
border: 0;
|
|
630
|
+
}
|
|
631
|
+
.drawer-rail:not(.drawer-expanded) :deep(.rail-icon) { margin-right: 0 !important; display: inline-flex; justify-content:center; align-items:center }
|
|
632
|
+
.drawer-rail:not(.drawer-expanded) :deep(ul li) { display:flex; align-items:center; justify-content:center; padding-left: 0.5rem; padding-right: 0.5rem }
|
|
633
|
+
.drawer-rail:not(.drawer-expanded) :deep(ul li .rail-icon) { width: 100%; display:flex; justify-content:center }
|
|
634
|
+
/* center group header icon when collapsed */
|
|
635
|
+
.drawer-rail:not(.drawer-expanded) :deep(.group-header) { display:flex; align-items:center; justify-content:center; padding-left:0.25rem; padding-right:0.25rem }
|
|
636
|
+
.drawer-rail:not(.drawer-expanded) :deep(.group-header) .text-lg { display:none }
|
|
637
|
+
.drawer-rail:not(.drawer-expanded) :deep(.group-header) .mr-2 { margin-right: 0 }
|
|
638
|
+
/* rail label animation & transitions */
|
|
639
|
+
.drawer-rail :deep(.rail-label) {
|
|
640
|
+
opacity: 1;
|
|
641
|
+
max-width: 1000px;
|
|
642
|
+
transform: translateX(0);
|
|
643
|
+
}
|
|
644
|
+
.drawer-rail:not(.drawer-expanded) :deep(.rail-label) {
|
|
645
|
+
opacity: 0 !important; /* visually hidden when collapsed */
|
|
646
|
+
max-width: 0 !important;
|
|
647
|
+
transform: translateX(-6px);
|
|
648
|
+
}
|
|
649
|
+
/* smooth container width transition */
|
|
650
|
+
.drawer-rail {
|
|
651
|
+
transition: width 700ms ease;
|
|
652
|
+
}
|
|
653
|
+
.drawer-expanded {
|
|
654
|
+
transition: width 700ms ease;
|
|
655
|
+
}
|
|
656
|
+
|
|
657
|
+
/* rotate chevron with a smooth transition for toggle buttons */
|
|
658
|
+
button[aria-expanded] :deep(svg) {
|
|
659
|
+
transition: transform 700ms ease;
|
|
660
|
+
}
|
|
661
|
+
</style>
|