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,43 @@
1
+ <script setup lang="ts">
2
+ import { computed } from 'vue'
3
+ import { isSameDay, isToday } from 'date-fns'
4
+ import { useCalendar } from './useCalendar.ts'
5
+
6
+ const props = defineProps<{
7
+ value?: any
8
+ minDate?: Date
9
+ maxDate?: Date
10
+ disabledDate?: (date: Date) => boolean
11
+ }>()
12
+
13
+ const emit = defineEmits<{
14
+ (e: 'select', v: Date): void
15
+ }>()
16
+
17
+ const calendar = useCalendar(new Date())
18
+ const days = computed(() => calendar.daysInView())
19
+
20
+ function disabled(d: Date) {
21
+ if (props.minDate && d < props.minDate) return true
22
+ if (props.maxDate && d > props.maxDate) return true
23
+ return props.disabledDate?.(d)
24
+ }
25
+ </script>
26
+
27
+ <template>
28
+ <div role="grid" class="dp-calendar">
29
+ <button
30
+ v-for="d in days"
31
+ :key="d.toISOString()"
32
+ role="gridcell"
33
+ :disabled="disabled(d)"
34
+ :class="{
35
+ today: isToday(d),
36
+ selected: isSameDay(d, value)
37
+ }"
38
+ @click="emit('select', d)"
39
+ >
40
+ {{ d.getDate() }}
41
+ </button>
42
+ </div>
43
+ </template>
@@ -0,0 +1,122 @@
1
+ <template>
2
+ <div class="dp-root" dir="ltr" lang="en">
3
+ <input
4
+ ref="inputRef"
5
+ class="dp-input"
6
+ :value="displayValue"
7
+ :disabled="disabled"
8
+ readonly
9
+ @click="
10
+ open = !open
11
+ emit('openChange', open)
12
+ "
13
+ />
14
+
15
+ <button v-if="clearable && value" class="dp-clear" @click="clear">×</button>
16
+
17
+ <teleport to="body">
18
+ <div v-if="open" class="dp-popup" role="dialog" dir="ltr" lang="en">
19
+ <CalendarPanel
20
+ :value="value"
21
+ :mode="mode"
22
+ :range="range"
23
+ :min-date="minDate"
24
+ :max-date="maxDate"
25
+ :disabled-date="disabledDate"
26
+ :locale="locale"
27
+ @select="onSelect"
28
+ />
29
+ </div>
30
+ </teleport>
31
+ </div>
32
+ </template>
33
+
34
+ <script setup lang="ts">
35
+ import { ref, computed, onMounted, onBeforeUnmount } from 'vue'
36
+ import { format } from 'date-fns'
37
+ import { useDatePicker } from './useDatePicker.ts'
38
+ import CalendarPanel from './CalendarPanel.vue'
39
+ import type { PickerMode, RangeValue, LocaleConfig } from './types.ts'
40
+
41
+ const props = defineProps<{
42
+ modelValue?: Date | RangeValue
43
+ defaultValue?: Date | RangeValue
44
+ mode?: PickerMode
45
+ range?: boolean
46
+ format?: string
47
+ disabled?: boolean
48
+ clearable?: boolean
49
+ minDate?: Date
50
+ maxDate?: Date
51
+ disabledDate?: (d: Date) => boolean
52
+ placement?: 'bottom-start' | 'bottom-end' | 'top-start' | 'top-end'
53
+ locale?: LocaleConfig
54
+ }>()
55
+
56
+ const emit = defineEmits<{
57
+ (e: 'update:modelValue', v: any): void
58
+ (e: 'change', v: any): void
59
+ (e: 'openChange', v: boolean): void
60
+ }>()
61
+
62
+ const open = ref(false)
63
+ const inputRef = ref<HTMLInputElement | null>(null)
64
+
65
+ const { value } = useDatePicker(props, emit)
66
+
67
+ const displayValue = computed(() => {
68
+ if (!value.value) return ''
69
+ if (Array.isArray(value.value)) {
70
+ const [s, e] = value.value
71
+ return s && e ? `${format(s, props.format ?? 'yyyy-MM-dd')} – ${format(e, props.format ?? 'yyyy-MM-dd')}` : ''
72
+ }
73
+ return format(value.value, props.format ?? 'yyyy-MM-dd')
74
+ })
75
+
76
+ function onSelect(date: any) {
77
+ value.value = date
78
+ open.value = false
79
+ emit('openChange', false)
80
+ }
81
+
82
+ function clear() {
83
+ value.value = undefined
84
+ }
85
+
86
+ function onKeydown(e: KeyboardEvent) {
87
+ if (e.key === 'Escape') open.value = false
88
+ }
89
+
90
+ onMounted(() => document.addEventListener('keydown', onKeydown))
91
+ onBeforeUnmount(() => document.removeEventListener('keydown', onKeydown))
92
+ </script>
93
+
94
+ <style>
95
+ .dp-root {
96
+ position: relative;
97
+ }
98
+
99
+ .dp-input {
100
+ width: 100%;
101
+ padding: 9.5rem;
102
+ /* background: var(--vts-color-surface); */
103
+ background: bg-red-500;
104
+ color: var(--vts-color-text);
105
+ border: 1px solid var(--vts-color-border);
106
+ border-radius: var(--vts-radius-md);
107
+ }
108
+
109
+ .dp-popup {
110
+ position: absolute;
111
+ z-index: 1000;
112
+ /* background: var(--vts-color-surfaceElevated); */
113
+ background: bg-red-500;
114
+ color: var(--vts-color-text);
115
+ border: 1px solid var(--vts-color-border);
116
+ border-radius: var(--vts-radius-md);
117
+ box-shadow: var(--vts-elevation-2, 0 4px 12px rgba(0, 0, 0, 0.12));
118
+ padding: 8px;
119
+ direction: rtl;
120
+ /* unicode-bidi: isolate; */
121
+ }
122
+ </style>
@@ -0,0 +1,11 @@
1
+ export type PickerMode = 'date' | 'week' | 'month' | 'quarter' | 'year' | 'time'
2
+
3
+ export type RangeValue = [Date | null, Date | null]
4
+
5
+ export interface LocaleConfig {
6
+ locale: string
7
+ weekStartsOn?: 0 | 1 | 2 | 3 | 4 | 5 | 6
8
+ months: string[]
9
+ weekdays: string[]
10
+ todayLabel?: string
11
+ }
@@ -0,0 +1,39 @@
1
+ import { ref } from 'vue';
2
+ import { addMonths, addYears, startOfMonth, endOfMonth, eachDayOfInterval, add, } from 'date-fns';
3
+
4
+ export function useCalendar(initial: Date){
5
+ const viewDate = ref(startOfMonth(initial))
6
+
7
+ function nextMonth(){
8
+ viewDate.value = addMonths(viewDate.value, 1)
9
+ }
10
+
11
+ function prevMonth(){
12
+ viewDate.value = addMonths(viewDate.value, -1)
13
+ }
14
+
15
+ function nextYear(){
16
+ viewDate.value = addYears(viewDate.value, 1)
17
+ }
18
+
19
+ function prevYear(){
20
+ viewDate.value = addYears(viewDate.value, -1)
21
+ }
22
+
23
+ function daysInView(){
24
+ return eachDayOfInterval({
25
+ start: startOfMonth(viewDate.value),
26
+ end: endOfMonth(viewDate.value)
27
+ })
28
+ }
29
+
30
+ return {
31
+ viewDate,
32
+ nextMonth,
33
+ prevMonth,
34
+ nextYear,
35
+ prevYear,
36
+ daysInView
37
+ }
38
+
39
+ }
@@ -0,0 +1,31 @@
1
+ import { ref, computed, watch } from 'vue'
2
+
3
+ export function useDatePicker<T>(
4
+ props: {
5
+ modelValue?: T
6
+ defaultValue?: T
7
+ },
8
+ emit: (e: string, v: any) => void
9
+ ) {
10
+ const internal = ref<T | undefined>(props.defaultValue)
11
+
12
+ const value = computed<T | undefined>({
13
+ get() {
14
+ return props.modelValue !== undefined ? props.modelValue : internal.value
15
+ },
16
+ set(v) {
17
+ internal.value = v
18
+ emit('update:modelValue', v)
19
+ emit('change', v)
20
+ }
21
+ })
22
+
23
+ watch(
24
+ () => props.modelValue,
25
+ v => {
26
+ if (v !== undefined) internal.value = v
27
+ }
28
+ )
29
+
30
+ return { value }
31
+ }
@@ -0,0 +1,51 @@
1
+ <template>
2
+ <div :class="positionClass" class="fixed z-50 flex flex-col gap-2">
3
+ <TransitionGroup name="toast" tag="div">
4
+ <ToastItem v-for="toast in toasts" :key="toast.id" :toast="toast" @close="remove(toast.id)" />
5
+ </TransitionGroup>
6
+ </div>
7
+ </template>
8
+
9
+ <script setup lang="ts">
10
+ import { computed } from 'vue'
11
+ import { toast } from '../../../pgo-components/services/toast.js'
12
+ import ToastItem from './ToastItem.vue'
13
+
14
+ const props = defineProps<{
15
+ position?: 'top-right' | 'top-left' | 'bottom-right' | 'bottom-left'
16
+ }>()
17
+
18
+ const toasts = toast.state.toasts
19
+
20
+ const positionClass = computed(() => {
21
+ switch (props.position) {
22
+ case 'top-left':
23
+ return 'top-4 left-4'
24
+ case 'bottom-left':
25
+ return 'bottom-4 left-4'
26
+ case 'bottom-right':
27
+ return 'bottom-4 right-4'
28
+ default:
29
+ return 'top-4 right-4'
30
+ }
31
+ })
32
+
33
+ function remove(id: number) {
34
+ toast.remove(id)
35
+ }
36
+ </script>
37
+
38
+ <style>
39
+ .toast-enter-from {
40
+ opacity: 0;
41
+ transform: translateY(-10px);
42
+ }
43
+ .toast-enter-active,
44
+ .toast-leave-active {
45
+ transition: all 0.25s ease;
46
+ }
47
+ .toast-leave-to {
48
+ opacity: 0;
49
+ transform: translateY(-10px);
50
+ }
51
+ </style>
@@ -0,0 +1,55 @@
1
+ <template>
2
+ <div
3
+ class="min-w-[260px] max-w-sm rounded-lg px-4 py-3 shadow-lg text-white flex items-start gap-3"
4
+ :class="typeClass"
5
+ @mouseenter="pause"
6
+ @mouseleave="resume"
7
+ >
8
+ <div class="flex-1 text-sm">
9
+ {{ toast.message }}
10
+ </div>
11
+
12
+ <button v-if="toast.closable" class="text-white/70 hover:text-white" @click="$emit('close')">✕</button>
13
+ </div>
14
+ </template>
15
+
16
+ <script setup lang="ts">
17
+ import { computed, onMounted, onBeforeUnmount } from 'vue'
18
+ import type { Toast } from '../../services/toast.ts'
19
+
20
+ const props = defineProps<{
21
+ toast: Toast
22
+ }>()
23
+
24
+ const emit = defineEmits(['close'])
25
+
26
+ let timeout: number
27
+ let remaining = props.toast.duration
28
+ let startTime = Date.now()
29
+
30
+ const typeClass = computed(() => {
31
+ return {
32
+ info: 'bg-blue-600',
33
+ success: 'bg-green-600',
34
+ warning: 'bg-yellow-500 text-black',
35
+ error: 'bg-red-600'
36
+ }[props.toast.type]
37
+ })
38
+
39
+ function startTimer() {
40
+ startTime = Date.now()
41
+ timeout = window.setTimeout(() => emit('close'), remaining)
42
+ }
43
+
44
+ function pause() {
45
+ clearTimeout(timeout)
46
+ remaining -= Date.now() - startTime
47
+ }
48
+
49
+ function resume() {
50
+ startTimer()
51
+ }
52
+
53
+ onMounted(startTimer)
54
+ onBeforeUnmount(() => clearTimeout(timeout))
55
+ </script>
@@ -0,0 +1,296 @@
1
+ <template>
2
+ <!-- Trigger -->
3
+ <div ref="triggerRef" @click="toggle" class="inline-block">
4
+ <slot name="trigger">
5
+ <button :class="triggerClasses">
6
+ <slot name="label">{{ label }}</slot>
7
+ <svg
8
+ class="ml-2 h-4 w-4 transition-transform"
9
+ :class="{ 'rotate-180': isOpen }"
10
+ fill="none"
11
+ stroke="currentColor"
12
+ viewBox="0 0 24 24"
13
+ >
14
+ <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M19 9l-7 7-7-7" />
15
+ </svg>
16
+ </button>
17
+ </slot>
18
+ </div>
19
+
20
+ <!-- Menu (Teleported) -->
21
+ <teleport to="body">
22
+ <transition
23
+ enter-active-class="transition ease-out duration-100"
24
+ enter-from-class="transform opacity-0 scale-95"
25
+ enter-to-class="transform opacity-100 scale-100"
26
+ leave-active-class="transition ease-in duration-75"
27
+ leave-from-class="transform opacity-100 scale-100"
28
+ leave-to-class="transform opacity-0 scale-95"
29
+ >
30
+ <div
31
+ v-if="isOpen"
32
+ ref="menuRef"
33
+ :style="menuStyle"
34
+ :class="menuClasses"
35
+ >
36
+ <slot>
37
+ <!-- Default items rendering -->
38
+ <div
39
+ v-for="(item, index) in items"
40
+ :key="index"
41
+ @click="selectItem(item)"
42
+ :class="itemClasses"
43
+ >
44
+ {{ item.label || item }}
45
+ </div>
46
+ </slot>
47
+ </div>
48
+ </transition>
49
+ </teleport>
50
+ </template>
51
+
52
+ <script setup>
53
+ import { ref, computed, onMounted, onBeforeUnmount, nextTick, watch } from 'vue'
54
+ import { sizes, iconSizes, roundedMap, colorMap } from '../../pgo-components/lib/componentConfig'
55
+
56
+ const props = defineProps({
57
+ modelValue: { type: Boolean, default: false },
58
+ label: { type: String, default: 'Menu' },
59
+ items: { type: Array, default: () => [] },
60
+ width: { type: [String, Number], default: 'auto' },
61
+ minWidth: { type: [String, Number], default: '200' },
62
+ maxHeight: { type: [String, Number], default: '400' },
63
+ align: { type: String, default: 'left' }, // left | right | center
64
+ offsetX: { type: Number, default: 0 },
65
+ offsetY: { type: Number, default: 8 },
66
+ openOnHover: { type: Boolean, default: false },
67
+ closeOnContentClick: { type: Boolean, default: true },
68
+ disabled: { type: Boolean, default: false },
69
+ elevation: { type: Number, default: 8 },
70
+ rounded: { type: String, default: 'md' }, // none | sm | md | lg | xl
71
+ dense: { type: Boolean, default: false },
72
+ color: { type: String, default: 'primary' },
73
+ variant: { type: String, default: 'contained' }, // contained | outlined | text | tonal | plain
74
+ size: { type: String, default: 'md' }, // xs | sm | md | lg | xl
75
+ icon: {type: String, default: '' },
76
+ })
77
+
78
+ const emit = defineEmits(['update:modelValue', 'select', 'opened', 'closed'])
79
+
80
+ const isOpen = ref(props.modelValue)
81
+ const triggerRef = ref(null)
82
+ const menuRef = ref(null)
83
+ const menuStyle = ref({})
84
+
85
+ // Classes
86
+ const variantClasses = computed(() => {
87
+ const c = colorMap[props.color] || colorMap.gray
88
+ return c[props.variant] || c.contained
89
+ })
90
+ const triggerClasses = computed(() => [
91
+ 'inline-flex items-center justify-center font-medium transition-all cursor-pointer select-none',
92
+ props.icon ? '' : sizes[props.size],
93
+ roundedMap[props.rounded],
94
+ 'focus:outline-none focus:ring-2 focus:ring-ring focus:ring-offset-2',
95
+ 'disabled:pointer-events-none disabled:opacity-50',
96
+ variantClasses.value,
97
+ `rounded-${props.rounded}`,
98
+ props.disabled ? 'opacity-50 cursor-not-allowed' : 'cursor-pointer'
99
+ ])
100
+
101
+ const menuClasses = computed(() => [
102
+ 'fixed z-50 bg-white text-gray-700',
103
+ 'border border-gray-200 shadow-lg',
104
+ 'animate-in fade-in-0 zoom-in-95',
105
+ `rounded-${props.rounded}`,
106
+ props.dense ? 'p-1' : 'p-2',
107
+ 'overflow-auto'
108
+ ])
109
+
110
+ const itemClasses = computed(() => [
111
+ 'relative flex cursor-pointer select-none items-center',
112
+ 'rounded-sm px-2 outline-none transition-colors',
113
+ 'hover:bg-gray-100 hover:text-accent-foreground',
114
+ 'focus:bg-accent focus:text-accent-foreground',
115
+ 'data-[disabled]:pointer-events-none data-[disabled]:opacity-50',
116
+ props.dense ? 'py-1 text-sm' : 'py-2 text-sm'
117
+ ])
118
+
119
+ // Watch modelValue
120
+ watch(() => props.modelValue, (val) => {
121
+ isOpen.value = val
122
+ if (val) {
123
+ nextTick(() => updatePosition())
124
+ }
125
+ })
126
+
127
+ // Toggle menu
128
+ const toggle = () => {
129
+ if (props.disabled) return
130
+ isOpen.value = !isOpen.value
131
+ emit('update:modelValue', isOpen.value)
132
+
133
+ if (isOpen.value) {
134
+ emit('opened')
135
+ nextTick(() => updatePosition())
136
+ } else {
137
+ emit('closed')
138
+ }
139
+ }
140
+
141
+ // Open menu
142
+ const open = () => {
143
+ if (props.disabled || isOpen.value) return
144
+ isOpen.value = true
145
+ emit('update:modelValue', true)
146
+ emit('opened')
147
+ nextTick(() => updatePosition())
148
+ }
149
+
150
+ // Close menu
151
+ const close = () => {
152
+ if (!isOpen.value) return
153
+ isOpen.value = false
154
+ emit('update:modelValue', false)
155
+ emit('closed')
156
+ }
157
+
158
+ // Select item
159
+ const selectItem = (item) => {
160
+ emit('select', item)
161
+ if (props.closeOnContentClick) {
162
+ close()
163
+ }
164
+ }
165
+
166
+ // Update menu position
167
+ const updatePosition = () => {
168
+ if (!triggerRef.value || !menuRef.value) return
169
+
170
+ const trigger = triggerRef.value.getBoundingClientRect()
171
+ const menu = menuRef.value.getBoundingClientRect()
172
+ const viewport = {
173
+ width: window.innerWidth,
174
+ height: window.innerHeight
175
+ }
176
+
177
+ let top = trigger.bottom + props.offsetY
178
+ let left = trigger.left + props.offsetX
179
+
180
+ // Check if menu would overflow viewport
181
+ const wouldOverflowBottom = top + menu.height > viewport.height
182
+ const wouldOverflowRight = left + menu.width > viewport.width
183
+
184
+ // Open upwards if would overflow bottom
185
+ if (wouldOverflowBottom) {
186
+ top = trigger.top - menu.height - props.offsetY
187
+ }
188
+
189
+ // Align based on prop
190
+ if (props.align === 'right') {
191
+ left = trigger.right - menu.width + props.offsetX
192
+ } else if (props.align === 'center') {
193
+ left = trigger.left + (trigger.width / 2) - (menu.width / 2) + props.offsetX
194
+ }
195
+
196
+ // Adjust if would overflow right
197
+ if (wouldOverflowRight) {
198
+ left = viewport.width - menu.width - 8
199
+ }
200
+
201
+ // Adjust if would overflow left
202
+ if (left < 8) {
203
+ left = 8
204
+ }
205
+
206
+ const width = props.width === 'auto'
207
+ ? `${Math.max(trigger.width, parseInt(props.minWidth))}px`
208
+ : typeof props.width === 'number'
209
+ ? `${props.width}px`
210
+ : props.width
211
+
212
+ menuStyle.value = {
213
+ position: 'fixed',
214
+ top: `${top}px`,
215
+ left: `${left}px`,
216
+ width: width,
217
+ minWidth: `${props.minWidth}px`,
218
+ maxHeight: `${props.maxHeight}px`,
219
+ zIndex: 50 + props.elevation
220
+ }
221
+ }
222
+
223
+ // Click outside handler
224
+ const handleClickOutside = (e) => {
225
+ if (
226
+ triggerRef.value && !triggerRef.value.contains(e.target) &&
227
+ menuRef.value && !menuRef.value.contains(e.target)
228
+ ) {
229
+ close()
230
+ }
231
+ }
232
+
233
+ // Hover handlers
234
+ const handleMouseEnter = () => {
235
+ if (props.openOnHover) {
236
+ open()
237
+ }
238
+ }
239
+
240
+ const handleMouseLeave = () => {
241
+ if (props.openOnHover) {
242
+ close()
243
+ }
244
+ }
245
+
246
+ // Lifecycle
247
+ onMounted(() => {
248
+ document.addEventListener('click', handleClickOutside)
249
+ window.addEventListener('scroll', updatePosition, true)
250
+ window.addEventListener('resize', updatePosition)
251
+
252
+ if (props.openOnHover && triggerRef.value) {
253
+ triggerRef.value.addEventListener('mouseenter', handleMouseEnter)
254
+ triggerRef.value.addEventListener('mouseleave', handleMouseLeave)
255
+ }
256
+ })
257
+
258
+ onBeforeUnmount(() => {
259
+ document.removeEventListener('click', handleClickOutside)
260
+ window.removeEventListener('scroll', updatePosition, true)
261
+ window.removeEventListener('resize', updatePosition)
262
+
263
+ if (props.openOnHover && triggerRef.value) {
264
+ triggerRef.value.removeEventListener('mouseenter', handleMouseEnter)
265
+ triggerRef.value.removeEventListener('mouseleave', handleMouseLeave)
266
+ }
267
+ })
268
+
269
+ // Expose methods
270
+ defineExpose({ open, close, toggle })
271
+ </script>
272
+
273
+ <style scoped>
274
+ .animate-in {
275
+ animation-duration: 150ms;
276
+ animation-fill-mode: both;
277
+ }
278
+
279
+ .fade-in-0 {
280
+ animation-name: fadeIn;
281
+ }
282
+
283
+ .zoom-in-95 {
284
+ animation-name: zoomIn;
285
+ }
286
+
287
+ @keyframes fadeIn {
288
+ from { opacity: 0; }
289
+ to { opacity: 1; }
290
+ }
291
+
292
+ @keyframes zoomIn {
293
+ from { transform: scale(0.95); }
294
+ to { transform: scale(1); }
295
+ }
296
+ </style>
@@ -0,0 +1,40 @@
1
+ <template>
2
+ <div
3
+ @click="handleClick"
4
+ :class="itemClasses"
5
+ role="menuitem"
6
+ :tabindex="disabled ? -1 : 0"
7
+ >
8
+ <slot name="prepend" />
9
+ <span class="flex-1"><slot /></span>
10
+ <slot name="append" />
11
+ </div>
12
+ </template>
13
+
14
+ <script setup>
15
+ import { computed } from 'vue'
16
+
17
+ const props = defineProps({
18
+ disabled: { type: Boolean, default: false },
19
+ active: { type: Boolean, default: false },
20
+ dense: { type: Boolean, default: false },
21
+ })
22
+
23
+ const emit = defineEmits(['select'])
24
+
25
+ const itemClasses = computed(() => [
26
+ 'relative flex cursor-pointer select-none items-center gap-2',
27
+ 'rounded-sm px-2 outline-none transition-colors',
28
+ props.disabled
29
+ ? 'pointer-events-none opacity-50'
30
+ : 'hover:bg-gray-100 hover:text-gray-900 focus:bg-gray-100 focus:text-gray-900',
31
+ props.active ? 'bg-accent text-accent-foreground' : '',
32
+ props.dense ? 'py-1 text-sm' : 'py-2 text-sm'
33
+ ])
34
+
35
+ const handleClick = () => {
36
+ if (!props.disabled) {
37
+ emit('select')
38
+ }
39
+ }
40
+ </script>