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.
Files changed (180) hide show
  1. package/.env +1 -0
  2. package/.env.production +1 -0
  3. package/.prettierrc +13 -0
  4. package/.vscode/extensions.json +3 -0
  5. package/BUTTON_GUIDE.md +257 -0
  6. package/README.md +49 -0
  7. package/THEME_REFERENCE.md +310 -0
  8. package/eslint.config.ts +27 -0
  9. package/index.html +13 -0
  10. package/package.json +85 -0
  11. package/public/favicon.ico +0 -0
  12. package/src/App.vue +368 -0
  13. package/src/assets/fonts/Faruma.ttf +0 -0
  14. package/src/components/examples/AppBarExample.vue +101 -0
  15. package/src/components/examples/AvatarExample.vue +47 -0
  16. package/src/components/examples/BannerExample.vue +287 -0
  17. package/src/components/examples/BaseInputExample.vue +25 -0
  18. package/src/components/examples/BreadcrumbExample.vue +53 -0
  19. package/src/components/examples/CardExample.vue +77 -0
  20. package/src/components/examples/ChipExample.vue +225 -0
  21. package/src/components/examples/DatePickerExample.vue +31 -0
  22. package/src/components/examples/DropdownExample.vue +84 -0
  23. package/src/components/examples/EditorExample.vue +200 -0
  24. package/src/components/examples/ExpansionPanelExample.vue +42 -0
  25. package/src/components/examples/FileUploadExample.vue +40 -0
  26. package/src/components/examples/FormExample.vue +121 -0
  27. package/src/components/examples/HugeTest.vue +8 -0
  28. package/src/components/examples/LayoutContainerExample.vue +80 -0
  29. package/src/components/examples/ModalExample.vue +82 -0
  30. package/src/components/examples/NavDrawerExample.vue +170 -0
  31. package/src/components/examples/NumberFieldExample.vue +145 -0
  32. package/src/components/examples/RadioButtonExample.vue +161 -0
  33. package/src/components/examples/SearchExample.vue +322 -0
  34. package/src/components/examples/SelectExample.vue +121 -0
  35. package/src/components/examples/StackedTableViewExample.vue +53 -0
  36. package/src/components/examples/TabExample.vue +336 -0
  37. package/src/components/examples/TableExample.vue +228 -0
  38. package/src/components/examples/TextFieldExample.vue +181 -0
  39. package/src/components/examples/TextareaExample.vue +173 -0
  40. package/src/components/examples/ThemeToggle.vue +50 -0
  41. package/src/components/examples/TimelineExample.vue +66 -0
  42. package/src/components/examples/TipTapEditorExample.vue +20 -0
  43. package/src/components/examples/TooltipExample.vue +53 -0
  44. package/src/components/examples/VueDatePickerShowcase.vue +214 -0
  45. package/src/components/examples/_DatePickerExample.vue +33 -0
  46. package/src/components/examples/__FormExample.vue +77 -0
  47. package/src/components/index.ts +25 -0
  48. package/src/components/pgo/AppBar.vue +347 -0
  49. package/src/components/pgo/Avatar.vue +139 -0
  50. package/src/components/pgo/Banner.vue +300 -0
  51. package/src/components/pgo/Breadcrumb.vue +101 -0
  52. package/src/components/pgo/Button.vue +171 -0
  53. package/src/components/pgo/Card.vue +178 -0
  54. package/src/components/pgo/ConfirmationModel.vue +32 -0
  55. package/src/components/pgo/DataTable.vue +845 -0
  56. package/src/components/pgo/DatePicker/CalendarPanel.vue +43 -0
  57. package/src/components/pgo/DatePicker/__DatePicker.vue +122 -0
  58. package/src/components/pgo/DatePicker/types.ts +11 -0
  59. package/src/components/pgo/DatePicker/useCalendar.ts +39 -0
  60. package/src/components/pgo/DatePicker/useDatePicker.ts +31 -0
  61. package/src/components/pgo/Deprecated/ToastContainer.vue +51 -0
  62. package/src/components/pgo/Deprecated/ToastItem.vue +55 -0
  63. package/src/components/pgo/Dropdown.vue +296 -0
  64. package/src/components/pgo/DropdownItem.vue +40 -0
  65. package/src/components/pgo/Editor.vue +511 -0
  66. package/src/components/pgo/ExpansionPanel.vue +185 -0
  67. package/src/components/pgo/Footer.vue +39 -0
  68. package/src/components/pgo/HeroIcon.vue +124 -0
  69. package/src/components/pgo/InputSearch.vue +194 -0
  70. package/src/components/pgo/LayoutContainer.vue +104 -0
  71. package/src/components/pgo/Main.vue +37 -0
  72. package/src/components/pgo/Modal.vue +273 -0
  73. package/src/components/pgo/NavDrawer.vue +127 -0
  74. package/src/components/pgo/NavDrawerItem.vue +161 -0
  75. package/src/components/pgo/NavigationDrawer.vue +849 -0
  76. package/src/components/pgo/OLDNavDrawer.vue +661 -0
  77. package/src/components/pgo/OldAppBar.vue +223 -0
  78. package/src/components/pgo/PApp.vue +102 -0
  79. package/src/components/pgo/Pagination.vue +242 -0
  80. package/src/components/pgo/Search copy.vue +310 -0
  81. package/src/components/pgo/Search.vue +411 -0
  82. package/src/components/pgo/StackedTableView.vue +167 -0
  83. package/src/components/pgo/Tab.vue +617 -0
  84. package/src/components/pgo/TestInput.vue +395 -0
  85. package/src/components/pgo/Timeline.vue +367 -0
  86. package/src/components/pgo/TimelineItem.vue +80 -0
  87. package/src/components/pgo/TipTapEditor.vue +315 -0
  88. package/src/components/pgo/Tooltip.NOTES.md +12 -0
  89. package/src/components/pgo/Tooltip.PROPS.md +21 -0
  90. package/src/components/pgo/Tooltip.vue +281 -0
  91. package/src/components/pgo/base/Base.vue +444 -0
  92. package/src/components/pgo/buttons/Chip.vue +324 -0
  93. package/src/components/pgo/buttons/ChipGroup.vue +224 -0
  94. package/src/components/pgo/buttons/Radio.vue +424 -0
  95. package/src/components/pgo/filters/FilterSection.vue +188 -0
  96. package/src/components/pgo/filters/Searchbar.vue +216 -0
  97. package/src/components/pgo/forms/DynamicForm.vue +45 -0
  98. package/src/components/pgo/forms/Form.vue +132 -0
  99. package/src/components/pgo/index.ts +15 -0
  100. package/src/components/pgo/inputs/Checkbox.vue +320 -0
  101. package/src/components/pgo/inputs/DatePicker.vue +395 -0
  102. package/src/components/pgo/inputs/FileUpload.vue +326 -0
  103. package/src/components/pgo/inputs/NumberField.vue +243 -0
  104. package/src/components/pgo/inputs/Radio.vue +162 -0
  105. package/src/components/pgo/inputs/RadioGroup.vue +188 -0
  106. package/src/components/pgo/inputs/Select.vue +535 -0
  107. package/src/components/pgo/inputs/TextField.vue +194 -0
  108. package/src/components/pgo/inputs/Textarea.vue +181 -0
  109. package/src/main.js +12 -0
  110. package/src/pgo-components/_index.js +31 -0
  111. package/src/pgo-components/assets/fonts/Faruma.ttf +0 -0
  112. package/src/pgo-components/assets/fonts/logo.png +0 -0
  113. package/src/pgo-components/composables/useTheme.js +10 -0
  114. package/src/pgo-components/directives/tooltip-directive.ts +393 -0
  115. package/src/pgo-components/index.js +96 -0
  116. package/src/pgo-components/lib/componentConfig.js +147 -0
  117. package/src/pgo-components/lib/core/composables/_useCalendar.ts +127 -0
  118. package/src/pgo-components/lib/core/composables/useDefaults.ts +15 -0
  119. package/src/pgo-components/lib/core/composables/useLanguageSelect.js +0 -0
  120. package/src/pgo-components/lib/core/composables/useRtl.ts +12 -0
  121. package/src/pgo-components/lib/core/defaults/createDefaults.ts +5 -0
  122. package/src/pgo-components/lib/core/defaults/defaults.ts +7 -0
  123. package/src/pgo-components/lib/core/rtl/rtl.ts +3 -0
  124. package/src/pgo-components/lib/core/rtl/setRtl.ts +19 -0
  125. package/src/pgo-components/lib/drawerState.ts +3 -0
  126. package/src/pgo-components/lib/i18n/defaultLables.js +71 -0
  127. package/src/pgo-components/lib/i18n/i18nPlugin.js +52 -0
  128. package/src/pgo-components/lib/i18n/useI18n.js +35 -0
  129. package/src/pgo-components/lib/index.ts +38 -0
  130. package/src/pgo-components/pages/Component.vue +7 -0
  131. package/src/pgo-components/pages/ComponentRenderer.vue +85 -0
  132. package/src/pgo-components/pages/Home.vue +130 -0
  133. package/src/pgo-components/pages/ListView.vue +370 -0
  134. package/src/pgo-components/pages/Page1.vue +296 -0
  135. package/src/pgo-components/pages/_Page1.vue +180 -0
  136. package/src/pgo-components/plugins/SnackBar.vue +251 -0
  137. package/src/pgo-components/plugins/SnackBarContainer.vue +53 -0
  138. package/src/pgo-components/plugins/SnackBarPlugin.ts +136 -0
  139. package/src/pgo-components/plugins/theme-plugin.js +114 -0
  140. package/src/pgo-components/plugins/types.ts +46 -0
  141. package/src/pgo-components/plugins/useSnackBar.js +11 -0
  142. package/src/pgo-components/plugins/useSnackBar.ts +21 -0
  143. package/src/pgo-components/plugins/validation-plugin.js +11 -0
  144. package/src/pgo-components/services/Entry.json +813 -0
  145. package/src/pgo-components/services/axios.js +54 -0
  146. package/src/pgo-components/services/data.json +90 -0
  147. package/src/pgo-components/services/person.json +260 -0
  148. package/src/pgo-components/services/toast.ts +44 -0
  149. package/src/pgo-components/styles/global.css +234 -0
  150. package/src/pgo-components/styles/reset.css +96 -0
  151. package/src/pgo-components/styles/tokens.css +18 -0
  152. package/src/pgo-components/styles/utilities/border-radius.css +57 -0
  153. package/src/pgo-components/styles/utilities/borders.css +85 -0
  154. package/src/pgo-components/styles/utilities/colors.css +38 -0
  155. package/src/pgo-components/styles/utilities/cursor.css +19 -0
  156. package/src/pgo-components/styles/utilities/display.css +78 -0
  157. package/src/pgo-components/styles/utilities/elevation.css +33 -0
  158. package/src/pgo-components/styles/utilities/flex.css +403 -0
  159. package/src/pgo-components/styles/utilities/float.css +41 -0
  160. package/src/pgo-components/styles/utilities/hover.css +9 -0
  161. package/src/pgo-components/styles/utilities/index.css +18 -0
  162. package/src/pgo-components/styles/utilities/opacity.css +27 -0
  163. package/src/pgo-components/styles/utilities/overflow.css +26 -0
  164. package/src/pgo-components/styles/utilities/palette.css +515 -0
  165. package/src/pgo-components/styles/utilities/position.css +14 -0
  166. package/src/pgo-components/styles/utilities/sizing.css +70 -0
  167. package/src/pgo-components/styles/utilities/spacing.css +578 -0
  168. package/src/pgo-components/styles/utilities/transitions.css +58 -0
  169. package/src/pgo-components/styles/utilities/typography.css +91 -0
  170. package/src/pgo-components/styles/utilities/z-index.css +11 -0
  171. package/src/pgo-components/tokens/index.js +337 -0
  172. package/src/router/index.js +88 -0
  173. package/src/shims-vue.d.ts +14 -0
  174. package/src/validations/validationRules.js +50 -0
  175. package/tailwind.config.js +73 -0
  176. package/test.php +5 -0
  177. package/tsconfig.json +25 -0
  178. package/ui +31 -0
  179. package/ui.pgo.mv.conf +18 -0
  180. 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>