@xy-planning-network/trees 0.4.0-rc-8 → 0.4.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/README.md +234 -41
- package/dist/trees.es.js +224 -217
- package/dist/trees.umd.js +4 -4
- package/package.json +4 -2
- package/src/lib-components/forms/BaseInput.vue +83 -0
- package/src/lib-components/forms/Checkbox.vue +46 -0
- package/src/lib-components/forms/DateRangePicker.vue +65 -0
- package/src/lib-components/forms/InputHelp.vue +24 -0
- package/src/lib-components/forms/InputLabel.vue +23 -0
- package/src/lib-components/forms/MultiCheckboxes.vue +55 -0
- package/src/lib-components/forms/Radio.vue +58 -0
- package/src/lib-components/forms/Select.vue +65 -0
- package/src/lib-components/forms/TextArea.vue +50 -0
- package/src/lib-components/forms/Toggle.vue +25 -0
- package/src/lib-components/forms/YesOrNoRadio.vue +70 -0
- package/src/lib-components/layout/DateFilter.vue +54 -0
- package/src/lib-components/layout/SidebarLayout.vue +239 -0
- package/src/lib-components/layout/StackedLayout.vue +172 -0
- package/src/lib-components/lists/Cards.vue +33 -0
- package/src/lib-components/lists/DetailList.vue +114 -0
- package/src/lib-components/lists/DownloadCell.vue +12 -0
- package/src/lib-components/lists/StaticTable.vue +83 -0
- package/src/lib-components/lists/Table.vue +291 -0
- package/src/lib-components/navigation/ActionsDropdown.vue +78 -0
- package/src/lib-components/navigation/Paginator.vue +115 -0
- package/src/lib-components/navigation/Steps.vue +83 -0
- package/src/lib-components/navigation/Tabs.vue +92 -0
- package/src/lib-components/overlays/ContentModal.vue +95 -0
- package/src/lib-components/overlays/Flash.vue +131 -0
- package/src/lib-components/overlays/Modal.vue +133 -0
- package/src/lib-components/overlays/Slideover.vue +87 -0
- package/src/lib-components/overlays/Spinner.vue +149 -0
- package/types/composables/nav.d.ts +2 -2
- package/types/composables/table.d.ts +2 -2
- package/types/composables/user.d.ts +1 -4
- package/types/lib-components/forms/Select.vue.d.ts +2 -2
|
@@ -0,0 +1,291 @@
|
|
|
1
|
+
<script setup lang="ts">
|
|
2
|
+
import { AxiosResponse } from "axios"
|
|
3
|
+
import {
|
|
4
|
+
ComponentPublicInstance,
|
|
5
|
+
computed,
|
|
6
|
+
getCurrentInstance,
|
|
7
|
+
ref,
|
|
8
|
+
watch,
|
|
9
|
+
} from "vue"
|
|
10
|
+
import DateRangePicker from "../forms/DateRangePicker.vue"
|
|
11
|
+
import Paginator from "../navigation/Paginator.vue"
|
|
12
|
+
import BaseAPI from "../../api/base"
|
|
13
|
+
import * as TableTypes from "@/composables/table"
|
|
14
|
+
|
|
15
|
+
const props = withDefaults(
|
|
16
|
+
defineProps<{
|
|
17
|
+
clickable?: boolean
|
|
18
|
+
loader?: boolean
|
|
19
|
+
tableData: TableTypes.Dynamic
|
|
20
|
+
}>(),
|
|
21
|
+
{
|
|
22
|
+
clickable: false,
|
|
23
|
+
loader: true,
|
|
24
|
+
}
|
|
25
|
+
)
|
|
26
|
+
|
|
27
|
+
const currentSort = ref(
|
|
28
|
+
props.tableData.defaultSort ? props.tableData.defaultSort : ""
|
|
29
|
+
)
|
|
30
|
+
const currentSortDirection = ref(
|
|
31
|
+
props.tableData.defaultSortDirection
|
|
32
|
+
? props.tableData.defaultSortDirection
|
|
33
|
+
: "desc"
|
|
34
|
+
)
|
|
35
|
+
const dateRange = ref({
|
|
36
|
+
minDate: 0,
|
|
37
|
+
maxDate: 0,
|
|
38
|
+
})
|
|
39
|
+
const items = ref<any[]>([])
|
|
40
|
+
const pagination = ref({
|
|
41
|
+
page: 1,
|
|
42
|
+
perPage: 10,
|
|
43
|
+
totalItems: 0,
|
|
44
|
+
totalPages: 0,
|
|
45
|
+
})
|
|
46
|
+
const query = ref("")
|
|
47
|
+
const cellValue = (
|
|
48
|
+
item: Record<string, any>,
|
|
49
|
+
col: TableTypes.Column
|
|
50
|
+
): string => {
|
|
51
|
+
if (col.key) {
|
|
52
|
+
// NOTE(dlk): supports dot notation for nested keys
|
|
53
|
+
return col.key.split(".").reduce((o, i) => o[i], item as any)
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
if (col.presenter) {
|
|
57
|
+
// TODO: discuss this pattern. Current usage can be replaced with modules.
|
|
58
|
+
// https://v3.vuejs.org/api/composition-api.html#getcurrentinstance
|
|
59
|
+
const internalInstance = getCurrentInstance()
|
|
60
|
+
return col.presenter(
|
|
61
|
+
item,
|
|
62
|
+
internalInstance?.proxy as ComponentPublicInstance
|
|
63
|
+
)
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
return ""
|
|
67
|
+
}
|
|
68
|
+
const dateRangeChanged = (newDateRange: {
|
|
69
|
+
minDate: number
|
|
70
|
+
maxDate: number
|
|
71
|
+
}): void => {
|
|
72
|
+
pagination.value.page = 1
|
|
73
|
+
dateRange.value = newDateRange
|
|
74
|
+
loadAndRender()
|
|
75
|
+
}
|
|
76
|
+
const handleSort = (selectedSort: string): void => {
|
|
77
|
+
if (currentSort.value == selectedSort) {
|
|
78
|
+
currentSortDirection.value =
|
|
79
|
+
currentSortDirection.value === "desc" ? "asc" : "desc"
|
|
80
|
+
} else {
|
|
81
|
+
currentSort.value = selectedSort
|
|
82
|
+
currentSortDirection.value = "desc"
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
loadAndRender()
|
|
86
|
+
}
|
|
87
|
+
const loadAndRender = (): void => {
|
|
88
|
+
const params = {
|
|
89
|
+
minDate: dateRange.value.minDate,
|
|
90
|
+
maxDate: dateRange.value.maxDate,
|
|
91
|
+
page: pagination.value.page,
|
|
92
|
+
perPage: pagination.value.perPage,
|
|
93
|
+
sort: currentSort.value,
|
|
94
|
+
sortDir: currentSortDirection.value,
|
|
95
|
+
q: query.value,
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
BaseAPI.get(props.tableData.url, { skipLoader: !props.loader }, params).then(
|
|
99
|
+
(success: AxiosResponse) => {
|
|
100
|
+
pagination.value = {
|
|
101
|
+
page: success.data.page,
|
|
102
|
+
perPage: success.data.perPage,
|
|
103
|
+
totalItems: success.data.totalItems,
|
|
104
|
+
totalPages: success.data.totalPages,
|
|
105
|
+
}
|
|
106
|
+
items.value = success.data.items
|
|
107
|
+
},
|
|
108
|
+
() => {
|
|
109
|
+
window.VueBus.emit(
|
|
110
|
+
"Flash-show-generic-error",
|
|
111
|
+
"membership@xyplanningnetwork.com"
|
|
112
|
+
)
|
|
113
|
+
}
|
|
114
|
+
)
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
const reloadTable = (): void => {
|
|
118
|
+
pagination.value.page = 1
|
|
119
|
+
loadAndRender()
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
const hasContent = computed((): boolean => {
|
|
123
|
+
return items.value.length ? true : false
|
|
124
|
+
})
|
|
125
|
+
|
|
126
|
+
watch(
|
|
127
|
+
() => props.tableData.refreshTrigger,
|
|
128
|
+
() => {
|
|
129
|
+
// This lets parent components trigger a refresh of the current page depending on external actions.
|
|
130
|
+
loadAndRender()
|
|
131
|
+
}
|
|
132
|
+
)
|
|
133
|
+
|
|
134
|
+
watch(
|
|
135
|
+
() => props.tableData.reloadTrigger,
|
|
136
|
+
() => {
|
|
137
|
+
// This lets parent components trigger a reload of page 1 depending on external actions.
|
|
138
|
+
reloadTable()
|
|
139
|
+
}
|
|
140
|
+
)
|
|
141
|
+
|
|
142
|
+
// onCreated
|
|
143
|
+
loadAndRender()
|
|
144
|
+
</script>
|
|
145
|
+
<template>
|
|
146
|
+
<div>
|
|
147
|
+
<div
|
|
148
|
+
class="flex flex-col mb-4 space-y-4 lg:space-y-0 lg:flex-row lg:justify-between"
|
|
149
|
+
>
|
|
150
|
+
<div class="w-full max-w-lg lg:max-w-xs" v-if="tableData.search">
|
|
151
|
+
<label for="search" class="sr-only">Search</label>
|
|
152
|
+
<div class="relative">
|
|
153
|
+
<div
|
|
154
|
+
class="absolute inset-y-0 left-0 flex items-center pl-3 pointer-events-none"
|
|
155
|
+
>
|
|
156
|
+
<svg
|
|
157
|
+
class="w-5 h-5 text-gray-400"
|
|
158
|
+
fill="currentColor"
|
|
159
|
+
viewBox="0 0 20 20"
|
|
160
|
+
>
|
|
161
|
+
<path
|
|
162
|
+
fill-rule="evenodd"
|
|
163
|
+
d="M8 4a4 4 0 100 8 4 4 0 000-8zM2 8a6 6 0 1110.89 3.476l4.817 4.817a1 1 0 01-1.414 1.414l-4.816-4.816A6 6 0 012 8z"
|
|
164
|
+
clip-rule="evenodd"
|
|
165
|
+
/>
|
|
166
|
+
</svg>
|
|
167
|
+
</div>
|
|
168
|
+
<input
|
|
169
|
+
class="pl-10"
|
|
170
|
+
type="search"
|
|
171
|
+
v-model.trim="query"
|
|
172
|
+
placeholder="Search"
|
|
173
|
+
@change="reloadTable()"
|
|
174
|
+
/>
|
|
175
|
+
</div>
|
|
176
|
+
</div>
|
|
177
|
+
<div class="w-full max-w-lg lg:max-w-xs" v-if="tableData.dateSearch">
|
|
178
|
+
<DateRangePicker
|
|
179
|
+
v-model="dateRange"
|
|
180
|
+
@update:modelValue="dateRangeChanged"
|
|
181
|
+
/>
|
|
182
|
+
</div>
|
|
183
|
+
</div>
|
|
184
|
+
|
|
185
|
+
<div
|
|
186
|
+
class="relative z-0 min-w-full align-middle border-b border-gray-200 shadow sm:rounded-lg overflow-x-auto"
|
|
187
|
+
>
|
|
188
|
+
<table class="min-w-full">
|
|
189
|
+
<thead>
|
|
190
|
+
<tr>
|
|
191
|
+
<th
|
|
192
|
+
class="px-6 py-3 text-xs font-medium tracking-wider text-left text-gray-900 uppercase border-b border-gray-200 bg-gray-50 leading-4"
|
|
193
|
+
v-for="(col, idx) in tableData.columns"
|
|
194
|
+
:key="idx"
|
|
195
|
+
>
|
|
196
|
+
<span v-if="!!col.display.length">{{ col.display }}</span>
|
|
197
|
+
<span
|
|
198
|
+
class="cursor-pointer"
|
|
199
|
+
@click.prevent="handleSort(col.sort as string)"
|
|
200
|
+
v-if="col.sort"
|
|
201
|
+
>
|
|
202
|
+
<svg
|
|
203
|
+
xmlns="http://www.w3.org/2000/svg"
|
|
204
|
+
fill="none"
|
|
205
|
+
viewBox="0 0 24 24"
|
|
206
|
+
stroke="currentColor"
|
|
207
|
+
class="h-5 inline"
|
|
208
|
+
v-if="currentSort !== col.sort"
|
|
209
|
+
>
|
|
210
|
+
<path
|
|
211
|
+
stroke-linecap="round"
|
|
212
|
+
stroke-linejoin="round"
|
|
213
|
+
stroke-width="2"
|
|
214
|
+
d="M8 9l4-4 4 4m0 6l-4 4-4-4"
|
|
215
|
+
/>
|
|
216
|
+
</svg>
|
|
217
|
+
<svg
|
|
218
|
+
xmlns="http://www.w3.org/2000/svg"
|
|
219
|
+
viewBox="0 0 20 20"
|
|
220
|
+
fill="currentColor"
|
|
221
|
+
class="h-5 inline"
|
|
222
|
+
v-else-if="currentSortDirection == 'desc'"
|
|
223
|
+
>
|
|
224
|
+
<path
|
|
225
|
+
fill-rule="evenodd"
|
|
226
|
+
d="M14.707 12.707a1 1 0 01-1.414 0L10 9.414l-3.293 3.293a1 1 0 01-1.414-1.414l4-4a1 1 0 011.414 0l4 4a1 1 0 010 1.414z"
|
|
227
|
+
clip-rule="evenodd"
|
|
228
|
+
/>
|
|
229
|
+
</svg>
|
|
230
|
+
<svg
|
|
231
|
+
xmlns="http://www.w3.org/2000/svg"
|
|
232
|
+
viewBox="0 0 20 20"
|
|
233
|
+
fill="currentColor"
|
|
234
|
+
class="h-5 inline"
|
|
235
|
+
v-else
|
|
236
|
+
>
|
|
237
|
+
<path
|
|
238
|
+
fill-rule="evenodd"
|
|
239
|
+
d="M5.293 7.293a1 1 0 011.414 0L10 10.586l3.293-3.293a1 1 0 111.414 1.414l-4 4a1 1 0 01-1.414 0l-4-4a1 1 0 010-1.414z"
|
|
240
|
+
clip-rule="evenodd"
|
|
241
|
+
/>
|
|
242
|
+
</svg>
|
|
243
|
+
</span>
|
|
244
|
+
</th>
|
|
245
|
+
</tr>
|
|
246
|
+
</thead>
|
|
247
|
+
|
|
248
|
+
<tbody class="bg-white">
|
|
249
|
+
<tr
|
|
250
|
+
v-for="(item, rowIdx) in items"
|
|
251
|
+
:key="item.id ? item.id : rowIdx"
|
|
252
|
+
@click="$emit('handleClick', item)"
|
|
253
|
+
:class="{ 'cursor-pointer': clickable }"
|
|
254
|
+
>
|
|
255
|
+
<td
|
|
256
|
+
class="px-6 py-4 text-sm text-gray-700 whitespace-nowrap border-b border-gray-200 leading-5"
|
|
257
|
+
v-for="(col, colIdx) in tableData.columns"
|
|
258
|
+
:key="rowIdx + '-' + colIdx"
|
|
259
|
+
:class="col.class"
|
|
260
|
+
>
|
|
261
|
+
<component
|
|
262
|
+
:is="col.component"
|
|
263
|
+
v-if="col.component"
|
|
264
|
+
:props-data="item"
|
|
265
|
+
:current-user="tableData.currentUser"
|
|
266
|
+
:attribute="col.key"
|
|
267
|
+
:items="col.items"
|
|
268
|
+
></component>
|
|
269
|
+
<div v-else v-text="cellValue(item, col)"></div>
|
|
270
|
+
</td>
|
|
271
|
+
</tr>
|
|
272
|
+
|
|
273
|
+
<tr v-if="!hasContent">
|
|
274
|
+
<td
|
|
275
|
+
:colspan="tableData.columns.length"
|
|
276
|
+
class="px-6 py-4 text-sm text-gray-700 whitespace-nowrap border-b border-gray-200 leading-5"
|
|
277
|
+
>
|
|
278
|
+
No items were found!
|
|
279
|
+
</td>
|
|
280
|
+
</tr>
|
|
281
|
+
</tbody>
|
|
282
|
+
</table>
|
|
283
|
+
</div>
|
|
284
|
+
|
|
285
|
+
<Paginator
|
|
286
|
+
v-model="pagination"
|
|
287
|
+
@update:modelValue="loadAndRender()"
|
|
288
|
+
v-if="hasContent"
|
|
289
|
+
/>
|
|
290
|
+
</div>
|
|
291
|
+
</template>
|
|
@@ -0,0 +1,78 @@
|
|
|
1
|
+
<script setup lang="ts">
|
|
2
|
+
import { Menu, MenuButton, MenuItem, MenuItems } from "@headlessui/vue"
|
|
3
|
+
import { DotsVerticalIcon } from "@heroicons/vue/solid"
|
|
4
|
+
import { onMounted, ref } from "vue"
|
|
5
|
+
import * as TableTypes from "@/composables/table"
|
|
6
|
+
import User from "@/composables/user"
|
|
7
|
+
|
|
8
|
+
const props = defineProps<{
|
|
9
|
+
currentUser: User
|
|
10
|
+
items: TableTypes.MenuItem[]
|
|
11
|
+
propsData: any
|
|
12
|
+
}>()
|
|
13
|
+
|
|
14
|
+
const hasActionItems = ref(false)
|
|
15
|
+
|
|
16
|
+
const emitEvent = (event: string): void => {
|
|
17
|
+
window.VueBus.emit(event, props.propsData)
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
const show = (item: TableTypes.MenuItem): boolean => {
|
|
21
|
+
if (!item.show) return true
|
|
22
|
+
return item.show(props.propsData, props.currentUser)
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
onMounted(() => {
|
|
26
|
+
for (let item of props.items) {
|
|
27
|
+
if (!item.show) {
|
|
28
|
+
hasActionItems.value = true
|
|
29
|
+
return
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
const showActionItem = item.show(props.propsData, props.currentUser)
|
|
33
|
+
if (showActionItem) {
|
|
34
|
+
hasActionItems.value = true
|
|
35
|
+
return
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
})
|
|
39
|
+
</script>
|
|
40
|
+
<template>
|
|
41
|
+
<Menu as="div" class="relative flex justify-end items-center">
|
|
42
|
+
<MenuButton
|
|
43
|
+
class="w-8 h-8 bg-white inline-flex items-center justify-center text-gray-700 rounded-full hover:text-gray-900 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-blue-500 disabled:opacity-50 disabled:cursor-not-allowed"
|
|
44
|
+
:disabled="!hasActionItems"
|
|
45
|
+
>
|
|
46
|
+
<span class="sr-only">Open options</span>
|
|
47
|
+
<DotsVerticalIcon class="w-5 h-5" aria-hidden="true" />
|
|
48
|
+
</MenuButton>
|
|
49
|
+
<transition
|
|
50
|
+
enter-active-class="transition ease-out duration-100"
|
|
51
|
+
enter-from-class="transform opacity-0 scale-95"
|
|
52
|
+
enter-to-class="transform opacity-100 scale-100"
|
|
53
|
+
leave-active-class="transition ease-in duration-75"
|
|
54
|
+
leave-from-class="transform opacity-100 scale-100"
|
|
55
|
+
leave-to-class="transform opacity-0 scale-95"
|
|
56
|
+
>
|
|
57
|
+
<MenuItems
|
|
58
|
+
class="z-10 mx-3 origin-top-right absolute right-7 top-0 w-48 mt-1 rounded-md shadow-lg bg-white ring-1 ring-black ring-opacity-5 divide-y divide-gray-200 focus:outline-none"
|
|
59
|
+
>
|
|
60
|
+
<div class="py-1">
|
|
61
|
+
<template v-for="(item, idx) in items" :key="idx">
|
|
62
|
+
<MenuItem as="div" v-slot="{ active }" v-if="show(item)">
|
|
63
|
+
<button
|
|
64
|
+
type="submit"
|
|
65
|
+
:class="[
|
|
66
|
+
active ? 'bg-gray-100 text-gray-900' : 'text-gray-700',
|
|
67
|
+
'block w-full text-left px-4 py-2 text-sm font-semibold',
|
|
68
|
+
]"
|
|
69
|
+
v-text="item.label"
|
|
70
|
+
@click="emitEvent(item.event)"
|
|
71
|
+
></button>
|
|
72
|
+
</MenuItem>
|
|
73
|
+
</template>
|
|
74
|
+
</div>
|
|
75
|
+
</MenuItems>
|
|
76
|
+
</transition>
|
|
77
|
+
</Menu>
|
|
78
|
+
</template>
|
|
@@ -0,0 +1,115 @@
|
|
|
1
|
+
<script setup lang="ts">
|
|
2
|
+
import { Pagination } from "@/composables/nav"
|
|
3
|
+
import { computed, ref } from "vue"
|
|
4
|
+
|
|
5
|
+
const props = defineProps<{
|
|
6
|
+
modelValue: Pagination
|
|
7
|
+
}>()
|
|
8
|
+
|
|
9
|
+
const emit = defineEmits<{
|
|
10
|
+
(e: "update:modelValue", pagination: Pagination): void
|
|
11
|
+
}>()
|
|
12
|
+
|
|
13
|
+
const pagination = ref<Pagination>(props.modelValue)
|
|
14
|
+
|
|
15
|
+
const updateModelValue = () => {
|
|
16
|
+
emit("update:modelValue", pagination.value)
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
const changePage = (page: number): void => {
|
|
20
|
+
pagination.value.page = page
|
|
21
|
+
updateModelValue()
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
const pageShortcuts = computed((): number[] => {
|
|
25
|
+
const shortcuts: number[] = []
|
|
26
|
+
|
|
27
|
+
// If total pages is less than or equal to 4, just return 1, 2, 3, 4
|
|
28
|
+
if (pagination.value.totalPages <= 4) {
|
|
29
|
+
for (let i = 0; i < pagination.value.totalPages; i++) {
|
|
30
|
+
shortcuts.push(i + 1)
|
|
31
|
+
}
|
|
32
|
+
return shortcuts
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
// If there are more than 3 pages left, show these
|
|
36
|
+
// e.g. [4, 5, 6, 7] when there are 8 total pages and the current page is 4
|
|
37
|
+
const pagesLeft: number = pagination.value.totalPages - pagination.value.page
|
|
38
|
+
if (pagesLeft >= 3) {
|
|
39
|
+
for (let i = 0; i < 4; i++) {
|
|
40
|
+
shortcuts.push(pagination.value.page + i)
|
|
41
|
+
}
|
|
42
|
+
return shortcuts
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
// If there are less than 3 pages left, count backwards from the last page
|
|
46
|
+
// e.g. [5, 6, 7, 8] when on page 5, 6, 7, and 8 and there are 8 total pages
|
|
47
|
+
for (let i = 0; i < 4; i++) {
|
|
48
|
+
shortcuts.unshift(pagination.value.totalPages - i)
|
|
49
|
+
}
|
|
50
|
+
return shortcuts
|
|
51
|
+
})
|
|
52
|
+
</script>
|
|
53
|
+
<template>
|
|
54
|
+
<div class="px-4 flex items-center justify-between sm:px-0">
|
|
55
|
+
<div class="w-0 flex-1 flex">
|
|
56
|
+
<a
|
|
57
|
+
href="#"
|
|
58
|
+
class="-mt-px border-t-2 border-transparent pt-4 pr-1 inline-flex items-center text-sm leading-5 font-medium focus:outline-none focus:text-gray-700 focus:border-gray-400"
|
|
59
|
+
@click.prevent="changePage(pagination.page - 1)"
|
|
60
|
+
:class="
|
|
61
|
+
pagination.page == 1
|
|
62
|
+
? 'text-gray-500 cursor-not-allowed pointer-events-none'
|
|
63
|
+
: 'text-gray-700 hover:text-gray-900 hover:border-gray-300'
|
|
64
|
+
"
|
|
65
|
+
>
|
|
66
|
+
<svg class="mr-3 h-5 w-5" fill="currentColor" viewBox="0 0 20 20">
|
|
67
|
+
<path
|
|
68
|
+
fill-rule="evenodd"
|
|
69
|
+
d="M7.707 14.707a1 1 0 01-1.414 0l-4-4a1 1 0 010-1.414l4-4a1 1 0 011.414 1.414L5.414 9H17a1 1 0 110 2H5.414l2.293 2.293a1 1 0 010 1.414z"
|
|
70
|
+
clip-rule="evenodd"
|
|
71
|
+
/>
|
|
72
|
+
</svg>
|
|
73
|
+
Previous
|
|
74
|
+
</a>
|
|
75
|
+
</div>
|
|
76
|
+
|
|
77
|
+
<div class="hidden md:flex">
|
|
78
|
+
<a
|
|
79
|
+
href="#"
|
|
80
|
+
class="-mt-px border-t-2 pt-4 px-4 inline-flex items-center text-sm leading-5 font-medium"
|
|
81
|
+
v-for="i in pageShortcuts"
|
|
82
|
+
:key="i"
|
|
83
|
+
v-text="i"
|
|
84
|
+
:class="
|
|
85
|
+
pagination.page === i
|
|
86
|
+
? 'border-blue-500 text-blue-600 focus:outline-none focus:text-blue-800 focus:border-blue-700'
|
|
87
|
+
: 'border-transparent text-gray-700 hover:text-gray-900 hover:border-gray-300 focus:outline-none focus:text-gray-700 focus:border-gray-400'
|
|
88
|
+
"
|
|
89
|
+
@click.prevent="changePage(i)"
|
|
90
|
+
></a>
|
|
91
|
+
</div>
|
|
92
|
+
|
|
93
|
+
<div class="w-0 flex-1 flex justify-end">
|
|
94
|
+
<a
|
|
95
|
+
href="#"
|
|
96
|
+
class="-mt-px border-t-2 border-transparent pt-4 pl-1 inline-flex items-center text-sm leading-5 font-medium focus:outline-none focus:text-gray-700 focus:border-gray-400"
|
|
97
|
+
@click.prevent="changePage(pagination.page + 1)"
|
|
98
|
+
:class="
|
|
99
|
+
pagination.page >= pagination.totalPages
|
|
100
|
+
? 'text-gray-500 cursor-not-allowed pointer-events-none'
|
|
101
|
+
: 'text-gray-700 hover:text-gray-900 hover:border-gray-300'
|
|
102
|
+
"
|
|
103
|
+
>
|
|
104
|
+
Next
|
|
105
|
+
<svg class="ml-3 h-5 w-5" fill="currentColor" viewBox="0 0 20 20">
|
|
106
|
+
<path
|
|
107
|
+
fill-rule="evenodd"
|
|
108
|
+
d="M12.293 5.293a1 1 0 011.414 0l4 4a1 1 0 010 1.414l-4 4a1 1 0 01-1.414-1.414L14.586 11H3a1 1 0 110-2h11.586l-2.293-2.293a1 1 0 010-1.414z"
|
|
109
|
+
clip-rule="evenodd"
|
|
110
|
+
/>
|
|
111
|
+
</svg>
|
|
112
|
+
</a>
|
|
113
|
+
</div>
|
|
114
|
+
</div>
|
|
115
|
+
</template>
|
|
@@ -0,0 +1,83 @@
|
|
|
1
|
+
<script setup lang="ts">
|
|
2
|
+
// TODO: think about whether there is value in updating and emiting step with next/previous
|
|
3
|
+
// TODO: add to docs
|
|
4
|
+
|
|
5
|
+
withDefaults(
|
|
6
|
+
defineProps<{
|
|
7
|
+
hideActions?: boolean
|
|
8
|
+
hidePrevious?: boolean
|
|
9
|
+
nextText?: string
|
|
10
|
+
previousText?: string
|
|
11
|
+
step: number
|
|
12
|
+
total: number
|
|
13
|
+
}>(),
|
|
14
|
+
{
|
|
15
|
+
hideActions: false,
|
|
16
|
+
hidePrevious: false,
|
|
17
|
+
nextText: "Next",
|
|
18
|
+
previousText: "Back",
|
|
19
|
+
}
|
|
20
|
+
)
|
|
21
|
+
|
|
22
|
+
const emit = defineEmits(["next", "previous"])
|
|
23
|
+
|
|
24
|
+
const next = (): void => {
|
|
25
|
+
emit("next")
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
const previous = (): void => {
|
|
29
|
+
emit("previous")
|
|
30
|
+
}
|
|
31
|
+
</script>
|
|
32
|
+
<template>
|
|
33
|
+
<div>
|
|
34
|
+
<nav class="flex items-center justify-center space-x-8">
|
|
35
|
+
<p class="font-medium">Step {{ step }} of {{ total }}</p>
|
|
36
|
+
<ul class="flex items-center space-x-5">
|
|
37
|
+
<li v-for="index in total" :key="index">
|
|
38
|
+
<span
|
|
39
|
+
class="block w-2.5 h-2.5 bg-xy-green rounded-full hover:bg-green-900 focus:bg-green-900"
|
|
40
|
+
v-if="step > index"
|
|
41
|
+
></span>
|
|
42
|
+
|
|
43
|
+
<div
|
|
44
|
+
class="relative flex items-center justify-center"
|
|
45
|
+
v-else-if="step === index"
|
|
46
|
+
>
|
|
47
|
+
<span class="absolute w-5 h-5 p-px flex">
|
|
48
|
+
<span class="w-full h-full rounded-full bg-green-100"></span>
|
|
49
|
+
</span>
|
|
50
|
+
<span
|
|
51
|
+
class="relative block w-2.5 h-2.5 bg-xy-green rounded-full"
|
|
52
|
+
></span>
|
|
53
|
+
</div>
|
|
54
|
+
|
|
55
|
+
<span
|
|
56
|
+
href="#"
|
|
57
|
+
class="block w-2.5 h-2.5 bg-gray-200 rounded-full hover:bg-gray-400 focus:bg-gray-400"
|
|
58
|
+
v-else
|
|
59
|
+
></span>
|
|
60
|
+
</li>
|
|
61
|
+
</ul>
|
|
62
|
+
</nav>
|
|
63
|
+
|
|
64
|
+
<div class="flex flex-shrink-0" v-if="!hideActions">
|
|
65
|
+
<span class="inline-flex rounded-md shadow-sm" v-if="!hidePrevious">
|
|
66
|
+
<button
|
|
67
|
+
type="button"
|
|
68
|
+
class="xy-btn-white"
|
|
69
|
+
@click="previous"
|
|
70
|
+
v-text="previousText"
|
|
71
|
+
></button>
|
|
72
|
+
</span>
|
|
73
|
+
<span class="ml-3 inline-flex rounded-md shadow-sm">
|
|
74
|
+
<button
|
|
75
|
+
type="button"
|
|
76
|
+
class="xy-btn"
|
|
77
|
+
@click="next"
|
|
78
|
+
v-text="nextText"
|
|
79
|
+
></button>
|
|
80
|
+
</span>
|
|
81
|
+
</div>
|
|
82
|
+
</div>
|
|
83
|
+
</template>
|
|
@@ -0,0 +1,92 @@
|
|
|
1
|
+
<script setup lang="ts">
|
|
2
|
+
import { computed } from "vue"
|
|
3
|
+
|
|
4
|
+
const props = withDefaults(
|
|
5
|
+
defineProps<{
|
|
6
|
+
modelValue: string
|
|
7
|
+
pillDesign?: boolean
|
|
8
|
+
tabs: Array<{
|
|
9
|
+
label: string
|
|
10
|
+
value: string
|
|
11
|
+
}>
|
|
12
|
+
}>(),
|
|
13
|
+
{
|
|
14
|
+
pillDesign: false,
|
|
15
|
+
}
|
|
16
|
+
)
|
|
17
|
+
|
|
18
|
+
const emit = defineEmits<{
|
|
19
|
+
(e: "update:modelValue", val: string): void
|
|
20
|
+
}>()
|
|
21
|
+
|
|
22
|
+
const updateModelValue = (modelValue: string): void => {
|
|
23
|
+
emit("update:modelValue", modelValue)
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
const classes = (currentTab: string, pastFirstTab: boolean): string => {
|
|
27
|
+
let c = ""
|
|
28
|
+
|
|
29
|
+
if (props.pillDesign) {
|
|
30
|
+
c =
|
|
31
|
+
"px-12 py-2 font-semibold text-md leading-5 rounded-t-md focus:outline-none "
|
|
32
|
+
|
|
33
|
+
if (props.modelValue === currentTab) {
|
|
34
|
+
c = c + "focus:bg-white text-gray-700 bg-white border-b-2 border-blue-500"
|
|
35
|
+
} else {
|
|
36
|
+
c =
|
|
37
|
+
c +
|
|
38
|
+
"text-gray-700 hover:text-gray-900 focus:text-gray-900 focus:bg-gray-100 border border-gray-200"
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
return c
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
c =
|
|
45
|
+
"px-1 py-4 text-sm font-semibold border-b-2 whitespace-nowrap leading-5 focus:outline-none "
|
|
46
|
+
if (props.modelValue === currentTab) {
|
|
47
|
+
c =
|
|
48
|
+
c +
|
|
49
|
+
"border-blue-500 text-xy-blue focus:text-blue-800 focus:border-blue-700"
|
|
50
|
+
} else {
|
|
51
|
+
c =
|
|
52
|
+
c +
|
|
53
|
+
"border-transparent text-gray-700 hover:text-gray-900 hover:border-gray-300 focus:text-gray-900 focus:border-gray-300"
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
if (pastFirstTab) c = c + " ml-8"
|
|
57
|
+
|
|
58
|
+
return c
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
const notPillDesign = computed((): boolean => {
|
|
62
|
+
return !props.pillDesign
|
|
63
|
+
})
|
|
64
|
+
</script>
|
|
65
|
+
<template>
|
|
66
|
+
<div>
|
|
67
|
+
<div class="sm:hidden" :class="{ 'mb-4': pillDesign }">
|
|
68
|
+
<label for="tabs" class="sr-only">Select a tab</label>
|
|
69
|
+
<Select
|
|
70
|
+
name="tabs"
|
|
71
|
+
:modelValue="modelValue"
|
|
72
|
+
@update:modelValue="updateModelValue($event)"
|
|
73
|
+
:options="tabs"
|
|
74
|
+
/>
|
|
75
|
+
</div>
|
|
76
|
+
<div class="hidden sm:block">
|
|
77
|
+
<div :class="{ 'border-b border-gray-200': notPillDesign }">
|
|
78
|
+
<nav class="flex" :class="[pillDesign ? 'ml-8' : '-mb-px']">
|
|
79
|
+
<a
|
|
80
|
+
href="#"
|
|
81
|
+
:class="classes(tab.value, idx > 0)"
|
|
82
|
+
v-for="(tab, idx) in tabs"
|
|
83
|
+
:key="idx"
|
|
84
|
+
v-text="tab.label"
|
|
85
|
+
@click.prevent="updateModelValue(tab.value)"
|
|
86
|
+
>
|
|
87
|
+
</a>
|
|
88
|
+
</nav>
|
|
89
|
+
</div>
|
|
90
|
+
</div>
|
|
91
|
+
</div>
|
|
92
|
+
</template>
|