pgo-uiux2 1.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.env +1 -0
- package/.env.production +1 -0
- package/.prettierrc +13 -0
- package/.vscode/extensions.json +3 -0
- package/BUTTON_GUIDE.md +257 -0
- package/README.md +49 -0
- package/THEME_REFERENCE.md +310 -0
- package/eslint.config.ts +27 -0
- package/index.html +13 -0
- package/package.json +85 -0
- package/public/favicon.ico +0 -0
- package/src/App.vue +368 -0
- package/src/assets/fonts/Faruma.ttf +0 -0
- package/src/components/examples/AppBarExample.vue +101 -0
- package/src/components/examples/AvatarExample.vue +47 -0
- package/src/components/examples/BannerExample.vue +287 -0
- package/src/components/examples/BaseInputExample.vue +25 -0
- package/src/components/examples/BreadcrumbExample.vue +53 -0
- package/src/components/examples/CardExample.vue +77 -0
- package/src/components/examples/ChipExample.vue +225 -0
- package/src/components/examples/DatePickerExample.vue +31 -0
- package/src/components/examples/DropdownExample.vue +84 -0
- package/src/components/examples/EditorExample.vue +200 -0
- package/src/components/examples/ExpansionPanelExample.vue +42 -0
- package/src/components/examples/FileUploadExample.vue +40 -0
- package/src/components/examples/FormExample.vue +121 -0
- package/src/components/examples/HugeTest.vue +8 -0
- package/src/components/examples/LayoutContainerExample.vue +80 -0
- package/src/components/examples/ModalExample.vue +82 -0
- package/src/components/examples/NavDrawerExample.vue +170 -0
- package/src/components/examples/NumberFieldExample.vue +145 -0
- package/src/components/examples/RadioButtonExample.vue +161 -0
- package/src/components/examples/SearchExample.vue +322 -0
- package/src/components/examples/SelectExample.vue +121 -0
- package/src/components/examples/StackedTableViewExample.vue +53 -0
- package/src/components/examples/TabExample.vue +336 -0
- package/src/components/examples/TableExample.vue +228 -0
- package/src/components/examples/TextFieldExample.vue +181 -0
- package/src/components/examples/TextareaExample.vue +173 -0
- package/src/components/examples/ThemeToggle.vue +50 -0
- package/src/components/examples/TimelineExample.vue +66 -0
- package/src/components/examples/TipTapEditorExample.vue +20 -0
- package/src/components/examples/TooltipExample.vue +53 -0
- package/src/components/examples/VueDatePickerShowcase.vue +214 -0
- package/src/components/examples/_DatePickerExample.vue +33 -0
- package/src/components/examples/__FormExample.vue +77 -0
- package/src/components/index.ts +25 -0
- package/src/components/pgo/AppBar.vue +347 -0
- package/src/components/pgo/Avatar.vue +139 -0
- package/src/components/pgo/Banner.vue +300 -0
- package/src/components/pgo/Breadcrumb.vue +101 -0
- package/src/components/pgo/Button.vue +171 -0
- package/src/components/pgo/Card.vue +178 -0
- package/src/components/pgo/ConfirmationModel.vue +32 -0
- package/src/components/pgo/DataTable.vue +845 -0
- package/src/components/pgo/DatePicker/CalendarPanel.vue +43 -0
- package/src/components/pgo/DatePicker/__DatePicker.vue +122 -0
- package/src/components/pgo/DatePicker/types.ts +11 -0
- package/src/components/pgo/DatePicker/useCalendar.ts +39 -0
- package/src/components/pgo/DatePicker/useDatePicker.ts +31 -0
- package/src/components/pgo/Deprecated/ToastContainer.vue +51 -0
- package/src/components/pgo/Deprecated/ToastItem.vue +55 -0
- package/src/components/pgo/Dropdown.vue +296 -0
- package/src/components/pgo/DropdownItem.vue +40 -0
- package/src/components/pgo/Editor.vue +511 -0
- package/src/components/pgo/ExpansionPanel.vue +185 -0
- package/src/components/pgo/Footer.vue +39 -0
- package/src/components/pgo/HeroIcon.vue +124 -0
- package/src/components/pgo/InputSearch.vue +194 -0
- package/src/components/pgo/LayoutContainer.vue +104 -0
- package/src/components/pgo/Main.vue +37 -0
- package/src/components/pgo/Modal.vue +273 -0
- package/src/components/pgo/NavDrawer.vue +127 -0
- package/src/components/pgo/NavDrawerItem.vue +161 -0
- package/src/components/pgo/NavigationDrawer.vue +849 -0
- package/src/components/pgo/OLDNavDrawer.vue +661 -0
- package/src/components/pgo/OldAppBar.vue +223 -0
- package/src/components/pgo/PApp.vue +102 -0
- package/src/components/pgo/Pagination.vue +242 -0
- package/src/components/pgo/Search copy.vue +310 -0
- package/src/components/pgo/Search.vue +411 -0
- package/src/components/pgo/StackedTableView.vue +167 -0
- package/src/components/pgo/Tab.vue +617 -0
- package/src/components/pgo/TestInput.vue +395 -0
- package/src/components/pgo/Timeline.vue +367 -0
- package/src/components/pgo/TimelineItem.vue +80 -0
- package/src/components/pgo/TipTapEditor.vue +315 -0
- package/src/components/pgo/Tooltip.NOTES.md +12 -0
- package/src/components/pgo/Tooltip.PROPS.md +21 -0
- package/src/components/pgo/Tooltip.vue +281 -0
- package/src/components/pgo/base/Base.vue +444 -0
- package/src/components/pgo/buttons/Chip.vue +324 -0
- package/src/components/pgo/buttons/ChipGroup.vue +224 -0
- package/src/components/pgo/buttons/Radio.vue +424 -0
- package/src/components/pgo/filters/FilterSection.vue +188 -0
- package/src/components/pgo/filters/Searchbar.vue +216 -0
- package/src/components/pgo/forms/DynamicForm.vue +45 -0
- package/src/components/pgo/forms/Form.vue +132 -0
- package/src/components/pgo/index.ts +15 -0
- package/src/components/pgo/inputs/Checkbox.vue +320 -0
- package/src/components/pgo/inputs/DatePicker.vue +395 -0
- package/src/components/pgo/inputs/FileUpload.vue +326 -0
- package/src/components/pgo/inputs/NumberField.vue +243 -0
- package/src/components/pgo/inputs/Radio.vue +162 -0
- package/src/components/pgo/inputs/RadioGroup.vue +188 -0
- package/src/components/pgo/inputs/Select.vue +535 -0
- package/src/components/pgo/inputs/TextField.vue +194 -0
- package/src/components/pgo/inputs/Textarea.vue +181 -0
- package/src/main.js +12 -0
- package/src/pgo-components/_index.js +31 -0
- package/src/pgo-components/assets/fonts/Faruma.ttf +0 -0
- package/src/pgo-components/assets/fonts/logo.png +0 -0
- package/src/pgo-components/composables/useTheme.js +10 -0
- package/src/pgo-components/directives/tooltip-directive.ts +393 -0
- package/src/pgo-components/index.js +96 -0
- package/src/pgo-components/lib/componentConfig.js +147 -0
- package/src/pgo-components/lib/core/composables/_useCalendar.ts +127 -0
- package/src/pgo-components/lib/core/composables/useDefaults.ts +15 -0
- package/src/pgo-components/lib/core/composables/useLanguageSelect.js +0 -0
- package/src/pgo-components/lib/core/composables/useRtl.ts +12 -0
- package/src/pgo-components/lib/core/defaults/createDefaults.ts +5 -0
- package/src/pgo-components/lib/core/defaults/defaults.ts +7 -0
- package/src/pgo-components/lib/core/rtl/rtl.ts +3 -0
- package/src/pgo-components/lib/core/rtl/setRtl.ts +19 -0
- package/src/pgo-components/lib/drawerState.ts +3 -0
- package/src/pgo-components/lib/i18n/defaultLables.js +71 -0
- package/src/pgo-components/lib/i18n/i18nPlugin.js +52 -0
- package/src/pgo-components/lib/i18n/useI18n.js +35 -0
- package/src/pgo-components/lib/index.ts +38 -0
- package/src/pgo-components/pages/Component.vue +7 -0
- package/src/pgo-components/pages/ComponentRenderer.vue +85 -0
- package/src/pgo-components/pages/Home.vue +130 -0
- package/src/pgo-components/pages/ListView.vue +370 -0
- package/src/pgo-components/pages/Page1.vue +296 -0
- package/src/pgo-components/pages/_Page1.vue +180 -0
- package/src/pgo-components/plugins/SnackBar.vue +251 -0
- package/src/pgo-components/plugins/SnackBarContainer.vue +53 -0
- package/src/pgo-components/plugins/SnackBarPlugin.ts +136 -0
- package/src/pgo-components/plugins/theme-plugin.js +114 -0
- package/src/pgo-components/plugins/types.ts +46 -0
- package/src/pgo-components/plugins/useSnackBar.js +11 -0
- package/src/pgo-components/plugins/useSnackBar.ts +21 -0
- package/src/pgo-components/plugins/validation-plugin.js +11 -0
- package/src/pgo-components/services/Entry.json +813 -0
- package/src/pgo-components/services/axios.js +54 -0
- package/src/pgo-components/services/data.json +90 -0
- package/src/pgo-components/services/person.json +260 -0
- package/src/pgo-components/services/toast.ts +44 -0
- package/src/pgo-components/styles/global.css +234 -0
- package/src/pgo-components/styles/reset.css +96 -0
- package/src/pgo-components/styles/tokens.css +18 -0
- package/src/pgo-components/styles/utilities/border-radius.css +57 -0
- package/src/pgo-components/styles/utilities/borders.css +85 -0
- package/src/pgo-components/styles/utilities/colors.css +38 -0
- package/src/pgo-components/styles/utilities/cursor.css +19 -0
- package/src/pgo-components/styles/utilities/display.css +78 -0
- package/src/pgo-components/styles/utilities/elevation.css +33 -0
- package/src/pgo-components/styles/utilities/flex.css +403 -0
- package/src/pgo-components/styles/utilities/float.css +41 -0
- package/src/pgo-components/styles/utilities/hover.css +9 -0
- package/src/pgo-components/styles/utilities/index.css +18 -0
- package/src/pgo-components/styles/utilities/opacity.css +27 -0
- package/src/pgo-components/styles/utilities/overflow.css +26 -0
- package/src/pgo-components/styles/utilities/palette.css +515 -0
- package/src/pgo-components/styles/utilities/position.css +14 -0
- package/src/pgo-components/styles/utilities/sizing.css +70 -0
- package/src/pgo-components/styles/utilities/spacing.css +578 -0
- package/src/pgo-components/styles/utilities/transitions.css +58 -0
- package/src/pgo-components/styles/utilities/typography.css +91 -0
- package/src/pgo-components/styles/utilities/z-index.css +11 -0
- package/src/pgo-components/tokens/index.js +337 -0
- package/src/router/index.js +88 -0
- package/src/shims-vue.d.ts +14 -0
- package/src/validations/validationRules.js +50 -0
- package/tailwind.config.js +73 -0
- package/test.php +5 -0
- package/tsconfig.json +25 -0
- package/ui +31 -0
- package/ui.pgo.mv.conf +18 -0
- package/vite.config.js +42 -0
|
@@ -0,0 +1,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>
|