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