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,845 @@
1
+ <template>
2
+ <div :class="['data-table-container', loading ? 'opacity-50 pointer-events-none relative' : '']">
3
+ <!-- Header with title and actions -->
4
+ <div v-if="title || $slots.header" class="data-table-header mb-6">
5
+ <div class="flex justify-between items-center">
6
+ <div>
7
+ <h2 v-if="title" :class="[headerClass || 'text-2xl font-semibold text-gray-800']">{{ title }}</h2>
8
+ <slot name="header" />
9
+ </div>
10
+ <div class="flex items-center gap-3">
11
+ <!-- Search -->
12
+ <div v-if="searchable" class="search-container">
13
+ <div class="relative">
14
+ <input
15
+ v-model="searchQuery"
16
+ type="text"
17
+ :placeholder="searchPlaceholder"
18
+ :class="[
19
+ 'pl-10 pr-4 py-2 rounded-lg w-64 text-sm transition-colors focus:outline-none focus:ring-2 focus:ring-offset-0',
20
+ inputBorder
21
+ ]"
22
+ @input="debounceSearch"
23
+ />
24
+ <div class="absolute left-3 top-1/2 transform -translate-y-1/2">
25
+ <svg class="w-4 h-4 text-gray-400" fill="none" stroke="currentColor" viewBox="0 0 24 24">
26
+ <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M21 21l-6-6m2-5a7 7 0 11-14 0 7 7 0 0114 0z" />
27
+ </svg>
28
+ </div>
29
+ </div>
30
+ </div>
31
+ <slot name="header-actions" />
32
+ </div>
33
+ </div>
34
+ </div>
35
+
36
+ <!-- Table Container -->
37
+ <div :class="['w-full', tableClass]">
38
+ <div :class="[rounded, border, shadow, bg, 'overflow-hidden', 'relative']">
39
+ <!-- Loading Bar -->
40
+ <div class="absolute top-0 left-0 right-0 z-10 h-1 overflow-hidden">
41
+ <div v-if="loading" class="bg-gray-200 w-full">
42
+ <div class="h-full bg-primary animate-loading-bar"></div>
43
+ </div>
44
+ </div>
45
+
46
+ <table class="w-full caption-bottom text-sm">
47
+ <!-- Table Header -->
48
+ <thead :class="[headerBg]">
49
+ <tr :class="[headerBorder, 'transition-colors']">
50
+ <!-- Selection checkbox -->
51
+ <th v-if="selectable" :class="['h-10 px-3 text-left align-middle font-medium', headerText, 'w-12']">
52
+ <div class="flex items-center justify-center">
53
+ <input
54
+ type="checkbox"
55
+ :checked="allSelected"
56
+ :indeterminate.prop="someSelected"
57
+ class="h-4 w-4 rounded border border-gray-200 text-primary focus:ring-2 focus:ring-primary-500 focus:ring-offset-0"
58
+ @change="toggleSelectAll"
59
+ />
60
+ </div>
61
+ </th>
62
+ <!-- Column Headers -->
63
+ <th
64
+ v-for="header in headers"
65
+ :key="header.value"
66
+ :class="[
67
+ 'h-10 px-3 text-left align-middle font-medium cursor-pointer select-none transition-colors',
68
+ headerText,
69
+ getColumnFont(header.lang),
70
+ 'hover:bg-input-hover-border',
71
+ { 'bg-surface': isSorted(header.value) }
72
+ ]"
73
+ @click="header.sortable !== false ? toggleSort(header.value) : null"
74
+ >
75
+ <div class="flex items-center space-x-2">
76
+ <span class="whitespace-nowrap">{{ header.title }}</span>
77
+ <div v-if="header.sortable !== false" class="flex h-4 w-4 items-center justify-center">
78
+ <!-- Unsorted state -->
79
+ <svg
80
+ v-if="!isSorted(header.value)"
81
+ class="h-4 w-4 text-gray-400"
82
+ fill="none"
83
+ stroke="currentColor"
84
+ viewBox="0 0 24 24"
85
+ >
86
+ <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M7 16V4m0 0L3 8m4-4l4 4m6 0v12m0 0l4-4m-4 4l-4-4" />
87
+ </svg>
88
+ <!-- Ascending -->
89
+ <svg
90
+ v-else-if="getSortDirection(header.value) === 'asc'"
91
+ class="h-4 w-4 text-blue-600"
92
+ fill="none"
93
+ stroke="currentColor"
94
+ viewBox="0 0 24 24"
95
+ >
96
+ <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M5 15l7-7 7 7" />
97
+ </svg>
98
+ <!-- Descending -->
99
+ <svg
100
+ v-else
101
+ class="h-4 w-4 text-blue-600"
102
+ fill="none"
103
+ stroke="currentColor"
104
+ viewBox="0 0 24 24"
105
+ >
106
+ <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M19 9l-7 7-7-7" />
107
+ </svg>
108
+ </div>
109
+ </div>
110
+ </th>
111
+ <!-- Actions column -->
112
+ <th v-if="$slots['item-actions'] || showActions" :class="['h-10 px-3 text-end align-middle font-medium', headerText]">
113
+ <Button
114
+ v-if="inlineEdit"
115
+ :label="editMode ? 'Cancel Edit' : 'Inline Edit'"
116
+ :color="editMode ? 'danger' : 'primary'"
117
+ size="xs"
118
+ @click="toggleEditMode"
119
+ />
120
+ </th>
121
+ </tr>
122
+ </thead>
123
+
124
+ <!-- Table Body -->
125
+ <tbody :class="[rowBorder, loading ? 'opacity-50 pointer-events-none' : '']">
126
+ <!-- No Data State -->
127
+ <tr v-if="!items || items.length === 0">
128
+ <td :colspan="totalColumns" class="px-6 py-12 text-center">
129
+ <div class="flex flex-col items-center">
130
+ <svg class="w-12 h-12 text-gray-300 mb-2" fill="none" stroke="currentColor" viewBox="0 0 24 24">
131
+ <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M9 12h6m-6 4h6m2 5H7a2 2 0 01-2-2V5a2 2 0 012-2h5.586a1 1 0 01.707.293l5.414 5.414a1 1 0 01.293.707V19a2 2 0 01-2 2z" />
132
+ </svg>
133
+ <span class="text-sm text-gray-500">{{ noDataText }}</span>
134
+ </div>
135
+ </td>
136
+ </tr>
137
+
138
+ <!-- Data Rows -->
139
+ <tr
140
+ v-else
141
+ v-for="(item, index) in items"
142
+ :key="getItemKey(item, index)"
143
+ :class="[
144
+ 'transition-colors cursor-pointer',
145
+ rowHover,
146
+ selectedItems.includes(getItemKey(item, index)) ? rowSelected : ''
147
+ ]"
148
+ @click="handleRowClick(item, index)"
149
+ >
150
+ <!-- Selection checkbox -->
151
+ <td v-if="selectable" class="px-6 py-4">
152
+ <div class="flex items-center justify-center">
153
+ <input
154
+ type="checkbox"
155
+ :checked="selectedItems.includes(getItemKey(item, index))"
156
+ class="h-4 w-4 rounded border-2 border-gray-300 text-blue-600 focus:ring-2 focus:ring-blue-500 focus:ring-offset-0"
157
+ @click.stop
158
+ @change="toggleItemSelection(item, index)"
159
+ />
160
+ </div>
161
+ </td>
162
+
163
+ <!-- Data columns -->
164
+ <template v-for="header in headers" :key="header.value">
165
+ <td
166
+ :class="[
167
+ 'px-6 py-4 whitespace-nowrap',
168
+ header.displayType == 'englishText' ? 'eng-font' : 'faruma ',
169
+ getColumnFont(header.lang)
170
+ ]"
171
+ @click.stop
172
+ >
173
+ <slot
174
+ :name="`item.${header.value}`"
175
+ :item="item"
176
+ :value="getNestedValue(item, header.value)"
177
+ :header="header"
178
+ :index="index"
179
+ >
180
+ <!-- Chip Display -->
181
+ <div v-if="header.displayType == 'chip'"
182
+ :class="['align-middle leading-none', getColumnFont(header.lang)]"
183
+ >
184
+ <Chip
185
+ size="small"
186
+ v-bind="header.displayProps?.[getNestedValue(item, header.value)]"
187
+ />
188
+ </div>
189
+
190
+ <!-- Checkbox (Inline Editable) -->
191
+ <div v-else-if="header.displayType == 'checkbox'"
192
+ :class="['align-middle leading-none', getColumnFont(header.lang)]"
193
+ >
194
+ <Checkbox
195
+ :model-value="!!getNestedValue(item, header.value)"
196
+ v-bind="header.displayProps"
197
+ :disabled="!editMode || !header.inlineEditable"
198
+ @update:model-value="(value) => handleCellUpdate(item, header.value, value, index)"
199
+ />
200
+ </div>
201
+
202
+ <!-- Select (Inline Editable) -->
203
+ <div v-else-if="header.displayType == 'select'"
204
+ :class="['align-middle leading-none', getColumnFont(header.lang)]"
205
+ >
206
+ <Select
207
+ v-if="editMode && header.inlineEditable"
208
+ :model-value="getNestedValue(item, header.value)"
209
+ v-bind="header.displayProps"
210
+ @update:model-value="(value) => handleCellUpdate(item, header.value, value, index)"
211
+ />
212
+ <div v-else :class="['text-sm', cellText]">
213
+ {{ formatCellValue(getNestedValue(item, header.value), header) }}
214
+ </div>
215
+ </div>
216
+
217
+ <!-- Text Input (Inline Editable) -->
218
+ <div v-else-if="header.displayType == 'input'"
219
+ :class="['align-middle leading-none', getColumnFont(header.lang)]"
220
+ >
221
+ <input
222
+ v-if="editMode && header.inlineEditable"
223
+ :value="getNestedValue(item, header.value)"
224
+ type="text"
225
+ class="w-full px-2 py-1 text-sm border rounded focus:outline-none focus:ring-2 focus:ring-primary"
226
+ @input="(e) => handleCellUpdate(item, header.value, e.target.value, index)"
227
+ />
228
+ <div v-else :class="['text-sm', cellText]">
229
+ {{ formatCellValue(getNestedValue(item, header.value), header) }}
230
+ </div>
231
+ </div>
232
+
233
+ <!-- Default Text Display -->
234
+ <div v-else :class="['text-sm', cellText]">
235
+ {{ formatCellValue(getNestedValue(item, header.value), header) }}
236
+ </div>
237
+ </slot>
238
+ </td>
239
+ </template>
240
+
241
+ <!-- Actions -->
242
+ <td v-if="$slots['item-actions'] || showActions" class="px-6 py-4">
243
+ <slot name="item-actions" :item="item" :index="index">
244
+ <div class="flex items-center justify-end space-x-2">
245
+ <Button v-if="showView" @click.stop="emit('view', item)" size="md" color="info" variant="text" icon="eye" icon-type="outline" label="" />
246
+ <Button v-if="showEdit" @click.stop="emit('edit', item)" size="md" icon="pencil-square" color="warning" variant="text" label="" />
247
+ <Button v-if="showDelete" @click.stop="emit('delete', item)" size="md" color="danger" variant="text" icon="trash" label="" />
248
+ </div>
249
+ </slot>
250
+ </td>
251
+ </tr>
252
+ </tbody>
253
+ </table>
254
+ </div>
255
+
256
+ <!-- Pagination Component -->
257
+ <Pagination
258
+ v-if="!hidePagination && !loading"
259
+ v-model:page="internalOptions.page"
260
+ v-model:items-per-page="internalOptions.itemsPerPage"
261
+ :items-length="internalOptions.itemsLength"
262
+ :items-per-page-options="itemsPerPageOptions"
263
+ :bg="paginationBg"
264
+ :border="paginationBorder"
265
+ :text-color="'text-textsecondary'"
266
+ :input-border="inputBorder"
267
+ :button-primary="buttonPrimary"
268
+ :button-secondary="buttonSecondary"
269
+ @change="handlePaginationChange"
270
+ />
271
+ </div>
272
+
273
+ <!-- Footer slot -->
274
+ <div v-if="$slots.footer" class="mt-4">
275
+ <slot name="footer" />
276
+ </div>
277
+
278
+ <!-- Confirmation Modal -->
279
+ <Model
280
+ v-model="showConfirmation"
281
+ persistent
282
+ lang="en"
283
+
284
+ title="Confirm Update"
285
+ >
286
+ <p class="text-sm text-gray-600 mb-6">
287
+ Are you sure you want to update <strong>{{ pendingUpdate.header }}</strong>
288
+ from <strong>{{ pendingUpdate.oldValue }}</strong> to <strong>{{ pendingUpdate.newValue }}</strong>?
289
+ </p>
290
+ <template #footer>
291
+ <div class="flex justify-end gap-2">
292
+ <Button
293
+ label="Cancel"
294
+ color="secondary"
295
+ variant="outlined"
296
+ @click="cancelUpdate"
297
+ />
298
+ <Button
299
+ label="Confirm"
300
+ color="primary"
301
+ :loading="updateLoading"
302
+ @click="confirmUpdate"
303
+ />
304
+ </div>
305
+ </template>
306
+ </Model>
307
+ </div>
308
+ </template>
309
+
310
+ <script setup>
311
+ import { ref, computed, watch, onMounted, inject } from 'vue'
312
+ import Button from '../pgo/Button.vue'
313
+ import Pagination from '../pgo/Pagination.vue'
314
+ import Chip from '../pgo/buttons/Chip.vue'
315
+ import Checkbox from '../pgo/inputs/Checkbox.vue'
316
+ import Select from '../pgo/inputs/Select.vue'
317
+ import Model from '../pgo/Modal.vue'
318
+
319
+ const props = defineProps({
320
+ // Data
321
+ items: {
322
+ type: Array,
323
+ default: () => []
324
+ },
325
+ headers: {
326
+ type: Array,
327
+ required: true
328
+ },
329
+
330
+ // Server-side options
331
+ serverSideOptions: {
332
+ type: Object,
333
+ default: () => ({
334
+ page: 1,
335
+ itemsPerPage: 10,
336
+ sortBy: [],
337
+ sortDesc: [],
338
+ itemsLength: 0
339
+ })
340
+ },
341
+
342
+ // Table configuration
343
+ title: {
344
+ type: String,
345
+ default: ''
346
+ },
347
+ loading: {
348
+ type: Boolean,
349
+ default: false
350
+ },
351
+ loadingText: {
352
+ type: String,
353
+ default: 'Loading...'
354
+ },
355
+ noDataText: {
356
+ type: String,
357
+ default: 'No data available'
358
+ },
359
+
360
+ // Features
361
+ showActions: {
362
+ type: Boolean,
363
+ default: true
364
+ },
365
+ searchable: {
366
+ type: Boolean,
367
+ default: false
368
+ },
369
+ searchPlaceholder: {
370
+ type: String,
371
+ default: 'Search...'
372
+ },
373
+ showView: {
374
+ type: Boolean,
375
+ default: true
376
+ },
377
+ showEdit: {
378
+ type: Boolean,
379
+ default: true
380
+ },
381
+ showDelete: {
382
+ type: Boolean,
383
+ default: true
384
+ },
385
+
386
+ inlineEdit: {
387
+ type: Boolean,
388
+ default: false
389
+ },
390
+ updateUrl: {
391
+ type: String,
392
+ default: '' // API endpoint for updates
393
+ },
394
+
395
+ // Styling props - using Card defaults
396
+ tableClass: {
397
+ type: String,
398
+ default: ''
399
+ },
400
+ headerClass: {
401
+ type: String,
402
+ default: ''
403
+ },
404
+ // Container styling
405
+ bg: {
406
+ type: String,
407
+ default: 'bg-background'
408
+ },
409
+ border: {
410
+ type: String,
411
+ default: 'border-0'
412
+ },
413
+ rounded: {
414
+ type: String,
415
+ default: 'rounded-sm'
416
+ },
417
+ shadow: {
418
+ type: String,
419
+ default: 'shadow-none'
420
+ },
421
+
422
+ // Header styling
423
+ headerBg: {
424
+ type: String,
425
+ default: 'bg-surface-elevated'
426
+ },
427
+ headerBorder: {
428
+ type: String,
429
+ default: 'border-b border-input-border'
430
+ },
431
+ headerText: {
432
+ type: String,
433
+ default: 'text-textcolor'
434
+ },
435
+
436
+ // Row styling
437
+ rowBorder: {
438
+ type: String,
439
+ default: 'divide-y divide-input-border'
440
+ },
441
+ rowHover: {
442
+ type: String,
443
+ default: 'hover:bg-input-hover-border'
444
+ },
445
+ rowSelected: {
446
+ type: String,
447
+ default: 'bg-surface-elevated'
448
+ },
449
+ cellText: {
450
+ type: String,
451
+ default: 'text-textcolor'
452
+ },
453
+
454
+ // Input styling
455
+ inputBorder: {
456
+ type: String,
457
+ default: 'border-input-border focus:border-input-focus-border focus:ring-input-focus-ring'
458
+ },
459
+
460
+ // Button styling
461
+ buttonPrimary: {
462
+ type: String,
463
+ default: 'bg-primary hover:bg-secondary text-white'
464
+ },
465
+ buttonSecondary: {
466
+ type: String,
467
+ default: 'bg-surface hover:bg-input-hover-border text-textcolor border border-input-border'
468
+ },
469
+
470
+ // Pagination styling
471
+ paginationBg: {
472
+ type: String,
473
+ default: 'bg-background'
474
+ },
475
+ paginationBorder: {
476
+ type: String,
477
+ default: 'border-t border-input-border'
478
+ },
479
+ selectable: {
480
+ type: Boolean,
481
+ default: false
482
+ },
483
+ hidePagination: {
484
+ type: Boolean,
485
+ default: false
486
+ },
487
+
488
+ // Pagination
489
+ itemsPerPageOptions: {
490
+ type: Array,
491
+ default: () => [5, 10, 25, 50, 100]
492
+ },
493
+
494
+ // Item key
495
+ itemKey: {
496
+ type: String,
497
+ default: 'id'
498
+ }
499
+ })
500
+
501
+ const emit = defineEmits([
502
+ 'update:options',
503
+ 'update:cell',
504
+ 'inline-update', // New event for inline updates
505
+ 'row-click',
506
+ 'selection-change',
507
+ 'search',
508
+ 'view',
509
+ 'edit',
510
+ 'delete'
511
+ ])
512
+
513
+ // Internal state
514
+ const searchQuery = ref('')
515
+ const selectedItems = ref([])
516
+ const searchTimeout = ref(null)
517
+ const editMode = ref(false)
518
+ const showConfirmation = ref(false)
519
+ const updateLoading = ref(false)
520
+ const pendingUpdate = ref({
521
+ item: null,
522
+ key: '',
523
+ newValue: null,
524
+ oldValue: null,
525
+ header: '',
526
+ index: null
527
+ })
528
+
529
+ // Internal options that sync with parent
530
+ const internalOptions = ref({
531
+ page: 1,
532
+ itemsPerPage: 10,
533
+ sortBy: [],
534
+ sortDesc: [],
535
+ itemsLength: 0,
536
+ search: '',
537
+ ...props.serverSideOptions
538
+ })
539
+
540
+ // Methods
541
+ const handlePaginationChange = ({ page, itemsPerPage }) => {
542
+ internalOptions.value.page = page
543
+ internalOptions.value.itemsPerPage = itemsPerPage
544
+ emitOptionsUpdate()
545
+ }
546
+
547
+ // Computed properties
548
+ const totalColumns = computed(() => {
549
+ let count = props.headers.length
550
+ if (props.selectable) count++
551
+ if (props.$slots?.['item-actions']) count++
552
+ return count
553
+ })
554
+
555
+
556
+ const allSelected = computed(() => {
557
+ return props.items.length > 0 && selectedItems.value.length === props.items.length
558
+ })
559
+
560
+ const someSelected = computed(() => {
561
+ return selectedItems.value.length > 0 && !allSelected.value
562
+ })
563
+
564
+ // Methods
565
+ const getItemKey = (item, index) => {
566
+ return item[props.itemKey] ?? index
567
+ }
568
+
569
+ // const getNestedValue = (obj, path) => {
570
+ // return path.split('.').reduce((current, key) => current?.[key], obj)
571
+ // }
572
+ const getNestedValue = (obj, path) => {
573
+ if (!path) return '' // Safety check
574
+ return path.split('.').reduce((current, key) => current?.[key], obj)
575
+ }
576
+
577
+ const formatCellValue = (value, header) => {
578
+ if (header.format && typeof header.format === 'function') {
579
+ return header.format(value)
580
+ }
581
+ return value ?? ''
582
+ }
583
+
584
+ const toggleSort = (key) => {
585
+ // Find the header to check if it's sortable
586
+ const header = props.headers.find(h => h.key === key)
587
+
588
+ // Don't sort if explicitly marked as not sortable
589
+ if (header && header.sortable === false) {
590
+ console.log('Column is not sortable:', key)
591
+ return
592
+ }
593
+
594
+ const sortBy = [...internalOptions.value.sortBy]
595
+ const sortDesc = [...internalOptions.value.sortDesc]
596
+
597
+ const index = sortBy.findIndex(item => item === key)
598
+
599
+ if (index === -1) {
600
+ // Add new sort
601
+ sortBy.push(key)
602
+ sortDesc.push(false)
603
+ console.log('Adding new sort - ASC')
604
+ } else {
605
+ // Toggle existing sort
606
+ if (!sortDesc[index]) {
607
+ sortDesc[index] = true
608
+ console.log('Changing to DESC')
609
+ } else {
610
+ // Remove sort
611
+ sortBy.splice(index, 1)
612
+ sortDesc.splice(index, 1)
613
+ console.log('Removing sort')
614
+ }
615
+ }
616
+
617
+ internalOptions.value.sortBy = sortBy
618
+ internalOptions.value.sortDesc = sortDesc
619
+ internalOptions.value.page = 1 // Reset to first page
620
+
621
+
622
+ emitOptionsUpdate()
623
+ }
624
+
625
+ const isSorted = (key) => {
626
+ return internalOptions.value.sortBy.includes(key)
627
+ }
628
+
629
+ const getSortDirection = (key) => {
630
+ const index = internalOptions.value.sortBy.findIndex(item => item === key)
631
+ if (index === -1) return null
632
+ return internalOptions.value.sortDesc[index] ? 'desc' : 'asc'
633
+ }
634
+
635
+ const getColumnFont = (lang) => {
636
+ if (!lang) return ''
637
+
638
+ const fontMap = {
639
+ 'en': 'font-inter', // or whatever your English font is
640
+ 'dv': 'faruma', // Dhivehi font
641
+ // Add more languages as needed
642
+ }
643
+
644
+ return fontMap[lang] || ''
645
+ }
646
+
647
+ const debounceSearch = () => {
648
+ if (searchTimeout.value) {
649
+ clearTimeout(searchTimeout.value)
650
+ }
651
+
652
+ searchTimeout.value = setTimeout(() => {
653
+ internalOptions.value.search = searchQuery.value
654
+ internalOptions.value.page = 1
655
+ emitOptionsUpdate()
656
+ emit('search', searchQuery.value)
657
+ }, 500)
658
+ }
659
+
660
+ const handleRowClick = (item, index) => {
661
+ emit('row-click', item, index)
662
+ }
663
+
664
+ const toggleItemSelection = (item, index) => {
665
+ const key = getItemKey(item, index)
666
+ const currentIndex = selectedItems.value.indexOf(key)
667
+
668
+ if (currentIndex === -1) {
669
+ selectedItems.value.push(key)
670
+ } else {
671
+ selectedItems.value.splice(currentIndex, 1)
672
+ }
673
+
674
+ emit('selection-change', selectedItems.value, getSelectedItems())
675
+ }
676
+
677
+ const toggleSelectAll = () => {
678
+ if (allSelected.value) {
679
+ selectedItems.value = []
680
+ } else {
681
+ selectedItems.value = props.items.map((item, index) => getItemKey(item, index))
682
+ }
683
+
684
+ emit('selection-change', selectedItems.value, getSelectedItems())
685
+ }
686
+
687
+ const getSelectedItems = () => {
688
+ return props.items.filter((item, index) =>
689
+ selectedItems.value.includes(getItemKey(item, index))
690
+ )
691
+ }
692
+
693
+ const emitOptionsUpdate = () => {
694
+ emit('update:options', { ...internalOptions.value })
695
+ }
696
+
697
+ // Toggle edit mode
698
+ const toggleEditMode = () => {
699
+ editMode.value = !editMode.value
700
+ if (!editMode.value) {
701
+ // Cancel any pending changes
702
+ showConfirmation.value = false
703
+ }
704
+ }
705
+
706
+ // Handle cell update
707
+ const handleCellUpdate = (item, key, newValue, index) => {
708
+ const header = props.headers.find(h => h.key === key)
709
+ const oldValue = getNestedValue(item, key)
710
+
711
+ // Only show confirmation if value actually changed
712
+ if (oldValue === newValue) return
713
+
714
+ pendingUpdate.value = {
715
+ item,
716
+ key,
717
+ newValue,
718
+ oldValue,
719
+ header: header?.title || key,
720
+ index
721
+ }
722
+
723
+ showConfirmation.value = true
724
+ }
725
+
726
+ // Confirm and send update
727
+ const confirmUpdate = async () => {
728
+ updateLoading.value = true
729
+
730
+ try {
731
+ const { item, key, newValue, index } = pendingUpdate.value
732
+
733
+ // Prepare update data
734
+ const updateData = {
735
+ [props.itemKey]: item[props.itemKey],
736
+ [key]: newValue
737
+ }
738
+
739
+ // Emit event to parent
740
+ emit('inline-update', {
741
+ item,
742
+ key,
743
+ value: newValue,
744
+ index,
745
+ updateData
746
+ })
747
+
748
+ // If updateUrl is provided, send API request
749
+ if (props.updateUrl) {
750
+ const api = inject('api', null)
751
+ if (api) {
752
+ const url = `${props.updateUrl}/${item[props.itemKey]}`
753
+ await api.patch(url, updateData)
754
+ }
755
+ }
756
+
757
+ // Update local data
758
+ item[key] = newValue
759
+
760
+ // Reset confirmation
761
+ showConfirmation.value = false
762
+ updateLoading.value = false
763
+ pendingUpdate.value = {
764
+ item: null,
765
+ key: '',
766
+ newValue: null,
767
+ oldValue: null,
768
+ header: '',
769
+ index: null
770
+ }
771
+ } catch (error) {
772
+ console.error('Update failed:', error)
773
+ updateLoading.value = false
774
+ // You can add error handling here
775
+ }
776
+ }
777
+
778
+ // Cancel update
779
+ const cancelUpdate = () => {
780
+ showConfirmation.value = false
781
+ pendingUpdate.value = {
782
+ item: null,
783
+ key: '',
784
+ newValue: null,
785
+ oldValue: null,
786
+ header: '',
787
+ index: null
788
+ }
789
+ }
790
+
791
+ // Watchers
792
+ watch(() => props.serverSideOptions, (newOptions) => {
793
+ internalOptions.value = { ...internalOptions.value, ...newOptions }
794
+ }, { deep: true })
795
+
796
+ // Clear selection when items change
797
+ watch(() => props.items, () => {
798
+ selectedItems.value = []
799
+ }, { deep: true })
800
+
801
+ // Initialize
802
+ onMounted(() => {
803
+ if (props.serverSideOptions) {
804
+ internalOptions.value = { ...internalOptions.value, ...props.serverSideOptions }
805
+ }
806
+
807
+ })
808
+ </script>
809
+
810
+ <style scoped>
811
+ /* Only essential styles - everything else uses Tailwind classes */
812
+
813
+ @keyframes loading-bar {
814
+ 0% {
815
+ transform: translateX(-100%);
816
+ }
817
+ 100% {
818
+ transform: translateX(100%);
819
+ }
820
+ }
821
+
822
+
823
+ .animate-loading-bar {
824
+ animation: loading-bar 1.5s ease-in-out infinite;
825
+ }
826
+
827
+ /* Table collapse */
828
+ table {
829
+ /* border-collapse: separate; */
830
+ border-spacing: 0;
831
+ }
832
+
833
+ /* Checkbox indeterminate state */
834
+ input[type="checkbox"]:indeterminate::after {
835
+ content: '';
836
+ position: absolute;
837
+ width: 0.5rem;
838
+ height: 0.125rem;
839
+ background-color: currentColor;
840
+ border-radius: 0.125rem;
841
+ top: 50%;
842
+ left: 50%;
843
+ transform: translate(-50%, -50%);
844
+ }
845
+ </style>