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,315 @@
1
+ <template>
2
+ <div class="tiptap-editor">
3
+ <div class="tiptap-editor-toolbar sticky-top" v-if="editor && editor.isEditable">
4
+ <!-- Toolbar buttons (similar to your original code) -->
5
+ <div v-for="(button, index) in buttons" :key="index" class="tiptap-editor-btn"
6
+ :v-tooltip="button.tooltip"
7
+ @click="button.action">
8
+ <v-icon small>{{ button.icon }}</v-icon>
9
+ </div>
10
+
11
+ <!-- More buttons here... -->
12
+
13
+ </div>
14
+ <div id="tiptap-html" v-if="viewHtml" contenteditable="true">{{ htmlContent }}</div>
15
+ <editor-content v-if="!viewHtml" class="tiptap-editor-content" :editor="editor" />
16
+
17
+ <div v-if="maximumLength !== null" class="tiptap-editor-footer">
18
+ <span :class="{ 'red--text': isLimitReached }">{{ currentCharCount }} / {{ maximumLength }}</span>
19
+ <span v-if="isLimitReached" class="red--text">Limit reached — editor disabled</span>
20
+ </div>
21
+ </div>
22
+ </template>
23
+
24
+ <script>
25
+ import { ref, watch, onMounted, onBeforeUnmount, computed } from 'vue';
26
+ import { Editor, EditorContent } from '@tiptap/vue-3';
27
+ import StarterKit from '@tiptap/starter-kit';
28
+ import Underline from '@tiptap/extension-underline';
29
+ import { TableRow } from '@tiptap/extension-table';
30
+ // import TableRow from '@tiptap/extension-table-row';
31
+ import { TableCell } from '@tiptap/extension-table';
32
+ // import TableCell from '@tiptap/extension-table-cell';
33
+ import { TableHeader } from '@tiptap/extension-table';
34
+ // import TableHeader from '@tiptap/extension-table-header';
35
+ // import Bdi from './extensions/bdi';
36
+ // import Indent from './extensions/indent';
37
+ // import Table from './extensions/customTable';
38
+ // import Toc from './extensions/toc';
39
+ // import CharacterCount from '@tiptap/extension-character-count';
40
+
41
+ export default {
42
+ components: { EditorContent },
43
+
44
+ props: {
45
+ value: {
46
+ type: String,
47
+ default: ''
48
+ },
49
+ maximumLength: {
50
+ type: Number,
51
+ default: null
52
+ }
53
+ },
54
+
55
+ setup(props, { emit }) {
56
+ // Reactive references
57
+ const editor = ref(null);
58
+ const viewHtml = ref(false);
59
+ const htmlContent = ref('');
60
+ const currentCharCount = ref(0);
61
+ const isLimitReached = computed(() => {
62
+ return props.maximumLength !== null && currentCharCount.value >= props.maximumLength;
63
+ });
64
+
65
+ // Watch value prop to update editor content
66
+ watch(() => props.value, (newValue) => {
67
+ if (editor.value && editor.value.getHTML() !== newValue) {
68
+ editor.value.commands.setContent(newValue, false);
69
+ }
70
+ });
71
+
72
+ // Methods
73
+ const setHtml = () => {
74
+ const tag = document.querySelector('#tiptap-html');
75
+ editor.value.commands.setContent(tag.innerHTML + '');
76
+ viewHtml.value = false;
77
+ };
78
+
79
+ // Initialize buttons
80
+ const buttons = ref([
81
+ { tooltip: 'Clear', icon: 'mdi-backspace-reverse-outline', action: () => editor.value.chain().focus().clearContent().run() },
82
+ { tooltip: 'Bold', icon: 'mdi-format-bold', action: () => editor.value.chain().focus().toggleBold().run() },
83
+ // More buttons here
84
+ ]);
85
+
86
+ // Setup editor instance on mounted
87
+ onMounted(() => {
88
+ // const charCountExt = props.maximumLength ? CharacterCount.configure({ limit: props.maximumLength }) : CharacterCount;
89
+ const charCountExt = 123;
90
+ editor.value = new Editor({
91
+ injectCSS: false,
92
+ parseOptions: { preserveWhitespace: true },
93
+ content: props.value,
94
+ extensions: [
95
+ StarterKit.configure({
96
+ horizontalRule: false,
97
+ blockquote: false,
98
+ hardBreak: false,
99
+ heading: false
100
+ }),
101
+ // Bdi,
102
+ // Indent,
103
+ // Table,
104
+ TableRow,
105
+ TableCell,
106
+ TableHeader,
107
+ // Underline,
108
+ // Toc,
109
+ charCountExt
110
+ ],
111
+ onUpdate: () => {
112
+ emit('input', editor.value.getHTML());
113
+ try {
114
+ const storage = editor.value.storage?.characterCount;
115
+ currentCharCount.value = storage?.characters || 0;
116
+ } catch (e) {
117
+ currentCharCount.value = editor.value.getText().length || 0;
118
+ }
119
+
120
+ if (props.maximumLength !== null) {
121
+ const reached = currentCharCount.value >= props.maximumLength;
122
+ if (reached && editor.value.isEditable) {
123
+ editor.value.setEditable(false);
124
+ emit('limitReached');
125
+ } else if (!reached && !editor.value.isEditable) {
126
+ editor.value.setEditable(true);
127
+ emit('limitOk');
128
+ }
129
+ }
130
+ }
131
+ });
132
+ });
133
+
134
+ // Cleanup on before unmount
135
+ onBeforeUnmount(() => {
136
+ if (editor.value) {
137
+ editor.value.destroy();
138
+ }
139
+ });
140
+
141
+ return {
142
+ editor,
143
+ viewHtml,
144
+ htmlContent,
145
+ currentCharCount,
146
+ isLimitReached,
147
+ buttons,
148
+ setHtml
149
+ };
150
+ }
151
+ };
152
+ </script>
153
+
154
+ <style>
155
+ .tiptap-editor {
156
+ border: 1px solid lightgrey;
157
+ /* border-radius: 4px; */
158
+ }
159
+ .tiptap-editor-toolbar.sticky-top {
160
+ position: sticky;
161
+ top: 0;
162
+ }
163
+ .tiptap-editor-toolbar {
164
+ display: flex;
165
+ gap: 2px;
166
+ background-color: lightgrey;
167
+ padding: 5px;
168
+ }
169
+ .tiptap-editor-btn {
170
+ border: 1px solid grey;
171
+ width: 30px;
172
+ text-align: center;
173
+ cursor: pointer;
174
+ }
175
+ .tiptap-editor-btn:hover {
176
+ background-color: #bbb;
177
+ }
178
+
179
+ .tiptap-editor-content {
180
+ padding: 5px;
181
+ background-color: #fafafa;
182
+ min-height: 40px;
183
+ /* text-align: justify; */
184
+ }
185
+ .ProseMirror:focus-visible {
186
+ outline: 0px solid transparent !important;
187
+ }
188
+ .tiptap-editor-content bdi {
189
+ background-color: lightgrey;
190
+ font-family: Roboto, sans-serif !important;
191
+ }
192
+ .flip {
193
+ transform: rotateY(180deg);
194
+ }
195
+ .tiptap-editor-btn.is-active {
196
+ border-color: rgb(0, 110, 255) !important;
197
+ box-shadow: inset 0 0 4px rgb(0, 110, 255);
198
+ }
199
+ .tiptap-editor-btn.is-active > * {
200
+ color: rgb(0, 110, 255);
201
+ }
202
+ </style>
203
+
204
+ <style lang="scss">
205
+ @for $i from 1 through 8 {
206
+ [data-indent='#{$i}'] {
207
+ $val: $i * 3rem;
208
+ padding-right: $val;
209
+ }
210
+
211
+ .ProseMirror {
212
+ font-family: faruma;
213
+ font-size: 16px;
214
+ background-color: white;
215
+
216
+ table {
217
+ border-collapse: collapse;
218
+ table-layout: auto;
219
+ width: 100%;
220
+ margin: 5px 0;
221
+ overflow: hidden;
222
+
223
+ td,
224
+ th {
225
+ min-width: 1em;
226
+ border: 1px dashed black;
227
+ padding: 3px 5px;
228
+ vertical-align: top;
229
+ box-sizing: border-box;
230
+ position: relative;
231
+ }
232
+
233
+ th {
234
+ font-weight: bold;
235
+ text-align: center;
236
+ background-color: #eee;
237
+ }
238
+
239
+ .selectedCell:after {
240
+ z-index: 2;
241
+ position: absolute;
242
+ content: '';
243
+ left: 0;
244
+ right: 0;
245
+ top: 0;
246
+ bottom: 0;
247
+ background: rgba(200, 200, 255, 0.4);
248
+ pointer-events: none;
249
+ }
250
+
251
+ .column-resize-handle {
252
+ position: absolute;
253
+ right: -2px;
254
+ top: 0;
255
+ bottom: -2px;
256
+ width: 4px;
257
+ background-color: #adf;
258
+ pointer-events: none;
259
+ }
260
+
261
+ p:last-of-type {
262
+ margin: 0;
263
+ }
264
+ }
265
+
266
+ table.border {
267
+ th,
268
+ td {
269
+ border: 1px solid #666;
270
+ }
271
+ }
272
+
273
+ ol {
274
+ list-style-type: none;
275
+ counter-reset: item;
276
+ margin: 0;
277
+ padding: 0;
278
+ }
279
+
280
+ ol > li {
281
+ display: table;
282
+ counter-increment: item;
283
+ margin-bottom: 0.6em;
284
+ }
285
+
286
+ ol > li:before {
287
+ content: counters(item, '.') '. ';
288
+ display: table-cell;
289
+ padding-right: 0.6em;
290
+ }
291
+
292
+ li ol > li {
293
+ margin: 0;
294
+ }
295
+
296
+ li ol > li:before {
297
+ content: counters(item, '.') ' ';
298
+ }
299
+ }
300
+
301
+ .resize-cursor {
302
+ cursor: ew-resize;
303
+ cursor: col-resize;
304
+ }
305
+
306
+ .tableWrapper {
307
+ padding: 10px 0;
308
+ overflow-x: auto;
309
+ }
310
+ .ProseMirror * {
311
+ white-space: pre-wrap;
312
+ word-wrap: break-word;
313
+ }
314
+ }
315
+ </style>
@@ -0,0 +1,12 @@
1
+ Architecture Notes
2
+
3
+ - Teleport: Tooltip content is rendered into `body` using Vue's `Teleport` to avoid overflow/clipping issues within parent containers.
4
+ - Positioning: Computes position from activator `getBoundingClientRect()` and accounts for page scroll. Only inline styles applied for `top/left/position/z-index` and arrow location.
5
+ - Triggers: Supports hover, focus, click via props `openOnHover`, `openOnFocus`, `openOnClick`. Manual control via `v-model:open` (i.e., `modelValue`).
6
+ - Controlled vs Uncontrolled: If `modelValue` is provided, component acts in controlled mode and emits `update:modelValue`. Otherwise uses internal state.
7
+ - RTL: Uses `globalRtl` reactive value; swaps left/right placement when RTL is enabled.
8
+ - Accessibility: Adds `role="tooltip"`, links activator with `aria-describedby`, and manages `aria-expanded`.
9
+ - Close Guards: Closes on `Escape` key and outside click; listeners installed on mount and removed on unmount.
10
+ - Theming: No hardcoded colors. Uses existing workspace CSS variables: `--vts-color-surfaceElevated`, `--vts-color-text`, `--vts-color-border`, `--vts-radius-sm`, `--vts-elevation-2`, and `--vts-z-tooltip`.
11
+ - SSR-safe: Avoids direct DOM usage during SSR; all DOM access happens in lifecycle hooks (`onMounted`) and guarded in computations.
12
+ - Tree-shakable: Minimal dependencies, no external UI libraries; component is standalone and uses `script setup` with TypeScript.
@@ -0,0 +1,21 @@
1
+ Props
2
+ - `modelValue: boolean | undefined` — controlled open state
3
+ - `openOnHover: boolean` — enable hover trigger (default true)
4
+ - `openOnFocus: boolean` — enable focus trigger (default true)
5
+ - `openOnClick: boolean` — enable click trigger (default false)
6
+ - `disabled: boolean` — disables tooltip
7
+ - `placement: 'top' | 'bottom' | 'left' | 'right'` — tooltip placement (RTL-aware for left/right)
8
+ - `offset: { x?: number; y?: number }` — pixel offsets (default { x: 0, y: 8 })
9
+ - `arrow: boolean` — show arrow indicator (default true)
10
+ - `showDelay: number` — delay (ms) before opening (default 80)
11
+ - `hideDelay: number` — delay (ms) before closing (default 80)
12
+ - `teleport: string` — Teleport target (default 'body')
13
+
14
+ Emits
15
+ - `update:modelValue` — when controlled open state changes
16
+ - `open` — fired after tooltip opened and positioned
17
+ - `close` — fired after tooltip closed
18
+
19
+ Slots
20
+ - `activator` — slot for trigger element, receives `{ open, close, isOpen }`
21
+ - default — tooltip content
@@ -0,0 +1,281 @@
1
+ <template>
2
+ <div
3
+ ref="activatorRef"
4
+ class="vts-d-inline-block"
5
+ @mouseenter="onMouseEnter"
6
+ @mouseleave="onMouseLeave"
7
+ @focusin="onFocusIn"
8
+ @focusout="onFocusOut"
9
+ @click="onClick"
10
+ :aria-describedby="tooltipId"
11
+ :aria-expanded="isOpen"
12
+ :aria-disabled="disabled ? 'true' : 'false'"
13
+ >
14
+ <slot name="activator" :props="activatorSlotProps"></slot>
15
+ </div>
16
+
17
+ <Teleport to="body">
18
+ <div
19
+ v-show="isOpen"
20
+ :id="tooltipId"
21
+ ref="tooltipRef"
22
+ class="vts-position-absolute vts-bg-surface-elevated vts-text vts-border vts-rounded-sm vts-elevation-2 vts-pt-2 vts-pb-2 vts-pl-3 vts-pr-3 vts-max-w-280"
23
+ role="tooltip"
24
+ :class="[placementClass]"
25
+ :style="tooltipStyles"
26
+ @keydown.esc.stop.prevent="close"
27
+ >
28
+ <div v-if="arrow" class="vts-position-absolute vts-bg-surface-elevated vts-border-t vts-border-s vts-border-color" :class="[placementClass]" :style="arrowStyles"></div>
29
+ <div class="vts-text-body-2">
30
+ <slot />
31
+ </div>
32
+ </div>
33
+ </Teleport>
34
+ </template>
35
+
36
+ <script setup lang="ts">
37
+ import { ref, computed, watch, onMounted, onBeforeUnmount, nextTick } from 'vue'
38
+ import { globalRtl } from '../../pgo-components/lib/core/rtl/rtl'
39
+
40
+ // Props
41
+ interface Offset {
42
+ x?: number
43
+ y?: number
44
+ }
45
+
46
+ type Trigger = 'hover' | 'focus' | 'click' | 'manual'
47
+
48
+ type PlacementBase = 'top' | 'bottom' | 'left' | 'right'
49
+
50
+
51
+ const props = defineProps({
52
+ modelValue: { type: Boolean, default: undefined }, // controlled mode
53
+ openOnHover: { type: Boolean, default: true },
54
+ openOnFocus: { type: Boolean, default: true },
55
+ openOnClick: { type: Boolean, default: false },
56
+ disabled: { type: Boolean, default: false },
57
+ placement: { type: String as () => PlacementBase, default: 'top' },
58
+ offset: { type: Object as () => Offset, default: () => ({ x: 0, y: 0 }) },
59
+ arrow: { type: Boolean, default: true },
60
+ showDelay: { type: Number, default: 80 },
61
+ hideDelay: { type: Number, default: 80 },
62
+ teleport: { type: String, default: 'body' }
63
+ })
64
+
65
+ const emit = defineEmits(['update:modelValue', 'open', 'close'])
66
+
67
+ // State
68
+ const isControlled = computed(() => props.modelValue !== undefined)
69
+ const internalOpen = ref(false)
70
+ const isOpen = computed({
71
+ get: () => (isControlled.value ? !!props.modelValue : internalOpen.value),
72
+ set: (val: boolean) => {
73
+ if (props.disabled) return
74
+ if (isControlled.value) emit('update:modelValue', val)
75
+ else internalOpen.value = val
76
+ }
77
+ })
78
+
79
+ // Elements
80
+ const activatorRef = ref<HTMLElement | null>(null)
81
+ const tooltipRef = ref<HTMLElement | null>(null)
82
+
83
+ // IDs
84
+ const tooltipId = `tooltip-${Math.random().toString(36).slice(2, 10)}`
85
+
86
+ // RTL
87
+ const isRtl = computed(() => globalRtl.value)
88
+
89
+ // Placement with RTL adjustment for left/right
90
+ const normalizedPlacement = computed<PlacementBase>(() => {
91
+ if (!isRtl.value) return props.placement
92
+ if (props.placement === 'left') return 'right'
93
+ if (props.placement === 'right') return 'left'
94
+ return props.placement
95
+ })
96
+
97
+ const placementClass = computed(() => `placement-${normalizedPlacement.value}`)
98
+
99
+ // Positioning
100
+ const tooltipStyles = ref<Record<string, string>>({})
101
+ const arrowStyles = ref<Record<string, string>>({})
102
+ const hasPositioned = ref(false)
103
+
104
+ // Track how tooltip was opened to coordinate closing behavior
105
+ type TriggerSource = 'hover' | 'focus' | 'click' | 'manual' | null
106
+ const lastTrigger = ref<TriggerSource>(null)
107
+
108
+ function computePosition() {
109
+ const activator = activatorRef.value
110
+ const tooltip = tooltipRef.value
111
+ if (!activator || !tooltip) return
112
+ // Ensure the tooltip can be measured on first render
113
+ if (!hasPositioned.value) {
114
+ tooltipStyles.value = {
115
+ position: 'absolute',
116
+ top: '-9999px',
117
+ left: '-9999px',
118
+ visibility: 'hidden',
119
+ zIndex: 'var(--vts-z-tooltip, 1000)'
120
+ }
121
+ // Force a reflow so getBoundingClientRect has correct size
122
+ void tooltip.offsetHeight
123
+ }
124
+
125
+ const rect = activator.getBoundingClientRect()
126
+ const tooltipRect = tooltip.getBoundingClientRect()
127
+
128
+ const scrollX = window.scrollX || window.pageXOffset
129
+ const scrollY = window.scrollY || window.pageYOffset
130
+
131
+ let top = 0
132
+ let left = 0
133
+
134
+ const offsetX = props.offset?.x ?? 0
135
+ const offsetY = props.offset?.y ?? 0
136
+ const gap = props.arrow ? 8 : 0
137
+
138
+ switch (normalizedPlacement.value) {
139
+ case 'top':
140
+ top = rect.top + scrollY - tooltipRect.height - gap - offsetY
141
+ left = rect.left + scrollX + rect.width / 2 - tooltipRect.width / 2 + offsetX
142
+ break
143
+ case 'bottom':
144
+ top = rect.bottom + scrollY + gap + offsetY
145
+ left = rect.left + scrollX + rect.width / 2 - tooltipRect.width / 2 + offsetX
146
+ break
147
+ case 'left':
148
+ top = rect.top + scrollY + rect.height / 2 - tooltipRect.height / 2 + offsetY
149
+ left = rect.left + scrollX - tooltipRect.width - gap - offsetX
150
+ break
151
+ case 'right':
152
+ top = rect.top + scrollY + rect.height / 2 - tooltipRect.height / 2 + offsetY
153
+ left = rect.right + scrollX + gap + offsetX
154
+ break
155
+ }
156
+
157
+ tooltipStyles.value = {
158
+ position: 'absolute',
159
+ top: `${top}px`,
160
+ left: `${left}px`,
161
+ zIndex: 'var(--vts-z-tooltip, 1000)',
162
+ visibility: ''
163
+ }
164
+
165
+ // Arrow positioning
166
+ if (props.arrow) {
167
+ const size = 8
168
+ const common = { width: `${size}px`, height: `${size}px` }
169
+ switch (normalizedPlacement.value) {
170
+ case 'top':
171
+ arrowStyles.value = { ...common, bottom: `-4px`, left: '50%', transform: 'translateX(-50%) rotate(45deg)' }
172
+ break
173
+ case 'bottom':
174
+ arrowStyles.value = { ...common, top: `-4px`, left: '50%', transform: 'translateX(-50%) rotate(45deg)' }
175
+ break
176
+ case 'left':
177
+ arrowStyles.value = { ...common, right: `-4px`, top: '50%', transform: 'translateY(-50%) rotate(45deg)' }
178
+ break
179
+ case 'right':
180
+ arrowStyles.value = { ...common, left: `-4px`, top: '50%', transform: 'translateY(-50%) rotate(45deg)' }
181
+ break
182
+ }
183
+ }
184
+
185
+ hasPositioned.value = true
186
+ }
187
+
188
+ // Open/close with delays
189
+ let showTimer: number | null = null
190
+ let hideTimer: number | null = null
191
+
192
+ function clearTimers() {
193
+ if (showTimer) {
194
+ window.clearTimeout(showTimer)
195
+ showTimer = null
196
+ }
197
+ if (hideTimer) {
198
+ window.clearTimeout(hideTimer)
199
+ hideTimer = null
200
+ }
201
+ }
202
+
203
+ function open(source: TriggerSource = null) {
204
+ if (props.disabled) return
205
+ clearTimers()
206
+ showTimer = window.setTimeout(async () => {
207
+ lastTrigger.value = source
208
+ isOpen.value = true
209
+ await nextTick()
210
+ computePosition()
211
+ emit('open')
212
+ }, props.showDelay)
213
+ }
214
+
215
+ function close() {
216
+ clearTimers()
217
+ hideTimer = window.setTimeout(() => {
218
+ isOpen.value = false
219
+ lastTrigger.value = null
220
+ hasPositioned.value = false
221
+ emit('close')
222
+ }, props.hideDelay)
223
+ }
224
+
225
+ // Triggers
226
+ const activatorSlotProps = computed(() => ({
227
+ open: (src?: TriggerSource) => open(src ?? 'manual'),
228
+ close,
229
+ isOpen: isOpen.value
230
+ }))
231
+
232
+ function onMouseEnter() {
233
+ if (props.openOnHover) open('hover')
234
+ }
235
+ function onMouseLeave() {
236
+ if (props.openOnHover) close()
237
+ }
238
+ function onFocusIn() {
239
+ if (props.openOnFocus) open('focus')
240
+ }
241
+ function onFocusOut() {
242
+ // Avoid immediate close on click-first interaction when openOnClick is enabled
243
+ if (props.openOnFocus && lastTrigger.value === 'focus') close()
244
+ }
245
+ function onClick() {
246
+ if (!props.openOnClick) return
247
+ if (isOpen.value) close()
248
+ else open('click')
249
+ }
250
+
251
+ // Outside click & escape
252
+ function onDocumentClick(e: MouseEvent) {
253
+ if (!isOpen.value) return
254
+ const target = e.target as Node
255
+ if (tooltipRef.value && tooltipRef.value.contains(target)) return
256
+ if (activatorRef.value && activatorRef.value.contains(target)) return
257
+ close()
258
+ }
259
+
260
+ function onDocumentKeydown(e: KeyboardEvent) {
261
+ if (e.key === 'Escape') close()
262
+ }
263
+
264
+ onMounted(() => {
265
+ computePosition()
266
+ document.addEventListener('click', onDocumentClick, { passive: true })
267
+ document.addEventListener('keydown', onDocumentKeydown)
268
+ })
269
+
270
+ onBeforeUnmount(() => {
271
+ document.removeEventListener('click', onDocumentClick)
272
+ document.removeEventListener('keydown', onDocumentKeydown)
273
+ })
274
+
275
+ watch(isOpen, val => {
276
+ if (val) nextTick(computePosition)
277
+ })
278
+ </script>
279
+
280
+ <style scoped>
281
+ </style>